From a1ee2f07648aea90a9df3d50c0a5341f1624d741 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 23 Dec 2025 09:06:46 -0800 Subject: [PATCH 1/7] apprt/gtk: store key sequences/tables in surface state --- src/apprt/gtk.zig | 1 + src/apprt/gtk/class/application.zig | 35 +++++++- src/apprt/gtk/class/surface.zig | 72 ++++++++++++++++ src/apprt/gtk/key.zig | 122 ++++++++++++++++++++++++++++ 4 files changed, 228 insertions(+), 2 deletions(-) diff --git a/src/apprt/gtk.zig b/src/apprt/gtk.zig index 415d3773d..07b4eb0e7 100644 --- a/src/apprt/gtk.zig +++ b/src/apprt/gtk.zig @@ -10,4 +10,5 @@ pub const WeakRef = @import("gtk/weak_ref.zig").WeakRef; test { @import("std").testing.refAllDecls(@This()); _ = @import("gtk/ext.zig"); + _ = @import("gtk/key.zig"); } diff --git a/src/apprt/gtk/class/application.zig b/src/apprt/gtk/class/application.zig index 1c0863f3c..b16bce049 100644 --- a/src/apprt/gtk/class/application.zig +++ b/src/apprt/gtk/class/application.zig @@ -669,6 +669,9 @@ pub const Application = extern struct { .inspector => return Action.controlInspector(target, value), + .key_sequence => return Action.keySequence(target, value), + .key_table => return Action.keyTable(target, value), + .mouse_over_link => Action.mouseOverLink(target, value), .mouse_shape => Action.mouseShape(target, value), .mouse_visibility => Action.mouseVisibility(target, value), @@ -743,8 +746,6 @@ pub const Application = extern struct { .toggle_visibility, .toggle_background_opacity, .cell_size, - .key_sequence, - .key_table, .render_inspector, .renderer_health, .color_change, @@ -2660,6 +2661,36 @@ const Action = struct { }, } } + + pub fn keySequence(target: apprt.Target, value: apprt.Action.Value(.key_sequence)) bool { + switch (target) { + .app => { + log.warn("key_sequence action to app is unexpected", .{}); + return false; + }, + .surface => |core| { + core.rt_surface.gobj().keySequenceAction(value) catch |err| { + log.warn("error handling key_sequence action: {}", .{err}); + }; + return true; + }, + } + } + + pub fn keyTable(target: apprt.Target, value: apprt.Action.Value(.key_table)) bool { + switch (target) { + .app => { + log.warn("key_table action to app is unexpected", .{}); + return false; + }, + .surface => |core| { + core.rt_surface.gobj().keyTableAction(value) catch |err| { + log.warn("error handling key_table action: {}", .{err}); + }; + return true; + }, + } + } }; /// This sets various GTK-related environment variables as necessary diff --git a/src/apprt/gtk/class/surface.zig b/src/apprt/gtk/class/surface.zig index 93d1beeb2..c35c78302 100644 --- a/src/apprt/gtk/class/surface.zig +++ b/src/apprt/gtk/class/surface.zig @@ -617,6 +617,10 @@ pub const Surface = extern struct { vscroll_policy: gtk.ScrollablePolicy = .natural, vadj_signal_group: ?*gobject.SignalGroup = null, + // Key state tracking for key sequences and tables + key_sequence: std.ArrayListUnmanaged([:0]const u8) = .empty, + key_tables: std.ArrayListUnmanaged([:0]const u8) = .empty, + // Template binds child_exited_overlay: *ChildExited, context_menu: *gtk.PopoverMenu, @@ -778,6 +782,66 @@ pub const Surface = extern struct { if (priv.inspector) |v| v.queueRender(); } + /// Handle a key sequence action from the apprt. + pub fn keySequenceAction( + self: *Self, + value: apprt.action.KeySequence, + ) Allocator.Error!void { + const priv = self.private(); + const alloc = Application.default().allocator(); + + switch (value) { + .trigger => |trigger| { + // Convert the trigger to a human-readable label + var buf: std.Io.Writer.Allocating = .init(alloc); + defer buf.deinit(); + if (gtk_key.labelFromTrigger(&buf.writer, trigger)) |success| { + if (!success) return; + } else |_| return error.OutOfMemory; + + // Make space + try priv.key_sequence.ensureUnusedCapacity(alloc, 1); + + // Copy and append + const duped = try buf.toOwnedSliceSentinel(0); + errdefer alloc.free(duped); + priv.key_sequence.appendAssumeCapacity(duped); + }, + .end => { + // Free all the stored strings and clear + for (priv.key_sequence.items) |s| alloc.free(s); + priv.key_sequence.clearAndFree(alloc); + }, + } + } + + /// Handle a key table action from the apprt. + pub fn keyTableAction( + self: *Self, + value: apprt.action.KeyTable, + ) Allocator.Error!void { + const priv = self.private(); + const alloc = Application.default().allocator(); + + switch (value) { + .activate => |name| { + // Duplicate the name string and push onto stack + const duped = try alloc.dupeZ(u8, name); + errdefer alloc.free(duped); + try priv.key_tables.append(alloc, duped); + }, + .deactivate => { + // Pop and free the top table + if (priv.key_tables.pop()) |s| alloc.free(s); + }, + .deactivate_all => { + // Free all tables and clear + for (priv.key_tables.items) |s| alloc.free(s); + priv.key_tables.clearAndFree(alloc); + }, + } + } + pub fn showOnScreenKeyboard(self: *Self, event: ?*gdk.Event) bool { const priv = self.private(); return priv.im_context.as(gtk.IMContext).activateOsk(event) != 0; @@ -1787,6 +1851,14 @@ pub const Surface = extern struct { glib.free(@ptrCast(@constCast(v))); priv.title_override = null; } + + // Clean up key sequence and key table state + const alloc = Application.default().allocator(); + for (priv.key_sequence.items) |s| alloc.free(s); + priv.key_sequence.deinit(alloc); + for (priv.key_tables.items) |s| alloc.free(s); + priv.key_tables.deinit(alloc); + self.clearCgroup(); gobject.Object.virtual_methods.finalize.call( diff --git a/src/apprt/gtk/key.zig b/src/apprt/gtk/key.zig index 35c9390b2..5f717e14a 100644 --- a/src/apprt/gtk/key.zig +++ b/src/apprt/gtk/key.zig @@ -233,6 +233,70 @@ pub fn keyvalFromKey(key: input.Key) ?c_uint { } } +/// Converts a trigger to a human-readable label for display in UI. +/// +/// Uses GTK accelerator-style formatting (e.g., "Ctrl+Shift+A"). +/// Returns false if the trigger cannot be formatted (e.g., catch_all). +pub fn labelFromTrigger( + writer: *std.Io.Writer, + trigger: input.Binding.Trigger, +) std.Io.Writer.Error!bool { + // Modifiers first, using human-readable format + if (trigger.mods.super) try writer.writeAll("Super+"); + if (trigger.mods.ctrl) try writer.writeAll("Ctrl+"); + if (trigger.mods.alt) try writer.writeAll("Alt+"); + if (trigger.mods.shift) try writer.writeAll("Shift+"); + + // Write the key + return writeTriggerKeyLabel(writer, trigger); +} + +/// Writes the key portion of a trigger in human-readable format. +fn writeTriggerKeyLabel( + writer: *std.Io.Writer, + trigger: input.Binding.Trigger, +) error{WriteFailed}!bool { + switch (trigger.key) { + .physical => |k| { + const keyval = keyvalFromKey(k) orelse return false; + const name = gdk.keyvalName(keyval) orelse return false; + // Capitalize the first letter for nicer display + const span = std.mem.span(name); + if (span.len > 0) { + if (span[0] >= 'a' and span[0] <= 'z') { + try writer.writeByte(span[0] - 'a' + 'A'); + if (span.len > 1) try writer.writeAll(span[1..]); + } else { + try writer.writeAll(span); + } + } + }, + + .unicode => |cp| { + // Try to get a nice name from GDK first + if (gdk.keyvalName(cp)) |name| { + const span = std.mem.span(name); + if (span.len > 0) { + // Capitalize the first letter for nicer display + if (span[0] >= 'a' and span[0] <= 'z') { + try writer.writeByte(span[0] - 'a' + 'A'); + if (span.len > 1) try writer.writeAll(span[1..]); + } else { + try writer.writeAll(span); + } + } + } else { + // Fall back to printing the character + try writer.print("{u}", .{cp}); + } + }, + + .catch_all => return false, + } + + return true; +} + test "accelFromTrigger" { const testing = std.testing; var buf: [256]u8 = undefined; @@ -263,6 +327,64 @@ test "xdgShortcutFromTrigger" { })).?); } +test "labelFromTrigger" { + const testing = std.testing; + + // Simple unicode key with modifier + { + var buf: std.Io.Writer.Allocating = .init(testing.allocator); + defer buf.deinit(); + try testing.expect(try labelFromTrigger(&buf.writer, .{ + .mods = .{ .super = true }, + .key = .{ .unicode = 'q' }, + })); + try testing.expectEqualStrings("Super+Q", buf.written()); + } + + // Multiple modifiers + { + var buf: std.Io.Writer.Allocating = .init(testing.allocator); + defer buf.deinit(); + try testing.expect(try labelFromTrigger(&buf.writer, .{ + .mods = .{ .ctrl = true, .alt = true, .super = true, .shift = true }, + .key = .{ .unicode = 92 }, + })); + try testing.expectEqualStrings("Super+Ctrl+Alt+Shift+Backslash", buf.written()); + } + + // Physical key + { + var buf: std.Io.Writer.Allocating = .init(testing.allocator); + defer buf.deinit(); + try testing.expect(try labelFromTrigger(&buf.writer, .{ + .mods = .{ .ctrl = true }, + .key = .{ .physical = .key_a }, + })); + try testing.expectEqualStrings("Ctrl+A", buf.written()); + } + + // No modifiers + { + var buf: std.Io.Writer.Allocating = .init(testing.allocator); + defer buf.deinit(); + try testing.expect(try labelFromTrigger(&buf.writer, .{ + .mods = .{}, + .key = .{ .physical = .escape }, + })); + try testing.expectEqualStrings("Escape", buf.written()); + } + + // catch_all returns false + { + var buf: std.Io.Writer.Allocating = .init(testing.allocator); + defer buf.deinit(); + try testing.expect(!try labelFromTrigger(&buf.writer, .{ + .mods = .{}, + .key = .catch_all, + })); + } +} + /// A raw entry in the keymap. Our keymap contains mappings between /// GDK keys and our own key enum. const RawEntry = struct { c_uint, input.Key }; From 1562967d5103355916b4065123caf89608b70ae4 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 23 Dec 2025 09:35:51 -0800 Subject: [PATCH 2/7] apprt/gtk: key state overlay --- src/apprt/gtk/build/gresource.zig | 1 + src/apprt/gtk/class/key_state_overlay.zig | 290 +++++++++++++++++++++ src/apprt/gtk/class/surface.zig | 6 + src/apprt/gtk/css/style.css | 12 + src/apprt/gtk/ui/1.2/key-state-overlay.blp | 58 +++++ src/apprt/gtk/ui/1.2/surface.blp | 3 + 6 files changed, 370 insertions(+) create mode 100644 src/apprt/gtk/class/key_state_overlay.zig create mode 100644 src/apprt/gtk/ui/1.2/key-state-overlay.blp diff --git a/src/apprt/gtk/build/gresource.zig b/src/apprt/gtk/build/gresource.zig index c77579aab..d3684c171 100644 --- a/src/apprt/gtk/build/gresource.zig +++ b/src/apprt/gtk/build/gresource.zig @@ -44,6 +44,7 @@ pub const blueprints: []const Blueprint = &.{ .{ .major = 1, .minor = 5, .name = "inspector-window" }, .{ .major = 1, .minor = 2, .name = "resize-overlay" }, .{ .major = 1, .minor = 2, .name = "search-overlay" }, + .{ .major = 1, .minor = 2, .name = "key-state-overlay" }, .{ .major = 1, .minor = 5, .name = "split-tree" }, .{ .major = 1, .minor = 5, .name = "split-tree-split" }, .{ .major = 1, .minor = 2, .name = "surface" }, diff --git a/src/apprt/gtk/class/key_state_overlay.zig b/src/apprt/gtk/class/key_state_overlay.zig new file mode 100644 index 000000000..15dc0d502 --- /dev/null +++ b/src/apprt/gtk/class/key_state_overlay.zig @@ -0,0 +1,290 @@ +const std = @import("std"); +const adw = @import("adw"); +const glib = @import("glib"); +const gobject = @import("gobject"); +const gdk = @import("gdk"); +const gtk = @import("gtk"); + +const gresource = @import("../build/gresource.zig"); +const Common = @import("../class.zig").Common; + +const log = std.log.scoped(.gtk_ghostty_key_state_overlay); + +/// An overlay that displays the current key table stack and pending key sequence. +/// This helps users understand what key bindings are active and what keys they've +/// pressed in a multi-key sequence. +pub const KeyStateOverlay = extern struct { + const Self = @This(); + parent_instance: Parent, + pub const Parent = adw.Bin; + pub const getGObjectType = gobject.ext.defineClass(Self, .{ + .name = "GhosttyKeyStateOverlay", + .instanceInit = &init, + .classInit = &Class.init, + .parent_class = &Class.parent, + .private = .{ .Type = Private, .offset = &Private.offset }, + }); + + pub const properties = struct { + pub const active = struct { + pub const name = "active"; + const impl = gobject.ext.defineProperty( + name, + Self, + bool, + .{ + .default = false, + .accessor = C.privateShallowFieldAccessor("active"), + }, + ); + }; + + pub const @"tables-text" = struct { + pub const name = "tables-text"; + const impl = gobject.ext.defineProperty( + name, + Self, + ?[:0]const u8, + .{ + .default = null, + .accessor = C.privateStringFieldAccessor("tables_text"), + }, + ); + }; + + pub const @"has-tables" = struct { + pub const name = "has-tables"; + const impl = gobject.ext.defineProperty( + name, + Self, + bool, + .{ + .default = false, + .accessor = gobject.ext.typedAccessor( + Self, + bool, + .{ .getter = getHasTables }, + ), + }, + ); + }; + + pub const @"sequence-text" = struct { + pub const name = "sequence-text"; + const impl = gobject.ext.defineProperty( + name, + Self, + ?[:0]const u8, + .{ + .default = null, + .accessor = C.privateStringFieldAccessor("sequence_text"), + }, + ); + }; + + pub const @"has-sequence" = struct { + pub const name = "has-sequence"; + const impl = gobject.ext.defineProperty( + name, + Self, + bool, + .{ + .default = false, + .accessor = gobject.ext.typedAccessor( + Self, + bool, + .{ .getter = getHasSequence }, + ), + }, + ); + }; + + pub const pending = struct { + pub const name = "pending"; + const impl = gobject.ext.defineProperty( + name, + Self, + bool, + .{ + .default = false, + .accessor = C.privateShallowFieldAccessor("pending"), + }, + ); + }; + + pub const @"valign-target" = struct { + pub const name = "valign-target"; + const impl = gobject.ext.defineProperty( + name, + Self, + gtk.Align, + .{ + .default = .end, + .accessor = C.privateShallowFieldAccessor("valign_target"), + }, + ); + }; + }; + + const Private = struct { + /// Whether the overlay is active/visible. + active: bool = false, + + /// The formatted key table stack text (e.g., "default › vim"). + tables_text: ?[:0]const u8 = null, + + /// The formatted key sequence text (e.g., "Ctrl+A B"). + sequence_text: ?[:0]const u8 = null, + + /// Whether we're waiting for more keys in a sequence. + pending: bool = false, + + /// Target vertical alignment for the overlay. + valign_target: gtk.Align = .end, + + pub var offset: c_int = 0; + }; + + fn init(self: *Self, _: *Class) callconv(.c) void { + gtk.Widget.initTemplate(self.as(gtk.Widget)); + + // Set dummy data for UI iteration + const priv = self.private(); + priv.active = true; + priv.tables_text = glib.ext.dupeZ(u8, "default › vim"); + priv.sequence_text = glib.ext.dupeZ(u8, "Ctrl+A"); + priv.pending = true; + + // Notify property changes so bindings update + const obj = self.as(gobject.Object); + obj.notifyByPspec(properties.active.impl.param_spec); + obj.notifyByPspec(properties.@"tables-text".impl.param_spec); + obj.notifyByPspec(properties.@"has-tables".impl.param_spec); + obj.notifyByPspec(properties.@"sequence-text".impl.param_spec); + obj.notifyByPspec(properties.@"has-sequence".impl.param_spec); + obj.notifyByPspec(properties.pending.impl.param_spec); + } + + fn getHasTables(self: *Self) bool { + return self.private().tables_text != null; + } + + fn getHasSequence(self: *Self) bool { + return self.private().sequence_text != null; + } + + fn closureShowChevron( + _: *Self, + has_tables: bool, + has_sequence: bool, + ) callconv(.c) c_int { + return if (has_tables and has_sequence) 1 else 0; + } + + //--------------------------------------------------------------- + // Template callbacks + + fn onDragEnd( + _: *gtk.GestureDrag, + _: f64, + offset_y: f64, + self: *Self, + ) callconv(.c) void { + // Key state overlay only moves between top-center and bottom-center. + // Horizontal alignment is always center. + const priv = self.private(); + const widget = self.as(gtk.Widget); + const parent = widget.getParent() orelse return; + + const parent_height: f64 = @floatFromInt(parent.getAllocatedHeight()); + const self_height: f64 = @floatFromInt(widget.getAllocatedHeight()); + + const self_y: f64 = if (priv.valign_target == .start) 0 else parent_height - self_height; + const new_y = self_y + offset_y + (self_height / 2); + + const new_valign: gtk.Align = if (new_y > parent_height / 2) .end else .start; + + if (new_valign != priv.valign_target) { + priv.valign_target = new_valign; + self.as(gobject.Object).notifyByPspec(properties.@"valign-target".impl.param_spec); + self.as(gtk.Widget).queueResize(); + } + } + + //--------------------------------------------------------------- + // Virtual methods + + fn dispose(self: *Self) callconv(.c) void { + gtk.Widget.disposeTemplate( + self.as(gtk.Widget), + getGObjectType(), + ); + + gobject.Object.virtual_methods.dispose.call( + Class.parent, + self.as(Parent), + ); + } + + fn finalize(self: *Self) callconv(.c) void { + const priv = self.private(); + + if (priv.tables_text) |v| { + glib.free(@ptrCast(@constCast(v))); + } + if (priv.sequence_text) |v| { + glib.free(@ptrCast(@constCast(v))); + } + + gobject.Object.virtual_methods.finalize.call( + Class.parent, + self.as(Parent), + ); + } + + const C = Common(Self, Private); + pub const as = C.as; + pub const ref = C.ref; + pub const unref = C.unref; + const private = C.private; + + pub const Class = extern struct { + parent_class: Parent.Class, + var parent: *Parent.Class = undefined; + pub const Instance = Self; + + fn init(class: *Class) callconv(.c) void { + gtk.Widget.Class.setTemplateFromResource( + class.as(gtk.Widget.Class), + comptime gresource.blueprint(.{ + .major = 1, + .minor = 2, + .name = "key-state-overlay", + }), + ); + + // Template Callbacks + class.bindTemplateCallback("on_drag_end", &onDragEnd); + class.bindTemplateCallback("show_chevron", &closureShowChevron); + + // Properties + gobject.ext.registerProperties(class, &.{ + properties.active.impl, + properties.@"tables-text".impl, + properties.@"has-tables".impl, + properties.@"sequence-text".impl, + properties.@"has-sequence".impl, + properties.pending.impl, + properties.@"valign-target".impl, + }); + + // Virtual methods + gobject.Object.virtual_methods.dispose.implement(class, &dispose); + gobject.Object.virtual_methods.finalize.implement(class, &finalize); + } + + pub const as = C.Class.as; + pub const bindTemplateChildPrivate = C.Class.bindTemplateChildPrivate; + pub const bindTemplateCallback = C.Class.bindTemplateCallback; + }; +}; diff --git a/src/apprt/gtk/class/surface.zig b/src/apprt/gtk/class/surface.zig index c35c78302..50d7f3dc2 100644 --- a/src/apprt/gtk/class/surface.zig +++ b/src/apprt/gtk/class/surface.zig @@ -26,6 +26,7 @@ const Application = @import("application.zig").Application; const Config = @import("config.zig").Config; const ResizeOverlay = @import("resize_overlay.zig").ResizeOverlay; const SearchOverlay = @import("search_overlay.zig").SearchOverlay; +const KeyStateOverlay = @import("key_state_overlay.zig").KeyStateOverlay; const ChildExited = @import("surface_child_exited.zig").SurfaceChildExited; const ClipboardConfirmationDialog = @import("clipboard_confirmation_dialog.zig").ClipboardConfirmationDialog; const TitleDialog = @import("surface_title_dialog.zig").SurfaceTitleDialog; @@ -553,6 +554,9 @@ pub const Surface = extern struct { /// The search overlay search_overlay: *SearchOverlay, + /// The key state overlay + key_state_overlay: *KeyStateOverlay, + /// The apprt Surface. rt_surface: ApprtSurface = undefined, @@ -3308,6 +3312,7 @@ pub const Surface = extern struct { fn init(class: *Class) callconv(.c) void { gobject.ext.ensureType(ResizeOverlay); gobject.ext.ensureType(SearchOverlay); + gobject.ext.ensureType(KeyStateOverlay); gobject.ext.ensureType(ChildExited); gtk.Widget.Class.setTemplateFromResource( class.as(gtk.Widget.Class), @@ -3328,6 +3333,7 @@ pub const Surface = extern struct { class.bindTemplateChildPrivate("progress_bar_overlay", .{}); class.bindTemplateChildPrivate("resize_overlay", .{}); class.bindTemplateChildPrivate("search_overlay", .{}); + class.bindTemplateChildPrivate("key_state_overlay", .{}); class.bindTemplateChildPrivate("terminal_page", .{}); class.bindTemplateChildPrivate("drop_target", .{}); class.bindTemplateChildPrivate("im_context", .{}); diff --git a/src/apprt/gtk/css/style.css b/src/apprt/gtk/css/style.css index 938d23ad8..f5491b7de 100644 --- a/src/apprt/gtk/css/style.css +++ b/src/apprt/gtk/css/style.css @@ -46,6 +46,18 @@ label.url-overlay.right { outline-width: 1px; } +/* + * GhosttySurface key state overlay + */ +.key-state-overlay { + padding: 6px 10px; + margin: 8px; + border-radius: 8px; + outline-style: solid; + outline-color: #555555; + outline-width: 1px; +} + /* * GhosttySurface resize overlay */ diff --git a/src/apprt/gtk/ui/1.2/key-state-overlay.blp b/src/apprt/gtk/ui/1.2/key-state-overlay.blp new file mode 100644 index 000000000..504d2e26e --- /dev/null +++ b/src/apprt/gtk/ui/1.2/key-state-overlay.blp @@ -0,0 +1,58 @@ +using Gtk 4.0; +using Adw 1; + +template $GhosttyKeyStateOverlay: Adw.Bin { + visible: bind template.active; + valign-target: end; + halign: center; + valign: bind template.valign-target; + + GestureDrag { + button: 1; + propagation-phase: capture; + drag-end => $on_drag_end(); + } + + Adw.Bin { + Box container { + styles [ + "background", + "key-state-overlay", + ] + + orientation: horizontal; + spacing: 6; + + Image { + icon-name: "input-keyboard-symbolic"; + pixel-size: 16; + } + + Label tables_label { + visible: bind template.has-tables; + label: bind template.tables-text; + xalign: 0.0; + } + + Label chevron_label { + visible: bind $show_chevron(template.has-tables, template.has-sequence) as ; + label: "›"; + + styles [ + "dim-label", + ] + } + + Label sequence_label { + visible: bind template.has-sequence; + label: bind template.sequence-text; + xalign: 0.0; + } + + Spinner pending_spinner { + visible: bind template.pending; + spinning: bind template.pending; + } + } + } +} diff --git a/src/apprt/gtk/ui/1.2/surface.blp b/src/apprt/gtk/ui/1.2/surface.blp index 4ebfeabfb..e9db4208e 100644 --- a/src/apprt/gtk/ui/1.2/surface.blp +++ b/src/apprt/gtk/ui/1.2/surface.blp @@ -155,6 +155,9 @@ Overlay terminal_page { previous-match => $search_previous_match(); } + [overlay] + $GhosttyKeyStateOverlay key_state_overlay {} + [overlay] // Apply unfocused-split-fill and unfocused-split-opacity to current surface // this is only applied when a tab has more than one surface From 85ce7d0b04f258cb53068aa753e80d26dad15865 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 23 Dec 2025 10:10:44 -0800 Subject: [PATCH 3/7] apprt/gtk: write StringList for boxed type of strings --- src/apprt/gtk/ext.zig | 3 + src/apprt/gtk/ext/slice.zig | 106 ++++++++++++++++++++++++++++++++++++ 2 files changed, 109 insertions(+) create mode 100644 src/apprt/gtk/ext/slice.zig diff --git a/src/apprt/gtk/ext.zig b/src/apprt/gtk/ext.zig index 9b1eeecc6..df9ab4ea2 100644 --- a/src/apprt/gtk/ext.zig +++ b/src/apprt/gtk/ext.zig @@ -12,6 +12,8 @@ const gobject = @import("gobject"); const gtk = @import("gtk"); pub const actions = @import("ext/actions.zig"); +const slice = @import("ext/slice.zig"); +pub const StringList = slice.StringList; /// Wrapper around `gobject.boxedCopy` to copy a boxed type `T`. pub fn boxedCopy(comptime T: type, ptr: *const T) *T { @@ -64,4 +66,5 @@ pub fn gValueHolds(value_: ?*const gobject.Value, g_type: gobject.Type) bool { test { _ = actions; + _ = slice; } diff --git a/src/apprt/gtk/ext/slice.zig b/src/apprt/gtk/ext/slice.zig new file mode 100644 index 000000000..a746d8045 --- /dev/null +++ b/src/apprt/gtk/ext/slice.zig @@ -0,0 +1,106 @@ +const std = @import("std"); +const Allocator = std.mem.Allocator; +const ArenaAllocator = std.heap.ArenaAllocator; +const glib = @import("glib"); +const gobject = @import("gobject"); + +/// A boxed type that holds a list of string slices. +pub const StringList = struct { + arena: ArenaAllocator, + strings: []const [:0]const u8, + + pub fn create( + alloc: Allocator, + strings: []const [:0]const u8, + ) Allocator.Error!*StringList { + var arena: ArenaAllocator = .init(alloc); + errdefer arena.deinit(); + const arena_alloc = arena.allocator(); + var stored = try arena_alloc.alloc([:0]const u8, strings.len); + for (strings, 0..) |s, i| stored[i] = try arena_alloc.dupeZ(u8, s); + + const ptr = try alloc.create(StringList); + errdefer alloc.destroy(ptr); + ptr.* = .{ .arena = arena, .strings = stored }; + + return ptr; + } + + pub fn deinit(self: *StringList) void { + self.arena.deinit(); + } + + pub fn destroy(self: *StringList) void { + const alloc = self.arena.child_allocator; + self.deinit(); + alloc.destroy(self); + } + + pub const getGObjectType = gobject.ext.defineBoxed( + StringList, + .{ + .name = "GhosttyStringList", + .funcs = .{ + .copy = &struct { + fn copy(self: *StringList) callconv(.c) *StringList { + return StringList.create( + self.arena.child_allocator, + self.strings, + ) catch @panic("OOM"); + } + }.copy, + .free = &struct { + fn free(self: *StringList) callconv(.c) void { + self.destroy(); + } + }.free, + }, + }, + ); +}; + +test "StringList create and destroy" { + const testing = std.testing; + const alloc = testing.allocator; + + const input: []const [:0]const u8 = &.{ "hello", "world" }; + const list = try StringList.create(alloc, input); + defer list.destroy(); + + try testing.expectEqual(@as(usize, 2), list.strings.len); + try testing.expectEqualStrings("hello", list.strings[0]); + try testing.expectEqualStrings("world", list.strings[1]); +} + +test "StringList create empty list" { + const testing = std.testing; + const alloc = testing.allocator; + + const input: []const [:0]const u8 = &.{}; + const list = try StringList.create(alloc, input); + defer list.destroy(); + + try testing.expectEqual(@as(usize, 0), list.strings.len); +} + +test "StringList boxedCopy and boxedFree" { + const testing = std.testing; + const alloc = testing.allocator; + + const input: []const [:0]const u8 = &.{ "foo", "bar", "baz" }; + const original = try StringList.create(alloc, input); + defer original.destroy(); + + const copied: *StringList = @ptrCast(@alignCast(gobject.boxedCopy( + StringList.getGObjectType(), + original, + ))); + defer gobject.boxedFree(StringList.getGObjectType(), copied); + + try testing.expectEqual(@as(usize, 3), copied.strings.len); + try testing.expectEqualStrings("foo", copied.strings[0]); + try testing.expectEqualStrings("bar", copied.strings[1]); + try testing.expectEqualStrings("baz", copied.strings[2]); + + try testing.expect(original.strings.ptr != copied.strings.ptr); +} From 481490bd1176760d154b4fa6584604d5de231757 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 23 Dec 2025 10:25:04 -0800 Subject: [PATCH 4/7] apprt/gtk: add getters for key-sequence and key-table --- src/apprt/gtk/class/surface.zig | 54 +++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/src/apprt/gtk/class/surface.zig b/src/apprt/gtk/class/surface.zig index 50d7f3dc2..556e5e2ec 100644 --- a/src/apprt/gtk/class/surface.zig +++ b/src/apprt/gtk/class/surface.zig @@ -361,6 +361,44 @@ pub const Surface = extern struct { }, ); }; + + pub const @"key-sequence" = struct { + pub const name = "key-sequence"; + const impl = gobject.ext.defineProperty( + name, + Self, + ?*ext.StringList, + .{ + .accessor = gobject.ext.typedAccessor( + Self, + ?*ext.StringList, + .{ + .getter = getKeySequence, + .getter_transfer = .full, + }, + ), + }, + ); + }; + + pub const @"key-table" = struct { + pub const name = "key-table"; + const impl = gobject.ext.defineProperty( + name, + Self, + ?*ext.StringList, + .{ + .accessor = gobject.ext.typedAccessor( + Self, + ?*ext.StringList, + .{ + .getter = getKeyTable, + .getter_transfer = .full, + }, + ), + }, + ); + }; }; pub const signals = struct { @@ -1949,6 +1987,20 @@ pub const Surface = extern struct { self.as(gobject.Object).notifyByPspec(properties.@"default-size".impl.param_spec); } + /// Get the key sequence list. Full transfer. + fn getKeySequence(self: *Self) ?*ext.StringList { + const priv = self.private(); + const alloc = Application.default().allocator(); + return ext.StringList.create(alloc, priv.key_sequence.items) catch null; + } + + /// Get the key table list. Full transfer. + fn getKeyTable(self: *Self) ?*ext.StringList { + const priv = self.private(); + const alloc = Application.default().allocator(); + return ext.StringList.create(alloc, priv.key_tables.items) catch null; + } + /// Return the min size, if set. pub fn getMinSize(self: *Self) ?*Size { const priv = self.private(); @@ -3385,6 +3437,8 @@ pub const Surface = extern struct { properties.@"error".impl, properties.@"font-size-request".impl, properties.focused.impl, + properties.@"key-sequence".impl, + properties.@"key-table".impl, properties.@"min-size".impl, properties.@"mouse-shape".impl, properties.@"mouse-hidden".impl, From 7ca3f41f6f25e13e5da065700eaad0c5798a9ddb Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 23 Dec 2025 10:31:53 -0800 Subject: [PATCH 5/7] apprt/gtk: key state overlay take bindings from surface --- src/apprt/gtk/class/key_state_overlay.zig | 111 ++++++++++++++-------- src/apprt/gtk/ext/slice.zig | 5 + src/apprt/gtk/ui/1.2/surface.blp | 5 +- 3 files changed, 83 insertions(+), 38 deletions(-) diff --git a/src/apprt/gtk/class/key_state_overlay.zig b/src/apprt/gtk/class/key_state_overlay.zig index 15dc0d502..20c0a8ab8 100644 --- a/src/apprt/gtk/class/key_state_overlay.zig +++ b/src/apprt/gtk/class/key_state_overlay.zig @@ -1,10 +1,9 @@ const std = @import("std"); const adw = @import("adw"); -const glib = @import("glib"); const gobject = @import("gobject"); -const gdk = @import("gdk"); const gtk = @import("gtk"); +const ext = @import("../ext.zig"); const gresource = @import("../build/gresource.zig"); const Common = @import("../class.zig").Common; @@ -39,15 +38,23 @@ pub const KeyStateOverlay = extern struct { ); }; - pub const @"tables-text" = struct { - pub const name = "tables-text"; + pub const tables = struct { + pub const name = "tables"; const impl = gobject.ext.defineProperty( name, Self, - ?[:0]const u8, + ?*ext.StringList, .{ - .default = null, - .accessor = C.privateStringFieldAccessor("tables_text"), + .accessor = gobject.ext.typedAccessor( + Self, + ?*ext.StringList, + .{ + .getter = getTables, + .getter_transfer = .full, + .setter = setTables, + .setter_transfer = .full, + }, + ), }, ); }; @@ -69,15 +76,23 @@ pub const KeyStateOverlay = extern struct { ); }; - pub const @"sequence-text" = struct { - pub const name = "sequence-text"; + pub const sequence = struct { + pub const name = "sequence"; const impl = gobject.ext.defineProperty( name, Self, - ?[:0]const u8, + ?*ext.StringList, .{ - .default = null, - .accessor = C.privateStringFieldAccessor("sequence_text"), + .accessor = gobject.ext.typedAccessor( + Self, + ?*ext.StringList, + .{ + .getter = getSequence, + .getter_transfer = .full, + .setter = setSequence, + .setter_transfer = .full, + }, + ), }, ); }; @@ -130,11 +145,11 @@ pub const KeyStateOverlay = extern struct { /// Whether the overlay is active/visible. active: bool = false, - /// The formatted key table stack text (e.g., "default › vim"). - tables_text: ?[:0]const u8 = null, + /// The key table stack. + tables: ?*ext.StringList = null, - /// The formatted key sequence text (e.g., "Ctrl+A B"). - sequence_text: ?[:0]const u8 = null, + /// The key sequence. + sequence: ?*ext.StringList = null, /// Whether we're waiting for more keys in a sequence. pending: bool = false, @@ -147,30 +162,52 @@ pub const KeyStateOverlay = extern struct { fn init(self: *Self, _: *Class) callconv(.c) void { gtk.Widget.initTemplate(self.as(gtk.Widget)); + } - // Set dummy data for UI iteration + fn getTables(self: *Self) ?*ext.StringList { const priv = self.private(); - priv.active = true; - priv.tables_text = glib.ext.dupeZ(u8, "default › vim"); - priv.sequence_text = glib.ext.dupeZ(u8, "Ctrl+A"); - priv.pending = true; + if (priv.tables) |tables| { + return ext.StringList.create(tables.allocator(), tables.strings) catch null; + } + return null; + } - // Notify property changes so bindings update - const obj = self.as(gobject.Object); - obj.notifyByPspec(properties.active.impl.param_spec); - obj.notifyByPspec(properties.@"tables-text".impl.param_spec); - obj.notifyByPspec(properties.@"has-tables".impl.param_spec); - obj.notifyByPspec(properties.@"sequence-text".impl.param_spec); - obj.notifyByPspec(properties.@"has-sequence".impl.param_spec); - obj.notifyByPspec(properties.pending.impl.param_spec); + fn getSequence(self: *Self) ?*ext.StringList { + const priv = self.private(); + if (priv.sequence) |sequence| { + return ext.StringList.create(sequence.allocator(), sequence.strings) catch null; + } + return null; + } + + fn setTables(self: *Self, value: ?*ext.StringList) void { + const priv = self.private(); + if (priv.tables) |old| { + old.destroy(); + priv.tables = null; + } + + priv.tables = value; + self.as(gobject.Object).notifyByPspec(properties.@"has-tables".impl.param_spec); + } + + fn setSequence(self: *Self, value: ?*ext.StringList) void { + const priv = self.private(); + if (priv.sequence) |old| { + old.destroy(); + priv.sequence = null; + } + + priv.sequence = value; + self.as(gobject.Object).notifyByPspec(properties.@"has-sequence".impl.param_spec); } fn getHasTables(self: *Self) bool { - return self.private().tables_text != null; + return self.private().tables != null; } fn getHasSequence(self: *Self) bool { - return self.private().sequence_text != null; + return self.private().sequence != null; } fn closureShowChevron( @@ -229,11 +266,11 @@ pub const KeyStateOverlay = extern struct { fn finalize(self: *Self) callconv(.c) void { const priv = self.private(); - if (priv.tables_text) |v| { - glib.free(@ptrCast(@constCast(v))); + if (priv.tables) |v| { + v.destroy(); } - if (priv.sequence_text) |v| { - glib.free(@ptrCast(@constCast(v))); + if (priv.sequence) |v| { + v.destroy(); } gobject.Object.virtual_methods.finalize.call( @@ -270,9 +307,9 @@ pub const KeyStateOverlay = extern struct { // Properties gobject.ext.registerProperties(class, &.{ properties.active.impl, - properties.@"tables-text".impl, + properties.tables.impl, properties.@"has-tables".impl, - properties.@"sequence-text".impl, + properties.sequence.impl, properties.@"has-sequence".impl, properties.pending.impl, properties.@"valign-target".impl, diff --git a/src/apprt/gtk/ext/slice.zig b/src/apprt/gtk/ext/slice.zig index a746d8045..49ad63d85 100644 --- a/src/apprt/gtk/ext/slice.zig +++ b/src/apprt/gtk/ext/slice.zig @@ -36,6 +36,11 @@ pub const StringList = struct { alloc.destroy(self); } + /// Returns the general-purpose allocator used by this StringList. + pub fn allocator(self: *const StringList) Allocator { + return self.arena.child_allocator; + } + pub const getGObjectType = gobject.ext.defineBoxed( StringList, .{ diff --git a/src/apprt/gtk/ui/1.2/surface.blp b/src/apprt/gtk/ui/1.2/surface.blp index e9db4208e..a594ba98f 100644 --- a/src/apprt/gtk/ui/1.2/surface.blp +++ b/src/apprt/gtk/ui/1.2/surface.blp @@ -156,7 +156,10 @@ Overlay terminal_page { } [overlay] - $GhosttyKeyStateOverlay key_state_overlay {} + $GhosttyKeyStateOverlay key_state_overlay { + tables: bind template.key-table; + sequence: bind template.key-sequence; + } [overlay] // Apply unfocused-split-fill and unfocused-split-opacity to current surface From 71d5ae5a51b96b4fc841e6cc07b2ccfc79489987 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 23 Dec 2025 10:43:40 -0800 Subject: [PATCH 6/7] apprt/gtk: key state overlay text is dynamic --- src/apprt/gtk/class/key_state_overlay.zig | 115 ++++++++++++--------- src/apprt/gtk/class/surface.zig | 8 ++ src/apprt/gtk/ui/1.2/key-state-overlay.blp | 10 +- 3 files changed, 78 insertions(+), 55 deletions(-) diff --git a/src/apprt/gtk/class/key_state_overlay.zig b/src/apprt/gtk/class/key_state_overlay.zig index 20c0a8ab8..7aca8f01d 100644 --- a/src/apprt/gtk/class/key_state_overlay.zig +++ b/src/apprt/gtk/class/key_state_overlay.zig @@ -1,10 +1,12 @@ const std = @import("std"); const adw = @import("adw"); +const glib = @import("glib"); const gobject = @import("gobject"); const gtk = @import("gtk"); const ext = @import("../ext.zig"); const gresource = @import("../build/gresource.zig"); +const Application = @import("application.zig").Application; const Common = @import("../class.zig").Common; const log = std.log.scoped(.gtk_ghostty_key_state_overlay); @@ -25,19 +27,6 @@ pub const KeyStateOverlay = extern struct { }); pub const properties = struct { - pub const active = struct { - pub const name = "active"; - const impl = gobject.ext.defineProperty( - name, - Self, - bool, - .{ - .default = false, - .accessor = C.privateShallowFieldAccessor("active"), - }, - ); - }; - pub const tables = struct { pub const name = "tables"; const impl = gobject.ext.defineProperty( @@ -50,7 +39,7 @@ pub const KeyStateOverlay = extern struct { ?*ext.StringList, .{ .getter = getTables, - .getter_transfer = .full, + .getter_transfer = .none, .setter = setTables, .setter_transfer = .full, }, @@ -88,7 +77,7 @@ pub const KeyStateOverlay = extern struct { ?*ext.StringList, .{ .getter = getSequence, - .getter_transfer = .full, + .getter_transfer = .none, .setter = setSequence, .setter_transfer = .full, }, @@ -114,19 +103,6 @@ pub const KeyStateOverlay = extern struct { ); }; - pub const pending = struct { - pub const name = "pending"; - const impl = gobject.ext.defineProperty( - name, - Self, - bool, - .{ - .default = false, - .accessor = C.privateShallowFieldAccessor("pending"), - }, - ); - }; - pub const @"valign-target" = struct { pub const name = "valign-target"; const impl = gobject.ext.defineProperty( @@ -142,18 +118,12 @@ pub const KeyStateOverlay = extern struct { }; const Private = struct { - /// Whether the overlay is active/visible. - active: bool = false, - /// The key table stack. tables: ?*ext.StringList = null, /// The key sequence. sequence: ?*ext.StringList = null, - /// Whether we're waiting for more keys in a sequence. - pending: bool = false, - /// Target vertical alignment for the overlay. valign_target: gtk.Align = .end, @@ -165,19 +135,11 @@ pub const KeyStateOverlay = extern struct { } fn getTables(self: *Self) ?*ext.StringList { - const priv = self.private(); - if (priv.tables) |tables| { - return ext.StringList.create(tables.allocator(), tables.strings) catch null; - } - return null; + return self.private().tables; } fn getSequence(self: *Self) ?*ext.StringList { - const priv = self.private(); - if (priv.sequence) |sequence| { - return ext.StringList.create(sequence.allocator(), sequence.strings) catch null; - } - return null; + return self.private().sequence; } fn setTables(self: *Self, value: ?*ext.StringList) void { @@ -186,8 +148,11 @@ pub const KeyStateOverlay = extern struct { old.destroy(); priv.tables = null; } + if (value) |v| { + priv.tables = v; + } - priv.tables = value; + self.as(gobject.Object).notifyByPspec(properties.tables.impl.param_spec); self.as(gobject.Object).notifyByPspec(properties.@"has-tables".impl.param_spec); } @@ -197,17 +162,22 @@ pub const KeyStateOverlay = extern struct { old.destroy(); priv.sequence = null; } + if (value) |v| { + priv.sequence = v; + } - priv.sequence = value; + self.as(gobject.Object).notifyByPspec(properties.sequence.impl.param_spec); self.as(gobject.Object).notifyByPspec(properties.@"has-sequence".impl.param_spec); } fn getHasTables(self: *Self) bool { - return self.private().tables != null; + const v = self.private().tables orelse return false; + return v.strings.len > 0; } fn getHasSequence(self: *Self) bool { - return self.private().sequence != null; + const v = self.private().sequence orelse return false; + return v.strings.len > 0; } fn closureShowChevron( @@ -218,6 +188,50 @@ pub const KeyStateOverlay = extern struct { return if (has_tables and has_sequence) 1 else 0; } + fn closureHasState( + _: *Self, + has_tables: bool, + has_sequence: bool, + ) callconv(.c) c_int { + return if (has_tables or has_sequence) 1 else 0; + } + + fn closureTablesText( + _: *Self, + tables: ?*ext.StringList, + ) callconv(.c) ?[*:0]const u8 { + const list = tables orelse return null; + if (list.strings.len == 0) return null; + + var buf: std.Io.Writer.Allocating = .init(Application.default().allocator()); + defer buf.deinit(); + + for (list.strings, 0..) |s, i| { + if (i > 0) buf.writer.writeAll(" > ") catch return null; + buf.writer.writeAll(s) catch return null; + } + + return glib.ext.dupeZ(u8, buf.written()); + } + + fn closureSequenceText( + _: *Self, + sequence: ?*ext.StringList, + ) callconv(.c) ?[*:0]const u8 { + const list = sequence orelse return null; + if (list.strings.len == 0) return null; + + var buf: std.Io.Writer.Allocating = .init(Application.default().allocator()); + defer buf.deinit(); + + for (list.strings, 0..) |s, i| { + if (i > 0) buf.writer.writeAll(" ") catch return null; + buf.writer.writeAll(s) catch return null; + } + + return glib.ext.dupeZ(u8, buf.written()); + } + //--------------------------------------------------------------- // Template callbacks @@ -303,15 +317,16 @@ pub const KeyStateOverlay = extern struct { // Template Callbacks class.bindTemplateCallback("on_drag_end", &onDragEnd); class.bindTemplateCallback("show_chevron", &closureShowChevron); + class.bindTemplateCallback("has_state", &closureHasState); + class.bindTemplateCallback("tables_text", &closureTablesText); + class.bindTemplateCallback("sequence_text", &closureSequenceText); // Properties gobject.ext.registerProperties(class, &.{ - properties.active.impl, properties.tables.impl, properties.@"has-tables".impl, properties.sequence.impl, properties.@"has-sequence".impl, - properties.pending.impl, properties.@"valign-target".impl, }); diff --git a/src/apprt/gtk/class/surface.zig b/src/apprt/gtk/class/surface.zig index 556e5e2ec..a14d53c32 100644 --- a/src/apprt/gtk/class/surface.zig +++ b/src/apprt/gtk/class/surface.zig @@ -832,6 +832,10 @@ pub const Surface = extern struct { const priv = self.private(); const alloc = Application.default().allocator(); + self.as(gobject.Object).freezeNotify(); + defer self.as(gobject.Object).thawNotify(); + self.as(gobject.Object).notifyByPspec(properties.@"key-sequence".impl.param_spec); + switch (value) { .trigger => |trigger| { // Convert the trigger to a human-readable label @@ -865,6 +869,10 @@ pub const Surface = extern struct { const priv = self.private(); const alloc = Application.default().allocator(); + self.as(gobject.Object).freezeNotify(); + defer self.as(gobject.Object).thawNotify(); + self.as(gobject.Object).notifyByPspec(properties.@"key-table".impl.param_spec); + switch (value) { .activate => |name| { // Duplicate the name string and push onto stack diff --git a/src/apprt/gtk/ui/1.2/key-state-overlay.blp b/src/apprt/gtk/ui/1.2/key-state-overlay.blp index 504d2e26e..c8654bfbb 100644 --- a/src/apprt/gtk/ui/1.2/key-state-overlay.blp +++ b/src/apprt/gtk/ui/1.2/key-state-overlay.blp @@ -2,7 +2,7 @@ using Gtk 4.0; using Adw 1; template $GhosttyKeyStateOverlay: Adw.Bin { - visible: bind template.active; + visible: bind $has_state(template.has-tables, template.has-sequence) as ; valign-target: end; halign: center; valign: bind template.valign-target; @@ -30,7 +30,7 @@ template $GhosttyKeyStateOverlay: Adw.Bin { Label tables_label { visible: bind template.has-tables; - label: bind template.tables-text; + label: bind $tables_text(template.tables) as ; xalign: 0.0; } @@ -45,13 +45,13 @@ template $GhosttyKeyStateOverlay: Adw.Bin { Label sequence_label { visible: bind template.has-sequence; - label: bind template.sequence-text; + label: bind $sequence_text(template.sequence) as ; xalign: 0.0; } Spinner pending_spinner { - visible: bind template.pending; - spinning: bind template.pending; + visible: bind template.has-sequence; + spinning: bind template.has-sequence; } } } From f2fe979bab56f434d9b3711b373a9b172ec1fc91 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 23 Dec 2025 11:23:02 -0800 Subject: [PATCH 7/7] update valgrind suppressions --- valgrind.supp | 64 +++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 62 insertions(+), 2 deletions(-) diff --git a/valgrind.supp b/valgrind.supp index eeb395d03..27479fd5c 100644 --- a/valgrind.supp +++ b/valgrind.supp @@ -72,7 +72,16 @@ fun:gdk_surface_handle_event ... } - +{ + GTK CSS Node Validation + Memcheck:Leak + match-leak-kinds: possible + fun:malloc + ... + fun:gtk_css_node_validate_internal + fun:gtk_css_node_validate + ... +} { GTK CSS Provider Leak Memcheck:Leak @@ -196,8 +205,44 @@ fun:svga_context_flush ... } - { + SVGA Stuff + Memcheck:Leak + match-leak-kinds: definite + fun:calloc + fun:svga_create_surface_view + fun:svga_set_framebuffer_state + fun:st_update_framebuffer_state + fun:st_Clear + fun:gsk_gpu_render_pass_op_gl_command + ... +} +{ + GTK Icon + Memcheck:Leak + match-leak-kinds: possible + fun:*alloc + ... + fun:gtk_icon_theme_set_display + fun:gtk_icon_theme_get_for_display + ... +} +{ + GDK Wayland Connection + Memcheck:Leak + match-leak-kinds: possible + fun:calloc + fun:wl_closure_init + fun:wl_connection_demarshal + fun:wl_display_read_events + fun:gdk_wayland_poll_source_check + fun:g_main_context_check_unlocked + fun:g_main_context_iterate_unlocked.isra.0 + fun:g_main_context_iteration + ... +} +{ + GSK Renderer GPU Stuff Memcheck:Leak match-leak-kinds: possible @@ -297,6 +342,21 @@ fun:g_main_context_iteration ... } +{ + GSK More Forms + Memcheck:Leak + match-leak-kinds: possible + ... + fun:gsk_gl_device_use_program + fun:gsk_gl_frame_use_program + fun:gsk_gpu_shader_op_gl_command_n + fun:gsk_gpu_render_pass_op_gl_command + fun:gsk_gl_frame_submit + fun:gsk_gpu_renderer_render_texture + fun:gsk_renderer_render_texture + fun:render_contents + ... +} { GTK Shader Selector Memcheck:Leak