From acac884cfe2f9dadafe6c1ff31f4e39f11b83fe9 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Tue, 17 Mar 2026 22:58:39 +0100 Subject: [PATCH] Add support for local keybinds Signed-off-by: AnErrupTion --- ly-ui/src/TerminalBuffer.zig | 46 ++++++++++++++++++++++------ ly-ui/src/Widget.zig | 3 ++ ly-ui/src/components/BigLabel.zig | 1 + ly-ui/src/components/CenteredBox.zig | 1 + ly-ui/src/components/Label.zig | 1 + ly-ui/src/components/Text.zig | 22 +++++++++++-- src/animations/Cascade.zig | 1 + src/animations/ColorMix.zig | 1 + src/animations/Doom.zig | 1 + src/animations/DurFile.zig | 1 + src/animations/GameOfLife.zig | 1 + src/animations/Matrix.zig | 1 + src/components/InfoLine.zig | 1 + src/components/Session.zig | 1 + src/components/UserList.zig | 1 + src/main.zig | 44 +++++++++----------------- 16 files changed, 87 insertions(+), 40 deletions(-) diff --git a/ly-ui/src/TerminalBuffer.zig b/ly-ui/src/TerminalBuffer.zig index ec5c6dc..28e03f0 100644 --- a/ly-ui/src/TerminalBuffer.zig +++ b/ly-ui/src/TerminalBuffer.zig @@ -15,8 +15,8 @@ const Widget = @import("Widget.zig"); const TerminalBuffer = @This(); -const KeybindCallbackFn = *const fn (*anyopaque) anyerror!bool; -const KeybindMap = std.AutoHashMap(keyboard.Key, struct { +pub const KeybindCallbackFn = *const fn (*anyopaque) anyerror!bool; +pub const KeybindMap = std.AutoHashMap(keyboard.Key, struct { callback: KeybindCallbackFn, context: *anyopaque, }); @@ -177,14 +177,14 @@ pub fn runEventLoop( inactivity_event_fn: ?*const fn (*anyopaque) anyerror!void, context: *anyopaque, ) !void { - try self.registerKeybind("Ctrl+K", &moveCursorUp, self); - try self.registerKeybind("Up", &moveCursorUp, self); + try self.registerGlobalKeybind("Ctrl+K", &moveCursorUp, self); + try self.registerGlobalKeybind("Up", &moveCursorUp, self); - try self.registerKeybind("Ctrl+J", &moveCursorDown, self); - try self.registerKeybind("Down", &moveCursorDown, self); + try self.registerGlobalKeybind("Ctrl+J", &moveCursorDown, self); + try self.registerGlobalKeybind("Down", &moveCursorDown, self); - try self.registerKeybind("Tab", &wrapCursor, self); - try self.registerKeybind("Shift+Tab", &wrapCursorReverse, self); + try self.registerGlobalKeybind("Tab", &wrapCursor, self); + try self.registerGlobalKeybind("Shift+Tab", &wrapCursorReverse, self); defer self.handlable_widgets.deinit(allocator); @@ -402,13 +402,14 @@ pub fn reclaim(self: TerminalBuffer) !void { pub fn registerKeybind( self: *TerminalBuffer, + keybinds: *KeybindMap, keybind: []const u8, callback: KeybindCallbackFn, context: *anyopaque, ) !void { const key = try self.parseKeybind(keybind); - self.keybinds.put(key, .{ + keybinds.put(key, .{ .callback = callback, .context = context, }) catch |err| { @@ -420,6 +421,15 @@ pub fn registerKeybind( }; } +pub fn registerGlobalKeybind( + self: *TerminalBuffer, + keybind: []const u8, + callback: KeybindCallbackFn, + context: *anyopaque, +) !void { + try self.registerKeybind(&self.keybinds, keybind, callback, context); +} + pub fn simulateKeybind(self: *TerminalBuffer, keybind: []const u8) !bool { const key = try self.parseKeybind(keybind); @@ -552,6 +562,24 @@ fn handleKeybind( return keys; } + + const current_widget = self.getActiveWidget(); + if (current_widget.keybinds) |keybinds| { + if (keybinds.get(key)) |binding| { + const passthrough_event = try @call( + .auto, + binding.callback, + .{binding.context}, + ); + + if (!passthrough_event) { + keys.deinit(allocator); + return null; + } + + return keys; + } + } } return keys; diff --git a/ly-ui/src/Widget.zig b/ly-ui/src/Widget.zig index 98891fd..5c613d8 100644 --- a/ly-ui/src/Widget.zig +++ b/ly-ui/src/Widget.zig @@ -14,11 +14,13 @@ const VTable = struct { id: u64, display_name: []const u8, +keybinds: ?TerminalBuffer.KeybindMap, pointer: *anyopaque, vtable: VTable, pub fn init( display_name: []const u8, + keybinds: ?TerminalBuffer.KeybindMap, pointer: anytype, comptime deinit_fn: ?fn (ptr: @TypeOf(pointer)) void, comptime realloc_fn: ?fn (ptr: @TypeOf(pointer)) anyerror!void, @@ -102,6 +104,7 @@ pub fn init( return .{ .id = @intFromPtr(Impl.vtable.draw_fn), .display_name = display_name, + .keybinds = keybinds, .pointer = pointer, .vtable = Impl.vtable, }; diff --git a/ly-ui/src/components/BigLabel.zig b/ly-ui/src/components/BigLabel.zig index 5159f66..156a498 100644 --- a/ly-ui/src/components/BigLabel.zig +++ b/ly-ui/src/components/BigLabel.zig @@ -88,6 +88,7 @@ pub fn deinit(self: *BigLabel) void { pub fn widget(self: *BigLabel) Widget { return Widget.init( "BigLabel", + null, self, deinit, null, diff --git a/ly-ui/src/components/CenteredBox.zig b/ly-ui/src/components/CenteredBox.zig index f0e7cd5..fe10880 100644 --- a/ly-ui/src/components/CenteredBox.zig +++ b/ly-ui/src/components/CenteredBox.zig @@ -62,6 +62,7 @@ pub fn init( pub fn widget(self: *CenteredBox) Widget { return Widget.init( "CenteredBox", + null, self, null, null, diff --git a/ly-ui/src/components/Label.zig b/ly-ui/src/components/Label.zig index f80793a..9f47820 100644 --- a/ly-ui/src/components/Label.zig +++ b/ly-ui/src/components/Label.zig @@ -46,6 +46,7 @@ pub fn deinit(self: *Label) void { pub fn widget(self: *Label) Widget { return Widget.init( "Label", + null, self, deinit, null, diff --git a/ly-ui/src/components/Text.zig b/ly-ui/src/components/Text.zig index d31aee7..ab139c8 100644 --- a/ly-ui/src/components/Text.zig +++ b/ly-ui/src/components/Text.zig @@ -23,6 +23,7 @@ masked: bool, maybe_mask: ?u32, fg: u32, bg: u32, +keybinds: TerminalBuffer.KeybindMap, pub fn init( allocator: Allocator, @@ -32,8 +33,9 @@ pub fn init( width: usize, fg: u32, bg: u32, -) Text { - return .{ +) !*Text { + var self = try allocator.create(Text); + self.* = Text{ .allocator = allocator, .buffer = buffer, .text = .empty, @@ -47,16 +49,24 @@ pub fn init( .maybe_mask = maybe_mask, .fg = fg, .bg = bg, + .keybinds = .init(allocator), }; + + try buffer.registerKeybind(&self.keybinds, "Ctrl+U", &clearTextEntry, self); + + return self; } pub fn deinit(self: *Text) void { self.text.deinit(self.allocator); + self.keybinds.deinit(); + self.allocator.destroy(self); } pub fn widget(self: *Text) Widget { return Widget.init( "Text", + self.keybinds, self, deinit, null, @@ -208,3 +218,11 @@ fn write(self: *Text, char: u8) !void { self.end += 1; self.goRight(); } + +fn clearTextEntry(ptr: *anyopaque) !bool { + var self: *Text = @ptrCast(@alignCast(ptr)); + + self.clear(); + self.buffer.drawNextFrame(true); + return false; +} diff --git a/src/animations/Cascade.zig b/src/animations/Cascade.zig index 402ccd9..970d99a 100644 --- a/src/animations/Cascade.zig +++ b/src/animations/Cascade.zig @@ -27,6 +27,7 @@ pub fn init( pub fn widget(self: *Cascade) Widget { return Widget.init( "Cascade", + null, self, null, null, diff --git a/src/animations/ColorMix.zig b/src/animations/ColorMix.zig index 2366e8d..227205f 100644 --- a/src/animations/ColorMix.zig +++ b/src/animations/ColorMix.zig @@ -69,6 +69,7 @@ pub fn init( pub fn widget(self: *ColorMix) Widget { return Widget.init( "ColorMix", + null, self, null, null, diff --git a/src/animations/Doom.zig b/src/animations/Doom.zig index a5db265..8fa842b 100644 --- a/src/animations/Doom.zig +++ b/src/animations/Doom.zig @@ -76,6 +76,7 @@ pub fn init( pub fn widget(self: *Doom) Widget { return Widget.init( "Doom", + null, self, deinit, realloc, diff --git a/src/animations/DurFile.zig b/src/animations/DurFile.zig index 3e80ead..04fc8ff 100644 --- a/src/animations/DurFile.zig +++ b/src/animations/DurFile.zig @@ -430,6 +430,7 @@ pub fn init( pub fn widget(self: *DurFile) Widget { return Widget.init( "DurFile", + null, self, deinit, realloc, diff --git a/src/animations/GameOfLife.zig b/src/animations/GameOfLife.zig index 3ad5a38..3f6e5d5 100644 --- a/src/animations/GameOfLife.zig +++ b/src/animations/GameOfLife.zig @@ -86,6 +86,7 @@ pub fn init( pub fn widget(self: *GameOfLife) Widget { return Widget.init( "GameOfLife", + null, self, deinit, realloc, diff --git a/src/animations/Matrix.zig b/src/animations/Matrix.zig index 8022e45..9becbd6 100644 --- a/src/animations/Matrix.zig +++ b/src/animations/Matrix.zig @@ -83,6 +83,7 @@ pub fn init( pub fn widget(self: *Matrix) Widget { return Widget.init( "Matrix", + null, self, deinit, realloc, diff --git a/src/components/InfoLine.zig b/src/components/InfoLine.zig index 1afcdcf..161639b 100644 --- a/src/components/InfoLine.zig +++ b/src/components/InfoLine.zig @@ -49,6 +49,7 @@ pub fn deinit(self: *InfoLine) void { pub fn widget(self: *InfoLine) Widget { return Widget.init( "InfoLine", + null, self, deinit, null, diff --git a/src/components/Session.zig b/src/components/Session.zig index 6ec4016..726de81 100644 --- a/src/components/Session.zig +++ b/src/components/Session.zig @@ -58,6 +58,7 @@ pub fn deinit(self: *Session) void { pub fn widget(self: *Session) Widget { return Widget.init( "Session", + null, self, deinit, null, diff --git a/src/components/UserList.zig b/src/components/UserList.zig index 77b8787..d872b67 100644 --- a/src/components/UserList.zig +++ b/src/components/UserList.zig @@ -92,6 +92,7 @@ pub fn deinit(self: *UserList) void { pub fn widget(self: *UserList) Widget { return Widget.init( "UserList", + null, self, deinit, null, diff --git a/src/main.zig b/src/main.zig index 46ea475..b715ff4 100644 --- a/src/main.zig +++ b/src/main.zig @@ -93,7 +93,7 @@ const UiState = struct { session: Session, saved_users: SavedUsers, login: UserList, - password: Text, + password: *Text, password_widget: Widget, insert_mode: bool, edge_margin: Position, @@ -792,7 +792,7 @@ pub fn main() !void { ); defer state.password_label.deinit(); - state.password = Text.init( + state.password = try Text.init( state.allocator, &state.buffer, true, @@ -1078,27 +1078,23 @@ pub fn main() !void { try widgets.append(state.allocator, &layer3); } - try state.buffer.registerKeybind("Esc", &disableInsertMode, &state); - try state.buffer.registerKeybind("I", &enableInsertMode, &state); + try state.buffer.registerGlobalKeybind("Esc", &disableInsertMode, &state); + try state.buffer.registerGlobalKeybind("I", &enableInsertMode, &state); - try state.buffer.registerKeybind("Ctrl+C", &quit, &state); + try state.buffer.registerGlobalKeybind("Ctrl+C", &quit, &state); - // TODO: Make this generic for any Text widget present in the UI - // TODO: Per-widget keybinds, will fix insert_mode hack too - try state.buffer.registerKeybind("Ctrl+U", &clearPassword, &state); + try state.buffer.registerGlobalKeybind("K", &viMoveCursorUp, &state); + try state.buffer.registerGlobalKeybind("J", &viMoveCursorDown, &state); - try state.buffer.registerKeybind("K", &viMoveCursorUp, &state); - try state.buffer.registerKeybind("J", &viMoveCursorDown, &state); + try state.buffer.registerGlobalKeybind("Enter", &authenticate, &state); - try state.buffer.registerKeybind("Enter", &authenticate, &state); - - try state.buffer.registerKeybind(state.config.shutdown_key, &shutdownCmd, &state); - try state.buffer.registerKeybind(state.config.restart_key, &restartCmd, &state); - try state.buffer.registerKeybind(state.config.show_password_key, &togglePasswordMask, &state); - if (state.config.sleep_cmd != null) try state.buffer.registerKeybind(state.config.sleep_key, &sleepCmd, &state); - if (state.config.hibernate_cmd != null) try state.buffer.registerKeybind(state.config.hibernate_key, &hibernateCmd, &state); - if (state.config.brightness_down_key) |key| try state.buffer.registerKeybind(key, &decreaseBrightnessCmd, &state); - if (state.config.brightness_up_key) |key| try state.buffer.registerKeybind(key, &increaseBrightnessCmd, &state); + try state.buffer.registerGlobalKeybind(state.config.shutdown_key, &shutdownCmd, &state); + try state.buffer.registerGlobalKeybind(state.config.restart_key, &restartCmd, &state); + try state.buffer.registerGlobalKeybind(state.config.show_password_key, &togglePasswordMask, &state); + if (state.config.sleep_cmd != null) try state.buffer.registerGlobalKeybind(state.config.sleep_key, &sleepCmd, &state); + if (state.config.hibernate_cmd != null) try state.buffer.registerGlobalKeybind(state.config.hibernate_key, &hibernateCmd, &state); + if (state.config.brightness_down_key) |key| try state.buffer.registerGlobalKeybind(key, &decreaseBrightnessCmd, &state); + if (state.config.brightness_up_key) |key| try state.buffer.registerGlobalKeybind(key, &increaseBrightnessCmd, &state); if (state.config.initial_info_text) |text| { try state.info_line.addMessage(text, state.config.bg, state.config.fg); @@ -1212,16 +1208,6 @@ fn viMoveCursorDown(ptr: *anyopaque) !bool { return try state.buffer.simulateKeybind("Down"); } -fn clearPassword(ptr: *anyopaque) !bool { - var state: *UiState = @ptrCast(@alignCast(ptr)); - - if (state.buffer.getActiveWidget().id == state.password_widget.id) { - state.password.clear(); - state.buffer.drawNextFrame(true); - } - return false; -} - fn togglePasswordMask(ptr: *anyopaque) !bool { var state: *UiState = @ptrCast(@alignCast(ptr));