Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions src/dashboard/widgets/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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
}

Expand Down
190 changes: 190 additions & 0 deletions src/dashboard/widgets/stopwatch.rs
Original file line number Diff line number Diff line change
@@ -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<Vec<StopwatchRow>>,
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<StopwatchRow> = 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<WidgetAction> {
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::<StopwatchConfig>(settings.clone()) {
self.cfg = cfg;
self.refresh_pending = true;
}
}

fn header_ui(&mut self, ui: &mut egui::Ui, _ctx: &DashboardContext<'_>) -> Option<WidgetAction> {
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
}
}