summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--README.md24
-rw-r--r--src/main.rs41
2 files changed, 43 insertions, 22 deletions
diff --git a/README.md b/README.md
index 41124d9..77b2afb 100644
--- a/README.md
+++ b/README.md
@@ -2,14 +2,20 @@
A fast, event-driven workspace indicator for [Waybar](https://github.com/Alexays/Waybar) and [niri](https://github.com/YaLTeR/niri).
+<img width="2880" height="1800" alt="image" src="https://github.com/user-attachments/assets/45c689b4-2c0d-4faf-8033-e34341cad8c2" />
+
## Features
-- **Visual bars** instead of numbers - each window is a colored bar
-- **Color-coded by app** - Chrome (Google colors), Discord (purple), Firefox (orange), nvim (green), Claude (orange), Codex (cyan)
-- **Terminal app detection** - detects Claude/Codex/nvim running inside terminals via /proc
-- **Focused window indicator** - █ for focused, ▌ for would-be-focused, | for others
-- **Dimmed unfocused workspaces** - colors are dimmed when workspace isn't focused
-- **Daemon mode** - persistent socket connection, no process spawning
+- **Visual bars instead of numbers** — each window is rendered as a colored bar.
+- **Event-driven daemon mode** — keeps a persistent niri socket connection and emits JSON updates only when workspace/window state changes.
+- **App-aware coloring** — built-in colors for Chrome, Firefox, Discord/Vesktop, Spotify, Todoist, Gmail, and terminals.
+- **Terminal app detection via `/proc`** — detects `jcode`, Claude, Codex, and Neovim/Vim running inside terminal windows.
+- **Terminal support** — works with Alacritty, Kitty, Ghostty, Foot, and Footclient.
+- **Focus semantics** — uses `█` for the focused window, `▌` for the active window on an unfocused workspace, and `|` for other windows.
+- **tmux hinting** — uses `¦` for non-focused tmux windows.
+- **Cleaner empty workspace handling** — unfocused empty workspaces are hidden; a focused empty workspace is shown as a dim `·`.
+- **Named workspace support** — shows the custom workspace name when that workspace is focused; tooltips include only non-empty workspaces.
+- **Safer terminal PID detection** — skips `/proc` descendant inference for shared terminal PIDs in cases that can otherwise cause false positives.
## Benchmarks
@@ -36,17 +42,19 @@ cargo build --release
cp target/release/niri-workspaces ~/.config/waybar/
```
+If Waybar is already running, restart Waybar or reload the module so the new binary is picked up.
+
## Waybar Configuration
```json
"custom/workspaces": {
- "exec": "/home/user/.config/waybar/niri-workspaces",
+ "exec": "~/.config/waybar/niri-workspaces",
"return-type": "json",
"format": "{}"
}
```
-No `interval` or `signal` needed - the daemon outputs JSON whenever workspace/window events occur.
+No `interval` or `signal` is needed — the daemon outputs JSON whenever relevant niri events occur.
## License
diff --git a/src/main.rs b/src/main.rs
index dedc0e2..e76831b 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -85,7 +85,7 @@ fn output_status(workspaces: &[Workspace], windows: &[Window]) {
.filter_map(|w| {
let pid = w.pid?;
let app_id = w.app_id.as_deref().unwrap_or("");
- if (app_id == "foot" || app_id == "footclient")
+ if (app_id == "foot" || app_id == "footclient" || app_id == "kitty")
&& pid_counts.get(&pid).copied().unwrap_or(0) > 1
{
return None;
@@ -119,10 +119,13 @@ fn output_status(workspaces: &[Workspace], windows: &[Window]) {
let ws_name = ws.name.as_deref().unwrap_or("");
let has_custom_name = !ws_name.is_empty();
- if has_custom_name {
- tooltip.push_str(&format!("{}: {} windows\\n", ws_name, win_count));
- } else {
- tooltip.push_str(&format!("ws{}: {} windows\\n", ws.idx, win_count));
+ // Only add to tooltip if workspace has windows
+ if win_count > 0 {
+ if has_custom_name {
+ tooltip.push_str(&format!("{}: {} windows\\n", ws_name, win_count));
+ } else {
+ tooltip.push_str(&format!("ws{}: {} windows\\n", ws.idx, win_count));
+ }
}
// Skip empty unfocused workspaces entirely
@@ -131,12 +134,8 @@ fn output_status(workspaces: &[Workspace], windows: &[Window]) {
}
let ws_output = if win_count == 0 {
- // Empty but focused
- if has_custom_name {
- format!("<span color='#888888'>{}</span>", ws_name)
- } else {
- "<span color='#888888'>·</span>".to_string()
- }
+ // Empty but focused - just show a dot, no name
+ "<span color='#888888'>·</span>".to_string()
} else {
let mut output = String::new();
@@ -227,12 +226,16 @@ fn is_claude_title(title: &str) -> bool {
fn detect_terminal_app(pid: u32) -> &'static str {
let mut found_claude = false;
let mut found_codex = false;
+ let mut found_jcode = false;
let mut found_nvim = false;
if let Ok(children) = get_all_descendants(pid) {
for child_pid in children {
if let Ok(cmdline) = fs::read_to_string(format!("/proc/{}/cmdline", child_pid)) {
let cmdline = cmdline.to_lowercase();
+ if cmdline.contains("jcode") {
+ found_jcode = true;
+ }
if cmdline.contains("claude") {
found_claude = true;
}
@@ -246,7 +249,9 @@ fn detect_terminal_app(pid: u32) -> &'static str {
}
}
- if found_claude {
+ if found_jcode {
+ "jcode"
+ } else if found_claude {
"claude"
} else if found_codex {
"codex"
@@ -276,11 +281,18 @@ fn get_all_descendants(pid: u32) -> std::io::Result<Vec<u32>> {
Ok(descendants)
}
-fn get_color(app_id: &str, title: &str, pid: Option<i32>, terminal_apps: &HashMap<i32, &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();
- if is_claude_title(title) {
+ if title_lower.contains("jcode") {
+ return "#999999".to_string();
+ } else if is_claude_title(title) {
return "#f5a623".to_string();
} else if title_lower.contains("codex") {
return "#56b6c2".to_string();
@@ -291,6 +303,7 @@ fn get_color(app_id: &str, title: &str, pid: Option<i32>, terminal_apps: &HashMa
if let Some(pid) = pid {
if let Some(&app) = terminal_apps.get(&pid) {
return match app {
+ "jcode" => "#999999",
"claude" => "#f5a623",
"codex" => "#56b6c2",
"nvim" => "#98c379",