diff options
| author | Can Celik <ogulcancelik@gmail.com> | 2026-09-10 21:51:57 +0300 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-09-10 21:51:57 +0300 |
| commit | 425c86179791cd32e2d4ce0ae7f269940b4b0663 (patch) | |
| tree | e52c4383a6d69216cbe2a32638bf90841d7e2189 /vendor/libghostty-vt/src/cli | |
| parent | 90e947a6e26e02f10ec7f1694c4eb5b78a86406b (diff) | |
chore: upgrade libghostty and preserve terminal behavior (#3906)
Diffstat (limited to 'vendor/libghostty-vt/src/cli')
25 files changed, 1059 insertions, 463 deletions
diff --git a/vendor/libghostty-vt/src/cli/Pager.zig b/vendor/libghostty-vt/src/cli/Pager.zig index 247c998e..1523c885 100644 --- a/vendor/libghostty-vt/src/cli/Pager.zig +++ b/vendor/libghostty-vt/src/cli/Pager.zig @@ -6,29 +6,31 @@ //! Setting either env var to an empty string disables paging. //! If stdout is not a TTY, writes go directly to stdout. const Pager = @This(); +const builtin = @import("builtin"); const std = @import("std"); const Allocator = std.mem.Allocator; const internal_os = @import("../os/main.zig"); +const global = @import("../global.zig"); /// The pager child process, if one was spawned. child: ?std.process.Child = null, /// The buffered file writer used for both the pager pipe and direct /// stdout paths. -file_writer: std.fs.File.Writer = undefined, +file_writer: std.Io.File.Writer = undefined, /// Initialize the pager. If stdout is a TTY, this spawns the pager /// process. Otherwise, output goes directly to stdout. -pub fn init(alloc: Allocator) Pager { - return .{ .child = initPager(alloc) }; +pub fn init() Pager { + return .{ .child = initPager() }; } /// Writes to the pager process if available; otherwise, stdout. pub fn writer(self: *Pager, buffer: []u8) *std.Io.Writer { if (self.child) |child| { - self.file_writer = child.stdin.?.writer(buffer); + self.file_writer = child.stdin.?.writer(global.io(), buffer); } else { - self.file_writer = std.fs.File.stdout().writer(buffer); + self.file_writer = std.Io.File.stdout().writer(global.io(), buffer); } return &self.file_writer.interface; } @@ -40,45 +42,44 @@ pub fn deinit(self: *Pager) void { // pager sees EOF, then wait for it to exit. self.file_writer.interface.flush() catch {}; if (child.stdin) |stdin| { - stdin.close(); + stdin.close(global.io()); child.stdin = null; } - _ = child.wait() catch {}; + _ = child.wait(global.io()) catch {}; } self.* = undefined; } -fn initPager(alloc: Allocator) ?std.process.Child { - const stdout_file: std.fs.File = .stdout(); - if (!stdout_file.isTty()) return null; +fn initPager() ?std.process.Child { + const stdout_file: std.Io.File = .stdout(); + if (!(stdout_file.isTty(global.io()) catch return null)) return null; + + var env = global.environMap() catch return null; + defer env.deinit(); // Resolve the pager command: $GHOSTTY_PAGER > $PAGER > `less`. // An empty value for either env var disables paging. - const ghostty_var = internal_os.getenv(alloc, "GHOSTTY_PAGER") catch null; - defer if (ghostty_var) |v| v.deinit(alloc); - const pager_var = internal_os.getenv(alloc, "PAGER") catch null; - defer if (pager_var) |v| v.deinit(alloc); - + const ghostty_var = env.get("GHOSTTY_PAGER"); + const pager_var = env.get("PAGER"); const cmd: ?[]const u8 = cmd: { - if (ghostty_var) |v| break :cmd if (v.value.len > 0) v.value else null; - if (pager_var) |v| break :cmd if (v.value.len > 0) v.value else null; + if (ghostty_var) |v| break :cmd if (v.len > 0) v else null; + if (pager_var) |v| break :cmd if (v.len > 0) v else null; break :cmd "less"; }; if (cmd == null) return null; - var child: std.process.Child = .init(&.{cmd.?}, alloc); - child.stdin_behavior = .Pipe; - child.stdout_behavior = .Inherit; - child.stderr_behavior = .Inherit; - - child.spawn() catch return null; - return child; + return std.process.spawn(global.io(), .{ + .argv = &.{cmd.?}, + .stdin = .pipe, + .stdout = .inherit, + .stderr = .inherit, + }) catch null; } test "pager: non-tty" { - var pager: Pager = .init(std.testing.allocator); + var pager: Pager = .init(); defer pager.deinit(); try std.testing.expect(pager.child == null); } diff --git a/vendor/libghostty-vt/src/cli/action.zig b/vendor/libghostty-vt/src/cli/action.zig index 41173a9f..6cedaa09 100644 --- a/vendor/libghostty-vt/src/cli/action.zig +++ b/vendor/libghostty-vt/src/cli/action.zig @@ -11,8 +11,8 @@ pub const DetectError = error{ }; /// Detect the action from CLI args. -pub fn detectArgs(comptime E: type, alloc: Allocator) !?E { - var iter = try std.process.argsWithAllocator(alloc); +pub fn detectArgs(comptime E: type, alloc: Allocator, args: std.process.Args) !?E { + var iter = try args.iterateAllocator(alloc); defer iter.deinit(); return try detectIter(E, &iter); } @@ -83,7 +83,7 @@ test "detect direct match" { const alloc = testing.allocator; const Enum = enum { foo, bar, baz }; - var iter = try std.process.ArgIteratorGeneral(.{}).init( + var iter = try std.process.Args.IteratorGeneral(.{}).init( alloc, "+foo", ); @@ -97,7 +97,7 @@ test "detect invalid match" { const alloc = testing.allocator; const Enum = enum { foo, bar, baz }; - var iter = try std.process.ArgIteratorGeneral(.{}).init( + var iter = try std.process.Args.IteratorGeneral(.{}).init( alloc, "+invalid", ); @@ -113,7 +113,7 @@ test "detect multiple actions" { const alloc = testing.allocator; const Enum = enum { foo, bar, baz }; - var iter = try std.process.ArgIteratorGeneral(.{}).init( + var iter = try std.process.Args.IteratorGeneral(.{}).init( alloc, "+foo +bar", ); @@ -129,7 +129,7 @@ test "detect no match" { const alloc = testing.allocator; const Enum = enum { foo, bar, baz }; - var iter = try std.process.ArgIteratorGeneral(.{}).init( + var iter = try std.process.Args.IteratorGeneral(.{}).init( alloc, "--some-flag", ); @@ -154,7 +154,7 @@ test "detect special case action" { }; { - var iter = try std.process.ArgIteratorGeneral(.{}).init( + var iter = try std.process.Args.IteratorGeneral(.{}).init( alloc, "--special +bar", ); @@ -164,7 +164,7 @@ test "detect special case action" { } { - var iter = try std.process.ArgIteratorGeneral(.{}).init( + var iter = try std.process.Args.IteratorGeneral(.{}).init( alloc, "+bar --special", ); @@ -174,7 +174,7 @@ test "detect special case action" { } { - var iter = try std.process.ArgIteratorGeneral(.{}).init( + var iter = try std.process.Args.IteratorGeneral(.{}).init( alloc, "+bar", ); @@ -200,7 +200,7 @@ test "detect special case fallback" { }; { - var iter = try std.process.ArgIteratorGeneral(.{}).init( + var iter = try std.process.Args.IteratorGeneral(.{}).init( alloc, "--special", ); @@ -210,7 +210,7 @@ test "detect special case fallback" { } { - var iter = try std.process.ArgIteratorGeneral(.{}).init( + var iter = try std.process.Args.IteratorGeneral(.{}).init( alloc, "+bar --special", ); @@ -220,7 +220,7 @@ test "detect special case fallback" { } { - var iter = try std.process.ArgIteratorGeneral(.{}).init( + var iter = try std.process.Args.IteratorGeneral(.{}).init( alloc, "--special +bar", ); @@ -246,7 +246,7 @@ test "detect special case abort_if_no_action" { }; { - var iter = try std.process.ArgIteratorGeneral(.{}).init( + var iter = try std.process.Args.IteratorGeneral(.{}).init( alloc, "-e", ); @@ -256,7 +256,7 @@ test "detect special case abort_if_no_action" { } { - var iter = try std.process.ArgIteratorGeneral(.{}).init( + var iter = try std.process.Args.IteratorGeneral(.{}).init( alloc, "+foo -e", ); @@ -266,7 +266,7 @@ test "detect special case abort_if_no_action" { } { - var iter = try std.process.ArgIteratorGeneral(.{}).init( + var iter = try std.process.Args.IteratorGeneral(.{}).init( alloc, "-e +bar", ); diff --git a/vendor/libghostty-vt/src/cli/args.zig b/vendor/libghostty-vt/src/cli/args.zig index bd5060d6..38616fc0 100644 --- a/vendor/libghostty-vt/src/cli/args.zig +++ b/vendor/libghostty-vt/src/cli/args.zig @@ -489,18 +489,13 @@ pub fn parseTaggedUnion(comptime T: type, alloc: Allocator, v: []const u8) !T { // We need to create a struct that looks like this union field. // This lets us use parseIntoField as if its a dedicated struct. - const Target = @Type(.{ .@"struct" = .{ - .layout = .auto, - .fields = &.{.{ - .name = field.name, - .type = field.type, - .default_value_ptr = null, - .is_comptime = false, - .alignment = @alignOf(field.type), - }}, - .decls = &.{}, - .is_tuple = false, - } }); + const Target = @Struct( + .auto, + null, + &.{field.name}, + &.{field.type}, + &.{.{ .@"align" = @alignOf(field.type) }}, + ); // Parse the value into the struct var t: Target = undefined; @@ -677,7 +672,7 @@ test "parse: simple" { } = .{}; defer if (data._arena) |arena| arena.deinit(); - var iter = try std.process.ArgIteratorGeneral(.{}).init( + var iter = try std.process.Args.IteratorGeneral(.{}).init( testing.allocator, "--a=42 --b --b-f=false", ); @@ -689,7 +684,7 @@ test "parse: simple" { try testing.expect(!data.@"b-f"); // Reparsing works - var iter2 = try std.process.ArgIteratorGeneral(.{}).init( + var iter2 = try std.process.Args.IteratorGeneral(.{}).init( testing.allocator, "--a=84", ); @@ -711,7 +706,7 @@ test "parse: quoted value" { } = .{}; defer if (data._arena) |arena| arena.deinit(); - var iter = try std.process.ArgIteratorGeneral(.{}).init( + var iter = try std.process.Args.IteratorGeneral(.{}).init( testing.allocator, "--a=\"42\" --b=\"hello!\"", ); @@ -731,7 +726,7 @@ test "parse: empty value resets to default" { } = .{}; defer if (data._arena) |arena| arena.deinit(); - var iter = try std.process.ArgIteratorGeneral(.{}).init( + var iter = try std.process.Args.IteratorGeneral(.{}).init( testing.allocator, "--a= --b=", ); @@ -750,7 +745,7 @@ test "parse: positional arguments are invalid" { } = .{}; defer if (data._arena) |arena| arena.deinit(); - var iter = try std.process.ArgIteratorGeneral(.{}).init( + var iter = try std.process.Args.IteratorGeneral(.{}).init( testing.allocator, "--a=84 what", ); @@ -774,7 +769,7 @@ test "parse: diagnostic tracking" { } = .{}; defer if (data._arena) |arena| arena.deinit(); - var iter = try std.process.ArgIteratorGeneral(.{}).init( + var iter = try std.process.Args.IteratorGeneral(.{}).init( testing.allocator, "--what --a=42", ); @@ -858,7 +853,7 @@ test "parse: compatibility handler" { } = .{}; defer if (data._arena) |arena| arena.deinit(); - var iter = try std.process.ArgIteratorGeneral(.{}).init( + var iter = try std.process.Args.IteratorGeneral(.{}).init( testing.allocator, "--a=yuh", ); @@ -884,7 +879,7 @@ test "parse: compatibility renamed" { } = .{}; defer if (data._arena) |arena| arena.deinit(); - var iter = try std.process.ArgIteratorGeneral(.{}).init( + var iter = try std.process.Args.IteratorGeneral(.{}).init( testing.allocator, "--old=true --b=true", ); @@ -1363,8 +1358,11 @@ pub fn ArgsIterator(comptime Iterator: type) type { } /// Create an args iterator for the process args. This will skip argv0. -pub fn argsIterator(alloc_gpa: Allocator) internal_os.args.ArgIterator.InitError!ArgsIterator(internal_os.args.ArgIterator) { - var iter = try internal_os.args.iterator(alloc_gpa); +pub fn argsIterator( + alloc_gpa: Allocator, + args: std.process.Args, +) std.process.Args.Iterator.InitError!ArgsIterator(std.process.Args.Iterator) { + var iter: std.process.Args.Iterator = try .initAllocator(args, alloc_gpa); errdefer iter.deinit(); _ = iter.next(); // skip argv0 return .{ .iterator = iter }; @@ -1373,7 +1371,7 @@ pub fn argsIterator(alloc_gpa: Allocator) internal_os.args.ArgIterator.InitError test "ArgsIterator" { const testing = std.testing; - const child = try std.process.ArgIteratorGeneral(.{}).init( + const child = try std.process.Args.IteratorGeneral(.{}).init( testing.allocator, "--what +list-things --a=42", ); @@ -1456,8 +1454,15 @@ pub const LineIterator = struct { entry = entry[0..trim.len]; } - // Ignore blank lines and comments - if (entry.len == 0 or entry[0] == '#') continue; + // Ignore blank lines and comments. If this line consumed the + // remaining buffer, refill before the loop condition checks for + // more data. + if (entry.len == 0 or entry[0] == '#') { + if (self.r.seek == self.r.end) { + self.r.fillMore() catch {}; + } + continue; + } break entry; } else return null; @@ -1625,3 +1630,27 @@ test "LineIterator with buffered and primed reader" { try testing.expectEqual(@as(?[]const u8, null), iter.next()); try testing.expectEqual(@as(?[]const u8, null), iter.next()); } + +test "LineIterator refills after ignored line at buffer boundary" { + const testing = std.testing; + + { + var f: std.Io.Reader = .fixed("#\nA\n"); + var buf: [2]u8 = undefined; + var r = f.limited(.unlimited, &buf); + var iter: LineIterator = .init(&r.interface); + + try testing.expectEqualStrings("--A", iter.next().?); + try testing.expectEqual(@as(?[]const u8, null), iter.next()); + } + + { + var f: std.Io.Reader = .fixed("\nA\n"); + var buf: [1]u8 = undefined; + var r = f.limited(.unlimited, &buf); + var iter: LineIterator = .init(&r.interface); + + try testing.expectEqualStrings("--A", iter.next().?); + try testing.expectEqual(@as(?[]const u8, null), iter.next()); + } +} diff --git a/vendor/libghostty-vt/src/cli/boo.zig b/vendor/libghostty-vt/src/cli/boo.zig index 2834eadb..a33d56ae 100644 --- a/vendor/libghostty-vt/src/cli/boo.zig +++ b/vendor/libghostty-vt/src/cli/boo.zig @@ -3,6 +3,7 @@ const builtin = @import("builtin"); const args = @import("args.zig"); const Action = @import("ghostty.zig").Action; const Allocator = std.mem.Allocator; +const global = @import("../global.zig"); const vaxis = @import("vaxis"); const framedata = @import("framedata").compressed; @@ -173,6 +174,9 @@ const Boo = struct { /// The `boo` command is used to display the animation from the Ghostty website in the terminal pub fn run(gpa: Allocator) !u8 { + var env_map = try global.environMap(); + defer env_map.deinit(); + // Disable on non-desktop systems. switch (builtin.os.tag) { .windows, .macos, .linux, .freebsd => {}, @@ -183,7 +187,7 @@ pub fn run(gpa: Allocator) !u8 { defer opts.deinit(); { - var iter = try args.argsIterator(gpa); + var iter = try args.argsIterator(gpa, global.args()); defer iter.deinit(); try args.parse(Options, gpa, &opts, &iter); } @@ -194,7 +198,7 @@ pub fn run(gpa: Allocator) !u8 { gpa.free(decompressed_data); } - var app = try vxfw.App.init(gpa); + var app = try vxfw.App.init(global.io(), gpa, &env_map, &.{}); defer app.deinit(); var boo: Boo = undefined; diff --git a/vendor/libghostty-vt/src/cli/crash_report.zig b/vendor/libghostty-vt/src/cli/crash_report.zig index f0940fda..e189ddae 100644 --- a/vendor/libghostty-vt/src/cli/crash_report.zig +++ b/vendor/libghostty-vt/src/cli/crash_report.zig @@ -4,6 +4,7 @@ const args = @import("args.zig"); const Action = @import("ghostty.zig").Action; const Config = @import("../config.zig").Config; const crash = @import("../crash/main.zig"); +const global = @import("../global.zig"); pub const Options = struct { pub fn deinit(self: Options) void { @@ -33,32 +34,36 @@ pub fn run(alloc_gpa: Allocator) !u8 { defer opts.deinit(); { - var iter = try args.argsIterator(alloc_gpa); + var iter = try args.argsIterator(alloc_gpa, global.args()); defer iter.deinit(); try args.parse(Options, alloc_gpa, &opts, &iter); } var buffer: [1024]u8 = undefined; - var stdout_file: std.fs.File = .stdout(); - var stdout_writer = stdout_file.writer(&buffer); + var stdout_file: std.Io.File = .stdout(); + var stdout_writer = stdout_file.writer(global.io(), &buffer); const stdout = &stdout_writer.interface; - const result = runInner(alloc, &stdout_file, stdout); + var environ_map = try global.environMap(); + defer environ_map.deinit(); + const result = runInner(global.io(), alloc, &environ_map, &stdout_file, stdout); stdout.flush() catch {}; return result; } fn runInner( + io: std.Io, alloc: Allocator, - stdout_file: *std.fs.File, + environ_map: *const std.process.Environ.Map, + stdout_file: *std.Io.File, stdout: *std.Io.Writer, ) !u8 { - const crash_dir = try crash.defaultDir(alloc); + const crash_dir = try crash.defaultDir(io, alloc, environ_map); var reports: std.ArrayList(crash.Report) = .empty; errdefer reports.deinit(alloc); - var it = try crash_dir.iterator(); - while (try it.next()) |report| try reports.append(alloc, .{ + var it = try crash_dir.iterator(io); + while (try it.next(io)) |report| try reports.append(alloc, .{ .name = try alloc.dupe(u8, report.name), .mtime = report.mtime, }); @@ -66,7 +71,7 @@ fn runInner( // If we have no reports, then we're done. If we have a tty then we // print a message, otherwise we do nothing. if (reports.items.len == 0) { - if (std.posix.isatty(stdout_file.handle)) { + if (try stdout_file.isTty(global.io())) { try stdout.writeAll("No crash reports! 👻\n"); } return 0; @@ -76,7 +81,7 @@ fn runInner( for (reports.items) |report| { var buf: [128]u8 = undefined; - const now = std.time.nanoTimestamp(); + const now = std.Io.Timestamp.now(global.io(), .real).toNanoseconds(); const diff = now - report.mtime; const since = if (diff <= 0) "now" else s: { const d = Config.Duration{ .duration = @intCast(diff) }; diff --git a/vendor/libghostty-vt/src/cli/diagnostics.zig b/vendor/libghostty-vt/src/cli/diagnostics.zig index 7f4dcc45..0e4048a0 100644 --- a/vendor/libghostty-vt/src/cli/diagnostics.zig +++ b/vendor/libghostty-vt/src/cli/diagnostics.zig @@ -93,7 +93,7 @@ pub const Location = union(enum) { /// and potentially in the future structure them differently. pub const DiagnosticList = struct { /// The list of diagnostics. - list: std.ArrayListUnmanaged(Diagnostic) = .{}, + list: std.ArrayList(Diagnostic) = .empty, /// Precomputed data for diagnostics. This is used specifically /// when we build libghostty so that we can precompute the messages @@ -111,7 +111,7 @@ pub const DiagnosticList = struct { }; const Precompute = if (precompute_enabled) struct { - messages: std.ArrayListUnmanaged([:0]const u8) = .{}, + messages: std.ArrayList([:0]const u8) = .empty, pub fn clone( self: *const Precompute, diff --git a/vendor/libghostty-vt/src/cli/edit_config.zig b/vendor/libghostty-vt/src/cli/edit_config.zig index c08651a0..cc6aec6b 100644 --- a/vendor/libghostty-vt/src/cli/edit_config.zig +++ b/vendor/libghostty-vt/src/cli/edit_config.zig @@ -7,6 +7,7 @@ const Action = @import("ghostty.zig").Action; const configpkg = @import("../config.zig"); const internal_os = @import("../os/main.zig"); const Config = configpkg.Config; +const global = @import("../global.zig"); pub const Options = struct { pub fn deinit(self: Options) void { @@ -48,14 +49,14 @@ pub fn run(alloc: Allocator) !u8 { // critical where setting up the defer cleanup is a problem. var buffer: [1024]u8 = undefined; - var stderr_writer = std.fs.File.stderr().writer(&buffer); + var stderr_writer = std.Io.File.stderr().writer(global.io(), &buffer); const stderr = &stderr_writer.interface; var opts: Options = .{}; defer opts.deinit(); { - var iter = try args.argsIterator(alloc); + var iter = try args.argsIterator(alloc, global.args()); defer iter.deinit(); try args.parse(Options, alloc, &opts, &iter); } @@ -67,6 +68,12 @@ pub fn run(alloc: Allocator) !u8 { } fn runInner(alloc: Allocator, stderr: *std.Io.Writer) !u8 { + // We require libc because we want to use std.c.environ for envp + // and not have to build that ourselves. We can remove this + // limitation later but Ghostty already heavily requires libc + // so this is not a big deal. + comptime assert(builtin.link_libc); + // We load the configuration once because that will write our // default configuration files to disk. We don't use the config. var config = try Config.load(alloc); @@ -82,99 +89,74 @@ fn runInner(alloc: Allocator, stderr: *std.Io.Writer) !u8 { \\The `ghostty +edit-config` command is not supported on Windows. \\Please edit the configuration file manually at the following path: \\ - \\{s} - \\ - , - .{path}, - ); - return 1; - } - - // Get our editor - const get_env_: ?internal_os.GetEnvResult = env: { - // VISUAL vs. EDITOR: https://unix.stackexchange.com/questions/4859/visual-vs-editor-what-s-the-difference - if (try internal_os.getenv(alloc, "VISUAL")) |v| { - if (v.value.len > 0) break :env v; - v.deinit(alloc); - } - - if (try internal_os.getenv(alloc, "EDITOR")) |v| { - if (v.value.len > 0) break :env v; - v.deinit(alloc); - } - - break :env null; - }; - defer if (get_env_) |v| v.deinit(alloc); - const editor: []const u8 = if (get_env_) |v| v.value else ""; - - // If we don't have `$EDITOR` set then we can't do anything - // but we can still print a helpful message. - if (editor.len == 0) { - try stderr.print( - \\The $EDITOR or $VISUAL environment variable is not set or is empty. - \\This environment variable is required to edit the Ghostty configuration - \\via this CLI command. - \\ - \\Please set the environment variable to your preferred terminal - \\text editor and try again. - \\ - \\If you prefer to edit the configuration file another way, - \\you can find the configuration file at the following path: - \\ \\ , .{}, ); - - // Output the path using the OSC8 sequence so that it is linked. - try stderr.print( - "\x1b]8;;file://{s}\x1b\\{s}\x1b]8;;\x1b\\\n", - .{ path, path }, - ); - return 1; } - const command = command: { - var buffer: std.io.Writer.Allocating = .init(alloc); - defer buffer.deinit(); - const writer = &buffer.writer; - try writer.writeAll(editor); - try writer.writeByte(' '); - { - var sh: internal_os.ShellEscapeWriter = .init(writer); - try sh.writer.writeAll(path); - try sh.writer.flush(); + const command = internal_os.getConfigEditCommand(alloc, path, .{ .default_editor = .failure }) catch |err| { + switch (err) { + error.NoEditorConfigured => { + try stderr.print( + \\The $EDITOR or $VISUAL environment variable is not set or is empty. + \\This environment variable is required to edit the Ghostty configuration + \\via this CLI command. + \\ + \\Please set the environment variable to your preferred terminal + \\text editor and try again. + \\ + \\If you prefer to edit the configuration file another way, + \\you can find the configuration file at the following path: + \\ + \\ + , + .{}, + ); + + // Output the path using the OSC8 sequence so that it is linked. + try stderr.print( + "\x1b]8;;file://{s}\x1b\\{s}\x1b]8;;\x1b\\\n", + .{ path, path }, + ); + + return 1; + }, + error.WriteFailed, + error.OutOfMemory, + => |e| return e, } - try writer.flush(); - break :command try buffer.toOwnedSliceSentinel(0); }; defer alloc.free(command); - // We require libc because we want to use std.c.environ for envp - // and not have to build that ourselves. We can remove this - // limitation later but Ghostty already heavily requires libc - // so this is not a big deal. - comptime assert(builtin.link_libc); - - const err = std.posix.execvpeZ( + // Run/replace process (using execve) + const argv = [_:null]?[*:0]const u8{ "/bin/sh", - &.{ "/bin/sh", "-c", command }, - std.c.environ, - ); + "-c", + command.ptr, + }; + const envp = std.c.environ; + const err = std.posix.errno(std.posix.system.execve(argv[0].?, &argv, envp)); // If we reached this point then exec failed. try stderr.print( - \\Failed to execute the editor. Error code={}. + \\Failed to execute the editor (E{t}). \\ \\This is usually due to the executable path not existing, invalid \\permissions, or the shell environment not being set up \\correctly. \\ - \\Editor: {s} - \\Path: {s} + \\Command: {s} \\ - , .{ err, editor, path }); + , .{ + err, + command, + }); + // Output the path using the OSC8 sequence so that it is linked. + try stderr.print( + "Path: \x1b]8;;file://{s}\x1b\\{s}\x1b]8;;\x1b\\\n", + .{ path, path }, + ); return 1; } diff --git a/vendor/libghostty-vt/src/cli/explain_config.zig b/vendor/libghostty-vt/src/cli/explain_config.zig index 4f034afe..6f03256d 100644 --- a/vendor/libghostty-vt/src/cli/explain_config.zig +++ b/vendor/libghostty-vt/src/cli/explain_config.zig @@ -7,6 +7,7 @@ const Config = @import("../config/Config.zig"); const ConfigKey = @import("../config/key.zig").Key; const KeybindAction = @import("../input/Binding.zig").Action; const Pager = @import("Pager.zig"); +const global = @import("../global.zig"); pub const Options = struct { /// The config option to explain. For example: @@ -51,7 +52,7 @@ pub fn run(alloc: Allocator) !u8 { var positional: ?[]const u8 = null; var no_pager: bool = false; - var iter = try args.argsIterator(alloc); + var iter = try args.argsIterator(alloc, global.args()); defer iter.deinit(); defer if (option_name) |s| alloc.free(s); defer if (keybind_name) |s| alloc.free(s); @@ -75,9 +76,9 @@ pub fn run(alloc: Allocator) !u8 { // respective lookup. A bare positional argument tries config // options first, then keybind actions as a fallback. const name = keybind_name orelse option_name orelse positional orelse { - var stderr: std.fs.File = .stderr(); + var stderr: std.Io.File = .stderr(); var buffer: [4096]u8 = undefined; - var stderr_writer = stderr.writer(&buffer); + var stderr_writer = stderr.writer(global.io(), &buffer); try stderr_writer.interface.writeAll("Usage: ghostty +explain-config <option>\n"); try stderr_writer.interface.writeAll(" ghostty +explain-config --option=<option>\n"); try stderr_writer.interface.writeAll(" ghostty +explain-config --keybind=<action>\n"); @@ -92,7 +93,7 @@ pub fn run(alloc: Allocator) !u8 { else explainOption(name) orelse explainKeybind(name); - var pager: Pager = if (!no_pager) .init(alloc) else .{}; + var pager: Pager = if (!no_pager) .init() else .{}; defer pager.deinit(); var buffer: [4096]u8 = undefined; const writer = pager.writer(&buffer); diff --git a/vendor/libghostty-vt/src/cli/ghostty.zig b/vendor/libghostty-vt/src/cli/ghostty.zig index 6e836851..572aa8ad 100644 --- a/vendor/libghostty-vt/src/cli/ghostty.zig +++ b/vendor/libghostty-vt/src/cli/ghostty.zig @@ -21,7 +21,9 @@ const crash_report = @import("crash_report.zig"); const show_face = @import("show_face.zig"); const boo = @import("boo.zig"); const new_window = @import("new_window.zig"); +const new_tab = @import("new_tab.zig"); const toggle_quick_terminal = @import("toggle_quick_terminal.zig"); +const global = @import("../global.zig"); /// Special commands that can be invoked via CLI flags. These are all /// invoked by using `+<action>` as a CLI flag. The only exception is @@ -78,6 +80,9 @@ pub const Action = enum { // Use IPC to tell the running Ghostty to open a new window. @"new-window", + // Use IPC to tell the running Ghostty to open a new tab. + @"new-tab", + // Use IPC to tell the running Ghostty to toggle the quick terminal. @"toggle-quick-terminal", @@ -120,7 +125,10 @@ pub const Action = enum { if (std.mem.eql(u8, field.name, @tagName(self))) { var buffer: [1024]u8 = undefined; - var stdout_writer = std.fs.File.stdout().writer(&buffer); + var stdout_writer = std.Io.File.stdout().writer( + global.io(), + &buffer, + ); const stdout = &stdout_writer.interface; const text = @field(help_strings.Action, field.name) ++ "\n"; stdout.writeAll(text) catch |write_err| { @@ -161,6 +169,7 @@ pub const Action = enum { .@"show-face" => try show_face.run(alloc), .boo => try boo.run(alloc), .@"new-window" => try new_window.run(alloc), + .@"new-tab" => try new_tab.run(alloc), .@"toggle-quick-terminal" => try toggle_quick_terminal.run(alloc), }; } @@ -203,6 +212,7 @@ pub const Action = enum { .@"show-face" => show_face.Options, .boo => boo.Options, .@"new-window" => new_window.Options, + .@"new-tab" => new_tab.Options, .@"toggle-quick-terminal" => toggle_quick_terminal.Options, }; } @@ -213,7 +223,7 @@ test "parse action none" { const testing = std.testing; const alloc = testing.allocator; - var iter = try std.process.ArgIteratorGeneral(.{}).init( + var iter = try std.process.Args.IteratorGeneral(.{}).init( alloc, "--a=42 --b --b-f=false", ); @@ -227,7 +237,7 @@ test "parse action version" { const alloc = testing.allocator; { - var iter = try std.process.ArgIteratorGeneral(.{}).init( + var iter = try std.process.Args.IteratorGeneral(.{}).init( alloc, "--a=42 --b --b-f=false --version", ); @@ -237,7 +247,7 @@ test "parse action version" { } { - var iter = try std.process.ArgIteratorGeneral(.{}).init( + var iter = try std.process.Args.IteratorGeneral(.{}).init( alloc, "--version --a=42 --b --b-f=false", ); @@ -247,7 +257,7 @@ test "parse action version" { } { - var iter = try std.process.ArgIteratorGeneral(.{}).init( + var iter = try std.process.Args.IteratorGeneral(.{}).init( alloc, "--c=84 --d --version --a=42 --b --b-f=false", ); @@ -262,7 +272,7 @@ test "parse action plus" { const alloc = testing.allocator; { - var iter = try std.process.ArgIteratorGeneral(.{}).init( + var iter = try std.process.Args.IteratorGeneral(.{}).init( alloc, "--a=42 --b --b-f=false +version", ); @@ -272,7 +282,7 @@ test "parse action plus" { } { - var iter = try std.process.ArgIteratorGeneral(.{}).init( + var iter = try std.process.Args.IteratorGeneral(.{}).init( alloc, "+version --a=42 --b --b-f=false", ); @@ -282,7 +292,7 @@ test "parse action plus" { } { - var iter = try std.process.ArgIteratorGeneral(.{}).init( + var iter = try std.process.Args.IteratorGeneral(.{}).init( alloc, "--c=84 --d +version --a=42 --b --b-f=false", ); @@ -297,7 +307,7 @@ test "parse action plus ignores -e" { const alloc = testing.allocator; { - var iter = try std.process.ArgIteratorGeneral(.{}).init( + var iter = try std.process.Args.IteratorGeneral(.{}).init( alloc, "--a=42 -e +version", ); @@ -307,7 +317,7 @@ test "parse action plus ignores -e" { } { - var iter = try std.process.ArgIteratorGeneral(.{}).init( + var iter = try std.process.Args.IteratorGeneral(.{}).init( alloc, "+list-fonts --a=42 -e +version", ); diff --git a/vendor/libghostty-vt/src/cli/help.zig b/vendor/libghostty-vt/src/cli/help.zig index a2b4dde8..ee67483a 100644 --- a/vendor/libghostty-vt/src/cli/help.zig +++ b/vendor/libghostty-vt/src/cli/help.zig @@ -2,6 +2,7 @@ const std = @import("std"); const Allocator = std.mem.Allocator; const args = @import("args.zig"); const Action = @import("ghostty.zig").Action; +const global = @import("../global.zig"); // Note that this options struct doesn't implement the `help` decl like other // actions. That is because the help command is special and wants to handle its @@ -16,7 +17,7 @@ pub const Options = struct { }; /// The `help` command shows general help about Ghostty. Recognized as either -/// `-h, `--help`, or like other actions `+help`. +/// `-h`, `--help`, or like other actions `+help`. /// /// You can also specify `--help` or `-h` along with any action such as /// `+list-themes` to see help for a specific action. @@ -25,13 +26,13 @@ pub fn run(alloc: Allocator) !u8 { defer opts.deinit(); { - var iter = try args.argsIterator(alloc); + var iter = try args.argsIterator(alloc, global.args()); defer iter.deinit(); try args.parse(Options, alloc, &opts, &iter); } var buffer: [2048]u8 = undefined; - var stdout_writer = std.fs.File.stdout().writer(&buffer); + var stdout_writer = std.Io.File.stdout().writer(global.io(), &buffer); const stdout = &stdout_writer.interface; try stdout.writeAll( \\Usage: ghostty [+action] [options] diff --git a/vendor/libghostty-vt/src/cli/list_actions.zig b/vendor/libghostty-vt/src/cli/list_actions.zig index aaef1195..ce4af5c4 100644 --- a/vendor/libghostty-vt/src/cli/list_actions.zig +++ b/vendor/libghostty-vt/src/cli/list_actions.zig @@ -3,6 +3,7 @@ const args = @import("args.zig"); const Action = @import("ghostty.zig").Action; const Allocator = std.mem.Allocator; const helpgen_actions = @import("../input/helpgen_actions.zig"); +const global = @import("../global.zig"); pub const Options = struct { /// If `true`, print out documentation about the action associated with the @@ -32,14 +33,14 @@ pub fn run(alloc: Allocator) !u8 { defer opts.deinit(); { - var iter = try args.argsIterator(alloc); + var iter = try args.argsIterator(alloc, global.args()); defer iter.deinit(); try args.parse(Options, alloc, &opts, &iter); } - var stdout: std.fs.File = .stdout(); + var stdout: std.Io.File = .stdout(); var buffer: [4096]u8 = undefined; - var stdout_writer = stdout.writer(&buffer); + var stdout_writer = stdout.writer(global.io(), &buffer); try helpgen_actions.generate( &stdout_writer.interface, .plaintext, diff --git a/vendor/libghostty-vt/src/cli/list_colors.zig b/vendor/libghostty-vt/src/cli/list_colors.zig index 50c12a69..cd76a1fb 100644 --- a/vendor/libghostty-vt/src/cli/list_colors.zig +++ b/vendor/libghostty-vt/src/cli/list_colors.zig @@ -6,6 +6,7 @@ const args = @import("args.zig"); const x11_color = @import("../terminal/main.zig").x11_color; const vaxis = @import("vaxis"); const tui = @import("tui.zig"); +const global = @import("../global.zig"); pub const Options = struct { pub fn deinit(self: Options) void { @@ -34,7 +35,7 @@ pub fn run(alloc: Allocator) !u8 { defer opts.deinit(); { - var iter = try args.argsIterator(alloc); + var iter = try args.argsIterator(alloc, global.args()); defer iter.deinit(); try args.parse(Options, alloc, &opts, &iter); } @@ -49,15 +50,14 @@ pub fn run(alloc: Allocator) !u8 { } }.lessThan); - // Despite being under the posix namespace, this also works on Windows as of zig 0.13.0 - var stdout: std.fs.File = .stdout(); - if (tui.can_pretty_print and !opts.plain and std.posix.isatty(stdout.handle)) { + var stdout: std.Io.File = .stdout(); + if (tui.can_pretty_print and !opts.plain and try stdout.isTty(global.io())) { var arena = std.heap.ArenaAllocator.init(alloc); defer arena.deinit(); return prettyPrint(arena.allocator(), keys.items); } else { var buffer: [4096]u8 = undefined; - var stdout_writer = stdout.writer(&buffer); + var stdout_writer = stdout.writer(global.io(), &buffer); const writer = &stdout_writer.interface; for (keys.items) |name| { const rgb = x11_color.map.get(name).?; @@ -74,11 +74,14 @@ pub fn run(alloc: Allocator) !u8 { } fn prettyPrint(alloc: Allocator, keys: [][]const u8) !u8 { + var env_map = try global.environMap(); + defer env_map.deinit(); + // Set up vaxis var buf: [1024]u8 = undefined; - var tty = try vaxis.Tty.init(&buf); + var tty = try vaxis.Tty.init(global.io(), &buf); defer tty.deinit(); - var vx = try vaxis.init(alloc, .{}); + var vx = try vaxis.init(global.io(), alloc, &env_map, .{}); defer vx.deinit(alloc, tty.writer()); // We know we are ghostty, so let's enable mode 2027. Vaxis normally does this but you need an @@ -97,7 +100,7 @@ fn prettyPrint(alloc: Allocator, keys: [][]const u8) !u8 { .y_pixel = 768, }, - else => try vaxis.Tty.getWinsize(tty.fd), + else => try tty.getWinsize(), }; try vx.resize(alloc, tty.writer(), winsize); diff --git a/vendor/libghostty-vt/src/cli/list_fonts.zig b/vendor/libghostty-vt/src/cli/list_fonts.zig index 6e08aa83..7cfce715 100644 --- a/vendor/libghostty-vt/src/cli/list_fonts.zig +++ b/vendor/libghostty-vt/src/cli/list_fonts.zig @@ -4,6 +4,7 @@ const ArenaAllocator = std.heap.ArenaAllocator; const Action = @import("ghostty.zig").Action; const args = @import("args.zig"); const font = @import("../font/main.zig"); +const global = @import("../global.zig"); const log = std.log.scoped(.list_fonts); @@ -60,7 +61,7 @@ pub const Options = struct { /// is identical to the `font-family` set of Ghostty configuration values, so /// this can be used to debug why your desired font may not be loading. pub fn run(alloc: Allocator) !u8 { - var iter = try args.argsIterator(alloc); + var iter = try args.argsIterator(alloc, global.args()); defer iter.deinit(); return try runArgs(alloc, &iter); } @@ -78,7 +79,7 @@ fn runArgs(alloc_gpa: Allocator, argsIter: anytype) !u8 { // Its possible to build Ghostty without font discovery! if (comptime font.Discover == void) { var buffer: [1024]u8 = undefined; - var stderr_writer = std.fs.File.stderr().writer(&buffer); + var stderr_writer = std.Io.File.stderr().writer(global.io(), &buffer); const stderr = &stderr_writer.interface; try stderr.print( \\Ghostty was built without a font discovery mechanism. This is a compile-time @@ -92,7 +93,7 @@ fn runArgs(alloc_gpa: Allocator, argsIter: anytype) !u8 { } var buffer: [2048]u8 = undefined; - var stdout_writer = std.fs.File.stdout().writer(&buffer); + var stdout_writer = std.Io.File.stdout().writer(global.io(), &buffer); const stdout = &stdout_writer.interface; // We'll be putting our fonts into a list categorized by family @@ -133,7 +134,7 @@ fn runArgs(alloc_gpa: Allocator, argsIter: anytype) !u8 { const gop = try map.getOrPut(family); if (!gop.found_existing) { try families.append(alloc, family); - gop.value_ptr.* = .{}; + gop.value_ptr.* = .empty; } try gop.value_ptr.append(alloc, full_name); } diff --git a/vendor/libghostty-vt/src/cli/list_keybinds.zig b/vendor/libghostty-vt/src/cli/list_keybinds.zig index 61050d0c..f8a2663c 100644 --- a/vendor/libghostty-vt/src/cli/list_keybinds.zig +++ b/vendor/libghostty-vt/src/cli/list_keybinds.zig @@ -10,6 +10,7 @@ const vaxis = @import("vaxis"); const input = @import("../input.zig"); const tui = @import("tui.zig"); const Binding = input.Binding; +const global = @import("../global.zig"); pub const Options = struct { /// If `true`, print out the default keybinds instead of the ones configured @@ -56,7 +57,7 @@ pub fn run(alloc: Allocator) !u8 { defer opts.deinit(); { - var iter = try args.argsIterator(alloc); + var iter = try args.argsIterator(alloc, global.args()); defer iter.deinit(); try args.parse(Options, alloc, &opts, &iter); } @@ -65,11 +66,11 @@ pub fn run(alloc: Allocator) !u8 { defer config.deinit(); var buffer: [1024]u8 = undefined; - const stdout: std.fs.File = .stdout(); - var stdout_writer = stdout.writer(&buffer); + const stdout: std.Io.File = .stdout(); + var stdout_writer = stdout.writer(global.io(), &buffer); const writer = &stdout_writer.interface; - if (tui.can_pretty_print and !opts.plain and stdout.isTty()) { + if (tui.can_pretty_print and !opts.plain and try stdout.isTty(global.io())) { var arena = std.heap.ArenaAllocator.init(alloc); defer arena.deinit(); return prettyPrint(arena.allocator(), config.keybind); @@ -218,11 +219,14 @@ const ChordBinding = struct { }; fn prettyPrint(alloc: Allocator, keybinds: Config.Keybinds) !u8 { + var env_map = try global.environMap(); + defer env_map.deinit(); + // Set up vaxis var buf: [1024]u8 = undefined; - var tty = try vaxis.Tty.init(&buf); + var tty = try vaxis.Tty.init(global.io(), &buf); defer tty.deinit(); - var vx = try vaxis.init(alloc, .{}); + var vx = try vaxis.init(global.io(), alloc, &env_map, .{}); const writer = tty.writer(); defer vx.deinit(alloc, writer); @@ -242,7 +246,7 @@ fn prettyPrint(alloc: Allocator, keybinds: Config.Keybinds) !u8 { .y_pixel = 768, }, - else => try vaxis.Tty.getWinsize(tty.fd), + else => try tty.getWinsize(), }; try vx.resize(alloc, writer, winsize); diff --git a/vendor/libghostty-vt/src/cli/list_themes.zig b/vendor/libghostty-vt/src/cli/list_themes.zig index 42aff9d5..8fbf650b 100644 --- a/vendor/libghostty-vt/src/cli/list_themes.zig +++ b/vendor/libghostty-vt/src/cli/list_themes.zig @@ -5,7 +5,7 @@ const Config = @import("../config/Config.zig"); const configpkg = @import("../config.zig"); const themepkg = @import("../config/theme.zig"); const tui = @import("tui.zig"); -const global_state = &@import("../global.zig").state; +const global = @import("../global.zig"); const vaxis = @import("vaxis"); const zf = @import("zf"); @@ -108,7 +108,7 @@ pub fn run(gpa_alloc: std.mem.Allocator) !u8 { defer opts.deinit(); { - var iter = try args.argsIterator(gpa_alloc); + var iter = try args.argsIterator(gpa_alloc, global.args()); defer iter.deinit(); try args.parse(Options, gpa_alloc, &opts, &iter); } @@ -117,15 +117,15 @@ pub fn run(gpa_alloc: std.mem.Allocator) !u8 { const alloc = arena.allocator(); var stdout_buf: [4096]u8 = undefined; - var stdout_file: std.fs.File = .stdout(); - var stdout_writer = stdout_file.writer(&stdout_buf); + var stdout_file: std.Io.File = .stdout(); + var stdout_writer = stdout_file.writer(global.io(), &stdout_buf); const stdout = &stdout_writer.interface; var stderr_buf: [4096]u8 = undefined; - var stderr_writer = std.fs.File.stderr().writer(&stderr_buf); + var stderr_writer = std.Io.File.stderr().writer(global.io(), &stderr_buf); const stderr = &stderr_writer.interface; - const resources_dir = global_state.resources_dir.app(); + const resources_dir = global.resourcesDir().app(); if (resources_dir == null) try stderr.print("Could not find the Ghostty resources directory. Please ensure " ++ "that Ghostty is installed correctly.\n", .{}); @@ -137,18 +137,22 @@ pub fn run(gpa_alloc: std.mem.Allocator) !u8 { var it: themepkg.LocationIterator = .{ .arena_alloc = arena.allocator() }; while (try it.next()) |loc| { - var dir = std.fs.cwd().openDir(loc.dir, .{ .iterate = true }) catch |err| switch (err) { + var dir = std.Io.Dir.cwd().openDir( + global.io(), + loc.dir, + .{ .iterate = true }, + ) catch |err| switch (err) { error.FileNotFound => continue, else => { std.debug.print("error trying to open {s}: {}\n", .{ loc.dir, err }); continue; }, }; - defer dir.close(); + defer dir.close(global.io()); var walker = dir.iterate(); - while (try walker.next()) |entry| { + while (try walker.next(global.io())) |entry| { switch (entry.kind) { .file, .sym_link => { if (std.mem.eql(u8, entry.name, ".DS_Store")) @@ -174,7 +178,7 @@ pub fn run(gpa_alloc: std.mem.Allocator) !u8 { std.mem.sortUnstable(ThemeListElement, themes.items, {}, ThemeListElement.lessThan); - if (tui.can_pretty_print and !opts.plain and stdout_file.isTty()) { + if (tui.can_pretty_print and !opts.plain and try stdout_file.isTty(global.io())) { try preview(gpa_alloc, themes.items, opts.color); return 0; } @@ -210,14 +214,18 @@ fn writeAutoThemeFile(alloc: std.mem.Allocator, theme_name: []const u8) !void { defer alloc.free(auto_path); if (std.fs.path.dirname(auto_path)) |dir| { - try std.fs.cwd().makePath(dir); + try std.Io.Dir.cwd().createDirPath(global.io(), dir); } - var f = try std.fs.createFileAbsolute(auto_path, .{ .truncate = true }); - defer f.close(); + var f = try std.Io.Dir.createFileAbsolute( + global.io(), + auto_path, + .{ .truncate = true }, + ); + defer f.close(global.io()); var buf: [128]u8 = undefined; - var w = f.writer(&buf); + var w = f.writer(global.io(), &buf); try w.interface.print("theme = {s}\n", .{theme_name}); try w.interface.flush(); } @@ -233,6 +241,7 @@ const Preview = struct { allocator: std.mem.Allocator, should_quit: bool, tty: vaxis.Tty, + env_map: std.process.Environ.Map, vx: vaxis.Vaxis, mouse: ?vaxis.Mouse, themes: []ThemeListElement, @@ -257,12 +266,14 @@ const Preview = struct { buf: []u8, ) !*Preview { const self = try allocator.create(Preview); + errdefer allocator.destroy(self); self.* = .{ .allocator = allocator, .should_quit = false, - .tty = try .init(buf), - .vx = try vaxis.init(allocator, .{}), + .tty = try .init(global.io(), buf), + .env_map = try global.environMap(), + .vx = undefined, .mouse = null, .themes = themes, .filtered = try .initCapacity(allocator, themes.len), @@ -274,6 +285,7 @@ const Preview = struct { .text_input = .init(allocator), .theme_filter = theme_filter, }; + self.vx = try vaxis.init(global.io(), allocator, &self.env_map, .{}); try self.updateFiltered(); @@ -285,23 +297,20 @@ const Preview = struct { self.filtered.deinit(allocator); self.text_input.deinit(); self.vx.deinit(allocator, self.tty.writer()); + self.env_map.deinit(); self.tty.deinit(); allocator.destroy(self); } pub fn run(self: *Preview) !void { - var loop: vaxis.Loop(Event) = .{ - .tty = &self.tty, - .vaxis = &self.vx, - }; - try loop.init(); + var loop: vaxis.Loop(Event) = .init(global.io(), &self.tty, &self.vx); try loop.start(); + defer loop.stop(); const writer = self.tty.writer(); - try self.vx.enterAltScreen(writer); try self.vx.setTitle(writer, "👻 Ghostty Theme Preview 👻"); - try self.vx.queryTerminal(writer, 1 * std.time.ns_per_s); + try self.vx.queryTerminal(writer, .fromSeconds(1)); try self.vx.setMouseMode(writer, true); if (self.vx.caps.color_scheme_updates) try self.vx.subscribeToColorSchemeUpdates(writer); @@ -311,8 +320,8 @@ const Preview = struct { defer arena.deinit(); const alloc = arena.allocator(); - loop.pollEvent(); - while (loop.tryEvent()) |event| { + try loop.pollEvent(); + while (try loop.tryEvent()) |event| { try self.update(event, alloc); } try self.draw(alloc); @@ -366,7 +375,12 @@ const Preview = struct { if (!shouldIncludeTheme(self.theme_filter, theme_config)) continue; theme.rank = zf.rank(theme.theme, tokens.items, .{ - .to_lower = true, + // NOTE: Changed from ".to_lower = true" (the option was + // renamed). I think this is the correct analog for case + // insensitive ranking (which is what I'm guessing + // ".to_lower = true" implies, but this comment serves as a + // hint if there's a regression. + .case_sensitive = false, .plain = true, }); if (theme.rank != null) try self.filtered.append(self.allocator, i); @@ -450,9 +464,9 @@ const Preview = struct { self.text_input.buf.clearRetainingCapacity(); try self.updateFiltered(); } - if (key.matchesAny(&.{ vaxis.Key.home, vaxis.Key.kp_home }, .{})) + if (key.matchesAny(&.{ vaxis.Key.home, vaxis.Key.kp_home, 'g' }, .{})) self.current = 0; - if (key.matchesAny(&.{ vaxis.Key.end, vaxis.Key.kp_end }, .{})) + if (key.matchesAny(&.{ vaxis.Key.end, vaxis.Key.kp_end, 'G' }, .{})) self.current = self.filtered.items.len - 1; if (key.matchesAny(&.{ 'j', '+', vaxis.Key.down, vaxis.Key.kp_down, vaxis.Key.kp_add }, .{})) self.down(1); @@ -620,13 +634,21 @@ const Preview = struct { self.down(1); } if (theme_list.hasMouse(mouse)) |_| { + // NOTE: mouse co-ordinates can be negative (see + // https://github.com/rockorager/libvaxis/pull/276). + // Working around it in this case, but putting this here in + // case there are issues. if (mouse.button == .left and mouse.type == .release) { - const selection = self.window + mouse.row; + const selection: usize = selection: { + var window: i32 = @min(self.window, std.math.maxInt(i32)); + window += mouse.row; + break :selection @max(0, window); + }; if (selection < self.filtered.items.len) { self.current = selection; } } - highlight = mouse.row; + highlight = @max(0, mouse.row); } } } @@ -757,8 +779,8 @@ const Preview = struct { .{ .keys = "d", .help = "Show palette numbers in decimal." }, .{ .keys = "c", .help = "Copy theme name to the clipboard." }, .{ .keys = "C", .help = "Copy theme path to the clipboard." }, - .{ .keys = "Home", .help = "Go to the start of the list." }, - .{ .keys = "End", .help = "Go to the end of the list." }, + .{ .keys = "Home, g", .help = "Go to the start of the list." }, + .{ .keys = "End, G", .help = "Go to the end of the list." }, .{ .keys = "/", .help = "Start search." }, .{ .keys = "^X, ^/", .help = "Clear search." }, .{ .keys = "⏎", .help = "Save theme or close search window." }, @@ -1183,7 +1205,7 @@ const Preview = struct { .x_off = x_off, .y_off = next_start, .width = width, - .height = 24, + .height = 28, }, ); const bold: vaxis.Style = .{ @@ -1360,7 +1382,9 @@ const Preview = struct { .{ .text = "pub ", .style = color5 }, .{ .text = "fn ", .style = color12 }, .{ .text = "main", .style = color2 }, - .{ .text = "() ", .style = standard }, + .{ .text = "(init: ", .style = standard }, + .{ .text = "std.process.Init", .style = color12 }, + .{ .text = ") ", .style = standard }, .{ .text = "!", .style = color5 }, .{ .text = "void", .style = color12 }, .{ .text = " {", .style = standard }, @@ -1373,10 +1397,11 @@ const Preview = struct { _ = child.print( &.{ .{ .text = " 4 │ ", .style = color238 }, - .{ .text = "const ", .style = color5 }, - .{ .text = "stdout ", .style = standard }, - .{ .text = "=", .style = color5 }, - .{ .text = " std.Io.getStdOut().writer();", .style = standard }, + .{ .text = "var ", .style = color5 }, + .{ .text = "buf:", .style = standard }, + .{ .text = " [1024]u8", .style = color12 }, + .{ .text = " =", .style = color5 }, + .{ .text = " undefined;", .style = standard }, }, .{ .row_offset = 7, @@ -1387,6 +1412,41 @@ const Preview = struct { &.{ .{ .text = " 5 │ ", .style = color238 }, .{ .text = "var ", .style = color5 }, + .{ .text = "stdout ", .style = standard }, + .{ .text = "=", .style = color5 }, + .{ .text = " std.Io.File.stdout().writer(init.io, &buf);", .style = standard }, + }, + .{ + .row_offset = 8, + .col_offset = 2, + }, + ); + _ = child.print( + &.{ + .{ .text = " 6 │ ", .style = color238 }, + .{ .text = "const ", .style = color5 }, + .{ .text = "w ", .style = standard }, + .{ .text = "=", .style = color5 }, + .{ .text = " &stdout.interface;", .style = standard }, + }, + .{ + .row_offset = 9, + .col_offset = 2, + }, + ); + _ = child.print( + &.{ + .{ .text = " 7 │", .style = color238 }, + }, + .{ + .row_offset = 10, + .col_offset = 2, + }, + ); + _ = child.print( + &.{ + .{ .text = " 8 │ ", .style = color238 }, + .{ .text = "var ", .style = color5 }, .{ .text = "i:", .style = standard }, .{ .text = " usize", .style = color12 }, .{ .text = " =", .style = color5 }, @@ -1394,13 +1454,13 @@ const Preview = struct { .{ .text = ";", .style = standard }, }, .{ - .row_offset = 8, + .row_offset = 11, .col_offset = 2, }, ); _ = child.print( &.{ - .{ .text = " 6 │ ", .style = color238 }, + .{ .text = " 9 │ ", .style = color238 }, .{ .text = "while ", .style = color5 }, .{ .text = "(i ", .style = standard }, .{ .text = "<= ", .style = color5 }, @@ -1411,13 +1471,13 @@ const Preview = struct { .{ .text = ") {", .style = standard }, }, .{ - .row_offset = 9, + .row_offset = 12, .col_offset = 2, }, ); _ = child.print( &.{ - .{ .text = " 7 │ ", .style = color238 }, + .{ .text = " 10 │ ", .style = color238 }, .{ .text = "if ", .style = color5 }, .{ .text = "(i ", .style = standard }, .{ .text = "% ", .style = color5 }, @@ -1427,28 +1487,28 @@ const Preview = struct { .{ .text = ") {", .style = standard }, }, .{ - .row_offset = 10, + .row_offset = 13, .col_offset = 2, }, ); _ = child.print( &.{ - .{ .text = " 8 │ ", .style = color238 }, + .{ .text = " 11 │ ", .style = color238 }, .{ .text = "try ", .style = color5 }, - .{ .text = "stdout.writeAll(", .style = standard }, + .{ .text = "w.writeAll(", .style = standard }, .{ .text = "\"ZiggZagg", .style = color10 }, .{ .text = "\\n", .style = color12 }, .{ .text = "\"", .style = color10 }, .{ .text = ");", .style = standard }, }, .{ - .row_offset = 11, + .row_offset = 14, .col_offset = 2, }, ); _ = child.print( &.{ - .{ .text = " 9 │ ", .style = color238 }, + .{ .text = " 12 │ ", .style = color238 }, .{ .text = "} ", .style = standard }, .{ .text = "else if ", .style = color5 }, .{ .text = "(i ", .style = standard }, @@ -1459,28 +1519,28 @@ const Preview = struct { .{ .text = ") {", .style = standard }, }, .{ - .row_offset = 12, + .row_offset = 15, .col_offset = 2, }, ); _ = child.print( &.{ - .{ .text = " 10 │ ", .style = color238 }, + .{ .text = " 13 │ ", .style = color238 }, .{ .text = "try ", .style = color5 }, - .{ .text = "stdout.writeAll(", .style = standard }, + .{ .text = "w.writeAll(", .style = standard }, .{ .text = "\"Zigg", .style = color10 }, .{ .text = "\\n", .style = color12 }, .{ .text = "\"", .style = color10 }, .{ .text = ");", .style = standard }, }, .{ - .row_offset = 13, + .row_offset = 16, .col_offset = 2, }, ); _ = child.print( &.{ - .{ .text = " 11 │ ", .style = color238 }, + .{ .text = " 14 │ ", .style = color238 }, .{ .text = "} ", .style = standard }, .{ .text = "else if ", .style = color5 }, .{ .text = "(i ", .style = standard }, @@ -1491,76 +1551,87 @@ const Preview = struct { .{ .text = ") {", .style = standard }, }, .{ - .row_offset = 14, + .row_offset = 17, .col_offset = 2, }, ); _ = child.print( &.{ - .{ .text = " 12 │ ", .style = color238 }, + .{ .text = " 15 │ ", .style = color238 }, .{ .text = "try ", .style = color5 }, - .{ .text = "stdout.writeAll(", .style = standard }, + .{ .text = "w.writeAll(", .style = standard }, .{ .text = "\"Zagg", .style = color10 }, .{ .text = "\\n", .style = color12 }, .{ .text = "\"", .style = color10 }, .{ .text = ");", .style = standard }, }, .{ - .row_offset = 15, + .row_offset = 18, .col_offset = 2, }, ); _ = child.print( &.{ - .{ .text = " 13 │ ", .style = color238 }, + .{ .text = " 16 │ ", .style = color238 }, .{ .text = "} ", .style = standard }, .{ .text = "else ", .style = color5 }, .{ .text = "{", .style = standard }, }, .{ - .row_offset = 16, + .row_offset = 19, .col_offset = 2, }, ); _ = child.print( &.{ - .{ .text = " 14 │ ", .style = color238 }, + .{ .text = " 17 │ ", .style = color238 }, .{ .text = "try ", .style = color5 }, - .{ .text = "stdout.print(\"{d}\\n\", .{i})", .style = standard_selection }, + .{ .text = "w.print(\"{d}\\n\", .{i})", .style = standard_selection }, .{ .text = ";", .style = cursor }, }, .{ - .row_offset = 17, + .row_offset = 20, .col_offset = 2, }, ); _ = child.print( &.{ - .{ .text = " 15 │ ", .style = color238 }, + .{ .text = " 18 │ ", .style = color238 }, .{ .text = "}", .style = standard }, }, .{ - .row_offset = 18, + .row_offset = 21, .col_offset = 2, }, ); _ = child.print( &.{ - .{ .text = " 16 │ ", .style = color238 }, + .{ .text = " 19 │ ", .style = color238 }, .{ .text = "}", .style = standard }, }, .{ - .row_offset = 19, + .row_offset = 22, + .col_offset = 2, + }, + ); + _ = child.print( + &.{ + .{ .text = " 20 │ ", .style = color238 }, + .{ .text = "try ", .style = color5 }, + .{ .text = "w.flush();", .style = standard }, + }, + .{ + .row_offset = 23, .col_offset = 2, }, ); _ = child.print( &.{ - .{ .text = " 17 │ ", .style = color238 }, + .{ .text = " 21 │ ", .style = color238 }, .{ .text = "}", .style = standard }, }, .{ - .row_offset = 20, + .row_offset = 24, .col_offset = 2, }, ); @@ -1573,7 +1644,7 @@ const Preview = struct { }, }, .{ - .row_offset = 21, + .row_offset = 25, .col_offset = 2, }, ); @@ -1588,7 +1659,7 @@ const Preview = struct { }, }, .{ - .row_offset = 21, + .row_offset = 25, .col_offset = col, }, ); @@ -1602,12 +1673,12 @@ const Preview = struct { .{ .text = " main ", .style = color4 }, .{ .text = "[+] ", .style = color1 }, .{ .text = "via ", .style = standard }, - .{ .text = " v0.13.0 ", .style = color3 }, + .{ .text = " v0.16.0 ", .style = color3 }, .{ .text = "via ", .style = standard }, .{ .text = " impure (ghostty-env)", .style = color4 }, }, .{ - .row_offset = 22, + .row_offset = 26, .col_offset = 2, }, ); @@ -1619,7 +1690,7 @@ const Preview = struct { .{ .text = "→", .style = color2 }, }, .{ - .row_offset = 23, + .row_offset = 27, .col_offset = 2, }, ); diff --git a/vendor/libghostty-vt/src/cli/new_tab.zig b/vendor/libghostty-vt/src/cli/new_tab.zig new file mode 100644 index 00000000..31176c9b --- /dev/null +++ b/vendor/libghostty-vt/src/cli/new_tab.zig @@ -0,0 +1,277 @@ +const std = @import("std"); +const Allocator = std.mem.Allocator; +const ArenaAllocator = std.heap.ArenaAllocator; +const Action = @import("../cli.zig").ghostty.Action; +const apprt = @import("../apprt.zig"); +const args = @import("args.zig"); +const diagnostics = @import("diagnostics.zig"); +const homedir = @import("../os/homedir.zig"); +const internal_os = @import("../os/main.zig"); +const global = @import("../global.zig"); + +pub const Options = struct { + /// We store an I/O implementation to make maintenance easier. + _io: std.Io, + + /// This is set by the CLI parser for deinit. + _arena: ?ArenaAllocator = null, + + /// If set, open up a new tab in a custom instance of Ghostty. + class: ?[:0]const u8 = null, + + /// The surface to target. + @"surface-id": ?u64 = null, + + /// Did the user specify a `--working-directory` argument on the command line? + _working_directory_seen: bool = false, + + /// All of the arguments after `+new-tab`. They will be sent to Ghosttty + /// for processing. + _arguments: std.ArrayList([:0]const u8) = .empty, + + /// Enable arg parsing diagnostics so that we don't get an error if + /// there is a "normal" config setting on the cli. + _diagnostics: diagnostics.DiagnosticList = .{}, + + pub const ParseManuallyHookError = error{InvalidValue} || error{InvalidCharacter} || error{Overflow} || + homedir.ExpandError || + std.Io.Dir.RealPathFileAllocError || + Allocator.Error; + + /// Manual parse hook, collect all of the arguments after `+new-tab`. + pub fn parseManuallyHook(self: *Options, alloc: Allocator, arg: []const u8, iter: anytype) ParseManuallyHookError!bool { + var e_seen: bool = std.mem.eql(u8, arg, "-e"); + + // Include the argument that triggered the manual parse hook. + if (try self.checkArg(alloc, arg)) |a| try self._arguments.append(alloc, a); + + // Gather up the rest of the arguments to use as the command. + while (iter.next()) |param| { + if (e_seen) { + try self._arguments.append(alloc, try alloc.dupeZ(u8, param)); + continue; + } + if (std.mem.eql(u8, param, "-e")) { + e_seen = true; + try self._arguments.append(alloc, try alloc.dupeZ(u8, param)); + continue; + } + if (try self.checkArg(alloc, param)) |a| try self._arguments.append(alloc, a); + } + + return false; + } + + pub const CheckArgError = error{InvalidValue} || error{Unexpected} || std.fmt.ParseIntError || homedir.ExpandError || std.Io.Dir.RealPathFileError || Allocator.Error; + + fn checkArg(self: *Options, alloc: Allocator, arg: []const u8) CheckArgError!?[:0]const u8 { + if (std.mem.cutPrefix(u8, arg, "--surface-id=")) |rest| { + self.@"surface-id" = try std.fmt.parseUnsigned(u64, std.mem.trim(u8, rest, &std.ascii.whitespace), 0); + return null; + } + + if (std.mem.cutPrefix(u8, arg, "--class=")) |rest| { + self.class = try alloc.dupeZ(u8, std.mem.trim(u8, rest, &std.ascii.whitespace)); + return null; + } + + if (std.mem.cutPrefix(u8, arg, "--working-directory=")) |rest| { + const stripped = std.mem.trim(u8, rest, &std.ascii.whitespace); + if (std.mem.eql(u8, stripped, "home")) return try alloc.dupeZ(u8, arg); + if (std.mem.eql(u8, stripped, "inherit")) return try alloc.dupeZ(u8, arg); + const cwd: std.Io.Dir = .cwd(); + var expandhome_buf: [std.fs.max_path_bytes]u8 = undefined; + const expanded = expanded: { + var env = try global.environMap(); + defer env.deinit(); + break :expanded try homedir.expandHome(global.io(), &env, stripped, &expandhome_buf); + }; + var realpath_buf: [std.fs.max_path_bytes]u8 = undefined; + const realpath = realpath_buf[0..try cwd.realPathFile(global.io(), expanded, &realpath_buf)]; + self._working_directory_seen = true; + return try std.fmt.allocPrintSentinel(alloc, "--working-directory={s}", .{realpath}, 0); + } + + return try alloc.dupeZ(u8, arg); + } + + pub fn deinit(self: *Options) void { + if (self._arena) |arena| arena.deinit(); + self.* = undefined; + } + + /// Enables "-h" and "--help" to work. + pub fn help(self: Options) !void { + _ = self; + return Action.help_error; + } +}; + +/// The `new-tab` will use native platform IPC to open up a new tab in a running +/// instance of Ghostty. +/// +/// If the `--class` flag is not set, the `new-tab` command will try and connect +/// to a running instance of Ghostty based on what optimizations the Ghostty +/// CLI was compiled with. Otherwise the `new-tab` command will try and contact +/// a running Ghostty instance that was configured with the same `class` as was +/// given on the command line. +/// +/// All of the arguments after the `+new-tab` argument (except for the `--class` +/// and `--surface-id` flag) will be sent to the remote Ghostty instance and +/// will be parsed as command line flags. These flags will override certain +/// settings when creating the first surface in the new tab. Currently, only +/// `--working-directory`, `--command`, `--shell-integration=<mode>`, and +/// `--title` are supported. `-e` will also work as an alias for `--command`, except that if +/// `-e` is found on the command line all following arguments will become part +/// of the command and no more arguments will be parsed for configuration +/// settings. +/// +/// If `--working-directory` is found on the command line and is a relative +/// path (i.e. doesn't start with `/`) it will be resolved to an absolute +/// path relative to the current working directory that the `ghostty +new-tab` +/// command is run from. `~/` prefixes will also be expanded to the user's home +/// directory. +/// +/// If `--working-directory` is _not_ found on the command line, the working +/// directory that `ghostty +new-tab` is run from will be passed to Ghostty. +/// +/// GTK uses an application ID to identify instances of applications. If Ghostty +/// is compiled with release optimizations, the default application ID will be +/// `com.mitchellh.ghostty`. If Ghostty is compiled with debug optimizations, +/// the default application ID will be `com.mitchellh.ghostty-debug`. The +/// `class` configuration entry can be used to set up a custom application +/// ID. The class name must follow the requirements defined [in the GTK +/// documentation](https://docs.gtk.org/gio/type_func.Application.id_is_valid.html) +/// or it will be ignored and Ghostty will use the default as defined above. +/// +/// On GTK, D-Bus activation must be properly configured. Ghostty does not need +/// to be running for this to open a new tab, making it suitable for binding +/// to keys in your window manager (if other methods for configuring global +/// shortcuts are unavailable). D-Bus will handle launching a new instance +/// of Ghostty if it is not already running. See the Ghostty website for +/// information on properly configuring D-Bus activation. +/// +/// Only supported on GTK. +/// +/// Flags: +/// +/// * `--class=<class>`: If set, open up a new window in a custom instance of +/// Ghostty. The class must be a valid GTK application ID. +/// +/// * `--surface-id=<surface id>`: If set, specifies a Ghostty surface ID. +/// This is used to identify the window that the new tab will be added to. +/// If the surface ID is not specified or is `0` the currently focused +/// surface will be used. +/// +/// * `--command`: The command to be executed in the first surface of the new tab. +/// +/// * `--shell-integration=<mode>`: Whether to enable shell integration and +/// which shell to use. See the main configuration documentation for +/// supported values. +/// +/// * `--working-directory=<directory>`: The working directory to pass to Ghostty. +/// +/// * `--title`: A title that will override the title of the first surface in +/// the new tab. The title override may be edited or removed later. +/// +/// * `-e`: Any arguments after this will be interpreted as a command to +/// execute inside the first surface of the new tab instead of the +/// default command. +/// +/// Available since: 1.4.0 +pub fn run(alloc: Allocator) !u8 { + var iter = try args.argsIterator(alloc, global.args()); + defer iter.deinit(); + + var buffer: [1024]u8 = undefined; + var stderr_writer = std.Io.File.stderr().writer(global.io(), &buffer); + const stderr = &stderr_writer.interface; + + const result = runArgs(alloc, &iter, stderr); + stderr.flush() catch {}; + return result; +} + +fn runArgs( + alloc_gpa: Allocator, + argsIter: anytype, + stderr: *std.Io.Writer, +) !u8 { + var opts: Options = .{ ._io = global.io() }; + defer opts.deinit(); + + args.parse(Options, alloc_gpa, &opts, argsIter) catch |err| switch (err) { + error.ActionHelpRequested => return err, + else => { + try stderr.print("Error parsing args: {}\n", .{err}); + return 1; + }, + }; + + // Print out any diagnostics, unless it's likely that the diagnostic was + // generated trying to parse a "normal" configuration setting. Exit with an + // error code if any diagnostics were printed. + if (!opts._diagnostics.empty()) { + var exit: bool = false; + outer: for (opts._diagnostics.items()) |diagnostic| { + if (diagnostic.location != .cli) continue :outer; + inner: inline for (@typeInfo(Options).@"struct".fields) |field| { + if (field.name[0] == '_') continue :inner; + if (std.mem.eql(u8, field.name, diagnostic.key)) { + try stderr.print("config error: {f}\n", .{diagnostic}); + exit = true; + } + } + } + if (exit) return 1; + } + + if (!opts._working_directory_seen) { + const alloc = opts._arena.?.allocator(); + const cwd: std.Io.Dir = .cwd(); + var buf: [std.fs.max_path_bytes]u8 = undefined; + const wd = buf[0..try cwd.realPathFile(global.io(), ".", &buf)]; + // This should be inserted at the beginning of the list, just in case `-e` was used. + try opts._arguments.insert(alloc, 0, try std.fmt.allocPrintSentinel( + alloc, + "--working-directory={s}", + .{wd}, + 0, + )); + } + + var arena = ArenaAllocator.init(alloc_gpa); + defer arena.deinit(); + const alloc = arena.allocator(); + + const surface_id = opts.@"surface-id" orelse surface: { + var env = global.environMap() catch break :surface 0; + defer env.deinit(); + const e = env.get("GHOSTTY_SURFACE_ID") orelse break :surface 0; + break :surface std.fmt.parseUnsigned(u64, e, 0) catch break :surface 0; + }; + + if (apprt.App.performIpc( + alloc, + if (opts.class) |class| .{ .class = class } else .detect, + .new_tab, + .{ + .surface_id = surface_id, + .arguments = if (opts._arguments.items.len == 0) null else opts._arguments.items, + }, + ) catch |err| switch (err) { + error.IPCFailed => { + // The apprt should have printed a more specific error message + // already. + return 1; + }, + else => { + try stderr.print("Sending the IPC failed: {}", .{err}); + return 1; + }, + }) return 0; + + // If we get here, the platform is not supported. + try stderr.print("+new-tab is not supported on this platform.\n", .{}); + return 1; +} diff --git a/vendor/libghostty-vt/src/cli/new_window.zig b/vendor/libghostty-vt/src/cli/new_window.zig index a89c4ffa..29115a2e 100644 --- a/vendor/libghostty-vt/src/cli/new_window.zig +++ b/vendor/libghostty-vt/src/cli/new_window.zig @@ -5,10 +5,13 @@ const Action = @import("../cli.zig").ghostty.Action; const apprt = @import("../apprt.zig"); const args = @import("args.zig"); const diagnostics = @import("diagnostics.zig"); -const lib = @import("../lib/main.zig"); const homedir = @import("../os/homedir.zig"); +const global = @import("../global.zig"); pub const Options = struct { + /// We store an I/O implementation to make maintenance easier. + _io: std.Io, + /// This is set by the CLI parser for deinit. _arena: ?ArenaAllocator = null, @@ -26,8 +29,18 @@ pub const Options = struct { /// there is a "normal" config setting on the cli. _diagnostics: diagnostics.DiagnosticList = .{}, + pub const ParseManuallyHookError = error{InvalidValue} || + homedir.ExpandError || + std.Io.Dir.RealPathFileAllocError || + Allocator.Error; + /// Manual parse hook, collect all of the arguments after `+new-window`. - pub fn parseManuallyHook(self: *Options, alloc: Allocator, arg: []const u8, iter: anytype) (error{InvalidValue} || homedir.ExpandError || std.fs.Dir.RealPathAllocError || Allocator.Error)!bool { + pub fn parseManuallyHook( + self: *Options, + alloc: Allocator, + arg: []const u8, + iter: anytype, + ) ParseManuallyHookError!bool { var e_seen: bool = std.mem.eql(u8, arg, "-e"); // Include the argument that triggered the manual parse hook. @@ -50,21 +63,30 @@ pub const Options = struct { return false; } - fn checkArg(self: *Options, alloc: Allocator, arg: []const u8) (error{InvalidValue} || homedir.ExpandError || std.fs.Dir.RealPathAllocError || Allocator.Error)!?[:0]const u8 { - if (lib.cutPrefix(u8, arg, "--class=")) |rest| { + const CheckArgError = error{InvalidValue} || + homedir.ExpandError || + std.Io.Dir.RealPathFileAllocError || + Allocator.Error; + + fn checkArg(self: *Options, alloc: Allocator, arg: []const u8) CheckArgError!?[:0]const u8 { + if (std.mem.cutPrefix(u8, arg, "--class=")) |rest| { self.class = try alloc.dupeZ(u8, std.mem.trim(u8, rest, &std.ascii.whitespace)); return null; } - if (lib.cutPrefix(u8, arg, "--working-directory=")) |rest| { + if (std.mem.cutPrefix(u8, arg, "--working-directory=")) |rest| { const stripped = std.mem.trim(u8, rest, &std.ascii.whitespace); if (std.mem.eql(u8, stripped, "home")) return try alloc.dupeZ(u8, arg); if (std.mem.eql(u8, stripped, "inherit")) return try alloc.dupeZ(u8, arg); - const cwd: std.fs.Dir = std.fs.cwd(); + const cwd: std.Io.Dir = .cwd(); var expandhome_buf: [std.fs.max_path_bytes]u8 = undefined; - const expanded = try homedir.expandHome(stripped, &expandhome_buf); + const expanded = expanded: { + var environ_map = try global.environMap(); + defer environ_map.deinit(); + break :expanded try homedir.expandHome(global.io(), &environ_map, stripped, &expandhome_buf); + }; var realpath_buf: [std.fs.max_path_bytes]u8 = undefined; - const realpath = try cwd.realpath(expanded, &realpath_buf); + const realpath = realpath_buf[0..try cwd.realPathFile(self._io, expanded, &realpath_buf)]; self._working_directory_seen = true; return try std.fmt.allocPrintSentinel(alloc, "--working-directory={s}", .{realpath}, 0); } @@ -97,10 +119,11 @@ pub const Options = struct { /// `--class` flag) will be sent to the remote Ghostty instance and will be /// parsed as command line flags. These flags will override certain settings /// when creating the first surface in the new window. Currently, only -/// `--working-directory`, `--command`, and `--title` are supported. `-e` will -/// also work as an alias for `--command`, except that if `-e` is found on the -/// command line all following arguments will become part of the command and no -/// more arguments will be parsed for configuration settings. +/// `--working-directory`, `--command`, `--shell-integration=<mode>`, and +/// `--title` are supported. `-e` will also work as an alias for `--command`, except that if +/// `-e` is found on the command line all following arguments will become part +/// of the command and no more arguments will be parsed for configuration +/// settings. /// /// If `--working-directory` is found on the command line and is a relative /// path (i.e. doesn't start with `/`) it will be resolved to an absolute path @@ -136,6 +159,10 @@ pub const Options = struct { /// /// * `--command`: The command to be executed in the first surface of the new window. /// +/// * `--shell-integration=<mode>`: Whether to enable shell integration and +/// which shell to use. See the main configuration documentation for +/// supported values. +/// /// * `--working-directory=<directory>`: The working directory to pass to Ghostty. /// /// * `--title`: A title that will override the title of the first surface in @@ -147,11 +174,11 @@ pub const Options = struct { /// /// Available since: 1.2.0 pub fn run(alloc: Allocator) !u8 { - var iter = try args.argsIterator(alloc); + var iter = try args.argsIterator(alloc, global.args()); defer iter.deinit(); var buffer: [1024]u8 = undefined; - var stderr_writer = std.fs.File.stderr().writer(&buffer); + var stderr_writer = std.Io.File.stderr().writer(global.io(), &buffer); const stderr = &stderr_writer.interface; const result = runArgs(alloc, &iter, stderr); @@ -164,7 +191,7 @@ fn runArgs( argsIter: anytype, stderr: *std.Io.Writer, ) !u8 { - var opts: Options = .{}; + var opts: Options = .{ ._io = global.io() }; defer opts.deinit(); args.parse(Options, alloc_gpa, &opts, argsIter) catch |err| switch (err) { @@ -195,11 +222,16 @@ fn runArgs( if (!opts._working_directory_seen) { const alloc = opts._arena.?.allocator(); - const cwd: std.fs.Dir = std.fs.cwd(); + const cwd: std.Io.Dir = .cwd(); var buf: [std.fs.max_path_bytes]u8 = undefined; - const wd = try cwd.realpath(".", &buf); + const wd = buf[0..try cwd.realPathFile(global.io(), ".", &buf)]; // This should be inserted at the beginning of the list, just in case `-e` was used. - try opts._arguments.insert(alloc, 0, try std.fmt.allocPrintSentinel(alloc, "--working-directory={s}", .{wd}, 0)); + try opts._arguments.insert(alloc, 0, try std.fmt.allocPrintSentinel( + alloc, + "--working-directory={s}", + .{wd}, + 0, + )); } var arena = ArenaAllocator.init(alloc_gpa); diff --git a/vendor/libghostty-vt/src/cli/show_config.zig b/vendor/libghostty-vt/src/cli/show_config.zig index cd0ba89a..60df2a77 100644 --- a/vendor/libghostty-vt/src/cli/show_config.zig +++ b/vendor/libghostty-vt/src/cli/show_config.zig @@ -5,6 +5,7 @@ const Action = @import("ghostty.zig").Action; const configpkg = @import("../config.zig"); const Config = configpkg.Config; const Pager = @import("Pager.zig"); +const global = @import("../global.zig"); pub const Options = struct { /// If true, do not load the user configuration, only load the defaults. @@ -66,7 +67,7 @@ pub fn run(alloc: Allocator) !u8 { defer opts.deinit(); { - var iter = try args.argsIterator(alloc); + var iter = try args.argsIterator(alloc, global.args()); defer iter.deinit(); try args.parse(Options, alloc, &opts, &iter); } @@ -81,7 +82,7 @@ pub fn run(alloc: Allocator) !u8 { .docs = opts.docs, }; - var pager: Pager = if (!opts.@"no-pager") .init(alloc) else .{}; + var pager: Pager = if (!opts.@"no-pager") .init() else .{}; defer pager.deinit(); var buffer: [4096]u8 = undefined; const writer = pager.writer(&buffer); diff --git a/vendor/libghostty-vt/src/cli/show_face.zig b/vendor/libghostty-vt/src/cli/show_face.zig index 9dee777b..30bd550a 100644 --- a/vendor/libghostty-vt/src/cli/show_face.zig +++ b/vendor/libghostty-vt/src/cli/show_face.zig @@ -7,6 +7,7 @@ const diagnostics = @import("diagnostics.zig"); const font = @import("../font/main.zig"); const configpkg = @import("../config.zig"); const Config = configpkg.Config; +const global = @import("../global.zig"); pub const Options = struct { /// This is set by the CLI parser for deinit. @@ -62,15 +63,15 @@ pub const Options = struct { /// style. Valid options are `text` and `emoji`. If unset, the presentation /// style of a codepoint will be inferred from the Unicode standard. pub fn run(alloc: Allocator) !u8 { - var iter = try args.argsIterator(alloc); + var iter = try args.argsIterator(alloc, global.args()); defer iter.deinit(); var stdout_buffer: [1024]u8 = undefined; - var stdout_writer = std.fs.File.stdout().writer(&stdout_buffer); + var stdout_writer = std.Io.File.stdout().writer(global.io(), &stdout_buffer); const stdout = &stdout_writer.interface; var stderr_buffer: [1024]u8 = undefined; - var stderr_writer = std.fs.File.stdout().writer(&stderr_buffer); + var stderr_writer = std.Io.File.stdout().writer(global.io(), &stderr_buffer); const stderr = &stderr_writer.interface; const result = runArgs( diff --git a/vendor/libghostty-vt/src/cli/ssh-cache/DiskCache.zig b/vendor/libghostty-vt/src/cli/ssh-cache/DiskCache.zig index bb11e74f..ea64dfe8 100644 --- a/vendor/libghostty-vt/src/cli/ssh-cache/DiskCache.zig +++ b/vendor/libghostty-vt/src/cli/ssh-cache/DiskCache.zig @@ -10,6 +10,7 @@ const Allocator = std.mem.Allocator; const internal_os = @import("../../os/main.zig"); const xdg = internal_os.xdg; const Entry = @import("Entry.zig"); +const global = @import("../../global.zig"); // 512KB - sufficient for approximately 10k entries const MAX_CACHE_SIZE = 512 * 1024; @@ -26,8 +27,12 @@ pub fn defaultPath( alloc: Allocator, program: []const u8, ) ![]const u8 { + var environ_map = try global.environMap(); + defer environ_map.deinit(); const state_dir: []const u8 = xdg.state( + global.io(), alloc, + &environ_map, .{ .subdir = program }, ) catch |err| return switch (err) { error.OutOfMemory => error.OutOfMemory, @@ -41,7 +46,7 @@ pub fn defaultPath( /// This removes the cache file from disk, effectively clearing all cached /// SSH terminfo entries. pub fn clear(self: DiskCache) !void { - std.fs.cwd().deleteFile(self.path) catch |err| switch (err) { + std.Io.Dir.cwd().deleteFile(global.io(), self.path) catch |err| switch (err) { error.FileNotFound => {}, else => return err, }; @@ -53,42 +58,50 @@ pub fn add( self: DiskCache, alloc: Allocator, key: []const u8, + version: []const u8, timestamp: i64, ) !void { if (!isValidCacheKey(key)) return error.InvalidCacheKey; // Create cache directory if needed if (std.fs.path.dirname(self.path)) |dir| { - std.fs.cwd().makePath(dir) catch |err| switch (err) { + std.Io.Dir.cwd().createDirPath(global.io(), dir) catch |err| switch (err) { error.PathAlreadyExists => {}, else => return err, }; } // Open or create cache file with secure permissions - const file = std.fs.createFileAbsolute(self.path, .{ + const file = std.Io.Dir.createFileAbsolute(global.io(), self.path, .{ .read = true, .truncate = false, - .mode = 0o600, + .permissions = if (builtin.os.tag != .windows and std.posix.mode_t != u0) + .fromMode(0o600) + else + .default_file, }) catch |err| switch (err) { error.PathAlreadyExists => blk: { - const existing_file = try std.fs.openFileAbsolute( + const existing_file = try std.Io.Dir.openFileAbsolute( + global.io(), self.path, .{ .mode = .read_write }, ); - errdefer existing_file.close(); + errdefer existing_file.close(global.io()); try fixupPermissions(existing_file); break :blk existing_file; }, else => return err, }; - defer file.close(); + defer file.close(global.io()); // Lock // Causes a compile failure in the Zig std library on Windows, see: // https://github.com/ziglang/zig/issues/18430 - if (comptime builtin.os.tag != .windows) _ = file.tryLock(.exclusive) catch return error.CacheLocked; - defer if (comptime builtin.os.tag != .windows) file.unlock(); + if (comptime builtin.os.tag != .windows) _ = file.tryLock( + global.io(), + .exclusive, + ) catch return error.CacheLocked; + defer if (comptime builtin.os.tag != .windows) file.unlock(global.io()); var entries = try readEntries(alloc, file); defer deinitEntries(alloc, &entries); @@ -99,16 +112,19 @@ pub fn add( // `deinitEntries` defer to walk. if (entries.getPtr(key)) |existing| { existing.timestamp = timestamp; + const version_copy = try alloc.dupe(u8, version); + alloc.free(existing.terminfo_version); + existing.terminfo_version = version_copy; } else { const key_copy = try alloc.dupe(u8, key); errdefer alloc.free(key_copy); - const terminfo_copy = try alloc.dupe(u8, "xterm-ghostty"); - errdefer alloc.free(terminfo_copy); + const version_copy = try alloc.dupe(u8, version); + errdefer alloc.free(version_copy); try entries.put(key_copy, .{ .hostname = key_copy, .timestamp = timestamp, - .terminfo_version = terminfo_copy, + .terminfo_version = version_copy, }); } @@ -125,21 +141,25 @@ pub fn remove( if (!isValidCacheKey(key)) return error.InvalidCacheKey; // Open our file - const file = std.fs.openFileAbsolute( + const file = std.Io.Dir.openFileAbsolute( + global.io(), self.path, .{ .mode = .read_write }, ) catch |err| switch (err) { error.FileNotFound => return false, else => return err, }; - defer file.close(); + defer file.close(global.io()); try fixupPermissions(file); // Lock // Causes a compile failure in the Zig std library on Windows, see: // https://github.com/ziglang/zig/issues/18430 - if (comptime builtin.os.tag != .windows) _ = file.tryLock(.exclusive) catch return error.CacheLocked; - defer if (comptime builtin.os.tag != .windows) file.unlock(); + if (comptime builtin.os.tag != .windows) _ = file.tryLock( + global.io(), + .exclusive, + ) catch return error.CacheLocked; + defer if (comptime builtin.os.tag != .windows) file.unlock(global.io()); // Read existing entries var entries = try readEntries(alloc, file); @@ -165,28 +185,32 @@ pub fn prune( alloc: Allocator, max_age_s: u64, ) !usize { - const file = std.fs.openFileAbsolute( + const file = std.Io.Dir.openFileAbsolute( + global.io(), self.path, .{ .mode = .read_write }, ) catch |err| switch (err) { error.FileNotFound => return 0, else => return err, }; - defer file.close(); + defer file.close(global.io()); try fixupPermissions(file); // Lock // Causes a compile failure in the Zig std library on Windows, see: // https://github.com/ziglang/zig/issues/18430 - if (comptime builtin.os.tag != .windows) _ = file.tryLock(.exclusive) catch return error.CacheLocked; - defer if (comptime builtin.os.tag != .windows) file.unlock(); + if (comptime builtin.os.tag != .windows) _ = file.tryLock( + global.io(), + .exclusive, + ) catch return error.CacheLocked; + defer if (comptime builtin.os.tag != .windows) file.unlock(global.io()); // Read existing entries var entries = try readEntries(alloc, file); defer deinitEntries(alloc, &entries); // Drop expired entries from the map, then persist what remains. - const now = std.time.timestamp(); + const now = std.Io.Timestamp.now(global.io(), .real).toSeconds(); var expired: std.ArrayList([]const u8) = .empty; defer expired.deinit(alloc); var iter = entries.iterator(); @@ -205,41 +229,44 @@ pub fn prune( return expired.items.len; } -/// Check if a key exists in the cache. -/// Returns false if the cache file doesn't exist. +/// Check if a key with `version` exists in the cache. +/// Returns false if the cache file doesn't exist or the version doesn't match. pub fn contains( self: DiskCache, alloc: Allocator, key: []const u8, + version: []const u8, ) !bool { if (!isValidCacheKey(key)) return error.InvalidCacheKey; // Open our file - const file = std.fs.openFileAbsolute( + const file = std.Io.Dir.openFileAbsolute( + global.io(), self.path, .{}, ) catch |err| switch (err) { error.FileNotFound => return false, else => return err, }; - defer file.close(); + defer file.close(global.io()); // Read existing entries var entries = try readEntries(alloc, file); defer deinitEntries(alloc, &entries); - return entries.contains(key); + const entry = entries.get(key) orelse return false; + return std.mem.eql(u8, entry.terminfo_version, version); } -fn fixupPermissions(file: std.fs.File) !void { +fn fixupPermissions(file: std.Io.File) !void { // Windows does not support chmod if (comptime builtin.os.tag == .windows) return; // Ensure file has correct permissions (readable/writable by // owner only) - const stat = try file.stat(); - if (stat.mode & 0o777 != 0o600) { - try file.chmod(0o600); + const stat = try file.stat(global.io()); + if (stat.permissions.toMode() & 0o777 != 0o600) { + try file.setPermissions(global.io(), .fromMode(0o600)); } } @@ -250,22 +277,27 @@ fn writeCacheFile( const cache_dir = std.fs.path.dirname(self.path) orelse return error.InvalidCachePath; const cache_basename = std.fs.path.basename(self.path); - var dir = try std.fs.cwd().openDir(cache_dir, .{}); - defer dir.close(); + var dir = try std.Io.Dir.cwd().openDir(global.io(), cache_dir, .{}); + defer dir.close(global.io()); var buf: [1024]u8 = undefined; - var atomic_file = try dir.atomicFile(cache_basename, .{ - .mode = 0o600, - .write_buffer = &buf, + var atomic_file = try dir.createFileAtomic(global.io(), cache_basename, .{ + .permissions = if (builtin.os.tag != .windows and std.posix.mode_t != u0) + .fromMode(0o600) + else + .default_file, + .replace = true, }); - defer atomic_file.deinit(); + defer atomic_file.deinit(global.io()); + var file_writer = atomic_file.file.writer(global.io(), &buf); var iter = entries.iterator(); while (iter.next()) |kv| { - try kv.value_ptr.format(&atomic_file.file_writer.interface); + try kv.value_ptr.format(&file_writer.interface); } - try atomic_file.finish(); + try file_writer.flush(); + try atomic_file.replace(global.io()); } /// List all entries in the cache. @@ -276,14 +308,15 @@ pub fn list( alloc: Allocator, ) !std.StringHashMap(Entry) { // Open our file - const file = std.fs.openFileAbsolute( + const file = std.Io.Dir.openFileAbsolute( + global.io(), self.path, .{}, ) catch |err| switch (err) { error.FileNotFound => return .init(alloc), else => return err, }; - defer file.close(); + defer file.close(global.io()); return readEntries(alloc, file); } @@ -306,9 +339,9 @@ pub fn deinitEntries( fn readEntries( alloc: Allocator, - file: std.fs.File, + file: std.Io.File, ) !std.StringHashMap(Entry) { - var reader = file.reader(&.{}); + var reader = file.reader(global.io(), &.{}); const content = try reader.interface.allocRemaining( alloc, .limited(MAX_CACHE_SIZE), @@ -367,6 +400,16 @@ fn readEntries( return entries; } +/// Whether an error from a cache operation reflects a problem with the cache +/// itself. A destination we can't key on and a lock held by another process +/// both leave the cache healthy, so neither is worth reporting. +pub fn isFailure(err: anyerror) bool { + return switch (err) { + error.InvalidCacheKey, error.CacheLocked => false, + else => true, + }; +} + // Supports both standalone hostnames and user@hostname format pub fn isValidCacheKey(key: []const u8) bool { if (key.len == 0) return false; @@ -385,14 +428,14 @@ pub fn isValidCacheKey(key: []const u8) bool { fn isValidHost(host: []const u8) bool { // First check for valid hostnames because this is assumed to be the more // likely ssh host format. - if (internal_os.hostname.isValid(host)) { + if (std.Io.net.HostName.validate(host)) |_| { return true; - } + } else |_| {} // We also accept valid IP addresses. In practice, IPv4 addresses are also // considered valid hostnames due to their overlapping syntax, so we can // simplify this check to be IPv6-specific. - if (std.net.Address.parseIp6(host, 0)) |_| { + if (std.Io.net.IpAddress.parseIp6(host, 0)) |_| { return true; } else |_| { return false; @@ -428,12 +471,12 @@ test "disk cache clear" { defer tmp.cleanup(); var buf: [4096]u8 = undefined; { - var file = try tmp.dir.createFile("cache", .{}); - defer file.close(); - var file_writer = file.writer(&buf); + var file = try tmp.dir.createFile(testing.io, "cache", .{}); + defer file.close(testing.io); + var file_writer = file.writer(testing.io, &buf); try file_writer.interface.writeAll("HELLO!"); } - const path = try tmp.dir.realpathAlloc(alloc, "cache"); + const path = try tmp.dir.realPathFileAlloc(testing.io, "cache", alloc); defer alloc.free(path); // Setup our cache @@ -443,7 +486,7 @@ test "disk cache clear" { // Verify the file is gone try testing.expectError( error.FileNotFound, - tmp.dir.openFile("cache", .{}), + tmp.dir.openFile(testing.io, "cache", .{}), ); } @@ -456,33 +499,38 @@ test "disk cache operations" { defer tmp.cleanup(); var buf: [4096]u8 = undefined; { - var file = try tmp.dir.createFile("cache", .{}); - defer file.close(); - var file_writer = file.writer(&buf); + var file = try tmp.dir.createFile(testing.io, "cache", .{}); + defer file.close(testing.io); + var file_writer = file.writer(testing.io, &buf); const writer = &file_writer.interface; try writer.writeAll("HELLO!"); try writer.flush(); } - const path = try tmp.dir.realpathAlloc(alloc, "cache"); + const path = try tmp.dir.realPathFileAlloc(testing.io, "cache", alloc); defer alloc.free(path); // Setup our cache. Adding the same key twice exercises both the new // and existing-entry paths. const cache: DiskCache = .{ .path = path }; - try cache.add(alloc, "example.com", std.time.timestamp()); - try cache.add(alloc, "example.com", std.time.timestamp()); - try testing.expect(try cache.contains(alloc, "example.com")); + try cache.add(alloc, "example.com", "v1", std.Io.Timestamp.now(testing.io, .real).toSeconds()); + try testing.expect(!try cache.contains(alloc, "example.com", "v2")); + try cache.add(alloc, "example.com", "v2", std.Io.Timestamp.now(testing.io, .real).toSeconds()); + try testing.expect(try cache.contains(alloc, "example.com", "v2")); // List var entries = try cache.list(alloc); - deinitEntries(alloc, &entries); + defer deinitEntries(alloc, &entries); + try testing.expectEqualStrings( + "v2", + entries.get("example.com").?.terminfo_version, + ); // Remove reports that it removed the entry, and a second remove of the // same key reports nothing to remove. try testing.expect(try cache.remove(alloc, "example.com")); try testing.expect(!try cache.remove(alloc, "example.com")); - try testing.expect(!(try cache.contains(alloc, "example.com"))); - try cache.add(alloc, "example.com", std.time.timestamp()); + try testing.expect(!(try cache.contains(alloc, "example.com", "v2"))); + try cache.add(alloc, "example.com", "v2", std.Io.Timestamp.now(testing.io, .real).toSeconds()); } test "disk cache cleans up temp files" { @@ -492,19 +540,19 @@ test "disk cache cleans up temp files" { var tmp = testing.tmpDir(.{ .iterate = true }); defer tmp.cleanup(); - const tmp_path = try tmp.dir.realpathAlloc(alloc, "."); + const tmp_path = try tmp.dir.realPathFileAlloc(testing.io, ".", alloc); defer alloc.free(tmp_path); const cache_path = try std.fs.path.join(alloc, &.{ tmp_path, "cache" }); defer alloc.free(cache_path); const cache: DiskCache = .{ .path = cache_path }; - try cache.add(alloc, "example.com", std.time.timestamp()); - try cache.add(alloc, "example.org", std.time.timestamp()); + try cache.add(alloc, "example.com", "v1", std.Io.Timestamp.now(testing.io, .real).toSeconds()); + try cache.add(alloc, "example.org", "v1", std.Io.Timestamp.now(testing.io, .real).toSeconds()); // Verify only the cache file exists and no temp files left behind var count: usize = 0; var iter = tmp.dir.iterate(); - while (try iter.next()) |entry| { + while (try iter.next(testing.io)) |entry| { count += 1; try testing.expectEqualStrings("cache", entry.name); } @@ -514,10 +562,11 @@ test "disk cache cleans up temp files" { test "disk cache prune" { const testing = std.testing; const alloc = testing.allocator; + const io = testing.io; var tmp = testing.tmpDir(.{}); defer tmp.cleanup(); - const tmp_path = try tmp.dir.realpathAlloc(alloc, "."); + const tmp_path = try tmp.dir.realPathFileAlloc(io, ".", alloc); defer alloc.free(tmp_path); const cache_path = try std.fs.path.join(alloc, &.{ tmp_path, "cache" }); defer alloc.free(cache_path); @@ -527,31 +576,32 @@ test "disk cache prune" { // Back-date one entry an hour old and one 100 days old. const day = std.time.s_per_day; const hour = std.time.s_per_hour; - const now = std.time.timestamp(); - try cache.add(alloc, "recent.com", now - hour); - try cache.add(alloc, "old.com", now - 100 * day); + const now = std.Io.Timestamp.now(testing.io, .real).toSeconds(); + try cache.add(alloc, "recent.com", "v1", now - hour); + try cache.add(alloc, "old.com", "v1", now - 100 * day); // Prune entries older than 90 days: only old.com goes. try testing.expectEqual(@as(usize, 1), try cache.prune(alloc, 90 * day)); - try testing.expect(try cache.contains(alloc, "recent.com")); - try testing.expect(!try cache.contains(alloc, "old.com")); + try testing.expect(try cache.contains(alloc, "recent.com", "v1")); + try testing.expect(!try cache.contains(alloc, "old.com", "v1")); // Pruning again removes nothing. try testing.expectEqual(@as(usize, 0), try cache.prune(alloc, 90 * day)); // Sub-day granularity: a 30-minute max age prunes the hour-old entry. try testing.expectEqual(@as(usize, 1), try cache.prune(alloc, 30 * std.time.s_per_min)); - try testing.expect(!try cache.contains(alloc, "recent.com")); + try testing.expect(!try cache.contains(alloc, "recent.com", "v1")); } test "disk cache prune missing file" { const testing = std.testing; const alloc = testing.allocator; + const io = testing.io; var tmp = testing.tmpDir(.{}); defer tmp.cleanup(); - const tmp_path = try tmp.dir.realpathAlloc(alloc, "."); + const tmp_path = try tmp.dir.realPathFileAlloc(io, ".", alloc); defer alloc.free(tmp_path); const cache_path = try std.fs.path.join(alloc, &.{ tmp_path, "cache" }); defer alloc.free(cache_path); @@ -563,6 +613,7 @@ test "disk cache prune missing file" { test "disk cache reads duplicate keys" { const testing = std.testing; const alloc = testing.allocator; + const io = testing.io; var tmp = testing.tmpDir(.{}); defer tmp.cleanup(); @@ -571,16 +622,16 @@ test "disk cache reads duplicate keys" { // key with the updated entry and ensure (via testing.allocator) that // we don't double-free or leak. { - var file = try tmp.dir.createFile("cache", .{}); - defer file.close(); + var file = try tmp.dir.createFile(io, "cache", .{}); + defer file.close(io); var buf: [256]u8 = undefined; - var file_writer = file.writer(&buf); + var file_writer = file.writer(io, &buf); try file_writer.interface.writeAll( "example.com|100|xterm-ghostty\nexample.com|200|xterm-newer\n", ); try file_writer.interface.flush(); } - const path = try tmp.dir.realpathAlloc(alloc, "cache"); + const path = try tmp.dir.realPathFileAlloc(io, "cache", alloc); defer alloc.free(path); const cache: DiskCache = .{ .path = path }; @@ -602,10 +653,10 @@ test "disk cache reads survive allocation failure" { // Exercise a populated cache containing a duplicate key to ensure // that we hit all of the possible allocation behaviors below. { - var file = try tmp.dir.createFile("cache", .{}); - defer file.close(); + var file = try tmp.dir.createFile(testing.io, "cache", .{}); + defer file.close(testing.io); var buf: [256]u8 = undefined; - var file_writer = file.writer(&buf); + var file_writer = file.writer(testing.io, &buf); try file_writer.interface.writeAll( "a.com|100|xterm-ghostty\n" ++ "b.com|100|xterm-ghostty\n" ++ @@ -614,7 +665,7 @@ test "disk cache reads survive allocation failure" { ); try file_writer.interface.flush(); } - const path = try tmp.dir.realpathAlloc(testing.allocator, "cache"); + const path = try tmp.dir.realPathFileAlloc(testing.io, "cache", testing.allocator); defer testing.allocator.free(path); const cache: DiskCache = .{ .path = path }; @@ -647,7 +698,7 @@ test "disk cache add survives allocation failure" { var tmp = testing.tmpDir(.{}); defer tmp.cleanup(); - const tmp_path = try tmp.dir.realpathAlloc(testing.allocator, "."); + const tmp_path = try tmp.dir.realPathFileAlloc(testing.io, ".", testing.allocator); defer testing.allocator.free(tmp_path); const path = try std.fs.path.join(testing.allocator, &.{ tmp_path, "cache" }); defer testing.allocator.free(path); @@ -660,14 +711,14 @@ test "disk cache add survives allocation failure" { // from a clean cache file. var fail_index: usize = 0; while (true) : (fail_index += 1) { - std.fs.cwd().deleteFile(path) catch {}; + std.Io.Dir.cwd().deleteFile(testing.io, path) catch {}; var failing = std.testing.FailingAllocator.init( testing.allocator, .{ .fail_index = fail_index }, ); const alloc = failing.allocator(); - if (cache.add(alloc, "user@example.com", 100)) |_| { + if (cache.add(alloc, "user@example.com", "v1", 100)) |_| { if (!failing.has_induced_failure) break; } else |err| { try testing.expectEqual(error.OutOfMemory, err); @@ -675,6 +726,15 @@ test "disk cache add survives allocation failure" { } } +test isFailure { + const testing = std.testing; + + try testing.expect(!isFailure(error.InvalidCacheKey)); + try testing.expect(!isFailure(error.CacheLocked)); + try testing.expect(isFailure(error.AccessDenied)); + try testing.expect(isFailure(error.IsDir)); +} + test isValidHost { const testing = std.testing; diff --git a/vendor/libghostty-vt/src/cli/ssh.zig b/vendor/libghostty-vt/src/cli/ssh.zig index 76bfb10e..d11a9463 100644 --- a/vendor/libghostty-vt/src/cli/ssh.zig +++ b/vendor/libghostty-vt/src/cli/ssh.zig @@ -1,4 +1,5 @@ const std = @import("std"); +const builtin = @import("builtin"); const Allocator = std.mem.Allocator; const ArenaAllocator = std.heap.ArenaAllocator; const cli_args = @import("args.zig"); @@ -6,7 +7,8 @@ const diagnostics = @import("diagnostics.zig"); const Action = @import("ghostty.zig").Action; const DiskCache = @import("ssh_cache.zig").DiskCache; const internal_os = @import("../os/main.zig"); -const ghostty_terminfo = @import("../terminfo/main.zig").ghostty; +const terminfopkg = @import("../terminfo/main.zig"); +const global = @import("../global.zig"); const log = std.log.scoped(.ssh); @@ -126,9 +128,9 @@ pub const Options = struct { /// forwarding to succeed. /// /// 2. **Terminfo install** (`--terminfo`). On the first connection to a -/// given destination, installs Ghostty's terminfo entry on the remote -/// host using `infocmp -x xterm-ghostty | ssh tic -x -` over a -/// shared `ControlMaster` connection. Successful installs are cached +/// given destination, installs Ghostty's embedded terminfo entry on the +/// remote host using `ssh tic -x -` over a shared `ControlMaster` +/// connection. Successful installs are cached /// (see `ghostty +ssh-cache`) so subsequent connections skip this /// step. When terminfo is successfully installed or already cached, /// `TERM` is set to `xterm-ghostty` instead of `xterm-256color`. @@ -160,17 +162,17 @@ pub const Options = struct { /// /// Examples: /// -/// # Basic invocation using defaults: -/// ghostty +ssh user@example.com +/// # Basic invocation using defaults: +/// ghostty +ssh user@example.com /// -/// # Forward Ghostty env vars but skip the terminfo install: -/// ghostty +ssh --terminfo=false user@example.com +/// # Forward Ghostty env vars but skip the terminfo install: +/// ghostty +ssh --terminfo=false user@example.com /// -/// # `ssh` flags (short-form `-p`, etc.) pass through unchanged: -/// ghostty +ssh -p 2222 -i ~/.ssh/id_ed25519 user@example.com +/// # `ssh` flags (short-form `-p`, etc.) pass through unchanged: +/// ghostty +ssh -p 2222 -i ~/.ssh/id_ed25519 user@example.com /// -/// # Use `--` explicitly if your ssh args might collide with our flags: -/// ghostty +ssh -- --some-rare-ssh-arg user@example.com +/// # Use `--` explicitly if your ssh args might collide with our flags: +/// ghostty +ssh -- --some-rare-ssh-arg user@example.com /// /// Pass `--verbose` to see what `+ssh` is doing. For cache inspection /// and management, see `ghostty +ssh-cache`. @@ -181,14 +183,14 @@ pub fn run(alloc_gpa: Allocator) !u8 { defer opts.deinit(); { - var iter = try cli_args.argsIterator(alloc_gpa); + var iter = try cli_args.argsIterator(alloc_gpa, global.args()); defer iter.deinit(); try cli_args.parse(Options, alloc_gpa, &opts, &iter); } var stderr_buffer: [1024]u8 = undefined; - var stderr_file: std.fs.File = .stderr(); - var stderr_writer = stderr_file.writer(&stderr_buffer); + var stderr_file: std.Io.File = .stderr(); + var stderr_writer = stderr_file.writer(global.io(), &stderr_buffer); const stderr = &stderr_writer.interface; // Any diagnostic from the arg parser is an unknown flag or bad @@ -243,14 +245,27 @@ fn runInner( const cache: ?DiskCache = if (opts.cache) cache: { const path = DiskCache.defaultPath(alloc, "ghostty") catch |err| { - warnPrint(stderr, "ghostty terminfo cache unavailable: {}", .{err}); + warnPrint(stderr, "ghostty terminfo cache unavailable: {t}", .{err}); break :session .{ .term = "xterm-256color" }; }; break :cache .{ .path = path }; } else null; if (cache) |c| { - if (c.contains(alloc, dest) catch false) { + const cached = c.contains( + alloc, + dest, + terminfopkg.version, + ) catch |err| cached: { + if (DiskCache.isFailure(err)) warnPrint( + stderr, + "unable to read the cache '{s}': {t}", + .{ c.path, err }, + ); + break :cached false; + }; + + if (cached) { verbosePrint(opts, stderr, "dest: {s} (cached, skipping install)", .{dest}); break :session .{ .term = "xterm-ghostty" }; } else { @@ -264,7 +279,7 @@ fn runInner( stderr.flush() catch {}; installRemoteTerminfo(alloc, opts, stderr) catch |err| { - warnPrint(stderr, "failed to install terminfo: {}", .{err}); + warnPrint(stderr, "failed to install terminfo: {t}", .{err}); break :session .{ .term = "xterm-256color" }; }; break :session .{ @@ -294,18 +309,36 @@ fn runInner( }); verbosePrint(opts, stderr, "exec: {f}", .{Joined{ .items = argv }}); - const exit_code = childExec(alloc, argv) catch |err| { - try stderr.print("Error: failed to run {s}: {}\n", .{ argv[0], err }); + const exit_code = childExec(argv) catch |err| { + try stderr.print("Error: failed to run {s}: {t}\n", .{ argv[0], err }); return 1; }; verbosePrint(opts, stderr, "exit: {d}", .{exit_code}); // Attempt to cache (if needed) on a successful ssh execution. if (exit_code == 0) if (session.to_cache) |entry| { - if (entry.cache.add(alloc, entry.dest, std.time.timestamp())) |_| { + if (entry.cache.add( + alloc, + entry.dest, + terminfopkg.version, + std.Io.Timestamp.now(global.io(), .real).toSeconds(), + )) |_| { verbosePrint(opts, stderr, "cache: wrote {s}", .{entry.dest}); } else |err| { - log.debug("cache add failed for '{s}': {}", .{ entry.dest, err }); + if (DiskCache.isFailure(err)) { + warnPrint( + stderr, + "unable to add '{s}' to the cache '{s}': {t}", + .{ entry.dest, entry.cache.path, err }, + ); + } else { + verbosePrint( + opts, + stderr, + "cache: skipped {s}: {t}", + .{ entry.dest, err }, + ); + } } }; @@ -370,7 +403,7 @@ const Joined = struct { fn checkExit(term: std.process.Child.Term, label: []const u8) error{ChildFailed}!void { switch (term) { - .Exited => |rc| if (rc != 0) { + .exited => |rc| if (rc != 0) { log.warn("{s} exited with non-zero status: {d}", .{ label, rc }); return error.ChildFailed; }, @@ -393,10 +426,11 @@ fn resolveDestination( &.{ ssh, "-G" }, args, }) catch return null; - const result = std.process.Child.run(.{ - .allocator = alloc, - .argv = argv, - }) catch |err| { + const result = std.process.run( + alloc, + global.io(), + .{ .argv = argv }, + ) catch |err| { log.warn("ssh -G spawn failed: {}", .{err}); return null; }; @@ -445,7 +479,7 @@ fn installRemoteTerminfo( ) !void { var buf: std.Io.Writer.Allocating = .init(alloc); defer buf.deinit(); - try ghostty_terminfo.encode(&buf.writer); + try terminfopkg.ghostty.encode(&buf.writer); const terminfo = buf.written(); // ControlPath is in TMPDIR with a short, random basename. ssh uses @@ -463,20 +497,17 @@ fn installRemoteTerminfo( // the most common failure source) and inherit ssh's stderr so it // reaches the user's terminal. Other steps stay quiet either way. const remote_script = if (opts.verbose) - \\infocmp xterm-ghostty >/dev/null 2>&1 && exit 0 \\command -v tic >/dev/null 2>&1 || exit 1 \\mkdir -p ~/.terminfo 2>/dev/null && tic -x - && exit 0 \\exit 1 else - \\infocmp xterm-ghostty >/dev/null 2>&1 && exit 0 \\command -v tic >/dev/null 2>&1 || exit 1 \\mkdir -p ~/.terminfo 2>/dev/null && tic -x - 2>/dev/null && exit 0 \\exit 1 ; // Set up an SSH ControlMaster scoped to this single install: - // - ControlMaster=yes makes our client also act as the master, - // so `infocmp | ssh tic` runs over a single connection. + // - ControlMaster=yes makes our client also act as the master. // - ControlPersist=no tears the master down when our client // exits; no socket lingers on the remote side. const argv = try std.mem.concat(alloc, []const u8, &.{ @@ -491,23 +522,23 @@ fn installRemoteTerminfo( }); verbosePrint(opts, stderr, "exec: {f}", .{Joined{ .items = argv }}); - var child: std.process.Child = .init(argv, alloc); - child.stdin_behavior = .Pipe; - child.stdout_behavior = .Ignore; - child.stderr_behavior = if (opts.verbose) .Inherit else .Ignore; - - child.spawn() catch |err| { + var child = std.process.spawn(global.io(), .{ + .argv = argv, + .stdin = .pipe, + .stdout = .ignore, + .stderr = if (opts.verbose) .inherit else .ignore, + }) catch |err| { log.warn("terminfo install spawn failed: {}", .{err}); return error.InstallFailed; }; if (child.stdin) |stdin| { - stdin.writeAll(terminfo) catch {}; - stdin.close(); + stdin.writeStreamingAll(global.io(), terminfo) catch {}; + stdin.close(global.io()); child.stdin = null; } - const term = child.wait() catch |err| { + const term = child.wait(global.io()) catch |err| { log.warn("terminfo install wait failed: {}", .{err}); return error.InstallFailed; }; @@ -515,23 +546,24 @@ fn installRemoteTerminfo( } /// Returns `128 + signum` for signal-killed children, matching shell convention. -fn childExec(alloc: Allocator, argv: []const []const u8) !u8 { - var child: std.process.Child = .init(argv, alloc); - child.stdin_behavior = .Inherit; - child.stdout_behavior = .Inherit; - child.stderr_behavior = .Inherit; - - try child.spawn(); - const term = try child.wait(); +fn childExec(argv: []const []const u8) !u8 { + var child = try std.process.spawn(global.io(), .{ + .argv = argv, + .stdin = .inherit, + .stdout = .inherit, + .stderr = .inherit, + }); + + const term = try child.wait(global.io()); return switch (term) { - .Exited => |rc| rc, - .Signal => |sig| @as(u8, 128) + @as(u8, @intCast(@min(sig, 127))), - .Stopped, .Unknown => 1, + .exited => |rc| rc, + .signal => |sig| @as(u8, 128) + @as(u8, @intCast(@min(@intFromEnum(sig), 127))), + .stopped, .unknown => 1, }; } fn parseTestArgs(alloc: Allocator, opts: *Options, line: []const u8) !void { - var iter = try std.process.ArgIteratorGeneral(.{}).init(alloc, line); + var iter = try std.process.Args.IteratorGeneral(.{}).init(alloc, line); defer iter.deinit(); try cli_args.parse(Options, alloc, opts, &iter); } diff --git a/vendor/libghostty-vt/src/cli/ssh_cache.zig b/vendor/libghostty-vt/src/cli/ssh_cache.zig index 83031e8e..5de0c2af 100644 --- a/vendor/libghostty-vt/src/cli/ssh_cache.zig +++ b/vendor/libghostty-vt/src/cli/ssh_cache.zig @@ -2,8 +2,10 @@ const std = @import("std"); const fs = std.fs; const Allocator = std.mem.Allocator; const args = @import("args.zig"); +const global = @import("../global.zig"); const Action = @import("ghostty.zig").Action; const Duration = @import("../config.zig").Config.Duration; +const terminfopkg = @import("../terminfo/main.zig"); pub const Entry = @import("ssh-cache/Entry.zig"); pub const DiskCache = @import("ssh-cache/DiskCache.zig"); @@ -48,13 +50,14 @@ pub const Options = struct { /// `--prune=6h`, `--prune=1y`. /// /// Examples: -/// ghostty +ssh-cache # List all cached destinations -/// ghostty +ssh-cache user@example.com # Show that destination -/// ghostty +ssh-cache example.com # Show all users on that host -/// ghostty +ssh-cache --add=user@example.com # Manually add a destination -/// ghostty +ssh-cache --remove=user@example.com # Remove a destination -/// ghostty +ssh-cache --prune=30d # Remove entries older than 30 days -/// ghostty +ssh-cache --clear # Clear entire cache +/// +/// ghostty +ssh-cache # List all cached destinations +/// ghostty +ssh-cache user@example.com # Show that destination +/// ghostty +ssh-cache example.com # Show all users on that host +/// ghostty +ssh-cache --add=user@example.com # Manually add a destination +/// ghostty +ssh-cache --remove=user@example.com # Remove a destination +/// ghostty +ssh-cache --prune=30d # Remove entries older than 30 days +/// ghostty +ssh-cache --clear # Clear entire cache pub fn run(alloc_gpa: Allocator) !u8 { var arena = std.heap.ArenaAllocator.init(alloc_gpa); defer arena.deinit(); @@ -64,13 +67,13 @@ pub fn run(alloc_gpa: Allocator) !u8 { defer opts.deinit(); var stdout_buffer: [1024]u8 = undefined; - var stdout_file: std.fs.File = .stdout(); - var stdout_writer = stdout_file.writer(&stdout_buffer); + var stdout_file: std.Io.File = .stdout(); + var stdout_writer = stdout_file.writer(global.io(), &stdout_buffer); const stdout = &stdout_writer.interface; var stderr_buffer: [1024]u8 = undefined; - var stderr_file: std.fs.File = .stderr(); - var stderr_writer = stderr_file.writer(&stderr_buffer); + var stderr_file: std.Io.File = .stderr(); + var stderr_writer = stderr_file.writer(global.io(), &stderr_buffer); const stderr = &stderr_writer.interface; // The cache is queried by a positional destination (`user@host` or a @@ -81,7 +84,7 @@ pub fn run(alloc_gpa: Allocator) !u8 { var query: ?[]const u8 = null; var flags: std.ArrayList([]const u8) = .empty; { - var iter = try args.argsIterator(alloc_gpa); + var iter = try args.argsIterator(alloc_gpa, global.args()); defer iter.deinit(); while (iter.next()) |arg| { const is_host_flag = std.mem.startsWith(u8, arg, "--host="); @@ -132,7 +135,24 @@ pub fn run(alloc_gpa: Allocator) !u8 { }; } - const result = runInner(alloc, opts, query, stdout, stderr); + // Setup our disk cache to the standard location + const cache_path = DiskCache.defaultPath(alloc, "ghostty") catch |err| { + try stderr.print( + "Error: unable to determine the cache path: {t}\n", + .{err}, + ); + stderr.flush() catch {}; + return 1; + }; + + const result = runInner( + alloc, + opts, + query, + .{ .path = cache_path }, + stdout, + stderr, + ); // Flushing *shouldn't* fail but... stdout.flush() catch {}; @@ -144,6 +164,7 @@ pub fn runInner( alloc: Allocator, opts: Options, query: ?[]const u8, + cache: DiskCache, stdout: *std.Io.Writer, stderr: *std.Io.Writer, ) !u8 { @@ -164,28 +185,35 @@ pub fn runInner( return 2; } - // Setup our disk cache to the standard location - const cache_path = try DiskCache.defaultPath(alloc, "ghostty"); - const cache: DiskCache = .{ .path = cache_path }; - if (opts.clear) { - try cache.clear(); + cache.clear() catch |err| { + try stderr.print( + "Error: unable to clear cache '{s}': {t}\n", + .{ cache.path, err }, + ); + return 1; + }; return 0; } if (opts.add) |dest| { - cache.add(alloc, dest, std.time.timestamp()) catch |err| switch (err) { + cache.add( + alloc, + dest, + terminfopkg.version, + std.Io.Timestamp.now(global.io(), .real).toSeconds(), + ) catch |err| switch (err) { error.InvalidCacheKey => { try stderr.print( - "Error: Invalid destination '{s}' (expected hostname or user@hostname)\n", + "Error: invalid destination '{s}' (expected hostname or user@hostname)\n", .{dest}, ); return 2; }, else => { try stderr.print( - "Error: Unable to add '{s}' to cache. Error: {}\n", - .{ dest, err }, + "Error: unable to add '{s}' to cache '{s}': {t}\n", + .{ dest, cache.path, err }, ); return 1; }, @@ -197,15 +225,15 @@ pub fn runInner( const removed = cache.remove(alloc, dest) catch |err| switch (err) { error.InvalidCacheKey => { try stderr.print( - "Error: Invalid destination '{s}' (expected hostname or user@hostname)\n", + "Error: invalid destination '{s}' (expected hostname or user@hostname)\n", .{dest}, ); return 2; }, else => { try stderr.print( - "Error: Unable to remove '{s}' from cache. Error: {}\n", - .{ dest, err }, + "Error: unable to remove '{s}' from cache '{s}': {t}\n", + .{ dest, cache.path, err }, ); return 1; }, @@ -228,14 +256,23 @@ pub fn runInner( return 2; } const pruned = cache.prune(alloc, max_age_s) catch |err| { - try stderr.print("Error: Unable to prune cache. Error: {}\n", .{err}); + try stderr.print( + "Error: unable to prune cache '{s}': {t}\n", + .{ cache.path, err }, + ); return 1; }; try stdout.print("Pruned cache entries: {d}\n", .{pruned}); return 0; } - var entries = try cache.list(alloc); + var entries = cache.list(alloc) catch |err| { + try stderr.print( + "Error: unable to read cache '{s}': {t}\n", + .{ cache.path, err }, + ); + return 1; + }; defer DiskCache.deinitEntries(alloc, &entries); // A positional query filters the listing: an exact `user@host` match, @@ -243,7 +280,7 @@ pub fn runInner( if (query) |q| { if (!DiskCache.isValidCacheKey(q)) { try stderr.print( - "Error: Invalid destination '{s}' (expected hostname or user@hostname)\n", + "Error: invalid destination '{s}' (expected hostname or user@hostname)\n", .{q}, ); return 2; @@ -295,7 +332,7 @@ fn listEntries( widest = @max(widest, entry.hostname.len); } - const now = std.time.timestamp(); + const now = std.Io.Timestamp.now(global.io(), .real).toSeconds(); for (items.items) |entry| { try writer.print("{s}", .{entry.hostname}); try writer.splatByteAll(' ', widest - entry.hostname.len + 2); @@ -469,10 +506,11 @@ test "runInner rejects multiple actions" { defer stderr.deinit(); // The check runs before any cache access, so it never touches disk. + const cache: DiskCache = .{ .path = "/nonexistent/ssh_cache" }; const code = try runInner(alloc, .{ .add = "example.com", .remove = "other.com", - }, null, &stdout.writer, &stderr.writer); + }, null, cache, &stdout.writer, &stderr.writer); try testing.expectEqual(@as(u8, 2), code); try testing.expectEqualStrings("", stdout.written()); @@ -482,7 +520,39 @@ test "runInner rejects multiple actions" { stderr.clearRetainingCapacity(); const code2 = try runInner(alloc, .{ .clear = true, - }, "example.com", &stdout.writer, &stderr.writer); + }, "example.com", cache, &stdout.writer, &stderr.writer); try testing.expectEqual(@as(u8, 2), code2); try testing.expect(std.mem.indexOf(u8, stderr.written(), "only one") != null); } + +test "runInner fails cleanly when the cache is unusable" { + const testing = std.testing; + const alloc = testing.allocator; + const io = testing.io; + + // A directory where the cache file belongs fails every operation. + var tmp = testing.tmpDir(.{}); + defer tmp.cleanup(); + try tmp.dir.createDirPath(io, "ssh_cache"); + const tmp_path = try tmp.dir.realPathFileAlloc(io, ".", alloc); + defer alloc.free(tmp_path); + const cache_path = try std.fs.path.join(alloc, &.{ tmp_path, "ssh_cache" }); + defer alloc.free(cache_path); + const cache: DiskCache = .{ .path = cache_path }; + + var stdout: std.Io.Writer.Allocating = .init(alloc); + defer stdout.deinit(); + var stderr: std.Io.Writer.Allocating = .init(alloc); + defer stderr.deinit(); + + const opts: []const Options = &.{ + .{}, // the default listing + .{ .add = "example.com" }, + .{ .remove = "example.com" }, + .{ .prune = .{ .duration = std.time.ns_per_s } }, + }; + for (opts) |o| { + const code = try runInner(alloc, o, null, cache, &stdout.writer, &stderr.writer); + try testing.expectEqual(@as(u8, 1), code); + } +} diff --git a/vendor/libghostty-vt/src/cli/toggle_quick_terminal.zig b/vendor/libghostty-vt/src/cli/toggle_quick_terminal.zig index 233700f7..6a1f043b 100644 --- a/vendor/libghostty-vt/src/cli/toggle_quick_terminal.zig +++ b/vendor/libghostty-vt/src/cli/toggle_quick_terminal.zig @@ -2,6 +2,7 @@ const std = @import("std"); const Allocator = std.mem.Allocator; const Action = @import("../cli.zig").ghostty.Action; const apprt = @import("../apprt.zig"); +const global = @import("../global.zig"); pub const Options = struct { /// If set, connect to a custom instance of Ghostty. @@ -39,7 +40,7 @@ pub const Options = struct { /// Available since: 1.4.0 pub fn run(alloc: Allocator) !u8 { var buf: [256]u8 = undefined; - var stderr_writer = std.fs.File.stderr().writer(&buf); + var stderr_writer = std.Io.File.stderr().writer(global.io(), &buf); const stderr = &stderr_writer.interface; if (apprt.App.performIpc( diff --git a/vendor/libghostty-vt/src/cli/validate_config.zig b/vendor/libghostty-vt/src/cli/validate_config.zig index 5586cf29..3d5f2886 100644 --- a/vendor/libghostty-vt/src/cli/validate_config.zig +++ b/vendor/libghostty-vt/src/cli/validate_config.zig @@ -3,6 +3,7 @@ const Allocator = std.mem.Allocator; const args = @import("args.zig"); const Action = @import("ghostty.zig").Action; const Config = @import("../config.zig").Config; +const global = @import("../global.zig"); pub const Options = struct { /// The path of the config file to validate. If this isn't specified, @@ -34,13 +35,13 @@ pub fn run(alloc: std.mem.Allocator) !u8 { defer opts.deinit(); { - var iter = try args.argsIterator(alloc); + var iter = try args.argsIterator(alloc, global.args()); defer iter.deinit(); try args.parse(Options, alloc, &opts, &iter); } var buffer: [1024]u8 = undefined; - var stdout_writer = std.fs.File.stdout().writer(&buffer); + var stdout_writer = std.Io.File.stdout().writer(global.io(), &buffer); const stdout = &stdout_writer.interface; const result = runInner(alloc, opts, stdout); try stdout_writer.end(); @@ -58,7 +59,11 @@ fn runInner( // If a config path is passed, validate it, otherwise validate default configs if (opts.@"config-file") |config_path| { var buf: [std.fs.max_path_bytes]u8 = undefined; - const abs_path = try std.fs.cwd().realpath(config_path, &buf); + const abs_path = buf[0..try std.Io.Dir.cwd().realPathFile( + global.io(), + config_path, + &buf, + )]; try cfg.loadFile(alloc, abs_path); try cfg.loadRecursiveFiles(alloc); } else { diff --git a/vendor/libghostty-vt/src/cli/version.zig b/vendor/libghostty-vt/src/cli/version.zig index cf8e66fa..ff203624 100644 --- a/vendor/libghostty-vt/src/cli/version.zig +++ b/vendor/libghostty-vt/src/cli/version.zig @@ -6,6 +6,7 @@ const build_config = @import("../build_config.zig"); const internal_os = @import("../os/main.zig"); const xev = @import("../global.zig").xev; const renderer = @import("../renderer.zig"); +const global = @import("../global.zig"); const gtk_version = @import("../apprt/gtk/gtk_version.zig"); const adw_version = @import("../apprt/gtk/adw_version.zig"); @@ -16,11 +17,14 @@ pub const Options = struct {}; /// either `+version` or `--version`. pub fn run(alloc: Allocator) !u8 { var buffer: [1024]u8 = undefined; - const stdout_file: std.fs.File = .stdout(); - var stdout_writer = stdout_file.writer(&buffer); + const stdout_file: std.Io.File = .stdout(); + var stdout_writer = stdout_file.writer(global.io(), &buffer); + + var environ_map = try global.environMap(); + defer environ_map.deinit(); const stdout = &stdout_writer.interface; - const tty = stdout_file.isTty(); + const tty = try stdout_file.isTty(global.io()); if (tty) if (build_config.version.build) |commit_hash| { try stdout.print( @@ -48,7 +52,7 @@ pub fn run(alloc: Allocator) !u8 { defer if (kernel_info) |k| alloc.free(k); try stdout.print(" - kernel version: {s}\n", .{kernel_info orelse "Kernel information unavailable"}); } - try stdout.print(" - desktop env : {t}\n", .{internal_os.desktopEnvironment()}); + try stdout.print(" - desktop env : {t}\n", .{internal_os.desktopEnvironment(&environ_map)}); try stdout.print(" - GTK version :\n", .{}); try stdout.print(" build : {f}\n", .{gtk_version.comptime_version}); try stdout.print(" runtime : {f}\n", .{gtk_version.getRuntimeVersion()}); |
