works with macos native ui

This commit is contained in:
Ultradesu
2025-07-22 13:02:22 +03:00
parent 9d3d52f38a
commit a3a96eebce
4 changed files with 171 additions and 4 deletions

View File

@@ -91,7 +91,10 @@ fn create_tray_icon(settings: &KhmSettings) -> (TrayIcon, MenuId, MenuId, MenuId
}; };
menu.append(&MenuItem::new(flow_text, false, None)).unwrap(); menu.append(&MenuItem::new(flow_text, false, None)).unwrap();
let sync_text = format!("Auto sync: {}", if settings.in_place { "On" } else { "Off" }); 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);
menu.append(&MenuItem::new(&sync_text, false, None)).unwrap(); menu.append(&MenuItem::new(&sync_text, false, None)).unwrap();
menu.append(&tray_icon::menu::PredefinedMenuItem::separator()).unwrap(); menu.append(&tray_icon::menu::PredefinedMenuItem::separator()).unwrap();
@@ -133,6 +136,7 @@ struct Application {
settings: Arc<Mutex<KhmSettings>>, settings: Arc<Mutex<KhmSettings>>,
_debouncer: Option<notify_debouncer_mini::Debouncer<notify::FsEventWatcher>>, _debouncer: Option<notify_debouncer_mini::Debouncer<notify::FsEventWatcher>>,
proxy: EventLoopProxy<UserEvent>, proxy: EventLoopProxy<UserEvent>,
auto_sync_handle: Option<std::thread::JoinHandle<()>>,
} }
impl Application { impl Application {
@@ -145,6 +149,7 @@ impl Application {
settings: Arc::new(Mutex::new(load_settings())), settings: Arc::new(Mutex::new(load_settings())),
_debouncer: None, _debouncer: None,
proxy, proxy,
auto_sync_handle: None,
} }
} }
@@ -168,7 +173,10 @@ impl Application {
}; };
menu.append(&MenuItem::new(flow_text, false, None)).unwrap(); menu.append(&MenuItem::new(flow_text, false, None)).unwrap();
let sync_text = format!("Auto sync: {}", if settings.in_place { "On" } else { "Off" }); 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);
menu.append(&MenuItem::new(&sync_text, false, None)).unwrap(); menu.append(&MenuItem::new(&sync_text, false, None)).unwrap();
menu.append(&tray_icon::menu::PredefinedMenuItem::separator()).unwrap(); menu.append(&tray_icon::menu::PredefinedMenuItem::separator()).unwrap();
@@ -225,6 +233,60 @@ impl Application {
} }
} }
} }
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(&current_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(&current_settings).await {
error!("Auto sync failed: {}", e);
} else {
info!("Auto sync completed successfully");
}
});
}
});
self.auto_sync_handle = Some(handle);
}
} }
impl ApplicationHandler<UserEvent> for Application { impl ApplicationHandler<UserEvent> for Application {
@@ -248,6 +310,7 @@ impl ApplicationHandler<UserEvent> for Application {
self.sync_id = Some(sync_id); self.sync_id = Some(sync_id);
self.setup_file_watcher(); self.setup_file_watcher();
self.start_auto_sync();
info!("KHM tray application ready"); info!("KHM tray application ready");
} }
} }
@@ -301,8 +364,18 @@ impl ApplicationHandler<UserEvent> for Application {
UserEvent::ConfigFileChanged => { UserEvent::ConfigFileChanged => {
debug!("Config file changed"); debug!("Config file changed");
let new_settings = load_settings(); let new_settings = load_settings();
let old_interval = self.settings.lock().unwrap().auto_sync_interval_minutes;
let new_interval = new_settings.auto_sync_interval_minutes;
*self.settings.lock().unwrap() = new_settings; *self.settings.lock().unwrap() = new_settings;
self.update_menu(); self.update_menu();
// 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();
}
} }
} }
} }

View File

