aboutsummaryrefslogtreecommitdiff
path: root/vendor/libghostty-vt/src/apprt/gtk/class/application.zig
diff options
context:
space:
mode:
authorCan Celik <ogulcancelik@gmail.com>2026-09-10 21:51:57 +0300
committerGitHub <noreply@github.com>2026-09-10 21:51:57 +0300
commit425c86179791cd32e2d4ce0ae7f269940b4b0663 (patch)
treee52c4383a6d69216cbe2a32638bf90841d7e2189 /vendor/libghostty-vt/src/apprt/gtk/class/application.zig
parent90e947a6e26e02f10ec7f1694c4eb5b78a86406b (diff)
chore: upgrade libghostty and preserve terminal behavior (#3906)
Diffstat (limited to 'vendor/libghostty-vt/src/apprt/gtk/class/application.zig')
-rw-r--r--vendor/libghostty-vt/src/apprt/gtk/class/application.zig756
1 files changed, 642 insertions, 114 deletions
diff --git a/vendor/libghostty-vt/src/apprt/gtk/class/application.zig b/vendor/libghostty-vt/src/apprt/gtk/class/application.zig
index a627086e..5b9d3bf2 100644
--- a/vendor/libghostty-vt/src/apprt/gtk/class/application.zig
+++ b/vendor/libghostty-vt/src/apprt/gtk/class/application.zig
@@ -1,3 +1,4 @@
+const builtin = @import("builtin");
const std = @import("std");
const assert = @import("../../../quirks.zig").inlineAssert;
const Allocator = std.mem.Allocator;
@@ -5,25 +6,27 @@ const adw = @import("adw");
const gdk = @import("gdk");
const gio = @import("gio");
const glib = @import("glib");
+const glibunix = @import("glibunix");
const gobject = @import("gobject");
const gtk = @import("gtk");
const build_config = @import("../../../build_config.zig");
const build_info = @import("../build/info.zig");
-const state = &@import("../../../global.zig").state;
+const cli = @import("../../../cli.zig");
+const global = @import("../../../global.zig");
const i18n = @import("../../../os/main.zig").i18n;
const apprt = @import("../../../apprt.zig");
const CoreApp = @import("../../../App.zig");
+const compat_file = @import("../../../lib/compat/file.zig");
const configpkg = @import("../../../config.zig");
const input = @import("../../../input.zig");
const internal_os = @import("../../../os/main.zig");
const systemd = @import("../../../os/systemd.zig");
const terminal = @import("../../../terminal/main.zig");
-const xev = @import("../../../global.zig").xev;
+const xev = global.xev;
const Binding = @import("../../../input.zig").Binding;
const CoreConfig = configpkg.Config;
const CoreSurface = @import("../../../Surface.zig");
-const lib = @import("../../../lib/main.zig");
const ext = @import("../ext.zig");
const key = @import("../key.zig");
@@ -42,9 +45,12 @@ const CloseConfirmationDialog = @import("close_confirmation_dialog.zig").CloseCo
const ConfigErrorsDialog = @import("config_errors_dialog.zig").ConfigErrorsDialog;
const GlobalShortcuts = @import("global_shortcuts.zig").GlobalShortcuts;
const OpenURI = @import("../portal.zig").OpenURI;
+const media = @import("../media.zig");
const log = std.log.scoped(.gtk_ghostty_application);
+extern "c" fn setenv(name: ?[*]const u8, value: ?[*]const u8, overwrite: c_int) c_int;
+
/// Function used to funnel GLib/GObject/GTK log messages into Zig's logging
/// system rather than just getting dumped directly to stderr.
fn glibLogWriterFunction(
@@ -218,6 +224,12 @@ pub const Application = extern struct {
open_uri: OpenURI = undefined,
+ // The audio bell's MediaFile, reused across bells so we don't leak a
+ // GStreamer pipeline (and its GL threads) on every ring. Built lazily
+ // on the first audio bell and rebuilt when `bell-audio-path` changes;
+ // unref'd on dispose. See ringBell and media.zig.
+ bell_media: ?*gtk.MediaFile = null,
+
pub var offset: c_int = 0;
};
@@ -271,15 +283,17 @@ pub const Application = extern struct {
};
defer config.deinit();
+ // Set the old language,
const saved_language: ?[:0]const u8 = saved_language: {
const old_language = old_language: {
- const result = (internal_os.getenv(alloc, "LANG") catch break :old_language null) orelse break :old_language null;
- defer result.deinit(alloc);
- break :old_language alloc.dupeZ(u8, result.value) catch break :old_language null;
+ const lang = global.environ().getPosix("LANG") orelse break :old_language null;
+ break :old_language alloc.dupeSentinel(u8, @ptrCast(lang), 0) catch null;
};
-
- if (config.language) |language| _ = internal_os.setenv("LANG", language);
-
+ if (config.language) |language| {
+ // Override LANG if we need to (sync global environs if so)
+ _ = setenv("LANG", @ptrCast(language), 1);
+ global.syncEnviron();
+ }
break :saved_language old_language;
};
@@ -294,7 +308,7 @@ pub const Application = extern struct {
// Setup our GTK init env vars
setGtkEnv(&config) catch |err| switch (err) {
- error.NoSpaceLeft => {
+ error.WriteFailed => {
// If we fail to set GTK environment variables then we still
// try to start the application...
log.warn(
@@ -338,7 +352,7 @@ pub const Application = extern struct {
// I'm unsure of any scenario where this happens. Because we don't
// want to litter null checks everywhere, we just exit here.
log.warn("gdk display is null, exiting", .{});
- std.posix.exit(1);
+ std.process.exit(1);
};
// Setup our windowing protocol logic
@@ -696,6 +710,11 @@ pub const Application = extern struct {
.initial_size => return Action.initialSize(target, value),
.inspector => return Action.controlInspector(target, value),
+ .export_terminal_io => return try Action.exportTerminalIO(
+ self,
+ target,
+ value,
+ ),
.key_sequence => return Action.keySequence(target, value),
.key_table => return Action.keyTable(target, value),
@@ -705,10 +724,11 @@ pub const Application = extern struct {
.mouse_visibility => Action.mouseVisibility(target, value),
.move_tab => return Action.moveTab(target, value),
+ .move_tab_to_new_window => return Action.moveTabToNewWindow(target),
.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,
@@ -719,7 +739,7 @@ pub const Application = extern struct {
.none,
),
- .open_config => return Action.openConfig(self),
+ .open_config => return Action.openConfig(self, value),
.open_url => Action.openUrl(self, value),
@@ -750,6 +770,7 @@ pub const Application = extern struct {
.set_title => Action.setTitle(target, value),
.set_tab_title => return Action.setTabTitle(target, value),
+ .set_window_title => return Action.setWindowTitle(target, value),
.show_child_exited => return Action.showChildExited(target, value),
@@ -1016,6 +1037,38 @@ pub const Application = extern struct {
\\}
\\
\\/*
+ \\ * Drag and Drop Overlay
+ \\ */
+ \\.drop-overlay.drop-left {
+ \\ background: linear-gradient(
+ \\ to left,
+ \\ transparent, 50%,
+ \\ color-mix(in srgb, var(--accent-bg-color), transparent 80%) 50%
+ \\ );
+ \\}
+ \\.drop-overlay.drop-right {
+ \\ background: linear-gradient(
+ \\ to right,
+ \\ transparent, 50%,
+ \\ color-mix(in srgb, var(--accent-bg-color), transparent 80%) 50%
+ \\ );
+ \\}
+ \\.drop-overlay.drop-top {
+ \\ background: linear-gradient(
+ \\ to top,
+ \\ transparent, 50%,
+ \\ color-mix(in srgb, var(--accent-bg-color), transparent 80%) 50%
+ \\ );
+ \\}
+ \\.drop-overlay.drop-bottom {
+ \\ background: linear-gradient(
+ \\ to bottom,
+ \\ transparent, 50%,
+ \\ color-mix(in srgb, var(--accent-bg-color), transparent 80%) 50%
+ \\ );
+ \\}
+ \\
+ \\/*
\\ * Splits
\\ */
\\
@@ -1063,7 +1116,11 @@ pub const Application = extern struct {
}
}
- fn loadCustomCss(self: *Self) (std.fs.File.ReadError || Allocator.Error)!void {
+ const LoadCustomCssError = std.Io.File.OpenError ||
+ compat_file.ReadToEndAllocError ||
+ std.mem.Allocator.Error;
+
+ fn loadCustomCss(self: *Self) LoadCustomCssError!void {
const priv: *Private = self.private();
const alloc = self.allocator();
const display = gdk.Display.getDefault() orelse {
@@ -1087,7 +1144,11 @@ pub const Application = extern struct {
.optional => |path| .{ path, true },
.required => |path| .{ path, false },
};
- const file = std.fs.openFileAbsolute(path, .{}) catch |err| {
+ const file = std.Io.Dir.openFileAbsolute(
+ global.io(),
+ path,
+ .{},
+ ) catch |err| {
if (err != error.FileNotFound or !optional) {
log.warn(
"error opening gtk-custom-css file {s}: {}",
@@ -1096,12 +1157,14 @@ pub const Application = extern struct {
}
continue;
};
- defer file.close();
+ defer file.close(global.io());
const css_file_size_limit = 5 * 1024 * 1024; // 5MB
log.info("loading gtk-custom-css path={s}", .{path});
- const contents = file.readToEndAlloc(
+
+ const contents = compat_file.readToEndAlloc(
+ file,
alloc,
css_file_size_limit,
) catch |err| switch (err) {
@@ -1111,6 +1174,7 @@ pub const Application = extern struct {
},
else => |e| return e,
};
+
defer alloc.free(contents);
const bytes = glib.Bytes.new(contents.ptr, contents.len);
@@ -1141,7 +1205,8 @@ pub const Application = extern struct {
fn syncActionAccelerators(self: *Self) void {
self.syncActionAccelerator("app.quit", .{ .quit = {} });
- self.syncActionAccelerator("app.open-config", .{ .open_config = {} });
+ self.syncActionAccelerator("app.open-config::os-open", .{ .open_config = .os_open });
+ self.syncActionAccelerator("app.open-config::new-window", .{ .open_config = .new_window });
self.syncActionAccelerator("app.reload-config", .{ .reload_config = {} });
self.syncActionAccelerator("win.toggle-inspector", .{ .inspector = .toggle });
self.syncActionAccelerator("app.show-gtk-inspector", .show_gtk_inspector);
@@ -1400,8 +1465,8 @@ pub const Application = extern struct {
fn startupSignals(self: *Self) void {
const priv = self.private();
assert(priv.signal_source == null);
- priv.signal_source = glib.unixSignalAdd(
- std.posix.SIG.USR2,
+ priv.signal_source = glibunix.signalAdd(
+ @intFromEnum(std.posix.SIG.USR2),
handleSigusr2,
self,
);
@@ -1415,14 +1480,22 @@ 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 s_variant_type = glib.ext.VariantType.newFor([:0]const u8);
+ defer s_variant_type.free();
+
const actions = [_]ext.actions.Action(Self){
.init("new-window", actionNewWindow, null),
.init("new-window-command", actionNewWindow, as_variant_type),
- .init("open-config", actionOpenConfig, null),
+ .init("new-tab", actionNewTab, tas_variant_type),
+ .init("open-config", actionOpenConfig, s_variant_type),
.init("present-surface", actionPresentSurface, t_variant_type),
.init("quit", actionQuit, null),
.init("reload-config", actionReloadConfig, null),
.init("toggle-quick-terminal", actionToggleQuickTerminal, null),
+ .init("ring-bell", actionRingBell, null),
};
ext.actions.add(Self, self, &actions);
@@ -1454,6 +1527,14 @@ pub const Application = extern struct {
self,
.{},
);
+
+ _ = GlobalShortcuts.signals.@"bind-failed".connect(
+ priv.global_shortcuts,
+ *Application,
+ globalShortcutBindFailed,
+ self,
+ .{},
+ );
}
fn activate(self: *Self) callconv(.c) void {
@@ -1461,7 +1542,7 @@ pub const Application = extern struct {
// Queue a new window
const priv = self.private();
- _ = priv.core_app.mailbox.push(.{
+ _ = priv.core_app.mailbox.push(global.io(), .{
.new_window = .{},
}, .{ .forever = {} });
@@ -1478,7 +1559,7 @@ pub const Application = extern struct {
diag.close();
diag.unref(); // strong ref from get()
}
- priv.config_errors_dialog.set(null);
+ priv.config_errors_dialog.deinit();
if (priv.signal_source) |v| {
if (glib.Source.remove(v) == 0) {
log.warn("unable to remove signal source", .{});
@@ -1486,6 +1567,11 @@ pub const Application = extern struct {
priv.signal_source = null;
}
+ if (priv.bell_media) |v| {
+ v.unref();
+ priv.bell_media = null;
+ }
+
gobject.Object.virtual_methods.dispose.call(
Class.parent,
self.as(Parent),
@@ -1662,6 +1748,41 @@ pub const Application = extern struct {
};
}
+ /// May fire before any window exists, hence a desktop notification
+ /// rather than a toast.
+ fn globalShortcutBindFailed(
+ _: *GlobalShortcuts,
+ failure: *const GlobalShortcuts.BindFailed,
+ self: *Self,
+ ) callconv(.c) void {
+ var label_buf: [128]u8 = undefined;
+ var writer: std.Io.Writer = .fixed(&label_buf);
+ const label: []const u8 = label: {
+ const ok = key.labelFromTrigger(&writer, failure.trigger) catch false;
+ break :label if (ok) writer.buffered() else "?";
+ };
+
+ const detail: [*:0]const u8 = detail: {
+ if (failure.message[0] != 0) break :detail failure.message;
+ break :detail if (failure.revoked)
+ i18n._("The keybind was revoked by the system.")
+ else
+ i18n._("The keybind was denied by the system.");
+ };
+
+ var body_buf: [512]u8 = undefined;
+ const body = std.fmt.bufPrintZ(
+ &body_buf,
+ "{s}: {s}",
+ .{ label, detail },
+ ) catch return;
+
+ Action.desktopNotification(self, .app, .{
+ .title = std.mem.span(i18n._("Global keybind unavailable")),
+ .body = body,
+ });
+ }
+
fn actionReloadConfig(
_: *gio.SimpleAction,
_: ?*glib.Variant,
@@ -1703,93 +1824,227 @@ 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,
+ .shell_integration = o.shell_integration,
+ .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;
- };
+ 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,
+ .shell_integration = overrides.shell_integration,
+ .working_directory = overrides.working_directory,
+ .title = overrides.title,
+ },
+ )) {
+ log.warn("new-tab: unable to create tab", .{});
+ }
+ } else {
+ Action.newWindow(
+ self,
+ null,
+ .{
+ .command = overrides.command,
+ .shell_integration = overrides.shell_integration,
+ .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,
+ shell_integration: ?configpkg.Config.ShellIntegration = 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;
+ var parsed_shell_integration: struct {
+ @"shell-integration": ?configpkg.Config.ShellIntegration = 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 (std.mem.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 (std.mem.cutPrefix(u8, str, "--shell-integration=")) |v| {
+ cli.args.parseIntoField(
+ @TypeOf(parsed_shell_integration),
+ arena_alloc,
+ &parsed_shell_integration,
+ "shell-integration",
+ std.mem.trim(u8, v, &std.ascii.whitespace),
+ ) catch |err| {
+ log.warn("unable to parse shell integration {s}: {t}", .{ v, err });
continue;
- }
+ };
+ continue;
+ }
+ if (std.mem.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 (std.mem.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;
}
}
@@ -1799,21 +2054,52 @@ pub const Application = extern struct {
};
}
- Action.newWindow(self, null, .{
+ return .{
.command = command,
+ .shell_integration = parsed_shell_integration.@"shell-integration",
.working_directory = working_directory,
.title = title,
- }) catch |err| {
- log.warn("unable to create new window: {t}", .{err});
};
}
pub fn actionOpenConfig(
_: *gio.SimpleAction,
- _: ?*glib.Variant,
+ parameter_: ?*glib.Variant,
self: *Self,
) callconv(.c) void {
- _ = self.core().mailbox.push(.open_config, .forever);
+ const target: CoreApp.Message.OpenConfig = target: {
+ const target_map: std.StaticStringMap(CoreApp.Message.OpenConfig) = .initComptime(&.{
+ .{ "os-open", .os_open },
+ .{ "new-window", .new_window },
+ });
+
+ const parameter = parameter_ orelse {
+ log.warn("no parameter provided to app.open-config action", .{});
+ break :target .os_open;
+ };
+
+ const s_variant_type = glib.ext.VariantType.newFor([:0]const u8);
+ defer s_variant_type.free();
+
+ if (parameter.isOfType(s_variant_type) == 0) {
+ log.warn("parameter to app.open-config action is of type '{s}' not '{s}'", .{
+ parameter.getTypeString(),
+ s_variant_type.peekString()[0..s_variant_type.getStringLength()],
+ });
+ break :target .os_open;
+ }
+
+ var len: usize = undefined;
+ const buf = parameter.getString(&len);
+ const str = buf[0..len];
+
+ break :target target_map.get(str) orelse {
+ log.warn("'{s}' is not configured as a target for the app.open-config action", .{str});
+ break :target .os_open;
+ };
+ };
+
+ _ = self.core().mailbox.push(global.io(), .{ .open_config = target }, .forever);
}
fn actionPresentSurface(
@@ -1839,6 +2125,7 @@ pub const Application = extern struct {
const surface = self.core().findSurfaceByID(surface_id) orelse return;
_ = self.core().mailbox.push(
+ global.io(),
.{
.surface_message = .{
.surface = surface,
@@ -1849,6 +2136,40 @@ pub const Application = extern struct {
);
}
+ pub fn actionRingBell(
+ _: *gio.SimpleAction,
+ _: ?*glib.Variant,
+ self: *Self,
+ ) callconv(.c) void {
+ const priv: *Private = self.private();
+ const config = priv.config.get();
+
+ // Do our sound
+ if (config.@"bell-features".audio) audio: {
+ const config_path = config.@"bell-audio-path" orelse break :audio;
+ const path, const required = switch (config_path) {
+ .optional => |path| .{ path, false },
+ .required => |path| .{ path, true },
+ };
+
+ const volume = std.math.clamp(
+ config.@"bell-audio-volume",
+ 0.0,
+ 1.0,
+ );
+
+ // Reuse one MediaFile per application (rebuilt only when the path
+ // changes) so each bell replays the same pipeline instead of
+ // leaking a fresh one. Assign unconditionally: bellMediaFile frees
+ // any stale MediaFile and returns the current slot value (possibly
+ // null if the path is now inaccessible), so priv.bell_media never
+ // dangles.
+ priv.bell_media = media.bellMediaFile(priv.bell_media, path, required);
+ const media_file = priv.bell_media orelse break :audio;
+ media.playBell(media_file, volume);
+ }
+ }
+
//----------------------------------------------------------------
// Boilerplate/Noise
@@ -1866,11 +2187,8 @@ pub const Application = extern struct {
fn init(class: *Class) callconv(.c) void {
// Register our compiled resources exactly once.
{
- const c = @cImport({
- // generated header files
- @cInclude("ghostty_resources.h");
- });
- if (c.ghostty_get_resource()) |ptr| {
+ const ghostty_gtk_resources = @import("ghostty_gtk_resources");
+ if (ghostty_gtk_resources.ghostty_get_resource()) |ptr| {
gio.resourcesRegister(@ptrCast(@alignCast(ptr)));
} else {
// If we fail to load resources then things will
@@ -1894,11 +2212,11 @@ pub const Application = extern struct {
};
pub fn openUrlFallback(self: *Application, kind: apprt.action.OpenUrl.Kind, url: []const u8) void {
+ _ = self;
// Fallback to the minimal cross-platform way of opening a URL.
// This is always a safe fallback and enables for example Windows
// to open URLs (GTK on Windows via WSL is a thing).
internal_os.open(
- self.allocator(),
kind,
url,
) catch |err| log.warn("unable to open url: {}", .{err});
@@ -1938,6 +2256,98 @@ const Action = struct {
};
}
+ pub fn exportTerminalIO(
+ self: *Application,
+ target: apprt.Target,
+ value: apprt.Action.Value(.export_terminal_io),
+ ) Allocator.Error!bool {
+ const surface = switch (target) {
+ .app => return false,
+ .surface => |v| v.rt_surface.gobj(),
+ };
+
+ const alloc = self.allocator();
+ const contents = try alloc.dupe(u8, value.contents);
+ errdefer alloc.free(contents);
+ const request = try alloc.create(ExportTerminalIORequest);
+ errdefer alloc.destroy(request);
+ request.* = .{
+ .alloc = alloc,
+ .contents = contents,
+ };
+
+ const parent = ext.getAncestor(gtk.Window, surface.as(gtk.Widget));
+ const dialog = gtk.FileChooserNative.new(
+ i18n._("Export Terminal IO Events"),
+ parent,
+ .save,
+ i18n._("Export"),
+ i18n._("Cancel"),
+ );
+ const chooser = dialog.as(gtk.FileChooser);
+ chooser.setCreateFolders(1);
+ chooser.setCurrentName("ghostty-terminal-io.txt");
+
+ _ = gtk.NativeDialog.signals.response.connect(
+ dialog,
+ *ExportTerminalIORequest,
+ ExportTerminalIORequest.response,
+ request,
+ .{ .destroyData = ExportTerminalIORequest.destroy },
+ );
+ dialog.as(gtk.NativeDialog).show();
+ return true;
+ }
+
+ const ExportTerminalIORequest = struct {
+ alloc: Allocator,
+ contents: []u8,
+
+ fn destroy(self: *ExportTerminalIORequest) callconv(.c) void {
+ self.alloc.free(self.contents);
+ self.alloc.destroy(self);
+ }
+
+ fn response(
+ dialog: *gtk.FileChooserNative,
+ response_id: c_int,
+ self: *ExportTerminalIORequest,
+ ) callconv(.c) void {
+ defer dialog.unref();
+
+ if (response_id != @intFromEnum(gtk.ResponseType.accept)) return;
+
+ const file = dialog.as(gtk.FileChooser).getFile() orelse {
+ log.warn("inspector export dialog returned no file", .{});
+ return;
+ };
+ defer file.unref();
+
+ var gerr: ?*glib.Error = null;
+ const replaced = file.replaceContents(
+ self.contents.ptr,
+ self.contents.len,
+ null,
+ 0,
+ .{},
+ null,
+ null,
+ &gerr,
+ );
+ if (gerr) |err| {
+ defer err.free();
+ log.err(
+ "failed to export terminal IO events err={s}",
+ .{err.f_message orelse "(unknown)"},
+ );
+ return;
+ }
+ if (replaced == 0) {
+ log.err("failed to export terminal IO events", .{});
+ }
+ }
+ };
+
pub fn configChange(
self: *Application,
target: apprt.Target,
@@ -2202,6 +2612,26 @@ const Action = struct {
}
}
+ pub fn moveTabToNewWindow(
+ target: apprt.Target,
+ ) bool {
+ switch (target) {
+ .app => return false,
+ .surface => |core| {
+ const surface = core.rt_surface.surface;
+ const window = ext.getAncestor(
+ Window,
+ surface.as(gtk.Widget),
+ ) orelse {
+ log.warn("surface is not in a window, ignoring new_tab_to_new_window", .{});
+ return false;
+ };
+
+ return window.moveTabToNewWindow(surface);
+ },
+ }
+ }
+
pub fn newSplit(
target: apprt.Target,
direction: apprt.action.SplitDirection,
@@ -2224,7 +2654,17 @@ const Action = struct {
}
}
- pub fn newTab(target: apprt.Target) bool {
+ pub fn newTab(
+ target: apprt.Target,
+ overrides: struct {
+ command: ?configpkg.Command = null,
+ shell_integration: ?configpkg.Config.ShellIntegration = 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", .{});
@@ -2243,7 +2683,12 @@ 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,
+ .shell_integration = overrides.shell_integration,
+ .working_directory = overrides.working_directory,
+ .title = overrides.title,
+ });
return true;
},
}
@@ -2254,6 +2699,7 @@ const Action = struct {
parent: ?*CoreSurface,
overrides: struct {
command: ?configpkg.Command = null,
+ shell_integration: ?configpkg.Config.ShellIntegration = null,
working_directory: ?[:0]const u8 = null,
title: ?[:0]const u8 = null,
@@ -2276,6 +2722,7 @@ const Action = struct {
parent,
.{
.command = overrides.command,
+ .shell_integration = overrides.shell_integration,
.working_directory = overrides.working_directory,
.title = overrides.title,
},
@@ -2288,6 +2735,7 @@ const Action = struct {
parent: ?*CoreSurface,
overrides: struct {
command: ?configpkg.Command = null,
+ shell_integration: ?configpkg.Config.ShellIntegration = null,
working_directory: ?[:0]const u8 = null,
title: ?[:0]const u8 = null,
@@ -2308,6 +2756,7 @@ const Action = struct {
// Create a new tab with window context (first tab in new window)
win.newTabForWindow(parent, .{
.command = overrides.command,
+ .shell_integration = overrides.shell_integration,
.working_directory = overrides.working_directory,
.title = overrides.title,
});
@@ -2328,18 +2777,55 @@ const Action = struct {
gtk.Window.present(win.as(gtk.Window));
}
- pub fn openConfig(self: *Application) bool {
- // Get the config file path
+ pub fn openConfig(self: *Application, value: apprt.action.OpenConfig) bool {
const alloc = self.allocator();
+
+ // Get the config file path
const path = configpkg.edit.openPath(alloc) catch |err| {
- log.warn("error getting config file path: {}", .{err});
+ log.warn("error getting config file path: {t}", .{err});
return false;
};
defer alloc.free(path);
- // Open it using openURL. "path" isn't actually a URL but
- // at the time of writing that works just fine for GTK.
- openUrl(self, .{ .kind = .text, .url = path });
+ switch (value) {
+ .os_open => {
+ // Open it using openUrl. "path" isn't actually a URL but
+ // at the time of writing that works just fine for GTK.
+ openUrl(self, .{ .kind = .text, .url = path });
+ },
+
+ .new_window => {
+ const cmd = internal_os.getConfigEditCommand(alloc, path, .default) catch |err| {
+ log.warn("unable to get command to edit the config: {t}", .{err});
+ return false;
+ };
+ defer alloc.free(cmd);
+
+ const command: configpkg.Command = .{
+ .direct = &.{ "/bin/sh", "-c", cmd },
+ };
+
+ const title = std.fmt.allocPrintSentinel(
+ alloc,
+ "{s} {s}",
+ .{ i18n._("Editing configuration file"), path },
+ 0,
+ ) catch |err| t: {
+ log.warn("unable to format title: {t}", .{err});
+ break :t null;
+ };
+ defer if (title) |t| alloc.free(t);
+
+ Action.newWindow(self, null, .{
+ .command = command,
+ .title = title,
+ }) catch |err| {
+ log.warn("unable to create new window: {t}", .{err});
+ return false;
+ };
+ },
+ }
+
return true;
}
@@ -2434,6 +2920,23 @@ const Action = struct {
},
}
},
+ .window => {
+ switch (target) {
+ .app => return false,
+ .surface => |v| {
+ const surface = v.rt_surface.surface;
+ const win = ext.getAncestor(
+ Window,
+ surface.as(gtk.Widget),
+ ) orelse {
+ log.warn("surface is not in a window, ignoring prompt_window_title", .{});
+ return false;
+ };
+ win.promptWindowTitle();
+ return true;
+ },
+ }
+ },
}
}
@@ -2605,6 +3108,30 @@ const Action = struct {
}
}
+ pub fn setWindowTitle(
+ target: apprt.Target,
+ value: apprt.action.SetTitle,
+ ) bool {
+ switch (target) {
+ .app => {
+ log.warn("set_tab_title to app is unexpected", .{});
+ return false;
+ },
+ .surface => |core| {
+ const surface = core.rt_surface.surface;
+ const window = ext.getAncestor(
+ Window,
+ surface.as(gtk.Widget),
+ ) orelse {
+ log.warn("surface is not in a window, ignoring set_window_title", .{});
+ return false;
+ };
+ window.setTitleOverride(if (value.title.len == 0) null else value.title);
+ return true;
+ },
+ }
+ }
+
pub fn showChildExited(
target: apprt.Target,
value: apprt.surface.Message.ChildExited,
@@ -2846,7 +3373,7 @@ const Action = struct {
/// given the runtime environment or configuration.
///
/// This must be called BEFORE GTK initialization.
-fn setGtkEnv(config: *const CoreConfig) error{NoSpaceLeft}!void {
+fn setGtkEnv(config: *const CoreConfig) std.Io.Writer.Error!void {
assert(gtk.isInitialized() == 0);
var gdk_debug: struct {
@@ -2863,7 +3390,7 @@ fn setGtkEnv(config: *const CoreConfig) error{NoSpaceLeft}!void {
} = .{
// `gtk-opengl-debug` dumps logs directly to stderr so both must be true
// to enable OpenGL debugging.
- .opengl = state.logging.stderr and config.@"gtk-opengl-debug",
+ .opengl = global.logging().stderr and config.@"gtk-opengl-debug",
};
var gdk_disable: struct {
@@ -2915,8 +3442,7 @@ fn setGtkEnv(config: *const CoreConfig) error{NoSpaceLeft}!void {
{
var buf: [1024]u8 = undefined;
- var fmt = std.io.fixedBufferStream(&buf);
- const writer = fmt.writer();
+ var writer: std.Io.Writer = .fixed(&buf);
var first: bool = true;
inline for (@typeInfo(@TypeOf(gdk_debug)).@"struct".fields) |field| {
if (@field(gdk_debug, field.name)) {
@@ -2926,15 +3452,14 @@ fn setGtkEnv(config: *const CoreConfig) error{NoSpaceLeft}!void {
}
}
try writer.writeByte(0);
- const value = fmt.getWritten();
+ const value = writer.buffered();
log.warn("setting GDK_DEBUG={s}", .{value[0 .. value.len - 1]});
- _ = internal_os.setenv("GDK_DEBUG", value[0 .. value.len - 1 :0]);
+ _ = setenv("GDK_DEBUG", @ptrCast(value[0 .. value.len - 1 :0]), 1);
}
{
var buf: [1024]u8 = undefined;
- var fmt = std.io.fixedBufferStream(&buf);
- const writer = fmt.writer();
+ var writer: std.Io.Writer = .fixed(&buf);
var first: bool = true;
inline for (@typeInfo(@TypeOf(gdk_disable)).@"struct".fields) |field| {
if (@field(gdk_disable, field.name)) {
@@ -2944,10 +3469,13 @@ fn setGtkEnv(config: *const CoreConfig) error{NoSpaceLeft}!void {
}
}
try writer.writeByte(0);
- const value = fmt.getWritten();
+ const value = writer.buffered();
log.warn("setting GDK_DISABLE={s}", .{value[0 .. value.len - 1]});
- _ = internal_os.setenv("GDK_DISABLE", value[0 .. value.len - 1 :0]);
+ _ = setenv("GDK_DISABLE", @ptrCast(value[0 .. value.len - 1 :0]), 1);
}
+
+ // Sync environ after altering system env
+ global.syncEnviron();
}
fn findActiveWindow(data: ?*const anyopaque, _: ?*const anyopaque) callconv(.c) c_int {