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).
This commit is contained in:
LGUG2Z committed 2026-10-31 12:41:55 -07:00
1 parent 146cd7ff54
commit bdd811f2d4
9 files changed
+440 -434

No files matched your search

@@ -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;
@@ -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<PathBuf>,
/// 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::<KomobarConfig>(
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::State>(&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::<komorebi_client::Notification>(
&notification_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()))
}
@@ -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;
@@ -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;
@@ -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;
@@ -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;
+4 -385
View File
@@ -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<PathBuf>,
/// 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::<KomobarConfig>(
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::State>(&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::<komorebi_client::Notification>(
&notification_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()
}