/
pushamp
/
PACman
Обзор
Документация
Войти
/
pushamp
/
PACman
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
master
src/settings_window.rs
487 строк
16 KB
Dmitry Zhiltsov
feat(core): Init commit
19 фев 2026, 18:01
19 фев 2026, 18:01
1fadda9
Код
Авторство
О чём код?
use std::cell::OnceCell; use std::fmt; use objc2::rc::Retained; use objc2::runtime::AnyObject; use objc2::sel; use objc2::{define_class, msg_send, DefinedClass, MainThreadOnly}; use objc2_app_kit::{ NSBackingStoreType, NSBorderType, NSButton, NSFont, NSSlider, NSTextField, NSTextView, NSWindow, NSWindowStyleMask, }; use objc2_foundation::{ ns_string, MainThreadMarker, NSObject, NSObjectProtocol, NSPoint, NSRect, NSSize, NSString, }; use crate::settings::SharedConfig; const WIN_WIDTH: f64 = 480.0; const WIN_HEIGHT: f64 = 560.0; const PADDING: f64 = 20.0; const LABEL_HEIGHT: f64 = 17.0; const FIELD_HEIGHT: f64 = 22.0; const ROW_GAP: f64 = 8.0; const SECTION_GAP: f64 = 16.0; const FIELD_WIDTH: f64 = WIN_WIDTH - PADDING * 2.0; struct SettingsWindowIvars { config: OnceCell<SharedConfig>, vpn_interface_field: OnceCell<Retained<NSTextField>>, network_service_field: OnceCell<Retained<NSTextField>>, poll_interval_slider: OnceCell<Retained<NSSlider>>, poll_interval_label: OnceCell<Retained<NSTextField>>, port_field: OnceCell<Retained<NSTextField>>, pac_text_view: OnceCell<Retained<NSTextView>>, launch_checkbox: OnceCell<Retained<NSButton>>, } impl fmt::Debug for SettingsWindowIvars { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("SettingsWindowIvars").finish() } } impl Default for SettingsWindowIvars { fn default() -> Self { Self { config: OnceCell::new(), vpn_interface_field: OnceCell::new(), network_service_field: OnceCell::new(), poll_interval_slider: OnceCell::new(), poll_interval_label: OnceCell::new(), port_field: OnceCell::new(), pac_text_view: OnceCell::new(), launch_checkbox: OnceCell::new(), } } } define_class!( #[unsafe(super = NSObject)] #[thread_kind = MainThreadOnly] #[ivars = SettingsWindowIvars] #[derive(Debug)] struct SettingsController; unsafe impl NSObjectProtocol for SettingsController {} impl SettingsController { #[unsafe(method(saveSettings:))] fn save_settings(&self, _sender: *mut NSObject) { let ivars = self.ivars(); let config = ivars.config.get().unwrap(); let vpn_interface = ivars .vpn_interface_field .get() .unwrap() .stringValue() .to_string(); let network_service = ivars .network_service_field .get() .unwrap() .stringValue() .to_string(); let poll_interval = ivars.poll_interval_slider.get().unwrap().doubleValue() as u64; let port_str = ivars.port_field.get().unwrap().stringValue().to_string(); let http_port: u16 = port_str.parse().unwrap_or(8888); let pac_content = ivars.pac_text_view.get().unwrap().string().to_string(); let launch_at_login = ivars.launch_checkbox.get().unwrap().state() != 0; let old_port = crate::pac_server::actual_port(); let port_changed = http_port != old_port; { let mut cfg = config.write().unwrap(); // Log changed settings let mut changes = Vec::new(); if cfg.vpn_interface != vpn_interface { changes.push(format!( "vpn_interface: '{}' → '{}'", cfg.vpn_interface, vpn_interface )); } if cfg.network_service != network_service { changes.push(format!( "network_service: '{}' → '{}'", cfg.network_service, network_service )); } let new_poll = poll_interval.clamp(5, 300); if cfg.poll_interval_secs != new_poll { changes.push(format!( "poll_interval: {}s → {}s", cfg.poll_interval_secs, new_poll )); } if cfg.http_port != http_port { changes.push(format!( "http_port: {} → {}", cfg.http_port, http_port )); } if cfg.pac_content != pac_content { changes.push("pac_content: changed".to_string()); } if cfg.launch_at_login != launch_at_login { changes.push(format!( "launch_at_login: {} → {}", cfg.launch_at_login, launch_at_login )); } if changes.is_empty() { crate::logging::log_info("Settings saved (no changes)"); } else { crate::logging::log_info(&format!( "Settings changed: {}", changes.join(", ") )); } cfg.vpn_interface = vpn_interface; cfg.network_service = network_service.clone(); cfg.poll_interval_secs = new_poll; cfg.http_port = http_port; cfg.pac_content = pac_content; cfg.launch_at_login = launch_at_login; if let Err(e) = cfg.save() { crate::logging::log_error(&format!("Save failed: {e}")); } } // Restart PAC server and update proxy if port changed if port_changed { let new_port = crate::pac_server::restart_pac_server(config.clone()); if new_port != 0 { let mtm = self.mtm(); crate::app::update_pac_url_in_menu(mtm, new_port); // Re-apply proxy settings if VPN is currently connected let pac_url = crate::pac_url(new_port); if let Err(e) = crate::proxy::enable_proxy(&network_service, &pac_url) { crate::logging::log_error(&format!( "Failed to update proxy after port change: {e}" )); } } } // Handle launch agent let cfg = config.read().unwrap(); if cfg.launch_at_login { install_launch_agent(); } else { remove_launch_agent(); } } #[unsafe(method(sliderChanged:))] fn slider_changed(&self, _sender: *mut NSObject) { let ivars = self.ivars(); if let (Some(slider), Some(label)) = ( ivars.poll_interval_slider.get(), ivars.poll_interval_label.get(), ) { let val = slider.doubleValue() as u64; label.setStringValue(&NSString::from_str(&format!("{val}s"))); } } } ); impl SettingsController { fn new(mtm: MainThreadMarker) -> Retained<Self> { let this = Self::alloc(mtm).set_ivars(SettingsWindowIvars::default()); unsafe { msg_send![super(this), init] } } } pub fn create_settings_window( mtm: MainThreadMarker, config: SharedConfig, ) -> Retained<NSWindow> { let controller = SettingsController::new(mtm); controller.ivars().config.set(config.clone()).unwrap(); let window = unsafe { NSWindow::initWithContentRect_styleMask_backing_defer( NSWindow::alloc(mtm), NSRect::new(NSPoint::new(0.0, 0.0), NSSize::new(WIN_WIDTH, WIN_HEIGHT)), NSWindowStyleMask::Titled | NSWindowStyleMask::Closable | NSWindowStyleMask::Miniaturizable, NSBackingStoreType::Buffered, false, ) }; unsafe { window.setReleasedWhenClosed(false) }; window.setTitle(ns_string!("PACman \u{2014} Settings")); window.center(); let content = window.contentView().unwrap(); let cfg = config.read().unwrap(); let mut y = WIN_HEIGHT - PADDING - LABEL_HEIGHT; // === VPN Section === let section_label = NSTextField::labelWithString(ns_string!("VPN"), mtm); section_label.setFont(Some(&NSFont::boldSystemFontOfSize(13.0))); section_label.setFrame(NSRect::new( NSPoint::new(PADDING, y), NSSize::new(FIELD_WIDTH, LABEL_HEIGHT), )); content.addSubview(§ion_label); y -= ROW_GAP + LABEL_HEIGHT; // VPN Interface let label = NSTextField::labelWithString(ns_string!("VPN Interface:"), mtm); label.setFrame(NSRect::new( NSPoint::new(PADDING, y), NSSize::new(FIELD_WIDTH, LABEL_HEIGHT), )); content.addSubview(&label); y -= FIELD_HEIGHT; let vpn_field = NSTextField::textFieldWithString(&NSString::from_str(&cfg.vpn_interface), mtm); vpn_field.setFrame(NSRect::new( NSPoint::new(PADDING, y), NSSize::new(FIELD_WIDTH, FIELD_HEIGHT), )); content.addSubview(&vpn_field); controller .ivars() .vpn_interface_field .set(vpn_field) .unwrap(); y -= ROW_GAP + LABEL_HEIGHT; // Network Service let label = NSTextField::labelWithString(ns_string!("Network Service:"), mtm); label.setFrame(NSRect::new( NSPoint::new(PADDING, y), NSSize::new(FIELD_WIDTH, LABEL_HEIGHT), )); content.addSubview(&label); y -= FIELD_HEIGHT; let svc_field = NSTextField::textFieldWithString(&NSString::from_str(&cfg.network_service), mtm); svc_field.setFrame(NSRect::new( NSPoint::new(PADDING, y), NSSize::new(FIELD_WIDTH, FIELD_HEIGHT), )); content.addSubview(&svc_field); controller .ivars() .network_service_field .set(svc_field) .unwrap(); y -= ROW_GAP + LABEL_HEIGHT; // Poll Interval let label = NSTextField::labelWithString(ns_string!("Poll Interval:"), mtm); label.setFrame(NSRect::new( NSPoint::new(PADDING, y), NSSize::new(150.0, LABEL_HEIGHT), )); content.addSubview(&label); let interval_label = NSTextField::labelWithString( &NSString::from_str(&format!("{}s", cfg.poll_interval_secs)), mtm, ); interval_label.setFrame(NSRect::new( NSPoint::new(WIN_WIDTH - PADDING - 50.0, y), NSSize::new(50.0, LABEL_HEIGHT), )); content.addSubview(&interval_label); controller .ivars() .poll_interval_label .set(interval_label) .unwrap(); y -= FIELD_HEIGHT; let slider = { let s = NSSlider::initWithFrame( NSSlider::alloc(mtm), NSRect::new( NSPoint::new(PADDING, y), NSSize::new(FIELD_WIDTH, FIELD_HEIGHT), ), ); s.setMinValue(5.0); s.setMaxValue(300.0); s.setDoubleValue(cfg.poll_interval_secs as f64); s.setContinuous(true); unsafe { s.setTarget(Some(controller.as_ref() as &AnyObject)); s.setAction(Some(sel!(sliderChanged:))); } s }; content.addSubview(&slider); controller.ivars().poll_interval_slider.set(slider).unwrap(); y -= SECTION_GAP + LABEL_HEIGHT; // === PAC Section === let section_label = NSTextField::labelWithString(ns_string!("PAC File"), mtm); section_label.setFont(Some(&NSFont::boldSystemFontOfSize(13.0))); section_label.setFrame(NSRect::new( NSPoint::new(PADDING, y), NSSize::new(FIELD_WIDTH, LABEL_HEIGHT), )); content.addSubview(§ion_label); y -= ROW_GAP + LABEL_HEIGHT; // Port let label = NSTextField::labelWithString(ns_string!("HTTP Port:"), mtm); label.setFrame(NSRect::new( NSPoint::new(PADDING, y), NSSize::new(FIELD_WIDTH, LABEL_HEIGHT), )); content.addSubview(&label); y -= FIELD_HEIGHT; let port_field = NSTextField::textFieldWithString(&NSString::from_str(&cfg.http_port.to_string()), mtm); port_field.setFrame(NSRect::new( NSPoint::new(PADDING, y), NSSize::new(80.0, FIELD_HEIGHT), )); content.addSubview(&port_field); controller.ivars().port_field.set(port_field).unwrap(); y -= ROW_GAP + LABEL_HEIGHT; // PAC Content let label = NSTextField::labelWithString(ns_string!("PAC Content:"), mtm); label.setFrame(NSRect::new( NSPoint::new(PADDING, y), NSSize::new(FIELD_WIDTH, LABEL_HEIGHT), )); content.addSubview(&label); y -= 4.0; let pac_height = 150.0; y -= pac_height; let scroll_view = NSTextView::scrollableTextView(mtm); scroll_view.setFrame(NSRect::new( NSPoint::new(PADDING, y), NSSize::new(FIELD_WIDTH, pac_height), )); scroll_view.setHasVerticalScroller(true); scroll_view.setBorderType(NSBorderType::BezelBorder); // Get the text view from the scroll view let doc_view = scroll_view.documentView().unwrap(); let text_view: Retained<NSTextView> = unsafe { Retained::cast_unchecked(doc_view) }; text_view.setString(&NSString::from_str(&cfg.pac_content)); text_view.setFont(Some(&NSFont::monospacedSystemFontOfSize_weight(11.0, 0.0))); content.addSubview(&scroll_view); controller.ivars().pac_text_view.set(text_view).unwrap(); y -= SECTION_GAP + LABEL_HEIGHT; // === Autostart Section === let section_label = NSTextField::labelWithString(ns_string!("Autostart"), mtm); section_label.setFont(Some(&NSFont::boldSystemFontOfSize(13.0))); section_label.setFrame(NSRect::new( NSPoint::new(PADDING, y), NSSize::new(FIELD_WIDTH, LABEL_HEIGHT), )); content.addSubview(§ion_label); y -= ROW_GAP + FIELD_HEIGHT; let checkbox = unsafe { NSButton::checkboxWithTitle_target_action( ns_string!("Launch at login"), None, None, mtm, ) }; checkbox.setFrame(NSRect::new( NSPoint::new(PADDING, y), NSSize::new(FIELD_WIDTH, FIELD_HEIGHT), )); if cfg.launch_at_login { checkbox.setState(1); // NSControlStateValueOn } content.addSubview(&checkbox); controller.ivars().launch_checkbox.set(checkbox).unwrap(); y -= SECTION_GAP + FIELD_HEIGHT; // Save button let save_btn = unsafe { NSButton::buttonWithTitle_target_action( ns_string!("Save"), Some(controller.as_ref() as &AnyObject), Some(sel!(saveSettings:)), mtm, ) }; #[allow(deprecated)] save_btn.setBezelStyle(objc2_app_kit::NSBezelStyle::Rounded); save_btn.setFrame(NSRect::new( NSPoint::new(WIN_WIDTH - PADDING - 80.0, y), NSSize::new(80.0, 32.0), )); content.addSubview(&save_btn); // Keep controller alive std::mem::forget(controller); window } fn launch_agent_path() -> std::path::PathBuf { let home = std::env::var("HOME").unwrap_or_else(|_| "/tmp".to_string()); std::path::PathBuf::from(home) .join("Library/LaunchAgents") .join(format!("{}.plist", crate::APP_ID)) } fn install_launch_agent() { let path = launch_agent_path(); if let Some(parent) = path.parent() { let _ = std::fs::create_dir_all(parent); } let exe = std::env::current_exe().unwrap_or_default(); let exe_str = exe.to_string_lossy(); let app_id = crate::APP_ID; let plist_content = format!( r#"<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <plist version="1.0"> <dict> <key>Label</key> <string>{app_id}</string> <key>ProgramArguments</key> <array> <string>{exe_str}</string> </array> <key>RunAtLoad</key> <true/> <key>KeepAlive</key> <false/> </dict> </plist>"# ); match std::fs::write(&path, plist_content) { Ok(()) => crate::logging::log_info("LaunchAgent installed"), Err(e) => crate::logging::log_error(&format!("Failed to install LaunchAgent: {e}")), } } fn remove_launch_agent() { let path = launch_agent_path(); if path.exists() { match std::fs::remove_file(&path) { Ok(()) => crate::logging::log_info("LaunchAgent removed"), Err(e) => crate::logging::log_error(&format!("Failed to remove LaunchAgent: {e}")), } } }