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,14 @@
|
|||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
charset = utf-8
|
||||||
|
end_of_line = lf
|
||||||
|
insert_final_newline = true
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
|
||||||
|
[*.sh]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 4
|
||||||
|
|
||||||
|
[*.md]
|
||||||
|
trim_trailing_whitespace = false
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
---
|
||||||
|
name: Bug report
|
||||||
|
about: Report a problem with HDMI recovery or session handling
|
||||||
|
title: "[Bug] "
|
||||||
|
labels: bug
|
||||||
|
---
|
||||||
|
|
||||||
|
## Environment
|
||||||
|
|
||||||
|
- Distribution:
|
||||||
|
- Hyprland version (`hyprctl version`):
|
||||||
|
- Kernel (`uname -r`):
|
||||||
|
- GPU / driver:
|
||||||
|
- Display manager:
|
||||||
|
- Monitor model:
|
||||||
|
- Output name (`hyprctl monitors`):
|
||||||
|
|
||||||
|
## What happened?
|
||||||
|
|
||||||
|
Describe the observed behavior.
|
||||||
|
|
||||||
|
## What did you expect?
|
||||||
|
|
||||||
|
Describe the expected behavior.
|
||||||
|
|
||||||
|
## Relevant logs
|
||||||
|
|
||||||
|
```text
|
||||||
|
journalctl --user -u revive-monitor-drm-watch.service -n 100 --no-pager
|
||||||
|
```
|
||||||
|
|
||||||
|
```text
|
||||||
|
journalctl --user -t revive-monitor-fix -n 100 --no-pager
|
||||||
|
```
|
||||||
|
|
||||||
|
## DRM hotplug test
|
||||||
|
|
||||||
|
Does this produce `HOTPLUG=1` while switching monitor inputs?
|
||||||
|
|
||||||
|
```bash
|
||||||
|
udevadm monitor --kernel --property --subsystem-match=drm
|
||||||
|
```
|
||||||
|
|
||||||
|
## Active session information
|
||||||
|
|
||||||
|
```bash
|
||||||
|
loginctl list-sessions
|
||||||
|
```
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
---
|
||||||
|
name: Feature request
|
||||||
|
about: Suggest an improvement
|
||||||
|
title: "[Feature] "
|
||||||
|
labels: enhancement
|
||||||
|
---
|
||||||
|
|
||||||
|
## Use case
|
||||||
|
|
||||||
|
Describe the problem or setup the feature would address.
|
||||||
|
|
||||||
|
## Proposed behavior
|
||||||
|
|
||||||
|
Describe how you think it should work.
|
||||||
|
|
||||||
|
## Additional context
|
||||||
|
|
||||||
|
Include relevant hardware, compositor, DRM, or systemd details.
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
## Summary
|
||||||
|
|
||||||
|
Describe the change and why it is needed.
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
- [ ] Shell syntax validated with `bash -n`
|
||||||
|
- [ ] Tested with an active Hyprland Wayland session
|
||||||
|
- [ ] Confirmed that no VT switch occurs before login
|
||||||
|
- [ ] Confirmed that HDMI recovery still works after switching inputs
|
||||||
|
- [ ] Updated documentation where necessary
|
||||||
|
|
||||||
|
## Security impact
|
||||||
|
|
||||||
|
Describe any change involving `sudo`, `chvt`, systemd permissions, or privilege boundaries.
|
||||||
+24
@@ -0,0 +1,24 @@
|
|||||||
|
# Local configuration
|
||||||
|
config.env
|
||||||
|
*.local
|
||||||
|
*.env.local
|
||||||
|
|
||||||
|
# Runtime / logs
|
||||||
|
*.log
|
||||||
|
*.pid
|
||||||
|
*.lock
|
||||||
|
*.tmp
|
||||||
|
|
||||||
|
# Editors / IDEs
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*~
|
||||||
|
|
||||||
|
# OS metadata
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Shell tooling
|
||||||
|
.shellcheckrc.local
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
# Changelog
|
||||||
|
|
||||||
|
All notable changes to this project will be documented here.
|
||||||
|
|
||||||
|
## 0.1.0 - 2026-09-23
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- DRM `HOTPLUG=1` event watcher using `udevadm monitor`.
|
||||||
|
- Dynamic detection of the active Wayland session and its VT via `loginctl`.
|
||||||
|
- VT-based HDMI recovery using `chvt`.
|
||||||
|
- Hyprland DPMS and monitor-focus refresh before and after the VT switch.
|
||||||
|
- Debounce protection for duplicate DRM events.
|
||||||
|
- `flock` protection against overlapping recovery executions.
|
||||||
|
- systemd user service.
|
||||||
|
- Installation and uninstall scripts.
|
||||||
|
- Configurable monitor name and timing values.
|
||||||
|
- Restricted sudoers examples.
|
||||||
|
- Boot-safety guard to avoid switching away from the display manager before login.
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2026 Johannes Rest
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
@@ -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).
|
||||||
+38
@@ -0,0 +1,38 @@
|
|||||||
|
# Security
|
||||||
|
|
||||||
|
This project intentionally keeps almost all functionality unprivileged.
|
||||||
|
|
||||||
|
## Privileged operation
|
||||||
|
|
||||||
|
Only `chvt` requires root privileges. The recommended sudoers rule is limited to:
|
||||||
|
|
||||||
|
```text
|
||||||
|
/usr/bin/chvt
|
||||||
|
```
|
||||||
|
|
||||||
|
or, when the machine uses fixed VTs, to explicit arguments such as:
|
||||||
|
|
||||||
|
```text
|
||||||
|
/usr/bin/chvt 1
|
||||||
|
/usr/bin/chvt 2
|
||||||
|
```
|
||||||
|
|
||||||
|
Do not configure unrestricted passwordless sudo for the user or for the recovery scripts.
|
||||||
|
|
||||||
|
## Non-interactive sudo
|
||||||
|
|
||||||
|
The recovery script always invokes sudo with `-n`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo -n /usr/bin/chvt ...
|
||||||
|
```
|
||||||
|
|
||||||
|
This prevents the systemd user service from hanging on a password prompt.
|
||||||
|
|
||||||
|
## Boot safety
|
||||||
|
|
||||||
|
DRM hotplug events can occur during boot. The watcher therefore verifies that the current user has an active Wayland session with a valid VT before any recovery or VT switch is attempted.
|
||||||
|
|
||||||
|
## Reporting security issues
|
||||||
|
|
||||||
|
If this repository is published publicly, report security-sensitive findings privately through the repository owner's preferred security contact rather than opening a public issue containing exploitable details.
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
# Hyprland output name. Check with: hyprctl monitors
|
||||||
|
MONITOR=HDMI-A-2
|
||||||
|
|
||||||
|
# Suppress repeated DRM hotplug events within this interval.
|
||||||
|
DEBOUNCE_SECONDS=30
|
||||||
|
|
||||||
|
# Allow the monitor/HDMI handshake to settle before the VT refresh.
|
||||||
|
HDMI_SETTLE_SECONDS=5
|
||||||
|
|
||||||
|
# Time spent on the temporary VT before returning to Hyprland.
|
||||||
|
VT_SWITCH_DELAY_SECONDS=1
|
||||||
|
|
||||||
|
# Time to wait after returning to the graphical VT.
|
||||||
|
VT_RETURN_SETTLE_SECONDS=2
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
# Architecture
|
||||||
|
|
||||||
|
## Problem
|
||||||
|
|
||||||
|
A monitor shared between multiple physical HDMI inputs can keep its EDID/output state visible to Linux even while the monitor is displaying another source. When switching back, Hyprland may report the output as completely healthy while the physical monitor remains black or reports no source.
|
||||||
|
|
||||||
|
Typical Hyprland state in the broken condition:
|
||||||
|
|
||||||
|
```text
|
||||||
|
focused: yes
|
||||||
|
dpmsStatus: 1
|
||||||
|
disabled: false
|
||||||
|
active workspace: 1
|
||||||
|
```
|
||||||
|
|
||||||
|
Commands such as `hyprctl reload`, `hyprctl dispatch dpms on`, and `hyprctl dispatch focusmonitor` may all return `ok` without restoring the image.
|
||||||
|
|
||||||
|
## Discovery
|
||||||
|
|
||||||
|
Listening to Hyprland's event socket did not provide a reliable monitor add/remove event for the physical input switch.
|
||||||
|
|
||||||
|
The kernel DRM subsystem did, however, emit:
|
||||||
|
|
||||||
|
```text
|
||||||
|
SUBSYSTEM=drm
|
||||||
|
HOTPLUG=1
|
||||||
|
```
|
||||||
|
|
||||||
|
via:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
udevadm monitor --kernel --property --subsystem-match=drm
|
||||||
|
```
|
||||||
|
|
||||||
|
A manual VT switch restored the image, indicating that the required reset happens below the compositor level.
|
||||||
|
|
||||||
|
## Final design
|
||||||
|
|
||||||
|
The long-running watcher consumes kernel DRM events. For every `HOTPLUG=1` event it:
|
||||||
|
|
||||||
|
1. checks `systemd-logind` for an active Wayland session belonging to the current user;
|
||||||
|
2. ignores the event if no such session exists;
|
||||||
|
3. dynamically determines that session's VT;
|
||||||
|
4. applies debounce protection;
|
||||||
|
5. calls the recovery script with the detected VT.
|
||||||
|
|
||||||
|
The recovery script:
|
||||||
|
|
||||||
|
1. uses `flock` to prevent overlap;
|
||||||
|
2. waits for the HDMI handshake to settle;
|
||||||
|
3. asks Hyprland to enable/focus the output;
|
||||||
|
4. uses `chvt` to move to another VT;
|
||||||
|
5. returns to the dynamically detected graphical VT;
|
||||||
|
6. refreshes Hyprland DPMS/focus once more.
|
||||||
|
|
||||||
|
## Why the Wayland-session guard is critical
|
||||||
|
|
||||||
|
GPU initialization during boot can generate DRM hotplug events. If the recovery script blindly executes a hard-coded `chvt 2` followed by `chvt 1`, the machine can end up showing tty1 even though the display manager is running on tty2.
|
||||||
|
|
||||||
|
The current implementation prevents this by requiring:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Name=<current user>
|
||||||
|
Type=wayland
|
||||||
|
Active=yes
|
||||||
|
VTNr>0
|
||||||
|
```
|
||||||
|
|
||||||
|
before performing any recovery.
|
||||||
|
|
||||||
|
## Privilege boundary
|
||||||
|
|
||||||
|
The watcher and recovery script run as the normal user. Only `chvt` needs elevated privileges.
|
||||||
|
|
||||||
|
The recommended sudoers rule limits passwordless sudo to:
|
||||||
|
|
||||||
|
```text
|
||||||
|
/usr/bin/chvt
|
||||||
|
```
|
||||||
|
|
||||||
|
The script always calls it through `sudo -n`, which fails instead of prompting for a password.
|
||||||
@@ -0,0 +1,139 @@
|
|||||||
|
# Troubleshooting
|
||||||
|
|
||||||
|
## Check the Hyprland output name
|
||||||
|
|
||||||
|
```bash
|
||||||
|
hyprctl monitors
|
||||||
|
```
|
||||||
|
|
||||||
|
Update:
|
||||||
|
|
||||||
|
```text
|
||||||
|
~/.config/revive-monitor/config.env
|
||||||
|
```
|
||||||
|
|
||||||
|
if the output is not `HDMI-A-2`.
|
||||||
|
|
||||||
|
## Check the user service
|
||||||
|
|
||||||
|
```bash
|
||||||
|
systemctl --user status revive-monitor-drm-watch.service
|
||||||
|
```
|
||||||
|
|
||||||
|
## Follow logs
|
||||||
|
|
||||||
|
```bash
|
||||||
|
journalctl --user -u revive-monitor-drm-watch.service -f
|
||||||
|
```
|
||||||
|
|
||||||
|
For the recovery script tag:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
journalctl --user -t revive-monitor-fix -f
|
||||||
|
```
|
||||||
|
|
||||||
|
## Verify DRM hotplug events manually
|
||||||
|
|
||||||
|
```bash
|
||||||
|
udevadm monitor --kernel --property --subsystem-match=drm
|
||||||
|
```
|
||||||
|
|
||||||
|
Switch the physical monitor input away and back. Look for:
|
||||||
|
|
||||||
|
```text
|
||||||
|
HOTPLUG=1
|
||||||
|
```
|
||||||
|
|
||||||
|
If no DRM hotplug event is emitted, this project has no reliable event to trigger from.
|
||||||
|
|
||||||
|
## Verify the active Wayland session
|
||||||
|
|
||||||
|
```bash
|
||||||
|
loginctl list-sessions
|
||||||
|
```
|
||||||
|
|
||||||
|
Then:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
for s in $(loginctl list-sessions --no-legend | awk '{print $1}'); do
|
||||||
|
echo "=== Session $s ==="
|
||||||
|
loginctl show-session "$s" -p Name -p Type -p Active -p VTNr
|
||||||
|
done
|
||||||
|
```
|
||||||
|
|
||||||
|
A normal Hyprland session should look similar to:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Name=your-user
|
||||||
|
VTNr=1
|
||||||
|
Type=wayland
|
||||||
|
Active=yes
|
||||||
|
```
|
||||||
|
|
||||||
|
## Display manager appears only after manually switching TTY
|
||||||
|
|
||||||
|
Check the boot journal:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
journalctl -b --no-pager | grep -E 'revive-monitor|COMMAND=/usr/bin/chvt|chvt'
|
||||||
|
```
|
||||||
|
|
||||||
|
If `chvt` runs before login, verify that you installed the current watcher script. It must ignore hotplug events when there is no active Wayland session.
|
||||||
|
|
||||||
|
Expected pre-login behavior:
|
||||||
|
|
||||||
|
```text
|
||||||
|
No active Wayland session - DRM hotplug ignored
|
||||||
|
```
|
||||||
|
|
||||||
|
## sudo / chvt fails
|
||||||
|
|
||||||
|
Test non-interactively:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo -n /usr/bin/chvt 2
|
||||||
|
```
|
||||||
|
|
||||||
|
If sudo asks for a password or exits with an error, verify:
|
||||||
|
|
||||||
|
```text
|
||||||
|
/etc/sudoers.d/revive-monitor-chvt
|
||||||
|
```
|
||||||
|
|
||||||
|
Always edit sudoers files with `visudo`.
|
||||||
|
|
||||||
|
## Recovery runs multiple times
|
||||||
|
|
||||||
|
Increase:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
DEBOUNCE_SECONDS=30
|
||||||
|
```
|
||||||
|
|
||||||
|
in:
|
||||||
|
|
||||||
|
```text
|
||||||
|
~/.config/revive-monitor/config.env
|
||||||
|
```
|
||||||
|
|
||||||
|
For noisy hardware, `60` may be more appropriate.
|
||||||
|
|
||||||
|
## Recovery runs but the monitor still stays black
|
||||||
|
|
||||||
|
Increase:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
HDMI_SETTLE_SECONDS=5
|
||||||
|
```
|
||||||
|
|
||||||
|
to `7` or `10` and test again.
|
||||||
|
|
||||||
|
Also manually verify that a VT switch actually restores the image:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo chvt 2
|
||||||
|
sleep 1
|
||||||
|
sudo chvt 1
|
||||||
|
```
|
||||||
|
|
||||||
|
Adjust the numbers to match the machine's graphical VT.
|
||||||
Executable
+35
@@ -0,0 +1,35 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
BIN_DIR="$HOME/.local/bin"
|
||||||
|
SYSTEMD_DIR="$HOME/.config/systemd/user"
|
||||||
|
CONFIG_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/revive-monitor"
|
||||||
|
|
||||||
|
mkdir -p "$BIN_DIR" "$SYSTEMD_DIR" "$CONFIG_DIR"
|
||||||
|
|
||||||
|
install -m 0755 "$REPO_ROOT/scripts/revive-monitor-fix.sh" "$BIN_DIR/revive-monitor-fix"
|
||||||
|
install -m 0755 "$REPO_ROOT/scripts/revive-monitor-drm-watch.sh" "$BIN_DIR/revive-monitor-drm-watch"
|
||||||
|
install -m 0644 "$REPO_ROOT/systemd/user/revive-monitor-drm-watch.service" "$SYSTEMD_DIR/revive-monitor-drm-watch.service"
|
||||||
|
|
||||||
|
if [[ ! -e "$CONFIG_DIR/config.env" ]]; then
|
||||||
|
install -m 0644 "$REPO_ROOT/config/revive-monitor.env.example" "$CONFIG_DIR/config.env"
|
||||||
|
printf 'Created %s\n' "$CONFIG_DIR/config.env"
|
||||||
|
printf 'Edit MONITOR before using the service if your output is not HDMI-A-2.\n'
|
||||||
|
else
|
||||||
|
printf 'Keeping existing config: %s\n' "$CONFIG_DIR/config.env"
|
||||||
|
fi
|
||||||
|
|
||||||
|
systemctl --user daemon-reload
|
||||||
|
systemctl --user enable --now revive-monitor-drm-watch.service
|
||||||
|
|
||||||
|
cat <<'MSG'
|
||||||
|
|
||||||
|
Installed successfully.
|
||||||
|
|
||||||
|
Next steps:
|
||||||
|
1. Verify ~/.config/revive-monitor/config.env
|
||||||
|
2. Configure /etc/sudoers.d/revive-monitor-chvt using one of the templates in sudoers/
|
||||||
|
3. Test with: systemctl --user status revive-monitor-drm-watch.service
|
||||||
|
4. Watch logs: journalctl --user -u revive-monitor-drm-watch.service -f
|
||||||
|
MSG
|
||||||
Executable
+115
@@ -0,0 +1,115 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -u
|
||||||
|
|
||||||
|
LOGTAG="revive-monitor-drm-watch"
|
||||||
|
CONFIG_FILE="${XDG_CONFIG_HOME:-$HOME/.config}/revive-monitor/config.env"
|
||||||
|
FIX_SCRIPT="${REVIVE_MONITOR_FIX_SCRIPT:-$HOME/.local/bin/revive-monitor-fix}"
|
||||||
|
|
||||||
|
# Defaults. Override in ~/.config/revive-monitor/config.env
|
||||||
|
DEBOUNCE_SECONDS="30"
|
||||||
|
|
||||||
|
if [[ -r "$CONFIG_FILE" ]]; then
|
||||||
|
# shellcheck disable=SC1090
|
||||||
|
source "$CONFIG_FILE"
|
||||||
|
fi
|
||||||
|
|
||||||
|
STATE_DIR="${XDG_RUNTIME_DIR:-/tmp}/revive-monitor"
|
||||||
|
LAST_RUN_FILE="$STATE_DIR/last-drm-hotplug-run"
|
||||||
|
|
||||||
|
mkdir -p "$STATE_DIR"
|
||||||
|
|
||||||
|
log() {
|
||||||
|
logger -t "$LOGTAG" -- "$*"
|
||||||
|
}
|
||||||
|
|
||||||
|
get_active_wayland_session() {
|
||||||
|
local sid
|
||||||
|
local name
|
||||||
|
local type
|
||||||
|
local active
|
||||||
|
local vt
|
||||||
|
|
||||||
|
while read -r sid; do
|
||||||
|
[[ -n "$sid" ]] || continue
|
||||||
|
|
||||||
|
name="$(loginctl show-session "$sid" -p Name --value 2>/dev/null)"
|
||||||
|
[[ "$name" == "$USER" ]] || continue
|
||||||
|
|
||||||
|
type="$(loginctl show-session "$sid" -p Type --value 2>/dev/null)"
|
||||||
|
[[ "$type" == "wayland" ]] || continue
|
||||||
|
|
||||||
|
active="$(loginctl show-session "$sid" -p Active --value 2>/dev/null)"
|
||||||
|
[[ "$active" == "yes" ]] || continue
|
||||||
|
|
||||||
|
vt="$(loginctl show-session "$sid" -p VTNr --value 2>/dev/null)"
|
||||||
|
|
||||||
|
if [[ "$vt" =~ ^[1-9][0-9]*$ ]]; then
|
||||||
|
printf '%s:%s\n' "$sid" "$vt"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
done < <(
|
||||||
|
loginctl list-sessions --no-legend --no-pager 2>/dev/null | awk '{print $1}'
|
||||||
|
)
|
||||||
|
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
run_recovery() {
|
||||||
|
local session_info
|
||||||
|
local session_id
|
||||||
|
local graphical_vt
|
||||||
|
local now
|
||||||
|
local last="0"
|
||||||
|
local diff
|
||||||
|
|
||||||
|
if ! session_info="$(get_active_wayland_session)"; then
|
||||||
|
log "No active Wayland session - DRM hotplug ignored"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
session_id="${session_info%%:*}"
|
||||||
|
graphical_vt="${session_info##*:}"
|
||||||
|
|
||||||
|
now="$(date +%s)"
|
||||||
|
|
||||||
|
if [[ -r "$LAST_RUN_FILE" ]]; then
|
||||||
|
last="$(cat "$LAST_RUN_FILE" 2>/dev/null || printf '0')"
|
||||||
|
fi
|
||||||
|
|
||||||
|
[[ "$last" =~ ^[0-9]+$ ]] || last="0"
|
||||||
|
diff=$((now - last))
|
||||||
|
|
||||||
|
if (( diff < DEBOUNCE_SECONDS )); then
|
||||||
|
log "DRM hotplug ignored because of debounce (${diff}s)"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf '%s\n' "$now" > "$LAST_RUN_FILE"
|
||||||
|
|
||||||
|
log "DRM hotplug detected - Wayland session $session_id on VT $graphical_vt"
|
||||||
|
|
||||||
|
"$FIX_SCRIPT" "$graphical_vt"
|
||||||
|
|
||||||
|
log "Recovery after DRM hotplug finished"
|
||||||
|
}
|
||||||
|
|
||||||
|
if ! command -v udevadm >/dev/null 2>&1; then
|
||||||
|
log "udevadm not found"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ ! -x "$FIX_SCRIPT" ]]; then
|
||||||
|
log "Recovery script is not executable: $FIX_SCRIPT"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "Listening for DRM hotplug events"
|
||||||
|
|
||||||
|
stdbuf -oL udevadm monitor --kernel --property --subsystem-match=drm |
|
||||||
|
while IFS= read -r line; do
|
||||||
|
case "$line" in
|
||||||
|
HOTPLUG=1)
|
||||||
|
run_recovery
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
Executable
+77
@@ -0,0 +1,77 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -u
|
||||||
|
|
||||||
|
LOGTAG="revive-monitor-fix"
|
||||||
|
CONFIG_FILE="${XDG_CONFIG_HOME:-$HOME/.config}/revive-monitor/config.env"
|
||||||
|
|
||||||
|
# Defaults. Override in ~/.config/revive-monitor/config.env
|
||||||
|
MONITOR="HDMI-A-2"
|
||||||
|
HDMI_SETTLE_SECONDS="5"
|
||||||
|
VT_SWITCH_DELAY_SECONDS="1"
|
||||||
|
VT_RETURN_SETTLE_SECONDS="2"
|
||||||
|
|
||||||
|
if [[ -r "$CONFIG_FILE" ]]; then
|
||||||
|
# shellcheck disable=SC1090
|
||||||
|
source "$CONFIG_FILE"
|
||||||
|
fi
|
||||||
|
|
||||||
|
GRAPHICAL_VT="${1:-}"
|
||||||
|
STATE_DIR="${XDG_RUNTIME_DIR:-/tmp}/revive-monitor"
|
||||||
|
LOCK_FILE="$STATE_DIR/fix.lock"
|
||||||
|
|
||||||
|
mkdir -p "$STATE_DIR"
|
||||||
|
|
||||||
|
log() {
|
||||||
|
logger -t "$LOGTAG" -- "$*"
|
||||||
|
}
|
||||||
|
|
||||||
|
run_quiet() {
|
||||||
|
"$@" >/dev/null 2>&1
|
||||||
|
}
|
||||||
|
|
||||||
|
if ! [[ "$GRAPHICAL_VT" =~ ^[1-9][0-9]*$ ]]; then
|
||||||
|
log "No valid graphical VT supplied - recovery aborted"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$GRAPHICAL_VT" -eq 1 ]]; then
|
||||||
|
TEMP_VT=2
|
||||||
|
else
|
||||||
|
TEMP_VT=1
|
||||||
|
fi
|
||||||
|
|
||||||
|
exec 9>"$LOCK_FILE"
|
||||||
|
if ! flock -n 9; then
|
||||||
|
log "Recovery already running - skipping"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "Recovery started for $MONITOR on VT $GRAPHICAL_VT"
|
||||||
|
|
||||||
|
sleep "$HDMI_SETTLE_SECONDS"
|
||||||
|
|
||||||
|
run_quiet hyprctl dispatch dpms on "$MONITOR"
|
||||||
|
run_quiet hyprctl dispatch focusmonitor "$MONITOR"
|
||||||
|
|
||||||
|
sleep 1
|
||||||
|
|
||||||
|
log "VT refresh: $TEMP_VT -> $GRAPHICAL_VT"
|
||||||
|
|
||||||
|
if ! sudo -n /usr/bin/chvt "$TEMP_VT"; then
|
||||||
|
log "Failed to switch to temporary VT $TEMP_VT"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
sleep "$VT_SWITCH_DELAY_SECONDS"
|
||||||
|
|
||||||
|
if ! sudo -n /usr/bin/chvt "$GRAPHICAL_VT"; then
|
||||||
|
log "Failed to return to graphical VT $GRAPHICAL_VT"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
sleep "$VT_RETURN_SETTLE_SECONDS"
|
||||||
|
|
||||||
|
run_quiet hyprctl dispatch dpms on "$MONITOR"
|
||||||
|
run_quiet hyprctl dispatch focusmonitor "$MONITOR"
|
||||||
|
|
||||||
|
log "Recovery completed for $MONITOR"
|
||||||
Executable
+24
@@ -0,0 +1,24 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
BIN_DIR="$HOME/.local/bin"
|
||||||
|
SYSTEMD_DIR="$HOME/.config/systemd/user"
|
||||||
|
|
||||||
|
systemctl --user disable --now revive-monitor-drm-watch.service 2>/dev/null || true
|
||||||
|
|
||||||
|
rm -f "$SYSTEMD_DIR/revive-monitor-drm-watch.service"
|
||||||
|
rm -f "$BIN_DIR/revive-monitor-fix"
|
||||||
|
rm -f "$BIN_DIR/revive-monitor-drm-watch"
|
||||||
|
|
||||||
|
systemctl --user daemon-reload
|
||||||
|
systemctl --user reset-failed
|
||||||
|
|
||||||
|
cat <<'MSG'
|
||||||
|
Removed the user service and installed scripts.
|
||||||
|
|
||||||
|
Not removed automatically:
|
||||||
|
~/.config/revive-monitor/config.env
|
||||||
|
/etc/sudoers.d/revive-monitor-chvt
|
||||||
|
|
||||||
|
Remove those manually if you no longer need them.
|
||||||
|
MSG
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
# Replace YOUR_USER with the Linux username running Hyprland.
|
||||||
|
# This permits passwordless sudo only for /usr/bin/chvt.
|
||||||
|
YOUR_USER ALL=(root) NOPASSWD: /usr/bin/chvt
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
# Replace YOUR_USER with the Linux username running Hyprland.
|
||||||
|
# Use this stricter version only if the machine always switches between VT1 and VT2.
|
||||||
|
YOUR_USER ALL=(root) NOPASSWD: /usr/bin/chvt 1, /usr/bin/chvt 2
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Watch DRM hotplug events and revive HDMI monitor
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
ExecStart=%h/.local/bin/revive-monitor-drm-watch
|
||||||
|
Restart=on-failure
|
||||||
|
RestartSec=2
|
||||||
|
Environment=XDG_RUNTIME_DIR=%t
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=default.target
|
||||||
Reference in new issue
Block a user