Add support for local keybinds

Signed-off-by: AnErrupTion <anerruption@disroot.org>
This commit is contained in:
AnErrupTion
2026-03-17 22:58:39 +01:00
parent a89c918c5d
commit acac884cfe
16 changed files with 87 additions and 40 deletions

View File

@@ -15,8 +15,8 @@ const Widget = @import("Widget.zig");
const TerminalBuffer = @This(); const TerminalBuffer = @This();
const KeybindCallbackFn = *const fn (*anyopaque) anyerror!bool; pub const KeybindCallbackFn = *const fn (*anyopaque) anyerror!bool;
const KeybindMap = std.AutoHashMap(keyboard.Key, struct { pub const KeybindMap = std.AutoHashMap(keyboard.Key, struct {
callback: KeybindCallbackFn, callback: KeybindCallbackFn,
context: *anyopaque, context: *anyopaque,
}); });
@@ -177,14 +177,14 @@ pub fn runEventLoop(
inactivity_event_fn: ?*const fn (*anyopaque) anyerror!void, inactivity_event_fn: ?*const fn (*anyopaque) anyerror!void,
context: *anyopaque, context: *anyopaque,
) !void { ) !void {
try self.registerKeybind("Ctrl+K", &moveCursorUp, self); try self.registerGlobalKeybind("Ctrl+K", &moveCursorUp, self);
try self.registerKeybind("Up", &moveCursorUp, self); try self.registerGlobalKeybind("Up", &moveCursorUp, self);
try self.registerKeybind("Ctrl+J", &moveCursorDown, self); try self.registerGlobalKeybind("Ctrl+J", &moveCursorDown, self);
try self.registerKeybind("Down", &moveCursorDown, self); try self.registerGlobalKeybind("Down", &moveCursorDown, self);
try self.registerKeybind("Tab", &wrapCursor, self); try self.registerGlobalKeybind("Tab", &wrapCursor, self);
try self.registerKeybind("Shift+Tab", &wrapCursorReverse, self); try self.registerGlobalKeybind("Shift+Tab", &wrapCursorReverse, self);
defer self.handlable_widgets.deinit(allocator); defer self.handlable_widgets.deinit(allocator);
@@ -402,13 +402,14 @@ pub fn reclaim(self: TerminalBuffer) !void {
pub fn registerKeybind( pub fn registerKeybind(
self: *TerminalBuffer, self: *TerminalBuffer,
keybinds: *KeybindMap,
keybind: []const u8, keybind: []const u8,
callback: KeybindCallbackFn, callback: KeybindCallbackFn,
context: *anyopaque, context: *anyopaque,
) !void { ) !void {
const key = try self.parseKeybind(keybind); const key = try self.parseKeybind(keybind);
self.keybinds.put(key, .{ keybinds.put(key, .{
.callback = callback, .callback = callback,
.context = context, .context = context,
}) catch |err| { }) 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 { pub fn simulateKeybind(self: *TerminalBuffer, keybind: []const u8) !bool {
const key = try self.parseKeybind(keybind); const key = try self.parseKeybind(keybind);
@@ -552,6 +562,24 @@ fn handleKeybind(
return keys; 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; return keys;

View File

@@ -14,11 +14,13 @@ const VTable = struct {
id: u64, id: u64,
display_name: []const u8, display_name: []const u8,
keybinds: ?TerminalBuffer.KeybindMap,
pointer: *anyopaque, pointer: *anyopaque,
vtable: VTable, vtable: VTable,
pub fn init( pub fn init(
display_name: []const u8, display_name: []const u8,
keybinds: ?TerminalBuffer.KeybindMap,
pointer: anytype, pointer: anytype,
comptime deinit_fn: ?fn (ptr: @TypeOf(pointer)) void, comptime deinit_fn: ?fn (ptr: @TypeOf(pointer)) void,
comptime realloc_fn: ?fn (ptr: @TypeOf(pointer)) anyerror!void, comptime realloc_fn: ?fn (ptr: @TypeOf(pointer)) anyerror!void,
@@ -102,6 +104,7 @@ pub fn init(
return .{ return .{
.id = @intFromPtr(Impl.vtable.draw_fn), .id = @intFromPtr(Impl.vtable.draw_fn),
.display_name = display_name, .display_name = display_name,
.keybinds = keybinds,
.pointer = pointer, .pointer = pointer,
.vtable = Impl.vtable, .vtable = Impl.vtable,
}; };

View File

@@ -88,6 +88,7 @@ pub fn deinit(self: *BigLabel) void {
pub fn widget(self: *BigLabel) Widget { pub fn widget(self: *BigLabel) Widget {
return Widget.init( return Widget.init(
"BigLabel", "BigLabel",
null,
self, self,
deinit, deinit,
null, null,

View File

@@ -62,6 +62,7 @@ pub fn init(
pub fn widget(self: *CenteredBox) Widget { pub fn widget(self: *CenteredBox) Widget {
return Widget.init( return Widget.init(
"CenteredBox", "CenteredBox",
null,
self, self,
null, null,
null, null,

View File

@@ -46,6 +46,7 @@ pub fn deinit(self: *Label) void {
pub fn widget(self: *Label) Widget { pub fn widget(self: *Label) Widget {
return Widget.init( return Widget.init(
"Label", "Label",
null,
self, self,
deinit, deinit,
null, null,

View File

@@ -23,6 +23,7 @@ masked: bool,
maybe_mask: ?u32, maybe_mask: ?u32,
fg: u32, fg: u32,
bg: u32, bg: u32,
keybinds: TerminalBuffer.KeybindMap,
pub fn init( pub fn init(
allocator: Allocator, allocator: Allocator,
@@ -32,8 +33,9 @@ pub fn init(
width: usize, width: usize,
fg: u32, fg: u32,
bg: u32, bg: u32,
) Text { ) !*Text {
return .{ var self = try allocator.create(Text);
self.* = Text{
.allocator = allocator, .allocator = allocator,
.buffer = buffer, .buffer = buffer,
.text = .empty, .text = .empty,
@@ -47,16 +49,24 @@ pub fn init(
.maybe_mask = maybe_mask, .maybe_mask = maybe_mask,
.fg = fg, .fg = fg,
.bg = bg, .bg = bg,
.keybinds = .init(allocator),
}; };
try buffer.registerKeybind(&self.keybinds, "Ctrl+U", &clearTextEntry, self);
return self;
} }
pub fn deinit(self: *Text) void { pub fn deinit(self: *Text) void {
self.text.deinit(self.allocator); self.text.deinit(self.allocator);
self.keybinds.deinit();
self.allocator.destroy(self);
} }
pub fn widget(self: *Text) Widget { pub fn widget(self: *Text) Widget {
return Widget.init( return Widget.init(
"Text", "Text",
self.keybinds,
self, self,
deinit, deinit,
null, null,
@@ -208,3 +218,11 @@ fn write(self: *Text, char: u8) !void {
self.end += 1; self.end += 1;
self.goRight(); self.goRight();
} }
fn clearTextEntry(ptr: *anyopaque) !bool {
var self: *Text = @ptrCast(@alignCast(ptr));
self.clear();
self.buffer.drawNextFrame(true);
return false;
}

View File

@@ -27,6 +27,7 @@ pub fn init(
pub fn widget(self: *Cascade) Widget { pub fn widget(self: *Cascade) Widget {
return Widget.init( return Widget.init(
"Cascade", "Cascade",
null,
self, self,
null, null,
null, null,

View File

@@ -69,6 +69,7 @@ pub fn init(
pub fn widget(self: *ColorMix) Widget { pub fn widget(self: *ColorMix) Widget {
return Widget.init( return Widget.init(
"ColorMix", "ColorMix",
null,
self, self,
null, null,
null, null,

View File

@@ -76,6 +76,7 @@ pub fn init(
pub fn widget(self: *Doom) Widget { pub fn widget(self: *Doom) Widget {
return Widget.init( return Widget.init(
"Doom", "Doom",
null,
self, self,
deinit, deinit,
realloc, realloc,

View File

@@ -430,6 +430,7 @@ pub fn init(
pub fn widget(self: *DurFile) Widget { pub fn widget(self: *DurFile) Widget {
return Widget.init( return Widget.init(
"DurFile", "DurFile",
null,
self, self,
deinit, deinit,
realloc, realloc,

View File

@@ -86,6 +86,7 @@ pub fn init(
pub fn widget(self: *GameOfLife) Widget { pub fn widget(self: *GameOfLife) Widget {
return Widget.init( return Widget.init(
"GameOfLife", "GameOfLife",
null,
self, self,
deinit, deinit,
realloc, realloc,

View File

@@ -83,6 +83,7 @@ pub fn init(
pub fn widget(self: *Matrix) Widget { pub fn widget(self: *Matrix) Widget {
return Widget.init( return Widget.init(
"Matrix", "Matrix",
null,
self, self,
deinit, deinit,
realloc, realloc,

View File

@@ -49,6 +49,7 @@ pub fn deinit(self: *InfoLine) void {
pub fn widget(self: *InfoLine) Widget { pub fn widget(self: *InfoLine) Widget {
return Widget.init( return Widget.init(
"InfoLine", "InfoLine",
null,
self, self,
deinit, deinit,
null, null,

View File

@@ -58,6 +58,7 @@ pub fn deinit(self: *Session) void {
pub fn widget(self: *Session) Widget { pub fn widget(self: *Session) Widget {
return Widget.init( return Widget.init(
"Session", "Session",
null,
self, self,
deinit, deinit,
null, null,

View File

@@ -92,6 +92,7 @@ pub fn deinit(self: *UserList) void {
pub fn widget(self: *UserList) Widget { pub fn widget(self: *UserList) Widget {
return Widget.init( return Widget.init(
"UserList", "UserList",
null,
self, self,
deinit, deinit,
null, null,

View File

@@ -93,7 +93,7 @@ const UiState = struct {
session: Session, session: Session,
saved_users: SavedUsers, saved_users: SavedUsers,
login: UserList, login: UserList,
password: Text, password: *Text,
password_widget: Widget, password_widget: Widget,
insert_mode: bool, insert_mode: bool,
edge_margin: Position, edge_margin: Position,
@@ -792,7 +792,7 @@ pub fn main() !void {
); );
defer state.password_label.deinit(); defer state.password_label.deinit();
state.password = Text.init( state.password = try Text.init(
state.allocator, state.allocator,
&state.buffer, &state.buffer,
true, true,
@@ -1078,27 +1078,23 @@ pub fn main() !void {
try widgets.append(state.allocator, &layer3); try widgets.append(state.allocator, &layer3);
} }
try state.buffer.registerKeybind("Esc", &disableInsertMode, &state); try state.buffer.registerGlobalKeybind("Esc", &disableInsertMode, &state);
try state.buffer.registerKeybind("I", &enableInsertMode, &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 try state.buffer.registerGlobalKeybind("K", &viMoveCursorUp, &state);
// TODO: Per-widget keybinds, will fix insert_mode hack too try state.buffer.registerGlobalKeybind("J", &viMoveCursorDown, &state);
try state.buffer.registerKeybind("Ctrl+U", &clearPassword, &state);
try state.buffer.registerKeybind("K", &viMoveCursorUp, &state); try state.buffer.registerGlobalKeybind("Enter", &authenticate, &state);
try state.buffer.registerKeybind("J", &viMoveCursorDown, &state);
try state.buffer.registerKeybind("Enter", &authenticate, &state); try state.buffer.registerGlobalKeybind(state.config.shutdown_key, &shutdownCmd, &state);
try state.buffer.registerGlobalKeybind(state.config.restart_key, &restartCmd, &state);
try state.buffer.registerKeybind(state.config.shutdown_key, &shutdownCmd, &state); try state.buffer.registerGlobalKeybind(state.config.show_password_key, &togglePasswordMask, &state);
try state.buffer.registerKeybind(state.config.restart_key, &restartCmd, &state); if (state.config.sleep_cmd != null) try state.buffer.registerGlobalKeybind(state.config.sleep_key, &sleepCmd, &state);
try state.buffer.registerKeybind(state.config.show_password_key, &togglePasswordMask, &state); if (state.config.hibernate_cmd != null) try state.buffer.registerGlobalKeybind(state.config.hibernate_key, &hibernateCmd, &state);
if (state.config.sleep_cmd != null) try state.buffer.registerKeybind(state.config.sleep_key, &sleepCmd, &state); if (state.config.brightness_down_key) |key| try state.buffer.registerGlobalKeybind(key, &decreaseBrightnessCmd, &state);
if (state.config.hibernate_cmd != null) try state.buffer.registerKeybind(state.config.hibernate_key, &hibernateCmd, &state); if (state.config.brightness_up_key) |key| try state.buffer.registerGlobalKeybind(key, &increaseBrightnessCmd, &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);
if (state.config.initial_info_text) |text| { if (state.config.initial_info_text) |text| {
try state.info_line.addMessage(text, state.config.bg, state.config.fg); 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"); 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 { fn togglePasswordMask(ptr: *anyopaque) !bool {
var state: *UiState = @ptrCast(@alignCast(ptr)); var state: *UiState = @ptrCast(@alignCast(ptr));