342 lines
8.4 KiB
Markdown
342 lines
8.4 KiB
Markdown
# Hyprland HDMI Revive
|
|
|
|
Automatically recover a Wayland/Hyprland desktop after switching a physical monitor input away from an Arch Linux workstation and back again.
|
|
|
|
This project was built for a setup where one external HDMI monitor is shared between:
|
|
|
|
- an Arch Linux workstation running Hyprland/Wayland
|
|
- a Windows laptop connected to another HDMI input on the same monitor
|
|
|
|
On some monitors/GPU combinations, switching back to the Arch input leaves the display black or showing `No source`, even though Hyprland still reports the monitor as enabled, focused, and DPMS-on.
|
|
|
|
The workaround that reliably restores the image is a short VT switch away from the active Hyprland VT and back again. This repository automates that workaround only when the kernel reports a DRM hotplug event and only while an active Wayland session exists.
|
|
|
|
## How it works
|
|
|
|
```text
|
|
Monitor input switch
|
|
|
|
|
v
|
|
Kernel DRM HOTPLUG=1 event
|
|
|
|
|
v
|
|
User service receives the event
|
|
|
|
|
v
|
|
Is there an active Wayland session for this user?
|
|
| |
|
|
no yes
|
|
| |
|
|
ignore detect active VT
|
|
|
|
|
v
|
|
wait for HDMI sync
|
|
|
|
|
v
|
|
Hyprland DPMS/focus refresh
|
|
|
|
|
v
|
|
temporary VT switch
|
|
|
|
|
v
|
|
switch back to Hyprland
|
|
```
|
|
|
|
The important detail is that the recovery is **event-driven**. There is no polling timer and no periodic DPMS off/on cycle, so the screen does not blink during normal desktop use.
|
|
|
|
## Repository layout
|
|
|
|
```text
|
|
.
|
|
├── config/
|
|
│ └── revive-monitor.env.example
|
|
├── docs/
|
|
│ ├── architecture.md
|
|
│ └── troubleshooting.md
|
|
├── scripts/
|
|
│ ├── install.sh
|
|
│ ├── revive-monitor-drm-watch.sh
|
|
│ ├── revive-monitor-fix.sh
|
|
│ └── uninstall.sh
|
|
├── sudoers/
|
|
│ ├── revive-monitor-chvt.example
|
|
│ └── revive-monitor-chvt.strict.example
|
|
├── systemd/
|
|
│ └── user/
|
|
│ └── revive-monitor-drm-watch.service
|
|
├── .gitea/
|
|
│ ├── issue_template/
|
|
│ └── pull_request_template.md
|
|
├── .editorconfig
|
|
├── .gitignore
|
|
├── CHANGELOG.md
|
|
├── LICENSE
|
|
├── SECURITY.md
|
|
└── README.md
|
|
```
|
|
|
|
## Requirements
|
|
|
|
- Arch Linux or another systemd-based Linux distribution
|
|
- Hyprland
|
|
- `systemd` / `systemd-logind`
|
|
- `udevadm`
|
|
- `hyprctl`
|
|
- `flock` from `util-linux`
|
|
- `chvt` from `kbd`
|
|
- `sudo`
|
|
|
|
On Arch Linux, the relevant packages are typically already present. If needed:
|
|
|
|
```bash
|
|
sudo pacman -S --needed hyprland systemd util-linux kbd sudo
|
|
```
|
|
|
|
## 1. Clone the repository
|
|
|
|
Example for Gitea:
|
|
|
|
```bash
|
|
git clone ssh://git@gitea.example.com/your-user/hyprland-hdmi-revive.git
|
|
cd hyprland-hdmi-revive
|
|
```
|
|
|
|
## 2. Configure the monitor
|
|
|
|
Copy the example configuration:
|
|
|
|
```bash
|
|
mkdir -p ~/.config/revive-monitor
|
|
cp config/revive-monitor.env.example ~/.config/revive-monitor/config.env
|
|
```
|
|
|
|
Determine the Hyprland output name:
|
|
|
|
```bash
|
|
hyprctl monitors
|
|
```
|
|
|
|
Example:
|
|
|
|
```text
|
|
Monitor HDMI-A-2 (ID 0):
|
|
```
|
|
|
|
Then edit:
|
|
|
|
```bash
|
|
nano ~/.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
|
|
```
|
|
|
|
## 3. Install the scripts and systemd user service
|
|
|
|
Run:
|
|
|
|
```bash
|
|
./scripts/install.sh
|
|
```
|
|
|
|
The installer copies the scripts to:
|
|
|
|
```text
|
|
~/.local/bin/revive-monitor-fix
|
|
~/.local/bin/revive-monitor-drm-watch
|
|
```
|
|
|
|
and installs the user service at:
|
|
|
|
```text
|
|
~/.config/systemd/user/revive-monitor-drm-watch.service
|
|
```
|
|
|
|
The service is intentionally attached to `default.target`, because not every Hyprland setup activates `graphical-session.target`.
|
|
|
|
The watcher itself performs an additional safety check and refuses to switch VTs unless it finds an **active Wayland session for the current user**. This prevents DRM hotplug events during boot from switching the display manager away from its VT.
|
|
|
|
## 4. Configure sudo safely
|
|
|
|
`chvt` requires root privileges. Do **not** give the script unrestricted passwordless sudo.
|
|
|
|
### Recommended general rule
|
|
|
|
Open a dedicated sudoers file:
|
|
|
|
```bash
|
|
sudo visudo -f /etc/sudoers.d/revive-monitor-chvt
|
|
```
|
|
|
|
Add:
|
|
|
|
```text
|
|
YOUR_USER ALL=(root) NOPASSWD: /usr/bin/chvt
|
|
```
|
|
|
|
Replace `YOUR_USER` with your username.
|
|
|
|
This grants passwordless sudo only for `/usr/bin/chvt`, not for arbitrary root commands.
|
|
|
|
### More restrictive rule
|
|
|
|
If your graphical session always uses VT1 and the temporary VT is always VT2, use:
|
|
|
|
```text
|
|
YOUR_USER ALL=(root) NOPASSWD: /usr/bin/chvt 1, /usr/bin/chvt 2
|
|
```
|
|
|
|
A template is included in:
|
|
|
|
```text
|
|
sudoers/revive-monitor-chvt.strict.example
|
|
```
|
|
|
|
The scripts use `sudo -n`, so they never block waiting for a password prompt.
|
|
|
|
Test the rule manually:
|
|
|
|
```bash
|
|
sudo -n /usr/bin/chvt 2
|
|
sleep 1
|
|
sudo -n /usr/bin/chvt 1
|
|
```
|
|
|
|
## 5. Start the service
|
|
|
|
The installer enables it automatically. To do it manually:
|
|
|
|
```bash
|
|
systemctl --user daemon-reload
|
|
systemctl --user enable --now revive-monitor-drm-watch.service
|
|
```
|
|
|
|
Check status:
|
|
|
|
```bash
|
|
systemctl --user status revive-monitor-drm-watch.service
|
|
```
|
|
|
|
Watch logs:
|
|
|
|
```bash
|
|
journalctl --user -u revive-monitor-drm-watch.service -f
|
|
```
|
|
|
|
## Why not xrandr?
|
|
|
|
`xrandr` is an X11 tool. Under Hyprland/Wayland it is not the correct interface for controlling physical outputs and may only see Xwayland state. Typical symptoms include:
|
|
|
|
```text
|
|
Authorization required, but no authorization protocol specified
|
|
Can't open display :0
|
|
```
|
|
|
|
This project uses `hyprctl` for compositor-side state and DRM/udev events for the actual hotplug trigger.
|
|
|
|
## Why not poll every 20 or 30 seconds?
|
|
|
|
A polling approach can accidentally run disruptive operations while the desktop is already working. In early versions, a periodic DPMS toggle caused the monitor to go dark and come back every few seconds.
|
|
|
|
This implementation does not use a timer. Recovery is triggered only by:
|
|
|
|
```text
|
|
SUBSYSTEM=drm
|
|
HOTPLUG=1
|
|
```
|
|
|
|
and only if an active Wayland session exists.
|
|
|
|
## Why the active Wayland-session check matters
|
|
|
|
During boot, GPUs and displays can generate DRM hotplug events before the display manager or Hyprland session is ready.
|
|
|
|
Without a session guard, a recovery script that always runs:
|
|
|
|
```bash
|
|
chvt 2
|
|
chvt 1
|
|
```
|
|
|
|
can leave the machine on tty1 while the display manager is actually running on tty2.
|
|
|
|
The watcher therefore queries `systemd-logind` via `loginctl` and only runs recovery when it finds a session matching all of these conditions:
|
|
|
|
```text
|
|
Name=<current user>
|
|
Type=wayland
|
|
Active=yes
|
|
VTNr>0
|
|
```
|
|
|
|
The current graphical VT is detected dynamically and passed to the recovery script. No VT is hard-coded.
|
|
|
|
## Example successful log
|
|
|
|
```text
|
|
revive-monitor-drm-watch: DRM hotplug detected - Wayland session 3 on VT 1
|
|
revive-monitor-fix: Recovery started for HDMI-A-2 on VT 1
|
|
revive-monitor-fix: VT refresh: 2 -> 1
|
|
sudo: COMMAND=/usr/bin/chvt 2
|
|
sudo: COMMAND=/usr/bin/chvt 1
|
|
revive-monitor-fix: Recovery completed for HDMI-A-2
|
|
revive-monitor-drm-watch: Recovery after DRM hotplug finished
|
|
```
|
|
|
|
## Verify the boot-safety fix
|
|
|
|
After rebooting, check:
|
|
|
|
```bash
|
|
journalctl -b --no-pager | grep -E 'revive-monitor|COMMAND=/usr/bin/chvt|chvt'
|
|
```
|
|
|
|
Before logging into Hyprland, you should either see no recovery at all or a message similar to:
|
|
|
|
```text
|
|
No active Wayland session - DRM hotplug ignored
|
|
```
|
|
|
|
There should be no `chvt` call before the graphical user session exists.
|
|
|
|
## Uninstall
|
|
|
|
Run:
|
|
|
|
```bash
|
|
./scripts/uninstall.sh
|
|
```
|
|
|
|
Then remove the sudoers file manually:
|
|
|
|
```bash
|
|
sudo rm /etc/sudoers.d/revive-monitor-chvt
|
|
```
|
|
|
|
The uninstall script intentionally does not remove sudoers configuration automatically.
|
|
|
|
## Troubleshooting
|
|
|
|
See [docs/troubleshooting.md](docs/troubleshooting.md).
|
|
|
|
## Security notes
|
|
|
|
- The service runs as a normal systemd **user service**.
|
|
- Only `chvt` requires root privileges.
|
|
- `sudo -n` prevents unattended password prompts.
|
|
- A lock file prevents overlapping recovery runs.
|
|
- Debouncing suppresses repeated DRM hotplug events.
|
|
- The recovery runs only for an active Wayland session belonging to the current user.
|
|
- The sudoers rule should be limited to `/usr/bin/chvt`, or ideally only the specific VTs used on the machine.
|
|
|
|
## License
|
|
|
|
MIT. See [LICENSE](LICENSE).
|