aboutsummaryrefslogtreecommitdiff
path: root/vendor/libghostty-vt/src/cli
diff options
context:
space:
mode:
authorCan Celik <ogulcancelik@gmail.com>2026-09-10 21:51:57 +0300
committerGitHub <noreply@github.com>2026-09-10 21:51:57 +0300
commit425c86179791cd32e2d4ce0ae7f269940b4b0663 (patch)
treee52c4383a6d69216cbe2a32638bf90841d7e2189 /vendor/libghostty-vt/src/cli
parent90e947a6e26e02f10ec7f1694c4eb5b78a86406b (diff)
chore: upgrade libghostty and preserve terminal behavior (#3906)
Diffstat (limited to 'vendor/libghostty-vt/src/cli')
-rw-r--r--vendor/libghostty-vt/src/cli/Pager.zig51
-rw-r--r--vendor/libghostty-vt/src/cli/action.zig30
-rw-r--r--vendor/libghostty-vt/src/cli/args.zig79
-rw-r--r--vendor/libghostty-vt/src/cli/boo.zig8
-rw-r--r--vendor/libghostty-vt/src/cli/crash_report.zig25
-rw-r--r--vendor/libghostty-vt/src/cli/diagnostics.zig4
-rw-r--r--vendor/libghostty-vt/src/cli/edit_config.zig132
-rw-r--r--vendor/libghostty-vt/src/cli/explain_config.zig9
-rw-r--r--vendor/libghostty-vt/src/cli/ghostty.zig30
-rw-r--r--vendor/libghostty-vt/src/cli/help.zig7
-rw-r--r--vendor/libghostty-vt/src/cli/list_actions.zig7
-rw-r--r--vendor/libghostty-vt/src/cli/list_colors.zig19
-rw-r--r--vendor/libghostty-vt/src/cli/list_fonts.zig9
-rw-r--r--vendor/libghostty-vt/src/cli/list_keybinds.zig18
-rw-r--r--vendor/libghostty-vt/src/cli/list_themes.zig215
-rw-r--r--vendor/libghostty-vt/src/cli/new_tab.zig277
-rw-r--r--vendor/libghostty-vt/src/cli/new_window.zig68
-rw-r--r--vendor/libghostty-vt/src/cli/show_config.zig5
-rw-r--r--vendor/libghostty-vt/src/cli/show_face.zig7
-rw-r--r--vendor/libghostty-vt/src/cli/ssh-cache/DiskCache.zig226
-rw-r--r--vendor/libghostty-vt/src/cli/ssh.zig138
-rw-r--r--vendor/libghostty-vt/src/cli/ssh_cache.zig132
-rw-r--r--vendor/libghostty-vt/src/cli/toggle_quick_terminal.zig3
-rw-r--r--vendor/libghostty-vt/src/cli/validate_config.zig11
-rw-r--r--vendor/libghostty-vt/src/cli/version.zig12
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()});