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