fix(hyprland): resolve desc: monitor names before generating config

Extract shared resolveMonitorNames to backend package so both
Hyprland and Sway backends resolve desc: prefixed names to actual
connector names before applying monitor layout.

Previously the Hyprland backend wrote desc:... literally into
monitors.conf, which Hyprland ignores, causing monitors to be
positioned incorrectly or not configured at all.
This commit is contained in:
Maksim Totmin
2026-06-23 09:06:08 +07:00
parent fb40347c68
commit ac2bb77113
3 changed files with 75 additions and 64 deletions
+1 -63
View File
@@ -202,7 +202,7 @@ func (s *swayBackend) GetMonitors(ctx context.Context) ([]MonitorInfo, error) {
// enable commands always precede disables.
func (s *swayBackend) ApplyLayout(ctx context.Context, monitors []MonitorConfig) error {
var err error
monitors, err = s.resolveMonitorNames(ctx, monitors)
monitors, err = resolveMonitorNames(ctx, monitors, s.GetMonitors, s.logger)
if err != nil {
return fmt.Errorf("resolve monitor names: %w", err)
}
@@ -222,68 +222,6 @@ func (s *swayBackend) ApplyLayout(ctx context.Context, monitors []MonitorConfig)
return nil
}
// resolveMonitorNames converts desc: prefixed monitor config names to the
// actual connector names (e.g. "desc:Xiaomi Corporation Mi Monitor" → "DP-9")
// by matching against the current set of connected monitors.
//
// Matching logic:
// - "desc:make model" — substring match against description (make + " " + model)
// - "desc:serial" — substring match against serial number
// - plain names are returned as-is
func (s *swayBackend) resolveMonitorNames(ctx context.Context, monitors []MonitorConfig) ([]MonitorConfig, error) {
var needsDescResolution bool
for _, m := range monitors {
if strings.HasPrefix(m.Name, "desc:") {
needsDescResolution = true
break
}
}
if !needsDescResolution {
return monitors, nil
}
current, err := s.GetMonitors(ctx)
if err != nil {
return nil, fmt.Errorf("get current monitors: %w", err)
}
resolved := make([]MonitorConfig, len(monitors))
for i, m := range monitors {
if !strings.HasPrefix(m.Name, "desc:") {
resolved[i] = m
continue
}
needle := strings.TrimPrefix(m.Name, "desc:")
var matches []MonitorInfo
for _, mi := range current {
if strings.Contains(mi.Description, needle) || strings.Contains(mi.Serial, needle) {
matches = append(matches, mi)
}
}
switch len(matches) {
case 0:
return nil, fmt.Errorf("no connected monitor matches desc:%q", needle)
case 1:
resolved[i] = m
resolved[i].Name = matches[0].Name
s.logger.Debug("resolved desc to connector",
"desc", needle, "connector", matches[0].Name)
default:
var names []string
for _, mat := range matches {
names = append(names, mat.Name)
}
return nil, fmt.Errorf(
"desc:%q matches multiple monitors: %s — use a more specific identifier",
needle, strings.Join(names, ", "))
}
}
return resolved, nil
}
// buildCommands assembles the swaymsg command string.
// Enabled monitors go first (enable + configure), then disabled ones.
// Commands are joined with ';' for batch execution.