feat: size: prefix, external auto-detection, auto-disable stale monitors

- size:WxH and size:WxH@R resolution for config names
- auto-detection of external monitors when external list is empty (any non-internal display)
- ensureCleanLayout: explicitly disable connected monitors not in target layout
- update docs (README, example.yaml) for all three features
This commit is contained in:
Maksim Totmin
2026-06-24 19:51:18 +07:00
parent ac2bb77113
commit 3fdccadd3c
5 changed files with 223 additions and 54 deletions
+29 -10
View File
@@ -157,10 +157,8 @@ func (c *Config) validate() error {
if countEnabled(docked.Monitors) == 0 {
return errors.New("docked mode must have at least one enabled monitor")
}
// At least one external monitor must be listed.
if len(c.External) == 0 {
return errors.New("at least one external monitor must be specified")
}
// External list is optional. When empty, the daemon auto-detects
// external monitors (any non-internal display with non-zero dimensions).
return nil
}
@@ -195,19 +193,40 @@ func countEnabled(entries []MonitorEntry) int {
return n
}
// MatchesExternal checks whether a monitor name or description matches any
// entry in the External list. Supports two match modes:
// isInternalConnector returns true if the connector name matches a known
// internal display pattern (eDP, LVDS, DSI). These are always part of the
// laptop or tablet and should never trigger docked mode.
func isInternalConnector(name string) bool {
prefixes := []string{"eDP-", "LVDS-", "DSI-", "EDP-"}
for _, p := range prefixes {
if strings.HasPrefix(name, p) {
return true
}
}
return false
}
// MatchesExternal checks whether a monitor matches the External list.
// Supports match modes:
//
// - Plain name: exact match against MonitorEntry.Name
// - desc: prefix: substring match against MonitorEntry.Description
// - Plain name: exact match against name
// - desc: prefix: substring match against description
//
// When the External list is empty, any monitor that is not an internal
// display connector (eDP-, LVDS-, DSI-) is automatically external.
func (c *Config) MatchesExternal(name, description string) bool {
if len(c.External) == 0 {
return !isInternalConnector(name)
}
for _, ext := range c.External {
if strings.HasPrefix(ext, "desc:") {
switch {
case strings.HasPrefix(ext, "desc:"):
desc := strings.TrimPrefix(ext, "desc:")
if strings.Contains(description, desc) {
return true
}
} else {
default:
if name == ext {
return true
}