summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-02-22 20:09:33 +0200
committerPaul Buetow <paul@buetow.org>2026-02-22 20:09:33 +0200
commit528a9c0888f1db2449d30bf9c0c8b55fcd3c645c (patch)
treed8891ac02c1e0d873c0caff3ebe29b4ea2a916af
parentd629558394465b8956285edac324d67688ddd2c1 (diff)
Use \$EDITOR as default editor, falling back to vi (task 344)
defaultConfig() now reads the \$EDITOR environment variable for EditCmd so users get their preferred editor automatically without touching the config file. Falls back to "vi" (universally available) when \$EDITOR is unset or empty. A JSON config value still takes precedence over \$EDITOR. Updated tests to: unset \$EDITOR where "vi" fallback is asserted, add TestLoad_editorEnvVar covering the three cases (\$EDITOR unset, \$EDITOR set, JSON override), and remove all stale "hx" references from comments. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
-rw-r--r--CLAUDE.md2
-rw-r--r--internal/config/config.go22
-rw-r--r--internal/config/config_test.go39
3 files changed, 50 insertions, 13 deletions
diff --git a/CLAUDE.md b/CLAUDE.md
index 4e5a5c8..7e62888 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -41,7 +41,7 @@ Config is read from `~/.config/foostore.json` at startup (merged over defaults).
- `data_dir`: Git repo where encrypted `.index` / `.data` file pairs are stored (default: `~/git/geheimlager`)
- `key_file`: Path to the raw encryption key file (default: `~/.geheimlager.key`)
- `export_dir`: Temporary directory for decrypted exports (default: `~/.geheimlagerexport`)
-- `edit_cmd`: Editor used by the `edit` command (default: `hx`)
+- `edit_cmd`: Editor used by the `edit` command (default: `$EDITOR`, falling back to `vi`)
- `sync_repos`: List of git remote names to push/pull when syncing
The PIN (entered at startup or via `$PIN` env var) is used to derive the AES IV; the actual key comes from `key_file`.
diff --git a/internal/config/config.go b/internal/config/config.go
index 6354d83..04c8302 100644
--- a/internal/config/config.go
+++ b/internal/config/config.go
@@ -31,11 +31,20 @@ type Config struct {
SyncRepos []string `json:"sync_repos"`
}
-// defaultConfig returns a Config populated with the same defaults as the
-// Ruby reference implementation's Config::DEFAULTS. It calls
-// os.UserHomeDir() so that path fields expand correctly at runtime.
+// 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.
+// It calls os.UserHomeDir() so that path fields expand correctly at runtime.
func defaultConfig() Config {
home, _ := os.UserHomeDir()
+
+ // Prefer $EDITOR; fall back to vi if not set.
+ editCmd := os.Getenv("EDITOR")
+ if editCmd == "" {
+ editCmd = "vi"
+ }
+
return Config{
DataDir: filepath.Join(home, "git", "geheimlager"),
ExportDir: filepath.Join(home, ".geheimlagerexport"),
@@ -43,7 +52,7 @@ func defaultConfig() Config {
KeyLength: 32,
EncAlg: "AES-256-CBC",
AddToIV: "Hello world",
- EditCmd: "hx",
+ EditCmd: editCmd,
GnomeClipboardCmd: "gpaste-client",
MacOSClipboardCmd: "pbcopy",
SyncRepos: []string{"git1", "git2"},
@@ -68,8 +77,9 @@ func expandPathFields(cfg *Config) {
cfg.KeyFile = expandTilde(cfg.KeyFile)
}
-// Load reads ~/.config/geheim.json and merges it over the built-in defaults.
-// Any field present in the JSON file overrides the corresponding default;
+// 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.
diff --git a/internal/config/config_test.go b/internal/config/config_test.go
index 3f8ace9..715a09e 100644
--- a/internal/config/config_test.go
+++ b/internal/config/config_test.go
@@ -69,9 +69,11 @@ func TestExpandTilde(t *testing.T) {
// TestLoad_defaults verifies all 10 default values when no config file exists.
// HOME is redirected to a temp dir so Load() looks for a file that will not exist.
+// EDITOR is unset so EditCmd falls back to "vi".
func TestLoad_defaults(t *testing.T) {
dir := t.TempDir()
t.Setenv("HOME", dir)
+ t.Setenv("EDITOR", "") // ensure fallback to "vi"
cfg := Load()
@@ -81,7 +83,7 @@ func TestLoad_defaults(t *testing.T) {
{"KeyFile", cfg.KeyFile, filepath.Join(dir, ".geheimlager.key")},
{"EncAlg", cfg.EncAlg, "AES-256-CBC"},
{"AddToIV", cfg.AddToIV, "Hello world"},
- {"EditCmd", cfg.EditCmd, "hx"},
+ {"EditCmd", cfg.EditCmd, "vi"},
{"GnomeClipboardCmd", cfg.GnomeClipboardCmd, "gpaste-client"},
{"MacOSClipboardCmd", cfg.MacOSClipboardCmd, "pbcopy"},
}
@@ -98,6 +100,27 @@ func TestLoad_defaults(t *testing.T) {
}
}
+// TestLoad_editorEnvVar verifies that when $EDITOR is set, defaultConfig uses it
+// as the EditCmd, and that a JSON config value overrides $EDITOR.
+func TestLoad_editorEnvVar(t *testing.T) {
+ dir := t.TempDir()
+ t.Setenv("HOME", dir)
+
+ // $EDITOR set, no config file — EditCmd must equal $EDITOR.
+ t.Setenv("EDITOR", "nano")
+ cfg := Load()
+ if cfg.EditCmd != "nano" {
+ t.Errorf("EditCmd = %q; want nano (from $EDITOR)", cfg.EditCmd)
+ }
+
+ // JSON config overrides $EDITOR.
+ writeUserConfig(t, dir, `{"edit_cmd":"vim"}`)
+ cfg = Load()
+ if cfg.EditCmd != "vim" {
+ t.Errorf("EditCmd = %q; want vim (from config file)", cfg.EditCmd)
+ }
+}
+
// TestLoad_override calls Load() directly (via a redirected HOME) and verifies
// that JSON-supplied fields override defaults while absent fields keep defaults.
func TestLoad_override(t *testing.T) {
@@ -143,10 +166,12 @@ func TestLoad_pathOverride(t *testing.T) {
}
// TestLoad_invalid_json verifies that invalid JSON causes Load() to emit a
-// warning to stderr and return defaults.
+// warning to stderr and return defaults (including EditCmd = "vi" when EDITOR
+// is unset).
func TestLoad_invalid_json(t *testing.T) {
dir := t.TempDir()
t.Setenv("HOME", dir)
+ t.Setenv("EDITOR", "") // ensure fallback to "vi"
writeUserConfig(t, dir, `{invalid json}`)
var cfg Config
@@ -156,8 +181,8 @@ func TestLoad_invalid_json(t *testing.T) {
t.Errorf("expected warning on stderr, got: %q", stderr)
}
// Defaults must be returned with the redirected HOME.
- if cfg.EditCmd != "hx" {
- t.Errorf("EditCmd = %q; want hx (default)", cfg.EditCmd)
+ if cfg.EditCmd != "vi" {
+ t.Errorf("EditCmd = %q; want vi (default)", cfg.EditCmd)
}
if cfg.KeyLength != 32 {
t.Errorf("KeyLength = %d; want 32 (default)", cfg.KeyLength)
@@ -179,12 +204,14 @@ func TestLoad_missing_file_no_warning(t *testing.T) {
// TestLoad_unreadable_file verifies that a config file that exists but cannot
// be read emits a warning and returns defaults (the !os.IsNotExist branch).
+// EDITOR is unset so EditCmd falls back to "vi".
func TestLoad_unreadable_file(t *testing.T) {
if os.Getuid() == 0 {
t.Skip("running as root: permission checks do not apply")
}
dir := t.TempDir()
t.Setenv("HOME", dir)
+ t.Setenv("EDITOR", "") // ensure fallback to "vi"
writeUserConfig(t, dir, `{"edit_cmd":"nvim"}`)
// Make the file unreadable.
@@ -201,7 +228,7 @@ func TestLoad_unreadable_file(t *testing.T) {
t.Errorf("expected warning on stderr, got: %q", stderr)
}
// Must return pure defaults, not the file content.
- if cfg.EditCmd != "hx" {
- t.Errorf("EditCmd = %q; want hx (default)", cfg.EditCmd)
+ if cfg.EditCmd != "vi" {
+ t.Errorf("EditCmd = %q; want vi (default)", cfg.EditCmd)
}
}