Skip to content

Commit 48ebfbd

Browse files
authored
Merge pull request #659 from multiplex55/codex/add-stopwatch-widget-to-dashboard
Add stopwatch dashboard widget
2 parents bc830ff + 81b213d commit 48ebfbd

2 files changed

Lines changed: 196 additions & 0 deletions

File tree

src/dashboard/widgets/mod.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ mod recent_commands;
2525
mod recent_notes;
2626
mod recycle_bin;
2727
mod snippets_favorites;
28+
mod stopwatch;
2829
mod system_actions;
2930
mod system_status;
3031
mod tempfiles;
@@ -53,6 +54,7 @@ pub use recent_commands::RecentCommandsWidget;
5354
pub use recent_notes::RecentNotesWidget;
5455
pub use recycle_bin::RecycleBinWidget;
5556
pub use snippets_favorites::SnippetsFavoritesWidget;
57+
pub use stopwatch::StopwatchWidget;
5658
pub use system_actions::SystemWidget;
5759
pub use system_status::SystemStatusWidget;
5860
pub use tempfiles::TempfilesWidget;
@@ -321,6 +323,10 @@ impl WidgetRegistry {
321323
"volume",
322324
WidgetFactory::new(VolumeWidget::new).with_settings_ui(VolumeWidget::settings_ui),
323325
);
326+
reg.register(
327+
"stopwatch",
328+
WidgetFactory::new(StopwatchWidget::new).with_settings_ui(StopwatchWidget::settings_ui),
329+
);
324330
reg
325331
}
326332

