diff --git a/include/ghostty.h b/include/ghostty.h
index 3189781f7..d64a01f48 100644
--- a/include/ghostty.h
+++ b/include/ghostty.h
@@ -571,7 +571,7 @@ typedef enum {
GHOSTTY_ACTION_TOGGLE_WINDOW_DECORATIONS,
GHOSTTY_ACTION_TOGGLE_QUICK_TERMINAL,
GHOSTTY_ACTION_TOGGLE_VISIBILITY,
- GHOSTTY_ACTION_TOGGLE_TOP_MENU,
+ GHOSTTY_ACTION_TOGGLE_MENUBAR,
GHOSTTY_ACTION_MOVE_TAB,
GHOSTTY_ACTION_GOTO_TAB,
GHOSTTY_ACTION_GOTO_SPLIT,
diff --git a/src/Surface.zig b/src/Surface.zig
index ddcfb3035..543e5ca8f 100644
--- a/src/Surface.zig
+++ b/src/Surface.zig
@@ -4213,9 +4213,9 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool
.toggle,
),
- .toggle_top_menu => try self.rt_app.performAction(
+ .toggle_menubar => try self.rt_app.performAction(
.{ .surface = self },
- .toggle_top_menu,
+ .toggle_menubar,
{},
),
diff --git a/src/apprt/action.zig b/src/apprt/action.zig
index 0414ebe78..98ad6f2c6 100644
--- a/src/apprt/action.zig
+++ b/src/apprt/action.zig
@@ -110,8 +110,8 @@ pub const Action = union(Key) {
/// Toggle the visibility of all Ghostty terminal windows.
toggle_visibility,
- /// Toggle whether the top menu is shown.
- toggle_top_menu,
+ /// Toggle whether the window menubar is shown.
+ toggle_menubar,
/// Moves a tab by a relative offset.
///
@@ -243,7 +243,7 @@ pub const Action = union(Key) {
toggle_window_decorations,
toggle_quick_terminal,
toggle_visibility,
- toggle_top_menu,
+ toggle_menubar,
move_tab,
goto_tab,
goto_split,
diff --git a/src/apprt/glfw.zig b/src/apprt/glfw.zig
index 5ef8e73df..e3d527f72 100644
--- a/src/apprt/glfw.zig
+++ b/src/apprt/glfw.zig
@@ -223,7 +223,7 @@ pub const App = struct {
.toggle_window_decorations,
.toggle_quick_terminal,
.toggle_visibility,
- .toggle_top_menu,
+ .toggle_menubar,
.goto_tab,
.move_tab,
.inspector,
diff --git a/src/apprt/gtk/App.zig b/src/apprt/gtk/App.zig
index 5712ad1ac..a3d24d8f7 100644
--- a/src/apprt/gtk/App.zig
+++ b/src/apprt/gtk/App.zig
@@ -506,7 +506,7 @@ pub fn performAction(
}),
.toggle_maximize => self.toggleMaximize(target),
.toggle_fullscreen => self.toggleFullscreen(target, value),
- .toggle_top_menu => self.toggleTopMenu(target),
+ .toggle_menubar => self.toggleMenubar(target),
.new_tab => try self.newTab(target),
.close_tab => try self.closeTab(target),
@@ -789,18 +789,18 @@ fn toggleWindowDecorations(
}
}
-fn toggleTopMenu(_: *App, target: apprt.Target) void {
+fn toggleMenubar(_: *App, target: apprt.Target) void {
switch (target) {
.app => {},
.surface => |v| {
const window = v.rt_surface.container.window() orelse {
log.info(
- "toggleTopMenu invalid for container={s}",
+ "toggleMenubar invalid for container={s}",
.{@tagName(v.rt_surface.container)},
);
return;
};
- window.toggleTopMenu();
+ window.toggleMenubar();
},
}
}
diff --git a/src/apprt/gtk/Surface.zig b/src/apprt/gtk/Surface.zig
index 7866877e3..bff1529d5 100644
--- a/src/apprt/gtk/Surface.zig
+++ b/src/apprt/gtk/Surface.zig
@@ -380,7 +380,7 @@ im_len: u7 = 0,
cgroup_path: ?[]const u8 = null,
/// Our context menu.
-context_menu: Menu(Surface, .context, .popover_menu),
+context_menu: Menu(Surface, "context_menu", .popover_menu),
/// Configuration used for initializing the surface. We have to copy some
/// data since initialization is delayed with GTK (on realize).
diff --git a/src/apprt/gtk/Window.zig b/src/apprt/gtk/Window.zig
index a2a31c385..bfccc7072 100644
--- a/src/apprt/gtk/Window.zig
+++ b/src/apprt/gtk/Window.zig
@@ -48,14 +48,14 @@ tab_overview: ?*c.GtkWidget,
/// can be either c.GtkNotebook or c.AdwTabView.
notebook: Notebook,
-/// The "top" menu that appears at the top of a window.
-top_menu: Menu(Window, .top, .popover_menu_bar),
+/// Menu that appears at the top of a window inbetween the titlebar and tabbar.
+menubar: Menu(Window, "menubar", .popover_menu_bar),
/// Revealer for showing/hiding top menu.
-top_menu_revealer: *c.GtkRevealer,
+menubar_revealer: *c.GtkRevealer,
/// The "main" menu that is attached to a button in the headerbar.
-titlebar_menu: Menu(Window, .titlebar, .popover_menu),
+titlebar_menu: Menu(Window, "titlebar_menu", .popover_menu),
/// The libadwaita widget for receiving toast send requests. If libadwaita is
/// not used, this is null and unused.
@@ -104,8 +104,8 @@ pub fn init(self: *Window, app: *App) !void {
.tab_overview = null,
.toast_overlay = null,
.notebook = undefined,
- .top_menu = undefined,
- .top_menu_revealer = undefined,
+ .menubar = undefined,
+ .menubar_revealer = undefined,
.titlebar_menu = undefined,
.winproto = .none,
};
@@ -149,12 +149,12 @@ pub fn init(self: *Window, app: *App) !void {
const box = c.gtk_box_new(c.GTK_ORIENTATION_VERTICAL, 0);
// Set up the menus
- self.top_menu.init();
+ self.menubar.init();
self.titlebar_menu.init();
- self.top_menu_revealer = @ptrCast(@alignCast(c.gtk_revealer_new()));
- c.gtk_revealer_set_child(self.top_menu_revealer, self.top_menu.asWidget());
- c.gtk_revealer_set_transition_type(self.top_menu_revealer, c.GTK_REVEALER_TRANSITION_TYPE_SLIDE_DOWN);
+ self.menubar_revealer = @ptrCast(@alignCast(c.gtk_revealer_new()));
+ c.gtk_revealer_set_child(self.menubar_revealer, self.menubar.asWidget());
+ c.gtk_revealer_set_transition_type(self.menubar_revealer, c.GTK_REVEALER_TRANSITION_TYPE_SLIDE_DOWN);
// Setup our notebook
self.notebook.init();
@@ -200,7 +200,7 @@ pub fn init(self: *Window, app: *App) !void {
_ = c.g_signal_connect_data(
btn,
"notify::active",
- c.G_CALLBACK(>kMenuActivate),
+ c.G_CALLBACK(>kTitlebarMenuActivate),
self,
null,
c.G_CONNECT_DEFAULT,
@@ -258,11 +258,11 @@ pub fn init(self: *Window, app: *App) !void {
.adw140 => {},
.adw, .adw130 => {
c.gtk_box_append(@ptrCast(box), self.headerbar.asWidget());
- c.gtk_box_append(@ptrCast(box), @ptrCast(@alignCast(self.top_menu_revealer)));
+ c.gtk_box_append(@ptrCast(box), @ptrCast(@alignCast(self.menubar_revealer)));
},
.gtk => {
c.gtk_window_set_titlebar(gtk_window, self.headerbar.asWidget());
- c.gtk_box_append(@ptrCast(box), @ptrCast(@alignCast(self.top_menu_revealer)));
+ c.gtk_box_append(@ptrCast(box), @ptrCast(@alignCast(self.menubar_revealer)));
},
}
@@ -344,7 +344,7 @@ pub fn init(self: *Window, app: *App) !void {
const top_box = c.gtk_box_new(c.GTK_ORIENTATION_VERTICAL, 0);
c.gtk_box_append(@ptrCast(top_box), self.headerbar.asWidget());
- c.gtk_box_append(@ptrCast(top_box), @ptrCast(@alignCast(self.top_menu_revealer)));
+ c.gtk_box_append(@ptrCast(top_box), @ptrCast(@alignCast(self.menubar_revealer)));
c.adw_toolbar_view_add_top_bar(toolbar_view, top_box);
@@ -644,9 +644,9 @@ pub fn toggleWindowDecorations(self: *Window) void {
}
/// Toggle top menu.
-pub fn toggleTopMenu(self: *Window) void {
- const is_revealed = c.gtk_revealer_get_reveal_child(self.top_menu_revealer) != 0;
- c.gtk_revealer_set_reveal_child(self.top_menu_revealer, @intFromBool(!is_revealed));
+pub fn toggleMenubar(self: *Window) void {
+ const is_revealed = c.gtk_revealer_get_reveal_child(self.menubar_revealer) != 0;
+ c.gtk_revealer_set_reveal_child(self.menubar_revealer, @intFromBool(!is_revealed));
}
/// Grabs focus on the currently selected tab.
@@ -1149,7 +1149,7 @@ fn userdataSelf(ud: *anyopaque) *Window {
return @ptrCast(@alignCast(ud));
}
-fn gtkMenuActivate(
+fn gtkTitlebarMenuActivate(
btn: *c.GtkMenuButton,
_: *c.GParamSpec,
ud: ?*anyopaque,
diff --git a/src/apprt/gtk/builder_check.zig b/src/apprt/gtk/builder_check.zig
new file mode 100644
index 000000000..7952bf709
--- /dev/null
+++ b/src/apprt/gtk/builder_check.zig
@@ -0,0 +1,24 @@
+const std = @import("std");
+
+pub const c = @cImport({
+ @cInclude("gtk/gtk.h");
+});
+
+pub fn main() !void {
+ var gpa = std.heap.GeneralPurposeAllocator(.{}){};
+ const alloc = gpa.allocator();
+
+ const filename = filename: {
+ var it = try std.process.argsWithAllocator(alloc);
+ defer it.deinit();
+
+ _ = it.next() orelse return error.NoFilename;
+ break :filename try alloc.dupeZ(u8, it.next() orelse return error.NoFilename);
+ };
+ defer alloc.free(filename);
+
+ c.gtk_init();
+
+ const builder = c.gtk_builder_new_from_file(filename.ptr);
+ defer c.g_object_unref(builder);
+}
diff --git a/src/apprt/gtk/gresource.zig b/src/apprt/gtk/gresource.zig
index 0b46a4e4f..7ab6db61b 100644
--- a/src/apprt/gtk/gresource.zig
+++ b/src/apprt/gtk/gresource.zig
@@ -53,6 +53,12 @@ const icons = [_]struct {
},
};
+pub const ui_files = [_][]const u8{
+ "menu-surface-context_menu",
+ "menu-window-menubar",
+ "menu-window-titlebar_menu",
+};
+
pub const gresource_xml = comptimeGenerateGResourceXML();
fn comptimeGenerateGResourceXML() []const u8 {
@@ -93,6 +99,17 @@ fn writeGResourceXML(writer: anytype) !void {
.{ icon.alias, icon.source },
);
}
+ try writer.writeAll(
+ \\
+ \\
+ \\
+ );
+ for (ui_files) |ui_file| {
+ try writer.print(
+ " src/apprt/gtk/ui/{0s}.ui\n",
+ .{ui_file},
+ );
+ }
try writer.writeAll(
\\
\\
@@ -101,7 +118,7 @@ fn writeGResourceXML(writer: anytype) !void {
}
pub const dependencies = deps: {
- const total = css_files.len + icons.len;
+ const total = css_files.len + icons.len + ui_files.len;
var deps: [total][]const u8 = undefined;
var index: usize = 0;
for (css_files) |css_file| {
@@ -112,5 +129,9 @@ pub const dependencies = deps: {
deps[index] = std.fmt.comptimePrint("images/icons/icon_{s}.png", .{icon.source});
index += 1;
}
+ for (ui_files) |ui_file| {
+ deps[index] = std.fmt.comptimePrint("src/apprt/gtk/ui/{s}.ui", .{ui_file});
+ index += 1;
+ }
break :deps deps;
};
diff --git a/src/apprt/gtk/menu.zig b/src/apprt/gtk/menu.zig
index 00ef92a13..880e3dd3a 100644
--- a/src/apprt/gtk/menu.zig
+++ b/src/apprt/gtk/menu.zig
@@ -10,7 +10,7 @@ const log = std.log.scoped(.gtk_menu);
pub fn Menu(
comptime T: type,
- comptime variant: enum { top, titlebar, context },
+ comptime variant: []const u8,
comptime style: enum { popover_menu, popover_menu_bar },
) type {
return struct {
@@ -29,12 +29,27 @@ pub fn Menu(
Surface => "surface",
else => unreachable,
};
- const parent: *T = @alignCast(@fieldParentPtr(@tagName(variant) ++ "_menu", self));
+ const parent: *T = @alignCast(@fieldParentPtr(variant, self));
- // embed the menu data using Zig @embedFile rather than as a GTK resource so that we get
- // compile-time errors if we try and embed a file that doesn't exist
- const data = @embedFile("ui/menu-" ++ name ++ "-" ++ @tagName(variant) ++ ".ui");
- const builder = c.gtk_builder_new_from_string(data.ptr, @intCast(data.len));
+ const path = "ui/menu-" ++ name ++ "-" ++ variant ++ ".ui";
+
+ comptime {
+ // Use @embedFile to make sure that the file exists at compile
+ // time. Zig _should_ discard the data so that it doesn't end up
+ // in the final executable. At runtime we will load the data from
+ // a GResource.
+ _ = @embedFile(path);
+
+ // Check to make sure that our file is listed as a `ui_file` in
+ // `gresource.zig`. If it isn't Ghostty could crash at runtime
+ // when we try and load a nonexistent GResource.
+ const gresource = @import("gresource.zig");
+ for (gresource.ui_files) |ui_file| {
+ if (std.mem.eql(u8, ui_file, "menu-" ++ name ++ "-" ++ variant)) break;
+ } else @compileError("missing 'menu-" ++ name ++ "-" ++ variant ++ "' in gresource.zig");
+ }
+
+ const builder = c.gtk_builder_new_from_resource("/com/mitchellh/ghostty/" ++ path);
defer c.g_object_unref(@ptrCast(builder));
const menu_model: *c.GMenuModel = @ptrCast(@alignCast(c.gtk_builder_get_object(builder, "menu")));
diff --git a/src/apprt/gtk/ui/menu-window-titlebar.ui b/src/apprt/gtk/ui/menu-surface-context_menu.ui
similarity index 52%
rename from src/apprt/gtk/ui/menu-window-titlebar.ui
rename to src/apprt/gtk/ui/menu-surface-context_menu.ui
index 9345e0aea..37d07bec2 100644
--- a/src/apprt/gtk/ui/menu-window-titlebar.ui
+++ b/src/apprt/gtk/ui/menu-surface-context_menu.ui
@@ -14,22 +14,12 @@
-
- New Window
- win.new-window
+ Clear
+ win.clear
-
- Close Window
- win.close
-
-
-
- -
- New Tab
- win.new-tab
-
- -
- Close Tab
- win.close-tab
+ Reset
+ win.reset
+
+ Tab
+
+ -
+ New Tab
+ win.new-tab
+
+ -
+ Close Tab
+ win.close-tab
+
+
+
+
+ Window
+
+ -
+ New Window
+ win.new-window
+
+ -
+ Close Window
+ win.close
+
+
+
- -
- Clear
- win.clear
-
- -
- Reset
- win.reset
-
-
-
- -
- Terminal Inspector
- win.toggle-inspector
-
- -
- Open Configuration
- app.open-config
-
- -
- Reload Configuration
- app.reload-config
-
-
-
- -
- About Ghostty
- win.about
-
- -
- Quit
- app.quit
-
+
+ Config
+
+ -
+ Open Configuration
+ app.open-config
+
+ -
+ Reload Configuration
+ app.reload-config
+
+
+
diff --git a/src/apprt/gtk/ui/menu-window-top.ui b/src/apprt/gtk/ui/menu-window-menubar.ui
similarity index 100%
rename from src/apprt/gtk/ui/menu-window-top.ui
rename to src/apprt/gtk/ui/menu-window-menubar.ui
diff --git a/src/apprt/gtk/ui/menu-surface-context.ui b/src/apprt/gtk/ui/menu-window-titlebar_menu.ui
similarity index 100%
rename from src/apprt/gtk/ui/menu-surface-context.ui
rename to src/apprt/gtk/ui/menu-window-titlebar_menu.ui
diff --git a/src/build/SharedDeps.zig b/src/build/SharedDeps.zig
index 64068658d..6fe11147f 100644
--- a/src/build/SharedDeps.zig
+++ b/src/build/SharedDeps.zig
@@ -469,6 +469,24 @@ pub fn add(
const wf = b.addWriteFiles();
const gresource_xml = wf.add("gresource.xml", gresource.gresource_xml);
+ {
+ const builder_check = b.addExecutable(.{
+ .name = "builder_check",
+ .root_source_file = b.path("src/apprt/gtk/builder_check.zig"),
+ .target = b.host,
+ });
+ builder_check.linkSystemLibrary2("gtk4", dynamic_link_opts);
+ builder_check.linkLibC();
+
+ for (gresource.dependencies) |pathname| {
+ const extension = std.fs.path.extension(pathname);
+ if (!std.mem.eql(u8, extension, ".ui")) continue;
+ const check = b.addRunArtifact(builder_check);
+ check.addFileArg(b.path(pathname));
+ step.step.dependOn(&check.step);
+ }
+ }
+
const generate_resources_c = b.addSystemCommand(&.{
"glib-compile-resources",
"--c-name",
diff --git a/src/input/Binding.zig b/src/input/Binding.zig
index f35e70be0..84c26fc4c 100644
--- a/src/input/Binding.zig
+++ b/src/input/Binding.zig
@@ -473,9 +473,9 @@ pub const Action = union(enum) {
/// This currently only works on macOS.
toggle_visibility: void,
- /// Show/hide the application menu that appears below the titlebar and above
- /// the tab bar.
- toggle_top_menu: void,
+ /// Show/hide the window menu that appears below the titlebar and above the
+ /// tab bar.
+ toggle_menubar: void,
/// Quit ghostty.
quit: void,
@@ -784,7 +784,7 @@ pub const Action = union(enum) {
.goto_tab,
.move_tab,
.toggle_tab_overview,
- .toggle_top_menu,
+ .toggle_menubar,
.new_split,
.goto_split,
.toggle_split_zoom,