summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorcashmere <cashmere@autistici.org>2026-08-16 08:21:37 +0200
committercashmere <cashmere@autistici.org>2026-08-16 08:21:37 +0200
commit4901b5d77d73db59f0c1545719ec6d1404aad979 (patch)
tree0b3233e82387ffe7a9cd5c0934971a665e0575a9 /src
parent91b84d1f1bfbc5de2eb140a48185a5252b1b592e (diff)
downloadi3bar-mango-main.tar.gz
Run detached and reload config liveHEADmain
Diffstat (limited to 'src')
-rw-r--r--src/bar.rs25
-rw-r--r--src/blocks_cache.rs9
-rw-r--r--src/config.rs98
-rw-r--r--src/event_loop.rs11
-rw-r--r--src/main.rs100
-rw-r--r--src/state.rs44
6 files changed, 238 insertions, 49 deletions
diff --git a/src/bar.rs b/src/bar.rs
index 115641d..7530f2e 100644
--- a/src/bar.rs
+++ b/src/bar.rs
@@ -132,6 +132,21 @@ impl Bar {
self.focused_window_computed = None;
}
+ pub fn config_changed(&mut self, conn: &mut Connection<State>, shared_state: &SharedState) {
+ self.height = shared_state.config.height;
+ self.tags_computed.clear();
+ self.layout_name_computed = None;
+ self.mode_computed = None;
+ self.focused_window_computed = None;
+ self.tray_toggle_computed = None;
+ self.layer_surface
+ .set_layer(conn, shared_state.config.layer.into());
+ if !self.hidden {
+ self.hide(conn);
+ self.show(conn, shared_state);
+ }
+ }
+
pub fn click(
&mut self,
conn: &mut Connection<State>,
@@ -525,7 +540,10 @@ impl Bar {
let mut rendered = Vec::new();
if expanded {
for item in &visible {
- let label = match (cfg.show_name, cfg.show_description && !item.tooltip.is_empty()) {
+ let label = match (
+ cfg.show_name,
+ cfg.show_description && !item.tooltip.is_empty(),
+ ) {
(true, true) if item.tooltip != item.title => {
format!("{} \u{2014} {}", item.title, item.tooltip)
}
@@ -583,7 +601,10 @@ impl Bar {
let icon_w = icon.width() as f64 * scale;
let icon_h = icon.height() as f64 * scale;
context.save().unwrap();
- context.translate(item_x + (icon_size - icon_w) / 2.0, (full_height - icon_h) / 2.0);
+ context.translate(
+ item_x + (icon_size - icon_w) / 2.0,
+ (full_height - icon_h) / 2.0,
+ );
context.scale(scale, scale);
context.set_source_surface(icon, 0.0, 0.0).unwrap();
context.paint().unwrap();
diff --git a/src/blocks_cache.rs b/src/blocks_cache.rs
index 1e607a6..555ac49 100644
--- a/src/blocks_cache.rs
+++ b/src/blocks_cache.rs
@@ -32,6 +32,15 @@ impl BlocksCache {
pub fn get_computed(&self) -> &[ComputedBlock] {
&self.computed
}
+
+ pub fn config_changed(&mut self, config: &Config) {
+ let blocks = self
+ .computed
+ .drain(..)
+ .map(|computed| computed.block)
+ .collect();
+ self.process_new_blocks(config, blocks);
+ }
}
impl ComputedBlock {
diff --git a/src/config.rs b/src/config.rs
index 4e41069..d354839 100644
--- a/src/config.rs
+++ b/src/config.rs
@@ -1,11 +1,15 @@
use crate::color::Color;
use crate::protocol::{zwlr_layer_shell_v1, zwlr_layer_surface_v1};
use anyhow::{Context, Result};
+use inotify::{Inotify, WatchMask};
use pangocairo::pango::FontDescription;
use serde::{Deserialize, de};
use std::collections::HashMap;
+use std::ffi::OsString;
use std::fs::read_to_string;
+use std::io;
use std::ops::Deref;
+use std::os::fd::{AsRawFd, RawFd};
use std::path::{Path, PathBuf};
use std::{env, fmt};
@@ -114,27 +118,21 @@ impl Default for Config {
impl Config {
pub fn new(path: Option<&Path>) -> Result<Self> {
- let buf;
-
- let path = match path {
- Some(path) => Some(path),
- None => {
- buf = config_path();
- buf.as_deref()
- }
- };
-
- Ok(match path {
+ match Self::path(path) {
Some(config_path) => {
let config = read_to_string(config_path).context("Failed to read configuration")?;
- toml::from_str(&config).context("Failed to deserialize configuration")?
+ toml::from_str(&config).context("Failed to deserialize configuration")
}
None => {
eprintln!("Could not find the configuration path");
eprintln!("Using default configuration");
- Self::default()
+ Ok(Self::default())
}
- })
+ }
+ }
+
+ pub fn path(path: Option<&Path>) -> Option<PathBuf> {
+ path.map(Path::to_path_buf).or_else(config_path)
}
pub fn output_enabled(&self, output: &str) -> bool {
@@ -145,6 +143,47 @@ impl Config {
}
}
+pub struct ConfigWatcher {
+ inotify: Inotify,
+ buffer: Vec<u8>,
+ filename: OsString,
+}
+
+impl ConfigWatcher {
+ pub fn new(path: &Path) -> io::Result<Self> {
+ let directory = path
+ .parent()
+ .filter(|parent| !parent.as_os_str().is_empty())
+ .unwrap_or_else(|| Path::new("."));
+ let filename = path
+ .file_name()
+ .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "invalid config path"))?
+ .to_os_string();
+ let inotify = Inotify::init()?;
+ inotify.watches().add(
+ directory,
+ WatchMask::CLOSE_WRITE | WatchMask::CREATE | WatchMask::MOVED_TO,
+ )?;
+ Ok(Self {
+ inotify,
+ buffer: vec![0; 4096],
+ filename,
+ })
+ }
+
+ pub fn fd(&self) -> RawFd {
+ self.inotify.as_raw_fd()
+ }
+
+ pub fn changed(&mut self) -> io::Result<bool> {
+ let mut changed = false;
+ for event in self.inotify.read_events(&mut self.buffer)? {
+ changed |= event.name.is_some_and(|name| name == self.filename);
+ }
+ Ok(changed)
+ }
+}
+
fn config_dir() -> Option<PathBuf> {
env::var_os("XDG_CONFIG_HOME")
.map(PathBuf::from)
@@ -296,3 +335,34 @@ impl<'de> de::Deserialize<'de> for Font {
deserializer.deserialize_str(FontVisitor)
}
}
+
+#[cfg(test)]
+mod tests {
+ use super::ConfigWatcher;
+ use std::fs;
+ use std::time::{SystemTime, UNIX_EPOCH};
+
+ #[test]
+ fn watcher_detects_overwrite_and_atomic_replacement() {
+ let unique = SystemTime::now()
+ .duration_since(UNIX_EPOCH)
+ .unwrap()
+ .as_nanos();
+ let directory =
+ std::env::temp_dir().join(format!("i3bar-mango-{unique}-{}", std::process::id()));
+ let config_path = directory.join("config.toml");
+ let replacement_path = directory.join("config.toml.new");
+ fs::create_dir(&directory).unwrap();
+ fs::write(&config_path, "height = 24").unwrap();
+
+ let mut watcher = ConfigWatcher::new(&config_path).unwrap();
+ fs::write(&config_path, "height = 25").unwrap();
+ assert!(watcher.changed().unwrap());
+
+ fs::write(&replacement_path, "height = 26").unwrap();
+ fs::rename(&replacement_path, &config_path).unwrap();
+ assert!(watcher.changed().unwrap());
+
+ fs::remove_dir_all(directory).unwrap();
+ }
+}
diff --git a/src/event_loop.rs b/src/event_loop.rs
index 89f6f81..28fc0dc 100644
--- a/src/event_loop.rs
+++ b/src/event_loop.rs
@@ -7,7 +7,7 @@ use wayrs_client::Connection;
use crate::state::State;
-type Callback = Box<dyn FnMut(EventLoopCtx) -> Result<Action>>;
+pub type Callback = Box<dyn FnMut(EventLoopCtx) -> Result<Action>>;
pub struct EventLoopCtx<'a> {
pub conn: &'a mut Connection<State>,
@@ -22,6 +22,7 @@ pub struct EventLoop {
pub enum Action {
Keep,
+ KeepAndRegister(RawFd, Callback),
Unregister,
}
@@ -80,6 +81,10 @@ impl EventLoop {
Action::Keep => {
self.cbs.insert(fd.fd, cb);
}
+ Action::KeepAndRegister(new_fd, new_cb) => {
+ self.cbs.insert(fd.fd, cb);
+ self.cbs.insert(new_fd, new_cb);
+ }
Action::Unregister => (),
}
}
@@ -88,6 +93,10 @@ impl EventLoop {
for mut cb in self.on_idle.drain(..) {
match cb(EventLoopCtx { conn, state })? {
Action::Keep => on_idle_scratch.push(cb),
+ Action::KeepAndRegister(fd, new_cb) => {
+ on_idle_scratch.push(cb);
+ self.cbs.insert(fd, new_cb);
+ }
Action::Unregister => (),
}
}
diff --git a/src/main.rs b/src/main.rs
index a4751d5..7c475c3 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -20,28 +20,74 @@ mod tray;
mod utils;
mod wm_info_provider;
+use std::env;
use std::io::{self, ErrorKind, Read};
-use std::os::fd::AsRawFd;
+use std::os::fd::{AsRawFd, RawFd};
use std::path::PathBuf;
+use anyhow::Context;
use clap::Parser;
+use daemonize::Daemonize;
use signal_hook::consts::*;
use wayrs_client::{Connection, IoMode};
-use event_loop::EventLoop;
+use config::{Config, ConfigWatcher};
+use event_loop::{Action, Callback, EventLoop};
use state::State;
#[derive(Parser)]
#[command(author, version, about, long_about = None)]
struct Cli {
- /// The path to a config file.
- #[arg(short, long, value_name = "FILE")]
+ #[arg(short, long, value_name = "FILE", help = "The path to a config file")]
config: Option<PathBuf>,
+ #[arg(long, help = "Keep the bar attached to the current terminal")]
+ foreground: bool,
+}
+
+fn status_callback(fd: RawFd) -> Callback {
+ Box::new(move |ctx| {
+ if ctx.state.status_cmd_fd() != Some(fd) {
+ return Ok(Action::Unregister);
+ }
+ match ctx
+ .state
+ .shared_state
+ .status_cmd
+ .as_mut()
+ .unwrap()
+ .receive_blocks()
+ {
+ Ok(None) => Ok(Action::Keep),
+ Ok(Some(blocks)) => {
+ ctx.state.set_blocks(ctx.conn, blocks);
+ Ok(Action::Keep)
+ }
+ Err(e) => {
+ let _ = ctx
+ .state
+ .shared_state
+ .status_cmd
+ .take()
+ .unwrap()
+ .child
+ .kill();
+ ctx.state.set_error(ctx.conn, "status", e);
+ Ok(Action::Unregister)
+ }
+ }
+ })
}
fn main() -> anyhow::Result<()> {
let args = Cli::parse();
-
+ let config_path = Config::path(args.config.as_deref());
+ if !args.foreground {
+ let working_directory = env::current_dir().context("failed to read working directory")?;
+ Daemonize::new()
+ .working_directory(working_directory)
+ .start()
+ .context("failed to start in the background")?;
+ }
let (mut sig_read, sig_write) = io::pipe()?;
signal_hook::low_level::pipe::register(SIGUSR1, sig_write)?;
@@ -49,7 +95,7 @@ fn main() -> anyhow::Result<()> {
conn.blocking_roundtrip()?;
let mut el = EventLoop::new();
- let mut state = State::new(&mut conn, &mut el, args.config.as_deref());
+ let mut state = State::new(&mut conn, &mut el, config_path.as_deref());
conn.flush(IoMode::Blocking)?;
el.add_on_idle(|ctx| {
@@ -73,31 +119,23 @@ fn main() -> anyhow::Result<()> {
});
if let Some(fd) = state.status_cmd_fd() {
- el.register_with_fd(fd, |ctx| {
- match ctx
- .state
- .shared_state
- .status_cmd
- .as_mut()
- .unwrap()
- .receive_blocks()
- {
- Ok(None) => Ok(event_loop::Action::Keep),
- Ok(Some(blocks)) => {
- ctx.state.set_blocks(ctx.conn, blocks);
- Ok(event_loop::Action::Keep)
- }
- Err(e) => {
- let _ = ctx
- .state
- .shared_state
- .status_cmd
- .take()
- .unwrap()
- .child
- .kill();
- ctx.state.set_error(ctx.conn, "status", e);
- Ok(event_loop::Action::Unregister)
+ el.register_with_fd(fd, status_callback(fd));
+ }
+
+ if let Some(config_path) = config_path {
+ let mut watcher = ConfigWatcher::new(&config_path)
+ .with_context(|| format!("failed to watch {}", config_path.display()))?;
+ let watcher_fd = watcher.fd();
+ el.register_with_fd(watcher_fd, move |ctx| {
+ if !watcher.changed()? {
+ return Ok(Action::Keep);
+ }
+ match ctx.state.reload_config(ctx.conn, &config_path) {
+ Ok(Some(fd)) => Ok(Action::KeepAndRegister(fd, status_callback(fd))),
+ Ok(None) => Ok(Action::Keep),
+ Err(error) => {
+ ctx.state.set_error(ctx.conn, "config", error);
+ Ok(Action::Keep)
}
}
});
diff --git a/src/state.rs b/src/state.rs
index 2853731..dc7ff15 100644
--- a/src/state.rs
+++ b/src/state.rs
@@ -188,6 +188,44 @@ impl State {
.map(|cmd| cmd.output.as_raw_fd())
}
+ pub fn reload_config(
+ &mut self,
+ conn: &mut Connection<Self>,
+ config_path: &Path,
+ ) -> anyhow::Result<Option<RawFd>> {
+ let config = Config::new(Some(config_path))?;
+ let command_changed = config.command != self.shared_state.config.command;
+ let status_cmd = if command_changed {
+ match config.command.as_deref() {
+ Some(command) => Some(StatusCmd::new(command)?),
+ None => None,
+ }
+ } else {
+ self.shared_state.status_cmd.take()
+ };
+
+ if command_changed && let Some(mut old_status_cmd) = self.shared_state.status_cmd.take() {
+ let _ = old_status_cmd.child.kill();
+ }
+
+ self.shared_state.config = config;
+ self.shared_state.status_cmd = status_cmd;
+ if command_changed || self.has_error {
+ self.shared_state.blocks_cache = BlocksCache::default();
+ } else {
+ self.shared_state
+ .blocks_cache
+ .config_changed(&self.shared_state.config);
+ }
+ self.has_error = false;
+ for bar in &mut self.bars {
+ bar.config_changed(conn, &self.shared_state);
+ }
+ self.draw_all(conn);
+
+ Ok(command_changed.then(|| self.status_cmd_fd()).flatten())
+ }
+
pub fn register_output(&mut self, conn: &mut Connection<Self>, output: Output) {
if !self.shared_state.config.output_enabled(&output.name) {
return;
@@ -264,7 +302,11 @@ impl State {
});
}
- pub fn focused_window_updated(&mut self, conn: &mut Connection<Self>, output: Option<WlOutput>) {
+ pub fn focused_window_updated(
+ &mut self,
+ conn: &mut Connection<Self>,
+ output: Option<WlOutput>,
+ ) {
self.for_each_bar(output, |bar, ss| {
bar.set_focused_window(ss.wm_info_provider.get_focused_window(&bar.output));
bar.frame(conn, ss);