@@ -32,6 +32,11 @@ impl eframe::App for KhmSettingsWindow {
ui.text_edit_singleline(&mut self.settings.basic_auth); ui.text_edit_singleline(&mut self.settings.basic_auth);
}); });
ui.horizontal(|ui| {
ui.label("Auto sync interval (minutes):");
ui.add(egui::DragValue::new(&mut self.settings.auto_sync_interval_minutes).range(5..=1440));
});
ui.checkbox(&mut self.settings.in_place, "Update known_hosts file in-place after sync"); ui.checkbox(&mut self.settings.in_place, "Update known_hosts file in-place after sync");
ui.separator(); ui.separator();
@@ -58,7 +63,7 @@ pub fn run_settings_window() {
let options = eframe::NativeOptions { let options = eframe::NativeOptions {
viewport: egui::ViewportBuilder::default() viewport: egui::ViewportBuilder::default()
.with_title("KHM Settings") .with_title("KHM Settings")
.with_inner_size([450.0, 350.0]), .with_inner_size([450.0, 385.0]),
..Default::default() ..Default::default()
}; };

View File

@@ -7,8 +7,12 @@ use log::{debug, error, info};
use objc::{msg_send, sel, sel_impl}; use objc::{msg_send, sel, sel_impl};
use std::ffi::CStr; use std::ffi::CStr;
// NSTextFieldBezelStyle constants
#[allow(non_upper_case_globals)]
const NSTextFieldSquareBezel: u32 = 0;
const WINDOW_WIDTH: f64 = 450.0; const WINDOW_WIDTH: f64 = 450.0;
const WINDOW_HEIGHT: f64 = 350.0; const WINDOW_HEIGHT: f64 = 385.0;
const MARGIN: f64 = 20.0; const MARGIN: f64 = 20.0;
const FIELD_HEIGHT: f64 = 24.0; const FIELD_HEIGHT: f64 = 24.0;
const BUTTON_HEIGHT: f64 = 32.0; const BUTTON_HEIGHT: f64 = 32.0;
@@ -26,6 +30,7 @@ struct MacOSKhmSettingsWindow {
flow_field: id, flow_field: id,
known_hosts_field: id, known_hosts_field: id,
basic_auth_field: id, basic_auth_field: id,
auto_sync_field: id,
in_place_checkbox: id, in_place_checkbox: id,
} }
@@ -77,6 +82,10 @@ impl MacOSKhmSettingsWindow {
) )
]; ];
let _: () = msg_send![host_field, setStringValue: NSString::alloc(nil).init_str(&settings.host)]; let _: () = msg_send![host_field, setStringValue: NSString::alloc(nil).init_str(&settings.host)];
let _: () = msg_send![host_field, setEditable: YES];
let _: () = msg_send![host_field, setSelectable: YES];
let _: () = msg_send![host_field, setBezeled: YES];
let _: () = msg_send![host_field, setBezelStyle: NSTextFieldSquareBezel];
let _: () = msg_send![content_view, addSubview: host_field]; let _: () = msg_send![content_view, addSubview: host_field];
current_y -= 35.0; current_y -= 35.0;
@@ -102,6 +111,10 @@ impl MacOSKhmSettingsWindow {
) )
]; ];
let _: () = msg_send![flow_field, setStringValue: NSString::alloc(nil).init_str(&settings.flow)]; let _: () = msg_send![flow_field, setStringValue: NSString::alloc(nil).init_str(&settings.flow)];
let _: () = msg_send![flow_field, setEditable: YES];
let _: () = msg_send![flow_field, setSelectable: YES];
let _: () = msg_send![flow_field, setBezeled: YES];
let _: () = msg_send![flow_field, setBezelStyle: NSTextFieldSquareBezel];
let _: () = msg_send![content_view, addSubview: flow_field]; let _: () = msg_send![content_view, addSubview: flow_field];
current_y -= 35.0; current_y -= 35.0;
@@ -127,6 +140,10 @@ impl MacOSKhmSettingsWindow {
) )
]; ];
let _: () = msg_send![known_hosts_field, setStringValue: NSString::alloc(nil).init_str(&settings.known_hosts)]; let _: () = msg_send![known_hosts_field, setStringValue: NSString::alloc(nil).init_str(&settings.known_hosts)];
let _: () = msg_send![known_hosts_field, setEditable: YES];
let _: () = msg_send![known_hosts_field, setSelectable: YES];
let _: () = msg_send![known_hosts_field, setBezeled: YES];
let _: () = msg_send![known_hosts_field, setBezelStyle: NSTextFieldSquareBezel];
let _: () = msg_send![content_view, addSubview: known_hosts_field]; let _: () = msg_send![content_view, addSubview: known_hosts_field];
current_y -= 35.0; current_y -= 35.0;
@@ -152,8 +169,41 @@ impl MacOSKhmSettingsWindow {
) )
]; ];
let _: () = msg_send![basic_auth_field, setStringValue: NSString::alloc(nil).init_str(&settings.basic_auth)]; let _: () = msg_send![basic_auth_field, setStringValue: NSString::alloc(nil).init_str(&settings.basic_auth)];
let _: () = msg_send![basic_auth_field, setEditable: YES];
let _: () = msg_send![basic_auth_field, setSelectable: YES];
let _: () = msg_send![basic_auth_field, setBezeled: YES];
let _: () = msg_send![basic_auth_field, setBezelStyle: NSTextFieldSquareBezel];
let _: () = msg_send![content_view, addSubview: basic_auth_field]; let _: () = msg_send![content_view, addSubview: basic_auth_field];
current_y -= 35.0;
// Auto sync interval label and field
let auto_sync_label: id = msg_send![NSTextField::alloc(nil),
initWithFrame: NSRect::new(
NSPoint::new(MARGIN, current_y),
NSSize::new(100.0, 20.0),
)
];
let _: () = msg_send![auto_sync_label, setStringValue: NSString::alloc(nil).init_str("Auto sync (min):")];
let _: () = msg_send![auto_sync_label, setBezeled: NO];
let _: () = msg_send![auto_sync_label, setDrawsBackground: NO];
let _: () = msg_send![auto_sync_label, setEditable: NO];
let _: () = msg_send![auto_sync_label, setSelectable: NO];
let _: () = msg_send![content_view, addSubview: auto_sync_label];
let auto_sync_field: id = msg_send![NSTextField::alloc(nil),
initWithFrame: NSRect::new(
NSPoint::new(MARGIN + 110.0, current_y),
NSSize::new(310.0, FIELD_HEIGHT),
)
];
let _: () = msg_send![auto_sync_field, setStringValue: NSString::alloc(nil).init_str(&settings.auto_sync_interval_minutes.to_string())];
let _: () = msg_send![auto_sync_field, setEditable: YES];
let _: () = msg_send![auto_sync_field, setSelectable: YES];
let _: () = msg_send![auto_sync_field, setBezeled: YES];
let _: () = msg_send![auto_sync_field, setBezelStyle: NSTextFieldSquareBezel];
let _: () = msg_send![content_view, addSubview: auto_sync_field];
current_y -= 40.0; current_y -= 40.0;
// In place checkbox // In place checkbox
@@ -196,6 +246,7 @@ impl MacOSKhmSettingsWindow {
flow_field, flow_field,
known_hosts_field, known_hosts_field,
basic_auth_field, basic_auth_field,
auto_sync_field,
in_place_checkbox, in_place_checkbox,
} }
} }
@@ -223,6 +274,12 @@ impl MacOSKhmSettingsWindow {
let basic_auth_ptr: *const i8 = msg_send![basic_auth_ns_string, UTF8String]; let basic_auth_ptr: *const i8 = msg_send![basic_auth_ns_string, UTF8String];
let basic_auth = CStr::from_ptr(basic_auth_ptr).to_string_lossy().to_string(); let basic_auth = CStr::from_ptr(basic_auth_ptr).to_string_lossy().to_string();
// Get auto sync interval
let auto_sync_ns_string: id = msg_send![self.auto_sync_field, stringValue];
let auto_sync_ptr: *const i8 = msg_send![auto_sync_ns_string, UTF8String];
let auto_sync_str = CStr::from_ptr(auto_sync_ptr).to_string_lossy().to_string();
let auto_sync_interval_minutes = auto_sync_str.parse::<u32>().unwrap_or(60); // Default to 60 if parse fails
// Get checkbox state // Get checkbox state
let in_place_state: i32 = msg_send![self.in_place_checkbox, state]; let in_place_state: i32 = msg_send![self.in_place_checkbox, state];
let in_place = in_place_state == NS_CONTROL_STATE_VALUE_ON; let in_place = in_place_state == NS_CONTROL_STATE_VALUE_ON;
@@ -233,6 +290,7 @@ impl MacOSKhmSettingsWindow {
known_hosts, known_hosts,
basic_auth, basic_auth,
in_place, in_place,
auto_sync_interval_minutes,
} }
} }
} }
@@ -308,6 +366,35 @@ pub fn run_settings_window() {
// Check if window is closed via close button or ESC // Check if window is closed via close button or ESC
if event_type == NSEventType::NSKeyDown { if event_type == NSEventType::NSKeyDown {
let key_code: u16 = msg_send![event, keyCode]; let key_code: u16 = msg_send![event, keyCode];
let flags: NSEventModifierFlags = msg_send![event, modifierFlags];
// Handle Cmd+V (paste), Cmd+C (copy), Cmd+X (cut), Cmd+A (select all)
if flags.contains(NSEventModifierFlags::NSCommandKeyMask) {
match key_code {
9 => { // V key - paste
let responder: id = msg_send![window, firstResponder];
let _: () = msg_send![responder, paste: nil];
continue;
}
8 => { // C key - copy
let responder: id = msg_send![window, firstResponder];
let _: () = msg_send![responder, copy: nil];
continue;
}
7 => { // X key - cut
let responder: id = msg_send![window, firstResponder];
let _: () = msg_send![responder, cut: nil];
continue;
}
0 => { // A key - select all
let responder: id = msg_send![window, firstResponder];
let _: () = msg_send![responder, selectAll: nil];
continue;
}
_ => {}
}
}
if key_code == 53 { // ESC key if key_code == 53 { // ESC key
info!("ESC pressed, closing settings window"); info!("ESC pressed, closing settings window");
should_close = true; should_close = true;

View File

@@ -11,6 +11,7 @@ pub struct KhmSettings {
pub known_hosts: String, pub known_hosts: String,
pub basic_auth: String, pub basic_auth: String,
pub in_place: bool, pub in_place: bool,
pub auto_sync_interval_minutes: u32,
} }
impl Default for KhmSettings { impl Default for KhmSettings {
@@ -21,6 +22,7 @@ impl Default for KhmSettings {
known_hosts: "~/.ssh/known_hosts".to_string(), known_hosts: "~/.ssh/known_hosts".to_string(),
basic_auth: String::new(), basic_auth: String::new(),
in_place: false, in_place: false,
auto_sync_interval_minutes: 60, // Default to 1 hour
} }
} }
} }