mirror of
https://github.com/LGUG2Z/komorebi.git
synced 2026-04-23 00:58:37 +02:00
feat(bar): initial commit
This commit is contained in:
19
Cargo.lock
generated
19
Cargo.lock
generated
@@ -243,6 +243,12 @@ dependencies = [
|
|||||||
"windows-sys 0.52.0",
|
"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]]
|
[[package]]
|
||||||
name = "arboard"
|
name = "arboard"
|
||||||
version = "3.4.0"
|
version = "3.4.0"
|
||||||
@@ -2357,6 +2363,19 @@ dependencies = [
|
|||||||
"winreg",
|
"winreg",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "komorebi-bar"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"chrono",
|
||||||
|
"crossbeam-channel",
|
||||||
|
"eframe",
|
||||||
|
"komorebi-client",
|
||||||
|
"serde_json",
|
||||||
|
"sysinfo",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "komorebi-client"
|
name = "komorebi-client"
|
||||||
version = "0.1.29"
|
version = "0.1.29"
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ members = [
|
|||||||
"komorebi-gui",
|
"komorebi-gui",
|
||||||
"komorebic",
|
"komorebic",
|
||||||
"komorebic-no-console",
|
"komorebic-no-console",
|
||||||
|
"komorebi-bar"
|
||||||
]
|
]
|
||||||
|
|
||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
|
|||||||
16
komorebi-bar/Cargo.toml
Normal file
16
komorebi-bar/Cargo.toml
Normal file
@@ -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"
|
||||||
48
komorebi-bar/src/date.rs
Normal file
48
komorebi-bar/src/date.rs
Normal file
@@ -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<String> {
|
||||||
|
vec![chrono::Local::now()
|
||||||
|
.format(&self.format.fmt_string())
|
||||||
|
.to_string()]
|
||||||
|
}
|
||||||
|
}
|
||||||
269
komorebi-bar/src/main.rs
Normal file
269
komorebi-bar/src/main.rs
Normal file
@@ -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::<komorebi_client::Notification>(&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<komorebi_client::Notification>,
|
||||||
|
selected_workspace: String,
|
||||||
|
workspaces: Vec<String>,
|
||||||
|
time: Time,
|
||||||
|
date: Date,
|
||||||
|
memory: Memory,
|
||||||
|
storage: Storage,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Komobar {
|
||||||
|
fn new(_cc: &eframe::CreationContext<'_>, rx: Receiver<komorebi_client::Notification>) -> 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::<Vec<&str>>()[0],
|
||||||
|
])
|
||||||
|
.output()
|
||||||
|
{
|
||||||
|
eprintln!("{}", error)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
ui.add_space(10.0);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
26
komorebi-bar/src/memory.rs
Normal file
26
komorebi-bar/src/memory.rs
Normal file
@@ -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<String> {
|
||||||
|
self.system.refresh_memory();
|
||||||
|
let used = self.system.used_memory();
|
||||||
|
let total = self.system.total_memory();
|
||||||
|
vec![format!("RAM: {}%", (used * 100) / total)]
|
||||||
|
}
|
||||||
|
}
|
||||||
40
komorebi-bar/src/storage.rs
Normal file
40
komorebi-bar/src/storage.rs
Normal file
@@ -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<String> {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
42
komorebi-bar/src/time.rs
Normal file
42
komorebi-bar/src/time.rs
Normal file
@@ -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<String> {
|
||||||
|
vec![chrono::Local::now()
|
||||||
|
.format(&self.format.fmt_string())
|
||||||
|
.to_string()]
|
||||||
|
}
|
||||||
|
}
|
||||||
3
komorebi-bar/src/widget.rs
Normal file
3
komorebi-bar/src/widget.rs
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
pub trait BarWidget {
|
||||||
|
fn output(&mut self) -> Vec<String>;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user