1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
|
//! Resolves session file locations from the environment.
//!
//! Layout (defaults; override base via `PANTO_SESSION_DIR`):
//!
//! $XDG_DATA_HOME/panto/sessions/<encoded-cwd>/
//! ↳ falls back to $HOME/.local/share/panto/sessions/<encoded-cwd>/
//! if $XDG_DATA_HOME is unset.
//!
//! `<encoded-cwd>` 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 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, `<encoded-cwd>/` 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);
}
if (environ_map.get("XDG_DATA_HOME")) |xdg| {
return try std.fs.path.join(allocator, &.{ xdg, "panto", "sessions" });
}
if (environ_map.get("HOME")) |home| {
return try std.fs.path.join(allocator, &.{ home, ".local", "share", "panto", "sessions" });
}
return error.NoHomeDirectory;
}
/// 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);
}
|