diff options
| author | Can Celik <ogulcancelik@gmail.com> | 2026-04-04 19:22:04 +0300 |
|---|---|---|
| committer | Can Celik <ogulcancelik@gmail.com> | 2026-04-05 01:23:06 +0300 |
| commit | 41dd733ca7123c9ac4bfc6c781697a660e95b052 (patch) | |
| tree | 3fc91815ca139ef2e0b281bba0abda4ef15175f4 /vendor/libghostty-vt/src/config/path.zig | |
| parent | 55c138263e190a1e79d4b997bfb9397306c0ec1f (diff) | |
feat: finish ghostty pane backend integration
Diffstat (limited to 'vendor/libghostty-vt/src/config/path.zig')
| -rw-r--r-- | vendor/libghostty-vt/src/config/path.zig | 510 |
1 files changed, 510 insertions, 0 deletions
diff --git a/vendor/libghostty-vt/src/config/path.zig b/vendor/libghostty-vt/src/config/path.zig new file mode 100644 index 00000000..793cf184 --- /dev/null +++ b/vendor/libghostty-vt/src/config/path.zig @@ -0,0 +1,510 @@ +const std = @import("std"); +const builtin = @import("builtin"); +const assert = @import("../quirks.zig").inlineAssert; +const Allocator = std.mem.Allocator; +const ArenaAllocator = std.heap.ArenaAllocator; + +const cli = @import("../cli.zig"); +const internal_os = @import("../os/main.zig"); +const formatterpkg = @import("formatter.zig"); + +const log = std.log.scoped(.config); + +pub const ParseError = error{ValueRequired} || Allocator.Error; + +/// Path is like a string that represents a path value. The difference is that +/// when loading the configuration the value for this will be automatically +/// expanded relative to the path of the config file (or the home directory). +pub const Path = union(enum) { + /// No error if the file does not exist. + optional: [:0]const u8, + + /// The file is required to exist. + required: [:0]const u8, + + pub fn len(self: Path) usize { + return switch (self) { + inline else => |path| path.len, + }; + } + + pub fn equal(self: Path, other: Path) bool { + return std.meta.eql(self, other); + } + + /// ghostty_config_path_s + pub const C = extern struct { + path: [*:0]const u8, + optional: bool, + }; + + /// Returns the path as a C-compatible struct. + pub fn cval(self: Path) C { + return switch (self) { + .optional => |path| .{ .path = path.ptr, .optional = true }, + .required => |path| .{ .path = path.ptr, .optional = false }, + }; + } + + /// Parse the input and return a Path. A leading `?` indicates that the path + /// is _optional_ and an error should not be logged or displayed to the user + /// if that path does not exist. Otherwise the path is required and an error + /// should be logged if the path does not exist. + pub fn parse( + /// Allocator to use. This must be an arena allocator because we assume + /// that any allocations will be cleaned up when the arena. + arena_alloc: Allocator, + /// The input. + input: ?[]const u8, + ) ParseError!?Path { + var value = input orelse return error.ValueRequired; + + if (value.len == 0) return null; + + const optional = if (value[0] == '?') opt: { + value = value[1..]; + break :opt true; + } else false; + + if (value.len >= 2 and value[0] == '"' and value[value.len - 1] == '"') { + value = value[1 .. value.len - 1]; + } + + if (optional) + return .{ .optional = try arena_alloc.dupeZ(u8, value) } + else + return .{ .required = try arena_alloc.dupeZ(u8, value) }; + } + + /// Parse CLI option. + pub fn parseCLI( + /// The path. The value will be overwritten. + self: *Path, + /// Allocator to use. This must be an arena allocator because we assume + /// that any allocations will be cleaned up when the arena. + arena_alloc: Allocator, + // The input. + input: ?[]const u8, + ) ParseError!void { + assert(input != null); + const item = try parse(arena_alloc, input) orelse return; + if (item.len() == 0) return; + self.* = item; + } + + /// Used by formatter. + pub fn formatEntry(self: *const Path, formatter: formatterpkg.EntryFormatter) !void { + var buf: [std.fs.max_path_bytes + 1]u8 = undefined; + const value = switch (self.*) { + .optional => |path| std.fmt.bufPrint( + &buf, + "?{s}", + .{path}, + ) catch |err| switch (err) { + // Required for builds on Linux where NoSpaceLeft + // isn't an allowed error for fmt. + error.NoSpaceLeft => return error.OutOfMemory, + }, + .required => |path| path, + }; + + try formatter.formatEntry([]const u8, value); + } + + /// Return a clone of the path. + pub fn clone( + /// The path to clone. + self: Path, + /// This must be an arena allocator because we rely on the arena to + /// clean up our allocations. + arena_alloc: Allocator, + ) Allocator.Error!Path { + return switch (self) { + .optional => |path| .{ + .optional = try arena_alloc.dupeZ(u8, path), + }, + .required => |path| .{ + .required = try arena_alloc.dupeZ(u8, path), + }, + }; + } + + /// Expand relative paths or paths prefixed with `~/`. The path will be + /// overwritten. + pub fn expand( + /// The path to expand. + self: *Path, + /// This must be an arena allocator because we rely on the arena to + /// clean up our allocations. + arena_alloc: Allocator, + /// The base directory to expand relative paths. It must be an absolute + /// path. + base: []const u8, + /// Errors will be added to the list of diagnostics if they occur. + diags: *cli.DiagnosticList, + ) !void { + assert(std.fs.path.isAbsolute(base)); + + const path = switch (self.*) { + .optional, .required => |path| path, + }; + + // If it is already absolute we can ignore it. + if (path.len == 0 or std.fs.path.isAbsolute(path)) return; + + // If it isn't absolute, we need to make it absolute relative + // to the base. + var buf: [std.fs.max_path_bytes]u8 = undefined; + + // Check if the path starts with a tilde and expand it to the + // home directory on Linux/macOS. We explicitly look for "~/" + // because we don't support alternate users such as "~alice/" + if (std.mem.startsWith(u8, path, "~/")) expand: { + // Windows isn't supported yet + if (comptime builtin.os.tag == .windows) break :expand; + + const expanded: []const u8 = internal_os.expandHome( + path, + &buf, + ) catch |err| { + try diags.append(arena_alloc, .{ + .message = try std.fmt.allocPrintSentinel( + arena_alloc, + "error expanding home directory for path {s}: {}", + .{ path, err }, + 0, + ), + }); + + // Blank this path so that we don't attempt to resolve it + // again + self.* = .{ .required = "" }; + + return; + }; + + log.debug( + "expanding file path from home directory: path={s}", + .{expanded}, + ); + + switch (self.*) { + .optional, .required => |*p| p.* = try arena_alloc.dupeZ(u8, expanded), + } + + return; + } + + var dir = try std.fs.openDirAbsolute(base, .{}); + defer dir.close(); + + const abs = dir.realpath(path, &buf) catch |err| abs: { + if (err == error.FileNotFound) { + // The file doesn't exist. Try to resolve the relative path + // another way. + const resolved = try std.fs.path.resolve(arena_alloc, &.{ base, path }); + defer arena_alloc.free(resolved); + @memcpy(buf[0..resolved.len], resolved); + break :abs buf[0..resolved.len]; + } + + try diags.append(arena_alloc, .{ + .message = try std.fmt.allocPrintSentinel( + arena_alloc, + "error resolving file path {s}: {}", + .{ path, err }, + 0, + ), + }); + + // Blank this path so that we don't attempt to resolve it again + self.* = .{ .required = "" }; + + return; + }; + + log.debug( + "expanding file path relative={s} abs={s}", + .{ path, abs }, + ); + + switch (self.*) { + .optional, .required => |*p| p.* = try arena_alloc.dupeZ(u8, abs), + } + } + + test "parse" { + const testing = std.testing; + var arena = ArenaAllocator.init(testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + + const Tag = std.meta.Tag(Path); + + { + const item = (try Path.parse(alloc, "config.1")).?; + try testing.expectEqual(Tag.required, @as(Tag, item)); + try testing.expectEqualStrings("config.1", item.required); + } + + { + const item = (try Path.parse(alloc, "?config.2")).?; + try testing.expectEqual(Tag.optional, @as(Tag, item)); + try testing.expectEqualStrings("config.2", item.optional); + } + + { + const item = (try Path.parse(alloc, "\"?config.3\"")).?; + try testing.expectEqual(Tag.required, @as(Tag, item)); + try testing.expectEqualStrings("?config.3", item.required); + } + + { + const item = (try Path.parse(alloc, "?\"config.4\"")).?; + try testing.expectEqual(Tag.optional, @as(Tag, item)); + try testing.expectEqualStrings("config.4", item.optional); + } + + { + const item = (try Path.parse(alloc, "?")).?; + try testing.expectEqual(Tag.optional, @as(Tag, item)); + try testing.expectEqualStrings("", item.optional); + } + + { + const item = (try Path.parse(alloc, "\"\"")).?; + try testing.expectEqual(Tag.required, @as(Tag, item)); + try testing.expectEqualStrings("", item.required); + } + + { + const item = (try Path.parse(alloc, "?\"\"")).?; + try testing.expectEqual(Tag.optional, @as(Tag, item)); + try testing.expectEqualStrings("", item.optional); + } + } + + test "parseCLI" { + const testing = std.testing; + var arena = ArenaAllocator.init(testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + + const Tag = std.meta.Tag(Path); + var item: Path = undefined; + + try item.parseCLI(alloc, "config.1"); + try testing.expectEqual(Tag.required, @as(Tag, item)); + try testing.expectEqualStrings("config.1", item.required); + + try item.parseCLI(alloc, "?config.2"); + try testing.expectEqual(Tag.optional, @as(Tag, item)); + try testing.expectEqualStrings("config.2", item.optional); + + try item.parseCLI(alloc, "\"?config.3\""); + try testing.expectEqual(Tag.required, @as(Tag, item)); + try testing.expectEqualStrings("?config.3", item.required); + + // Zero-length values, ignored + + try item.parseCLI(alloc, "?"); + try testing.expectEqual(Tag.required, @as(Tag, item)); + try testing.expectEqualStrings("?config.3", item.required); + + try item.parseCLI(alloc, "\"\""); + try testing.expectEqual(Tag.required, @as(Tag, item)); + try testing.expectEqualStrings("?config.3", item.required); + + try item.parseCLI(alloc, "?\"\""); + try testing.expectEqual(Tag.required, @as(Tag, item)); + try testing.expectEqualStrings("?config.3", item.required); + } + + test "formatConfig single item" { + const testing = std.testing; + var buf: std.Io.Writer.Allocating = .init(testing.allocator); + defer buf.deinit(); + + var arena = ArenaAllocator.init(testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + + var item: Path = undefined; + try item.parseCLI(alloc, "A"); + try item.formatEntry(formatterpkg.entryFormatter("a", &buf.writer)); + try std.testing.expectEqualSlices(u8, "a = A\n", buf.written()); + } + + test "formatConfig multiple items" { + const testing = std.testing; + var buf: std.Io.Writer.Allocating = .init(testing.allocator); + defer buf.deinit(); + + var arena = ArenaAllocator.init(testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + + var item: Path = undefined; + try item.parseCLI(alloc, "A"); + try item.parseCLI(alloc, "?B"); + try item.formatEntry(formatterpkg.entryFormatter("a", &buf.writer)); + try std.testing.expectEqualSlices(u8, "a = ?B\n", buf.written()); + } +}; + +/// RepeatablePath is like repeatable string but represents a path value. The +/// difference is that when loading the configuration any values for this will +/// be automatically expanded relative to the path of the config file (or the home +/// directory). +pub const RepeatablePath = struct { + value: std.ArrayListUnmanaged(Path) = .{}, + + pub fn parseCLI(self: *RepeatablePath, alloc: Allocator, input: ?[]const u8) ParseError!void { + const item = try Path.parse(alloc, input) orelse { + self.value.clearRetainingCapacity(); + return; + }; + + if (item.len() == 0) { + // This handles the case of zero length paths after removing any ? + // prefixes or surrounding quotes. In this case, we don't reset the + // list. + return; + } + + try self.value.append(alloc, item); + } + + /// Deep copy of the struct. Required by Config. + pub fn clone(self: *const RepeatablePath, alloc: Allocator) Allocator.Error!RepeatablePath { + const value = try self.value.clone(alloc); + for (value.items) |*item| { + item.* = try item.clone(alloc); + } + + return .{ + .value = value, + }; + } + + /// Compare if two of our value are equal. Required by Config. + pub fn equal(self: RepeatablePath, other: RepeatablePath) bool { + if (self.value.items.len != other.value.items.len) return false; + for (self.value.items, other.value.items) |a, b| { + if (!a.equal(b)) return false; + } + + return true; + } + + /// Used by Formatter + pub fn formatEntry(self: RepeatablePath, formatter: formatterpkg.EntryFormatter) !void { + if (self.value.items.len == 0) { + try formatter.formatEntry(void, {}); + return; + } + + var buf: [std.fs.max_path_bytes + 1]u8 = undefined; + for (self.value.items) |item| { + const value = switch (item) { + .optional => |path| std.fmt.bufPrint( + &buf, + "?{s}", + .{path}, + ) catch |err| switch (err) { + // Required for builds on Linux where NoSpaceLeft + // isn't an allowed error for fmt. + error.NoSpaceLeft => return error.OutOfMemory, + }, + .required => |path| path, + }; + + try formatter.formatEntry([]const u8, value); + } + } + + /// Expand all the paths relative to the base directory. + pub fn expand( + self: *RepeatablePath, + alloc: Allocator, + base: []const u8, + diags: *cli.DiagnosticList, + ) !void { + for (self.value.items) |*path| { + try path.expand(alloc, base, diags); + } + } + test "parseCLI" { + const testing = std.testing; + var arena = ArenaAllocator.init(testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + + var list: RepeatablePath = .{}; + try list.parseCLI(alloc, "config.1"); + try list.parseCLI(alloc, "?config.2"); + try list.parseCLI(alloc, "\"?config.3\""); + + try testing.expectEqual(@as(usize, 3), list.value.items.len); + + // Zero-length values, ignored + try list.parseCLI(alloc, "?"); + try list.parseCLI(alloc, "\"\""); + + try testing.expectEqual(@as(usize, 3), list.value.items.len); + + const Tag = std.meta.Tag(Path); + try testing.expectEqual(Tag.required, @as(Tag, list.value.items[0])); + try testing.expectEqualStrings("config.1", list.value.items[0].required); + + try testing.expectEqual(Tag.optional, @as(Tag, list.value.items[1])); + try testing.expectEqualStrings("config.2", list.value.items[1].optional); + + try testing.expectEqual(Tag.required, @as(Tag, list.value.items[2])); + try testing.expectEqualStrings("?config.3", list.value.items[2].required); + + try list.parseCLI(alloc, ""); + try testing.expectEqual(@as(usize, 0), list.value.items.len); + } + + test "formatConfig empty" { + const testing = std.testing; + var buf: std.Io.Writer.Allocating = .init(testing.allocator); + defer buf.deinit(); + + var list: RepeatablePath = .{}; + try list.formatEntry(formatterpkg.entryFormatter("a", &buf.writer)); + try std.testing.expectEqualSlices(u8, "a = \n", buf.written()); + } + + test "formatConfig single item" { + const testing = std.testing; + var buf: std.Io.Writer.Allocating = .init(testing.allocator); + defer buf.deinit(); + + var arena = ArenaAllocator.init(testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + + var list: RepeatablePath = .{}; + try list.parseCLI(alloc, "A"); + try list.formatEntry(formatterpkg.entryFormatter("a", &buf.writer)); + try std.testing.expectEqualSlices(u8, "a = A\n", buf.written()); + } + + test "formatConfig multiple items" { + const testing = std.testing; + var buf: std.Io.Writer.Allocating = .init(testing.allocator); + defer buf.deinit(); + + var arena = ArenaAllocator.init(testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + + var list: RepeatablePath = .{}; + try list.parseCLI(alloc, "A"); + try list.parseCLI(alloc, "?B"); + try list.formatEntry(formatterpkg.entryFormatter("a", &buf.writer)); + try std.testing.expectEqualSlices(u8, "a = A\na = ?B\n", buf.written()); + } +}; |
