// Copyright (C) 2026 Danilo M. // // This program is free software; you can redistribute it and/or modify // it under the terms of the GNU General Public License version 2 as // published by the Free Software Foundation. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. package notify import ( "encoding/json" "errors" "log" "os" "path/filepath" ) // Timeouts are the balloon lifetimes in seconds per urgency, used only when a // client leaves the choice to the server. 0 means never expires. type Timeouts struct { Low int `json:"low"` Normal int `json:"normal"` Critical int `json:"critical"` } // Config is the daemon's share of ~/.config/notifyd/config.json. The file // also carries the balloon shell's keys (placement, DND, mute); they are not // declared here and the decoder ignores them. type Config struct { Timeouts Timeouts `json:"timeouts"` LiveCap int `json:"liveCap"` HistoryCap int `json:"historyCap"` } // DefaultConfig is the behaviour before the file existed. func DefaultConfig() Config { return Config{Timeouts: Timeouts{Low: 10, Normal: 10, Critical: 0}, LiveCap: 20, HistoryCap: 20} } // ConfigPath is $XDG_CONFIG_HOME/notifyd/config.json, ~/.config when unset. func ConfigPath() string { dir, err := os.UserConfigDir() if err != nil { dir = filepath.Join(homeDir(), ".config") } return filepath.Join(dir, "notifyd", "config.json") } // LoadConfig never fails: a hand typo must not stop the daemon. A missing // file or broken JSON is the defaults; a key of the wrong type keeps its // default while the rest of the file applies, which is what encoding/json // does on an UnmarshalTypeError. Out-of-range values clamp. func LoadConfig(path string) Config { def := DefaultConfig() data, err := os.ReadFile(path) if err != nil { if !errors.Is(err, os.ErrNotExist) { log.Printf("notifyd: config: %v", err) } return def } c := def if err := json.Unmarshal(data, &c); err != nil { var typeErr *json.UnmarshalTypeError if !errors.As(err, &typeErr) { log.Printf("notifyd: config %s: %v, using defaults", path, err) return def } log.Printf("notifyd: config %s: %v", path, err) } c.LiveCap = clamp(c.LiveCap, 1, 100) c.HistoryCap = clamp(c.HistoryCap, 1, 100) if c.Timeouts.Low < 0 { c.Timeouts.Low = def.Timeouts.Low } if c.Timeouts.Normal < 0 { c.Timeouts.Normal = def.Timeouts.Normal } if c.Timeouts.Critical < 0 { c.Timeouts.Critical = def.Timeouts.Critical } return c } func clamp(v, lo, hi int) int { return min(max(v, lo), hi) }