1use std::sync::Arc;
9use std::sync::atomic::{AtomicU64, Ordering};
10
11use lattice_cells::{Cell, Headerline, HeaderlineRow};
12
13#[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
23pub 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 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; 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
138pub const COMPILATION_HEADERLINE_PROVIDER_ID: u64 = 0x636f_6d70_686c_0300; #[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, 0x999999, 0x44cc88, 0xff4444, 0x888888, )
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}