mirror of
https://github.com/house-of-vanity/khm.git
synced 2025-08-22 14:37:15 +00:00
works with macos native ui
This commit is contained in:
70
src/gui/settings/cross.rs
Normal file
70
src/gui/settings/cross.rs
Normal file
@@ -0,0 +1,70 @@
|
||||
use super::{load_settings, save_settings, KhmSettings};
|
||||
use eframe::egui;
|
||||
use log::error;
|
||||
|
||||
struct KhmSettingsWindow {
|
||||
settings: KhmSettings,
|
||||
}
|
||||
|
||||
impl eframe::App for KhmSettingsWindow {
|
||||
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
|
||||
egui::CentralPanel::default().show(ctx, |ui| {
|
||||
ui.heading("KHM Settings");
|
||||
ui.separator();
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Host URL:");
|
||||
ui.text_edit_singleline(&mut self.settings.host);
|
||||
});
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Flow Name:");
|
||||
ui.text_edit_singleline(&mut self.settings.flow);
|
||||
});
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Known Hosts:");
|
||||
ui.text_edit_singleline(&mut self.settings.known_hosts);
|
||||
});
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Basic Auth:");
|
||||
ui.text_edit_singleline(&mut self.settings.basic_auth);
|
||||
});
|
||||
|
||||
ui.checkbox(&mut self.settings.in_place, "Update known_hosts file in-place after sync");
|
||||
|
||||
ui.separator();
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
if ui.button("Save").clicked() {
|
||||
if let Err(e) = save_settings(&self.settings) {
|
||||
error!("Failed to save KHM settings: {}", e);
|
||||
}
|
||||
ctx.send_viewport_cmd(egui::ViewportCommand::Close);
|
||||
}
|
||||
|
||||
if ui.button("Cancel").clicked() {
|
||||
ctx.send_viewport_cmd(egui::ViewportCommand::Close);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
pub fn run_settings_window() {
|
||||
let settings = load_settings();
|
||||
|
||||
let options = eframe::NativeOptions {
|
||||
viewport: egui::ViewportBuilder::default()
|
||||
.with_title("KHM Settings")
|
||||
.with_inner_size([450.0, 350.0]),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let _ = eframe::run_native(
|
||||
"KHM Settings",
|
||||
options,
|
||||
Box::new(|_cc| Ok(Box::new(KhmSettingsWindow { settings }))),
|
||||
);
|
||||
}
|
327
src/gui/settings/macos.rs
Normal file
327
src/gui/settings/macos.rs
Normal file
@@ -0,0 +1,327 @@
|
||||
#![allow(unexpected_cfgs)]
|
||||
use super::{load_settings, save_settings, KhmSettings};
|
||||
use cocoa::appkit::*;
|
||||
use cocoa::base::{id, nil, NO, YES};
|
||||
use cocoa::foundation::{NSAutoreleasePool, NSPoint, NSRect, NSSize, NSString, NSDefaultRunLoopMode};
|
||||
use log::{debug, error, info};
|
||||
use objc::{msg_send, sel, sel_impl};
|
||||
use std::ffi::CStr;
|
||||
|
||||
const WINDOW_WIDTH: f64 = 450.0;
|
||||
const WINDOW_HEIGHT: f64 = 350.0;
|
||||
const MARGIN: f64 = 20.0;
|
||||
const FIELD_HEIGHT: f64 = 24.0;
|
||||
const BUTTON_HEIGHT: f64 = 32.0;
|
||||
|
||||
// NSControl state constants
|
||||
const NS_CONTROL_STATE_VALUE_OFF: i32 = 0;
|
||||
const NS_CONTROL_STATE_VALUE_ON: i32 = 1;
|
||||
|
||||
// NSButton type constants
|
||||
const NS_SWITCH_BUTTON: u32 = 3;
|
||||
|
||||
struct MacOSKhmSettingsWindow {
|
||||
window: id,
|
||||
host_field: id,
|
||||
flow_field: id,
|
||||
known_hosts_field: id,
|
||||
basic_auth_field: id,
|
||||
in_place_checkbox: id,
|
||||
}
|
||||
|
||||
impl MacOSKhmSettingsWindow {
|
||||
fn new() -> Self {
|
||||
info!("Creating macOS KHM settings window");
|
||||
unsafe {
|
||||
let settings = load_settings();
|
||||
info!("KHM Settings loaded: host={}, flow={}", settings.host, settings.flow);
|
||||
|
||||
// Create window
|
||||
let window: id = msg_send![NSWindow::alloc(nil),
|
||||
initWithContentRect: NSRect::new(
|
||||
NSPoint::new(100.0, 100.0),
|
||||
NSSize::new(WINDOW_WIDTH, WINDOW_HEIGHT),
|
||||
)
|
||||
styleMask: NSWindowStyleMask::NSTitledWindowMask | NSWindowStyleMask::NSClosableWindowMask | NSWindowStyleMask::NSMiniaturizableWindowMask
|
||||
backing: NSBackingStoreType::NSBackingStoreBuffered
|
||||
defer: NO
|
||||
];
|
||||
info!("Window allocated and initialized");
|
||||
|
||||
let _: () = msg_send![window, setTitle: NSString::alloc(nil).init_str("KHM Settings")];
|
||||
let _: () = msg_send![window, center];
|
||||
let _: () = msg_send![window, setReleasedWhenClosed: NO];
|
||||
|
||||
let content_view: id = msg_send![window, contentView];
|
||||
|
||||
let mut current_y = WINDOW_HEIGHT - MARGIN - 30.0;
|
||||
|
||||
// Host label and field
|
||||
let host_label: id = msg_send![NSTextField::alloc(nil),
|
||||
initWithFrame: NSRect::new(
|
||||
NSPoint::new(MARGIN, current_y),
|
||||
NSSize::new(100.0, 20.0),
|
||||
)
|
||||
];
|
||||
let _: () = msg_send![host_label, setStringValue: NSString::alloc(nil).init_str("Host URL:")];
|
||||
let _: () = msg_send![host_label, setBezeled: NO];
|
||||
let _: () = msg_send![host_label, setDrawsBackground: NO];
|
||||
let _: () = msg_send![host_label, setEditable: NO];
|
||||
let _: () = msg_send![host_label, setSelectable: NO];
|
||||
let _: () = msg_send![content_view, addSubview: host_label];
|
||||
|
||||
let host_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![host_field, setStringValue: NSString::alloc(nil).init_str(&settings.host)];
|
||||
let _: () = msg_send![content_view, addSubview: host_field];
|
||||
|
||||
current_y -= 35.0;
|
||||
|
||||
// Flow label and field
|
||||
let flow_label: id = msg_send![NSTextField::alloc(nil),
|
||||
initWithFrame: NSRect::new(
|
||||
NSPoint::new(MARGIN, current_y),
|
||||
NSSize::new(100.0, 20.0),
|
||||
)
|
||||
];
|
||||
let _: () = msg_send![flow_label, setStringValue: NSString::alloc(nil).init_str("Flow Name:")];
|
||||
let _: () = msg_send![flow_label, setBezeled: NO];
|
||||
let _: () = msg_send![flow_label, setDrawsBackground: NO];
|
||||
let _: () = msg_send![flow_label, setEditable: NO];
|
||||
let _: () = msg_send![flow_label, setSelectable: NO];
|
||||
let _: () = msg_send![content_view, addSubview: flow_label];
|
||||
|
||||
let flow_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![flow_field, setStringValue: NSString::alloc(nil).init_str(&settings.flow)];
|
||||
let _: () = msg_send![content_view, addSubview: flow_field];
|
||||
|
||||
current_y -= 35.0;
|
||||
|
||||
// Known hosts label and field
|
||||
let known_hosts_label: id = msg_send![NSTextField::alloc(nil),
|
||||
initWithFrame: NSRect::new(
|
||||
NSPoint::new(MARGIN, current_y),
|
||||
NSSize::new(100.0, 20.0),
|
||||
)
|
||||
];
|
||||
let _: () = msg_send![known_hosts_label, setStringValue: NSString::alloc(nil).init_str("Known Hosts:")];
|
||||
let _: () = msg_send![known_hosts_label, setBezeled: NO];
|
||||
let _: () = msg_send![known_hosts_label, setDrawsBackground: NO];
|
||||
let _: () = msg_send![known_hosts_label, setEditable: NO];
|
||||
let _: () = msg_send![known_hosts_label, setSelectable: NO];
|
||||
let _: () = msg_send![content_view, addSubview: known_hosts_label];
|
||||
|
||||
let known_hosts_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![known_hosts_field, setStringValue: NSString::alloc(nil).init_str(&settings.known_hosts)];
|
||||
let _: () = msg_send![content_view, addSubview: known_hosts_field];
|
||||
|
||||
current_y -= 35.0;
|
||||
|
||||
// Basic auth label and field
|
||||
let basic_auth_label: id = msg_send![NSTextField::alloc(nil),
|
||||
initWithFrame: NSRect::new(
|
||||
NSPoint::new(MARGIN, current_y),
|
||||
NSSize::new(100.0, 20.0),
|
||||
)
|
||||
];
|
||||
let _: () = msg_send![basic_auth_label, setStringValue: NSString::alloc(nil).init_str("Basic Auth:")];
|
||||
let _: () = msg_send![basic_auth_label, setBezeled: NO];
|
||||
let _: () = msg_send![basic_auth_label, setDrawsBackground: NO];
|
||||
let _: () = msg_send![basic_auth_label, setEditable: NO];
|
||||
let _: () = msg_send![basic_auth_label, setSelectable: NO];
|
||||
let _: () = msg_send![content_view, addSubview: basic_auth_label];
|
||||
|
||||
let basic_auth_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![basic_auth_field, setStringValue: NSString::alloc(nil).init_str(&settings.basic_auth)];
|
||||
let _: () = msg_send![content_view, addSubview: basic_auth_field];
|
||||
|
||||
current_y -= 40.0;
|
||||
|
||||
// In place checkbox
|
||||
let in_place_checkbox: id = msg_send![NSButton::alloc(nil),
|
||||
initWithFrame: NSRect::new(
|
||||
NSPoint::new(MARGIN, current_y),
|
||||
NSSize::new(400.0, 24.0),
|
||||
)
|
||||
];
|
||||
let _: () = msg_send![in_place_checkbox, setButtonType: NS_SWITCH_BUTTON];
|
||||
let _: () = msg_send![in_place_checkbox, setTitle: NSString::alloc(nil).init_str("Update known_hosts file in-place after sync")];
|
||||
let _: () = msg_send![in_place_checkbox, setState: if settings.in_place { NS_CONTROL_STATE_VALUE_ON } else { NS_CONTROL_STATE_VALUE_OFF }];
|
||||
let _: () = msg_send![content_view, addSubview: in_place_checkbox];
|
||||
|
||||
// Save button
|
||||
let save_button: id = msg_send![NSButton::alloc(nil),
|
||||
initWithFrame: NSRect::new(
|
||||
NSPoint::new(WINDOW_WIDTH - 180.0, MARGIN),
|
||||
NSSize::new(80.0, BUTTON_HEIGHT),
|
||||
)
|
||||
];
|
||||
let _: () = msg_send![save_button, setTitle: NSString::alloc(nil).init_str("Save")];
|
||||
let _: () = msg_send![content_view, addSubview: save_button];
|
||||
|
||||
// Cancel button
|
||||
let cancel_button: id = msg_send![NSButton::alloc(nil),
|
||||
initWithFrame: NSRect::new(
|
||||
NSPoint::new(WINDOW_WIDTH - 90.0, MARGIN),
|
||||
NSSize::new(80.0, BUTTON_HEIGHT),
|
||||
)
|
||||
];
|
||||
let _: () = msg_send![cancel_button, setTitle: NSString::alloc(nil).init_str("Cancel")];
|
||||
let _: () = msg_send![content_view, addSubview: cancel_button];
|
||||
|
||||
info!("All KHM UI elements created successfully");
|
||||
|
||||
Self {
|
||||
window,
|
||||
host_field,
|
||||
flow_field,
|
||||
known_hosts_field,
|
||||
basic_auth_field,
|
||||
in_place_checkbox,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn collect_settings(&self) -> KhmSettings {
|
||||
unsafe {
|
||||
// Get host
|
||||
let host_ns_string: id = msg_send![self.host_field, stringValue];
|
||||
let host_ptr: *const i8 = msg_send![host_ns_string, UTF8String];
|
||||
let host = CStr::from_ptr(host_ptr).to_string_lossy().to_string();
|
||||
|
||||
// Get flow
|
||||
let flow_ns_string: id = msg_send![self.flow_field, stringValue];
|
||||
let flow_ptr: *const i8 = msg_send![flow_ns_string, UTF8String];
|
||||
let flow = CStr::from_ptr(flow_ptr).to_string_lossy().to_string();
|
||||
|
||||
// Get known hosts path
|
||||
let known_hosts_ns_string: id = msg_send![self.known_hosts_field, stringValue];
|
||||
let known_hosts_ptr: *const i8 = msg_send![known_hosts_ns_string, UTF8String];
|
||||
let known_hosts = CStr::from_ptr(known_hosts_ptr).to_string_lossy().to_string();
|
||||
|
||||
// Get basic auth
|
||||
let basic_auth_ns_string: id = msg_send![self.basic_auth_field, stringValue];
|
||||
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();
|
||||
|
||||
// Get 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;
|
||||
|
||||
KhmSettings {
|
||||
host,
|
||||
flow,
|
||||
known_hosts,
|
||||
basic_auth,
|
||||
in_place,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn run_settings_window() {
|
||||
info!("Starting native macOS KHM settings window");
|
||||
unsafe {
|
||||
let pool = NSAutoreleasePool::new(nil);
|
||||
let app = NSApp();
|
||||
info!("NSApp created for settings window");
|
||||
|
||||
// Set activation policy to regular for this standalone window
|
||||
let _: () = msg_send![app, setActivationPolicy: 0]; // NSApplicationActivationPolicyRegular
|
||||
info!("Activation policy set to Regular for settings window");
|
||||
|
||||
let settings_window = MacOSKhmSettingsWindow::new();
|
||||
let window = settings_window.window;
|
||||
info!("KHM settings window created");
|
||||
|
||||
// Show window and activate app
|
||||
let _: () = msg_send![app, activateIgnoringOtherApps: YES];
|
||||
let _: () = msg_send![window, makeKeyAndOrderFront: nil];
|
||||
let _: () = msg_send![window, orderFrontRegardless];
|
||||
info!("Settings window should be visible now");
|
||||
|
||||
// Run event loop until window is closed
|
||||
let mut should_close = false;
|
||||
while !should_close {
|
||||
let event: id = msg_send![app,
|
||||
nextEventMatchingMask: NSEventMask::NSAnyEventMask.bits()
|
||||
untilDate: nil
|
||||
inMode: NSDefaultRunLoopMode
|
||||
dequeue: YES
|
||||
];
|
||||
|
||||
if event == nil {
|
||||
continue;
|
||||
}
|
||||
|
||||
let event_type: NSEventType = msg_send![event, type];
|
||||
|
||||
// Handle window close button
|
||||
if event_type == NSEventType::NSLeftMouseDown {
|
||||
let event_window: id = msg_send![event, window];
|
||||
if event_window == window {
|
||||
let location: NSPoint = msg_send![event, locationInWindow];
|
||||
|
||||
// Check if click is on Save button
|
||||
if location.x >= WINDOW_WIDTH - 180.0 && location.x <= WINDOW_WIDTH - 100.0 &&
|
||||
location.y >= MARGIN && location.y <= MARGIN + BUTTON_HEIGHT {
|
||||
info!("Save button clicked");
|
||||
let settings = settings_window.collect_settings();
|
||||
if let Err(e) = save_settings(&settings) {
|
||||
error!("Failed to save KHM settings: {}", e);
|
||||
} else {
|
||||
info!("KHM settings saved from native macOS window");
|
||||
}
|
||||
should_close = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if click is on Cancel button
|
||||
if location.x >= WINDOW_WIDTH - 90.0 && location.x <= WINDOW_WIDTH - 10.0 &&
|
||||
location.y >= MARGIN && location.y <= MARGIN + BUTTON_HEIGHT {
|
||||
info!("Cancel button clicked");
|
||||
should_close = true;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check if window is closed via close button or ESC
|
||||
if event_type == NSEventType::NSKeyDown {
|
||||
let key_code: u16 = msg_send![event, keyCode];
|
||||
if key_code == 53 { // ESC key
|
||||
info!("ESC pressed, closing settings window");
|
||||
should_close = true;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Forward event to application
|
||||
let _: () = msg_send![app, sendEvent: event];
|
||||
}
|
||||
|
||||
let _: () = msg_send![window, close];
|
||||
info!("Native macOS KHM settings window closed");
|
||||
|
||||
pool.drain();
|
||||
}
|
||||
}
|
76
src/gui/settings/mod.rs
Normal file
76
src/gui/settings/mod.rs
Normal file
@@ -0,0 +1,76 @@
|
||||
use dirs::home_dir;
|
||||
use log::{debug, error, info};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct KhmSettings {
|
||||
pub host: String,
|
||||
pub flow: String,
|
||||
pub known_hosts: String,
|
||||
pub basic_auth: String,
|
||||
pub in_place: bool,
|
||||
}
|
||||
|
||||
impl Default for KhmSettings {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
host: String::new(),
|
||||
flow: String::new(),
|
||||
known_hosts: "~/.ssh/known_hosts".to_string(),
|
||||
basic_auth: String::new(),
|
||||
in_place: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_config_path() -> PathBuf {
|
||||
let mut path = home_dir().expect("Could not find home directory");
|
||||
path.push(".khm");
|
||||
fs::create_dir_all(&path).ok();
|
||||
path.push("khm_config.json");
|
||||
path
|
||||
}
|
||||
|
||||
pub fn load_settings() -> KhmSettings {
|
||||
let path = get_config_path();
|
||||
match fs::read_to_string(&path) {
|
||||
Ok(contents) => serde_json::from_str(&contents).unwrap_or_else(|e| {
|
||||
error!("Failed to parse KHM config: {}", e);
|
||||
KhmSettings::default()
|
||||
}),
|
||||
Err(_) => {
|
||||
debug!("KHM config file not found, using defaults");
|
||||
KhmSettings::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn save_settings(settings: &KhmSettings) -> Result<(), std::io::Error> {
|
||||
let path = get_config_path();
|
||||
let json = serde_json::to_string_pretty(settings)?;
|
||||
fs::write(&path, json)?;
|
||||
info!("KHM settings saved");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
mod macos;
|
||||
#[cfg(target_os = "macos")]
|
||||
pub use macos::run_settings_window;
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
mod cross;
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
pub use cross::run_settings_window;
|
||||
|
||||
// Helper function to expand tilde in path
|
||||
pub fn expand_path(path: &str) -> String {
|
||||
if path.starts_with("~/") {
|
||||
if let Some(home) = home_dir() {
|
||||
return home.join(&path[2..]).to_string_lossy().to_string();
|
||||
}
|
||||
}
|
||||
path.to_string()
|
||||
}
|
Reference in New Issue
Block a user