From cd9c10f2dd3768da3f13836c44740453114e83fa Mon Sep 17 00:00:00 2001 From: Ultradesu Date: Tue, 22 Jul 2025 14:22:57 +0300 Subject: [PATCH] Added tooltip --- src/client.rs | 4 +- src/gui/mod.rs | 182 +++++++++++++++++++++++++++++++++++++++++++------ src/main.rs | 4 +- 3 files changed, 167 insertions(+), 23 deletions(-) diff --git a/src/client.rs b/src/client.rs index 78d3006..a6a26dd 100644 --- a/src/client.rs +++ b/src/client.rs @@ -8,14 +8,14 @@ use std::io::{self, BufRead, Write}; use std::path::Path; #[derive(Serialize, Deserialize, Clone, Debug)] -struct SshKey { +pub struct SshKey { server: String, public_key: String, #[serde(default)] deprecated: bool, } -fn read_known_hosts(file_path: &str) -> io::Result> { +pub fn read_known_hosts(file_path: &str) -> io::Result> { let path = Path::new(file_path); let file = File::open(&path)?; let reader = io::BufReader::new(file); diff --git a/src/gui/mod.rs b/src/gui/mod.rs index 9f675f8..996ea79 100644 --- a/src/gui/mod.rs +++ b/src/gui/mod.rs @@ -24,7 +24,7 @@ pub fn run_settings_window() { } // Function to perform sync operation using KHM client logic -async fn perform_sync(settings: &KhmSettings) -> std::io::Result<()> { +async fn perform_sync(settings: &KhmSettings) -> Result { use crate::Args; info!("Starting sync with settings: host={}, flow={}, known_hosts={}, in_place={}", @@ -51,7 +51,23 @@ async fn perform_sync(settings: &KhmSettings) -> std::io::Result<()> { info!("Expanded known_hosts path: {}", args.known_hosts); - crate::client::run_client(args).await + // Get keys count before and after sync + let keys_before = crate::client::read_known_hosts(&args.known_hosts) + .unwrap_or_else(|_| Vec::new()) + .len(); + + crate::client::run_client(args.clone()).await?; + + let keys_after = if args.in_place { + crate::client::read_known_hosts(&args.known_hosts) + .unwrap_or_else(|_| Vec::new()) + .len() + } else { + keys_before + }; + + info!("Sync completed: {} keys before, {} keys after", keys_before, keys_after); + Ok(keys_after) } #[derive(Debug)] @@ -59,9 +75,27 @@ enum UserEvent { TrayIconEvent, MenuEvent(tray_icon::menu::MenuEvent), ConfigFileChanged, + UpdateMenu, } -fn create_tray_icon(settings: &KhmSettings) -> (TrayIcon, MenuId, MenuId, MenuId) { +#[derive(Debug, Clone)] +struct SyncStatus { + last_sync_time: Option, + last_sync_keys: Option, + next_sync_in_seconds: Option, +} + +impl Default for SyncStatus { + fn default() -> Self { + Self { + last_sync_time: None, + last_sync_keys: None, + next_sync_in_seconds: None, + } + } +} + +fn create_tray_icon(settings: &KhmSettings, sync_status: &SyncStatus) -> (TrayIcon, MenuId, MenuId, MenuId) { // Create simple blue icon with "KHM" text representation let icon_data: Vec = (0..32*32).flat_map(|i| { let y = i / 32; @@ -76,7 +110,7 @@ fn create_tray_icon(settings: &KhmSettings) -> (TrayIcon, MenuId, MenuId, MenuId let icon = tray_icon::Icon::from_rgba(icon_data, 32, 32).unwrap(); let menu = Menu::new(); - // Show current configuration status + // Show current configuration status (static) let host_text = if settings.host.is_empty() { "Host: Not configured" } else { @@ -118,8 +152,16 @@ fn create_tray_icon(settings: &KhmSettings) -> (TrayIcon, MenuId, MenuId, MenuId let quit_id = quit_item.id().clone(); menu.append(&quit_item).unwrap(); + // Create initial tooltip + let mut tooltip = format!("KHM - SSH Key Manager\nHost: {}\nFlow: {}", settings.host, settings.flow); + if let Some(keys_count) = sync_status.last_sync_keys { + tooltip.push_str(&format!("\nLast sync: {} keys", keys_count)); + } else { + tooltip.push_str("\nLast sync: Never"); + } + let tray_icon = TrayIconBuilder::new() - .with_tooltip("KHM - SSH Key Manager") + .with_tooltip(&tooltip) .with_icon(icon) .with_menu(Box::new(menu)) .build() @@ -134,6 +176,7 @@ struct Application { quit_id: Option, sync_id: Option, settings: Arc>, + sync_status: Arc>, _debouncer: Option>, proxy: EventLoopProxy, auto_sync_handle: Option>, @@ -147,6 +190,7 @@ impl Application { quit_id: None, sync_id: None, settings: Arc::new(Mutex::new(load_settings())), + sync_status: Arc::new(Mutex::new(SyncStatus::default())), _debouncer: None, proxy, auto_sync_handle: None, @@ -158,7 +202,7 @@ impl Application { let settings = self.settings.lock().unwrap(); let menu = Menu::new(); - // Show current configuration status + // Show current configuration status (static) let host_text = if settings.host.is_empty() { "Host: Not configured" } else { @@ -246,6 +290,8 @@ impl Application { info!("Starting auto sync with interval {} minutes", settings.auto_sync_interval_minutes); let settings_clone = Arc::clone(&self.settings); + let sync_status_clone = Arc::clone(&self.sync_status); + let proxy_clone = self.proxy.clone(); let interval_minutes = settings.auto_sync_interval_minutes; let handle = std::thread::spawn(move || { @@ -255,14 +301,30 @@ impl Application { if !current_settings.host.is_empty() && !current_settings.flow.is_empty() { let rt = tokio::runtime::Runtime::new().unwrap(); rt.block_on(async { - if let Err(e) = perform_sync(¤t_settings).await { - error!("Initial sync failed: {}", e); - } else { - info!("Initial sync completed successfully"); + match perform_sync(¤t_settings).await { + Ok(keys_count) => { + info!("Initial sync completed successfully with {} keys", keys_count); + let mut status = sync_status_clone.lock().unwrap(); + status.last_sync_time = Some(std::time::Instant::now()); + status.last_sync_keys = Some(keys_count); + let _ = proxy_clone.send_event(UserEvent::UpdateMenu); + } + Err(e) => { + error!("Initial sync failed: {}", e); + } } }); } + // Start menu update timer + let proxy_timer = proxy_clone.clone(); + std::thread::spawn(move || { + loop { + std::thread::sleep(std::time::Duration::from_secs(1)); + let _ = proxy_timer.send_event(UserEvent::UpdateMenu); + } + }); + // Periodic sync loop { std::thread::sleep(std::time::Duration::from_secs(interval_minutes as u64 * 60)); @@ -276,10 +338,17 @@ impl Application { info!("Performing scheduled auto sync"); let rt = tokio::runtime::Runtime::new().unwrap(); rt.block_on(async { - if let Err(e) = perform_sync(¤t_settings).await { - error!("Auto sync failed: {}", e); - } else { - info!("Auto sync completed successfully"); + match perform_sync(¤t_settings).await { + Ok(keys_count) => { + info!("Auto sync completed successfully with {} keys", keys_count); + let mut status = sync_status_clone.lock().unwrap(); + status.last_sync_time = Some(std::time::Instant::now()); + status.last_sync_keys = Some(keys_count); + let _ = proxy_clone.send_event(UserEvent::UpdateMenu); + } + Err(e) => { + error!("Auto sync failed: {}", e); + } } }); } @@ -301,8 +370,10 @@ impl ApplicationHandler for Application { if self.tray_icon.is_none() { info!("Creating tray icon"); let settings = self.settings.lock().unwrap(); - let (tray_icon, settings_id, quit_id, sync_id) = create_tray_icon(&settings); + let sync_status = self.sync_status.lock().unwrap(); + let (tray_icon, settings_id, quit_id, sync_id) = create_tray_icon(&settings, &sync_status); drop(settings); + drop(sync_status); self.tray_icon = Some(tray_icon); self.settings_id = Some(settings_id); @@ -318,6 +389,47 @@ impl ApplicationHandler for Application { fn user_event(&mut self, event_loop: &winit::event_loop::ActiveEventLoop, event: UserEvent) { match event { UserEvent::TrayIconEvent => {} + UserEvent::UpdateMenu => { + // Update tooltip with sync status instead of menu items + let settings = self.settings.lock().unwrap(); + if !settings.host.is_empty() && !settings.flow.is_empty() && settings.in_place { + let mut sync_status = self.sync_status.lock().unwrap(); + if let Some(last_sync) = sync_status.last_sync_time { + let elapsed = last_sync.elapsed().as_secs(); + let interval_seconds = settings.auto_sync_interval_minutes as u64 * 60; + + if elapsed < interval_seconds { + sync_status.next_sync_in_seconds = Some(interval_seconds - elapsed); + } else { + sync_status.next_sync_in_seconds = Some(0); + } + } else { + sync_status.next_sync_in_seconds = None; + } + + // Update tooltip with current status + if let Some(tray_icon) = &self.tray_icon { + let mut tooltip = format!("KHM - SSH Key Manager\nHost: {}\nFlow: {}", settings.host, settings.flow); + + if let Some(keys_count) = sync_status.last_sync_keys { + tooltip.push_str(&format!("\nLast sync: {} keys", keys_count)); + } else { + tooltip.push_str("\nLast sync: Never"); + } + + if let Some(seconds) = sync_status.next_sync_in_seconds { + if seconds > 60 { + tooltip.push_str(&format!("\nNext sync: {}m {}s", seconds / 60, seconds % 60)); + } else { + tooltip.push_str(&format!("\nNext sync: {}s", seconds)); + } + } + + tray_icon.set_tooltip(Some(&tooltip)); + } + } + drop(settings); + } UserEvent::MenuEvent(event) => { if let (Some(settings_id), Some(quit_id), Some(sync_id)) = (&self.settings_id, &self.quit_id, &self.sync_id) { if event.id == *settings_id { @@ -339,6 +451,8 @@ impl ApplicationHandler for Application { } else if event.id == *sync_id { info!("Starting sync operation"); let settings = self.settings.lock().unwrap().clone(); + let sync_status_clone = Arc::clone(&self.sync_status); + let proxy_clone = self.proxy.clone(); // Check if settings are valid if settings.host.is_empty() || settings.flow.is_empty() { @@ -350,10 +464,17 @@ impl ApplicationHandler for Application { std::thread::spawn(move || { let rt = tokio::runtime::Runtime::new().unwrap(); rt.block_on(async { - if let Err(e) = perform_sync(&settings).await { - error!("Sync failed: {}", e); - } else { - info!("Sync completed successfully"); + match perform_sync(&settings).await { + Ok(keys_count) => { + info!("Sync completed successfully with {} keys", keys_count); + let mut status = sync_status_clone.lock().unwrap(); + status.last_sync_time = Some(std::time::Instant::now()); + status.last_sync_keys = Some(keys_count); + let _ = proxy_clone.send_event(UserEvent::UpdateMenu); + } + Err(e) => { + error!("Sync failed: {}", e); + } } }); }); @@ -370,6 +491,29 @@ impl ApplicationHandler for Application { *self.settings.lock().unwrap() = new_settings; self.update_menu(); + // Update tooltip with new settings + if let Some(tray_icon) = &self.tray_icon { + let settings = self.settings.lock().unwrap(); + let sync_status = self.sync_status.lock().unwrap(); + let mut tooltip = format!("KHM - SSH Key Manager\nHost: {}\nFlow: {}", settings.host, settings.flow); + + if let Some(keys_count) = sync_status.last_sync_keys { + tooltip.push_str(&format!("\nLast sync: {} keys", keys_count)); + } else { + tooltip.push_str("\nLast sync: Never"); + } + + if let Some(seconds) = sync_status.next_sync_in_seconds { + if seconds > 60 { + tooltip.push_str(&format!("\nNext sync: {}m {}s", seconds / 60, seconds % 60)); + } else { + tooltip.push_str(&format!("\nNext sync: {}s", seconds)); + } + } + + tray_icon.set_tooltip(Some(&tooltip)); + } + // Restart auto sync if interval changed or settings changed if old_interval != new_interval { info!("Auto sync interval changed from {} to {} minutes, restarting auto sync", old_interval, new_interval); diff --git a/src/main.rs b/src/main.rs index ac1755c..2f36397 100644 --- a/src/main.rs +++ b/src/main.rs @@ -11,7 +11,7 @@ use log::{error, info}; /// This application manages SSH keys and flows, either as a server or client. /// In server mode, it stores keys and flows in a PostgreSQL database. /// In client mode, it sends keys to the server and can update the known_hosts file with keys from the server. -#[derive(Parser, Debug)] +#[derive(Parser, Debug, Clone)] #[command( author = env!("CARGO_PKG_AUTHORS"), version = env!("CARGO_PKG_VERSION"), @@ -27,7 +27,7 @@ use log::{error, info}; \n\ " )] -struct Args { +pub struct Args { /// Run in server mode (default: false) #[arg(long, help = "Run in server mode")] server: bool,