mirror of
https://github.com/house-of-vanity/khm.git
synced 2025-08-21 14:27:14 +00:00
works with macos native ui
This commit is contained in:
@@ -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(¤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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -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()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@@ -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;
|
||||||
|
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user