- Rust 83.6%
- QML 13.2%
- JavaScript 2.5%
- Makefile 0.7%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
| banvik-monitor | ||
| banvik-omarchy-monitor-info | ||
| monitor-analyzer | ||
| .gitignore | ||
| Cargo.lock | ||
| Cargo.toml | ||
| CLAUDE.md | ||
| Makefile | ||
| README.md | ||
Monitor Analyzer
A Linux Ratatui application listing connected DRM display connectors and their supported resolutions and refresh rates. Targets Linux 7; uses standard DRM sysfs and udev APIs. Works independently of X11 or Wayland, including from a virtual console.
Run
Install Rust, pkg-config, and libudev development files (Arch: pkgconf systemd;
Debian/Ubuntu: pkg-config libudev-dev), then:
cargo run --release
Run inside an interactive terminal. Root is normally unnecessary.
One optional step: the v key forces a display re-detect by bouncing the
virtual terminal, and the VT ioctls need a capability on the binary:
sudo setcap cap_sys_tty_config+ep target/release/monitor-analyzer
Grant it once, after each cargo build --release — a fresh binary does not
inherit it. Everything else works without it, and v logs the reason and
changes nothing when it is missing. What it does is described under the key
list below.
Panels take focus one at a time; the focused one has a thick border and is what the arrow keys act on.
- Tab or Right: next panel — monitors, resolutions, refresh rates, event log, then back to monitors. Shift-Tab or Left goes the other way.
- Up/Down or j/k: move within the focused panel.
- PageUp/PageDown: jump a screenful within the focused panel.
- l: focus the event log directly.
- r: rescan now.
- v: force a re-detect (see below).
- q, Escape, or Ctrl-C: quit.
The application reads /sys/class/drm/card*-*/status and modes across all
GPUs. Only connected connectors are shown, including built-in displays.
Duplicate resolution names are collapsed, preserving the driver's order.
The third column shows supported refresh rates for the selected resolution.
Rates come from read-only DRM connector queries on /dev/dri/card*, using
connector_id to match the sysfs connector. Fractional rates are calculated
from mode timings and displayed to two decimals; interlaced modes are labeled.
Bold green marks enabled monitors and their current resolution and refresh rate.
The shaded background and arrow show keyboard selection independently.
Current modes are read through the connector encoder and DRM CRTC; enabled
state also uses the sysfs enabled attribute. If the driver does not expose
the current mode, resolutions and rates remain unmarked. The refresh rate is
the configured mode timing, not an instantaneous VRR measurement.
If DRM device access is unavailable, resolutions remain visible and a warning
explains why refresh rates could not be read. Run as your normal desktop user
with access to the DRM card devices. The app does not acquire DRM master.
The modes file does not reliably identify the current or preferred mode. Virtual displays without DRM connectors are not shown.
The bottom half of the view is an event log: monitors connecting and disconnecting, being enabled and disabled, changing resolution or refresh rate, and failures reading a display. It is a diff of consecutive scans, so it stays silent while nothing moves, and a repeated failure is logged once rather than every three seconds.
A mode switch usually moves resolution and refresh rate together, and those arrive as one line rather than two halves of the same change:
09:51:50 [INFO] card1-DP-2 enabled
09:51:50 [INFO] card1-DP-2 mode at 3440x1440 59.97 Hz
09:52:04 [INFO] card1-DP-2 mode 3440x1440 59.97 Hz → 2560x1080 60.00 Hz
09:52:31 [INFO] card1-DP-2 refresh 60.00 Hz → 50.00 Hz
09:53:02 [INFO] card1-DP-2 disabled
When only one of the two moves it keeps its own line, so the entry never implies more changed than did. A display can be connected without being enabled — plugged in but not driven — which is why enablement is its own event rather than something inferred from the mode.
Each entry is tagged [INFO], [WARN], or [ERROR]: a display that cannot
be read fully is a warning, while a failed scan or an unusable log file is an
error. The tags are one width, so the text stays in a column. The newest entry
is at the bottom and stays selected until you scroll back; returning to the
newest entry resumes following it. The log takes half the view where the
terminal allows, and yields its rows entirely when there are too few to draw
it, so the 70 x 13 minimum still holds.
Timestamps are local. The pane shows the wall clock; the file records the
offset too (2026-09-18T09:48:14+02:00), so a line stays unambiguous across a
daylight-saving change or when read on another machine. The zone comes from
jiff, which reads the system database, so TZ is honoured and entries either
side of a DST switch keep the clock they were written at.
Entries are appended to $XDG_CACHE_HOME/monitor-analyzer/events.log
(~/.cache/monitor-analyzer/events.log by default), one line per event. The
file is truncated at startup once it passes 1 MiB. If it cannot be written, the reason
appears as the first log entry and the application continues with the on-screen
log only.
v forces a re-detect for a display the driver has stopped reporting
correctly: it switches the seat to an unused virtual terminal and straight
back, which makes the kernel and the compositor run a full modeset and re-read
every connector. The screen goes to a text console for as long as that takes.
Both legs block until the switch has landed, so the rescan afterwards sees the
result. This is the only thing the program does that changes system state.
The setcap cap_sys_tty_config+ep step under Run is what makes it
work: the VT ioctls are permitted only for a process that owns the VT, which a
program on a pty never does. Without the capability, v logs the reason and
changes nothing.
The session's own VT device is what gets opened, which logind hands to the
session user; /dev/tty0 is root-only and would additionally need
cap_dac_override, a much wider grant.
The VT to return to is read from /sys/class/tty/tty0/active rather than
assumed to be 1, since under uwsm and a greeter it may be anything.
DRM udev events trigger a rescan within approximately 150 ms. A three-second rescan also handles delayed driver updates or missed events. If udev setup fails, the application displays the error and continues with polling. Scan failures retain the previous results with a visible warning; unreadable modes do not hide an otherwise connected monitor. No display configuration is changed.
Omarchy bar plugin
banvik-omarchy-monitor-info puts the same data in the Omarchy top bar: a
monitor icon with a bug perched on its corner — themed, like every other bar
icon — which opens a popup
listing every connected monitor with its EDID identity, its current mode, and
the supported resolutions with their refresh rates. The popup is a Quickshell
bar widget hosted by the already-running omarchy-shell, not a second
Quickshell process. The binary is the widget's data source and its
command-line control.
cargo build --release
install -Dm755 target/release/banvik-omarchy-monitor-info ~/.local/bin/
ln -sfn "$PWD/banvik-omarchy-monitor-info/omarchy-plugin" ~/.config/omarchy/plugins/banvik.monitor-info
omarchy-shell shell rescanPlugins
omarchy plugin enable banvik.monitor-info right
Move it with omarchy bar move banvik.monitor-info --section center, remove it
with omarchy plugin disable banvik.monitor-info. The widget takes one
setting, command, when the binary is not on the shell's PATH or lives under
another name:
{ "id": "banvik.monitor-info", "command": "/home/you/.local/bin/banvik-omarchy-monitor-info" }
Left-click the glyph to open the popup, j/k or the mouse to select a monitor,
Escape to close. The current resolution and the current refresh rate are
highlighted independently: the selected monitor's active resolution is bold in
the mode list, and within that row only the rate actually in use is bold — the
other rates it supports stay dim. --show, --toggle, and --hide drive the
same popup over omarchy-shell IPC, so a Hyprland keybinding gets identical
behavior.
--json prints one snapshot and exits; --watch prints newline-delimited
snapshots and re-emits on DRM hotplug. The widget runs --watch only while its
popup is open — one udev subscription per bar surface would otherwise idle on
every monitor. Both are plain stdout, usable without Omarchy.
Development
cargo test --workspace
cargo clippy --workspace --all-targets -- -D warnings
cargo fmt --check
The workspace runs monitor-analyzer by default, so --workspace is what
covers the library and the Omarchy binary too.
The plugin is installed as a symlink into the repo, so editing its QML edits
the installed plugin — but the shell's file watcher does not follow the
symlink, and omarchy-shell shell rescanPlugins re-reads manifests without
recompiling changed QML. Apply plugin edits with:
omarchy restart shell
omarchy plugin validate banvik-omarchy-monitor-info/omarchy-plugin checks the
manifest, and journalctl --user -f | grep -i qml shows QML errors.
Tests use synthetic sysfs fixtures for connection changes and missing files, plus Ratatui's test backend for rendering. Actual physical hotplug requires hardware testing.
References: Ratatui, udev monitor.
Monitor identification is read from each connector's EDID 1.x base block.
The list shows the model name, manufacturer, and text serial number, falling
back to the numeric serial when needed. Manufacturer codes are resolved through
/usr/share/hwdata/pnp.ids or /usr/share/misc/pnp.ids, loaded once at startup
on first use. If the database or entry is missing, the three-letter code remains.
Missing, unreadable, or invalid EDID fields are omitted; connector names remain
visible. Base-block headers and checksums are validated. DisplayID
extension identification is not currently decoded.
Identification fields are limited to 64 characters including a trailing …
when truncated, and shortened further with an ellipsis to fit the monitor
column. The minimum terminal size is 70 × 13.