- Python 84.7%
- QML 15.3%
Add a shared window-focus core (window_focus.py) that maps a Claude Code session pid to its terminal window by walking the /proc parent chain and matching the nearest ancestor against niri's window list, then focusing it via `niri msg action focus-window`. Degrades to a silent no-op off niri or under tmux/SSH. - popout card: TapHandler/HoverHandler -> execDetached(focus-session.py), then dismiss the popout (handlers keep the DankFlickable scrollable) - notification: notify-send -A open=Open (non-blocking Popen + reader thread); --print-id still drives auto-close. on_open is injected for testability - focus-session.py launcher installed alongside run-monitor.py - tests: window_focus, focus-session, extended notifier/monitor/install (108 passing, 87% coverage); qmllint clean - docs: READMEs + design spec note the niri requirement and tmux/SSH limit |
||
|---|---|---|
| docs/superpowers/specs | ||
| plugin | ||
| scripts | ||
| tests | ||
| .gitignore | ||
| pyproject.toml | ||
| README.md | ||
| README_zh.md | ||
Claude Code Monitor — DankMaterialShell plugin
A DankMaterialShell (Quickshell/QML) bar plugin that monitors your running Claude Code sessions and notifies you when one needs your input.
English | 简体中文
Features
- DankBar pill with the Claude Code logo + a live count (
running/total, orN!when a session is waiting), color-coded by aggregate state. - Left-click popout listing one card per session. The popout height auto-adjusts to the number of sessions and scrolls once it would get too tall.
- Each card shows, with the Claude Code logo on the left: folder name · full path · status · PID · context usage % · token usage.
- Click a card to jump to its window — the terminal hosting that Claude Code session is focused (and its workspace activated) via the compositor.
- Desktop notifications when a session enters waiting (needs input/selection), with per-session cooldown/dedupe, auto-close when the session resumes, a sound, and the CC icon. Each waiting notification carries an Open action button that focuses that session's window.
How it works
Claude Code ──hooks──▶ $XDG_RUNTIME_DIR/claude-code-monitor/session-<id>.json (state, cwd, pid)
──statusLine▶ $XDG_RUNTIME_DIR/claude-code-monitor/context-<id>.json (ctx %, tokens)
│
run-monitor.py (daemon) ── polls + prunes dead PIDs
│ ── fires notifications (notifier)
▼
one summary JSON line per change on stdout
│
QML widget (Quickshell.Io.Process) ── parses ──▶ pills + session-card popout
The QML widget launches the daemon as a long-lived Process (the cavaVisualizer pattern), so
the daemon's lifetime is tied to the plugin — no PID files, no orphans. The daemon owns data +
notifications (pure tested Python); QML is a thin reactive view.
Waiting detection comes only from Claude Code's Notification hook (permission_prompt /
idle_prompt / elicitation_dialog) — never from scanning /proc — so tool execution is never
mistaken for "waiting". A /proc scan is a fallback used only when no hook files exist yet.
This package is self-contained and uses its own claude-code-monitor namespace, so it coexists
with the upstream claude-monitor tray app if you also have it installed.
Jumping to a session's window
Both the card click and the notification Open button resolve a session to its window the
same way: the claude process runs as a descendant of its terminal emulator
(claude → shell → terminal), so the monitor walks the /proc parent chain up from the session
PID and matches the nearest ancestor against the compositor's window list, then focuses it.
- Compositor support: niri (
niri msg --json windows+niri msg action focus-window). On other compositors this degrades to a silent no-op; the seam inwindow_focus.pymakes adding another backend (e.g. Hyprland) straightforward. - Limitation: if the session runs under tmux / screen / SSH,
claudeis not a descendant of a local window, so no window is matched and the focus is a no-op. Everything else still works.
Requirements
- DankMaterialShell (Quickshell)
- niri — for click-to-focus / notification Open (other compositors: features no-op gracefully)
- Python ≥ 3.10 (standard library only — no
pip installneeded) notify-send(libnotify) for notifications; optionalgdbus(auto-close) andcanberra-gtk-play/paplay/pw-play(sound)
Install
# 1. Register hooks + statusLine wrapper, copy the daemon to ~/.local/share/claude-code-monitor/
python3 scripts/install.py
# 2. Make the plugin visible to DankMaterialShell
ln -sfn "$PWD/plugin" ~/.config/DankMaterialShell/plugins/claude-code-monitor
# 3. Enable it + add it to the bar
# - plugin_settings.json: "claude-code-monitor": { "enabled": true }
# - settings.json *Widgets array: add { "id": "claude-code-monitor", "enabled": true }
# (or use the DankMaterialShell settings GUI: Plugins → enable, then add the widget to the bar)
# 4. Reload
dms restart
Hooks load at Claude Code session start — restart any running Claude Code sessions after installing so they begin reporting. New sessions are picked up automatically (a
SessionStarthook is registered).
install.py backs up ~/.claude/settings.json to settings.json.bak, writes it atomically,
merges its entries non-destructively (it never removes other tools' hooks or your env/
statusLine), wraps any existing statusLine (saved verbatim and delegated to), and removes the
legacy dankbar prototype hooks if present.
Configuration
~/.config/claude-code-monitor/config.json (the daemon reloads it each poll, so changes apply
within ~1s; the plugin's Settings panel writes this file for you):
| Key | Default | Description |
|---|---|---|
scan_interval_sec |
1 | Poll interval (seconds) |
notify_cooldown_sec |
5 | Min seconds between notifications per session |
enable_notifications |
true | Send desktop notifications |
notify_on_error |
true | Notify on session errors |
notify_sound |
"message" | freedesktop sound event, a file path, or "" to disable |
Project layout
plugin/ # the DMS plugin (symlinked into ~/.config/DankMaterialShell/plugins/)
plugin.json
ClaudeMonitorWidget.qml # pills + launches the daemon Process + popout wiring
ClaudeMonitorPopout.qml # PopoutComponent: header + auto-height scrollable card list
SessionCard.qml # one session card (all fields + CC logo + state dot)
ClaudeMonitorSettings.qml # toggles -> config.json
assets/claudecode-color.svg
scripts/
claude_code_monitor/ # pure-stdlib package
models.py state.py hook_watcher.py scanner.py notifier.py
window_focus.py # pid -> terminal window resolution + focus (niri)
monitor.py # the daemon (poll loop + summary emitter)
hook_script.py # Claude Code hook (writes session-<id>.json)
statusline_script.py # statusLine wrapper (writes context-<id>.json)
paths.py
run-monitor.py # daemon launcher (fixes sys.path)
focus-session.py # window-focus launcher (called by the popout card click)
install.py # settings.json merge + runtime install + legacy cleanup
tests/ # pytest suite (87% coverage)
Development
python3 -m pytest --cov=claude_code_monitor # from the repo root (pythonpath=scripts)
# QML can be syntax-checked offline:
qmllint -I /usr/share/quickshell/dms plugin/*.qml
Uninstall
python3 scripts/install.py --uninstall # removes our hooks, restores original statusLine
rm ~/.config/DankMaterialShell/plugins/claude-code-monitor
# then remove the widget from the bar in settings.json and disable it in plugin_settings.json
License
MIT