From 6b75ca40ca2a1e092c92eb0a9575a231abee140b Mon Sep 17 00:00:00 2001 From: Maciej Bartczak <39600846+maciekbartczak@users.noreply.github.com> Date: Wed, 19 Feb 2025 20:55:15 +0100 Subject: [PATCH 01/18] Implement a prompt that allows the user to set the title --- src/apprt/gtk/App.zig | 103 +++++++++++++++++++++++++++++++++++++- src/apprt/gtk/Surface.zig | 35 +++++++++++++ src/apprt/gtk/Window.zig | 14 ++++++ 3 files changed, 151 insertions(+), 1 deletion(-) diff --git a/src/apprt/gtk/App.zig b/src/apprt/gtk/App.zig index b26bc046f..9323d88cb 100644 --- a/src/apprt/gtk/App.zig +++ b/src/apprt/gtk/App.zig @@ -495,6 +495,7 @@ pub fn performAction( .toggle_split_zoom => self.toggleSplitZoom(target), .toggle_window_decorations => self.toggleWindowDecorations(target), .quit_timer => self.quitTimer(value), + .prompt_title => try self.promptTitle(target), // Unimplemented .close_all_windows, @@ -506,7 +507,6 @@ pub fn performAction( .render_inspector, .renderer_health, .color_change, - .prompt_title, => { log.warn("unimplemented action={}", .{action}); return false; @@ -770,6 +770,15 @@ fn quitTimer(self: *App, mode: apprt.action.QuitTimer) void { } } +fn promptTitle(_: *App, target: apprt.Target) !void { + switch (target) { + .app => {}, + .surface => |v| { + try v.rt_surface.promptTitle(); + }, + } +} + fn setTitle( _: *App, target: apprt.Target, @@ -1016,6 +1025,7 @@ fn syncActionAccelerators(self: *App) !void { try self.syncActionAccelerator("win.paste", .{ .paste_from_clipboard = {} }); try self.syncActionAccelerator("win.reset", .{ .reset = {} }); try self.syncActionAccelerator("win.clear", .{ .clear_screen = {} }); + try self.syncActionAccelerator("win.prompt-title", .{ .prompt_surface_title = {} }); } fn syncActionAccelerator( @@ -1766,6 +1776,97 @@ fn initActions(self: *App) void { } } +<<<<<<< HEAD +======= +/// Initializes and populates the provided GMenu with sections and actions. +/// This function is used to set up the application's menu structure, either for +/// the main menu button or as a context menu when window decorations are disabled. +fn initMenuContent(menu: *c.GMenu) void { + { + 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 Tab", "win.close_tab"); + c.g_menu_append(section, "Split Right", "win.split_right"); + c.g_menu_append(section, "Split Down", "win.split_down"); + 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, "Terminal Inspector", "win.toggle_inspector"); + c.g_menu_append(section, "Open Configuration", "app.open-config"); + c.g_menu_append(section, "Reload Configuration", "app.reload-config"); + c.g_menu_append(section, "About Ghostty", "win.about"); + } +} + +/// 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); + initMenuContent(@ptrCast(menu)); + self.menu = menu; +} + +fn initContextMenu(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, "Copy", "win.copy"); + c.g_menu_append(section, "Paste", "win.paste"); + } + + { + 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, "Split Right", "win.split_right"); + c.g_menu_append(section, "Split Down", "win.split_down"); + } + + { + 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, "Change Title...", "win.prompt-title"); + } + + { + 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, "Reset", "win.reset"); + c.g_menu_append(section, "Terminal Inspector", "win.toggle_inspector"); + } + + const section = c.g_menu_new(); + defer c.g_object_unref(section); + const submenu = c.g_menu_new(); + defer c.g_object_unref(submenu); + + initMenuContent(@ptrCast(submenu)); + c.g_menu_append_submenu(section, "Menu", @ptrCast(@alignCast(submenu))); + c.g_menu_append_section(menu, null, @ptrCast(@alignCast(section))); + + self.context_menu = menu; +} + +pub fn refreshContextMenu(_: *App, window: ?*c.GtkWindow, has_selection: bool) void { + const action: ?*c.GSimpleAction = @ptrCast(c.g_action_map_lookup_action(@ptrCast(window), "copy")); + c.g_simple_action_set_enabled(action, if (has_selection) 1 else 0); +} + +>>>>>>> a9ae01e38 (Implement a prompt that allows the user to set the title) fn isValidAppId(app_id: [:0]const u8) bool { if (app_id.len > 255 or app_id.len == 0) return false; if (app_id[0] == '.') return false; diff --git a/src/apprt/gtk/Surface.zig b/src/apprt/gtk/Surface.zig index 9835b5b77..beff7b9ce 100644 --- a/src/apprt/gtk/Surface.zig +++ b/src/apprt/gtk/Surface.zig @@ -987,6 +987,31 @@ pub fn getTitle(self: *Surface) ?[:0]const u8 { return null; } +const PromptTitleDialogContext = struct { + entry: *c.GtkWidget, + self: *Surface, +}; + +pub fn promptTitle(self: *Surface) !void { + const window = self.container.window() orelse return; + + const context = try self.app.core_app.alloc.create(PromptTitleDialogContext); + context.self = self; + + const dialog = c.gtk_message_dialog_new(window.window, c.GTK_DIALOG_MODAL, c.GTK_MESSAGE_QUESTION, c.GTK_BUTTONS_OK_CANCEL, "Set Tab Title"); + + const content_area = c.gtk_message_dialog_get_message_area(@ptrCast(dialog)); + + const entry = c.gtk_entry_new(); + context.entry = entry; + c.gtk_box_append(@ptrCast(content_area), entry); + c.gtk_widget_show(entry); + + _ = c.g_signal_connect_data(dialog, "response", c.G_CALLBACK(>kPromptTitleResponse), context, null, c.G_CONNECT_DEFAULT); + + c.gtk_widget_show(dialog); +} + /// Set the current working directory of the surface. /// /// In addition, update the tab's tooltip text, and if we are the focused child, @@ -2273,3 +2298,13 @@ fn g_value_holds(value_: ?*c.GValue, g_type: c.GType) bool { } return false; } + +fn gtkPromptTitleResponse(dialog: *c.GtkDialog, response: c.gint, ud: ?*anyopaque) callconv(.C) void { + const context: *PromptTitleDialogContext = @ptrCast(@alignCast(ud)); + if (response == c.GTK_RESPONSE_OK) { + const buffer = c.gtk_entry_get_buffer(@ptrCast(context.entry)); + const title = c.gtk_entry_buffer_get_text(buffer); + context.self.setTitle(std.mem.span(title)) catch {}; + } + c.gtk_window_destroy(@ptrCast(dialog)); +} diff --git a/src/apprt/gtk/Window.zig b/src/apprt/gtk/Window.zig index 7b74da722..22148706c 100644 --- a/src/apprt/gtk/Window.zig +++ b/src/apprt/gtk/Window.zig @@ -478,6 +478,7 @@ fn initActions(self: *Window) void { .{ "paste", >kActionPaste }, .{ "reset", >kActionReset }, .{ "clear", >kActionClear }, + .{ "prompt_title", >kActionPromptTitle }, }; inline for (actions) |entry| { @@ -1071,6 +1072,19 @@ fn gtkActionClear( }; } +fn gtkActionPromptTitle( + _: *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(.{ .prompt_surface_title = {} }) catch |err| { + log.warn("error performing binding action error={}", .{err}); + return; + }; +} + /// Returns the surface to use for an action. pub fn actionSurface(self: *Window) ?*CoreSurface { const tab = self.notebook.currentTab() orelse return null; From 5e9908af279a7d1cae607a78b61fff947850efb2 Mon Sep 17 00:00:00 2001 From: Maciej Bartczak <39600846+maciekbartczak@users.noreply.github.com> Date: Thu, 20 Feb 2025 20:39:59 +0100 Subject: [PATCH 02/18] make the change of the title persistent & allow the user to restore to a default one --- src/apprt/gtk/App.zig | 2 +- src/apprt/gtk/Surface.zig | 71 ++++++++++++++++++++++++++++++++++----- 2 files changed, 64 insertions(+), 9 deletions(-) diff --git a/src/apprt/gtk/App.zig b/src/apprt/gtk/App.zig index 9323d88cb..fec2626bd 100644 --- a/src/apprt/gtk/App.zig +++ b/src/apprt/gtk/App.zig @@ -786,7 +786,7 @@ fn setTitle( ) !void { switch (target) { .app => {}, - .surface => |v| try v.rt_surface.setTitle(title.title), + .surface => |v| try v.rt_surface.setTitle(title.title, .TERMINAL), } } diff --git a/src/apprt/gtk/Surface.zig b/src/apprt/gtk/Surface.zig index beff7b9ce..7fdac9aa8 100644 --- a/src/apprt/gtk/Surface.zig +++ b/src/apprt/gtk/Surface.zig @@ -347,6 +347,11 @@ cursor: ?*c.GdkCursor = null, /// pass it to GTK. title_text: ?[:0]const u8 = null, +/// The title of the surface as reported by the terminal. +/// If it is null, the title reported by the terminal is currently being used. +/// If the title was manually overriden by the user, this will be set to a non-null value representing the default terminal title. +title_from_terminal: ?[:0]const u8 = null, + /// Our current working directory. We use this value for setting tooltips in /// the headerbar subtitle if we have focus. When set, the text in this buf /// will be null-terminated because we need to pass it to GTK. @@ -940,8 +945,9 @@ fn updateTitleLabels(self: *Surface) void { } const zoom_title_prefix = "🔍 "; +pub const SetTitleSource = enum { USER, TERMINAL }; -pub fn setTitle(self: *Surface, slice: [:0]const u8) !void { +pub fn setTitle(self: *Surface, slice: [:0]const u8, source: SetTitleSource) !void { const alloc = self.app.core_app.alloc; // Always allocate with the "🔍 " at the beginning and slice accordingly @@ -954,6 +960,14 @@ pub fn setTitle(self: *Surface, slice: [:0]const u8) !void { }; errdefer alloc.free(copy); + // If The user has overriden the title we only want to update the terminal provided title + // so that it can be restored to the most recent state + if (self.title_from_terminal != null and source == .TERMINAL) { + alloc.free(self.title_from_terminal.?); + self.title_from_terminal = copy; + return; + } + if (self.title_text) |old| alloc.free(old); self.title_text = copy; @@ -978,15 +992,27 @@ fn updateTitleTimerExpired(ctx: ?*anyopaque) callconv(.C) c.gboolean { pub fn getTitle(self: *Surface) ?[:0]const u8 { if (self.title_text) |title_text| { - return if (self.zoomed_in) - title_text - else - title_text[zoom_title_prefix.len..]; + return self.resolveTitle(title_text); } return null; } +pub fn getTerminalTitle(self: *Surface) ?[:0]const u8 { + if (self.title_from_terminal) |title_text| { + return self.resolveTitle(title_text); + } + + return null; +} + +fn resolveTitle(self: *Surface, title: [:0]const u8) [:0]const u8 { + return if (self.zoomed_in) + title + else + title[zoom_title_prefix.len..]; +} + const PromptTitleDialogContext = struct { entry: *c.GtkWidget, self: *Surface, @@ -998,12 +1024,15 @@ pub fn promptTitle(self: *Surface) !void { const context = try self.app.core_app.alloc.create(PromptTitleDialogContext); context.self = self; - const dialog = c.gtk_message_dialog_new(window.window, c.GTK_DIALOG_MODAL, c.GTK_MESSAGE_QUESTION, c.GTK_BUTTONS_OK_CANCEL, "Set Tab Title"); + const dialog = c.gtk_message_dialog_new(window.window, c.GTK_DIALOG_MODAL, c.GTK_MESSAGE_QUESTION, c.GTK_BUTTONS_OK_CANCEL, "Change Terminal Title"); + c.gtk_message_dialog_format_secondary_text(@ptrCast(dialog), "Leave blank to restore the default title."); const content_area = c.gtk_message_dialog_get_message_area(@ptrCast(dialog)); const entry = c.gtk_entry_new(); context.entry = entry; + const buffer = c.gtk_entry_get_buffer(@ptrCast(entry)); + c.gtk_entry_buffer_set_text(buffer, self.getTitle() orelse "", -1); c.gtk_box_append(@ptrCast(content_area), entry); c.gtk_widget_show(entry); @@ -2301,10 +2330,36 @@ fn g_value_holds(value_: ?*c.GValue, g_type: c.GType) bool { fn gtkPromptTitleResponse(dialog: *c.GtkDialog, response: c.gint, ud: ?*anyopaque) callconv(.C) void { const context: *PromptTitleDialogContext = @ptrCast(@alignCast(ud)); + if (response == c.GTK_RESPONSE_OK) { const buffer = c.gtk_entry_get_buffer(@ptrCast(context.entry)); - const title = c.gtk_entry_buffer_get_text(buffer); - context.self.setTitle(std.mem.span(title)) catch {}; + const title = std.mem.span(c.gtk_entry_buffer_get_text(buffer)); + + // if the new title is empty and the user has set the title previously, restore the terminal provided title + if (title.len == 0 and context.self.title_from_terminal != null) { + if (context.self.getTerminalTitle()) |terminal_title| { + context.self.setTitle(terminal_title, .USER) catch {}; + context.self.app.core_app.alloc.free(context.self.title_from_terminal.?); + context.self.title_from_terminal = null; + } + } else { + // if this is the first time the user is setting the title, save the current terminal provided title + if (context.self.title_from_terminal == null and context.self.title_text != null) { + const current_title = context.self.getTitle().?; + context.self.title_from_terminal = context.self.app.core_app.alloc.dupeZ(u8, current_title) catch |err| switch (err) { + error.OutOfMemory => { + log.err("Failed to allocate memory for title: {}", .{err}); + context.self.app.core_app.alloc.destroy(context); + c.gtk_window_destroy(@ptrCast(dialog)); + return; + }, + }; + } + + context.self.setTitle(title, .USER) catch {}; + } } + + context.self.app.core_app.alloc.destroy(context); c.gtk_window_destroy(@ptrCast(dialog)); } From cc9c45de2a5795a0f0085694eb75e536f7a44d87 Mon Sep 17 00:00:00 2001 From: Maciej Bartczak <39600846+maciekbartczak@users.noreply.github.com> Date: Thu, 20 Feb 2025 20:55:50 +0100 Subject: [PATCH 03/18] fix the edge case when user tries to revert the title to default and hasn't set a title before --- src/apprt/gtk/Surface.zig | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/apprt/gtk/Surface.zig b/src/apprt/gtk/Surface.zig index 7fdac9aa8..af20bceaa 100644 --- a/src/apprt/gtk/Surface.zig +++ b/src/apprt/gtk/Surface.zig @@ -960,8 +960,8 @@ pub fn setTitle(self: *Surface, slice: [:0]const u8, source: SetTitleSource) !vo }; errdefer alloc.free(copy); - // If The user has overriden the title we only want to update the terminal provided title - // so that it can be restored to the most recent state + // The user has overriden the title + // We only want to update the terminal provided title so that it can be restored to the most recent state. if (self.title_from_terminal != null and source == .TERMINAL) { alloc.free(self.title_from_terminal.?); self.title_from_terminal = copy; @@ -2342,11 +2342,10 @@ fn gtkPromptTitleResponse(dialog: *c.GtkDialog, response: c.gint, ud: ?*anyopaqu context.self.app.core_app.alloc.free(context.self.title_from_terminal.?); context.self.title_from_terminal = null; } - } else { + } else if (title.len > 0) { // if this is the first time the user is setting the title, save the current terminal provided title if (context.self.title_from_terminal == null and context.self.title_text != null) { - const current_title = context.self.getTitle().?; - context.self.title_from_terminal = context.self.app.core_app.alloc.dupeZ(u8, current_title) catch |err| switch (err) { + context.self.title_from_terminal = context.self.app.core_app.alloc.dupeZ(u8, context.self.title_text.?) catch |err| switch (err) { error.OutOfMemory => { log.err("Failed to allocate memory for title: {}", .{err}); context.self.app.core_app.alloc.destroy(context); From 95fc5ad1e9f5022c83346706fab7010f7bde1797 Mon Sep 17 00:00:00 2001 From: Maciej Bartczak <39600846+maciekbartczak@users.noreply.github.com> Date: Thu, 20 Feb 2025 21:02:07 +0100 Subject: [PATCH 04/18] remove outdated comment --- src/input/Binding.zig | 1 - 1 file changed, 1 deletion(-) diff --git a/src/input/Binding.zig b/src/input/Binding.zig index f91967293..d1e66210b 100644 --- a/src/input/Binding.zig +++ b/src/input/Binding.zig @@ -349,7 +349,6 @@ pub const Action = union(enum) { toggle_tab_overview: void, /// Change the title of the current focused surface via a prompt. - /// This only works on macOS currently. prompt_surface_title: void, /// Create a new split in the given direction. From 3542778d848019f815c4b7a83b9bc4b3a61eff2c Mon Sep 17 00:00:00 2001 From: Maciej Bartczak <39600846+maciekbartczak@users.noreply.github.com> Date: Thu, 20 Feb 2025 21:20:03 +0100 Subject: [PATCH 05/18] free the terminal title when destroy is run --- src/apprt/gtk/Surface.zig | 1 + 1 file changed, 1 insertion(+) diff --git a/src/apprt/gtk/Surface.zig b/src/apprt/gtk/Surface.zig index af20bceaa..72482b59e 100644 --- a/src/apprt/gtk/Surface.zig +++ b/src/apprt/gtk/Surface.zig @@ -668,6 +668,7 @@ fn realize(self: *Surface) !void { pub fn deinit(self: *Surface) void { self.init_config.deinit(self.app.core_app.alloc); if (self.title_text) |title| self.app.core_app.alloc.free(title); + if (self.title_from_terminal) |title| self.app.core_app.alloc.free(title); if (self.pwd) |pwd| self.app.core_app.alloc.free(pwd); // We don't allocate anything if we aren't realized. From dcd17c6ac4953a0086272bb1339aba15a3913b3f Mon Sep 17 00:00:00 2001 From: Maciej Bartczak <39600846+maciekbartczak@users.noreply.github.com> Date: Thu, 20 Feb 2025 21:31:41 +0100 Subject: [PATCH 06/18] set the ok widget to be activated by default --- src/apprt/gtk/Surface.zig | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/apprt/gtk/Surface.zig b/src/apprt/gtk/Surface.zig index 72482b59e..42662f2da 100644 --- a/src/apprt/gtk/Surface.zig +++ b/src/apprt/gtk/Surface.zig @@ -1025,8 +1025,10 @@ pub fn promptTitle(self: *Surface) !void { const context = try self.app.core_app.alloc.create(PromptTitleDialogContext); context.self = self; - const dialog = c.gtk_message_dialog_new(window.window, c.GTK_DIALOG_MODAL, c.GTK_MESSAGE_QUESTION, c.GTK_BUTTONS_OK_CANCEL, "Change Terminal Title"); + const dialog = c.gtk_message_dialog_new(window.window, c.GTK_DIALOG_MODAL, c.GTK_MESSAGE_OTHER, c.GTK_BUTTONS_OK_CANCEL, "Change Terminal Title"); c.gtk_message_dialog_format_secondary_text(@ptrCast(dialog), "Leave blank to restore the default title."); + const ok_widget = c.gtk_dialog_get_widget_for_response(@ptrCast(dialog), c.GTK_RESPONSE_OK); + c.gtk_window_set_default_widget(@ptrCast(dialog), ok_widget); const content_area = c.gtk_message_dialog_get_message_area(@ptrCast(dialog)); @@ -1035,6 +1037,7 @@ pub fn promptTitle(self: *Surface) !void { const buffer = c.gtk_entry_get_buffer(@ptrCast(entry)); c.gtk_entry_buffer_set_text(buffer, self.getTitle() orelse "", -1); c.gtk_box_append(@ptrCast(content_area), entry); + c.gtk_entry_set_activates_default(@ptrCast(entry), 1); c.gtk_widget_show(entry); _ = c.g_signal_connect_data(dialog, "response", c.G_CALLBACK(>kPromptTitleResponse), context, null, c.G_CONNECT_DEFAULT); From 6189f5d09ec29a5010be360e8ca4e18f72665ac1 Mon Sep 17 00:00:00 2001 From: Maciej Bartczak <39600846+maciekbartczak@users.noreply.github.com> Date: Fri, 21 Feb 2025 21:53:28 +0100 Subject: [PATCH 07/18] code review: - use blueprint for the dialog content - use zig-gobject bindings - make the enum values lowercase --- src/apprt/gtk/App.zig | 2 +- src/apprt/gtk/Surface.zig | 63 ++++++++++-------------- src/apprt/gtk/gresource.zig | 2 +- src/apprt/gtk/ui/prompt-title-dialog.blp | 9 ++++ 4 files changed, 38 insertions(+), 38 deletions(-) create mode 100644 src/apprt/gtk/ui/prompt-title-dialog.blp diff --git a/src/apprt/gtk/App.zig b/src/apprt/gtk/App.zig index fec2626bd..481e35e20 100644 --- a/src/apprt/gtk/App.zig +++ b/src/apprt/gtk/App.zig @@ -786,7 +786,7 @@ fn setTitle( ) !void { switch (target) { .app => {}, - .surface => |v| try v.rt_surface.setTitle(title.title, .TERMINAL), + .surface => |v| try v.rt_surface.setTitle(title.title, .terminal), } } diff --git a/src/apprt/gtk/Surface.zig b/src/apprt/gtk/Surface.zig index 42662f2da..589e32ef1 100644 --- a/src/apprt/gtk/Surface.zig +++ b/src/apprt/gtk/Surface.zig @@ -4,6 +4,10 @@ const Surface = @This(); const std = @import("std"); +const adw = @import("adw"); +const gtk = @import("gtk"); +const gio = @import("gio"); +const glib = @import("glib"); const Allocator = std.mem.Allocator; const build_config = @import("../../build_config.zig"); const build_options = @import("build_options"); @@ -26,6 +30,7 @@ const ResizeOverlay = @import("ResizeOverlay.zig"); const inspector = @import("inspector.zig"); const gtk_key = @import("key.zig"); const c = @import("c.zig").c; +const Builder = @import("Builder.zig"); const log = std.log.scoped(.gtk_surface); @@ -946,7 +951,7 @@ fn updateTitleLabels(self: *Surface) void { } const zoom_title_prefix = "🔍 "; -pub const SetTitleSource = enum { USER, TERMINAL }; +pub const SetTitleSource = enum { user, terminal }; pub fn setTitle(self: *Surface, slice: [:0]const u8, source: SetTitleSource) !void { const alloc = self.app.core_app.alloc; @@ -963,7 +968,7 @@ pub fn setTitle(self: *Surface, slice: [:0]const u8, source: SetTitleSource) !vo // The user has overriden the title // We only want to update the terminal provided title so that it can be restored to the most recent state. - if (self.title_from_terminal != null and source == .TERMINAL) { + if (self.title_from_terminal != null and source == .terminal) { alloc.free(self.title_from_terminal.?); self.title_from_terminal = copy; return; @@ -1022,27 +1027,14 @@ const PromptTitleDialogContext = struct { pub fn promptTitle(self: *Surface) !void { const window = self.container.window() orelse return; - const context = try self.app.core_app.alloc.create(PromptTitleDialogContext); - context.self = self; + var builder = Builder.init("prompt-title-dialog", .blp); + defer builder.deinit(); - const dialog = c.gtk_message_dialog_new(window.window, c.GTK_DIALOG_MODAL, c.GTK_MESSAGE_OTHER, c.GTK_BUTTONS_OK_CANCEL, "Change Terminal Title"); - c.gtk_message_dialog_format_secondary_text(@ptrCast(dialog), "Leave blank to restore the default title."); - const ok_widget = c.gtk_dialog_get_widget_for_response(@ptrCast(dialog), c.GTK_RESPONSE_OK); - c.gtk_window_set_default_widget(@ptrCast(dialog), ok_widget); + const dialog: *adw.AlertDialog = @ptrCast(builder.getObject("prompt_title_dialog")); + dialog.addResponse("cancel", "Cancel"); + dialog.addResponse("ok", "OK"); - const content_area = c.gtk_message_dialog_get_message_area(@ptrCast(dialog)); - - const entry = c.gtk_entry_new(); - context.entry = entry; - const buffer = c.gtk_entry_get_buffer(@ptrCast(entry)); - c.gtk_entry_buffer_set_text(buffer, self.getTitle() orelse "", -1); - c.gtk_box_append(@ptrCast(content_area), entry); - c.gtk_entry_set_activates_default(@ptrCast(entry), 1); - c.gtk_widget_show(entry); - - _ = c.g_signal_connect_data(dialog, "response", c.G_CALLBACK(>kPromptTitleResponse), context, null, c.G_CONNECT_DEFAULT); - - c.gtk_widget_show(dialog); + dialog.choose(@ptrCast(window.window), null, @ptrCast(>kPromptTitleResponse), self); } /// Set the current working directory of the surface. @@ -2332,37 +2324,36 @@ fn g_value_holds(value_: ?*c.GValue, g_type: c.GType) bool { return false; } -fn gtkPromptTitleResponse(dialog: *c.GtkDialog, response: c.gint, ud: ?*anyopaque) callconv(.C) void { - const context: *PromptTitleDialogContext = @ptrCast(@alignCast(ud)); +fn gtkPromptTitleResponse(dialog: *adw.AlertDialog, result: *gio.AsyncResult, ud: ?*anyopaque) callconv(.C) void { + const self = userdataSelf(ud.?); - if (response == c.GTK_RESPONSE_OK) { - const buffer = c.gtk_entry_get_buffer(@ptrCast(context.entry)); - const title = std.mem.span(c.gtk_entry_buffer_get_text(buffer)); + const response = dialog.chooseFinish(result); + if (glib.strEqual("ok", response) != 0) { + const title_entry: *gtk.Entry = @ptrCast(dialog.getExtraChild()); + const title = std.mem.span(title_entry.getBuffer().getText()); // if the new title is empty and the user has set the title previously, restore the terminal provided title - if (title.len == 0 and context.self.title_from_terminal != null) { - if (context.self.getTerminalTitle()) |terminal_title| { - context.self.setTitle(terminal_title, .USER) catch {}; - context.self.app.core_app.alloc.free(context.self.title_from_terminal.?); - context.self.title_from_terminal = null; + if (title.len == 0 and self.title_from_terminal != null) { + if (self.getTerminalTitle()) |terminal_title| { + self.setTitle(terminal_title, .user) catch {}; + self.app.core_app.alloc.free(self.title_from_terminal.?); + self.title_from_terminal = null; } } else if (title.len > 0) { // if this is the first time the user is setting the title, save the current terminal provided title - if (context.self.title_from_terminal == null and context.self.title_text != null) { - context.self.title_from_terminal = context.self.app.core_app.alloc.dupeZ(u8, context.self.title_text.?) catch |err| switch (err) { + if (self.title_from_terminal == null and self.title_text != null) { + self.title_from_terminal = self.app.core_app.alloc.dupeZ(u8, self.title_text.?) catch |err| switch (err) { error.OutOfMemory => { log.err("Failed to allocate memory for title: {}", .{err}); - context.self.app.core_app.alloc.destroy(context); c.gtk_window_destroy(@ptrCast(dialog)); return; }, }; } - context.self.setTitle(title, .USER) catch {}; + self.setTitle(title, .user) catch {}; } } - context.self.app.core_app.alloc.destroy(context); c.gtk_window_destroy(@ptrCast(dialog)); } diff --git a/src/apprt/gtk/gresource.zig b/src/apprt/gtk/gresource.zig index d45997d6c..82452bb53 100644 --- a/src/apprt/gtk/gresource.zig +++ b/src/apprt/gtk/gresource.zig @@ -57,7 +57,7 @@ pub const ui_files = [_][]const u8{ "menu-window-titlebar_menu", "menu-surface-context_menu", }; -pub const blueprint_files = [_][]const u8{}; +pub const blueprint_files = [_][]const u8{"prompt-title-dialog"}; pub fn main() !void { var gpa = std.heap.GeneralPurposeAllocator(.{}){}; diff --git a/src/apprt/gtk/ui/prompt-title-dialog.blp b/src/apprt/gtk/ui/prompt-title-dialog.blp new file mode 100644 index 000000000..cdc7fb42c --- /dev/null +++ b/src/apprt/gtk/ui/prompt-title-dialog.blp @@ -0,0 +1,9 @@ +using Gtk 4.0; +using Adw 1; + +Adw.AlertDialog prompt_title_dialog { + heading: "Change Terminal Title"; + body: "Leave blank to restore the default title."; + + extra-child: Entry title_entry {}; +} \ No newline at end of file From 454a53b3f138d8f317db681a140c60252bff8b61 Mon Sep 17 00:00:00 2001 From: Maciej Bartczak <39600846+maciekbartczak@users.noreply.github.com> Date: Fri, 21 Feb 2025 21:59:53 +0100 Subject: [PATCH 08/18] code review: - remove the menu entry defined in code --- src/apprt/gtk/App.zig | 91 ------------------------------------------- 1 file changed, 91 deletions(-) diff --git a/src/apprt/gtk/App.zig b/src/apprt/gtk/App.zig index 481e35e20..e9434bb5b 100644 --- a/src/apprt/gtk/App.zig +++ b/src/apprt/gtk/App.zig @@ -1776,97 +1776,6 @@ fn initActions(self: *App) void { } } -<<<<<<< HEAD -======= -/// Initializes and populates the provided GMenu with sections and actions. -/// This function is used to set up the application's menu structure, either for -/// the main menu button or as a context menu when window decorations are disabled. -fn initMenuContent(menu: *c.GMenu) void { - { - 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 Tab", "win.close_tab"); - c.g_menu_append(section, "Split Right", "win.split_right"); - c.g_menu_append(section, "Split Down", "win.split_down"); - 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, "Terminal Inspector", "win.toggle_inspector"); - c.g_menu_append(section, "Open Configuration", "app.open-config"); - c.g_menu_append(section, "Reload Configuration", "app.reload-config"); - c.g_menu_append(section, "About Ghostty", "win.about"); - } -} - -/// 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); - initMenuContent(@ptrCast(menu)); - self.menu = menu; -} - -fn initContextMenu(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, "Copy", "win.copy"); - c.g_menu_append(section, "Paste", "win.paste"); - } - - { - 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, "Split Right", "win.split_right"); - c.g_menu_append(section, "Split Down", "win.split_down"); - } - - { - 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, "Change Title...", "win.prompt-title"); - } - - { - 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, "Reset", "win.reset"); - c.g_menu_append(section, "Terminal Inspector", "win.toggle_inspector"); - } - - const section = c.g_menu_new(); - defer c.g_object_unref(section); - const submenu = c.g_menu_new(); - defer c.g_object_unref(submenu); - - initMenuContent(@ptrCast(submenu)); - c.g_menu_append_submenu(section, "Menu", @ptrCast(@alignCast(submenu))); - c.g_menu_append_section(menu, null, @ptrCast(@alignCast(section))); - - self.context_menu = menu; -} - -pub fn refreshContextMenu(_: *App, window: ?*c.GtkWindow, has_selection: bool) void { - const action: ?*c.GSimpleAction = @ptrCast(c.g_action_map_lookup_action(@ptrCast(window), "copy")); - c.g_simple_action_set_enabled(action, if (has_selection) 1 else 0); -} - ->>>>>>> a9ae01e38 (Implement a prompt that allows the user to set the title) fn isValidAppId(app_id: [:0]const u8) bool { if (app_id.len > 255 or app_id.len == 0) return false; if (app_id[0] == '.') return false; From cd287b4161633e1c45b0042aecd79c23cd77f7a6 Mon Sep 17 00:00:00 2001 From: Maciej Bartczak <39600846+maciekbartczak@users.noreply.github.com> Date: Fri, 21 Feb 2025 22:11:32 +0100 Subject: [PATCH 09/18] - remove the unused dialog context struct - set the current title in the input buffer - fix formatting --- src/apprt/gtk/Surface.zig | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/apprt/gtk/Surface.zig b/src/apprt/gtk/Surface.zig index 589e32ef1..c31c173e2 100644 --- a/src/apprt/gtk/Surface.zig +++ b/src/apprt/gtk/Surface.zig @@ -1019,11 +1019,6 @@ fn resolveTitle(self: *Surface, title: [:0]const u8) [:0]const u8 { title[zoom_title_prefix.len..]; } -const PromptTitleDialogContext = struct { - entry: *c.GtkWidget, - self: *Surface, -}; - pub fn promptTitle(self: *Surface) !void { const window = self.container.window() orelse return; @@ -1033,6 +1028,8 @@ pub fn promptTitle(self: *Surface) !void { const dialog: *adw.AlertDialog = @ptrCast(builder.getObject("prompt_title_dialog")); dialog.addResponse("cancel", "Cancel"); dialog.addResponse("ok", "OK"); + const entry: *gtk.Entry = @ptrCast(builder.getObject("title_entry")); + entry.getBuffer().setText(self.getTitle() orelse "", -1); dialog.choose(@ptrCast(window.window), null, @ptrCast(>kPromptTitleResponse), self); } From 1ee8dfc99c25898acea082a4ae56809c7b792512 Mon Sep 17 00:00:00 2001 From: Maciej Bartczak <39600846+maciekbartczak@users.noreply.github.com> Date: Sat, 22 Feb 2025 11:57:41 +0100 Subject: [PATCH 10/18] code review: - remove unnecessary @ptrCast - set the default focus to the text entry field --- src/apprt/gtk/Surface.zig | 8 ++++++-- src/apprt/gtk/ui/prompt-title-dialog.blp | 9 +++++---- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/src/apprt/gtk/Surface.zig b/src/apprt/gtk/Surface.zig index c31c173e2..5dab3ec32 100644 --- a/src/apprt/gtk/Surface.zig +++ b/src/apprt/gtk/Surface.zig @@ -7,6 +7,7 @@ const std = @import("std"); const adw = @import("adw"); const gtk = @import("gtk"); const gio = @import("gio"); +const gobject = @import("gobject"); const glib = @import("glib"); const Allocator = std.mem.Allocator; const build_config = @import("../../build_config.zig"); @@ -1028,10 +1029,12 @@ pub fn promptTitle(self: *Surface) !void { const dialog: *adw.AlertDialog = @ptrCast(builder.getObject("prompt_title_dialog")); dialog.addResponse("cancel", "Cancel"); dialog.addResponse("ok", "OK"); + dialog.setResponseAppearance("ok", adw.ResponseAppearance.suggested); + const entry: *gtk.Entry = @ptrCast(builder.getObject("title_entry")); entry.getBuffer().setText(self.getTitle() orelse "", -1); - dialog.choose(@ptrCast(window.window), null, @ptrCast(>kPromptTitleResponse), self); + dialog.choose(@ptrCast(window.window), null, >kPromptTitleResponse, self); } /// Set the current working directory of the surface. @@ -2321,7 +2324,8 @@ fn g_value_holds(value_: ?*c.GValue, g_type: c.GType) bool { return false; } -fn gtkPromptTitleResponse(dialog: *adw.AlertDialog, result: *gio.AsyncResult, ud: ?*anyopaque) callconv(.C) void { +fn gtkPromptTitleResponse(source_object: ?*gobject.Object, result: *gio.AsyncResult, ud: ?*anyopaque) callconv(.C) void { + const dialog: *adw.AlertDialog = @ptrCast(source_object.?); const self = userdataSelf(ud.?); const response = dialog.chooseFinish(result); diff --git a/src/apprt/gtk/ui/prompt-title-dialog.blp b/src/apprt/gtk/ui/prompt-title-dialog.blp index cdc7fb42c..4dd091c52 100644 --- a/src/apprt/gtk/ui/prompt-title-dialog.blp +++ b/src/apprt/gtk/ui/prompt-title-dialog.blp @@ -2,8 +2,9 @@ using Gtk 4.0; using Adw 1; Adw.AlertDialog prompt_title_dialog { - heading: "Change Terminal Title"; - body: "Leave blank to restore the default title."; + heading: "Change Terminal Title"; + body: "Leave blank to restore the default title."; + focus-widget: title_entry; - extra-child: Entry title_entry {}; -} \ No newline at end of file + extra-child: Entry title_entry {}; +} From 8758295647d479e44096a91ac50c2565f9d71139 Mon Sep 17 00:00:00 2001 From: Maciej Bartczak <39600846+maciekbartczak@users.noreply.github.com> Date: Sat, 22 Feb 2025 13:05:02 +0100 Subject: [PATCH 11/18] code review: - move responses definition to the blueprint, use translatable strings - minor changes in the response callback --- src/apprt/gtk/Surface.zig | 24 ++++++++++-------------- src/apprt/gtk/ui/prompt-title-dialog.blp | 8 ++++++-- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/src/apprt/gtk/Surface.zig b/src/apprt/gtk/Surface.zig index 5dab3ec32..00799da6f 100644 --- a/src/apprt/gtk/Surface.zig +++ b/src/apprt/gtk/Surface.zig @@ -8,7 +8,6 @@ const adw = @import("adw"); const gtk = @import("gtk"); const gio = @import("gio"); const gobject = @import("gobject"); -const glib = @import("glib"); const Allocator = std.mem.Allocator; const build_config = @import("../../build_config.zig"); const build_options = @import("build_options"); @@ -1026,14 +1025,10 @@ pub fn promptTitle(self: *Surface) !void { var builder = Builder.init("prompt-title-dialog", .blp); defer builder.deinit(); - const dialog: *adw.AlertDialog = @ptrCast(builder.getObject("prompt_title_dialog")); - dialog.addResponse("cancel", "Cancel"); - dialog.addResponse("ok", "OK"); - dialog.setResponseAppearance("ok", adw.ResponseAppearance.suggested); - const entry: *gtk.Entry = @ptrCast(builder.getObject("title_entry")); entry.getBuffer().setText(self.getTitle() orelse "", -1); + const dialog: *adw.AlertDialog = @ptrCast(builder.getObject("prompt_title_dialog")); dialog.choose(@ptrCast(window.window), null, >kPromptTitleResponse, self); } @@ -2329,14 +2324,16 @@ fn gtkPromptTitleResponse(source_object: ?*gobject.Object, result: *gio.AsyncRes const self = userdataSelf(ud.?); const response = dialog.chooseFinish(result); - if (glib.strEqual("ok", response) != 0) { - const title_entry: *gtk.Entry = @ptrCast(dialog.getExtraChild()); + if (std.mem.orderZ(u8, "ok", response) == .eq) { + const title_entry: *gtk.Entry = gobject.ext.cast(gtk.Entry, dialog.getExtraChild().?).?; const title = std.mem.span(title_entry.getBuffer().getText()); // if the new title is empty and the user has set the title previously, restore the terminal provided title - if (title.len == 0 and self.title_from_terminal != null) { + if (title.len == 0) { if (self.getTerminalTitle()) |terminal_title| { - self.setTitle(terminal_title, .user) catch {}; + self.setTitle(terminal_title, .user) catch |err| { + log.err("Failed to set title: {}", .{err}); + }; self.app.core_app.alloc.free(self.title_from_terminal.?); self.title_from_terminal = null; } @@ -2346,15 +2343,14 @@ fn gtkPromptTitleResponse(source_object: ?*gobject.Object, result: *gio.AsyncRes self.title_from_terminal = self.app.core_app.alloc.dupeZ(u8, self.title_text.?) catch |err| switch (err) { error.OutOfMemory => { log.err("Failed to allocate memory for title: {}", .{err}); - c.gtk_window_destroy(@ptrCast(dialog)); return; }, }; } - self.setTitle(title, .user) catch {}; + self.setTitle(title, .user) catch |err| { + log.err("Failed to set title: {}", .{err}); + }; } } - - c.gtk_window_destroy(@ptrCast(dialog)); } diff --git a/src/apprt/gtk/ui/prompt-title-dialog.blp b/src/apprt/gtk/ui/prompt-title-dialog.blp index 4dd091c52..3187eccc5 100644 --- a/src/apprt/gtk/ui/prompt-title-dialog.blp +++ b/src/apprt/gtk/ui/prompt-title-dialog.blp @@ -2,8 +2,12 @@ using Gtk 4.0; using Adw 1; Adw.AlertDialog prompt_title_dialog { - heading: "Change Terminal Title"; - body: "Leave blank to restore the default title."; + heading: _("Change Terminal Title"); + body: _("Leave blank to restore the default title."); + responses [ + cancel: _("Cancel"), + ok: _("OK") suggested + ] focus-widget: title_entry; extra-child: Entry title_entry {}; From 7c19dd5a3322f6caedd30e676d78439f21c3debd Mon Sep 17 00:00:00 2001 From: Maciej Bartczak <39600846+maciekbartczak@users.noreply.github.com> Date: Sat, 22 Feb 2025 18:14:46 +0100 Subject: [PATCH 12/18] format the blueprint file using `blueprint-compiler format` --- src/apprt/gtk/ui/prompt-title-dialog.blp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/apprt/gtk/ui/prompt-title-dialog.blp b/src/apprt/gtk/ui/prompt-title-dialog.blp index 3187eccc5..7cd5a2657 100644 --- a/src/apprt/gtk/ui/prompt-title-dialog.blp +++ b/src/apprt/gtk/ui/prompt-title-dialog.blp @@ -4,10 +4,12 @@ using Adw 1; Adw.AlertDialog prompt_title_dialog { heading: _("Change Terminal Title"); body: _("Leave blank to restore the default title."); + responses [ cancel: _("Cancel"), ok: _("OK") suggested ] + focus-widget: title_entry; extra-child: Entry title_entry {}; From 4f3c4037aa35914af9633cbf7d051c9d9f85b65d Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Sat, 22 Feb 2025 12:54:08 -0600 Subject: [PATCH 13/18] gtk: get 'Change Title' working with older distros --- src/apprt/gtk/Builder.zig | 2 +- src/apprt/gtk/Surface.zig | 15 ++++--- src/apprt/gtk/blueprint_compiler.zig | 57 ++++++++++++++++++++++++ src/apprt/gtk/gresource.zig | 20 ++++++--- src/apprt/gtk/ui/prompt-title-dialog.blp | 4 +- src/build/SharedDeps.zig | 24 +++++++--- src/build/docker/debian/Dockerfile | 2 + 7 files changed, 103 insertions(+), 21 deletions(-) create mode 100644 src/apprt/gtk/blueprint_compiler.zig diff --git a/src/apprt/gtk/Builder.zig b/src/apprt/gtk/Builder.zig index f9b0c226a..473abc0f7 100644 --- a/src/apprt/gtk/Builder.zig +++ b/src/apprt/gtk/Builder.zig @@ -25,7 +25,7 @@ pub fn init(comptime name: []const u8, comptime kind: enum { blp, ui }) Builder // GResource. const gresource = @import("gresource.zig"); for (gresource.blueprint_files) |blueprint_file| { - if (std.mem.eql(u8, blueprint_file, name)) break; + if (std.mem.eql(u8, blueprint_file.name, name)) break; } else @compileError("missing blueprint file '" ++ name ++ "' in gresource.zig"); }, .ui => { diff --git a/src/apprt/gtk/Surface.zig b/src/apprt/gtk/Surface.zig index 00799da6f..9836e7fbf 100644 --- a/src/apprt/gtk/Surface.zig +++ b/src/apprt/gtk/Surface.zig @@ -31,6 +31,7 @@ const inspector = @import("inspector.zig"); const gtk_key = @import("key.zig"); const c = @import("c.zig").c; const Builder = @import("Builder.zig"); +const adwaita = @import("adwaita.zig"); const log = std.log.scoped(.gtk_surface); @@ -1020,16 +1021,17 @@ fn resolveTitle(self: *Surface, title: [:0]const u8) [:0]const u8 { } pub fn promptTitle(self: *Surface) !void { + if (!adwaita.versionAtLeast(1, 5, 0)) return; const window = self.container.window() orelse return; var builder = Builder.init("prompt-title-dialog", .blp); defer builder.deinit(); - const entry: *gtk.Entry = @ptrCast(builder.getObject("title_entry")); + const entry = gobject.ext.cast(gtk.Entry, builder.getObject("title_entry").?).?; entry.getBuffer().setText(self.getTitle() orelse "", -1); - const dialog: *adw.AlertDialog = @ptrCast(builder.getObject("prompt_title_dialog")); - dialog.choose(@ptrCast(window.window), null, >kPromptTitleResponse, self); + const dialog = gobject.ext.cast(adw.AlertDialog, builder.getObject("prompt_title_dialog").?).?; + dialog.choose(@ptrCast(window.window), null, gtkPromptTitleResponse, self); } /// Set the current working directory of the surface. @@ -2320,12 +2322,13 @@ fn g_value_holds(value_: ?*c.GValue, g_type: c.GType) bool { } fn gtkPromptTitleResponse(source_object: ?*gobject.Object, result: *gio.AsyncResult, ud: ?*anyopaque) callconv(.C) void { - const dialog: *adw.AlertDialog = @ptrCast(source_object.?); - const self = userdataSelf(ud.?); + if (!adwaita.versionAtLeast(1, 5, 0)) return; + const dialog = gobject.ext.cast(adw.AlertDialog, source_object.?).?; + const self = userdataSelf(ud orelse return); const response = dialog.chooseFinish(result); if (std.mem.orderZ(u8, "ok", response) == .eq) { - const title_entry: *gtk.Entry = gobject.ext.cast(gtk.Entry, dialog.getExtraChild().?).?; + const title_entry = gobject.ext.cast(gtk.Entry, dialog.getExtraChild().?).?; const title = std.mem.span(title_entry.getBuffer().getText()); // if the new title is empty and the user has set the title previously, restore the terminal provided title diff --git a/src/apprt/gtk/blueprint_compiler.zig b/src/apprt/gtk/blueprint_compiler.zig new file mode 100644 index 000000000..f1d42c43d --- /dev/null +++ b/src/apprt/gtk/blueprint_compiler.zig @@ -0,0 +1,57 @@ +const std = @import("std"); + +pub const c = @cImport({ + @cInclude("adwaita.h"); +}); + +pub fn main() !void { + var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + const alloc = gpa.allocator(); + + var it = try std.process.argsWithAllocator(alloc); + defer it.deinit(); + + _ = it.next(); + + const major = try std.fmt.parseUnsigned(u8, it.next() orelse return error.NoMajorVersion, 10); + const minor = try std.fmt.parseUnsigned(u8, it.next() orelse return error.NoMinorVersion, 10); + const micro = try std.fmt.parseUnsigned(u8, it.next() orelse return error.NoMicroVersion, 10); + const output = it.next() orelse return error.NoOutput; + const input = it.next() orelse return error.NoInput; + + if (c.ADW_MAJOR_VERSION < major or + (c.ADW_MAJOR_VERSION == major and c.ADW_MINOR_VERSION < minor) or + (c.ADW_MAJOR_VERSION == major and c.ADW_MINOR_VERSION == minor and c.ADW_MICRO_VERSION < micro)) + { + // If the Adwaita version is too old, generate an "empty" file. + const file = try std.fs.createFileAbsolute(output, .{ + .truncate = true, + }); + try file.writeAll( + \\ + \\ + ); + defer file.close(); + + return; + } + + var compiler = std.process.Child.init( + &.{ + "blueprint-compiler", + "compile", + "--output", + output, + input, + }, + alloc, + ); + + const term = try compiler.spawnAndWait(); + switch (term) { + .Exited => |rc| { + if (rc != 0) std.posix.exit(1); + }, + else => std.posix.exit(1), + } +} diff --git a/src/apprt/gtk/gresource.zig b/src/apprt/gtk/gresource.zig index 82452bb53..83978c337 100644 --- a/src/apprt/gtk/gresource.zig +++ b/src/apprt/gtk/gresource.zig @@ -57,7 +57,17 @@ pub const ui_files = [_][]const u8{ "menu-window-titlebar_menu", "menu-surface-context_menu", }; -pub const blueprint_files = [_][]const u8{"prompt-title-dialog"}; + +pub const VersionedBlueprint = struct { + major: u16, + minor: u16, + micro: u16, + name: []const u8, +}; + +pub const blueprint_files = [_]VersionedBlueprint{ + .{ .major = 1, .minor = 5, .micro = 0, .name = "prompt-title-dialog" }, +}; pub fn main() !void { var gpa = std.heap.GeneralPurposeAllocator(.{}){}; @@ -72,9 +82,9 @@ pub fn main() !void { var it = try std.process.argsWithAllocator(alloc); defer it.deinit(); - while (it.next()) |filename| { - if (std.mem.eql(u8, std.fs.path.extension(filename), ".ui")) { - try extra_ui_files.append(try alloc.dupe(u8, filename)); + while (it.next()) |argument| { + if (std.mem.eql(u8, std.fs.path.extension(argument), ".ui")) { + try extra_ui_files.append(try alloc.dupe(u8, argument)); } } @@ -144,7 +154,7 @@ pub const dependencies = deps: { index += 1; } for (blueprint_files) |blueprint_file| { - deps[index] = std.fmt.comptimePrint("src/apprt/gtk/ui/{s}.blp", .{blueprint_file}); + deps[index] = std.fmt.comptimePrint("src/apprt/gtk/ui/{s}.blp", .{blueprint_file.name}); index += 1; } break :deps deps; diff --git a/src/apprt/gtk/ui/prompt-title-dialog.blp b/src/apprt/gtk/ui/prompt-title-dialog.blp index 7cd5a2657..ffe38c980 100644 --- a/src/apprt/gtk/ui/prompt-title-dialog.blp +++ b/src/apprt/gtk/ui/prompt-title-dialog.blp @@ -6,8 +6,8 @@ Adw.AlertDialog prompt_title_dialog { body: _("Leave blank to restore the default title."); responses [ - cancel: _("Cancel"), - ok: _("OK") suggested + cancel: _("Cancel") suggested, + ok: _("OK") destructive ] focus-widget: title_entry; diff --git a/src/build/SharedDeps.zig b/src/build/SharedDeps.zig index a90fc330a..65b2b47da 100644 --- a/src/build/SharedDeps.zig +++ b/src/build/SharedDeps.zig @@ -443,6 +443,7 @@ pub fn add( .{ "glib", "glib2" }, .{ "gtk", "gtk4" }, .{ "gdk", "gdk4" }, + .{ "adw", "adw1" }, }; inline for (gobject_imports) |import| { const name, const module = import; @@ -451,7 +452,6 @@ pub fn add( step.linkSystemLibrary2("gtk4", dynamic_link_opts); step.linkSystemLibrary2("libadwaita-1", dynamic_link_opts); - step.root_module.addImport("adw", gobject.module("adw1")); if (self.config.x11) { step.linkSystemLibrary2("X11", dynamic_link_opts); @@ -500,14 +500,24 @@ pub fn add( const generate = b.addRunArtifact(generate_gresource_xml); + const gtk_blueprint_compiler = b.addExecutable(.{ + .name = "gtk_blueprint_compiler", + .root_source_file = b.path("src/apprt/gtk/blueprint_compiler.zig"), + .target = b.host, + }); + gtk_blueprint_compiler.linkSystemLibrary2("gtk4", dynamic_link_opts); + gtk_blueprint_compiler.linkSystemLibrary2("libadwaita-1", dynamic_link_opts); + gtk_blueprint_compiler.linkLibC(); + for (gresource.blueprint_files) |blueprint_file| { - const blueprint_compiler = b.addSystemCommand(&.{ - "blueprint-compiler", - "compile", - "--output", + const blueprint_compiler = b.addRunArtifact(gtk_blueprint_compiler); + blueprint_compiler.addArgs(&.{ + b.fmt("{d}", .{blueprint_file.major}), + b.fmt("{d}", .{blueprint_file.minor}), + b.fmt("{d}", .{blueprint_file.micro}), }); - const ui_file = blueprint_compiler.addOutputFileArg(b.fmt("{s}.ui", .{blueprint_file})); - blueprint_compiler.addFileArg(b.path(b.fmt("src/apprt/gtk/ui/{s}.blp", .{blueprint_file}))); + const ui_file = blueprint_compiler.addOutputFileArg(b.fmt("{s}.ui", .{blueprint_file.name})); + blueprint_compiler.addFileArg(b.path(b.fmt("src/apprt/gtk/ui/{s}.blp", .{blueprint_file.name}))); generate.addFileArg(ui_file); } diff --git a/src/build/docker/debian/Dockerfile b/src/build/docker/debian/Dockerfile index 307fb7521..7f60ddf1d 100644 --- a/src/build/docker/debian/Dockerfile +++ b/src/build/docker/debian/Dockerfile @@ -5,9 +5,11 @@ FROM docker.io/library/debian:${DISTRO_VERSION} RUN DEBIAN_FRONTEND="noninteractive" apt-get -qq update && \ apt-get -qq -y --no-install-recommends install \ # Build Tools + blueprint-compiler \ build-essential \ libbz2-dev \ libonig-dev \ + libxml2-utils \ lintian \ lsb-release \ libxml2-utils \ From 51dc1e2e8cf4adaa9bf0e1d12b1031a8b99e631f Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Sat, 22 Feb 2025 13:05:54 -0600 Subject: [PATCH 14/18] gtk: fix typos --- src/apprt/gtk/Surface.zig | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/apprt/gtk/Surface.zig b/src/apprt/gtk/Surface.zig index 9836e7fbf..80039522b 100644 --- a/src/apprt/gtk/Surface.zig +++ b/src/apprt/gtk/Surface.zig @@ -353,9 +353,10 @@ cursor: ?*c.GdkCursor = null, /// pass it to GTK. title_text: ?[:0]const u8 = null, -/// The title of the surface as reported by the terminal. -/// If it is null, the title reported by the terminal is currently being used. -/// If the title was manually overriden by the user, this will be set to a non-null value representing the default terminal title. +/// The title of the surface as reported by the terminal. If it is null, the +/// title reported by the terminal is currently being used. If the title was +/// manually overridden by the user, this will be set to a non-null value +/// representing the default terminal title. title_from_terminal: ?[:0]const u8 = null, /// Our current working directory. We use this value for setting tooltips in @@ -967,7 +968,7 @@ pub fn setTitle(self: *Surface, slice: [:0]const u8, source: SetTitleSource) !vo }; errdefer alloc.free(copy); - // The user has overriden the title + // The user has overridden the title // We only want to update the terminal provided title so that it can be restored to the most recent state. if (self.title_from_terminal != null and source == .terminal) { alloc.free(self.title_from_terminal.?); From d4bcac0150c5d2293f6317d1009f21263bd16c72 Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Sat, 22 Feb 2025 13:25:14 -0600 Subject: [PATCH 15/18] snap: add blueprint-compiler to snap --- snap/snapcraft.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml index 9ef2f5cc4..ddd38b30d 100644 --- a/snap/snapcraft.yaml +++ b/snap/snapcraft.yaml @@ -70,6 +70,7 @@ parts: plugin: nil build-attributes: [enable-patchelf] build-packages: + - blueprint-compiler - libgtk-4-dev - libadwaita-1-dev - git From 32a62ff862486f1616a33e7498a4fda022e17f8e Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Sat, 22 Feb 2025 13:36:32 -0600 Subject: [PATCH 16/18] snap: add libxml2-utils (for xmllint) to snap --- snap/snapcraft.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml index ddd38b30d..49d452ef9 100644 --- a/snap/snapcraft.yaml +++ b/snap/snapcraft.yaml @@ -73,6 +73,7 @@ parts: - blueprint-compiler - libgtk-4-dev - libadwaita-1-dev + - libxml2-utils - git - patchelf override-build: | From 5d80db2ef8f05423437d8d0113223d13dcab4e41 Mon Sep 17 00:00:00 2001 From: Maciej Bartczak <39600846+maciekbartczak@users.noreply.github.com> Date: Sat, 22 Feb 2025 20:39:27 +0100 Subject: [PATCH 17/18] code review: fix log format --- src/apprt/gtk/Surface.zig | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/apprt/gtk/Surface.zig b/src/apprt/gtk/Surface.zig index 80039522b..15e33aee3 100644 --- a/src/apprt/gtk/Surface.zig +++ b/src/apprt/gtk/Surface.zig @@ -2336,7 +2336,7 @@ fn gtkPromptTitleResponse(source_object: ?*gobject.Object, result: *gio.AsyncRes if (title.len == 0) { if (self.getTerminalTitle()) |terminal_title| { self.setTitle(terminal_title, .user) catch |err| { - log.err("Failed to set title: {}", .{err}); + log.err("failed to set title={}", .{err}); }; self.app.core_app.alloc.free(self.title_from_terminal.?); self.title_from_terminal = null; @@ -2346,14 +2346,14 @@ fn gtkPromptTitleResponse(source_object: ?*gobject.Object, result: *gio.AsyncRes if (self.title_from_terminal == null and self.title_text != null) { self.title_from_terminal = self.app.core_app.alloc.dupeZ(u8, self.title_text.?) catch |err| switch (err) { error.OutOfMemory => { - log.err("Failed to allocate memory for title: {}", .{err}); + log.err("failed to allocate memory for title={}", .{err}); return; }, }; } self.setTitle(title, .user) catch |err| { - log.err("Failed to set title: {}", .{err}); + log.err("failed to set title={}", .{err}); }; } } From bde5b963d0117afc700c47dd00d0b5fdf1e852d0 Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Sat, 22 Feb 2025 16:38:09 -0600 Subject: [PATCH 18/18] gtk: fix Builder api changes --- src/apprt/gtk/Surface.zig | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/apprt/gtk/Surface.zig b/src/apprt/gtk/Surface.zig index 15e33aee3..2636b41aa 100644 --- a/src/apprt/gtk/Surface.zig +++ b/src/apprt/gtk/Surface.zig @@ -1028,10 +1028,10 @@ pub fn promptTitle(self: *Surface) !void { var builder = Builder.init("prompt-title-dialog", .blp); defer builder.deinit(); - const entry = gobject.ext.cast(gtk.Entry, builder.getObject("title_entry").?).?; + const entry = builder.getObject(gtk.Entry, "title_entry").?; entry.getBuffer().setText(self.getTitle() orelse "", -1); - const dialog = gobject.ext.cast(adw.AlertDialog, builder.getObject("prompt_title_dialog").?).?; + const dialog = builder.getObject(adw.AlertDialog, "prompt_title_dialog").?; dialog.choose(@ptrCast(window.window), null, gtkPromptTitleResponse, self); }