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 {
allocator: Allocator,
active_widget_index: usize,
handlable_widgets: std.ArrayList(*Widget),
auth_fails: u64,
run: bool,
update: bool,
@@ -97,7 +99,7 @@ const UiState = struct {
saved_users: SavedUsers,
login: UserList,
password: Text,
active_input: enums.Input,
password_widget: Widget,
insert_mode: bool,
edge_margin: Position,
config: Config,
@@ -780,6 +782,8 @@ pub fn main() !void {
);
defer state.password.deinit();
state.password_widget = state.password.widget();
state.version_label = Label.init(
ly_version_str,
null,
@@ -941,12 +945,12 @@ pub fn main() !void {
state.config.auth_fails,
);
state.active_widget_index = 0;
state.auth_fails = 0;
state.run = true;
state.update = true;
state.animation_timed_out = false;
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.edge_margin = Position.init(
state.config.edge_margin,
@@ -955,6 +959,8 @@ pub fn main() !void {
// Load last saved username and desktop selection, if any
// 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.saved_users.last_username_index) |index| load_last_user: {
// 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);
}
@@ -979,6 +985,10 @@ pub fn main() !void {
// TODO: Layer system where we can put widgets in specific layers (to
// 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;
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.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.widget());
try widgets.append(state.allocator, session_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.widget());
try widgets.append(state.allocator, state.password_widget);
if (!state.config.hide_version_string) {
try widgets.append(state.allocator, state.version_label.widget());
}
@@ -1026,28 +1036,6 @@ pub fn main() !void {
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("I", &enableInsertMode);
@@ -1057,11 +1045,11 @@ pub fn main() !void {
try state.buffer.registerKeybind("Ctrl+K", &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("Down", &moveCursorDown);
try state.buffer.registerKeybind("K", &viMoveCursorDown);
try state.buffer.registerKeybind("J", &viMoseCursorDown);
try state.buffer.registerKeybind("Tab", &wrapCursor);
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);
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) {
if (state.update) {
for (widgets.items) |*widget| try widget.update(&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)});
},
}
// Reset cursor
const current_widget = getActiveWidget(&state);
current_widget.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 set cursor in active widget: {s}",
.{@errorName(err)},
);
};
try TerminalBuffer.clearScreen(false);
@@ -1237,19 +1255,20 @@ pub fn main() !void {
if (maybe_keys) |*keys| {
defer keys.deinit(state.allocator);
const current_widget = getActiveWidget(&state);
for (keys.items) |key| {
switch (state.active_input) {
.info_line => state.info_line.label.handle(key, state.insert_mode),
.session => state.session.label.handle(key, state.insert_mode),
.login => state.login.label.handle(key, state.insert_mode),
.password => state.password.handle(key, state.insert_mode) catch {
try state.info_line.addMessage(
state.lang.err_alloc,
state.config.error_bg,
state.config.error_fg,
);
},
}
current_widget.handle(key, 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 active widget: {s}",
.{@errorName(err)},
);
};
}
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 {
var state: *UiState = @ptrCast(@alignCast(ptr));
@@ -1279,7 +1308,7 @@ fn enableInsertMode(ptr: *anyopaque) !bool {
fn clearPassword(ptr: *anyopaque) !bool {
var state: *UiState = @ptrCast(@alignCast(ptr));
if (state.active_input == .password) {
if (getActiveWidget(state) == &state.password_widget) {
state.password.clear();
state.update = true;
}
@@ -1288,34 +1317,38 @@ fn clearPassword(ptr: *anyopaque) !bool {
fn moveCursorUp(ptr: *anyopaque) !bool {
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;
return false;
}
fn viMoseCursorUp(ptr: *anyopaque) !bool {
fn viMoveCursorUp(ptr: *anyopaque) !bool {
var state: *UiState = @ptrCast(@alignCast(ptr));
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;
return false;
}
fn moveCursorDown(ptr: *anyopaque) !bool {
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;
return false;
}
fn viMoveCursorDown(ptr: *anyopaque) !bool {
fn viMoseCursorDown(ptr: *anyopaque) !bool {
var state: *UiState = @ptrCast(@alignCast(ptr));
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;
return false;
}
@@ -1323,7 +1356,7 @@ fn viMoveCursorDown(ptr: *anyopaque) !bool {
fn wrapCursor(ptr: *anyopaque) !bool {
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;
return false;
}
@@ -1331,7 +1364,7 @@ fn wrapCursor(ptr: *anyopaque) !bool {
fn wrapCursorReverse(ptr: *anyopaque) !bool {
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;
return false;
}
@@ -1500,7 +1533,7 @@ fn authenticate(ptr: *anyopaque) !bool {
const auth_err = shared_err.readError();
if (auth_err) |err| {
state.auth_fails += 1;
state.active_input = .password;
setActiveWidget(state, &state.password_widget);
try state.info_line.addMessage(
getAuthErrorMsg(err, state.lang),

View File

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