From cc1137608c2d5d083c10434975e69b4dc2ff0787 Mon Sep 17 00:00:00 2001 From: Maksim Totmin Date: Mon, 22 Jun 2026 20:49:02 +0700 Subject: [PATCH] refactor: move backend options into backend_config section - Add BackendConfig map[string]any to Config struct - Deprecate top-level output_path (backward compatible via EffectiveBackendConfig()) - NewHyprland now accepts opts map[string]any instead of raw outputPath - Factory signature unified to func(*slog.Logger, map[string]any) for all backends - Update README with backend_config docs and fix Sway skeleton signature - Update example.yaml to show backend_config instead of output_path --- README.md | 22 +++++++++++++++++++++- cmd/monitor-lets-go/main.go | 12 +++++++----- internal/backend/hyprland.go | 17 ++++++++++++++--- internal/config/config.go | 26 ++++++++++++++++++++++++++ monitor-lets-go.example.yaml | 10 ++++++---- 5 files changed, 74 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 554a7b5..cbac86a 100644 --- a/README.md +++ b/README.md @@ -166,6 +166,25 @@ Two match modes in the `external` list: Run `hyprctl monitors all` to see your monitor names and descriptions. +### Backend-specific configuration + +Use the `backend_config` section for compositor-specific options: + +```yaml +backend_config: + output_path: ~/.config/hypr/custom-monitors.conf # override generated config path +``` + +**Hyprland keys:** + +| Key | Type | Default | Description | +|-----|------|---------|-------------| +| `output_path` | string | `~/.config/hypr/monitors.conf` | Path to the generated monitor config file. Supports `~` expansion. | + +The deprecated top-level `output_path` key still works — `backend_config` takes priority if both are set. + +When adding new compositor backends, their options go into the same `backend_config` section without changing the core config structure. + ### Hook commands Hooks are shell commands executed via `sh -c`. Each command gets a **30-second timeout**. Commands run in parallel — one slow hook won't block others. @@ -273,8 +292,9 @@ package backend type swayBackend struct { logger *slog.Logger } -func NewSway(logger *slog.Logger) (Backend, error) { +func NewSway(logger *slog.Logger, opts map[string]any) (Backend, error) { // Check if SWAYSOCK is set and swaymsg is available + // opts carries backend_config from the config file return &swayBackend{logger: logger}, nil } diff --git a/cmd/monitor-lets-go/main.go b/cmd/monitor-lets-go/main.go index 3563266..1388b1b 100644 --- a/cmd/monitor-lets-go/main.go +++ b/cmd/monitor-lets-go/main.go @@ -60,7 +60,9 @@ func run(logger *slog.Logger) error { logger.Info("config loaded", "path", configPath, "backend", cfg.Backend) // Resolve the backend (auto-detect or explicit). - b, err := resolveBackend(cfg.Backend, cfg.OutputPath, logger) + // EffectiveBackendConfig merges deprecated top-level keys + // into the backend_config map with proper priority. + b, err := resolveBackend(cfg.Backend, cfg.EffectiveBackendConfig(), logger) if err != nil { return fmt.Errorf("resolve backend: %w", err) } @@ -86,11 +88,11 @@ func run(logger *slog.Logger) error { // resolveBackend returns the first available backend. When backendName is // "auto", backends are probed in priority order. To add a new backend, // register it in the backends slice below. -func resolveBackend(backendName, outputPath string, logger *slog.Logger) (backend.Backend, error) { +func resolveBackend(backendName string, backendConfig map[string]any, logger *slog.Logger) (backend.Backend, error) { // Ordered by priority: preferred backends first. backends := []struct { name string - factory func(*slog.Logger, string) (backend.Backend, error) + factory func(*slog.Logger, map[string]any) (backend.Backend, error) }{ {"hyprland", backend.NewHyprland}, // Future: {"sway", backend.NewSway}, @@ -99,7 +101,7 @@ func resolveBackend(backendName, outputPath string, logger *slog.Logger) (backen if backendName == "auto" { for _, entry := range backends { - b, err := entry.factory(logger, outputPath) + b, err := entry.factory(logger, backendConfig) if err == nil { logger.Info("auto-detected backend", "name", b.Name()) return b, nil @@ -112,7 +114,7 @@ func resolveBackend(backendName, outputPath string, logger *slog.Logger) (backen // Explicit backend selection. for _, entry := range backends { if entry.name == backendName { - b, err := entry.factory(logger, outputPath) + b, err := entry.factory(logger, backendConfig) if err != nil { return nil, fmt.Errorf("backend %s: %w", backendName, err) } diff --git a/internal/backend/hyprland.go b/internal/backend/hyprland.go index e7bf379..5717d37 100644 --- a/internal/backend/hyprland.go +++ b/internal/backend/hyprland.go @@ -30,6 +30,7 @@ type hyprlandBackend struct { // outputPath overrides the generated monitor config file path. // Empty means the default: ~/.config/hypr/monitors.conf. + // Read from backend_config.output_path at construction time. outputPath string // conn is the current socket2 connection; guarded by mu. @@ -40,9 +41,12 @@ type hyprlandBackend struct { // NewHyprland creates a Hyprland backend. The backend auto-detects the // Hyprland instance signature from the environment. // -// outputPath overrides the generated monitor config file location. -// When empty, defaults to ~/.config/hypr/monitors.conf. -func NewHyprland(logger *slog.Logger, outputPath string) (Backend, error) { +// opts carries backend-specific configuration from the config file's +// backend_config section. Hyprland supports: +// +// output_path — path to the generated monitor config file +// (default: ~/.config/hypr/monitors.conf) +func NewHyprland(logger *slog.Logger, opts map[string]any) (Backend, error) { sig := os.Getenv("HYPRLAND_INSTANCE_SIGNATURE") if sig == "" { return nil, errors.New("HYPRLAND_INSTANCE_SIGNATURE is not set; is Hyprland running?") @@ -57,6 +61,13 @@ func NewHyprland(logger *slog.Logger, outputPath string) (Backend, error) { return nil, fmt.Errorf("socket2 not found at %s: %w", socketPath, err) } + var outputPath string + if opts != nil { + if v, ok := opts["output_path"]; ok { + outputPath, _ = v.(string) + } + } + return &hyprlandBackend{logger: logger, outputPath: outputPath}, nil } diff --git a/internal/config/config.go b/internal/config/config.go index 7717cea..1b70676 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -52,9 +52,16 @@ type Config struct { // OutputPath overrides the generated monitor config file path. // Supports ~ for home directory expansion. + // Deprecated: use backend_config.output_path instead. // Empty means the default: ~/.config/hypr/monitors.conf. OutputPath string `yaml:"output_path"` + // BackendConfig holds backend-specific configuration options. + // Keys and values vary by the selected backend. + // Hyprland supports: + // output_path — path to the generated monitor config file + BackendConfig map[string]any `yaml:"backend_config"` + // External lists monitor identifiers that trigger docked mode. // Each entry is a name (DP-1) or a desc: prefix (desc:Dell U2723QE). External []string `yaml:"external"` @@ -158,6 +165,25 @@ func (c *Config) validate() error { return nil } +// EffectiveBackendConfig merges top-level deprecated backend keys +// (OutputPath) into the BackendConfig map. backend_config values +// take priority over deprecated top-level keys. +// +// The caller receives a non-nil map even when no backend_config is +// set and no deprecated keys are present. +func (c *Config) EffectiveBackendConfig() map[string]any { + bc := c.BackendConfig + if bc == nil { + bc = make(map[string]any) + } + if c.OutputPath != "" { + if _, exists := bc["output_path"]; !exists { + bc["output_path"] = c.OutputPath + } + } + return bc +} + // countEnabled returns how many MonitorEntries are enabled. func countEnabled(entries []MonitorEntry) int { n := 0 diff --git a/monitor-lets-go.example.yaml b/monitor-lets-go.example.yaml index d5d1976..602e996 100644 --- a/monitor-lets-go.example.yaml +++ b/monitor-lets-go.example.yaml @@ -15,10 +15,12 @@ poll_interval: 5s # Whether to restore the portable layout when the daemon shuts down. restore_on_exit: true -# Optional: override where the generated Hyprland monitor config is written. -# Supports ~ for home directory expansion. -# Defaults to ~/.config/hypr/monitors.conf. -# output_path: ~/.config/hypr/custom-monitors.conf +# Backend-specific options. Keys depend on the selected backend. +# Hyprland supports: +# output_path — path to the generated monitor config file +# (default: ~/.config/hypr/monitors.conf) +# backend_config: +# output_path: ~/.config/hypr/custom-monitors.conf # External monitors that trigger docked mode. # Plain name: matches the connector name (e.g. DP-1, HDMI-A-1).