works with macos native ui

This commit is contained in:
Ultradesu
2025-07-22 12:50:01 +03:00
parent af6c4d7e61
commit 07ff4454d2
8 changed files with 4438 additions and 67 deletions

70
src/gui/settings/cross.rs Normal file
View 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
View 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
View 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()
}