feat: add optional output_path, change default to monitors.conf
This commit is contained in:
parent
2a49aa98ed
commit
15691c0918
@ -39,7 +39,7 @@ Plugs into your dock — external monitors turn on, built-in turns off. Unplug
|
|||||||
1. **Startup** — daemon queries connected monitors, determines portable/docked, applies layout
|
1. **Startup** — daemon queries connected monitors, determines portable/docked, applies layout
|
||||||
2. **Hotplug events** — listens to compositor socket for `monitoradded`/`monitorremoved`
|
2. **Hotplug events** — listens to compositor socket for `monitoradded`/`monitorremoved`
|
||||||
3. **Debounce** — waits 1200ms after the last event (docks fire multiple events)
|
3. **Debounce** — waits 1200ms after the last event (docks fire multiple events)
|
||||||
4. **Apply** — writes `monitor-lets-go-monitors.conf`, calls `hyprctl reload` (atomic)
|
4. **Apply** — writes `monitors.conf`, calls `hyprctl reload` (atomic)
|
||||||
5. **Hooks** — runs shell commands after layout change (waybar, wallpapers, etc.)
|
5. **Hooks** — runs shell commands after layout change (waybar, wallpapers, etc.)
|
||||||
6. **Fallback polling** — every 5s checks monitor state (catches missed events)
|
6. **Fallback polling** — every 5s checks monitor state (catches missed events)
|
||||||
|
|
||||||
@ -80,7 +80,7 @@ make systemd-install # → enable & start systemd user service
|
|||||||
Add one line to `~/.config/hypr/hyprland.conf` (or `hyprland.lua`):
|
Add one line to `~/.config/hypr/hyprland.conf` (or `hyprland.lua`):
|
||||||
|
|
||||||
```
|
```
|
||||||
source = ~/.config/hypr/monitor-lets-go-monitors.conf
|
source = ~/.config/hypr/monitors.conf
|
||||||
```
|
```
|
||||||
|
|
||||||
Remove any static `monitor=...` lines — monitor-lets-go manages monitors now.
|
Remove any static `monitor=...` lines — monitor-lets-go manages monitors now.
|
||||||
@ -314,7 +314,7 @@ Hyprland is not running or `HYPRLAND_INSTANCE_SIGNATURE` is not set. Make sure m
|
|||||||
|
|
||||||
### "source file not found" on hyprctl reload
|
### "source file not found" on hyprctl reload
|
||||||
|
|
||||||
The `source = ~/.config/hypr/monitor-lets-go-monitors.conf` line must be added to hyprland.conf. The daemon creates this file on first run.
|
The `source = ~/.config/hypr/monitors.conf` line must be added to hyprland.conf. The daemon creates this file on first run.
|
||||||
|
|
||||||
### Hooks not running
|
### Hooks not running
|
||||||
|
|
||||||
|
|||||||
@ -60,7 +60,7 @@ func run(logger *slog.Logger) error {
|
|||||||
logger.Info("config loaded", "path", configPath, "backend", cfg.Backend)
|
logger.Info("config loaded", "path", configPath, "backend", cfg.Backend)
|
||||||
|
|
||||||
// Resolve the backend (auto-detect or explicit).
|
// Resolve the backend (auto-detect or explicit).
|
||||||
b, err := resolveBackend(cfg.Backend, logger)
|
b, err := resolveBackend(cfg.Backend, cfg.OutputPath, logger)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("resolve backend: %w", err)
|
return fmt.Errorf("resolve backend: %w", err)
|
||||||
}
|
}
|
||||||
@ -86,11 +86,11 @@ func run(logger *slog.Logger) error {
|
|||||||
// resolveBackend returns the first available backend. When backendName is
|
// resolveBackend returns the first available backend. When backendName is
|
||||||
// "auto", backends are probed in priority order. To add a new backend,
|
// "auto", backends are probed in priority order. To add a new backend,
|
||||||
// register it in the backends slice below.
|
// register it in the backends slice below.
|
||||||
func resolveBackend(backendName string, logger *slog.Logger) (backend.Backend, error) {
|
func resolveBackend(backendName, outputPath string, logger *slog.Logger) (backend.Backend, error) {
|
||||||
// Ordered by priority: preferred backends first.
|
// Ordered by priority: preferred backends first.
|
||||||
backends := []struct {
|
backends := []struct {
|
||||||
name string
|
name string
|
||||||
factory func(*slog.Logger) (backend.Backend, error)
|
factory func(*slog.Logger, string) (backend.Backend, error)
|
||||||
}{
|
}{
|
||||||
{"hyprland", backend.NewHyprland},
|
{"hyprland", backend.NewHyprland},
|
||||||
// Future: {"sway", backend.NewSway},
|
// Future: {"sway", backend.NewSway},
|
||||||
@ -99,7 +99,7 @@ func resolveBackend(backendName string, logger *slog.Logger) (backend.Backend, e
|
|||||||
|
|
||||||
if backendName == "auto" {
|
if backendName == "auto" {
|
||||||
for _, entry := range backends {
|
for _, entry := range backends {
|
||||||
b, err := entry.factory(logger)
|
b, err := entry.factory(logger, outputPath)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
logger.Info("auto-detected backend", "name", b.Name())
|
logger.Info("auto-detected backend", "name", b.Name())
|
||||||
return b, nil
|
return b, nil
|
||||||
@ -112,7 +112,7 @@ func resolveBackend(backendName string, logger *slog.Logger) (backend.Backend, e
|
|||||||
// Explicit backend selection.
|
// Explicit backend selection.
|
||||||
for _, entry := range backends {
|
for _, entry := range backends {
|
||||||
if entry.name == backendName {
|
if entry.name == backendName {
|
||||||
b, err := entry.factory(logger)
|
b, err := entry.factory(logger, outputPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("backend %s: %w", backendName, err)
|
return nil, fmt.Errorf("backend %s: %w", backendName, err)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -19,23 +19,30 @@ import (
|
|||||||
// hyprlandBackend implements Backend for the Hyprland compositor.
|
// hyprlandBackend implements Backend for the Hyprland compositor.
|
||||||
//
|
//
|
||||||
// Monitor queries: hyprctl -j monitors all
|
// Monitor queries: hyprctl -j monitors all
|
||||||
// Layout application: write ~/.config/hypr/monitor-lets-go-monitors.conf + hyprctl reload
|
// Layout application: write ~/.config/hypr/monitors.conf + hyprctl reload
|
||||||
// Hotplug events: socket2 unix socket ($XDG_RUNTIME_DIR/hypr/$HIS/.socket2.sock)
|
// Hotplug events: socket2 unix socket ($XDG_RUNTIME_DIR/hypr/$HIS/.socket2.sock)
|
||||||
//
|
//
|
||||||
// The user must add the following line to their hyprland.lua:
|
// The user must add the following line to their hyprland.lua:
|
||||||
//
|
//
|
||||||
// source = os.getenv("HOME") .. "/.config/hypr/monitor-lets-go-monitors.conf"
|
// source = os.getenv("HOME") .. "/.config/hypr/monitors.conf"
|
||||||
type hyprlandBackend struct {
|
type hyprlandBackend struct {
|
||||||
logger *slog.Logger
|
logger *slog.Logger
|
||||||
|
|
||||||
|
// outputPath overrides the generated monitor config file path.
|
||||||
|
// Empty means the default: ~/.config/hypr/monitors.conf.
|
||||||
|
outputPath string
|
||||||
|
|
||||||
// conn is the current socket2 connection; guarded by mu.
|
// conn is the current socket2 connection; guarded by mu.
|
||||||
conn net.Conn
|
conn net.Conn
|
||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewHyprland creates a Hyprland backend. The backend auto-detects the
|
// NewHyprland creates a Hyprland backend. The backend auto-detects the
|
||||||
// Hyprland instance signature from the environment.
|
// Hyprland instance signature from the environment.
|
||||||
func NewHyprland(logger *slog.Logger) (Backend, error) {
|
//
|
||||||
|
// 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) {
|
||||||
sig := os.Getenv("HYPRLAND_INSTANCE_SIGNATURE")
|
sig := os.Getenv("HYPRLAND_INSTANCE_SIGNATURE")
|
||||||
if sig == "" {
|
if sig == "" {
|
||||||
return nil, errors.New("HYPRLAND_INSTANCE_SIGNATURE is not set; is Hyprland running?")
|
return nil, errors.New("HYPRLAND_INSTANCE_SIGNATURE is not set; is Hyprland running?")
|
||||||
@ -50,7 +57,7 @@ func NewHyprland(logger *slog.Logger) (Backend, error) {
|
|||||||
return nil, fmt.Errorf("socket2 not found at %s: %w", socketPath, err)
|
return nil, fmt.Errorf("socket2 not found at %s: %w", socketPath, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return &hyprlandBackend{logger: logger}, nil
|
return &hyprlandBackend{logger: logger, outputPath: outputPath}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *hyprlandBackend) Name() string { return "hyprland" }
|
func (h *hyprlandBackend) Name() string { return "hyprland" }
|
||||||
@ -77,11 +84,10 @@ func (h *hyprlandBackend) GetMonitors(ctx context.Context) ([]MonitorInfo, error
|
|||||||
func (h *hyprlandBackend) ApplyLayout(ctx context.Context, monitors []MonitorConfig) error {
|
func (h *hyprlandBackend) ApplyLayout(ctx context.Context, monitors []MonitorConfig) error {
|
||||||
content := h.generateConf(monitors)
|
content := h.generateConf(monitors)
|
||||||
|
|
||||||
configDir, err := h.hyprConfigDir()
|
destPath, err := h.resolveOutputPath()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("hypr config dir: %w", err)
|
return fmt.Errorf("resolve output path: %w", err)
|
||||||
}
|
}
|
||||||
destPath := filepath.Join(configDir, "monitor-lets-go-monitors.conf")
|
|
||||||
|
|
||||||
// Atomic write: temp file, write, fsync, rename.
|
// Atomic write: temp file, write, fsync, rename.
|
||||||
if err := atomicWrite(destPath, []byte(content)); err != nil {
|
if err := atomicWrite(destPath, []byte(content)); err != nil {
|
||||||
@ -285,6 +291,32 @@ func (h *hyprlandBackend) hyprConfigDir() (string, error) {
|
|||||||
return dir, nil
|
return dir, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// resolveOutputPath returns the path for the generated monitor config file.
|
||||||
|
// When outputPath is set (via constructor), it is used after ~ expansion.
|
||||||
|
// Otherwise the default ~/.config/hypr/monitors.conf is returned.
|
||||||
|
func (h *hyprlandBackend) resolveOutputPath() (string, error) {
|
||||||
|
if h.outputPath != "" {
|
||||||
|
p := h.outputPath
|
||||||
|
if strings.HasPrefix(p, "~") {
|
||||||
|
home, err := os.UserHomeDir()
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("resolve home directory: %w", err)
|
||||||
|
}
|
||||||
|
p = filepath.Join(home, p[1:])
|
||||||
|
}
|
||||||
|
if err := os.MkdirAll(filepath.Dir(p), 0o755); err != nil {
|
||||||
|
return "", fmt.Errorf("create output directory: %w", err)
|
||||||
|
}
|
||||||
|
return p, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
configDir, err := h.hyprConfigDir()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return filepath.Join(configDir, "monitors.conf"), nil
|
||||||
|
}
|
||||||
|
|
||||||
func (h *hyprlandBackend) Close() error {
|
func (h *hyprlandBackend) Close() error {
|
||||||
h.mu.Lock()
|
h.mu.Lock()
|
||||||
defer h.mu.Unlock()
|
defer h.mu.Unlock()
|
||||||
|
|||||||
@ -50,6 +50,11 @@ type Config struct {
|
|||||||
// Uses *bool so we can distinguish "not set" from explicit false.
|
// Uses *bool so we can distinguish "not set" from explicit false.
|
||||||
RestoreOnExit *bool `yaml:"restore_on_exit"`
|
RestoreOnExit *bool `yaml:"restore_on_exit"`
|
||||||
|
|
||||||
|
// OutputPath overrides the generated monitor config file path.
|
||||||
|
// Supports ~ for home directory expansion.
|
||||||
|
// Empty means the default: ~/.config/hypr/monitors.conf.
|
||||||
|
OutputPath string `yaml:"output_path"`
|
||||||
|
|
||||||
// External lists monitor identifiers that trigger docked mode.
|
// External lists monitor identifiers that trigger docked mode.
|
||||||
// Each entry is a name (DP-1) or a desc: prefix (desc:Dell U2723QE).
|
// Each entry is a name (DP-1) or a desc: prefix (desc:Dell U2723QE).
|
||||||
External []string `yaml:"external"`
|
External []string `yaml:"external"`
|
||||||
|
|||||||
@ -15,6 +15,11 @@ poll_interval: 5s
|
|||||||
# Whether to restore the portable layout when the daemon shuts down.
|
# Whether to restore the portable layout when the daemon shuts down.
|
||||||
restore_on_exit: true
|
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
|
||||||
|
|
||||||
# External monitors that trigger docked mode.
|
# External monitors that trigger docked mode.
|
||||||
# Plain name: matches the connector name (e.g. DP-1, HDMI-A-1).
|
# Plain name: matches the connector name (e.g. DP-1, HDMI-A-1).
|
||||||
# desc: prefix: matches by monitor description (survives rename).
|
# desc: prefix: matches by monitor description (survives rename).
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user