Merge pull request #486 from mitchellh/gtk-app-window

gtk: Menu, Header Buttons, About Window
This commit is contained in:
Mitchell Hashimoto
2023-09-18 15:56:05 -07:00
committed by GitHub
11 changed files with 462 additions and 162 deletions

View File

@ -87,7 +87,7 @@ jobs:
nix_path: nixpkgs=channel:nixos-unstable nix_path: nixpkgs=channel:nixos-unstable
- name: test - name: test
run: nix develop -c zig build test run: nix develop -c zig build -Dapp-runtime=none test
- name: Test GTK Build - name: Test GTK Build
run: nix develop -c zig build -Dapp-runtime=gtk run: nix develop -c zig build -Dapp-runtime=gtk

View File

@ -206,7 +206,11 @@ pub fn build(b: *std.Build) !void {
const exe_options = b.addOptions(); const exe_options = b.addOptions();
exe_options.addOption(std.SemanticVersion, "app_version", version); exe_options.addOption(std.SemanticVersion, "app_version", version);
exe_options.addOption([]const u8, "app_version_string", b.fmt("{}", .{version})); exe_options.addOption([:0]const u8, "app_version_string", try std.fmt.allocPrintZ(
b.allocator,
"{}",
.{version},
));
exe_options.addOption(bool, "tracy_enabled", tracy); exe_options.addOption(bool, "tracy_enabled", tracy);
exe_options.addOption(bool, "flatpak", flatpak); exe_options.addOption(bool, "flatpak", flatpak);
exe_options.addOption(apprt.Runtime, "app_runtime", app_runtime); exe_options.addOption(apprt.Runtime, "app_runtime", app_runtime);

View File

@ -1,8 +1,6 @@
{ mkShell, lib, stdenv { mkShell, lib, stdenv
, bashInteractive , bashInteractive
, debugedit
, flatpak-builder
, gdb , gdb
, glxinfo , glxinfo
, ncurses , ncurses
@ -92,10 +90,6 @@ in mkShell rec {
# by default so we have to include this. # by default so we have to include this.
bashInteractive bashInteractive
# Flatpak builds
debugedit
flatpak-builder
gdb gdb
valgrind valgrind
wraptest wraptest

View File

@ -25,7 +25,7 @@ pub const surface = @import("apprt/surface.zig");
/// Window or something. /// Window or something.
pub const runtime = switch (build_config.artifact) { pub const runtime = switch (build_config.artifact) {
.exe => switch (build_config.app_runtime) { .exe => switch (build_config.app_runtime) {
.none => @compileError("exe with no runtime not allowed"), .none => struct {},
.glfw => glfw, .glfw => glfw,
.gtk => gtk, .gtk => gtk,
}, },
@ -64,5 +64,6 @@ pub const Runtime = enum {
}; };
test { test {
@import("std").testing.refAllDecls(@This()); _ = Runtime;
_ = runtime;
} }

View File

@ -5,4 +5,6 @@ pub const Surface = @import("gtk/Surface.zig");
test { test {
@import("std").testing.refAllDecls(@This()); @import("std").testing.refAllDecls(@This());
_ = @import("gtk/key.zig");
} }

View File

@ -15,6 +15,7 @@ const assert = std.debug.assert;
const builtin = @import("builtin"); const builtin = @import("builtin");
const glfw = @import("glfw"); const glfw = @import("glfw");
const configpkg = @import("../../config.zig"); const configpkg = @import("../../config.zig");
const input = @import("../../input.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");
@ -23,6 +24,7 @@ const Surface = @import("Surface.zig");
const Window = @import("Window.zig"); const Window = @import("Window.zig");
const ConfigErrorsWindow = @import("ConfigErrorsWindow.zig"); const ConfigErrorsWindow = @import("ConfigErrorsWindow.zig");
const c = @import("c.zig"); const c = @import("c.zig");
const key = @import("key.zig");
const log = std.log.scoped(.gtk); const log = std.log.scoped(.gtk);
@ -37,6 +39,9 @@ ctx: *c.GMainContext,
/// The "none" cursor. We use one that is shared across the entire app. /// The "none" cursor. We use one that is shared across the entire app.
cursor_none: ?*c.GdkCursor, cursor_none: ?*c.GdkCursor,
/// The shared application menu.
menu: ?*c.GMenu = null,
/// The configuration errors window, if it is currently open. /// The configuration errors window, if it is currently open.
config_errors_window: ?*ConfigErrorsWindow = null, config_errors_window: ?*ConfigErrorsWindow = null,
@ -140,6 +145,7 @@ pub fn terminate(self: *App) void {
c.g_object_unref(self.app); c.g_object_unref(self.app);
if (self.cursor_none) |cursor| c.g_object_unref(cursor); if (self.cursor_none) |cursor| c.g_object_unref(cursor);
if (self.menu) |menu| c.g_object_unref(menu);
self.config.deinit(); self.config.deinit();
@ -159,15 +165,19 @@ pub fn reloadConfig(self: *App) !?*const Config {
// Update the existing config, be sure to clean up the old one. // Update the existing config, be sure to clean up the old one.
self.config.deinit(); self.config.deinit();
self.config = config; self.config = config;
self.syncConfigChanges() catch |err| {
// If there were errors, report them log.warn("error handling configuration changes err={}", .{err});
self.updateConfigErrors() catch |err| {
log.warn("error handling configuration errors err={}", .{err});
}; };
return &self.config; return &self.config;
} }
/// Call this anytime the configuration changes.
fn syncConfigChanges(self: *App) !void {
try self.updateConfigErrors();
try self.syncActionAccelerators();
}
/// This should be called whenever the configuration changes to update /// This should be called whenever the configuration changes to update
/// the state of our config errors window. This will show the window if /// the state of our config errors window. This will show the window if
/// there are new configuration errors and hide the window if the errors /// there are new configuration errors and hide the window if the errors
@ -185,6 +195,35 @@ fn updateConfigErrors(self: *App) !void {
} }
} }
fn syncActionAccelerators(self: *App) !void {
try self.syncActionAccelerator("app.quit", .{ .quit = {} });
try self.syncActionAccelerator("app.reload_config", .{ .reload_config = {} });
try self.syncActionAccelerator("win.close", .{ .close_surface = {} });
try self.syncActionAccelerator("win.new_window", .{ .new_window = {} });
try self.syncActionAccelerator("win.new_tab", .{ .new_tab = {} });
}
fn syncActionAccelerator(
self: *App,
gtk_action: [:0]const u8,
action: input.Binding.Action,
) !void {
// Reset it initially
const zero = [_]?[*:0]const u8{null};
c.gtk_application_set_accels_for_action(@ptrCast(self.app), gtk_action.ptr, &zero);
const trigger = self.config.keybind.set.getTrigger(action) orelse return;
var buf: [256]u8 = undefined;
const accel = try key.accelFromTrigger(&buf, trigger) orelse return;
const accels = [_]?[*:0]const u8{ accel, null };
c.gtk_application_set_accels_for_action(
@ptrCast(self.app),
gtk_action.ptr,
&accels,
);
}
/// Called by CoreApp to wake up the event loop. /// Called by CoreApp to wake up the event loop.
pub fn wakeup(self: App) void { pub fn wakeup(self: App) void {
_ = self; _ = self;
@ -195,10 +234,15 @@ pub fn wakeup(self: App) void {
pub fn run(self: *App) !void { pub fn run(self: *App) !void {
if (!self.running) return; if (!self.running) return;
// If we're not remote, then we also setup our actions and menus.
self.initActions();
self.initMenu();
// On startup, we want to check for configuration errors right away // On startup, we want to check for configuration errors right away
// so we can show our error window. // so we can show our error window. We also need to setup other initial
self.updateConfigErrors() catch |err| { // state.
log.warn("error handling configuration errors err={}", .{err}); self.syncConfigChanges() catch |err| {
log.warn("error handling configuration changes err={}", .{err});
}; };
while (self.running) { while (self.running) {
@ -331,3 +375,81 @@ fn gtkActivate(app: *c.GtkApplication, ud: ?*anyopaque) callconv(.C) void {
.new_window = .{}, .new_window = .{},
}, .{ .forever = {} }); }, .{ .forever = {} });
} }
fn gtkActionReloadConfig(
_: *c.GSimpleAction,
_: *c.GVariant,
ud: ?*anyopaque,
) callconv(.C) void {
const self: *App = @ptrCast(@alignCast(ud orelse return));
_ = self.core_app.mailbox.push(.{
.reload_config = {},
}, .{ .forever = {} });
}
fn gtkActionQuit(
_: *c.GSimpleAction,
_: *c.GVariant,
ud: ?*anyopaque,
) callconv(.C) void {
const self: *App = @ptrCast(@alignCast(ud orelse return));
self.core_app.setQuit() catch |err| {
log.warn("error setting quit err={}", .{err});
return;
};
}
/// This is called to setup the action map that this application supports.
/// This should be called only once on startup.
fn initActions(self: *App) void {
const actions = .{
.{ "quit", &gtkActionQuit },
.{ "reload_config", &gtkActionReloadConfig },
};
inline for (actions) |entry| {
const action = c.g_simple_action_new(entry[0], null);
defer c.g_object_unref(action);
_ = c.g_signal_connect_data(
action,
"activate",
c.G_CALLBACK(entry[1]),
self,
null,
c.G_CONNECT_DEFAULT,
);
c.g_action_map_add_action(@ptrCast(self.app), @ptrCast(action));
}
}
/// This sets the self.menu property to the application menu that can be
/// shared by all application windows.
fn initMenu(self: *App) void {
const menu = c.g_menu_new();
errdefer c.g_object_unref(menu);
{
const section = c.g_menu_new();
defer c.g_object_unref(section);
c.g_menu_append_section(menu, null, @ptrCast(@alignCast(section)));
c.g_menu_append(section, "New Window", "win.new_window");
c.g_menu_append(section, "New Tab", "win.new_tab");
c.g_menu_append(section, "Close Window", "win.close");
}
{
const section = c.g_menu_new();
defer c.g_object_unref(section);
c.g_menu_append_section(menu, null, @ptrCast(@alignCast(section)));
c.g_menu_append(section, "Reload Configuration", "app.reload_config");
c.g_menu_append(section, "About Ghostty", "win.about");
}
// {
// const section = c.g_menu_new();
// defer c.g_object_unref(section);
// c.g_menu_append_submenu(menu, "File", @ptrCast(@alignCast(section)));
// }
self.menu = menu;
}

View File

@ -47,7 +47,7 @@ fn destroy(self: *ConfigErrors) void {
fn init(self: *ConfigErrors, app: *App) !void { fn init(self: *ConfigErrors, app: *App) !void {
// Create the window // Create the window
const window = c.gtk_application_window_new(app.app); const window = c.gtk_window_new();
const gtk_window: *c.GtkWindow = @ptrCast(window); const gtk_window: *c.GtkWindow = @ptrCast(window);
errdefer c.gtk_window_destroy(gtk_window); errdefer c.gtk_window_destroy(gtk_window);
c.gtk_window_set_title(gtk_window, "Configuration Errors"); c.gtk_window_set_title(gtk_window, "Configuration Errors");

View File

@ -1008,133 +1008,3 @@ fn translateMods(state: c.GdkModifierType) input.Mods {
if (state & c.GDK_LOCK_MASK != 0) mods.caps_lock = true; if (state & c.GDK_LOCK_MASK != 0) mods.caps_lock = true;
return mods; return mods;
} }
fn translateKey(keyval: c.guint) input.Key {
return switch (keyval) {
c.GDK_KEY_a => .a,
c.GDK_KEY_b => .b,
c.GDK_KEY_c => .c,
c.GDK_KEY_d => .d,
c.GDK_KEY_e => .e,
c.GDK_KEY_f => .f,
c.GDK_KEY_g => .g,
c.GDK_KEY_h => .h,
c.GDK_KEY_i => .i,
c.GDK_KEY_j => .j,
c.GDK_KEY_k => .k,
c.GDK_KEY_l => .l,
c.GDK_KEY_m => .m,
c.GDK_KEY_n => .n,
c.GDK_KEY_o => .o,
c.GDK_KEY_p => .p,
c.GDK_KEY_q => .q,
c.GDK_KEY_r => .r,
c.GDK_KEY_s => .s,
c.GDK_KEY_t => .t,
c.GDK_KEY_u => .u,
c.GDK_KEY_v => .v,
c.GDK_KEY_w => .w,
c.GDK_KEY_x => .x,
c.GDK_KEY_y => .y,
c.GDK_KEY_z => .z,
c.GDK_KEY_0 => .zero,
c.GDK_KEY_1 => .one,
c.GDK_KEY_2 => .two,
c.GDK_KEY_3 => .three,
c.GDK_KEY_4 => .four,
c.GDK_KEY_5 => .five,
c.GDK_KEY_6 => .six,
c.GDK_KEY_7 => .seven,
c.GDK_KEY_8 => .eight,
c.GDK_KEY_9 => .nine,
c.GDK_KEY_semicolon => .semicolon,
c.GDK_KEY_space => .space,
c.GDK_KEY_apostrophe => .apostrophe,
c.GDK_KEY_comma => .comma,
c.GDK_KEY_grave => .grave_accent, // `
c.GDK_KEY_period => .period,
c.GDK_KEY_slash => .slash,
c.GDK_KEY_minus => .minus,
c.GDK_KEY_equal => .equal,
c.GDK_KEY_bracketleft => .left_bracket, // [
c.GDK_KEY_bracketright => .right_bracket, // ]
c.GDK_KEY_backslash => .backslash, // /
c.GDK_KEY_Up => .up,
c.GDK_KEY_Down => .down,
c.GDK_KEY_Right => .right,
c.GDK_KEY_Left => .left,
c.GDK_KEY_Home => .home,
c.GDK_KEY_End => .end,
c.GDK_KEY_Insert => .insert,
c.GDK_KEY_Delete => .delete,
c.GDK_KEY_Caps_Lock => .caps_lock,
c.GDK_KEY_Scroll_Lock => .scroll_lock,
c.GDK_KEY_Num_Lock => .num_lock,
c.GDK_KEY_Page_Up => .page_up,
c.GDK_KEY_Page_Down => .page_down,
c.GDK_KEY_Escape => .escape,
c.GDK_KEY_Return => .enter,
c.GDK_KEY_Tab => .tab,
c.GDK_KEY_BackSpace => .backspace,
c.GDK_KEY_Print => .print_screen,
c.GDK_KEY_Pause => .pause,
c.GDK_KEY_F1 => .f1,
c.GDK_KEY_F2 => .f2,
c.GDK_KEY_F3 => .f3,
c.GDK_KEY_F4 => .f4,
c.GDK_KEY_F5 => .f5,
c.GDK_KEY_F6 => .f6,
c.GDK_KEY_F7 => .f7,
c.GDK_KEY_F8 => .f8,
c.GDK_KEY_F9 => .f9,
c.GDK_KEY_F10 => .f10,
c.GDK_KEY_F11 => .f11,
c.GDK_KEY_F12 => .f12,
c.GDK_KEY_F13 => .f13,
c.GDK_KEY_F14 => .f14,
c.GDK_KEY_F15 => .f15,
c.GDK_KEY_F16 => .f16,
c.GDK_KEY_F17 => .f17,
c.GDK_KEY_F18 => .f18,
c.GDK_KEY_F19 => .f19,
c.GDK_KEY_F20 => .f20,
c.GDK_KEY_F21 => .f21,
c.GDK_KEY_F22 => .f22,
c.GDK_KEY_F23 => .f23,
c.GDK_KEY_F24 => .f24,
c.GDK_KEY_F25 => .f25,
c.GDK_KEY_KP_0 => .kp_0,
c.GDK_KEY_KP_1 => .kp_1,
c.GDK_KEY_KP_2 => .kp_2,
c.GDK_KEY_KP_3 => .kp_3,
c.GDK_KEY_KP_4 => .kp_4,
c.GDK_KEY_KP_5 => .kp_5,
c.GDK_KEY_KP_6 => .kp_6,
c.GDK_KEY_KP_7 => .kp_7,
c.GDK_KEY_KP_8 => .kp_8,
c.GDK_KEY_KP_9 => .kp_9,
c.GDK_KEY_KP_Decimal => .kp_decimal,
c.GDK_KEY_KP_Divide => .kp_divide,
c.GDK_KEY_KP_Multiply => .kp_multiply,
c.GDK_KEY_KP_Subtract => .kp_subtract,
c.GDK_KEY_KP_Add => .kp_add,
c.GDK_KEY_KP_Enter => .kp_enter,
c.GDK_KEY_KP_Equal => .kp_equal,
c.GDK_KEY_Shift_L => .left_shift,
c.GDK_KEY_Control_L => .left_control,
c.GDK_KEY_Alt_L => .left_alt,
c.GDK_KEY_Super_L => .left_super,
c.GDK_KEY_Shift_R => .right_shift,
c.GDK_KEY_Control_R => .right_control,
c.GDK_KEY_Alt_R => .right_alt,
c.GDK_KEY_Super_R => .right_super,
else => .invalid,
};
}

View File

@ -3,6 +3,7 @@ const Window = @This();
const std = @import("std"); const std = @import("std");
const builtin = @import("builtin"); const builtin = @import("builtin");
const build_config = @import("../../build_config.zig");
const Allocator = std.mem.Allocator; const Allocator = std.mem.Allocator;
const assert = std.debug.assert; const assert = std.debug.assert;
const configpkg = @import("../../config.zig"); const configpkg = @import("../../config.zig");
@ -89,6 +90,23 @@ pub fn init(self: *Window, app: *App) !void {
c.gtk_widget_set_opacity(@ptrCast(window), app.config.@"background-opacity"); c.gtk_widget_set_opacity(@ptrCast(window), app.config.@"background-opacity");
} }
// Use the new GTK4 header bar
const header = c.gtk_header_bar_new();
c.gtk_window_set_titlebar(gtk_window, header);
{
const btn = c.gtk_menu_button_new();
c.gtk_widget_set_tooltip_text(btn, "Main Menu");
c.gtk_menu_button_set_icon_name(@ptrCast(btn), "open-menu-symbolic");
c.gtk_menu_button_set_menu_model(@ptrCast(btn), @ptrCast(@alignCast(app.menu)));
c.gtk_header_bar_pack_end(@ptrCast(header), btn);
}
{
const btn = c.gtk_button_new_from_icon_name("tab-new-symbolic");
c.gtk_widget_set_tooltip_text(btn, "New Tab");
c.gtk_header_bar_pack_end(@ptrCast(header), btn);
_ = c.g_signal_connect_data(btn, "clicked", c.G_CALLBACK(&gtkActionNewTab), self, null, c.G_CONNECT_DEFAULT);
}
// Hide window decoration if configured. This has to happen before // Hide window decoration if configured. This has to happen before
// `gtk_widget_show`. // `gtk_widget_show`.
if (!app.config.@"window-decoration") { if (!app.config.@"window-decoration") {
@ -109,10 +127,6 @@ pub fn init(self: *Window, app: *App) !void {
c.gtk_widget_set_vexpand(notebook_widget, 1); c.gtk_widget_set_vexpand(notebook_widget, 1);
c.gtk_widget_set_hexpand(notebook_widget, 1); c.gtk_widget_set_hexpand(notebook_widget, 1);
// Create our add button for new tabs
const notebook_add_btn = c.gtk_button_new_from_icon_name("list-add-symbolic");
c.gtk_notebook_set_action_widget(notebook, notebook_add_btn, c.GTK_PACK_END);
// Create our box which will hold our widgets. // Create our box which will hold our widgets.
const box = c.gtk_box_new(c.GTK_ORIENTATION_VERTICAL, 0); const box = c.gtk_box_new(c.GTK_ORIENTATION_VERTICAL, 0);
@ -129,12 +143,14 @@ pub fn init(self: *Window, app: *App) !void {
// All of our events // All of our events
_ = c.g_signal_connect_data(window, "close-request", c.G_CALLBACK(&gtkCloseRequest), self, null, c.G_CONNECT_DEFAULT); _ = c.g_signal_connect_data(window, "close-request", c.G_CALLBACK(&gtkCloseRequest), self, null, c.G_CONNECT_DEFAULT);
_ = c.g_signal_connect_data(window, "destroy", c.G_CALLBACK(&gtkDestroy), self, null, c.G_CONNECT_DEFAULT); _ = c.g_signal_connect_data(window, "destroy", c.G_CALLBACK(&gtkDestroy), self, null, c.G_CONNECT_DEFAULT);
_ = c.g_signal_connect_data(notebook_add_btn, "clicked", c.G_CALLBACK(&gtkTabAddClick), self, null, c.G_CONNECT_DEFAULT);
_ = c.g_signal_connect_data(notebook, "page-added", c.G_CALLBACK(&gtkPageAdded), self, null, c.G_CONNECT_DEFAULT); _ = c.g_signal_connect_data(notebook, "page-added", c.G_CALLBACK(&gtkPageAdded), self, null, c.G_CONNECT_DEFAULT);
_ = c.g_signal_connect_data(notebook, "page-removed", c.G_CALLBACK(&gtkPageRemoved), self, null, c.G_CONNECT_DEFAULT); _ = c.g_signal_connect_data(notebook, "page-removed", c.G_CALLBACK(&gtkPageRemoved), self, null, c.G_CONNECT_DEFAULT);
_ = c.g_signal_connect_data(notebook, "switch-page", c.G_CALLBACK(&gtkSwitchPage), self, null, c.G_CONNECT_DEFAULT); _ = c.g_signal_connect_data(notebook, "switch-page", c.G_CALLBACK(&gtkSwitchPage), self, null, c.G_CONNECT_DEFAULT);
_ = c.g_signal_connect_data(notebook, "create-window", c.G_CALLBACK(&gtkNotebookCreateWindow), self, null, c.G_CONNECT_DEFAULT); _ = c.g_signal_connect_data(notebook, "create-window", c.G_CALLBACK(&gtkNotebookCreateWindow), self, null, c.G_CONNECT_DEFAULT);
// Our actions for the menu
initActions(self);
// The box is our main child // The box is our main child
c.gtk_window_set_child(gtk_window, box); c.gtk_window_set_child(gtk_window, box);
@ -142,6 +158,32 @@ pub fn init(self: *Window, app: *App) !void {
c.gtk_widget_show(window); c.gtk_widget_show(window);
} }
/// Sets up the GTK actions for the window scope. Actions are how GTK handles
/// menus and such. The menu is defined in App.zig but the action is defined
/// here. The string name binds them.
fn initActions(self: *Window) void {
const actions = .{
.{ "about", &gtkActionAbout },
.{ "close", &gtkActionClose },
.{ "new_window", &gtkActionNewWindow },
.{ "new_tab", &gtkActionNewTab },
};
inline for (actions) |entry| {
const action = c.g_simple_action_new(entry[0], null);
defer c.g_object_unref(action);
_ = c.g_signal_connect_data(
action,
"activate",
c.G_CALLBACK(entry[1]),
self,
null,
c.G_CONNECT_DEFAULT,
);
c.g_action_map_add_action(@ptrCast(self.window), @ptrCast(action));
}
}
pub fn deinit(self: *Window) void { pub fn deinit(self: *Window) void {
if (self.icon_search_dir) |ptr| self.app.core_app.alloc.free(ptr); if (self.icon_search_dir) |ptr| self.app.core_app.alloc.free(ptr);
} }
@ -298,15 +340,6 @@ fn focusCurrentTab(self: *Window) void {
_ = c.gtk_widget_grab_focus(widget); _ = c.gtk_widget_grab_focus(widget);
} }
fn gtkTabAddClick(_: *c.GtkButton, ud: ?*anyopaque) callconv(.C) void {
const self = userdataSelf(ud.?);
const parent = self.app.core_app.focusedSurface();
self.newTab(parent) catch |err| {
log.warn("error adding new tab: {}", .{err});
return;
};
}
fn gtkTabCloseClick(_: *c.GtkButton, ud: ?*anyopaque) callconv(.C) void { fn gtkTabCloseClick(_: *c.GtkButton, ud: ?*anyopaque) callconv(.C) void {
const surface: *Surface = @ptrCast(@alignCast(ud)); const surface: *Surface = @ptrCast(@alignCast(ud));
surface.core_surface.close(); surface.core_surface.close();
@ -455,6 +488,78 @@ fn getNotebookPageIndex(page: *c.GtkNotebookPage) c_int {
return c.g_value_get_int(&value); return c.g_value_get_int(&value);
} }
fn gtkActionAbout(
_: *c.GSimpleAction,
_: *c.GVariant,
ud: ?*anyopaque,
) callconv(.C) void {
const self: *Window = @ptrCast(@alignCast(ud orelse return));
c.gtk_show_about_dialog(
self.window,
"program-name",
"Ghostty",
"logo-icon-name",
"com.mitchellh.ghostty",
"title",
"About Ghostty",
"version",
build_config.version_string.ptr,
"website",
"https://github.com/mitchellh/ghostty",
@as(?*anyopaque, null),
);
}
fn gtkActionClose(
_: *c.GSimpleAction,
_: *c.GVariant,
ud: ?*anyopaque,
) callconv(.C) void {
const self: *Window = @ptrCast(@alignCast(ud orelse return));
const surface = self.actionSurface() orelse return;
surface.performBindingAction(.{ .close_surface = {} }) catch |err| {
log.warn("error performing binding action error={}", .{err});
return;
};
}
fn gtkActionNewWindow(
_: *c.GSimpleAction,
_: *c.GVariant,
ud: ?*anyopaque,
) callconv(.C) void {
const self: *Window = @ptrCast(@alignCast(ud orelse return));
const surface = self.actionSurface() orelse return;
surface.performBindingAction(.{ .new_window = {} }) catch |err| {
log.warn("error performing binding action error={}", .{err});
return;
};
}
fn gtkActionNewTab(
_: *c.GSimpleAction,
_: *c.GVariant,
ud: ?*anyopaque,
) callconv(.C) void {
const self: *Window = @ptrCast(@alignCast(ud orelse return));
const surface = self.actionSurface() orelse return;
surface.performBindingAction(.{ .new_tab = {} }) catch |err| {
log.warn("error performing binding action error={}", .{err});
return;
};
}
/// Returns the surface to use for an action.
fn actionSurface(self: *Window) ?*CoreSurface {
const page_idx = c.gtk_notebook_get_current_page(self.notebook);
const page = c.gtk_notebook_get_nth_page(self.notebook, page_idx);
const surface: *Surface = @ptrCast(@alignCast(
c.g_object_get_data(@ptrCast(page), GL_AREA_SURFACE) orelse return null,
));
return &surface.core_surface;
}
fn userdataSelf(ud: *anyopaque) *Window { fn userdataSelf(ud: *anyopaque) *Window {
return @ptrCast(@alignCast(ud)); return @ptrCast(@alignCast(ud));
} }

201
src/apprt/gtk/key.zig Normal file
View File

@ -0,0 +1,201 @@
const std = @import("std");
const input = @import("../../input.zig");
const c = @import("c.zig");
/// Returns a GTK accelerator string from a trigger.
pub fn accelFromTrigger(buf: []u8, trigger: input.Binding.Trigger) !?[:0]const u8 {
var buf_stream = std.io.fixedBufferStream(buf);
const writer = buf_stream.writer();
// Modifiers
if (trigger.mods.shift) try writer.writeAll("<Shift>");
if (trigger.mods.ctrl) try writer.writeAll("<Ctrl>");
if (trigger.mods.alt) try writer.writeAll("<Alt>");
if (trigger.mods.super) try writer.writeAll("<Super>");
// Write our key
const keyval = keyvalFromKey(trigger.key) orelse return null;
try writer.writeAll(std.mem.sliceTo(c.gdk_keyval_name(keyval), 0));
// We need to make the string null terminated.
try writer.writeByte(0);
const slice = buf_stream.getWritten();
return slice[0 .. slice.len - 1 :0];
}
pub fn translateMods(state: c.GdkModifierType) input.Mods {
var mods: input.Mods = .{};
if (state & c.GDK_SHIFT_MASK != 0) mods.shift = true;
if (state & c.GDK_CONTROL_MASK != 0) mods.ctrl = true;
if (state & c.GDK_ALT_MASK != 0) mods.alt = true;
if (state & c.GDK_SUPER_MASK != 0) mods.super = true;
// Lock is dependent on the X settings but we just assume caps lock.
if (state & c.GDK_LOCK_MASK != 0) mods.caps_lock = true;
return mods;
}
/// Returns an input key from a keyval or null if we don't have a mapping.
pub fn keyFromKeyval(keyval: c.guint) ?input.Key {
for (keymap) |entry| {
if (entry[0] == keyval) return entry[1];
}
return null;
}
/// Returns a keyval from an input key or null if we don't have a mapping.
pub fn keyvalFromKey(key: input.Key) ?c.guint {
switch (key) {
inline else => |key_comptime| {
return comptime value: {
@setEvalBranchQuota(10_000);
for (keymap) |entry| {
if (entry[1] == key_comptime) break :value entry[0];
}
break :value null;
};
},
}
}
test "accelFromTrigger" {
const testing = std.testing;
var buf: [256]u8 = undefined;
try testing.expectEqualStrings("<Super>q", (try accelFromTrigger(&buf, .{
.mods = .{ .super = true },
.key = .q,
})).?);
}
/// A raw entry in the keymap. Our keymap contains mappings between
/// GDK keys and our own key enum.
const RawEntry = struct { c.guint, input.Key };
const keymap: []const RawEntry = &.{
.{ c.GDK_KEY_a, .a },
.{ c.GDK_KEY_b, .b },
.{ c.GDK_KEY_c, .c },
.{ c.GDK_KEY_d, .d },
.{ c.GDK_KEY_e, .e },
.{ c.GDK_KEY_f, .f },
.{ c.GDK_KEY_g, .g },
.{ c.GDK_KEY_h, .h },
.{ c.GDK_KEY_i, .i },
.{ c.GDK_KEY_j, .j },
.{ c.GDK_KEY_k, .k },
.{ c.GDK_KEY_l, .l },
.{ c.GDK_KEY_m, .m },
.{ c.GDK_KEY_n, .n },
.{ c.GDK_KEY_o, .o },
.{ c.GDK_KEY_p, .p },
.{ c.GDK_KEY_q, .q },
.{ c.GDK_KEY_r, .r },
.{ c.GDK_KEY_s, .s },
.{ c.GDK_KEY_t, .t },
.{ c.GDK_KEY_u, .u },
.{ c.GDK_KEY_v, .v },
.{ c.GDK_KEY_w, .w },
.{ c.GDK_KEY_x, .x },
.{ c.GDK_KEY_y, .y },
.{ c.GDK_KEY_z, .z },
.{ c.GDK_KEY_0, .zero },
.{ c.GDK_KEY_1, .one },
.{ c.GDK_KEY_2, .two },
.{ c.GDK_KEY_3, .three },
.{ c.GDK_KEY_4, .four },
.{ c.GDK_KEY_5, .five },
.{ c.GDK_KEY_6, .six },
.{ c.GDK_KEY_7, .seven },
.{ c.GDK_KEY_8, .eight },
.{ c.GDK_KEY_9, .nine },
.{ c.GDK_KEY_semicolon, .semicolon },
.{ c.GDK_KEY_space, .space },
.{ c.GDK_KEY_apostrophe, .apostrophe },
.{ c.GDK_KEY_comma, .comma },
.{ c.GDK_KEY_grave, .grave_accent },
.{ c.GDK_KEY_period, .period },
.{ c.GDK_KEY_slash, .slash },
.{ c.GDK_KEY_minus, .minus },
.{ c.GDK_KEY_equal, .equal },
.{ c.GDK_KEY_bracketleft, .left_bracket },
.{ c.GDK_KEY_bracketright, .right_bracket },
.{ c.GDK_KEY_backslash, .backslash },
.{ c.GDK_KEY_Up, .up },
.{ c.GDK_KEY_Down, .down },
.{ c.GDK_KEY_Right, .right },
.{ c.GDK_KEY_Left, .left },
.{ c.GDK_KEY_Home, .home },
.{ c.GDK_KEY_End, .end },
.{ c.GDK_KEY_Insert, .insert },
.{ c.GDK_KEY_Delete, .delete },
.{ c.GDK_KEY_Caps_Lock, .caps_lock },
.{ c.GDK_KEY_Scroll_Lock, .scroll_lock },
.{ c.GDK_KEY_Num_Lock, .num_lock },
.{ c.GDK_KEY_Page_Up, .page_up },
.{ c.GDK_KEY_Page_Down, .page_down },
.{ c.GDK_KEY_Escape, .escape },
.{ c.GDK_KEY_Return, .enter },
.{ c.GDK_KEY_Tab, .tab },
.{ c.GDK_KEY_BackSpace, .backspace },
.{ c.GDK_KEY_Print, .print_screen },
.{ c.GDK_KEY_Pause, .pause },
.{ c.GDK_KEY_F1, .f1 },
.{ c.GDK_KEY_F2, .f2 },
.{ c.GDK_KEY_F3, .f3 },
.{ c.GDK_KEY_F4, .f4 },
.{ c.GDK_KEY_F5, .f5 },
.{ c.GDK_KEY_F6, .f6 },
.{ c.GDK_KEY_F7, .f7 },
.{ c.GDK_KEY_F8, .f8 },
.{ c.GDK_KEY_F9, .f9 },
.{ c.GDK_KEY_F10, .f10 },
.{ c.GDK_KEY_F11, .f11 },
.{ c.GDK_KEY_F12, .f12 },
.{ c.GDK_KEY_F13, .f13 },
.{ c.GDK_KEY_F14, .f14 },
.{ c.GDK_KEY_F15, .f15 },
.{ c.GDK_KEY_F16, .f16 },
.{ c.GDK_KEY_F17, .f17 },
.{ c.GDK_KEY_F18, .f18 },
.{ c.GDK_KEY_F19, .f19 },
.{ c.GDK_KEY_F20, .f20 },
.{ c.GDK_KEY_F21, .f21 },
.{ c.GDK_KEY_F22, .f22 },
.{ c.GDK_KEY_F23, .f23 },
.{ c.GDK_KEY_F24, .f24 },
.{ c.GDK_KEY_F25, .f25 },
.{ c.GDK_KEY_KP_0, .kp_0 },
.{ c.GDK_KEY_KP_1, .kp_1 },
.{ c.GDK_KEY_KP_2, .kp_2 },
.{ c.GDK_KEY_KP_3, .kp_3 },
.{ c.GDK_KEY_KP_4, .kp_4 },
.{ c.GDK_KEY_KP_5, .kp_5 },
.{ c.GDK_KEY_KP_6, .kp_6 },
.{ c.GDK_KEY_KP_7, .kp_7 },
.{ c.GDK_KEY_KP_8, .kp_8 },
.{ c.GDK_KEY_KP_9, .kp_9 },
.{ c.GDK_KEY_KP_Decimal, .kp_decimal },
.{ c.GDK_KEY_KP_Divide, .kp_divide },
.{ c.GDK_KEY_KP_Multiply, .kp_multiply },
.{ c.GDK_KEY_KP_Subtract, .kp_subtract },
.{ c.GDK_KEY_KP_Add, .kp_add },
.{ c.GDK_KEY_KP_Enter, .kp_enter },
.{ c.GDK_KEY_KP_Equal, .kp_equal },
.{ c.GDK_KEY_Shift_L, .left_shift },
.{ c.GDK_KEY_Control_L, .left_control },
.{ c.GDK_KEY_Alt_L, .left_alt },
.{ c.GDK_KEY_Super_L, .left_super },
.{ c.GDK_KEY_Shift_R, .right_shift },
.{ c.GDK_KEY_Control_R, .right_control },
.{ c.GDK_KEY_Alt_R, .right_alt },
.{ c.GDK_KEY_Super_R, .right_super },
};

View File

@ -259,6 +259,7 @@ test {
_ = @import("Pty.zig"); _ = @import("Pty.zig");
_ = @import("Command.zig"); _ = @import("Command.zig");
_ = @import("font/main.zig"); _ = @import("font/main.zig");
_ = @import("apprt.zig");
_ = @import("renderer.zig"); _ = @import("renderer.zig");
_ = @import("termio.zig"); _ = @import("termio.zig");
_ = @import("input.zig"); _ = @import("input.zig");