mirror of
https://github.com/ghostty-org/ghostty.git
synced 2026-09-21 21:17:40 +00:00
gtk: add +new-tab cli action
This commit is contained in:
@@ -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");
|
||||
const global = @import("../global.zig");
|
||||
|
||||
@@ -79,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",
|
||||
|
||||
@@ -165,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),
|
||||
};
|
||||
}
|
||||
@@ -207,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,
|
||||
};
|
||||
}
|
||||
|
||||
272
src/cli/new_tab.zig
Normal file
272
src/cli/new_tab.zig
Normal file
@@ -0,0 +1,272 @@
|
||||
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`, 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, 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;
|
||||
}
|
||||
Reference in New Issue
Block a user