Skip to main content

lattice_compilation/
headerline.rs

1//! Compilation headerline — the compilation-mode-owned status bar
2//! rendered as a sticky virtual row above the `*compilation*` buffer.
3//!
4//! Renders the build command with a state icon leading and a status
5//! badge trailing. The command is double-quoted for readability and
6//! emphasised in `command_fg` (warm yellow).
7
8use std::sync::Arc;
9use std::sync::atomic::{AtomicU64, Ordering};
10
11use lattice_cells::{Cell, Headerline, HeaderlineRow};
12
13/// Shared state the compilation drain updates and the headerline
14/// renderer reads.
15#[derive(Debug, Clone, Default)]
16pub struct CompilationHeadlineState {
17    pub command: String,
18    pub last_counts: Option<(usize, usize)>,
19    pub running: bool,
20    pub killed: bool,
21}
22
23/// A `Headerline` impl backed by `CompilationHeadlineState`.
24///
25/// Renders:
26///   ` ⟳ "cargo build --release" … `          (running)
27///   ` ✔ "cargo build --release" ok `         (finished clean)
28///   ` ✗ "cargo build --release" 3e 2w `      (finished with errors)
29///   ` ■ "cargo build --release" killed `     (explicitly killed)
30pub struct CompilationHeaderline {
31    state: Arc<std::sync::RwLock<CompilationHeadlineState>>,
32    version: Arc<AtomicU64>,
33    command_fg: u32,
34    in_progress_fg: u32,
35    success_fg: u32,
36    failure_fg: u32,
37    dim_fg: u32,
38}
39
40impl CompilationHeaderline {
41    pub fn new(
42        state: Arc<std::sync::RwLock<CompilationHeadlineState>>,
43        version: Arc<AtomicU64>,
44        command_fg: u32,
45        in_progress_fg: u32,
46        success_fg: u32,
47        failure_fg: u32,
48        dim_fg: u32,
49    ) -> Self {
50        Self {
51            state,
52            version,
53            command_fg,
54            in_progress_fg,
55            success_fg,
56            failure_fg,
57            dim_fg,
58        }
59    }
60}
61
62impl Headerline for CompilationHeaderline {
63    fn version(&self) -> u64 {
64        self.version.load(Ordering::Acquire)
65    }
66
67    fn render(&self) -> Option<HeaderlineRow> {
68        let state = self.state.read().ok()?;
69        if state.command.is_empty() {
70            return None;
71        }
72        let running = state.running;
73        let killed = state.killed;
74        let (errors, warnings) = state.last_counts.unwrap_or((0, 0));
75        let has_errors = errors > 0;
76
77        let (icon, icon_fg) = if running {
78            ("\u{27f3}", self.in_progress_fg)
79        } else if killed {
80            ("\u{25a0}", self.failure_fg)
81        } else if has_errors {
82            ("\u{2717}", self.failure_fg)
83        } else {
84            ("\u{2714}", self.success_fg)
85        };
86
87        let quoted = format!("\"{}\"", state.command);
88
89        if has_errors && !running && !killed {
90            // Per-count spans: error count in failure_fg, warning count in dim_fg.
91            let mut parts: Vec<(String, u32)> = Vec::new();
92            parts.push((format!(" {icon} "), icon_fg));
93            parts.push((quoted, self.command_fg));
94            if errors > 0 {
95                parts.push((format!(" {errors}e"), self.failure_fg));
96            }
97            if warnings > 0 {
98                parts.push((format!(" {warnings}w"), self.dim_fg));
99            }
100            let cells: Arc<[Cell]> = parts
101                .iter()
102                .flat_map(|(s, fg)| s.chars().map(move |c| Cell::new(c as u32, *fg, 0, 0)))
103                .collect::<Vec<_>>()
104                .into();
105            return Some(HeaderlineRow { cells, bg: None });
106        }
107
108        let status = if running {
109            " \u{2026}"
110        } else if killed {
111            " killed"
112        } else {
113            " ok"
114        };
115
116        let text = format!(" {icon} {quoted}{status}");
117        let icon_len = icon.chars().count() + 2; // leading space + icon + trailing space
118        let cmd_end = icon_len + quoted.chars().count();
119        let cells = text
120            .chars()
121            .enumerate()
122            .map(|(i, c)| {
123                let fg = if i < icon_len {
124                    icon_fg
125                } else if i < cmd_end {
126                    self.command_fg
127                } else {
128                    self.dim_fg
129                };
130                Cell::new(c as u32, fg, 0, 0)
131            })
132            .collect::<Vec<_>>()
133            .into();
134        Some(HeaderlineRow { cells, bg: None })
135    }
136}
137
138/// Provider id tag for the compilation headerline so re-activations
139/// can `unregister` / `register` idempotently.
140pub const COMPILATION_HEADERLINE_PROVIDER_ID: u64 = 0x636f_6d70_686c_0300; // "comp-hl"
141
142#[cfg(test)]
143mod tests {
144    use super::*;
145    use std::sync::RwLock;
146
147    fn state(
148        cmd: &str,
149        running: bool,
150        killed: bool,
151        counts: Option<(usize, usize)>,
152    ) -> Arc<RwLock<CompilationHeadlineState>> {
153        Arc::new(RwLock::new(CompilationHeadlineState {
154            command: cmd.to_string(),
155            last_counts: counts,
156            running,
157            killed,
158        }))
159    }
160
161    fn hl() -> CompilationHeaderline {
162        CompilationHeaderline::new(
163            Arc::new(RwLock::new(CompilationHeadlineState::default())),
164            Arc::new(AtomicU64::new(1)),
165            0xf9e2af, // command_fg (warm yellow)
166            0x999999, // in_progress_fg (grey)
167            0x44cc88, // success_fg (green)
168            0xff4444, // failure_fg (red)
169            0x888888, // dim_fg (muted)
170        )
171    }
172
173    #[test]
174    fn empty_command_returns_none() {
175        let h = hl();
176        assert!(h.render().is_none(), "no headerline when command is empty");
177    }
178
179    #[test]
180    fn running_shows_spinner_and_ellipsis() {
181        let h = CompilationHeaderline::new(
182            state("cargo build", true, false, None),
183            Arc::new(AtomicU64::new(1)),
184            0xf9e2af,
185            0x999999,
186            0x44cc88,
187            0xff4444,
188            0x888888,
189        );
190        let row = h.render().expect("headerline must render");
191        let text: String = row
192            .cells
193            .iter()
194            .map(|c| char::from_u32(c.codepoint).unwrap())
195            .collect();
196        assert!(text.contains('\u{27f3}'), "spinner icon \u{27f3} present");
197        assert!(text.contains('"'), "command is quoted");
198        assert!(text.contains("cargo build"), "command text present");
199        assert!(text.contains('\u{2026}'), "ellipsis present");
200    }
201
202    #[test]
203    fn finished_clean_shows_checkmark_and_ok() {
204        let h = CompilationHeaderline::new(
205            state("cargo build", false, false, Some((0, 0))),
206            Arc::new(AtomicU64::new(1)),
207            0xf9e2af,
208            0x999999,
209            0x44cc88,
210            0xff4444,
211            0x888888,
212        );
213        let row = h.render().expect("headerline must render");
214        let text: String = row
215            .cells
216            .iter()
217            .map(|c| char::from_u32(c.codepoint).unwrap())
218            .collect();
219        assert!(text.contains('\u{2714}'), "checkmark icon present");
220        assert!(text.contains("ok"), "ok status present");
221    }
222
223    #[test]
224    fn finished_errors_shows_counts() {
225        let h = CompilationHeaderline::new(
226            state("cargo build", false, false, Some((3, 2))),
227            Arc::new(AtomicU64::new(1)),
228            0xf9e2af,
229            0x999999,
230            0x44cc88,
231            0xff4444,
232            0x888888,
233        );
234        let row = h.render().expect("headerline must render");
235        let text: String = row
236            .cells
237            .iter()
238            .map(|c| char::from_u32(c.codepoint).unwrap())
239            .collect();
240        assert!(text.contains('\u{2717}'), "cross icon present");
241        assert!(text.contains("3e"), "error count present");
242        assert!(text.contains("2w"), "warning count present");
243    }
244
245    #[test]
246    fn killed_shows_square_and_killed_text() {
247        let h = CompilationHeaderline::new(
248            state("cargo build", false, true, Some((0, 0))),
249            Arc::new(AtomicU64::new(1)),
250            0xf9e2af,
251            0x999999,
252            0x44cc88,
253            0xff4444,
254            0x888888,
255        );
256        let row = h.render().expect("headerline must render");
257        let text: String = row
258            .cells
259            .iter()
260            .map(|c| char::from_u32(c.codepoint).unwrap())
261            .collect();
262        assert!(text.contains('\u{25a0}'), "square icon present");
263        assert!(text.contains("killed"), "killed text present");
264    }
265}