1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
|
// Package config handles loading and storing foostore configuration.
// Defaults mirror the Ruby reference (geheim.rb Config::DEFAULTS).
// A JSON file at ~/.config/foostore.json overrides individual fields;
// missing fields keep their default values because Go's json.Unmarshal
// only touches fields that are present in the JSON document.
package config
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"strings"
)
const (
// configPath is the location of the optional user config file.
configPath = "~/.config/foostore.json"
// fallbackHomeDirName is used when we cannot resolve a valid home directory.
fallbackHomeDirName = "foostore-home"
)
// Config holds all application-wide configuration values.
// JSON field names use snake_case to match the original geheim.rb Config::DEFAULTS keys.
type Config struct {
DataDir string `json:"data_dir"`
ExportDir string `json:"export_dir"`
KeyFile string `json:"key_file"`
KeyLength int `json:"key_length"`
EncAlg string `json:"enc_alg"`
AddToIV string `json:"add_to_iv"`
EditCmd string `json:"edit_cmd"`
GnomeClipboardCmd string `json:"gnome_clipboard_cmd"`
MacOSClipboardCmd string `json:"macos_clipboard_cmd"`
SyncRepos []string `json:"sync_repos"`
}
// resolveHomeDir resolves the current user's home directory from OS state.
// If resolution succeeds with os.UserHomeDir, error is nil.
// If resolution falls back to HOME or a temp-based directory, an explanatory
// non-nil error is returned so callers can warn without failing hard.
func resolveHomeDir() (string, error) {
return resolveHomeDirFrom(os.UserHomeDir, os.Getenv("HOME"), os.TempDir())
}
// resolveHomeDirFrom is a test seam for home directory resolution.
func resolveHomeDirFrom(userHomeDir func() (string, error), envHome, tempDir string) (string, error) {
home, err := userHomeDir()
if err == nil && home != "" {
return home, nil
}
if envHome != "" && filepath.IsAbs(envHome) {
if err != nil {
return envHome, fmt.Errorf("os.UserHomeDir failed; using HOME=%q: %w", envHome, err)
}
return envHome, fmt.Errorf("os.UserHomeDir returned empty home; using HOME=%q", envHome)
}
fallbackHome := filepath.Join(tempDir, fallbackHomeDirName)
if envHome != "" && !filepath.IsAbs(envHome) {
if err != nil {
return fallbackHome, fmt.Errorf("os.UserHomeDir failed; HOME is not absolute (%q), using %q: %w", envHome, fallbackHome, err)
}
return fallbackHome, fmt.Errorf("os.UserHomeDir returned empty home; HOME is not absolute (%q), using %q", envHome, fallbackHome)
}
if err != nil {
return fallbackHome, fmt.Errorf("os.UserHomeDir failed and HOME is unavailable; using %q: %w", fallbackHome, err)
}
return fallbackHome, fmt.Errorf("os.UserHomeDir returned empty home and HOME is unavailable; using %q", fallbackHome)
}
// homeDirOrFallback resolves a usable home path and logs fallback reasons.
func homeDirOrFallback() string {
home, err := resolveHomeDir()
if err != nil {
fmt.Fprintf(os.Stderr, "Warning: %v\n", err)
}
return home
}
// defaultConfigWithHome returns built-in defaults using the supplied home path.
func defaultConfigWithHome(home string) Config {
// Prefer $EDITOR; fall back to vi if not set.
editCmd := os.Getenv("EDITOR")
if editCmd == "" {
editCmd = "vi"
}
return Config{
DataDir: filepath.Join(home, "git", "foostoredb"),
ExportDir: filepath.Join(home, ".foostore-export"),
KeyFile: filepath.Join(home, ".foostore.key"),
KeyLength: 32,
EncAlg: "AES-256-CBC",
AddToIV: "Hello world",
EditCmd: editCmd,
GnomeClipboardCmd: "gpaste-client",
MacOSClipboardCmd: "pbcopy",
SyncRepos: []string{"git1", "git2"},
}
}
// defaultConfig returns a Config populated with built-in defaults.
// EditCmd honours the $EDITOR environment variable and falls back to "vi"
// when the variable is unset or empty, so users get their preferred editor
// automatically without touching the config file.
func defaultConfig() Config {
return defaultConfigWithHome(homeDirOrFallback())
}
// expandTildeWithHome replaces a leading "~" in path with the supplied home.
func expandTildeWithHome(path, home string) string {
if path == "" || !strings.HasPrefix(path, "~") {
return path
}
// Replace only the leading "~"; preserve any subdirectory suffix.
return home + path[1:]
}
// expandTilde replaces a leading "~" in path with the user's home directory.
// Non-tilde paths and empty strings are returned unchanged.
func expandTilde(path string) string {
return expandTildeWithHome(path, homeDirOrFallback())
}
// expandPathFieldsWithHome tilde-expands every path-typed field in cfg in place.
func expandPathFieldsWithHome(cfg *Config, home string) {
cfg.DataDir = expandTildeWithHome(cfg.DataDir, home)
cfg.ExportDir = expandTildeWithHome(cfg.ExportDir, home)
cfg.KeyFile = expandTildeWithHome(cfg.KeyFile, home)
}
// expandPathFields tilde-expands every path-typed field in cfg in place.
func expandPathFields(cfg *Config) {
expandPathFieldsWithHome(cfg, homeDirOrFallback())
}
// Load reads ~/.config/foostore.json and merges it over the built-in defaults.
// Any field present in the JSON file overrides the corresponding default
// (including edit_cmd, which defaults to $EDITOR or "vi" when unset);
// fields absent from the file keep their default values.
// If the file is missing or contains invalid JSON a warning is printed to
// stderr and the pure defaults are returned.
// Note: the Ruby reference uses puts (stdout) for this warning; we use stderr
// intentionally because warnings belong on the error stream.
func Load() Config {
home := homeDirOrFallback()
cfg := defaultConfigWithHome(home)
path := expandTildeWithHome(configPath, home)
data, err := os.ReadFile(path)
if err != nil {
// File missing or unreadable — use defaults silently only when the
// error is "not found"; otherwise warn the caller.
if !os.IsNotExist(err) {
fmt.Fprintf(os.Stderr, "Unable to read %s, using defaults! %v\n", path, err)
}
return cfg
}
// Unmarshal into the defaults struct so that only fields present in the
// JSON document are overwritten; all others retain their default values.
if err := json.Unmarshal(data, &cfg); err != nil {
fmt.Fprintf(os.Stderr, "Unable to read %s, using defaults! %v\n", path, err)
return defaultConfigWithHome(home)
}
// Tilde-expand path fields that may have been supplied as "~/…" strings
// in the JSON file (defaultConfig() already returns absolute paths, but
// user-supplied values might use "~").
expandPathFieldsWithHome(&cfg, home)
return cfg
}
|