//! Resolves session file locations from the environment. //! //! Layout (defaults; override base via `PANTO_SESSION_DIR`): //! //! $XDG_DATA_HOME/panto/sessions// //! ↳ falls back to $HOME/.local/share/panto/sessions// //! if $XDG_DATA_HOME is unset. //! //! `` is the working directory with leading `/` stripped and //! every `/` or `:` replaced by `-`, with `--` glued to both ends. This //! gives a flat directory name per project, easy to spot in `ls`. const std = @import("std"); const panto_home = @import("panto_home.zig"); const Allocator = std.mem.Allocator; /// Resolve the absolute sessions directory for the given cwd. Caller owns /// the returned slice. /// /// Precedence: /// 1. `PANTO_SESSION_DIR` (full path to the base dir, used as-is — no /// "panto/sessions" suffix is added) /// 2. `XDG_DATA_HOME/panto/sessions` /// 3. `HOME/.local/share/panto/sessions` /// /// In all three cases, `/` is appended. pub fn sessionDirForCwd( allocator: Allocator, environ_map: *const std.process.Environ.Map, cwd: []const u8, ) ![]u8 { const base = try resolveSessionsBase(allocator, environ_map); defer allocator.free(base); const encoded = try encodeCwd(allocator, cwd); defer allocator.free(encoded); return try std.fs.path.join(allocator, &.{ base, encoded }); } /// Resolve the absolute "sessions" base directory, before per-cwd grouping. /// Caller owns the returned slice. pub fn resolveSessionsBase( allocator: Allocator, environ_map: *const std.process.Environ.Map, ) ![]u8 { if (environ_map.get("PANTO_SESSION_DIR")) |explicit| { return try allocator.dupe(u8, explicit); } const home = try panto_home.homePath(allocator, environ_map); defer allocator.free(home); return try std.fs.path.join(allocator, &.{ home, "sessions" }); } /// Encode a working directory into a flat directory name. Caller owns. /// /// Example: `/Users/travis/Code/pantograph` → `--Users-travis-Code-pantograph--` pub fn encodeCwd(allocator: Allocator, cwd: []const u8) ![]u8 { // Strip leading slash(es), then replace `/` and `:` with `-`. var start: usize = 0; while (start < cwd.len and (cwd[start] == '/' or cwd[start] == '\\')) : (start += 1) {} const body = cwd[start..]; const out = try allocator.alloc(u8, body.len + 4); // `--` + body + `--` out[0] = '-'; out[1] = '-'; for (body, 0..) |c, i| { out[2 + i] = if (c == '/' or c == '\\' or c == ':') '-' else c; } out[out.len - 2] = '-'; out[out.len - 1] = '-'; return out; } // ============================================================================= // Tests // ============================================================================= const testing = std.testing; test "encodeCwd: replaces slashes and colons" { const a = testing.allocator; const got = try encodeCwd(a, "/Users/travis/Code/pantograph"); defer a.free(got); try testing.expectEqualStrings("--Users-travis-Code-pantograph--", got); } test "encodeCwd: handles already-relative paths" { const a = testing.allocator; const got = try encodeCwd(a, "Users/travis"); defer a.free(got); try testing.expectEqualStrings("--Users-travis--", got); } test "resolveSessionsBase: PANTO_SESSION_DIR wins" { const a = testing.allocator; var env: std.process.Environ.Map = .init(a); defer env.deinit(); try env.put("PANTO_SESSION_DIR", "/custom/sessions"); try env.put("XDG_DATA_HOME", "/ignored"); const got = try resolveSessionsBase(a, &env); defer a.free(got); try testing.expectEqualStrings("/custom/sessions", got); } test "resolveSessionsBase: XDG_DATA_HOME before HOME" { const a = testing.allocator; var env: std.process.Environ.Map = .init(a); defer env.deinit(); try env.put("XDG_DATA_HOME", "/x/data"); try env.put("HOME", "/h"); const got = try resolveSessionsBase(a, &env); defer a.free(got); try testing.expectEqualStrings("/x/data/panto/sessions", got); } test "resolveSessionsBase: falls back to HOME/.local/share" { const a = testing.allocator; var env: std.process.Environ.Map = .init(a); defer env.deinit(); try env.put("HOME", "/home/user"); const got = try resolveSessionsBase(a, &env); defer a.free(got); try testing.expectEqualStrings("/home/user/.local/share/panto/sessions", got); } test "sessionDirForCwd: joins base and encoded cwd" { const a = testing.allocator; var env: std.process.Environ.Map = .init(a); defer env.deinit(); try env.put("PANTO_SESSION_DIR", "/sess"); const got = try sessionDirForCwd(a, &env, "/Users/travis/Code/pantograph"); defer a.free(got); try testing.expectEqualStrings("/sess/--Users-travis-Code-pantograph--", got); }