- promptForFilename setting (default true; env VERAE_PROMPT_FOR_FILENAME), 'verae-print config --prompt-for-filename', last Save As folder in ~/.verae/state.json - Windows helper: common file dialog (PDF filter, overwrite prompt, topmost owner) before stamping; macOS printer path: choose file name (NSSavePanel) via 'stamp -prompt-name -title'; cancel discards the job (no stamp/credit) - job.Options.OutPath: original saved at the chosen path, certificate and .verae.json next to it with the chosen base name (never overwritten) - held jobs keep the chosen path (pendingMeta.OutPath) - installer: 'Ask for a file name for each print' checkbox; removed the 'Open the user guide' finish-page checkbox; mac-key-setup asks too - auth test no longer touches the real macOS keychain (VERAE_SKIP_KEYCHAIN) - version 0.3.2
190 lines
5.5 KiB
Go
190 lines
5.5 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"`
|
|
// PromptForFilename shows a Save As dialog for every print (default true).
|
|
PromptForFilename *bool `json:"promptForFilename,omitempty"`
|
|
}
|
|
|
|
// Prompt reports the effective promptForFilename value.
|
|
func (s Settings) Prompt() bool { return s.PromptForFilename == nil || *s.PromptForFilename }
|
|
|
|
func parseBool(v string) (bool, bool) {
|
|
switch strings.ToLower(strings.TrimSpace(v)) {
|
|
case "1", "true", "yes", "on":
|
|
return true, true
|
|
case "0", "false", "no", "off":
|
|
return false, true
|
|
}
|
|
return false, false
|
|
}
|
|
|
|
// 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 {
|
|
if s.LogLevel != "" {
|
|
eff.LogLevel = strings.ToLower(strings.TrimSpace(s.LogLevel))
|
|
}
|
|
if s.PromptForFilename != nil {
|
|
v := *s.PromptForFilename
|
|
eff.PromptForFilename = &v
|
|
}
|
|
}
|
|
}
|
|
if b, ok := parseBool(os.Getenv("VERAE_PROMPT_FOR_FILENAME")); ok {
|
|
eff.PromptForFilename = &b
|
|
}
|
|
if v := NormalizeOpen(os.Getenv("VERAE_OPEN_ON_COMPLETE")); v != "" {
|
|
eff.OpenOnComplete, src = v, "env VERAE_OPEN_ON_COMPLETE"
|
|
}
|
|
return eff, src
|
|
}
|
|
|
|
// SetKey merges one key into the JSON file at path (unknown keys preserved).
|
|
func SetKey(path, key string, value any) error {
|
|
m := map[string]any{}
|
|
if b, err := os.ReadFile(path); err == nil {
|
|
_ = json.Unmarshal([]byte(strings.TrimPrefix(string(b), "\uFEFF")), &m)
|
|
}
|
|
m[key] = value
|
|
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)
|
|
}
|
|
|
|
// SetPromptForFilename writes promptForFilename (true/false, yes/no, 1/0).
|
|
func SetPromptForFilename(path, value string) error {
|
|
b, ok := parseBool(value)
|
|
if !ok {
|
|
return fmt.Errorf("promptForFilename must be true or false (got %q)", value)
|
|
}
|
|
return SetKey(path, "promptForFilename", b)
|
|
}
|
|
|
|
// statePath holds non-preference state (last Save As folder).
|
|
func statePath() string { return filepath.Join(filepath.Dir(UserPath("")), "state.json") }
|
|
|
|
// LastSaveDir returns the folder used last in the Save As dialog ("" if none/gone).
|
|
func LastSaveDir() string {
|
|
b, err := os.ReadFile(statePath())
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
var st struct {
|
|
LastSaveDir string `json:"lastSaveDir"`
|
|
}
|
|
if json.Unmarshal(b, &st) != nil || st.LastSaveDir == "" {
|
|
return ""
|
|
}
|
|
if fi, err := os.Stat(st.LastSaveDir); err != nil || !fi.IsDir() {
|
|
return ""
|
|
}
|
|
return st.LastSaveDir
|
|
}
|
|
|
|
// SetLastSaveDir remembers the folder chosen in the Save As dialog.
|
|
func SetLastSaveDir(dir string) error { return SetKey(statePath(), "lastSaveDir", dir) }
|
|
|
|
// 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)
|
|
}
|
|
return SetKey(path, "openOnComplete", v)
|
|
}
|