mirror of
https://github.com/ghostty-org/ghostty.git
synced 2026-04-18 21:40:29 +00:00
apprt/gtk: store key sequences/tables in surface state
This commit is contained in:
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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 };
|
||||
|
||||
Reference in New Issue
Block a user