From 852a602032743c79017be93a5dcf5b28edbb0898 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Mon, 9 Feb 2026 11:11:00 +0100 Subject: [PATCH] Support more configurable keybindings (closes #679) Signed-off-by: AnErrupTion --- res/config.ini | 8 +- src/main.zig | 774 +++++++++++++++++++++++-------------- src/tui/TerminalBuffer.zig | 67 +++- src/tui/keyboard.zig | 735 +++++++++++++++++++++++++++++++++++ 4 files changed, 1286 insertions(+), 298 deletions(-) create mode 100644 src/tui/keyboard.zig diff --git a/res/config.ini b/res/config.ini index 5a10e35..b3f28c5 100644 --- a/res/config.ini +++ b/res/config.ini @@ -233,7 +233,7 @@ gameoflife_initial_density = 0.4 # Command executed when pressing hibernate key (can be null) hibernate_cmd = null -# Specifies the key used for hibernate (F1-F12) +# Specifies the key used for hibernate hibernate_key = F4 # Remove main box borders @@ -304,7 +304,7 @@ path = /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin # Command executed when pressing restart_key restart_cmd = /sbin/shutdown -r now -# Specifies the key used for restart (F1-F12) +# Specifies the key used for restart restart_key = F2 # Save the current desktop and login as defaults, and load them on startup @@ -328,13 +328,13 @@ setup_cmd = $CONFIG_DIRECTORY/ly/setup.sh # Command executed when pressing shutdown_key shutdown_cmd = /sbin/shutdown $PLATFORM_SHUTDOWN_ARG now -# Specifies the key used for shutdown (F1-F12) +# Specifies the key used for shutdown shutdown_key = F1 # Command executed when pressing sleep key (can be null) sleep_cmd = null -# Specifies the key used for sleep (F1-F12) +# Specifies the key used for sleep sleep_key = F3 # Command executed when starting Ly (before the TTY is taken control of) diff --git a/src/main.zig b/src/main.zig index bb114ce..382bd66 100644 --- a/src/main.zig +++ b/src/main.zig @@ -1,4 +1,5 @@ const std = @import("std"); +const Allocator = std.mem.Allocator; const StringList = std.ArrayListUnmanaged([]const u8); const temporary_allocator = std.heap.page_allocator; const builtin = @import("builtin"); @@ -66,8 +67,13 @@ fn ttyControlTransferSignalHandler(_: c_int) callconv(.c) void { } const UiState = struct { + allocator: Allocator, auth_fails: u64, + run: bool, update: bool, + is_autologin: bool, + use_kmscon_vt: bool, + active_tty: u8, buffer: *TerminalBuffer, labels_max_length: usize, animation_timed_out: bool, @@ -91,6 +97,7 @@ const UiState = struct { info_line: *InfoLine, animate: bool, session: *Session, + saved_users: SavedUsers, login: *UserList, password: *Text, active_input: enums.Input, @@ -99,15 +106,18 @@ const UiState = struct { config: Config, lang: Lang, log_file: *LogFile, + save_path: []const u8, + old_save_path: ?[]const u8, battery_buf: [16:0]u8, bigclock_format_buf: [16:0]u8, clock_buf: [64:0]u8, bigclock_buf: [32:0]u8, }; +var shutdown = false; +var restart = false; + pub fn main() !void { - var shutdown = false; - var restart = false; var shutdown_cmd: []const u8 = undefined; var restart_cmd: []const u8 = undefined; var commands_allocated = false; @@ -324,10 +334,15 @@ pub fn main() !void { .full_color = config.full_color, .is_tty = true, }; - var buffer = try TerminalBuffer.init(buffer_options, &log_file, random); + var buffer = try TerminalBuffer.init( + allocator, + buffer_options, + &log_file, + random, + ); defer { log_file.info("tui", "shutting down terminal buffer", .{}) catch {}; - TerminalBuffer.shutdownStatic(); + buffer.deinit(); } const act = std.posix.Sigaction{ @@ -707,10 +722,28 @@ pub fn main() !void { is_autologin = true; } + // Switch to selected TTY + const active_tty = interop.getActiveTty(allocator, use_kmscon_vt) catch |err| no_tty_found: { + try info_line.addMessage(lang.err_get_active_tty, config.error_bg, config.error_fg); + try log_file.err("sys", "failed to get active tty: {s}", .{@errorName(err)}); + break :no_tty_found build_options.fallback_tty; + }; + if (!use_kmscon_vt) { + interop.switchTty(active_tty) catch |err| { + try info_line.addMessage(lang.err_switch_tty, config.error_bg, config.error_fg); + try log_file.err("sys", "failed to switch to tty {d}: {s}", .{ active_tty, @errorName(err) }); + }; + } + var animation: ?Animation = null; var state = UiState{ + .allocator = allocator, .auth_fails = 0, + .run = true, .update = true, + .is_autologin = is_autologin, + .use_kmscon_vt = use_kmscon_vt, + .active_tty = active_tty, .buffer = &buffer, .labels_max_length = labels_max_length, .animation_timed_out = false, @@ -734,6 +767,7 @@ pub fn main() !void { .info_line = &info_line, .animate = config.animation != .none, .session = &session, + .saved_users = saved_users, .login = &login, .password = &password, .active_input = config.default_input, @@ -745,6 +779,8 @@ pub fn main() !void { .config = config, .lang = lang, .log_file = &log_file, + .save_path = save_path, + .old_save_path = if (old_save_parser != null) old_save_path else null, .battery_buf = undefined, .bigclock_format_buf = undefined, .clock_buf = undefined, @@ -775,7 +811,7 @@ pub fn main() !void { } } - // Position components + // Position components and place cursor accordingly try updateComponents(&state); positionComponents(&state); @@ -815,31 +851,37 @@ pub fn main() !void { } defer if (animation) |*a| a.deinit(); - const shutdown_key = try std.fmt.parseInt(u8, config.shutdown_key[1..], 10); - const restart_key = try std.fmt.parseInt(u8, config.restart_key[1..], 10); - const sleep_key = try std.fmt.parseInt(u8, config.sleep_key[1..], 10); - const hibernate_key = try std.fmt.parseInt(u8, config.hibernate_key[1..], 10); - const brightness_down_key = if (config.brightness_down_key) |key| try std.fmt.parseInt(u8, key[1..], 10) else null; - const brightness_up_key = if (config.brightness_up_key) |key| try std.fmt.parseInt(u8, key[1..], 10) else null; + try buffer.registerKeybind("Esc", &disableInsertMode); + try buffer.registerKeybind("I", &enableInsertMode); + + try buffer.registerKeybind("Ctrl+C", &quit); + + try buffer.registerKeybind("Ctrl+U", &clearPassword); + + try buffer.registerKeybind("Ctrl+K", &moveCursorUp); + try buffer.registerKeybind("Up", &moveCursorUp); + try buffer.registerKeybind("J", &viMoseCursorUp); + + try buffer.registerKeybind("Ctrl+J", &moveCursorDown); + try buffer.registerKeybind("Down", &moveCursorDown); + try buffer.registerKeybind("K", &viMoveCursorDown); + + try buffer.registerKeybind("Tab", &wrapCursor); + try buffer.registerKeybind("Shift+Tab", &wrapCursorReverse); + + try buffer.registerKeybind("Enter", &authenticate); + + try buffer.registerKeybind(config.shutdown_key, &shutdownCmd); + try buffer.registerKeybind(config.restart_key, &restartCmd); + if (config.sleep_cmd != null) try buffer.registerKeybind(config.sleep_key, &sleepCmd); + if (config.hibernate_cmd != null) try buffer.registerKeybind(config.hibernate_key, &hibernateCmd); + if (config.brightness_down_key) |key| try buffer.registerKeybind(key, &decreaseBrightnessCmd); + if (config.brightness_up_key) |key| try buffer.registerKeybind(key, &increaseBrightnessCmd); var event: termbox.tb_event = undefined; - var run = true; var inactivity_time_start = try interop.getTimeOfDay(); var inactivity_cmd_ran = false; - // Switch to selected TTY - const active_tty = interop.getActiveTty(allocator, use_kmscon_vt) catch |err| no_tty_found: { - try info_line.addMessage(lang.err_get_active_tty, config.error_bg, config.error_fg); - try log_file.err("sys", "failed to get active tty: {s}", .{@errorName(err)}); - break :no_tty_found build_options.fallback_tty; - }; - if (!use_kmscon_vt) { - interop.switchTty(active_tty) catch |err| { - try info_line.addMessage(lang.err_switch_tty, config.error_bg, config.error_fg); - try log_file.err("sys", "failed to switch to tty {d}: {s}", .{ active_tty, @errorName(err) }); - }; - } - if (config.initial_info_text) |text| { try info_line.addMessage(text, config.bg, config.fg); } else get_host_name: { @@ -853,7 +895,7 @@ pub fn main() !void { try info_line.addMessage(hostname, config.bg, config.fg); } - while (run) { + while (state.run) { if (state.update) { try updateComponents(&state); @@ -935,6 +977,9 @@ pub fn main() !void { if (event_error < 0) continue; } + // Input of some kind was detected, so reset the inactivity timer + inactivity_time_start = try interop.getTimeOfDay(); + if (event.type == termbox.TB_EVENT_RESIZE) { state.buffer.width = TerminalBuffer.getWidthStatic(); state.buffer.height = TerminalBuffer.getHeightStatic(); @@ -952,269 +997,416 @@ pub fn main() !void { continue; } - // Input of some kind was detected, so reset the inactivity timer - inactivity_time_start = try interop.getTimeOfDay(); + const passthrough_event = try buffer.handleKeybind( + allocator, + event, + &state, + ); + if (passthrough_event) { + switch (state.active_input) { + .info_line => info_line.label.handle(&event, state.insert_mode), + .session => session.label.handle(&event, state.insert_mode), + .login => login.label.handle(&event, state.insert_mode), + .password => password.handle(&event, state.insert_mode) catch { + try info_line.addMessage( + lang.err_alloc, + config.error_bg, + config.error_fg, + ); + }, + } - switch (event.key) { - termbox.TB_KEY_ESC => { - if (config.vi_mode and state.insert_mode) { - state.insert_mode = false; - state.update = true; - } - }, - termbox.TB_KEY_F12...termbox.TB_KEY_F1 => { - const pressed_key = 0xFFFF - event.key + 1; - if (pressed_key == shutdown_key) { - shutdown = true; - run = false; - } else if (pressed_key == restart_key) { - restart = true; - run = false; - } else if (pressed_key == sleep_key) { - if (config.sleep_cmd) |sleep_cmd| { - var sleep = std.process.Child.init(&[_][]const u8{ "/bin/sh", "-c", sleep_cmd }, allocator); - sleep.stdout_behavior = .Ignore; - sleep.stderr_behavior = .Ignore; - - handle_sleep_cmd: { - const process_result = sleep.spawnAndWait() catch { - break :handle_sleep_cmd; - }; - if (process_result.Exited != 0) { - try info_line.addMessage(lang.err_sleep, config.error_bg, config.error_fg); - try log_file.err("sys", "failed to execute sleep command: exit code {d}", .{process_result.Exited}); - } - } - } - } else if (pressed_key == hibernate_key) { - if (config.hibernate_cmd) |hibernate_cmd| { - var hibernate = std.process.Child.init(&[_][]const u8{ "/bin/sh", "-c", hibernate_cmd }, allocator); - hibernate.stdout_behavior = .Ignore; - hibernate.stderr_behavior = .Ignore; - - handle_hibernate_cmd: { - const process_result = hibernate.spawnAndWait() catch { - break :handle_hibernate_cmd; - }; - if (process_result.Exited != 0) { - try info_line.addMessage(lang.err_hibernate, config.error_bg, config.error_fg); - try log_file.err("sys", "failed to execute hibernate command: exit code {d}", .{process_result.Exited}); - } - } - } - } else if (brightness_down_key != null and pressed_key == brightness_down_key.?) { - adjustBrightness(allocator, config.brightness_down_cmd) catch |err| { - try info_line.addMessage(lang.err_brightness_change, config.error_bg, config.error_fg); - try log_file.err("sys", "failed to change brightness: {s}", .{@errorName(err)}); - }; - } else if (brightness_up_key != null and pressed_key == brightness_up_key.?) { - adjustBrightness(allocator, config.brightness_up_cmd) catch |err| { - try info_line.addMessage(lang.err_brightness_change, config.error_bg, config.error_fg); - try log_file.err("sys", "failed to change brightness: {s}", .{@errorName(err)}); - }; - } - }, - termbox.TB_KEY_CTRL_C => run = false, - termbox.TB_KEY_CTRL_U => if (state.active_input == .password) { - password.clear(); - state.update = true; - }, - termbox.TB_KEY_CTRL_K, termbox.TB_KEY_ARROW_UP => { - state.active_input.move(true, false); - state.update = true; - }, - termbox.TB_KEY_CTRL_J, termbox.TB_KEY_ARROW_DOWN => { - state.active_input.move(false, false); - state.update = true; - }, - termbox.TB_KEY_TAB => { - state.active_input.move(false, true); - state.update = true; - }, - termbox.TB_KEY_BACK_TAB => { - state.active_input.move(true, true); - state.update = true; - }, - termbox.TB_KEY_ENTER => authenticate: { - try log_file.info("auth", "starting authentication", .{}); - - if (!config.allow_empty_password and password.text.items.len == 0) { - // Let's not log this message for security reasons - try info_line.addMessage(lang.err_empty_password, config.error_bg, config.error_fg); - info_line.clearRendered(allocator) catch |err| { - try info_line.addMessage(lang.err_alloc, config.error_bg, config.error_fg); - try log_file.err("tui", "failed to clear info line: {s}", .{@errorName(err)}); - }; - info_line.label.draw(); - _ = TerminalBuffer.presentBufferStatic(); - break :authenticate; - } - - try info_line.addMessage(lang.authenticating, config.bg, config.fg); - info_line.clearRendered(allocator) catch |err| { - try info_line.addMessage(lang.err_alloc, config.error_bg, config.error_fg); - try log_file.err("tui", "failed to clear info line: {s}", .{@errorName(err)}); - }; - info_line.label.draw(); - _ = TerminalBuffer.presentBufferStatic(); - - if (config.save) save_last_settings: { - // It isn't worth cluttering the code with precise error - // handling, so let's just report a generic error message, - // that should be good enough for debugging anyway. - errdefer log_file.err("conf", "failed to save current user data", .{}) catch {}; - - var file = std.fs.cwd().createFile(save_path, .{}) catch |err| { - log_file.err("sys", "failed to create save file: {s}", .{@errorName(err)}) catch break :save_last_settings; - break :save_last_settings; - }; - defer file.close(); - - var file_buffer: [256]u8 = undefined; - var file_writer = file.writer(&file_buffer); - var writer = &file_writer.interface; - - try writer.print("{d}\n", .{login.label.current}); - for (saved_users.user_list.items) |user| { - try writer.print("{s}:{d}\n", .{ user.username, user.session_index }); - } - try writer.flush(); - - // Delete previous save file if it exists - if (migrator.maybe_save_file) |path| { - std.fs.cwd().deleteFile(path) catch {}; - } else if (old_save_parser != null) { - std.fs.cwd().deleteFile(old_save_path) catch {}; - } - } - - var shared_err = try SharedError.init(); - defer shared_err.deinit(); - - { - log_file.deinit(); - - session_pid = try std.posix.fork(); - if (session_pid == 0) { - const current_environment = session.label.list.items[session.label.current].environment; - - // Use auto_login_service for autologin, otherwise use configured service - const service_name = if (is_autologin) config.auto_login_service else config.service_name; - const password_text = if (is_autologin) "" else password.text.items; - - const auth_options = auth.AuthOptions{ - .tty = active_tty, - .service_name = service_name, - .path = config.path, - .session_log = config.session_log, - .xauth_cmd = config.xauth_cmd, - .setup_cmd = config.setup_cmd, - .login_cmd = config.login_cmd, - .x_cmd = config.x_cmd, - .x_vt = config.x_vt, - .session_pid = session_pid, - .use_kmscon_vt = use_kmscon_vt, - }; - - // Signal action to give up control on the TTY - const tty_control_transfer_act = std.posix.Sigaction{ - .handler = .{ .handler = &ttyControlTransferSignalHandler }, - .mask = std.posix.sigemptyset(), - .flags = 0, - }; - std.posix.sigaction(std.posix.SIG.CHLD, &tty_control_transfer_act, null); - - try log_file.reinit(); - - auth.authenticate(allocator, &log_file, auth_options, current_environment, login.getCurrentUsername(), password_text) catch |err| { - shared_err.writeError(err); - - log_file.deinit(); - std.process.exit(1); - }; - - log_file.deinit(); - std.process.exit(0); - } - - _ = std.posix.waitpid(session_pid, 0); - // HACK: It seems like the session process is not exiting immediately after the waitpid call. - // This is a workaround to ensure the session process has exited before re-initializing the TTY. - std.Thread.sleep(std.time.ns_per_s * 1); - session_pid = -1; - - try log_file.reinit(); - } - - try buffer.reclaim(); - - const auth_err = shared_err.readError(); - if (auth_err) |err| { - state.auth_fails += 1; - state.active_input = .password; - - try info_line.addMessage(getAuthErrorMsg(err, lang), config.error_bg, config.error_fg); - try log_file.err("auth", "failed to authenticate: {s}", .{@errorName(err)}); - - if (config.clear_password or err != error.PamAuthError) password.clear(); - } else { - if (config.logout_cmd) |logout_cmd| { - var logout_process = std.process.Child.init(&[_][]const u8{ "/bin/sh", "-c", logout_cmd }, allocator); - _ = logout_process.spawnAndWait() catch .{}; - } - - password.clear(); - is_autologin = false; - try info_line.addMessage(lang.logout, config.bg, config.fg); - try log_file.info("auth", "logged out", .{}); - } - - if (config.auth_fails == 0 or state.auth_fails < config.auth_fails) { - try TerminalBuffer.clearScreenStatic(true); - state.update = true; - } - - // Restore the cursor - TerminalBuffer.setCursorStatic(0, 0); - _ = TerminalBuffer.presentBufferStatic(); - }, - else => { - if (!state.insert_mode) { - switch (event.ch) { - 'k' => { - state.active_input.move(true, false); - state.update = true; - continue; - }, - 'j' => { - state.active_input.move(false, false); - state.update = true; - continue; - }, - 'i' => { - state.insert_mode = true; - state.update = true; - continue; - }, - else => {}, - } - } - - switch (state.active_input) { - .info_line => info_line.label.handle(&event, state.insert_mode), - .session => session.label.handle(&event, state.insert_mode), - .login => login.label.handle(&event, state.insert_mode), - .password => password.handle(&event, state.insert_mode) catch { - try info_line.addMessage(lang.err_alloc, config.error_bg, config.error_fg); - }, - } - - state.update = true; - }, + state.update = true; } } } +fn disableInsertMode(ptr: *anyopaque) !bool { + var state: *UiState = @ptrCast(@alignCast(ptr)); + + if (state.config.vi_mode and state.insert_mode) { + state.insert_mode = false; + state.update = true; + } + return false; +} + +fn enableInsertMode(ptr: *anyopaque) !bool { + var state: *UiState = @ptrCast(@alignCast(ptr)); + if (state.insert_mode) return true; + + state.insert_mode = true; + state.update = true; + return false; +} + +fn clearPassword(ptr: *anyopaque) !bool { + var state: *UiState = @ptrCast(@alignCast(ptr)); + + if (state.active_input == .password) { + state.password.clear(); + state.update = true; + } + return false; +} + +fn moveCursorUp(ptr: *anyopaque) !bool { + var state: *UiState = @ptrCast(@alignCast(ptr)); + + state.active_input.move(true, false); + state.update = true; + return false; +} + +fn viMoseCursorUp(ptr: *anyopaque) !bool { + var state: *UiState = @ptrCast(@alignCast(ptr)); + if (state.insert_mode) return true; + + state.active_input.move(false, false); + state.update = true; + return false; +} + +fn moveCursorDown(ptr: *anyopaque) !bool { + var state: *UiState = @ptrCast(@alignCast(ptr)); + + state.active_input.move(false, false); + state.update = true; + return false; +} + +fn viMoveCursorDown(ptr: *anyopaque) !bool { + var state: *UiState = @ptrCast(@alignCast(ptr)); + if (state.insert_mode) return true; + + state.active_input.move(true, false); + state.update = true; + return false; +} + +fn wrapCursor(ptr: *anyopaque) !bool { + var state: *UiState = @ptrCast(@alignCast(ptr)); + + state.active_input.move(false, true); + state.update = true; + return false; +} + +fn wrapCursorReverse(ptr: *anyopaque) !bool { + var state: *UiState = @ptrCast(@alignCast(ptr)); + + state.active_input.move(true, true); + state.update = true; + return false; +} + +fn quit(ptr: *anyopaque) !bool { + var state: *UiState = @ptrCast(@alignCast(ptr)); + + state.run = false; + return false; +} + +fn authenticate(ptr: *anyopaque) !bool { + var state: *UiState = @ptrCast(@alignCast(ptr)); + + try state.log_file.info("auth", "starting authentication", .{}); + + if (!state.config.allow_empty_password and state.password.text.items.len == 0) { + // Let's not log this message for security reasons + try state.info_line.addMessage( + state.lang.err_empty_password, + state.config.error_bg, + state.config.error_fg, + ); + state.info_line.clearRendered(state.allocator) 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 clear info line: {s}", + .{@errorName(err)}, + ); + }; + state.info_line.label.draw(); + _ = TerminalBuffer.presentBufferStatic(); + return false; + } + + try state.info_line.addMessage( + state.lang.authenticating, + state.config.bg, + state.config.fg, + ); + state.info_line.clearRendered(state.allocator) 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 clear info line: {s}", + .{@errorName(err)}, + ); + }; + state.info_line.label.draw(); + _ = TerminalBuffer.presentBufferStatic(); + + if (state.config.save) save_last_settings: { + // It isn't worth cluttering the code with precise error + // handling, so let's just report a generic error message, + // that should be good enough for debugging anyway. + errdefer state.log_file.err( + "conf", + "failed to save current user data", + .{}, + ) catch {}; + + var file = std.fs.cwd().createFile(state.save_path, .{}) catch |err| { + state.log_file.err( + "sys", + "failed to create save file: {s}", + .{@errorName(err)}, + ) catch break :save_last_settings; + break :save_last_settings; + }; + defer file.close(); + + var file_buffer: [256]u8 = undefined; + var file_writer = file.writer(&file_buffer); + var writer = &file_writer.interface; + + try writer.print("{d}\n", .{state.login.label.current}); + for (state.saved_users.user_list.items) |user| { + try writer.print("{s}:{d}\n", .{ user.username, user.session_index }); + } + try writer.flush(); + + // Delete previous save file if it exists + if (migrator.maybe_save_file) |path| { + std.fs.cwd().deleteFile(path) catch {}; + } else if (state.old_save_path) |path| { + std.fs.cwd().deleteFile(path) catch {}; + } + } + + var shared_err = try SharedError.init(); + defer shared_err.deinit(); + + { + state.log_file.deinit(); + + session_pid = try std.posix.fork(); + if (session_pid == 0) { + const current_environment = state.session.label.list.items[state.session.label.current].environment; + + // Use auto_login_service for autologin, otherwise use configured service + const service_name = if (state.is_autologin) state.config.auto_login_service else state.config.service_name; + const password_text = if (state.is_autologin) "" else state.password.text.items; + + const auth_options = auth.AuthOptions{ + .tty = state.active_tty, + .service_name = service_name, + .path = state.config.path, + .session_log = state.config.session_log, + .xauth_cmd = state.config.xauth_cmd, + .setup_cmd = state.config.setup_cmd, + .login_cmd = state.config.login_cmd, + .x_cmd = state.config.x_cmd, + .x_vt = state.config.x_vt, + .session_pid = session_pid, + .use_kmscon_vt = state.use_kmscon_vt, + }; + + // Signal action to give up control on the TTY + const tty_control_transfer_act = std.posix.Sigaction{ + .handler = .{ .handler = &ttyControlTransferSignalHandler }, + .mask = std.posix.sigemptyset(), + .flags = 0, + }; + std.posix.sigaction(std.posix.SIG.CHLD, &tty_control_transfer_act, null); + + try state.log_file.reinit(); + + auth.authenticate( + state.allocator, + state.log_file, + auth_options, + current_environment, + state.login.getCurrentUsername(), + password_text, + ) catch |err| { + shared_err.writeError(err); + + state.log_file.deinit(); + std.process.exit(1); + }; + + state.log_file.deinit(); + std.process.exit(0); + } + + _ = std.posix.waitpid(session_pid, 0); + // HACK: It seems like the session process is not exiting immediately after the waitpid call. + // This is a workaround to ensure the session process has exited before re-initializing the TTY. + std.Thread.sleep(std.time.ns_per_s * 1); + session_pid = -1; + + try state.log_file.reinit(); + } + + try state.buffer.reclaim(); + + const auth_err = shared_err.readError(); + if (auth_err) |err| { + state.auth_fails += 1; + state.active_input = .password; + + try state.info_line.addMessage( + getAuthErrorMsg(err, state.lang), + state.config.error_bg, + state.config.error_fg, + ); + try state.log_file.err( + "auth", + "failed to authenticate: {s}", + .{@errorName(err)}, + ); + + if (state.config.clear_password or err != error.PamAuthError) state.password.clear(); + } else { + if (state.config.logout_cmd) |logout_cmd| { + var logout_process = std.process.Child.init(&[_][]const u8{ "/bin/sh", "-c", logout_cmd }, state.allocator); + _ = logout_process.spawnAndWait() catch .{}; + } + + state.password.clear(); + state.is_autologin = false; + try state.info_line.addMessage( + state.lang.logout, + state.config.bg, + state.config.fg, + ); + try state.log_file.info("auth", "logged out", .{}); + } + + if (state.config.auth_fails == 0 or state.auth_fails < state.config.auth_fails) { + try TerminalBuffer.clearScreenStatic(true); + state.update = true; + } + + // Restore the cursor + TerminalBuffer.setCursorStatic(0, 0); + _ = TerminalBuffer.presentBufferStatic(); + return false; +} + +fn shutdownCmd(ptr: *anyopaque) !bool { + var state: *UiState = @ptrCast(@alignCast(ptr)); + + shutdown = true; + state.run = false; + return false; +} + +fn restartCmd(ptr: *anyopaque) !bool { + var state: *UiState = @ptrCast(@alignCast(ptr)); + + restart = true; + state.run = false; + return false; +} + +fn sleepCmd(ptr: *anyopaque) !bool { + var state: *UiState = @ptrCast(@alignCast(ptr)); + + if (state.config.sleep_cmd) |sleep_cmd| { + var sleep = std.process.Child.init(&[_][]const u8{ "/bin/sh", "-c", sleep_cmd }, state.allocator); + sleep.stdout_behavior = .Ignore; + sleep.stderr_behavior = .Ignore; + + const process_result = sleep.spawnAndWait() catch return false; + if (process_result.Exited != 0) { + try state.info_line.addMessage( + state.lang.err_sleep, + state.config.error_bg, + state.config.error_fg, + ); + try state.log_file.err( + "sys", + "failed to execute sleep command: exit code {d}", + .{process_result.Exited}, + ); + } + } + return false; +} + +fn hibernateCmd(ptr: *anyopaque) !bool { + var state: *UiState = @ptrCast(@alignCast(ptr)); + + if (state.config.hibernate_cmd) |hibernate_cmd| { + var hibernate = std.process.Child.init(&[_][]const u8{ "/bin/sh", "-c", hibernate_cmd }, state.allocator); + hibernate.stdout_behavior = .Ignore; + hibernate.stderr_behavior = .Ignore; + + const process_result = hibernate.spawnAndWait() catch return false; + if (process_result.Exited != 0) { + try state.info_line.addMessage( + state.lang.err_hibernate, + state.config.error_bg, + state.config.error_fg, + ); + try state.log_file.err( + "sys", + "failed to execute hibernate command: exit code {d}", + .{process_result.Exited}, + ); + } + } + return false; +} + +fn decreaseBrightnessCmd(ptr: *anyopaque) !bool { + var state: *UiState = @ptrCast(@alignCast(ptr)); + + adjustBrightness(state.allocator, state.config.brightness_down_cmd) catch |err| { + try state.info_line.addMessage( + state.lang.err_brightness_change, + state.config.error_bg, + state.config.error_fg, + ); + try state.log_file.err( + "sys", + "failed to decrease brightness: {s}", + .{@errorName(err)}, + ); + }; + return false; +} + +fn increaseBrightnessCmd(ptr: *anyopaque) !bool { + var state: *UiState = @ptrCast(@alignCast(ptr)); + + adjustBrightness(state.allocator, state.config.brightness_up_cmd) catch |err| { + try state.info_line.addMessage( + state.lang.err_brightness_change, + state.config.error_bg, + state.config.error_fg, + ); + try state.log_file.err( + "sys", + "failed to increase brightness: {s}", + .{@errorName(err)}, + ); + }; + return false; +} + fn updateComponents(state: *UiState) !void { if (state.config.battery_id != null) { try state.battery_label.update(state); @@ -1559,7 +1751,7 @@ fn findSessionByName(session: *Session, name: []const u8) ?usize { return null; } -fn getAllUsernames(allocator: std.mem.Allocator, login_defs_path: []const u8, uid_range_error: *?anyerror) !StringList { +fn getAllUsernames(allocator: Allocator, login_defs_path: []const u8, uid_range_error: *?anyerror) !StringList { const uid_range = interop.getUserIdRange(allocator, login_defs_path) catch |err| no_uid_range: { uid_range_error.* = err; break :no_uid_range UidRange{ @@ -1606,18 +1798,14 @@ fn getAllUsernames(allocator: std.mem.Allocator, login_defs_path: []const u8, ui return usernames; } -fn adjustBrightness(allocator: std.mem.Allocator, cmd: []const u8) !void { +fn adjustBrightness(allocator: Allocator, cmd: []const u8) !void { var brightness = std.process.Child.init(&[_][]const u8{ "/bin/sh", "-c", cmd }, allocator); brightness.stdout_behavior = .Ignore; brightness.stderr_behavior = .Ignore; - handle_brightness_cmd: { - const process_result = brightness.spawnAndWait() catch { - break :handle_brightness_cmd; - }; - if (process_result.Exited != 0) { - return error.BrightnessChangeFailed; - } + const process_result = brightness.spawnAndWait() catch return; + if (process_result.Exited != 0) { + return error.BrightnessChangeFailed; } } diff --git a/src/tui/TerminalBuffer.zig b/src/tui/TerminalBuffer.zig index 391fc60..302cf0b 100644 --- a/src/tui/TerminalBuffer.zig +++ b/src/tui/TerminalBuffer.zig @@ -1,4 +1,5 @@ const std = @import("std"); +const Allocator = std.mem.Allocator; const Random = std.Random; const ly_core = @import("ly-core"); @@ -7,10 +8,14 @@ const LogFile = ly_core.LogFile; pub const termbox = @import("termbox2"); const Cell = @import("Cell.zig"); +const keyboard = @import("keyboard.zig"); const Position = @import("Position.zig"); const TerminalBuffer = @This(); +const KeybindCallbackFn = *const fn (*anyopaque) anyerror!bool; +const KeybindMap = std.AutoHashMap(keyboard.Key, KeybindCallbackFn); + pub const InitOptions = struct { fg: u32, bg: u32, @@ -59,6 +64,7 @@ pub const Color = struct { pub const START_POSITION = Position.init(0, 0); +log_file: *LogFile, random: Random, width: usize, height: usize, @@ -78,8 +84,9 @@ box_chars: struct { blank_cell: Cell, full_color: bool, termios: ?std.posix.termios, +keybinds: KeybindMap, -pub fn init(options: InitOptions, log_file: *LogFile, random: Random) !TerminalBuffer { +pub fn init(allocator: Allocator, options: InitOptions, log_file: *LogFile, random: Random) !TerminalBuffer { // Initialize termbox _ = termbox.tb_init(); @@ -101,6 +108,7 @@ pub fn init(options: InitOptions, log_file: *LogFile, random: Random) !TerminalB try log_file.info("tui", "screen resolution is {d}x{d}", .{ width, height }); return .{ + .log_file = log_file, .random = random, .width = width, .height = height, @@ -130,9 +138,15 @@ pub fn init(options: InitOptions, log_file: *LogFile, random: Random) !TerminalB .full_color = options.full_color, // Needed to reclaim the TTY after giving up its control .termios = try std.posix.tcgetattr(std.posix.STDIN_FILENO), + .keybinds = KeybindMap.init(allocator), }; } +pub fn deinit(self: *TerminalBuffer) void { + self.keybinds.deinit(); + TerminalBuffer.shutdownStatic(); +} + pub fn getWidthStatic() usize { return @intCast(termbox.tb_width()); } @@ -205,6 +219,57 @@ pub fn cascade(self: TerminalBuffer) bool { return changed; } +pub fn registerKeybind(self: *TerminalBuffer, keybind: []const u8, callback: KeybindCallbackFn) !void { + var key = std.mem.zeroes(keyboard.Key); + var iterator = std.mem.splitScalar(u8, keybind, '+'); + + while (iterator.next()) |item| { + var found = false; + + inline for (std.meta.fields(keyboard.Key)) |field| { + if (std.ascii.eqlIgnoreCase(field.name, item)) { + @field(key, field.name) = true; + found = true; + break; + } + } + + if (!found) { + try self.log_file.err( + "tui", + "failed to parse key {s} of keybind {s}", + .{ item, keybind }, + ); + } + } + + self.keybinds.put(key, callback) catch |err| { + try self.log_file.err( + "tui", + "failed to register keybind {s}: {s}", + .{ keybind, @errorName(err) }, + ); + }; +} + +pub fn handleKeybind( + self: *TerminalBuffer, + allocator: Allocator, + tb_event: termbox.tb_event, + context: *anyopaque, +) !bool { + var keys = try keyboard.getKeyList(allocator, tb_event); + defer keys.deinit(allocator); + + for (keys.items) |key| { + if (self.keybinds.get(key)) |callback| { + return @call(.auto, callback, .{context}); + } + } + + return true; +} + pub fn drawText( text: []const u8, x: usize, diff --git a/src/tui/keyboard.zig b/src/tui/keyboard.zig new file mode 100644 index 0000000..0dcd9b6 --- /dev/null +++ b/src/tui/keyboard.zig @@ -0,0 +1,735 @@ +const std = @import("std"); +const Allocator = std.mem.Allocator; +const KeyList = std.ArrayList(Key); + +const TerminalBuffer = @import("TerminalBuffer.zig"); +const termbox = TerminalBuffer.termbox; + +pub const Key = packed struct { + ctrl: bool, + shift: bool, + alt: bool, + + f1: bool, + f2: bool, + f3: bool, + f4: bool, + f5: bool, + f6: bool, + f7: bool, + f8: bool, + f9: bool, + f10: bool, + f11: bool, + f12: bool, + + insert: bool, + delete: bool, + home: bool, + end: bool, + pageup: bool, + pagedown: bool, + up: bool, + down: bool, + left: bool, + right: bool, + tab: bool, + backspace: bool, + enter: bool, + space: bool, + + @"!": bool, + @"`": bool, + esc: bool, + @"[": bool, + @"\\": bool, + @"]": bool, + @"/": bool, + _: bool, + @"'": bool, + @"\"": bool, + @",": bool, + @"-": bool, + @".": bool, + @"#": bool, + @"$": bool, + @"%": bool, + @"&": bool, + @"*": bool, + @"(": bool, + @")": bool, + @"+": bool, + @"=": bool, + @":": bool, + @";": bool, + @"<": bool, + @">": bool, + @"?": bool, + @"@": bool, + @"^": bool, + @"~": bool, + @"{": bool, + @"}": bool, + @"|": bool, + + @"0": bool, + @"1": bool, + @"2": bool, + @"3": bool, + @"4": bool, + @"5": bool, + @"6": bool, + @"7": bool, + @"8": bool, + @"9": bool, + + a: bool, + b: bool, + c: bool, + d: bool, + e: bool, + f: bool, + g: bool, + h: bool, + i: bool, + j: bool, + k: bool, + l: bool, + m: bool, + n: bool, + o: bool, + p: bool, + q: bool, + r: bool, + s: bool, + t: bool, + u: bool, + v: bool, + w: bool, + x: bool, + y: bool, + z: bool, +}; + +pub fn getKeyList(allocator: Allocator, tb_event: termbox.tb_event) !KeyList { + var keys: KeyList = .empty; + var key = std.mem.zeroes(Key); + + if (tb_event.mod & termbox.TB_MOD_CTRL != 0) key.ctrl = true; + if (tb_event.mod & termbox.TB_MOD_SHIFT != 0) key.shift = true; + if (tb_event.mod & termbox.TB_MOD_ALT != 0) key.alt = true; + + if (tb_event.key == termbox.TB_KEY_BACK_TAB) { + key.shift = true; + key.tab = true; + } else if (tb_event.key > termbox.TB_KEY_BACK_TAB) { + const code = 0xFFFF - tb_event.key; + + switch (code) { + 0 => key.f1 = true, + 1 => key.f2 = true, + 2 => key.f3 = true, + 3 => key.f4 = true, + 4 => key.f5 = true, + 5 => key.f6 = true, + 6 => key.f7 = true, + 7 => key.f8 = true, + 8 => key.f9 = true, + 9 => key.f10 = true, + 10 => key.f11 = true, + 11 => key.f12 = true, + 12 => key.insert = true, + 13 => key.delete = true, + 14 => key.home = true, + 15 => key.end = true, + 16 => key.pageup = true, + 17 => key.pagedown = true, + 18 => key.up = true, + 19 => key.down = true, + 20 => key.left = true, + 21 => key.right = true, + else => {}, + } + } else if (tb_event.ch < 128) { + const code = if (tb_event.ch == 0 and tb_event.key < 128) tb_event.key else tb_event.ch; + + switch (code) { + 0 => { + key.ctrl = true; + key.@"2" = true; + try keys.append(allocator, key); + + key = std.mem.zeroes(Key); + key.@"`" = true; + }, + 1 => { + key.ctrl = true; + key.a = true; + }, + 2 => { + key.ctrl = true; + key.b = true; + }, + 3 => { + key.ctrl = true; + key.c = true; + }, + 4 => { + key.ctrl = true; + key.d = true; + }, + 5 => { + key.ctrl = true; + key.e = true; + }, + 6 => { + key.ctrl = true; + key.f = true; + }, + 7 => { + key.ctrl = true; + key.g = true; + }, + 8 => { + key.ctrl = true; + key.h = true; + try keys.append(allocator, key); + + key = std.mem.zeroes(Key); + key.backspace = true; + }, + 9 => { + key.ctrl = true; + key.i = true; + try keys.append(allocator, key); + + key = std.mem.zeroes(Key); + key.tab = true; + }, + 10 => { + key.ctrl = true; + key.j = true; + }, + 11 => { + key.ctrl = true; + key.k = true; + }, + 12 => { + key.ctrl = true; + key.l = true; + }, + 13 => { + key.ctrl = true; + key.m = true; + try keys.append(allocator, key); + + key = std.mem.zeroes(Key); + key.enter = true; + }, + 14 => { + key.ctrl = true; + key.n = true; + }, + 15 => { + key.ctrl = true; + key.o = true; + }, + 16 => { + key.ctrl = true; + key.p = true; + }, + 17 => { + key.ctrl = true; + key.q = true; + }, + 18 => { + key.ctrl = true; + key.r = true; + }, + 19 => { + key.ctrl = true; + key.s = true; + }, + 20 => { + key.ctrl = true; + key.t = true; + }, + 21 => { + key.ctrl = true; + key.u = true; + }, + 22 => { + key.ctrl = true; + key.v = true; + }, + 23 => { + key.ctrl = true; + key.w = true; + }, + 24 => { + key.ctrl = true; + key.x = true; + }, + 25 => { + key.ctrl = true; + key.y = true; + }, + 26 => { + key.ctrl = true; + key.z = true; + }, + 27 => { + key.ctrl = true; + key.@"3" = true; + try keys.append(allocator, key); + + key = std.mem.zeroes(Key); + key.esc = true; + try keys.append(allocator, key); + + key = std.mem.zeroes(Key); + key.@"[" = true; + }, + 28 => { + key.ctrl = true; + key.@"4" = true; + try keys.append(allocator, key); + + key = std.mem.zeroes(Key); + key.@"\\" = true; + }, + 29 => { + key.ctrl = true; + key.@"5" = true; + try keys.append(allocator, key); + + key = std.mem.zeroes(Key); + key.@"]" = true; + }, + 30 => { + key.ctrl = true; + try keys.append(allocator, key); + + key = std.mem.zeroes(Key); + key.@"6" = true; + }, + 31 => { + key.ctrl = true; + key.@"7" = true; + try keys.append(allocator, key); + + key = std.mem.zeroes(Key); + key.@"/" = true; + try keys.append(allocator, key); + + key = std.mem.zeroes(Key); + key._ = true; + }, + 32 => { + key.space = true; + }, + 33 => { + key.shift = true; + key.@"1" = true; + try keys.append(allocator, key); + + key = std.mem.zeroes(Key); + key.@"!" = true; + }, + 34 => { + key.shift = true; + key.@"2" = true; + try keys.append(allocator, key); + + key = std.mem.zeroes(Key); + key.@"\"" = true; + }, + 35 => { + key.shift = true; + key.@"3" = true; + try keys.append(allocator, key); + + key = std.mem.zeroes(Key); + key.@"#" = true; + }, + 36 => { + key.shift = true; + key.@"4" = true; + try keys.append(allocator, key); + + key = std.mem.zeroes(Key); + key.@"$" = true; + }, + 37 => { + key.shift = true; + key.@"5" = true; + try keys.append(allocator, key); + + key = std.mem.zeroes(Key); + key.@"%" = true; + }, + 38 => { + key.shift = true; + key.@"6" = true; + try keys.append(allocator, key); + + key = std.mem.zeroes(Key); + key.@"&" = true; + }, + 39 => { + key.@"'" = true; + }, + 40 => { + key.shift = true; + key.@"9" = true; + try keys.append(allocator, key); + + key = std.mem.zeroes(Key); + key.@"(" = true; + }, + 41 => { + key.shift = true; + key.@"0" = true; + try keys.append(allocator, key); + + key = std.mem.zeroes(Key); + key.@")" = true; + }, + 42 => { + key.shift = true; + key.@"8" = true; + try keys.append(allocator, key); + + key = std.mem.zeroes(Key); + key.@"*" = true; + }, + 43 => { + key.shift = true; + key.@"7" = true; + try keys.append(allocator, key); + + key = std.mem.zeroes(Key); + key.@"+" = true; + }, + 44 => { + key.@"," = true; + }, + 45 => { + key.@"-" = true; + }, + 46 => { + key.@"." = true; + }, + 47 => { + key.@"/" = true; + }, + 48 => { + key.@"0" = true; + }, + 49 => { + key.@"1" = true; + }, + 50 => { + key.@"2" = true; + }, + 51 => { + key.@"3" = true; + }, + 52 => { + key.@"4" = true; + }, + 53 => { + key.@"5" = true; + }, + 54 => { + key.@"6" = true; + }, + 55 => { + key.@"7" = true; + }, + 56 => { + key.@"8" = true; + }, + 57 => { + key.@"9" = true; + }, + 58 => { + key.shift = true; + key.@":" = true; + }, + 59 => { + key.@";" = true; + }, + 60 => { + key.shift = true; + key.@"<" = true; + }, + 61 => { + key.@"=" = true; + }, + 62 => { + key.shift = true; + key.@">" = true; + }, + 63 => { + key.shift = true; + key.@"?" = true; + }, + 64 => { + key.shift = true; + key.@"2" = true; + try keys.append(allocator, key); + + key = std.mem.zeroes(Key); + key.@"@" = true; + }, + 65 => { + key.shift = true; + key.a = true; + }, + 66 => { + key.shift = true; + key.b = true; + }, + 67 => { + key.shift = true; + key.c = true; + }, + 68 => { + key.shift = true; + key.d = true; + }, + 69 => { + key.shift = true; + key.e = true; + }, + 70 => { + key.shift = true; + key.f = true; + }, + 71 => { + key.shift = true; + key.g = true; + }, + 72 => { + key.shift = true; + key.h = true; + }, + 73 => { + key.shift = true; + key.i = true; + }, + 74 => { + key.shift = true; + key.j = true; + }, + 75 => { + key.shift = true; + key.k = true; + }, + 76 => { + key.shift = true; + key.l = true; + }, + 77 => { + key.shift = true; + key.m = true; + }, + 78 => { + key.shift = true; + key.n = true; + }, + 79 => { + key.shift = true; + key.o = true; + }, + 80 => { + key.shift = true; + key.p = true; + }, + 81 => { + key.shift = true; + key.q = true; + }, + 82 => { + key.shift = true; + key.r = true; + }, + 83 => { + key.shift = true; + key.s = true; + }, + 84 => { + key.shift = true; + key.t = true; + }, + 85 => { + key.shift = true; + key.u = true; + }, + 86 => { + key.shift = true; + key.v = true; + }, + 87 => { + key.shift = true; + key.w = true; + }, + 88 => { + key.shift = true; + key.x = true; + }, + 89 => { + key.shift = true; + key.y = true; + }, + 90 => { + key.shift = true; + key.z = true; + }, + 91 => { + key.@"[" = true; + }, + 92 => { + key.@"\\" = true; + }, + 93 => { + key.@"]" = true; + }, + 94 => { + key.shift = true; + key.@"6" = true; + try keys.append(allocator, key); + + key = std.mem.zeroes(Key); + key.@"^" = true; + }, + 95 => { + key.shift = true; + key.@"-" = true; + try keys.append(allocator, key); + + key = std.mem.zeroes(Key); + key._ = true; + }, + 96 => { + key.@"`" = true; + }, + 97 => { + key.a = true; + }, + 98 => { + key.b = true; + }, + 99 => { + key.c = true; + }, + 100 => { + key.d = true; + }, + 101 => { + key.e = true; + }, + 102 => { + key.f = true; + }, + 103 => { + key.g = true; + }, + 104 => { + key.h = true; + }, + 105 => { + key.i = true; + }, + 106 => { + key.j = true; + }, + 107 => { + key.k = true; + }, + 108 => { + key.l = true; + }, + 109 => { + key.m = true; + }, + 110 => { + key.n = true; + }, + 111 => { + key.o = true; + }, + 112 => { + key.p = true; + }, + 113 => { + key.q = true; + }, + 114 => { + key.r = true; + }, + 115 => { + key.s = true; + }, + 116 => { + key.t = true; + }, + 117 => { + key.u = true; + }, + 118 => { + key.v = true; + }, + 119 => { + key.w = true; + }, + 120 => { + key.x = true; + }, + 121 => { + key.y = true; + }, + 122 => { + key.z = true; + }, + 123 => { + key.shift = true; + key.@"{" = true; + }, + 124 => { + key.shift = true; + key.@"\\" = true; + try keys.append(allocator, key); + + key = std.mem.zeroes(Key); + key.@"|" = true; + }, + 125 => { + key.shift = true; + key.@"}" = true; + }, + 126 => { + key.shift = true; + key.@"`" = true; + try keys.append(allocator, key); + + key = std.mem.zeroes(Key); + key.@"~" = true; + }, + 127 => { + key.ctrl = true; + key.@"8" = true; + try keys.append(allocator, key); + + key = std.mem.zeroes(Key); + key.backspace = true; + }, + else => {}, + } + } + + try keys.append(allocator, key); + + return keys; +}