mirror of
https://github.com/house-of-vanity/khm.git
synced 2025-08-22 06:27:15 +00:00
332 lines
12 KiB
Rust
332 lines
12 KiB
Rust
![]() |
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;
|
||
|
|
||
|
// 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(),
|
||
|
};
|
||
|
|
||
|
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();
|
||
|
|
||
|
let sync_text = format!("Auto sync: {}", if settings.in_place { "On" } else { "Off" });
|
||
|
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>,
|
||
|
}
|
||
|
|
||
|
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,
|
||
|
}
|
||
|
}
|
||
|
|
||
|
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();
|
||
|
|
||
|
let sync_text = format!("Auto sync: {}", if settings.in_place { "On" } else { "Off" });
|
||
|
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");
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
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();
|
||
|
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();
|
||
|
tokio::spawn(async move {
|
||
|
if let Err(e) = perform_sync(&settings).await {
|
||
|
error!("Sync failed: {}", e);
|
||
|
} else {
|
||
|
info!("Sync completed successfully");
|
||
|
}
|
||
|
});
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
UserEvent::ConfigFileChanged => {
|
||
|
debug!("Config file changed");
|
||
|
let new_settings = load_settings();
|
||
|
*self.settings.lock().unwrap() = new_settings;
|
||
|
self.update_menu();
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
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
|
||
|
}
|