diff --git a/internal/config/config.go b/internal/config/config.go index 34c5518..429238f 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -6,11 +6,31 @@ import ( "encoding/json" "os" "path/filepath" + "strings" "omarchy-send/internal/protocol" "omarchy-send/internal/security" ) +// ExpandHome resolves a leading "~" or "~/" to the user's home directory, so a +// receiveDir stored as "~/Omarchy-Send" (hand-edited, or typed into the +// Settings tab) means what the user means — and is not treated as a relative +// path that silently creates a literal "~" directory under the process cwd. +func ExpandHome(p string) string { + if p == "~" { + if home, err := os.UserHomeDir(); err == nil { + return home + } + return p + } + if strings.HasPrefix(p, "~/") { + if home, err := os.UserHomeDir(); err == nil { + return filepath.Join(home, p[2:]) + } + } + return p +} + // Config is the persisted user configuration. type Config struct { Alias string `json:"alias"` @@ -106,6 +126,9 @@ func Load() (Config, error) { if cfg.ReceiveDir == "" { cfg.ReceiveDir = d.ReceiveDir } + // Normalise a ~-form receive dir to absolute; Load persists below, so the + // stored value is unambiguous from then on. + cfg.ReceiveDir = ExpandHome(cfg.ReceiveDir) if cfg.DeviceType == "" { cfg.DeviceType = d.DeviceType } diff --git a/internal/config/config_test.go b/internal/config/config_test.go new file mode 100644 index 0000000..69db720 --- /dev/null +++ b/internal/config/config_test.go @@ -0,0 +1,76 @@ +package config + +import ( + "encoding/json" + "os" + "path/filepath" + "testing" +) + +// ExpandHome resolves the ~-forms a user may type or hand-edit, and leaves +// everything else untouched. +func TestExpandHome(t *testing.T) { + home, err := os.UserHomeDir() + if err != nil { + t.Skipf("no home dir: %v", err) + } + cases := map[string]string{ + "~": home, + "~/Omarchy-Send": filepath.Join(home, "Omarchy-Send"), + "~/a/b": filepath.Join(home, "a", "b"), + "/abs/path": "/abs/path", + "relative/path": "relative/path", + "~user/not-ours": "~user/not-ours", // ~user expansion is not supported + "mid/~/not-leading": "mid/~/not-leading", + "": "", + } + for in, want := range cases { + if got := ExpandHome(in); got != want { + t.Errorf("ExpandHome(%q) = %q, want %q", in, got, want) + } + } +} + +// A config file whose receiveDir was stored as "~/…" is normalised to an +// absolute path by Load — the regression that sent files into a literal "~" +// directory under the process cwd. +func TestLoadExpandsTildeReceiveDir(t *testing.T) { + home, err := os.UserHomeDir() + if err != nil { + t.Skipf("no home dir: %v", err) + } + cfgHome := t.TempDir() + t.Setenv("XDG_CONFIG_HOME", cfgHome) + + dir := filepath.Join(cfgHome, "omarchy-send") + if err := os.MkdirAll(dir, 0o755); err != nil { + t.Fatalf("mkdir: %v", err) + } + seed := map[string]any{"alias": "t", "receiveDir": "~/Omarchy-Send"} + data, _ := json.Marshal(seed) + if err := os.WriteFile(filepath.Join(dir, "config.json"), data, 0o600); err != nil { + t.Fatalf("seed config: %v", err) + } + + cfg, err := Load() + if err != nil { + t.Fatalf("Load: %v", err) + } + want := filepath.Join(home, "Omarchy-Send") + if cfg.ReceiveDir != want { + t.Fatalf("ReceiveDir = %q, want %q", cfg.ReceiveDir, want) + } + + // And the normalised value is what got persisted back. + raw, err := os.ReadFile(filepath.Join(dir, "config.json")) + if err != nil { + t.Fatalf("read back: %v", err) + } + var onDisk map[string]any + if err := json.Unmarshal(raw, &onDisk); err != nil { + t.Fatalf("unmarshal: %v", err) + } + if onDisk["receiveDir"] != want { + t.Fatalf("persisted receiveDir = %q, want %q", onDisk["receiveDir"], want) + } +} diff --git a/internal/tui/model.go b/internal/tui/model.go index 5d8c95a..399eb9f 100644 --- a/internal/tui/model.go +++ b/internal/tui/model.go @@ -734,7 +734,9 @@ func (m Model) saveEdit() (tea.Model, tea.Cmd) { m.cfg.DeviceModel = alias } if dir != "" { - m.cfg.ReceiveDir = dir + // Expand a typed ~-form immediately so the live server and the saved + // config both carry the absolute path. + m.cfg.ReceiveDir = config.ExpandHome(dir) } m.cfg.PIN = pin _ = m.cfg.Save() @@ -1354,21 +1356,10 @@ func collapseHome(p string) string { } // expandHome resolves a leading ~ (or ~/) to the user's home directory. It is -// the inverse of collapseHome and tolerates the ~-form a user may type into the -// receive-dir setting. +// the inverse of collapseHome; the canonical implementation lives in config so +// every consumer of ReceiveDir expands the same way. func expandHome(p string) string { - if p == "~" { - if home, err := os.UserHomeDir(); err == nil { - return home - } - return p - } - if strings.HasPrefix(p, "~/") { - if home, err := os.UserHomeDir(); err == nil { - return filepath.Join(home, p[2:]) - } - } - return p + return config.ExpandHome(p) } func truncate(s string, n int) string {