summaryrefslogtreecommitdiff
path: root/src/fs.zig
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-09-30 14:53:46 -0300
committerGabriel Schneider <[email protected]>2026-10-01 00:12:17 -0300
commit12c257e624d3f1f80ca3df041b54026ff772d638 (patch)
tree6bba52da1148e167ed695e219bb3a9c742337d95 /src/fs.zig
parente56a8ba396566eaab818b6c742850b5842e880bb (diff)
downloadpardes-12c257e624d3f1f80ca3df041b54026ff772d638.tar.gz
pardes-12c257e624d3f1f80ca3df041b54026ff772d638.zip
A path typed with a leading ~ is the home directory, at every entry point that takes one, through one helper
Only DumpDir (dump.zig) and ShaderBuild expanded `~/`, each its own way. So a look at `~/notes.txt`, `name ~/x`, `Save ~/x`, ThemeFile, Restore and a quoted `pardes '~/x'` all took `~` as a directory of that name under the pane's. There is now one rule, filesystem.expandHome. A leading `~`, alone or before `/` or `:`, is $HOME, else the passwd entry's home; `~user` is that user's (getpwnam). As in a shell, it applies even beside a file named `~`, and `./~` names that file. Every entry point runs its typed path through it: - look, B3 and 9P, before any parsing, so `~/x:12`, `~/x:12:3` and `~/x:/re/` all work; - name, and a tag's rename; - Save <path>; - ThemeFile, DumpDir, Restore and ShaderBuild's shader files; - the forwarding launch. Recent rows are not shown with `~` and need nothing. How names are shown is unchanged. Tests: expandHome's cases, and HOME unset (passwd), with HOME passed in rather than set, since setenv moves the environ a spawned child reads. fs.py covers Save into missing directories under `~`, `name ~/x`, a look at `~/x:3`, and a forwarded `pardes '~/x'`. Co-Authored-By: Claude Opus 5.5 <[email protected]>
Diffstat (limited to 'src/fs.zig')
-rw-r--r--src/fs.zig57
1 files changed, 56 insertions, 1 deletions
diff --git a/src/fs.zig b/src/fs.zig
index 30ad9c43..7695d497 100644
--- a/src/fs.zig
+++ b/src/fs.zig
@@ -732,6 +732,59 @@ pub fn dotRelative(word: []const u8) bool {
/// Where a path really is on the host, and whether it is a directory. The
/// path may lie in a mount this editor serves, so the turn is given up for
/// the syscalls: another thread answers them.
+/// A path's leading `~` -- alone, or before `/` or `:` (`~/x:12`) -- is the
+/// home directory: $HOME, else the passwd entry's. `~user/...` is that
+/// user's. As in a shell, `~` is home even beside a file named `~` in the
+/// current directory. Every entry point that takes a path the user typed
+/// runs it through here: look (B3 and 9P), name, Save, ThemeFile, DumpDir,
+/// Restore, a forwarded `pardes ~/x`. The path itself when there is no
+/// `~` to expand, or no home for it.
+pub fn expandHome(path: []const u8, out: *[4096]u8) []const u8 {
+ if (comptime !pardes.hosted) return path;
+ return expandHomeFrom(path, out, std.c.getenv("HOME"));
+}
+
+/// expandHome with HOME given (null: unset), so a test need not change the
+/// process's environment, which a child spawned later reads.
+fn expandHomeFrom(path: []const u8, out: *[4096]u8, env_home: ?[*:0]const u8) []const u8 {
+ if (path.len == 0 or path[0] != '~') return path;
+ const end = std.mem.indexOfAny(u8, path, "/:") orelse path.len;
+ const home = (if (end == 1) homeDir(env_home) else userHome(path[1..end])) orelse return path;
+ const trimmed = std.mem.trimEnd(u8, home, "/");
+ const rest = path[end..];
+ if (trimmed.len == 0 and (rest.len == 0 or rest[0] != '/')) return std.fmt.bufPrint(out, "/{s}", .{rest}) catch path;
+ return std.fmt.bufPrint(out, "{s}{s}", .{ trimmed, rest }) catch path;
+}
+
+fn homeDir(env_home: ?[*:0]const u8) ?[]const u8 {
+ if (env_home) |h| if (h[0] != 0) return std.mem.span(h);
+ const pw = std.c.getpwuid(std.c.getuid()) orelse return null;
+ return std.mem.span(pw.dir orelse return null);
+}
+
+fn userHome(name: []const u8) ?[]const u8 {
+ var z: [256]u8 = undefined;
+ const name_z = std.fmt.bufPrintSentinel(&z, "{s}", .{name}, 0) catch return null;
+ const pw = std.c.getpwnam(name_z) orelse return null;
+ return std.mem.span(pw.dir orelse return null);
+}
+
+test "a leading ~ is home, from HOME or else the passwd entry" {
+ var out: [4096]u8 = undefined;
+ const home = homeDir(std.c.getenv("HOME")) orelse return error.SkipZigTest;
+ var want: [4096]u8 = undefined;
+ try std.testing.expectEqualStrings(try std.fmt.bufPrint(&want, "{s}/x:12", .{std.mem.trimEnd(u8, home, "/")}), expandHome("~/x:12", &out));
+ try std.testing.expectEqualStrings(std.mem.trimEnd(u8, home, "/"), expandHome("~", &out));
+ try std.testing.expectEqualStrings("a/~/b", expandHome("a/~/b", &out));
+ try std.testing.expectEqualStrings("~no-such-user-pardes/x", expandHome("~no-such-user-pardes/x", &out));
+ try std.testing.expectEqualStrings("/", expandHome("~root", &out)[0..1]);
+
+ // HOME unset: the passwd entry's home.
+ const pw = std.c.getpwuid(std.c.getuid()) orelse return error.SkipZigTest;
+ var from_passwd: [4096]u8 = undefined;
+ try std.testing.expectEqualStrings(try std.fmt.bufPrint(&from_passwd, "{s}/y", .{std.mem.trimEnd(u8, std.mem.span(pw.dir.?), "/")}), expandHomeFrom("~/y", &out, null));
+}
+
pub fn resolveOs(path: []const u8, out: *[4096]u8) ?Resolved {
if (comptime !platform_has_fs) return null;
var z: [4096]u8 = undefined;
@@ -1171,7 +1224,9 @@ pub fn readFile(gpa: std.mem.Allocator, path: []const u8) ![]u8 {
/// Restore first tries a relative argument under the default dump directory,
/// then preserves the ordinary file lookup. Absolute paths remain verbatim.
-pub fn readRestore(gpa: std.mem.Allocator, path: []const u8, dir_setting: []const u8) ![]u8 {
+pub fn readRestore(gpa: std.mem.Allocator, typed: []const u8, dir_setting: []const u8) ![]u8 {
+ var home_buf: [4096]u8 = undefined;
+ const path = expandHome(typed, &home_buf);
var dir_buf: [4096]u8 = undefined;
const dir = @import("dump.zig").directory(&dir_buf, dir_setting);
return readRestoreIn(gpa, path, dir);