kbd-disp-switch - kbd-disp-switch - shared-monitor HDMI input switching (udev + ddcutil)

About | Log | Files | Refs | License

kbd-disp-switch

Two PCs share one monitor and one USB keyboard/mouse dongle. Plug the dongle into a PC and the monitor automatically switches to that PC's HDMI input. Unplug it and the display stays put ("display follows arrivals only").

Built around udev + ddcutil, with a small ssh side-channel between the two hosts so that unplug/replug on the same host never causes a pointless monitor re-handshake (a 2–3 s black flicker on many monitors).

How it works

        +------------------+          +------------------+
        |  host A          |          |  host B          |
        |  HDMI-1          |          |  HDMI-2          |
        |                  |          |                  |
        |  udev rule       |          |  udev rule       |
        |  kbd_disp_switch |          |  kbd_disp_switch |
        |  (active)        |          |  (passive)       |
        +--------+---------+          +--------+---------+
                 |                              |
                 |  ssh (query + push)          |  reads pushed state
                 +------------------------------+

Why the ssh side-channel?

"Unplug → replug" is locally identical for (a) a round trip on the same host (no-op needed) and (b) a round trip through the peer (switch needed — the peer's attach already moved the display). Only the peer knows which happened, so the two hosts exchange departure timestamps. The hosts are asymmetric: one is active (queries the peer over ssh), the other passive (reads a state file the active host pushes). This avoids needing sshd on both hosts.

Why systemd-run?

udev kills leftover processes of RUN programs (cgroup kill — even setsid doesn't help), so a backgrounded decision job simply dies. A transient systemd-run unit survives and is cancellable via systemctl stop. The decision unit sleeps SETTLE internally because systemd-run --on-active timers proved unreliable (fired 6–60 s late in practice).

Why a custom DDC command?

Standard input switching (ddcutil setvcp 60 ...) is broken on recent LG monitors (returns "Invalid value"). The working method is the manufacturer side-channel: ddcutil setvcp xF4 x0090 --i2c-source-addr=x50 --noverify (HDMI 1) / x0091 (HDMI 2). See INSTALL.md for how to find the right command for your monitor.

Requirements

Install

See INSTALL.md — step by step, including finding your device IDs, your monitor's DDC bus, and the right input-switching command.

Files

File Purpose
kbd_disp_switch main script (install to /usr/local/sbin/)
kbd_disp_peer whitelisted ssh wrapper for the passive host
kbd_disp.conf.example per-host config template (/etc/kbd_disp.conf)
99-kbd-disp.rules udev rule (replace the USB IDs)

Configuration reference

Variable Meaning
MY_INPUT 1 → monitor HDMI 1, 2 → HDMI 2
SETTLE debounce seconds before the decision (default 2)
PEER_QUERY active host: user@peer for querying the peer's departure
PEER_PUSH active host: user@peer for pushing our departure
SSH_KEY automation key path (default /root/.ssh/kbd_disp)
PEER_STATE_FILE passive host: local file the active host's pushes land in
DDC_CMD DDC write command; %s = input value (default: LG side-channel)

Troubleshooting

License

MIT — see LICENSE.