diff options
| author | Can Celik <ogulcancelik@gmail.com> | 2026-09-10 21:51:57 +0300 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-09-10 21:51:57 +0300 |
| commit | 425c86179791cd32e2d4ce0ae7f269940b4b0663 (patch) | |
| tree | e52c4383a6d69216cbe2a32638bf90841d7e2189 /vendor/libghostty-vt/src/cli/new_tab.zig | |
| parent | 90e947a6e26e02f10ec7f1694c4eb5b78a86406b (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.zig | 277 |
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; +} |
