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:
parent
fb40347c68
commit
ac2bb77113
@ -93,6 +93,12 @@ func (h *hyprlandBackend) GetMonitors(ctx context.Context) ([]MonitorInfo, error
|
|||||||
// ApplyLayout writes the monitor configuration file and calls hyprctl reload.
|
// ApplyLayout writes the monitor configuration file and calls hyprctl reload.
|
||||||
// The config is written atomically (temp file + rename) to prevent corruption.
|
// The config is written atomically (temp file + rename) to prevent corruption.
|
||||||
func (h *hyprlandBackend) ApplyLayout(ctx context.Context, monitors []MonitorConfig) error {
|
func (h *hyprlandBackend) ApplyLayout(ctx context.Context, monitors []MonitorConfig) error {
|
||||||
|
var err error
|
||||||
|
monitors, err = resolveMonitorNames(ctx, monitors, h.GetMonitors, h.logger)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("resolve monitor names: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
content := h.generateConf(monitors)
|
content := h.generateConf(monitors)
|
||||||
|
|
||||||
destPath, err := h.resolveOutputPath()
|
destPath, err := h.resolveOutputPath()
|
||||||
|
|||||||
@ -8,7 +8,12 @@
|
|||||||
// cmd/monitor-lets-go/main.go.
|
// cmd/monitor-lets-go/main.go.
|
||||||
package backend
|
package backend
|
||||||
|
|
||||||
import "context"
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
// MonitorInfo represents a physical display as reported by the compositor.
|
// MonitorInfo represents a physical display as reported by the compositor.
|
||||||
// Fields use JSON tags matching hyprctl -j output; other compositors
|
// Fields use JSON tags matching hyprctl -j output; other compositors
|
||||||
@ -97,3 +102,65 @@ type Backend interface {
|
|||||||
// Close releases any resources held by the backend.
|
// Close releases any resources held by the backend.
|
||||||
Close() error
|
Close() error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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 resolveMonitorNames(ctx context.Context, monitors []MonitorConfig, getMonitors func(context.Context) ([]MonitorInfo, error), logger *slog.Logger) ([]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 := 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
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|||||||
@ -202,7 +202,7 @@ func (s *swayBackend) GetMonitors(ctx context.Context) ([]MonitorInfo, error) {
|
|||||||
// enable commands always precede disables.
|
// enable commands always precede disables.
|
||||||
func (s *swayBackend) ApplyLayout(ctx context.Context, monitors []MonitorConfig) error {
|
func (s *swayBackend) ApplyLayout(ctx context.Context, monitors []MonitorConfig) error {
|
||||||
var err error
|
var err error
|
||||||
monitors, err = s.resolveMonitorNames(ctx, monitors)
|
monitors, err = resolveMonitorNames(ctx, monitors, s.GetMonitors, s.logger)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("resolve monitor names: %w", err)
|
return fmt.Errorf("resolve monitor names: %w", err)
|
||||||
}
|
}
|
||||||
@ -222,68 +222,6 @@ func (s *swayBackend) ApplyLayout(ctx context.Context, monitors []MonitorConfig)
|
|||||||
return nil
|
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.
|
// buildCommands assembles the swaymsg command string.
|
||||||
// Enabled monitors go first (enable + configure), then disabled ones.
|
// Enabled monitors go first (enable + configure), then disabled ones.
|
||||||
// Commands are joined with ';' for batch execution.
|
// Commands are joined with ';' for batch execution.
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user