feat: restore portable layout on shutdown and before suspend
Add WriteConfig/Prepare backend methods (real for Hyprland, no-op for Sway), -apply/-prepare/-no-reload CLI flags, ExecStartPre cleanup and suspend systemd hooks so a stale docked config can never leave the built-in display disabled after resume.
This commit is contained in:
@@ -133,6 +133,88 @@ func (h *hyprlandBackend) ApplyLayout(ctx context.Context, monitors []MonitorCon
|
||||
return nil
|
||||
}
|
||||
|
||||
// WriteConfig writes the monitor configuration to disk without calling
|
||||
// hyprctl reload. This is used during shutdown when the compositor may
|
||||
// not be available. Resolves monitor names with a short timeout; falls
|
||||
// back to plain-name-only entries if resolution fails.
|
||||
func (h *hyprlandBackend) WriteConfig(ctx context.Context, monitors []MonitorConfig) error {
|
||||
resolveCtx, cancel := context.WithTimeout(ctx, 3*time.Second)
|
||||
defer cancel()
|
||||
|
||||
resolved, err := resolveMonitorNames(resolveCtx, monitors, h.GetMonitors, h.logger)
|
||||
if err != nil {
|
||||
h.logger.Warn("cannot resolve monitor names for WriteConfig — using plain names only",
|
||||
"error", err)
|
||||
var plain []MonitorConfig
|
||||
for _, m := range monitors {
|
||||
if !strings.HasPrefix(m.Name, "desc:") && !strings.HasPrefix(m.Name, "size:") {
|
||||
plain = append(plain, m)
|
||||
}
|
||||
}
|
||||
if len(plain) == 0 {
|
||||
plain = []MonitorConfig{
|
||||
{Name: "eDP-1", Enabled: true, Mode: "preferred", Position: "auto", Scale: 1.0},
|
||||
}
|
||||
}
|
||||
resolved = plain
|
||||
}
|
||||
|
||||
content := h.generateConf(resolved)
|
||||
destPath, err := h.resolveOutputPath()
|
||||
if err != nil {
|
||||
return fmt.Errorf("resolve output path: %w", err)
|
||||
}
|
||||
if err := atomicWrite(destPath, []byte(content)); err != nil {
|
||||
return fmt.Errorf("write monitor config: %w", err)
|
||||
}
|
||||
h.logger.Info("config written to disk", "path", destPath, "monitors", len(resolved))
|
||||
return nil
|
||||
}
|
||||
|
||||
// Prepare removes all monitor=...,disabled lines from the monitors.conf
|
||||
// file. This prevents the "no active displays" issue when Hyprland starts
|
||||
// with a stale config. If the resulting file would be empty (or doesn't
|
||||
// exist), a generic built-in entry is written as a fallback.
|
||||
func (h *hyprlandBackend) Prepare(ctx context.Context) error {
|
||||
destPath, err := h.resolveOutputPath()
|
||||
if err != nil {
|
||||
return fmt.Errorf("resolve output path: %w", err)
|
||||
}
|
||||
|
||||
data, err := os.ReadFile(destPath)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
h.logger.Debug("prepare: no config file to clean")
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("read %s: %w", destPath, err)
|
||||
}
|
||||
|
||||
lines := strings.Split(string(data), "\n")
|
||||
var cleaned []string
|
||||
for _, line := range lines {
|
||||
trimmed := strings.TrimSpace(line)
|
||||
if trimmed == "" {
|
||||
continue
|
||||
}
|
||||
if strings.HasPrefix(trimmed, "monitor=") && strings.HasSuffix(trimmed, ",disabled") {
|
||||
continue
|
||||
}
|
||||
cleaned = append(cleaned, line)
|
||||
}
|
||||
|
||||
content := strings.Join(cleaned, "\n")
|
||||
if strings.TrimSpace(content) == "" {
|
||||
content = "monitor=eDP-1,preferred,auto,1\n"
|
||||
}
|
||||
|
||||
if err := atomicWrite(destPath, []byte(content)); err != nil {
|
||||
return fmt.Errorf("write cleaned config: %w", err)
|
||||
}
|
||||
h.logger.Info("prepare: cleaned disabled entries from config", "path", destPath)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Events connects to the socket2 unix socket and emits parsed hotplug events.
|
||||
// On connection loss it attempts reconnection with exponential backoff.
|
||||
func (h *hyprlandBackend) Events(ctx context.Context) (<-chan Event, <-chan error) {
|
||||
|
||||
Reference in New Issue
Block a user