1pub mod disk_monitor;
39pub mod git_poller;
40pub mod system_monitor;
41pub mod widgets;
42
43use std::time::Instant;
44
45use crate::badge::SessionVariables;
46use crate::config::{Config, StatusBarPosition, StatusBarSection};
47use disk_monitor::DiskMonitor;
48use git_poller::GitBranchPoller;
49use system_monitor::SystemMonitor;
50use widgets::{WidgetContext, sorted_widgets_for_section, widget_text};
51
52pub use git_poller::GitStatus;
53
54#[derive(Debug, Clone, PartialEq, Eq)]
56pub enum StatusBarAction {
57 ShowUpdateDialog,
59}
60
61pub struct StatusBarUI {
63 system_monitor: SystemMonitor,
65 disk_monitor: DiskMonitor,
67 git_poller: GitBranchPoller,
69 last_mouse_activity: Instant,
71 visible: bool,
73 last_valid_time_format: String,
75 pub update_available_version: Option<String>,
77}
78
79impl StatusBarUI {
80 pub fn new() -> Self {
82 Self {
83 system_monitor: SystemMonitor::new(),
84 disk_monitor: DiskMonitor::new(),
85 git_poller: GitBranchPoller::new(),
86 last_mouse_activity: Instant::now(),
87 visible: true,
88 last_valid_time_format: "%H:%M:%S".to_string(),
89 update_available_version: None,
90 }
91 }
92
93 pub fn signal_shutdown(&self) {
96 self.system_monitor.signal_stop();
97 self.disk_monitor.signal_stop();
98 self.git_poller.signal_stop();
99 }
100
101 pub fn height(&self, config: &Config, is_fullscreen: bool) -> f32 {
105 if !config.status_bar.status_bar_enabled || self.should_hide(config, is_fullscreen) {
106 0.0
107 } else {
108 config.status_bar.status_bar_height
109 }
110 }
111
112 pub fn should_hide(&self, config: &Config, is_fullscreen: bool) -> bool {
114 if !config.status_bar.status_bar_enabled {
115 return true;
116 }
117 if config.status_bar.status_bar_auto_hide_fullscreen && is_fullscreen {
118 return true;
119 }
120 if config.status_bar.status_bar_auto_hide_mouse_inactive {
121 let elapsed = self.last_mouse_activity.elapsed().as_secs_f32();
122 if elapsed > config.status_bar.status_bar_mouse_inactive_timeout {
123 return true;
124 }
125 }
126 false
127 }
128
129 pub fn on_mouse_activity(&mut self) {
131 self.last_mouse_activity = Instant::now();
132 self.visible = true;
133 }
134
135 pub fn sync_monitor_state(&self, config: &Config) {
137 if !config.status_bar.status_bar_enabled {
138 if self.system_monitor.is_running() {
139 self.system_monitor.stop();
140 }
141 if self.disk_monitor.is_running() {
142 self.disk_monitor.stop();
143 }
144 if self.git_poller.is_running() {
145 self.git_poller.stop();
146 }
147 return;
148 }
149
150 let needs_monitor = config
152 .status_bar
153 .status_bar_widgets
154 .iter()
155 .any(|w| w.enabled && w.id.needs_system_monitor());
156
157 if needs_monitor && !self.system_monitor.is_running() {
158 self.system_monitor
159 .start(config.status_bar.status_bar_system_poll_interval);
160 } else if !needs_monitor && self.system_monitor.is_running() {
161 self.system_monitor.stop();
162 }
163
164 let needs_disk = config
166 .status_bar
167 .status_bar_widgets
168 .iter()
169 .any(|w| w.enabled && w.id.needs_disk_monitor());
170
171 self.disk_monitor
172 .set_follow_cwd(config.status_bar.status_bar_disk_follow_cwd);
173
174 if needs_disk && !self.disk_monitor.is_running() {
175 self.disk_monitor
176 .start(config.status_bar.status_bar_disk_poll_interval);
177 } else if !needs_disk && self.disk_monitor.is_running() {
178 self.disk_monitor.stop();
179 }
180
181 let needs_git = config
183 .status_bar
184 .status_bar_widgets
185 .iter()
186 .any(|w| w.enabled && w.id == crate::config::WidgetId::GitBranch);
187
188 if needs_git && !self.git_poller.is_running() {
189 self.git_poller
190 .start(config.status_bar.status_bar_git_poll_interval);
191 } else if !needs_git && self.git_poller.is_running() {
192 self.git_poller.stop();
193 }
194 }
195
196 pub fn render(
201 &mut self,
202 ctx: &egui::Context,
203 config: &Config,
204 session_vars: &SessionVariables,
205 is_fullscreen: bool,
206 ) -> (f32, Option<StatusBarAction>) {
207 if !config.status_bar.status_bar_enabled || self.should_hide(config, is_fullscreen) {
208 return (0.0, None);
209 }
210
211 let cwd = if session_vars.path.is_empty() {
213 None
214 } else {
215 Some(session_vars.path.as_str())
216 };
217 self.git_poller.set_cwd(cwd);
218 self.disk_monitor.set_cwd(cwd);
219
220 {
222 use chrono::format::strftime::StrftimeItems;
223 let valid = !config.status_bar.status_bar_time_format.is_empty()
224 && StrftimeItems::new(&config.status_bar.status_bar_time_format)
225 .all(|item| !matches!(item, chrono::format::Item::Error));
226 if valid {
227 self.last_valid_time_format = config.status_bar.status_bar_time_format.clone();
228 }
229 }
230
231 let git_status = self.git_poller.status();
233 let disk_data = self.disk_monitor.data();
234 let widget_ctx = WidgetContext {
235 session_vars: session_vars.clone(),
236 system_data: self.system_monitor.data(),
237 git_branch: git_status.branch,
238 git_ahead: git_status.ahead,
239 git_behind: git_status.behind,
240 git_dirty: git_status.dirty,
241 git_show_status: config.status_bar.status_bar_git_show_status,
242 time_format: self.last_valid_time_format.clone(),
243 update_available_version: self.update_available_version.clone(),
244 disk_free_percent: disk_data.free_percent,
245 disk_free_bytes: disk_data.free_bytes,
246 disk_total_bytes: disk_data.total_bytes,
247 };
248
249 let bar_height = config.status_bar.status_bar_height;
250 let [bg_r, bg_g, bg_b] = config.status_bar.status_bar_bg_color;
251 let bg_alpha = (config.status_bar.status_bar_bg_alpha * 255.0) as u8;
252 let bg_color = egui::Color32::from_rgba_unmultiplied(bg_r, bg_g, bg_b, bg_alpha);
253
254 let [fg_r, fg_g, fg_b] = config.status_bar.status_bar_fg_color;
255 let fg_color = egui::Color32::from_rgb(fg_r, fg_g, fg_b);
256 let font_size = config.status_bar.status_bar_font_size;
257 let separator = &config.status_bar.status_bar_separator;
258 let sep_color = fg_color.linear_multiply(0.4);
259
260 let h_margin: f32 = 8.0; let v_margin: f32 = 2.0; let scrollbar_reserved = config.scrollbar.scrollbar_width + 2.0;
266 let viewport = ctx.input(|i| i.viewport_rect());
267 let content_width = (viewport.width() - scrollbar_reserved - h_margin * 2.0).max(0.0);
269 let content_height = (bar_height - v_margin * 2.0).max(0.0);
270
271 let bar_pos = match config.status_bar.status_bar_position {
272 StatusBarPosition::Top => egui::pos2(0.0, 0.0),
273 StatusBarPosition::Bottom => egui::pos2(0.0, viewport.height() - bar_height),
274 };
275
276 let frame = egui::Frame::NONE
277 .fill(bg_color)
278 .inner_margin(egui::Margin::symmetric(h_margin as i8, v_margin as i8));
279
280 let make_rich_text = |text: &str| -> egui::RichText {
281 egui::RichText::new(text)
282 .color(fg_color)
283 .size(font_size)
284 .monospace()
285 };
286
287 let make_sep = |sep: &str| -> egui::RichText {
288 egui::RichText::new(sep)
289 .color(sep_color)
290 .size(font_size)
291 .monospace()
292 };
293
294 let mut action: Option<StatusBarAction> = None;
295
296 egui::Area::new(egui::Id::new("status_bar"))
297 .fixed_pos(bar_pos)
298 .order(egui::Order::Background)
299 .interactable(true)
300 .show(ctx, |ui| {
301 ui.set_max_width(content_width + h_margin * 2.0);
304 ui.set_max_height(bar_height);
305
306 frame.show(ui, |ui| {
307 ui.set_min_size(egui::vec2(content_width, content_height));
308 ui.set_max_size(egui::vec2(content_width, content_height));
309
310 ui.horizontal_centered(|ui| {
311 ui.set_clip_rect(ui.max_rect());
314
315 let left_widgets = sorted_widgets_for_section(
317 &config.status_bar.status_bar_widgets,
318 StatusBarSection::Left,
319 );
320 let mut first = true;
321 for w in &left_widgets {
322 let text = widget_text(&w.id, &widget_ctx, w.format.as_deref());
323 if text.is_empty() {
324 continue;
325 }
326 if !first {
327 ui.label(make_sep(separator));
328 }
329 first = false;
330 ui.label(make_rich_text(&text));
331 }
332
333 let center_widgets = sorted_widgets_for_section(
335 &config.status_bar.status_bar_widgets,
336 StatusBarSection::Center,
337 );
338 if !center_widgets.is_empty() {
339 ui.with_layout(
340 egui::Layout::centered_and_justified(egui::Direction::LeftToRight),
341 |ui| {
342 let mut first = true;
343 for w in ¢er_widgets {
344 let text =
345 widget_text(&w.id, &widget_ctx, w.format.as_deref());
346 if text.is_empty() {
347 continue;
348 }
349 if !first {
350 ui.label(make_sep(separator));
351 }
352 first = false;
353 ui.label(make_rich_text(&text));
354 }
355 },
356 );
357 }
358
359 let right_widgets = sorted_widgets_for_section(
361 &config.status_bar.status_bar_widgets,
362 StatusBarSection::Right,
363 );
364 if !right_widgets.is_empty() {
365 ui.with_layout(
366 egui::Layout::right_to_left(egui::Align::Center),
367 |ui| {
368 let mut first = true;
369 for w in right_widgets.iter().rev() {
370 let text =
371 widget_text(&w.id, &widget_ctx, w.format.as_deref());
372 if text.is_empty() {
373 continue;
374 }
375 if !first {
376 ui.label(make_sep(separator));
377 }
378 first = false;
379 if w.id == crate::config::WidgetId::UpdateAvailable {
380 let update_text = egui::RichText::new(&text)
381 .color(egui::Color32::from_rgb(255, 200, 50))
382 .size(font_size)
383 .monospace();
384 if ui
385 .add(
386 egui::Label::new(update_text)
387 .sense(egui::Sense::click()),
388 )
389 .clicked()
390 {
391 action = Some(StatusBarAction::ShowUpdateDialog);
392 }
393 } else {
394 ui.label(make_rich_text(&text));
395 }
396 }
397 },
398 );
399 }
400 });
401 });
402 });
403
404 (bar_height, action)
405 }
406}
407
408impl Default for StatusBarUI {
409 fn default() -> Self {
410 Self::new()
411 }
412}
413
414impl Drop for StatusBarUI {
415 fn drop(&mut self) {
416 self.system_monitor.stop();
417 self.disk_monitor.stop();
418 }
419}