feat(bar): add komorebi-bar

This commit adds an initial version of the komorebi status bar.

At this point the bar is still considered "alpha" and the configuration
format may see some small breaking changes based on usage and feedback.

There is an accompanying video series which details the creation of this
bar on YouTube: https://www.youtube.com/watch?v=x2Z5-K05bHs

Some high level notes on the bar:

* An external application which exclusively consumes komorebi_client's
  public API surface - anyone can create this without hacking directly
  on komorebi's internals
* Generally a very simple bar with limited configuration options - users
  who want more configurability should use alternatives such as yasb or
  zebar
* Scope is deliberately limited to provide a tighter, more focused
  experience: Windows-only, komorebi-only, single-monitor-only,
  horizontal-only
* No support for custom widgets or templating
* Colours are controlled exclusively through themes which adhere to a
  palette framework such as Base16 or Catppuccin (and possibly others in
  the future)

This commit contains all of the commits listed:

e5fa03c33c
feat(bar): initial commit

b3990590f3
feat(bar): add config struct with basic opts

bc2f4a172e
feat(bar): handle komorebi restarts gracefully

ca6bf69ac7
feat(bar): add basic widget config opts

18358efed8
feat(bar): add interactive layout and media widgets

92bb9f680b
perf(bar): use explicit redraw and data refresh strategies

8e74e97706
feat(bar): add battery and network widgets

fdc7706d23
feat(bar): add custom font loader

025162769b
feat(bar): allow right side widget ordering

a1688691cf
feat(bar): add app icon next to focused window title

9f78739c3f
feat(bar): add komorebi widget (+config) and themes

a4ef85859e
feat(bar): use phosphor icons for uniformity

e99138a97e
feat(bar): add first pass at configuration loader

d6ccf4cf9a
feat(bar): add logging and config hotwatch

34d2431947
feat(bar): handle monocle containers in komorebi widget

7907dfeb79
feat(bar): add optional data refresh intervals to config

96a9cb320e
feat(bar): add flag to list system fonts

42b7a13693
feat(bar): add activity to network widget

ac38f52407
feat(bar): to_pretty_bytes on network activity

6803ffd741
feat(bar): configurable network activity fill char len

7d7a5d758d99808cd2b81da2b3ddbb11c52aa92f
ci(github): add bar to wix and goreleaser configs

da307e36fc1faf84ecca3f91811fdd15f70ef2ff
feat(bar): expand theme sources

c580ff7899889309dfa849ad4fb05b80b6af8d9b
feat(bar): add accent config for themes

bc4dabda4a941c0c9764fae2c8d11abbfdc0a9f5
feat(bar): add accents to widget emojis

a574837529dd6c5add73edf394c1c9c2e6cc6315
feat(bar): add to hard-coded float identifiers

