From c6446db3e202bb7774bd24a0330eb2438a03b78d Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Sat, 6 Dec 2025 15:58:39 +0100 Subject: [PATCH 001/176] Start Ly v1.4.0 development cycle Signed-off-by: AnErrupTion --- build.zig | 2 +- build.zig.zon | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/build.zig b/build.zig index d7dd3b4..e2e6605 100644 --- a/build.zig +++ b/build.zig @@ -23,7 +23,7 @@ comptime { } } -const ly_version = std.SemanticVersion{ .major = 1, .minor = 3, .patch = 0 }; +const ly_version = std.SemanticVersion{ .major = 1, .minor = 4, .patch = 0 }; var dest_directory: []const u8 = undefined; var config_directory: []const u8 = undefined; diff --git a/build.zig.zon b/build.zig.zon index 3c6a247..da2e056 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -1,6 +1,6 @@ .{ .name = .ly, - .version = "1.3.0", + .version = "1.4.0", .fingerprint = 0xa148ffcc5dc2cb59, .minimum_zig_version = "0.15.0", .dependencies = .{ From ced8f9bee35ad97c1e59ef1e182e064c04eb76a7 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Wed, 17 Dec 2025 17:32:16 +0100 Subject: [PATCH 002/176] Fix session not being saved correctly Signed-off-by: AnErrupTion --- src/tui/components/Session.zig | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/tui/components/Session.zig b/src/tui/components/Session.zig index 74ab7a2..5db4672 100644 --- a/src/tui/components/Session.zig +++ b/src/tui/components/Session.zig @@ -41,15 +41,19 @@ pub fn addEnvironment(self: *Session, environment: Environment) !void { const env = Env{ .environment = environment, .index = self.label.list.items.len }; try self.label.addItem(env); - sessionChanged(env, self.user_list); + addedSession(env, self.user_list); +} + +fn addedSession(env: Env, user_list: *UserList) void { + const user = user_list.label.list.items[user_list.label.current]; + if (!user.first_run) return; + + user.session_index.* = env.index; } fn sessionChanged(env: Env, maybe_user_list: ?*UserList) void { if (maybe_user_list) |user_list| { - const user = user_list.label.list.items[user_list.label.current]; - if (!user.first_run) return; - - user.session_index.* = env.index; + user_list.label.list.items[user_list.label.current].session_index.* = env.index; } } From 04d44472734d414e9d5cb557b0575e004e9aeb68 Mon Sep 17 00:00:00 2001 From: qbe Date: Thu, 18 Dec 2025 12:17:00 +0100 Subject: [PATCH 003/176] Escape TTY in systemd service (closes #889) (#890) ## What are the changes about? * fix templated systemd dependencies: %I -> %i * amend systemd-specific documentation in readme with section specific to systemd-logind / autovt ## What existing issue does this resolve? [issue #889](https://codeberg.org/fairyglade/ly/issues/889) ## Pre-requisites - [x] I have tested & confirmed the changes work locally Reviewed-on: https://codeberg.org/fairyglade/ly/pulls/890 Reviewed-by: AnErrupTion Co-authored-by: qbe Co-committed-by: qbe --- readme.md | 12 +++++++++--- res/ly@.service | 4 ++-- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/readme.md b/readme.md index e0308e3..b7afade 100644 --- a/readme.md +++ b/readme.md @@ -121,9 +121,15 @@ that Ly will run on, otherwise bad things will happen. For example, to disable ` # systemctl disable getty@tty2.service ``` -You can change the TTY Ly will run on by editing the corresponding -service file for your platform, or on systemd, by enabling the service on -different TTYs, as is done above. +On platforms that use systemd-logind to dynamically start `autovt@.service` instances when the switch to a new tty occurs, any ly instances for ttys *except the default tty* need to be enabled using a different mechanism: To autostart ly on switch to `tty2`, do not enable any `ly` unit directly, instead symlink `autovt@tty2.service` to `ly@tty2.service` within `/usr/lib/systemd/system/` (analogous for every other tty you want to enable ly on). + +The target of the symlink, `ly@ttyN.service`, does not actually exist, but systemd nevertheless recognizes that the instanciation of `autovt@.service` with `%I` equal to `ttyN` now points to an instanciation of `ly@.service` with `%I` set to `ttyN`. + +Compare to `man 5 logind.conf`, especially regarding the `NAutoVTs=` and `ReserveVT=` parameters. + + +On non-systemd systems, you can change the TTY Ly will run on by editing the corresponding +service file for your platform. ### OpenRC diff --git a/res/ly@.service b/res/ly@.service index 21d20df..54d8bf6 100644 --- a/res/ly@.service +++ b/res/ly@.service @@ -1,8 +1,8 @@ [Unit] Description=TUI display manager After=systemd-user-sessions.service plymouth-quit-wait.service -After=getty@%I.service -Conflicts=getty@%I.service +After=getty@%i.service +Conflicts=getty@%i.service [Service] Type=idle From e57de5172eacd4a52120bd170d02fdf09e69b2ba Mon Sep 17 00:00:00 2001 From: ViSzKe Date: Mon, 29 Dec 2025 23:36:47 +0100 Subject: [PATCH 004/176] Update Swedish translation (#899) ## What are the changes about? Adding proper Swedish translation ## What existing issue does this resolve? The sub-standard Swedish translation ## Pre-requisites - [x] I have tested & confirmed the changes work locally Reviewed-on: https://codeberg.org/fairyglade/ly/pulls/899 Reviewed-by: AnErrupTion Co-authored-by: ViSzKe Co-committed-by: ViSzKe --- res/lang/sv.ini | 124 ++++++++++++++++++++++++------------------------ 1 file changed, 62 insertions(+), 62 deletions(-) diff --git a/res/lang/sv.ini b/res/lang/sv.ini index 661eb62..5f869ca 100644 --- a/res/lang/sv.ini +++ b/res/lang/sv.ini @@ -1,78 +1,78 @@ - - - +authenticating = autentiserar... +brightness_down = minska ljusstyrka +brightness_up = öka ljusstyrka capslock = capslock - -err_alloc = misslyckad minnesallokering - - -err_bounds = utanför banan index - +custom = anpassad +err_alloc = minnesallokering misslyckades +err_args = tolkning av kommandoargument misslyckades +err_autologin_session = autologin-session hittades inte +err_bounds = index-värde utanför intervallet +err_brightness_change = ändring av ljusstyrka misslyckades err_chdir = misslyckades att öppna hemkatalog - - - +err_clock_too_long = klocksträng för lång +err_config = tolkning av konfigfil misslyckades +err_crawl = genomsökning av sessionskataloger misslyckades err_dgn_oob = loggmeddelande -err_domain = okänd domän - - - - -err_hostname = misslyckades att hämta värdnamn - - - -err_mlock = misslyckades att låsa lösenordsminne -err_null = nullpekare - +err_domain = ogitlig domän +err_empty_password = tomt lösenord godtas ej +err_envlist = hämtning av env-lista misslyckades +err_get_active_tty = hämtning av aktiv tty misslyckades +err_hibernate = vilolägets kommando misslyckades +err_hostname = hämtning av hostname misslyckades +err_inactivity = inaktivitetslägets kommando misslyckades +err_lock_state = hämtning av låsningsstatus misslyckades +err_log = öppning av loggfil misslyckades +err_mlock = låsning av lösenordsminne misslyckades +err_null = null pointer +err_numlock = inställning av numlock misslyckades err_pam = pam-transaktion misslyckades err_pam_abort = pam-transaktion avbröts -err_pam_acct_expired = konto upphört -err_pam_auth = autentiseringsfel -err_pam_authinfo_unavail = misslyckades att hämta användarinfo -err_pam_authok_reqd = token utgången -err_pam_buf = minnesbuffer fel -err_pam_cred_err = misslyckades att ställa in inloggningsuppgifter -err_pam_cred_expired = inloggningsuppgifter upphörda +err_pam_acct_expired = kontot har löpt ut +err_pam_auth = autentisering misslyckades +err_pam_authinfo_unavail = hämtning av användarinformation misslyckades +err_pam_authok_reqd = token har löpt ut +err_pam_buf = minnesbufferfel +err_pam_cred_err = inställning av inloggningsuppgifter misslyckades +err_pam_cred_expired = inloggningsuppgifterna har löpt ut err_pam_cred_insufficient = otillräckliga inloggningsuppgifter -err_pam_cred_unavail = misslyckades att hämta inloggningsuppgifter -err_pam_maxtries = nådde maximal försöksgräns -err_pam_perm_denied = åtkomst nekad +err_pam_cred_unavail = hämtning av inloggningsuppgifter misslyckades +err_pam_maxtries = gränsen för antal försök nådd +err_pam_perm_denied = tillstånd nekas err_pam_session = sessionsfel err_pam_sys = systemfel err_pam_user_unknown = okänd användare -err_path = misslyckades att ställa in sökväg -err_perm_dir = misslyckades att ändra aktuell katalog -err_perm_group = misslyckades att nergradera gruppbehörigheter -err_perm_user = misslyckades att nergradera användarbehörigheter -err_pwnam = misslyckades att hämta användarinfo - - - - - - - -err_user_gid = misslyckades att ställa in användar-GID -err_user_init = misslyckades att initialisera användaren -err_user_uid = misslyckades att ställa in användar-UID - - -err_xsessions_dir = misslyckades att hitta sessionskatalog -err_xsessions_open = misslyckades att öppna sessionskatalog - - +err_path = inställning av sökväg misslyckades +err_perm_dir = byte av nuvarande katalog misslyckades +err_perm_group = nedgradering av grupptillstånd misslyckades +err_perm_user = nedgradering av användartillstånd misslyckades +err_pwnam = hämtning av användarinformation misslyckades +err_sleep = strömsparlägets kommando misslyckades +err_start = startkommando misslyckades +err_battery = hämtning av batteristatus misslyckades +err_switch_tty = byte av tty misslyckades +err_tty_ctrl = överföring av tty-kontroll misslyckades +err_no_users = inga användare hittades +err_uid_range = dynamisk hämtning av uid-intervall misslyckades +err_user_gid = inställning av användarens GID misslyckades +err_user_init = initiering av användare misslyckades +err_user_uid = inställning av användarens UID misslyckades +err_xauth = xauth-kommando misslyckades +err_xcb_conn = xcb-anslutning misslyckades +err_xsessions_dir = sessionskatalog hittades inte +err_xsessions_open = öppning av sessionskatalog misslyckades +hibernate = viloläge +insert = infoga login = inloggning logout = utloggad - - +no_x11_support = x11-stöd inaktiverat vid kompilering +normal = normal numlock = numlock - +other = övrig password = lösenord restart = starta om -shell = skal +shell = shell shutdown = stäng av - +sleep = viloläge wayland = wayland - -xinitrc = xinitrc +x11 = x11 +xinitrc = xinitrc \ No newline at end of file From 2e7bb3eb58855034be63c682460d6486a948a791 Mon Sep 17 00:00:00 2001 From: Theo Gaige Date: Mon, 29 Dec 2025 23:39:10 +0100 Subject: [PATCH 005/176] Use buf_height and buf_width in Matrix.draw() (#903) ## What are the changes about? buf_height is declared at the start of the draw() function of the Matrix animation but both buf_height and self.terminal_buffer.height are used in the function. replace every occurence of self.terminal_buffer.height by buf_height for consistency. The same goes for buf_width and self.terminal_buffer.width. no functionnal changes ## What existing issue does this resolve? N/A ## Pre-requisites - [x] I have tested & confirmed the changes work locally Reviewed-on: https://codeberg.org/fairyglade/ly/pulls/903 Reviewed-by: AnErrupTion Co-authored-by: Theo Gaige Co-committed-by: Theo Gaige --- src/animations/Matrix.zig | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/animations/Matrix.zig b/src/animations/Matrix.zig index 5def466..9d01464 100644 --- a/src/animations/Matrix.zig +++ b/src/animations/Matrix.zig @@ -86,17 +86,17 @@ fn draw(self: *Matrix) void { self.count = 0; var x: usize = 0; - while (x < self.terminal_buffer.width) : (x += 2) { + while (x < buf_width) : (x += 2) { var tail: usize = 0; var line = &self.lines[x]; if (self.frame <= line.update) continue; - if (self.dots[x].value == null and self.dots[self.terminal_buffer.width + x].value == ' ') { + if (self.dots[x].value == null and self.dots[buf_width + x].value == ' ') { if (line.space > 0) { line.space -= 1; } else { const randint = self.terminal_buffer.random.int(u16); - const h = self.terminal_buffer.height; + const h = buf_height; line.length = @mod(randint, h - 3) + 3; self.dots[x].value = @mod(randint, self.max_codepoint) + self.min_codepoint; line.space = @mod(randint, h + 1); @@ -153,7 +153,7 @@ fn draw(self: *Matrix) void { var x: usize = 0; while (x < buf_width) : (x += 2) { var y: usize = 1; - while (y <= self.terminal_buffer.height) : (y += 1) { + while (y <= buf_height) : (y += 1) { const dot = self.dots[buf_width * y + x]; const cell = if (dot.value == null or dot.value == ' ') self.default_cell else Cell{ .ch = @intCast(dot.value.?), From add7f25f0dd7c3df05ba1a1de418b896501591c9 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Tue, 30 Dec 2025 17:20:04 +0100 Subject: [PATCH 006/176] Create session log directory if non-existent (closes #896) Signed-off-by: AnErrupTion --- src/auth.zig | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/auth.zig b/src/auth.zig index 8bd7621..2463e6c 100644 --- a/src/auth.zig +++ b/src/auth.zig @@ -502,6 +502,14 @@ fn executeCmd(global_log_file: *LogFile, allocator: std.mem.Allocator, shell: [] } fn redirectStandardStreams(global_log_file: *LogFile, session_log: []const u8, create: bool) !std.fs.File { + create_session_log_dir: { + const session_log_dir = std.fs.path.dirname(session_log) orelse break :create_session_log_dir; + std.fs.cwd().makePath(session_log_dir) catch |err| { + try global_log_file.file_writer.interface.print("failed to create session log file directory: {s}\n", .{@errorName(err)}); + return err; + }; + } + const log_file = if (create) (std.fs.cwd().createFile(session_log, .{ .mode = 0o666 }) catch |err| { try global_log_file.file_writer.interface.print("failed to create new session log file: {s}\n", .{@errorName(err)}); return err; From b1cb576f67073a063e0035fbf5e7eab60dd553a3 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Tue, 30 Dec 2025 17:38:22 +0100 Subject: [PATCH 007/176] Check for session file name in autologin (closes #895) Signed-off-by: AnErrupTion --- src/Environment.zig | 1 + src/main.zig | 8 +++++--- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/Environment.zig b/src/Environment.zig index eab875d..48b68a3 100644 --- a/src/Environment.zig +++ b/src/Environment.zig @@ -14,6 +14,7 @@ pub const DesktopEntry = struct { pub const Entry = struct { @"Desktop Entry": DesktopEntry = .{} }; entry_ini: ?Ini(Entry) = null, +file_name: []const u8 = "", name: []const u8 = "", xdg_session_desktop: ?[]const u8 = null, xdg_session_desktop_owned: bool = false, diff --git a/src/main.zig b/src/main.zig index 8565c9a..7bba2f1 100644 --- a/src/main.zig +++ b/src/main.zig @@ -1251,6 +1251,7 @@ fn crawl(session: *Session, lang: Lang, path: []const u8, display_server: Displa }); errdefer entry_ini.deinit(); + const file_name = std.fs.path.stem(item.name); const entry = entry_ini.data.@"Desktop Entry"; var maybe_xdg_session_desktop: ?[]const u8 = null; var maybe_xdg_desktop_names: ?[]const u8 = null; @@ -1268,15 +1269,15 @@ fn crawl(session: *Session, lang: Lang, path: []const u8, display_server: Displa } else if (display_server != .custom) { // If DesktopNames is empty, and this isn't a custom session entry, // we'll take the name of the session file - const stem = std.fs.path.stem(item.name); - if (stem.len > 0) { - maybe_xdg_session_desktop = try session.label.allocator.dupe(u8, stem); + if (file_name.len > 0) { + maybe_xdg_session_desktop = try session.label.allocator.dupe(u8, file_name); xdg_session_desktop_owned = true; } } try session.addEnvironment(.{ .entry_ini = entry_ini, + .file_name = std.fs.path.stem(item.name), .name = entry.Name, .xdg_session_desktop = maybe_xdg_session_desktop, .xdg_session_desktop_owned = xdg_session_desktop_owned, @@ -1310,6 +1311,7 @@ fn findSessionByName(session: *Session, name: []const u8) ?usize { if (std.ascii.eqlIgnoreCase(session_desktop_name, name)) return i; } if (std.ascii.eqlIgnoreCase(env.environment.name, name)) return i; + if (std.ascii.eqlIgnoreCase(env.environment.file_name, name)) return i; } return null; } From 26e7585b0bc3fd7400641b6afb4daef8a02508b0 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Tue, 30 Dec 2025 17:43:00 +0100 Subject: [PATCH 008/176] Don't forget to allocate :D Signed-off-by: AnErrupTion --- src/Environment.zig | 1 - src/main.zig | 11 +++-------- src/tui/components/Session.zig | 4 +--- 3 files changed, 4 insertions(+), 12 deletions(-) diff --git a/src/Environment.zig b/src/Environment.zig index 48b68a3..8184f92 100644 --- a/src/Environment.zig +++ b/src/Environment.zig @@ -17,7 +17,6 @@ entry_ini: ?Ini(Entry) = null, file_name: []const u8 = "", name: []const u8 = "", xdg_session_desktop: ?[]const u8 = null, -xdg_session_desktop_owned: bool = false, xdg_desktop_names: ?[]const u8 = null, cmd: ?[]const u8 = null, specifier: []const u8 = "", diff --git a/src/main.zig b/src/main.zig index 7bba2f1..e1a13a7 100644 --- a/src/main.zig +++ b/src/main.zig @@ -1251,11 +1251,10 @@ fn crawl(session: *Session, lang: Lang, path: []const u8, display_server: Displa }); errdefer entry_ini.deinit(); - const file_name = std.fs.path.stem(item.name); + const file_name = try session.label.allocator.dupe(u8, std.fs.path.stem(item.name)); const entry = entry_ini.data.@"Desktop Entry"; var maybe_xdg_session_desktop: ?[]const u8 = null; var maybe_xdg_desktop_names: ?[]const u8 = null; - var xdg_session_desktop_owned = false; // Prepare the XDG_SESSION_DESKTOP and XDG_CURRENT_DESKTOP environment // variables here @@ -1269,18 +1268,14 @@ fn crawl(session: *Session, lang: Lang, path: []const u8, display_server: Displa } else if (display_server != .custom) { // If DesktopNames is empty, and this isn't a custom session entry, // we'll take the name of the session file - if (file_name.len > 0) { - maybe_xdg_session_desktop = try session.label.allocator.dupe(u8, file_name); - xdg_session_desktop_owned = true; - } + if (file_name.len > 0) maybe_xdg_session_desktop = file_name; } try session.addEnvironment(.{ .entry_ini = entry_ini, - .file_name = std.fs.path.stem(item.name), + .file_name = file_name, .name = entry.Name, .xdg_session_desktop = maybe_xdg_session_desktop, - .xdg_session_desktop_owned = xdg_session_desktop_owned, .xdg_desktop_names = maybe_xdg_desktop_names, .cmd = entry.Exec, .specifier = switch (display_server) { diff --git a/src/tui/components/Session.zig b/src/tui/components/Session.zig index 5db4672..3283ff0 100644 --- a/src/tui/components/Session.zig +++ b/src/tui/components/Session.zig @@ -29,9 +29,7 @@ pub fn init(allocator: Allocator, buffer: *TerminalBuffer, user_list: *UserList) pub fn deinit(self: *Session) void { for (self.label.list.items) |*env| { if (env.environment.entry_ini) |*entry_ini| entry_ini.deinit(); - if (env.environment.xdg_session_desktop_owned) { - self.label.allocator.free(env.environment.xdg_session_desktop.?); - } + self.label.allocator.free(env.environment.file_name); } self.label.deinit(); From 8e893932f2cce4d8181e7b35d31fbb4b1de3caff Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Tue, 30 Dec 2025 18:09:46 +0100 Subject: [PATCH 009/176] Clamp user session index if invalid Signed-off-by: AnErrupTion --- src/main.zig | 2 +- src/tui/components/UserList.zig | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/main.zig b/src/main.zig index e1a13a7..f1d7c4a 100644 --- a/src/main.zig +++ b/src/main.zig @@ -529,7 +529,7 @@ pub fn main() !void { active_input = .password; - if (user.session_index < session.label.list.items.len) session.label.current = user.session_index; + session.label.current = @min(user.session_index, session.label.list.items.len - 1); } } diff --git a/src/tui/components/UserList.zig b/src/tui/components/UserList.zig index d9ed846..91ddc15 100644 --- a/src/tui/components/UserList.zig +++ b/src/tui/components/UserList.zig @@ -71,8 +71,7 @@ pub fn getCurrentUsername(self: UserList) []const u8 { fn usernameChanged(user: User, maybe_session: ?*Session) void { if (maybe_session) |session| { - if (user.session_index.* >= session.label.list.items.len) return; - session.label.current = user.session_index.*; + session.label.current = @min(user.session_index.*, session.label.list.items.len - 1); } } From 9e4147bfb4783c680652b2306d69716e4162bbb5 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Tue, 30 Dec 2025 19:29:47 +0100 Subject: [PATCH 010/176] Fix invalid XDG_RUNTIME_DIR if D-Bus isn't used Signed-off-by: AnErrupTion --- src/auth.zig | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/src/auth.zig b/src/auth.zig index 2463e6c..8f7ae5d 100644 --- a/src/auth.zig +++ b/src/auth.zig @@ -169,6 +169,7 @@ fn startSession( // Reset the XDG environment variables try setXdgEnv(allocator, tty_str, current_environment); + try setXdgRuntimeDir(allocator); // Set the PAM variables const pam_env_vars: ?[*:null]?[*:0]u8 = interop.pam.pam_getenvlist(handle); @@ -217,6 +218,15 @@ fn setXdgEnv(allocator: std.mem.Allocator, tty_str: []u8, environment: Environme .custom => if (environment.is_terminal) "tty" else "unspecified", }, false); + if (environment.xdg_desktop_names) |xdg_desktop_names| try interop.setEnvironmentVariable(allocator, "XDG_CURRENT_DESKTOP", xdg_desktop_names, false); + try interop.setEnvironmentVariable(allocator, "XDG_SESSION_CLASS", "user", false); + try interop.setEnvironmentVariable(allocator, "XDG_SESSION_ID", "1", false); + if (environment.xdg_session_desktop) |desktop_name| try interop.setEnvironmentVariable(allocator, "XDG_SESSION_DESKTOP", desktop_name, false); + try interop.setEnvironmentVariable(allocator, "XDG_SEAT", "seat0", false); + try interop.setEnvironmentVariable(allocator, "XDG_VTNR", tty_str, false); +} + +fn setXdgRuntimeDir(allocator: std.mem.Allocator) !void { // The "/run/user/%d" directory is not available on FreeBSD. It is much // better to stick to the defaults and let applications using // XDG_RUNTIME_DIR to fall back to directories inside user's home @@ -228,13 +238,6 @@ fn setXdgEnv(allocator: std.mem.Allocator, tty_str: []u8, environment: Environme try interop.setEnvironmentVariable(allocator, "XDG_RUNTIME_DIR", uid_str, false); } - - if (environment.xdg_desktop_names) |xdg_desktop_names| try interop.setEnvironmentVariable(allocator, "XDG_CURRENT_DESKTOP", xdg_desktop_names, false); - try interop.setEnvironmentVariable(allocator, "XDG_SESSION_CLASS", "user", false); - try interop.setEnvironmentVariable(allocator, "XDG_SESSION_ID", "1", false); - if (environment.xdg_session_desktop) |desktop_name| try interop.setEnvironmentVariable(allocator, "XDG_SESSION_DESKTOP", desktop_name, false); - try interop.setEnvironmentVariable(allocator, "XDG_SEAT", "seat0", false); - try interop.setEnvironmentVariable(allocator, "XDG_VTNR", tty_str, false); } fn loginConv( From c0c400e0b6c162c81585bd7a7f13f5f91db00bb9 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Tue, 30 Dec 2025 19:35:40 +0100 Subject: [PATCH 011/176] Recursively create xauth file directory if non-existent Signed-off-by: AnErrupTion --- src/auth.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/auth.zig b/src/auth.zig index 8f7ae5d..defce47 100644 --- a/src/auth.zig +++ b/src/auth.zig @@ -369,7 +369,7 @@ fn createXauthFile(pwd: []const u8, buffer: []u8) ![]const u8 { const xauthority: []u8 = try std.fmt.bufPrint(buffer, "{s}/{s}", .{ trimmed_xauth_dir, xauth_file }); - std.fs.makeDirAbsolute(trimmed_xauth_dir) catch {}; + std.fs.cwd().makePath(trimmed_xauth_dir) catch {}; const file = try std.fs.createFileAbsolute(xauthority, .{}); file.close(); From 135d1e40f6c6658a0f5a0d9e49dfee3df765140e Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Tue, 6 Jan 2026 22:18:31 +0100 Subject: [PATCH 012/176] Update termbox2 Signed-off-by: AnErrupTion --- build.zig.zon | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.zig.zon b/build.zig.zon index da2e056..caaa439 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -13,8 +13,8 @@ .hash = "zigini-0.3.3-36M0FRJJAADZVq5HPm-hYKMpFFTr0OgjbEYcK2ijKZ5n", }, .termbox2 = .{ - .url = "git+https://github.com/AnErrupTion/termbox2?ref=master#290ac6b8225aacfd16851224682b851b65fcb918", - .hash = "N-V-__8AAGcUBQAa5vov1Yi_9AXEffFQ1e2KsXaK4dgygRKq", + .url = "git+https://github.com/AnErrupTion/termbox2?ref=master#918369f9697dc8bb5927f69b3277a3ffbb920df4", + .hash = "N-V-__8AANIVBQBaDTuwFu-ndBuC1-k_th40iguIyAA70QPW", }, }, .paths = .{""}, From 5a51d5ced50f23ab7c340da477ece35803e7db81 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Tue, 6 Jan 2026 22:27:36 +0100 Subject: [PATCH 013/176] Fix building on musl (closes #760) Signed-off-by: AnErrupTion --- build.zig.zon | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.zig.zon b/build.zig.zon index caaa439..0c61dd9 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -13,8 +13,8 @@ .hash = "zigini-0.3.3-36M0FRJJAADZVq5HPm-hYKMpFFTr0OgjbEYcK2ijKZ5n", }, .termbox2 = .{ - .url = "git+https://github.com/AnErrupTion/termbox2?ref=master#918369f9697dc8bb5927f69b3277a3ffbb920df4", - .hash = "N-V-__8AANIVBQBaDTuwFu-ndBuC1-k_th40iguIyAA70QPW", + .url = "git+https://github.com/AnErrupTion/termbox2?ref=master#496730697c662893eec43192f48ff616c2539da6", + .hash = "N-V-__8AAOEWBQDt5tNdIzIFY6n8DdZsCP-6MyLoNS20wgpA", }, }, .paths = .{""}, From 82d24d772500baef8e3dac1f0e0048397543a9d3 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Sat, 17 Jan 2026 10:48:12 +0100 Subject: [PATCH 014/176] Add donation links Signed-off-by: AnErrupTion --- .github/FUNDING.yml | 2 ++ readme.md | 8 ++++++-- 2 files changed, 8 insertions(+), 2 deletions(-) create mode 100644 .github/FUNDING.yml diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..ba1041c --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,2 @@ +github: AnErrupTion +liberapay: ShiningLea diff --git a/readme.md b/readme.md index b7afade..8862999 100644 --- a/readme.md +++ b/readme.md @@ -121,13 +121,12 @@ that Ly will run on, otherwise bad things will happen. For example, to disable ` # systemctl disable getty@tty2.service ``` -On platforms that use systemd-logind to dynamically start `autovt@.service` instances when the switch to a new tty occurs, any ly instances for ttys *except the default tty* need to be enabled using a different mechanism: To autostart ly on switch to `tty2`, do not enable any `ly` unit directly, instead symlink `autovt@tty2.service` to `ly@tty2.service` within `/usr/lib/systemd/system/` (analogous for every other tty you want to enable ly on). +On platforms that use systemd-logind to dynamically start `autovt@.service` instances when the switch to a new tty occurs, any ly instances for ttys _except the default tty_ need to be enabled using a different mechanism: To autostart ly on switch to `tty2`, do not enable any `ly` unit directly, instead symlink `autovt@tty2.service` to `ly@tty2.service` within `/usr/lib/systemd/system/` (analogous for every other tty you want to enable ly on). The target of the symlink, `ly@ttyN.service`, does not actually exist, but systemd nevertheless recognizes that the instanciation of `autovt@.service` with `%I` equal to `ttyN` now points to an instanciation of `ly@.service` with `%I` set to `ttyN`. Compare to `man 5 logind.conf`, especially regarding the `NAutoVTs=` and `ReserveVT=` parameters. - On non-systemd systems, you can change the TTY Ly will run on by editing the corresponding service file for your platform. @@ -294,3 +293,8 @@ A typical shebang for a shell script looks like this: The name "Ly" is a tribute to the fairy from the game Rayman. Ly was tested by oxodao, who is some seriously awesome dude. + +### Donate + +If you like Ly and wish to support my work further, feel free to donate via my +[Liberapay link](https://liberapay.com/ShiningLea)! From 94c306758a92d6e9cfb0b4c940fb1cbd505847e1 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Sat, 17 Jan 2026 16:58:05 +0100 Subject: [PATCH 015/176] Update Matrix space link (envs.net migration) Signed-off-by: AnErrupTion --- readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/readme.md b/readme.md index 8862999..fc281e3 100644 --- a/readme.md +++ b/readme.md @@ -5,7 +5,7 @@ Ly is a lightweight TUI (ncurses-like) display manager for Linux and BSD, designed with portability in mind (e.g. it does not require systemd to run). -Join us on Matrix over at [#ly:envs.net](https://matrix.to/#/#ly:envs.net)! +Join us on Matrix over at [#ly-dm:matrix.org](https://matrix.to/#/#ly-dm:matrix.org)! **Note**: Development happens on [Codeberg](https://codeberg.org/fairyglade/ly) with a mirror on [GitHub](https://github.com/fairyglade/ly). From 2b1e4dc6c95bfbf983357e725b95f7c398fb24f0 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Sun, 18 Jan 2026 18:03:21 +0100 Subject: [PATCH 016/176] Fix undefined value in XCB connection check Signed-off-by: AnErrupTion --- src/auth.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/auth.zig b/src/auth.zig index defce47..ad49de1 100644 --- a/src/auth.zig +++ b/src/auth.zig @@ -437,7 +437,7 @@ fn executeX11Cmd(log_file: *LogFile, allocator: std.mem.Allocator, shell: []cons std.process.exit(1); } - var ok: c_int = undefined; + var ok: c_int = -1; var xcb: ?*interop.xcb.xcb_connection_t = null; while (ok != 0) { xcb = interop.xcb.xcb_connect(null, null); From 456916f0592578470540ee1a3c22129b95b9b555 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Sun, 18 Jan 2026 20:49:32 +0100 Subject: [PATCH 017/176] Remove unused import in auth.zig Signed-off-by: AnErrupTion --- src/auth.zig | 1 - 1 file changed, 1 deletion(-) diff --git a/src/auth.zig b/src/auth.zig index ad49de1..598f759 100644 --- a/src/auth.zig +++ b/src/auth.zig @@ -1,7 +1,6 @@ const std = @import("std"); const build_options = @import("build_options"); const builtin = @import("builtin"); -const enums = @import("enums.zig"); const Environment = @import("Environment.zig"); const interop = @import("interop.zig"); const SharedError = @import("SharedError.zig"); From d7f64676ee9769a75714b13d0938e2ecbe6e63ea Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Sun, 18 Jan 2026 21:07:53 +0100 Subject: [PATCH 018/176] Split core code into ly-core library Signed-off-by: AnErrupTion --- build.zig | 3 +++ build.zig.zon | 3 +++ ly-core/build.zig | 19 +++++++++++++++++++ ly-core/build.zig.zon | 12 ++++++++++++ {src => ly-core/src}/LogFile.zig | 0 {src => ly-core/src}/SharedError.zig | 0 {src => ly-core/src}/UidRange.zig | 0 {src => ly-core/src}/interop.zig | 2 -- ly-core/src/root.zig | 4 ++++ src/auth.zig | 7 ++++--- src/bigclock.zig | 3 ++- src/bigclock/Lang.zig | 4 ++-- src/enums.zig | 1 + src/main.zig | 11 ++++++----- src/tui/Cell.zig | 4 ++-- src/tui/TerminalBuffer.zig | 6 ++++-- src/tui/components/Text.zig | 3 +-- src/tui/components/generic.zig | 3 +-- 18 files changed, 64 insertions(+), 21 deletions(-) create mode 100644 ly-core/build.zig create mode 100644 ly-core/build.zig.zon rename {src => ly-core/src}/LogFile.zig (100%) rename {src => ly-core/src}/SharedError.zig (100%) rename {src => ly-core/src}/UidRange.zig (100%) rename {src => ly-core/src}/interop.zig (99%) create mode 100644 ly-core/src/root.zig diff --git a/build.zig b/build.zig index e2e6605..924e245 100644 --- a/build.zig +++ b/build.zig @@ -72,6 +72,9 @@ pub fn build(b: *std.Build) !void { .use_llvm = true, }); + const ly_core = b.dependency("ly_core", .{ .target = target, .optimize = optimize }); + exe.root_module.addImport("ly-core", ly_core.module("ly-core")); + const zigini = b.dependency("zigini", .{ .target = target, .optimize = optimize }); exe.root_module.addImport("zigini", zigini.module("zigini")); diff --git a/build.zig.zon b/build.zig.zon index 0c61dd9..fc1349d 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -4,6 +4,9 @@ .fingerprint = 0xa148ffcc5dc2cb59, .minimum_zig_version = "0.15.0", .dependencies = .{ + .ly_core = .{ + .path = "ly-core", + }, .clap = .{ .url = "git+https://github.com/Hejsil/zig-clap#5289e0753cd274d65344bef1c114284c633536ea", .hash = "clap-0.11.0-oBajB-HnAQDPCKYzwF7rO3qDFwRcD39Q0DALlTSz5H7e", diff --git a/ly-core/build.zig b/ly-core/build.zig new file mode 100644 index 0000000..0fff11c --- /dev/null +++ b/ly-core/build.zig @@ -0,0 +1,19 @@ +const std = @import("std"); + +pub fn build(b: *std.Build) void { + const target = b.standardTargetOptions(.{}); + const optimize = b.standardOptimizeOption(.{}); + const mod = b.addModule("ly-core", .{ + .root_source_file = b.path("src/root.zig"), + .target = target, + .optimize = optimize, + }); + + const mod_tests = b.addTest(.{ + .root_module = mod, + }); + const run_mod_tests = b.addRunArtifact(mod_tests); + + const test_step = b.step("test", "Run tests"); + test_step.dependOn(&run_mod_tests.step); +} diff --git a/ly-core/build.zig.zon b/ly-core/build.zig.zon new file mode 100644 index 0000000..13a7faf --- /dev/null +++ b/ly-core/build.zig.zon @@ -0,0 +1,12 @@ +.{ + .name = .ly_core, + .version = "1.0.0", + .fingerprint = 0xddda7afda795472, + .minimum_zig_version = "0.15.0", + .dependencies = .{}, + .paths = .{ + "build.zig", + "build.zig.zon", + "src", + }, +} diff --git a/src/LogFile.zig b/ly-core/src/LogFile.zig similarity index 100% rename from src/LogFile.zig rename to ly-core/src/LogFile.zig diff --git a/src/SharedError.zig b/ly-core/src/SharedError.zig similarity index 100% rename from src/SharedError.zig rename to ly-core/src/SharedError.zig diff --git a/src/UidRange.zig b/ly-core/src/UidRange.zig similarity index 100% rename from src/UidRange.zig rename to ly-core/src/UidRange.zig diff --git a/src/interop.zig b/ly-core/src/interop.zig similarity index 99% rename from src/interop.zig rename to ly-core/src/interop.zig index 4d52f49..5709e8a 100644 --- a/src/interop.zig +++ b/ly-core/src/interop.zig @@ -2,8 +2,6 @@ const std = @import("std"); const builtin = @import("builtin"); const UidRange = @import("UidRange.zig"); -pub const termbox = @import("termbox2"); - pub const pam = @cImport({ @cInclude("security/pam_appl.h"); }); diff --git a/ly-core/src/root.zig b/ly-core/src/root.zig new file mode 100644 index 0000000..afc7ea0 --- /dev/null +++ b/ly-core/src/root.zig @@ -0,0 +1,4 @@ +pub const interop = @import("interop.zig"); +pub const UidRange = @import("UidRange.zig"); +pub const LogFile = @import("LogFile.zig"); +pub const SharedError = @import("SharedError.zig"); diff --git a/src/auth.zig b/src/auth.zig index 598f759..4c885e0 100644 --- a/src/auth.zig +++ b/src/auth.zig @@ -1,12 +1,13 @@ const std = @import("std"); const build_options = @import("build_options"); const builtin = @import("builtin"); +const ly_core = @import("ly-core"); const Environment = @import("Environment.zig"); -const interop = @import("interop.zig"); -const SharedError = @import("SharedError.zig"); -const LogFile = @import("LogFile.zig"); const Md5 = std.crypto.hash.Md5; +const interop = ly_core.interop; +const SharedError = ly_core.SharedError; +const LogFile = ly_core.LogFile; const utmp = interop.utmp; const Utmp = utmp.utmpx; diff --git a/src/bigclock.zig b/src/bigclock.zig index 63aa02a..bb3617c 100644 --- a/src/bigclock.zig +++ b/src/bigclock.zig @@ -1,11 +1,12 @@ const std = @import("std"); -const interop = @import("interop.zig"); +const ly_core = @import("ly-core"); const enums = @import("enums.zig"); const Lang = @import("bigclock/Lang.zig"); const en = @import("bigclock/en.zig"); const fa = @import("bigclock/fa.zig"); const Cell = @import("tui/Cell.zig"); +const interop = ly_core.interop; const Bigclock = enums.Bigclock; pub const WIDTH = Lang.WIDTH; pub const HEIGHT = Lang.HEIGHT; diff --git a/src/bigclock/Lang.zig b/src/bigclock/Lang.zig index 3e3be4e..b09b552 100644 --- a/src/bigclock/Lang.zig +++ b/src/bigclock/Lang.zig @@ -1,10 +1,10 @@ -const interop = @import("../interop.zig"); +const ly_core = @import("ly-core"); pub const WIDTH = 5; pub const HEIGHT = 5; pub const SIZE = WIDTH * HEIGHT; -pub const X: u32 = if (interop.supportsUnicode()) 0x2593 else '#'; +pub const X: u32 = if (ly_core.interop.supportsUnicode()) 0x2593 else '#'; pub const O: u32 = 0; // zig fmt: off diff --git a/src/enums.zig b/src/enums.zig index 337d6bf..2cec482 100644 --- a/src/enums.zig +++ b/src/enums.zig @@ -1,4 +1,5 @@ const std = @import("std"); + pub const Animation = enum { none, doom, diff --git a/src/main.zig b/src/main.zig index f1d7c4a..2cae9b6 100644 --- a/src/main.zig +++ b/src/main.zig @@ -1,13 +1,13 @@ const std = @import("std"); const build_options = @import("build_options"); const builtin = @import("builtin"); +const ly_core = @import("ly-core"); const clap = @import("clap"); const ini = @import("zigini"); const auth = @import("auth.zig"); const bigclock = @import("bigclock.zig"); const enums = @import("enums.zig"); const Environment = @import("Environment.zig"); -const interop = @import("interop.zig"); const ColorMix = @import("animations/ColorMix.zig"); const Doom = @import("animations/Doom.zig"); const Dummy = @import("animations/Dummy.zig"); @@ -25,15 +25,16 @@ const Lang = @import("config/Lang.zig"); const OldSave = @import("config/OldSave.zig"); const SavedUsers = @import("config/SavedUsers.zig"); const migrator = @import("config/migrator.zig"); -const SharedError = @import("SharedError.zig"); -const LogFile = @import("LogFile.zig"); -const UidRange = @import("UidRange.zig"); const StringList = std.ArrayListUnmanaged([]const u8); const Ini = ini.Ini; const DisplayServer = enums.DisplayServer; const Entry = Environment.Entry; -const termbox = interop.termbox; +const interop = ly_core.interop; +const UidRange = ly_core.UidRange; +const LogFile = ly_core.LogFile; +const SharedError = ly_core.SharedError; +const termbox = TerminalBuffer.termbox; const temporary_allocator = std.heap.page_allocator; const ly_version_str = "Ly version " ++ build_options.version; diff --git a/src/tui/Cell.zig b/src/tui/Cell.zig index 66d06f8..6e1ca45 100644 --- a/src/tui/Cell.zig +++ b/src/tui/Cell.zig @@ -1,6 +1,6 @@ -const interop = @import("../interop.zig"); +const TerminalBuffer = @import("TerminalBuffer.zig"); -const termbox = interop.termbox; +const termbox = TerminalBuffer.termbox; const Cell = @This(); diff --git a/src/tui/TerminalBuffer.zig b/src/tui/TerminalBuffer.zig index 999dfca..dc5c69b 100644 --- a/src/tui/TerminalBuffer.zig +++ b/src/tui/TerminalBuffer.zig @@ -1,10 +1,12 @@ const std = @import("std"); -const interop = @import("../interop.zig"); +const ly_core = @import("ly-core"); const Cell = @import("Cell.zig"); +pub const termbox = @import("termbox2"); + const Random = std.Random; -const termbox = interop.termbox; +const interop = ly_core.interop; const TerminalBuffer = @This(); diff --git a/src/tui/components/Text.zig b/src/tui/components/Text.zig index f205b91..25fff7c 100644 --- a/src/tui/components/Text.zig +++ b/src/tui/components/Text.zig @@ -1,11 +1,10 @@ const std = @import("std"); -const interop = @import("../../interop.zig"); const TerminalBuffer = @import("../TerminalBuffer.zig"); const Allocator = std.mem.Allocator; const DynamicString = std.ArrayListUnmanaged(u8); -const termbox = interop.termbox; +const termbox = TerminalBuffer.termbox; const Text = @This(); diff --git a/src/tui/components/generic.zig b/src/tui/components/generic.zig index 2f3c25a..004be2b 100644 --- a/src/tui/components/generic.zig +++ b/src/tui/components/generic.zig @@ -1,5 +1,4 @@ const std = @import("std"); -const interop = @import("../../interop.zig"); const TerminalBuffer = @import("../TerminalBuffer.zig"); pub fn CyclableLabel(comptime ItemType: type, comptime ChangeItemType: type) type { @@ -9,7 +8,7 @@ pub fn CyclableLabel(comptime ItemType: type, comptime ChangeItemType: type) typ const DrawItemFn = *const fn (*Self, ItemType, usize, usize) bool; const ChangeItemFn = *const fn (ItemType, ?ChangeItemType) void; - const termbox = interop.termbox; + const termbox = TerminalBuffer.termbox; const Self = @This(); From 2eea68307887721065fe912647463bd2b7fddcbb Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Sun, 18 Jan 2026 21:35:49 +0100 Subject: [PATCH 019/176] Fix wrong session being chosen in autologin (closes #911) Signed-off-by: AnErrupTion --- res/config.ini | 4 ++-- src/main.zig | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/res/config.ini b/res/config.ini index 52fb15d..06d6a2f 100644 --- a/res/config.ini +++ b/res/config.ini @@ -62,7 +62,7 @@ auto_login_service = ly-autologin # To find available session names, check the .desktop files in: # - /usr/share/xsessions/ (for X11 sessions) # - /usr/share/wayland-sessions/ (for Wayland sessions) -# Use the filename without .desktop extension, or the value of DesktopNames field +# Use the filename without .desktop extension, the Name field inside the file or the value of the DesktopNames field # Examples: "i3", "sway", "gnome", "plasma", "xfce" # If null, automatic login is disabled auto_login_session = null @@ -200,7 +200,7 @@ fg = 0x00FFFFFF # TB_WHITE 0x0008 # If full color is off, the styling options still work. The colors are # always 32-bit values with the styling in the most significant byte. -# Note: If using the dur_file animation option and the dur file's color range +# Note: If using the dur_file animation option and the dur file's color range # is saved as 256 with this option disabled, the file will not be drawn. full_color = true diff --git a/src/main.zig b/src/main.zig index 2cae9b6..1faae80 100644 --- a/src/main.zig +++ b/src/main.zig @@ -1300,14 +1300,14 @@ fn isValidUsername(username: []const u8, usernames: StringList) bool { fn findSessionByName(session: *Session, name: []const u8) ?usize { for (session.label.list.items, 0..) |env, i| { + if (std.ascii.eqlIgnoreCase(env.environment.file_name, name)) return i; + if (std.ascii.eqlIgnoreCase(env.environment.name, name)) return i; if (env.environment.xdg_session_desktop) |session_desktop| { if (session_desktop.len > 0 and std.ascii.eqlIgnoreCase(session_desktop, name)) return i; } if (env.environment.xdg_desktop_names) |session_desktop_name| { if (std.ascii.eqlIgnoreCase(session_desktop_name, name)) return i; } - if (std.ascii.eqlIgnoreCase(env.environment.name, name)) return i; - if (std.ascii.eqlIgnoreCase(env.environment.file_name, name)) return i; } return null; } From a4076b83da2602fe69a42f6980f5a71672d29aca Mon Sep 17 00:00:00 2001 From: hynak Date: Sun, 25 Jan 2026 23:08:42 +0100 Subject: [PATCH 020/176] [dur] Add support for alignments and negative offsets + Ly logo (#893) ## What are the changes about? Add support for letting a user use a negative offset (#880), alignment, and logo. Below is example of the logo file, I hope it is what was request :). It has no padding so a user can move the alignment and offset to get it how they want on screen. This technically is good to go, except I didn't upload the logo file as I'm not sure where to add the animation file to get it to here: $CONFIG_DIRECTORY/ly/example.dur ![logo-preview](/attachments/5a829dbd-7708-4d0a-9841-d024902ede68) ## What existing issue does this resolve? #880 ## Pre-requisites - [x] I have tested & confirmed the changes work locally Reviewed-on: https://codeberg.org/fairyglade/ly/pulls/893 Reviewed-by: AnErrupTion Co-authored-by: hynak Co-committed-by: hynak --- build.zig | 2 + res/config.ini | 9 ++- res/example.dur | Bin 0 -> 2061 bytes src/animations/DurFile.zig | 119 ++++++++++++++++++++++++++++--------- src/config/Config.zig | 6 +- src/enums.zig | 12 ++++ src/main.zig | 2 +- 7 files changed, 116 insertions(+), 34 deletions(-) create mode 100644 res/example.dur diff --git a/build.zig b/build.zig index 924e245..71508cf 100644 --- a/build.zig +++ b/build.zig @@ -197,6 +197,8 @@ fn install_ly(allocator: std.mem.Allocator, patch_map: PatchMap, install_config: const patched_setup = try patchFile(allocator, "res/setup.sh", patch_map); try installText(patched_setup, config_dir, ly_config_directory, "setup.sh", .{ .mode = 0o755 }); + + try installFile("res/example.dur", config_dir, ly_config_directory, "example.dur", .{ .override_mode = 0o755 }); } { diff --git a/res/config.ini b/res/config.ini index 06d6a2f..7a29aa6 100644 --- a/res/config.ini +++ b/res/config.ini @@ -167,10 +167,15 @@ doom_bottom_color = 0x00FFFFFF # Dur file path dur_file_path = $CONFIG_DIRECTORY/ly/example.dur -# Dur offset x direction +# Dur file alignment +# The dur file can be aligned with a direction and centered easily with the flags below +# Available inputs: topleft, topcenter, topright, centerleft, center, centerright, bottomleft, bottomcenter, bottomright +dur_offset_alignment = center + +# Dur offset x direction (value is added to the current position determined by alignment, negatives are supported) dur_x_offset = 0 -# Dur offset y direction +# Dur offset y direction (value is added to the current position determined by alignment, negatives are supported) dur_y_offset = 0 # Set margin to the edges of the DM (useful for curved monitors) diff --git a/res/example.dur b/res/example.dur new file mode 100644 index 0000000000000000000000000000000000000000..00e82062df1126bc4678fae8eb4ec6a86320002d GIT binary patch literal 2061 zcmV+o2=ezIiwFqz%0y`b|7Kxna(OLhY++<&Eo^URZ!<1rb#eggT}yA{HW0q&R}jAV z;u4~Sjp*cB>}`82T4V#rO`O7jV=ok^*mjZsUdgs>J@^<>q(pKRAXZFK!}&Pmh>!Wg zA2$fietpls?El%UXLsmF@x4fS+2^mT!|&_-ZL{AM$?4*h{a{F&R()UPht1oep7N3x zS@PAP%yx^TbL!jXzx5wQ%BQrJ@Mn<_-&RF_Kjf<-VfVh>)fUlys%C1@By1qQM?nyl_5?5-% z)ayyiRHvm(DHS)gL0l1xD&bT{+{|&&02c|Q zt-{yYQHL(>gs(~)Xu>bNEwa?1D6(yovTiT{bH{K*c0*K4_|zJ@jg({{bsA|HWtA-x zP~$!+kP%+@tt-K8@try*?CMBl-RGhT&+`(}Y^!2lkaRo?=TrD-%QfMX7UEY=R#P$M*@UE<(JXglMDj9-l@E;xd1`=^+3mj zKutohY7BD_0QF8CZ%CtceB%&wjX{TXs=CJT3qqvYp6DpAw#bN^8VZ7OW2CH$o;NTz zMl$zYs)nS`NYwxmo)sdwg-lL)sIi8Wf?gf=AW+!MA_ZTVn1`7D%bbs>!U4_7uocu^ z0kFm*%4r>f3`u_o5j)X11TG+52v$83x96sU7St0$IuHbSClv`B-U<=S3d&M=R-m>$ zQUrSg_pkyT%?if)0-Ze4DDQ=#Jd&4hXNzN`$o3d1fRU=t^97%0Z2`=keO|$^xQ?W} zb>Yxz9`$ieb({ss09LjK+h3S=c>>*QGqdCA-lnaW`_2LE{)ZET5l$pbIPpkRVA~WS zIn*xCfPiWMu76j5eJo_Wz_>Rr5Myb$a6Jn<0vVGABZB#p6?w2o3_gkwN-AX>FPJAr z7(Sm=gqJ%OXna?3P~<{71v4lJ09EFss|Y4@h!RZdR+|FW5g{PCH5BXUg^7^y)M6;L zBsSF&P$fhN;W!8=&sfBRStjSI#$su)GDBHR`Q;bHsGwN* zL;_{;u>pyAa;yqfCw|LN8sRB{kSMds)y2vG6L${+HFB9CnB1jv5=oE{Xul2|I0WiyLyG)> zpd82N0tAc3aDg4#6A2)ec+|LoK332}(kB?p;@D3Gkb$T^i%!)f1mj2moglRWA?gnS z791TZ05TqDtfjjoR9YMuVk=->jrkxkhS*+~H{H##`S`{dfV|NI0)=>5TPz@>^dE7r zHVeTL2}|q`ZN<>opu&xAg`R{&s}O!3E4bVj!tqExzZZOdJNrCF3Sgw_^L)YQSz7>e zXP;Lv0$4{1bv4hEHPrxCwte1T2w;~dfW5Y9)7BvfQvicQ5CVvV2`3&YfT*x=V)Md@ zkaWu!J$Z?VE9VuOp*M0)vuQ{4V|eU8SB3->Cy@OT9?xMm^5XCtkF!aQu^u&t5pj;S zXfZVvL2NmON@}HaRmVyk26)kIYGk5LjcjO)ktQ`V=_hKO=JF_D3u^r?CBJP%cqbu1y3XkYJj6PRkIUGB_IL*2 zRFOKQ!i8LtxtI|&=dgogq~*zE{%j&d21ba?6d8VMC+1=KL9K!#Ua=B?OS2FtNeuVk zk789Onqfku9vN;NLL^3#fndTPS*j6S&2a}Y0;Pep!9uA@pfJMYp0G&0S}lp3T6-Q8 z0(E?~B=QP^fjvo=A-HY@IzjM%FT)e+)5M|k3j%euA!$938USQ2>c+q(L24!HX6WFX zf&ZcTFpep8F%FRWvAa6p2JBb?rDk{l14kyqPKg+G)@C$7dDMxg#W3-K|M#w$VEv&W z3{9D6_iEoxShQya08Pm>18S(_3AOs_*bppI={R5qXh_Bgjgu96;Ii8n=;VS~@RYpMaPZ2P>w5Wp@^0DEoIrmaH|rT~U1 r5@w1=3N$J#oY=f@A|$;yVezK=_Hpy^KL7v#|NjF3YFqWid#V5c!Pn9S literal 0 HcmV?d00001 diff --git a/src/animations/DurFile.zig b/src/animations/DurFile.zig index 86b951d..9ec9eaf 100644 --- a/src/animations/DurFile.zig +++ b/src/animations/DurFile.zig @@ -2,6 +2,8 @@ const std = @import("std"); const Animation = @import("../tui/Animation.zig"); const Cell = @import("../tui/Cell.zig"); const TerminalBuffer = @import("../tui/TerminalBuffer.zig"); +const enums = @import("../enums.zig"); +const DurOffsetAlignment = enums.DurOffsetAlignment; const Color = TerminalBuffer.Color; const Styling = TerminalBuffer.Styling; const Allocator = std.mem.Allocator; @@ -286,25 +288,74 @@ fn convert_256_to_rgb(color_256: u32) u32 { return rgb_color; } +const UVec2 = @Vector(2, u32); +const IVec2 = @Vector(2, i64); + +const VEC_X = 0; +const VEC_Y = 1; + const DurFile = @This(); allocator: Allocator, terminal_buffer: *TerminalBuffer, -frames: u64, -time_previous: i64, -x_offset: u32, -y_offset: u32, -full_color: bool, dur_movie: DurFormat, -frame_width: u32, -frame_height: u32, +frames: u64, +frame_size: UVec2, +start_pos: IVec2, +full_color: bool, frame_time: u32, +time_previous: i64, is_color_format_16: bool, +offset_alignment: DurOffsetAlignment, +offset: IVec2, -pub fn init(allocator: Allocator, terminal_buffer: *TerminalBuffer, log_writer: *std.io.Writer, file_path: []const u8, x_offset: u32, y_offset: u32, full_color: bool) !DurFile { +// if the user has an even number of columns or rows, we will default to the left or higher position (e.g. 4 columns center = .x..) +fn center(v: u32) i64 { + return @intCast((v / 2) + (v % 2)); +} + +fn calc_start_position(terminal_buffer: *TerminalBuffer, dur_movie: *DurFormat, offset_alignment: DurOffsetAlignment, offset: IVec2) IVec2 { + const buf_width: u32 = @intCast(terminal_buffer.width); + const buf_height: u32 = @intCast(terminal_buffer.height); + + var movie_width: u32 = @intCast(dur_movie.columns.?); + var movie_height: u32 = @intCast(dur_movie.lines.?); + + if (movie_width > buf_width) movie_width = buf_width; + if (movie_height > buf_height) movie_height = buf_height; + + const start_pos: IVec2 = switch (offset_alignment) { + DurOffsetAlignment.center => .{ center(buf_width) - center(movie_width), center(buf_height) - center(movie_height) }, + DurOffsetAlignment.topleft => .{ 0, 0 }, + DurOffsetAlignment.topcenter => .{ center(buf_width) - center(movie_width), 0 }, + DurOffsetAlignment.topright => .{ buf_width - movie_width, 0 }, + DurOffsetAlignment.centerleft => .{ 0, center(buf_height) - center(movie_height) }, + DurOffsetAlignment.centerright => .{ buf_width - movie_width, center(buf_height) - center(movie_height) }, + DurOffsetAlignment.bottomleft => .{ 0, buf_height - movie_height }, + DurOffsetAlignment.bottomcenter => .{ center(buf_width) - center(movie_width), buf_height - movie_height }, + DurOffsetAlignment.bottomright => .{ buf_width - movie_width, buf_height - movie_height }, + }; + + return start_pos + offset; +} + +fn calc_frame_size(terminal_buffer: *TerminalBuffer, dur_movie: *DurFormat) UVec2 { + const buf_width: u32 = @intCast(terminal_buffer.width); + const buf_height: u32 = @intCast(terminal_buffer.height); + + const movie_width: u32 = @intCast(dur_movie.columns.?); + const movie_height: u32 = @intCast(dur_movie.lines.?); + + // Draw only the needed amount if movie smaller than screen. If movie is bigger, we will just draw entire screen + const frame_width = if (movie_width < buf_width) movie_width else buf_width; + const frame_height = if (movie_height < buf_height) movie_height else buf_height; + + return .{ frame_width, frame_height }; +} + +pub fn init(allocator: Allocator, terminal_buffer: *TerminalBuffer, log_writer: *std.io.Writer, file_path: []const u8, offset_alignment: DurOffsetAlignment, x_offset: i32, y_offset: i32, full_color: bool) !DurFile { var dur_movie: DurFormat = .init(allocator); - // error state is recoverable when thrown to main and results in no background with Dummy in main dur_movie.create_from_file(allocator, file_path) catch |err| switch (err) { error.FileNotFound => { try log_writer.print("error: dur_file was not found at: {s}\n", .{file_path}); @@ -324,19 +375,10 @@ pub fn init(allocator: Allocator, terminal_buffer: *TerminalBuffer, log_writer: return error.InvalidColorFormat; } - const buf_width: u32 = @intCast(terminal_buffer.width); - const buf_height: u32 = @intCast(terminal_buffer.height); + const offset: IVec2 = .{ x_offset, y_offset }; - const movie_width: u32 = @intCast(dur_movie.columns.?); - const movie_height: u32 = @intCast(dur_movie.lines.?); - - // Clamp to prevent user from exceeding draw window - const x_offset_clamped = std.math.clamp(x_offset, 0, buf_width - 1); - const y_offset_clamped = std.math.clamp(y_offset, 0, buf_height - 1); - - // Ensure if user offsets and frame goes offscreen, it will not overflow draw - const frame_width = if ((movie_width + x_offset_clamped) < buf_width) movie_width else buf_width - x_offset_clamped; - const frame_height = if ((movie_height + y_offset_clamped) < buf_height) movie_height else buf_height - y_offset_clamped; + const start_pos = calc_start_position(terminal_buffer, &dur_movie, offset_alignment, offset); + const frame_size = calc_frame_size(terminal_buffer, &dur_movie); // Convert dur fps to frames per ms const frame_time: u32 = @intFromFloat(1000 / dur_movie.framerate.?); @@ -346,14 +388,14 @@ pub fn init(allocator: Allocator, terminal_buffer: *TerminalBuffer, log_writer: .terminal_buffer = terminal_buffer, .frames = 0, .time_previous = std.time.milliTimestamp(), - .x_offset = x_offset_clamped, - .y_offset = y_offset_clamped, + .frame_size = frame_size, + .start_pos = start_pos, .full_color = full_color, .dur_movie = dur_movie, - .frame_width = frame_width, - .frame_height = frame_height, .frame_time = frame_time, .is_color_format_16 = eql(u8, dur_movie.colorFormat.?, "16"), + .offset_alignment = offset_alignment, + .offset = offset, }; } @@ -365,15 +407,34 @@ fn deinit(self: *DurFile) void { self.dur_movie.deinit(); } -fn realloc(_: *DurFile) anyerror!void {} +fn realloc(self: *DurFile) anyerror!void { + // when terminal size changes, we need to recalculate the start_pos and frame_size based on the new size + self.start_pos = calc_start_position(self.terminal_buffer, &self.dur_movie, self.offset_alignment, self.offset); + self.frame_size = calc_frame_size(self.terminal_buffer, &self.dur_movie); +} fn draw(self: *DurFile) void { const current_frame = self.dur_movie.frames.items[self.frames]; - for (0..self.frame_height) |y| { + const buf_width: u32 = @intCast(self.terminal_buffer.width); + const buf_height: u32 = @intCast(self.terminal_buffer.height); + + // y is used as an iterator in the durformat, while cell_y gives us the correct placement for the cell (same for x) + for (0..self.frame_size[VEC_Y]) |y| { + const y_offset_i = @as(i32, @intCast(y)) + self.start_pos[VEC_Y]; + // we skip the pass if it falls outside of the draw window (ensure no int underflow) + const cell_y: u32 = if (y_offset_i >= 0 and y_offset_i < buf_height) @intCast(y_offset_i) else continue; + var iter = std.unicode.Utf8View.initUnchecked(current_frame.contents[y]).iterator(); - for (0..self.frame_width) |x| { + for (0..self.frame_size[VEC_X]) |x| { + const x_offset_i = @as(i32, @intCast(x)) + self.start_pos[VEC_X]; + // skip pass, same as y but also increment the codepoint iter to fetch correct values in later passes + const cell_x: u32 = if (x_offset_i >= 0 and x_offset_i < buf_width) @intCast(x_offset_i) else { + _ = iter.nextCodepoint().?; + continue; + }; + const codepoint: u21 = iter.nextCodepoint().?; const color_map = current_frame.colorMap[x][y]; @@ -390,7 +451,7 @@ fn draw(self: *DurFile) void { const cell = Cell{ .ch = @intCast(codepoint), .fg = fg_color, .bg = bg_color }; - cell.put(x + self.x_offset, y + self.y_offset); + cell.put(cell_x, cell_y); } } diff --git a/src/config/Config.zig b/src/config/Config.zig index e9169bd..3e0cf1e 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -5,6 +5,7 @@ const Animation = enums.Animation; const Input = enums.Input; const ViMode = enums.ViMode; const Bigclock = enums.Bigclock; +const DurOffsetAlignment = enums.DurOffsetAlignment; allow_empty_password: bool = true, animation: Animation = .none, @@ -43,8 +44,9 @@ doom_top_color: u32 = 0x00FF0000, doom_middle_color: u32 = 0x00FFFF00, doom_bottom_color: u32 = 0x00FFFFFF, dur_file_path: []const u8 = build_options.config_directory ++ "/ly/example.dur", -dur_x_offset: u32 = 0, -dur_y_offset: u32 = 0, +dur_offset_alignment: DurOffsetAlignment = .center, +dur_x_offset: i32 = 0, +dur_y_offset: i32 = 0, edge_margin: u8 = 0, error_bg: u32 = 0x00000000, error_fg: u32 = 0x01FF0000, diff --git a/src/enums.zig b/src/enums.zig index 2cec482..47771da 100644 --- a/src/enums.zig +++ b/src/enums.zig @@ -54,3 +54,15 @@ pub const Bigclock = enum { en, fa, }; + +pub const DurOffsetAlignment = enum { + topleft, + topcenter, + topright, + centerleft, + center, + centerright, + bottomleft, + bottomcenter, + bottomright, +}; diff --git a/src/main.zig b/src/main.zig index 1faae80..1b9dc2f 100644 --- a/src/main.zig +++ b/src/main.zig @@ -580,7 +580,7 @@ pub fn main() !void { animation = game_of_life.animation(); }, .dur_file => { - var dur = try DurFile.init(allocator, &buffer, log_writer, config.dur_file_path, config.dur_x_offset, config.dur_y_offset, config.full_color); + var dur = try DurFile.init(allocator, &buffer, log_writer, config.dur_file_path, config.dur_offset_alignment, config.dur_x_offset, config.dur_y_offset, config.full_color); animation = dur.animation(); }, } From 5bfa1670cc8ff0ce831ecd2bcdea301f41efc406 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Mon, 26 Jan 2026 20:36:44 +0100 Subject: [PATCH 021/176] Better systemd-homed user detection (fixes #913) Signed-off-by: AnErrupTion --- ly-core/src/interop.zig | 16 ---------------- src/main.zig | 22 +++++++++++++++++++++- 2 files changed, 21 insertions(+), 17 deletions(-) diff --git a/ly-core/src/interop.zig b/ly-core/src/interop.zig index 5709e8a..1ba8613 100644 --- a/ly-core/src/interop.zig +++ b/ly-core/src/interop.zig @@ -79,9 +79,6 @@ fn PlatformStruct() type { pub const vt_activate = vt.VT_ACTIVATE; pub const vt_waitactive = vt.VT_WAITACTIVE; - const SYSTEMD_HOMED_UID_MIN = 60001; - const SYSTEMD_HOMED_UID_MAX = 60513; - pub fn setUserContextImpl(username: [*:0]const u8, entry: UsernameEntry) !void { const status = grp.initgroups(username, @intCast(entry.gid)); if (status != 0) return error.GroupInitializationFailed; @@ -185,19 +182,6 @@ fn PlatformStruct() type { if (!nameFound) return error.UidNameNotFound; - // This code assumes the OS has a login.defs file with UID_MIN - // and UID_MAX values defined in it, which should be the case - // for most systemd-based Linux distributions out there. - // This should be a good enough safeguard for now, as there's - // no reliable (and clean) way to check for systemd support - if (uid_range.uid_min > SYSTEMD_HOMED_UID_MIN) { - uid_range.uid_min = SYSTEMD_HOMED_UID_MIN; - } - - if (uid_range.uid_max < SYSTEMD_HOMED_UID_MAX) { - uid_range.uid_max = SYSTEMD_HOMED_UID_MAX; - } - return uid_range; } diff --git a/src/main.zig b/src/main.zig index 1b9dc2f..3d8e0cb 100644 --- a/src/main.zig +++ b/src/main.zig @@ -1321,13 +1321,33 @@ fn getAllUsernames(allocator: std.mem.Allocator, login_defs_path: []const u8, ui }; }; + // There's no reliable (and clean) way to check for systemd support, so + // let's just define a range and check if a user is within it + const SYSTEMD_HOMED_UID_MIN = 60001; + const SYSTEMD_HOMED_UID_MAX = 60513; + const homed_uid_range = UidRange{ + .uid_min = SYSTEMD_HOMED_UID_MIN, + .uid_max = SYSTEMD_HOMED_UID_MAX, + }; + var usernames: StringList = .empty; var maybe_entry = interop.getNextUsernameEntry(); while (maybe_entry) |entry| { // We check if the UID is equal to 0 because we always want to add root // as a username (even if you can't log into it) - if (entry.uid >= uid_range.uid_min and entry.uid <= uid_range.uid_max or entry.uid == 0 and entry.username != null) { + const is_within_range = + entry.uid >= uid_range.uid_min and + entry.uid <= uid_range.uid_max; + const is_within_homed_range = + builtin.os.tag == .linux and + entry.uid >= homed_uid_range.uid_min and + entry.uid <= homed_uid_range.uid_max; + const is_root = + entry.uid == 0 and + entry.username != null; + + if (is_within_range or is_within_homed_range or is_root) { const username = try allocator.dupe(u8, entry.username.?); try usernames.append(allocator, username); } From a158098df05efa7057485e35dbcb366269659faf Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Mon, 26 Jan 2026 20:45:33 +0100 Subject: [PATCH 022/176] Add config.x_vt option (fixes #910) Signed-off-by: AnErrupTion --- res/config.ini | 5 +++++ src/auth.zig | 3 ++- src/config/Config.zig | 1 + src/main.zig | 1 + 4 files changed, 9 insertions(+), 1 deletion(-) diff --git a/res/config.ini b/res/config.ini index 7a29aa6..cbebcde 100644 --- a/res/config.ini +++ b/res/config.ini @@ -359,6 +359,11 @@ waylandsessions = $PREFIX_DIRECTORY/share/wayland-sessions # Xorg server command x_cmd = $PREFIX_DIRECTORY/bin/X +# Xorg virtual terminal number +# Mostly useful for FreeBSD where choosing the current TTY causes issues +# If null, the current TTY will be chosen +x_vt = null + # Xorg xauthority edition tool xauth_cmd = $PREFIX_DIRECTORY/bin/xauth diff --git a/src/auth.zig b/src/auth.zig index 4c885e0..a70f5da 100644 --- a/src/auth.zig +++ b/src/auth.zig @@ -20,6 +20,7 @@ pub const AuthOptions = struct { setup_cmd: []const u8, login_cmd: ?[]const u8, x_cmd: []const u8, + x_vt: ?u8, session_pid: std.posix.pid_t, }; @@ -189,7 +190,7 @@ fn startSession( .wayland, .shell, .custom => try executeCmd(log_file, allocator, user_entry.shell.?, options, current_environment.is_terminal, current_environment.cmd), .xinitrc, .x11 => if (build_options.enable_x11_support) { var vt_buf: [5]u8 = undefined; - const vt = try std.fmt.bufPrint(&vt_buf, "vt{d}", .{options.tty}); + const vt = try std.fmt.bufPrint(&vt_buf, "vt{d}", .{options.x_vt orelse options.tty}); try executeX11Cmd(log_file, allocator, user_entry.shell.?, user_entry.home.?, options, current_environment.cmd orelse "", vt); }, } diff --git a/src/config/Config.zig b/src/config/Config.zig index 3e0cf1e..d48bf09 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -92,6 +92,7 @@ vi_default_mode: ViMode = .normal, vi_mode: bool = false, waylandsessions: []const u8 = build_options.prefix_directory ++ "/share/wayland-sessions", x_cmd: []const u8 = build_options.prefix_directory ++ "/bin/X", +x_vt: ?u8 = null, xauth_cmd: []const u8 = build_options.prefix_directory ++ "/bin/xauth", xinitrc: ?[]const u8 = "~/.xinitrc", xsessions: []const u8 = build_options.prefix_directory ++ "/share/xsessions", diff --git a/src/main.zig b/src/main.zig index 3d8e0cb..918c5f1 100644 --- a/src/main.zig +++ b/src/main.zig @@ -1086,6 +1086,7 @@ pub fn main() !void { .setup_cmd = config.setup_cmd, .login_cmd = config.login_cmd, .x_cmd = config.x_cmd, + .x_vt = config.x_vt, .session_pid = session_pid, }; From 7934060d3bd8d87acdf8eef38e3b0d01bc02a62b Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Thu, 29 Jan 2026 21:31:45 +0100 Subject: [PATCH 023/176] Credit Kawaii-Ash in the README Signed-off-by: AnErrupTion --- readme.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/readme.md b/readme.md index fc281e3..9014f5e 100644 --- a/readme.md +++ b/readme.md @@ -294,6 +294,8 @@ A typical shebang for a shell script looks like this: The name "Ly" is a tribute to the fairy from the game Rayman. Ly was tested by oxodao, who is some seriously awesome dude. +Also, Ly wouldn't be there today without [Kawaii-Ash](https://github.com/Kawaii-Ash), who has done significant contributions to the project for the Zig rewrite, which lead to the release of Ly v1.0.0. Massive thanks, and sorry for not crediting you enough beforehand! + ### Donate If you like Ly and wish to support my work further, feel free to donate via my From 0c120083272ce8e55a9aebfe796a0e24249350d4 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Sat, 31 Jan 2026 10:36:08 +0100 Subject: [PATCH 024/176] Move more termbox usage into TerminalBuffer Signed-off-by: AnErrupTion --- src/main.zig | 95 +++++++++++++------------------------- src/tui/TerminalBuffer.zig | 82 ++++++++++++++++++++++++++++++-- 2 files changed, 108 insertions(+), 69 deletions(-) diff --git a/src/main.zig b/src/main.zig index 918c5f1..0cc1831 100644 --- a/src/main.zig +++ b/src/main.zig @@ -49,12 +49,12 @@ fn signalHandler(i: c_int) callconv(.c) void { _ = std.c.waitpid(session_pid, &status, 0); } - _ = termbox.tb_shutdown(); + TerminalBuffer.shutdownStatic(); std.c.exit(i); } fn ttyControlTransferSignalHandler(_: c_int) callconv(.c) void { - _ = termbox.tb_shutdown(); + TerminalBuffer.shutdownStatic(); } const ConfigError = struct { @@ -303,37 +303,8 @@ pub fn main() !void { } } - // Initialize termbox - try log_writer.writeAll("initializing termbox2\n"); - _ = termbox.tb_init(); - defer { - log_writer.writeAll("shutting down termbox2\n") catch {}; - _ = termbox.tb_shutdown(); - } - - const act = std.posix.Sigaction{ - .handler = .{ .handler = &signalHandler }, - .mask = std.posix.sigemptyset(), - .flags = 0, - }; - std.posix.sigaction(std.posix.SIG.TERM, &act, null); - - if (config.full_color) { - _ = termbox.tb_set_output_mode(termbox.TB_OUTPUT_TRUECOLOR); - try log_writer.writeAll("termbox2 set to 24-bit color output mode\n"); - } else { - try log_writer.writeAll("termbox2 set to eight-color output mode\n"); - } - - _ = termbox.tb_clear(); - - // Let's take some precautions here and clear the back buffer as well - try ttyClearScreen(); - - // Needed to reset termbox after auth - const tb_termios = try std.posix.tcgetattr(std.posix.STDIN_FILENO); - // Initialize terminal buffer + try log_writer.writeAll("initializing terminal buffer\n"); const labels_max_length = @max(lang.login.len, lang.password.len); var seed: u64 = undefined; @@ -349,10 +320,22 @@ pub fn main() !void { .margin_box_h = config.margin_box_h, .margin_box_v = config.margin_box_v, .input_len = config.input_len, + .full_color = config.full_color, + .labels_max_length = labels_max_length, + .is_tty = true, }; - var buffer = TerminalBuffer.init(buffer_options, labels_max_length, random); + var buffer = try TerminalBuffer.init(buffer_options, &log_file, random); + defer { + log_writer.writeAll("shutting down terminal buffer\n") catch {}; + TerminalBuffer.shutdownStatic(); + } - try log_writer.print("screen resolution is {d}x{d}\n", .{ buffer.width, buffer.height }); + const act = std.posix.Sigaction{ + .handler = .{ .handler = &signalHandler }, + .mask = std.posix.sigemptyset(), + .flags = 0, + }; + std.posix.sigaction(std.posix.SIG.TERM, &act, null); // Initialize components var info_line = InfoLine.init(allocator, &buffer); @@ -637,10 +620,10 @@ pub fn main() !void { if (!update or animate) { if (!update) std.Thread.sleep(std.time.ns_per_ms * 100); - _ = termbox.tb_present(); // Required to update tb_width() and tb_height() - - const width: usize = @intCast(termbox.tb_width()); - const height: usize = @intCast(termbox.tb_height()); + // Required to update tb_width() and tb_height() + const new_dimensions = TerminalBuffer.presentBufferStatic(); + const width = new_dimensions.width; + const height = new_dimensions.height; if (width != buffer.width or height != buffer.height) { // If it did change, then update the cell buffer, reallocate the current animation's buffers, and force a draw update @@ -670,11 +653,11 @@ pub fn main() !void { auth_fails = 0; } - _ = termbox.tb_present(); + _ = TerminalBuffer.presentBufferStatic(); continue; } - - _ = termbox.tb_clear(); + + try TerminalBuffer.clearScreenStatic(false); var length: usize = config.edge_margin; @@ -859,7 +842,7 @@ pub fn main() !void { login.label.draw(); password.draw(); - _ = termbox.tb_present(); + _ = TerminalBuffer.presentBufferStatic(); } var timeout: i32 = -1; @@ -1021,7 +1004,7 @@ pub fn main() !void { try log_writer.print("failed to clear info line: {s}\n", .{@errorName(err)}); }; info_line.label.draw(); - _ = termbox.tb_present(); + _ = TerminalBuffer.presentBufferStatic(); break :authenticate; } @@ -1031,7 +1014,7 @@ pub fn main() !void { try log_writer.print("failed to clear info line: {s}\n", .{@errorName(err)}); }; info_line.label.draw(); - _ = termbox.tb_present(); + _ = TerminalBuffer.presentBufferStatic(); if (config.save) save_last_settings: { // It isn't worth cluttering the code with precise error @@ -1120,12 +1103,7 @@ pub fn main() !void { try log_file.reinit(); } - // Take back control of the TTY - _ = termbox.tb_init(); - - if (config.full_color) { - _ = termbox.tb_set_output_mode(termbox.TB_OUTPUT_TRUECOLOR); - } + try buffer.reclaim(); const auth_err = shared_err.readError(); if (auth_err) |err| { @@ -1148,18 +1126,14 @@ pub fn main() !void { try log_writer.writeAll("logged out\n"); } - try std.posix.tcsetattr(std.posix.STDIN_FILENO, .FLUSH, tb_termios); - if (config.auth_fails == 0 or auth_fails < config.auth_fails) { - _ = termbox.tb_clear(); - try ttyClearScreen(); - + try TerminalBuffer.clearScreenStatic(true); update = true; } // Restore the cursor - _ = termbox.tb_set_cursor(0, 0); - _ = termbox.tb_present(); + TerminalBuffer.setCursorStatic(0, 0); + _ = TerminalBuffer.presentBufferStatic(); }, else => { if (!insert_mode) { @@ -1208,13 +1182,6 @@ fn configErrorHandler(type_name: []const u8, key: []const u8, value: []const u8, }) catch return; } -fn ttyClearScreen() !void { - // Clear the TTY because termbox2 doesn't seem to do it properly - const capability = termbox.global.caps[termbox.TB_CAP_CLEAR_SCREEN]; - const capability_slice = std.mem.span(capability); - _ = try std.posix.write(termbox.global.ttyfd, capability_slice); -} - fn addOtherEnvironment(session: *Session, lang: Lang, display_server: DisplayServer, exec: ?[]const u8) !void { const name = switch (display_server) { .shell => lang.shell, diff --git a/src/tui/TerminalBuffer.zig b/src/tui/TerminalBuffer.zig index dc5c69b..191e58a 100644 --- a/src/tui/TerminalBuffer.zig +++ b/src/tui/TerminalBuffer.zig @@ -7,6 +7,7 @@ pub const termbox = @import("termbox2"); const Random = std.Random; const interop = ly_core.interop; +const LogFile = ly_core.LogFile; const TerminalBuffer = @This(); @@ -17,6 +18,9 @@ pub const InitOptions = struct { margin_box_h: u8, margin_box_v: u8, input_len: u8, + full_color: bool, + labels_max_length: usize, + is_tty: bool, }; pub const Styling = struct { @@ -81,12 +85,36 @@ box_height: usize, margin_box_v: u8, margin_box_h: u8, blank_cell: Cell, +full_color: bool, +termios: ?std.posix.termios, + +pub fn init(options: InitOptions, log_file: *LogFile, random: Random) !TerminalBuffer { + var log_writer = &log_file.file_writer.interface; + + // Initialize termbox + _ = termbox.tb_init(); + + if (options.full_color) { + _ = termbox.tb_set_output_mode(termbox.TB_OUTPUT_TRUECOLOR); + try log_writer.writeAll("termbox2 set to 24-bit color output mode\n"); + } else { + try log_writer.writeAll("termbox2 set to eight-color output mode\n"); + } + + _ = termbox.tb_clear(); + + // Let's take some precautions here and clear the back buffer as well + try clearBackBuffer(); + + const width: usize = @intCast(termbox.tb_width()); + const height: usize = @intCast(termbox.tb_height()); + + try log_writer.print("screen resolution is {d}x{d}\n", .{ width, height }); -pub fn init(options: InitOptions, labels_max_length: usize, random: Random) TerminalBuffer { return .{ .random = random, - .width = @intCast(termbox.tb_width()), - .height = @intCast(termbox.tb_height()), + .width = width, + .height = height, .fg = options.fg, .bg = options.bg, .border_fg = options.border_fg, @@ -109,17 +137,54 @@ pub fn init(options: InitOptions, labels_max_length: usize, random: Random) Term .left = '|', .right = '|', }, - .labels_max_length = labels_max_length, + .labels_max_length = options.labels_max_length, .box_x = 0, .box_y = 0, - .box_width = (2 * options.margin_box_h) + options.input_len + 1 + labels_max_length, + .box_width = (2 * options.margin_box_h) + options.input_len + 1 + options.labels_max_length, .box_height = 7 + (2 * options.margin_box_v), .margin_box_v = options.margin_box_v, .margin_box_h = options.margin_box_h, .blank_cell = Cell.init(' ', options.fg, options.bg), + .full_color = options.full_color, + // Needed to reclaim the TTY after giving up its control + .termios = try std.posix.tcgetattr(std.posix.STDIN_FILENO), }; } +pub fn setCursorStatic(x: usize, y: usize) void { + _ = termbox.tb_set_cursor(@intCast(x), @intCast(y)); +} + +pub fn clearScreenStatic(clear_back_buffer: bool) !void { + _ = termbox.tb_clear(); + if (clear_back_buffer) try clearBackBuffer(); +} + +pub fn shutdownStatic() void { + _ = termbox.tb_shutdown(); +} + +pub fn presentBufferStatic() struct { width: usize, height: usize } { + _ = termbox.tb_present(); + return .{ + .width = @intCast(termbox.tb_width()), + .height = @intCast(termbox.tb_height()), + }; +} + +pub fn reclaim(self: TerminalBuffer) !void { + if (self.termios) |termios| { + // Take back control of the TTY + _ = termbox.tb_init(); + + if (self.full_color) { + _ = termbox.tb_set_output_mode(termbox.TB_OUTPUT_TRUECOLOR); + } + + try std.posix.tcsetattr(std.posix.STDIN_FILENO, .FLUSH, termios); + } +} + pub fn cascade(self: TerminalBuffer) bool { var changed = false; var y = self.height - 2; @@ -260,3 +325,10 @@ pub fn strWidth(str: []const u8) !u8 { return @intCast(i); } + +fn clearBackBuffer() !void { + // Clear the TTY because termbox2 doesn't seem to do it properly + const capability = termbox.global.caps[termbox.TB_CAP_CLEAR_SCREEN]; + const capability_slice = std.mem.span(capability); + _ = try std.posix.write(termbox.global.ttyfd, capability_slice); +} From 2e04ea4d79876b0c3a553f1047d4201331b4e515 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Sat, 31 Jan 2026 11:17:36 +0100 Subject: [PATCH 025/176] Add time, severity & category in logs Signed-off-by: AnErrupTion --- ly-core/src/LogFile.zig | 22 ++++++++- src/animations/DurFile.zig | 26 +++++++---- src/auth.zig | 29 +++++------- src/main.zig | 93 ++++++++++++++++++-------------------- src/tui/TerminalBuffer.zig | 8 ++-- 5 files changed, 95 insertions(+), 83 deletions(-) diff --git a/ly-core/src/LogFile.zig b/ly-core/src/LogFile.zig index 46b353a..5a2ef8f 100644 --- a/ly-core/src/LogFile.zig +++ b/ly-core/src/LogFile.zig @@ -1,4 +1,5 @@ const std = @import("std"); +const interop = @import("interop.zig"); const LogFile = @This(); @@ -19,10 +20,29 @@ pub fn reinit(self: *LogFile) !void { } pub fn deinit(self: *LogFile) void { - self.file_writer.interface.flush() catch {}; self.file.close(); } +pub fn info(self: *LogFile, category: []const u8, comptime message: []const u8, args: anytype) !void { + var buffer: [128:0]u8 = undefined; + const time = interop.timeAsString(&buffer, "%Y-%m-%d %H:%M:%S"); + + try self.file_writer.interface.print("{s} [info/{s}] ", .{ time, category }); + try self.file_writer.interface.print(message, args); + try self.file_writer.interface.writeByte('\n'); + try self.file_writer.interface.flush(); +} + +pub fn err(self: *LogFile, category: []const u8, comptime message: []const u8, args: anytype) !void { + var buffer: [128:0]u8 = undefined; + const time = interop.timeAsString(&buffer, "%Y-%m-%d %H:%M:%S"); + + try self.file_writer.interface.print("{s} [err/{s}] ", .{ time, category }); + try self.file_writer.interface.print(message, args); + try self.file_writer.interface.writeByte('\n'); + try self.file_writer.interface.flush(); +} + fn openLogFile(path: []const u8, log_file: *LogFile) !bool { var could_open_log_file = true; open_log_file: { diff --git a/src/animations/DurFile.zig b/src/animations/DurFile.zig index 9ec9eaf..a76b88d 100644 --- a/src/animations/DurFile.zig +++ b/src/animations/DurFile.zig @@ -1,16 +1,22 @@ const std = @import("std"); +const ly_core = @import("ly-core"); const Animation = @import("../tui/Animation.zig"); const Cell = @import("../tui/Cell.zig"); const TerminalBuffer = @import("../tui/TerminalBuffer.zig"); const enums = @import("../enums.zig"); + const DurOffsetAlignment = enums.DurOffsetAlignment; + const Color = TerminalBuffer.Color; const Styling = TerminalBuffer.Styling; + const Allocator = std.mem.Allocator; const Json = std.json; const eql = std.mem.eql; const flate = std.compress.flate; +const LogFile = ly_core.LogFile; + fn read_decompress_file(allocator: Allocator, file_path: []const u8) ![]u8 { const file_buffer = std.fs.cwd().openFile(file_path, .{}) catch { return error.FileNotFound; @@ -310,8 +316,8 @@ offset_alignment: DurOffsetAlignment, offset: IVec2, // if the user has an even number of columns or rows, we will default to the left or higher position (e.g. 4 columns center = .x..) -fn center(v: u32) i64 { - return @intCast((v / 2) + (v % 2)); +fn center(v: u32) i64 { + return @intCast((v / 2) + (v % 2)); } fn calc_start_position(terminal_buffer: *TerminalBuffer, dur_movie: *DurFormat, offset_alignment: DurOffsetAlignment, offset: IVec2) IVec2 { @@ -333,9 +339,9 @@ fn calc_start_position(terminal_buffer: *TerminalBuffer, dur_movie: *DurFormat, DurOffsetAlignment.centerright => .{ buf_width - movie_width, center(buf_height) - center(movie_height) }, DurOffsetAlignment.bottomleft => .{ 0, buf_height - movie_height }, DurOffsetAlignment.bottomcenter => .{ center(buf_width) - center(movie_width), buf_height - movie_height }, - DurOffsetAlignment.bottomright => .{ buf_width - movie_width, buf_height - movie_height }, + DurOffsetAlignment.bottomright => .{ buf_width - movie_width, buf_height - movie_height }, }; - + return start_pos + offset; } @@ -353,16 +359,16 @@ fn calc_frame_size(terminal_buffer: *TerminalBuffer, dur_movie: *DurFormat) UVec return .{ frame_width, frame_height }; } -pub fn init(allocator: Allocator, terminal_buffer: *TerminalBuffer, log_writer: *std.io.Writer, file_path: []const u8, offset_alignment: DurOffsetAlignment, x_offset: i32, y_offset: i32, full_color: bool) !DurFile { +pub fn init(allocator: Allocator, terminal_buffer: *TerminalBuffer, log_file: *LogFile, file_path: []const u8, offset_alignment: DurOffsetAlignment, x_offset: i32, y_offset: i32, full_color: bool) !DurFile { var dur_movie: DurFormat = .init(allocator); dur_movie.create_from_file(allocator, file_path) catch |err| switch (err) { error.FileNotFound => { - try log_writer.print("error: dur_file was not found at: {s}\n", .{file_path}); + try log_file.err("tui", "dur_file was not found at: {s}", .{file_path}); return err; }, error.NotValidFile => { - try log_writer.print("error: dur_file loaded was invalid or not a dur file!\n", .{}); + try log_file.err("tui", "dur_file loaded was invalid or not a dur file!", .{}); return err; }, else => return err, @@ -370,7 +376,7 @@ pub fn init(allocator: Allocator, terminal_buffer: *TerminalBuffer, log_writer: // 4 bit mode with 256 color is unsupported if (!full_color and eql(u8, dur_movie.colorFormat.?, "256")) { - try log_writer.print("error: dur_file can not be 256 color encoded when not using full_color option!\n", .{}); + try log_file.err("tui", "dur_file can not be 256 color encoded when not using full_color option!", .{}); dur_movie.deinit(); return error.InvalidColorFormat; } @@ -424,7 +430,7 @@ fn draw(self: *DurFile) void { const y_offset_i = @as(i32, @intCast(y)) + self.start_pos[VEC_Y]; // we skip the pass if it falls outside of the draw window (ensure no int underflow) const cell_y: u32 = if (y_offset_i >= 0 and y_offset_i < buf_height) @intCast(y_offset_i) else continue; - + var iter = std.unicode.Utf8View.initUnchecked(current_frame.contents[y]).iterator(); for (0..self.frame_size[VEC_X]) |x| { @@ -434,7 +440,7 @@ fn draw(self: *DurFile) void { _ = iter.nextCodepoint().?; continue; }; - + const codepoint: u21 = iter.nextCodepoint().?; const color_map = current_frame.colorMap[x][y]; diff --git a/src/auth.zig b/src/auth.zig index a70f5da..3722a85 100644 --- a/src/auth.zig +++ b/src/auth.zig @@ -59,33 +59,31 @@ pub fn authenticate(allocator: std.mem.Allocator, log_file: *LogFile, options: A }; var handle: ?*interop.pam.pam_handle = undefined; - var log_writer = &log_file.file_writer.interface; - - try log_writer.writeAll("[pam] starting session\n"); + try log_file.info("auth/pam", "starting session", .{}); var status = interop.pam.pam_start(options.service_name, null, &conv, &handle); if (status != interop.pam.PAM_SUCCESS) return pamDiagnose(status); defer _ = interop.pam.pam_end(handle, status); // Set PAM_TTY as the current TTY. This is required in case it isn't being set by another PAM module - try log_writer.writeAll("[pam] setting tty\n"); + try log_file.info("auth/pam", "setting tty", .{}); status = interop.pam.pam_set_item(handle, interop.pam.PAM_TTY, pam_tty_str.ptr); if (status != interop.pam.PAM_SUCCESS) return pamDiagnose(status); // Do the PAM routine - try log_writer.writeAll("[pam] authenticating\n"); + try log_file.info("auth/pam", "authenticating", .{}); status = interop.pam.pam_authenticate(handle, 0); if (status != interop.pam.PAM_SUCCESS) return pamDiagnose(status); - try log_writer.writeAll("[pam] validating account\n"); + try log_file.info("auth/pam", "validating account", .{}); status = interop.pam.pam_acct_mgmt(handle, 0); if (status != interop.pam.PAM_SUCCESS) return pamDiagnose(status); - try log_writer.writeAll("[pam] setting credentials\n"); + try log_file.info("auth/pam", "setting credentials", .{}); status = interop.pam.pam_setcred(handle, interop.pam.PAM_ESTABLISH_CRED); if (status != interop.pam.PAM_SUCCESS) return pamDiagnose(status); defer status = interop.pam.pam_setcred(handle, interop.pam.PAM_DELETE_CRED); - try log_writer.writeAll("[pam] opening session\n"); + try log_file.info("auth/pam", "opening session", .{}); status = interop.pam.pam_open_session(handle, 0); if (status != interop.pam.PAM_SUCCESS) return pamDiagnose(status); defer status = interop.pam.pam_close_session(handle, 0); @@ -109,9 +107,7 @@ pub fn authenticate(allocator: std.mem.Allocator, log_file: *LogFile, options: A child_pid = try std.posix.fork(); if (child_pid == 0) { try log_file.reinit(); - log_writer = &log_file.file_writer.interface; - - try log_writer.writeAll("starting session\n"); + try log_file.info("auth", "starting session", .{}); startSession(log_file, allocator, options, tty_str, user_entry, handle, current_environment) catch |e| { shared_err.writeError(e); @@ -413,10 +409,9 @@ fn xauth(log_file: *LogFile, allocator: std.mem.Allocator, display_name: []u8, s } fn executeX11Cmd(log_file: *LogFile, allocator: std.mem.Allocator, shell: []const u8, home: []const u8, options: AuthOptions, desktop_cmd: []const u8, vt: []const u8) !void { - var log_writer = &log_file.file_writer.interface; var xauth_buffer: [256]u8 = undefined; - try log_writer.writeAll("[x11] getting free display\n"); + try log_file.info("auth/x11", "getting free display", .{}); const display_num = try getFreeDisplay(); var buf: [4]u8 = undefined; const display_name = try std.fmt.bufPrint(&buf, ":{d}", .{display_num}); @@ -424,10 +419,10 @@ fn executeX11Cmd(log_file: *LogFile, allocator: std.mem.Allocator, shell: []cons const shell_z = try allocator.dupeZ(u8, shell); defer allocator.free(shell_z); - try log_writer.writeAll("[x11] creating xauth file\n"); + try log_file.info("auth/x11", "creating xauth file", .{}); try xauth(log_file, allocator, display_name, shell_z, home, &xauth_buffer, options); - try log_writer.writeAll("[x11] starting x server\n"); + try log_file.info("auth/x11", "starting x server", .{}); const pid = try std.posix.fork(); if (pid == 0) { var cmd_buffer: [1024]u8 = undefined; @@ -450,10 +445,10 @@ fn executeX11Cmd(log_file: *LogFile, allocator: std.mem.Allocator, shell: []cons // X Server detaches from the process. // PID can be fetched from /tmp/X{d}.lock - try log_writer.writeAll("[x11] getting x server pid\n"); + try log_file.info("auth/x11", "getting x server pid", .{}); const x_pid = try getXPid(display_num); - try log_writer.writeAll("[x11] launching environment\n"); + try log_file.info("auth/x11", "launching environment", .{}); xorg_pid = try std.posix.fork(); if (xorg_pid == 0) { var cmd_buffer: [1024]u8 = undefined; diff --git a/src/main.zig b/src/main.zig index 0cc1831..31276e6 100644 --- a/src/main.zig +++ b/src/main.zig @@ -282,8 +282,6 @@ pub fn main() !void { var log_file = try LogFile.init(config.ly_log, &log_file_buffer); defer log_file.deinit(); - var log_writer = &log_file.file_writer.interface; - // These strings only end up getting freed if the user quits Ly using Ctrl+C, which is fine since in the other cases // we end up shutting down or restarting the system shutdown_cmd = try temporary_allocator.dupe(u8, config.shutdown_cmd); @@ -304,7 +302,7 @@ pub fn main() !void { } // Initialize terminal buffer - try log_writer.writeAll("initializing terminal buffer\n"); + try log_file.info("tui", "initializing terminal buffer", .{}); const labels_max_length = @max(lang.login.len, lang.password.len); var seed: u64 = undefined; @@ -326,7 +324,7 @@ pub fn main() !void { }; var buffer = try TerminalBuffer.init(buffer_options, &log_file, random); defer { - log_writer.writeAll("shutting down terminal buffer\n") catch {}; + log_file.info("tui", "shutting down terminal buffer", .{}) catch {}; TerminalBuffer.shutdownStatic(); } @@ -347,23 +345,23 @@ pub fn main() !void { longest.name = diag.arg; try info_line.addMessage(lang.err_args, config.error_bg, config.error_fg); - try log_writer.print("unable to parse argument '{s}{s}': {s}\n", .{ longest.kind.prefix(), longest.name, @errorName(arg_parse_error) }); + try log_file.err("cli", "unable to parse argument '{s}{s}': {s}", .{ longest.kind.prefix(), longest.name, @errorName(arg_parse_error) }); } if (maybe_uid_range_error) |err| { try info_line.addMessage(lang.err_uid_range, config.error_bg, config.error_fg); - try log_writer.print("failed to get uid range: {s}; falling back to default\n", .{@errorName(err)}); + try log_file.err("sys", "failed to get uid range: {s}; falling back to default", .{@errorName(err)}); } if (start_cmd_exit_code != 0) { try info_line.addMessage(lang.err_start, config.error_bg, config.error_fg); - try log_writer.print("failed to execute start command: exit code {d}\n", .{start_cmd_exit_code}); + try log_file.err("sys", "failed to execute start command: exit code {d}", .{start_cmd_exit_code}); } if (maybe_config_load_error) |err| { // We can't localize this since the config failed to load so we'd fallback to the default language anyway try info_line.addMessage("unable to parse config file", config.error_bg, config.error_fg); - try log_writer.print("unable to parse config file: {s}\n", .{@errorName(err)}); + try log_file.err("conf", "unable to parse config file: {s}", .{@errorName(err)}); defer config_errors.deinit(temporary_allocator); @@ -375,21 +373,18 @@ pub fn main() !void { temporary_allocator.free(config_error.value); } - try log_writer.print("failed to convert value '{s}' of option '{s}' to type '{s}': {s}\n", .{ config_error.value, config_error.key, config_error.type_name, config_error.error_name }); - - // Flush immediately so we can free the allocated memory afterwards - try log_writer.flush(); + try log_file.err("conf", "failed to convert value '{s}' of option '{s}' to type '{s}': {s}", .{ config_error.value, config_error.key, config_error.type_name, config_error.error_name }); } } if (!log_file.could_open_log_file) { try info_line.addMessage(lang.err_log, config.error_bg, config.error_fg); - try log_writer.writeAll("failed to open log file\n"); + try log_file.err("sys", "failed to open log file", .{}); } interop.setNumlock(config.numlock) catch |err| { try info_line.addMessage(lang.err_numlock, config.error_bg, config.error_fg); - try log_writer.print("failed to set numlock: {s}\n", .{@errorName(err)}); + try log_file.err("sys", "failed to set numlock: {s}", .{@errorName(err)}); }; var login: UserList = undefined; @@ -402,19 +397,19 @@ pub fn main() !void { addOtherEnvironment(&session, lang, .shell, null) catch |err| { try info_line.addMessage(lang.err_alloc, config.error_bg, config.error_fg); - try log_writer.print("failed to add shell environment: {s}\n", .{@errorName(err)}); + try log_file.err("sys", "failed to add shell environment: {s}", .{@errorName(err)}); }; if (build_options.enable_x11_support) { if (config.xinitrc) |xinitrc_cmd| { addOtherEnvironment(&session, lang, .xinitrc, xinitrc_cmd) catch |err| { try info_line.addMessage(lang.err_alloc, config.error_bg, config.error_fg); - try log_writer.print("failed to add xinitrc environment: {s}\n", .{@errorName(err)}); + try log_file.err("sys", "failed to add xinitrc environment: {s}", .{@errorName(err)}); }; } } else { try info_line.addMessage(lang.no_x11_support, config.bg, config.fg); - try log_writer.writeAll("x11 support disabled at compile-time\n"); + try log_file.err("comp", "x11 support disabled at compile-time"); } var has_crawl_error = false; @@ -424,7 +419,7 @@ pub fn main() !void { while (wayland_session_dirs.next()) |dir| { crawl(&session, lang, dir, .wayland) catch |err| { has_crawl_error = true; - try log_writer.print("failed to crawl wayland session directory '{s}': {s}\n", .{ dir, @errorName(err) }); + try log_file.err("sys", "failed to crawl wayland session directory '{s}': {s}", .{ dir, @errorName(err) }); }; } @@ -433,7 +428,7 @@ pub fn main() !void { while (x_session_dirs.next()) |dir| { crawl(&session, lang, dir, .x11) catch |err| { has_crawl_error = true; - try log_writer.print("failed to crawl x11 session directory '{s}': {s}\n", .{ dir, @errorName(err) }); + try log_file.err("sys", "failed to crawl x11 session directory '{s}': {s}", .{ dir, @errorName(err) }); }; } } @@ -442,7 +437,7 @@ pub fn main() !void { while (custom_session_dirs.next()) |dir| { crawl(&session, lang, dir, .custom) catch |err| { has_crawl_error = true; - try log_writer.print("failed to crawl custom session directory '{s}': {s}\n", .{ dir, @errorName(err) }); + try log_file.err("sys", "failed to crawl custom session directory '{s}': {s}", .{ dir, @errorName(err) }); }; } @@ -456,7 +451,7 @@ pub fn main() !void { // accounts *and* no root account...but at this point, if that's the // case, you have bigger problems to deal with in the first place. :D try info_line.addMessage(lang.err_no_users, config.error_bg, config.error_fg); - try log_writer.writeAll("no users found\n"); + try log_file.err("sys", "no users found", .{}); } var password = Text.init(allocator, &buffer, true, config.asterisk); @@ -472,16 +467,16 @@ pub fn main() !void { if (!isValidUsername(auto_user, usernames)) { try info_line.addMessage(lang.err_pam_user_unknown, config.error_bg, config.error_fg); - try log_writer.print("autologin failed: username '{s}' not found\n", .{auto_user}); + try log_file.err("auth", "autologin failed: username '{s}' not found", .{auto_user}); break :check_autologin; } const session_index = findSessionByName(&session, auto_session) orelse { - try log_writer.print("autologin failed: session '{s}' not found\n", .{auto_session}); + try log_file.err("auth", "autologin failed: session '{s}' not found", .{auto_session}); try info_line.addMessage(lang.err_autologin_session, config.error_bg, config.error_fg); break :check_autologin; }; - try log_writer.print("attempting autologin for user '{s}' with session '{s}'\n", .{ auto_user, auto_session }); + try log_file.err("auth", "attempting autologin for user '{s}' with session '{s}'", .{ auto_user, auto_session }); session.label.current = session_index; for (login.label.list.items, 0..) |username, i| { @@ -533,7 +528,7 @@ pub fn main() !void { .login => login.label.handle(null, insert_mode), .password => password.handle(null, insert_mode) catch |err| { try info_line.addMessage(lang.err_alloc, config.error_bg, config.error_fg); - try log_writer.print("failed to handle password input: {s}\n", .{@errorName(err)}); + try log_file.err("tui", "failed to handle password input: {s}", .{@errorName(err)}); }, } } @@ -563,7 +558,7 @@ pub fn main() !void { animation = game_of_life.animation(); }, .dur_file => { - var dur = try DurFile.init(allocator, &buffer, log_writer, config.dur_file_path, config.dur_offset_alignment, config.dur_x_offset, config.dur_y_offset, config.full_color); + var dur = try DurFile.init(allocator, &buffer, &log_file, config.dur_file_path, config.dur_offset_alignment, config.dur_x_offset, config.dur_y_offset, config.full_color); animation = dur.animation(); }, } @@ -594,12 +589,12 @@ pub fn main() !void { // Switch to selected TTY const active_tty = interop.getActiveTty(allocator) catch |err| no_tty_found: { try info_line.addMessage(lang.err_get_active_tty, config.error_bg, config.error_fg); - try log_writer.print("failed to get active tty: {s}\n", .{@errorName(err)}); + try log_file.err("sys", "failed to get active tty: {s}", .{@errorName(err)}); break :no_tty_found build_options.fallback_tty; }; interop.switchTty(active_tty) catch |err| { try info_line.addMessage(lang.err_switch_tty, config.error_bg, config.error_fg); - try log_writer.print("failed to switch tty: {s}\n", .{@errorName(err)}); + try log_file.err("sys", "failed to switch tty: {s}", .{@errorName(err)}); }; if (config.initial_info_text) |text| { @@ -609,7 +604,7 @@ pub fn main() !void { var name_buf: [std.posix.HOST_NAME_MAX]u8 = undefined; const hostname = std.posix.gethostname(&name_buf) catch |err| { try info_line.addMessage(lang.err_hostname, config.error_bg, config.error_fg); - try log_writer.print("failed to get hostname: {s}\n", .{@errorName(err)}); + try log_file.err("sys", "failed to get hostname: {s}", .{@errorName(err)}); break :get_host_name; }; try info_line.addMessage(hostname, config.bg, config.fg); @@ -627,14 +622,14 @@ pub fn main() !void { if (width != buffer.width or height != buffer.height) { // If it did change, then update the cell buffer, reallocate the current animation's buffers, and force a draw update - try log_writer.print("screen resolution updated to {d}x{d}\n", .{ width, height }); + try log_file.info("tui", "screen resolution updated to {d}x{d}", .{ width, height }); buffer.width = width; buffer.height = height; animation.realloc() catch |err| { try info_line.addMessage(lang.err_alloc, config.error_bg, config.error_fg); - try log_writer.print("failed to reallocate animation buffers: {s}\n", .{@errorName(err)}); + try log_file.err("tui", "failed to reallocate animation buffers: {s}", .{@errorName(err)}); }; update = true; @@ -656,7 +651,7 @@ pub fn main() !void { _ = TerminalBuffer.presentBufferStatic(); continue; } - + try TerminalBuffer.clearScreenStatic(false); var length: usize = config.edge_margin; @@ -671,7 +666,7 @@ pub fn main() !void { if (!can_draw_battery) break :draw_battery; const battery_percentage = getBatteryPercentage(id) catch |err| { - try log_writer.print("failed to get battery percentage: {s}\n", .{@errorName(err)}); + try log_file.err("sys", "failed to get battery percentage: {s}", .{@errorName(err)}); try info_line.addMessage(lang.err_battery, config.error_bg, config.error_fg); can_draw_battery = false; break :draw_battery; @@ -728,7 +723,7 @@ pub fn main() !void { .login => login.label.handle(null, insert_mode), .password => password.handle(null, insert_mode) catch |err| { try info_line.addMessage(lang.err_alloc, config.error_bg, config.error_fg); - try log_writer.print("failed to handle password input: {s}\n", .{@errorName(err)}); + try log_file.err("tui", "failed to handle password input: {s}", .{@errorName(err)}); }, } @@ -741,7 +736,7 @@ pub fn main() !void { if (clock_str.len == 0) { try info_line.addMessage(lang.err_clock_too_long, config.error_bg, config.error_fg); can_draw_clock = false; - try log_writer.writeAll("clock string too long\n"); + try log_file.err("tui", "clock string too long", .{}); break :draw_clock; } @@ -821,7 +816,7 @@ pub fn main() !void { const lock_state = interop.getLockState() catch |err| { try info_line.addMessage(lang.err_lock_state, config.error_bg, config.error_fg); can_get_lock_state = false; - try log_writer.print("failed to get lock state: {s}\n", .{@errorName(err)}); + try log_file.err("sys", "failed to get lock state: {s}", .{@errorName(err)}); break :draw_lock_state; }; @@ -882,7 +877,7 @@ pub fn main() !void { }; if (process_result.Exited != 0) { try info_line.addMessage(lang.err_inactivity, config.error_bg, config.error_fg); - try log_writer.print("failed to execute inactivity command: exit code {d}\n", .{process_result.Exited}); + try log_file.err("sys", "failed to execute inactivity command: exit code {d}", .{process_result.Exited}); } } @@ -940,7 +935,7 @@ pub fn main() !void { }; if (process_result.Exited != 0) { try info_line.addMessage(lang.err_sleep, config.error_bg, config.error_fg); - try log_writer.print("failed to execute sleep command: exit code {d}\n", .{process_result.Exited}); + try log_file.err("sys", "failed to execute sleep command: exit code {d}", .{process_result.Exited}); } } } @@ -956,19 +951,19 @@ pub fn main() !void { }; if (process_result.Exited != 0) { try info_line.addMessage(lang.err_hibernate, config.error_bg, config.error_fg); - try log_writer.print("failed to execute hibernate command: exit code {d}\n", .{process_result.Exited}); + 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_writer.print("failed to change brightness: {s}\n", .{@errorName(err)}); + 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_writer.print("failed to change brightness: {s}\n", .{@errorName(err)}); + try log_file.err("sys", "failed to change brightness: {s}", .{@errorName(err)}); }; } }, @@ -994,14 +989,14 @@ pub fn main() !void { update = true; }, termbox.TB_KEY_ENTER => authenticate: { - try log_writer.writeAll("authenticating...\n"); + try log_file.info("auth", "authenticating...", .{}); 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); InfoLine.clearRendered(allocator, buffer) catch |err| { try info_line.addMessage(lang.err_alloc, config.error_bg, config.error_fg); - try log_writer.print("failed to clear info line: {s}\n", .{@errorName(err)}); + try log_file.err("tui", "failed to clear info line: {s}", .{@errorName(err)}); }; info_line.label.draw(); _ = TerminalBuffer.presentBufferStatic(); @@ -1011,7 +1006,7 @@ pub fn main() !void { try info_line.addMessage(lang.authenticating, config.bg, config.fg); InfoLine.clearRendered(allocator, buffer) catch |err| { try info_line.addMessage(lang.err_alloc, config.error_bg, config.error_fg); - try log_writer.print("failed to clear info line: {s}\n", .{@errorName(err)}); + try log_file.err("tui", "failed to clear info line: {s}", .{@errorName(err)}); }; info_line.label.draw(); _ = TerminalBuffer.presentBufferStatic(); @@ -1020,10 +1015,10 @@ pub fn main() !void { // 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_writer.writeAll("failed to save current user data\n") catch {}; + errdefer log_file.err("conf", "failed to save current user data", .{}) catch {}; var file = std.fs.cwd().createFile(save_path, .{}) catch |err| { - log_writer.print("failed to create save file: {s}\n", .{@errorName(err)}) catch break :save_last_settings; + log_file.err("sys", "failed to create save file: {s}", .{@errorName(err)}) catch break :save_last_settings; break :save_last_settings; }; defer file.close(); @@ -1111,7 +1106,7 @@ pub fn main() !void { active_input = .password; try info_line.addMessage(getAuthErrorMsg(err, lang), config.error_bg, config.error_fg); - try log_writer.print("failed to authenticate: {s}\n", .{@errorName(err)}); + try log_file.err("auth", "failed to authenticate: {s}", .{@errorName(err)}); if (config.clear_password or err != error.PamAuthError) password.clear(); } else { @@ -1123,7 +1118,7 @@ pub fn main() !void { password.clear(); is_autologin = false; try info_line.addMessage(lang.logout, config.bg, config.fg); - try log_writer.writeAll("logged out\n"); + try log_file.info("auth", "logged out", .{}); } if (config.auth_fails == 0 or auth_fails < config.auth_fails) { @@ -1168,8 +1163,6 @@ pub fn main() !void { update = true; }, } - - try log_writer.flush(); } } diff --git a/src/tui/TerminalBuffer.zig b/src/tui/TerminalBuffer.zig index 191e58a..61d6e74 100644 --- a/src/tui/TerminalBuffer.zig +++ b/src/tui/TerminalBuffer.zig @@ -89,16 +89,14 @@ full_color: bool, termios: ?std.posix.termios, pub fn init(options: InitOptions, log_file: *LogFile, random: Random) !TerminalBuffer { - var log_writer = &log_file.file_writer.interface; - // Initialize termbox _ = termbox.tb_init(); if (options.full_color) { _ = termbox.tb_set_output_mode(termbox.TB_OUTPUT_TRUECOLOR); - try log_writer.writeAll("termbox2 set to 24-bit color output mode\n"); + try log_file.info("tui", "termbox2 set to 24-bit color output mode", .{}); } else { - try log_writer.writeAll("termbox2 set to eight-color output mode\n"); + try log_file.info("tui", "termbox2 set to eight-color output mode", .{}); } _ = termbox.tb_clear(); @@ -109,7 +107,7 @@ pub fn init(options: InitOptions, log_file: *LogFile, random: Random) !TerminalB const width: usize = @intCast(termbox.tb_width()); const height: usize = @intCast(termbox.tb_height()); - try log_writer.print("screen resolution is {d}x{d}\n", .{ width, height }); + try log_file.info("tui", "screen resolution is {d}x{d}", .{ width, height }); return .{ .random = random, From b672d04dc6097505cfc23c531c5e868517c260f7 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Sat, 31 Jan 2026 11:46:55 +0100 Subject: [PATCH 026/176] Improve authentication logging Signed-off-by: AnErrupTion --- src/auth.zig | 44 +++++++++++++++++++++++++++++++++++++------- src/main.zig | 2 +- 2 files changed, 38 insertions(+), 8 deletions(-) diff --git a/src/auth.zig b/src/auth.zig index 3722a85..9e90021 100644 --- a/src/auth.zig +++ b/src/auth.zig @@ -42,9 +42,11 @@ pub fn authenticate(allocator: std.mem.Allocator, log_file: *LogFile, options: A const pam_tty_str = try std.fmt.bufPrintZ(&pam_tty_buffer, "tty{d}", .{options.tty}); // Set the XDG environment variables + try log_file.info("auth/env", "setting xdg environment variables", .{}); try setXdgEnv(allocator, tty_str, current_environment); // Open the PAM session + try log_file.info("auth/pam", "encoding credentials", .{}); const login_z = try allocator.dupeZ(u8, login); defer allocator.free(login_z); @@ -88,6 +90,7 @@ pub fn authenticate(allocator: std.mem.Allocator, log_file: *LogFile, options: A if (status != interop.pam.PAM_SUCCESS) return pamDiagnose(status); defer status = interop.pam.pam_close_session(handle, 0); + try log_file.info("auth/passwd", "getting struct", .{}); var user_entry: interop.UsernameEntry = undefined; { defer interop.closePasswordDatabase(); @@ -97,6 +100,7 @@ pub fn authenticate(allocator: std.mem.Allocator, log_file: *LogFile, options: A } // Set user shell if it hasn't already been set + try log_file.info("auth/passwd", "setting user shell", .{}); if (user_entry.shell == null) interop.setUserShell(&user_entry); var shared_err = try SharedError.init(); @@ -107,7 +111,7 @@ pub fn authenticate(allocator: std.mem.Allocator, log_file: *LogFile, options: A child_pid = try std.posix.fork(); if (child_pid == 0) { try log_file.reinit(); - try log_file.info("auth", "starting session", .{}); + try log_file.info("auth/sys", "starting session", .{}); startSession(log_file, allocator, options, tty_str, user_entry, handle, current_environment) catch |e| { shared_err.writeError(e); @@ -144,6 +148,7 @@ pub fn authenticate(allocator: std.mem.Allocator, log_file: *LogFile, options: A try log_file.reinit(); + try log_file.info("auth/utmp", "removing utmp entry", .{}); removeUtmpEntry(&entry); if (shared_err.readError()) |err| return err; @@ -159,12 +164,15 @@ fn startSession( current_environment: Environment, ) !void { // Set the user's GID & PID + try log_file.info("auth/passwd", "setting user context", .{}); try interop.setUserContext(allocator, user_entry); // Set up the environment + try log_file.info("auth/env", "setting environment variables", .{}); try initEnv(allocator, user_entry, options.path); // Reset the XDG environment variables + try log_file.info("auth/env", "resetting xdg environment variables", .{}); try setXdgEnv(allocator, tty_str, current_environment); try setXdgRuntimeDir(allocator); @@ -173,12 +181,18 @@ fn startSession( if (pam_env_vars == null) return error.GetEnvListFailed; const env_list = std.mem.span(pam_env_vars.?); - for (env_list) |env_var| try interop.putEnvironmentVariable(env_var); + for (env_list) |env_var| { + if (env_var == null) continue; + try log_file.info("auth/env", "setting pam environment variable: {s}", .{std.mem.span(env_var.?)}); + try interop.putEnvironmentVariable(env_var); + } // Change to the user's home directory + try log_file.info("auth/sys", "changing cwd to user home", .{}); std.posix.chdir(user_entry.home.?) catch return error.ChangeDirectoryFailed; // Signal to the session process to give up control on the TTY + try log_file.info("auth/sys", "releasing tty", .{}); std.posix.kill(options.session_pid, std.posix.SIG.CHLD) catch return error.TtyControlTransferFailed; // Execute what the user requested @@ -187,6 +201,8 @@ fn startSession( .xinitrc, .x11 => if (build_options.enable_x11_support) { var vt_buf: [5]u8 = undefined; const vt = try std.fmt.bufPrint(&vt_buf, "vt{d}", .{options.x_vt orelse options.tty}); + + try log_file.info("auth/x11", "setting vt to {s}", .{vt}); try executeX11Cmd(log_file, allocator, user_entry.shell.?, user_entry.home.?, options, current_environment.cmd orelse "", vt); }, } @@ -322,7 +338,7 @@ fn getXPid(display_num: u8) !i32 { return std.fmt.parseInt(i32, std.mem.trim(u8, buffer[0..written], " "), 10); } -fn createXauthFile(pwd: []const u8, buffer: []u8) ![]const u8 { +fn createXauthFile(log_file: *LogFile, pwd: []const u8, buffer: []u8) ![]const u8 { var xauth_buf: [100]u8 = undefined; var xauth_dir: []const u8 = undefined; const xdg_rt_dir = std.posix.getenv("XDG_RUNTIME_DIR"); @@ -368,6 +384,8 @@ fn createXauthFile(pwd: []const u8, buffer: []u8) ![]const u8 { std.fs.cwd().makePath(trimmed_xauth_dir) catch {}; + try log_file.info("auth/x11", "creating xauth file: {s}", .{xauthority}); + const file = try std.fs.createFileAbsolute(xauthority, .{}); file.close(); @@ -385,7 +403,7 @@ fn mcookie() [Md5.digest_length * 2]u8 { } fn xauth(log_file: *LogFile, allocator: std.mem.Allocator, display_name: []u8, shell: [*:0]const u8, home: []const u8, xauth_buffer: []u8, options: AuthOptions) !void { - const xauthority = try createXauthFile(home, xauth_buffer); + const xauthority = try createXauthFile(log_file, home, xauth_buffer); try interop.setEnvironmentVariable(allocator, "XAUTHORITY", xauthority, true); try interop.setEnvironmentVariable(allocator, "DISPLAY", display_name, true); @@ -396,6 +414,7 @@ fn xauth(log_file: *LogFile, allocator: std.mem.Allocator, display_name: []u8, s var cmd_buffer: [1024]u8 = undefined; const cmd_str = std.fmt.bufPrintZ(&cmd_buffer, "{s} add {s} . {s}", .{ options.xauth_cmd, display_name, magic_cookie }) catch std.process.exit(1); + try log_file.info("auth/x11", "executing: {s} -c {s}", .{ shell, cmd_str }); const args = [_:null]?[*:0]const u8{ shell, "-c", cmd_str }; std.posix.execveZ(shell, &args, std.c.environ) catch {}; std.process.exit(1); @@ -415,6 +434,7 @@ fn executeX11Cmd(log_file: *LogFile, allocator: std.mem.Allocator, shell: []cons const display_num = try getFreeDisplay(); var buf: [4]u8 = undefined; const display_name = try std.fmt.bufPrint(&buf, ":{d}", .{display_num}); + try log_file.info("auth/x11", "got free display: {d}", .{display_num}); const shell_z = try allocator.dupeZ(u8, shell); defer allocator.free(shell_z); @@ -427,12 +447,14 @@ fn executeX11Cmd(log_file: *LogFile, allocator: std.mem.Allocator, shell: []cons if (pid == 0) { var cmd_buffer: [1024]u8 = undefined; const cmd_str = std.fmt.bufPrintZ(&cmd_buffer, "{s} {s} {s}", .{ options.x_cmd, display_name, vt }) catch std.process.exit(1); + try log_file.info("auth/x11", "executing: {s} -c {s}", .{ shell, cmd_str }); const args = [_:null]?[*:0]const u8{ shell_z, "-c", cmd_str }; std.posix.execveZ(shell_z, &args, std.c.environ) catch {}; std.process.exit(1); } + try log_file.info("auth/x11", "waiting for xcb connection", .{}); var ok: c_int = -1; var xcb: ?*interop.xcb.xcb_connection_t = null; while (ok != 0) { @@ -447,12 +469,14 @@ fn executeX11Cmd(log_file: *LogFile, allocator: std.mem.Allocator, shell: []cons // PID can be fetched from /tmp/X{d}.lock try log_file.info("auth/x11", "getting x server pid", .{}); const x_pid = try getXPid(display_num); + try log_file.info("auth/x11", "got x server pid: {d}", .{x_pid}); try log_file.info("auth/x11", "launching environment", .{}); xorg_pid = try std.posix.fork(); if (xorg_pid == 0) { var cmd_buffer: [1024]u8 = undefined; const cmd_str = std.fmt.bufPrintZ(&cmd_buffer, "{s} {s} {s}", .{ options.setup_cmd, options.login_cmd orelse "", desktop_cmd }) catch std.process.exit(1); + try log_file.info("auth/x11", "executing: {s} -c {s}", .{ shell, cmd_str }); const args = [_:null]?[*:0]const u8{ shell_z, "-c", cmd_str }; std.posix.execveZ(shell_z, &args, std.c.environ) catch {}; @@ -468,6 +492,8 @@ fn executeX11Cmd(log_file: *LogFile, allocator: std.mem.Allocator, shell: []cons std.posix.sigaction(std.posix.SIG.TERM, &act, null); _ = std.posix.waitpid(xorg_pid, 0); + + try log_file.info("auth/x11", "disconnecting xcb", .{}); interop.xcb.xcb_disconnect(xcb); // TODO: Find a more robust way to ensure that X has been terminated (pidfds?) @@ -479,12 +505,15 @@ fn executeX11Cmd(log_file: *LogFile, allocator: std.mem.Allocator, shell: []cons } fn executeCmd(global_log_file: *LogFile, allocator: std.mem.Allocator, shell: []const u8, options: AuthOptions, is_terminal: bool, exec_cmd: ?[]const u8) !void { + try global_log_file.info("auth/sys", "launching wayland/shell/custom session", .{}); + var maybe_log_file: ?std.fs.File = null; if (!is_terminal) { // For custom desktop entries, the "Terminal" value here determines if // we redirect standard output & error or not. That is, we redirect only // if it's equal to false (so if it's not running in a TTY). if (options.session_log) |log_path| { + try global_log_file.info("auth/sys", "setting up stdio & stderr redirection", .{}); maybe_log_file = try redirectStandardStreams(global_log_file, log_path, true); } } @@ -496,6 +525,7 @@ fn executeCmd(global_log_file: *LogFile, allocator: std.mem.Allocator, shell: [] var cmd_buffer: [1024]u8 = undefined; const cmd_str = try std.fmt.bufPrintZ(&cmd_buffer, "{s} {s} {s}", .{ options.setup_cmd, options.login_cmd orelse "", exec_cmd orelse shell }); + try global_log_file.info("auth/sys", "executing: {s} -c {s}", .{ shell, cmd_str }); const args = [_:null]?[*:0]const u8{ shell_z, "-c", cmd_str }; return std.posix.execveZ(shell_z, &args, std.c.environ); } @@ -504,16 +534,16 @@ fn redirectStandardStreams(global_log_file: *LogFile, session_log: []const u8, c create_session_log_dir: { const session_log_dir = std.fs.path.dirname(session_log) orelse break :create_session_log_dir; std.fs.cwd().makePath(session_log_dir) catch |err| { - try global_log_file.file_writer.interface.print("failed to create session log file directory: {s}\n", .{@errorName(err)}); + try global_log_file.err("auth/sys", "failed to create session log file directory: {s}", .{@errorName(err)}); return err; }; } const log_file = if (create) (std.fs.cwd().createFile(session_log, .{ .mode = 0o666 }) catch |err| { - try global_log_file.file_writer.interface.print("failed to create new session log file: {s}\n", .{@errorName(err)}); + try global_log_file.err("auth/sys", "failed to create new session log file: {s}", .{@errorName(err)}); return err; }) else (std.fs.cwd().openFile(session_log, .{ .mode = .read_write }) catch |err| { - try global_log_file.file_writer.interface.print("failed to open existing session log file: {s}\n", .{@errorName(err)}); + try global_log_file.err("auth/sys", "failed to open existing session log file: {s}", .{@errorName(err)}); return err; }); diff --git a/src/main.zig b/src/main.zig index 31276e6..200597e 100644 --- a/src/main.zig +++ b/src/main.zig @@ -989,7 +989,7 @@ pub fn main() !void { update = true; }, termbox.TB_KEY_ENTER => authenticate: { - try log_file.info("auth", "authenticating...", .{}); + 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 From b0dcc127854e2a75f4a2a1b2be2b070b596a8e40 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Sat, 31 Jan 2026 22:35:28 +0100 Subject: [PATCH 027/176] Move UI drawing to separate function Signed-off-by: AnErrupTion --- src/main.zig | 569 +++++++++++++++++++++++++++------------------------ 1 file changed, 304 insertions(+), 265 deletions(-) diff --git a/src/main.zig b/src/main.zig index 200597e..7e17bb8 100644 --- a/src/main.zig +++ b/src/main.zig @@ -65,6 +65,31 @@ const ConfigError = struct { }; var config_errors: std.ArrayList(ConfigError) = .empty; +const UiState = struct { + auth_fails: u64, + update: bool, + buffer: *TerminalBuffer, + animation_timed_out: bool, + animation: *Animation, + can_draw_battery: bool, + info_line: *InfoLine, + animate: bool, + resolution_changed: bool, + session: *Session, + login: *UserList, + password: *Text, + active_input: enums.Input, + insert_mode: bool, + can_draw_clock: bool, + shutdown_len: u8, + restart_len: u8, + sleep_len: u8, + hibernate_len: u8, + brightness_down_len: u8, + brightness_up_len: u8, + can_get_lock_state: bool, +}; + pub fn main() !void { var shutdown = false; var restart = false; @@ -99,11 +124,10 @@ pub fn main() !void { var gpa = std.heap.DebugAllocator(.{}).init; defer _ = gpa.deinit(); + const allocator = gpa.allocator(); + // Allows stopping an animation after some time const animation_time_start = try interop.getTimeOfDay(); - var animation_timed_out: bool = false; - - const allocator = gpa.allocator(); // Load arguments const params = comptime clap.parseParamsComptime( @@ -127,9 +151,6 @@ pub fn main() !void { var old_save_file_exists = false; var maybe_config_load_error: ?anyerror = null; var start_cmd_exit_code: u8 = 0; - var can_get_lock_state = true; - var can_draw_clock = true; - var can_draw_battery = true; var saved_users = SavedUsers.init(); defer saved_users.deinit(allocator); @@ -457,8 +478,6 @@ pub fn main() !void { var password = Text.init(allocator, &buffer, true, config.asterisk); defer password.deinit(); - var active_input = config.default_input; - var insert_mode = !config.vi_mode or config.vi_default_mode == .insert; var is_autologin = false; check_autologin: { @@ -488,6 +507,32 @@ pub fn main() !void { is_autologin = true; } + var animation: Animation = undefined; + var state = UiState{ + .auth_fails = 0, + .update = true, + .buffer = &buffer, + .animation_timed_out = false, + .animation = &animation, + .can_draw_battery = true, + .info_line = &info_line, + .animate = config.animation != .none, + .resolution_changed = false, + .session = &session, + .login = &login, + .password = &password, + .active_input = config.default_input, + .insert_mode = !config.vi_mode or config.vi_default_mode == .insert, + .can_draw_clock = true, + .shutdown_len = try TerminalBuffer.strWidth(lang.shutdown), + .restart_len = try TerminalBuffer.strWidth(lang.restart), + .sleep_len = try TerminalBuffer.strWidth(lang.sleep), + .hibernate_len = try TerminalBuffer.strWidth(lang.hibernate), + .brightness_down_len = try TerminalBuffer.strWidth(lang.brightness_down), + .brightness_up_len = try TerminalBuffer.strWidth(lang.brightness_up), + .can_get_lock_state = true, + }; + // Load last saved username and desktop selection, if any // Skip if autologin is active to prevent overriding autologin session if (config.save and !is_autologin) { @@ -506,7 +551,7 @@ pub fn main() !void { } } - active_input = .password; + state.active_input = .password; session.label.current = @min(user.session_index, session.label.list.items.len - 1); } @@ -522,11 +567,11 @@ pub fn main() !void { login.label.position(coordinates.x, coordinates.y + 4, coordinates.visible_length, config.text_in_center); password.position(coordinates.x, coordinates.y + 6, coordinates.visible_length); - switch (active_input) { - .info_line => info_line.label.handle(null, insert_mode), - .session => session.label.handle(null, insert_mode), - .login => login.label.handle(null, insert_mode), - .password => password.handle(null, insert_mode) catch |err| { + switch (state.active_input) { + .info_line => info_line.label.handle(null, state.insert_mode), + .session => session.label.handle(null, state.insert_mode), + .login => login.label.handle(null, state.insert_mode), + .password => password.handle(null, state.insert_mode) catch |err| { try info_line.addMessage(lang.err_alloc, config.error_bg, config.error_fg); try log_file.err("tui", "failed to handle password input: {s}", .{@errorName(err)}); }, @@ -534,8 +579,6 @@ pub fn main() !void { } // Initialize the animation, if any - var animation: Animation = undefined; - switch (config.animation) { .none => { var dummy = Dummy{}; @@ -564,25 +607,15 @@ pub fn main() !void { } defer animation.deinit(); - const animate = config.animation != .none; const shutdown_key = try std.fmt.parseInt(u8, config.shutdown_key[1..], 10); - const shutdown_len = try TerminalBuffer.strWidth(lang.shutdown); const restart_key = try std.fmt.parseInt(u8, config.restart_key[1..], 10); - const restart_len = try TerminalBuffer.strWidth(lang.restart); const sleep_key = try std.fmt.parseInt(u8, config.sleep_key[1..], 10); - const sleep_len = try TerminalBuffer.strWidth(lang.sleep); const hibernate_key = try std.fmt.parseInt(u8, config.hibernate_key[1..], 10); - const hibernate_len = try TerminalBuffer.strWidth(lang.hibernate); const brightness_down_key = if (config.brightness_down_key) |key| try std.fmt.parseInt(u8, key[1..], 10) else null; - const brightness_down_len = try TerminalBuffer.strWidth(lang.brightness_down); const brightness_up_key = if (config.brightness_up_key) |key| try std.fmt.parseInt(u8, key[1..], 10) else null; - const brightness_up_len = try TerminalBuffer.strWidth(lang.brightness_up); var event: termbox.tb_event = undefined; var run = true; - var update = true; - var resolution_changed = false; - var auth_fails: u64 = 0; var inactivity_time_start = try interop.getTimeOfDay(); var inactivity_cmd_ran = false; @@ -612,8 +645,8 @@ pub fn main() !void { while (run) { // If there's no input or there's an animation, a resolution change needs to be checked - if (!update or animate) { - if (!update) std.Thread.sleep(std.time.ns_per_ms * 100); + if (!state.update or state.animate) { + if (!state.update) std.Thread.sleep(std.time.ns_per_ms * 100); // Required to update tb_width() and tb_height() const new_dimensions = TerminalBuffer.presentBufferStatic(); @@ -632,232 +665,33 @@ pub fn main() !void { try log_file.err("tui", "failed to reallocate animation buffers: {s}", .{@errorName(err)}); }; - update = true; - resolution_changed = true; + state.update = true; + state.resolution_changed = true; } } - if (update) { - // If the user entered a wrong password 10 times in a row, play a cascade animation, else update normally - if (config.auth_fails > 0 and auth_fails >= config.auth_fails) { - std.Thread.sleep(std.time.ns_per_ms * 10); - update = buffer.cascade(); - - if (!update) { - std.Thread.sleep(std.time.ns_per_s * 7); - auth_fails = 0; - } - - _ = TerminalBuffer.presentBufferStatic(); - continue; - } - - try TerminalBuffer.clearScreenStatic(false); - - var length: usize = config.edge_margin; - - if (!animation_timed_out) animation.draw(); - - if (!config.hide_version_string) { - buffer.drawLabel(ly_version_str, config.edge_margin, buffer.height - 1 - config.edge_margin); - } - - if (config.battery_id) |id| draw_battery: { - if (!can_draw_battery) break :draw_battery; - - const battery_percentage = getBatteryPercentage(id) catch |err| { - try log_file.err("sys", "failed to get battery percentage: {s}", .{@errorName(err)}); - try info_line.addMessage(lang.err_battery, config.error_bg, config.error_fg); - can_draw_battery = false; - break :draw_battery; - }; - - var battery_buf: [16:0]u8 = undefined; - const battery_str = std.fmt.bufPrintZ(&battery_buf, "BAT: {d}%", .{battery_percentage}) catch break :draw_battery; - - var battery_y: usize = config.edge_margin; - if (!config.hide_key_hints) { - battery_y += 1; - } - buffer.drawLabel(battery_str, config.edge_margin, battery_y); - can_draw_battery = true; - } - - if (config.bigclock != .none and buffer.box_height + (bigclock.HEIGHT + 2) * 2 < buffer.height) { - var format_buf: [16:0]u8 = undefined; - var clock_buf: [32:0]u8 = undefined; - // We need the slice/c-string returned by `bufPrintZ`. - const format = try std.fmt.bufPrintZ(&format_buf, "{s}{s}{s}{s}", .{ - if (config.bigclock_12hr) "%I" else "%H", - ":%M", - if (config.bigclock_seconds) ":%S" else "", - if (config.bigclock_12hr) "%P" else "", - }); - const xo = buffer.width / 2 - @min(buffer.width, (format.len * (bigclock.WIDTH + 1))) / 2; - const yo = (buffer.height - buffer.box_height) / 2 - bigclock.HEIGHT - 2; - - const clock_str = interop.timeAsString(&clock_buf, format); - - for (clock_str, 0..) |c, i| { - // TODO: Show error - const clock_cell = try bigclock.clockCell(animate, c, buffer.fg, buffer.bg, config.bigclock); - bigclock.alphaBlit(xo + i * (bigclock.WIDTH + 1), yo, buffer.width, buffer.height, clock_cell); - } - } - - buffer.drawBoxCenter(!config.hide_borders, config.blank_box); - - if (resolution_changed) { - const coordinates = buffer.calculateComponentCoordinates(); - info_line.label.position(coordinates.start_x, coordinates.y, coordinates.full_visible_length, null); - session.label.position(coordinates.x, coordinates.y + 2, coordinates.visible_length, config.text_in_center); - login.label.position(coordinates.x, coordinates.y + 4, coordinates.visible_length, config.text_in_center); - password.position(coordinates.x, coordinates.y + 6, coordinates.visible_length); - - resolution_changed = false; - } - - switch (active_input) { - .info_line => info_line.label.handle(null, insert_mode), - .session => session.label.handle(null, insert_mode), - .login => login.label.handle(null, insert_mode), - .password => password.handle(null, insert_mode) catch |err| { - try info_line.addMessage(lang.err_alloc, config.error_bg, config.error_fg); - try log_file.err("tui", "failed to handle password input: {s}", .{@errorName(err)}); - }, - } - - if (config.clock) |clock| draw_clock: { - if (!can_draw_clock) break :draw_clock; - - var clock_buf: [64:0]u8 = undefined; - const clock_str = interop.timeAsString(&clock_buf, clock); - - if (clock_str.len == 0) { - try info_line.addMessage(lang.err_clock_too_long, config.error_bg, config.error_fg); - can_draw_clock = false; - try log_file.err("tui", "clock string too long", .{}); - break :draw_clock; - } - - buffer.drawLabel(clock_str, buffer.width - @min(buffer.width, clock_str.len) - config.edge_margin, config.edge_margin); - } - - const label_x = buffer.box_x + buffer.margin_box_h; - const label_y = buffer.box_y + buffer.margin_box_v; - - buffer.drawLabel(lang.login, label_x, label_y + 4); - buffer.drawLabel(lang.password, label_x, label_y + 6); - - info_line.label.draw(); - - if (!config.hide_key_hints) { - buffer.drawLabel(config.shutdown_key, length, config.edge_margin); - length += config.shutdown_key.len + 1; - buffer.drawLabel(" ", length - 1, config.edge_margin); - - buffer.drawLabel(lang.shutdown, length, config.edge_margin); - length += shutdown_len + 1; - - buffer.drawLabel(config.restart_key, length, config.edge_margin); - length += config.restart_key.len + 1; - buffer.drawLabel(" ", length - 1, config.edge_margin); - - buffer.drawLabel(lang.restart, length, config.edge_margin); - length += restart_len + 1; - - if (config.sleep_cmd != null) { - buffer.drawLabel(config.sleep_key, length, config.edge_margin); - length += config.sleep_key.len + 1; - buffer.drawLabel(" ", length - 1, config.edge_margin); - - buffer.drawLabel(lang.sleep, length, config.edge_margin); - length += sleep_len + 1; - } - - if (config.hibernate_cmd != null) { - buffer.drawLabel(config.hibernate_key, length, config.edge_margin); - length += config.hibernate_key.len + 1; - buffer.drawLabel(" ", length - 1, config.edge_margin); - - buffer.drawLabel(lang.hibernate, length, config.edge_margin); - length += hibernate_len + 1; - } - - if (config.brightness_down_key) |key| { - buffer.drawLabel(key, length, config.edge_margin); - length += key.len + 1; - buffer.drawLabel(" ", length - 1, config.edge_margin); - - buffer.drawLabel(lang.brightness_down, length, config.edge_margin); - length += brightness_down_len + 1; - } - - if (config.brightness_up_key) |key| { - buffer.drawLabel(key, length, config.edge_margin); - length += key.len + 1; - buffer.drawLabel(" ", length - 1, config.edge_margin); - - buffer.drawLabel(lang.brightness_up, length, config.edge_margin); - length += brightness_up_len + 1; - } - } - - if (config.box_title) |title| { - buffer.drawConfinedLabel(title, buffer.box_x, buffer.box_y - 1, buffer.box_width); - } - - if (config.vi_mode) { - const label_txt = if (insert_mode) lang.insert else lang.normal; - buffer.drawLabel(label_txt, buffer.box_x, buffer.box_y + buffer.box_height); - } - - if (!config.hide_keyboard_locks and can_get_lock_state) draw_lock_state: { - const lock_state = interop.getLockState() catch |err| { - try info_line.addMessage(lang.err_lock_state, config.error_bg, config.error_fg); - can_get_lock_state = false; - try log_file.err("sys", "failed to get lock state: {s}", .{@errorName(err)}); - break :draw_lock_state; - }; - - var lock_state_x = buffer.width - @min(buffer.width, lang.numlock.len) - config.edge_margin; - var lock_state_y: usize = config.edge_margin; - - if (config.clock != null) lock_state_y += 1; - - if (lock_state.numlock) buffer.drawLabel(lang.numlock, lock_state_x, lock_state_y); - - if (lock_state_x >= lang.capslock.len + 1) { - lock_state_x -= lang.capslock.len + 1; - if (lock_state.capslock) buffer.drawLabel(lang.capslock, lock_state_x, lock_state_y); - } - } - - session.label.draw(); - login.label.draw(); - password.draw(); - - _ = TerminalBuffer.presentBufferStatic(); + if (state.update) { + if (!try drawUi(config, lang, &log_file, &state)) continue; } var timeout: i32 = -1; // Calculate the maximum timeout based on current animations, or the (big) clock. If there's none, we wait for the event indefinitely instead - if (animate and !animation_timed_out) { + if (state.animate and !state.animation_timed_out) { timeout = config.min_refresh_delta; // Check how long we've been running so we can turn off the animation const time = try interop.getTimeOfDay(); if (config.animation_timeout_sec > 0 and time.seconds - animation_time_start.seconds > config.animation_timeout_sec) { - animation_timed_out = true; + state.animation_timed_out = true; animation.deinit(); } } else if (config.bigclock != .none and config.clock == null) { const time = try interop.getTimeOfDay(); timeout = @intCast((60 - @rem(time.seconds, 60)) * 1000 - @divTrunc(time.microseconds, 1000) + 1); - } else if (config.clock != null or (config.auth_fails > 0 and auth_fails >= config.auth_fails)) { + } else if (config.clock != null or (config.auth_fails > 0 and state.auth_fails >= config.auth_fails)) { const time = try interop.getTimeOfDay(); timeout = @intCast(1000 - @divTrunc(time.microseconds, 1000) + 1); @@ -900,7 +734,7 @@ pub fn main() !void { } else { const event_error = if (timeout == -1) termbox.tb_poll_event(&event) else termbox.tb_peek_event(&event, timeout); - update = timeout != -1; + state.update = timeout != -1; if (event_error < 0 or event.type != termbox.TB_EVENT_KEY) continue; } @@ -910,9 +744,9 @@ pub fn main() !void { switch (event.key) { termbox.TB_KEY_ESC => { - if (config.vi_mode and insert_mode) { - insert_mode = false; - update = true; + if (config.vi_mode and state.insert_mode) { + state.insert_mode = false; + state.update = true; } }, termbox.TB_KEY_F12...termbox.TB_KEY_F1 => { @@ -968,25 +802,25 @@ pub fn main() !void { } }, termbox.TB_KEY_CTRL_C => run = false, - termbox.TB_KEY_CTRL_U => if (active_input == .password) { + termbox.TB_KEY_CTRL_U => if (state.active_input == .password) { password.clear(); - update = true; + state.update = true; }, termbox.TB_KEY_CTRL_K, termbox.TB_KEY_ARROW_UP => { - active_input.move(true, false); - update = true; + state.active_input.move(true, false); + state.update = true; }, termbox.TB_KEY_CTRL_J, termbox.TB_KEY_ARROW_DOWN => { - active_input.move(false, false); - update = true; + state.active_input.move(false, false); + state.update = true; }, termbox.TB_KEY_TAB => { - active_input.move(false, true); - update = true; + state.active_input.move(false, true); + state.update = true; }, termbox.TB_KEY_BACK_TAB => { - active_input.move(true, true); - update = true; + state.active_input.move(true, true); + state.update = true; }, termbox.TB_KEY_ENTER => authenticate: { try log_file.info("auth", "starting authentication", .{}); @@ -1102,8 +936,8 @@ pub fn main() !void { const auth_err = shared_err.readError(); if (auth_err) |err| { - auth_fails += 1; - active_input = .password; + 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)}); @@ -1121,9 +955,9 @@ pub fn main() !void { try log_file.info("auth", "logged out", .{}); } - if (config.auth_fails == 0 or auth_fails < config.auth_fails) { + if (config.auth_fails == 0 or state.auth_fails < config.auth_fails) { try TerminalBuffer.clearScreenStatic(true); - update = true; + state.update = true; } // Restore the cursor @@ -1131,41 +965,246 @@ pub fn main() !void { _ = TerminalBuffer.presentBufferStatic(); }, else => { - if (!insert_mode) { + if (!state.insert_mode) { switch (event.ch) { 'k' => { - active_input.move(true, false); - update = true; + state.active_input.move(true, false); + state.update = true; continue; }, 'j' => { - active_input.move(false, false); - update = true; + state.active_input.move(false, false); + state.update = true; continue; }, 'i' => { - insert_mode = true; - update = true; + state.insert_mode = true; + state.update = true; continue; }, else => {}, } } - switch (active_input) { - .info_line => info_line.label.handle(&event, insert_mode), - .session => session.label.handle(&event, insert_mode), - .login => login.label.handle(&event, insert_mode), - .password => password.handle(&event, insert_mode) catch { + 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); }, } - update = true; + + state.update = true; }, } } } +fn drawUi(config: Config, lang: Lang, log_file: *LogFile, state: *UiState) !bool { + // If the user entered a wrong password 10 times in a row, play a cascade animation, else update normally + if (config.auth_fails > 0 and state.auth_fails >= config.auth_fails) { + std.Thread.sleep(std.time.ns_per_ms * 10); + state.update = state.buffer.cascade(); + + if (!state.update) { + std.Thread.sleep(std.time.ns_per_s * 7); + state.auth_fails = 0; + } + + _ = TerminalBuffer.presentBufferStatic(); + return false; + } + + try TerminalBuffer.clearScreenStatic(false); + + var length: usize = config.edge_margin; + + if (!state.animation_timed_out) state.animation.draw(); + + if (!config.hide_version_string) { + state.buffer.drawLabel(ly_version_str, config.edge_margin, state.buffer.height - 1 - config.edge_margin); + } + + if (config.battery_id) |id| draw_battery: { + if (!state.can_draw_battery) break :draw_battery; + + const battery_percentage = getBatteryPercentage(id) catch |err| { + try log_file.err("sys", "failed to get battery percentage: {s}", .{@errorName(err)}); + try state.info_line.addMessage(lang.err_battery, config.error_bg, config.error_fg); + state.can_draw_battery = false; + break :draw_battery; + }; + + var battery_buf: [16:0]u8 = undefined; + const battery_str = std.fmt.bufPrintZ(&battery_buf, "BAT: {d}%", .{battery_percentage}) catch break :draw_battery; + + var battery_y: usize = config.edge_margin; + if (!config.hide_key_hints) { + battery_y += 1; + } + state.buffer.drawLabel(battery_str, config.edge_margin, battery_y); + state.can_draw_battery = true; + } + + if (config.bigclock != .none and state.buffer.box_height + (bigclock.HEIGHT + 2) * 2 < state.buffer.height) { + var format_buf: [16:0]u8 = undefined; + var clock_buf: [32:0]u8 = undefined; + // We need the slice/c-string returned by `bufPrintZ`. + const format = try std.fmt.bufPrintZ(&format_buf, "{s}{s}{s}{s}", .{ + if (config.bigclock_12hr) "%I" else "%H", + ":%M", + if (config.bigclock_seconds) ":%S" else "", + if (config.bigclock_12hr) "%P" else "", + }); + const xo = state.buffer.width / 2 - @min(state.buffer.width, (format.len * (bigclock.WIDTH + 1))) / 2; + const yo = (state.buffer.height - state.buffer.box_height) / 2 - bigclock.HEIGHT - 2; + + const clock_str = interop.timeAsString(&clock_buf, format); + + for (clock_str, 0..) |c, i| { + // TODO: Show error + const clock_cell = try bigclock.clockCell(state.animate, c, state.buffer.fg, state.buffer.bg, config.bigclock); + bigclock.alphaBlit(xo + i * (bigclock.WIDTH + 1), yo, state.buffer.width, state.buffer.height, clock_cell); + } + } + + state.buffer.drawBoxCenter(!config.hide_borders, config.blank_box); + + if (state.resolution_changed) { + const coordinates = state.buffer.calculateComponentCoordinates(); + state.info_line.label.position(coordinates.start_x, coordinates.y, coordinates.full_visible_length, null); + state.session.label.position(coordinates.x, coordinates.y + 2, coordinates.visible_length, config.text_in_center); + state.login.label.position(coordinates.x, coordinates.y + 4, coordinates.visible_length, config.text_in_center); + state.password.position(coordinates.x, coordinates.y + 6, coordinates.visible_length); + + state.resolution_changed = false; + } + + 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(lang.err_alloc, config.error_bg, config.error_fg); + try log_file.err("tui", "failed to handle password input: {s}", .{@errorName(err)}); + }, + } + + if (config.clock) |clock| draw_clock: { + if (!state.can_draw_clock) break :draw_clock; + + var clock_buf: [64:0]u8 = undefined; + const clock_str = interop.timeAsString(&clock_buf, clock); + + if (clock_str.len == 0) { + try state.info_line.addMessage(lang.err_clock_too_long, config.error_bg, config.error_fg); + state.can_draw_clock = false; + try log_file.err("tui", "clock string too long", .{}); + break :draw_clock; + } + + state.buffer.drawLabel(clock_str, state.buffer.width - @min(state.buffer.width, clock_str.len) - config.edge_margin, config.edge_margin); + } + + const label_x = state.buffer.box_x + state.buffer.margin_box_h; + const label_y = state.buffer.box_y + state.buffer.margin_box_v; + + state.buffer.drawLabel(lang.login, label_x, label_y + 4); + state.buffer.drawLabel(lang.password, label_x, label_y + 6); + + state.info_line.label.draw(); + + if (!config.hide_key_hints) { + state.buffer.drawLabel(config.shutdown_key, length, config.edge_margin); + length += config.shutdown_key.len + 1; + state.buffer.drawLabel(" ", length - 1, config.edge_margin); + + state.buffer.drawLabel(lang.shutdown, length, config.edge_margin); + length += state.shutdown_len + 1; + + state.buffer.drawLabel(config.restart_key, length, config.edge_margin); + length += config.restart_key.len + 1; + state.buffer.drawLabel(" ", length - 1, config.edge_margin); + + state.buffer.drawLabel(lang.restart, length, config.edge_margin); + length += state.restart_len + 1; + + if (config.sleep_cmd != null) { + state.buffer.drawLabel(config.sleep_key, length, config.edge_margin); + length += config.sleep_key.len + 1; + state.buffer.drawLabel(" ", length - 1, config.edge_margin); + + state.buffer.drawLabel(lang.sleep, length, config.edge_margin); + length += state.sleep_len + 1; + } + + if (config.hibernate_cmd != null) { + state.buffer.drawLabel(config.hibernate_key, length, config.edge_margin); + length += config.hibernate_key.len + 1; + state.buffer.drawLabel(" ", length - 1, config.edge_margin); + + state.buffer.drawLabel(lang.hibernate, length, config.edge_margin); + length += state.hibernate_len + 1; + } + + if (config.brightness_down_key) |key| { + state.buffer.drawLabel(key, length, config.edge_margin); + length += key.len + 1; + state.buffer.drawLabel(" ", length - 1, config.edge_margin); + + state.buffer.drawLabel(lang.brightness_down, length, config.edge_margin); + length += state.brightness_down_len + 1; + } + + if (config.brightness_up_key) |key| { + state.buffer.drawLabel(key, length, config.edge_margin); + length += key.len + 1; + state.buffer.drawLabel(" ", length - 1, config.edge_margin); + + state.buffer.drawLabel(lang.brightness_up, length, config.edge_margin); + length += state.brightness_up_len + 1; + } + } + + if (config.box_title) |title| { + state.buffer.drawConfinedLabel(title, state.buffer.box_x, state.buffer.box_y - 1, state.buffer.box_width); + } + + if (config.vi_mode) { + const label_txt = if (state.insert_mode) lang.insert else lang.normal; + state.buffer.drawLabel(label_txt, state.buffer.box_x, state.buffer.box_y + state.buffer.box_height); + } + + if (!config.hide_keyboard_locks and state.can_get_lock_state) draw_lock_state: { + const lock_state = interop.getLockState() catch |err| { + try state.info_line.addMessage(lang.err_lock_state, config.error_bg, config.error_fg); + state.can_get_lock_state = false; + try log_file.err("sys", "failed to get lock state: {s}", .{@errorName(err)}); + break :draw_lock_state; + }; + + var lock_state_x = state.buffer.width - @min(state.buffer.width, lang.numlock.len) - config.edge_margin; + var lock_state_y: usize = config.edge_margin; + + if (config.clock != null) lock_state_y += 1; + + if (lock_state.numlock) state.buffer.drawLabel(lang.numlock, lock_state_x, lock_state_y); + + if (lock_state_x >= lang.capslock.len + 1) { + lock_state_x -= lang.capslock.len + 1; + if (lock_state.capslock) state.buffer.drawLabel(lang.capslock, lock_state_x, lock_state_y); + } + } + + state.session.label.draw(); + state.login.label.draw(); + state.password.draw(); + + _ = TerminalBuffer.presentBufferStatic(); + return true; +} + fn configErrorHandler(type_name: []const u8, key: []const u8, value: []const u8, err: anyerror) void { config_errors.append(temporary_allocator, .{ .type_name = temporary_allocator.dupe(u8, type_name) catch return, From 1a04a608a18c25cc47793715728ec280b4d69062 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Sat, 31 Jan 2026 22:51:44 +0100 Subject: [PATCH 028/176] Remove dummy animation Signed-off-by: AnErrupTion --- src/animations/Dummy.zig | 14 -------------- src/main.zig | 18 +++++++----------- 2 files changed, 7 insertions(+), 25 deletions(-) delete mode 100644 src/animations/Dummy.zig diff --git a/src/animations/Dummy.zig b/src/animations/Dummy.zig deleted file mode 100644 index 1dddfeb..0000000 --- a/src/animations/Dummy.zig +++ /dev/null @@ -1,14 +0,0 @@ -const std = @import("std"); -const Animation = @import("../tui/Animation.zig"); - -const Dummy = @This(); - -pub fn animation(self: *Dummy) Animation { - return Animation.init(self, deinit, realloc, draw); -} - -fn deinit(_: *Dummy) void {} - -fn realloc(_: *Dummy) anyerror!void {} - -fn draw(_: *Dummy) void {} diff --git a/src/main.zig b/src/main.zig index 7e17bb8..a164b79 100644 --- a/src/main.zig +++ b/src/main.zig @@ -10,7 +10,6 @@ const enums = @import("enums.zig"); const Environment = @import("Environment.zig"); const ColorMix = @import("animations/ColorMix.zig"); const Doom = @import("animations/Doom.zig"); -const Dummy = @import("animations/Dummy.zig"); const Matrix = @import("animations/Matrix.zig"); const GameOfLife = @import("animations/GameOfLife.zig"); const DurFile = @import("animations/DurFile.zig"); @@ -70,7 +69,7 @@ const UiState = struct { update: bool, buffer: *TerminalBuffer, animation_timed_out: bool, - animation: *Animation, + animation: *?Animation, can_draw_battery: bool, info_line: *InfoLine, animate: bool, @@ -507,7 +506,7 @@ pub fn main() !void { is_autologin = true; } - var animation: Animation = undefined; + var animation: ?Animation = null; var state = UiState{ .auth_fails = 0, .update = true, @@ -580,10 +579,7 @@ pub fn main() !void { // Initialize the animation, if any switch (config.animation) { - .none => { - var dummy = Dummy{}; - animation = dummy.animation(); - }, + .none => {}, .doom => { var doom = try Doom.init(allocator, &buffer, config.doom_top_color, config.doom_middle_color, config.doom_bottom_color, config.doom_fire_height, config.doom_fire_spread); animation = doom.animation(); @@ -605,7 +601,7 @@ pub fn main() !void { animation = dur.animation(); }, } - defer animation.deinit(); + 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); @@ -660,7 +656,7 @@ pub fn main() !void { buffer.width = width; buffer.height = height; - animation.realloc() catch |err| { + if (animation) |*a| a.realloc() catch |err| { try info_line.addMessage(lang.err_alloc, config.error_bg, config.error_fg); try log_file.err("tui", "failed to reallocate animation buffers: {s}", .{@errorName(err)}); }; @@ -685,7 +681,7 @@ pub fn main() !void { if (config.animation_timeout_sec > 0 and time.seconds - animation_time_start.seconds > config.animation_timeout_sec) { state.animation_timed_out = true; - animation.deinit(); + if (animation) |*a| a.deinit(); } } else if (config.bigclock != .none and config.clock == null) { const time = try interop.getTimeOfDay(); @@ -1020,7 +1016,7 @@ fn drawUi(config: Config, lang: Lang, log_file: *LogFile, state: *UiState) !bool var length: usize = config.edge_margin; - if (!state.animation_timed_out) state.animation.draw(); + if (!state.animation_timed_out) if (state.animation.*) |*a| a.draw(); if (!config.hide_version_string) { state.buffer.drawLabel(ly_version_str, config.edge_margin, state.buffer.height - 1 - config.edge_margin); From 7ce8ff61fe23dc1b276fafdb047d5830560c53cc Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Sun, 1 Feb 2026 10:41:06 +0100 Subject: [PATCH 029/176] Unify ini parsing logic Signed-off-by: AnErrupTion --- ly-core/build.zig | 3 + ly-core/build.zig.zon | 7 +- ly-core/src/root.zig | 72 ++++++++++++++++++++ src/config/migrator.zig | 77 ++++++++++----------- src/main.zig | 144 ++++++++++++---------------------------- 5 files changed, 162 insertions(+), 141 deletions(-) diff --git a/ly-core/build.zig b/ly-core/build.zig index 0fff11c..0671528 100644 --- a/ly-core/build.zig +++ b/ly-core/build.zig @@ -9,6 +9,9 @@ pub fn build(b: *std.Build) void { .optimize = optimize, }); + const zigini = b.dependency("zigini", .{ .target = target, .optimize = optimize }); + mod.addImport("zigini", zigini.module("zigini")); + const mod_tests = b.addTest(.{ .root_module = mod, }); diff --git a/ly-core/build.zig.zon b/ly-core/build.zig.zon index 13a7faf..98233c5 100644 --- a/ly-core/build.zig.zon +++ b/ly-core/build.zig.zon @@ -3,7 +3,12 @@ .version = "1.0.0", .fingerprint = 0xddda7afda795472, .minimum_zig_version = "0.15.0", - .dependencies = .{}, + .dependencies = .{ + .zigini = .{ + .url = "git+https://github.com/AnErrupTion/zigini?ref=zig-0.15.0#9281f47702b57779e831d7618e158abb8eb4d4a2", + .hash = "zigini-0.3.3-36M0FRJJAADZVq5HPm-hYKMpFFTr0OgjbEYcK2ijKZ5n", + }, + }, .paths = .{ "build.zig", "build.zig.zon", diff --git a/ly-core/src/root.zig b/ly-core/src/root.zig index afc7ea0..a7929f4 100644 --- a/ly-core/src/root.zig +++ b/ly-core/src/root.zig @@ -1,4 +1,76 @@ +const std = @import("std"); +const ini = @import("zigini"); + pub const interop = @import("interop.zig"); pub const UidRange = @import("UidRange.zig"); pub const LogFile = @import("LogFile.zig"); pub const SharedError = @import("SharedError.zig"); + +pub fn IniParser(comptime Struct: type) type { + return struct { + const Self = @This(); + const temporary_allocator = std.heap.page_allocator; + + pub const Error = struct { + type_name: []const u8, + key: []const u8, + value: []const u8, + error_name: []const u8, + }; + pub var global_errors: std.ArrayList(Error) = .empty; + + ini_struct: ini.Ini(Struct), + structure: Struct, + maybe_load_error: ?anyerror, + errors: std.ArrayList(Error), + + pub fn init( + allocator: std.mem.Allocator, + path: []const u8, + field_handler: ?fn (allocator: std.mem.Allocator, field: ini.IniField) ?ini.IniField, + ) !Self { + var ini_struct = ini.Ini(Struct).init(allocator); + errdefer ini_struct.deinit(); + + var maybe_load_error: ?anyerror = null; + + const structure = ini_struct.readFileToStruct(path, .{ + .fieldHandler = field_handler, + .errorHandler = errorHandler, + .comment_characters = "#", + }) catch |err| load_error: { + maybe_load_error = err; + break :load_error Struct{}; + }; + + return .{ + .ini_struct = ini_struct, + .structure = structure, + .maybe_load_error = maybe_load_error, + .errors = global_errors, + }; + } + + pub fn deinit(self: *Self) void { + self.ini_struct.deinit(); + + for (0..global_errors.items.len) |i| { + const err = global_errors.items[i]; + temporary_allocator.free(err.type_name); + temporary_allocator.free(err.key); + temporary_allocator.free(err.value); + } + + global_errors.deinit(temporary_allocator); + } + + fn errorHandler(type_name: []const u8, key: []const u8, value: []const u8, err: anyerror) void { + global_errors.append(temporary_allocator, .{ + .type_name = temporary_allocator.dupe(u8, type_name) catch return, + .key = temporary_allocator.dupe(u8, key) catch return, + .value = temporary_allocator.dupe(u8, value) catch return, + .error_name = @errorName(err), + }) catch return; + } + }; +} diff --git a/src/config/migrator.zig b/src/config/migrator.zig index 124ec89..cfd69cc 100644 --- a/src/config/migrator.zig +++ b/src/config/migrator.zig @@ -4,11 +4,14 @@ const std = @import("std"); const ini = @import("zigini"); +const ly_core = @import("ly-core"); const Config = @import("Config.zig"); const OldSave = @import("OldSave.zig"); const SavedUsers = @import("SavedUsers.zig"); const TerminalBuffer = @import("../tui/TerminalBuffer.zig"); +const IniParser = ly_core.IniParser; + const Color = TerminalBuffer.Color; const Styling = TerminalBuffer.Styling; @@ -187,13 +190,40 @@ pub fn lateConfigFieldHandler(config: *Config) void { } } -pub fn tryMigrateFirstSaveFile(user_buf: *[32]u8) OldSave { - var save = OldSave{}; +pub fn tryMigrateIniSaveFile(allocator: std.mem.Allocator, path: []const u8, saved_users: *SavedUsers, usernames: [][]const u8) !?IniParser(OldSave) { + var save_parser = try IniParser(OldSave).init(allocator, path, null); + errdefer save_parser.deinit(); + var user_buf: [32]u8 = undefined; + const maybe_save = if (save_parser.maybe_load_error == null) save_parser.structure else tryMigrateFirstSaveFile(&user_buf); + + if (maybe_save) |save| { + // Add all other users to the list + for (usernames, 0..) |username, i| { + if (save.user) |user| { + if (std.mem.eql(u8, user, username)) saved_users.last_username_index = i; + } + + try saved_users.user_list.append(allocator, .{ + .username = username, + .session_index = save.session_index orelse 0, + .first_run = false, + .allocated_username = false, + }); + } + + return save_parser; + } + + return null; +} + +fn tryMigrateFirstSaveFile(user_buf: *[32]u8) ?OldSave { if (maybe_save_file) |path| { defer temporary_allocator.free(path); - var file = std.fs.openFileAbsolute(path, .{}) catch return save; + var save = OldSave{}; + var file = std.fs.openFileAbsolute(path, .{}) catch return null; defer file.close(); var file_buffer: [64]u8 = undefined; @@ -201,50 +231,21 @@ pub fn tryMigrateFirstSaveFile(user_buf: *[32]u8) OldSave { var reader = &file_reader.interface; var user_writer = std.Io.Writer.fixed(user_buf); - var written = reader.streamDelimiter(&user_writer, '\n') catch return save; + var written = reader.streamDelimiter(&user_writer, '\n') catch return null; if (written > 0) save.user = user_buf[0..written]; var session_buf: [20]u8 = undefined; var session_writer = std.Io.Writer.fixed(&session_buf); - written = reader.streamDelimiter(&session_writer, '\n') catch return save; + written = reader.streamDelimiter(&session_writer, '\n') catch return null; var session_index: ?usize = null; if (written > 0) { - session_index = std.fmt.parseUnsigned(usize, session_buf[0..written], 10) catch return save; + session_index = std.fmt.parseUnsigned(usize, session_buf[0..written], 10) catch return null; } save.session_index = session_index; + + return save; } - return save; -} - -pub fn tryMigrateIniSaveFile(allocator: std.mem.Allocator, save_ini: *ini.Ini(OldSave), path: []const u8, saved_users: *SavedUsers, usernames: [][]const u8) !bool { - var old_save_file_exists = true; - - var user_buf: [32]u8 = undefined; - const save = save_ini.readFileToStruct(path, .{ - .fieldHandler = null, - .comment_characters = "#", - }) catch no_save_file: { - old_save_file_exists = false; - break :no_save_file tryMigrateFirstSaveFile(&user_buf); - }; - - if (!old_save_file_exists) return false; - - // Add all other users to the list - for (usernames, 0..) |username, i| { - if (save.user) |user| { - if (std.mem.eql(u8, user, username)) saved_users.last_username_index = i; - } - - try saved_users.user_list.append(allocator, .{ - .username = username, - .session_index = save.session_index orelse 0, - .first_run = false, - .allocated_username = false, - }); - } - - return true; + return null; } diff --git a/src/main.zig b/src/main.zig index a164b79..a8c4bda 100644 --- a/src/main.zig +++ b/src/main.zig @@ -33,6 +33,7 @@ const interop = ly_core.interop; const UidRange = ly_core.UidRange; const LogFile = ly_core.LogFile; const SharedError = ly_core.SharedError; +const IniParser = ly_core.IniParser; const termbox = TerminalBuffer.termbox; const temporary_allocator = std.heap.page_allocator; const ly_version_str = "Ly version " ++ build_options.version; @@ -56,14 +57,6 @@ fn ttyControlTransferSignalHandler(_: c_int) callconv(.c) void { TerminalBuffer.shutdownStatic(); } -const ConfigError = struct { - type_name: []const u8, - key: []const u8, - value: []const u8, - error_name: []const u8, -}; -var config_errors: std.ArrayList(ConfigError) = .empty; - const UiState = struct { auth_fails: u64, update: bool, @@ -145,15 +138,15 @@ pub fn main() !void { }; defer if (maybe_res) |*res| res.deinit(); - var config: Config = undefined; - var lang: Lang = undefined; - var old_save_file_exists = false; - var maybe_config_load_error: ?anyerror = null; + var old_save_parser: ?IniParser(OldSave) = null; + defer if (old_save_parser) |*str| str.deinit(); + var start_cmd_exit_code: u8 = 0; var saved_users = SavedUsers.init(); defer saved_users.deinit(allocator); + var config_parent_path: []const u8 = build_options.config_directory ++ "/ly"; if (maybe_res) |*res| { if (res.args.help != 0) { try clap.help(stderr, clap.Help, ¶ms, .{}); @@ -167,79 +160,44 @@ pub fn main() !void { try stderr.flush(); std.process.exit(0); } + if (res.args.config) |path| config_parent_path = path; } // Load configuration file - var config_ini = Ini(Config).init(allocator); - defer config_ini.deinit(); - - var lang_ini = Ini(Lang).init(allocator); - defer lang_ini.deinit(); - - var old_save_ini = ini.Ini(OldSave).init(allocator); - defer old_save_ini.deinit(); - var save_path: []const u8 = build_options.config_directory ++ "/ly/save.txt"; var old_save_path: []const u8 = build_options.config_directory ++ "/ly/save.ini"; var save_path_alloc = false; - defer { - if (save_path_alloc) allocator.free(save_path); - if (save_path_alloc) allocator.free(old_save_path); + defer if (save_path_alloc) { + allocator.free(save_path); + allocator.free(old_save_path); + }; + + const config_path = try std.fs.path.join(allocator, &[_][]const u8{ config_parent_path, "config.ini" }); + defer allocator.free(config_path); + + var config_parser = try IniParser(Config).init(allocator, config_path, migrator.configFieldHandler); + defer config_parser.deinit(); + + var config = config_parser.structure; + + var lang_buffer: [16]u8 = undefined; + const lang_file = try std.fmt.bufPrint(&lang_buffer, "{s}.ini", .{config.lang}); + + const lang_path = try std.fs.path.join(allocator, &[_][]const u8{ config_parent_path, "lang", lang_file }); + defer allocator.free(lang_path); + + var lang_parser = try IniParser(Lang).init(allocator, lang_path, null); + defer lang_parser.deinit(); + + const lang = lang_parser.structure; + + if (config.save) { + save_path = try std.fs.path.join(allocator, &[_][]const u8{ config_parent_path, "save.txt" }); + old_save_path = try std.fs.path.join(allocator, &[_][]const u8{ config_parent_path, "save.ini" }); + save_path_alloc = true; } - const comment_characters = "#"; - - if (maybe_res != null and maybe_res.?.args.config != null) { - const s = maybe_res.?.args.config.?; - const trailing_slash = if (s[s.len - 1] != '/') "/" else ""; - - const config_path = try std.fmt.allocPrint(allocator, "{s}{s}config.ini", .{ s, trailing_slash }); - defer allocator.free(config_path); - - config = config_ini.readFileToStruct(config_path, .{ - .fieldHandler = migrator.configFieldHandler, - .errorHandler = configErrorHandler, - .comment_characters = comment_characters, - }) catch |err| load_error: { - maybe_config_load_error = err; - break :load_error Config{}; - }; - - const lang_path = try std.fmt.allocPrint(allocator, "{s}{s}lang/{s}.ini", .{ s, trailing_slash, config.lang }); - defer allocator.free(lang_path); - - lang = lang_ini.readFileToStruct(lang_path, .{ - .fieldHandler = null, - .comment_characters = comment_characters, - }) catch Lang{}; - - if (config.save) { - save_path = try std.fmt.allocPrint(allocator, "{s}{s}save.txt", .{ s, trailing_slash }); - old_save_path = try std.fmt.allocPrint(allocator, "{s}{s}save.ini", .{ s, trailing_slash }); - save_path_alloc = true; - } - } else { - const config_path = build_options.config_directory ++ "/ly/config.ini"; - - config = config_ini.readFileToStruct(config_path, .{ - .fieldHandler = migrator.configFieldHandler, - .errorHandler = configErrorHandler, - .comment_characters = comment_characters, - }) catch |err| load_error: { - maybe_config_load_error = err; - break :load_error Config{}; - }; - - const lang_path = try std.fmt.allocPrint(allocator, "{s}/ly/lang/{s}.ini", .{ build_options.config_directory, config.lang }); - defer allocator.free(lang_path); - - lang = lang_ini.readFileToStruct(lang_path, .{ - .fieldHandler = null, - .comment_characters = comment_characters, - }) catch Lang{}; - } - - if (maybe_config_load_error == null) { + if (config_parser.maybe_load_error == null) { migrator.lateConfigFieldHandler(&config); } @@ -251,10 +209,10 @@ pub fn main() !void { } if (config.save) read_save_file: { - old_save_file_exists = migrator.tryMigrateIniSaveFile(allocator, &old_save_ini, old_save_path, &saved_users, usernames.items) catch break :read_save_file; + old_save_parser = migrator.tryMigrateIniSaveFile(allocator, old_save_path, &saved_users, usernames.items) catch break :read_save_file; // Don't read the new save file if the old one still exists - if (old_save_file_exists) break :read_save_file; + if (old_save_parser != null) break :read_save_file; var save_file = std.fs.cwd().openFile(save_path, .{}) catch break :read_save_file; defer save_file.close(); @@ -378,22 +336,13 @@ pub fn main() !void { try log_file.err("sys", "failed to execute start command: exit code {d}", .{start_cmd_exit_code}); } - if (maybe_config_load_error) |err| { + if (config_parser.maybe_load_error) |load_error| { // We can't localize this since the config failed to load so we'd fallback to the default language anyway try info_line.addMessage("unable to parse config file", config.error_bg, config.error_fg); - try log_file.err("conf", "unable to parse config file: {s}", .{@errorName(err)}); + try log_file.err("conf", "unable to parse config file: {s}", .{@errorName(load_error)}); - defer config_errors.deinit(temporary_allocator); - - for (0..config_errors.items.len) |i| { - const config_error = config_errors.items[i]; - defer { - temporary_allocator.free(config_error.type_name); - temporary_allocator.free(config_error.key); - temporary_allocator.free(config_error.value); - } - - try log_file.err("conf", "failed to convert value '{s}' of option '{s}' to type '{s}': {s}", .{ config_error.value, config_error.key, config_error.type_name, config_error.error_name }); + for (config_parser.errors.items) |err| { + try log_file.err("conf", "failed to convert value '{s}' of option '{s}' to type '{s}': {s}", .{ err.value, err.key, err.type_name, err.error_name }); } } @@ -866,7 +815,7 @@ 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_file_exists) { + } else if (old_save_parser != null) { std.fs.cwd().deleteFile(old_save_path) catch {}; } } @@ -1201,15 +1150,6 @@ fn drawUi(config: Config, lang: Lang, log_file: *LogFile, state: *UiState) !bool return true; } -fn configErrorHandler(type_name: []const u8, key: []const u8, value: []const u8, err: anyerror) void { - config_errors.append(temporary_allocator, .{ - .type_name = temporary_allocator.dupe(u8, type_name) catch return, - .key = temporary_allocator.dupe(u8, key) catch return, - .value = temporary_allocator.dupe(u8, value) catch return, - .error_name = @errorName(err), - }) catch return; -} - fn addOtherEnvironment(session: *Session, lang: Lang, display_server: DisplayServer, exec: ?[]const u8) !void { const name = switch (display_server) { .shell => lang.shell, From b00d6899e5f0451cbd85712c2304a6c2d876e15d Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Wed, 4 Feb 2026 20:16:53 +0100 Subject: [PATCH 030/176] Fix buffer not resizing with no animation Signed-off-by: AnErrupTion --- src/main.zig | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/main.zig b/src/main.zig index a8c4bda..9bbddfb 100644 --- a/src/main.zig +++ b/src/main.zig @@ -590,7 +590,7 @@ pub fn main() !void { while (run) { // If there's no input or there's an animation, a resolution change needs to be checked - if (!state.update or state.animate) { + if (!state.update or state.animate or config.bigclock != .none or config.clock != null) { if (!state.update) std.Thread.sleep(std.time.ns_per_ms * 100); // Required to update tb_width() and tb_height() @@ -598,14 +598,14 @@ pub fn main() !void { const width = new_dimensions.width; const height = new_dimensions.height; - if (width != buffer.width or height != buffer.height) { + if (width != state.buffer.width or height != state.buffer.height) { // If it did change, then update the cell buffer, reallocate the current animation's buffers, and force a draw update try log_file.info("tui", "screen resolution updated to {d}x{d}", .{ width, height }); - buffer.width = width; - buffer.height = height; + state.buffer.width = width; + state.buffer.height = height; - if (animation) |*a| a.realloc() catch |err| { + if (state.animation.*) |*a| a.realloc() catch |err| { try info_line.addMessage(lang.err_alloc, config.error_bg, config.error_fg); try log_file.err("tui", "failed to reallocate animation buffers: {s}", .{@errorName(err)}); }; @@ -630,7 +630,7 @@ pub fn main() !void { if (config.animation_timeout_sec > 0 and time.seconds - animation_time_start.seconds > config.animation_timeout_sec) { state.animation_timed_out = true; - if (animation) |*a| a.deinit(); + if (state.animation.*) |*a| a.deinit(); } } else if (config.bigclock != .none and config.clock == null) { const time = try interop.getTimeOfDay(); From 72f43fbc5626213f57299e7bd038f7151fb00c32 Mon Sep 17 00:00:00 2001 From: hynak Date: Wed, 4 Feb 2026 20:36:55 +0100 Subject: [PATCH 031/176] Add shell script showing how change TTY colors (#920) ## What are the changes about? What was discussed in !912 before I accidentally caused it to auto merge (still not sure how that happened). I assume this is what was meant when asking for it to be in the startup script commented out. ## What existing issue does this resolve? N/A ## Pre-requisites - [x] I have tested & confirmed the changes work locally Reviewed-on: https://codeberg.org/fairyglade/ly/pulls/920 Reviewed-by: AnErrupTion Co-authored-by: hynak Co-committed-by: hynak --- build.zig | 2 ++ res/config.ini | 4 ++-- res/startup.sh | 38 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 42 insertions(+), 2 deletions(-) create mode 100644 res/startup.sh diff --git a/build.zig b/build.zig index 71508cf..d59d0d1 100644 --- a/build.zig +++ b/build.zig @@ -198,6 +198,8 @@ fn install_ly(allocator: std.mem.Allocator, patch_map: PatchMap, install_config: const patched_setup = try patchFile(allocator, "res/setup.sh", patch_map); try installText(patched_setup, config_dir, ly_config_directory, "setup.sh", .{ .mode = 0o755 }); + try installFile("res/startup.sh", config_dir, ly_config_directory, "startup.sh", .{ .override_mode = 0o755 }); + try installFile("res/example.dur", config_dir, ly_config_directory, "example.dur", .{ .override_mode = 0o755 }); } diff --git a/res/config.ini b/res/config.ini index cbebcde..f2793d4 100644 --- a/res/config.ini +++ b/res/config.ini @@ -337,8 +337,8 @@ sleep_cmd = null sleep_key = F3 # Command executed when starting Ly (before the TTY is taken control of) -# If null, no command will be executed -start_cmd = null +# See file at path below for an example of changing the default TTY colors +start_cmd = $CONFIG_DIRECTORY/ly/startup.sh # Center the session name. text_in_center = false diff --git a/res/startup.sh b/res/startup.sh new file mode 100644 index 0000000..6685403 --- /dev/null +++ b/res/startup.sh @@ -0,0 +1,38 @@ +#!/bin/sh +# This file is executed when starting Ly (before the TTY is taken control of) +# Custom startup code can be placed in this file or the start_cmd var can be pointed to a different file + + +# Uncomment the example below for an example of changing the default TTY colors to an alternitive palette on linux +# Colors are in red/green/blue hex (the current colors are a brighter palette than default) +# +#if [ "$TERM" = "linux" ]; then +# declare -a colors=( +# [0]="232323" # black +# [1]="D75F5F" # dark red +# [2]="87AF5F" # dark green +# [3]="D7AF87" # dark yellow +# [4]="8787AF" # dark blue +# [5]="BD53A5" # dark magenta +# [6]="5FAFAF" # dark cyan +# [7]="E5E5E5" # light gray +# [8]="2B2B2B" # dark gray +# [9]="E33636" # red +# [10]="98E34D" # green +# [11]="FFD75F" # yellow +# [12]="7373C9" # blue +# [13]="D633B2" # magenta +# [14]="44C9C9" # cyan +# [15]="FFFFFF" # white +# ) +# +# control_palette_str="\e]P" +# +# for i in ${!colors[@]} +# do +# echo -en "${control_palette_str}$( printf "%x" ${i} )${colors[i]}" +# done +# +# clear # for fixing background artifacting after changing color +#fi + From 2b46a8179653fa694acf000ee772000ca254c60d Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Thu, 5 Feb 2026 15:01:45 +0100 Subject: [PATCH 032/176] Add basic KMSCON support (closes #886) It's not perfect yet, but at least it works! Signed-off-by: AnErrupTion --- build.zig | 3 +++ res/config.ini | 5 +++-- res/ly-kmsconvt@.service | 17 +++++++++++++++++ src/auth.zig | 12 +++++++++--- src/main.zig | 4 ++++ 5 files changed, 36 insertions(+), 5 deletions(-) create mode 100644 res/ly-kmsconvt@.service diff --git a/build.zig b/build.zig index d59d0d1..1688603 100644 --- a/build.zig +++ b/build.zig @@ -269,6 +269,9 @@ fn install_service(allocator: std.mem.Allocator, patch_map: PatchMap) !void { const patched_service = try patchFile(allocator, "res/ly@.service", patch_map); try installText(patched_service, service_dir, service_path, "ly@.service", .{ .mode = 0o644 }); + + const patched_kmsconvt_service = try patchFile(allocator, "res/ly-kmsconvt@.service", patch_map); + try installText(patched_kmsconvt_service, service_dir, service_path, "ly-kmsconvt@.service", .{ .mode = 0o644 }); }, .openrc => { const service_path = try std.fs.path.join(allocator, &[_][]const u8{ dest_directory, config_directory, "/init.d" }); diff --git a/res/config.ini b/res/config.ini index f2793d4..5a10e35 100644 --- a/res/config.ini +++ b/res/config.ini @@ -316,8 +316,9 @@ service_name = ly # Session log file path # This will contain stdout and stderr of Wayland sessions # By default it's saved in the user's home directory -# Important: due to technical limitations, X11 and shell sessions aren't supported, which -# means you won't get any logs from those sessions. +# Important: due to technical limitations, X11, shell sessions as well as +# launching session via KMSCON aren't supported, which means you won't get any +# logs from those sessions. # If null, no session log will be created session_log = .local/state/ly-session.log diff --git a/res/ly-kmsconvt@.service b/res/ly-kmsconvt@.service new file mode 100644 index 0000000..e6212c0 --- /dev/null +++ b/res/ly-kmsconvt@.service @@ -0,0 +1,17 @@ +[Unit] +Description=TUI display manager using KMSCON +After=systemd-user-sessions.service plymouth-quit-wait.service +After=kmsconvt@%i.service +Conflicts=kmsconvt@%i.service + +[Service] +ExecStart=$PREFIX_DIRECTORY/bin/kmscon --vt=%I --seats=seat0 --login -- $PREFIX_DIRECTORY/bin/ly --use-kmscon-vt +StandardInput=tty +UtmpIdentifier=%I +TTYPath=/dev/%I +TTYReset=yes +TTYVHangup=yes +TTYVTDisallocate=yes + +[Install] +WantedBy=multi-user.target diff --git a/src/auth.zig b/src/auth.zig index 9e90021..ddf34f9 100644 --- a/src/auth.zig +++ b/src/auth.zig @@ -22,6 +22,7 @@ pub const AuthOptions = struct { x_cmd: []const u8, x_vt: ?u8, session_pid: std.posix.pid_t, + use_kmscon_vt: bool, }; var xorg_pid: std.posix.pid_t = 0; @@ -475,7 +476,7 @@ fn executeX11Cmd(log_file: *LogFile, allocator: std.mem.Allocator, shell: []cons xorg_pid = try std.posix.fork(); if (xorg_pid == 0) { var cmd_buffer: [1024]u8 = undefined; - const cmd_str = std.fmt.bufPrintZ(&cmd_buffer, "{s} {s} {s}", .{ options.setup_cmd, options.login_cmd orelse "", desktop_cmd }) catch std.process.exit(1); + const cmd_str = std.fmt.bufPrintZ(&cmd_buffer, "{s} {s} {s} {s}", .{ if (options.use_kmscon_vt) "kmscon-launch-gui" else "", options.setup_cmd, options.login_cmd orelse "", desktop_cmd }) catch std.process.exit(1); try log_file.info("auth/x11", "executing: {s} -c {s}", .{ shell, cmd_str }); const args = [_:null]?[*:0]const u8{ shell_z, "-c", cmd_str }; @@ -508,7 +509,12 @@ fn executeCmd(global_log_file: *LogFile, allocator: std.mem.Allocator, shell: [] try global_log_file.info("auth/sys", "launching wayland/shell/custom session", .{}); var maybe_log_file: ?std.fs.File = null; - if (!is_terminal) { + if (!is_terminal) redirect_streams: { + if (options.use_kmscon_vt) { + try global_log_file.err("auth/sys", "cannot redirect stdio & stderr with kmscon", .{}); + break :redirect_streams; + } + // For custom desktop entries, the "Terminal" value here determines if // we redirect standard output & error or not. That is, we redirect only // if it's equal to false (so if it's not running in a TTY). @@ -523,7 +529,7 @@ fn executeCmd(global_log_file: *LogFile, allocator: std.mem.Allocator, shell: [] defer allocator.free(shell_z); var cmd_buffer: [1024]u8 = undefined; - const cmd_str = try std.fmt.bufPrintZ(&cmd_buffer, "{s} {s} {s}", .{ options.setup_cmd, options.login_cmd orelse "", exec_cmd orelse shell }); + const cmd_str = try std.fmt.bufPrintZ(&cmd_buffer, "{s} {s} {s} {s}", .{ if (!is_terminal and options.use_kmscon_vt) "kmscon-launch-gui" else "", options.setup_cmd, options.login_cmd orelse "", exec_cmd orelse shell }); try global_log_file.info("auth/sys", "executing: {s} -c {s}", .{ shell, cmd_str }); const args = [_:null]?[*:0]const u8{ shell_z, "-c", cmd_str }; diff --git a/src/main.zig b/src/main.zig index 9bbddfb..f2e2fcd 100644 --- a/src/main.zig +++ b/src/main.zig @@ -126,6 +126,7 @@ pub fn main() !void { \\-h, --help Shows all commands. \\-v, --version Shows the version of Ly. \\-c, --config Overrides the default configuration path. Example: --config /usr/share/ly + \\--use-kmscon-vt Use KMSCON instead of kernel VT ); var diag = clap.Diagnostic{}; @@ -141,6 +142,7 @@ pub fn main() !void { var old_save_parser: ?IniParser(OldSave) = null; defer if (old_save_parser) |*str| str.deinit(); + var use_kmscon_vt = false; var start_cmd_exit_code: u8 = 0; var saved_users = SavedUsers.init(); @@ -161,6 +163,7 @@ pub fn main() !void { std.process.exit(0); } if (res.args.config) |path| config_parent_path = path; + if (res.args.@"use-kmscon-vt" != 0) use_kmscon_vt = true; } // Load configuration file @@ -845,6 +848,7 @@ pub fn main() !void { .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 From 21fca058e73e6a386cdf49c255882d5d444a5b80 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Thu, 5 Feb 2026 15:03:05 +0100 Subject: [PATCH 033/176] Don't install startup.sh with installnoconf Signed-off-by: AnErrupTion --- build.zig | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.zig b/build.zig index 1688603..dcac1a9 100644 --- a/build.zig +++ b/build.zig @@ -190,6 +190,8 @@ fn install_ly(allocator: std.mem.Allocator, patch_map: PatchMap, install_config: if (install_config) { const patched_config = try patchFile(allocator, "res/config.ini", patch_map); try installText(patched_config, config_dir, ly_config_directory, "config.ini", .{}); + + try installFile("res/startup.sh", config_dir, ly_config_directory, "startup.sh", .{ .override_mode = 0o755 }); } const patched_example_config = try patchFile(allocator, "res/config.ini", patch_map); @@ -198,8 +200,6 @@ fn install_ly(allocator: std.mem.Allocator, patch_map: PatchMap, install_config: const patched_setup = try patchFile(allocator, "res/setup.sh", patch_map); try installText(patched_setup, config_dir, ly_config_directory, "setup.sh", .{ .mode = 0o755 }); - try installFile("res/startup.sh", config_dir, ly_config_directory, "startup.sh", .{ .override_mode = 0o755 }); - try installFile("res/example.dur", config_dir, ly_config_directory, "example.dur", .{ .override_mode = 0o755 }); } From 11735290b848a30c4b47e26fd2e95f23da623d5e Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Thu, 5 Feb 2026 19:50:48 +0100 Subject: [PATCH 034/176] Fix active TTY detection for KMSCON Signed-off-by: AnErrupTion --- ly-core/src/interop.zig | 21 +++++++++++++++++---- src/main.zig | 4 ++-- 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/ly-core/src/interop.zig b/ly-core/src/interop.zig index 1ba8613..ef542f7 100644 --- a/ly-core/src/interop.zig +++ b/ly-core/src/interop.zig @@ -96,8 +96,21 @@ fn PlatformStruct() type { // 4. Finally, compare the major and minor device numbers with the // extracted values. If they correspond, parse [dir] to get the // TTY ID - pub fn getActiveTtyImpl(allocator: std.mem.Allocator) !u8 { + pub fn getActiveTtyImpl(allocator: std.mem.Allocator, use_kmscon_vt: bool) !u8 { var file_buffer: [256]u8 = undefined; + + if (use_kmscon_vt) { + var file = try std.fs.openFileAbsolute("/sys/class/tty/tty0/active", .{}); + defer file.close(); + + var reader = file.reader(&file_buffer); + var buffer: [16]u8 = undefined; + const read = try readBuffer(&reader.interface, &buffer); + + const tty = buffer[0..(read - 1)]; + return std.fmt.parseInt(u8, tty["tty".len..], 10); + } + var tty_major: u16 = undefined; var tty_minor: u16 = undefined; @@ -242,7 +255,7 @@ fn PlatformStruct() type { if (result != 0) return error.SetUserUidFailed; } - pub fn getActiveTtyImpl(_: std.mem.Allocator) !u8 { + pub fn getActiveTtyImpl(_: std.mem.Allocator, _: bool) !u8 { return error.FeatureUnimplemented; } @@ -285,8 +298,8 @@ pub fn getTimeOfDay() !TimeOfDay { }; } -pub fn getActiveTty(allocator: std.mem.Allocator) !u8 { - return platform_struct.getActiveTtyImpl(allocator); +pub fn getActiveTty(allocator: std.mem.Allocator, use_kmscon_vt: bool) !u8 { + return platform_struct.getActiveTtyImpl(allocator, use_kmscon_vt); } pub fn switchTty(tty: u8) !void { diff --git a/src/main.zig b/src/main.zig index f2e2fcd..cc305c4 100644 --- a/src/main.zig +++ b/src/main.zig @@ -568,14 +568,14 @@ pub fn main() !void { var inactivity_cmd_ran = false; // Switch to selected TTY - const active_tty = interop.getActiveTty(allocator) catch |err| no_tty_found: { + 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; }; 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 tty: {s}", .{@errorName(err)}); + try log_file.err("sys", "failed to switch to tty {d}: {s}", .{ active_tty, @errorName(err) }); }; if (config.initial_info_text) |text| { From 7744745f0989f43f2d7d48149e32c747430ebc44 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Thu, 5 Feb 2026 19:55:40 +0100 Subject: [PATCH 035/176] Don't try to switch TTY with KMSCON It can do it automatically (see the corresponding service file). Signed-off-by: AnErrupTion --- src/main.zig | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/main.zig b/src/main.zig index cc305c4..c55f1d9 100644 --- a/src/main.zig +++ b/src/main.zig @@ -573,10 +573,12 @@ pub fn main() !void { try log_file.err("sys", "failed to get active tty: {s}", .{@errorName(err)}); break :no_tty_found build_options.fallback_tty; }; - 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 (!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); From b032c9b2961f2453038299e06c8f87c10fc2005a Mon Sep 17 00:00:00 2001 From: WinuxVidYapan Date: Thu, 5 Feb 2026 20:19:41 +0100 Subject: [PATCH 036/176] Update Turkish translations (#917) So i am sorry for unintended behavior i am new to Codeberg and pull requests But here are the words. - [x] I have tested & confirmed the changes work locally Reviewed-on: https://codeberg.org/fairyglade/ly/pulls/917 Reviewed-by: AnErrupTion Co-authored-by: WinuxVidYapan Co-committed-by: WinuxVidYapan --- res/lang/tr.ini | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/res/lang/tr.ini b/res/lang/tr.ini index e351076..d3ad9ba 100644 --- a/res/lang/tr.ini +++ b/res/lang/tr.ini @@ -1,6 +1,6 @@ - - +brightness_down = parlakligi azalt +brightness_up = parlakligi arttir capslock = capslock err_alloc = basarisiz bellek ayirma @@ -60,19 +60,19 @@ err_user_uid = kullanici icin UID ayarlanamadi err_xsessions_dir = oturumlar klasoru bulunamadi err_xsessions_open = oturumlar klasoru acilamadi - +hibernate = askiya al login = kullanici logout = oturumdan cikis yapildi numlock = numlock - +other = baska password = sifre restart = yeniden baslat shell = shell shutdown = makineyi kapat - +sleep = uykuya al wayland = wayland -xinitrc = xinitrc +xinitrc = xinitrc \ No newline at end of file From ce0d00771d4e25f3734de86893197cd9770bfc81 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Sat, 7 Feb 2026 10:43:08 +0100 Subject: [PATCH 037/176] Use the unifont engine for KMSCON Signed-off-by: AnErrupTion --- res/ly-kmsconvt@.service | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/res/ly-kmsconvt@.service b/res/ly-kmsconvt@.service index e6212c0..3e7d1fd 100644 --- a/res/ly-kmsconvt@.service +++ b/res/ly-kmsconvt@.service @@ -5,7 +5,7 @@ After=kmsconvt@%i.service Conflicts=kmsconvt@%i.service [Service] -ExecStart=$PREFIX_DIRECTORY/bin/kmscon --vt=%I --seats=seat0 --login -- $PREFIX_DIRECTORY/bin/ly --use-kmscon-vt +ExecStart=$PREFIX_DIRECTORY/bin/kmscon --font-engine unifont --vt=%I --seats=seat0 --login -- $PREFIX_DIRECTORY/bin/ly --use-kmscon-vt StandardInput=tty UtmpIdentifier=%I TTYPath=/dev/%I From 950eeed3eed69e0a730cda7ef15a05c42dd70a0b Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Sat, 7 Feb 2026 10:48:05 +0100 Subject: [PATCH 038/176] Log which VT is used Signed-off-by: AnErrupTion --- src/main.zig | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main.zig b/src/main.zig index c55f1d9..3167c4f 100644 --- a/src/main.zig +++ b/src/main.zig @@ -263,6 +263,8 @@ pub fn main() !void { var log_file = try LogFile.init(config.ly_log, &log_file_buffer); defer log_file.deinit(); + try log_file.info("tui", "using {s} vt", .{if (use_kmscon_vt) "kmscon" else "default"}); + // These strings only end up getting freed if the user quits Ly using Ctrl+C, which is fine since in the other cases // we end up shutting down or restarting the system shutdown_cmd = try temporary_allocator.dupe(u8, config.shutdown_cmd); From fd81da7cbddc5e7c4dca50a50a3fa08998cbf496 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Sat, 7 Feb 2026 17:01:11 +0100 Subject: [PATCH 039/176] Organise imports Signed-off-by: AnErrupTion --- src/Environment.zig | 6 ++-- src/animations/ColorMix.zig | 3 +- src/animations/Doom.zig | 1 + src/animations/DurFile.zig | 20 +++++------- src/animations/GameOfLife.zig | 4 +-- src/animations/Matrix.zig | 6 ++-- src/auth.zig | 11 ++++--- src/bigclock.zig | 13 ++++---- src/bigclock/en.zig | 1 - src/bigclock/fa.zig | 1 - src/config/Config.zig | 2 +- src/config/migrator.zig | 15 ++++----- src/main.zig | 58 +++++++++++++++++---------------- src/tui/Cell.zig | 1 - src/tui/TerminalBuffer.zig | 9 +++-- src/tui/components/InfoLine.zig | 4 +-- src/tui/components/Session.zig | 8 ++--- src/tui/components/Text.zig | 10 +++--- src/tui/components/UserList.zig | 6 ++-- src/tui/components/generic.zig | 1 + 20 files changed, 89 insertions(+), 91 deletions(-) diff --git a/src/Environment.zig b/src/Environment.zig index 8184f92..f99ebb7 100644 --- a/src/Environment.zig +++ b/src/Environment.zig @@ -1,9 +1,9 @@ -const enums = @import("enums.zig"); const ini = @import("zigini"); - -const DisplayServer = enums.DisplayServer; const Ini = ini.Ini; +const enums = @import("enums.zig"); +const DisplayServer = enums.DisplayServer; + pub const DesktopEntry = struct { Exec: []const u8 = "", Name: []const u8 = "", diff --git a/src/animations/ColorMix.zig b/src/animations/ColorMix.zig index 3db2e46..3adefb0 100644 --- a/src/animations/ColorMix.zig +++ b/src/animations/ColorMix.zig @@ -1,11 +1,12 @@ const std = @import("std"); +const math = std.math; + const Animation = @import("../tui/Animation.zig"); const Cell = @import("../tui/Cell.zig"); const TerminalBuffer = @import("../tui/TerminalBuffer.zig"); const ColorMix = @This(); -const math = std.math; const Vec2 = @Vector(2, f32); const time_scale: f32 = 0.01; diff --git a/src/animations/Doom.zig b/src/animations/Doom.zig index 9917bcd..008ba25 100644 --- a/src/animations/Doom.zig +++ b/src/animations/Doom.zig @@ -1,5 +1,6 @@ const std = @import("std"); const Allocator = std.mem.Allocator; + const Animation = @import("../tui/Animation.zig"); const Cell = @import("../tui/Cell.zig"); const TerminalBuffer = @import("../tui/TerminalBuffer.zig"); diff --git a/src/animations/DurFile.zig b/src/animations/DurFile.zig index a76b88d..24d66c2 100644 --- a/src/animations/DurFile.zig +++ b/src/animations/DurFile.zig @@ -1,22 +1,20 @@ const std = @import("std"); -const ly_core = @import("ly-core"); -const Animation = @import("../tui/Animation.zig"); -const Cell = @import("../tui/Cell.zig"); -const TerminalBuffer = @import("../tui/TerminalBuffer.zig"); -const enums = @import("../enums.zig"); - -const DurOffsetAlignment = enums.DurOffsetAlignment; - -const Color = TerminalBuffer.Color; -const Styling = TerminalBuffer.Styling; - const Allocator = std.mem.Allocator; const Json = std.json; const eql = std.mem.eql; const flate = std.compress.flate; +const ly_core = @import("ly-core"); const LogFile = ly_core.LogFile; +const enums = @import("../enums.zig"); +const DurOffsetAlignment = enums.DurOffsetAlignment; +const Animation = @import("../tui/Animation.zig"); +const Cell = @import("../tui/Cell.zig"); +const TerminalBuffer = @import("../tui/TerminalBuffer.zig"); +const Color = TerminalBuffer.Color; +const Styling = TerminalBuffer.Styling; + fn read_decompress_file(allocator: Allocator, file_path: []const u8) ![]u8 { const file_buffer = std.fs.cwd().openFile(file_path, .{}) catch { return error.FileNotFound; diff --git a/src/animations/GameOfLife.zig b/src/animations/GameOfLife.zig index 9cbefaa..8dbcb85 100644 --- a/src/animations/GameOfLife.zig +++ b/src/animations/GameOfLife.zig @@ -1,10 +1,10 @@ const std = @import("std"); +const Allocator = std.mem.Allocator; + const Animation = @import("../tui/Animation.zig"); const Cell = @import("../tui/Cell.zig"); const TerminalBuffer = @import("../tui/TerminalBuffer.zig"); -const Allocator = std.mem.Allocator; - const GameOfLife = @This(); // Visual styles - using block characters like other animations diff --git a/src/animations/Matrix.zig b/src/animations/Matrix.zig index 9d01464..0ea4940 100644 --- a/src/animations/Matrix.zig +++ b/src/animations/Matrix.zig @@ -1,11 +1,11 @@ const std = @import("std"); +const Allocator = std.mem.Allocator; +const Random = std.Random; + const Animation = @import("../tui/Animation.zig"); const Cell = @import("../tui/Cell.zig"); const TerminalBuffer = @import("../tui/TerminalBuffer.zig"); -const Allocator = std.mem.Allocator; -const Random = std.Random; - pub const FRAME_DELAY: usize = 8; // Characters change mid-scroll diff --git a/src/auth.zig b/src/auth.zig index ddf34f9..b87965c 100644 --- a/src/auth.zig +++ b/src/auth.zig @@ -1,16 +1,17 @@ const std = @import("std"); -const build_options = @import("build_options"); -const builtin = @import("builtin"); -const ly_core = @import("ly-core"); -const Environment = @import("Environment.zig"); - const Md5 = std.crypto.hash.Md5; +const builtin = @import("builtin"); +const build_options = @import("build_options"); + +const ly_core = @import("ly-core"); const interop = ly_core.interop; const SharedError = ly_core.SharedError; const LogFile = ly_core.LogFile; const utmp = interop.utmp; const Utmp = utmp.utmpx; +const Environment = @import("Environment.zig"); + pub const AuthOptions = struct { tty: u8, service_name: [:0]const u8, diff --git a/src/bigclock.zig b/src/bigclock.zig index bb3617c..d8a090d 100644 --- a/src/bigclock.zig +++ b/src/bigclock.zig @@ -1,16 +1,17 @@ const std = @import("std"); + const ly_core = @import("ly-core"); -const enums = @import("enums.zig"); -const Lang = @import("bigclock/Lang.zig"); +const interop = ly_core.interop; + const en = @import("bigclock/en.zig"); const fa = @import("bigclock/fa.zig"); -const Cell = @import("tui/Cell.zig"); - -const interop = ly_core.interop; -const Bigclock = enums.Bigclock; +const Lang = @import("bigclock/Lang.zig"); pub const WIDTH = Lang.WIDTH; pub const HEIGHT = Lang.HEIGHT; pub const SIZE = Lang.SIZE; +const enums = @import("enums.zig"); +const Bigclock = enums.Bigclock; +const Cell = @import("tui/Cell.zig"); pub fn clockCell(animate: bool, char: u8, fg: u32, bg: u32, bigclock: Bigclock) ![SIZE]Cell { var cells: [SIZE]Cell = undefined; diff --git a/src/bigclock/en.zig b/src/bigclock/en.zig index 42a45f9..5f3b146 100644 --- a/src/bigclock/en.zig +++ b/src/bigclock/en.zig @@ -1,5 +1,4 @@ const Lang = @import("Lang.zig"); - const LocaleChars = Lang.LocaleChars; const X = Lang.X; const O = Lang.O; diff --git a/src/bigclock/fa.zig b/src/bigclock/fa.zig index acfde42..9b626b8 100644 --- a/src/bigclock/fa.zig +++ b/src/bigclock/fa.zig @@ -1,5 +1,4 @@ const Lang = @import("Lang.zig"); - const LocaleChars = Lang.LocaleChars; const X = Lang.X; const O = Lang.O; diff --git a/src/config/Config.zig b/src/config/Config.zig index d48bf09..51dca91 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -1,6 +1,6 @@ const build_options = @import("build_options"); -const enums = @import("../enums.zig"); +const enums = @import("../enums.zig"); const Animation = enums.Animation; const Input = enums.Input; const ViMode = enums.ViMode; diff --git a/src/config/migrator.zig b/src/config/migrator.zig index cfd69cc..7761d70 100644 --- a/src/config/migrator.zig +++ b/src/config/migrator.zig @@ -3,17 +3,18 @@ // Color codes interpreted differently since 1.1.0 const std = @import("std"); +var temporary_allocator = std.heap.page_allocator; + const ini = @import("zigini"); const ly_core = @import("ly-core"); +const IniParser = ly_core.IniParser; + +const TerminalBuffer = @import("../tui/TerminalBuffer.zig"); +const Color = TerminalBuffer.Color; +const Styling = TerminalBuffer.Styling; const Config = @import("Config.zig"); const OldSave = @import("OldSave.zig"); const SavedUsers = @import("SavedUsers.zig"); -const TerminalBuffer = @import("../tui/TerminalBuffer.zig"); - -const IniParser = ly_core.IniParser; - -const Color = TerminalBuffer.Color; -const Styling = TerminalBuffer.Styling; const color_properties = [_][]const u8{ "bg", @@ -44,8 +45,6 @@ const removed_properties = [_][]const u8{ "load", }; -var temporary_allocator = std.heap.page_allocator; - pub var auto_eight_colors: bool = true; pub var maybe_animate: ?bool = null; diff --git a/src/main.zig b/src/main.zig index 3167c4f..692dae6 100644 --- a/src/main.zig +++ b/src/main.zig @@ -1,41 +1,43 @@ const std = @import("std"); -const build_options = @import("build_options"); +const temporary_allocator = std.heap.page_allocator; const builtin = @import("builtin"); -const ly_core = @import("ly-core"); +const build_options = @import("build_options"); + const clap = @import("clap"); const ini = @import("zigini"); -const auth = @import("auth.zig"); -const bigclock = @import("bigclock.zig"); -const enums = @import("enums.zig"); -const Environment = @import("Environment.zig"); -const ColorMix = @import("animations/ColorMix.zig"); -const Doom = @import("animations/Doom.zig"); -const Matrix = @import("animations/Matrix.zig"); -const GameOfLife = @import("animations/GameOfLife.zig"); -const DurFile = @import("animations/DurFile.zig"); -const Animation = @import("tui/Animation.zig"); -const TerminalBuffer = @import("tui/TerminalBuffer.zig"); -const Session = @import("tui/components/Session.zig"); -const Text = @import("tui/components/Text.zig"); -const InfoLine = @import("tui/components/InfoLine.zig"); -const UserList = @import("tui/components/UserList.zig"); -const Config = @import("config/Config.zig"); -const Lang = @import("config/Lang.zig"); -const OldSave = @import("config/OldSave.zig"); -const SavedUsers = @import("config/SavedUsers.zig"); -const migrator = @import("config/migrator.zig"); - -const StringList = std.ArrayListUnmanaged([]const u8); const Ini = ini.Ini; -const DisplayServer = enums.DisplayServer; -const Entry = Environment.Entry; +const ly_core = @import("ly-core"); const interop = ly_core.interop; const UidRange = ly_core.UidRange; const LogFile = ly_core.LogFile; const SharedError = ly_core.SharedError; const IniParser = ly_core.IniParser; + +const ColorMix = @import("animations/ColorMix.zig"); +const Doom = @import("animations/Doom.zig"); +const DurFile = @import("animations/DurFile.zig"); +const GameOfLife = @import("animations/GameOfLife.zig"); +const Matrix = @import("animations/Matrix.zig"); +const auth = @import("auth.zig"); +const bigclock = @import("bigclock.zig"); +const Config = @import("config/Config.zig"); +const Lang = @import("config/Lang.zig"); +const migrator = @import("config/migrator.zig"); +const OldSave = @import("config/OldSave.zig"); +const SavedUsers = @import("config/SavedUsers.zig"); +const enums = @import("enums.zig"); +const DisplayServer = enums.DisplayServer; +const Environment = @import("Environment.zig"); +const Entry = Environment.Entry; +const Animation = @import("tui/Animation.zig"); +const InfoLine = @import("tui/components/InfoLine.zig"); +const Session = @import("tui/components/Session.zig"); +const Text = @import("tui/components/Text.zig"); +const UserList = @import("tui/components/UserList.zig"); +const TerminalBuffer = @import("tui/TerminalBuffer.zig"); const termbox = TerminalBuffer.termbox; -const temporary_allocator = std.heap.page_allocator; + +const StringList = std.ArrayListUnmanaged([]const u8); const ly_version_str = "Ly version " ++ build_options.version; var session_pid: std.posix.pid_t = -1; @@ -673,7 +675,7 @@ pub fn main() !void { // Skip event polling if autologin is set, use simulated Enter key press instead if (is_autologin) { - event = termbox.tb_event{ + event = .{ .type = termbox.TB_EVENT_KEY, .key = termbox.TB_KEY_ENTER, .ch = 0, diff --git a/src/tui/Cell.zig b/src/tui/Cell.zig index 6e1ca45..4389a2f 100644 --- a/src/tui/Cell.zig +++ b/src/tui/Cell.zig @@ -1,5 +1,4 @@ const TerminalBuffer = @import("TerminalBuffer.zig"); - const termbox = TerminalBuffer.termbox; const Cell = @This(); diff --git a/src/tui/TerminalBuffer.zig b/src/tui/TerminalBuffer.zig index 61d6e74..8f72e07 100644 --- a/src/tui/TerminalBuffer.zig +++ b/src/tui/TerminalBuffer.zig @@ -1,13 +1,12 @@ const std = @import("std"); -const ly_core = @import("ly-core"); -const Cell = @import("Cell.zig"); - -pub const termbox = @import("termbox2"); - const Random = std.Random; +const ly_core = @import("ly-core"); const interop = ly_core.interop; const LogFile = ly_core.LogFile; +pub const termbox = @import("termbox2"); + +const Cell = @import("Cell.zig"); const TerminalBuffer = @This(); diff --git a/src/tui/components/InfoLine.zig b/src/tui/components/InfoLine.zig index f31fc11..9e103cf 100644 --- a/src/tui/components/InfoLine.zig +++ b/src/tui/components/InfoLine.zig @@ -1,9 +1,9 @@ const std = @import("std"); +const Allocator = std.mem.Allocator; + const TerminalBuffer = @import("../TerminalBuffer.zig"); const generic = @import("generic.zig"); -const Allocator = std.mem.Allocator; - const MessageLabel = generic.CyclableLabel(Message, Message); const InfoLine = @This(); diff --git a/src/tui/components/Session.zig b/src/tui/components/Session.zig index 3283ff0..fd2aa3f 100644 --- a/src/tui/components/Session.zig +++ b/src/tui/components/Session.zig @@ -1,13 +1,13 @@ const std = @import("std"); -const TerminalBuffer = @import("../TerminalBuffer.zig"); +const Allocator = std.mem.Allocator; + const enums = @import("../../enums.zig"); +const DisplayServer = enums.DisplayServer; const Environment = @import("../../Environment.zig"); +const TerminalBuffer = @import("../TerminalBuffer.zig"); const generic = @import("generic.zig"); const UserList = @import("UserList.zig"); -const Allocator = std.mem.Allocator; -const DisplayServer = enums.DisplayServer; - const Env = struct { environment: Environment, index: usize, diff --git a/src/tui/components/Text.zig b/src/tui/components/Text.zig index 25fff7c..4d3ba35 100644 --- a/src/tui/components/Text.zig +++ b/src/tui/components/Text.zig @@ -1,11 +1,11 @@ const std = @import("std"); -const TerminalBuffer = @import("../TerminalBuffer.zig"); - const Allocator = std.mem.Allocator; -const DynamicString = std.ArrayListUnmanaged(u8); +const TerminalBuffer = @import("../TerminalBuffer.zig"); const termbox = TerminalBuffer.termbox; +const DynamicString = std.ArrayListUnmanaged(u8); + const Text = @This(); allocator: Allocator, @@ -21,12 +21,10 @@ masked: bool, maybe_mask: ?u32, pub fn init(allocator: Allocator, buffer: *TerminalBuffer, masked: bool, maybe_mask: ?u32) Text { - const text: DynamicString = .empty; - return .{ .allocator = allocator, .buffer = buffer, - .text = text, + .text = .empty, .end = 0, .cursor = 0, .visible_start = 0, diff --git a/src/tui/components/UserList.zig b/src/tui/components/UserList.zig index 91ddc15..b8d712d 100644 --- a/src/tui/components/UserList.zig +++ b/src/tui/components/UserList.zig @@ -1,12 +1,12 @@ const std = @import("std"); +const Allocator = std.mem.Allocator; + +const SavedUsers = @import("../../config/SavedUsers.zig"); const TerminalBuffer = @import("../TerminalBuffer.zig"); const generic = @import("generic.zig"); const Session = @import("Session.zig"); -const SavedUsers = @import("../../config/SavedUsers.zig"); const StringList = std.ArrayListUnmanaged([]const u8); -const Allocator = std.mem.Allocator; - pub const User = struct { name: []const u8, session_index: *usize, diff --git a/src/tui/components/generic.zig b/src/tui/components/generic.zig index 004be2b..333ded1 100644 --- a/src/tui/components/generic.zig +++ b/src/tui/components/generic.zig @@ -1,4 +1,5 @@ const std = @import("std"); + const TerminalBuffer = @import("../TerminalBuffer.zig"); pub fn CyclableLabel(comptime ItemType: type, comptime ChangeItemType: type) type { From 8c08359e5135e549af90ef0c9170c94ad9702464 Mon Sep 17 00:00:00 2001 From: laur Date: Sun, 8 Feb 2026 13:16:25 +0100 Subject: [PATCH 040/176] custom-sessions/README: remove extraneous comma (#927) ## What are the changes about? Fix a typo. ## What existing issue does this resolve? N/A Co-authored-by: laur Reviewed-on: https://codeberg.org/fairyglade/ly/pulls/927 Co-authored-by: laur Co-committed-by: laur --- res/custom-sessions/README | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/res/custom-sessions/README b/res/custom-sessions/README index 2911923..b706118 100644 --- a/res/custom-sessions/README +++ b/res/custom-sessions/README @@ -17,7 +17,7 @@ redirected to the session log file found in Ly's configuration file. If set to true, Ly will consider the program is going to run in a TTY, and thus will not redirect standard output & error. It is optional and defaults to false. -Finally, do note that, if the Terminal value is set to true, the +Finally, do note that if the Terminal value is set to true, the XDG_SESSION_TYPE environment variable will be set to "tty". Otherwise, it will be set to "unspecified" (without quotes), which is behavior that at least systemd recognizes (see pam_systemd's man page). From bca38856b1aa68a001aca8429ce4b78778247d0c Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Sun, 8 Feb 2026 14:53:36 +0100 Subject: [PATCH 041/176] Completely refactor widget placement code Signed-off-by: AnErrupTion --- src/main.zig | 133 +++++++++++++++++--------- src/tui/Position.zig | 32 +++++++ src/tui/TerminalBuffer.zig | 85 +---------------- src/tui/components/CenteredBox.zig | 147 +++++++++++++++++++++++++++++ src/tui/components/InfoLine.zig | 44 ++++++--- src/tui/components/Session.zig | 32 +++++-- src/tui/components/Text.zig | 71 ++++++++++---- src/tui/components/UserList.zig | 33 +++++-- src/tui/components/generic.zig | 109 +++++++++++++++------ 9 files changed, 477 insertions(+), 209 deletions(-) create mode 100644 src/tui/Position.zig create mode 100644 src/tui/components/CenteredBox.zig diff --git a/src/main.zig b/src/main.zig index 692dae6..e36165c 100644 --- a/src/main.zig +++ b/src/main.zig @@ -30,6 +30,7 @@ const DisplayServer = enums.DisplayServer; const Environment = @import("Environment.zig"); const Entry = Environment.Entry; const Animation = @import("tui/Animation.zig"); +const CenteredBox = @import("tui/components/CenteredBox.zig"); const InfoLine = @import("tui/components/InfoLine.zig"); const Session = @import("tui/components/Session.zig"); const Text = @import("tui/components/Text.zig"); @@ -63,9 +64,11 @@ const UiState = struct { auth_fails: u64, update: bool, buffer: *TerminalBuffer, + labels_max_length: usize, animation_timed_out: bool, animation: *?Animation, can_draw_battery: bool, + box: *CenteredBox, info_line: *InfoLine, animate: bool, resolution_changed: bool, @@ -300,11 +303,7 @@ pub fn main() !void { .fg = config.fg, .bg = config.bg, .border_fg = config.border_fg, - .margin_box_h = config.margin_box_h, - .margin_box_v = config.margin_box_v, - .input_len = config.input_len, .full_color = config.full_color, - .labels_max_length = labels_max_length, .is_tty = true, }; var buffer = try TerminalBuffer.init(buffer_options, &log_file, random); @@ -321,7 +320,23 @@ pub fn main() !void { std.posix.sigaction(std.posix.SIG.TERM, &act, null); // Initialize components - var info_line = InfoLine.init(allocator, &buffer); + var box = CenteredBox.init( + &buffer, + config.margin_box_h, + config.margin_box_v, + (2 * config.margin_box_h) + config.input_len + 1 + labels_max_length, + 7 + (2 * config.margin_box_v), + !config.hide_borders, + config.blank_box, + config.box_title, + null, + ); + + var info_line = InfoLine.init( + allocator, + &buffer, + box.width - 2 * box.horizontal_margin, + ); defer info_line.deinit(); if (maybe_res == null) { @@ -365,10 +380,24 @@ pub fn main() !void { var login: UserList = undefined; - var session = Session.init(allocator, &buffer, &login); + var session = Session.init( + allocator, + &buffer, + &login, + box.width - 2 * box.horizontal_margin - labels_max_length - 1, + config.text_in_center, + ); defer session.deinit(); - login = try UserList.init(allocator, &buffer, usernames, &saved_users, &session); + login = try UserList.init( + allocator, + &buffer, + usernames, + &saved_users, + &session, + box.width - 2 * box.horizontal_margin - labels_max_length - 1, + config.text_in_center, + ); defer login.deinit(); addOtherEnvironment(&session, lang, .shell, null) catch |err| { @@ -430,7 +459,13 @@ pub fn main() !void { try log_file.err("sys", "no users found", .{}); } - var password = Text.init(allocator, &buffer, true, config.asterisk); + var password = Text.init( + allocator, + &buffer, + true, + config.asterisk, + box.width - 2 * box.horizontal_margin - labels_max_length - 1, + ); defer password.deinit(); var is_autologin = false; @@ -467,9 +502,11 @@ pub fn main() !void { .auth_fails = 0, .update = true, .buffer = &buffer, + .labels_max_length = labels_max_length, .animation_timed_out = false, .animation = &animation, .can_draw_battery = true, + .box = &box, .info_line = &info_line, .animate = config.animation != .none, .resolution_changed = false, @@ -512,25 +549,21 @@ pub fn main() !void { } } - // Place components on the screen - { - buffer.drawBoxCenter(!config.hide_borders, config.blank_box); + // Position components + state.box.position(TerminalBuffer.START_POSITION); + state.info_line.label.positionY(state.box.childrenPosition()); + state.session.label.positionY(state.info_line.label.childrenPosition().addY(1).addX(state.labels_max_length + 1)); + state.login.label.positionY(state.session.label.childrenPosition().addY(1)); + state.password.positionY(state.login.label.childrenPosition().addY(1)); - const coordinates = buffer.calculateComponentCoordinates(); - info_line.label.position(coordinates.start_x, coordinates.y, coordinates.full_visible_length, null); - session.label.position(coordinates.x, coordinates.y + 2, coordinates.visible_length, config.text_in_center); - login.label.position(coordinates.x, coordinates.y + 4, coordinates.visible_length, config.text_in_center); - password.position(coordinates.x, coordinates.y + 6, coordinates.visible_length); - - switch (state.active_input) { - .info_line => info_line.label.handle(null, state.insert_mode), - .session => session.label.handle(null, state.insert_mode), - .login => login.label.handle(null, state.insert_mode), - .password => password.handle(null, state.insert_mode) catch |err| { - try info_line.addMessage(lang.err_alloc, config.error_bg, config.error_fg); - try log_file.err("tui", "failed to handle password input: {s}", .{@errorName(err)}); - }, - } + switch (state.active_input) { + .info_line => info_line.label.handle(null, state.insert_mode), + .session => session.label.handle(null, state.insert_mode), + .login => login.label.handle(null, state.insert_mode), + .password => password.handle(null, state.insert_mode) catch |err| { + try info_line.addMessage(lang.err_alloc, config.error_bg, config.error_fg); + try log_file.err("tui", "failed to handle password input: {s}", .{@errorName(err)}); + }, } // Initialize the animation, if any @@ -782,7 +815,7 @@ pub fn main() !void { 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); - InfoLine.clearRendered(allocator, buffer) catch |err| { + 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)}); }; @@ -792,7 +825,7 @@ pub fn main() !void { } try info_line.addMessage(lang.authenticating, config.bg, config.fg); - InfoLine.clearRendered(allocator, buffer) catch |err| { + 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)}); }; @@ -1002,7 +1035,7 @@ fn drawUi(config: Config, lang: Lang, log_file: *LogFile, state: *UiState) !bool state.can_draw_battery = true; } - if (config.bigclock != .none and state.buffer.box_height + (bigclock.HEIGHT + 2) * 2 < state.buffer.height) { + if (config.bigclock != .none and state.box.height + (bigclock.HEIGHT + 2) * 2 < state.buffer.height) { var format_buf: [16:0]u8 = undefined; var clock_buf: [32:0]u8 = undefined; // We need the slice/c-string returned by `bufPrintZ`. @@ -1013,7 +1046,7 @@ fn drawUi(config: Config, lang: Lang, log_file: *LogFile, state: *UiState) !bool if (config.bigclock_12hr) "%P" else "", }); const xo = state.buffer.width / 2 - @min(state.buffer.width, (format.len * (bigclock.WIDTH + 1))) / 2; - const yo = (state.buffer.height - state.buffer.box_height) / 2 - bigclock.HEIGHT - 2; + const yo = (state.buffer.height - state.box.height) / 2 - bigclock.HEIGHT - 2; const clock_str = interop.timeAsString(&clock_buf, format); @@ -1024,14 +1057,14 @@ fn drawUi(config: Config, lang: Lang, log_file: *LogFile, state: *UiState) !bool } } - state.buffer.drawBoxCenter(!config.hide_borders, config.blank_box); + state.box.draw(); if (state.resolution_changed) { - const coordinates = state.buffer.calculateComponentCoordinates(); - state.info_line.label.position(coordinates.start_x, coordinates.y, coordinates.full_visible_length, null); - state.session.label.position(coordinates.x, coordinates.y + 2, coordinates.visible_length, config.text_in_center); - state.login.label.position(coordinates.x, coordinates.y + 4, coordinates.visible_length, config.text_in_center); - state.password.position(coordinates.x, coordinates.y + 6, coordinates.visible_length); + state.box.position(TerminalBuffer.START_POSITION); + state.info_line.label.positionY(state.box.childrenPosition()); + state.session.label.positionY(state.info_line.label.childrenPosition().addY(1).addX(state.labels_max_length + 1)); + state.login.label.positionY(state.session.label.childrenPosition().addY(1)); + state.password.positionY(state.login.label.childrenPosition().addY(1)); state.resolution_changed = false; } @@ -1062,11 +1095,22 @@ fn drawUi(config: Config, lang: Lang, log_file: *LogFile, state: *UiState) !bool state.buffer.drawLabel(clock_str, state.buffer.width - @min(state.buffer.width, clock_str.len) - config.edge_margin, config.edge_margin); } - const label_x = state.buffer.box_x + state.buffer.margin_box_h; - const label_y = state.buffer.box_y + state.buffer.margin_box_v; - - state.buffer.drawLabel(lang.login, label_x, label_y + 4); - state.buffer.drawLabel(lang.password, label_x, label_y + 6); + const env = state.session.label.list.items[state.session.label.current]; + state.buffer.drawLabel( + env.environment.specifier, + state.box.childrenPosition().x, + state.session.label.component_pos.y, + ); + state.buffer.drawLabel( + lang.login, + state.box.childrenPosition().x, + state.login.label.component_pos.y, + ); + state.buffer.drawLabel( + lang.password, + state.box.childrenPosition().x, + state.password.component_pos.y, + ); state.info_line.label.draw(); @@ -1122,13 +1166,8 @@ fn drawUi(config: Config, lang: Lang, log_file: *LogFile, state: *UiState) !bool } } - if (config.box_title) |title| { - state.buffer.drawConfinedLabel(title, state.buffer.box_x, state.buffer.box_y - 1, state.buffer.box_width); - } - if (config.vi_mode) { - const label_txt = if (state.insert_mode) lang.insert else lang.normal; - state.buffer.drawLabel(label_txt, state.buffer.box_x, state.buffer.box_y + state.buffer.box_height); + state.box.bottom_title = if (state.insert_mode) lang.insert else lang.normal; } if (!config.hide_keyboard_locks and state.can_get_lock_state) draw_lock_state: { diff --git a/src/tui/Position.zig b/src/tui/Position.zig new file mode 100644 index 0000000..1a61f61 --- /dev/null +++ b/src/tui/Position.zig @@ -0,0 +1,32 @@ +const Position = @This(); + +x: usize, +y: usize, + +pub fn init(x: usize, y: usize) Position { + return .{ + .x = x, + .y = y, + }; +} + +pub fn add(self: Position, other: Position) Position { + return .{ + .x = self.x + other.x, + .y = self.y + other.y, + }; +} + +pub fn addX(self: Position, x: usize) Position { + return .{ + .x = self.x + x, + .y = self.y, + }; +} + +pub fn addY(self: Position, y: usize) Position { + return .{ + .x = self.x, + .y = self.y + y, + }; +} diff --git a/src/tui/TerminalBuffer.zig b/src/tui/TerminalBuffer.zig index 8f72e07..d02a858 100644 --- a/src/tui/TerminalBuffer.zig +++ b/src/tui/TerminalBuffer.zig @@ -7,6 +7,7 @@ const LogFile = ly_core.LogFile; pub const termbox = @import("termbox2"); const Cell = @import("Cell.zig"); +const Position = @import("Position.zig"); const TerminalBuffer = @This(); @@ -14,11 +15,7 @@ pub const InitOptions = struct { fg: u32, bg: u32, border_fg: u32, - margin_box_h: u8, - margin_box_v: u8, - input_len: u8, full_color: bool, - labels_max_length: usize, is_tty: bool, }; @@ -60,6 +57,8 @@ pub const Color = struct { pub const ECOL_WHITE = 8; }; +pub const START_POSITION = Position.init(0, 0); + random: Random, width: usize, height: usize, @@ -76,13 +75,6 @@ box_chars: struct { left: u32, right: u32, }, -labels_max_length: usize, -box_x: usize, -box_y: usize, -box_width: usize, -box_height: usize, -margin_box_v: u8, -margin_box_h: u8, blank_cell: Cell, full_color: bool, termios: ?std.posix.termios, @@ -134,13 +126,6 @@ pub fn init(options: InitOptions, log_file: *LogFile, random: Random) !TerminalB .left = '|', .right = '|', }, - .labels_max_length = options.labels_max_length, - .box_x = 0, - .box_y = 0, - .box_width = (2 * options.margin_box_h) + options.input_len + 1 + options.labels_max_length, - .box_height = 7 + (2 * options.margin_box_v), - .margin_box_v = options.margin_box_v, - .margin_box_h = options.margin_box_h, .blank_cell = Cell.init(' ', options.fg, options.bg), .full_color = options.full_color, // Needed to reclaim the TTY after giving up its control @@ -216,70 +201,6 @@ pub fn cascade(self: TerminalBuffer) bool { return changed; } -pub fn drawBoxCenter(self: *TerminalBuffer, show_borders: bool, blank_box: bool) void { - if (self.width < 2 or self.height < 2) return; - const x1 = (self.width - @min(self.width - 2, self.box_width)) / 2; - const y1 = (self.height - @min(self.height - 2, self.box_height)) / 2; - const x2 = (self.width + @min(self.width, self.box_width)) / 2; - const y2 = (self.height + @min(self.height, self.box_height)) / 2; - - self.box_x = x1; - self.box_y = y1; - - if (show_borders) { - _ = termbox.tb_set_cell(@intCast(x1 - 1), @intCast(y1 - 1), self.box_chars.left_up, self.border_fg, self.bg); - _ = termbox.tb_set_cell(@intCast(x2), @intCast(y1 - 1), self.box_chars.right_up, self.border_fg, self.bg); - _ = termbox.tb_set_cell(@intCast(x1 - 1), @intCast(y2), self.box_chars.left_down, self.border_fg, self.bg); - _ = termbox.tb_set_cell(@intCast(x2), @intCast(y2), self.box_chars.right_down, self.border_fg, self.bg); - - var c1 = Cell.init(self.box_chars.top, self.border_fg, self.bg); - var c2 = Cell.init(self.box_chars.bottom, self.border_fg, self.bg); - - for (0..self.box_width) |i| { - c1.put(x1 + i, y1 - 1); - c2.put(x1 + i, y2); - } - - c1.ch = self.box_chars.left; - c2.ch = self.box_chars.right; - - for (0..self.box_height) |i| { - c1.put(x1 - 1, y1 + i); - c2.put(x2, y1 + i); - } - } - - if (blank_box) { - for (0..self.box_height) |y| { - for (0..self.box_width) |x| { - self.blank_cell.put(x1 + x, y1 + y); - } - } - } -} - -pub fn calculateComponentCoordinates(self: TerminalBuffer) struct { - start_x: usize, - x: usize, - y: usize, - full_visible_length: usize, - visible_length: usize, -} { - const start_x = self.box_x + self.margin_box_h; - const x = start_x + self.labels_max_length + 1; - const y = self.box_y + self.margin_box_v; - const full_visible_length = self.box_x + self.box_width - self.margin_box_h - start_x; - const visible_length = self.box_x + self.box_width - self.margin_box_h - x; - - return .{ - .start_x = start_x, - .x = x, - .y = y, - .full_visible_length = full_visible_length, - .visible_length = visible_length, - }; -} - pub fn drawLabel(self: TerminalBuffer, text: []const u8, x: usize, y: usize) void { drawColorLabel(text, x, y, self.fg, self.bg); } diff --git a/src/tui/components/CenteredBox.zig b/src/tui/components/CenteredBox.zig new file mode 100644 index 0000000..da0028b --- /dev/null +++ b/src/tui/components/CenteredBox.zig @@ -0,0 +1,147 @@ +const std = @import("std"); +const Allocator = std.mem.Allocator; + +const Cell = @import("../Cell.zig"); +const TerminalBuffer = @import("../TerminalBuffer.zig"); +const Position = @import("../Position.zig"); +const termbox = TerminalBuffer.termbox; + +const CenteredBox = @This(); + +buffer: *TerminalBuffer, +horizontal_margin: usize, +vertical_margin: usize, +width: usize, +height: usize, +show_borders: bool, +blank_box: bool, +top_title: ?[]const u8, +bottom_title: ?[]const u8, +left_pos: Position, +right_pos: Position, +children_pos: Position, + +pub fn init( + buffer: *TerminalBuffer, + horizontal_margin: usize, + vertical_margin: usize, + width: usize, + height: usize, + show_borders: bool, + blank_box: bool, + top_title: ?[]const u8, + bottom_title: ?[]const u8, +) CenteredBox { + return .{ + .buffer = buffer, + .horizontal_margin = horizontal_margin, + .vertical_margin = vertical_margin, + .width = width, + .height = height, + .show_borders = show_borders, + .blank_box = blank_box, + .top_title = top_title, + .bottom_title = bottom_title, + .left_pos = TerminalBuffer.START_POSITION, + .right_pos = TerminalBuffer.START_POSITION, + .children_pos = TerminalBuffer.START_POSITION, + }; +} + +pub fn position(self: *CenteredBox, original_pos: Position) void { + if (self.buffer.width < 2 or self.buffer.height < 2) return; + + self.left_pos = Position.init( + (self.buffer.width - @min(self.buffer.width - 2, self.width)) / 2, + (self.buffer.height - @min(self.buffer.height - 2, self.height)) / 2, + ).add(original_pos); + + self.right_pos = Position.init( + (self.buffer.width + @min(self.buffer.width, self.width)) / 2, + (self.buffer.height + @min(self.buffer.height, self.height)) / 2, + ).add(original_pos); + + self.children_pos = Position.init( + self.left_pos.x + self.horizontal_margin, + self.left_pos.y + self.vertical_margin, + ).add(original_pos); +} + +pub fn childrenPosition(self: CenteredBox) Position { + return self.children_pos; +} + +pub fn draw(self: CenteredBox) void { + if (self.show_borders) { + _ = termbox.tb_set_cell( + @intCast(self.left_pos.x - 1), + @intCast(self.left_pos.y - 1), + self.buffer.box_chars.left_up, + self.buffer.border_fg, + self.buffer.bg, + ); + _ = termbox.tb_set_cell( + @intCast(self.right_pos.x), + @intCast(self.left_pos.y - 1), + self.buffer.box_chars.right_up, + self.buffer.border_fg, + self.buffer.bg, + ); + _ = termbox.tb_set_cell( + @intCast(self.left_pos.x - 1), + @intCast(self.right_pos.y), + self.buffer.box_chars.left_down, + self.buffer.border_fg, + self.buffer.bg, + ); + _ = termbox.tb_set_cell( + @intCast(self.right_pos.x), + @intCast(self.right_pos.y), + self.buffer.box_chars.right_down, + self.buffer.border_fg, + self.buffer.bg, + ); + + var c1 = Cell.init(self.buffer.box_chars.top, self.buffer.border_fg, self.buffer.bg); + var c2 = Cell.init(self.buffer.box_chars.bottom, self.buffer.border_fg, self.buffer.bg); + + for (0..self.width) |i| { + c1.put(self.left_pos.x + i, self.left_pos.y - 1); + c2.put(self.left_pos.x + i, self.right_pos.y); + } + + c1.ch = self.buffer.box_chars.left; + c2.ch = self.buffer.box_chars.right; + + for (0..self.height) |i| { + c1.put(self.left_pos.x - 1, self.left_pos.y + i); + c2.put(self.right_pos.x, self.left_pos.y + i); + } + } + + if (self.blank_box) { + for (0..self.height) |y| { + for (0..self.width) |x| { + self.buffer.blank_cell.put(self.left_pos.x + x, self.left_pos.y + y); + } + } + } + + if (self.top_title) |title| { + self.buffer.drawConfinedLabel( + title, + self.left_pos.x, + self.left_pos.y - 1, + self.width, + ); + } + + if (self.bottom_title) |title| { + self.buffer.drawConfinedLabel( + title, + self.left_pos.x, + self.left_pos.y + self.height, + self.width, + ); + } +} diff --git a/src/tui/components/InfoLine.zig b/src/tui/components/InfoLine.zig index 9e103cf..7e85c0b 100644 --- a/src/tui/components/InfoLine.zig +++ b/src/tui/components/InfoLine.zig @@ -17,9 +17,21 @@ const Message = struct { label: MessageLabel, -pub fn init(allocator: Allocator, buffer: *TerminalBuffer) InfoLine { +pub fn init( + allocator: Allocator, + buffer: *TerminalBuffer, + width: usize, +) InfoLine { return .{ - .label = MessageLabel.init(allocator, buffer, drawItem, null, null), + .label = MessageLabel.init( + allocator, + buffer, + drawItem, + null, + null, + width, + true, + ), }; } @@ -38,23 +50,31 @@ pub fn addMessage(self: *InfoLine, text: []const u8, bg: u32, fg: u32) !void { }); } -pub fn clearRendered(allocator: Allocator, buffer: TerminalBuffer) !void { +pub fn clearRendered(self: InfoLine, allocator: Allocator) !void { // Draw over the area - const y = buffer.box_y + buffer.margin_box_v; - const spaces = try allocator.alloc(u8, buffer.box_width); + const spaces = try allocator.alloc(u8, self.label.width - 2); defer allocator.free(spaces); @memset(spaces, ' '); - buffer.drawLabel(spaces, buffer.box_x, y); + self.label.buffer.drawLabel( + spaces, + self.label.component_pos.x + 2, + self.label.component_pos.y, + ); } -fn drawItem(label: *MessageLabel, message: Message, _: usize, _: usize) bool { - if (message.width == 0 or label.buffer.box_width <= message.width) return false; +fn drawItem(label: *MessageLabel, message: Message, x: usize, y: usize, width: usize) void { + if (message.width == 0 or width <= message.width) return; - const x = label.buffer.box_x + ((label.buffer.box_width - message.width) / 2); - label.first_char_x = x + message.width; + const x_offset = if (label.text_in_center) (width - message.width) / 2 else 0; - TerminalBuffer.drawColorLabel(message.text, x, label.y, message.fg, message.bg); - return true; + label.item_width = message.width + x_offset; + TerminalBuffer.drawColorLabel( + message.text, + x + x_offset, + y, + message.fg, + message.bg, + ); } diff --git a/src/tui/components/Session.zig b/src/tui/components/Session.zig index fd2aa3f..baa15ed 100644 --- a/src/tui/components/Session.zig +++ b/src/tui/components/Session.zig @@ -19,9 +19,23 @@ const Session = @This(); label: EnvironmentLabel, user_list: *UserList, -pub fn init(allocator: Allocator, buffer: *TerminalBuffer, user_list: *UserList) Session { +pub fn init( + allocator: Allocator, + buffer: *TerminalBuffer, + user_list: *UserList, + width: usize, + text_in_center: bool, +) Session { return .{ - .label = EnvironmentLabel.init(allocator, buffer, drawItem, sessionChanged, user_list), + .label = EnvironmentLabel.init( + allocator, + buffer, + drawItem, + sessionChanged, + user_list, + width, + text_in_center, + ), .user_list = user_list, }; } @@ -55,14 +69,12 @@ fn sessionChanged(env: Env, maybe_user_list: ?*UserList) void { } } -fn drawItem(label: *EnvironmentLabel, env: Env, x: usize, y: usize) bool { - const length = @min(env.environment.name.len, label.visible_length - 3); - if (length == 0) return false; +fn drawItem(label: *EnvironmentLabel, env: Env, x: usize, y: usize, width: usize) void { + const length = @min(env.environment.name.len, width - 3); + if (length == 0) return; - const nx = if (label.text_in_center) (label.x + (label.visible_length - env.environment.name.len) / 2) else (label.x + 2); - label.first_char_x = nx + env.environment.name.len; + const x_offset = if (label.text_in_center) (width - length) / 2 else 0; - label.buffer.drawLabel(env.environment.specifier, x, y); - label.buffer.drawLabel(env.environment.name, nx, label.y); - return true; + label.item_width = length + x_offset; + label.buffer.drawLabel(env.environment.name, x + x_offset, y); } diff --git a/src/tui/components/Text.zig b/src/tui/components/Text.zig index 4d3ba35..c31bcb4 100644 --- a/src/tui/components/Text.zig +++ b/src/tui/components/Text.zig @@ -2,6 +2,7 @@ const std = @import("std"); const Allocator = std.mem.Allocator; const TerminalBuffer = @import("../TerminalBuffer.zig"); +const Position = @import("../Position.zig"); const termbox = TerminalBuffer.termbox; const DynamicString = std.ArrayListUnmanaged(u8); @@ -14,13 +15,19 @@ text: DynamicString, end: usize, cursor: usize, visible_start: usize, -visible_length: usize, -x: usize, -y: usize, +width: usize, +component_pos: Position, +children_pos: Position, masked: bool, maybe_mask: ?u32, -pub fn init(allocator: Allocator, buffer: *TerminalBuffer, masked: bool, maybe_mask: ?u32) Text { +pub fn init( + allocator: Allocator, + buffer: *TerminalBuffer, + masked: bool, + maybe_mask: ?u32, + width: usize, +) Text { return .{ .allocator = allocator, .buffer = buffer, @@ -28,9 +35,9 @@ pub fn init(allocator: Allocator, buffer: *TerminalBuffer, masked: bool, maybe_m .end = 0, .cursor = 0, .visible_start = 0, - .visible_length = 0, - .x = 0, - .y = 0, + .width = width, + .component_pos = TerminalBuffer.START_POSITION, + .children_pos = TerminalBuffer.START_POSITION, .masked = masked, .maybe_mask = maybe_mask, }; @@ -40,10 +47,26 @@ pub fn deinit(self: *Text) void { self.text.deinit(self.allocator); } -pub fn position(self: *Text, x: usize, y: usize, visible_length: usize) void { - self.x = x; - self.y = y; - self.visible_length = visible_length; +pub fn positionX(self: *Text, original_pos: Position) void { + self.component_pos = original_pos; + self.children_pos = original_pos.addX(self.width); +} + +pub fn positionY(self: *Text, original_pos: Position) void { + self.component_pos = original_pos; + self.children_pos = original_pos.addY(1); +} + +pub fn positionXY(self: *Text, original_pos: Position) void { + self.component_pos = original_pos; + self.children_pos = Position.init( + self.width, + 1, + ).add(original_pos); +} + +pub fn childrenPosition(self: Text) Position { + return self.children_pos; } pub fn handle(self: *Text, maybe_event: ?*termbox.tb_event, insert_mode: bool) !void { @@ -79,36 +102,44 @@ pub fn handle(self: *Text, maybe_event: ?*termbox.tb_event, insert_mode: bool) ! } if (self.masked and self.maybe_mask == null) { - _ = termbox.tb_set_cursor(@intCast(self.x), @intCast(self.y)); + _ = termbox.tb_set_cursor(@intCast(self.component_pos.x), @intCast(self.component_pos.y)); return; } - _ = termbox.tb_set_cursor(@intCast(self.x + (self.cursor - self.visible_start)), @intCast(self.y)); + _ = termbox.tb_set_cursor( + @intCast(self.component_pos.x + (self.cursor - self.visible_start)), + @intCast(self.component_pos.y), + ); } pub fn draw(self: Text) void { if (self.masked) { if (self.maybe_mask) |mask| { - const length = @min(self.text.items.len, self.visible_length - 1); + const length = @min(self.text.items.len, self.width - 1); if (length == 0) return; - self.buffer.drawCharMultiple(mask, self.x, self.y, length); + self.buffer.drawCharMultiple( + mask, + self.component_pos.x, + self.component_pos.y, + length, + ); } return; } - const length = @min(self.text.items.len, self.visible_length); + const length = @min(self.text.items.len, self.width); if (length == 0) return; const visible_slice = vs: { - if (self.text.items.len > self.visible_length and self.cursor < self.text.items.len) { - break :vs self.text.items[self.visible_start..(self.visible_length + self.visible_start)]; + if (self.text.items.len > self.width and self.cursor < self.text.items.len) { + break :vs self.text.items[self.visible_start..(self.width + self.visible_start)]; } else { break :vs self.text.items[self.visible_start..]; } }; - self.buffer.drawLabel(visible_slice, self.x, self.y); + self.buffer.drawLabel(visible_slice, self.component_pos.x, self.component_pos.y); } pub fn clear(self: *Text) void { @@ -127,7 +158,7 @@ fn goLeft(self: *Text) void { fn goRight(self: *Text) void { if (self.cursor >= self.end) return; - if (self.cursor - self.visible_start == self.visible_length - 1) self.visible_start += 1; + if (self.cursor - self.visible_start == self.width - 1) self.visible_start += 1; self.cursor += 1; } diff --git a/src/tui/components/UserList.zig b/src/tui/components/UserList.zig index b8d712d..9433a0e 100644 --- a/src/tui/components/UserList.zig +++ b/src/tui/components/UserList.zig @@ -19,9 +19,25 @@ const UserList = @This(); label: UserLabel, -pub fn init(allocator: Allocator, buffer: *TerminalBuffer, usernames: StringList, saved_users: *SavedUsers, session: *Session) !UserList { +pub fn init( + allocator: Allocator, + buffer: *TerminalBuffer, + usernames: StringList, + saved_users: *SavedUsers, + session: *Session, + width: usize, + text_in_center: bool, +) !UserList { var userList = UserList{ - .label = UserLabel.init(allocator, buffer, drawItem, usernameChanged, session), + .label = UserLabel.init( + allocator, + buffer, + drawItem, + usernameChanged, + session, + width, + text_in_center, + ), }; for (usernames.items) |username| { @@ -75,13 +91,12 @@ fn usernameChanged(user: User, maybe_session: ?*Session) void { } } -fn drawItem(label: *UserLabel, user: User, _: usize, _: usize) bool { - const length = @min(user.name.len, label.visible_length - 3); - if (length == 0) return false; +fn drawItem(label: *UserLabel, user: User, x: usize, y: usize, width: usize) void { + const length = @min(user.name.len, width - 3); + if (length == 0) return; - const x = if (label.text_in_center) (label.x + (label.visible_length - user.name.len) / 2) else (label.x + 2); - label.first_char_x = x + user.name.len; + const x_offset = if (label.text_in_center) (width - length) / 2 else 0; - label.buffer.drawLabel(user.name, x, label.y); - return true; + label.item_width = length + x_offset; + label.buffer.drawLabel(user.name, x + x_offset, y); } diff --git a/src/tui/components/generic.zig b/src/tui/components/generic.zig index 333ded1..479b14f 100644 --- a/src/tui/components/generic.zig +++ b/src/tui/components/generic.zig @@ -1,12 +1,13 @@ const std = @import("std"); const TerminalBuffer = @import("../TerminalBuffer.zig"); +const Position = @import("../Position.zig"); pub fn CyclableLabel(comptime ItemType: type, comptime ChangeItemType: type) type { return struct { const Allocator = std.mem.Allocator; const ItemList = std.ArrayListUnmanaged(ItemType); - const DrawItemFn = *const fn (*Self, ItemType, usize, usize) bool; + const DrawItemFn = *const fn (*Self, ItemType, usize, usize, usize) void; const ChangeItemFn = *const fn (ItemType, ?ChangeItemType) void; const termbox = TerminalBuffer.termbox; @@ -17,26 +18,34 @@ pub fn CyclableLabel(comptime ItemType: type, comptime ChangeItemType: type) typ buffer: *TerminalBuffer, list: ItemList, current: usize, - visible_length: usize, - x: usize, - y: usize, - first_char_x: usize, + width: usize, + component_pos: Position, + children_pos: Position, text_in_center: bool, + item_width: usize, draw_item_fn: DrawItemFn, change_item_fn: ?ChangeItemFn, change_item_arg: ?ChangeItemType, - pub fn init(allocator: Allocator, buffer: *TerminalBuffer, draw_item_fn: DrawItemFn, change_item_fn: ?ChangeItemFn, change_item_arg: ?ChangeItemType) Self { + pub fn init( + allocator: Allocator, + buffer: *TerminalBuffer, + draw_item_fn: DrawItemFn, + change_item_fn: ?ChangeItemFn, + change_item_arg: ?ChangeItemType, + width: usize, + text_in_center: bool, + ) Self { return .{ .allocator = allocator, .buffer = buffer, .list = .empty, .current = 0, - .visible_length = 0, - .x = 0, - .y = 0, - .first_char_x = 0, - .text_in_center = false, + .width = width, + .component_pos = TerminalBuffer.START_POSITION, + .children_pos = TerminalBuffer.START_POSITION, + .text_in_center = text_in_center, + .item_width = 0, .draw_item_fn = draw_item_fn, .change_item_fn = change_item_fn, .change_item_arg = change_item_arg, @@ -47,14 +56,29 @@ pub fn CyclableLabel(comptime ItemType: type, comptime ChangeItemType: type) typ self.list.deinit(self.allocator); } - pub fn position(self: *Self, x: usize, y: usize, visible_length: usize, text_in_center: ?bool) void { - self.x = x; - self.y = y; - self.visible_length = visible_length; - self.first_char_x = x + 2; - if (text_in_center) |value| { - self.text_in_center = value; - } + pub fn positionX(self: *Self, original_pos: Position) void { + self.component_pos = original_pos; + self.item_width = self.component_pos.x + 2; + self.children_pos = original_pos.addX(self.width); + } + + pub fn positionY(self: *Self, original_pos: Position) void { + self.component_pos = original_pos; + self.item_width = self.component_pos.x + 2; + self.children_pos = original_pos.addY(1); + } + + pub fn positionXY(self: *Self, original_pos: Position) void { + self.component_pos = original_pos; + self.item_width = self.component_pos.x + 2; + self.children_pos = Position.init( + self.width, + 1, + ).add(original_pos); + } + + pub fn childrenPosition(self: Self) Position { + return self.children_pos; } pub fn addItem(self: *Self, item: ItemType) !void { @@ -81,28 +105,51 @@ pub fn CyclableLabel(comptime ItemType: type, comptime ChangeItemType: type) typ } } - _ = termbox.tb_set_cursor(@intCast(self.first_char_x), @intCast(self.y)); + _ = termbox.tb_set_cursor( + @intCast(self.component_pos.x + self.item_width + 2), + @intCast(self.component_pos.y), + ); } pub fn draw(self: *Self) void { if (self.list.items.len == 0) return; + _ = termbox.tb_set_cell( + @intCast(self.component_pos.x), + @intCast(self.component_pos.y), + '<', + self.buffer.fg, + self.buffer.bg, + ); + _ = termbox.tb_set_cell( + @intCast(self.component_pos.x + self.width - 1), + @intCast(self.component_pos.y), + '>', + self.buffer.fg, + self.buffer.bg, + ); + const current_item = self.list.items[self.current]; - const x = self.buffer.box_x + self.buffer.margin_box_h; - const y = self.buffer.box_y + self.buffer.margin_box_v + 2; + const x = self.component_pos.x + 2; + const y = self.component_pos.y; + const width = self.width - 2; - const continue_drawing = @call(.auto, self.draw_item_fn, .{ self, current_item, x, y }); - if (!continue_drawing) return; - - _ = termbox.tb_set_cell(@intCast(self.x), @intCast(self.y), '<', self.buffer.fg, self.buffer.bg); - _ = termbox.tb_set_cell(@intCast(self.x + self.visible_length - 1), @intCast(self.y), '>', self.buffer.fg, self.buffer.bg); + @call( + .auto, + self.draw_item_fn, + .{ self, current_item, x, y, width }, + ); } fn goLeft(self: *Self) void { self.current = if (self.current == 0) self.list.items.len - 1 else self.current - 1; if (self.change_item_fn) |change_item_fn| { - @call(.auto, change_item_fn, .{ self.list.items[self.current], self.change_item_arg }); + @call( + .auto, + change_item_fn, + .{ self.list.items[self.current], self.change_item_arg }, + ); } } @@ -110,7 +157,11 @@ pub fn CyclableLabel(comptime ItemType: type, comptime ChangeItemType: type) typ self.current = if (self.current == self.list.items.len - 1) 0 else self.current + 1; if (self.change_item_fn) |change_item_fn| { - @call(.auto, change_item_fn, .{ self.list.items[self.current], self.change_item_arg }); + @call( + .auto, + change_item_fn, + .{ self.list.items[self.current], self.change_item_arg }, + ); } } }; From 7bbdebe58b48e14690217042161ccc48db7ad4c5 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Sun, 8 Feb 2026 15:09:55 +0100 Subject: [PATCH 042/176] Fix center offset error Signed-off-by: AnErrupTion --- src/tui/components/InfoLine.zig | 2 +- src/tui/components/Session.zig | 2 +- src/tui/components/UserList.zig | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/tui/components/InfoLine.zig b/src/tui/components/InfoLine.zig index 7e85c0b..6248391 100644 --- a/src/tui/components/InfoLine.zig +++ b/src/tui/components/InfoLine.zig @@ -67,7 +67,7 @@ pub fn clearRendered(self: InfoLine, allocator: Allocator) !void { fn drawItem(label: *MessageLabel, message: Message, x: usize, y: usize, width: usize) void { if (message.width == 0 or width <= message.width) return; - const x_offset = if (label.text_in_center) (width - message.width) / 2 else 0; + const x_offset = if (label.text_in_center) (width - message.width - 1) / 2 else 0; label.item_width = message.width + x_offset; TerminalBuffer.drawColorLabel( diff --git a/src/tui/components/Session.zig b/src/tui/components/Session.zig index baa15ed..2c2ce62 100644 --- a/src/tui/components/Session.zig +++ b/src/tui/components/Session.zig @@ -73,7 +73,7 @@ fn drawItem(label: *EnvironmentLabel, env: Env, x: usize, y: usize, width: usize const length = @min(env.environment.name.len, width - 3); if (length == 0) return; - const x_offset = if (label.text_in_center) (width - length) / 2 else 0; + const x_offset = if (label.text_in_center) (width - length - 1) / 2 else 0; label.item_width = length + x_offset; label.buffer.drawLabel(env.environment.name, x + x_offset, y); diff --git a/src/tui/components/UserList.zig b/src/tui/components/UserList.zig index 9433a0e..26eee4c 100644 --- a/src/tui/components/UserList.zig +++ b/src/tui/components/UserList.zig @@ -95,7 +95,7 @@ fn drawItem(label: *UserLabel, user: User, x: usize, y: usize, width: usize) voi const length = @min(user.name.len, width - 3); if (length == 0) return; - const x_offset = if (label.text_in_center) (width - length) / 2 else 0; + const x_offset = if (label.text_in_center) (width - length - 1) / 2 else 0; label.item_width = length + x_offset; label.buffer.drawLabel(user.name, x + x_offset, y); From f22593f828bda35854545207fbc5a1f771ba8bb1 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Sun, 8 Feb 2026 17:40:50 +0100 Subject: [PATCH 043/176] Add Label component & make colors custom This commit also makes Ly more resilient to (impossible) screen resolutions. Signed-off-by: AnErrupTion --- src/main.zig | 409 +++++++++++++++++++++-------- src/tui/Position.zig | 189 +++++++++++++ src/tui/TerminalBuffer.zig | 34 ++- src/tui/components/CenteredBox.zig | 42 +-- src/tui/components/InfoLine.zig | 17 +- src/tui/components/Label.zig | 109 ++++++++ src/tui/components/Session.zig | 19 +- src/tui/components/Text.zig | 20 +- src/tui/components/UserList.zig | 25 +- src/tui/components/generic.zig | 27 +- 10 files changed, 737 insertions(+), 154 deletions(-) create mode 100644 src/tui/components/Label.zig diff --git a/src/main.zig b/src/main.zig index e36165c..3f8042a 100644 --- a/src/main.zig +++ b/src/main.zig @@ -30,8 +30,10 @@ const DisplayServer = enums.DisplayServer; const Environment = @import("Environment.zig"); const Entry = Environment.Entry; const Animation = @import("tui/Animation.zig"); +const Position = @import("tui/Position.zig"); const CenteredBox = @import("tui/components/CenteredBox.zig"); const InfoLine = @import("tui/components/InfoLine.zig"); +const Label = @import("tui/components/Label.zig"); const Session = @import("tui/components/Session.zig"); const Text = @import("tui/components/Text.zig"); const UserList = @import("tui/components/UserList.zig"); @@ -68,6 +70,20 @@ const UiState = struct { animation_timed_out: bool, animation: *?Animation, can_draw_battery: bool, + shutdown_label: *Label, + restart_label: *Label, + sleep_label: *Label, + hibernate_label: *Label, + brightness_down_label: *Label, + brightness_up_label: *Label, + numlock_label: *Label, + capslock_label: *Label, + battery_label: *Label, + clock_label: *Label, + session_specifier_label: *Label, + login_label: *Label, + password_label: *Label, + version_label: *Label, box: *CenteredBox, info_line: *InfoLine, animate: bool, @@ -85,6 +101,9 @@ const UiState = struct { brightness_down_len: u8, brightness_up_len: u8, can_get_lock_state: bool, + edge_margin: Position, + hide_key_hints: bool, + uses_clock: bool, }; pub fn main() !void { @@ -320,6 +339,127 @@ pub fn main() !void { std.posix.sigaction(std.posix.SIG.TERM, &act, null); // Initialize components + var shutdown_label = Label.init( + "", + null, + buffer.fg, + buffer.bg, + ); + defer shutdown_label.deinit(allocator); + + var restart_label = Label.init( + "", + null, + buffer.fg, + buffer.bg, + ); + defer restart_label.deinit(allocator); + + var sleep_label = Label.init( + "", + null, + buffer.fg, + buffer.bg, + ); + defer sleep_label.deinit(allocator); + + var hibernate_label = Label.init( + "", + null, + buffer.fg, + buffer.bg, + ); + defer hibernate_label.deinit(allocator); + + var brightness_down_label = Label.init( + "", + null, + buffer.fg, + buffer.bg, + ); + defer brightness_down_label.deinit(allocator); + + var brightness_up_label = Label.init( + "", + null, + buffer.fg, + buffer.bg, + ); + defer brightness_up_label.deinit(allocator); + + if (!config.hide_key_hints) { + try shutdown_label.setTextAlloc( + allocator, + "{s} {s}", + .{ config.shutdown_key, lang.shutdown }, + ); + try restart_label.setTextAlloc( + allocator, + "{s} {s}", + .{ config.restart_key, lang.restart }, + ); + if (config.sleep_cmd != null) { + try sleep_label.setTextAlloc( + allocator, + "{s} {s}", + .{ config.sleep_key, lang.sleep }, + ); + } + if (config.hibernate_cmd != null) { + try hibernate_label.setTextAlloc( + allocator, + "{s} {s}", + .{ config.hibernate_key, lang.hibernate }, + ); + } + if (config.brightness_down_key) |key| { + try brightness_down_label.setTextAlloc( + allocator, + "{s} {s}", + .{ key, lang.brightness_down }, + ); + } + if (config.brightness_up_key) |key| { + try brightness_up_label.setTextAlloc( + allocator, + "{s} {s}", + .{ key, lang.brightness_up }, + ); + } + } + + var numlock_label = Label.init( + lang.numlock, + null, + buffer.fg, + buffer.bg, + ); + defer numlock_label.deinit(null); + + var capslock_label = Label.init( + lang.capslock, + null, + buffer.fg, + buffer.bg, + ); + defer capslock_label.deinit(null); + + var battery_label = Label.init( + "", + null, + buffer.fg, + buffer.bg, + ); + defer battery_label.deinit(null); + + var clock_label = Label.init( + "", + null, + buffer.fg, + buffer.bg, + ); + defer clock_label.deinit(null); + var box = CenteredBox.init( &buffer, config.margin_box_h, @@ -330,12 +470,17 @@ pub fn main() !void { config.blank_box, config.box_title, null, + buffer.border_fg, + buffer.fg, + buffer.bg, ); var info_line = InfoLine.init( allocator, &buffer, box.width - 2 * box.horizontal_margin, + buffer.fg, + buffer.bg, ); defer info_line.deinit(); @@ -380,15 +525,33 @@ pub fn main() !void { var login: UserList = undefined; + var session_specifier_label = Label.init( + "", + null, + buffer.fg, + buffer.bg, + ); + defer session_specifier_label.deinit(null); + var session = Session.init( allocator, &buffer, &login, box.width - 2 * box.horizontal_margin - labels_max_length - 1, config.text_in_center, + buffer.fg, + buffer.bg, ); defer session.deinit(); + var login_label = Label.init( + lang.login, + null, + buffer.fg, + buffer.bg, + ); + defer login_label.deinit(null); + login = try UserList.init( allocator, &buffer, @@ -397,6 +560,8 @@ pub fn main() !void { &session, box.width - 2 * box.horizontal_margin - labels_max_length - 1, config.text_in_center, + buffer.fg, + buffer.bg, ); defer login.deinit(); @@ -459,15 +624,33 @@ pub fn main() !void { try log_file.err("sys", "no users found", .{}); } + var password_label = Label.init( + lang.password, + null, + buffer.fg, + buffer.bg, + ); + defer password_label.deinit(null); + var password = Text.init( allocator, &buffer, true, config.asterisk, box.width - 2 * box.horizontal_margin - labels_max_length - 1, + buffer.fg, + buffer.bg, ); defer password.deinit(); + var version_label = Label.init( + ly_version_str, + null, + buffer.fg, + buffer.bg, + ); + defer version_label.deinit(null); + var is_autologin = false; check_autologin: { @@ -506,6 +689,20 @@ pub fn main() !void { .animation_timed_out = false, .animation = &animation, .can_draw_battery = true, + .shutdown_label = &shutdown_label, + .restart_label = &restart_label, + .sleep_label = &sleep_label, + .hibernate_label = &hibernate_label, + .brightness_down_label = &brightness_down_label, + .brightness_up_label = &brightness_up_label, + .numlock_label = &numlock_label, + .capslock_label = &capslock_label, + .battery_label = &battery_label, + .clock_label = &clock_label, + .session_specifier_label = &session_specifier_label, + .login_label = &login_label, + .password_label = &password_label, + .version_label = &version_label, .box = &box, .info_line = &info_line, .animate = config.animation != .none, @@ -523,6 +720,12 @@ pub fn main() !void { .brightness_down_len = try TerminalBuffer.strWidth(lang.brightness_down), .brightness_up_len = try TerminalBuffer.strWidth(lang.brightness_up), .can_get_lock_state = true, + .edge_margin = Position.init( + config.edge_margin, + config.edge_margin, + ), + .hide_key_hints = config.hide_key_hints, + .uses_clock = config.clock != null, }; // Load last saved username and desktop selection, if any @@ -550,17 +753,13 @@ pub fn main() !void { } // Position components - state.box.position(TerminalBuffer.START_POSITION); - state.info_line.label.positionY(state.box.childrenPosition()); - state.session.label.positionY(state.info_line.label.childrenPosition().addY(1).addX(state.labels_max_length + 1)); - state.login.label.positionY(state.session.label.childrenPosition().addY(1)); - state.password.positionY(state.login.label.childrenPosition().addY(1)); + positionComponents(&state); switch (state.active_input) { - .info_line => info_line.label.handle(null, state.insert_mode), - .session => session.label.handle(null, state.insert_mode), - .login => login.label.handle(null, state.insert_mode), - .password => password.handle(null, state.insert_mode) catch |err| { + .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 info_line.addMessage(lang.err_alloc, config.error_bg, config.error_fg); try log_file.err("tui", "failed to handle password input: {s}", .{@errorName(err)}); }, @@ -1006,13 +1205,9 @@ fn drawUi(config: Config, lang: Lang, log_file: *LogFile, state: *UiState) !bool try TerminalBuffer.clearScreenStatic(false); - var length: usize = config.edge_margin; - if (!state.animation_timed_out) if (state.animation.*) |*a| a.draw(); - if (!config.hide_version_string) { - state.buffer.drawLabel(ly_version_str, config.edge_margin, state.buffer.height - 1 - config.edge_margin); - } + if (!config.hide_version_string) state.version_label.draw(); if (config.battery_id) |id| draw_battery: { if (!state.can_draw_battery) break :draw_battery; @@ -1025,14 +1220,12 @@ fn drawUi(config: Config, lang: Lang, log_file: *LogFile, state: *UiState) !bool }; var battery_buf: [16:0]u8 = undefined; - const battery_str = std.fmt.bufPrintZ(&battery_buf, "BAT: {d}%", .{battery_percentage}) catch break :draw_battery; - - var battery_y: usize = config.edge_margin; - if (!config.hide_key_hints) { - battery_y += 1; - } - state.buffer.drawLabel(battery_str, config.edge_margin, battery_y); - state.can_draw_battery = true; + state.battery_label.setTextBuf( + &battery_buf, + "BAT: {d}%", + .{battery_percentage}, + ) catch break :draw_battery; + state.battery_label.draw(); } if (config.bigclock != .none and state.box.height + (bigclock.HEIGHT + 2) * 2 < state.buffer.height) { @@ -1060,12 +1253,7 @@ fn drawUi(config: Config, lang: Lang, log_file: *LogFile, state: *UiState) !bool state.box.draw(); if (state.resolution_changed) { - state.box.position(TerminalBuffer.START_POSITION); - state.info_line.label.positionY(state.box.childrenPosition()); - state.session.label.positionY(state.info_line.label.childrenPosition().addY(1).addX(state.labels_max_length + 1)); - state.login.label.positionY(state.session.label.childrenPosition().addY(1)); - state.password.positionY(state.login.label.childrenPosition().addY(1)); - + positionComponents(state); state.resolution_changed = false; } @@ -1092,78 +1280,25 @@ fn drawUi(config: Config, lang: Lang, log_file: *LogFile, state: *UiState) !bool break :draw_clock; } - state.buffer.drawLabel(clock_str, state.buffer.width - @min(state.buffer.width, clock_str.len) - config.edge_margin, config.edge_margin); + state.clock_label.setText(clock_str); + state.clock_label.draw(); } const env = state.session.label.list.items[state.session.label.current]; - state.buffer.drawLabel( - env.environment.specifier, - state.box.childrenPosition().x, - state.session.label.component_pos.y, - ); - state.buffer.drawLabel( - lang.login, - state.box.childrenPosition().x, - state.login.label.component_pos.y, - ); - state.buffer.drawLabel( - lang.password, - state.box.childrenPosition().x, - state.password.component_pos.y, - ); + state.session_specifier_label.setText(env.environment.specifier); + state.session_specifier_label.draw(); + state.login_label.draw(); + state.password_label.draw(); state.info_line.label.draw(); if (!config.hide_key_hints) { - state.buffer.drawLabel(config.shutdown_key, length, config.edge_margin); - length += config.shutdown_key.len + 1; - state.buffer.drawLabel(" ", length - 1, config.edge_margin); - - state.buffer.drawLabel(lang.shutdown, length, config.edge_margin); - length += state.shutdown_len + 1; - - state.buffer.drawLabel(config.restart_key, length, config.edge_margin); - length += config.restart_key.len + 1; - state.buffer.drawLabel(" ", length - 1, config.edge_margin); - - state.buffer.drawLabel(lang.restart, length, config.edge_margin); - length += state.restart_len + 1; - - if (config.sleep_cmd != null) { - state.buffer.drawLabel(config.sleep_key, length, config.edge_margin); - length += config.sleep_key.len + 1; - state.buffer.drawLabel(" ", length - 1, config.edge_margin); - - state.buffer.drawLabel(lang.sleep, length, config.edge_margin); - length += state.sleep_len + 1; - } - - if (config.hibernate_cmd != null) { - state.buffer.drawLabel(config.hibernate_key, length, config.edge_margin); - length += config.hibernate_key.len + 1; - state.buffer.drawLabel(" ", length - 1, config.edge_margin); - - state.buffer.drawLabel(lang.hibernate, length, config.edge_margin); - length += state.hibernate_len + 1; - } - - if (config.brightness_down_key) |key| { - state.buffer.drawLabel(key, length, config.edge_margin); - length += key.len + 1; - state.buffer.drawLabel(" ", length - 1, config.edge_margin); - - state.buffer.drawLabel(lang.brightness_down, length, config.edge_margin); - length += state.brightness_down_len + 1; - } - - if (config.brightness_up_key) |key| { - state.buffer.drawLabel(key, length, config.edge_margin); - length += key.len + 1; - state.buffer.drawLabel(" ", length - 1, config.edge_margin); - - state.buffer.drawLabel(lang.brightness_up, length, config.edge_margin); - length += state.brightness_up_len + 1; - } + state.shutdown_label.draw(); + state.restart_label.draw(); + state.sleep_label.draw(); + state.hibernate_label.draw(); + state.brightness_down_label.draw(); + state.brightness_up_label.draw(); } if (config.vi_mode) { @@ -1178,17 +1313,8 @@ fn drawUi(config: Config, lang: Lang, log_file: *LogFile, state: *UiState) !bool break :draw_lock_state; }; - var lock_state_x = state.buffer.width - @min(state.buffer.width, lang.numlock.len) - config.edge_margin; - var lock_state_y: usize = config.edge_margin; - - if (config.clock != null) lock_state_y += 1; - - if (lock_state.numlock) state.buffer.drawLabel(lang.numlock, lock_state_x, lock_state_y); - - if (lock_state_x >= lang.capslock.len + 1) { - lock_state_x -= lang.capslock.len + 1; - if (lock_state.capslock) state.buffer.drawLabel(lang.capslock, lock_state_x, lock_state_y); - } + if (lock_state.numlock) state.numlock_label.draw(); + if (lock_state.capslock) state.capslock_label.draw(); } state.session.label.draw(); @@ -1199,6 +1325,81 @@ fn drawUi(config: Config, lang: Lang, log_file: *LogFile, state: *UiState) !bool return true; } +fn positionComponents(state: *UiState) void { + if (!state.hide_key_hints) { + state.shutdown_label.positionX(state.edge_margin + .add(TerminalBuffer.START_POSITION)); + state.restart_label.positionX(state.shutdown_label + .childrenPosition() + .addX(1)); + state.sleep_label.positionX(state.restart_label + .childrenPosition() + .addX(1)); + state.hibernate_label.positionX(state.sleep_label + .childrenPosition() + .addX(1)); + state.brightness_down_label.positionX(state.hibernate_label + .childrenPosition() + .addX(1)); + state.brightness_up_label.positionX(state.brightness_down_label + .childrenPosition() + .addX(1)); + } + + state.battery_label.positionXY(state.edge_margin + .add(TerminalBuffer.START_POSITION) + .addYFromIf(state.shutdown_label.childrenPosition(), !state.hide_key_hints) + .removeYFromIf(state.edge_margin, !state.hide_key_hints)); + // TODO: Fix not showing on first try (with separate update function) + state.clock_label.positionXY(state.edge_margin + .add(TerminalBuffer.START_POSITION) + .invertX(state.buffer.width) + .removeXIf(state.clock_label.text.len, state.buffer.width > state.clock_label.text.len + state.edge_margin.x)); + + state.numlock_label.positionX(state.edge_margin + .add(TerminalBuffer.START_POSITION) + .addYFromIf(state.clock_label.childrenPosition(), state.uses_clock) + .removeYFromIf(state.edge_margin, state.uses_clock) + .invertX(state.buffer.width) + .removeXIf(state.numlock_label.text.len, state.buffer.width > state.numlock_label.text.len + state.edge_margin.x)); + state.capslock_label.positionX(state.numlock_label + .childrenPosition() + .removeX(state.numlock_label.text.len + state.capslock_label.text.len + 1)); + + state.box.positionXY(TerminalBuffer.START_POSITION); + + state.info_line.label.positionY(state.box + .childrenPosition()); + + // TODO: Same as above + state.session_specifier_label.positionX(state.info_line.label + .childrenPosition() + .addY(1)); + state.session.label.positionY(state.session_specifier_label + .childrenPosition() + .addX(state.labels_max_length - state.session_specifier_label.text.len + 1)); + + state.login_label.positionX(state.session.label + .childrenPosition() + .resetXFrom(state.info_line.label.childrenPosition()) + .addY(1)); + state.login.label.positionY(state.login_label + .childrenPosition() + .addX(state.labels_max_length - state.login_label.text.len + 1)); + + state.password_label.positionX(state.login.label + .childrenPosition() + .resetXFrom(state.info_line.label.childrenPosition()) + .addY(1)); + state.password.positionY(state.password_label + .childrenPosition() + .addX(state.labels_max_length - state.password_label.text.len + 1)); + + state.version_label.positionXY(state.edge_margin + .add(TerminalBuffer.START_POSITION) + .invertY(state.buffer.height - 1)); +} + fn addOtherEnvironment(session: *Session, lang: Lang, display_server: DisplayServer, exec: ?[]const u8) !void { const name = switch (display_server) { .shell => lang.shell, diff --git a/src/tui/Position.zig b/src/tui/Position.zig index 1a61f61..7073cba 100644 --- a/src/tui/Position.zig +++ b/src/tui/Position.zig @@ -17,6 +17,13 @@ pub fn add(self: Position, other: Position) Position { }; } +pub fn addIf(self: Position, other: Position, condition: bool) Position { + return .{ + .x = self.x + if (condition) other.x else 0, + .y = self.y + if (condition) other.y else 0, + }; +} + pub fn addX(self: Position, x: usize) Position { return .{ .x = self.x + x, @@ -30,3 +37,185 @@ pub fn addY(self: Position, y: usize) Position { .y = self.y + y, }; } + +pub fn addXIf(self: Position, x: usize, condition: bool) Position { + return .{ + .x = self.x + if (condition) x else 0, + .y = self.y, + }; +} + +pub fn addYIf(self: Position, y: usize, condition: bool) Position { + return .{ + .x = self.x, + .y = self.y + if (condition) y else 0, + }; +} + +pub fn addXFrom(self: Position, other: Position) Position { + return .{ + .x = self.x + other.x, + .y = self.y, + }; +} + +pub fn addYFrom(self: Position, other: Position) Position { + return .{ + .x = self.x, + .y = self.y + other.y, + }; +} + +pub fn addXFromIf(self: Position, other: Position, condition: bool) Position { + return .{ + .x = self.x + if (condition) other.x else 0, + .y = self.y, + }; +} + +pub fn addYFromIf(self: Position, other: Position, condition: bool) Position { + return .{ + .x = self.x, + .y = self.y + if (condition) other.y else 0, + }; +} + +pub fn remove(self: Position, other: Position) Position { + return .{ + .x = self.x - other.x, + .y = self.y - other.y, + }; +} + +pub fn removeIf(self: Position, other: Position, condition: bool) Position { + return .{ + .x = self.x - if (condition) other.x else 0, + .y = self.y - if (condition) other.y else 0, + }; +} + +pub fn removeX(self: Position, x: usize) Position { + return .{ + .x = self.x - x, + .y = self.y, + }; +} + +pub fn removeY(self: Position, y: usize) Position { + return .{ + .x = self.x, + .y = self.y - y, + }; +} + +pub fn removeXIf(self: Position, x: usize, condition: bool) Position { + return .{ + .x = self.x - if (condition) x else 0, + .y = self.y, + }; +} + +pub fn removeYIf(self: Position, y: usize, condition: bool) Position { + return .{ + .x = self.x, + .y = self.y - if (condition) y else 0, + }; +} + +pub fn removeXFrom(self: Position, other: Position) Position { + return .{ + .x = self.x - other.x, + .y = self.y, + }; +} + +pub fn removeYFrom(self: Position, other: Position) Position { + return .{ + .x = self.x, + .y = self.y - other.y, + }; +} + +pub fn removeXFromIf(self: Position, other: Position, condition: bool) Position { + return .{ + .x = self.x - if (condition) other.x else 0, + .y = self.y, + }; +} + +pub fn removeYFromIf(self: Position, other: Position, condition: bool) Position { + return .{ + .x = self.x, + .y = self.y - if (condition) other.y else 0, + }; +} + +pub fn invert(self: Position, other: Position) Position { + return .{ + .x = other.x - self.x, + .y = other.y - self.y, + }; +} + +pub fn invertIf(self: Position, other: Position, condition: bool) Position { + return .{ + .x = if (condition) other.x - self.x else self.x, + .y = if (condition) other.y - self.y else self.y, + }; +} + +pub fn invertX(self: Position, width: usize) Position { + return .{ + .x = width - self.x, + .y = self.y, + }; +} + +pub fn invertY(self: Position, height: usize) Position { + return .{ + .x = self.x, + .y = height - self.y, + }; +} + +pub fn invertXIf(self: Position, width: usize, condition: bool) Position { + return .{ + .x = if (condition) width - self.x else self.x, + .y = self.y, + }; +} + +pub fn invertYIf(self: Position, height: usize, condition: bool) Position { + return .{ + .x = self.x, + .y = if (condition) height - self.y else self.y, + }; +} + +pub fn resetXFrom(self: Position, other: Position) Position { + return .{ + .x = other.x, + .y = self.y, + }; +} + +pub fn resetYFrom(self: Position, other: Position) Position { + return .{ + .x = self.x, + .y = other.y, + }; +} + +pub fn resetXFromIf(self: Position, other: Position, condition: bool) Position { + return .{ + .x = if (condition) other.x else self.x, + .y = self.y, + }; +} + +pub fn resetYFromIf(self: Position, other: Position, condition: bool) Position { + return .{ + .x = self.x, + .y = if (condition) other.y else self.y, + }; +} diff --git a/src/tui/TerminalBuffer.zig b/src/tui/TerminalBuffer.zig index d02a858..7b322b9 100644 --- a/src/tui/TerminalBuffer.zig +++ b/src/tui/TerminalBuffer.zig @@ -201,11 +201,13 @@ pub fn cascade(self: TerminalBuffer) bool { return changed; } -pub fn drawLabel(self: TerminalBuffer, text: []const u8, x: usize, y: usize) void { - drawColorLabel(text, x, y, self.fg, self.bg); -} - -pub fn drawColorLabel(text: []const u8, x: usize, y: usize, fg: u32, bg: u32) void { +pub fn drawText( + text: []const u8, + x: usize, + y: usize, + fg: u32, + bg: u32, +) void { const yc: c_int = @intCast(y); const utf8view = std.unicode.Utf8View.init(text) catch return; var utf8 = utf8view.iterator(); @@ -216,7 +218,14 @@ pub fn drawColorLabel(text: []const u8, x: usize, y: usize, fg: u32, bg: u32) vo } } -pub fn drawConfinedLabel(self: TerminalBuffer, text: []const u8, x: usize, y: usize, max_length: usize) void { +pub fn drawConfinedText( + text: []const u8, + x: usize, + y: usize, + max_length: usize, + fg: u32, + bg: u32, +) void { const yc: c_int = @intCast(y); const utf8view = std.unicode.Utf8View.init(text) catch return; var utf8 = utf8view.iterator(); @@ -224,12 +233,19 @@ pub fn drawConfinedLabel(self: TerminalBuffer, text: []const u8, x: usize, y: us var i: c_int = @intCast(x); while (utf8.nextCodepoint()) |codepoint| : (i += termbox.tb_wcwidth(codepoint)) { if (i - @as(c_int, @intCast(x)) >= max_length) break; - _ = termbox.tb_set_cell(i, yc, codepoint, self.fg, self.bg); + _ = termbox.tb_set_cell(i, yc, codepoint, fg, bg); } } -pub fn drawCharMultiple(self: TerminalBuffer, char: u32, x: usize, y: usize, length: usize) void { - const cell = Cell.init(char, self.fg, self.bg); +pub fn drawCharMultiple( + char: u32, + x: usize, + y: usize, + length: usize, + fg: u32, + bg: u32, +) void { + const cell = Cell.init(char, fg, bg); for (0..length) |xx| cell.put(x + xx, y); } diff --git a/src/tui/components/CenteredBox.zig b/src/tui/components/CenteredBox.zig index da0028b..e0f1798 100644 --- a/src/tui/components/CenteredBox.zig +++ b/src/tui/components/CenteredBox.zig @@ -1,9 +1,8 @@ const std = @import("std"); -const Allocator = std.mem.Allocator; const Cell = @import("../Cell.zig"); -const TerminalBuffer = @import("../TerminalBuffer.zig"); const Position = @import("../Position.zig"); +const TerminalBuffer = @import("../TerminalBuffer.zig"); const termbox = TerminalBuffer.termbox; const CenteredBox = @This(); @@ -17,6 +16,9 @@ show_borders: bool, blank_box: bool, top_title: ?[]const u8, bottom_title: ?[]const u8, +border_fg: u32, +title_fg: u32, +bg: u32, left_pos: Position, right_pos: Position, children_pos: Position, @@ -31,6 +33,9 @@ pub fn init( blank_box: bool, top_title: ?[]const u8, bottom_title: ?[]const u8, + border_fg: u32, + title_fg: u32, + bg: u32, ) CenteredBox { return .{ .buffer = buffer, @@ -42,13 +47,16 @@ pub fn init( .blank_box = blank_box, .top_title = top_title, .bottom_title = bottom_title, + .border_fg = border_fg, + .title_fg = title_fg, + .bg = bg, .left_pos = TerminalBuffer.START_POSITION, .right_pos = TerminalBuffer.START_POSITION, .children_pos = TerminalBuffer.START_POSITION, }; } -pub fn position(self: *CenteredBox, original_pos: Position) void { +pub fn positionXY(self: *CenteredBox, original_pos: Position) void { if (self.buffer.width < 2 or self.buffer.height < 2) return; self.left_pos = Position.init( @@ -77,33 +85,33 @@ pub fn draw(self: CenteredBox) void { @intCast(self.left_pos.x - 1), @intCast(self.left_pos.y - 1), self.buffer.box_chars.left_up, - self.buffer.border_fg, - self.buffer.bg, + self.border_fg, + self.bg, ); _ = termbox.tb_set_cell( @intCast(self.right_pos.x), @intCast(self.left_pos.y - 1), self.buffer.box_chars.right_up, - self.buffer.border_fg, - self.buffer.bg, + self.border_fg, + self.bg, ); _ = termbox.tb_set_cell( @intCast(self.left_pos.x - 1), @intCast(self.right_pos.y), self.buffer.box_chars.left_down, - self.buffer.border_fg, - self.buffer.bg, + self.border_fg, + self.bg, ); _ = termbox.tb_set_cell( @intCast(self.right_pos.x), @intCast(self.right_pos.y), self.buffer.box_chars.right_down, - self.buffer.border_fg, - self.buffer.bg, + self.border_fg, + self.bg, ); - var c1 = Cell.init(self.buffer.box_chars.top, self.buffer.border_fg, self.buffer.bg); - var c2 = Cell.init(self.buffer.box_chars.bottom, self.buffer.border_fg, self.buffer.bg); + var c1 = Cell.init(self.buffer.box_chars.top, self.border_fg, self.bg); + var c2 = Cell.init(self.buffer.box_chars.bottom, self.border_fg, self.bg); for (0..self.width) |i| { c1.put(self.left_pos.x + i, self.left_pos.y - 1); @@ -128,20 +136,24 @@ pub fn draw(self: CenteredBox) void { } if (self.top_title) |title| { - self.buffer.drawConfinedLabel( + TerminalBuffer.drawConfinedText( title, self.left_pos.x, self.left_pos.y - 1, self.width, + self.title_fg, + self.bg, ); } if (self.bottom_title) |title| { - self.buffer.drawConfinedLabel( + TerminalBuffer.drawConfinedText( title, self.left_pos.x, self.left_pos.y + self.height, self.width, + self.title_fg, + self.bg, ); } } diff --git a/src/tui/components/InfoLine.zig b/src/tui/components/InfoLine.zig index 6248391..ecff573 100644 --- a/src/tui/components/InfoLine.zig +++ b/src/tui/components/InfoLine.zig @@ -21,6 +21,8 @@ pub fn init( allocator: Allocator, buffer: *TerminalBuffer, width: usize, + arrow_fg: u32, + arrow_bg: u32, ) InfoLine { return .{ .label = MessageLabel.init( @@ -31,6 +33,8 @@ pub fn init( null, width, true, + arrow_fg, + arrow_bg, ), }; } @@ -57,23 +61,26 @@ pub fn clearRendered(self: InfoLine, allocator: Allocator) !void { @memset(spaces, ' '); - self.label.buffer.drawLabel( + TerminalBuffer.drawText( spaces, self.label.component_pos.x + 2, self.label.component_pos.y, + TerminalBuffer.Color.DEFAULT, + TerminalBuffer.Color.DEFAULT, ); } fn drawItem(label: *MessageLabel, message: Message, x: usize, y: usize, width: usize) void { - if (message.width == 0 or width <= message.width) return; + if (message.width == 0) return; - const x_offset = if (label.text_in_center) (width - message.width - 1) / 2 else 0; + const x_offset = if (label.text_in_center and width >= message.width) (width - message.width) / 2 else 0; - label.item_width = message.width + x_offset; - TerminalBuffer.drawColorLabel( + label.cursor = message.width + x_offset; + TerminalBuffer.drawConfinedText( message.text, x + x_offset, y, + width, message.fg, message.bg, ); diff --git a/src/tui/components/Label.zig b/src/tui/components/Label.zig new file mode 100644 index 0000000..c12ee11 --- /dev/null +++ b/src/tui/components/Label.zig @@ -0,0 +1,109 @@ +const std = @import("std"); +const Allocator = std.mem.Allocator; + +const Cell = @import("../Cell.zig"); +const Position = @import("../Position.zig"); +const TerminalBuffer = @import("../TerminalBuffer.zig"); +const termbox = TerminalBuffer.termbox; + +const Label = @This(); + +text: []const u8, +max_width: ?usize, +fg: u32, +bg: u32, +is_text_allocated: bool, +component_pos: Position, +children_pos: Position, + +pub fn init( + text: []const u8, + max_width: ?usize, + fg: u32, + bg: u32, +) Label { + return .{ + .text = text, + .max_width = max_width, + .fg = fg, + .bg = bg, + .is_text_allocated = false, + .component_pos = TerminalBuffer.START_POSITION, + .children_pos = TerminalBuffer.START_POSITION, + }; +} + +pub fn setTextAlloc( + self: *Label, + allocator: Allocator, + comptime fmt: []const u8, + args: anytype, +) !void { + self.text = try std.fmt.allocPrint(allocator, fmt, args); + self.is_text_allocated = true; +} + +pub fn setTextBuf( + self: *Label, + buffer: []u8, + comptime fmt: []const u8, + args: anytype, +) !void { + self.text = try std.fmt.bufPrint(buffer, fmt, args); + self.is_text_allocated = false; +} + +pub fn setText(self: *Label, text: []const u8) void { + self.text = text; + self.is_text_allocated = false; +} + +pub fn deinit(self: Label, allocator: ?Allocator) void { + if (self.is_text_allocated) { + if (allocator) |alloc| alloc.free(self.text); + } +} + +pub fn positionX(self: *Label, original_pos: Position) void { + self.component_pos = original_pos; + self.children_pos = original_pos.addX(self.text.len); +} + +pub fn positionY(self: *Label, original_pos: Position) void { + self.component_pos = original_pos; + self.children_pos = original_pos.addY(1); +} + +pub fn positionXY(self: *Label, original_pos: Position) void { + self.component_pos = original_pos; + self.children_pos = Position.init( + self.text.len, + 1, + ).add(original_pos); +} + +pub fn childrenPosition(self: Label) Position { + return self.children_pos; +} + +pub fn draw(self: Label) void { + if (self.max_width) |width| { + TerminalBuffer.drawConfinedText( + self.text, + self.component_pos.x, + self.component_pos.y, + width, + self.fg, + self.bg, + ); + return; + } + + TerminalBuffer.drawText( + self.text, + self.component_pos.x, + self.component_pos.y, + self.fg, + self.bg, + ); +} diff --git a/src/tui/components/Session.zig b/src/tui/components/Session.zig index 2c2ce62..d468eb0 100644 --- a/src/tui/components/Session.zig +++ b/src/tui/components/Session.zig @@ -25,6 +25,8 @@ pub fn init( user_list: *UserList, width: usize, text_in_center: bool, + fg: u32, + bg: u32, ) Session { return .{ .label = EnvironmentLabel.init( @@ -35,6 +37,8 @@ pub fn init( user_list, width, text_in_center, + fg, + bg, ), .user_list = user_list, }; @@ -70,11 +74,20 @@ fn sessionChanged(env: Env, maybe_user_list: ?*UserList) void { } fn drawItem(label: *EnvironmentLabel, env: Env, x: usize, y: usize, width: usize) void { + if (width < 3) return; + const length = @min(env.environment.name.len, width - 3); if (length == 0) return; - const x_offset = if (label.text_in_center) (width - length - 1) / 2 else 0; + const x_offset = if (label.text_in_center and width >= length) (width - length) / 2 else 0; - label.item_width = length + x_offset; - label.buffer.drawLabel(env.environment.name, x + x_offset, y); + label.cursor = length + x_offset; + TerminalBuffer.drawConfinedText( + env.environment.name, + x + x_offset, + y, + width, + label.fg, + label.bg, + ); } diff --git a/src/tui/components/Text.zig b/src/tui/components/Text.zig index c31bcb4..095253e 100644 --- a/src/tui/components/Text.zig +++ b/src/tui/components/Text.zig @@ -20,6 +20,8 @@ component_pos: Position, children_pos: Position, masked: bool, maybe_mask: ?u32, +fg: u32, +bg: u32, pub fn init( allocator: Allocator, @@ -27,6 +29,8 @@ pub fn init( masked: bool, maybe_mask: ?u32, width: usize, + fg: u32, + bg: u32, ) Text { return .{ .allocator = allocator, @@ -40,6 +44,8 @@ pub fn init( .children_pos = TerminalBuffer.START_POSITION, .masked = masked, .maybe_mask = maybe_mask, + .fg = fg, + .bg = bg, }; } @@ -115,14 +121,18 @@ pub fn handle(self: *Text, maybe_event: ?*termbox.tb_event, insert_mode: bool) ! pub fn draw(self: Text) void { if (self.masked) { if (self.maybe_mask) |mask| { + if (self.width < 1) return; + const length = @min(self.text.items.len, self.width - 1); if (length == 0) return; - self.buffer.drawCharMultiple( + TerminalBuffer.drawCharMultiple( mask, self.component_pos.x, self.component_pos.y, length, + self.fg, + self.bg, ); } return; @@ -139,7 +149,13 @@ pub fn draw(self: Text) void { } }; - self.buffer.drawLabel(visible_slice, self.component_pos.x, self.component_pos.y); + TerminalBuffer.drawText( + visible_slice, + self.component_pos.x, + self.component_pos.y, + self.fg, + self.bg, + ); } pub fn clear(self: *Text) void { diff --git a/src/tui/components/UserList.zig b/src/tui/components/UserList.zig index 26eee4c..3bb96b3 100644 --- a/src/tui/components/UserList.zig +++ b/src/tui/components/UserList.zig @@ -27,8 +27,10 @@ pub fn init( session: *Session, width: usize, text_in_center: bool, + fg: u32, + bg: u32, ) !UserList { - var userList = UserList{ + var user_list = UserList{ .label = UserLabel.init( allocator, buffer, @@ -37,6 +39,8 @@ pub fn init( session, width, text_in_center, + fg, + bg, ), }; @@ -60,7 +64,7 @@ pub fn init( allocated_index = true; } - try userList.label.addItem(.{ + try user_list.label.addItem(.{ .name = username, .session_index = maybe_session_index.?, .allocated_index = allocated_index, @@ -68,7 +72,7 @@ pub fn init( }); } - return userList; + return user_list; } pub fn deinit(self: *UserList) void { @@ -92,11 +96,20 @@ fn usernameChanged(user: User, maybe_session: ?*Session) void { } fn drawItem(label: *UserLabel, user: User, x: usize, y: usize, width: usize) void { + if (width < 3) return; + const length = @min(user.name.len, width - 3); if (length == 0) return; - const x_offset = if (label.text_in_center) (width - length - 1) / 2 else 0; + const x_offset = if (label.text_in_center and width >= length) (width - length) / 2 else 0; - label.item_width = length + x_offset; - label.buffer.drawLabel(user.name, x + x_offset, y); + label.cursor = length + x_offset; + TerminalBuffer.drawConfinedText( + user.name, + x + x_offset, + y, + width, + label.fg, + label.bg, + ); } diff --git a/src/tui/components/generic.zig b/src/tui/components/generic.zig index 479b14f..d2ddfd0 100644 --- a/src/tui/components/generic.zig +++ b/src/tui/components/generic.zig @@ -22,7 +22,9 @@ pub fn CyclableLabel(comptime ItemType: type, comptime ChangeItemType: type) typ component_pos: Position, children_pos: Position, text_in_center: bool, - item_width: usize, + fg: u32, + bg: u32, + cursor: usize, draw_item_fn: DrawItemFn, change_item_fn: ?ChangeItemFn, change_item_arg: ?ChangeItemType, @@ -35,6 +37,8 @@ pub fn CyclableLabel(comptime ItemType: type, comptime ChangeItemType: type) typ change_item_arg: ?ChangeItemType, width: usize, text_in_center: bool, + fg: u32, + bg: u32, ) Self { return .{ .allocator = allocator, @@ -45,7 +49,9 @@ pub fn CyclableLabel(comptime ItemType: type, comptime ChangeItemType: type) typ .component_pos = TerminalBuffer.START_POSITION, .children_pos = TerminalBuffer.START_POSITION, .text_in_center = text_in_center, - .item_width = 0, + .fg = fg, + .bg = bg, + .cursor = 0, .draw_item_fn = draw_item_fn, .change_item_fn = change_item_fn, .change_item_arg = change_item_arg, @@ -58,19 +64,19 @@ pub fn CyclableLabel(comptime ItemType: type, comptime ChangeItemType: type) typ pub fn positionX(self: *Self, original_pos: Position) void { self.component_pos = original_pos; - self.item_width = self.component_pos.x + 2; + self.cursor = self.component_pos.x + 2; self.children_pos = original_pos.addX(self.width); } pub fn positionY(self: *Self, original_pos: Position) void { self.component_pos = original_pos; - self.item_width = self.component_pos.x + 2; + self.cursor = self.component_pos.x + 2; self.children_pos = original_pos.addY(1); } pub fn positionXY(self: *Self, original_pos: Position) void { self.component_pos = original_pos; - self.item_width = self.component_pos.x + 2; + self.cursor = self.component_pos.x + 2; self.children_pos = Position.init( self.width, 1, @@ -106,27 +112,28 @@ pub fn CyclableLabel(comptime ItemType: type, comptime ChangeItemType: type) typ } _ = termbox.tb_set_cursor( - @intCast(self.component_pos.x + self.item_width + 2), + @intCast(self.component_pos.x + self.cursor + 2), @intCast(self.component_pos.y), ); } pub fn draw(self: *Self) void { if (self.list.items.len == 0) return; + if (self.width < 2) return; _ = termbox.tb_set_cell( @intCast(self.component_pos.x), @intCast(self.component_pos.y), '<', - self.buffer.fg, - self.buffer.bg, + self.fg, + self.bg, ); _ = termbox.tb_set_cell( @intCast(self.component_pos.x + self.width - 1), @intCast(self.component_pos.y), '>', - self.buffer.fg, - self.buffer.bg, + self.fg, + self.bg, ); const current_item = self.list.items[self.current]; From f678e3bb281981f07d5293c06b4ea3b92da64859 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Sun, 8 Feb 2026 19:50:32 +0100 Subject: [PATCH 044/176] Add update function for Label Signed-off-by: AnErrupTion --- src/main.zig | 278 ++++++++++++++++++++--------------- src/tui/components/Label.zig | 109 -------------- src/tui/components/label.zig | 126 ++++++++++++++++ 3 files changed, 285 insertions(+), 228 deletions(-) delete mode 100644 src/tui/components/Label.zig create mode 100644 src/tui/components/label.zig diff --git a/src/main.zig b/src/main.zig index 3f8042a..69a3b93 100644 --- a/src/main.zig +++ b/src/main.zig @@ -33,7 +33,7 @@ const Animation = @import("tui/Animation.zig"); const Position = @import("tui/Position.zig"); const CenteredBox = @import("tui/components/CenteredBox.zig"); const InfoLine = @import("tui/components/InfoLine.zig"); -const Label = @import("tui/components/Label.zig"); +const label = @import("tui/components/label.zig"); const Session = @import("tui/components/Session.zig"); const Text = @import("tui/components/Text.zig"); const UserList = @import("tui/components/UserList.zig"); @@ -41,6 +41,8 @@ const TerminalBuffer = @import("tui/TerminalBuffer.zig"); const termbox = TerminalBuffer.termbox; const StringList = std.ArrayListUnmanaged([]const u8); + +const Label = label.Label; const ly_version_str = "Ly version " ++ build_options.version; var session_pid: std.posix.pid_t = -1; @@ -62,6 +64,7 @@ fn ttyControlTransferSignalHandler(_: c_int) callconv(.c) void { TerminalBuffer.shutdownStatic(); } +const NoType = struct {}; const UiState = struct { auth_fails: u64, update: bool, @@ -69,21 +72,20 @@ const UiState = struct { labels_max_length: usize, animation_timed_out: bool, animation: *?Animation, - can_draw_battery: bool, - shutdown_label: *Label, - restart_label: *Label, - sleep_label: *Label, - hibernate_label: *Label, - brightness_down_label: *Label, - brightness_up_label: *Label, - numlock_label: *Label, - capslock_label: *Label, - battery_label: *Label, - clock_label: *Label, - session_specifier_label: *Label, - login_label: *Label, - password_label: *Label, - version_label: *Label, + shutdown_label: *Label(NoType), + restart_label: *Label(NoType), + sleep_label: *Label(NoType), + hibernate_label: *Label(NoType), + brightness_down_label: *Label(NoType), + brightness_up_label: *Label(NoType), + numlock_label: *Label(*UiState), + capslock_label: *Label(*UiState), + battery_label: *Label(*UiState), + clock_label: *Label(*UiState), + session_specifier_label: *Label(*UiState), + login_label: *Label(NoType), + password_label: *Label(NoType), + version_label: *Label(NoType), box: *CenteredBox, info_line: *InfoLine, animate: bool, @@ -93,17 +95,12 @@ const UiState = struct { password: *Text, active_input: enums.Input, insert_mode: bool, - can_draw_clock: bool, - shutdown_len: u8, - restart_len: u8, - sleep_len: u8, - hibernate_len: u8, - brightness_down_len: u8, - brightness_up_len: u8, - can_get_lock_state: bool, edge_margin: Position, - hide_key_hints: bool, - uses_clock: bool, + config: Config, + lang: Lang, + log_file: *LogFile, + battery_buf: [16:0]u8, + clock_buf: [64:0]u8, }; pub fn main() !void { @@ -339,51 +336,57 @@ pub fn main() !void { std.posix.sigaction(std.posix.SIG.TERM, &act, null); // Initialize components - var shutdown_label = Label.init( + var shutdown_label = Label(NoType).init( "", null, buffer.fg, buffer.bg, + null, ); defer shutdown_label.deinit(allocator); - var restart_label = Label.init( + var restart_label = Label(NoType).init( "", null, buffer.fg, buffer.bg, + null, ); defer restart_label.deinit(allocator); - var sleep_label = Label.init( + var sleep_label = Label(NoType).init( "", null, buffer.fg, buffer.bg, + null, ); defer sleep_label.deinit(allocator); - var hibernate_label = Label.init( + var hibernate_label = Label(NoType).init( "", null, buffer.fg, buffer.bg, + null, ); defer hibernate_label.deinit(allocator); - var brightness_down_label = Label.init( + var brightness_down_label = Label(NoType).init( "", null, buffer.fg, buffer.bg, + null, ); defer brightness_down_label.deinit(allocator); - var brightness_up_label = Label.init( + var brightness_up_label = Label(NoType).init( "", null, buffer.fg, buffer.bg, + null, ); defer brightness_up_label.deinit(allocator); @@ -428,35 +431,39 @@ pub fn main() !void { } } - var numlock_label = Label.init( - lang.numlock, + var numlock_label = Label(*UiState).init( + "", null, buffer.fg, buffer.bg, + &updateNumlock, ); defer numlock_label.deinit(null); - var capslock_label = Label.init( - lang.capslock, + var capslock_label = Label(*UiState).init( + "", null, buffer.fg, buffer.bg, + &updateCapslock, ); defer capslock_label.deinit(null); - var battery_label = Label.init( + var battery_label = Label(*UiState).init( "", null, buffer.fg, buffer.bg, + &updateBattery, ); defer battery_label.deinit(null); - var clock_label = Label.init( + var clock_label = Label(*UiState).init( "", null, buffer.fg, buffer.bg, + &updateClock, ); defer clock_label.deinit(null); @@ -525,11 +532,12 @@ pub fn main() !void { var login: UserList = undefined; - var session_specifier_label = Label.init( + var session_specifier_label = Label(*UiState).init( "", null, buffer.fg, buffer.bg, + &updateSessionSpecifier, ); defer session_specifier_label.deinit(null); @@ -544,11 +552,12 @@ pub fn main() !void { ); defer session.deinit(); - var login_label = Label.init( + var login_label = Label(NoType).init( lang.login, null, buffer.fg, buffer.bg, + null, ); defer login_label.deinit(null); @@ -624,11 +633,12 @@ pub fn main() !void { try log_file.err("sys", "no users found", .{}); } - var password_label = Label.init( + var password_label = Label(NoType).init( lang.password, null, buffer.fg, buffer.bg, + null, ); defer password_label.deinit(null); @@ -643,11 +653,12 @@ pub fn main() !void { ); defer password.deinit(); - var version_label = Label.init( + var version_label = Label(NoType).init( ly_version_str, null, buffer.fg, buffer.bg, + null, ); defer version_label.deinit(null); @@ -688,7 +699,6 @@ pub fn main() !void { .labels_max_length = labels_max_length, .animation_timed_out = false, .animation = &animation, - .can_draw_battery = true, .shutdown_label = &shutdown_label, .restart_label = &restart_label, .sleep_label = &sleep_label, @@ -712,20 +722,15 @@ pub fn main() !void { .password = &password, .active_input = config.default_input, .insert_mode = !config.vi_mode or config.vi_default_mode == .insert, - .can_draw_clock = true, - .shutdown_len = try TerminalBuffer.strWidth(lang.shutdown), - .restart_len = try TerminalBuffer.strWidth(lang.restart), - .sleep_len = try TerminalBuffer.strWidth(lang.sleep), - .hibernate_len = try TerminalBuffer.strWidth(lang.hibernate), - .brightness_down_len = try TerminalBuffer.strWidth(lang.brightness_down), - .brightness_up_len = try TerminalBuffer.strWidth(lang.brightness_up), - .can_get_lock_state = true, .edge_margin = Position.init( config.edge_margin, config.edge_margin, ), - .hide_key_hints = config.hide_key_hints, - .uses_clock = config.clock != null, + .config = config, + .lang = lang, + .log_file = &log_file, + .battery_buf = undefined, + .clock_buf = undefined, }; // Load last saved username and desktop selection, if any @@ -753,6 +758,7 @@ pub fn main() !void { } // Position components + try updateComponents(&state); positionComponents(&state); switch (state.active_input) { @@ -857,7 +863,8 @@ pub fn main() !void { } if (state.update) { - if (!try drawUi(config, lang, &log_file, &state)) continue; + try updateComponents(&state); + if (!try drawUi(&log_file, &state)) continue; } var timeout: i32 = -1; @@ -1188,9 +1195,26 @@ pub fn main() !void { } } -fn drawUi(config: Config, lang: Lang, log_file: *LogFile, state: *UiState) !bool { +fn updateComponents(state: *UiState) !void { + if (state.config.battery_id != null) { + try state.battery_label.update(state); + } + + if (state.config.clock != null) { + try state.clock_label.update(state); + } + + try state.session_specifier_label.update(state); + + if (!state.config.hide_keyboard_locks) { + try state.numlock_label.update(state); + try state.capslock_label.update(state); + } +} + +fn drawUi(log_file: *LogFile, state: *UiState) !bool { // If the user entered a wrong password 10 times in a row, play a cascade animation, else update normally - if (config.auth_fails > 0 and state.auth_fails >= config.auth_fails) { + if (state.config.auth_fails > 0 and state.auth_fails >= state.config.auth_fails) { std.Thread.sleep(std.time.ns_per_ms * 10); state.update = state.buffer.cascade(); @@ -1207,36 +1231,19 @@ fn drawUi(config: Config, lang: Lang, log_file: *LogFile, state: *UiState) !bool if (!state.animation_timed_out) if (state.animation.*) |*a| a.draw(); - if (!config.hide_version_string) state.version_label.draw(); + if (!state.config.hide_version_string) state.version_label.draw(); - if (config.battery_id) |id| draw_battery: { - if (!state.can_draw_battery) break :draw_battery; + if (state.config.battery_id != null) state.battery_label.draw(); - const battery_percentage = getBatteryPercentage(id) catch |err| { - try log_file.err("sys", "failed to get battery percentage: {s}", .{@errorName(err)}); - try state.info_line.addMessage(lang.err_battery, config.error_bg, config.error_fg); - state.can_draw_battery = false; - break :draw_battery; - }; - - var battery_buf: [16:0]u8 = undefined; - state.battery_label.setTextBuf( - &battery_buf, - "BAT: {d}%", - .{battery_percentage}, - ) catch break :draw_battery; - state.battery_label.draw(); - } - - if (config.bigclock != .none and state.box.height + (bigclock.HEIGHT + 2) * 2 < state.buffer.height) { + if (state.config.bigclock != .none and state.box.height + (bigclock.HEIGHT + 2) * 2 < state.buffer.height) { var format_buf: [16:0]u8 = undefined; var clock_buf: [32:0]u8 = undefined; // We need the slice/c-string returned by `bufPrintZ`. const format = try std.fmt.bufPrintZ(&format_buf, "{s}{s}{s}{s}", .{ - if (config.bigclock_12hr) "%I" else "%H", + if (state.config.bigclock_12hr) "%I" else "%H", ":%M", - if (config.bigclock_seconds) ":%S" else "", - if (config.bigclock_12hr) "%P" else "", + if (state.config.bigclock_seconds) ":%S" else "", + if (state.config.bigclock_12hr) "%P" else "", }); const xo = state.buffer.width / 2 - @min(state.buffer.width, (format.len * (bigclock.WIDTH + 1))) / 2; const yo = (state.buffer.height - state.box.height) / 2 - bigclock.HEIGHT - 2; @@ -1245,7 +1252,7 @@ fn drawUi(config: Config, lang: Lang, log_file: *LogFile, state: *UiState) !bool for (clock_str, 0..) |c, i| { // TODO: Show error - const clock_cell = try bigclock.clockCell(state.animate, c, state.buffer.fg, state.buffer.bg, config.bigclock); + const clock_cell = try bigclock.clockCell(state.animate, c, state.buffer.fg, state.buffer.bg, state.config.bigclock); bigclock.alphaBlit(xo + i * (bigclock.WIDTH + 1), yo, state.buffer.width, state.buffer.height, clock_cell); } } @@ -1262,37 +1269,20 @@ fn drawUi(config: Config, lang: Lang, log_file: *LogFile, state: *UiState) !bool .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(lang.err_alloc, config.error_bg, config.error_fg); + try state.info_line.addMessage(state.lang.err_alloc, state.config.error_bg, state.config.error_fg); try log_file.err("tui", "failed to handle password input: {s}", .{@errorName(err)}); }, } - if (config.clock) |clock| draw_clock: { - if (!state.can_draw_clock) break :draw_clock; + if (state.config.clock != null) state.clock_label.draw(); - var clock_buf: [64:0]u8 = undefined; - const clock_str = interop.timeAsString(&clock_buf, clock); - - if (clock_str.len == 0) { - try state.info_line.addMessage(lang.err_clock_too_long, config.error_bg, config.error_fg); - state.can_draw_clock = false; - try log_file.err("tui", "clock string too long", .{}); - break :draw_clock; - } - - state.clock_label.setText(clock_str); - state.clock_label.draw(); - } - - const env = state.session.label.list.items[state.session.label.current]; - state.session_specifier_label.setText(env.environment.specifier); state.session_specifier_label.draw(); state.login_label.draw(); state.password_label.draw(); state.info_line.label.draw(); - if (!config.hide_key_hints) { + if (!state.config.hide_key_hints) { state.shutdown_label.draw(); state.restart_label.draw(); state.sleep_label.draw(); @@ -1301,20 +1291,13 @@ fn drawUi(config: Config, lang: Lang, log_file: *LogFile, state: *UiState) !bool state.brightness_up_label.draw(); } - if (config.vi_mode) { - state.box.bottom_title = if (state.insert_mode) lang.insert else lang.normal; + if (state.config.vi_mode) { + state.box.bottom_title = if (state.insert_mode) state.lang.insert else state.lang.normal; } - if (!config.hide_keyboard_locks and state.can_get_lock_state) draw_lock_state: { - const lock_state = interop.getLockState() catch |err| { - try state.info_line.addMessage(lang.err_lock_state, config.error_bg, config.error_fg); - state.can_get_lock_state = false; - try log_file.err("sys", "failed to get lock state: {s}", .{@errorName(err)}); - break :draw_lock_state; - }; - - if (lock_state.numlock) state.numlock_label.draw(); - if (lock_state.capslock) state.capslock_label.draw(); + if (!state.config.hide_keyboard_locks) { + state.numlock_label.draw(); + state.capslock_label.draw(); } state.session.label.draw(); @@ -1325,8 +1308,67 @@ fn drawUi(config: Config, lang: Lang, log_file: *LogFile, state: *UiState) !bool return true; } +fn updateNumlock(self: *Label(*UiState), state: *UiState) !void { + const lock_state = interop.getLockState() catch |err| { + self.update_fn = null; + try state.info_line.addMessage(state.lang.err_lock_state, state.config.error_bg, state.config.error_fg); + try state.log_file.err("sys", "failed to get lock state: {s}", .{@errorName(err)}); + return; + }; + + self.setText(if (lock_state.numlock) state.lang.numlock else ""); +} + +fn updateCapslock(self: *Label(*UiState), state: *UiState) !void { + const lock_state = interop.getLockState() catch |err| { + self.update_fn = null; + try state.info_line.addMessage(state.lang.err_lock_state, state.config.error_bg, state.config.error_fg); + try state.log_file.err("sys", "failed to get lock state: {s}", .{@errorName(err)}); + return; + }; + + self.setText(if (lock_state.capslock) state.lang.capslock else ""); +} + +fn updateBattery(self: *Label(*UiState), state: *UiState) !void { + if (state.config.battery_id) |id| { + const battery_percentage = getBatteryPercentage(id) catch |err| { + self.update_fn = null; + try state.log_file.err("sys", "failed to get battery percentage: {s}", .{@errorName(err)}); + try state.info_line.addMessage(state.lang.err_battery, state.config.error_bg, state.config.error_fg); + return; + }; + + try self.setTextBuf( + &state.battery_buf, + "BAT: {d}%", + .{battery_percentage}, + ); + } +} + +fn updateClock(self: *Label(*UiState), state: *UiState) !void { + if (state.config.clock) |clock| draw_clock: { + const clock_str = interop.timeAsString(&state.clock_buf, clock); + + if (clock_str.len == 0) { + self.update_fn = null; + try state.info_line.addMessage(state.lang.err_clock_too_long, state.config.error_bg, state.config.error_fg); + try state.log_file.err("tui", "clock string too long", .{}); + break :draw_clock; + } + + self.setText(clock_str); + } +} + +fn updateSessionSpecifier(self: *Label(*UiState), state: *UiState) !void { + const env = state.session.label.list.items[state.session.label.current]; + self.setText(env.environment.specifier); +} + fn positionComponents(state: *UiState) void { - if (!state.hide_key_hints) { + if (!state.config.hide_key_hints) { state.shutdown_label.positionX(state.edge_margin .add(TerminalBuffer.START_POSITION)); state.restart_label.positionX(state.shutdown_label @@ -1348,9 +1390,8 @@ fn positionComponents(state: *UiState) void { state.battery_label.positionXY(state.edge_margin .add(TerminalBuffer.START_POSITION) - .addYFromIf(state.shutdown_label.childrenPosition(), !state.hide_key_hints) - .removeYFromIf(state.edge_margin, !state.hide_key_hints)); - // TODO: Fix not showing on first try (with separate update function) + .addYFromIf(state.shutdown_label.childrenPosition(), !state.config.hide_key_hints) + .removeYFromIf(state.edge_margin, !state.config.hide_key_hints)); state.clock_label.positionXY(state.edge_margin .add(TerminalBuffer.START_POSITION) .invertX(state.buffer.width) @@ -1358,8 +1399,8 @@ fn positionComponents(state: *UiState) void { state.numlock_label.positionX(state.edge_margin .add(TerminalBuffer.START_POSITION) - .addYFromIf(state.clock_label.childrenPosition(), state.uses_clock) - .removeYFromIf(state.edge_margin, state.uses_clock) + .addYFromIf(state.clock_label.childrenPosition(), state.config.clock != null) + .removeYFromIf(state.edge_margin, state.config.clock != null) .invertX(state.buffer.width) .removeXIf(state.numlock_label.text.len, state.buffer.width > state.numlock_label.text.len + state.edge_margin.x)); state.capslock_label.positionX(state.numlock_label @@ -1371,7 +1412,6 @@ fn positionComponents(state: *UiState) void { state.info_line.label.positionY(state.box .childrenPosition()); - // TODO: Same as above state.session_specifier_label.positionX(state.info_line.label .childrenPosition() .addY(1)); diff --git a/src/tui/components/Label.zig b/src/tui/components/Label.zig deleted file mode 100644 index c12ee11..0000000 --- a/src/tui/components/Label.zig +++ /dev/null @@ -1,109 +0,0 @@ -const std = @import("std"); -const Allocator = std.mem.Allocator; - -const Cell = @import("../Cell.zig"); -const Position = @import("../Position.zig"); -const TerminalBuffer = @import("../TerminalBuffer.zig"); -const termbox = TerminalBuffer.termbox; - -const Label = @This(); - -text: []const u8, -max_width: ?usize, -fg: u32, -bg: u32, -is_text_allocated: bool, -component_pos: Position, -children_pos: Position, - -pub fn init( - text: []const u8, - max_width: ?usize, - fg: u32, - bg: u32, -) Label { - return .{ - .text = text, - .max_width = max_width, - .fg = fg, - .bg = bg, - .is_text_allocated = false, - .component_pos = TerminalBuffer.START_POSITION, - .children_pos = TerminalBuffer.START_POSITION, - }; -} - -pub fn setTextAlloc( - self: *Label, - allocator: Allocator, - comptime fmt: []const u8, - args: anytype, -) !void { - self.text = try std.fmt.allocPrint(allocator, fmt, args); - self.is_text_allocated = true; -} - -pub fn setTextBuf( - self: *Label, - buffer: []u8, - comptime fmt: []const u8, - args: anytype, -) !void { - self.text = try std.fmt.bufPrint(buffer, fmt, args); - self.is_text_allocated = false; -} - -pub fn setText(self: *Label, text: []const u8) void { - self.text = text; - self.is_text_allocated = false; -} - -pub fn deinit(self: Label, allocator: ?Allocator) void { - if (self.is_text_allocated) { - if (allocator) |alloc| alloc.free(self.text); - } -} - -pub fn positionX(self: *Label, original_pos: Position) void { - self.component_pos = original_pos; - self.children_pos = original_pos.addX(self.text.len); -} - -pub fn positionY(self: *Label, original_pos: Position) void { - self.component_pos = original_pos; - self.children_pos = original_pos.addY(1); -} - -pub fn positionXY(self: *Label, original_pos: Position) void { - self.component_pos = original_pos; - self.children_pos = Position.init( - self.text.len, - 1, - ).add(original_pos); -} - -pub fn childrenPosition(self: Label) Position { - return self.children_pos; -} - -pub fn draw(self: Label) void { - if (self.max_width) |width| { - TerminalBuffer.drawConfinedText( - self.text, - self.component_pos.x, - self.component_pos.y, - width, - self.fg, - self.bg, - ); - return; - } - - TerminalBuffer.drawText( - self.text, - self.component_pos.x, - self.component_pos.y, - self.fg, - self.bg, - ); -} diff --git a/src/tui/components/label.zig b/src/tui/components/label.zig new file mode 100644 index 0000000..ba483bb --- /dev/null +++ b/src/tui/components/label.zig @@ -0,0 +1,126 @@ +const std = @import("std"); +const Allocator = std.mem.Allocator; + +const Cell = @import("../Cell.zig"); +const Position = @import("../Position.zig"); +const TerminalBuffer = @import("../TerminalBuffer.zig"); +const termbox = TerminalBuffer.termbox; + +pub fn Label(comptime ContextType: type) type { + return struct { + const Self = @This(); + + text: []const u8, + max_width: ?usize, + fg: u32, + bg: u32, + update_fn: ?*const fn (*Self, ContextType) anyerror!void, + is_text_allocated: bool, + component_pos: Position, + children_pos: Position, + + pub fn init( + text: []const u8, + max_width: ?usize, + fg: u32, + bg: u32, + update_fn: ?*const fn (*Self, ContextType) anyerror!void, + ) Self { + return .{ + .text = text, + .max_width = max_width, + .fg = fg, + .bg = bg, + .update_fn = update_fn, + .is_text_allocated = false, + .component_pos = TerminalBuffer.START_POSITION, + .children_pos = TerminalBuffer.START_POSITION, + }; + } + + pub fn setTextAlloc( + self: *Self, + allocator: Allocator, + comptime fmt: []const u8, + args: anytype, + ) !void { + self.text = try std.fmt.allocPrint(allocator, fmt, args); + self.is_text_allocated = true; + } + + pub fn setTextBuf( + self: *Self, + buffer: []u8, + comptime fmt: []const u8, + args: anytype, + ) !void { + self.text = try std.fmt.bufPrint(buffer, fmt, args); + self.is_text_allocated = false; + } + + pub fn setText(self: *Self, text: []const u8) void { + self.text = text; + self.is_text_allocated = false; + } + + pub fn deinit(self: Self, allocator: ?Allocator) void { + if (self.is_text_allocated) { + if (allocator) |alloc| alloc.free(self.text); + } + } + + pub fn positionX(self: *Self, original_pos: Position) void { + self.component_pos = original_pos; + self.children_pos = original_pos.addX(self.text.len); + } + + pub fn positionY(self: *Self, original_pos: Position) void { + self.component_pos = original_pos; + self.children_pos = original_pos.addY(1); + } + + pub fn positionXY(self: *Self, original_pos: Position) void { + self.component_pos = original_pos; + self.children_pos = Position.init( + self.text.len, + 1, + ).add(original_pos); + } + + pub fn childrenPosition(self: Self) Position { + return self.children_pos; + } + + pub fn draw(self: Self) void { + if (self.max_width) |width| { + TerminalBuffer.drawConfinedText( + self.text, + self.component_pos.x, + self.component_pos.y, + width, + self.fg, + self.bg, + ); + return; + } + + TerminalBuffer.drawText( + self.text, + self.component_pos.x, + self.component_pos.y, + self.fg, + self.bg, + ); + } + + pub fn update(self: *Self, context: ContextType) !void { + if (self.update_fn) |update_fn| { + return @call( + .auto, + update_fn, + .{ self, context }, + ); + } + } + }; +} From 769aefd6e96b886566c4670261daa87e5f99dbd2 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Sun, 8 Feb 2026 21:14:49 +0100 Subject: [PATCH 045/176] Add separate big label widget for bigclock Signed-off-by: AnErrupTion --- src/bigclock.zig | 60 ----- src/bigclock/Lang.zig | 28 --- src/main.zig | 155 +++++++------ src/tui/components/bigLabel.zig | 210 ++++++++++++++++++ .../components/bigLabelLocales}/en.zig | 8 +- .../components/bigLabelLocales}/fa.zig | 8 +- 6 files changed, 311 insertions(+), 158 deletions(-) delete mode 100644 src/bigclock.zig delete mode 100644 src/bigclock/Lang.zig create mode 100644 src/tui/components/bigLabel.zig rename src/{bigclock => tui/components/bigLabelLocales}/en.zig (93%) rename src/{bigclock => tui/components/bigLabelLocales}/fa.zig (93%) diff --git a/src/bigclock.zig b/src/bigclock.zig deleted file mode 100644 index d8a090d..0000000 --- a/src/bigclock.zig +++ /dev/null @@ -1,60 +0,0 @@ -const std = @import("std"); - -const ly_core = @import("ly-core"); -const interop = ly_core.interop; - -const en = @import("bigclock/en.zig"); -const fa = @import("bigclock/fa.zig"); -const Lang = @import("bigclock/Lang.zig"); -pub const WIDTH = Lang.WIDTH; -pub const HEIGHT = Lang.HEIGHT; -pub const SIZE = Lang.SIZE; -const enums = @import("enums.zig"); -const Bigclock = enums.Bigclock; -const Cell = @import("tui/Cell.zig"); - -pub fn clockCell(animate: bool, char: u8, fg: u32, bg: u32, bigclock: Bigclock) ![SIZE]Cell { - var cells: [SIZE]Cell = undefined; - - const time = try interop.getTimeOfDay(); - const clock_chars = toBigNumber(if (animate and char == ':' and @divTrunc(time.microseconds, 500000) != 0) ' ' else char, bigclock); - for (0..cells.len) |i| cells[i] = Cell.init(clock_chars[i], fg, bg); - - return cells; -} - -pub fn alphaBlit(x: usize, y: usize, tb_width: usize, tb_height: usize, cells: [SIZE]Cell) void { - if (x + WIDTH >= tb_width or y + HEIGHT >= tb_height) return; - - for (0..HEIGHT) |yy| { - for (0..WIDTH) |xx| { - const cell = cells[yy * WIDTH + xx]; - cell.put(x + xx, y + yy); - } - } -} - -fn toBigNumber(char: u8, bigclock: Bigclock) [SIZE]u21 { - const locale_chars = switch (bigclock) { - .fa => fa.locale_chars, - .en => en.locale_chars, - .none => unreachable, - }; - return switch (char) { - '0' => locale_chars.ZERO, - '1' => locale_chars.ONE, - '2' => locale_chars.TWO, - '3' => locale_chars.THREE, - '4' => locale_chars.FOUR, - '5' => locale_chars.FIVE, - '6' => locale_chars.SIX, - '7' => locale_chars.SEVEN, - '8' => locale_chars.EIGHT, - '9' => locale_chars.NINE, - 'p', 'P' => locale_chars.P, - 'a', 'A' => locale_chars.A, - 'm', 'M' => locale_chars.M, - ':' => locale_chars.S, - else => locale_chars.E, - }; -} diff --git a/src/bigclock/Lang.zig b/src/bigclock/Lang.zig deleted file mode 100644 index b09b552..0000000 --- a/src/bigclock/Lang.zig +++ /dev/null @@ -1,28 +0,0 @@ -const ly_core = @import("ly-core"); - -pub const WIDTH = 5; -pub const HEIGHT = 5; -pub const SIZE = WIDTH * HEIGHT; - -pub const X: u32 = if (ly_core.interop.supportsUnicode()) 0x2593 else '#'; -pub const O: u32 = 0; - -// zig fmt: off -pub const LocaleChars = struct { - ZERO: [SIZE]u21, - ONE: [SIZE]u21, - TWO: [SIZE]u21, - THREE: [SIZE]u21, - FOUR: [SIZE]u21, - FIVE: [SIZE]u21, - SIX: [SIZE]u21, - SEVEN: [SIZE]u21, - EIGHT: [SIZE]u21, - NINE: [SIZE]u21, - S: [SIZE]u21, - E: [SIZE]u21, - P: [SIZE]u21, - A: [SIZE]u21, - M: [SIZE]u21, -}; -// zig fmt: on diff --git a/src/main.zig b/src/main.zig index 69a3b93..c3b9ca5 100644 --- a/src/main.zig +++ b/src/main.zig @@ -1,4 +1,5 @@ const std = @import("std"); +const StringList = std.ArrayListUnmanaged([]const u8); const temporary_allocator = std.heap.page_allocator; const builtin = @import("builtin"); const build_options = @import("build_options"); @@ -19,7 +20,6 @@ const DurFile = @import("animations/DurFile.zig"); const GameOfLife = @import("animations/GameOfLife.zig"); const Matrix = @import("animations/Matrix.zig"); const auth = @import("auth.zig"); -const bigclock = @import("bigclock.zig"); const Config = @import("config/Config.zig"); const Lang = @import("config/Lang.zig"); const migrator = @import("config/migrator.zig"); @@ -31,18 +31,19 @@ const Environment = @import("Environment.zig"); const Entry = Environment.Entry; const Animation = @import("tui/Animation.zig"); const Position = @import("tui/Position.zig"); +const bigLabel = @import("tui/components/bigLabel.zig"); +const BigclockLabel = bigLabel.BigLabel(*UiState); const CenteredBox = @import("tui/components/CenteredBox.zig"); const InfoLine = @import("tui/components/InfoLine.zig"); const label = @import("tui/components/label.zig"); +const RegularLabel = label.Label(struct {}); +const UpdatableLabel = label.Label(*UiState); const Session = @import("tui/components/Session.zig"); const Text = @import("tui/components/Text.zig"); const UserList = @import("tui/components/UserList.zig"); const TerminalBuffer = @import("tui/TerminalBuffer.zig"); const termbox = TerminalBuffer.termbox; -const StringList = std.ArrayListUnmanaged([]const u8); - -const Label = label.Label; const ly_version_str = "Ly version " ++ build_options.version; var session_pid: std.posix.pid_t = -1; @@ -64,7 +65,6 @@ fn ttyControlTransferSignalHandler(_: c_int) callconv(.c) void { TerminalBuffer.shutdownStatic(); } -const NoType = struct {}; const UiState = struct { auth_fails: u64, update: bool, @@ -72,20 +72,21 @@ const UiState = struct { labels_max_length: usize, animation_timed_out: bool, animation: *?Animation, - shutdown_label: *Label(NoType), - restart_label: *Label(NoType), - sleep_label: *Label(NoType), - hibernate_label: *Label(NoType), - brightness_down_label: *Label(NoType), - brightness_up_label: *Label(NoType), - numlock_label: *Label(*UiState), - capslock_label: *Label(*UiState), - battery_label: *Label(*UiState), - clock_label: *Label(*UiState), - session_specifier_label: *Label(*UiState), - login_label: *Label(NoType), - password_label: *Label(NoType), - version_label: *Label(NoType), + shutdown_label: *RegularLabel, + restart_label: *RegularLabel, + sleep_label: *RegularLabel, + hibernate_label: *RegularLabel, + brightness_down_label: *RegularLabel, + brightness_up_label: *RegularLabel, + numlock_label: *UpdatableLabel, + capslock_label: *UpdatableLabel, + battery_label: *UpdatableLabel, + clock_label: *UpdatableLabel, + session_specifier_label: *UpdatableLabel, + login_label: *RegularLabel, + password_label: *RegularLabel, + version_label: *RegularLabel, + bigclock_label: *BigclockLabel, box: *CenteredBox, info_line: *InfoLine, animate: bool, @@ -100,7 +101,9 @@ const UiState = struct { lang: Lang, log_file: *LogFile, 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 { @@ -336,7 +339,7 @@ pub fn main() !void { std.posix.sigaction(std.posix.SIG.TERM, &act, null); // Initialize components - var shutdown_label = Label(NoType).init( + var shutdown_label = RegularLabel.init( "", null, buffer.fg, @@ -345,7 +348,7 @@ pub fn main() !void { ); defer shutdown_label.deinit(allocator); - var restart_label = Label(NoType).init( + var restart_label = RegularLabel.init( "", null, buffer.fg, @@ -354,7 +357,7 @@ pub fn main() !void { ); defer restart_label.deinit(allocator); - var sleep_label = Label(NoType).init( + var sleep_label = RegularLabel.init( "", null, buffer.fg, @@ -363,7 +366,7 @@ pub fn main() !void { ); defer sleep_label.deinit(allocator); - var hibernate_label = Label(NoType).init( + var hibernate_label = RegularLabel.init( "", null, buffer.fg, @@ -372,7 +375,7 @@ pub fn main() !void { ); defer hibernate_label.deinit(allocator); - var brightness_down_label = Label(NoType).init( + var brightness_down_label = RegularLabel.init( "", null, buffer.fg, @@ -381,7 +384,7 @@ pub fn main() !void { ); defer brightness_down_label.deinit(allocator); - var brightness_up_label = Label(NoType).init( + var brightness_up_label = RegularLabel.init( "", null, buffer.fg, @@ -431,7 +434,7 @@ pub fn main() !void { } } - var numlock_label = Label(*UiState).init( + var numlock_label = UpdatableLabel.init( "", null, buffer.fg, @@ -440,7 +443,7 @@ pub fn main() !void { ); defer numlock_label.deinit(null); - var capslock_label = Label(*UiState).init( + var capslock_label = UpdatableLabel.init( "", null, buffer.fg, @@ -449,7 +452,7 @@ pub fn main() !void { ); defer capslock_label.deinit(null); - var battery_label = Label(*UiState).init( + var battery_label = UpdatableLabel.init( "", null, buffer.fg, @@ -458,7 +461,7 @@ pub fn main() !void { ); defer battery_label.deinit(null); - var clock_label = Label(*UiState).init( + var clock_label = UpdatableLabel.init( "", null, buffer.fg, @@ -467,6 +470,20 @@ pub fn main() !void { ); defer clock_label.deinit(null); + var bigclock_label = BigclockLabel.init( + &buffer, + "", + null, + buffer.fg, + buffer.bg, + switch (config.bigclock) { + .none, .en => .en, + .fa => .fa, + }, + &updateBigClock, + ); + defer bigclock_label.deinit(null); + var box = CenteredBox.init( &buffer, config.margin_box_h, @@ -532,7 +549,7 @@ pub fn main() !void { var login: UserList = undefined; - var session_specifier_label = Label(*UiState).init( + var session_specifier_label = UpdatableLabel.init( "", null, buffer.fg, @@ -552,7 +569,7 @@ pub fn main() !void { ); defer session.deinit(); - var login_label = Label(NoType).init( + var login_label = RegularLabel.init( lang.login, null, buffer.fg, @@ -633,7 +650,7 @@ pub fn main() !void { try log_file.err("sys", "no users found", .{}); } - var password_label = Label(NoType).init( + var password_label = RegularLabel.init( lang.password, null, buffer.fg, @@ -653,7 +670,7 @@ pub fn main() !void { ); defer password.deinit(); - var version_label = Label(NoType).init( + var version_label = RegularLabel.init( ly_version_str, null, buffer.fg, @@ -713,6 +730,7 @@ pub fn main() !void { .login_label = &login_label, .password_label = &password_label, .version_label = &version_label, + .bigclock_label = &bigclock_label, .box = &box, .info_line = &info_line, .animate = config.animation != .none, @@ -730,7 +748,9 @@ pub fn main() !void { .lang = lang, .log_file = &log_file, .battery_buf = undefined, + .bigclock_format_buf = undefined, .clock_buf = undefined, + .bigclock_buf = undefined, }; // Load last saved username and desktop selection, if any @@ -1204,6 +1224,10 @@ fn updateComponents(state: *UiState) !void { try state.clock_label.update(state); } + if (state.config.bigclock != .none) { + try state.bigclock_label.update(state); + } + try state.session_specifier_label.update(state); if (!state.config.hide_keyboard_locks) { @@ -1230,32 +1254,9 @@ fn drawUi(log_file: *LogFile, state: *UiState) !bool { try TerminalBuffer.clearScreenStatic(false); if (!state.animation_timed_out) if (state.animation.*) |*a| a.draw(); - if (!state.config.hide_version_string) state.version_label.draw(); - if (state.config.battery_id != null) state.battery_label.draw(); - - if (state.config.bigclock != .none and state.box.height + (bigclock.HEIGHT + 2) * 2 < state.buffer.height) { - var format_buf: [16:0]u8 = undefined; - var clock_buf: [32:0]u8 = undefined; - // We need the slice/c-string returned by `bufPrintZ`. - const format = try std.fmt.bufPrintZ(&format_buf, "{s}{s}{s}{s}", .{ - if (state.config.bigclock_12hr) "%I" else "%H", - ":%M", - if (state.config.bigclock_seconds) ":%S" else "", - if (state.config.bigclock_12hr) "%P" else "", - }); - const xo = state.buffer.width / 2 - @min(state.buffer.width, (format.len * (bigclock.WIDTH + 1))) / 2; - const yo = (state.buffer.height - state.box.height) / 2 - bigclock.HEIGHT - 2; - - const clock_str = interop.timeAsString(&clock_buf, format); - - for (clock_str, 0..) |c, i| { - // TODO: Show error - const clock_cell = try bigclock.clockCell(state.animate, c, state.buffer.fg, state.buffer.bg, state.config.bigclock); - bigclock.alphaBlit(xo + i * (bigclock.WIDTH + 1), yo, state.buffer.width, state.buffer.height, clock_cell); - } - } + if (state.config.bigclock != .none) state.bigclock_label.draw(); state.box.draw(); @@ -1308,7 +1309,7 @@ fn drawUi(log_file: *LogFile, state: *UiState) !bool { return true; } -fn updateNumlock(self: *Label(*UiState), state: *UiState) !void { +fn updateNumlock(self: *UpdatableLabel, state: *UiState) !void { const lock_state = interop.getLockState() catch |err| { self.update_fn = null; try state.info_line.addMessage(state.lang.err_lock_state, state.config.error_bg, state.config.error_fg); @@ -1319,7 +1320,7 @@ fn updateNumlock(self: *Label(*UiState), state: *UiState) !void { self.setText(if (lock_state.numlock) state.lang.numlock else ""); } -fn updateCapslock(self: *Label(*UiState), state: *UiState) !void { +fn updateCapslock(self: *UpdatableLabel, state: *UiState) !void { const lock_state = interop.getLockState() catch |err| { self.update_fn = null; try state.info_line.addMessage(state.lang.err_lock_state, state.config.error_bg, state.config.error_fg); @@ -1330,7 +1331,7 @@ fn updateCapslock(self: *Label(*UiState), state: *UiState) !void { self.setText(if (lock_state.capslock) state.lang.capslock else ""); } -fn updateBattery(self: *Label(*UiState), state: *UiState) !void { +fn updateBattery(self: *UpdatableLabel, state: *UiState) !void { if (state.config.battery_id) |id| { const battery_percentage = getBatteryPercentage(id) catch |err| { self.update_fn = null; @@ -1347,7 +1348,7 @@ fn updateBattery(self: *Label(*UiState), state: *UiState) !void { } } -fn updateClock(self: *Label(*UiState), state: *UiState) !void { +fn updateClock(self: *UpdatableLabel, state: *UiState) !void { if (state.config.clock) |clock| draw_clock: { const clock_str = interop.timeAsString(&state.clock_buf, clock); @@ -1362,7 +1363,30 @@ fn updateClock(self: *Label(*UiState), state: *UiState) !void { } } -fn updateSessionSpecifier(self: *Label(*UiState), state: *UiState) !void { +fn updateBigClock(self: *BigclockLabel, state: *UiState) !void { + if (state.box.height + (bigLabel.CHAR_HEIGHT + 2) * 2 >= state.buffer.height) return; + + const time = try interop.getTimeOfDay(); + const animate_time = @divTrunc(time.microseconds, 500_000); + const separator = if (state.animate and animate_time != 0) " " else ":"; + const format = try std.fmt.bufPrintZ( + &state.bigclock_format_buf, + "{s}{s}{s}{s}{s}{s}", + .{ + if (state.config.bigclock_12hr) "%I" else "%H", + separator, + "%M", + if (state.config.bigclock_seconds) separator else "", + if (state.config.bigclock_seconds) "%S" else "", + if (state.config.bigclock_12hr) "%P" else "", + }, + ); + + const clock_str = interop.timeAsString(&state.bigclock_buf, format); + self.setText(clock_str); +} + +fn updateSessionSpecifier(self: *UpdatableLabel, state: *UiState) !void { const env = state.session.label.list.items[state.session.label.current]; self.setText(env.environment.specifier); } @@ -1409,6 +1433,13 @@ fn positionComponents(state: *UiState) void { state.box.positionXY(TerminalBuffer.START_POSITION); + if (state.config.bigclock != .none) { + state.bigclock_label.positionXY(Position.init( + state.buffer.width / 2 - @min(state.buffer.width, (state.bigclock_label.text.len * (bigLabel.CHAR_WIDTH + 1))) / 2, + (state.buffer.height - state.box.height) / 2 - bigLabel.CHAR_HEIGHT - 2, + ).add(TerminalBuffer.START_POSITION)); + } + state.info_line.label.positionY(state.box .childrenPosition()); diff --git a/src/tui/components/bigLabel.zig b/src/tui/components/bigLabel.zig new file mode 100644 index 0000000..6e9feae --- /dev/null +++ b/src/tui/components/bigLabel.zig @@ -0,0 +1,210 @@ +const std = @import("std"); +const Allocator = std.mem.Allocator; + +const ly_core = @import("ly-core"); +const interop = ly_core.interop; + +const en = @import("bigLabelLocales/en.zig"); +const fa = @import("bigLabelLocales/fa.zig"); +const Cell = @import("../Cell.zig"); +const Position = @import("../Position.zig"); +const TerminalBuffer = @import("../TerminalBuffer.zig"); +const termbox = TerminalBuffer.termbox; + +pub const CHAR_WIDTH = 5; +pub const CHAR_HEIGHT = 5; +pub const CHAR_SIZE = CHAR_WIDTH * CHAR_HEIGHT; +pub const X: u32 = if (ly_core.interop.supportsUnicode()) 0x2593 else '#'; +pub const O: u32 = 0; + +// zig fmt: off +pub const LocaleChars = struct { + ZERO: [CHAR_SIZE]u21, + ONE: [CHAR_SIZE]u21, + TWO: [CHAR_SIZE]u21, + THREE: [CHAR_SIZE]u21, + FOUR: [CHAR_SIZE]u21, + FIVE: [CHAR_SIZE]u21, + SIX: [CHAR_SIZE]u21, + SEVEN: [CHAR_SIZE]u21, + EIGHT: [CHAR_SIZE]u21, + NINE: [CHAR_SIZE]u21, + S: [CHAR_SIZE]u21, + E: [CHAR_SIZE]u21, + P: [CHAR_SIZE]u21, + A: [CHAR_SIZE]u21, + M: [CHAR_SIZE]u21, +}; +// zig fmt: on + +pub const BigLabelLocale = enum { + en, + fa, +}; + +pub fn BigLabel(comptime ContextType: type) type { + return struct { + const Self = @This(); + + buffer: *TerminalBuffer, + text: []const u8, + max_width: ?usize, + fg: u32, + bg: u32, + locale: BigLabelLocale, + update_fn: ?*const fn (*Self, ContextType) anyerror!void, + is_text_allocated: bool, + component_pos: Position, + children_pos: Position, + + pub fn init( + buffer: *TerminalBuffer, + text: []const u8, + max_width: ?usize, + fg: u32, + bg: u32, + locale: BigLabelLocale, + update_fn: ?*const fn (*Self, ContextType) anyerror!void, + ) Self { + return .{ + .buffer = buffer, + .text = text, + .max_width = max_width, + .fg = fg, + .bg = bg, + .locale = locale, + .update_fn = update_fn, + .is_text_allocated = false, + .component_pos = TerminalBuffer.START_POSITION, + .children_pos = TerminalBuffer.START_POSITION, + }; + } + + pub fn setTextAlloc( + self: *Self, + allocator: Allocator, + comptime fmt: []const u8, + args: anytype, + ) !void { + self.text = try std.fmt.allocPrint(allocator, fmt, args); + self.is_text_allocated = true; + } + + pub fn setTextBuf( + self: *Self, + buffer: []u8, + comptime fmt: []const u8, + args: anytype, + ) !void { + self.text = try std.fmt.bufPrint(buffer, fmt, args); + self.is_text_allocated = false; + } + + pub fn setText(self: *Self, text: []const u8) void { + self.text = text; + self.is_text_allocated = false; + } + + pub fn deinit(self: Self, allocator: ?Allocator) void { + if (self.is_text_allocated) { + if (allocator) |alloc| alloc.free(self.text); + } + } + + pub fn positionX(self: *Self, original_pos: Position) void { + self.component_pos = original_pos; + self.children_pos = original_pos.addX(self.text.len * CHAR_WIDTH); + } + + pub fn positionY(self: *Self, original_pos: Position) void { + self.component_pos = original_pos; + self.children_pos = original_pos.addY(CHAR_HEIGHT); + } + + pub fn positionXY(self: *Self, original_pos: Position) void { + self.component_pos = original_pos; + self.children_pos = Position.init( + self.text.len * CHAR_WIDTH, + CHAR_HEIGHT, + ).add(original_pos); + } + + pub fn childrenPosition(self: Self) Position { + return self.children_pos; + } + + pub fn draw(self: Self) void { + for (self.text, 0..) |c, i| { + const clock_cell = clockCell( + c, + self.fg, + self.bg, + self.locale, + ); + + alphaBlit( + self.component_pos.x + i * (CHAR_WIDTH + 1), + self.component_pos.y, + self.buffer.width, + self.buffer.height, + clock_cell, + ); + } + } + + pub fn update(self: *Self, context: ContextType) !void { + if (self.update_fn) |update_fn| { + return @call( + .auto, + update_fn, + .{ self, context }, + ); + } + } + + fn clockCell(char: u8, fg: u32, bg: u32, locale: BigLabelLocale) [CHAR_SIZE]Cell { + var cells: [CHAR_SIZE]Cell = undefined; + + //@divTrunc(time.microseconds, 500000) != 0) + const clock_chars = toBigNumber(char, locale); + for (0..cells.len) |i| cells[i] = Cell.init(clock_chars[i], fg, bg); + + return cells; + } + + fn alphaBlit(x: usize, y: usize, tb_width: usize, tb_height: usize, cells: [CHAR_SIZE]Cell) void { + if (x + CHAR_WIDTH >= tb_width or y + CHAR_HEIGHT >= tb_height) return; + + for (0..CHAR_HEIGHT) |yy| { + for (0..CHAR_WIDTH) |xx| { + const cell = cells[yy * CHAR_WIDTH + xx]; + cell.put(x + xx, y + yy); + } + } + } + + fn toBigNumber(char: u8, locale: BigLabelLocale) [CHAR_SIZE]u21 { + const locale_chars = switch (locale) { + .fa => fa.locale_chars, + .en => en.locale_chars, + }; + return switch (char) { + '0' => locale_chars.ZERO, + '1' => locale_chars.ONE, + '2' => locale_chars.TWO, + '3' => locale_chars.THREE, + '4' => locale_chars.FOUR, + '5' => locale_chars.FIVE, + '6' => locale_chars.SIX, + '7' => locale_chars.SEVEN, + '8' => locale_chars.EIGHT, + '9' => locale_chars.NINE, + 'p', 'P' => locale_chars.P, + 'a', 'A' => locale_chars.A, + 'm', 'M' => locale_chars.M, + ':' => locale_chars.S, + else => locale_chars.E, + }; + } + }; +} diff --git a/src/bigclock/en.zig b/src/tui/components/bigLabelLocales/en.zig similarity index 93% rename from src/bigclock/en.zig rename to src/tui/components/bigLabelLocales/en.zig index 5f3b146..261227c 100644 --- a/src/bigclock/en.zig +++ b/src/tui/components/bigLabelLocales/en.zig @@ -1,7 +1,7 @@ -const Lang = @import("Lang.zig"); -const LocaleChars = Lang.LocaleChars; -const X = Lang.X; -const O = Lang.O; +const bigLabel = @import("../bigLabel.zig"); +const LocaleChars = bigLabel.LocaleChars; +const X = bigLabel.X; +const O = bigLabel.O; // zig fmt: off pub const locale_chars = LocaleChars{ diff --git a/src/bigclock/fa.zig b/src/tui/components/bigLabelLocales/fa.zig similarity index 93% rename from src/bigclock/fa.zig rename to src/tui/components/bigLabelLocales/fa.zig index 9b626b8..ea48737 100644 --- a/src/bigclock/fa.zig +++ b/src/tui/components/bigLabelLocales/fa.zig @@ -1,7 +1,7 @@ -const Lang = @import("Lang.zig"); -const LocaleChars = Lang.LocaleChars; -const X = Lang.X; -const O = Lang.O; +const bigLabel = @import("../bigLabel.zig"); +const LocaleChars = bigLabel.LocaleChars; +const X = bigLabel.X; +const O = bigLabel.O; // zig fmt: off pub const locale_chars = LocaleChars{ From e9e2d512615234d6ecf7ac70abeed7e251ce4541 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Sun, 8 Feb 2026 21:36:18 +0100 Subject: [PATCH 046/176] Better bigclock positioning Signed-off-by: AnErrupTion --- src/main.zig | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/main.zig b/src/main.zig index c3b9ca5..0efb5d4 100644 --- a/src/main.zig +++ b/src/main.zig @@ -1434,10 +1434,15 @@ fn positionComponents(state: *UiState) void { state.box.positionXY(TerminalBuffer.START_POSITION); if (state.config.bigclock != .none) { - state.bigclock_label.positionXY(Position.init( - state.buffer.width / 2 - @min(state.buffer.width, (state.bigclock_label.text.len * (bigLabel.CHAR_WIDTH + 1))) / 2, - (state.buffer.height - state.box.height) / 2 - bigLabel.CHAR_HEIGHT - 2, - ).add(TerminalBuffer.START_POSITION)); + const half_width = state.buffer.width / 2; + const half_label_width = (state.bigclock_label.text.len * (bigLabel.CHAR_WIDTH + 1)) / 2; + const half_height = (if (state.buffer.height > state.box.height) state.buffer.height - state.box.height else state.buffer.height) / 2; + + state.bigclock_label.positionXY(TerminalBuffer.START_POSITION + .addX(half_width) + .removeXIf(half_label_width, half_width > half_label_width) + .addY(half_height) + .removeYIf(bigLabel.CHAR_HEIGHT + 2, half_height > bigLabel.CHAR_HEIGHT + 2)); } state.info_line.label.positionY(state.box From 941b7e0dae0f4e82809d93eb005e834f4c61d444 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Sun, 8 Feb 2026 22:04:09 +0100 Subject: [PATCH 047/176] Properly calculate string lengths Signed-off-by: AnErrupTion --- src/main.zig | 16 ++++++++-------- src/tui/TerminalBuffer.zig | 13 ++++++++----- src/tui/components/InfoLine.zig | 4 ++-- src/tui/components/Session.zig | 2 +- src/tui/components/Text.zig | 7 ++++--- src/tui/components/UserList.zig | 2 +- src/tui/components/bigLabel.zig | 4 ++-- src/tui/components/label.zig | 4 ++-- 8 files changed, 28 insertions(+), 24 deletions(-) diff --git a/src/main.zig b/src/main.zig index 0efb5d4..f7e5777 100644 --- a/src/main.zig +++ b/src/main.zig @@ -310,7 +310,7 @@ pub fn main() !void { // Initialize terminal buffer try log_file.info("tui", "initializing terminal buffer", .{}); - const labels_max_length = @max(lang.login.len, lang.password.len); + const labels_max_length = @max(TerminalBuffer.strWidth(lang.login), TerminalBuffer.strWidth(lang.password)); var seed: u64 = undefined; std.crypto.random.bytes(std.mem.asBytes(&seed)); // Get a random seed for the PRNG (used by animations) @@ -1419,23 +1419,23 @@ fn positionComponents(state: *UiState) void { state.clock_label.positionXY(state.edge_margin .add(TerminalBuffer.START_POSITION) .invertX(state.buffer.width) - .removeXIf(state.clock_label.text.len, state.buffer.width > state.clock_label.text.len + state.edge_margin.x)); + .removeXIf(TerminalBuffer.strWidth(state.clock_label.text), state.buffer.width > TerminalBuffer.strWidth(state.clock_label.text) + state.edge_margin.x)); state.numlock_label.positionX(state.edge_margin .add(TerminalBuffer.START_POSITION) .addYFromIf(state.clock_label.childrenPosition(), state.config.clock != null) .removeYFromIf(state.edge_margin, state.config.clock != null) .invertX(state.buffer.width) - .removeXIf(state.numlock_label.text.len, state.buffer.width > state.numlock_label.text.len + state.edge_margin.x)); + .removeXIf(TerminalBuffer.strWidth(state.numlock_label.text), state.buffer.width > TerminalBuffer.strWidth(state.numlock_label.text) + state.edge_margin.x)); state.capslock_label.positionX(state.numlock_label .childrenPosition() - .removeX(state.numlock_label.text.len + state.capslock_label.text.len + 1)); + .removeX(TerminalBuffer.strWidth(state.numlock_label.text) + TerminalBuffer.strWidth(state.capslock_label.text) + 1)); state.box.positionXY(TerminalBuffer.START_POSITION); if (state.config.bigclock != .none) { const half_width = state.buffer.width / 2; - const half_label_width = (state.bigclock_label.text.len * (bigLabel.CHAR_WIDTH + 1)) / 2; + const half_label_width = (TerminalBuffer.strWidth(state.bigclock_label.text) * (bigLabel.CHAR_WIDTH + 1)) / 2; const half_height = (if (state.buffer.height > state.box.height) state.buffer.height - state.box.height else state.buffer.height) / 2; state.bigclock_label.positionXY(TerminalBuffer.START_POSITION @@ -1453,7 +1453,7 @@ fn positionComponents(state: *UiState) void { .addY(1)); state.session.label.positionY(state.session_specifier_label .childrenPosition() - .addX(state.labels_max_length - state.session_specifier_label.text.len + 1)); + .addX(state.labels_max_length - TerminalBuffer.strWidth(state.session_specifier_label.text) + 1)); state.login_label.positionX(state.session.label .childrenPosition() @@ -1461,7 +1461,7 @@ fn positionComponents(state: *UiState) void { .addY(1)); state.login.label.positionY(state.login_label .childrenPosition() - .addX(state.labels_max_length - state.login_label.text.len + 1)); + .addX(state.labels_max_length - TerminalBuffer.strWidth(state.login_label.text) + 1)); state.password_label.positionX(state.login.label .childrenPosition() @@ -1469,7 +1469,7 @@ fn positionComponents(state: *UiState) void { .addY(1)); state.password.positionY(state.password_label .childrenPosition() - .addX(state.labels_max_length - state.password_label.text.len + 1)); + .addX(state.labels_max_length - TerminalBuffer.strWidth(state.password_label.text) + 1)); state.version_label.positionXY(state.edge_margin .add(TerminalBuffer.START_POSITION) diff --git a/src/tui/TerminalBuffer.zig b/src/tui/TerminalBuffer.zig index 7b322b9..7f62d14 100644 --- a/src/tui/TerminalBuffer.zig +++ b/src/tui/TerminalBuffer.zig @@ -251,13 +251,16 @@ pub fn drawCharMultiple( // Every codepoint is assumed to have a width of 1. // Since Ly is normally running in a TTY, this should be fine. -pub fn strWidth(str: []const u8) !u8 { - const utf8view = try std.unicode.Utf8View.init(str); +pub fn strWidth(str: []const u8) usize { + const utf8view = std.unicode.Utf8View.init(str) catch return str.len; var utf8 = utf8view.iterator(); - var i: c_int = 0; - while (utf8.nextCodepoint()) |codepoint| i += termbox.tb_wcwidth(codepoint); + var length: c_int = 0; - return @intCast(i); + while (utf8.nextCodepoint()) |codepoint| { + length += termbox.tb_wcwidth(codepoint); + } + + return @intCast(length); } fn clearBackBuffer() !void { diff --git a/src/tui/components/InfoLine.zig b/src/tui/components/InfoLine.zig index ecff573..08d0c85 100644 --- a/src/tui/components/InfoLine.zig +++ b/src/tui/components/InfoLine.zig @@ -9,7 +9,7 @@ const MessageLabel = generic.CyclableLabel(Message, Message); const InfoLine = @This(); const Message = struct { - width: u8, + width: usize, text: []const u8, bg: u32, fg: u32, @@ -47,7 +47,7 @@ pub fn addMessage(self: *InfoLine, text: []const u8, bg: u32, fg: u32) !void { if (text.len == 0) return; try self.label.addItem(.{ - .width = try TerminalBuffer.strWidth(text), + .width = TerminalBuffer.strWidth(text), .text = text, .bg = bg, .fg = fg, diff --git a/src/tui/components/Session.zig b/src/tui/components/Session.zig index d468eb0..1f0dcca 100644 --- a/src/tui/components/Session.zig +++ b/src/tui/components/Session.zig @@ -76,7 +76,7 @@ fn sessionChanged(env: Env, maybe_user_list: ?*UserList) void { fn drawItem(label: *EnvironmentLabel, env: Env, x: usize, y: usize, width: usize) void { if (width < 3) return; - const length = @min(env.environment.name.len, width - 3); + const length = @min(TerminalBuffer.strWidth(env.environment.name), width - 3); if (length == 0) return; const x_offset = if (label.text_in_center and width >= length) (width - length) / 2 else 0; diff --git a/src/tui/components/Text.zig b/src/tui/components/Text.zig index 095253e..bc58348 100644 --- a/src/tui/components/Text.zig +++ b/src/tui/components/Text.zig @@ -123,7 +123,7 @@ pub fn draw(self: Text) void { if (self.maybe_mask) |mask| { if (self.width < 1) return; - const length = @min(self.text.items.len, self.width - 1); + const length = @min(TerminalBuffer.strWidth(self.text.items), self.width - 1); if (length == 0) return; TerminalBuffer.drawCharMultiple( @@ -138,11 +138,12 @@ pub fn draw(self: Text) void { return; } - const length = @min(self.text.items.len, self.width); + const str_length = TerminalBuffer.strWidth(self.text.items); + const length = @min(str_length, self.width); if (length == 0) return; const visible_slice = vs: { - if (self.text.items.len > self.width and self.cursor < self.text.items.len) { + if (str_length > self.width and self.cursor < str_length) { break :vs self.text.items[self.visible_start..(self.width + self.visible_start)]; } else { break :vs self.text.items[self.visible_start..]; diff --git a/src/tui/components/UserList.zig b/src/tui/components/UserList.zig index 3bb96b3..4e22951 100644 --- a/src/tui/components/UserList.zig +++ b/src/tui/components/UserList.zig @@ -98,7 +98,7 @@ fn usernameChanged(user: User, maybe_session: ?*Session) void { fn drawItem(label: *UserLabel, user: User, x: usize, y: usize, width: usize) void { if (width < 3) return; - const length = @min(user.name.len, width - 3); + const length = @min(TerminalBuffer.strWidth(user.name), width - 3); if (length == 0) return; const x_offset = if (label.text_in_center and width >= length) (width - length) / 2 else 0; diff --git a/src/tui/components/bigLabel.zig b/src/tui/components/bigLabel.zig index 6e9feae..26b3257 100644 --- a/src/tui/components/bigLabel.zig +++ b/src/tui/components/bigLabel.zig @@ -113,7 +113,7 @@ pub fn BigLabel(comptime ContextType: type) type { pub fn positionX(self: *Self, original_pos: Position) void { self.component_pos = original_pos; - self.children_pos = original_pos.addX(self.text.len * CHAR_WIDTH); + self.children_pos = original_pos.addX(TerminalBuffer.strWidth(self.text) * CHAR_WIDTH); } pub fn positionY(self: *Self, original_pos: Position) void { @@ -124,7 +124,7 @@ pub fn BigLabel(comptime ContextType: type) type { pub fn positionXY(self: *Self, original_pos: Position) void { self.component_pos = original_pos; self.children_pos = Position.init( - self.text.len * CHAR_WIDTH, + TerminalBuffer.strWidth(self.text) * CHAR_WIDTH, CHAR_HEIGHT, ).add(original_pos); } diff --git a/src/tui/components/label.zig b/src/tui/components/label.zig index ba483bb..21b7a99 100644 --- a/src/tui/components/label.zig +++ b/src/tui/components/label.zig @@ -71,7 +71,7 @@ pub fn Label(comptime ContextType: type) type { pub fn positionX(self: *Self, original_pos: Position) void { self.component_pos = original_pos; - self.children_pos = original_pos.addX(self.text.len); + self.children_pos = original_pos.addX(TerminalBuffer.strWidth(self.text)); } pub fn positionY(self: *Self, original_pos: Position) void { @@ -82,7 +82,7 @@ pub fn Label(comptime ContextType: type) type { pub fn positionXY(self: *Self, original_pos: Position) void { self.component_pos = original_pos; self.children_pos = Position.init( - self.text.len, + TerminalBuffer.strWidth(self.text), 1, ).add(original_pos); } From 1db780c7a7016658260df31ae63852985709608d Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Sun, 8 Feb 2026 22:18:00 +0100 Subject: [PATCH 048/176] Better handle screen resizes Signed-off-by: AnErrupTion --- src/main.zig | 68 +++++++++++++++++--------------------- src/tui/TerminalBuffer.zig | 14 +++++--- 2 files changed, 39 insertions(+), 43 deletions(-) diff --git a/src/main.zig b/src/main.zig index f7e5777..f8dbcbd 100644 --- a/src/main.zig +++ b/src/main.zig @@ -856,35 +856,37 @@ pub fn main() !void { } while (run) { - // If there's no input or there's an animation, a resolution change needs to be checked - if (!state.update or state.animate or config.bigclock != .none or config.clock != null) { - if (!state.update) std.Thread.sleep(std.time.ns_per_ms * 100); + if (state.resolution_changed) { + state.buffer.width = TerminalBuffer.getWidthStatic(); + state.buffer.height = TerminalBuffer.getHeightStatic(); - // Required to update tb_width() and tb_height() - const new_dimensions = TerminalBuffer.presentBufferStatic(); - const width = new_dimensions.width; - const height = new_dimensions.height; + try log_file.info("tui", "screen resolution updated to {d}x{d}", .{ state.buffer.width, state.buffer.height }); - if (width != state.buffer.width or height != state.buffer.height) { - // If it did change, then update the cell buffer, reallocate the current animation's buffers, and force a draw update - try log_file.info("tui", "screen resolution updated to {d}x{d}", .{ width, height }); + if (state.animation.*) |*a| a.realloc() catch |err| { + try info_line.addMessage(lang.err_alloc, config.error_bg, config.error_fg); + try log_file.err("tui", "failed to reallocate animation buffers: {s}", .{@errorName(err)}); + }; - state.buffer.width = width; - state.buffer.height = height; + positionComponents(&state); - if (state.animation.*) |*a| a.realloc() catch |err| { - try info_line.addMessage(lang.err_alloc, config.error_bg, config.error_fg); - try log_file.err("tui", "failed to reallocate animation buffers: {s}", .{@errorName(err)}); - }; - - state.update = true; - state.resolution_changed = true; - } + state.update = true; + state.resolution_changed = false; } if (state.update) { try updateComponents(&state); - if (!try drawUi(&log_file, &state)) continue; + + 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 log_file.err("tui", "failed to handle password input: {s}", .{@errorName(err)}); + }, + } + + if (!try drawUi(&state)) continue; } var timeout: i32 = -1; @@ -949,7 +951,12 @@ pub fn main() !void { state.update = timeout != -1; - if (event_error < 0 or event.type != termbox.TB_EVENT_KEY) continue; + if (event_error < 0) continue; + } + + if (event.type == termbox.TB_EVENT_RESIZE) { + state.resolution_changed = true; + continue; } // Input of some kind was detected, so reset the inactivity timer @@ -1236,7 +1243,7 @@ fn updateComponents(state: *UiState) !void { } } -fn drawUi(log_file: *LogFile, state: *UiState) !bool { +fn drawUi(state: *UiState) !bool { // If the user entered a wrong password 10 times in a row, play a cascade animation, else update normally if (state.config.auth_fails > 0 and state.auth_fails >= state.config.auth_fails) { std.Thread.sleep(std.time.ns_per_ms * 10); @@ -1260,21 +1267,6 @@ fn drawUi(log_file: *LogFile, state: *UiState) !bool { state.box.draw(); - if (state.resolution_changed) { - positionComponents(state); - state.resolution_changed = false; - } - - 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 log_file.err("tui", "failed to handle password input: {s}", .{@errorName(err)}); - }, - } - if (state.config.clock != null) state.clock_label.draw(); state.session_specifier_label.draw(); diff --git a/src/tui/TerminalBuffer.zig b/src/tui/TerminalBuffer.zig index 7f62d14..391fc60 100644 --- a/src/tui/TerminalBuffer.zig +++ b/src/tui/TerminalBuffer.zig @@ -133,6 +133,14 @@ pub fn init(options: InitOptions, log_file: *LogFile, random: Random) !TerminalB }; } +pub fn getWidthStatic() usize { + return @intCast(termbox.tb_width()); +} + +pub fn getHeightStatic() usize { + return @intCast(termbox.tb_height()); +} + pub fn setCursorStatic(x: usize, y: usize) void { _ = termbox.tb_set_cursor(@intCast(x), @intCast(y)); } @@ -146,12 +154,8 @@ pub fn shutdownStatic() void { _ = termbox.tb_shutdown(); } -pub fn presentBufferStatic() struct { width: usize, height: usize } { +pub fn presentBufferStatic() void { _ = termbox.tb_present(); - return .{ - .width = @intCast(termbox.tb_width()), - .height = @intCast(termbox.tb_height()), - }; } pub fn reclaim(self: TerminalBuffer) !void { From cf5f62661ccdc4a62da1cdcf67e46f53c07ec280 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Sun, 8 Feb 2026 22:23:07 +0100 Subject: [PATCH 049/176] Remove resolution_changed bool Signed-off-by: AnErrupTion --- src/main.zig | 33 +++++++++++++-------------------- 1 file changed, 13 insertions(+), 20 deletions(-) diff --git a/src/main.zig b/src/main.zig index f8dbcbd..bb114ce 100644 --- a/src/main.zig +++ b/src/main.zig @@ -90,7 +90,6 @@ const UiState = struct { box: *CenteredBox, info_line: *InfoLine, animate: bool, - resolution_changed: bool, session: *Session, login: *UserList, password: *Text, @@ -734,7 +733,6 @@ pub fn main() !void { .box = &box, .info_line = &info_line, .animate = config.animation != .none, - .resolution_changed = false, .session = &session, .login = &login, .password = &password, @@ -856,23 +854,6 @@ pub fn main() !void { } while (run) { - if (state.resolution_changed) { - state.buffer.width = TerminalBuffer.getWidthStatic(); - state.buffer.height = TerminalBuffer.getHeightStatic(); - - try log_file.info("tui", "screen resolution updated to {d}x{d}", .{ state.buffer.width, state.buffer.height }); - - if (state.animation.*) |*a| a.realloc() catch |err| { - try info_line.addMessage(lang.err_alloc, config.error_bg, config.error_fg); - try log_file.err("tui", "failed to reallocate animation buffers: {s}", .{@errorName(err)}); - }; - - positionComponents(&state); - - state.update = true; - state.resolution_changed = false; - } - if (state.update) { try updateComponents(&state); @@ -955,7 +936,19 @@ pub fn main() !void { } if (event.type == termbox.TB_EVENT_RESIZE) { - state.resolution_changed = true; + state.buffer.width = TerminalBuffer.getWidthStatic(); + state.buffer.height = TerminalBuffer.getHeightStatic(); + + try log_file.info("tui", "screen resolution updated to {d}x{d}", .{ state.buffer.width, state.buffer.height }); + + if (state.animation.*) |*a| a.realloc() catch |err| { + try info_line.addMessage(lang.err_alloc, config.error_bg, config.error_fg); + try log_file.err("tui", "failed to reallocate animation buffers: {s}", .{@errorName(err)}); + }; + + positionComponents(&state); + + state.update = true; continue; } From 852a602032743c79017be93a5dcf5b28edbb0898 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Mon, 9 Feb 2026 11:11:00 +0100 Subject: [PATCH 050/176] 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; +} From d1810d8c98c9640e94cc5fc8f84a77d61bbcbf92 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Mon, 9 Feb 2026 11:20:27 +0100 Subject: [PATCH 051/176] Fix numlock & capslock positioning Signed-off-by: AnErrupTion --- res/config.ini | 12 ++++++------ src/main.zig | 4 ++-- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/res/config.ini b/res/config.ini index b3f28c5..31ffce6 100644 --- a/res/config.ini +++ b/res/config.ini @@ -101,13 +101,13 @@ box_title = null # Brightness decrease command brightness_down_cmd = $PREFIX_DIRECTORY/bin/brightnessctl -q -n s 10%- -# Brightness decrease key, or null to disable +# Brightness decrease key combination, or null to disable brightness_down_key = F5 # Brightness increase command brightness_up_cmd = $PREFIX_DIRECTORY/bin/brightnessctl -q -n s +10% -# Brightness increase key, or null to disable +# Brightness increase key combination, or null to disable brightness_up_key = F6 # Erase password input on failure @@ -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 +# Specifies the key combination 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 +# Specifies the key combination 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 +# Specifies the key combination used for shutdown shutdown_key = F1 # Command executed when pressing sleep key (can be null) sleep_cmd = null -# Specifies the key used for sleep +# Specifies the key combination 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 382bd66..3c8456e 100644 --- a/src/main.zig +++ b/src/main.zig @@ -1603,10 +1603,10 @@ fn positionComponents(state: *UiState) void { .addYFromIf(state.clock_label.childrenPosition(), state.config.clock != null) .removeYFromIf(state.edge_margin, state.config.clock != null) .invertX(state.buffer.width) - .removeXIf(TerminalBuffer.strWidth(state.numlock_label.text), state.buffer.width > TerminalBuffer.strWidth(state.numlock_label.text) + state.edge_margin.x)); + .removeXIf(TerminalBuffer.strWidth(state.lang.numlock), state.buffer.width > TerminalBuffer.strWidth(state.lang.numlock) + state.edge_margin.x)); state.capslock_label.positionX(state.numlock_label .childrenPosition() - .removeX(TerminalBuffer.strWidth(state.numlock_label.text) + TerminalBuffer.strWidth(state.capslock_label.text) + 1)); + .removeX(TerminalBuffer.strWidth(state.lang.numlock) + TerminalBuffer.strWidth(state.lang.capslock) + 1)); state.box.positionXY(TerminalBuffer.START_POSITION); From 99dba44e463b354e5cacdf23932ba7f10f866925 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Mon, 9 Feb 2026 11:38:56 +0100 Subject: [PATCH 052/176] Rename min_refresh_delta option to animation_frame_delay (closes #925) Signed-off-by: AnErrupTion --- res/config.ini | 6 +++--- src/config/Config.zig | 2 +- src/config/migrator.zig | 8 ++++++++ src/main.zig | 12 ++++++------ 4 files changed, 18 insertions(+), 10 deletions(-) diff --git a/res/config.ini b/res/config.ini index 31ffce6..9e917f1 100644 --- a/res/config.ini +++ b/res/config.ini @@ -27,6 +27,9 @@ allow_empty_password = true # dur_file -> .dur file format (https://github.com/cmang/durdraw/tree/master) animation = none +# Delay between each animation frame in milliseconds +animation_frame_delay = 5 + # Stop the animation after some time # 0 -> Run forever # 1..2e12 -> Stop the animation after this many seconds @@ -291,9 +294,6 @@ margin_box_h = 2 # Main box vertical margin margin_box_v = 1 -# Event timeout in milliseconds -min_refresh_delta = 5 - # Set numlock on/off at startup numlock = false diff --git a/src/config/Config.zig b/src/config/Config.zig index 51dca91..1672dbb 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -9,6 +9,7 @@ const DurOffsetAlignment = enums.DurOffsetAlignment; allow_empty_password: bool = true, animation: Animation = .none, +animation_frame_delay: u16 = 5, animation_timeout_sec: u12 = 0, asterisk: ?u32 = '*', auth_fails: u64 = 10, @@ -73,7 +74,6 @@ logout_cmd: ?[]const u8 = null, ly_log: []const u8 = "/var/log/ly.log", margin_box_h: u8 = 2, margin_box_v: u8 = 1, -min_refresh_delta: u16 = 5, numlock: bool = false, path: ?[]const u8 = "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", restart_cmd: []const u8 = "/sbin/shutdown -r now", diff --git a/src/config/migrator.zig b/src/config/migrator.zig index 7761d70..5e6394d 100644 --- a/src/config/migrator.zig +++ b/src/config/migrator.zig @@ -153,6 +153,14 @@ pub fn configFieldHandler(_: std.mem.Allocator, field: ini.IniField) ?ini.IniFie return field; } + if (std.mem.eql(u8, field.key, "min_refresh_delta")) { + // The option has simply been renamed + var mapped_field = field; + mapped_field.key = "animation_frame_delay"; + + return mapped_field; + } + return field; } diff --git a/src/main.zig b/src/main.zig index 3c8456e..4c1614e 100644 --- a/src/main.zig +++ b/src/main.zig @@ -916,7 +916,7 @@ pub fn main() !void { // Calculate the maximum timeout based on current animations, or the (big) clock. If there's none, we wait for the event indefinitely instead if (state.animate and !state.animation_timed_out) { - timeout = config.min_refresh_delta; + timeout = config.animation_frame_delay; // Check how long we've been running so we can turn off the animation const time = try interop.getTimeOfDay(); @@ -1132,7 +1132,7 @@ fn authenticate(ptr: *anyopaque) !bool { ); }; state.info_line.label.draw(); - _ = TerminalBuffer.presentBufferStatic(); + TerminalBuffer.presentBufferStatic(); return false; } @@ -1154,7 +1154,7 @@ fn authenticate(ptr: *anyopaque) !bool { ); }; state.info_line.label.draw(); - _ = TerminalBuffer.presentBufferStatic(); + TerminalBuffer.presentBufferStatic(); if (state.config.save) save_last_settings: { // It isn't worth cluttering the code with precise error @@ -1301,7 +1301,7 @@ fn authenticate(ptr: *anyopaque) !bool { // Restore the cursor TerminalBuffer.setCursorStatic(0, 0); - _ = TerminalBuffer.presentBufferStatic(); + TerminalBuffer.presentBufferStatic(); return false; } @@ -1439,7 +1439,7 @@ fn drawUi(state: *UiState) !bool { state.auth_fails = 0; } - _ = TerminalBuffer.presentBufferStatic(); + TerminalBuffer.presentBufferStatic(); return false; } @@ -1482,7 +1482,7 @@ fn drawUi(state: *UiState) !bool { state.login.label.draw(); state.password.draw(); - _ = TerminalBuffer.presentBufferStatic(); + TerminalBuffer.presentBufferStatic(); return true; } From 207b352888b953d0f4ec740ff6a90e1cfd17be14 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Tue, 10 Feb 2026 00:22:27 +0100 Subject: [PATCH 053/176] Add central Widget struct + clean up code In particular, move all termbox2 usage to TerminalBuffer.zig & keyboard.zig Signed-off-by: AnErrupTion --- src/animations/ColorMix.zig | 17 +- src/animations/Doom.zig | 15 +- src/animations/DurFile.zig | 15 +- src/animations/GameOfLife.zig | 15 +- src/animations/Matrix.zig | 15 +- src/main.zig | 1194 ++++++++++++--------- src/tui/Animation.zig | 61 -- src/tui/Cell.zig | 3 +- src/tui/TerminalBuffer.zig | 43 +- src/tui/Widget.zig | 150 +++ src/tui/components/BigLabel.zig | 215 ++++ src/tui/components/CenteredBox.zig | 59 +- src/tui/components/InfoLine.zig | 21 + src/tui/components/Label.zig | 131 +++ src/tui/components/Session.zig | 21 + src/tui/components/Text.zig | 69 +- src/tui/components/UserList.zig | 21 + src/tui/components/bigLabel.zig | 210 ---- src/tui/components/bigLabelLocales/en.zig | 8 +- src/tui/components/bigLabelLocales/fa.zig | 8 +- src/tui/components/generic.zig | 52 +- src/tui/components/label.zig | 126 --- src/tui/keyboard.zig | 16 +- 23 files changed, 1456 insertions(+), 1029 deletions(-) delete mode 100644 src/tui/Animation.zig create mode 100644 src/tui/Widget.zig create mode 100644 src/tui/components/BigLabel.zig create mode 100644 src/tui/components/Label.zig delete mode 100644 src/tui/components/bigLabel.zig delete mode 100644 src/tui/components/label.zig diff --git a/src/animations/ColorMix.zig b/src/animations/ColorMix.zig index 3adefb0..21edc09 100644 --- a/src/animations/ColorMix.zig +++ b/src/animations/ColorMix.zig @@ -1,9 +1,9 @@ const std = @import("std"); const math = std.math; -const Animation = @import("../tui/Animation.zig"); const Cell = @import("../tui/Cell.zig"); const TerminalBuffer = @import("../tui/TerminalBuffer.zig"); +const Widget = @import("../tui/Widget.zig"); const ColorMix = @This(); @@ -45,14 +45,17 @@ pub fn init(terminal_buffer: *TerminalBuffer, col1: u32, col2: u32, col3: u32) C }; } -pub fn animation(self: *ColorMix) Animation { - return Animation.init(self, deinit, realloc, draw); +pub fn widget(self: *ColorMix) Widget { + return Widget.init( + self, + null, + null, + draw, + null, + null, + ); } -fn deinit(_: *ColorMix) void {} - -fn realloc(_: *ColorMix) anyerror!void {} - fn draw(self: *ColorMix) void { self.frames +%= 1; const time: f32 = @as(f32, @floatFromInt(self.frames)) * time_scale; diff --git a/src/animations/Doom.zig b/src/animations/Doom.zig index 008ba25..e0a09b1 100644 --- a/src/animations/Doom.zig +++ b/src/animations/Doom.zig @@ -1,9 +1,9 @@ const std = @import("std"); const Allocator = std.mem.Allocator; -const Animation = @import("../tui/Animation.zig"); const Cell = @import("../tui/Cell.zig"); const TerminalBuffer = @import("../tui/TerminalBuffer.zig"); +const Widget = @import("../tui/Widget.zig"); const Doom = @This(); @@ -49,15 +49,22 @@ pub fn init(allocator: Allocator, terminal_buffer: *TerminalBuffer, top_color: u }; } -pub fn animation(self: *Doom) Animation { - return Animation.init(self, deinit, realloc, draw); +pub fn widget(self: *Doom) Widget { + return Widget.init( + self, + deinit, + realloc, + draw, + null, + null, + ); } fn deinit(self: *Doom) void { self.allocator.free(self.buffer); } -fn realloc(self: *Doom) anyerror!void { +fn realloc(self: *Doom) !void { const buffer = try self.allocator.realloc(self.buffer, self.terminal_buffer.width * self.terminal_buffer.height); initBuffer(buffer, self.terminal_buffer.width); self.buffer = buffer; diff --git a/src/animations/DurFile.zig b/src/animations/DurFile.zig index 24d66c2..f3f2eb2 100644 --- a/src/animations/DurFile.zig +++ b/src/animations/DurFile.zig @@ -9,11 +9,11 @@ const LogFile = ly_core.LogFile; const enums = @import("../enums.zig"); const DurOffsetAlignment = enums.DurOffsetAlignment; -const Animation = @import("../tui/Animation.zig"); const Cell = @import("../tui/Cell.zig"); const TerminalBuffer = @import("../tui/TerminalBuffer.zig"); const Color = TerminalBuffer.Color; const Styling = TerminalBuffer.Styling; +const Widget = @import("../tui/Widget.zig"); fn read_decompress_file(allocator: Allocator, file_path: []const u8) ![]u8 { const file_buffer = std.fs.cwd().openFile(file_path, .{}) catch { @@ -403,15 +403,22 @@ pub fn init(allocator: Allocator, terminal_buffer: *TerminalBuffer, log_file: *L }; } -pub fn animation(self: *DurFile) Animation { - return Animation.init(self, deinit, realloc, draw); +pub fn widget(self: *DurFile) Widget { + return Widget.init( + self, + deinit, + realloc, + draw, + null, + null, + ); } fn deinit(self: *DurFile) void { self.dur_movie.deinit(); } -fn realloc(self: *DurFile) anyerror!void { +fn realloc(self: *DurFile) !void { // when terminal size changes, we need to recalculate the start_pos and frame_size based on the new size self.start_pos = calc_start_position(self.terminal_buffer, &self.dur_movie, self.offset_alignment, self.offset); self.frame_size = calc_frame_size(self.terminal_buffer, &self.dur_movie); diff --git a/src/animations/GameOfLife.zig b/src/animations/GameOfLife.zig index 8dbcb85..ac87226 100644 --- a/src/animations/GameOfLife.zig +++ b/src/animations/GameOfLife.zig @@ -1,9 +1,9 @@ const std = @import("std"); const Allocator = std.mem.Allocator; -const Animation = @import("../tui/Animation.zig"); const Cell = @import("../tui/Cell.zig"); const TerminalBuffer = @import("../tui/TerminalBuffer.zig"); +const Widget = @import("../tui/Widget.zig"); const GameOfLife = @This(); @@ -60,8 +60,15 @@ pub fn init(allocator: Allocator, terminal_buffer: *TerminalBuffer, fg_color: u3 return game; } -pub fn animation(self: *GameOfLife) Animation { - return Animation.init(self, deinit, realloc, draw); +pub fn widget(self: *GameOfLife) Widget { + return Widget.init( + self, + deinit, + realloc, + draw, + null, + null, + ); } fn deinit(self: *GameOfLife) void { @@ -69,7 +76,7 @@ fn deinit(self: *GameOfLife) void { self.allocator.free(self.next_grid); } -fn realloc(self: *GameOfLife) anyerror!void { +fn realloc(self: *GameOfLife) !void { const new_width = self.terminal_buffer.width; const new_height = self.terminal_buffer.height; const new_size = new_width * new_height; diff --git a/src/animations/Matrix.zig b/src/animations/Matrix.zig index 0ea4940..a3c4138 100644 --- a/src/animations/Matrix.zig +++ b/src/animations/Matrix.zig @@ -2,9 +2,9 @@ const std = @import("std"); const Allocator = std.mem.Allocator; const Random = std.Random; -const Animation = @import("../tui/Animation.zig"); const Cell = @import("../tui/Cell.zig"); const TerminalBuffer = @import("../tui/TerminalBuffer.zig"); +const Widget = @import("../tui/Widget.zig"); pub const FRAME_DELAY: usize = 8; @@ -57,8 +57,15 @@ pub fn init(allocator: Allocator, terminal_buffer: *TerminalBuffer, fg: u32, hea }; } -pub fn animation(self: *Matrix) Animation { - return Animation.init(self, deinit, realloc, draw); +pub fn widget(self: *Matrix) Widget { + return Widget.init( + self, + deinit, + realloc, + draw, + null, + null, + ); } fn deinit(self: *Matrix) void { @@ -66,7 +73,7 @@ fn deinit(self: *Matrix) void { self.allocator.free(self.lines); } -fn realloc(self: *Matrix) anyerror!void { +fn realloc(self: *Matrix) !void { const dots = try self.allocator.realloc(self.dots, self.terminal_buffer.width * (self.terminal_buffer.height + 1)); const lines = try self.allocator.realloc(self.lines, self.terminal_buffer.width); diff --git a/src/main.zig b/src/main.zig index 4c1614e..c5294d8 100644 --- a/src/main.zig +++ b/src/main.zig @@ -30,20 +30,17 @@ const enums = @import("enums.zig"); const DisplayServer = enums.DisplayServer; const Environment = @import("Environment.zig"); const Entry = Environment.Entry; -const Animation = @import("tui/Animation.zig"); const Position = @import("tui/Position.zig"); -const bigLabel = @import("tui/components/bigLabel.zig"); -const BigclockLabel = bigLabel.BigLabel(*UiState); +const BigLabel = @import("tui/components/BigLabel.zig"); const CenteredBox = @import("tui/components/CenteredBox.zig"); const InfoLine = @import("tui/components/InfoLine.zig"); -const label = @import("tui/components/label.zig"); -const RegularLabel = label.Label(struct {}); -const UpdatableLabel = label.Label(*UiState); +const Label = @import("tui/components/Label.zig"); const Session = @import("tui/components/Session.zig"); const Text = @import("tui/components/Text.zig"); const UserList = @import("tui/components/UserList.zig"); const TerminalBuffer = @import("tui/TerminalBuffer.zig"); const termbox = TerminalBuffer.termbox; +const Widget = @import("tui/Widget.zig"); const ly_version_str = "Ly version " ++ build_options.version; @@ -58,12 +55,12 @@ fn signalHandler(i: c_int) callconv(.c) void { _ = std.c.waitpid(session_pid, &status, 0); } - TerminalBuffer.shutdownStatic(); + TerminalBuffer.shutdown(); std.c.exit(i); } fn ttyControlTransferSignalHandler(_: c_int) callconv(.c) void { - TerminalBuffer.shutdownStatic(); + TerminalBuffer.shutdown(); } const UiState = struct { @@ -74,40 +71,41 @@ const UiState = struct { is_autologin: bool, use_kmscon_vt: bool, active_tty: u8, - buffer: *TerminalBuffer, + buffer: TerminalBuffer, labels_max_length: usize, animation_timed_out: bool, - animation: *?Animation, - shutdown_label: *RegularLabel, - restart_label: *RegularLabel, - sleep_label: *RegularLabel, - hibernate_label: *RegularLabel, - brightness_down_label: *RegularLabel, - brightness_up_label: *RegularLabel, - numlock_label: *UpdatableLabel, - capslock_label: *UpdatableLabel, - battery_label: *UpdatableLabel, - clock_label: *UpdatableLabel, - session_specifier_label: *UpdatableLabel, - login_label: *RegularLabel, - password_label: *RegularLabel, - version_label: *RegularLabel, - bigclock_label: *BigclockLabel, - box: *CenteredBox, - info_line: *InfoLine, + animation: ?Widget, + shutdown_label: Label, + restart_label: Label, + sleep_label: Label, + hibernate_label: Label, + brightness_down_label: Label, + brightness_up_label: Label, + numlock_label: Label, + capslock_label: Label, + battery_label: Label, + clock_label: Label, + session_specifier_label: Label, + login_label: Label, + password_label: Label, + version_label: Label, + bigclock_label: BigLabel, + box: CenteredBox, + info_line: InfoLine, animate: bool, - session: *Session, + session: Session, saved_users: SavedUsers, - login: *UserList, - password: *Text, + login: UserList, + password: Text, active_input: enums.Input, insert_mode: bool, edge_margin: Position, config: Config, lang: Lang, - log_file: *LogFile, + log_file: LogFile, save_path: []const u8, - old_save_path: ?[]const u8, + old_save_path: []const u8, + has_old_save: bool, battery_buf: [16:0]u8, bigclock_format_buf: [16:0]u8, clock_buf: [64:0]u8, @@ -121,6 +119,7 @@ pub fn main() !void { var shutdown_cmd: []const u8 = undefined; var restart_cmd: []const u8 = undefined; var commands_allocated = false; + var state: UiState = undefined; var stderr_buffer: [128]u8 = undefined; var stderr_writer = std.fs.File.stderr().writer(&stderr_buffer); @@ -149,7 +148,7 @@ pub fn main() !void { var gpa = std.heap.DebugAllocator(.{}).init; defer _ = gpa.deinit(); - const allocator = gpa.allocator(); + state.allocator = gpa.allocator(); // Allows stopping an animation after some time const animation_time_start = try interop.getTimeOfDay(); @@ -164,7 +163,7 @@ pub fn main() !void { var diag = clap.Diagnostic{}; var arg_parse_error: anyerror = undefined; - var maybe_res = clap.parse(clap.Help, ¶ms, clap.parsers.default, .{ .diagnostic = &diag, .allocator = allocator }) catch |err| parse_error: { + var maybe_res = clap.parse(clap.Help, ¶ms, clap.parsers.default, .{ .diagnostic = &diag, .allocator = state.allocator }) catch |err| parse_error: { arg_parse_error = err; diag.report(stderr, err) catch {}; try stderr.flush(); @@ -175,11 +174,12 @@ pub fn main() !void { var old_save_parser: ?IniParser(OldSave) = null; defer if (old_save_parser) |*str| str.deinit(); - var use_kmscon_vt = false; + state.use_kmscon_vt = false; + var start_cmd_exit_code: u8 = 0; - var saved_users = SavedUsers.init(); - defer saved_users.deinit(allocator); + state.saved_users = SavedUsers.init(); + defer state.saved_users.deinit(state.allocator); var config_parent_path: []const u8 = build_options.config_directory ++ "/ly"; if (maybe_res) |*res| { @@ -196,61 +196,67 @@ pub fn main() !void { std.process.exit(0); } if (res.args.config) |path| config_parent_path = path; - if (res.args.@"use-kmscon-vt" != 0) use_kmscon_vt = true; + if (res.args.@"use-kmscon-vt" != 0) state.use_kmscon_vt = true; } // Load configuration file - var save_path: []const u8 = build_options.config_directory ++ "/ly/save.txt"; - var old_save_path: []const u8 = build_options.config_directory ++ "/ly/save.ini"; var save_path_alloc = false; + + state.save_path = build_options.config_directory ++ "/ly/save.txt"; + state.old_save_path = build_options.config_directory ++ "/ly/save.ini"; defer if (save_path_alloc) { - allocator.free(save_path); - allocator.free(old_save_path); + state.allocator.free(state.save_path); + state.allocator.free(state.old_save_path); }; - const config_path = try std.fs.path.join(allocator, &[_][]const u8{ config_parent_path, "config.ini" }); - defer allocator.free(config_path); + const config_path = try std.fs.path.join(state.allocator, &[_][]const u8{ config_parent_path, "config.ini" }); + defer state.allocator.free(config_path); - var config_parser = try IniParser(Config).init(allocator, config_path, migrator.configFieldHandler); + var config_parser = try IniParser(Config).init(state.allocator, config_path, migrator.configFieldHandler); defer config_parser.deinit(); - var config = config_parser.structure; + state.config = config_parser.structure; var lang_buffer: [16]u8 = undefined; - const lang_file = try std.fmt.bufPrint(&lang_buffer, "{s}.ini", .{config.lang}); + const lang_file = try std.fmt.bufPrint(&lang_buffer, "{s}.ini", .{state.config.lang}); - const lang_path = try std.fs.path.join(allocator, &[_][]const u8{ config_parent_path, "lang", lang_file }); - defer allocator.free(lang_path); + const lang_path = try std.fs.path.join(state.allocator, &[_][]const u8{ config_parent_path, "lang", lang_file }); + defer state.allocator.free(lang_path); - var lang_parser = try IniParser(Lang).init(allocator, lang_path, null); + var lang_parser = try IniParser(Lang).init(state.allocator, lang_path, null); defer lang_parser.deinit(); - const lang = lang_parser.structure; + state.lang = lang_parser.structure; - if (config.save) { - save_path = try std.fs.path.join(allocator, &[_][]const u8{ config_parent_path, "save.txt" }); - old_save_path = try std.fs.path.join(allocator, &[_][]const u8{ config_parent_path, "save.ini" }); + if (state.config.save) { + state.save_path = try std.fs.path.join(state.allocator, &[_][]const u8{ config_parent_path, "save.txt" }); + state.old_save_path = try std.fs.path.join(state.allocator, &[_][]const u8{ config_parent_path, "save.ini" }); save_path_alloc = true; } if (config_parser.maybe_load_error == null) { - migrator.lateConfigFieldHandler(&config); + migrator.lateConfigFieldHandler(&state.config); } var maybe_uid_range_error: ?anyerror = null; - var usernames = try getAllUsernames(allocator, config.login_defs_path, &maybe_uid_range_error); + var usernames = try getAllUsernames(state.allocator, state.config.login_defs_path, &maybe_uid_range_error); defer { - for (usernames.items) |username| allocator.free(username); - usernames.deinit(allocator); + for (usernames.items) |username| state.allocator.free(username); + usernames.deinit(state.allocator); } - if (config.save) read_save_file: { - old_save_parser = migrator.tryMigrateIniSaveFile(allocator, old_save_path, &saved_users, usernames.items) catch break :read_save_file; + state.has_old_save = false; + + if (state.config.save) read_save_file: { + old_save_parser = migrator.tryMigrateIniSaveFile(state.allocator, state.old_save_path, &state.saved_users, usernames.items) catch break :read_save_file; // Don't read the new save file if the old one still exists - if (old_save_parser != null) break :read_save_file; + if (old_save_parser != null) { + state.has_old_save = true; + break :read_save_file; + } - var save_file = std.fs.cwd().openFile(save_path, .{}) catch break :read_save_file; + var save_file = std.fs.cwd().openFile(state.save_path, .{}) catch break :read_save_file; defer save_file.close(); var file_buffer: [256]u8 = undefined; @@ -258,7 +264,7 @@ pub fn main() !void { var reader = &file_reader.interface; const last_username_index_str = reader.takeDelimiterInclusive('\n') catch break :read_save_file; - saved_users.last_username_index = std.fmt.parseInt(usize, last_username_index_str[0..(last_username_index_str.len - 1)], 10) catch break :read_save_file; + state.saved_users.last_username_index = std.fmt.parseInt(usize, last_username_index_str[0..(last_username_index_str.len - 1)], 10) catch break :read_save_file; while (reader.seek < reader.buffer.len) { const line = reader.takeDelimiterInclusive('\n') catch break; @@ -269,8 +275,8 @@ pub fn main() !void { const session_index = std.fmt.parseInt(usize, session_index_str, 10) catch continue; - try saved_users.user_list.append(allocator, .{ - .username = try allocator.dupe(u8, username), + try state.saved_users.user_list.append(state.allocator, .{ + .username = try state.allocator.dupe(u8, username), .session_index = session_index, .first_run = false, .allocated_username = true, @@ -280,9 +286,9 @@ pub fn main() !void { // If no save file previously existed, fill it up with all usernames // TODO: Add new username with existing save file - if (config.save and saved_users.user_list.items.len == 0) { + if (state.config.save and state.saved_users.user_list.items.len == 0) { for (usernames.items) |user| { - try saved_users.user_list.append(allocator, .{ + try state.saved_users.user_list.append(state.allocator, .{ .username = user, .session_index = 0, .first_run = true, @@ -293,19 +299,19 @@ pub fn main() !void { var log_file_buffer: [1024]u8 = undefined; - var log_file = try LogFile.init(config.ly_log, &log_file_buffer); - defer log_file.deinit(); + state.log_file = try LogFile.init(state.config.ly_log, &log_file_buffer); + defer state.log_file.deinit(); - try log_file.info("tui", "using {s} vt", .{if (use_kmscon_vt) "kmscon" else "default"}); + try state.log_file.info("tui", "using {s} vt", .{if (state.use_kmscon_vt) "kmscon" else "default"}); // These strings only end up getting freed if the user quits Ly using Ctrl+C, which is fine since in the other cases // we end up shutting down or restarting the system - shutdown_cmd = try temporary_allocator.dupe(u8, config.shutdown_cmd); - restart_cmd = try temporary_allocator.dupe(u8, config.restart_cmd); + shutdown_cmd = try temporary_allocator.dupe(u8, state.config.shutdown_cmd); + restart_cmd = try temporary_allocator.dupe(u8, state.config.restart_cmd); commands_allocated = true; - if (config.start_cmd) |start_cmd| { - var start = std.process.Child.init(&[_][]const u8{ "/bin/sh", "-c", start_cmd }, allocator); + if (state.config.start_cmd) |start_cmd| { + var start = std.process.Child.init(&[_][]const u8{ "/bin/sh", "-c", start_cmd }, state.allocator); start.stdout_behavior = .Ignore; start.stderr_behavior = .Ignore; @@ -318,8 +324,8 @@ pub fn main() !void { } // Initialize terminal buffer - try log_file.info("tui", "initializing terminal buffer", .{}); - const labels_max_length = @max(TerminalBuffer.strWidth(lang.login), TerminalBuffer.strWidth(lang.password)); + try state.log_file.info("tui", "initializing terminal buffer", .{}); + state.labels_max_length = @max(TerminalBuffer.strWidth(state.lang.login), TerminalBuffer.strWidth(state.lang.password)); var seed: u64 = undefined; std.crypto.random.bytes(std.mem.asBytes(&seed)); // Get a random seed for the PRNG (used by animations) @@ -328,21 +334,21 @@ pub fn main() !void { const random = prng.random(); const buffer_options = TerminalBuffer.InitOptions{ - .fg = config.fg, - .bg = config.bg, - .border_fg = config.border_fg, - .full_color = config.full_color, + .fg = state.config.fg, + .bg = state.config.bg, + .border_fg = state.config.border_fg, + .full_color = state.config.full_color, .is_tty = true, }; - var buffer = try TerminalBuffer.init( - allocator, + state.buffer = try TerminalBuffer.init( + state.allocator, buffer_options, - &log_file, + &state.log_file, random, ); defer { - log_file.info("tui", "shutting down terminal buffer", .{}) catch {}; - buffer.deinit(); + state.log_file.info("tui", "shutting down terminal buffer", .{}) catch {}; + state.buffer.deinit(); } const act = std.posix.Sigaction{ @@ -353,306 +359,395 @@ pub fn main() !void { std.posix.sigaction(std.posix.SIG.TERM, &act, null); // Initialize components - var shutdown_label = RegularLabel.init( + state.shutdown_label = Label.init( "", null, - buffer.fg, - buffer.bg, + state.buffer.fg, + state.buffer.bg, null, ); - defer shutdown_label.deinit(allocator); + defer state.shutdown_label.deinit(); - var restart_label = RegularLabel.init( + state.restart_label = Label.init( "", null, - buffer.fg, - buffer.bg, + state.buffer.fg, + state.buffer.bg, null, ); - defer restart_label.deinit(allocator); + defer state.restart_label.deinit(); - var sleep_label = RegularLabel.init( + state.sleep_label = Label.init( "", null, - buffer.fg, - buffer.bg, + state.buffer.fg, + state.buffer.bg, null, ); - defer sleep_label.deinit(allocator); + defer state.sleep_label.deinit(); - var hibernate_label = RegularLabel.init( + state.hibernate_label = Label.init( "", null, - buffer.fg, - buffer.bg, + state.buffer.fg, + state.buffer.bg, null, ); - defer hibernate_label.deinit(allocator); + defer state.hibernate_label.deinit(); - var brightness_down_label = RegularLabel.init( + state.brightness_down_label = Label.init( "", null, - buffer.fg, - buffer.bg, + state.buffer.fg, + state.buffer.bg, null, ); - defer brightness_down_label.deinit(allocator); + defer state.brightness_down_label.deinit(); - var brightness_up_label = RegularLabel.init( + state.brightness_up_label = Label.init( "", null, - buffer.fg, - buffer.bg, + state.buffer.fg, + state.buffer.bg, null, ); - defer brightness_up_label.deinit(allocator); + defer state.brightness_up_label.deinit(); - if (!config.hide_key_hints) { - try shutdown_label.setTextAlloc( - allocator, + if (!state.config.hide_key_hints) { + try state.shutdown_label.setTextAlloc( + state.allocator, "{s} {s}", - .{ config.shutdown_key, lang.shutdown }, + .{ state.config.shutdown_key, state.lang.shutdown }, ); - try restart_label.setTextAlloc( - allocator, + try state.restart_label.setTextAlloc( + state.allocator, "{s} {s}", - .{ config.restart_key, lang.restart }, + .{ state.config.restart_key, state.lang.restart }, ); - if (config.sleep_cmd != null) { - try sleep_label.setTextAlloc( - allocator, + if (state.config.sleep_cmd != null) { + try state.sleep_label.setTextAlloc( + state.allocator, "{s} {s}", - .{ config.sleep_key, lang.sleep }, + .{ state.config.sleep_key, state.lang.sleep }, ); } - if (config.hibernate_cmd != null) { - try hibernate_label.setTextAlloc( - allocator, + if (state.config.hibernate_cmd != null) { + try state.hibernate_label.setTextAlloc( + state.allocator, "{s} {s}", - .{ config.hibernate_key, lang.hibernate }, + .{ state.config.hibernate_key, state.lang.hibernate }, ); } - if (config.brightness_down_key) |key| { - try brightness_down_label.setTextAlloc( - allocator, + if (state.config.brightness_down_key) |key| { + try state.brightness_down_label.setTextAlloc( + state.allocator, "{s} {s}", - .{ key, lang.brightness_down }, + .{ key, state.lang.brightness_down }, ); } - if (config.brightness_up_key) |key| { - try brightness_up_label.setTextAlloc( - allocator, + if (state.config.brightness_up_key) |key| { + try state.brightness_up_label.setTextAlloc( + state.allocator, "{s} {s}", - .{ key, lang.brightness_up }, + .{ key, state.lang.brightness_up }, ); } } - var numlock_label = UpdatableLabel.init( + state.numlock_label = Label.init( "", null, - buffer.fg, - buffer.bg, + state.buffer.fg, + state.buffer.bg, &updateNumlock, ); - defer numlock_label.deinit(null); + defer state.numlock_label.deinit(); - var capslock_label = UpdatableLabel.init( + state.capslock_label = Label.init( "", null, - buffer.fg, - buffer.bg, + state.buffer.fg, + state.buffer.bg, &updateCapslock, ); - defer capslock_label.deinit(null); + defer state.capslock_label.deinit(); - var battery_label = UpdatableLabel.init( + state.battery_label = Label.init( "", null, - buffer.fg, - buffer.bg, + state.buffer.fg, + state.buffer.bg, &updateBattery, ); - defer battery_label.deinit(null); + defer state.battery_label.deinit(); - var clock_label = UpdatableLabel.init( + state.clock_label = Label.init( "", null, - buffer.fg, - buffer.bg, + state.buffer.fg, + state.buffer.bg, &updateClock, ); - defer clock_label.deinit(null); + defer state.clock_label.deinit(); - var bigclock_label = BigclockLabel.init( - &buffer, + state.bigclock_label = BigLabel.init( + &state.buffer, "", null, - buffer.fg, - buffer.bg, - switch (config.bigclock) { + state.buffer.fg, + state.buffer.bg, + switch (state.config.bigclock) { .none, .en => .en, .fa => .fa, }, &updateBigClock, ); - defer bigclock_label.deinit(null); + defer state.bigclock_label.deinit(); - var box = CenteredBox.init( - &buffer, - config.margin_box_h, - config.margin_box_v, - (2 * config.margin_box_h) + config.input_len + 1 + labels_max_length, - 7 + (2 * config.margin_box_v), - !config.hide_borders, - config.blank_box, - config.box_title, + state.box = CenteredBox.init( + &state.buffer, + state.config.margin_box_h, + state.config.margin_box_v, + (2 * state.config.margin_box_h) + state.config.input_len + 1 + state.labels_max_length, + 7 + (2 * state.config.margin_box_v), + !state.config.hide_borders, + state.config.blank_box, + state.config.box_title, null, - buffer.border_fg, - buffer.fg, - buffer.bg, + state.buffer.border_fg, + state.buffer.fg, + state.buffer.bg, ); - var info_line = InfoLine.init( - allocator, - &buffer, - box.width - 2 * box.horizontal_margin, - buffer.fg, - buffer.bg, + state.info_line = InfoLine.init( + state.allocator, + &state.buffer, + state.box.width - 2 * state.box.horizontal_margin, + state.buffer.fg, + state.buffer.bg, ); - defer info_line.deinit(); + defer state.info_line.deinit(); if (maybe_res == null) { var longest = diag.name.longest(); if (longest.kind == .positional) longest.name = diag.arg; - try info_line.addMessage(lang.err_args, config.error_bg, config.error_fg); - try log_file.err("cli", "unable to parse argument '{s}{s}': {s}", .{ longest.kind.prefix(), longest.name, @errorName(arg_parse_error) }); + try state.info_line.addMessage( + state.lang.err_args, + state.config.error_bg, + state.config.error_fg, + ); + try state.log_file.err( + "cli", + "unable to parse argument '{s}{s}': {s}", + .{ longest.kind.prefix(), longest.name, @errorName(arg_parse_error) }, + ); } if (maybe_uid_range_error) |err| { - try info_line.addMessage(lang.err_uid_range, config.error_bg, config.error_fg); - try log_file.err("sys", "failed to get uid range: {s}; falling back to default", .{@errorName(err)}); + try state.info_line.addMessage( + state.lang.err_uid_range, + state.config.error_bg, + state.config.error_fg, + ); + try state.log_file.err( + "sys", + "failed to get uid range: {s}; falling back to default", + .{@errorName(err)}, + ); } if (start_cmd_exit_code != 0) { - try info_line.addMessage(lang.err_start, config.error_bg, config.error_fg); - try log_file.err("sys", "failed to execute start command: exit code {d}", .{start_cmd_exit_code}); + try state.info_line.addMessage( + state.lang.err_start, + state.config.error_bg, + state.config.error_fg, + ); + try state.log_file.err( + "sys", + "failed to execute start command: exit code {d}", + .{start_cmd_exit_code}, + ); } if (config_parser.maybe_load_error) |load_error| { // We can't localize this since the config failed to load so we'd fallback to the default language anyway - try info_line.addMessage("unable to parse config file", config.error_bg, config.error_fg); - try log_file.err("conf", "unable to parse config file: {s}", .{@errorName(load_error)}); + try state.info_line.addMessage( + "unable to parse config file", + state.config.error_bg, + state.config.error_fg, + ); + try state.log_file.err( + "conf", + "unable to parse config file: {s}", + .{@errorName(load_error)}, + ); for (config_parser.errors.items) |err| { - try log_file.err("conf", "failed to convert value '{s}' of option '{s}' to type '{s}': {s}", .{ err.value, err.key, err.type_name, err.error_name }); + try state.log_file.err( + "conf", + "failed to convert value '{s}' of option '{s}' to type '{s}': {s}", + .{ err.value, err.key, err.type_name, err.error_name }, + ); } } - if (!log_file.could_open_log_file) { - try info_line.addMessage(lang.err_log, config.error_bg, config.error_fg); - try log_file.err("sys", "failed to open log file", .{}); + if (!state.log_file.could_open_log_file) { + try state.info_line.addMessage( + state.lang.err_log, + state.config.error_bg, + state.config.error_fg, + ); + try state.log_file.err( + "sys", + "failed to open log file", + .{}, + ); } - interop.setNumlock(config.numlock) catch |err| { - try info_line.addMessage(lang.err_numlock, config.error_bg, config.error_fg); - try log_file.err("sys", "failed to set numlock: {s}", .{@errorName(err)}); + interop.setNumlock(state.config.numlock) catch |err| { + try state.info_line.addMessage( + state.lang.err_numlock, + state.config.error_bg, + state.config.error_fg, + ); + try state.log_file.err( + "sys", + "failed to set numlock: {s}", + .{@errorName(err)}, + ); }; - var login: UserList = undefined; - - var session_specifier_label = UpdatableLabel.init( + state.session_specifier_label = Label.init( "", null, - buffer.fg, - buffer.bg, + state.buffer.fg, + state.buffer.bg, &updateSessionSpecifier, ); - defer session_specifier_label.deinit(null); + defer state.session_specifier_label.deinit(); - var session = Session.init( - allocator, - &buffer, - &login, - box.width - 2 * box.horizontal_margin - labels_max_length - 1, - config.text_in_center, - buffer.fg, - buffer.bg, + state.session = Session.init( + state.allocator, + &state.buffer, + &state.login, + state.box.width - 2 * state.box.horizontal_margin - state.labels_max_length - 1, + state.config.text_in_center, + state.buffer.fg, + state.buffer.bg, ); - defer session.deinit(); + defer state.session.deinit(); - var login_label = RegularLabel.init( - lang.login, + state.login_label = Label.init( + state.lang.login, null, - buffer.fg, - buffer.bg, + state.buffer.fg, + state.buffer.bg, null, ); - defer login_label.deinit(null); + defer state.login_label.deinit(); - login = try UserList.init( - allocator, - &buffer, + state.login = try UserList.init( + state.allocator, + &state.buffer, usernames, - &saved_users, - &session, - box.width - 2 * box.horizontal_margin - labels_max_length - 1, - config.text_in_center, - buffer.fg, - buffer.bg, + &state.saved_users, + &state.session, + state.box.width - 2 * state.box.horizontal_margin - state.labels_max_length - 1, + state.config.text_in_center, + state.buffer.fg, + state.buffer.bg, ); - defer login.deinit(); + defer state.login.deinit(); - addOtherEnvironment(&session, lang, .shell, null) catch |err| { - try info_line.addMessage(lang.err_alloc, config.error_bg, config.error_fg); - try log_file.err("sys", "failed to add shell environment: {s}", .{@errorName(err)}); + addOtherEnvironment(&state.session, state.lang, .shell, null) catch |err| { + try state.info_line.addMessage( + state.lang.err_alloc, + state.config.error_bg, + state.config.error_fg, + ); + try state.log_file.err( + "sys", + "failed to add shell environment: {s}", + .{@errorName(err)}, + ); }; if (build_options.enable_x11_support) { - if (config.xinitrc) |xinitrc_cmd| { - addOtherEnvironment(&session, lang, .xinitrc, xinitrc_cmd) catch |err| { - try info_line.addMessage(lang.err_alloc, config.error_bg, config.error_fg); - try log_file.err("sys", "failed to add xinitrc environment: {s}", .{@errorName(err)}); + if (state.config.xinitrc) |xinitrc_cmd| { + addOtherEnvironment(&state.session, state.lang, .xinitrc, xinitrc_cmd) catch |err| { + try state.info_line.addMessage( + state.lang.err_alloc, + state.config.error_bg, + state.config.error_fg, + ); + try state.log_file.err( + "sys", + "failed to add xinitrc environment: {s}", + .{@errorName(err)}, + ); }; } } else { - try info_line.addMessage(lang.no_x11_support, config.bg, config.fg); - try log_file.err("comp", "x11 support disabled at compile-time"); + try state.info_line.addMessage( + state.lang.no_x11_support, + state.config.bg, + state.config.fg, + ); + try state.log_file.info( + "comp", + "x11 support disabled at compile-time", + ); } var has_crawl_error = false; // Crawl session directories (Wayland, X11 and custom respectively) - var wayland_session_dirs = std.mem.splitScalar(u8, config.waylandsessions, ':'); + var wayland_session_dirs = std.mem.splitScalar(u8, state.config.waylandsessions, ':'); while (wayland_session_dirs.next()) |dir| { - crawl(&session, lang, dir, .wayland) catch |err| { + crawl(&state.session, state.lang, dir, .wayland) catch |err| { has_crawl_error = true; - try log_file.err("sys", "failed to crawl wayland session directory '{s}': {s}", .{ dir, @errorName(err) }); + try state.log_file.err( + "sys", + "failed to crawl wayland session directory '{s}': {s}", + .{ dir, @errorName(err) }, + ); }; } if (build_options.enable_x11_support) { - var x_session_dirs = std.mem.splitScalar(u8, config.xsessions, ':'); + var x_session_dirs = std.mem.splitScalar(u8, state.config.xsessions, ':'); while (x_session_dirs.next()) |dir| { - crawl(&session, lang, dir, .x11) catch |err| { + crawl(&state.session, state.lang, dir, .x11) catch |err| { has_crawl_error = true; - try log_file.err("sys", "failed to crawl x11 session directory '{s}': {s}", .{ dir, @errorName(err) }); + try state.log_file.err( + "sys", + "failed to crawl x11 session directory '{s}': {s}", + .{ dir, @errorName(err) }, + ); }; } } - var custom_session_dirs = std.mem.splitScalar(u8, config.custom_sessions, ':'); + var custom_session_dirs = std.mem.splitScalar(u8, state.config.custom_sessions, ':'); while (custom_session_dirs.next()) |dir| { - crawl(&session, lang, dir, .custom) catch |err| { + crawl(&state.session, state.lang, dir, .custom) catch |err| { has_crawl_error = true; - try log_file.err("sys", "failed to crawl custom session directory '{s}': {s}", .{ dir, @errorName(err) }); + try state.log_file.err( + "sys", + "failed to crawl custom session directory '{s}': {s}", + .{ dir, @errorName(err) }, + ); }; } if (has_crawl_error) { - try info_line.addMessage(lang.err_crawl, config.error_bg, config.error_fg); + try state.info_line.addMessage( + state.lang.err_crawl, + state.config.error_bg, + state.config.error_fg, + ); } if (usernames.items.len == 0) { @@ -660,159 +755,260 @@ pub fn main() !void { // This effectively means you can't login, since there would be no local // accounts *and* no root account...but at this point, if that's the // case, you have bigger problems to deal with in the first place. :D - try info_line.addMessage(lang.err_no_users, config.error_bg, config.error_fg); - try log_file.err("sys", "no users found", .{}); + try state.info_line.addMessage(state.lang.err_no_users, state.config.error_bg, state.config.error_fg); + try state.log_file.err("sys", "no users found", .{}); } - var password_label = RegularLabel.init( - lang.password, + state.password_label = Label.init( + state.lang.password, null, - buffer.fg, - buffer.bg, + state.buffer.fg, + state.buffer.bg, null, ); - defer password_label.deinit(null); + defer state.password_label.deinit(); - var password = Text.init( - allocator, - &buffer, + state.password = Text.init( + state.allocator, + &state.buffer, true, - config.asterisk, - box.width - 2 * box.horizontal_margin - labels_max_length - 1, - buffer.fg, - buffer.bg, + state.config.asterisk, + state.box.width - 2 * state.box.horizontal_margin - state.labels_max_length - 1, + state.buffer.fg, + state.buffer.bg, ); - defer password.deinit(); + defer state.password.deinit(); - var version_label = RegularLabel.init( + state.version_label = Label.init( ly_version_str, null, - buffer.fg, - buffer.bg, + state.buffer.fg, + state.buffer.bg, null, ); - defer version_label.deinit(null); + defer state.version_label.deinit(); - var is_autologin = false; + state.is_autologin = false; check_autologin: { - const auto_user = config.auto_login_user orelse break :check_autologin; - const auto_session = config.auto_login_session orelse break :check_autologin; + const auto_user = state.config.auto_login_user orelse break :check_autologin; + const auto_session = state.config.auto_login_session orelse break :check_autologin; if (!isValidUsername(auto_user, usernames)) { - try info_line.addMessage(lang.err_pam_user_unknown, config.error_bg, config.error_fg); - try log_file.err("auth", "autologin failed: username '{s}' not found", .{auto_user}); + try state.info_line.addMessage( + state.lang.err_pam_user_unknown, + state.config.error_bg, + state.config.error_fg, + ); + try state.log_file.err( + "auth", + "autologin failed: username '{s}' not found", + .{auto_user}, + ); break :check_autologin; } - const session_index = findSessionByName(&session, auto_session) orelse { - try log_file.err("auth", "autologin failed: session '{s}' not found", .{auto_session}); - try info_line.addMessage(lang.err_autologin_session, config.error_bg, config.error_fg); + const session_index = findSessionByName(&state.session, auto_session) orelse { + try state.log_file.err( + "auth", + "autologin failed: session '{s}' not found", + .{auto_session}, + ); + try state.info_line.addMessage( + state.lang.err_autologin_session, + state.config.error_bg, + state.config.error_fg, + ); break :check_autologin; }; - try log_file.err("auth", "attempting autologin for user '{s}' with session '{s}'", .{ auto_user, auto_session }); + try state.log_file.info( + "auth", + "attempting autologin for user '{s}' with session '{s}'", + .{ auto_user, auto_session }, + ); - session.label.current = session_index; - for (login.label.list.items, 0..) |username, i| { + state.session.label.current = session_index; + for (state.login.label.list.items, 0..) |username, i| { if (std.mem.eql(u8, username.name, auto_user)) { - login.label.current = i; + state.login.label.current = i; break; } } - is_autologin = true; + state.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)}); + state.active_tty = interop.getActiveTty(state.allocator, state.use_kmscon_vt) catch |err| no_tty_found: { + try state.info_line.addMessage( + state.lang.err_get_active_tty, + state.config.error_bg, + state.config.error_fg, + ); + try state.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 (!state.use_kmscon_vt) { + interop.switchTty(state.active_tty) catch |err| { + try state.info_line.addMessage( + state.lang.err_switch_tty, + state.config.error_bg, + state.config.error_fg, + ); + try state.log_file.err( + "sys", + "failed to switch to tty {d}: {s}", + .{ state.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, - .animation = &animation, - .shutdown_label = &shutdown_label, - .restart_label = &restart_label, - .sleep_label = &sleep_label, - .hibernate_label = &hibernate_label, - .brightness_down_label = &brightness_down_label, - .brightness_up_label = &brightness_up_label, - .numlock_label = &numlock_label, - .capslock_label = &capslock_label, - .battery_label = &battery_label, - .clock_label = &clock_label, - .session_specifier_label = &session_specifier_label, - .login_label = &login_label, - .password_label = &password_label, - .version_label = &version_label, - .bigclock_label = &bigclock_label, - .box = &box, - .info_line = &info_line, - .animate = config.animation != .none, - .session = &session, - .saved_users = saved_users, - .login = &login, - .password = &password, - .active_input = config.default_input, - .insert_mode = !config.vi_mode or config.vi_default_mode == .insert, - .edge_margin = Position.init( - config.edge_margin, - config.edge_margin, - ), - .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, - .bigclock_buf = undefined, - }; + // Initialize the animation, if any + switch (state.config.animation) { + .none => { + state.animation = null; + }, + .doom => { + var doom = try Doom.init( + state.allocator, + &state.buffer, + state.config.doom_top_color, + state.config.doom_middle_color, + state.config.doom_bottom_color, + state.config.doom_fire_height, + state.config.doom_fire_spread, + ); + state.animation = doom.widget(); + }, + .matrix => { + var matrix = try Matrix.init( + state.allocator, + &state.buffer, + state.config.cmatrix_fg, + state.config.cmatrix_head_col, + state.config.cmatrix_min_codepoint, + state.config.cmatrix_max_codepoint, + ); + state.animation = matrix.widget(); + }, + .colormix => { + var color_mix = ColorMix.init( + &state.buffer, + state.config.colormix_col1, + state.config.colormix_col2, + state.config.colormix_col3, + ); + state.animation = color_mix.widget(); + }, + .gameoflife => { + var game_of_life = try GameOfLife.init( + state.allocator, + &state.buffer, + state.config.gameoflife_fg, + state.config.gameoflife_entropy_interval, + state.config.gameoflife_frame_delay, + state.config.gameoflife_initial_density, + ); + state.animation = game_of_life.widget(); + }, + .dur_file => { + var dur = try DurFile.init( + state.allocator, + &state.buffer, + &state.log_file, + state.config.dur_file_path, + state.config.dur_offset_alignment, + state.config.dur_x_offset, + state.config.dur_y_offset, + state.config.full_color, + ); + state.animation = dur.widget(); + }, + } + defer if (state.animation) |*a| a.deinit(); + + 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, + state.config.edge_margin, + ); // Load last saved username and desktop selection, if any // Skip if autologin is active to prevent overriding autologin session - if (config.save and !is_autologin) { - if (saved_users.last_username_index) |index| load_last_user: { + 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 - if (index >= saved_users.user_list.items.len) break :load_last_user; + if (index >= state.saved_users.user_list.items.len) break :load_last_user; - const user = saved_users.user_list.items[index]; + const user = state.saved_users.user_list.items[index]; // Find user with saved name, and switch over to it // If it doesn't exist (anymore), we don't change the value for (usernames.items, 0..) |username, i| { if (std.mem.eql(u8, username, user.username)) { - login.label.current = i; + state.login.label.current = i; break; } } state.active_input = .password; - session.label.current = @min(user.session_index, session.label.list.items.len - 1); + state.session.label.current = @min(user.session_index, state.session.label.list.items.len - 1); } } + var widgets: std.ArrayList(Widget) = .empty; + defer widgets.deinit(state.allocator); + + if (!state.config.hide_key_hints) { + try widgets.append(state.allocator, state.shutdown_label.widget()); + try widgets.append(state.allocator, state.restart_label.widget()); + if (state.config.sleep_cmd != null) { + try widgets.append(state.allocator, state.sleep_label.widget()); + } + if (state.config.brightness_down_key != null) { + try widgets.append(state.allocator, state.brightness_down_label.widget()); + } + if (state.config.brightness_up_key != null) { + try widgets.append(state.allocator, state.brightness_up_label.widget()); + } + } + if (state.config.battery_id != null) { + try widgets.append(state.allocator, state.battery_label.widget()); + } + if (state.config.clock != null) { + try widgets.append(state.allocator, state.clock_label.widget()); + } + if (state.config.bigclock != .none) { + try widgets.append(state.allocator, state.bigclock_label.widget()); + } + if (!state.config.hide_keyboard_locks) { + try widgets.append(state.allocator, state.numlock_label.widget()); + try widgets.append(state.allocator, state.capslock_label.widget()); + } + try widgets.append(state.allocator, state.box.widget()); + try widgets.append(state.allocator, state.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, state.login_label.widget()); + try widgets.append(state.allocator, state.login.widget()); + try widgets.append(state.allocator, state.password_label.widget()); + try widgets.append(state.allocator, state.password.widget()); + if (!state.config.hide_version_string) { + try widgets.append(state.allocator, state.version_label.widget()); + } + // Position components and place cursor accordingly - try updateComponents(&state); + try updateComponents(&state, widgets); positionComponents(&state); switch (state.active_input) { @@ -820,84 +1016,78 @@ pub fn main() !void { .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 info_line.addMessage(lang.err_alloc, config.error_bg, config.error_fg); - try log_file.err("tui", "failed to handle password input: {s}", .{@errorName(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)}, + ); }, } - // Initialize the animation, if any - switch (config.animation) { - .none => {}, - .doom => { - var doom = try Doom.init(allocator, &buffer, config.doom_top_color, config.doom_middle_color, config.doom_bottom_color, config.doom_fire_height, config.doom_fire_spread); - animation = doom.animation(); - }, - .matrix => { - var matrix = try Matrix.init(allocator, &buffer, config.cmatrix_fg, config.cmatrix_head_col, config.cmatrix_min_codepoint, config.cmatrix_max_codepoint); - animation = matrix.animation(); - }, - .colormix => { - var color_mix = ColorMix.init(&buffer, config.colormix_col1, config.colormix_col2, config.colormix_col3); - animation = color_mix.animation(); - }, - .gameoflife => { - var game_of_life = try GameOfLife.init(allocator, &buffer, config.gameoflife_fg, config.gameoflife_entropy_interval, config.gameoflife_frame_delay, config.gameoflife_initial_density); - animation = game_of_life.animation(); - }, - .dur_file => { - var dur = try DurFile.init(allocator, &buffer, &log_file, config.dur_file_path, config.dur_offset_alignment, config.dur_x_offset, config.dur_y_offset, config.full_color); - animation = dur.animation(); - }, - } - defer if (animation) |*a| a.deinit(); + try state.buffer.registerKeybind("Esc", &disableInsertMode); + try state.buffer.registerKeybind("I", &enableInsertMode); - try buffer.registerKeybind("Esc", &disableInsertMode); - try buffer.registerKeybind("I", &enableInsertMode); + try state.buffer.registerKeybind("Ctrl+C", &quit); - try buffer.registerKeybind("Ctrl+C", &quit); + try state.buffer.registerKeybind("Ctrl+U", &clearPassword); - try buffer.registerKeybind("Ctrl+U", &clearPassword); + try state.buffer.registerKeybind("Ctrl+K", &moveCursorUp); + try state.buffer.registerKeybind("Up", &moveCursorUp); + try state.buffer.registerKeybind("J", &viMoseCursorUp); - try buffer.registerKeybind("Ctrl+K", &moveCursorUp); - try buffer.registerKeybind("Up", &moveCursorUp); - try buffer.registerKeybind("J", &viMoseCursorUp); + try state.buffer.registerKeybind("Ctrl+J", &moveCursorDown); + try state.buffer.registerKeybind("Down", &moveCursorDown); + try state.buffer.registerKeybind("K", &viMoveCursorDown); - try buffer.registerKeybind("Ctrl+J", &moveCursorDown); - try buffer.registerKeybind("Down", &moveCursorDown); - try buffer.registerKeybind("K", &viMoveCursorDown); + try state.buffer.registerKeybind("Tab", &wrapCursor); + try state.buffer.registerKeybind("Shift+Tab", &wrapCursorReverse); - try buffer.registerKeybind("Tab", &wrapCursor); - try buffer.registerKeybind("Shift+Tab", &wrapCursorReverse); + try state.buffer.registerKeybind("Enter", &authenticate); - 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); + try state.buffer.registerKeybind(state.config.shutdown_key, &shutdownCmd); + try state.buffer.registerKeybind(state.config.restart_key, &restartCmd); + if (state.config.sleep_cmd != null) try state.buffer.registerKeybind(state.config.sleep_key, &sleepCmd); + if (state.config.hibernate_cmd != null) try state.buffer.registerKeybind(state.config.hibernate_key, &hibernateCmd); + if (state.config.brightness_down_key) |key| try state.buffer.registerKeybind(key, &decreaseBrightnessCmd); + if (state.config.brightness_up_key) |key| try state.buffer.registerKeybind(key, &increaseBrightnessCmd); var event: termbox.tb_event = undefined; var inactivity_time_start = try interop.getTimeOfDay(); var inactivity_cmd_ran = false; - if (config.initial_info_text) |text| { - try info_line.addMessage(text, config.bg, config.fg); + if (state.config.initial_info_text) |text| { + try state.info_line.addMessage(text, state.config.bg, state.config.fg); } else get_host_name: { // Initialize information line with host name var name_buf: [std.posix.HOST_NAME_MAX]u8 = undefined; const hostname = std.posix.gethostname(&name_buf) catch |err| { - try info_line.addMessage(lang.err_hostname, config.error_bg, config.error_fg); - try log_file.err("sys", "failed to get hostname: {s}", .{@errorName(err)}); + try state.info_line.addMessage( + state.lang.err_hostname, + state.config.error_bg, + state.config.error_fg, + ); + try state.log_file.err( + "sys", + "failed to get hostname: {s}", + .{@errorName(err)}, + ); break :get_host_name; }; - try info_line.addMessage(hostname, config.bg, config.fg); + try state.info_line.addMessage( + hostname, + state.config.bg, + state.config.fg, + ); } while (state.run) { if (state.update) { - try updateComponents(&state); + try updateComponents(&state, widgets); switch (state.active_input) { .info_line => state.info_line.label.handle(null, state.insert_mode), @@ -905,41 +1095,41 @@ pub fn main() !void { .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 log_file.err("tui", "failed to handle password input: {s}", .{@errorName(err)}); + try state.log_file.err("tui", "failed to handle password input: {s}", .{@errorName(err)}); }, } - if (!try drawUi(&state)) continue; + if (!try drawUi(&state, widgets)) continue; } var timeout: i32 = -1; // Calculate the maximum timeout based on current animations, or the (big) clock. If there's none, we wait for the event indefinitely instead if (state.animate and !state.animation_timed_out) { - timeout = config.animation_frame_delay; + timeout = state.config.animation_frame_delay; // Check how long we've been running so we can turn off the animation const time = try interop.getTimeOfDay(); - if (config.animation_timeout_sec > 0 and time.seconds - animation_time_start.seconds > config.animation_timeout_sec) { + if (state.config.animation_timeout_sec > 0 and time.seconds - animation_time_start.seconds > state.config.animation_timeout_sec) { state.animation_timed_out = true; - if (state.animation.*) |*a| a.deinit(); + if (state.animation) |*a| a.deinit(); } - } else if (config.bigclock != .none and config.clock == null) { + } else if (state.config.bigclock != .none and state.config.clock == null) { const time = try interop.getTimeOfDay(); timeout = @intCast((60 - @rem(time.seconds, 60)) * 1000 - @divTrunc(time.microseconds, 1000) + 1); - } else if (config.clock != null or (config.auth_fails > 0 and state.auth_fails >= config.auth_fails)) { + } else if (state.config.clock != null or (state.config.auth_fails > 0 and state.auth_fails >= state.config.auth_fails)) { const time = try interop.getTimeOfDay(); timeout = @intCast(1000 - @divTrunc(time.microseconds, 1000) + 1); } - if (config.inactivity_cmd) |inactivity_cmd| { + if (state.config.inactivity_cmd) |inactivity_cmd| { const time = try interop.getTimeOfDay(); - if (!inactivity_cmd_ran and time.seconds - inactivity_time_start.seconds > config.inactivity_delay) { - var inactivity = std.process.Child.init(&[_][]const u8{ "/bin/sh", "-c", inactivity_cmd }, allocator); + if (!inactivity_cmd_ran and time.seconds - inactivity_time_start.seconds > state.config.inactivity_delay) { + var inactivity = std.process.Child.init(&[_][]const u8{ "/bin/sh", "-c", inactivity_cmd }, state.allocator); inactivity.stdout_behavior = .Ignore; inactivity.stderr_behavior = .Ignore; @@ -948,8 +1138,16 @@ pub fn main() !void { break :handle_inactivity_cmd; }; if (process_result.Exited != 0) { - try info_line.addMessage(lang.err_inactivity, config.error_bg, config.error_fg); - try log_file.err("sys", "failed to execute inactivity command: exit code {d}", .{process_result.Exited}); + try state.info_line.addMessage( + state.lang.err_inactivity, + state.config.error_bg, + state.config.error_fg, + ); + try state.log_file.err( + "sys", + "failed to execute inactivity command: exit code {d}", + .{process_result.Exited}, + ); } } @@ -958,7 +1156,7 @@ pub fn main() !void { } // Skip event polling if autologin is set, use simulated Enter key press instead - if (is_autologin) { + if (state.is_autologin) { event = .{ .type = termbox.TB_EVENT_KEY, .key = termbox.TB_KEY_ENTER, @@ -981,14 +1179,22 @@ pub fn main() !void { inactivity_time_start = try interop.getTimeOfDay(); if (event.type == termbox.TB_EVENT_RESIZE) { - state.buffer.width = TerminalBuffer.getWidthStatic(); - state.buffer.height = TerminalBuffer.getHeightStatic(); + state.buffer.width = TerminalBuffer.getWidth(); + state.buffer.height = TerminalBuffer.getHeight(); - try log_file.info("tui", "screen resolution updated to {d}x{d}", .{ state.buffer.width, state.buffer.height }); + try state.log_file.info("tui", "screen resolution updated to {d}x{d}", .{ state.buffer.width, state.buffer.height }); - if (state.animation.*) |*a| a.realloc() catch |err| { - try info_line.addMessage(lang.err_alloc, config.error_bg, config.error_fg); - try log_file.err("tui", "failed to reallocate animation buffers: {s}", .{@errorName(err)}); + if (state.animation) |*a| a.realloc() 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 reallocate animation buffers: {s}", + .{@errorName(err)}, + ); }; positionComponents(&state); @@ -997,23 +1203,27 @@ pub fn main() !void { continue; } - const passthrough_event = try buffer.handleKeybind( - allocator, + var maybe_keys = try state.buffer.handleKeybind( + state.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, - ); - }, + if (maybe_keys) |*keys| { + defer keys.deinit(state.allocator); + + 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, + ); + }, + } } state.update = true; @@ -1132,7 +1342,7 @@ fn authenticate(ptr: *anyopaque) !bool { ); }; state.info_line.label.draw(); - TerminalBuffer.presentBufferStatic(); + TerminalBuffer.presentBuffer(); return false; } @@ -1154,7 +1364,7 @@ fn authenticate(ptr: *anyopaque) !bool { ); }; state.info_line.label.draw(); - TerminalBuffer.presentBufferStatic(); + TerminalBuffer.presentBuffer(); if (state.config.save) save_last_settings: { // It isn't worth cluttering the code with precise error @@ -1189,8 +1399,8 @@ fn authenticate(ptr: *anyopaque) !bool { // 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 {}; + } else if (state.has_old_save) { + std.fs.cwd().deleteFile(state.old_save_path) catch {}; } } @@ -1234,7 +1444,7 @@ fn authenticate(ptr: *anyopaque) !bool { auth.authenticate( state.allocator, - state.log_file, + &state.log_file, auth_options, current_environment, state.login.getCurrentUsername(), @@ -1295,13 +1505,13 @@ fn authenticate(ptr: *anyopaque) !bool { } if (state.config.auth_fails == 0 or state.auth_fails < state.config.auth_fails) { - try TerminalBuffer.clearScreenStatic(true); + try TerminalBuffer.clearScreen(true); state.update = true; } // Restore the cursor - TerminalBuffer.setCursorStatic(0, 0); - TerminalBuffer.presentBufferStatic(); + TerminalBuffer.setCursor(0, 0); + TerminalBuffer.presentBuffer(); return false; } @@ -1407,28 +1617,13 @@ fn increaseBrightnessCmd(ptr: *anyopaque) !bool { return false; } -fn updateComponents(state: *UiState) !void { - if (state.config.battery_id != null) { - try state.battery_label.update(state); - } - - if (state.config.clock != null) { - try state.clock_label.update(state); - } - - if (state.config.bigclock != .none) { - try state.bigclock_label.update(state); - } - - try state.session_specifier_label.update(state); - - if (!state.config.hide_keyboard_locks) { - try state.numlock_label.update(state); - try state.capslock_label.update(state); +fn updateComponents(state: *UiState, widgets: std.ArrayList(Widget)) !void { + for (widgets.items) |*widget| { + try widget.update(state); } } -fn drawUi(state: *UiState) !bool { +fn drawUi(state: *UiState, widgets: std.ArrayList(Widget)) !bool { // If the user entered a wrong password 10 times in a row, play a cascade animation, else update normally if (state.config.auth_fails > 0 and state.auth_fails >= state.config.auth_fails) { std.Thread.sleep(std.time.ns_per_ms * 10); @@ -1439,65 +1634,50 @@ fn drawUi(state: *UiState) !bool { state.auth_fails = 0; } - TerminalBuffer.presentBufferStatic(); + TerminalBuffer.presentBuffer(); return false; } - try TerminalBuffer.clearScreenStatic(false); + try TerminalBuffer.clearScreen(false); - if (!state.animation_timed_out) if (state.animation.*) |*a| a.draw(); - if (!state.config.hide_version_string) state.version_label.draw(); - if (state.config.battery_id != null) state.battery_label.draw(); - if (state.config.bigclock != .none) state.bigclock_label.draw(); + if (!state.animation_timed_out) if (state.animation) |*a| a.draw(); - state.box.draw(); - - if (state.config.clock != null) state.clock_label.draw(); - - state.session_specifier_label.draw(); - state.login_label.draw(); - state.password_label.draw(); - - state.info_line.label.draw(); - - if (!state.config.hide_key_hints) { - state.shutdown_label.draw(); - state.restart_label.draw(); - state.sleep_label.draw(); - state.hibernate_label.draw(); - state.brightness_down_label.draw(); - state.brightness_up_label.draw(); + for (widgets.items) |*widget| { + widget.draw(); } if (state.config.vi_mode) { state.box.bottom_title = if (state.insert_mode) state.lang.insert else state.lang.normal; } - if (!state.config.hide_keyboard_locks) { - state.numlock_label.draw(); - state.capslock_label.draw(); - } - - state.session.label.draw(); - state.login.label.draw(); - state.password.draw(); - - TerminalBuffer.presentBufferStatic(); + TerminalBuffer.presentBuffer(); return true; } -fn updateNumlock(self: *UpdatableLabel, state: *UiState) !void { +fn updateNumlock(self: *Label, ptr: *anyopaque) !void { + var state: *UiState = @ptrCast(@alignCast(ptr)); + const lock_state = interop.getLockState() catch |err| { self.update_fn = null; - try state.info_line.addMessage(state.lang.err_lock_state, state.config.error_bg, state.config.error_fg); - try state.log_file.err("sys", "failed to get lock state: {s}", .{@errorName(err)}); + try state.info_line.addMessage( + state.lang.err_lock_state, + state.config.error_bg, + state.config.error_fg, + ); + try state.log_file.err( + "sys", + "failed to get lock state: {s}", + .{@errorName(err)}, + ); return; }; self.setText(if (lock_state.numlock) state.lang.numlock else ""); } -fn updateCapslock(self: *UpdatableLabel, state: *UiState) !void { +fn updateCapslock(self: *Label, ptr: *anyopaque) !void { + var state: *UiState = @ptrCast(@alignCast(ptr)); + const lock_state = interop.getLockState() catch |err| { self.update_fn = null; try state.info_line.addMessage(state.lang.err_lock_state, state.config.error_bg, state.config.error_fg); @@ -1508,12 +1688,22 @@ fn updateCapslock(self: *UpdatableLabel, state: *UiState) !void { self.setText(if (lock_state.capslock) state.lang.capslock else ""); } -fn updateBattery(self: *UpdatableLabel, state: *UiState) !void { +fn updateBattery(self: *Label, ptr: *anyopaque) !void { + var state: *UiState = @ptrCast(@alignCast(ptr)); + if (state.config.battery_id) |id| { const battery_percentage = getBatteryPercentage(id) catch |err| { self.update_fn = null; - try state.log_file.err("sys", "failed to get battery percentage: {s}", .{@errorName(err)}); - try state.info_line.addMessage(state.lang.err_battery, state.config.error_bg, state.config.error_fg); + try state.log_file.err( + "sys", + "failed to get battery percentage: {s}", + .{@errorName(err)}, + ); + try state.info_line.addMessage( + state.lang.err_battery, + state.config.error_bg, + state.config.error_fg, + ); return; }; @@ -1525,14 +1715,24 @@ fn updateBattery(self: *UpdatableLabel, state: *UiState) !void { } } -fn updateClock(self: *UpdatableLabel, state: *UiState) !void { +fn updateClock(self: *Label, ptr: *anyopaque) !void { + var state: *UiState = @ptrCast(@alignCast(ptr)); + if (state.config.clock) |clock| draw_clock: { const clock_str = interop.timeAsString(&state.clock_buf, clock); if (clock_str.len == 0) { self.update_fn = null; - try state.info_line.addMessage(state.lang.err_clock_too_long, state.config.error_bg, state.config.error_fg); - try state.log_file.err("tui", "clock string too long", .{}); + try state.info_line.addMessage( + state.lang.err_clock_too_long, + state.config.error_bg, + state.config.error_fg, + ); + try state.log_file.err( + "tui", + "clock string too long", + .{}, + ); break :draw_clock; } @@ -1540,8 +1740,10 @@ fn updateClock(self: *UpdatableLabel, state: *UiState) !void { } } -fn updateBigClock(self: *BigclockLabel, state: *UiState) !void { - if (state.box.height + (bigLabel.CHAR_HEIGHT + 2) * 2 >= state.buffer.height) return; +fn updateBigClock(self: *BigLabel, ptr: *anyopaque) !void { + var state: *UiState = @ptrCast(@alignCast(ptr)); + + if (state.box.height + (BigLabel.CHAR_HEIGHT + 2) * 2 >= state.buffer.height) return; const time = try interop.getTimeOfDay(); const animate_time = @divTrunc(time.microseconds, 500_000); @@ -1563,7 +1765,9 @@ fn updateBigClock(self: *BigclockLabel, state: *UiState) !void { self.setText(clock_str); } -fn updateSessionSpecifier(self: *UpdatableLabel, state: *UiState) !void { +fn updateSessionSpecifier(self: *Label, ptr: *anyopaque) !void { + const state: *UiState = @ptrCast(@alignCast(ptr)); + const env = state.session.label.list.items[state.session.label.current]; self.setText(env.environment.specifier); } @@ -1612,14 +1816,14 @@ fn positionComponents(state: *UiState) void { if (state.config.bigclock != .none) { const half_width = state.buffer.width / 2; - const half_label_width = (TerminalBuffer.strWidth(state.bigclock_label.text) * (bigLabel.CHAR_WIDTH + 1)) / 2; + const half_label_width = (TerminalBuffer.strWidth(state.bigclock_label.text) * (BigLabel.CHAR_WIDTH + 1)) / 2; const half_height = (if (state.buffer.height > state.box.height) state.buffer.height - state.box.height else state.buffer.height) / 2; state.bigclock_label.positionXY(TerminalBuffer.START_POSITION .addX(half_width) .removeXIf(half_label_width, half_width > half_label_width) .addY(half_height) - .removeYIf(bigLabel.CHAR_HEIGHT + 2, half_height > bigLabel.CHAR_HEIGHT + 2)); + .removeYIf(BigLabel.CHAR_HEIGHT + 2, half_height > BigLabel.CHAR_HEIGHT + 2)); } state.info_line.label.positionY(state.box diff --git a/src/tui/Animation.zig b/src/tui/Animation.zig deleted file mode 100644 index 2311bba..0000000 --- a/src/tui/Animation.zig +++ /dev/null @@ -1,61 +0,0 @@ -const Animation = @This(); - -const VTable = struct { - deinit_fn: *const fn (ptr: *anyopaque) void, - realloc_fn: *const fn (ptr: *anyopaque) anyerror!void, - draw_fn: *const fn (ptr: *anyopaque) void, -}; - -pointer: *anyopaque, -vtable: VTable, - -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, -) Animation { - const Pointer = @TypeOf(pointer); - const Impl = struct { - pub fn deinitImpl(ptr: *anyopaque) void { - const impl: Pointer = @ptrCast(@alignCast(ptr)); - return @call(.always_inline, deinit_fn, .{impl}); - } - - pub fn reallocImpl(ptr: *anyopaque) anyerror!void { - const impl: Pointer = @ptrCast(@alignCast(ptr)); - return @call(.always_inline, realloc_fn, .{impl}); - } - - pub fn drawImpl(ptr: *anyopaque) void { - const impl: Pointer = @ptrCast(@alignCast(ptr)); - return @call(.always_inline, draw_fn, .{impl}); - } - - const vtable = VTable{ - .deinit_fn = deinitImpl, - .realloc_fn = reallocImpl, - .draw_fn = drawImpl, - }; - }; - - return .{ - .pointer = pointer, - .vtable = Impl.vtable, - }; -} - -pub fn deinit(self: *Animation) void { - const impl: @TypeOf(self.pointer) = @ptrCast(@alignCast(self.pointer)); - return @call(.auto, self.vtable.deinit_fn, .{impl}); -} - -pub fn realloc(self: *Animation) anyerror!void { - const impl: @TypeOf(self.pointer) = @ptrCast(@alignCast(self.pointer)); - return @call(.auto, self.vtable.realloc_fn, .{impl}); -} - -pub fn draw(self: *Animation) void { - const impl: @TypeOf(self.pointer) = @ptrCast(@alignCast(self.pointer)); - return @call(.auto, self.vtable.draw_fn, .{impl}); -} diff --git a/src/tui/Cell.zig b/src/tui/Cell.zig index 4389a2f..2a6fc72 100644 --- a/src/tui/Cell.zig +++ b/src/tui/Cell.zig @@ -1,5 +1,4 @@ const TerminalBuffer = @import("TerminalBuffer.zig"); -const termbox = TerminalBuffer.termbox; const Cell = @This(); @@ -18,5 +17,5 @@ pub fn init(ch: u32, fg: u32, bg: u32) Cell { pub fn put(self: Cell, x: usize, y: usize) void { if (self.ch == 0) return; - _ = termbox.tb_set_cell(@intCast(x), @intCast(y), self.ch, self.fg, self.bg); + TerminalBuffer.setCell(x, y, self.ch, self.fg, self.bg); } diff --git a/src/tui/TerminalBuffer.zig b/src/tui/TerminalBuffer.zig index 302cf0b..8250acb 100644 --- a/src/tui/TerminalBuffer.zig +++ b/src/tui/TerminalBuffer.zig @@ -144,34 +144,50 @@ pub fn init(allocator: Allocator, options: InitOptions, log_file: *LogFile, rand pub fn deinit(self: *TerminalBuffer) void { self.keybinds.deinit(); - TerminalBuffer.shutdownStatic(); + TerminalBuffer.shutdown(); } -pub fn getWidthStatic() usize { +pub fn getWidth() usize { return @intCast(termbox.tb_width()); } -pub fn getHeightStatic() usize { +pub fn getHeight() usize { return @intCast(termbox.tb_height()); } -pub fn setCursorStatic(x: usize, y: usize) void { +pub fn setCursor(x: usize, y: usize) void { _ = termbox.tb_set_cursor(@intCast(x), @intCast(y)); } -pub fn clearScreenStatic(clear_back_buffer: bool) !void { +pub fn clearScreen(clear_back_buffer: bool) !void { _ = termbox.tb_clear(); if (clear_back_buffer) try clearBackBuffer(); } -pub fn shutdownStatic() void { +pub fn shutdown() void { _ = termbox.tb_shutdown(); } -pub fn presentBufferStatic() void { +pub fn presentBuffer() void { _ = termbox.tb_present(); } +pub fn setCell( + x: usize, + y: usize, + ch: u32, + fg: u32, + bg: u32, +) void { + _ = termbox.tb_set_cell( + @intCast(x), + @intCast(y), + ch, + fg, + bg, + ); +} + pub fn reclaim(self: TerminalBuffer) !void { if (self.termios) |termios| { // Take back control of the TTY @@ -257,17 +273,22 @@ pub fn handleKeybind( allocator: Allocator, tb_event: termbox.tb_event, context: *anyopaque, -) !bool { +) !?std.ArrayList(keyboard.Key) { 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}); + const passthrough_event = try @call(.auto, callback, .{context}); + if (!passthrough_event) { + keys.deinit(allocator); + return null; + } + + return keys; } } - return true; + return keys; } pub fn drawText( diff --git a/src/tui/Widget.zig b/src/tui/Widget.zig new file mode 100644 index 0000000..6ffb0b3 --- /dev/null +++ b/src/tui/Widget.zig @@ -0,0 +1,150 @@ +const Widget = @This(); + +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, + 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, +}; + +pointer: *anyopaque, +vtable: VTable, + +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 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 { + const Pointer = @TypeOf(pointer); + const Impl = struct { + pub fn deinitImpl(ptr: *anyopaque) void { + const impl: Pointer = @ptrCast(@alignCast(ptr)); + + if (deinit_fn) |func| { + return @call( + .always_inline, + func, + .{impl}, + ); + } + } + + pub fn reallocImpl(ptr: *anyopaque) !void { + const impl: Pointer = @ptrCast(@alignCast(ptr)); + + if (realloc_fn) |func| { + return @call( + .always_inline, + func, + .{impl}, + ); + } + } + + pub fn drawImpl(ptr: *anyopaque) void { + const impl: Pointer = @ptrCast(@alignCast(ptr)); + + if (draw_fn) |func| { + return @call( + .always_inline, + func, + .{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 }, + ); + } + } + + 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 }, + ); + } + } + + const vtable = VTable{ + .deinit_fn = deinitImpl, + .realloc_fn = reallocImpl, + .draw_fn = drawImpl, + .update_fn = updateImpl, + .handle_fn = handleImpl, + }; + }; + + return .{ + .pointer = pointer, + .vtable = Impl.vtable, + }; +} + +pub fn deinit(self: *Widget) void { + const impl: @TypeOf(self.pointer) = @ptrCast(@alignCast(self.pointer)); + + return @call( + .auto, + self.vtable.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}, + ); +} + +pub fn draw(self: *Widget) void { + const impl: @TypeOf(self.pointer) = @ptrCast(@alignCast(self.pointer)); + + return @call( + .auto, + self.vtable.draw_fn, + .{impl}, + ); +} + +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 }, + ); +} + +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 }, + ); +} diff --git a/src/tui/components/BigLabel.zig b/src/tui/components/BigLabel.zig new file mode 100644 index 0000000..0e6bf51 --- /dev/null +++ b/src/tui/components/BigLabel.zig @@ -0,0 +1,215 @@ +const BigLabel = @This(); + +const std = @import("std"); +const Allocator = std.mem.Allocator; + +const ly_core = @import("ly-core"); +const interop = ly_core.interop; + +const en = @import("bigLabelLocales/en.zig"); +const fa = @import("bigLabelLocales/fa.zig"); +const Cell = @import("../Cell.zig"); +const Position = @import("../Position.zig"); +const TerminalBuffer = @import("../TerminalBuffer.zig"); +const Widget = @import("../Widget.zig"); + +pub const CHAR_WIDTH = 5; +pub const CHAR_HEIGHT = 5; +pub const CHAR_SIZE = CHAR_WIDTH * CHAR_HEIGHT; +pub const X: u32 = if (ly_core.interop.supportsUnicode()) 0x2593 else '#'; +pub const O: u32 = 0; + +// zig fmt: off +pub const LocaleChars = struct { + ZERO: [CHAR_SIZE]u21, + ONE: [CHAR_SIZE]u21, + TWO: [CHAR_SIZE]u21, + THREE: [CHAR_SIZE]u21, + FOUR: [CHAR_SIZE]u21, + FIVE: [CHAR_SIZE]u21, + SIX: [CHAR_SIZE]u21, + SEVEN: [CHAR_SIZE]u21, + EIGHT: [CHAR_SIZE]u21, + NINE: [CHAR_SIZE]u21, + S: [CHAR_SIZE]u21, + E: [CHAR_SIZE]u21, + P: [CHAR_SIZE]u21, + A: [CHAR_SIZE]u21, + M: [CHAR_SIZE]u21, +}; +// zig fmt: on + +pub const BigLabelLocale = enum { + en, + fa, +}; + +allocator: ?Allocator = null, +buffer: *TerminalBuffer, +text: []const u8, +max_width: ?usize, +fg: u32, +bg: u32, +locale: BigLabelLocale, +update_fn: ?*const fn (*BigLabel, *anyopaque) anyerror!void, +component_pos: Position, +children_pos: Position, + +pub fn init( + buffer: *TerminalBuffer, + text: []const u8, + max_width: ?usize, + fg: u32, + bg: u32, + locale: BigLabelLocale, + update_fn: ?*const fn (*BigLabel, *anyopaque) anyerror!void, +) BigLabel { + return .{ + .allocator = null, + .buffer = buffer, + .text = text, + .max_width = max_width, + .fg = fg, + .bg = bg, + .locale = locale, + .update_fn = update_fn, + .component_pos = TerminalBuffer.START_POSITION, + .children_pos = TerminalBuffer.START_POSITION, + }; +} + +pub fn deinit(self: *BigLabel) void { + if (self.allocator) |allocator| allocator.free(self.text); +} + +pub fn widget(self: *BigLabel) Widget { + return Widget.init( + self, + deinit, + null, + draw, + update, + null, + ); +} + +pub fn setTextAlloc( + self: *BigLabel, + allocator: Allocator, + comptime fmt: []const u8, + args: anytype, +) !void { + self.text = try std.fmt.allocPrint(allocator, fmt, args); + self.allocator = allocator; +} + +pub fn setTextBuf( + self: *BigLabel, + buffer: []u8, + comptime fmt: []const u8, + args: anytype, +) !void { + self.text = try std.fmt.bufPrint(buffer, fmt, args); + self.allocator = null; +} + +pub fn setText(self: *BigLabel, text: []const u8) void { + self.text = text; + self.allocator = null; +} + +pub fn positionX(self: *BigLabel, original_pos: Position) void { + self.component_pos = original_pos; + self.children_pos = original_pos.addX(TerminalBuffer.strWidth(self.text) * CHAR_WIDTH); +} + +pub fn positionY(self: *BigLabel, original_pos: Position) void { + self.component_pos = original_pos; + self.children_pos = original_pos.addY(CHAR_HEIGHT); +} + +pub fn positionXY(self: *BigLabel, original_pos: Position) void { + self.component_pos = original_pos; + self.children_pos = Position.init( + TerminalBuffer.strWidth(self.text) * CHAR_WIDTH, + CHAR_HEIGHT, + ).add(original_pos); +} + +pub fn childrenPosition(self: BigLabel) Position { + return self.children_pos; +} + +pub fn draw(self: *BigLabel) void { + for (self.text, 0..) |c, i| { + const clock_cell = clockCell( + c, + self.fg, + self.bg, + self.locale, + ); + + alphaBlit( + self.component_pos.x + i * (CHAR_WIDTH + 1), + self.component_pos.y, + self.buffer.width, + self.buffer.height, + clock_cell, + ); + } +} + +pub fn update(self: *BigLabel, context: *anyopaque) !void { + if (self.update_fn) |update_fn| { + return @call( + .auto, + update_fn, + .{ self, context }, + ); + } +} + +fn clockCell(char: u8, fg: u32, bg: u32, locale: BigLabelLocale) [CHAR_SIZE]Cell { + var cells: [CHAR_SIZE]Cell = undefined; + + //@divTrunc(time.microseconds, 500000) != 0) + const clock_chars = toBigNumber(char, locale); + for (0..cells.len) |i| cells[i] = Cell.init(clock_chars[i], fg, bg); + + return cells; +} + +fn alphaBlit(x: usize, y: usize, tb_width: usize, tb_height: usize, cells: [CHAR_SIZE]Cell) void { + if (x + CHAR_WIDTH >= tb_width or y + CHAR_HEIGHT >= tb_height) return; + + for (0..CHAR_HEIGHT) |yy| { + for (0..CHAR_WIDTH) |xx| { + const cell = cells[yy * CHAR_WIDTH + xx]; + cell.put(x + xx, y + yy); + } + } +} + +fn toBigNumber(char: u8, locale: BigLabelLocale) [CHAR_SIZE]u21 { + const locale_chars = switch (locale) { + .fa => fa.locale_chars, + .en => en.locale_chars, + }; + return switch (char) { + '0' => locale_chars.ZERO, + '1' => locale_chars.ONE, + '2' => locale_chars.TWO, + '3' => locale_chars.THREE, + '4' => locale_chars.FOUR, + '5' => locale_chars.FIVE, + '6' => locale_chars.SIX, + '7' => locale_chars.SEVEN, + '8' => locale_chars.EIGHT, + '9' => locale_chars.NINE, + 'p', 'P' => locale_chars.P, + 'a', 'A' => locale_chars.A, + 'm', 'M' => locale_chars.M, + ':' => locale_chars.S, + else => locale_chars.E, + }; +} diff --git a/src/tui/components/CenteredBox.zig b/src/tui/components/CenteredBox.zig index e0f1798..38c566a 100644 --- a/src/tui/components/CenteredBox.zig +++ b/src/tui/components/CenteredBox.zig @@ -3,7 +3,7 @@ const std = @import("std"); const Cell = @import("../Cell.zig"); const Position = @import("../Position.zig"); const TerminalBuffer = @import("../TerminalBuffer.zig"); -const termbox = TerminalBuffer.termbox; +const Widget = @import("../Widget.zig"); const CenteredBox = @This(); @@ -56,6 +56,17 @@ pub fn init( }; } +pub fn widget(self: *CenteredBox) Widget { + return Widget.init( + self, + null, + null, + draw, + null, + null, + ); +} + pub fn positionXY(self: *CenteredBox, original_pos: Position) void { if (self.buffer.width < 2 or self.buffer.height < 2) return; @@ -79,51 +90,55 @@ pub fn childrenPosition(self: CenteredBox) Position { return self.children_pos; } -pub fn draw(self: CenteredBox) void { +pub fn draw(self: *CenteredBox) void { if (self.show_borders) { - _ = termbox.tb_set_cell( - @intCast(self.left_pos.x - 1), - @intCast(self.left_pos.y - 1), + var left_up = Cell.init( self.buffer.box_chars.left_up, self.border_fg, self.bg, ); - _ = termbox.tb_set_cell( - @intCast(self.right_pos.x), - @intCast(self.left_pos.y - 1), + var right_up = Cell.init( self.buffer.box_chars.right_up, self.border_fg, self.bg, ); - _ = termbox.tb_set_cell( - @intCast(self.left_pos.x - 1), - @intCast(self.right_pos.y), + var left_down = Cell.init( self.buffer.box_chars.left_down, self.border_fg, self.bg, ); - _ = termbox.tb_set_cell( - @intCast(self.right_pos.x), - @intCast(self.right_pos.y), + var right_down = Cell.init( self.buffer.box_chars.right_down, self.border_fg, self.bg, ); + var top = Cell.init( + self.buffer.box_chars.top, + self.border_fg, + self.bg, + ); + var bottom = Cell.init( + self.buffer.box_chars.bottom, + self.border_fg, + self.bg, + ); - var c1 = Cell.init(self.buffer.box_chars.top, self.border_fg, self.bg); - var c2 = Cell.init(self.buffer.box_chars.bottom, self.border_fg, self.bg); + left_up.put(self.left_pos.x - 1, self.left_pos.y - 1); + right_up.put(self.right_pos.x, self.left_pos.y - 1); + left_down.put(self.left_pos.x - 1, self.right_pos.y); + right_down.put(self.right_pos.x, self.right_pos.y); for (0..self.width) |i| { - c1.put(self.left_pos.x + i, self.left_pos.y - 1); - c2.put(self.left_pos.x + i, self.right_pos.y); + top.put(self.left_pos.x + i, self.left_pos.y - 1); + bottom.put(self.left_pos.x + i, self.right_pos.y); } - c1.ch = self.buffer.box_chars.left; - c2.ch = self.buffer.box_chars.right; + top.ch = self.buffer.box_chars.left; + bottom.ch = self.buffer.box_chars.right; for (0..self.height) |i| { - c1.put(self.left_pos.x - 1, self.left_pos.y + i); - c2.put(self.right_pos.x, self.left_pos.y + i); + top.put(self.left_pos.x - 1, self.left_pos.y + i); + bottom.put(self.right_pos.x, self.left_pos.y + i); } } diff --git a/src/tui/components/InfoLine.zig b/src/tui/components/InfoLine.zig index 08d0c85..d059c81 100644 --- a/src/tui/components/InfoLine.zig +++ b/src/tui/components/InfoLine.zig @@ -1,7 +1,9 @@ const std = @import("std"); const Allocator = std.mem.Allocator; +const keyboard = @import("../keyboard.zig"); const TerminalBuffer = @import("../TerminalBuffer.zig"); +const Widget = @import("../Widget.zig"); const generic = @import("generic.zig"); const MessageLabel = generic.CyclableLabel(Message, Message); @@ -43,6 +45,25 @@ pub fn deinit(self: *InfoLine) void { self.label.deinit(); } +pub fn widget(self: *InfoLine) Widget { + return Widget.init( + self, + deinit, + null, + draw, + null, + handle, + ); +} + +pub fn draw(self: *InfoLine) void { + self.label.draw(); +} + +pub fn handle(self: *InfoLine, maybe_key: ?keyboard.Key, insert_mode: bool) !void { + self.label.handle(maybe_key, insert_mode); +} + pub fn addMessage(self: *InfoLine, text: []const u8, bg: u32, fg: u32) !void { if (text.len == 0) return; diff --git a/src/tui/components/Label.zig b/src/tui/components/Label.zig new file mode 100644 index 0000000..fcc7aa1 --- /dev/null +++ b/src/tui/components/Label.zig @@ -0,0 +1,131 @@ +const Label = @This(); + +const std = @import("std"); +const Allocator = std.mem.Allocator; + +const Cell = @import("../Cell.zig"); +const Position = @import("../Position.zig"); +const TerminalBuffer = @import("../TerminalBuffer.zig"); +const Widget = @import("../Widget.zig"); + +allocator: ?Allocator, +text: []const u8, +max_width: ?usize, +fg: u32, +bg: u32, +update_fn: ?*const fn (*Label, *anyopaque) anyerror!void, +component_pos: Position, +children_pos: Position, + +pub fn init( + text: []const u8, + max_width: ?usize, + fg: u32, + bg: u32, + update_fn: ?*const fn (*Label, *anyopaque) anyerror!void, +) Label { + return .{ + .allocator = null, + .text = text, + .max_width = max_width, + .fg = fg, + .bg = bg, + .update_fn = update_fn, + .component_pos = TerminalBuffer.START_POSITION, + .children_pos = TerminalBuffer.START_POSITION, + }; +} + +pub fn deinit(self: *Label) void { + if (self.allocator) |allocator| allocator.free(self.text); +} + +pub fn widget(self: *Label) Widget { + return Widget.init( + self, + deinit, + null, + draw, + update, + null, + ); +} + +pub fn setTextAlloc( + self: *Label, + allocator: Allocator, + comptime fmt: []const u8, + args: anytype, +) !void { + self.text = try std.fmt.allocPrint(allocator, fmt, args); + self.allocator = allocator; +} + +pub fn setTextBuf( + self: *Label, + buffer: []u8, + comptime fmt: []const u8, + args: anytype, +) !void { + self.text = try std.fmt.bufPrint(buffer, fmt, args); + self.allocator = null; +} + +pub fn setText(self: *Label, text: []const u8) void { + self.text = text; + self.allocator = null; +} + +pub fn positionX(self: *Label, original_pos: Position) void { + self.component_pos = original_pos; + self.children_pos = original_pos.addX(TerminalBuffer.strWidth(self.text)); +} + +pub fn positionY(self: *Label, original_pos: Position) void { + self.component_pos = original_pos; + self.children_pos = original_pos.addY(1); +} + +pub fn positionXY(self: *Label, original_pos: Position) void { + self.component_pos = original_pos; + self.children_pos = Position.init( + TerminalBuffer.strWidth(self.text), + 1, + ).add(original_pos); +} + +pub fn childrenPosition(self: Label) Position { + return self.children_pos; +} + +pub fn draw(self: *Label) void { + if (self.max_width) |width| { + TerminalBuffer.drawConfinedText( + self.text, + self.component_pos.x, + self.component_pos.y, + width, + self.fg, + self.bg, + ); + return; + } + + TerminalBuffer.drawText( + self.text, + self.component_pos.x, + self.component_pos.y, + self.fg, + self.bg, + ); +} + +pub fn update(self: *Label, ctx: *anyopaque) !void { + if (self.update_fn) |update_fn| { + return @call( + .auto, + update_fn, + .{ self, ctx }, + ); + } +} diff --git a/src/tui/components/Session.zig b/src/tui/components/Session.zig index 1f0dcca..ced126c 100644 --- a/src/tui/components/Session.zig +++ b/src/tui/components/Session.zig @@ -4,7 +4,9 @@ const Allocator = std.mem.Allocator; const enums = @import("../../enums.zig"); const DisplayServer = enums.DisplayServer; const Environment = @import("../../Environment.zig"); +const keyboard = @import("../keyboard.zig"); const TerminalBuffer = @import("../TerminalBuffer.zig"); +const Widget = @import("../Widget.zig"); const generic = @import("generic.zig"); const UserList = @import("UserList.zig"); @@ -53,6 +55,25 @@ pub fn deinit(self: *Session) void { self.label.deinit(); } +pub fn widget(self: *Session) Widget { + return Widget.init( + self, + deinit, + null, + draw, + null, + handle, + ); +} + +pub fn draw(self: *Session) void { + self.label.draw(); +} + +pub fn handle(self: *Session, maybe_key: ?keyboard.Key, insert_mode: bool) !void { + self.label.handle(maybe_key, insert_mode); +} + pub fn addEnvironment(self: *Session, environment: Environment) !void { const env = Env{ .environment = environment, .index = self.label.list.items.len }; diff --git a/src/tui/components/Text.zig b/src/tui/components/Text.zig index bc58348..e81feb6 100644 --- a/src/tui/components/Text.zig +++ b/src/tui/components/Text.zig @@ -1,9 +1,10 @@ const std = @import("std"); const Allocator = std.mem.Allocator; +const keyboard = @import("../keyboard.zig"); const TerminalBuffer = @import("../TerminalBuffer.zig"); const Position = @import("../Position.zig"); -const termbox = TerminalBuffer.termbox; +const Widget = @import("../Widget.zig"); const DynamicString = std.ArrayListUnmanaged(u8); @@ -53,6 +54,17 @@ pub fn deinit(self: *Text) void { self.text.deinit(self.allocator); } +pub fn widget(self: *Text) Widget { + return Widget.init( + self, + deinit, + null, + draw, + null, + handle, + ); +} + pub fn positionX(self: *Text, original_pos: Position) void { self.component_pos = original_pos; self.children_pos = original_pos.addX(self.width); @@ -75,50 +87,37 @@ pub fn childrenPosition(self: Text) Position { return self.children_pos; } -pub fn handle(self: *Text, maybe_event: ?*termbox.tb_event, insert_mode: bool) !void { - if (maybe_event) |event| blk: { - if (event.type != termbox.TB_EVENT_KEY) break :blk; - - switch (event.key) { - termbox.TB_KEY_ARROW_LEFT => self.goLeft(), - termbox.TB_KEY_ARROW_RIGHT => self.goRight(), - termbox.TB_KEY_DELETE => self.delete(), - termbox.TB_KEY_BACKSPACE, termbox.TB_KEY_BACKSPACE2 => { - if (insert_mode) { - self.backspace(); - } else { - self.goLeft(); - } - }, - termbox.TB_KEY_SPACE => try self.write(' '), - else => { - if (event.ch > 31 and event.ch < 127) { - if (insert_mode) { - try self.write(@intCast(event.ch)); - } else { - switch (event.ch) { - 'h' => self.goLeft(), - 'l' => self.goRight(), - else => {}, - } - } - } - }, +pub fn handle(self: *Text, maybe_key: ?keyboard.Key, insert_mode: bool) !void { + if (maybe_key) |key| { + if (key.left or (!insert_mode and (key.h or key.backspace))) { + self.goLeft(); + } else if (key.right or (!insert_mode and key.l)) { + self.goRight(); + } else if (key.delete) { + self.delete(); + } else if (key.backspace) { + self.backspace(); + } else if (insert_mode) { + const maybe_character = key.getEnabledPrintableAscii(); + if (maybe_character) |character| try self.write(character); } } if (self.masked and self.maybe_mask == null) { - _ = termbox.tb_set_cursor(@intCast(self.component_pos.x), @intCast(self.component_pos.y)); + TerminalBuffer.setCursor( + self.component_pos.x, + self.component_pos.y, + ); return; } - _ = termbox.tb_set_cursor( - @intCast(self.component_pos.x + (self.cursor - self.visible_start)), - @intCast(self.component_pos.y), + TerminalBuffer.setCursor( + self.component_pos.x + (self.cursor - self.visible_start), + self.component_pos.y, ); } -pub fn draw(self: Text) void { +pub fn draw(self: *Text) void { if (self.masked) { if (self.maybe_mask) |mask| { if (self.width < 1) return; diff --git a/src/tui/components/UserList.zig b/src/tui/components/UserList.zig index 4e22951..d053623 100644 --- a/src/tui/components/UserList.zig +++ b/src/tui/components/UserList.zig @@ -2,7 +2,9 @@ const std = @import("std"); const Allocator = std.mem.Allocator; const SavedUsers = @import("../../config/SavedUsers.zig"); +const keyboard = @import("../keyboard.zig"); const TerminalBuffer = @import("../TerminalBuffer.zig"); +const Widget = @import("../Widget.zig"); const generic = @import("generic.zig"); const Session = @import("Session.zig"); @@ -85,10 +87,29 @@ pub fn deinit(self: *UserList) void { self.label.deinit(); } +pub fn widget(self: *UserList) Widget { + return Widget.init( + self, + deinit, + null, + draw, + null, + handle, + ); +} + pub fn getCurrentUsername(self: UserList) []const u8 { return self.label.list.items[self.label.current].name; } +fn draw(self: *UserList) void { + self.label.draw(); +} + +fn handle(self: *UserList, maybe_key: ?keyboard.Key, insert_mode: bool) !void { + self.label.handle(maybe_key, insert_mode); +} + fn usernameChanged(user: User, maybe_session: ?*Session) void { if (maybe_session) |session| { session.label.current = @min(user.session_index.*, session.label.list.items.len - 1); diff --git a/src/tui/components/bigLabel.zig b/src/tui/components/bigLabel.zig deleted file mode 100644 index 26b3257..0000000 --- a/src/tui/components/bigLabel.zig +++ /dev/null @@ -1,210 +0,0 @@ -const std = @import("std"); -const Allocator = std.mem.Allocator; - -const ly_core = @import("ly-core"); -const interop = ly_core.interop; - -const en = @import("bigLabelLocales/en.zig"); -const fa = @import("bigLabelLocales/fa.zig"); -const Cell = @import("../Cell.zig"); -const Position = @import("../Position.zig"); -const TerminalBuffer = @import("../TerminalBuffer.zig"); -const termbox = TerminalBuffer.termbox; - -pub const CHAR_WIDTH = 5; -pub const CHAR_HEIGHT = 5; -pub const CHAR_SIZE = CHAR_WIDTH * CHAR_HEIGHT; -pub const X: u32 = if (ly_core.interop.supportsUnicode()) 0x2593 else '#'; -pub const O: u32 = 0; - -// zig fmt: off -pub const LocaleChars = struct { - ZERO: [CHAR_SIZE]u21, - ONE: [CHAR_SIZE]u21, - TWO: [CHAR_SIZE]u21, - THREE: [CHAR_SIZE]u21, - FOUR: [CHAR_SIZE]u21, - FIVE: [CHAR_SIZE]u21, - SIX: [CHAR_SIZE]u21, - SEVEN: [CHAR_SIZE]u21, - EIGHT: [CHAR_SIZE]u21, - NINE: [CHAR_SIZE]u21, - S: [CHAR_SIZE]u21, - E: [CHAR_SIZE]u21, - P: [CHAR_SIZE]u21, - A: [CHAR_SIZE]u21, - M: [CHAR_SIZE]u21, -}; -// zig fmt: on - -pub const BigLabelLocale = enum { - en, - fa, -}; - -pub fn BigLabel(comptime ContextType: type) type { - return struct { - const Self = @This(); - - buffer: *TerminalBuffer, - text: []const u8, - max_width: ?usize, - fg: u32, - bg: u32, - locale: BigLabelLocale, - update_fn: ?*const fn (*Self, ContextType) anyerror!void, - is_text_allocated: bool, - component_pos: Position, - children_pos: Position, - - pub fn init( - buffer: *TerminalBuffer, - text: []const u8, - max_width: ?usize, - fg: u32, - bg: u32, - locale: BigLabelLocale, - update_fn: ?*const fn (*Self, ContextType) anyerror!void, - ) Self { - return .{ - .buffer = buffer, - .text = text, - .max_width = max_width, - .fg = fg, - .bg = bg, - .locale = locale, - .update_fn = update_fn, - .is_text_allocated = false, - .component_pos = TerminalBuffer.START_POSITION, - .children_pos = TerminalBuffer.START_POSITION, - }; - } - - pub fn setTextAlloc( - self: *Self, - allocator: Allocator, - comptime fmt: []const u8, - args: anytype, - ) !void { - self.text = try std.fmt.allocPrint(allocator, fmt, args); - self.is_text_allocated = true; - } - - pub fn setTextBuf( - self: *Self, - buffer: []u8, - comptime fmt: []const u8, - args: anytype, - ) !void { - self.text = try std.fmt.bufPrint(buffer, fmt, args); - self.is_text_allocated = false; - } - - pub fn setText(self: *Self, text: []const u8) void { - self.text = text; - self.is_text_allocated = false; - } - - pub fn deinit(self: Self, allocator: ?Allocator) void { - if (self.is_text_allocated) { - if (allocator) |alloc| alloc.free(self.text); - } - } - - pub fn positionX(self: *Self, original_pos: Position) void { - self.component_pos = original_pos; - self.children_pos = original_pos.addX(TerminalBuffer.strWidth(self.text) * CHAR_WIDTH); - } - - pub fn positionY(self: *Self, original_pos: Position) void { - self.component_pos = original_pos; - self.children_pos = original_pos.addY(CHAR_HEIGHT); - } - - pub fn positionXY(self: *Self, original_pos: Position) void { - self.component_pos = original_pos; - self.children_pos = Position.init( - TerminalBuffer.strWidth(self.text) * CHAR_WIDTH, - CHAR_HEIGHT, - ).add(original_pos); - } - - pub fn childrenPosition(self: Self) Position { - return self.children_pos; - } - - pub fn draw(self: Self) void { - for (self.text, 0..) |c, i| { - const clock_cell = clockCell( - c, - self.fg, - self.bg, - self.locale, - ); - - alphaBlit( - self.component_pos.x + i * (CHAR_WIDTH + 1), - self.component_pos.y, - self.buffer.width, - self.buffer.height, - clock_cell, - ); - } - } - - pub fn update(self: *Self, context: ContextType) !void { - if (self.update_fn) |update_fn| { - return @call( - .auto, - update_fn, - .{ self, context }, - ); - } - } - - fn clockCell(char: u8, fg: u32, bg: u32, locale: BigLabelLocale) [CHAR_SIZE]Cell { - var cells: [CHAR_SIZE]Cell = undefined; - - //@divTrunc(time.microseconds, 500000) != 0) - const clock_chars = toBigNumber(char, locale); - for (0..cells.len) |i| cells[i] = Cell.init(clock_chars[i], fg, bg); - - return cells; - } - - fn alphaBlit(x: usize, y: usize, tb_width: usize, tb_height: usize, cells: [CHAR_SIZE]Cell) void { - if (x + CHAR_WIDTH >= tb_width or y + CHAR_HEIGHT >= tb_height) return; - - for (0..CHAR_HEIGHT) |yy| { - for (0..CHAR_WIDTH) |xx| { - const cell = cells[yy * CHAR_WIDTH + xx]; - cell.put(x + xx, y + yy); - } - } - } - - fn toBigNumber(char: u8, locale: BigLabelLocale) [CHAR_SIZE]u21 { - const locale_chars = switch (locale) { - .fa => fa.locale_chars, - .en => en.locale_chars, - }; - return switch (char) { - '0' => locale_chars.ZERO, - '1' => locale_chars.ONE, - '2' => locale_chars.TWO, - '3' => locale_chars.THREE, - '4' => locale_chars.FOUR, - '5' => locale_chars.FIVE, - '6' => locale_chars.SIX, - '7' => locale_chars.SEVEN, - '8' => locale_chars.EIGHT, - '9' => locale_chars.NINE, - 'p', 'P' => locale_chars.P, - 'a', 'A' => locale_chars.A, - 'm', 'M' => locale_chars.M, - ':' => locale_chars.S, - else => locale_chars.E, - }; - } - }; -} diff --git a/src/tui/components/bigLabelLocales/en.zig b/src/tui/components/bigLabelLocales/en.zig index 261227c..0593eed 100644 --- a/src/tui/components/bigLabelLocales/en.zig +++ b/src/tui/components/bigLabelLocales/en.zig @@ -1,7 +1,7 @@ -const bigLabel = @import("../bigLabel.zig"); -const LocaleChars = bigLabel.LocaleChars; -const X = bigLabel.X; -const O = bigLabel.O; +const BigLabel = @import("../BigLabel.zig"); +const LocaleChars = BigLabel.LocaleChars; +const X = BigLabel.X; +const O = BigLabel.O; // zig fmt: off pub const locale_chars = LocaleChars{ diff --git a/src/tui/components/bigLabelLocales/fa.zig b/src/tui/components/bigLabelLocales/fa.zig index ea48737..06e7036 100644 --- a/src/tui/components/bigLabelLocales/fa.zig +++ b/src/tui/components/bigLabelLocales/fa.zig @@ -1,7 +1,7 @@ -const bigLabel = @import("../bigLabel.zig"); -const LocaleChars = bigLabel.LocaleChars; -const X = bigLabel.X; -const O = bigLabel.O; +const BigLabel = @import("../BigLabel.zig"); +const LocaleChars = BigLabel.LocaleChars; +const X = BigLabel.X; +const O = BigLabel.O; // zig fmt: off pub const locale_chars = LocaleChars{ diff --git a/src/tui/components/generic.zig b/src/tui/components/generic.zig index d2ddfd0..4f9c9e7 100644 --- a/src/tui/components/generic.zig +++ b/src/tui/components/generic.zig @@ -1,5 +1,7 @@ const std = @import("std"); +const Cell = @import("../Cell.zig"); +const keyboard = @import("../keyboard.zig"); const TerminalBuffer = @import("../TerminalBuffer.zig"); const Position = @import("../Position.zig"); @@ -10,8 +12,6 @@ pub fn CyclableLabel(comptime ItemType: type, comptime ChangeItemType: type) typ const DrawItemFn = *const fn (*Self, ItemType, usize, usize, usize) void; const ChangeItemFn = *const fn (ItemType, ?ChangeItemType) void; - const termbox = TerminalBuffer.termbox; - const Self = @This(); allocator: Allocator, @@ -92,28 +92,18 @@ pub fn CyclableLabel(comptime ItemType: type, comptime ChangeItemType: type) typ self.current = self.list.items.len - 1; } - pub fn handle(self: *Self, maybe_event: ?*termbox.tb_event, insert_mode: bool) void { - if (maybe_event) |event| blk: { - if (event.type != termbox.TB_EVENT_KEY) break :blk; - - switch (event.key) { - termbox.TB_KEY_ARROW_LEFT, termbox.TB_KEY_CTRL_H => self.goLeft(), - termbox.TB_KEY_ARROW_RIGHT, termbox.TB_KEY_CTRL_L => self.goRight(), - else => { - if (!insert_mode) { - switch (event.ch) { - 'h' => self.goLeft(), - 'l' => self.goRight(), - else => {}, - } - } - }, + pub fn handle(self: *Self, maybe_key: ?keyboard.Key, insert_mode: bool) void { + if (maybe_key) |key| { + if (key.left or (key.ctrl and key.h) or (!insert_mode and key.h)) { + self.goLeft(); + } else if (key.right or (key.ctrl and key.l) or (!insert_mode and key.l)) { + self.goRight(); } } - _ = termbox.tb_set_cursor( - @intCast(self.component_pos.x + self.cursor + 2), - @intCast(self.component_pos.y), + TerminalBuffer.setCursor( + self.component_pos.x + self.cursor + 2, + self.component_pos.y, ); } @@ -121,19 +111,13 @@ pub fn CyclableLabel(comptime ItemType: type, comptime ChangeItemType: type) typ if (self.list.items.len == 0) return; if (self.width < 2) return; - _ = termbox.tb_set_cell( - @intCast(self.component_pos.x), - @intCast(self.component_pos.y), - '<', - self.fg, - self.bg, - ); - _ = termbox.tb_set_cell( - @intCast(self.component_pos.x + self.width - 1), - @intCast(self.component_pos.y), - '>', - self.fg, - self.bg, + var left_arrow = Cell.init('<', self.fg, self.bg); + var right_arrow = Cell.init('>', self.fg, self.bg); + + left_arrow.put(self.component_pos.x, self.component_pos.y); + right_arrow.put( + self.component_pos.x + self.width - 1, + self.component_pos.y, ); const current_item = self.list.items[self.current]; diff --git a/src/tui/components/label.zig b/src/tui/components/label.zig deleted file mode 100644 index 21b7a99..0000000 --- a/src/tui/components/label.zig +++ /dev/null @@ -1,126 +0,0 @@ -const std = @import("std"); -const Allocator = std.mem.Allocator; - -const Cell = @import("../Cell.zig"); -const Position = @import("../Position.zig"); -const TerminalBuffer = @import("../TerminalBuffer.zig"); -const termbox = TerminalBuffer.termbox; - -pub fn Label(comptime ContextType: type) type { - return struct { - const Self = @This(); - - text: []const u8, - max_width: ?usize, - fg: u32, - bg: u32, - update_fn: ?*const fn (*Self, ContextType) anyerror!void, - is_text_allocated: bool, - component_pos: Position, - children_pos: Position, - - pub fn init( - text: []const u8, - max_width: ?usize, - fg: u32, - bg: u32, - update_fn: ?*const fn (*Self, ContextType) anyerror!void, - ) Self { - return .{ - .text = text, - .max_width = max_width, - .fg = fg, - .bg = bg, - .update_fn = update_fn, - .is_text_allocated = false, - .component_pos = TerminalBuffer.START_POSITION, - .children_pos = TerminalBuffer.START_POSITION, - }; - } - - pub fn setTextAlloc( - self: *Self, - allocator: Allocator, - comptime fmt: []const u8, - args: anytype, - ) !void { - self.text = try std.fmt.allocPrint(allocator, fmt, args); - self.is_text_allocated = true; - } - - pub fn setTextBuf( - self: *Self, - buffer: []u8, - comptime fmt: []const u8, - args: anytype, - ) !void { - self.text = try std.fmt.bufPrint(buffer, fmt, args); - self.is_text_allocated = false; - } - - pub fn setText(self: *Self, text: []const u8) void { - self.text = text; - self.is_text_allocated = false; - } - - pub fn deinit(self: Self, allocator: ?Allocator) void { - if (self.is_text_allocated) { - if (allocator) |alloc| alloc.free(self.text); - } - } - - pub fn positionX(self: *Self, original_pos: Position) void { - self.component_pos = original_pos; - self.children_pos = original_pos.addX(TerminalBuffer.strWidth(self.text)); - } - - pub fn positionY(self: *Self, original_pos: Position) void { - self.component_pos = original_pos; - self.children_pos = original_pos.addY(1); - } - - pub fn positionXY(self: *Self, original_pos: Position) void { - self.component_pos = original_pos; - self.children_pos = Position.init( - TerminalBuffer.strWidth(self.text), - 1, - ).add(original_pos); - } - - pub fn childrenPosition(self: Self) Position { - return self.children_pos; - } - - pub fn draw(self: Self) void { - if (self.max_width) |width| { - TerminalBuffer.drawConfinedText( - self.text, - self.component_pos.x, - self.component_pos.y, - width, - self.fg, - self.bg, - ); - return; - } - - TerminalBuffer.drawText( - self.text, - self.component_pos.x, - self.component_pos.y, - self.fg, - self.bg, - ); - } - - pub fn update(self: *Self, context: ContextType) !void { - if (self.update_fn) |update_fn| { - return @call( - .auto, - update_fn, - .{ self, context }, - ); - } - } - }; -} diff --git a/src/tui/keyboard.zig b/src/tui/keyboard.zig index 0dcd9b6..ea4d665 100644 --- a/src/tui/keyboard.zig +++ b/src/tui/keyboard.zig @@ -36,8 +36,8 @@ pub const Key = packed struct { tab: bool, backspace: bool, enter: bool, - space: bool, + @" ": bool, @"!": bool, @"`": bool, esc: bool, @@ -109,6 +109,18 @@ pub const Key = packed struct { x: bool, y: bool, z: bool, + + pub fn getEnabledPrintableAscii(self: Key) ?u8 { + if (self.ctrl or self.shift or self.alt) return null; + + inline for (std.meta.fields(Key)) |field| { + if (field.name.len == 1 and std.ascii.isPrint(field.name[0]) and @field(self, field.name)) { + return field.name[0]; + } + } + + return null; + } }; pub fn getKeyList(allocator: Allocator, tb_event: termbox.tb_event) !KeyList { @@ -326,7 +338,7 @@ pub fn getKeyList(allocator: Allocator, tb_event: termbox.tb_event) !KeyList { key._ = true; }, 32 => { - key.space = true; + key.@" " = true; }, 33 => { key.shift = true; From 70e95f094ad0e61ecaa3163b08574c069937dba9 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Tue, 10 Feb 2026 10:41:08 +0100 Subject: [PATCH 054/176] Fix incorrect key parsing Signed-off-by: AnErrupTion --- src/tui/keyboard.zig | 51 ++++++-------------------------------------- 1 file changed, 6 insertions(+), 45 deletions(-) diff --git a/src/tui/keyboard.zig b/src/tui/keyboard.zig index ea4d665..2e6e0e2 100644 --- a/src/tui/keyboard.zig +++ b/src/tui/keyboard.zig @@ -111,10 +111,15 @@ pub const Key = packed struct { z: bool, pub fn getEnabledPrintableAscii(self: Key) ?u8 { - if (self.ctrl or self.shift or self.alt) return null; + if (self.ctrl or self.alt) return null; inline for (std.meta.fields(Key)) |field| { if (field.name.len == 1 and std.ascii.isPrint(field.name[0]) and @field(self, field.name)) { + if (self.shift) { + if (!std.ascii.isAlphanumeric(field.name[0])) return null; + return std.ascii.toUpper(field.name[0]); + } + return field.name[0]; } } @@ -341,50 +346,26 @@ pub fn getKeyList(allocator: Allocator, tb_event: termbox.tb_event) !KeyList { key.@" " = 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; }, @@ -392,34 +373,18 @@ pub fn getKeyList(allocator: Allocator, tb_event: termbox.tb_event) !KeyList { 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; }, @@ -609,10 +574,6 @@ pub fn getKeyList(allocator: Allocator, tb_event: termbox.tb_event) !KeyList { key.@"]" = true; }, 94 => { - key.shift = true; - key.@"6" = true; - try keys.append(allocator, key); - key = std.mem.zeroes(Key); key.@"^" = true; }, From f320d3f666286dc47b6e7badbc5d9b49c071e355 Mon Sep 17 00:00:00 2001 From: Cyaxares Date: Tue, 10 Feb 2026 10:47:50 +0100 Subject: [PATCH 055/176] Add Kurdish translation (#930) Reviewed-on: https://codeberg.org/fairyglade/ly/pulls/930 Reviewed-by: AnErrupTion Co-authored-by: Cyaxares Co-committed-by: Cyaxares --- res/lang/ku.ini | 78 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 res/lang/ku.ini diff --git a/res/lang/ku.ini b/res/lang/ku.ini new file mode 100644 index 0000000..897d557 --- /dev/null +++ b/res/lang/ku.ini @@ -0,0 +1,78 @@ +authenticating = tê piştrastkirin... +brightness_down = ronahiyê kêm bike +brightness_up = ronahiyê bilind bike +capslock = tîpên girdek (capslock) +custom = kesane +err_alloc = veqetandina bîrê têk çû +err_args = argumanên rêzika fermanê nehatin analîzkirin +err_autologin_session = danişîna têketina xweber nehate dîtin +err_bounds = îndeksa derveyî sînor +err_brightness_change = guherandina ronahiyê têk çû +err_chdir = vekirina peldanka malê têk çû +err_clock_too_long = rêzika demjimêrê pir dirêj e +err_config = pela rêkxistinê nehat analîzkirin +err_crawl = gerandina pelrêçên danişînê têk çû +err_dgn_oob = peyama têketinê +err_domain = navpara nederbasdar +err_empty_password = borînpeyv nabe ku vala be +err_envlist = girtina lîsteya jîngehê (envlist) têk çû +err_get_active_tty = girtina tty ya çalak têk çû +err_hibernate = fermana cemidaninê nehat xebitandin +err_hostname = girtina navê mêvandar têk çû +err_inactivity = fermana neçalaktiyê nehat xebitandin +err_lock_state = girtina rewşa kilîtkirinê têk çû +err_log = vekirina pelê têkeinê têk çû +err_mlock = kilîtkirina bîra borînpeyvê têk çû +err_null = nîşandera null +err_numlock = sazkirina numlock têk çû +err_pam = danûstendina pam têk çû +err_pam_abort = danûstendina pam hate têkbirin +err_pam_acct_expired = dema jimarê derbas bûye +err_pam_auth = şaşetiya piştrastkirinê +err_pam_authinfo_unavail = zanyariyên bikarhêner nehatin girtin +err_pam_authok_reqd = dema nîşandanê derbas bûye +err_pam_buf = şaşetiya bîra demkî +err_pam_cred_err = sazkirina rastkitinê têk çû +err_pam_cred_expired = dema rastkitinê derbas bûye +err_pam_cred_insufficient = rastkitinê kêm +err_pam_cred_unavail = girtina rastkitinê têk çû +err_pam_maxtries = sînorê hewldanên herî bilind hat gihîştin +err_pam_perm_denied = mafdayîn hat paşguhkirin +err_pam_session = şaşetiya danişînê +err_pam_sys = şaşetiya pergalê +err_pam_user_unknown = bikarhênerê nenas +err_path = sazkirina rêgehê têk çû +err_perm_dir = guhertina pelrêçê heyî têk çû +err_perm_group = kêmkirina mafdayînên komê têk çû +err_perm_user = kêmkirina mafdayînên bikarhêner têk çû +err_pwnam = girtina zanyariyên bikarhêner têk çû +err_sleep = fermana cemidaninê nehat xebitandin +err_start = fermana destpêkirinê nehat xebitandin +err_battery = barkirina rewşa betariyê têk çû +err_switch_tty = guhertina tty têk çû +err_tty_ctrl = guhertina kontrola tty têk çû +err_no_users = tu bikarhêner nehatin dîtin +err_uid_range = girtina rêjeya dînamîk a sînorê uid têk çû +err_user_gid = sazkirina GID a bikarhêner têk çû +err_user_init = destpêkirina bikarhêner têk çû +err_user_uid = sazkirina UID a bikarhêner têk çû +err_xauth = fermana xauth têk çû +err_xcb_conn = girêdana xcb têk çû +err_xsessions_dir = dîtina peldanka danişînan têk çû +err_xsessions_open = vekirina peldanka danişînan têk çû +hibernate = bicemidîne +insert = têxîne +login = têketin +logout = derkeve +no_x11_support = piştgiriya x11 di dema berhevkirinê de hatiye girtin +normal = normal +numlock = numlock +other = ên din +password = borînpeyv +restart = ji nû ve bide destpêkirin +shell = shell +shutdown = vemirîne +sleep = têxîne xewê +wayland = wayland +x11 = x11 +xinitrc = xinitrc From d268d5bb4551d4f5f7b43b6eea3010873a8352e2 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Tue, 10 Feb 2026 17:43:55 +0100 Subject: [PATCH 056/176] Add the cascade animation as a separate widget Signed-off-by: AnErrupTion --- src/animations/Cascade.zig | 81 +++++++++++++++++ src/animations/ColorMix.zig | 12 ++- src/animations/Doom.zig | 15 +++- src/animations/DurFile.zig | 16 +++- src/animations/GameOfLife.zig | 14 ++- src/animations/Matrix.zig | 14 ++- src/main.zig | 135 ++++++++++++++--------------- src/tui/Cell.zig | 2 +- src/tui/TerminalBuffer.zig | 64 +++++--------- src/tui/components/BigLabel.zig | 4 +- src/tui/components/CenteredBox.zig | 15 +++- src/tui/components/InfoLine.zig | 16 ++-- src/tui/components/Label.zig | 4 +- src/tui/components/Session.zig | 16 ++-- src/tui/components/Text.zig | 16 ++-- 15 files changed, 276 insertions(+), 148 deletions(-) create mode 100644 src/animations/Cascade.zig diff --git a/src/animations/Cascade.zig b/src/animations/Cascade.zig new file mode 100644 index 0000000..06cfdfe --- /dev/null +++ b/src/animations/Cascade.zig @@ -0,0 +1,81 @@ +const std = @import("std"); +const math = std.math; + +const Cell = @import("../tui/Cell.zig"); +const TerminalBuffer = @import("../tui/TerminalBuffer.zig"); +const Widget = @import("../tui/Widget.zig"); + +const Cascade = @This(); + +buffer: *TerminalBuffer, +current_auth_fails: *usize, +max_auth_fails: usize, + +pub fn init( + buffer: *TerminalBuffer, + current_auth_fails: *usize, + max_auth_fails: usize, +) Cascade { + return .{ + .buffer = buffer, + .current_auth_fails = current_auth_fails, + .max_auth_fails = max_auth_fails, + }; +} + +pub fn widget(self: *Cascade) Widget { + return Widget.init( + self, + null, + null, + draw, + null, + null, + ); +} + +fn draw(self: *Cascade) void { + while (self.current_auth_fails.* >= self.max_auth_fails) { + std.Thread.sleep(std.time.ns_per_ms * 10); + + var changed = false; + var y = self.buffer.height - 2; + + while (y > 0) : (y -= 1) { + for (0..self.buffer.width) |x| { + const cell = TerminalBuffer.getCell(x, y - 1); + const cell_under = TerminalBuffer.getCell(x, y); + + // This shouldn't happen under normal circumstances, but because + // this is a *secret* animation, there's no need to care that much + if (cell == null or cell_under == null) continue; + + const char: u8 = @truncate(cell.?.ch); + if (std.ascii.isWhitespace(char)) continue; + + const char_under: u8 = @truncate(cell_under.?.ch); + if (!std.ascii.isWhitespace(char_under)) continue; + + changed = true; + + if ((self.buffer.random.int(u16) % 10) > 7) continue; + + cell.?.put(x, y); + + var space = Cell.init( + ' ', + cell_under.?.fg, + cell_under.?.bg, + ); + space.put(x, y - 1); + } + } + + if (!changed) { + std.Thread.sleep(std.time.ns_per_s * 7); + self.current_auth_fails.* = 0; + } + + TerminalBuffer.presentBuffer(); + } +} diff --git a/src/animations/ColorMix.zig b/src/animations/ColorMix.zig index 21edc09..206986e 100644 --- a/src/animations/ColorMix.zig +++ b/src/animations/ColorMix.zig @@ -17,14 +17,22 @@ fn length(vec: Vec2) f32 { } terminal_buffer: *TerminalBuffer, +timeout: *bool, frames: u64, pattern_cos_mod: f32, pattern_sin_mod: f32, palette: [palette_len]Cell, -pub fn init(terminal_buffer: *TerminalBuffer, col1: u32, col2: u32, col3: u32) ColorMix { +pub fn init( + terminal_buffer: *TerminalBuffer, + col1: u32, + col2: u32, + col3: u32, + timeout: *bool, +) ColorMix { return .{ .terminal_buffer = terminal_buffer, + .timeout = timeout, .frames = 0, .pattern_cos_mod = terminal_buffer.random.float(f32) * math.pi * 2.0, .pattern_sin_mod = terminal_buffer.random.float(f32) * math.pi * 2.0, @@ -57,6 +65,8 @@ pub fn widget(self: *ColorMix) Widget { } fn draw(self: *ColorMix) void { + if (self.timeout.*) return; + self.frames +%= 1; const time: f32 = @as(f32, @floatFromInt(self.frames)) * time_scale; diff --git a/src/animations/Doom.zig b/src/animations/Doom.zig index e0a09b1..a657dbc 100644 --- a/src/animations/Doom.zig +++ b/src/animations/Doom.zig @@ -13,12 +13,22 @@ pub const SPREAD_MAX = 4; allocator: Allocator, terminal_buffer: *TerminalBuffer, +timeout: *bool, buffer: []u8, height: u8, spread: u8, fire: [STEPS + 1]Cell, -pub fn init(allocator: Allocator, terminal_buffer: *TerminalBuffer, top_color: u32, middle_color: u32, bottom_color: u32, fire_height: u8, fire_spread: u8) !Doom { +pub fn init( + allocator: Allocator, + terminal_buffer: *TerminalBuffer, + top_color: u32, + middle_color: u32, + bottom_color: u32, + fire_height: u8, + fire_spread: u8, + timeout: *bool, +) !Doom { const buffer = try allocator.alloc(u8, terminal_buffer.width * terminal_buffer.height); initBuffer(buffer, terminal_buffer.width); @@ -42,6 +52,7 @@ pub fn init(allocator: Allocator, terminal_buffer: *TerminalBuffer, top_color: u return .{ .allocator = allocator, .terminal_buffer = terminal_buffer, + .timeout = timeout, .buffer = buffer, .height = @min(HEIGHT_MAX, fire_height), .spread = @min(SPREAD_MAX, fire_spread), @@ -71,6 +82,8 @@ fn realloc(self: *Doom) !void { } fn draw(self: *Doom) void { + if (self.timeout.*) return; + for (0..self.terminal_buffer.width) |x| { // We start from 1 so that we always have the topmost line when spreading fire for (1..self.terminal_buffer.height) |y| { diff --git a/src/animations/DurFile.zig b/src/animations/DurFile.zig index f3f2eb2..9ed9207 100644 --- a/src/animations/DurFile.zig +++ b/src/animations/DurFile.zig @@ -307,6 +307,7 @@ frames: u64, frame_size: UVec2, start_pos: IVec2, full_color: bool, +timeout: *bool, frame_time: u32, time_previous: i64, is_color_format_16: bool, @@ -357,7 +358,17 @@ fn calc_frame_size(terminal_buffer: *TerminalBuffer, dur_movie: *DurFormat) UVec return .{ frame_width, frame_height }; } -pub fn init(allocator: Allocator, terminal_buffer: *TerminalBuffer, log_file: *LogFile, file_path: []const u8, offset_alignment: DurOffsetAlignment, x_offset: i32, y_offset: i32, full_color: bool) !DurFile { +pub fn init( + allocator: Allocator, + terminal_buffer: *TerminalBuffer, + log_file: *LogFile, + file_path: []const u8, + offset_alignment: DurOffsetAlignment, + x_offset: i32, + y_offset: i32, + full_color: bool, + timeout: *bool, +) !DurFile { var dur_movie: DurFormat = .init(allocator); dur_movie.create_from_file(allocator, file_path) catch |err| switch (err) { @@ -395,6 +406,7 @@ pub fn init(allocator: Allocator, terminal_buffer: *TerminalBuffer, log_file: *L .frame_size = frame_size, .start_pos = start_pos, .full_color = full_color, + .timeout = timeout, .dur_movie = dur_movie, .frame_time = frame_time, .is_color_format_16 = eql(u8, dur_movie.colorFormat.?, "16"), @@ -425,6 +437,8 @@ fn realloc(self: *DurFile) !void { } fn draw(self: *DurFile) void { + if (self.timeout.*) return; + const current_frame = self.dur_movie.frames.items[self.frames]; const buf_width: u32 = @intCast(self.terminal_buffer.width); diff --git a/src/animations/GameOfLife.zig b/src/animations/GameOfLife.zig index ac87226..d90590c 100644 --- a/src/animations/GameOfLife.zig +++ b/src/animations/GameOfLife.zig @@ -26,11 +26,20 @@ fg_color: u32, entropy_interval: usize, frame_delay: usize, initial_density: f32, +timeout: *bool, dead_cell: Cell, width: usize, height: usize, -pub fn init(allocator: Allocator, terminal_buffer: *TerminalBuffer, fg_color: u32, entropy_interval: usize, frame_delay: usize, initial_density: f32) !GameOfLife { +pub fn init( + allocator: Allocator, + terminal_buffer: *TerminalBuffer, + fg_color: u32, + entropy_interval: usize, + frame_delay: usize, + initial_density: f32, + timeout: *bool, +) !GameOfLife { const width = terminal_buffer.width; const height = terminal_buffer.height; const grid_size = width * height; @@ -49,6 +58,7 @@ pub fn init(allocator: Allocator, terminal_buffer: *TerminalBuffer, fg_color: u3 .entropy_interval = entropy_interval, .frame_delay = frame_delay, .initial_density = initial_density, + .timeout = timeout, .dead_cell = .{ .ch = DEAD_CHAR, .fg = @intCast(TerminalBuffer.Color.DEFAULT), .bg = terminal_buffer.bg }, .width = width, .height = height, @@ -94,6 +104,8 @@ fn realloc(self: *GameOfLife) !void { } fn draw(self: *GameOfLife) void { + if (self.timeout.*) return; + // Update game state at controlled frame rate self.frame_counter += 1; if (self.frame_counter >= self.frame_delay) { diff --git a/src/animations/Matrix.zig b/src/animations/Matrix.zig index a3c4138..25ff6d1 100644 --- a/src/animations/Matrix.zig +++ b/src/animations/Matrix.zig @@ -34,9 +34,18 @@ fg: u32, head_col: u32, min_codepoint: u16, max_codepoint: u16, +timeout: *bool, default_cell: Cell, -pub fn init(allocator: Allocator, terminal_buffer: *TerminalBuffer, fg: u32, head_col: u32, min_codepoint: u16, max_codepoint: u16) !Matrix { +pub fn init( + allocator: Allocator, + terminal_buffer: *TerminalBuffer, + fg: u32, + head_col: u32, + min_codepoint: u16, + max_codepoint: u16, + timeout: *bool, +) !Matrix { const dots = try allocator.alloc(Dot, terminal_buffer.width * (terminal_buffer.height + 1)); const lines = try allocator.alloc(Line, terminal_buffer.width); @@ -53,6 +62,7 @@ pub fn init(allocator: Allocator, terminal_buffer: *TerminalBuffer, fg: u32, hea .head_col = head_col, .min_codepoint = min_codepoint, .max_codepoint = max_codepoint - min_codepoint, + .timeout = timeout, .default_cell = .{ .ch = ' ', .fg = fg, .bg = terminal_buffer.bg }, }; } @@ -84,6 +94,8 @@ fn realloc(self: *Matrix) !void { } fn draw(self: *Matrix) void { + if (self.timeout.*) return; + const buf_height = self.terminal_buffer.height; const buf_width = self.terminal_buffer.width; self.count += 1; diff --git a/src/main.zig b/src/main.zig index c5294d8..9e9113f 100644 --- a/src/main.zig +++ b/src/main.zig @@ -15,6 +15,7 @@ const LogFile = ly_core.LogFile; const SharedError = ly_core.SharedError; const IniParser = ly_core.IniParser; +const Cascade = @import("animations/Cascade.zig"); const ColorMix = @import("animations/ColorMix.zig"); const Doom = @import("animations/Doom.zig"); const DurFile = @import("animations/DurFile.zig"); @@ -74,7 +75,6 @@ const UiState = struct { buffer: TerminalBuffer, labels_max_length: usize, animation_timed_out: bool, - animation: ?Widget, shutdown_label: Label, restart_label: Label, sleep_label: Label, @@ -517,6 +517,7 @@ pub fn main() !void { state.buffer.border_fg, state.buffer.fg, state.buffer.bg, + &updateBox, ); state.info_line = InfoLine.init( @@ -867,10 +868,9 @@ pub fn main() !void { } // Initialize the animation, if any + var animation: ?Widget = null; switch (state.config.animation) { - .none => { - state.animation = null; - }, + .none => {}, .doom => { var doom = try Doom.init( state.allocator, @@ -880,8 +880,9 @@ pub fn main() !void { state.config.doom_bottom_color, state.config.doom_fire_height, state.config.doom_fire_spread, + &state.animation_timed_out, ); - state.animation = doom.widget(); + animation = doom.widget(); }, .matrix => { var matrix = try Matrix.init( @@ -891,8 +892,9 @@ pub fn main() !void { state.config.cmatrix_head_col, state.config.cmatrix_min_codepoint, state.config.cmatrix_max_codepoint, + &state.animation_timed_out, ); - state.animation = matrix.widget(); + animation = matrix.widget(); }, .colormix => { var color_mix = ColorMix.init( @@ -900,8 +902,9 @@ pub fn main() !void { state.config.colormix_col1, state.config.colormix_col2, state.config.colormix_col3, + &state.animation_timed_out, ); - state.animation = color_mix.widget(); + animation = color_mix.widget(); }, .gameoflife => { var game_of_life = try GameOfLife.init( @@ -911,8 +914,9 @@ pub fn main() !void { state.config.gameoflife_entropy_interval, state.config.gameoflife_frame_delay, state.config.gameoflife_initial_density, + &state.animation_timed_out, ); - state.animation = game_of_life.widget(); + animation = game_of_life.widget(); }, .dur_file => { var dur = try DurFile.init( @@ -924,11 +928,18 @@ pub fn main() !void { state.config.dur_x_offset, state.config.dur_y_offset, state.config.full_color, + &state.animation_timed_out, ); - state.animation = dur.widget(); + animation = dur.widget(); }, } - defer if (state.animation) |*a| a.deinit(); + defer if (animation) |*a| a.deinit(); + + var cascade = Cascade.init( + &state.buffer, + &state.auth_fails, + state.config.auth_fails, + ); state.auth_fails = 0; state.run = true; @@ -966,9 +977,14 @@ 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) var widgets: std.ArrayList(Widget) = .empty; defer widgets.deinit(state.allocator); + if (animation) |a| { + try widgets.append(state.allocator, a); + } if (!state.config.hide_key_hints) { try widgets.append(state.allocator, state.shutdown_label.widget()); try widgets.append(state.allocator, state.restart_label.widget()); @@ -1006,9 +1022,12 @@ pub fn main() !void { if (!state.config.hide_version_string) { try widgets.append(state.allocator, state.version_label.widget()); } + if (state.config.auth_fails > 0) { + try widgets.append(state.allocator, cascade.widget()); + } // Position components and place cursor accordingly - try updateComponents(&state, widgets); + for (widgets.items) |*widget| try widget.update(&state); positionComponents(&state); switch (state.active_input) { @@ -1085,9 +1104,11 @@ pub fn main() !void { ); } + if (state.is_autologin) _ = try authenticate(&state); + while (state.run) { if (state.update) { - try updateComponents(&state, widgets); + for (widgets.items) |*widget| try widget.update(&state); switch (state.active_input) { .info_line => state.info_line.label.handle(null, state.insert_mode), @@ -1099,7 +1120,11 @@ pub fn main() !void { }, } - if (!try drawUi(&state, widgets)) continue; + try TerminalBuffer.clearScreen(false); + + for (widgets.items) |*widget| widget.draw(); + + TerminalBuffer.presentBuffer(); } var timeout: i32 = -1; @@ -1113,7 +1138,7 @@ pub fn main() !void { if (state.config.animation_timeout_sec > 0 and time.seconds - animation_time_start.seconds > state.config.animation_timeout_sec) { state.animation_timed_out = true; - if (state.animation) |*a| a.deinit(); + if (animation) |*a| a.deinit(); } } else if (state.config.bigclock != .none and state.config.clock == null) { const time = try interop.getTimeOfDay(); @@ -1155,25 +1180,11 @@ pub fn main() !void { } } - // Skip event polling if autologin is set, use simulated Enter key press instead - if (state.is_autologin) { - event = .{ - .type = termbox.TB_EVENT_KEY, - .key = termbox.TB_KEY_ENTER, - .ch = 0, - .w = 0, - .h = 0, - .x = 0, - .y = 0, - .mod = 0, - }; - } else { - const event_error = if (timeout == -1) termbox.tb_poll_event(&event) else termbox.tb_peek_event(&event, timeout); + const event_error = if (timeout == -1) termbox.tb_poll_event(&event) else termbox.tb_peek_event(&event, timeout); - state.update = timeout != -1; + state.update = timeout != -1; - if (event_error < 0) continue; - } + if (event_error < 0) continue; // Input of some kind was detected, so reset the inactivity timer inactivity_time_start = try interop.getTimeOfDay(); @@ -1184,7 +1195,7 @@ pub fn main() !void { try state.log_file.info("tui", "screen resolution updated to {d}x{d}", .{ state.buffer.width, state.buffer.height }); - if (state.animation) |*a| a.realloc() catch |err| { + if (animation) |*a| a.realloc() catch |err| { try state.info_line.addMessage( state.lang.err_alloc, state.config.error_bg, @@ -1197,6 +1208,21 @@ pub fn main() !void { ); }; + for (widgets.items) |*widget| { + widget.realloc() 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 reallocate widget: {s}", + .{@errorName(err)}, + ); + }; + } + positionComponents(&state); state.update = true; @@ -1617,43 +1643,6 @@ fn increaseBrightnessCmd(ptr: *anyopaque) !bool { return false; } -fn updateComponents(state: *UiState, widgets: std.ArrayList(Widget)) !void { - for (widgets.items) |*widget| { - try widget.update(state); - } -} - -fn drawUi(state: *UiState, widgets: std.ArrayList(Widget)) !bool { - // If the user entered a wrong password 10 times in a row, play a cascade animation, else update normally - if (state.config.auth_fails > 0 and state.auth_fails >= state.config.auth_fails) { - std.Thread.sleep(std.time.ns_per_ms * 10); - state.update = state.buffer.cascade(); - - if (!state.update) { - std.Thread.sleep(std.time.ns_per_s * 7); - state.auth_fails = 0; - } - - TerminalBuffer.presentBuffer(); - return false; - } - - try TerminalBuffer.clearScreen(false); - - if (!state.animation_timed_out) if (state.animation) |*a| a.draw(); - - for (widgets.items) |*widget| { - widget.draw(); - } - - if (state.config.vi_mode) { - state.box.bottom_title = if (state.insert_mode) state.lang.insert else state.lang.normal; - } - - TerminalBuffer.presentBuffer(); - return true; -} - fn updateNumlock(self: *Label, ptr: *anyopaque) !void { var state: *UiState = @ptrCast(@alignCast(ptr)); @@ -1765,6 +1754,14 @@ fn updateBigClock(self: *BigLabel, ptr: *anyopaque) !void { self.setText(clock_str); } +fn updateBox(self: *CenteredBox, ptr: *anyopaque) !void { + const state: *UiState = @ptrCast(@alignCast(ptr)); + + if (state.config.vi_mode) { + self.bottom_title = if (state.insert_mode) state.lang.insert else state.lang.normal; + } +} + fn updateSessionSpecifier(self: *Label, ptr: *anyopaque) !void { const state: *UiState = @ptrCast(@alignCast(ptr)); diff --git a/src/tui/Cell.zig b/src/tui/Cell.zig index 2a6fc72..35d0cf0 100644 --- a/src/tui/Cell.zig +++ b/src/tui/Cell.zig @@ -17,5 +17,5 @@ pub fn init(ch: u32, fg: u32, bg: u32) Cell { pub fn put(self: Cell, x: usize, y: usize) void { if (self.ch == 0) return; - TerminalBuffer.setCell(x, y, self.ch, self.fg, self.bg); + TerminalBuffer.setCell(x, y, self); } diff --git a/src/tui/TerminalBuffer.zig b/src/tui/TerminalBuffer.zig index 8250acb..56b96da 100644 --- a/src/tui/TerminalBuffer.zig +++ b/src/tui/TerminalBuffer.zig @@ -172,19 +172,29 @@ pub fn presentBuffer() void { _ = termbox.tb_present(); } -pub fn setCell( - x: usize, - y: usize, - ch: u32, - fg: u32, - bg: u32, -) void { +pub fn getCell(x: usize, y: usize) ?Cell { + var maybe_cell: ?*termbox.tb_cell = undefined; + _ = termbox.tb_get_cell( + @intCast(x), + @intCast(y), + 1, + &maybe_cell, + ); + + if (maybe_cell) |cell| { + return Cell.init(cell.ch, cell.fg, cell.bg); + } + + return null; +} + +pub fn setCell(x: usize, y: usize, cell: Cell) void { _ = termbox.tb_set_cell( @intCast(x), @intCast(y), - ch, - fg, - bg, + cell.ch, + cell.fg, + cell.bg, ); } @@ -201,40 +211,6 @@ pub fn reclaim(self: TerminalBuffer) !void { } } -pub fn cascade(self: TerminalBuffer) bool { - var changed = false; - var y = self.height - 2; - - while (y > 0) : (y -= 1) { - for (0..self.width) |x| { - var cell: ?*termbox.tb_cell = undefined; - var cell_under: ?*termbox.tb_cell = undefined; - - _ = termbox.tb_get_cell(@intCast(x), @intCast(y - 1), 1, &cell); - _ = termbox.tb_get_cell(@intCast(x), @intCast(y), 1, &cell_under); - - // This shouldn't happen under normal circumstances, but because - // this is a *secret* animation, there's no need to care that much - if (cell == null or cell_under == null) continue; - - const char: u8 = @truncate(cell.?.ch); - if (std.ascii.isWhitespace(char)) continue; - - const char_under: u8 = @truncate(cell_under.?.ch); - if (!std.ascii.isWhitespace(char_under)) continue; - - changed = true; - - if ((self.random.int(u16) % 10) > 7) continue; - - _ = termbox.tb_set_cell(@intCast(x), @intCast(y), cell.?.ch, cell.?.fg, cell.?.bg); - _ = termbox.tb_set_cell(@intCast(x), @intCast(y - 1), ' ', cell_under.?.fg, cell_under.?.bg); - } - } - - 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, '+'); diff --git a/src/tui/components/BigLabel.zig b/src/tui/components/BigLabel.zig index 0e6bf51..625647f 100644 --- a/src/tui/components/BigLabel.zig +++ b/src/tui/components/BigLabel.zig @@ -140,7 +140,7 @@ pub fn childrenPosition(self: BigLabel) Position { return self.children_pos; } -pub fn draw(self: *BigLabel) void { +fn draw(self: *BigLabel) void { for (self.text, 0..) |c, i| { const clock_cell = clockCell( c, @@ -159,7 +159,7 @@ pub fn draw(self: *BigLabel) void { } } -pub fn update(self: *BigLabel, context: *anyopaque) !void { +fn update(self: *BigLabel, context: *anyopaque) !void { if (self.update_fn) |update_fn| { return @call( .auto, diff --git a/src/tui/components/CenteredBox.zig b/src/tui/components/CenteredBox.zig index 38c566a..fd46057 100644 --- a/src/tui/components/CenteredBox.zig +++ b/src/tui/components/CenteredBox.zig @@ -19,6 +19,7 @@ bottom_title: ?[]const u8, border_fg: u32, title_fg: u32, bg: u32, +update_fn: ?*const fn (*CenteredBox, *anyopaque) anyerror!void, left_pos: Position, right_pos: Position, children_pos: Position, @@ -36,6 +37,7 @@ pub fn init( border_fg: u32, title_fg: u32, bg: u32, + update_fn: ?*const fn (*CenteredBox, *anyopaque) anyerror!void, ) CenteredBox { return .{ .buffer = buffer, @@ -50,6 +52,7 @@ pub fn init( .border_fg = border_fg, .title_fg = title_fg, .bg = bg, + .update_fn = update_fn, .left_pos = TerminalBuffer.START_POSITION, .right_pos = TerminalBuffer.START_POSITION, .children_pos = TerminalBuffer.START_POSITION, @@ -90,7 +93,7 @@ pub fn childrenPosition(self: CenteredBox) Position { return self.children_pos; } -pub fn draw(self: *CenteredBox) void { +fn draw(self: *CenteredBox) void { if (self.show_borders) { var left_up = Cell.init( self.buffer.box_chars.left_up, @@ -172,3 +175,13 @@ pub fn draw(self: *CenteredBox) void { ); } } + +fn update(self: *CenteredBox, ctx: *anyopaque) !void { + if (self.update_fn) |update_fn| { + return @call( + .auto, + update_fn, + .{ self, ctx }, + ); + } +} diff --git a/src/tui/components/InfoLine.zig b/src/tui/components/InfoLine.zig index d059c81..e3d9904 100644 --- a/src/tui/components/InfoLine.zig +++ b/src/tui/components/InfoLine.zig @@ -56,14 +56,6 @@ pub fn widget(self: *InfoLine) Widget { ); } -pub fn draw(self: *InfoLine) void { - self.label.draw(); -} - -pub fn handle(self: *InfoLine, maybe_key: ?keyboard.Key, insert_mode: bool) !void { - self.label.handle(maybe_key, insert_mode); -} - pub fn addMessage(self: *InfoLine, text: []const u8, bg: u32, fg: u32) !void { if (text.len == 0) return; @@ -91,6 +83,14 @@ pub fn clearRendered(self: InfoLine, allocator: Allocator) !void { ); } +fn draw(self: *InfoLine) void { + self.label.draw(); +} + +fn handle(self: *InfoLine, maybe_key: ?keyboard.Key, insert_mode: bool) !void { + self.label.handle(maybe_key, insert_mode); +} + fn drawItem(label: *MessageLabel, message: Message, x: usize, y: usize, width: usize) void { if (message.width == 0) return; diff --git a/src/tui/components/Label.zig b/src/tui/components/Label.zig index fcc7aa1..fb95a98 100644 --- a/src/tui/components/Label.zig +++ b/src/tui/components/Label.zig @@ -98,7 +98,7 @@ pub fn childrenPosition(self: Label) Position { return self.children_pos; } -pub fn draw(self: *Label) void { +fn draw(self: *Label) void { if (self.max_width) |width| { TerminalBuffer.drawConfinedText( self.text, @@ -120,7 +120,7 @@ pub fn draw(self: *Label) void { ); } -pub fn update(self: *Label, ctx: *anyopaque) !void { +fn update(self: *Label, ctx: *anyopaque) !void { if (self.update_fn) |update_fn| { return @call( .auto, diff --git a/src/tui/components/Session.zig b/src/tui/components/Session.zig index ced126c..00782b0 100644 --- a/src/tui/components/Session.zig +++ b/src/tui/components/Session.zig @@ -66,14 +66,6 @@ pub fn widget(self: *Session) Widget { ); } -pub fn draw(self: *Session) void { - self.label.draw(); -} - -pub fn handle(self: *Session, maybe_key: ?keyboard.Key, insert_mode: bool) !void { - self.label.handle(maybe_key, insert_mode); -} - pub fn addEnvironment(self: *Session, environment: Environment) !void { const env = Env{ .environment = environment, .index = self.label.list.items.len }; @@ -81,6 +73,14 @@ pub fn addEnvironment(self: *Session, environment: Environment) !void { addedSession(env, self.user_list); } +fn draw(self: *Session) void { + self.label.draw(); +} + +fn handle(self: *Session, maybe_key: ?keyboard.Key, insert_mode: bool) !void { + self.label.handle(maybe_key, insert_mode); +} + fn addedSession(env: Env, user_list: *UserList) void { const user = user_list.label.list.items[user_list.label.current]; if (!user.first_run) return; diff --git a/src/tui/components/Text.zig b/src/tui/components/Text.zig index e81feb6..23dec80 100644 --- a/src/tui/components/Text.zig +++ b/src/tui/components/Text.zig @@ -87,6 +87,13 @@ pub fn childrenPosition(self: Text) Position { return self.children_pos; } +pub fn clear(self: *Text) void { + self.text.clearRetainingCapacity(); + self.end = 0; + self.cursor = 0; + self.visible_start = 0; +} + pub fn handle(self: *Text, maybe_key: ?keyboard.Key, insert_mode: bool) !void { if (maybe_key) |key| { if (key.left or (!insert_mode and (key.h or key.backspace))) { @@ -117,7 +124,7 @@ pub fn handle(self: *Text, maybe_key: ?keyboard.Key, insert_mode: bool) !void { ); } -pub fn draw(self: *Text) void { +fn draw(self: *Text) void { if (self.masked) { if (self.maybe_mask) |mask| { if (self.width < 1) return; @@ -158,13 +165,6 @@ pub fn draw(self: *Text) void { ); } -pub fn clear(self: *Text) void { - self.text.clearRetainingCapacity(); - self.end = 0; - self.cursor = 0; - self.visible_start = 0; -} - fn goLeft(self: *Text) void { if (self.cursor == 0) return; if (self.visible_start > 0) self.visible_start -= 1; From 4a72e41e44624f0f2d4a147f67e22dfed3e34b79 Mon Sep 17 00:00:00 2001 From: hynak Date: Tue, 10 Feb 2026 22:20:30 +0100 Subject: [PATCH 057/176] Fix default startup script (#929) ## What are the changes about? Discussed in !920. Adds fixes to the startup script by removing the array usage (some shells use arrays different/unsupported) and adds stdout_behavior Inherit flag to the child process to propagate the echos to the TTY. ## What existing issue does this resolve? N/A ## Pre-requisites - [x] I have tested & confirmed the changes work locally Reviewed-on: https://codeberg.org/fairyglade/ly/pulls/929 Reviewed-by: AnErrupTion Co-authored-by: hynak Co-committed-by: hynak --- res/startup.sh | 44 +++++++++++++++++++++++--------------------- src/main.zig | 2 +- 2 files changed, 24 insertions(+), 22 deletions(-) mode change 100644 => 100755 res/startup.sh diff --git a/res/startup.sh b/res/startup.sh old mode 100644 new mode 100755 index 6685403..2297e2e --- a/res/startup.sh +++ b/res/startup.sh @@ -7,32 +7,34 @@ # Colors are in red/green/blue hex (the current colors are a brighter palette than default) # #if [ "$TERM" = "linux" ]; then -# declare -a colors=( -# [0]="232323" # black -# [1]="D75F5F" # dark red -# [2]="87AF5F" # dark green -# [3]="D7AF87" # dark yellow -# [4]="8787AF" # dark blue -# [5]="BD53A5" # dark magenta -# [6]="5FAFAF" # dark cyan -# [7]="E5E5E5" # light gray -# [8]="2B2B2B" # dark gray -# [9]="E33636" # red -# [10]="98E34D" # green -# [11]="FFD75F" # yellow -# [12]="7373C9" # blue -# [13]="D633B2" # magenta -# [14]="44C9C9" # cyan -# [15]="FFFFFF" # white -# ) +# BLACK="232323" +# DARK_RED="D75F5F" +# DARK_GREEN="87AF5F" +# DARK_YELLOW="D7AF87" +# DARK_BLUE="8787AF" +# DARK_MAGENTA="BD53A5" +# DARK_CYAN="5FAFAF" +# LIGHT_GRAY="E5E5E5" +# DARK_GRAY="2B2B2B" +# RED="E33636" +# GREEN="98E34D" +# YELLOW="FFD75F" +# BLUE="7373C9" +# MAGENTA="D633B2" +# CYAN="44C9C9" +# WHITE="FFFFFF" +# +# COLORS="${BLACK} ${DARK_RED} ${DARK_GREEN} ${DARK_YELLOW} ${DARK_BLUE} ${DARK_MAGENTA} ${DARK_CYAN} ${LIGHT_GRAY} ${DARK_GRAY} ${RED} ${GREEN} ${YELLOW} ${BLUE} ${MAGENTA} ${CYAN} ${WHITE}" # # control_palette_str="\e]P" # -# for i in ${!colors[@]} +# i=0 +# while [ $i -lt 16 ] # do -# echo -en "${control_palette_str}$( printf "%x" ${i} )${colors[i]}" +# echo -en "${control_palette_str}$( printf "%x" ${i} )$(echo $COLORS | cut -d ' ' -f`expr $i + 1`)" +# +# i=`expr $i + 1` # done # # clear # for fixing background artifacting after changing color #fi - diff --git a/src/main.zig b/src/main.zig index 9e9113f..832b3af 100644 --- a/src/main.zig +++ b/src/main.zig @@ -312,7 +312,7 @@ pub fn main() !void { if (state.config.start_cmd) |start_cmd| { var start = std.process.Child.init(&[_][]const u8{ "/bin/sh", "-c", start_cmd }, state.allocator); - start.stdout_behavior = .Ignore; + start.stdout_behavior = .Inherit; start.stderr_behavior = .Ignore; handle_start_cmd: { From b389e379faebe80d1cf7285de961ec6d01f644d3 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Wed, 11 Feb 2026 21:00:00 +0100 Subject: [PATCH 058/176] Make handling inputs widget-independent Signed-off-by: AnErrupTion --- src/main.zig | 157 +++++++++++++++++++++++++++------------------ src/tui/Widget.zig | 130 ++++++++++++++++++------------------- 2 files changed, 160 insertions(+), 127 deletions(-) diff --git a/src/main.zig b/src/main.zig index 832b3af..71a410b 100644 --- a/src/main.zig +++ b/src/main.zig @@ -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), diff --git a/src/tui/Widget.zig b/src/tui/Widget.zig index 6ffb0b3..80413a7 100644 --- a/src/tui/Widget.zig +++ b/src/tui/Widget.zig @@ -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 }, + ); + } } From 6773f7478838aa17848eafd87f556f30de9b0994 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Wed, 11 Feb 2026 21:51:07 +0100 Subject: [PATCH 059/176] Add widget display name to improve logging Signed-off-by: AnErrupTion --- src/animations/Cascade.zig | 1 + src/animations/ColorMix.zig | 1 + src/animations/Doom.zig | 1 + src/animations/DurFile.zig | 1 + src/animations/GameOfLife.zig | 1 + src/animations/Matrix.zig | 1 + src/main.zig | 25 ++++++------------------- src/tui/Widget.zig | 3 +++ src/tui/components/BigLabel.zig | 1 + src/tui/components/CenteredBox.zig | 1 + src/tui/components/InfoLine.zig | 1 + src/tui/components/Label.zig | 1 + src/tui/components/Session.zig | 1 + src/tui/components/Text.zig | 1 + src/tui/components/UserList.zig | 1 + 15 files changed, 22 insertions(+), 19 deletions(-) diff --git a/src/animations/Cascade.zig b/src/animations/Cascade.zig index 06cfdfe..2ec2452 100644 --- a/src/animations/Cascade.zig +++ b/src/animations/Cascade.zig @@ -25,6 +25,7 @@ pub fn init( pub fn widget(self: *Cascade) Widget { return Widget.init( + "Cascade", self, null, null, diff --git a/src/animations/ColorMix.zig b/src/animations/ColorMix.zig index 206986e..01c9cbe 100644 --- a/src/animations/ColorMix.zig +++ b/src/animations/ColorMix.zig @@ -55,6 +55,7 @@ pub fn init( pub fn widget(self: *ColorMix) Widget { return Widget.init( + "ColorMix", self, null, null, diff --git a/src/animations/Doom.zig b/src/animations/Doom.zig index a657dbc..1bd4dba 100644 --- a/src/animations/Doom.zig +++ b/src/animations/Doom.zig @@ -62,6 +62,7 @@ pub fn init( pub fn widget(self: *Doom) Widget { return Widget.init( + "Doom", self, deinit, realloc, diff --git a/src/animations/DurFile.zig b/src/animations/DurFile.zig index 9ed9207..24a268e 100644 --- a/src/animations/DurFile.zig +++ b/src/animations/DurFile.zig @@ -417,6 +417,7 @@ pub fn init( pub fn widget(self: *DurFile) Widget { return Widget.init( + "DurFile", self, deinit, realloc, diff --git a/src/animations/GameOfLife.zig b/src/animations/GameOfLife.zig index d90590c..32390c1 100644 --- a/src/animations/GameOfLife.zig +++ b/src/animations/GameOfLife.zig @@ -72,6 +72,7 @@ pub fn init( pub fn widget(self: *GameOfLife) Widget { return Widget.init( + "GameOfLife", self, deinit, realloc, diff --git a/src/animations/Matrix.zig b/src/animations/Matrix.zig index 25ff6d1..1cb9c29 100644 --- a/src/animations/Matrix.zig +++ b/src/animations/Matrix.zig @@ -69,6 +69,7 @@ pub fn init( pub fn widget(self: *Matrix) Widget { return Widget.init( + "Matrix", self, deinit, realloc, diff --git a/src/main.zig b/src/main.zig index 71a410b..b0bdced 100644 --- a/src/main.zig +++ b/src/main.zig @@ -1133,8 +1133,8 @@ pub fn main() !void { ); try state.log_file.err( "tui", - "failed to set cursor in active widget: {s}", - .{@errorName(err)}, + "failed to set cursor in active widget '{s}': {s}", + .{ current_widget.display_name, @errorName(err) }, ); }; @@ -1213,19 +1213,6 @@ pub fn main() !void { try state.log_file.info("tui", "screen resolution updated to {d}x{d}", .{ state.buffer.width, state.buffer.height }); - if (animation) |*a| a.realloc() 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 reallocate animation buffers: {s}", - .{@errorName(err)}, - ); - }; - for (widgets.items) |*widget| { widget.realloc() catch |err| { try state.info_line.addMessage( @@ -1235,8 +1222,8 @@ pub fn main() !void { ); try state.log_file.err( "tui", - "failed to reallocate widget: {s}", - .{@errorName(err)}, + "failed to reallocate widget '{s}': {s}", + .{ widget.display_name, @errorName(err) }, ); }; } @@ -1265,8 +1252,8 @@ pub fn main() !void { ); try state.log_file.err( "tui", - "failed to handle active widget: {s}", - .{@errorName(err)}, + "failed to handle active widget '{s}': {s}", + .{ current_widget.display_name, @errorName(err) }, ); }; } diff --git a/src/tui/Widget.zig b/src/tui/Widget.zig index 80413a7..dc9b609 100644 --- a/src/tui/Widget.zig +++ b/src/tui/Widget.zig @@ -12,10 +12,12 @@ const VTable = struct { }; id: u64, +display_name: []const u8, pointer: *anyopaque, vtable: VTable, pub fn init( + display_name: []const u8, pointer: anytype, comptime deinit_fn: ?fn (ptr: @TypeOf(pointer)) void, comptime realloc_fn: ?fn (ptr: @TypeOf(pointer)) anyerror!void, @@ -86,6 +88,7 @@ pub fn init( return .{ .id = @intFromPtr(Impl.vtable.draw_fn), + .display_name = display_name, .pointer = pointer, .vtable = Impl.vtable, }; diff --git a/src/tui/components/BigLabel.zig b/src/tui/components/BigLabel.zig index 625647f..934102e 100644 --- a/src/tui/components/BigLabel.zig +++ b/src/tui/components/BigLabel.zig @@ -84,6 +84,7 @@ pub fn deinit(self: *BigLabel) void { pub fn widget(self: *BigLabel) Widget { return Widget.init( + "BigLabel", self, deinit, null, diff --git a/src/tui/components/CenteredBox.zig b/src/tui/components/CenteredBox.zig index fd46057..1e69be5 100644 --- a/src/tui/components/CenteredBox.zig +++ b/src/tui/components/CenteredBox.zig @@ -61,6 +61,7 @@ pub fn init( pub fn widget(self: *CenteredBox) Widget { return Widget.init( + "CenteredBox", self, null, null, diff --git a/src/tui/components/InfoLine.zig b/src/tui/components/InfoLine.zig index e3d9904..c8d51b7 100644 --- a/src/tui/components/InfoLine.zig +++ b/src/tui/components/InfoLine.zig @@ -47,6 +47,7 @@ pub fn deinit(self: *InfoLine) void { pub fn widget(self: *InfoLine) Widget { return Widget.init( + "InfoLine", self, deinit, null, diff --git a/src/tui/components/Label.zig b/src/tui/components/Label.zig index fb95a98..7f5015d 100644 --- a/src/tui/components/Label.zig +++ b/src/tui/components/Label.zig @@ -42,6 +42,7 @@ pub fn deinit(self: *Label) void { pub fn widget(self: *Label) Widget { return Widget.init( + "Label", self, deinit, null, diff --git a/src/tui/components/Session.zig b/src/tui/components/Session.zig index 00782b0..81460c0 100644 --- a/src/tui/components/Session.zig +++ b/src/tui/components/Session.zig @@ -57,6 +57,7 @@ pub fn deinit(self: *Session) void { pub fn widget(self: *Session) Widget { return Widget.init( + "Session", self, deinit, null, diff --git a/src/tui/components/Text.zig b/src/tui/components/Text.zig index 23dec80..7e04d09 100644 --- a/src/tui/components/Text.zig +++ b/src/tui/components/Text.zig @@ -56,6 +56,7 @@ pub fn deinit(self: *Text) void { pub fn widget(self: *Text) Widget { return Widget.init( + "Text", self, deinit, null, diff --git a/src/tui/components/UserList.zig b/src/tui/components/UserList.zig index d053623..f4d9179 100644 --- a/src/tui/components/UserList.zig +++ b/src/tui/components/UserList.zig @@ -89,6 +89,7 @@ pub fn deinit(self: *UserList) void { pub fn widget(self: *UserList) Widget { return Widget.init( + "UserList", self, deinit, null, From 57c96a3478fea90d9d0d4f4a13a6f4e2f85e1d2f Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Wed, 11 Feb 2026 22:55:10 +0100 Subject: [PATCH 060/176] Fix animation timeout bug + remove redundant check Signed-off-by: AnErrupTion --- src/main.zig | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/main.zig b/src/main.zig index b0bdced..b74200b 100644 --- a/src/main.zig +++ b/src/main.zig @@ -1156,13 +1156,12 @@ pub fn main() !void { if (state.config.animation_timeout_sec > 0 and time.seconds - animation_time_start.seconds > state.config.animation_timeout_sec) { state.animation_timed_out = true; - if (animation) |*a| a.deinit(); } } else if (state.config.bigclock != .none and state.config.clock == null) { const time = try interop.getTimeOfDay(); timeout = @intCast((60 - @rem(time.seconds, 60)) * 1000 - @divTrunc(time.microseconds, 1000) + 1); - } else if (state.config.clock != null or (state.config.auth_fails > 0 and state.auth_fails >= state.config.auth_fails)) { + } else if (state.config.clock != null) { const time = try interop.getTimeOfDay(); timeout = @intCast(1000 - @divTrunc(time.microseconds, 1000) + 1); From 7c7aed9cb2740fc6a4e04f3bc4f3bdc2adfeb8e6 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Wed, 11 Feb 2026 23:51:32 +0100 Subject: [PATCH 061/176] Make animation timeout independent of event loop Signed-off-by: AnErrupTion --- src/animations/ColorMix.zig | 29 +++++++++++++++++++++++------ src/animations/Doom.zig | 27 ++++++++++++++++++++++----- src/animations/DurFile.zig | 25 ++++++++++++++++++++----- src/animations/GameOfLife.zig | 27 ++++++++++++++++++++++----- src/animations/Matrix.zig | 27 ++++++++++++++++++++++----- src/main.zig | 31 ++++++++++++------------------- 6 files changed, 121 insertions(+), 45 deletions(-) diff --git a/src/animations/ColorMix.zig b/src/animations/ColorMix.zig index 01c9cbe..a6ebd6d 100644 --- a/src/animations/ColorMix.zig +++ b/src/animations/ColorMix.zig @@ -1,6 +1,10 @@ const std = @import("std"); const math = std.math; +const ly_core = @import("ly-core"); +const interop = ly_core.interop; +const TimeOfDay = interop.TimeOfDay; + const Cell = @import("../tui/Cell.zig"); const TerminalBuffer = @import("../tui/TerminalBuffer.zig"); const Widget = @import("../tui/Widget.zig"); @@ -16,8 +20,10 @@ fn length(vec: Vec2) f32 { return math.sqrt(vec[0] * vec[0] + vec[1] * vec[1]); } +start_time: TimeOfDay, terminal_buffer: *TerminalBuffer, -timeout: *bool, +animate: *bool, +timeout_sec: u12, frames: u64, pattern_cos_mod: f32, pattern_sin_mod: f32, @@ -28,11 +34,14 @@ pub fn init( col1: u32, col2: u32, col3: u32, - timeout: *bool, -) ColorMix { + animate: *bool, + timeout_sec: u12, +) !ColorMix { return .{ + .start_time = try interop.getTimeOfDay(), .terminal_buffer = terminal_buffer, - .timeout = timeout, + .animate = animate, + .timeout_sec = timeout_sec, .frames = 0, .pattern_cos_mod = terminal_buffer.random.float(f32) * math.pi * 2.0, .pattern_sin_mod = terminal_buffer.random.float(f32) * math.pi * 2.0, @@ -60,13 +69,13 @@ pub fn widget(self: *ColorMix) Widget { null, null, draw, - null, + update, null, ); } fn draw(self: *ColorMix) void { - if (self.timeout.*) return; + if (!self.animate.*) return; self.frames +%= 1; const time: f32 = @as(f32, @floatFromInt(self.frames)) * time_scale; @@ -99,3 +108,11 @@ fn draw(self: *ColorMix) void { } } } + +fn update(self: *ColorMix, _: *anyopaque) !void { + const time = try interop.getTimeOfDay(); + + if (self.timeout_sec > 0 and time.seconds - self.start_time.seconds > self.timeout_sec) { + self.animate.* = false; + } +} diff --git a/src/animations/Doom.zig b/src/animations/Doom.zig index 1bd4dba..bb017ef 100644 --- a/src/animations/Doom.zig +++ b/src/animations/Doom.zig @@ -1,6 +1,10 @@ const std = @import("std"); const Allocator = std.mem.Allocator; +const ly_core = @import("ly-core"); +const interop = ly_core.interop; +const TimeOfDay = interop.TimeOfDay; + const Cell = @import("../tui/Cell.zig"); const TerminalBuffer = @import("../tui/TerminalBuffer.zig"); const Widget = @import("../tui/Widget.zig"); @@ -11,9 +15,11 @@ pub const STEPS = 12; pub const HEIGHT_MAX = 9; pub const SPREAD_MAX = 4; +start_time: TimeOfDay, allocator: Allocator, terminal_buffer: *TerminalBuffer, -timeout: *bool, +animate: *bool, +timeout_sec: u12, buffer: []u8, height: u8, spread: u8, @@ -27,7 +33,8 @@ pub fn init( bottom_color: u32, fire_height: u8, fire_spread: u8, - timeout: *bool, + animate: *bool, + timeout_sec: u12, ) !Doom { const buffer = try allocator.alloc(u8, terminal_buffer.width * terminal_buffer.height); initBuffer(buffer, terminal_buffer.width); @@ -50,9 +57,11 @@ pub fn init( }; return .{ + .start_time = try interop.getTimeOfDay(), .allocator = allocator, .terminal_buffer = terminal_buffer, - .timeout = timeout, + .animate = animate, + .timeout_sec = timeout_sec, .buffer = buffer, .height = @min(HEIGHT_MAX, fire_height), .spread = @min(SPREAD_MAX, fire_spread), @@ -67,7 +76,7 @@ pub fn widget(self: *Doom) Widget { deinit, realloc, draw, - null, + update, null, ); } @@ -83,7 +92,7 @@ fn realloc(self: *Doom) !void { } fn draw(self: *Doom) void { - if (self.timeout.*) return; + if (!self.animate.*) return; for (0..self.terminal_buffer.width) |x| { // We start from 1 so that we always have the topmost line when spreading fire @@ -130,3 +139,11 @@ fn initBuffer(buffer: []u8, width: usize) void { @memset(slice_start, 0); @memset(slice_end, STEPS); } + +fn update(self: *Doom, _: *anyopaque) !void { + const time = try interop.getTimeOfDay(); + + if (self.timeout_sec > 0 and time.seconds - self.start_time.seconds > self.timeout_sec) { + self.animate.* = false; + } +} diff --git a/src/animations/DurFile.zig b/src/animations/DurFile.zig index 24a268e..4f425bd 100644 --- a/src/animations/DurFile.zig +++ b/src/animations/DurFile.zig @@ -5,6 +5,8 @@ const eql = std.mem.eql; const flate = std.compress.flate; const ly_core = @import("ly-core"); +const interop = ly_core.interop; +const TimeOfDay = interop.TimeOfDay; const LogFile = ly_core.LogFile; const enums = @import("../enums.zig"); @@ -300,6 +302,7 @@ const VEC_Y = 1; const DurFile = @This(); +start_time: TimeOfDay, allocator: Allocator, terminal_buffer: *TerminalBuffer, dur_movie: DurFormat, @@ -307,7 +310,8 @@ frames: u64, frame_size: UVec2, start_pos: IVec2, full_color: bool, -timeout: *bool, +animate: *bool, +timeout_sec: u12, frame_time: u32, time_previous: i64, is_color_format_16: bool, @@ -367,7 +371,8 @@ pub fn init( x_offset: i32, y_offset: i32, full_color: bool, - timeout: *bool, + animate: *bool, + timeout_sec: u12, ) !DurFile { var dur_movie: DurFormat = .init(allocator); @@ -399,6 +404,7 @@ pub fn init( const frame_time: u32 = @intFromFloat(1000 / dur_movie.framerate.?); return .{ + .start_time = try interop.getTimeOfDay(), .allocator = allocator, .terminal_buffer = terminal_buffer, .frames = 0, @@ -406,7 +412,8 @@ pub fn init( .frame_size = frame_size, .start_pos = start_pos, .full_color = full_color, - .timeout = timeout, + .animate = animate, + .timeout_sec = timeout_sec, .dur_movie = dur_movie, .frame_time = frame_time, .is_color_format_16 = eql(u8, dur_movie.colorFormat.?, "16"), @@ -422,7 +429,7 @@ pub fn widget(self: *DurFile) Widget { deinit, realloc, draw, - null, + update, null, ); } @@ -438,7 +445,7 @@ fn realloc(self: *DurFile) !void { } fn draw(self: *DurFile) void { - if (self.timeout.*) return; + if (!self.animate.*) return; const current_frame = self.dur_movie.frames.items[self.frames]; @@ -493,3 +500,11 @@ fn draw(self: *DurFile) void { self.frames = (self.frames + 1) % frame_count; } } + +fn update(self: *DurFile, _: *anyopaque) !void { + const time = try interop.getTimeOfDay(); + + if (self.timeout_sec > 0 and time.seconds - self.start_time.seconds > self.timeout_sec) { + self.animate.* = false; + } +} diff --git a/src/animations/GameOfLife.zig b/src/animations/GameOfLife.zig index 32390c1..7ccfd49 100644 --- a/src/animations/GameOfLife.zig +++ b/src/animations/GameOfLife.zig @@ -1,6 +1,10 @@ const std = @import("std"); const Allocator = std.mem.Allocator; +const ly_core = @import("ly-core"); +const interop = ly_core.interop; +const TimeOfDay = interop.TimeOfDay; + const Cell = @import("../tui/Cell.zig"); const TerminalBuffer = @import("../tui/TerminalBuffer.zig"); const Widget = @import("../tui/Widget.zig"); @@ -16,6 +20,7 @@ const NEIGHBOR_DIRS = [_][2]i8{ .{ 1, 0 }, .{ 1, 1 }, }; +start_time: TimeOfDay, allocator: Allocator, terminal_buffer: *TerminalBuffer, current_grid: []bool, @@ -26,7 +31,8 @@ fg_color: u32, entropy_interval: usize, frame_delay: usize, initial_density: f32, -timeout: *bool, +animate: *bool, +timeout_sec: u12, dead_cell: Cell, width: usize, height: usize, @@ -38,7 +44,8 @@ pub fn init( entropy_interval: usize, frame_delay: usize, initial_density: f32, - timeout: *bool, + animate: *bool, + timeout_sec: u12, ) !GameOfLife { const width = terminal_buffer.width; const height = terminal_buffer.height; @@ -48,6 +55,7 @@ pub fn init( const next_grid = try allocator.alloc(bool, grid_size); var game = GameOfLife{ + .start_time = try interop.getTimeOfDay(), .allocator = allocator, .terminal_buffer = terminal_buffer, .current_grid = current_grid, @@ -58,7 +66,8 @@ pub fn init( .entropy_interval = entropy_interval, .frame_delay = frame_delay, .initial_density = initial_density, - .timeout = timeout, + .animate = animate, + .timeout_sec = timeout_sec, .dead_cell = .{ .ch = DEAD_CHAR, .fg = @intCast(TerminalBuffer.Color.DEFAULT), .bg = terminal_buffer.bg }, .width = width, .height = height, @@ -77,7 +86,7 @@ pub fn widget(self: *GameOfLife) Widget { deinit, realloc, draw, - null, + update, null, ); } @@ -105,7 +114,7 @@ fn realloc(self: *GameOfLife) !void { } fn draw(self: *GameOfLife) void { - if (self.timeout.*) return; + if (!self.animate.*) return; // Update game state at controlled frame rate self.frame_counter += 1; @@ -132,6 +141,14 @@ fn draw(self: *GameOfLife) void { } } +fn update(self: *GameOfLife, _: *anyopaque) !void { + const time = try interop.getTimeOfDay(); + + if (self.timeout_sec > 0 and time.seconds - self.start_time.seconds > self.timeout_sec) { + self.animate.* = false; + } +} + fn updateGeneration(self: *GameOfLife) void { // Conway's Game of Life rules with optimized neighbor counting for (0..self.height) |y| { diff --git a/src/animations/Matrix.zig b/src/animations/Matrix.zig index 1cb9c29..4a6ef95 100644 --- a/src/animations/Matrix.zig +++ b/src/animations/Matrix.zig @@ -2,6 +2,10 @@ const std = @import("std"); const Allocator = std.mem.Allocator; const Random = std.Random; +const ly_core = @import("ly-core"); +const interop = ly_core.interop; +const TimeOfDay = interop.TimeOfDay; + const Cell = @import("../tui/Cell.zig"); const TerminalBuffer = @import("../tui/TerminalBuffer.zig"); const Widget = @import("../tui/Widget.zig"); @@ -24,6 +28,7 @@ pub const Line = struct { update: usize, }; +start_time: TimeOfDay, allocator: Allocator, terminal_buffer: *TerminalBuffer, dots: []Dot, @@ -34,7 +39,8 @@ fg: u32, head_col: u32, min_codepoint: u16, max_codepoint: u16, -timeout: *bool, +animate: *bool, +timeout_sec: u12, default_cell: Cell, pub fn init( @@ -44,7 +50,8 @@ pub fn init( head_col: u32, min_codepoint: u16, max_codepoint: u16, - timeout: *bool, + animate: *bool, + timeout_sec: u12, ) !Matrix { const dots = try allocator.alloc(Dot, terminal_buffer.width * (terminal_buffer.height + 1)); const lines = try allocator.alloc(Line, terminal_buffer.width); @@ -52,6 +59,7 @@ pub fn init( initBuffers(dots, lines, terminal_buffer.width, terminal_buffer.height, terminal_buffer.random); return .{ + .start_time = try interop.getTimeOfDay(), .allocator = allocator, .terminal_buffer = terminal_buffer, .dots = dots, @@ -62,7 +70,8 @@ pub fn init( .head_col = head_col, .min_codepoint = min_codepoint, .max_codepoint = max_codepoint - min_codepoint, - .timeout = timeout, + .animate = animate, + .timeout_sec = timeout_sec, .default_cell = .{ .ch = ' ', .fg = fg, .bg = terminal_buffer.bg }, }; } @@ -74,7 +83,7 @@ pub fn widget(self: *Matrix) Widget { deinit, realloc, draw, - null, + update, null, ); } @@ -95,7 +104,7 @@ fn realloc(self: *Matrix) !void { } fn draw(self: *Matrix) void { - if (self.timeout.*) return; + if (!self.animate.*) return; const buf_height = self.terminal_buffer.height; const buf_width = self.terminal_buffer.width; @@ -188,6 +197,14 @@ fn draw(self: *Matrix) void { } } +fn update(self: *Matrix, _: *anyopaque) !void { + const time = try interop.getTimeOfDay(); + + if (self.timeout_sec > 0 and time.seconds - self.start_time.seconds > self.timeout_sec) { + self.animate.* = false; + } +} + fn initBuffers(dots: []Dot, lines: []Line, width: usize, height: usize, random: Random) void { var y: usize = 0; while (y <= height) : (y += 1) { diff --git a/src/main.zig b/src/main.zig index b74200b..dcd44ab 100644 --- a/src/main.zig +++ b/src/main.zig @@ -76,7 +76,6 @@ const UiState = struct { active_tty: u8, buffer: TerminalBuffer, labels_max_length: usize, - animation_timed_out: bool, shutdown_label: Label, restart_label: Label, sleep_label: Label, @@ -152,9 +151,6 @@ pub fn main() !void { state.allocator = gpa.allocator(); - // Allows stopping an animation after some time - const animation_time_start = try interop.getTimeOfDay(); - // Load arguments const params = comptime clap.parseParamsComptime( \\-h, --help Shows all commands. @@ -884,7 +880,8 @@ pub fn main() !void { state.config.doom_bottom_color, state.config.doom_fire_height, state.config.doom_fire_spread, - &state.animation_timed_out, + &state.animate, + state.config.animation_timeout_sec, ); animation = doom.widget(); }, @@ -896,17 +893,19 @@ pub fn main() !void { state.config.cmatrix_head_col, state.config.cmatrix_min_codepoint, state.config.cmatrix_max_codepoint, - &state.animation_timed_out, + &state.animate, + state.config.animation_timeout_sec, ); animation = matrix.widget(); }, .colormix => { - var color_mix = ColorMix.init( + var color_mix = try ColorMix.init( &state.buffer, state.config.colormix_col1, state.config.colormix_col2, state.config.colormix_col3, - &state.animation_timed_out, + &state.animate, + state.config.animation_timeout_sec, ); animation = color_mix.widget(); }, @@ -918,7 +917,8 @@ pub fn main() !void { state.config.gameoflife_entropy_interval, state.config.gameoflife_frame_delay, state.config.gameoflife_initial_density, - &state.animation_timed_out, + &state.animate, + state.config.animation_timeout_sec, ); animation = game_of_life.widget(); }, @@ -932,7 +932,8 @@ pub fn main() !void { state.config.dur_x_offset, state.config.dur_y_offset, state.config.full_color, - &state.animation_timed_out, + &state.animate, + state.config.animation_timeout_sec, ); animation = dur.widget(); }, @@ -949,7 +950,6 @@ pub fn main() !void { state.auth_fails = 0; state.run = true; state.update = true; - state.animation_timed_out = false; state.animate = state.config.animation != .none; state.insert_mode = !state.config.vi_mode or state.config.vi_default_mode == .insert; state.edge_margin = Position.init( @@ -1148,15 +1148,8 @@ pub fn main() !void { var timeout: i32 = -1; // Calculate the maximum timeout based on current animations, or the (big) clock. If there's none, we wait for the event indefinitely instead - if (state.animate and !state.animation_timed_out) { + if (state.animate) { timeout = state.config.animation_frame_delay; - - // Check how long we've been running so we can turn off the animation - const time = try interop.getTimeOfDay(); - - if (state.config.animation_timeout_sec > 0 and time.seconds - animation_time_start.seconds > state.config.animation_timeout_sec) { - state.animation_timed_out = true; - } } else if (state.config.bigclock != .none and state.config.clock == null) { const time = try interop.getTimeOfDay(); From 5564fed6646b572f56892578d95fdadc50e62bd4 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Thu, 12 Feb 2026 00:27:07 +0100 Subject: [PATCH 062/176] Add Widget.calculateTimeout function Signed-off-by: AnErrupTion --- src/animations/Cascade.zig | 1 + src/animations/ColorMix.zig | 8 ++++ src/animations/Doom.zig | 8 ++++ src/animations/DurFile.zig | 8 ++++ src/animations/GameOfLife.zig | 8 ++++ src/animations/Matrix.zig | 8 ++++ src/main.zig | 59 ++++++++++++++++++++++-------- src/tui/Widget.zig | 27 ++++++++++++++ src/tui/components/BigLabel.zig | 16 ++++++++ src/tui/components/CenteredBox.zig | 1 + src/tui/components/InfoLine.zig | 1 + src/tui/components/Label.zig | 16 ++++++++ src/tui/components/Session.zig | 1 + src/tui/components/Text.zig | 1 + src/tui/components/UserList.zig | 1 + 15 files changed, 149 insertions(+), 15 deletions(-) diff --git a/src/animations/Cascade.zig b/src/animations/Cascade.zig index 2ec2452..32f2f6a 100644 --- a/src/animations/Cascade.zig +++ b/src/animations/Cascade.zig @@ -32,6 +32,7 @@ pub fn widget(self: *Cascade) Widget { draw, null, null, + null, ); } diff --git a/src/animations/ColorMix.zig b/src/animations/ColorMix.zig index a6ebd6d..278f646 100644 --- a/src/animations/ColorMix.zig +++ b/src/animations/ColorMix.zig @@ -24,6 +24,7 @@ start_time: TimeOfDay, terminal_buffer: *TerminalBuffer, animate: *bool, timeout_sec: u12, +frame_delay: u16, frames: u64, pattern_cos_mod: f32, pattern_sin_mod: f32, @@ -36,12 +37,14 @@ pub fn init( col3: u32, animate: *bool, timeout_sec: u12, + frame_delay: u16, ) !ColorMix { return .{ .start_time = try interop.getTimeOfDay(), .terminal_buffer = terminal_buffer, .animate = animate, .timeout_sec = timeout_sec, + .frame_delay = frame_delay, .frames = 0, .pattern_cos_mod = terminal_buffer.random.float(f32) * math.pi * 2.0, .pattern_sin_mod = terminal_buffer.random.float(f32) * math.pi * 2.0, @@ -71,6 +74,7 @@ pub fn widget(self: *ColorMix) Widget { draw, update, null, + calculateTimeout, ); } @@ -116,3 +120,7 @@ fn update(self: *ColorMix, _: *anyopaque) !void { self.animate.* = false; } } + +fn calculateTimeout(self: *ColorMix, _: *anyopaque) !?usize { + return self.frame_delay; +} diff --git a/src/animations/Doom.zig b/src/animations/Doom.zig index bb017ef..e580bd9 100644 --- a/src/animations/Doom.zig +++ b/src/animations/Doom.zig @@ -20,6 +20,7 @@ allocator: Allocator, terminal_buffer: *TerminalBuffer, animate: *bool, timeout_sec: u12, +frame_delay: u16, buffer: []u8, height: u8, spread: u8, @@ -35,6 +36,7 @@ pub fn init( fire_spread: u8, animate: *bool, timeout_sec: u12, + frame_delay: u16, ) !Doom { const buffer = try allocator.alloc(u8, terminal_buffer.width * terminal_buffer.height); initBuffer(buffer, terminal_buffer.width); @@ -62,6 +64,7 @@ pub fn init( .terminal_buffer = terminal_buffer, .animate = animate, .timeout_sec = timeout_sec, + .frame_delay = frame_delay, .buffer = buffer, .height = @min(HEIGHT_MAX, fire_height), .spread = @min(SPREAD_MAX, fire_spread), @@ -78,6 +81,7 @@ pub fn widget(self: *Doom) Widget { draw, update, null, + calculateTimeout, ); } @@ -147,3 +151,7 @@ fn update(self: *Doom, _: *anyopaque) !void { self.animate.* = false; } } + +fn calculateTimeout(self: *Doom, _: *anyopaque) !?usize { + return self.frame_delay; +} diff --git a/src/animations/DurFile.zig b/src/animations/DurFile.zig index 4f425bd..9b24349 100644 --- a/src/animations/DurFile.zig +++ b/src/animations/DurFile.zig @@ -312,6 +312,7 @@ start_pos: IVec2, full_color: bool, animate: *bool, timeout_sec: u12, +frame_delay: u16, frame_time: u32, time_previous: i64, is_color_format_16: bool, @@ -373,6 +374,7 @@ pub fn init( full_color: bool, animate: *bool, timeout_sec: u12, + frame_delay: u16, ) !DurFile { var dur_movie: DurFormat = .init(allocator); @@ -414,6 +416,7 @@ pub fn init( .full_color = full_color, .animate = animate, .timeout_sec = timeout_sec, + .frame_delay = frame_delay, .dur_movie = dur_movie, .frame_time = frame_time, .is_color_format_16 = eql(u8, dur_movie.colorFormat.?, "16"), @@ -431,6 +434,7 @@ pub fn widget(self: *DurFile) Widget { draw, update, null, + calculateTimeout, ); } @@ -508,3 +512,7 @@ fn update(self: *DurFile, _: *anyopaque) !void { self.animate.* = false; } } + +fn calculateTimeout(self: *DurFile, _: *anyopaque) !?usize { + return self.frame_delay; +} diff --git a/src/animations/GameOfLife.zig b/src/animations/GameOfLife.zig index 7ccfd49..929a7c0 100644 --- a/src/animations/GameOfLife.zig +++ b/src/animations/GameOfLife.zig @@ -33,6 +33,7 @@ frame_delay: usize, initial_density: f32, animate: *bool, timeout_sec: u12, +animation_frame_delay: u16, dead_cell: Cell, width: usize, height: usize, @@ -46,6 +47,7 @@ pub fn init( initial_density: f32, animate: *bool, timeout_sec: u12, + animation_frame_delay: u16, ) !GameOfLife { const width = terminal_buffer.width; const height = terminal_buffer.height; @@ -68,6 +70,7 @@ pub fn init( .initial_density = initial_density, .animate = animate, .timeout_sec = timeout_sec, + .animation_frame_delay = animation_frame_delay, .dead_cell = .{ .ch = DEAD_CHAR, .fg = @intCast(TerminalBuffer.Color.DEFAULT), .bg = terminal_buffer.bg }, .width = width, .height = height, @@ -88,6 +91,7 @@ pub fn widget(self: *GameOfLife) Widget { draw, update, null, + calculateTimeout, ); } @@ -149,6 +153,10 @@ fn update(self: *GameOfLife, _: *anyopaque) !void { } } +fn calculateTimeout(self: *GameOfLife, _: *anyopaque) !?usize { + return self.animation_frame_delay; +} + fn updateGeneration(self: *GameOfLife) void { // Conway's Game of Life rules with optimized neighbor counting for (0..self.height) |y| { diff --git a/src/animations/Matrix.zig b/src/animations/Matrix.zig index 4a6ef95..2ec2aad 100644 --- a/src/animations/Matrix.zig +++ b/src/animations/Matrix.zig @@ -41,6 +41,7 @@ min_codepoint: u16, max_codepoint: u16, animate: *bool, timeout_sec: u12, +frame_delay: u16, default_cell: Cell, pub fn init( @@ -52,6 +53,7 @@ pub fn init( max_codepoint: u16, animate: *bool, timeout_sec: u12, + frame_delay: u16, ) !Matrix { const dots = try allocator.alloc(Dot, terminal_buffer.width * (terminal_buffer.height + 1)); const lines = try allocator.alloc(Line, terminal_buffer.width); @@ -72,6 +74,7 @@ pub fn init( .max_codepoint = max_codepoint - min_codepoint, .animate = animate, .timeout_sec = timeout_sec, + .frame_delay = frame_delay, .default_cell = .{ .ch = ' ', .fg = fg, .bg = terminal_buffer.bg }, }; } @@ -85,6 +88,7 @@ pub fn widget(self: *Matrix) Widget { draw, update, null, + calculateTimeout, ); } @@ -205,6 +209,10 @@ fn update(self: *Matrix, _: *anyopaque) !void { } } +fn calculateTimeout(self: *Matrix, _: *anyopaque) !?usize { + return self.frame_delay; +} + fn initBuffers(dots: []Dot, lines: []Line, width: usize, height: usize, random: Random) void { var y: usize = 0; while (y <= height) : (y += 1) { diff --git a/src/main.zig b/src/main.zig index dcd44ab..30a631f 100644 --- a/src/main.zig +++ b/src/main.zig @@ -363,6 +363,7 @@ pub fn main() !void { state.buffer.fg, state.buffer.bg, null, + null, ); defer state.shutdown_label.deinit(); @@ -372,6 +373,7 @@ pub fn main() !void { state.buffer.fg, state.buffer.bg, null, + null, ); defer state.restart_label.deinit(); @@ -381,6 +383,7 @@ pub fn main() !void { state.buffer.fg, state.buffer.bg, null, + null, ); defer state.sleep_label.deinit(); @@ -390,6 +393,7 @@ pub fn main() !void { state.buffer.fg, state.buffer.bg, null, + null, ); defer state.hibernate_label.deinit(); @@ -399,6 +403,7 @@ pub fn main() !void { state.buffer.fg, state.buffer.bg, null, + null, ); defer state.brightness_down_label.deinit(); @@ -408,6 +413,7 @@ pub fn main() !void { state.buffer.fg, state.buffer.bg, null, + null, ); defer state.brightness_up_label.deinit(); @@ -458,6 +464,7 @@ pub fn main() !void { state.buffer.fg, state.buffer.bg, &updateNumlock, + null, ); defer state.numlock_label.deinit(); @@ -467,6 +474,7 @@ pub fn main() !void { state.buffer.fg, state.buffer.bg, &updateCapslock, + null, ); defer state.capslock_label.deinit(); @@ -476,6 +484,7 @@ pub fn main() !void { state.buffer.fg, state.buffer.bg, &updateBattery, + null, ); defer state.battery_label.deinit(); @@ -485,6 +494,7 @@ pub fn main() !void { state.buffer.fg, state.buffer.bg, &updateClock, + &calculateClockTimeout, ); defer state.clock_label.deinit(); @@ -499,6 +509,7 @@ pub fn main() !void { .fa => .fa, }, &updateBigClock, + &calculateBigClockTimeout, ); defer state.bigclock_label.deinit(); @@ -624,6 +635,7 @@ pub fn main() !void { state.buffer.fg, state.buffer.bg, &updateSessionSpecifier, + null, ); defer state.session_specifier_label.deinit(); @@ -644,6 +656,7 @@ pub fn main() !void { state.buffer.fg, state.buffer.bg, null, + null, ); defer state.login_label.deinit(); @@ -764,6 +777,7 @@ pub fn main() !void { state.buffer.fg, state.buffer.bg, null, + null, ); defer state.password_label.deinit(); @@ -786,6 +800,7 @@ pub fn main() !void { state.buffer.fg, state.buffer.bg, null, + null, ); defer state.version_label.deinit(); @@ -882,6 +897,7 @@ pub fn main() !void { state.config.doom_fire_spread, &state.animate, state.config.animation_timeout_sec, + state.config.animation_frame_delay, ); animation = doom.widget(); }, @@ -895,6 +911,7 @@ pub fn main() !void { state.config.cmatrix_max_codepoint, &state.animate, state.config.animation_timeout_sec, + state.config.animation_frame_delay, ); animation = matrix.widget(); }, @@ -906,6 +923,7 @@ pub fn main() !void { state.config.colormix_col3, &state.animate, state.config.animation_timeout_sec, + state.config.animation_frame_delay, ); animation = color_mix.widget(); }, @@ -919,6 +937,7 @@ pub fn main() !void { state.config.gameoflife_initial_density, &state.animate, state.config.animation_timeout_sec, + state.config.animation_frame_delay, ); animation = game_of_life.widget(); }, @@ -934,6 +953,7 @@ pub fn main() !void { state.config.full_color, &state.animate, state.config.animation_timeout_sec, + state.config.animation_frame_delay, ); animation = dur.widget(); }, @@ -1145,19 +1165,11 @@ pub fn main() !void { TerminalBuffer.presentBuffer(); } - var timeout: i32 = -1; - - // Calculate the maximum timeout based on current animations, or the (big) clock. If there's none, we wait for the event indefinitely instead - if (state.animate) { - timeout = state.config.animation_frame_delay; - } else if (state.config.bigclock != .none and state.config.clock == null) { - const time = try interop.getTimeOfDay(); - - timeout = @intCast((60 - @rem(time.seconds, 60)) * 1000 - @divTrunc(time.microseconds, 1000) + 1); - } else if (state.config.clock != null) { - const time = try interop.getTimeOfDay(); - - timeout = @intCast(1000 - @divTrunc(time.microseconds, 1000) + 1); + var maybe_timeout: ?usize = null; + for (widgets.items) |*widget| { + if (try widget.calculateTimeout(&state)) |widget_timeout| { + if (maybe_timeout == null or widget_timeout < maybe_timeout.?) maybe_timeout = widget_timeout; + } } if (state.config.inactivity_cmd) |inactivity_cmd| { @@ -1190,9 +1202,9 @@ pub fn main() !void { } } - const event_error = if (timeout == -1) termbox.tb_poll_event(&event) else termbox.tb_peek_event(&event, timeout); + const event_error = if (maybe_timeout) |timeout| termbox.tb_peek_event(&event, @intCast(timeout)) else termbox.tb_poll_event(&event); - state.update = timeout != -1; + state.update = maybe_timeout != null; if (event_error < 0) continue; @@ -1741,6 +1753,12 @@ fn updateClock(self: *Label, ptr: *anyopaque) !void { } } +fn calculateClockTimeout(_: *Label, _: *anyopaque) !?usize { + const time = try interop.getTimeOfDay(); + + return @intCast(1000 - @divTrunc(time.microseconds, 1000) + 1); +} + fn updateBigClock(self: *BigLabel, ptr: *anyopaque) !void { var state: *UiState = @ptrCast(@alignCast(ptr)); @@ -1766,6 +1784,17 @@ fn updateBigClock(self: *BigLabel, ptr: *anyopaque) !void { self.setText(clock_str); } +fn calculateBigClockTimeout(_: *BigLabel, ptr: *anyopaque) !?usize { + const state: *UiState = @ptrCast(@alignCast(ptr)); + const time = try interop.getTimeOfDay(); + + if (state.config.bigclock_seconds) { + return @intCast(1000 - @divTrunc(time.microseconds, 1000) + 1); + } + + return @intCast((60 - @rem(time.seconds, 60)) * 1000 - @divTrunc(time.microseconds, 1000) + 1); +} + fn updateBox(self: *CenteredBox, ptr: *anyopaque) !void { const state: *UiState = @ptrCast(@alignCast(ptr)); diff --git a/src/tui/Widget.zig b/src/tui/Widget.zig index dc9b609..98891fd 100644 --- a/src/tui/Widget.zig +++ b/src/tui/Widget.zig @@ -9,6 +9,7 @@ const VTable = struct { 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, + calculate_timeout_fn: ?*const fn (ptr: *anyopaque, ctx: *anyopaque) anyerror!?usize, }; id: u64, @@ -24,6 +25,7 @@ pub fn init( 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, + comptime calculate_timeout_fn: ?fn (ptr: @TypeOf(pointer), ctx: *anyopaque) anyerror!?usize, ) Widget { const Pointer = @TypeOf(pointer); const Impl = struct { @@ -77,12 +79,23 @@ pub fn init( ); } + pub fn calculateTimeoutImpl(ptr: *anyopaque, ctx: *anyopaque) !?usize { + const impl: Pointer = @ptrCast(@alignCast(ptr)); + + return @call( + .always_inline, + calculate_timeout_fn.?, + .{ impl, ctx }, + ); + } + const vtable = VTable{ .deinit_fn = if (deinit_fn != null) deinitImpl else null, .realloc_fn = if (realloc_fn != null) reallocImpl else null, .draw_fn = drawImpl, .update_fn = if (update_fn != null) updateImpl else null, .handle_fn = if (handle_fn != null) handleImpl else null, + .calculate_timeout_fn = if (calculate_timeout_fn != null) calculateTimeoutImpl else null, }; }; @@ -151,3 +164,17 @@ pub fn handle(self: *Widget, maybe_key: ?keyboard.Key, insert_mode: bool) !void ); } } + +pub fn calculateTimeout(self: *Widget, ctx: *anyopaque) !?usize { + const impl: @TypeOf(self.pointer) = @ptrCast(@alignCast(self.pointer)); + + if (self.vtable.calculate_timeout_fn) |calculate_timeout_fn| { + return @call( + .auto, + calculate_timeout_fn, + .{ impl, ctx }, + ); + } + + return null; +} diff --git a/src/tui/components/BigLabel.zig b/src/tui/components/BigLabel.zig index 934102e..5159f66 100644 --- a/src/tui/components/BigLabel.zig +++ b/src/tui/components/BigLabel.zig @@ -52,6 +52,7 @@ fg: u32, bg: u32, locale: BigLabelLocale, update_fn: ?*const fn (*BigLabel, *anyopaque) anyerror!void, +calculate_timeout_fn: ?*const fn (*BigLabel, *anyopaque) anyerror!?usize, component_pos: Position, children_pos: Position, @@ -63,6 +64,7 @@ pub fn init( bg: u32, locale: BigLabelLocale, update_fn: ?*const fn (*BigLabel, *anyopaque) anyerror!void, + calculate_timeout_fn: ?*const fn (*BigLabel, *anyopaque) anyerror!?usize, ) BigLabel { return .{ .allocator = null, @@ -73,6 +75,7 @@ pub fn init( .bg = bg, .locale = locale, .update_fn = update_fn, + .calculate_timeout_fn = calculate_timeout_fn, .component_pos = TerminalBuffer.START_POSITION, .children_pos = TerminalBuffer.START_POSITION, }; @@ -91,6 +94,7 @@ pub fn widget(self: *BigLabel) Widget { draw, update, null, + calculateTimeout, ); } @@ -170,6 +174,18 @@ fn update(self: *BigLabel, context: *anyopaque) !void { } } +fn calculateTimeout(self: *BigLabel, ctx: *anyopaque) !?usize { + if (self.calculate_timeout_fn) |calculate_timeout_fn| { + return @call( + .auto, + calculate_timeout_fn, + .{ self, ctx }, + ); + } + + return null; +} + fn clockCell(char: u8, fg: u32, bg: u32, locale: BigLabelLocale) [CHAR_SIZE]Cell { var cells: [CHAR_SIZE]Cell = undefined; diff --git a/src/tui/components/CenteredBox.zig b/src/tui/components/CenteredBox.zig index 1e69be5..f0e7cd5 100644 --- a/src/tui/components/CenteredBox.zig +++ b/src/tui/components/CenteredBox.zig @@ -68,6 +68,7 @@ pub fn widget(self: *CenteredBox) Widget { draw, null, null, + null, ); } diff --git a/src/tui/components/InfoLine.zig b/src/tui/components/InfoLine.zig index c8d51b7..95578f5 100644 --- a/src/tui/components/InfoLine.zig +++ b/src/tui/components/InfoLine.zig @@ -54,6 +54,7 @@ pub fn widget(self: *InfoLine) Widget { draw, null, handle, + null, ); } diff --git a/src/tui/components/Label.zig b/src/tui/components/Label.zig index 7f5015d..f80793a 100644 --- a/src/tui/components/Label.zig +++ b/src/tui/components/Label.zig @@ -14,6 +14,7 @@ max_width: ?usize, fg: u32, bg: u32, update_fn: ?*const fn (*Label, *anyopaque) anyerror!void, +calculate_timeout_fn: ?*const fn (*Label, *anyopaque) anyerror!?usize, component_pos: Position, children_pos: Position, @@ -23,6 +24,7 @@ pub fn init( fg: u32, bg: u32, update_fn: ?*const fn (*Label, *anyopaque) anyerror!void, + calculate_timeout_fn: ?*const fn (*Label, *anyopaque) anyerror!?usize, ) Label { return .{ .allocator = null, @@ -31,6 +33,7 @@ pub fn init( .fg = fg, .bg = bg, .update_fn = update_fn, + .calculate_timeout_fn = calculate_timeout_fn, .component_pos = TerminalBuffer.START_POSITION, .children_pos = TerminalBuffer.START_POSITION, }; @@ -49,6 +52,7 @@ pub fn widget(self: *Label) Widget { draw, update, null, + calculateTimeout, ); } @@ -130,3 +134,15 @@ fn update(self: *Label, ctx: *anyopaque) !void { ); } } + +fn calculateTimeout(self: *Label, ctx: *anyopaque) !?usize { + if (self.calculate_timeout_fn) |calculate_timeout_fn| { + return @call( + .auto, + calculate_timeout_fn, + .{ self, ctx }, + ); + } + + return null; +} diff --git a/src/tui/components/Session.zig b/src/tui/components/Session.zig index 81460c0..c221ef6 100644 --- a/src/tui/components/Session.zig +++ b/src/tui/components/Session.zig @@ -64,6 +64,7 @@ pub fn widget(self: *Session) Widget { draw, null, handle, + null, ); } diff --git a/src/tui/components/Text.zig b/src/tui/components/Text.zig index 7e04d09..3890e79 100644 --- a/src/tui/components/Text.zig +++ b/src/tui/components/Text.zig @@ -63,6 +63,7 @@ pub fn widget(self: *Text) Widget { draw, null, handle, + null, ); } diff --git a/src/tui/components/UserList.zig b/src/tui/components/UserList.zig index f4d9179..dadf8dc 100644 --- a/src/tui/components/UserList.zig +++ b/src/tui/components/UserList.zig @@ -96,6 +96,7 @@ pub fn widget(self: *UserList) Widget { draw, null, handle, + null, ); } From 32d5330efb26f599f5fd3cf5668be685d514e39d Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Thu, 12 Feb 2026 01:19:19 +0100 Subject: [PATCH 063/176] Move the event loop to a separate function Signed-off-by: AnErrupTion --- src/main.zig | 312 ++++++++--------------------------- src/tui/TerminalBuffer.zig | 325 ++++++++++++++++++++++++++++++++----- 2 files changed, 353 insertions(+), 284 deletions(-) diff --git a/src/main.zig b/src/main.zig index 30a631f..a79ce00 100644 --- a/src/main.zig +++ b/src/main.zig @@ -66,11 +66,7 @@ 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, is_autologin: bool, use_kmscon_vt: bool, active_tty: u8, @@ -966,10 +962,7 @@ pub fn main() !void { state.config.auth_fails, ); - state.active_widget_index = 0; state.auth_fails = 0; - state.run = true; - state.update = true; state.animate = state.config.animation != .none; state.insert_mode = !state.config.vi_mode or state.config.vi_default_mode == .insert; state.edge_margin = Position.init( @@ -1056,36 +1049,24 @@ pub fn main() !void { try widgets.append(state.allocator, cascade.widget()); } - try state.buffer.registerKeybind("Esc", &disableInsertMode); - try state.buffer.registerKeybind("I", &enableInsertMode); + try state.buffer.registerKeybind("Esc", &disableInsertMode, &state); + try state.buffer.registerKeybind("I", &enableInsertMode, &state); - try state.buffer.registerKeybind("Ctrl+C", &quit); + try state.buffer.registerKeybind("Ctrl+C", &quit, &state); - try state.buffer.registerKeybind("Ctrl+U", &clearPassword); + try state.buffer.registerKeybind("Ctrl+U", &clearPassword, &state); - try state.buffer.registerKeybind("Ctrl+K", &moveCursorUp); - try state.buffer.registerKeybind("Up", &moveCursorUp); - try state.buffer.registerKeybind("K", &viMoveCursorUp); + try state.buffer.registerKeybind("K", &viMoveCursorUp, &state); + try state.buffer.registerKeybind("J", &viMoveCursorDown, &state); - try state.buffer.registerKeybind("Ctrl+J", &moveCursorDown); - try state.buffer.registerKeybind("Down", &moveCursorDown); - try state.buffer.registerKeybind("J", &viMoseCursorDown); + try state.buffer.registerKeybind("Enter", &authenticate, &state); - try state.buffer.registerKeybind("Tab", &wrapCursor); - try state.buffer.registerKeybind("Shift+Tab", &wrapCursorReverse); - - try state.buffer.registerKeybind("Enter", &authenticate); - - try state.buffer.registerKeybind(state.config.shutdown_key, &shutdownCmd); - try state.buffer.registerKeybind(state.config.restart_key, &restartCmd); - if (state.config.sleep_cmd != null) try state.buffer.registerKeybind(state.config.sleep_key, &sleepCmd); - if (state.config.hibernate_cmd != null) try state.buffer.registerKeybind(state.config.hibernate_key, &hibernateCmd); - if (state.config.brightness_down_key) |key| try state.buffer.registerKeybind(key, &decreaseBrightnessCmd); - if (state.config.brightness_up_key) |key| try state.buffer.registerKeybind(key, &increaseBrightnessCmd); - - var event: termbox.tb_event = undefined; - var inactivity_time_start = try interop.getTimeOfDay(); - var inactivity_cmd_ran = false; + try state.buffer.registerKeybind(state.config.shutdown_key, &shutdownCmd, &state); + try state.buffer.registerKeybind(state.config.restart_key, &restartCmd, &state); + if (state.config.sleep_cmd != null) try state.buffer.registerKeybind(state.config.sleep_key, &sleepCmd, &state); + if (state.config.hibernate_cmd != null) try state.buffer.registerKeybind(state.config.hibernate_key, &hibernateCmd, &state); + if (state.config.brightness_down_key) |key| try state.buffer.registerKeybind(key, &decreaseBrightnessCmd, &state); + if (state.config.brightness_up_key) |key| try state.buffer.registerKeybind(key, &increaseBrightnessCmd, &state); if (state.config.initial_info_text) |text| { try state.info_line.addMessage(text, state.config.bg, state.config.fg); @@ -1122,159 +1103,20 @@ pub fn main() !void { .password => state.password_widget, }; - // Run the event loop - state.handlable_widgets = .empty; - defer state.handlable_widgets.deinit(state.allocator); + var shared_error = try SharedError.init(); + defer shared_error.deinit(); - 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); - - // 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}': {s}", - .{ current_widget.display_name, @errorName(err) }, - ); - }; - - try TerminalBuffer.clearScreen(false); - - for (widgets.items) |*widget| widget.draw(); - - TerminalBuffer.presentBuffer(); - } - - var maybe_timeout: ?usize = null; - for (widgets.items) |*widget| { - if (try widget.calculateTimeout(&state)) |widget_timeout| { - if (maybe_timeout == null or widget_timeout < maybe_timeout.?) maybe_timeout = widget_timeout; - } - } - - if (state.config.inactivity_cmd) |inactivity_cmd| { - const time = try interop.getTimeOfDay(); - - if (!inactivity_cmd_ran and time.seconds - inactivity_time_start.seconds > state.config.inactivity_delay) { - var inactivity = std.process.Child.init(&[_][]const u8{ "/bin/sh", "-c", inactivity_cmd }, state.allocator); - inactivity.stdout_behavior = .Ignore; - inactivity.stderr_behavior = .Ignore; - - handle_inactivity_cmd: { - const process_result = inactivity.spawnAndWait() catch { - break :handle_inactivity_cmd; - }; - if (process_result.Exited != 0) { - try state.info_line.addMessage( - state.lang.err_inactivity, - state.config.error_bg, - state.config.error_fg, - ); - try state.log_file.err( - "sys", - "failed to execute inactivity command: exit code {d}", - .{process_result.Exited}, - ); - } - } - - inactivity_cmd_ran = true; - } - } - - const event_error = if (maybe_timeout) |timeout| termbox.tb_peek_event(&event, @intCast(timeout)) else termbox.tb_poll_event(&event); - - state.update = maybe_timeout != null; - - 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.getWidth(); - state.buffer.height = TerminalBuffer.getHeight(); - - try state.log_file.info("tui", "screen resolution updated to {d}x{d}", .{ state.buffer.width, state.buffer.height }); - - for (widgets.items) |*widget| { - widget.realloc() 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 reallocate widget '{s}': {s}", - .{ widget.display_name, @errorName(err) }, - ); - }; - } - - positionComponents(&state); - - state.update = true; - continue; - } - - var maybe_keys = try state.buffer.handleKeybind( - state.allocator, - event, - &state, - ); - if (maybe_keys) |*keys| { - defer keys.deinit(state.allocator); - - const current_widget = getActiveWidget(&state); - for (keys.items) |key| { - 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}': {s}", - .{ current_widget.display_name, @errorName(err) }, - ); - }; - } - - state.update = true; - } - } -} - -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; - } + try state.buffer.runEventLoop( + state.allocator, + shared_error, + widgets.items, + active_widget, + state.config.inactivity_delay, + &state.insert_mode, // FIXME: Hack + positionWidgets, + handleInactivity, + &state, + ); } fn disableInsertMode(ptr: *anyopaque) !bool { @@ -1282,7 +1124,7 @@ fn disableInsertMode(ptr: *anyopaque) !bool { if (state.config.vi_mode and state.insert_mode) { state.insert_mode = false; - state.update = true; + state.buffer.drawNextFrame(true); } return false; } @@ -1292,78 +1134,38 @@ fn enableInsertMode(ptr: *anyopaque) !bool { 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 (getActiveWidget(state) == &state.password_widget) { - state.password.clear(); - state.update = true; - } - return false; -} - -fn moveCursorUp(ptr: *anyopaque) !bool { - var state: *UiState = @ptrCast(@alignCast(ptr)); - if (state.active_widget_index == 0) return false; - - state.active_widget_index -= 1; - state.update = true; + state.buffer.drawNextFrame(true); return false; } 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_widget_index -= 1; - state.update = true; - return false; + return try state.buffer.simulateKeybind("Up"); } -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_widget_index += 1; - state.update = true; - return false; -} - -fn viMoseCursorDown(ptr: *anyopaque) !bool { +fn viMoveCursorDown(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_widget_index += 1; - state.update = true; - return false; + return try state.buffer.simulateKeybind("Down"); } -fn wrapCursor(ptr: *anyopaque) !bool { +fn clearPassword(ptr: *anyopaque) !bool { var state: *UiState = @ptrCast(@alignCast(ptr)); - state.active_widget_index = (state.active_widget_index + 1) % state.handlable_widgets.items.len; - state.update = true; - return false; -} - -fn wrapCursorReverse(ptr: *anyopaque) !bool { - var state: *UiState = @ptrCast(@alignCast(ptr)); - - state.active_widget_index = (state.active_widget_index - 1) % state.handlable_widgets.items.len; - state.update = true; + if (state.buffer.getActiveWidget().id == state.password_widget.id) { + state.password.clear(); + state.buffer.drawNextFrame(true); + } return false; } fn quit(ptr: *anyopaque) !bool { var state: *UiState = @ptrCast(@alignCast(ptr)); - state.run = false; + state.buffer.stopEventLoop(); return false; } @@ -1524,7 +1326,7 @@ fn authenticate(ptr: *anyopaque) !bool { const auth_err = shared_err.readError(); if (auth_err) |err| { state.auth_fails += 1; - setActiveWidget(state, &state.password_widget); + state.buffer.setActiveWidget(state.password_widget); try state.info_line.addMessage( getAuthErrorMsg(err, state.lang), @@ -1556,7 +1358,7 @@ fn authenticate(ptr: *anyopaque) !bool { if (state.config.auth_fails == 0 or state.auth_fails < state.config.auth_fails) { try TerminalBuffer.clearScreen(true); - state.update = true; + state.buffer.drawNextFrame(true); } // Restore the cursor @@ -1569,7 +1371,7 @@ fn shutdownCmd(ptr: *anyopaque) !bool { var state: *UiState = @ptrCast(@alignCast(ptr)); shutdown = true; - state.run = false; + state.buffer.stopEventLoop(); return false; } @@ -1577,7 +1379,7 @@ fn restartCmd(ptr: *anyopaque) !bool { var state: *UiState = @ptrCast(@alignCast(ptr)); restart = true; - state.run = false; + state.buffer.stopEventLoop(); return false; } @@ -1810,7 +1612,9 @@ fn updateSessionSpecifier(self: *Label, ptr: *anyopaque) !void { self.setText(env.environment.specifier); } -fn positionComponents(state: *UiState) void { +fn positionWidgets(ptr: *anyopaque) !void { + var state: *UiState = @ptrCast(@alignCast(ptr)); + if (!state.config.hide_key_hints) { state.shutdown_label.positionX(state.edge_margin .add(TerminalBuffer.START_POSITION)); @@ -1895,6 +1699,34 @@ fn positionComponents(state: *UiState) void { .invertY(state.buffer.height - 1)); } +fn handleInactivity(ptr: *anyopaque) !void { + var state: *UiState = @ptrCast(@alignCast(ptr)); + + if (state.config.inactivity_cmd) |inactivity_cmd| { + var inactivity = std.process.Child.init(&[_][]const u8{ "/bin/sh", "-c", inactivity_cmd }, state.allocator); + inactivity.stdout_behavior = .Ignore; + inactivity.stderr_behavior = .Ignore; + + handle_inactivity_cmd: { + const process_result = inactivity.spawnAndWait() catch { + break :handle_inactivity_cmd; + }; + if (process_result.Exited != 0) { + try state.info_line.addMessage( + state.lang.err_inactivity, + state.config.error_bg, + state.config.error_fg, + ); + try state.log_file.err( + "sys", + "failed to execute inactivity command: exit code {d}", + .{process_result.Exited}, + ); + } + } + } +} + fn addOtherEnvironment(session: *Session, lang: Lang, display_server: DisplayServer, exec: ?[]const u8) !void { const name = switch (display_server) { .shell => lang.shell, diff --git a/src/tui/TerminalBuffer.zig b/src/tui/TerminalBuffer.zig index 56b96da..e203669 100644 --- a/src/tui/TerminalBuffer.zig +++ b/src/tui/TerminalBuffer.zig @@ -5,16 +5,21 @@ const Random = std.Random; const ly_core = @import("ly-core"); const interop = ly_core.interop; const LogFile = ly_core.LogFile; +const SharedError = ly_core.SharedError; pub const termbox = @import("termbox2"); const Cell = @import("Cell.zig"); const keyboard = @import("keyboard.zig"); const Position = @import("Position.zig"); +const Widget = @import("Widget.zig"); const TerminalBuffer = @This(); const KeybindCallbackFn = *const fn (*anyopaque) anyerror!bool; -const KeybindMap = std.AutoHashMap(keyboard.Key, KeybindCallbackFn); +const KeybindMap = std.AutoHashMap(keyboard.Key, struct { + callback: KeybindCallbackFn, + context: *anyopaque, +}); pub const InitOptions = struct { fg: u32, @@ -85,8 +90,17 @@ blank_cell: Cell, full_color: bool, termios: ?std.posix.termios, keybinds: KeybindMap, +handlable_widgets: std.ArrayList(*Widget), +run: bool, +update: bool, +active_widget_index: usize, -pub fn init(allocator: Allocator, 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(); @@ -139,6 +153,10 @@ pub fn init(allocator: Allocator, options: InitOptions, log_file: *LogFile, rand // Needed to reclaim the TTY after giving up its control .termios = try std.posix.tcgetattr(std.posix.STDIN_FILENO), .keybinds = KeybindMap.init(allocator), + .handlable_widgets = .empty, + .run = true, + .update = true, + .active_widget_index = 0, }; } @@ -147,6 +165,159 @@ pub fn deinit(self: *TerminalBuffer) void { TerminalBuffer.shutdown(); } +pub fn runEventLoop( + self: *TerminalBuffer, + allocator: Allocator, + shared_error: SharedError, + widgets: []Widget, + active_widget: Widget, + inactivity_delay: u16, + insert_mode: *bool, + position_widgets_fn: *const fn (*anyopaque) anyerror!void, + inactivity_event_fn: ?*const fn (*anyopaque) anyerror!void, + context: *anyopaque, +) !void { + try self.registerKeybind("Ctrl+K", &moveCursorUp, self); + try self.registerKeybind("Up", &moveCursorUp, self); + + try self.registerKeybind("Ctrl+J", &moveCursorDown, self); + try self.registerKeybind("Down", &moveCursorDown, self); + + try self.registerKeybind("Tab", &wrapCursor, self); + try self.registerKeybind("Shift+Tab", &wrapCursorReverse, self); + + defer self.handlable_widgets.deinit(allocator); + + var i: usize = 0; + for (widgets) |*widget| { + if (widget.vtable.handle_fn != null) { + try self.handlable_widgets.append(allocator, widget); + + if (widget.id == active_widget.id) self.active_widget_index = i; + i += 1; + } + } + + for (widgets) |*widget| try widget.update(context); + try @call(.auto, position_widgets_fn, .{context}); + + var event: termbox.tb_event = undefined; + var inactivity_cmd_ran = false; + var inactivity_time_start = try interop.getTimeOfDay(); + + while (self.run) { + if (self.update) { + for (widgets) |*widget| try widget.update(context); + + // Reset cursor + const current_widget = self.getActiveWidget(); + current_widget.handle(null, insert_mode.*) catch |err| { + shared_error.writeError(error.SetCursorFailed); + try self.log_file.err( + "tui", + "failed to set cursor in active widget '{s}': {s}", + .{ current_widget.display_name, @errorName(err) }, + ); + }; + + try TerminalBuffer.clearScreen(false); + + for (widgets) |*widget| widget.draw(); + + TerminalBuffer.presentBuffer(); + } + + var maybe_timeout: ?usize = null; + for (widgets) |*widget| { + if (try widget.calculateTimeout(context)) |widget_timeout| { + if (maybe_timeout == null or widget_timeout < maybe_timeout.?) maybe_timeout = widget_timeout; + } + } + + if (inactivity_event_fn) |inactivity_fn| { + const time = try interop.getTimeOfDay(); + + if (!inactivity_cmd_ran and time.seconds - inactivity_time_start.seconds > inactivity_delay) { + try @call(.auto, inactivity_fn, .{context}); + inactivity_cmd_ran = true; + } + } + + const event_error = if (maybe_timeout) |timeout| termbox.tb_peek_event(&event, @intCast(timeout)) else termbox.tb_poll_event(&event); + + self.update = maybe_timeout != null; + + 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) { + self.width = TerminalBuffer.getWidth(); + self.height = TerminalBuffer.getHeight(); + + try self.log_file.info( + "tui", + "screen resolution updated to {d}x{d}", + .{ self.width, self.height }, + ); + + for (widgets) |*widget| { + widget.realloc() catch |err| { + shared_error.writeError(error.WidgetReallocationFailed); + try self.log_file.err( + "tui", + "failed to reallocate widget '{s}': {s}", + .{ widget.display_name, @errorName(err) }, + ); + }; + } + + try @call(.auto, position_widgets_fn, .{context}); + + self.update = true; + continue; + } + + var maybe_keys = try self.handleKeybind(allocator, event); + if (maybe_keys) |*keys| { + defer keys.deinit(allocator); + + const current_widget = self.getActiveWidget(); + for (keys.items) |key| { + current_widget.handle(key, insert_mode.*) catch |err| { + shared_error.writeError(error.CurrentWidgetHandlingFailed); + try self.log_file.err( + "tui", + "failed to handle active widget '{s}': {s}", + .{ current_widget.display_name, @errorName(err) }, + ); + }; + } + + self.update = true; + } + } +} + +pub fn stopEventLoop(self: *TerminalBuffer) void { + self.run = false; +} + +pub fn drawNextFrame(self: *TerminalBuffer, value: bool) void { + self.update = value; +} + +pub fn getActiveWidget(self: *TerminalBuffer) *Widget { + return self.handlable_widgets.items[self.active_widget_index]; +} + +pub fn setActiveWidget(self: *TerminalBuffer, widget: Widget) void { + for (self.handlable_widgets.items, 0..) |widg, i| { + if (widg.id == widget.id) self.active_widget_index = i; + } +} + pub fn getWidth() usize { return @intCast(termbox.tb_width()); } @@ -211,31 +382,18 @@ pub fn reclaim(self: TerminalBuffer) !void { } } -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, '+'); +pub fn registerKeybind( + self: *TerminalBuffer, + keybind: []const u8, + callback: KeybindCallbackFn, + context: *anyopaque, +) !void { + const key = try self.parseKeybind(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| { + self.keybinds.put(key, .{ + .callback = callback, + .context = context, + }) catch |err| { try self.log_file.err( "tui", "failed to register keybind {s}: {s}", @@ -244,27 +402,18 @@ pub fn registerKeybind(self: *TerminalBuffer, keybind: []const u8, callback: Key }; } -pub fn handleKeybind( - self: *TerminalBuffer, - allocator: Allocator, - tb_event: termbox.tb_event, - context: *anyopaque, -) !?std.ArrayList(keyboard.Key) { - var keys = try keyboard.getKeyList(allocator, tb_event); +pub fn simulateKeybind(self: *TerminalBuffer, keybind: []const u8) !bool { + const key = try self.parseKeybind(keybind); - for (keys.items) |key| { - if (self.keybinds.get(key)) |callback| { - const passthrough_event = try @call(.auto, callback, .{context}); - if (!passthrough_event) { - keys.deinit(allocator); - return null; - } - - return keys; - } + if (self.keybinds.get(key)) |binding| { + return try @call( + .auto, + binding.callback, + .{binding.context}, + ); } - return keys; + return true; } pub fn drawText( @@ -335,3 +484,91 @@ fn clearBackBuffer() !void { const capability_slice = std.mem.span(capability); _ = try std.posix.write(termbox.global.ttyfd, capability_slice); } + +fn parseKeybind(self: *TerminalBuffer, keybind: []const u8) !keyboard.Key { + 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 }, + ); + } + } + + return key; +} + +fn handleKeybind( + self: *TerminalBuffer, + allocator: Allocator, + tb_event: termbox.tb_event, +) !?std.ArrayList(keyboard.Key) { + var keys = try keyboard.getKeyList(allocator, tb_event); + + for (keys.items) |key| { + if (self.keybinds.get(key)) |binding| { + const passthrough_event = try @call( + .auto, + binding.callback, + .{binding.context}, + ); + + if (!passthrough_event) { + keys.deinit(allocator); + return null; + } + + return keys; + } + } + + return keys; +} + +fn moveCursorUp(ptr: *anyopaque) !bool { + var state: *TerminalBuffer = @ptrCast(@alignCast(ptr)); + if (state.active_widget_index == 0) return false; + + state.active_widget_index -= 1; + state.update = true; + return false; +} + +fn moveCursorDown(ptr: *anyopaque) !bool { + var state: *TerminalBuffer = @ptrCast(@alignCast(ptr)); + if (state.active_widget_index == state.handlable_widgets.items.len - 1) return false; + + state.active_widget_index += 1; + state.update = true; + return false; +} + +fn wrapCursor(ptr: *anyopaque) !bool { + var state: *TerminalBuffer = @ptrCast(@alignCast(ptr)); + + state.active_widget_index = (state.active_widget_index + 1) % state.handlable_widgets.items.len; + state.update = true; + return false; +} + +fn wrapCursorReverse(ptr: *anyopaque) !bool { + var state: *TerminalBuffer = @ptrCast(@alignCast(ptr)); + + state.active_widget_index = if (state.active_widget_index == 0) state.handlable_widgets.items.len - 1 else state.active_widget_index - 1; + state.update = true; + return false; +} From 01dcfa207e3b90d7a893eb4d10f4ecc63ad09095 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Thu, 12 Feb 2026 11:20:54 +0100 Subject: [PATCH 064/176] Show UI errors in info line again Signed-off-by: AnErrupTion --- ly-core/src/SharedError.zig | 17 +++++++++++++++-- src/auth.zig | 2 +- src/main.zig | 35 ++++++++++++++++++++++++++++++++--- 3 files changed, 48 insertions(+), 6 deletions(-) diff --git a/ly-core/src/SharedError.zig b/ly-core/src/SharedError.zig index 9e5de3b..ba3656e 100644 --- a/ly-core/src/SharedError.zig +++ b/ly-core/src/SharedError.zig @@ -10,11 +10,20 @@ const ErrorHandler = packed struct { const SharedError = @This(); data: []align(std.heap.page_size_min) u8, +write_error_event_fn: ?*const fn (anyerror, *anyopaque) anyerror!void, +ctx: ?*anyopaque, -pub fn init() !SharedError { +pub fn init( + write_error_event_fn: ?*const fn (anyerror, *anyopaque) anyerror!void, + ctx: ?*anyopaque, +) !SharedError { const data = try std.posix.mmap(null, @sizeOf(ErrorHandler), std.posix.PROT.READ | std.posix.PROT.WRITE, .{ .TYPE = .SHARED, .ANONYMOUS = true }, -1, 0); - return .{ .data = data }; + return .{ + .data = data, + .write_error_event_fn = write_error_event_fn, + .ctx = ctx, + }; } pub fn deinit(self: *SharedError) void { @@ -25,6 +34,10 @@ pub fn writeError(self: SharedError, err: anyerror) void { var buf_stream = std.io.fixedBufferStream(self.data); const writer = buf_stream.writer(); writer.writeStruct(ErrorHandler{ .has_error = true, .err_int = @intFromError(err) }) catch {}; + + if (self.write_error_event_fn) |write_error_event_fn| { + @call(.auto, write_error_event_fn, .{ err, self.ctx.? }) catch {}; + } } pub fn readError(self: SharedError) ?anyerror { diff --git a/src/auth.zig b/src/auth.zig index b87965c..2dd3393 100644 --- a/src/auth.zig +++ b/src/auth.zig @@ -105,7 +105,7 @@ pub fn authenticate(allocator: std.mem.Allocator, log_file: *LogFile, options: A try log_file.info("auth/passwd", "setting user shell", .{}); if (user_entry.shell == null) interop.setUserShell(&user_entry); - var shared_err = try SharedError.init(); + var shared_err = try SharedError.init(null, null); defer shared_err.deinit(); log_file.deinit(); diff --git a/src/main.zig b/src/main.zig index a79ce00..21b07d6 100644 --- a/src/main.zig +++ b/src/main.zig @@ -1054,6 +1054,7 @@ pub fn main() !void { try state.buffer.registerKeybind("Ctrl+C", &quit, &state); + // TODO: Make this generic for any Text widget present in the UI try state.buffer.registerKeybind("Ctrl+U", &clearPassword, &state); try state.buffer.registerKeybind("K", &viMoveCursorUp, &state); @@ -1093,7 +1094,6 @@ pub fn main() !void { ); } - // Position components and place cursor accordingly if (state.is_autologin) _ = try authenticate(&state); const active_widget = switch (default_input) { @@ -1103,7 +1103,7 @@ pub fn main() !void { .password => state.password_widget, }; - var shared_error = try SharedError.init(); + var shared_error = try SharedError.init(&uiErrorHandler, &state); defer shared_error.deinit(); try state.buffer.runEventLoop( @@ -1119,6 +1119,35 @@ pub fn main() !void { ); } +fn uiErrorHandler(err: anyerror, ctx: *anyopaque) anyerror!void { + var state: *UiState = @ptrCast(@alignCast(ctx)); + + switch (err) { + error.SetCursorFailed => { + try state.info_line.addMessage( + state.lang.err_alloc, + state.config.error_bg, + state.config.error_fg, + ); + }, + error.WidgetReallocationFailed => { + try state.info_line.addMessage( + state.lang.err_alloc, + state.config.error_bg, + state.config.error_fg, + ); + }, + error.CurrentWidgetHandlingFailed => { + try state.info_line.addMessage( + state.lang.err_alloc, + state.config.error_bg, + state.config.error_fg, + ); + }, + else => unreachable, + } +} + fn disableInsertMode(ptr: *anyopaque) !bool { var state: *UiState = @ptrCast(@alignCast(ptr)); @@ -1256,7 +1285,7 @@ fn authenticate(ptr: *anyopaque) !bool { } } - var shared_err = try SharedError.init(); + var shared_err = try SharedError.init(null, null); defer shared_err.deinit(); { From 5a4605ffb628a45e52453cf3d873596ade8a41d7 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Thu, 12 Feb 2026 11:29:14 +0100 Subject: [PATCH 065/176] Add layering system for widgets Signed-off-by: AnErrupTion --- src/main.zig | 58 +++++++++++++++++++++--------------- src/tui/TerminalBuffer.zig | 60 +++++++++++++++++++++++++------------- 2 files changed, 73 insertions(+), 45 deletions(-) diff --git a/src/main.zig b/src/main.zig index 21b07d6..2aa8517 100644 --- a/src/main.zig +++ b/src/main.zig @@ -996,57 +996,67 @@ 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; + var widgets: std.ArrayList([]Widget) = .empty; defer widgets.deinit(state.allocator); + // Layer 1 if (animation) |a| { - try widgets.append(state.allocator, a); + var layer1 = [_]Widget{a}; + try widgets.append(state.allocator, &layer1); } + + // Layer 2 + var layer2: std.ArrayList(Widget) = .empty; + defer layer2.deinit(state.allocator); + if (!state.config.hide_key_hints) { - try widgets.append(state.allocator, state.shutdown_label.widget()); - try widgets.append(state.allocator, state.restart_label.widget()); + try layer2.append(state.allocator, state.shutdown_label.widget()); + try layer2.append(state.allocator, state.restart_label.widget()); if (state.config.sleep_cmd != null) { - try widgets.append(state.allocator, state.sleep_label.widget()); + try layer2.append(state.allocator, state.sleep_label.widget()); } if (state.config.brightness_down_key != null) { - try widgets.append(state.allocator, state.brightness_down_label.widget()); + try layer2.append(state.allocator, state.brightness_down_label.widget()); } if (state.config.brightness_up_key != null) { - try widgets.append(state.allocator, state.brightness_up_label.widget()); + try layer2.append(state.allocator, state.brightness_up_label.widget()); } } if (state.config.battery_id != null) { - try widgets.append(state.allocator, state.battery_label.widget()); + try layer2.append(state.allocator, state.battery_label.widget()); } if (state.config.clock != null) { - try widgets.append(state.allocator, state.clock_label.widget()); + try layer2.append(state.allocator, state.clock_label.widget()); } if (state.config.bigclock != .none) { - try widgets.append(state.allocator, state.bigclock_label.widget()); + try layer2.append(state.allocator, state.bigclock_label.widget()); } if (!state.config.hide_keyboard_locks) { - try widgets.append(state.allocator, state.numlock_label.widget()); - try widgets.append(state.allocator, state.capslock_label.widget()); + try layer2.append(state.allocator, state.numlock_label.widget()); + try layer2.append(state.allocator, state.capslock_label.widget()); } - try widgets.append(state.allocator, state.box.widget()); - try widgets.append(state.allocator, info_line_widget); - try widgets.append(state.allocator, state.session_specifier_label.widget()); - try widgets.append(state.allocator, session_widget); - try widgets.append(state.allocator, state.login_label.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 layer2.append(state.allocator, state.box.widget()); + try layer2.append(state.allocator, info_line_widget); + try layer2.append(state.allocator, state.session_specifier_label.widget()); + try layer2.append(state.allocator, session_widget); + try layer2.append(state.allocator, state.login_label.widget()); + try layer2.append(state.allocator, login_widget); + try layer2.append(state.allocator, state.password_label.widget()); + try layer2.append(state.allocator, state.password_widget); if (!state.config.hide_version_string) { - try widgets.append(state.allocator, state.version_label.widget()); + try layer2.append(state.allocator, state.version_label.widget()); } + + try widgets.append(state.allocator, layer2.items); + + // Layer 3 if (state.config.auth_fails > 0) { - try widgets.append(state.allocator, cascade.widget()); + var layer3 = [_]Widget{cascade.widget()}; + try widgets.append(state.allocator, &layer3); } try state.buffer.registerKeybind("Esc", &disableInsertMode, &state); diff --git a/src/tui/TerminalBuffer.zig b/src/tui/TerminalBuffer.zig index e203669..ec5c6dc 100644 --- a/src/tui/TerminalBuffer.zig +++ b/src/tui/TerminalBuffer.zig @@ -169,7 +169,7 @@ pub fn runEventLoop( self: *TerminalBuffer, allocator: Allocator, shared_error: SharedError, - widgets: []Widget, + layers: [][]Widget, active_widget: Widget, inactivity_delay: u16, insert_mode: *bool, @@ -189,16 +189,22 @@ pub fn runEventLoop( defer self.handlable_widgets.deinit(allocator); var i: usize = 0; - for (widgets) |*widget| { - if (widget.vtable.handle_fn != null) { - try self.handlable_widgets.append(allocator, widget); + for (layers) |layer| { + for (layer) |*widget| { + if (widget.vtable.handle_fn != null) { + try self.handlable_widgets.append(allocator, widget); - if (widget.id == active_widget.id) self.active_widget_index = i; - i += 1; + if (widget.id == active_widget.id) self.active_widget_index = i; + i += 1; + } } } - for (widgets) |*widget| try widget.update(context); + for (layers) |layer| { + for (layer) |*widget| { + try widget.update(context); + } + } try @call(.auto, position_widgets_fn, .{context}); var event: termbox.tb_event = undefined; @@ -207,7 +213,11 @@ pub fn runEventLoop( while (self.run) { if (self.update) { - for (widgets) |*widget| try widget.update(context); + for (layers) |layer| { + for (layer) |*widget| { + try widget.update(context); + } + } // Reset cursor const current_widget = self.getActiveWidget(); @@ -222,15 +232,21 @@ pub fn runEventLoop( try TerminalBuffer.clearScreen(false); - for (widgets) |*widget| widget.draw(); + for (layers) |layer| { + for (layer) |*widget| { + widget.draw(); + } + } TerminalBuffer.presentBuffer(); } var maybe_timeout: ?usize = null; - for (widgets) |*widget| { - if (try widget.calculateTimeout(context)) |widget_timeout| { - if (maybe_timeout == null or widget_timeout < maybe_timeout.?) maybe_timeout = widget_timeout; + for (layers) |layer| { + for (layer) |*widget| { + if (try widget.calculateTimeout(context)) |widget_timeout| { + if (maybe_timeout == null or widget_timeout < maybe_timeout.?) maybe_timeout = widget_timeout; + } } } @@ -262,15 +278,17 @@ pub fn runEventLoop( .{ self.width, self.height }, ); - for (widgets) |*widget| { - widget.realloc() catch |err| { - shared_error.writeError(error.WidgetReallocationFailed); - try self.log_file.err( - "tui", - "failed to reallocate widget '{s}': {s}", - .{ widget.display_name, @errorName(err) }, - ); - }; + for (layers) |layer| { + for (layer) |*widget| { + widget.realloc() catch |err| { + shared_error.writeError(error.WidgetReallocationFailed); + try self.log_file.err( + "tui", + "failed to reallocate widget '{s}': {s}", + .{ widget.display_name, @errorName(err) }, + ); + }; + } } try @call(.auto, position_widgets_fn, .{context}); From b01e4afc79d5ae01c504aaee864e1223e87f5a9c Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Thu, 12 Feb 2026 17:10:52 +0100 Subject: [PATCH 066/176] Make default startup script more compatible Signed-off-by: AnErrupTion --- res/startup.sh | 61 ++++++++++++++++++++++++-------------------------- 1 file changed, 29 insertions(+), 32 deletions(-) diff --git a/res/startup.sh b/res/startup.sh index 2297e2e..c1e88c1 100755 --- a/res/startup.sh +++ b/res/startup.sh @@ -6,35 +6,32 @@ # Uncomment the example below for an example of changing the default TTY colors to an alternitive palette on linux # Colors are in red/green/blue hex (the current colors are a brighter palette than default) # -#if [ "$TERM" = "linux" ]; then -# BLACK="232323" -# DARK_RED="D75F5F" -# DARK_GREEN="87AF5F" -# DARK_YELLOW="D7AF87" -# DARK_BLUE="8787AF" -# DARK_MAGENTA="BD53A5" -# DARK_CYAN="5FAFAF" -# LIGHT_GRAY="E5E5E5" -# DARK_GRAY="2B2B2B" -# RED="E33636" -# GREEN="98E34D" -# YELLOW="FFD75F" -# BLUE="7373C9" -# MAGENTA="D633B2" -# CYAN="44C9C9" -# WHITE="FFFFFF" -# -# COLORS="${BLACK} ${DARK_RED} ${DARK_GREEN} ${DARK_YELLOW} ${DARK_BLUE} ${DARK_MAGENTA} ${DARK_CYAN} ${LIGHT_GRAY} ${DARK_GRAY} ${RED} ${GREEN} ${YELLOW} ${BLUE} ${MAGENTA} ${CYAN} ${WHITE}" -# -# control_palette_str="\e]P" -# -# i=0 -# while [ $i -lt 16 ] -# do -# echo -en "${control_palette_str}$( printf "%x" ${i} )$(echo $COLORS | cut -d ' ' -f`expr $i + 1`)" -# -# i=`expr $i + 1` -# done -# -# clear # for fixing background artifacting after changing color -#fi +# if [ "$TERM" = "linux" ]; then +# BLACK="232323" +# DARK_RED="D75F5F" +# DARK_GREEN="87AF5F" +# DARK_YELLOW="D7AF87" +# DARK_BLUE="8787AF" +# DARK_MAGENTA="BD53A5" +# DARK_CYAN="5FAFAF" +# LIGHT_GRAY="E5E5E5" +# DARK_GRAY="2B2B2B" +# RED="E33636" +# GREEN="98E34D" +# YELLOW="FFD75F" +# BLUE="7373C9" +# MAGENTA="D633B2" +# CYAN="44C9C9" +# WHITE="FFFFFF" + +# COLORS="${BLACK} ${DARK_RED} ${DARK_GREEN} ${DARK_YELLOW} ${DARK_BLUE} ${DARK_MAGENTA} ${DARK_CYAN} ${LIGHT_GRAY} ${DARK_GRAY} ${RED} ${GREEN} ${YELLOW} ${BLUE} ${MAGENTA} ${CYAN} ${WHITE}" + +# i=0 +# while [ $i -lt 16 ]; do +# printf "\033]P%x%s" ${i} "$(echo "$COLORS" | cut -d ' ' -f$(( i + 1)))" + +# i=$(( i + 1 )) +# done + +# clear # for fixing background artifacting after changing color +# fi From 03d976171acbf3d7feb09469c03f98070e8f7ac5 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Fri, 27 Feb 2026 20:28:11 +0100 Subject: [PATCH 067/176] Fix battery level overlapping shutdown label (fixes #935) Signed-off-by: AnErrupTion --- src/main.zig | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main.zig b/src/main.zig index 2aa8517..a1d88b8 100644 --- a/src/main.zig +++ b/src/main.zig @@ -1669,14 +1669,14 @@ fn positionWidgets(ptr: *anyopaque) !void { state.brightness_down_label.positionX(state.hibernate_label .childrenPosition() .addX(1)); - state.brightness_up_label.positionX(state.brightness_down_label + state.brightness_up_label.positionXY(state.brightness_down_label .childrenPosition() .addX(1)); } state.battery_label.positionXY(state.edge_margin .add(TerminalBuffer.START_POSITION) - .addYFromIf(state.shutdown_label.childrenPosition(), !state.config.hide_key_hints) + .addYFromIf(state.brightness_up_label.childrenPosition(), !state.config.hide_key_hints) .removeYFromIf(state.edge_margin, !state.config.hide_key_hints)); state.clock_label.positionXY(state.edge_margin .add(TerminalBuffer.START_POSITION) From 9cde291ac7f56c0b6a3feb568cc8afb80a716de8 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Fri, 27 Feb 2026 22:19:09 +0100 Subject: [PATCH 068/176] Remove unused termbox alias Signed-off-by: AnErrupTion --- src/main.zig | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main.zig b/src/main.zig index a1d88b8..692e1dc 100644 --- a/src/main.zig +++ b/src/main.zig @@ -40,7 +40,6 @@ const Session = @import("tui/components/Session.zig"); const Text = @import("tui/components/Text.zig"); const UserList = @import("tui/components/UserList.zig"); const TerminalBuffer = @import("tui/TerminalBuffer.zig"); -const termbox = TerminalBuffer.termbox; const Widget = @import("tui/Widget.zig"); const ly_version_str = "Ly version " ++ build_options.version; From f31c55b562f4c9da291e900fa9816bf90b5e8697 Mon Sep 17 00:00:00 2001 From: OSVidYapan Date: Mon, 16 Mar 2026 21:13:08 +0100 Subject: [PATCH 069/176] Improve README.md (#940) For anyone who doesn't know see this thread; https://codeberg.org/fairyglade/ly/pulls/934 This is the original post i have fixed the commit log issue (by recreating entire new fork) Note that is is also grammarly fixed version. (mostly please advise if i still have those mistakes) Reviewed-on: https://codeberg.org/fairyglade/ly/pulls/940 Reviewed-by: AnErrupTion Co-authored-by: OSVidYapan Co-committed-by: OSVidYapan --- readme.md | 129 +++++++++++++++++------------------------------------- 1 file changed, 41 insertions(+), 88 deletions(-) diff --git a/readme.md b/readme.md index 9014f5e..88f419d 100644 --- a/readme.md +++ b/readme.md @@ -2,25 +2,32 @@ ![Ly screenshot](.github/screenshot.png "Ly screenshot") -Ly is a lightweight TUI (ncurses-like) display manager for Linux and BSD, -designed with portability in mind (e.g. it does not require systemd to run). +Ly is a lightweight TUI (ncurses-like) display manager for Linux and BSD, designed with portability in mind (e.g. it does not require systemd to run). Join us on Matrix over at [#ly-dm:matrix.org](https://matrix.to/#/#ly-dm:matrix.org)! -**Note**: Development happens on [Codeberg](https://codeberg.org/fairyglade/ly) -with a mirror on [GitHub](https://github.com/fairyglade/ly). +**Note**: Development happens on [Codeberg](https://codeberg.org/fairyglade/ly) with a mirror on [GitHub](https://github.com/fairyglade/ly). ## Dependencies - Compile-time: + - zig 0.15.x + - libc + - pam + - xcb (optional, required by default; needed for X11 support) + - Runtime (with default config): + - xorg + - xorg-xauth + - shutdown + - brightnessctl ### Debian @@ -31,8 +38,7 @@ with a mirror on [GitHub](https://github.com/fairyglade/ly). ### Fedora -**Warning**: You may encounter issues with SELinux on Fedora. -It is recommended to add a rule for Ly as it currently does not ship one. +**Warning**: You may encounter issues with SELinux on Fedora. It is recommended to add a rule for Ly as it currently does not ship one. ``` # dnf install kernel-devel pam-devel libxcb-devel zig xorg-x11-xauth xorg-x11-server brightnessctl @@ -50,16 +56,22 @@ It is recommended to add a rule for Ly as it currently does not ship one. ## Support -Ly has been tested with a wide variety of desktop environments and window -managers, all of which you can find in the sections below: +Every environment that works on other login managers also should work on Ly. -[Wayland environments](#supported-wayland-environments) +- Unlike most login managers Ly has xinitrc entry and it also supports shell. -[X11 environments](#supported-x11-environments) +- If you installed your favorite environment and you don't see it, that's because Ly doesn't automatically refresh itself. To fix this you should restart Ly service (depends on your init system) or the easy way is to reboot your system. + +- If your environment is still missing then check at `/usr/share/xsessions` or `/usr/share/wayland-sessions` to see if a .desktop file is present. + +- If there isn't a .desktop file then create a new one at `/etc/ly/custom-sessions` that launches your favorite environment. These .desktop files can be only seen by Ly and if you want them system-wide you also can create at those directories instead. + +- If only Xorg sessions doesn't work then check if your distro compiles Ly with Xorg support as it can be compiled with Xorg support disabled. Logs are defined by `/etc/ly/config.ini`: - The session log is located at `~/.local/state/ly-session.log` by default. + - The system log is located at `/var/log/ly.log` by default. ## Manually building @@ -72,23 +84,17 @@ $ cd ly $ zig build ``` -After building, you can (optionally) test Ly in a terminal emulator, although -authentication will **not** work: +After building, you can (optionally) test Ly in a terminal emulator, although authentication will **not** work: ``` $ zig build run ``` -**Important**: While you can also run Ly in a terminal emulator as root, it is -**not** recommended either. If you want to properly test Ly, please enable its -service (as described below) and reboot your machine. +**Important**: While you can also run Ly in a terminal emulator as root, it is **not** recommended either. If you want to properly test Ly, please enable its service (as described below) and reboot your machine. -The following sections show how to install Ly for a particular init system. -Because the procedure is very similar for all of them, the commands will only -be detailed for the first section (which is about systemd). +The following sections show how to install Ly for a particular init system. Because the procedure is very similar for all of them, the commands will only be detailed for the first section (which is about systemd). -**Note**: All following sections will assume you are using LightDM for -convenience sake. +**Note**: All following sections will assume you are using LightDM for convenience sake. ### systemd @@ -100,9 +106,7 @@ Now, you can install Ly on your system: **Note**: The `init_system` parameter is optional and defaults to `systemd`. -Note that you also need to disable your current display manager. For example, -if LightDM is the current display manager, you can execute the following -command: +Note that you also need to disable your current display manager. For example, if LightDM is the current display manager, you can execute the following command: ``` # systemctl disable lightdm.service @@ -114,8 +118,7 @@ Then, similarly to the previous command, you need to enable the Ly service: # systemctl enable ly@tty2.service ``` -**Important**: Because Ly runs in a TTY, you **must** disable the TTY service -that Ly will run on, otherwise bad things will happen. For example, to disable `getty` spawning on TTY 2, you need to execute the following command: +**Important**: Because Ly runs in a TTY, you **must** disable the TTY service that Ly will run on, otherwise bad things will happen. For example, to disable `getty` spawning on TTY 2, you need to execute the following command: ``` # systemctl disable getty@tty2.service @@ -127,8 +130,7 @@ The target of the symlink, `ly@ttyN.service`, does not actually exist, but syste Compare to `man 5 logind.conf`, especially regarding the `NAutoVTs=` and `ReserveVT=` parameters. -On non-systemd systems, you can change the TTY Ly will run on by editing the corresponding -service file for your platform. +On non-systemd systems, you can change the TTY Ly will run on by editing the corresponding service file for your platform. ### OpenRC @@ -139,8 +141,7 @@ service file for your platform. # rc-update del agetty.tty2 ``` -**Note**: On Gentoo specifically, you also **must** comment out the appropriate -line for the TTY in /etc/inittab. +**Note**: On Gentoo specifically, you also **must** comment out the appropriate line for the TTY in /etc/inittab. ### runit @@ -171,8 +172,7 @@ To disable TTY 2, edit `/etc/s6/config/tty2.conf` and set `SPAWN="no"`. # dinitctl enable ly ``` -To disable TTY 2, go to `/etc/dinit.d/config/console.conf` and modify -`ACTIVE_CONSOLES`. +To disable TTY 2, go to `/etc/dinit.d/config/console.conf` and modify `ACTIVE_CONSOLES`. ### sysvinit @@ -199,8 +199,7 @@ Ly:\ :al=root: ``` -Then, modify the command field of the `ttyv1` terminal entry in `/etc/ttys` -(TTYs in FreeBSD start at 0): +Then, modify the command field of the `ttyv1` terminal entry in `/etc/ttys` (TTYs in FreeBSD start at 0): ``` ttyv1 "/usr/libexec/getty Ly" xterm on secure @@ -208,37 +207,27 @@ ttyv1 "/usr/libexec/getty Ly" xterm on secure ### Updating -You can also install Ly without overrding the current configuration file. This -is called **updating**. To update, simply run: +You can also install Ly without overrding the current configuration file. This is called **updating**. To update, simply run: ``` # zig build installnoconf ``` -You can, of course, still select the init system of your choice when using this -command. +You can, of course, still select the init system of your choice when using this command. ## Configuration -You can find all the configuration in `/etc/ly/config.ini`. The file is fully -commented, and includes the default values. +You can find all the configuration in `/etc/ly/config.ini`. The file is fully commented, and includes the default values. ## Controls -Use the Up/Down arrow keys to change the current field, and the Left/Right -arrow keys to scroll through the different fields (whether it be the info line, -the desktop environment, or the username). The info line is where messages and -errors are displayed. +Use the Up/Down arrow keys to change the current field, and the Left/Right arrow keys to scroll through the different fields (whether it be the info line, the desktop environment, or the username). The info line is where messages and errors are displayed. ## A note on .xinitrc -If your `.xinitrc` file doesn't work ,make sure it is executable and includes a -shebang. This file is supposed to be a shell script! Quoting from `xinit`'s man -page: +If your `.xinitrc` file doesn't work ,make sure it is executable and includes a shebang. This file is supposed to be a shell script! Quoting from `xinit`'s man page: -> If no specific client program is given on the command line, xinit will look -> for a file in the user's home directory called .xinitrc to run as a shell -> script to start up client programs. +> If no specific client program is given on the command line, xinit will look for a file in the user's home directory called .xinitrc to run as a shell script to start up client programs. A typical shebang for a shell script looks like this: @@ -249,50 +238,14 @@ A typical shebang for a shell script looks like this: ## Tips - The numlock and capslock state is printed in the top-right corner. + - Use the F1 and F2 keys to respectively shutdown and reboot. -- Take a look at your `.xsession` file if X doesn't start, as it can interfere - (this file is launched with X to configure the display properly). -## Supported Wayland environments - -- budgie -- cosmic -- deepin -- enlightenment -- gnome -- hyprland -- kde -- labwc -- niri -- pantheon -- sway -- weston - -## Supported X11 environments - -- awesome -- bspwm -- budgie -- cinnamon -- dwm -- enlightenment -- gnome -- kde -- leftwm -- lxde -- mate -- maxx -- pantheon -- qwm -- spectrwm -- windowmaker -- xfce -- xmonad +- Take a look at your `.xsession` file if X doesn't start, as it can interfere (this file is launched with X to configure the display properly). ## A final note -The name "Ly" is a tribute to the fairy from the game Rayman. Ly was tested by -oxodao, who is some seriously awesome dude. +The name "Ly" is a tribute to the fairy from the game Rayman. Ly was tested by oxodao, who is some seriously awesome dude. Also, Ly wouldn't be there today without [Kawaii-Ash](https://github.com/Kawaii-Ash), who has done significant contributions to the project for the Zig rewrite, which lead to the release of Ly v1.0.0. Massive thanks, and sorry for not crediting you enough beforehand! From 7cefff45705f32a8ec6f093adb7f93900e86e041 Mon Sep 17 00:00:00 2001 From: GalaxyShard Date: Mon, 16 Mar 2026 23:16:43 +0100 Subject: [PATCH 070/176] Add Esperanto translation (#942) Title. - [x] I have tested & confirmed the changes work locally Reviewed-on: https://codeberg.org/fairyglade/ly/pulls/942 Reviewed-by: AnErrupTion Co-authored-by: GalaxyShard Co-committed-by: GalaxyShard --- res/lang/eo.ini | 78 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 res/lang/eo.ini diff --git a/res/lang/eo.ini b/res/lang/eo.ini new file mode 100644 index 0000000..9c75dc3 --- /dev/null +++ b/res/lang/eo.ini @@ -0,0 +1,78 @@ +authenticating = aŭtentigado... +brightness_down = malpliigi helecon +brightness_up = pliigi helecon +capslock = majuskla baskulo +custom = propra +err_alloc = malsukcesis memorasignon +err_args = ne povas analizi argumentojn de komanda linio +err_autologin_session = aŭtomatan ensalutan seancon ne trovis +err_bounds = indico estas ekster-intervala +err_brightness_change = malsukcesis ŝanĝi la helecon +err_chdir = malsukcesis malfermi hejman dosierujon +err_clock_too_long = horloĝa ĉeno estas tro longa +err_config = ne povas analizi agordan dosieron +err_crawl = malsukcesis dum serĉado de seancaj dosierujoj +err_dgn_oob = protokola mesaĝo +err_domain = malvalida domajno +err_empty_password = ne akceptas malplenan pasvorton +err_envlist = malsukcesis preni la medivariablojn +err_get_active_tty = malsukcesis preni la aktivan TTY-on +err_hibernate = malsukcesis ruli la komandon por diskodormo +err_hostname = malsukcesis preni la sistemnomon +err_inactivity = malsukcesis ruli la agorditan komandon por malaktiveco +err_lock_state = malsukcesis preni la ŝlosan staton +err_log = malsukcesis malfermi la protokolan dosieron +err_mlock = malsukcesis ŝlosi pasvortan memoron +err_null = nula memorloko +err_numlock = malsukcesis agordi numeran baskulon +err_pam = PAM-a transakcio malsukcesis +err_pam_abort = PAM-a transakcio malsukcesis +err_pam_acct_expired = konto eksvalidiĝis +err_pam_auth = aŭtentiga eraro +err_pam_authinfo_unavail = malsukcesis preni uzantajn informojn +err_pam_authok_reqd = memorsigno eksvalidiĝis +err_pam_buf = bufra eraro +err_pam_cred_err = malsukcesis agordi akreditaĵon +err_pam_cred_expired = akreditaĵo eksvalidiĝis +err_pam_cred_insufficient = nesufiĉa akreditaĵo +err_pam_cred_unavail = malsukcesis preni akreditaĵon +err_pam_maxtries = atingis maksimuman kvanton da provoj +err_pam_perm_denied = permeso negis +err_pam_session = seancan eraron +err_pam_sys = sisteman eraron +err_pam_user_unknown = ne konas uzanton +err_path = malsukcesis agordi la median dosierindikon +err_perm_dir = malsukcesis ŝanĝi la nunan dosierujon +err_perm_group = malsukcesis redukti grupajn permesojn +err_perm_user = malsukcesis redukti uzantajn permesojn +err_pwnam = malsukcesis preni uzantajn informojn +err_sleep = malsukcesis ruli memordorman komandon +err_start = malsukcesis ruli startan komandon +err_battery = malsukcesis ŝargi baterian staton +err_switch_tty = malsukcesis ŝanĝi TTY-on +err_tty_ctrl = TTY-an stiran transigon malsukcesis +err_no_users = nul uzantojn trovas +err_uid_range = malsukcesis dinamike preni UID-an intervalon +err_user_gid = malsukcesis agordi uzantan GID-on +err_user_init = malsukcesis iniciĝi uzanto +err_user_uid = malsukcesis agordi uzantan UID-on +err_xauth = malsukcesis plenumi je xauth +err_xcb_conn = malsukcesis dum konectado al xcb +err_xsessions_dir = malsukcesis trovi seancan dosierujon +err_xsessions_open = malsukcesis malfermi seancan dosierujon +hibernate = diskodormi +insert = enmeti +login = uzanto +logout = elsalutis +no_x11_support = x11 estas foriĝita de kompil-tempo +normal = normala +numlock = numera baskulo +other = alia +password = pasvorto +restart = restartigi +shell = ŝelo +shutdown = malŝalti +sleep = memordormi +wayland = wayland +x11 = x11 +xinitrc = xinitrc From 3a4109eb2d52c9183fb48c265fd128377864bb1a Mon Sep 17 00:00:00 2001 From: Luna Date: Tue, 17 Mar 2026 12:27:23 +0100 Subject: [PATCH 071/176] Add toggle visibility to password (#938) ## What are the changes about? I have add a keybinding to toggle the visibility of the password ## What existing issue does this resolve? N/A ## Pre-requisites - [x] I have tested & confirmed the changes work locally Reviewed-on: https://codeberg.org/fairyglade/ly/pulls/938 Reviewed-by: AnErrupTion Co-authored-by: Luna Co-committed-by: Luna --- res/config.ini | 3 +++ res/lang/ar.ini | 1 + res/lang/bg.ini | 1 + res/lang/cat.ini | 1 + res/lang/cs.ini | 1 + res/lang/de.ini | 1 + res/lang/en.ini | 1 + res/lang/es.ini | 1 + res/lang/fr.ini | 1 + res/lang/it.ini | 1 + res/lang/ja_JP.ini | 1 + res/lang/ku.ini | 1 + res/lang/lv.ini | 1 + res/lang/pl.ini | 1 + res/lang/pt.ini | 1 + res/lang/pt_BR.ini | 1 + res/lang/ro.ini | 1 + res/lang/ru.ini | 1 + res/lang/sr.ini | 1 + res/lang/sv.ini | 3 ++- res/lang/tr.ini | 3 ++- res/lang/uk.ini | 1 + res/lang/zh_CN.ini | 1 + src/config/Config.zig | 1 + src/config/Lang.zig | 1 + src/main.zig | 34 +++++++++++++++++++++++++++++++++- src/tui/components/Text.zig | 4 ++++ 27 files changed, 66 insertions(+), 3 deletions(-) diff --git a/res/config.ini b/res/config.ini index 9e917f1..715d679 100644 --- a/res/config.ini +++ b/res/config.ini @@ -325,6 +325,9 @@ session_log = .local/state/ly-session.log # Setup command setup_cmd = $CONFIG_DIRECTORY/ly/setup.sh +# Specifies the key combination used for showing the password +show_password_key = F7 + # Command executed when pressing shutdown_key shutdown_cmd = /sbin/shutdown $PLATFORM_SHUTDOWN_ARG now diff --git a/res/lang/ar.ini b/res/lang/ar.ini index 6971326..14aaee6 100644 --- a/res/lang/ar.ini +++ b/res/lang/ar.ini @@ -73,6 +73,7 @@ restart = اعادة التشغيل shell = shell shutdown = ايقاف التشغيل sleep = وضع السكون + wayland = wayland x11 = x11 xinitrc = xinitrc diff --git a/res/lang/bg.ini b/res/lang/bg.ini index 8557023..ee38f60 100644 --- a/res/lang/bg.ini +++ b/res/lang/bg.ini @@ -73,6 +73,7 @@ restart = рестартиране shell = обвивка shutdown = изключване sleep = заспиване + wayland = wayland x11 = x11 xinitrc = xinitrc diff --git a/res/lang/cat.ini b/res/lang/cat.ini index 85dbe53..967c728 100644 --- a/res/lang/cat.ini +++ b/res/lang/cat.ini @@ -73,6 +73,7 @@ restart = reiniciar shell = shell shutdown = aturar sleep = suspendre + wayland = wayland x11 = x11 xinitrc = xinitrc diff --git a/res/lang/cs.ini b/res/lang/cs.ini index ff6943e..9e879e1 100644 --- a/res/lang/cs.ini +++ b/res/lang/cs.ini @@ -73,6 +73,7 @@ restart = restartovat shell = příkazový řádek shutdown = vypnout + wayland = wayland xinitrc = xinitrc diff --git a/res/lang/de.ini b/res/lang/de.ini index 60ca3bd..d4a5f30 100644 --- a/res/lang/de.ini +++ b/res/lang/de.ini @@ -73,6 +73,7 @@ restart = Neustarten shell = Shell shutdown = Herunterfahren sleep = Sleep + wayland = wayland x11 = X11 xinitrc = xinitrc diff --git a/res/lang/en.ini b/res/lang/en.ini index b840816..082b02f 100644 --- a/res/lang/en.ini +++ b/res/lang/en.ini @@ -73,6 +73,7 @@ restart = reboot shell = shell shutdown = shutdown sleep = sleep +toggle_password = toggle password wayland = wayland x11 = x11 xinitrc = xinitrc diff --git a/res/lang/es.ini b/res/lang/es.ini index fd3450a..9f04ecb 100644 --- a/res/lang/es.ini +++ b/res/lang/es.ini @@ -73,6 +73,7 @@ restart = reiniciar shell = shell shutdown = apagar sleep = suspender + wayland = wayland xinitrc = xinitrc diff --git a/res/lang/fr.ini b/res/lang/fr.ini index 17258de..886149b 100644 --- a/res/lang/fr.ini +++ b/res/lang/fr.ini @@ -73,6 +73,7 @@ restart = redémarrer shell = shell shutdown = éteindre sleep = veille + wayland = wayland x11 = x11 xinitrc = xinitrc diff --git a/res/lang/it.ini b/res/lang/it.ini index d909609..245c84e 100644 --- a/res/lang/it.ini +++ b/res/lang/it.ini @@ -73,6 +73,7 @@ restart = riavvio shell = shell shutdown = arresto + wayland = wayland xinitrc = xinitrc diff --git a/res/lang/ja_JP.ini b/res/lang/ja_JP.ini index f77abd3..9c98b93 100644 --- a/res/lang/ja_JP.ini +++ b/res/lang/ja_JP.ini @@ -73,6 +73,7 @@ restart = 再起動 shell = シェル shutdown = シャットダウン sleep = スリープ + wayland = Wayland x11 = X11 xinitrc = xinitrc diff --git a/res/lang/ku.ini b/res/lang/ku.ini index 897d557..a47ef65 100644 --- a/res/lang/ku.ini +++ b/res/lang/ku.ini @@ -73,6 +73,7 @@ restart = ji nû ve bide destpêkirin shell = shell shutdown = vemirîne sleep = têxîne xewê + wayland = wayland x11 = x11 xinitrc = xinitrc diff --git a/res/lang/lv.ini b/res/lang/lv.ini index f572a99..6def7f8 100644 --- a/res/lang/lv.ini +++ b/res/lang/lv.ini @@ -73,6 +73,7 @@ restart = restartēt shell = terminālis shutdown = izslēgt sleep = snauda + wayland = wayland x11 = x11 xinitrc = xinitrc diff --git a/res/lang/pl.ini b/res/lang/pl.ini index aadc9ff..3adef78 100644 --- a/res/lang/pl.ini +++ b/res/lang/pl.ini @@ -73,6 +73,7 @@ restart = uruchom ponownie shell = powłoka shutdown = wyłącz sleep = uśpij + wayland = wayland x11 = x11 xinitrc = xinitrc diff --git a/res/lang/pt.ini b/res/lang/pt.ini index 16d25a3..608a122 100644 --- a/res/lang/pt.ini +++ b/res/lang/pt.ini @@ -73,6 +73,7 @@ restart = reiniciar shell = shell shutdown = encerrar + wayland = wayland xinitrc = xinitrc diff --git a/res/lang/pt_BR.ini b/res/lang/pt_BR.ini index f8d0e26..fb5d58e 100644 --- a/res/lang/pt_BR.ini +++ b/res/lang/pt_BR.ini @@ -73,6 +73,7 @@ restart = reiniciar shell = shell shutdown = desligar + wayland = wayland xinitrc = xinitrc diff --git a/res/lang/ro.ini b/res/lang/ro.ini index 9dfd75a..33c6e5d 100644 --- a/res/lang/ro.ini +++ b/res/lang/ro.ini @@ -73,6 +73,7 @@ restart = resetează shell = shell shutdown = opreşte sistemul + wayland = wayland xinitrc = xinitrc diff --git a/res/lang/ru.ini b/res/lang/ru.ini index e47a41e..baad2f2 100644 --- a/res/lang/ru.ini +++ b/res/lang/ru.ini @@ -73,6 +73,7 @@ restart = перезагрузить shell = оболочка shutdown = выключить sleep = сон + wayland = wayland x11 = x11 xinitrc = xinitrc diff --git a/res/lang/sr.ini b/res/lang/sr.ini index 71c7ea1..e5dcd4b 100644 --- a/res/lang/sr.ini +++ b/res/lang/sr.ini @@ -73,6 +73,7 @@ restart = ponovo pokreni shell = shell shutdown = ugasi + wayland = wayland xinitrc = xinitrc diff --git a/res/lang/sv.ini b/res/lang/sv.ini index 5f869ca..2cb113c 100644 --- a/res/lang/sv.ini +++ b/res/lang/sv.ini @@ -73,6 +73,7 @@ restart = starta om shell = shell shutdown = stäng av sleep = viloläge + wayland = wayland x11 = x11 -xinitrc = xinitrc \ No newline at end of file +xinitrc = xinitrc diff --git a/res/lang/tr.ini b/res/lang/tr.ini index d3ad9ba..807cf30 100644 --- a/res/lang/tr.ini +++ b/res/lang/tr.ini @@ -73,6 +73,7 @@ restart = yeniden baslat shell = shell shutdown = makineyi kapat sleep = uykuya al + wayland = wayland -xinitrc = xinitrc \ No newline at end of file +xinitrc = xinitrc diff --git a/res/lang/uk.ini b/res/lang/uk.ini index f1dcf54..fe37435 100644 --- a/res/lang/uk.ini +++ b/res/lang/uk.ini @@ -73,6 +73,7 @@ restart = перезавантажити shell = оболонка shutdown = вимкнути + wayland = wayland xinitrc = xinitrc diff --git a/res/lang/zh_CN.ini b/res/lang/zh_CN.ini index 493ceac..ce1b23c 100644 --- a/res/lang/zh_CN.ini +++ b/res/lang/zh_CN.ini @@ -73,6 +73,7 @@ password = 密码 shell = shell + wayland = wayland x11 = x11 xinitrc = xinitrc diff --git a/src/config/Config.zig b/src/config/Config.zig index 1672dbb..b3f1e53 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -82,6 +82,7 @@ save: bool = true, service_name: [:0]const u8 = "ly", session_log: ?[]const u8 = "ly-session.log", setup_cmd: []const u8 = build_options.config_directory ++ "/ly/setup.sh", +show_password_key: []const u8 = "F7", shutdown_cmd: []const u8 = "/sbin/shutdown -a now", shutdown_key: []const u8 = "F1", sleep_cmd: ?[]const u8 = null, diff --git a/src/config/Lang.zig b/src/config/Lang.zig index 79145e5..e21700a 100644 --- a/src/config/Lang.zig +++ b/src/config/Lang.zig @@ -78,6 +78,7 @@ restart: []const u8 = "reboot", shell: [:0]const u8 = "shell", shutdown: []const u8 = "shutdown", sleep: []const u8 = "sleep", +toggle_password: []const u8 = "toggle password", wayland: []const u8 = "wayland", x11: []const u8 = "x11", xinitrc: [:0]const u8 = "xinitrc", diff --git a/src/main.zig b/src/main.zig index a1d88b8..0f5df1d 100644 --- a/src/main.zig +++ b/src/main.zig @@ -76,6 +76,7 @@ const UiState = struct { restart_label: Label, sleep_label: Label, hibernate_label: Label, + toggle_password_label: Label, brightness_down_label: Label, brightness_up_label: Label, numlock_label: Label, @@ -393,6 +394,16 @@ pub fn main() !void { ); defer state.hibernate_label.deinit(); + state.toggle_password_label = Label.init( + "", + null, + state.buffer.fg, + state.buffer.bg, + null, + null, + ); + defer state.toggle_password_label.deinit(); + state.brightness_down_label = Label.init( "", null, @@ -424,6 +435,11 @@ pub fn main() !void { "{s} {s}", .{ state.config.restart_key, state.lang.restart }, ); + try state.toggle_password_label.setTextAlloc( + state.allocator, + "{s} {s}", + .{ state.config.show_password_key, state.lang.toggle_password }, + ); if (state.config.sleep_cmd != null) { try state.sleep_label.setTextAlloc( state.allocator, @@ -1019,6 +1035,10 @@ pub fn main() !void { if (state.config.sleep_cmd != null) { try layer2.append(state.allocator, state.sleep_label.widget()); } + if (state.config.hibernate_cmd != null) { + try layer2.append(state.allocator, state.hibernate_label.widget()); + } + try layer2.append(state.allocator, state.toggle_password_label.widget()); if (state.config.brightness_down_key != null) { try layer2.append(state.allocator, state.brightness_down_label.widget()); } @@ -1074,6 +1094,7 @@ pub fn main() !void { try state.buffer.registerKeybind(state.config.shutdown_key, &shutdownCmd, &state); try state.buffer.registerKeybind(state.config.restart_key, &restartCmd, &state); + try state.buffer.registerKeybind(state.config.show_password_key, &togglePasswordMask, &state); if (state.config.sleep_cmd != null) try state.buffer.registerKeybind(state.config.sleep_key, &sleepCmd, &state); if (state.config.hibernate_cmd != null) try state.buffer.registerKeybind(state.config.hibernate_key, &hibernateCmd, &state); if (state.config.brightness_down_key) |key| try state.buffer.registerKeybind(key, &decreaseBrightnessCmd, &state); @@ -1201,6 +1222,14 @@ fn clearPassword(ptr: *anyopaque) !bool { return false; } +fn togglePasswordMask(ptr: *anyopaque) !bool { + var state: *UiState = @ptrCast(@alignCast(ptr)); + + state.password.toggleMask(); + state.buffer.drawNextFrame(true); + return false; +} + fn quit(ptr: *anyopaque) !bool { var state: *UiState = @ptrCast(@alignCast(ptr)); @@ -1666,7 +1695,10 @@ fn positionWidgets(ptr: *anyopaque) !void { state.hibernate_label.positionX(state.sleep_label .childrenPosition() .addX(1)); - state.brightness_down_label.positionX(state.hibernate_label + state.toggle_password_label.positionX(state.hibernate_label + .childrenPosition() + .addX(1)); + state.brightness_down_label.positionX(state.toggle_password_label .childrenPosition() .addX(1)); state.brightness_up_label.positionXY(state.brightness_down_label diff --git a/src/tui/components/Text.zig b/src/tui/components/Text.zig index 3890e79..d31aee7 100644 --- a/src/tui/components/Text.zig +++ b/src/tui/components/Text.zig @@ -96,6 +96,10 @@ pub fn clear(self: *Text) void { self.visible_start = 0; } +pub fn toggleMask(self: *Text) void { + self.masked = !self.masked; +} + pub fn handle(self: *Text, maybe_key: ?keyboard.Key, insert_mode: bool) !void { if (maybe_key) |key| { if (key.left or (!insert_mode and (key.h or key.backspace))) { From 83e98a185fa8280ae296fda696001b6796bc7c38 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Tue, 17 Mar 2026 20:01:47 +0100 Subject: [PATCH 072/176] Add TODO in main.zig Signed-off-by: AnErrupTion --- src/main.zig | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main.zig b/src/main.zig index 4de9af2..0f3f0ab 100644 --- a/src/main.zig +++ b/src/main.zig @@ -1084,6 +1084,7 @@ pub fn main() !void { try state.buffer.registerKeybind("Ctrl+C", &quit, &state); // TODO: Make this generic for any Text widget present in the UI + // TODO: Per-widget keybinds, will fix insert_mode hack too try state.buffer.registerKeybind("Ctrl+U", &clearPassword, &state); try state.buffer.registerKeybind("K", &viMoveCursorUp, &state); From 4f26eeada0c6f8ebe3addc38c561442cb9ee7581 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Tue, 17 Mar 2026 20:10:47 +0100 Subject: [PATCH 073/176] Fix double spacing issue in labels Signed-off-by: AnErrupTion --- src/main.zig | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/src/main.zig b/src/main.zig index 0f3f0ab..5523231 100644 --- a/src/main.zig +++ b/src/main.zig @@ -1686,22 +1686,34 @@ fn positionWidgets(ptr: *anyopaque) !void { if (!state.config.hide_key_hints) { state.shutdown_label.positionX(state.edge_margin .add(TerminalBuffer.START_POSITION)); - state.restart_label.positionX(state.shutdown_label + var last_label = state.shutdown_label; + state.restart_label.positionX(last_label .childrenPosition() .addX(1)); - state.sleep_label.positionX(state.restart_label + last_label = state.restart_label; + state.sleep_label.positionX(last_label .childrenPosition() .addX(1)); - state.hibernate_label.positionX(state.sleep_label + if (state.config.sleep_cmd != null) { + last_label = state.sleep_label; + } + state.hibernate_label.positionX(last_label .childrenPosition() .addX(1)); - state.toggle_password_label.positionX(state.hibernate_label + if (state.config.hibernate_cmd != null) { + last_label = state.hibernate_label; + } + state.toggle_password_label.positionX(last_label .childrenPosition() .addX(1)); - state.brightness_down_label.positionX(state.toggle_password_label + last_label = state.toggle_password_label; + state.brightness_down_label.positionX(last_label .childrenPosition() .addX(1)); - state.brightness_up_label.positionXY(state.brightness_down_label + if (state.config.brightness_down_key != null) { + last_label = state.brightness_down_label; + } + state.brightness_up_label.positionXY(last_label .childrenPosition() .addX(1)); } From 93696a6b305585c28038c65f4e354ba298b16384 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Tue, 17 Mar 2026 21:03:59 +0100 Subject: [PATCH 074/176] Remove unused import + add TODO Signed-off-by: AnErrupTion --- src/tui/components/Session.zig | 2 -- src/tui/components/UserList.zig | 1 + 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/tui/components/Session.zig b/src/tui/components/Session.zig index c221ef6..3f7609e 100644 --- a/src/tui/components/Session.zig +++ b/src/tui/components/Session.zig @@ -1,8 +1,6 @@ const std = @import("std"); const Allocator = std.mem.Allocator; -const enums = @import("../../enums.zig"); -const DisplayServer = enums.DisplayServer; const Environment = @import("../../Environment.zig"); const keyboard = @import("../keyboard.zig"); const TerminalBuffer = @import("../TerminalBuffer.zig"); diff --git a/src/tui/components/UserList.zig b/src/tui/components/UserList.zig index dadf8dc..067cc2f 100644 --- a/src/tui/components/UserList.zig +++ b/src/tui/components/UserList.zig @@ -25,6 +25,7 @@ pub fn init( allocator: Allocator, buffer: *TerminalBuffer, usernames: StringList, + // TODO: Remove dependency on SavedUsers saved_users: *SavedUsers, session: *Session, width: usize, From 64539f43422055a3dfcfd171334ce6606b2c36fb Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Tue, 17 Mar 2026 21:44:33 +0100 Subject: [PATCH 075/176] Split UI code into ly-ui library Signed-off-by: AnErrupTion --- build.zig | 22 +---------- build.zig.zon | 12 +----- {src => ly-core/src}/Environment.zig | 0 {src/config => ly-core/src}/SavedUsers.zig | 0 ly-core/src/enums.zig | 7 ++++ ly-core/src/root.zig | 6 ++- ly-ui/build.zig | 37 +++++++++++++++++++ ly-ui/build.zig.zon | 20 ++++++++++ {src/tui => ly-ui/src}/Cell.zig | 0 {src/tui => ly-ui/src}/Position.zig | 0 {src/tui => ly-ui/src}/TerminalBuffer.zig | 0 {src/tui => ly-ui/src}/Widget.zig | 0 .../tui => ly-ui/src}/components/BigLabel.zig | 0 .../src}/components/CenteredBox.zig | 0 .../tui => ly-ui/src}/components/InfoLine.zig | 0 {src/tui => ly-ui/src}/components/Label.zig | 0 {src/tui => ly-ui/src}/components/Session.zig | 5 ++- {src/tui => ly-ui/src}/components/Text.zig | 0 .../tui => ly-ui/src}/components/UserList.zig | 4 +- .../src}/components/bigLabelLocales/en.zig | 0 .../src}/components/bigLabelLocales/fa.zig | 0 {src/tui => ly-ui/src}/components/generic.zig | 0 {src/tui => ly-ui/src}/keyboard.zig | 0 ly-ui/src/root.zig | 16 ++++++++ src/animations/Cascade.zig | 7 ++-- src/animations/ColorMix.zig | 11 +++--- src/animations/Doom.zig | 11 +++--- src/animations/DurFile.zig | 14 ++++--- src/animations/GameOfLife.zig | 11 +++--- src/animations/Matrix.zig | 11 +++--- src/auth.zig | 5 +-- src/config/migrator.zig | 13 ++++--- src/enums.zig | 8 ---- src/main.zig | 36 +++++++++--------- 34 files changed, 159 insertions(+), 97 deletions(-) rename {src => ly-core/src}/Environment.zig (100%) rename {src/config => ly-core/src}/SavedUsers.zig (100%) create mode 100644 ly-core/src/enums.zig create mode 100644 ly-ui/build.zig create mode 100644 ly-ui/build.zig.zon rename {src/tui => ly-ui/src}/Cell.zig (100%) rename {src/tui => ly-ui/src}/Position.zig (100%) rename {src/tui => ly-ui/src}/TerminalBuffer.zig (100%) rename {src/tui => ly-ui/src}/Widget.zig (100%) rename {src/tui => ly-ui/src}/components/BigLabel.zig (100%) rename {src/tui => ly-ui/src}/components/CenteredBox.zig (100%) rename {src/tui => ly-ui/src}/components/InfoLine.zig (100%) rename {src/tui => ly-ui/src}/components/Label.zig (100%) rename {src/tui => ly-ui/src}/components/Session.zig (95%) rename {src/tui => ly-ui/src}/components/Text.zig (100%) rename {src/tui => ly-ui/src}/components/UserList.zig (97%) rename {src/tui => ly-ui/src}/components/bigLabelLocales/en.zig (100%) rename {src/tui => ly-ui/src}/components/bigLabelLocales/fa.zig (100%) rename {src/tui => ly-ui/src}/components/generic.zig (100%) rename {src/tui => ly-ui/src}/keyboard.zig (100%) create mode 100644 ly-ui/src/root.zig diff --git a/build.zig b/build.zig index dcac1a9..c53b905 100644 --- a/build.zig +++ b/build.zig @@ -72,36 +72,18 @@ pub fn build(b: *std.Build) !void { .use_llvm = true, }); - const ly_core = b.dependency("ly_core", .{ .target = target, .optimize = optimize }); - exe.root_module.addImport("ly-core", ly_core.module("ly-core")); - - const zigini = b.dependency("zigini", .{ .target = target, .optimize = optimize }); - exe.root_module.addImport("zigini", zigini.module("zigini")); + const ly_ui = b.dependency("ly_ui", .{ .target = target, .optimize = optimize }); + exe.root_module.addImport("ly-ui", ly_ui.module("ly-ui")); exe.root_module.addOptions("build_options", build_options); const clap = b.dependency("clap", .{ .target = target, .optimize = optimize }); exe.root_module.addImport("clap", clap.module("clap")); - const termbox_dep = b.dependency("termbox2", .{ - .target = target, - .optimize = optimize, - }); - exe.linkSystemLibrary("pam"); if (enable_x11_support) exe.linkSystemLibrary("xcb"); exe.linkLibC(); - const translate_c = b.addTranslateC(.{ - .root_source_file = termbox_dep.path("termbox2.h"), - .target = target, - .optimize = optimize, - }); - translate_c.defineCMacroRaw("TB_IMPL"); - translate_c.defineCMacro("TB_OPT_ATTR_W", "32"); // Enable 24-bit color support + styling (32-bit) - const termbox2 = translate_c.addModule("termbox2"); - exe.root_module.addImport("termbox2", termbox2); - b.installArtifact(exe); const run_cmd = b.addRunArtifact(exe); diff --git a/build.zig.zon b/build.zig.zon index fc1349d..5471106 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -4,21 +4,13 @@ .fingerprint = 0xa148ffcc5dc2cb59, .minimum_zig_version = "0.15.0", .dependencies = .{ - .ly_core = .{ - .path = "ly-core", + .ly_ui = .{ + .path = "ly-ui", }, .clap = .{ .url = "git+https://github.com/Hejsil/zig-clap#5289e0753cd274d65344bef1c114284c633536ea", .hash = "clap-0.11.0-oBajB-HnAQDPCKYzwF7rO3qDFwRcD39Q0DALlTSz5H7e", }, - .zigini = .{ - .url = "git+https://github.com/AnErrupTion/zigini?ref=zig-0.15.0#9281f47702b57779e831d7618e158abb8eb4d4a2", - .hash = "zigini-0.3.3-36M0FRJJAADZVq5HPm-hYKMpFFTr0OgjbEYcK2ijKZ5n", - }, - .termbox2 = .{ - .url = "git+https://github.com/AnErrupTion/termbox2?ref=master#496730697c662893eec43192f48ff616c2539da6", - .hash = "N-V-__8AAOEWBQDt5tNdIzIFY6n8DdZsCP-6MyLoNS20wgpA", - }, }, .paths = .{""}, } diff --git a/src/Environment.zig b/ly-core/src/Environment.zig similarity index 100% rename from src/Environment.zig rename to ly-core/src/Environment.zig diff --git a/src/config/SavedUsers.zig b/ly-core/src/SavedUsers.zig similarity index 100% rename from src/config/SavedUsers.zig rename to ly-core/src/SavedUsers.zig diff --git a/ly-core/src/enums.zig b/ly-core/src/enums.zig new file mode 100644 index 0000000..6947a2b --- /dev/null +++ b/ly-core/src/enums.zig @@ -0,0 +1,7 @@ +pub const DisplayServer = enum { + wayland, + shell, + xinitrc, + x11, + custom, +}; diff --git a/ly-core/src/root.zig b/ly-core/src/root.zig index a7929f4..3f63a84 100644 --- a/ly-core/src/root.zig +++ b/ly-core/src/root.zig @@ -1,10 +1,14 @@ const std = @import("std"); -const ini = @import("zigini"); + +pub const ini = @import("zigini"); pub const interop = @import("interop.zig"); pub const UidRange = @import("UidRange.zig"); pub const LogFile = @import("LogFile.zig"); pub const SharedError = @import("SharedError.zig"); +pub const SavedUsers = @import("SavedUsers.zig"); +pub const Environment = @import("Environment.zig"); +pub const DisplayServer = @import("enums.zig").DisplayServer; pub fn IniParser(comptime Struct: type) type { return struct { diff --git a/ly-ui/build.zig b/ly-ui/build.zig new file mode 100644 index 0000000..589f7c7 --- /dev/null +++ b/ly-ui/build.zig @@ -0,0 +1,37 @@ +const std = @import("std"); + +pub fn build(b: *std.Build) void { + const target = b.standardTargetOptions(.{}); + const optimize = b.standardOptimizeOption(.{}); + const mod = b.addModule("ly-ui", .{ + .root_source_file = b.path("src/root.zig"), + .target = target, + .optimize = optimize, + }); + + const ly_core = b.dependency("ly_core", .{ .target = target, .optimize = optimize }); + mod.addImport("ly-core", ly_core.module("ly-core")); + + const termbox_dep = b.dependency("termbox2", .{ + .target = target, + .optimize = optimize, + }); + + const translate_c = b.addTranslateC(.{ + .root_source_file = termbox_dep.path("termbox2.h"), + .target = target, + .optimize = optimize, + }); + translate_c.defineCMacroRaw("TB_IMPL"); + translate_c.defineCMacro("TB_OPT_ATTR_W", "32"); // Enable 24-bit color support + styling (32-bit) + const termbox2 = translate_c.addModule("termbox2"); + mod.addImport("termbox2", termbox2); + + const mod_tests = b.addTest(.{ + .root_module = mod, + }); + const run_mod_tests = b.addRunArtifact(mod_tests); + + const test_step = b.step("test", "Run tests"); + test_step.dependOn(&run_mod_tests.step); +} diff --git a/ly-ui/build.zig.zon b/ly-ui/build.zig.zon new file mode 100644 index 0000000..a16ce9e --- /dev/null +++ b/ly-ui/build.zig.zon @@ -0,0 +1,20 @@ +.{ + .name = .ly_ui, + .version = "1.0.0", + .fingerprint = 0x8d11bf85a74ec803, + .minimum_zig_version = "0.15.0", + .dependencies = .{ + .ly_core = .{ + .path = "../ly-core", + }, + .termbox2 = .{ + .url = "git+https://github.com/AnErrupTion/termbox2?ref=master#496730697c662893eec43192f48ff616c2539da6", + .hash = "N-V-__8AAOEWBQDt5tNdIzIFY6n8DdZsCP-6MyLoNS20wgpA", + }, + }, + .paths = .{ + "build.zig", + "build.zig.zon", + "src", + }, +} diff --git a/src/tui/Cell.zig b/ly-ui/src/Cell.zig similarity index 100% rename from src/tui/Cell.zig rename to ly-ui/src/Cell.zig diff --git a/src/tui/Position.zig b/ly-ui/src/Position.zig similarity index 100% rename from src/tui/Position.zig rename to ly-ui/src/Position.zig diff --git a/src/tui/TerminalBuffer.zig b/ly-ui/src/TerminalBuffer.zig similarity index 100% rename from src/tui/TerminalBuffer.zig rename to ly-ui/src/TerminalBuffer.zig diff --git a/src/tui/Widget.zig b/ly-ui/src/Widget.zig similarity index 100% rename from src/tui/Widget.zig rename to ly-ui/src/Widget.zig diff --git a/src/tui/components/BigLabel.zig b/ly-ui/src/components/BigLabel.zig similarity index 100% rename from src/tui/components/BigLabel.zig rename to ly-ui/src/components/BigLabel.zig diff --git a/src/tui/components/CenteredBox.zig b/ly-ui/src/components/CenteredBox.zig similarity index 100% rename from src/tui/components/CenteredBox.zig rename to ly-ui/src/components/CenteredBox.zig diff --git a/src/tui/components/InfoLine.zig b/ly-ui/src/components/InfoLine.zig similarity index 100% rename from src/tui/components/InfoLine.zig rename to ly-ui/src/components/InfoLine.zig diff --git a/src/tui/components/Label.zig b/ly-ui/src/components/Label.zig similarity index 100% rename from src/tui/components/Label.zig rename to ly-ui/src/components/Label.zig diff --git a/src/tui/components/Session.zig b/ly-ui/src/components/Session.zig similarity index 95% rename from src/tui/components/Session.zig rename to ly-ui/src/components/Session.zig index 3f7609e..d7503eb 100644 --- a/src/tui/components/Session.zig +++ b/ly-ui/src/components/Session.zig @@ -1,7 +1,9 @@ const std = @import("std"); const Allocator = std.mem.Allocator; -const Environment = @import("../../Environment.zig"); +const ly_core = @import("ly-core"); +const Environment = ly_core.Environment; + const keyboard = @import("../keyboard.zig"); const TerminalBuffer = @import("../TerminalBuffer.zig"); const Widget = @import("../Widget.zig"); @@ -9,6 +11,7 @@ const generic = @import("generic.zig"); const UserList = @import("UserList.zig"); const Env = struct { + // TODO: Remove dependency on Environment environment: Environment, index: usize, }; diff --git a/src/tui/components/Text.zig b/ly-ui/src/components/Text.zig similarity index 100% rename from src/tui/components/Text.zig rename to ly-ui/src/components/Text.zig diff --git a/src/tui/components/UserList.zig b/ly-ui/src/components/UserList.zig similarity index 97% rename from src/tui/components/UserList.zig rename to ly-ui/src/components/UserList.zig index 067cc2f..76bd796 100644 --- a/src/tui/components/UserList.zig +++ b/ly-ui/src/components/UserList.zig @@ -1,7 +1,9 @@ const std = @import("std"); const Allocator = std.mem.Allocator; -const SavedUsers = @import("../../config/SavedUsers.zig"); +const ly_core = @import("ly-core"); +const SavedUsers = ly_core.SavedUsers; + const keyboard = @import("../keyboard.zig"); const TerminalBuffer = @import("../TerminalBuffer.zig"); const Widget = @import("../Widget.zig"); diff --git a/src/tui/components/bigLabelLocales/en.zig b/ly-ui/src/components/bigLabelLocales/en.zig similarity index 100% rename from src/tui/components/bigLabelLocales/en.zig rename to ly-ui/src/components/bigLabelLocales/en.zig diff --git a/src/tui/components/bigLabelLocales/fa.zig b/ly-ui/src/components/bigLabelLocales/fa.zig similarity index 100% rename from src/tui/components/bigLabelLocales/fa.zig rename to ly-ui/src/components/bigLabelLocales/fa.zig diff --git a/src/tui/components/generic.zig b/ly-ui/src/components/generic.zig similarity index 100% rename from src/tui/components/generic.zig rename to ly-ui/src/components/generic.zig diff --git a/src/tui/keyboard.zig b/ly-ui/src/keyboard.zig similarity index 100% rename from src/tui/keyboard.zig rename to ly-ui/src/keyboard.zig diff --git a/ly-ui/src/root.zig b/ly-ui/src/root.zig new file mode 100644 index 0000000..d21fffe --- /dev/null +++ b/ly-ui/src/root.zig @@ -0,0 +1,16 @@ +pub const ly_core = @import("ly-core"); + +pub const Cell = @import("Cell.zig"); +pub const keyboard = @import("keyboard.zig"); +pub const Position = @import("Position.zig"); +pub const TerminalBuffer = @import("TerminalBuffer.zig"); +pub const Widget = @import("Widget.zig"); + +pub const BigLabel = @import("components/BigLabel.zig"); +pub const CenteredBox = @import("components/CenteredBox.zig"); +pub const CyclableLabel = @import("components/generic.zig").CyclableLabel; +pub const InfoLine = @import("components/InfoLine.zig"); +pub const Label = @import("components/Label.zig"); +pub const Session = @import("components/Session.zig"); +pub const Text = @import("components/Text.zig"); +pub const UserList = @import("components/UserList.zig"); diff --git a/src/animations/Cascade.zig b/src/animations/Cascade.zig index 32f2f6a..402ccd9 100644 --- a/src/animations/Cascade.zig +++ b/src/animations/Cascade.zig @@ -1,9 +1,10 @@ const std = @import("std"); const math = std.math; -const Cell = @import("../tui/Cell.zig"); -const TerminalBuffer = @import("../tui/TerminalBuffer.zig"); -const Widget = @import("../tui/Widget.zig"); +const ly_ui = @import("ly-ui"); +const Cell = ly_ui.Cell; +const TerminalBuffer = ly_ui.TerminalBuffer; +const Widget = ly_ui.Widget; const Cascade = @This(); diff --git a/src/animations/ColorMix.zig b/src/animations/ColorMix.zig index 278f646..2366e8d 100644 --- a/src/animations/ColorMix.zig +++ b/src/animations/ColorMix.zig @@ -1,14 +1,15 @@ const std = @import("std"); const math = std.math; -const ly_core = @import("ly-core"); +const ly_ui = @import("ly-ui"); +const Cell = ly_ui.Cell; +const TerminalBuffer = ly_ui.TerminalBuffer; +const Widget = ly_ui.Widget; + +const ly_core = ly_ui.ly_core; const interop = ly_core.interop; const TimeOfDay = interop.TimeOfDay; -const Cell = @import("../tui/Cell.zig"); -const TerminalBuffer = @import("../tui/TerminalBuffer.zig"); -const Widget = @import("../tui/Widget.zig"); - const ColorMix = @This(); const Vec2 = @Vector(2, f32); diff --git a/src/animations/Doom.zig b/src/animations/Doom.zig index e580bd9..a5db265 100644 --- a/src/animations/Doom.zig +++ b/src/animations/Doom.zig @@ -1,14 +1,15 @@ const std = @import("std"); const Allocator = std.mem.Allocator; -const ly_core = @import("ly-core"); +const ly_ui = @import("ly-ui"); +const Cell = ly_ui.Cell; +const TerminalBuffer = ly_ui.TerminalBuffer; +const Widget = ly_ui.Widget; + +const ly_core = ly_ui.ly_core; const interop = ly_core.interop; const TimeOfDay = interop.TimeOfDay; -const Cell = @import("../tui/Cell.zig"); -const TerminalBuffer = @import("../tui/TerminalBuffer.zig"); -const Widget = @import("../tui/Widget.zig"); - const Doom = @This(); pub const STEPS = 12; diff --git a/src/animations/DurFile.zig b/src/animations/DurFile.zig index 9b24349..3e80ead 100644 --- a/src/animations/DurFile.zig +++ b/src/animations/DurFile.zig @@ -4,18 +4,20 @@ const Json = std.json; const eql = std.mem.eql; const flate = std.compress.flate; -const ly_core = @import("ly-core"); +const ly_ui = @import("ly-ui"); +const Cell = ly_ui.Cell; +const TerminalBuffer = ly_ui.TerminalBuffer; +const Color = TerminalBuffer.Color; +const Styling = TerminalBuffer.Styling; +const Widget = ly_ui.Widget; + +const ly_core = ly_ui.ly_core; const interop = ly_core.interop; const TimeOfDay = interop.TimeOfDay; const LogFile = ly_core.LogFile; const enums = @import("../enums.zig"); const DurOffsetAlignment = enums.DurOffsetAlignment; -const Cell = @import("../tui/Cell.zig"); -const TerminalBuffer = @import("../tui/TerminalBuffer.zig"); -const Color = TerminalBuffer.Color; -const Styling = TerminalBuffer.Styling; -const Widget = @import("../tui/Widget.zig"); fn read_decompress_file(allocator: Allocator, file_path: []const u8) ![]u8 { const file_buffer = std.fs.cwd().openFile(file_path, .{}) catch { diff --git a/src/animations/GameOfLife.zig b/src/animations/GameOfLife.zig index 929a7c0..3ad5a38 100644 --- a/src/animations/GameOfLife.zig +++ b/src/animations/GameOfLife.zig @@ -1,14 +1,15 @@ const std = @import("std"); const Allocator = std.mem.Allocator; -const ly_core = @import("ly-core"); +const ly_ui = @import("ly-ui"); +const Cell = ly_ui.Cell; +const TerminalBuffer = ly_ui.TerminalBuffer; +const Widget = ly_ui.Widget; + +const ly_core = ly_ui.ly_core; const interop = ly_core.interop; const TimeOfDay = interop.TimeOfDay; -const Cell = @import("../tui/Cell.zig"); -const TerminalBuffer = @import("../tui/TerminalBuffer.zig"); -const Widget = @import("../tui/Widget.zig"); - const GameOfLife = @This(); // Visual styles - using block characters like other animations diff --git a/src/animations/Matrix.zig b/src/animations/Matrix.zig index 2ec2aad..8022e45 100644 --- a/src/animations/Matrix.zig +++ b/src/animations/Matrix.zig @@ -2,14 +2,15 @@ const std = @import("std"); const Allocator = std.mem.Allocator; const Random = std.Random; -const ly_core = @import("ly-core"); +const ly_ui = @import("ly-ui"); +const Cell = ly_ui.Cell; +const TerminalBuffer = ly_ui.TerminalBuffer; +const Widget = ly_ui.Widget; + +const ly_core = ly_ui.ly_core; const interop = ly_core.interop; const TimeOfDay = interop.TimeOfDay; -const Cell = @import("../tui/Cell.zig"); -const TerminalBuffer = @import("../tui/TerminalBuffer.zig"); -const Widget = @import("../tui/Widget.zig"); - pub const FRAME_DELAY: usize = 8; // Characters change mid-scroll diff --git a/src/auth.zig b/src/auth.zig index 2dd3393..5b2b8fe 100644 --- a/src/auth.zig +++ b/src/auth.zig @@ -3,15 +3,14 @@ const Md5 = std.crypto.hash.Md5; const builtin = @import("builtin"); const build_options = @import("build_options"); -const ly_core = @import("ly-core"); +const ly_core = @import("ly-ui").ly_core; const interop = ly_core.interop; const SharedError = ly_core.SharedError; const LogFile = ly_core.LogFile; +const Environment = ly_core.Environment; const utmp = interop.utmp; const Utmp = utmp.utmpx; -const Environment = @import("Environment.zig"); - pub const AuthOptions = struct { tty: u8, service_name: [:0]const u8, diff --git a/src/config/migrator.zig b/src/config/migrator.zig index 5e6394d..89d250b 100644 --- a/src/config/migrator.zig +++ b/src/config/migrator.zig @@ -5,16 +5,17 @@ const std = @import("std"); var temporary_allocator = std.heap.page_allocator; -const ini = @import("zigini"); -const ly_core = @import("ly-core"); -const IniParser = ly_core.IniParser; - -const TerminalBuffer = @import("../tui/TerminalBuffer.zig"); +const ly_ui = @import("ly-ui"); +const TerminalBuffer = ly_ui.TerminalBuffer; const Color = TerminalBuffer.Color; const Styling = TerminalBuffer.Styling; +const ly_core = ly_ui.ly_core; +const IniParser = ly_core.IniParser; +const SavedUsers = ly_core.SavedUsers; +const ini = ly_core.ini; + const Config = @import("Config.zig"); const OldSave = @import("OldSave.zig"); -const SavedUsers = @import("SavedUsers.zig"); const color_properties = [_][]const u8{ "bg", diff --git a/src/enums.zig b/src/enums.zig index 47771da..cbe8ed8 100644 --- a/src/enums.zig +++ b/src/enums.zig @@ -9,14 +9,6 @@ pub const Animation = enum { dur_file, }; -pub const DisplayServer = enum { - wayland, - shell, - xinitrc, - x11, - custom, -}; - pub const Input = enum { info_line, session, diff --git a/src/main.zig b/src/main.zig index 5523231..2033fa4 100644 --- a/src/main.zig +++ b/src/main.zig @@ -6,14 +6,29 @@ const builtin = @import("builtin"); const build_options = @import("build_options"); const clap = @import("clap"); -const ini = @import("zigini"); -const Ini = ini.Ini; -const ly_core = @import("ly-core"); +const ly_ui = @import("ly-ui"); +const Position = ly_ui.Position; +const BigLabel = ly_ui.BigLabel; +const CenteredBox = ly_ui.CenteredBox; +const InfoLine = ly_ui.InfoLine; +const Label = ly_ui.Label; +const Session = ly_ui.Session; +const Text = ly_ui.Text; +const UserList = ly_ui.UserList; +const TerminalBuffer = ly_ui.TerminalBuffer; +const Widget = ly_ui.Widget; +const ly_core = ly_ui.ly_core; const interop = ly_core.interop; const UidRange = ly_core.UidRange; const LogFile = ly_core.LogFile; const SharedError = ly_core.SharedError; const IniParser = ly_core.IniParser; +const SavedUsers = ly_core.SavedUsers; +const DisplayServer = ly_core.DisplayServer; +const Environment = ly_core.Environment; +const Entry = Environment.Entry; +const ini = ly_core.ini; +const Ini = ini.Ini; const Cascade = @import("animations/Cascade.zig"); const ColorMix = @import("animations/ColorMix.zig"); @@ -26,21 +41,6 @@ const Config = @import("config/Config.zig"); const Lang = @import("config/Lang.zig"); const migrator = @import("config/migrator.zig"); const OldSave = @import("config/OldSave.zig"); -const SavedUsers = @import("config/SavedUsers.zig"); -const enums = @import("enums.zig"); -const DisplayServer = enums.DisplayServer; -const Environment = @import("Environment.zig"); -const Entry = Environment.Entry; -const Position = @import("tui/Position.zig"); -const BigLabel = @import("tui/components/BigLabel.zig"); -const CenteredBox = @import("tui/components/CenteredBox.zig"); -const InfoLine = @import("tui/components/InfoLine.zig"); -const Label = @import("tui/components/Label.zig"); -const Session = @import("tui/components/Session.zig"); -const Text = @import("tui/components/Text.zig"); -const UserList = @import("tui/components/UserList.zig"); -const TerminalBuffer = @import("tui/TerminalBuffer.zig"); -const Widget = @import("tui/Widget.zig"); const ly_version_str = "Ly version " ++ build_options.version; From a89c918c5d9542c21d9020ae2b8bdc906cefb826 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Tue, 17 Mar 2026 21:59:24 +0100 Subject: [PATCH 076/176] Move back custom widgets into main project Signed-off-by: AnErrupTion --- ly-core/src/enums.zig | 7 ------- ly-core/src/root.zig | 3 --- ly-ui/src/root.zig | 3 --- {ly-core/src => src}/Environment.zig | 2 +- src/auth.zig | 3 ++- {ly-ui/src => src}/components/InfoLine.zig | 11 ++++++----- {ly-ui/src => src}/components/Session.zig | 15 +++++++-------- {ly-ui/src => src}/components/UserList.zig | 15 +++++++-------- {ly-core/src => src/config}/SavedUsers.zig | 0 src/config/migrator.zig | 2 +- src/enums.zig | 8 ++++++++ src/main.zig | 14 +++++++------- 12 files changed, 39 insertions(+), 44 deletions(-) delete mode 100644 ly-core/src/enums.zig rename {ly-core/src => src}/Environment.zig (93%) rename {ly-ui/src => src}/components/InfoLine.zig (89%) rename {ly-ui/src => src}/components/Session.zig (87%) rename {ly-ui/src => src}/components/UserList.zig (89%) rename {ly-core/src => src/config}/SavedUsers.zig (100%) diff --git a/ly-core/src/enums.zig b/ly-core/src/enums.zig deleted file mode 100644 index 6947a2b..0000000 --- a/ly-core/src/enums.zig +++ /dev/null @@ -1,7 +0,0 @@ -pub const DisplayServer = enum { - wayland, - shell, - xinitrc, - x11, - custom, -}; diff --git a/ly-core/src/root.zig b/ly-core/src/root.zig index 3f63a84..4bcb6a8 100644 --- a/ly-core/src/root.zig +++ b/ly-core/src/root.zig @@ -6,9 +6,6 @@ pub const interop = @import("interop.zig"); pub const UidRange = @import("UidRange.zig"); pub const LogFile = @import("LogFile.zig"); pub const SharedError = @import("SharedError.zig"); -pub const SavedUsers = @import("SavedUsers.zig"); -pub const Environment = @import("Environment.zig"); -pub const DisplayServer = @import("enums.zig").DisplayServer; pub fn IniParser(comptime Struct: type) type { return struct { diff --git a/ly-ui/src/root.zig b/ly-ui/src/root.zig index d21fffe..0baa857 100644 --- a/ly-ui/src/root.zig +++ b/ly-ui/src/root.zig @@ -9,8 +9,5 @@ pub const Widget = @import("Widget.zig"); pub const BigLabel = @import("components/BigLabel.zig"); pub const CenteredBox = @import("components/CenteredBox.zig"); pub const CyclableLabel = @import("components/generic.zig").CyclableLabel; -pub const InfoLine = @import("components/InfoLine.zig"); pub const Label = @import("components/Label.zig"); -pub const Session = @import("components/Session.zig"); pub const Text = @import("components/Text.zig"); -pub const UserList = @import("components/UserList.zig"); diff --git a/ly-core/src/Environment.zig b/src/Environment.zig similarity index 93% rename from ly-core/src/Environment.zig rename to src/Environment.zig index f99ebb7..b661c14 100644 --- a/ly-core/src/Environment.zig +++ b/src/Environment.zig @@ -1,4 +1,4 @@ -const ini = @import("zigini"); +const ini = @import("ly-ui").ly_core.ini; const Ini = ini.Ini; const enums = @import("enums.zig"); diff --git a/src/auth.zig b/src/auth.zig index 5b2b8fe..a9cad0c 100644 --- a/src/auth.zig +++ b/src/auth.zig @@ -7,10 +7,11 @@ const ly_core = @import("ly-ui").ly_core; const interop = ly_core.interop; const SharedError = ly_core.SharedError; const LogFile = ly_core.LogFile; -const Environment = ly_core.Environment; const utmp = interop.utmp; const Utmp = utmp.utmpx; +const Environment = @import("Environment.zig"); + pub const AuthOptions = struct { tty: u8, service_name: [:0]const u8, diff --git a/ly-ui/src/components/InfoLine.zig b/src/components/InfoLine.zig similarity index 89% rename from ly-ui/src/components/InfoLine.zig rename to src/components/InfoLine.zig index 95578f5..1afcdcf 100644 --- a/ly-ui/src/components/InfoLine.zig +++ b/src/components/InfoLine.zig @@ -1,12 +1,13 @@ const std = @import("std"); const Allocator = std.mem.Allocator; -const keyboard = @import("../keyboard.zig"); -const TerminalBuffer = @import("../TerminalBuffer.zig"); -const Widget = @import("../Widget.zig"); -const generic = @import("generic.zig"); +const ly_ui = @import("ly-ui"); +const keyboard = ly_ui.keyboard; +const TerminalBuffer = ly_ui.TerminalBuffer; +const Widget = ly_ui.Widget; +const CyclableLabel = ly_ui.CyclableLabel; -const MessageLabel = generic.CyclableLabel(Message, Message); +const MessageLabel = CyclableLabel(Message, Message); const InfoLine = @This(); diff --git a/ly-ui/src/components/Session.zig b/src/components/Session.zig similarity index 87% rename from ly-ui/src/components/Session.zig rename to src/components/Session.zig index d7503eb..6ec4016 100644 --- a/ly-ui/src/components/Session.zig +++ b/src/components/Session.zig @@ -1,21 +1,20 @@ const std = @import("std"); const Allocator = std.mem.Allocator; -const ly_core = @import("ly-core"); -const Environment = ly_core.Environment; +const ly_ui = @import("ly-ui"); +const keyboard = ly_ui.keyboard; +const TerminalBuffer = ly_ui.TerminalBuffer; +const Widget = ly_ui.Widget; +const CyclableLabel = ly_ui.CyclableLabel; -const keyboard = @import("../keyboard.zig"); -const TerminalBuffer = @import("../TerminalBuffer.zig"); -const Widget = @import("../Widget.zig"); -const generic = @import("generic.zig"); const UserList = @import("UserList.zig"); +const Environment = @import("../Environment.zig"); const Env = struct { - // TODO: Remove dependency on Environment environment: Environment, index: usize, }; -const EnvironmentLabel = generic.CyclableLabel(Env, *UserList); +const EnvironmentLabel = CyclableLabel(Env, *UserList); const Session = @This(); diff --git a/ly-ui/src/components/UserList.zig b/src/components/UserList.zig similarity index 89% rename from ly-ui/src/components/UserList.zig rename to src/components/UserList.zig index 76bd796..77b8787 100644 --- a/ly-ui/src/components/UserList.zig +++ b/src/components/UserList.zig @@ -1,14 +1,14 @@ const std = @import("std"); const Allocator = std.mem.Allocator; -const ly_core = @import("ly-core"); -const SavedUsers = ly_core.SavedUsers; +const ly_ui = @import("ly-ui"); +const keyboard = ly_ui.keyboard; +const TerminalBuffer = ly_ui.TerminalBuffer; +const Widget = ly_ui.Widget; +const CyclableLabel = ly_ui.CyclableLabel; -const keyboard = @import("../keyboard.zig"); -const TerminalBuffer = @import("../TerminalBuffer.zig"); -const Widget = @import("../Widget.zig"); -const generic = @import("generic.zig"); const Session = @import("Session.zig"); +const SavedUsers = @import("../config/SavedUsers.zig"); const StringList = std.ArrayListUnmanaged([]const u8); pub const User = struct { @@ -17,7 +17,7 @@ pub const User = struct { allocated_index: bool, first_run: bool, }; -const UserLabel = generic.CyclableLabel(User, *Session); +const UserLabel = CyclableLabel(User, *Session); const UserList = @This(); @@ -27,7 +27,6 @@ pub fn init( allocator: Allocator, buffer: *TerminalBuffer, usernames: StringList, - // TODO: Remove dependency on SavedUsers saved_users: *SavedUsers, session: *Session, width: usize, diff --git a/ly-core/src/SavedUsers.zig b/src/config/SavedUsers.zig similarity index 100% rename from ly-core/src/SavedUsers.zig rename to src/config/SavedUsers.zig diff --git a/src/config/migrator.zig b/src/config/migrator.zig index 89d250b..cc32cf4 100644 --- a/src/config/migrator.zig +++ b/src/config/migrator.zig @@ -11,11 +11,11 @@ const Color = TerminalBuffer.Color; const Styling = TerminalBuffer.Styling; const ly_core = ly_ui.ly_core; const IniParser = ly_core.IniParser; -const SavedUsers = ly_core.SavedUsers; const ini = ly_core.ini; const Config = @import("Config.zig"); const OldSave = @import("OldSave.zig"); +const SavedUsers = @import("SavedUsers.zig"); const color_properties = [_][]const u8{ "bg", diff --git a/src/enums.zig b/src/enums.zig index cbe8ed8..47771da 100644 --- a/src/enums.zig +++ b/src/enums.zig @@ -9,6 +9,14 @@ pub const Animation = enum { dur_file, }; +pub const DisplayServer = enum { + wayland, + shell, + xinitrc, + x11, + custom, +}; + pub const Input = enum { info_line, session, diff --git a/src/main.zig b/src/main.zig index 2033fa4..46ea475 100644 --- a/src/main.zig +++ b/src/main.zig @@ -10,11 +10,8 @@ const ly_ui = @import("ly-ui"); const Position = ly_ui.Position; const BigLabel = ly_ui.BigLabel; const CenteredBox = ly_ui.CenteredBox; -const InfoLine = ly_ui.InfoLine; const Label = ly_ui.Label; -const Session = ly_ui.Session; const Text = ly_ui.Text; -const UserList = ly_ui.UserList; const TerminalBuffer = ly_ui.TerminalBuffer; const Widget = ly_ui.Widget; const ly_core = ly_ui.ly_core; @@ -23,10 +20,6 @@ const UidRange = ly_core.UidRange; const LogFile = ly_core.LogFile; const SharedError = ly_core.SharedError; const IniParser = ly_core.IniParser; -const SavedUsers = ly_core.SavedUsers; -const DisplayServer = ly_core.DisplayServer; -const Environment = ly_core.Environment; -const Entry = Environment.Entry; const ini = ly_core.ini; const Ini = ini.Ini; @@ -37,10 +30,17 @@ const DurFile = @import("animations/DurFile.zig"); const GameOfLife = @import("animations/GameOfLife.zig"); const Matrix = @import("animations/Matrix.zig"); const auth = @import("auth.zig"); +const InfoLine = @import("components/InfoLine.zig"); +const Session = @import("components/Session.zig"); +const UserList = @import("components/UserList.zig"); const Config = @import("config/Config.zig"); const Lang = @import("config/Lang.zig"); const migrator = @import("config/migrator.zig"); const OldSave = @import("config/OldSave.zig"); +const SavedUsers = @import("config/SavedUsers.zig"); +const DisplayServer = @import("enums.zig").DisplayServer; +const Environment = @import("Environment.zig"); +const Entry = Environment.Entry; const ly_version_str = "Ly version " ++ build_options.version; From acac884cfe2f9dadafe6c1ff31f4e39f11b83fe9 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Tue, 17 Mar 2026 22:58:39 +0100 Subject: [PATCH 077/176] Add support for local keybinds Signed-off-by: AnErrupTion --- ly-ui/src/TerminalBuffer.zig | 46 ++++++++++++++++++++++------ ly-ui/src/Widget.zig | 3 ++ ly-ui/src/components/BigLabel.zig | 1 + ly-ui/src/components/CenteredBox.zig | 1 + ly-ui/src/components/Label.zig | 1 + ly-ui/src/components/Text.zig | 22 +++++++++++-- src/animations/Cascade.zig | 1 + src/animations/ColorMix.zig | 1 + src/animations/Doom.zig | 1 + src/animations/DurFile.zig | 1 + src/animations/GameOfLife.zig | 1 + src/animations/Matrix.zig | 1 + src/components/InfoLine.zig | 1 + src/components/Session.zig | 1 + src/components/UserList.zig | 1 + src/main.zig | 44 +++++++++----------------- 16 files changed, 87 insertions(+), 40 deletions(-) diff --git a/ly-ui/src/TerminalBuffer.zig b/ly-ui/src/TerminalBuffer.zig index ec5c6dc..28e03f0 100644 --- a/ly-ui/src/TerminalBuffer.zig +++ b/ly-ui/src/TerminalBuffer.zig @@ -15,8 +15,8 @@ const Widget = @import("Widget.zig"); const TerminalBuffer = @This(); -const KeybindCallbackFn = *const fn (*anyopaque) anyerror!bool; -const KeybindMap = std.AutoHashMap(keyboard.Key, struct { +pub const KeybindCallbackFn = *const fn (*anyopaque) anyerror!bool; +pub const KeybindMap = std.AutoHashMap(keyboard.Key, struct { callback: KeybindCallbackFn, context: *anyopaque, }); @@ -177,14 +177,14 @@ pub fn runEventLoop( inactivity_event_fn: ?*const fn (*anyopaque) anyerror!void, context: *anyopaque, ) !void { - try self.registerKeybind("Ctrl+K", &moveCursorUp, self); - try self.registerKeybind("Up", &moveCursorUp, self); + try self.registerGlobalKeybind("Ctrl+K", &moveCursorUp, self); + try self.registerGlobalKeybind("Up", &moveCursorUp, self); - try self.registerKeybind("Ctrl+J", &moveCursorDown, self); - try self.registerKeybind("Down", &moveCursorDown, self); + try self.registerGlobalKeybind("Ctrl+J", &moveCursorDown, self); + try self.registerGlobalKeybind("Down", &moveCursorDown, self); - try self.registerKeybind("Tab", &wrapCursor, self); - try self.registerKeybind("Shift+Tab", &wrapCursorReverse, self); + try self.registerGlobalKeybind("Tab", &wrapCursor, self); + try self.registerGlobalKeybind("Shift+Tab", &wrapCursorReverse, self); defer self.handlable_widgets.deinit(allocator); @@ -402,13 +402,14 @@ pub fn reclaim(self: TerminalBuffer) !void { pub fn registerKeybind( self: *TerminalBuffer, + keybinds: *KeybindMap, keybind: []const u8, callback: KeybindCallbackFn, context: *anyopaque, ) !void { const key = try self.parseKeybind(keybind); - self.keybinds.put(key, .{ + keybinds.put(key, .{ .callback = callback, .context = context, }) catch |err| { @@ -420,6 +421,15 @@ pub fn registerKeybind( }; } +pub fn registerGlobalKeybind( + self: *TerminalBuffer, + keybind: []const u8, + callback: KeybindCallbackFn, + context: *anyopaque, +) !void { + try self.registerKeybind(&self.keybinds, keybind, callback, context); +} + pub fn simulateKeybind(self: *TerminalBuffer, keybind: []const u8) !bool { const key = try self.parseKeybind(keybind); @@ -552,6 +562,24 @@ fn handleKeybind( return keys; } + + const current_widget = self.getActiveWidget(); + if (current_widget.keybinds) |keybinds| { + if (keybinds.get(key)) |binding| { + const passthrough_event = try @call( + .auto, + binding.callback, + .{binding.context}, + ); + + if (!passthrough_event) { + keys.deinit(allocator); + return null; + } + + return keys; + } + } } return keys; diff --git a/ly-ui/src/Widget.zig b/ly-ui/src/Widget.zig index 98891fd..5c613d8 100644 --- a/ly-ui/src/Widget.zig +++ b/ly-ui/src/Widget.zig @@ -14,11 +14,13 @@ const VTable = struct { id: u64, display_name: []const u8, +keybinds: ?TerminalBuffer.KeybindMap, pointer: *anyopaque, vtable: VTable, pub fn init( display_name: []const u8, + keybinds: ?TerminalBuffer.KeybindMap, pointer: anytype, comptime deinit_fn: ?fn (ptr: @TypeOf(pointer)) void, comptime realloc_fn: ?fn (ptr: @TypeOf(pointer)) anyerror!void, @@ -102,6 +104,7 @@ pub fn init( return .{ .id = @intFromPtr(Impl.vtable.draw_fn), .display_name = display_name, + .keybinds = keybinds, .pointer = pointer, .vtable = Impl.vtable, }; diff --git a/ly-ui/src/components/BigLabel.zig b/ly-ui/src/components/BigLabel.zig index 5159f66..156a498 100644 --- a/ly-ui/src/components/BigLabel.zig +++ b/ly-ui/src/components/BigLabel.zig @@ -88,6 +88,7 @@ pub fn deinit(self: *BigLabel) void { pub fn widget(self: *BigLabel) Widget { return Widget.init( "BigLabel", + null, self, deinit, null, diff --git a/ly-ui/src/components/CenteredBox.zig b/ly-ui/src/components/CenteredBox.zig index f0e7cd5..fe10880 100644 --- a/ly-ui/src/components/CenteredBox.zig +++ b/ly-ui/src/components/CenteredBox.zig @@ -62,6 +62,7 @@ pub fn init( pub fn widget(self: *CenteredBox) Widget { return Widget.init( "CenteredBox", + null, self, null, null, diff --git a/ly-ui/src/components/Label.zig b/ly-ui/src/components/Label.zig index f80793a..9f47820 100644 --- a/ly-ui/src/components/Label.zig +++ b/ly-ui/src/components/Label.zig @@ -46,6 +46,7 @@ pub fn deinit(self: *Label) void { pub fn widget(self: *Label) Widget { return Widget.init( "Label", + null, self, deinit, null, diff --git a/ly-ui/src/components/Text.zig b/ly-ui/src/components/Text.zig index d31aee7..ab139c8 100644 --- a/ly-ui/src/components/Text.zig +++ b/ly-ui/src/components/Text.zig @@ -23,6 +23,7 @@ masked: bool, maybe_mask: ?u32, fg: u32, bg: u32, +keybinds: TerminalBuffer.KeybindMap, pub fn init( allocator: Allocator, @@ -32,8 +33,9 @@ pub fn init( width: usize, fg: u32, bg: u32, -) Text { - return .{ +) !*Text { + var self = try allocator.create(Text); + self.* = Text{ .allocator = allocator, .buffer = buffer, .text = .empty, @@ -47,16 +49,24 @@ pub fn init( .maybe_mask = maybe_mask, .fg = fg, .bg = bg, + .keybinds = .init(allocator), }; + + try buffer.registerKeybind(&self.keybinds, "Ctrl+U", &clearTextEntry, self); + + return self; } pub fn deinit(self: *Text) void { self.text.deinit(self.allocator); + self.keybinds.deinit(); + self.allocator.destroy(self); } pub fn widget(self: *Text) Widget { return Widget.init( "Text", + self.keybinds, self, deinit, null, @@ -208,3 +218,11 @@ fn write(self: *Text, char: u8) !void { self.end += 1; self.goRight(); } + +fn clearTextEntry(ptr: *anyopaque) !bool { + var self: *Text = @ptrCast(@alignCast(ptr)); + + self.clear(); + self.buffer.drawNextFrame(true); + return false; +} diff --git a/src/animations/Cascade.zig b/src/animations/Cascade.zig index 402ccd9..970d99a 100644 --- a/src/animations/Cascade.zig +++ b/src/animations/Cascade.zig @@ -27,6 +27,7 @@ pub fn init( pub fn widget(self: *Cascade) Widget { return Widget.init( "Cascade", + null, self, null, null, diff --git a/src/animations/ColorMix.zig b/src/animations/ColorMix.zig index 2366e8d..227205f 100644 --- a/src/animations/ColorMix.zig +++ b/src/animations/ColorMix.zig @@ -69,6 +69,7 @@ pub fn init( pub fn widget(self: *ColorMix) Widget { return Widget.init( "ColorMix", + null, self, null, null, diff --git a/src/animations/Doom.zig b/src/animations/Doom.zig index a5db265..8fa842b 100644 --- a/src/animations/Doom.zig +++ b/src/animations/Doom.zig @@ -76,6 +76,7 @@ pub fn init( pub fn widget(self: *Doom) Widget { return Widget.init( "Doom", + null, self, deinit, realloc, diff --git a/src/animations/DurFile.zig b/src/animations/DurFile.zig index 3e80ead..04fc8ff 100644 --- a/src/animations/DurFile.zig +++ b/src/animations/DurFile.zig @@ -430,6 +430,7 @@ pub fn init( pub fn widget(self: *DurFile) Widget { return Widget.init( "DurFile", + null, self, deinit, realloc, diff --git a/src/animations/GameOfLife.zig b/src/animations/GameOfLife.zig index 3ad5a38..3f6e5d5 100644 --- a/src/animations/GameOfLife.zig +++ b/src/animations/GameOfLife.zig @@ -86,6 +86,7 @@ pub fn init( pub fn widget(self: *GameOfLife) Widget { return Widget.init( "GameOfLife", + null, self, deinit, realloc, diff --git a/src/animations/Matrix.zig b/src/animations/Matrix.zig index 8022e45..9becbd6 100644 --- a/src/animations/Matrix.zig +++ b/src/animations/Matrix.zig @@ -83,6 +83,7 @@ pub fn init( pub fn widget(self: *Matrix) Widget { return Widget.init( "Matrix", + null, self, deinit, realloc, diff --git a/src/components/InfoLine.zig b/src/components/InfoLine.zig index 1afcdcf..161639b 100644 --- a/src/components/InfoLine.zig +++ b/src/components/InfoLine.zig @@ -49,6 +49,7 @@ pub fn deinit(self: *InfoLine) void { pub fn widget(self: *InfoLine) Widget { return Widget.init( "InfoLine", + null, self, deinit, null, diff --git a/src/components/Session.zig b/src/components/Session.zig index 6ec4016..726de81 100644 --- a/src/components/Session.zig +++ b/src/components/Session.zig @@ -58,6 +58,7 @@ pub fn deinit(self: *Session) void { pub fn widget(self: *Session) Widget { return Widget.init( "Session", + null, self, deinit, null, diff --git a/src/components/UserList.zig b/src/components/UserList.zig index 77b8787..d872b67 100644 --- a/src/components/UserList.zig +++ b/src/components/UserList.zig @@ -92,6 +92,7 @@ pub fn deinit(self: *UserList) void { pub fn widget(self: *UserList) Widget { return Widget.init( "UserList", + null, self, deinit, null, diff --git a/src/main.zig b/src/main.zig index 46ea475..b715ff4 100644 --- a/src/main.zig +++ b/src/main.zig @@ -93,7 +93,7 @@ const UiState = struct { session: Session, saved_users: SavedUsers, login: UserList, - password: Text, + password: *Text, password_widget: Widget, insert_mode: bool, edge_margin: Position, @@ -792,7 +792,7 @@ pub fn main() !void { ); defer state.password_label.deinit(); - state.password = Text.init( + state.password = try Text.init( state.allocator, &state.buffer, true, @@ -1078,27 +1078,23 @@ pub fn main() !void { try widgets.append(state.allocator, &layer3); } - try state.buffer.registerKeybind("Esc", &disableInsertMode, &state); - try state.buffer.registerKeybind("I", &enableInsertMode, &state); + try state.buffer.registerGlobalKeybind("Esc", &disableInsertMode, &state); + try state.buffer.registerGlobalKeybind("I", &enableInsertMode, &state); - try state.buffer.registerKeybind("Ctrl+C", &quit, &state); + try state.buffer.registerGlobalKeybind("Ctrl+C", &quit, &state); - // TODO: Make this generic for any Text widget present in the UI - // TODO: Per-widget keybinds, will fix insert_mode hack too - try state.buffer.registerKeybind("Ctrl+U", &clearPassword, &state); + try state.buffer.registerGlobalKeybind("K", &viMoveCursorUp, &state); + try state.buffer.registerGlobalKeybind("J", &viMoveCursorDown, &state); - try state.buffer.registerKeybind("K", &viMoveCursorUp, &state); - try state.buffer.registerKeybind("J", &viMoveCursorDown, &state); + try state.buffer.registerGlobalKeybind("Enter", &authenticate, &state); - try state.buffer.registerKeybind("Enter", &authenticate, &state); - - try state.buffer.registerKeybind(state.config.shutdown_key, &shutdownCmd, &state); - try state.buffer.registerKeybind(state.config.restart_key, &restartCmd, &state); - try state.buffer.registerKeybind(state.config.show_password_key, &togglePasswordMask, &state); - if (state.config.sleep_cmd != null) try state.buffer.registerKeybind(state.config.sleep_key, &sleepCmd, &state); - if (state.config.hibernate_cmd != null) try state.buffer.registerKeybind(state.config.hibernate_key, &hibernateCmd, &state); - if (state.config.brightness_down_key) |key| try state.buffer.registerKeybind(key, &decreaseBrightnessCmd, &state); - if (state.config.brightness_up_key) |key| try state.buffer.registerKeybind(key, &increaseBrightnessCmd, &state); + try state.buffer.registerGlobalKeybind(state.config.shutdown_key, &shutdownCmd, &state); + try state.buffer.registerGlobalKeybind(state.config.restart_key, &restartCmd, &state); + try state.buffer.registerGlobalKeybind(state.config.show_password_key, &togglePasswordMask, &state); + if (state.config.sleep_cmd != null) try state.buffer.registerGlobalKeybind(state.config.sleep_key, &sleepCmd, &state); + if (state.config.hibernate_cmd != null) try state.buffer.registerGlobalKeybind(state.config.hibernate_key, &hibernateCmd, &state); + if (state.config.brightness_down_key) |key| try state.buffer.registerGlobalKeybind(key, &decreaseBrightnessCmd, &state); + if (state.config.brightness_up_key) |key| try state.buffer.registerGlobalKeybind(key, &increaseBrightnessCmd, &state); if (state.config.initial_info_text) |text| { try state.info_line.addMessage(text, state.config.bg, state.config.fg); @@ -1212,16 +1208,6 @@ fn viMoveCursorDown(ptr: *anyopaque) !bool { return try state.buffer.simulateKeybind("Down"); } -fn clearPassword(ptr: *anyopaque) !bool { - var state: *UiState = @ptrCast(@alignCast(ptr)); - - if (state.buffer.getActiveWidget().id == state.password_widget.id) { - state.password.clear(); - state.buffer.drawNextFrame(true); - } - return false; -} - fn togglePasswordMask(ptr: *anyopaque) !bool { var state: *UiState = @ptrCast(@alignCast(ptr)); From 9c5029705991b80f1a142d0a8cc45256110c889b Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Tue, 17 Mar 2026 23:58:06 +0100 Subject: [PATCH 078/176] Fix insert mode hack + fix bugs Signed-off-by: AnErrupTion --- ly-ui/src/TerminalBuffer.zig | 16 ++++++-- ly-ui/src/Widget.zig | 12 +++--- ly-ui/src/components/CenteredBox.zig | 2 +- ly-ui/src/components/Text.zig | 55 +++++++++++++++++----------- ly-ui/src/components/generic.zig | 38 ++++++++++++------- src/components/InfoLine.zig | 12 +++--- src/components/Session.zig | 12 +++--- src/components/UserList.zig | 10 ++--- src/main.zig | 37 +++++++++++++++++-- 9 files changed, 129 insertions(+), 65 deletions(-) diff --git a/ly-ui/src/TerminalBuffer.zig b/ly-ui/src/TerminalBuffer.zig index 28e03f0..66b915f 100644 --- a/ly-ui/src/TerminalBuffer.zig +++ b/ly-ui/src/TerminalBuffer.zig @@ -172,7 +172,6 @@ pub fn runEventLoop( layers: [][]Widget, active_widget: Widget, inactivity_delay: u16, - insert_mode: *bool, position_widgets_fn: *const fn (*anyopaque) anyerror!void, inactivity_event_fn: ?*const fn (*anyopaque) anyerror!void, context: *anyopaque, @@ -221,7 +220,7 @@ pub fn runEventLoop( // Reset cursor const current_widget = self.getActiveWidget(); - current_widget.handle(null, insert_mode.*) catch |err| { + current_widget.handle(null) catch |err| { shared_error.writeError(error.SetCursorFailed); try self.log_file.err( "tui", @@ -303,7 +302,7 @@ pub fn runEventLoop( const current_widget = self.getActiveWidget(); for (keys.items) |key| { - current_widget.handle(key, insert_mode.*) catch |err| { + current_widget.handle(key) catch |err| { shared_error.writeError(error.CurrentWidgetHandlingFailed); try self.log_file.err( "tui", @@ -441,6 +440,17 @@ pub fn simulateKeybind(self: *TerminalBuffer, keybind: []const u8) !bool { ); } + const current_widget = self.getActiveWidget(); + if (current_widget.keybinds) |keybinds| { + if (keybinds.get(key)) |binding| { + return try @call( + .auto, + binding.callback, + .{binding.context}, + ); + } + } + return true; } diff --git a/ly-ui/src/Widget.zig b/ly-ui/src/Widget.zig index 5c613d8..d66fce8 100644 --- a/ly-ui/src/Widget.zig +++ b/ly-ui/src/Widget.zig @@ -8,7 +8,7 @@ const VTable = struct { 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, + handle_fn: ?*const fn (ptr: *anyopaque, maybe_key: ?keyboard.Key) anyerror!void, calculate_timeout_fn: ?*const fn (ptr: *anyopaque, ctx: *anyopaque) anyerror!?usize, }; @@ -26,7 +26,7 @@ pub fn init( comptime realloc_fn: ?fn (ptr: @TypeOf(pointer)) anyerror!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, + comptime handle_fn: ?fn (ptr: @TypeOf(pointer), maybe_key: ?keyboard.Key) anyerror!void, comptime calculate_timeout_fn: ?fn (ptr: @TypeOf(pointer), ctx: *anyopaque) anyerror!?usize, ) Widget { const Pointer = @TypeOf(pointer); @@ -71,13 +71,13 @@ pub fn init( ); } - pub fn handleImpl(ptr: *anyopaque, maybe_key: ?keyboard.Key, insert_mode: bool) !void { + pub fn handleImpl(ptr: *anyopaque, maybe_key: ?keyboard.Key) !void { const impl: Pointer = @ptrCast(@alignCast(ptr)); return @call( .always_inline, handle_fn.?, - .{ impl, maybe_key, insert_mode }, + .{ impl, maybe_key }, ); } @@ -156,14 +156,14 @@ pub fn update(self: *Widget, ctx: *anyopaque) !void { } } -pub fn handle(self: *Widget, maybe_key: ?keyboard.Key, insert_mode: bool) !void { +pub fn handle(self: *Widget, maybe_key: ?keyboard.Key) !void { const impl: @TypeOf(self.pointer) = @ptrCast(@alignCast(self.pointer)); if (self.vtable.handle_fn) |handle_fn| { return @call( .auto, handle_fn, - .{ impl, maybe_key, insert_mode }, + .{ impl, maybe_key }, ); } } diff --git a/ly-ui/src/components/CenteredBox.zig b/ly-ui/src/components/CenteredBox.zig index fe10880..934a83c 100644 --- a/ly-ui/src/components/CenteredBox.zig +++ b/ly-ui/src/components/CenteredBox.zig @@ -67,7 +67,7 @@ pub fn widget(self: *CenteredBox) Widget { null, null, draw, - null, + update, null, null, ); diff --git a/ly-ui/src/components/Text.zig b/ly-ui/src/components/Text.zig index ab139c8..0ad2128 100644 --- a/ly-ui/src/components/Text.zig +++ b/ly-ui/src/components/Text.zig @@ -19,6 +19,7 @@ visible_start: usize, width: usize, component_pos: Position, children_pos: Position, +should_insert: bool, masked: bool, maybe_mask: ?u32, fg: u32, @@ -28,6 +29,7 @@ keybinds: TerminalBuffer.KeybindMap, pub fn init( allocator: Allocator, buffer: *TerminalBuffer, + should_insert: bool, masked: bool, maybe_mask: ?u32, width: usize, @@ -45,6 +47,7 @@ pub fn init( .width = width, .component_pos = TerminalBuffer.START_POSITION, .children_pos = TerminalBuffer.START_POSITION, + .should_insert = should_insert, .masked = masked, .maybe_mask = maybe_mask, .fg = fg, @@ -52,6 +55,10 @@ pub fn init( .keybinds = .init(allocator), }; + try buffer.registerKeybind(&self.keybinds, "Left", &goLeft, self); + try buffer.registerKeybind(&self.keybinds, "Right", &goRight, self); + try buffer.registerKeybind(&self.keybinds, "Delete", &delete, self); + try buffer.registerKeybind(&self.keybinds, "Backspace", &backspace, self); try buffer.registerKeybind(&self.keybinds, "Ctrl+U", &clearTextEntry, self); return self; @@ -110,17 +117,9 @@ pub fn toggleMask(self: *Text) void { self.masked = !self.masked; } -pub fn handle(self: *Text, maybe_key: ?keyboard.Key, insert_mode: bool) !void { +pub fn handle(self: *Text, maybe_key: ?keyboard.Key) !void { if (maybe_key) |key| { - if (key.left or (!insert_mode and (key.h or key.backspace))) { - self.goLeft(); - } else if (key.right or (!insert_mode and key.l)) { - self.goRight(); - } else if (key.delete) { - self.delete(); - } else if (key.backspace) { - self.backspace(); - } else if (insert_mode) { + if (self.should_insert) { const maybe_character = key.getEnabledPrintableAscii(); if (maybe_character) |character| try self.write(character); } @@ -181,33 +180,45 @@ fn draw(self: *Text) void { ); } -fn goLeft(self: *Text) void { - if (self.cursor == 0) return; +fn goLeft(ptr: *anyopaque) !bool { + var self: *Text = @ptrCast(@alignCast(ptr)); + + if (self.cursor == 0) return false; if (self.visible_start > 0) self.visible_start -= 1; self.cursor -= 1; + return false; } -fn goRight(self: *Text) void { - if (self.cursor >= self.end) return; +fn goRight(ptr: *anyopaque) !bool { + var self: *Text = @ptrCast(@alignCast(ptr)); + + if (self.cursor >= self.end) return false; if (self.cursor - self.visible_start == self.width - 1) self.visible_start += 1; self.cursor += 1; + return false; } -fn delete(self: *Text) void { - if (self.cursor >= self.end) return; +fn delete(ptr: *anyopaque) !bool { + var self: *Text = @ptrCast(@alignCast(ptr)); + + if (self.cursor >= self.end or !self.should_insert) return false; _ = self.text.orderedRemove(self.cursor); self.end -= 1; + return false; } -fn backspace(self: *Text) void { - if (self.cursor == 0) return; +fn backspace(ptr: *anyopaque) !bool { + const self: *Text = @ptrCast(@alignCast(ptr)); - self.goLeft(); - self.delete(); + if (self.cursor == 0 or !self.should_insert) return false; + + _ = try goLeft(ptr); + _ = try delete(ptr); + return false; } fn write(self: *Text, char: u8) !void { @@ -216,12 +227,14 @@ fn write(self: *Text, char: u8) !void { try self.text.insert(self.allocator, self.cursor, char); self.end += 1; - self.goRight(); + _ = try goRight(self); } fn clearTextEntry(ptr: *anyopaque) !bool { var self: *Text = @ptrCast(@alignCast(ptr)); + if (!self.should_insert) return false; + self.clear(); self.buffer.drawNextFrame(true); return false; diff --git a/ly-ui/src/components/generic.zig b/ly-ui/src/components/generic.zig index 4f9c9e7..d7a609d 100644 --- a/ly-ui/src/components/generic.zig +++ b/ly-ui/src/components/generic.zig @@ -28,6 +28,7 @@ pub fn CyclableLabel(comptime ItemType: type, comptime ChangeItemType: type) typ draw_item_fn: DrawItemFn, change_item_fn: ?ChangeItemFn, change_item_arg: ?ChangeItemType, + keybinds: TerminalBuffer.KeybindMap, pub fn init( allocator: Allocator, @@ -39,8 +40,9 @@ pub fn CyclableLabel(comptime ItemType: type, comptime ChangeItemType: type) typ text_in_center: bool, fg: u32, bg: u32, - ) Self { - return .{ + ) !*Self { + var self = try allocator.create(Self); + self.* = .{ .allocator = allocator, .buffer = buffer, .list = .empty, @@ -55,11 +57,21 @@ pub fn CyclableLabel(comptime ItemType: type, comptime ChangeItemType: type) typ .draw_item_fn = draw_item_fn, .change_item_fn = change_item_fn, .change_item_arg = change_item_arg, + .keybinds = .init(allocator), }; + + try buffer.registerKeybind(&self.keybinds, "Left", &goLeft, self); + try buffer.registerKeybind(&self.keybinds, "Ctrl+H", &goLeft, self); + try buffer.registerKeybind(&self.keybinds, "Right", &goRight, self); + try buffer.registerKeybind(&self.keybinds, "Ctrl+L", &goRight, self); + + return self; } pub fn deinit(self: *Self) void { self.list.deinit(self.allocator); + self.keybinds.deinit(); + self.allocator.destroy(self); } pub fn positionX(self: *Self, original_pos: Position) void { @@ -92,15 +104,7 @@ pub fn CyclableLabel(comptime ItemType: type, comptime ChangeItemType: type) typ self.current = self.list.items.len - 1; } - pub fn handle(self: *Self, maybe_key: ?keyboard.Key, insert_mode: bool) void { - if (maybe_key) |key| { - if (key.left or (key.ctrl and key.h) or (!insert_mode and key.h)) { - self.goLeft(); - } else if (key.right or (key.ctrl and key.l) or (!insert_mode and key.l)) { - self.goRight(); - } - } - + pub fn handle(self: *Self, _: ?keyboard.Key) void { TerminalBuffer.setCursor( self.component_pos.x + self.cursor + 2, self.component_pos.y, @@ -132,7 +136,9 @@ pub fn CyclableLabel(comptime ItemType: type, comptime ChangeItemType: type) typ ); } - fn goLeft(self: *Self) void { + fn goLeft(ptr: *anyopaque) !bool { + var self: *Self = @ptrCast(@alignCast(ptr)); + self.current = if (self.current == 0) self.list.items.len - 1 else self.current - 1; if (self.change_item_fn) |change_item_fn| { @@ -142,9 +148,13 @@ pub fn CyclableLabel(comptime ItemType: type, comptime ChangeItemType: type) typ .{ self.list.items[self.current], self.change_item_arg }, ); } + + return false; } - fn goRight(self: *Self) void { + fn goRight(ptr: *anyopaque) !bool { + var self: *Self = @ptrCast(@alignCast(ptr)); + self.current = if (self.current == self.list.items.len - 1) 0 else self.current + 1; if (self.change_item_fn) |change_item_fn| { @@ -154,6 +164,8 @@ pub fn CyclableLabel(comptime ItemType: type, comptime ChangeItemType: type) typ .{ self.list.items[self.current], self.change_item_arg }, ); } + + return false; } }; } diff --git a/src/components/InfoLine.zig b/src/components/InfoLine.zig index 161639b..7e81cf3 100644 --- a/src/components/InfoLine.zig +++ b/src/components/InfoLine.zig @@ -18,7 +18,7 @@ const Message = struct { fg: u32, }; -label: MessageLabel, +label: *MessageLabel, pub fn init( allocator: Allocator, @@ -26,9 +26,9 @@ pub fn init( width: usize, arrow_fg: u32, arrow_bg: u32, -) InfoLine { +) !InfoLine { return .{ - .label = MessageLabel.init( + .label = try MessageLabel.init( allocator, buffer, drawItem, @@ -49,7 +49,7 @@ pub fn deinit(self: *InfoLine) void { pub fn widget(self: *InfoLine) Widget { return Widget.init( "InfoLine", - null, + self.label.keybinds, self, deinit, null, @@ -91,8 +91,8 @@ fn draw(self: *InfoLine) void { self.label.draw(); } -fn handle(self: *InfoLine, maybe_key: ?keyboard.Key, insert_mode: bool) !void { - self.label.handle(maybe_key, insert_mode); +fn handle(self: *InfoLine, maybe_key: ?keyboard.Key) !void { + self.label.handle(maybe_key); } fn drawItem(label: *MessageLabel, message: Message, x: usize, y: usize, width: usize) void { diff --git a/src/components/Session.zig b/src/components/Session.zig index 726de81..373145e 100644 --- a/src/components/Session.zig +++ b/src/components/Session.zig @@ -18,7 +18,7 @@ const EnvironmentLabel = CyclableLabel(Env, *UserList); const Session = @This(); -label: EnvironmentLabel, +label: *EnvironmentLabel, user_list: *UserList, pub fn init( @@ -29,9 +29,9 @@ pub fn init( text_in_center: bool, fg: u32, bg: u32, -) Session { +) !Session { return .{ - .label = EnvironmentLabel.init( + .label = try EnvironmentLabel.init( allocator, buffer, drawItem, @@ -58,7 +58,7 @@ pub fn deinit(self: *Session) void { pub fn widget(self: *Session) Widget { return Widget.init( "Session", - null, + self.label.keybinds, self, deinit, null, @@ -80,8 +80,8 @@ fn draw(self: *Session) void { self.label.draw(); } -fn handle(self: *Session, maybe_key: ?keyboard.Key, insert_mode: bool) !void { - self.label.handle(maybe_key, insert_mode); +fn handle(self: *Session, maybe_key: ?keyboard.Key) !void { + self.label.handle(maybe_key); } fn addedSession(env: Env, user_list: *UserList) void { diff --git a/src/components/UserList.zig b/src/components/UserList.zig index d872b67..5747ed7 100644 --- a/src/components/UserList.zig +++ b/src/components/UserList.zig @@ -21,7 +21,7 @@ const UserLabel = CyclableLabel(User, *Session); const UserList = @This(); -label: UserLabel, +label: *UserLabel, pub fn init( allocator: Allocator, @@ -35,7 +35,7 @@ pub fn init( bg: u32, ) !UserList { var user_list = UserList{ - .label = UserLabel.init( + .label = try UserLabel.init( allocator, buffer, drawItem, @@ -92,7 +92,7 @@ pub fn deinit(self: *UserList) void { pub fn widget(self: *UserList) Widget { return Widget.init( "UserList", - null, + self.label.keybinds, self, deinit, null, @@ -111,8 +111,8 @@ fn draw(self: *UserList) void { self.label.draw(); } -fn handle(self: *UserList, maybe_key: ?keyboard.Key, insert_mode: bool) !void { - self.label.handle(maybe_key, insert_mode); +fn handle(self: *UserList, maybe_key: ?keyboard.Key) !void { + self.label.handle(maybe_key); } fn usernameChanged(user: User, maybe_session: ?*Session) void { diff --git a/src/main.zig b/src/main.zig index b715ff4..9d29599 100644 --- a/src/main.zig +++ b/src/main.zig @@ -540,7 +540,7 @@ pub fn main() !void { &updateBox, ); - state.info_line = InfoLine.init( + state.info_line = try InfoLine.init( state.allocator, &state.buffer, state.box.width - 2 * state.box.horizontal_margin, @@ -549,6 +549,9 @@ pub fn main() !void { ); defer state.info_line.deinit(); + try state.buffer.registerKeybind(&state.info_line.label.keybinds, "H", &viGoLeft, &state); + try state.buffer.registerKeybind(&state.info_line.label.keybinds, "L", &viGoRight, &state); + if (maybe_res == null) { var longest = diag.name.longest(); if (longest.kind == .positional) @@ -650,7 +653,7 @@ pub fn main() !void { ); defer state.session_specifier_label.deinit(); - state.session = Session.init( + state.session = try Session.init( state.allocator, &state.buffer, &state.login, @@ -661,6 +664,9 @@ pub fn main() !void { ); defer state.session.deinit(); + try state.buffer.registerKeybind(&state.session.label.keybinds, "H", &viGoLeft, &state); + try state.buffer.registerKeybind(&state.session.label.keybinds, "L", &viGoRight, &state); + state.login_label = Label.init( state.lang.login, null, @@ -684,6 +690,9 @@ pub fn main() !void { ); defer state.login.deinit(); + try state.buffer.registerKeybind(&state.login.label.keybinds, "H", &viGoLeft, &state); + try state.buffer.registerKeybind(&state.login.label.keybinds, "L", &viGoRight, &state); + addOtherEnvironment(&state.session, state.lang, .shell, null) catch |err| { try state.info_line.addMessage( state.lang.err_alloc, @@ -792,9 +801,12 @@ pub fn main() !void { ); defer state.password_label.deinit(); + state.insert_mode = !state.config.vi_mode or state.config.vi_default_mode == .insert; + state.password = try Text.init( state.allocator, &state.buffer, + state.insert_mode, true, state.config.asterisk, state.box.width - 2 * state.box.horizontal_margin - state.labels_max_length - 1, @@ -803,6 +815,9 @@ pub fn main() !void { ); defer state.password.deinit(); + try state.buffer.registerKeybind(&state.password.keybinds, "H", &viGoLeft, &state); + try state.buffer.registerKeybind(&state.password.keybinds, "L", &viGoRight, &state); + state.password_widget = state.password.widget(); state.version_label = Label.init( @@ -979,7 +994,6 @@ pub fn main() !void { state.auth_fails = 0; state.animate = state.config.animation != .none; - state.insert_mode = !state.config.vi_mode or state.config.vi_default_mode == .insert; state.edge_margin = Position.init( state.config.edge_margin, state.config.edge_margin, @@ -1139,7 +1153,6 @@ pub fn main() !void { widgets.items, active_widget, state.config.inactivity_delay, - &state.insert_mode, // FIXME: Hack positionWidgets, handleInactivity, &state, @@ -1180,6 +1193,7 @@ fn disableInsertMode(ptr: *anyopaque) !bool { if (state.config.vi_mode and state.insert_mode) { state.insert_mode = false; + state.password.should_insert = false; state.buffer.drawNextFrame(true); } return false; @@ -1190,10 +1204,25 @@ fn enableInsertMode(ptr: *anyopaque) !bool { if (state.insert_mode) return true; state.insert_mode = true; + state.password.should_insert = true; state.buffer.drawNextFrame(true); return false; } +fn viGoLeft(ptr: *anyopaque) !bool { + var self: *UiState = @ptrCast(@alignCast(ptr)); + if (self.insert_mode) return true; + + return try self.buffer.simulateKeybind("Left"); +} + +fn viGoRight(ptr: *anyopaque) !bool { + var state: *UiState = @ptrCast(@alignCast(ptr)); + if (state.insert_mode) return true; + + return try state.buffer.simulateKeybind("Right"); +} + fn viMoveCursorUp(ptr: *anyopaque) !bool { var state: *UiState = @ptrCast(@alignCast(ptr)); if (state.insert_mode) return true; From dda56eab373d8abb9cbc59bc4159aa1acd92af73 Mon Sep 17 00:00:00 2001 From: "Mr. Cat" Date: Wed, 18 Mar 2026 20:18:45 +0100 Subject: [PATCH 079/176] Fix compilation error when building without X11 support (#947) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## What are the changes about? Add an argument to info log function to fix compiler error. ## What existing issue does this resolve? If building with `zig build -Denable_x11_support=false` there is a compiler error about a missing argument. ``` install └─ install ly └─ compile exe ly Debug native 1 errors src/main.zig:730:27: error: member function expected 3 argument(s), found 2 try state.log_file.info( ~~~~~~~~~~~~~~^~~~~ ly-core/src/LogFile.zig:26:5: note: function declared here pub fn info(self: *LogFile, category: []const u8, comptime message: []const u8, args: anytype) !void { ~~~~^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ referenced by: callMain [inlined]: /home/oskar/.cache/zig/p/N-V-__8AACFNhBSaulceFT2Wx6Mx-ycGtZh7CEztyVdtX2jW/lib/std/start.zig:627:37 callMainWithArgs [inlined]: /home/oskar/.cache/zig/p/N-V-__8AACFNhBSaulceFT2Wx6Mx-ycGtZh7CEztyVdtX2jW/lib/std/start.zig:587:20 main: /home/oskar/.cache/zig/p/N-V-__8AACFNhBSaulceFT2Wx6Mx-ycGtZh7CEztyVdtX2jW/lib/std/start.zig:602:28 1 reference(s) hidden; use '-freference-trace=4' to see all references ``` ## Pre-requisites - [x] I have tested & confirmed the changes work locally Reviewed-on: https://codeberg.org/fairyglade/ly/pulls/947 Reviewed-by: AnErrupTion Co-authored-by: Mr. Cat Co-committed-by: Mr. Cat --- src/main.zig | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main.zig b/src/main.zig index 9d29599..0e261db 100644 --- a/src/main.zig +++ b/src/main.zig @@ -730,6 +730,7 @@ pub fn main() !void { try state.log_file.info( "comp", "x11 support disabled at compile-time", + .{}, ); } From abe72c74ff12c1fe8dc1170efa132c93ed7c7bf3 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Wed, 18 Mar 2026 20:26:00 +0100 Subject: [PATCH 080/176] Optimise event loop initialisation Signed-off-by: AnErrupTion --- ly-ui/src/TerminalBuffer.zig | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/ly-ui/src/TerminalBuffer.zig b/ly-ui/src/TerminalBuffer.zig index 66b915f..b2383af 100644 --- a/ly-ui/src/TerminalBuffer.zig +++ b/ly-ui/src/TerminalBuffer.zig @@ -190,6 +190,8 @@ pub fn runEventLoop( var i: usize = 0; for (layers) |layer| { for (layer) |*widget| { + try widget.update(context); + if (widget.vtable.handle_fn != null) { try self.handlable_widgets.append(allocator, widget); @@ -199,11 +201,6 @@ pub fn runEventLoop( } } - for (layers) |layer| { - for (layer) |*widget| { - try widget.update(context); - } - } try @call(.auto, position_widgets_fn, .{context}); var event: termbox.tb_event = undefined; From aa392837bcd358cbc673e7d2d9278f320a4af85f Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Sat, 21 Mar 2026 16:01:03 +0100 Subject: [PATCH 081/176] Require "zig fmt" to be run in a PR Signed-off-by: AnErrupTion --- .github/pull_request_template.md | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 0cc851f..45044a3 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -9,3 +9,4 @@ _Replace this with a reference to an existing issue, or N/A if there is none_ ## Pre-requisites - [ ] I have tested & confirmed the changes work locally +- [ ] I have run `zig fmt` throughout my changes From 60e3380375e4752be713f020d4fd9067c1eaeeb5 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Sat, 21 Mar 2026 16:19:33 +0100 Subject: [PATCH 082/176] Switch to single-instance Widget model And make widget() functions return pointers to widgets instead of just widgets Signed-off-by: AnErrupTion --- ly-ui/src/TerminalBuffer.zig | 16 ++++++++-------- ly-ui/src/components/BigLabel.zig | 8 ++++++-- ly-ui/src/components/CenteredBox.zig | 8 ++++++-- ly-ui/src/components/Label.zig | 8 ++++++-- ly-ui/src/components/Text.zig | 8 ++++++-- src/animations/Cascade.zig | 8 ++++++-- src/animations/ColorMix.zig | 8 ++++++-- src/animations/Doom.zig | 8 ++++++-- src/animations/DurFile.zig | 8 ++++++-- src/animations/GameOfLife.zig | 8 ++++++-- src/animations/Matrix.zig | 8 ++++++-- src/components/InfoLine.zig | 8 ++++++-- src/components/Session.zig | 8 ++++++-- src/components/UserList.zig | 8 ++++++-- src/main.zig | 14 +++++++------- 15 files changed, 93 insertions(+), 41 deletions(-) diff --git a/ly-ui/src/TerminalBuffer.zig b/ly-ui/src/TerminalBuffer.zig index b2383af..034916c 100644 --- a/ly-ui/src/TerminalBuffer.zig +++ b/ly-ui/src/TerminalBuffer.zig @@ -169,8 +169,8 @@ pub fn runEventLoop( self: *TerminalBuffer, allocator: Allocator, shared_error: SharedError, - layers: [][]Widget, - active_widget: Widget, + layers: [][]*Widget, + active_widget: *Widget, inactivity_delay: u16, position_widgets_fn: *const fn (*anyopaque) anyerror!void, inactivity_event_fn: ?*const fn (*anyopaque) anyerror!void, @@ -189,7 +189,7 @@ pub fn runEventLoop( var i: usize = 0; for (layers) |layer| { - for (layer) |*widget| { + for (layer) |widget| { try widget.update(context); if (widget.vtable.handle_fn != null) { @@ -210,7 +210,7 @@ pub fn runEventLoop( while (self.run) { if (self.update) { for (layers) |layer| { - for (layer) |*widget| { + for (layer) |widget| { try widget.update(context); } } @@ -229,7 +229,7 @@ pub fn runEventLoop( try TerminalBuffer.clearScreen(false); for (layers) |layer| { - for (layer) |*widget| { + for (layer) |widget| { widget.draw(); } } @@ -239,7 +239,7 @@ pub fn runEventLoop( var maybe_timeout: ?usize = null; for (layers) |layer| { - for (layer) |*widget| { + for (layer) |widget| { if (try widget.calculateTimeout(context)) |widget_timeout| { if (maybe_timeout == null or widget_timeout < maybe_timeout.?) maybe_timeout = widget_timeout; } @@ -275,7 +275,7 @@ pub fn runEventLoop( ); for (layers) |layer| { - for (layer) |*widget| { + for (layer) |widget| { widget.realloc() catch |err| { shared_error.writeError(error.WidgetReallocationFailed); try self.log_file.err( @@ -326,7 +326,7 @@ pub fn getActiveWidget(self: *TerminalBuffer) *Widget { return self.handlable_widgets.items[self.active_widget_index]; } -pub fn setActiveWidget(self: *TerminalBuffer, widget: Widget) void { +pub fn setActiveWidget(self: *TerminalBuffer, widget: *Widget) void { for (self.handlable_widgets.items, 0..) |widg, i| { if (widg.id == widget.id) self.active_widget_index = i; } diff --git a/ly-ui/src/components/BigLabel.zig b/ly-ui/src/components/BigLabel.zig index 156a498..bd9af6e 100644 --- a/ly-ui/src/components/BigLabel.zig +++ b/ly-ui/src/components/BigLabel.zig @@ -44,6 +44,7 @@ pub const BigLabelLocale = enum { fa, }; +instance: ?Widget = null, allocator: ?Allocator = null, buffer: *TerminalBuffer, text: []const u8, @@ -67,6 +68,7 @@ pub fn init( calculate_timeout_fn: ?*const fn (*BigLabel, *anyopaque) anyerror!?usize, ) BigLabel { return .{ + .instance = null, .allocator = null, .buffer = buffer, .text = text, @@ -85,8 +87,9 @@ pub fn deinit(self: *BigLabel) void { if (self.allocator) |allocator| allocator.free(self.text); } -pub fn widget(self: *BigLabel) Widget { - return Widget.init( +pub fn widget(self: *BigLabel) *Widget { + if (self.instance) |*instance| return instance; + self.instance = Widget.init( "BigLabel", null, self, @@ -97,6 +100,7 @@ pub fn widget(self: *BigLabel) Widget { null, calculateTimeout, ); + return &self.instance.?; } pub fn setTextAlloc( diff --git a/ly-ui/src/components/CenteredBox.zig b/ly-ui/src/components/CenteredBox.zig index 934a83c..f68709e 100644 --- a/ly-ui/src/components/CenteredBox.zig +++ b/ly-ui/src/components/CenteredBox.zig @@ -7,6 +7,7 @@ const Widget = @import("../Widget.zig"); const CenteredBox = @This(); +instance: ?Widget = null, buffer: *TerminalBuffer, horizontal_margin: usize, vertical_margin: usize, @@ -40,6 +41,7 @@ pub fn init( update_fn: ?*const fn (*CenteredBox, *anyopaque) anyerror!void, ) CenteredBox { return .{ + .instance = null, .buffer = buffer, .horizontal_margin = horizontal_margin, .vertical_margin = vertical_margin, @@ -59,8 +61,9 @@ pub fn init( }; } -pub fn widget(self: *CenteredBox) Widget { - return Widget.init( +pub fn widget(self: *CenteredBox) *Widget { + if (self.instance) |*instance| return instance; + self.instance = Widget.init( "CenteredBox", null, self, @@ -71,6 +74,7 @@ pub fn widget(self: *CenteredBox) Widget { null, null, ); + return &self.instance.?; } pub fn positionXY(self: *CenteredBox, original_pos: Position) void { diff --git a/ly-ui/src/components/Label.zig b/ly-ui/src/components/Label.zig index 9f47820..9874fcb 100644 --- a/ly-ui/src/components/Label.zig +++ b/ly-ui/src/components/Label.zig @@ -8,6 +8,7 @@ const Position = @import("../Position.zig"); const TerminalBuffer = @import("../TerminalBuffer.zig"); const Widget = @import("../Widget.zig"); +instance: ?Widget, allocator: ?Allocator, text: []const u8, max_width: ?usize, @@ -27,6 +28,7 @@ pub fn init( calculate_timeout_fn: ?*const fn (*Label, *anyopaque) anyerror!?usize, ) Label { return .{ + .instance = null, .allocator = null, .text = text, .max_width = max_width, @@ -43,8 +45,9 @@ pub fn deinit(self: *Label) void { if (self.allocator) |allocator| allocator.free(self.text); } -pub fn widget(self: *Label) Widget { - return Widget.init( +pub fn widget(self: *Label) *Widget { + if (self.instance) |*instance| return instance; + self.instance = Widget.init( "Label", null, self, @@ -55,6 +58,7 @@ pub fn widget(self: *Label) Widget { null, calculateTimeout, ); + return &self.instance.?; } pub fn setTextAlloc( diff --git a/ly-ui/src/components/Text.zig b/ly-ui/src/components/Text.zig index 0ad2128..fc04fe7 100644 --- a/ly-ui/src/components/Text.zig +++ b/ly-ui/src/components/Text.zig @@ -10,6 +10,7 @@ const DynamicString = std.ArrayListUnmanaged(u8); const Text = @This(); +instance: ?Widget, allocator: Allocator, buffer: *TerminalBuffer, text: DynamicString, @@ -38,6 +39,7 @@ pub fn init( ) !*Text { var self = try allocator.create(Text); self.* = Text{ + .instance = null, .allocator = allocator, .buffer = buffer, .text = .empty, @@ -70,8 +72,9 @@ pub fn deinit(self: *Text) void { self.allocator.destroy(self); } -pub fn widget(self: *Text) Widget { - return Widget.init( +pub fn widget(self: *Text) *Widget { + if (self.instance) |*instance| return instance; + self.instance = Widget.init( "Text", self.keybinds, self, @@ -82,6 +85,7 @@ pub fn widget(self: *Text) Widget { handle, null, ); + return &self.instance.?; } pub fn positionX(self: *Text, original_pos: Position) void { diff --git a/src/animations/Cascade.zig b/src/animations/Cascade.zig index 970d99a..b11c728 100644 --- a/src/animations/Cascade.zig +++ b/src/animations/Cascade.zig @@ -8,6 +8,7 @@ const Widget = ly_ui.Widget; const Cascade = @This(); +instance: ?Widget = null, buffer: *TerminalBuffer, current_auth_fails: *usize, max_auth_fails: usize, @@ -18,14 +19,16 @@ pub fn init( max_auth_fails: usize, ) Cascade { return .{ + .instance = null, .buffer = buffer, .current_auth_fails = current_auth_fails, .max_auth_fails = max_auth_fails, }; } -pub fn widget(self: *Cascade) Widget { - return Widget.init( +pub fn widget(self: *Cascade) *Widget { + if (self.instance) |*instance| return instance; + self.instance = Widget.init( "Cascade", null, self, @@ -36,6 +39,7 @@ pub fn widget(self: *Cascade) Widget { null, null, ); + return &self.instance.?; } fn draw(self: *Cascade) void { diff --git a/src/animations/ColorMix.zig b/src/animations/ColorMix.zig index 227205f..0715825 100644 --- a/src/animations/ColorMix.zig +++ b/src/animations/ColorMix.zig @@ -21,6 +21,7 @@ fn length(vec: Vec2) f32 { return math.sqrt(vec[0] * vec[0] + vec[1] * vec[1]); } +instance: ?Widget = null, start_time: TimeOfDay, terminal_buffer: *TerminalBuffer, animate: *bool, @@ -41,6 +42,7 @@ pub fn init( frame_delay: u16, ) !ColorMix { return .{ + .instance = null, .start_time = try interop.getTimeOfDay(), .terminal_buffer = terminal_buffer, .animate = animate, @@ -66,8 +68,9 @@ pub fn init( }; } -pub fn widget(self: *ColorMix) Widget { - return Widget.init( +pub fn widget(self: *ColorMix) *Widget { + if (self.instance) |*instance| return instance; + self.instance = Widget.init( "ColorMix", null, self, @@ -78,6 +81,7 @@ pub fn widget(self: *ColorMix) Widget { null, calculateTimeout, ); + return &self.instance.?; } fn draw(self: *ColorMix) void { diff --git a/src/animations/Doom.zig b/src/animations/Doom.zig index 8fa842b..9b0abae 100644 --- a/src/animations/Doom.zig +++ b/src/animations/Doom.zig @@ -16,6 +16,7 @@ pub const STEPS = 12; pub const HEIGHT_MAX = 9; pub const SPREAD_MAX = 4; +instance: ?Widget = null, start_time: TimeOfDay, allocator: Allocator, terminal_buffer: *TerminalBuffer, @@ -60,6 +61,7 @@ pub fn init( }; return .{ + .instance = null, .start_time = try interop.getTimeOfDay(), .allocator = allocator, .terminal_buffer = terminal_buffer, @@ -73,8 +75,9 @@ pub fn init( }; } -pub fn widget(self: *Doom) Widget { - return Widget.init( +pub fn widget(self: *Doom) *Widget { + if (self.instance) |*instance| return instance; + self.instance = Widget.init( "Doom", null, self, @@ -85,6 +88,7 @@ pub fn widget(self: *Doom) Widget { null, calculateTimeout, ); + return &self.instance.?; } fn deinit(self: *Doom) void { diff --git a/src/animations/DurFile.zig b/src/animations/DurFile.zig index 04fc8ff..b6aceae 100644 --- a/src/animations/DurFile.zig +++ b/src/animations/DurFile.zig @@ -304,6 +304,7 @@ const VEC_Y = 1; const DurFile = @This(); +instance: ?Widget = null, start_time: TimeOfDay, allocator: Allocator, terminal_buffer: *TerminalBuffer, @@ -408,6 +409,7 @@ pub fn init( const frame_time: u32 = @intFromFloat(1000 / dur_movie.framerate.?); return .{ + .instance = null, .start_time = try interop.getTimeOfDay(), .allocator = allocator, .terminal_buffer = terminal_buffer, @@ -427,8 +429,9 @@ pub fn init( }; } -pub fn widget(self: *DurFile) Widget { - return Widget.init( +pub fn widget(self: *DurFile) *Widget { + if (self.instance) |*instance| return instance; + self.instance = Widget.init( "DurFile", null, self, @@ -439,6 +442,7 @@ pub fn widget(self: *DurFile) Widget { null, calculateTimeout, ); + return &self.instance.?; } fn deinit(self: *DurFile) void { diff --git a/src/animations/GameOfLife.zig b/src/animations/GameOfLife.zig index 3f6e5d5..c100c52 100644 --- a/src/animations/GameOfLife.zig +++ b/src/animations/GameOfLife.zig @@ -21,6 +21,7 @@ const NEIGHBOR_DIRS = [_][2]i8{ .{ 1, 0 }, .{ 1, 1 }, }; +instance: ?Widget = null, start_time: TimeOfDay, allocator: Allocator, terminal_buffer: *TerminalBuffer, @@ -58,6 +59,7 @@ pub fn init( const next_grid = try allocator.alloc(bool, grid_size); var game = GameOfLife{ + .instance = null, .start_time = try interop.getTimeOfDay(), .allocator = allocator, .terminal_buffer = terminal_buffer, @@ -83,8 +85,9 @@ pub fn init( return game; } -pub fn widget(self: *GameOfLife) Widget { - return Widget.init( +pub fn widget(self: *GameOfLife) *Widget { + if (self.instance) |*instance| return instance; + self.instance = Widget.init( "GameOfLife", null, self, @@ -95,6 +98,7 @@ pub fn widget(self: *GameOfLife) Widget { null, calculateTimeout, ); + return &self.instance.?; } fn deinit(self: *GameOfLife) void { diff --git a/src/animations/Matrix.zig b/src/animations/Matrix.zig index 9becbd6..e7af489 100644 --- a/src/animations/Matrix.zig +++ b/src/animations/Matrix.zig @@ -29,6 +29,7 @@ pub const Line = struct { update: usize, }; +instance: ?Widget = null, start_time: TimeOfDay, allocator: Allocator, terminal_buffer: *TerminalBuffer, @@ -62,6 +63,7 @@ pub fn init( initBuffers(dots, lines, terminal_buffer.width, terminal_buffer.height, terminal_buffer.random); return .{ + .instance = null, .start_time = try interop.getTimeOfDay(), .allocator = allocator, .terminal_buffer = terminal_buffer, @@ -80,8 +82,9 @@ pub fn init( }; } -pub fn widget(self: *Matrix) Widget { - return Widget.init( +pub fn widget(self: *Matrix) *Widget { + if (self.instance) |*instance| return instance; + self.instance = Widget.init( "Matrix", null, self, @@ -92,6 +95,7 @@ pub fn widget(self: *Matrix) Widget { null, calculateTimeout, ); + return &self.instance.?; } fn deinit(self: *Matrix) void { diff --git a/src/components/InfoLine.zig b/src/components/InfoLine.zig index 7e81cf3..7a1a06b 100644 --- a/src/components/InfoLine.zig +++ b/src/components/InfoLine.zig @@ -18,6 +18,7 @@ const Message = struct { fg: u32, }; +instance: ?Widget = null, label: *MessageLabel, pub fn init( @@ -28,6 +29,7 @@ pub fn init( arrow_bg: u32, ) !InfoLine { return .{ + .instance = null, .label = try MessageLabel.init( allocator, buffer, @@ -46,8 +48,9 @@ pub fn deinit(self: *InfoLine) void { self.label.deinit(); } -pub fn widget(self: *InfoLine) Widget { - return Widget.init( +pub fn widget(self: *InfoLine) *Widget { + if (self.instance) |*instance| return instance; + self.instance = Widget.init( "InfoLine", self.label.keybinds, self, @@ -58,6 +61,7 @@ pub fn widget(self: *InfoLine) Widget { handle, null, ); + return &self.instance.?; } pub fn addMessage(self: *InfoLine, text: []const u8, bg: u32, fg: u32) !void { diff --git a/src/components/Session.zig b/src/components/Session.zig index 373145e..1abf9b9 100644 --- a/src/components/Session.zig +++ b/src/components/Session.zig @@ -18,6 +18,7 @@ const EnvironmentLabel = CyclableLabel(Env, *UserList); const Session = @This(); +instance: ?Widget = null, label: *EnvironmentLabel, user_list: *UserList, @@ -31,6 +32,7 @@ pub fn init( bg: u32, ) !Session { return .{ + .instance = null, .label = try EnvironmentLabel.init( allocator, buffer, @@ -55,8 +57,9 @@ pub fn deinit(self: *Session) void { self.label.deinit(); } -pub fn widget(self: *Session) Widget { - return Widget.init( +pub fn widget(self: *Session) *Widget { + if (self.instance) |*instance| return instance; + self.instance = Widget.init( "Session", self.label.keybinds, self, @@ -67,6 +70,7 @@ pub fn widget(self: *Session) Widget { handle, null, ); + return &self.instance.?; } pub fn addEnvironment(self: *Session, environment: Environment) !void { diff --git a/src/components/UserList.zig b/src/components/UserList.zig index 5747ed7..2131cae 100644 --- a/src/components/UserList.zig +++ b/src/components/UserList.zig @@ -21,6 +21,7 @@ const UserLabel = CyclableLabel(User, *Session); const UserList = @This(); +instance: ?Widget = null, label: *UserLabel, pub fn init( @@ -35,6 +36,7 @@ pub fn init( bg: u32, ) !UserList { var user_list = UserList{ + .instance = null, .label = try UserLabel.init( allocator, buffer, @@ -89,8 +91,9 @@ pub fn deinit(self: *UserList) void { self.label.deinit(); } -pub fn widget(self: *UserList) Widget { - return Widget.init( +pub fn widget(self: *UserList) *Widget { + if (self.instance) |*instance| return instance; + self.instance = Widget.init( "UserList", self.label.keybinds, self, @@ -101,6 +104,7 @@ pub fn widget(self: *UserList) Widget { handle, null, ); + return &self.instance.?; } pub fn getCurrentUsername(self: UserList) []const u8 { diff --git a/src/main.zig b/src/main.zig index 0e261db..fe3c748 100644 --- a/src/main.zig +++ b/src/main.zig @@ -94,7 +94,7 @@ const UiState = struct { saved_users: SavedUsers, login: UserList, password: *Text, - password_widget: Widget, + password_widget: *Widget, insert_mode: bool, edge_margin: Position, config: Config, @@ -910,7 +910,7 @@ pub fn main() !void { } // Initialize the animation, if any - var animation: ?Widget = null; + var animation: ?*Widget = null; switch (state.config.animation) { .none => {}, .doom => { @@ -985,7 +985,7 @@ pub fn main() !void { animation = dur.widget(); }, } - defer if (animation) |*a| a.deinit(); + defer if (animation) |a| a.deinit(); var cascade = Cascade.init( &state.buffer, @@ -1030,17 +1030,17 @@ pub fn main() !void { const session_widget = state.session.widget(); const login_widget = state.login.widget(); - var widgets: std.ArrayList([]Widget) = .empty; + var widgets: std.ArrayList([]*Widget) = .empty; defer widgets.deinit(state.allocator); // Layer 1 if (animation) |a| { - var layer1 = [_]Widget{a}; + var layer1 = [_]*Widget{a}; try widgets.append(state.allocator, &layer1); } // Layer 2 - var layer2: std.ArrayList(Widget) = .empty; + var layer2: std.ArrayList(*Widget) = .empty; defer layer2.deinit(state.allocator); if (!state.config.hide_key_hints) { @@ -1089,7 +1089,7 @@ pub fn main() !void { // Layer 3 if (state.config.auth_fails > 0) { - var layer3 = [_]Widget{cascade.widget()}; + var layer3 = [_]*Widget{cascade.widget()}; try widgets.append(state.allocator, &layer3); } From e5483334730386405003f637e91719d1a5812e66 Mon Sep 17 00:00:00 2001 From: Tom Date: Sat, 21 Mar 2026 19:30:23 +0100 Subject: [PATCH 083/176] Improve README.md (#946) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Tom ## What are the changes about? Sentence structure updates and converting text to md syntax ## What existing issue does this resolve? Fixes: #943 - Updated a few sentences to make them more understandable - Converted the **Note**'s etc to md formatting. ## Pre-requisites - [✓] I have tested & confirmed the changes work locally Reviewed-on: https://codeberg.org/fairyglade/ly/pulls/946 Reviewed-by: AnErrupTion Co-authored-by: Tom Co-committed-by: Tom --- readme.md | 31 +++++++++++++++++++------------ 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/readme.md b/readme.md index 88f419d..8ecec06 100644 --- a/readme.md +++ b/readme.md @@ -2,11 +2,12 @@ ![Ly screenshot](.github/screenshot.png "Ly screenshot") -Ly is a lightweight TUI (ncurses-like) display manager for Linux and BSD, designed with portability in mind (e.g. it does not require systemd to run). +Ly is a lightweight TUI (ncurses-like) display manager for Linux and BSD, designed with portability in mind and doesn't require systemd to run. Join us on Matrix over at [#ly-dm:matrix.org](https://matrix.to/#/#ly-dm:matrix.org)! -**Note**: Development happens on [Codeberg](https://codeberg.org/fairyglade/ly) with a mirror on [GitHub](https://github.com/fairyglade/ly). +> [!NOTE] +> Development happens on [Codeberg](https://codeberg.org/fairyglade/ly) with a mirror on [GitHub](https://github.com/fairyglade/ly). ## Dependencies @@ -38,7 +39,8 @@ Join us on Matrix over at [#ly-dm:matrix.org](https://matrix.to/#/#ly-dm:matrix. ### Fedora -**Warning**: You may encounter issues with SELinux on Fedora. It is recommended to add a rule for Ly as it currently does not ship one. +> [!WARNING] +> You may encounter issues with SELinux on Fedora. It is recommended to add a rule for Ly as it currently does not ship one. ``` # dnf install kernel-devel pam-devel libxcb-devel zig xorg-x11-xauth xorg-x11-server brightnessctl @@ -58,7 +60,7 @@ Join us on Matrix over at [#ly-dm:matrix.org](https://matrix.to/#/#ly-dm:matrix. Every environment that works on other login managers also should work on Ly. -- Unlike most login managers Ly has xinitrc entry and it also supports shell. +- Unlike most login managers Ly has an xinitrc and shell entry. - If you installed your favorite environment and you don't see it, that's because Ly doesn't automatically refresh itself. To fix this you should restart Ly service (depends on your init system) or the easy way is to reboot your system. @@ -66,7 +68,7 @@ Every environment that works on other login managers also should work on Ly. - If there isn't a .desktop file then create a new one at `/etc/ly/custom-sessions` that launches your favorite environment. These .desktop files can be only seen by Ly and if you want them system-wide you also can create at those directories instead. -- If only Xorg sessions doesn't work then check if your distro compiles Ly with Xorg support as it can be compiled with Xorg support disabled. +- If Xorg sessions don't work then check if your distro compiles Ly with Xorg. Logs are defined by `/etc/ly/config.ini`: @@ -90,11 +92,13 @@ After building, you can (optionally) test Ly in a terminal emulator, although au $ zig build run ``` -**Important**: While you can also run Ly in a terminal emulator as root, it is **not** recommended either. If you want to properly test Ly, please enable its service (as described below) and reboot your machine. +> [!IMPORTANT] +> While you can run Ly in a terminal emulator as root, it is **not** recommended. If you want to test Ly, please enable its service (as described below) and reboot your machine. -The following sections show how to install Ly for a particular init system. Because the procedure is very similar for all of them, the commands will only be detailed for the first section (which is about systemd). +The next sections will explain how to use Ly with a variety of init systems. Detailed explanation is only given for systemd, but should be applicable for all. -**Note**: All following sections will assume you are using LightDM for convenience sake. +> [!NOTE] +> All following sections will assume you are using LightDM for convenience sake. ### systemd @@ -104,9 +108,10 @@ Now, you can install Ly on your system: # zig build installexe -Dinit_system=systemd ``` -**Note**: The `init_system` parameter is optional and defaults to `systemd`. +> [!NOTE] +> The `init_system` parameter is optional and defaults to `systemd`. -Note that you also need to disable your current display manager. For example, if LightDM is the current display manager, you can execute the following command: +Note that you also need to disable your current display manager. For example, if you are using LightDM, you can execute the following command: ``` # systemctl disable lightdm.service @@ -118,7 +123,8 @@ Then, similarly to the previous command, you need to enable the Ly service: # systemctl enable ly@tty2.service ``` -**Important**: Because Ly runs in a TTY, you **must** disable the TTY service that Ly will run on, otherwise bad things will happen. For example, to disable `getty` spawning on TTY 2, you need to execute the following command: +> [!IMPORTANT] +> Because Ly runs in a TTY, you **must** disable the TTY service that Ly will run on, otherwise bad things will happen. For example, to disable `getty` spawning on TTY 2, you need to execute the following command: ``` # systemctl disable getty@tty2.service @@ -141,7 +147,8 @@ On non-systemd systems, you can change the TTY Ly will run on by editing the cor # rc-update del agetty.tty2 ``` -**Note**: On Gentoo specifically, you also **must** comment out the appropriate line for the TTY in /etc/inittab. +> [!NOTE] +> On Gentoo specifically, you also **must** comment out the appropriate line for the TTY in /etc/inittab. ### runit From ac78ccc3982da0bd3b703581cdee7dc373e3fd7d Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Wed, 25 Mar 2026 20:06:59 +0100 Subject: [PATCH 084/176] Update Kawaii-Ash's GitHub username Signed-off-by: AnErrupTion --- readme.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/readme.md b/readme.md index 8ecec06..c85caa4 100644 --- a/readme.md +++ b/readme.md @@ -12,7 +12,6 @@ Join us on Matrix over at [#ly-dm:matrix.org](https://matrix.to/#/#ly-dm:matrix. ## Dependencies - Compile-time: - - zig 0.15.x - libc @@ -22,7 +21,6 @@ Join us on Matrix over at [#ly-dm:matrix.org](https://matrix.to/#/#ly-dm:matrix. - xcb (optional, required by default; needed for X11 support) - Runtime (with default config): - - xorg - xorg-xauth @@ -254,7 +252,7 @@ A typical shebang for a shell script looks like this: The name "Ly" is a tribute to the fairy from the game Rayman. Ly was tested by oxodao, who is some seriously awesome dude. -Also, Ly wouldn't be there today without [Kawaii-Ash](https://github.com/Kawaii-Ash), who has done significant contributions to the project for the Zig rewrite, which lead to the release of Ly v1.0.0. Massive thanks, and sorry for not crediting you enough beforehand! +Also, Ly wouldn't be there today without [ashametrine](https://github.com/ashametrine), who has done significant contributions to the project for the Zig rewrite, which lead to the release of Ly v1.0.0. Massive thanks, and sorry for not crediting you enough beforehand! ### Donate From 5e1c6813852ffc52e2a02450e7fe1bb0cba47a41 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Wed, 25 Mar 2026 20:49:53 +0100 Subject: [PATCH 085/176] Update screenshot (closes #948) Signed-off-by: AnErrupTion --- .github/screenshot.png | Bin 34754 -> 53096 bytes readme.md | 2 ++ 2 files changed, 2 insertions(+) diff --git a/.github/screenshot.png b/.github/screenshot.png index 748feccf62e1f157b825977c0d5a56a90eacec6b..396a251fb6d49c43842e392bb425ed9d21543941 100644 GIT binary patch literal 53096 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%0A71P4)*@~ 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 D+TLdG literal 34754 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 diff --git a/readme.md b/readme.md index c85caa4..79f5941 100644 --- a/readme.md +++ b/readme.md @@ -2,6 +2,8 @@ ![Ly screenshot](.github/screenshot.png "Ly screenshot") +_Note: the above animation can be found [here](https://codeberg.org/attachments/f336d6ac-8331-4323-91fc-0e4619803401)!_ + Ly is a lightweight TUI (ncurses-like) display manager for Linux and BSD, designed with portability in mind and doesn't require systemd to run. Join us on Matrix over at [#ly-dm:matrix.org](https://matrix.to/#/#ly-dm:matrix.org)! From 3758b5da1b53a2c6217aeb2fbde5019fe60ab30c Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Wed, 25 Mar 2026 21:10:09 +0100 Subject: [PATCH 086/176] Mention -quiet argument for X11 server (closes #722) Signed-off-by: AnErrupTion --- res/config.ini | 1 + 1 file changed, 1 insertion(+) diff --git a/res/config.ini b/res/config.ini index 715d679..0814165 100644 --- a/res/config.ini +++ b/res/config.ini @@ -361,6 +361,7 @@ vi_mode = false waylandsessions = $PREFIX_DIRECTORY/share/wayland-sessions # Xorg server command +# Add the -quiet argument to hide startup logs from the server x_cmd = $PREFIX_DIRECTORY/bin/X # Xorg virtual terminal number From 549576aa3e4872e592ed6ad21d206a80ca465770 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Wed, 25 Mar 2026 22:06:57 +0100 Subject: [PATCH 087/176] Make box widget not position-dependent Signed-off-by: AnErrupTion --- .../components/{CenteredBox.zig => Box.zig} | 38 +++++++++---------- ly-ui/src/root.zig | 2 +- src/main.zig | 12 +++--- 3 files changed, 25 insertions(+), 27 deletions(-) rename ly-ui/src/components/{CenteredBox.zig => Box.zig} (81%) diff --git a/ly-ui/src/components/CenteredBox.zig b/ly-ui/src/components/Box.zig similarity index 81% rename from ly-ui/src/components/CenteredBox.zig rename to ly-ui/src/components/Box.zig index f68709e..7c753ae 100644 --- a/ly-ui/src/components/CenteredBox.zig +++ b/ly-ui/src/components/Box.zig @@ -5,7 +5,7 @@ const Position = @import("../Position.zig"); const TerminalBuffer = @import("../TerminalBuffer.zig"); const Widget = @import("../Widget.zig"); -const CenteredBox = @This(); +const Box = @This(); instance: ?Widget = null, buffer: *TerminalBuffer, @@ -20,7 +20,7 @@ bottom_title: ?[]const u8, border_fg: u32, title_fg: u32, bg: u32, -update_fn: ?*const fn (*CenteredBox, *anyopaque) anyerror!void, +update_fn: ?*const fn (*Box, *anyopaque) anyerror!void, left_pos: Position, right_pos: Position, children_pos: Position, @@ -38,8 +38,8 @@ pub fn init( border_fg: u32, title_fg: u32, bg: u32, - update_fn: ?*const fn (*CenteredBox, *anyopaque) anyerror!void, -) CenteredBox { + update_fn: ?*const fn (*Box, *anyopaque) anyerror!void, +) Box { return .{ .instance = null, .buffer = buffer, @@ -61,10 +61,10 @@ pub fn init( }; } -pub fn widget(self: *CenteredBox) *Widget { +pub fn widget(self: *Box) *Widget { if (self.instance) |*instance| return instance; self.instance = Widget.init( - "CenteredBox", + "Box", null, self, null, @@ -77,30 +77,26 @@ pub fn widget(self: *CenteredBox) *Widget { return &self.instance.?; } -pub fn positionXY(self: *CenteredBox, original_pos: Position) void { +pub fn positionXY(self: *Box, original_pos: Position) void { if (self.buffer.width < 2 or self.buffer.height < 2) return; - self.left_pos = Position.init( - (self.buffer.width - @min(self.buffer.width - 2, self.width)) / 2, - (self.buffer.height - @min(self.buffer.height - 2, self.height)) / 2, - ).add(original_pos); - + self.left_pos = original_pos; self.right_pos = Position.init( - (self.buffer.width + @min(self.buffer.width, self.width)) / 2, - (self.buffer.height + @min(self.buffer.height, self.height)) / 2, - ).add(original_pos); + @min(self.buffer.width, self.width), + @min(self.buffer.height, self.height), + ).add(self.left_pos); self.children_pos = Position.init( - self.left_pos.x + self.horizontal_margin, - self.left_pos.y + self.vertical_margin, - ).add(original_pos); + self.horizontal_margin, + self.vertical_margin, + ).add(self.left_pos); } -pub fn childrenPosition(self: CenteredBox) Position { +pub fn childrenPosition(self: Box) Position { return self.children_pos; } -fn draw(self: *CenteredBox) void { +fn draw(self: *Box) void { if (self.show_borders) { var left_up = Cell.init( self.buffer.box_chars.left_up, @@ -183,7 +179,7 @@ fn draw(self: *CenteredBox) void { } } -fn update(self: *CenteredBox, ctx: *anyopaque) !void { +fn update(self: *Box, ctx: *anyopaque) !void { if (self.update_fn) |update_fn| { return @call( .auto, diff --git a/ly-ui/src/root.zig b/ly-ui/src/root.zig index 0baa857..dba4740 100644 --- a/ly-ui/src/root.zig +++ b/ly-ui/src/root.zig @@ -7,7 +7,7 @@ pub const TerminalBuffer = @import("TerminalBuffer.zig"); pub const Widget = @import("Widget.zig"); pub const BigLabel = @import("components/BigLabel.zig"); -pub const CenteredBox = @import("components/CenteredBox.zig"); +pub const Box = @import("components/Box.zig"); pub const CyclableLabel = @import("components/generic.zig").CyclableLabel; pub const Label = @import("components/Label.zig"); pub const Text = @import("components/Text.zig"); diff --git a/src/main.zig b/src/main.zig index fe3c748..db4df59 100644 --- a/src/main.zig +++ b/src/main.zig @@ -9,7 +9,7 @@ const clap = @import("clap"); const ly_ui = @import("ly-ui"); const Position = ly_ui.Position; const BigLabel = ly_ui.BigLabel; -const CenteredBox = ly_ui.CenteredBox; +const Box = ly_ui.Box; const Label = ly_ui.Label; const Text = ly_ui.Text; const TerminalBuffer = ly_ui.TerminalBuffer; @@ -87,7 +87,7 @@ const UiState = struct { password_label: Label, version_label: Label, bigclock_label: BigLabel, - box: CenteredBox, + box: Box, info_line: InfoLine, animate: bool, session: Session, @@ -524,7 +524,7 @@ pub fn main() !void { ); defer state.bigclock_label.deinit(); - state.box = CenteredBox.init( + state.box = Box.init( &state.buffer, state.config.margin_box_h, state.config.margin_box_v, @@ -1681,7 +1681,7 @@ fn calculateBigClockTimeout(_: *BigLabel, ptr: *anyopaque) !?usize { return @intCast((60 - @rem(time.seconds, 60)) * 1000 - @divTrunc(time.microseconds, 1000) + 1); } -fn updateBox(self: *CenteredBox, ptr: *anyopaque) !void { +fn updateBox(self: *Box, ptr: *anyopaque) !void { const state: *UiState = @ptrCast(@alignCast(ptr)); if (state.config.vi_mode) { @@ -1753,7 +1753,9 @@ fn positionWidgets(ptr: *anyopaque) !void { .childrenPosition() .removeX(TerminalBuffer.strWidth(state.lang.numlock) + TerminalBuffer.strWidth(state.lang.capslock) + 1)); - state.box.positionXY(TerminalBuffer.START_POSITION); + state.box.positionXY(TerminalBuffer.START_POSITION + .addX((state.buffer.width - @min(state.buffer.width - 2, state.box.width)) / 2) + .addY((state.buffer.height - @min(state.buffer.height - 2, state.box.height)) / 2)); if (state.config.bigclock != .none) { const half_width = state.buffer.width / 2; From a6fc5d67e8a78281de53c5e2a4d3f25c7e634633 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Wed, 25 Mar 2026 22:43:05 +0100 Subject: [PATCH 088/176] Use upstream zigini library Signed-off-by: AnErrupTion --- ly-core/build.zig.zon | 4 ++-- src/main.zig | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/ly-core/build.zig.zon b/ly-core/build.zig.zon index 98233c5..7dc470f 100644 --- a/ly-core/build.zig.zon +++ b/ly-core/build.zig.zon @@ -5,8 +5,8 @@ .minimum_zig_version = "0.15.0", .dependencies = .{ .zigini = .{ - .url = "git+https://github.com/AnErrupTion/zigini?ref=zig-0.15.0#9281f47702b57779e831d7618e158abb8eb4d4a2", - .hash = "zigini-0.3.3-36M0FRJJAADZVq5HPm-hYKMpFFTr0OgjbEYcK2ijKZ5n", + .url = "git+https://github.com/AshAmetrine/zigini?ref=master#831f6aff55703b7fa34b43c972d60eb3d5d6f4a4", + .hash = "zigini-0.3.2-BSkB7UVDAAAErcEC6s7zF9bKTe7CjdBgrV35K3DGHpbr", }, }, .paths = .{ diff --git a/src/main.zig b/src/main.zig index db4df59..a3e767f 100644 --- a/src/main.zig +++ b/src/main.zig @@ -1860,14 +1860,14 @@ fn crawl(session: *Session, lang: Lang, path: []const u8, display_server: Displa const entry_path = try std.fmt.allocPrint(session.label.allocator, "{s}/{s}", .{ path, item.name }); defer session.label.allocator.free(entry_path); var entry_ini = Ini(Entry).init(session.label.allocator); - _ = try entry_ini.readFileToStruct(entry_path, .{ + const data = try entry_ini.readFileToStruct(entry_path, .{ .fieldHandler = null, .comment_characters = "#", }); errdefer entry_ini.deinit(); const file_name = try session.label.allocator.dupe(u8, std.fs.path.stem(item.name)); - const entry = entry_ini.data.@"Desktop Entry"; + const entry = data.@"Desktop Entry"; var maybe_xdg_session_desktop: ?[]const u8 = null; var maybe_xdg_desktop_names: ?[]const u8 = null; From ed486c29d244cab62867fa7577cef47e62a16b17 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Thu, 26 Mar 2026 21:10:50 +0100 Subject: [PATCH 089/176] Add xauth file as X server argument Signed-off-by: AnErrupTion --- src/auth.zig | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/auth.zig b/src/auth.zig index a9cad0c..e74843f 100644 --- a/src/auth.zig +++ b/src/auth.zig @@ -404,7 +404,7 @@ fn mcookie() [Md5.digest_length * 2]u8 { return std.fmt.bytesToHex(&out, .lower); } -fn xauth(log_file: *LogFile, allocator: std.mem.Allocator, display_name: []u8, shell: [*:0]const u8, home: []const u8, xauth_buffer: []u8, options: AuthOptions) !void { +fn xauth(log_file: *LogFile, allocator: std.mem.Allocator, display_name: []u8, shell: [*:0]const u8, home: []const u8, xauth_buffer: []u8, options: AuthOptions) ![]const u8 { const xauthority = try createXauthFile(log_file, home, xauth_buffer); try interop.setEnvironmentVariable(allocator, "XAUTHORITY", xauthority, true); try interop.setEnvironmentVariable(allocator, "DISPLAY", display_name, true); @@ -427,6 +427,8 @@ fn xauth(log_file: *LogFile, allocator: std.mem.Allocator, display_name: []u8, s try log_file.file_writer.interface.print("xauth command failed with status {d}\n", .{status.status}); return error.XauthFailed; } + + return xauthority; } fn executeX11Cmd(log_file: *LogFile, allocator: std.mem.Allocator, shell: []const u8, home: []const u8, options: AuthOptions, desktop_cmd: []const u8, vt: []const u8) !void { @@ -442,14 +444,14 @@ fn executeX11Cmd(log_file: *LogFile, allocator: std.mem.Allocator, shell: []cons defer allocator.free(shell_z); try log_file.info("auth/x11", "creating xauth file", .{}); - try xauth(log_file, allocator, display_name, shell_z, home, &xauth_buffer, options); + const xauthority = try xauth(log_file, allocator, display_name, shell_z, home, &xauth_buffer, options); try log_file.info("auth/x11", "starting x server", .{}); const pid = try std.posix.fork(); if (pid == 0) { var cmd_buffer: [1024]u8 = undefined; - const cmd_str = std.fmt.bufPrintZ(&cmd_buffer, "{s} {s} {s}", .{ options.x_cmd, display_name, vt }) catch std.process.exit(1); - try log_file.info("auth/x11", "executing: {s} -c {s}", .{ shell, cmd_str }); + const cmd_str = std.fmt.bufPrintZ(&cmd_buffer, "{s} {s} {s} -auth {s}", .{ options.x_cmd, display_name, vt, xauthority }) catch std.process.exit(1); + try log_file.info("auth/x11", "executing: {s} -c {s} -auth {s}", .{ shell, cmd_str, xauthority }); const args = [_:null]?[*:0]const u8{ shell_z, "-c", cmd_str }; std.posix.execveZ(shell_z, &args, std.c.environ) catch {}; From 984ac596afc6f7d4a1fe695b7c902cd1a71414ec Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Thu, 26 Mar 2026 21:55:35 +0100 Subject: [PATCH 090/176] Group for loops in event loop Signed-off-by: AnErrupTion --- ly-ui/src/TerminalBuffer.zig | 26 +++++++++----------------- 1 file changed, 9 insertions(+), 17 deletions(-) diff --git a/ly-ui/src/TerminalBuffer.zig b/ly-ui/src/TerminalBuffer.zig index 034916c..4551d68 100644 --- a/ly-ui/src/TerminalBuffer.zig +++ b/ly-ui/src/TerminalBuffer.zig @@ -208,12 +208,10 @@ pub fn runEventLoop( var inactivity_time_start = try interop.getTimeOfDay(); while (self.run) { + var maybe_timeout: ?usize = null; + if (self.update) { - for (layers) |layer| { - for (layer) |widget| { - try widget.update(context); - } - } + try TerminalBuffer.clearScreen(false); // Reset cursor const current_widget = self.getActiveWidget(); @@ -226,26 +224,20 @@ pub fn runEventLoop( ); }; - try TerminalBuffer.clearScreen(false); - for (layers) |layer| { for (layer) |widget| { + try widget.update(context); widget.draw(); + + if (try widget.calculateTimeout(context)) |widget_timeout| { + if (maybe_timeout == null or widget_timeout < maybe_timeout.?) maybe_timeout = widget_timeout; + } } } TerminalBuffer.presentBuffer(); } - var maybe_timeout: ?usize = null; - for (layers) |layer| { - for (layer) |widget| { - if (try widget.calculateTimeout(context)) |widget_timeout| { - if (maybe_timeout == null or widget_timeout < maybe_timeout.?) maybe_timeout = widget_timeout; - } - } - } - if (inactivity_event_fn) |inactivity_fn| { const time = try interop.getTimeOfDay(); @@ -257,7 +249,7 @@ pub fn runEventLoop( const event_error = if (maybe_timeout) |timeout| termbox.tb_peek_event(&event, @intCast(timeout)) else termbox.tb_poll_event(&event); - self.update = maybe_timeout != null; + self.update = maybe_timeout != null or event_error >= 0; if (event_error < 0) continue; From 7a8d913531d2f3638268bc894766c51845bc411a Mon Sep 17 00:00:00 2001 From: RadsammyT Date: Fri, 27 Mar 2026 17:15:49 +0100 Subject: [PATCH 091/176] Feature: Add custom command & label support (#945) ## What are the changes about? Adds customizable commands and labels to ly. Solves https://codeberg.org/fairyglade/ly/issues/905. Since Ly doesn't use INI headers. I use them exclusively for declarations of custom commands and labels. ### Commands Bind a keybind to a command, and add a hint to the HUD. Useful for use cases like display brightness, switching between GPUs, etc. Supports localization in the `name` field only. ex: where `lang = es`: `$brightness_up` => `bajar brillo` Declared in config.ini with the following: ```ini [cmd:F8] name = custom command 2 cmd = touch /tmp/ly.gaming ``` ### Labels Add a label to the HUD. As specified in #905. The text of the label corresponds to the output of the command specified in `[lbl:NAME]`. Only shows the first line of the output. Declared in config.ini with the following: ```ini [lbl:kernel] cmd = uname -srn refresh = 0 ``` Example to add to the config.ini: ```ini # Declare a command with the F8 binding. [cmd:F8] #The name of the command to show up in Ly. name = custom command cmd = touch /tmp/ly.gaming # Declare a label with an ID. This ID should be unique across all labels. [lbl:kernel] cmd = uname -srn # In frames, the time to re-run the command and update the label. If 0, only run once- do not refresh. refresh = 0 # Once you're done setting up labels and commands, add an empty header # below to continue configurating the rest of Ly. # Put other settings not belonging to custom commands/labels below here. [] ``` ## Pre-requisites - [x] I have tested & confirmed the changes work locally ![image](/attachments/f9373ac9-567e-4f47-987c-1df6f4ee0d84) Reviewed-on: https://codeberg.org/fairyglade/ly/pulls/945 Reviewed-by: AnErrupTion Co-authored-by: RadsammyT Co-committed-by: RadsammyT --- ly-ui/src/Widget.zig | 5 +- res/config.ini | 37 +++++++++ res/lang/ar.ini | 3 + res/lang/bg.ini | 3 + res/lang/cat.ini | 3 + res/lang/cs.ini | 3 + res/lang/de.ini | 3 + res/lang/en.ini | 3 + res/lang/eo.ini | 4 + res/lang/es.ini | 3 + res/lang/fr.ini | 3 + res/lang/it.ini | 3 + res/lang/ja_JP.ini | 3 + res/lang/ku.ini | 3 + res/lang/lv.ini | 3 + res/lang/pl.ini | 3 + res/lang/pt.ini | 3 + res/lang/pt_BR.ini | 3 + res/lang/ro.ini | 3 + res/lang/ru.ini | 3 + res/lang/sr.ini | 3 + res/lang/sv.ini | 3 + res/lang/tr.ini | 3 + res/lang/uk.ini | 3 + res/lang/zh_CN.ini | 3 + src/config/Config.zig | 1 + src/config/Lang.zig | 3 + src/config/custom.zig | 25 ++++++ src/config/migrator.zig | 56 +++++++++++++ src/main.zig | 180 ++++++++++++++++++++++++++++++++++++++++ 30 files changed, 376 insertions(+), 1 deletion(-) create mode 100644 src/config/custom.zig diff --git a/ly-ui/src/Widget.zig b/ly-ui/src/Widget.zig index d66fce8..07f76c3 100644 --- a/ly-ui/src/Widget.zig +++ b/ly-ui/src/Widget.zig @@ -12,6 +12,8 @@ const VTable = struct { calculate_timeout_fn: ?*const fn (ptr: *anyopaque, ctx: *anyopaque) anyerror!?usize, }; +pub var idCounter: u64 = 0; + id: u64, display_name: []const u8, keybinds: ?TerminalBuffer.KeybindMap, @@ -101,8 +103,9 @@ pub fn init( }; }; + idCounter += 1; return .{ - .id = @intFromPtr(Impl.vtable.draw_fn), + .id = idCounter, .display_name = display_name, .keybinds = keybinds, .pointer = pointer, diff --git a/res/config.ini b/res/config.ini index 0814165..8bcbd29 100644 --- a/res/config.ini +++ b/res/config.ini @@ -143,6 +143,11 @@ colormix_col2 = 0x000000FF # Color mixing animation third color id colormix_col3 = 0x20000000 +# For custom binds: the horizontal limit in characters for each +# line of custom binds before moving on to the next. +# If null, defaults to the width of the terminal instead. +custom_bind_width = null + # Custom sessions directory # You can specify multiple directories, # e.g. $CONFIG_DIRECTORY/ly/custom-sessions:$PREFIX_DIRECTORY/share/custom-sessions @@ -380,3 +385,35 @@ xinitrc = ~/.xinitrc # You can specify multiple directories, # e.g. $PREFIX_DIRECTORY/share/xsessions:$PREFIX_DIRECTORY/local/share/xsessions xsessions = $PREFIX_DIRECTORY/share/xsessions + +# Custom Commands and Labels: +# The following examples below give an outline for setting up custom commands and labels. +# Unless specified as optional, an option is mandatory. + +# Comments preceding with '##' are for documentation. +# Comments preceding with '#' comment out the example INI. + +##--- +## Declare a command with the F8 binding. +#[cmd:F8] +## The name of the command to show up in Ly. +## Note: "$" in "$brightness_up" fetches the appropriate string from the specified locale file +## and is replaced with the value representing "brightness_up". +## You can see the list of keys in any locale file in $CONFIG_DIRECTORY/ly/lang. +#name = custom command $brightness_up +#cmd = touch /tmp/ly.gaming +# +## Declare a label with an ID. This ID should be unique across all labels. +#[lbl:kernel] +#cmd = uname -srn +## Optional, defaulting to 0. +## In frames, the time to re-run the command and update the label. +## If 0, only run once and do not refresh afterwards +#refresh = 0 +# +## Once you're done setting up labels and commands, add an empty header +## below to continue configurating the rest of Ly. +## Put other settings not belonging to custom commands/labels below here. +#[] +# +##--- diff --git a/res/lang/ar.ini b/res/lang/ar.ini index 14aaee6..d8cbecf 100644 --- a/res/lang/ar.ini +++ b/res/lang/ar.ini @@ -3,6 +3,9 @@ brightness_down = خفض السطوع brightness_up = رفع السطوع capslock = capslock + + + err_alloc = فشل في تخصيص الذاكرة diff --git a/res/lang/bg.ini b/res/lang/bg.ini index ee38f60..6795a4d 100644 --- a/res/lang/bg.ini +++ b/res/lang/bg.ini @@ -3,6 +3,9 @@ brightness_down = намаляване на яркостта brightness_up = увеличаване на яркостта capslock = caps lock custom = персонализирано + + + err_alloc = неуспешно заделяне на памет err_args = неуспешен анализ на аргументите от командния ред err_autologin_session = сесията за автоматично влизане не е намерена diff --git a/res/lang/cat.ini b/res/lang/cat.ini index 967c728..152cb96 100644 --- a/res/lang/cat.ini +++ b/res/lang/cat.ini @@ -3,6 +3,9 @@ brightness_down = abaixar brillantor brightness_up = apujar brillantor capslock = Bloq Majús + + + err_alloc = assignació de memòria fallida diff --git a/res/lang/cs.ini b/res/lang/cs.ini index 9e879e1..da0bce2 100644 --- a/res/lang/cs.ini +++ b/res/lang/cs.ini @@ -3,6 +3,9 @@ capslock = capslock + + + err_alloc = alokace paměti selhala diff --git a/res/lang/de.ini b/res/lang/de.ini index d4a5f30..f869dcb 100644 --- a/res/lang/de.ini +++ b/res/lang/de.ini @@ -3,6 +3,9 @@ brightness_down = Helligkeit- brightness_up = Helligkeit+ capslock = Feststelltaste + + + err_alloc = Speicherzuweisung fehlgeschlagen diff --git a/res/lang/en.ini b/res/lang/en.ini index 082b02f..b3d40f2 100644 --- a/res/lang/en.ini +++ b/res/lang/en.ini @@ -3,6 +3,9 @@ brightness_down = decrease brightness brightness_up = increase brightness capslock = capslock custom = custom +custom_info_err_output_long = output too long +custom_info_err_no_output = no output +custom_info_err_no_output_error = , possible error err_alloc = failed memory allocation err_args = unable to parse command line arguments err_autologin_session = autologin session not found diff --git a/res/lang/eo.ini b/res/lang/eo.ini index 9c75dc3..8463432 100644 --- a/res/lang/eo.ini +++ b/res/lang/eo.ini @@ -3,6 +3,9 @@ brightness_down = malpliigi helecon brightness_up = pliigi helecon capslock = majuskla baskulo custom = propra + + + err_alloc = malsukcesis memorasignon err_args = ne povas analizi argumentojn de komanda linio err_autologin_session = aŭtomatan ensalutan seancon ne trovis @@ -73,6 +76,7 @@ restart = restartigi shell = ŝelo shutdown = malŝalti sleep = memordormi + wayland = wayland x11 = x11 xinitrc = xinitrc diff --git a/res/lang/es.ini b/res/lang/es.ini index 9f04ecb..38c2b9d 100644 --- a/res/lang/es.ini +++ b/res/lang/es.ini @@ -3,6 +3,9 @@ brightness_down = bajar brillo brightness_up = subir brillo capslock = Bloq Mayús + + + err_alloc = asignación de memoria fallida diff --git a/res/lang/fr.ini b/res/lang/fr.ini index 886149b..df619d2 100644 --- a/res/lang/fr.ini +++ b/res/lang/fr.ini @@ -3,6 +3,9 @@ brightness_down = diminuer la luminosité brightness_up = augmenter la luminosité capslock = verr.maj custom = customisé + + + err_alloc = échec d'allocation mémoire err_args = échec de l'analyse des arguments en lignes de commande err_autologin_session = session de connexion automatique introuvable diff --git a/res/lang/it.ini b/res/lang/it.ini index 245c84e..e8af2a6 100644 --- a/res/lang/it.ini +++ b/res/lang/it.ini @@ -3,6 +3,9 @@ capslock = capslock + + + err_alloc = impossibile allocare memoria diff --git a/res/lang/ja_JP.ini b/res/lang/ja_JP.ini index 9c98b93..309ff39 100644 --- a/res/lang/ja_JP.ini +++ b/res/lang/ja_JP.ini @@ -3,6 +3,9 @@ brightness_down = 明るさを下げる brightness_up = 明るさを上げる capslock = CapsLock + + + err_alloc = メモリ割り当て失敗 diff --git a/res/lang/ku.ini b/res/lang/ku.ini index a47ef65..5775274 100644 --- a/res/lang/ku.ini +++ b/res/lang/ku.ini @@ -3,6 +3,9 @@ brightness_down = ronahiyê kêm bike brightness_up = ronahiyê bilind bike capslock = tîpên girdek (capslock) custom = kesane + + + err_alloc = veqetandina bîrê têk çû err_args = argumanên rêzika fermanê nehatin analîzkirin err_autologin_session = danişîna têketina xweber nehate dîtin diff --git a/res/lang/lv.ini b/res/lang/lv.ini index 6def7f8..a1a3fa9 100644 --- a/res/lang/lv.ini +++ b/res/lang/lv.ini @@ -3,6 +3,9 @@ brightness_down = samazināt spilgtumu brightness_up = palielināt spilgtumu capslock = caps lock custom = pielāgots + + + err_alloc = neizdevās atmiņas piešķiršana diff --git a/res/lang/pl.ini b/res/lang/pl.ini index 3adef78..4c521e4 100644 --- a/res/lang/pl.ini +++ b/res/lang/pl.ini @@ -3,6 +3,9 @@ brightness_down = zmniejsz jasność brightness_up = zwiększ jasność capslock = capslock custom = własny + + + err_alloc = nieudana alokacja pamięci err_autologin_session = nie znaleziono sesji autologowania diff --git a/res/lang/pt.ini b/res/lang/pt.ini index 608a122..0b13276 100644 --- a/res/lang/pt.ini +++ b/res/lang/pt.ini @@ -3,6 +3,9 @@ capslock = capslock + + + err_alloc = erro na atribuição de memória diff --git a/res/lang/pt_BR.ini b/res/lang/pt_BR.ini index fb5d58e..ca96a3e 100644 --- a/res/lang/pt_BR.ini +++ b/res/lang/pt_BR.ini @@ -3,6 +3,9 @@ capslock = caixa alta + + + err_alloc = alocação de memória malsucedida diff --git a/res/lang/ro.ini b/res/lang/ro.ini index 33c6e5d..0bf92f2 100644 --- a/res/lang/ro.ini +++ b/res/lang/ro.ini @@ -22,6 +22,9 @@ capslock = capslock + + + diff --git a/res/lang/ru.ini b/res/lang/ru.ini index baad2f2..23cec27 100644 --- a/res/lang/ru.ini +++ b/res/lang/ru.ini @@ -3,6 +3,9 @@ brightness_down = уменьшить яркость brightness_up = увеличить яркость capslock = capslock custom = пользовательский + + + err_alloc = не удалось выделить память err_autologin_session = не найдена сессия с автологином diff --git a/res/lang/sr.ini b/res/lang/sr.ini index e5dcd4b..d0ad85a 100644 --- a/res/lang/sr.ini +++ b/res/lang/sr.ini @@ -3,6 +3,9 @@ capslock = capslock + + + err_alloc = neuspijesna alokacija memorije diff --git a/res/lang/sv.ini b/res/lang/sv.ini index 2cb113c..adec801 100644 --- a/res/lang/sv.ini +++ b/res/lang/sv.ini @@ -3,6 +3,9 @@ brightness_down = minska ljusstyrka brightness_up = öka ljusstyrka capslock = capslock custom = anpassad + + + err_alloc = minnesallokering misslyckades err_args = tolkning av kommandoargument misslyckades err_autologin_session = autologin-session hittades inte diff --git a/res/lang/tr.ini b/res/lang/tr.ini index 807cf30..4ee5960 100644 --- a/res/lang/tr.ini +++ b/res/lang/tr.ini @@ -3,6 +3,9 @@ brightness_down = parlakligi azalt brightness_up = parlakligi arttir capslock = capslock + + + err_alloc = basarisiz bellek ayirma diff --git a/res/lang/uk.ini b/res/lang/uk.ini index fe37435..7b47f8a 100644 --- a/res/lang/uk.ini +++ b/res/lang/uk.ini @@ -3,6 +3,9 @@ capslock = capslock + + + err_alloc = невдале виділення пам'яті diff --git a/res/lang/zh_CN.ini b/res/lang/zh_CN.ini index ce1b23c..d2af6c3 100644 --- a/res/lang/zh_CN.ini +++ b/res/lang/zh_CN.ini @@ -3,6 +3,9 @@ capslock = 大写锁定 + + + err_alloc = 内存分配失败 diff --git a/src/config/Config.zig b/src/config/Config.zig index b3f1e53..ec6d60a 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -37,6 +37,7 @@ cmatrix_max_codepoint: u16 = 0x7B, colormix_col1: u32 = 0x00FF0000, colormix_col2: u32 = 0x000000FF, colormix_col3: u32 = 0x20000000, +custom_bind_width: ?u32 = null, custom_sessions: []const u8 = build_options.config_directory ++ "/ly/custom-sessions", default_input: Input = .login, doom_fire_height: u8 = 6, diff --git a/src/config/Lang.zig b/src/config/Lang.zig index e21700a..c01cff0 100644 --- a/src/config/Lang.zig +++ b/src/config/Lang.zig @@ -8,6 +8,9 @@ brightness_down: []const u8 = "decrease brightness", brightness_up: []const u8 = "increase brightness", capslock: []const u8 = "capslock", custom: []const u8 = "custom", +custom_info_err_output_long: []const u8 = "output too long", +custom_info_err_no_output: []const u8 = "no output", +custom_info_err_no_output_error: []const u8 = ", possible error", err_alloc: []const u8 = "failed memory allocation", err_args: []const u8 = "unable to parse command line arguments", err_autologin_session: []const u8 = "autologin session not found", diff --git a/src/config/custom.zig b/src/config/custom.zig new file mode 100644 index 0000000..057e063 --- /dev/null +++ b/src/config/custom.zig @@ -0,0 +1,25 @@ +const std = @import("std"); + +const custom = @This(); + +pub const CustomCommandBind = struct { + name: []const u8 = "", + cmd: []const u8 = "", +}; + +pub const UNDEFINED_CMD: []const u8 = "echo \"You forgot to define 'cmd'!\""; + +pub const CustomCommandInfo = struct { + name: []const u8 = "", + cmd: ?[]const u8 = null, + /// To be set to the label's widget ID + id: u64 = 0, + + /// In frames, the refresh rate for the `cmd` to run again + /// If 0, only run once. + refresh: u32 = 0, + counter: u32 = 0, +}; + +pub var binds: std.StringHashMap(CustomCommandBind) = undefined; +pub var labels: std.StringHashMap(CustomCommandInfo) = undefined; diff --git a/src/config/migrator.zig b/src/config/migrator.zig index cc32cf4..1c95d51 100644 --- a/src/config/migrator.zig +++ b/src/config/migrator.zig @@ -16,6 +16,7 @@ const ini = ly_core.ini; const Config = @import("Config.zig"); const OldSave = @import("OldSave.zig"); const SavedUsers = @import("SavedUsers.zig"); +const custom = @import("custom.zig"); const color_properties = [_][]const u8{ "bg", @@ -162,6 +163,61 @@ pub fn configFieldHandler(_: std.mem.Allocator, field: ini.IniField) ?ini.IniFie return mapped_field; } + // TODO: Dearest Melpert, + // I pray this message finds you well, as daylight dwindles and the witching hour + // approaches, I find it more and more imperative as time continues that I place + // this reminder here in such a format that you cannot ignore. + // Do you know how long I have been waiting for this petition to be authorized + // in regards to this particular segment of computerized instructions? + // It has been many a moon since this particular audit has been + // posted regarding the position of handling configurable literature + // apparatuses and plans for a new feature to the configuration + // interface and as time continues onwards I grow more restless + // on the progress of said interface, only to find out afterwards + // that you have PROCRASTINATED on the efforts meant to enhance + // configuration. Thus the requirement for this reminder larger + // compared to the two reminders regarding better methods of + // X termination detection and new usernames with existing + // save files. + // + // Thus is my que to leave this TODO at thy request, + // + // Forever Sullied, + // + // Ly Contributor. + // + if (std.mem.startsWith(u8, field.header, "cmd:")) { + const key = field.header["cmd:".len..]; + const keyZ = temporary_allocator.dupe(u8, key) catch ""; + if (!custom.binds.contains(key)) { + custom.binds.put(keyZ, .{}) catch {}; + } + if (custom.binds.getPtr(keyZ)) |command| { + if (std.mem.eql(u8, field.key, "name")) { + command.name = temporary_allocator.dupe(u8, field.value) catch ""; + } + if (std.mem.eql(u8, field.key, "cmd")) { + command.cmd = temporary_allocator.dupe(u8, field.value) catch ""; + } + } + } + + if (std.mem.startsWith(u8, field.header, "lbl:")) { + const key = field.header["lbl:".len..]; + const keyZ = temporary_allocator.dupe(u8, key) catch ""; + if (!custom.labels.contains(keyZ)) { + custom.labels.put(keyZ, .{ .name = keyZ }) catch {}; + } + if (custom.labels.getPtr(keyZ)) |label| { + if (std.mem.eql(u8, field.key, "cmd")) { + label.cmd = temporary_allocator.dupe(u8, field.value) catch ""; + } + if (std.mem.eql(u8, field.key, "refresh")) { + label.refresh = std.fmt.parseInt(u32, field.value, 10) catch 0; + } + } + } + return field; } diff --git a/src/main.zig b/src/main.zig index a3e767f..844a832 100644 --- a/src/main.zig +++ b/src/main.zig @@ -38,6 +38,7 @@ const Lang = @import("config/Lang.zig"); const migrator = @import("config/migrator.zig"); const OldSave = @import("config/OldSave.zig"); const SavedUsers = @import("config/SavedUsers.zig"); +const custom = @import("config/custom.zig"); const DisplayServer = @import("enums.zig").DisplayServer; const Environment = @import("Environment.zig"); const Entry = Environment.Entry; @@ -63,6 +64,17 @@ fn ttyControlTransferSignalHandler(_: c_int) callconv(.c) void { TerminalBuffer.shutdown(); } +const CustomBindLabel = struct { + cmd: custom.CustomCommandBind, + key: []const u8, + lbl: Label, +}; + +const CustomInfoLabel = struct { + info: custom.CustomCommandInfo, + lbl: Label, +}; + const UiState = struct { allocator: Allocator, auth_fails: u64, @@ -107,6 +119,8 @@ const UiState = struct { bigclock_format_buf: [16:0]u8, clock_buf: [64:0]u8, bigclock_buf: [32:0]u8, + custom_binds: std.ArrayList(CustomBindLabel), + custom_info: std.ArrayList(CustomInfoLabel), }; var shutdown = false; @@ -206,8 +220,26 @@ pub fn main() !void { const config_path = try std.fs.path.join(state.allocator, &[_][]const u8{ config_parent_path, "config.ini" }); defer state.allocator.free(config_path); + custom.binds = .init(state.allocator); + custom.labels = .init(state.allocator); var config_parser = try IniParser(Config).init(state.allocator, config_path, migrator.configFieldHandler); defer config_parser.deinit(); + defer if (!shutdown or !restart) { + var iter = custom.binds.iterator(); + while (iter.next()) |i| { + temporary_allocator.free(i.key_ptr.*); + temporary_allocator.free(i.value_ptr.*.cmd); + temporary_allocator.free(i.value_ptr.*.name); + } + custom.binds.deinit(); + var labelIter = custom.labels.iterator(); + while (labelIter.next()) |i| { + temporary_allocator.free(i.key_ptr.*); + if (i.value_ptr.cmd) |cmd| + temporary_allocator.free(cmd); + } + custom.labels.deinit(); + }; state.config = config_parser.structure; @@ -1043,6 +1075,56 @@ pub fn main() !void { var layer2: std.ArrayList(*Widget) = .empty; defer layer2.deinit(state.allocator); + state.custom_binds = .empty; + defer state.custom_binds.deinit(state.allocator); + + state.custom_info = .empty; + defer state.custom_info.deinit(state.allocator); + + var lblIter = custom.labels.iterator(); + // NOTE: Because widgets have a pointer to the underlying Label, we have to ensure + // that the ArrayList doesn't allocate more memory than what we ensured. Otherwise + // the pointer to the Label becomes invalid. + try state.custom_info.ensureTotalCapacity(state.allocator, @intCast(custom.labels.count())); + while (lblIter.next()) |i| { + try state.custom_info.append(state.allocator, .{ + .info = i.value_ptr.*, + .lbl = .init("", null, state.buffer.fg, state.buffer.bg, updateCustomInfo, null), + }); + var latest = &state.custom_info.items[state.custom_info.items.len - 1]; + latest.info.id = latest.lbl.widget().id; + latest.info.counter = 1; + } + defer for (state.custom_info.items) |*item| { + item.lbl.deinit(); + }; + + var iter = custom.binds.iterator(); + while (iter.next()) |i| { + var concat = try std.mem.concat(state.allocator, u8, &[_][]const u8{ i.key_ptr.*, " ", i.value_ptr.name }); + inline for (@typeInfo(Lang).@"struct".fields) |lang_key| { + const new = try std.mem.replaceOwned(u8, state.allocator, concat, "$" ++ lang_key.name, @field(state.lang, lang_key.name)); + state.allocator.free(concat); + concat = new; + } + try state.custom_binds.append(state.allocator, .{ + .lbl = .init( + concat, + null, + state.buffer.fg, + state.buffer.bg, + null, + null, + ), + .cmd = i.value_ptr.*, + .key = i.key_ptr.*, + }); + state.custom_binds.items[state.custom_binds.items.len - 1].lbl.allocator = state.allocator; + } + defer for (state.custom_binds.items) |*i| { + i.lbl.deinit(); + }; + if (!state.config.hide_key_hints) { try layer2.append(state.allocator, state.shutdown_label.widget()); try layer2.append(state.allocator, state.restart_label.widget()); @@ -1085,6 +1167,13 @@ pub fn main() !void { try layer2.append(state.allocator, state.version_label.widget()); } + for (state.custom_binds.items) |*item| { + try layer2.append(state.allocator, item.lbl.widget()); + } + for (state.custom_info.items) |*item| { + try layer2.append(state.allocator, item.lbl.widget()); + } + try widgets.append(state.allocator, layer2.items); // Layer 3 @@ -1093,6 +1182,10 @@ pub fn main() !void { try widgets.append(state.allocator, &layer3); } + for (state.custom_binds.items) |*item| { + try state.buffer.registerGlobalKeybind(item.key, &customCommand, item); + } + try state.buffer.registerGlobalKeybind("Esc", &disableInsertMode, &state); try state.buffer.registerGlobalKeybind("I", &enableInsertMode, &state); @@ -1253,6 +1346,16 @@ fn quit(ptr: *anyopaque) !bool { return false; } +fn customCommand(ptr: *anyopaque) !bool { + const lbl: *CustomBindLabel = @ptrCast(@alignCast(ptr)); + var proc = std.process.Child.init(&[_][]const u8{ "/bin/sh", "-c", lbl.cmd.cmd }, lbl.lbl.allocator.?); + proc.stdout_behavior = .Ignore; + proc.stderr_behavior = .Ignore; + const res = proc.spawnAndWait() catch return false; + if (res.Exited != 0) return error.CommandFailed; + return false; +} + fn authenticate(ptr: *anyopaque) !bool { var state: *UiState = @ptrCast(@alignCast(ptr)); @@ -1639,6 +1742,63 @@ fn updateClock(self: *Label, ptr: *anyopaque) !void { } } +fn updateCustomInfo(lbl: *Label, ptr: *anyopaque) !void { + const state: *UiState = @ptrCast(@alignCast(ptr)); + const wid = lbl.widget().id; + var stdout = std.ArrayList(u8).empty; + defer stdout.deinit(state.allocator); + + var stderr = std.ArrayList(u8).empty; + defer stderr.deinit(state.allocator); + for (state.custom_info.items) |*i| { + if (i.info.id != wid) continue; + // Here, a counter ticks down every time `updateCustomInfo` runs on that + // particular label. It will only run the command and update the label + // once it reaches to 1. If a refresh value is defined it's then reset to + // that refresh value. + if (i.info.counter == 1) { + var c = std.process.Child.init(&[_][]const u8{ "/bin/sh", "-c", i.info.cmd orelse custom.UNDEFINED_CMD }, state.allocator); + c.stderr_behavior = .Pipe; + c.stdout_behavior = .Pipe; + try c.spawn(); + + c.collectOutput(state.allocator, &stdout, &stderr, state.buffer.width) catch { + try stdout.print(state.allocator, "{s}: [{s}]", .{ i.info.name, state.lang.custom_info_err_output_long }); + }; + + const newlineIdx = std.mem.indexOfAny(u8, stdout.items, "\n"); + if (newlineIdx) |idx| { + stdout.shrinkAndFree(state.allocator, idx); + } + + if (stdout.items.len > state.buffer.width) { + stdout.clearRetainingCapacity(); + try stdout.print(state.allocator, "{s}: [{s}]", .{ i.info.name, state.lang.custom_info_err_output_long }); + } + + _ = try c.wait(); + + // Sometimes, the output of a command would have an unprintable character at + // the end of its output, causing '�' (U+FFFD) to appear in its place. Here, we check + // if this is the case and remove it. + if (stdout.items.len != 0 and !std.ascii.isPrint(stdout.items[stdout.items.len - 1])) { + _ = stdout.pop(); + } else if (stdout.items.len == 0) { + try stdout.print(state.allocator, "{s}: [{s}{s}]", .{ i.info.name, state.lang.custom_info_err_no_output, if (stderr.items.len > 0) state.lang.custom_info_err_no_output_error else "" }); + } + state.allocator.free(lbl.text); + try lbl.setTextAlloc(state.allocator, "{s}", .{stdout.items}); + + // Called to re-position the widgets after they receive their output. + try positionWidgets(state); + if (i.info.refresh != 0) + i.info.counter = i.info.refresh; + } + if (i.info.counter != 0) + i.info.counter -= 1; + } +} + fn calculateClockTimeout(_: *Label, _: *anyopaque) !?usize { const time = try interop.getTimeOfDay(); @@ -1732,6 +1892,26 @@ fn positionWidgets(ptr: *anyopaque) !void { state.brightness_up_label.positionXY(last_label .childrenPosition() .addX(1)); + var x_offset: usize = 0; + var y_offset: usize = 1; + for (state.custom_binds.items) |*item| { + item.lbl.positionXY(state.edge_margin + .addY(y_offset) + .addX(x_offset)); + x_offset += item.lbl.text.len + 1; + if (x_offset + item.lbl.text.len > state.config.custom_bind_width orelse state.buffer.width) { + x_offset = 0; + y_offset += 1; + } + } + } + for (state.custom_info.items, 0..) |*item, i| { + item.lbl.positionXY(state.edge_margin + .addY(@intCast(i)) + .invertX(state.buffer.width) + .removeX(item.lbl.text.len) + .invertY(state.buffer.height) + .removeY(1)); } state.battery_label.positionXY(state.edge_margin From 074bb0a68a7f36d7ba85f00b3c762d16e042159f Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Fri, 27 Mar 2026 20:34:39 +0100 Subject: [PATCH 092/176] Improve custom command sample config readability (closes #949) Signed-off-by: AnErrupTion --- res/config.ini | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/res/config.ini b/res/config.ini index 8bcbd29..e3d4f93 100644 --- a/res/config.ini +++ b/res/config.ini @@ -393,16 +393,15 @@ xsessions = $PREFIX_DIRECTORY/share/xsessions # Comments preceding with '##' are for documentation. # Comments preceding with '#' comment out the example INI. -##--- ## Declare a command with the F8 binding. #[cmd:F8] ## The name of the command to show up in Ly. ## Note: "$" in "$brightness_up" fetches the appropriate string from the specified locale file ## and is replaced with the value representing "brightness_up". ## You can see the list of keys in any locale file in $CONFIG_DIRECTORY/ly/lang. -#name = custom command $brightness_up #cmd = touch /tmp/ly.gaming -# +#name = custom command $brightness_up + ## Declare a label with an ID. This ID should be unique across all labels. #[lbl:kernel] #cmd = uname -srn @@ -410,10 +409,3 @@ xsessions = $PREFIX_DIRECTORY/share/xsessions ## In frames, the time to re-run the command and update the label. ## If 0, only run once and do not refresh afterwards #refresh = 0 -# -## Once you're done setting up labels and commands, add an empty header -## below to continue configurating the rest of Ly. -## Put other settings not belonging to custom commands/labels below here. -#[] -# -##--- From 5b7c7dfdf5a2720a2089c5d78016f8045b85db46 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Fri, 27 Mar 2026 21:34:32 +0100 Subject: [PATCH 093/176] migrator.zig: Run zig fmt Signed-off-by: AnErrupTion --- src/config/migrator.zig | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/config/migrator.zig b/src/config/migrator.zig index 1c95d51..53e4011 100644 --- a/src/config/migrator.zig +++ b/src/config/migrator.zig @@ -170,18 +170,18 @@ pub fn configFieldHandler(_: std.mem.Allocator, field: ini.IniField) ?ini.IniFie // Do you know how long I have been waiting for this petition to be authorized // in regards to this particular segment of computerized instructions? // It has been many a moon since this particular audit has been - // posted regarding the position of handling configurable literature + // posted regarding the position of handling configurable literature // apparatuses and plans for a new feature to the configuration // interface and as time continues onwards I grow more restless // on the progress of said interface, only to find out afterwards // that you have PROCRASTINATED on the efforts meant to enhance // configuration. Thus the requirement for this reminder larger // compared to the two reminders regarding better methods of - // X termination detection and new usernames with existing + // X termination detection and new usernames with existing // save files. // // Thus is my que to leave this TODO at thy request, - // + // // Forever Sullied, // // Ly Contributor. From fe6942d4061a08b97ee8622f0974dc0e53b17fb2 Mon Sep 17 00:00:00 2001 From: RadsammyT Date: Fri, 27 Mar 2026 22:46:37 +0100 Subject: [PATCH 094/176] fix: custom label and bind ordering (#951) ## What are the changes about? Fixes the order of custom labels and binds because of a HashMap shenanigan (no guaranteed order), so we use `ArrayHashMap` instead which preserves insertion order. They should now be shown in the order they are declared in the config. ![image](/attachments/7a928c5f-fbbe-4a60-b120-3feddbcdfdb6) ![image](/attachments/22afe011-00a0-4a29-90ab-060e1d059c75) ## What existing issue does this resolve? !950 ## Pre-requisites - [x] I have tested & confirmed the changes work locally - [x] I have run `zig fmt` throughout my changes Reviewed-on: https://codeberg.org/fairyglade/ly/pulls/951 Reviewed-by: AnErrupTion Co-authored-by: RadsammyT Co-committed-by: RadsammyT --- src/config/custom.zig | 4 ++-- src/main.zig | 36 ++++++++++++++++++------------------ 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/src/config/custom.zig b/src/config/custom.zig index 057e063..28cded0 100644 --- a/src/config/custom.zig +++ b/src/config/custom.zig @@ -21,5 +21,5 @@ pub const CustomCommandInfo = struct { counter: u32 = 0, }; -pub var binds: std.StringHashMap(CustomCommandBind) = undefined; -pub var labels: std.StringHashMap(CustomCommandInfo) = undefined; +pub var binds: std.StringArrayHashMap(CustomCommandBind) = undefined; +pub var labels: std.StringArrayHashMap(CustomCommandInfo) = undefined; diff --git a/src/main.zig b/src/main.zig index 844a832..076bb33 100644 --- a/src/main.zig +++ b/src/main.zig @@ -225,20 +225,20 @@ pub fn main() !void { var config_parser = try IniParser(Config).init(state.allocator, config_path, migrator.configFieldHandler); defer config_parser.deinit(); defer if (!shutdown or !restart) { - var iter = custom.binds.iterator(); - while (iter.next()) |i| { - temporary_allocator.free(i.key_ptr.*); - temporary_allocator.free(i.value_ptr.*.cmd); - temporary_allocator.free(i.value_ptr.*.name); - } - custom.binds.deinit(); - var labelIter = custom.labels.iterator(); - while (labelIter.next()) |i| { - temporary_allocator.free(i.key_ptr.*); - if (i.value_ptr.cmd) |cmd| - temporary_allocator.free(cmd); - } - custom.labels.deinit(); + var iter = custom.binds.iterator(); + while (iter.next()) |i| { + temporary_allocator.free(i.key_ptr.*); + temporary_allocator.free(i.value_ptr.*.cmd); + temporary_allocator.free(i.value_ptr.*.name); + } + custom.binds.deinit(); + var labelIter = custom.labels.iterator(); + while (labelIter.next()) |i| { + temporary_allocator.free(i.key_ptr.*); + if (i.value_ptr.cmd) |cmd| + temporary_allocator.free(cmd); + } + custom.labels.deinit(); }; state.config = config_parser.structure; @@ -1096,7 +1096,7 @@ pub fn main() !void { latest.info.counter = 1; } defer for (state.custom_info.items) |*item| { - item.lbl.deinit(); + item.lbl.deinit(); }; var iter = custom.binds.iterator(); @@ -1122,7 +1122,7 @@ pub fn main() !void { state.custom_binds.items[state.custom_binds.items.len - 1].lbl.allocator = state.allocator; } defer for (state.custom_binds.items) |*i| { - i.lbl.deinit(); + i.lbl.deinit(); }; if (!state.config.hide_key_hints) { @@ -1907,11 +1907,11 @@ fn positionWidgets(ptr: *anyopaque) !void { } for (state.custom_info.items, 0..) |*item, i| { item.lbl.positionXY(state.edge_margin - .addY(@intCast(i)) .invertX(state.buffer.width) .removeX(item.lbl.text.len) .invertY(state.buffer.height) - .removeY(1)); + .removeY(state.custom_info.items.len) + .addY(i)); } state.battery_label.positionXY(state.edge_margin From e882eea22a3f4483bfeb21c6830a17d13f266601 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Fri, 27 Mar 2026 23:18:16 +0100 Subject: [PATCH 095/176] Improve bug report template Notably, don't make the issue reproduction on a fresh install required. This'll likely filter out the honest people who have actually done it from the others who haven't. Signed-off-by: AnErrupTion --- .github/ISSUE_TEMPLATE/bug.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug.yml b/.github/ISSUE_TEMPLATE/bug.yml index 909ba6c..1a6d57a 100644 --- a/.github/ISSUE_TEMPLATE/bug.yml +++ b/.github/ISSUE_TEMPLATE/bug.yml @@ -12,8 +12,8 @@ body: - label: I have looked for any other duplicate issues required: true - label: I have reproduced the issue on a fresh install of my OS & Ly with default settings, except ones I will mention - required: true - - label: I have confirmed this issue also occurs on the latest development version + required: false + - label: I have confirmed this issue also occurs on the latest development version (found in the `master` branch) required: true - type: input id: version From fad683e03594afc9483483ac0d0dde9a435a72c0 Mon Sep 17 00:00:00 2001 From: RacerBG Date: Sat, 28 Mar 2026 12:07:10 +0100 Subject: [PATCH 096/176] Update the Bulgarian translation (#952) ## What are the changes about? As the title says. ## What existing issue does this resolve? N/A Reviewed-on: https://codeberg.org/fairyglade/ly/pulls/952 Reviewed-by: AnErrupTion Co-authored-by: RacerBG Co-committed-by: RacerBG --- res/lang/bg.ini | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/res/lang/bg.ini b/res/lang/bg.ini index 6795a4d..1ac9246 100644 --- a/res/lang/bg.ini +++ b/res/lang/bg.ini @@ -3,9 +3,9 @@ brightness_down = намаляване на яркостта brightness_up = увеличаване на яркостта capslock = caps lock custom = персонализирано - - - +custom_info_err_output_long = резултатът е твърде дълъг +custom_info_err_no_output = няма резултат +custom_info_err_no_output_error = , възможна грешка err_alloc = неуспешно заделяне на памет err_args = неуспешен анализ на аргументите от командния ред err_autologin_session = сесията за автоматично влизане не е намерена @@ -76,7 +76,7 @@ restart = рестартиране shell = обвивка shutdown = изключване sleep = заспиване - +toggle_password = превключване на паролата wayland = wayland x11 = x11 xinitrc = xinitrc From 142476041d31b751cdd38683f197426b9facc750 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Sat, 28 Mar 2026 12:09:41 +0100 Subject: [PATCH 097/176] Update French translation Signed-off-by: AnErrupTion --- res/lang/fr.ini | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/res/lang/fr.ini b/res/lang/fr.ini index df619d2..472115e 100644 --- a/res/lang/fr.ini +++ b/res/lang/fr.ini @@ -3,9 +3,9 @@ brightness_down = diminuer la luminosité brightness_up = augmenter la luminosité capslock = verr.maj custom = customisé - - - +custom_info_err_output_long = sortie trop longue +custom_info_err_no_output = pas de sortie +custom_info_err_no_output_error = , erreur possible err_alloc = échec d'allocation mémoire err_args = échec de l'analyse des arguments en lignes de commande err_autologin_session = session de connexion automatique introuvable @@ -76,7 +76,7 @@ restart = redémarrer shell = shell shutdown = éteindre sleep = veille - +toggle_password = afficher le mot de passe wayland = wayland x11 = x11 xinitrc = xinitrc From 10a873acb9fa0c22c2967ed43e57cb0c02f93851 Mon Sep 17 00:00:00 2001 From: Jackson Delahunt Date: Sun, 29 Mar 2026 08:32:27 +0200 Subject: [PATCH 098/176] config: allow waylandsessions and xsessions to be set to null (#954) `waylandsessions` and `xsessions` are currently non-optional string fields, so there is no clean way to disable session type discovery for users who do not use Wayland or X11. Setting them to a nonexistent path works but produces log errors on every startup. This change makes both fields optional (`?[]const u8`), consistent with other nullable config fields such as `xinitrc`. Setting either to `null` in `config.ini` cleanly skips crawling for that session type with no side effects. Co-authored-by: Jackson Delahunt Reviewed-on: https://codeberg.org/fairyglade/ly/pulls/954 Reviewed-by: AnErrupTion Co-authored-by: Jackson Delahunt Co-committed-by: Jackson Delahunt --- res/config.ini | 2 ++ src/config/Config.zig | 4 ++-- src/main.zig | 38 +++++++++++++++++++++----------------- 3 files changed, 25 insertions(+), 19 deletions(-) diff --git a/res/config.ini b/res/config.ini index e3d4f93..52ed290 100644 --- a/res/config.ini +++ b/res/config.ini @@ -363,6 +363,7 @@ vi_mode = false # Wayland desktop environments # You can specify multiple directories, # e.g. $PREFIX_DIRECTORY/share/wayland-sessions:$PREFIX_DIRECTORY/local/share/wayland-sessions +# If null, Wayland sessions will not be shown waylandsessions = $PREFIX_DIRECTORY/share/wayland-sessions # Xorg server command @@ -384,6 +385,7 @@ xinitrc = ~/.xinitrc # Xorg desktop environments # You can specify multiple directories, # e.g. $PREFIX_DIRECTORY/share/xsessions:$PREFIX_DIRECTORY/local/share/xsessions +# If null, X11 sessions will not be shown xsessions = $PREFIX_DIRECTORY/share/xsessions # Custom Commands and Labels: diff --git a/src/config/Config.zig b/src/config/Config.zig index ec6d60a..1bff3c0 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -92,9 +92,9 @@ start_cmd: ?[]const u8 = null, text_in_center: bool = false, vi_default_mode: ViMode = .normal, vi_mode: bool = false, -waylandsessions: []const u8 = build_options.prefix_directory ++ "/share/wayland-sessions", +waylandsessions: ?[]const u8 = build_options.prefix_directory ++ "/share/wayland-sessions", x_cmd: []const u8 = build_options.prefix_directory ++ "/bin/X", x_vt: ?u8 = null, xauth_cmd: []const u8 = build_options.prefix_directory ++ "/bin/xauth", xinitrc: ?[]const u8 = "~/.xinitrc", -xsessions: []const u8 = build_options.prefix_directory ++ "/share/xsessions", +xsessions: ?[]const u8 = build_options.prefix_directory ++ "/share/xsessions", diff --git a/src/main.zig b/src/main.zig index 076bb33..38da21f 100644 --- a/src/main.zig +++ b/src/main.zig @@ -769,32 +769,36 @@ pub fn main() !void { var has_crawl_error = false; // Crawl session directories (Wayland, X11 and custom respectively) - var wayland_session_dirs = std.mem.splitScalar(u8, state.config.waylandsessions, ':'); - while (wayland_session_dirs.next()) |dir| { - crawl(&state.session, state.lang, dir, .wayland) catch |err| { - has_crawl_error = true; - try state.log_file.err( - "sys", - "failed to crawl wayland session directory '{s}': {s}", - .{ dir, @errorName(err) }, - ); - }; - } - - if (build_options.enable_x11_support) { - var x_session_dirs = std.mem.splitScalar(u8, state.config.xsessions, ':'); - while (x_session_dirs.next()) |dir| { - crawl(&state.session, state.lang, dir, .x11) catch |err| { + if (state.config.waylandsessions) |waylandsessions| { + var wayland_session_dirs = std.mem.splitScalar(u8, waylandsessions, ':'); + while (wayland_session_dirs.next()) |dir| { + crawl(&state.session, state.lang, dir, .wayland) catch |err| { has_crawl_error = true; try state.log_file.err( "sys", - "failed to crawl x11 session directory '{s}': {s}", + "failed to crawl wayland session directory '{s}': {s}", .{ dir, @errorName(err) }, ); }; } } + if (build_options.enable_x11_support) { + if (state.config.xsessions) |xsessions| { + var x_session_dirs = std.mem.splitScalar(u8, xsessions, ':'); + while (x_session_dirs.next()) |dir| { + crawl(&state.session, state.lang, dir, .x11) catch |err| { + has_crawl_error = true; + try state.log_file.err( + "sys", + "failed to crawl x11 session directory '{s}': {s}", + .{ dir, @errorName(err) }, + ); + }; + } + } + } + var custom_session_dirs = std.mem.splitScalar(u8, state.config.custom_sessions, ':'); while (custom_session_dirs.next()) |dir| { crawl(&state.session, state.lang, dir, .custom) catch |err| { From b8048234d97fc48079d7a293fe811cd36c1840a4 Mon Sep 17 00:00:00 2001 From: Jackson Delahunt Date: Wed, 1 Apr 2026 18:51:37 +0200 Subject: [PATCH 099/176] config: add show_tty option to display active TTY in top right corner (#956) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When running multiple ly instances across different TTYs there is no way to tell which TTY a given login screen belongs to at a glance. This change adds a `show_tty` boolean config option (default `false`) that displays the active TTY number (e.g. `tty3`) in the top right corner. When the clock is also enabled the TTY label sits immediately to its right on the same row. When the clock is disabled it occupies the top right corner on its own. I'm open to advice from the maintainers on the placement of the TTY label — positioning it next to the clock is simply my personal preference and it doesn't need to stay there if a different position is more appropriate. Co-authored-by: Jackson Delahunt Reviewed-on: https://codeberg.org/fairyglade/ly/pulls/956 Reviewed-by: AnErrupTion Co-authored-by: Jackson Delahunt Co-committed-by: Jackson Delahunt --- res/config.ini | 5 +++++ src/config/Config.zig | 1 + src/main.zig | 28 +++++++++++++++++++++++++++- 3 files changed, 33 insertions(+), 1 deletion(-) diff --git a/res/config.ini b/res/config.ini index 52ed290..4970520 100644 --- a/res/config.ini +++ b/res/config.ini @@ -333,6 +333,11 @@ setup_cmd = $CONFIG_DIRECTORY/ly/setup.sh # Specifies the key combination used for showing the password show_password_key = F7 +# Display the active TTY number (e.g. tty3) to the right of the clock in the top right corner +# If the clock is disabled, the TTY label occupies the top right corner on its own +# If false, the TTY number will not be shown +show_tty = false + # Command executed when pressing shutdown_key shutdown_cmd = /sbin/shutdown $PLATFORM_SHUTDOWN_ARG now diff --git a/src/config/Config.zig b/src/config/Config.zig index 1bff3c0..c0a4350 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -84,6 +84,7 @@ service_name: [:0]const u8 = "ly", session_log: ?[]const u8 = "ly-session.log", setup_cmd: []const u8 = build_options.config_directory ++ "/ly/setup.sh", show_password_key: []const u8 = "F7", +show_tty: bool = false, shutdown_cmd: []const u8 = "/sbin/shutdown -a now", shutdown_key: []const u8 = "F1", sleep_cmd: ?[]const u8 = null, diff --git a/src/main.zig b/src/main.zig index 38da21f..08c5177 100644 --- a/src/main.zig +++ b/src/main.zig @@ -94,6 +94,7 @@ const UiState = struct { capslock_label: Label, battery_label: Label, clock_label: Label, + tty_label: Label, session_specifier_label: Label, login_label: Label, password_label: Label, @@ -118,6 +119,7 @@ const UiState = struct { battery_buf: [16:0]u8, bigclock_format_buf: [16:0]u8, clock_buf: [64:0]u8, + tty_buf: [8:0]u8, bigclock_buf: [32:0]u8, custom_binds: std.ArrayList(CustomBindLabel), custom_info: std.ArrayList(CustomInfoLabel), @@ -541,6 +543,16 @@ pub fn main() !void { ); defer state.clock_label.deinit(); + state.tty_label = Label.init( + "", + null, + state.buffer.fg, + state.buffer.bg, + null, + null, + ); + defer state.tty_label.deinit(); + state.bigclock_label = BigLabel.init( &state.buffer, "", @@ -945,6 +957,10 @@ pub fn main() !void { }; } + if (state.config.show_tty) { + try state.tty_label.setTextBuf(&state.tty_buf, "tty{d}", .{state.active_tty}); + } + // Initialize the animation, if any var animation: ?*Widget = null; switch (state.config.animation) { @@ -1152,6 +1168,9 @@ pub fn main() !void { if (state.config.clock != null) { try layer2.append(state.allocator, state.clock_label.widget()); } + if (state.config.show_tty) { + try layer2.append(state.allocator, state.tty_label.widget()); + } if (state.config.bigclock != .none) { try layer2.append(state.allocator, state.bigclock_label.widget()); } @@ -1922,10 +1941,17 @@ fn positionWidgets(ptr: *anyopaque) !void { .add(TerminalBuffer.START_POSITION) .addYFromIf(state.brightness_up_label.childrenPosition(), !state.config.hide_key_hints) .removeYFromIf(state.edge_margin, !state.config.hide_key_hints)); + + const tty_label_width = if (state.config.show_tty) TerminalBuffer.strWidth(state.tty_label.text) else 0; + const tty_label_gap = if (state.config.show_tty and state.config.clock != null) @as(usize, 1) else 0; + state.tty_label.positionXY(state.edge_margin + .add(TerminalBuffer.START_POSITION) + .invertX(state.buffer.width) + .removeXIf(tty_label_width, state.buffer.width > tty_label_width + state.edge_margin.x)); state.clock_label.positionXY(state.edge_margin .add(TerminalBuffer.START_POSITION) .invertX(state.buffer.width) - .removeXIf(TerminalBuffer.strWidth(state.clock_label.text), state.buffer.width > TerminalBuffer.strWidth(state.clock_label.text) + state.edge_margin.x)); + .removeXIf(TerminalBuffer.strWidth(state.clock_label.text) + tty_label_width + tty_label_gap, state.buffer.width > TerminalBuffer.strWidth(state.clock_label.text) + tty_label_width + tty_label_gap + state.edge_margin.x)); state.numlock_label.positionX(state.edge_margin .add(TerminalBuffer.START_POSITION) From eec83179b93d63b18cb253e1ad8c41d8a9b155d8 Mon Sep 17 00:00:00 2001 From: Jackson Delahunt Date: Wed, 1 Apr 2026 19:00:37 +0200 Subject: [PATCH 100/176] config: add shell option to hide the shell session (#955) The shell session is unconditionally added to the session list with no way to hide it. This is inconsistent with `xinitrc`, which is omitted from the list when set to `null`. This change adds a `shell` boolean config option (default `true`). Setting it to `false` hides the shell session from the list, following the same pattern as `xinitrc`. Co-authored-by: Jackson Delahunt Reviewed-on: https://codeberg.org/fairyglade/ly/pulls/955 Reviewed-by: AnErrupTion Co-authored-by: Jackson Delahunt Co-committed-by: Jackson Delahunt --- res/config.ini | 4 ++++ src/config/Config.zig | 1 + src/main.zig | 26 ++++++++++++++------------ 3 files changed, 19 insertions(+), 12 deletions(-) diff --git a/res/config.ini b/res/config.ini index 4970520..86f44b4 100644 --- a/res/config.ini +++ b/res/config.ini @@ -330,6 +330,10 @@ session_log = .local/state/ly-session.log # Setup command setup_cmd = $CONFIG_DIRECTORY/ly/setup.sh +# Show the shell session in the session list +# If false, the shell session will be hidden +shell = true + # Specifies the key combination used for showing the password show_password_key = F7 diff --git a/src/config/Config.zig b/src/config/Config.zig index c0a4350..2233cb4 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -83,6 +83,7 @@ save: bool = true, service_name: [:0]const u8 = "ly", session_log: ?[]const u8 = "ly-session.log", setup_cmd: []const u8 = build_options.config_directory ++ "/ly/setup.sh", +shell: bool = true, show_password_key: []const u8 = "F7", show_tty: bool = false, shutdown_cmd: []const u8 = "/sbin/shutdown -a now", diff --git a/src/main.zig b/src/main.zig index 08c5177..bd9294c 100644 --- a/src/main.zig +++ b/src/main.zig @@ -737,18 +737,20 @@ pub fn main() !void { try state.buffer.registerKeybind(&state.login.label.keybinds, "H", &viGoLeft, &state); try state.buffer.registerKeybind(&state.login.label.keybinds, "L", &viGoRight, &state); - addOtherEnvironment(&state.session, state.lang, .shell, null) catch |err| { - try state.info_line.addMessage( - state.lang.err_alloc, - state.config.error_bg, - state.config.error_fg, - ); - try state.log_file.err( - "sys", - "failed to add shell environment: {s}", - .{@errorName(err)}, - ); - }; + if (state.config.shell) { + addOtherEnvironment(&state.session, state.lang, .shell, null) catch |err| { + try state.info_line.addMessage( + state.lang.err_alloc, + state.config.error_bg, + state.config.error_fg, + ); + try state.log_file.err( + "sys", + "failed to add shell environment: {s}", + .{@errorName(err)}, + ); + }; + } if (build_options.enable_x11_support) { if (state.config.xinitrc) |xinitrc_cmd| { From 5edf5251f672088058383d3f82805e5f32459127 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Sat, 25 Apr 2026 17:37:34 +0200 Subject: [PATCH 101/176] Update to Zig 0.16.0 (#962) Signed-off-by: AnErrupTion ## What are the changes about? Ports the code base to Zig 0.16.0. ## What existing issue does this resolve? N/A ## Pre-requisites - [x] I have tested & confirmed the changes work locally - [x] I have run `zig fmt` throughout my changes Reviewed-on: https://codeberg.org/fairyglade/ly/pulls/962 --- .gitignore | 1 + build.zig | 271 ++++++++++---------- build.zig.zon | 12 +- create_vendor_tarball.sh | 2 + ly-core/build.zig | 41 +++ ly-core/build.zig.zon | 10 +- ly-core/src/LogFile.zig | 34 +-- ly-core/src/SharedError.zig | 14 +- ly-core/src/interop.zig | 123 ++++----- ly-core/src/root.zig | 3 +- ly-ui/build.zig | 28 ++- ly-ui/build.zig.zon | 10 +- ly-ui/src/TerminalBuffer.zig | 43 ++-- ly-ui/src/components/Text.zig | 11 +- ly-ui/src/components/generic.zig | 9 +- readme.md | 2 +- src/animations/Cascade.zig | 7 +- src/animations/ColorMix.zig | 2 +- src/animations/DurFile.zig | 31 +-- src/auth.zig | 237 ++++++++--------- src/components/InfoLine.zig | 2 + src/components/Session.zig | 2 + src/components/UserList.zig | 2 + src/config/custom.zig | 4 +- src/config/migrator.zig | 18 +- src/main.zig | 419 ++++++++++++++++++------------- 26 files changed, 751 insertions(+), 587 deletions(-) create mode 100755 create_vendor_tarball.sh diff --git a/.gitignore b/.gitignore index de08f4f..fa23ae8 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ zig-cache/ zig-out/ valgrind.log .zig-cache +zig-pkg diff --git a/build.zig b/build.zig index c53b905..ff5ce4a 100644 --- a/build.zig +++ b/build.zig @@ -12,7 +12,7 @@ const InitSystem = enum { freebsd, }; -const min_zig_string = "0.15.0"; +const min_zig_string = "0.16.0"; const current_zig = builtin.zig_version; // Implementing zig version detection through compile time @@ -67,8 +67,8 @@ pub fn build(b: *std.Build) !void { .root_source_file = b.path("src/main.zig"), .target = target, .optimize = optimize, + .link_libc = true, }), - // Here until the native backend matures in terms of performance .use_llvm = true, }); @@ -80,9 +80,8 @@ pub fn build(b: *std.Build) !void { const clap = b.dependency("clap", .{ .target = target, .optimize = optimize }); exe.root_module.addImport("clap", clap.module("clap")); - exe.linkSystemLibrary("pam"); - if (enable_x11_support) exe.linkSystemLibrary("xcb"); - exe.linkLibC(); + exe.root_module.linkSystemLibrary("pam", .{}); + if (enable_x11_support) exe.root_module.linkSystemLibrary("xcb", .{}); b.installArtifact(exe); @@ -113,6 +112,8 @@ pub fn build(b: *std.Build) !void { pub fn Installer(install_config: bool) type { return struct { pub fn make(step: *std.Build.Step, _: std.Build.Step.MakeOptions) !void { + var threaded: std.Io.Threaded = .init_single_threaded; + const io = threaded.io(); const allocator = step.owner.allocator; var patch_map = PatchMap.init(allocator); @@ -127,75 +128,75 @@ pub fn Installer(install_config: bool) type { // instead to shutdown the system. try patch_map.put("$PLATFORM_SHUTDOWN_ARG", if (init_system == .freebsd) "-p" else "-a"); - try install_ly(allocator, patch_map, install_config); - try install_service(allocator, patch_map); + try install_ly(allocator, io, patch_map, install_config); + try install_service(allocator, io, patch_map); } }; } -fn install_ly(allocator: std.mem.Allocator, patch_map: PatchMap, install_config: bool) !void { - const ly_config_directory = try std.fs.path.join(allocator, &[_][]const u8{ dest_directory, config_directory, "/ly" }); +fn install_ly(allocator: std.mem.Allocator, io: std.Io, patch_map: PatchMap, install_config: bool) !void { + const ly_config_directory = try std.Io.Dir.path.join(allocator, &[_][]const u8{ dest_directory, config_directory, "/ly" }); - std.fs.cwd().makePath(ly_config_directory) catch { + std.Io.Dir.cwd().createDirPath(io, ly_config_directory) catch { std.debug.print("warn: {s} already exists as a directory.\n", .{ly_config_directory}); }; - const ly_custom_sessions_directory = try std.fs.path.join(allocator, &[_][]const u8{ dest_directory, config_directory, "/ly/custom-sessions" }); + const ly_custom_sessions_directory = try std.Io.Dir.path.join(allocator, &[_][]const u8{ dest_directory, config_directory, "/ly/custom-sessions" }); - std.fs.cwd().makePath(ly_custom_sessions_directory) catch { + std.Io.Dir.cwd().createDirPath(io, ly_custom_sessions_directory) catch { std.debug.print("warn: {s} already exists as a directory.\n", .{ly_custom_sessions_directory}); }; - const ly_lang_path = try std.fs.path.join(allocator, &[_][]const u8{ dest_directory, config_directory, "/ly/lang" }); - std.fs.cwd().makePath(ly_lang_path) catch { + const ly_lang_path = try std.Io.Dir.path.join(allocator, &[_][]const u8{ dest_directory, config_directory, "/ly/lang" }); + std.Io.Dir.cwd().createDirPath(io, ly_lang_path) catch { std.debug.print("warn: {s} already exists as a directory.\n", .{ly_lang_path}); }; { - const exe_path = try std.fs.path.join(allocator, &[_][]const u8{ dest_directory, prefix_directory, "/bin" }); - std.fs.cwd().makePath(exe_path) catch { + const exe_path = try std.Io.Dir.path.join(allocator, &[_][]const u8{ dest_directory, prefix_directory, "/bin" }); + std.Io.Dir.cwd().createDirPath(io, exe_path) catch { if (!std.mem.eql(u8, dest_directory, "")) { std.debug.print("warn: {s} already exists as a directory.\n", .{exe_path}); } }; - var executable_dir = std.fs.cwd().openDir(exe_path, .{}) catch unreachable; - defer executable_dir.close(); + var executable_dir = std.Io.Dir.cwd().openDir(io, exe_path, .{}) catch unreachable; + defer executable_dir.close(io); - try installFile("zig-out/bin/ly", executable_dir, exe_path, executable_name, .{}); + try installFile(io, "zig-out/bin/ly", executable_dir, exe_path, executable_name, .{}); } { - var config_dir = std.fs.cwd().openDir(ly_config_directory, .{}) catch unreachable; - defer config_dir.close(); + var config_dir = std.Io.Dir.cwd().openDir(io, ly_config_directory, .{}) catch unreachable; + defer config_dir.close(io); if (install_config) { - const patched_config = try patchFile(allocator, "res/config.ini", patch_map); - try installText(patched_config, config_dir, ly_config_directory, "config.ini", .{}); + const patched_config = try patchFile(allocator, io, "res/config.ini", patch_map); + try installText(io, patched_config, config_dir, ly_config_directory, "config.ini", .{}); - try installFile("res/startup.sh", config_dir, ly_config_directory, "startup.sh", .{ .override_mode = 0o755 }); + try installFile(io, "res/startup.sh", config_dir, ly_config_directory, "startup.sh", .{ .permissions = .fromMode(0o755) }); } - const patched_example_config = try patchFile(allocator, "res/config.ini", patch_map); - try installText(patched_example_config, config_dir, ly_config_directory, "config.ini.example", .{}); + const patched_example_config = try patchFile(allocator, io, "res/config.ini", patch_map); + try installText(io, patched_example_config, config_dir, ly_config_directory, "config.ini.example", .{}); - const patched_setup = try patchFile(allocator, "res/setup.sh", patch_map); - try installText(patched_setup, config_dir, ly_config_directory, "setup.sh", .{ .mode = 0o755 }); + const patched_setup = try patchFile(allocator, io, "res/setup.sh", patch_map); + try installText(io, patched_setup, config_dir, ly_config_directory, "setup.sh", .{ .permissions = .fromMode(0o755) }); - try installFile("res/example.dur", config_dir, ly_config_directory, "example.dur", .{ .override_mode = 0o755 }); + try installFile(io, "res/example.dur", config_dir, ly_config_directory, "example.dur", .{ .permissions = .fromMode(0o755) }); } { - var custom_sessions_dir = std.fs.cwd().openDir(ly_custom_sessions_directory, .{}) catch unreachable; - defer custom_sessions_dir.close(); + var custom_sessions_dir = std.Io.Dir.cwd().openDir(io, ly_custom_sessions_directory, .{}) catch unreachable; + defer custom_sessions_dir.close(io); - const patched_readme = try patchFile(allocator, "res/custom-sessions/README", patch_map); - try installText(patched_readme, custom_sessions_dir, ly_custom_sessions_directory, "README", .{}); + const patched_readme = try patchFile(allocator, io, "res/custom-sessions/README", patch_map); + try installText(io, patched_readme, custom_sessions_dir, ly_custom_sessions_directory, "README", .{}); } { - var lang_dir = std.fs.cwd().openDir(ly_lang_path, .{}) catch unreachable; - defer lang_dir.close(); + var lang_dir = std.Io.Dir.cwd().openDir(io, ly_lang_path, .{}) catch unreachable; + defer lang_dir.close(io); const languages = [_][]const u8{ "ar.ini", @@ -221,66 +222,66 @@ fn install_ly(allocator: std.mem.Allocator, patch_map: PatchMap, install_config: }; inline for (languages) |language| { - try installFile("res/lang/" ++ language, lang_dir, ly_lang_path, language, .{}); + try installFile(io, "res/lang/" ++ language, lang_dir, ly_lang_path, language, .{}); } } { - const pam_path = try std.fs.path.join(allocator, &[_][]const u8{ dest_directory, config_directory, "/pam.d" }); - std.fs.cwd().makePath(pam_path) catch { + const pam_path = try std.Io.Dir.path.join(allocator, &[_][]const u8{ dest_directory, config_directory, "/pam.d" }); + std.Io.Dir.cwd().createDirPath(io, pam_path) catch { if (!std.mem.eql(u8, dest_directory, "")) { std.debug.print("warn: {s} already exists as a directory.\n", .{pam_path}); } }; - var pam_dir = std.fs.cwd().openDir(pam_path, .{}) catch unreachable; - defer pam_dir.close(); + var pam_dir = std.Io.Dir.cwd().openDir(io, pam_path, .{}) catch unreachable; + defer pam_dir.close(io); - try installFile(if (init_system == .freebsd) "res/pam.d/ly-freebsd" else "res/pam.d/ly-linux", pam_dir, pam_path, "ly", .{ .override_mode = 0o644 }); - try installFile(if (init_system == .freebsd) "res/pam.d/ly-freebsd-autologin" else "res/pam.d/ly-linux-autologin", pam_dir, pam_path, "ly-autologin", .{ .override_mode = 0o644 }); + try installFile(io, if (init_system == .freebsd) "res/pam.d/ly-freebsd" else "res/pam.d/ly-linux", pam_dir, pam_path, "ly", .{ .permissions = .fromMode(0o644) }); + try installFile(io, if (init_system == .freebsd) "res/pam.d/ly-freebsd-autologin" else "res/pam.d/ly-linux-autologin", pam_dir, pam_path, "ly-autologin", .{ .permissions = .fromMode(0o644) }); } } -fn install_service(allocator: std.mem.Allocator, patch_map: PatchMap) !void { +fn install_service(allocator: std.mem.Allocator, io: std.Io, patch_map: PatchMap) !void { switch (init_system) { .systemd => { - const service_path = try std.fs.path.join(allocator, &[_][]const u8{ dest_directory, prefix_directory, "/lib/systemd/system" }); - std.fs.cwd().makePath(service_path) catch {}; - var service_dir = std.fs.cwd().openDir(service_path, .{}) catch unreachable; - defer service_dir.close(); + const service_path = try std.Io.Dir.path.join(allocator, &[_][]const u8{ dest_directory, prefix_directory, "/lib/systemd/system" }); + std.Io.Dir.cwd().createDirPath(io, service_path) catch {}; + var service_dir = std.Io.Dir.cwd().openDir(io, service_path, .{}) catch unreachable; + defer service_dir.close(io); - const patched_service = try patchFile(allocator, "res/ly@.service", patch_map); - try installText(patched_service, service_dir, service_path, "ly@.service", .{ .mode = 0o644 }); + const patched_service = try patchFile(allocator, io, "res/ly@.service", patch_map); + try installText(io, patched_service, service_dir, service_path, "ly@.service", .{ .permissions = .fromMode(0o644) }); - const patched_kmsconvt_service = try patchFile(allocator, "res/ly-kmsconvt@.service", patch_map); - try installText(patched_kmsconvt_service, service_dir, service_path, "ly-kmsconvt@.service", .{ .mode = 0o644 }); + const patched_kmsconvt_service = try patchFile(allocator, io, "res/ly-kmsconvt@.service", patch_map); + try installText(io, patched_kmsconvt_service, service_dir, service_path, "ly-kmsconvt@.service", .{ .permissions = .fromMode(0o644) }); }, .openrc => { - const service_path = try std.fs.path.join(allocator, &[_][]const u8{ dest_directory, config_directory, "/init.d" }); - std.fs.cwd().makePath(service_path) catch {}; - var service_dir = std.fs.cwd().openDir(service_path, .{}) catch unreachable; - defer service_dir.close(); + const service_path = try std.Io.Dir.path.join(allocator, &[_][]const u8{ dest_directory, config_directory, "/init.d" }); + std.Io.Dir.cwd().createDirPath(io, service_path) catch {}; + var service_dir = std.Io.Dir.cwd().openDir(io, service_path, .{}) catch unreachable; + defer service_dir.close(io); - const patched_service = try patchFile(allocator, "res/ly-openrc", patch_map); - try installText(patched_service, service_dir, service_path, executable_name, .{ .mode = 0o755 }); + const patched_service = try patchFile(allocator, io, "res/ly-openrc", patch_map); + try installText(io, patched_service, service_dir, service_path, executable_name, .{ .permissions = .fromMode(0o755) }); }, .runit => { - const service_path = try std.fs.path.join(allocator, &[_][]const u8{ dest_directory, config_directory, "/sv/ly" }); - std.fs.cwd().makePath(service_path) catch {}; - var service_dir = std.fs.cwd().openDir(service_path, .{}) catch unreachable; - defer service_dir.close(); + const service_path = try std.Io.Dir.path.join(allocator, &[_][]const u8{ dest_directory, config_directory, "/sv/ly" }); + std.Io.Dir.cwd().createDirPath(io, service_path) catch {}; + var service_dir = std.Io.Dir.cwd().openDir(io, service_path, .{}) catch unreachable; + defer service_dir.close(io); - const supervise_path = try std.fs.path.join(allocator, &[_][]const u8{ service_path, "supervise" }); + const supervise_path = try std.Io.Dir.path.join(allocator, &[_][]const u8{ service_path, "supervise" }); - const patched_conf = try patchFile(allocator, "res/ly-runit-service/conf", patch_map); - try installText(patched_conf, service_dir, service_path, "conf", .{}); + const patched_conf = try patchFile(allocator, io, "res/ly-runit-service/conf", patch_map); + try installText(io, patched_conf, service_dir, service_path, "conf", .{}); - try installFile("res/ly-runit-service/finish", service_dir, service_path, "finish", .{ .override_mode = 0o755 }); + try installFile(io, "res/ly-runit-service/finish", service_dir, service_path, "finish", .{ .permissions = .fromMode(0o755) }); - const patched_run = try patchFile(allocator, "res/ly-runit-service/run", patch_map); - try installText(patched_run, service_dir, service_path, "run", .{ .mode = 0o755 }); + const patched_run = try patchFile(allocator, io, "res/ly-runit-service/run", patch_map); + try installText(io, patched_run, service_dir, service_path, "run", .{ .permissions = .fromMode(0o755) }); - std.fs.cwd().symLink("/run/runit/supervise.ly", supervise_path, .{}) catch |err| { + std.Io.Dir.cwd().symLink(io, "/run/runit/supervise.ly", supervise_path, .{}) catch |err| { if (err == error.PathAlreadyExists) { std.debug.print("warn: /run/runit/supervise.ly already exists as a symbolic link.\n", .{}); } else { @@ -290,49 +291,49 @@ fn install_service(allocator: std.mem.Allocator, patch_map: PatchMap) !void { std.debug.print("info: installed symlink /run/runit/supervise.ly\n", .{}); }, .s6 => { - const admin_service_path = try std.fs.path.join(allocator, &[_][]const u8{ dest_directory, config_directory, "/s6/adminsv/default/contents.d" }); - std.fs.cwd().makePath(admin_service_path) catch {}; - var admin_service_dir = std.fs.cwd().openDir(admin_service_path, .{}) catch unreachable; - defer admin_service_dir.close(); + const admin_service_path = try std.Io.Dir.path.join(allocator, &[_][]const u8{ dest_directory, config_directory, "/s6/adminsv/default/contents.d" }); + std.Io.Dir.cwd().createDirPath(io, admin_service_path) catch {}; + var admin_service_dir = std.Io.Dir.cwd().openDir(io, admin_service_path, .{}) catch unreachable; + defer admin_service_dir.close(io); - const file = try admin_service_dir.createFile("ly-srv", .{}); - file.close(); + const file = try admin_service_dir.createFile(io, "ly-srv", .{}); + file.close(io); - const service_path = try std.fs.path.join(allocator, &[_][]const u8{ dest_directory, config_directory, "/s6/sv/ly-srv" }); - std.fs.cwd().makePath(service_path) catch {}; - var service_dir = std.fs.cwd().openDir(service_path, .{}) catch unreachable; - defer service_dir.close(); + const service_path = try std.Io.Dir.path.join(allocator, &[_][]const u8{ dest_directory, config_directory, "/s6/sv/ly-srv" }); + std.Io.Dir.cwd().createDirPath(io, service_path) catch {}; + var service_dir = std.Io.Dir.cwd().openDir(io, service_path, .{}) catch unreachable; + defer service_dir.close(io); - const patched_run = try patchFile(allocator, "res/ly-s6/run", patch_map); - try installText(patched_run, service_dir, service_path, "run", .{ .mode = 0o755 }); + const patched_run = try patchFile(allocator, io, "res/ly-s6/run", patch_map); + try installText(io, patched_run, service_dir, service_path, "run", .{ .permissions = .fromMode(0o755) }); - try installFile("res/ly-s6/type", service_dir, service_path, "type", .{}); + try installFile(io, "res/ly-s6/type", service_dir, service_path, "type", .{}); }, .dinit => { - const service_path = try std.fs.path.join(allocator, &[_][]const u8{ dest_directory, config_directory, "/dinit.d" }); - std.fs.cwd().makePath(service_path) catch {}; - var service_dir = std.fs.cwd().openDir(service_path, .{}) catch unreachable; - defer service_dir.close(); + const service_path = try std.Io.Dir.path.join(allocator, &[_][]const u8{ dest_directory, config_directory, "/dinit.d" }); + std.Io.Dir.cwd().createDirPath(io, service_path) catch {}; + var service_dir = std.Io.Dir.cwd().openDir(io, service_path, .{}) catch unreachable; + defer service_dir.close(io); - const patched_service = try patchFile(allocator, "res/ly-dinit", patch_map); - try installText(patched_service, service_dir, service_path, "ly", .{}); + const patched_service = try patchFile(allocator, io, "res/ly-dinit", patch_map); + try installText(io, patched_service, service_dir, service_path, "ly", .{}); }, .sysvinit => { - const service_path = try std.fs.path.join(allocator, &[_][]const u8{ dest_directory, config_directory, "/init.d" }); - std.fs.cwd().makePath(service_path) catch {}; - var service_dir = std.fs.cwd().openDir(service_path, .{}) catch unreachable; - defer service_dir.close(); + const service_path = try std.Io.Dir.path.join(allocator, &[_][]const u8{ dest_directory, config_directory, "/init.d" }); + std.Io.Dir.cwd().createDirPath(io, service_path) catch {}; + var service_dir = std.Io.Dir.cwd().openDir(io, service_path, .{}) catch unreachable; + defer service_dir.close(io); - const patched_service = try patchFile(allocator, "res/ly-sysvinit", patch_map); - try installText(patched_service, service_dir, service_path, "ly", .{ .mode = 0o755 }); + const patched_service = try patchFile(allocator, io, "res/ly-sysvinit", patch_map); + try installText(io, patched_service, service_dir, service_path, "ly", .{ .permissions = .fromMode(0o755) }); }, .freebsd => { - const exe_path = try std.fs.path.join(allocator, &[_][]const u8{ dest_directory, prefix_directory, "/bin" }); - var executable_dir = std.fs.cwd().openDir(exe_path, .{}) catch unreachable; - defer executable_dir.close(); + const exe_path = try std.Io.Dir.path.join(allocator, &[_][]const u8{ dest_directory, prefix_directory, "/bin" }); + var executable_dir = std.Io.Dir.cwd().openDir(io, exe_path, .{}) catch unreachable; + defer executable_dir.close(io); - const patched_wrapper = try patchFile(allocator, "res/ly-freebsd-wrapper", patch_map); - try installText(patched_wrapper, executable_dir, exe_path, "ly_wrapper", .{ .mode = 0o755 }); + const patched_wrapper = try patchFile(allocator, io, "res/ly-freebsd-wrapper", patch_map); + try installText(io, patched_wrapper, executable_dir, exe_path, "ly_wrapper", .{ .permissions = .fromMode(0o755) }); }, } } @@ -340,33 +341,35 @@ fn install_service(allocator: std.mem.Allocator, patch_map: PatchMap) !void { pub fn Uninstaller(uninstall_config: bool) type { return struct { pub fn make(step: *std.Build.Step, _: std.Build.Step.MakeOptions) !void { + var threaded: std.Io.Threaded = .init_single_threaded; + const io = threaded.io(); const allocator = step.owner.allocator; if (uninstall_config) { - try deleteTree(allocator, config_directory, "/ly", "ly config directory not found"); + try deleteTree(allocator, io, config_directory, "/ly", "ly config directory not found"); } - const exe_path = try std.fs.path.join(allocator, &[_][]const u8{ prefix_directory, "/bin/", executable_name }); + const exe_path = try std.Io.Dir.path.join(allocator, &[_][]const u8{ prefix_directory, "/bin/", executable_name }); var success = true; - std.fs.cwd().deleteFile(exe_path) catch { + std.Io.Dir.cwd().deleteFile(io, exe_path) catch { std.debug.print("warn: ly executable not found\n", .{}); success = false; }; if (success) std.debug.print("info: deleted {s}\n", .{exe_path}); - try deleteFile(allocator, config_directory, "/pam.d/ly", "ly pam file not found"); + try deleteFile(allocator, io, config_directory, "/pam.d/ly", "ly pam file not found"); switch (init_system) { - .systemd => try deleteFile(allocator, prefix_directory, "/lib/systemd/system/ly@.service", "systemd service not found"), - .openrc => try deleteFile(allocator, config_directory, "/init.d/ly", "openrc service not found"), - .runit => try deleteTree(allocator, config_directory, "/sv/ly", "runit service not found"), + .systemd => try deleteFile(allocator, io, prefix_directory, "/lib/systemd/system/ly@.service", "systemd service not found"), + .openrc => try deleteFile(allocator, io, config_directory, "/init.d/ly", "openrc service not found"), + .runit => try deleteTree(allocator, io, config_directory, "/sv/ly", "runit service not found"), .s6 => { - try deleteTree(allocator, config_directory, "/s6/sv/ly-srv", "s6 service not found"); - try deleteFile(allocator, config_directory, "/s6/adminsv/default/contents.d/ly-srv", "s6 admin service not found"); + try deleteTree(allocator, io, config_directory, "/s6/sv/ly-srv", "s6 service not found"); + try deleteFile(allocator, io, config_directory, "/s6/adminsv/default/contents.d/ly-srv", "s6 admin service not found"); }, - .dinit => try deleteFile(allocator, config_directory, "/dinit.d/ly", "dinit service not found"), - .sysvinit => try deleteFile(allocator, config_directory, "/init.d/ly", "sysvinit service not found"), - .freebsd => try deleteFile(allocator, prefix_directory, "/bin/ly_wrapper", "freebsd wrapper not found"), + .dinit => try deleteFile(allocator, io, config_directory, "/dinit.d/ly", "dinit service not found"), + .sysvinit => try deleteFile(allocator, io, config_directory, "/init.d/ly", "sysvinit service not found"), + .freebsd => try deleteFile(allocator, io, prefix_directory, "/bin/ly_wrapper", "freebsd wrapper not found"), } } }; @@ -384,11 +387,11 @@ fn getVersionStr(b: *std.Build, name: []const u8, version: std.SemanticVersion) "--match", "*.*.*", "--tags", - }, &status, .Ignore) catch { + }, &status, .ignore) catch { return version_str; }; var git_describe = std.mem.trim(u8, git_describe_raw, " \n\r"); - git_describe = std.mem.trimLeft(u8, git_describe, "v"); + git_describe = std.mem.trimStart(u8, git_describe, "v"); switch (std.mem.count(u8, git_describe, "-")) { 0 => { @@ -401,7 +404,7 @@ fn getVersionStr(b: *std.Build, name: []const u8, version: std.SemanticVersion) 2 => { // Untagged development build (e.g. 0.10.0-dev.2025+ecf0050a9). var it = std.mem.splitScalar(u8, git_describe, '-'); - const tagged_ancestor = std.mem.trimLeft(u8, it.first(), "v"); + const tagged_ancestor = std.mem.trimStart(u8, it.first(), "v"); const commit_height = it.next().?; const commit_id = it.next().?; @@ -428,24 +431,25 @@ fn getVersionStr(b: *std.Build, name: []const u8, version: std.SemanticVersion) } fn installFile( + io: std.Io, source_file: []const u8, - destination_directory: std.fs.Dir, + destination_directory: std.Io.Dir, destination_directory_path: []const u8, destination_file: []const u8, - options: std.fs.Dir.CopyFileOptions, + options: std.Io.Dir.CopyFileOptions, ) !void { - try std.fs.cwd().copyFile(source_file, destination_directory, destination_file, options); + try std.Io.Dir.cwd().copyFile(source_file, destination_directory, destination_file, io, options); std.debug.print("info: installed {s}/{s}\n", .{ destination_directory_path, destination_file }); } -fn patchFile(allocator: std.mem.Allocator, source_file: []const u8, patch_map: PatchMap) ![]const u8 { - var file = try std.fs.cwd().openFile(source_file, .{}); - defer file.close(); +fn patchFile(allocator: std.mem.Allocator, io: std.Io, source_file: []const u8, patch_map: PatchMap) ![]const u8 { + var file = try std.Io.Dir.cwd().openFile(io, source_file, .{}); + defer file.close(io); - const stat = try file.stat(); + const stat = try file.stat(io); var buffer: [4096]u8 = undefined; - var reader = file.reader(&buffer); + var reader = file.reader(io, &buffer); var text = try reader.interface.readAlloc(allocator, stat.size); var iterator = patch_map.iterator(); @@ -459,17 +463,18 @@ fn patchFile(allocator: std.mem.Allocator, source_file: []const u8, patch_map: P } fn installText( + io: std.Io, text: []const u8, - destination_directory: std.fs.Dir, + destination_directory: std.Io.Dir, destination_directory_path: []const u8, destination_file: []const u8, - options: std.fs.File.CreateFlags, + options: std.Io.File.CreateFlags, ) !void { - var file = try destination_directory.createFile(destination_file, options); - defer file.close(); + var file = try destination_directory.createFile(io, destination_file, options); + defer file.close(io); var buffer: [1024]u8 = undefined; - var writer = file.writer(&buffer); + var writer = file.writer(io, &buffer); try writer.interface.writeAll(text); try writer.interface.flush(); @@ -478,13 +483,14 @@ fn installText( fn deleteFile( allocator: std.mem.Allocator, + io: std.Io, prefix: []const u8, file: []const u8, warning: []const u8, ) !void { - const path = try std.fs.path.join(allocator, &[_][]const u8{ dest_directory, prefix, file }); + const path = try std.Io.Dir.path.join(allocator, &[_][]const u8{ dest_directory, prefix, file }); - std.fs.cwd().deleteFile(path) catch |err| { + std.Io.Dir.cwd().deleteFile(io, path) catch |err| { if (err == error.FileNotFound) { std.debug.print("warn: {s}\n", .{warning}); return; @@ -498,13 +504,14 @@ fn deleteFile( fn deleteTree( allocator: std.mem.Allocator, + io: std.Io, prefix: []const u8, directory: []const u8, warning: []const u8, ) !void { - const path = try std.fs.path.join(allocator, &[_][]const u8{ dest_directory, prefix, directory }); + const path = try std.Io.Dir.path.join(allocator, &[_][]const u8{ dest_directory, prefix, directory }); - var dir = std.fs.cwd().openDir(path, .{}) catch |err| { + var dir = std.Io.Dir.cwd().openDir(io, path, .{}) catch |err| { if (err == error.FileNotFound) { std.debug.print("warn: {s}\n", .{warning}); return; @@ -512,9 +519,9 @@ fn deleteTree( return err; }; - dir.close(); + dir.close(io); - try std.fs.cwd().deleteTree(path); + try std.Io.Dir.cwd().deleteTree(io, path); std.debug.print("info: deleted {s}\n", .{path}); } diff --git a/build.zig.zon b/build.zig.zon index 5471106..63e8c32 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -2,15 +2,19 @@ .name = .ly, .version = "1.4.0", .fingerprint = 0xa148ffcc5dc2cb59, - .minimum_zig_version = "0.15.0", + .minimum_zig_version = "0.16.0", .dependencies = .{ .ly_ui = .{ .path = "ly-ui", }, .clap = .{ - .url = "git+https://github.com/Hejsil/zig-clap#5289e0753cd274d65344bef1c114284c633536ea", - .hash = "clap-0.11.0-oBajB-HnAQDPCKYzwF7rO3qDFwRcD39Q0DALlTSz5H7e", + .url = "git+https://github.com/Hejsil/zig-clap#fc1e5cc3f6d9d3001112385ee6256d694e959d2f", + .hash = "clap-0.11.0-oBajB7foAQC3Iyn4IVCkUdYaOVVng5IZkSncySTjNig1", }, }, - .paths = .{""}, + .paths = .{ + "build.zig", + "build.zig.zon", + "src", + }, } diff --git a/create_vendor_tarball.sh b/create_vendor_tarball.sh new file mode 100755 index 0000000..b42f58c --- /dev/null +++ b/create_vendor_tarball.sh @@ -0,0 +1,2 @@ +#!/bin/sh +tar --zstd -cvf vendor.tar.zst zig-pkg ly-ui/zig-pkg ly-core/zig-pkg diff --git a/ly-core/build.zig b/ly-core/build.zig index 0671528..614404e 100644 --- a/ly-core/build.zig +++ b/ly-core/build.zig @@ -1,4 +1,5 @@ const std = @import("std"); +const Translator = @import("translate_c").Translator; pub fn build(b: *std.Build) void { const target = b.standardTargetOptions(.{}); @@ -12,6 +13,29 @@ pub fn build(b: *std.Build) void { const zigini = b.dependency("zigini", .{ .target = target, .optimize = optimize }); mod.addImport("zigini", zigini.module("zigini")); + const translate_c = b.dependency("translate_c", .{ + .target = target, + .optimize = optimize, + }); + + addCImport(b, mod, translate_c, target, optimize, "pam", "#include "); + addCImport(b, mod, translate_c, target, optimize, "utmp", "#include "); + addCImport(b, mod, translate_c, target, optimize, "xcb", "#include "); + if (target.result.os.tag == .freebsd) { + addCImport(b, mod, translate_c, target, optimize, "pwd", + \\#include + \\#include + \\#include + ); + } else { + addCImport(b, mod, translate_c, target, optimize, "pwd", "#include "); + } + addCImport(b, mod, translate_c, target, optimize, "stdlib", "#include "); + addCImport(b, mod, translate_c, target, optimize, "unistd", "#include "); + addCImport(b, mod, translate_c, target, optimize, "grp", "#include "); + addCImport(b, mod, translate_c, target, optimize, "system_time", "#include "); + addCImport(b, mod, translate_c, target, optimize, "time", "#include "); + const mod_tests = b.addTest(.{ .root_module = mod, }); @@ -20,3 +44,20 @@ pub fn build(b: *std.Build) void { const test_step = b.step("test", "Run tests"); test_step.dependOn(&run_mod_tests.step); } + +fn addCImport( + b: *std.Build, + mod: *std.Build.Module, + translate_c: *std.Build.Dependency, + target: std.Build.ResolvedTarget, + optimize: std.builtin.OptimizeMode, + comptime name: []const u8, + comptime bytes: []const u8, +) void { + const pam: Translator = .init(translate_c, .{ + .c_source_file = b.addWriteFiles().add(name ++ ".h", bytes), + .target = target, + .optimize = optimize, + }); + mod.addImport(name, pam.mod); +} diff --git a/ly-core/build.zig.zon b/ly-core/build.zig.zon index 7dc470f..a389405 100644 --- a/ly-core/build.zig.zon +++ b/ly-core/build.zig.zon @@ -2,11 +2,15 @@ .name = .ly_core, .version = "1.0.0", .fingerprint = 0xddda7afda795472, - .minimum_zig_version = "0.15.0", + .minimum_zig_version = "0.16.0", .dependencies = .{ .zigini = .{ - .url = "git+https://github.com/AshAmetrine/zigini?ref=master#831f6aff55703b7fa34b43c972d60eb3d5d6f4a4", - .hash = "zigini-0.3.2-BSkB7UVDAAAErcEC6s7zF9bKTe7CjdBgrV35K3DGHpbr", + .url = "git+https://github.com/AshAmetrine/zigini?ref=master#a665d081dda42664a96da2840ea09c5ccf9d0692", + .hash = "zigini-0.5.0-BSkB7e9WAACfyCBABNZiWL3gFMw18GKn3qBcPs8L1Ec1", + }, + .translate_c = .{ + .url = "git+https://codeberg.org/ziglang/translate-c#7a1a9fdc4ab00835748a6657ecbb835e3d5d45f7", + .hash = "translate_c-0.0.0-Q_BUWvP1BgCjAk6PWv5286tOlvzD9-X-NkuTzh0KxY0Q", }, }, .paths = .{ diff --git a/ly-core/src/LogFile.zig b/ly-core/src/LogFile.zig index 5a2ef8f..f60b153 100644 --- a/ly-core/src/LogFile.zig +++ b/ly-core/src/LogFile.zig @@ -5,27 +5,27 @@ const LogFile = @This(); path: []const u8, could_open_log_file: bool = undefined, -file: std.fs.File = undefined, +file: std.Io.File = undefined, buffer: []u8, -file_writer: std.fs.File.Writer = undefined, +file_writer: std.Io.File.Writer = undefined, -pub fn init(path: []const u8, buffer: []u8) !LogFile { +pub fn init(io: std.Io, path: []const u8, buffer: []u8) !LogFile { var log_file = LogFile{ .path = path, .buffer = buffer }; - log_file.could_open_log_file = try openLogFile(path, &log_file); + log_file.could_open_log_file = try openLogFile(io, path, &log_file); return log_file; } -pub fn reinit(self: *LogFile) !void { - self.could_open_log_file = try openLogFile(self.path, self); +pub fn reinit(self: *LogFile, io: std.Io) !void { + self.could_open_log_file = try openLogFile(io, self.path, self); } -pub fn deinit(self: *LogFile) void { - self.file.close(); +pub fn deinit(self: *LogFile, io: std.Io) void { + self.file.close(io); } -pub fn info(self: *LogFile, category: []const u8, comptime message: []const u8, args: anytype) !void { +pub fn info(self: *LogFile, io: std.Io, category: []const u8, comptime message: []const u8, args: anytype) !void { var buffer: [128:0]u8 = undefined; - const time = interop.timeAsString(&buffer, "%Y-%m-%d %H:%M:%S"); + const time = interop.timeAsString(io, &buffer, "%Y-%m-%d %H:%M:%S"); try self.file_writer.interface.print("{s} [info/{s}] ", .{ time, category }); try self.file_writer.interface.print(message, args); @@ -33,9 +33,9 @@ pub fn info(self: *LogFile, category: []const u8, comptime message: []const u8, try self.file_writer.interface.flush(); } -pub fn err(self: *LogFile, category: []const u8, comptime message: []const u8, args: anytype) !void { +pub fn err(self: *LogFile, io: std.Io, category: []const u8, comptime message: []const u8, args: anytype) !void { var buffer: [128:0]u8 = undefined; - const time = interop.timeAsString(&buffer, "%Y-%m-%d %H:%M:%S"); + const time = interop.timeAsString(io, &buffer, "%Y-%m-%d %H:%M:%S"); try self.file_writer.interface.print("{s} [err/{s}] ", .{ time, category }); try self.file_writer.interface.print(message, args); @@ -43,10 +43,10 @@ pub fn err(self: *LogFile, category: []const u8, comptime message: []const u8, a try self.file_writer.interface.flush(); } -fn openLogFile(path: []const u8, log_file: *LogFile) !bool { +fn openLogFile(io: std.Io, path: []const u8, log_file: *LogFile) !bool { var could_open_log_file = true; open_log_file: { - log_file.file = std.fs.cwd().openFile(path, .{ .mode = .write_only }) catch std.fs.cwd().createFile(path, .{ .mode = 0o666 }) catch { + log_file.file = std.Io.Dir.cwd().openFile(io, path, .{ .mode = .write_only }) catch std.Io.Dir.cwd().createFile(io, path, .{ .permissions = .fromMode(0o666) }) catch { // If we could neither open an existing log file nor create a new // one, abort. could_open_log_file = false; @@ -55,14 +55,14 @@ fn openLogFile(path: []const u8, log_file: *LogFile) !bool { } if (!could_open_log_file) { - log_file.file = try std.fs.openFileAbsolute("/dev/null", .{ .mode = .write_only }); + log_file.file = try std.Io.Dir.openFileAbsolute(io, "/dev/null", .{ .mode = .write_only }); } - var log_file_writer = log_file.file.writer(log_file.buffer); + var log_file_writer = log_file.file.writer(io, log_file.buffer); // Seek to the end of the log file if (could_open_log_file) { - const stat = try log_file.file.stat(); + const stat = try log_file.file.stat(io); try log_file_writer.seekTo(stat.size); } diff --git a/ly-core/src/SharedError.zig b/ly-core/src/SharedError.zig index ba3656e..c3ce088 100644 --- a/ly-core/src/SharedError.zig +++ b/ly-core/src/SharedError.zig @@ -1,10 +1,12 @@ const std = @import("std"); const ErrInt = std.meta.Int(.unsigned, @bitSizeOf(anyerror)); +const PaddingInt = std.meta.Int(.unsigned, 8 - (@bitSizeOf(ErrInt) + @bitSizeOf(bool)) % 8); const ErrorHandler = packed struct { has_error: bool = false, err_int: ErrInt = 0, + padding: PaddingInt = 0, }; const SharedError = @This(); @@ -17,7 +19,7 @@ pub fn init( write_error_event_fn: ?*const fn (anyerror, *anyopaque) anyerror!void, ctx: ?*anyopaque, ) !SharedError { - const data = try std.posix.mmap(null, @sizeOf(ErrorHandler), std.posix.PROT.READ | std.posix.PROT.WRITE, .{ .TYPE = .SHARED, .ANONYMOUS = true }, -1, 0); + const data = try std.posix.mmap(null, @sizeOf(ErrorHandler), .{ .READ = true, .WRITE = true }, .{ .TYPE = .SHARED, .ANONYMOUS = true }, -1, 0); return .{ .data = data, @@ -31,9 +33,8 @@ pub fn deinit(self: *SharedError) void { } pub fn writeError(self: SharedError, err: anyerror) void { - var buf_stream = std.io.fixedBufferStream(self.data); - const writer = buf_stream.writer(); - writer.writeStruct(ErrorHandler{ .has_error = true, .err_int = @intFromError(err) }) catch {}; + var writer: std.Io.Writer = .fixed(self.data); + writer.writeStruct(ErrorHandler{ .has_error = true, .err_int = @intFromError(err) }, .native) catch {}; if (self.write_error_event_fn) |write_error_event_fn| { @call(.auto, write_error_event_fn, .{ err, self.ctx.? }) catch {}; @@ -41,9 +42,8 @@ pub fn writeError(self: SharedError, err: anyerror) void { } pub fn readError(self: SharedError) ?anyerror { - var buf_stream = std.io.fixedBufferStream(self.data); - const reader = buf_stream.reader(); - const err_handler = try reader.readStruct(ErrorHandler); + var reader: std.Io.Reader = .fixed(self.data); + const err_handler = try reader.takeStruct(ErrorHandler, .native); if (err_handler.has_error) return @errorFromInt(err_handler.err_int); diff --git a/ly-core/src/interop.zig b/ly-core/src/interop.zig index ef542f7..c580a49 100644 --- a/ly-core/src/interop.zig +++ b/ly-core/src/interop.zig @@ -1,49 +1,17 @@ const std = @import("std"); const builtin = @import("builtin"); const UidRange = @import("UidRange.zig"); +const pwd = @import("pwd"); +const stdlib = @import("stdlib"); +const unistd = @import("unistd"); +const grp = @import("grp"); +const system_time = @import("system_time"); +const time = @import("time"); -pub const pam = @cImport({ - @cInclude("security/pam_appl.h"); -}); - -pub const utmp = @cImport({ - @cInclude("utmpx.h"); -}); - +pub const pam = @import("pam"); +pub const utmp = @import("utmp"); // Exists for X11 support only -pub const xcb = @cImport({ - @cInclude("xcb/xcb.h"); -}); - -const pwd = @cImport({ - @cInclude("pwd.h"); - // We include a FreeBSD-specific header here since login_cap.h references - // the passwd struct directly, so we can't import it separately - if (builtin.os.tag == .freebsd) { - @cInclude("sys/types.h"); - @cInclude("login_cap.h"); - } -}); - -const stdlib = @cImport({ - @cInclude("stdlib.h"); -}); - -const unistd = @cImport({ - @cInclude("unistd.h"); -}); - -const grp = @cImport({ - @cInclude("grp.h"); -}); - -const system_time = @cImport({ - @cInclude("sys/time.h"); -}); - -const time = @cImport({ - @cInclude("time.h"); -}); +pub const xcb = @import("xcb"); pub const TimeOfDay = struct { seconds: i64, @@ -83,8 +51,8 @@ fn PlatformStruct() type { const status = grp.initgroups(username, @intCast(entry.gid)); if (status != 0) return error.GroupInitializationFailed; - std.posix.setgid(@intCast(entry.gid)) catch return error.SetUserGidFailed; - std.posix.setuid(@intCast(entry.uid)) catch return error.SetUserUidFailed; + if (isError(std.posix.system.setgid(@intCast(entry.gid)))) return error.SetUserGidFailed; + if (isError(std.posix.system.setuid(@intCast(entry.uid)))) return error.SetUserUidFailed; } // Procedure: @@ -96,14 +64,14 @@ fn PlatformStruct() type { // 4. Finally, compare the major and minor device numbers with the // extracted values. If they correspond, parse [dir] to get the // TTY ID - pub fn getActiveTtyImpl(allocator: std.mem.Allocator, use_kmscon_vt: bool) !u8 { + pub fn getActiveTtyImpl(allocator: std.mem.Allocator, io: std.Io, use_kmscon_vt: bool) !u8 { var file_buffer: [256]u8 = undefined; if (use_kmscon_vt) { - var file = try std.fs.openFileAbsolute("/sys/class/tty/tty0/active", .{}); - defer file.close(); + var file = try std.Io.Dir.openFileAbsolute(io, "/sys/class/tty/tty0/active", .{}); + defer file.close(io); - var reader = file.reader(&file_buffer); + var reader = file.reader(io, &file_buffer); var buffer: [16]u8 = undefined; const read = try readBuffer(&reader.interface, &buffer); @@ -115,10 +83,10 @@ fn PlatformStruct() type { var tty_minor: u16 = undefined; { - var file = try std.fs.openFileAbsolute("/proc/self/stat", .{}); - defer file.close(); + var file = try std.Io.Dir.openFileAbsolute(io, "/proc/self/stat", .{}); + defer file.close(io); - var reader = file.reader(&file_buffer); + var reader = file.reader(io, &file_buffer); var buffer: [1024]u8 = undefined; const read = try readBuffer(&reader.interface, &buffer); @@ -136,18 +104,18 @@ fn PlatformStruct() type { tty_minor = tty_nr % 256; } - var directory = try std.fs.openDirAbsolute("/sys/class/tty", .{ .iterate = true }); - defer directory.close(); + var directory = try std.Io.Dir.openDirAbsolute(io, "/sys/class/tty", .{ .iterate = true }); + defer directory.close(io); var iterator = directory.iterate(); - while (try iterator.next()) |entry| { + while (try iterator.next(io)) |entry| { const path = try std.fmt.allocPrint(allocator, "/sys/class/tty/{s}/dev", .{entry.name}); defer allocator.free(path); - var file = try std.fs.openFileAbsolute(path, .{}); - defer file.close(); + var file = try std.Io.Dir.openFileAbsolute(io, path, .{}); + defer file.close(io); - var reader = file.reader(&file_buffer); + var reader = file.reader(io, &file_buffer); var buffer: [16]u8 = undefined; const read = try readBuffer(&reader.interface, &buffer); @@ -170,11 +138,14 @@ fn PlatformStruct() type { // This is very bad parsing, but we only need to get 2 values.. // and the format of the file seems to be standard? So this should // be fine... - pub fn getUserIdRange(allocator: std.mem.Allocator, file_path: []const u8) !UidRange { - const login_defs_file = try std.fs.cwd().openFile(file_path, .{}); - defer login_defs_file.close(); + pub fn getUserIdRange(allocator: std.mem.Allocator, io: std.Io, file_path: []const u8) !UidRange { + const login_defs_file = try std.Io.Dir.cwd().openFile(io, file_path, .{}); + defer login_defs_file.close(io); - const login_defs_buffer = try login_defs_file.readToEndAlloc(allocator, std.math.maxInt(u16)); + var buffer: [4096]u8 = undefined; + var reader = login_defs_file.reader(io, &buffer); + + const login_defs_buffer = try reader.interface.allocRemaining(allocator, .unlimited); defer allocator.free(login_defs_buffer); var iterator = std.mem.splitScalar(u8, login_defs_buffer, '\n'); @@ -255,11 +226,11 @@ fn PlatformStruct() type { if (result != 0) return error.SetUserUidFailed; } - pub fn getActiveTtyImpl(_: std.mem.Allocator, _: bool) !u8 { + pub fn getActiveTtyImpl(_: std.mem.Allocator, _: std.Io, _: bool) !u8 { return error.FeatureUnimplemented; } - pub fn getUserIdRange(_: std.mem.Allocator, _: []const u8) !UidRange { + pub fn getUserIdRange(_: std.mem.Allocator, _: std.Io, _: []const u8) !UidRange { return .{ // Hardcoded default values chosen from // /usr/src/usr.sbin/pw/pw_conf.c @@ -274,12 +245,28 @@ fn PlatformStruct() type { const platform_struct = PlatformStruct(); +// TODO 0.16.0: Can we get away with this? +pub fn isError(result: anytype) bool { + if (@typeInfo(@TypeOf(result)).int.signedness == .signed) { + return result < 0; + } + + if (@typeInfo(@TypeOf(result)).int.signedness == .unsigned) { + return switch (builtin.os.tag) { + .linux => std.os.linux.errno(result) != .SUCCESS, + else => @compileError("interop.isError() not implemented for current target!"), + }; + } + + unreachable; +} + pub fn supportsUnicode() bool { return builtin.os.tag == .linux or builtin.os.tag == .freebsd; } -pub fn timeAsString(buf: [:0]u8, format: [:0]const u8) []u8 { - const timer = std.time.timestamp(); +pub fn timeAsString(io: std.Io, buf: [:0]u8, format: [:0]const u8) []u8 { + const timer = std.Io.Timestamp.now(io, .real).toSeconds(); const tm_info = time.localtime(&timer); const len = time.strftime(buf, buf.len, format, tm_info); @@ -298,8 +285,8 @@ pub fn getTimeOfDay() !TimeOfDay { }; } -pub fn getActiveTty(allocator: std.mem.Allocator, use_kmscon_vt: bool) !u8 { - return platform_struct.getActiveTtyImpl(allocator, use_kmscon_vt); +pub fn getActiveTty(allocator: std.mem.Allocator, io: std.Io, use_kmscon_vt: bool) !u8 { + return platform_struct.getActiveTtyImpl(allocator, io, use_kmscon_vt); } pub fn switchTty(tty: u8) !void { @@ -402,6 +389,6 @@ pub fn closePasswordDatabase() void { // This is very bad parsing, but we only need to get 2 values... and the format // of the file doesn't seem to be standard? So this should be fine... -pub fn getUserIdRange(allocator: std.mem.Allocator, file_path: []const u8) !UidRange { - return platform_struct.getUserIdRange(allocator, file_path); +pub fn getUserIdRange(allocator: std.mem.Allocator, io: std.Io, file_path: []const u8) !UidRange { + return platform_struct.getUserIdRange(allocator, io, file_path); } diff --git a/ly-core/src/root.zig b/ly-core/src/root.zig index 4bcb6a8..42198f5 100644 --- a/ly-core/src/root.zig +++ b/ly-core/src/root.zig @@ -27,6 +27,7 @@ pub fn IniParser(comptime Struct: type) type { pub fn init( allocator: std.mem.Allocator, + io: std.Io, path: []const u8, field_handler: ?fn (allocator: std.mem.Allocator, field: ini.IniField) ?ini.IniField, ) !Self { @@ -35,7 +36,7 @@ pub fn IniParser(comptime Struct: type) type { var maybe_load_error: ?anyerror = null; - const structure = ini_struct.readFileToStruct(path, .{ + const structure = ini_struct.readFileToStruct(io, path, .{ .fieldHandler = field_handler, .errorHandler = errorHandler, .comment_characters = "#", diff --git a/ly-ui/build.zig b/ly-ui/build.zig index 589f7c7..a7a3051 100644 --- a/ly-ui/build.zig +++ b/ly-ui/build.zig @@ -1,4 +1,5 @@ const std = @import("std"); +const Translator = @import("translate_c").Translator; pub fn build(b: *std.Build) void { const target = b.standardTargetOptions(.{}); @@ -17,15 +18,30 @@ pub fn build(b: *std.Build) void { .optimize = optimize, }); - const translate_c = b.addTranslateC(.{ - .root_source_file = termbox_dep.path("termbox2.h"), + const translate_c_dep = b.dependency("translate_c", .{ .target = target, .optimize = optimize, }); - translate_c.defineCMacroRaw("TB_IMPL"); - translate_c.defineCMacro("TB_OPT_ATTR_W", "32"); // Enable 24-bit color support + styling (32-bit) - const termbox2 = translate_c.addModule("termbox2"); - mod.addImport("termbox2", termbox2); + + const termbox2: Translator = .init(translate_c_dep, .{ + .c_source_file = termbox_dep.path("termbox2.h"), + .target = target, + .optimize = optimize, + }); + termbox2.defineCMacro("TB_IMPL", null); + // TODO 0.16.0: Workaround until Aro gets better... + // https://codeberg.org/ziglang/translate-c/issues/319 + termbox2.defineCMacro("_XOPEN_SOURCE", "700"); + termbox2.defineCMacro("TB_OPT_ATTR_W", "32"); // Enable 24-bit color support + styling (32-bit) + // TODO 0.16.0: Including with -OReleaseSafe causes + // __attribute__(__error__()) to be called. Below + // is the workaround. + termbox2.defineCMacro("_FORTIFY_SOURCE", "0"); + // TODO 0.16.0: Needed for now + if (target.result.os.tag == .freebsd) { + termbox2.defineCMacro("__BSD_VISIBLE", "1"); + } + mod.addImport("termbox2", termbox2.mod); const mod_tests = b.addTest(.{ .root_module = mod, diff --git a/ly-ui/build.zig.zon b/ly-ui/build.zig.zon index a16ce9e..598cba0 100644 --- a/ly-ui/build.zig.zon +++ b/ly-ui/build.zig.zon @@ -2,14 +2,18 @@ .name = .ly_ui, .version = "1.0.0", .fingerprint = 0x8d11bf85a74ec803, - .minimum_zig_version = "0.15.0", + .minimum_zig_version = "0.16.0", .dependencies = .{ .ly_core = .{ .path = "../ly-core", }, .termbox2 = .{ - .url = "git+https://github.com/AnErrupTion/termbox2?ref=master#496730697c662893eec43192f48ff616c2539da6", - .hash = "N-V-__8AAOEWBQDt5tNdIzIFY6n8DdZsCP-6MyLoNS20wgpA", + .url = "git+https://github.com/AnErrupTion/termbox2?ref=master#c7f241e8888ce243e1748b05c26a42fcfaaad936", + .hash = "N-V-__8AAAUXBQD6Fwpi9m0MBqWXFFaqW5l1lVrJC2Ynj7a-", + }, + .translate_c = .{ + .url = "git+https://codeberg.org/ziglang/translate-c#7a1a9fdc4ab00835748a6657ecbb835e3d5d45f7", + .hash = "translate_c-0.0.0-Q_BUWvP1BgCjAk6PWv5286tOlvzD9-X-NkuTzh0KxY0Q", }, }, .paths = .{ diff --git a/ly-ui/src/TerminalBuffer.zig b/ly-ui/src/TerminalBuffer.zig index 4551d68..565593b 100644 --- a/ly-ui/src/TerminalBuffer.zig +++ b/ly-ui/src/TerminalBuffer.zig @@ -97,6 +97,7 @@ active_widget_index: usize, pub fn init( allocator: Allocator, + io: std.Io, options: InitOptions, log_file: *LogFile, random: Random, @@ -106,9 +107,9 @@ pub fn init( if (options.full_color) { _ = termbox.tb_set_output_mode(termbox.TB_OUTPUT_TRUECOLOR); - try log_file.info("tui", "termbox2 set to 24-bit color output mode", .{}); + try log_file.info(io, "tui", "termbox2 set to 24-bit color output mode", .{}); } else { - try log_file.info("tui", "termbox2 set to eight-color output mode", .{}); + try log_file.info(io, "tui", "termbox2 set to eight-color output mode", .{}); } _ = termbox.tb_clear(); @@ -119,7 +120,7 @@ pub fn init( const width: usize = @intCast(termbox.tb_width()); const height: usize = @intCast(termbox.tb_height()); - try log_file.info("tui", "screen resolution is {d}x{d}", .{ width, height }); + try log_file.info(io, "tui", "screen resolution is {d}x{d}", .{ width, height }); return .{ .log_file = log_file, @@ -168,6 +169,7 @@ pub fn deinit(self: *TerminalBuffer) void { pub fn runEventLoop( self: *TerminalBuffer, allocator: Allocator, + io: std.Io, shared_error: SharedError, layers: [][]*Widget, active_widget: *Widget, @@ -176,14 +178,14 @@ pub fn runEventLoop( inactivity_event_fn: ?*const fn (*anyopaque) anyerror!void, context: *anyopaque, ) !void { - try self.registerGlobalKeybind("Ctrl+K", &moveCursorUp, self); - try self.registerGlobalKeybind("Up", &moveCursorUp, self); + try self.registerGlobalKeybind(io, "Ctrl+K", &moveCursorUp, self); + try self.registerGlobalKeybind(io, "Up", &moveCursorUp, self); - try self.registerGlobalKeybind("Ctrl+J", &moveCursorDown, self); - try self.registerGlobalKeybind("Down", &moveCursorDown, self); + try self.registerGlobalKeybind(io, "Ctrl+J", &moveCursorDown, self); + try self.registerGlobalKeybind(io, "Down", &moveCursorDown, self); - try self.registerGlobalKeybind("Tab", &wrapCursor, self); - try self.registerGlobalKeybind("Shift+Tab", &wrapCursorReverse, self); + try self.registerGlobalKeybind(io, "Tab", &wrapCursor, self); + try self.registerGlobalKeybind(io, "Shift+Tab", &wrapCursorReverse, self); defer self.handlable_widgets.deinit(allocator); @@ -218,6 +220,7 @@ pub fn runEventLoop( current_widget.handle(null) catch |err| { shared_error.writeError(error.SetCursorFailed); try self.log_file.err( + io, "tui", "failed to set cursor in active widget '{s}': {s}", .{ current_widget.display_name, @errorName(err) }, @@ -261,6 +264,7 @@ pub fn runEventLoop( self.height = TerminalBuffer.getHeight(); try self.log_file.info( + io, "tui", "screen resolution updated to {d}x{d}", .{ self.width, self.height }, @@ -271,6 +275,7 @@ pub fn runEventLoop( widget.realloc() catch |err| { shared_error.writeError(error.WidgetReallocationFailed); try self.log_file.err( + io, "tui", "failed to reallocate widget '{s}': {s}", .{ widget.display_name, @errorName(err) }, @@ -294,6 +299,7 @@ pub fn runEventLoop( current_widget.handle(key) catch |err| { shared_error.writeError(error.CurrentWidgetHandlingFailed); try self.log_file.err( + io, "tui", "failed to handle active widget '{s}': {s}", .{ current_widget.display_name, @errorName(err) }, @@ -390,18 +396,20 @@ pub fn reclaim(self: TerminalBuffer) !void { pub fn registerKeybind( self: *TerminalBuffer, + io: std.Io, keybinds: *KeybindMap, keybind: []const u8, callback: KeybindCallbackFn, context: *anyopaque, ) !void { - const key = try self.parseKeybind(keybind); + const key = try self.parseKeybind(io, keybind); keybinds.put(key, .{ .callback = callback, .context = context, }) catch |err| { try self.log_file.err( + io, "tui", "failed to register keybind {s}: {s}", .{ keybind, @errorName(err) }, @@ -411,15 +419,16 @@ pub fn registerKeybind( pub fn registerGlobalKeybind( self: *TerminalBuffer, + io: std.Io, keybind: []const u8, callback: KeybindCallbackFn, context: *anyopaque, ) !void { - try self.registerKeybind(&self.keybinds, keybind, callback, context); + try self.registerKeybind(io, &self.keybinds, keybind, callback, context); } -pub fn simulateKeybind(self: *TerminalBuffer, keybind: []const u8) !bool { - const key = try self.parseKeybind(keybind); +pub fn simulateKeybind(self: *TerminalBuffer, io: std.Io, keybind: []const u8) !bool { + const key = try self.parseKeybind(io, keybind); if (self.keybinds.get(key)) |binding| { return try @call( @@ -509,10 +518,13 @@ fn clearBackBuffer() !void { // Clear the TTY because termbox2 doesn't seem to do it properly const capability = termbox.global.caps[termbox.TB_CAP_CLEAR_SCREEN]; const capability_slice = std.mem.span(capability); - _ = try std.posix.write(termbox.global.ttyfd, capability_slice); + const result = std.posix.system.write(termbox.global.ttyfd, capability_slice.ptr, capability_slice.len); + + if (result != capability_slice.len) return error.PartialClearBackBuffer; + if (result < 0) return error.ClearBackBufferFailed; } -fn parseKeybind(self: *TerminalBuffer, keybind: []const u8) !keyboard.Key { +fn parseKeybind(self: *TerminalBuffer, io: std.Io, keybind: []const u8) !keyboard.Key { var key = std.mem.zeroes(keyboard.Key); var iterator = std.mem.splitScalar(u8, keybind, '+'); @@ -529,6 +541,7 @@ fn parseKeybind(self: *TerminalBuffer, keybind: []const u8) !keyboard.Key { if (!found) { try self.log_file.err( + io, "tui", "failed to parse key {s} of keybind {s}", .{ item, keybind }, diff --git a/ly-ui/src/components/Text.zig b/ly-ui/src/components/Text.zig index fc04fe7..4fc35bf 100644 --- a/ly-ui/src/components/Text.zig +++ b/ly-ui/src/components/Text.zig @@ -29,6 +29,7 @@ keybinds: TerminalBuffer.KeybindMap, pub fn init( allocator: Allocator, + io: std.Io, buffer: *TerminalBuffer, should_insert: bool, masked: bool, @@ -57,11 +58,11 @@ pub fn init( .keybinds = .init(allocator), }; - try buffer.registerKeybind(&self.keybinds, "Left", &goLeft, self); - try buffer.registerKeybind(&self.keybinds, "Right", &goRight, self); - try buffer.registerKeybind(&self.keybinds, "Delete", &delete, self); - try buffer.registerKeybind(&self.keybinds, "Backspace", &backspace, self); - try buffer.registerKeybind(&self.keybinds, "Ctrl+U", &clearTextEntry, self); + try buffer.registerKeybind(io, &self.keybinds, "Left", &goLeft, self); + try buffer.registerKeybind(io, &self.keybinds, "Right", &goRight, self); + try buffer.registerKeybind(io, &self.keybinds, "Delete", &delete, self); + try buffer.registerKeybind(io, &self.keybinds, "Backspace", &backspace, self); + try buffer.registerKeybind(io, &self.keybinds, "Ctrl+U", &clearTextEntry, self); return self; } diff --git a/ly-ui/src/components/generic.zig b/ly-ui/src/components/generic.zig index d7a609d..0a76c4f 100644 --- a/ly-ui/src/components/generic.zig +++ b/ly-ui/src/components/generic.zig @@ -32,6 +32,7 @@ pub fn CyclableLabel(comptime ItemType: type, comptime ChangeItemType: type) typ pub fn init( allocator: Allocator, + io: std.Io, buffer: *TerminalBuffer, draw_item_fn: DrawItemFn, change_item_fn: ?ChangeItemFn, @@ -60,10 +61,10 @@ pub fn CyclableLabel(comptime ItemType: type, comptime ChangeItemType: type) typ .keybinds = .init(allocator), }; - try buffer.registerKeybind(&self.keybinds, "Left", &goLeft, self); - try buffer.registerKeybind(&self.keybinds, "Ctrl+H", &goLeft, self); - try buffer.registerKeybind(&self.keybinds, "Right", &goRight, self); - try buffer.registerKeybind(&self.keybinds, "Ctrl+L", &goRight, self); + try buffer.registerKeybind(io, &self.keybinds, "Left", &goLeft, self); + try buffer.registerKeybind(io, &self.keybinds, "Ctrl+H", &goLeft, self); + try buffer.registerKeybind(io, &self.keybinds, "Right", &goRight, self); + try buffer.registerKeybind(io, &self.keybinds, "Ctrl+L", &goRight, self); return self; } diff --git a/readme.md b/readme.md index 79f5941..aa23db8 100644 --- a/readme.md +++ b/readme.md @@ -14,7 +14,7 @@ Join us on Matrix over at [#ly-dm:matrix.org](https://matrix.to/#/#ly-dm:matrix. ## Dependencies - Compile-time: - - zig 0.15.x + - zig 0.16.x - libc diff --git a/src/animations/Cascade.zig b/src/animations/Cascade.zig index b11c728..c1e88f1 100644 --- a/src/animations/Cascade.zig +++ b/src/animations/Cascade.zig @@ -8,17 +8,20 @@ const Widget = ly_ui.Widget; const Cascade = @This(); +io: std.Io, instance: ?Widget = null, buffer: *TerminalBuffer, current_auth_fails: *usize, max_auth_fails: usize, pub fn init( + io: std.Io, buffer: *TerminalBuffer, current_auth_fails: *usize, max_auth_fails: usize, ) Cascade { return .{ + .io = io, .instance = null, .buffer = buffer, .current_auth_fails = current_auth_fails, @@ -44,7 +47,7 @@ pub fn widget(self: *Cascade) *Widget { fn draw(self: *Cascade) void { while (self.current_auth_fails.* >= self.max_auth_fails) { - std.Thread.sleep(std.time.ns_per_ms * 10); + self.io.sleep(.fromMilliseconds(10), .real) catch {}; var changed = false; var y = self.buffer.height - 2; @@ -80,7 +83,7 @@ fn draw(self: *Cascade) void { } if (!changed) { - std.Thread.sleep(std.time.ns_per_s * 7); + self.io.sleep(.fromSeconds(7), .real) catch {}; self.current_auth_fails.* = 0; } diff --git a/src/animations/ColorMix.zig b/src/animations/ColorMix.zig index 0715825..49c6238 100644 --- a/src/animations/ColorMix.zig +++ b/src/animations/ColorMix.zig @@ -113,7 +113,7 @@ fn draw(self: *ColorMix) void { uv -= @splat(1.0 * math.cos(uv[0] + uv[1]) - math.sin(uv[0] * 0.7 - uv[1])); } - const cell = self.palette[@as(usize, @intFromFloat(math.floor(length(uv) * 5.0))) % palette_len]; + const cell = self.palette[@as(usize, @trunc(math.floor(length(uv) * 5.0))) % palette_len]; cell.put(x, y); } } diff --git a/src/animations/DurFile.zig b/src/animations/DurFile.zig index b6aceae..18844b8 100644 --- a/src/animations/DurFile.zig +++ b/src/animations/DurFile.zig @@ -19,16 +19,16 @@ const LogFile = ly_core.LogFile; const enums = @import("../enums.zig"); const DurOffsetAlignment = enums.DurOffsetAlignment; -fn read_decompress_file(allocator: Allocator, file_path: []const u8) ![]u8 { - const file_buffer = std.fs.cwd().openFile(file_path, .{}) catch { +fn read_decompress_file(allocator: Allocator, io: std.Io, file_path: []const u8) ![]u8 { + const file_buffer = std.Io.Dir.cwd().openFile(io, file_path, .{}) catch { return error.FileNotFound; }; - defer file_buffer.close(); + defer file_buffer.close(io); var file_reader_buffer: [4096]u8 = undefined; var decompress_buffer: [flate.max_window_len]u8 = undefined; - var file_reader = file_buffer.reader(&file_reader_buffer); + var file_reader = file_buffer.reader(io, &file_reader_buffer); var decompress: flate.Decompress = .init(&file_reader.interface, .gzip, &decompress_buffer); const file_decompressed = decompress.reader.allocRemaining(allocator, .unlimited) catch { @@ -150,8 +150,8 @@ const DurFormat = struct { } } - pub fn create_from_file(self: *DurFormat, allocator: Allocator, file_path: []const u8) !void { - const file_decompressed = try read_decompress_file(allocator, file_path); + pub fn create_from_file(self: *DurFormat, allocator: Allocator, io: std.Io, file_path: []const u8) !void { + const file_decompressed = try read_decompress_file(allocator, io, file_path); defer allocator.free(file_decompressed); const parsed = try Json.parseFromSlice(Json.Value, allocator, file_decompressed, .{}); @@ -307,6 +307,7 @@ const DurFile = @This(); instance: ?Widget = null, start_time: TimeOfDay, allocator: Allocator, +io: std.Io, terminal_buffer: *TerminalBuffer, dur_movie: DurFormat, frames: u64, @@ -368,6 +369,7 @@ fn calc_frame_size(terminal_buffer: *TerminalBuffer, dur_movie: *DurFormat) UVec pub fn init( allocator: Allocator, + io: std.Io, terminal_buffer: *TerminalBuffer, log_file: *LogFile, file_path: []const u8, @@ -381,13 +383,13 @@ pub fn init( ) !DurFile { var dur_movie: DurFormat = .init(allocator); - dur_movie.create_from_file(allocator, file_path) catch |err| switch (err) { + dur_movie.create_from_file(allocator, io, file_path) catch |err| switch (err) { error.FileNotFound => { - try log_file.err("tui", "dur_file was not found at: {s}", .{file_path}); + try log_file.err(io, "tui", "dur_file was not found at: {s}", .{file_path}); return err; }, error.NotValidFile => { - try log_file.err("tui", "dur_file loaded was invalid or not a dur file!", .{}); + try log_file.err(io, "tui", "dur_file loaded was invalid or not a dur file!", .{}); return err; }, else => return err, @@ -395,7 +397,7 @@ pub fn init( // 4 bit mode with 256 color is unsupported if (!full_color and eql(u8, dur_movie.colorFormat.?, "256")) { - try log_file.err("tui", "dur_file can not be 256 color encoded when not using full_color option!", .{}); + try log_file.err(io, "tui", "dur_file can not be 256 color encoded when not using full_color option!", .{}); dur_movie.deinit(); return error.InvalidColorFormat; } @@ -406,15 +408,16 @@ pub fn init( const frame_size = calc_frame_size(terminal_buffer, &dur_movie); // Convert dur fps to frames per ms - const frame_time: u32 = @intFromFloat(1000 / dur_movie.framerate.?); + const frame_time: u32 = @trunc(1000 / dur_movie.framerate.?); return .{ .instance = null, .start_time = try interop.getTimeOfDay(), .allocator = allocator, + .io = io, .terminal_buffer = terminal_buffer, .frames = 0, - .time_previous = std.time.milliTimestamp(), + .time_previous = std.Io.Timestamp.now(io, .real).toMilliseconds(), .frame_size = frame_size, .start_pos = start_pos, .full_color = full_color, @@ -499,11 +502,11 @@ fn draw(self: *DurFile) void { } } - const time_current = std.time.milliTimestamp(); + const time_current = std.Io.Timestamp.now(self.io, .real).toMilliseconds(); const delta_time = time_current - self.time_previous; // Convert delay from sec to ms - const delay_time: u32 = @intFromFloat(current_frame.delay * 1000); + const delay_time: u32 = @trunc(current_frame.delay * 1000); if (delta_time > (self.frame_time + delay_time)) { self.time_previous = time_current; diff --git a/src/auth.zig b/src/auth.zig index e74843f..b765c08 100644 --- a/src/auth.zig +++ b/src/auth.zig @@ -27,16 +27,16 @@ pub const AuthOptions = struct { }; var xorg_pid: std.posix.pid_t = 0; -pub fn xorgSignalHandler(i: c_int) callconv(.c) void { - if (xorg_pid > 0) _ = std.c.kill(xorg_pid, i); +pub fn xorgSignalHandler(sig: std.posix.SIG) callconv(.c) void { + if (xorg_pid > 0) _ = std.c.kill(xorg_pid, sig); } var child_pid: std.posix.pid_t = 0; -pub fn sessionSignalHandler(i: c_int) callconv(.c) void { - if (child_pid > 0) _ = std.c.kill(child_pid, i); +pub fn sessionSignalHandler(sig: std.posix.SIG) callconv(.c) void { + if (child_pid > 0) _ = std.c.kill(child_pid, sig); } -pub fn authenticate(allocator: std.mem.Allocator, log_file: *LogFile, options: AuthOptions, current_environment: Environment, login: []const u8, password: []const u8) !void { +pub fn authenticate(allocator: std.mem.Allocator, io: std.Io, log_file: *LogFile, options: AuthOptions, current_environment: Environment, login: []const u8, password: []const u8) !void { var tty_buffer: [3]u8 = undefined; const tty_str = try std.fmt.bufPrint(&tty_buffer, "{d}", .{options.tty}); @@ -44,11 +44,11 @@ pub fn authenticate(allocator: std.mem.Allocator, log_file: *LogFile, options: A const pam_tty_str = try std.fmt.bufPrintZ(&pam_tty_buffer, "tty{d}", .{options.tty}); // Set the XDG environment variables - try log_file.info("auth/env", "setting xdg environment variables", .{}); + try log_file.info(io, "auth/env", "setting xdg environment variables", .{}); try setXdgEnv(allocator, tty_str, current_environment); // Open the PAM session - try log_file.info("auth/pam", "encoding credentials", .{}); + try log_file.info(io, "auth/pam", "encoding credentials", .{}); const login_z = try allocator.dupeZ(u8, login); defer allocator.free(login_z); @@ -63,36 +63,36 @@ pub fn authenticate(allocator: std.mem.Allocator, log_file: *LogFile, options: A }; var handle: ?*interop.pam.pam_handle = undefined; - try log_file.info("auth/pam", "starting session", .{}); + try log_file.info(io, "auth/pam", "starting session", .{}); var status = interop.pam.pam_start(options.service_name, null, &conv, &handle); if (status != interop.pam.PAM_SUCCESS) return pamDiagnose(status); defer _ = interop.pam.pam_end(handle, status); // Set PAM_TTY as the current TTY. This is required in case it isn't being set by another PAM module - try log_file.info("auth/pam", "setting tty", .{}); + try log_file.info(io, "auth/pam", "setting tty", .{}); status = interop.pam.pam_set_item(handle, interop.pam.PAM_TTY, pam_tty_str.ptr); if (status != interop.pam.PAM_SUCCESS) return pamDiagnose(status); // Do the PAM routine - try log_file.info("auth/pam", "authenticating", .{}); + try log_file.info(io, "auth/pam", "authenticating", .{}); status = interop.pam.pam_authenticate(handle, 0); if (status != interop.pam.PAM_SUCCESS) return pamDiagnose(status); - try log_file.info("auth/pam", "validating account", .{}); + try log_file.info(io, "auth/pam", "validating account", .{}); status = interop.pam.pam_acct_mgmt(handle, 0); if (status != interop.pam.PAM_SUCCESS) return pamDiagnose(status); - try log_file.info("auth/pam", "setting credentials", .{}); + try log_file.info(io, "auth/pam", "setting credentials", .{}); status = interop.pam.pam_setcred(handle, interop.pam.PAM_ESTABLISH_CRED); if (status != interop.pam.PAM_SUCCESS) return pamDiagnose(status); defer status = interop.pam.pam_setcred(handle, interop.pam.PAM_DELETE_CRED); - try log_file.info("auth/pam", "opening session", .{}); + try log_file.info(io, "auth/pam", "opening session", .{}); status = interop.pam.pam_open_session(handle, 0); if (status != interop.pam.PAM_SUCCESS) return pamDiagnose(status); defer status = interop.pam.pam_close_session(handle, 0); - try log_file.info("auth/passwd", "getting struct", .{}); + try log_file.info(io, "auth/passwd", "getting struct", .{}); var user_entry: interop.UsernameEntry = undefined; { defer interop.closePasswordDatabase(); @@ -102,27 +102,27 @@ pub fn authenticate(allocator: std.mem.Allocator, log_file: *LogFile, options: A } // Set user shell if it hasn't already been set - try log_file.info("auth/passwd", "setting user shell", .{}); + try log_file.info(io, "auth/passwd", "setting user shell", .{}); if (user_entry.shell == null) interop.setUserShell(&user_entry); var shared_err = try SharedError.init(null, null); defer shared_err.deinit(); - log_file.deinit(); + log_file.deinit(io); - child_pid = try std.posix.fork(); + child_pid = std.posix.system.fork(); if (child_pid == 0) { - try log_file.reinit(); - try log_file.info("auth/sys", "starting session", .{}); + try log_file.reinit(io); + try log_file.info(io, "auth/sys", "starting session", .{}); - startSession(log_file, allocator, options, tty_str, user_entry, handle, current_environment) catch |e| { + startSession(log_file, allocator, io, options, tty_str, user_entry, handle, current_environment) catch |e| { shared_err.writeError(e); - log_file.deinit(); + log_file.deinit(io); std.process.exit(1); }; - log_file.deinit(); + log_file.deinit(io); std.process.exit(0); } @@ -132,7 +132,8 @@ pub fn authenticate(allocator: std.mem.Allocator, log_file: *LogFile, options: A // If an error occurs here, we can send SIGTERM to the session errdefer cleanup: { std.posix.kill(child_pid, std.posix.SIG.TERM) catch break :cleanup; - _ = std.posix.waitpid(child_pid, 0); + var child_status: c_int = undefined; + _ = std.posix.system.waitpid(child_pid, &child_status, 0); } // If we receive SIGTERM, forward it to child_pid @@ -143,14 +144,15 @@ pub fn authenticate(allocator: std.mem.Allocator, log_file: *LogFile, options: A }; std.posix.sigaction(std.posix.SIG.TERM, &act, null); - try addUtmpEntry(&entry, user_entry.username.?, child_pid); + try addUtmpEntry(io, &entry, user_entry.username.?, child_pid); } // Wait for the session to stop - _ = std.posix.waitpid(child_pid, 0); + var child_status: c_int = undefined; + _ = std.posix.system.waitpid(child_pid, &child_status, 0); - try log_file.reinit(); + try log_file.reinit(io); - try log_file.info("auth/utmp", "removing utmp entry", .{}); + try log_file.info(io, "auth/utmp", "removing utmp entry", .{}); removeUtmpEntry(&entry); if (shared_err.readError()) |err| return err; @@ -159,6 +161,7 @@ pub fn authenticate(allocator: std.mem.Allocator, log_file: *LogFile, options: A fn startSession( log_file: *LogFile, allocator: std.mem.Allocator, + io: std.Io, options: AuthOptions, tty_str: []u8, user_entry: interop.UsernameEntry, @@ -166,15 +169,15 @@ fn startSession( current_environment: Environment, ) !void { // Set the user's GID & PID - try log_file.info("auth/passwd", "setting user context", .{}); + try log_file.info(io, "auth/passwd", "setting user context", .{}); try interop.setUserContext(allocator, user_entry); // Set up the environment - try log_file.info("auth/env", "setting environment variables", .{}); + try log_file.info(io, "auth/env", "setting environment variables", .{}); try initEnv(allocator, user_entry, options.path); // Reset the XDG environment variables - try log_file.info("auth/env", "resetting xdg environment variables", .{}); + try log_file.info(io, "auth/env", "resetting xdg environment variables", .{}); try setXdgEnv(allocator, tty_str, current_environment); try setXdgRuntimeDir(allocator); @@ -185,27 +188,30 @@ fn startSession( const env_list = std.mem.span(pam_env_vars.?); for (env_list) |env_var| { if (env_var == null) continue; - try log_file.info("auth/env", "setting pam environment variable: {s}", .{std.mem.span(env_var.?)}); + try log_file.info(io, "auth/env", "setting pam environment variable: {s}", .{std.mem.span(env_var.?)}); try interop.putEnvironmentVariable(env_var); } + const home_z = try allocator.dupeZ(u8, user_entry.home.?); + defer allocator.free(home_z); + // Change to the user's home directory - try log_file.info("auth/sys", "changing cwd to user home", .{}); - std.posix.chdir(user_entry.home.?) catch return error.ChangeDirectoryFailed; + try log_file.info(io, "auth/sys", "changing cwd to user home", .{}); + if (std.posix.system.chdir(home_z.ptr) < 0) return error.ChangeDirectoryFailed; // Signal to the session process to give up control on the TTY - try log_file.info("auth/sys", "releasing tty", .{}); + try log_file.info(io, "auth/sys", "releasing tty", .{}); std.posix.kill(options.session_pid, std.posix.SIG.CHLD) catch return error.TtyControlTransferFailed; // Execute what the user requested switch (current_environment.display_server) { - .wayland, .shell, .custom => try executeCmd(log_file, allocator, user_entry.shell.?, options, current_environment.is_terminal, current_environment.cmd), + .wayland, .shell, .custom => try executeCmd(log_file, allocator, io, user_entry.shell.?, options, current_environment.is_terminal, current_environment.cmd), .xinitrc, .x11 => if (build_options.enable_x11_support) { var vt_buf: [5]u8 = undefined; const vt = try std.fmt.bufPrint(&vt_buf, "vt{d}", .{options.x_vt orelse options.tty}); - try log_file.info("auth/x11", "setting vt to {s}", .{vt}); - try executeX11Cmd(log_file, allocator, user_entry.shell.?, user_entry.home.?, options, current_environment.cmd orelse "", vt); + try log_file.info(io, "auth/x11", "setting vt to {s}", .{vt}); + try executeX11Cmd(log_file, allocator, io, user_entry.shell.?, user_entry.home.?, options, current_environment.cmd orelse "", vt); }, } } @@ -247,7 +253,7 @@ fn setXdgRuntimeDir(allocator: std.mem.Allocator) !void { // XDG_RUNTIME_DIR to fall back to directories inside user's home // directory. if (builtin.os.tag != .freebsd) { - const uid = std.posix.getuid(); + const uid = std.posix.system.getuid(); var uid_buffer: [32]u8 = undefined; // No UID can be larger than this const uid_str = try std.fmt.bufPrint(&uid_buffer, "/run/user/{d}", .{uid}); @@ -317,20 +323,20 @@ fn getFreeDisplay() !u8 { var buf: [15]u8 = undefined; var i: u8 = 0; while (i < 200) : (i += 1) { - const xlock = try std.fmt.bufPrint(&buf, "/tmp/.X{d}-lock", .{i}); - std.posix.access(xlock, std.posix.F_OK) catch break; + const xlock = try std.fmt.bufPrintZ(&buf, "/tmp/.X{d}-lock", .{i}); + if (interop.isError(std.posix.system.access(xlock.ptr, std.posix.F_OK))) break; } return i; } -fn getXPid(display_num: u8) !i32 { +fn getXPid(io: std.Io, display_num: u8) !i32 { var buf: [15]u8 = undefined; const file_name = try std.fmt.bufPrint(&buf, "/tmp/.X{d}-lock", .{display_num}); - const file = try std.fs.openFileAbsolute(file_name, .{}); - defer file.close(); + const file = try std.Io.Dir.openFileAbsolute(io, file_name, .{}); + defer file.close(io); var file_buffer: [32]u8 = undefined; - var file_reader = file.reader(&file_buffer); + var file_reader = file.reader(io, &file_buffer); var reader = &file_reader.interface; var buffer: [20]u8 = undefined; @@ -340,41 +346,41 @@ fn getXPid(display_num: u8) !i32 { return std.fmt.parseInt(i32, std.mem.trim(u8, buffer[0..written], " "), 10); } -fn createXauthFile(log_file: *LogFile, pwd: []const u8, buffer: []u8) ![]const u8 { +fn createXauthFile(log_file: *LogFile, io: std.Io, pwd: []const u8, buffer: []u8) ![]const u8 { var xauth_buf: [100]u8 = undefined; var xauth_dir: []const u8 = undefined; - const xdg_rt_dir = std.posix.getenv("XDG_RUNTIME_DIR"); + const xdg_rt_dir = std.posix.system.getenv("XDG_RUNTIME_DIR"); var xauth_file: []const u8 = "lyxauth"; if (xdg_rt_dir == null) no_rt_dir: { - const xdg_cfg_home = std.posix.getenv("XDG_CONFIG_HOME"); + const xdg_cfg_home = std.posix.system.getenv("XDG_CONFIG_HOME"); if (xdg_cfg_home == null) no_cfg_home: { xauth_dir = try std.fmt.bufPrint(&xauth_buf, "{s}/.config", .{pwd}); - var dir = std.fs.cwd().openDir(xauth_dir, .{}) catch { + var dir = std.Io.Dir.cwd().openDir(io, xauth_dir, .{}) catch { // xauth_dir isn't a directory xauth_dir = pwd; xauth_file = ".lyxauth"; break :no_cfg_home; }; - dir.close(); + dir.close(io); // xauth_dir is a directory, use it to store Xauthority xauth_dir = try std.fmt.bufPrint(&xauth_buf, "{s}/.config/ly", .{pwd}); } else { - xauth_dir = try std.fmt.bufPrint(&xauth_buf, "{s}/ly", .{xdg_cfg_home.?}); + xauth_dir = try std.fmt.bufPrint(&xauth_buf, "{s}/ly", .{std.mem.span(xdg_cfg_home.?)}); } - const file = std.fs.cwd().openFile(xauth_dir, .{}) catch break :no_rt_dir; - file.close(); + const file = std.Io.Dir.cwd().openFile(io, xauth_dir, .{}) catch break :no_rt_dir; + file.close(io); // xauth_dir is a file, create the parent directory - std.posix.mkdir(xauth_dir, 777) catch { + std.Io.Dir.createDirAbsolute(io, xauth_dir, .fromMode(777)) catch { xauth_dir = pwd; xauth_file = ".lyxauth"; }; } else { - xauth_dir = xdg_rt_dir.?; + xauth_dir = std.mem.span(xdg_rt_dir.?); } // Trim trailing slashes @@ -384,19 +390,19 @@ fn createXauthFile(log_file: *LogFile, pwd: []const u8, buffer: []u8) ![]const u const xauthority: []u8 = try std.fmt.bufPrint(buffer, "{s}/{s}", .{ trimmed_xauth_dir, xauth_file }); - std.fs.cwd().makePath(trimmed_xauth_dir) catch {}; + std.Io.Dir.cwd().createDirPath(io, trimmed_xauth_dir) catch {}; - try log_file.info("auth/x11", "creating xauth file: {s}", .{xauthority}); + try log_file.info(io, "auth/x11", "creating xauth file: {s}", .{xauthority}); - const file = try std.fs.createFileAbsolute(xauthority, .{}); - file.close(); + const file = try std.Io.Dir.createFileAbsolute(io, xauthority, .{}); + file.close(io); return xauthority; } -fn mcookie() [Md5.digest_length * 2]u8 { +fn mcookie(io: std.Io) [Md5.digest_length * 2]u8 { var buf: [4096]u8 = undefined; - std.crypto.random.bytes(&buf); + io.random(&buf); var out: [Md5.digest_length]u8 = undefined; Md5.hash(&buf, &out, .{}); @@ -404,86 +410,87 @@ fn mcookie() [Md5.digest_length * 2]u8 { return std.fmt.bytesToHex(&out, .lower); } -fn xauth(log_file: *LogFile, allocator: std.mem.Allocator, display_name: []u8, shell: [*:0]const u8, home: []const u8, xauth_buffer: []u8, options: AuthOptions) ![]const u8 { - const xauthority = try createXauthFile(log_file, home, xauth_buffer); +fn xauth(log_file: *LogFile, allocator: std.mem.Allocator, io: std.Io, display_name: []u8, shell: [*:0]const u8, home: []const u8, xauth_buffer: []u8, options: AuthOptions) ![]const u8 { + const xauthority = try createXauthFile(log_file, io, home, xauth_buffer); try interop.setEnvironmentVariable(allocator, "XAUTHORITY", xauthority, true); try interop.setEnvironmentVariable(allocator, "DISPLAY", display_name, true); - const magic_cookie = mcookie(); + const magic_cookie = mcookie(io); - const pid = try std.posix.fork(); + const pid = std.posix.system.fork(); if (pid == 0) { var cmd_buffer: [1024]u8 = undefined; const cmd_str = std.fmt.bufPrintZ(&cmd_buffer, "{s} add {s} . {s}", .{ options.xauth_cmd, display_name, magic_cookie }) catch std.process.exit(1); - try log_file.info("auth/x11", "executing: {s} -c {s}", .{ shell, cmd_str }); + try log_file.info(io, "auth/x11", "executing: {s} -c {s}", .{ shell, cmd_str }); const args = [_:null]?[*:0]const u8{ shell, "-c", cmd_str }; - std.posix.execveZ(shell, &args, std.c.environ) catch {}; + _ = std.posix.system.execve(shell, &args, std.c.environ); std.process.exit(1); } - const status = std.posix.waitpid(pid, 0); - if (status.status != 0) { - try log_file.file_writer.interface.print("xauth command failed with status {d}\n", .{status.status}); + var status: c_int = undefined; + const result = std.posix.system.waitpid(pid, &status, 0); + if (interop.isError(result) or status != 0) { + try log_file.file_writer.interface.print("xauth command failed with status {d}\n", .{status}); return error.XauthFailed; } return xauthority; } -fn executeX11Cmd(log_file: *LogFile, allocator: std.mem.Allocator, shell: []const u8, home: []const u8, options: AuthOptions, desktop_cmd: []const u8, vt: []const u8) !void { +fn executeX11Cmd(log_file: *LogFile, allocator: std.mem.Allocator, io: std.Io, shell: []const u8, home: []const u8, options: AuthOptions, desktop_cmd: []const u8, vt: []const u8) !void { var xauth_buffer: [256]u8 = undefined; - try log_file.info("auth/x11", "getting free display", .{}); + try log_file.info(io, "auth/x11", "getting free display", .{}); const display_num = try getFreeDisplay(); var buf: [4]u8 = undefined; const display_name = try std.fmt.bufPrint(&buf, ":{d}", .{display_num}); - try log_file.info("auth/x11", "got free display: {d}", .{display_num}); + try log_file.info(io, "auth/x11", "got free display: {d}", .{display_num}); const shell_z = try allocator.dupeZ(u8, shell); defer allocator.free(shell_z); - try log_file.info("auth/x11", "creating xauth file", .{}); - const xauthority = try xauth(log_file, allocator, display_name, shell_z, home, &xauth_buffer, options); + try log_file.info(io, "auth/x11", "creating xauth file", .{}); + const xauthority = try xauth(log_file, allocator, io, display_name, shell_z, home, &xauth_buffer, options); - try log_file.info("auth/x11", "starting x server", .{}); - const pid = try std.posix.fork(); + try log_file.info(io, "auth/x11", "starting x server", .{}); + const pid = std.posix.system.fork(); if (pid == 0) { var cmd_buffer: [1024]u8 = undefined; const cmd_str = std.fmt.bufPrintZ(&cmd_buffer, "{s} {s} {s} -auth {s}", .{ options.x_cmd, display_name, vt, xauthority }) catch std.process.exit(1); - try log_file.info("auth/x11", "executing: {s} -c {s} -auth {s}", .{ shell, cmd_str, xauthority }); + try log_file.info(io, "auth/x11", "executing: {s} -c {s} -auth {s}", .{ shell, cmd_str, xauthority }); const args = [_:null]?[*:0]const u8{ shell_z, "-c", cmd_str }; - std.posix.execveZ(shell_z, &args, std.c.environ) catch {}; + _ = std.posix.system.execve(shell_z, &args, std.c.environ); std.process.exit(1); } - try log_file.info("auth/x11", "waiting for xcb connection", .{}); + try log_file.info(io, "auth/x11", "waiting for xcb connection", .{}); var ok: c_int = -1; var xcb: ?*interop.xcb.xcb_connection_t = null; while (ok != 0) { xcb = interop.xcb.xcb_connect(null, null); ok = interop.xcb.xcb_connection_has_error(xcb); - std.posix.kill(pid, 0) catch |e| { + std.posix.kill(pid, @enumFromInt(0)) catch |e| { if (e == error.ProcessNotFound and ok != 0) return error.XcbConnectionFailed; }; } // X Server detaches from the process. // PID can be fetched from /tmp/X{d}.lock - try log_file.info("auth/x11", "getting x server pid", .{}); - const x_pid = try getXPid(display_num); - try log_file.info("auth/x11", "got x server pid: {d}", .{x_pid}); + try log_file.info(io, "auth/x11", "getting x server pid", .{}); + const x_pid = try getXPid(io, display_num); + try log_file.info(io, "auth/x11", "got x server pid: {d}", .{x_pid}); - try log_file.info("auth/x11", "launching environment", .{}); - xorg_pid = try std.posix.fork(); + try log_file.info(io, "auth/x11", "launching environment", .{}); + xorg_pid = std.posix.system.fork(); if (xorg_pid == 0) { var cmd_buffer: [1024]u8 = undefined; const cmd_str = std.fmt.bufPrintZ(&cmd_buffer, "{s} {s} {s} {s}", .{ if (options.use_kmscon_vt) "kmscon-launch-gui" else "", options.setup_cmd, options.login_cmd orelse "", desktop_cmd }) catch std.process.exit(1); - try log_file.info("auth/x11", "executing: {s} -c {s}", .{ shell, cmd_str }); + try log_file.info(io, "auth/x11", "executing: {s} -c {s}", .{ shell, cmd_str }); const args = [_:null]?[*:0]const u8{ shell_z, "-c", cmd_str }; - std.posix.execveZ(shell_z, &args, std.c.environ) catch {}; + _ = std.posix.system.execve(shell_z, &args, std.c.environ); std.process.exit(1); } @@ -495,26 +502,28 @@ fn executeX11Cmd(log_file: *LogFile, allocator: std.mem.Allocator, shell: []cons }; std.posix.sigaction(std.posix.SIG.TERM, &act, null); - _ = std.posix.waitpid(xorg_pid, 0); + var xorg_status: c_int = undefined; + _ = std.posix.system.waitpid(xorg_pid, &xorg_status, 0); - try log_file.info("auth/x11", "disconnecting xcb", .{}); + try log_file.info(io, "auth/x11", "disconnecting xcb", .{}); interop.xcb.xcb_disconnect(xcb); // TODO: Find a more robust way to ensure that X has been terminated (pidfds?) std.posix.kill(x_pid, std.posix.SIG.TERM) catch {}; - std.Thread.sleep(std.time.ns_per_s * 1); // Wait 1 second before sending SIGKILL + io.sleep(.fromSeconds(1), .real) catch {}; // Wait 1 second before sending SIGKILL std.posix.kill(x_pid, std.posix.SIG.KILL) catch return; - _ = std.posix.waitpid(x_pid, 0); + var x_status: c_int = undefined; + _ = std.posix.system.waitpid(x_pid, &x_status, 0); } -fn executeCmd(global_log_file: *LogFile, allocator: std.mem.Allocator, shell: []const u8, options: AuthOptions, is_terminal: bool, exec_cmd: ?[]const u8) !void { - try global_log_file.info("auth/sys", "launching wayland/shell/custom session", .{}); +fn executeCmd(global_log_file: *LogFile, allocator: std.mem.Allocator, io: std.Io, shell: []const u8, options: AuthOptions, is_terminal: bool, exec_cmd: ?[]const u8) !void { + try global_log_file.info(io, "auth/sys", "launching wayland/shell/custom session", .{}); - var maybe_log_file: ?std.fs.File = null; + var maybe_log_file: ?std.Io.File = null; if (!is_terminal) redirect_streams: { if (options.use_kmscon_vt) { - try global_log_file.err("auth/sys", "cannot redirect stdio & stderr with kmscon", .{}); + try global_log_file.err(io, "auth/sys", "cannot redirect stdio & stderr with kmscon", .{}); break :redirect_streams; } @@ -522,11 +531,11 @@ fn executeCmd(global_log_file: *LogFile, allocator: std.mem.Allocator, shell: [] // we redirect standard output & error or not. That is, we redirect only // if it's equal to false (so if it's not running in a TTY). if (options.session_log) |log_path| { - try global_log_file.info("auth/sys", "setting up stdio & stderr redirection", .{}); - maybe_log_file = try redirectStandardStreams(global_log_file, log_path, true); + try global_log_file.info(io, "auth/sys", "setting up stdio & stderr redirection", .{}); + maybe_log_file = try redirectStandardStreams(global_log_file, io, log_path, true); } } - defer if (maybe_log_file) |log_file| log_file.close(); + defer if (maybe_log_file) |log_file| log_file.close(io); const shell_z = try allocator.dupeZ(u8, shell); defer allocator.free(shell_z); @@ -534,40 +543,42 @@ fn executeCmd(global_log_file: *LogFile, allocator: std.mem.Allocator, shell: [] var cmd_buffer: [1024]u8 = undefined; const cmd_str = try std.fmt.bufPrintZ(&cmd_buffer, "{s} {s} {s} {s}", .{ if (!is_terminal and options.use_kmscon_vt) "kmscon-launch-gui" else "", options.setup_cmd, options.login_cmd orelse "", exec_cmd orelse shell }); - try global_log_file.info("auth/sys", "executing: {s} -c {s}", .{ shell, cmd_str }); + try global_log_file.info(io, "auth/sys", "executing: {s} -c {s}", .{ shell, cmd_str }); const args = [_:null]?[*:0]const u8{ shell_z, "-c", cmd_str }; - return std.posix.execveZ(shell_z, &args, std.c.environ); + _ = std.posix.system.execve(shell_z, &args, std.c.environ); + return error.CmdExecveFailed; } -fn redirectStandardStreams(global_log_file: *LogFile, session_log: []const u8, create: bool) !std.fs.File { +fn redirectStandardStreams(global_log_file: *LogFile, io: std.Io, session_log: []const u8, create: bool) !std.Io.File { create_session_log_dir: { - const session_log_dir = std.fs.path.dirname(session_log) orelse break :create_session_log_dir; - std.fs.cwd().makePath(session_log_dir) catch |err| { - try global_log_file.err("auth/sys", "failed to create session log file directory: {s}", .{@errorName(err)}); + const session_log_dir = std.Io.Dir.path.dirname(session_log) orelse break :create_session_log_dir; + std.Io.Dir.cwd().createDirPath(io, session_log_dir) catch |err| { + try global_log_file.err(io, "auth/sys", "failed to create session log file directory: {s}", .{@errorName(err)}); return err; }; } - const log_file = if (create) (std.fs.cwd().createFile(session_log, .{ .mode = 0o666 }) catch |err| { - try global_log_file.err("auth/sys", "failed to create new session log file: {s}", .{@errorName(err)}); + const log_file = if (create) (std.Io.Dir.cwd().createFile(io, session_log, .{ .permissions = .fromMode(0o666) }) catch |err| { + try global_log_file.err(io, "auth/sys", "failed to create new session log file: {s}", .{@errorName(err)}); return err; - }) else (std.fs.cwd().openFile(session_log, .{ .mode = .read_write }) catch |err| { - try global_log_file.err("auth/sys", "failed to open existing session log file: {s}", .{@errorName(err)}); + }) else (std.Io.Dir.cwd().openFile(io, session_log, .{ .mode = .read_write }) catch |err| { + try global_log_file.err(io, "auth/sys", "failed to open existing session log file: {s}", .{@errorName(err)}); return err; }); - try std.posix.dup2(std.posix.STDOUT_FILENO, std.posix.STDERR_FILENO); - try std.posix.dup2(log_file.handle, std.posix.STDOUT_FILENO); + if (interop.isError(std.posix.system.dup2(std.posix.STDOUT_FILENO, std.posix.STDERR_FILENO))) return error.StdoutDup2Failed; + if (interop.isError(std.posix.system.dup2(log_file.handle, std.posix.STDOUT_FILENO))) return error.LogFileDup2Failed; return log_file; } -fn addUtmpEntry(entry: *Utmp, username: []const u8, pid: c_int) !void { +fn addUtmpEntry(io: std.Io, entry: *Utmp, username: []const u8, pid: c_int) !void { entry.ut_type = utmp.USER_PROCESS; entry.ut_pid = pid; - var buf: [std.fs.max_path_bytes]u8 = undefined; - const tty_path = try std.os.getFdPath(std.posix.STDIN_FILENO, &buf); + var buf: [std.Io.Dir.max_path_bytes]u8 = undefined; + const length = try std.Io.File.stdin().realPath(io, &buf); + const tty_path = buf[0..length]; // Get the TTY name (i.e. without the /dev/ prefix) var ttyname_buf: [@sizeOf(@TypeOf(entry.ut_line))]u8 = undefined; diff --git a/src/components/InfoLine.zig b/src/components/InfoLine.zig index 7a1a06b..97a8a73 100644 --- a/src/components/InfoLine.zig +++ b/src/components/InfoLine.zig @@ -23,6 +23,7 @@ label: *MessageLabel, pub fn init( allocator: Allocator, + io: std.Io, buffer: *TerminalBuffer, width: usize, arrow_fg: u32, @@ -32,6 +33,7 @@ pub fn init( .instance = null, .label = try MessageLabel.init( allocator, + io, buffer, drawItem, null, diff --git a/src/components/Session.zig b/src/components/Session.zig index 1abf9b9..1e975c6 100644 --- a/src/components/Session.zig +++ b/src/components/Session.zig @@ -24,6 +24,7 @@ user_list: *UserList, pub fn init( allocator: Allocator, + io: std.Io, buffer: *TerminalBuffer, user_list: *UserList, width: usize, @@ -35,6 +36,7 @@ pub fn init( .instance = null, .label = try EnvironmentLabel.init( allocator, + io, buffer, drawItem, sessionChanged, diff --git a/src/components/UserList.zig b/src/components/UserList.zig index 2131cae..c407173 100644 --- a/src/components/UserList.zig +++ b/src/components/UserList.zig @@ -26,6 +26,7 @@ label: *UserLabel, pub fn init( allocator: Allocator, + io: std.Io, buffer: *TerminalBuffer, usernames: StringList, saved_users: *SavedUsers, @@ -39,6 +40,7 @@ pub fn init( .instance = null, .label = try UserLabel.init( allocator, + io, buffer, drawItem, usernameChanged, diff --git a/src/config/custom.zig b/src/config/custom.zig index 28cded0..464316d 100644 --- a/src/config/custom.zig +++ b/src/config/custom.zig @@ -21,5 +21,5 @@ pub const CustomCommandInfo = struct { counter: u32 = 0, }; -pub var binds: std.StringArrayHashMap(CustomCommandBind) = undefined; -pub var labels: std.StringArrayHashMap(CustomCommandInfo) = undefined; +pub var binds: std.array_hash_map.String(CustomCommandBind) = undefined; +pub var labels: std.array_hash_map.String(CustomCommandInfo) = undefined; diff --git a/src/config/migrator.zig b/src/config/migrator.zig index 53e4011..366bc81 100644 --- a/src/config/migrator.zig +++ b/src/config/migrator.zig @@ -190,7 +190,7 @@ pub fn configFieldHandler(_: std.mem.Allocator, field: ini.IniField) ?ini.IniFie const key = field.header["cmd:".len..]; const keyZ = temporary_allocator.dupe(u8, key) catch ""; if (!custom.binds.contains(key)) { - custom.binds.put(keyZ, .{}) catch {}; + custom.binds.put(temporary_allocator, keyZ, .{}) catch {}; } if (custom.binds.getPtr(keyZ)) |command| { if (std.mem.eql(u8, field.key, "name")) { @@ -206,7 +206,7 @@ pub fn configFieldHandler(_: std.mem.Allocator, field: ini.IniField) ?ini.IniFie const key = field.header["lbl:".len..]; const keyZ = temporary_allocator.dupe(u8, key) catch ""; if (!custom.labels.contains(keyZ)) { - custom.labels.put(keyZ, .{ .name = keyZ }) catch {}; + custom.labels.put(temporary_allocator, keyZ, .{ .name = keyZ }) catch {}; } if (custom.labels.getPtr(keyZ)) |label| { if (std.mem.eql(u8, field.key, "cmd")) { @@ -254,12 +254,12 @@ pub fn lateConfigFieldHandler(config: *Config) void { } } -pub fn tryMigrateIniSaveFile(allocator: std.mem.Allocator, path: []const u8, saved_users: *SavedUsers, usernames: [][]const u8) !?IniParser(OldSave) { - var save_parser = try IniParser(OldSave).init(allocator, path, null); +pub fn tryMigrateIniSaveFile(allocator: std.mem.Allocator, io: std.Io, path: []const u8, saved_users: *SavedUsers, usernames: [][]const u8) !?IniParser(OldSave) { + var save_parser = try IniParser(OldSave).init(allocator, io, path, null); errdefer save_parser.deinit(); var user_buf: [32]u8 = undefined; - const maybe_save = if (save_parser.maybe_load_error == null) save_parser.structure else tryMigrateFirstSaveFile(&user_buf); + const maybe_save = if (save_parser.maybe_load_error == null) save_parser.structure else tryMigrateFirstSaveFile(io, &user_buf); if (maybe_save) |save| { // Add all other users to the list @@ -282,16 +282,16 @@ pub fn tryMigrateIniSaveFile(allocator: std.mem.Allocator, path: []const u8, sav return null; } -fn tryMigrateFirstSaveFile(user_buf: *[32]u8) ?OldSave { +fn tryMigrateFirstSaveFile(io: std.Io, user_buf: *[32]u8) ?OldSave { if (maybe_save_file) |path| { defer temporary_allocator.free(path); var save = OldSave{}; - var file = std.fs.openFileAbsolute(path, .{}) catch return null; - defer file.close(); + var file = std.Io.Dir.openFileAbsolute(io, path, .{}) catch return null; + defer file.close(io); var file_buffer: [64]u8 = undefined; - var file_reader = file.reader(&file_buffer); + var file_reader = file.reader(io, &file_buffer); var reader = &file_reader.interface; var user_writer = std.Io.Writer.fixed(user_buf); diff --git a/src/main.zig b/src/main.zig index bd9294c..ac112a2 100644 --- a/src/main.zig +++ b/src/main.zig @@ -46,21 +46,21 @@ const Entry = Environment.Entry; const ly_version_str = "Ly version " ++ build_options.version; var session_pid: std.posix.pid_t = -1; -fn signalHandler(i: c_int) callconv(.c) void { +fn signalHandler(sig: std.posix.SIG) callconv(.c) void { if (session_pid == 0) return; // Forward signal to session to clean up if (session_pid > 0) { - _ = std.c.kill(session_pid, i); + _ = std.c.kill(session_pid, sig); var status: c_int = 0; _ = std.c.waitpid(session_pid, &status, 0); } TerminalBuffer.shutdown(); - std.c.exit(i); + std.c.exit(@intCast(@intFromEnum(sig))); } -fn ttyControlTransferSignalHandler(_: c_int) callconv(.c) void { +fn ttyControlTransferSignalHandler(_: std.posix.SIG) callconv(.c) void { TerminalBuffer.shutdown(); } @@ -68,6 +68,7 @@ const CustomBindLabel = struct { cmd: custom.CustomCommandBind, key: []const u8, lbl: Label, + io: std.Io, }; const CustomInfoLabel = struct { @@ -77,6 +78,7 @@ const CustomInfoLabel = struct { const UiState = struct { allocator: Allocator, + io: std.Io, auth_fails: u64, is_autologin: bool, use_kmscon_vt: bool, @@ -128,24 +130,26 @@ const UiState = struct { var shutdown = false; var restart = false; -pub fn main() !void { +pub fn main(init: std.process.Init) !void { var shutdown_cmd: []const u8 = undefined; var restart_cmd: []const u8 = undefined; var commands_allocated = false; var state: UiState = undefined; + state.io = init.io; + var stderr_buffer: [128]u8 = undefined; - var stderr_writer = std.fs.File.stderr().writer(&stderr_buffer); + var stderr_writer = std.Io.File.stderr().writer(state.io, &stderr_buffer); var stderr = &stderr_writer.interface; defer { // If we can't shutdown or restart due to an error, we print it to standard error. If that fails, just bail out if (shutdown) { - const shutdown_error = std.process.execv(temporary_allocator, &[_][]const u8{ "/bin/sh", "-c", shutdown_cmd }); + const shutdown_error = std.process.replace(state.io, .{ .argv = &[_][]const u8{ "/bin/sh", "-c", shutdown_cmd } }); stderr.print("error: couldn't shutdown: {s}\n", .{@errorName(shutdown_error)}) catch std.process.exit(1); stderr.flush() catch std.process.exit(1); } else if (restart) { - const restart_error = std.process.execv(temporary_allocator, &[_][]const u8{ "/bin/sh", "-c", restart_cmd }); + const restart_error = std.process.replace(state.io, .{ .argv = &[_][]const u8{ "/bin/sh", "-c", restart_cmd } }); stderr.print("error: couldn't restart: {s}\n", .{@errorName(restart_error)}) catch std.process.exit(1); stderr.flush() catch std.process.exit(1); } else { @@ -173,7 +177,7 @@ pub fn main() !void { var diag = clap.Diagnostic{}; var arg_parse_error: anyerror = undefined; - var maybe_res = clap.parse(clap.Help, ¶ms, clap.parsers.default, .{ .diagnostic = &diag, .allocator = state.allocator }) catch |err| parse_error: { + var maybe_res = clap.parse(clap.Help, ¶ms, clap.parsers.default, init.minimal.args, .{ .diagnostic = &diag, .allocator = state.allocator }) catch |err| parse_error: { arg_parse_error = err; diag.report(stderr, err) catch {}; try stderr.flush(); @@ -219,12 +223,12 @@ pub fn main() !void { state.allocator.free(state.old_save_path); }; - const config_path = try std.fs.path.join(state.allocator, &[_][]const u8{ config_parent_path, "config.ini" }); + const config_path = try std.Io.Dir.path.join(state.allocator, &[_][]const u8{ config_parent_path, "config.ini" }); defer state.allocator.free(config_path); - custom.binds = .init(state.allocator); - custom.labels = .init(state.allocator); - var config_parser = try IniParser(Config).init(state.allocator, config_path, migrator.configFieldHandler); + custom.binds = .empty; + custom.labels = .empty; + var config_parser = try IniParser(Config).init(state.allocator, state.io, config_path, migrator.configFieldHandler); defer config_parser.deinit(); defer if (!shutdown or !restart) { var iter = custom.binds.iterator(); @@ -233,14 +237,14 @@ pub fn main() !void { temporary_allocator.free(i.value_ptr.*.cmd); temporary_allocator.free(i.value_ptr.*.name); } - custom.binds.deinit(); + custom.binds.deinit(temporary_allocator); var labelIter = custom.labels.iterator(); while (labelIter.next()) |i| { temporary_allocator.free(i.key_ptr.*); if (i.value_ptr.cmd) |cmd| temporary_allocator.free(cmd); } - custom.labels.deinit(); + custom.labels.deinit(temporary_allocator); }; state.config = config_parser.structure; @@ -248,17 +252,17 @@ pub fn main() !void { var lang_buffer: [16]u8 = undefined; const lang_file = try std.fmt.bufPrint(&lang_buffer, "{s}.ini", .{state.config.lang}); - const lang_path = try std.fs.path.join(state.allocator, &[_][]const u8{ config_parent_path, "lang", lang_file }); + const lang_path = try std.Io.Dir.path.join(state.allocator, &[_][]const u8{ config_parent_path, "lang", lang_file }); defer state.allocator.free(lang_path); - var lang_parser = try IniParser(Lang).init(state.allocator, lang_path, null); + var lang_parser = try IniParser(Lang).init(state.allocator, state.io, lang_path, null); defer lang_parser.deinit(); state.lang = lang_parser.structure; if (state.config.save) { - state.save_path = try std.fs.path.join(state.allocator, &[_][]const u8{ config_parent_path, "save.txt" }); - state.old_save_path = try std.fs.path.join(state.allocator, &[_][]const u8{ config_parent_path, "save.ini" }); + state.save_path = try std.Io.Dir.path.join(state.allocator, &[_][]const u8{ config_parent_path, "save.txt" }); + state.old_save_path = try std.Io.Dir.path.join(state.allocator, &[_][]const u8{ config_parent_path, "save.ini" }); save_path_alloc = true; } @@ -267,7 +271,7 @@ pub fn main() !void { } var maybe_uid_range_error: ?anyerror = null; - var usernames = try getAllUsernames(state.allocator, state.config.login_defs_path, &maybe_uid_range_error); + var usernames = try getAllUsernames(state.allocator, state.io, state.config.login_defs_path, &maybe_uid_range_error); defer { for (usernames.items) |username| state.allocator.free(username); usernames.deinit(state.allocator); @@ -276,7 +280,7 @@ pub fn main() !void { state.has_old_save = false; if (state.config.save) read_save_file: { - old_save_parser = migrator.tryMigrateIniSaveFile(state.allocator, state.old_save_path, &state.saved_users, usernames.items) catch break :read_save_file; + old_save_parser = migrator.tryMigrateIniSaveFile(state.allocator, state.io, state.old_save_path, &state.saved_users, usernames.items) catch break :read_save_file; // Don't read the new save file if the old one still exists if (old_save_parser != null) { @@ -284,11 +288,11 @@ pub fn main() !void { break :read_save_file; } - var save_file = std.fs.cwd().openFile(state.save_path, .{}) catch break :read_save_file; - defer save_file.close(); + var save_file = std.Io.Dir.cwd().openFile(state.io, state.save_path, .{}) catch break :read_save_file; + defer save_file.close(state.io); var file_buffer: [256]u8 = undefined; - var file_reader = save_file.reader(&file_buffer); + var file_reader = save_file.reader(state.io, &file_buffer); var reader = &file_reader.interface; const last_username_index_str = reader.takeDelimiterInclusive('\n') catch break :read_save_file; @@ -327,10 +331,10 @@ pub fn main() !void { var log_file_buffer: [1024]u8 = undefined; - state.log_file = try LogFile.init(state.config.ly_log, &log_file_buffer); - defer state.log_file.deinit(); + state.log_file = try LogFile.init(state.io, state.config.ly_log, &log_file_buffer); + defer state.log_file.deinit(state.io); - try state.log_file.info("tui", "using {s} vt", .{if (state.use_kmscon_vt) "kmscon" else "default"}); + try state.log_file.info(state.io, "tui", "using {s} vt", .{if (state.use_kmscon_vt) "kmscon" else "default"}); // These strings only end up getting freed if the user quits Ly using Ctrl+C, which is fine since in the other cases // we end up shutting down or restarting the system @@ -338,25 +342,27 @@ pub fn main() !void { restart_cmd = try temporary_allocator.dupe(u8, state.config.restart_cmd); commands_allocated = true; - if (state.config.start_cmd) |start_cmd| { - var start = std.process.Child.init(&[_][]const u8{ "/bin/sh", "-c", start_cmd }, state.allocator); - start.stdout_behavior = .Inherit; - start.stderr_behavior = .Ignore; + if (state.config.start_cmd) |start_cmd| handle_start_cmd: { + var process = std.process.spawn(state.io, .{ + .argv = &[_][]const u8{ "/bin/sh", "-c", start_cmd }, + .stdout = .inherit, + .stderr = .ignore, + }) catch { + break :handle_start_cmd; + }; - handle_start_cmd: { - const process_result = start.spawnAndWait() catch { - break :handle_start_cmd; - }; - start_cmd_exit_code = process_result.Exited; - } + const process_result = process.wait(state.io) catch { + break :handle_start_cmd; + }; + start_cmd_exit_code = process_result.exited; } // Initialize terminal buffer - try state.log_file.info("tui", "initializing terminal buffer", .{}); + try state.log_file.info(state.io, "tui", "initializing terminal buffer", .{}); state.labels_max_length = @max(TerminalBuffer.strWidth(state.lang.login), TerminalBuffer.strWidth(state.lang.password)); var seed: u64 = undefined; - std.crypto.random.bytes(std.mem.asBytes(&seed)); // Get a random seed for the PRNG (used by animations) + state.io.random(std.mem.asBytes(&seed)); // Get a random seed for the PRNG (used by animations) var prng = std.Random.DefaultPrng.init(seed); const random = prng.random(); @@ -370,12 +376,13 @@ pub fn main() !void { }; state.buffer = try TerminalBuffer.init( state.allocator, + state.io, buffer_options, &state.log_file, random, ); defer { - state.log_file.info("tui", "shutting down terminal buffer", .{}) catch {}; + state.log_file.info(state.io, "tui", "shutting down terminal buffer", .{}) catch {}; state.buffer.deinit(); } @@ -586,6 +593,7 @@ pub fn main() !void { state.info_line = try InfoLine.init( state.allocator, + state.io, &state.buffer, state.box.width - 2 * state.box.horizontal_margin, state.buffer.fg, @@ -593,8 +601,8 @@ pub fn main() !void { ); defer state.info_line.deinit(); - try state.buffer.registerKeybind(&state.info_line.label.keybinds, "H", &viGoLeft, &state); - try state.buffer.registerKeybind(&state.info_line.label.keybinds, "L", &viGoRight, &state); + try state.buffer.registerKeybind(state.io, &state.info_line.label.keybinds, "H", &viGoLeft, &state); + try state.buffer.registerKeybind(state.io, &state.info_line.label.keybinds, "L", &viGoRight, &state); if (maybe_res == null) { var longest = diag.name.longest(); @@ -607,6 +615,7 @@ pub fn main() !void { state.config.error_fg, ); try state.log_file.err( + state.io, "cli", "unable to parse argument '{s}{s}': {s}", .{ longest.kind.prefix(), longest.name, @errorName(arg_parse_error) }, @@ -620,6 +629,7 @@ pub fn main() !void { state.config.error_fg, ); try state.log_file.err( + state.io, "sys", "failed to get uid range: {s}; falling back to default", .{@errorName(err)}, @@ -633,6 +643,7 @@ pub fn main() !void { state.config.error_fg, ); try state.log_file.err( + state.io, "sys", "failed to execute start command: exit code {d}", .{start_cmd_exit_code}, @@ -647,6 +658,7 @@ pub fn main() !void { state.config.error_fg, ); try state.log_file.err( + state.io, "conf", "unable to parse config file: {s}", .{@errorName(load_error)}, @@ -654,6 +666,7 @@ pub fn main() !void { for (config_parser.errors.items) |err| { try state.log_file.err( + state.io, "conf", "failed to convert value '{s}' of option '{s}' to type '{s}': {s}", .{ err.value, err.key, err.type_name, err.error_name }, @@ -668,6 +681,7 @@ pub fn main() !void { state.config.error_fg, ); try state.log_file.err( + state.io, "sys", "failed to open log file", .{}, @@ -681,6 +695,7 @@ pub fn main() !void { state.config.error_fg, ); try state.log_file.err( + state.io, "sys", "failed to set numlock: {s}", .{@errorName(err)}, @@ -699,6 +714,7 @@ pub fn main() !void { state.session = try Session.init( state.allocator, + state.io, &state.buffer, &state.login, state.box.width - 2 * state.box.horizontal_margin - state.labels_max_length - 1, @@ -708,8 +724,8 @@ pub fn main() !void { ); defer state.session.deinit(); - try state.buffer.registerKeybind(&state.session.label.keybinds, "H", &viGoLeft, &state); - try state.buffer.registerKeybind(&state.session.label.keybinds, "L", &viGoRight, &state); + try state.buffer.registerKeybind(state.io, &state.session.label.keybinds, "H", &viGoLeft, &state); + try state.buffer.registerKeybind(state.io, &state.session.label.keybinds, "L", &viGoRight, &state); state.login_label = Label.init( state.lang.login, @@ -723,6 +739,7 @@ pub fn main() !void { state.login = try UserList.init( state.allocator, + state.io, &state.buffer, usernames, &state.saved_users, @@ -734,8 +751,8 @@ pub fn main() !void { ); defer state.login.deinit(); - try state.buffer.registerKeybind(&state.login.label.keybinds, "H", &viGoLeft, &state); - try state.buffer.registerKeybind(&state.login.label.keybinds, "L", &viGoRight, &state); + try state.buffer.registerKeybind(state.io, &state.login.label.keybinds, "H", &viGoLeft, &state); + try state.buffer.registerKeybind(state.io, &state.login.label.keybinds, "L", &viGoRight, &state); if (state.config.shell) { addOtherEnvironment(&state.session, state.lang, .shell, null) catch |err| { @@ -745,6 +762,7 @@ pub fn main() !void { state.config.error_fg, ); try state.log_file.err( + state.io, "sys", "failed to add shell environment: {s}", .{@errorName(err)}, @@ -761,6 +779,7 @@ pub fn main() !void { state.config.error_fg, ); try state.log_file.err( + state.io, "sys", "failed to add xinitrc environment: {s}", .{@errorName(err)}, @@ -774,6 +793,7 @@ pub fn main() !void { state.config.fg, ); try state.log_file.info( + state.io, "comp", "x11 support disabled at compile-time", .{}, @@ -786,9 +806,10 @@ pub fn main() !void { if (state.config.waylandsessions) |waylandsessions| { var wayland_session_dirs = std.mem.splitScalar(u8, waylandsessions, ':'); while (wayland_session_dirs.next()) |dir| { - crawl(&state.session, state.lang, dir, .wayland) catch |err| { + crawl(&state.session, state.io, state.lang, dir, .wayland) catch |err| { has_crawl_error = true; try state.log_file.err( + state.io, "sys", "failed to crawl wayland session directory '{s}': {s}", .{ dir, @errorName(err) }, @@ -801,9 +822,10 @@ pub fn main() !void { if (state.config.xsessions) |xsessions| { var x_session_dirs = std.mem.splitScalar(u8, xsessions, ':'); while (x_session_dirs.next()) |dir| { - crawl(&state.session, state.lang, dir, .x11) catch |err| { + crawl(&state.session, state.io, state.lang, dir, .x11) catch |err| { has_crawl_error = true; try state.log_file.err( + state.io, "sys", "failed to crawl x11 session directory '{s}': {s}", .{ dir, @errorName(err) }, @@ -815,9 +837,10 @@ pub fn main() !void { var custom_session_dirs = std.mem.splitScalar(u8, state.config.custom_sessions, ':'); while (custom_session_dirs.next()) |dir| { - crawl(&state.session, state.lang, dir, .custom) catch |err| { + crawl(&state.session, state.io, state.lang, dir, .custom) catch |err| { has_crawl_error = true; try state.log_file.err( + state.io, "sys", "failed to crawl custom session directory '{s}': {s}", .{ dir, @errorName(err) }, @@ -839,7 +862,7 @@ pub fn main() !void { // accounts *and* no root account...but at this point, if that's the // case, you have bigger problems to deal with in the first place. :D try state.info_line.addMessage(state.lang.err_no_users, state.config.error_bg, state.config.error_fg); - try state.log_file.err("sys", "no users found", .{}); + try state.log_file.err(state.io, "sys", "no users found", .{}); } state.password_label = Label.init( @@ -856,6 +879,7 @@ pub fn main() !void { state.password = try Text.init( state.allocator, + state.io, &state.buffer, state.insert_mode, true, @@ -866,8 +890,8 @@ pub fn main() !void { ); defer state.password.deinit(); - try state.buffer.registerKeybind(&state.password.keybinds, "H", &viGoLeft, &state); - try state.buffer.registerKeybind(&state.password.keybinds, "L", &viGoRight, &state); + try state.buffer.registerKeybind(state.io, &state.password.keybinds, "H", &viGoLeft, &state); + try state.buffer.registerKeybind(state.io, &state.password.keybinds, "L", &viGoRight, &state); state.password_widget = state.password.widget(); @@ -894,6 +918,7 @@ pub fn main() !void { state.config.error_fg, ); try state.log_file.err( + state.io, "auth", "autologin failed: username '{s}' not found", .{auto_user}, @@ -903,6 +928,7 @@ pub fn main() !void { const session_index = findSessionByName(&state.session, auto_session) orelse { try state.log_file.err( + state.io, "auth", "autologin failed: session '{s}' not found", .{auto_session}, @@ -915,6 +941,7 @@ pub fn main() !void { break :check_autologin; }; try state.log_file.info( + state.io, "auth", "attempting autologin for user '{s}' with session '{s}'", .{ auto_user, auto_session }, @@ -931,13 +958,14 @@ pub fn main() !void { } // Switch to selected TTY - state.active_tty = interop.getActiveTty(state.allocator, state.use_kmscon_vt) catch |err| no_tty_found: { + state.active_tty = interop.getActiveTty(state.allocator, state.io, state.use_kmscon_vt) catch |err| no_tty_found: { try state.info_line.addMessage( state.lang.err_get_active_tty, state.config.error_bg, state.config.error_fg, ); try state.log_file.err( + state.io, "sys", "failed to get active tty: {s}", .{@errorName(err)}, @@ -952,6 +980,7 @@ pub fn main() !void { state.config.error_fg, ); try state.log_file.err( + state.io, "sys", "failed to switch to tty {d}: {s}", .{ state.active_tty, @errorName(err) }, @@ -1025,6 +1054,7 @@ pub fn main() !void { .dur_file => { var dur = try DurFile.init( state.allocator, + state.io, &state.buffer, &state.log_file, state.config.dur_file_path, @@ -1042,6 +1072,7 @@ pub fn main() !void { defer if (animation) |a| a.deinit(); var cascade = Cascade.init( + state.io, &state.buffer, &state.auth_fails, state.config.auth_fails, @@ -1140,6 +1171,7 @@ pub fn main() !void { ), .cmd = i.value_ptr.*, .key = i.key_ptr.*, + .io = state.io, }); state.custom_binds.items[state.custom_binds.items.len - 1].lbl.allocator = state.allocator; } @@ -1208,26 +1240,26 @@ pub fn main() !void { } for (state.custom_binds.items) |*item| { - try state.buffer.registerGlobalKeybind(item.key, &customCommand, item); + try state.buffer.registerGlobalKeybind(state.io, item.key, &customCommand, item); } - try state.buffer.registerGlobalKeybind("Esc", &disableInsertMode, &state); - try state.buffer.registerGlobalKeybind("I", &enableInsertMode, &state); + try state.buffer.registerGlobalKeybind(state.io, "Esc", &disableInsertMode, &state); + try state.buffer.registerGlobalKeybind(state.io, "I", &enableInsertMode, &state); - try state.buffer.registerGlobalKeybind("Ctrl+C", &quit, &state); + try state.buffer.registerGlobalKeybind(state.io, "Ctrl+C", &quit, &state); - try state.buffer.registerGlobalKeybind("K", &viMoveCursorUp, &state); - try state.buffer.registerGlobalKeybind("J", &viMoveCursorDown, &state); + try state.buffer.registerGlobalKeybind(state.io, "K", &viMoveCursorUp, &state); + try state.buffer.registerGlobalKeybind(state.io, "J", &viMoveCursorDown, &state); - try state.buffer.registerGlobalKeybind("Enter", &authenticate, &state); + try state.buffer.registerGlobalKeybind(state.io, "Enter", &authenticate, &state); - try state.buffer.registerGlobalKeybind(state.config.shutdown_key, &shutdownCmd, &state); - try state.buffer.registerGlobalKeybind(state.config.restart_key, &restartCmd, &state); - try state.buffer.registerGlobalKeybind(state.config.show_password_key, &togglePasswordMask, &state); - if (state.config.sleep_cmd != null) try state.buffer.registerGlobalKeybind(state.config.sleep_key, &sleepCmd, &state); - if (state.config.hibernate_cmd != null) try state.buffer.registerGlobalKeybind(state.config.hibernate_key, &hibernateCmd, &state); - if (state.config.brightness_down_key) |key| try state.buffer.registerGlobalKeybind(key, &decreaseBrightnessCmd, &state); - if (state.config.brightness_up_key) |key| try state.buffer.registerGlobalKeybind(key, &increaseBrightnessCmd, &state); + try state.buffer.registerGlobalKeybind(state.io, state.config.shutdown_key, &shutdownCmd, &state); + try state.buffer.registerGlobalKeybind(state.io, state.config.restart_key, &restartCmd, &state); + try state.buffer.registerGlobalKeybind(state.io, state.config.show_password_key, &togglePasswordMask, &state); + if (state.config.sleep_cmd != null) try state.buffer.registerGlobalKeybind(state.io, state.config.sleep_key, &sleepCmd, &state); + if (state.config.hibernate_cmd != null) try state.buffer.registerGlobalKeybind(state.io, state.config.hibernate_key, &hibernateCmd, &state); + if (state.config.brightness_down_key) |key| try state.buffer.registerGlobalKeybind(state.io, key, &decreaseBrightnessCmd, &state); + if (state.config.brightness_up_key) |key| try state.buffer.registerGlobalKeybind(state.io, key, &increaseBrightnessCmd, &state); if (state.config.initial_info_text) |text| { try state.info_line.addMessage(text, state.config.bg, state.config.fg); @@ -1241,6 +1273,7 @@ pub fn main() !void { state.config.error_fg, ); try state.log_file.err( + state.io, "sys", "failed to get hostname: {s}", .{@errorName(err)}, @@ -1268,6 +1301,7 @@ pub fn main() !void { try state.buffer.runEventLoop( state.allocator, + state.io, shared_error, widgets.items, active_widget, @@ -1329,31 +1363,31 @@ fn enableInsertMode(ptr: *anyopaque) !bool { } fn viGoLeft(ptr: *anyopaque) !bool { - var self: *UiState = @ptrCast(@alignCast(ptr)); - if (self.insert_mode) return true; + var state: *UiState = @ptrCast(@alignCast(ptr)); + if (state.insert_mode) return true; - return try self.buffer.simulateKeybind("Left"); + return try state.buffer.simulateKeybind(state.io, "Left"); } fn viGoRight(ptr: *anyopaque) !bool { var state: *UiState = @ptrCast(@alignCast(ptr)); if (state.insert_mode) return true; - return try state.buffer.simulateKeybind("Right"); + return try state.buffer.simulateKeybind(state.io, "Right"); } fn viMoveCursorUp(ptr: *anyopaque) !bool { var state: *UiState = @ptrCast(@alignCast(ptr)); if (state.insert_mode) return true; - return try state.buffer.simulateKeybind("Up"); + return try state.buffer.simulateKeybind(state.io, "Up"); } fn viMoveCursorDown(ptr: *anyopaque) !bool { var state: *UiState = @ptrCast(@alignCast(ptr)); if (state.insert_mode) return true; - return try state.buffer.simulateKeybind("Down"); + return try state.buffer.simulateKeybind(state.io, "Down"); } fn togglePasswordMask(ptr: *anyopaque) !bool { @@ -1373,18 +1407,21 @@ fn quit(ptr: *anyopaque) !bool { fn customCommand(ptr: *anyopaque) !bool { const lbl: *CustomBindLabel = @ptrCast(@alignCast(ptr)); - var proc = std.process.Child.init(&[_][]const u8{ "/bin/sh", "-c", lbl.cmd.cmd }, lbl.lbl.allocator.?); - proc.stdout_behavior = .Ignore; - proc.stderr_behavior = .Ignore; - const res = proc.spawnAndWait() catch return false; - if (res.Exited != 0) return error.CommandFailed; + var proc = std.process.spawn(lbl.io, .{ + .argv = &[_][]const u8{ "/bin/sh", "-c", lbl.cmd.cmd }, + .stdout = .ignore, + .stderr = .ignore, + }) catch return false; + + const res = proc.wait(lbl.io) catch return false; + if (res.exited != 0) return error.CommandFailed; return false; } fn authenticate(ptr: *anyopaque) !bool { var state: *UiState = @ptrCast(@alignCast(ptr)); - try state.log_file.info("auth", "starting authentication", .{}); + try state.log_file.info(state.io, "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 @@ -1400,6 +1437,7 @@ fn authenticate(ptr: *anyopaque) !bool { state.config.error_fg, ); try state.log_file.err( + state.io, "tui", "failed to clear info line: {s}", .{@errorName(err)}, @@ -1422,6 +1460,7 @@ fn authenticate(ptr: *anyopaque) !bool { state.config.error_fg, ); try state.log_file.err( + state.io, "tui", "failed to clear info line: {s}", .{@errorName(err)}, @@ -1435,23 +1474,25 @@ fn authenticate(ptr: *anyopaque) !bool { // handling, so let's just report a generic error message, // that should be good enough for debugging anyway. errdefer state.log_file.err( + state.io, "conf", "failed to save current user data", .{}, ) catch {}; - var file = std.fs.cwd().createFile(state.save_path, .{}) catch |err| { + var file = std.Io.Dir.cwd().createFile(state.io, state.save_path, .{}) catch |err| { state.log_file.err( + state.io, "sys", "failed to create save file: {s}", .{@errorName(err)}, ) catch break :save_last_settings; break :save_last_settings; }; - defer file.close(); + defer file.close(state.io); var file_buffer: [256]u8 = undefined; - var file_writer = file.writer(&file_buffer); + var file_writer = file.writer(state.io, &file_buffer); var writer = &file_writer.interface; try writer.print("{d}\n", .{state.login.label.current}); @@ -1462,9 +1503,9 @@ fn authenticate(ptr: *anyopaque) !bool { // Delete previous save file if it exists if (migrator.maybe_save_file) |path| { - std.fs.cwd().deleteFile(path) catch {}; + std.Io.Dir.cwd().deleteFile(state.io, path) catch {}; } else if (state.has_old_save) { - std.fs.cwd().deleteFile(state.old_save_path) catch {}; + std.Io.Dir.cwd().deleteFile(state.io, state.old_save_path) catch {}; } } @@ -1472,9 +1513,9 @@ fn authenticate(ptr: *anyopaque) !bool { defer shared_err.deinit(); { - state.log_file.deinit(); + state.log_file.deinit(state.io); - session_pid = try std.posix.fork(); + session_pid = std.posix.system.fork(); if (session_pid == 0) { const current_environment = state.session.label.list.items[state.session.label.current].environment; @@ -1504,10 +1545,11 @@ fn authenticate(ptr: *anyopaque) !bool { }; std.posix.sigaction(std.posix.SIG.CHLD, &tty_control_transfer_act, null); - try state.log_file.reinit(); + try state.log_file.reinit(state.io); auth.authenticate( state.allocator, + state.io, &state.log_file, auth_options, current_environment, @@ -1516,21 +1558,22 @@ fn authenticate(ptr: *anyopaque) !bool { ) catch |err| { shared_err.writeError(err); - state.log_file.deinit(); + state.log_file.deinit(state.io); std.process.exit(1); }; - state.log_file.deinit(); + state.log_file.deinit(state.io); std.process.exit(0); } - _ = std.posix.waitpid(session_pid, 0); + var session_status: c_int = undefined; + _ = std.posix.system.waitpid(session_pid, &session_status, 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); + state.io.sleep(.fromSeconds(1), .real) catch {}; session_pid = -1; - try state.log_file.reinit(); + try state.log_file.reinit(state.io); } try state.buffer.reclaim(); @@ -1546,6 +1589,7 @@ fn authenticate(ptr: *anyopaque) !bool { state.config.error_fg, ); try state.log_file.err( + state.io, "auth", "failed to authenticate: {s}", .{@errorName(err)}, @@ -1553,9 +1597,11 @@ fn authenticate(ptr: *anyopaque) !bool { 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 .{}; + if (state.config.logout_cmd) |logout_cmd| execute_cmd: { + var process = std.process.spawn(state.io, .{ + .argv = &[_][]const u8{ "/bin/sh", "-c", logout_cmd }, + }) catch break :execute_cmd; + _ = process.wait(state.io) catch {}; } state.password.clear(); @@ -1565,7 +1611,7 @@ fn authenticate(ptr: *anyopaque) !bool { state.config.bg, state.config.fg, ); - try state.log_file.info("auth", "logged out", .{}); + try state.log_file.info(state.io, "auth", "logged out", .{}); } if (state.config.auth_fails == 0 or state.auth_fails < state.config.auth_fails) { @@ -1599,21 +1645,24 @@ 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; + var process = std.process.spawn(state.io, .{ + .argv = &[_][]const u8{ "/bin/sh", "-c", sleep_cmd }, + .stdout = .ignore, + .stderr = .ignore, + }) catch return false; - const process_result = sleep.spawnAndWait() catch return false; - if (process_result.Exited != 0) { + const process_result = process.wait(state.io) 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( + state.io, "sys", "failed to execute sleep command: exit code {d}", - .{process_result.Exited}, + .{process_result.exited}, ); } } @@ -1624,21 +1673,24 @@ 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; + var process = std.process.spawn(state.io, .{ + .argv = &[_][]const u8{ "/bin/sh", "-c", hibernate_cmd }, + .stdout = .ignore, + .stderr = .ignore, + }) catch return false; - const process_result = hibernate.spawnAndWait() catch return false; - if (process_result.Exited != 0) { + const process_result = process.wait(state.io) 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( + state.io, "sys", "failed to execute hibernate command: exit code {d}", - .{process_result.Exited}, + .{process_result.exited}, ); } } @@ -1648,13 +1700,14 @@ fn hibernateCmd(ptr: *anyopaque) !bool { fn decreaseBrightnessCmd(ptr: *anyopaque) !bool { var state: *UiState = @ptrCast(@alignCast(ptr)); - adjustBrightness(state.allocator, state.config.brightness_down_cmd) catch |err| { + adjustBrightness(state.io, 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( + state.io, "sys", "failed to decrease brightness: {s}", .{@errorName(err)}, @@ -1666,13 +1719,14 @@ fn decreaseBrightnessCmd(ptr: *anyopaque) !bool { fn increaseBrightnessCmd(ptr: *anyopaque) !bool { var state: *UiState = @ptrCast(@alignCast(ptr)); - adjustBrightness(state.allocator, state.config.brightness_up_cmd) catch |err| { + adjustBrightness(state.io, 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( + state.io, "sys", "failed to increase brightness: {s}", .{@errorName(err)}, @@ -1692,6 +1746,7 @@ fn updateNumlock(self: *Label, ptr: *anyopaque) !void { state.config.error_fg, ); try state.log_file.err( + state.io, "sys", "failed to get lock state: {s}", .{@errorName(err)}, @@ -1708,7 +1763,7 @@ fn updateCapslock(self: *Label, ptr: *anyopaque) !void { const lock_state = interop.getLockState() catch |err| { self.update_fn = null; try state.info_line.addMessage(state.lang.err_lock_state, state.config.error_bg, state.config.error_fg); - try state.log_file.err("sys", "failed to get lock state: {s}", .{@errorName(err)}); + try state.log_file.err(state.io, "sys", "failed to get lock state: {s}", .{@errorName(err)}); return; }; @@ -1719,9 +1774,10 @@ fn updateBattery(self: *Label, ptr: *anyopaque) !void { var state: *UiState = @ptrCast(@alignCast(ptr)); if (state.config.battery_id) |id| { - const battery_percentage = getBatteryPercentage(id) catch |err| { + const battery_percentage = getBatteryPercentage(state.io, id) catch |err| { self.update_fn = null; try state.log_file.err( + state.io, "sys", "failed to get battery percentage: {s}", .{@errorName(err)}, @@ -1746,7 +1802,7 @@ fn updateClock(self: *Label, ptr: *anyopaque) !void { var state: *UiState = @ptrCast(@alignCast(ptr)); if (state.config.clock) |clock| draw_clock: { - const clock_str = interop.timeAsString(&state.clock_buf, clock); + const clock_str = interop.timeAsString(state.io, &state.clock_buf, clock); if (clock_str.len == 0) { self.update_fn = null; @@ -1756,6 +1812,7 @@ fn updateClock(self: *Label, ptr: *anyopaque) !void { state.config.error_fg, ); try state.log_file.err( + state.io, "tui", "clock string too long", .{}, @@ -1770,11 +1827,7 @@ fn updateClock(self: *Label, ptr: *anyopaque) !void { fn updateCustomInfo(lbl: *Label, ptr: *anyopaque) !void { const state: *UiState = @ptrCast(@alignCast(ptr)); const wid = lbl.widget().id; - var stdout = std.ArrayList(u8).empty; - defer stdout.deinit(state.allocator); - var stderr = std.ArrayList(u8).empty; - defer stderr.deinit(state.allocator); for (state.custom_info.items) |*i| { if (i.info.id != wid) continue; // Here, a counter ticks down every time `updateCustomInfo` runs on that @@ -1782,37 +1835,40 @@ fn updateCustomInfo(lbl: *Label, ptr: *anyopaque) !void { // once it reaches to 1. If a refresh value is defined it's then reset to // that refresh value. if (i.info.counter == 1) { - var c = std.process.Child.init(&[_][]const u8{ "/bin/sh", "-c", i.info.cmd orelse custom.UNDEFINED_CMD }, state.allocator); - c.stderr_behavior = .Pipe; - c.stdout_behavior = .Pipe; - try c.spawn(); + var c = try std.process.spawn(state.io, .{ + .argv = &[_][]const u8{ "/bin/sh", "-c", i.info.cmd orelse custom.UNDEFINED_CMD }, + .stdout = .pipe, + .stderr = .pipe, + }); - c.collectOutput(state.allocator, &stdout, &stderr, state.buffer.width) catch { - try stdout.print(state.allocator, "{s}: [{s}]", .{ i.info.name, state.lang.custom_info_err_output_long }); + var stdout_buffer: [1024]u8 = undefined; + var stdout_file_reader = c.stdout.?.reader(state.io, &stdout_buffer); + + const stdout = stdout_file_reader.interface.allocRemaining(state.allocator, .limited(state.buffer.width)) catch alloc_error: { + break :alloc_error try std.fmt.allocPrint(state.allocator, "{s}: [{s}]", .{ i.info.name, state.lang.custom_info_err_output_long }); }; + defer state.allocator.free(stdout); - const newlineIdx = std.mem.indexOfAny(u8, stdout.items, "\n"); - if (newlineIdx) |idx| { - stdout.shrinkAndFree(state.allocator, idx); - } + var cur_stdout = stdout; + const newline_index = std.mem.indexOfAny(u8, stdout, "\n"); + if (newline_index) |idx| cur_stdout = stdout[0..idx]; - if (stdout.items.len > state.buffer.width) { - stdout.clearRetainingCapacity(); - try stdout.print(state.allocator, "{s}: [{s}]", .{ i.info.name, state.lang.custom_info_err_output_long }); - } - - _ = try c.wait(); + _ = try c.wait(state.io); // Sometimes, the output of a command would have an unprintable character at // the end of its output, causing '�' (U+FFFD) to appear in its place. Here, we check // if this is the case and remove it. - if (stdout.items.len != 0 and !std.ascii.isPrint(stdout.items[stdout.items.len - 1])) { - _ = stdout.pop(); - } else if (stdout.items.len == 0) { - try stdout.print(state.allocator, "{s}: [{s}{s}]", .{ i.info.name, state.lang.custom_info_err_no_output, if (stderr.items.len > 0) state.lang.custom_info_err_no_output_error else "" }); + if (cur_stdout.len != 0 and !std.ascii.isPrint(cur_stdout[cur_stdout.len - 1])) { + cur_stdout = cur_stdout[0 .. cur_stdout.len - 1]; } + state.allocator.free(lbl.text); - try lbl.setTextAlloc(state.allocator, "{s}", .{stdout.items}); + if (cur_stdout.len == 0) { + const stderr_length = try c.stderr.?.length(state.io); + try lbl.setTextAlloc(state.allocator, "{s}: [{s}{s}]", .{ i.info.name, state.lang.custom_info_err_no_output, if (stderr_length > 0) state.lang.custom_info_err_no_output_error else "" }); + } else { + try lbl.setTextAlloc(state.allocator, "{s}", .{cur_stdout}); + } // Called to re-position the widgets after they receive their output. try positionWidgets(state); @@ -1851,7 +1907,7 @@ fn updateBigClock(self: *BigLabel, ptr: *anyopaque) !void { }, ); - const clock_str = interop.timeAsString(&state.bigclock_buf, format); + const clock_str = interop.timeAsString(state.io, &state.bigclock_buf, format); self.setText(clock_str); } @@ -2015,27 +2071,28 @@ fn positionWidgets(ptr: *anyopaque) !void { fn handleInactivity(ptr: *anyopaque) !void { var state: *UiState = @ptrCast(@alignCast(ptr)); - if (state.config.inactivity_cmd) |inactivity_cmd| { - var inactivity = std.process.Child.init(&[_][]const u8{ "/bin/sh", "-c", inactivity_cmd }, state.allocator); - inactivity.stdout_behavior = .Ignore; - inactivity.stderr_behavior = .Ignore; + if (state.config.inactivity_cmd) |inactivity_cmd| handle_inactivity_cmd: { + var process = std.process.spawn(state.io, .{ + .argv = &[_][]const u8{ "/bin/sh", "-c", inactivity_cmd }, + .stdout = .ignore, + .stderr = .ignore, + }) catch break :handle_inactivity_cmd; - handle_inactivity_cmd: { - const process_result = inactivity.spawnAndWait() catch { - break :handle_inactivity_cmd; - }; - if (process_result.Exited != 0) { - try state.info_line.addMessage( - state.lang.err_inactivity, - state.config.error_bg, - state.config.error_fg, - ); - try state.log_file.err( - "sys", - "failed to execute inactivity command: exit code {d}", - .{process_result.Exited}, - ); - } + const process_result = process.wait(state.io) catch { + break :handle_inactivity_cmd; + }; + if (process_result.exited != 0) { + try state.info_line.addMessage( + state.lang.err_inactivity, + state.config.error_bg, + state.config.error_fg, + ); + try state.log_file.err( + state.io, + "sys", + "failed to execute inactivity command: exit code {d}", + .{process_result.exited}, + ); } } } @@ -2059,26 +2116,26 @@ fn addOtherEnvironment(session: *Session, lang: Lang, display_server: DisplaySer }); } -fn crawl(session: *Session, lang: Lang, path: []const u8, display_server: DisplayServer) !void { - if (!std.fs.path.isAbsolute(path)) return error.PathNotAbsolute; +fn crawl(session: *Session, io: std.Io, lang: Lang, path: []const u8, display_server: DisplayServer) !void { + if (!std.Io.Dir.path.isAbsolute(path)) return error.PathNotAbsolute; - var iterable_directory = try std.fs.openDirAbsolute(path, .{ .iterate = true }); - defer iterable_directory.close(); + var iterable_directory = try std.Io.Dir.openDirAbsolute(io, path, .{ .iterate = true }); + defer iterable_directory.close(io); var iterator = iterable_directory.iterate(); - while (try iterator.next()) |item| { - if (!std.mem.eql(u8, std.fs.path.extension(item.name), ".desktop")) continue; + while (try iterator.next(io)) |item| { + if (!std.mem.eql(u8, std.Io.Dir.path.extension(item.name), ".desktop")) continue; const entry_path = try std.fmt.allocPrint(session.label.allocator, "{s}/{s}", .{ path, item.name }); defer session.label.allocator.free(entry_path); var entry_ini = Ini(Entry).init(session.label.allocator); - const data = try entry_ini.readFileToStruct(entry_path, .{ + const data = try entry_ini.readFileToStruct(io, entry_path, .{ .fieldHandler = null, .comment_characters = "#", }); errdefer entry_ini.deinit(); - const file_name = try session.label.allocator.dupe(u8, std.fs.path.stem(item.name)); + const file_name = try session.label.allocator.dupe(u8, std.Io.Dir.path.stem(item.name)); const entry = data.@"Desktop Entry"; var maybe_xdg_session_desktop: ?[]const u8 = null; var maybe_xdg_desktop_names: ?[]const u8 = null; @@ -2138,8 +2195,8 @@ fn findSessionByName(session: *Session, name: []const u8) ?usize { return null; } -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: { +fn getAllUsernames(allocator: Allocator, io: std.Io, login_defs_path: []const u8, uid_range_error: *?anyerror) !StringList { + const uid_range = interop.getUserIdRange(allocator, io, login_defs_path) catch |err| no_uid_range: { uid_range_error.* = err; break :no_uid_range UidRange{ .uid_min = build_options.fallback_uid_min, @@ -2185,29 +2242,31 @@ fn getAllUsernames(allocator: Allocator, login_defs_path: []const u8, uid_range_ return usernames; } -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; +fn adjustBrightness(io: std.Io, cmd: []const u8) !void { + var process = std.process.spawn(io, .{ + .argv = &[_][]const u8{ "/bin/sh", "-c", cmd }, + .stdout = .ignore, + .stderr = .ignore, + }) catch return; - const process_result = brightness.spawnAndWait() catch return; - if (process_result.Exited != 0) { + const process_result = process.wait(io) catch return; + if (process_result.exited != 0) { return error.BrightnessChangeFailed; } } -fn getBatteryPercentage(battery_id: []const u8) !u8 { +fn getBatteryPercentage(io: std.Io, battery_id: []const u8) !u8 { const path = try std.fmt.allocPrint(temporary_allocator, "/sys/class/power_supply/{s}/capacity", .{battery_id}); defer temporary_allocator.free(path); - const battery_file = try std.fs.cwd().openFile(path, .{}); - defer battery_file.close(); + const battery_file = try std.Io.Dir.cwd().openFile(io, path, .{}); + defer battery_file.close(io); var buffer: [8]u8 = undefined; - const bytes_read = try battery_file.read(&buffer); + const bytes_read = try battery_file.readStreaming(io, &.{&buffer}); const capacity_str = buffer[0..bytes_read]; - const trimmed = std.mem.trimRight(u8, capacity_str, "\n\r"); + const trimmed = std.mem.trimEnd(u8, capacity_str, "\n\r"); return try std.fmt.parseInt(u8, trimmed, 10); } From 4f45d92ea87315a32f1413c17d5b14c6fd1b7e9b Mon Sep 17 00:00:00 2001 From: RadsammyT Date: Sun, 26 Apr 2026 09:37:39 +0200 Subject: [PATCH 102/176] Fix: battery label positioning, custom keybinds not disappearing on `hide_key_hints = false` (#970) ## What are the changes about? Fixes battery label positioning in regards to custom binds. Since the label was on the top-left, it only accounted for the first line of built-in keybinds, and it didn't account for the other lines of custom ones. This also fixes the custom keybinds not disappearing on `hide_key_hints = false`, which is my bad. whoops. Also, with https://codeberg.org/fairyglade/ly/pulls/963 being a thing, we should probably think about deprecating this hardcoded battery label in favor of a custom label command, top-left by default. ## What existing issue does this resolve? N/A ## Pre-requisites - [x] I have tested & confirmed the changes work locally - [x] I have run `zig fmt` throughout my changes Reviewed-on: https://codeberg.org/fairyglade/ly/pulls/970 Reviewed-by: AnErrupTion --- src/main.zig | 52 ++++++++++++++++++++++++++++------------------------ 1 file changed, 28 insertions(+), 24 deletions(-) diff --git a/src/main.zig b/src/main.zig index ac112a2..53400df 100644 --- a/src/main.zig +++ b/src/main.zig @@ -1153,33 +1153,33 @@ pub fn main(init: std.process.Init) !void { }; var iter = custom.binds.iterator(); - while (iter.next()) |i| { - var concat = try std.mem.concat(state.allocator, u8, &[_][]const u8{ i.key_ptr.*, " ", i.value_ptr.name }); - inline for (@typeInfo(Lang).@"struct".fields) |lang_key| { - const new = try std.mem.replaceOwned(u8, state.allocator, concat, "$" ++ lang_key.name, @field(state.lang, lang_key.name)); - state.allocator.free(concat); - concat = new; - } - try state.custom_binds.append(state.allocator, .{ - .lbl = .init( - concat, - null, - state.buffer.fg, - state.buffer.bg, - null, - null, - ), - .cmd = i.value_ptr.*, - .key = i.key_ptr.*, - .io = state.io, - }); - state.custom_binds.items[state.custom_binds.items.len - 1].lbl.allocator = state.allocator; - } defer for (state.custom_binds.items) |*i| { i.lbl.deinit(); }; if (!state.config.hide_key_hints) { + while (iter.next()) |i| { + var concat = try std.mem.concat(state.allocator, u8, &[_][]const u8{ i.key_ptr.*, " ", i.value_ptr.name }); + inline for (@typeInfo(Lang).@"struct".fields) |lang_key| { + const new = try std.mem.replaceOwned(u8, state.allocator, concat, "$" ++ lang_key.name, @field(state.lang, lang_key.name)); + state.allocator.free(concat); + concat = new; + } + try state.custom_binds.append(state.allocator, .{ + .lbl = .init( + concat, + null, + state.buffer.fg, + state.buffer.bg, + null, + null, + ), + .cmd = i.value_ptr.*, + .key = i.key_ptr.*, + .io = state.io, + }); + state.custom_binds.items[state.custom_binds.items.len - 1].lbl.allocator = state.allocator; + } try layer2.append(state.allocator, state.shutdown_label.widget()); try layer2.append(state.allocator, state.restart_label.widget()); if (state.config.sleep_cmd != null) { @@ -1940,6 +1940,11 @@ fn updateSessionSpecifier(self: *Label, ptr: *anyopaque) !void { fn positionWidgets(ptr: *anyopaque) !void { var state: *UiState = @ptrCast(@alignCast(ptr)); + // Offsets for custom bind placement. Declared here instead of the + // below if stmt as we need these for `battery_label` positioning. + var x_offset: usize = 0; + // To account for the first row of built-in key hints + var y_offset: usize = 1; if (!state.config.hide_key_hints) { state.shutdown_label.positionX(state.edge_margin .add(TerminalBuffer.START_POSITION)); @@ -1973,8 +1978,6 @@ fn positionWidgets(ptr: *anyopaque) !void { state.brightness_up_label.positionXY(last_label .childrenPosition() .addX(1)); - var x_offset: usize = 0; - var y_offset: usize = 1; for (state.custom_binds.items) |*item| { item.lbl.positionXY(state.edge_margin .addY(y_offset) @@ -1998,6 +2001,7 @@ fn positionWidgets(ptr: *anyopaque) !void { state.battery_label.positionXY(state.edge_margin .add(TerminalBuffer.START_POSITION) .addYFromIf(state.brightness_up_label.childrenPosition(), !state.config.hide_key_hints) + .addYIf(y_offset, !state.config.hide_key_hints) .removeYFromIf(state.edge_margin, !state.config.hide_key_hints)); const tty_label_width = if (state.config.show_tty) TerminalBuffer.strWidth(state.tty_label.text) else 0; From 80d4b114f35e3966b1cce71b1414a84b917b6c67 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Sun, 26 Apr 2026 18:37:33 +0200 Subject: [PATCH 103/176] Fix 32-bit issues on Ly's side Signed-off-by: AnErrupTion --- build.zig | 2 +- ly-core/src/interop.zig | 2 +- src/animations/Cascade.zig | 8 ++++---- src/animations/DurFile.zig | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/build.zig b/build.zig index ff5ce4a..2aeac26 100644 --- a/build.zig +++ b/build.zig @@ -450,7 +450,7 @@ fn patchFile(allocator: std.mem.Allocator, io: std.Io, source_file: []const u8, var buffer: [4096]u8 = undefined; var reader = file.reader(io, &buffer); - var text = try reader.interface.readAlloc(allocator, stat.size); + var text = try reader.interface.readAlloc(allocator, @intCast(stat.size)); var iterator = patch_map.iterator(); while (iterator.next()) |kv| { diff --git a/ly-core/src/interop.zig b/ly-core/src/interop.zig index c580a49..5b2b954 100644 --- a/ly-core/src/interop.zig +++ b/ly-core/src/interop.zig @@ -266,7 +266,7 @@ pub fn supportsUnicode() bool { } pub fn timeAsString(io: std.Io, buf: [:0]u8, format: [:0]const u8) []u8 { - const timer = std.Io.Timestamp.now(io, .real).toSeconds(); + const timer: isize = @intCast(std.Io.Timestamp.now(io, .real).toSeconds()); const tm_info = time.localtime(&timer); const len = time.strftime(buf, buf.len, format, tm_info); diff --git a/src/animations/Cascade.zig b/src/animations/Cascade.zig index c1e88f1..587cd37 100644 --- a/src/animations/Cascade.zig +++ b/src/animations/Cascade.zig @@ -11,14 +11,14 @@ const Cascade = @This(); io: std.Io, instance: ?Widget = null, buffer: *TerminalBuffer, -current_auth_fails: *usize, -max_auth_fails: usize, +current_auth_fails: *u64, +max_auth_fails: u64, pub fn init( io: std.Io, buffer: *TerminalBuffer, - current_auth_fails: *usize, - max_auth_fails: usize, + current_auth_fails: *u64, + max_auth_fails: u64, ) Cascade { return .{ .io = io, diff --git a/src/animations/DurFile.zig b/src/animations/DurFile.zig index 18844b8..c20c2d2 100644 --- a/src/animations/DurFile.zig +++ b/src/animations/DurFile.zig @@ -310,7 +310,7 @@ allocator: Allocator, io: std.Io, terminal_buffer: *TerminalBuffer, dur_movie: DurFormat, -frames: u64, +frames: usize, frame_size: UVec2, start_pos: IVec2, full_color: bool, From 59c07aa3ba05746e6f1fffa2e9210bca5cde23d6 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Tue, 28 Apr 2026 19:35:21 +0200 Subject: [PATCH 104/176] Fix xauth log not being flushed Signed-off-by: AnErrupTion --- src/auth.zig | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/auth.zig b/src/auth.zig index b765c08..d191d55 100644 --- a/src/auth.zig +++ b/src/auth.zig @@ -431,7 +431,12 @@ fn xauth(log_file: *LogFile, allocator: std.mem.Allocator, io: std.Io, display_n var status: c_int = undefined; const result = std.posix.system.waitpid(pid, &status, 0); if (interop.isError(result) or status != 0) { - try log_file.file_writer.interface.print("xauth command failed with status {d}\n", .{status}); + try log_file.err( + io, + "auth/x11", + "xauth command failed with status: {d}", + .{status}, + ); return error.XauthFailed; } From 51c5c3ee0bcb4d7e109f480f92f5e34cd5995e16 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Wed, 29 Apr 2026 22:52:58 +0200 Subject: [PATCH 105/176] Fix waitpid() being interrupted by SIGCHLD Signed-off-by: AnErrupTion --- src/main.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main.zig b/src/main.zig index 53400df..382e5f3 100644 --- a/src/main.zig +++ b/src/main.zig @@ -1541,7 +1541,7 @@ fn authenticate(ptr: *anyopaque) !bool { const tty_control_transfer_act = std.posix.Sigaction{ .handler = .{ .handler = &ttyControlTransferSignalHandler }, .mask = std.posix.sigemptyset(), - .flags = 0, + .flags = std.posix.SA.RESTART, // For waitpid() }; std.posix.sigaction(std.posix.SIG.CHLD, &tty_control_transfer_act, null); From 15cd0c4779b083b75347b0d5d92956679dd10e45 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Thu, 30 Apr 2026 14:38:56 +0200 Subject: [PATCH 106/176] Use SIGINT instead of SIGCHILD for TTY control transfer Signed-off-by: AnErrupTion --- src/auth.zig | 2 +- src/main.zig | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/auth.zig b/src/auth.zig index d191d55..87ddc09 100644 --- a/src/auth.zig +++ b/src/auth.zig @@ -201,7 +201,7 @@ fn startSession( // Signal to the session process to give up control on the TTY try log_file.info(io, "auth/sys", "releasing tty", .{}); - std.posix.kill(options.session_pid, std.posix.SIG.CHLD) catch return error.TtyControlTransferFailed; + std.posix.kill(options.session_pid, std.posix.SIG.INT) catch return error.TtyControlTransferFailed; // Execute what the user requested switch (current_environment.display_server) { diff --git a/src/main.zig b/src/main.zig index 382e5f3..81c44b4 100644 --- a/src/main.zig +++ b/src/main.zig @@ -1541,9 +1541,9 @@ fn authenticate(ptr: *anyopaque) !bool { const tty_control_transfer_act = std.posix.Sigaction{ .handler = .{ .handler = &ttyControlTransferSignalHandler }, .mask = std.posix.sigemptyset(), - .flags = std.posix.SA.RESTART, // For waitpid() + .flags = 0, }; - std.posix.sigaction(std.posix.SIG.CHLD, &tty_control_transfer_act, null); + std.posix.sigaction(std.posix.SIG.INT, &tty_control_transfer_act, null); try state.log_file.reinit(state.io); From 807f6d249a1494c1c20949d82a9409497d067a54 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Thu, 30 Apr 2026 22:48:29 +0200 Subject: [PATCH 107/176] Remove further & all @cImport() usage in interop Signed-off-by: AnErrupTion --- ly-core/build.zig | 8 ++++++++ ly-core/src/interop.zig | 18 ++++-------------- 2 files changed, 12 insertions(+), 14 deletions(-) diff --git a/ly-core/build.zig b/ly-core/build.zig index 614404e..dc722b5 100644 --- a/ly-core/build.zig +++ b/ly-core/build.zig @@ -36,6 +36,14 @@ pub fn build(b: *std.Build) void { addCImport(b, mod, translate_c, target, optimize, "system_time", "#include "); addCImport(b, mod, translate_c, target, optimize, "time", "#include "); + if (target.result.os.tag == .linux) { + addCImport(b, mod, translate_c, target, optimize, "kd", "#include "); + addCImport(b, mod, translate_c, target, optimize, "vt", "#include "); + } else if (target.result.os.tag == .freebsd) { + addCImport(b, mod, translate_c, target, optimize, "kbio", "#include "); + addCImport(b, mod, translate_c, target, optimize, "consio", "#include "); + } + const mod_tests = b.addTest(.{ .root_module = mod, }); diff --git a/ly-core/src/interop.zig b/ly-core/src/interop.zig index 5b2b954..f23583c 100644 --- a/ly-core/src/interop.zig +++ b/ly-core/src/interop.zig @@ -31,13 +31,8 @@ pub const UsernameEntry = struct { fn PlatformStruct() type { return switch (builtin.os.tag) { .linux => struct { - pub const kd = @cImport({ - @cInclude("sys/kd.h"); - }); - - pub const vt = @cImport({ - @cInclude("sys/vt.h"); - }); + pub const kd = @import("kd"); + pub const vt = @import("vt"); pub const LedState = c_char; pub const get_led_state = kd.KDGKBLED; @@ -197,13 +192,8 @@ fn PlatformStruct() type { } }, .freebsd => struct { - pub const kbio = @cImport({ - @cInclude("sys/kbio.h"); - }); - - pub const consio = @cImport({ - @cInclude("sys/consio.h"); - }); + pub const kbio = @import("kbio"); + pub const consio = @import("consio"); pub const LedState = c_int; pub const get_led_state = kbio.KDGETLED; From 5905e054c587df65f9e4559f33595de1bfccaaae Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Fri, 1 May 2026 07:47:48 +0200 Subject: [PATCH 108/176] Start Ly v1.5.0 development cycle Signed-off-by: AnErrupTion --- build.zig | 2 +- build.zig.zon | 2 +- ly-core/build.zig.zon | 2 +- ly-ui/build.zig.zon | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/build.zig b/build.zig index 2aeac26..0e626d7 100644 --- a/build.zig +++ b/build.zig @@ -23,7 +23,7 @@ comptime { } } -const ly_version = std.SemanticVersion{ .major = 1, .minor = 4, .patch = 0 }; +const ly_version = std.SemanticVersion{ .major = 1, .minor = 5, .patch = 0 }; var dest_directory: []const u8 = undefined; var config_directory: []const u8 = undefined; diff --git a/build.zig.zon b/build.zig.zon index 63e8c32..3c091cb 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -1,6 +1,6 @@ .{ .name = .ly, - .version = "1.4.0", + .version = "1.5.0", .fingerprint = 0xa148ffcc5dc2cb59, .minimum_zig_version = "0.16.0", .dependencies = .{ diff --git a/ly-core/build.zig.zon b/ly-core/build.zig.zon index a389405..b40c8f8 100644 --- a/ly-core/build.zig.zon +++ b/ly-core/build.zig.zon @@ -1,6 +1,6 @@ .{ .name = .ly_core, - .version = "1.0.0", + .version = "1.1.0", .fingerprint = 0xddda7afda795472, .minimum_zig_version = "0.16.0", .dependencies = .{ diff --git a/ly-ui/build.zig.zon b/ly-ui/build.zig.zon index 598cba0..2a7e175 100644 --- a/ly-ui/build.zig.zon +++ b/ly-ui/build.zig.zon @@ -1,6 +1,6 @@ .{ .name = .ly_ui, - .version = "1.0.0", + .version = "1.1.0", .fingerprint = 0x8d11bf85a74ec803, .minimum_zig_version = "0.16.0", .dependencies = .{ From 3869bfd2f95acc4cd7dcc08675155047da1d4676 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Fri, 1 May 2026 17:40:04 +0200 Subject: [PATCH 109/176] Add config validation argument (closes #969) Signed-off-by: AnErrupTion --- readme.md | 6 ++++++ src/main.zig | 27 ++++++++++++++++++++++++++- 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/readme.md b/readme.md index aa23db8..78ebffc 100644 --- a/readme.md +++ b/readme.md @@ -226,6 +226,12 @@ You can, of course, still select the init system of your choice when using this You can find all the configuration in `/etc/ly/config.ini`. The file is fully commented, and includes the default values. +You may also check the validity of your configuration file (i.e. if there are any errors in it) with the following command: + +``` +$ ly --validate-config /etc/ly/config.ini +``` + ## Controls Use the Up/Down arrow keys to change the current field, and the Left/Right arrow keys to scroll through the different fields (whether it be the info line, the desktop environment, or the username). The info line is where messages and errors are displayed. diff --git a/src/main.zig b/src/main.zig index 81c44b4..6373751 100644 --- a/src/main.zig +++ b/src/main.zig @@ -172,7 +172,8 @@ pub fn main(init: std.process.Init) !void { \\-h, --help Shows all commands. \\-v, --version Shows the version of Ly. \\-c, --config Overrides the default configuration path. Example: --config /usr/share/ly - \\--use-kmscon-vt Use KMSCON instead of kernel VT + \\--use-kmscon-vt Uses KMSCON instead of the kernel VT. + \\--validate-config Validates the given configuration file. ); var diag = clap.Diagnostic{}; @@ -211,6 +212,30 @@ pub fn main(init: std.process.Init) !void { } if (res.args.config) |path| config_parent_path = path; if (res.args.@"use-kmscon-vt" != 0) state.use_kmscon_vt = true; + if (res.args.@"validate-config") |path| { + var parser = try IniParser(Config).init( + state.allocator, + state.io, + path, + migrator.configFieldHandler, + ); + defer parser.deinit(); + + for (parser.errors.items) |err| { + std.log.err( + "failed to convert value '{s}' of option '{s}' to type '{s}': {s}", + .{ err.value, err.key, err.type_name, err.error_name }, + ); + } + + if (parser.maybe_load_error) |err| { + std.log.err("failed to load config file: {s}", .{@errorName(err)}); + std.process.exit(1); + } + + std.log.info("no errors detected!", .{}); + std.process.exit(0); + } } // Load configuration file From 79eebd8ee0a1a8e9a958679605f81620c611ecb4 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Fri, 1 May 2026 17:43:28 +0200 Subject: [PATCH 110/176] Prefer std.log instead of stderr directly Signed-off-by: AnErrupTion --- src/main.zig | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/src/main.zig b/src/main.zig index 6373751..ce222db 100644 --- a/src/main.zig +++ b/src/main.zig @@ -146,12 +146,10 @@ pub fn main(init: std.process.Init) !void { // If we can't shutdown or restart due to an error, we print it to standard error. If that fails, just bail out if (shutdown) { const shutdown_error = std.process.replace(state.io, .{ .argv = &[_][]const u8{ "/bin/sh", "-c", shutdown_cmd } }); - stderr.print("error: couldn't shutdown: {s}\n", .{@errorName(shutdown_error)}) catch std.process.exit(1); - stderr.flush() catch std.process.exit(1); + std.log.err("couldn't shutdown: {s}", .{@errorName(shutdown_error)}); } else if (restart) { const restart_error = std.process.replace(state.io, .{ .argv = &[_][]const u8{ "/bin/sh", "-c", restart_cmd } }); - stderr.print("error: couldn't restart: {s}\n", .{@errorName(restart_error)}) catch std.process.exit(1); - stderr.flush() catch std.process.exit(1); + std.log.err("couldn't restart: {s}", .{@errorName(restart_error)}); } else { // The user has quit Ly using Ctrl+C if (commands_allocated) { @@ -201,13 +199,11 @@ pub fn main(init: std.process.Init) !void { if (res.args.help != 0) { try clap.help(stderr, clap.Help, ¶ms, .{}); - _ = try stderr.write("Note: if you want to configure Ly, please check the config file, which is located at " ++ build_options.config_directory ++ "/ly/config.ini.\n"); - try stderr.flush(); + std.log.info("note: if you want to configure Ly, please check the config file, which is located at " ++ build_options.config_directory ++ "/ly/config.ini.", .{}); std.process.exit(0); } if (res.args.version != 0) { - _ = try stderr.write("Ly version " ++ build_options.version ++ "\n"); - try stderr.flush(); + std.log.info("ly version " ++ build_options.version, .{}); std.process.exit(0); } if (res.args.config) |path| config_parent_path = path; From fdf241bed53baf386a6023785c29b0cbd591fb34 Mon Sep 17 00:00:00 2001 From: MartorSkull Date: Fri, 1 May 2026 20:09:34 +0200 Subject: [PATCH 111/176] Add option to move the box relative to the screen size (#964) ## What are the changes about? Added a new option in the configuration file for moving the box relative to the screen size. ``` box_h_position = 0.5 box_v_position = 0.5 ``` The big clock is centered relative to the box. In the cases where it would be outside of the screen, it moves the box to fit in the screen. ## What existing issue does this resolve? Add more options for personalization ## Examples Normal usage: ``` box_h_position = 0.15 box_v_position = 0.35 ``` ![image](/attachments/6595dfa9-aade-45f4-887c-e5db7f8d5a89) Clock would be outside of the screen vertically: ``` box_h_position = 0.15 box_v_position = -1.0 ``` ![image](/attachments/0d6bdcc4-e9dd-4671-a65d-b8b9f063ffb6) Clock would be outside of the screen horizontally and vertically: ``` box_h_position = -1.0 box_v_position = -1.0 input_len = 3 ``` ![image](/attachments/630b07fd-b400-4e71-8a67-1baf3a6700a0) Clock would be outside of the screen horizontally and vertically on the bottom left of the screen: ``` box_h_position = 2.0 box_v_position = 2.0 input_len = 3 ``` ![image](/attachments/28902967-11a8-4c02-a4c9-1b92f9a728ee) ## What existing issue does this resolve? _Replace this with a reference to an existing issue, or N/A if there is none_ ## Pre-requisites - [x] I have tested & confirmed the changes work locally - [x] I have run `zig fmt` throughout my changes Co-authored-by: AnErrupTion Reviewed-on: https://codeberg.org/fairyglade/ly/pulls/964 Reviewed-by: AnErrupTion --- res/config.ini | 8 ++++++++ src/config/Config.zig | 2 ++ src/main.zig | 38 ++++++++++++++++++++++++++------------ 3 files changed, 36 insertions(+), 12 deletions(-) diff --git a/res/config.ini b/res/config.ini index 86f44b4..c2d885c 100644 --- a/res/config.ini +++ b/res/config.ini @@ -97,6 +97,14 @@ blank_box = true # Border foreground color id border_fg = 0x00FFFFFF +# Relative horizontal position from the end of the screen +# default: 0.5 +box_position_h = 0.5 + +# Relative vertical position from the bottom of the screen +# default: 0.4 +box_position_v = 0.4 + # Title to show at the top of the main box # If set to null, none will be shown box_title = null diff --git a/src/config/Config.zig b/src/config/Config.zig index 2233cb4..9efaaf7 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -23,6 +23,8 @@ bigclock_12hr: bool = false, bigclock_seconds: bool = false, blank_box: bool = true, border_fg: u32 = 0x00FFFFFF, +box_position_h: f32 = 0.5, +box_position_v: f32 = 0.4, box_title: ?[]const u8 = null, brightness_down_cmd: [:0]const u8 = build_options.prefix_directory ++ "/bin/brightnessctl -q -n s 10%-", brightness_down_key: ?[]const u8 = "F5", diff --git a/src/main.zig b/src/main.zig index ce222db..35239d7 100644 --- a/src/main.zig +++ b/src/main.zig @@ -2046,22 +2046,36 @@ fn positionWidgets(ptr: *anyopaque) !void { .childrenPosition() .removeX(TerminalBuffer.strWidth(state.lang.numlock) + TerminalBuffer.strWidth(state.lang.capslock) + 1)); - state.box.positionXY(TerminalBuffer.START_POSITION - .addX((state.buffer.width - @min(state.buffer.width - 2, state.box.width)) / 2) - .addY((state.buffer.height - @min(state.buffer.height - 2, state.box.height)) / 2)); + var bb_height = state.box.height; + var bb_width = state.box.width; + const clock_text_len = TerminalBuffer.strWidth(state.bigclock_label.text) * (BigLabel.CHAR_WIDTH + 1); if (state.config.bigclock != .none) { - const half_width = state.buffer.width / 2; - const half_label_width = (TerminalBuffer.strWidth(state.bigclock_label.text) * (BigLabel.CHAR_WIDTH + 1)) / 2; - const half_height = (if (state.buffer.height > state.box.height) state.buffer.height - state.box.height else state.buffer.height) / 2; - - state.bigclock_label.positionXY(TerminalBuffer.START_POSITION - .addX(half_width) - .removeXIf(half_label_width, half_width > half_label_width) - .addY(half_height) - .removeYIf(BigLabel.CHAR_HEIGHT + 2, half_height > BigLabel.CHAR_HEIGHT + 2)); + bb_height += BigLabel.CHAR_HEIGHT + 2; + bb_width = @max(bb_width, clock_text_len); } + const max_v_position: f32 = @floatFromInt(state.buffer.height - bb_height - 1); + const max_h_position: f32 = @floatFromInt(state.buffer.width - bb_width - 1); + + bb_height = @min(bb_height, state.buffer.height - 2); + bb_width = @min(bb_width, state.buffer.width - 2); + + const v_space: f32 = @floatFromInt(state.buffer.height - bb_height); + const v_position: usize = @intFromFloat(std.math.clamp(v_space * state.config.box_position_v, 1.0, max_v_position)); + const h_space: f32 = @floatFromInt(state.buffer.width - bb_width); + const h_position: usize = @intFromFloat(std.math.clamp(h_space * state.config.box_position_h, 1.0, max_h_position)); + + if (state.config.bigclock != .none) { + state.bigclock_label.positionXY(TerminalBuffer.START_POSITION + .addX(h_position + (bb_width - clock_text_len) / 2) + .addY(v_position)); + } + + state.box.positionXY(TerminalBuffer.START_POSITION + .addX(h_position + (bb_width - state.box.width) / 2) + .addY(v_position + (bb_height - state.box.height))); + state.info_line.label.positionY(state.box .childrenPosition()); From c50af66407b3005dd72979a9c62b9a81cefa3682 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Fri, 1 May 2026 20:54:54 +0200 Subject: [PATCH 112/176] Fix log file race condition Signed-off-by: AnErrupTion --- src/auth.zig | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/auth.zig b/src/auth.zig index 87ddc09..6a97fff 100644 --- a/src/auth.zig +++ b/src/auth.zig @@ -417,19 +417,27 @@ fn xauth(log_file: *LogFile, allocator: std.mem.Allocator, io: std.Io, display_n const magic_cookie = mcookie(io); + log_file.deinit(io); + const pid = std.posix.system.fork(); if (pid == 0) { + try log_file.reinit(io); + var cmd_buffer: [1024]u8 = undefined; const cmd_str = std.fmt.bufPrintZ(&cmd_buffer, "{s} add {s} . {s}", .{ options.xauth_cmd, display_name, magic_cookie }) catch std.process.exit(1); try log_file.info(io, "auth/x11", "executing: {s} -c {s}", .{ shell, cmd_str }); const args = [_:null]?[*:0]const u8{ shell, "-c", cmd_str }; _ = std.posix.system.execve(shell, &args, std.c.environ); + + log_file.deinit(io); std.process.exit(1); } var status: c_int = undefined; const result = std.posix.system.waitpid(pid, &status, 0); + + try log_file.reinit(io); if (interop.isError(result) or status != 0) { try log_file.err( io, From 864f5f289244f35890875a313c056df1ae775c0b Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Fri, 1 May 2026 21:51:27 +0200 Subject: [PATCH 113/176] Implement syslog functionality (closes # Signed-off-by: AnErrupTion --- ly-core/src/LogFile.zig | 85 +++++++++++++++++++++++++++++------------ res/config.ini | 1 + src/config/Config.zig | 2 +- 3 files changed, 62 insertions(+), 26 deletions(-) diff --git a/ly-core/src/LogFile.zig b/ly-core/src/LogFile.zig index f60b153..192f338 100644 --- a/ly-core/src/LogFile.zig +++ b/ly-core/src/LogFile.zig @@ -3,50 +3,85 @@ const interop = @import("interop.zig"); const LogFile = @This(); -path: []const u8, +maybe_path: ?[]const u8, could_open_log_file: bool = undefined, -file: std.Io.File = undefined, +maybe_file: ?std.Io.File = null, buffer: []u8, -file_writer: std.Io.File.Writer = undefined, +maybe_file_writer: ?std.Io.File.Writer = null, + +pub fn init(io: std.Io, path: ?[]const u8, buffer: []u8) !LogFile { + var log_file = LogFile{ + .maybe_path = path, + .buffer = buffer, + }; + + if (path) |p| { + log_file.could_open_log_file = try openLogFile(io, p, &log_file); + } else { + std.posix.system.openlog("ly", 0, 0); + log_file.could_open_log_file = true; + } -pub fn init(io: std.Io, path: []const u8, buffer: []u8) !LogFile { - var log_file = LogFile{ .path = path, .buffer = buffer }; - log_file.could_open_log_file = try openLogFile(io, path, &log_file); return log_file; } pub fn reinit(self: *LogFile, io: std.Io) !void { - self.could_open_log_file = try openLogFile(io, self.path, self); + if (self.maybe_path) |path| { + self.could_open_log_file = try openLogFile(io, path, self); + } else { + std.posix.system.openlog("ly", 0, 0); + self.could_open_log_file = true; + } } pub fn deinit(self: *LogFile, io: std.Io) void { - self.file.close(io); + if (self.maybe_file) |file| { + file.close(io); + } else { + std.posix.system.closelog(); + } } pub fn info(self: *LogFile, io: std.Io, category: []const u8, comptime message: []const u8, args: anytype) !void { - var buffer: [128:0]u8 = undefined; - const time = interop.timeAsString(io, &buffer, "%Y-%m-%d %H:%M:%S"); + if (self.maybe_file_writer) |*writer| { + var buffer: [128:0]u8 = undefined; + const time = interop.timeAsString(io, &buffer, "%Y-%m-%d %H:%M:%S"); - try self.file_writer.interface.print("{s} [info/{s}] ", .{ time, category }); - try self.file_writer.interface.print(message, args); - try self.file_writer.interface.writeByte('\n'); - try self.file_writer.interface.flush(); + try writer.interface.print("{s} [info/{s}] ", .{ time, category }); + try writer.interface.print(message, args); + try writer.interface.writeByte('\n'); + try writer.interface.flush(); + } else { + var buffer: [1024]u8 = undefined; + const slice = try std.fmt.bufPrint(&buffer, message, args); + const msg = try std.fmt.bufPrintZ(buffer[slice.len..], "[info/{s}] {s}", .{ category, slice }); + + std.posix.system.syslog(std.posix.LOG.INFO, msg.ptr); + } } pub fn err(self: *LogFile, io: std.Io, category: []const u8, comptime message: []const u8, args: anytype) !void { - var buffer: [128:0]u8 = undefined; - const time = interop.timeAsString(io, &buffer, "%Y-%m-%d %H:%M:%S"); + if (self.maybe_file_writer) |*writer| { + var buffer: [128:0]u8 = undefined; + const time = interop.timeAsString(io, &buffer, "%Y-%m-%d %H:%M:%S"); - try self.file_writer.interface.print("{s} [err/{s}] ", .{ time, category }); - try self.file_writer.interface.print(message, args); - try self.file_writer.interface.writeByte('\n'); - try self.file_writer.interface.flush(); + try writer.interface.print("{s} [err/{s}] ", .{ time, category }); + try writer.interface.print(message, args); + try writer.interface.writeByte('\n'); + try writer.interface.flush(); + } else { + var buffer: [1024]u8 = undefined; + const slice = try std.fmt.bufPrint(&buffer, message, args); + const msg = try std.fmt.bufPrintZ(buffer[slice.len..], "[info/{s}] {s}", .{ category, slice }); + + std.posix.system.syslog(std.posix.LOG.ERR, msg.ptr); + } } fn openLogFile(io: std.Io, path: []const u8, log_file: *LogFile) !bool { var could_open_log_file = true; open_log_file: { - log_file.file = std.Io.Dir.cwd().openFile(io, path, .{ .mode = .write_only }) catch std.Io.Dir.cwd().createFile(io, path, .{ .permissions = .fromMode(0o666) }) catch { + log_file.maybe_file = std.Io.Dir.cwd().openFile(io, path, .{ .mode = .write_only }) catch std.Io.Dir.cwd().createFile(io, path, .{ .permissions = .fromMode(0o666) }) catch { // If we could neither open an existing log file nor create a new // one, abort. could_open_log_file = false; @@ -55,17 +90,17 @@ fn openLogFile(io: std.Io, path: []const u8, log_file: *LogFile) !bool { } if (!could_open_log_file) { - log_file.file = try std.Io.Dir.openFileAbsolute(io, "/dev/null", .{ .mode = .write_only }); + log_file.maybe_file = try std.Io.Dir.openFileAbsolute(io, "/dev/null", .{ .mode = .write_only }); } - var log_file_writer = log_file.file.writer(io, log_file.buffer); + var log_file_writer = log_file.maybe_file.?.writer(io, log_file.buffer); // Seek to the end of the log file if (could_open_log_file) { - const stat = try log_file.file.stat(io); + const stat = try log_file.maybe_file.?.stat(io); try log_file_writer.seekTo(stat.size); } - log_file.file_writer = log_file_writer; + log_file.maybe_file_writer = log_file_writer; return could_open_log_file; } diff --git a/res/config.ini b/res/config.ini index c2d885c..a290773 100644 --- a/res/config.ini +++ b/res/config.ini @@ -299,6 +299,7 @@ login_defs_path = /etc/login.defs logout_cmd = null # General log file path +# If null, syslog will be used instead ly_log = /var/log/ly.log # Main box horizontal margin diff --git a/src/config/Config.zig b/src/config/Config.zig index 9efaaf7..a592faa 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -74,7 +74,7 @@ lang: []const u8 = "en", login_cmd: ?[]const u8 = null, login_defs_path: []const u8 = "/etc/login.defs", logout_cmd: ?[]const u8 = null, -ly_log: []const u8 = "/var/log/ly.log", +ly_log: ?[]const u8 = "/var/log/ly.log", margin_box_h: u8 = 2, margin_box_v: u8 = 1, numlock: bool = false, From 4db9295102ac43d054c4e12f067429c5104f6e19 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Sun, 3 May 2026 20:05:44 +0200 Subject: [PATCH 114/176] Fix building without X11 Signed-off-by: AnErrupTion --- build.zig | 6 +++++- ly-core/build.zig | 5 ++++- ly-ui/build.zig | 7 ++++++- 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/build.zig b/build.zig index 0e626d7..45a548b 100644 --- a/build.zig +++ b/build.zig @@ -72,7 +72,11 @@ pub fn build(b: *std.Build) !void { .use_llvm = true, }); - const ly_ui = b.dependency("ly_ui", .{ .target = target, .optimize = optimize }); + const ly_ui = b.dependency("ly_ui", .{ + .target = target, + .optimize = optimize, + .enable_x11_support = enable_x11_support, + }); exe.root_module.addImport("ly-ui", ly_ui.module("ly-ui")); exe.root_module.addOptions("build_options", build_options); diff --git a/ly-core/build.zig b/ly-core/build.zig index dc722b5..f6574de 100644 --- a/ly-core/build.zig +++ b/ly-core/build.zig @@ -4,6 +4,7 @@ const Translator = @import("translate_c").Translator; pub fn build(b: *std.Build) void { const target = b.standardTargetOptions(.{}); const optimize = b.standardOptimizeOption(.{}); + const enable_x11_support = b.option(bool, "enable_x11_support", "Enable X11 support") orelse true; const mod = b.addModule("ly-core", .{ .root_source_file = b.path("src/root.zig"), .target = target, @@ -20,7 +21,9 @@ pub fn build(b: *std.Build) void { addCImport(b, mod, translate_c, target, optimize, "pam", "#include "); addCImport(b, mod, translate_c, target, optimize, "utmp", "#include "); - addCImport(b, mod, translate_c, target, optimize, "xcb", "#include "); + if (enable_x11_support) { + addCImport(b, mod, translate_c, target, optimize, "xcb", "#include "); + } if (target.result.os.tag == .freebsd) { addCImport(b, mod, translate_c, target, optimize, "pwd", \\#include diff --git a/ly-ui/build.zig b/ly-ui/build.zig index a7a3051..7397873 100644 --- a/ly-ui/build.zig +++ b/ly-ui/build.zig @@ -4,13 +4,18 @@ const Translator = @import("translate_c").Translator; pub fn build(b: *std.Build) void { const target = b.standardTargetOptions(.{}); const optimize = b.standardOptimizeOption(.{}); + const enable_x11_support = b.option(bool, "enable_x11_support", "Enable X11 support") orelse true; const mod = b.addModule("ly-ui", .{ .root_source_file = b.path("src/root.zig"), .target = target, .optimize = optimize, }); - const ly_core = b.dependency("ly_core", .{ .target = target, .optimize = optimize }); + const ly_core = b.dependency("ly_core", .{ + .target = target, + .optimize = optimize, + .enable_x11_support = enable_x11_support, + }); mod.addImport("ly-core", ly_core.module("ly-core")); const termbox_dep = b.dependency("termbox2", .{ From b8ae1266236e23a7f2620045cf630ec5e26f03c1 Mon Sep 17 00:00:00 2001 From: Titanium Brain Date: Mon, 4 May 2026 12:34:32 +0200 Subject: [PATCH 115/176] Apply the typestate pattern to DurFormat (#972) --- src/animations/DurFile.zig | 168 +++++++++++++++++++++++++++---------- 1 file changed, 123 insertions(+), 45 deletions(-) diff --git a/src/animations/DurFile.zig b/src/animations/DurFile.zig index c20c2d2..57d38c0 100644 --- a/src/animations/DurFile.zig +++ b/src/animations/DurFile.zig @@ -62,7 +62,7 @@ const Frame = struct { }; // https://github.com/cmang/durdraw/blob/0.29.0/durformat.md -const DurFormat = struct { +const DurFormatRaw = struct { allocator: Allocator, formatVersion: ?i64 = null, colorFormat: ?[]const u8 = null, @@ -72,38 +72,48 @@ const DurFormat = struct { lines: ?i64 = null, frames: std.ArrayList(Frame) = undefined, - pub fn valid(self: *DurFormat) bool { - if (self.formatVersion != null and - self.colorFormat != null and - self.encoding != null and - self.framerate != null and - self.columns != null and - self.lines != null and - self.frames.items.len >= 1) - { - // v8 may have breaking changes like changing the colormap xy direction - // (https://github.com/cmang/durdraw/issues/24) - if (self.formatVersion.? != 7) return false; + // Validate data and return a valid DurFormat + // Consumes `self`, making it unusable after + pub fn validate(self: *DurFormatRaw) !DurFormat { + // v8 may have breaking changes like changing the colormap xy direction + // (https://github.com/cmang/durdraw/issues/24) + const format_version = self.formatVersion orelse return error.MissingFieldVersion; + if (format_version != 7) return error.UnsupportedVersion; - // Code currently only supports 16 and 256 color format only - if (!(eql(u8, "16", self.colorFormat.?) or eql(u8, "256", self.colorFormat.?))) - return false; + const color_format_str = self.colorFormat orelse return error.MissingFieldColorFormat; + // Code currently only supports 16 and 256 color format only + const color_format: DurColorFormat = + if (eql(u8, color_format_str, "16")) .@"16" else if (eql(u8, color_format_str, "256")) .@"256" else return error.UnsupportedColorFormat; - // Code currently supports only utf-8 encoding - if (!eql(u8, self.encoding.?, "utf-8")) return false; + const encoding_str = self.encoding orelse return error.MissingFieldEncoding; + // Code currently supports only utf-8 encoding + const encoding: DurEncoding = if (eql(u8, encoding_str, "utf-8")) .utf_8 else return error.UnsupportedEncoding; - // Sanity check on file - if (self.columns.? <= 0) return false; - if (self.lines.? <= 0) return false; - if (self.framerate.? < 0) return false; + if (self.framerate == null) return error.MissingFieldFramerate; + if (self.framerate.? <= 0) return error.InvalidFramerate; + const framerate: f64 = self.framerate.?; - return true; - } + // Sanity check on file + if (self.columns == null or self.lines == null) return error.MissingDimensions; + const columns = std.math.cast(u32, self.columns.?) orelse return error.InvalidColumnCount; + const lines = std.math.cast(u32, self.lines.?) orelse return error.InvalidLineCount; - return false; + if (self.frames.items.len == 0) return error.NoFrames; + const frames = self.frames; + + return .{ + .allocator = self.allocator, + .formatVersion = format_version, + .colorFormat = color_format, + .encoding = encoding, + .framerate = framerate, + .columns = columns, + .lines = lines, + .frames = frames, + }; } - fn parse_dur_from_json(self: *DurFormat, allocator: Allocator, dur_json_root: Json.Value) !void { + fn parse_dur_from_json(self: *DurFormatRaw, allocator: Allocator, dur_json_root: Json.Value) !void { var dur_movie = if (dur_json_root.object.get("DurMovie")) |dm| dm.object else return error.NotValidFile; // Depending on the version, a dur file can have different json object names (ie: columns vs sizeX) @@ -150,7 +160,7 @@ const DurFormat = struct { } } - pub fn create_from_file(self: *DurFormat, allocator: Allocator, io: std.Io, file_path: []const u8) !void { + pub fn create_from_file(self: *DurFormatRaw, allocator: Allocator, io: std.Io, file_path: []const u8) !void { const file_decompressed = try read_decompress_file(allocator, io, file_path); defer allocator.free(file_decompressed); @@ -158,20 +168,36 @@ const DurFormat = struct { defer parsed.deinit(); try parse_dur_from_json(self, allocator, parsed.value); - - if (!self.valid()) { - return error.NotValidFile; - } } - pub fn init(allocator: Allocator) DurFormat { + pub fn init(allocator: Allocator) DurFormatRaw { return .{ .allocator = allocator }; } - pub fn deinit(self: *DurFormat) void { + pub fn deinit(self: *DurFormatRaw) void { if (self.colorFormat) |str| self.allocator.free(str); if (self.encoding) |str| self.allocator.free(str); + } +}; +const DurColorFormat = enum { + @"16", + @"256", +}; + +const DurEncoding = enum { utf_8 }; + +const DurFormat = struct { + allocator: Allocator, + formatVersion: i64, + colorFormat: DurColorFormat, + encoding: DurEncoding, + framerate: f64, + columns: u32, + lines: u32, + frames: std.ArrayList(Frame), + + pub fn deinit(self: *DurFormat) void { for (self.frames.items) |frame| { frame.deinit(self.allocator); } @@ -324,16 +350,16 @@ offset_alignment: DurOffsetAlignment, offset: IVec2, // if the user has an even number of columns or rows, we will default to the left or higher position (e.g. 4 columns center = .x..) -fn center(v: u32) i64 { - return @intCast((v / 2) + (v % 2)); +fn center(v: i64) i64 { + return @intCast(@divTrunc(v, 2) + @mod(v, 2)); } fn calc_start_position(terminal_buffer: *TerminalBuffer, dur_movie: *DurFormat, offset_alignment: DurOffsetAlignment, offset: IVec2) IVec2 { const buf_width: u32 = @intCast(terminal_buffer.width); const buf_height: u32 = @intCast(terminal_buffer.height); - var movie_width: u32 = @intCast(dur_movie.columns.?); - var movie_height: u32 = @intCast(dur_movie.lines.?); + var movie_width: u32 = dur_movie.columns; + var movie_height: u32 = dur_movie.lines; if (movie_width > buf_width) movie_width = buf_width; if (movie_height > buf_height) movie_height = buf_height; @@ -357,8 +383,8 @@ fn calc_frame_size(terminal_buffer: *TerminalBuffer, dur_movie: *DurFormat) UVec const buf_width: u32 = @intCast(terminal_buffer.width); const buf_height: u32 = @intCast(terminal_buffer.height); - const movie_width: u32 = @intCast(dur_movie.columns.?); - const movie_height: u32 = @intCast(dur_movie.lines.?); + const movie_width: u32 = dur_movie.columns; + const movie_height: u32 = dur_movie.lines; // Draw only the needed amount if movie smaller than screen. If movie is bigger, we will just draw entire screen const frame_width = if (movie_width < buf_width) movie_width else buf_width; @@ -381,9 +407,10 @@ pub fn init( timeout_sec: u12, frame_delay: u16, ) !DurFile { - var dur_movie: DurFormat = .init(allocator); + var dur_movie_raw: DurFormatRaw = .init(allocator); + defer dur_movie_raw.deinit(); - dur_movie.create_from_file(allocator, io, file_path) catch |err| switch (err) { + dur_movie_raw.create_from_file(allocator, io, file_path) catch |err| switch (err) { error.FileNotFound => { try log_file.err(io, "tui", "dur_file was not found at: {s}", .{file_path}); return err; @@ -395,11 +422,62 @@ pub fn init( else => return err, }; + var dur_movie = dur_movie_raw.validate() catch |err| switch (err) { + error.MissingFieldVersion => { + try log_file.err(io, "tui", "dur_file loaded was invalid: missing field formatVersion!", .{}); + return err; + }, + error.UnsupportedVersion => { + try log_file.err(io, "tui", "dur_file loaded was invalid: unsupported version ({d})!", .{dur_movie_raw.formatVersion.?}); + return err; + }, + error.MissingFieldColorFormat => { + try log_file.err(io, "tui", "dur_file loaded was invalid: missing field colorFormat!", .{}); + return err; + }, + error.UnsupportedColorFormat => { + try log_file.err(io, "tui", "dur_file loaded was invalid: unsupported colorFormat ({s})!", .{dur_movie_raw.colorFormat.?}); + return err; + }, + error.MissingFieldEncoding => { + try log_file.err(io, "tui", "dur_file loaded was invalid: missing field encoding!", .{}); + return err; + }, + error.UnsupportedEncoding => { + try log_file.err(io, "tui", "dur_file loaded was invalid: unsupported encoding ({s})!", .{dur_movie_raw.encoding.?}); + return err; + }, + error.MissingFieldFramerate => { + try log_file.err(io, "tui", "dur_file loaded was invalid: missing field framerate!", .{}); + return err; + }, + error.InvalidFramerate => { + try log_file.err(io, "tui", "dur_file loaded was invalid: negative framerate value found!", .{}); + return err; + }, + error.MissingDimensions => { + try log_file.err(io, "tui", "dur_file loaded was invalid: missing field(s) lines and/or columns!", .{}); + return err; + }, + error.InvalidColumnCount => { + try log_file.err(io, "tui", "dur_file loaded was invalid: columns value falls outside of supported range ({d})!", .{dur_movie_raw.columns.?}); + return err; + }, + error.InvalidLineCount => { + try log_file.err(io, "tui", "dur_file loaded was invalid: lines value falls outside of supported range ({d})!", .{dur_movie_raw.lines.?}); + return err; + }, + error.NoFrames => { + try log_file.err(io, "tui", "dur_file loaded was invalid: animation has no frames!", .{}); + return err; + }, + }; + // 4 bit mode with 256 color is unsupported - if (!full_color and eql(u8, dur_movie.colorFormat.?, "256")) { + if (!full_color and dur_movie.colorFormat == .@"256") { try log_file.err(io, "tui", "dur_file can not be 256 color encoded when not using full_color option!", .{}); dur_movie.deinit(); - return error.InvalidColorFormat; + return error.NotFullColor; } const offset: IVec2 = .{ x_offset, y_offset }; @@ -408,7 +486,7 @@ pub fn init( const frame_size = calc_frame_size(terminal_buffer, &dur_movie); // Convert dur fps to frames per ms - const frame_time: u32 = @trunc(1000 / dur_movie.framerate.?); + const frame_time: u32 = @trunc(1000 / dur_movie.framerate); return .{ .instance = null, @@ -426,7 +504,7 @@ pub fn init( .frame_delay = frame_delay, .dur_movie = dur_movie, .frame_time = frame_time, - .is_color_format_16 = eql(u8, dur_movie.colorFormat.?, "16"), + .is_color_format_16 = dur_movie.colorFormat == .@"16", .offset_alignment = offset_alignment, .offset = offset, }; From b3830d5bb6de38f5ebe529b48b9682069467da06 Mon Sep 17 00:00:00 2001 From: Titanium Brain Date: Sat, 9 May 2026 21:06:45 +0200 Subject: [PATCH 116/176] fix(dur): apply correct offset for animations bigger than the terminal (#966) Animations bigger than the rendering area would have an alignment to the top left instead of center. ## What are the changes about? Fixes incorrect offset calculations for dur movies bigger than the screen. ## What existing issue does this resolve? N/A ## Pre-requisites - [x] I have tested & confirmed the changes work locally - [x] I have run `zig fmt` throughout my changes Reviewed-on: https://codeberg.org/fairyglade/ly/pulls/966 Reviewed-by: AnErrupTion --- ly-ui/src/TerminalBuffer.zig | 6 +++++ src/animations/DurFile.zig | 51 +++++++----------------------------- 2 files changed, 16 insertions(+), 41 deletions(-) diff --git a/ly-ui/src/TerminalBuffer.zig b/ly-ui/src/TerminalBuffer.zig index 565593b..1fd9999 100644 --- a/ly-ui/src/TerminalBuffer.zig +++ b/ly-ui/src/TerminalBuffer.zig @@ -381,6 +381,12 @@ pub fn setCell(x: usize, y: usize, cell: Cell) void { ); } +pub fn setCellBoundsChecked(self: *TerminalBuffer, x: isize, y: isize, cell: Cell) void { + if (0 <= x and x < self.width and 0 <= y and y < self.height) { + cell.put(@intCast(x), @intCast(y)); + } +} + pub fn reclaim(self: TerminalBuffer) !void { if (self.termios) |termios| { // Take back control of the TTY diff --git a/src/animations/DurFile.zig b/src/animations/DurFile.zig index 57d38c0..969157b 100644 --- a/src/animations/DurFile.zig +++ b/src/animations/DurFile.zig @@ -337,7 +337,6 @@ io: std.Io, terminal_buffer: *TerminalBuffer, dur_movie: DurFormat, frames: usize, -frame_size: UVec2, start_pos: IVec2, full_color: bool, animate: *bool, @@ -355,14 +354,11 @@ fn center(v: i64) i64 { } fn calc_start_position(terminal_buffer: *TerminalBuffer, dur_movie: *DurFormat, offset_alignment: DurOffsetAlignment, offset: IVec2) IVec2 { - const buf_width: u32 = @intCast(terminal_buffer.width); - const buf_height: u32 = @intCast(terminal_buffer.height); + const buf_width: i64 = @intCast(terminal_buffer.width); + const buf_height: i64 = @intCast(terminal_buffer.height); - var movie_width: u32 = dur_movie.columns; - var movie_height: u32 = dur_movie.lines; - - if (movie_width > buf_width) movie_width = buf_width; - if (movie_height > buf_height) movie_height = buf_height; + const movie_width: i64 = @intCast(dur_movie.columns); + const movie_height: i64 = @intCast(dur_movie.lines); const start_pos: IVec2 = switch (offset_alignment) { DurOffsetAlignment.center => .{ center(buf_width) - center(movie_width), center(buf_height) - center(movie_height) }, @@ -379,20 +375,6 @@ fn calc_start_position(terminal_buffer: *TerminalBuffer, dur_movie: *DurFormat, return start_pos + offset; } -fn calc_frame_size(terminal_buffer: *TerminalBuffer, dur_movie: *DurFormat) UVec2 { - const buf_width: u32 = @intCast(terminal_buffer.width); - const buf_height: u32 = @intCast(terminal_buffer.height); - - const movie_width: u32 = dur_movie.columns; - const movie_height: u32 = dur_movie.lines; - - // Draw only the needed amount if movie smaller than screen. If movie is bigger, we will just draw entire screen - const frame_width = if (movie_width < buf_width) movie_width else buf_width; - const frame_height = if (movie_height < buf_height) movie_height else buf_height; - - return .{ frame_width, frame_height }; -} - pub fn init( allocator: Allocator, io: std.Io, @@ -483,7 +465,6 @@ pub fn init( const offset: IVec2 = .{ x_offset, y_offset }; const start_pos = calc_start_position(terminal_buffer, &dur_movie, offset_alignment, offset); - const frame_size = calc_frame_size(terminal_buffer, &dur_movie); // Convert dur fps to frames per ms const frame_time: u32 = @trunc(1000 / dur_movie.framerate); @@ -496,7 +477,6 @@ pub fn init( .terminal_buffer = terminal_buffer, .frames = 0, .time_previous = std.Io.Timestamp.now(io, .real).toMilliseconds(), - .frame_size = frame_size, .start_pos = start_pos, .full_color = full_color, .animate = animate, @@ -531,9 +511,8 @@ fn deinit(self: *DurFile) void { } fn realloc(self: *DurFile) !void { - // when terminal size changes, we need to recalculate the start_pos and frame_size based on the new size + // when terminal size changes, we need to recalculate the start_pos based on the new size self.start_pos = calc_start_position(self.terminal_buffer, &self.dur_movie, self.offset_alignment, self.offset); - self.frame_size = calc_frame_size(self.terminal_buffer, &self.dur_movie); } fn draw(self: *DurFile) void { @@ -541,24 +520,14 @@ fn draw(self: *DurFile) void { const current_frame = self.dur_movie.frames.items[self.frames]; - const buf_width: u32 = @intCast(self.terminal_buffer.width); - const buf_height: u32 = @intCast(self.terminal_buffer.height); - // y is used as an iterator in the durformat, while cell_y gives us the correct placement for the cell (same for x) - for (0..self.frame_size[VEC_Y]) |y| { - const y_offset_i = @as(i32, @intCast(y)) + self.start_pos[VEC_Y]; - // we skip the pass if it falls outside of the draw window (ensure no int underflow) - const cell_y: u32 = if (y_offset_i >= 0 and y_offset_i < buf_height) @intCast(y_offset_i) else continue; + for (0..@intCast(self.dur_movie.lines)) |y| { + const cell_y = @as(i32, @intCast(y)) + self.start_pos[VEC_Y]; var iter = std.unicode.Utf8View.initUnchecked(current_frame.contents[y]).iterator(); - for (0..self.frame_size[VEC_X]) |x| { - const x_offset_i = @as(i32, @intCast(x)) + self.start_pos[VEC_X]; - // skip pass, same as y but also increment the codepoint iter to fetch correct values in later passes - const cell_x: u32 = if (x_offset_i >= 0 and x_offset_i < buf_width) @intCast(x_offset_i) else { - _ = iter.nextCodepoint().?; - continue; - }; + for (0..@intCast(self.dur_movie.columns)) |x| { + const cell_x = @as(i32, @intCast(x)) + self.start_pos[VEC_X]; const codepoint: u21 = iter.nextCodepoint().?; const color_map = current_frame.colorMap[x][y]; @@ -576,7 +545,7 @@ fn draw(self: *DurFile) void { const cell = Cell{ .ch = @intCast(codepoint), .fg = fg_color, .bg = bg_color }; - cell.put(cell_x, cell_y); + self.terminal_buffer.setCellBoundsChecked(cell_x, cell_y, cell); } } From 9ff4ddd129a607ffb8f8fe56f0f09e5f0ffdfaab Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Sun, 10 May 2026 13:55:19 +0200 Subject: [PATCH 117/176] Improve keyboard handling (closes #982) Signed-off-by: AnErrupTion --- ly-ui/src/keyboard.zig | 55 ++++++++++++++++++++++-------------------- 1 file changed, 29 insertions(+), 26 deletions(-) diff --git a/ly-ui/src/keyboard.zig b/ly-ui/src/keyboard.zig index 2e6e0e2..a90148b 100644 --- a/ly-ui/src/keyboard.zig +++ b/ly-ui/src/keyboard.zig @@ -171,6 +171,7 @@ pub fn getKeyList(allocator: Allocator, tb_event: termbox.tb_event) !KeyList { const code = if (tb_event.ch == 0 and tb_event.key < 128) tb_event.key else tb_event.ch; switch (code) { + // Non-standard control codes 0 => { key.ctrl = true; key.@"2" = true; @@ -342,7 +343,9 @@ pub fn getKeyList(allocator: Allocator, tb_event: termbox.tb_event) !KeyList { key = std.mem.zeroes(Key); key._ = true; }, + // Standard ASCII characters 32 => { + key = std.mem.zeroes(Key); key.@" " = true; }, 33 => { @@ -370,6 +373,7 @@ pub fn getKeyList(allocator: Allocator, tb_event: termbox.tb_event) !KeyList { key.@"&" = true; }, 39 => { + key = std.mem.zeroes(Key); key.@"'" = true; }, 40 => { @@ -389,74 +393,86 @@ pub fn getKeyList(allocator: Allocator, tb_event: termbox.tb_event) !KeyList { key.@"+" = true; }, 44 => { + key = std.mem.zeroes(Key); key.@"," = true; }, 45 => { + key = std.mem.zeroes(Key); key.@"-" = true; }, 46 => { + key = std.mem.zeroes(Key); key.@"." = true; }, 47 => { + key = std.mem.zeroes(Key); key.@"/" = true; }, 48 => { + key = std.mem.zeroes(Key); key.@"0" = true; }, 49 => { + key = std.mem.zeroes(Key); key.@"1" = true; }, 50 => { + key = std.mem.zeroes(Key); key.@"2" = true; }, 51 => { + key = std.mem.zeroes(Key); key.@"3" = true; }, 52 => { + key = std.mem.zeroes(Key); key.@"4" = true; }, 53 => { + key = std.mem.zeroes(Key); key.@"5" = true; }, 54 => { + key = std.mem.zeroes(Key); key.@"6" = true; }, 55 => { + key = std.mem.zeroes(Key); key.@"7" = true; }, 56 => { + key = std.mem.zeroes(Key); key.@"8" = true; }, 57 => { + key = std.mem.zeroes(Key); key.@"9" = true; }, 58 => { - key.shift = true; + key = std.mem.zeroes(Key); key.@":" = true; }, 59 => { + key = std.mem.zeroes(Key); key.@";" = true; }, 60 => { - key.shift = true; + key = std.mem.zeroes(Key); key.@"<" = true; }, 61 => { + key = std.mem.zeroes(Key); key.@"=" = true; }, 62 => { - key.shift = true; + key = std.mem.zeroes(Key); key.@">" = true; }, 63 => { - key.shift = true; + key = std.mem.zeroes(Key); key.@"?" = true; }, 64 => { - key.shift = true; - key.@"2" = true; - try keys.append(allocator, key); - key = std.mem.zeroes(Key); key.@"@" = true; }, @@ -565,12 +581,15 @@ pub fn getKeyList(allocator: Allocator, tb_event: termbox.tb_event) !KeyList { key.z = true; }, 91 => { + key = std.mem.zeroes(Key); key.@"[" = true; }, 92 => { + key = std.mem.zeroes(Key); key.@"\\" = true; }, 93 => { + key = std.mem.zeroes(Key); key.@"]" = true; }, 94 => { @@ -578,14 +597,11 @@ pub fn getKeyList(allocator: Allocator, tb_event: termbox.tb_event) !KeyList { key.@"^" = true; }, 95 => { - key.shift = true; - key.@"-" = true; - try keys.append(allocator, key); - key = std.mem.zeroes(Key); key._ = true; }, 96 => { + key = std.mem.zeroes(Key); key.@"`" = true; }, 97 => { @@ -667,34 +683,21 @@ pub fn getKeyList(allocator: Allocator, tb_event: termbox.tb_event) !KeyList { 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 = std.mem.zeroes(Key); 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; }, From ee3196bab89edb2bcdd2359903586bf288c8177f Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Mon, 11 May 2026 21:00:52 +0200 Subject: [PATCH 118/176] Fix labels_max_length calculation (closes #984) Signed-off-by: AnErrupTion --- src/main.zig | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/src/main.zig b/src/main.zig index 35239d7..6413935 100644 --- a/src/main.zig +++ b/src/main.zig @@ -380,7 +380,16 @@ pub fn main(init: std.process.Init) !void { // Initialize terminal buffer try state.log_file.info(state.io, "tui", "initializing terminal buffer", .{}); - state.labels_max_length = @max(TerminalBuffer.strWidth(state.lang.login), TerminalBuffer.strWidth(state.lang.password)); + var labels = [_][]const u8{ + state.lang.login, + state.lang.password, + state.lang.wayland, + state.lang.x11, + state.lang.shell, + state.lang.xinitrc, + state.lang.custom, + }; + state.labels_max_length = maxWidths(&labels); var seed: u64 = undefined; state.io.random(std.mem.asBytes(&seed)); // Get a random seed for the PRNG (used by animations) @@ -1333,6 +1342,16 @@ pub fn main(init: std.process.Init) !void { ); } +fn maxWidths(labels: [][]const u8) usize { + var max_width: usize = 0; + + for (labels) |label| { + max_width = @max(max_width, TerminalBuffer.strWidth(label)); + } + + return max_width; +} + fn uiErrorHandler(err: anyerror, ctx: *anyopaque) anyerror!void { var state: *UiState = @ptrCast(@alignCast(ctx)); From de8579854c3c8d704d7eff548ef151ffd2fec2f5 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Mon, 11 May 2026 21:03:01 +0200 Subject: [PATCH 119/176] Use $EXECUTABLE_NAME in kmscon service Signed-off-by: AnErrupTion --- res/ly-kmsconvt@.service | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/res/ly-kmsconvt@.service b/res/ly-kmsconvt@.service index 3e7d1fd..80eb1da 100644 --- a/res/ly-kmsconvt@.service +++ b/res/ly-kmsconvt@.service @@ -5,7 +5,7 @@ After=kmsconvt@%i.service Conflicts=kmsconvt@%i.service [Service] -ExecStart=$PREFIX_DIRECTORY/bin/kmscon --font-engine unifont --vt=%I --seats=seat0 --login -- $PREFIX_DIRECTORY/bin/ly --use-kmscon-vt +ExecStart=$PREFIX_DIRECTORY/bin/kmscon --font-engine unifont --vt=%I --seats=seat0 --login -- $PREFIX_DIRECTORY/bin/$EXECUTABLE_NAME --use-kmscon-vt StandardInput=tty UtmpIdentifier=%I TTYPath=/dev/%I From 741e9e034571a2a7d84486877fd665dbb3c22a4a Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Tue, 12 May 2026 20:35:17 +0200 Subject: [PATCH 120/176] Use correct naming convention for functions in DurFile.zig Signed-off-by: AnErrupTion --- src/animations/DurFile.zig | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/src/animations/DurFile.zig b/src/animations/DurFile.zig index 969157b..5999dbf 100644 --- a/src/animations/DurFile.zig +++ b/src/animations/DurFile.zig @@ -19,7 +19,7 @@ const LogFile = ly_core.LogFile; const enums = @import("../enums.zig"); const DurOffsetAlignment = enums.DurOffsetAlignment; -fn read_decompress_file(allocator: Allocator, io: std.Io, file_path: []const u8) ![]u8 { +fn readDecompressFile(allocator: Allocator, io: std.Io, file_path: []const u8) ![]u8 { const file_buffer = std.Io.Dir.cwd().openFile(io, file_path, .{}) catch { return error.FileNotFound; }; @@ -113,7 +113,7 @@ const DurFormatRaw = struct { }; } - fn parse_dur_from_json(self: *DurFormatRaw, allocator: Allocator, dur_json_root: Json.Value) !void { + fn parseFromJson(self: *DurFormatRaw, allocator: Allocator, dur_json_root: Json.Value) !void { var dur_movie = if (dur_json_root.object.get("DurMovie")) |dm| dm.object else return error.NotValidFile; // Depending on the version, a dur file can have different json object names (ie: columns vs sizeX) @@ -160,14 +160,14 @@ const DurFormatRaw = struct { } } - pub fn create_from_file(self: *DurFormatRaw, allocator: Allocator, io: std.Io, file_path: []const u8) !void { - const file_decompressed = try read_decompress_file(allocator, io, file_path); + pub fn createFromFile(self: *DurFormatRaw, allocator: Allocator, io: std.Io, file_path: []const u8) !void { + const file_decompressed = try readDecompressFile(allocator, io, file_path); defer allocator.free(file_decompressed); const parsed = try Json.parseFromSlice(Json.Value, allocator, file_decompressed, .{}); defer parsed.deinit(); - try parse_dur_from_json(self, allocator, parsed.value); + try parseFromJson(self, allocator, parsed.value); } pub fn init(allocator: Allocator) DurFormatRaw { @@ -266,7 +266,7 @@ const durcolor_table_to_color16 = [17]u32{ 15, // 16 bright white }; -fn sixcube_to_channel(sixcube: u32) u32 { +fn sixCubeToChannel(sixcube: u32) u32 { // Although the range top for the extended range is 0xFF, 6 is not divisible into 0xFF, // so we use 0xF0 instead with a scaler const equal_divisions = 0xF0 / 6; @@ -277,7 +277,7 @@ fn sixcube_to_channel(sixcube: u32) u32 { return if (sixcube > 0) (sixcube * equal_divisions) + scaler else 0; } -fn convert_256_to_rgb(color_256: u32) u32 { +fn convert256ToRgb(color_256: u32) u32 { var rgb_color: u32 = 0; // 0 - 15 is the standard color range, map to array table @@ -293,9 +293,9 @@ fn convert_256_to_rgb(color_256: u32) u32 { // divide by 1 gets the height of the cube (divide 1 for clarity for what we are doing) // each channel can be 6 levels of brightness hence remander operation of 6 // finally bitshift to correct rgb channel (16 for red, 8 for green, 0 for blue) - rgb_color |= sixcube_to_channel(((color_256 - 16) / 36) % 6) << 16; - rgb_color |= sixcube_to_channel(((color_256 - 16) / 6) % 6) << 8; - rgb_color |= sixcube_to_channel(((color_256 - 16) / 1) % 6); + rgb_color |= sixCubeToChannel(((color_256 - 16) / 36) % 6) << 16; + rgb_color |= sixCubeToChannel(((color_256 - 16) / 6) % 6) << 8; + rgb_color |= sixCubeToChannel(((color_256 - 16) / 1) % 6); } // 232 - 255 is the grayscale range else { @@ -353,7 +353,7 @@ fn center(v: i64) i64 { return @intCast(@divTrunc(v, 2) + @mod(v, 2)); } -fn calc_start_position(terminal_buffer: *TerminalBuffer, dur_movie: *DurFormat, offset_alignment: DurOffsetAlignment, offset: IVec2) IVec2 { +fn calculateStartPos(terminal_buffer: *TerminalBuffer, dur_movie: *DurFormat, offset_alignment: DurOffsetAlignment, offset: IVec2) IVec2 { const buf_width: i64 = @intCast(terminal_buffer.width); const buf_height: i64 = @intCast(terminal_buffer.height); @@ -392,7 +392,7 @@ pub fn init( var dur_movie_raw: DurFormatRaw = .init(allocator); defer dur_movie_raw.deinit(); - dur_movie_raw.create_from_file(allocator, io, file_path) catch |err| switch (err) { + dur_movie_raw.createFromFile(allocator, io, file_path) catch |err| switch (err) { error.FileNotFound => { try log_file.err(io, "tui", "dur_file was not found at: {s}", .{file_path}); return err; @@ -464,7 +464,7 @@ pub fn init( const offset: IVec2 = .{ x_offset, y_offset }; - const start_pos = calc_start_position(terminal_buffer, &dur_movie, offset_alignment, offset); + const start_pos = calculateStartPos(terminal_buffer, &dur_movie, offset_alignment, offset); // Convert dur fps to frames per ms const frame_time: u32 = @trunc(1000 / dur_movie.framerate); @@ -512,7 +512,7 @@ fn deinit(self: *DurFile) void { fn realloc(self: *DurFile) !void { // when terminal size changes, we need to recalculate the start_pos based on the new size - self.start_pos = calc_start_position(self.terminal_buffer, &self.dur_movie, self.offset_alignment, self.offset); + self.start_pos = calculateStartPos(self.terminal_buffer, &self.dur_movie, self.offset_alignment, self.offset); } fn draw(self: *DurFile) void { @@ -540,8 +540,8 @@ fn draw(self: *DurFile) void { color_map_1 = durcolor_table_to_color16[color_map_1 + 1]; // Add 1, dur source stores it like this for some reason } - const fg_color = if (self.full_color) convert_256_to_rgb(color_map_0) else tb_color_16[color_map_0]; - const bg_color = if (self.full_color) convert_256_to_rgb(color_map_1) else tb_color_16[color_map_1]; + const fg_color = if (self.full_color) convert256ToRgb(color_map_0) else tb_color_16[color_map_0]; + const bg_color = if (self.full_color) convert256ToRgb(color_map_1) else tb_color_16[color_map_1]; const cell = Cell{ .ch = @intCast(codepoint), .fg = fg_color, .bg = bg_color }; From 9b1965a3d813ddd3c34e51a2772b58d82aab78fb Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Fri, 15 May 2026 21:42:13 +0200 Subject: [PATCH 121/176] Always build translate-c in Debug Signed-off-by: AnErrupTion --- ly-core/build.zig | 1 - ly-ui/build.zig | 1 - 2 files changed, 2 deletions(-) diff --git a/ly-core/build.zig b/ly-core/build.zig index f6574de..56dc4ec 100644 --- a/ly-core/build.zig +++ b/ly-core/build.zig @@ -16,7 +16,6 @@ pub fn build(b: *std.Build) void { const translate_c = b.dependency("translate_c", .{ .target = target, - .optimize = optimize, }); addCImport(b, mod, translate_c, target, optimize, "pam", "#include "); diff --git a/ly-ui/build.zig b/ly-ui/build.zig index 7397873..2e3ce6b 100644 --- a/ly-ui/build.zig +++ b/ly-ui/build.zig @@ -25,7 +25,6 @@ pub fn build(b: *std.Build) void { const translate_c_dep = b.dependency("translate_c", .{ .target = target, - .optimize = optimize, }); const termbox2: Translator = .init(translate_c_dep, .{ From afee1d91944851f2f9306ad22d55a331ab5bfb20 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Fri, 15 May 2026 21:46:20 +0200 Subject: [PATCH 122/176] Remove TODO and stop forcing LLVM usage (again) Signed-off-by: AnErrupTion --- build.zig | 1 - ly-core/src/interop.zig | 1 - 2 files changed, 2 deletions(-) diff --git a/build.zig b/build.zig index 45a548b..9e89029 100644 --- a/build.zig +++ b/build.zig @@ -69,7 +69,6 @@ pub fn build(b: *std.Build) !void { .optimize = optimize, .link_libc = true, }), - .use_llvm = true, }); const ly_ui = b.dependency("ly_ui", .{ diff --git a/ly-core/src/interop.zig b/ly-core/src/interop.zig index f23583c..69e02ff 100644 --- a/ly-core/src/interop.zig +++ b/ly-core/src/interop.zig @@ -235,7 +235,6 @@ fn PlatformStruct() type { const platform_struct = PlatformStruct(); -// TODO 0.16.0: Can we get away with this? pub fn isError(result: anytype) bool { if (@typeInfo(@TypeOf(result)).int.signedness == .signed) { return result < 0; From 78794b3e10005291ffa9bde92e27568241bec8b7 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Fri, 15 May 2026 23:22:50 +0200 Subject: [PATCH 123/176] Stream dur reading instead of reading all at once Signed-off-by: AnErrupTion --- src/animations/DurFile.zig | 38 ++++++++++++++------------------------ 1 file changed, 14 insertions(+), 24 deletions(-) diff --git a/src/animations/DurFile.zig b/src/animations/DurFile.zig index 5999dbf..ca3922c 100644 --- a/src/animations/DurFile.zig +++ b/src/animations/DurFile.zig @@ -19,25 +19,6 @@ const LogFile = ly_core.LogFile; const enums = @import("../enums.zig"); const DurOffsetAlignment = enums.DurOffsetAlignment; -fn readDecompressFile(allocator: Allocator, io: std.Io, file_path: []const u8) ![]u8 { - const file_buffer = std.Io.Dir.cwd().openFile(io, file_path, .{}) catch { - return error.FileNotFound; - }; - defer file_buffer.close(io); - - var file_reader_buffer: [4096]u8 = undefined; - var decompress_buffer: [flate.max_window_len]u8 = undefined; - - var file_reader = file_buffer.reader(io, &file_reader_buffer); - var decompress: flate.Decompress = .init(&file_reader.interface, .gzip, &decompress_buffer); - - const file_decompressed = decompress.reader.allocRemaining(allocator, .unlimited) catch { - return error.NotValidFile; - }; - - return file_decompressed; -} - const Frame = struct { frameNumber: i32, delay: f32, @@ -161,13 +142,22 @@ const DurFormatRaw = struct { } pub fn createFromFile(self: *DurFormatRaw, allocator: Allocator, io: std.Io, file_path: []const u8) !void { - const file_decompressed = try readDecompressFile(allocator, io, file_path); - defer allocator.free(file_decompressed); + const file = try std.Io.Dir.cwd().openFile(io, file_path, .{}); + defer file.close(io); - const parsed = try Json.parseFromSlice(Json.Value, allocator, file_decompressed, .{}); - defer parsed.deinit(); + var reader_buffer: [4096]u8 = undefined; + var decompress_buffer: [flate.max_window_len]u8 = undefined; - try parseFromJson(self, allocator, parsed.value); + var file_reader = file.reader(io, &reader_buffer); + var decompress: flate.Decompress = .init(&file_reader.interface, .gzip, &decompress_buffer); + + var json_reader = Json.Reader.init(allocator, &decompress.reader); + defer json_reader.deinit(); + + const json = try Json.parseFromTokenSource(Json.Value, allocator, &json_reader, .{}); + defer json.deinit(); + + try parseFromJson(self, allocator, json.value); } pub fn init(allocator: Allocator) DurFormatRaw { From 05c1d4becee7ca4812b2d2a395659d6bb0e2f3b5 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Sat, 16 May 2026 10:33:06 +0200 Subject: [PATCH 124/176] Improve errors for lock state Signed-off-by: AnErrupTion --- src/main.zig | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/src/main.zig b/src/main.zig index 6413935..a2a8512 100644 --- a/src/main.zig +++ b/src/main.zig @@ -1788,7 +1788,7 @@ fn updateNumlock(self: *Label, ptr: *anyopaque) !void { try state.log_file.err( state.io, "sys", - "failed to get lock state: {s}", + "failed to get lock state for numlock: {s}", .{@errorName(err)}, ); return; @@ -1802,8 +1802,17 @@ fn updateCapslock(self: *Label, ptr: *anyopaque) !void { const lock_state = interop.getLockState() catch |err| { self.update_fn = null; - try state.info_line.addMessage(state.lang.err_lock_state, state.config.error_bg, state.config.error_fg); - try state.log_file.err(state.io, "sys", "failed to get lock state: {s}", .{@errorName(err)}); + try state.info_line.addMessage( + state.lang.err_lock_state, + state.config.error_bg, + state.config.error_fg, + ); + try state.log_file.err( + state.io, + "sys", + "failed to get lock state for capslock: {s}", + .{@errorName(err)}, + ); return; }; From 4c066ce564f18080b38422b2a2f28261c87fc1ae Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Sun, 17 May 2026 13:26:46 +0200 Subject: [PATCH 125/176] Handle termbox2 errors Signed-off-by: AnErrupTion --- ly-ui/src/Cell.zig | 4 +- ly-ui/src/TerminalBuffer.zig | 108 ++++++++++++++++++------------ ly-ui/src/components/BigLabel.zig | 2 +- ly-ui/src/components/Box.zig | 22 +++--- ly-ui/src/components/Label.zig | 4 +- ly-ui/src/components/Text.zig | 8 +-- ly-ui/src/components/generic.zig | 8 +-- src/animations/Cascade.zig | 6 +- src/animations/ColorMix.zig | 2 +- src/animations/Doom.zig | 6 +- src/animations/DurFile.zig | 2 +- src/animations/GameOfLife.zig | 2 +- src/animations/Matrix.zig | 4 +- src/components/InfoLine.zig | 6 +- src/components/Session.zig | 4 +- src/components/UserList.zig | 4 +- src/main.zig | 12 ++-- 17 files changed, 114 insertions(+), 90 deletions(-) diff --git a/ly-ui/src/Cell.zig b/ly-ui/src/Cell.zig index 35d0cf0..59eb4aa 100644 --- a/ly-ui/src/Cell.zig +++ b/ly-ui/src/Cell.zig @@ -14,8 +14,8 @@ pub fn init(ch: u32, fg: u32, bg: u32) Cell { }; } -pub fn put(self: Cell, x: usize, y: usize) void { +pub fn put(self: Cell, x: usize, y: usize) !void { if (self.ch == 0) return; - TerminalBuffer.setCell(x, y, self); + try TerminalBuffer.setCell(x, y, self); } diff --git a/ly-ui/src/TerminalBuffer.zig b/ly-ui/src/TerminalBuffer.zig index 1fd9999..d0b3e3d 100644 --- a/ly-ui/src/TerminalBuffer.zig +++ b/ly-ui/src/TerminalBuffer.zig @@ -103,24 +103,39 @@ pub fn init( random: Random, ) !TerminalBuffer { // Initialize termbox - _ = termbox.tb_init(); + if (termbox.tb_init() != 0) return error.TermboxInitFailed; if (options.full_color) { - _ = termbox.tb_set_output_mode(termbox.TB_OUTPUT_TRUECOLOR); - try log_file.info(io, "tui", "termbox2 set to 24-bit color output mode", .{}); + if (termbox.tb_set_output_mode(termbox.TB_OUTPUT_TRUECOLOR) != 0) { + return error.TermboxSetOutputModeFailed; + } + try log_file.info( + io, + "tui", + "termbox2 set to 24-bit color output mode", + .{}, + ); } else { - try log_file.info(io, "tui", "termbox2 set to eight-color output mode", .{}); + try log_file.info( + io, + "tui", + "termbox2 set to eight-color output mode", + .{}, + ); } - _ = termbox.tb_clear(); - // Let's take some precautions here and clear the back buffer as well - try clearBackBuffer(); + try clearScreen(true); - const width: usize = @intCast(termbox.tb_width()); - const height: usize = @intCast(termbox.tb_height()); + const width = getWidth(); + const height = getHeight(); - try log_file.info(io, "tui", "screen resolution is {d}x{d}", .{ width, height }); + try log_file.info( + io, + "tui", + "screen resolution is {d}x{d}", + .{ width, height }, + ); return .{ .log_file = log_file, @@ -163,7 +178,7 @@ pub fn init( pub fn deinit(self: *TerminalBuffer) void { self.keybinds.deinit(); - TerminalBuffer.shutdown(); + TerminalBuffer.shutdown() catch {}; } pub fn runEventLoop( @@ -238,7 +253,7 @@ pub fn runEventLoop( } } - TerminalBuffer.presentBuffer(); + try TerminalBuffer.presentBuffer(); } if (inactivity_event_fn) |inactivity_fn| { @@ -338,31 +353,35 @@ pub fn getHeight() usize { return @intCast(termbox.tb_height()); } -pub fn setCursor(x: usize, y: usize) void { - _ = termbox.tb_set_cursor(@intCast(x), @intCast(y)); +pub fn setCursor(x: usize, y: usize) !void { + if (termbox.tb_set_cursor(@intCast(x), @intCast(y)) != 0) { + return error.TermboxSetCursorFailed; + } } pub fn clearScreen(clear_back_buffer: bool) !void { - _ = termbox.tb_clear(); + if (termbox.tb_clear() != 0) return error.TermboxClearFailed; if (clear_back_buffer) try clearBackBuffer(); } -pub fn shutdown() void { - _ = termbox.tb_shutdown(); +pub fn shutdown() !void { + if (termbox.tb_shutdown() != 0) return error.TermboxShutdownFailed; } -pub fn presentBuffer() void { - _ = termbox.tb_present(); +pub fn presentBuffer() !void { + if (termbox.tb_present() != 0) return error.TermboxPresentFailed; } pub fn getCell(x: usize, y: usize) ?Cell { var maybe_cell: ?*termbox.tb_cell = undefined; - _ = termbox.tb_get_cell( + if (termbox.tb_get_cell( @intCast(x), @intCast(y), 1, &maybe_cell, - ); + ) != 0) { + return null; + } if (maybe_cell) |cell| { return Cell.init(cell.ch, cell.fg, cell.bg); @@ -371,29 +390,31 @@ pub fn getCell(x: usize, y: usize) ?Cell { return null; } -pub fn setCell(x: usize, y: usize, cell: Cell) void { - _ = termbox.tb_set_cell( +pub fn setCell(x: usize, y: usize, cell: Cell) !void { + if (termbox.tb_set_cell( @intCast(x), @intCast(y), cell.ch, cell.fg, cell.bg, - ); + ) != 0) { + return error.TermboxSetCellFailed; + } } -pub fn setCellBoundsChecked(self: *TerminalBuffer, x: isize, y: isize, cell: Cell) void { +pub fn setCellBoundsChecked(self: *TerminalBuffer, x: isize, y: isize, cell: Cell) !void { if (0 <= x and x < self.width and 0 <= y and y < self.height) { - cell.put(@intCast(x), @intCast(y)); + try cell.put(@intCast(x), @intCast(y)); } } pub fn reclaim(self: TerminalBuffer) !void { if (self.termios) |termios| { // Take back control of the TTY - _ = termbox.tb_init(); + if (termbox.tb_init() != 0) return error.TermboxReinitFailed; - if (self.full_color) { - _ = termbox.tb_set_output_mode(termbox.TB_OUTPUT_TRUECOLOR); + if (self.full_color and termbox.tb_set_output_mode(termbox.TB_OUTPUT_TRUECOLOR) != 0) { + return error.TermboxSetOutputModeFailed; } try std.posix.tcsetattr(std.posix.STDIN_FILENO, .FLUSH, termios); @@ -464,14 +485,14 @@ pub fn drawText( y: usize, fg: u32, bg: u32, -) void { - const yc: c_int = @intCast(y); +) !void { const utf8view = std.unicode.Utf8View.init(text) catch return; var utf8 = utf8view.iterator(); - var i: c_int = @intCast(x); - while (utf8.nextCodepoint()) |codepoint| : (i += termbox.tb_wcwidth(codepoint)) { - _ = termbox.tb_set_cell(i, yc, codepoint, fg, bg); + var i = x; + while (utf8.nextCodepoint()) |codepoint| : (i += @intCast(termbox.tb_wcwidth(codepoint))) { + const cell = Cell.init(codepoint, fg, bg); + try cell.put(i, y); } } @@ -482,15 +503,16 @@ pub fn drawConfinedText( max_length: usize, fg: u32, bg: u32, -) void { - const yc: c_int = @intCast(y); +) !void { const utf8view = std.unicode.Utf8View.init(text) catch return; var utf8 = utf8view.iterator(); - var i: c_int = @intCast(x); - while (utf8.nextCodepoint()) |codepoint| : (i += termbox.tb_wcwidth(codepoint)) { - if (i - @as(c_int, @intCast(x)) >= max_length) break; - _ = termbox.tb_set_cell(i, yc, codepoint, fg, bg); + var i = x; + while (utf8.nextCodepoint()) |codepoint| : (i += @intCast(termbox.tb_wcwidth(codepoint))) { + if (i - x >= max_length) break; + + const cell = Cell.init(codepoint, fg, bg); + try cell.put(i, y); } } @@ -501,9 +523,9 @@ pub fn drawCharMultiple( length: usize, fg: u32, bg: u32, -) void { +) !void { const cell = Cell.init(char, fg, bg); - for (0..length) |xx| cell.put(x + xx, y); + for (0..length) |xx| try cell.put(x + xx, y); } // Every codepoint is assumed to have a width of 1. @@ -521,6 +543,8 @@ pub fn strWidth(str: []const u8) usize { } fn clearBackBuffer() !void { + if (termbox.global.initialized == 0) return; + // Clear the TTY because termbox2 doesn't seem to do it properly const capability = termbox.global.caps[termbox.TB_CAP_CLEAR_SCREEN]; const capability_slice = std.mem.span(capability); diff --git a/ly-ui/src/components/BigLabel.zig b/ly-ui/src/components/BigLabel.zig index bd9af6e..cb2f6b5 100644 --- a/ly-ui/src/components/BigLabel.zig +++ b/ly-ui/src/components/BigLabel.zig @@ -207,7 +207,7 @@ fn alphaBlit(x: usize, y: usize, tb_width: usize, tb_height: usize, cells: [CHAR for (0..CHAR_HEIGHT) |yy| { for (0..CHAR_WIDTH) |xx| { const cell = cells[yy * CHAR_WIDTH + xx]; - cell.put(x + xx, y + yy); + cell.put(x + xx, y + yy) catch {}; } } } diff --git a/ly-ui/src/components/Box.zig b/ly-ui/src/components/Box.zig index 7c753ae..af3a687 100644 --- a/ly-ui/src/components/Box.zig +++ b/ly-ui/src/components/Box.zig @@ -129,29 +129,29 @@ fn draw(self: *Box) void { self.bg, ); - left_up.put(self.left_pos.x - 1, self.left_pos.y - 1); - right_up.put(self.right_pos.x, self.left_pos.y - 1); - left_down.put(self.left_pos.x - 1, self.right_pos.y); - right_down.put(self.right_pos.x, self.right_pos.y); + left_up.put(self.left_pos.x - 1, self.left_pos.y - 1) catch {}; + right_up.put(self.right_pos.x, self.left_pos.y - 1) catch {}; + left_down.put(self.left_pos.x - 1, self.right_pos.y) catch {}; + right_down.put(self.right_pos.x, self.right_pos.y) catch {}; for (0..self.width) |i| { - top.put(self.left_pos.x + i, self.left_pos.y - 1); - bottom.put(self.left_pos.x + i, self.right_pos.y); + top.put(self.left_pos.x + i, self.left_pos.y - 1) catch {}; + bottom.put(self.left_pos.x + i, self.right_pos.y) catch {}; } top.ch = self.buffer.box_chars.left; bottom.ch = self.buffer.box_chars.right; for (0..self.height) |i| { - top.put(self.left_pos.x - 1, self.left_pos.y + i); - bottom.put(self.right_pos.x, self.left_pos.y + i); + top.put(self.left_pos.x - 1, self.left_pos.y + i) catch {}; + bottom.put(self.right_pos.x, self.left_pos.y + i) catch {}; } } if (self.blank_box) { for (0..self.height) |y| { for (0..self.width) |x| { - self.buffer.blank_cell.put(self.left_pos.x + x, self.left_pos.y + y); + self.buffer.blank_cell.put(self.left_pos.x + x, self.left_pos.y + y) catch {}; } } } @@ -164,7 +164,7 @@ fn draw(self: *Box) void { self.width, self.title_fg, self.bg, - ); + ) catch {}; } if (self.bottom_title) |title| { @@ -175,7 +175,7 @@ fn draw(self: *Box) void { self.width, self.title_fg, self.bg, - ); + ) catch {}; } } diff --git a/ly-ui/src/components/Label.zig b/ly-ui/src/components/Label.zig index 9874fcb..d54cb9c 100644 --- a/ly-ui/src/components/Label.zig +++ b/ly-ui/src/components/Label.zig @@ -117,7 +117,7 @@ fn draw(self: *Label) void { width, self.fg, self.bg, - ); + ) catch {}; return; } @@ -127,7 +127,7 @@ fn draw(self: *Label) void { self.component_pos.y, self.fg, self.bg, - ); + ) catch {}; } fn update(self: *Label, ctx: *anyopaque) !void { diff --git a/ly-ui/src/components/Text.zig b/ly-ui/src/components/Text.zig index 4fc35bf..0ca5312 100644 --- a/ly-ui/src/components/Text.zig +++ b/ly-ui/src/components/Text.zig @@ -131,14 +131,14 @@ pub fn handle(self: *Text, maybe_key: ?keyboard.Key) !void { } if (self.masked and self.maybe_mask == null) { - TerminalBuffer.setCursor( + try TerminalBuffer.setCursor( self.component_pos.x, self.component_pos.y, ); return; } - TerminalBuffer.setCursor( + try TerminalBuffer.setCursor( self.component_pos.x + (self.cursor - self.visible_start), self.component_pos.y, ); @@ -159,7 +159,7 @@ fn draw(self: *Text) void { length, self.fg, self.bg, - ); + ) catch {}; } return; } @@ -182,7 +182,7 @@ fn draw(self: *Text) void { self.component_pos.y, self.fg, self.bg, - ); + ) catch {}; } fn goLeft(ptr: *anyopaque) !bool { diff --git a/ly-ui/src/components/generic.zig b/ly-ui/src/components/generic.zig index 0a76c4f..572fe3f 100644 --- a/ly-ui/src/components/generic.zig +++ b/ly-ui/src/components/generic.zig @@ -105,8 +105,8 @@ pub fn CyclableLabel(comptime ItemType: type, comptime ChangeItemType: type) typ self.current = self.list.items.len - 1; } - pub fn handle(self: *Self, _: ?keyboard.Key) void { - TerminalBuffer.setCursor( + pub fn handle(self: *Self, _: ?keyboard.Key) !void { + try TerminalBuffer.setCursor( self.component_pos.x + self.cursor + 2, self.component_pos.y, ); @@ -119,11 +119,11 @@ pub fn CyclableLabel(comptime ItemType: type, comptime ChangeItemType: type) typ var left_arrow = Cell.init('<', self.fg, self.bg); var right_arrow = Cell.init('>', self.fg, self.bg); - left_arrow.put(self.component_pos.x, self.component_pos.y); + left_arrow.put(self.component_pos.x, self.component_pos.y) catch {}; right_arrow.put( self.component_pos.x + self.width - 1, self.component_pos.y, - ); + ) catch {}; const current_item = self.list.items[self.current]; const x = self.component_pos.x + 2; diff --git a/src/animations/Cascade.zig b/src/animations/Cascade.zig index 587cd37..c9b7769 100644 --- a/src/animations/Cascade.zig +++ b/src/animations/Cascade.zig @@ -71,14 +71,14 @@ fn draw(self: *Cascade) void { if ((self.buffer.random.int(u16) % 10) > 7) continue; - cell.?.put(x, y); + cell.?.put(x, y) catch {}; var space = Cell.init( ' ', cell_under.?.fg, cell_under.?.bg, ); - space.put(x, y - 1); + space.put(x, y - 1) catch {}; } } @@ -87,6 +87,6 @@ fn draw(self: *Cascade) void { self.current_auth_fails.* = 0; } - TerminalBuffer.presentBuffer(); + TerminalBuffer.presentBuffer() catch {}; } } diff --git a/src/animations/ColorMix.zig b/src/animations/ColorMix.zig index 49c6238..e9063a3 100644 --- a/src/animations/ColorMix.zig +++ b/src/animations/ColorMix.zig @@ -114,7 +114,7 @@ fn draw(self: *ColorMix) void { } const cell = self.palette[@as(usize, @trunc(math.floor(length(uv) * 5.0))) % palette_len]; - cell.put(x, y); + cell.put(x, y) catch {}; } } } diff --git a/src/animations/Doom.zig b/src/animations/Doom.zig index 9b0abae..504831a 100644 --- a/src/animations/Doom.zig +++ b/src/animations/Doom.zig @@ -129,13 +129,13 @@ fn draw(self: *Doom) void { // Send known fire levels to terminal buffer const from_cell = self.fire[level_buf_from]; const to_cell = self.fire[level_buf_to]; - from_cell.put(x, y); - to_cell.put(to_x, to_y); + from_cell.put(x, y) catch {}; + to_cell.put(to_x, to_y) catch {}; } // Draw bottom line (fire source) const src_cell = self.fire[STEPS]; - src_cell.put(x, self.terminal_buffer.height - 1); + src_cell.put(x, self.terminal_buffer.height - 1) catch {}; } } diff --git a/src/animations/DurFile.zig b/src/animations/DurFile.zig index ca3922c..95677f8 100644 --- a/src/animations/DurFile.zig +++ b/src/animations/DurFile.zig @@ -535,7 +535,7 @@ fn draw(self: *DurFile) void { const cell = Cell{ .ch = @intCast(codepoint), .fg = fg_color, .bg = bg_color }; - self.terminal_buffer.setCellBoundsChecked(cell_x, cell_y, cell); + self.terminal_buffer.setCellBoundsChecked(cell_x, cell_y, cell) catch {}; } } diff --git a/src/animations/GameOfLife.zig b/src/animations/GameOfLife.zig index c100c52..6e9a16c 100644 --- a/src/animations/GameOfLife.zig +++ b/src/animations/GameOfLife.zig @@ -146,7 +146,7 @@ fn draw(self: *GameOfLife) void { const row_offset = y * self.width; for (0..self.width) |x| { const cell = if (self.current_grid[row_offset + x]) alive_cell else self.dead_cell; - cell.put(x, y); + cell.put(x, y) catch {}; } } } diff --git a/src/animations/Matrix.zig b/src/animations/Matrix.zig index e7af489..3d43118 100644 --- a/src/animations/Matrix.zig +++ b/src/animations/Matrix.zig @@ -200,9 +200,9 @@ fn draw(self: *Matrix) void { .bg = self.terminal_buffer.bg, }; - cell.put(x, y - 1); + cell.put(x, y - 1) catch {}; // Fill background in between columns - self.default_cell.put(x + 1, y - 1); + self.default_cell.put(x + 1, y - 1) catch {}; } } } diff --git a/src/components/InfoLine.zig b/src/components/InfoLine.zig index 97a8a73..45b5da3 100644 --- a/src/components/InfoLine.zig +++ b/src/components/InfoLine.zig @@ -84,7 +84,7 @@ pub fn clearRendered(self: InfoLine, allocator: Allocator) !void { @memset(spaces, ' '); - TerminalBuffer.drawText( + try TerminalBuffer.drawText( spaces, self.label.component_pos.x + 2, self.label.component_pos.y, @@ -98,7 +98,7 @@ fn draw(self: *InfoLine) void { } fn handle(self: *InfoLine, maybe_key: ?keyboard.Key) !void { - self.label.handle(maybe_key); + try self.label.handle(maybe_key); } fn drawItem(label: *MessageLabel, message: Message, x: usize, y: usize, width: usize) void { @@ -114,5 +114,5 @@ fn drawItem(label: *MessageLabel, message: Message, x: usize, y: usize, width: u width, message.fg, message.bg, - ); + ) catch {}; } diff --git a/src/components/Session.zig b/src/components/Session.zig index 1e975c6..6cb252a 100644 --- a/src/components/Session.zig +++ b/src/components/Session.zig @@ -87,7 +87,7 @@ fn draw(self: *Session) void { } fn handle(self: *Session, maybe_key: ?keyboard.Key) !void { - self.label.handle(maybe_key); + try self.label.handle(maybe_key); } fn addedSession(env: Env, user_list: *UserList) void { @@ -119,5 +119,5 @@ fn drawItem(label: *EnvironmentLabel, env: Env, x: usize, y: usize, width: usize width, label.fg, label.bg, - ); + ) catch {}; } diff --git a/src/components/UserList.zig b/src/components/UserList.zig index c407173..9f01bc5 100644 --- a/src/components/UserList.zig +++ b/src/components/UserList.zig @@ -118,7 +118,7 @@ fn draw(self: *UserList) void { } fn handle(self: *UserList, maybe_key: ?keyboard.Key) !void { - self.label.handle(maybe_key); + try self.label.handle(maybe_key); } fn usernameChanged(user: User, maybe_session: ?*Session) void { @@ -143,5 +143,5 @@ fn drawItem(label: *UserLabel, user: User, x: usize, y: usize, width: usize) voi width, label.fg, label.bg, - ); + ) catch {}; } diff --git a/src/main.zig b/src/main.zig index a2a8512..0f6f1e0 100644 --- a/src/main.zig +++ b/src/main.zig @@ -56,12 +56,12 @@ fn signalHandler(sig: std.posix.SIG) callconv(.c) void { _ = std.c.waitpid(session_pid, &status, 0); } - TerminalBuffer.shutdown(); + TerminalBuffer.shutdown() catch {}; std.c.exit(@intCast(@intFromEnum(sig))); } fn ttyControlTransferSignalHandler(_: std.posix.SIG) callconv(.c) void { - TerminalBuffer.shutdown(); + TerminalBuffer.shutdown() catch {}; } const CustomBindLabel = struct { @@ -1484,7 +1484,7 @@ fn authenticate(ptr: *anyopaque) !bool { ); }; state.info_line.label.draw(); - TerminalBuffer.presentBuffer(); + try TerminalBuffer.presentBuffer(); return false; } @@ -1507,7 +1507,7 @@ fn authenticate(ptr: *anyopaque) !bool { ); }; state.info_line.label.draw(); - TerminalBuffer.presentBuffer(); + try TerminalBuffer.presentBuffer(); if (state.config.save) save_last_settings: { // It isn't worth cluttering the code with precise error @@ -1660,8 +1660,8 @@ fn authenticate(ptr: *anyopaque) !bool { } // Restore the cursor - TerminalBuffer.setCursor(0, 0); - TerminalBuffer.presentBuffer(); + try TerminalBuffer.setCursor(0, 0); + try TerminalBuffer.presentBuffer(); return false; } From 0cd8b2ebfc564427f7bb90c9e8e3dd672cec99cc Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Mon, 18 May 2026 19:46:28 +0200 Subject: [PATCH 126/176] README: Add section for testing config changes (closes #994) Signed-off-by: AnErrupTion --- readme.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/readme.md b/readme.md index 78ebffc..5cbe81d 100644 --- a/readme.md +++ b/readme.md @@ -95,6 +95,9 @@ $ zig build run > [!IMPORTANT] > While you can run Ly in a terminal emulator as root, it is **not** recommended. If you want to test Ly, please enable its service (as described below) and reboot your machine. +> [!NOTE] +> You can, however, test your configuration file changes like that. Note that you must do Ctrl+C in order to exit Ly. + The next sections will explain how to use Ly with a variety of init systems. Detailed explanation is only given for systemd, but should be applicable for all. > [!NOTE] From f03aca037952e9be481caefc989f97545a5e345f Mon Sep 17 00:00:00 2001 From: Louis Pate Date: Wed, 20 May 2026 20:17:11 +0200 Subject: [PATCH 127/176] Be specific about zig version in readme (#996) ## What are the changes about? There have been quite a few issues about the required zig version which have appeared on codeberg recently. Largely these issues are opened (like mine, #995) because individuals attempt to build using development versions of zig and not the tagged releases. The lack of a locked version (only a minimum) creates these issues. Barring a locking mechanism, which I do not feel is my place to implement, we should be clear to newer developers in Zig that you need this specific version, not a development/master versioned release. This is WIP mainly because I feel that a version file would be a better solution than a README note, and I don't have enough zig experience or knowledge of this repo to make a guess at what is best for this scenario. ## What existing issue does this resolve? N/A ## Pre-requisites - [x] I have tested & confirmed the changes work locally - [x] I have run `zig fmt` throughout my changes Co-authored-by: Louis Pate Reviewed-on: https://codeberg.org/fairyglade/ly/pulls/996 Reviewed-by: AnErrupTion --- readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/readme.md b/readme.md index 5cbe81d..79c3a63 100644 --- a/readme.md +++ b/readme.md @@ -14,7 +14,7 @@ Join us on Matrix over at [#ly-dm:matrix.org](https://matrix.to/#/#ly-dm:matrix. ## Dependencies - Compile-time: - - zig 0.16.x + - zig 0.16.x (you must use a __release version__ of zig; check that `zig version` does not have a `-dev*` suffix) - libc From 0cee1c039a3a8ecaeb2868f44e91b9c3baedfd76 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Fri, 22 May 2026 17:15:17 +0200 Subject: [PATCH 128/176] ly-kmsconvt: Remove usage of gone --seats argument Signed-off-by: AnErrupTion --- res/ly-kmsconvt@.service | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/res/ly-kmsconvt@.service b/res/ly-kmsconvt@.service index 80eb1da..6e50b8a 100644 --- a/res/ly-kmsconvt@.service +++ b/res/ly-kmsconvt@.service @@ -5,7 +5,7 @@ After=kmsconvt@%i.service Conflicts=kmsconvt@%i.service [Service] -ExecStart=$PREFIX_DIRECTORY/bin/kmscon --font-engine unifont --vt=%I --seats=seat0 --login -- $PREFIX_DIRECTORY/bin/$EXECUTABLE_NAME --use-kmscon-vt +ExecStart=$PREFIX_DIRECTORY/bin/kmscon --font-engine unifont --vt=%I --login -- $PREFIX_DIRECTORY/bin/$EXECUTABLE_NAME --use-kmscon-vt StandardInput=tty UtmpIdentifier=%I TTYPath=/dev/%I From 9d8ccf6709f61266ad8d181c13d922770b773f57 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Sat, 23 May 2026 12:31:05 +0200 Subject: [PATCH 129/176] Fix termbox already being initialised when reclaiming Signed-off-by: AnErrupTion --- ly-ui/src/TerminalBuffer.zig | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ly-ui/src/TerminalBuffer.zig b/ly-ui/src/TerminalBuffer.zig index d0b3e3d..7d115eb 100644 --- a/ly-ui/src/TerminalBuffer.zig +++ b/ly-ui/src/TerminalBuffer.zig @@ -411,7 +411,8 @@ pub fn setCellBoundsChecked(self: *TerminalBuffer, x: isize, y: isize, cell: Cel pub fn reclaim(self: TerminalBuffer) !void { if (self.termios) |termios| { // Take back control of the TTY - if (termbox.tb_init() != 0) return error.TermboxReinitFailed; + const err = termbox.tb_init(); + if (err != 0 and err != termbox.TB_ERR_INIT_ALREADY) return error.TermboxReinitFailed; if (self.full_color and termbox.tb_set_output_mode(termbox.TB_OUTPUT_TRUECOLOR) != 0) { return error.TermboxSetOutputModeFailed; From 0ac11065f4df9f6b9aedf112376be7e880b1a1d5 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Sat, 30 May 2026 12:17:06 +0200 Subject: [PATCH 130/176] ly-kmsconvt: Set TERM=linux Signed-off-by: AnErrupTion --- res/ly-kmsconvt@.service | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/res/ly-kmsconvt@.service b/res/ly-kmsconvt@.service index 6e50b8a..85b0f05 100644 --- a/res/ly-kmsconvt@.service +++ b/res/ly-kmsconvt@.service @@ -5,7 +5,7 @@ After=kmsconvt@%i.service Conflicts=kmsconvt@%i.service [Service] -ExecStart=$PREFIX_DIRECTORY/bin/kmscon --font-engine unifont --vt=%I --login -- $PREFIX_DIRECTORY/bin/$EXECUTABLE_NAME --use-kmscon-vt +ExecStart=$PREFIX_DIRECTORY/bin/kmscon --term=linux --font-engine unifont --vt=%I --login -- $PREFIX_DIRECTORY/bin/$EXECUTABLE_NAME --use-kmscon-vt StandardInput=tty UtmpIdentifier=%I TTYPath=/dev/%I From 35be66e66fa333598ea6373293c737df238f2d55 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Sat, 30 May 2026 12:17:25 +0200 Subject: [PATCH 131/176] termbox2: Log init errors Signed-off-by: AnErrupTion --- ly-ui/src/TerminalBuffer.zig | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/ly-ui/src/TerminalBuffer.zig b/ly-ui/src/TerminalBuffer.zig index 7d115eb..9dfa1f3 100644 --- a/ly-ui/src/TerminalBuffer.zig +++ b/ly-ui/src/TerminalBuffer.zig @@ -103,10 +103,26 @@ pub fn init( random: Random, ) !TerminalBuffer { // Initialize termbox - if (termbox.tb_init() != 0) return error.TermboxInitFailed; + var err = termbox.tb_init(); + if (err != 0) { + try log_file.err( + io, + "tui", + "failed to initialise termbox2: {s}, term: {s}", + .{ termbox.tb_strerror(err), std.c.getenv("TERM").? }, + ); + return error.TermboxInitFailed; + } if (options.full_color) { - if (termbox.tb_set_output_mode(termbox.TB_OUTPUT_TRUECOLOR) != 0) { + err = termbox.tb_set_output_mode(termbox.TB_OUTPUT_TRUECOLOR); + if (err != 0) { + try log_file.err( + io, + "tui", + "failed to set termbox2 output mode to 24-bit color: {s}", + .{termbox.tb_strerror(err)}, + ); return error.TermboxSetOutputModeFailed; } try log_file.info( From ace79f76b5a832dfeffa294d2016ddac8fbb27b2 Mon Sep 17 00:00:00 2001 From: Wicin-134 Date: Sat, 6 Jun 2026 11:48:31 +0200 Subject: [PATCH 132/176] Overhaul language files (#997) - Added sr_Cyrl.ini as new Cyrillic variant for the Serbian Language - Added zh_TW.ini (Traditional Chinese) - Completed ALL missing keys in EVERY translation Reviewed-on: https://codeberg.org/fairyglade/ly/pulls/997 Reviewed-by: AnErrupTion --- res/lang/ar.ini | 42 +++++++------- res/lang/cat.ini | 52 +++++++++--------- res/lang/cs.ini | 76 ++++++++++++------------- res/lang/de.ini | 44 +++++++-------- res/lang/eo.ini | 8 +-- res/lang/es.ini | 62 ++++++++++----------- res/lang/it.ini | 76 ++++++++++++------------- res/lang/ja_JP.ini | 44 +++++++-------- res/lang/ku.ini | 8 +-- res/lang/lv.ini | 24 ++++---- res/lang/pl.ini | 24 ++++---- res/lang/pt.ini | 76 ++++++++++++------------- res/lang/pt_BR.ini | 76 ++++++++++++------------- res/lang/ro.ini | 108 ++++++++++++++++++------------------ res/lang/ru.ini | 24 ++++---- res/lang/sr.ini | 128 +++++++++++++++++++++---------------------- res/lang/sr_Cyrl.ini | 82 +++++++++++++++++++++++++++ res/lang/sv.ini | 10 ++-- res/lang/tr.ini | 66 +++++++++++----------- res/lang/uk.ini | 76 ++++++++++++------------- res/lang/zh_CN.ini | 78 +++++++++++++------------- res/lang/zh_TW.ini | 82 +++++++++++++++++++++++++++ 22 files changed, 715 insertions(+), 551 deletions(-) create mode 100644 res/lang/sr_Cyrl.ini create mode 100644 res/lang/zh_TW.ini diff --git a/res/lang/ar.ini b/res/lang/ar.ini index d8cbecf..fd51b51 100644 --- a/res/lang/ar.ini +++ b/res/lang/ar.ini @@ -2,29 +2,29 @@ authenticating = جاري المصادقة... brightness_down = خفض السطوع brightness_up = رفع السطوع capslock = capslock - - - - +custom = مخصص +custom_info_err_output_long = الإخراج طويل جداً +custom_info_err_no_output = لا يوجد إخراج +custom_info_err_no_output_error = ، خطأ محتمل err_alloc = فشل في تخصيص الذاكرة - - +err_args = تعذر تحليل وسيطات سطر الأوامر +err_autologin_session = لم يتم العثور على جلسة تسجيل الدخول التلقائي err_bounds = out-of-bounds index err_brightness_change = فشل في تغيير سطوع الشاشة err_chdir = فشل في فتح مجلد المنزل - +err_clock_too_long = نص الساعة طويل جداً err_config = فشل في تفسير ملف الإعدادات - +err_crawl = فشل الزحف في أدلة الجلسة err_dgn_oob = رسالة سجل (Log) err_domain = اسم نطاق غير صالح err_empty_password = لا يُسمح بكلمة مرور فارغة err_envlist = فشل في جلب قائمة المتغيرات البيئية - - +err_get_active_tty = فشل الحصول على tty النشط +err_hibernate = فشل تنفيذ أمر الإسبات err_hostname = فشل في جلب اسم المضيف (Hostname) - - - +err_inactivity = فشل تنفيذ أمر عدم النشاط +err_lock_state = فشل الحصول على حالة القفل +err_log = فشل فتح ملف السجل err_mlock = فشل في تأمين ذاكرة كلمة المرور (mlock) err_null = مؤشر فارغ (Null pointer) err_numlock = فشل في ضبط Num Lock @@ -50,12 +50,12 @@ err_perm_group = فشل في تخفيض صلاحيات المجموعة (Group p err_perm_user = فشل في تخفيض صلاحيات المستخدم (User permissions) err_pwnam = فشل في جلب معلومات المستخدم err_sleep = فشل في تنفيذ أمر sleep - - - +err_start = فشل تنفيذ أمر البدء +err_battery = فشل تحميل حالة البطارية +err_switch_tty = فشل تبديل tty err_tty_ctrl = فشل في نقل تحكم الطرفية (TTY) - - +err_no_users = لم يتم العثور على مستخدمين +err_uid_range = فشل الحصول الديناميكي على نطاق uid err_user_gid = فشل في تعيين معرّف المجموعة (GID) للمستخدم err_user_init = فشل في تهيئة بيانات المستخدم err_user_uid = فشل في تعيين معرّف المستخدم (UID) @@ -63,11 +63,11 @@ err_xauth = فشل في تنفيذ أمر xauth err_xcb_conn = فشل في الاتصال بمكتبة XCB err_xsessions_dir = فشل في العثور على مجلد Xsessions err_xsessions_open = فشل في فتح مجلد Xsessions - +hibernate = إسبات insert = ادخال login = تسجيل الدخول logout = تم تسجيل خروجك -no_x11_support = تم تعطيل دعم x11 اثناء وقت الـ compile +no_x11_support = دعم x11 معطّل في وقت الترجمة normal = عادي numlock = numlock other = اخر @@ -76,7 +76,7 @@ restart = اعادة التشغيل shell = shell shutdown = ايقاف التشغيل sleep = وضع السكون - +toggle_password = إظهار/إخفاء كلمة المرور wayland = wayland x11 = x11 xinitrc = xinitrc diff --git a/res/lang/cat.ini b/res/lang/cat.ini index 152cb96..3bf271b 100644 --- a/res/lang/cat.ini +++ b/res/lang/cat.ini @@ -2,29 +2,29 @@ authenticating = autenticant... brightness_down = abaixar brillantor brightness_up = apujar brillantor capslock = Bloq Majús - - - - +custom = personalitzat +custom_info_err_output_long = sortida massa llarga +custom_info_err_no_output = sense sortida +custom_info_err_no_output_error = , possible error err_alloc = assignació de memòria fallida - - +err_args = no s'han pogut analitzar els arguments de la línia d'ordres +err_autologin_session = no s'ha trobat la sessió d'inici de sessió automàtic err_bounds = índex fora de límits err_brightness_change = error en canviar la brillantor err_chdir = error en obrir la carpeta home - - - +err_clock_too_long = la cadena del rellotge és massa llarga +err_config = no s'ha pogut analitzar el fitxer de configuració +err_crawl = no s'han pogut explorar els directoris de sessió err_dgn_oob = missatge de registre err_domain = domini invàlid - +err_empty_password = no es permet la contrasenya buida err_envlist = error en obtenir l'envlist - - +err_get_active_tty = no s'ha pogut obtenir el tty actiu +err_hibernate = no s'ha pogut executar l'ordre d'hibernació err_hostname = error en obtenir el nom de l'amfitrió - - - +err_inactivity = no s'ha pogut executar l'ordre d'inactivitat +err_lock_state = no s'ha pogut obtenir l'estat de bloqueig +err_log = no s'ha pogut obrir el fitxer de registre err_mlock = error en bloquejar la memòria de clau err_null = punter nul err_numlock = error en establir el Bloq num @@ -49,13 +49,13 @@ err_perm_dir = error en canviar el directori actual err_perm_group = error en degradar els permisos de grup err_perm_user = error en degradar els permisos de l'usuari err_pwnam = error en obtenir la informació de l'usuari - - - - - - - +err_sleep = no s'ha pogut executar l'ordre de suspensió +err_start = no s'ha pogut executar l'ordre d'inici +err_battery = no s'ha pogut carregar l'estat de la bateria +err_switch_tty = no s'ha pogut canviar de tty +err_tty_ctrl = ha fallat la transferència del control tty +err_no_users = no s'han trobat usuaris +err_uid_range = no s'ha pogut obtenir dinàmicament el rang d'uid err_user_gid = error en establir el GID de l'usuari err_user_init = error en inicialitzar usuari err_user_uid = error en establir l'UID de l'usuari @@ -63,20 +63,20 @@ err_xauth = error en la comanda xauth err_xcb_conn = error en la connexió xcb err_xsessions_dir = error en trobar la carpeta de sessions err_xsessions_open = error en obrir la carpeta de sessions - +hibernate = hibernar insert = inserir login = iniciar sessió logout = sessió tancada -no_x11_support = el suport per x11 ha estat desactivat en la compilació +no_x11_support = suport x11 desactivat en temps de compilació normal = normal numlock = Bloq Num - +other = altres password = Clau restart = reiniciar shell = shell shutdown = aturar sleep = suspendre - +toggle_password = mostrar/amagar contrasenya wayland = wayland x11 = x11 xinitrc = xinitrc diff --git a/res/lang/cs.ini b/res/lang/cs.ini index da0bce2..4f9456c 100644 --- a/res/lang/cs.ini +++ b/res/lang/cs.ini @@ -1,33 +1,33 @@ - - - +authenticating = ověřování... +brightness_down = snížit jas +brightness_up = zvýšit jas capslock = capslock - - - - +custom = vlastní +custom_info_err_output_long = výstup je příliš dlouhý +custom_info_err_no_output = žádný výstup +custom_info_err_no_output_error = , možná chyba err_alloc = alokace paměti selhala - - +err_args = nelze analyzovat argumenty příkazového řádku +err_autologin_session = relace automatického přihlášení nebyla nalezena err_bounds = index je mimo hranice pole - +err_brightness_change = nepodařilo se změnit jas err_chdir = nelze otevřít domovský adresář - - - +err_clock_too_long = řetězec hodin je příliš dlouhý +err_config = nelze analyzovat konfigurační soubor +err_crawl = nepodařilo se prohledat adresáře relací err_dgn_oob = zpráva protokolu err_domain = neplatná doména - - - - +err_empty_password = prázdné heslo není povoleno +err_envlist = nepodařilo se získat seznam proměnných prostředí +err_get_active_tty = nepodařilo se získat aktivní tty +err_hibernate = nepodařilo se spustit příkaz hibernace err_hostname = nelze získat název hostitele - - - +err_inactivity = nepodařilo se spustit příkaz nečinnosti +err_lock_state = nepodařilo se získat stav zámku +err_log = nepodařilo se otevřít soubor protokolu err_mlock = uzamčení paměti hesel selhalo err_null = nulový ukazatel - +err_numlock = nepodařilo se nastavit numlock err_pam = pam transakce selhala err_pam_abort = pam transakce přerušena err_pam_acct_expired = platnost účtu vypršela @@ -49,34 +49,34 @@ err_perm_dir = nepodařilo se změnit adresář err_perm_group = nepodařilo se snížit skupinová oprávnění err_perm_user = nepodařilo se snížit uživatelská oprávnění err_pwnam = nelze získat informace o uživateli - - - - - - - +err_sleep = nepodařilo se spustit příkaz spánku +err_start = nepodařilo se spustit příkaz spuštění +err_battery = nepodařilo se načíst stav baterie +err_switch_tty = nepodařilo se přepnout tty +err_tty_ctrl = přenos řízení tty selhal +err_no_users = nebyli nalezeni žádní uživatelé +err_uid_range = nepodařilo se dynamicky získat rozsah uid err_user_gid = nastavení GID uživatele selhalo err_user_init = inicializace uživatele selhala err_user_uid = nastavení UID uživateli selhalo - - +err_xauth = příkaz xauth selhal +err_xcb_conn = připojení xcb selhalo err_xsessions_dir = nepodařilo se najít složku relací err_xsessions_open = nepodařilo se otevřít složku relací - - +hibernate = hibernace +insert = vložit login = uživatel logout = odhlášen - - +no_x11_support = podpora x11 zakázána při kompilaci +normal = normální numlock = numlock - +other = jiné password = heslo restart = restartovat shell = příkazový řádek shutdown = vypnout - - +sleep = uspat +toggle_password = zobrazit/skrýt heslo wayland = wayland - +x11 = x11 xinitrc = xinitrc diff --git a/res/lang/de.ini b/res/lang/de.ini index f869dcb..ca3a0ef 100644 --- a/res/lang/de.ini +++ b/res/lang/de.ini @@ -2,29 +2,29 @@ authenticating = authentifizieren... brightness_down = Helligkeit- brightness_up = Helligkeit+ capslock = Feststelltaste - - - - +custom = benutzerdefiniert +custom_info_err_output_long = Ausgabe zu lang +custom_info_err_no_output = keine Ausgabe +custom_info_err_no_output_error = , möglicher Fehler err_alloc = Speicherzuweisung fehlgeschlagen - - +err_args = Kommandozeilenargumente konnten nicht verarbeitet werden +err_autologin_session = Autologin-Sitzung nicht gefunden err_bounds = Index ausserhalb des Bereichs err_brightness_change = Helligkeitsänderung fehlgeschlagen err_chdir = Fehler beim Oeffnen des Home-Ordners - +err_clock_too_long = Uhrzeitzeichenkette zu lang err_config = Fehler beim Verarbeiten der Konfigurationsdatei - +err_crawl = Sitzungsverzeichnisse konnten nicht durchsucht werden err_dgn_oob = Diagnose-Nachricht err_domain = Ungueltige Domain err_empty_password = Leeres Passwort nicht zugelassen err_envlist = Fehler beim Abrufen der Umgebungs-Variablen - - +err_get_active_tty = Aktives tty konnte nicht ermittelt werden +err_hibernate = Ruhezustand-Befehl konnte nicht ausgeführt werden err_hostname = Abrufen des Hostnames fehlgeschlagen - - - +err_inactivity = Inaktivitätsbefehl konnte nicht ausgeführt werden +err_lock_state = Sperrstatus konnte nicht ermittelt werden +err_log = Protokolldatei konnte nicht geöffnet werden err_mlock = Sperren des Passwortspeichers fehlgeschlagen err_null = Null Pointer err_numlock = Numlock konnte nicht aktiviert werden @@ -50,12 +50,12 @@ err_perm_group = Fehler beim Heruntersetzen der Gruppenberechtigungen err_perm_user = Fehler beim Heruntersetzen der Nutzerberechtigungen err_pwnam = Abrufen der Benutzerinformationen fehlgeschlagen err_sleep = Sleep-Befehl fehlgeschlagen - - - +err_start = Startbefehl konnte nicht ausgeführt werden +err_battery = Akkustand konnte nicht geladen werden +err_switch_tty = tty konnte nicht gewechselt werden err_tty_ctrl = Fehler bei der TTY-Uebergabe - - +err_no_users = Keine Benutzer gefunden +err_uid_range = uid-Bereich konnte nicht dynamisch ermittelt werden err_user_gid = Fehler beim Setzen der Gruppen-ID err_user_init = Nutzer-Initialisierung fehlgeschlagen err_user_uid = Setzen der Benutzer-ID fehlgeschlagen @@ -63,11 +63,11 @@ err_xauth = Xauth-Befehl fehlgeschlagen err_xcb_conn = xcb-Verbindung fehlgeschlagen err_xsessions_dir = Fehler beim Finden des Sitzungsordners err_xsessions_open = Fehler beim Oeffnen des Sitzungsordners - +hibernate = Ruhezustand insert = Einfügen login = Nutzer logout = Abmelden -no_x11_support = X11-Support bei Kompilierung deaktiviert +no_x11_support = x11-Unterstützung zur Kompilierzeit deaktiviert normal = Normal numlock = Numlock other = Andere @@ -76,7 +76,7 @@ restart = Neustarten shell = Shell shutdown = Herunterfahren sleep = Sleep - +toggle_password = Passwort anzeigen/verbergen wayland = wayland -x11 = X11 +x11 = x11 xinitrc = xinitrc diff --git a/res/lang/eo.ini b/res/lang/eo.ini index 8463432..c8cbb96 100644 --- a/res/lang/eo.ini +++ b/res/lang/eo.ini @@ -3,9 +3,9 @@ brightness_down = malpliigi helecon brightness_up = pliigi helecon capslock = majuskla baskulo custom = propra - - - +custom_info_err_output_long = eligo tro longa +custom_info_err_no_output = neniu eligo +custom_info_err_no_output_error = , ebla eraro err_alloc = malsukcesis memorasignon err_args = ne povas analizi argumentojn de komanda linio err_autologin_session = aŭtomatan ensalutan seancon ne trovis @@ -76,7 +76,7 @@ restart = restartigi shell = ŝelo shutdown = malŝalti sleep = memordormi - +toggle_password = montri/kaŝi pasvorton wayland = wayland x11 = x11 xinitrc = xinitrc diff --git a/res/lang/es.ini b/res/lang/es.ini index 38c2b9d..f587fe7 100644 --- a/res/lang/es.ini +++ b/res/lang/es.ini @@ -2,32 +2,32 @@ authenticating = autenticando... brightness_down = bajar brillo brightness_up = subir brillo capslock = Bloq Mayús - - - - +custom = personalizado +custom_info_err_output_long = salida demasiado larga +custom_info_err_no_output = sin salida +custom_info_err_no_output_error = , posible error err_alloc = asignación de memoria fallida - - +err_args = no se pudieron analizar los argumentos de la línea de comandos +err_autologin_session = no se encontró la sesión de inicio de sesión automático err_bounds = índice fuera de límites - +err_brightness_change = no se pudo cambiar el brillo err_chdir = error al abrir la carpeta home - - - +err_clock_too_long = la cadena del reloj es demasiado larga +err_config = no se pudo analizar el archivo de configuración +err_crawl = no se pudieron explorar los directorios de sesión err_dgn_oob = mensaje de registro err_domain = dominio inválido - - - - +err_empty_password = no se permite contraseña vacía +err_envlist = no se pudo obtener la lista de variables de entorno +err_get_active_tty = no se pudo obtener el tty activo +err_hibernate = no se pudo ejecutar el comando de hibernación err_hostname = error al obtener el nombre de host - - - +err_inactivity = no se pudo ejecutar el comando de inactividad +err_lock_state = no se pudo obtener el estado de bloqueo +err_log = no se pudo abrir el archivo de registro err_mlock = error al bloquear la contraseña de memoria err_null = puntero nulo - +err_numlock = no se pudo configurar numlock err_pam = error en la transacción pam err_pam_abort = transacción pam abortada err_pam_acct_expired = cuenta expirada @@ -49,25 +49,25 @@ err_perm_dir = error al cambiar el directorio actual err_perm_group = error al degradar los permisos del grupo err_perm_user = error al degradar los permisos del usuario err_pwnam = error al obtener la información del usuario - - - - - - - +err_sleep = no se pudo ejecutar el comando de suspensión +err_start = no se pudo ejecutar el comando de inicio +err_battery = no se pudo cargar el estado de la batería +err_switch_tty = no se pudo cambiar de tty +err_tty_ctrl = falló la transferencia de control tty +err_no_users = no se encontraron usuarios +err_uid_range = no se pudo obtener dinámicamente el rango de uid err_user_gid = error al establecer el GID del usuario err_user_init = error al inicializar usuario err_user_uid = error al establecer el UID del usuario - - +err_xauth = falló el comando xauth +err_xcb_conn = falló la conexión xcb err_xsessions_dir = error al buscar la carpeta de sesiones err_xsessions_open = error al abrir la carpeta de sesiones - +hibernate = hibernar insert = insertar login = usuario logout = cerrar sesión -no_x11_support = soporte para x11 deshabilitado en tiempo de compilación +no_x11_support = soporte x11 desactivado en tiempo de compilación normal = normal numlock = Bloq Num other = otro @@ -76,7 +76,7 @@ restart = reiniciar shell = shell shutdown = apagar sleep = suspender - +toggle_password = mostrar/ocultar contraseña wayland = wayland - +x11 = x11 xinitrc = xinitrc diff --git a/res/lang/it.ini b/res/lang/it.ini index e8af2a6..9c9679d 100644 --- a/res/lang/it.ini +++ b/res/lang/it.ini @@ -1,33 +1,33 @@ - - - +authenticating = autenticazione in corso... +brightness_down = diminuisci luminosità +brightness_up = aumenta luminosità capslock = capslock - - - - +custom = personalizzato +custom_info_err_output_long = output troppo lungo +custom_info_err_no_output = nessun output +custom_info_err_no_output_error = , possibile errore err_alloc = impossibile allocare memoria - - +err_args = impossibile analizzare gli argomenti della riga di comando +err_autologin_session = sessione di accesso automatico non trovata err_bounds = indice fuori limite - +err_brightness_change = impossibile modificare la luminosità err_chdir = impossibile aprire home directory - - - +err_clock_too_long = stringa dell'orologio troppo lunga +err_config = impossibile analizzare il file di configurazione +err_crawl = impossibile esplorare le directory delle sessioni err_dgn_oob = messaggio log err_domain = dominio non valido - - - - +err_empty_password = password vuota non consentita +err_envlist = impossibile ottenere la lista delle variabili d'ambiente +err_get_active_tty = impossibile ottenere il tty attivo +err_hibernate = impossibile eseguire il comando di ibernazione err_hostname = impossibile ottenere hostname - - - +err_inactivity = impossibile eseguire il comando di inattività +err_lock_state = impossibile ottenere lo stato di blocco +err_log = impossibile aprire il file di log err_mlock = impossibile ottenere lock per la password in memoria err_null = puntatore nullo - +err_numlock = impossibile impostare il numlock err_pam = transazione PAM fallita err_pam_abort = transazione PAM interrotta err_pam_acct_expired = account scaduto @@ -49,34 +49,34 @@ err_perm_dir = impossibile cambiare directory corrente err_perm_group = impossibile ridurre permessi gruppo err_perm_user = impossibile ridurre permessi utente err_pwnam = impossibile ottenere dati utente - - - - - - - +err_sleep = impossibile eseguire il comando di sospensione +err_start = impossibile eseguire il comando di avvio +err_battery = impossibile caricare lo stato della batteria +err_switch_tty = impossibile cambiare tty +err_tty_ctrl = trasferimento del controllo tty fallito +err_no_users = nessun utente trovato +err_uid_range = impossibile ottenere dinamicamente l'intervallo uid err_user_gid = impossibile impostare GID utente err_user_init = impossibile inizializzare utente err_user_uid = impossible impostare UID utente - - +err_xauth = comando xauth fallito +err_xcb_conn = connessione xcb fallita err_xsessions_dir = impossibile localizzare cartella sessioni err_xsessions_open = impossibile aprire cartella sessioni - - +hibernate = ibernazione +insert = inserisci login = username logout = scollegato - - +no_x11_support = supporto x11 disabilitato in fase di compilazione +normal = normale numlock = numlock - +other = altro password = password restart = riavvio shell = shell shutdown = arresto - - +sleep = sospendi +toggle_password = mostra/nascondi password wayland = wayland - +x11 = x11 xinitrc = xinitrc diff --git a/res/lang/ja_JP.ini b/res/lang/ja_JP.ini index 309ff39..a14feab 100644 --- a/res/lang/ja_JP.ini +++ b/res/lang/ja_JP.ini @@ -2,29 +2,29 @@ authenticating = 認証中... brightness_down = 明るさを下げる brightness_up = 明るさを上げる capslock = CapsLock - - - - +custom = カスタム +custom_info_err_output_long = 出力が長すぎます +custom_info_err_no_output = 出力なし +custom_info_err_no_output_error = 、エラーの可能性あり err_alloc = メモリ割り当て失敗 - - +err_args = コマンドライン引数を解析できません +err_autologin_session = 自動ログインセッションが見つかりません err_bounds = 境界外インデックス err_brightness_change = 明るさの変更に失敗しました err_chdir = ホームフォルダを開けませんでした - +err_clock_too_long = 時計の文字列が長すぎます err_config = 設定ファイルを解析できません - +err_crawl = セッションディレクトリのクロールに失敗しました err_dgn_oob = ログメッセージ err_domain = 無効なドメイン err_empty_password = 空のパスワードは許可されていません err_envlist = 環境変数リストの取得に失敗しました - - +err_get_active_tty = アクティブなttyの取得に失敗しました +err_hibernate = 休止状態コマンドの実行に失敗しました err_hostname = ホスト名の取得に失敗しました - - - +err_inactivity = 無操作コマンドの実行に失敗しました +err_lock_state = ロック状態の取得に失敗しました +err_log = ログファイルを開けませんでした err_mlock = パスワードメモリのロックに失敗しました err_null = ヌルポインタ err_numlock = NumLockの設定に失敗しました @@ -50,12 +50,12 @@ err_perm_group = グループ権限のダウングレードに失敗しました err_perm_user = ユーザー権限のダウングレードに失敗しました err_pwnam = ユーザー情報の取得に失敗しました err_sleep = スリープコマンドの実行に失敗しました - - - +err_start = 起動コマンドの実行に失敗しました +err_battery = バッテリー状態の読み込みに失敗しました +err_switch_tty = ttyの切り替えに失敗しました err_tty_ctrl = TTY制御の転送に失敗しました - - +err_no_users = ユーザーが見つかりません +err_uid_range = uidの範囲を動的に取得できませんでした err_user_gid = ユーザーGIDの設定に失敗しました err_user_init = ユーザーの初期化に失敗しました err_user_uid = ユーザーUIDの設定に失敗しました @@ -63,11 +63,11 @@ err_xauth = xauthコマンドの実行に失敗しました err_xcb_conn = XCB接続に失敗しました err_xsessions_dir = セッションフォルダが見つかりませんでした err_xsessions_open = セッションフォルダを開けませんでした - +hibernate = 休止状態 insert = 挿入 login = ログイン logout = ログアウト済み -no_x11_support = X11サポートはコンパイル時に無効化されています +no_x11_support = x11サポートはコンパイル時に無効化されています normal = 通常 numlock = NumLock other = その他 @@ -76,7 +76,7 @@ restart = 再起動 shell = シェル shutdown = シャットダウン sleep = スリープ - +toggle_password = パスワードの表示/非表示 wayland = Wayland -x11 = X11 +x11 = x11 xinitrc = xinitrc diff --git a/res/lang/ku.ini b/res/lang/ku.ini index 5775274..0c39030 100644 --- a/res/lang/ku.ini +++ b/res/lang/ku.ini @@ -3,9 +3,9 @@ brightness_down = ronahiyê kêm bike brightness_up = ronahiyê bilind bike capslock = tîpên girdek (capslock) custom = kesane - - - +custom_info_err_output_long = encam pir dirêj e +custom_info_err_no_output = encam tune +custom_info_err_no_output_error = , xeletiya mimkun err_alloc = veqetandina bîrê têk çû err_args = argumanên rêzika fermanê nehatin analîzkirin err_autologin_session = danişîna têketina xweber nehate dîtin @@ -76,7 +76,7 @@ restart = ji nû ve bide destpêkirin shell = shell shutdown = vemirîne sleep = têxîne xewê - +toggle_password = şîfre nîşan bide/veşêre wayland = wayland x11 = x11 xinitrc = xinitrc diff --git a/res/lang/lv.ini b/res/lang/lv.ini index a1a3fa9..8f54d89 100644 --- a/res/lang/lv.ini +++ b/res/lang/lv.ini @@ -3,26 +3,26 @@ brightness_down = samazināt spilgtumu brightness_up = palielināt spilgtumu capslock = caps lock custom = pielāgots - - - +custom_info_err_output_long = izvade pārāk gara +custom_info_err_no_output = nav izvades +custom_info_err_no_output_error = , iespējama kļūda err_alloc = neizdevās atmiņas piešķiršana - - +err_args = nevar parsēt komandrindas argumentus +err_autologin_session = automātiskās pieteikšanās sesija nav atrasta err_bounds = indekss ārpus robežām err_brightness_change = neizdevās mainīt spilgtumu err_chdir = neizdevās atvērt mājas mapi err_clock_too_long = pulksteņa virkne pārāk gara err_config = neizdevās parsēt konfigurācijas failu - +err_crawl = neizdevās pārlūkot sesiju direktorijus err_dgn_oob = žurnāla ziņojums err_domain = nederīgs domēns err_empty_password = tukša parole nav atļauta err_envlist = neizdevās iegūt vides mainīgo sarakstu err_get_active_tty = neizdevās iegūt aktīvo tty - +err_hibernate = neizdevās izpildīt hibernācijas komandu err_hostname = neizdevās iegūt hostname - +err_inactivity = neizdevās izpildīt neaktivitātes komandu err_lock_state = neizdevās iegūt bloķēšanas stāvokli err_log = neizdevās atvērt žurnāla failu err_mlock = neizdevās bloķēt paroles atmiņu @@ -50,12 +50,12 @@ err_perm_group = neizdevās pazemināt grupas atļaujas err_perm_user = neizdevās pazemināt lietotāja atļaujas err_pwnam = neizdevās iegūt lietotāja informāciju err_sleep = neizdevās izpildīt miega komandu - +err_start = neizdevās izpildīt startēšanas komandu err_battery = neizdevās ielādēt akumulatora stāvokli err_switch_tty = neizdevās pārslēgt tty err_tty_ctrl = tty vadības nodošana neizdevās err_no_users = lietotāji nav atrasti - +err_uid_range = neizdevās dinamiski iegūt uid diapazonu err_user_gid = neizdevās iestatīt lietotāja GID err_user_init = neizdevās inicializēt lietotāju err_user_uid = neizdevās iestatīt lietotāja UID @@ -63,7 +63,7 @@ err_xauth = xauth komanda neizdevās err_xcb_conn = xcb savienojums neizdevās err_xsessions_dir = neizdevās atrast sesiju mapi err_xsessions_open = neizdevās atvērt sesiju mapi - +hibernate = hibernācija insert = ievietot login = lietotājs logout = iziet @@ -76,7 +76,7 @@ restart = restartēt shell = terminālis shutdown = izslēgt sleep = snauda - +toggle_password = rādīt/slēpt paroli wayland = wayland x11 = x11 xinitrc = xinitrc diff --git a/res/lang/pl.ini b/res/lang/pl.ini index 4c521e4..536680d 100644 --- a/res/lang/pl.ini +++ b/res/lang/pl.ini @@ -3,26 +3,26 @@ brightness_down = zmniejsz jasność brightness_up = zwiększ jasność capslock = capslock custom = własny - - - +custom_info_err_output_long = wyjście zbyt długie +custom_info_err_no_output = brak wyjścia +custom_info_err_no_output_error = , możliwy błąd err_alloc = nieudana alokacja pamięci - +err_args = nie można przetworzyć argumentów wiersza poleceń err_autologin_session = nie znaleziono sesji autologowania err_bounds = indeks poza zakresem err_brightness_change = nie udało się zmienić jasności err_chdir = nie udało się otworzyć folderu domowego err_clock_too_long = ciąg znaków zegara jest za długi err_config = nie można przetworzyć pliku konfiguracyjnego - +err_crawl = nie udało się przeszukać katalogów sesji err_dgn_oob = wiadomość loga err_domain = niepoprawna domena err_empty_password = puste hasło jest niedozwolone err_envlist = nie udało się pobrać listy zmiennych środowiskowych err_get_active_tty = nie udało się uzyskać aktywnego tty - +err_hibernate = nie udało się wykonać polecenia hibernacji err_hostname = nie udało się uzyskać nazwy hosta - +err_inactivity = nie udało się wykonać polecenia nieaktywności err_lock_state = nie udało się uzyskać stanu blokady err_log = nie udało się otworzyć pliku logu err_mlock = nie udało się zablokować pamięci haseł @@ -50,12 +50,12 @@ err_perm_group = nie udało się obniżyć uprawnień grupy err_perm_user = nie udało się obniżyć uprawnień użytkownika err_pwnam = nie udało się uzyskać informacji o użytkowniku err_sleep = nie udało się wykonać polecenia sleep - +err_start = nie udało się wykonać polecenia startowego err_battery = nie udało się sprawdzić statusu baterii err_switch_tty = nie można przełączyć tty err_tty_ctrl = nie udało się przekazać kontroli tty err_no_users = nie znaleziono żadnego użytkownika - +err_uid_range = nie udało się dynamicznie pobrać zakresu uid err_user_gid = nie udało się ustawić GID użytkownika err_user_init = nie udało się zainicjalizować użytkownika err_user_uid = nie udało się ustawić UID użytkownika @@ -63,11 +63,11 @@ err_xauth = polecenie xauth nie powiodło się err_xcb_conn = połączenie xcb nie powiodło się err_xsessions_dir = nie udało się znaleźć folderu sesji err_xsessions_open = nie udało się otworzyć folderu sesji - +hibernate = hibernuj insert = wstaw login = login logout = wylogowano -no_x11_support = wsparcie X11 wyłączone podczas kompilacji +no_x11_support = obsługa x11 wyłączona podczas kompilacji normal = normalny numlock = numlock other = inny @@ -76,7 +76,7 @@ restart = uruchom ponownie shell = powłoka shutdown = wyłącz sleep = uśpij - +toggle_password = Pokaż/ukryj hasło wayland = wayland x11 = x11 xinitrc = xinitrc diff --git a/res/lang/pt.ini b/res/lang/pt.ini index 0b13276..204a9f1 100644 --- a/res/lang/pt.ini +++ b/res/lang/pt.ini @@ -1,33 +1,33 @@ - - - +authenticating = a autenticar... +brightness_down = diminuir brilho +brightness_up = aumentar brilho capslock = capslock - - - - +custom = personalizado +custom_info_err_output_long = saída demasiado longa +custom_info_err_no_output = sem saída +custom_info_err_no_output_error = , possível erro err_alloc = erro na atribuição de memória - - +err_args = não foi possível analisar os argumentos da linha de comandos +err_autologin_session = sessão de início de sessão automático não encontrada err_bounds = índice fora de limites - +err_brightness_change = não foi possível alterar o brilho err_chdir = erro ao abrir a pasta home - - - +err_clock_too_long = a cadeia de caracteres do relógio é demasiado longa +err_config = não foi possível analisar o ficheiro de configuração +err_crawl = não foi possível explorar os diretórios de sessão err_dgn_oob = mensagem de registo err_domain = domínio inválido - - - - +err_empty_password = palavra-passe vazia não é permitida +err_envlist = não foi possível obter a lista de variáveis de ambiente +err_get_active_tty = não foi possível obter o tty ativo +err_hibernate = não foi possível executar o comando de hibernação err_hostname = erro ao obter o nome do host - - - +err_inactivity = não foi possível executar o comando de inatividade +err_lock_state = não foi possível obter o estado de bloqueio +err_log = não foi possível abrir o ficheiro de registo err_mlock = erro de bloqueio de memória err_null = ponteiro nulo - +err_numlock = não foi possível definir o numlock err_pam = erro na transação pam err_pam_abort = transação pam abortada err_pam_acct_expired = conta expirada @@ -49,34 +49,34 @@ err_perm_dir = erro ao alterar o diretório atual err_perm_group = erro ao reduzir as permissões do grupo err_perm_user = erro ao reduzir as permissões do utilizador err_pwnam = erro ao obter informação do utilizador - - - - - - - +err_sleep = não foi possível executar o comando de suspensão +err_start = não foi possível executar o comando de início +err_battery = não foi possível carregar o estado da bateria +err_switch_tty = não foi possível mudar de tty +err_tty_ctrl = falhou a transferência de controlo do tty +err_no_users = nenhum utilizador encontrado +err_uid_range = não foi possível obter dinamicamente o intervalo de uid err_user_gid = erro ao definir o GID do utilizador err_user_init = erro ao iniciar o utilizador err_user_uid = erro ao definir o UID do utilizador - - +err_xauth = o comando xauth falhou +err_xcb_conn = a ligação xcb falhou err_xsessions_dir = erro ao localizar a pasta das sessões err_xsessions_open = erro ao abrir a pasta das sessões - - +hibernate = hibernar +insert = inserir login = iniciar sessão logout = terminar sessão - - +no_x11_support = suporte a x11 desativado em tempo de compilação +normal = normal numlock = numlock - +other = outro password = palavra-passe restart = reiniciar shell = shell shutdown = encerrar - - +sleep = suspender +toggle_password = mostrar/ocultar palavra-passe wayland = wayland - +x11 = x11 xinitrc = xinitrc diff --git a/res/lang/pt_BR.ini b/res/lang/pt_BR.ini index ca96a3e..1ccc4c4 100644 --- a/res/lang/pt_BR.ini +++ b/res/lang/pt_BR.ini @@ -1,33 +1,33 @@ - - - +authenticating = autenticando... +brightness_down = diminuir brilho +brightness_up = aumentar brilho capslock = caixa alta - - - - +custom = personalizado +custom_info_err_output_long = saída muito longa +custom_info_err_no_output = sem saída +custom_info_err_no_output_error = , possível erro err_alloc = alocação de memória malsucedida - - +err_args = não foi possível analisar os argumentos da linha de comando +err_autologin_session = sessão de login automático não encontrada err_bounds = índice fora de limites - +err_brightness_change = não foi possível alterar o brilho err_chdir = não foi possível abrir o diretório home - - - +err_clock_too_long = a string do relógio é muito longa +err_config = não foi possível analisar o arquivo de configuração +err_crawl = não foi possível explorar os diretórios de sessão err_dgn_oob = mensagem de log err_domain = domínio inválido - - - - +err_empty_password = senha vazia não é permitida +err_envlist = não foi possível obter a lista de variáveis de ambiente +err_get_active_tty = não foi possível obter o tty ativo +err_hibernate = não foi possível executar o comando de hibernação err_hostname = não foi possível obter o nome do host - - - +err_inactivity = não foi possível executar o comando de inatividade +err_lock_state = não foi possível obter o estado de bloqueio +err_log = não foi possível abrir o arquivo de log err_mlock = bloqueio da memória de senha malsucedido err_null = ponteiro nulo - +err_numlock = não foi possível definir o numlock err_pam = transação pam malsucedida err_pam_abort = transação pam abortada err_pam_acct_expired = conta expirada @@ -49,34 +49,34 @@ err_perm_dir = não foi possível alterar o diretório atual err_perm_group = não foi possível reduzir as permissões de grupo err_perm_user = não foi possível reduzir as permissões de usuário err_pwnam = não foi possível obter informações do usuário - - - - - - - +err_sleep = não foi possível executar o comando de suspensão +err_start = não foi possível executar o comando de início +err_battery = não foi possível carregar o status da bateria +err_switch_tty = não foi possível mudar de tty +err_tty_ctrl = falhou a transferência de controle do tty +err_no_users = nenhum usuário encontrado +err_uid_range = não foi possível obter dinamicamente o intervalo de uid err_user_gid = não foi possível definir o GID do usuário err_user_init = não foi possível iniciar o usuário err_user_uid = não foi possível definir o UID do usuário - - +err_xauth = o comando xauth falhou +err_xcb_conn = a conexão xcb falhou err_xsessions_dir = não foi possível encontrar a pasta das sessões err_xsessions_open = não foi possível abrir a pasta das sessões - - +hibernate = hibernar +insert = inserir login = conectar logout = desconectado - - +no_x11_support = suporte a x11 desativado em tempo de compilação +normal = normal numlock = numlock - +other = outro password = senha restart = reiniciar shell = shell shutdown = desligar - - +sleep = suspender +toggle_password = mostrar/ocultar senha wayland = wayland - +x11 = x11 xinitrc = xinitrc diff --git a/res/lang/ro.ini b/res/lang/ro.ini index 0bf92f2..3b771a8 100644 --- a/res/lang/ro.ini +++ b/res/lang/ro.ini @@ -1,34 +1,34 @@ - - - +authenticating = autentificare... +brightness_down = scade luminozitatea +brightness_up = crește luminozitatea capslock = capslock - - - - - - - - - - - - - - - - - - - - - - - - - - - +custom = personalizat +custom_info_err_output_long = ieșire prea lungă +custom_info_err_no_output = fără ieșire +custom_info_err_no_output_error = , posibil eroare +err_alloc = alocare de memorie eșuată +err_args = imposibil de analizat argumentele liniei de comandă +err_autologin_session = sesiunea de autentificare automată nu a fost găsită +err_bounds = index în afara limitelor +err_brightness_change = imposibil de schimbat luminozitatea +err_chdir = imposibil de deschis folderul de acasă +err_clock_too_long = șirul de ceas este prea lung +err_config = imposibil de analizat fișierul de configurare +err_crawl = imposibil de explorat directoarele de sesiune +err_dgn_oob = mesaj jurnal +err_domain = domeniu invalid +err_empty_password = parola goală nu este permisă +err_envlist = imposibil de obținut lista de variabile de mediu +err_get_active_tty = imposibil de obținut tty-ul activ +err_hibernate = imposibil de executat comanda de hibernare +err_hostname = imposibil de obținut numele gazdei +err_inactivity = imposibil de executat comanda de inactivitate +err_lock_state = imposibil de obținut starea de blocare +err_log = imposibil de deschis fișierul jurnal +err_mlock = imposibil de blocat memoria parolei +err_null = pointer nul +err_numlock = imposibil de setat numlock +err_pam = tranzacție pam eșuată err_pam_abort = tranzacţie pam anulată err_pam_acct_expired = cont expirat err_pam_auth = eroare de autentificare @@ -44,39 +44,39 @@ err_pam_perm_denied = acces interzis err_pam_session = eroare de sesiune err_pam_sys = eroare de sistem err_pam_user_unknown = utilizator necunoscut - +err_path = imposibil de setat calea err_perm_dir = nu s-a putut schimba dosarul (folder-ul) curent err_perm_group = nu s-a putut face downgrade permisiunilor de grup err_perm_user = nu s-a putut face downgrade permisiunilor de utilizator - - - - - - - - - - - - - - - - - +err_pwnam = imposibil de obținut informații despre utilizator +err_sleep = imposibil de executat comanda de repaus +err_start = imposibil de executat comanda de pornire +err_battery = imposibil de încărcat starea bateriei +err_switch_tty = imposibil de comutat tty +err_tty_ctrl = transferul controlului tty a eșuat +err_no_users = niciun utilizator găsit +err_uid_range = imposibil de obținut dinamic intervalul uid +err_user_gid = imposibil de setat GID-ul utilizatorului +err_user_init = imposibil de inițializa utilizatorul +err_user_uid = imposibil de setat UID-ul utilizatorului +err_xauth = comanda xauth a eșuat +err_xcb_conn = conexiunea xcb a eșuat +err_xsessions_dir = imposibil de găsit folderul de sesiuni +err_xsessions_open = imposibil de deschis folderul de sesiuni +hibernate = hibernare +insert = inserare login = utilizator logout = opreşte sesiunea - - +no_x11_support = suportul x11 dezactivat la compilare +normal = normal numlock = numlock - +other = altul password = parolă restart = resetează shell = shell shutdown = opreşte sistemul - - +sleep = repaus +toggle_password = afișare/ascundere parolă wayland = wayland - +x11 = x11 xinitrc = xinitrc diff --git a/res/lang/ru.ini b/res/lang/ru.ini index 23cec27..4d2b94d 100644 --- a/res/lang/ru.ini +++ b/res/lang/ru.ini @@ -3,26 +3,26 @@ brightness_down = уменьшить яркость brightness_up = увеличить яркость capslock = capslock custom = пользовательский - - - +custom_info_err_output_long = вывод слишком длинный +custom_info_err_no_output = нет вывода +custom_info_err_no_output_error = , возможная ошибка err_alloc = не удалось выделить память - +err_args = не удалось разобрать аргументы командной строки err_autologin_session = не найдена сессия с автологином err_bounds = за пределами индекса err_brightness_change = не удалось изменить яркость err_chdir = не удалось открыть домашнюю папку err_clock_too_long = строка часов слишком длинная err_config = не удалось разобрать файл конфигурации - +err_crawl = не удалось просканировать каталоги сессий err_dgn_oob = отладочное сообщение (log) err_domain = неверный домен err_empty_password = пустой пароль не допустим err_envlist = не удалось получить список переменных среды err_get_active_tty = не удалось получить активный tty - +err_hibernate = не удалось выполнить команду гибернации err_hostname = не удалось получить имя хоста - +err_inactivity = не удалось выполнить команду бездействия err_lock_state = не удалось получить состояние lock err_log = не удалось открыть файл log err_mlock = сбой блокировки памяти @@ -50,12 +50,12 @@ err_perm_group = не удалось понизить права доступа err_perm_user = не удалось понизить права доступа пользователя err_pwnam = не удалось получить информацию о пользователе err_sleep = не удалось выполнить команду sleep - +err_start = не удалось выполнить команду запуска err_battery = не удалось получить статус батареи err_switch_tty = не удалось переключить tty err_tty_ctrl = передача управления tty не удалась err_no_users = пользователи не найдены - +err_uid_range = не удалось динамически получить диапазон uid err_user_gid = не удалось установить GID пользователя err_user_init = не удалось инициализировать пользователя err_user_uid = не удалось установить UID пользователя @@ -63,11 +63,11 @@ err_xauth = команда xauth не выполнена err_xcb_conn = ошибка подключения xcb err_xsessions_dir = не удалось найти сессионную папку err_xsessions_open = не удалось открыть сессионную папку - +hibernate = гибернация insert = вставка login = логин logout = вышел из системы -no_x11_support = поддержка x11 отключена во время компиляции +no_x11_support = поддержка x11 отключена при компиляции normal = обычный numlock = numlock other = прочие @@ -76,7 +76,7 @@ restart = перезагрузить shell = оболочка shutdown = выключить sleep = сон - +toggle_password = показать/скрыть пароль wayland = wayland x11 = x11 xinitrc = xinitrc diff --git a/res/lang/sr.ini b/res/lang/sr.ini index d0ad85a..a2678bf 100644 --- a/res/lang/sr.ini +++ b/res/lang/sr.ini @@ -1,82 +1,82 @@ - - - +authenticating = autentifikacija... +brightness_down = smanji osvetljenost +brightness_up = povećaj osvetljenost capslock = capslock - - - - -err_alloc = neuspijesna alokacija memorije - - +custom = prilagođeno +custom_info_err_output_long = izlaz predugačak +custom_info_err_no_output = nema izlaza +custom_info_err_no_output_error = , moguća greška +err_alloc = neuspješna alokacija memorije +err_args = nije moguće raščlaniti argumente komandne linije +err_autologin_session = sesija automatske prijave nije pronađena err_bounds = izvan granica indeksa - -err_chdir = neuspijesno otvaranje home foldera - - - +err_brightness_change = nije uspela promena osvetljenosti +err_chdir = neuspješno otvaranje home foldera +err_clock_too_long = niska sata je preduga +err_config = nije moguće raščlaniti konfiguracioni fajl +err_crawl = nije uspelo skeniranje direktorijuma sesija err_dgn_oob = log poruka -err_domain = nevazeci domen - - - - -err_hostname = neuspijesno trazenje hostname-a - - - -err_mlock = neuspijesno zakljucavanje memorije lozinke -err_null = null pokazivac - -err_pam = pam transakcija neuspijesna +err_domain = nevažeći domen +err_empty_password = prazna lozinka nije dozvoljena +err_envlist = nije uspelo dobijanje liste promenljivih okruženja +err_get_active_tty = nije uspelo dobijanje aktivnog tty +err_hibernate = nije uspelo izvršavanje komande hibernacije +err_hostname = neuspješno traženje hostname-a +err_inactivity = nije uspelo izvršavanje komande neaktivnosti +err_lock_state = nije uspelo dobijanje stanja zaključavanja +err_log = nije uspelo otvaranje fajla dnevnika +err_mlock = neuspješno zaključavanje memorije lozinke +err_null = null pokazivač +err_numlock = nije uspelo postavljanje numlock +err_pam = pam transakcija neuspješna err_pam_abort = pam transakcija prekinuta err_pam_acct_expired = nalog istekao -err_pam_auth = greska pri autentikaciji -err_pam_authinfo_unavail = neuspjelo uzimanje informacija o korisniku +err_pam_auth = greška pri autentikaciji +err_pam_authinfo_unavail = neuspješno uzimanje informacija o korisniku err_pam_authok_reqd = token istekao -err_pam_buf = greska bafera memorije -err_pam_cred_err = neuspjelo postavljanje kredencijala +err_pam_buf = greška bafera memorije +err_pam_cred_err = neuspješno postavljanje kredencijala err_pam_cred_expired = kredencijali istekli err_pam_cred_insufficient = nedovoljni kredencijali -err_pam_cred_unavail = neuspjelo uzimanje kredencijala -err_pam_maxtries = dostignut maksimalan broj pokusaja -err_pam_perm_denied = nedozovoljeno -err_pam_session = greska sesije -err_pam_sys = greska sistema +err_pam_cred_unavail = neuspješno uzimanje kredencijala +err_pam_maxtries = dostignut maksimalan broj pokušaja +err_pam_perm_denied = nedozvoljeno +err_pam_session = greška sesije +err_pam_sys = greška sistema err_pam_user_unknown = nepoznat korisnik -err_path = neuspjelo postavljanje path-a -err_perm_dir = neuspjelo mijenjanje foldera -err_perm_group = neuspjesno snizavanje dozvola grupe -err_perm_user = neuspijesno snizavanje dozvola korisnika -err_pwnam = neuspijesno skupljanje informacija o korisniku - - - - - - - -err_user_gid = neuspijesno postavljanje korisničkog GID-a -err_user_init = neuspijensa inicijalizacija korisnika -err_user_uid = neuspijesno postavljanje UID-a korisnika - - -err_xsessions_dir = neuspijesno pronalazenje foldera sesija -err_xsessions_open = neuspijesno otvaranje foldera sesija - - +err_path = neuspješno postavljanje path-a +err_perm_dir = neuspješno mijenjanje foldera +err_perm_group = neuspješno snižavanje dozvola grupe +err_perm_user = neuspješno snižavanje dozvola korisnika +err_pwnam = neuspješno skupljanje informacija o korisniku +err_sleep = nije uspelo izvršavanje komande spavanja +err_start = nije uspelo izvršavanje komande pokretanja +err_battery = nije uspelo učitavanje statusa baterije +err_switch_tty = nije uspelo prebacivanje tty +err_tty_ctrl = prenos kontrole tty nije uspeo +err_no_users = nisu pronađeni korisnici +err_uid_range = nije uspelo dinamičko dobijanje opsega uid +err_user_gid = neuspješno postavljanje korisničkog GID-a +err_user_init = neuspješna inicijalizacija korisnika +err_user_uid = neuspješno postavljanje UID-a korisnika +err_xauth = komanda xauth nije uspela +err_xcb_conn = xcb veza nije uspela +err_xsessions_dir = neuspješno pronalaženje foldera sesija +err_xsessions_open = neuspješno otvaranje foldera sesija +hibernate = hibernacija +insert = umetni login = korisnik logout = izlogovan - - +no_x11_support = x11 podrška onemogućena tokom prevođenja +normal = normalno numlock = numlock - +other = ostalo password = lozinka restart = ponovo pokreni shell = shell shutdown = ugasi - - +sleep = uspavaj +toggle_password = prikaži/sakrij lozinku wayland = wayland - +x11 = x11 xinitrc = xinitrc diff --git a/res/lang/sr_Cyrl.ini b/res/lang/sr_Cyrl.ini new file mode 100644 index 0000000..6222b3d --- /dev/null +++ b/res/lang/sr_Cyrl.ini @@ -0,0 +1,82 @@ +authenticating = аутентификација... +brightness_down = смањи осветљеност +brightness_up = повећај осветљеност +capslock = capslock +custom = прилагођено +custom_info_err_output_long = излаз предугачак +custom_info_err_no_output = нема излаза +custom_info_err_no_output_error = , могућа грешка +err_alloc = неуспешна алокација меморије +err_args = није могуће рашчланити аргументе командне линије +err_autologin_session = сесија аутоматске пријаве није пронађена +err_bounds = изван граница индекса +err_brightness_change = није успела промена осветљености +err_chdir = неуспешно отварање home фолдера +err_clock_too_long = ниска сата је предуга +err_config = није могуће рашчланити конфигурациони фајл +err_crawl = није успело скенирање директоријума сесија +err_dgn_oob = лог порука +err_domain = неважећи домен +err_empty_password = празна лозинка није дозвољена +err_envlist = није успело добијање листе променљивих окружења +err_get_active_tty = није успело добијање активног tty +err_hibernate = није успело извршавање команде хибернације +err_hostname = неуспешно тражење hostname-а +err_inactivity = није успело извршавање команде неактивности +err_lock_state = није успело добијање стања закључавања +err_log = није успело отварање фајла дневника +err_mlock = неуспешно закључавање меморије лозинке +err_null = null показивач +err_numlock = није успело постављање numlock +err_pam = pam трансакција неуспешна +err_pam_abort = pam трансакција прекинута +err_pam_acct_expired = налог истекао +err_pam_auth = грешка при аутентикацији +err_pam_authinfo_unavail = неуспешно узимање информација о кориснику +err_pam_authok_reqd = токен истекао +err_pam_buf = грешка бафера меморије +err_pam_cred_err = неуспешно постављање акредитива +err_pam_cred_expired = акредитиви истекли +err_pam_cred_insufficient = недовољни акредитиви +err_pam_cred_unavail = неуспешно узимање акредитива +err_pam_maxtries = достигнут максималан број покушаја +err_pam_perm_denied = недозвољено +err_pam_session = грешка сесије +err_pam_sys = грешка система +err_pam_user_unknown = непознат корисник +err_path = неуспешно постављање путање +err_perm_dir = неуспешно мењање фолдера +err_perm_group = неуспешно снижавање дозвола групе +err_perm_user = неуспешно снижавање дозвола корисника +err_pwnam = неуспешно прикупљање информација о кориснику +err_sleep = није успело извршавање команде спавања +err_start = није успело извршавање команде покретања +err_battery = није успело учитавање статуса батерије +err_switch_tty = није успело пребацивање tty +err_tty_ctrl = пренос контроле tty није успео +err_no_users = нису пронађени корисници +err_uid_range = није успело динамичко добијање опсега uid +err_user_gid = неуспешно постављање корисничког GID-а +err_user_init = неуспешна иницијализација корисника +err_user_uid = неуспешно постављање UID-а корисника +err_xauth = команда xauth није успела +err_xcb_conn = xcb веза није успела +err_xsessions_dir = неуспешно проналажење фолдера сесија +err_xsessions_open = неуспешно отварање фолдера сесија +hibernate = хибернација +insert = уметни +login = корисник +logout = одјављен +no_x11_support = x11 подршка онемогућена током превођења +normal = нормално +numlock = numlock +other = остало +password = лозинка +restart = поново покрени +shell = shell +shutdown = угаси +sleep = успавај +toggle_password = прикажи/сакриј лозинку +wayland = wayland +x11 = x11 +xinitrc = xinitrc diff --git a/res/lang/sv.ini b/res/lang/sv.ini index adec801..45e36d4 100644 --- a/res/lang/sv.ini +++ b/res/lang/sv.ini @@ -3,9 +3,9 @@ brightness_down = minska ljusstyrka brightness_up = öka ljusstyrka capslock = capslock custom = anpassad - - - +custom_info_err_output_long = utdata för lång +custom_info_err_no_output = ingen utdata +custom_info_err_no_output_error = , möjligt fel err_alloc = minnesallokering misslyckades err_args = tolkning av kommandoargument misslyckades err_autologin_session = autologin-session hittades inte @@ -26,7 +26,7 @@ err_inactivity = inaktivitetslägets kommando misslyckades err_lock_state = hämtning av låsningsstatus misslyckades err_log = öppning av loggfil misslyckades err_mlock = låsning av lösenordsminne misslyckades -err_null = null pointer +err_null = nullpekare err_numlock = inställning av numlock misslyckades err_pam = pam-transaktion misslyckades err_pam_abort = pam-transaktion avbröts @@ -76,7 +76,7 @@ restart = starta om shell = shell shutdown = stäng av sleep = viloläge - +toggle_password = visa/dölj lösenord wayland = wayland x11 = x11 xinitrc = xinitrc diff --git a/res/lang/tr.ini b/res/lang/tr.ini index 4ee5960..414f4d5 100644 --- a/res/lang/tr.ini +++ b/res/lang/tr.ini @@ -1,33 +1,33 @@ - +authenticating = kimlik doğrulanıyor... brightness_down = parlakligi azalt brightness_up = parlakligi arttir capslock = capslock - - - - +custom = özel +custom_info_err_output_long = çıktı çok uzun +custom_info_err_no_output = çıktı yok +custom_info_err_no_output_error = , olası hata err_alloc = basarisiz bellek ayirma - - +err_args = komut satırı argümanları ayrıştırılamıyor +err_autologin_session = otomatik oturum açma oturumu bulunamadı err_bounds = sinirlarin disinda dizin - +err_brightness_change = parlaklık değiştirilemedi err_chdir = ev klasoru acilamadi - - - +err_clock_too_long = saat dizesi çok uzun +err_config = yapılandırma dosyası ayrıştırılamıyor +err_crawl = oturum dizinleri taranamadı err_dgn_oob = log mesaji err_domain = gecersiz etki alani - - - - +err_empty_password = boş parola kullanılamaz +err_envlist = ortam değişkenleri listesi alınamadı +err_get_active_tty = aktif tty alınamadı +err_hibernate = hazırda bekletme komutu çalıştırılamadı err_hostname = ana bilgisayar adi alinamadi - - - +err_inactivity = hareketsizlik komutu çalıştırılamadı +err_lock_state = kilit durumu alınamadı +err_log = günlük dosyası açılamadı err_mlock = parola bellegi kilitlenemedi err_null = bos isaretci hatasi - +err_numlock = numlock ayarlanamadı err_pam = pam islemi basarisiz oldu err_pam_abort = pam islemi durduruldu err_pam_acct_expired = hesabin suresi dolmus @@ -49,26 +49,26 @@ err_perm_dir = gecerli dizin degistirilemedi err_perm_group = grup izinleri dusurulemedi err_perm_user = kullanici izinleri dusurulemedi err_pwnam = kullanici bilgileri alinamadi - - - - - - - +err_sleep = uyku komutu çalıştırılamadı +err_start = başlatma komutu çalıştırılamadı +err_battery = pil durumu yüklenemedi +err_switch_tty = tty değiştirilemedi +err_tty_ctrl = tty kontrol aktarımı başarısız oldu +err_no_users = kullanıcı bulunamadı +err_uid_range = uid aralığı dinamik olarak alınamadı err_user_gid = kullanici icin GID ayarlanamadi err_user_init = kullanici oturumu baslatilamadi err_user_uid = kullanici icin UID ayarlanamadi - - +err_xauth = xauth komutu başarısız oldu +err_xcb_conn = xcb bağlantısı başarısız oldu err_xsessions_dir = oturumlar klasoru bulunamadi err_xsessions_open = oturumlar klasoru acilamadi hibernate = askiya al - +insert = ekle login = kullanici logout = oturumdan cikis yapildi - - +no_x11_support = x11 desteği derleme zamanında devre dışı bırakıldı +normal = normal numlock = numlock other = baska password = sifre @@ -76,7 +76,7 @@ restart = yeniden baslat shell = shell shutdown = makineyi kapat sleep = uykuya al - +toggle_password = parolayı göster/gizle wayland = wayland - +x11 = x11 xinitrc = xinitrc diff --git a/res/lang/uk.ini b/res/lang/uk.ini index 7b47f8a..6baa469 100644 --- a/res/lang/uk.ini +++ b/res/lang/uk.ini @@ -1,33 +1,33 @@ - - - +authenticating = автентифікація... +brightness_down = зменшити яскравість +brightness_up = збільшити яскравість capslock = capslock - - - - +custom = власний +custom_info_err_output_long = вивід занадто довгий +custom_info_err_no_output = немає виводу +custom_info_err_no_output_error = , можлива помилка err_alloc = невдале виділення пам'яті - - +err_args = не вдалося розібрати аргументи командного рядка +err_autologin_session = сеанс автоматичного входу не знайдено err_bounds = поза межами індексу - +err_brightness_change = не вдалося змінити яскравість err_chdir = не вдалося відкрити домашній каталог - - - +err_clock_too_long = рядок годинника занадто довгий +err_config = не вдалося розібрати файл конфігурації +err_crawl = не вдалося сканувати каталоги сесій err_dgn_oob = повідомлення журналу (log) err_domain = недійсний домен - - - - +err_empty_password = порожній пароль не дозволено +err_envlist = не вдалося отримати список змінних середовища +err_get_active_tty = не вдалося отримати активний tty +err_hibernate = не вдалося виконати команду гібернації err_hostname = не вдалося отримати ім'я хосту - - - +err_inactivity = не вдалося виконати команду неактивності +err_lock_state = не вдалося отримати стан блокування +err_log = не вдалося відкрити файл журналу err_mlock = збій блокування пам'яті err_null = нульовий вказівник - +err_numlock = не вдалося встановити numlock err_pam = невдала pam транзакція err_pam_abort = pam транзакція перервана err_pam_acct_expired = термін дії акаунту вичерпано @@ -49,34 +49,34 @@ err_perm_dir = не вдалося змінити поточний катало err_perm_group = не вдалося понизити права доступу групи err_perm_user = не вдалося понизити права доступу користувача err_pwnam = не вдалося отримати дані користувача - - - - - - - +err_sleep = не вдалося виконати команду сну +err_start = не вдалося виконати команду запуску +err_battery = не вдалося завантажити стан акумулятора +err_switch_tty = не вдалося переключити tty +err_tty_ctrl = передача керування tty не вдалася +err_no_users = користувачів не знайдено +err_uid_range = не вдалося динамічно отримати діапазон uid err_user_gid = не вдалося змінити GID користувача err_user_init = не вдалося ініціалізувати користувача err_user_uid = не вдалося змінити UID користувача - - +err_xauth = команда xauth не вдалася +err_xcb_conn = з'єднання xcb не вдалося err_xsessions_dir = не вдалося знайти каталог сесій err_xsessions_open = не вдалося відкрити каталог сесій - - +hibernate = гібернація +insert = вставити login = логін logout = вийти - - +no_x11_support = підтримку x11 вимкнено під час компіляції +normal = нормальний numlock = numlock - +other = інший password = пароль restart = перезавантажити shell = оболонка shutdown = вимкнути - - +sleep = сплячий режим +toggle_password = показати/приховати пароль wayland = wayland - +x11 = x11 xinitrc = xinitrc diff --git a/res/lang/zh_CN.ini b/res/lang/zh_CN.ini index d2af6c3..4562624 100644 --- a/res/lang/zh_CN.ini +++ b/res/lang/zh_CN.ini @@ -1,33 +1,33 @@ - - - +authenticating = 正在认证... +brightness_down = 降低亮度 +brightness_up = 提高亮度 capslock = 大写锁定 - - - - +custom = 自定义 +custom_info_err_output_long = 输出过长 +custom_info_err_no_output = 无输出 +custom_info_err_no_output_error = ,可能有错误 err_alloc = 内存分配失败 - - +err_args = 无法解析命令行参数 +err_autologin_session = 未找到自动登录会话 err_bounds = 索引越界 - +err_brightness_change = 无法更改亮度 err_chdir = 无法打开home文件夹 - - - +err_clock_too_long = 时钟字符串过长 +err_config = 无法解析配置文件 +err_crawl = 无法扫描会话目录 err_dgn_oob = 日志消息 err_domain = 无效的域 - - - - +err_empty_password = 不允许空密码 +err_envlist = 无法获取环境变量列表 +err_get_active_tty = 无法获取当前活动的tty +err_hibernate = 无法执行休眠命令 err_hostname = 获取主机名失败 - - - +err_inactivity = 无法执行非活动命令 +err_lock_state = 无法获取锁定状态 +err_log = 无法打开日志文件 err_mlock = 锁定密码存储器失败 err_null = 空指针 - +err_numlock = 无法设置numlock err_pam = PAM事件失败 err_pam_abort = PAM事务已中止 err_pam_acct_expired = 帐户已过期 @@ -49,34 +49,34 @@ err_perm_dir = 更改当前目录失败 err_perm_group = 组权限降级失败 err_perm_user = 用户权限降级失败 err_pwnam = 获取用户信息失败 - - - - - - - +err_sleep = 无法执行睡眠命令 +err_start = 无法执行启动命令 +err_battery = 无法加载电池状态 +err_switch_tty = 无法切换tty +err_tty_ctrl = tty控制转移失败 +err_no_users = 未找到用户 +err_uid_range = 无法动态获取uid范围 err_user_gid = 设置用户GID失败 err_user_init = 初始化用户失败 err_user_uid = 设置用户UID失败 - - +err_xauth = xauth命令失败 +err_xcb_conn = xcb连接失败 err_xsessions_dir = 找不到会话文件夹 err_xsessions_open = 无法打开会话文件夹 - - +hibernate = 休眠 +insert = 插入 login = 登录 logout = 注销 - - +no_x11_support = x11支持在编译时被禁用 +normal = 正常 numlock = 数字锁定 - +other = 其他 password = 密码 - +restart = 重启 shell = shell - - - +shutdown = 关机 +sleep = 睡眠 +toggle_password = 显示/隐藏密码 wayland = wayland x11 = x11 xinitrc = xinitrc diff --git a/res/lang/zh_TW.ini b/res/lang/zh_TW.ini new file mode 100644 index 0000000..50d5f68 --- /dev/null +++ b/res/lang/zh_TW.ini @@ -0,0 +1,82 @@ +authenticating = 正在驗證... +brightness_down = 降低亮度 +brightness_up = 提高亮度 +capslock = 大寫鎖定 +custom = 自訂 +custom_info_err_output_long = 輸出過長 +custom_info_err_no_output = 無輸出 +custom_info_err_no_output_error = ,可能有錯誤 +err_alloc = 記憶體配置失敗 +err_args = 無法解析命令列參數 +err_autologin_session = 找不到自動登入工作階段 +err_bounds = 索引超出範圍 +err_brightness_change = 無法變更亮度 +err_chdir = 無法開啟家目錄 +err_clock_too_long = 時鐘字串過長 +err_config = 無法解析設定檔 +err_crawl = 無法掃描工作階段目錄 +err_dgn_oob = 日誌訊息 +err_domain = 無效的網域 +err_empty_password = 不允許空密碼 +err_envlist = 無法取得環境變數清單 +err_get_active_tty = 無法取得目前使用中的 tty +err_hibernate = 無法執行休眠命令 +err_hostname = 取得主機名稱失敗 +err_inactivity = 無法執行閒置命令 +err_lock_state = 無法取得鎖定狀態 +err_log = 無法開啟日誌檔案 +err_mlock = 鎖定密碼記憶體失敗 +err_null = 空指標 +err_numlock = 無法設定 numlock +err_pam = PAM 交易失敗 +err_pam_abort = PAM 交易已中止 +err_pam_acct_expired = 帳號已過期 +err_pam_auth = 驗證錯誤 +err_pam_authinfo_unavail = 取得使用者資訊失敗 +err_pam_authok_reqd = 金鑰已過期 +err_pam_buf = 記憶體緩衝區錯誤 +err_pam_cred_err = 設定憑證失敗 +err_pam_cred_expired = 憑證已過期 +err_pam_cred_insufficient = 憑證不足 +err_pam_cred_unavail = 無法取得憑證 +err_pam_maxtries = 已達到最大嘗試次數限制 +err_pam_perm_denied = 拒絕存取 +err_pam_session = 工作階段錯誤 +err_pam_sys = 系統錯誤 +err_pam_user_unknown = 未知的使用者 +err_path = 無法設定路徑 +err_perm_dir = 變更目前目錄失敗 +err_perm_group = 群組權限降級失敗 +err_perm_user = 使用者權限降級失敗 +err_pwnam = 取得使用者資訊失敗 +err_sleep = 無法執行睡眠命令 +err_start = 無法執行啟動命令 +err_battery = 無法載入電池狀態 +err_switch_tty = 無法切換 tty +err_tty_ctrl = tty 控制權移轉失敗 +err_no_users = 找不到使用者 +err_uid_range = 無法動態取得 uid 範圍 +err_user_gid = 設定使用者 GID 失敗 +err_user_init = 初始化使用者失敗 +err_user_uid = 設定使用者 UID 失敗 +err_xauth = xauth 命令失敗 +err_xcb_conn = xcb 連線失敗 +err_xsessions_dir = 找不到工作階段資料夾 +err_xsessions_open = 無法開啟工作階段資料夾 +hibernate = 休眠 +insert = 插入 +login = 登入 +logout = 登出 +no_x11_support = x11 支援已在編譯時停用 +normal = 正常 +numlock = 數字鎖定 +other = 其他 +password = 密碼 +restart = 重新啟動 +shell = shell +shutdown = 關機 +sleep = 睡眠 +toggle_password = 顯示/隱藏密碼 +wayland = wayland +x11 = x11 +xinitrc = xinitrc From 32436d438e82a4195a15758fc3ae6218d4815e5f Mon Sep 17 00:00:00 2001 From: koru Date: Sat, 6 Jun 2026 11:54:29 +0200 Subject: [PATCH 133/176] Update `err_crawl` for Russian lang (#1011) See: https://codeberg.org/fairyglade/ly/pulls/997#issuecomment-15674144 Reviewed-on: https://codeberg.org/fairyglade/ly/pulls/1011 Reviewed-by: AnErrupTion --- res/lang/ru.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/res/lang/ru.ini b/res/lang/ru.ini index 4d2b94d..014c170 100644 --- a/res/lang/ru.ini +++ b/res/lang/ru.ini @@ -14,7 +14,7 @@ err_brightness_change = не удалось изменить яркость err_chdir = не удалось открыть домашнюю папку err_clock_too_long = строка часов слишком длинная err_config = не удалось разобрать файл конфигурации -err_crawl = не удалось просканировать каталоги сессий +err_crawl = не удалось просканировать каталоги сессии err_dgn_oob = отладочное сообщение (log) err_domain = неверный домен err_empty_password = пустой пароль не допустим From 5b8ebeba6ebbad84923b12f5690f735d68330b8d Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Wed, 10 Jun 2026 14:45:48 +0200 Subject: [PATCH 134/176] Create CONTRIBUTING.md Signed-off-by: AnErrupTion --- CONTRIBUTING.md | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 CONTRIBUTING.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..eddaffb --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,29 @@ +# Contributing to Ly + +To contribute to Ly, you have to open a pull request on [Codeberg](https://codeberg.org/fairyglade/ly), as [GitHub](https://github.com/fairyglade/ly) is just a mirror. However, you also have to respect the following rules, otherwise your PR may end up being rejected: + +## AI usage + +While we cannot control your usage of any LLM whatsoever, we do heavily discourage their use for environmental, ethical & moral reasons that you can learn more about on the Internet. However, if you do end up still using them, here are a couple rules you **must absolutely** respect for your PR not to get instantly shot down. Of course, it goes without saying that **all** the other rules in the other sections below **must** be adhered to, even more so when AI is used. + +1. **Communicate through you**, not the AI; as in, responses to reviews or other comments in the PR must be written by you and not an LLM. If English isn't your native language and you have a lot of trouble speaking it, you _may_ use AI, but responses must not be your typical, alienating AI-generated text: https://github.com/realrossmanngroup/no_ai_slop_writing_rules +2. Control the code, as in, **don't vibecode**, especially if you don't know Zig. Don't waste the maintainers's time, and **heavily test your code**, as AI-generated code is typically more error-prone in subtle ways (even if they can be better than humans in some other aspects). Finally, don't generate any "summary" or whatever the AI may come up with during the process. **If you cannot understand the generated code, do not contribute it**. Maintainers are busy and are often simple volunteers, so the less work they have to do, the better. +3. **Be transparent about AI usage**. Precise what AI model was used in the process, and preferably, how you went about creating your changes (i.e. how did you use AI to make the pull request). The more honest you are about it, the more likely your PR will be accepted and merged into the code base. + +If all the above rules are respected, your changes have much more likely to be accepted into the code base. + +## Code style + +You must follow Zig's [style guide](https://ziglang.org/documentation/master/#Style-Guide). In most cases, all you'll have to do is run `zig fmt` after you have completed your changes, though it does not fix everything, notably variable, function & field naming, as well as the maximum length of aline. + +For the former, please refer to the aforementioned style guide in that case. For the latter, you must respect a maximum line length of **80 characters**. For function calls with many parameters or anything similar that may overflow this limit, consider adding a trailing comma to the list so that `zig fmt` can split it into multiple lines. Ideally, few or no lines end up having to get soft wrapped by an editor having this limit set. + +## Commit names & descriptions + +1. **Commit names must be descriptive**, resorting to the commit description if they are too long. In such cases, the commit name **must** be shortened while still making sense. With that said, no particular convention (emojis, prefixes & suffixes, etc.) is mandated for them, and you are free to adopt any one you think is best or that you are used to. +2. **Do not force push**, as PRs will get squashed into a single commit anyway. This erases history and makes it impossible to see the changes done over time and by a particular commit. +3. While not a requirement per se, **consider signing your commits** with an SSH or GPG key for security purposes. This ensures to a lesser extent that you are the person who you claim to be, and reduces the potential amount of malicious contributions that could infiltrate the code base. + +## Additional requirements + +Other requirements (such as testing your code) for merging the changes will be available when you open your pull request via the template (which you **must** use), however, these are not stated here because they may not be mandatory in some cases (e.g. if the changes are still work-in-progress), so they may temporarily be ommitted for a certain period of time until the changes are fully ready to be reviewed, for instance. From d8c3aec6685e8d9f0aed25846d34ece830f2988a Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Wed, 10 Jun 2026 14:49:39 +0200 Subject: [PATCH 135/176] Update PR template to reflect new contributing guidelines Signed-off-by: AnErrupTion --- .github/pull_request_template.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 45044a3..a32f0c2 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -9,4 +9,4 @@ _Replace this with a reference to an existing issue, or N/A if there is none_ ## Pre-requisites - [ ] I have tested & confirmed the changes work locally -- [ ] I have run `zig fmt` throughout my changes +- [ ] I have read and fully adhere to the rules set in the contributing guidelines found in `CONTRIBUTING.md` From a07ca88c743f5f30bb207695e47b5c98cb1a85aa Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Thu, 11 Jun 2026 20:35:42 +0200 Subject: [PATCH 136/176] build.zig: Fix new locales not being installed Signed-off-by: AnErrupTion --- build.zig | 5 +++++ CONTRIBUTING.md => contributing.md | 0 2 files changed, 5 insertions(+) rename CONTRIBUTING.md => contributing.md (100%) diff --git a/build.zig b/build.zig index 9e89029..21c56b4 100644 --- a/build.zig +++ b/build.zig @@ -203,14 +203,17 @@ fn install_ly(allocator: std.mem.Allocator, io: std.Io, patch_map: PatchMap, ins const languages = [_][]const u8{ "ar.ini", + "bg.ini", "cat.ini", "cs.ini", "de.ini", "en.ini", + "eo.ini", "es.ini", "fr.ini", "it.ini", "ja_JP.ini", + "ku.ini", "lv.ini", "pl.ini", "pt.ini", @@ -218,10 +221,12 @@ fn install_ly(allocator: std.mem.Allocator, io: std.Io, patch_map: PatchMap, ins "ro.ini", "ru.ini", "sr.ini", + "sr_Cyrl.ini", "sv.ini", "tr.ini", "uk.ini", "zh_CN.ini", + "zh_TW.ini", }; inline for (languages) |language| { diff --git a/CONTRIBUTING.md b/contributing.md similarity index 100% rename from CONTRIBUTING.md rename to contributing.md From 008d413295ff56a0a6491fc83c6939e530bc5398 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D0=BC=D0=B8=D1=82=D1=80=D0=B8=D0=B9?= Date: Fri, 12 Jun 2026 12:09:37 +0200 Subject: [PATCH 137/176] Fix user detection if /etc/login.defs does not contain both UID_MIN and UID_MAX (#1014) ## What are the changes about? I modified getUserIdRange(), so it checks if either of UID_MIN and UID_MAX is set in /etc/login.defs. If any, but not both, is not set, it uses fallback value. I don't know zig build system, so I added ugly fallback values passthrough. ## What existing issue does this resolve? #1013 ## Pre-requisites - [X] I have tested & confirmed the changes work locally - [X] I have read and fully adhere to the rules set in the contributing guidelines found in `CONTRIBUTING.md` Reviewed-on: https://codeberg.org/fairyglade/ly/pulls/1014 Reviewed-by: AnErrupTion --- build.zig | 2 ++ ly-core/build.zig | 7 +++++++ ly-core/src/interop.zig | 20 ++++++++++++++++---- ly-ui/build.zig | 5 +++++ 4 files changed, 30 insertions(+), 4 deletions(-) diff --git a/build.zig b/build.zig index 21c56b4..2de19f7 100644 --- a/build.zig +++ b/build.zig @@ -75,6 +75,8 @@ pub fn build(b: *std.Build) !void { .target = target, .optimize = optimize, .enable_x11_support = enable_x11_support, + .fallback_uid_min = fallback_uid_min, + .fallback_uid_max = fallback_uid_max }); exe.root_module.addImport("ly-ui", ly_ui.module("ly-ui")); diff --git a/ly-core/build.zig b/ly-core/build.zig index 56dc4ec..cf0dbf8 100644 --- a/ly-core/build.zig +++ b/ly-core/build.zig @@ -11,6 +11,13 @@ pub fn build(b: *std.Build) void { .optimize = optimize, }); + const fallback_uid_min = b.option(std.posix.uid_t, "fallback_uid_min", "Set the fallback minimum UID (default is 1000). This value gets embedded into the binary").?; + const fallback_uid_max = b.option(std.posix.uid_t, "fallback_uid_max", "Set the fallback maximum UID (default is 60000). This value gets embedded into the binary").?; + const build_options = b.addOptions(); + build_options.addOption(std.posix.uid_t, "fallback_uid_min", fallback_uid_min); + build_options.addOption(std.posix.uid_t, "fallback_uid_max", fallback_uid_max); + mod.addOptions("build_options", build_options); + const zigini = b.dependency("zigini", .{ .target = target, .optimize = optimize }); mod.addImport("zigini", zigini.module("zigini")); diff --git a/ly-core/src/interop.zig b/ly-core/src/interop.zig index 69e02ff..7d09381 100644 --- a/ly-core/src/interop.zig +++ b/ly-core/src/interop.zig @@ -1,5 +1,6 @@ const std = @import("std"); const builtin = @import("builtin"); +const build_options = @import("build_options"); const UidRange = @import("UidRange.zig"); const pwd = @import("pwd"); const stdlib = @import("stdlib"); @@ -145,21 +146,32 @@ fn PlatformStruct() type { var iterator = std.mem.splitScalar(u8, login_defs_buffer, '\n'); var uid_range = UidRange{}; - var nameFound = false; + var uid_min_Found = false; + var uid_max_Found = false; while (iterator.next()) |line| { const trimmed_line = std.mem.trim(u8, line, " \n\r\t"); if (std.mem.startsWith(u8, trimmed_line, "UID_MIN")) { uid_range.uid_min = try parseValue(std.posix.uid_t, "UID_MIN", trimmed_line); - nameFound = true; + uid_min_Found = true; } else if (std.mem.startsWith(u8, trimmed_line, "UID_MAX")) { uid_range.uid_max = try parseValue(std.posix.uid_t, "UID_MAX", trimmed_line); - nameFound = true; + uid_max_Found = true; } } - if (!nameFound) return error.UidNameNotFound; + if (!(uid_min_Found or uid_max_Found)) { + return error.UidNameNotFound; + } + + if (!uid_min_Found) { + uid_range.uid_min = build_options.fallback_uid_min; + } + + if (!uid_max_Found) { + uid_range.uid_max = build_options.fallback_uid_max; + } return uid_range; } diff --git a/ly-ui/build.zig b/ly-ui/build.zig index 2e3ce6b..0ab5b70 100644 --- a/ly-ui/build.zig +++ b/ly-ui/build.zig @@ -11,10 +11,15 @@ pub fn build(b: *std.Build) void { .optimize = optimize, }); + const fallback_uid_min = b.option(std.posix.uid_t, "fallback_uid_min", "Set the fallback minimum UID (default is 1000). This value gets embedded into the binary"); + const fallback_uid_max = b.option(std.posix.uid_t, "fallback_uid_max", "Set the fallback maximum UID (default is 60000). This value gets embedded into the binary"); + const ly_core = b.dependency("ly_core", .{ .target = target, .optimize = optimize, .enable_x11_support = enable_x11_support, + .fallback_uid_min = fallback_uid_min, + .fallback_uid_max = fallback_uid_max }); mod.addImport("ly-core", ly_core.module("ly-core")); From f683a82f562fb20dd19776e21669e5a9f6c8d010 Mon Sep 17 00:00:00 2001 From: oliebe Date: Sun, 14 Jun 2026 21:03:41 +0200 Subject: [PATCH 138/176] Fix small typos in readme.md (#1017) ## What are the changes about? I fixed some incorrect inflections and typos in the readme ## What existing issue does this resolve? N/A ## Pre-requisites - [x] I have tested & confirmed the changes work locally - [x] I have read and fully adhere to the rules set in the contributing guidelines found in `CONTRIBUTING.md` Reviewed-on: https://codeberg.org/fairyglade/ly/pulls/1017 Reviewed-by: AnErrupTion --- readme.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/readme.md b/readme.md index 79c3a63..4d11596 100644 --- a/readme.md +++ b/readme.md @@ -217,7 +217,7 @@ ttyv1 "/usr/libexec/getty Ly" xterm on secure ### Updating -You can also install Ly without overrding the current configuration file. This is called **updating**. To update, simply run: +You can also install Ly without overriding the current configuration file. This is called **updating**. To update, simply run: ``` # zig build installnoconf @@ -241,7 +241,7 @@ Use the Up/Down arrow keys to change the current field, and the Left/Right arrow ## A note on .xinitrc -If your `.xinitrc` file doesn't work ,make sure it is executable and includes a shebang. This file is supposed to be a shell script! Quoting from `xinit`'s man page: +If your `.xinitrc` file doesn't work, make sure it is executable and includes a shebang. This file is supposed to be a shell script! Quoting from `xinit`'s man page: > If no specific client program is given on the command line, xinit will look for a file in the user's home directory called .xinitrc to run as a shell script to start up client programs. @@ -253,7 +253,7 @@ A typical shebang for a shell script looks like this: ## Tips -- The numlock and capslock state is printed in the top-right corner. +- The numlock and capslock states are printed in the top-right corner. - Use the F1 and F2 keys to respectively shutdown and reboot. From ed783b5a2392102daee66946211601f4471d15c8 Mon Sep 17 00:00:00 2001 From: Yuri dos Santos Date: Sun, 14 Jun 2026 21:59:15 +0200 Subject: [PATCH 139/176] Minor pt_BR translation correction to sound more natural (#1018) ## What are the changes about? Changed two sentences to sound more natural to Brazilian Portuguese speakers. - Token to masculine adj. Token is often (if not always) described as masculine subject (e.g [Wikipedia](https://pt.wikipedia.org/wiki/Token_(an%C3%A1lise_l%C3%A9xica))) - Moved sentence to be written using "falhou" always in the end of a phrase to follow the same pattern as the other similar sentences. Also, using "falhou" at the beginning of the sentence is not wrong, but sounds a bit weird and it's not common in formal texts ## What existing issue does this resolve? Just translation related issues ## Pre-requisites - [x] I have tested & confirmed the changes work locally - [x] I have read and fully adhere to the rules set in the contributing guidelines found in `CONTRIBUTING.md` Reviewed-on: https://codeberg.org/fairyglade/ly/pulls/1018 Reviewed-by: AnErrupTion --- res/lang/pt_BR.ini | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/res/lang/pt_BR.ini b/res/lang/pt_BR.ini index 1ccc4c4..af8c970 100644 --- a/res/lang/pt_BR.ini +++ b/res/lang/pt_BR.ini @@ -33,7 +33,7 @@ err_pam_abort = transação pam abortada err_pam_acct_expired = conta expirada err_pam_auth = erro de autenticação err_pam_authinfo_unavail = não foi possível obter informações do usuário -err_pam_authok_reqd = token expirada +err_pam_authok_reqd = token expirado err_pam_buf = erro de buffer de memória err_pam_cred_err = erro para definir credenciais err_pam_cred_expired = credenciais expiradas @@ -53,7 +53,7 @@ err_sleep = não foi possível executar o comando de suspensão err_start = não foi possível executar o comando de início err_battery = não foi possível carregar o status da bateria err_switch_tty = não foi possível mudar de tty -err_tty_ctrl = falhou a transferência de controle do tty +err_tty_ctrl = a transferência de controle do tty falhou err_no_users = nenhum usuário encontrado err_uid_range = não foi possível obter dinamicamente o intervalo de uid err_user_gid = não foi possível definir o GID do usuário From aa00f7c8c7437a3bc2a9dc7d474985d28e68f509 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Mon, 15 Jun 2026 16:53:24 +0200 Subject: [PATCH 140/176] Replace ArrayListUnmanaged with ArrayList Signed-off-by: AnErrupTion --- ly-ui/src/components/Text.zig | 2 +- ly-ui/src/components/generic.zig | 2 +- src/components/UserList.zig | 2 +- src/main.zig | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/ly-ui/src/components/Text.zig b/ly-ui/src/components/Text.zig index 0ca5312..b4aad21 100644 --- a/ly-ui/src/components/Text.zig +++ b/ly-ui/src/components/Text.zig @@ -6,7 +6,7 @@ const TerminalBuffer = @import("../TerminalBuffer.zig"); const Position = @import("../Position.zig"); const Widget = @import("../Widget.zig"); -const DynamicString = std.ArrayListUnmanaged(u8); +const DynamicString = std.ArrayList(u8); const Text = @This(); diff --git a/ly-ui/src/components/generic.zig b/ly-ui/src/components/generic.zig index 572fe3f..2d0b3c4 100644 --- a/ly-ui/src/components/generic.zig +++ b/ly-ui/src/components/generic.zig @@ -8,7 +8,7 @@ const Position = @import("../Position.zig"); pub fn CyclableLabel(comptime ItemType: type, comptime ChangeItemType: type) type { return struct { const Allocator = std.mem.Allocator; - const ItemList = std.ArrayListUnmanaged(ItemType); + const ItemList = std.ArrayList(ItemType); const DrawItemFn = *const fn (*Self, ItemType, usize, usize, usize) void; const ChangeItemFn = *const fn (ItemType, ?ChangeItemType) void; diff --git a/src/components/UserList.zig b/src/components/UserList.zig index 9f01bc5..1be513c 100644 --- a/src/components/UserList.zig +++ b/src/components/UserList.zig @@ -10,7 +10,7 @@ const CyclableLabel = ly_ui.CyclableLabel; const Session = @import("Session.zig"); const SavedUsers = @import("../config/SavedUsers.zig"); -const StringList = std.ArrayListUnmanaged([]const u8); +const StringList = std.ArrayList([]const u8); pub const User = struct { name: []const u8, session_index: *usize, diff --git a/src/main.zig b/src/main.zig index 0f6f1e0..b739442 100644 --- a/src/main.zig +++ b/src/main.zig @@ -1,6 +1,6 @@ const std = @import("std"); const Allocator = std.mem.Allocator; -const StringList = std.ArrayListUnmanaged([]const u8); +const StringList = std.ArrayList([]const u8); const temporary_allocator = std.heap.page_allocator; const builtin = @import("builtin"); const build_options = @import("build_options"); From a801d1a5696a73a2535b26010162dd2cc2bd8638 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Wed, 17 Jun 2026 22:23:12 +0200 Subject: [PATCH 141/176] Clean up save code Signed-off-by: AnErrupTion --- src/main.zig | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/main.zig b/src/main.zig index b739442..887ec41 100644 --- a/src/main.zig +++ b/src/main.zig @@ -235,11 +235,7 @@ pub fn main(init: std.process.Init) !void { } // Load configuration file - var save_path_alloc = false; - - state.save_path = build_options.config_directory ++ "/ly/save.txt"; - state.old_save_path = build_options.config_directory ++ "/ly/save.ini"; - defer if (save_path_alloc) { + defer if (state.config.save) { state.allocator.free(state.save_path); state.allocator.free(state.old_save_path); }; @@ -284,7 +280,6 @@ pub fn main(init: std.process.Init) !void { if (state.config.save) { state.save_path = try std.Io.Dir.path.join(state.allocator, &[_][]const u8{ config_parent_path, "save.txt" }); state.old_save_path = try std.Io.Dir.path.join(state.allocator, &[_][]const u8{ config_parent_path, "save.ini" }); - save_path_alloc = true; } if (config_parser.maybe_load_error == null) { From e13856387464b01d411c1e72dd6e1c75a31a037f Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Thu, 18 Jun 2026 19:51:18 +0200 Subject: [PATCH 142/176] README: Specify Alpine for /etc/inittab Signed-off-by: AnErrupTion --- readme.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/readme.md b/readme.md index 4d11596..620929e 100644 --- a/readme.md +++ b/readme.md @@ -14,7 +14,7 @@ Join us on Matrix over at [#ly-dm:matrix.org](https://matrix.to/#/#ly-dm:matrix. ## Dependencies - Compile-time: - - zig 0.16.x (you must use a __release version__ of zig; check that `zig version` does not have a `-dev*` suffix) + - zig 0.16.x (you must use a **release version** of zig; check that `zig version` does not have a `-dev*` suffix) - libc @@ -151,7 +151,7 @@ On non-systemd systems, you can change the TTY Ly will run on by editing the cor ``` > [!NOTE] -> On Gentoo specifically, you also **must** comment out the appropriate line for the TTY in /etc/inittab. +> On Gentoo, Alpine and potentially others, you also **must** comment out the appropriate line for the TTY in /etc/inittab. ### runit From e833c4bc1d639979c7970918b6a426cd367a9ac2 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Thu, 18 Jun 2026 20:16:25 +0200 Subject: [PATCH 143/176] auth: Create XDG_RUNTIME_DIR if /run/user/UID exists Signed-off-by: AnErrupTion --- src/auth.zig | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/src/auth.zig b/src/auth.zig index 6a97fff..7a3c033 100644 --- a/src/auth.zig +++ b/src/auth.zig @@ -179,7 +179,7 @@ fn startSession( // Reset the XDG environment variables try log_file.info(io, "auth/env", "resetting xdg environment variables", .{}); try setXdgEnv(allocator, tty_str, current_environment); - try setXdgRuntimeDir(allocator); + try setXdgRuntimeDir(allocator, io); // Set the PAM variables const pam_env_vars: ?[*:null]?[*:0]u8 = interop.pam.pam_getenvlist(handle); @@ -247,18 +247,20 @@ fn setXdgEnv(allocator: std.mem.Allocator, tty_str: []u8, environment: Environme try interop.setEnvironmentVariable(allocator, "XDG_VTNR", tty_str, false); } -fn setXdgRuntimeDir(allocator: std.mem.Allocator) !void { - // The "/run/user/%d" directory is not available on FreeBSD. It is much - // better to stick to the defaults and let applications using - // XDG_RUNTIME_DIR to fall back to directories inside user's home - // directory. - if (builtin.os.tag != .freebsd) { - const uid = std.posix.system.getuid(); - var uid_buffer: [32]u8 = undefined; // No UID can be larger than this - const uid_str = try std.fmt.bufPrint(&uid_buffer, "/run/user/{d}", .{uid}); +fn setXdgRuntimeDir(allocator: std.mem.Allocator, io: std.Io) !void { + // The "/run/user/%d" directory is not available on some operating systems, + // like FreeBSD and Alpine + const uid = std.posix.system.getuid(); + var uid_buffer: [32]u8 = undefined; // No UID can be larger than this + const uid_str = try std.fmt.bufPrint(&uid_buffer, "/run/user/{d}", .{uid}); - try interop.setEnvironmentVariable(allocator, "XDG_RUNTIME_DIR", uid_str, false); - } + var xdg_dir = std.Io.Dir.openDirAbsolute(io, uid_str, .{}) catch |err| { + if (err == error.FileNotFound) return; + return err; + }; + xdg_dir.close(io); + + try interop.setEnvironmentVariable(allocator, "XDG_RUNTIME_DIR", uid_str, false); } fn loginConv( @@ -390,7 +392,7 @@ fn createXauthFile(log_file: *LogFile, io: std.Io, pwd: []const u8, buffer: []u8 const xauthority: []u8 = try std.fmt.bufPrint(buffer, "{s}/{s}", .{ trimmed_xauth_dir, xauth_file }); - std.Io.Dir.cwd().createDirPath(io, trimmed_xauth_dir) catch {}; + try std.Io.Dir.cwd().createDirPath(io, trimmed_xauth_dir); try log_file.info(io, "auth/x11", "creating xauth file: {s}", .{xauthority}); From eeccb7421b5b23eda5a5a1bc03bbebcf65ae003f Mon Sep 17 00:00:00 2001 From: RadsammyT Date: Fri, 19 Jun 2026 20:05:00 +0200 Subject: [PATCH 144/176] feat: LuaJIT Animations (#1001) ## What are the changes about? TL;DR: ![ly_meme](/attachments/f4c3a93b-6ede-42ab-a351-292e2105d4e2) Slaps the entire LuaJIT runtime onto Ly, allowing for the creation of custom dynamic animations like GameOfLife, ColorWave, Doom, etc. This PR adds the [ziglua](https://github.com/natecraddock/ziglua?ref=zig-0.16) dependency for its zig bindings and considerable buildtime config (mainly lua version selection). ### Example ```lua ly.frame_delay = 5 local timer = 0 local clock = os.clock() local clock_diff = 0 function draw() timer = timer + 1 byte = string.byte(' ') clock_diff = os.clock() - clock clock = os.clock() timer = timer + clock_diff for x = 0, ly.width-1 do for y = 0, ly.height-1 do local xc = 0xFF if x < 255 then xc = ((x + math.floor(timer / 2)) * 3) % 255 else xc = 0 end local yc = 0xFF if y < 255 then yc = ((y) * 3) % 255 else yc = 0 end ly.putCell(byte, xc, bit.bor(xc, yc), x, y) end end end ``` ### The API The API that Ly gives to the user is minimal. A table is globally available, named `ly`, which provides the following: | Member | Purpose | |---------|---------| | `ly.width` & `ly.height` | Respective Width/Height from the `TerminalBuffer` | | `ly.putCell(byte, fg, bg, x, y)` | Literally `Cell.init(byte, fg, bg).put(x, y)`.| | `ly.clock()` | The current real-time, in microseconds. | ### Error Handling On a Lua Error, Ly won't quit but will instead paint the entire background red. The lua error in question can be found in the Ly log file and on-screen. ```log 2026-05-19 16:13:40 [err/Lua] Error (Cannot call draw()): attempt to call a nil value 2026-05-19 11:05:51 [err/Lua] Lua Error: ...dsammyt/programming/probe/ly/scratch/testConfig/test.lua:30: bad argument #1 to 'ipairs' (table expected, got number) ``` ## Pre-requisites - [X] I have tested & confirmed the changes work locally - [X] I have run `zig fmt` throughout my changes Reviewed-on: https://codeberg.org/fairyglade/ly/pulls/1001 Reviewed-by: AnErrupTion --- build.zig | 11 +- build.zig.zon | 4 + res/config.ini | 4 + res/example.lua | 119 ++++++++++++++++ src/animations/Lua.zig | 302 +++++++++++++++++++++++++++++++++++++++++ src/config/Config.zig | 1 + src/enums.zig | 1 + src/main.zig | 18 +++ 8 files changed, 459 insertions(+), 1 deletion(-) create mode 100644 res/example.lua create mode 100644 src/animations/Lua.zig diff --git a/build.zig b/build.zig index 2de19f7..a3a5f22 100644 --- a/build.zig +++ b/build.zig @@ -71,12 +71,19 @@ pub fn build(b: *std.Build) !void { }), }); + const zlua = b.dependency("zlua", .{ + .target = target, + .optimize = optimize, + .lang = .luajit, + }); + exe.root_module.addImport("zlua", zlua.module("zlua")); + const ly_ui = b.dependency("ly_ui", .{ .target = target, .optimize = optimize, .enable_x11_support = enable_x11_support, .fallback_uid_min = fallback_uid_min, - .fallback_uid_max = fallback_uid_max + .fallback_uid_max = fallback_uid_max, }); exe.root_module.addImport("ly-ui", ly_ui.module("ly-ui")); @@ -189,6 +196,8 @@ fn install_ly(allocator: std.mem.Allocator, io: std.Io, patch_map: PatchMap, ins try installText(io, patched_setup, config_dir, ly_config_directory, "setup.sh", .{ .permissions = .fromMode(0o755) }); try installFile(io, "res/example.dur", config_dir, ly_config_directory, "example.dur", .{ .permissions = .fromMode(0o755) }); + + try installFile(io, "res/example.lua", config_dir, ly_config_directory, "example.lua", .{ .permissions = .fromMode(0o755) }); } { diff --git a/build.zig.zon b/build.zig.zon index 3c091cb..1d19b69 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -11,6 +11,10 @@ .url = "git+https://github.com/Hejsil/zig-clap#fc1e5cc3f6d9d3001112385ee6256d694e959d2f", .hash = "clap-0.11.0-oBajB7foAQC3Iyn4IVCkUdYaOVVng5IZkSncySTjNig1", }, + .zlua = .{ + .url = "git+https://github.com/natecraddock/ziglua?ref=zig-0.16#8f271c82baa5fc43aa02a72f6da020c2025d9436", + .hash = "zlua-0.1.0-hGRpC2aABQD4D9PBVH3wAW8k32-I4969MRQ0CpOwoley", + }, }, .paths = .{ "build.zig", diff --git a/res/config.ini b/res/config.ini index a290773..169f5ea 100644 --- a/res/config.ini +++ b/res/config.ini @@ -25,6 +25,7 @@ allow_empty_password = true # colormix -> Color mixing shader # gameoflife -> John Conway's Game of Life # dur_file -> .dur file format (https://github.com/cmang/durdraw/tree/master) +# lua -> user-made animation written in LuaJIT animation = none # Delay between each animation frame in milliseconds @@ -298,6 +299,9 @@ login_defs_path = /etc/login.defs # no need to add `exec "$@"` at the end logout_cmd = null +# The file pointing to the Lua file to be used when using the Lua animation option +lua_animation_file = $CONFIG_DIRECTORY/ly/example.lua + # General log file path # If null, syslog will be used instead ly_log = /var/log/ly.log diff --git a/res/example.lua b/res/example.lua new file mode 100644 index 0000000..7309da6 --- /dev/null +++ b/res/example.lua @@ -0,0 +1,119 @@ +-- [[ +-- This is an example of using LuaJIT to create a custom animation in Ly, in this case +-- bouncing squares that change colors. +-- +-- You are given the following `ly` table: +-- { +-- height: number -- The height of the terminal +-- width: number -- The width of the terminal +-- putCell(byte, fg, bg, x, y) -- Draw a cell. +-- All arguments to this function are integers, and +-- must be in the unsigned 32-bit integer range: 0 to 2^32-1. +-- If an argument cannot be converted to this range, it will throw +-- an error. +-- +-- For reference, the XY coordinates (0,0) draw a cell on the top-left +-- of the terminal, where the positive-X axis moves right and the +-- positive-Y axis moves down. +-- +-- For arguments fg and bg: they are colors in the format +-- 0xSSRRGGBB, where SS is for styling. See your +-- config.ini for more details. +-- +-- For the byte argument, you may use string.byte to fill this argument. +-- +-- putCell(byte, fg, bg, x, y, w, h) -- Draw a rectangle. +-- Arguments are the same as putCell except for w and h, which are also +-- unsigned integers. The rectangle will be drawn from the top-left, with +-- argument w extending it to the right and argument h extending downwards. +-- +-- +-- putLabel(str, fg, bg, x, y) -- Draw text in argument str. See putCell() +-- for info on the rest of the arguments. +-- +-- clock() -- The time, in microseconds. +-- } +-- +-- A function named `draw()` must be declared in the script. This is ran every +-- frame. +-- +-- In addition to the base library, you are also given the following standard +-- libraries: +-- bit (A library exclusive to LuaJIT, see https://bitop.luajit.org/api.html) +-- math +-- string +-- table +-- +-- The std libraries io and debug are NOT included. +-- +-- ]] + +-- You should probably copy FPS and FPS_COUNT into any future LuaJIT animations +-- you create. +local FPS_COUNT = 40 +local function FPS() + return (1/FPS_COUNT)*1000000 +end + + +local SQUARE_WIDTH = 10 +local SQUARE_HEIGHT = 5 + +local SQUARE_COUNT = 25 + +local squares = {} + +for i = 1, SQUARE_COUNT do + local vx = 1 + local vy = 1 + if math.random(1, 2) == 2 then vx = -vx end + if math.random(1, 2) == 2 then vy = -vy end + squares[#squares+1] = { + x = math.random(1, ly.width - SQUARE_WIDTH), + y = math.random(1, ly.height - SQUARE_HEIGHT), + vx = vx, + vy = vy, + color = math.random(0xFFFFFF) + } +end + +local timer = ly.clock() +local perf = ly.clock() + +function draw() + -- Rather than progressing the animation by frame, do it based on + -- seconds, via ly.clock(). In this timeframe, you can update the animation + -- state. + -- DO NOT DRAW CELLS IN THIS TIMEFRAME. You will get flickering. + + -- if this check passes, we can update the animation + if timer + FPS() < ly.clock() then + for i, v in ipairs(squares) do + v.x = v.x + v.vx + v.y = v.y + v.vy + if v.x == 0 then + v.vx = 1; v.color = math.random(0xFFFFFF) + end + if v.x + SQUARE_WIDTH >= ly.width-1 then + v.vx = -1; v.color = math.random(0xFFFFFF) + end + if v.y == 0 then + v.vy = 1; v.color = math.random(0xFFFFFF) + end + if v.y + SQUARE_HEIGHT >= ly.height-1 then + v.vy = -1; v.color = math.random(0xFFFFFF) + end + end + timer = ly.clock() + end + + + for i, v in ipairs(squares) do + ly.putRect(string.byte(' '), 0, v.color, v.x, v.y, SQUARE_WIDTH, SQUARE_HEIGHT) + end + + local new_perf = ly.clock() + local str = "FT: "..((new_perf - perf) / 1000).."ms" + ly.putLabel(str , 0x00FFFFFF, 0, (ly.width/2) - (string.len(str)/2), ly.height-1) + perf = new_perf +end diff --git a/src/animations/Lua.zig b/src/animations/Lua.zig new file mode 100644 index 0000000..d212e03 --- /dev/null +++ b/src/animations/Lua.zig @@ -0,0 +1,302 @@ +const std = @import("std"); +const ly_ui = @import("ly-ui"); +const LogFile = ly_ui.ly_core.LogFile; +const Widget = ly_ui.Widget; +const TerminalBuffer = ly_ui.TerminalBuffer; +const Cell = ly_ui.Cell; +const Allocator = std.mem.Allocator; +const InfoLine = @import("../components/InfoLine.zig"); +const Lang = @import("../config/Lang.zig"); + +const zlua = @import("zlua"); + +const ly_lua = @embedFile("ly.lua"); + +const Lua = @This(); + +allocator: Allocator, +instance: ?Widget = null, +lua: *zlua.Lua, +log: *LogFile, +terminal_buffer: *TerminalBuffer, +width: usize, +height: usize, +margin: usize, +io: std.Io, +animation_delay: u16, + +info_line: *InfoLine, +fg: u32, +bg: u32, + +lang: Lang, +full_color: bool, + +lua_error: bool = false, +lua_error_logged: bool = false, +lua_str: ?[:0]const u8 = null, + +pub fn init( + io: std.Io, + alloc: Allocator, + log: *LogFile, + buf: *TerminalBuffer, + file: []const u8, + margin: u8, + animation_delay: u16, + info_line: *InfoLine, + fg: u32, + bg: u32, + lang: Lang, + full_color: bool, +) !Lua { + var self: Lua = .{ + .lua = try zlua.Lua.init(alloc), + .allocator = alloc, + .terminal_buffer = buf, + .instance = null, + .log = log, + .width = 0, + .height = 0, + .margin = margin, + .io = io, + .animation_delay = animation_delay, + .info_line = info_line, + .fg = fg, + .bg = bg, + .lang = lang, + .full_color = full_color, + }; + + // exclude IO and debug libraries + self.lua.openBase(); + self.lua.openBit(); + self.lua.openMath(); + self.lua.openString(); + self.lua.openTable(); + + file_loading: { + const zf = std.mem.concatWithSentinel(alloc, u8, &[1][]const u8{file}, 0) catch |e| { + try self.log.err(self.io, "lua", "failed to allocate file path: {}", .{e}); + self.lua_str = "failed to allocate file path!"; + self.info_line.addMessage(lang.err_alloc, self.bg, self.fg) catch {}; + return e; + }; + defer alloc.free(zf); + + // create the ly table + self.lua.newTable(); + self.lua.setGlobal("ly"); + + // create ly.width and ly.height from TerminalBuffer width/height + self.propagateTerminalBounds(); + + _ = self.lua.getGlobal("ly"); + _ = self.lua.pushString("clock"); + self.lua.pushFunction(luaLyClock); + self.lua.setTable(-3); + _ = self.lua.pushString("putCell"); + self.lua.pushFunction(luaPutCell); + self.lua.setTable(-3); + _ = self.lua.pushString("putLabel"); + self.lua.pushFunction(luaPutLabel); + self.lua.setTable(-3); + _ = self.lua.pushString("putRect"); + self.lua.pushFunction(luaPutRect); + self.lua.setTable(-3); + self.lua.setGlobal("ly"); + + self.lua.doFile(zf) catch { + const errorStr = self.lua.toString(-1) catch unreachable; + self.lua_str = try self.allocator.dupeSentinel(u8, errorStr, 0); + try self.log.err(self.io, "lua", "lua error: {s}", .{errorStr}); + self.lua_error = true; + break :file_loading; + }; + } + + return self; +} + +fn draw(self: *Lua) void { + self.propagateTerminalBounds(); + if (self.lua_error) { + // Ly's Red Screen of Omega-Death:tm: + const RED: u32 = if (self.full_color) TerminalBuffer.Color.TRUE_RED else TerminalBuffer.Color.ECOL_RED; + const cell = Cell.init(0x2588, RED, RED); + for (0..self.terminal_buffer.height) |y| + for (0..self.terminal_buffer.width) |x| + cell.put(x, y) catch {}; + if (self.lua_str) |str| + for (str, 0..) |c, i| { + Cell.init(c, 0x00FFFFFF, 0).put( + @divFloor(self.width, 2) - @divFloor(str.len, 2) + i, + self.margin + 5, + ) catch {}; + }; + if (!self.lua_error_logged) { + self.info_line.addMessage("lua animation failed", self.bg, self.fg) catch {}; + self.lua_error_logged = true; + } + return; + } + + _ = self.lua.getGlobal("draw"); + self.lua.protectedCall(.{}) catch { + const errorStr = self.lua.toString(-1) catch unreachable; + self.lua_str = std.mem.concatWithSentinel( + self.allocator, + u8, + &.{ "cannot call draw(): ", errorStr }, + 0, + ) catch unreachable; + self.log.err(self.io, "lua", "error (cannot call draw()): {s}", .{errorStr}) catch unreachable; + self.lua_error = true; + }; +} + +fn calculateTimeout(self: *Lua, _: *anyopaque) !?usize { + return self.animation_delay; +} + +fn deinit(self: *Lua) void { + if (self.lua_str) |str| self.allocator.free(str); + self.lua.deinit(); +} + +pub fn widget(self: *Lua) *Widget { + if (self.instance) |*inst| return inst; + self.instance = Widget.init( + "Lua", + null, + self, + deinit, + null, + draw, + null, + null, + calculateTimeout, + ); + return &self.instance.?; +} + +fn propagateTerminalBounds(self: *Lua) void { + if (self.terminal_buffer.height == self.height and + self.terminal_buffer.width == self.width) + return; + self.width = self.terminal_buffer.width; + self.height = self.terminal_buffer.height; + _ = self.lua.getGlobal("ly"); + _ = self.lua.pushString("width"); + self.lua.pushInteger(@intCast(self.terminal_buffer.width)); + self.lua.setTable(-3); + _ = self.lua.pushString("height"); + self.lua.pushInteger(@intCast(self.terminal_buffer.height)); + self.lua.setTable(-3); + self.lua.setGlobal("ly"); +} + +fn luaLyClock(state: ?*zlua.LuaState) callconv(.c) c_int { + var threaded = std.Io.Threaded.init_single_threaded; + const lua: *zlua.Lua = @ptrCast(@alignCast(state orelse unreachable)); + lua.pushInteger(std.Io.Timestamp.now(threaded.io(), .real).toMicroseconds()); + return 1; +} + +fn luaPutCell(state: ?*zlua.LuaState) callconv(.c) c_int { + const lua: *zlua.Lua = @ptrCast(@alignCast(state orelse unreachable)); + const MSG = "ly.putCell: cannot convert %s-typed "; + const byte = lua.toNumeric(u32, 1) catch { + const t = lua.typeName(lua.typeOf(1)); + lua.raiseErrorStr(MSG ++ "byte to u32", .{t.ptr}); + }; + const fg = lua.toNumeric(u32, 2) catch { + const t = lua.typeName(lua.typeOf(2)); + lua.raiseErrorStr(MSG ++ "fg to u32", .{t.ptr}); + }; + const bg = lua.toNumeric(u32, 3) catch { + const t = lua.typeName(lua.typeOf(3)); + lua.raiseErrorStr(MSG ++ "bg to u32", .{t.ptr}); + }; + const x = lua.toNumeric(usize, 4) catch { + const t = lua.typeName(lua.typeOf(4)); + lua.raiseErrorStr(MSG ++ "x to usize", .{t.ptr}); + }; + const y = lua.toNumeric(usize, 5) catch { + const t = lua.typeName(lua.typeOf(5)); + lua.raiseErrorStr(MSG ++ "y to usize", .{t.ptr}); + }; + TerminalBuffer.setCell(x, y, .{ + .fg = fg, + .bg = bg, + .ch = byte, + }) catch {}; + return 0; +} + +fn luaPutRect(state: ?*zlua.LuaState) callconv(.c) c_int { + const lua: *zlua.Lua = @ptrCast(@alignCast(state orelse unreachable)); + const MSG = "ly.putRect: cannot convert %s-typed "; + const byte = lua.toNumeric(u32, 1) catch { + const t = lua.typeName(lua.typeOf(1)); + lua.raiseErrorStr(MSG ++ "byte to u32", .{t.ptr}); + }; + const fg = lua.toNumeric(u32, 2) catch { + const t = lua.typeName(lua.typeOf(2)); + lua.raiseErrorStr(MSG ++ "fg to u32", .{t.ptr}); + }; + const bg = lua.toNumeric(u32, 3) catch { + const t = lua.typeName(lua.typeOf(3)); + lua.raiseErrorStr(MSG ++ "bg to u32", .{t.ptr}); + }; + const x = lua.toNumeric(usize, 4) catch { + const t = lua.typeName(lua.typeOf(4)); + lua.raiseErrorStr(MSG ++ "x to usize", .{t.ptr}); + }; + const y = lua.toNumeric(usize, 5) catch { + const t = lua.typeName(lua.typeOf(5)); + lua.raiseErrorStr(MSG ++ "y to usize", .{t.ptr}); + }; + const w = lua.toNumeric(usize, 6) catch { + const t = lua.typeName(lua.typeOf(5)); + lua.raiseErrorStr(MSG ++ "w to usize", .{t.ptr}); + }; + const h = lua.toNumeric(usize, 7) catch { + const t = lua.typeName(lua.typeOf(5)); + lua.raiseErrorStr(MSG ++ "h to usize", .{t.ptr}); + }; + for (0..w) |wx| for (0..h) |hy| + TerminalBuffer.setCell(x + wx, y + hy, .{ + .fg = fg, + .bg = bg, + .ch = byte, + }) catch {}; + return 0; +} + +fn luaPutLabel(state: ?*zlua.LuaState) callconv(.c) c_int { + const lua: *zlua.Lua = @ptrCast(@alignCast(state orelse unreachable)); + const MSG = "ly.putLabel: cannot convert %s-typed "; + const str = lua.toString(1) catch { + const t = lua.typeName(lua.typeOf(2)); + lua.raiseErrorStr(MSG ++ "str to string", .{t.ptr}); + }; + const fg = lua.toNumeric(u32, 2) catch { + const t = lua.typeName(lua.typeOf(2)); + lua.raiseErrorStr(MSG ++ "fg to u32", .{t.ptr}); + }; + const bg = lua.toNumeric(u32, 3) catch { + const t = lua.typeName(lua.typeOf(3)); + lua.raiseErrorStr(MSG ++ "bg to u32", .{t.ptr}); + }; + const x = lua.toNumeric(usize, 4) catch { + const t = lua.typeName(lua.typeOf(4)); + lua.raiseErrorStr(MSG ++ "x to usize", .{t.ptr}); + }; + const y = lua.toNumeric(usize, 5) catch { + const t = lua.typeName(lua.typeOf(5)); + lua.raiseErrorStr(MSG ++ "y to usize", .{t.ptr}); + }; + TerminalBuffer.drawText(str, x, y, fg, bg) catch {}; + return 0; +} diff --git a/src/config/Config.zig b/src/config/Config.zig index a592faa..b7b258b 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -74,6 +74,7 @@ lang: []const u8 = "en", login_cmd: ?[]const u8 = null, login_defs_path: []const u8 = "/etc/login.defs", logout_cmd: ?[]const u8 = null, +lua_animation_file: []const u8 = build_options.config_directory ++ "/ly/example.lua", ly_log: ?[]const u8 = "/var/log/ly.log", margin_box_h: u8 = 2, margin_box_v: u8 = 1, diff --git a/src/enums.zig b/src/enums.zig index 47771da..ff69f84 100644 --- a/src/enums.zig +++ b/src/enums.zig @@ -7,6 +7,7 @@ pub const Animation = enum { colormix, gameoflife, dur_file, + lua, }; pub const DisplayServer = enum { diff --git a/src/main.zig b/src/main.zig index 887ec41..41ba15d 100644 --- a/src/main.zig +++ b/src/main.zig @@ -29,6 +29,7 @@ const Doom = @import("animations/Doom.zig"); const DurFile = @import("animations/DurFile.zig"); const GameOfLife = @import("animations/GameOfLife.zig"); const Matrix = @import("animations/Matrix.zig"); +const Lua = @import("animations/Lua.zig"); const auth = @import("auth.zig"); const InfoLine = @import("components/InfoLine.zig"); const Session = @import("components/Session.zig"); @@ -1093,6 +1094,23 @@ pub fn main(init: std.process.Init) !void { ); animation = dur.widget(); }, + .lua => { + var lua = try Lua.init( + state.io, + state.allocator, + &state.log_file, + &state.buffer, + state.config.lua_animation_file, + state.config.edge_margin, + state.config.animation_frame_delay, + &state.info_line, + state.config.error_fg, + state.config.error_bg, + state.lang, + state.config.full_color, + ); + animation = lua.widget(); + }, } defer if (animation) |a| a.deinit(); From 4d882f7997102b9b3f34154367587a04faad1e11 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Fri, 19 Jun 2026 20:18:45 +0200 Subject: [PATCH 145/176] Fix example.lua doc + add ly-community in README Signed-off-by: AnErrupTion --- readme.md | 4 +++ res/example.lua | 96 ++++++++++++++++++++++++------------------------- 2 files changed, 52 insertions(+), 48 deletions(-) diff --git a/readme.md b/readme.md index 620929e..d5ea1fc 100644 --- a/readme.md +++ b/readme.md @@ -56,6 +56,10 @@ Join us on Matrix over at [#ly-dm:matrix.org](https://matrix.to/#/#ly-dm:matrix. [![Packaging status](https://repology.org/badge/vertical-allrepos/ly-display-manager.svg?exclude_unsupported=1)](https://repology.org/project/ly-display-manager/versions) +## Custom animations & user scripts + +If you want to pimp your Ly installation even further than what the traditional configuration file allows you to, a [community repository](https://codeberg.org/fairyglade/ly-community) exists containing custom animations & scripts, so go check it out! + ## Support Every environment that works on other login managers also should work on Ly. diff --git a/res/example.lua b/res/example.lua index 7309da6..81f9bc9 100644 --- a/res/example.lua +++ b/res/example.lua @@ -6,13 +6,13 @@ -- { -- height: number -- The height of the terminal -- width: number -- The width of the terminal --- putCell(byte, fg, bg, x, y) -- Draw a cell. --- All arguments to this function are integers, and +-- putCell(byte, fg, bg, x, y) -- Draw a cell. +-- All arguments to this function are integers, and -- must be in the unsigned 32-bit integer range: 0 to 2^32-1. -- If an argument cannot be converted to this range, it will throw -- an error. -- --- For reference, the XY coordinates (0,0) draw a cell on the top-left +-- For reference, the XY coordinates (0,0) draw a cell on the top-left -- of the terminal, where the positive-X axis moves right and the -- positive-Y axis moves down. -- @@ -22,7 +22,7 @@ -- -- For the byte argument, you may use string.byte to fill this argument. -- --- putCell(byte, fg, bg, x, y, w, h) -- Draw a rectangle. +-- putRect(byte, fg, bg, x, y, w, h) -- Draw a rectangle. -- Arguments are the same as putCell except for w and h, which are also -- unsigned integers. The rectangle will be drawn from the top-left, with -- argument w extending it to the right and argument h extending downwards. @@ -45,14 +45,14 @@ -- table -- -- The std libraries io and debug are NOT included. --- +-- -- ]] -- You should probably copy FPS and FPS_COUNT into any future LuaJIT animations -- you create. local FPS_COUNT = 40 local function FPS() - return (1/FPS_COUNT)*1000000 + return (1 / FPS_COUNT) * 1000000 end @@ -64,56 +64,56 @@ local SQUARE_COUNT = 25 local squares = {} for i = 1, SQUARE_COUNT do - local vx = 1 - local vy = 1 - if math.random(1, 2) == 2 then vx = -vx end - if math.random(1, 2) == 2 then vy = -vy end - squares[#squares+1] = { - x = math.random(1, ly.width - SQUARE_WIDTH), - y = math.random(1, ly.height - SQUARE_HEIGHT), - vx = vx, - vy = vy, - color = math.random(0xFFFFFF) - } + local vx = 1 + local vy = 1 + if math.random(1, 2) == 2 then vx = -vx end + if math.random(1, 2) == 2 then vy = -vy end + squares[#squares + 1] = { + x = math.random(1, ly.width - SQUARE_WIDTH), + y = math.random(1, ly.height - SQUARE_HEIGHT), + vx = vx, + vy = vy, + color = math.random(0xFFFFFF) + } end local timer = ly.clock() local perf = ly.clock() function draw() - -- Rather than progressing the animation by frame, do it based on - -- seconds, via ly.clock(). In this timeframe, you can update the animation - -- state. - -- DO NOT DRAW CELLS IN THIS TIMEFRAME. You will get flickering. + -- Rather than progressing the animation by frame, do it based on + -- seconds, via ly.clock(). In this timeframe, you can update the animation + -- state. + -- DO NOT DRAW CELLS IN THIS TIMEFRAME. You will get flickering. - -- if this check passes, we can update the animation - if timer + FPS() < ly.clock() then - for i, v in ipairs(squares) do - v.x = v.x + v.vx - v.y = v.y + v.vy - if v.x == 0 then - v.vx = 1; v.color = math.random(0xFFFFFF) - end - if v.x + SQUARE_WIDTH >= ly.width-1 then - v.vx = -1; v.color = math.random(0xFFFFFF) - end - if v.y == 0 then - v.vy = 1; v.color = math.random(0xFFFFFF) - end - if v.y + SQUARE_HEIGHT >= ly.height-1 then - v.vy = -1; v.color = math.random(0xFFFFFF) - end - end - timer = ly.clock() - end + -- if this check passes, we can update the animation + if timer + FPS() < ly.clock() then + for i, v in ipairs(squares) do + v.x = v.x + v.vx + v.y = v.y + v.vy + if v.x == 0 then + v.vx = 1; v.color = math.random(0xFFFFFF) + end + if v.x + SQUARE_WIDTH >= ly.width - 1 then + v.vx = -1; v.color = math.random(0xFFFFFF) + end + if v.y == 0 then + v.vy = 1; v.color = math.random(0xFFFFFF) + end + if v.y + SQUARE_HEIGHT >= ly.height - 1 then + v.vy = -1; v.color = math.random(0xFFFFFF) + end + end + timer = ly.clock() + end - for i, v in ipairs(squares) do - ly.putRect(string.byte(' '), 0, v.color, v.x, v.y, SQUARE_WIDTH, SQUARE_HEIGHT) - end + for i, v in ipairs(squares) do + ly.putRect(string.byte(' '), 0, v.color, v.x, v.y, SQUARE_WIDTH, SQUARE_HEIGHT) + end - local new_perf = ly.clock() - local str = "FT: "..((new_perf - perf) / 1000).."ms" - ly.putLabel(str , 0x00FFFFFF, 0, (ly.width/2) - (string.len(str)/2), ly.height-1) - perf = new_perf + local new_perf = ly.clock() + local str = "FT: " .. ((new_perf - perf) / 1000) .. "ms" + ly.putLabel(str, 0x00FFFFFF, 0, (ly.width / 2) - (string.len(str) / 2), ly.height - 1) + perf = new_perf end From 8f4ca8d3f0b4cfbc6e6aa063ece1a4f0c4c6f218 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Fri, 3 Jul 2026 18:33:58 +0200 Subject: [PATCH 146/176] Autologin: Don't draw info line (closes #1002) Signed-off-by: AnErrupTion --- src/main.zig | 48 ++++++++++++++++++++++++++---------------------- 1 file changed, 26 insertions(+), 22 deletions(-) diff --git a/src/main.zig b/src/main.zig index 41ba15d..0e57d41 100644 --- a/src/main.zig +++ b/src/main.zig @@ -1483,6 +1483,32 @@ fn authenticate(ptr: *anyopaque) !bool { state.config.error_bg, state.config.error_fg, ); + if (!state.is_autologin) { + 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( + state.io, + "tui", + "failed to clear info line: {s}", + .{@errorName(err)}, + ); + }; + state.info_line.label.draw(); + try TerminalBuffer.presentBuffer(); + } + return false; + } + + try state.info_line.addMessage( + state.lang.authenticating, + state.config.bg, + state.config.fg, + ); + if (!state.is_autologin) { state.info_line.clearRendered(state.allocator) catch |err| { try state.info_line.addMessage( state.lang.err_alloc, @@ -1498,30 +1524,8 @@ fn authenticate(ptr: *anyopaque) !bool { }; state.info_line.label.draw(); try TerminalBuffer.presentBuffer(); - 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( - state.io, - "tui", - "failed to clear info line: {s}", - .{@errorName(err)}, - ); - }; - state.info_line.label.draw(); - try TerminalBuffer.presentBuffer(); - 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, From 63f0f4931c31d3e7ed4c381a5201c313d568e634 Mon Sep 17 00:00:00 2001 From: Tim132 Date: Fri, 3 Jul 2026 18:37:40 +0200 Subject: [PATCH 147/176] Fix authentication error with ly-freebsd-autologin (#1020) ## What are the changes about? Remove the line "auth include login" from ly-freebsd-autologin ## What existing issue does this resolve? The autologin at the beginning and further logins with password always failed with this message in the log: "failed to authenticate: PamAuthError". When I removed the line "auth include login" from ly-freebsd-autologin, autologin and login worked great. ## Pre-requisites - [X] I have tested & confirmed the changes work locally - [X] I have read and fully adhere to the rules set in the contributing guidelines found in `CONTRIBUTING.md` Reviewed-on: https://codeberg.org/fairyglade/ly/pulls/1020 Reviewed-by: AnErrupTion --- res/pam.d/ly-freebsd-autologin | 1 - 1 file changed, 1 deletion(-) diff --git a/res/pam.d/ly-freebsd-autologin b/res/pam.d/ly-freebsd-autologin index e2448ad..b6f56e3 100644 --- a/res/pam.d/ly-freebsd-autologin +++ b/res/pam.d/ly-freebsd-autologin @@ -3,7 +3,6 @@ # OpenPAM (used in FreeBSD) doesn't support prepending "-" for ignoring missing # modules. auth required pam_permit.so -auth include login account include login password include login session include login From 1d272ff74fd2f34fee61eee61a4c781e4b1622e8 Mon Sep 17 00:00:00 2001 From: dumbsplash Date: Sun, 5 Jul 2026 20:59:40 +0200 Subject: [PATCH 148/176] Updated readme to address SELinux problems (closes #494) (#1019) PR is an edit of the repo readme. See commit message. Co-authored-by: AnErrupTion Reviewed-on: https://codeberg.org/fairyglade/ly/pulls/1019 Reviewed-by: AnErrupTion --- readme.md | 31 ++++++++++++++++++++++++++++--- 1 file changed, 28 insertions(+), 3 deletions(-) diff --git a/readme.md b/readme.md index d5ea1fc..33ef70b 100644 --- a/readme.md +++ b/readme.md @@ -39,13 +39,38 @@ Join us on Matrix over at [#ly-dm:matrix.org](https://matrix.to/#/#ly-dm:matrix. ### Fedora -> [!WARNING] -> You may encounter issues with SELinux on Fedora. It is recommended to add a rule for Ly as it currently does not ship one. - ``` # dnf install kernel-devel pam-devel libxcb-devel zig xorg-x11-xauth xorg-x11-server brightnessctl ``` +> [!WARNING] +> Distributions using SELinux such as Fedora and openSUSE Tumbleweed may encounter issues. If you encounter such issues, such as session launch failures, proceed with the following steps: +> +> ``` +> # ausearch -m avc -ts recent +> ``` +> +> If SELinux is denying process context transition, you will see this output: +> +> ``` +> denied { transition } for pid=XXXX comm="ly" path="/usr/bin/bash" +> scontext=system_u:system_r:unconfined_service_t:s0 +> tcontext=unconfined_u:unconfined_r:unconfined_t:s0 +> tclass=process permissive=0 +> ``` +> +> Pipe this output into `audit2allow` to generate a security module package. This will persist regardless of changes to filesystem permissions: +> +> ``` +> # ausearch -m avc -ts recent | audit2allow -M ly-local +> ``` +> +> ``` +> # semodule -i ly-local.pp +> ``` +> +> *This fix has been confirmed on Fedora 39 and 44 and openSUSE Tumbleweed. (#494)* + ### FreeBSD ``` From cf731dbce13bd5c07c58086b9cbcec14acf4af48 Mon Sep 17 00:00:00 2001 From: Lars Engels Date: Sun, 5 Jul 2026 21:19:30 +0200 Subject: [PATCH 149/176] Add battery capacity support on FreeBSD (#988) ## What are the changes about? So far ly only supported showing battery capacity on Linux. This adds support for FreeBSD as well using `sysctlbyname()` instead of reading from `/sys/...` ## What existing issue does this resolve? No battery shown on FreeBSD. _Replace this with a reference to an existing issue, or N/A if there is none_ ## Pre-requisites - [x] I have tested & confirmed the changes work locally - [x] I have run `zig fmt` throughout my changes Co-authored-by: AnErrupTion Co-authored-by: AnErrupTion Reviewed-on: https://codeberg.org/fairyglade/ly/pulls/988 Reviewed-by: AnErrupTion --- ly-core/build.zig | 1 + ly-core/src/interop.zig | 3 +++ res/config.ini | 1 + src/main.zig | 33 ++++++++++++++++++++++++--------- 4 files changed, 29 insertions(+), 9 deletions(-) diff --git a/ly-core/build.zig b/ly-core/build.zig index cf0dbf8..75ecb3f 100644 --- a/ly-core/build.zig +++ b/ly-core/build.zig @@ -51,6 +51,7 @@ pub fn build(b: *std.Build) void { } else if (target.result.os.tag == .freebsd) { addCImport(b, mod, translate_c, target, optimize, "kbio", "#include "); addCImport(b, mod, translate_c, target, optimize, "consio", "#include "); + addCImport(b, mod, translate_c, target, optimize, "sysctl", "#include "); } const mod_tests = b.addTest(.{ diff --git a/ly-core/src/interop.zig b/ly-core/src/interop.zig index 7d09381..736d8ca 100644 --- a/ly-core/src/interop.zig +++ b/ly-core/src/interop.zig @@ -14,6 +14,9 @@ pub const utmp = @import("utmp"); // Exists for X11 support only pub const xcb = @import("xcb"); +// FreeBSD only +pub const sysctl = @import("sysctl"); + pub const TimeOfDay = struct { seconds: i64, microseconds: i64, diff --git a/res/config.ini b/res/config.ini index 169f5ea..4269e7f 100644 --- a/res/config.ini +++ b/res/config.ini @@ -50,6 +50,7 @@ auth_fails = 10 # Identifier for battery whose charge to display at top left # Primary battery is usually BAT0 or BAT1 # If set to null, battery status won't be shown +# Unused on FreeBSD (a sysctl is used there) battery_id = null # Automatic login configuration diff --git a/src/main.zig b/src/main.zig index 0e57d41..91f3af8 100644 --- a/src/main.zig +++ b/src/main.zig @@ -2340,19 +2340,34 @@ fn adjustBrightness(io: std.Io, cmd: []const u8) !void { } fn getBatteryPercentage(io: std.Io, battery_id: []const u8) !u8 { - const path = try std.fmt.allocPrint(temporary_allocator, "/sys/class/power_supply/{s}/capacity", .{battery_id}); - defer temporary_allocator.free(path); + if (builtin.os.tag == .freebsd) { + // battery_id is unused on FreeBSD; sysctl exposes a single aggregate value. + // For multi-battery systems the MIB would be hw.acpi.battery.N.life, + // but hw.acpi.battery.life is the standard single-battery interface. + var capacity: c_int = -1; + var size: usize = @sizeOf(c_int); - const battery_file = try std.Io.Dir.cwd().openFile(io, path, .{}); - defer battery_file.close(io); + const ret = interop.sysctl.sysctlbyname("hw.acpi.battery.life", &capacity, &size, null, 0); - var buffer: [8]u8 = undefined; - const bytes_read = try battery_file.readStreaming(io, &.{&buffer}); - const capacity_str = buffer[0..bytes_read]; + if (ret != 0) return error.SysctlFailed; + if (capacity < 0 or capacity > 100) return error.InvalidBatteryCapacity; - const trimmed = std.mem.trimEnd(u8, capacity_str, "\n\r"); + return @intCast(capacity); + } else { + const path = try std.fmt.allocPrint(temporary_allocator, "/sys/class/power_supply/{s}/capacity", .{battery_id}); + defer temporary_allocator.free(path); - return try std.fmt.parseInt(u8, trimmed, 10); + const battery_file = try std.Io.Dir.cwd().openFile(io, path, .{}); + defer battery_file.close(io); + + var buffer: [8]u8 = undefined; + const bytes_read = try battery_file.readStreaming(io, &.{&buffer}); + const capacity_str = buffer[0..bytes_read]; + + const trimmed = std.mem.trimEnd(u8, capacity_str, "\n\r"); + + return try std.fmt.parseInt(u8, trimmed, 10); + } } fn getAuthErrorMsg(err: anyerror, lang: Lang) []const u8 { From 006749b2ecb706826da1c62efd0a741b2440ea96 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Sun, 5 Jul 2026 21:20:02 +0200 Subject: [PATCH 150/176] Format README Signed-off-by: AnErrupTion --- readme.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/readme.md b/readme.md index 33ef70b..8c554fd 100644 --- a/readme.md +++ b/readme.md @@ -45,31 +45,31 @@ Join us on Matrix over at [#ly-dm:matrix.org](https://matrix.to/#/#ly-dm:matrix. > [!WARNING] > Distributions using SELinux such as Fedora and openSUSE Tumbleweed may encounter issues. If you encounter such issues, such as session launch failures, proceed with the following steps: -> +> > ``` > # ausearch -m avc -ts recent > ``` > > If SELinux is denying process context transition, you will see this output: -> +> > ``` > denied { transition } for pid=XXXX comm="ly" path="/usr/bin/bash" > scontext=system_u:system_r:unconfined_service_t:s0 > tcontext=unconfined_u:unconfined_r:unconfined_t:s0 > tclass=process permissive=0 > ``` -> +> > Pipe this output into `audit2allow` to generate a security module package. This will persist regardless of changes to filesystem permissions: -> +> > ``` > # ausearch -m avc -ts recent | audit2allow -M ly-local > ``` -> +> > ``` > # semodule -i ly-local.pp > ``` -> -> *This fix has been confirmed on Fedora 39 and 44 and openSUSE Tumbleweed. (#494)* +> +> _This fix has been confirmed on Fedora 39 and 44 and openSUSE Tumbleweed. (#494)_ ### FreeBSD From 25fad28c341d5385b8ecdd74ef4667e79f6a4b12 Mon Sep 17 00:00:00 2001 From: HigherOrderLogic Date: Sun, 5 Jul 2026 22:43:15 +0200 Subject: [PATCH 151/176] Add `type_username` option (closes #891) (#1012) ## What are the changes about? Add an option to allow user manually input username, instead of selecting from a list of discovered users. I have not tested the changes yet since I wasnt able to get it build locally. Will try to resolve this asap. ## What existing issue does this resolve? #891 ## Pre-requisites - [ ] I have tested & confirmed the changes work locally - [x] I have run `zig fmt` throughout my changes Co-authored-by: HigherOrderLogic <73709188+HigherOrderLogic@users.noreply.github.com> Co-authored-by: AnErrupTion Reviewed-on: https://codeberg.org/fairyglade/ly/pulls/1012 Reviewed-by: AnErrupTion --- ly-ui/src/components/Text.zig | 4 + res/config.ini | 4 + src/components/Session.zig | 14 ++-- src/config/Config.zig | 1 + src/main.zig | 137 +++++++++++++++++++++++++--------- 5 files changed, 119 insertions(+), 41 deletions(-) diff --git a/ly-ui/src/components/Text.zig b/ly-ui/src/components/Text.zig index b4aad21..875021a 100644 --- a/ly-ui/src/components/Text.zig +++ b/ly-ui/src/components/Text.zig @@ -144,6 +144,10 @@ pub fn handle(self: *Text, maybe_key: ?keyboard.Key) !void { ); } +pub fn writeText(self: *Text, str: []const u8) !void { + for (str) |c| try self.write(c); +} + fn draw(self: *Text) void { if (self.masked) { if (self.maybe_mask) |mask| { diff --git a/res/config.ini b/res/config.ini index 4269e7f..a6f5d65 100644 --- a/res/config.ini +++ b/res/config.ini @@ -375,6 +375,10 @@ start_cmd = $CONFIG_DIRECTORY/ly/startup.sh # Center the session name. text_in_center = false +# If true, user will need to manually type username instead of selecting from the list +# of discovered users +type_username = false + # Default vi mode # normal -> normal mode # insert -> insert mode diff --git a/src/components/Session.zig b/src/components/Session.zig index 6cb252a..fe8c498 100644 --- a/src/components/Session.zig +++ b/src/components/Session.zig @@ -14,19 +14,19 @@ const Env = struct { environment: Environment, index: usize, }; -const EnvironmentLabel = CyclableLabel(Env, *UserList); +const EnvironmentLabel = CyclableLabel(Env, *?UserList); const Session = @This(); instance: ?Widget = null, label: *EnvironmentLabel, -user_list: *UserList, +user_list: *?UserList, pub fn init( allocator: Allocator, io: std.Io, buffer: *TerminalBuffer, - user_list: *UserList, + user_list: *?UserList, width: usize, text_in_center: bool, fg: u32, @@ -79,7 +79,7 @@ pub fn addEnvironment(self: *Session, environment: Environment) !void { const env = Env{ .environment = environment, .index = self.label.list.items.len }; try self.label.addItem(env); - addedSession(env, self.user_list); + if (self.user_list.*) |w| addedSession(env, w); } fn draw(self: *Session) void { @@ -90,16 +90,16 @@ fn handle(self: *Session, maybe_key: ?keyboard.Key) !void { try self.label.handle(maybe_key); } -fn addedSession(env: Env, user_list: *UserList) void { +fn addedSession(env: Env, user_list: UserList) void { const user = user_list.label.list.items[user_list.label.current]; if (!user.first_run) return; user.session_index.* = env.index; } -fn sessionChanged(env: Env, maybe_user_list: ?*UserList) void { +fn sessionChanged(env: Env, maybe_user_list: ?*?UserList) void { if (maybe_user_list) |user_list| { - user_list.label.list.items[user_list.label.current].session_index.* = env.index; + if (user_list.*) |w| w.label.list.items[w.label.current].session_index.* = env.index; } } diff --git a/src/config/Config.zig b/src/config/Config.zig index b7b258b..85d3ea1 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -95,6 +95,7 @@ sleep_cmd: ?[]const u8 = null, sleep_key: []const u8 = "F3", start_cmd: ?[]const u8 = null, text_in_center: bool = false, +type_username: bool = false, vi_default_mode: ViMode = .normal, vi_mode: bool = false, waylandsessions: ?[]const u8 = build_options.prefix_directory ++ "/share/wayland-sessions", diff --git a/src/main.zig b/src/main.zig index 91f3af8..5d4b074 100644 --- a/src/main.zig +++ b/src/main.zig @@ -107,8 +107,10 @@ const UiState = struct { info_line: InfoLine, animate: bool, session: Session, + saved_username: ?[]const u8, saved_users: SavedUsers, - login: UserList, + login: ?UserList, + login_text: ?*Text, password: *Text, password_widget: *Widget, insert_mode: bool, @@ -295,6 +297,7 @@ pub fn main(init: std.process.Init) !void { } state.has_old_save = false; + state.saved_username = null; if (state.config.save) read_save_file: { old_save_parser = migrator.tryMigrateIniSaveFile(state.allocator, state.io, state.old_save_path, &state.saved_users, usernames.items) catch break :read_save_file; @@ -312,8 +315,19 @@ pub fn main(init: std.process.Init) !void { var file_reader = save_file.reader(state.io, &file_buffer); var reader = &file_reader.interface; - const last_username_index_str = reader.takeDelimiterInclusive('\n') catch break :read_save_file; - state.saved_users.last_username_index = std.fmt.parseInt(usize, last_username_index_str[0..(last_username_index_str.len - 1)], 10) catch break :read_save_file; + const username_line = reader.takeDelimiterInclusive('\n') catch break :read_save_file; + + if (std.mem.containsAtLeastScalar2(u8, username_line, '-', 1)) read_username: { + var iterator = std.mem.splitScalar(u8, username_line[0..(username_line.len - 1)], '-'); + if (iterator.next() == null) break :read_username; // Would be index + + const maybe_username = iterator.next(); + if (maybe_username) |username| { + state.saved_username = try state.allocator.dupe(u8, username); + } + } else if (!state.config.type_username) { + state.saved_users.last_username_index = std.fmt.parseInt(usize, username_line[0..(username_line.len - 1)], 10) catch break :read_save_file; + } while (reader.seek < reader.buffer.len) { const line = reader.takeDelimiterInclusive('\n') catch break; @@ -763,22 +777,25 @@ pub fn main(init: std.process.Init) !void { ); defer state.login_label.deinit(); - state.login = try UserList.init( - state.allocator, - state.io, - &state.buffer, - usernames, - &state.saved_users, - &state.session, - state.box.width - 2 * state.box.horizontal_margin - state.labels_max_length - 1, - state.config.text_in_center, - state.buffer.fg, - state.buffer.bg, - ); - defer state.login.deinit(); + state.login = null; + if (!state.config.type_username) { + state.login = try UserList.init( + state.allocator, + state.io, + &state.buffer, + usernames, + &state.saved_users, + &state.session, + state.box.width - 2 * state.box.horizontal_margin - state.labels_max_length - 1, + state.config.text_in_center, + state.buffer.fg, + state.buffer.bg, + ); - try state.buffer.registerKeybind(state.io, &state.login.label.keybinds, "H", &viGoLeft, &state); - try state.buffer.registerKeybind(state.io, &state.login.label.keybinds, "L", &viGoRight, &state); + try state.buffer.registerKeybind(state.io, &state.login.?.label.keybinds, "H", &viGoLeft, &state); + try state.buffer.registerKeybind(state.io, &state.login.?.label.keybinds, "L", &viGoRight, &state); + } + defer if (state.login) |*w| w.deinit(); if (state.config.shell) { addOtherEnvironment(&state.session, state.lang, .shell, null) catch |err| { @@ -887,7 +904,7 @@ pub fn main(init: std.process.Init) !void { // This effectively means you can't login, since there would be no local // accounts *and* no root account...but at this point, if that's the // case, you have bigger problems to deal with in the first place. :D - try state.info_line.addMessage(state.lang.err_no_users, state.config.error_bg, state.config.error_fg); + if (!state.config.type_username) try state.info_line.addMessage(state.lang.err_no_users, state.config.error_bg, state.config.error_fg); try state.log_file.err(state.io, "sys", "no users found", .{}); } @@ -920,6 +937,22 @@ pub fn main(init: std.process.Init) !void { try state.buffer.registerKeybind(state.io, &state.password.keybinds, "L", &viGoRight, &state); state.password_widget = state.password.widget(); + state.login_text = null; + + if (state.config.type_username) { + state.login_text = try Text.init( + state.allocator, + state.io, + &state.buffer, + state.insert_mode, + false, + null, + state.box.width - 2 * state.box.horizontal_margin - state.labels_max_length - 1, + state.buffer.fg, + state.buffer.bg, + ); + } + defer if (state.login_text) |lt| lt.deinit(); state.version_label = Label.init( ly_version_str, @@ -974,13 +1007,18 @@ pub fn main(init: std.process.Init) !void { ); state.session.label.current = session_index; - for (state.login.label.list.items, 0..) |username, i| { - if (std.mem.eql(u8, username.name, auto_user)) { - state.login.label.current = i; - break; + state.is_autologin = true; + + if (state.login_text) |box| { + try box.writeText(auto_user); + } else { + for (state.login.?.label.list.items, 0..) |username, i| { + if (std.mem.eql(u8, username.name, auto_user)) { + state.login.?.label.current = i; + break; + } } } - state.is_autologin = true; } // Switch to selected TTY @@ -1133,7 +1171,22 @@ pub fn main(init: std.process.Init) !void { 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 (state.login_text) |box| { + if (state.saved_username) |username| { + defer state.allocator.free(username); + + try box.writeText(username); + + default_input = .password; + + for (state.saved_users.user_list.items) |user| { + if (std.mem.eql(u8, username, user.username)) { + state.session.label.current = @min(user.session_index, state.session.label.list.items.len - 1); + break; + } + } + } + } else if (state.saved_users.last_username_index) |index| load_last_user: { // If the saved index isn't valid, bail out if (index >= state.saved_users.user_list.items.len) break :load_last_user; @@ -1143,7 +1196,7 @@ pub fn main(init: std.process.Init) !void { // If it doesn't exist (anymore), we don't change the value for (usernames.items, 0..) |username, i| { if (std.mem.eql(u8, username, user.username)) { - state.login.label.current = i; + state.login.?.label.current = i; break; } } @@ -1156,7 +1209,7 @@ pub fn main(init: std.process.Init) !void { const info_line_widget = state.info_line.widget(); const session_widget = state.session.widget(); - const login_widget = state.login.widget(); + const login_widget = if (state.config.type_username) state.login_text.?.widget() else state.login.?.widget(); var widgets: std.ArrayList([]*Widget) = .empty; defer widgets.deinit(state.allocator); @@ -1400,6 +1453,7 @@ fn disableInsertMode(ptr: *anyopaque) !bool { if (state.config.vi_mode and state.insert_mode) { state.insert_mode = false; state.password.should_insert = false; + if (state.login_text) |lt| lt.should_insert = false; state.buffer.drawNextFrame(true); } return false; @@ -1411,6 +1465,7 @@ fn enableInsertMode(ptr: *anyopaque) !bool { state.insert_mode = true; state.password.should_insert = true; + if (state.login_text) |lt| lt.should_insert = true; state.buffer.drawNextFrame(true); return false; } @@ -1552,7 +1607,11 @@ fn authenticate(ptr: *anyopaque) !bool { var file_writer = file.writer(state.io, &file_buffer); var writer = &file_writer.interface; - try writer.print("{d}\n", .{state.login.label.current}); + if (state.login_text) |box| { + try writer.print("0-{s}\n", .{box.text.items}); + } else { + 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 }); } @@ -1610,7 +1669,7 @@ fn authenticate(ptr: *anyopaque) !bool { &state.log_file, auth_options, current_environment, - state.login.getCurrentUsername(), + if (state.login_text) |box| box.text.items else state.login.?.getCurrentUsername(), password_text, ) catch |err| { shared_err.writeError(err); @@ -2135,12 +2194,22 @@ fn positionWidgets(ptr: *anyopaque) !void { .childrenPosition() .resetXFrom(state.info_line.label.childrenPosition()) .addY(1)); - state.login.label.positionY(state.login_label - .childrenPosition() - .addX(state.labels_max_length - TerminalBuffer.strWidth(state.login_label.text) + 1)); + if (state.login_text) |box| { + box.positionY(state.login_label + .childrenPosition() + .addX(state.labels_max_length - TerminalBuffer.strWidth(state.login_label.text) + 1)); + } else { + state.login.?.label.positionY(state.login_label + .childrenPosition() + .addX(state.labels_max_length - TerminalBuffer.strWidth(state.login_label.text) + 1)); + } - state.password_label.positionX(state.login.label - .childrenPosition() + const login_children_pos = if (state.login_text) |box| + box.childrenPosition() + else + state.login.?.label.childrenPosition(); + + state.password_label.positionX(login_children_pos .resetXFrom(state.info_line.label.childrenPosition()) .addY(1)); state.password.positionY(state.password_label From e0c287306ee280d057d0b3f5d46504c1882025ee Mon Sep 17 00:00:00 2001 From: Luna Date: Sun, 5 Jul 2026 23:22:03 +0200 Subject: [PATCH 152/176] feature: add corner customization (closes #961) (#963) ## What are the changes about? Add corner customization (what is in which corner of the screen) ## What existing issue does this resolve? Issue #961 ## Pre-requisites - [x] I have tested & confirmed the changes work locally - [x] I have run `zig fmt` throughout my changes Co-authored-by: AnErrupTion Reviewed-on: https://codeberg.org/fairyglade/ly/pulls/963 Reviewed-by: AnErrupTion --- res/config.ini | 28 ++++ src/config/Config.zig | 4 + src/main.zig | 363 ++++++++++++++++++++++++++++++++---------- 3 files changed, 315 insertions(+), 80 deletions(-) diff --git a/res/config.ini b/res/config.ini index a6f5d65..c57c7f4 100644 --- a/res/config.ini +++ b/res/config.ini @@ -153,6 +153,34 @@ colormix_col2 = 0x000000FF # Color mixing animation third color id colormix_col3 = 0x20000000 +# Screen corners customization +# Keywords: +# keys -> Power management keys (shutdown, restart, etc.) +# clock -> Clock (format defined by 'clock' option) +# tty -> Active TTY number +# battery -> Battery percentage +# version -> Ly version string +# numlock -> Numlock state +# capslock -> Capslock state +# labels -> All custom info labels (lbl:) +# binds -> All custom keybind hints (cmd:) +# lbl:name -> Specific custom info label +# cmd:key -> Specific custom keybind hint +# +# The order defines the vertical stack (first item is at the edge). + +# Bottom left +corner_bottom_left = version + +# Bottom right +corner_bottom_right = labels + +# Top left +corner_top_left = keys + +# Top right +corner_top_right = clock tty + # For custom binds: the horizontal limit in characters for each # line of custom binds before moving on to the next. # If null, defaults to the width of the terminal instead. diff --git a/src/config/Config.zig b/src/config/Config.zig index 85d3ea1..36c5a41 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -39,6 +39,10 @@ cmatrix_max_codepoint: u16 = 0x7B, colormix_col1: u32 = 0x00FF0000, colormix_col2: u32 = 0x000000FF, colormix_col3: u32 = 0x20000000, +corner_bottom_left: []const u8 = "version", +corner_bottom_right: []const u8 = "labels", +corner_top_left: []const u8 = "keys", +corner_top_right: []const u8 = "clock tty", custom_bind_width: ?u32 = null, custom_sessions: []const u8 = build_options.config_directory ++ "/ly/custom-sessions", default_input: Input = .login, diff --git a/src/main.zig b/src/main.zig index 5d4b074..f77f63a 100644 --- a/src/main.zig +++ b/src/main.zig @@ -2062,93 +2062,300 @@ fn updateSessionSpecifier(self: *Label, ptr: *anyopaque) !void { self.setText(env.environment.specifier); } -fn positionWidgets(ptr: *anyopaque) !void { - var state: *UiState = @ptrCast(@alignCast(ptr)); +const Corner = enum { + bottomLeft, + bottomRight, + topLeft, + topRight, +}; - // Offsets for custom bind placement. Declared here instead of the - // below if stmt as we need these for `battery_label` positioning. - var x_offset: usize = 0; - // To account for the first row of built-in key hints - var y_offset: usize = 1; - if (!state.config.hide_key_hints) { - state.shutdown_label.positionX(state.edge_margin - .add(TerminalBuffer.START_POSITION)); - var last_label = state.shutdown_label; - state.restart_label.positionX(last_label - .childrenPosition() - .addX(1)); - last_label = state.restart_label; - state.sleep_label.positionX(last_label - .childrenPosition() - .addX(1)); - if (state.config.sleep_cmd != null) { - last_label = state.sleep_label; +const PositionedWidgets = struct { + binds: []bool, + labels: []bool, +}; + +fn positionSingleWidget(state: *UiState, item: []const u8, current_x: *usize, current_y: usize, is_left: bool, is_top: bool, positioned: *PositionedWidgets) !bool { + const base_x = state.edge_margin.x; + + if (std.mem.eql(u8, item, "keys")) { + if (state.config.hide_key_hints) return false; + + var local_x = current_x.*; + var local_y = current_y; + + const labels = [_]?*Label{ + &state.shutdown_label, + &state.restart_label, + if (state.config.sleep_cmd != null) &state.sleep_label else null, + if (state.config.hibernate_cmd != null) &state.hibernate_label else null, + &state.toggle_password_label, + if (state.config.brightness_down_key != null) &state.brightness_down_label else null, + if (state.config.brightness_up_key != null) &state.brightness_up_label else null, + }; + + for (labels) |maybe_label| { + const label = maybe_label orelse continue; + const width = TerminalBuffer.strWidth(label.text); + + if (is_left) { + if (local_x + width > state.buffer.width - state.edge_margin.x) { + local_x = base_x; + if (is_top) local_y += 1 else local_y -= 1; + } + label.positionXY(Position.init(local_x, local_y)); + local_x += width + 1; + } else { + if (width + state.edge_margin.x > local_x) { + local_x = state.buffer.width - state.edge_margin.x; + if (is_top) local_y += 1 else local_y -= 1; + } + label.positionXY(Position.init(local_x - width, local_y)); + local_x -= width + 1; + } } - state.hibernate_label.positionX(last_label - .childrenPosition() - .addX(1)); - if (state.config.hibernate_cmd != null) { - last_label = state.hibernate_label; + current_x.* = local_x; + return true; + } else if (std.mem.eql(u8, item, "clock") or std.mem.eql(u8, item, "time")) { + if (state.config.clock == null) return false; + const width = TerminalBuffer.strWidth(state.clock_label.text); + if (is_left) { + state.clock_label.positionXY(Position.init(current_x.*, current_y)); + current_x.* += width + 1; + } else { + state.clock_label.positionXY(Position.init(current_x.* - width, current_y)); + current_x.* -= width + 1; } - state.toggle_password_label.positionX(last_label - .childrenPosition() - .addX(1)); - last_label = state.toggle_password_label; - state.brightness_down_label.positionX(last_label - .childrenPosition() - .addX(1)); - if (state.config.brightness_down_key != null) { - last_label = state.brightness_down_label; + return true; + } else if (std.mem.eql(u8, item, "tty")) { + if (!state.config.show_tty) return false; + const width = TerminalBuffer.strWidth(state.tty_label.text); + if (is_left) { + state.tty_label.positionXY(Position.init(current_x.*, current_y)); + current_x.* += width + 1; + } else { + state.tty_label.positionXY(Position.init(current_x.* - width, current_y)); + current_x.* -= width + 1; } - state.brightness_up_label.positionXY(last_label - .childrenPosition() - .addX(1)); - for (state.custom_binds.items) |*item| { - item.lbl.positionXY(state.edge_margin - .addY(y_offset) - .addX(x_offset)); - x_offset += item.lbl.text.len + 1; - if (x_offset + item.lbl.text.len > state.config.custom_bind_width orelse state.buffer.width) { - x_offset = 0; - y_offset += 1; + return true; + } else if (std.mem.eql(u8, item, "battery")) { + if (state.config.battery_id == null) return false; + const width = TerminalBuffer.strWidth(state.battery_label.text); + if (is_left) { + state.battery_label.positionXY(Position.init(current_x.*, current_y)); + current_x.* += width + 1; + } else { + state.battery_label.positionXY(Position.init(current_x.* - width, current_y)); + current_x.* -= width + 1; + } + return true; + } else if (std.mem.eql(u8, item, "version")) { + if (state.config.hide_version_string) return false; + const width = TerminalBuffer.strWidth(state.version_label.text); + if (is_left) { + state.version_label.positionXY(Position.init(current_x.*, current_y)); + current_x.* += width + 1; + } else { + state.version_label.positionXY(Position.init(current_x.* - width, current_y)); + current_x.* -= width + 1; + } + return true; + } else if (std.mem.eql(u8, item, "numlock")) { + if (state.config.hide_keyboard_locks) return false; + const width = TerminalBuffer.strWidth(state.lang.numlock); + if (is_left) { + state.numlock_label.positionXY(Position.init(current_x.*, current_y)); + current_x.* += width + 1; + } else { + state.numlock_label.positionXY(Position.init(current_x.* - width, current_y)); + current_x.* -= width + 1; + } + return true; + } else if (std.mem.eql(u8, item, "capslock")) { + if (state.config.hide_keyboard_locks) return false; + const width = TerminalBuffer.strWidth(state.lang.capslock); + if (is_left) { + state.capslock_label.positionXY(Position.init(current_x.*, current_y)); + current_x.* += width + 1; + } else { + state.capslock_label.positionXY(Position.init(current_x.* - width, current_y)); + current_x.* -= width + 1; + } + return true; + } else if (std.mem.eql(u8, item, "labels")) { + for (state.custom_info.items, 0..) |*info, i| { + if (positioned.labels[i]) continue; + const width = TerminalBuffer.strWidth(info.lbl.text); + if (is_left) { + info.lbl.positionXY(Position.init(current_x.*, current_y)); + current_x.* += width + 1; + } else { + info.lbl.positionXY(Position.init(current_x.* - width, current_y)); + current_x.* -= width + 1; + } + positioned.labels[i] = true; + } + return true; + } else if (std.mem.eql(u8, item, "binds")) { + var local_x = current_x.*; + var local_y = current_y; + + for (state.custom_binds.items, 0..) |*bind, i| { + if (positioned.binds[i]) continue; + const width = TerminalBuffer.strWidth(bind.lbl.text); + + if (is_left) { + if (local_x + width > (state.config.custom_bind_width orelse state.buffer.width) - state.edge_margin.x) { + local_x = base_x; + if (is_top) local_y += 1 else local_y -= 1; + } + bind.lbl.positionXY(Position.init(local_x, local_y)); + local_x += width + 1; + } else { + if (width + state.edge_margin.x > local_x) { + local_x = state.buffer.width - state.edge_margin.x; + if (is_top) local_y += 1 else local_y -= 1; + } + bind.lbl.positionXY(Position.init(local_x - width, local_y)); + local_x -= width + 1; + } + positioned.binds[i] = true; + } + current_x.* = local_x; + return true; + } else if (std.mem.startsWith(u8, item, "lbl:")) { + const name = item["lbl:".len..]; + for (state.custom_info.items, 0..) |*info, i| { + if (std.mem.eql(u8, info.info.name, name)) { + if (positioned.labels[i]) return false; + const width = TerminalBuffer.strWidth(info.lbl.text); + if (is_left) { + info.lbl.positionXY(Position.init(current_x.*, current_y)); + current_x.* += width + 1; + } else { + info.lbl.positionXY(Position.init(current_x.* - width, current_y)); + current_x.* -= width + 1; + } + positioned.labels[i] = true; + return true; + } + } + } else if (std.mem.startsWith(u8, item, "cmd:")) { + const key = item["cmd:".len..]; + for (state.custom_binds.items, 0..) |*bind, i| { + if (std.mem.eql(u8, bind.key, key)) { + if (positioned.binds[i]) return false; + const width = TerminalBuffer.strWidth(bind.lbl.text); + if (is_left) { + bind.lbl.positionXY(Position.init(current_x.*, current_y)); + current_x.* += width + 1; + } else { + bind.lbl.positionXY(Position.init(current_x.* - width, current_y)); + current_x.* -= width + 1; + } + positioned.binds[i] = true; + return true; } } } - for (state.custom_info.items, 0..) |*item, i| { - item.lbl.positionXY(state.edge_margin - .invertX(state.buffer.width) - .removeX(item.lbl.text.len) - .invertY(state.buffer.height) - .removeY(state.custom_info.items.len) - .addY(i)); + return false; +} + +fn positionItem(state: *UiState, item: []const u8, current_x: usize, current_y: usize, is_left: bool, is_top: bool, positioned: *PositionedWidgets) !bool { + // Check if item contains a comma for compound widgets + if (std.mem.indexOf(u8, item, ",") == null) { + var line_x = current_x; + return positionSingleWidget(state, item, &line_x, current_y, is_left, is_top, positioned); } - state.battery_label.positionXY(state.edge_margin - .add(TerminalBuffer.START_POSITION) - .addYFromIf(state.brightness_up_label.childrenPosition(), !state.config.hide_key_hints) - .addYIf(y_offset, !state.config.hide_key_hints) - .removeYFromIf(state.edge_margin, !state.config.hide_key_hints)); + var it = std.mem.tokenizeAny(u8, item, ","); + var success = false; - const tty_label_width = if (state.config.show_tty) TerminalBuffer.strWidth(state.tty_label.text) else 0; - const tty_label_gap = if (state.config.show_tty and state.config.clock != null) @as(usize, 1) else 0; - state.tty_label.positionXY(state.edge_margin - .add(TerminalBuffer.START_POSITION) - .invertX(state.buffer.width) - .removeXIf(tty_label_width, state.buffer.width > tty_label_width + state.edge_margin.x)); - state.clock_label.positionXY(state.edge_margin - .add(TerminalBuffer.START_POSITION) - .invertX(state.buffer.width) - .removeXIf(TerminalBuffer.strWidth(state.clock_label.text) + tty_label_width + tty_label_gap, state.buffer.width > TerminalBuffer.strWidth(state.clock_label.text) + tty_label_width + tty_label_gap + state.edge_margin.x)); + // Check if we need to wrap to next line + var line_x = current_x; - state.numlock_label.positionX(state.edge_margin - .add(TerminalBuffer.START_POSITION) - .addYFromIf(state.clock_label.childrenPosition(), state.config.clock != null) - .removeYFromIf(state.edge_margin, state.config.clock != null) - .invertX(state.buffer.width) - .removeXIf(TerminalBuffer.strWidth(state.lang.numlock), state.buffer.width > TerminalBuffer.strWidth(state.lang.numlock) + state.edge_margin.x)); - state.capslock_label.positionX(state.numlock_label - .childrenPosition() - .removeX(TerminalBuffer.strWidth(state.lang.numlock) + TerminalBuffer.strWidth(state.lang.capslock) + 1)); + // Now position each subitem + while (it.next()) |subitem| { + const trimmed = std.mem.trim(u8, subitem, " "); + + if (try positionSingleWidget(state, trimmed, &line_x, current_y, is_left, is_top, positioned)) { + success = true; + } + } + + return success; +} + +fn positionCorner(state: *UiState, config_str: []const u8, corner: Corner, positioned: *PositionedWidgets) !void { + const is_left = corner == .topLeft or corner == .bottomLeft; + const is_top = corner == .topLeft or corner == .topRight; + + var y_offset: usize = 0; + var i: usize = 0; + const len = config_str.len; + + while (i < len) { + // Skip whitespace and brackets + while (i < len and (config_str[i] == ' ' or config_str[i] == '\t' or config_str[i] == '[' or config_str[i] == ']')) { + i += 1; + } + if (i >= len) break; + + const start = i; + while (i < len and config_str[i] != ' ' and config_str[i] != '\t' and config_str[i] != '[' and config_str[i] != ']') { + i += 1; + } + const token = config_str[start..i]; + + const current_x = if (is_left) state.edge_margin.x else state.buffer.width - state.edge_margin.x; + const current_y = if (is_top) state.edge_margin.y + y_offset else state.buffer.height - 1 - state.edge_margin.y - y_offset; + + if (try positionItem(state, token, current_x, current_y, is_left, is_top, positioned)) { + y_offset += 1; + } + } +} + +fn positionWidgets(ptr: *anyopaque) !void { + var state: *UiState = @ptrCast(@alignCast(ptr)); + + const offscreen = Position.init(state.buffer.width + 1, state.buffer.height + 1); + + // Reset all potential corner widgets to offscreen + state.shutdown_label.positionXY(offscreen); + state.restart_label.positionXY(offscreen); + state.sleep_label.positionXY(offscreen); + state.hibernate_label.positionXY(offscreen); + state.toggle_password_label.positionXY(offscreen); + state.brightness_down_label.positionXY(offscreen); + state.brightness_up_label.positionXY(offscreen); + state.clock_label.positionXY(offscreen); + state.tty_label.positionXY(offscreen); + state.battery_label.positionXY(offscreen); + state.version_label.positionXY(offscreen); + state.numlock_label.positionXY(offscreen); + state.capslock_label.positionXY(offscreen); + + for (state.custom_binds.items) |*bind| { + bind.lbl.positionXY(offscreen); + } + for (state.custom_info.items) |*info| { + info.lbl.positionXY(offscreen); + } + + var positioned = PositionedWidgets{ + .binds = try state.allocator.alloc(bool, state.custom_binds.items.len), + .labels = try state.allocator.alloc(bool, state.custom_info.items.len), + }; + defer state.allocator.free(positioned.binds); + defer state.allocator.free(positioned.labels); + + @memset(positioned.binds, false); + @memset(positioned.labels, false); + + try positionCorner(state, state.config.corner_top_left, .topLeft, &positioned); + try positionCorner(state, state.config.corner_top_right, .topRight, &positioned); + try positionCorner(state, state.config.corner_bottom_left, .bottomLeft, &positioned); + try positionCorner(state, state.config.corner_bottom_right, .bottomRight, &positioned); var bb_height = state.box.height; var bb_width = state.box.width; @@ -2215,10 +2422,6 @@ fn positionWidgets(ptr: *anyopaque) !void { state.password.positionY(state.password_label .childrenPosition() .addX(state.labels_max_length - TerminalBuffer.strWidth(state.password_label.text) + 1)); - - state.version_label.positionXY(state.edge_margin - .add(TerminalBuffer.START_POSITION) - .invertY(state.buffer.height - 1)); } fn handleInactivity(ptr: *anyopaque) !void { From 102d1c9a9b61eef1f9620570e3c3838e402c2c63 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Mon, 6 Jul 2026 00:22:51 +0200 Subject: [PATCH 153/176] Unset Shift and Ctrl for arrow keys (closes #1015) Signed-off-by: AnErrupTion --- ly-ui/src/keyboard.zig | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/ly-ui/src/keyboard.zig b/ly-ui/src/keyboard.zig index a90148b..626f21f 100644 --- a/ly-ui/src/keyboard.zig +++ b/ly-ui/src/keyboard.zig @@ -167,6 +167,12 @@ pub fn getKeyList(allocator: Allocator, tb_event: termbox.tb_event) !KeyList { 21 => key.right = true, else => {}, } + + if (code >= 18 and code <= 21) { + // https://github.com/termbox/termbox2/blob/605398fa79108412976191e062ea14bd4bd30213/termbox2.h#L446 + key.ctrl = false; + key.shift = false; + } } 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; From 4513033bcb8d0ba1004bcc5bfa4d6e22a117a16d Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Mon, 6 Jul 2026 02:22:40 +0200 Subject: [PATCH 154/176] ly@.service: Use agetty Signed-off-by: AnErrupTion --- readme.md | 6 ------ res/ly@.service | 38 ++++++++++++++++++++++++++++++++++---- 2 files changed, 34 insertions(+), 10 deletions(-) diff --git a/readme.md b/readme.md index 8c554fd..3b79436 100644 --- a/readme.md +++ b/readme.md @@ -162,12 +162,6 @@ Then, similarly to the previous command, you need to enable the Ly service: # systemctl disable getty@tty2.service ``` -On platforms that use systemd-logind to dynamically start `autovt@.service` instances when the switch to a new tty occurs, any ly instances for ttys _except the default tty_ need to be enabled using a different mechanism: To autostart ly on switch to `tty2`, do not enable any `ly` unit directly, instead symlink `autovt@tty2.service` to `ly@tty2.service` within `/usr/lib/systemd/system/` (analogous for every other tty you want to enable ly on). - -The target of the symlink, `ly@ttyN.service`, does not actually exist, but systemd nevertheless recognizes that the instanciation of `autovt@.service` with `%I` equal to `ttyN` now points to an instanciation of `ly@.service` with `%I` set to `ttyN`. - -Compare to `man 5 logind.conf`, especially regarding the `NAutoVTs=` and `ReserveVT=` parameters. - On non-systemd systems, you can change the TTY Ly will run on by editing the corresponding service file for your platform. ### OpenRC diff --git a/res/ly@.service b/res/ly@.service index 54d8bf6..2db6df4 100644 --- a/res/ly@.service +++ b/res/ly@.service @@ -1,16 +1,46 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +# +# This file is part of systemd. +# +# systemd is free software; you can redistribute it and/or modify it +# under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation; either version 2.1 of the License, or +# (at your option) any later version. +# +# Modified for Ly by AnErrupTion + [Unit] -Description=TUI display manager -After=systemd-user-sessions.service plymouth-quit-wait.service -After=getty@%i.service +Description=TUI display manager on %I +After=systemd-user-sessions.service plymouth-quit-wait.service getty@%i.service Conflicts=getty@%i.service [Service] +ExecStart=$PREFIX_DIRECTORY/bin/agetty -nl $PREFIX_DIRECTORY/bin/$EXECUTABLE_NAME %I ${TERM} Type=idle -ExecStart=$PREFIX_DIRECTORY/bin/$EXECUTABLE_NAME +Restart=always +RestartSec=0 +UtmpIdentifier=%I StandardInput=tty +StandardOutput=tty TTYPath=/dev/%I TTYReset=yes TTYVHangup=yes +TTYVTDisallocate=yes +IgnoreSIGPIPE=no +SendSIGHUP=yes + +ImportCredential=tty.virtual.%I.agetty.*:agetty. +ImportCredential=tty.virtual.%I.login.*:login. +ImportCredential=agetty.* +ImportCredential=login.* +ImportCredential=shell.* + +# Unset locale for the console getty since the console has problems +# displaying some internationalized messages. +UnsetEnvironment=LANG LANGUAGE LC_CTYPE LC_NUMERIC LC_TIME LC_COLLATE LC_MONETARY LC_MESSAGES LC_PAPER LC_NAME LC_ADDRESS LC_TELEPHONE LC_MEASUREMENT LC_IDENTIFICATION [Install] +Alias=autovt@.service + WantedBy=multi-user.target +DefaultInstance=tty2 From 9b8046e37c93db564feb3f6a2349dd16caa67f81 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Mon, 6 Jul 2026 11:10:26 +0200 Subject: [PATCH 155/176] auth: chown & chmod TTY (closes #944) Signed-off-by: AnErrupTion --- src/auth.zig | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/auth.zig b/src/auth.zig index 7a3c033..00ae005 100644 --- a/src/auth.zig +++ b/src/auth.zig @@ -105,6 +105,11 @@ pub fn authenticate(allocator: std.mem.Allocator, io: std.Io, log_file: *LogFile try log_file.info(io, "auth/passwd", "setting user shell", .{}); if (user_entry.shell == null) interop.setUserShell(&user_entry); + // chown & chmod stdin (which is the TTY) + // https://github.com/mirror/busybox/blob/371fe9f71d445d18be28c82a2a6d82115c8af19d/loginutils/login.c#L558 + if (interop.isError(std.posix.system.fchown(std.posix.STDIN_FILENO, user_entry.uid, user_entry.gid))) return error.TtyChownFailed; + if (interop.isError(std.posix.system.fchmod(std.posix.STDIN_FILENO, 0o600))) return error.TtyChmodFailed; + var shared_err = try SharedError.init(null, null); defer shared_err.deinit(); From 32f11cdbe56cc081079cfc2f1e1ab4ade50e66eb Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Mon, 6 Jul 2026 11:47:37 +0200 Subject: [PATCH 156/176] Modify link to black hole animation (closes #1006) Signed-off-by: AnErrupTion --- readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/readme.md b/readme.md index 3b79436..2d26c31 100644 --- a/readme.md +++ b/readme.md @@ -2,7 +2,7 @@ ![Ly screenshot](.github/screenshot.png "Ly screenshot") -_Note: the above animation can be found [here](https://codeberg.org/attachments/f336d6ac-8331-4323-91fc-0e4619803401)!_ +_Note: the above animation can be found [here](https://codeberg.org/fairyglade/ly-community/src/branch/main/animations/dur/blackhole-smooth-240x67.dur)!_ Ly is a lightweight TUI (ncurses-like) display manager for Linux and BSD, designed with portability in mind and doesn't require systemd to run. From 7eae544418d8fce4fa59f72946bff30bbfbe8702 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Mon, 6 Jul 2026 12:43:10 +0200 Subject: [PATCH 157/176] systemd: Add more conflicts, better kmscon service Signed-off-by: AnErrupTion --- res/ly-kmsconvt@.service | 21 ++++++++++++++++----- res/ly@.service | 2 +- 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/res/ly-kmsconvt@.service b/res/ly-kmsconvt@.service index 85b0f05..9384bdf 100644 --- a/res/ly-kmsconvt@.service +++ b/res/ly-kmsconvt@.service @@ -1,17 +1,28 @@ +# Default kmsconvt@.service +# Modified for Ly by AnErrupTion + [Unit] -Description=TUI display manager using KMSCON -After=systemd-user-sessions.service plymouth-quit-wait.service -After=kmsconvt@%i.service -Conflicts=kmsconvt@%i.service +Description=TUI display manager on %I using KMSCON +After=systemd-user-sessions.service plymouth-quit-wait.service rc-local.service kmsconvt@%i.service +Conflicts=getty@%i.service kmsconvt@%i.service ly@%i.service +OnFailure=ly@%i.service [Service] ExecStart=$PREFIX_DIRECTORY/bin/kmscon --term=linux --font-engine unifont --vt=%I --login -- $PREFIX_DIRECTORY/bin/$EXECUTABLE_NAME --use-kmscon-vt -StandardInput=tty +Type=idle +Restart=always +RestartSec=0 UtmpIdentifier=%I +StandardInput=tty +StandardOutput=tty TTYPath=/dev/%I TTYReset=yes TTYVHangup=yes TTYVTDisallocate=yes +IgnoreSIGPIPE=no +SendSIGHUP=yes [Install] +Alias=autovt@.service WantedBy=multi-user.target +DefaultInstance=tty2 diff --git a/res/ly@.service b/res/ly@.service index 2db6df4..b2fb5ff 100644 --- a/res/ly@.service +++ b/res/ly@.service @@ -12,7 +12,7 @@ [Unit] Description=TUI display manager on %I After=systemd-user-sessions.service plymouth-quit-wait.service getty@%i.service -Conflicts=getty@%i.service +Conflicts=getty@%i.service kmsconvt@%i.service ly-kmsconvt@%i.service [Service] ExecStart=$PREFIX_DIRECTORY/bin/agetty -nl $PREFIX_DIRECTORY/bin/$EXECUTABLE_NAME %I ${TERM} From 954e90b3bd99ffed44695e8167cd33d638056b6c Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Mon, 6 Jul 2026 17:20:05 +0200 Subject: [PATCH 158/176] Remove options superseded by corner customisation Signed-off-by: AnErrupTion --- res/config.ini | 18 +------- src/config/Config.zig | 6 +-- src/config/migrator.zig | 5 +++ src/main.zig | 93 +++++++++++++++++++++++++---------------- 4 files changed, 66 insertions(+), 56 deletions(-) diff --git a/res/config.ini b/res/config.ini index c57c7f4..c29b50a 100644 --- a/res/config.ini +++ b/res/config.ini @@ -155,7 +155,7 @@ colormix_col3 = 0x20000000 # Screen corners customization # Keywords: -# keys -> Power management keys (shutdown, restart, etc.) +# keys -> Power management, brightness control & toggle password keys # clock -> Clock (format defined by 'clock' option) # tty -> Active TTY number # battery -> Battery percentage @@ -179,7 +179,7 @@ corner_bottom_right = labels corner_top_left = keys # Top right -corner_top_right = clock tty +corner_top_right = clock # For custom binds: the horizontal limit in characters for each # line of custom binds before moving on to the next. @@ -285,15 +285,6 @@ hibernate_key = F4 # Remove main box borders hide_borders = false -# Remove power management command hints -hide_key_hints = false - -# Remove keyboard lock states from the top right corner -hide_keyboard_locks = false - -# Remove version number from the top left corner -hide_version_string = false - # Command executed when no input is detected for a certain time # If null, no command will be executed inactivity_cmd = null @@ -379,11 +370,6 @@ shell = true # Specifies the key combination used for showing the password show_password_key = F7 -# Display the active TTY number (e.g. tty3) to the right of the clock in the top right corner -# If the clock is disabled, the TTY label occupies the top right corner on its own -# If false, the TTY number will not be shown -show_tty = false - # Command executed when pressing shutdown_key shutdown_cmd = /sbin/shutdown $PLATFORM_SHUTDOWN_ARG now diff --git a/src/config/Config.zig b/src/config/Config.zig index 36c5a41..0436f5f 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -42,7 +42,7 @@ colormix_col3: u32 = 0x20000000, corner_bottom_left: []const u8 = "version", corner_bottom_right: []const u8 = "labels", corner_top_left: []const u8 = "keys", -corner_top_right: []const u8 = "clock tty", +corner_top_right: []const u8 = "clock", custom_bind_width: ?u32 = null, custom_sessions: []const u8 = build_options.config_directory ++ "/ly/custom-sessions", default_input: Input = .login, @@ -67,9 +67,6 @@ gameoflife_initial_density: f32 = 0.4, hibernate_cmd: ?[]const u8 = null, hibernate_key: []const u8 = "F4", hide_borders: bool = false, -hide_key_hints: bool = false, -hide_keyboard_locks: bool = false, -hide_version_string: bool = false, inactivity_cmd: ?[]const u8 = null, inactivity_delay: u16 = 0, initial_info_text: ?[]const u8 = null, @@ -92,7 +89,6 @@ session_log: ?[]const u8 = "ly-session.log", setup_cmd: []const u8 = build_options.config_directory ++ "/ly/setup.sh", shell: bool = true, show_password_key: []const u8 = "F7", -show_tty: bool = false, shutdown_cmd: []const u8 = "/sbin/shutdown -a now", shutdown_key: []const u8 = "F1", sleep_cmd: ?[]const u8 = null, diff --git a/src/config/migrator.zig b/src/config/migrator.zig index 366bc81..b174a48 100644 --- a/src/config/migrator.zig +++ b/src/config/migrator.zig @@ -45,6 +45,11 @@ const removed_properties = [_][]const u8{ "wayland_cmd", "console_dev", "load", + // Migrating these isn't worth the effort so just say we removed them + "hide_key_hints", + "hide_keyboard_locks", + "hide_version_string", + "show_tty", }; pub var auto_eight_colors: bool = true; diff --git a/src/main.zig b/src/main.zig index f77f63a..7eb8ff3 100644 --- a/src/main.zig +++ b/src/main.zig @@ -103,6 +103,13 @@ const UiState = struct { password_label: Label, version_label: Label, bigclock_label: BigLabel, + show_tty: bool, + hide_key_hints: bool, + hide_numlock: bool, + hide_capslock: bool, + hide_version_string: bool, + hide_labels: bool, + hide_binds: bool, box: Box, info_line: InfoLine, animate: bool, @@ -433,6 +440,14 @@ pub fn main(init: std.process.Init) !void { }; std.posix.sigaction(std.posix.SIG.TERM, &act, null); + state.show_tty = cornersContain(state, "tty"); + state.hide_key_hints = !cornersContain(state, "keys"); + state.hide_numlock = !cornersContain(state, "numlock"); + state.hide_capslock = !cornersContain(state, "capslock"); + state.hide_version_string = !cornersContain(state, "version"); + state.hide_labels = !cornersContain(state, "labels") and !cornersContain(state, "lbl:"); + state.hide_binds = !cornersContain(state, "binds") and !cornersContain(state, "cmd:"); + // Initialize components state.shutdown_label = Label.init( "", @@ -504,7 +519,7 @@ pub fn main(init: std.process.Init) !void { ); defer state.brightness_up_label.deinit(); - if (!state.config.hide_key_hints) { + if (!state.hide_key_hints) { try state.shutdown_label.setTextAlloc( state.allocator, "{s} {s}", @@ -1052,7 +1067,7 @@ pub fn main(init: std.process.Init) !void { }; } - if (state.config.show_tty) { + if (state.show_tty) { try state.tty_label.setTextBuf(&state.tty_buf, "tty{d}", .{state.active_tty}); } @@ -1226,34 +1241,35 @@ pub fn main(init: std.process.Init) !void { state.custom_binds = .empty; defer state.custom_binds.deinit(state.allocator); - - state.custom_info = .empty; - defer state.custom_info.deinit(state.allocator); - - var lblIter = custom.labels.iterator(); - // NOTE: Because widgets have a pointer to the underlying Label, we have to ensure - // that the ArrayList doesn't allocate more memory than what we ensured. Otherwise - // the pointer to the Label becomes invalid. - try state.custom_info.ensureTotalCapacity(state.allocator, @intCast(custom.labels.count())); - while (lblIter.next()) |i| { - try state.custom_info.append(state.allocator, .{ - .info = i.value_ptr.*, - .lbl = .init("", null, state.buffer.fg, state.buffer.bg, updateCustomInfo, null), - }); - var latest = &state.custom_info.items[state.custom_info.items.len - 1]; - latest.info.id = latest.lbl.widget().id; - latest.info.counter = 1; - } - defer for (state.custom_info.items) |*item| { - item.lbl.deinit(); - }; - - var iter = custom.binds.iterator(); defer for (state.custom_binds.items) |*i| { i.lbl.deinit(); }; - if (!state.config.hide_key_hints) { + state.custom_info = .empty; + defer state.custom_info.deinit(state.allocator); + defer for (state.custom_info.items) |*item| { + item.lbl.deinit(); + }; + + if (!state.hide_labels) { + var lblIter = custom.labels.iterator(); + // NOTE: Because widgets have a pointer to the underlying Label, we have to ensure + // that the ArrayList doesn't allocate more memory than what we ensured. Otherwise + // the pointer to the Label becomes invalid. + try state.custom_info.ensureTotalCapacity(state.allocator, @intCast(custom.labels.count())); + while (lblIter.next()) |i| { + try state.custom_info.append(state.allocator, .{ + .info = i.value_ptr.*, + .lbl = .init("", null, state.buffer.fg, state.buffer.bg, updateCustomInfo, null), + }); + var latest = &state.custom_info.items[state.custom_info.items.len - 1]; + latest.info.id = latest.lbl.widget().id; + latest.info.counter = 1; + } + } + + if (!state.hide_binds) { + var iter = custom.binds.iterator(); while (iter.next()) |i| { var concat = try std.mem.concat(state.allocator, u8, &[_][]const u8{ i.key_ptr.*, " ", i.value_ptr.name }); inline for (@typeInfo(Lang).@"struct".fields) |lang_key| { @@ -1276,6 +1292,8 @@ pub fn main(init: std.process.Init) !void { }); state.custom_binds.items[state.custom_binds.items.len - 1].lbl.allocator = state.allocator; } + } + if (!state.hide_key_hints) { try layer2.append(state.allocator, state.shutdown_label.widget()); try layer2.append(state.allocator, state.restart_label.widget()); if (state.config.sleep_cmd != null) { @@ -1298,14 +1316,16 @@ pub fn main(init: std.process.Init) !void { if (state.config.clock != null) { try layer2.append(state.allocator, state.clock_label.widget()); } - if (state.config.show_tty) { + if (state.show_tty) { try layer2.append(state.allocator, state.tty_label.widget()); } if (state.config.bigclock != .none) { try layer2.append(state.allocator, state.bigclock_label.widget()); } - if (!state.config.hide_keyboard_locks) { + if (!state.hide_numlock) { try layer2.append(state.allocator, state.numlock_label.widget()); + } + if (!state.hide_capslock) { try layer2.append(state.allocator, state.capslock_label.widget()); } try layer2.append(state.allocator, state.box.widget()); @@ -1316,7 +1336,7 @@ pub fn main(init: std.process.Init) !void { try layer2.append(state.allocator, login_widget); try layer2.append(state.allocator, state.password_label.widget()); try layer2.append(state.allocator, state.password_widget); - if (!state.config.hide_version_string) { + if (!state.hide_version_string) { try layer2.append(state.allocator, state.version_label.widget()); } @@ -1408,6 +1428,15 @@ pub fn main(init: std.process.Init) !void { ); } +fn cornersContain(state: UiState, text: []const u8) bool { + const top_left = std.mem.containsAtLeast(u8, state.config.corner_top_left, 1, text); + const top_right = std.mem.containsAtLeast(u8, state.config.corner_top_right, 1, text); + const bottom_left = std.mem.containsAtLeast(u8, state.config.corner_bottom_left, 1, text); + const bottom_right = std.mem.containsAtLeast(u8, state.config.corner_bottom_right, 1, text); + + return top_left or top_right or bottom_left or bottom_right; +} + fn maxWidths(labels: [][]const u8) usize { var max_width: usize = 0; @@ -2078,8 +2107,6 @@ fn positionSingleWidget(state: *UiState, item: []const u8, current_x: *usize, cu const base_x = state.edge_margin.x; if (std.mem.eql(u8, item, "keys")) { - if (state.config.hide_key_hints) return false; - var local_x = current_x.*; var local_y = current_y; @@ -2127,7 +2154,6 @@ fn positionSingleWidget(state: *UiState, item: []const u8, current_x: *usize, cu } return true; } else if (std.mem.eql(u8, item, "tty")) { - if (!state.config.show_tty) return false; const width = TerminalBuffer.strWidth(state.tty_label.text); if (is_left) { state.tty_label.positionXY(Position.init(current_x.*, current_y)); @@ -2149,7 +2175,6 @@ fn positionSingleWidget(state: *UiState, item: []const u8, current_x: *usize, cu } return true; } else if (std.mem.eql(u8, item, "version")) { - if (state.config.hide_version_string) return false; const width = TerminalBuffer.strWidth(state.version_label.text); if (is_left) { state.version_label.positionXY(Position.init(current_x.*, current_y)); @@ -2160,7 +2185,6 @@ fn positionSingleWidget(state: *UiState, item: []const u8, current_x: *usize, cu } return true; } else if (std.mem.eql(u8, item, "numlock")) { - if (state.config.hide_keyboard_locks) return false; const width = TerminalBuffer.strWidth(state.lang.numlock); if (is_left) { state.numlock_label.positionXY(Position.init(current_x.*, current_y)); @@ -2171,7 +2195,6 @@ fn positionSingleWidget(state: *UiState, item: []const u8, current_x: *usize, cu } return true; } else if (std.mem.eql(u8, item, "capslock")) { - if (state.config.hide_keyboard_locks) return false; const width = TerminalBuffer.strWidth(state.lang.capslock); if (is_left) { state.capslock_label.positionXY(Position.init(current_x.*, current_y)); From e1696a4e73c68fa5774488e8adc2545e62e74941 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Mon, 6 Jul 2026 17:32:49 +0200 Subject: [PATCH 159/176] config: Better document corner customisation Signed-off-by: AnErrupTion --- res/config.ini | 11 ++++++++--- src/config/Config.zig | 4 ++-- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/res/config.ini b/res/config.ini index c29b50a..ae63e6f 100644 --- a/res/config.ini +++ b/res/config.ini @@ -167,7 +167,12 @@ colormix_col3 = 0x20000000 # lbl:name -> Specific custom info label # cmd:key -> Specific custom keybind hint # -# The order defines the vertical stack (first item is at the edge). +# If using a keyword that groups multiple labels into one (e.g. keys, labels, +# binds, ...), they'll be placed horizontally +# +# Also, the order defines the vertical stack (first item is at the edge) +# If items are separted by commas, they'll be placed horizontally +# It is possible to have both horizontal and vertical items on the same corner # Bottom left corner_bottom_left = version @@ -176,10 +181,10 @@ corner_bottom_left = version corner_bottom_right = labels # Top left -corner_top_left = keys +corner_top_left = keys battery # Top right -corner_top_right = clock +corner_top_right = clock numlock,capslock # For custom binds: the horizontal limit in characters for each # line of custom binds before moving on to the next. diff --git a/src/config/Config.zig b/src/config/Config.zig index 0436f5f..8ad10ae 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -41,8 +41,8 @@ colormix_col2: u32 = 0x000000FF, colormix_col3: u32 = 0x20000000, corner_bottom_left: []const u8 = "version", corner_bottom_right: []const u8 = "labels", -corner_top_left: []const u8 = "keys", -corner_top_right: []const u8 = "clock", +corner_top_left: []const u8 = "keys battery", +corner_top_right: []const u8 = "clock numlock,capslock", custom_bind_width: ?u32 = null, custom_sessions: []const u8 = build_options.config_directory ++ "/ly/custom-sessions", default_input: Input = .login, From acb532b5d4f092b9b7ebb213069616c25ba535d7 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Mon, 6 Jul 2026 18:33:13 +0200 Subject: [PATCH 160/176] migrator: Free old save path later Signed-off-by: AnErrupTion --- src/config/migrator.zig | 2 -- src/main.zig | 1 + 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/config/migrator.zig b/src/config/migrator.zig index b174a48..a2659ef 100644 --- a/src/config/migrator.zig +++ b/src/config/migrator.zig @@ -289,8 +289,6 @@ pub fn tryMigrateIniSaveFile(allocator: std.mem.Allocator, io: std.Io, path: []c fn tryMigrateFirstSaveFile(io: std.Io, user_buf: *[32]u8) ?OldSave { if (maybe_save_file) |path| { - defer temporary_allocator.free(path); - var save = OldSave{}; var file = std.Io.Dir.openFileAbsolute(io, path, .{}) catch return null; defer file.close(io); diff --git a/src/main.zig b/src/main.zig index 7eb8ff3..83aa008 100644 --- a/src/main.zig +++ b/src/main.zig @@ -1648,6 +1648,7 @@ fn authenticate(ptr: *anyopaque) !bool { // Delete previous save file if it exists if (migrator.maybe_save_file) |path| { + defer temporary_allocator.free(path); std.Io.Dir.cwd().deleteFile(state.io, path) catch {}; } else if (state.has_old_save) { std.Io.Dir.cwd().deleteFile(state.io, state.old_save_path) catch {}; From 5fc5fd62a075635ac7119dd07a3fb90a5501c83a Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Mon, 6 Jul 2026 19:44:10 +0200 Subject: [PATCH 161/176] Remove shutdown_cmd & restart_cmd + sleep & hibernate Signed-off-by: AnErrupTion --- build.zig | 4 -- ly-core/src/interop.zig | 35 +++++++++++ res/config.ini | 18 ------ src/config/Config.zig | 6 -- src/config/migrator.zig | 48 ++++++++++++++- src/main.zig | 130 ++-------------------------------------- 6 files changed, 85 insertions(+), 156 deletions(-) diff --git a/build.zig b/build.zig index a3a5f22..b4f216c 100644 --- a/build.zig +++ b/build.zig @@ -136,10 +136,6 @@ pub fn Installer(install_config: bool) type { try patch_map.put("$PREFIX_DIRECTORY", prefix_directory); try patch_map.put("$EXECUTABLE_NAME", executable_name); - // The "-a" argument doesn't exist on FreeBSD, so we use "-p" - // instead to shutdown the system. - try patch_map.put("$PLATFORM_SHUTDOWN_ARG", if (init_system == .freebsd) "-p" else "-a"); - try install_ly(allocator, io, patch_map, install_config); try install_service(allocator, io, patch_map); } diff --git a/ly-core/src/interop.zig b/ly-core/src/interop.zig index 736d8ca..873db15 100644 --- a/ly-core/src/interop.zig +++ b/ly-core/src/interop.zig @@ -179,6 +179,20 @@ fn PlatformStruct() type { return uid_range; } + pub fn shutdownSystemImpl() !void { + std.posix.system.sync(); + if (isError(std.os.linux.reboot(.MAGIC1, .MAGIC2, .POWER_OFF, null))) { + return error.CouldntShutdown; + } + } + + pub fn rebootSystemImpl() !void { + std.posix.system.sync(); + if (isError(std.os.linux.reboot(.MAGIC1, .MAGIC2, .RESTART, null))) { + return error.CouldntShutdown; + } + } + fn parseValue(comptime T: type, name: []const u8, buffer: []const u8) !T { var iterator = std.mem.splitAny(u8, buffer, " \t"); var maybe_value: ?T = null; @@ -209,6 +223,7 @@ fn PlatformStruct() type { .freebsd => struct { pub const kbio = @import("kbio"); pub const consio = @import("consio"); + pub const reboot = @import("reboot"); pub const LedState = c_int; pub const get_led_state = kbio.KDGETLED; @@ -243,6 +258,18 @@ fn PlatformStruct() type { .uid_max = FREEBSD_UID_MAX, }; } + + pub fn shutdownSystemImpl() !void { + if (isError(reboot.reboot(reboot.RB_POWEROFF))) { + return error.CouldntShutdown; + } + } + + pub fn rebootSystemImpl() !void { + if (isError(reboot.reboot(reboot.RB_AUTOBOOT))) { + return error.CouldntReboot; + } + } }, else => @compileError("Unsupported target: " ++ builtin.os.tag), }; @@ -396,3 +423,11 @@ pub fn closePasswordDatabase() void { pub fn getUserIdRange(allocator: std.mem.Allocator, io: std.Io, file_path: []const u8) !UidRange { return platform_struct.getUserIdRange(allocator, io, file_path); } + +pub fn shutdownSystem() !void { + try platform_struct.shutdownSystemImpl(); +} + +pub fn rebootSystem() !void { + try platform_struct.rebootSystemImpl(); +} diff --git a/res/config.ini b/res/config.ini index ae63e6f..0ae21b7 100644 --- a/res/config.ini +++ b/res/config.ini @@ -281,12 +281,6 @@ gameoflife_frame_delay = 6 # 0.7+ -> Dense, chaotic patterns gameoflife_initial_density = 0.4 -# Command executed when pressing hibernate key (can be null) -hibernate_cmd = null - -# Specifies the key combination used for hibernate -hibernate_key = F4 - # Remove main box borders hide_borders = false @@ -344,9 +338,6 @@ numlock = false # If null, ly doesn't set a path 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 combination used for restart restart_key = F2 @@ -375,18 +366,9 @@ shell = true # Specifies the key combination used for showing the password show_password_key = F7 -# Command executed when pressing shutdown_key -shutdown_cmd = /sbin/shutdown $PLATFORM_SHUTDOWN_ARG now - # Specifies the key combination used for shutdown shutdown_key = F1 -# Command executed when pressing sleep key (can be null) -sleep_cmd = null - -# Specifies the key combination used for sleep -sleep_key = F3 - # Command executed when starting Ly (before the TTY is taken control of) # See file at path below for an example of changing the default TTY colors start_cmd = $CONFIG_DIRECTORY/ly/startup.sh diff --git a/src/config/Config.zig b/src/config/Config.zig index 8ad10ae..f78c169 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -64,8 +64,6 @@ gameoflife_fg: u32 = 0x0000FF00, gameoflife_entropy_interval: usize = 10, gameoflife_frame_delay: usize = 6, gameoflife_initial_density: f32 = 0.4, -hibernate_cmd: ?[]const u8 = null, -hibernate_key: []const u8 = "F4", hide_borders: bool = false, inactivity_cmd: ?[]const u8 = null, inactivity_delay: u16 = 0, @@ -81,7 +79,6 @@ margin_box_h: u8 = 2, margin_box_v: u8 = 1, numlock: bool = false, path: ?[]const u8 = "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", -restart_cmd: []const u8 = "/sbin/shutdown -r now", restart_key: []const u8 = "F2", save: bool = true, service_name: [:0]const u8 = "ly", @@ -89,10 +86,7 @@ session_log: ?[]const u8 = "ly-session.log", setup_cmd: []const u8 = build_options.config_directory ++ "/ly/setup.sh", shell: bool = true, show_password_key: []const u8 = "F7", -shutdown_cmd: []const u8 = "/sbin/shutdown -a now", shutdown_key: []const u8 = "F1", -sleep_cmd: ?[]const u8 = null, -sleep_key: []const u8 = "F3", start_cmd: ?[]const u8 = null, text_in_center: bool = false, type_username: bool = false, diff --git a/src/config/migrator.zig b/src/config/migrator.zig index a2659ef..758f44f 100644 --- a/src/config/migrator.zig +++ b/src/config/migrator.zig @@ -14,6 +14,7 @@ const IniParser = ly_core.IniParser; const ini = ly_core.ini; const Config = @import("Config.zig"); +const Lang = @import("Lang.zig"); const OldSave = @import("OldSave.zig"); const SavedUsers = @import("SavedUsers.zig"); const custom = @import("custom.zig"); @@ -45,6 +46,8 @@ const removed_properties = [_][]const u8{ "wayland_cmd", "console_dev", "load", + "shutdown_cmd", + "restart_cmd", // Migrating these isn't worth the effort so just say we removed them "hide_key_hints", "hide_keyboard_locks", @@ -56,6 +59,10 @@ pub var auto_eight_colors: bool = true; pub var maybe_animate: ?bool = null; pub var maybe_save_file: ?[]const u8 = null; +pub var maybe_sleep_key: ?[]const u8 = null; +pub var maybe_sleep_cmd: ?[]const u8 = null; +pub var maybe_hibernate_key: ?[]const u8 = null; +pub var maybe_hibernate_cmd: ?[]const u8 = null; pub fn configFieldHandler(_: std.mem.Allocator, field: ini.IniField) ?ini.IniField { if (std.mem.eql(u8, field.key, "animate")) { @@ -129,7 +136,6 @@ pub fn configFieldHandler(_: std.mem.Allocator, field: ini.IniField) ?ini.IniFie if (std.mem.eql(u8, field.key, "save_file")) { // The option doesn't exist anymore, but we save its value for migration later on maybe_save_file = temporary_allocator.dupe(u8, field.value) catch return null; - return null; } @@ -168,6 +174,26 @@ pub fn configFieldHandler(_: std.mem.Allocator, field: ini.IniField) ?ini.IniFie return mapped_field; } + if (std.mem.eql(u8, field.key, "sleep_key")) { + maybe_sleep_key = temporary_allocator.dupe(u8, field.value) catch return null; + return null; + } + + if (std.mem.eql(u8, field.key, "sleep_cmd")) { + maybe_sleep_cmd = temporary_allocator.dupe(u8, field.value) catch return null; + return null; + } + + if (std.mem.eql(u8, field.key, "hibernate_key")) { + maybe_hibernate_key = temporary_allocator.dupe(u8, field.value) catch return null; + return null; + } + + if (std.mem.eql(u8, field.key, "hibernate_cmd")) { + maybe_hibernate_cmd = temporary_allocator.dupe(u8, field.value) catch return null; + return null; + } + // TODO: Dearest Melpert, // I pray this message finds you well, as daylight dwindles and the witching hour // approaches, I find it more and more imperative as time continues that I place @@ -228,7 +254,7 @@ pub fn configFieldHandler(_: std.mem.Allocator, field: ini.IniField) ?ini.IniFie // This is the stuff we only handle after reading the config. // For example, the "animate" field could come after "animation" -pub fn lateConfigFieldHandler(config: *Config) void { +pub fn lateConfigFieldHandler(config: *Config, lang: Lang) void { if (maybe_animate) |animate| { if (!animate) config.*.animation = .none; } @@ -257,6 +283,24 @@ pub fn lateConfigFieldHandler(config: *Config) void { if (!set_color_properties[7]) config.error_fg = Styling.BOLD | Color.ECOL_RED; if (!set_color_properties[8]) config.fg = Color.ECOL_WHITE; } + + if (maybe_sleep_key) |key| { + if (maybe_sleep_cmd) |cmd| { + custom.binds.put(temporary_allocator, key, .{ + .name = temporary_allocator.dupe(u8, lang.sleep) catch "", + .cmd = cmd, + }) catch {}; + } + } + + if (maybe_hibernate_key) |key| { + if (maybe_hibernate_cmd) |cmd| { + custom.binds.put(temporary_allocator, key, .{ + .name = temporary_allocator.dupe(u8, lang.hibernate) catch "", + .cmd = cmd, + }) catch {}; + } + } } pub fn tryMigrateIniSaveFile(allocator: std.mem.Allocator, io: std.Io, path: []const u8, saved_users: *SavedUsers, usernames: [][]const u8) !?IniParser(OldSave) { diff --git a/src/main.zig b/src/main.zig index 83aa008..8ae1227 100644 --- a/src/main.zig +++ b/src/main.zig @@ -88,8 +88,6 @@ const UiState = struct { labels_max_length: usize, shutdown_label: Label, restart_label: Label, - sleep_label: Label, - hibernate_label: Label, toggle_password_label: Label, brightness_down_label: Label, brightness_up_label: Label, @@ -141,9 +139,6 @@ var shutdown = false; var restart = false; pub fn main(init: std.process.Init) !void { - var shutdown_cmd: []const u8 = undefined; - var restart_cmd: []const u8 = undefined; - var commands_allocated = false; var state: UiState = undefined; state.io = init.io; @@ -152,21 +147,12 @@ pub fn main(init: std.process.Init) !void { var stderr_writer = std.Io.File.stderr().writer(state.io, &stderr_buffer); var stderr = &stderr_writer.interface; + // If we can't shutdown or restart due to an error, we print it to standard error. If that fails, just bail out defer { - // If we can't shutdown or restart due to an error, we print it to standard error. If that fails, just bail out if (shutdown) { - const shutdown_error = std.process.replace(state.io, .{ .argv = &[_][]const u8{ "/bin/sh", "-c", shutdown_cmd } }); - std.log.err("couldn't shutdown: {s}", .{@errorName(shutdown_error)}); + interop.shutdownSystem() catch std.log.err("whoopsie doodle, couldn't shutdown system!", .{}); } else if (restart) { - const restart_error = std.process.replace(state.io, .{ .argv = &[_][]const u8{ "/bin/sh", "-c", restart_cmd } }); - std.log.err("couldn't restart: {s}", .{@errorName(restart_error)}); - } else { - // The user has quit Ly using Ctrl+C - if (commands_allocated) { - // Necessary if we error out before allocating - temporary_allocator.free(shutdown_cmd); - temporary_allocator.free(restart_cmd); - } + interop.rebootSystem() catch std.log.err("whoopsie doodle, couldn't restart system!", .{}); } } @@ -293,7 +279,7 @@ pub fn main(init: std.process.Init) !void { } if (config_parser.maybe_load_error == null) { - migrator.lateConfigFieldHandler(&state.config); + migrator.lateConfigFieldHandler(&state.config, state.lang); } var maybe_uid_range_error: ?anyerror = null; @@ -374,12 +360,6 @@ pub fn main(init: std.process.Init) !void { try state.log_file.info(state.io, "tui", "using {s} vt", .{if (state.use_kmscon_vt) "kmscon" else "default"}); - // These strings only end up getting freed if the user quits Ly using Ctrl+C, which is fine since in the other cases - // we end up shutting down or restarting the system - shutdown_cmd = try temporary_allocator.dupe(u8, state.config.shutdown_cmd); - restart_cmd = try temporary_allocator.dupe(u8, state.config.restart_cmd); - commands_allocated = true; - if (state.config.start_cmd) |start_cmd| handle_start_cmd: { var process = std.process.spawn(state.io, .{ .argv = &[_][]const u8{ "/bin/sh", "-c", start_cmd }, @@ -469,26 +449,6 @@ pub fn main(init: std.process.Init) !void { ); defer state.restart_label.deinit(); - state.sleep_label = Label.init( - "", - null, - state.buffer.fg, - state.buffer.bg, - null, - null, - ); - defer state.sleep_label.deinit(); - - state.hibernate_label = Label.init( - "", - null, - state.buffer.fg, - state.buffer.bg, - null, - null, - ); - defer state.hibernate_label.deinit(); - state.toggle_password_label = Label.init( "", null, @@ -535,20 +495,6 @@ pub fn main(init: std.process.Init) !void { "{s} {s}", .{ state.config.show_password_key, state.lang.toggle_password }, ); - if (state.config.sleep_cmd != null) { - try state.sleep_label.setTextAlloc( - state.allocator, - "{s} {s}", - .{ state.config.sleep_key, state.lang.sleep }, - ); - } - if (state.config.hibernate_cmd != null) { - try state.hibernate_label.setTextAlloc( - state.allocator, - "{s} {s}", - .{ state.config.hibernate_key, state.lang.hibernate }, - ); - } if (state.config.brightness_down_key) |key| { try state.brightness_down_label.setTextAlloc( state.allocator, @@ -1296,12 +1242,6 @@ pub fn main(init: std.process.Init) !void { if (!state.hide_key_hints) { try layer2.append(state.allocator, state.shutdown_label.widget()); try layer2.append(state.allocator, state.restart_label.widget()); - if (state.config.sleep_cmd != null) { - try layer2.append(state.allocator, state.sleep_label.widget()); - } - if (state.config.hibernate_cmd != null) { - try layer2.append(state.allocator, state.hibernate_label.widget()); - } try layer2.append(state.allocator, state.toggle_password_label.widget()); if (state.config.brightness_down_key != null) { try layer2.append(state.allocator, state.brightness_down_label.widget()); @@ -1372,8 +1312,6 @@ pub fn main(init: std.process.Init) !void { try state.buffer.registerGlobalKeybind(state.io, state.config.shutdown_key, &shutdownCmd, &state); try state.buffer.registerGlobalKeybind(state.io, state.config.restart_key, &restartCmd, &state); try state.buffer.registerGlobalKeybind(state.io, state.config.show_password_key, &togglePasswordMask, &state); - if (state.config.sleep_cmd != null) try state.buffer.registerGlobalKeybind(state.io, state.config.sleep_key, &sleepCmd, &state); - if (state.config.hibernate_cmd != null) try state.buffer.registerGlobalKeybind(state.io, state.config.hibernate_key, &hibernateCmd, &state); if (state.config.brightness_down_key) |key| try state.buffer.registerGlobalKeybind(state.io, key, &decreaseBrightnessCmd, &state); if (state.config.brightness_up_key) |key| try state.buffer.registerGlobalKeybind(state.io, key, &increaseBrightnessCmd, &state); @@ -1787,62 +1725,6 @@ fn restartCmd(ptr: *anyopaque) !bool { return false; } -fn sleepCmd(ptr: *anyopaque) !bool { - var state: *UiState = @ptrCast(@alignCast(ptr)); - - if (state.config.sleep_cmd) |sleep_cmd| { - var process = std.process.spawn(state.io, .{ - .argv = &[_][]const u8{ "/bin/sh", "-c", sleep_cmd }, - .stdout = .ignore, - .stderr = .ignore, - }) catch return false; - - const process_result = process.wait(state.io) 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( - state.io, - "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 process = std.process.spawn(state.io, .{ - .argv = &[_][]const u8{ "/bin/sh", "-c", hibernate_cmd }, - .stdout = .ignore, - .stderr = .ignore, - }) catch return false; - - const process_result = process.wait(state.io) 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( - state.io, - "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)); @@ -2114,8 +1996,6 @@ fn positionSingleWidget(state: *UiState, item: []const u8, current_x: *usize, cu const labels = [_]?*Label{ &state.shutdown_label, &state.restart_label, - if (state.config.sleep_cmd != null) &state.sleep_label else null, - if (state.config.hibernate_cmd != null) &state.hibernate_label else null, &state.toggle_password_label, if (state.config.brightness_down_key != null) &state.brightness_down_label else null, if (state.config.brightness_up_key != null) &state.brightness_up_label else null, @@ -2347,8 +2227,6 @@ fn positionWidgets(ptr: *anyopaque) !void { // Reset all potential corner widgets to offscreen state.shutdown_label.positionXY(offscreen); state.restart_label.positionXY(offscreen); - state.sleep_label.positionXY(offscreen); - state.hibernate_label.positionXY(offscreen); state.toggle_password_label.positionXY(offscreen); state.brightness_down_label.positionXY(offscreen); state.brightness_up_label.positionXY(offscreen); From 26377fd31908a6ef9a1e62965930ea27db525b6a Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Mon, 6 Jul 2026 19:56:24 +0200 Subject: [PATCH 162/176] FreeBSD: Fix compilation Signed-off-by: AnErrupTion --- ly-core/build.zig | 1 + ly-core/src/interop.zig | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/ly-core/build.zig b/ly-core/build.zig index 75ecb3f..f62fb2e 100644 --- a/ly-core/build.zig +++ b/ly-core/build.zig @@ -52,6 +52,7 @@ pub fn build(b: *std.Build) void { addCImport(b, mod, translate_c, target, optimize, "kbio", "#include "); addCImport(b, mod, translate_c, target, optimize, "consio", "#include "); addCImport(b, mod, translate_c, target, optimize, "sysctl", "#include "); + addCImport(b, mod, translate_c, target, optimize, "reboot", "#include "); } const mod_tests = b.addTest(.{ diff --git a/ly-core/src/interop.zig b/ly-core/src/interop.zig index 873db15..f04b2a4 100644 --- a/ly-core/src/interop.zig +++ b/ly-core/src/interop.zig @@ -260,13 +260,13 @@ fn PlatformStruct() type { } pub fn shutdownSystemImpl() !void { - if (isError(reboot.reboot(reboot.RB_POWEROFF))) { + if (isError(unistd.reboot(reboot.RB_POWEROFF))) { return error.CouldntShutdown; } } pub fn rebootSystemImpl() !void { - if (isError(reboot.reboot(reboot.RB_AUTOBOOT))) { + if (isError(unistd.reboot(reboot.RB_AUTOBOOT))) { return error.CouldntReboot; } } From 7e6474924c438197aa6dc603e1677465bed74eb5 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Mon, 6 Jul 2026 19:57:29 +0200 Subject: [PATCH 163/176] config: Order battery_id alphabetically Signed-off-by: AnErrupTion --- res/config.ini | 12 ++++++------ src/config/Config.zig | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/res/config.ini b/res/config.ini index 0ae21b7..5c63043 100644 --- a/res/config.ini +++ b/res/config.ini @@ -47,12 +47,6 @@ asterisk = * # If set to 0, the animation will never be played auth_fails = 10 -# Identifier for battery whose charge to display at top left -# Primary battery is usually BAT0 or BAT1 -# If set to null, battery status won't be shown -# Unused on FreeBSD (a sysctl is used there) -battery_id = null - # Automatic login configuration # This feature allows Ly to automatically log in a user without password prompt. # IMPORTANT: Both auto_login_user and auto_login_session must be set for this to work. @@ -77,6 +71,12 @@ auto_login_session = null # If null, automatic login is disabled auto_login_user = null +# Identifier for battery whose charge to display at top left +# Primary battery is usually BAT0 or BAT1 +# If set to null, battery status won't be shown +# Unused on FreeBSD (a sysctl is used there) +battery_id = null + # Background color id bg = 0x00000000 diff --git a/src/config/Config.zig b/src/config/Config.zig index f78c169..1be16ec 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -13,10 +13,10 @@ animation_frame_delay: u16 = 5, animation_timeout_sec: u12 = 0, asterisk: ?u32 = '*', auth_fails: u64 = 10, -battery_id: ?[]const u8 = null, auto_login_service: [:0]const u8 = "ly-autologin", auto_login_session: ?[]const u8 = null, auto_login_user: ?[]const u8 = null, +battery_id: ?[]const u8 = null, bg: u32 = 0x00000000, bigclock: Bigclock = .none, bigclock_12hr: bool = false, From 54f1ff14eea4518ea723bf63bf444123f3a57e9d Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Mon, 6 Jul 2026 20:17:46 +0200 Subject: [PATCH 164/176] corners: Break up "keys" into individual keys Signed-off-by: AnErrupTion --- res/config.ini | 12 ++-- src/config/Config.zig | 2 +- src/main.zig | 124 ++++++++++++++++++++++++++++-------------- 3 files changed, 91 insertions(+), 47 deletions(-) diff --git a/res/config.ini b/res/config.ini index 5c63043..445022e 100644 --- a/res/config.ini +++ b/res/config.ini @@ -155,7 +155,11 @@ colormix_col3 = 0x20000000 # Screen corners customization # Keywords: -# keys -> Power management, brightness control & toggle password keys +# shutdown -> Shutdown key +# restart -> Restart key +# britup -> Brightness up key +# britdown -> Brightness down key +# password -> Toggle password key # clock -> Clock (format defined by 'clock' option) # tty -> Active TTY number # battery -> Battery percentage @@ -167,8 +171,8 @@ colormix_col3 = 0x20000000 # lbl:name -> Specific custom info label # cmd:key -> Specific custom keybind hint # -# If using a keyword that groups multiple labels into one (e.g. keys, labels, -# binds, ...), they'll be placed horizontally +# If using a keyword that groups multiple labels into one (e.g. labels, binds), +# they'll be placed horizontally # # Also, the order defines the vertical stack (first item is at the edge) # If items are separted by commas, they'll be placed horizontally @@ -181,7 +185,7 @@ corner_bottom_left = version corner_bottom_right = labels # Top left -corner_top_left = keys battery +corner_top_left = shutdown,restart,britup,britdown,password battery # Top right corner_top_right = clock numlock,capslock diff --git a/src/config/Config.zig b/src/config/Config.zig index 1be16ec..a68458d 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -41,7 +41,7 @@ colormix_col2: u32 = 0x000000FF, colormix_col3: u32 = 0x20000000, corner_bottom_left: []const u8 = "version", corner_bottom_right: []const u8 = "labels", -corner_top_left: []const u8 = "keys battery", +corner_top_left: []const u8 = "shutdown,restart,britup,britdown,password battery", corner_top_right: []const u8 = "clock numlock,capslock", custom_bind_width: ?u32 = null, custom_sessions: []const u8 = build_options.config_directory ++ "/ly/custom-sessions", diff --git a/src/main.zig b/src/main.zig index 8ae1227..b008e1c 100644 --- a/src/main.zig +++ b/src/main.zig @@ -102,7 +102,9 @@ const UiState = struct { version_label: Label, bigclock_label: BigLabel, show_tty: bool, - hide_key_hints: bool, + hide_shutdown: bool, + hide_restart: bool, + hide_toggle_password: bool, hide_numlock: bool, hide_capslock: bool, hide_version_string: bool, @@ -421,7 +423,9 @@ pub fn main(init: std.process.Init) !void { std.posix.sigaction(std.posix.SIG.TERM, &act, null); state.show_tty = cornersContain(state, "tty"); - state.hide_key_hints = !cornersContain(state, "keys"); + state.hide_shutdown = !cornersContain(state, "shutdown"); + state.hide_restart = !cornersContain(state, "restart"); + state.hide_toggle_password = !cornersContain(state, "password"); state.hide_numlock = !cornersContain(state, "numlock"); state.hide_capslock = !cornersContain(state, "capslock"); state.hide_version_string = !cornersContain(state, "version"); @@ -479,22 +483,28 @@ pub fn main(init: std.process.Init) !void { ); defer state.brightness_up_label.deinit(); - if (!state.hide_key_hints) { + if (!state.hide_shutdown) { try state.shutdown_label.setTextAlloc( state.allocator, "{s} {s}", .{ state.config.shutdown_key, state.lang.shutdown }, ); + } + if (!state.hide_restart) { try state.restart_label.setTextAlloc( state.allocator, "{s} {s}", .{ state.config.restart_key, state.lang.restart }, ); + } + if (!state.hide_toggle_password) { try state.toggle_password_label.setTextAlloc( state.allocator, "{s} {s}", .{ state.config.show_password_key, state.lang.toggle_password }, ); + } + if (state.config.brightness_down_key != null) { if (state.config.brightness_down_key) |key| { try state.brightness_down_label.setTextAlloc( state.allocator, @@ -502,6 +512,8 @@ pub fn main(init: std.process.Init) !void { .{ key, state.lang.brightness_down }, ); } + } + if (state.config.brightness_up_key != null) { if (state.config.brightness_up_key) |key| { try state.brightness_up_label.setTextAlloc( state.allocator, @@ -1239,16 +1251,26 @@ pub fn main(init: std.process.Init) !void { state.custom_binds.items[state.custom_binds.items.len - 1].lbl.allocator = state.allocator; } } - if (!state.hide_key_hints) { + if (!state.hide_shutdown) { try layer2.append(state.allocator, state.shutdown_label.widget()); + } + if (!state.hide_restart) { try layer2.append(state.allocator, state.restart_label.widget()); + } + if (!state.hide_shutdown) { + try layer2.append(state.allocator, state.shutdown_label.widget()); + } + if (!state.hide_restart) { + try layer2.append(state.allocator, state.restart_label.widget()); + } + if (!state.hide_toggle_password) { try layer2.append(state.allocator, state.toggle_password_label.widget()); - if (state.config.brightness_down_key != null) { - try layer2.append(state.allocator, state.brightness_down_label.widget()); - } - if (state.config.brightness_up_key != null) { - try layer2.append(state.allocator, state.brightness_up_label.widget()); - } + } + if (state.config.brightness_down_key != null) { + try layer2.append(state.allocator, state.brightness_down_label.widget()); + } + if (state.config.brightness_up_key != null) { + try layer2.append(state.allocator, state.brightness_up_label.widget()); } if (state.config.battery_id != null) { try layer2.append(state.allocator, state.battery_label.widget()); @@ -1989,39 +2011,57 @@ const PositionedWidgets = struct { fn positionSingleWidget(state: *UiState, item: []const u8, current_x: *usize, current_y: usize, is_left: bool, is_top: bool, positioned: *PositionedWidgets) !bool { const base_x = state.edge_margin.x; - if (std.mem.eql(u8, item, "keys")) { - var local_x = current_x.*; - var local_y = current_y; - - const labels = [_]?*Label{ - &state.shutdown_label, - &state.restart_label, - &state.toggle_password_label, - if (state.config.brightness_down_key != null) &state.brightness_down_label else null, - if (state.config.brightness_up_key != null) &state.brightness_up_label else null, - }; - - for (labels) |maybe_label| { - const label = maybe_label orelse continue; - const width = TerminalBuffer.strWidth(label.text); - - if (is_left) { - if (local_x + width > state.buffer.width - state.edge_margin.x) { - local_x = base_x; - if (is_top) local_y += 1 else local_y -= 1; - } - label.positionXY(Position.init(local_x, local_y)); - local_x += width + 1; - } else { - if (width + state.edge_margin.x > local_x) { - local_x = state.buffer.width - state.edge_margin.x; - if (is_top) local_y += 1 else local_y -= 1; - } - label.positionXY(Position.init(local_x - width, local_y)); - local_x -= width + 1; - } + if (std.mem.eql(u8, item, "shutdown")) { + const width = TerminalBuffer.strWidth(state.shutdown_label.text); + if (is_left) { + state.shutdown_label.positionXY(Position.init(current_x.*, current_y)); + current_x.* += width + 1; + } else { + state.shutdown_label.positionXY(Position.init(current_x.* - width, current_y)); + current_x.* -= width + 1; + } + return true; + } else if (std.mem.eql(u8, item, "restart")) { + const width = TerminalBuffer.strWidth(state.restart_label.text); + if (is_left) { + state.restart_label.positionXY(Position.init(current_x.*, current_y)); + current_x.* += width + 1; + } else { + state.restart_label.positionXY(Position.init(current_x.* - width, current_y)); + current_x.* -= width + 1; + } + return true; + } else if (std.mem.eql(u8, item, "britup")) { + if (state.config.brightness_up_key == null) return false; + const width = TerminalBuffer.strWidth(state.brightness_up_label.text); + if (is_left) { + state.brightness_up_label.positionXY(Position.init(current_x.*, current_y)); + current_x.* += width + 1; + } else { + state.brightness_up_label.positionXY(Position.init(current_x.* - width, current_y)); + current_x.* -= width + 1; + } + return true; + } else if (std.mem.eql(u8, item, "britdown")) { + if (state.config.brightness_down_key == null) return false; + const width = TerminalBuffer.strWidth(state.brightness_down_label.text); + if (is_left) { + state.brightness_down_label.positionXY(Position.init(current_x.*, current_y)); + current_x.* += width + 1; + } else { + state.brightness_down_label.positionXY(Position.init(current_x.* - width, current_y)); + current_x.* -= width + 1; + } + return true; + } else if (std.mem.eql(u8, item, "password")) { + const width = TerminalBuffer.strWidth(state.toggle_password_label.text); + if (is_left) { + state.toggle_password_label.positionXY(Position.init(current_x.*, current_y)); + current_x.* += width + 1; + } else { + state.toggle_password_label.positionXY(Position.init(current_x.* - width, current_y)); + current_x.* -= width + 1; } - current_x.* = local_x; return true; } else if (std.mem.eql(u8, item, "clock") or std.mem.eql(u8, item, "time")) { if (state.config.clock == null) return false; From 018d797ed67327b083af4b6b9084beeb17a439ab Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Mon, 6 Jul 2026 20:19:40 +0200 Subject: [PATCH 165/176] Brother what am I doing Signed-off-by: AnErrupTion --- src/main.zig | 28 ++++++++++++---------------- 1 file changed, 12 insertions(+), 16 deletions(-) diff --git a/src/main.zig b/src/main.zig index b008e1c..b1a3957 100644 --- a/src/main.zig +++ b/src/main.zig @@ -504,23 +504,19 @@ pub fn main(init: std.process.Init) !void { .{ state.config.show_password_key, state.lang.toggle_password }, ); } - if (state.config.brightness_down_key != null) { - if (state.config.brightness_down_key) |key| { - try state.brightness_down_label.setTextAlloc( - state.allocator, - "{s} {s}", - .{ key, state.lang.brightness_down }, - ); - } + if (state.config.brightness_down_key) |key| { + try state.brightness_down_label.setTextAlloc( + state.allocator, + "{s} {s}", + .{ key, state.lang.brightness_down }, + ); } - if (state.config.brightness_up_key != null) { - if (state.config.brightness_up_key) |key| { - try state.brightness_up_label.setTextAlloc( - state.allocator, - "{s} {s}", - .{ key, state.lang.brightness_up }, - ); - } + if (state.config.brightness_up_key) |key| { + try state.brightness_up_label.setTextAlloc( + state.allocator, + "{s} {s}", + .{ key, state.lang.brightness_up }, + ); } state.numlock_label = Label.init( From e4e399cd076f1ce2bfd9295a2b0f763aa514f52b Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Mon, 6 Jul 2026 21:18:49 +0200 Subject: [PATCH 166/176] Allow toggling visibility of shutdown, restart & show password keybinds (closes #993) Signed-off-by: AnErrupTion --- res/config.ini | 9 +++++++-- src/config/Config.zig | 6 +++--- src/main.zig | 43 ++++++++++++++++--------------------------- 3 files changed, 26 insertions(+), 32 deletions(-) diff --git a/res/config.ini b/res/config.ini index 445022e..081e534 100644 --- a/res/config.ini +++ b/res/config.ini @@ -114,13 +114,15 @@ box_title = null # Brightness decrease command brightness_down_cmd = $PREFIX_DIRECTORY/bin/brightnessctl -q -n s 10%- -# Brightness decrease key combination, or null to disable +# Brightness decrease key combination +# If null, the keybind is disabled and isn't shown brightness_down_key = F5 # Brightness increase command brightness_up_cmd = $PREFIX_DIRECTORY/bin/brightnessctl -q -n s +10% -# Brightness increase key combination, or null to disable +# Brightness increase key combination +# If null, the keybind is disabled and isn't shown brightness_up_key = F6 # Erase password input on failure @@ -343,6 +345,7 @@ numlock = false path = /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin # Specifies the key combination used for restart +# If null, the keybind is disabled and isn't shown restart_key = F2 # Save the current desktop and login as defaults, and load them on startup @@ -368,9 +371,11 @@ setup_cmd = $CONFIG_DIRECTORY/ly/setup.sh shell = true # Specifies the key combination used for showing the password +# If null, the keybind is disabled and isn't shown show_password_key = F7 # Specifies the key combination used for shutdown +# If null, the keybind is disabled and isn't shown shutdown_key = F1 # Command executed when starting Ly (before the TTY is taken control of) diff --git a/src/config/Config.zig b/src/config/Config.zig index a68458d..32e1b97 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -79,14 +79,14 @@ margin_box_h: u8 = 2, margin_box_v: u8 = 1, numlock: bool = false, path: ?[]const u8 = "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", -restart_key: []const u8 = "F2", +restart_key: ?[]const u8 = "F2", save: bool = true, service_name: [:0]const u8 = "ly", session_log: ?[]const u8 = "ly-session.log", setup_cmd: []const u8 = build_options.config_directory ++ "/ly/setup.sh", shell: bool = true, -show_password_key: []const u8 = "F7", -shutdown_key: []const u8 = "F1", +show_password_key: ?[]const u8 = "F7", +shutdown_key: ?[]const u8 = "F1", start_cmd: ?[]const u8 = null, text_in_center: bool = false, type_username: bool = false, diff --git a/src/main.zig b/src/main.zig index b1a3957..471bb6e 100644 --- a/src/main.zig +++ b/src/main.zig @@ -102,9 +102,6 @@ const UiState = struct { version_label: Label, bigclock_label: BigLabel, show_tty: bool, - hide_shutdown: bool, - hide_restart: bool, - hide_toggle_password: bool, hide_numlock: bool, hide_capslock: bool, hide_version_string: bool, @@ -423,9 +420,6 @@ pub fn main(init: std.process.Init) !void { std.posix.sigaction(std.posix.SIG.TERM, &act, null); state.show_tty = cornersContain(state, "tty"); - state.hide_shutdown = !cornersContain(state, "shutdown"); - state.hide_restart = !cornersContain(state, "restart"); - state.hide_toggle_password = !cornersContain(state, "password"); state.hide_numlock = !cornersContain(state, "numlock"); state.hide_capslock = !cornersContain(state, "capslock"); state.hide_version_string = !cornersContain(state, "version"); @@ -483,25 +477,25 @@ pub fn main(init: std.process.Init) !void { ); defer state.brightness_up_label.deinit(); - if (!state.hide_shutdown) { + if (state.config.shutdown_key) |key| { try state.shutdown_label.setTextAlloc( state.allocator, "{s} {s}", - .{ state.config.shutdown_key, state.lang.shutdown }, + .{ key, state.lang.shutdown }, ); } - if (!state.hide_restart) { + if (state.config.restart_key) |key| { try state.restart_label.setTextAlloc( state.allocator, "{s} {s}", - .{ state.config.restart_key, state.lang.restart }, + .{ key, state.lang.restart }, ); } - if (!state.hide_toggle_password) { + if (state.config.show_password_key) |key| { try state.toggle_password_label.setTextAlloc( state.allocator, "{s} {s}", - .{ state.config.show_password_key, state.lang.toggle_password }, + .{ key, state.lang.toggle_password }, ); } if (state.config.brightness_down_key) |key| { @@ -1021,9 +1015,7 @@ pub fn main(init: std.process.Init) !void { }; } - if (state.show_tty) { - try state.tty_label.setTextBuf(&state.tty_buf, "tty{d}", .{state.active_tty}); - } + try state.tty_label.setTextBuf(&state.tty_buf, "tty{d}", .{state.active_tty}); // Initialize the animation, if any var animation: ?*Widget = null; @@ -1247,19 +1239,13 @@ pub fn main(init: std.process.Init) !void { state.custom_binds.items[state.custom_binds.items.len - 1].lbl.allocator = state.allocator; } } - if (!state.hide_shutdown) { + if (state.config.shutdown_key != null) { try layer2.append(state.allocator, state.shutdown_label.widget()); } - if (!state.hide_restart) { + if (state.config.restart_key != null) { try layer2.append(state.allocator, state.restart_label.widget()); } - if (!state.hide_shutdown) { - try layer2.append(state.allocator, state.shutdown_label.widget()); - } - if (!state.hide_restart) { - try layer2.append(state.allocator, state.restart_label.widget()); - } - if (!state.hide_toggle_password) { + if (state.config.show_password_key != null) { try layer2.append(state.allocator, state.toggle_password_label.widget()); } if (state.config.brightness_down_key != null) { @@ -1327,9 +1313,9 @@ pub fn main(init: std.process.Init) !void { try state.buffer.registerGlobalKeybind(state.io, "Enter", &authenticate, &state); - try state.buffer.registerGlobalKeybind(state.io, state.config.shutdown_key, &shutdownCmd, &state); - try state.buffer.registerGlobalKeybind(state.io, state.config.restart_key, &restartCmd, &state); - try state.buffer.registerGlobalKeybind(state.io, state.config.show_password_key, &togglePasswordMask, &state); + if (state.config.shutdown_key) |key| try state.buffer.registerGlobalKeybind(state.io, key, &shutdownCmd, &state); + if (state.config.restart_key) |key| try state.buffer.registerGlobalKeybind(state.io, key, &restartCmd, &state); + if (state.config.show_password_key) |key| try state.buffer.registerGlobalKeybind(state.io, key, &togglePasswordMask, &state); if (state.config.brightness_down_key) |key| try state.buffer.registerGlobalKeybind(state.io, key, &decreaseBrightnessCmd, &state); if (state.config.brightness_up_key) |key| try state.buffer.registerGlobalKeybind(state.io, key, &increaseBrightnessCmd, &state); @@ -2008,6 +1994,7 @@ fn positionSingleWidget(state: *UiState, item: []const u8, current_x: *usize, cu const base_x = state.edge_margin.x; if (std.mem.eql(u8, item, "shutdown")) { + if (state.config.shutdown_key == null) return false; const width = TerminalBuffer.strWidth(state.shutdown_label.text); if (is_left) { state.shutdown_label.positionXY(Position.init(current_x.*, current_y)); @@ -2018,6 +2005,7 @@ fn positionSingleWidget(state: *UiState, item: []const u8, current_x: *usize, cu } return true; } else if (std.mem.eql(u8, item, "restart")) { + if (state.config.restart_key == null) return false; const width = TerminalBuffer.strWidth(state.restart_label.text); if (is_left) { state.restart_label.positionXY(Position.init(current_x.*, current_y)); @@ -2050,6 +2038,7 @@ fn positionSingleWidget(state: *UiState, item: []const u8, current_x: *usize, cu } return true; } else if (std.mem.eql(u8, item, "password")) { + if (state.config.show_password_key == null) return false; const width = TerminalBuffer.strWidth(state.toggle_password_label.text); if (is_left) { state.toggle_password_label.positionXY(Position.init(current_x.*, current_y)); From a41ebe8f6f33307b3445b91ece675a0f5e4189f4 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Mon, 6 Jul 2026 21:33:30 +0200 Subject: [PATCH 167/176] Remove cornersContain() Signed-off-by: AnErrupTion --- src/main.zig | 143 +++++++++++++++++---------------------------------- 1 file changed, 47 insertions(+), 96 deletions(-) diff --git a/src/main.zig b/src/main.zig index 471bb6e..73861c4 100644 --- a/src/main.zig +++ b/src/main.zig @@ -101,12 +101,6 @@ const UiState = struct { password_label: Label, version_label: Label, bigclock_label: BigLabel, - show_tty: bool, - hide_numlock: bool, - hide_capslock: bool, - hide_version_string: bool, - hide_labels: bool, - hide_binds: bool, box: Box, info_line: InfoLine, animate: bool, @@ -419,13 +413,6 @@ pub fn main(init: std.process.Init) !void { }; std.posix.sigaction(std.posix.SIG.TERM, &act, null); - state.show_tty = cornersContain(state, "tty"); - state.hide_numlock = !cornersContain(state, "numlock"); - state.hide_capslock = !cornersContain(state, "capslock"); - state.hide_version_string = !cornersContain(state, "version"); - state.hide_labels = !cornersContain(state, "labels") and !cornersContain(state, "lbl:"); - state.hide_binds = !cornersContain(state, "binds") and !cornersContain(state, "cmd:"); - // Initialize components state.shutdown_label = Label.init( "", @@ -1197,81 +1184,55 @@ pub fn main(init: std.process.Init) !void { item.lbl.deinit(); }; - if (!state.hide_labels) { - var lblIter = custom.labels.iterator(); - // NOTE: Because widgets have a pointer to the underlying Label, we have to ensure - // that the ArrayList doesn't allocate more memory than what we ensured. Otherwise - // the pointer to the Label becomes invalid. - try state.custom_info.ensureTotalCapacity(state.allocator, @intCast(custom.labels.count())); - while (lblIter.next()) |i| { - try state.custom_info.append(state.allocator, .{ - .info = i.value_ptr.*, - .lbl = .init("", null, state.buffer.fg, state.buffer.bg, updateCustomInfo, null), - }); - var latest = &state.custom_info.items[state.custom_info.items.len - 1]; - latest.info.id = latest.lbl.widget().id; - latest.info.counter = 1; - } + var lblIter = custom.labels.iterator(); + // NOTE: Because widgets have a pointer to the underlying Label, we have to ensure + // that the ArrayList doesn't allocate more memory than what we ensured. Otherwise + // the pointer to the Label becomes invalid. + try state.custom_info.ensureTotalCapacity(state.allocator, @intCast(custom.labels.count())); + while (lblIter.next()) |i| { + try state.custom_info.append(state.allocator, .{ + .info = i.value_ptr.*, + .lbl = .init("", null, state.buffer.fg, state.buffer.bg, updateCustomInfo, null), + }); + var latest = &state.custom_info.items[state.custom_info.items.len - 1]; + latest.info.id = latest.lbl.widget().id; + latest.info.counter = 1; } - if (!state.hide_binds) { - var iter = custom.binds.iterator(); - while (iter.next()) |i| { - var concat = try std.mem.concat(state.allocator, u8, &[_][]const u8{ i.key_ptr.*, " ", i.value_ptr.name }); - inline for (@typeInfo(Lang).@"struct".fields) |lang_key| { - const new = try std.mem.replaceOwned(u8, state.allocator, concat, "$" ++ lang_key.name, @field(state.lang, lang_key.name)); - state.allocator.free(concat); - concat = new; - } - try state.custom_binds.append(state.allocator, .{ - .lbl = .init( - concat, - null, - state.buffer.fg, - state.buffer.bg, - null, - null, - ), - .cmd = i.value_ptr.*, - .key = i.key_ptr.*, - .io = state.io, - }); - state.custom_binds.items[state.custom_binds.items.len - 1].lbl.allocator = state.allocator; + var iter = custom.binds.iterator(); + while (iter.next()) |i| { + var concat = try std.mem.concat(state.allocator, u8, &[_][]const u8{ i.key_ptr.*, " ", i.value_ptr.name }); + inline for (@typeInfo(Lang).@"struct".fields) |lang_key| { + const new = try std.mem.replaceOwned(u8, state.allocator, concat, "$" ++ lang_key.name, @field(state.lang, lang_key.name)); + state.allocator.free(concat); + concat = new; } + try state.custom_binds.append(state.allocator, .{ + .lbl = .init( + concat, + null, + state.buffer.fg, + state.buffer.bg, + null, + null, + ), + .cmd = i.value_ptr.*, + .key = i.key_ptr.*, + .io = state.io, + }); + state.custom_binds.items[state.custom_binds.items.len - 1].lbl.allocator = state.allocator; } - if (state.config.shutdown_key != null) { - try layer2.append(state.allocator, state.shutdown_label.widget()); - } - if (state.config.restart_key != null) { - try layer2.append(state.allocator, state.restart_label.widget()); - } - if (state.config.show_password_key != null) { - try layer2.append(state.allocator, state.toggle_password_label.widget()); - } - if (state.config.brightness_down_key != null) { - try layer2.append(state.allocator, state.brightness_down_label.widget()); - } - if (state.config.brightness_up_key != null) { - try layer2.append(state.allocator, state.brightness_up_label.widget()); - } - if (state.config.battery_id != null) { - try layer2.append(state.allocator, state.battery_label.widget()); - } - if (state.config.clock != null) { - try layer2.append(state.allocator, state.clock_label.widget()); - } - if (state.show_tty) { - try layer2.append(state.allocator, state.tty_label.widget()); - } - if (state.config.bigclock != .none) { - try layer2.append(state.allocator, state.bigclock_label.widget()); - } - if (!state.hide_numlock) { - try layer2.append(state.allocator, state.numlock_label.widget()); - } - if (!state.hide_capslock) { - try layer2.append(state.allocator, state.capslock_label.widget()); - } + try layer2.append(state.allocator, state.shutdown_label.widget()); + try layer2.append(state.allocator, state.restart_label.widget()); + try layer2.append(state.allocator, state.toggle_password_label.widget()); + try layer2.append(state.allocator, state.brightness_down_label.widget()); + try layer2.append(state.allocator, state.brightness_up_label.widget()); + try layer2.append(state.allocator, state.battery_label.widget()); + try layer2.append(state.allocator, state.clock_label.widget()); + try layer2.append(state.allocator, state.tty_label.widget()); + try layer2.append(state.allocator, state.bigclock_label.widget()); + try layer2.append(state.allocator, state.numlock_label.widget()); + try layer2.append(state.allocator, state.capslock_label.widget()); try layer2.append(state.allocator, state.box.widget()); try layer2.append(state.allocator, info_line_widget); try layer2.append(state.allocator, state.session_specifier_label.widget()); @@ -1280,9 +1241,7 @@ pub fn main(init: std.process.Init) !void { try layer2.append(state.allocator, login_widget); try layer2.append(state.allocator, state.password_label.widget()); try layer2.append(state.allocator, state.password_widget); - if (!state.hide_version_string) { - try layer2.append(state.allocator, state.version_label.widget()); - } + try layer2.append(state.allocator, state.version_label.widget()); for (state.custom_binds.items) |*item| { try layer2.append(state.allocator, item.lbl.widget()); @@ -1370,15 +1329,6 @@ pub fn main(init: std.process.Init) !void { ); } -fn cornersContain(state: UiState, text: []const u8) bool { - const top_left = std.mem.containsAtLeast(u8, state.config.corner_top_left, 1, text); - const top_right = std.mem.containsAtLeast(u8, state.config.corner_top_right, 1, text); - const bottom_left = std.mem.containsAtLeast(u8, state.config.corner_bottom_left, 1, text); - const bottom_right = std.mem.containsAtLeast(u8, state.config.corner_bottom_right, 1, text); - - return top_left or top_right or bottom_left or bottom_right; -} - fn maxWidths(labels: [][]const u8) usize { var max_width: usize = 0; @@ -2256,6 +2206,7 @@ fn positionWidgets(ptr: *anyopaque) !void { state.brightness_down_label.positionXY(offscreen); state.brightness_up_label.positionXY(offscreen); state.clock_label.positionXY(offscreen); + state.bigclock_label.positionXY(offscreen); state.tty_label.positionXY(offscreen); state.battery_label.positionXY(offscreen); state.version_label.positionXY(offscreen); From 78c7c2e2e85b2c7e1c05ecf62fe1104fb4763105 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Mon, 6 Jul 2026 22:11:52 +0200 Subject: [PATCH 168/176] Decouple save file path from config directory (closes #1025) Signed-off-by: AnErrupTion --- res/config.ini | 5 +++-- src/config/Config.zig | 2 +- src/config/migrator.zig | 14 ++++++++++++++ src/main.zig | 14 +++++++------- 4 files changed, 25 insertions(+), 10 deletions(-) diff --git a/res/config.ini b/res/config.ini index 081e534..8924b1b 100644 --- a/res/config.ini +++ b/res/config.ini @@ -348,8 +348,9 @@ path = /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin # If null, the keybind is disabled and isn't shown restart_key = F2 -# Save the current desktop and login as defaults, and load them on startup -save = true +# Absolute directory for the save file +# If null, current desktop & login won't be saved nor loaded +save_file_dir = $CONFIG_DIRECTORY/ly # Service name (set to ly to use the provided pam config file) service_name = ly diff --git a/src/config/Config.zig b/src/config/Config.zig index 32e1b97..90913ad 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -80,7 +80,7 @@ margin_box_v: u8 = 1, numlock: bool = false, path: ?[]const u8 = "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", restart_key: ?[]const u8 = "F2", -save: bool = true, +save_file_dir: ?[]const u8 = build_options.config_directory ++ "/ly", service_name: [:0]const u8 = "ly", session_log: ?[]const u8 = "ly-session.log", setup_cmd: []const u8 = build_options.config_directory ++ "/ly/setup.sh", diff --git a/src/config/migrator.zig b/src/config/migrator.zig index 758f44f..4ed4d2e 100644 --- a/src/config/migrator.zig +++ b/src/config/migrator.zig @@ -2,6 +2,7 @@ // Properties removed or changed since 0.6.0 // Color codes interpreted differently since 1.1.0 +const build_options = @import("build_options"); const std = @import("std"); var temporary_allocator = std.heap.page_allocator; @@ -194,6 +195,19 @@ pub fn configFieldHandler(_: std.mem.Allocator, field: ini.IniField) ?ini.IniFie return null; } + if (std.mem.eql(u8, field.key, "save")) { + // The option now uses a string for the file's parent directory + var mapped_field = field; + + if (std.mem.eql(u8, field.value, "true")) { + mapped_field.value = build_options.config_directory ++ "/ly"; + } else if (std.mem.eql(u8, field.value, "false")) { + mapped_field.value = "null"; + } + + return mapped_field; + } + // TODO: Dearest Melpert, // I pray this message finds you well, as daylight dwindles and the witching hour // approaches, I find it more and more imperative as time continues that I place diff --git a/src/main.zig b/src/main.zig index 73861c4..85ababf 100644 --- a/src/main.zig +++ b/src/main.zig @@ -224,7 +224,7 @@ pub fn main(init: std.process.Init) !void { } // Load configuration file - defer if (state.config.save) { + defer if (state.config.save_file_dir != null) { state.allocator.free(state.save_path); state.allocator.free(state.old_save_path); }; @@ -266,8 +266,8 @@ pub fn main(init: std.process.Init) !void { state.lang = lang_parser.structure; - if (state.config.save) { - state.save_path = try std.Io.Dir.path.join(state.allocator, &[_][]const u8{ config_parent_path, "save.txt" }); + if (state.config.save_file_dir) |dir| { + state.save_path = try std.Io.Dir.path.join(state.allocator, &[_][]const u8{ dir, "save.txt" }); state.old_save_path = try std.Io.Dir.path.join(state.allocator, &[_][]const u8{ config_parent_path, "save.ini" }); } @@ -285,7 +285,7 @@ pub fn main(init: std.process.Init) !void { state.has_old_save = false; state.saved_username = null; - if (state.config.save) read_save_file: { + if (state.config.save_file_dir != null) read_save_file: { old_save_parser = migrator.tryMigrateIniSaveFile(state.allocator, state.io, state.old_save_path, &state.saved_users, usernames.items) catch break :read_save_file; // Don't read the new save file if the old one still exists @@ -335,7 +335,7 @@ pub fn main(init: std.process.Init) !void { // If no save file previously existed, fill it up with all usernames // TODO: Add new username with existing save file - if (state.config.save and state.saved_users.user_list.items.len == 0) { + if (state.config.save_file_dir != null and state.saved_users.user_list.items.len == 0) { for (usernames.items) |user| { try state.saved_users.user_list.append(state.allocator, .{ .username = user, @@ -1118,7 +1118,7 @@ pub fn main(init: std.process.Init) !void { // Skip if autologin is active to prevent overriding autologin session var default_input = state.config.default_input; - if (state.config.save and !state.is_autologin) { + if (state.config.save_file_dir != null and !state.is_autologin) { if (state.login_text) |box| { if (state.saved_username) |username| { defer state.allocator.free(username); @@ -1502,7 +1502,7 @@ fn authenticate(ptr: *anyopaque) !bool { try TerminalBuffer.presentBuffer(); } - if (state.config.save) save_last_settings: { + if (state.config.save_file_dir != null) 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. From 5ccd91c132dbf0b4b7d8905eb3533af00d1e0519 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Tue, 7 Jul 2026 11:51:05 +0200 Subject: [PATCH 169/176] Prepare for Zig 0.17.0 Still requires Zig 0.16.x for now. Signed-off-by: AnErrupTion --- build.zig | 474 +++++++---------------------------- install.zig | 445 ++++++++++++++++++++++++++++++++ ly-core/build.zig.zon | 4 +- ly-ui/build.zig | 2 +- ly-ui/build.zig.zon | 4 +- ly-ui/src/TerminalBuffer.zig | 6 +- ly-ui/src/keyboard.zig | 10 +- 7 files changed, 546 insertions(+), 399 deletions(-) create mode 100644 install.zig diff --git a/build.zig b/build.zig index b4f216c..cdb0d1f 100644 --- a/build.zig +++ b/build.zig @@ -1,7 +1,6 @@ const std = @import("std"); const builtin = @import("builtin"); -const PatchMap = std.StringHashMap([]const u8); const InitSystem = enum { systemd, openrc, @@ -11,6 +10,12 @@ const InitSystem = enum { sysvinit, freebsd, }; +const InstallType = enum { + installexe, + installnoconf, + uninstallexe, + uninstallnoconf, +}; const min_zig_string = "0.16.0"; const current_zig = builtin.zig_version; @@ -25,19 +30,45 @@ comptime { const ly_version = std.SemanticVersion{ .major = 1, .minor = 5, .patch = 0 }; -var dest_directory: []const u8 = undefined; -var config_directory: []const u8 = undefined; -var prefix_directory: []const u8 = undefined; -var executable_name: []const u8 = undefined; -var init_system: InitSystem = undefined; -var default_tty_str: []const u8 = undefined; +fn InstallStep( + b: *std.Build, + target: std.Build.ResolvedTarget, + comptime install_type: InstallType, + dest_directory: []const u8, + config_directory: []const u8, + prefix_directory: []const u8, + executable_name: []const u8, + init_system: InitSystem, + default_tty_str: []const u8, +) *std.Build.Step.Run { + const step = b.addRunArtifact(b.addExecutable(.{ + .name = "install", + .root_module = b.createModule(.{ + .root_source_file = b.path("install.zig"), + .target = target, + }), + })); + step.step.dependOn(b.getInstallStep()); + + step.addArgs(&.{ + std.enums.tagName(InstallType, install_type).?, + dest_directory, + config_directory, + prefix_directory, + executable_name, + std.enums.tagName(InitSystem, init_system).?, + default_tty_str, + }); + + return step; +} pub fn build(b: *std.Build) !void { - dest_directory = b.option([]const u8, "dest_directory", "Specify a destination directory for installation") orelse ""; - config_directory = b.option([]const u8, "config_directory", "Specify a default config directory (default is /etc). This path gets embedded into the binary") orelse "/etc"; - prefix_directory = b.option([]const u8, "prefix_directory", "Specify a default prefix directory (default is /usr)") orelse "/usr"; - executable_name = b.option([]const u8, "name", "Specify installed executable file name (default is ly)") orelse "ly"; - init_system = b.option(InitSystem, "init_system", "Specify the target init system (default is systemd)") orelse .systemd; + const dest_directory = b.option([]const u8, "dest_directory", "Specify a destination directory for installation") orelse ""; + const config_directory = b.option([]const u8, "config_directory", "Specify a default config directory (default is /etc). This path gets embedded into the binary") orelse "/etc"; + const prefix_directory = b.option([]const u8, "prefix_directory", "Specify a default prefix directory (default is /usr)") orelse "/usr"; + const executable_name = b.option([]const u8, "name", "Specify installed executable file name (default is ly)") orelse "ly"; + const init_system = b.option(InitSystem, "init_system", "Specify the target init system (default is systemd)") orelse .systemd; const build_options = b.addOptions(); const version_str = try getVersionStr(b, "ly", ly_version); @@ -47,7 +78,7 @@ pub fn build(b: *std.Build) !void { const fallback_uid_min = b.option(std.posix.uid_t, "fallback_uid_min", "Set the fallback minimum UID (default is 1000). This value gets embedded into the binary") orelse 1000; const fallback_uid_max = b.option(std.posix.uid_t, "fallback_uid_max", "Set the fallback maximum UID (default is 60000). This value gets embedded into the binary") orelse 60000; - default_tty_str = try std.fmt.allocPrint(b.allocator, "{d}", .{default_tty}); + const default_tty_str = try std.fmt.allocPrint(b.allocator, "{d}", .{default_tty}); build_options.addOption([]const u8, "config_directory", config_directory); build_options.addOption([]const u8, "prefix_directory", prefix_directory); @@ -98,296 +129,63 @@ pub fn build(b: *std.Build) !void { b.installArtifact(exe); const run_cmd = b.addRunArtifact(exe); - run_cmd.step.dependOn(b.getInstallStep()); - if (b.args) |args| run_cmd.addArgs(args); const run_step = b.step("run", "Run the app"); run_step.dependOn(&run_cmd.step); const installexe_step = b.step("installexe", "Install Ly and the selected init system service"); - installexe_step.makeFn = Installer(true).make; - installexe_step.dependOn(b.getInstallStep()); + installexe_step.dependOn(&InstallStep( + b, + target, + .installexe, + dest_directory, + config_directory, + prefix_directory, + executable_name, + init_system, + default_tty_str, + ).step); const installnoconf_step = b.step("installnoconf", "Install Ly and the selected init system service, but not the configuration file"); - installnoconf_step.makeFn = Installer(false).make; - installnoconf_step.dependOn(b.getInstallStep()); + installnoconf_step.dependOn(&InstallStep( + b, + target, + .installnoconf, + dest_directory, + config_directory, + prefix_directory, + executable_name, + init_system, + default_tty_str, + ).step); const uninstallexe_step = b.step("uninstallexe", "Uninstall Ly and remove the selected init system service"); - uninstallexe_step.makeFn = Uninstaller(true).make; + uninstallexe_step.dependOn(&InstallStep( + b, + target, + .uninstallexe, + dest_directory, + config_directory, + prefix_directory, + executable_name, + init_system, + default_tty_str, + ).step); const uninstallnoconf_step = b.step("uninstallnoconf", "Uninstall Ly and remove the selected init system service, but keep the configuration directory"); - uninstallnoconf_step.makeFn = Uninstaller(false).make; -} - -pub fn Installer(install_config: bool) type { - return struct { - pub fn make(step: *std.Build.Step, _: std.Build.Step.MakeOptions) !void { - var threaded: std.Io.Threaded = .init_single_threaded; - const io = threaded.io(); - const allocator = step.owner.allocator; - - var patch_map = PatchMap.init(allocator); - defer patch_map.deinit(); - - try patch_map.put("$DEFAULT_TTY", default_tty_str); - try patch_map.put("$CONFIG_DIRECTORY", config_directory); - try patch_map.put("$PREFIX_DIRECTORY", prefix_directory); - try patch_map.put("$EXECUTABLE_NAME", executable_name); - - try install_ly(allocator, io, patch_map, install_config); - try install_service(allocator, io, patch_map); - } - }; -} - -fn install_ly(allocator: std.mem.Allocator, io: std.Io, patch_map: PatchMap, install_config: bool) !void { - const ly_config_directory = try std.Io.Dir.path.join(allocator, &[_][]const u8{ dest_directory, config_directory, "/ly" }); - - std.Io.Dir.cwd().createDirPath(io, ly_config_directory) catch { - std.debug.print("warn: {s} already exists as a directory.\n", .{ly_config_directory}); - }; - - const ly_custom_sessions_directory = try std.Io.Dir.path.join(allocator, &[_][]const u8{ dest_directory, config_directory, "/ly/custom-sessions" }); - - std.Io.Dir.cwd().createDirPath(io, ly_custom_sessions_directory) catch { - std.debug.print("warn: {s} already exists as a directory.\n", .{ly_custom_sessions_directory}); - }; - - const ly_lang_path = try std.Io.Dir.path.join(allocator, &[_][]const u8{ dest_directory, config_directory, "/ly/lang" }); - std.Io.Dir.cwd().createDirPath(io, ly_lang_path) catch { - std.debug.print("warn: {s} already exists as a directory.\n", .{ly_lang_path}); - }; - - { - const exe_path = try std.Io.Dir.path.join(allocator, &[_][]const u8{ dest_directory, prefix_directory, "/bin" }); - std.Io.Dir.cwd().createDirPath(io, exe_path) catch { - if (!std.mem.eql(u8, dest_directory, "")) { - std.debug.print("warn: {s} already exists as a directory.\n", .{exe_path}); - } - }; - - var executable_dir = std.Io.Dir.cwd().openDir(io, exe_path, .{}) catch unreachable; - defer executable_dir.close(io); - - try installFile(io, "zig-out/bin/ly", executable_dir, exe_path, executable_name, .{}); - } - - { - var config_dir = std.Io.Dir.cwd().openDir(io, ly_config_directory, .{}) catch unreachable; - defer config_dir.close(io); - - if (install_config) { - const patched_config = try patchFile(allocator, io, "res/config.ini", patch_map); - try installText(io, patched_config, config_dir, ly_config_directory, "config.ini", .{}); - - try installFile(io, "res/startup.sh", config_dir, ly_config_directory, "startup.sh", .{ .permissions = .fromMode(0o755) }); - } - - const patched_example_config = try patchFile(allocator, io, "res/config.ini", patch_map); - try installText(io, patched_example_config, config_dir, ly_config_directory, "config.ini.example", .{}); - - const patched_setup = try patchFile(allocator, io, "res/setup.sh", patch_map); - try installText(io, patched_setup, config_dir, ly_config_directory, "setup.sh", .{ .permissions = .fromMode(0o755) }); - - try installFile(io, "res/example.dur", config_dir, ly_config_directory, "example.dur", .{ .permissions = .fromMode(0o755) }); - - try installFile(io, "res/example.lua", config_dir, ly_config_directory, "example.lua", .{ .permissions = .fromMode(0o755) }); - } - - { - var custom_sessions_dir = std.Io.Dir.cwd().openDir(io, ly_custom_sessions_directory, .{}) catch unreachable; - defer custom_sessions_dir.close(io); - - const patched_readme = try patchFile(allocator, io, "res/custom-sessions/README", patch_map); - try installText(io, patched_readme, custom_sessions_dir, ly_custom_sessions_directory, "README", .{}); - } - - { - var lang_dir = std.Io.Dir.cwd().openDir(io, ly_lang_path, .{}) catch unreachable; - defer lang_dir.close(io); - - const languages = [_][]const u8{ - "ar.ini", - "bg.ini", - "cat.ini", - "cs.ini", - "de.ini", - "en.ini", - "eo.ini", - "es.ini", - "fr.ini", - "it.ini", - "ja_JP.ini", - "ku.ini", - "lv.ini", - "pl.ini", - "pt.ini", - "pt_BR.ini", - "ro.ini", - "ru.ini", - "sr.ini", - "sr_Cyrl.ini", - "sv.ini", - "tr.ini", - "uk.ini", - "zh_CN.ini", - "zh_TW.ini", - }; - - inline for (languages) |language| { - try installFile(io, "res/lang/" ++ language, lang_dir, ly_lang_path, language, .{}); - } - } - - { - const pam_path = try std.Io.Dir.path.join(allocator, &[_][]const u8{ dest_directory, config_directory, "/pam.d" }); - std.Io.Dir.cwd().createDirPath(io, pam_path) catch { - if (!std.mem.eql(u8, dest_directory, "")) { - std.debug.print("warn: {s} already exists as a directory.\n", .{pam_path}); - } - }; - - var pam_dir = std.Io.Dir.cwd().openDir(io, pam_path, .{}) catch unreachable; - defer pam_dir.close(io); - - try installFile(io, if (init_system == .freebsd) "res/pam.d/ly-freebsd" else "res/pam.d/ly-linux", pam_dir, pam_path, "ly", .{ .permissions = .fromMode(0o644) }); - try installFile(io, if (init_system == .freebsd) "res/pam.d/ly-freebsd-autologin" else "res/pam.d/ly-linux-autologin", pam_dir, pam_path, "ly-autologin", .{ .permissions = .fromMode(0o644) }); - } -} - -fn install_service(allocator: std.mem.Allocator, io: std.Io, patch_map: PatchMap) !void { - switch (init_system) { - .systemd => { - const service_path = try std.Io.Dir.path.join(allocator, &[_][]const u8{ dest_directory, prefix_directory, "/lib/systemd/system" }); - std.Io.Dir.cwd().createDirPath(io, service_path) catch {}; - var service_dir = std.Io.Dir.cwd().openDir(io, service_path, .{}) catch unreachable; - defer service_dir.close(io); - - const patched_service = try patchFile(allocator, io, "res/ly@.service", patch_map); - try installText(io, patched_service, service_dir, service_path, "ly@.service", .{ .permissions = .fromMode(0o644) }); - - const patched_kmsconvt_service = try patchFile(allocator, io, "res/ly-kmsconvt@.service", patch_map); - try installText(io, patched_kmsconvt_service, service_dir, service_path, "ly-kmsconvt@.service", .{ .permissions = .fromMode(0o644) }); - }, - .openrc => { - const service_path = try std.Io.Dir.path.join(allocator, &[_][]const u8{ dest_directory, config_directory, "/init.d" }); - std.Io.Dir.cwd().createDirPath(io, service_path) catch {}; - var service_dir = std.Io.Dir.cwd().openDir(io, service_path, .{}) catch unreachable; - defer service_dir.close(io); - - const patched_service = try patchFile(allocator, io, "res/ly-openrc", patch_map); - try installText(io, patched_service, service_dir, service_path, executable_name, .{ .permissions = .fromMode(0o755) }); - }, - .runit => { - const service_path = try std.Io.Dir.path.join(allocator, &[_][]const u8{ dest_directory, config_directory, "/sv/ly" }); - std.Io.Dir.cwd().createDirPath(io, service_path) catch {}; - var service_dir = std.Io.Dir.cwd().openDir(io, service_path, .{}) catch unreachable; - defer service_dir.close(io); - - const supervise_path = try std.Io.Dir.path.join(allocator, &[_][]const u8{ service_path, "supervise" }); - - const patched_conf = try patchFile(allocator, io, "res/ly-runit-service/conf", patch_map); - try installText(io, patched_conf, service_dir, service_path, "conf", .{}); - - try installFile(io, "res/ly-runit-service/finish", service_dir, service_path, "finish", .{ .permissions = .fromMode(0o755) }); - - const patched_run = try patchFile(allocator, io, "res/ly-runit-service/run", patch_map); - try installText(io, patched_run, service_dir, service_path, "run", .{ .permissions = .fromMode(0o755) }); - - std.Io.Dir.cwd().symLink(io, "/run/runit/supervise.ly", supervise_path, .{}) catch |err| { - if (err == error.PathAlreadyExists) { - std.debug.print("warn: /run/runit/supervise.ly already exists as a symbolic link.\n", .{}); - } else { - return err; - } - }; - std.debug.print("info: installed symlink /run/runit/supervise.ly\n", .{}); - }, - .s6 => { - const admin_service_path = try std.Io.Dir.path.join(allocator, &[_][]const u8{ dest_directory, config_directory, "/s6/adminsv/default/contents.d" }); - std.Io.Dir.cwd().createDirPath(io, admin_service_path) catch {}; - var admin_service_dir = std.Io.Dir.cwd().openDir(io, admin_service_path, .{}) catch unreachable; - defer admin_service_dir.close(io); - - const file = try admin_service_dir.createFile(io, "ly-srv", .{}); - file.close(io); - - const service_path = try std.Io.Dir.path.join(allocator, &[_][]const u8{ dest_directory, config_directory, "/s6/sv/ly-srv" }); - std.Io.Dir.cwd().createDirPath(io, service_path) catch {}; - var service_dir = std.Io.Dir.cwd().openDir(io, service_path, .{}) catch unreachable; - defer service_dir.close(io); - - const patched_run = try patchFile(allocator, io, "res/ly-s6/run", patch_map); - try installText(io, patched_run, service_dir, service_path, "run", .{ .permissions = .fromMode(0o755) }); - - try installFile(io, "res/ly-s6/type", service_dir, service_path, "type", .{}); - }, - .dinit => { - const service_path = try std.Io.Dir.path.join(allocator, &[_][]const u8{ dest_directory, config_directory, "/dinit.d" }); - std.Io.Dir.cwd().createDirPath(io, service_path) catch {}; - var service_dir = std.Io.Dir.cwd().openDir(io, service_path, .{}) catch unreachable; - defer service_dir.close(io); - - const patched_service = try patchFile(allocator, io, "res/ly-dinit", patch_map); - try installText(io, patched_service, service_dir, service_path, "ly", .{}); - }, - .sysvinit => { - const service_path = try std.Io.Dir.path.join(allocator, &[_][]const u8{ dest_directory, config_directory, "/init.d" }); - std.Io.Dir.cwd().createDirPath(io, service_path) catch {}; - var service_dir = std.Io.Dir.cwd().openDir(io, service_path, .{}) catch unreachable; - defer service_dir.close(io); - - const patched_service = try patchFile(allocator, io, "res/ly-sysvinit", patch_map); - try installText(io, patched_service, service_dir, service_path, "ly", .{ .permissions = .fromMode(0o755) }); - }, - .freebsd => { - const exe_path = try std.Io.Dir.path.join(allocator, &[_][]const u8{ dest_directory, prefix_directory, "/bin" }); - var executable_dir = std.Io.Dir.cwd().openDir(io, exe_path, .{}) catch unreachable; - defer executable_dir.close(io); - - const patched_wrapper = try patchFile(allocator, io, "res/ly-freebsd-wrapper", patch_map); - try installText(io, patched_wrapper, executable_dir, exe_path, "ly_wrapper", .{ .permissions = .fromMode(0o755) }); - }, - } -} - -pub fn Uninstaller(uninstall_config: bool) type { - return struct { - pub fn make(step: *std.Build.Step, _: std.Build.Step.MakeOptions) !void { - var threaded: std.Io.Threaded = .init_single_threaded; - const io = threaded.io(); - const allocator = step.owner.allocator; - - if (uninstall_config) { - try deleteTree(allocator, io, config_directory, "/ly", "ly config directory not found"); - } - - const exe_path = try std.Io.Dir.path.join(allocator, &[_][]const u8{ prefix_directory, "/bin/", executable_name }); - var success = true; - std.Io.Dir.cwd().deleteFile(io, exe_path) catch { - std.debug.print("warn: ly executable not found\n", .{}); - success = false; - }; - if (success) std.debug.print("info: deleted {s}\n", .{exe_path}); - - try deleteFile(allocator, io, config_directory, "/pam.d/ly", "ly pam file not found"); - - switch (init_system) { - .systemd => try deleteFile(allocator, io, prefix_directory, "/lib/systemd/system/ly@.service", "systemd service not found"), - .openrc => try deleteFile(allocator, io, config_directory, "/init.d/ly", "openrc service not found"), - .runit => try deleteTree(allocator, io, config_directory, "/sv/ly", "runit service not found"), - .s6 => { - try deleteTree(allocator, io, config_directory, "/s6/sv/ly-srv", "s6 service not found"); - try deleteFile(allocator, io, config_directory, "/s6/adminsv/default/contents.d/ly-srv", "s6 admin service not found"); - }, - .dinit => try deleteFile(allocator, io, config_directory, "/dinit.d/ly", "dinit service not found"), - .sysvinit => try deleteFile(allocator, io, config_directory, "/init.d/ly", "sysvinit service not found"), - .freebsd => try deleteFile(allocator, io, prefix_directory, "/bin/ly_wrapper", "freebsd wrapper not found"), - } - } - }; + uninstallnoconf_step.dependOn(&InstallStep( + b, + target, + .uninstallnoconf, + dest_directory, + config_directory, + prefix_directory, + executable_name, + init_system, + default_tty_str, + ).step); } fn getVersionStr(b: *std.Build, name: []const u8, version: std.SemanticVersion) ![]const u8 { @@ -444,99 +242,3 @@ fn getVersionStr(b: *std.Build, name: []const u8, version: std.SemanticVersion) }, } } - -fn installFile( - io: std.Io, - source_file: []const u8, - destination_directory: std.Io.Dir, - destination_directory_path: []const u8, - destination_file: []const u8, - options: std.Io.Dir.CopyFileOptions, -) !void { - try std.Io.Dir.cwd().copyFile(source_file, destination_directory, destination_file, io, options); - std.debug.print("info: installed {s}/{s}\n", .{ destination_directory_path, destination_file }); -} - -fn patchFile(allocator: std.mem.Allocator, io: std.Io, source_file: []const u8, patch_map: PatchMap) ![]const u8 { - var file = try std.Io.Dir.cwd().openFile(io, source_file, .{}); - defer file.close(io); - - const stat = try file.stat(io); - - var buffer: [4096]u8 = undefined; - var reader = file.reader(io, &buffer); - var text = try reader.interface.readAlloc(allocator, @intCast(stat.size)); - - var iterator = patch_map.iterator(); - while (iterator.next()) |kv| { - const new_text = try std.mem.replaceOwned(u8, allocator, text, kv.key_ptr.*, kv.value_ptr.*); - allocator.free(text); - text = new_text; - } - - return text; -} - -fn installText( - io: std.Io, - text: []const u8, - destination_directory: std.Io.Dir, - destination_directory_path: []const u8, - destination_file: []const u8, - options: std.Io.File.CreateFlags, -) !void { - var file = try destination_directory.createFile(io, destination_file, options); - defer file.close(io); - - var buffer: [1024]u8 = undefined; - var writer = file.writer(io, &buffer); - try writer.interface.writeAll(text); - try writer.interface.flush(); - - std.debug.print("info: installed {s}/{s}\n", .{ destination_directory_path, destination_file }); -} - -fn deleteFile( - allocator: std.mem.Allocator, - io: std.Io, - prefix: []const u8, - file: []const u8, - warning: []const u8, -) !void { - const path = try std.Io.Dir.path.join(allocator, &[_][]const u8{ dest_directory, prefix, file }); - - std.Io.Dir.cwd().deleteFile(io, path) catch |err| { - if (err == error.FileNotFound) { - std.debug.print("warn: {s}\n", .{warning}); - return; - } - - return err; - }; - - std.debug.print("info: deleted {s}\n", .{path}); -} - -fn deleteTree( - allocator: std.mem.Allocator, - io: std.Io, - prefix: []const u8, - directory: []const u8, - warning: []const u8, -) !void { - const path = try std.Io.Dir.path.join(allocator, &[_][]const u8{ dest_directory, prefix, directory }); - - var dir = std.Io.Dir.cwd().openDir(io, path, .{}) catch |err| { - if (err == error.FileNotFound) { - std.debug.print("warn: {s}\n", .{warning}); - return; - } - - return err; - }; - dir.close(io); - - try std.Io.Dir.cwd().deleteTree(io, path); - - std.debug.print("info: deleted {s}\n", .{path}); -} diff --git a/install.zig b/install.zig new file mode 100644 index 0000000..b5649cd --- /dev/null +++ b/install.zig @@ -0,0 +1,445 @@ +const std = @import("std"); + +const PatchMap = std.StringHashMap([]const u8); +const InitSystem = enum { + systemd, + openrc, + runit, + s6, + dinit, + sysvinit, + freebsd, +}; +const InstallType = enum { + installexe, + installnoconf, + uninstallexe, + uninstallnoconf, +}; + +var dest_directory: []const u8 = undefined; +var config_directory: []const u8 = undefined; +var prefix_directory: []const u8 = undefined; +var executable_name: []const u8 = undefined; +var init_system: InitSystem = undefined; + +pub fn main(init: std.process.Init) !void { + const io = init.io; + const allocator = init.gpa; + + var args = init.minimal.args.iterate(); + if (!args.skip()) return error.NoProgramName; + + const install_type = std.meta.stringToEnum(InstallType, args.next().?).?; + dest_directory = args.next().?; + config_directory = args.next().?; + prefix_directory = args.next().?; + executable_name = args.next().?; + init_system = std.meta.stringToEnum(InitSystem, args.next().?).?; + const default_tty_str = args.next().?; + + switch (install_type) { + .installexe, .installnoconf => { + var patch_map = PatchMap.init(allocator); + defer patch_map.deinit(); + + try patch_map.put("$DEFAULT_TTY", default_tty_str); + try patch_map.put("$CONFIG_DIRECTORY", config_directory); + try patch_map.put("$PREFIX_DIRECTORY", prefix_directory); + try patch_map.put("$EXECUTABLE_NAME", executable_name); + + try installLy(allocator, io, patch_map, install_type == .installexe); + try installService(allocator, io, patch_map); + }, + .uninstallexe, .uninstallnoconf => { + if (install_type == .uninstallexe) { + try deleteTree(allocator, io, config_directory, "/ly", "ly config directory not found"); + } + + const exe_path = try std.Io.Dir.path.join(allocator, &[_][]const u8{ prefix_directory, "/bin/", executable_name }); + defer allocator.free(exe_path); + + var success = true; + std.Io.Dir.cwd().deleteFile(io, exe_path) catch { + std.debug.print("warn: ly executable not found\n", .{}); + success = false; + }; + if (success) std.debug.print("info: deleted {s}\n", .{exe_path}); + + try deleteFile(allocator, io, config_directory, "/pam.d/ly", "ly pam file not found"); + + switch (init_system) { + .systemd => try deleteFile(allocator, io, prefix_directory, "/lib/systemd/system/ly@.service", "systemd service not found"), + .openrc => try deleteFile(allocator, io, config_directory, "/init.d/ly", "openrc service not found"), + .runit => try deleteTree(allocator, io, config_directory, "/sv/ly", "runit service not found"), + .s6 => { + try deleteTree(allocator, io, config_directory, "/s6/sv/ly-srv", "s6 service not found"); + try deleteFile(allocator, io, config_directory, "/s6/adminsv/default/contents.d/ly-srv", "s6 admin service not found"); + }, + .dinit => try deleteFile(allocator, io, config_directory, "/dinit.d/ly", "dinit service not found"), + .sysvinit => try deleteFile(allocator, io, config_directory, "/init.d/ly", "sysvinit service not found"), + .freebsd => try deleteFile(allocator, io, prefix_directory, "/bin/ly_wrapper", "freebsd wrapper not found"), + } + }, + } +} + +fn installLy(allocator: std.mem.Allocator, io: std.Io, patch_map: PatchMap, install_config: bool) !void { + const ly_config_directory = try std.Io.Dir.path.join(allocator, &[_][]const u8{ dest_directory, config_directory, "/ly" }); + defer allocator.free(ly_config_directory); + + std.Io.Dir.cwd().createDirPath(io, ly_config_directory) catch { + std.debug.print("warn: {s} already exists as a directory.\n", .{ly_config_directory}); + }; + + const ly_custom_sessions_directory = try std.Io.Dir.path.join(allocator, &[_][]const u8{ dest_directory, config_directory, "/ly/custom-sessions" }); + defer allocator.free(ly_custom_sessions_directory); + + std.Io.Dir.cwd().createDirPath(io, ly_custom_sessions_directory) catch { + std.debug.print("warn: {s} already exists as a directory.\n", .{ly_custom_sessions_directory}); + }; + + const ly_lang_path = try std.Io.Dir.path.join(allocator, &[_][]const u8{ dest_directory, config_directory, "/ly/lang" }); + defer allocator.free(ly_lang_path); + + std.Io.Dir.cwd().createDirPath(io, ly_lang_path) catch { + std.debug.print("warn: {s} already exists as a directory.\n", .{ly_lang_path}); + }; + + { + const exe_path = try std.Io.Dir.path.join(allocator, &[_][]const u8{ dest_directory, prefix_directory, "/bin" }); + defer allocator.free(exe_path); + + std.Io.Dir.cwd().createDirPath(io, exe_path) catch { + if (!std.mem.eql(u8, dest_directory, "")) { + std.debug.print("warn: {s} already exists as a directory.\n", .{exe_path}); + } + }; + + var executable_dir = std.Io.Dir.cwd().openDir(io, exe_path, .{}) catch unreachable; + defer executable_dir.close(io); + + try installFile(io, "zig-out/bin/ly", executable_dir, exe_path, executable_name, .{}); + } + + { + var config_dir = std.Io.Dir.cwd().openDir(io, ly_config_directory, .{}) catch unreachable; + defer config_dir.close(io); + + if (install_config) { + const patched_config = try patchFile(allocator, io, "res/config.ini", patch_map); + defer allocator.free(patched_config); + + try installText(io, patched_config, config_dir, ly_config_directory, "config.ini", .{}); + + try installFile(io, "res/startup.sh", config_dir, ly_config_directory, "startup.sh", .{ .permissions = .fromMode(0o755) }); + } + + const patched_example_config = try patchFile(allocator, io, "res/config.ini", patch_map); + defer allocator.free(patched_example_config); + + try installText(io, patched_example_config, config_dir, ly_config_directory, "config.ini.example", .{}); + + const patched_setup = try patchFile(allocator, io, "res/setup.sh", patch_map); + defer allocator.free(patched_setup); + + try installText(io, patched_setup, config_dir, ly_config_directory, "setup.sh", .{ .permissions = .fromMode(0o755) }); + + try installFile(io, "res/example.dur", config_dir, ly_config_directory, "example.dur", .{ .permissions = .fromMode(0o755) }); + + try installFile(io, "res/example.lua", config_dir, ly_config_directory, "example.lua", .{ .permissions = .fromMode(0o755) }); + } + + { + var custom_sessions_dir = std.Io.Dir.cwd().openDir(io, ly_custom_sessions_directory, .{}) catch unreachable; + defer custom_sessions_dir.close(io); + + const patched_readme = try patchFile(allocator, io, "res/custom-sessions/README", patch_map); + defer allocator.free(patched_readme); + + try installText(io, patched_readme, custom_sessions_dir, ly_custom_sessions_directory, "README", .{}); + } + + { + var lang_dir = std.Io.Dir.cwd().openDir(io, ly_lang_path, .{}) catch unreachable; + defer lang_dir.close(io); + + const languages = [_][]const u8{ + "ar.ini", + "bg.ini", + "cat.ini", + "cs.ini", + "de.ini", + "en.ini", + "eo.ini", + "es.ini", + "fr.ini", + "it.ini", + "ja_JP.ini", + "ku.ini", + "lv.ini", + "pl.ini", + "pt.ini", + "pt_BR.ini", + "ro.ini", + "ru.ini", + "sr.ini", + "sr_Cyrl.ini", + "sv.ini", + "tr.ini", + "uk.ini", + "zh_CN.ini", + "zh_TW.ini", + }; + + inline for (languages) |language| { + try installFile(io, "res/lang/" ++ language, lang_dir, ly_lang_path, language, .{}); + } + } + + { + const pam_path = try std.Io.Dir.path.join(allocator, &[_][]const u8{ dest_directory, config_directory, "/pam.d" }); + defer allocator.free(pam_path); + + std.Io.Dir.cwd().createDirPath(io, pam_path) catch { + if (!std.mem.eql(u8, dest_directory, "")) { + std.debug.print("warn: {s} already exists as a directory.\n", .{pam_path}); + } + }; + + var pam_dir = std.Io.Dir.cwd().openDir(io, pam_path, .{}) catch unreachable; + defer pam_dir.close(io); + + try installFile(io, if (init_system == .freebsd) "res/pam.d/ly-freebsd" else "res/pam.d/ly-linux", pam_dir, pam_path, "ly", .{ .permissions = .fromMode(0o644) }); + try installFile(io, if (init_system == .freebsd) "res/pam.d/ly-freebsd-autologin" else "res/pam.d/ly-linux-autologin", pam_dir, pam_path, "ly-autologin", .{ .permissions = .fromMode(0o644) }); + } +} + +fn installService(allocator: std.mem.Allocator, io: std.Io, patch_map: PatchMap) !void { + switch (init_system) { + .systemd => { + const service_path = try std.Io.Dir.path.join(allocator, &[_][]const u8{ dest_directory, prefix_directory, "/lib/systemd/system" }); + defer allocator.free(service_path); + + std.Io.Dir.cwd().createDirPath(io, service_path) catch {}; + var service_dir = std.Io.Dir.cwd().openDir(io, service_path, .{}) catch unreachable; + defer service_dir.close(io); + + const patched_service = try patchFile(allocator, io, "res/ly@.service", patch_map); + defer allocator.free(patched_service); + + try installText(io, patched_service, service_dir, service_path, "ly@.service", .{ .permissions = .fromMode(0o644) }); + + const patched_kmsconvt_service = try patchFile(allocator, io, "res/ly-kmsconvt@.service", patch_map); + defer allocator.free(patched_kmsconvt_service); + + try installText(io, patched_kmsconvt_service, service_dir, service_path, "ly-kmsconvt@.service", .{ .permissions = .fromMode(0o644) }); + }, + .openrc => { + const service_path = try std.Io.Dir.path.join(allocator, &[_][]const u8{ dest_directory, config_directory, "/init.d" }); + defer allocator.free(service_path); + + std.Io.Dir.cwd().createDirPath(io, service_path) catch {}; + var service_dir = std.Io.Dir.cwd().openDir(io, service_path, .{}) catch unreachable; + defer service_dir.close(io); + + const patched_service = try patchFile(allocator, io, "res/ly-openrc", patch_map); + defer allocator.free(patched_service); + + try installText(io, patched_service, service_dir, service_path, executable_name, .{ .permissions = .fromMode(0o755) }); + }, + .runit => { + const service_path = try std.Io.Dir.path.join(allocator, &[_][]const u8{ dest_directory, config_directory, "/sv/ly" }); + defer allocator.free(service_path); + + std.Io.Dir.cwd().createDirPath(io, service_path) catch {}; + var service_dir = std.Io.Dir.cwd().openDir(io, service_path, .{}) catch unreachable; + defer service_dir.close(io); + + const supervise_path = try std.Io.Dir.path.join(allocator, &[_][]const u8{ service_path, "supervise" }); + defer allocator.free(supervise_path); + + const patched_conf = try patchFile(allocator, io, "res/ly-runit-service/conf", patch_map); + defer allocator.free(patched_conf); + + try installText(io, patched_conf, service_dir, service_path, "conf", .{}); + + try installFile(io, "res/ly-runit-service/finish", service_dir, service_path, "finish", .{ .permissions = .fromMode(0o755) }); + + const patched_run = try patchFile(allocator, io, "res/ly-runit-service/run", patch_map); + defer allocator.free(patched_run); + + try installText(io, patched_run, service_dir, service_path, "run", .{ .permissions = .fromMode(0o755) }); + + std.Io.Dir.cwd().symLink(io, "/run/runit/supervise.ly", supervise_path, .{}) catch |err| { + if (err == error.PathAlreadyExists) { + std.debug.print("warn: /run/runit/supervise.ly already exists as a symbolic link.\n", .{}); + } else { + return err; + } + }; + std.debug.print("info: installed symlink /run/runit/supervise.ly\n", .{}); + }, + .s6 => { + const admin_service_path = try std.Io.Dir.path.join(allocator, &[_][]const u8{ dest_directory, config_directory, "/s6/adminsv/default/contents.d" }); + std.Io.Dir.cwd().createDirPath(io, admin_service_path) catch {}; + defer allocator.free(admin_service_path); + + var admin_service_dir = std.Io.Dir.cwd().openDir(io, admin_service_path, .{}) catch unreachable; + defer admin_service_dir.close(io); + + const file = try admin_service_dir.createFile(io, "ly-srv", .{}); + file.close(io); + + const service_path = try std.Io.Dir.path.join(allocator, &[_][]const u8{ dest_directory, config_directory, "/s6/sv/ly-srv" }); + defer allocator.free(service_path); + + std.Io.Dir.cwd().createDirPath(io, service_path) catch {}; + var service_dir = std.Io.Dir.cwd().openDir(io, service_path, .{}) catch unreachable; + defer service_dir.close(io); + + const patched_run = try patchFile(allocator, io, "res/ly-s6/run", patch_map); + defer allocator.free(patched_run); + + try installText(io, patched_run, service_dir, service_path, "run", .{ .permissions = .fromMode(0o755) }); + + try installFile(io, "res/ly-s6/type", service_dir, service_path, "type", .{}); + }, + .dinit => { + const service_path = try std.Io.Dir.path.join(allocator, &[_][]const u8{ dest_directory, config_directory, "/dinit.d" }); + defer allocator.free(service_path); + + std.Io.Dir.cwd().createDirPath(io, service_path) catch {}; + var service_dir = std.Io.Dir.cwd().openDir(io, service_path, .{}) catch unreachable; + defer service_dir.close(io); + + const patched_service = try patchFile(allocator, io, "res/ly-dinit", patch_map); + defer allocator.free(patched_service); + + try installText(io, patched_service, service_dir, service_path, "ly", .{}); + }, + .sysvinit => { + const service_path = try std.Io.Dir.path.join(allocator, &[_][]const u8{ dest_directory, config_directory, "/init.d" }); + defer allocator.free(service_path); + + std.Io.Dir.cwd().createDirPath(io, service_path) catch {}; + var service_dir = std.Io.Dir.cwd().openDir(io, service_path, .{}) catch unreachable; + defer service_dir.close(io); + + const patched_service = try patchFile(allocator, io, "res/ly-sysvinit", patch_map); + defer allocator.free(patched_service); + + try installText(io, patched_service, service_dir, service_path, "ly", .{ .permissions = .fromMode(0o755) }); + }, + .freebsd => { + const exe_path = try std.Io.Dir.path.join(allocator, &[_][]const u8{ dest_directory, prefix_directory, "/bin" }); + defer allocator.free(exe_path); + + var executable_dir = std.Io.Dir.cwd().openDir(io, exe_path, .{}) catch unreachable; + defer executable_dir.close(io); + + const patched_wrapper = try patchFile(allocator, io, "res/ly-freebsd-wrapper", patch_map); + defer allocator.free(patched_wrapper); + + try installText(io, patched_wrapper, executable_dir, exe_path, "ly_wrapper", .{ .permissions = .fromMode(0o755) }); + }, + } +} + +fn installFile( + io: std.Io, + source_file: []const u8, + destination_directory: std.Io.Dir, + destination_directory_path: []const u8, + destination_file: []const u8, + options: std.Io.Dir.CopyFileOptions, +) !void { + try std.Io.Dir.cwd().copyFile(source_file, destination_directory, destination_file, io, options); + std.debug.print("info: installed {s}/{s}\n", .{ destination_directory_path, destination_file }); +} + +fn patchFile(allocator: std.mem.Allocator, io: std.Io, source_file: []const u8, patch_map: PatchMap) ![]const u8 { + var file = try std.Io.Dir.cwd().openFile(io, source_file, .{}); + defer file.close(io); + + const stat = try file.stat(io); + + var buffer: [4096]u8 = undefined; + var reader = file.reader(io, &buffer); + var text = try reader.interface.readAlloc(allocator, @intCast(stat.size)); + + var iterator = patch_map.iterator(); + while (iterator.next()) |kv| { + const new_text = try std.mem.replaceOwned(u8, allocator, text, kv.key_ptr.*, kv.value_ptr.*); + allocator.free(text); + text = new_text; + } + + return text; +} + +fn installText( + io: std.Io, + text: []const u8, + destination_directory: std.Io.Dir, + destination_directory_path: []const u8, + destination_file: []const u8, + options: std.Io.File.CreateFlags, +) !void { + var file = try destination_directory.createFile(io, destination_file, options); + defer file.close(io); + + var buffer: [1024]u8 = undefined; + var writer = file.writer(io, &buffer); + try writer.interface.writeAll(text); + try writer.interface.flush(); + + std.debug.print("info: installed {s}/{s}\n", .{ destination_directory_path, destination_file }); +} + +fn deleteFile( + allocator: std.mem.Allocator, + io: std.Io, + prefix: []const u8, + file: []const u8, + warning: []const u8, +) !void { + const path = try std.Io.Dir.path.join(allocator, &[_][]const u8{ dest_directory, prefix, file }); + defer allocator.free(path); + + std.Io.Dir.cwd().deleteFile(io, path) catch |err| { + if (err == error.FileNotFound) { + std.debug.print("warn: {s}\n", .{warning}); + return; + } + + return err; + }; + + std.debug.print("info: deleted {s}\n", .{path}); +} + +fn deleteTree( + allocator: std.mem.Allocator, + io: std.Io, + prefix: []const u8, + directory: []const u8, + warning: []const u8, +) !void { + const path = try std.Io.Dir.path.join(allocator, &[_][]const u8{ dest_directory, prefix, directory }); + defer allocator.free(path); + + var dir = std.Io.Dir.cwd().openDir(io, path, .{}) catch |err| { + if (err == error.FileNotFound) { + std.debug.print("warn: {s}\n", .{warning}); + return; + } + + return err; + }; + dir.close(io); + + try std.Io.Dir.cwd().deleteTree(io, path); + + std.debug.print("info: deleted {s}\n", .{path}); +} diff --git a/ly-core/build.zig.zon b/ly-core/build.zig.zon index b40c8f8..0d84326 100644 --- a/ly-core/build.zig.zon +++ b/ly-core/build.zig.zon @@ -9,8 +9,8 @@ .hash = "zigini-0.5.0-BSkB7e9WAACfyCBABNZiWL3gFMw18GKn3qBcPs8L1Ec1", }, .translate_c = .{ - .url = "git+https://codeberg.org/ziglang/translate-c#7a1a9fdc4ab00835748a6657ecbb835e3d5d45f7", - .hash = "translate_c-0.0.0-Q_BUWvP1BgCjAk6PWv5286tOlvzD9-X-NkuTzh0KxY0Q", + .url = "git+https://codeberg.org/ziglang/translate-c?ref=zig-0.16.x#6fe0ffc4549f15c5f2d9432c2b4460ba90ff85ac", + .hash = "translate_c-1.0.0-Q_BUWo_5BgD4flHdUhA31zOz0XvZk9k7lQv1ouzyNXj2", }, }, .paths = .{ diff --git a/ly-ui/build.zig b/ly-ui/build.zig index 0ab5b70..b13633f 100644 --- a/ly-ui/build.zig +++ b/ly-ui/build.zig @@ -19,7 +19,7 @@ pub fn build(b: *std.Build) void { .optimize = optimize, .enable_x11_support = enable_x11_support, .fallback_uid_min = fallback_uid_min, - .fallback_uid_max = fallback_uid_max + .fallback_uid_max = fallback_uid_max, }); mod.addImport("ly-core", ly_core.module("ly-core")); diff --git a/ly-ui/build.zig.zon b/ly-ui/build.zig.zon index 2a7e175..68131d1 100644 --- a/ly-ui/build.zig.zon +++ b/ly-ui/build.zig.zon @@ -12,8 +12,8 @@ .hash = "N-V-__8AAAUXBQD6Fwpi9m0MBqWXFFaqW5l1lVrJC2Ynj7a-", }, .translate_c = .{ - .url = "git+https://codeberg.org/ziglang/translate-c#7a1a9fdc4ab00835748a6657ecbb835e3d5d45f7", - .hash = "translate_c-0.0.0-Q_BUWvP1BgCjAk6PWv5286tOlvzD9-X-NkuTzh0KxY0Q", + .url = "git+https://codeberg.org/ziglang/translate-c?ref=zig-0.16.x#6fe0ffc4549f15c5f2d9432c2b4460ba90ff85ac", + .hash = "translate_c-1.0.0-Q_BUWo_5BgD4flHdUhA31zOz0XvZk9k7lQv1ouzyNXj2", }, }, .paths = .{ diff --git a/ly-ui/src/TerminalBuffer.zig b/ly-ui/src/TerminalBuffer.zig index 9dfa1f3..7ead34f 100644 --- a/ly-ui/src/TerminalBuffer.zig +++ b/ly-ui/src/TerminalBuffer.zig @@ -578,9 +578,9 @@ fn parseKeybind(self: *TerminalBuffer, io: std.Io, keybind: []const u8) !keyboar 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; + inline for (comptime std.meta.fieldNames(keyboard.Key)) |name| { + if (std.ascii.eqlIgnoreCase(name, item)) { + @field(key, name) = true; found = true; break; } diff --git a/ly-ui/src/keyboard.zig b/ly-ui/src/keyboard.zig index 626f21f..16b9e77 100644 --- a/ly-ui/src/keyboard.zig +++ b/ly-ui/src/keyboard.zig @@ -113,14 +113,14 @@ pub const Key = packed struct { pub fn getEnabledPrintableAscii(self: Key) ?u8 { if (self.ctrl or self.alt) return null; - inline for (std.meta.fields(Key)) |field| { - if (field.name.len == 1 and std.ascii.isPrint(field.name[0]) and @field(self, field.name)) { + inline for (comptime std.meta.fieldNames(Key)) |name| { + if (name.len == 1 and std.ascii.isPrint(name[0]) and @field(self, name)) { if (self.shift) { - if (!std.ascii.isAlphanumeric(field.name[0])) return null; - return std.ascii.toUpper(field.name[0]); + if (!std.ascii.isAlphanumeric(name[0])) return null; + return std.ascii.toUpper(name[0]); } - return field.name[0]; + return name[0]; } } From fb544b33578be6dda9c9b87d67adee42e60f16e3 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Tue, 7 Jul 2026 12:22:43 +0200 Subject: [PATCH 170/176] Change default box_position_v to 0.5 This can potentially be confusing to users. Signed-off-by: AnErrupTion --- res/config.ini | 4 ++-- src/config/Config.zig | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/res/config.ini b/res/config.ini index 8924b1b..2bd2624 100644 --- a/res/config.ini +++ b/res/config.ini @@ -104,8 +104,8 @@ border_fg = 0x00FFFFFF box_position_h = 0.5 # Relative vertical position from the bottom of the screen -# default: 0.4 -box_position_v = 0.4 +# default: 0.5 +box_position_v = 0.5 # Title to show at the top of the main box # If set to null, none will be shown diff --git a/src/config/Config.zig b/src/config/Config.zig index 90913ad..2ba8444 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -24,7 +24,7 @@ bigclock_seconds: bool = false, blank_box: bool = true, border_fg: u32 = 0x00FFFFFF, box_position_h: f32 = 0.5, -box_position_v: f32 = 0.4, +box_position_v: f32 = 0.5, box_title: ?[]const u8 = null, brightness_down_cmd: [:0]const u8 = build_options.prefix_directory ++ "/bin/brightnessctl -q -n s 10%-", brightness_down_key: ?[]const u8 = "F5", From 014a00d33ec76358824dcbabab5ff1ca90347cfd Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Tue, 7 Jul 2026 22:35:06 +0200 Subject: [PATCH 171/176] Improve templates Signed-off-by: AnErrupTion --- .github/ISSUE_TEMPLATE/bug.yml | 17 ++++++++++++++--- .github/ISSUE_TEMPLATE/feature.yml | 7 +++++++ .github/pull_request_template.md | 4 ++-- 3 files changed, 23 insertions(+), 5 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug.yml b/.github/ISSUE_TEMPLATE/bug.yml index 1a6d57a..64eef9c 100644 --- a/.github/ISSUE_TEMPLATE/bug.yml +++ b/.github/ISSUE_TEMPLATE/bug.yml @@ -11,15 +11,19 @@ body: options: - label: I have looked for any other duplicate issues required: true - - label: I have reproduced the issue on a fresh install of my OS & Ly with default settings, except ones I will mention + - label: I have reproduced the issue with the default config of Ly + required: true + - label: I have reproduced the issue on a fresh install of Ly **as well as** my OS and my desktop environment/window manager, all with **default settings**, except ones I will mention required: false - label: I have confirmed this issue also occurs on the latest development version (found in the `master` branch) required: true + - label: I understand that this issue **will** be closed if requirements are not or not properly met + required: true - type: input id: version attributes: label: Ly version - description: The output of `ly --version`. Please note that only Ly v1.2.0 and above are supported. + description: The output of `ly --version`. Please note that only the latest release and the current version in development are supported. placeholder: 1.1.0-dev.12+2b0301c validations: required: true @@ -41,9 +45,16 @@ body: id: desktop attributes: label: OS + Desktop environment/Window manager - description: Which OS and DE (or WM) did you use when observing the problem? + description: Which operating system and DE (or WM) did you use when observing the problem? validations: required: true + - type: input + id: osimage + attributes: + label: OS version/snapshot used to reproduce + description: Which version (or snapshot if rolling release) of the OS did you use to reproduce the issue? This is only required if you have reproduced the issue on a fresh copy of the OS. + validations: + required: false - type: textarea id: reproduction attributes: diff --git a/.github/ISSUE_TEMPLATE/feature.yml b/.github/ISSUE_TEMPLATE/feature.yml index b9a26af..b4cc299 100644 --- a/.github/ISSUE_TEMPLATE/feature.yml +++ b/.github/ISSUE_TEMPLATE/feature.yml @@ -20,3 +20,10 @@ body: description: What do you want to be added? Describe the behavior clearly. validations: required: true + - type: textarea + id: alternatives + attributes: + label: Alternatives + description: Are there any alternatives to the new behavior you wish to see being added? + validations: + required: true diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index a32f0c2..606d00f 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -2,9 +2,9 @@ _Replace this with a brief description of your changes_ -## What existing issue does this resolve? +## What existing issue(s) does this resolve? -_Replace this with a reference to an existing issue, or N/A if there is none_ +_Replace this with a reference to (an) existing issue(s), or N/A if there is none_ ## Pre-requisites From d00780d6ce7f1c9552f013a4180ddf4885e83f10 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Wed, 8 Jul 2026 01:29:57 +0200 Subject: [PATCH 172/176] zig: Remove usage of [*c] Signed-off-by: AnErrupTion --- ly-core/src/interop.zig | 6 +++--- src/auth.zig | 10 ++++++---- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/ly-core/src/interop.zig b/ly-core/src/interop.zig index f04b2a4..d894485 100644 --- a/ly-core/src/interop.zig +++ b/ly-core/src/interop.zig @@ -28,7 +28,7 @@ pub const UsernameEntry = struct { gid: std.posix.gid_t, home: ?[]const u8, shell: ?[]const u8, - passwd_struct: [*c]pwd.passwd, + passwd_struct: [*]pwd.passwd, }; // Contains the platform-specific code @@ -381,8 +381,8 @@ pub fn setEnvironmentVariable(allocator: std.mem.Allocator, name: []const u8, va if (status != 0) return error.SetEnvironmentVariableFailed; } -pub fn putEnvironmentVariable(name_and_value: [*c]u8) !void { - const status = stdlib.putenv(name_and_value); +pub fn putEnvironmentVariable(name_and_value: []u8) !void { + const status = stdlib.putenv(name_and_value.ptr); if (status != 0) return error.PutEnvironmentVariableFailed; } diff --git a/src/auth.zig b/src/auth.zig index 00ae005..c9abf15 100644 --- a/src/auth.zig +++ b/src/auth.zig @@ -191,10 +191,12 @@ fn startSession( if (pam_env_vars == null) return error.GetEnvListFailed; const env_list = std.mem.span(pam_env_vars.?); - for (env_list) |env_var| { - if (env_var == null) continue; - try log_file.info(io, "auth/env", "setting pam environment variable: {s}", .{std.mem.span(env_var.?)}); - try interop.putEnvironmentVariable(env_var); + for (env_list) |maybe_env_var| { + if (maybe_env_var) |env_var| { + const env_var_slice = std.mem.span(env_var); + try log_file.info(io, "auth/env", "setting pam environment variable: {s}", .{env_var_slice}); + try interop.putEnvironmentVariable(env_var_slice); + } } const home_z = try allocator.dupeZ(u8, user_entry.home.?); From 84950136c96a7e22e2dbbd29da74e1ffd1d102bb Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Fri, 10 Jul 2026 00:15:07 +0200 Subject: [PATCH 173/176] positionWidgets: Less integer overflows possible Signed-off-by: AnErrupTion --- ly-ui/src/TerminalBuffer.zig | 3 ++- src/animations/Lua.zig | 4 +++- src/main.zig | 22 +++++++++++++++------- 3 files changed, 20 insertions(+), 9 deletions(-) diff --git a/ly-ui/src/TerminalBuffer.zig b/ly-ui/src/TerminalBuffer.zig index 7ead34f..4881a2e 100644 --- a/ly-ui/src/TerminalBuffer.zig +++ b/ly-ui/src/TerminalBuffer.zig @@ -269,7 +269,8 @@ pub fn runEventLoop( } } - try TerminalBuffer.presentBuffer(); + // We don't care about present errors here + TerminalBuffer.presentBuffer() catch {}; } if (inactivity_event_fn) |inactivity_fn| { diff --git a/src/animations/Lua.zig b/src/animations/Lua.zig index d212e03..2d2956f 100644 --- a/src/animations/Lua.zig +++ b/src/animations/Lua.zig @@ -129,8 +129,10 @@ fn draw(self: *Lua) void { cell.put(x, y) catch {}; if (self.lua_str) |str| for (str, 0..) |c, i| { + const dwidth = @divFloor(self.width, 2); + const dlen = @divFloor(str.len, 2); Cell.init(c, 0x00FFFFFF, 0).put( - @divFloor(self.width, 2) - @divFloor(str.len, 2) + i, + (if (dlen > dwidth) 0 else dwidth - dlen) + i, self.margin + 5, ) catch {}; }; diff --git a/src/main.zig b/src/main.zig index 85ababf..382ace4 100644 --- a/src/main.zig +++ b/src/main.zig @@ -2244,26 +2244,34 @@ fn positionWidgets(ptr: *anyopaque) !void { bb_width = @max(bb_width, clock_text_len); } - const max_v_position: f32 = @floatFromInt(state.buffer.height - bb_height - 1); - const max_h_position: f32 = @floatFromInt(state.buffer.width - bb_width - 1); + const dheight = if (bb_height + 1 > state.buffer.height) 1 else state.buffer.height - bb_height - 1; + const dwidth = if (bb_width + 1 > state.buffer.width) 1 else state.buffer.width - bb_width - 1; + + const max_v_position: f32 = @floatFromInt(dheight); + const max_h_position: f32 = @floatFromInt(dwidth); bb_height = @min(bb_height, state.buffer.height - 2); bb_width = @min(bb_width, state.buffer.width - 2); const v_space: f32 = @floatFromInt(state.buffer.height - bb_height); - const v_position: usize = @intFromFloat(std.math.clamp(v_space * state.config.box_position_v, 1.0, max_v_position)); + const v_position: usize = @intFromFloat(if (max_v_position < 1.0) 1.0 else std.math.clamp(v_space * state.config.box_position_v, 1.0, max_v_position)); const h_space: f32 = @floatFromInt(state.buffer.width - bb_width); - const h_position: usize = @intFromFloat(std.math.clamp(h_space * state.config.box_position_h, 1.0, max_h_position)); + const h_position: usize = @intFromFloat(if (max_h_position < 1.0) 1.0 else std.math.clamp(h_space * state.config.box_position_h, 1.0, max_h_position)); if (state.config.bigclock != .none) { + const cwidth = if (clock_text_len > bb_width) 0 else bb_width - clock_text_len; + state.bigclock_label.positionXY(TerminalBuffer.START_POSITION - .addX(h_position + (bb_width - clock_text_len) / 2) + .addX(h_position + cwidth / 2) .addY(v_position)); } + const bwidth = if (state.box.width > bb_width) 0 else bb_width - state.box.width; + const bheight = if (state.box.height > bb_height) 0 else bb_height - state.box.height; + state.box.positionXY(TerminalBuffer.START_POSITION - .addX(h_position + (bb_width - state.box.width) / 2) - .addY(v_position + (bb_height - state.box.height))); + .addX(h_position + bwidth / 2) + .addY(v_position + bheight)); state.info_line.label.positionY(state.box .childrenPosition()); From 7c2ae2738ca71c4df3b590c9ed4068911c23b527 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Fri, 10 Jul 2026 13:02:58 +0200 Subject: [PATCH 174/176] positionSingleWidget: Resist integer overflows Signed-off-by: AnErrupTion --- src/main.zig | 75 +++++++++++++++++++++++++++++++--------------------- 1 file changed, 45 insertions(+), 30 deletions(-) diff --git a/src/main.zig b/src/main.zig index 382ace4..31a8519 100644 --- a/src/main.zig +++ b/src/main.zig @@ -1950,8 +1950,9 @@ fn positionSingleWidget(state: *UiState, item: []const u8, current_x: *usize, cu state.shutdown_label.positionXY(Position.init(current_x.*, current_y)); current_x.* += width + 1; } else { - state.shutdown_label.positionXY(Position.init(current_x.* - width, current_y)); - current_x.* -= width + 1; + const dwidth = if (width > current_x.*) 0 else current_x.* - width; + state.shutdown_label.positionXY(Position.init(dwidth, current_y)); + if (width + 1 <= current_x.*) current_x.* -= width + 1; } return true; } else if (std.mem.eql(u8, item, "restart")) { @@ -1961,8 +1962,9 @@ fn positionSingleWidget(state: *UiState, item: []const u8, current_x: *usize, cu state.restart_label.positionXY(Position.init(current_x.*, current_y)); current_x.* += width + 1; } else { - state.restart_label.positionXY(Position.init(current_x.* - width, current_y)); - current_x.* -= width + 1; + const dwidth = if (width > current_x.*) 0 else current_x.* - width; + state.restart_label.positionXY(Position.init(dwidth, current_y)); + if (width + 1 <= current_x.*) current_x.* -= width + 1; } return true; } else if (std.mem.eql(u8, item, "britup")) { @@ -1972,8 +1974,9 @@ fn positionSingleWidget(state: *UiState, item: []const u8, current_x: *usize, cu state.brightness_up_label.positionXY(Position.init(current_x.*, current_y)); current_x.* += width + 1; } else { - state.brightness_up_label.positionXY(Position.init(current_x.* - width, current_y)); - current_x.* -= width + 1; + const dwidth = if (width > current_x.*) 0 else current_x.* - width; + state.brightness_up_label.positionXY(Position.init(dwidth, current_y)); + if (width + 1 <= current_x.*) current_x.* -= width + 1; } return true; } else if (std.mem.eql(u8, item, "britdown")) { @@ -1983,8 +1986,9 @@ fn positionSingleWidget(state: *UiState, item: []const u8, current_x: *usize, cu state.brightness_down_label.positionXY(Position.init(current_x.*, current_y)); current_x.* += width + 1; } else { - state.brightness_down_label.positionXY(Position.init(current_x.* - width, current_y)); - current_x.* -= width + 1; + const dwidth = if (width > current_x.*) 0 else current_x.* - width; + state.brightness_down_label.positionXY(Position.init(dwidth, current_y)); + if (width + 1 <= current_x.*) current_x.* -= width + 1; } return true; } else if (std.mem.eql(u8, item, "password")) { @@ -1994,8 +1998,9 @@ fn positionSingleWidget(state: *UiState, item: []const u8, current_x: *usize, cu state.toggle_password_label.positionXY(Position.init(current_x.*, current_y)); current_x.* += width + 1; } else { - state.toggle_password_label.positionXY(Position.init(current_x.* - width, current_y)); - current_x.* -= width + 1; + const dwidth = if (width > current_x.*) 0 else current_x.* - width; + state.toggle_password_label.positionXY(Position.init(dwidth, current_y)); + if (width + 1 <= current_x.*) current_x.* -= width + 1; } return true; } else if (std.mem.eql(u8, item, "clock") or std.mem.eql(u8, item, "time")) { @@ -2005,8 +2010,9 @@ fn positionSingleWidget(state: *UiState, item: []const u8, current_x: *usize, cu state.clock_label.positionXY(Position.init(current_x.*, current_y)); current_x.* += width + 1; } else { - state.clock_label.positionXY(Position.init(current_x.* - width, current_y)); - current_x.* -= width + 1; + const dwidth = if (width > current_x.*) 0 else current_x.* - width; + state.clock_label.positionXY(Position.init(dwidth, current_y)); + if (width + 1 <= current_x.*) current_x.* -= width + 1; } return true; } else if (std.mem.eql(u8, item, "tty")) { @@ -2015,8 +2021,9 @@ fn positionSingleWidget(state: *UiState, item: []const u8, current_x: *usize, cu state.tty_label.positionXY(Position.init(current_x.*, current_y)); current_x.* += width + 1; } else { - state.tty_label.positionXY(Position.init(current_x.* - width, current_y)); - current_x.* -= width + 1; + const dwidth = if (width > current_x.*) 0 else current_x.* - width; + state.tty_label.positionXY(Position.init(dwidth, current_y)); + if (width + 1 <= current_x.*) current_x.* -= width + 1; } return true; } else if (std.mem.eql(u8, item, "battery")) { @@ -2026,8 +2033,9 @@ fn positionSingleWidget(state: *UiState, item: []const u8, current_x: *usize, cu state.battery_label.positionXY(Position.init(current_x.*, current_y)); current_x.* += width + 1; } else { - state.battery_label.positionXY(Position.init(current_x.* - width, current_y)); - current_x.* -= width + 1; + const dwidth = if (width > current_x.*) 0 else current_x.* - width; + state.battery_label.positionXY(Position.init(dwidth, current_y)); + if (width + 1 <= current_x.*) current_x.* -= width + 1; } return true; } else if (std.mem.eql(u8, item, "version")) { @@ -2036,8 +2044,9 @@ fn positionSingleWidget(state: *UiState, item: []const u8, current_x: *usize, cu state.version_label.positionXY(Position.init(current_x.*, current_y)); current_x.* += width + 1; } else { - state.version_label.positionXY(Position.init(current_x.* - width, current_y)); - current_x.* -= width + 1; + const dwidth = if (width > current_x.*) 0 else current_x.* - width; + state.version_label.positionXY(Position.init(dwidth, current_y)); + if (width + 1 <= current_x.*) current_x.* -= width + 1; } return true; } else if (std.mem.eql(u8, item, "numlock")) { @@ -2046,8 +2055,9 @@ fn positionSingleWidget(state: *UiState, item: []const u8, current_x: *usize, cu state.numlock_label.positionXY(Position.init(current_x.*, current_y)); current_x.* += width + 1; } else { - state.numlock_label.positionXY(Position.init(current_x.* - width, current_y)); - current_x.* -= width + 1; + const dwidth = if (width > current_x.*) 0 else current_x.* - width; + state.numlock_label.positionXY(Position.init(dwidth, current_y)); + if (width + 1 <= current_x.*) current_x.* -= width + 1; } return true; } else if (std.mem.eql(u8, item, "capslock")) { @@ -2056,8 +2066,9 @@ fn positionSingleWidget(state: *UiState, item: []const u8, current_x: *usize, cu state.capslock_label.positionXY(Position.init(current_x.*, current_y)); current_x.* += width + 1; } else { - state.capslock_label.positionXY(Position.init(current_x.* - width, current_y)); - current_x.* -= width + 1; + const dwidth = if (width > current_x.*) 0 else current_x.* - width; + state.capslock_label.positionXY(Position.init(dwidth, current_y)); + if (width + 1 <= current_x.*) current_x.* -= width + 1; } return true; } else if (std.mem.eql(u8, item, "labels")) { @@ -2068,8 +2079,9 @@ fn positionSingleWidget(state: *UiState, item: []const u8, current_x: *usize, cu info.lbl.positionXY(Position.init(current_x.*, current_y)); current_x.* += width + 1; } else { - info.lbl.positionXY(Position.init(current_x.* - width, current_y)); - current_x.* -= width + 1; + const dwidth = if (width > current_x.*) 0 else current_x.* - width; + info.lbl.positionXY(Position.init(dwidth, current_y)); + if (width + 1 <= current_x.*) current_x.* -= width + 1; } positioned.labels[i] = true; } @@ -2094,8 +2106,9 @@ fn positionSingleWidget(state: *UiState, item: []const u8, current_x: *usize, cu local_x = state.buffer.width - state.edge_margin.x; if (is_top) local_y += 1 else local_y -= 1; } - bind.lbl.positionXY(Position.init(local_x - width, local_y)); - local_x -= width + 1; + const dwidth = if (width > local_x) 0 else local_x - width; + bind.lbl.positionXY(Position.init(dwidth, current_y)); + if (width + 1 <= local_x) local_x -= width + 1; } positioned.binds[i] = true; } @@ -2111,8 +2124,9 @@ fn positionSingleWidget(state: *UiState, item: []const u8, current_x: *usize, cu info.lbl.positionXY(Position.init(current_x.*, current_y)); current_x.* += width + 1; } else { - info.lbl.positionXY(Position.init(current_x.* - width, current_y)); - current_x.* -= width + 1; + const dwidth = if (width > current_x.*) 0 else current_x.* - width; + info.lbl.positionXY(Position.init(dwidth, current_y)); + if (width + 1 <= current_x.*) current_x.* -= width + 1; } positioned.labels[i] = true; return true; @@ -2128,8 +2142,9 @@ fn positionSingleWidget(state: *UiState, item: []const u8, current_x: *usize, cu bind.lbl.positionXY(Position.init(current_x.*, current_y)); current_x.* += width + 1; } else { - bind.lbl.positionXY(Position.init(current_x.* - width, current_y)); - current_x.* -= width + 1; + const dwidth = if (width > current_x.*) 0 else current_x.* - width; + bind.lbl.positionXY(Position.init(dwidth, current_y)); + if (width + 1 <= current_x.*) current_x.* -= width + 1; } positioned.binds[i] = true; return true; From b6fba46b087cd27fefa4d67c0a3b0eb084c7b016 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Tue, 28 Jul 2026 01:38:48 +0200 Subject: [PATCH 175/176] GPA: Add more tracking in Debug Signed-off-by: AnErrupTion --- src/main.zig | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/main.zig b/src/main.zig index 31a8519..1d0db99 100644 --- a/src/main.zig +++ b/src/main.zig @@ -149,8 +149,11 @@ pub fn main(init: std.process.Init) !void { } } - var gpa = std.heap.DebugAllocator(.{}).init; - defer _ = gpa.deinit(); + var gpa: std.heap.DebugAllocator(.{ + .never_unmap = builtin.mode == .Debug, + .retain_metadata = builtin.mode == .Debug, + }) = .init; + defer if (gpa.deinit() == .leak) std.log.err("attention please, memory has been leaked!", .{}); state.allocator = gpa.allocator(); From a22805d44b7f277d5901036edc11d01d88b749ab Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Fri, 31 Jul 2026 23:52:19 +0200 Subject: [PATCH 176/176] Auth: Handle symlinks for session log Signed-off-by: AnErrupTion --- src/auth.zig | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/auth.zig b/src/auth.zig index c9abf15..7d9c66c 100644 --- a/src/auth.zig +++ b/src/auth.zig @@ -574,7 +574,15 @@ fn executeCmd(global_log_file: *LogFile, allocator: std.mem.Allocator, io: std.I fn redirectStandardStreams(global_log_file: *LogFile, io: std.Io, session_log: []const u8, create: bool) !std.Io.File { create_session_log_dir: { const session_log_dir = std.Io.Dir.path.dirname(session_log) orelse break :create_session_log_dir; - std.Io.Dir.cwd().createDirPath(io, session_log_dir) catch |err| { + + var buffer = std.mem.zeroes([std.Io.Dir.max_path_bytes]u8); + const len = std.Io.Dir.cwd().realPathFile(io, session_log_dir, &buffer) catch |err| { + try global_log_file.err(io, "auth/sys", "failed to resolve path for session log file directory: {s}", .{@errorName(err)}); + return err; + }; + const resolved_path = buffer[0..len]; + + std.Io.Dir.cwd().createDirPath(io, resolved_path) catch |err| { try global_log_file.err(io, "auth/sys", "failed to create session log file directory: {s}", .{@errorName(err)}); return err; };