Jeffrey C. Ollie 2026-06-03 14:35:18 +08:00 committed by GitHub
commit 8e603ddbc3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 612 additions and 80 deletions

View File

@ -1054,6 +1054,7 @@ typedef union {
// apprt.ipc.Action.Key
typedef enum {
GHOSTTY_IPC_ACTION_NEW_WINDOW,
GHOSTTY_IPC_ACTION_NEW_TAB,
GHOSTTY_IPC_ACTION_TOGGLE_QUICK_TERMINAL,
} ghostty_ipc_action_tag_e;

View File

@ -455,7 +455,7 @@ pub const Action = union(Key) {
// At the time of writing, we don't promise ABI compatibility
// so we can change this but I want to be aware of it.
assert(@sizeOf(CValue) == switch (@sizeOf(usize)) {
4 => 16,
4 => 24,
8 => 24,
else => unreachable,
});

View File

@ -336,6 +336,7 @@ pub const App = struct {
) (Allocator.Error || std.posix.WriteError || apprt.ipc.Errors)!bool {
switch (action) {
.new_window => return false,
.new_tab => return false,
.toggle_quick_terminal => return false,
}
}

View File

@ -13,6 +13,7 @@ const CoreApp = @import("../../App.zig");
const Application = @import("class/application.zig").Application;
const Surface = @import("Surface.zig");
const ipcNewWindow = @import("ipc/new_window.zig").newWindow;
const ipcNewTab = @import("ipc/new_tab.zig").newTab;
const ipcToggleQuickTerminal = @import("ipc/toggle_quick_terminal.zig").toggleQuickTerminal;
const log = std.log.scoped(.gtk);
@ -85,6 +86,7 @@ pub fn performIpc(
) !bool {
switch (action) {
.new_window => return try ipcNewWindow(alloc, target, value),
.new_tab => return try ipcNewTab(alloc, target, value),
.toggle_quick_terminal => return try ipcToggleQuickTerminal(alloc, target),
}
}

View File

@ -708,7 +708,7 @@ pub const Application = extern struct {
.new_split => return Action.newSplit(target, value),
.new_tab => return Action.newTab(target),
.new_tab => return Action.newTab(target, .none),
.new_window => try Action.newWindow(
self,
@ -1412,9 +1412,13 @@ pub const Application = extern struct {
const as_variant_type = glib.VariantType.new("as");
defer as_variant_type.free();
const tas_variant_type = glib.VariantType.new("(tas)");
defer tas_variant_type.free();
const actions = [_]ext.actions.Action(Self){
.init("new-window", actionNewWindow, null),
.init("new-window-command", actionNewWindow, as_variant_type),
.init("new-tab", actionNewTab, tas_variant_type),
.init("open-config", actionOpenConfig, null),
.init("present-surface", actionPresentSurface, t_variant_type),
.init("quit", actionQuit, null),
@ -1700,93 +1704,207 @@ pub const Application = extern struct {
) callconv(.c) void {
log.debug("received new window action", .{});
var arena: std.heap.ArenaAllocator = .init(Application.default().allocator());
var arena: std.heap.ArenaAllocator = .init(self.core().alloc);
defer arena.deinit();
const alloc = arena.allocator();
var working_directory: ?[:0]const u8 = null;
var title: ?[:0]const u8 = null;
var command: ?configpkg.Command = null;
var args: std.ArrayList([:0]const u8) = .empty;
overrides: {
const overrides = overrides: {
// were we given a parameter?
const parameter = parameter_ orelse break :overrides;
const arguments = parameter_ orelse break :overrides null;
const as_variant_type = glib.VariantType.new("as");
defer as_variant_type.free();
// ensure that the supplied parameter is an array of strings
if (glib.Variant.isOfType(parameter, as_variant_type) == 0) {
if (glib.Variant.isOfType(arguments, as_variant_type) == 0) {
log.warn("parameter is of type '{s}', not '{s}'", .{
parameter.getTypeString(),
arguments.getTypeString(),
as_variant_type.peekString()[0..as_variant_type.getStringLength()],
});
break :overrides;
break :overrides null;
}
const s_variant_type = glib.VariantType.new("s");
defer s_variant_type.free();
var arguments_it: glib.VariantIter = undefined;
_ = arguments_it.init(arguments);
var it: glib.VariantIter = undefined;
_ = it.init(parameter);
break :overrides parseOverrides(alloc, &arguments_it) catch null;
};
var e_seen: bool = false;
var i: usize = 0;
Action.newWindow(
self,
null,
if (overrides) |o| .{
.command = o.command,
.working_directory = o.working_directory,
.title = o.title,
} else .none,
) catch |err| {
log.warn("unable to create new window: {t}", .{err});
};
}
while (it.nextValue()) |value| : (i += 1) {
defer value.unref();
/// Handle `app.new-tab` GTK action
pub fn actionNewTab(
_: *gio.SimpleAction,
parameter_: ?*glib.Variant,
self: *Self,
) callconv(.c) void {
log.debug("received new tab action", .{});
// just to be sure
if (value.isOfType(s_variant_type) == 0) continue;
const core_app = self.core();
var len: usize = undefined;
const buf = value.getString(&len);
const str = buf[0..len];
var arena: std.heap.ArenaAllocator = .init(core_app.alloc);
defer arena.deinit();
log.debug("new-window argument: {d} {s}", .{ i, str });
const alloc = arena.allocator();
if (e_seen) {
const duplicated = alloc.dupeZ(u8, str) catch |err| {
log.warn("unable to duplicate argument {d} {s}: {t}", .{ i, str, err });
break :overrides;
};
args.append(alloc, duplicated) catch |err| {
log.warn("unable to append argument {d} {s}: {t}", .{ i, str, err });
break :overrides;
};
continue;
}
const surface_id_, const overrides = result: {
// were we given a parameter?
const parameter = parameter_ orelse {
log.warn("app.new-tab did not receive a parameter", .{});
return;
};
if (std.mem.eql(u8, str, "-e")) {
e_seen = true;
continue;
}
log.warn("got parameter: {s}", .{parameter.getTypeString()});
if (lib.cutPrefix(u8, str, "--command=")) |v| {
var cmd: configpkg.Command = undefined;
cmd.parseCLI(alloc, v) catch |err| {
log.warn("unable to parse command: {t}", .{err});
continue;
};
command = cmd;
continue;
}
if (lib.cutPrefix(u8, str, "--working-directory=")) |v| {
working_directory = alloc.dupeZ(u8, std.mem.trim(u8, v, &std.ascii.whitespace)) catch |err| wd: {
log.warn("unable to duplicate working directory: {t}", .{err});
break :wd null;
};
continue;
}
if (lib.cutPrefix(u8, str, "--title=")) |v| {
title = alloc.dupeZ(u8, std.mem.trim(u8, v, &std.ascii.whitespace)) catch |err| t: {
log.warn("unable to duplicate title: {t}", .{err});
break :t null;
};
continue;
}
const tas_variant_type = glib.VariantType.new("(tas)");
defer tas_variant_type.free();
// ensure that the supplied parameter is an array of strings
if (glib.Variant.isOfType(parameter, tas_variant_type) == 0) {
log.warn("parameter is of type '{s}', not '{s}'", .{
parameter.getTypeString(),
tas_variant_type.peekString()[0..tas_variant_type.getStringLength()],
});
return;
}
var surface_id: u64 = 0;
var arguments_it_: ?*glib.VariantIter = null;
defer if (arguments_it_) |arguments_it| arguments_it.free();
parameter.get("(tas)", &surface_id, &arguments_it_);
const arguments_it = arguments_it_ orelse return;
const overrides = parseOverrides(alloc, arguments_it) catch return;
break :result .{ if (surface_id == 0) null else surface_id, overrides };
};
const surface_ = surface: {
if (surface_id_) |surface_id| find_by_id: {
break :surface core_app.findSurfaceByID(surface_id) orelse {
log.warn("new-tab: unable to find surface 0x{x:0>16}", .{surface_id});
break :find_by_id;
};
}
find_focused: {
break :surface core_app.focusedSurface() orelse {
log.warn("new-tab: unable to find surface", .{});
break :find_focused;
};
}
break :surface null;
};
if (surface_) |surface| {
if (!Action.newTab(
.{
.surface = surface,
},
.{
.command = overrides.command,
.working_directory = overrides.working_directory,
.title = overrides.title,
},
)) {
log.warn("new-tab: unable to create tab", .{});
}
} else {
Action.newWindow(
self,
null,
.{
.command = overrides.command,
.working_directory = overrides.working_directory,
.title = overrides.title,
},
) catch |err| {
log.warn("new-tab: unable to create new window: {t}", .{err});
};
}
}
fn parseOverrides(arena_alloc: Allocator, arguments_it: *glib.VariantIter) (Allocator.Error || error{ValueRequired})!struct {
command: ?configpkg.Command = null,
working_directory: ?[:0]const u8 = null,
title: ?[:0]const u8 = null,
} {
var args: std.ArrayList([:0]const u8) = .empty;
var working_directory: ?[:0]const u8 = null;
var title: ?[:0]const u8 = null;
var command: ?configpkg.Command = null;
const s_variant_type = glib.VariantType.new("s");
defer s_variant_type.free();
var e_seen: bool = false;
var i: usize = 0;
while (arguments_it.nextValue()) |value| : (i += 1) {
defer value.unref();
// just to be sure
if (value.isOfType(s_variant_type) == 0) continue;
var len: usize = undefined;
const buf = value.getString(&len);
const str = buf[0..len];
log.debug("argument: {d} {s}", .{ i, str });
if (e_seen) {
const copy = arena_alloc.dupeZ(u8, str) catch |err| {
log.warn("unable to duplicate argument {d} {s}: {t}", .{ i, str, err });
return err;
};
args.append(arena_alloc, copy) catch |err| {
log.warn("unable to append argument {d} {s}: {t}", .{ i, str, err });
return err;
};
continue;
}
if (std.mem.eql(u8, str, "-e")) {
e_seen = true;
continue;
}
if (lib.cutPrefix(u8, str, "--command=")) |v| {
var cmd: configpkg.Command = undefined;
cmd.parseCLI(arena_alloc, v) catch |err| {
log.warn("unable to parse command: {t}", .{err});
return err;
};
command = cmd;
continue;
}
if (lib.cutPrefix(u8, str, "--working-directory=")) |v| {
working_directory = arena_alloc.dupeZ(u8, std.mem.trim(u8, v, &std.ascii.whitespace)) catch |err| {
log.warn("unable to duplicate working directory: {t}", .{err});
return err;
};
continue;
}
if (lib.cutPrefix(u8, str, "--title=")) |v| {
title = arena_alloc.dupeZ(u8, std.mem.trim(u8, v, &std.ascii.whitespace)) catch |err| {
log.warn("unable to duplicate title: {t}", .{err});
return err;
};
continue;
}
}
@ -1796,12 +1914,10 @@ pub const Application = extern struct {
};
}
Action.newWindow(self, null, .{
return .{
.command = command,
.working_directory = working_directory,
.title = title,
}) catch |err| {
log.warn("unable to create new window: {t}", .{err});
};
}
@ -2221,7 +2337,16 @@ const Action = struct {
}
}
pub fn newTab(target: apprt.Target) bool {
pub fn newTab(
target: apprt.Target,
overrides: struct {
command: ?configpkg.Command = null,
working_directory: ?[:0]const u8 = null,
title: ?[:0]const u8 = null,
pub const none: @This() = .{};
},
) bool {
switch (target) {
.app => {
log.warn("new tab to app is unexpected", .{});
@ -2240,7 +2365,11 @@ const Action = struct {
log.warn("surface is not in a window, ignoring new_tab", .{});
return false;
};
window.newTab(core);
window.newTab(core, .{
.command = overrides.command,
.working_directory = overrides.working_directory,
.title = overrides.title,
});
return true;
},
}

View File

@ -390,8 +390,18 @@ pub const Window = extern struct {
/// Create a new tab with the given parent. The tab will be inserted
/// at the position dictated by the `window-new-tab-position` config.
/// The new tab will be selected.
pub fn newTab(self: *Self, parent_: ?*CoreSurface) void {
_ = self.newTabPage(parent_, .tab, .none);
pub fn newTab(self: *Self, parent_: ?*CoreSurface, overrides: struct {
command: ?configpkg.Command = null,
working_directory: ?[:0]const u8 = null,
title: ?[:0]const u8 = null,
pub const none: @This() = .{};
}) void {
_ = self.newTabPage(parent_, .tab, .{
.command = overrides.command,
.working_directory = overrides.working_directory,
.title = overrides.title,
});
}
pub fn newTabForWindow(

View File

@ -0,0 +1,71 @@
const std = @import("std");
const Allocator = std.mem.Allocator;
const glib = @import("glib");
const apprt = @import("../../../apprt.zig");
const DBus = @import("DBus.zig");
// Use a D-Bus method call to open a new tab on GTK.
// See: https://wiki.gnome.org/Projects/GLib/GApplication/DBusAPI
//
// `ghostty +new-tab` is equivalent to the following command (on a release build):
//
// ```
// gdbus call --session --dest com.mitchellh.ghostty --object-path /com/mitchellh/ghostty --method org.gtk.Actions.Activate new-tab '[<@(tas) (0, [])>]' []
// ```
//
// `ghostty +new-tab -e echo hello` would be equivalent to the following command (on a release build):
//
// ```
// gdbus call --session --dest com.mitchellh.ghostty --object-path /com/mitchellh/ghostty --method org.gtk.Actions.Activate new-tab '[<@(tas) (0, ["-e" "echo" "hello"])>]' []
// ```
pub fn newTab(alloc: Allocator, target: apprt.ipc.Target, value: apprt.ipc.Action.NewTab) (Allocator.Error || std.Io.Writer.Error || apprt.ipc.Errors)!bool {
var dbus = try DBus.init(alloc, target, "new-tab");
defer dbus.deinit(alloc);
const tas_variant_type = glib.VariantType.new("(tas)");
defer tas_variant_type.free();
var parameter: glib.VariantBuilder = undefined;
parameter.init(tas_variant_type);
errdefer parameter.clear();
{
// Add the target surface ID to the parameter.
const t = glib.Variant.newUint64(value.surface_id);
parameter.addValue(t);
}
{
// If any arguments were specified on the command line, this value is an
// array of strings that contain the arguments. They will be sent to the
// main Ghostty instance and interpreted as CLI arguments.
const as_variant_type = glib.VariantType.new("as");
defer as_variant_type.free();
const s_variant_type = glib.VariantType.new("s");
defer s_variant_type.free();
var command: glib.VariantBuilder = undefined;
command.init(as_variant_type);
errdefer command.clear();
if (value.arguments) |arguments| {
for (arguments) |argument| {
const bytes = glib.Bytes.new(argument.ptr, argument.len + 1);
defer bytes.unref();
const string = glib.Variant.newFromBytes(s_variant_type, bytes, @intFromBool(true));
command.addValue(string);
}
}
parameter.addValue(command.end());
}
dbus.addParameter(parameter.end());
try dbus.send();
return true;
}

View File

@ -70,23 +70,82 @@ pub const Action = union(enum) {
// entry. If the value type is void then only the key needs to be
// added. Ensure the order matches exactly with the Zig code.
/// The arguments to pass to Ghostty as the command.
/// Create a new window.
new_window: NewWindow,
/// Create a new tab.
new_tab: NewTab,
/// Toggle the quick terminal.
toggle_quick_terminal: void,
pub const NewWindow = struct {
/// A list of command arguments to launch in the new window. If this is
/// `null` the command configured in the config or the user's default
/// shell should be launched.
/// A list of configuration entries that will override the configuration
/// of the first surface of the new window. If this is `null` there
/// are no overrides. Note that not all configuration entries may be
/// overridden.
///
/// It is an error for this to be non-`null`, but zero length.
arguments: ?[][:0]const u8,
pub const C = extern struct {
/// null terminated list of arguments
/// it will be null itself if there are no arguments
/// A list of configuration entries that will override the
/// configuration of the first surface of the new tab. If this is
/// `null` there are no overrides. Note that not all configuration
/// entries may be overridden.
///
/// It is an error for this to be non-`null`, but zero length.
arguments: ?[*]?[*:0]const u8,
pub fn deinit(self: *NewWindow.C, alloc: Allocator) void {
if (self.arguments) |arguments| alloc.free(arguments);
}
};
pub fn cval(self: *NewWindow, alloc: Allocator) Allocator.Error!NewWindow.C {
var result: NewWindow.C = undefined;
if (self.arguments) |arguments| {
result.arguments = try alloc.alloc([*:0]const u8, arguments.len + 1);
for (arguments, 0..) |argument, i|
result.arguments[i] = argument.ptr;
// add null terminator
result.arguments[arguments.len] = null;
} else {
result.arguments = null;
}
return result;
}
};
pub const NewTab = struct {
/// The unique ID of a surface. If the ID is zero the currently focused
/// surface is assumed. This will be used to identify the window to add
/// the tab to.
surface_id: u64,
/// A list of configuration entries that will override the configuration
/// of the first surface of the new tab. If this is `null` there
/// are no overrides. Note that not all configuration entries may be
/// overridden.
///
/// It is an error for this to be non-`null`, but zero length.
arguments: ?[][:0]const u8,
pub const C = extern struct {
/// The unique ID of a surface. If the ID is zero the currently
/// focused surface is assumed. This will be used to identify the
/// window to add the tab to.
surface_id: u64,
/// A list of configuration entries that will override the
/// configuration of the first surface of the new tab. If this is
/// `null` there are no overrides. Note that not all configuration
/// entries may be overridden.
///
/// It is an error for this to be non-`null`, but zero length.
arguments: ?[*]?[*:0]const u8,
pub fn deinit(self: *NewWindow.C, alloc: Allocator) void {
@ -116,6 +175,7 @@ pub const Action = union(enum) {
/// Sync with: ghostty_ipc_action_tag_e
pub const Key = enum(c_int) {
new_window,
new_tab,
toggle_quick_terminal,
test "ghostty.h Action.Key" {
@ -162,8 +222,8 @@ pub const Action = union(enum) {
// At the time of writing, we don't promise ABI compatibility
// so we can change this but I want to be aware of it.
assert(@sizeOf(CValue) == switch (@sizeOf(usize)) {
4 => 4,
8 => 8,
4 => 12,
8 => 16,
else => unreachable,
});
}

View File

@ -21,6 +21,7 @@ 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");
/// Special commands that can be invoked via CLI flags. These are all
@ -78,6 +79,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",
@ -161,6 +165,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 +208,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,
};
}

252
src/cli/new_tab.zig Normal file
View File

@ -0,0 +1,252 @@
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 lib = @import("../lib/main.zig");
const homedir = @import("../os/homedir.zig");
const internal_os = @import("../os/main.zig");
pub const Options = struct {
/// 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 = .{},
/// Manual parse hook, collect all of the arguments after `+new-tab`.
pub fn parseManuallyHook(self: *Options, alloc: Allocator, arg: []const u8, iter: anytype) (error{InvalidValue} || std.fmt.ParseIntError || homedir.ExpandError || std.fs.Dir.RealPathAllocError || Allocator.Error)!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;
}
fn checkArg(self: *Options, alloc: Allocator, arg: []const u8) (error{InvalidValue} || std.fmt.ParseIntError || homedir.ExpandError || std.fs.Dir.RealPathAllocError || Allocator.Error)!?[:0]const u8 {
if (lib.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 (lib.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| {
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();
var expandhome_buf: [std.fs.max_path_bytes]u8 = undefined;
const expanded = try homedir.expandHome(stripped, &expandhome_buf);
var realpath_buf: [std.fs.max_path_bytes]u8 = undefined;
const realpath = try cwd.realpath(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`, 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.
///
/// * `--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);
defer iter.deinit();
var buffer: [1024]u8 = undefined;
var stderr_writer = std.fs.File.stderr().writer(&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 = .{};
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.fs.Dir = std.fs.cwd();
var buf: [std.fs.max_path_bytes]u8 = undefined;
const wd = try cwd.realpath(".", &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: {
const e = try internal_os.getenv(alloc, "GHOSTTY_SURFACE") orelse break :surface 0;
defer e.deinit(alloc);
break :surface try std.fmt.parseUnsigned(u64, e.value, 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;
}