From c8ed860da5f510898f21cb5d4b0644c2004e2b28 Mon Sep 17 00:00:00 2001 From: multiplex55 <6619098+multiplex55@users.noreply.github.com> Date: Thu, 8 Jan 2026 16:12:06 -0500 Subject: [PATCH] Add recycle bin dashboard widget --- src/dashboard/data_cache.rs | 68 +++++++++++- src/dashboard/widgets/mod.rs | 18 +++- src/dashboard/widgets/recycle_bin.rs | 155 +++++++++++++++++++++++++++ src/launcher.rs | 29 +++++ 4 files changed, 261 insertions(+), 9 deletions(-) create mode 100644 src/dashboard/widgets/recycle_bin.rs diff --git a/src/dashboard/data_cache.rs b/src/dashboard/data_cache.rs index 6ee3d8b9..6c1896ae 100644 --- a/src/dashboard/data_cache.rs +++ b/src/dashboard/data_cache.rs @@ -5,6 +5,7 @@ use crate::plugins::fav::{load_favs, FavEntry, FAV_FILE}; use crate::plugins::note::{load_notes, Note}; use crate::plugins::snippets::{load_snippets, SnippetEntry, SNIPPETS_FILE}; use crate::plugins::todo::{load_todos, TodoEntry, TODO_FILE}; +use crate::{launcher, launcher::RecycleBinInfo}; use std::sync::{Arc, Mutex}; use std::time::{Duration, Instant}; use sysinfo::{Disks, Networks, System}; @@ -20,6 +21,21 @@ pub struct SystemStatusSnapshot { pub brightness_percent: Option, } +#[derive(Clone, Debug, Default)] +pub struct RecycleBinSnapshot { + pub size_bytes: u64, + pub items: u64, +} + +impl From for RecycleBinSnapshot { + fn from(info: RecycleBinInfo) -> Self { + Self { + size_bytes: info.size_bytes, + items: info.items, + } + } +} + #[derive(Clone)] pub struct DashboardDataSnapshot { pub clipboard_history: Arc>, @@ -30,6 +46,7 @@ pub struct DashboardDataSnapshot { pub favorites: Arc>, pub process_error: Option, pub system_status: Option, + pub recycle_bin: Option, } impl Default for DashboardDataSnapshot { @@ -43,6 +60,7 @@ impl Default for DashboardDataSnapshot { favorites: Arc::new(Vec::new()), process_error: None, system_status: None, + recycle_bin: None, } } } @@ -58,6 +76,7 @@ impl DashboardDataSnapshot { favorites: Arc::clone(&self.favorites), process_error: self.process_error.clone(), system_status: self.system_status.clone(), + recycle_bin: self.recycle_bin.clone(), } } @@ -71,6 +90,7 @@ impl DashboardDataSnapshot { favorites: Arc::clone(&self.favorites), process_error: self.process_error.clone(), system_status: self.system_status.clone(), + recycle_bin: self.recycle_bin.clone(), } } @@ -84,6 +104,7 @@ impl DashboardDataSnapshot { favorites: Arc::clone(&self.favorites), process_error: self.process_error.clone(), system_status: self.system_status.clone(), + recycle_bin: self.recycle_bin.clone(), } } @@ -97,6 +118,7 @@ impl DashboardDataSnapshot { favorites: Arc::clone(&self.favorites), process_error: self.process_error.clone(), system_status: self.system_status.clone(), + recycle_bin: self.recycle_bin.clone(), } } @@ -110,6 +132,7 @@ impl DashboardDataSnapshot { favorites: Arc::new(favorites), process_error: self.process_error.clone(), system_status: self.system_status.clone(), + recycle_bin: self.recycle_bin.clone(), } } @@ -123,6 +146,7 @@ impl DashboardDataSnapshot { favorites: Arc::clone(&self.favorites), process_error, system_status: self.system_status.clone(), + recycle_bin: self.recycle_bin.clone(), } } @@ -136,6 +160,21 @@ impl DashboardDataSnapshot { favorites: Arc::clone(&self.favorites), process_error: self.process_error.clone(), system_status, + recycle_bin: self.recycle_bin.clone(), + } + } + + fn with_recycle_bin(&self, recycle_bin: Option) -> Self { + Self { + clipboard_history: Arc::clone(&self.clipboard_history), + snippets: Arc::clone(&self.snippets), + notes: Arc::clone(&self.notes), + todos: Arc::clone(&self.todos), + processes: Arc::clone(&self.processes), + favorites: Arc::clone(&self.favorites), + process_error: self.process_error.clone(), + system_status: self.system_status.clone(), + recycle_bin, } } } @@ -144,6 +183,7 @@ struct DashboardDataState { snapshot: Arc, last_process_refresh: Instant, last_system_refresh: Instant, + last_recycle_refresh: Instant, last_network_totals: (u64, u64), last_network_time: Instant, } @@ -159,6 +199,7 @@ impl DashboardDataCache { snapshot: Arc::new(DashboardDataSnapshot::default()), last_process_refresh: Instant::now() - Duration::from_secs(60), last_system_refresh: Instant::now() - Duration::from_secs(60), + last_recycle_refresh: Instant::now() - Duration::from_secs(60), last_network_totals: (0, 0), last_network_time: Instant::now() - Duration::from_secs(60), }), @@ -180,6 +221,7 @@ impl DashboardDataCache { self.refresh_favorites(); self.refresh_processes(plugins); self.refresh_system_status(); + self.refresh_recycle_bin(); } pub fn refresh_clipboard(&self) { @@ -314,6 +356,25 @@ impl DashboardDataCache { } } + pub fn maybe_refresh_recycle_bin(&self, interval: Duration) { + let should_refresh = self + .state + .lock() + .map(|state| state.last_recycle_refresh.elapsed() >= interval) + .unwrap_or(false); + if should_refresh { + self.refresh_recycle_bin(); + } + } + + pub fn refresh_recycle_bin(&self) { + let snapshot = launcher::query_recycle_bin().map(|data| RecycleBinSnapshot::from(data)); + if let Ok(mut state) = self.state.lock() { + state.snapshot = Arc::new(state.snapshot.with_recycle_bin(snapshot)); + state.last_recycle_refresh = Instant::now(); + } + } + fn load_processes(plugins: &PluginManager) -> (Vec, Option) { let plugin = plugins.iter().find(|p| p.name() == "processes"); if let Some(plugin) = plugin { @@ -360,12 +421,12 @@ fn get_system_volume() -> Option { #[cfg(target_os = "windows")] fn get_main_display_brightness() -> Option { - use windows::Win32::Foundation::{BOOL, LPARAM, RECT}; - use windows::Win32::Graphics::Gdi::{EnumDisplayMonitors, HDC, HMONITOR}; use windows::Win32::Devices::Display::{ DestroyPhysicalMonitors, GetMonitorBrightness, GetNumberOfPhysicalMonitorsFromHMONITOR, GetPhysicalMonitorsFromHMONITOR, PHYSICAL_MONITOR, }; + use windows::Win32::Foundation::{BOOL, LPARAM, RECT}; + use windows::Win32::Graphics::Gdi::{EnumDisplayMonitors, HDC, HMONITOR}; unsafe extern "system" fn enum_monitors( hmonitor: HMONITOR, @@ -382,8 +443,7 @@ fn get_main_display_brightness() -> Option { let mut min = 0u32; let mut cur = 0u32; let mut max = 0u32; - if GetMonitorBrightness(m.hPhysicalMonitor, &mut min, &mut cur, &mut max) != 0 - { + if GetMonitorBrightness(m.hPhysicalMonitor, &mut min, &mut cur, &mut max) != 0 { if max > min { *percent_ptr = ((cur - min) * 100 / (max - min)) as u32; } else { diff --git a/src/dashboard/widgets/mod.rs b/src/dashboard/widgets/mod.rs index 97e2dba9..e413f2a2 100644 --- a/src/dashboard/widgets/mod.rs +++ b/src/dashboard/widgets/mod.rs @@ -20,17 +20,18 @@ mod pinned_query_results; mod plugin_home; mod process_list; mod query_list; +mod quick_tools; mod recent_commands; mod recent_notes; +mod recycle_bin; mod snippets_favorites; mod system_actions; mod system_status; -mod todo_focus; mod todo; +mod todo_focus; mod weather_site; mod window_list; mod windows_overview; -mod quick_tools; pub use active_timers::ActiveTimersWidget; pub use browser_tabs::BrowserTabsWidget; @@ -45,17 +46,18 @@ pub use pinned_query_results::PinnedQueryResultsWidget; pub use plugin_home::PluginHomeWidget; pub use process_list::ProcessesWidget; pub use query_list::QueryListWidget; +pub use quick_tools::QuickToolsWidget; pub use recent_commands::RecentCommandsWidget; pub use recent_notes::RecentNotesWidget; +pub use recycle_bin::RecycleBinWidget; pub use snippets_favorites::SnippetsFavoritesWidget; pub use system_actions::SystemWidget; pub use system_status::SystemStatusWidget; -pub use todo_focus::TodoFocusWidget; pub use todo::TodoWidget; +pub use todo_focus::TodoFocusWidget; pub use weather_site::WeatherSiteWidget; pub use window_list::WindowsWidget; pub use windows_overview::WindowsOverviewWidget; -pub use quick_tools::QuickToolsWidget; /// Result of a widget activation. #[derive(Debug, Clone)] @@ -299,7 +301,13 @@ impl WidgetRegistry { ); reg.register( "quick_tools", - WidgetFactory::new(QuickToolsWidget::new).with_settings_ui(QuickToolsWidget::settings_ui), + WidgetFactory::new(QuickToolsWidget::new) + .with_settings_ui(QuickToolsWidget::settings_ui), + ); + reg.register( + "recycle_bin", + WidgetFactory::new(RecycleBinWidget::new) + .with_settings_ui(RecycleBinWidget::settings_ui), ); reg } diff --git a/src/dashboard/widgets/recycle_bin.rs b/src/dashboard/widgets/recycle_bin.rs new file mode 100644 index 00000000..954200e3 --- /dev/null +++ b/src/dashboard/widgets/recycle_bin.rs @@ -0,0 +1,155 @@ +use super::{ + edit_typed_settings, refresh_interval_setting, Widget, WidgetAction, WidgetSettingsContext, + WidgetSettingsUiResult, +}; +use crate::actions::Action; +use crate::dashboard::dashboard::{DashboardContext, WidgetActivation}; +use eframe::egui; +use serde::{Deserialize, Serialize}; +use std::time::Duration; + +fn default_refresh_interval() -> f32 { + 60.0 +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct RecycleBinConfig { + #[serde(default = "default_refresh_interval")] + pub refresh_interval_secs: f32, + #[serde(default)] + pub manual_refresh_only: bool, +} + +impl Default for RecycleBinConfig { + fn default() -> Self { + Self { + refresh_interval_secs: default_refresh_interval(), + manual_refresh_only: false, + } + } +} + +pub struct RecycleBinWidget { + cfg: RecycleBinConfig, + refresh_pending: bool, +} + +impl RecycleBinWidget { + pub fn new(cfg: RecycleBinConfig) -> Self { + Self { + cfg, + refresh_pending: false, + } + } + + 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 RecycleBinConfig, _ctx| { + refresh_interval_setting( + ui, + &mut cfg.refresh_interval_secs, + &mut cfg.manual_refresh_only, + "Recycle bin data is cached between refreshes.", + ) + }) + } + + fn refresh_interval(&self) -> Duration { + Duration::from_secs_f32(self.cfg.refresh_interval_secs.max(1.0)) + } + + fn fmt_size(bytes: u64) -> String { + const KB: f64 = 1024.0; + const MB: f64 = KB * 1024.0; + const GB: f64 = MB * 1024.0; + const TB: f64 = GB * 1024.0; + let bytes_f = bytes as f64; + if bytes_f >= TB { + format!("{:.2} TB", bytes_f / TB) + } else if bytes_f >= GB { + format!("{:.2} GB", bytes_f / GB) + } else if bytes_f >= MB { + format!("{:.1} MB", bytes_f / MB) + } else if bytes_f >= KB { + format!("{:.1} KB", bytes_f / KB) + } else { + format!("{} B", bytes) + } + } + + fn clean_action() -> Action { + Action { + label: "Clean Recycle Bin".into(), + desc: "Recycle Bin".into(), + action: "recycle:clean".into(), + args: None, + } + } +} + +impl Default for RecycleBinWidget { + fn default() -> Self { + Self::new(RecycleBinConfig::default()) + } +} + +impl Widget for RecycleBinWidget { + fn render( + &mut self, + ui: &mut egui::Ui, + ctx: &DashboardContext<'_>, + _activation: WidgetActivation, + ) -> Option { + if self.refresh_pending { + ctx.data_cache.refresh_recycle_bin(); + self.refresh_pending = false; + } else if !self.cfg.manual_refresh_only { + ctx.data_cache + .maybe_refresh_recycle_bin(self.refresh_interval()); + } + + let snapshot = ctx.data_cache.snapshot(); + let Some(info) = snapshot.recycle_bin.as_ref() else { + ui.label("Recycle bin data unavailable."); + return None; + }; + + ui.label(format!("Size: {}", Self::fmt_size(info.size_bytes))); + ui.label(format!("Items: {}", info.items)); + + if ui.button("Clean").clicked() { + let action = Self::clean_action(); + return Some(WidgetAction { + query_override: Some(action.label.clone()), + action, + }); + } + + None + } + + 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!( + "Cached for {:.0}s. Refresh to update recycle bin stats immediately.", + self.cfg.refresh_interval_secs + ) + }; + if ui.small_button("Refresh").on_hover_text(tooltip).clicked() { + ctx.data_cache.refresh_recycle_bin(); + } + None + } +} diff --git a/src/launcher.rs b/src/launcher.rs index 0895b40b..25ac8c7e 100644 --- a/src/launcher.rs +++ b/src/launcher.rs @@ -229,6 +229,35 @@ pub(crate) fn clean_recycle_bin() -> windows::core::Result<()> { } } +#[derive(Debug, Clone, Copy)] +pub(crate) struct RecycleBinInfo { + pub size_bytes: u64, + pub items: u64, +} + +#[cfg(target_os = "windows")] +pub(crate) fn query_recycle_bin() -> Option { + use windows::Win32::UI::Shell::{SHQueryRecycleBinW, SHQUERYRBINFO}; + let mut info = SHQUERYRBINFO { + cbSize: std::mem::size_of::() as u32, + ..Default::default() + }; + let result = unsafe { SHQueryRecycleBinW(None, &mut info) }; + if result.is_ok() { + Some(RecycleBinInfo { + size_bytes: info.i64Size.max(0) as u64, + items: info.i64NumItems.max(0) as u64, + }) + } else { + None + } +} + +#[cfg(not(target_os = "windows"))] +pub(crate) fn query_recycle_bin() -> Option { + None +} + pub(crate) fn system_command(action: &str) -> Option { use std::process::Command; match action {