Import initial Hyprland HDMI recovery implementation
This commit is contained in:
commit
73e727ef4c
19 files changed
+1041
No files matched your search
@@ -0,0 +1,341 @@
|
||||
# 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).
|
||||
Reference in new issue
Block a user