use std::sync::{Arc, Mutex}; use gpui::{ App, Bounds, Global, KeyBinding, Menu, MenuItem, SharedString, TitlebarOptions, WindowBackgroundAppearance, WindowBounds, WindowDecorations, WindowKind, WindowOptions, actions, point, px, size, }; use gpui_kit::component::{Theme, ThemeMode}; use gpui_platform::application; use native_theme::watch::ThemeSubscription; use native_theme_gpui::apply_system_theme; use nostr_sdk::prelude::SecretKey; use settings::{AppSettings, AppearanceMode}; use state::{APP_ID, CLIENT_NAME}; use workspace::TAB_BAR_HEIGHT; actions!(coop, [Quit]); fn main() { // Initialize logging tracing_subscriber::fmt::init(); // Parse CLI arguments for --sec let cli_key = parse_cli_key(); if let Err(ref e) = cli_key { eprintln!("Failed to parse --sec argument: {e}"); std::process::exit(1); } let cli_key = cli_key.unwrap(); // Run application application() .with_assets(assets::CombinedAssets) .with_http_client(Arc::new(reqwest_client::ReqwestClient::new())) .run(move |cx| { // Initialize the framework before anything renders gpui_kit::init(cx); // Load embedded fonts in assets/fonts load_embedded_fonts(cx); // Set app identity cx.set_app_identity(APP_ID, CLIENT_NAME); // Register the `quit` function cx.on_action(quit); // Register the `quit` function with CMD+Q (macOS) #[cfg(target_os = "macos")] cx.bind_keys([KeyBinding::new("cmd-q", Quit, None)]); // Register the `quit` function with Super+Q (others) #[cfg(not(target_os = "macos"))] cx.bind_keys([KeyBinding::new("super-q", Quit, None)]); // Set menu items cx.set_menus(vec![Menu { name: "Coop".into(), items: vec![MenuItem::action("Quit", Quit)], disabled: false, }]); // Initialize settings settings::init(cx); let appearance = settings::persisted_appearance_mode(); let font_size = settings::persisted_theme().font_size; match apply_native_theme(appearance, font_size, cx) { Ok(()) => start_theme_watcher(appearance, cx), Err(error) => { log::warn!("native theme unavailable ({error})"); apply_appearance(appearance, cx); AppSettings::apply_theme_style(cx); } } // Initialize the nostr client state::init(cx, cli_key); // Initialize person registry person::init(cx); // Initialize device signer // // NIP-4e: https://github.com/nostr-protocol/nips/blob/per-device-keys/4e.md nip4e::init(cx); // Initialize app registry chat::init(cx); // Initialize groups registry nip29::init(cx); // Initialize auto update auto_update::init(cx); // Set up the window bounds let bounds = Bounds::centered(None, size(px(960.0), px(720.0)), cx); // Set up the window options let opts = WindowOptions { window_background: WindowBackgroundAppearance::Opaque, window_decorations: Some(WindowDecorations::Client), window_bounds: Some(WindowBounds::Windowed(bounds)), kind: WindowKind::Normal, app_id: Some(APP_ID.to_owned()), titlebar: Some(TitlebarOptions { title: Some(SharedString::new_static(CLIENT_NAME)), traffic_light_position: Some(point( px(9.0), px(TAB_BAR_HEIGHT / px(2.) - 14. / 2.), )), appears_transparent: true, }), app_owns_titlebar_drag: true, ..Default::default() }; gpui_kit::open_window(opts, cx, workspace::init) .expect("Failed to open window. Please restart the application."); cx.activate(true); }); } fn load_embedded_fonts(cx: &App) { let asset_source = cx.asset_source(); let font_paths = asset_source.list("fonts").unwrap(); let embedded_fonts = Mutex::new(vec![]); let executor = cx.background_executor(); cx.foreground_executor().block_on(executor.scoped(|scope| { for font_path in &font_paths { if !font_path.ends_with(".ttf") { continue; } scope.spawn(async { let font_bytes = asset_source.load(font_path.as_str()).unwrap().unwrap(); embedded_fonts.lock().unwrap().push(font_bytes); }); } })); cx.text_system() .add_fonts(embedded_fonts.into_inner().unwrap()) .unwrap(); } fn parse_cli_key() -> Result, String> { let args: Vec = std::env::args().collect(); let mut i = 0; while i < args.len() { if args[i] == "--sec" { if i + 1 < args.len() { let nsec = &args[i + 1]; return SecretKey::parse(nsec) .map(Some) .map_err(|e| format!("Invalid nsec key '{nsec}': {e}")); } else { return Err("--sec requires a value (nsec1...)".to_string()); } } i += 1; } Ok(None) } fn quit(_ev: &Quit, cx: &mut App) { log::info!("Gracefully quitting the application . . ."); cx.quit(); } /// Apply the native theme to the app, scaled to the user's font size, /// then layer the persisted theme tuning on top fn apply_native_theme( appearance: AppearanceMode, font_size: f32, cx: &mut App, ) -> native_theme::Result<()> { let mut system = native_theme_gpui::SystemTheme::from_system()?; let native = system.light.defaults.font.size; if native > 0.0 { system.accessibility.text_scaling_factor *= font_size / native; } apply_system_theme(&system, cx); apply_appearance(appearance, cx); // Applying the native theme replaces the whole theme, so the user's // tuning must be layered on afterwards. AppSettings::apply_theme_style(cx); Ok(()) } /// Honor the appearance mode against the installed (native) palettes fn apply_appearance(appearance: AppearanceMode, cx: &mut App) { match appearance { AppearanceMode::System => Theme::sync_system_appearance(None, cx), AppearanceMode::Light => Theme::change(ThemeMode::Light, None, cx), AppearanceMode::Dark => Theme::change(ThemeMode::Dark, None, cx), } } /// Holds the watcher subscription so it lives as long as the app. struct ThemeWatcher { _subscription: ThemeSubscription, /// The appearance mode the observer in `workspace` was registered with. appearance: std::cell::Cell, } impl Global for ThemeWatcher {} /// Start a background task to watch for OS theme changes and re-apply the native theme. fn start_theme_watcher(initial_appearance: AppearanceMode, cx: &mut App) { let (sender, receiver) = flume::unbounded(); match native_theme::watch::on_theme_change(move |event| { if let Err(error) = sender.send(event) { log::warn!("theme change dropped ({error})"); } }) { Ok(subscription) => { cx.set_global(ThemeWatcher { _subscription: subscription, appearance: std::cell::Cell::new(initial_appearance), }); cx.spawn(async move |cx| { while receiver.recv_async().await.is_ok() { log::info!("OS theme changed; re-applying"); cx.update(|cx| { let appearance = AppSettings::get_appearance_mode(cx); let font_size = AppSettings::get_theme(cx).font_size; if let Some(watcher) = cx.try_global::() { watcher.appearance.set(appearance); } if let Err(error) = apply_native_theme(appearance, font_size, cx) { log::warn!("failed to apply updated OS theme ({error})"); } }); } }) .detach(); } Err(error) => log::warn!("OS theme watching unavailable ({error})"), } }