// Package modernzconf parses, edits, and writes ModernZ's modernz.conf // (mpv script-opts) while preserving comments and untouched lines. The // parse/edit/atomic-write machinery is shared with other script-opts confs // in internal/scriptopts; this package holds the ModernZ option catalog, // file naming, and managed install defaults. package modernzconf import ( "fmt" "os" "gitgud.io/mike/mpv-manager/internal/fileops" "gitgud.io/mike/mpv-manager/internal/scriptopts" ) // confFileName is the ModernZ script-opts file name. const confFileName = "modernz.conf" // ModernZConf holds the parsed representation of a modernz.conf file. type ModernZConf struct { file *scriptopts.File } // GetModernZConfPath returns the platform-aware path to the user's // modernz.conf, in the script-opts directory next to mpv.conf. func GetModernZConfPath() (string, error) { return scriptopts.ConfPath(confFileName) } // ParseModernZConf reads and parses the modernz.conf at path. // A missing file is not an error: the returned conf has no explicit entries // and GetValue falls back to the KnownOptions defaults. func ParseModernZConf(path string) (*ModernZConf, error) { file, err := scriptopts.Parse(spec, path) if err != nil { return nil, err } return &ModernZConf{file: file}, nil } // EditValueContent prepares a validated single-option edit of a captured // snapshot. It performs no writes, allowing durable transactions to record // the expected result before publishing the changed file. func EditValueContent(content []byte, key, value string) ([]byte, error) { file := scriptopts.ParseContent(spec, "", content) if err := file.SetValue(key, value); err != nil { return nil, err } return []byte(file.Serialize()), nil } // GetValue returns the explicitly set value for key, or the KnownOptions // default when the user has no explicit line. Unknown keys return "". func (c *ModernZConf) GetValue(key string) string { return c.file.GetValue(key) } // IsSet reports whether the user has an explicit line for key. func (c *ModernZConf) IsSet(key string) bool { return c.file.IsSet(key) } // SetValue sets key to value after validating it against KnownOptions. // An existing line is updated in place (preserving position and inline // comment); a new key is appended at the end of the file. func (c *ModernZConf) SetValue(key, value string) error { return c.file.SetValue(key, value) } // ResetValue removes the explicit line(s) for key, reverting it to the // KnownOptions default. Unknown keys are rejected. func (c *ModernZConf) ResetValue(key string) error { return c.file.ResetValue(key) } // Write writes the conf back to disk using an atomic write pattern. // A timestamped backup of any existing file is created before overwriting. func (c *ModernZConf) Write() error { return c.file.Write() } // SetValueFile applies one option to a fresh file snapshot while holding the // path lock through backup and atomic replacement. func SetValueFile(path, key, value string) error { return scriptopts.SetValueFile(spec, path, key, value) } // SetValueFileLocked applies one option while the caller holds the shared // fileops lock for path. It supports transactions that span modernz.conf and // the manager configuration without recursively acquiring the same lock. func SetValueFileLocked(path, key, value string) error { return scriptopts.SetValueFileLocked(spec, path, key, value) } // ResetValueFile removes one explicit option from a fresh file snapshot while // holding the path lock through backup and atomic replacement. func ResetValueFile(path, key string) error { return scriptopts.ResetValueFile(spec, path, key) } // ManagedScalingValues reads the effective scaling values from an existing // modernz.conf. A missing file returns nil so a new install receives the // manager defaults; an existing file returns effective values even when a key // is implicit, preserving the user's prior behavior across ModernZ updates. func ManagedScalingValues(path string) (map[string]string, error) { if _, err := os.Stat(path); err != nil { if os.IsNotExist(err) { return nil, nil } return nil, fmt.Errorf("stat modernz.conf: %w", err) } conf, err := ParseModernZConf(path) if err != nil { return nil, err } values := make(map[string]string, len(managedInstallDefaults)) for _, setting := range managedInstallDefaults { value := conf.GetValue(setting.key) if err := ValidateValue(setting.key, value); err != nil { return nil, fmt.Errorf("validate existing ModernZ setting %q: %w", setting.key, err) } values[setting.key] = value } return values, nil } // ApplyManagedInstallDefaults updates a verified, staged upstream modernz.conf // with the defaults chosen for new mpv-manager installations, followed by any // scaling values preserved from an existing install. It intentionally does not // create a backup: callers must use it before the staged file replaces the live // user configuration. func ApplyManagedInstallDefaults(path string, preservedScaling map[string]string) error { if path == "" { return fmt.Errorf("modernz.conf path is empty") } for key, value := range preservedScaling { if err := ValidateValue(key, value); err != nil { return fmt.Errorf("validate preserved ModernZ setting %q: %w", key, err) } } return fileops.WithLock(path, func() error { conf, err := ParseModernZConf(path) if err != nil { return err } for _, setting := range managedInstallDefaults { if err := conf.SetValue(setting.key, setting.value); err != nil { return fmt.Errorf("set managed ModernZ default %q: %w", setting.key, err) } } // Follow the stable known-option order so output is deterministic even // if an upstream config omits one of the preserved keys. for _, opt := range knownOptions { value, ok := preservedScaling[opt.Key] if !ok { continue } if err := conf.SetValue(opt.Key, value); err != nil { return fmt.Errorf("restore ModernZ setting %q: %w", opt.Key, err) } } return scriptopts.AtomicWrite(path, []byte(conf.file.Serialize())) }) }