aboutsummaryrefslogtreecommitdiff
path: root/vendor/libghostty-vt/src/apprt/gtk/ext
diff options
context:
space:
mode:
authorCan Celik <ogulcancelik@gmail.com>2026-04-04 19:22:04 +0300
committerCan Celik <ogulcancelik@gmail.com>2026-04-05 01:23:06 +0300
commit41dd733ca7123c9ac4bfc6c781697a660e95b052 (patch)
tree3fc91815ca139ef2e0b281bba0abda4ef15175f4 /vendor/libghostty-vt/src/apprt/gtk/ext
parent55c138263e190a1e79d4b997bfb9397306c0ec1f (diff)
feat: finish ghostty pane backend integration
Diffstat (limited to 'vendor/libghostty-vt/src/apprt/gtk/ext')
-rw-r--r--vendor/libghostty-vt/src/apprt/gtk/ext/actions.zig198
-rw-r--r--vendor/libghostty-vt/src/apprt/gtk/ext/slice.zig111
2 files changed, 309 insertions, 0 deletions
diff --git a/vendor/libghostty-vt/src/apprt/gtk/ext/actions.zig b/vendor/libghostty-vt/src/apprt/gtk/ext/actions.zig
new file mode 100644
index 00000000..3232bc18
--- /dev/null
+++ b/vendor/libghostty-vt/src/apprt/gtk/ext/actions.zig
@@ -0,0 +1,198 @@
+const std = @import("std");
+
+const assert = @import("../../../quirks.zig").inlineAssert;
+const testing = std.testing;
+
+const gio = @import("gio");
+const glib = @import("glib");
+const gobject = @import("gobject");
+const gtk = @import("gtk");
+
+const gValueHolds = @import("../ext.zig").gValueHolds;
+
+/// Check that an action name is valid.
+///
+/// Reimplementation of `g_action_name_is_valid()` so that it can be
+/// used at comptime.
+///
+/// See:
+/// https://docs.gtk.org/gio/type_func.Action.name_is_valid.html
+fn gActionNameIsValid(name: [:0]const u8) bool {
+ if (name.len == 0) return false;
+
+ for (name) |c| switch (c) {
+ '-' => continue,
+ '.' => continue,
+ '0'...'9' => continue,
+ 'a'...'z' => continue,
+ 'A'...'Z' => continue,
+ else => return false,
+ };
+
+ return true;
+}
+
+test "gActionNameIsValid" {
+ try testing.expect(gActionNameIsValid("ring-bell"));
+ try testing.expect(!gActionNameIsValid("ring_bell"));
+}
+
+/// Function to create a structure for describing an action.
+pub fn Action(comptime T: type) type {
+ return struct {
+ const Self = @This();
+ pub const Callback = *const fn (*gio.SimpleAction, ?*glib.Variant, *T) callconv(.c) void;
+
+ name: [:0]const u8,
+ callback: Callback,
+ parameter_type: ?*const glib.VariantType,
+ state: ?*glib.Variant = null,
+
+ /// Function to initialize a new action so that we can comptime check
+ /// the name.
+ pub fn init(
+ comptime name: [:0]const u8,
+ callback: Callback,
+ parameter_type: ?*const glib.VariantType,
+ ) Self {
+ comptime assert(gActionNameIsValid(name));
+
+ return .{
+ .name = name,
+ .callback = callback,
+ .parameter_type = parameter_type,
+ };
+ }
+
+ /// Function to initialize a new stateful action so that we can comptime
+ /// check the name.
+ pub fn initStateful(
+ comptime name: [:0]const u8,
+ callback: Callback,
+ parameter_type: ?*const glib.VariantType,
+ state: *glib.Variant,
+ ) Self {
+ comptime assert(gActionNameIsValid(name));
+ return .{
+ .name = name,
+ .callback = callback,
+ .parameter_type = parameter_type,
+ .state = state,
+ };
+ }
+ };
+}
+
+/// Add actions to a widget that implements gio.ActionMap.
+pub fn add(comptime T: type, self: *T, actions: []const Action(T)) void {
+ addToMap(T, self, self.as(gio.ActionMap), actions);
+}
+
+/// Add actions to the given map.
+pub fn addToMap(comptime T: type, self: *T, map: *gio.ActionMap, actions: []const Action(T)) void {
+ for (actions) |entry| {
+ assert(gActionNameIsValid(entry.name));
+ const action = action: {
+ if (entry.state) |state| {
+ break :action gio.SimpleAction.newStateful(
+ entry.name,
+ entry.parameter_type,
+ state,
+ );
+ }
+ break :action gio.SimpleAction.new(
+ entry.name,
+ entry.parameter_type,
+ );
+ };
+ defer action.unref();
+ _ = gio.SimpleAction.signals.activate.connect(
+ action,
+ *T,
+ entry.callback,
+ self,
+ .{},
+ );
+ map.addAction(action.as(gio.Action));
+ }
+}
+
+/// Add actions to a widget that doesn't implement ActionGroup directly.
+pub fn addAsGroup(comptime T: type, self: *T, comptime name: [:0]const u8, actions: []const Action(T)) *gio.SimpleActionGroup {
+ comptime assert(gActionNameIsValid(name));
+
+ // Collect our actions into a group since we're just a plain widget that
+ // doesn't implement ActionGroup directly.
+ const group = gio.SimpleActionGroup.new();
+ errdefer group.unref();
+
+ addToMap(T, self, group.as(gio.ActionMap), actions);
+
+ self.as(gtk.Widget).insertActionGroup(
+ name,
+ group.as(gio.ActionGroup),
+ );
+
+ return group;
+}
+
+test "adding actions to an object" {
+ // This test requires a connection to an active display environment.
+ if (gtk.initCheck() == 0) return error.SkipZigTest;
+
+ _ = glib.MainContext.acquire(null);
+ defer glib.MainContext.release(null);
+
+ const callbacks = struct {
+ fn callback(_: *gio.SimpleAction, variant_: ?*glib.Variant, self: *gtk.Box) callconv(.c) void {
+ const i32_variant_type = glib.ext.VariantType.newFor(i32);
+ defer i32_variant_type.free();
+
+ const variant = variant_ orelse return;
+ assert(variant.isOfType(i32_variant_type) != 0);
+
+ var value = std.mem.zeroes(gobject.Value);
+ _ = value.init(gobject.ext.types.int);
+ defer value.unset();
+
+ value.setInt(variant.getInt32());
+
+ self.as(gobject.Object).setProperty("spacing", &value);
+ }
+ };
+
+ const box = gtk.Box.new(.vertical, 0);
+ _ = box.as(gobject.Object).refSink();
+ defer box.unref();
+
+ {
+ const i32_variant_type = glib.ext.VariantType.newFor(i32);
+ defer i32_variant_type.free();
+
+ const actions = [_]Action(gtk.Box){
+ .init("test", callbacks.callback, i32_variant_type),
+ };
+
+ _ = addAsGroup(gtk.Box, box, "test", &actions);
+ }
+
+ const expected = std.crypto.random.intRangeAtMost(i32, 1, std.math.maxInt(u31));
+ const parameter = glib.Variant.newInt32(expected);
+
+ try testing.expect(box.as(gtk.Widget).activateActionVariant("test.test", parameter) != 0);
+
+ _ = glib.MainContext.iteration(null, @intFromBool(true));
+
+ var value = std.mem.zeroes(gobject.Value);
+ _ = value.init(gobject.ext.types.int);
+ defer value.unset();
+
+ box.as(gobject.Object).getProperty("spacing", &value);
+
+ try testing.expect(gValueHolds(&value, gobject.ext.types.int));
+
+ const actual = value.getInt();
+ try testing.expectEqual(expected, actual);
+
+ while (glib.MainContext.iteration(null, 0) != 0) {}
+}
diff --git a/vendor/libghostty-vt/src/apprt/gtk/ext/slice.zig b/vendor/libghostty-vt/src/apprt/gtk/ext/slice.zig
new file mode 100644
index 00000000..49ad63d8
--- /dev/null
+++ b/vendor/libghostty-vt/src/apprt/gtk/ext/slice.zig
@@ -0,0 +1,111 @@
+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);
+ }
+
+ /// 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,
+ .{
+ .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);
+}