ff41b552613f911e56b1790e68389525ee7e603c
chore(deps): bump base16-egui-themes
This commit is contained in:
LGUG2Z
2024-07-25 17:30:01 -07:00
parent c06d9afa3b
commit bc67936dd3
23 changed files with 4565 additions and 61 deletions
+280
View File
@@ -0,0 +1,280 @@
mod bar;
mod battery;
mod config;
mod date;
mod komorebi;
mod media;
mod memory;
mod network;
mod storage;
mod time;
mod widget;
use crate::bar::Komobar;
use crate::config::KomobarConfig;
use crate::config::Position;
use clap::Parser;
use eframe::egui::ViewportBuilder;
use font_loader::system_fonts;
use hotwatch::EventKind;
use hotwatch::Hotwatch;
use komorebi_client::SocketMessage;
use schemars::gen::SchemaSettings;
use std::io::BufReader;
use std::io::Read;
use std::path::PathBuf;
use std::sync::Arc;
use std::time::Duration;
use tracing_subscriber::EnvFilter;
pub static WIDGET_SPACING: f32 = 10.0;
#[derive(Parser)]
#[clap(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)]
config: Option<PathBuf>,
}
fn main() -> color_eyre::Result<()> {
let opts: Opts = Opts::parse();
if opts.schema {
let settings = SchemaSettings::default().with(|s| {
s.option_nullable = false;
s.option_add_null_type = false;
s.inline_subschemas = true;
});
let gen = settings.into_generator();
let socket_message = gen.into_root_schema_for::<KomobarConfig>();
let schema = serde_json::to_string_pretty(&socket_message)?;
println!("{schema}");
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() {
std::env::set_var("RUST_LIB_BACKTRACE", "1");
}
color_eyre::install()?;
if std::env::var("RUST_LOG").is_err() {
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"),
|home_path| {
let home = PathBuf::from(&home_path);
if home.as_path().is_dir() {
home
} else {
panic!("$Env:KOMOREBI_CONFIG_HOME is set to '{home_path}', which is not a valid directory");
}
},
);
let config_path = opts.config.map_or_else(
|| {
let mut config = home_dir.join("komorebi.bar.json");
if !config.is_file() {
config.pop();
config.push("komorebi.bar.yaml");
}
if !config.is_file() {
None
} else {
Some(config)
}
},
Option::from,
);
let config = match config_path {
None => panic!(
"no komorebi.bar.json or komorebi.bar.yaml found in {}",
home_dir.as_path().to_string_lossy()
),
Some(ref config) => {
tracing::info!(
"found configuration file: {}",
config.as_path().to_string_lossy()
);
KomobarConfig::read(config)?
}
};
let config_path = config_path.unwrap();
let mut viewport_builder = ViewportBuilder::default()
.with_decorations(false)
// .with_transparent(config.transparent)
.with_taskbar(false)
.with_position(Position { x: 0.0, y: 0.0 })
.with_inner_size({
let state = serde_json::from_str::<komorebi_client::State>(
&komorebi_client::send_query(&SocketMessage::State).unwrap(),
)?;
Position {
x: state.monitors.elements()[config.monitor.index].size().right as f32,
y: 20.0,
}
});
if let Some(viewport) = &config.viewport {
if let Some(position) = &viewport.position {
let b = viewport_builder.clone();
viewport_builder = b.with_position(*position);
}
if let Some(inner_size) = &viewport.inner_size {
let b = viewport_builder.clone();
viewport_builder = b.with_inner_size(*inner_size);
}
}
let native_options = eframe::NativeOptions {
viewport: viewport_builder,
..Default::default()
};
if let Some(rect) = &config.monitor.work_area_offset {
komorebi_client::send_message(&SocketMessage::MonitorWorkAreaOffset(
config.monitor.index,
*rect,
))?;
tracing::info!(
"work area offset applied to monitor: {}",
config.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) => {
tx_config.send(updated).unwrap();
tracing::info!(
"configuration file updated: {}",
config_path_cl.as_path().to_string_lossy()
);
}
Err(error) => {
tracing::error!("{error}");
}
},
_ => {}
})?;
tracing::info!("watching configuration file for changes");
let config_arc = Arc::new(config);
eframe::run_native(
"komorebi-bar",
native_options,
Box::new(|cc| {
let config_cl = config_arc.clone();
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 listener = komorebi_client::subscribe("komorebi-bar").unwrap();
tracing::info!("subscribed to komorebi notifications: \"komorebi-bar\"");
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(String::from(
"komorebi-bar",
)),
)
.is_err()
{
std::thread::sleep(Duration::from_secs(1));
}
tracing::info!("reconnected to komorebi");
if let Some(rect) = &config_cl.monitor.work_area_offset {
while komorebi_client::send_message(
&SocketMessage::MonitorWorkAreaOffset(
config_cl.monitor.index,
*rect,
),
)
.is_err()
{
std::thread::sleep(Duration::from_secs(1));
}
}
}
if let Ok(notification) =
serde_json::from_str::<komorebi_client::Notification>(
&String::from_utf8(buffer).unwrap(),
)
{
tracing::debug!("received notification from komorebi");
tx_gui.send(notification).unwrap();
ctx_komorebi.request_repaint();
}
}
Err(error) => {
tracing::error!("{error}");
}
}
}
});
Ok(Box::new(Komobar::new(cc, rx_gui, rx_config, config_arc)))
}),
)
.map_err(|error| color_eyre::eyre::Error::msg(error.to_string()))
}