From 12c257e624d3f1f80ca3df041b54026ff772d638 Mon Sep 17 00:00:00 2001 From: Gabriel Schneider Date: Wed, 30 Sep 2026 14:53:46 -0300 Subject: 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 ; - 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 --- src/fs.zig | 57 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 56 insertions(+), 1 deletion(-) (limited to 'src/fs.zig') 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); -- cgit v1.3