src/dashboard/widgets/stopwatch.rs

Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
use super::{
2+
edit_typed_settings, refresh_interval_setting, TimedCache, Widget, WidgetAction,
3+
WidgetSettingsContext, WidgetSettingsUiResult,
4+
};
5+
use crate::actions::Action;
6+
use crate::dashboard::dashboard::{DashboardContext, WidgetActivation};
7+
use crate::plugins::stopwatch;
8+
use eframe::egui;
9+
use serde::{Deserialize, Serialize};
10+
use std::time::Duration;
11+
12+
fn default_refresh_interval() -> f32 {
13+
stopwatch::refresh_rate().max(1.0)
14+
}
15+
16+
#[derive(Debug, Clone, Serialize, Deserialize)]
17+
pub struct StopwatchConfig {
18+
#[serde(default = "default_refresh_interval")]
19+
pub refresh_interval_secs: f32,
20+
#[serde(default)]
21+
pub manual_refresh_only: bool,
22+
}
23+
24+
impl Default for StopwatchConfig {
25+
fn default() -> Self {
26+
Self {
27+
refresh_interval_secs: default_refresh_interval(),
28+
manual_refresh_only: false,
29+
}
30+
}
31+
}
32+
33+
#[derive(Debug, Clone)]
34+
struct StopwatchRow {
35+
id: u64,
36+
label: String,
37+
running: bool,
38+
}
39+
40+
pub struct StopwatchWidget {
41+
cfg: StopwatchConfig,
42+
cache: TimedCache<Vec<StopwatchRow>>,
43+
refresh_pending: bool,
44+
}
45+
46+
impl StopwatchWidget {
47+
pub fn new(cfg: StopwatchConfig) -> Self {
48+
let interval = Duration::from_secs_f32(cfg.refresh_interval_secs.max(1.0));
49+
Self {
50+
cfg,
51+
cache: TimedCache::new(Vec::new(), interval),
52+
refresh_pending: true,
53+
}
54+
}
55+
56+
pub fn settings_ui(
57+
ui: &mut egui::Ui,
58+
value: &mut serde_json::Value,
59+
ctx: &WidgetSettingsContext<'_>,
60+
) -> WidgetSettingsUiResult {
61+
edit_typed_settings(ui, value, ctx, |ui, cfg: &mut StopwatchConfig, _ctx| {
62+
refresh_interval_setting(
63+
ui,
64+
&mut cfg.refresh_interval_secs,
65+
&mut cfg.manual_refresh_only,
66+
"Stopwatch list refreshes on this interval unless manual refresh is enabled.",
67+
)
68+
})
69+
}
70+
71+
fn refresh_interval(&self) -> Duration {
72+
Duration::from_secs_f32(self.cfg.refresh_interval_secs.max(1.0))
73+
}
74+
75+
fn refresh(&mut self) {
76+
self.cache.set_interval(self.refresh_interval());
77+
self.cache.refresh(|rows| {
78+
let mut next: Vec<StopwatchRow> = stopwatch::all_stopwatches()
79+
.into_iter()
80+
.map(|(id, label, _elapsed, running)| StopwatchRow { id, label, running })
81+
.collect();
82+
next.sort_by(|a, b| a.label.cmp(&b.label).then(a.id.cmp(&b.id)));
83+
*rows = next;
84+
});
85+
}
86+
87+
fn maybe_refresh(&mut self) {
88+
if self.refresh_pending {
89+
self.refresh_pending = false;
90+
self.refresh();
91+
} else if !self.cfg.manual_refresh_only && self.cache.should_refresh() {
92+
self.refresh();
93+
}
94+
}
95+
96+
fn action_for(id: u64, label: &str, action: &str, query: &str) -> WidgetAction {
97+
WidgetAction {
98+
action: Action {
99+
label: format!("{label} stopwatch {id}"),
100+
desc: "Stopwatch".into(),
101+
action: action.to_string(),
102+
args: None,
103+
},
104+
query_override: Some(query.to_string()),
105+
}
106+
}
107+
}
108+
109+
impl Default for StopwatchWidget {
110+
fn default() -> Self {
111+
Self::new(StopwatchConfig::default())
112+
}
113+
}
114+
115+
impl Widget for StopwatchWidget {
116+
fn render(
117+
&mut self,
118+
ui: &mut egui::Ui,
119+
_ctx: &DashboardContext<'_>,
120+
_activation: WidgetActivation,
121+
) -> Option<WidgetAction> {
122+
self.maybe_refresh();
123+
124+
let mut clicked = None;
125+
126+
if self.cache.data.is_empty() {
127+
ui.label("No stopwatches running");
128+
return None;
129+
}
130+
131+
for row in &self.cache.data {
132+
let status = if row.running { "Running" } else { "Paused" };
133+
let elapsed = stopwatch::format_elapsed(row.id).unwrap_or_else(|| "--".into());
134+
ui.horizontal(|ui| {
135+
ui.label(&row.label);
136+
ui.label(egui::RichText::new(elapsed).monospace());
137+
ui.label(egui::RichText::new(status).small());
138+
if row.running {
139+
if ui.small_button("Pause").clicked() {
140+
clicked = Some(Self::action_for(
141+
row.id,
142+
"Pause",
143+
&format!("stopwatch:pause:{}", row.id),
144+
"sw pause",
145+
));
146+
}
147+
} else if ui.small_button("Resume").clicked() {
148+
clicked = Some(Self::action_for(
149+
row.id,
150+
"Resume",
151+
&format!("stopwatch:resume:{}", row.id),
152+
"sw resume",
153+
));
154+
}
155+
if ui.small_button("Stop").clicked() {
156+
clicked = Some(Self::action_for(
157+
row.id,
158+
"Stop",
159+
&format!("stopwatch:stop:{}", row.id),
160+
"sw stop",
161+
));
162+
}
163+
});
164+
}
165+
166+
clicked
167+
}
168+
169+
fn on_config_updated(&mut self, settings: &serde_json::Value) {
170+
if let Ok(cfg) = serde_json::from_value::<StopwatchConfig>(settings.clone()) {
171+
self.cfg = cfg;
172+
self.refresh_pending = true;
173+
}
174+
}
175+
176+
fn header_ui(&mut self, ui: &mut egui::Ui, _ctx: &DashboardContext<'_>) -> Option<WidgetAction> {
177+
let tooltip = if self.cfg.manual_refresh_only {
178+
"Manual refresh only.".to_string()
179+
} else {
180+
format!(
181+
"Refreshes every {:.0}s unless you refresh manually.",
182+
self.cfg.refresh_interval_secs
183+
)
184+
};
185+
if ui.small_button("Refresh").on_hover_text(tooltip).clicked() {
186+
self.refresh_pending = true;
187+
}
188+
None
189+
}
190+
}

0 commit comments

Comments
 (0)