commit 1569735cdc71dbb74da49b64144d95a3f451a9a6
parent e7a279d0e5bb7d80a85546929edf0f0863e7480d
Author: 1jehuang <[email protected]>
Date: Fri, 19 Dec 2025 04:07:39 -0800
Implement daemon mode with persistent niri socket connection
- Uses niri-ipc event stream for real-time updates
- No signal/interval needed - outputs JSON on relevant events
- Auto-reconnects on socket errors
- Near-zero CPU when idle
Diffstat:
| M | Cargo.lock | | | 153 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| M | Cargo.toml | | | 1 | + |
| M | src/main.rs | | | 132 | ++++++++++++++++++++++++++++++++++++++++++++++--------------------------------- |
3 files changed, 231 insertions(+), 55 deletions(-)
diff --git a/Cargo.lock b/Cargo.lock
@@ -3,6 +3,114 @@
version = 4
[[package]]
+name = "anstream"
+version = "0.6.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a"
+dependencies = [
+ "anstyle",
+ "anstyle-parse",
+ "anstyle-query",
+ "anstyle-wincon",
+ "colorchoice",
+ "is_terminal_polyfill",
+ "utf8parse",
+]
+
+[[package]]
+name = "anstyle"
+version = "1.0.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78"
+
+[[package]]
+name = "anstyle-parse"
+version = "0.2.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2"
+dependencies = [
+ "utf8parse",
+]
+
+[[package]]
+name = "anstyle-query"
+version = "1.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
+dependencies = [
+ "windows-sys",
+]
+
+[[package]]
+name = "anstyle-wincon"
+version = "3.0.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
+dependencies = [
+ "anstyle",
+ "once_cell_polyfill",
+ "windows-sys",
+]
+
+[[package]]
+name = "clap"
+version = "4.5.53"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c9e340e012a1bf4935f5282ed1436d1489548e8f72308207ea5df0e23d2d03f8"
+dependencies = [
+ "clap_builder",
+ "clap_derive",
+]
+
+[[package]]
+name = "clap_builder"
+version = "4.5.53"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d76b5d13eaa18c901fd2f7fca939fefe3a0727a953561fefdf3b2922b8569d00"
+dependencies = [
+ "anstream",
+ "anstyle",
+ "clap_lex",
+ "strsim",
+]
+
+[[package]]
+name = "clap_derive"
+version = "4.5.49"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2a0b5487afeab2deb2ff4e03a807ad1a03ac532ff5a2cee5d86884440c7f7671"
+dependencies = [
+ "heck",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "clap_lex"
+version = "0.7.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d"
+
+[[package]]
+name = "colorchoice"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75"
+
+[[package]]
+name = "heck"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
+
+[[package]]
+name = "is_terminal_polyfill"
+version = "1.70.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
+
+[[package]]
name = "itoa"
version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -15,14 +123,32 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273"
[[package]]
+name = "niri-ipc"
+version = "25.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "12bd9eb4e437f5282ce853cf66837658379cb537b4b6bae687da59246005329a"
+dependencies = [
+ "clap",
+ "serde",
+ "serde_json",
+]
+
+[[package]]
name = "niri-workspaces"
version = "0.1.0"
dependencies = [
+ "niri-ipc",
"serde",
"serde_json",
]
[[package]]
+name = "once_cell_polyfill"
+version = "1.70.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
+
+[[package]]
name = "proc-macro2"
version = "1.0.103"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -90,6 +216,12 @@ dependencies = [
]
[[package]]
+name = "strsim"
+version = "0.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
+
+[[package]]
name = "syn"
version = "2.0.111"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -105,3 +237,24 @@ name = "unicode-ident"
version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5"
+
+[[package]]
+name = "utf8parse"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
+
+[[package]]
+name = "windows-link"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
+
+[[package]]
+name = "windows-sys"
+version = "0.61.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
+dependencies = [
+ "windows-link",
+]
diff --git a/Cargo.toml b/Cargo.toml
@@ -4,6 +4,7 @@ version = "0.1.0"
edition = "2021"
[dependencies]
+niri-ipc = { version = "25.11.0", features = ["clap"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
diff --git a/src/main.rs b/src/main.rs
@@ -1,66 +1,85 @@
-use serde::Deserialize;
+use niri_ipc::socket::Socket;
+use niri_ipc::{Event, Request, Response, Window, Workspace};
use std::collections::HashMap;
use std::fs;
-use std::process::Command;
-
-#[derive(Deserialize)]
-struct Workspace {
- id: i64,
- idx: i32,
- is_focused: bool,
- active_window_id: Option<i64>,
-}
-#[derive(Deserialize)]
-struct Window {
- id: i64,
- workspace_id: i64,
- app_id: String,
- title: String,
- is_focused: bool,
- pid: Option<u32>,
+fn main() {
+ loop {
+ if let Err(e) = run_daemon() {
+ eprintln!("Daemon error: {}, reconnecting in 1s...", e);
+ std::thread::sleep(std::time::Duration::from_secs(1));
+ }
+ }
}
-fn main() {
- // Get data from niri in parallel
- let ws_handle = std::thread::spawn(|| {
- Command::new("niri")
- .args(["msg", "-j", "workspaces"])
- .output()
- .ok()
- .and_then(|o| String::from_utf8(o.stdout).ok())
- });
-
- let win_handle = std::thread::spawn(|| {
- Command::new("niri")
- .args(["msg", "-j", "windows"])
- .output()
- .ok()
- .and_then(|o| String::from_utf8(o.stdout).ok())
- });
-
- let (workspaces_json, windows_json) = match (ws_handle.join().ok().flatten(), win_handle.join().ok().flatten()) {
- (Some(w), Some(win)) => (w, win),
- _ => {
- println!(r#"{{"text": "", "tooltip": "niri not running"}}"#);
- return;
+fn run_daemon() -> Result<(), Box<dyn std::error::Error>> {
+ // Get initial state and output
+ let (workspaces, windows) = fetch_state()?;
+ output_status(&workspaces, &windows);
+
+ // Subscribe to event stream
+ let mut socket = Socket::connect()?;
+ let reply = socket.send(Request::EventStream)?;
+
+ match reply {
+ Ok(Response::Handled) => {}
+ Ok(other) => return Err(format!("Unexpected response: {:?}", other).into()),
+ Err(msg) => return Err(msg.into()),
+ }
+
+ let mut read_event = socket.read_events();
+
+ loop {
+ let event = read_event()?;
+
+ // Only update on relevant events
+ match event {
+ Event::WorkspacesChanged { .. }
+ | Event::WorkspaceActivated { .. }
+ | Event::WindowsChanged { .. }
+ | Event::WindowOpenedOrChanged { .. }
+ | Event::WindowClosed { .. }
+ | Event::WindowFocusChanged { .. } => {
+ if let Ok((ws, win)) = fetch_state() {
+ output_status(&ws, &win);
+ }
+ }
+ _ => {}
}
+ }
+}
+
+fn fetch_state() -> Result<(Vec<Workspace>, Vec<Window>), Box<dyn std::error::Error>> {
+ let mut socket = Socket::connect()?;
+
+ let workspaces = match socket.send(Request::Workspaces)? {
+ Ok(Response::Workspaces(ws)) => ws,
+ Ok(other) => return Err(format!("Unexpected: {:?}", other).into()),
+ Err(msg) => return Err(msg.into()),
};
- let mut workspaces: Vec<Workspace> = serde_json::from_str(&workspaces_json).unwrap_or_default();
- let windows: Vec<Window> = serde_json::from_str(&windows_json).unwrap_or_default();
+ let mut socket = Socket::connect()?;
+ let windows = match socket.send(Request::Windows)? {
+ Ok(Response::Windows(ws)) => ws,
+ Ok(other) => return Err(format!("Unexpected: {:?}", other).into()),
+ Err(msg) => return Err(msg.into()),
+ };
- workspaces.sort_by_key(|w| w.idx);
+ Ok((workspaces, windows))
+}
- // Pre-compute terminal apps by checking /proc
- let terminal_apps: HashMap<u32, &str> = windows
+fn output_status(workspaces: &[Workspace], windows: &[Window]) {
+ // Pre-compute terminal apps
+ let terminal_apps: HashMap<i32, &str> = windows
.iter()
- .filter(|w| is_terminal(&w.app_id))
- .filter_map(|w| w.pid)
- .map(|pid| (pid, detect_terminal_app(pid)))
+ .filter(|w| is_terminal(w.app_id.as_deref().unwrap_or("")))
+ .filter_map(|w| w.pid.map(|pid| (pid, detect_terminal_app(pid as u32))))
.filter(|(_, app)| !app.is_empty())
.collect();
+ let mut sorted_workspaces: Vec<&Workspace> = workspaces.iter().collect();
+ sorted_workspaces.sort_by_key(|w| w.idx);
+
let mut before_focused: Vec<String> = Vec::new();
let mut after_focused: Vec<String> = Vec::new();
let mut focused_output = String::new();
@@ -68,10 +87,10 @@ fn main() {
let mut chrome_idx = 0;
let mut tooltip = String::new();
- for ws in &workspaces {
+ for ws in &sorted_workspaces {
let mut ws_windows: Vec<&Window> = windows
.iter()
- .filter(|w| w.workspace_id == ws.id)
+ .filter(|w| w.workspace_id == Some(ws.id))
.collect();
ws_windows.sort_by_key(|w| w.id);
@@ -88,7 +107,9 @@ fn main() {
} else {
let mut output = String::new();
for win in &ws_windows {
- let mut color = get_color(&win.app_id, &win.title, win.pid, &terminal_apps);
+ let app_id = win.app_id.as_deref().unwrap_or("");
+ let title = win.title.as_deref().unwrap_or("");
+ let mut color = get_color(app_id, title, win.pid, &terminal_apps);
if color == "chrome" {
const CHROME_COLORS: [&str; 4] = ["#ea4335", "#fbbc05", "#34a853", "#4285f4"];
@@ -102,7 +123,7 @@ fn main() {
let bar = if win.is_focused {
"█"
- } else if !ws.is_focused && Some(win.id) == ws.active_window_id {
+ } else if !ws.is_focused && ws.active_window_id == Some(win.id) {
"▌"
} else {
"|"
@@ -182,7 +203,7 @@ fn get_all_descendants(pid: u32) -> std::io::Result<Vec<u32>> {
Ok(descendants)
}
-fn get_color(app_id: &str, title: &str, pid: Option<u32>, terminal_apps: &HashMap<u32, &str>) -> String {
+fn get_color(app_id: &str, title: &str, pid: Option<i32>, terminal_apps: &HashMap<i32, &str>) -> String {
if is_terminal(app_id) {
let title_lower = title.to_lowercase();
@@ -201,7 +222,8 @@ fn get_color(app_id: &str, title: &str, pid: Option<u32>, terminal_apps: &HashMa
"codex" => "#56b6c2",
"nvim" => "#98c379",
_ => "#888888",
- }.to_string();
+ }
+ .to_string();
}
}