Files
coop/desktop/src/main.rs
T

248 lines
8.4 KiB
Rust

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 <nsec1>
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<Option<SecretKey>, String> {
let args: Vec<String> = 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<AppearanceMode>,
}
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::<ThemeWatcher>() {
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})"),
}
}