/
pushamp
/
PACman
Обзор
Документация
Войти
/
pushamp
/
PACman
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
master
src/app.rs
445 строк
14 KB
Dmitry Zhiltsov
feat(core): Init commit
19 фев 2026, 18:01
19 фев 2026, 18:01
1fadda9
Код
Авторство
О чём код?
use std::cell::{Cell, OnceCell, RefCell}; use objc2::rc::Retained; use objc2::runtime::ProtocolObject; use objc2::sel; use objc2::{define_class, msg_send, AnyThread, DefinedClass, MainThreadOnly}; use objc2_app_kit::{ NSApplication, NSApplicationActivationPolicy, NSApplicationDelegate, NSBackingStoreType, NSButton, NSFont, NSImage, NSMenu, NSMenuItem, NSStatusBar, NSStatusItem, NSTextField, NSVariableStatusItemLength, NSWindow, NSWindowStyleMask, NSWorkspace, }; use objc2_foundation::{ ns_string, MainThreadMarker, NSNotification, NSObject, NSObjectProtocol, NSPoint, NSRect, NSSize, NSString, NSURL, }; use crate::settings::{self, SharedConfig}; #[derive(Debug)] struct AppDelegateIvars { status_item: OnceCell<Retained<NSStatusItem>>, config: OnceCell<SharedConfig>, pac_port: Cell<u16>, vpn_connected: Cell<bool>, settings_window: RefCell<Option<Retained<objc2_app_kit::NSWindow>>>, about_window: RefCell<Option<Retained<objc2_app_kit::NSWindow>>>, } impl Default for AppDelegateIvars { fn default() -> Self { Self { status_item: OnceCell::new(), config: OnceCell::new(), pac_port: Cell::new(0), vpn_connected: Cell::new(false), settings_window: RefCell::new(None), about_window: RefCell::new(None), } } } define_class!( #[unsafe(super = NSObject)] #[thread_kind = MainThreadOnly] #[ivars = AppDelegateIvars] #[derive(Debug)] struct AppDelegate; unsafe impl NSObjectProtocol for AppDelegate {} unsafe impl NSApplicationDelegate for AppDelegate { #[unsafe(method(applicationDidFinishLaunching:))] fn did_finish_launching(&self, _notification: &NSNotification) { let mtm = self.mtm(); // Load config let config = settings::load_shared(); self.ivars().config.set(config.clone()).unwrap(); // Set activation policy — no Dock icon let app = NSApplication::sharedApplication(mtm); app.setActivationPolicy(NSApplicationActivationPolicy::Accessory); // Create status bar item let status_bar = NSStatusBar::systemStatusBar(); let status_item = status_bar.statusItemWithLength(NSVariableStatusItemLength); // Set initial icon (PACman mouth closed = disconnected) if let Some(button) = status_item.button(mtm) { let img = crate::icons::disconnected(); button.setImage(Some(&img)); } // Build menu let menu = build_menu(mtm); status_item.setMenu(Some(&menu)); self.ivars().status_item.set(status_item).unwrap(); // Start PAC server let port = crate::pac_server::start_pac_server(config.clone()); self.ivars().pac_port.set(port); // Update PAC URL in menu if let Some(status_item) = self.ivars().status_item.get() && let Some(menu) = status_item.menu(mtm) && let Some(item) = menu.itemAtIndex(1) { item.setTitle(&NSString::from_str(&format!( "PAC: {}", crate::pac_url(port) ))); } // Start monitor crate::monitor::start_monitor(config, move |state| { dispatch2::run_on_main(move |mtm| { let app = NSApplication::sharedApplication(mtm); if let Some(delegate) = app.delegate() { // SAFETY: we know the delegate is our AppDelegate let delegate: Retained<AppDelegate> = unsafe { Retained::cast_unchecked(delegate) }; delegate.update_vpn_state(state.connected, mtm); } }); }); crate::logging::log_default("PACman started"); } } // Action methods — these are found via the responder chain // (NSApplication delegates to its delegate for unhandled actions) impl AppDelegate { #[unsafe(method(openSettings:))] fn open_settings_action(&self, _sender: *mut NSObject) { self.open_settings(); } #[unsafe(method(showAbout:))] fn show_about_action(&self, _sender: *mut NSObject) { self.show_about(); } #[unsafe(method(openWebsite:))] fn open_website_action(&self, _sender: *mut NSObject) { let url = NSURL::initWithString( NSURL::alloc(), ns_string!("https://apps.pushamp.com/PACman"), ); if let Some(url) = url { NSWorkspace::sharedWorkspace().openURL(&url); } } #[unsafe(method(viewLogs:))] fn view_logs_action(&self, _sender: *mut NSObject) { let path = std::env::temp_dir().join("vpnproxy-logs.command"); let script = format!("#!/bin/sh\nlog stream --predicate 'subsystem == \"{}\"' --level info\n", crate::APP_ID); if std::fs::write(&path, script).is_ok() { use std::os::unix::fs::PermissionsExt; let _ = std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o755)); let _ = std::process::Command::new("open").arg(&path).spawn(); } } } ); impl AppDelegate { fn new(mtm: MainThreadMarker) -> Retained<Self> { let this = Self::alloc(mtm).set_ivars(AppDelegateIvars::default()); unsafe { msg_send![super(this), init] } } fn update_vpn_state(&self, connected: bool, mtm: MainThreadMarker) { self.ivars().vpn_connected.set(connected); if let Some(status_item) = self.ivars().status_item.get() { if let Some(button) = status_item.button(mtm) { let img = if crate::monitor::had_error() { // Error: use SF Symbol for warning triangle let sf = NSImage::imageWithSystemSymbolName_accessibilityDescription( &NSString::from_str("exclamationmark.triangle"), Some(ns_string!("Error")), ); if let Some(i) = sf { i.setTemplate(true); i } else { crate::icons::disconnected() } } else if connected { crate::icons::connected() } else { crate::icons::disconnected() }; button.setImage(Some(&img)); } if let Some(menu) = status_item.menu(mtm) && let Some(item) = menu.itemAtIndex(0) { let status_text = if connected { "VPN: connected" } else { "VPN: disconnected" }; item.setTitle(&NSString::from_str(status_text)); } } } fn update_pac_menu_url(&self, port: u16) { let mtm = self.mtm(); if let Some(status_item) = self.ivars().status_item.get() && let Some(menu) = status_item.menu(mtm) && let Some(item) = menu.itemAtIndex(1) { item.setTitle(&NSString::from_str(&format!( "PAC: http://127.0.0.1:{port}/proxy.pac" ))); } self.ivars().pac_port.set(port); } fn show_about(&self) { let mtm = self.mtm(); // Reuse existing window or create new { let existing = self.ivars().about_window.borrow(); if let Some(window) = existing.as_ref() { window.makeKeyAndOrderFront(None); #[allow(deprecated)] NSApplication::sharedApplication(mtm).activateIgnoringOtherApps(true); return; } } let window = create_about_window(mtm); window.makeKeyAndOrderFront(None); #[allow(deprecated)] NSApplication::sharedApplication(mtm).activateIgnoringOtherApps(true); *self.ivars().about_window.borrow_mut() = Some(window); } fn open_settings(&self) { let mtm = self.mtm(); let config = self.ivars().config.get().unwrap().clone(); // Reuse existing window or create new { let existing = self.ivars().settings_window.borrow(); if let Some(window) = existing.as_ref() { window.makeKeyAndOrderFront(None); #[allow(deprecated)] NSApplication::sharedApplication(mtm).activateIgnoringOtherApps(true); return; } } let window = crate::settings_window::create_settings_window(mtm, config); window.makeKeyAndOrderFront(None); #[allow(deprecated)] NSApplication::sharedApplication(mtm).activateIgnoringOtherApps(true); *self.ivars().settings_window.borrow_mut() = Some(window); } } fn build_menu(mtm: MainThreadMarker) -> Retained<NSMenu> { let menu = NSMenu::initWithTitle(NSMenu::alloc(mtm), ns_string!("")); // VPN status (disabled, informational) let vpn_item = unsafe { NSMenuItem::initWithTitle_action_keyEquivalent( NSMenuItem::alloc(mtm), ns_string!("VPN: checking..."), None, ns_string!(""), ) }; vpn_item.setEnabled(false); menu.addItem(&vpn_item); // PAC URL (disabled, informational) let pac_item = unsafe { NSMenuItem::initWithTitle_action_keyEquivalent( NSMenuItem::alloc(mtm), ns_string!("PAC: starting..."), None, ns_string!(""), ) }; pac_item.setEnabled(false); menu.addItem(&pac_item); // Separator menu.addItem(&NSMenuItem::separatorItem(mtm)); // Settings let settings_item = unsafe { NSMenuItem::initWithTitle_action_keyEquivalent( NSMenuItem::alloc(mtm), ns_string!("Settings..."), Some(sel!(openSettings:)), ns_string!(","), ) }; menu.addItem(&settings_item); // View Logs let logs_item = unsafe { NSMenuItem::initWithTitle_action_keyEquivalent( NSMenuItem::alloc(mtm), ns_string!("View Logs..."), Some(sel!(viewLogs:)), ns_string!(""), ) }; menu.addItem(&logs_item); // About let about_item = unsafe { NSMenuItem::initWithTitle_action_keyEquivalent( NSMenuItem::alloc(mtm), ns_string!("About PACman"), Some(sel!(showAbout:)), ns_string!(""), ) }; menu.addItem(&about_item); // Separator menu.addItem(&NSMenuItem::separatorItem(mtm)); // Quit let quit_item = unsafe { NSMenuItem::initWithTitle_action_keyEquivalent( NSMenuItem::alloc(mtm), ns_string!("Quit"), Some(sel!(terminate:)), ns_string!("q"), ) }; menu.addItem(&quit_item); menu } fn create_about_window(mtm: MainThreadMarker) -> Retained<NSWindow> { let w: f64 = 320.0; let h: f64 = 200.0; let pad: f64 = 20.0; let fw: f64 = w - pad * 2.0; let window = unsafe { NSWindow::initWithContentRect_styleMask_backing_defer( NSWindow::alloc(mtm), NSRect::new(NSPoint::new(0.0, 0.0), NSSize::new(w, h)), NSWindowStyleMask::Titled | NSWindowStyleMask::Closable, NSBackingStoreType::Buffered, false, ) }; unsafe { window.setReleasedWhenClosed(false) }; window.setTitle(ns_string!("About PACman")); window.center(); let content = window.contentView().unwrap(); let mut y = h - pad - 24.0; // App name let name = NSTextField::labelWithString(ns_string!("PACman"), mtm); name.setFont(Some(&NSFont::boldSystemFontOfSize(20.0))); name.setFrame(NSRect::new( NSPoint::new(pad, y), NSSize::new(fw, 24.0), )); let _: () = unsafe { msg_send![&name, setAlignment: 2isize] }; // NSTextAlignmentCenter content.addSubview(&name); y -= 24.0; // Version let ver = NSTextField::labelWithString( &NSString::from_str(&format!("Version {}", env!("CARGO_PKG_VERSION"))), mtm, ); ver.setFrame(NSRect::new( NSPoint::new(pad, y), NSSize::new(fw, 17.0), )); let _: () = unsafe { msg_send![&ver, setAlignment: 2isize] }; content.addSubview(&ver); y -= 22.0; // Description let desc = NSTextField::labelWithString( ns_string!("VPN Proxy Auto-Configuration"), mtm, ); desc.setFont(Some(&NSFont::systemFontOfSize(11.0))); desc.setFrame(NSRect::new( NSPoint::new(pad, y), NSSize::new(fw, 17.0), )); let _: () = unsafe { msg_send![&desc, setAlignment: 2isize] }; content.addSubview(&desc); y -= 26.0; // Copyright let copy_label = NSTextField::labelWithString( ns_string!("\u{00a9} 2025 LLC PushAMP"), mtm, ); copy_label.setFrame(NSRect::new( NSPoint::new(pad, y), NSSize::new(fw, 17.0), )); let _: () = unsafe { msg_send![©_label, setAlignment: 2isize] }; content.addSubview(©_label); y -= 30.0; // Website button let btn = unsafe { NSButton::buttonWithTitle_target_action( ns_string!("apps.pushamp.com/PACman"), None, Some(sel!(openWebsite:)), mtm, ) }; #[allow(deprecated)] btn.setBezelStyle(objc2_app_kit::NSBezelStyle::Rounded); let btn_w: f64 = 220.0; btn.setFrame(NSRect::new( NSPoint::new((w - btn_w) / 2.0, y), NSSize::new(btn_w, 32.0), )); content.addSubview(&btn); window } /// Update the PAC URL displayed in the menu bar. Must be called on the main thread. pub fn update_pac_url_in_menu(mtm: MainThreadMarker, port: u16) { let app = NSApplication::sharedApplication(mtm); if let Some(delegate) = app.delegate() { // SAFETY: we know the delegate is our AppDelegate let delegate: Retained<AppDelegate> = unsafe { Retained::cast_unchecked(delegate) }; delegate.update_pac_menu_url(port); } } pub fn run_app() { let mtm = MainThreadMarker::new().expect("must be on main thread"); let app = NSApplication::sharedApplication(mtm); let delegate = AppDelegate::new(mtm); app.setDelegate(Some(ProtocolObject::from_ref(&*delegate))); // Keep delegate alive for the lifetime of the app std::mem::forget(delegate); app.run(); }