2025-07-22 12:50:01 +03:00
|
|
|
use log::{debug, error, info};
|
|
|
|
use notify::RecursiveMode;
|
|
|
|
use notify_debouncer_mini::{new_debouncer, DebounceEventResult};
|
|
|
|
use std::sync::{Arc, Mutex};
|
|
|
|
use std::time::Duration;
|
|
|
|
use tray_icon::{
|
|
|
|
menu::{Menu, MenuEvent, MenuItem, MenuId},
|
|
|
|
TrayIcon, TrayIconBuilder,
|
|
|
|
};
|
|
|
|
use winit::{
|
|
|
|
application::ApplicationHandler,
|
|
|
|
event_loop::{EventLoop, EventLoopProxy},
|
|
|
|
};
|
|
|
|
|
|
|
|
#[cfg(target_os = "macos")]
|
|
|
|
use winit::platform::macos::EventLoopBuilderExtMacOS;
|
|
|
|
|
|
|
|
mod settings;
|
|
|
|
pub use settings::{KhmSettings, load_settings};
|
|
|
|
|
|
|
|
// Function to run settings window (for --settings-ui mode)
|
|
|
|
pub fn run_settings_window() {
|
|
|
|
settings::run_settings_window();
|
|
|
|
}
|
|
|
|
|
|
|
|
// Function to perform sync operation using KHM client logic
|
|
|
|
async fn perform_sync(settings: &KhmSettings) -> std::io::Result<()> {
|
|
|
|
use crate::Args;
|
|
|
|
|
2025-07-22 12:50:56 +03:00
|
|
|
info!("Starting sync with settings: host={}, flow={}, known_hosts={}, in_place={}",
|
|
|
|
settings.host, settings.flow, settings.known_hosts, settings.in_place);
|
|
|
|
|
2025-07-22 12:50:01 +03:00
|
|
|
// Convert KhmSettings to Args for client module
|
|
|
|
let args = Args {
|
|
|
|
server: false,
|
|
|
|
gui: false,
|
|
|
|
settings_ui: false,
|
|
|
|
in_place: settings.in_place,
|
|
|
|
flows: vec!["default".to_string()], // Not used in client mode
|
|
|
|
ip: "127.0.0.1".to_string(), // Not used in client mode
|
|
|
|
port: 8080, // Not used in client mode
|
|
|
|
db_host: "127.0.0.1".to_string(), // Not used in client mode
|
|
|
|
db_name: "khm".to_string(), // Not used in client mode
|
|
|
|
db_user: None, // Not used in client mode
|
|
|
|
db_password: None, // Not used in client mode
|
|
|
|
host: Some(settings.host.clone()),
|
|
|
|
flow: Some(settings.flow.clone()),
|
|
|
|
known_hosts: settings::expand_path(&settings.known_hosts),
|
|
|
|
basic_auth: settings.basic_auth.clone(),
|
|
|
|
};
|
|
|
|
|
2025-07-22 12:50:56 +03:00
|
|
|
info!("Expanded known_hosts path: {}", args.known_hosts);
|
|
|
|
|
2025-07-22 12:50:01 +03:00
|
|
|
crate::client::run_client(args).await
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Debug)]
|
|
|
|
enum UserEvent {
|
|
|
|
TrayIconEvent,
|
|
|
|
MenuEvent(tray_icon::menu::MenuEvent),
|
|
|
|
ConfigFileChanged,
|
|
|
|
}
|
|
|
|
|
|
|
|
fn create_tray_icon(settings: &KhmSettings) -> (TrayIcon, MenuId, MenuId, MenuId) {
|
|
|
|
// Create simple blue icon with "KHM" text representation
|
|
|
|
let icon_data: Vec<u8> = (0..32*32).flat_map(|i| {
|
|
|
|
let y = i / 32;
|
|
|
|
let x = i % 32;
|
|
|
|
if x < 2 || x >= 30 || y < 2 || y >= 30 {
|
|
|
|
[255, 255, 255, 255] // White border
|
|
|
|
} else {
|
|
|
|
[64, 128, 255, 255] // Blue center
|
|
|
|
}
|
|
|
|
}).collect();
|
|
|
|
|
|
|
|
let icon = tray_icon::Icon::from_rgba(icon_data, 32, 32).unwrap();
|
|
|
|
let menu = Menu::new();
|
|
|
|
|
|
|
|
// Show current configuration status
|
|
|
|
let host_text = if settings.host.is_empty() {
|
|
|
|
"Host: Not configured"
|
|
|
|
} else {
|
|
|
|
&format!("Host: {}", settings.host)
|
|
|
|
};
|
|
|
|
menu.append(&MenuItem::new(host_text, false, None)).unwrap();
|
|
|
|
|
|
|
|
let flow_text = if settings.flow.is_empty() {
|
|
|
|
"Flow: Not configured"
|
|
|
|
} else {
|
|
|
|
&format!("Flow: {}", settings.flow)
|
|
|
|
};
|
|
|
|
menu.append(&MenuItem::new(flow_text, false, None)).unwrap();
|
|
|
|
|
2025-07-22 13:02:22 +03:00
|
|
|
let is_auto_sync_enabled = !settings.host.is_empty() && !settings.flow.is_empty() && settings.in_place;
|
|
|
|
let sync_text = format!("Auto sync: {} ({}min)",
|
|
|
|
if is_auto_sync_enabled { "On" } else { "Off" },
|
|
|
|
settings.auto_sync_interval_minutes);
|
2025-07-22 12:50:01 +03:00
|
|
|
menu.append(&MenuItem::new(&sync_text, false, None)).unwrap();
|
|
|
|
|
|
|
|
menu.append(&tray_icon::menu::PredefinedMenuItem::separator()).unwrap();
|
|
|
|
|
|
|
|
// Sync Now menu item
|
|
|
|
let sync_item = MenuItem::new("Sync Now", !settings.host.is_empty() && !settings.flow.is_empty(), None);
|
|
|
|
let sync_id = sync_item.id().clone();
|
|
|
|
menu.append(&sync_item).unwrap();
|
|
|
|
|
|
|
|
menu.append(&tray_icon::menu::PredefinedMenuItem::separator()).unwrap();
|
|
|
|
|
|
|
|
// Settings menu item
|
|
|
|
let settings_item = MenuItem::new("Settings", true, None);
|
|
|
|
let settings_id = settings_item.id().clone();
|
|
|
|
menu.append(&settings_item).unwrap();
|
|
|
|
|
|
|
|
menu.append(&tray_icon::menu::PredefinedMenuItem::separator()).unwrap();
|
|
|
|
|
|
|
|
// Quit menu item
|
|
|
|
let quit_item = MenuItem::new("Quit", true, None);
|
|
|
|
let quit_id = quit_item.id().clone();
|
|
|
|
menu.append(&quit_item).unwrap();
|
|
|
|
|
|
|
|
let tray_icon = TrayIconBuilder::new()
|
|
|
|
.with_tooltip("KHM - SSH Key Manager")
|
|
|
|
.with_icon(icon)
|
|
|
|
.with_menu(Box::new(menu))
|
|
|
|
.build()
|
|
|
|
.unwrap();
|
|
|
|
|
|
|
|
(tray_icon, settings_id, quit_id, sync_id)
|
|
|
|
}
|
|
|
|
|
|
|
|
struct Application {
|
|
|
|
tray_icon: Option<TrayIcon>,
|
|
|
|
settings_id: Option<MenuId>,
|
|
|
|
quit_id: Option<MenuId>,
|
|
|
|
sync_id: Option<MenuId>,
|
|
|
|
settings: Arc<Mutex<KhmSettings>>,
|
|
|
|
_debouncer: Option<notify_debouncer_mini::Debouncer<notify::FsEventWatcher>>,
|
|
|
|
proxy: EventLoopProxy<UserEvent>,
|
2025-07-22 13:02:22 +03:00
|
|
|
auto_sync_handle: Option<std::thread::JoinHandle<()>>,
|
2025-07-22 12:50:01 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
impl Application {
|
|
|
|
fn new(proxy: EventLoopProxy<UserEvent>) -> Self {
|
|
|
|
Self {
|
|
|
|
tray_icon: None,
|
|
|
|
settings_id: None,
|
|
|
|
quit_id: None,
|
|
|
|
sync_id: None,
|
|
|
|
settings: Arc::new(Mutex::new(load_settings())),
|
|
|
|
_debouncer: None,
|
|
|
|
proxy,
|
2025-07-22 13:02:22 +03:00
|
|
|
auto_sync_handle: None,
|
2025-07-22 12:50:01 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
fn update_menu(&mut self) {
|
|
|
|
if let Some(tray_icon) = &self.tray_icon {
|
|
|
|
let settings = self.settings.lock().unwrap();
|
|
|
|
let menu = Menu::new();
|
|
|
|
|
|
|
|
// Show current configuration status
|
|
|
|
let host_text = if settings.host.is_empty() {
|
|
|
|
"Host: Not configured"
|
|
|
|
} else {
|
|
|
|
&format!("Host: {}", settings.host)
|
|
|
|
};
|
|
|
|
menu.append(&MenuItem::new(host_text, false, None)).unwrap();
|
|
|
|
|
|
|
|
let flow_text = if settings.flow.is_empty() {
|
|
|
|
"Flow: Not configured"
|
|
|
|
} else {
|
|
|
|
&format!("Flow: {}", settings.flow)
|
|
|
|
};
|
|
|
|
menu.append(&MenuItem::new(flow_text, false, None)).unwrap();
|
|
|
|
|
2025-07-22 13:02:22 +03:00
|
|
|
let is_auto_sync_enabled = !settings.host.is_empty() && !settings.flow.is_empty() && settings.in_place;
|
|
|
|
let sync_text = format!("Auto sync: {} ({}min)",
|
|
|
|
if is_auto_sync_enabled { "On" } else { "Off" },
|
|
|
|
settings.auto_sync_interval_minutes);
|
2025-07-22 12:50:01 +03:00
|
|
|
menu.append(&MenuItem::new(&sync_text, false, None)).unwrap();
|
|
|
|
|
|
|
|
menu.append(&tray_icon::menu::PredefinedMenuItem::separator()).unwrap();
|
|
|
|
|
|
|
|
// Sync Now menu item
|
|
|
|
let sync_item = MenuItem::new("Sync Now", !settings.host.is_empty() && !settings.flow.is_empty(), None);
|
|
|
|
let new_sync_id = sync_item.id().clone();
|
|
|
|
menu.append(&sync_item).unwrap();
|
|
|
|
|
|
|
|
menu.append(&tray_icon::menu::PredefinedMenuItem::separator()).unwrap();
|
|
|
|
|
|
|
|
// Settings menu item
|
|
|
|
let settings_item = MenuItem::new("Settings", true, None);
|
|
|
|
let new_settings_id = settings_item.id().clone();
|
|
|
|
menu.append(&settings_item).unwrap();
|
|
|
|
|
|
|
|
menu.append(&tray_icon::menu::PredefinedMenuItem::separator()).unwrap();
|
|
|
|
|
|
|
|
// Quit menu item
|
|
|
|
let quit_item = MenuItem::new("Quit", true, None);
|
|
|
|
let new_quit_id = quit_item.id().clone();
|
|
|
|
menu.append(&quit_item).unwrap();
|
|
|
|
|
|
|
|
tray_icon.set_menu(Some(Box::new(menu)));
|
|
|
|
self.settings_id = Some(new_settings_id);
|
|
|
|
self.quit_id = Some(new_quit_id);
|
|
|
|
self.sync_id = Some(new_sync_id);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
fn setup_file_watcher(&mut self) {
|
|
|
|
let config_path = settings::get_config_path();
|
|
|
|
let (tx, rx) = std::sync::mpsc::channel::<DebounceEventResult>();
|
|
|
|
let proxy = self.proxy.clone();
|
|
|
|
|
|
|
|
std::thread::spawn(move || {
|
|
|
|
while let Ok(result) = rx.recv() {
|
|
|
|
if let Ok(events) = result {
|
|
|
|
if events.iter().any(|e| e.path.to_string_lossy().contains("khm_config.json")) {
|
|
|
|
let _ = proxy.send_event(UserEvent::ConfigFileChanged);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
if let Ok(mut debouncer) = new_debouncer(Duration::from_millis(500), tx) {
|
|
|
|
if let Some(config_dir) = config_path.parent() {
|
|
|
|
if debouncer.watcher().watch(config_dir, RecursiveMode::NonRecursive).is_ok() {
|
|
|
|
debug!("File watcher started");
|
|
|
|
self._debouncer = Some(debouncer);
|
|
|
|
} else {
|
|
|
|
error!("Failed to start file watcher");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2025-07-22 13:02:22 +03:00
|
|
|
|
|
|
|
fn start_auto_sync(&mut self) {
|
|
|
|
let settings = self.settings.lock().unwrap().clone();
|
|
|
|
|
|
|
|
// Only start auto sync if settings are valid and in_place is enabled
|
|
|
|
if settings.host.is_empty() || settings.flow.is_empty() || !settings.in_place {
|
|
|
|
info!("Auto sync disabled or settings invalid");
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
info!("Starting auto sync with interval {} minutes", settings.auto_sync_interval_minutes);
|
|
|
|
|
|
|
|
let settings_clone = Arc::clone(&self.settings);
|
|
|
|
let interval_minutes = settings.auto_sync_interval_minutes;
|
|
|
|
|
|
|
|
let handle = std::thread::spawn(move || {
|
|
|
|
// Initial sync on startup
|
|
|
|
info!("Performing initial sync on startup");
|
|
|
|
let current_settings = settings_clone.lock().unwrap().clone();
|
|
|
|
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");
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
// Periodic sync
|
|
|
|
loop {
|
|
|
|
std::thread::sleep(std::time::Duration::from_secs(interval_minutes as u64 * 60));
|
|
|
|
|
|
|
|
let current_settings = settings_clone.lock().unwrap().clone();
|
|
|
|
if current_settings.host.is_empty() || current_settings.flow.is_empty() || !current_settings.in_place {
|
|
|
|
info!("Auto sync stopped due to invalid settings or disabled in_place");
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
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");
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
self.auto_sync_handle = Some(handle);
|
|
|
|
}
|
2025-07-22 12:50:01 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
impl ApplicationHandler<UserEvent> for Application {
|
|
|
|
fn window_event(
|
|
|
|
&mut self,
|
|
|
|
_event_loop: &winit::event_loop::ActiveEventLoop,
|
|
|
|
_window_id: winit::window::WindowId,
|
|
|
|
_event: winit::event::WindowEvent,
|
|
|
|
) {}
|
|
|
|
|
|
|
|
fn resumed(&mut self, _event_loop: &winit::event_loop::ActiveEventLoop) {
|
|
|
|
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);
|
|
|
|
drop(settings);
|
|
|
|
|
|
|
|
self.tray_icon = Some(tray_icon);
|
|
|
|
self.settings_id = Some(settings_id);
|
|
|
|
self.quit_id = Some(quit_id);
|
|
|
|
self.sync_id = Some(sync_id);
|
|
|
|
|
|
|
|
self.setup_file_watcher();
|
2025-07-22 13:02:22 +03:00
|
|
|
self.start_auto_sync();
|
2025-07-22 12:50:01 +03:00
|
|
|
info!("KHM tray application ready");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
fn user_event(&mut self, event_loop: &winit::event_loop::ActiveEventLoop, event: UserEvent) {
|
|
|
|
match event {
|
|
|
|
UserEvent::TrayIconEvent => {}
|
|
|
|
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 {
|
|
|
|
info!("Settings menu clicked");
|
|
|
|
if let Ok(exe_path) = std::env::current_exe() {
|
|
|
|
std::thread::spawn(move || {
|
|
|
|
if let Err(e) = std::process::Command::new(&exe_path)
|
|
|
|
.arg("--gui")
|
|
|
|
.arg("--settings-ui")
|
|
|
|
.spawn()
|
|
|
|
{
|
|
|
|
error!("Failed to launch settings window: {}", e);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
} else if event.id == *quit_id {
|
|
|
|
info!("Quitting KHM application");
|
|
|
|
event_loop.exit();
|
|
|
|
} else if event.id == *sync_id {
|
|
|
|
info!("Starting sync operation");
|
|
|
|
let settings = self.settings.lock().unwrap().clone();
|
2025-07-22 12:50:56 +03:00
|
|
|
|
|
|
|
// Check if settings are valid
|
|
|
|
if settings.host.is_empty() || settings.flow.is_empty() {
|
|
|
|
error!("Cannot sync: host or flow not configured");
|
|
|
|
} else {
|
|
|
|
info!("Syncing with host: {}, flow: {}", settings.host, settings.flow);
|
|
|
|
|
|
|
|
// Run sync in separate thread with its own tokio runtime
|
|
|
|
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");
|
|
|
|
}
|
|
|
|
});
|
|
|
|
});
|
|
|
|
}
|
2025-07-22 12:50:01 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
UserEvent::ConfigFileChanged => {
|
|
|
|
debug!("Config file changed");
|
|
|
|
let new_settings = load_settings();
|
2025-07-22 13:02:22 +03:00
|
|
|
let old_interval = self.settings.lock().unwrap().auto_sync_interval_minutes;
|
|
|
|
let new_interval = new_settings.auto_sync_interval_minutes;
|
|
|
|
|
2025-07-22 12:50:01 +03:00
|
|
|
*self.settings.lock().unwrap() = new_settings;
|
|
|
|
self.update_menu();
|
2025-07-22 13:02:22 +03:00
|
|
|
|
|
|
|
// 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);
|
|
|
|
// Note: The auto sync thread will automatically stop and restart based on settings
|
|
|
|
self.start_auto_sync();
|
|
|
|
}
|
2025-07-22 12:50:01 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
async fn run_tray() -> std::io::Result<()> {
|
|
|
|
#[cfg(target_os = "macos")]
|
|
|
|
let event_loop = {
|
|
|
|
use winit::platform::macos::ActivationPolicy;
|
|
|
|
EventLoop::<UserEvent>::with_user_event()
|
|
|
|
.with_activation_policy(ActivationPolicy::Accessory)
|
|
|
|
.build()
|
|
|
|
.map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, format!("Failed to create event loop: {}", e)))?
|
|
|
|
};
|
|
|
|
|
|
|
|
#[cfg(not(target_os = "macos"))]
|
|
|
|
let event_loop = EventLoop::<UserEvent>::with_user_event().build()
|
|
|
|
.map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, format!("Failed to create event loop: {}", e)))?;
|
|
|
|
|
|
|
|
let proxy = event_loop.create_proxy();
|
|
|
|
|
|
|
|
let proxy_clone = proxy.clone();
|
|
|
|
tray_icon::TrayIconEvent::set_event_handler(Some(move |_event| {
|
|
|
|
let _ = proxy_clone.send_event(UserEvent::TrayIconEvent);
|
|
|
|
}));
|
|
|
|
|
|
|
|
let proxy_clone = proxy.clone();
|
|
|
|
MenuEvent::set_event_handler(Some(move |event: MenuEvent| {
|
|
|
|
let _ = proxy_clone.send_event(UserEvent::MenuEvent(event));
|
|
|
|
}));
|
|
|
|
|
|
|
|
let mut app = Application::new(proxy);
|
|
|
|
|
|
|
|
event_loop.run_app(&mut app)
|
|
|
|
.map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, format!("Event loop error: {:?}", e)))?;
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
|
|
|
pub async fn run_gui() -> std::io::Result<()> {
|
|
|
|
info!("Starting KHM tray application");
|
|
|
|
run_tray().await
|
|
|
|
}
|