From bdd811f2d4acaaad4eedb003ebf954e1ce5e3be9 Mon Sep 17 00:00:00 2001 From: LGUG2Z Date: Sat, 31 Oct 2026 12:41:00 -0700 Subject: [PATCH] refactor(bar): the macOS bar is a module of its package (move) komorebi-for-mac/komorebi-bar/src/{bar,config,os}.rs and widgets/ move to src/macos/, main.rs to macos/mod.rs (pub fn main); src/main.rs is a caller. Mechanical edits only: crate:: becomes crate::macos:: (all sites), the include_str! path gets one more ../, and the unused root re-export of komorebi_bar_core::platform is dropped. Gates run: macOS fake-CC check of komorebi-bar-macos --locked --all-targets PASS (type check only); bar-core schema oracle PASS; S/bar2 harness regenerates both bar schemas byte-identical to the checked-in files (macOS 3774 lines, Windows 9091). Not run: any bar run on a Mac (the user's Mac check). --- .../komorebi-bar/src/{ => macos}/bar.rs | 46 +-- .../komorebi-bar/src/{ => macos}/config.rs | 0 .../komorebi-bar/src/macos/mod.rs | 387 +++++++++++++++++ .../komorebi-bar/src/{ => macos}/os.rs | 2 +- .../src/{ => macos}/widgets/komorebi.rs | 20 +- .../src/{ => macos}/widgets/mod.rs | 0 .../src/{ => macos}/widgets/network.rs | 10 +- .../src/{ => macos}/widgets/widget.rs | 20 +- komorebi-for-mac/komorebi-bar/src/main.rs | 389 +----------------- 9 files changed, 440 insertions(+), 434 deletions(-) rename komorebi-for-mac/komorebi-bar/src/{ => macos}/bar.rs (97%) rename komorebi-for-mac/komorebi-bar/src/{ => macos}/config.rs (100%) create mode 100644 komorebi-for-mac/komorebi-bar/src/macos/mod.rs rename komorebi-for-mac/komorebi-bar/src/{ => macos}/os.rs (98%) rename komorebi-for-mac/komorebi-bar/src/{ => macos}/widgets/komorebi.rs (98%) rename komorebi-for-mac/komorebi-bar/src/{ => macos}/widgets/mod.rs (100%) rename komorebi-for-mac/komorebi-bar/src/{ => macos}/widgets/network.rs (99%) rename komorebi-for-mac/komorebi-bar/src/{ => macos}/widgets/widget.rs (79%) diff --git a/komorebi-for-mac/komorebi-bar/src/bar.rs b/komorebi-for-mac/komorebi-bar/src/macos/bar.rs similarity index 97% rename from komorebi-for-mac/komorebi-bar/src/bar.rs rename to komorebi-for-mac/komorebi-bar/src/macos/bar.rs index 2a47ef88..b46f4bce 100644 --- a/komorebi-for-mac/komorebi-bar/src/bar.rs +++ b/komorebi-for-mac/komorebi-bar/src/macos/bar.rs @@ -1,26 +1,26 @@ -use crate::AUTO_SELECT_FILL_COLOUR; -use crate::AUTO_SELECT_TEXT_COLOUR; -use crate::BAR_HEIGHT; -use crate::DEFAULT_PADDING; -use crate::KomorebiEvent; -use crate::MAX_LABEL_WIDTH; -use crate::MONITOR_LEFT; -use crate::MONITOR_RIGHT; -use crate::MONITOR_TOP; -use crate::config::KomobarConfig; -use crate::config::MonitorConfigOrIndex; -use crate::config::Position; -use crate::config::PositionConfig; -use crate::config::get_individual_spacing; -use crate::render::Color32Ext; -use crate::render::Grouping; -use crate::render::RenderConfig; -use crate::render::RenderExt; -use crate::widgets::komorebi::Komorebi; -use crate::widgets::komorebi::MonitorInfo; -use crate::widgets::widget::BarWidget; -use crate::widgets::widget::WidgetConfig; -use crate::widgets::widget::WidgetConfigExt; +use crate::macos::AUTO_SELECT_FILL_COLOUR; +use crate::macos::AUTO_SELECT_TEXT_COLOUR; +use crate::macos::BAR_HEIGHT; +use crate::macos::DEFAULT_PADDING; +use crate::macos::KomorebiEvent; +use crate::macos::MAX_LABEL_WIDTH; +use crate::macos::MONITOR_LEFT; +use crate::macos::MONITOR_RIGHT; +use crate::macos::MONITOR_TOP; +use crate::macos::config::KomobarConfig; +use crate::macos::config::MonitorConfigOrIndex; +use crate::macos::config::Position; +use crate::macos::config::PositionConfig; +use crate::macos::config::get_individual_spacing; +use crate::macos::render::Color32Ext; +use crate::macos::render::Grouping; +use crate::macos::render::RenderConfig; +use crate::macos::render::RenderExt; +use crate::macos::widgets::komorebi::Komorebi; +use crate::macos::widgets::komorebi::MonitorInfo; +use crate::macos::widgets::widget::BarWidget; +use crate::macos::widgets::widget::WidgetConfig; +use crate::macos::widgets::widget::WidgetConfigExt; use crossbeam_channel::Receiver; use crossbeam_channel::TryRecvError; use eframe::egui::Align; diff --git a/komorebi-for-mac/komorebi-bar/src/config.rs b/komorebi-for-mac/komorebi-bar/src/macos/config.rs similarity index 100% rename from komorebi-for-mac/komorebi-bar/src/config.rs rename to komorebi-for-mac/komorebi-bar/src/macos/config.rs diff --git a/komorebi-for-mac/komorebi-bar/src/macos/mod.rs b/komorebi-for-mac/komorebi-bar/src/macos/mod.rs new file mode 100644 index 00000000..0c89b083 --- /dev/null +++ b/komorebi-for-mac/komorebi-bar/src/macos/mod.rs @@ -0,0 +1,387 @@ +mod bar; +mod config; +mod os; +pub use komorebi_bar_core::render; +pub use komorebi_bar_core::selected_frame; +pub use komorebi_bar_core::ui; +mod widgets; + +use crate::macos::bar::Komobar; +use crate::macos::config::KomobarConfig; +use crate::macos::config::Position; +use crate::macos::config::PositionConfig; +use clap::Parser; +use color_eyre::eyre; +use config::MonitorConfigOrIndex; +use eframe::egui::ViewportBuilder; +use font_loader::system_fonts; +use hotwatch::EventKind; +use hotwatch::Hotwatch; +use komorebi_client::PathExt; +use komorebi_client::SocketMessage; +use komorebi_client::SubscribeOptions; +use komorebi_client::replace_env_in_path; +use objc2::MainThreadMarker; +use objc2_app_kit::NSApplication; +use objc2_app_kit::NSApplicationActivationPolicy; +use objc2_app_kit::NSView; +use raw_window_handle::HasWindowHandle; +use raw_window_handle::RawWindowHandle; +use std::io::BufReader; +use std::io::Read; +use std::path::PathBuf; +use std::sync::atomic::Ordering; +use std::time::Duration; +use tracing_subscriber::EnvFilter; + +pub use komorebi_bar_core::statics::*; + +#[derive(Parser)] +#[clap(name = "komorebi-bar", author, about, version)] +struct Opts { + /// Print the JSON schema of the configuration file and exit + #[clap(long)] + schema: bool, + /// Print a list of fonts available on this system and exit + #[clap(long)] + fonts: bool, + /// Path to a JSON or YAML configuration file + #[clap(short, long)] + #[clap(value_parser = replace_env_in_path)] + config: Option, + /// Write an example komorebi.bar.json to disk + #[clap(long)] + quickstart: bool, + /// Print a list of aliases that can be renamed to canonical variants + #[clap(long)] + #[clap(hide = true)] + aliases: bool, +} + +pub enum KomorebiEvent { + Notification(komorebi_client::Notification), + Reconnect, +} + +pub fn main() -> eyre::Result<()> { + komorebi_bar_core::platform::install(&os::MacBar); + + let opts: Opts = Opts::parse(); + + #[cfg(feature = "schemars")] + if opts.schema { + print!( + "{}", + komorebi_bar_core::schema::generated_text::( + komorebi_bar_core::schema::SchemaPlatform::MacOs + ) + ); + std::process::exit(0); + } + + if opts.fonts { + for font in system_fonts::query_all() { + println!("{font}"); + } + + std::process::exit(0); + } + + if std::env::var("RUST_LIB_BACKTRACE").is_err() { + unsafe { + std::env::set_var("RUST_LIB_BACKTRACE", "1"); + } + } + + color_eyre::install()?; + + if std::env::var("RUST_LOG").is_err() { + unsafe { + std::env::set_var("RUST_LOG", "info"); + } + } + + tracing::subscriber::set_global_default( + tracing_subscriber::fmt::Subscriber::builder() + .with_env_filter(EnvFilter::from_default_env()) + .finish(), + )?; + + let home_dir: PathBuf = std::env::var("KOMOREBI_CONFIG_HOME").map_or_else( + |_| { + dirs::home_dir() + .expect("there is no home directory") + .join(".config") + .join("komorebi") + }, + |home_path| { + let home = home_path.replace_env(); + + assert!( + home.is_dir(), + "$Env:KOMOREBI_CONFIG_HOME is set to '{home_path}', which is not a valid directory" + ); + + home + }, + ); + + if opts.quickstart { + let komorebi_bar_json = include_str!("../../../docs/komorebi.bar.example.json").to_string(); + std::fs::write(home_dir.join("komorebi.bar.json"), komorebi_bar_json)?; + println!( + "Example komorebi.bar.json file written to {}", + home_dir.display() + ); + + std::process::exit(0); + } + + let default_config_path = home_dir.join("komorebi.bar.json"); + + let config_path = opts.config.or_else(|| { + default_config_path + .is_file() + .then_some(default_config_path.clone()) + }); + + let mut config = match config_path { + None => { + let komorebi_bar_json = + include_str!("../../../docs/komorebi.bar.example.json").to_string(); + + std::fs::write(&default_config_path, komorebi_bar_json)?; + tracing::info!( + "created example configuration file: {}", + default_config_path.display() + ); + + KomobarConfig::read(&default_config_path)? + } + Some(ref config) => { + if !opts.aliases { + tracing::info!("found configuration file: {}", config.display()); + } + + KomobarConfig::read(config)? + } + }; + + let config_path = config_path.unwrap_or(default_config_path); + + if opts.aliases { + KomobarConfig::aliases(&std::fs::read_to_string(&config_path)?); + std::process::exit(0); + } + + let state = serde_json::from_str::(&komorebi_client::send_query( + &SocketMessage::State, + )?)?; + + let (usr_monitor_index, work_area_offset) = match &config.monitor { + Some(MonitorConfigOrIndex::MonitorConfig(monitor_config)) => { + (monitor_config.index, monitor_config.work_area_offset) + } + Some(MonitorConfigOrIndex::Index(idx)) => (*idx, None), + None => (0, None), + }; + let monitor_index = state + .monitor_usr_idx_map + .get(&usr_monitor_index) + .map_or(usr_monitor_index, |i| *i); + + MONITOR_RIGHT.store( + state.monitors.elements()[monitor_index].size.right, + Ordering::SeqCst, + ); + + MONITOR_TOP.store( + state.monitors.elements()[monitor_index].size.top, + Ordering::SeqCst, + ); + + MONITOR_LEFT.store( + state.monitors.elements()[monitor_index].size.left, + Ordering::SeqCst, + ); + + MONITOR_INDEX.store(monitor_index, Ordering::SeqCst); + + // use monitor.size for the actual monitor bounds (top-left origin in global coordinates) + // the window level kCGBackstopMenuLevel positions the bar behind the menu bar + match config.position { + None => { + config.position = Some(PositionConfig { + start: Some(Position { + x: state.monitors.elements()[monitor_index].size.left as f32, + y: state.monitors.elements()[monitor_index].size.top as f32, + }), + end: Some(Position { + x: state.monitors.elements()[monitor_index].size.right as f32, + y: 50.0, + }), + }) + } + Some(ref mut position) => { + if position.start.is_none() { + position.start = Some(Position { + x: state.monitors.elements()[monitor_index].size.left as f32, + y: state.monitors.elements()[monitor_index].size.top as f32, + }); + } + + if position.end.is_none() { + position.end = Some(Position { + x: state.monitors.elements()[monitor_index].size.right as f32, + y: 50.0, + }) + } + } + } + + let viewport_builder = ViewportBuilder::default() + .with_decorations(false) + .with_transparent(true) + .with_has_shadow(false) + .with_taskbar(false); + + let native_options = eframe::NativeOptions { + viewport: viewport_builder, + ..Default::default() + }; + + if let Some(rect) = &work_area_offset { + komorebi_client::send_message(&SocketMessage::MonitorWorkAreaOffset(monitor_index, *rect))?; + tracing::info!("work area offset applied to monitor: {}", monitor_index); + } + + let (tx_gui, rx_gui) = crossbeam_channel::unbounded(); + let (tx_config, rx_config) = crossbeam_channel::unbounded(); + + let mut hotwatch = Hotwatch::new()?; + let config_path_cl = config_path.clone(); + + hotwatch.watch(config_path, move |event| match event.kind { + EventKind::Modify(_) | EventKind::Remove(_) => match KomobarConfig::read(&config_path_cl) { + Ok(updated) => { + tracing::info!("configuration file updated: {}", config_path_cl.display()); + + if let Err(error) = tx_config.send(updated) { + tracing::error!("could not send configuration update to gui: {error}") + } + } + Err(error) => { + tracing::error!("{error}"); + } + }, + _ => {} + })?; + + tracing::info!("watching configuration file for changes"); + + eframe::run_native( + "komorebi-bar", + native_options, + Box::new(|cc| { + unsafe { + let app = NSApplication::sharedApplication(MainThreadMarker::new_unchecked()); + app.setActivationPolicy(NSApplicationActivationPolicy::Accessory); + } + + if let Ok(handle) = cc.window_handle() + && let RawWindowHandle::AppKit(appkit_handle) = handle.as_raw() { + // kCGBackstopMenuLevel = -20 (behind menu bar, at notch level) + const K_CG_BACKSTOP_MENU_LEVEL: isize = -20; + + unsafe { + let ns_view: &NSView = appkit_handle.ns_view.cast().as_ref(); + if let Some(ns_window) = ns_view.window() { + ns_window.setLevel(K_CG_BACKSTOP_MENU_LEVEL); + } + } + } + + let ctx_repainter = cc.egui_ctx.clone(); + std::thread::spawn(move || loop { + std::thread::sleep(Duration::from_secs(1)); + ctx_repainter.request_repaint(); + }); + + let ctx_komorebi = cc.egui_ctx.clone(); + std::thread::spawn(move || { + let subscriber_name = format!("komorebi-bar-{}", random_word::get(random_word::Lang::En)); + + let listener = komorebi_client::subscribe_with_options(&subscriber_name, SubscribeOptions { + filter_state_changes: true, + }) + .expect("could not subscribe to komorebi notifications"); + + tracing::info!("subscribed to komorebi notifications: \"{}\"", subscriber_name); + + for client in listener.incoming() { + match client { + Ok(subscription) => { + let mut buffer = Vec::new(); + let mut reader = BufReader::new(subscription); + + // this is when we know a shutdown has been sent + if matches!(reader.read_to_end(&mut buffer), Ok(0)) { + tracing::info!("disconnected from komorebi"); + + // keep trying to reconnect to komorebi + while komorebi_client::send_message( + &SocketMessage::AddSubscriberSocket(subscriber_name.clone()), + ) + .is_err() + { + std::thread::sleep(Duration::from_secs(1)); + } + + tracing::info!("reconnected to komorebi"); + + if let Err(error) = tx_gui.send(KomorebiEvent::Reconnect) { + tracing::error!("could not send komorebi reconnect event to gui thread: {error}") + } + + ctx_komorebi.request_repaint(); + continue; + } + + match String::from_utf8(buffer) { + Ok(notification_string) => { + match serde_json::from_str::( + ¬ification_string, + ) { + Ok(notification) => { + tracing::debug!("received notification from komorebi"); + + if let Err(error) = tx_gui.send(KomorebiEvent::Notification(notification)) { + tracing::error!("could not send komorebi notification update to gui thread: {error}") + } + + ctx_komorebi.request_repaint(); + } + Err(error) => { + tracing::error!("could not deserialize komorebi notification: {error}"); + } + } + } + Err(error) => { + tracing::error!( + "komorebi notification string was invalid utf8: {error}" + ) + } + } + } + Err(error) => { + tracing::error!("{error}"); + } + } + } + }); + + Ok(Box::new(Komobar::new(cc, rx_gui, rx_config, config))) + }), + ) + .map_err(|error| color_eyre::eyre::Error::msg(error.to_string())) +} diff --git a/komorebi-for-mac/komorebi-bar/src/os.rs b/komorebi-for-mac/komorebi-bar/src/macos/os.rs similarity index 98% rename from komorebi-for-mac/komorebi-bar/src/os.rs rename to komorebi-for-mac/komorebi-bar/src/macos/os.rs index cccfcccd..3275456e 100644 --- a/komorebi-for-mac/komorebi-bar/src/os.rs +++ b/komorebi-for-mac/komorebi-bar/src/macos/os.rs @@ -1,4 +1,4 @@ -use crate::widgets::ImageIcon; +use crate::macos::widgets::ImageIcon; use egui_phosphor::regular::CALENDAR; use komorebi_bar_core::platform::BarPlatform; use komorebi_bar_core::platform::SystemTool; diff --git a/komorebi-for-mac/komorebi-bar/src/widgets/komorebi.rs b/komorebi-for-mac/komorebi-bar/src/macos/widgets/komorebi.rs similarity index 98% rename from komorebi-for-mac/komorebi-bar/src/widgets/komorebi.rs rename to komorebi-for-mac/komorebi-bar/src/macos/widgets/komorebi.rs index 6ad709ae..7f357184 100644 --- a/komorebi-for-mac/komorebi-bar/src/widgets/komorebi.rs +++ b/komorebi-for-mac/komorebi-bar/src/macos/widgets/komorebi.rs @@ -1,12 +1,12 @@ use super::ImageIcon; -use crate::MAX_LABEL_WIDTH; -use crate::MONITOR_INDEX; -use crate::config::DisplayFormat; -use crate::config::DisplayFormat::*; -use crate::config::WorkspacesDisplayFormat; -use crate::render::RenderConfig; -use crate::selected_frame::SelectableFrame; -use crate::ui::CustomUi; +use crate::macos::MAX_LABEL_WIDTH; +use crate::macos::MONITOR_INDEX; +use crate::macos::config::DisplayFormat; +use crate::macos::config::DisplayFormat::*; +use crate::macos::config::WorkspacesDisplayFormat; +use crate::macos::render::RenderConfig; +use crate::macos::selected_frame::SelectableFrame; +use crate::macos::ui::CustomUi; pub use komorebi_bar_core::widgets::komorebi_config::KomorebiConfig; pub use komorebi_bar_core::widgets::komorebi_config::KomorebiConfigurationSwitcherConfig; pub use komorebi_bar_core::widgets::komorebi_config::KomorebiFocusedContainerConfig; @@ -15,8 +15,8 @@ pub use komorebi_bar_core::widgets::komorebi_config::KomorebiLockedContainerConf pub use komorebi_bar_core::widgets::komorebi_config::KomorebiWorkspaceLayerConfig; pub use komorebi_bar_core::widgets::komorebi_config::KomorebiWorkspacesConfig; pub use komorebi_bar_core::widgets::komorebi_config::KomorebiLayout; -use crate::widgets::komorebi_layout::KomorebiLayoutExt; -use crate::widgets::widget::BarWidget; +use crate::macos::widgets::komorebi_layout::KomorebiLayoutExt; +use crate::macos::widgets::widget::BarWidget; use eframe::egui::Align; use eframe::egui::Color32; use eframe::egui::Context; diff --git a/komorebi-for-mac/komorebi-bar/src/widgets/mod.rs b/komorebi-for-mac/komorebi-bar/src/macos/widgets/mod.rs similarity index 100% rename from komorebi-for-mac/komorebi-bar/src/widgets/mod.rs rename to komorebi-for-mac/komorebi-bar/src/macos/widgets/mod.rs diff --git a/komorebi-for-mac/komorebi-bar/src/widgets/network.rs b/komorebi-for-mac/komorebi-bar/src/macos/widgets/network.rs similarity index 99% rename from komorebi-for-mac/komorebi-bar/src/widgets/network.rs rename to komorebi-for-mac/komorebi-bar/src/macos/widgets/network.rs index bd6f2476..fb7b4041 100644 --- a/komorebi-for-mac/komorebi-bar/src/widgets/network.rs +++ b/komorebi-for-mac/komorebi-bar/src/macos/widgets/network.rs @@ -1,10 +1,10 @@ -use crate::bar::Alignment; +use crate::macos::bar::Alignment; pub use komorebi_bar_core::widgets::network_config::NetworkConfig; pub use komorebi_bar_core::widgets::network_config::NetworkSelectConfig; -use crate::config::LabelPrefix; -use crate::render::RenderConfig; -use crate::selected_frame::SelectableFrame; -use crate::widgets::widget::BarWidget; +use crate::macos::config::LabelPrefix; +use crate::macos::render::RenderConfig; +use crate::macos::selected_frame::SelectableFrame; +use crate::macos::widgets::widget::BarWidget; use eframe::egui::Align; use eframe::egui::Color32; use eframe::egui::Context; diff --git a/komorebi-for-mac/komorebi-bar/src/widgets/widget.rs b/komorebi-for-mac/komorebi-bar/src/macos/widgets/widget.rs similarity index 79% rename from komorebi-for-mac/komorebi-bar/src/widgets/widget.rs rename to komorebi-for-mac/komorebi-bar/src/macos/widgets/widget.rs index 6880736b..e2d56474 100644 --- a/komorebi-for-mac/komorebi-bar/src/widgets/widget.rs +++ b/komorebi-for-mac/komorebi-bar/src/macos/widgets/widget.rs @@ -1,13 +1,13 @@ -use crate::widgets::applications::Applications; -use crate::widgets::battery::Battery; -use crate::widgets::cpu::Cpu; -use crate::widgets::date::Date; -use crate::widgets::komorebi::Komorebi; -use crate::widgets::memory::Memory; -use crate::widgets::network::Network; -use crate::widgets::storage::Storage; -use crate::widgets::time::Time; -use crate::widgets::update::Update; +use crate::macos::widgets::applications::Applications; +use crate::macos::widgets::battery::Battery; +use crate::macos::widgets::cpu::Cpu; +use crate::macos::widgets::date::Date; +use crate::macos::widgets::komorebi::Komorebi; +use crate::macos::widgets::memory::Memory; +use crate::macos::widgets::network::Network; +use crate::macos::widgets::storage::Storage; +use crate::macos::widgets::time::Time; +use crate::macos::widgets::update::Update; use eframe::egui::Context; use eframe::egui::Ui; diff --git a/komorebi-for-mac/komorebi-bar/src/main.rs b/komorebi-for-mac/komorebi-bar/src/main.rs index f3a41542..0aa802b4 100644 --- a/komorebi-for-mac/komorebi-bar/src/main.rs +++ b/komorebi-for-mac/komorebi-bar/src/main.rs @@ -1,388 +1,7 @@ -mod bar; -mod config; -mod os; -pub use komorebi_bar_core::platform; -pub use komorebi_bar_core::render; -pub use komorebi_bar_core::selected_frame; -pub use komorebi_bar_core::ui; -mod widgets; +//! `komorebi-bar` for macOS. The code is in the `macos` module. -use crate::bar::Komobar; -use crate::config::KomobarConfig; -use crate::config::Position; -use crate::config::PositionConfig; -use clap::Parser; -use color_eyre::eyre; -use config::MonitorConfigOrIndex; -use eframe::egui::ViewportBuilder; -use font_loader::system_fonts; -use hotwatch::EventKind; -use hotwatch::Hotwatch; -use komorebi_client::PathExt; -use komorebi_client::SocketMessage; -use komorebi_client::SubscribeOptions; -use komorebi_client::replace_env_in_path; -use objc2::MainThreadMarker; -use objc2_app_kit::NSApplication; -use objc2_app_kit::NSApplicationActivationPolicy; -use objc2_app_kit::NSView; -use raw_window_handle::HasWindowHandle; -use raw_window_handle::RawWindowHandle; -use std::io::BufReader; -use std::io::Read; -use std::path::PathBuf; -use std::sync::atomic::Ordering; -use std::time::Duration; -use tracing_subscriber::EnvFilter; +mod macos; -pub use komorebi_bar_core::statics::*; - -#[derive(Parser)] -#[clap(name = "komorebi-bar", author, about, version)] -struct Opts { - /// Print the JSON schema of the configuration file and exit - #[clap(long)] - schema: bool, - /// Print a list of fonts available on this system and exit - #[clap(long)] - fonts: bool, - /// Path to a JSON or YAML configuration file - #[clap(short, long)] - #[clap(value_parser = replace_env_in_path)] - config: Option, - /// Write an example komorebi.bar.json to disk - #[clap(long)] - quickstart: bool, - /// Print a list of aliases that can be renamed to canonical variants - #[clap(long)] - #[clap(hide = true)] - aliases: bool, -} - -pub enum KomorebiEvent { - Notification(komorebi_client::Notification), - Reconnect, -} - -fn main() -> eyre::Result<()> { - komorebi_bar_core::platform::install(&os::MacBar); - - let opts: Opts = Opts::parse(); - - #[cfg(feature = "schemars")] - if opts.schema { - print!( - "{}", - komorebi_bar_core::schema::generated_text::( - komorebi_bar_core::schema::SchemaPlatform::MacOs - ) - ); - std::process::exit(0); - } - - if opts.fonts { - for font in system_fonts::query_all() { - println!("{font}"); - } - - std::process::exit(0); - } - - if std::env::var("RUST_LIB_BACKTRACE").is_err() { - unsafe { - std::env::set_var("RUST_LIB_BACKTRACE", "1"); - } - } - - color_eyre::install()?; - - if std::env::var("RUST_LOG").is_err() { - unsafe { - std::env::set_var("RUST_LOG", "info"); - } - } - - tracing::subscriber::set_global_default( - tracing_subscriber::fmt::Subscriber::builder() - .with_env_filter(EnvFilter::from_default_env()) - .finish(), - )?; - - let home_dir: PathBuf = std::env::var("KOMOREBI_CONFIG_HOME").map_or_else( - |_| { - dirs::home_dir() - .expect("there is no home directory") - .join(".config") - .join("komorebi") - }, - |home_path| { - let home = home_path.replace_env(); - - assert!( - home.is_dir(), - "$Env:KOMOREBI_CONFIG_HOME is set to '{home_path}', which is not a valid directory" - ); - - home - }, - ); - - if opts.quickstart { - let komorebi_bar_json = include_str!("../../docs/komorebi.bar.example.json").to_string(); - std::fs::write(home_dir.join("komorebi.bar.json"), komorebi_bar_json)?; - println!( - "Example komorebi.bar.json file written to {}", - home_dir.display() - ); - - std::process::exit(0); - } - - let default_config_path = home_dir.join("komorebi.bar.json"); - - let config_path = opts.config.or_else(|| { - default_config_path - .is_file() - .then_some(default_config_path.clone()) - }); - - let mut config = match config_path { - None => { - let komorebi_bar_json = - include_str!("../../docs/komorebi.bar.example.json").to_string(); - - std::fs::write(&default_config_path, komorebi_bar_json)?; - tracing::info!( - "created example configuration file: {}", - default_config_path.display() - ); - - KomobarConfig::read(&default_config_path)? - } - Some(ref config) => { - if !opts.aliases { - tracing::info!("found configuration file: {}", config.display()); - } - - KomobarConfig::read(config)? - } - }; - - let config_path = config_path.unwrap_or(default_config_path); - - if opts.aliases { - KomobarConfig::aliases(&std::fs::read_to_string(&config_path)?); - std::process::exit(0); - } - - let state = serde_json::from_str::(&komorebi_client::send_query( - &SocketMessage::State, - )?)?; - - let (usr_monitor_index, work_area_offset) = match &config.monitor { - Some(MonitorConfigOrIndex::MonitorConfig(monitor_config)) => { - (monitor_config.index, monitor_config.work_area_offset) - } - Some(MonitorConfigOrIndex::Index(idx)) => (*idx, None), - None => (0, None), - }; - let monitor_index = state - .monitor_usr_idx_map - .get(&usr_monitor_index) - .map_or(usr_monitor_index, |i| *i); - - MONITOR_RIGHT.store( - state.monitors.elements()[monitor_index].size.right, - Ordering::SeqCst, - ); - - MONITOR_TOP.store( - state.monitors.elements()[monitor_index].size.top, - Ordering::SeqCst, - ); - - MONITOR_LEFT.store( - state.monitors.elements()[monitor_index].size.left, - Ordering::SeqCst, - ); - - MONITOR_INDEX.store(monitor_index, Ordering::SeqCst); - - // use monitor.size for the actual monitor bounds (top-left origin in global coordinates) - // the window level kCGBackstopMenuLevel positions the bar behind the menu bar - match config.position { - None => { - config.position = Some(PositionConfig { - start: Some(Position { - x: state.monitors.elements()[monitor_index].size.left as f32, - y: state.monitors.elements()[monitor_index].size.top as f32, - }), - end: Some(Position { - x: state.monitors.elements()[monitor_index].size.right as f32, - y: 50.0, - }), - }) - } - Some(ref mut position) => { - if position.start.is_none() { - position.start = Some(Position { - x: state.monitors.elements()[monitor_index].size.left as f32, - y: state.monitors.elements()[monitor_index].size.top as f32, - }); - } - - if position.end.is_none() { - position.end = Some(Position { - x: state.monitors.elements()[monitor_index].size.right as f32, - y: 50.0, - }) - } - } - } - - let viewport_builder = ViewportBuilder::default() - .with_decorations(false) - .with_transparent(true) - .with_has_shadow(false) - .with_taskbar(false); - - let native_options = eframe::NativeOptions { - viewport: viewport_builder, - ..Default::default() - }; - - if let Some(rect) = &work_area_offset { - komorebi_client::send_message(&SocketMessage::MonitorWorkAreaOffset(monitor_index, *rect))?; - tracing::info!("work area offset applied to monitor: {}", monitor_index); - } - - let (tx_gui, rx_gui) = crossbeam_channel::unbounded(); - let (tx_config, rx_config) = crossbeam_channel::unbounded(); - - let mut hotwatch = Hotwatch::new()?; - let config_path_cl = config_path.clone(); - - hotwatch.watch(config_path, move |event| match event.kind { - EventKind::Modify(_) | EventKind::Remove(_) => match KomobarConfig::read(&config_path_cl) { - Ok(updated) => { - tracing::info!("configuration file updated: {}", config_path_cl.display()); - - if let Err(error) = tx_config.send(updated) { - tracing::error!("could not send configuration update to gui: {error}") - } - } - Err(error) => { - tracing::error!("{error}"); - } - }, - _ => {} - })?; - - tracing::info!("watching configuration file for changes"); - - eframe::run_native( - "komorebi-bar", - native_options, - Box::new(|cc| { - unsafe { - let app = NSApplication::sharedApplication(MainThreadMarker::new_unchecked()); - app.setActivationPolicy(NSApplicationActivationPolicy::Accessory); - } - - if let Ok(handle) = cc.window_handle() - && let RawWindowHandle::AppKit(appkit_handle) = handle.as_raw() { - // kCGBackstopMenuLevel = -20 (behind menu bar, at notch level) - const K_CG_BACKSTOP_MENU_LEVEL: isize = -20; - - unsafe { - let ns_view: &NSView = appkit_handle.ns_view.cast().as_ref(); - if let Some(ns_window) = ns_view.window() { - ns_window.setLevel(K_CG_BACKSTOP_MENU_LEVEL); - } - } - } - - let ctx_repainter = cc.egui_ctx.clone(); - std::thread::spawn(move || loop { - std::thread::sleep(Duration::from_secs(1)); - ctx_repainter.request_repaint(); - }); - - let ctx_komorebi = cc.egui_ctx.clone(); - std::thread::spawn(move || { - let subscriber_name = format!("komorebi-bar-{}", random_word::get(random_word::Lang::En)); - - let listener = komorebi_client::subscribe_with_options(&subscriber_name, SubscribeOptions { - filter_state_changes: true, - }) - .expect("could not subscribe to komorebi notifications"); - - tracing::info!("subscribed to komorebi notifications: \"{}\"", subscriber_name); - - for client in listener.incoming() { - match client { - Ok(subscription) => { - let mut buffer = Vec::new(); - let mut reader = BufReader::new(subscription); - - // this is when we know a shutdown has been sent - if matches!(reader.read_to_end(&mut buffer), Ok(0)) { - tracing::info!("disconnected from komorebi"); - - // keep trying to reconnect to komorebi - while komorebi_client::send_message( - &SocketMessage::AddSubscriberSocket(subscriber_name.clone()), - ) - .is_err() - { - std::thread::sleep(Duration::from_secs(1)); - } - - tracing::info!("reconnected to komorebi"); - - if let Err(error) = tx_gui.send(KomorebiEvent::Reconnect) { - tracing::error!("could not send komorebi reconnect event to gui thread: {error}") - } - - ctx_komorebi.request_repaint(); - continue; - } - - match String::from_utf8(buffer) { - Ok(notification_string) => { - match serde_json::from_str::( - ¬ification_string, - ) { - Ok(notification) => { - tracing::debug!("received notification from komorebi"); - - if let Err(error) = tx_gui.send(KomorebiEvent::Notification(notification)) { - tracing::error!("could not send komorebi notification update to gui thread: {error}") - } - - ctx_komorebi.request_repaint(); - } - Err(error) => { - tracing::error!("could not deserialize komorebi notification: {error}"); - } - } - } - Err(error) => { - tracing::error!( - "komorebi notification string was invalid utf8: {error}" - ) - } - } - } - Err(error) => { - tracing::error!("{error}"); - } - } - } - }); - - Ok(Box::new(Komobar::new(cc, rx_gui, rx_config, config))) - }), - ) - .map_err(|error| color_eyre::eyre::Error::msg(error.to_string())) +fn main() -> color_eyre::Result<()> { + macos::main() }