aboutsummaryrefslogtreecommitdiff
path: root/internal/xdg
diff options
context:
space:
mode:
authorLukasz Kasprzak <lukas@labunix.xyz>2026-09-11 14:47:10 +0200
committerLukasz Kasprzak <lukas@labunix.xyz>2026-09-11 15:01:57 +0200
commit42b02c47be9b285099203e44a2570636d4ca6f03 (patch)
tree82bcb9e19bd886f36e1ca7b2d94a204c988f1fd1 /internal/xdg
downloadkrino-42b02c47be9b285099203e44a2570636d4ca6f03.tar.gz
krino-42b02c47be9b285099203e44a2570636d4ca6f03.zip
krino: foundation — sexp reader, config language, init/new/check
Diffstat (limited to 'internal/xdg')
-rw-r--r--internal/xdg/xdg.go60
-rw-r--r--internal/xdg/xdg_test.go60
2 files changed, 120 insertions, 0 deletions
diff --git a/internal/xdg/xdg.go b/internal/xdg/xdg.go
new file mode 100644
index 0000000..ed34838
--- /dev/null
+++ b/internal/xdg/xdg.go
@@ -0,0 +1,60 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+// Package xdg resolves the XDG base directories and expands ~ in paths.
+package xdg
+
+import (
+ "os"
+ "path/filepath"
+ "strings"
+)
+
+// ConfigHome is $XDG_CONFIG_HOME, or ~/.config.
+func ConfigHome() string { return base("XDG_CONFIG_HOME", ".config") }
+
+// StateHome is $XDG_STATE_HOME, or ~/.local/state.
+func StateHome() string { return base("XDG_STATE_HOME", filepath.Join(".local", "state")) }
+
+// DataHome is $XDG_DATA_HOME, or ~/.local/share.
+func DataHome() string { return base("XDG_DATA_HOME", filepath.Join(".local", "share")) }
+
+// base follows the XDG rule that a relative value is invalid and ignored.
+func base(env, fallback string) string {
+ if v := os.Getenv(env); filepath.IsAbs(v) {
+ return filepath.Clean(v)
+ }
+ return filepath.Join(Home(), fallback)
+}
+
+// Home is the user's home directory, or "/" when it is unknown.
+func Home() string {
+ if h, err := os.UserHomeDir(); err == nil && h != "" {
+ return filepath.Clean(h)
+ }
+ return "/"
+}
+
+// Expand replaces a leading "~" or "~/" with the home directory.
+// "~user" is left alone.
+func Expand(p string) string {
+ if p == "~" {
+ return Home()
+ }
+ if strings.HasPrefix(p, "~/") {
+ return filepath.Join(Home(), p[2:])
+ }
+ return p
+}
+
+// Abbrev replaces a leading home directory with "~", for display and for
+// paths written into config files.
+func Abbrev(p string) string {
+ h := Home()
+ if p == h {
+ return "~"
+ }
+ if h != "/" && strings.HasPrefix(p, h+"/") {
+ return "~/" + p[len(h)+1:]
+ }
+ return p
+}
diff --git a/internal/xdg/xdg_test.go b/internal/xdg/xdg_test.go
new file mode 100644
index 0000000..7c7de87
--- /dev/null
+++ b/internal/xdg/xdg_test.go
@@ -0,0 +1,60 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+package xdg
+
+import "testing"
+
+func TestBaseDirs(t *testing.T) {
+ t.Setenv("HOME", "/home/u")
+ t.Setenv("XDG_CONFIG_HOME", "")
+ t.Setenv("XDG_STATE_HOME", "relative/ignored")
+ t.Setenv("XDG_DATA_HOME", "/data/")
+ if got := ConfigHome(); got != "/home/u/.config" {
+ t.Errorf("ConfigHome() = %q", got)
+ }
+ if got := StateHome(); got != "/home/u/.local/state" {
+ t.Errorf("StateHome() = %q, a relative value must be ignored", got)
+ }
+ if got := DataHome(); got != "/data" {
+ t.Errorf("DataHome() = %q", got)
+ }
+}
+
+// TestHomeTrailingSlash is item H: Home() must clean its result, or a
+// trailing slash from $HOME breaks Abbrev's prefix check.
+func TestHomeTrailingSlash(t *testing.T) {
+ t.Setenv("HOME", "/home/u/")
+ if got := Abbrev("/home/u/x"); got != "~/x" {
+ t.Errorf("Abbrev(/home/u/x) = %q, want ~/x", got)
+ }
+ if got := Expand("~/x"); got != "/home/u/x" {
+ t.Errorf("Expand(~/x) = %q, want /home/u/x", got)
+ }
+}
+
+func TestExpandAbbrev(t *testing.T) {
+ t.Setenv("HOME", "/home/u")
+ expand := map[string]string{
+ "~": "/home/u",
+ "~/d/x": "/home/u/d/x",
+ "~other/x": "~other/x",
+ "/abs": "/abs",
+ "rel/x": "rel/x",
+ }
+ for in, want := range expand {
+ if got := Expand(in); got != want {
+ t.Errorf("Expand(%q) = %q, want %q", in, got, want)
+ }
+ }
+ abbrev := map[string]string{
+ "/home/u": "~",
+ "/home/u/d/x": "~/d/x",
+ "/home/ux/y": "/home/ux/y",
+ "/etc": "/etc",
+ }
+ for in, want := range abbrev {
+ if got := Abbrev(in); got != want {
+ t.Errorf("Abbrev(%q) = %q, want %q", in, got, want)
+ }
+ }
+}