# Hyprland HDMI Revive Recover a black HDMI display after switching a shared physical monitor away from an Arch Linux / Hyprland workstation and back. A DRM hotplug event triggers a short switch away from the active graphical VT and back. There is no periodic DPMS toggle or polling timer. **Version 0.2.0.** The initial implementation and the review fixes are separate commits. See [the code review](docs/code-review.md) and [changelog](CHANGELOG.md). ## Requirements A systemd-based Linux desktop with Hyprland, a local `seat0` Wayland session, Bash, coreutils (`timeout`, `stdbuf`, `stat`), `jq`, `flock`, `sudo` and `/usr/bin/chvt`. On Arch Linux: ```bash sudo pacman -S --needed hyprland systemd bash coreutils jq util-linux kbd sudo ``` The intended setup is one local Hyprland session per user. The monitor must emit DRM `HOTPLUG=1` events when its physical input changes. No event means no automatic recovery. Hyprland must still expose the configured monitor over its IPC socket. ## Install from the package Download `hyprland-hdmi-revive-0.2.0.tar.gz` and `SHA256SUMS` from the [Gitea release](https://gitea.jr-it-services.de/johannes/hyprland-hdmi-revive/releases). Run as your desktop user, **without sudo**: ```bash sha256sum --ignore-missing -c SHA256SUMS tar -xzf hyprland-hdmi-revive-0.2.0.tar.gz cd hyprland-hdmi-revive-0.2.0 ./scripts/install.sh ``` This is a portable user installer bundle, not a pacman system package. It installs: - executables under `~/.local/bin/`; - shared code under `~/.local/lib/hyprland-hdmi-revive/`; - the unit under `${XDG_CONFIG_HOME:-$HOME/.config}/systemd/user/`; - a new config under `${XDG_CONFIG_HOME:-$HOME/.config}/revive-monitor/config.env`. Existing configuration is preserved. A running instance is stopped before its files are replaced. Installation alone does not enable or start the service. Alternatively, clone the source and use the same installer: ```bash git clone https://gitea.jr-it-services.de/johannes/hyprland-hdmi-revive.git cd hyprland-hdmi-revive ./scripts/install.sh ``` ## Configure Find the monitor name from a terminal inside Hyprland: ```bash hyprctl monitors nano "${XDG_CONFIG_HOME:-$HOME/.config}/revive-monitor/config.env" ``` Example: ```bash MONITOR=HDMI-A-2 DEBOUNCE_SECONDS=30 HDMI_SETTLE_SECONDS=5 VT_SWITCH_DELAY_SECONDS=1 VT_RETURN_SETTLE_SECONDS=2 ``` Timing values are whole seconds: 0–300 for waits and 0–3600 for debounce. Config is trusted Bash sourced as your user; do not copy untrusted shell code into it. ### Allow the VT switch Edit with `sudo visudo -f /etc/sudoers.d/revive-monitor-chvt`. Prefer explicit VT arguments when the machine always uses VT1 and VT2: ```sudoers YOUR_USER ALL=(root) NOPASSWD: /usr/bin/chvt 1, /usr/bin/chvt 2 ``` Replace `YOUR_USER`. For changing graphical VTs, a broader chvt-only rule is in `sudoers/revive-monitor-chvt.example`. Never grant sudo access to these scripts. The helper checks permission for both directions before leaving the desktop and uses `sudo -n` with command timeouts. A return failure gets one emergency retry. ### Connect the Hyprland session The user service manager does not automatically inherit a terminal's Hyprland environment. Add this line to your Hyprland configuration (commonly `~/.config/hypr/hyprland.conf`): ```ini exec-once = sh -c 'export XDG_CONFIG_HOME="${XDG_CONFIG_HOME:-$HOME/.config}"; systemctl --user import-environment HYPRLAND_INSTANCE_SIGNATURE XDG_CONFIG_HOME && systemctl --user restart revive-monitor-drm-watch.service' ``` It imports the current IPC instance at each Hyprland login, including after logging out and back in. A custom `XDG_CONFIG_HOME` must also be consistent with the user manager's unit search path. See the official [hyprctl documentation](https://wiki.hypr.land/configuring/core/advanced-configuration/using-hyprctl/) and [systemd environment documentation](https://www.freedesktop.org/software/systemd/man/latest/environment.d.html). For the current login, after configuring the output and sudoers: ```bash ./scripts/install.sh --start systemctl --user status revive-monitor-drm-watch.service journalctl --user -u revive-monitor-drm-watch.service -b ``` `--start` imports the current instance, reloads systemd, enables and restarts the unit. Use it for updates as well. The service remains attached to `default.target` because not every Hyprland setup activates `graphical-session.target`; session and IPC guards prevent pre-login VT switching. ## Behavior and limits - Events are accepted only for this UID's active local Wayland session on seat0. A reported non-Hyprland desktop is excluded. - The helper independently verifies the requested VT and rechecks the session after settling and permission checks. - A private `XDG_RUNTIME_DIR` is required. Locks prevent duplicate watchers and overlapping recoveries; there is no shared `/tmp` fallback. - Missing IPC, invalid monitor names, command failures and lost event streams produce error logs. A terminated event stream causes systemd to restart the watcher. - Signals trigger a best-effort return to the graphical VT. SIGKILL, power loss and persistent sudo/kernel failures cannot be recovered by a shell trap. - DRM events are not reliably attributed to a particular monitor. Another output's hotplug can trigger this workaround too. - Debounce is a cooldown measured after a recovery attempt. It suppresses queued events, including events caused by the VT switch. It can also suppress a real rapid input return; if that happens, try a shorter `DEBOUNCE_SECONDS` or run the helper manually with the correct graphical VT. ## Uninstall From the unpacked bundle or checkout: ```bash ./scripts/uninstall.sh ``` The user manager must be reachable. Config and sudoers remain intact. Remove the Hyprland startup line and, if no longer needed, the dedicated sudoers entry with `visudo`. Failure state for unrelated services is preserved. ## Build and validate Python 3 is needed only for tests; ShellCheck is a development dependency. ```bash for script in scripts/*.sh lib/*.sh; do bash -n "$script" || exit; done shellcheck -x scripts/*.sh lib/*.sh python3 -B -m unittest discover -s tests -v ./scripts/build-release.sh ``` The release builder requires clean tracked files and packages **committed HEAD**. It creates a `.tar.gz` installer bundle, source ZIP, Base64 ZIP and `SHA256SUMS` in `dist/`. Untracked files and local config are excluded. The Base64 ZIP can also be decoded on Windows with `certutil -decode `. Tests mock loginctl, Hyprland IPC, udev, sudo and systemd. They never switch the real display. A hardware input-switch test after installation remains necessary. CI runs the shell checks and behavioral tests on a configured Gitea Actions runner. See [architecture](docs/architecture.md), [troubleshooting](docs/troubleshooting.md) and [security notes](SECURITY.md). MIT license: [LICENSE](LICENSE).