mirror of
https://github.com/LGUG2Z/komorebi.git
synced 2026-11-15 13:22:19 +01:00
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:
1 parent
146cd7ff54
commit
bdd811f2d4
9 files changed
+440
-434
No files matched your search
+23
-23
@@ -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;
|
||||
File renamed without changes.
@@ -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>(
|
||||
¬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()))
|
||||
}
|
||||
+1
-1
@@ -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;
|
||||
+10
-10
@@ -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;
|
||||
File renamed without changes.
+5
-5
@@ -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;
|
||||
+10
-10
@@ -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;
|
||||
|
||||
@@ -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>(
|
||||
¬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()
|
||||
}
|
||||
Reference in new issue
Block a user