// dirs — XDG base directory paths. Port of Hare's lib/dirs // (ref/hare/dirs/xdg.ha) using the lib/temp-style static `[256]u8` // pathbuf in place of Hare's `path::buffer` (lib/path doesn't ship // a buffer type yet). Surface is 1:1 with Hare's xdg.ha minus runtime(). // // Returns are static-buffer views — borrowed for the lifetime of // the next dirs call (any of the four above). Callers needing the // bytes to outlive the next call duplicate via [[strings.dup]]. // Same precedent as Hare's dirs:: (which doc-strings the same // "overwritten on subsequent calls" contract). // // Lookup algorithm (mirrors Hare's xdg.ha:lookup): // // 1. If $XDG__HOME is set AND its first byte is '/' // (Hare's path::abs check), return "/" after // mkdir-recursive (mode 0o755). // 2. Otherwise return "$HOME//" after mkdir- // recursive. The non-absolute and unset/empty XDG cases both // fall through to this branch — matches Hare's `yield` // after the `path::abs` test. // // rt_aborts in two cases (matches Hare's `as str` cast on missing // HOME, plus its `fs::strerror` fatal on mkdir failure): // // - HOME is not set // - mkdirs(path, 0o755) fails for a non-EEXIST reason // // Skipped from v1 (with reasons preserved for the next graduator): // // - runtime() — needs lib/os.stat + a getuid primitive for the // uid/perm/isdir verification Hare's runtime() does. A relaxed // env-only version would defeat the perm contract that's the // entire point of XDG_RUNTIME_DIR. Defer (drew/rob aligned). // // - XDG_CONFIG_DIRS / XDG_DATA_DIRS — system search paths. Hare's // xdg.ha doesn't ship them; lib/CLAUDE.md prohibits richer- // than-Hare surfaces. Defer until upstream Hare adds them. // // - lib/fmt's fatalf-style error reporting on mkdir failure. Hare // uses `fmt::fatalf("Error creating {}: {}", path, ...)`; ww's // lib/fmt is print-string-only (no {n}-placeholder parser), so // we hand the bare context "dirs: mkdirs failed" to rt_abort. // Graduates when the {n}-placeholder parser lands. package dirs; import os; @symbol("rt_abort") fn rtabort(msg: str) void; // pathbuf — module-level scratch path. Sized for any // "/.local/share/" composition under reasonable // HOME and prog lengths; dirs returns a view into pathbuf[0..pathlen]. // NUL byte at pathbuf[pathlen] for direct handoff to [[os.mkdirs]] // (same precedent as lib/temp). let pathbuf: [256]u8; let pathlen: i32 = 0; // SEP — '/' byte. Same constant as lib/path's SEP. def SEP: u8 = 47u8; // MODE_0755 — directory creation mode passed to [[os.mkdirs]]. // Hare uses the literal `0o755` at every call site; ww doesn't ship // octal literals, so we name the constant once. def MODE_0755: i32 = 493i32; // Caps at 255 to leave room for the trailing NUL. Same shape as // lib/temp.puts. fn puts(off: i32, s: str) i32 = { let i: i32 = 0; for (i < s.len) { if (off + i >= 255) { break; }; pathbuf[off + i] = s[i]; i += 1; }; return off + i; }; // Embedded '/' in `sub` (e.g. ".local/share") is fine — [[os.mkdirs]] // handles intermediate dirs. fn build(base: str, sub: str, prog: str) void = { // ref/hare/dirs/xdg.ha routes through path::set/push, whose too_long // error the `!` turns into a loud abort. ww's fixed 256B pathbuf // (dirs.ww:58) is a documented simplification, but the overflow must // be LOUD, not a silently-wrong directory that then gets mkdir'd: // reject up front when the composition (+ trailing NUL) won't fit. // Shape (a); routing dirs through lib/path is the fidelity follow-up. let need: i32 = base.len + 1 + prog.len; if (sub.len > 0) { need += 1 + sub.len; }; if (need >= 256) { rtabort("dirs: path too long"); }; let off: i32 = 0; off = puts(off, base); if (sub.len > 0) { pathbuf[off] = SEP; off += 1; off = puts(off, sub); }; pathbuf[off] = SEP; off += 1; off = puts(off, prog); pathbuf[off] = 0u8; pathlen = off; }; // Borrowed for the lifetime of the next dirs call. fn view() str = { let r: str; r.ptr = &pathbuf[0]; r.len = pathlen; return r; }; // ensure — wrap [[os.mkdirs]] with the rt_abort-on-non-EEXIST // behaviour Hare's lookup uses (`fmt::fatalf` on the HOME branch). // Centralised so both branches in [[lookup]] share the error path. fn ensure() void = { let pv: str; pv.ptr = &pathbuf[0]; pv.len = pathlen; match (os.mkdirs(pv, MODE_0755)) { case void => {}; case let _e: os.oserror => rtabort("dirs: mkdirs failed"); }; }; // lookup — Hare's lookup() inlined: probe $envvar, fall through to // $HOME/ on unset / empty / non-absolute XDG values. Auto- // mkdirs the result. rt_aborts if HOME is unset (no fallback at // the bottom of the chain — matches Hare's `as str` cast which // would also fault on missing HOME). fn lookup(prog: str, envvar: str, dflt: str) str = { match (os.getenv(envvar)) { case let xdg: str => { if (xdg.len > 0) { if (xdg[0] == SEP) { // Hare's path::abs(path) build(xdg, "", prog); ensure(); return view(); }; }; }; case void => {}; }; let home: str; match (os.getenv("HOME")) { case let h: str => { home = h; }; case void => rtabort("dirs: HOME is not set"); }; build(home, dflt, prog); ensure(); return view(); }; // config — directory suitable for storing config files for `prog`. // $XDG_CONFIG_HOME/ if set+absolute, else $HOME/.config/. // Auto-created with mode 0o755. Returns a borrowed str view into // the module-level pathbuf; subsequent dirs calls overwrite it. // // Mirrors Hare's dirs::config (xdg.ha:47). export fn config(prog: str) str = { return lookup(prog, "XDG_CONFIG_HOME", ".config"); }; // cache — directory suitable for cache files for `prog`. // $XDG_CACHE_HOME/ if set+absolute, else $HOME/.cache/. // // Mirrors Hare's dirs::cache (xdg.ha:53). export fn cache(prog: str) str = { return lookup(prog, "XDG_CACHE_HOME", ".cache"); }; // data — directory suitable for persistent data files for `prog`. // $XDG_DATA_HOME/ if set+absolute, else $HOME/.local/share/. // Hare composes the default via path::set(.local, share); we emit // the composed string directly since lib/path doesn't ship a // path::buffer type yet. // // Mirrors Hare's dirs::data (xdg.ha:59). export fn data(prog: str) str = { return lookup(prog, "XDG_DATA_HOME", ".local/share"); }; // state — directory suitable for storing program state files for // `prog`. $XDG_STATE_HOME/ if set+absolute, else // $HOME/.local/state/. // // Mirrors Hare's dirs::state (xdg.ha:69). export fn state(prog: str) str = { return lookup(prog, "XDG_STATE_HOME", ".local/state"); };