mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-19 18:26:13 +03:00
cli/gtk: add +new-window action (#7896)
This will (on GTK) use a D-Bus method call to tell a running Ghostty instance to open a new window. If D-Bus activation is configured properly, Ghostty does not need to be running first. This could be extended to other platforms, e.g. AppleScript on macOS. When Ghostty develops a native API, that could be used instead to create a new window.
This commit is contained in:
@ -803,6 +803,36 @@ typedef struct {
|
|||||||
ghostty_runtime_close_surface_cb close_surface_cb;
|
ghostty_runtime_close_surface_cb close_surface_cb;
|
||||||
} ghostty_runtime_config_s;
|
} ghostty_runtime_config_s;
|
||||||
|
|
||||||
|
// apprt.ipc.Target.Key
|
||||||
|
typedef enum {
|
||||||
|
GHOSTTY_IPC_TARGET_CLASS,
|
||||||
|
GHOSTTY_IPC_TARGET_DETECT,
|
||||||
|
} ghostty_ipc_target_tag_e;
|
||||||
|
|
||||||
|
typedef union {
|
||||||
|
char *klass;
|
||||||
|
} ghostty_ipc_target_u;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
ghostty_ipc_target_tag_e tag;
|
||||||
|
ghostty_ipc_target_u target;
|
||||||
|
} chostty_ipc_target_s;
|
||||||
|
|
||||||
|
// apprt.ipc.Action.NewWindow
|
||||||
|
typedef struct {
|
||||||
|
// This should be a null terminated list of strings.
|
||||||
|
const char **arguments;
|
||||||
|
} ghostty_ipc_action_new_window_s;
|
||||||
|
|
||||||
|
typedef union {
|
||||||
|
ghostty_ipc_action_new_window_s new_window;
|
||||||
|
} ghostty_ipc_action_u;
|
||||||
|
|
||||||
|
// apprt.ipc.Action.Key
|
||||||
|
typedef enum {
|
||||||
|
GHOSTTY_IPC_ACTION_NEW_WINDOW,
|
||||||
|
} ghostty_ipc_action_tag_e;
|
||||||
|
|
||||||
//-------------------------------------------------------------------
|
//-------------------------------------------------------------------
|
||||||
// Published API
|
// Published API
|
||||||
|
|
||||||
|
@ -15,6 +15,7 @@ const build_config = @import("build_config.zig");
|
|||||||
const structs = @import("apprt/structs.zig");
|
const structs = @import("apprt/structs.zig");
|
||||||
|
|
||||||
pub const action = @import("apprt/action.zig");
|
pub const action = @import("apprt/action.zig");
|
||||||
|
pub const ipc = @import("apprt/ipc.zig");
|
||||||
pub const gtk = @import("apprt/gtk.zig");
|
pub const gtk = @import("apprt/gtk.zig");
|
||||||
pub const none = @import("apprt/none.zig");
|
pub const none = @import("apprt/none.zig");
|
||||||
pub const browser = @import("apprt/browser.zig");
|
pub const browser = @import("apprt/browser.zig");
|
||||||
@ -32,6 +33,7 @@ pub const ColorScheme = structs.ColorScheme;
|
|||||||
pub const CursorPos = structs.CursorPos;
|
pub const CursorPos = structs.CursorPos;
|
||||||
pub const IMEPos = structs.IMEPos;
|
pub const IMEPos = structs.IMEPos;
|
||||||
pub const Selection = structs.Selection;
|
pub const Selection = structs.Selection;
|
||||||
|
pub const OpenNewWindowIPCOptions = structs.OpenNewWindowIPCOptions;
|
||||||
pub const SurfaceSize = structs.SurfaceSize;
|
pub const SurfaceSize = structs.SurfaceSize;
|
||||||
|
|
||||||
/// The implementation to use for the app runtime. This is comptime chosen
|
/// The implementation to use for the app runtime. This is comptime chosen
|
||||||
|
@ -319,6 +319,23 @@ pub const App = struct {
|
|||||||
else => {},
|
else => {},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Send the given IPC to a running Ghostty. Returns `true` if the action was
|
||||||
|
/// able to be performed, `false` otherwise.
|
||||||
|
///
|
||||||
|
/// Note that this is a static function. Since this is called from a CLI app (or
|
||||||
|
/// some other process that is not Ghostty) there is no full-featured apprt App
|
||||||
|
/// to use.
|
||||||
|
pub fn performIpc(
|
||||||
|
_: Allocator,
|
||||||
|
_: apprt.ipc.Target,
|
||||||
|
comptime action: apprt.ipc.Action.Key,
|
||||||
|
_: apprt.ipc.Action.Value(action),
|
||||||
|
) (Allocator.Error || std.posix.WriteError || apprt.ipc.Errors)!bool {
|
||||||
|
switch (action) {
|
||||||
|
.new_window => return false,
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Platform-specific configuration for libghostty.
|
/// Platform-specific configuration for libghostty.
|
||||||
|
@ -34,6 +34,7 @@ const terminal = @import("../../terminal/main.zig");
|
|||||||
const Config = configpkg.Config;
|
const Config = configpkg.Config;
|
||||||
const CoreApp = @import("../../App.zig");
|
const CoreApp = @import("../../App.zig");
|
||||||
const CoreSurface = @import("../../Surface.zig");
|
const CoreSurface = @import("../../Surface.zig");
|
||||||
|
const ipc = @import("ipc.zig");
|
||||||
|
|
||||||
const cgroup = @import("cgroup.zig");
|
const cgroup = @import("cgroup.zig");
|
||||||
const Surface = @import("Surface.zig");
|
const Surface = @import("Surface.zig");
|
||||||
@ -547,6 +548,23 @@ pub fn performAction(
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Send the given IPC to a running Ghostty. Returns `true` if the action was
|
||||||
|
/// able to be performed, `false` otherwise.
|
||||||
|
///
|
||||||
|
/// Note that this is a static function. Since this is called from a CLI app (or
|
||||||
|
/// some other process that is not Ghostty) there is no full-featured apprt App
|
||||||
|
/// to use.
|
||||||
|
pub fn performIpc(
|
||||||
|
alloc: Allocator,
|
||||||
|
target: apprt.ipc.Target,
|
||||||
|
comptime action: apprt.ipc.Action.Key,
|
||||||
|
value: apprt.ipc.Action.Value(action),
|
||||||
|
) (Allocator.Error || std.posix.WriteError || apprt.ipc.Errors)!bool {
|
||||||
|
switch (action) {
|
||||||
|
.new_window => return try ipc.openNewWindow(alloc, target, value),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn newTab(_: *App, target: apprt.Target) !void {
|
fn newTab(_: *App, target: apprt.Target) !void {
|
||||||
switch (target) {
|
switch (target) {
|
||||||
.app => {},
|
.app => {},
|
||||||
@ -1731,10 +1749,44 @@ fn gtkActionShowGTKInspector(
|
|||||||
|
|
||||||
fn gtkActionNewWindow(
|
fn gtkActionNewWindow(
|
||||||
_: *gio.SimpleAction,
|
_: *gio.SimpleAction,
|
||||||
_: ?*glib.Variant,
|
parameter_: ?*glib.Variant,
|
||||||
self: *App,
|
self: *App,
|
||||||
) callconv(.c) void {
|
) callconv(.c) void {
|
||||||
log.info("received new window action", .{});
|
log.debug("received new window action", .{});
|
||||||
|
|
||||||
|
parameter: {
|
||||||
|
// were we given a parameter?
|
||||||
|
const parameter = parameter_ orelse break :parameter;
|
||||||
|
|
||||||
|
const as = glib.VariantType.new("as");
|
||||||
|
defer as.free();
|
||||||
|
|
||||||
|
// ensure that the supplied parameter is an array of strings
|
||||||
|
if (glib.Variant.isOfType(parameter, as) == 0) {
|
||||||
|
log.warn("parameter is of type {s}", .{parameter.getTypeString()});
|
||||||
|
break :parameter;
|
||||||
|
}
|
||||||
|
|
||||||
|
const s = glib.VariantType.new("s");
|
||||||
|
defer s.free();
|
||||||
|
|
||||||
|
var it: glib.VariantIter = undefined;
|
||||||
|
_ = it.init(parameter);
|
||||||
|
|
||||||
|
while (it.nextValue()) |value| {
|
||||||
|
defer value.unref();
|
||||||
|
|
||||||
|
// just to be sure
|
||||||
|
if (value.isOfType(s) == 0) continue;
|
||||||
|
|
||||||
|
var len: usize = undefined;
|
||||||
|
const buf = value.getString(&len);
|
||||||
|
const str = buf[0..len];
|
||||||
|
|
||||||
|
log.debug("new-window command argument: {s}", .{str});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
_ = self.core_app.mailbox.push(.{
|
_ = self.core_app.mailbox.push(.{
|
||||||
.new_window = .{},
|
.new_window = .{},
|
||||||
}, .{ .forever = {} });
|
}, .{ .forever = {} });
|
||||||
@ -1751,7 +1803,10 @@ fn initActions(self: *App) void {
|
|||||||
// For action names:
|
// For action names:
|
||||||
// https://docs.gtk.org/gio/type_func.Action.name_is_valid.html
|
// https://docs.gtk.org/gio/type_func.Action.name_is_valid.html
|
||||||
const t = glib.ext.VariantType.newFor(u64);
|
const t = glib.ext.VariantType.newFor(u64);
|
||||||
defer glib.VariantType.free(t);
|
defer t.free();
|
||||||
|
|
||||||
|
const as = glib.VariantType.new("as");
|
||||||
|
defer as.free();
|
||||||
|
|
||||||
const actions = .{
|
const actions = .{
|
||||||
.{ "quit", gtkActionQuit, null },
|
.{ "quit", gtkActionQuit, null },
|
||||||
@ -1760,6 +1815,7 @@ fn initActions(self: *App) void {
|
|||||||
.{ "present-surface", gtkActionPresentSurface, t },
|
.{ "present-surface", gtkActionPresentSurface, t },
|
||||||
.{ "show-gtk-inspector", gtkActionShowGTKInspector, null },
|
.{ "show-gtk-inspector", gtkActionShowGTKInspector, null },
|
||||||
.{ "new-window", gtkActionNewWindow, null },
|
.{ "new-window", gtkActionNewWindow, null },
|
||||||
|
.{ "new-window-command", gtkActionNewWindow, as },
|
||||||
};
|
};
|
||||||
|
|
||||||
inline for (actions) |entry| {
|
inline for (actions) |entry| {
|
||||||
|
1
src/apprt/gtk/ipc.zig
Normal file
1
src/apprt/gtk/ipc.zig
Normal file
@ -0,0 +1 @@
|
|||||||
|
pub const openNewWindow = @import("ipc/new_window.zig").openNewWindow;
|
172
src/apprt/gtk/ipc/new_window.zig
Normal file
172
src/apprt/gtk/ipc/new_window.zig
Normal file
@ -0,0 +1,172 @@
|
|||||||
|
const std = @import("std");
|
||||||
|
const builtin = @import("builtin");
|
||||||
|
const Allocator = std.mem.Allocator;
|
||||||
|
|
||||||
|
const gio = @import("gio");
|
||||||
|
const glib = @import("glib");
|
||||||
|
const apprt = @import("../../../apprt.zig");
|
||||||
|
|
||||||
|
// Use a D-Bus method call to open a new window on GTK.
|
||||||
|
// See: https://wiki.gnome.org/Projects/GLib/GApplication/DBusAPI
|
||||||
|
//
|
||||||
|
// `ghostty +new-window` 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-window [] []
|
||||||
|
// ```
|
||||||
|
//
|
||||||
|
// `ghostty +new-window -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-window-command '[<@as ["echo" "hello"]>]' []
|
||||||
|
// ```
|
||||||
|
pub fn openNewWindow(alloc: Allocator, target: apprt.ipc.Target, value: apprt.ipc.Action.NewWindow) (Allocator.Error || std.posix.WriteError || apprt.ipc.Errors)!bool {
|
||||||
|
const stderr = std.io.getStdErr().writer();
|
||||||
|
|
||||||
|
// Get the appropriate bus name and object path for contacting the
|
||||||
|
// Ghostty instance we're interested in.
|
||||||
|
const bus_name: [:0]const u8, const object_path: [:0]const u8 = switch (target) {
|
||||||
|
.class => |class| result: {
|
||||||
|
// Force the usage of the class specified on the CLI to determine the
|
||||||
|
// bus name and object path.
|
||||||
|
const object_path = try std.fmt.allocPrintZ(alloc, "/{s}", .{class});
|
||||||
|
|
||||||
|
std.mem.replaceScalar(u8, object_path, '.', '/');
|
||||||
|
std.mem.replaceScalar(u8, object_path, '-', '_');
|
||||||
|
|
||||||
|
break :result .{ class, object_path };
|
||||||
|
},
|
||||||
|
.detect => switch (builtin.mode) {
|
||||||
|
.Debug, .ReleaseSafe => .{ "com.mitchellh.ghostty-debug", "/com/mitchellh/ghostty_debug" },
|
||||||
|
.ReleaseFast, .ReleaseSmall => .{ "com.mitchellh.ghostty", "/com/mitchellh/ghostty" },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
defer {
|
||||||
|
switch (target) {
|
||||||
|
.class => alloc.free(object_path),
|
||||||
|
.detect => {},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (gio.Application.idIsValid(bus_name.ptr) == 0) {
|
||||||
|
try stderr.print("D-Bus bus name is not valid: {s}\n", .{bus_name});
|
||||||
|
return error.IPCFailed;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (glib.Variant.isObjectPath(object_path.ptr) == 0) {
|
||||||
|
try stderr.print("D-Bus object path is not valid: {s}\n", .{object_path});
|
||||||
|
return error.IPCFailed;
|
||||||
|
}
|
||||||
|
|
||||||
|
const dbus = dbus: {
|
||||||
|
var err_: ?*glib.Error = null;
|
||||||
|
defer if (err_) |err| err.free();
|
||||||
|
|
||||||
|
const dbus_ = gio.busGetSync(.session, null, &err_);
|
||||||
|
if (err_) |err| {
|
||||||
|
try stderr.print(
|
||||||
|
"Unable to establish connection to D-Bus session bus: {s}\n",
|
||||||
|
.{err.f_message orelse "(unknown)"},
|
||||||
|
);
|
||||||
|
return error.IPCFailed;
|
||||||
|
}
|
||||||
|
|
||||||
|
break :dbus dbus_ orelse {
|
||||||
|
try stderr.print("gio.busGetSync returned null\n", .{});
|
||||||
|
return error.IPCFailed;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
defer dbus.unref();
|
||||||
|
|
||||||
|
// use a builder to create the D-Bus method call payload
|
||||||
|
const payload = payload: {
|
||||||
|
const builder_type = glib.VariantType.new("(sava{sv})");
|
||||||
|
defer glib.free(builder_type);
|
||||||
|
|
||||||
|
// Initialize our builder to build up our parameters
|
||||||
|
var builder: glib.VariantBuilder = undefined;
|
||||||
|
builder.init(builder_type);
|
||||||
|
errdefer builder.clear();
|
||||||
|
|
||||||
|
// action
|
||||||
|
if (value.arguments == null) {
|
||||||
|
builder.add("s", "new-window");
|
||||||
|
} else {
|
||||||
|
builder.add("s", "new-window-command");
|
||||||
|
}
|
||||||
|
|
||||||
|
// parameters
|
||||||
|
{
|
||||||
|
const av = glib.VariantType.new("av");
|
||||||
|
defer av.free();
|
||||||
|
|
||||||
|
var parameters: glib.VariantBuilder = undefined;
|
||||||
|
parameters.init(av);
|
||||||
|
errdefer parameters.clear();
|
||||||
|
|
||||||
|
if (value.arguments) |arguments| {
|
||||||
|
// If `-e` was specified on the command line, the first
|
||||||
|
// parameter is an array of strings that contain the arguments
|
||||||
|
// that came after `-e`, which will be interpreted as a command
|
||||||
|
// to run.
|
||||||
|
{
|
||||||
|
const as = glib.VariantType.new("as");
|
||||||
|
defer as.free();
|
||||||
|
|
||||||
|
var command: glib.VariantBuilder = undefined;
|
||||||
|
command.init(as);
|
||||||
|
errdefer command.clear();
|
||||||
|
|
||||||
|
for (arguments) |argument| {
|
||||||
|
command.add("s", argument.ptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
parameters.add("v", command.end());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.addValue(parameters.end());
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const platform_data = glib.VariantType.new("a{sv}");
|
||||||
|
defer platform_data.free();
|
||||||
|
|
||||||
|
builder.open(platform_data);
|
||||||
|
defer builder.close();
|
||||||
|
|
||||||
|
// we have no platform data
|
||||||
|
}
|
||||||
|
|
||||||
|
break :payload builder.end();
|
||||||
|
};
|
||||||
|
|
||||||
|
{
|
||||||
|
var err_: ?*glib.Error = null;
|
||||||
|
defer if (err_) |err| err.free();
|
||||||
|
|
||||||
|
const result_ = dbus.callSync(
|
||||||
|
bus_name,
|
||||||
|
object_path,
|
||||||
|
"org.gtk.Actions",
|
||||||
|
"Activate",
|
||||||
|
payload,
|
||||||
|
null, // We don't care about the return type, we don't do anything with it.
|
||||||
|
.{}, // no flags
|
||||||
|
-1, // default timeout
|
||||||
|
null, // not cancellable
|
||||||
|
&err_,
|
||||||
|
);
|
||||||
|
defer if (result_) |result| result.unref();
|
||||||
|
|
||||||
|
if (err_) |err| {
|
||||||
|
try stderr.print(
|
||||||
|
"D-Bus method call returned an error err={s}\n",
|
||||||
|
.{err.f_message orelse "(unknown)"},
|
||||||
|
);
|
||||||
|
return error.IPCFailed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
183
src/apprt/ipc.zig
Normal file
183
src/apprt/ipc.zig
Normal file
@ -0,0 +1,183 @@
|
|||||||
|
//! Inter-process Communication to a running Ghostty instance from a separate
|
||||||
|
//! process.
|
||||||
|
const std = @import("std");
|
||||||
|
const Allocator = std.mem.Allocator;
|
||||||
|
const assert = std.debug.assert;
|
||||||
|
|
||||||
|
pub const Errors = error{
|
||||||
|
/// The IPC failed. If a function returns this error, it's expected that
|
||||||
|
/// an a more specific error message will have been written to stderr (or
|
||||||
|
/// otherwise shown to the user in an appropriate way).
|
||||||
|
IPCFailed,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const Target = union(Key) {
|
||||||
|
/// Open up a new window in a custom instance of Ghostty.
|
||||||
|
class: [:0]const u8,
|
||||||
|
|
||||||
|
/// Detect which instance to open a new window in.
|
||||||
|
detect,
|
||||||
|
|
||||||
|
// Sync with: ghostty_ipc_target_tag_e
|
||||||
|
pub const Key = enum(c_int) {
|
||||||
|
class,
|
||||||
|
detect,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Sync with: ghostty_ipc_target_u
|
||||||
|
pub const CValue = extern union {
|
||||||
|
class: [*:0]const u8,
|
||||||
|
detect: void,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Sync with: ghostty_ipc_target_s
|
||||||
|
pub const C = extern struct {
|
||||||
|
key: Key,
|
||||||
|
value: CValue,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Convert to ghostty_ipc_target_s.
|
||||||
|
pub fn cval(self: Target) C {
|
||||||
|
return .{
|
||||||
|
.key = @as(Key, self),
|
||||||
|
.value = switch (self) {
|
||||||
|
.class => |class| .{ .class = class.ptr },
|
||||||
|
.detect => .{ .detect = {} },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const Action = union(enum) {
|
||||||
|
// A GUIDE TO ADDING NEW ACTIONS:
|
||||||
|
//
|
||||||
|
// 1. Add the action to the `Key` enum. The order of the enum matters
|
||||||
|
// because it maps directly to the libghostty C enum. For ABI
|
||||||
|
// compatibility, new actions should be added to the end of the enum.
|
||||||
|
//
|
||||||
|
// 2. Add the action and optional value to the Action union.
|
||||||
|
//
|
||||||
|
// 3. If the value type is not void, ensure the value is C ABI
|
||||||
|
// compatible (extern). If it is not, add a `C` decl to the value
|
||||||
|
// and a `cval` function to convert to the C ABI compatible value.
|
||||||
|
//
|
||||||
|
// 4. Update `include/ghostty.h`: add the new key, value, and union
|
||||||
|
// 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.
|
||||||
|
new_window: NewWindow,
|
||||||
|
|
||||||
|
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.
|
||||||
|
///
|
||||||
|
/// 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
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Sync with: ghostty_ipc_action_tag_e
|
||||||
|
pub const Key = enum(c_uint) {
|
||||||
|
new_window,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Sync with: ghostty_ipc_action_u
|
||||||
|
pub const CValue = cvalue: {
|
||||||
|
const key_fields = @typeInfo(Key).@"enum".fields;
|
||||||
|
var union_fields: [key_fields.len]std.builtin.Type.UnionField = undefined;
|
||||||
|
for (key_fields, 0..) |field, i| {
|
||||||
|
const action = @unionInit(Action, field.name, undefined);
|
||||||
|
const Type = t: {
|
||||||
|
const Type = @TypeOf(@field(action, field.name));
|
||||||
|
// Types can provide custom types for their CValue.
|
||||||
|
if (Type != void and @hasDecl(Type, "C")) break :t Type.C;
|
||||||
|
break :t Type;
|
||||||
|
};
|
||||||
|
|
||||||
|
union_fields[i] = .{
|
||||||
|
.name = field.name,
|
||||||
|
.type = Type,
|
||||||
|
.alignment = @alignOf(Type),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
break :cvalue @Type(.{ .@"union" = .{
|
||||||
|
.layout = .@"extern",
|
||||||
|
.tag_type = null,
|
||||||
|
.fields = &union_fields,
|
||||||
|
.decls = &.{},
|
||||||
|
} });
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Sync with: ghostty_ipc_action_s
|
||||||
|
pub const C = extern struct {
|
||||||
|
key: Key,
|
||||||
|
value: CValue,
|
||||||
|
};
|
||||||
|
|
||||||
|
comptime {
|
||||||
|
// For ABI compatibility, we expect that this is our union size.
|
||||||
|
// 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,
|
||||||
|
else => unreachable,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the value type for the given key.
|
||||||
|
pub fn Value(comptime key: Key) type {
|
||||||
|
inline for (@typeInfo(Action).@"union".fields) |field| {
|
||||||
|
const field_key = @field(Key, field.name);
|
||||||
|
if (field_key == key) return field.type;
|
||||||
|
}
|
||||||
|
|
||||||
|
unreachable;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convert to ghostty_ipc_action_s.
|
||||||
|
pub fn cval(self: Action, alloc: Allocator) C {
|
||||||
|
const value: CValue = switch (self) {
|
||||||
|
inline else => |v, tag| @unionInit(
|
||||||
|
CValue,
|
||||||
|
@tagName(tag),
|
||||||
|
if (@TypeOf(v) != void and @hasDecl(@TypeOf(v), "cval")) v.cval(alloc) else v,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
return .{
|
||||||
|
.key = @as(Key, self),
|
||||||
|
.value = value,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
@ -1,4 +1,19 @@
|
|||||||
|
const std = @import("std");
|
||||||
|
const Allocator = std.mem.Allocator;
|
||||||
|
|
||||||
const internal_os = @import("../os/main.zig");
|
const internal_os = @import("../os/main.zig");
|
||||||
|
const apprt = @import("../apprt.zig");
|
||||||
pub const resourcesDir = internal_os.resourcesDir;
|
pub const resourcesDir = internal_os.resourcesDir;
|
||||||
pub const App = struct {};
|
|
||||||
|
pub const App = struct {
|
||||||
|
/// Always return false as there is no apprt to communicate with.
|
||||||
|
pub fn performIpc(
|
||||||
|
_: Allocator,
|
||||||
|
_: apprt.ipc.Target,
|
||||||
|
comptime action: apprt.ipc.Action.Key,
|
||||||
|
_: apprt.ipc.Action.Value(action),
|
||||||
|
) !bool {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
pub const Surface = struct {};
|
pub const Surface = struct {};
|
||||||
|
@ -153,7 +153,7 @@ pub fn parse(
|
|||||||
|
|
||||||
// The error set is dependent on comptime T, so we always add
|
// The error set is dependent on comptime T, so we always add
|
||||||
// an extra error so we can have the "else" below.
|
// an extra error so we can have the "else" below.
|
||||||
const ErrSet = @TypeOf(err) || error{ Unknown, OutOfMemory };
|
const ErrSet = @TypeOf(err) || error{ Unknown, OutOfMemory } || Error;
|
||||||
const message: [:0]const u8 = switch (@as(ErrSet, @errorCast(err))) {
|
const message: [:0]const u8 = switch (@as(ErrSet, @errorCast(err))) {
|
||||||
// OOM is not recoverable since we need to allocate to
|
// OOM is not recoverable since we need to allocate to
|
||||||
// track more error messages.
|
// track more error messages.
|
||||||
|
@ -18,6 +18,7 @@ const validate_config = @import("validate_config.zig");
|
|||||||
const crash_report = @import("crash_report.zig");
|
const crash_report = @import("crash_report.zig");
|
||||||
const show_face = @import("show_face.zig");
|
const show_face = @import("show_face.zig");
|
||||||
const boo = @import("boo.zig");
|
const boo = @import("boo.zig");
|
||||||
|
const new_window = @import("new_window.zig");
|
||||||
|
|
||||||
/// Special commands that can be invoked via CLI flags. These are all
|
/// Special commands that can be invoked via CLI flags. These are all
|
||||||
/// invoked by using `+<action>` as a CLI flag. The only exception is
|
/// invoked by using `+<action>` as a CLI flag. The only exception is
|
||||||
@ -65,6 +66,9 @@ pub const Action = enum {
|
|||||||
// Boo!
|
// Boo!
|
||||||
boo,
|
boo,
|
||||||
|
|
||||||
|
// Use IPC to tell the running Ghostty to open a new window.
|
||||||
|
@"new-window",
|
||||||
|
|
||||||
pub fn detectSpecialCase(arg: []const u8) ?SpecialCase(Action) {
|
pub fn detectSpecialCase(arg: []const u8) ?SpecialCase(Action) {
|
||||||
// If we see a "-e" and we haven't seen a command yet, then
|
// If we see a "-e" and we haven't seen a command yet, then
|
||||||
// we are done looking for commands. This special case enables
|
// we are done looking for commands. This special case enables
|
||||||
@ -136,6 +140,7 @@ pub const Action = enum {
|
|||||||
.@"crash-report" => try crash_report.run(alloc),
|
.@"crash-report" => try crash_report.run(alloc),
|
||||||
.@"show-face" => try show_face.run(alloc),
|
.@"show-face" => try show_face.run(alloc),
|
||||||
.boo => try boo.run(alloc),
|
.boo => try boo.run(alloc),
|
||||||
|
.@"new-window" => try new_window.run(alloc),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -174,6 +179,7 @@ pub const Action = enum {
|
|||||||
.@"crash-report" => crash_report.Options,
|
.@"crash-report" => crash_report.Options,
|
||||||
.@"show-face" => show_face.Options,
|
.@"show-face" => show_face.Options,
|
||||||
.boo => boo.Options,
|
.boo => boo.Options,
|
||||||
|
.@"new-window" => new_window.Options,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
172
src/cli/new_window.zig
Normal file
172
src/cli/new_window.zig
Normal file
@ -0,0 +1,172 @@
|
|||||||
|
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");
|
||||||
|
|
||||||
|
pub const Options = struct {
|
||||||
|
/// This is set by the CLI parser for deinit.
|
||||||
|
_arena: ?ArenaAllocator = null,
|
||||||
|
|
||||||
|
/// If set, open up a new window in a custom instance of Ghostty.
|
||||||
|
class: ?[:0]const u8 = null,
|
||||||
|
|
||||||
|
/// If `-e` is found in the arguments, this will contain all of the
|
||||||
|
/// arguments to pass to Ghostty as the command.
|
||||||
|
_arguments: ?[][:0]const u8 = null,
|
||||||
|
|
||||||
|
/// 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, used to deal with `-e`
|
||||||
|
pub fn parseManuallyHook(self: *Options, alloc: Allocator, arg: []const u8, iter: anytype) Allocator.Error!bool {
|
||||||
|
// If it's not `-e` continue with the standard argument parsning.
|
||||||
|
if (!std.mem.eql(u8, arg, "-e")) return true;
|
||||||
|
|
||||||
|
var arguments: std.ArrayListUnmanaged([:0]const u8) = .empty;
|
||||||
|
errdefer {
|
||||||
|
for (arguments.items) |argument| alloc.free(argument);
|
||||||
|
arguments.deinit(alloc);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise gather up the rest of the arguments to use as the command.
|
||||||
|
while (iter.next()) |param| {
|
||||||
|
try arguments.append(alloc, try alloc.dupeZ(u8, param));
|
||||||
|
}
|
||||||
|
|
||||||
|
self._arguments = try arguments.toOwnedSlice(alloc);
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
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-window` will use native platform IPC to open up a new window in a
|
||||||
|
/// running instance of Ghostty.
|
||||||
|
///
|
||||||
|
/// If the `--class` flag is not set, the `new-window` command will try and
|
||||||
|
/// connect to a running instance of Ghostty based on what optimizations the
|
||||||
|
/// Ghostty CLI was compiled with. Otherwise the `new-window` command will try
|
||||||
|
/// and contact a running Ghostty instance that was configured with the same
|
||||||
|
/// `class` as was given on the command line.
|
||||||
|
///
|
||||||
|
/// If the `-e` flag is included on the command line, any arguments that follow
|
||||||
|
/// will be sent to the running Ghostty instance and used as the command to run
|
||||||
|
/// in the new window rather than the default. If `-e` is not specified, Ghostty
|
||||||
|
/// will use the default command (either specified with `command` in your config
|
||||||
|
/// or your default shell as configured on your system).
|
||||||
|
///
|
||||||
|
/// 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 window, 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.
|
||||||
|
///
|
||||||
|
/// * `-e`: Any arguments after this will be interpreted as a command to
|
||||||
|
/// execute inside the new window instead of the default command.
|
||||||
|
///
|
||||||
|
/// Available since: 1.2.0
|
||||||
|
pub fn run(alloc: Allocator) !u8 {
|
||||||
|
var iter = try args.argsIterator(alloc);
|
||||||
|
defer iter.deinit();
|
||||||
|
return try runArgs(alloc, &iter);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn runArgs(alloc_gpa: Allocator, argsIter: anytype) !u8 {
|
||||||
|
const stderr = std.io.getStdErr().writer();
|
||||||
|
|
||||||
|
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.writeAll("config error: ");
|
||||||
|
try diagnostic.write(stderr);
|
||||||
|
try stderr.writeAll("\n");
|
||||||
|
exit = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (exit) return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (opts._arguments) |arguments| {
|
||||||
|
if (arguments.len == 0) {
|
||||||
|
try stderr.print("The -e flag was specified on the command line, but no other arguments were found.\n", .{});
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var arena = ArenaAllocator.init(alloc_gpa);
|
||||||
|
defer arena.deinit();
|
||||||
|
const alloc = arena.allocator();
|
||||||
|
|
||||||
|
if (apprt.App.performIpc(
|
||||||
|
alloc,
|
||||||
|
if (opts.class) |class| .{ .class = class } else .detect,
|
||||||
|
.new_window,
|
||||||
|
.{
|
||||||
|
.arguments = opts._arguments,
|
||||||
|
},
|
||||||
|
) 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-window is not supported on this platform.\n", .{});
|
||||||
|
return 1;
|
||||||
|
}
|
Reference in New Issue
Block a user