Skip to main content

jk_core/
lib.rs

1//! Shared log records used by `jk` integration and TUI crates.
2//!
3//! `jk-core` keeps the boundary between opaque `jj` rendering and semantic log state explicit. The
4//! rendered log body is still owned by `jj`; `jk` only keeps enough structured metadata to move by
5//! change, preserve selection across refresh, and insert inline details at the right rendered line.
6
7/// A rendered `jj` log view plus semantic records for navigation.
8///
9/// The `rendered` body is opaque terminal text produced by `jj`. Each [`LogEntry`] in `entries`
10/// must correspond to one commit row in that rendered text through [`LogEntry::rendered_line`].
11#[derive(Clone, Debug, Default, Eq, PartialEq)]
12pub struct LogSnapshot {
13    title: String,
14    rendered: String,
15    entries: Vec<LogEntry>,
16}
17
18impl LogSnapshot {
19    /// Creates a log snapshot from rendered terminal text and semantic entries.
20    ///
21    /// The caller is responsible for ensuring each entry's rendered line points at the matching
22    /// commit row in `rendered`.
23    #[must_use]
24    pub fn new(rendered: impl Into<String>, entries: Vec<LogEntry>) -> Self {
25        Self {
26            title: String::new(),
27            rendered: rendered.into(),
28            entries,
29        }
30    }
31
32    /// Sets the command context shown in the title bar.
33    #[must_use]
34    pub fn with_title(mut self, title: impl Into<String>) -> Self {
35        self.title = title.into();
36        self
37    }
38
39    /// Returns the human-readable command context for the current view.
40    #[must_use]
41    pub fn title(&self) -> &str {
42        &self.title
43    }
44
45    /// Returns the opaque output rendered by `jj`.
46    #[must_use]
47    pub fn rendered(&self) -> &str {
48        &self.rendered
49    }
50
51    /// Returns semantic records aligned to the rendered log body.
52    #[must_use]
53    pub fn entries(&self) -> &[LogEntry] {
54        &self.entries
55    }
56
57    /// Consumes the snapshot into its owned title, rendered body, and entries.
58    #[must_use]
59    pub fn into_parts(self) -> (String, String, Vec<LogEntry>) {
60        (self.title, self.rendered, self.entries)
61    }
62}
63
64/// A rendered `jj diff` view for one change.
65#[derive(Clone, Debug, Default, Eq, PartialEq)]
66pub struct DiffSnapshot {
67    title: String,
68    change_id: String,
69    rendered: String,
70    file_stats: Vec<DiffFileStat>,
71}
72
73impl DiffSnapshot {
74    /// Creates a diff snapshot from the target change and rendered terminal text.
75    #[must_use]
76    pub fn new(change_id: impl Into<String>, rendered: impl Into<String>) -> Self {
77        Self {
78            title: String::new(),
79            change_id: change_id.into(),
80            rendered: rendered.into(),
81            file_stats: Vec::new(),
82        }
83    }
84
85    /// Sets the command context shown in the title bar.
86    #[must_use]
87    pub fn with_title(mut self, title: impl Into<String>) -> Self {
88        self.title = title.into();
89        self
90    }
91
92    /// Sets per-file line counts for the rendered diff.
93    #[must_use]
94    pub fn with_file_stats(mut self, file_stats: Vec<DiffFileStat>) -> Self {
95        self.file_stats = file_stats;
96        self
97    }
98
99    /// Returns the human-readable command context for the current view.
100    #[must_use]
101    pub fn title(&self) -> &str {
102        &self.title
103    }
104
105    /// Returns the `jj` change identifier being inspected.
106    #[must_use]
107    pub fn change_id(&self) -> &str {
108        &self.change_id
109    }
110
111    /// Returns the opaque output rendered by `jj`.
112    #[must_use]
113    pub fn rendered(&self) -> &str {
114        &self.rendered
115    }
116
117    /// Returns per-file line counts for changed files.
118    #[must_use]
119    pub fn file_stats(&self) -> &[DiffFileStat] {
120        &self.file_stats
121    }
122
123    /// Consumes the snapshot into its owned title, target change, and rendered body.
124    #[must_use]
125    pub fn into_parts(self) -> (String, String, String, Vec<DiffFileStat>) {
126        (self.title, self.change_id, self.rendered, self.file_stats)
127    }
128}
129
130/// Per-file line counts for a rendered diff.
131#[derive(Clone, Debug, Default, Eq, PartialEq)]
132pub struct DiffFileStat {
133    path: String,
134    added: usize,
135    removed: usize,
136    rendered: String,
137}
138
139impl DiffFileStat {
140    /// Creates line-count stats for one displayed diff path.
141    #[must_use]
142    pub fn new(path: impl Into<String>, added: usize, removed: usize) -> Self {
143        Self {
144            path: path.into(),
145            added,
146            removed,
147            rendered: String::new(),
148        }
149    }
150
151    /// Sets the rendered `--stat` suffix for this path, including the separator.
152    #[must_use]
153    pub fn with_rendered(mut self, rendered: impl Into<String>) -> Self {
154        self.rendered = rendered.into();
155        self
156    }
157
158    /// Returns the displayed diff path.
159    #[must_use]
160    pub fn path(&self) -> &str {
161        &self.path
162    }
163
164    /// Returns the number of added lines.
165    #[must_use]
166    pub const fn added(&self) -> usize {
167        self.added
168    }
169
170    /// Returns the number of removed lines.
171    #[must_use]
172    pub const fn removed(&self) -> usize {
173        self.removed
174    }
175
176    /// Returns the rendered `--stat` suffix for this path.
177    #[must_use]
178    pub fn rendered(&self) -> &str {
179        &self.rendered
180    }
181}
182
183/// A semantic log item that the TUI can navigate without knowing how `jj` produced it.
184#[derive(Clone, Debug, Eq, PartialEq)]
185pub struct LogEntry {
186    change_id: String,
187    commit_id: String,
188    description: String,
189    details: String,
190    rendered_line: usize,
191}
192
193impl LogEntry {
194    /// Creates a log entry from `jj` change metadata.
195    ///
196    /// New entries default to rendered line `0`; callers that build a [`LogSnapshot`] should set
197    /// the aligned rendered line with [`Self::with_rendered_line`] while constructing entries.
198    #[must_use]
199    pub fn new(
200        change_id: impl Into<String>,
201        commit_id: impl Into<String>,
202        description: impl Into<String>,
203    ) -> Self {
204        Self {
205            change_id: change_id.into(),
206            commit_id: commit_id.into(),
207            description: description.into(),
208            details: String::new(),
209            rendered_line: 0,
210        }
211    }
212
213    /// Sets the template-derived detail text for inline expansion.
214    #[must_use]
215    pub fn with_details(mut self, details: impl Into<String>) -> Self {
216        self.details = details.into();
217        self
218    }
219
220    /// Sets the rendered line for this entry.
221    #[must_use]
222    pub const fn with_rendered_line(mut self, rendered_line: usize) -> Self {
223        self.rendered_line = rendered_line;
224        self
225    }
226
227    /// Returns the stable `jj` change identifier.
228    #[must_use]
229    pub fn change_id(&self) -> &str {
230        &self.change_id
231    }
232
233    /// Returns the commit identifier for follow-up inspection commands.
234    #[must_use]
235    pub fn commit_id(&self) -> &str {
236        &self.commit_id
237    }
238
239    /// Returns the full commit description.
240    #[must_use]
241    pub fn description(&self) -> &str {
242        &self.description
243    }
244
245    /// Returns template-derived detail text for inline expansion.
246    #[must_use]
247    pub fn details(&self) -> &str {
248        &self.details
249    }
250
251    /// Returns the zero-based line in the rendered `jj` output for this entry.
252    #[must_use]
253    pub const fn rendered_line(&self) -> usize {
254        self.rendered_line
255    }
256
257    /// Returns the first line of the commit description, or a placeholder for empty descriptions.
258    #[must_use]
259    pub fn summary(&self) -> &str {
260        let first_line = self.description.lines().next().unwrap_or_default();
261        if first_line.is_empty() {
262            "(no description set)"
263        } else {
264            first_line
265        }
266    }
267}