Support more configurable keybindings (closes #679)

Signed-off-by: AnErrupTion <anerruption@disroot.org>
This commit is contained in:
AnErrupTion
2026-02-09 11:11:00 +01:00
parent cf5f62661c
commit 852a602032
4 changed files with 1286 additions and 298 deletions

View File

@@ -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)

View File

@@ -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,
};
pub fn main() !void {
var shutdown = false;
var restart = false;
pub fn main() !void {
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,120 +997,181 @@ 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.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;
}
},
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;
return false;
}
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;
fn enableInsertMode(ptr: *anyopaque) !bool {
var state: *UiState = @ptrCast(@alignCast(ptr));
if (state.insert_mode) return true;
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.insert_mode = true;
state.update = true;
},
termbox.TB_KEY_CTRL_K, termbox.TB_KEY_ARROW_UP => {
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;
},
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;
return false;
}
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)});
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)},
);
};
info_line.label.draw();
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 (config.save) save_last_settings: {
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 log_file.err("conf", "failed to save current user data", .{}) catch {};
errdefer state.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;
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();
@@ -1074,8 +1180,8 @@ pub fn main() !void {
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("{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();
@@ -1083,8 +1189,8 @@ pub fn main() !void {
// 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 {};
} else if (state.old_save_path) |path| {
std.fs.cwd().deleteFile(path) catch {};
}
}
@@ -1092,28 +1198,28 @@ pub fn main() !void {
defer shared_err.deinit();
{
log_file.deinit();
state.log_file.deinit();
session_pid = try std.posix.fork();
if (session_pid == 0) {
const current_environment = session.label.list.items[session.label.current].environment;
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 (is_autologin) config.auto_login_service else config.service_name;
const password_text = if (is_autologin) "" else password.text.items;
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 = active_tty,
.tty = state.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,
.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 = use_kmscon_vt,
.use_kmscon_vt = state.use_kmscon_vt,
};
// Signal action to give up control on the TTY
@@ -1124,16 +1230,23 @@ pub fn main() !void {
};
std.posix.sigaction(std.posix.SIG.CHLD, &tty_control_transfer_act, null);
try log_file.reinit();
try state.log_file.reinit();
auth.authenticate(allocator, &log_file, auth_options, current_environment, login.getCurrentUsername(), password_text) catch |err| {
auth.authenticate(
state.allocator,
state.log_file,
auth_options,
current_environment,
state.login.getCurrentUsername(),
password_text,
) catch |err| {
shared_err.writeError(err);
log_file.deinit();
state.log_file.deinit();
std.process.exit(1);
};
log_file.deinit();
state.log_file.deinit();
std.process.exit(0);
}
@@ -1143,33 +1256,45 @@ pub fn main() !void {
std.Thread.sleep(std.time.ns_per_s * 1);
session_pid = -1;
try log_file.reinit();
try state.log_file.reinit();
}
try buffer.reclaim();
try state.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)});
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 (config.clear_password or err != error.PamAuthError) password.clear();
if (state.config.clear_password or err != error.PamAuthError) state.password.clear();
} else {
if (config.logout_cmd) |logout_cmd| {
var logout_process = std.process.Child.init(&[_][]const u8{ "/bin/sh", "-c", logout_cmd }, allocator);
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 .{};
}
password.clear();
is_autologin = false;
try info_line.addMessage(lang.logout, config.bg, config.fg);
try log_file.info("auth", "logged out", .{});
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 (config.auth_fails == 0 or state.auth_fails < config.auth_fails) {
if (state.config.auth_fails == 0 or state.auth_fails < state.config.auth_fails) {
try TerminalBuffer.clearScreenStatic(true);
state.update = true;
}
@@ -1177,42 +1302,109 @@ pub fn main() !void {
// 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 => {},
}
return false;
}
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);
},
fn shutdownCmd(ptr: *anyopaque) !bool {
var state: *UiState = @ptrCast(@alignCast(ptr));
shutdown = true;
state.run = false;
return false;
}
state.update = true;
},
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 {
@@ -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,20 +1798,16 @@ 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;
};
const process_result = brightness.spawnAndWait() catch return;
if (process_result.Exited != 0) {
return error.BrightnessChangeFailed;
}
}
}
fn getBatteryPercentage(battery_id: []const u8) !u8 {
const path = try std.fmt.allocPrint(temporary_allocator, "/sys/class/power_supply/{s}/capacity", .{battery_id});

View File

@@ -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,

735
src/tui/keyboard.zig Normal file
View File

@@ -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;
}