No description
  • Rust 83.6%
  • QML 13.2%
  • JavaScript 2.5%
  • Makefile 0.7%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
2026-09-22 09:22:14 +02:00
banvik-monitor Initial 2026-09-18 09:21:17 +02:00
banvik-omarchy-monitor-info Bug in the icon 2026-09-18 09:31:07 +02:00
monitor-analyzer More monitor information 2026-09-22 09:19:07 +02:00
.gitignore Initial 2026-09-18 09:21:17 +02:00
Cargo.lock More monitor information 2026-09-22 09:19:07 +02:00
Cargo.toml More monitor information 2026-09-22 09:19:07 +02:00
CLAUDE.md More monitor information 2026-09-22 09:19:07 +02:00
Makefile Added Makefile for buils with extras 2026-09-22 09:22:14 +02:00
README.md More monitor information 2026-09-22 09:19:07 +02:00

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.