aboutsummaryrefslogtreecommitdiff
path: root/vendor/libghostty-vt/src/cli/new_tab.zig
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/new_tab.zig
parent90e947a6e26e02f10ec7f1694c4eb5b78a86406b (diff)
chore: upgrade libghostty and preserve terminal behavior (#3906)
Diffstat (limited to 'vendor/libghostty-vt/src/cli/new_tab.zig')
-rw-r--r--vendor/libghostty-vt/src/cli/new_tab.zig277
1 files changed, 277 insertions, 0 deletions
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;
+}