cli/gtk: move actual IPC code tp apprt

This commit is contained in:
Jeffrey C. Ollie
2025-07-12 22:59:44 -05:00
parent 824185f23e
commit 72e47cf8bc
8 changed files with 107 additions and 61 deletions

View File

@@ -1,14 +1,10 @@
const std = @import("std");
const builtin = @import("builtin");
const Allocator = std.mem.Allocator;
const ArenaAllocator = std.heap.ArenaAllocator;
const build_config = @import("../build_config.zig");
const Action = @import("../cli.zig").ghostty.Action;
const apprt = @import("../apprt.zig");
const args = @import("args.zig");
const diagnostics = @import("diagnostics.zig");
const font = @import("../font/main.zig");
const configpkg = @import("../config.zig");
const Config = configpkg.Config;
pub const Options = struct {
/// This is set by the CLI parser for deinit.
@@ -65,12 +61,26 @@ pub const Options = struct {
/// The `new-window` will use native platform IPC to open up a new window in a
/// running instance of Ghostty.
///
/// If none of `--release`, `--debug`, and `--class` flags are not set, the
/// `new-window` command will try and find the class of the running Ghostty
/// instance in the `GHOSTTY_CLASS` environment variable. If this environment
/// variable is not set, a release instance of Ghostty will be opened.
///
/// 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
@@ -78,22 +88,6 @@ pub const Options = struct {
/// of Ghostty if it is not already running. See the Ghostty website for
/// information on properly configuring D-Bus activation.
///
/// GTK uses an application ID to identify instances of applications. If
/// Ghostty is compiled with debug optimizations, the application ID will
/// be `com.mitchellh.ghostty-debug`. If Ghostty is compiled with release
/// optimizations, the application ID will be `com.mitchellh.ghostty`.
///
/// 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 application ID as
/// defined above.
///
/// The `new-window` command will try and find the application ID of the running
/// Ghostty instance in the `GHOSTTY_CLASS` environment variable. If this
/// environment variable is not set, and any of the command line flags defined
/// below are not set, a release instance of Ghostty will be opened.
///
/// Only supported on GTK.
///
/// Flags:
@@ -170,12 +164,23 @@ fn runArgs(alloc_gpa: Allocator, argsIter: anytype) !u8 {
defer arena.deinit();
const alloc = arena.allocator();
if (comptime build_config.app_runtime == .gtk) {
const new_window = @import("new_window/gtk.zig").new_window;
return try new_window(alloc, stderr, opts);
if (@hasDecl(apprt.IPC, "openNewWindow")) {
return try apprt.IPC.openNewWindow(
alloc,
stderr,
.{
.instance = instance: {
if (opts.class) |class| break :instance .{ .class = class };
if (opts.release) break :instance .release;
if (opts.debug) break :instance .debug;
break :instance .detect;
},
.arguments = opts._arguments.items,
},
);
}
// If we get here, the platform is unsupported.
try stderr.print("+new-window is unsupported on this platform.\n", .{});
// If we get here, the platform is not supported.
try stderr.print("+new-window is not supported on this platform.\n", .{});
return 1;
}

View File

@@ -1,176 +0,0 @@
const std = @import("std");
const Allocator = std.mem.Allocator;
const gio = @import("gio");
const glib = @import("glib");
const Options = @import("../new_window.zig").Options;
// 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 --release` is equivalent to the following command:
//
// ```
// gdbus call --session --dest com.mitchellh.ghostty --object-path /com/mitchellh/ghostty --method org.gtk.Actions.Activate new-window [] []
// ```
//
// `ghostty +new-window --release -e echo hello` would be equivalent to the following command:
//
// ```
// gdbus call --session --dest con.mitchellh.ghostty --object-path /com/mitchellh/ghostty --method org.gtk.Actions.Activate new-window-command '[<@as ["echo" "hello"]>]' []
// ```
pub fn new_window(alloc: Allocator, stderr: std.fs.File.Writer, opts: Options) (Allocator.Error || std.posix.WriteError)!u8 {
// 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 = result: {
// Force the usage of the class specified on the CLI to determine the
// bus name and object path.
if (opts.class) |class| {
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 };
}
// Force the usage of the release bus name and object path.
if (opts.release) {
break :result .{ "com.mitchellh.ghostty", "/com/mitchellh/ghostty" };
}
// Force the usage of the debug bus name and object path.
if (opts.debug) {
break :result .{ "com.mitchellh.ghostty-debug", "/com/mitchellh/ghostty_debug" };
}
// If there is a `GHOSTTY_CLASS` environment variable, use that as the basis
// for the bus name and object path.
if (std.posix.getenv("GHOSTTY_CLASS")) |class| {
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 };
}
// Otherwise fall back to the release bus name and object path.
break :result .{ "com.mitchellh.ghostty", "/com/mitchellh/ghostty" };
};
if (gio.Application.idIsValid(bus_name.ptr) == 0) {
try stderr.print("D-Bus bus name is not valid: {s}\n", .{bus_name});
return 1;
}
if (glib.Variant.isObjectPath(object_path.ptr) == 0) {
try stderr.print("D-Bus object path is not valid: {s}\n", .{object_path});
return 1;
}
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 1;
}
break :dbus dbus_ orelse {
try stderr.print("gio.busGetSync returned null\n", .{});
return 1;
};
};
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.unref();
// action
if (opts._arguments.items.len == 0) {
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);
if (opts._arguments.items.len > 0) {
// If `-e` was specified on the command line, he first parameter
// is an array of strings that contain the arguments that came
// afer `-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);
for (opts._arguments.items) |argument| {
command.add("s", argument.ptr);
}
parameters.add("v", command.end());
}
}
builder.addValue(parameters.end());
}
{
const platform_data = glib.VariantType.new("a{sv}");
defer glib.free(platform_data);
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 1;
}
}
return 0;
}