gtk: add +new-tab cli action
parent
4b7bf0b20e
commit
0b4b2f7d22
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -387,8 +387,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(
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,6 +20,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
|
||||
|
|
@ -74,6 +75,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",
|
||||
|
||||
|
|
@ -156,6 +160,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),
|
||||
};
|
||||
}
|
||||
|
|
@ -197,6 +202,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,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
Loading…
Reference in New Issue