PrintToVeraeTimeStamp/internal/settings/settings.go

128 lines
3.6 KiB
Go

// Package settings holds user preferences for the print helper.
//
// Lookup order (first hit wins per field):
// 1. env VERAE_OPEN_ON_COMPLETE
// 2. user file ~/.verae/config.json (written by the installers for the console/logged-in user)
// 3. machine file %ProgramData%\Verae\PrintStamp\config.json (Windows)
// or /Library/Application Support/Verae/PrintStamp/config.json (macOS)
// 4. defaults (OpenOnComplete = "folder", LogLevel = info)
package settings
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"runtime"
"strings"
)
// OpenOnComplete values.
const (
OpenFolder = "folder" // reveal the certificate PDF in Explorer/Finder
OpenPDF = "pdf" // open the certificate PDF in the default viewer
OpenNone = "none"
)
// Settings is the on-disk JSON shape.
type Settings struct {
OpenOnComplete string `json:"openOnComplete,omitempty"`
// LogLevel "debug" enables verbose helper logs (same as VERAE_LOG_LEVEL=debug).
LogLevel string `json:"logLevel,omitempty"`
}
// Default settings.
var Default = Settings{OpenOnComplete: OpenFolder}
// NormalizeOpen maps user input to a valid value ("" if invalid).
func NormalizeOpen(v string) string {
switch strings.ToLower(strings.TrimSpace(v)) {
case "folder", "reveal", "finder", "explorer", "show":
return OpenFolder
case "pdf", "open", "file":
return OpenPDF
case "none", "off", "no", "false", "0":
return OpenNone
}
return ""
}
// UserPath is ~/.verae/config.json for the given home ("" = current user).
func UserPath(home string) string {
if home == "" {
home, _ = os.UserHomeDir()
}
return filepath.Join(home, ".verae", "config.json")
}
// MachinePath is the all-users default file.
func MachinePath() string {
if runtime.GOOS == "windows" {
pd := os.Getenv("ProgramData")
if pd == "" {
pd = `C:\ProgramData`
}
return filepath.Join(pd, "Verae", "PrintStamp", "config.json")
}
return "/Library/Application Support/Verae/PrintStamp/config.json"
}
func readFile(p string) (Settings, bool) {
var s Settings
b, err := os.ReadFile(p)
if err != nil {
return s, false
}
b = []byte(strings.TrimPrefix(string(b), "\uFEFF"))
if json.Unmarshal(b, &s) != nil {
return Settings{}, false
}
return s, true
}
// Load resolves the effective settings and a short description of where each came from.
func Load() (Settings, string) {
eff := Default
src := "default"
if s, ok := readFile(MachinePath()); ok {
if v := NormalizeOpen(s.OpenOnComplete); v != "" {
eff.OpenOnComplete, src = v, MachinePath()
}
}
if s, ok := readFile(UserPath("")); ok {
if v := NormalizeOpen(s.OpenOnComplete); v != "" {
eff.OpenOnComplete, src = v, UserPath("")
}
}
for _, p := range []string{MachinePath(), UserPath("")} {
if s, ok := readFile(p); ok && s.LogLevel != "" {
eff.LogLevel = strings.ToLower(strings.TrimSpace(s.LogLevel))
}
}
if v := NormalizeOpen(os.Getenv("VERAE_OPEN_ON_COMPLETE")); v != "" {
eff.OpenOnComplete, src = v, "env VERAE_OPEN_ON_COMPLETE"
}
return eff, src
}
// SetOpenOnComplete updates (merges) the JSON file at path, preserving unknown keys.
func SetOpenOnComplete(path, value string) error {
v := NormalizeOpen(value)
if v == "" {
return fmt.Errorf("openOnComplete must be folder, pdf or none (got %q)", value)
}
m := map[string]any{}
if b, err := os.ReadFile(path); err == nil {
_ = json.Unmarshal([]byte(strings.TrimPrefix(string(b), "\uFEFF")), &m)
}
m["openOnComplete"] = v
if err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil {
return err
}
b, _ := json.MarshalIndent(m, "", " ")
tmp := path + ".tmp"
if err := os.WriteFile(tmp, append(b, '\n'), 0o600); err != nil {
return err
}
return os.Rename(tmp, path)
}