From 81b213d3863a2519874fe3be91804b89026637f3 Mon Sep 17 00:00:00 2001 From: multiplex55 <6619098+multiplex55@users.noreply.github.com> Date: Thu, 8 Jan 2026 17:45:20 -0500 Subject: [PATCH] Add stopwatch dashboard widget --- src/dashboard/widgets/mod.rs | 6 + src/dashboard/widgets/stopwatch.rs | 190 +++++++++++++++++++++++++++++ 2 files changed, 196 insertions(+) create mode 100644 src/dashboard/widgets/stopwatch.rs diff --git a/src/dashboard/widgets/mod.rs b/src/dashboard/widgets/mod.rs index 823bae83..b7f1c199 100644 --- a/src/dashboard/widgets/mod.rs +++ b/src/dashboard/widgets/mod.rs @@ -25,6 +25,7 @@ mod recent_commands; mod recent_notes; mod recycle_bin; mod snippets_favorites; +mod stopwatch; mod system_actions; mod system_status; mod tempfiles; @@ -53,6 +54,7 @@ pub use recent_commands::RecentCommandsWidget; pub use recent_notes::RecentNotesWidget; pub use recycle_bin::RecycleBinWidget; pub use snippets_favorites::SnippetsFavoritesWidget; +pub use stopwatch::StopwatchWidget; pub use system_actions::SystemWidget; pub use system_status::SystemStatusWidget; pub use tempfiles::TempfilesWidget; @@ -321,6 +323,10 @@ impl WidgetRegistry { "volume", WidgetFactory::new(VolumeWidget::new).with_settings_ui(VolumeWidget::settings_ui), ); + reg.register( + "stopwatch", + WidgetFactory::new(StopwatchWidget::new).with_settings_ui(StopwatchWidget::settings_ui), + ); reg } diff --git a/src/dashboard/widgets/stopwatch.rs b/src/dashboard/widgets/stopwatch.rs new file mode 100644 index 00000000..d508bfc8 --- /dev/null +++ b/src/dashboard/widgets/stopwatch.rs @@ -0,0 +1,190 @@ +use super::{ + edit_typed_settings, refresh_interval_setting, TimedCache, Widget, WidgetAction, + WidgetSettingsContext, WidgetSettingsUiResult, +}; +use crate::actions::Action; +use crate::dashboard::dashboard::{DashboardContext, WidgetActivation}; +use crate::plugins::stopwatch; +use eframe::egui; +use serde::{Deserialize, Serialize}; +use std::time::Duration; + +fn default_refresh_interval() -> f32 { + stopwatch::refresh_rate().max(1.0) +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct StopwatchConfig { + #[serde(default = "default_refresh_interval")] + pub refresh_interval_secs: f32, + #[serde(default)] + pub manual_refresh_only: bool, +} + +impl Default for StopwatchConfig { + fn default() -> Self { + Self { + refresh_interval_secs: default_refresh_interval(), + manual_refresh_only: false, + } + } +} + +#[derive(Debug, Clone)] +struct StopwatchRow { + id: u64, + label: String, + running: bool, +} + +pub struct StopwatchWidget { + cfg: StopwatchConfig, + cache: TimedCache>, + refresh_pending: bool, +} + +impl StopwatchWidget { + pub fn new(cfg: StopwatchConfig) -> Self { + let interval = Duration::from_secs_f32(cfg.refresh_interval_secs.max(1.0)); + Self { + cfg, + cache: TimedCache::new(Vec::new(), interval), + refresh_pending: true, + } + } + + pub fn settings_ui( + ui: &mut egui::Ui, + value: &mut serde_json::Value, + ctx: &WidgetSettingsContext<'_>, + ) -> WidgetSettingsUiResult { + edit_typed_settings(ui, value, ctx, |ui, cfg: &mut StopwatchConfig, _ctx| { + refresh_interval_setting( + ui, + &mut cfg.refresh_interval_secs, + &mut cfg.manual_refresh_only, + "Stopwatch list refreshes on this interval unless manual refresh is enabled.", + ) + }) + } + + fn refresh_interval(&self) -> Duration { + Duration::from_secs_f32(self.cfg.refresh_interval_secs.max(1.0)) + } + + fn refresh(&mut self) { + self.cache.set_interval(self.refresh_interval()); + self.cache.refresh(|rows| { + let mut next: Vec = stopwatch::all_stopwatches() + .into_iter() + .map(|(id, label, _elapsed, running)| StopwatchRow { id, label, running }) + .collect(); + next.sort_by(|a, b| a.label.cmp(&b.label).then(a.id.cmp(&b.id))); + *rows = next; + }); + } + + fn maybe_refresh(&mut self) { + if self.refresh_pending { + self.refresh_pending = false; + self.refresh(); + } else if !self.cfg.manual_refresh_only && self.cache.should_refresh() { + self.refresh(); + } + } + + fn action_for(id: u64, label: &str, action: &str, query: &str) -> WidgetAction { + WidgetAction { + action: Action { + label: format!("{label} stopwatch {id}"), + desc: "Stopwatch".into(), + action: action.to_string(), + args: None, + }, + query_override: Some(query.to_string()), + } + } +} + +impl Default for StopwatchWidget { + fn default() -> Self { + Self::new(StopwatchConfig::default()) + } +} + +impl Widget for StopwatchWidget { + fn render( + &mut self, + ui: &mut egui::Ui, + _ctx: &DashboardContext<'_>, + _activation: WidgetActivation, + ) -> Option { + self.maybe_refresh(); + + let mut clicked = None; + + if self.cache.data.is_empty() { + ui.label("No stopwatches running"); + return None; + } + + for row in &self.cache.data { + let status = if row.running { "Running" } else { "Paused" }; + let elapsed = stopwatch::format_elapsed(row.id).unwrap_or_else(|| "--".into()); + ui.horizontal(|ui| { + ui.label(&row.label); + ui.label(egui::RichText::new(elapsed).monospace()); + ui.label(egui::RichText::new(status).small()); + if row.running { + if ui.small_button("Pause").clicked() { + clicked = Some(Self::action_for( + row.id, + "Pause", + &format!("stopwatch:pause:{}", row.id), + "sw pause", + )); + } + } else if ui.small_button("Resume").clicked() { + clicked = Some(Self::action_for( + row.id, + "Resume", + &format!("stopwatch:resume:{}", row.id), + "sw resume", + )); + } + if ui.small_button("Stop").clicked() { + clicked = Some(Self::action_for( + row.id, + "Stop", + &format!("stopwatch:stop:{}", row.id), + "sw stop", + )); + } + }); + } + + clicked + } + + fn on_config_updated(&mut self, settings: &serde_json::Value) { + if let Ok(cfg) = serde_json::from_value::(settings.clone()) { + self.cfg = cfg; + self.refresh_pending = true; + } + } + + fn header_ui(&mut self, ui: &mut egui::Ui, _ctx: &DashboardContext<'_>) -> Option { + let tooltip = if self.cfg.manual_refresh_only { + "Manual refresh only.".to_string() + } else { + format!( + "Refreshes every {:.0}s unless you refresh manually.", + self.cfg.refresh_interval_secs + ) + }; + if ui.small_button("Refresh").on_hover_text(tooltip).clicked() { + self.refresh_pending = true; + } + None + } +}