feat: serial:/size: external matching, exclude Hyprland FALLBACK output

- external list now accepts serial: and size: prefixes; desc: matches
  serial too. Fixes docked-mode deadlock where Hyprland's synthetic
  FALLBACK output (created when the built-in is disabled by a stale
  docked config) was treated as a real external monitor, leaving the
  daemon stuck in docked after undocking.
- modes name resolution gains serial: prefix (resolveSerial).
- add -log-level flag for observability.
- add unit tests for external matching and name resolution.
- update README and example config.
This commit is contained in:
Maksim Totmin
2026-08-06 12:25:47 +07:00
parent 3515608fd3
commit ab6cad0505
10 changed files with 393 additions and 31 deletions
+21 -3
View File
@@ -183,6 +183,9 @@ func (d *Daemon) pollCheck(ctx context.Context) {
}
// determineState checks whether any external monitor is physically connected.
// Synthetic outputs that never represent a real external display (e.g.
// Hyprland's FALLBACK monitor) are excluded by MatchesExternal, so a stuck
// docked config cannot keep the daemon in docked mode after undocking.
func (d *Daemon) determineState(monitors []backend.MonitorInfo) backend.State {
for _, m := range monitors {
// Skip phantom or disconnected monitors.
@@ -191,7 +194,14 @@ func (d *Daemon) determineState(monitors []backend.MonitorInfo) backend.State {
}
// A monitor is physically present if it reports non-zero dimensions.
if m.Width > 0 && m.Height > 0 {
if d.config.MatchesExternal(m.Name, m.Description) {
if d.config.MatchesExternal(config.ExternalMonitor{
Name: m.Name,
Description: m.Description,
Serial: m.Serial,
Width: m.Width,
Height: m.Height,
RefreshRate: m.RefreshRate,
}) {
return backend.StateDocked
}
}
@@ -261,11 +271,19 @@ func (d *Daemon) applyState(ctx context.Context, state backend.State) error {
}
// anyExternalConnected returns true if at least one configured external
// monitor is physically present.
// monitor is physically present. Synthetic outputs (e.g. Hyprland's FALLBACK)
// are excluded via MatchesExternal.
func (d *Daemon) anyExternalConnected(monitors []backend.MonitorInfo) bool {
for _, m := range monitors {
if m.Width > 0 && m.Height > 0 && m.Name != "" {
if d.config.MatchesExternal(m.Name, m.Description) {
if d.config.MatchesExternal(config.ExternalMonitor{
Name: m.Name,
Description: m.Description,
Serial: m.Serial,
Width: m.Width,
Height: m.Height,
RefreshRate: m.RefreshRate,
}) {
return true
}
}