diff --git a/Cargo.lock b/Cargo.lock index de79e213..537b4e38 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -243,6 +243,12 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "anyhow" +version = "1.0.86" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" + [[package]] name = "arboard" version = "3.4.0" @@ -2357,6 +2363,19 @@ dependencies = [ "winreg", ] +[[package]] +name = "komorebi-bar" +version = "0.1.0" +dependencies = [ + "anyhow", + "chrono", + "crossbeam-channel", + "eframe", + "komorebi-client", + "serde_json", + "sysinfo", +] + [[package]] name = "komorebi-client" version = "0.1.29" diff --git a/Cargo.toml b/Cargo.toml index 6ae588cb..2537946f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,6 +7,7 @@ members = [ "komorebi-gui", "komorebic", "komorebic-no-console", + "komorebi-bar" ] [workspace.dependencies] diff --git a/komorebi-bar/Cargo.toml b/komorebi-bar/Cargo.toml new file mode 100644 index 00000000..f32f35dc --- /dev/null +++ b/komorebi-bar/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "komorebi-bar" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +komorebi-client = { path = "../komorebi-client" } + +anyhow = "1" +chrono = "0.4" +eframe = "0.28" +serde_json = "1" +sysinfo = "0.30" +crossbeam-channel = "0.5" \ No newline at end of file diff --git a/komorebi-bar/src/date.rs b/komorebi-bar/src/date.rs new file mode 100644 index 00000000..efbdea62 --- /dev/null +++ b/komorebi-bar/src/date.rs @@ -0,0 +1,48 @@ +use crate::widget::BarWidget; + +pub enum DateFormat { + MonthDateYear, + YearMonthDate, + DateMonthYear, + DayDateMonthYear, +} + +impl DateFormat { + pub fn next(&mut self) { + match self { + DateFormat::MonthDateYear => *self = Self::YearMonthDate, + DateFormat::YearMonthDate => *self = Self::DateMonthYear, + DateFormat::DateMonthYear => *self = Self::DayDateMonthYear, + DateFormat::DayDateMonthYear => *self = Self::MonthDateYear, + }; + } + + fn fmt_string(&self) -> String { + match self { + DateFormat::MonthDateYear => String::from("%D"), + DateFormat::YearMonthDate => String::from("%F"), + DateFormat::DateMonthYear => String::from("%v"), + DateFormat::DayDateMonthYear => String::from("%A %e %B %Y"), + } + } +} + +pub struct Date { + pub format: DateFormat, +} + +impl Default for Date { + fn default() -> Self { + Self { + format: DateFormat::MonthDateYear, + } + } +} + +impl BarWidget for Date { + fn output(&mut self) -> Vec { + vec![chrono::Local::now() + .format(&self.format.fmt_string()) + .to_string()] + } +} diff --git a/komorebi-bar/src/main.rs b/komorebi-bar/src/main.rs new file mode 100644 index 00000000..fac6713b --- /dev/null +++ b/komorebi-bar/src/main.rs @@ -0,0 +1,269 @@ +mod date; +mod memory; +mod storage; +mod time; +mod widget; + +use crate::date::Date; +use crate::memory::Memory; +use crate::storage::Storage; +use crate::widget::BarWidget; +use crossbeam_channel::Receiver; +use eframe::egui; +use eframe::egui::Align; +use eframe::egui::CursorIcon; +use eframe::egui::Layout; +use eframe::egui::ViewportBuilder; +use eframe::egui::Visuals; +use komorebi_client::SocketMessage; +use std::io::BufRead; +use std::io::BufReader; +use std::process::Command; +use std::time::Duration; +use time::Time; + +fn main() -> eframe::Result<()> { + let native_options = eframe::NativeOptions { + viewport: ViewportBuilder::default() + .with_decorations(false) + // TODO: expose via config + .with_transparent(true) + // TODO: expose via config + .with_position([0.0, 0.0]) + // TODO: expose via config + .with_inner_size([5120.0, 20.0]), + ..Default::default() + }; + + komorebi_client::send_message(&SocketMessage::MonitorWorkAreaOffset( + 0, + komorebi_client::Rect { + left: 0, + top: 40, + right: 0, + bottom: 40, + }, + )) + .unwrap(); + + let (tx_gui, rx_gui) = crossbeam_channel::unbounded(); + + eframe::run_native( + "komorebi-bar", + native_options, + Box::new(|cc| { + let frame = cc.egui_ctx.clone(); + std::thread::spawn(move || { + let listener = komorebi_client::subscribe("komorebi-bar").unwrap(); + + for client in listener.incoming() { + match client { + Ok(subscription) => { + let reader = BufReader::new(subscription); + + for line in reader.lines().flatten() { + if let Ok(notification) = + serde_json::from_str::(&line) + { + tx_gui.send(notification).unwrap(); + frame.request_repaint(); + } + } + } + Err(error) => { + if error.raw_os_error().expect("could not get raw os error") == 109 { + while komorebi_client::send_message( + &SocketMessage::AddSubscriberSocket(String::from( + "komorebi-bar", + )), + ) + .is_err() + { + std::thread::sleep(Duration::from_secs(5)); + } + } + } + } + } + }); + + Ok(Box::new(Komobar::new(cc, rx_gui))) + }), + ) +} + +struct Komobar { + state_receiver: Receiver, + selected_workspace: String, + workspaces: Vec, + time: Time, + date: Date, + memory: Memory, + storage: Storage, +} + +impl Komobar { + fn new(_cc: &eframe::CreationContext<'_>, rx: Receiver) -> Self { + // Customize egui here with cc.egui_ctx.set_fonts and cc.egui_ctx.set_visuals. + // Restore app state using cc.storage (requires the "persistence" feature). + // Use the cc.gl (a glow::Context) to create graphics shaders and buffers that you can use + // for e.g. egui::PaintCallback. + + Self { + state_receiver: rx, + selected_workspace: String::new(), + workspaces: vec![], + time: Time::default(), + date: Date::default(), + memory: Memory::default(), + storage: Storage::default(), + } + } +} + +impl Komobar { + fn handle_komorebi_notification(&mut self) { + if let Ok(notification) = self.state_receiver.try_recv() { + self.workspaces = { + let mut workspaces = vec![]; + // TODO: komobar only operates on the 0th monitor (for now) + let monitor = ¬ification.state.monitors.elements()[0]; + let focused_workspace_idx = monitor.focused_workspace_idx(); + self.selected_workspace = monitor.workspaces()[focused_workspace_idx] + .name() + .to_owned() + .unwrap_or_else(|| format!("{}", focused_workspace_idx + 1)); + + for (i, ws) in monitor.workspaces().iter().enumerate() { + workspaces.push(ws.name().to_owned().unwrap_or_else(|| format!("{}", i + 1))); + } + + workspaces + } + } + } +} + +impl eframe::App for Komobar { + // TODO: I think this is needed for transparency?? + fn clear_color(&self, _visuals: &Visuals) -> [f32; 4] { + let mut background = egui::Color32::from_gray(18).to_normalized_gamma_f32(); + background[3] = 0.9; + background + } + + fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { + self.handle_komorebi_notification(); + + egui::CentralPanel::default() + .frame( + egui::Frame::none() + // TODO: make this configurable + .outer_margin(egui::Margin::symmetric(10.0, 10.0)), + ) + .show(ctx, |ui| { + ui.horizontal(|ui| { + ui.with_layout(Layout::left_to_right(Align::Center), |ui| { + // TODO: maybe this should be a widget?? + for (i, ws) in self.workspaces.iter().enumerate() { + if ui + .add(egui::SelectableLabel::new( + self.selected_workspace.eq(ws), + ws.to_string(), + )) + .clicked() + { + self.selected_workspace = ws.to_string(); + komorebi_client::send_message(&SocketMessage::MouseFollowsFocus( + false, + )) + .unwrap(); + komorebi_client::send_message( + &SocketMessage::FocusWorkspaceNumber(i), + ) + .unwrap(); + // TODO: store MFF value from state and restore that here instead of "true" + komorebi_client::send_message(&SocketMessage::MouseFollowsFocus( + true, + )) + .unwrap(); + komorebi_client::send_message(&SocketMessage::Retile).unwrap(); + } + } + }); + + // TODO: make the order configurable + // TODO: make each widget optional?? + ui.with_layout(Layout::right_to_left(Align::Center), |ui| { + for time in self.time.output() { + ctx.request_repaint(); + if ui + .label(format!("🕐 {}", time)) + .on_hover_cursor(CursorIcon::default()) + .clicked() + { + // TODO: make default format configurable + self.time.format.toggle() + } + } + + // TODO: make spacing configurable + ui.add_space(10.0); + + for date in self.date.output() { + if ui + .label(format!("📅 {}", date)) + .on_hover_cursor(CursorIcon::default()) + .clicked() + { + // TODO: make default format configurable + self.date.format.next() + }; + } + + // TODO: make spacing configurable + ui.add_space(10.0); + + for ram in self.memory.output() { + if ui + // TODO: make label configurable?? + .label(format!("🐏 {}", ram)) + .on_hover_cursor(CursorIcon::default()) + .clicked() + { + if let Err(error) = + Command::new("cmd.exe").args(["/C", "taskmgr.exe"]).output() + { + eprintln!("{}", error) + } + }; + } + + ui.add_space(10.0); + + for disk in self.storage.output() { + if ui + // TODO: Make emoji configurable?? + .label(format!("🖴 {}", disk)) + .on_hover_cursor(CursorIcon::default()) + .clicked() + { + if let Err(error) = Command::new("cmd.exe") + .args([ + "/C", + "explorer.exe", + disk.split(' ').collect::>()[0], + ]) + .output() + { + eprintln!("{}", error) + } + }; + + ui.add_space(10.0); + } + }) + }) + }); + } +} diff --git a/komorebi-bar/src/memory.rs b/komorebi-bar/src/memory.rs new file mode 100644 index 00000000..b25fe7dd --- /dev/null +++ b/komorebi-bar/src/memory.rs @@ -0,0 +1,26 @@ +use crate::widget::BarWidget; +use sysinfo::RefreshKind; +use sysinfo::System; + +pub struct Memory { + system: System, +} + +impl Default for Memory { + fn default() -> Self { + Self { + system: System::new_with_specifics( + RefreshKind::default().without_cpu().without_processes(), + ), + } + } +} + +impl BarWidget for Memory { + fn output(&mut self) -> Vec { + self.system.refresh_memory(); + let used = self.system.used_memory(); + let total = self.system.total_memory(); + vec![format!("RAM: {}%", (used * 100) / total)] + } +} diff --git a/komorebi-bar/src/storage.rs b/komorebi-bar/src/storage.rs new file mode 100644 index 00000000..4769d708 --- /dev/null +++ b/komorebi-bar/src/storage.rs @@ -0,0 +1,40 @@ +use crate::widget::BarWidget; +use sysinfo::Disks; + +pub struct Storage { + disks: Disks, +} + +impl Default for Storage { + fn default() -> Self { + Self { + disks: Disks::new_with_refreshed_list(), + } + } +} + +impl BarWidget for Storage { + fn output(&mut self) -> Vec { + self.disks.refresh(); + + let mut disks = vec![]; + + for disk in &self.disks { + let mount = disk.mount_point(); + let total = disk.total_space(); + let available = disk.available_space(); + let used = total - available; + + disks.push(format!( + "{} {}%", + mount.to_string_lossy(), + (used * 100) / total + )) + } + + disks.sort(); + disks.reverse(); + + disks + } +} diff --git a/komorebi-bar/src/time.rs b/komorebi-bar/src/time.rs new file mode 100644 index 00000000..74017ad1 --- /dev/null +++ b/komorebi-bar/src/time.rs @@ -0,0 +1,42 @@ +use crate::widget::BarWidget; + +pub enum TimeFormat { + TwelveHour, + TwentyFourHour, +} + +impl TimeFormat { + pub fn toggle(&mut self) { + match self { + TimeFormat::TwelveHour => *self = TimeFormat::TwentyFourHour, + TimeFormat::TwentyFourHour => *self = TimeFormat::TwelveHour, + }; + } + + fn fmt_string(&self) -> String { + match self { + TimeFormat::TwelveHour => String::from("%l:%M:%S %p"), + TimeFormat::TwentyFourHour => String::from("%T"), + } + } +} + +pub struct Time { + pub format: TimeFormat, +} + +impl Default for Time { + fn default() -> Self { + Self { + format: TimeFormat::TwelveHour, + } + } +} + +impl BarWidget for Time { + fn output(&mut self) -> Vec { + vec![chrono::Local::now() + .format(&self.format.fmt_string()) + .to_string()] + } +} diff --git a/komorebi-bar/src/widget.rs b/komorebi-bar/src/widget.rs new file mode 100644 index 00000000..2a89d2d3 --- /dev/null +++ b/komorebi-bar/src/widget.rs @@ -0,0 +1,3 @@ +pub trait BarWidget { + fn output(&mut self) -> Vec; +}