Make handling inputs widget-independent

Signed-off-by: AnErrupTion <anerruption@disroot.org>
This commit is contained in:
AnErrupTion
2026-02-11 21:00:00 +01:00
parent 4a72e41e44
commit b389e379fa
2 changed files with 160 additions and 127 deletions

View File

@@ -66,6 +66,8 @@ fn ttyControlTransferSignalHandler(_: c_int) callconv(.c) void {
const UiState = struct { const UiState = struct {
allocator: Allocator, allocator: Allocator,
active_widget_index: usize,
handlable_widgets: std.ArrayList(*Widget),
auth_fails: u64, auth_fails: u64,
run: bool, run: bool,
update: bool, update: bool,
@@ -97,7 +99,7 @@ const UiState = struct {
saved_users: SavedUsers, saved_users: SavedUsers,
login: UserList, login: UserList,
password: Text, password: Text,
active_input: enums.Input, password_widget: Widget,
insert_mode: bool, insert_mode: bool,
edge_margin: Position, edge_margin: Position,
config: Config, config: Config,
@@ -780,6 +782,8 @@ pub fn main() !void {
); );
defer state.password.deinit(); defer state.password.deinit();
state.password_widget = state.password.widget();
state.version_label = Label.init( state.version_label = Label.init(
ly_version_str, ly_version_str,
null, null,
@@ -941,12 +945,12 @@ pub fn main() !void {
state.config.auth_fails, state.config.auth_fails,
); );
state.active_widget_index = 0;
state.auth_fails = 0; state.auth_fails = 0;
state.run = true; state.run = true;
state.update = true; state.update = true;
state.animation_timed_out = false; state.animation_timed_out = false;
state.animate = state.config.animation != .none; state.animate = state.config.animation != .none;
state.active_input = state.config.default_input;
state.insert_mode = !state.config.vi_mode or state.config.vi_default_mode == .insert; state.insert_mode = !state.config.vi_mode or state.config.vi_default_mode == .insert;
state.edge_margin = Position.init( state.edge_margin = Position.init(
state.config.edge_margin, state.config.edge_margin,
@@ -955,6 +959,8 @@ pub fn main() !void {
// Load last saved username and desktop selection, if any // Load last saved username and desktop selection, if any
// Skip if autologin is active to prevent overriding autologin session // Skip if autologin is active to prevent overriding autologin session
var default_input = state.config.default_input;
if (state.config.save and !state.is_autologin) { if (state.config.save and !state.is_autologin) {
if (state.saved_users.last_username_index) |index| load_last_user: { if (state.saved_users.last_username_index) |index| load_last_user: {
// If the saved index isn't valid, bail out // If the saved index isn't valid, bail out
@@ -971,7 +977,7 @@ pub fn main() !void {
} }
} }
state.active_input = .password; default_input = .password;
state.session.label.current = @min(user.session_index, state.session.label.list.items.len - 1); state.session.label.current = @min(user.session_index, state.session.label.list.items.len - 1);
} }
@@ -979,6 +985,10 @@ pub fn main() !void {
// TODO: Layer system where we can put widgets in specific layers (to // TODO: Layer system where we can put widgets in specific layers (to
// allow certain widgets to be below or above others, like animations) // allow certain widgets to be below or above others, like animations)
const info_line_widget = state.info_line.widget();
const session_widget = state.session.widget();
const login_widget = state.login.widget();
var widgets: std.ArrayList(Widget) = .empty; var widgets: std.ArrayList(Widget) = .empty;
defer widgets.deinit(state.allocator); defer widgets.deinit(state.allocator);
@@ -1012,13 +1022,13 @@ pub fn main() !void {
try widgets.append(state.allocator, state.capslock_label.widget()); try widgets.append(state.allocator, state.capslock_label.widget());
} }
try widgets.append(state.allocator, state.box.widget()); try widgets.append(state.allocator, state.box.widget());
try widgets.append(state.allocator, state.info_line.widget()); try widgets.append(state.allocator, info_line_widget);
try widgets.append(state.allocator, state.session_specifier_label.widget()); try widgets.append(state.allocator, state.session_specifier_label.widget());
try widgets.append(state.allocator, state.session.widget()); try widgets.append(state.allocator, session_widget);
try widgets.append(state.allocator, state.login_label.widget()); try widgets.append(state.allocator, state.login_label.widget());
try widgets.append(state.allocator, state.login.widget()); try widgets.append(state.allocator, login_widget);
try widgets.append(state.allocator, state.password_label.widget()); try widgets.append(state.allocator, state.password_label.widget());
try widgets.append(state.allocator, state.password.widget()); try widgets.append(state.allocator, state.password_widget);
if (!state.config.hide_version_string) { if (!state.config.hide_version_string) {
try widgets.append(state.allocator, state.version_label.widget()); try widgets.append(state.allocator, state.version_label.widget());
} }
@@ -1026,28 +1036,6 @@ pub fn main() !void {
try widgets.append(state.allocator, cascade.widget()); try widgets.append(state.allocator, cascade.widget());
} }
// Position components and place cursor accordingly
for (widgets.items) |*widget| try widget.update(&state);
positionComponents(&state);
switch (state.active_input) {
.info_line => state.info_line.label.handle(null, state.insert_mode),
.session => state.session.label.handle(null, state.insert_mode),
.login => state.login.label.handle(null, state.insert_mode),
.password => state.password.handle(null, state.insert_mode) catch |err| {
try state.info_line.addMessage(
state.lang.err_alloc,
state.config.error_bg,
state.config.error_fg,
);
try state.log_file.err(
"tui",
"failed to handle password input: {s}",
.{@errorName(err)},
);
},
}
try state.buffer.registerKeybind("Esc", &disableInsertMode); try state.buffer.registerKeybind("Esc", &disableInsertMode);
try state.buffer.registerKeybind("I", &enableInsertMode); try state.buffer.registerKeybind("I", &enableInsertMode);
@@ -1057,11 +1045,11 @@ pub fn main() !void {
try state.buffer.registerKeybind("Ctrl+K", &moveCursorUp); try state.buffer.registerKeybind("Ctrl+K", &moveCursorUp);
try state.buffer.registerKeybind("Up", &moveCursorUp); try state.buffer.registerKeybind("Up", &moveCursorUp);
try state.buffer.registerKeybind("J", &viMoseCursorUp); try state.buffer.registerKeybind("K", &viMoveCursorUp);
try state.buffer.registerKeybind("Ctrl+J", &moveCursorDown); try state.buffer.registerKeybind("Ctrl+J", &moveCursorDown);
try state.buffer.registerKeybind("Down", &moveCursorDown); try state.buffer.registerKeybind("Down", &moveCursorDown);
try state.buffer.registerKeybind("K", &viMoveCursorDown); try state.buffer.registerKeybind("J", &viMoseCursorDown);
try state.buffer.registerKeybind("Tab", &wrapCursor); try state.buffer.registerKeybind("Tab", &wrapCursor);
try state.buffer.registerKeybind("Shift+Tab", &wrapCursorReverse); try state.buffer.registerKeybind("Shift+Tab", &wrapCursorReverse);
@@ -1104,21 +1092,51 @@ pub fn main() !void {
); );
} }
// Position components and place cursor accordingly
if (state.is_autologin) _ = try authenticate(&state); if (state.is_autologin) _ = try authenticate(&state);
const active_widget = switch (default_input) {
.info_line => info_line_widget,
.session => session_widget,
.login => login_widget,
.password => state.password_widget,
};
// Run the event loop
state.handlable_widgets = .empty;
defer state.handlable_widgets.deinit(state.allocator);
var i: usize = 0;
for (widgets.items) |*widget| {
if (widget.vtable.handle_fn != null) {
try state.handlable_widgets.append(state.allocator, widget);
if (widget.id == active_widget.id) state.active_widget_index = i;
i += 1;
}
}
for (widgets.items) |*widget| try widget.update(&state);
positionComponents(&state);
while (state.run) { while (state.run) {
if (state.update) { if (state.update) {
for (widgets.items) |*widget| try widget.update(&state); for (widgets.items) |*widget| try widget.update(&state);
switch (state.active_input) { // Reset cursor
.info_line => state.info_line.label.handle(null, state.insert_mode), const current_widget = getActiveWidget(&state);
.session => state.session.label.handle(null, state.insert_mode), current_widget.handle(null, state.insert_mode) catch |err| {
.login => state.login.label.handle(null, state.insert_mode), try state.info_line.addMessage(
.password => state.password.handle(null, state.insert_mode) catch |err| { state.lang.err_alloc,
try state.info_line.addMessage(state.lang.err_alloc, state.config.error_bg, state.config.error_fg); state.config.error_bg,
try state.log_file.err("tui", "failed to handle password input: {s}", .{@errorName(err)}); state.config.error_fg,
}, );
} try state.log_file.err(
"tui",
"failed to set cursor in active widget: {s}",
.{@errorName(err)},
);
};
try TerminalBuffer.clearScreen(false); try TerminalBuffer.clearScreen(false);
@@ -1237,19 +1255,20 @@ pub fn main() !void {
if (maybe_keys) |*keys| { if (maybe_keys) |*keys| {
defer keys.deinit(state.allocator); defer keys.deinit(state.allocator);
const current_widget = getActiveWidget(&state);
for (keys.items) |key| { for (keys.items) |key| {
switch (state.active_input) { current_widget.handle(key, state.insert_mode) catch |err| {
.info_line => state.info_line.label.handle(key, state.insert_mode), try state.info_line.addMessage(
.session => state.session.label.handle(key, state.insert_mode), state.lang.err_alloc,
.login => state.login.label.handle(key, state.insert_mode), state.config.error_bg,
.password => state.password.handle(key, state.insert_mode) catch { state.config.error_fg,
try state.info_line.addMessage( );
state.lang.err_alloc, try state.log_file.err(
state.config.error_bg, "tui",
state.config.error_fg, "failed to handle active widget: {s}",
); .{@errorName(err)},
}, );
} };
} }
state.update = true; state.update = true;
@@ -1257,6 +1276,16 @@ pub fn main() !void {
} }
} }
fn getActiveWidget(state: *UiState) *Widget {
return state.handlable_widgets.items[state.active_widget_index];
}
fn setActiveWidget(state: *UiState, widget: *Widget) void {
for (state.handlable_widgets.items, 0..) |widg, i| {
if (widg.id == widget.id) state.active_widget_index = i;
}
}
fn disableInsertMode(ptr: *anyopaque) !bool { fn disableInsertMode(ptr: *anyopaque) !bool {
var state: *UiState = @ptrCast(@alignCast(ptr)); var state: *UiState = @ptrCast(@alignCast(ptr));
@@ -1279,7 +1308,7 @@ fn enableInsertMode(ptr: *anyopaque) !bool {
fn clearPassword(ptr: *anyopaque) !bool { fn clearPassword(ptr: *anyopaque) !bool {
var state: *UiState = @ptrCast(@alignCast(ptr)); var state: *UiState = @ptrCast(@alignCast(ptr));
if (state.active_input == .password) { if (getActiveWidget(state) == &state.password_widget) {
state.password.clear(); state.password.clear();
state.update = true; state.update = true;
} }
@@ -1288,34 +1317,38 @@ fn clearPassword(ptr: *anyopaque) !bool {
fn moveCursorUp(ptr: *anyopaque) !bool { fn moveCursorUp(ptr: *anyopaque) !bool {
var state: *UiState = @ptrCast(@alignCast(ptr)); var state: *UiState = @ptrCast(@alignCast(ptr));
if (state.active_widget_index == 0) return false;
state.active_input.move(true, false); state.active_widget_index -= 1;
state.update = true; state.update = true;
return false; return false;
} }
fn viMoseCursorUp(ptr: *anyopaque) !bool { fn viMoveCursorUp(ptr: *anyopaque) !bool {
var state: *UiState = @ptrCast(@alignCast(ptr)); var state: *UiState = @ptrCast(@alignCast(ptr));
if (state.insert_mode) return true; if (state.insert_mode) return true;
if (state.active_widget_index == 0) return false;
state.active_input.move(false, false); state.active_widget_index -= 1;
state.update = true; state.update = true;
return false; return false;
} }
fn moveCursorDown(ptr: *anyopaque) !bool { fn moveCursorDown(ptr: *anyopaque) !bool {
var state: *UiState = @ptrCast(@alignCast(ptr)); var state: *UiState = @ptrCast(@alignCast(ptr));
if (state.active_widget_index == state.handlable_widgets.items.len - 1) return false;
state.active_input.move(false, false); state.active_widget_index += 1;
state.update = true; state.update = true;
return false; return false;
} }
fn viMoveCursorDown(ptr: *anyopaque) !bool { fn viMoseCursorDown(ptr: *anyopaque) !bool {
var state: *UiState = @ptrCast(@alignCast(ptr)); var state: *UiState = @ptrCast(@alignCast(ptr));
if (state.insert_mode) return true; if (state.insert_mode) return true;
if (state.active_widget_index == state.handlable_widgets.items.len - 1) return false;
state.active_input.move(true, false); state.active_widget_index += 1;
state.update = true; state.update = true;
return false; return false;
} }
@@ -1323,7 +1356,7 @@ fn viMoveCursorDown(ptr: *anyopaque) !bool {
fn wrapCursor(ptr: *anyopaque) !bool { fn wrapCursor(ptr: *anyopaque) !bool {
var state: *UiState = @ptrCast(@alignCast(ptr)); var state: *UiState = @ptrCast(@alignCast(ptr));
state.active_input.move(false, true); state.active_widget_index = (state.active_widget_index + 1) % state.handlable_widgets.items.len;
state.update = true; state.update = true;
return false; return false;
} }
@@ -1331,7 +1364,7 @@ fn wrapCursor(ptr: *anyopaque) !bool {
fn wrapCursorReverse(ptr: *anyopaque) !bool { fn wrapCursorReverse(ptr: *anyopaque) !bool {
var state: *UiState = @ptrCast(@alignCast(ptr)); var state: *UiState = @ptrCast(@alignCast(ptr));
state.active_input.move(true, true); state.active_widget_index = (state.active_widget_index - 1) % state.handlable_widgets.items.len;
state.update = true; state.update = true;
return false; return false;
} }
@@ -1500,7 +1533,7 @@ fn authenticate(ptr: *anyopaque) !bool {
const auth_err = shared_err.readError(); const auth_err = shared_err.readError();
if (auth_err) |err| { if (auth_err) |err| {
state.auth_fails += 1; state.auth_fails += 1;
state.active_input = .password; setActiveWidget(state, &state.password_widget);
try state.info_line.addMessage( try state.info_line.addMessage(
getAuthErrorMsg(err, state.lang), getAuthErrorMsg(err, state.lang),

View File

@@ -4,13 +4,14 @@ const keyboard = @import("keyboard.zig");
const TerminalBuffer = @import("TerminalBuffer.zig"); const TerminalBuffer = @import("TerminalBuffer.zig");
const VTable = struct { const VTable = struct {
deinit_fn: *const fn (ptr: *anyopaque) void, deinit_fn: ?*const fn (ptr: *anyopaque) void,
realloc_fn: *const fn (ptr: *anyopaque) anyerror!void, realloc_fn: ?*const fn (ptr: *anyopaque) anyerror!void,
draw_fn: *const fn (ptr: *anyopaque) void, draw_fn: *const fn (ptr: *anyopaque) void,
update_fn: *const fn (ptr: *anyopaque, ctx: *anyopaque) anyerror!void, update_fn: ?*const fn (ptr: *anyopaque, ctx: *anyopaque) anyerror!void,
handle_fn: *const fn (ptr: *anyopaque, maybe_key: ?keyboard.Key, insert_mode: bool) anyerror!void, handle_fn: ?*const fn (ptr: *anyopaque, maybe_key: ?keyboard.Key, insert_mode: bool) anyerror!void,
}; };
id: u64,
pointer: *anyopaque, pointer: *anyopaque,
vtable: VTable, vtable: VTable,
@@ -18,7 +19,7 @@ pub fn init(
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,
comptime draw_fn: ?fn (ptr: @TypeOf(pointer)) void, comptime draw_fn: fn (ptr: @TypeOf(pointer)) void,
comptime update_fn: ?fn (ptr: @TypeOf(pointer), ctx: *anyopaque) anyerror!void, comptime update_fn: ?fn (ptr: @TypeOf(pointer), ctx: *anyopaque) anyerror!void,
comptime handle_fn: ?fn (ptr: @TypeOf(pointer), maybe_key: ?keyboard.Key, insert_mode: bool) anyerror!void, comptime handle_fn: ?fn (ptr: @TypeOf(pointer), maybe_key: ?keyboard.Key, insert_mode: bool) anyerror!void,
) Widget { ) Widget {
@@ -27,73 +28,64 @@ pub fn init(
pub fn deinitImpl(ptr: *anyopaque) void { pub fn deinitImpl(ptr: *anyopaque) void {
const impl: Pointer = @ptrCast(@alignCast(ptr)); const impl: Pointer = @ptrCast(@alignCast(ptr));
if (deinit_fn) |func| { return @call(
return @call( .always_inline,
.always_inline, deinit_fn.?,
func, .{impl},
.{impl}, );
);
}
} }
pub fn reallocImpl(ptr: *anyopaque) !void { pub fn reallocImpl(ptr: *anyopaque) !void {
const impl: Pointer = @ptrCast(@alignCast(ptr)); const impl: Pointer = @ptrCast(@alignCast(ptr));
if (realloc_fn) |func| { return @call(
return @call( .always_inline,
.always_inline, realloc_fn.?,
func, .{impl},
.{impl}, );
);
}
} }
pub fn drawImpl(ptr: *anyopaque) void { pub fn drawImpl(ptr: *anyopaque) void {
const impl: Pointer = @ptrCast(@alignCast(ptr)); const impl: Pointer = @ptrCast(@alignCast(ptr));
if (draw_fn) |func| { return @call(
return @call( .always_inline,
.always_inline, draw_fn,
func, .{impl},
.{impl}, );
);
}
} }
pub fn updateImpl(ptr: *anyopaque, ctx: *anyopaque) !void { pub fn updateImpl(ptr: *anyopaque, ctx: *anyopaque) !void {
const impl: Pointer = @ptrCast(@alignCast(ptr)); const impl: Pointer = @ptrCast(@alignCast(ptr));
if (update_fn) |func| { return @call(
return @call( .always_inline,
.always_inline, update_fn.?,
func, .{ impl, ctx },
.{ impl, ctx }, );
);
}
} }
pub fn handleImpl(ptr: *anyopaque, maybe_key: ?keyboard.Key, insert_mode: bool) !void { pub fn handleImpl(ptr: *anyopaque, maybe_key: ?keyboard.Key, insert_mode: bool) !void {
const impl: Pointer = @ptrCast(@alignCast(ptr)); const impl: Pointer = @ptrCast(@alignCast(ptr));
if (handle_fn) |func| { return @call(
return @call( .always_inline,
.always_inline, handle_fn.?,
func, .{ impl, maybe_key, insert_mode },
.{ impl, maybe_key, insert_mode }, );
);
}
} }
const vtable = VTable{ const vtable = VTable{
.deinit_fn = deinitImpl, .deinit_fn = if (deinit_fn != null) deinitImpl else null,
.realloc_fn = reallocImpl, .realloc_fn = if (realloc_fn != null) reallocImpl else null,
.draw_fn = drawImpl, .draw_fn = drawImpl,
.update_fn = updateImpl, .update_fn = if (update_fn != null) updateImpl else null,
.handle_fn = handleImpl, .handle_fn = if (handle_fn != null) handleImpl else null,
}; };
}; };
return .{ return .{
.id = @intFromPtr(Impl.vtable.draw_fn),
.pointer = pointer, .pointer = pointer,
.vtable = Impl.vtable, .vtable = Impl.vtable,
}; };
@@ -102,27 +94,31 @@ pub fn init(
pub fn deinit(self: *Widget) void { pub fn deinit(self: *Widget) void {
const impl: @TypeOf(self.pointer) = @ptrCast(@alignCast(self.pointer)); const impl: @TypeOf(self.pointer) = @ptrCast(@alignCast(self.pointer));
return @call( if (self.vtable.deinit_fn) |deinit_fn| {
.auto, return @call(
self.vtable.deinit_fn, .auto,
.{impl}, deinit_fn,
); .{impl},
);
}
} }
pub fn realloc(self: *Widget) !void { pub fn realloc(self: *Widget) !void {
const impl: @TypeOf(self.pointer) = @ptrCast(@alignCast(self.pointer)); const impl: @TypeOf(self.pointer) = @ptrCast(@alignCast(self.pointer));
return @call( if (self.vtable.realloc_fn) |realloc_fn| {
.auto, return @call(
self.vtable.realloc_fn, .auto,
.{impl}, realloc_fn,
); .{impl},
);
}
} }
pub fn draw(self: *Widget) void { pub fn draw(self: *Widget) void {
const impl: @TypeOf(self.pointer) = @ptrCast(@alignCast(self.pointer)); const impl: @TypeOf(self.pointer) = @ptrCast(@alignCast(self.pointer));
return @call( @call(
.auto, .auto,
self.vtable.draw_fn, self.vtable.draw_fn,
.{impl}, .{impl},
@@ -132,19 +128,23 @@ pub fn draw(self: *Widget) void {
pub fn update(self: *Widget, ctx: *anyopaque) !void { pub fn update(self: *Widget, ctx: *anyopaque) !void {
const impl: @TypeOf(self.pointer) = @ptrCast(@alignCast(self.pointer)); const impl: @TypeOf(self.pointer) = @ptrCast(@alignCast(self.pointer));
return @call( if (self.vtable.update_fn) |update_fn| {
.auto, return @call(
self.vtable.update_fn, .auto,
.{ impl, ctx }, update_fn,
); .{ impl, ctx },
);
}
} }
pub fn handle(self: *Widget, maybe_key: ?keyboard.Key, insert_mode: bool) !void { pub fn handle(self: *Widget, maybe_key: ?keyboard.Key, insert_mode: bool) !void {
const impl: @TypeOf(self.pointer) = @ptrCast(@alignCast(self.pointer)); const impl: @TypeOf(self.pointer) = @ptrCast(@alignCast(self.pointer));
return @call( if (self.vtable.handle_fn) |handle_fn| {
.auto, return @call(
self.vtable.handle_fn, .auto,
.{ impl, maybe_key, insert_mode }, handle_fn,
); .{ impl, maybe_key, insert_mode },
);
}
} }