From 4eb4e15e43d307d1a3795768fdd23ee837d189d0 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Wed, 3 Jul 2024 09:44:23 +0200 Subject: [PATCH 001/530] Start v1.1.0 development cycle Signed-off-by: AnErrupTion --- build.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.zig b/build.zig index 65c06a2..4b48eff 100644 --- a/build.zig +++ b/build.zig @@ -1,6 +1,6 @@ const std = @import("std"); -const ly_version = std.SemanticVersion{ .major = 1, .minor = 0, .patch = 0 }; +const ly_version = std.SemanticVersion{ .major = 1, .minor = 1, .patch = 0 }; var dest_directory: []const u8 = undefined; var data_directory: []const u8 = undefined; var exe_name: []const u8 = undefined; From cbe7b37564f307fddfeba3732c68d5024d30f4f7 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Wed, 3 Jul 2024 09:50:56 +0200 Subject: [PATCH 002/530] Fix dest_directory embedded in binary Signed-off-by: AnErrupTion --- build.zig | 36 +++++++++++++++++++----------------- 1 file changed, 19 insertions(+), 17 deletions(-) diff --git a/build.zig b/build.zig index 4b48eff..efb51eb 100644 --- a/build.zig +++ b/build.zig @@ -6,13 +6,15 @@ var data_directory: []const u8 = undefined; var exe_name: []const u8 = undefined; pub fn build(b: *std.Build) !void { - dest_directory = b.option([]const u8, "dest_directory", "Specify a dest directory for installation") orelse ""; - data_directory = b.option([]const u8, "data_directory", "Specify a default data directory (default is /etc/ly)") orelse "/etc/ly"; - data_directory = try std.fs.path.join(b.allocator, &[_][]const u8{ dest_directory, data_directory }); + dest_directory = b.option([]const u8, "dest_directory", "Specify a destination directory for installation") orelse ""; + data_directory = b.option([]const u8, "data_directory", "Specify a default data directory (default is /etc/ly). This path gets embedded into the binary") orelse "/etc/ly"; exe_name = b.option([]const u8, "name", "Specify installed executable file name (default is ly)") orelse "ly"; + const bin_directory = try b.allocator.dupe(u8, data_directory); + data_directory = try std.fs.path.join(b.allocator, &[_][]const u8{ dest_directory, data_directory }); + const build_options = b.addOptions(); - build_options.addOption([]const u8, "data_directory", data_directory); + build_options.addOption([]const u8, "data_directory", bin_directory); const version_str = try getVersionStr(b, "ly", ly_version); @@ -113,7 +115,7 @@ pub fn ServiceInstaller(comptime init_system: InitSystem) type { .Openrc => { const service_path = try std.fs.path.join(allocator, &[_][]const u8{ dest_directory, "/etc/init.d" }); std.fs.cwd().makePath(service_path) catch {}; - var service_dir = std.fs.openDirAbsolute(service_path, .{}) catch unreachable; + var service_dir = std.fs.cwd().openDir(service_path, .{}) catch unreachable; defer service_dir.close(); try std.fs.cwd().copyFile("res/ly-openrc", service_dir, exe_name, .{ .override_mode = 755 }); @@ -121,7 +123,7 @@ pub fn ServiceInstaller(comptime init_system: InitSystem) type { .Runit => { const service_path = try std.fs.path.join(allocator, &[_][]const u8{ dest_directory, "/etc/sv/ly" }); std.fs.cwd().makePath(service_path) catch {}; - var service_dir = std.fs.openDirAbsolute(service_path, .{}) catch unreachable; + var service_dir = std.fs.cwd().openDir(service_path, .{}) catch unreachable; defer service_dir.close(); try std.fs.cwd().copyFile("res/ly-runit-service/conf", service_dir, "conf", .{}); @@ -131,7 +133,7 @@ pub fn ServiceInstaller(comptime init_system: InitSystem) type { .Systemd => { const service_path = try std.fs.path.join(allocator, &[_][]const u8{ dest_directory, "/usr/lib/systemd/system" }); std.fs.cwd().makePath(service_path) catch {}; - var service_dir = std.fs.openDirAbsolute(service_path, .{}) catch unreachable; + var service_dir = std.fs.cwd().openDir(service_path, .{}) catch unreachable; defer service_dir.close(); try std.fs.cwd().copyFile("res/ly.service", service_dir, "ly.service", .{ .override_mode = 644 }); @@ -161,14 +163,14 @@ fn install_ly(allocator: std.mem.Allocator, install_config: bool) !void { }; } - var executable_dir = std.fs.openDirAbsolute(exe_path, .{}) catch unreachable; + var executable_dir = std.fs.cwd().openDir(exe_path, .{}) catch unreachable; defer executable_dir.close(); try current_dir.copyFile("zig-out/bin/ly", executable_dir, exe_name, .{}); } { - var config_dir = std.fs.openDirAbsolute(data_directory, .{}) catch unreachable; + var config_dir = std.fs.cwd().openDir(data_directory, .{}) catch unreachable; defer config_dir.close(); if (install_config) { @@ -179,7 +181,7 @@ fn install_ly(allocator: std.mem.Allocator, install_config: bool) !void { } { - var lang_dir = std.fs.openDirAbsolute(lang_path, .{}) catch unreachable; + var lang_dir = std.fs.cwd().openDir(lang_path, .{}) catch unreachable; defer lang_dir.close(); try current_dir.copyFile("res/lang/cat.ini", lang_dir, "cat.ini", .{}); @@ -208,7 +210,7 @@ fn install_ly(allocator: std.mem.Allocator, install_config: bool) !void { }; } - var pam_dir = std.fs.openDirAbsolute(pam_path, .{}) catch unreachable; + var pam_dir = std.fs.cwd().openDir(pam_path, .{}) catch unreachable; defer pam_dir.close(); try current_dir.copyFile("res/pam.d/ly", pam_dir, "ly", .{ .override_mode = 644 }); @@ -217,27 +219,27 @@ fn install_ly(allocator: std.mem.Allocator, install_config: bool) !void { pub fn uninstallall(step: *std.Build.Step, progress: *std.Progress.Node) !void { _ = progress; - try std.fs.deleteTreeAbsolute(data_directory); + try std.fs.cwd().deleteTree(data_directory); const allocator = step.owner.allocator; const exe_path = try std.fs.path.join(allocator, &[_][]const u8{ dest_directory, "/usr/bin/", exe_name }); - try std.fs.deleteFileAbsolute(exe_path); + try std.fs.cwd().deleteFile(exe_path); const pam_path = try std.fs.path.join(allocator, &[_][]const u8{ dest_directory, "/etc/pam.d/ly" }); - try std.fs.deleteFileAbsolute(pam_path); + try std.fs.cwd().deleteFile(pam_path); const systemd_service_path = try std.fs.path.join(allocator, &[_][]const u8{ dest_directory, "/usr/lib/systemd/system/ly.service" }); - std.fs.deleteFileAbsolute(systemd_service_path) catch { + std.fs.cwd().deleteFile(systemd_service_path) catch { std.debug.print("warn: systemd service not found.\n", .{}); }; const openrc_service_path = try std.fs.path.join(allocator, &[_][]const u8{ dest_directory, "/etc/init.d/ly" }); - std.fs.deleteFileAbsolute(openrc_service_path) catch { + std.fs.cwd().deleteFile(openrc_service_path) catch { std.debug.print("warn: openrc service not found.\n", .{}); }; const runit_service_path = try std.fs.path.join(allocator, &[_][]const u8{ dest_directory, "/etc/sv/ly" }); - std.fs.deleteTreeAbsolute(runit_service_path) catch { + std.fs.cwd().deleteTree(runit_service_path) catch { std.debug.print("warn: runit service not found.\n", .{}); }; } From 5cdd6af7386e9d8cb60e854d17cc79c1ac55d3f4 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Wed, 3 Jul 2024 09:52:00 +0200 Subject: [PATCH 003/530] Start Ly v1.0.1 development cycle Signed-off-by: AnErrupTion --- build.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.zig b/build.zig index 65c06a2..1d747f1 100644 --- a/build.zig +++ b/build.zig @@ -1,6 +1,6 @@ const std = @import("std"); -const ly_version = std.SemanticVersion{ .major = 1, .minor = 0, .patch = 0 }; +const ly_version = std.SemanticVersion{ .major = 1, .minor = 0, .patch = 1 }; var dest_directory: []const u8 = undefined; var data_directory: []const u8 = undefined; var exe_name: []const u8 = undefined; From 53d252232f465e7b8cb099ae2678ea195c299827 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Wed, 3 Jul 2024 09:52:24 +0200 Subject: [PATCH 004/530] Backport: Fix dest_directory embedded in binary Signed-off-by: AnErrupTion --- build.zig | 36 +++++++++++++++++++----------------- 1 file changed, 19 insertions(+), 17 deletions(-) diff --git a/build.zig b/build.zig index 1d747f1..adcbfcc 100644 --- a/build.zig +++ b/build.zig @@ -6,13 +6,15 @@ var data_directory: []const u8 = undefined; var exe_name: []const u8 = undefined; pub fn build(b: *std.Build) !void { - dest_directory = b.option([]const u8, "dest_directory", "Specify a dest directory for installation") orelse ""; - data_directory = b.option([]const u8, "data_directory", "Specify a default data directory (default is /etc/ly)") orelse "/etc/ly"; - data_directory = try std.fs.path.join(b.allocator, &[_][]const u8{ dest_directory, data_directory }); + dest_directory = b.option([]const u8, "dest_directory", "Specify a destination directory for installation") orelse ""; + data_directory = b.option([]const u8, "data_directory", "Specify a default data directory (default is /etc/ly). This path gets embedded into the binary") orelse "/etc/ly"; exe_name = b.option([]const u8, "name", "Specify installed executable file name (default is ly)") orelse "ly"; + const bin_directory = try b.allocator.dupe(u8, data_directory); + data_directory = try std.fs.path.join(b.allocator, &[_][]const u8{ dest_directory, data_directory }); + const build_options = b.addOptions(); - build_options.addOption([]const u8, "data_directory", data_directory); + build_options.addOption([]const u8, "data_directory", bin_directory); const version_str = try getVersionStr(b, "ly", ly_version); @@ -113,7 +115,7 @@ pub fn ServiceInstaller(comptime init_system: InitSystem) type { .Openrc => { const service_path = try std.fs.path.join(allocator, &[_][]const u8{ dest_directory, "/etc/init.d" }); std.fs.cwd().makePath(service_path) catch {}; - var service_dir = std.fs.openDirAbsolute(service_path, .{}) catch unreachable; + var service_dir = std.fs.cwd().openDir(service_path, .{}) catch unreachable; defer service_dir.close(); try std.fs.cwd().copyFile("res/ly-openrc", service_dir, exe_name, .{ .override_mode = 755 }); @@ -121,7 +123,7 @@ pub fn ServiceInstaller(comptime init_system: InitSystem) type { .Runit => { const service_path = try std.fs.path.join(allocator, &[_][]const u8{ dest_directory, "/etc/sv/ly" }); std.fs.cwd().makePath(service_path) catch {}; - var service_dir = std.fs.openDirAbsolute(service_path, .{}) catch unreachable; + var service_dir = std.fs.cwd().openDir(service_path, .{}) catch unreachable; defer service_dir.close(); try std.fs.cwd().copyFile("res/ly-runit-service/conf", service_dir, "conf", .{}); @@ -131,7 +133,7 @@ pub fn ServiceInstaller(comptime init_system: InitSystem) type { .Systemd => { const service_path = try std.fs.path.join(allocator, &[_][]const u8{ dest_directory, "/usr/lib/systemd/system" }); std.fs.cwd().makePath(service_path) catch {}; - var service_dir = std.fs.openDirAbsolute(service_path, .{}) catch unreachable; + var service_dir = std.fs.cwd().openDir(service_path, .{}) catch unreachable; defer service_dir.close(); try std.fs.cwd().copyFile("res/ly.service", service_dir, "ly.service", .{ .override_mode = 644 }); @@ -161,14 +163,14 @@ fn install_ly(allocator: std.mem.Allocator, install_config: bool) !void { }; } - var executable_dir = std.fs.openDirAbsolute(exe_path, .{}) catch unreachable; + var executable_dir = std.fs.cwd().openDir(exe_path, .{}) catch unreachable; defer executable_dir.close(); try current_dir.copyFile("zig-out/bin/ly", executable_dir, exe_name, .{}); } { - var config_dir = std.fs.openDirAbsolute(data_directory, .{}) catch unreachable; + var config_dir = std.fs.cwd().openDir(data_directory, .{}) catch unreachable; defer config_dir.close(); if (install_config) { @@ -179,7 +181,7 @@ fn install_ly(allocator: std.mem.Allocator, install_config: bool) !void { } { - var lang_dir = std.fs.openDirAbsolute(lang_path, .{}) catch unreachable; + var lang_dir = std.fs.cwd().openDir(lang_path, .{}) catch unreachable; defer lang_dir.close(); try current_dir.copyFile("res/lang/cat.ini", lang_dir, "cat.ini", .{}); @@ -208,7 +210,7 @@ fn install_ly(allocator: std.mem.Allocator, install_config: bool) !void { }; } - var pam_dir = std.fs.openDirAbsolute(pam_path, .{}) catch unreachable; + var pam_dir = std.fs.cwd().openDir(pam_path, .{}) catch unreachable; defer pam_dir.close(); try current_dir.copyFile("res/pam.d/ly", pam_dir, "ly", .{ .override_mode = 644 }); @@ -217,27 +219,27 @@ fn install_ly(allocator: std.mem.Allocator, install_config: bool) !void { pub fn uninstallall(step: *std.Build.Step, progress: *std.Progress.Node) !void { _ = progress; - try std.fs.deleteTreeAbsolute(data_directory); + try std.fs.cwd().deleteTree(data_directory); const allocator = step.owner.allocator; const exe_path = try std.fs.path.join(allocator, &[_][]const u8{ dest_directory, "/usr/bin/", exe_name }); - try std.fs.deleteFileAbsolute(exe_path); + try std.fs.cwd().deleteFile(exe_path); const pam_path = try std.fs.path.join(allocator, &[_][]const u8{ dest_directory, "/etc/pam.d/ly" }); - try std.fs.deleteFileAbsolute(pam_path); + try std.fs.cwd().deleteFile(pam_path); const systemd_service_path = try std.fs.path.join(allocator, &[_][]const u8{ dest_directory, "/usr/lib/systemd/system/ly.service" }); - std.fs.deleteFileAbsolute(systemd_service_path) catch { + std.fs.cwd().deleteFile(systemd_service_path) catch { std.debug.print("warn: systemd service not found.\n", .{}); }; const openrc_service_path = try std.fs.path.join(allocator, &[_][]const u8{ dest_directory, "/etc/init.d/ly" }); - std.fs.deleteFileAbsolute(openrc_service_path) catch { + std.fs.cwd().deleteFile(openrc_service_path) catch { std.debug.print("warn: openrc service not found.\n", .{}); }; const runit_service_path = try std.fs.path.join(allocator, &[_][]const u8{ dest_directory, "/etc/sv/ly" }); - std.fs.deleteTreeAbsolute(runit_service_path) catch { + std.fs.cwd().deleteTree(runit_service_path) catch { std.debug.print("warn: runit service not found.\n", .{}); }; } From dc8d143fac5b1a621f2b13f16c56f7a68d0065ff Mon Sep 17 00:00:00 2001 From: tubi16 <49732553+tubi16@users.noreply.github.com> Date: Thu, 4 Jul 2024 14:17:56 +0300 Subject: [PATCH 005/530] Add brightness control support with brightnessctl (#626) * Added key binds to control brightness I added keybinds to control brightness with brightnessctl. F5 to decrease brightness. f6 to increase brightness. * Update src/main.zig Co-authored-by: ShiningLea * added proper keybinds and configs for brightness control * Update src/main.zig Co-authored-by: ShiningLea * code improvement and changes * updated en.ini --------- Co-authored-by: ShiningLea --- res/config.ini | 5 +++++ res/lang/en.ini | 1 + src/config/Config.zig | 4 ++++ src/config/Lang.zig | 3 +++ src/interop.zig | 4 ++++ src/main.zig | 35 +++++++++++++++++++++++++++++++++++ 6 files changed, 52 insertions(+) diff --git a/res/config.ini b/res/config.ini index 68a39bb..2738c01 100644 --- a/res/config.ini +++ b/res/config.ini @@ -160,3 +160,8 @@ xauth_cmd = /usr/bin/xauth # Xorg desktop environments xsessions = /usr/share/xsessions + +# Brightness control +brightness_down_key = F5 +brightness_up_key = F6 +Brightness_change = 10 \ No newline at end of file diff --git a/res/lang/en.ini b/res/lang/en.ini index d49882a..56c4fd7 100644 --- a/res/lang/en.ini +++ b/res/lang/en.ini @@ -2,6 +2,7 @@ authenticating = authenticating... capslock = capslock err_alloc = failed memory allocation err_bounds = out-of-bounds index +err_brightness_change = failed to change brightness err_chdir = failed to open home folder err_console_dev = failed to access console err_dgn_oob = log message diff --git a/src/config/Config.zig b/src/config/Config.zig index bddda19..e7d2c83 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -51,3 +51,7 @@ xinitrc: ?[]const u8 = "~/.xinitrc", x_cmd_setup: []const u8 = build_options.data_directory ++ "/xsetup.sh", xauth_cmd: []const u8 = "/usr/bin/xauth", xsessions: []const u8 = "/usr/share/xsessions", +brightness_down_key: []const u8 = "F5", +brightness_up_key: []const u8 = "F6", +brightnessctl: []const u8 = "/usr/bin/brightnessctl", +brightness_change: []const u8 = "10", diff --git a/src/config/Lang.zig b/src/config/Lang.zig index 8c4051c..58a9703 100644 --- a/src/config/Lang.zig +++ b/src/config/Lang.zig @@ -2,6 +2,7 @@ authenticating: []const u8 = "authenticating...", capslock: []const u8 = "capslock", err_alloc: []const u8 = "failed memory allocation", err_bounds: []const u8 = "out-of-bounds index", +err_brightness_change: []const u8 = "failed to change brightness", err_chdir: []const u8 = "failed to open home folder", err_console_dev: []const u8 = "failed to access console", err_dgn_oob: []const u8 = "log message", @@ -54,3 +55,5 @@ sleep: []const u8 = "sleep", wayland: []const u8 = "wayland", xinitrc: [:0]const u8 = "xinitrc", x11: []const u8 = "x11", +brightness_down: []const u8 = "decrease brightness", +brightness_up: []const u8 = "increase brightness", diff --git a/src/interop.zig b/src/interop.zig index 14ce55a..b378f34 100644 --- a/src/interop.zig +++ b/src/interop.zig @@ -16,6 +16,10 @@ pub const xcb = @cImport({ @cInclude("xcb/xcb.h"); }); +pub const unistd = @cImport({ + @cInclude("unistd.h"); +}); + pub const c_size = u64; pub const c_uid = u32; pub const c_gid = u32; diff --git a/src/main.zig b/src/main.zig index 6171361..62e1a04 100644 --- a/src/main.zig +++ b/src/main.zig @@ -21,6 +21,7 @@ const utils = @import("tui/utils.zig"); const Ini = ini.Ini; const termbox = interop.termbox; +const unistd = interop.unistd; var session_pid: std.posix.pid_t = -1; pub fn signalHandler(i: c_int) callconv(.C) void { @@ -245,6 +246,10 @@ pub fn main() !void { const restart_key = try std.fmt.parseInt(u8, config.restart_key[1..], 10); const restart_len = try utils.strWidth(lang.restart); const sleep_key = try std.fmt.parseInt(u8, config.sleep_key[1..], 10); + const brightness_down_key = try std.fmt.parseInt(u8, config.brightness_down_key[1..], 10); + const brightness_down_len = try utils.strWidth(lang.brightness_down); + const brightness_up_key = try std.fmt.parseInt(u8, config.brightness_up_key[1..], 10); + const brightness_up_len = try utils.strWidth(lang.brightness_up); var event: termbox.tb_event = undefined; var run = true; @@ -388,6 +393,20 @@ pub fn main() !void { buffer.drawLabel(lang.restart, length, 0); length += restart_len + 1; + buffer.drawLabel(config.brightness_down_key, length, 0); + length += config.brightness_down_key.len + 1; + buffer.drawLabel(" ", length - 1, 0); + + buffer.drawLabel(lang.brightness_down, length, 0); + length += brightness_down_len + 1; + + buffer.drawLabel(config.brightness_up_key, length, 0); + length += config.brightness_up_key.len + 1; + buffer.drawLabel(" ", length - 1, 0); + + buffer.drawLabel(lang.brightness_up, length, 0); + length += brightness_up_len + 1; + if (config.sleep_cmd != null) { buffer.drawLabel(config.sleep_key, length, 0); length += config.sleep_key.len + 1; @@ -482,6 +501,22 @@ pub fn main() !void { var sleep = std.ChildProcess.init(&[_][]const u8{ "/bin/sh", "-c", sleep_cmd }, allocator); _ = sleep.spawnAndWait() catch .{}; } + } else if (pressed_key == brightness_down_key and unistd.access(&config.brightnessctl[0], unistd.X_OK) == 0) brightness_change: { + const brightness_str = std.fmt.allocPrint(allocator, "{s}%-", .{config.brightness_change}) catch { + try info_line.setText(lang.err_brightness_change); + break :brightness_change; + }; + defer allocator.free(brightness_str); + var brightness = std.ChildProcess.init(&[_][]const u8{ config.brightnessctl, "-q", "s", brightness_str }, allocator); + _ = brightness.spawnAndWait() catch .{}; + } else if (pressed_key == brightness_up_key and unistd.access(&config.brightnessctl[0], unistd.X_OK) == 0) brightness_change: { + const brightness_str = std.fmt.allocPrint(allocator, "+{s}%", .{config.brightness_change}) catch { + try info_line.setText(lang.err_brightness_change); + break :brightness_change; + }; + defer allocator.free(brightness_str); + var brightness = std.ChildProcess.init(&[_][]const u8{ config.brightnessctl, "-q", "s", brightness_str }, allocator); + _ = brightness.spawnAndWait() catch .{}; } }, termbox.TB_KEY_CTRL_C => run = false, From c6d7d177b73ae7ce8e09c3e25ab9d41a873fdd1f Mon Sep 17 00:00:00 2001 From: liesen Date: Fri, 12 Jul 2024 23:32:46 +0300 Subject: [PATCH 006/530] Fix incorrect shebang in xsetup.sh (#640) --- res/xsetup.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/res/xsetup.sh b/res/xsetup.sh index 487a078..9418530 100755 --- a/res/xsetup.sh +++ b/res/xsetup.sh @@ -1,4 +1,4 @@ -#! /bin/sh +#!/bin/sh # Xsession - run as user # Copyright (C) 2016 Pier Luigi Fiorini From 5f8fbe381cc2f72752f67c30227f37ab0600fab7 Mon Sep 17 00:00:00 2001 From: simonfogliato Date: Fri, 12 Jul 2024 13:37:18 -0700 Subject: [PATCH 007/530] Fix documentation issue about DOOM animation (#647) --- readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/readme.md b/readme.md index 1402122..ad63e5c 100644 --- a/readme.md +++ b/readme.md @@ -196,7 +196,7 @@ Take a look at your .xsession if X doesn't start, as it can interfere ## PSX DOOM fire animation To enable the famous PSX DOOM fire described by [Fabien Sanglard](http://fabiensanglard.net/doom_fire_psx/index.html), -just uncomment `animate = true` in `/etc/ly/config.ini`. You may also +just set `animation = doom` in `/etc/ly/config.ini`. You may also disable the main box borders with `hide_borders = true`. ## Additional Information From e4abf79ad5d5548d64981293409efbde2bb3219d Mon Sep 17 00:00:00 2001 From: jinzhongjia Date: Sat, 13 Jul 2024 04:40:01 +0800 Subject: [PATCH 008/530] Support Zig 0.13.0 and setting default TTY at build time (#632) * feat: support zig `0.13.0` * 12 compatible * update clap * feat: add default tty to build option * little fix * update `zigini` --- .gitignore | 3 ++- build.zig | 34 +++++++++++++++++++++++++--------- build.zig.zon | 8 ++++---- src/config/Config.zig | 2 +- src/main.zig | 4 ++-- 5 files changed, 34 insertions(+), 17 deletions(-) diff --git a/.gitignore b/.gitignore index 60f36fa..de08f4f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ .idea/ zig-cache/ zig-out/ -valgrind.log \ No newline at end of file +valgrind.log +.zig-cache diff --git a/build.zig b/build.zig index efb51eb..588465c 100644 --- a/build.zig +++ b/build.zig @@ -1,13 +1,30 @@ const std = @import("std"); +const builtin = @import("builtin"); + +const min_zig_string = "0.12.0"; +const current_zig = builtin.zig_version; + +// Implementing zig version detection through compile time +comptime { + const min_zig = std.SemanticVersion.parse(min_zig_string) catch unreachable; + if (current_zig.order(min_zig) == .lt) { + @compileError(std.fmt.comptimePrint("Your Zig version v{} does not meet the minimum build requirement of v{}", .{ current_zig, min_zig })); + } +} const ly_version = std.SemanticVersion{ .major = 1, .minor = 1, .patch = 0 }; var dest_directory: []const u8 = undefined; var data_directory: []const u8 = undefined; +var default_tty: u8 = undefined; var exe_name: []const u8 = undefined; +const ProgressNode = if (current_zig.minor == 12) *std.Progress.Node else std.Progress.Node; + pub fn build(b: *std.Build) !void { dest_directory = b.option([]const u8, "dest_directory", "Specify a destination directory for installation") orelse ""; data_directory = b.option([]const u8, "data_directory", "Specify a default data directory (default is /etc/ly). This path gets embedded into the binary") orelse "/etc/ly"; + default_tty = b.option(u8, "default_tty", "set default TTY") orelse 2; + exe_name = b.option([]const u8, "name", "Specify installed executable file name (default is ly)") orelse "ly"; const bin_directory = try b.allocator.dupe(u8, data_directory); @@ -20,12 +37,14 @@ pub fn build(b: *std.Build) !void { build_options.addOption([]const u8, "version", version_str); + build_options.addOption(u8, "tty", default_tty); + const target = b.standardTargetOptions(.{}); const optimize = b.standardOptimizeOption(.{}); const exe = b.addExecutable(.{ .name = "ly", - .root_source_file = .{ .path = "src/main.zig" }, + .root_source_file = b.path("src/main.zig"), .target = target, .optimize = optimize, }); @@ -38,14 +57,14 @@ 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.addIncludePath(.{ .path = "include" }); + exe.addIncludePath(b.path("include")); exe.linkSystemLibrary("pam"); exe.linkSystemLibrary("xcb"); exe.linkLibC(); // HACK: Only fails with ReleaseSafe, so we'll override it. const translate_c = b.addTranslateC(.{ - .root_source_file = .{ .path = "include/termbox2.h" }, + .root_source_file = b.path("include/termbox2.h"), .target = target, .optimize = if (optimize == .ReleaseSafe) .ReleaseFast else optimize, }); @@ -94,8 +113,7 @@ pub fn build(b: *std.Build) !void { pub fn ExeInstaller(install_conf: bool) type { return struct { - pub fn make(step: *std.Build.Step, progress: *std.Progress.Node) !void { - _ = progress; + pub fn make(step: *std.Build.Step, _: ProgressNode) !void { try install_ly(step.owner.allocator, install_conf); } }; @@ -108,8 +126,7 @@ const InitSystem = enum { }; pub fn ServiceInstaller(comptime init_system: InitSystem) type { return struct { - pub fn make(step: *std.Build.Step, progress: *std.Progress.Node) !void { - _ = progress; + pub fn make(step: *std.Build.Step, _: ProgressNode) !void { const allocator = step.owner.allocator; switch (init_system) { .Openrc => { @@ -217,8 +234,7 @@ fn install_ly(allocator: std.mem.Allocator, install_config: bool) !void { } } -pub fn uninstallall(step: *std.Build.Step, progress: *std.Progress.Node) !void { - _ = progress; +pub fn uninstallall(step: *std.Build.Step, _: ProgressNode) !void { try std.fs.cwd().deleteTree(data_directory); const allocator = step.owner.allocator; diff --git a/build.zig.zon b/build.zig.zon index 85900c9..23ac4e4 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -4,12 +4,12 @@ .minimum_zig_version = "0.12.0", .dependencies = .{ .clap = .{ - .url = "https://github.com/Hejsil/zig-clap/archive/8c98e6404b22aafc0184e999d8f068b81cc22fa1.tar.gz", - .hash = "122014e73fd712190e109950837b97f6143f02d7e2b6986e1db70b6f4aadb5ba6a0d", + .url = "https://github.com/Hejsil/zig-clap/archive/refs/tags/0.9.1.tar.gz", + .hash = "122062d301a203d003547b414237229b09a7980095061697349f8bef41be9c30266b", }, .zigini = .{ - .url = "https://github.com/Kawaii-Ash/zigini/archive/ce1f322482099db058f5d9fdd05fbfa255d79723.tar.gz", - .hash = "1220e7a99793a0430e0a7c0b938cb3c98321035bc297e21cd0e2413cf740b4923b9f", + .url = "https://github.com/Kawaii-Ash/zigini/archive/refs/tags/0.2.2.tar.gz", + .hash = "1220afda2f3258cd0bb042dd3c2d5a35069ce1785c11325e65f136c5220013b36d00", }, }, .paths = .{""}, diff --git a/src/config/Config.zig b/src/config/Config.zig index e7d2c83..fac60ef 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -42,7 +42,7 @@ sleep_cmd: ?[]const u8 = null, sleep_key: []const u8 = "F3", term_reset_cmd: [:0]const u8 = "/usr/bin/tput reset", term_restore_cursor_cmd: []const u8 = "/usr/bin/tput cnorm", -tty: u8 = 2, +tty: u8 = build_options.tty, vi_mode: bool = false, wayland_cmd: []const u8 = build_options.data_directory ++ "/wsetup.sh", waylandsessions: []const u8 = "/usr/share/wayland-sessions", diff --git a/src/main.zig b/src/main.zig index 62e1a04..83630e3 100644 --- a/src/main.zig +++ b/src/main.zig @@ -498,7 +498,7 @@ pub fn main() !void { run = false; } else if (pressed_key == sleep_key) { if (config.sleep_cmd) |sleep_cmd| { - var sleep = std.ChildProcess.init(&[_][]const u8{ "/bin/sh", "-c", sleep_cmd }, allocator); + var sleep = std.process.Child.init(&[_][]const u8{ "/bin/sh", "-c", sleep_cmd }, allocator); _ = sleep.spawnAndWait() catch .{}; } } else if (pressed_key == brightness_down_key and unistd.access(&config.brightnessctl[0], unistd.X_OK) == 0) brightness_change: { @@ -619,7 +619,7 @@ pub fn main() !void { update = true; - var restore_cursor = std.ChildProcess.init(&[_][]const u8{ "/bin/sh", "-c", config.term_restore_cursor_cmd }, allocator); + var restore_cursor = std.process.Child.init(&[_][]const u8{ "/bin/sh", "-c", config.term_restore_cursor_cmd }, allocator); _ = restore_cursor.spawnAndWait() catch .{}; }, else => { From 3dc148260373c7bae47341d88bf8a1601aea2576 Mon Sep 17 00:00:00 2001 From: 0xNiffin <0xNiffin@proton.me> Date: Wed, 17 Jul 2024 12:27:54 +0100 Subject: [PATCH 009/530] fixed 'std' has no member 'ChildProcess' error when building with zig build command. (#651) --- src/main.zig | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main.zig b/src/main.zig index 83630e3..69fd4b9 100644 --- a/src/main.zig +++ b/src/main.zig @@ -507,7 +507,7 @@ pub fn main() !void { break :brightness_change; }; defer allocator.free(brightness_str); - var brightness = std.ChildProcess.init(&[_][]const u8{ config.brightnessctl, "-q", "s", brightness_str }, allocator); + var brightness = std.process.Child.init(&[_][]const u8{ config.brightnessctl, "-q", "s", brightness_str }, allocator); _ = brightness.spawnAndWait() catch .{}; } else if (pressed_key == brightness_up_key and unistd.access(&config.brightnessctl[0], unistd.X_OK) == 0) brightness_change: { const brightness_str = std.fmt.allocPrint(allocator, "+{s}%", .{config.brightness_change}) catch { @@ -515,7 +515,7 @@ pub fn main() !void { break :brightness_change; }; defer allocator.free(brightness_str); - var brightness = std.ChildProcess.init(&[_][]const u8{ config.brightnessctl, "-q", "s", brightness_str }, allocator); + var brightness = std.process.Child.init(&[_][]const u8{ config.brightnessctl, "-q", "s", brightness_str }, allocator); _ = brightness.spawnAndWait() catch .{}; } }, From 1d7a001a0bea5f48136f0c7738d920a75d2043d8 Mon Sep 17 00:00:00 2001 From: 0xNiffin <0xNiffin@proton.me> Date: Mon, 22 Jul 2024 15:44:49 +0100 Subject: [PATCH 010/530] Add customizable foreground color to CMatrix (#652) * Final fixes, adding fg_ini as field * Capital B changed to lowercase --- res/config.ini | 5 ++++- src/animations/Matrix.zig | 7 +++++-- src/config/Config.zig | 1 + src/main.zig | 2 +- 4 files changed, 11 insertions(+), 4 deletions(-) diff --git a/res/config.ini b/res/config.ini index 2738c01..0a1a240 100644 --- a/res/config.ini +++ b/res/config.ini @@ -44,6 +44,9 @@ bg = 0 # Foreground color id fg = 8 +# CMatrix animation foreground color id +cmatrix_fg = 3 + # Border color border_fg = 8 @@ -164,4 +167,4 @@ xsessions = /usr/share/xsessions # Brightness control brightness_down_key = F5 brightness_up_key = F6 -Brightness_change = 10 \ No newline at end of file +brightness_change = 10 diff --git a/src/animations/Matrix.zig b/src/animations/Matrix.zig index 5956eb8..9ed242e 100644 --- a/src/animations/Matrix.zig +++ b/src/animations/Matrix.zig @@ -34,8 +34,9 @@ dots: []Dot, lines: []Line, frame: u64, count: u64, +fg_ini: u16, -pub fn init(allocator: Allocator, terminal_buffer: *TerminalBuffer) !Matrix { +pub fn init(allocator: Allocator, terminal_buffer: *TerminalBuffer, fg_ini: u16) !Matrix { const dots = try allocator.alloc(Dot, terminal_buffer.width * (terminal_buffer.height + 1)); const lines = try allocator.alloc(Line, terminal_buffer.width); @@ -48,6 +49,7 @@ pub fn init(allocator: Allocator, terminal_buffer: *TerminalBuffer) !Matrix { .lines = lines, .frame = 3, .count = 0, + .fg_ini = fg_ini, }; } @@ -145,7 +147,8 @@ pub fn draw(self: *Matrix) void { var y: u64 = 1; while (y <= self.terminal_buffer.height) : (y += 1) { const dot = self.dots[buf_width * y + x]; - var fg: u16 = @intCast(termbox.TB_GREEN); + + var fg: u16 = self.fg_ini; if (dot.value == -1 or dot.value == ' ') { _ = termbox.tb_set_cell(@intCast(x), @intCast(y - 1), ' ', fg, termbox.TB_DEFAULT); diff --git a/src/config/Config.zig b/src/config/Config.zig index fac60ef..168a6f7 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -16,6 +16,7 @@ clock: ?[:0]const u8 = null, console_dev: [:0]const u8 = "/dev/console", default_input: Input = .login, fg: u8 = 8, +cmatrix_fg: u8 = 3, hide_borders: bool = false, hide_key_hints: bool = false, initial_info_text: ?[]const u8 = null, diff --git a/src/main.zig b/src/main.zig index 69fd4b9..05c06e6 100644 --- a/src/main.zig +++ b/src/main.zig @@ -230,7 +230,7 @@ pub fn main() !void { switch (config.animation) { .none => {}, .doom => doom = try Doom.init(allocator, &buffer), - .matrix => matrix = try Matrix.init(allocator, &buffer), + .matrix => matrix = try Matrix.init(allocator, &buffer, config.cmatrix_fg), } defer { switch (config.animation) { From 49b8697546d373b3aa8a3ddad6f4dced953d43bd Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Fri, 26 Jul 2024 18:54:27 +0200 Subject: [PATCH 011/530] Use octal prefix for file modes in build.zig Signed-off-by: AnErrupTion --- build.zig | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/build.zig b/build.zig index 588465c..d072c81 100644 --- a/build.zig +++ b/build.zig @@ -135,7 +135,7 @@ pub fn ServiceInstaller(comptime init_system: InitSystem) type { var service_dir = std.fs.cwd().openDir(service_path, .{}) catch unreachable; defer service_dir.close(); - try std.fs.cwd().copyFile("res/ly-openrc", service_dir, exe_name, .{ .override_mode = 755 }); + try std.fs.cwd().copyFile("res/ly-openrc", service_dir, exe_name, .{ .override_mode = 0o755 }); }, .Runit => { const service_path = try std.fs.path.join(allocator, &[_][]const u8{ dest_directory, "/etc/sv/ly" }); @@ -153,7 +153,7 @@ pub fn ServiceInstaller(comptime init_system: InitSystem) type { var service_dir = std.fs.cwd().openDir(service_path, .{}) catch unreachable; defer service_dir.close(); - try std.fs.cwd().copyFile("res/ly.service", service_dir, "ly.service", .{ .override_mode = 644 }); + try std.fs.cwd().copyFile("res/ly.service", service_dir, "ly.service", .{ .override_mode = 0o644 }); }, } } @@ -230,7 +230,7 @@ fn install_ly(allocator: std.mem.Allocator, install_config: bool) !void { var pam_dir = std.fs.cwd().openDir(pam_path, .{}) catch unreachable; defer pam_dir.close(); - try current_dir.copyFile("res/pam.d/ly", pam_dir, "ly", .{ .override_mode = 644 }); + try current_dir.copyFile("res/pam.d/ly", pam_dir, "ly", .{ .override_mode = 0o644 }); } } From 6b7e7be387de486a7f5a314a35d3f798c18355bd Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Fri, 26 Jul 2024 19:32:30 +0200 Subject: [PATCH 012/530] Backport: Use octal prefix for file modes in build.zig Signed-off-by: AnErrupTion --- build.zig | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/build.zig b/build.zig index adcbfcc..6cbe1a4 100644 --- a/build.zig +++ b/build.zig @@ -118,7 +118,7 @@ pub fn ServiceInstaller(comptime init_system: InitSystem) type { var service_dir = std.fs.cwd().openDir(service_path, .{}) catch unreachable; defer service_dir.close(); - try std.fs.cwd().copyFile("res/ly-openrc", service_dir, exe_name, .{ .override_mode = 755 }); + try std.fs.cwd().copyFile("res/ly-openrc", service_dir, exe_name, .{ .override_mode = 0o755 }); }, .Runit => { const service_path = try std.fs.path.join(allocator, &[_][]const u8{ dest_directory, "/etc/sv/ly" }); @@ -136,7 +136,7 @@ pub fn ServiceInstaller(comptime init_system: InitSystem) type { var service_dir = std.fs.cwd().openDir(service_path, .{}) catch unreachable; defer service_dir.close(); - try std.fs.cwd().copyFile("res/ly.service", service_dir, "ly.service", .{ .override_mode = 644 }); + try std.fs.cwd().copyFile("res/ly.service", service_dir, "ly.service", .{ .override_mode = 0o644 }); }, } } @@ -213,7 +213,7 @@ fn install_ly(allocator: std.mem.Allocator, install_config: bool) !void { var pam_dir = std.fs.cwd().openDir(pam_path, .{}) catch unreachable; defer pam_dir.close(); - try current_dir.copyFile("res/pam.d/ly", pam_dir, "ly", .{ .override_mode = 644 }); + try current_dir.copyFile("res/pam.d/ly", pam_dir, "ly", .{ .override_mode = 0o644 }); } } From 8e534c7bcd1b9130eb26a4c86edcd746a91d0c21 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Fri, 26 Jul 2024 19:34:12 +0200 Subject: [PATCH 013/530] Backport: Fix incorrect shebang in xsetup.sh Signed-off-by: AnErrupTion --- res/xsetup.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/res/xsetup.sh b/res/xsetup.sh index 487a078..9418530 100755 --- a/res/xsetup.sh +++ b/res/xsetup.sh @@ -1,4 +1,4 @@ -#! /bin/sh +#!/bin/sh # Xsession - run as user # Copyright (C) 2016 Pier Luigi Fiorini From 391104cf342c0820e28d92baf1f2ed097b53379c Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Fri, 26 Jul 2024 19:34:46 +0200 Subject: [PATCH 014/530] Backport: Fix documentation issue about DOOM animation Signed-off-by: AnErrupTion --- readme.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/readme.md b/readme.md index 1402122..b323c0d 100644 --- a/readme.md +++ b/readme.md @@ -132,7 +132,7 @@ then you have to disable getty, so it doesn't respawn on top of ly # ln -s /etc/sv/ly /var/service/ ``` -By default, ly will run on tty2. To change the tty it must be set in `/etc/ly/config.ini` +By default, ly will run on tty2. To change the tty it must be set in `/etc/ly/config.ini` You should as well disable your existing display manager service if needed, e.g.: @@ -196,7 +196,7 @@ Take a look at your .xsession if X doesn't start, as it can interfere ## PSX DOOM fire animation To enable the famous PSX DOOM fire described by [Fabien Sanglard](http://fabiensanglard.net/doom_fire_psx/index.html), -just uncomment `animate = true` in `/etc/ly/config.ini`. You may also +just set `animation = doom` in `/etc/ly/config.ini`. You may also disable the main box borders with `hide_borders = true`. ## Additional Information From b84e6c9eedfd381bfa29dd7fd25b9c28dfc354f4 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Fri, 26 Jul 2024 21:39:27 +0200 Subject: [PATCH 015/530] Use default PRNG and retrieve better seed Signed-off-by: AnErrupTion --- src/main.zig | 9 ++++++++- src/tui/TerminalBuffer.zig | 8 +++----- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/src/main.zig b/src/main.zig index 05c06e6..f35a460 100644 --- a/src/main.zig +++ b/src/main.zig @@ -162,7 +162,14 @@ pub fn main() !void { // Initialize terminal buffer const labels_max_length = @max(lang.login.len, lang.password.len); - var buffer = TerminalBuffer.init(config, labels_max_length); + // Get a random seed for the PRNG (used by animations) + var seed: u64 = undefined; + try std.posix.getrandom(std.mem.asBytes(&seed)); + + var prng = std.Random.DefaultPrng.init(seed); + const random = prng.random(); + + var buffer = TerminalBuffer.init(config, labels_max_length, random); // Initialize components var desktop = try Desktop.init(allocator, &buffer, config.max_desktop_len, lang); diff --git a/src/tui/TerminalBuffer.zig b/src/tui/TerminalBuffer.zig index eec1a85..9692365 100644 --- a/src/tui/TerminalBuffer.zig +++ b/src/tui/TerminalBuffer.zig @@ -4,7 +4,7 @@ const interop = @import("../interop.zig"); const utils = @import("utils.zig"); const Config = @import("../config/Config.zig"); -const Random = std.rand.Random; +const Random = std.Random; const termbox = interop.termbox; @@ -35,11 +35,9 @@ box_height: u64, margin_box_v: u8, margin_box_h: u8, -pub fn init(config: Config, labels_max_length: u64) TerminalBuffer { - var prng = std.rand.Isaac64.init(@intCast(std.time.timestamp())); - +pub fn init(config: Config, labels_max_length: u64, random: Random) TerminalBuffer { return .{ - .random = prng.random(), + .random = random, .width = @intCast(termbox.tb_width()), .height = @intCast(termbox.tb_height()), .buffer = termbox.tb_cell_buffer(), From 2b0301c1d00747121bc02e5e0ffc09503ad126f7 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Fri, 26 Jul 2024 21:58:18 +0200 Subject: [PATCH 016/530] Make runit run and finish scripts executable Signed-off-by: AnErrupTion --- build.zig | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.zig b/build.zig index d072c81..0176348 100644 --- a/build.zig +++ b/build.zig @@ -144,8 +144,8 @@ pub fn ServiceInstaller(comptime init_system: InitSystem) type { defer service_dir.close(); try std.fs.cwd().copyFile("res/ly-runit-service/conf", service_dir, "conf", .{}); - try std.fs.cwd().copyFile("res/ly-runit-service/finish", service_dir, "finish", .{}); - try std.fs.cwd().copyFile("res/ly-runit-service/run", service_dir, "run", .{}); + try std.fs.cwd().copyFile("res/ly-runit-service/finish", service_dir, "finish", .{ .override_mode = 0o755 }); + try std.fs.cwd().copyFile("res/ly-runit-service/run", service_dir, "run", .{ .override_mode = 0o755 }); }, .Systemd => { const service_path = try std.fs.path.join(allocator, &[_][]const u8{ dest_directory, "/usr/lib/systemd/system" }); From a042749a72bc0765b18e16bc7b6195251cc085b3 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Fri, 26 Jul 2024 21:59:21 +0200 Subject: [PATCH 017/530] Backport: Make runit run and finish scripts executable Signed-off-by: AnErrupTion --- build.zig | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.zig b/build.zig index 6cbe1a4..8535cfb 100644 --- a/build.zig +++ b/build.zig @@ -127,8 +127,8 @@ pub fn ServiceInstaller(comptime init_system: InitSystem) type { defer service_dir.close(); try std.fs.cwd().copyFile("res/ly-runit-service/conf", service_dir, "conf", .{}); - try std.fs.cwd().copyFile("res/ly-runit-service/finish", service_dir, "finish", .{}); - try std.fs.cwd().copyFile("res/ly-runit-service/run", service_dir, "run", .{}); + try std.fs.cwd().copyFile("res/ly-runit-service/finish", service_dir, "finish", .{ .override_mode = 0o755 }); + try std.fs.cwd().copyFile("res/ly-runit-service/run", service_dir, "run", .{ .override_mode = 0o755 }); }, .Systemd => { const service_path = try std.fs.path.join(allocator, &[_][]const u8{ dest_directory, "/usr/lib/systemd/system" }); From a939b821791c80146602fc1d353263bc6ad4830b Mon Sep 17 00:00:00 2001 From: Varun Vasan V Date: Sat, 27 Jul 2024 15:01:55 +0530 Subject: [PATCH 018/530] Add configurable default vi mode (#660) - `vi_default_mode` added - supports `normal` and `insert` --- res/config.ini | 5 +++++ src/config/Config.zig | 2 ++ src/enums.zig | 5 +++++ src/main.zig | 2 +- 4 files changed, 13 insertions(+), 1 deletion(-) diff --git a/res/config.ini b/res/config.ini index 0a1a240..b0f0fb4 100644 --- a/res/config.ini +++ b/res/config.ini @@ -19,6 +19,11 @@ clear_password = false # Enable vi keybindings vi_mode = false +# Default vi mode +# normal -> normal mode +# insert -> insert mode +vi_default_mode = normal + # The `fg` and `bg` color settings take a digit 0-8 corresponding to: #define TB_DEFAULT 0x00 #define TB_BLACK 0x01 diff --git a/src/config/Config.zig b/src/config/Config.zig index 168a6f7..ca8290a 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -3,6 +3,7 @@ const enums = @import("../enums.zig"); const Animation = enums.Animation; const Input = enums.Input; +const ViMode = enums.ViMode; animation: Animation = .none, asterisk: u8 = '*', @@ -45,6 +46,7 @@ term_reset_cmd: [:0]const u8 = "/usr/bin/tput reset", term_restore_cursor_cmd: []const u8 = "/usr/bin/tput cnorm", tty: u8 = build_options.tty, vi_mode: bool = false, +vi_default_mode: ViMode = .normal, wayland_cmd: []const u8 = build_options.data_directory ++ "/wsetup.sh", waylandsessions: []const u8 = "/usr/share/wayland-sessions", x_cmd: []const u8 = "/usr/bin/X", diff --git a/src/enums.zig b/src/enums.zig index 4cfa564..d62673b 100644 --- a/src/enums.zig +++ b/src/enums.zig @@ -16,3 +16,8 @@ pub const Input = enum { login, password, }; + +pub const ViMode = enum { + normal, + insert, +}; diff --git a/src/main.zig b/src/main.zig index f35a460..f5500da 100644 --- a/src/main.zig +++ b/src/main.zig @@ -194,7 +194,7 @@ pub fn main() !void { defer password.deinit(); var active_input = config.default_input; - var insert_mode = !config.vi_mode; + var insert_mode = !config.vi_mode or config.vi_default_mode == .insert; // Load last saved username and desktop selection, if any if (config.load) { From 2eaa473144813e06c74a636f65ef8e640cb0cd42 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Sat, 27 Jul 2024 14:06:28 +0200 Subject: [PATCH 019/530] Add issue template Signed-off-by: AnErrupTion --- .github/ISSUE_TEMPLATE/bug.yml | 55 ++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/bug.yml diff --git a/.github/ISSUE_TEMPLATE/bug.yml b/.github/ISSUE_TEMPLATE/bug.yml new file mode 100644 index 0000000..a3131d0 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug.yml @@ -0,0 +1,55 @@ +name: Bug Report +description: File a bug report. +title: "[Bug] " +labels: ["bug"] +body: + - type: checkboxes + id: prerequisites + attributes: + label: Pre-requisites + description: By submitting this issue, you agree to have done the following. + options: + - label: I have looked for any other duplicate issues + validations: + required: true + - type: input + id: version + attributes: + label: Ly version + description: The output of `ly --version` + placeholder: 1.1.0-dev.12+2b0301c + validations: + required: true + - type: textarea + id: observed + attributes: + label: Observed behavior + description: What happened? + validations: + required: true + - type: textarea + id: expected + attributes: + label: Expected behavior + description: What did you expect to happen instead? + validations: + required: true + - type: textarea + id: reproduction + attributes: + label: Steps to reproduce + description: What **exactly** can someone else do in order to observe the problem you observed? + placeholder: | + 1. Authenticate with ... + 2. Go to ... + 3. Create file ... + 4. Log out and log back in + 5. Observe error + validations: + required: true + - type: textarea + id: logs + attributes: + label: Relevant logs + description: Please copy and paste any relevant logs, error messages or any other output. This will be automatically formatted into code, so no need for backticks. Screenshots are accepted if they make life easier for you. + render: shell From 23c71755284e238f3b8f55e463251d9bde9ffd1e Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Sat, 27 Jul 2024 14:11:42 +0200 Subject: [PATCH 020/530] Make pre-requisites required Signed-off-by: AnErrupTion --- .github/ISSUE_TEMPLATE/bug.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug.yml b/.github/ISSUE_TEMPLATE/bug.yml index a3131d0..4e9eb61 100644 --- a/.github/ISSUE_TEMPLATE/bug.yml +++ b/.github/ISSUE_TEMPLATE/bug.yml @@ -10,8 +10,7 @@ body: description: By submitting this issue, you agree to have done the following. options: - label: I have looked for any other duplicate issues - validations: - required: true + required: true - type: input id: version attributes: From ce90f91bbf0b0c47965601e845ac9619f70316e0 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Sat, 27 Jul 2024 14:16:40 +0200 Subject: [PATCH 021/530] Add issue template for feature requests Signed-off-by: AnErrupTion --- .github/ISSUE_TEMPLATE/feature.yml | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/feature.yml diff --git a/.github/ISSUE_TEMPLATE/feature.yml b/.github/ISSUE_TEMPLATE/feature.yml new file mode 100644 index 0000000..9cb61ea --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature.yml @@ -0,0 +1,20 @@ +name: Feature Request +description: Request a new feature or enhancement. +title: "[Feature] " +labels: ["feature"] +body: + - type: checkboxes + id: prerequisites + attributes: + label: Pre-requisites + description: By submitting this issue, you agree to have done the following. + options: + - label: I have looked for any other duplicate issues + required: true + - type: textarea + id: observed + attributes: + label: Wanted behavior + description: What do you want to be added? Describe the behavior clearly. + validations: + required: true From d87344330a2234366ff7f420a981dee76521e7a2 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Sat, 27 Jul 2024 14:18:09 +0200 Subject: [PATCH 022/530] Start Ly v1.0.2 development cycle Signed-off-by: AnErrupTion --- build.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.zig b/build.zig index 8535cfb..4452475 100644 --- a/build.zig +++ b/build.zig @@ -1,6 +1,6 @@ const std = @import("std"); -const ly_version = std.SemanticVersion{ .major = 1, .minor = 0, .patch = 1 }; +const ly_version = std.SemanticVersion{ .major = 1, .minor = 0, .patch = 2 }; var dest_directory: []const u8 = undefined; var data_directory: []const u8 = undefined; var exe_name: []const u8 = undefined; From a807e8e11c3d3364767ea4b1a3611be57cfaa6de Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Sat, 27 Jul 2024 14:25:30 +0200 Subject: [PATCH 023/530] Backport: Use default PRNG and retrieve better seed Signed-off-by: AnErrupTion --- src/main.zig | 9 ++++++++- src/tui/TerminalBuffer.zig | 8 +++----- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/src/main.zig b/src/main.zig index 6171361..29a4a8c 100644 --- a/src/main.zig +++ b/src/main.zig @@ -161,7 +161,14 @@ pub fn main() !void { // Initialize terminal buffer const labels_max_length = @max(lang.login.len, lang.password.len); - var buffer = TerminalBuffer.init(config, labels_max_length); + // Get a random seed for the PRNG (used by animations) + var seed: u64 = undefined; + try std.posix.getrandom(std.mem.asBytes(&seed)); + + var prng = std.Random.DefaultPrng.init(seed); + const random = prng.random(); + + var buffer = TerminalBuffer.init(config, labels_max_length, random); // Initialize components var desktop = try Desktop.init(allocator, &buffer, config.max_desktop_len, lang); diff --git a/src/tui/TerminalBuffer.zig b/src/tui/TerminalBuffer.zig index eec1a85..9692365 100644 --- a/src/tui/TerminalBuffer.zig +++ b/src/tui/TerminalBuffer.zig @@ -4,7 +4,7 @@ const interop = @import("../interop.zig"); const utils = @import("utils.zig"); const Config = @import("../config/Config.zig"); -const Random = std.rand.Random; +const Random = std.Random; const termbox = interop.termbox; @@ -35,11 +35,9 @@ box_height: u64, margin_box_v: u8, margin_box_h: u8, -pub fn init(config: Config, labels_max_length: u64) TerminalBuffer { - var prng = std.rand.Isaac64.init(@intCast(std.time.timestamp())); - +pub fn init(config: Config, labels_max_length: u64, random: Random) TerminalBuffer { return .{ - .random = prng.random(), + .random = random, .width = @intCast(termbox.tb_width()), .height = @intCast(termbox.tb_height()), .buffer = termbox.tb_cell_buffer(), From 0cead672da2796cd8ddc8ff759e290f057052889 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Sat, 27 Jul 2024 15:00:07 +0200 Subject: [PATCH 024/530] Add s6 support Co-authored-by: userbook Co-authored-by: TerminalJunki <158248817+TerminalJunki@users.noreply.github.com> Signed-off-by: AnErrupTion --- build.zig | 39 ++++++++++++++++++++++++++++++++++++--- readme.md | 17 ++++++++++++++++- res/ly-s6/run | 2 ++ res/ly-s6/type | 1 + 4 files changed, 55 insertions(+), 4 deletions(-) create mode 100644 res/ly-s6/run create mode 100644 res/ly-s6/type diff --git a/build.zig b/build.zig index 0176348..66542ea 100644 --- a/build.zig +++ b/build.zig @@ -106,6 +106,11 @@ pub fn build(b: *std.Build) !void { const installrunit_step = b.step("installrunit", "Install the Ly runit service"); installrunit_step.makeFn = ServiceInstaller(.Runit).make; installrunit_step.dependOn(installexe_step); + installopenrc_step.dependOn(installexe_step); + + const installs6_step = b.step("installs6", "Install the Ly s6 service"); + installs6_step.makeFn = ServiceInstaller(.S6).make; + installs6_step.dependOn(installexe_step); const uninstallall_step = b.step("uninstallall", "Uninstall Ly and all services"); uninstallall_step.makeFn = uninstallall; @@ -123,12 +128,21 @@ const InitSystem = enum { Systemd, Openrc, Runit, + S6, }; pub fn ServiceInstaller(comptime init_system: InitSystem) type { return struct { pub fn make(step: *std.Build.Step, _: ProgressNode) !void { const allocator = step.owner.allocator; switch (init_system) { + .Systemd => { + const service_path = try std.fs.path.join(allocator, &[_][]const u8{ dest_directory, "/usr/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(); + + try std.fs.cwd().copyFile("res/ly.service", service_dir, "ly.service", .{ .override_mode = 0o644 }); + }, .Openrc => { const service_path = try std.fs.path.join(allocator, &[_][]const u8{ dest_directory, "/etc/init.d" }); std.fs.cwd().makePath(service_path) catch {}; @@ -147,13 +161,22 @@ pub fn ServiceInstaller(comptime init_system: InitSystem) type { try std.fs.cwd().copyFile("res/ly-runit-service/finish", service_dir, "finish", .{ .override_mode = 0o755 }); try std.fs.cwd().copyFile("res/ly-runit-service/run", service_dir, "run", .{ .override_mode = 0o755 }); }, - .Systemd => { - const service_path = try std.fs.path.join(allocator, &[_][]const u8{ dest_directory, "/usr/lib/systemd/system" }); + .S6 => { + const admin_service_path = try std.fs.path.join(allocator, &[_][]const u8{ dest_directory, "/etc/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 file = try admin_service_dir.createFile("ly-srv", .{}); + file.close(); + + const service_path = try std.fs.path.join(allocator, &[_][]const u8{ dest_directory, "/etc/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(); - try std.fs.cwd().copyFile("res/ly.service", service_dir, "ly.service", .{ .override_mode = 0o644 }); + try std.fs.cwd().copyFile("res/ly-s6/run", service_dir, "run", .{ .override_mode = 0o755 }); + try std.fs.cwd().copyFile("res/ly-s6/type", service_dir, "type", .{}); }, } } @@ -258,6 +281,16 @@ pub fn uninstallall(step: *std.Build.Step, _: ProgressNode) !void { std.fs.cwd().deleteTree(runit_service_path) catch { std.debug.print("warn: runit service not found.\n", .{}); }; + + const s6_service_path = try std.fs.path.join(allocator, &[_][]const u8{ dest_directory, "/etc/s6/sv/ly-srv" }); + std.fs.cwd().deleteTree(s6_service_path) catch { + std.debug.print("warn: s6 service not found.\n", .{}); + }; + + const s6_admin_service_file = try std.fs.path.join(allocator, &[_][]const u8{ dest_directory, "/etc/s6/adminsv/default/contents.d/ly-srv" }); + std.fs.cwd().deleteFile(s6_admin_service_file) catch { + std.debug.print("warn: s6 admin service not found.\n", .{}); + }; } fn getVersionStr(b: *std.Build, name: []const u8, version: std.SemanticVersion) ![]const u8 { diff --git a/readme.md b/readme.md index ad63e5c..1ab5b27 100644 --- a/readme.md +++ b/readme.md @@ -132,7 +132,7 @@ then you have to disable getty, so it doesn't respawn on top of ly # ln -s /etc/sv/ly /var/service/ ``` -By default, ly will run on tty2. To change the tty it must be set in `/etc/ly/config.ini` +By default, ly will run on tty2. To change the tty it must be set in `/etc/ly/config.ini` You should as well disable your existing display manager service if needed, e.g.: @@ -148,6 +148,21 @@ you should disable the agetty-tty2 service like this: # rm /var/service/agetty-tty2 ``` +### s6 +``` +# zig build installs6 +``` + +Then, edit `/etc/s6/config/ttyX.conf` and set `SPAWN="no"`, where X is the TTY ID (e.g. `2`). + +Finally, enable the service: + +``` +# s6-service add default ly-srv +# s6-db-reload +# s6-rc -u change ly-srv +``` + ### Updating You can also install Ly without copying the system service and the configuration file. That's called *updating*. To update, simply run: diff --git a/res/ly-s6/run b/res/ly-s6/run new file mode 100644 index 0000000..bf64302 --- /dev/null +++ b/res/ly-s6/run @@ -0,0 +1,2 @@ +#!/bin/execlineb -P +exec agetty -L -8 -n -l /usr/bin/ly tty2 115200 diff --git a/res/ly-s6/type b/res/ly-s6/type new file mode 100644 index 0000000..5883cff --- /dev/null +++ b/res/ly-s6/type @@ -0,0 +1 @@ +longrun From 9cd58123c450ead3d8a42fefe9fe6b21448852f4 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Sat, 27 Jul 2024 15:02:01 +0200 Subject: [PATCH 025/530] Fix silly mistake Signed-off-by: AnErrupTion --- build.zig | 1 - 1 file changed, 1 deletion(-) diff --git a/build.zig b/build.zig index 66542ea..8f9b8c3 100644 --- a/build.zig +++ b/build.zig @@ -106,7 +106,6 @@ pub fn build(b: *std.Build) !void { const installrunit_step = b.step("installrunit", "Install the Ly runit service"); installrunit_step.makeFn = ServiceInstaller(.Runit).make; installrunit_step.dependOn(installexe_step); - installopenrc_step.dependOn(installexe_step); const installs6_step = b.step("installs6", "Install the Ly s6 service"); installs6_step.makeFn = ServiceInstaller(.S6).make; From e775827c8b18d5300c3300486facbfe2e6377436 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Sat, 27 Jul 2024 15:20:42 +0200 Subject: [PATCH 026/530] Fix possible overflow with TTY ID Co-authored-by: Kevin Morris 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 dbbdfda..2da71ab 100644 --- a/src/auth.zig +++ b/src/auth.zig @@ -21,7 +21,7 @@ pub fn sessionSignalHandler(i: c_int) callconv(.C) void { } pub fn authenticate(config: Config, current_environment: Desktop.Environment, login: [:0]const u8, password: [:0]const u8) !void { - var tty_buffer: [2]u8 = undefined; + var tty_buffer: [3]u8 = undefined; const tty_str = try std.fmt.bufPrintZ(&tty_buffer, "{d}", .{config.tty}); // Set the XDG environment variables From b7934e42d1f8304448e214e73a58eb35e480e0f0 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Sat, 27 Jul 2024 15:59:02 +0200 Subject: [PATCH 027/530] Add dinit support Co-authored-by: Simon Pflaumer Signed-off-by: AnErrupTion --- build.zig | 22 ++++++++++++++++++++-- readme.md | 10 ++++++++++ res/ly-dinit | 9 +++++++++ 3 files changed, 39 insertions(+), 2 deletions(-) create mode 100644 res/ly-dinit diff --git a/build.zig b/build.zig index 8f9b8c3..43a4987 100644 --- a/build.zig +++ b/build.zig @@ -111,6 +111,10 @@ pub fn build(b: *std.Build) !void { installs6_step.makeFn = ServiceInstaller(.S6).make; installs6_step.dependOn(installexe_step); + const installdinit_step = b.step("installdinit", "Install the Ly dinit service"); + installdinit_step.makeFn = ServiceInstaller(.Dinit).make; + installdinit_step.dependOn(installexe_step); + const uninstallall_step = b.step("uninstallall", "Uninstall Ly and all services"); uninstallall_step.makeFn = uninstallall; } @@ -128,6 +132,7 @@ const InitSystem = enum { Openrc, Runit, S6, + Dinit, }; pub fn ServiceInstaller(comptime init_system: InitSystem) type { return struct { @@ -177,6 +182,14 @@ pub fn ServiceInstaller(comptime init_system: InitSystem) type { try std.fs.cwd().copyFile("res/ly-s6/run", service_dir, "run", .{ .override_mode = 0o755 }); try std.fs.cwd().copyFile("res/ly-s6/type", service_dir, "type", .{}); }, + .Dinit => { + const service_path = try std.fs.path.join(allocator, &[_][]const u8{ dest_directory, "/etc/dinit.d" }); + std.fs.cwd().makePath(service_path) catch {}; + var service_dir = std.fs.cwd().openDir(service_path, .{}) catch unreachable; + defer service_dir.close(); + + try std.fs.cwd().copyFile("res/ly-dinit", service_dir, "ly", .{}); + }, } } }; @@ -286,10 +299,15 @@ pub fn uninstallall(step: *std.Build.Step, _: ProgressNode) !void { std.debug.print("warn: s6 service not found.\n", .{}); }; - const s6_admin_service_file = try std.fs.path.join(allocator, &[_][]const u8{ dest_directory, "/etc/s6/adminsv/default/contents.d/ly-srv" }); - std.fs.cwd().deleteFile(s6_admin_service_file) catch { + const s6_admin_service_path = try std.fs.path.join(allocator, &[_][]const u8{ dest_directory, "/etc/s6/adminsv/default/contents.d/ly-srv" }); + std.fs.cwd().deleteFile(s6_admin_service_path) catch { std.debug.print("warn: s6 admin service not found.\n", .{}); }; + + const dinit_service_path = try std.fs.path.join(allocator, &[_][]const u8{ dest_directory, "/etc/dinit.d/ly" }); + std.fs.cwd().deleteFile(dinit_service_path) catch { + std.debug.print("warn: dinit service not found.\n", .{}); + }; } fn getVersionStr(b: *std.Build, name: []const u8, version: std.SemanticVersion) ![]const u8 { diff --git a/readme.md b/readme.md index 1ab5b27..a4d542e 100644 --- a/readme.md +++ b/readme.md @@ -163,6 +163,16 @@ Finally, enable the service: # s6-rc -u change ly-srv ``` +### dinit +``` +# zig build installdinit +# dinitctl enable ly +``` + +In addition to the steps above, you will also have to keep a TTY free within `/etc/dinit.d/config/console.conf`. + +To do that, change `ACTIVE_CONSOLES` so that the tty that ly should use in `/etc/ly/config.ini` is free. + ### Updating You can also install Ly without copying the system service and the configuration file. That's called *updating*. To update, simply run: diff --git a/res/ly-dinit b/res/ly-dinit new file mode 100644 index 0000000..cb2c620 --- /dev/null +++ b/res/ly-dinit @@ -0,0 +1,9 @@ +type = process +restart = true +smooth-recovery = true +# note: /usr/bin/ly-dm when installing from pacman on artix, /usr/bin/ly when building from source +command = /usr/bin/ly +depends-on = loginready +termsignal = HUP +# ly needs access to the console while loginready already occupies it +options = shares-console From 0ee28927cf8cd4c9db6ee9c6eaf98da2180a1253 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Sat, 27 Jul 2024 18:15:34 +0200 Subject: [PATCH 028/530] Update French translation Signed-off-by: AnErrupTion --- res/lang/fr.ini | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/res/lang/fr.ini b/res/lang/fr.ini index cb763be..3d1fb50 100644 --- a/res/lang/fr.ini +++ b/res/lang/fr.ini @@ -1,19 +1,23 @@ -capslock = verr.maj +authenticating = authentification... +capslock = verr.maj err_alloc = échec d'allocation mémoire err_bounds = indice hors-limite +err_brightness_change = échec du changement de luminosité err_chdir = échec de l'ouverture du répertoire home err_console_dev = échec d'accès à la console err_dgn_oob = message err_domain = domaine invalide +err_envlist = échec de lecture de la liste d'environnement err_hostname = échec de lecture du nom d'hôte +err_mcookie = échec de la commande mcookie err_mlock = échec du verrouillage mémoire err_null = pointeur null err_pam = échec de la transaction pam err_pam_abort = transaction pam avortée err_pam_acct_expired = compte expiré err_pam_auth = erreur d'authentification -err_pam_authok_reqd = tiquet expiré err_pam_authinfo_unavail = échec de l'obtention des infos utilisateur +err_pam_authok_reqd = tiquet expiré err_pam_buf = erreur de mémoire tampon err_pam_cred_err = échec de la modification des identifiants err_pam_cred_expired = identifiants expirés @@ -29,17 +33,24 @@ err_perm_dir = échec de changement de répertoire err_perm_group = échec du déclassement des permissions de groupe err_perm_user = échec du déclassement des permissions utilisateur err_pwnam = échec de lecture des infos utilisateur +err_unknown = une erreur inconnue est survenue err_user_gid = échec de modification du GID err_user_init = échec d'initialisation de l'utilisateur err_user_uid = échec de modification du UID +err_xauth = échec de la commande xauth +err_xcb_conn = échec de la connexion xcb err_xsessions_dir = échec de la recherche du dossier de sessions err_xsessions_open = échec de l'ouverture du dossier de sessions +insert = insertion login = identifiant -logout = déconnection +logout = déconnecté +normal = normal numlock = verr.num password = mot de passe restart = redémarrer shell = shell shutdown = éteindre +sleep = veille wayland = wayland xinitrc = xinitrc +x11 = x11 From 8c694720651142d3783a0459aa3b6349e3413639 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Sat, 27 Jul 2024 18:35:58 +0200 Subject: [PATCH 029/530] Allow building without X11 support Signed-off-by: AnErrupTion --- build.zig | 13 ++++++------- res/lang/en.ini | 1 + res/lang/fr.ini | 1 + src/auth.zig | 3 ++- src/config/Lang.zig | 1 + src/main.zig | 15 ++++++++++----- 6 files changed, 21 insertions(+), 13 deletions(-) diff --git a/build.zig b/build.zig index 43a4987..ec8c013 100644 --- a/build.zig +++ b/build.zig @@ -13,9 +13,9 @@ comptime { } const ly_version = std.SemanticVersion{ .major = 1, .minor = 1, .patch = 0 }; + var dest_directory: []const u8 = undefined; var data_directory: []const u8 = undefined; -var default_tty: u8 = undefined; var exe_name: []const u8 = undefined; const ProgressNode = if (current_zig.minor == 12) *std.Progress.Node else std.Progress.Node; @@ -23,21 +23,20 @@ const ProgressNode = if (current_zig.minor == 12) *std.Progress.Node else std.Pr pub fn build(b: *std.Build) !void { dest_directory = b.option([]const u8, "dest_directory", "Specify a destination directory for installation") orelse ""; data_directory = b.option([]const u8, "data_directory", "Specify a default data directory (default is /etc/ly). This path gets embedded into the binary") orelse "/etc/ly"; - default_tty = b.option(u8, "default_tty", "set default TTY") orelse 2; - exe_name = b.option([]const u8, "name", "Specify installed executable file name (default is ly)") orelse "ly"; const bin_directory = try b.allocator.dupe(u8, data_directory); data_directory = try std.fs.path.join(b.allocator, &[_][]const u8{ dest_directory, data_directory }); const build_options = b.addOptions(); - build_options.addOption([]const u8, "data_directory", bin_directory); - const version_str = try getVersionStr(b, "ly", ly_version); + const default_tty = b.option(u8, "default_tty", "Set the TTY (default is 2)") orelse 2; + const enable_x11_support = b.option(bool, "enable_x11_support", "Enable X11 support (default is on)") orelse true; + build_options.addOption([]const u8, "data_directory", bin_directory); build_options.addOption([]const u8, "version", version_str); - build_options.addOption(u8, "tty", default_tty); + build_options.addOption(bool, "enable_x11_support", enable_x11_support); const target = b.standardTargetOptions(.{}); const optimize = b.standardOptimizeOption(.{}); @@ -59,7 +58,7 @@ pub fn build(b: *std.Build) !void { exe.addIncludePath(b.path("include")); exe.linkSystemLibrary("pam"); - exe.linkSystemLibrary("xcb"); + if (enable_x11_support) exe.linkSystemLibrary("xcb"); exe.linkLibC(); // HACK: Only fails with ReleaseSafe, so we'll override it. diff --git a/res/lang/en.ini b/res/lang/en.ini index 56c4fd7..c8de400 100644 --- a/res/lang/en.ini +++ b/res/lang/en.ini @@ -45,6 +45,7 @@ insert = insert login = login logout = logged out normal = normal +no_x11_support = x11 support disabled at compile-time numlock = numlock password = password restart = reboot diff --git a/res/lang/fr.ini b/res/lang/fr.ini index 3d1fb50..e2741f4 100644 --- a/res/lang/fr.ini +++ b/res/lang/fr.ini @@ -45,6 +45,7 @@ insert = insertion login = identifiant logout = déconnecté normal = normal +no_x11_support = support pour x11 désactivé lors de la compilation numlock = verr.num password = mot de passe restart = redémarrer diff --git a/src/auth.zig b/src/auth.zig index 2da71ab..7497c6d 100644 --- a/src/auth.zig +++ b/src/auth.zig @@ -1,4 +1,5 @@ const std = @import("std"); +const build_options = @import("build_options"); const enums = @import("enums.zig"); const interop = @import("interop.zig"); const TerminalBuffer = @import("tui/TerminalBuffer.zig"); @@ -142,7 +143,7 @@ fn startSession( switch (current_environment.display_server) { .wayland => try executeWaylandCmd(pwd.pw_shell, config.wayland_cmd, current_environment.cmd), .shell => try executeShellCmd(pwd.pw_shell), - .xinitrc, .x11 => { + .xinitrc, .x11 => if (build_options.enable_x11_support) { var vt_buf: [5]u8 = undefined; const vt = try std.fmt.bufPrint(&vt_buf, "vt{d}", .{config.tty}); try executeX11Cmd(pwd.pw_shell, pwd.pw_dir, config, current_environment.cmd, vt); diff --git a/src/config/Lang.zig b/src/config/Lang.zig index 58a9703..24bd011 100644 --- a/src/config/Lang.zig +++ b/src/config/Lang.zig @@ -45,6 +45,7 @@ insert: []const u8 = "insert", login: []const u8 = "login:", logout: []const u8 = "logged out", normal: []const u8 = "normal", +no_x11_support: []const u8 = "x11 support disabled at compile-time", numlock: []const u8 = "numlock", other: []const u8 = "other", password: []const u8 = "password:", diff --git a/src/main.zig b/src/main.zig index f5500da..37e338f 100644 --- a/src/main.zig +++ b/src/main.zig @@ -128,6 +128,8 @@ pub fn main() !void { } } + if (!build_options.enable_x11_support) try info_line.setText(lang.no_x11_support); + interop.setNumlock(config.numlock) catch {}; if (config.initial_info_text) |text| { @@ -178,14 +180,17 @@ pub fn main() !void { desktop.addEnvironment(.{ .Name = lang.shell }, "", .shell) catch { try info_line.setText(lang.err_alloc); }; - if (config.xinitrc) |xinitrc| { - desktop.addEnvironment(.{ .Name = lang.xinitrc, .Exec = xinitrc }, "", .xinitrc) catch { - try info_line.setText(lang.err_alloc); - }; + + if (build_options.enable_x11_support) { + if (config.xinitrc) |xinitrc| { + desktop.addEnvironment(.{ .Name = lang.xinitrc, .Exec = xinitrc }, "", .xinitrc) catch { + try info_line.setText(lang.err_alloc); + }; + } } try desktop.crawl(config.waylandsessions, .wayland); - try desktop.crawl(config.xsessions, .x11); + if (build_options.enable_x11_support) try desktop.crawl(config.xsessions, .x11); var login = try Text.init(allocator, &buffer, config.max_login_len); defer login.deinit(); From da2ea0807831af34591dbc22a1ee55e5a230ab1d Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Sat, 27 Jul 2024 18:38:07 +0200 Subject: [PATCH 030/530] Clarify compile-time dependencies Signed-off-by: AnErrupTion --- readme.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/readme.md b/readme.md index a4d542e..6759114 100644 --- a/readme.md +++ b/readme.md @@ -6,10 +6,10 @@ Ly is a lightweight TUI (ncurses-like) display manager for Linux and BSD. ## Dependencies - Compile-time: - - zig 0.12.0 - - a C standard library + - zig >=0.12.0 + - libc - pam - - xcb + - xcb (optional, required by default; needed for X11 support) - Runtime (with default config): - xorg - xorg-xauth From dc310ca80e3c1e4803c6ad84de1ab2db238e97d2 Mon Sep 17 00:00:00 2001 From: mietinen Date: Sat, 27 Jul 2024 20:35:24 +0200 Subject: [PATCH 031/530] Unlock GNOME Keyring & KWallet on login (#496) --- res/pam.d/ly | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/res/pam.d/ly b/res/pam.d/ly index 322461b..c4da380 100644 --- a/res/pam.d/ly +++ b/res/pam.d/ly @@ -1,6 +1,10 @@ #%PAM-1.0 auth include login +-auth optional pam_gnome_keyring.so +-auth optional pam_kwallet5.so account include login password include login session include login +-session optional pam_gnome_keyring.so auto_start +-session optional pam_kwallet5.so auto_start From b7e1c81ad1868b5b6f908b12af0595e7ecd0ab00 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Sat, 27 Jul 2024 20:36:45 +0200 Subject: [PATCH 032/530] Re-arrange keyring PAM files + integrate with pam_systemd & elogind Signed-off-by: AnErrupTion --- res/pam.d/ly | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/res/pam.d/ly b/res/pam.d/ly index c4da380..781605e 100644 --- a/res/pam.d/ly +++ b/res/pam.d/ly @@ -1,10 +1,21 @@ #%PAM-1.0 -auth include login +# Unlock GNOME Keyring -auth optional pam_gnome_keyring.so +-session optional pam_gnome_keyring.so auto_start + +# Unlock KWallet -auth optional pam_kwallet5.so +-session optional pam_kwallet5.so auto_start + +# Integrate with systemd-logind +-session optional pam_systemd.so class=greeter + +# Integrate with elogind +-session optional pam_elogind.so + +# Include system defaults +auth include login account include login password include login session include login --session optional pam_gnome_keyring.so auto_start --session optional pam_kwallet5.so auto_start From 10cd9615ef768a8caa6ed4462b3a2d72a31c77f7 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Sat, 27 Jul 2024 21:07:41 +0200 Subject: [PATCH 033/530] Backport: Fix possible overflow with TTY ID Co-authored-by: Kevin Morris 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 dbbdfda..2da71ab 100644 --- a/src/auth.zig +++ b/src/auth.zig @@ -21,7 +21,7 @@ pub fn sessionSignalHandler(i: c_int) callconv(.C) void { } pub fn authenticate(config: Config, current_environment: Desktop.Environment, login: [:0]const u8, password: [:0]const u8) !void { - var tty_buffer: [2]u8 = undefined; + var tty_buffer: [3]u8 = undefined; const tty_str = try std.fmt.bufPrintZ(&tty_buffer, "{d}", .{config.tty}); // Set the XDG environment variables From a7e8b55c6e3dc547471c26d9ab270cc501bf6d84 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Sat, 27 Jul 2024 21:13:55 +0200 Subject: [PATCH 034/530] Add COSMIC in the list of tested DEs Signed-off-by: AnErrupTion --- readme.md | 1 + 1 file changed, 1 insertion(+) diff --git a/readme.md b/readme.md index 6759114..d69e590 100644 --- a/readme.md +++ b/readme.md @@ -36,6 +36,7 @@ The following desktop environments were tested with success: - bspwm - budgie - cinnamon + - cosmic - deepin - dwl - dwm From 92c6a388350c567dacf7e9164003891a73f19a32 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Sat, 27 Jul 2024 22:21:50 +0200 Subject: [PATCH 035/530] Fix ~/.profile not being loaded with Fish Signed-off-by: AnErrupTion --- res/wsetup.sh | 1 + res/xsetup.sh | 1 + 2 files changed, 2 insertions(+) diff --git a/res/wsetup.sh b/res/wsetup.sh index 68041a3..fd3a583 100755 --- a/res/wsetup.sh +++ b/res/wsetup.sh @@ -40,6 +40,7 @@ case $SHELL in ;; */fish) [ -f /etc/profile ] && . /etc/profile + [ -f $HOME/.profile ] && . $HOME/.profile xsess_tmp=`mktemp /tmp/xsess-env-XXXXXX` $SHELL --login -c "/bin/sh -c 'export -p' > $xsess_tmp" . $xsess_tmp diff --git a/res/xsetup.sh b/res/xsetup.sh index 9418530..2c962f5 100755 --- a/res/xsetup.sh +++ b/res/xsetup.sh @@ -40,6 +40,7 @@ case $SHELL in ;; */fish) [ -f /etc/profile ] && . /etc/profile + [ -f $HOME/.profile ] && . $HOME/.profile xsess_tmp=`mktemp /tmp/xsess-env-XXXXXX` $SHELL --login -c "/bin/sh -c 'export -p' > $xsess_tmp" . $xsess_tmp From 1df890b238ddec3854651818ccbb60443bfb1ac4 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Sat, 27 Jul 2024 22:31:56 +0200 Subject: [PATCH 036/530] Set PAM_TTY (fixes #248) Signed-off-by: AnErrupTion --- src/auth.zig | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/auth.zig b/src/auth.zig index 7497c6d..d0f619d 100644 --- a/src/auth.zig +++ b/src/auth.zig @@ -25,6 +25,9 @@ pub fn authenticate(config: Config, current_environment: Desktop.Environment, lo var tty_buffer: [3]u8 = undefined; const tty_str = try std.fmt.bufPrintZ(&tty_buffer, "{d}", .{config.tty}); + var pam_tty_buffer: [6]u8 = undefined; + const pam_tty_str = try std.fmt.bufPrintZ(&pam_tty_buffer, "tty{d}", .{config.tty}); + // Set the XDG environment variables setXdgSessionEnv(current_environment.display_server); try setXdgEnv(tty_str, current_environment.xdg_session_desktop, current_environment.xdg_desktop_names orelse ""); @@ -42,6 +45,10 @@ pub fn authenticate(config: Config, current_environment: Desktop.Environment, lo 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 + 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 status = interop.pam.pam_authenticate(handle, 0); if (status != interop.pam.PAM_SUCCESS) return pamDiagnose(status); From 7ece95965b3c2ac961c306670c28066302055867 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Sat, 27 Jul 2024 22:44:35 +0200 Subject: [PATCH 037/530] Backport: Fix ~/.profile not being loaded with Fish Signed-off-by: AnErrupTion --- res/wsetup.sh | 1 + res/xsetup.sh | 1 + 2 files changed, 2 insertions(+) diff --git a/res/wsetup.sh b/res/wsetup.sh index 68041a3..fd3a583 100755 --- a/res/wsetup.sh +++ b/res/wsetup.sh @@ -40,6 +40,7 @@ case $SHELL in ;; */fish) [ -f /etc/profile ] && . /etc/profile + [ -f $HOME/.profile ] && . $HOME/.profile xsess_tmp=`mktemp /tmp/xsess-env-XXXXXX` $SHELL --login -c "/bin/sh -c 'export -p' > $xsess_tmp" . $xsess_tmp diff --git a/res/xsetup.sh b/res/xsetup.sh index 9418530..2c962f5 100755 --- a/res/xsetup.sh +++ b/res/xsetup.sh @@ -40,6 +40,7 @@ case $SHELL in ;; */fish) [ -f /etc/profile ] && . /etc/profile + [ -f $HOME/.profile ] && . $HOME/.profile xsess_tmp=`mktemp /tmp/xsess-env-XXXXXX` $SHELL --login -c "/bin/sh -c 'export -p' > $xsess_tmp" . $xsess_tmp From 802ad6bbed84859e2cfab25191dc3f89d3420f9f Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Sat, 27 Jul 2024 22:45:20 +0200 Subject: [PATCH 038/530] Backport: Set PAM_TTY Signed-off-by: AnErrupTion --- src/auth.zig | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/auth.zig b/src/auth.zig index 2da71ab..b55c9d2 100644 --- a/src/auth.zig +++ b/src/auth.zig @@ -24,6 +24,9 @@ pub fn authenticate(config: Config, current_environment: Desktop.Environment, lo var tty_buffer: [3]u8 = undefined; const tty_str = try std.fmt.bufPrintZ(&tty_buffer, "{d}", .{config.tty}); + var pam_tty_buffer: [6]u8 = undefined; + const pam_tty_str = try std.fmt.bufPrintZ(&pam_tty_buffer, "tty{d}", .{config.tty}); + // Set the XDG environment variables setXdgSessionEnv(current_environment.display_server); try setXdgEnv(tty_str, current_environment.xdg_session_desktop, current_environment.xdg_desktop_names orelse ""); @@ -41,6 +44,10 @@ pub fn authenticate(config: Config, current_environment: Desktop.Environment, lo 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 + 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 status = interop.pam.pam_authenticate(handle, 0); if (status != interop.pam.PAM_SUCCESS) return pamDiagnose(status); From 2bc12549a18db544ac5515998c331b2e2be18742 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Sat, 27 Jul 2024 23:39:09 +0200 Subject: [PATCH 039/530] Switch to utmpx Signed-off-by: AnErrupTion --- src/auth.zig | 15 ++++++++------- src/interop.zig | 2 +- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/src/auth.zig b/src/auth.zig index d0f619d..bab791f 100644 --- a/src/auth.zig +++ b/src/auth.zig @@ -1,5 +1,6 @@ const std = @import("std"); const build_options = @import("build_options"); +const builtin = @import("builtin"); const enums = @import("enums.zig"); const interop = @import("interop.zig"); const TerminalBuffer = @import("tui/TerminalBuffer.zig"); @@ -8,7 +9,7 @@ const Text = @import("tui/components/Text.zig"); const Config = @import("config/Config.zig"); const Allocator = std.mem.Allocator; const utmp = interop.utmp; -const Utmp = utmp.utmp; +const Utmp = utmp.utmpx; const SharedError = @import("SharedError.zig"); var xorg_pid: std.posix.pid_t = 0; @@ -486,18 +487,18 @@ fn addUtmpEntry(entry: *Utmp, username: [*:0]const u8, pid: c_int) !void { }; entry.ut_addr_v6[0] = 0; - utmp.setutent(); - _ = utmp.pututline(entry); - utmp.endutent(); + utmp.setutxent(); + _ = utmp.pututxline(entry); + utmp.endutxent(); } fn removeUtmpEntry(entry: *Utmp) void { entry.ut_type = utmp.DEAD_PROCESS; entry.ut_line[0] = 0; entry.ut_user[0] = 0; - utmp.setutent(); - _ = utmp.pututline(entry); - utmp.endutent(); + utmp.setutxent(); + _ = utmp.pututxline(entry); + utmp.endutxent(); } fn pamDiagnose(status: c_int) anyerror { diff --git a/src/interop.zig b/src/interop.zig index b378f34..972cce4 100644 --- a/src/interop.zig +++ b/src/interop.zig @@ -9,7 +9,7 @@ pub const pam = @cImport({ }); pub const utmp = @cImport({ - @cInclude("utmp.h"); + @cInclude("utmpx.h"); }); pub const xcb = @cImport({ From 8333f7ea7769e433f5e24d85f100b080610ce6be Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Sun, 28 Jul 2024 00:05:37 +0200 Subject: [PATCH 040/530] Update screenshot Signed-off-by: AnErrupTion --- .github/screenshot.png | Bin 0 -> 121750 bytes readme.md | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 .github/screenshot.png diff --git a/.github/screenshot.png b/.github/screenshot.png new file mode 100644 index 0000000000000000000000000000000000000000..9d0db4bd0bf792c36c63bb21a498497675d3dd08 GIT binary patch literal 121750 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\E98=BtKFgMP}Q?TyE(GC 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

F|>no4{(d^3M;!CF?;p39QC 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 Date: Sun, 28 Jul 2024 00:06:08 +0200 Subject: [PATCH 041/530] Remove changelog.md Signed-off-by: AnErrupTion --- changelog.md | 52 ---------------------------------------------------- 1 file changed, 52 deletions(-) delete mode 100644 changelog.md diff --git a/changelog.md b/changelog.md deleted file mode 100644 index b7abd61..0000000 --- a/changelog.md +++ /dev/null @@ -1,52 +0,0 @@ -# Zig Rewrite (Version 1.0.0) - -## Config Options - -res/config.ini contains all of the available config options and their default values. - -### Additions - -+ `border_fg` has been introduced to change the color of the borders. -+ `term_restore_cursor_cmd` should restore the cursor to it's usual state. -+ `vi_mode` to enable vi keybindings. -+ `sleep_key` and `sleep_cmd`. -+ `numlock` to set numlock on startup. -+ `initial_info_text` allows changing the initial text on the info line. -+ `box_title` to display a title at the top of the main box - -Note: `sleep_cmd` is unset by default, meaning it's hidden and has no effect. - -### Changes - -+ xinitrc can be set to null to hide it. -+ `blank_password` has been renamed to `clear_password`. -+ `save_file` has been deprecated and will be removed in a future version. - -### Removals - -+ `wayland_specifier` has been removed. - -## Save File - -The save file is now in .ini format and stored in the same directory as the config. -Older save files will be migrated to the new format. - -Example: - -```ini -user = ash -session_index = 0 -``` - -## Misc - -+ Display server name added next to selected session. -+ getty@tty2 has been added as a conflict in res/ly.service, so if it is running, ly should still be able to start. -+ `XDG_CURRENT_DESKTOP` is now set by ly. -+ LANG is no longer set by ly. -+ X Server PID is fetched from /tmp/X{d}.lock to be able to kill the process since it detaches. -+ Non .desktop files are now ignored in sessions directory. -+ PAM auth is now done in a child process. (Fixes some issues with logging out and back in). -+ When ly receives SIGTERM, the terminal is now cleared and existing child processes are cleaned up. -+ Shift+Tab now focuses previous input. -+ Display text in the info line when authenticating. From 075bf67cef6935417b6e13c1c5e0c3ed74f36abb Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Sun, 28 Jul 2024 00:09:59 +0200 Subject: [PATCH 042/530] Fix missing brightness localized strings Signed-off-by: AnErrupTion --- res/lang/en.ini | 2 ++ res/lang/fr.ini | 2 ++ src/config/Lang.zig | 4 ++-- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/res/lang/en.ini b/res/lang/en.ini index c8de400..21ccc30 100644 --- a/res/lang/en.ini +++ b/res/lang/en.ini @@ -1,4 +1,6 @@ authenticating = authenticating... +brightness_down = decrease brightness +brightness_up = increase brightness capslock = capslock err_alloc = failed memory allocation err_bounds = out-of-bounds index diff --git a/res/lang/fr.ini b/res/lang/fr.ini index e2741f4..c6c6761 100644 --- a/res/lang/fr.ini +++ b/res/lang/fr.ini @@ -1,4 +1,6 @@ authenticating = authentification... +brightness_down = diminuer la luminosité +brightness_up = augmenter la luminosité capslock = verr.maj err_alloc = échec d'allocation mémoire err_bounds = indice hors-limite diff --git a/src/config/Lang.zig b/src/config/Lang.zig index 24bd011..cba027c 100644 --- a/src/config/Lang.zig +++ b/src/config/Lang.zig @@ -1,4 +1,6 @@ authenticating: []const u8 = "authenticating...", +brightness_down: []const u8 = "decrease brightness", +brightness_up: []const u8 = "increase brightness", capslock: []const u8 = "capslock", err_alloc: []const u8 = "failed memory allocation", err_bounds: []const u8 = "out-of-bounds index", @@ -56,5 +58,3 @@ sleep: []const u8 = "sleep", wayland: []const u8 = "wayland", xinitrc: [:0]const u8 = "xinitrc", x11: []const u8 = "x11", -brightness_down: []const u8 = "decrease brightness", -brightness_up: []const u8 = "increase brightness", From 93554d9ba3604404c8b413add71010c5998dac5a Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Sun, 28 Jul 2024 11:34:33 +0200 Subject: [PATCH 043/530] Add missing supervise symlink on runit (fixes #610) Signed-off-by: AnErrupTion --- build.zig | 3 +++ 1 file changed, 3 insertions(+) diff --git a/build.zig b/build.zig index ec8c013..bbd538a 100644 --- a/build.zig +++ b/build.zig @@ -160,9 +160,12 @@ pub fn ServiceInstaller(comptime init_system: InitSystem) type { var service_dir = std.fs.cwd().openDir(service_path, .{}) catch unreachable; defer service_dir.close(); + const supervise_path = try std.fs.path.join(allocator, &[_][]const u8{ service_path, "supervise" }); + try std.fs.cwd().copyFile("res/ly-runit-service/conf", service_dir, "conf", .{}); try std.fs.cwd().copyFile("res/ly-runit-service/finish", service_dir, "finish", .{ .override_mode = 0o755 }); try std.fs.cwd().copyFile("res/ly-runit-service/run", service_dir, "run", .{ .override_mode = 0o755 }); + try std.fs.cwd().symLink("/run/runit/supervise.ly", supervise_path, .{}); }, .S6 => { const admin_service_path = try std.fs.path.join(allocator, &[_][]const u8{ dest_directory, "/etc/s6/adminsv/default/contents.d" }); From 5796720a9ce7a1ab996eadf746af94df16cd78b6 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Sun, 28 Jul 2024 11:35:23 +0200 Subject: [PATCH 044/530] Backport: Add missing supervise symlink on runit Signed-off-by: AnErrupTion --- build.zig | 3 +++ 1 file changed, 3 insertions(+) diff --git a/build.zig b/build.zig index 4452475..083bd2a 100644 --- a/build.zig +++ b/build.zig @@ -126,9 +126,12 @@ pub fn ServiceInstaller(comptime init_system: InitSystem) type { var service_dir = std.fs.cwd().openDir(service_path, .{}) catch unreachable; defer service_dir.close(); + const supervise_path = try std.fs.path.join(allocator, &[_][]const u8{ service_path, "supervise" }); + try std.fs.cwd().copyFile("res/ly-runit-service/conf", service_dir, "conf", .{}); try std.fs.cwd().copyFile("res/ly-runit-service/finish", service_dir, "finish", .{ .override_mode = 0o755 }); try std.fs.cwd().copyFile("res/ly-runit-service/run", service_dir, "run", .{ .override_mode = 0o755 }); + try std.fs.cwd().symLink("/run/runit/supervise.ly", supervise_path, .{}); }, .Systemd => { const service_path = try std.fs.path.join(allocator, &[_][]const u8{ dest_directory, "/usr/lib/systemd/system" }); From 19d4b195f323f67b8426f10db79dfad56b23db30 Mon Sep 17 00:00:00 2001 From: Moabeat Date: Sun, 28 Jul 2024 13:02:42 +0200 Subject: [PATCH 045/530] Display error messages differently in info line (#661) * Add list of error messages to InfoLine.zig * Change info and error cases according to review * Add changes from review for width calculation --- res/config.ini | 7 ++++ src/bigclock.zig | 2 +- src/config/Config.zig | 6 ++-- src/main.zig | 52 +++++++++++++++++------------ src/tui/TerminalBuffer.zig | 10 ++++-- src/tui/components/InfoLine.zig | 58 ++++++++++++++++++++++++++++----- 6 files changed, 101 insertions(+), 34 deletions(-) diff --git a/res/config.ini b/res/config.ini index b0f0fb4..da149a6 100644 --- a/res/config.ini +++ b/res/config.ini @@ -49,6 +49,13 @@ bg = 0 # Foreground color id fg = 8 +# Background color errors +error_bg = 0 + +# Foreground color errors +# Default is red and bold: TB_RED | TB_BOLD +error_fg = 258 + # CMatrix animation foreground color id cmatrix_fg = 3 diff --git a/src/bigclock.zig b/src/bigclock.zig index 00f7939..9054a75 100644 --- a/src/bigclock.zig +++ b/src/bigclock.zig @@ -99,7 +99,7 @@ const E = [_]u21{ }; // zig fmt: on -pub fn clockCell(animate: bool, char: u8, fg: u8, bg: u8) [SIZE]termbox.tb_cell { +pub fn clockCell(animate: bool, char: u8, fg: u16, bg: u16) [SIZE]termbox.tb_cell { var cells: [SIZE]termbox.tb_cell = undefined; var tv: std.c.timeval = undefined; diff --git a/src/config/Config.zig b/src/config/Config.zig index ca8290a..0274ce5 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -7,7 +7,7 @@ const ViMode = enums.ViMode; animation: Animation = .none, asterisk: u8 = '*', -bg: u8 = 0, +bg: u16 = 0, bigclock: bool = false, blank_box: bool = true, border_fg: u8 = 8, @@ -16,7 +16,9 @@ clear_password: bool = false, clock: ?[:0]const u8 = null, console_dev: [:0]const u8 = "/dev/console", default_input: Input = .login, -fg: u8 = 8, +error_bg: u16 = 0, +error_fg: u16 = 258, +fg: u16 = 8, cmatrix_fg: u8 = 3, hide_borders: bool = false, hide_key_hints: bool = false, diff --git a/src/main.zig b/src/main.zig index 37e338f..27fd6ec 100644 --- a/src/main.zig +++ b/src/main.zig @@ -62,7 +62,8 @@ pub fn main() !void { var config: Config = undefined; var lang: Lang = undefined; var save: Save = undefined; - var info_line = InfoLine{}; + var info_line = InfoLine.init(allocator); + defer info_line.deinit(); if (res.args.help != 0) { try clap.help(stderr, clap.Help, ¶ms, .{}); @@ -100,7 +101,11 @@ pub fn main() !void { const config_path = try std.fmt.allocPrint(allocator, "{s}{s}config.ini", .{ s, trailing_slash }); defer allocator.free(config_path); - config = config_ini.readFileToStructWithMap(config_path, mapped_config_fields) catch Config{}; + config = config_ini.readFileToStructWithMap(config_path, mapped_config_fields) catch _config: { + // literal error message, due to language file not yet available + try info_line.addError("unable to parse config file"); + break :_config Config{}; + }; const lang_path = try std.fmt.allocPrint(allocator, "{s}{s}lang/{s}.ini", .{ s, trailing_slash, config.lang }); defer allocator.free(lang_path); @@ -115,7 +120,11 @@ pub fn main() !void { save = save_ini.readFileToStruct(save_path) catch migrator.tryMigrateSaveFile(&user_buf, config.save_file); } } else { - config = config_ini.readFileToStructWithMap(build_options.data_directory ++ "/config.ini", mapped_config_fields) catch Config{}; + config = config_ini.readFileToStructWithMap(build_options.data_directory ++ "/config.ini", mapped_config_fields) catch _config: { + // literal error message, due to language file not yet available + try info_line.addError("unable to parse config file"); + break :_config Config{}; + }; const lang_path = try std.fmt.allocPrint(allocator, "{s}/lang/{s}.ini", .{ build_options.data_directory, config.lang }); defer allocator.free(lang_path); @@ -128,6 +137,9 @@ pub fn main() !void { } } + info_line.error_bg = config.error_bg; + info_line.error_fg = config.error_fg; + if (!build_options.enable_x11_support) try info_line.setText(lang.no_x11_support); interop.setNumlock(config.numlock) catch {}; @@ -178,13 +190,13 @@ pub fn main() !void { defer desktop.deinit(); desktop.addEnvironment(.{ .Name = lang.shell }, "", .shell) catch { - try info_line.setText(lang.err_alloc); + try info_line.addError(lang.err_alloc); }; if (build_options.enable_x11_support) { if (config.xinitrc) |xinitrc| { desktop.addEnvironment(.{ .Name = lang.xinitrc, .Exec = xinitrc }, "", .xinitrc) catch { - try info_line.setText(lang.err_alloc); + try info_line.addError(lang.err_alloc); }; } } @@ -227,10 +239,10 @@ pub fn main() !void { switch (active_input) { .session => desktop.handle(null, insert_mode), .login => login.handle(null, insert_mode) catch { - try info_line.setText(lang.err_alloc); + try info_line.addError(lang.err_alloc); }, .password => password.handle(null, insert_mode) catch { - try info_line.setText(lang.err_alloc); + try info_line.addError(lang.err_alloc); }, } } @@ -274,7 +286,7 @@ pub fn main() !void { // Switch to selected TTY if possible open_console_dev: { const fd = std.posix.open(config.console_dev, .{ .ACCMODE = .WRONLY }, 0) catch { - try info_line.setText(lang.err_console_dev); + try info_line.addError(lang.err_console_dev); break :open_console_dev; }; defer std.posix.close(fd); @@ -310,10 +322,10 @@ pub fn main() !void { switch (config.animation) { .none => {}, .doom => doom.realloc() catch { - try info_line.setText(lang.err_alloc); + try info_line.addError(lang.err_alloc); }, .matrix => matrix.realloc() catch { - try info_line.setText(lang.err_alloc); + try info_line.addError(lang.err_alloc); }, } @@ -362,10 +374,10 @@ pub fn main() !void { switch (active_input) { .session => desktop.handle(null, insert_mode), .login => login.handle(null, insert_mode) catch { - try info_line.setText(lang.err_alloc); + try info_line.addError(lang.err_alloc); }, .password => password.handle(null, insert_mode) catch { - try info_line.setText(lang.err_alloc); + try info_line.addError(lang.err_alloc); }, } @@ -386,7 +398,7 @@ pub fn main() !void { buffer.drawLabel(lang.login, label_x, label_y + 4); buffer.drawLabel(lang.password, label_x, label_y + 6); - info_line.draw(buffer); + try info_line.draw(buffer); if (!config.hide_key_hints) { var length: u64 = 0; @@ -439,7 +451,7 @@ pub fn main() !void { draw_lock_state: { const lock_state = interop.getLockState(config.console_dev) catch { - try info_line.setText(lang.err_console_dev); + try info_line.addError(lang.err_console_dev); break :draw_lock_state; }; @@ -515,7 +527,7 @@ pub fn main() !void { } } else if (pressed_key == brightness_down_key and unistd.access(&config.brightnessctl[0], unistd.X_OK) == 0) brightness_change: { const brightness_str = std.fmt.allocPrint(allocator, "{s}%-", .{config.brightness_change}) catch { - try info_line.setText(lang.err_brightness_change); + try info_line.addError(lang.err_brightness_change); break :brightness_change; }; defer allocator.free(brightness_str); @@ -523,7 +535,7 @@ pub fn main() !void { _ = brightness.spawnAndWait() catch .{}; } else if (pressed_key == brightness_up_key and unistd.access(&config.brightnessctl[0], unistd.X_OK) == 0) brightness_change: { const brightness_str = std.fmt.allocPrint(allocator, "+{s}%", .{config.brightness_change}) catch { - try info_line.setText(lang.err_brightness_change); + try info_line.addError(lang.err_brightness_change); break :brightness_change; }; defer allocator.free(brightness_str); @@ -595,7 +607,7 @@ pub fn main() !void { try info_line.setText(lang.authenticating); InfoLine.clearRendered(allocator, buffer) catch {}; - info_line.draw(buffer); + try info_line.draw(buffer); _ = termbox.tb_present(); session_pid = try std.posix.fork(); @@ -616,7 +628,7 @@ pub fn main() !void { if (auth_err) |err| { auth_fails += 1; active_input = .password; - try info_line.setText(getAuthErrorMsg(err, lang)); + try info_line.addError(getAuthErrorMsg(err, lang)); if (config.clear_password or err != error.PamAuthError) password.clear(); } else { password.clear(); @@ -665,10 +677,10 @@ pub fn main() !void { switch (active_input) { .session => desktop.handle(&event, insert_mode), .login => login.handle(&event, insert_mode) catch { - try info_line.setText(lang.err_alloc); + try info_line.addError(lang.err_alloc); }, .password => password.handle(&event, insert_mode) catch { - try info_line.setText(lang.err_alloc); + try info_line.addError(lang.err_alloc); }, } update = true; diff --git a/src/tui/TerminalBuffer.zig b/src/tui/TerminalBuffer.zig index 9692365..ebc8ca7 100644 --- a/src/tui/TerminalBuffer.zig +++ b/src/tui/TerminalBuffer.zig @@ -14,8 +14,8 @@ random: Random, width: u64, height: u64, buffer: [*]termbox.tb_cell, -fg: u8, -bg: u8, +fg: u16, +bg: u16, border_fg: u8, box_chars: struct { left_up: u32, @@ -158,13 +158,17 @@ pub fn calculateComponentCoordinates(self: TerminalBuffer) struct { } pub fn drawLabel(self: TerminalBuffer, text: []const u8, x: u64, y: u64) void { + drawColorLabel(text, x, y, self.fg, self.bg); +} + +pub fn drawColorLabel(text: []const u8, x: u64, y: u64, fg: u16, bg: u16) void { const yc: c_int = @intCast(y); const utf8view = std.unicode.Utf8View.init(text) catch return; var utf8 = utf8view.iterator(); var i = x; while (utf8.nextCodepoint()) |codepoint| : (i += 1) { - _ = termbox.tb_set_cell(@intCast(i), yc, codepoint, self.fg, self.bg); + _ = termbox.tb_set_cell(@intCast(i), yc, codepoint, fg, bg); } } diff --git a/src/tui/components/InfoLine.zig b/src/tui/components/InfoLine.zig index 82d33ee..2a61af6 100644 --- a/src/tui/components/InfoLine.zig +++ b/src/tui/components/InfoLine.zig @@ -2,25 +2,67 @@ const std = @import("std"); const utils = @import("../utils.zig"); const TerminalBuffer = @import("../TerminalBuffer.zig"); +const ArrayList = std.ArrayList; +const ErrorMessage = struct { width: u8, text: []const u8 }; + const InfoLine = @This(); -text: []const u8 = "", -width: u8 = 0, +error_list: ArrayList(ErrorMessage), +error_bg: u16, +error_fg: u16, +text: []const u8, +width: u8, + +pub fn init(allocator: std.mem.Allocator) InfoLine { + return .{ + .error_list = ArrayList(ErrorMessage).init(allocator), + .error_bg = 0, + .error_fg = 258, + .text = "", + .width = 0, + }; +} + +pub fn deinit(self: InfoLine) void { + self.error_list.deinit(); +} pub fn setText(self: *InfoLine, text: []const u8) !void { self.width = if (text.len > 0) try utils.strWidth(text) else 0; self.text = text; } -pub fn draw(self: InfoLine, buffer: TerminalBuffer) void { - if (self.width > 0 and buffer.box_width > self.width) { - const label_y = buffer.box_y + buffer.margin_box_v; - const x = buffer.box_x + ((buffer.box_width - self.width) / 2); - - buffer.drawLabel(self.text, x, label_y); +pub fn addError(self: *InfoLine, error_message: []const u8) !void { + if (error_message.len > 0) { + const entry = .{ + .width = try utils.strWidth(error_message), + .text = error_message, + }; + try self.error_list.append(entry); } } +pub fn draw(self: InfoLine, buffer: TerminalBuffer) !void { + var text: []const u8 = self.text; + var bg: u16 = buffer.bg; + var fg: u16 = buffer.fg; + var width: u8 = self.width; + + if (self.error_list.items.len > 0) { + const entry = self.error_list.getLast(); + text = entry.text; + bg = self.error_bg; + fg = self.error_fg; + width = entry.width; + } + + if (width > 0 and buffer.box_width > width) { + const label_y = buffer.box_y + buffer.margin_box_v; + const x = buffer.box_x + ((buffer.box_width - width) / 2); + TerminalBuffer.drawColorLabel(text, x, label_y, fg, bg); + } +} + pub fn clearRendered(allocator: std.mem.Allocator, buffer: TerminalBuffer) !void { // draw over the area const y = buffer.box_y + buffer.margin_box_v; From 2dd83b41e86126944ef20e69d8670ea4d5fde577 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Sun, 28 Jul 2024 13:15:49 +0200 Subject: [PATCH 046/530] Incorporate some FreeBSD authentication patches Signed-off-by: AnErrupTion --- src/auth.zig | 31 ++++++++++++++++++++++--------- src/interop.zig | 6 ++++++ 2 files changed, 28 insertions(+), 9 deletions(-) diff --git a/src/auth.zig b/src/auth.zig index bab791f..33b16f1 100644 --- a/src/auth.zig +++ b/src/auth.zig @@ -130,8 +130,14 @@ fn startSession( const status = interop.initgroups(pwd.pw_name, pwd.pw_gid); if (status != 0) return error.GroupInitializationFailed; - std.posix.setgid(pwd.pw_gid) catch return error.SetUserGidFailed; - std.posix.setuid(pwd.pw_uid) catch return error.SetUserUidFailed; + if (builtin.os.tag == .freebsd) { + // FreeBSD sets the GID and UID with setusercontext() + const result = std.c.setusercontext(null, pwd, pwd.pw_uid, interop.logincap.LOGIN_SETALL); + if (result != 0) return error.SetUserUidFailed; + } else { + std.posix.setgid(pwd.pw_gid) catch return error.SetUserGidFailed; + std.posix.setuid(pwd.pw_uid) catch return error.SetUserUidFailed; + } // Set up the environment try initEnv(pwd, config.path); @@ -181,12 +187,19 @@ fn setXdgSessionEnv(display_server: enums.DisplayServer) void { } fn setXdgEnv(tty_str: [:0]u8, desktop_name: [:0]const u8, xdg_desktop_names: [:0]const u8) !void { - const uid = interop.getuid(); - var uid_buffer: [10 + @sizeOf(u32) + 1]u8 = undefined; - const uid_str = try std.fmt.bufPrintZ(&uid_buffer, "/run/user/{d}", .{uid}); + // 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 = interop.getuid(); + var uid_buffer: [10 + @sizeOf(u32) + 1]u8 = undefined; + const uid_str = try std.fmt.bufPrintZ(&uid_buffer, "/run/user/{d}", .{uid}); + + _ = interop.setenv("XDG_RUNTIME_DIR", uid_str.ptr, 0); + } _ = interop.setenv("XDG_CURRENT_DESKTOP", xdg_desktop_names.ptr, 0); - _ = interop.setenv("XDG_RUNTIME_DIR", uid_str.ptr, 0); _ = interop.setenv("XDG_SESSION_CLASS", "user", 0); _ = interop.setenv("XDG_SESSION_ID", "1", 0); _ = interop.setenv("XDG_SESSION_DESKTOP", desktop_name.ptr, 0); @@ -463,18 +476,18 @@ fn addUtmpEntry(entry: *Utmp, username: [*:0]const u8, pid: c_int) !void { var buf: [4096]u8 = undefined; const ttyname = try std.os.getFdPath(std.posix.STDIN_FILENO, &buf); - var ttyname_buf: [32]u8 = undefined; + var ttyname_buf: [@sizeOf(@TypeOf(entry.ut_line))]u8 = undefined; _ = try std.fmt.bufPrintZ(&ttyname_buf, "{s}", .{ttyname["/dev/".len..]}); entry.ut_line = ttyname_buf; entry.ut_id = ttyname_buf["tty".len..7].*; - var username_buf: [32]u8 = undefined; + var username_buf: [@sizeOf(@TypeOf(entry.ut_user))]u8 = undefined; _ = try std.fmt.bufPrintZ(&username_buf, "{s}", .{username}); entry.ut_user = username_buf; - var host: [256]u8 = undefined; + var host: [@sizeOf(@TypeOf(entry.ut_host))]u8 = undefined; host[0] = 0; entry.ut_host = host; diff --git a/src/interop.zig b/src/interop.zig index 972cce4..23a90b9 100644 --- a/src/interop.zig +++ b/src/interop.zig @@ -12,6 +12,7 @@ pub const utmp = @cImport({ @cInclude("utmpx.h"); }); +// Exists for X11 support only pub const xcb = @cImport({ @cInclude("xcb/xcb.h"); }); @@ -20,6 +21,11 @@ pub const unistd = @cImport({ @cInclude("unistd.h"); }); +// Exists for FreeBSD only +pub const logincap = @cImport({ + @cInclude("login_cap.h"); +}); + pub const c_size = u64; pub const c_uid = u32; pub const c_gid = u32; From 04a0ad3b33ad881b6aaca7e8a462d915cbfc2d41 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Sun, 28 Jul 2024 18:17:14 +0200 Subject: [PATCH 047/530] Move Gentoo/OpenRC installation tip to the OpenRC section Signed-off-by: AnErrupTion --- readme.md | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/readme.md b/readme.md index 2a0589b..293cace 100644 --- a/readme.md +++ b/readme.md @@ -105,7 +105,7 @@ disable getty on Ly's tty to prevent "login" from spawning on top of it ``` ### OpenRC -**NOTE**: On Gentoo, Ly will disable the `display-manager-init` service in order to run. +**NOTE 1**: On Gentoo, Ly will disable the `display-manager-init` service in order to run. Clone, compile and test. @@ -127,6 +127,8 @@ then you have to disable getty, so it doesn't respawn on top of ly # rc-update del agetty.tty2 ``` +**NOTE 2**: To avoid a console spawning on top on Ly, comment out the appropriate line from /etc/inittab (default is 2). + ### runit ``` # zig build installrunit @@ -228,6 +230,3 @@ disable the main box borders with `hide_borders = true`. ## Additional Information The name "Ly" is a tribute to the fairy from the game Rayman. Ly was tested by oxodao, who is some seriously awesome dude. - -## Gentoo (OpenRC) installation tip -To avoid a console spawning on top on Ly, comment out the appropriate line from /etc/inittab (default is 2). From 56c210372d3c1ac4ad0834451fb2708dcc23fcce Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Sun, 28 Jul 2024 23:05:12 +0200 Subject: [PATCH 048/530] Better handle info line messages internally Signed-off-by: AnErrupTion --- src/main.zig | 51 ++++++++++++------------ src/tui/components/InfoLine.zig | 69 +++++++++++++-------------------- 2 files changed, 50 insertions(+), 70 deletions(-) diff --git a/src/main.zig b/src/main.zig index 27fd6ec..42e617f 100644 --- a/src/main.zig +++ b/src/main.zig @@ -102,8 +102,8 @@ pub fn main() !void { defer allocator.free(config_path); config = config_ini.readFileToStructWithMap(config_path, mapped_config_fields) catch _config: { - // literal error message, due to language file not yet available - try info_line.addError("unable to parse config file"); + // We're using a literal error message here since the language file hasn't yet been loaded + try info_line.addMessage("unable to parse config file", @intCast(interop.termbox.TB_DEFAULT), @intCast(interop.termbox.TB_RED | interop.termbox.TB_BOLD)); break :_config Config{}; }; @@ -122,7 +122,7 @@ pub fn main() !void { } else { config = config_ini.readFileToStructWithMap(build_options.data_directory ++ "/config.ini", mapped_config_fields) catch _config: { // literal error message, due to language file not yet available - try info_line.addError("unable to parse config file"); + try info_line.addMessage("unable to parse config file", @intCast(interop.termbox.TB_DEFAULT), @intCast(interop.termbox.TB_RED | interop.termbox.TB_BOLD)); break :_config Config{}; }; @@ -137,23 +137,20 @@ pub fn main() !void { } } - info_line.error_bg = config.error_bg; - info_line.error_fg = config.error_fg; - - if (!build_options.enable_x11_support) try info_line.setText(lang.no_x11_support); + if (!build_options.enable_x11_support) try info_line.addMessage(lang.no_x11_support, config.bg, config.fg); interop.setNumlock(config.numlock) catch {}; if (config.initial_info_text) |text| { - try info_line.setText(text); + try info_line.addMessage(text, config.bg, 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 { - try info_line.setText(lang.err_hostname); + try info_line.addMessage(lang.err_hostname, config.error_bg, config.error_fg); break :get_host_name; }; - try info_line.setText(hostname); + try info_line.addMessage(hostname, config.bg, config.fg); } // Initialize termbox @@ -190,13 +187,13 @@ pub fn main() !void { defer desktop.deinit(); desktop.addEnvironment(.{ .Name = lang.shell }, "", .shell) catch { - try info_line.addError(lang.err_alloc); + try info_line.addMessage(lang.err_alloc, config.error_bg, config.error_fg); }; if (build_options.enable_x11_support) { if (config.xinitrc) |xinitrc| { desktop.addEnvironment(.{ .Name = lang.xinitrc, .Exec = xinitrc }, "", .xinitrc) catch { - try info_line.addError(lang.err_alloc); + try info_line.addMessage(lang.err_alloc, config.error_bg, config.error_fg); }; } } @@ -239,10 +236,10 @@ pub fn main() !void { switch (active_input) { .session => desktop.handle(null, insert_mode), .login => login.handle(null, insert_mode) catch { - try info_line.addError(lang.err_alloc); + try info_line.addMessage(lang.err_alloc, config.error_bg, config.error_fg); }, .password => password.handle(null, insert_mode) catch { - try info_line.addError(lang.err_alloc); + try info_line.addMessage(lang.err_alloc, config.error_bg, config.error_fg); }, } } @@ -286,7 +283,7 @@ pub fn main() !void { // Switch to selected TTY if possible open_console_dev: { const fd = std.posix.open(config.console_dev, .{ .ACCMODE = .WRONLY }, 0) catch { - try info_line.addError(lang.err_console_dev); + try info_line.addMessage(lang.err_console_dev, config.error_bg, config.error_fg); break :open_console_dev; }; defer std.posix.close(fd); @@ -322,10 +319,10 @@ pub fn main() !void { switch (config.animation) { .none => {}, .doom => doom.realloc() catch { - try info_line.addError(lang.err_alloc); + try info_line.addMessage(lang.err_alloc, config.error_bg, config.error_fg); }, .matrix => matrix.realloc() catch { - try info_line.addError(lang.err_alloc); + try info_line.addMessage(lang.err_alloc, config.error_bg, config.error_fg); }, } @@ -374,10 +371,10 @@ pub fn main() !void { switch (active_input) { .session => desktop.handle(null, insert_mode), .login => login.handle(null, insert_mode) catch { - try info_line.addError(lang.err_alloc); + try info_line.addMessage(lang.err_alloc, config.error_bg, config.error_fg); }, .password => password.handle(null, insert_mode) catch { - try info_line.addError(lang.err_alloc); + try info_line.addMessage(lang.err_alloc, config.error_bg, config.error_fg); }, } @@ -451,7 +448,7 @@ pub fn main() !void { draw_lock_state: { const lock_state = interop.getLockState(config.console_dev) catch { - try info_line.addError(lang.err_console_dev); + try info_line.addMessage(lang.err_console_dev, config.error_bg, config.error_fg); break :draw_lock_state; }; @@ -527,7 +524,7 @@ pub fn main() !void { } } else if (pressed_key == brightness_down_key and unistd.access(&config.brightnessctl[0], unistd.X_OK) == 0) brightness_change: { const brightness_str = std.fmt.allocPrint(allocator, "{s}%-", .{config.brightness_change}) catch { - try info_line.addError(lang.err_brightness_change); + try info_line.addMessage(lang.err_brightness_change, config.error_bg, config.error_fg); break :brightness_change; }; defer allocator.free(brightness_str); @@ -535,7 +532,7 @@ pub fn main() !void { _ = brightness.spawnAndWait() catch .{}; } else if (pressed_key == brightness_up_key and unistd.access(&config.brightnessctl[0], unistd.X_OK) == 0) brightness_change: { const brightness_str = std.fmt.allocPrint(allocator, "+{s}%", .{config.brightness_change}) catch { - try info_line.addError(lang.err_brightness_change); + try info_line.addMessage(lang.err_brightness_change, config.error_bg, config.error_fg); break :brightness_change; }; defer allocator.free(brightness_str); @@ -605,7 +602,7 @@ pub fn main() !void { const password_text = try allocator.dupeZ(u8, password.text.items); defer allocator.free(password_text); - try info_line.setText(lang.authenticating); + try info_line.addMessage(lang.authenticating, config.bg, config.fg); InfoLine.clearRendered(allocator, buffer) catch {}; try info_line.draw(buffer); _ = termbox.tb_present(); @@ -628,11 +625,11 @@ pub fn main() !void { if (auth_err) |err| { auth_fails += 1; active_input = .password; - try info_line.addError(getAuthErrorMsg(err, lang)); + try info_line.addMessage(getAuthErrorMsg(err, lang), config.error_bg, config.error_fg); if (config.clear_password or err != error.PamAuthError) password.clear(); } else { password.clear(); - try info_line.setText(lang.logout); + try info_line.addMessage(lang.logout, config.bg, config.fg); } try std.posix.tcsetattr(std.posix.STDIN_FILENO, .FLUSH, tb_termios); @@ -677,10 +674,10 @@ pub fn main() !void { switch (active_input) { .session => desktop.handle(&event, insert_mode), .login => login.handle(&event, insert_mode) catch { - try info_line.addError(lang.err_alloc); + try info_line.addMessage(lang.err_alloc, config.error_bg, config.error_fg); }, .password => password.handle(&event, insert_mode) catch { - try info_line.addError(lang.err_alloc); + try info_line.addMessage(lang.err_alloc, config.error_bg, config.error_fg); }, } update = true; diff --git a/src/tui/components/InfoLine.zig b/src/tui/components/InfoLine.zig index 2a61af6..1c41474 100644 --- a/src/tui/components/InfoLine.zig +++ b/src/tui/components/InfoLine.zig @@ -2,69 +2,52 @@ const std = @import("std"); const utils = @import("../utils.zig"); const TerminalBuffer = @import("../TerminalBuffer.zig"); -const ArrayList = std.ArrayList; -const ErrorMessage = struct { width: u8, text: []const u8 }; - const InfoLine = @This(); -error_list: ArrayList(ErrorMessage), -error_bg: u16, -error_fg: u16, -text: []const u8, -width: u8, +const Message = struct { + width: u8, + text: []const u8, + bg: u16, + fg: u16, +}; +const MessageList = std.ArrayList(Message); + +messages: MessageList, pub fn init(allocator: std.mem.Allocator) InfoLine { return .{ - .error_list = ArrayList(ErrorMessage).init(allocator), - .error_bg = 0, - .error_fg = 258, - .text = "", - .width = 0, + .messages = MessageList.init(allocator), }; } pub fn deinit(self: InfoLine) void { - self.error_list.deinit(); + self.messages.deinit(); } -pub fn setText(self: *InfoLine, text: []const u8) !void { - self.width = if (text.len > 0) try utils.strWidth(text) else 0; - self.text = text; -} +pub fn addMessage(self: *InfoLine, text: []const u8, bg: u16, fg: u16) !void { + if (text.len == 0) return; -pub fn addError(self: *InfoLine, error_message: []const u8) !void { - if (error_message.len > 0) { - const entry = .{ - .width = try utils.strWidth(error_message), - .text = error_message, - }; - try self.error_list.append(entry); - } + try self.messages.append(.{ + .width = try utils.strWidth(text), + .text = text, + .bg = bg, + .fg = fg, + }); } pub fn draw(self: InfoLine, buffer: TerminalBuffer) !void { - var text: []const u8 = self.text; - var bg: u16 = buffer.bg; - var fg: u16 = buffer.fg; - var width: u8 = self.width; + if (self.messages.items.len == 0) return; - if (self.error_list.items.len > 0) { - const entry = self.error_list.getLast(); - text = entry.text; - bg = self.error_bg; - fg = self.error_fg; - width = entry.width; - } + const entry = self.messages.getLast(); + if (entry.width == 0 or buffer.box_width <= entry.width) return; - if (width > 0 and buffer.box_width > width) { - const label_y = buffer.box_y + buffer.margin_box_v; - const x = buffer.box_x + ((buffer.box_width - width) / 2); - TerminalBuffer.drawColorLabel(text, x, label_y, fg, bg); - } + const label_y = buffer.box_y + buffer.margin_box_v; + const x = buffer.box_x + ((buffer.box_width - entry.width) / 2); + TerminalBuffer.drawColorLabel(entry.text, x, label_y, entry.fg, entry.bg); } pub fn clearRendered(allocator: std.mem.Allocator, buffer: TerminalBuffer) !void { - // draw over the area + // Draw over the area const y = buffer.box_y + buffer.margin_box_v; const spaces = try allocator.alloc(u8, buffer.box_width); defer allocator.free(spaces); From ee488ba36e1a9b7915aa2b3bc0725d230062b332 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Mon, 29 Jul 2024 12:25:44 +0200 Subject: [PATCH 049/530] Revert "Redirect stderr to systemd journal in service (#621)" This reverts commit 3d8d8d67df7c8db9becc918e65113fd1b6394be5. Signed-off-by: AnErrupTion --- res/ly.service | 1 - 1 file changed, 1 deletion(-) diff --git a/res/ly.service b/res/ly.service index 09c1559..2fd120a 100644 --- a/res/ly.service +++ b/res/ly.service @@ -7,7 +7,6 @@ Conflicts=getty@tty2.service [Service] Type=idle ExecStart=/usr/bin/ly -StandardError=journal StandardInput=tty TTYPath=/dev/tty2 TTYReset=yes From 67fd024f6a52dde0ce4092dcb8453ccb2cbeaca5 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Mon, 29 Jul 2024 13:35:04 +0200 Subject: [PATCH 050/530] Revert "Redirect stderr to systemd journal in service (#621)" This reverts commit 3d8d8d67df7c8db9becc918e65113fd1b6394be5. Signed-off-by: AnErrupTion --- res/ly.service | 1 - 1 file changed, 1 deletion(-) diff --git a/res/ly.service b/res/ly.service index 09c1559..2fd120a 100644 --- a/res/ly.service +++ b/res/ly.service @@ -7,7 +7,6 @@ Conflicts=getty@tty2.service [Service] Type=idle ExecStart=/usr/bin/ly -StandardError=journal StandardInput=tty TTYPath=/dev/tty2 TTYReset=yes From c1f1c8f5c1372165d969114b8c645271c98098b3 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Mon, 29 Jul 2024 13:46:14 +0200 Subject: [PATCH 051/530] Use usize instead of u64 in most places for better 32-bit compatibility Signed-off-by: AnErrupTion --- src/animations/Doom.zig | 2 +- src/animations/Matrix.zig | 20 ++++++++++---------- src/bigclock.zig | 2 +- src/config/Save.zig | 2 +- src/config/migrator.zig | 4 ++-- src/interop.zig | 4 ++-- src/main.zig | 8 ++++---- src/tui/TerminalBuffer.zig | 30 +++++++++++++++--------------- src/tui/components/Desktop.zig | 12 ++++++------ src/tui/components/Text.zig | 16 ++++++++-------- 10 files changed, 50 insertions(+), 50 deletions(-) diff --git a/src/animations/Doom.zig b/src/animations/Doom.zig index 1583165..daa203a 100644 --- a/src/animations/Doom.zig +++ b/src/animations/Doom.zig @@ -74,7 +74,7 @@ pub fn draw(self: Doom) void { } } -fn initBuffer(buffer: []u8, width: u64) void { +fn initBuffer(buffer: []u8, width: usize) void { const length = buffer.len - width; const slice_start = buffer[0..length]; const slice_end = buffer[length..]; diff --git a/src/animations/Matrix.zig b/src/animations/Matrix.zig index 9ed242e..ba7e6e1 100644 --- a/src/animations/Matrix.zig +++ b/src/animations/Matrix.zig @@ -77,9 +77,9 @@ pub fn draw(self: *Matrix) void { if (self.frame > 4) self.frame = 1; self.count = 0; - var x: u64 = 0; + var x: usize = 0; while (x < self.terminal_buffer.width) : (x += 2) { - var tail: u64 = 0; + var tail: usize = 0; var line = &self.lines[x]; if (self.frame <= line.update) continue; @@ -95,7 +95,7 @@ pub fn draw(self: *Matrix) void { } } - var y: u64 = 0; + var y: usize = 0; var first_col = true; var seg_len: u64 = 0; height_it: while (y <= buf_height) : (y += 1) { @@ -142,13 +142,13 @@ pub fn draw(self: *Matrix) void { } } - var x: u64 = 0; + var x: usize = 0; while (x < buf_width) : (x += 2) { - var y: u64 = 1; + var y: usize = 1; while (y <= self.terminal_buffer.height) : (y += 1) { const dot = self.dots[buf_width * y + x]; - var fg: u16 = self.fg_ini; + var fg = self.fg_ini; if (dot.value == -1 or dot.value == ' ') { _ = termbox.tb_set_cell(@intCast(x), @intCast(y - 1), ' ', fg, termbox.TB_DEFAULT); @@ -161,16 +161,16 @@ pub fn draw(self: *Matrix) void { } } -fn initBuffers(dots: []Dot, lines: []Line, width: u64, height: u64, random: Random) void { - var y: u64 = 0; +fn initBuffers(dots: []Dot, lines: []Line, width: usize, height: usize, random: Random) void { + var y: usize = 0; while (y <= height) : (y += 1) { - var x: u64 = 0; + var x: usize = 0; while (x < width) : (x += 2) { dots[y * width + x].value = -1; } } - var x: u64 = 0; + var x: usize = 0; while (x < width) : (x += 2) { var line = lines[x]; const h: isize = @intCast(height); diff --git a/src/bigclock.zig b/src/bigclock.zig index 9054a75..cfe61f5 100644 --- a/src/bigclock.zig +++ b/src/bigclock.zig @@ -111,7 +111,7 @@ pub fn clockCell(animate: bool, char: u8, fg: u16, bg: u16) [SIZE]termbox.tb_cel return cells; } -pub fn alphaBlit(buffer: [*]termbox.tb_cell, x: u64, y: u64, tb_width: u64, tb_height: u64, cells: [SIZE]termbox.tb_cell) void { +pub fn alphaBlit(buffer: [*]termbox.tb_cell, x: usize, y: usize, tb_width: usize, tb_height: usize, cells: [SIZE]termbox.tb_cell) void { if (x + WIDTH >= tb_width or y + HEIGHT >= tb_height) return; for (0..HEIGHT) |yy| { diff --git a/src/config/Save.zig b/src/config/Save.zig index 972c2b7..6186f14 100644 --- a/src/config/Save.zig +++ b/src/config/Save.zig @@ -1,2 +1,2 @@ user: ?[]const u8 = null, -session_index: ?u64 = null, +session_index: ?usize = null, diff --git a/src/config/migrator.zig b/src/config/migrator.zig index 0ae6001..1295fa3 100644 --- a/src/config/migrator.zig +++ b/src/config/migrator.zig @@ -20,9 +20,9 @@ pub fn tryMigrateSaveFile(user_buf: *[32]u8, path: []const u8) Save { reader.streamUntilDelimiter(session_fbs.writer(), '\n', 20) catch {}; const session_index_str = session_fbs.getWritten(); - var session_index: ?u64 = null; + var session_index: ?usize = null; if (session_index_str.len > 0) { - session_index = std.fmt.parseUnsigned(u64, session_index_str, 10) catch return save; + session_index = std.fmt.parseUnsigned(usize, session_index_str, 10) catch return save; } save.session_index = session_index; diff --git a/src/interop.zig b/src/interop.zig index 23a90b9..23bb83e 100644 --- a/src/interop.zig +++ b/src/interop.zig @@ -26,10 +26,10 @@ pub const logincap = @cImport({ @cInclude("login_cap.h"); }); -pub const c_size = u64; +pub const c_size = usize; pub const c_uid = u32; pub const c_gid = u32; -pub const c_time = c_long; +pub const c_time = c_longlong; pub const tm = extern struct { tm_sec: c_int, tm_min: c_int, diff --git a/src/main.zig b/src/main.zig index 42e617f..d10437d 100644 --- a/src/main.zig +++ b/src/main.zig @@ -299,8 +299,8 @@ pub fn main() !void { _ = termbox.tb_present(); // Required to update tb_width(), tb_height() and tb_cell_buffer() - const width: u64 = @intCast(termbox.tb_width()); - const height: u64 = @intCast(termbox.tb_height()); + const width: usize = @intCast(termbox.tb_width()); + const height: usize = @intCast(termbox.tb_height()); if (width != buffer.width) { buffer.width = width; @@ -398,7 +398,7 @@ pub fn main() !void { try info_line.draw(buffer); if (!config.hide_key_hints) { - var length: u64 = 0; + var length: usize = 0; buffer.drawLabel(config.shutdown_key, length, 0); length += config.shutdown_key.len + 1; @@ -453,7 +453,7 @@ pub fn main() !void { }; var lock_state_x = buffer.width - @min(buffer.width, lang.numlock.len); - const lock_state_y: u64 = if (config.clock != null) 1 else 0; + const lock_state_y: usize = if (config.clock != null) 1 else 0; if (lock_state.numlock) buffer.drawLabel(lang.numlock, lock_state_x, lock_state_y); diff --git a/src/tui/TerminalBuffer.zig b/src/tui/TerminalBuffer.zig index ebc8ca7..71bf8aa 100644 --- a/src/tui/TerminalBuffer.zig +++ b/src/tui/TerminalBuffer.zig @@ -11,8 +11,8 @@ const termbox = interop.termbox; const TerminalBuffer = @This(); random: Random, -width: u64, -height: u64, +width: usize, +height: usize, buffer: [*]termbox.tb_cell, fg: u16, bg: u16, @@ -27,15 +27,15 @@ box_chars: struct { left: u32, right: u32, }, -labels_max_length: u64, -box_x: u64, -box_y: u64, -box_width: u64, -box_height: u64, +labels_max_length: usize, +box_x: usize, +box_y: usize, +box_width: usize, +box_height: usize, margin_box_v: u8, margin_box_h: u8, -pub fn init(config: Config, labels_max_length: u64, random: Random) TerminalBuffer { +pub fn init(config: Config, labels_max_length: usize, random: Random) TerminalBuffer { return .{ .random = random, .width = @intCast(termbox.tb_width()), @@ -142,9 +142,9 @@ pub fn drawBoxCenter(self: *TerminalBuffer, show_borders: bool, blank_box: bool) } pub fn calculateComponentCoordinates(self: TerminalBuffer) struct { - x: u64, - y: u64, - visible_length: u64, + x: usize, + y: usize, + visible_length: usize, } { const x = self.box_x + self.margin_box_h + self.labels_max_length + 1; const y = self.box_y + self.margin_box_v; @@ -157,11 +157,11 @@ pub fn calculateComponentCoordinates(self: TerminalBuffer) struct { }; } -pub fn drawLabel(self: TerminalBuffer, text: []const u8, x: u64, y: u64) void { +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: u64, y: u64, fg: u16, bg: u16) void { +pub fn drawColorLabel(text: []const u8, x: usize, y: usize, fg: u16, bg: u16) void { const yc: c_int = @intCast(y); const utf8view = std.unicode.Utf8View.init(text) catch return; var utf8 = utf8view.iterator(); @@ -172,7 +172,7 @@ pub fn drawColorLabel(text: []const u8, x: u64, y: u64, fg: u16, bg: u16) void { } } -pub fn drawConfinedLabel(self: TerminalBuffer, text: []const u8, x: u64, y: u64, max_length: u64) void { +pub fn drawConfinedLabel(self: TerminalBuffer, text: []const u8, x: usize, y: usize, max_length: usize) void { const yc: c_int = @intCast(y); const utf8view = std.unicode.Utf8View.init(text) catch return; var utf8 = utf8view.iterator(); @@ -184,7 +184,7 @@ pub fn drawConfinedLabel(self: TerminalBuffer, text: []const u8, x: u64, y: u64, } } -pub fn drawCharMultiple(self: TerminalBuffer, char: u8, x: u64, y: u64, length: u64) void { +pub fn drawCharMultiple(self: TerminalBuffer, char: u8, x: usize, y: usize, length: usize) void { const yc: c_int = @intCast(y); const cell = utils.initCell(char, self.fg, self.bg); diff --git a/src/tui/components/Desktop.zig b/src/tui/components/Desktop.zig index 98870d0..9151f97 100644 --- a/src/tui/components/Desktop.zig +++ b/src/tui/components/Desktop.zig @@ -35,13 +35,13 @@ pub const Entry = struct { @"Desktop Entry": DesktopEntry = DesktopEntry{} }; allocator: Allocator, buffer: *TerminalBuffer, environments: EnvironmentList, -current: u64, -visible_length: u64, -x: u64, -y: u64, +current: usize, +visible_length: usize, +x: usize, +y: usize, lang: Lang, -pub fn init(allocator: Allocator, buffer: *TerminalBuffer, max_length: u64, lang: Lang) !Desktop { +pub fn init(allocator: Allocator, buffer: *TerminalBuffer, max_length: usize, lang: Lang) !Desktop { return .{ .allocator = allocator, .buffer = buffer, @@ -64,7 +64,7 @@ pub fn deinit(self: Desktop) void { self.environments.deinit(); } -pub fn position(self: *Desktop, x: u64, y: u64, visible_length: u64) void { +pub fn position(self: *Desktop, x: usize, y: usize, visible_length: usize) void { self.x = x; self.y = y; self.visible_length = visible_length; diff --git a/src/tui/components/Text.zig b/src/tui/components/Text.zig index 412476a..e65e4a6 100644 --- a/src/tui/components/Text.zig +++ b/src/tui/components/Text.zig @@ -13,14 +13,14 @@ const Text = @This(); allocator: Allocator, buffer: *TerminalBuffer, text: DynamicString, -end: u64, -cursor: u64, -visible_start: u64, -visible_length: u64, -x: u64, -y: u64, +end: usize, +cursor: usize, +visible_start: usize, +visible_length: usize, +x: usize, +y: usize, -pub fn init(allocator: Allocator, buffer: *TerminalBuffer, max_length: u64) !Text { +pub fn init(allocator: Allocator, buffer: *TerminalBuffer, max_length: usize) !Text { const text = try DynamicString.initCapacity(allocator, max_length); return .{ @@ -40,7 +40,7 @@ pub fn deinit(self: Text) void { self.text.deinit(); } -pub fn position(self: *Text, x: u64, y: u64, visible_length: u64) void { +pub fn position(self: *Text, x: usize, y: usize, visible_length: usize) void { self.x = x; self.y = y; self.visible_length = visible_length; From f646dddd0266004704f394b725f4145524962ef9 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Mon, 29 Jul 2024 14:18:23 +0200 Subject: [PATCH 052/530] Fix clock & bigclock not updating without input 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 d10437d..631533c 100644 --- a/src/main.zig +++ b/src/main.zig @@ -466,8 +466,6 @@ pub fn main() !void { desktop.draw(); login.draw(); password.drawMasked(config.asterisk); - - update = animate; } else { std.time.sleep(std.time.ns_per_ms * 10); update = buffer.cascade(); @@ -500,6 +498,8 @@ pub fn main() !void { const event_error = if (timeout == -1) termbox.tb_poll_event(&event) else termbox.tb_peek_event(&event, timeout); + update = timeout != -1; + if (event_error < 0 or event.type != termbox.TB_EVENT_KEY) continue; switch (event.key) { From 9374d2df32d9d964da43cd02bc41dcc3097e6a46 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Mon, 29 Jul 2024 14:19:47 +0200 Subject: [PATCH 053/530] Backport: Fix clock & bigclock not updating without input 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 29a4a8c..1a692de 100644 --- a/src/main.zig +++ b/src/main.zig @@ -433,8 +433,6 @@ pub fn main() !void { desktop.draw(); login.draw(); password.drawMasked(config.asterisk); - - update = animate; } else { std.time.sleep(std.time.ns_per_ms * 10); update = buffer.cascade(); @@ -467,6 +465,8 @@ pub fn main() !void { const event_error = if (timeout == -1) termbox.tb_poll_event(&event) else termbox.tb_peek_event(&event, timeout); + update = timeout != -1; + if (event_error < 0 or event.type != termbox.TB_EVENT_KEY) continue; switch (event.key) { From 48185bdfe0bfcdcedc39366ca0e7f31acf92d867 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Mon, 29 Jul 2024 14:44:56 +0200 Subject: [PATCH 054/530] More verbose output in build.zig Signed-off-by: AnErrupTion --- build.zig | 155 ++++++++++++++++++++++++++++++++---------------------- 1 file changed, 91 insertions(+), 64 deletions(-) diff --git a/build.zig b/build.zig index bbd538a..0acb400 100644 --- a/build.zig +++ b/build.zig @@ -133,6 +133,7 @@ const InitSystem = enum { S6, Dinit, }; + pub fn ServiceInstaller(comptime init_system: InitSystem) type { return struct { pub fn make(step: *std.Build.Step, _: ProgressNode) !void { @@ -144,7 +145,7 @@ pub fn ServiceInstaller(comptime init_system: InitSystem) type { var service_dir = std.fs.cwd().openDir(service_path, .{}) catch unreachable; defer service_dir.close(); - try std.fs.cwd().copyFile("res/ly.service", service_dir, "ly.service", .{ .override_mode = 0o644 }); + try installFile("res/ly.service", service_dir, service_path, "ly.service", .{ .override_mode = 0o644 }); }, .Openrc => { const service_path = try std.fs.path.join(allocator, &[_][]const u8{ dest_directory, "/etc/init.d" }); @@ -152,7 +153,7 @@ pub fn ServiceInstaller(comptime init_system: InitSystem) type { var service_dir = std.fs.cwd().openDir(service_path, .{}) catch unreachable; defer service_dir.close(); - try std.fs.cwd().copyFile("res/ly-openrc", service_dir, exe_name, .{ .override_mode = 0o755 }); + try installFile("res/ly-openrc", service_dir, service_path, exe_name, .{ .override_mode = 0o755 }); }, .Runit => { const service_path = try std.fs.path.join(allocator, &[_][]const u8{ dest_directory, "/etc/sv/ly" }); @@ -162,10 +163,12 @@ pub fn ServiceInstaller(comptime init_system: InitSystem) type { const supervise_path = try std.fs.path.join(allocator, &[_][]const u8{ service_path, "supervise" }); - try std.fs.cwd().copyFile("res/ly-runit-service/conf", service_dir, "conf", .{}); - try std.fs.cwd().copyFile("res/ly-runit-service/finish", service_dir, "finish", .{ .override_mode = 0o755 }); - try std.fs.cwd().copyFile("res/ly-runit-service/run", service_dir, "run", .{ .override_mode = 0o755 }); + try installFile("res/ly-runit-service/conf", service_dir, service_path, "conf", .{}); + try installFile("res/ly-runit-service/finish", service_dir, service_path, "finish", .{ .override_mode = 0o755 }); + try installFile("res/ly-runit-service/run", service_dir, service_path, "run", .{ .override_mode = 0o755 }); + try std.fs.cwd().symLink("/run/runit/supervise.ly", supervise_path, .{}); + 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, "/etc/s6/adminsv/default/contents.d" }); @@ -181,8 +184,8 @@ pub fn ServiceInstaller(comptime init_system: InitSystem) type { var service_dir = std.fs.cwd().openDir(service_path, .{}) catch unreachable; defer service_dir.close(); - try std.fs.cwd().copyFile("res/ly-s6/run", service_dir, "run", .{ .override_mode = 0o755 }); - try std.fs.cwd().copyFile("res/ly-s6/type", service_dir, "type", .{}); + try installFile("res/ly-s6/run", service_dir, service_path, "run", .{ .override_mode = 0o755 }); + try installFile("res/ly-s6/type", service_dir, service_path, "type", .{}); }, .Dinit => { const service_path = try std.fs.path.join(allocator, &[_][]const u8{ dest_directory, "/etc/dinit.d" }); @@ -190,7 +193,7 @@ pub fn ServiceInstaller(comptime init_system: InitSystem) type { var service_dir = std.fs.cwd().openDir(service_path, .{}) catch unreachable; defer service_dir.close(); - try std.fs.cwd().copyFile("res/ly-dinit", service_dir, "ly", .{}); + try installFile("res/ly-dinit", service_dir, service_path, "ly", .{}); }, } } @@ -207,8 +210,6 @@ fn install_ly(allocator: std.mem.Allocator, install_config: bool) !void { std.debug.print("warn: {s} already exists as a directory.\n", .{data_directory}); }; - var current_dir = std.fs.cwd(); - { const exe_path = try std.fs.path.join(allocator, &[_][]const u8{ dest_directory, "/usr/bin" }); if (!std.mem.eql(u8, dest_directory, "")) { @@ -220,7 +221,7 @@ fn install_ly(allocator: std.mem.Allocator, install_config: bool) !void { var executable_dir = std.fs.cwd().openDir(exe_path, .{}) catch unreachable; defer executable_dir.close(); - try current_dir.copyFile("zig-out/bin/ly", executable_dir, exe_name, .{}); + try installFile("zig-out/bin/ly", executable_dir, exe_path, exe_name, .{}); } { @@ -228,32 +229,32 @@ fn install_ly(allocator: std.mem.Allocator, install_config: bool) !void { defer config_dir.close(); if (install_config) { - try current_dir.copyFile("res/config.ini", config_dir, "config.ini", .{}); + try installFile("res/config.ini", config_dir, data_directory, "config.ini", .{}); } - try current_dir.copyFile("res/xsetup.sh", config_dir, "xsetup.sh", .{}); - try current_dir.copyFile("res/wsetup.sh", config_dir, "wsetup.sh", .{}); + try installFile("res/xsetup.sh", config_dir, data_directory, "xsetup.sh", .{}); + try installFile("res/wsetup.sh", config_dir, data_directory, "wsetup.sh", .{}); } { var lang_dir = std.fs.cwd().openDir(lang_path, .{}) catch unreachable; defer lang_dir.close(); - try current_dir.copyFile("res/lang/cat.ini", lang_dir, "cat.ini", .{}); - try current_dir.copyFile("res/lang/cs.ini", lang_dir, "cs.ini", .{}); - try current_dir.copyFile("res/lang/de.ini", lang_dir, "de.ini", .{}); - try current_dir.copyFile("res/lang/en.ini", lang_dir, "en.ini", .{}); - try current_dir.copyFile("res/lang/es.ini", lang_dir, "es.ini", .{}); - try current_dir.copyFile("res/lang/fr.ini", lang_dir, "fr.ini", .{}); - try current_dir.copyFile("res/lang/it.ini", lang_dir, "it.ini", .{}); - try current_dir.copyFile("res/lang/pl.ini", lang_dir, "pl.ini", .{}); - try current_dir.copyFile("res/lang/pt.ini", lang_dir, "pt.ini", .{}); - try current_dir.copyFile("res/lang/pt_BR.ini", lang_dir, "pt_BR.ini", .{}); - try current_dir.copyFile("res/lang/ro.ini", lang_dir, "ro.ini", .{}); - try current_dir.copyFile("res/lang/ru.ini", lang_dir, "ru.ini", .{}); - try current_dir.copyFile("res/lang/sr.ini", lang_dir, "sr.ini", .{}); - try current_dir.copyFile("res/lang/sv.ini", lang_dir, "sv.ini", .{}); - try current_dir.copyFile("res/lang/tr.ini", lang_dir, "tr.ini", .{}); - try current_dir.copyFile("res/lang/uk.ini", lang_dir, "uk.ini", .{}); + try installFile("res/lang/cat.ini", lang_dir, lang_path, "cat.ini", .{}); + try installFile("res/lang/cs.ini", lang_dir, lang_path, "cs.ini", .{}); + try installFile("res/lang/de.ini", lang_dir, lang_path, "de.ini", .{}); + try installFile("res/lang/en.ini", lang_dir, lang_path, "en.ini", .{}); + try installFile("res/lang/es.ini", lang_dir, lang_path, "es.ini", .{}); + try installFile("res/lang/fr.ini", lang_dir, lang_path, "fr.ini", .{}); + try installFile("res/lang/it.ini", lang_dir, lang_path, "it.ini", .{}); + try installFile("res/lang/pl.ini", lang_dir, lang_path, "pl.ini", .{}); + try installFile("res/lang/pt.ini", lang_dir, lang_path, "pt.ini", .{}); + try installFile("res/lang/pt_BR.ini", lang_dir, lang_path, "pt_BR.ini", .{}); + try installFile("res/lang/ro.ini", lang_dir, lang_path, "ro.ini", .{}); + try installFile("res/lang/ru.ini", lang_dir, lang_path, "ru.ini", .{}); + try installFile("res/lang/sr.ini", lang_dir, lang_path, "sr.ini", .{}); + try installFile("res/lang/sv.ini", lang_dir, lang_path, "sv.ini", .{}); + try installFile("res/lang/tr.ini", lang_dir, lang_path, "tr.ini", .{}); + try installFile("res/lang/uk.ini", lang_dir, lang_path, "uk.ini", .{}); } { @@ -267,49 +268,32 @@ fn install_ly(allocator: std.mem.Allocator, install_config: bool) !void { var pam_dir = std.fs.cwd().openDir(pam_path, .{}) catch unreachable; defer pam_dir.close(); - try current_dir.copyFile("res/pam.d/ly", pam_dir, "ly", .{ .override_mode = 0o644 }); + try installFile("res/pam.d/ly", pam_dir, pam_path, "ly", .{ .override_mode = 0o644 }); } } pub fn uninstallall(step: *std.Build.Step, _: ProgressNode) !void { - try std.fs.cwd().deleteTree(data_directory); + std.fs.cwd().deleteTree(data_directory) catch { + std.debug.print("warn: ly data directory not found.", .{}); + }; + const allocator = step.owner.allocator; const exe_path = try std.fs.path.join(allocator, &[_][]const u8{ dest_directory, "/usr/bin/", exe_name }); - try std.fs.cwd().deleteFile(exe_path); - - const pam_path = try std.fs.path.join(allocator, &[_][]const u8{ dest_directory, "/etc/pam.d/ly" }); - try std.fs.cwd().deleteFile(pam_path); - - const systemd_service_path = try std.fs.path.join(allocator, &[_][]const u8{ dest_directory, "/usr/lib/systemd/system/ly.service" }); - std.fs.cwd().deleteFile(systemd_service_path) catch { - std.debug.print("warn: systemd service not found.\n", .{}); + var success = true; + std.fs.cwd().deleteFile(exe_path) catch { + std.debug.print("warn: ly executable not found.", .{}); + success = false; }; + if (success) std.debug.print("info: deleted {s}\n", .{exe_path}); - const openrc_service_path = try std.fs.path.join(allocator, &[_][]const u8{ dest_directory, "/etc/init.d/ly" }); - std.fs.cwd().deleteFile(openrc_service_path) catch { - std.debug.print("warn: openrc service not found.\n", .{}); - }; - - const runit_service_path = try std.fs.path.join(allocator, &[_][]const u8{ dest_directory, "/etc/sv/ly" }); - std.fs.cwd().deleteTree(runit_service_path) catch { - std.debug.print("warn: runit service not found.\n", .{}); - }; - - const s6_service_path = try std.fs.path.join(allocator, &[_][]const u8{ dest_directory, "/etc/s6/sv/ly-srv" }); - std.fs.cwd().deleteTree(s6_service_path) catch { - std.debug.print("warn: s6 service not found.\n", .{}); - }; - - const s6_admin_service_path = try std.fs.path.join(allocator, &[_][]const u8{ dest_directory, "/etc/s6/adminsv/default/contents.d/ly-srv" }); - std.fs.cwd().deleteFile(s6_admin_service_path) catch { - std.debug.print("warn: s6 admin service not found.\n", .{}); - }; - - const dinit_service_path = try std.fs.path.join(allocator, &[_][]const u8{ dest_directory, "/etc/dinit.d/ly" }); - std.fs.cwd().deleteFile(dinit_service_path) catch { - std.debug.print("warn: dinit service not found.\n", .{}); - }; + try deleteFile(allocator, "/etc/pam.d/ly", "ly pam file not found"); + try deleteFile(allocator, "/usr/lib/systemd/system/ly.service", "systemd service not found"); + try deleteFile(allocator, "/etc/init.d/ly", "openrc service not found"); + try deleteTree(allocator, "/etc/sv/ly", "runit service not found"); + try deleteTree(allocator, "/etc/s6/sv/ly-srv", "s6 service not found"); + try deleteFile(allocator, "/etc/s6/adminsv/default/contents.d/ly-srv", "s6 admin service not found"); + try deleteFile(allocator, "/etc/dinit.d/ly", "dinit service not found"); } fn getVersionStr(b: *std.Build, name: []const u8, version: std.SemanticVersion) ![]const u8 { @@ -366,3 +350,46 @@ fn getVersionStr(b: *std.Build, name: []const u8, version: std.SemanticVersion) }, } } + +fn installFile( + source_file: []const u8, + destination_directory: std.fs.Dir, + destination_directory_path: []const u8, + destination_file: []const u8, + options: std.fs.Dir.CopyFileOptions, +) !void { + try std.fs.cwd().copyFile(source_file, destination_directory, destination_file, options); + std.debug.print("info: installed {s}/{s}\n", .{ destination_directory_path, destination_file }); +} + +fn deleteFile( + allocator: std.mem.Allocator, + file: []const u8, + warning: []const u8, +) !void { + const path = try std.fs.path.join(allocator, &[_][]const u8{ dest_directory, file }); + var success = true; + + std.fs.cwd().deleteFile(path) catch { + std.debug.print("warn: {s}\n", .{warning}); + success = false; + }; + + if (success) std.debug.print("info: deleted {s}\n", .{path}); +} + +fn deleteTree( + allocator: std.mem.Allocator, + directory: []const u8, + warning: []const u8, +) !void { + const path = try std.fs.path.join(allocator, &[_][]const u8{ dest_directory, directory }); + var success = true; + + std.fs.cwd().deleteTree(path) catch { + std.debug.print("warn: {s}\n", .{warning}); + success = false; + }; + + if (success) std.debug.print("info: deleted {s}\n", .{path}); +} From 5f2f21620a68f5fd9bdee10b51b33a9a8c7f6921 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Tue, 30 Jul 2024 09:43:56 +0200 Subject: [PATCH 055/530] Update zigini (fixes incorrect comment parsing) Signed-off-by: AnErrupTion --- build.zig.zon | 4 ++-- src/config/migrator.zig | 12 ++++++++++++ src/main.zig | 19 ++++++++++--------- src/tui/components/Desktop.zig | 2 +- 4 files changed, 25 insertions(+), 12 deletions(-) diff --git a/build.zig.zon b/build.zig.zon index 23ac4e4..5bd9d66 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -8,8 +8,8 @@ .hash = "122062d301a203d003547b414237229b09a7980095061697349f8bef41be9c30266b", }, .zigini = .{ - .url = "https://github.com/Kawaii-Ash/zigini/archive/refs/tags/0.2.2.tar.gz", - .hash = "1220afda2f3258cd0bb042dd3c2d5a35069ce1785c11325e65f136c5220013b36d00", + .url = "https://github.com/Kawaii-Ash/zigini/archive/bdb6fd15c6dcedb0c6c2a46381f2d298e2f05fff.tar.gz", + .hash = "12203feb831e21bec081af6aae70dd19b127f1627aa55f3415bd1fa476c174a511cc", }, }, .paths = .{""}, diff --git a/src/config/migrator.zig b/src/config/migrator.zig index 1295fa3..d8a732e 100644 --- a/src/config/migrator.zig +++ b/src/config/migrator.zig @@ -1,7 +1,19 @@ +// The migrator ensures compatibility with <=0.6.0 configuration files + const std = @import("std"); const ini = @import("zigini"); const Save = @import("Save.zig"); +pub fn configFieldHandler(_: std.mem.Allocator, field: ini.IniField) ?ini.IniField { + var mapped_field = field; + + if (std.mem.eql(u8, field.key, "blank_password")) { + mapped_field.key = "clear_password"; + } + + return mapped_field; +} + pub fn tryMigrateSaveFile(user_buf: *[32]u8, path: []const u8) Save { var save = Save{}; diff --git a/src/main.zig b/src/main.zig index 631533c..9615fee 100644 --- a/src/main.zig +++ b/src/main.zig @@ -92,8 +92,7 @@ pub fn main() !void { if (save_path_alloc) allocator.free(save_path); } - // Compatibility with v0.6.0 - const mapped_config_fields = .{.{ "blank_password", "clear_password" }}; + const comment_characters = "#"; if (res.args.config) |s| { const trailing_slash = if (s[s.len - 1] != '/') "/" else ""; @@ -101,7 +100,7 @@ pub fn main() !void { const config_path = try std.fmt.allocPrint(allocator, "{s}{s}config.ini", .{ s, trailing_slash }); defer allocator.free(config_path); - config = config_ini.readFileToStructWithMap(config_path, mapped_config_fields) catch _config: { + config = config_ini.readFileToStruct(config_path, comment_characters, migrator.configFieldHandler) catch _config: { // We're using a literal error message here since the language file hasn't yet been loaded try info_line.addMessage("unable to parse config file", @intCast(interop.termbox.TB_DEFAULT), @intCast(interop.termbox.TB_RED | interop.termbox.TB_BOLD)); break :_config Config{}; @@ -110,17 +109,19 @@ pub fn main() !void { 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) catch Lang{}; + lang = lang_ini.readFileToStruct(lang_path, comment_characters, null) catch Lang{}; if (config.load) { save_path = try std.fmt.allocPrint(allocator, "{s}{s}save.ini", .{ s, trailing_slash }); save_path_alloc = true; var user_buf: [32]u8 = undefined; - save = save_ini.readFileToStruct(save_path) catch migrator.tryMigrateSaveFile(&user_buf, config.save_file); + save = save_ini.readFileToStruct(save_path, comment_characters, null) catch migrator.tryMigrateSaveFile(&user_buf, config.save_file); } } else { - config = config_ini.readFileToStructWithMap(build_options.data_directory ++ "/config.ini", mapped_config_fields) catch _config: { + const config_path = build_options.data_directory ++ "/config.ini"; + + config = config_ini.readFileToStruct(config_path, comment_characters, migrator.configFieldHandler) catch _config: { // literal error message, due to language file not yet available try info_line.addMessage("unable to parse config file", @intCast(interop.termbox.TB_DEFAULT), @intCast(interop.termbox.TB_RED | interop.termbox.TB_BOLD)); break :_config Config{}; @@ -129,11 +130,11 @@ pub fn main() !void { const lang_path = try std.fmt.allocPrint(allocator, "{s}/lang/{s}.ini", .{ build_options.data_directory, config.lang }); defer allocator.free(lang_path); - lang = lang_ini.readFileToStruct(lang_path) catch Lang{}; + lang = lang_ini.readFileToStruct(lang_path, comment_characters, null) catch Lang{}; if (config.load) { var user_buf: [32]u8 = undefined; - save = save_ini.readFileToStruct(save_path) catch migrator.tryMigrateSaveFile(&user_buf, config.save_file); + save = save_ini.readFileToStruct(save_path, comment_characters, null) catch migrator.tryMigrateSaveFile(&user_buf, config.save_file); } } @@ -590,7 +591,7 @@ pub fn main() !void { .user = login.text.items, .session_index = desktop.current, }; - ini.writeFromStruct(save_data, file.writer(), null) catch break :save_last_settings; + ini.writeFromStruct(save_data, file.writer(), null, true, .{}) catch break :save_last_settings; } var shared_err = try SharedError.init(); diff --git a/src/tui/components/Desktop.zig b/src/tui/components/Desktop.zig index 9151f97..9c0fdf1 100644 --- a/src/tui/components/Desktop.zig +++ b/src/tui/components/Desktop.zig @@ -150,7 +150,7 @@ pub fn crawl(self: *Desktop, path: []const u8, display_server: DisplayServer) !v const entry_path = try std.fmt.allocPrint(self.allocator, "{s}/{s}", .{ path, item.name }); defer self.allocator.free(entry_path); var entry_ini = Ini(Entry).init(self.allocator); - _ = try entry_ini.readFileToStruct(entry_path); + _ = try entry_ini.readFileToStruct(entry_path, "#", null); errdefer entry_ini.deinit(); var xdg_session_desktop: []const u8 = undefined; From 75cc971f9c7596349d79b6cd138afda30269c19a Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Tue, 30 Jul 2024 09:51:42 +0200 Subject: [PATCH 056/530] Backport: Update zigini (fixes incorrect comment parsing) Signed-off-by: AnErrupTion --- build.zig.zon | 4 ++-- src/config/migrator.zig | 12 ++++++++++++ src/main.zig | 19 ++++++++++--------- src/tui/components/Desktop.zig | 2 +- 4 files changed, 25 insertions(+), 12 deletions(-) diff --git a/build.zig.zon b/build.zig.zon index 85900c9..ba6345e 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -8,8 +8,8 @@ .hash = "122014e73fd712190e109950837b97f6143f02d7e2b6986e1db70b6f4aadb5ba6a0d", }, .zigini = .{ - .url = "https://github.com/Kawaii-Ash/zigini/archive/ce1f322482099db058f5d9fdd05fbfa255d79723.tar.gz", - .hash = "1220e7a99793a0430e0a7c0b938cb3c98321035bc297e21cd0e2413cf740b4923b9f", + .url = "https://github.com/Kawaii-Ash/zigini/archive/bdb6fd15c6dcedb0c6c2a46381f2d298e2f05fff.tar.gz", + .hash = "12203feb831e21bec081af6aae70dd19b127f1627aa55f3415bd1fa476c174a511cc", }, }, .paths = .{""}, diff --git a/src/config/migrator.zig b/src/config/migrator.zig index 0ae6001..377ee1a 100644 --- a/src/config/migrator.zig +++ b/src/config/migrator.zig @@ -1,7 +1,19 @@ +// The migrator ensures compatibility with <=0.6.0 configuration files + const std = @import("std"); const ini = @import("zigini"); const Save = @import("Save.zig"); +pub fn configFieldHandler(_: std.mem.Allocator, field: ini.IniField) ?ini.IniField { + var mapped_field = field; + + if (std.mem.eql(u8, field.key, "blank_password")) { + mapped_field.key = "clear_password"; + } + + return mapped_field; +} + pub fn tryMigrateSaveFile(user_buf: *[32]u8, path: []const u8) Save { var save = Save{}; diff --git a/src/main.zig b/src/main.zig index 1a692de..b0e9a96 100644 --- a/src/main.zig +++ b/src/main.zig @@ -90,8 +90,7 @@ pub fn main() !void { if (save_path_alloc) allocator.free(save_path); } - // Compatibility with v0.6.0 - const mapped_config_fields = .{.{ "blank_password", "clear_password" }}; + const comment_characters = "#"; if (res.args.config) |s| { const trailing_slash = if (s[s.len - 1] != '/') "/" else ""; @@ -99,31 +98,33 @@ pub fn main() !void { const config_path = try std.fmt.allocPrint(allocator, "{s}{s}config.ini", .{ s, trailing_slash }); defer allocator.free(config_path); - config = config_ini.readFileToStructWithMap(config_path, mapped_config_fields) catch Config{}; + config = config_ini.readFileToStruct(config_path, comment_characters, migrator.configFieldHandler) catch 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) catch Lang{}; + lang = lang_ini.readFileToStruct(lang_path, comment_characters, null) catch Lang{}; if (config.load) { save_path = try std.fmt.allocPrint(allocator, "{s}{s}save.ini", .{ s, trailing_slash }); save_path_alloc = true; var user_buf: [32]u8 = undefined; - save = save_ini.readFileToStruct(save_path) catch migrator.tryMigrateSaveFile(&user_buf, config.save_file); + save = save_ini.readFileToStruct(save_path, comment_characters, null) catch migrator.tryMigrateSaveFile(&user_buf, config.save_file); } } else { - config = config_ini.readFileToStructWithMap(build_options.data_directory ++ "/config.ini", mapped_config_fields) catch Config{}; + const config_path = build_options.data_directory ++ "/config.ini"; + + config = config_ini.readFileToStruct(config_path, comment_characters, migrator.configFieldHandler) catch Config{}; const lang_path = try std.fmt.allocPrint(allocator, "{s}/lang/{s}.ini", .{ build_options.data_directory, config.lang }); defer allocator.free(lang_path); - lang = lang_ini.readFileToStruct(lang_path) catch Lang{}; + lang = lang_ini.readFileToStruct(lang_path, comment_characters, null) catch Lang{}; if (config.load) { var user_buf: [32]u8 = undefined; - save = save_ini.readFileToStruct(save_path) catch migrator.tryMigrateSaveFile(&user_buf, config.save_file); + save = save_ini.readFileToStruct(save_path, comment_characters, null) catch migrator.tryMigrateSaveFile(&user_buf, config.save_file); } } @@ -541,7 +542,7 @@ pub fn main() !void { .user = login.text.items, .session_index = desktop.current, }; - ini.writeFromStruct(save_data, file.writer(), null) catch break :save_last_settings; + ini.writeFromStruct(save_data, file.writer(), null, true, .{}) catch break :save_last_settings; } var shared_err = try SharedError.init(); diff --git a/src/tui/components/Desktop.zig b/src/tui/components/Desktop.zig index 98870d0..5bbaf60 100644 --- a/src/tui/components/Desktop.zig +++ b/src/tui/components/Desktop.zig @@ -150,7 +150,7 @@ pub fn crawl(self: *Desktop, path: []const u8, display_server: DisplayServer) !v const entry_path = try std.fmt.allocPrint(self.allocator, "{s}/{s}", .{ path, item.name }); defer self.allocator.free(entry_path); var entry_ini = Ini(Entry).init(self.allocator); - _ = try entry_ini.readFileToStruct(entry_path); + _ = try entry_ini.readFileToStruct(entry_path, "#", null); errdefer entry_ini.deinit(); var xdg_session_desktop: []const u8 = undefined; From 2dec2e0b7f2f531317948e99be64e084606be374 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Tue, 30 Jul 2024 10:54:30 +0200 Subject: [PATCH 057/530] Print data directory deletion in build.zig Signed-off-by: AnErrupTion --- build.zig | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/build.zig b/build.zig index 0acb400..c2dbea9 100644 --- a/build.zig +++ b/build.zig @@ -273,12 +273,10 @@ fn install_ly(allocator: std.mem.Allocator, install_config: bool) !void { } pub fn uninstallall(step: *std.Build.Step, _: ProgressNode) !void { - std.fs.cwd().deleteTree(data_directory) catch { - std.debug.print("warn: ly data directory not found.", .{}); - }; - const allocator = step.owner.allocator; + try deleteTree(allocator, data_directory, "ly data directory not found"); + const exe_path = try std.fs.path.join(allocator, &[_][]const u8{ dest_directory, "/usr/bin/", exe_name }); var success = true; std.fs.cwd().deleteFile(exe_path) catch { From b1bf89a4cf0f80aea857b51dfc0fe1d12f4bd1bf Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Tue, 30 Jul 2024 11:56:21 +0200 Subject: [PATCH 058/530] Only shutdown or restart after deinitializing everything Signed-off-by: AnErrupTion --- src/main.zig | 37 ++++++++++++++++++++++++++++--------- 1 file changed, 28 insertions(+), 9 deletions(-) diff --git a/src/main.zig b/src/main.zig index 9615fee..160087f 100644 --- a/src/main.zig +++ b/src/main.zig @@ -22,6 +22,7 @@ const utils = @import("tui/utils.zig"); const Ini = ini.Ini; const termbox = interop.termbox; const unistd = interop.unistd; +const temporary_allocator = std.heap.page_allocator; var session_pid: std.posix.pid_t = -1; pub fn signalHandler(i: c_int) callconv(.C) void { @@ -39,11 +40,32 @@ pub fn signalHandler(i: c_int) callconv(.C) void { } pub fn main() !void { + var shutdown = false; + var restart = false; + var shutdown_cmd: []const u8 = undefined; + var restart_cmd: []const u8 = undefined; + + const stderr = std.io.getStdErr().writer(); + + 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 }); + stderr.print("error: couldn't shutdown: {any}\n", .{shutdown_error}) catch std.process.exit(1); + } else if (restart) { + const restart_error = std.process.execv(temporary_allocator, &[_][]const u8{ "/bin/sh", "-c", restart_cmd }); + stderr.print("error: couldn't restart: {any}\n", .{restart_error}) catch std.process.exit(1); + } else { + // The user has quit Ly using Ctrl+C + temporary_allocator.free(shutdown_cmd); + temporary_allocator.free(restart_cmd); + } + } + var gpa = std.heap.GeneralPurposeAllocator(.{}){}; defer _ = gpa.deinit(); const allocator = gpa.allocator(); - const stderr = std.io.getStdErr().writer(); // Load arguments const params = comptime clap.parseParamsComptime( @@ -138,6 +160,11 @@ pub fn main() !void { } } + // 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); + if (!build_options.enable_x11_support) try info_line.addMessage(lang.no_x11_support, config.bg, config.fg); interop.setNumlock(config.numlock) catch {}; @@ -277,8 +304,6 @@ pub fn main() !void { var run = true; var update = true; var resolution_changed = false; - var shutdown = false; - var restart = false; var auth_fails: u64 = 0; // Switch to selected TTY if possible @@ -685,12 +710,6 @@ pub fn main() !void { }, } } - - if (shutdown) { - return std.process.execv(allocator, &[_][]const u8{ "/bin/sh", "-c", config.shutdown_cmd }); - } else if (restart) { - return std.process.execv(allocator, &[_][]const u8{ "/bin/sh", "-c", config.restart_cmd }); - } } fn getAuthErrorMsg(err: anyerror, lang: Lang) []const u8 { From 40d180da632d925f92ae61b436ec9d51a44f668b Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Tue, 30 Jul 2024 12:04:25 +0200 Subject: [PATCH 059/530] Backport: Only shutdown or restart after deinitializing everything Signed-off-by: AnErrupTion --- src/main.zig | 37 ++++++++++++++++++++++++++++--------- 1 file changed, 28 insertions(+), 9 deletions(-) diff --git a/src/main.zig b/src/main.zig index b0e9a96..ddee718 100644 --- a/src/main.zig +++ b/src/main.zig @@ -21,6 +21,7 @@ const utils = @import("tui/utils.zig"); const Ini = ini.Ini; const termbox = interop.termbox; +const temporary_allocator = std.heap.page_allocator; var session_pid: std.posix.pid_t = -1; pub fn signalHandler(i: c_int) callconv(.C) void { @@ -38,11 +39,32 @@ pub fn signalHandler(i: c_int) callconv(.C) void { } pub fn main() !void { + var shutdown = false; + var restart = false; + var shutdown_cmd: []const u8 = undefined; + var restart_cmd: []const u8 = undefined; + + const stderr = std.io.getStdErr().writer(); + + 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 }); + stderr.print("error: couldn't shutdown: {any}\n", .{shutdown_error}) catch std.process.exit(1); + } else if (restart) { + const restart_error = std.process.execv(temporary_allocator, &[_][]const u8{ "/bin/sh", "-c", restart_cmd }); + stderr.print("error: couldn't restart: {any}\n", .{restart_error}) catch std.process.exit(1); + } else { + // The user has quit Ly using Ctrl+C + temporary_allocator.free(shutdown_cmd); + temporary_allocator.free(restart_cmd); + } + } + var gpa = std.heap.GeneralPurposeAllocator(.{}){}; defer _ = gpa.deinit(); const allocator = gpa.allocator(); - const stderr = std.io.getStdErr().writer(); // Load arguments const params = comptime clap.parseParamsComptime( @@ -128,6 +150,11 @@ pub fn main() !void { } } + // 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); + interop.setNumlock(config.numlock) catch {}; if (config.initial_info_text) |text| { @@ -258,8 +285,6 @@ pub fn main() !void { var run = true; var update = true; var resolution_changed = false; - var shutdown = false; - var restart = false; var auth_fails: u64 = 0; // Switch to selected TTY if possible @@ -636,12 +661,6 @@ pub fn main() !void { }, } } - - if (shutdown) { - return std.process.execv(allocator, &[_][]const u8{ "/bin/sh", "-c", config.shutdown_cmd }); - } else if (restart) { - return std.process.execv(allocator, &[_][]const u8{ "/bin/sh", "-c", config.restart_cmd }); - } } fn getAuthErrorMsg(err: anyerror, lang: Lang) []const u8 { From 3fedb59fdb9d9c12eb75a7a2703c9915eabdcaab Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Tue, 30 Jul 2024 18:38:21 +0200 Subject: [PATCH 060/530] Make setting numlock work on BSD + less homebrew interop Signed-off-by: AnErrupTion --- src/auth.zig | 26 ++++++------- src/config/Config.zig | 2 +- src/interop.zig | 91 ++++++++++++++++++++++++------------------- src/main.zig | 13 ++----- 4 files changed, 67 insertions(+), 65 deletions(-) diff --git a/src/auth.zig b/src/auth.zig index 33b16f1..9f295af 100644 --- a/src/auth.zig +++ b/src/auth.zig @@ -65,16 +65,16 @@ pub fn authenticate(config: Config, current_environment: Desktop.Environment, lo if (status != interop.pam.PAM_SUCCESS) return pamDiagnose(status); defer status = interop.pam.pam_close_session(handle, 0); - var pwd: *interop.passwd = undefined; + var pwd: *std.c.passwd = undefined; { defer interop.endpwent(); // Get password structure from username - pwd = interop.getpwnam(login.ptr) orelse return error.GetPasswordNameFailed; + pwd = std.c.getpwnam(login.ptr) orelse return error.GetPasswordNameFailed; } // Set user shell if it hasn't already been set - if (pwd.pw_shell[0] == 0) { + if (pwd.pw_shell == null) { interop.setusershell(); pwd.pw_shell = interop.getusershell(); interop.endusershell(); @@ -109,25 +109,25 @@ pub fn authenticate(config: Config, current_environment: Desktop.Environment, lo }; try std.posix.sigaction(std.posix.SIG.TERM, &act, null); - try addUtmpEntry(&entry, pwd.pw_name, child_pid); + try addUtmpEntry(&entry, pwd.pw_name.?, child_pid); } // Wait for the session to stop _ = std.posix.waitpid(child_pid, 0); removeUtmpEntry(&entry); - try resetTerminal(pwd.pw_shell, config.term_reset_cmd); + try resetTerminal(pwd.pw_shell.?, config.term_reset_cmd); if (shared_err.readError()) |err| return err; } fn startSession( config: Config, - pwd: *interop.passwd, + pwd: *std.c.passwd, handle: ?*interop.pam.pam_handle, current_environment: Desktop.Environment, ) !void { - const status = interop.initgroups(pwd.pw_name, pwd.pw_gid); + const status = interop.initgroups(pwd.pw_name.?, pwd.pw_gid); if (status != 0) return error.GroupInitializationFailed; if (builtin.os.tag == .freebsd) { @@ -150,22 +150,22 @@ fn startSession( for (env_list) |env_var| _ = interop.putenv(env_var.?); // Execute what the user requested - std.posix.chdirZ(pwd.pw_dir) catch return error.ChangeDirectoryFailed; + std.posix.chdirZ(pwd.pw_dir.?) catch return error.ChangeDirectoryFailed; - try resetTerminal(pwd.pw_shell, config.term_reset_cmd); + try resetTerminal(pwd.pw_shell.?, config.term_reset_cmd); switch (current_environment.display_server) { - .wayland => try executeWaylandCmd(pwd.pw_shell, config.wayland_cmd, current_environment.cmd), - .shell => try executeShellCmd(pwd.pw_shell), + .wayland => try executeWaylandCmd(pwd.pw_shell.?, config.wayland_cmd, current_environment.cmd), + .shell => try executeShellCmd(pwd.pw_shell.?), .xinitrc, .x11 => if (build_options.enable_x11_support) { var vt_buf: [5]u8 = undefined; const vt = try std.fmt.bufPrint(&vt_buf, "vt{d}", .{config.tty}); - try executeX11Cmd(pwd.pw_shell, pwd.pw_dir, config, current_environment.cmd, vt); + try executeX11Cmd(pwd.pw_shell.?, pwd.pw_dir.?, config, current_environment.cmd, vt); }, } } -fn initEnv(pwd: *interop.passwd, path_env: ?[:0]const u8) !void { +fn initEnv(pwd: *std.c.passwd, path_env: ?[:0]const u8) !void { _ = interop.setenv("HOME", pwd.pw_dir, 1); _ = interop.setenv("PWD", pwd.pw_dir, 1); _ = interop.setenv("SHELL", pwd.pw_shell, 1); diff --git a/src/config/Config.zig b/src/config/Config.zig index 0274ce5..11006d7 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -14,7 +14,7 @@ border_fg: u8 = 8, box_title: ?[]const u8 = null, clear_password: bool = false, clock: ?[:0]const u8 = null, -console_dev: [:0]const u8 = "/dev/console", +console_dev: []const u8 = "/dev/console", default_input: Input = .login, error_bg: u16 = 0, error_fg: u16 = 258, diff --git a/src/interop.zig b/src/interop.zig index 23bb83e..beba61c 100644 --- a/src/interop.zig +++ b/src/interop.zig @@ -21,11 +21,25 @@ pub const unistd = @cImport({ @cInclude("unistd.h"); }); -// Exists for FreeBSD only +// FreeBSD-specific headers pub const logincap = @cImport({ @cInclude("login_cap.h"); }); +// BSD-specific headers +pub const kbio = @cImport({ + @cInclude("sys/kbio.h"); +}); + +// Linux-specific headers +pub const kd = @cImport({ + @cInclude("sys/kd.h"); +}); + +pub const vt = @cImport({ + @cInclude("sys/vt.h"); +}); + pub const c_size = usize; pub const c_uid = u32; pub const c_gid = u32; @@ -41,37 +55,12 @@ pub const tm = extern struct { tm_yday: c_int, tm_isdst: c_int, }; -pub const passwd = extern struct { - pw_name: [*:0]u8, - pw_passwd: [*:0]u8, - - pw_uid: c_uid, - pw_gid: c_gid, - pw_gecos: [*:0]u8, - pw_dir: [*:0]u8, - pw_shell: [*:0]u8, -}; - -pub const VT_ACTIVATE: c_int = 0x5606; -pub const VT_WAITACTIVE: c_int = 0x5607; - -pub const KDGETLED: c_int = 0x4B31; -pub const KDSETLED: c_int = 0x4B32; -pub const KDGKBLED: c_int = 0x4B64; -pub const KDSKBLED: c_int = 0x4B65; - -pub const LED_NUM: c_int = 0x02; -pub const LED_CAP: c_int = 0x04; - -pub const K_NUMLOCK: c_int = 0x02; -pub const K_CAPSLOCK: c_int = 0x04; pub extern "c" fn localtime(timer: *const c_time) *tm; pub extern "c" fn strftime(str: [*:0]u8, maxsize: c_size, format: [*:0]const u8, timeptr: *const tm) c_size; -pub extern "c" fn setenv(name: [*:0]const u8, value: [*:0]const u8, overwrite: c_int) c_int; +pub extern "c" fn setenv(name: [*:0]const u8, value: ?[*:0]const u8, overwrite: c_int) c_int; pub extern "c" fn putenv(name: [*:0]u8) c_int; pub extern "c" fn getuid() c_uid; -pub extern "c" fn getpwnam(name: [*:0]const u8) ?*passwd; pub extern "c" fn endpwent() void; pub extern "c" fn setusershell() void; pub extern "c" fn getusershell() [*:0]u8; @@ -88,27 +77,34 @@ pub fn timeAsString(buf: [:0]u8, format: [:0]const u8) ![]u8 { return buf[0..len]; } -pub fn getLockState(console_dev: [:0]const u8) !struct { +pub fn switchTty(console_dev: []const u8, tty: u8) !void { + const fd = try std.posix.open(console_dev, .{ .ACCMODE = .WRONLY }, 0); + defer std.posix.close(fd); + + _ = std.c.ioctl(fd, vt.VT_ACTIVATE, tty); + _ = std.c.ioctl(fd, vt.VT_WAITACTIVE, tty); +} + +pub fn getLockState(console_dev: []const u8) !struct { numlock: bool, capslock: bool, } { - const fd = std.c.open(console_dev, .{ .ACCMODE = .RDONLY }); - if (fd < 0) return error.CannotOpenConsoleDev; - defer _ = std.c.close(fd); + const fd = try std.posix.open(console_dev, .{ .ACCMODE = .RDONLY }, 0); + defer std.posix.close(fd); var numlock = false; var capslock = false; if (builtin.os.tag.isBSD()) { var led: c_int = undefined; - _ = std.c.ioctl(fd, KDGETLED, &led); - numlock = (led & LED_NUM) != 0; - capslock = (led & LED_CAP) != 0; + _ = std.c.ioctl(fd, kbio.KDGETLED, &led); + numlock = (led & kbio.LED_NUM) != 0; + capslock = (led & kbio.LED_CAP) != 0; } else { var led: c_char = undefined; - _ = std.c.ioctl(fd, KDGKBLED, &led); - numlock = (led & K_NUMLOCK) != 0; - capslock = (led & K_CAPSLOCK) != 0; + _ = std.c.ioctl(fd, kd.KDGKBLED, &led); + numlock = (led & kd.K_NUMLOCK) != 0; + capslock = (led & kd.K_CAPSLOCK) != 0; } return .{ @@ -118,12 +114,25 @@ pub fn getLockState(console_dev: [:0]const u8) !struct { } pub fn setNumlock(val: bool) !void { - var led: c_char = undefined; - _ = std.c.ioctl(0, KDGKBLED, &led); + if (builtin.os.tag.isBSD()) { + var led: c_int = undefined; + _ = std.c.ioctl(0, kbio.KDGETLED, &led); - const numlock = (led & K_NUMLOCK) != 0; + const numlock = (led & kbio.LED_NUM) != 0; + if (numlock != val) { + const status = std.c.ioctl(std.posix.STDIN_FILENO, kbio.KDSETLED, led ^ kbio.LED_NUM); + if (status != 0) return error.FailedToSetNumlock; + } + + return; + } + + var led: c_char = undefined; + _ = std.c.ioctl(0, kd.KDGKBLED, &led); + + const numlock = (led & kd.K_NUMLOCK) != 0; if (numlock != val) { - const status = std.c.ioctl(std.posix.STDIN_FILENO, KDSKBLED, led ^ K_NUMLOCK); + const status = std.c.ioctl(std.posix.STDIN_FILENO, kd.KDSKBLED, led ^ kd.K_NUMLOCK); if (status != 0) return error.FailedToSetNumlock; } } diff --git a/src/main.zig b/src/main.zig index 160087f..60d4f3c 100644 --- a/src/main.zig +++ b/src/main.zig @@ -307,16 +307,9 @@ pub fn main() !void { var auth_fails: u64 = 0; // Switch to selected TTY if possible - open_console_dev: { - const fd = std.posix.open(config.console_dev, .{ .ACCMODE = .WRONLY }, 0) catch { - try info_line.addMessage(lang.err_console_dev, config.error_bg, config.error_fg); - break :open_console_dev; - }; - defer std.posix.close(fd); - - _ = std.c.ioctl(fd, interop.VT_ACTIVATE, config.tty); - _ = std.c.ioctl(fd, interop.VT_WAITACTIVE, config.tty); - } + interop.switchTty(config.console_dev, config.tty) catch { + try info_line.addMessage(lang.err_console_dev, config.error_bg, config.error_fg); + }; while (run) { // If there's no input or there's an animation, a resolution change needs to be checked From b592a11fb0580f10d69ca6fd10d5428801d5ff74 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Wed, 31 Jul 2024 09:44:25 +0200 Subject: [PATCH 061/530] Improve the config migrator Signed-off-by: AnErrupTion --- src/config/migrator.zig | 71 ++++++++++++++++++++++++++++++++++++++--- src/main.zig | 24 ++++++++++++++ 2 files changed, 91 insertions(+), 4 deletions(-) diff --git a/src/config/migrator.zig b/src/config/migrator.zig index d8a732e..8268f37 100644 --- a/src/config/migrator.zig +++ b/src/config/migrator.zig @@ -3,15 +3,78 @@ const std = @import("std"); const ini = @import("zigini"); const Save = @import("Save.zig"); +const enums = @import("../enums.zig"); + +var animate = false; + +pub var mapped_config_fields = false; pub fn configFieldHandler(_: std.mem.Allocator, field: ini.IniField) ?ini.IniField { - var mapped_field = field; + if (std.mem.eql(u8, field.key, "animate")) { + // The option doesn't exist anymore, but we save its value for "animation" + animate = std.mem.eql(u8, field.value, "true"); - if (std.mem.eql(u8, field.key, "blank_password")) { - mapped_field.key = "clear_password"; + mapped_config_fields = true; + return null; } - return mapped_field; + if (std.mem.eql(u8, field.key, "animation")) { + // The option now uses a string (which then gets converted into an enum) instead of an integer + // It also combines the previous "animate" and "animation" options + const animation = std.fmt.parseInt(u8, field.value, 10) catch return field; + var mapped_field = field; + + mapped_field.value = switch (animation) { + 0 => "doom", + 1 => "matrix", + else => "none", + }; + + mapped_config_fields = true; + return mapped_field; + } + + if (std.mem.eql(u8, field.key, "blank_password")) { + // The option has simply been renamed + var mapped_field = field; + mapped_field.key = "clear_password"; + + mapped_config_fields = true; + return mapped_field; + } + + if (std.mem.eql(u8, field.key, "default_input")) { + // The option now uses a string (which then gets converted into an enum) instead of an integer + const default_input = std.fmt.parseInt(u8, field.value, 10) catch return field; + var mapped_field = field; + + mapped_field.value = switch (default_input) { + 0 => "session", + 1 => "login", + 2 => "password", + else => "login", + }; + + mapped_config_fields = true; + return mapped_field; + } + + if (std.mem.eql(u8, field.key, "wayland_specifier")) { + // The option doesn't exist anymore + + mapped_config_fields = true; + return null; + } + + return field; +} + +// This is the stuff we only handle after reading the config. +// For example, the "animate" field could come after "animation" +pub fn lateConfigFieldHandler(animation: *enums.Animation) void { + if (!mapped_config_fields) return; + + if (!animate) animation.* = .none; } pub fn tryMigrateSaveFile(user_buf: *[32]u8, path: []const u8) Save { diff --git a/src/main.zig b/src/main.zig index 60d4f3c..1f12bd7 100644 --- a/src/main.zig +++ b/src/main.zig @@ -140,6 +140,18 @@ pub fn main() !void { var user_buf: [32]u8 = undefined; save = save_ini.readFileToStruct(save_path, comment_characters, null) catch migrator.tryMigrateSaveFile(&user_buf, config.save_file); } + + migrator.lateConfigFieldHandler(&config.animation); + + // if (migrator.mapped_config_fields) save_migrated_config: { + // var file = try std.fs.cwd().createFile(config_path, .{}); + // defer file.close(); + + // const writer = file.writer(); + // ini.writeFromStruct(config, writer, null, true, .{}) catch { + // break :save_migrated_config; + // }; + // } } else { const config_path = build_options.data_directory ++ "/config.ini"; @@ -158,6 +170,18 @@ pub fn main() !void { var user_buf: [32]u8 = undefined; save = save_ini.readFileToStruct(save_path, comment_characters, null) catch migrator.tryMigrateSaveFile(&user_buf, config.save_file); } + + migrator.lateConfigFieldHandler(&config.animation); + + // if (migrator.mapped_config_fields) save_migrated_config: { + // var file = try std.fs.cwd().createFile(config_path, .{}); + // defer file.close(); + + // const writer = file.writer(); + // ini.writeFromStruct(config, writer, null, true, .{}) catch { + // break :save_migrated_config; + // }; + // } } // These strings only end up getting freed if the user quits Ly using Ctrl+C, which is fine since in the other cases From a64d7efc695d28a9efd19f095365b8446b35aef3 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Wed, 31 Jul 2024 10:34:27 +0200 Subject: [PATCH 062/530] Make asterisk optional (hides password if so) Signed-off-by: AnErrupTion --- res/config.ini | 24 +++++++++++++++--------- src/config/Config.zig | 2 +- src/main.zig | 6 +++--- src/tui/components/Text.zig | 28 ++++++++++++++++++++-------- 4 files changed, 39 insertions(+), 21 deletions(-) diff --git a/res/config.ini b/res/config.ini index da149a6..d2a7eee 100644 --- a/res/config.ini +++ b/res/config.ini @@ -5,12 +5,14 @@ animation = none # Format string for clock in top right corner (see strftime specification). Example: %c +# If null, the clock won't be shown clock = null # Enable/disable big clock bigclock = false # The character used to mask the password +# If null, the password will be hidden asterisk = * # Erase password input on failure @@ -35,12 +37,12 @@ vi_default_mode = normal #define TB_CYAN 0x07 #define TB_WHITE 0x08 # -# Setting both to zero makes `bg` black and `fg` white. To set the actual color palette you are encouraged to use another tool -# such as [mkinitcpio-colors](https://github.com/evanpurkhiser/mkinitcpio-colors). Note that the color palette defined with -# `mkinitcpio-colors` takes 16 colors (0-15), only values 0-8 are valid for `ly` config and these values do not correspond -# exactly. For instance, in defining palettes with `mkinitcpio-colors` the order is black, dark red, dark green, brown, dark -# blue, dark purple, dark cyan, light gray, dark gray, bright red, bright green, yellow, bright blue, bright purple, bright -# cyan, and white, indexed in that order 0 through 15. For example, the color defined for white (indexed at 15 in the mkinitcpio +# Setting both to zero makes `bg` black and `fg` white. To set the actual color palette you are encouraged to use another tool +# such as [mkinitcpio-colors](https://github.com/evanpurkhiser/mkinitcpio-colors). Note that the color palette defined with +# `mkinitcpio-colors` takes 16 colors (0-15), only values 0-8 are valid for `ly` config and these values do not correspond +# exactly. For instance, in defining palettes with `mkinitcpio-colors` the order is black, dark red, dark green, brown, dark +# blue, dark purple, dark cyan, light gray, dark gray, bright red, bright green, yellow, bright blue, bright purple, bright +# cyan, and white, indexed in that order 0 through 15. For example, the color defined for white (indexed at 15 in the mkinitcpio # config) will be used by `ly` for `fg = 8`. # Background color id @@ -63,9 +65,11 @@ cmatrix_fg = 3 border_fg = 8 # Title to show at the top of the main box +# If set to null, none will be shown box_title = null -# Initial text to show on the info line (Defaults to hostname) +# Initial text to show on the info line +# If set to null, the info line defaults to the hostname initial_info_text = null # Blank main box background @@ -134,7 +138,8 @@ tty = 2 # Console path console_dev = /dev/console -# Default path. If null, ly doesn't set a path. +# Default path +# If null, ly doesn't set a path path = /sbin:/bin:/usr/local/sbin:/usr/local/bin:/usr/bin:/usr/sbin # Event timeout in milliseconds @@ -161,7 +166,8 @@ wayland_cmd = /etc/ly/wsetup.sh # Wayland desktop environments waylandsessions = /usr/share/wayland-sessions -# xinitrc (hidden if null) +# xinitrc +# If null, the xinitrc session will be hidden xinitrc = ~/.xinitrc # Xorg server command diff --git a/src/config/Config.zig b/src/config/Config.zig index 11006d7..c14db0f 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -6,7 +6,7 @@ const Input = enums.Input; const ViMode = enums.ViMode; animation: Animation = .none, -asterisk: u8 = '*', +asterisk: ?u8 = '*', bg: u16 = 0, bigclock: bool = false, blank_box: bool = true, diff --git a/src/main.zig b/src/main.zig index 1f12bd7..7f49dfc 100644 --- a/src/main.zig +++ b/src/main.zig @@ -253,10 +253,10 @@ pub fn main() !void { try desktop.crawl(config.waylandsessions, .wayland); if (build_options.enable_x11_support) try desktop.crawl(config.xsessions, .x11); - var login = try Text.init(allocator, &buffer, config.max_login_len); + var login = try Text.init(allocator, &buffer, config.max_login_len, false, null); defer login.deinit(); - var password = try Text.init(allocator, &buffer, config.max_password_len); + var password = try Text.init(allocator, &buffer, config.max_password_len, true, config.asterisk); defer password.deinit(); var active_input = config.default_input; @@ -508,7 +508,7 @@ pub fn main() !void { desktop.draw(); login.draw(); - password.drawMasked(config.asterisk); + password.draw(); } else { std.time.sleep(std.time.ns_per_ms * 10); update = buffer.cascade(); diff --git a/src/tui/components/Text.zig b/src/tui/components/Text.zig index e65e4a6..2d76b76 100644 --- a/src/tui/components/Text.zig +++ b/src/tui/components/Text.zig @@ -19,8 +19,10 @@ visible_start: usize, visible_length: usize, x: usize, y: usize, +masked: bool, +maybe_mask: ?u8, -pub fn init(allocator: Allocator, buffer: *TerminalBuffer, max_length: usize) !Text { +pub fn init(allocator: Allocator, buffer: *TerminalBuffer, max_length: usize, masked: bool, maybe_mask: ?u8) !Text { const text = try DynamicString.initCapacity(allocator, max_length); return .{ @@ -33,6 +35,8 @@ pub fn init(allocator: Allocator, buffer: *TerminalBuffer, max_length: usize) !T .visible_length = 0, .x = 0, .y = 0, + .masked = masked, + .maybe_mask = maybe_mask, }; } @@ -78,10 +82,25 @@ 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)); + return; + } + _ = termbox.tb_set_cursor(@intCast(self.x + (self.cursor - self.visible_start)), @intCast(self.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); + if (length == 0) return; + + self.buffer.drawCharMultiple(mask, self.x, self.y, length); + } + return; + } + const length = @min(self.text.items.len, self.visible_length); if (length == 0) return; @@ -96,13 +115,6 @@ pub fn draw(self: Text) void { self.buffer.drawLabel(visible_slice, self.x, self.y); } -pub fn drawMasked(self: Text, mask: u8) void { - const length = @min(self.text.items.len, self.visible_length - 1); - if (length == 0) return; - - self.buffer.drawCharMultiple(mask, self.x, self.y, length); -} - pub fn clear(self: *Text) void { self.text.clearRetainingCapacity(); self.end = 0; From 8b12ade3721b171d199d16db65060daa800f2923 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joaqu=C3=ADn=20Guerra?= <45147327+warbacon@users.noreply.github.com> Date: Wed, 31 Jul 2024 13:14:29 +0200 Subject: [PATCH 063/530] Update spanish translation (#668) --- res/lang/es.ini | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/res/lang/es.ini b/res/lang/es.ini index 1160fa8..73a9acd 100644 --- a/res/lang/es.ini +++ b/res/lang/es.ini @@ -1,3 +1,6 @@ +authenticating = autenticando... +brightness_down = bajar brillo +brightness_up = subir brillo capslock = Bloq Mayús err_alloc = asignación de memoria fallida err_bounds = índice fuera de límites @@ -34,12 +37,17 @@ err_user_init = error al inicializar usuario err_user_uid = error al establecer el UID del usuario err_xsessions_dir = error al buscar la carpeta de sesiones err_xsessions_open = error al abrir la carpeta de sesiones -login = iniciar sesión +insert = insertar +login = usuario logout = cerrar sesión +no_x11_support = soporte para x11 deshabilitado en tiempo de compilación +normal = normal numlock = Bloq Num +other = otro password = contraseña restart = reiniciar shell = shell shutdown = apagar +sleep = suspender wayland = wayland xinitrc = xinitrc From 961018e753852427c53dd0b0a461b4553613d8b4 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Wed, 31 Jul 2024 13:21:54 +0200 Subject: [PATCH 064/530] Add generic cyclable label & base session component off it Signed-off-by: AnErrupTion --- src/auth.zig | 6 +- src/main.zig | 32 ++--- src/tui/components/Desktop.zig | 223 --------------------------------- src/tui/components/Session.zig | 153 ++++++++++++++++++++++ src/tui/components/generic.zig | 105 ++++++++++++++++ 5 files changed, 277 insertions(+), 242 deletions(-) delete mode 100644 src/tui/components/Desktop.zig create mode 100644 src/tui/components/Session.zig create mode 100644 src/tui/components/generic.zig diff --git a/src/auth.zig b/src/auth.zig index 9f295af..c05b9b3 100644 --- a/src/auth.zig +++ b/src/auth.zig @@ -4,7 +4,7 @@ const builtin = @import("builtin"); const enums = @import("enums.zig"); const interop = @import("interop.zig"); const TerminalBuffer = @import("tui/TerminalBuffer.zig"); -const Desktop = @import("tui/components/Desktop.zig"); +const Session = @import("tui/components/Session.zig"); const Text = @import("tui/components/Text.zig"); const Config = @import("config/Config.zig"); const Allocator = std.mem.Allocator; @@ -22,7 +22,7 @@ pub fn sessionSignalHandler(i: c_int) callconv(.C) void { if (child_pid > 0) _ = std.c.kill(child_pid, i); } -pub fn authenticate(config: Config, current_environment: Desktop.Environment, login: [:0]const u8, password: [:0]const u8) !void { +pub fn authenticate(config: Config, current_environment: Session.Environment, login: [:0]const u8, password: [:0]const u8) !void { var tty_buffer: [3]u8 = undefined; const tty_str = try std.fmt.bufPrintZ(&tty_buffer, "{d}", .{config.tty}); @@ -125,7 +125,7 @@ fn startSession( config: Config, pwd: *std.c.passwd, handle: ?*interop.pam.pam_handle, - current_environment: Desktop.Environment, + current_environment: Session.Environment, ) !void { const status = interop.initgroups(pwd.pw_name.?, pwd.pw_gid); if (status != 0) return error.GroupInitializationFailed; diff --git a/src/main.zig b/src/main.zig index 7f49dfc..e5ea380 100644 --- a/src/main.zig +++ b/src/main.zig @@ -9,7 +9,7 @@ const interop = @import("interop.zig"); const Doom = @import("animations/Doom.zig"); const Matrix = @import("animations/Matrix.zig"); const TerminalBuffer = @import("tui/TerminalBuffer.zig"); -const Desktop = @import("tui/components/Desktop.zig"); +const Session = @import("tui/components/Session.zig"); const Text = @import("tui/components/Text.zig"); const InfoLine = @import("tui/components/InfoLine.zig"); const Config = @import("config/Config.zig"); @@ -235,23 +235,23 @@ pub fn main() !void { var buffer = TerminalBuffer.init(config, labels_max_length, random); // Initialize components - var desktop = try Desktop.init(allocator, &buffer, config.max_desktop_len, lang); - defer desktop.deinit(); + var session = try Session.init(allocator, &buffer, config.max_desktop_len, lang); + defer session.deinit(); - desktop.addEnvironment(.{ .Name = lang.shell }, "", .shell) catch { + session.addEnvironment(.{ .Name = lang.shell }, "", .shell) catch { try info_line.addMessage(lang.err_alloc, config.error_bg, config.error_fg); }; if (build_options.enable_x11_support) { if (config.xinitrc) |xinitrc| { - desktop.addEnvironment(.{ .Name = lang.xinitrc, .Exec = xinitrc }, "", .xinitrc) catch { + session.addEnvironment(.{ .Name = lang.xinitrc, .Exec = xinitrc }, "", .xinitrc) catch { try info_line.addMessage(lang.err_alloc, config.error_bg, config.error_fg); }; } } - try desktop.crawl(config.waylandsessions, .wayland); - if (build_options.enable_x11_support) try desktop.crawl(config.xsessions, .x11); + try session.crawl(config.waylandsessions, .wayland); + if (build_options.enable_x11_support) try session.crawl(config.xsessions, .x11); var login = try Text.init(allocator, &buffer, config.max_login_len, false, null); defer login.deinit(); @@ -272,7 +272,7 @@ pub fn main() !void { } if (save.session_index) |session_index| { - if (session_index < desktop.environments.items.len) desktop.current = session_index; + if (session_index < session.label.list.items.len) session.label.current = session_index; } } @@ -281,12 +281,12 @@ pub fn main() !void { buffer.drawBoxCenter(!config.hide_borders, config.blank_box); const coordinates = buffer.calculateComponentCoordinates(); - desktop.position(coordinates.x, coordinates.y + 2, coordinates.visible_length); + session.label.position(coordinates.x, coordinates.y + 2, coordinates.visible_length); login.position(coordinates.x, coordinates.y + 4, coordinates.visible_length); password.position(coordinates.x, coordinates.y + 6, coordinates.visible_length); switch (active_input) { - .session => desktop.handle(null, insert_mode), + .session => session.label.handle(null, insert_mode), .login => login.handle(null, insert_mode) catch { try info_line.addMessage(lang.err_alloc, config.error_bg, config.error_fg); }, @@ -404,7 +404,7 @@ pub fn main() !void { if (resolution_changed) { const coordinates = buffer.calculateComponentCoordinates(); - desktop.position(coordinates.x, coordinates.y + 2, coordinates.visible_length); + session.label.position(coordinates.x, coordinates.y + 2, coordinates.visible_length); login.position(coordinates.x, coordinates.y + 4, coordinates.visible_length); password.position(coordinates.x, coordinates.y + 6, coordinates.visible_length); @@ -412,7 +412,7 @@ pub fn main() !void { } switch (active_input) { - .session => desktop.handle(null, insert_mode), + .session => session.label.handle(null, insert_mode), .login => login.handle(null, insert_mode) catch { try info_line.addMessage(lang.err_alloc, config.error_bg, config.error_fg); }, @@ -506,7 +506,7 @@ pub fn main() !void { } } - desktop.draw(); + session.label.draw(); login.draw(); password.draw(); } else { @@ -631,7 +631,7 @@ pub fn main() !void { const save_data = Save{ .user = login.text.items, - .session_index = desktop.current, + .session_index = session.label.current, }; ini.writeFromStruct(save_data, file.writer(), null, true, .{}) catch break :save_last_settings; } @@ -652,7 +652,7 @@ pub fn main() !void { session_pid = try std.posix.fork(); if (session_pid == 0) { - const current_environment = desktop.environments.items[desktop.current]; + const current_environment = session.label.list.items[session.label.current]; auth.authenticate(config, current_environment, login_text, password_text) catch |err| { shared_err.writeError(err); std.process.exit(1); @@ -715,7 +715,7 @@ pub fn main() !void { } switch (active_input) { - .session => desktop.handle(&event, insert_mode), + .session => session.label.handle(&event, insert_mode), .login => login.handle(&event, insert_mode) catch { try info_line.addMessage(lang.err_alloc, config.error_bg, config.error_fg); }, diff --git a/src/tui/components/Desktop.zig b/src/tui/components/Desktop.zig deleted file mode 100644 index 9c0fdf1..0000000 --- a/src/tui/components/Desktop.zig +++ /dev/null @@ -1,223 +0,0 @@ -const std = @import("std"); -const enums = @import("../../enums.zig"); -const interop = @import("../../interop.zig"); -const TerminalBuffer = @import("../TerminalBuffer.zig"); -const Ini = @import("zigini").Ini; -const Lang = @import("../../config/Lang.zig"); - -const Allocator = std.mem.Allocator; -const EnvironmentList = std.ArrayList(Environment); - -const DisplayServer = enums.DisplayServer; - -const termbox = interop.termbox; - -const Desktop = @This(); - -pub const Environment = struct { - entry_ini: ?Ini(Entry) = null, - name: [:0]const u8 = "", - xdg_session_desktop: [:0]const u8 = "", - xdg_desktop_names: ?[:0]const u8 = "", - cmd: []const u8 = "", - specifier: []const u8 = "", - display_server: DisplayServer = .wayland, -}; - -const DesktopEntry = struct { - Exec: []const u8 = "", - Name: [:0]const u8 = "", - DesktopNames: ?[]const u8 = null, -}; - -pub const Entry = struct { @"Desktop Entry": DesktopEntry = DesktopEntry{} }; - -allocator: Allocator, -buffer: *TerminalBuffer, -environments: EnvironmentList, -current: usize, -visible_length: usize, -x: usize, -y: usize, -lang: Lang, - -pub fn init(allocator: Allocator, buffer: *TerminalBuffer, max_length: usize, lang: Lang) !Desktop { - return .{ - .allocator = allocator, - .buffer = buffer, - .environments = try EnvironmentList.initCapacity(allocator, max_length), - .current = 0, - .visible_length = 0, - .x = 0, - .y = 0, - .lang = lang, - }; -} - -pub fn deinit(self: Desktop) void { - for (self.environments.items) |*environment| { - if (environment.entry_ini) |*entry_ini| entry_ini.deinit(); - if (environment.xdg_desktop_names) |desktop_name| self.allocator.free(desktop_name); - self.allocator.free(environment.xdg_session_desktop); - } - - self.environments.deinit(); -} - -pub fn position(self: *Desktop, x: usize, y: usize, visible_length: usize) void { - self.x = x; - self.y = y; - self.visible_length = visible_length; -} - -pub fn addEnvironment(self: *Desktop, entry: DesktopEntry, xdg_session_desktop: []const u8, display_server: DisplayServer) !void { - var xdg_desktop_names: ?[:0]const u8 = null; - if (entry.DesktopNames) |desktop_names| { - const desktop_names_z = try self.allocator.dupeZ(u8, desktop_names); - for (desktop_names_z) |*c| { - if (c.* == ';') c.* = ':'; - } - xdg_desktop_names = desktop_names_z; - } - - errdefer { - if (xdg_desktop_names) |desktop_names| self.allocator.free(desktop_names); - } - - const session_desktop = try self.allocator.dupeZ(u8, xdg_session_desktop); - errdefer self.allocator.free(session_desktop); - - try self.environments.append(.{ - .entry_ini = null, - .name = entry.Name, - .xdg_session_desktop = session_desktop, - .xdg_desktop_names = xdg_desktop_names, - .cmd = entry.Exec, - .specifier = switch (display_server) { - .wayland => self.lang.wayland, - .x11 => self.lang.x11, - else => self.lang.other, - }, - .display_server = display_server, - }); - - self.current = self.environments.items.len - 1; -} - -pub fn addEnvironmentWithIni(self: *Desktop, entry_ini: Ini(Entry), xdg_session_desktop: []const u8, display_server: DisplayServer) !void { - const entry = entry_ini.data.@"Desktop Entry"; - var xdg_desktop_names: ?[:0]const u8 = null; - if (entry.DesktopNames) |desktop_names| { - const desktop_names_z = try self.allocator.dupeZ(u8, desktop_names); - for (desktop_names_z) |*c| { - if (c.* == ';') c.* = ':'; - } - xdg_desktop_names = desktop_names_z; - } - - errdefer { - if (xdg_desktop_names) |desktop_names| self.allocator.free(desktop_names); - } - - const session_desktop = try self.allocator.dupeZ(u8, xdg_session_desktop); - errdefer self.allocator.free(session_desktop); - - try self.environments.append(.{ - .entry_ini = entry_ini, - .name = entry.Name, - .xdg_session_desktop = session_desktop, - .xdg_desktop_names = xdg_desktop_names, - .cmd = entry.Exec, - .specifier = switch (display_server) { - .wayland => self.lang.wayland, - .x11 => self.lang.x11, - else => self.lang.other, - }, - .display_server = display_server, - }); - - self.current = self.environments.items.len - 1; -} - -pub fn crawl(self: *Desktop, path: []const u8, display_server: DisplayServer) !void { - var iterable_directory = std.fs.openDirAbsolute(path, .{ .iterate = true }) catch return; - defer iterable_directory.close(); - - var iterator = iterable_directory.iterate(); - while (try iterator.next()) |item| { - if (!std.mem.eql(u8, std.fs.path.extension(item.name), ".desktop")) continue; - - const entry_path = try std.fmt.allocPrint(self.allocator, "{s}/{s}", .{ path, item.name }); - defer self.allocator.free(entry_path); - var entry_ini = Ini(Entry).init(self.allocator); - _ = try entry_ini.readFileToStruct(entry_path, "#", null); - errdefer entry_ini.deinit(); - - var xdg_session_desktop: []const u8 = undefined; - const maybe_desktop_names = entry_ini.data.@"Desktop Entry".DesktopNames; - if (maybe_desktop_names) |desktop_names| { - xdg_session_desktop = std.mem.sliceTo(desktop_names, ';'); - } else { - // if DesktopNames is empty, we'll take the name of the session file - xdg_session_desktop = std.fs.path.stem(item.name); - } - - try self.addEnvironmentWithIni(entry_ini, xdg_session_desktop, display_server); - } -} - -pub fn handle(self: *Desktop, 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 => {}, - } - } - }, - } - } - - _ = termbox.tb_set_cursor(@intCast(self.x + 2), @intCast(self.y)); -} - -pub fn draw(self: Desktop) void { - const environment = self.environments.items[self.current]; - - const length = @min(environment.name.len, self.visible_length - 3); - if (length == 0) return; - - const x = self.buffer.box_x + self.buffer.margin_box_h; - const y = self.buffer.box_y + self.buffer.margin_box_v + 2; - self.buffer.drawLabel(environment.specifier, x, y); - - _ = 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); - - self.buffer.drawLabel(environment.name, self.x + 2, self.y); -} - -fn goLeft(self: *Desktop) void { - if (self.current == 0) { - self.current = self.environments.items.len - 1; - return; - } - - self.current -= 1; -} - -fn goRight(self: *Desktop) void { - if (self.current == self.environments.items.len - 1) { - self.current = 0; - return; - } - - self.current += 1; -} diff --git a/src/tui/components/Session.zig b/src/tui/components/Session.zig new file mode 100644 index 0000000..f994ca2 --- /dev/null +++ b/src/tui/components/Session.zig @@ -0,0 +1,153 @@ +const std = @import("std"); +const TerminalBuffer = @import("../TerminalBuffer.zig"); +const enums = @import("../../enums.zig"); +const generic = @import("generic.zig"); +const Ini = @import("zigini").Ini; +const Lang = @import("../../config/Lang.zig"); + +const Allocator = std.mem.Allocator; + +const DisplayServer = enums.DisplayServer; + +const EnvironmentLabel = generic.CyclableLabel(Environment); + +const Session = @This(); + +pub const Environment = struct { + entry_ini: ?Ini(Entry) = null, + name: [:0]const u8 = "", + xdg_session_desktop: [:0]const u8 = "", + xdg_desktop_names: ?[:0]const u8 = "", + cmd: []const u8 = "", + specifier: []const u8 = "", + display_server: DisplayServer = .wayland, +}; + +const DesktopEntry = struct { + Exec: []const u8 = "", + Name: [:0]const u8 = "", + DesktopNames: ?[]const u8 = null, +}; + +pub const Entry = struct { @"Desktop Entry": DesktopEntry = .{} }; + +label: EnvironmentLabel, +lang: Lang, + +pub fn init(allocator: Allocator, buffer: *TerminalBuffer, max_length: usize, lang: Lang) !Session { + return .{ + .label = try EnvironmentLabel.init(allocator, buffer, max_length, drawItem), + .lang = lang, + }; +} + +pub fn deinit(self: Session) void { + for (self.label.list.items) |*environment| { + if (environment.entry_ini) |*entry_ini| entry_ini.deinit(); + if (environment.xdg_desktop_names) |desktop_name| self.label.allocator.free(desktop_name); + self.label.allocator.free(environment.xdg_session_desktop); + } + + self.label.deinit(); +} + +pub fn addEnvironment(self: *Session, entry: DesktopEntry, xdg_session_desktop: []const u8, display_server: DisplayServer) !void { + var xdg_desktop_names: ?[:0]const u8 = null; + if (entry.DesktopNames) |desktop_names| { + const desktop_names_z = try self.label.allocator.dupeZ(u8, desktop_names); + for (desktop_names_z) |*c| { + if (c.* == ';') c.* = ':'; + } + xdg_desktop_names = desktop_names_z; + } + + errdefer { + if (xdg_desktop_names) |desktop_names| self.label.allocator.free(desktop_names); + } + + const session_desktop = try self.label.allocator.dupeZ(u8, xdg_session_desktop); + errdefer self.label.allocator.free(session_desktop); + + try self.label.addItem(.{ + .entry_ini = null, + .name = entry.Name, + .xdg_session_desktop = session_desktop, + .xdg_desktop_names = xdg_desktop_names, + .cmd = entry.Exec, + .specifier = switch (display_server) { + .wayland => self.lang.wayland, + .x11 => self.lang.x11, + else => self.lang.other, + }, + .display_server = display_server, + }); +} + +pub fn addEnvironmentWithIni(self: *Session, entry_ini: Ini(Entry), xdg_session_desktop: []const u8, display_server: DisplayServer) !void { + const entry = entry_ini.data.@"Desktop Entry"; + var xdg_desktop_names: ?[:0]const u8 = null; + if (entry.DesktopNames) |desktop_names| { + const desktop_names_z = try self.label.allocator.dupeZ(u8, desktop_names); + for (desktop_names_z) |*c| { + if (c.* == ';') c.* = ':'; + } + xdg_desktop_names = desktop_names_z; + } + + errdefer { + if (xdg_desktop_names) |desktop_names| self.label.allocator.free(desktop_names); + } + + const session_desktop = try self.label.allocator.dupeZ(u8, xdg_session_desktop); + errdefer self.label.allocator.free(session_desktop); + + try self.label.addItem(.{ + .entry_ini = entry_ini, + .name = entry.Name, + .xdg_session_desktop = session_desktop, + .xdg_desktop_names = xdg_desktop_names, + .cmd = entry.Exec, + .specifier = switch (display_server) { + .wayland => self.lang.wayland, + .x11 => self.lang.x11, + else => self.lang.other, + }, + .display_server = display_server, + }); +} + +pub fn crawl(self: *Session, path: []const u8, display_server: DisplayServer) !void { + var iterable_directory = std.fs.openDirAbsolute(path, .{ .iterate = true }) catch return; + defer iterable_directory.close(); + + var iterator = iterable_directory.iterate(); + while (try iterator.next()) |item| { + if (!std.mem.eql(u8, std.fs.path.extension(item.name), ".desktop")) continue; + + const entry_path = try std.fmt.allocPrint(self.label.allocator, "{s}/{s}", .{ path, item.name }); + defer self.label.allocator.free(entry_path); + var entry_ini = Ini(Entry).init(self.label.allocator); + _ = try entry_ini.readFileToStruct(entry_path, "#", null); + errdefer entry_ini.deinit(); + + var xdg_session_desktop: []const u8 = undefined; + const maybe_desktop_names = entry_ini.data.@"Desktop Entry".DesktopNames; + if (maybe_desktop_names) |desktop_names| { + xdg_session_desktop = std.mem.sliceTo(desktop_names, ';'); + } else { + // if DesktopNames is empty, we'll take the name of the session file + xdg_session_desktop = std.fs.path.stem(item.name); + } + + try self.addEnvironmentWithIni(entry_ini, xdg_session_desktop, display_server); + } +} + +fn drawItem(label: EnvironmentLabel, environment: Environment, x: usize, y: usize) bool { + const length = @min(environment.name.len, label.visible_length - 3); + if (length == 0) return false; + + label.buffer.drawLabel(environment.specifier, x, y); + label.buffer.drawLabel(environment.name, label.x + 2, label.y); + return true; +} diff --git a/src/tui/components/generic.zig b/src/tui/components/generic.zig new file mode 100644 index 0000000..99d4a27 --- /dev/null +++ b/src/tui/components/generic.zig @@ -0,0 +1,105 @@ +const std = @import("std"); +const enums = @import("../../enums.zig"); +const interop = @import("../../interop.zig"); +const TerminalBuffer = @import("../TerminalBuffer.zig"); + +pub fn CyclableLabel(comptime ItemType: type) type { + return struct { + const Allocator = std.mem.Allocator; + const ItemList = std.ArrayList(ItemType); + const DrawItemFn = *const fn (Self, ItemType, usize, usize) bool; + + const termbox = interop.termbox; + + const Self = @This(); + + allocator: Allocator, + buffer: *TerminalBuffer, + list: ItemList, + current: usize, + visible_length: usize, + x: usize, + y: usize, + draw_item_fn: DrawItemFn, + + pub fn init(allocator: Allocator, buffer: *TerminalBuffer, max_length: usize, draw_item_fn: DrawItemFn) !Self { + return .{ + .allocator = allocator, + .buffer = buffer, + .list = try ItemList.initCapacity(allocator, max_length), + .current = 0, + .visible_length = 0, + .x = 0, + .y = 0, + .draw_item_fn = draw_item_fn, + }; + } + + pub fn deinit(self: Self) void { + self.list.deinit(); + } + + pub fn position(self: *Self, x: usize, y: usize, visible_length: usize) void { + self.x = x; + self.y = y; + self.visible_length = visible_length; + } + + pub fn addItem(self: *Self, item: ItemType) !void { + try self.list.append(item); + 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 => {}, + } + } + }, + } + } + + _ = termbox.tb_set_cursor(@intCast(self.x + 2), @intCast(self.y)); + } + + pub fn draw(self: Self) void { + 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 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); + } + + fn goLeft(self: *Self) void { + if (self.current == 0) { + self.current = self.list.items.len - 1; + return; + } + + self.current -= 1; + } + + fn goRight(self: *Self) void { + if (self.current == self.list.items.len - 1) { + self.current = 0; + return; + } + + self.current += 1; + } + }; +} From a3935252125cc8567493481ed090dcf87e02fc04 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Wed, 31 Jul 2024 13:58:49 +0200 Subject: [PATCH 065/530] Support multiple info lines in UI Signed-off-by: AnErrupTion --- src/enums.zig | 1 + src/main.zig | 96 +++++++++++++++++++-------------- src/tui/TerminalBuffer.zig | 8 ++- src/tui/components/InfoLine.zig | 41 +++++++------- src/tui/components/Session.zig | 2 +- src/tui/components/generic.zig | 11 ++-- 6 files changed, 94 insertions(+), 65 deletions(-) diff --git a/src/enums.zig b/src/enums.zig index d62673b..84b011e 100644 --- a/src/enums.zig +++ b/src/enums.zig @@ -12,6 +12,7 @@ pub const DisplayServer = enum { }; pub const Input = enum { + info_line, session, login, password, diff --git a/src/main.zig b/src/main.zig index e5ea380..6f75c8a 100644 --- a/src/main.zig +++ b/src/main.zig @@ -84,8 +84,7 @@ pub fn main() !void { var config: Config = undefined; var lang: Lang = undefined; var save: Save = undefined; - var info_line = InfoLine.init(allocator); - defer info_line.deinit(); + var config_load_failed = false; if (res.args.help != 0) { try clap.help(stderr, clap.Help, ¶ms, .{}); @@ -123,8 +122,7 @@ pub fn main() !void { defer allocator.free(config_path); config = config_ini.readFileToStruct(config_path, comment_characters, migrator.configFieldHandler) catch _config: { - // We're using a literal error message here since the language file hasn't yet been loaded - try info_line.addMessage("unable to parse config file", @intCast(interop.termbox.TB_DEFAULT), @intCast(interop.termbox.TB_RED | interop.termbox.TB_BOLD)); + config_load_failed = true; break :_config Config{}; }; @@ -156,8 +154,7 @@ pub fn main() !void { const config_path = build_options.data_directory ++ "/config.ini"; config = config_ini.readFileToStruct(config_path, comment_characters, migrator.configFieldHandler) catch _config: { - // literal error message, due to language file not yet available - try info_line.addMessage("unable to parse config file", @intCast(interop.termbox.TB_DEFAULT), @intCast(interop.termbox.TB_RED | interop.termbox.TB_BOLD)); + config_load_failed = true; break :_config Config{}; }; @@ -189,22 +186,8 @@ pub fn main() !void { shutdown_cmd = try temporary_allocator.dupe(u8, config.shutdown_cmd); restart_cmd = try temporary_allocator.dupe(u8, config.restart_cmd); - if (!build_options.enable_x11_support) try info_line.addMessage(lang.no_x11_support, config.bg, config.fg); - interop.setNumlock(config.numlock) catch {}; - if (config.initial_info_text) |text| { - try info_line.addMessage(text, config.bg, 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 { - try info_line.addMessage(lang.err_hostname, config.error_bg, config.error_fg); - break :get_host_name; - }; - try info_line.addMessage(hostname, config.bg, config.fg); - } - // Initialize termbox _ = termbox.tb_init(); defer _ = termbox.tb_shutdown(); @@ -225,9 +208,8 @@ pub fn main() !void { // Initialize terminal buffer const labels_max_length = @max(lang.login.len, lang.password.len); - // Get a random seed for the PRNG (used by animations) var seed: u64 = undefined; - try std.posix.getrandom(std.mem.asBytes(&seed)); + try std.posix.getrandom(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(); @@ -235,6 +217,13 @@ pub fn main() !void { var buffer = TerminalBuffer.init(config, labels_max_length, random); // Initialize components + var info_line = try InfoLine.init(allocator, &buffer, 255); + defer info_line.deinit(); + + if (config_load_failed) { + try info_line.addMessage("unable to parse config file", config.error_bg, config.error_fg); + } + var session = try Session.init(allocator, &buffer, config.max_desktop_len, lang); defer session.deinit(); @@ -248,6 +237,20 @@ pub fn main() !void { try info_line.addMessage(lang.err_alloc, config.error_bg, config.error_fg); }; } + } else { + try info_line.addMessage(lang.no_x11_support, config.bg, config.fg); + } + + if (config.initial_info_text) |text| { + try info_line.addMessage(text, config.bg, 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 { + try info_line.addMessage(lang.err_hostname, config.error_bg, config.error_fg); + break :get_host_name; + }; + try info_line.addMessage(hostname, config.bg, config.fg); } try session.crawl(config.waylandsessions, .wayland); @@ -281,11 +284,13 @@ pub fn main() !void { buffer.drawBoxCenter(!config.hide_borders, config.blank_box); const coordinates = buffer.calculateComponentCoordinates(); + info_line.label.position(coordinates.start_x, coordinates.y, coordinates.full_visible_length); session.label.position(coordinates.x, coordinates.y + 2, coordinates.visible_length); login.position(coordinates.x, coordinates.y + 4, coordinates.visible_length); 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.handle(null, insert_mode) catch { try info_line.addMessage(lang.err_alloc, config.error_bg, config.error_fg); @@ -404,6 +409,7 @@ pub fn main() !void { if (resolution_changed) { const coordinates = buffer.calculateComponentCoordinates(); + info_line.label.position(coordinates.start_x, coordinates.y, coordinates.full_visible_length); session.label.position(coordinates.x, coordinates.y + 2, coordinates.visible_length); login.position(coordinates.x, coordinates.y + 4, coordinates.visible_length); password.position(coordinates.x, coordinates.y + 6, coordinates.visible_length); @@ -412,6 +418,7 @@ pub fn main() !void { } switch (active_input) { + .info_line => info_line.label.handle(null, insert_mode), .session => session.label.handle(null, insert_mode), .login => login.handle(null, insert_mode) catch { try info_line.addMessage(lang.err_alloc, config.error_bg, config.error_fg); @@ -438,7 +445,7 @@ pub fn main() !void { buffer.drawLabel(lang.login, label_x, label_y + 4); buffer.drawLabel(lang.password, label_x, label_y + 6); - try info_line.draw(buffer); + info_line.label.draw(); if (!config.hide_key_hints) { var length: usize = 0; @@ -489,22 +496,22 @@ pub fn main() !void { buffer.drawLabel(label_txt, buffer.box_x, buffer.box_y + buffer.box_height); } - draw_lock_state: { - const lock_state = interop.getLockState(config.console_dev) catch { - try info_line.addMessage(lang.err_console_dev, config.error_bg, config.error_fg); - break :draw_lock_state; - }; + // draw_lock_state: { + // const lock_state = interop.getLockState(config.console_dev) catch { + // try info_line.addMessage(lang.err_console_dev, config.error_bg, config.error_fg); + // break :draw_lock_state; + // }; - var lock_state_x = buffer.width - @min(buffer.width, lang.numlock.len); - const lock_state_y: usize = if (config.clock != null) 1 else 0; + // var lock_state_x = buffer.width - @min(buffer.width, lang.numlock.len); + // const lock_state_y: usize = if (config.clock != null) 1 else 0; - if (lock_state.numlock) buffer.drawLabel(lang.numlock, lock_state_x, lock_state_y); + // 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); - } - } + // 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.draw(); @@ -595,13 +602,15 @@ pub fn main() !void { }, termbox.TB_KEY_CTRL_K, termbox.TB_KEY_ARROW_UP => { active_input = switch (active_input) { - .session, .login => .session, + .session, .info_line => .info_line, + .login => .session, .password => .login, }; update = true; }, termbox.TB_KEY_CTRL_J, termbox.TB_KEY_ARROW_DOWN => { active_input = switch (active_input) { + .info_line => .session, .session => .login, .login, .password => .password, }; @@ -609,15 +618,17 @@ pub fn main() !void { }, termbox.TB_KEY_TAB => { active_input = switch (active_input) { + .info_line => .session, .session => .login, .login => .password, - .password => .session, + .password => .info_line, }; update = true; }, termbox.TB_KEY_BACK_TAB => { active_input = switch (active_input) { - .session => .password, + .info_line => .password, + .session => .info_line, .login => .session, .password => .login, }; @@ -647,7 +658,7 @@ pub fn main() !void { try info_line.addMessage(lang.authenticating, config.bg, config.fg); InfoLine.clearRendered(allocator, buffer) catch {}; - try info_line.draw(buffer); + info_line.label.draw(); _ = termbox.tb_present(); session_pid = try std.posix.fork(); @@ -691,7 +702,8 @@ pub fn main() !void { switch (event.ch) { 'k' => { active_input = switch (active_input) { - .session, .login => .session, + .session, .info_line => .info_line, + .login => .session, .password => .login, }; update = true; @@ -699,6 +711,7 @@ pub fn main() !void { }, 'j' => { active_input = switch (active_input) { + .info_line => .session, .session => .login, .login, .password => .password, }; @@ -715,6 +728,7 @@ pub fn main() !void { } switch (active_input) { + .info_line => info_line.label.handle(&event, insert_mode), .session => session.label.handle(&event, insert_mode), .login => login.handle(&event, insert_mode) catch { try info_line.addMessage(lang.err_alloc, config.error_bg, config.error_fg); diff --git a/src/tui/TerminalBuffer.zig b/src/tui/TerminalBuffer.zig index 71bf8aa..2d36a54 100644 --- a/src/tui/TerminalBuffer.zig +++ b/src/tui/TerminalBuffer.zig @@ -142,17 +142,23 @@ pub fn drawBoxCenter(self: *TerminalBuffer, show_borders: bool, blank_box: bool) } pub fn calculateComponentCoordinates(self: TerminalBuffer) struct { + start_x: usize, x: usize, y: usize, + full_visible_length: usize, visible_length: usize, } { - const x = self.box_x + self.margin_box_h + self.labels_max_length + 1; + 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, }; } diff --git a/src/tui/components/InfoLine.zig b/src/tui/components/InfoLine.zig index 1c41474..695c8f5 100644 --- a/src/tui/components/InfoLine.zig +++ b/src/tui/components/InfoLine.zig @@ -1,6 +1,11 @@ const std = @import("std"); -const utils = @import("../utils.zig"); const TerminalBuffer = @import("../TerminalBuffer.zig"); +const generic = @import("generic.zig"); +const utils = @import("../utils.zig"); + +const Allocator = std.mem.Allocator; + +const MessageLabel = generic.CyclableLabel(Message); const InfoLine = @This(); @@ -10,24 +15,23 @@ const Message = struct { bg: u16, fg: u16, }; -const MessageList = std.ArrayList(Message); -messages: MessageList, +label: MessageLabel, -pub fn init(allocator: std.mem.Allocator) InfoLine { +pub fn init(allocator: Allocator, buffer: *TerminalBuffer, max_length: usize) !InfoLine { return .{ - .messages = MessageList.init(allocator), + .label = try MessageLabel.init(allocator, buffer, max_length, drawItem), }; } pub fn deinit(self: InfoLine) void { - self.messages.deinit(); + self.label.deinit(); } pub fn addMessage(self: *InfoLine, text: []const u8, bg: u16, fg: u16) !void { if (text.len == 0) return; - try self.messages.append(.{ + try self.label.addItem(.{ .width = try utils.strWidth(text), .text = text, .bg = bg, @@ -35,18 +39,7 @@ pub fn addMessage(self: *InfoLine, text: []const u8, bg: u16, fg: u16) !void { }); } -pub fn draw(self: InfoLine, buffer: TerminalBuffer) !void { - if (self.messages.items.len == 0) return; - - const entry = self.messages.getLast(); - if (entry.width == 0 or buffer.box_width <= entry.width) return; - - const label_y = buffer.box_y + buffer.margin_box_v; - const x = buffer.box_x + ((buffer.box_width - entry.width) / 2); - TerminalBuffer.drawColorLabel(entry.text, x, label_y, entry.fg, entry.bg); -} - -pub fn clearRendered(allocator: std.mem.Allocator, buffer: TerminalBuffer) !void { +pub fn clearRendered(allocator: Allocator, buffer: TerminalBuffer) !void { // Draw over the area const y = buffer.box_y + buffer.margin_box_v; const spaces = try allocator.alloc(u8, buffer.box_width); @@ -56,3 +49,13 @@ pub fn clearRendered(allocator: std.mem.Allocator, buffer: TerminalBuffer) !void buffer.drawLabel(spaces, buffer.box_x, y); } + +fn drawItem(label: *MessageLabel, message: Message, _: usize, _: usize) bool { + if (message.width == 0 or label.buffer.box_width <= message.width) return false; + + const x = label.buffer.box_x + ((label.buffer.box_width - message.width) / 2); + label.first_char_x = x; + + TerminalBuffer.drawColorLabel(message.text, x, label.y, message.fg, message.bg); + return true; +} diff --git a/src/tui/components/Session.zig b/src/tui/components/Session.zig index f994ca2..0127a10 100644 --- a/src/tui/components/Session.zig +++ b/src/tui/components/Session.zig @@ -143,7 +143,7 @@ pub fn crawl(self: *Session, path: []const u8, display_server: DisplayServer) !v } } -fn drawItem(label: EnvironmentLabel, environment: Environment, x: usize, y: usize) bool { +fn drawItem(label: *EnvironmentLabel, environment: Environment, x: usize, y: usize) bool { const length = @min(environment.name.len, label.visible_length - 3); if (length == 0) return false; diff --git a/src/tui/components/generic.zig b/src/tui/components/generic.zig index 99d4a27..b5252b6 100644 --- a/src/tui/components/generic.zig +++ b/src/tui/components/generic.zig @@ -7,7 +7,7 @@ pub fn CyclableLabel(comptime ItemType: type) type { return struct { const Allocator = std.mem.Allocator; const ItemList = std.ArrayList(ItemType); - const DrawItemFn = *const fn (Self, ItemType, usize, usize) bool; + const DrawItemFn = *const fn (*Self, ItemType, usize, usize) bool; const termbox = interop.termbox; @@ -20,6 +20,7 @@ pub fn CyclableLabel(comptime ItemType: type) type { visible_length: usize, x: usize, y: usize, + first_char_x: usize, draw_item_fn: DrawItemFn, pub fn init(allocator: Allocator, buffer: *TerminalBuffer, max_length: usize, draw_item_fn: DrawItemFn) !Self { @@ -31,6 +32,7 @@ pub fn CyclableLabel(comptime ItemType: type) type { .visible_length = 0, .x = 0, .y = 0, + .first_char_x = 0, .draw_item_fn = draw_item_fn, }; } @@ -43,6 +45,7 @@ pub fn CyclableLabel(comptime ItemType: type) type { self.x = x; self.y = y; self.visible_length = visible_length; + self.first_char_x = x + 2; } pub fn addItem(self: *Self, item: ItemType) !void { @@ -69,10 +72,12 @@ pub fn CyclableLabel(comptime ItemType: type) type { } } - _ = termbox.tb_set_cursor(@intCast(self.x + 2), @intCast(self.y)); + _ = termbox.tb_set_cursor(@intCast(self.first_char_x), @intCast(self.y)); } - pub fn draw(self: Self) void { + pub fn draw(self: *Self) void { + if (self.list.items.len == 0) return; + 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; From 46f9ddd5fc6a447ee1615d25bff2fea1e5b1cbbe Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Wed, 31 Jul 2024 14:01:44 +0200 Subject: [PATCH 066/530] Add translatable string for config parse error Signed-off-by: AnErrupTion --- src/config/Lang.zig | 1 + src/main.zig | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/config/Lang.zig b/src/config/Lang.zig index cba027c..5598c30 100644 --- a/src/config/Lang.zig +++ b/src/config/Lang.zig @@ -6,6 +6,7 @@ err_alloc: []const u8 = "failed memory allocation", err_bounds: []const u8 = "out-of-bounds index", err_brightness_change: []const u8 = "failed to change brightness", err_chdir: []const u8 = "failed to open home folder", +err_config: []const u8 = "unable to parse config file", err_console_dev: []const u8 = "failed to access console", err_dgn_oob: []const u8 = "log message", err_domain: []const u8 = "invalid domain", diff --git a/src/main.zig b/src/main.zig index 6f75c8a..af11f88 100644 --- a/src/main.zig +++ b/src/main.zig @@ -221,7 +221,7 @@ pub fn main() !void { defer info_line.deinit(); if (config_load_failed) { - try info_line.addMessage("unable to parse config file", config.error_bg, config.error_fg); + try info_line.addMessage(lang.err_config, config.error_bg, config.error_fg); } var session = try Session.init(allocator, &buffer, config.max_desktop_len, lang); From 48f28e40c456a6e95bf76ff5924cf380cc92fc57 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Wed, 31 Jul 2024 14:02:43 +0200 Subject: [PATCH 067/530] Fix an oopsie Signed-off-by: AnErrupTion --- res/lang/en.ini | 1 + res/lang/fr.ini | 1 + src/main.zig | 26 +++++++++++++------------- 3 files changed, 15 insertions(+), 13 deletions(-) diff --git a/res/lang/en.ini b/res/lang/en.ini index 21ccc30..285defa 100644 --- a/res/lang/en.ini +++ b/res/lang/en.ini @@ -6,6 +6,7 @@ err_alloc = failed memory allocation err_bounds = out-of-bounds index err_brightness_change = failed to change brightness err_chdir = failed to open home folder +err_config = unable to parse config file err_console_dev = failed to access console err_dgn_oob = log message err_domain = invalid domain diff --git a/res/lang/fr.ini b/res/lang/fr.ini index c6c6761..7582699 100644 --- a/res/lang/fr.ini +++ b/res/lang/fr.ini @@ -6,6 +6,7 @@ err_alloc = échec d'allocation mémoire err_bounds = indice hors-limite err_brightness_change = échec du changement de luminosité err_chdir = échec de l'ouverture du répertoire home +err_config = échec de l'analyse du fichier de configuration err_console_dev = échec d'accès à la console err_dgn_oob = message err_domain = domaine invalide diff --git a/src/main.zig b/src/main.zig index af11f88..8dfb630 100644 --- a/src/main.zig +++ b/src/main.zig @@ -496,22 +496,22 @@ pub fn main() !void { buffer.drawLabel(label_txt, buffer.box_x, buffer.box_y + buffer.box_height); } - // draw_lock_state: { - // const lock_state = interop.getLockState(config.console_dev) catch { - // try info_line.addMessage(lang.err_console_dev, config.error_bg, config.error_fg); - // break :draw_lock_state; - // }; + draw_lock_state: { + const lock_state = interop.getLockState(config.console_dev) catch { + try info_line.addMessage(lang.err_console_dev, config.error_bg, config.error_fg); + break :draw_lock_state; + }; - // var lock_state_x = buffer.width - @min(buffer.width, lang.numlock.len); - // const lock_state_y: usize = if (config.clock != null) 1 else 0; + var lock_state_x = buffer.width - @min(buffer.width, lang.numlock.len); + const lock_state_y: usize = if (config.clock != null) 1 else 0; - // if (lock_state.numlock) buffer.drawLabel(lang.numlock, lock_state_x, lock_state_y); + 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); - // } - // } + 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.draw(); From 548a411ae2baef8d20f9aeaf1a3a1897bed25015 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Wed, 31 Jul 2024 14:31:28 +0200 Subject: [PATCH 068/530] Remove maximum length config options + don't localize config parse error Signed-off-by: AnErrupTion --- res/config.ini | 7 +------ res/lang/en.ini | 1 - res/lang/fr.ini | 1 - src/config/Config.zig | 3 --- src/config/migrator.zig | 17 ++++++++------- src/main.zig | 37 +++++++++++++++++---------------- src/tui/components/InfoLine.zig | 4 ++-- src/tui/components/Session.zig | 4 ++-- src/tui/components/Text.zig | 4 ++-- src/tui/components/generic.zig | 4 ++-- 10 files changed, 38 insertions(+), 44 deletions(-) diff --git a/res/config.ini b/res/config.ini index d2a7eee..a8745ee 100644 --- a/res/config.ini +++ b/res/config.ini @@ -86,13 +86,8 @@ margin_box_v = 1 # Input boxes length input_len = 34 -# Max input sizes -max_desktop_len = 100 -max_login_len = 255 -max_password_len = 255 - # Input box active by default on startup -# Available inputs: session, login, password +# Available inputs: info_line, session, login, password default_input = login # Load the saved desktop and username diff --git a/res/lang/en.ini b/res/lang/en.ini index 285defa..21ccc30 100644 --- a/res/lang/en.ini +++ b/res/lang/en.ini @@ -6,7 +6,6 @@ err_alloc = failed memory allocation err_bounds = out-of-bounds index err_brightness_change = failed to change brightness err_chdir = failed to open home folder -err_config = unable to parse config file err_console_dev = failed to access console err_dgn_oob = log message err_domain = invalid domain diff --git a/res/lang/fr.ini b/res/lang/fr.ini index 7582699..c6c6761 100644 --- a/res/lang/fr.ini +++ b/res/lang/fr.ini @@ -6,7 +6,6 @@ err_alloc = échec d'allocation mémoire err_bounds = indice hors-limite err_brightness_change = échec du changement de luminosité err_chdir = échec de l'ouverture du répertoire home -err_config = échec de l'analyse du fichier de configuration err_console_dev = échec d'accès à la console err_dgn_oob = message err_domain = domaine invalide diff --git a/src/config/Config.zig b/src/config/Config.zig index c14db0f..7b7c384 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -28,9 +28,6 @@ lang: []const u8 = "en", load: bool = true, margin_box_h: u8 = 2, margin_box_v: u8 = 1, -max_desktop_len: u8 = 100, -max_login_len: u8 = 255, -max_password_len: u8 = 255, mcookie_cmd: [:0]const u8 = "/usr/bin/mcookie", min_refresh_delta: u16 = 5, numlock: bool = false, diff --git a/src/config/migrator.zig b/src/config/migrator.zig index 8268f37..569cdf8 100644 --- a/src/config/migrator.zig +++ b/src/config/migrator.zig @@ -5,14 +5,14 @@ const ini = @import("zigini"); const Save = @import("Save.zig"); const enums = @import("../enums.zig"); -var animate = false; +var maybe_animate: ?bool = null; pub var mapped_config_fields = false; pub fn configFieldHandler(_: std.mem.Allocator, field: ini.IniField) ?ini.IniField { if (std.mem.eql(u8, field.key, "animate")) { // The option doesn't exist anymore, but we save its value for "animation" - animate = std.mem.eql(u8, field.value, "true"); + maybe_animate = std.mem.eql(u8, field.value, "true"); mapped_config_fields = true; return null; @@ -59,9 +59,12 @@ pub fn configFieldHandler(_: std.mem.Allocator, field: ini.IniField) ?ini.IniFie return mapped_field; } - if (std.mem.eql(u8, field.key, "wayland_specifier")) { - // The option doesn't exist anymore - + if (std.mem.eql(u8, field.key, "wayland_specifier") or + std.mem.eql(u8, field.key, "max_desktop_len") or + std.mem.eql(u8, field.key, "max_login_len") or + std.mem.eql(u8, field.key, "max_password_len")) + { + // The options don't exist anymore mapped_config_fields = true; return null; } @@ -72,9 +75,9 @@ 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(animation: *enums.Animation) void { - if (!mapped_config_fields) return; + if (maybe_animate == null) return; - if (!animate) animation.* = .none; + if (!maybe_animate.?) animation.* = .none; } pub fn tryMigrateSaveFile(user_buf: *[32]u8, path: []const u8) Save { diff --git a/src/main.zig b/src/main.zig index 8dfb630..500938e 100644 --- a/src/main.zig +++ b/src/main.zig @@ -217,14 +217,15 @@ pub fn main() !void { var buffer = TerminalBuffer.init(config, labels_max_length, random); // Initialize components - var info_line = try InfoLine.init(allocator, &buffer, 255); + var info_line = InfoLine.init(allocator, &buffer); defer info_line.deinit(); if (config_load_failed) { - try info_line.addMessage(lang.err_config, config.error_bg, config.error_fg); + // 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); } - var session = try Session.init(allocator, &buffer, config.max_desktop_len, lang); + var session = Session.init(allocator, &buffer, lang); defer session.deinit(); session.addEnvironment(.{ .Name = lang.shell }, "", .shell) catch { @@ -256,10 +257,10 @@ pub fn main() !void { try session.crawl(config.waylandsessions, .wayland); if (build_options.enable_x11_support) try session.crawl(config.xsessions, .x11); - var login = try Text.init(allocator, &buffer, config.max_login_len, false, null); + var login = Text.init(allocator, &buffer, false, null); defer login.deinit(); - var password = try Text.init(allocator, &buffer, config.max_password_len, true, config.asterisk); + var password = Text.init(allocator, &buffer, true, config.asterisk); defer password.deinit(); var active_input = config.default_input; @@ -496,22 +497,22 @@ pub fn main() !void { buffer.drawLabel(label_txt, buffer.box_x, buffer.box_y + buffer.box_height); } - draw_lock_state: { - const lock_state = interop.getLockState(config.console_dev) catch { - try info_line.addMessage(lang.err_console_dev, config.error_bg, config.error_fg); - break :draw_lock_state; - }; + // draw_lock_state: { + // const lock_state = interop.getLockState(config.console_dev) catch { + // try info_line.addMessage(lang.err_console_dev, config.error_bg, config.error_fg); + // break :draw_lock_state; + // }; - var lock_state_x = buffer.width - @min(buffer.width, lang.numlock.len); - const lock_state_y: usize = if (config.clock != null) 1 else 0; + // var lock_state_x = buffer.width - @min(buffer.width, lang.numlock.len); + // const lock_state_y: usize = if (config.clock != null) 1 else 0; - if (lock_state.numlock) buffer.drawLabel(lang.numlock, lock_state_x, lock_state_y); + // 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); - } - } + // 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.draw(); diff --git a/src/tui/components/InfoLine.zig b/src/tui/components/InfoLine.zig index 695c8f5..d4ef7a4 100644 --- a/src/tui/components/InfoLine.zig +++ b/src/tui/components/InfoLine.zig @@ -18,9 +18,9 @@ const Message = struct { label: MessageLabel, -pub fn init(allocator: Allocator, buffer: *TerminalBuffer, max_length: usize) !InfoLine { +pub fn init(allocator: Allocator, buffer: *TerminalBuffer) InfoLine { return .{ - .label = try MessageLabel.init(allocator, buffer, max_length, drawItem), + .label = MessageLabel.init(allocator, buffer, drawItem), }; } diff --git a/src/tui/components/Session.zig b/src/tui/components/Session.zig index 0127a10..273c18a 100644 --- a/src/tui/components/Session.zig +++ b/src/tui/components/Session.zig @@ -34,9 +34,9 @@ pub const Entry = struct { @"Desktop Entry": DesktopEntry = .{} }; label: EnvironmentLabel, lang: Lang, -pub fn init(allocator: Allocator, buffer: *TerminalBuffer, max_length: usize, lang: Lang) !Session { +pub fn init(allocator: Allocator, buffer: *TerminalBuffer, lang: Lang) Session { return .{ - .label = try EnvironmentLabel.init(allocator, buffer, max_length, drawItem), + .label = EnvironmentLabel.init(allocator, buffer, drawItem), .lang = lang, }; } diff --git a/src/tui/components/Text.zig b/src/tui/components/Text.zig index 2d76b76..18223b2 100644 --- a/src/tui/components/Text.zig +++ b/src/tui/components/Text.zig @@ -22,8 +22,8 @@ y: usize, masked: bool, maybe_mask: ?u8, -pub fn init(allocator: Allocator, buffer: *TerminalBuffer, max_length: usize, masked: bool, maybe_mask: ?u8) !Text { - const text = try DynamicString.initCapacity(allocator, max_length); +pub fn init(allocator: Allocator, buffer: *TerminalBuffer, masked: bool, maybe_mask: ?u8) Text { + const text = DynamicString.init(allocator); return .{ .allocator = allocator, diff --git a/src/tui/components/generic.zig b/src/tui/components/generic.zig index b5252b6..215a876 100644 --- a/src/tui/components/generic.zig +++ b/src/tui/components/generic.zig @@ -23,11 +23,11 @@ pub fn CyclableLabel(comptime ItemType: type) type { first_char_x: usize, draw_item_fn: DrawItemFn, - pub fn init(allocator: Allocator, buffer: *TerminalBuffer, max_length: usize, draw_item_fn: DrawItemFn) !Self { + pub fn init(allocator: Allocator, buffer: *TerminalBuffer, draw_item_fn: DrawItemFn) Self { return .{ .allocator = allocator, .buffer = buffer, - .list = try ItemList.initCapacity(allocator, max_length), + .list = ItemList.init(allocator), .current = 0, .visible_length = 0, .x = 0, From 598fa6a505501914b73ac6227bebb9775a242ba8 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Wed, 31 Jul 2024 14:32:39 +0200 Subject: [PATCH 069/530] I need to stop doing this Signed-off-by: AnErrupTion --- src/main.zig | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/src/main.zig b/src/main.zig index 500938e..d646caa 100644 --- a/src/main.zig +++ b/src/main.zig @@ -497,22 +497,22 @@ pub fn main() !void { buffer.drawLabel(label_txt, buffer.box_x, buffer.box_y + buffer.box_height); } - // draw_lock_state: { - // const lock_state = interop.getLockState(config.console_dev) catch { - // try info_line.addMessage(lang.err_console_dev, config.error_bg, config.error_fg); - // break :draw_lock_state; - // }; + draw_lock_state: { + const lock_state = interop.getLockState(config.console_dev) catch { + try info_line.addMessage(lang.err_console_dev, config.error_bg, config.error_fg); + break :draw_lock_state; + }; - // var lock_state_x = buffer.width - @min(buffer.width, lang.numlock.len); - // const lock_state_y: usize = if (config.clock != null) 1 else 0; + var lock_state_x = buffer.width - @min(buffer.width, lang.numlock.len); + const lock_state_y: usize = if (config.clock != null) 1 else 0; - // if (lock_state.numlock) buffer.drawLabel(lang.numlock, lock_state_x, lock_state_y); + 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); - // } - // } + 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.draw(); From 6df91cac12cfb5e054782a47dcaa6acd042e0a88 Mon Sep 17 00:00:00 2001 From: ello <123702615+elloskelling@users.noreply.github.com> Date: Wed, 31 Jul 2024 19:49:19 +0000 Subject: [PATCH 070/530] Add an animation timeout (#659) * added animation timeout * Updated animation timeout to u12 * updated config comment to reflect the new range for animation timeout --- res/config.ini | 5 +++++ src/config/Config.zig | 1 + src/main.zig | 31 ++++++++++++++++++++++++++----- 3 files changed, 32 insertions(+), 5 deletions(-) diff --git a/res/config.ini b/res/config.ini index a8745ee..864a30d 100644 --- a/res/config.ini +++ b/res/config.ini @@ -4,6 +4,11 @@ # matrix -> CMatrix animation = none +# Stop the animation after some time +# 0 -> Run forever (default) +# 1..2e12 -> Stop the animation after this many seconds +animation_timeout_sec = 0 + # Format string for clock in top right corner (see strftime specification). Example: %c # If null, the clock won't be shown clock = null diff --git a/src/config/Config.zig b/src/config/Config.zig index 7b7c384..95b0771 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -57,3 +57,4 @@ brightness_down_key: []const u8 = "F5", brightness_up_key: []const u8 = "F6", brightnessctl: []const u8 = "/usr/bin/brightnessctl", brightness_change: []const u8 = "10", +animation_timeout_sec: u12 = 0, diff --git a/src/main.zig b/src/main.zig index d646caa..4696644 100644 --- a/src/main.zig +++ b/src/main.zig @@ -65,6 +65,12 @@ pub fn main() !void { var gpa = std.heap.GeneralPurposeAllocator(.{}){}; defer _ = gpa.deinit(); + // to be able to stop the animation after some time + + var tv_zero: std.c.timeval = undefined; + _ = std.c.gettimeofday(&tv_zero, null); + var animation_timed_out: bool = false; + const allocator = gpa.allocator(); // Load arguments @@ -384,10 +390,12 @@ pub fn main() !void { if (auth_fails < 10) { _ = termbox.tb_clear(); - switch (config.animation) { - .none => {}, - .doom => doom.draw(), - .matrix => matrix.draw(), + if (!animation_timed_out) { + switch (config.animation) { + .none => {}, + .doom => doom.draw(), + .matrix => matrix.draw(), + } } if (config.bigclock and buffer.box_height + (bigclock.HEIGHT + 2) * 2 < buffer.height) draw_big_clock: { @@ -533,8 +541,21 @@ 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 (animate) { + if (animate and !animation_timed_out) { timeout = config.min_refresh_delta; + + // check how long we have been running so we can turn off the animation + var tv: std.c.timeval = undefined; + _ = std.c.gettimeofday(&tv, null); + + if (config.animation_timeout_sec > 0 and tv.tv_sec - tv_zero.tv_sec > config.animation_timeout_sec) { + animation_timed_out = true; + switch (config.animation) { + .none => {}, + .doom => doom.deinit(), + .matrix => matrix.deinit(), + } + } } else if (config.bigclock and config.clock == null) { var tv: std.c.timeval = undefined; _ = std.c.gettimeofday(&tv, null); From ee0c00574af23588dd8272b3c6380103028ff433 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Wed, 31 Jul 2024 22:35:36 +0200 Subject: [PATCH 071/530] Patch resource files + add prefix directory Signed-off-by: AnErrupTion --- build.zig | 232 ++++++++++++++++++++++++++------------ res/config.ini | 24 ++-- res/ly-dinit | 3 +- res/ly-openrc | 6 +- res/ly-runit-service/conf | 4 +- res/ly-runit-service/run | 2 +- res/ly-s6/run | 2 +- res/ly.service | 8 +- res/wsetup.sh | 10 +- res/xsetup.sh | 28 ++--- src/config/Config.zig | 4 +- src/main.zig | 6 +- 12 files changed, 209 insertions(+), 120 deletions(-) diff --git a/build.zig b/build.zig index c2dbea9..9b217c3 100644 --- a/build.zig +++ b/build.zig @@ -1,6 +1,8 @@ const std = @import("std"); const builtin = @import("builtin"); +const PatchMap = std.StringHashMap([]const u8); + const min_zig_string = "0.12.0"; const current_zig = builtin.zig_version; @@ -15,25 +17,30 @@ comptime { const ly_version = std.SemanticVersion{ .major = 1, .minor = 1, .patch = 0 }; var dest_directory: []const u8 = undefined; -var data_directory: []const u8 = undefined; -var exe_name: []const u8 = undefined; +var config_directory: []const u8 = undefined; +var prefix_directory: []const u8 = undefined; +var executable_name: []const u8 = undefined; +var default_tty_str: []const u8 = undefined; const ProgressNode = if (current_zig.minor == 12) *std.Progress.Node else std.Progress.Node; pub fn build(b: *std.Build) !void { dest_directory = b.option([]const u8, "dest_directory", "Specify a destination directory for installation") orelse ""; - data_directory = b.option([]const u8, "data_directory", "Specify a default data directory (default is /etc/ly). This path gets embedded into the binary") orelse "/etc/ly"; - exe_name = b.option([]const u8, "name", "Specify installed executable file name (default is ly)") orelse "ly"; + 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"; - const bin_directory = try b.allocator.dupe(u8, data_directory); - data_directory = try std.fs.path.join(b.allocator, &[_][]const u8{ dest_directory, data_directory }); + const bin_directory = try b.allocator.dupe(u8, config_directory); + config_directory = try std.fs.path.join(b.allocator, &[_][]const u8{ dest_directory, config_directory }); const build_options = b.addOptions(); const version_str = try getVersionStr(b, "ly", ly_version); - const default_tty = b.option(u8, "default_tty", "Set the TTY (default is 2)") orelse 2; const enable_x11_support = b.option(bool, "enable_x11_support", "Enable X11 support (default is on)") orelse true; + const default_tty = b.option(u8, "default_tty", "Set the TTY (default is 2)") orelse 2; - build_options.addOption([]const u8, "data_directory", bin_directory); + default_tty_str = try std.fmt.allocPrint(b.allocator, "{d}", .{default_tty}); + + build_options.addOption([]const u8, "config_directory", bin_directory); build_options.addOption([]const u8, "version", version_str); build_options.addOption(u8, "tty", default_tty); build_options.addOption(bool, "enable_x11_support", enable_x11_support); @@ -138,40 +145,55 @@ pub fn ServiceInstaller(comptime init_system: InitSystem) type { return struct { pub fn make(step: *std.Build.Step, _: ProgressNode) !void { 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); + switch (init_system) { .Systemd => { - const service_path = try std.fs.path.join(allocator, &[_][]const u8{ dest_directory, "/usr/lib/systemd/system" }); + 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(); - try installFile("res/ly.service", service_dir, service_path, "ly.service", .{ .override_mode = 0o644 }); + const patched_service = try patchFile(allocator, "res/ly.service", patch_map); + try installText(patched_service, service_dir, service_path, "ly.service", .{ .mode = 0o644 }); }, .Openrc => { - const service_path = try std.fs.path.join(allocator, &[_][]const u8{ dest_directory, "/etc/init.d" }); + 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(); - try installFile("res/ly-openrc", service_dir, service_path, exe_name, .{ .override_mode = 0o755 }); + const patched_service = try patchFile(allocator, "res/ly-openrc", patch_map); + try installText(patched_service, service_dir, service_path, executable_name, .{ .mode = 0o755 }); }, .Runit => { - const service_path = try std.fs.path.join(allocator, &[_][]const u8{ dest_directory, "/etc/sv/ly" }); + 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 supervise_path = try std.fs.path.join(allocator, &[_][]const u8{ service_path, "supervise" }); - try installFile("res/ly-runit-service/conf", service_dir, service_path, "conf", .{}); + const patched_conf = try patchFile(allocator, "res/ly-runit-service/conf", patch_map); + try installText(patched_conf, service_dir, service_path, "conf", .{}); + try installFile("res/ly-runit-service/finish", service_dir, service_path, "finish", .{ .override_mode = 0o755 }); - try installFile("res/ly-runit-service/run", service_dir, service_path, "run", .{ .override_mode = 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 }); try std.fs.cwd().symLink("/run/runit/supervise.ly", supervise_path, .{}); 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, "/etc/s6/adminsv/default/contents.d" }); + 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(); @@ -179,21 +201,24 @@ pub fn ServiceInstaller(comptime init_system: InitSystem) type { const file = try admin_service_dir.createFile("ly-srv", .{}); file.close(); - const service_path = try std.fs.path.join(allocator, &[_][]const u8{ dest_directory, "/etc/s6/sv/ly-srv" }); + 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(); - try installFile("res/ly-s6/run", service_dir, service_path, "run", .{ .override_mode = 0o755 }); + const patched_run = try patchFile(allocator, "res/ly-s6/run", patch_map); + try installText(patched_run, service_dir, service_path, "run", .{ .mode = 0o755 }); + try installFile("res/ly-s6/type", service_dir, service_path, "type", .{}); }, .Dinit => { - const service_path = try std.fs.path.join(allocator, &[_][]const u8{ dest_directory, "/etc/dinit.d" }); + 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(); - try installFile("res/ly-dinit", service_dir, service_path, "ly", .{}); + const patched_service = try patchFile(allocator, "res/ly-dinit", patch_map); + try installText(patched_service, service_dir, service_path, "ly", .{}); }, } } @@ -201,17 +226,19 @@ pub fn ServiceInstaller(comptime init_system: InitSystem) type { } fn install_ly(allocator: std.mem.Allocator, install_config: bool) !void { - std.fs.cwd().makePath(data_directory) catch { - std.debug.print("warn: {s} already exists as a directory.\n", .{data_directory}); + const ly_config_directory = try std.fs.path.join(allocator, &[_][]const u8{ config_directory, "/ly" }); + + std.fs.cwd().makePath(ly_config_directory) catch { + std.debug.print("warn: {s} already exists as a directory.\n", .{ly_config_directory}); }; - const lang_path = try std.fs.path.join(allocator, &[_][]const u8{ data_directory, "/lang" }); - std.fs.cwd().makePath(lang_path) catch { - std.debug.print("warn: {s} already exists as a directory.\n", .{data_directory}); + const ly_lang_path = try std.fs.path.join(allocator, &[_][]const u8{ config_directory, "/ly/lang" }); + std.fs.cwd().makePath(ly_lang_path) catch { + std.debug.print("warn: {s} already exists as a directory.\n", .{config_directory}); }; { - const exe_path = try std.fs.path.join(allocator, &[_][]const u8{ dest_directory, "/usr/bin" }); + const exe_path = try std.fs.path.join(allocator, &[_][]const u8{ dest_directory, prefix_directory, "/bin" }); if (!std.mem.eql(u8, dest_directory, "")) { std.fs.cwd().makePath(exe_path) catch { std.debug.print("warn: {s} already exists as a directory.\n", .{exe_path}); @@ -221,44 +248,63 @@ fn install_ly(allocator: std.mem.Allocator, install_config: bool) !void { var executable_dir = std.fs.cwd().openDir(exe_path, .{}) catch unreachable; defer executable_dir.close(); - try installFile("zig-out/bin/ly", executable_dir, exe_path, exe_name, .{}); + try installFile("zig-out/bin/ly", executable_dir, exe_path, executable_name, .{}); } { - var config_dir = std.fs.cwd().openDir(data_directory, .{}) catch unreachable; + var config_dir = std.fs.cwd().openDir(ly_config_directory, .{}) catch unreachable; defer config_dir.close(); if (install_config) { - try installFile("res/config.ini", config_dir, data_directory, "config.ini", .{}); + 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); + + const patched_config = try patchFile(allocator, "res/config.ini", patch_map); + try installText(patched_config, config_dir, ly_config_directory, "config.ini", .{}); + } + + { + var patch_map = PatchMap.init(allocator); + defer patch_map.deinit(); + + try patch_map.put("$PREFIX_DIRECTORY", prefix_directory); + + const patched_xsetup = try patchFile(allocator, "res/xsetup.sh", patch_map); + const patched_wsetup = try patchFile(allocator, "res/wsetup.sh", patch_map); + + try installText(patched_xsetup, config_dir, ly_config_directory, "xsetup.sh", .{}); + try installText(patched_wsetup, config_dir, ly_config_directory, "wsetup.sh", .{}); } - try installFile("res/xsetup.sh", config_dir, data_directory, "xsetup.sh", .{}); - try installFile("res/wsetup.sh", config_dir, data_directory, "wsetup.sh", .{}); } { - var lang_dir = std.fs.cwd().openDir(lang_path, .{}) catch unreachable; + var lang_dir = std.fs.cwd().openDir(ly_lang_path, .{}) catch unreachable; defer lang_dir.close(); - try installFile("res/lang/cat.ini", lang_dir, lang_path, "cat.ini", .{}); - try installFile("res/lang/cs.ini", lang_dir, lang_path, "cs.ini", .{}); - try installFile("res/lang/de.ini", lang_dir, lang_path, "de.ini", .{}); - try installFile("res/lang/en.ini", lang_dir, lang_path, "en.ini", .{}); - try installFile("res/lang/es.ini", lang_dir, lang_path, "es.ini", .{}); - try installFile("res/lang/fr.ini", lang_dir, lang_path, "fr.ini", .{}); - try installFile("res/lang/it.ini", lang_dir, lang_path, "it.ini", .{}); - try installFile("res/lang/pl.ini", lang_dir, lang_path, "pl.ini", .{}); - try installFile("res/lang/pt.ini", lang_dir, lang_path, "pt.ini", .{}); - try installFile("res/lang/pt_BR.ini", lang_dir, lang_path, "pt_BR.ini", .{}); - try installFile("res/lang/ro.ini", lang_dir, lang_path, "ro.ini", .{}); - try installFile("res/lang/ru.ini", lang_dir, lang_path, "ru.ini", .{}); - try installFile("res/lang/sr.ini", lang_dir, lang_path, "sr.ini", .{}); - try installFile("res/lang/sv.ini", lang_dir, lang_path, "sv.ini", .{}); - try installFile("res/lang/tr.ini", lang_dir, lang_path, "tr.ini", .{}); - try installFile("res/lang/uk.ini", lang_dir, lang_path, "uk.ini", .{}); + try installFile("res/lang/cat.ini", lang_dir, ly_lang_path, "cat.ini", .{}); + try installFile("res/lang/cs.ini", lang_dir, ly_lang_path, "cs.ini", .{}); + try installFile("res/lang/de.ini", lang_dir, ly_lang_path, "de.ini", .{}); + try installFile("res/lang/en.ini", lang_dir, ly_lang_path, "en.ini", .{}); + try installFile("res/lang/es.ini", lang_dir, ly_lang_path, "es.ini", .{}); + try installFile("res/lang/fr.ini", lang_dir, ly_lang_path, "fr.ini", .{}); + try installFile("res/lang/it.ini", lang_dir, ly_lang_path, "it.ini", .{}); + try installFile("res/lang/pl.ini", lang_dir, ly_lang_path, "pl.ini", .{}); + try installFile("res/lang/pt.ini", lang_dir, ly_lang_path, "pt.ini", .{}); + try installFile("res/lang/pt_BR.ini", lang_dir, ly_lang_path, "pt_BR.ini", .{}); + try installFile("res/lang/ro.ini", lang_dir, ly_lang_path, "ro.ini", .{}); + try installFile("res/lang/ru.ini", lang_dir, ly_lang_path, "ru.ini", .{}); + try installFile("res/lang/sr.ini", lang_dir, ly_lang_path, "sr.ini", .{}); + try installFile("res/lang/sv.ini", lang_dir, ly_lang_path, "sv.ini", .{}); + try installFile("res/lang/tr.ini", lang_dir, ly_lang_path, "tr.ini", .{}); + try installFile("res/lang/uk.ini", lang_dir, ly_lang_path, "uk.ini", .{}); } { - const pam_path = try std.fs.path.join(allocator, &[_][]const u8{ dest_directory, "/etc/pam.d" }); + const pam_path = try std.fs.path.join(allocator, &[_][]const u8{ dest_directory, config_directory, "/pam.d" }); if (!std.mem.eql(u8, dest_directory, "")) { std.fs.cwd().makePath(pam_path) catch { std.debug.print("warn: {s} already exists as a directory.\n", .{pam_path}); @@ -275,23 +321,23 @@ fn install_ly(allocator: std.mem.Allocator, install_config: bool) !void { pub fn uninstallall(step: *std.Build.Step, _: ProgressNode) !void { const allocator = step.owner.allocator; - try deleteTree(allocator, data_directory, "ly data directory not found"); + try deleteTree(allocator, config_directory, "/ly", "ly data directory not found"); - const exe_path = try std.fs.path.join(allocator, &[_][]const u8{ dest_directory, "/usr/bin/", exe_name }); + const exe_path = try std.fs.path.join(allocator, &[_][]const u8{ dest_directory, prefix_directory, "/bin/", executable_name }); var success = true; std.fs.cwd().deleteFile(exe_path) catch { - std.debug.print("warn: ly executable not found.", .{}); + 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, "/etc/pam.d/ly", "ly pam file not found"); - try deleteFile(allocator, "/usr/lib/systemd/system/ly.service", "systemd service not found"); - try deleteFile(allocator, "/etc/init.d/ly", "openrc service not found"); - try deleteTree(allocator, "/etc/sv/ly", "runit service not found"); - try deleteTree(allocator, "/etc/s6/sv/ly-srv", "s6 service not found"); - try deleteFile(allocator, "/etc/s6/adminsv/default/contents.d/ly-srv", "s6 admin service not found"); - try deleteFile(allocator, "/etc/dinit.d/ly", "dinit service not found"); + try deleteFile(allocator, config_directory, "/pam.d/ly", "ly pam file not found"); + try deleteFile(allocator, prefix_directory, "/lib/systemd/system/ly.service", "systemd service not found"); + try deleteFile(allocator, config_directory, "/init.d/ly", "openrc service not found"); + try deleteTree(allocator, config_directory, "/sv/ly", "runit service not found"); + 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 deleteFile(allocator, config_directory, "/dinit.d/ly", "dinit service not found"); } fn getVersionStr(b: *std.Build, name: []const u8, version: std.SemanticVersion) ![]const u8 { @@ -360,34 +406,78 @@ fn installFile( 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(); + + const reader = file.reader(); + var text = try reader.readAllAlloc(allocator, std.math.maxInt(u16)); + + 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( + text: []const u8, + destination_directory: std.fs.Dir, + destination_directory_path: []const u8, + destination_file: []const u8, + options: std.fs.File.CreateFlags, +) !void { + var file = try destination_directory.createFile(destination_file, options); + defer file.close(); + + const writer = file.writer(); + try writer.writeAll(text); + + std.debug.print("info: installed {s}/{s}\n", .{ destination_directory_path, destination_file }); +} + fn deleteFile( allocator: std.mem.Allocator, + prefix: []const u8, file: []const u8, warning: []const u8, ) !void { - const path = try std.fs.path.join(allocator, &[_][]const u8{ dest_directory, file }); - var success = true; + const path = try std.fs.path.join(allocator, &[_][]const u8{ dest_directory, prefix, file }); - std.fs.cwd().deleteFile(path) catch { - std.debug.print("warn: {s}\n", .{warning}); - success = false; + std.fs.cwd().deleteFile(path) catch |err| { + if (err == error.FileNotFound) { + std.debug.print("warn: {s}\n", .{warning}); + return; + } + + return err; }; - if (success) std.debug.print("info: deleted {s}\n", .{path}); + std.debug.print("info: deleted {s}\n", .{path}); } fn deleteTree( allocator: std.mem.Allocator, + prefix: []const u8, directory: []const u8, warning: []const u8, ) !void { - const path = try std.fs.path.join(allocator, &[_][]const u8{ dest_directory, directory }); - var success = true; + const path = try std.fs.path.join(allocator, &[_][]const u8{ dest_directory, prefix, directory }); - std.fs.cwd().deleteTree(path) catch { - std.debug.print("warn: {s}\n", .{warning}); - success = false; + var dir = std.fs.cwd().openDir(path, .{}) catch |err| { + if (err == error.FileNotFound) { + std.debug.print("warn: {s}\n", .{warning}); + return; + } + + return err; }; + dir.close(); - if (success) std.debug.print("info: deleted {s}\n", .{path}); + try std.fs.cwd().deleteTree(path); + + std.debug.print("info: deleted {s}\n", .{path}); } diff --git a/res/config.ini b/res/config.ini index a8745ee..b077e44 100644 --- a/res/config.ini +++ b/res/config.ini @@ -100,7 +100,7 @@ save = true # New save files are now loaded from the same directory as the config # Currently used to migrate old save files to the new version # File in which to save and load the default desktop and login -save_file = /etc/ly/save +save_file = $CONFIG_DIRECTORY/ly/save # Remove power management command hints hide_key_hints = false @@ -124,11 +124,11 @@ restart_cmd = /sbin/shutdown -r now sleep_cmd = null # Active language -# Available languages are found in /etc/ly/lang/ +# Available languages are found in $CONFIG_DIRECTORY/ly/lang/ lang = en # TTY in use -tty = 2 +tty = $DEFAULT_TTY # Console path console_dev = /dev/console @@ -147,35 +147,35 @@ numlock = false service_name = ly # Terminal reset command (tput is faster) -term_reset_cmd = /usr/bin/tput reset +term_reset_cmd = $PREFIX_DIRECTORY/bin/tput reset # Terminal restore cursor command -term_restore_cursor_cmd = /usr/bin/tput cnorm +term_restore_cursor_cmd = $PREFIX_DIRECTORY/bin/tput cnorm # Cookie generator -mcookie_cmd = /usr/bin/mcookie +mcookie_cmd = $PREFIX_DIRECTORY/bin/mcookie # Wayland setup command -wayland_cmd = /etc/ly/wsetup.sh +wayland_cmd = $CONFIG_DIRECTORY/ly/wsetup.sh # Wayland desktop environments -waylandsessions = /usr/share/wayland-sessions +waylandsessions = $PREFIX_DIRECTORY/share/wayland-sessions # xinitrc # If null, the xinitrc session will be hidden xinitrc = ~/.xinitrc # Xorg server command -x_cmd = /usr/bin/X +x_cmd = $PREFIX_DIRECTORY/bin/X # Xorg setup command -x_cmd_setup = /etc/ly/xsetup.sh +x_cmd_setup = $CONFIG_DIRECTORY/ly/xsetup.sh # Xorg xauthority edition tool -xauth_cmd = /usr/bin/xauth +xauth_cmd = $PREFIX_DIRECTORY/bin/xauth # Xorg desktop environments -xsessions = /usr/share/xsessions +xsessions = $PREFIX_DIRECTORY/share/xsessions # Brightness control brightness_down_key = F5 diff --git a/res/ly-dinit b/res/ly-dinit index cb2c620..a20a71f 100644 --- a/res/ly-dinit +++ b/res/ly-dinit @@ -1,8 +1,7 @@ type = process restart = true smooth-recovery = true -# note: /usr/bin/ly-dm when installing from pacman on artix, /usr/bin/ly when building from source -command = /usr/bin/ly +command = $PREFIX_DIRECTORY/bin/$EXE_NAME depends-on = loginready termsignal = HUP # ly needs access to the console while loginready already occupies it diff --git a/res/ly-openrc b/res/ly-openrc index 7021829..81da5d8 100644 --- a/res/ly-openrc +++ b/res/ly-openrc @@ -23,13 +23,13 @@ fi CONFTTY=$(cat /etc/ly/config.ini | sed -n 's/^tty.*=[^1-9]*// p') ## The execution vars -# If CONFTTY is empty then default to 2 -TTY="tty${CONFTTY:-2}" +# If CONFTTY is empty then default to $DEFAULT_TTY +TTY="tty${CONFTTY:-$DEFAULT_TTY}" TERM=linux BAUD=38400 # If we don't have getty then we should have agetty command=${commandB:-$commandUL} -command_args_foreground="-nl /usr/bin/ly $TTY $BAUD $TERM" +command_args_foreground="-nl $PREFIX_DIRECTORY/bin/$EXE_NAME $TTY $BAUD $TERM" depend() { after agetty diff --git a/res/ly-runit-service/conf b/res/ly-runit-service/conf index d4aad3b..76ceb87 100644 --- a/res/ly-runit-service/conf +++ b/res/ly-runit-service/conf @@ -8,5 +8,5 @@ fi BAUD_RATE=38400 TERM_NAME=linux -auxtty=$(/bin/cat /etc/ly/config.ini 2>/dev/null 1| /bin/sed -n 's/\(^[[:space:]]*tty[[:space:]]*=[[:space:]]*\)\([[:digit:]][[:digit:]]*\)\(.*\)/\2/p') -TTY=tty${auxtty:-2} +auxtty=$(/bin/cat $CONFIG_DIRECTORY/ly/config.ini 2>/dev/null 1| /bin/sed -n 's/\(^[[:space:]]*tty[[:space:]]*=[[:space:]]*\)\([[:digit:]][[:digit:]]*\)\(.*\)/\2/p') +TTY=tty${auxtty:-$DEFAULT_TTY} diff --git a/res/ly-runit-service/run b/res/ly-runit-service/run index 1e199c7..68fb2d6 100644 --- a/res/ly-runit-service/run +++ b/res/ly-runit-service/run @@ -10,4 +10,4 @@ elif [ -x /sbin/agetty -o -x /bin/agetty ]; then GETTY=agetty fi -exec setsid ${GETTY} ${GETTY_ARGS} -nl /usr/bin/ly "${TTY}" "${BAUD_RATE}" "${TERM_NAME}" +exec setsid ${GETTY} ${GETTY_ARGS} -nl $PREFIX_DIRECTORY/bin/$EXECUTABLE_NAME "${TTY}" "${BAUD_RATE}" "${TERM_NAME}" diff --git a/res/ly-s6/run b/res/ly-s6/run index bf64302..e63826f 100644 --- a/res/ly-s6/run +++ b/res/ly-s6/run @@ -1,2 +1,2 @@ #!/bin/execlineb -P -exec agetty -L -8 -n -l /usr/bin/ly tty2 115200 +exec agetty -L -8 -n -l $PREFIX_DIRECTORY/bin/$EXE_NAME tty$DEFAULT_TTY 115200 diff --git a/res/ly.service b/res/ly.service index 2fd120a..0b72699 100644 --- a/res/ly.service +++ b/res/ly.service @@ -1,14 +1,14 @@ [Unit] Description=TUI display manager After=systemd-user-sessions.service plymouth-quit-wait.service -After=getty@tty2.service -Conflicts=getty@tty2.service +After=getty@tty$DEFAULT_TTY.service +Conflicts=getty@tty$DEFAULT_TTY.service [Service] Type=idle -ExecStart=/usr/bin/ly +ExecStart=$PREFIX_DIRECTORY/bin/$EXECUTABLE_NAME StandardInput=tty -TTYPath=/dev/tty2 +TTYPath=/dev/tty$DEFAULT_TTY TTYReset=yes TTYVHangup=yes diff --git a/res/wsetup.sh b/res/wsetup.sh index fd3a583..bf8b7e3 100755 --- a/res/wsetup.sh +++ b/res/wsetup.sh @@ -10,7 +10,7 @@ case $SHELL in */bash) [ -z "$BASH" ] && exec $SHELL $0 "$@" set +o posix - [ -f /etc/profile ] && . /etc/profile + [ -f $CONFIG_DIRECTORY/profile ] && . $CONFIG_DIRECTORY/profile if [ -f $HOME/.bash_profile ]; then . $HOME/.bash_profile elif [ -f $HOME/.bash_login ]; then @@ -21,7 +21,7 @@ case $SHELL in ;; */zsh) [ -z "$ZSH_NAME" ] && exec $SHELL $0 "$@" - [ -d /etc/zsh ] && zdir=/etc/zsh || zdir=/etc + [ -d $CONFIG_DIRECTORY/zsh ] && zdir=$CONFIG_DIRECTORY/zsh || zdir=$CONFIG_DIRECTORY zhome=${ZDOTDIR:-$HOME} # zshenv is always sourced automatically. [ -f $zdir/zprofile ] && . $zdir/zprofile @@ -34,12 +34,12 @@ case $SHELL in # [t]cshrc is always sourced automatically. # Note that sourcing csh.login after .cshrc is non-standard. wlsess_tmp=`mktemp /tmp/wlsess-env-XXXXXX` - $SHELL -c "if (-f /etc/csh.login) source /etc/csh.login; if (-f ~/.login) source ~/.login; /bin/sh -c 'export -p' >! $wlsess_tmp" + $SHELL -c "if (-f $CONFIG_DIRECTORY/csh.login) source $CONFIG_DIRECTORY/csh.login; if (-f ~/.login) source ~/.login; /bin/sh -c 'export -p' >! $wlsess_tmp" . $wlsess_tmp rm -f $wlsess_tmp ;; */fish) - [ -f /etc/profile ] && . /etc/profile + [ -f $CONFIG_DIRECTORY/profile ] && . $CONFIG_DIRECTORY/profile [ -f $HOME/.profile ] && . $HOME/.profile xsess_tmp=`mktemp /tmp/xsess-env-XXXXXX` $SHELL --login -c "/bin/sh -c 'export -p' > $xsess_tmp" @@ -47,7 +47,7 @@ case $SHELL in rm -f $xsess_tmp ;; *) # Plain sh, ksh, and anything we do not know. - [ -f /etc/profile ] && . /etc/profile + [ -f $CONFIG_DIRECTORY/profile ] && . $CONFIG_DIRECTORY/profile [ -f $HOME/.profile ] && . $HOME/.profile ;; esac diff --git a/res/xsetup.sh b/res/xsetup.sh index 2c962f5..24d0a4a 100755 --- a/res/xsetup.sh +++ b/res/xsetup.sh @@ -10,7 +10,7 @@ case $SHELL in */bash) [ -z "$BASH" ] && exec $SHELL $0 "$@" set +o posix - [ -f /etc/profile ] && . /etc/profile + [ -f $CONFIG_DIRECTORY/profile ] && . $CONFIG_DIRECTORY/profile if [ -f $HOME/.bash_profile ]; then . $HOME/.bash_profile elif [ -f $HOME/.bash_login ]; then @@ -21,7 +21,7 @@ case $SHELL in ;; */zsh) [ -z "$ZSH_NAME" ] && exec $SHELL $0 "$@" - [ -d /etc/zsh ] && zdir=/etc/zsh || zdir=/etc + [ -d $CONFIG_DIRECTORY/zsh ] && zdir=$CONFIG_DIRECTORY/zsh || zdir=$CONFIG_DIRECTORY zhome=${ZDOTDIR:-$HOME} # zshenv is always sourced automatically. [ -f $zdir/zprofile ] && . $zdir/zprofile @@ -34,12 +34,12 @@ case $SHELL in # [t]cshrc is always sourced automatically. # Note that sourcing csh.login after .cshrc is non-standard. xsess_tmp=`mktemp /tmp/xsess-env-XXXXXX` - $SHELL -c "if (-f /etc/csh.login) source /etc/csh.login; if (-f ~/.login) source ~/.login; /bin/sh -c 'export -p' >! $xsess_tmp" + $SHELL -c "if (-f $CONFIG_DIRECTORY/csh.login) source $CONFIG_DIRECTORY/csh.login; if (-f ~/.login) source ~/.login; /bin/sh -c 'export -p' >! $xsess_tmp" . $xsess_tmp rm -f $xsess_tmp ;; */fish) - [ -f /etc/profile ] && . /etc/profile + [ -f $CONFIG_DIRECTORY/profile ] && . $CONFIG_DIRECTORY/profile [ -f $HOME/.profile ] && . $HOME/.profile xsess_tmp=`mktemp /tmp/xsess-env-XXXXXX` $SHELL --login -c "/bin/sh -c 'export -p' > $xsess_tmp" @@ -47,17 +47,17 @@ case $SHELL in rm -f $xsess_tmp ;; *) # Plain sh, ksh, and anything we do not know. - [ -f /etc/profile ] && . /etc/profile + [ -f $CONFIG_DIRECTORY/profile ] && . $CONFIG_DIRECTORY/profile [ -f $HOME/.profile ] && . $HOME/.profile ;; esac -[ -f /etc/xprofile ] && . /etc/xprofile +[ -f $CONFIG_DIRECTORY/xprofile ] && . $CONFIG_DIRECTORY/xprofile [ -f $HOME/.xprofile ] && . $HOME/.xprofile # run all system xinitrc shell scripts. -if [ -d /etc/X11/xinit/xinitrc.d ]; then - for i in /etc/X11/xinit/xinitrc.d/* ; do +if [ -d $CONFIG_DIRECTORY/X11/xinit/xinitrc.d ]; then + for i in $CONFIG_DIRECTORY/X11/xinit/xinitrc.d/* ; do if [ -x "$i" ]; then . "$i" fi @@ -67,8 +67,8 @@ fi # Load Xsession scripts # OPTIONFILE, USERXSESSION, USERXSESSIONRC and ALTUSERXSESSION are required # by the scripts to work -xsessionddir="/etc/X11/Xsession.d" -OPTIONFILE=/etc/X11/Xsession.options +xsessionddir="$CONFIG_DIRECTORY/X11/Xsession.d" +OPTIONFILE=$CONFIG_DIRECTORY/X11/Xsession.options USERXSESSION=$HOME/.xsession USERXSESSIONRC=$HOME/.xsessionrc ALTUSERXSESSION=$HOME/.Xsession @@ -83,12 +83,12 @@ if [ -d "$xsessionddir" ]; then done fi -if [ -d /etc/X11/Xresources ]; then - for i in /etc/X11/Xresources/*; do +if [ -d $CONFIG_DIRECTORY/X11/Xresources ]; then + for i in $CONFIG_DIRECTORY/X11/Xresources/*; do [ -f $i ] && xrdb -merge $i done -elif [ -f /etc/X11/Xresources ]; then - xrdb -merge /etc/X11/Xresources +elif [ -f $CONFIG_DIRECTORY/X11/Xresources ]; then + xrdb -merge $CONFIG_DIRECTORY/X11/Xresources fi [ -f $HOME/.Xresources ] && xrdb -merge $HOME/.Xresources [ -f $XDG_CONFIG_HOME/X11/Xresources ] && xrdb -merge $XDG_CONFIG_HOME/X11/Xresources diff --git a/src/config/Config.zig b/src/config/Config.zig index 7b7c384..863759a 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -46,11 +46,11 @@ term_restore_cursor_cmd: []const u8 = "/usr/bin/tput cnorm", tty: u8 = build_options.tty, vi_mode: bool = false, vi_default_mode: ViMode = .normal, -wayland_cmd: []const u8 = build_options.data_directory ++ "/wsetup.sh", +wayland_cmd: []const u8 = build_options.config_directory ++ "/ly/wsetup.sh", waylandsessions: []const u8 = "/usr/share/wayland-sessions", x_cmd: []const u8 = "/usr/bin/X", xinitrc: ?[]const u8 = "~/.xinitrc", -x_cmd_setup: []const u8 = build_options.data_directory ++ "/xsetup.sh", +x_cmd_setup: []const u8 = build_options.config_directory ++ "/ly/xsetup.sh", xauth_cmd: []const u8 = "/usr/bin/xauth", xsessions: []const u8 = "/usr/share/xsessions", brightness_down_key: []const u8 = "F5", diff --git a/src/main.zig b/src/main.zig index d646caa..dd3b25a 100644 --- a/src/main.zig +++ b/src/main.zig @@ -107,7 +107,7 @@ pub fn main() !void { var save_ini = Ini(Save).init(allocator); defer save_ini.deinit(); - var save_path: []const u8 = build_options.data_directory ++ "/save.ini"; + var 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); @@ -151,14 +151,14 @@ pub fn main() !void { // }; // } } else { - const config_path = build_options.data_directory ++ "/config.ini"; + const config_path = build_options.config_directory ++ "/ly/config.ini"; config = config_ini.readFileToStruct(config_path, comment_characters, migrator.configFieldHandler) catch _config: { config_load_failed = true; break :_config Config{}; }; - const lang_path = try std.fmt.allocPrint(allocator, "{s}/lang/{s}.ini", .{ build_options.data_directory, config.lang }); + 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, comment_characters, null) catch Lang{}; From 9b4d381f1e6c2985dc596623897678d15be7882f Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Thu, 1 Aug 2024 00:18:46 +0200 Subject: [PATCH 072/530] Update zigini (fixes an escaping bug) Signed-off-by: AnErrupTion --- build.zig.zon | 4 ++-- res/config.ini | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/build.zig.zon b/build.zig.zon index 5bd9d66..c4c9cbd 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -8,8 +8,8 @@ .hash = "122062d301a203d003547b414237229b09a7980095061697349f8bef41be9c30266b", }, .zigini = .{ - .url = "https://github.com/Kawaii-Ash/zigini/archive/bdb6fd15c6dcedb0c6c2a46381f2d298e2f05fff.tar.gz", - .hash = "12203feb831e21bec081af6aae70dd19b127f1627aa55f3415bd1fa476c174a511cc", + .url = "https://github.com/Kawaii-Ash/zigini/archive/0bba97a12582928e097f4074cc746c43351ba4c8.tar.gz", + .hash = "12209b971367b4066d40ecad4728e6fdffc4cc4f19356d424c2de57f5b69ac7a619a", }, }, .paths = .{""}, diff --git a/res/config.ini b/res/config.ini index cf76d35..7faf2e7 100644 --- a/res/config.ini +++ b/res/config.ini @@ -18,6 +18,7 @@ bigclock = false # The character used to mask the password # If null, the password will be hidden +# Note: you can use a # by escaping it like so: \# asterisk = * # Erase password input on failure From 872b15c0d4f2a2d978747e4503931b89fee1225c Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Thu, 1 Aug 2024 00:39:00 +0200 Subject: [PATCH 073/530] Fix authentication Signed-off-by: AnErrupTion --- build.zig | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/build.zig b/build.zig index 9b217c3..ed72b22 100644 --- a/build.zig +++ b/build.zig @@ -271,13 +271,13 @@ fn install_ly(allocator: std.mem.Allocator, install_config: bool) !void { var patch_map = PatchMap.init(allocator); defer patch_map.deinit(); - try patch_map.put("$PREFIX_DIRECTORY", prefix_directory); + try patch_map.put("$CONFIG_DIRECTORY", config_directory); const patched_xsetup = try patchFile(allocator, "res/xsetup.sh", patch_map); const patched_wsetup = try patchFile(allocator, "res/wsetup.sh", patch_map); - try installText(patched_xsetup, config_dir, ly_config_directory, "xsetup.sh", .{}); - try installText(patched_wsetup, config_dir, ly_config_directory, "wsetup.sh", .{}); + try installText(patched_xsetup, config_dir, ly_config_directory, "xsetup.sh", .{ .mode = 0o755 }); + try installText(patched_wsetup, config_dir, ly_config_directory, "wsetup.sh", .{ .mode = 0o755 }); } } From 1314c577963ed5e8f2ede56b626c5a6bced1d15f Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Thu, 1 Aug 2024 00:54:00 +0200 Subject: [PATCH 074/530] Fix config.brightnessctl missing + bugs Signed-off-by: AnErrupTion --- build.zig | 1 + res/config.ini | 1 + src/config/Config.zig | 18 +++++++++--------- src/main.zig | 4 ++-- 4 files changed, 13 insertions(+), 11 deletions(-) diff --git a/build.zig b/build.zig index ed72b22..b9b2a14 100644 --- a/build.zig +++ b/build.zig @@ -41,6 +41,7 @@ pub fn build(b: *std.Build) !void { default_tty_str = try std.fmt.allocPrint(b.allocator, "{d}", .{default_tty}); build_options.addOption([]const u8, "config_directory", bin_directory); + build_options.addOption([]const u8, "prefix_directory", prefix_directory); build_options.addOption([]const u8, "version", version_str); build_options.addOption(u8, "tty", default_tty); build_options.addOption(bool, "enable_x11_support", enable_x11_support); diff --git a/res/config.ini b/res/config.ini index 7faf2e7..619dd6a 100644 --- a/res/config.ini +++ b/res/config.ini @@ -186,4 +186,5 @@ xsessions = $PREFIX_DIRECTORY/share/xsessions # Brightness control brightness_down_key = F5 brightness_up_key = F6 +brightnessctl = $PREFIX_DIRECTORY/bin/brightnessctl brightness_change = 10 diff --git a/src/config/Config.zig b/src/config/Config.zig index 4d2b270..64718e4 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -28,33 +28,33 @@ lang: []const u8 = "en", load: bool = true, margin_box_h: u8 = 2, margin_box_v: u8 = 1, -mcookie_cmd: [:0]const u8 = "/usr/bin/mcookie", +mcookie_cmd: [:0]const u8 = build_options.prefix_directory ++ "/bin/mcookie", min_refresh_delta: u16 = 5, numlock: bool = false, path: ?[:0]const u8 = "/sbin:/bin:/usr/local/sbin:/usr/local/bin:/usr/bin:/usr/sbin", restart_cmd: []const u8 = "/sbin/shutdown -r now", restart_key: []const u8 = "F2", save: bool = true, -save_file: []const u8 = "/etc/ly/save", +save_file: []const u8 = build_options.config_directory ++ "/ly/save", service_name: [:0]const u8 = "ly", shutdown_cmd: []const u8 = "/sbin/shutdown -a now", shutdown_key: []const u8 = "F1", sleep_cmd: ?[]const u8 = null, sleep_key: []const u8 = "F3", -term_reset_cmd: [:0]const u8 = "/usr/bin/tput reset", -term_restore_cursor_cmd: []const u8 = "/usr/bin/tput cnorm", +term_reset_cmd: [:0]const u8 = build_options.prefix_directory ++ "/bin/tput reset", +term_restore_cursor_cmd: []const u8 = build_options.prefix_directory ++ "/bin/tput cnorm", tty: u8 = build_options.tty, vi_mode: bool = false, vi_default_mode: ViMode = .normal, wayland_cmd: []const u8 = build_options.config_directory ++ "/ly/wsetup.sh", -waylandsessions: []const u8 = "/usr/share/wayland-sessions", -x_cmd: []const u8 = "/usr/bin/X", +waylandsessions: []const u8 = build_options.prefix_directory ++ "/share/wayland-sessions", +x_cmd: []const u8 = build_options.prefix_directory ++ "/bin/X", xinitrc: ?[]const u8 = "~/.xinitrc", x_cmd_setup: []const u8 = build_options.config_directory ++ "/ly/xsetup.sh", -xauth_cmd: []const u8 = "/usr/bin/xauth", -xsessions: []const u8 = "/usr/share/xsessions", +xauth_cmd: []const u8 = build_options.prefix_directory ++ "/bin/xauth", +xsessions: []const u8 = build_options.prefix_directory ++ "/share/xsessions", brightness_down_key: []const u8 = "F5", brightness_up_key: []const u8 = "F6", -brightnessctl: []const u8 = "/usr/bin/brightnessctl", +brightnessctl: [:0]const u8 = build_options.prefix_directory ++ "/bin/brightnessctl", brightness_change: []const u8 = "10", animation_timeout_sec: u12 = 0, diff --git a/src/main.zig b/src/main.zig index 1bd9dd8..3cd9ac5 100644 --- a/src/main.zig +++ b/src/main.zig @@ -594,7 +594,7 @@ pub fn main() !void { var sleep = std.process.Child.init(&[_][]const u8{ "/bin/sh", "-c", sleep_cmd }, allocator); _ = sleep.spawnAndWait() catch .{}; } - } else if (pressed_key == brightness_down_key and unistd.access(&config.brightnessctl[0], unistd.X_OK) == 0) brightness_change: { + } else if (pressed_key == brightness_down_key and unistd.access(config.brightnessctl, unistd.X_OK) == 0) brightness_change: { const brightness_str = std.fmt.allocPrint(allocator, "{s}%-", .{config.brightness_change}) catch { try info_line.addMessage(lang.err_brightness_change, config.error_bg, config.error_fg); break :brightness_change; @@ -602,7 +602,7 @@ pub fn main() !void { defer allocator.free(brightness_str); var brightness = std.process.Child.init(&[_][]const u8{ config.brightnessctl, "-q", "s", brightness_str }, allocator); _ = brightness.spawnAndWait() catch .{}; - } else if (pressed_key == brightness_up_key and unistd.access(&config.brightnessctl[0], unistd.X_OK) == 0) brightness_change: { + } else if (pressed_key == brightness_up_key and unistd.access(config.brightnessctl, unistd.X_OK) == 0) brightness_change: { const brightness_str = std.fmt.allocPrint(allocator, "+{s}%", .{config.brightness_change}) catch { try info_line.addMessage(lang.err_brightness_change, config.error_bg, config.error_fg); break :brightness_change; From 61f3fadfbf33e393687e99f8ec3dd5efaf805ae7 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Thu, 1 Aug 2024 13:15:54 +0200 Subject: [PATCH 075/530] Make code more portable + remove mcookie usage Signed-off-by: AnErrupTion --- res/config.ini | 3 -- res/lang/en.ini | 1 - res/lang/fr.ini | 1 - src/auth.zig | 108 +++++++++++++++++----------------------- src/config/Config.zig | 1 - src/config/Lang.zig | 1 - src/config/migrator.zig | 3 +- src/interop.zig | 93 ++++++++++++---------------------- src/main.zig | 3 +- 9 files changed, 82 insertions(+), 132 deletions(-) diff --git a/res/config.ini b/res/config.ini index 619dd6a..d420af0 100644 --- a/res/config.ini +++ b/res/config.ini @@ -158,9 +158,6 @@ term_reset_cmd = $PREFIX_DIRECTORY/bin/tput reset # Terminal restore cursor command term_restore_cursor_cmd = $PREFIX_DIRECTORY/bin/tput cnorm -# Cookie generator -mcookie_cmd = $PREFIX_DIRECTORY/bin/mcookie - # Wayland setup command wayland_cmd = $CONFIG_DIRECTORY/ly/wsetup.sh diff --git a/res/lang/en.ini b/res/lang/en.ini index 21ccc30..511200e 100644 --- a/res/lang/en.ini +++ b/res/lang/en.ini @@ -11,7 +11,6 @@ err_dgn_oob = log message err_domain = invalid domain err_envlist = failed to get envlist err_hostname = failed to get hostname -err_mcookie = mcookie command failed err_mlock = failed to lock password memory err_null = null pointer err_pam = pam transaction failed diff --git a/res/lang/fr.ini b/res/lang/fr.ini index c6c6761..32e5598 100644 --- a/res/lang/fr.ini +++ b/res/lang/fr.ini @@ -11,7 +11,6 @@ err_dgn_oob = message err_domain = domaine invalide err_envlist = échec de lecture de la liste d'environnement err_hostname = échec de lecture du nom d'hôte -err_mcookie = échec de la commande mcookie err_mlock = échec du verrouillage mémoire err_null = pointeur null err_pam = échec de la transaction pam diff --git a/src/auth.zig b/src/auth.zig index c05b9b3..f9bc28d 100644 --- a/src/auth.zig +++ b/src/auth.zig @@ -8,6 +8,7 @@ const Session = @import("tui/components/Session.zig"); const Text = @import("tui/components/Text.zig"); const Config = @import("config/Config.zig"); const Allocator = std.mem.Allocator; +const Md5 = std.crypto.hash.Md5; const utmp = interop.utmp; const Utmp = utmp.utmpx; const SharedError = @import("SharedError.zig"); @@ -42,7 +43,7 @@ pub fn authenticate(config: Config, current_environment: Session.Environment, lo }; var handle: ?*interop.pam.pam_handle = undefined; - var status = interop.pam.pam_start(config.service_name.ptr, null, &conv, &handle); + var status = interop.pam.pam_start(config.service_name, null, &conv, &handle); if (status != interop.pam.PAM_SUCCESS) return pamDiagnose(status); defer _ = interop.pam.pam_end(handle, status); @@ -65,19 +66,19 @@ pub fn authenticate(config: Config, current_environment: Session.Environment, lo if (status != interop.pam.PAM_SUCCESS) return pamDiagnose(status); defer status = interop.pam.pam_close_session(handle, 0); - var pwd: *std.c.passwd = undefined; + var pwd: *interop.pwd.passwd = undefined; { - defer interop.endpwent(); + defer interop.pwd.endpwent(); // Get password structure from username - pwd = std.c.getpwnam(login.ptr) orelse return error.GetPasswordNameFailed; + pwd = interop.pwd.getpwnam(login) orelse return error.GetPasswordNameFailed; } // Set user shell if it hasn't already been set if (pwd.pw_shell == null) { - interop.setusershell(); - pwd.pw_shell = interop.getusershell(); - interop.endusershell(); + interop.unistd.setusershell(); + pwd.pw_shell = interop.unistd.getusershell(); + interop.unistd.endusershell(); } var shared_err = try SharedError.init(); @@ -123,18 +124,22 @@ pub fn authenticate(config: Config, current_environment: Session.Environment, lo fn startSession( config: Config, - pwd: *std.c.passwd, + pwd: *interop.pwd.passwd, handle: ?*interop.pam.pam_handle, current_environment: Session.Environment, ) !void { - const status = interop.initgroups(pwd.pw_name.?, pwd.pw_gid); - if (status != 0) return error.GroupInitializationFailed; - if (builtin.os.tag == .freebsd) { + // FreeBSD has initgroups() in unistd + const status = interop.unistd.initgroups(pwd.pw_name, pwd.pw_gid); + if (status != 0) return error.GroupInitializationFailed; + // FreeBSD sets the GID and UID with setusercontext() const result = std.c.setusercontext(null, pwd, pwd.pw_uid, interop.logincap.LOGIN_SETALL); if (result != 0) return error.SetUserUidFailed; } else { + const status = interop.grp.initgroups(pwd.pw_name, pwd.pw_gid); + if (status != 0) return error.GroupInitializationFailed; + std.posix.setgid(pwd.pw_gid) catch return error.SetUserGidFailed; std.posix.setuid(pwd.pw_uid) catch return error.SetUserUidFailed; } @@ -147,7 +152,7 @@ fn startSession( if (pam_env_vars == null) return error.GetEnvListFailed; const env_list = std.mem.span(pam_env_vars.?); - for (env_list) |env_var| _ = interop.putenv(env_var.?); + for (env_list) |env_var| _ = interop.stdlib.putenv(env_var); // Execute what the user requested std.posix.chdirZ(pwd.pw_dir.?) catch return error.ChangeDirectoryFailed; @@ -165,21 +170,21 @@ fn startSession( } } -fn initEnv(pwd: *std.c.passwd, path_env: ?[:0]const u8) !void { - _ = interop.setenv("HOME", pwd.pw_dir, 1); - _ = interop.setenv("PWD", pwd.pw_dir, 1); - _ = interop.setenv("SHELL", pwd.pw_shell, 1); - _ = interop.setenv("USER", pwd.pw_name, 1); - _ = interop.setenv("LOGNAME", pwd.pw_name, 1); +fn initEnv(pwd: *interop.pwd.passwd, path_env: ?[:0]const u8) !void { + _ = interop.stdlib.setenv("HOME", pwd.pw_dir, 1); + _ = interop.stdlib.setenv("PWD", pwd.pw_dir, 1); + _ = interop.stdlib.setenv("SHELL", pwd.pw_shell, 1); + _ = interop.stdlib.setenv("USER", pwd.pw_name, 1); + _ = interop.stdlib.setenv("LOGNAME", pwd.pw_name, 1); if (path_env) |path| { - const status = interop.setenv("PATH", path, 1); + const status = interop.stdlib.setenv("PATH", path, 1); if (status != 0) return error.SetPathFailed; } } fn setXdgSessionEnv(display_server: enums.DisplayServer) void { - _ = interop.setenv("XDG_SESSION_TYPE", switch (display_server) { + _ = interop.stdlib.setenv("XDG_SESSION_TYPE", switch (display_server) { .wayland => "wayland", .shell => "tty", .xinitrc, .x11 => "x11", @@ -192,19 +197,19 @@ fn setXdgEnv(tty_str: [:0]u8, desktop_name: [:0]const u8, xdg_desktop_names: [:0 // XDG_RUNTIME_DIR to fall back to directories inside user's home // directory. if (builtin.os.tag != .freebsd) { - const uid = interop.getuid(); + const uid = interop.unistd.getuid(); var uid_buffer: [10 + @sizeOf(u32) + 1]u8 = undefined; const uid_str = try std.fmt.bufPrintZ(&uid_buffer, "/run/user/{d}", .{uid}); - _ = interop.setenv("XDG_RUNTIME_DIR", uid_str.ptr, 0); + _ = interop.stdlib.setenv("XDG_RUNTIME_DIR", uid_str, 0); } - _ = interop.setenv("XDG_CURRENT_DESKTOP", xdg_desktop_names.ptr, 0); - _ = interop.setenv("XDG_SESSION_CLASS", "user", 0); - _ = interop.setenv("XDG_SESSION_ID", "1", 0); - _ = interop.setenv("XDG_SESSION_DESKTOP", desktop_name.ptr, 0); - _ = interop.setenv("XDG_SEAT", "seat0", 0); - _ = interop.setenv("XDG_VTNR", tty_str.ptr, 0); + _ = interop.stdlib.setenv("XDG_CURRENT_DESKTOP", xdg_desktop_names, 0); + _ = interop.stdlib.setenv("XDG_SESSION_CLASS", "user", 0); + _ = interop.stdlib.setenv("XDG_SESSION_ID", "1", 0); + _ = interop.stdlib.setenv("XDG_SESSION_DESKTOP", desktop_name, 0); + _ = interop.stdlib.setenv("XDG_SEAT", "seat0", 0); + _ = interop.stdlib.setenv("XDG_VTNR", tty_str, 0); } fn loginConv( @@ -235,7 +240,7 @@ fn loginConv( status = interop.pam.PAM_BUF_ERR; break :set_credentials; }; - response[i].resp = username.?.ptr; + response[i].resp = username.?; }, interop.pam.PAM_PROMPT_ECHO_OFF => { const data: [*][*:0]u8 = @ptrCast(@alignCast(appdata_ptr)); @@ -243,7 +248,7 @@ fn loginConv( status = interop.pam.PAM_BUF_ERR; break :set_credentials; }; - response[i].resp = password.?.ptr; + response[i].resp = password.?; }, interop.pam.PAM_ERROR_MSG => { status = interop.pam.PAM_CONV_ERR; @@ -349,49 +354,30 @@ fn createXauthFile(pwd: [:0]const u8) ![:0]const u8 { return xauthority; } -pub fn mcookie(cmd: [:0]const u8) ![32]u8 { - const pipe = try std.posix.pipe(); - defer std.posix.close(pipe[1]); +fn mcookie() [Md5.digest_length * 2]u8 { + var buf: [4096]u8 = undefined; + std.crypto.random.bytes(&buf); - const output = std.fs.File{ .handle = pipe[0] }; - defer output.close(); + var out: [Md5.digest_length]u8 = undefined; + Md5.hash(&buf, &out, .{}); - const pid = try std.posix.fork(); - if (pid == 0) { - std.posix.close(pipe[0]); - - std.posix.dup2(pipe[1], std.posix.STDOUT_FILENO) catch std.process.exit(1); - std.posix.close(pipe[1]); - - const args = [_:null]?[*:0]u8{}; - std.posix.execveZ(cmd.ptr, &args, std.c.environ) catch {}; - std.process.exit(1); - } - - const result = std.posix.waitpid(pid, 0); - - if (result.status != 0) return error.McookieFailed; - - var buf: [32]u8 = undefined; - const len = try output.read(&buf); - if (len != 32) return error.McookieFailed; - return buf; + return std.fmt.bytesToHex(&out, .lower); } -fn xauth(display_name: [:0]u8, shell: [*:0]const u8, pw_dir: [*:0]const u8, xauth_cmd: []const u8, mcookie_cmd: [:0]const u8) !void { +fn xauth(display_name: [:0]u8, shell: [*:0]const u8, pw_dir: [*:0]const u8, xauth_cmd: []const u8) !void { var pwd_buf: [100]u8 = undefined; const pwd = try std.fmt.bufPrintZ(&pwd_buf, "{s}", .{pw_dir}); const xauthority = try createXauthFile(pwd); - _ = interop.setenv("XAUTHORITY", xauthority, 1); - _ = interop.setenv("DISPLAY", display_name, 1); + _ = interop.stdlib.setenv("XAUTHORITY", xauthority, 1); + _ = interop.stdlib.setenv("DISPLAY", display_name, 1); - const mcookie_output = try mcookie(mcookie_cmd); + const magic_cookie = mcookie(); const pid = try std.posix.fork(); if (pid == 0) { var cmd_buffer: [1024]u8 = undefined; - const cmd_str = std.fmt.bufPrintZ(&cmd_buffer, "{s} add {s} . {s}", .{ xauth_cmd, display_name, mcookie_output }) catch std.process.exit(1); + const cmd_str = std.fmt.bufPrintZ(&cmd_buffer, "{s} add {s} . {s}", .{ xauth_cmd, display_name, magic_cookie }) catch std.process.exit(1); const args = [_:null]?[*:0]const u8{ shell, "-c", cmd_str }; std.posix.execveZ(shell, &args, std.c.environ) catch {}; std.process.exit(1); @@ -417,7 +403,7 @@ fn executeX11Cmd(shell: [*:0]const u8, pw_dir: [*:0]const u8, config: Config, de const display_num = try getFreeDisplay(); var buf: [5]u8 = undefined; const display_name = try std.fmt.bufPrintZ(&buf, ":{d}", .{display_num}); - try xauth(display_name, shell, pw_dir, config.xauth_cmd, config.mcookie_cmd); + try xauth(display_name, shell, pw_dir, config.xauth_cmd); const pid = try std.posix.fork(); if (pid == 0) { diff --git a/src/config/Config.zig b/src/config/Config.zig index 64718e4..d20c871 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -28,7 +28,6 @@ lang: []const u8 = "en", load: bool = true, margin_box_h: u8 = 2, margin_box_v: u8 = 1, -mcookie_cmd: [:0]const u8 = build_options.prefix_directory ++ "/bin/mcookie", min_refresh_delta: u16 = 5, numlock: bool = false, path: ?[:0]const u8 = "/sbin:/bin:/usr/local/sbin:/usr/local/bin:/usr/bin:/usr/sbin", diff --git a/src/config/Lang.zig b/src/config/Lang.zig index 5598c30..3a81108 100644 --- a/src/config/Lang.zig +++ b/src/config/Lang.zig @@ -12,7 +12,6 @@ err_dgn_oob: []const u8 = "log message", err_domain: []const u8 = "invalid domain", err_envlist: []const u8 = "failed to get envlist", err_hostname: []const u8 = "failed to get hostname", -err_mcookie: []const u8 = "mcookie command failed", err_mlock: []const u8 = "failed to lock password memory", err_null: []const u8 = "null pointer", err_pam: []const u8 = "pam transaction failed", diff --git a/src/config/migrator.zig b/src/config/migrator.zig index 569cdf8..66d4cfc 100644 --- a/src/config/migrator.zig +++ b/src/config/migrator.zig @@ -62,7 +62,8 @@ pub fn configFieldHandler(_: std.mem.Allocator, field: ini.IniField) ?ini.IniFie if (std.mem.eql(u8, field.key, "wayland_specifier") or std.mem.eql(u8, field.key, "max_desktop_len") or std.mem.eql(u8, field.key, "max_login_len") or - std.mem.eql(u8, field.key, "max_password_len")) + std.mem.eql(u8, field.key, "max_password_len") or + std.mem.eql(u8, field.key, "mcookie_cmd")) { // The options don't exist anymore mapped_config_fields = true; diff --git a/src/interop.zig b/src/interop.zig index beba61c..882c375 100644 --- a/src/interop.zig +++ b/src/interop.zig @@ -21,6 +21,22 @@ pub const unistd = @cImport({ @cInclude("unistd.h"); }); +pub const time = @cImport({ + @cInclude("time.h"); +}); + +pub const stdlib = @cImport({ + @cInclude("stdlib.h"); +}); + +pub const pwd = @cImport({ + @cInclude("pwd.h"); +}); + +pub const grp = @cImport({ + @cInclude("grp.h"); +}); + // FreeBSD-specific headers pub const logincap = @cImport({ @cInclude("login_cap.h"); @@ -40,38 +56,18 @@ pub const vt = @cImport({ @cInclude("sys/vt.h"); }); -pub const c_size = usize; -pub const c_uid = u32; -pub const c_gid = u32; -pub const c_time = c_longlong; -pub const tm = extern struct { - tm_sec: c_int, - tm_min: c_int, - tm_hour: c_int, - tm_mday: c_int, - tm_mon: c_int, - tm_year: c_int, - tm_wday: c_int, - tm_yday: c_int, - tm_isdst: c_int, -}; - -pub extern "c" fn localtime(timer: *const c_time) *tm; -pub extern "c" fn strftime(str: [*:0]u8, maxsize: c_size, format: [*:0]const u8, timeptr: *const tm) c_size; -pub extern "c" fn setenv(name: [*:0]const u8, value: ?[*:0]const u8, overwrite: c_int) c_int; -pub extern "c" fn putenv(name: [*:0]u8) c_int; -pub extern "c" fn getuid() c_uid; -pub extern "c" fn endpwent() void; -pub extern "c" fn setusershell() void; -pub extern "c" fn getusershell() [*:0]u8; -pub extern "c" fn endusershell() void; -pub extern "c" fn initgroups(user: [*:0]const u8, group: c_gid) c_int; +// Used for getting & setting the lock state +const LedState = if (builtin.os.tag.isBSD()) c_int else c_char; +const get_led_state = if (builtin.os.tag.isBSD()) kbio.KDGETLED else kd.KDGKBLED; +const set_led_state = if (builtin.os.tag.isBSD()) kbio.KDSETLED else kd.KDSKBLED; +const numlock_led = if (builtin.os.tag.isBSD()) kbio.LED_NUM else kd.K_NUMLOCK; +const capslock_led = if (builtin.os.tag.isBSD()) kbio.LED_CAP else kd.K_CAPSLOCK; pub fn timeAsString(buf: [:0]u8, format: [:0]const u8) ![]u8 { const timer = std.time.timestamp(); - const tm_info = localtime(&timer); + const tm_info = time.localtime(&timer); - const len = strftime(buf, buf.len, format, tm_info); + const len = time.strftime(buf, buf.len, format, tm_info); if (len < 0) return error.CannotGetFormattedTime; return buf[0..len]; @@ -92,47 +88,22 @@ pub fn getLockState(console_dev: []const u8) !struct { const fd = try std.posix.open(console_dev, .{ .ACCMODE = .RDONLY }, 0); defer std.posix.close(fd); - var numlock = false; - var capslock = false; - - if (builtin.os.tag.isBSD()) { - var led: c_int = undefined; - _ = std.c.ioctl(fd, kbio.KDGETLED, &led); - numlock = (led & kbio.LED_NUM) != 0; - capslock = (led & kbio.LED_CAP) != 0; - } else { - var led: c_char = undefined; - _ = std.c.ioctl(fd, kd.KDGKBLED, &led); - numlock = (led & kd.K_NUMLOCK) != 0; - capslock = (led & kd.K_CAPSLOCK) != 0; - } + var led: LedState = undefined; + _ = std.c.ioctl(fd, get_led_state, &led); return .{ - .numlock = numlock, - .capslock = capslock, + .numlock = (led & numlock_led) != 0, + .capslock = (led & capslock_led) != 0, }; } pub fn setNumlock(val: bool) !void { - if (builtin.os.tag.isBSD()) { - var led: c_int = undefined; - _ = std.c.ioctl(0, kbio.KDGETLED, &led); + var led: LedState = undefined; + _ = std.c.ioctl(0, get_led_state, &led); - const numlock = (led & kbio.LED_NUM) != 0; - if (numlock != val) { - const status = std.c.ioctl(std.posix.STDIN_FILENO, kbio.KDSETLED, led ^ kbio.LED_NUM); - if (status != 0) return error.FailedToSetNumlock; - } - - return; - } - - var led: c_char = undefined; - _ = std.c.ioctl(0, kd.KDGKBLED, &led); - - const numlock = (led & kd.K_NUMLOCK) != 0; + const numlock = (led & numlock_led) != 0; if (numlock != val) { - const status = std.c.ioctl(std.posix.STDIN_FILENO, kd.KDSKBLED, led ^ kd.K_NUMLOCK); + const status = std.c.ioctl(std.posix.STDIN_FILENO, set_led_state, led ^ numlock_led); if (status != 0) return error.FailedToSetNumlock; } } diff --git a/src/main.zig b/src/main.zig index 3cd9ac5..1f0b0e8 100644 --- a/src/main.zig +++ b/src/main.zig @@ -215,7 +215,7 @@ pub fn main() !void { const labels_max_length = @max(lang.login.len, lang.password.len); var seed: u64 = undefined; - try std.posix.getrandom(std.mem.asBytes(&seed)); // Get a random seed for the PRNG (used by animations) + std.crypto.random.bytes(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(); @@ -770,7 +770,6 @@ fn getAuthErrorMsg(err: anyerror, lang: Lang) []const u8 { error.GetPasswordNameFailed => lang.err_pwnam, error.GetEnvListFailed => lang.err_envlist, error.XauthFailed => lang.err_xauth, - error.McookieFailed => lang.err_mcookie, error.XcbConnectionFailed => lang.err_xcb_conn, error.GroupInitializationFailed => lang.err_user_init, error.SetUserGidFailed => lang.err_user_gid, From 3ca2e8524b47b13ad36ed7ee1e4053b72be8fbd4 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Thu, 1 Aug 2024 13:20:23 +0200 Subject: [PATCH 076/530] Fix mcookie usage (fixes #669) Signed-off-by: AnErrupTion --- src/auth.zig | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/auth.zig b/src/auth.zig index b55c9d2..1892c25 100644 --- a/src/auth.zig +++ b/src/auth.zig @@ -334,7 +334,7 @@ fn createXauthFile(pwd: [:0]const u8) ![:0]const u8 { return xauthority; } -pub fn mcookie(cmd: [:0]const u8) ![32]u8 { +pub fn mcookie(shell: [*:0]const u8, cmd: [:0]const u8) ![32]u8 { const pipe = try std.posix.pipe(); defer std.posix.close(pipe[1]); @@ -348,8 +348,8 @@ pub fn mcookie(cmd: [:0]const u8) ![32]u8 { std.posix.dup2(pipe[1], std.posix.STDOUT_FILENO) catch std.process.exit(1); std.posix.close(pipe[1]); - const args = [_:null]?[*:0]u8{}; - std.posix.execveZ(cmd.ptr, &args, std.c.environ) catch {}; + const args = [_:null]?[*:0]const u8{ shell, "-c", cmd }; + std.posix.execveZ(shell, &args, std.c.environ) catch {}; std.process.exit(1); } @@ -371,7 +371,7 @@ fn xauth(display_name: [:0]u8, shell: [*:0]const u8, pw_dir: [*:0]const u8, xaut _ = interop.setenv("XAUTHORITY", xauthority, 1); _ = interop.setenv("DISPLAY", display_name, 1); - const mcookie_output = try mcookie(mcookie_cmd); + const mcookie_output = try mcookie(shell, mcookie_cmd); const pid = try std.posix.fork(); if (pid == 0) { From 7300247e577b741d5e8f823e1b703f5e0ca20130 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Thu, 1 Aug 2024 13:29:34 +0200 Subject: [PATCH 077/530] Backport: Update zigini (fixes an escaping bug) 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 ba6345e..557dcd9 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -8,8 +8,8 @@ .hash = "122014e73fd712190e109950837b97f6143f02d7e2b6986e1db70b6f4aadb5ba6a0d", }, .zigini = .{ - .url = "https://github.com/Kawaii-Ash/zigini/archive/bdb6fd15c6dcedb0c6c2a46381f2d298e2f05fff.tar.gz", - .hash = "12203feb831e21bec081af6aae70dd19b127f1627aa55f3415bd1fa476c174a511cc", + .url = "https://github.com/Kawaii-Ash/zigini/archive/0bba97a12582928e097f4074cc746c43351ba4c8.tar.gz", + .hash = "12209b971367b4066d40ecad4728e6fdffc4cc4f19356d424c2de57f5b69ac7a619a", }, }, .paths = .{""}, From d40ec873a789055b43c0ddfee6be32eb7f0e7448 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Thu, 1 Aug 2024 14:23:52 +0200 Subject: [PATCH 078/530] Retrieve gettimeofday() from sys/time.h Signed-off-by: AnErrupTion --- src/auth.zig | 4 ++-- src/bigclock.zig | 4 ++-- src/interop.zig | 4 ++++ src/main.zig | 16 ++++++++-------- 4 files changed, 16 insertions(+), 12 deletions(-) diff --git a/src/auth.zig b/src/auth.zig index f9bc28d..4fc46b5 100644 --- a/src/auth.zig +++ b/src/auth.zig @@ -477,8 +477,8 @@ fn addUtmpEntry(entry: *Utmp, username: [*:0]const u8, pid: c_int) !void { host[0] = 0; entry.ut_host = host; - var tv: std.c.timeval = undefined; - _ = std.c.gettimeofday(&tv, null); + var tv: interop.system_time.timeval = undefined; + _ = interop.system_time.gettimeofday(&tv, null); entry.ut_tv = .{ .tv_sec = @intCast(tv.tv_sec), diff --git a/src/bigclock.zig b/src/bigclock.zig index cfe61f5..bcae4ed 100644 --- a/src/bigclock.zig +++ b/src/bigclock.zig @@ -102,8 +102,8 @@ const E = [_]u21{ pub fn clockCell(animate: bool, char: u8, fg: u16, bg: u16) [SIZE]termbox.tb_cell { var cells: [SIZE]termbox.tb_cell = undefined; - var tv: std.c.timeval = undefined; - _ = std.c.gettimeofday(&tv, null); + var tv: interop.system_time.timeval = undefined; + _ = interop.system_time.gettimeofday(&tv, null); const clock_chars = toBigNumber(if (animate and char == ':' and @divTrunc(tv.tv_usec, 500000) != 0) ' ' else char); for (0..cells.len) |i| cells[i] = utils.initCell(clock_chars[i], fg, bg); diff --git a/src/interop.zig b/src/interop.zig index 882c375..e2f4ea4 100644 --- a/src/interop.zig +++ b/src/interop.zig @@ -25,6 +25,10 @@ pub const time = @cImport({ @cInclude("time.h"); }); +pub const system_time = @cImport({ + @cInclude("sys/time.h"); +}); + pub const stdlib = @cImport({ @cInclude("stdlib.h"); }); diff --git a/src/main.zig b/src/main.zig index 1f0b0e8..fe4f9ac 100644 --- a/src/main.zig +++ b/src/main.zig @@ -67,8 +67,8 @@ pub fn main() !void { // to be able to stop the animation after some time - var tv_zero: std.c.timeval = undefined; - _ = std.c.gettimeofday(&tv_zero, null); + var tv_zero: interop.system_time.timeval = undefined; + _ = interop.system_time.gettimeofday(&tv_zero, null); var animation_timed_out: bool = false; const allocator = gpa.allocator(); @@ -545,8 +545,8 @@ pub fn main() !void { timeout = config.min_refresh_delta; // check how long we have been running so we can turn off the animation - var tv: std.c.timeval = undefined; - _ = std.c.gettimeofday(&tv, null); + var tv: interop.system_time.timeval = undefined; + _ = interop.system_time.gettimeofday(&tv, null); if (config.animation_timeout_sec > 0 and tv.tv_sec - tv_zero.tv_sec > config.animation_timeout_sec) { animation_timed_out = true; @@ -557,13 +557,13 @@ pub fn main() !void { } } } else if (config.bigclock and config.clock == null) { - var tv: std.c.timeval = undefined; - _ = std.c.gettimeofday(&tv, null); + var tv: interop.system_time.timeval = undefined; + _ = interop.system_time.gettimeofday(&tv, null); timeout = @intCast((60 - @rem(tv.tv_sec, 60)) * 1000 - @divTrunc(tv.tv_usec, 1000) + 1); } else if (config.clock != null or auth_fails >= 10) { - var tv: std.c.timeval = undefined; - _ = std.c.gettimeofday(&tv, null); + var tv: interop.system_time.timeval = undefined; + _ = interop.system_time.gettimeofday(&tv, null); timeout = @intCast(1000 - @divTrunc(tv.tv_usec, 1000) + 1); } From 5d3cd62434e2702428d94a6dfcf16b05daa27edd Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Thu, 1 Aug 2024 17:02:08 +0200 Subject: [PATCH 079/530] Swap /usr/bin and /usr/sbin in PATH Signed-off-by: AnErrupTion --- res/config.ini | 2 +- src/config/Config.zig | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/res/config.ini b/res/config.ini index d420af0..b93c687 100644 --- a/res/config.ini +++ b/res/config.ini @@ -141,7 +141,7 @@ console_dev = /dev/console # Default path # If null, ly doesn't set a path -path = /sbin:/bin:/usr/local/sbin:/usr/local/bin:/usr/bin:/usr/sbin +path = /sbin:/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin # Event timeout in milliseconds min_refresh_delta = 5 diff --git a/src/config/Config.zig b/src/config/Config.zig index d20c871..4c57fe6 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -30,7 +30,7 @@ margin_box_h: u8 = 2, margin_box_v: u8 = 1, min_refresh_delta: u16 = 5, numlock: bool = false, -path: ?[:0]const u8 = "/sbin:/bin:/usr/local/sbin:/usr/local/bin:/usr/bin:/usr/sbin", +path: ?[:0]const u8 = "/sbin:/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin", restart_cmd: []const u8 = "/sbin/shutdown -r now", restart_key: []const u8 = "F2", save: bool = true, From 56202bc30e663d9b5ba584d170334f335bc49fd5 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Thu, 1 Aug 2024 18:02:03 +0200 Subject: [PATCH 080/530] Backport: Swap /usr/bin and /usr/sbin in PATH Signed-off-by: AnErrupTion --- res/config.ini | 14 +++++++------- src/config/Config.zig | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/res/config.ini b/res/config.ini index 68a39bb..e8fd099 100644 --- a/res/config.ini +++ b/res/config.ini @@ -30,12 +30,12 @@ vi_mode = false #define TB_CYAN 0x07 #define TB_WHITE 0x08 # -# Setting both to zero makes `bg` black and `fg` white. To set the actual color palette you are encouraged to use another tool -# such as [mkinitcpio-colors](https://github.com/evanpurkhiser/mkinitcpio-colors). Note that the color palette defined with -# `mkinitcpio-colors` takes 16 colors (0-15), only values 0-8 are valid for `ly` config and these values do not correspond -# exactly. For instance, in defining palettes with `mkinitcpio-colors` the order is black, dark red, dark green, brown, dark -# blue, dark purple, dark cyan, light gray, dark gray, bright red, bright green, yellow, bright blue, bright purple, bright -# cyan, and white, indexed in that order 0 through 15. For example, the color defined for white (indexed at 15 in the mkinitcpio +# Setting both to zero makes `bg` black and `fg` white. To set the actual color palette you are encouraged to use another tool +# such as [mkinitcpio-colors](https://github.com/evanpurkhiser/mkinitcpio-colors). Note that the color palette defined with +# `mkinitcpio-colors` takes 16 colors (0-15), only values 0-8 are valid for `ly` config and these values do not correspond +# exactly. For instance, in defining palettes with `mkinitcpio-colors` the order is black, dark red, dark green, brown, dark +# blue, dark purple, dark cyan, light gray, dark gray, bright red, bright green, yellow, bright blue, bright purple, bright +# cyan, and white, indexed in that order 0 through 15. For example, the color defined for white (indexed at 15 in the mkinitcpio # config) will be used by `ly` for `fg = 8`. # Background color id @@ -120,7 +120,7 @@ tty = 2 console_dev = /dev/console # Default path. If null, ly doesn't set a path. -path = /sbin:/bin:/usr/local/sbin:/usr/local/bin:/usr/bin:/usr/sbin +path = /sbin:/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin # Event timeout in milliseconds min_refresh_delta = 5 diff --git a/src/config/Config.zig b/src/config/Config.zig index bddda19..ad119c4 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -30,7 +30,7 @@ max_password_len: u8 = 255, mcookie_cmd: [:0]const u8 = "/usr/bin/mcookie", min_refresh_delta: u16 = 5, numlock: bool = false, -path: ?[:0]const u8 = "/sbin:/bin:/usr/local/sbin:/usr/local/bin:/usr/bin:/usr/sbin", +path: ?[:0]const u8 = "/sbin:/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin", restart_cmd: []const u8 = "/sbin/shutdown -r now", restart_key: []const u8 = "F2", save: bool = true, From 042aa50ff0ca5bff5996393022e98df3d8c9f6b2 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Fri, 2 Aug 2024 19:37:11 +0200 Subject: [PATCH 081/530] Start Ly v1.0.3 development cycle Signed-off-by: AnErrupTion --- build.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.zig b/build.zig index 083bd2a..e1dae11 100644 --- a/build.zig +++ b/build.zig @@ -1,6 +1,6 @@ const std = @import("std"); -const ly_version = std.SemanticVersion{ .major = 1, .minor = 0, .patch = 2 }; +const ly_version = std.SemanticVersion{ .major = 1, .minor = 0, .patch = 3 }; var dest_directory: []const u8 = undefined; var data_directory: []const u8 = undefined; var exe_name: []const u8 = undefined; From fadbbf676ad88e57cecffd501b5a4d4634064688 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Fri, 2 Aug 2024 19:40:09 +0200 Subject: [PATCH 082/530] Support Zig 0.13.0 Signed-off-by: AnErrupTion --- .gitignore | 3 ++- build.zig | 26 ++++++++++++++++++++------ build.zig.zon | 4 ++-- src/main.zig | 4 ++-- 4 files changed, 26 insertions(+), 11 deletions(-) diff --git a/.gitignore b/.gitignore index 60f36fa..de08f4f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ .idea/ zig-cache/ zig-out/ -valgrind.log \ No newline at end of file +valgrind.log +.zig-cache diff --git a/build.zig b/build.zig index e1dae11..995536d 100644 --- a/build.zig +++ b/build.zig @@ -1,10 +1,24 @@ const std = @import("std"); +const builtin = @import("builtin"); + +const min_zig_string = "0.12.0"; +const current_zig = builtin.zig_version; + +// Implementing zig version detection through compile time +comptime { + const min_zig = std.SemanticVersion.parse(min_zig_string) catch unreachable; + if (current_zig.order(min_zig) == .lt) { + @compileError(std.fmt.comptimePrint("Your Zig version v{} does not meet the minimum build requirement of v{}", .{ current_zig, min_zig })); + } +} const ly_version = std.SemanticVersion{ .major = 1, .minor = 0, .patch = 3 }; var dest_directory: []const u8 = undefined; var data_directory: []const u8 = undefined; var exe_name: []const u8 = undefined; +const ProgressNode = if (current_zig.minor == 12) *std.Progress.Node else std.Progress.Node; + pub fn build(b: *std.Build) !void { dest_directory = b.option([]const u8, "dest_directory", "Specify a destination directory for installation") orelse ""; data_directory = b.option([]const u8, "data_directory", "Specify a default data directory (default is /etc/ly). This path gets embedded into the binary") orelse "/etc/ly"; @@ -25,7 +39,7 @@ pub fn build(b: *std.Build) !void { const exe = b.addExecutable(.{ .name = "ly", - .root_source_file = .{ .path = "src/main.zig" }, + .root_source_file = b.path("src/main.zig"), .target = target, .optimize = optimize, }); @@ -38,14 +52,14 @@ 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.addIncludePath(.{ .path = "include" }); + exe.addIncludePath(b.path("include")); exe.linkSystemLibrary("pam"); exe.linkSystemLibrary("xcb"); exe.linkLibC(); // HACK: Only fails with ReleaseSafe, so we'll override it. const translate_c = b.addTranslateC(.{ - .root_source_file = .{ .path = "include/termbox2.h" }, + .root_source_file = b.path("include/termbox2.h"), .target = target, .optimize = if (optimize == .ReleaseSafe) .ReleaseFast else optimize, }); @@ -94,7 +108,7 @@ pub fn build(b: *std.Build) !void { pub fn ExeInstaller(install_conf: bool) type { return struct { - pub fn make(step: *std.Build.Step, progress: *std.Progress.Node) !void { + pub fn make(step: *std.Build.Step, progress: ProgressNode) !void { _ = progress; try install_ly(step.owner.allocator, install_conf); } @@ -108,7 +122,7 @@ const InitSystem = enum { }; pub fn ServiceInstaller(comptime init_system: InitSystem) type { return struct { - pub fn make(step: *std.Build.Step, progress: *std.Progress.Node) !void { + pub fn make(step: *std.Build.Step, progress: ProgressNode) !void { _ = progress; const allocator = step.owner.allocator; switch (init_system) { @@ -220,7 +234,7 @@ fn install_ly(allocator: std.mem.Allocator, install_config: bool) !void { } } -pub fn uninstallall(step: *std.Build.Step, progress: *std.Progress.Node) !void { +pub fn uninstallall(step: *std.Build.Step, progress: ProgressNode) !void { _ = progress; try std.fs.cwd().deleteTree(data_directory); const allocator = step.owner.allocator; diff --git a/build.zig.zon b/build.zig.zon index 557dcd9..c4c9cbd 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -4,8 +4,8 @@ .minimum_zig_version = "0.12.0", .dependencies = .{ .clap = .{ - .url = "https://github.com/Hejsil/zig-clap/archive/8c98e6404b22aafc0184e999d8f068b81cc22fa1.tar.gz", - .hash = "122014e73fd712190e109950837b97f6143f02d7e2b6986e1db70b6f4aadb5ba6a0d", + .url = "https://github.com/Hejsil/zig-clap/archive/refs/tags/0.9.1.tar.gz", + .hash = "122062d301a203d003547b414237229b09a7980095061697349f8bef41be9c30266b", }, .zigini = .{ .url = "https://github.com/Kawaii-Ash/zigini/archive/0bba97a12582928e097f4074cc746c43351ba4c8.tar.gz", diff --git a/src/main.zig b/src/main.zig index ddee718..022ff54 100644 --- a/src/main.zig +++ b/src/main.zig @@ -512,7 +512,7 @@ pub fn main() !void { run = false; } else if (pressed_key == sleep_key) { if (config.sleep_cmd) |sleep_cmd| { - var sleep = std.ChildProcess.init(&[_][]const u8{ "/bin/sh", "-c", sleep_cmd }, allocator); + var sleep = std.process.Child.init(&[_][]const u8{ "/bin/sh", "-c", sleep_cmd }, allocator); _ = sleep.spawnAndWait() catch .{}; } } @@ -617,7 +617,7 @@ pub fn main() !void { update = true; - var restore_cursor = std.ChildProcess.init(&[_][]const u8{ "/bin/sh", "-c", config.term_restore_cursor_cmd }, allocator); + var restore_cursor = std.process.Child.init(&[_][]const u8{ "/bin/sh", "-c", config.term_restore_cursor_cmd }, allocator); _ = restore_cursor.spawnAndWait() catch .{}; }, else => { From ea2dec50f5430b1e31fb0c0657fde066abe6abe1 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Fri, 2 Aug 2024 19:40:25 +0200 Subject: [PATCH 083/530] Update README.md Signed-off-by: AnErrupTion --- readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/readme.md b/readme.md index b323c0d..676d612 100644 --- a/readme.md +++ b/readme.md @@ -6,7 +6,7 @@ Ly is a lightweight TUI (ncurses-like) display manager for Linux and BSD. ## Dependencies - Compile-time: - - zig 0.12.0 + - zig 0.12.0 or 0.13.0 - a C standard library - pam - xcb From ce3b310e58ffec580b2db9951dc1cf177d17f708 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Fri, 2 Aug 2024 22:31:35 +0200 Subject: [PATCH 084/530] Make authentication fail count configurable Signed-off-by: AnErrupTion --- res/config.ini | 3 +++ src/config/Config.zig | 1 + src/main.zig | 6 +++--- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/res/config.ini b/res/config.ini index b93c687..caf1f47 100644 --- a/res/config.ini +++ b/res/config.ini @@ -1,3 +1,6 @@ +# The number of failed authentications before a special animation is played... ;) +auth_fails = 10 + # The active animation # none -> Nothing (default) # doom -> PSX DOOM fire diff --git a/src/config/Config.zig b/src/config/Config.zig index 4c57fe6..1680327 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; +auth_fails: u64 = 10, animation: Animation = .none, asterisk: ?u8 = '*', bg: u16 = 0, diff --git a/src/main.zig b/src/main.zig index fe4f9ac..cf2c2ba 100644 --- a/src/main.zig +++ b/src/main.zig @@ -387,7 +387,7 @@ pub fn main() !void { if (update) { // If the user entered a wrong password 10 times in a row, play a cascade animation, else update normally - if (auth_fails < 10) { + if (auth_fails < config.auth_fails) { _ = termbox.tb_clear(); if (!animation_timed_out) { @@ -561,7 +561,7 @@ pub fn main() !void { _ = interop.system_time.gettimeofday(&tv, null); timeout = @intCast((60 - @rem(tv.tv_sec, 60)) * 1000 - @divTrunc(tv.tv_usec, 1000) + 1); - } else if (config.clock != null or auth_fails >= 10) { + } else if (config.clock != null or auth_fails >= config.auth_fails) { var tv: interop.system_time.timeval = undefined; _ = interop.system_time.gettimeofday(&tv, null); @@ -709,7 +709,7 @@ pub fn main() !void { } try std.posix.tcsetattr(std.posix.STDIN_FILENO, .FLUSH, tb_termios); - if (auth_fails < 10) { + if (auth_fails < config.auth_fails) { _ = termbox.tb_clear(); _ = termbox.tb_present(); } From 57d5d7497b1c187b1606253e5c3ee4dded56a779 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Sat, 3 Aug 2024 07:17:06 +0200 Subject: [PATCH 085/530] Fix PAM module order Signed-off-by: AnErrupTion --- res/pam.d/ly | 25 ++++++++++--------------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/res/pam.d/ly b/res/pam.d/ly index 781605e..1f89dc7 100644 --- a/res/pam.d/ly +++ b/res/pam.d/ly @@ -1,21 +1,16 @@ #%PAM-1.0 -# Unlock GNOME Keyring --auth optional pam_gnome_keyring.so --session optional pam_gnome_keyring.so auto_start - -# Unlock KWallet --auth optional pam_kwallet5.so --session optional pam_kwallet5.so auto_start - -# Integrate with systemd-logind --session optional pam_systemd.so class=greeter - -# Integrate with elogind --session optional pam_elogind.so - -# Include system defaults auth include login +-auth optional pam_gnome_keyring.so +-auth optional pam_kwallet5.so + account include login + password include login +-password optional pam_gnome_keyring.so use_authtok + session include login +-session optional pam_gnome_keyring.so auto_start +-session optional pam_kwallet5.so auto_start +-session optional pam_systemd.so class=greeter +-session optional pam_elogind.so From 8995c590ebe3ebc11f48e440bdb9f2ddd1524873 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Sat, 3 Aug 2024 08:36:26 +0200 Subject: [PATCH 086/530] Fix CLI note & OpenRC service not using config directory option Signed-off-by: AnErrupTion --- res/ly-openrc | 2 +- src/main.zig | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/res/ly-openrc b/res/ly-openrc index 81da5d8..83ac7b1 100644 --- a/res/ly-openrc +++ b/res/ly-openrc @@ -20,7 +20,7 @@ then fi ## Get the tty from the conf file -CONFTTY=$(cat /etc/ly/config.ini | sed -n 's/^tty.*=[^1-9]*// p') +CONFTTY=$(cat $CONFIG_DIRECTORY/ly/config.ini | sed -n 's/^tty.*=[^1-9]*// p') ## The execution vars # If CONFTTY is empty then default to $DEFAULT_TTY diff --git a/src/main.zig b/src/main.zig index cf2c2ba..4b64132 100644 --- a/src/main.zig +++ b/src/main.zig @@ -95,7 +95,7 @@ pub fn main() !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 usually located at /etc/ly/config.ini.\n"); + _ = 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"); std.process.exit(0); } if (res.args.version != 0) { From cab3a7d21483ea9ab515916795b66564074c1e49 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Sat, 3 Aug 2024 10:34:13 +0200 Subject: [PATCH 087/530] Delete old save file if it's been migrated Signed-off-by: AnErrupTion --- src/main.zig | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/main.zig b/src/main.zig index 4b64132..80ffb04 100644 --- a/src/main.zig +++ b/src/main.zig @@ -667,6 +667,9 @@ pub fn main() !void { .session_index = session.label.current, }; ini.writeFromStruct(save_data, file.writer(), null, true, .{}) catch break :save_last_settings; + + // Delete previous save file if it exists + std.fs.cwd().deleteFile(config.save_file) catch {}; } var shared_err = try SharedError.init(); From b18f29a81aefa24792bd6a67df9a3f5658f34ce4 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Sat, 3 Aug 2024 13:39:50 +0200 Subject: [PATCH 088/530] Load logind PAM modules before required ones Signed-off-by: AnErrupTion --- res/pam.d/ly | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/res/pam.d/ly b/res/pam.d/ly index 1f89dc7..1a5fe94 100644 --- a/res/pam.d/ly +++ b/res/pam.d/ly @@ -9,8 +9,8 @@ account include login password include login -password optional pam_gnome_keyring.so use_authtok +-session optional pam_systemd.so class=greeter +-session optional pam_elogind.so session include login -session optional pam_gnome_keyring.so auto_start -session optional pam_kwallet5.so auto_start --session optional pam_systemd.so class=greeter --session optional pam_elogind.so From 0bbe9c78dd1c27766d7029a1ded63e37a94281c4 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Sat, 3 Aug 2024 15:17:02 +0200 Subject: [PATCH 089/530] Reduce heap allocations a bit Signed-off-by: AnErrupTion --- src/auth.zig | 2 +- src/main.zig | 4 +-- src/tui/components/Session.zig | 46 ++++++++++++---------------------- 3 files changed, 19 insertions(+), 33 deletions(-) diff --git a/src/auth.zig b/src/auth.zig index 4fc46b5..983fea8 100644 --- a/src/auth.zig +++ b/src/auth.zig @@ -32,7 +32,7 @@ pub fn authenticate(config: Config, current_environment: Session.Environment, lo // Set the XDG environment variables setXdgSessionEnv(current_environment.display_server); - try setXdgEnv(tty_str, current_environment.xdg_session_desktop, current_environment.xdg_desktop_names orelse ""); + try setXdgEnv(tty_str, current_environment.xdg_session_desktop orelse "", current_environment.xdg_desktop_names orelse ""); // Open the PAM session var credentials = [_:null]?[*:0]const u8{ login, password }; diff --git a/src/main.zig b/src/main.zig index 80ffb04..60f53fa 100644 --- a/src/main.zig +++ b/src/main.zig @@ -234,13 +234,13 @@ pub fn main() !void { var session = Session.init(allocator, &buffer, lang); defer session.deinit(); - session.addEnvironment(.{ .Name = lang.shell }, "", .shell) catch { + session.addEnvironment(.{ .Name = lang.shell }, null, .shell) catch { try info_line.addMessage(lang.err_alloc, config.error_bg, config.error_fg); }; if (build_options.enable_x11_support) { if (config.xinitrc) |xinitrc| { - session.addEnvironment(.{ .Name = lang.xinitrc, .Exec = xinitrc }, "", .xinitrc) catch { + session.addEnvironment(.{ .Name = lang.xinitrc, .Exec = xinitrc }, null, .xinitrc) catch { try info_line.addMessage(lang.err_alloc, config.error_bg, config.error_fg); }; } diff --git a/src/tui/components/Session.zig b/src/tui/components/Session.zig index 273c18a..fbc2620 100644 --- a/src/tui/components/Session.zig +++ b/src/tui/components/Session.zig @@ -16,8 +16,8 @@ const Session = @This(); pub const Environment = struct { entry_ini: ?Ini(Entry) = null, name: [:0]const u8 = "", - xdg_session_desktop: [:0]const u8 = "", - xdg_desktop_names: ?[:0]const u8 = "", + xdg_session_desktop: ?[:0]const u8 = null, + xdg_desktop_names: ?[:0]const u8 = null, cmd: []const u8 = "", specifier: []const u8 = "", display_server: DisplayServer = .wayland, @@ -26,7 +26,7 @@ pub const Environment = struct { const DesktopEntry = struct { Exec: []const u8 = "", Name: [:0]const u8 = "", - DesktopNames: ?[]const u8 = null, + DesktopNames: ?[:0]u8 = null, }; pub const Entry = struct { @"Desktop Entry": DesktopEntry = .{} }; @@ -44,34 +44,25 @@ pub fn init(allocator: Allocator, buffer: *TerminalBuffer, lang: Lang) Session { pub fn deinit(self: Session) void { for (self.label.list.items) |*environment| { if (environment.entry_ini) |*entry_ini| entry_ini.deinit(); - if (environment.xdg_desktop_names) |desktop_name| self.label.allocator.free(desktop_name); - self.label.allocator.free(environment.xdg_session_desktop); + if (environment.xdg_session_desktop) |session_desktop| self.label.allocator.free(session_desktop); } self.label.deinit(); } -pub fn addEnvironment(self: *Session, entry: DesktopEntry, xdg_session_desktop: []const u8, display_server: DisplayServer) !void { +pub fn addEnvironment(self: *Session, entry: DesktopEntry, xdg_session_desktop: ?[:0]const u8, display_server: DisplayServer) !void { var xdg_desktop_names: ?[:0]const u8 = null; if (entry.DesktopNames) |desktop_names| { - const desktop_names_z = try self.label.allocator.dupeZ(u8, desktop_names); - for (desktop_names_z) |*c| { + for (desktop_names) |*c| { if (c.* == ';') c.* = ':'; } - xdg_desktop_names = desktop_names_z; + xdg_desktop_names = desktop_names; } - errdefer { - if (xdg_desktop_names) |desktop_names| self.label.allocator.free(desktop_names); - } - - const session_desktop = try self.label.allocator.dupeZ(u8, xdg_session_desktop); - errdefer self.label.allocator.free(session_desktop); - try self.label.addItem(.{ .entry_ini = null, .name = entry.Name, - .xdg_session_desktop = session_desktop, + .xdg_session_desktop = xdg_session_desktop, .xdg_desktop_names = xdg_desktop_names, .cmd = entry.Exec, .specifier = switch (display_server) { @@ -83,28 +74,20 @@ pub fn addEnvironment(self: *Session, entry: DesktopEntry, xdg_session_desktop: }); } -pub fn addEnvironmentWithIni(self: *Session, entry_ini: Ini(Entry), xdg_session_desktop: []const u8, display_server: DisplayServer) !void { +pub fn addEnvironmentWithIni(self: *Session, entry_ini: Ini(Entry), xdg_session_desktop: ?[:0]const u8, display_server: DisplayServer) !void { const entry = entry_ini.data.@"Desktop Entry"; var xdg_desktop_names: ?[:0]const u8 = null; if (entry.DesktopNames) |desktop_names| { - const desktop_names_z = try self.label.allocator.dupeZ(u8, desktop_names); - for (desktop_names_z) |*c| { + for (desktop_names) |*c| { if (c.* == ';') c.* = ':'; } - xdg_desktop_names = desktop_names_z; + xdg_desktop_names = desktop_names; } - errdefer { - if (xdg_desktop_names) |desktop_names| self.label.allocator.free(desktop_names); - } - - const session_desktop = try self.label.allocator.dupeZ(u8, xdg_session_desktop); - errdefer self.label.allocator.free(session_desktop); - try self.label.addItem(.{ .entry_ini = entry_ini, .name = entry.Name, - .xdg_session_desktop = session_desktop, + .xdg_session_desktop = xdg_session_desktop, .xdg_desktop_names = xdg_desktop_names, .cmd = entry.Exec, .specifier = switch (display_server) { @@ -139,7 +122,10 @@ pub fn crawl(self: *Session, path: []const u8, display_server: DisplayServer) !v xdg_session_desktop = std.fs.path.stem(item.name); } - try self.addEnvironmentWithIni(entry_ini, xdg_session_desktop, display_server); + const session_desktop = try self.label.allocator.dupeZ(u8, xdg_session_desktop); + errdefer self.label.allocator.free(session_desktop); + + try self.addEnvironmentWithIni(entry_ini, session_desktop, display_server); } } From b73c78d2fb798f5963435a8cb8255d68dd34b240 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Sat, 3 Aug 2024 21:48:38 +0200 Subject: [PATCH 090/530] Remove tput dependency & use termbox2 instead Signed-off-by: AnErrupTion --- readme.md | 2 -- res/config.ini | 6 ------ src/auth.zig | 18 +++++------------- src/config/Config.zig | 2 -- src/config/migrator.zig | 4 +++- src/main.zig | 10 ++++------ 6 files changed, 12 insertions(+), 30 deletions(-) diff --git a/readme.md b/readme.md index 293cace..8b80b92 100644 --- a/readme.md +++ b/readme.md @@ -13,8 +13,6 @@ Ly is a lightweight TUI (ncurses-like) display manager for Linux and BSD. - Runtime (with default config): - xorg - xorg-xauth - - mcookie - - tput - shutdown ### Debian diff --git a/res/config.ini b/res/config.ini index caf1f47..06f434b 100644 --- a/res/config.ini +++ b/res/config.ini @@ -155,12 +155,6 @@ numlock = false # Service name (set to ly to use the provided pam config file) service_name = ly -# Terminal reset command (tput is faster) -term_reset_cmd = $PREFIX_DIRECTORY/bin/tput reset - -# Terminal restore cursor command -term_restore_cursor_cmd = $PREFIX_DIRECTORY/bin/tput cnorm - # Wayland setup command wayland_cmd = $CONFIG_DIRECTORY/ly/wsetup.sh diff --git a/src/auth.zig b/src/auth.zig index 983fea8..c0dc550 100644 --- a/src/auth.zig +++ b/src/auth.zig @@ -117,7 +117,9 @@ pub fn authenticate(config: Config, current_environment: Session.Environment, lo removeUtmpEntry(&entry); - try resetTerminal(pwd.pw_shell.?, config.term_reset_cmd); + // Take back control of the TTY + _ = interop.termbox.tb_init(); + _ = interop.termbox.tb_set_output_mode(interop.termbox.TB_OUTPUT_NORMAL); if (shared_err.readError()) |err| return err; } @@ -157,7 +159,8 @@ fn startSession( // Execute what the user requested std.posix.chdirZ(pwd.pw_dir.?) catch return error.ChangeDirectoryFailed; - try resetTerminal(pwd.pw_shell.?, config.term_reset_cmd); + // Give up control on the TTY + _ = interop.termbox.tb_shutdown(); switch (current_environment.display_server) { .wayland => try executeWaylandCmd(pwd.pw_shell.?, config.wayland_cmd, current_environment.cmd), @@ -270,17 +273,6 @@ fn loginConv( return status; } -fn resetTerminal(shell: [*:0]const u8, term_reset_cmd: [:0]const u8) !void { - const pid = try std.posix.fork(); - if (pid == 0) { - const args = [_:null]?[*:0]const u8{ shell, "-c", term_reset_cmd }; - std.posix.execveZ(shell, &args, std.c.environ) catch {}; - std.process.exit(1); - } - - _ = std.posix.waitpid(pid, 0); -} - fn getFreeDisplay() !u8 { var buf: [15]u8 = undefined; var i: u8 = 0; diff --git a/src/config/Config.zig b/src/config/Config.zig index 1680327..0726d39 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -41,8 +41,6 @@ shutdown_cmd: []const u8 = "/sbin/shutdown -a now", shutdown_key: []const u8 = "F1", sleep_cmd: ?[]const u8 = null, sleep_key: []const u8 = "F3", -term_reset_cmd: [:0]const u8 = build_options.prefix_directory ++ "/bin/tput reset", -term_restore_cursor_cmd: []const u8 = build_options.prefix_directory ++ "/bin/tput cnorm", tty: u8 = build_options.tty, vi_mode: bool = false, vi_default_mode: ViMode = .normal, diff --git a/src/config/migrator.zig b/src/config/migrator.zig index 66d4cfc..183dde9 100644 --- a/src/config/migrator.zig +++ b/src/config/migrator.zig @@ -63,7 +63,9 @@ pub fn configFieldHandler(_: std.mem.Allocator, field: ini.IniField) ?ini.IniFie std.mem.eql(u8, field.key, "max_desktop_len") or std.mem.eql(u8, field.key, "max_login_len") or std.mem.eql(u8, field.key, "max_password_len") or - std.mem.eql(u8, field.key, "mcookie_cmd")) + std.mem.eql(u8, field.key, "mcookie_cmd") or + std.mem.eql(u8, field.key, "term_reset_cmd") or + std.mem.eql(u8, field.key, "term_restore_cursor_cmd")) { // The options don't exist anymore mapped_config_fields = true; diff --git a/src/main.zig b/src/main.zig index 60f53fa..f22b1b3 100644 --- a/src/main.zig +++ b/src/main.zig @@ -712,15 +712,13 @@ pub fn main() !void { } try std.posix.tcsetattr(std.posix.STDIN_FILENO, .FLUSH, tb_termios); - if (auth_fails < config.auth_fails) { - _ = termbox.tb_clear(); - _ = termbox.tb_present(); - } + if (auth_fails < config.auth_fails) _ = termbox.tb_clear(); update = true; - var restore_cursor = std.process.Child.init(&[_][]const u8{ "/bin/sh", "-c", config.term_restore_cursor_cmd }, allocator); - _ = restore_cursor.spawnAndWait() catch .{}; + // Restore the cursor + _ = termbox.tb_set_cursor(0, 0); + _ = termbox.tb_present(); }, else => { if (!insert_mode) { From 7b9f03176dbc3d1013d4d5da3650483d9d2ec693 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Sun, 4 Aug 2024 11:04:23 +0200 Subject: [PATCH 091/530] FreeBSD fixes Signed-off-by: AnErrupTion --- src/auth.zig | 12 ++++++------ src/interop.zig | 8 +++++++- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/src/auth.zig b/src/auth.zig index c0dc550..7b8f136 100644 --- a/src/auth.zig +++ b/src/auth.zig @@ -66,12 +66,12 @@ pub fn authenticate(config: Config, current_environment: Session.Environment, lo if (status != interop.pam.PAM_SUCCESS) return pamDiagnose(status); defer status = interop.pam.pam_close_session(handle, 0); - var pwd: *interop.pwd.passwd = undefined; + var pwd: *interop.passwd = undefined; { - defer interop.pwd.endpwent(); + defer interop.endpwent(); // Get password structure from username - pwd = interop.pwd.getpwnam(login) orelse return error.GetPasswordNameFailed; + pwd = interop.getpwnam(login) orelse return error.GetPasswordNameFailed; } // Set user shell if it hasn't already been set @@ -126,7 +126,7 @@ pub fn authenticate(config: Config, current_environment: Session.Environment, lo fn startSession( config: Config, - pwd: *interop.pwd.passwd, + pwd: *interop.passwd, handle: ?*interop.pam.pam_handle, current_environment: Session.Environment, ) !void { @@ -136,7 +136,7 @@ fn startSession( if (status != 0) return error.GroupInitializationFailed; // FreeBSD sets the GID and UID with setusercontext() - const result = std.c.setusercontext(null, pwd, pwd.pw_uid, interop.logincap.LOGIN_SETALL); + const result = interop.logincap.setusercontext(null, pwd, pwd.pw_uid, interop.logincap.LOGIN_SETALL); if (result != 0) return error.SetUserUidFailed; } else { const status = interop.grp.initgroups(pwd.pw_name, pwd.pw_gid); @@ -173,7 +173,7 @@ fn startSession( } } -fn initEnv(pwd: *interop.pwd.passwd, path_env: ?[:0]const u8) !void { +fn initEnv(pwd: *interop.passwd, path_env: ?[:0]const u8) !void { _ = interop.stdlib.setenv("HOME", pwd.pw_dir, 1); _ = interop.stdlib.setenv("PWD", pwd.pw_dir, 1); _ = interop.stdlib.setenv("SHELL", pwd.pw_shell, 1); diff --git a/src/interop.zig b/src/interop.zig index e2f4ea4..aba5d8c 100644 --- a/src/interop.zig +++ b/src/interop.zig @@ -41,8 +41,9 @@ pub const grp = @cImport({ @cInclude("grp.h"); }); -// FreeBSD-specific headers +// FreeBSD-specific headers (except for pwd.h) pub const logincap = @cImport({ + @cInclude("pwd.h"); @cInclude("login_cap.h"); }); @@ -60,6 +61,11 @@ pub const vt = @cImport({ @cInclude("sys/vt.h"); }); +// On FreeBSD, login_cap.h references the passwd struct directly, so we must use logincap.passwd instead +pub const passwd = if (builtin.os.tag == .freebsd) logincap.passwd else pwd.passwd; +pub const endpwent = if (builtin.os.tag == .freebsd) logincap.endpwent else pwd.endpwent; +pub const getpwnam = if (builtin.os.tag == .freebsd) logincap.getpwnam else pwd.getpwnam; + // Used for getting & setting the lock state const LedState = if (builtin.os.tag.isBSD()) c_int else c_char; const get_led_state = if (builtin.os.tag.isBSD()) kbio.KDGETLED else kd.KDGKBLED; From 37061269a41bb47c591da1dcc00b12adbd231a18 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Sun, 4 Aug 2024 17:08:50 +0200 Subject: [PATCH 092/530] Remove config.save_file Signed-off-by: AnErrupTion --- res/config.ini | 6 ----- src/config/Config.zig | 1 - src/config/migrator.zig | 55 ++++++++++++++++++++++++++--------------- src/main.zig | 36 ++++++++++----------------- 4 files changed, 48 insertions(+), 50 deletions(-) diff --git a/res/config.ini b/res/config.ini index 06f434b..64dbb3d 100644 --- a/res/config.ini +++ b/res/config.ini @@ -105,12 +105,6 @@ load = true # Save the current desktop and login as defaults save = true -# Deprecated - Will be removed in a future version -# New save files are now loaded from the same directory as the config -# Currently used to migrate old save files to the new version -# File in which to save and load the default desktop and login -save_file = $CONFIG_DIRECTORY/ly/save - # Remove power management command hints hide_key_hints = false diff --git a/src/config/Config.zig b/src/config/Config.zig index 0726d39..beb8e41 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -35,7 +35,6 @@ path: ?[:0]const u8 = "/sbin:/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/ restart_cmd: []const u8 = "/sbin/shutdown -r now", restart_key: []const u8 = "F2", save: bool = true, -save_file: []const u8 = build_options.config_directory ++ "/ly/save", service_name: [:0]const u8 = "ly", shutdown_cmd: []const u8 = "/sbin/shutdown -a now", shutdown_key: []const u8 = "F1", diff --git a/src/config/migrator.zig b/src/config/migrator.zig index 183dde9..93c470f 100644 --- a/src/config/migrator.zig +++ b/src/config/migrator.zig @@ -5,7 +5,10 @@ const ini = @import("zigini"); const Save = @import("Save.zig"); const enums = @import("../enums.zig"); -var maybe_animate: ?bool = null; +var temporary_allocator = std.heap.page_allocator; + +pub var maybe_animate: ?bool = null; +pub var maybe_save_file: ?[]const u8 = null; pub var mapped_config_fields = false; @@ -59,6 +62,14 @@ pub fn configFieldHandler(_: std.mem.Allocator, field: ini.IniField) ?ini.IniFie return mapped_field; } + 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; + + mapped_config_fields = true; + return null; + } + if (std.mem.eql(u8, field.key, "wayland_specifier") or std.mem.eql(u8, field.key, "max_desktop_len") or std.mem.eql(u8, field.key, "max_login_len") or @@ -78,34 +89,38 @@ 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(animation: *enums.Animation) void { - if (maybe_animate == null) return; - - if (!maybe_animate.?) animation.* = .none; + if (maybe_animate) |animate| { + if (!animate) animation.* = .none; + } } -pub fn tryMigrateSaveFile(user_buf: *[32]u8, path: []const u8) Save { +pub fn tryMigrateSaveFile(user_buf: *[32]u8) Save { var save = Save{}; - var file = std.fs.openFileAbsolute(path, .{}) catch return save; - defer file.close(); + if (maybe_save_file) |path| { + defer temporary_allocator.free(path); - const reader = file.reader(); + var file = std.fs.openFileAbsolute(path, .{}) catch return save; + defer file.close(); - var user_fbs = std.io.fixedBufferStream(user_buf); - reader.streamUntilDelimiter(user_fbs.writer(), '\n', 32) catch return save; - const user = user_fbs.getWritten(); - if (user.len > 0) save.user = user; + const reader = file.reader(); - var session_buf: [20]u8 = undefined; - var session_fbs = std.io.fixedBufferStream(&session_buf); - reader.streamUntilDelimiter(session_fbs.writer(), '\n', 20) catch {}; + var user_fbs = std.io.fixedBufferStream(user_buf); + reader.streamUntilDelimiter(user_fbs.writer(), '\n', 32) catch return save; + const user = user_fbs.getWritten(); + if (user.len > 0) save.user = user; - const session_index_str = session_fbs.getWritten(); - var session_index: ?usize = null; - if (session_index_str.len > 0) { - session_index = std.fmt.parseUnsigned(usize, session_index_str, 10) catch return save; + var session_buf: [20]u8 = undefined; + var session_fbs = std.io.fixedBufferStream(&session_buf); + reader.streamUntilDelimiter(session_fbs.writer(), '\n', 20) catch {}; + + const session_index_str = session_fbs.getWritten(); + var session_index: ?usize = null; + if (session_index_str.len > 0) { + session_index = std.fmt.parseUnsigned(usize, session_index_str, 10) catch return save; + } + save.session_index = session_index; } - save.session_index = session_index; return save; } diff --git a/src/main.zig b/src/main.zig index f22b1b3..ec4a2f2 100644 --- a/src/main.zig +++ b/src/main.zig @@ -142,20 +142,10 @@ pub fn main() !void { save_path_alloc = true; var user_buf: [32]u8 = undefined; - save = save_ini.readFileToStruct(save_path, comment_characters, null) catch migrator.tryMigrateSaveFile(&user_buf, config.save_file); + save = save_ini.readFileToStruct(save_path, comment_characters, null) catch migrator.tryMigrateSaveFile(&user_buf); } migrator.lateConfigFieldHandler(&config.animation); - - // if (migrator.mapped_config_fields) save_migrated_config: { - // var file = try std.fs.cwd().createFile(config_path, .{}); - // defer file.close(); - - // const writer = file.writer(); - // ini.writeFromStruct(config, writer, null, true, .{}) catch { - // break :save_migrated_config; - // }; - // } } else { const config_path = build_options.config_directory ++ "/ly/config.ini"; @@ -171,22 +161,22 @@ pub fn main() !void { if (config.load) { var user_buf: [32]u8 = undefined; - save = save_ini.readFileToStruct(save_path, comment_characters, null) catch migrator.tryMigrateSaveFile(&user_buf, config.save_file); + save = save_ini.readFileToStruct(save_path, comment_characters, null) catch migrator.tryMigrateSaveFile(&user_buf); } migrator.lateConfigFieldHandler(&config.animation); - - // if (migrator.mapped_config_fields) save_migrated_config: { - // var file = try std.fs.cwd().createFile(config_path, .{}); - // defer file.close(); - - // const writer = file.writer(); - // ini.writeFromStruct(config, writer, null, true, .{}) catch { - // break :save_migrated_config; - // }; - // } } + // if (migrator.mapped_config_fields) save_migrated_config: { + // var file = try std.fs.cwd().createFile(config_path, .{}); + // defer file.close(); + + // const writer = file.writer(); + // ini.writeFromStruct(config, writer, null, true, .{}) catch { + // break :save_migrated_config; + // }; + // } + // 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); @@ -669,7 +659,7 @@ pub fn main() !void { ini.writeFromStruct(save_data, file.writer(), null, true, .{}) catch break :save_last_settings; // Delete previous save file if it exists - std.fs.cwd().deleteFile(config.save_file) catch {}; + if (migrator.maybe_save_file) |path| std.fs.cwd().deleteFile(path) catch {}; } var shared_err = try SharedError.init(); From ef86ea19aca88b54e342b846b8726424d78c1745 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Sun, 4 Aug 2024 19:40:26 +0200 Subject: [PATCH 093/530] Update termbox2 Signed-off-by: AnErrupTion --- build.zig | 7 +------ include/termbox2.h | 45 ++++++++++++++++++++++++++++++++++++--------- 2 files changed, 37 insertions(+), 15 deletions(-) diff --git a/build.zig b/build.zig index b9b2a14..2686f09 100644 --- a/build.zig +++ b/build.zig @@ -69,20 +69,15 @@ pub fn build(b: *std.Build) !void { if (enable_x11_support) exe.linkSystemLibrary("xcb"); exe.linkLibC(); - // HACK: Only fails with ReleaseSafe, so we'll override it. const translate_c = b.addTranslateC(.{ .root_source_file = b.path("include/termbox2.h"), .target = target, - .optimize = if (optimize == .ReleaseSafe) .ReleaseFast else optimize, + .optimize = optimize, }); translate_c.defineCMacroRaw("TB_IMPL"); const termbox2 = translate_c.addModule("termbox2"); exe.root_module.addImport("termbox2", termbox2); - if (optimize == .ReleaseSafe) { - std.debug.print("warn: termbox2 module is being built in ReleaseFast due to a bug.\n", .{}); - } - b.installArtifact(exe); const run_cmd = b.addRunArtifact(exe); diff --git a/include/termbox2.h b/include/termbox2.h index 4f1088d..5fc791f 100644 --- a/include/termbox2.h +++ b/include/termbox2.h @@ -23,8 +23,8 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -#ifndef __TERMBOX_H -#define __TERMBOX_H +#ifndef TERMBOX_H_INCL +#define TERMBOX_H_INCL #ifndef _XOPEN_SOURCE #define _XOPEN_SOURCE @@ -105,7 +105,7 @@ extern "C" { #elif defined TB_OPT_ATTR_W && TB_OPT_ATTR_W == 64 #else #undef TB_OPT_ATTR_W -#if defined TB_OPT_TRUECOLOR // Back-compat for old flag +#if defined TB_OPT_TRUECOLOR // Deprecated. Back-compat for old flag. #define TB_OPT_ATTR_W 32 #else #define TB_OPT_ATTR_W 16 @@ -347,7 +347,7 @@ extern "C" { #define TB_ERR_SELECT TB_ERR_POLL #define TB_ERR_RESIZE_SELECT TB_ERR_RESIZE_POLL -/* Function types to be used with tb_set_func() */ +/* Deprecated. Function types to be used with tb_set_func(). */ #define TB_FUNC_EXTRACT_PRE 0 #define TB_FUNC_EXTRACT_POST 1 @@ -646,8 +646,8 @@ int tb_printf_ex(int x, int y, uintattr_t fg, uintattr_t bg, size_t *out_w, int tb_send(const char *buf, size_t nbuf); int tb_sendf(const char *fmt, ...); -/* Set custom functions. fn_type is one of TB_FUNC_* constants, fn is a - * compatible function pointer, or NULL to clear. +/* Deprecated. Set custom functions. fn_type is one of TB_FUNC_* constants, fn + * is a compatible function pointer, or NULL to clear. * * TB_FUNC_EXTRACT_PRE: * If specified, invoke this function BEFORE termbox tries to extract any @@ -683,17 +683,35 @@ int tb_utf8_unicode_to_char(char *out, uint32_t c); /* Library utility functions */ int tb_last_errno(void); const char *tb_strerror(int err); -struct tb_cell *tb_cell_buffer(void); +struct tb_cell *tb_cell_buffer(void); // Deprecated int tb_has_truecolor(void); int tb_has_egc(void); int tb_attr_width(void); const char *tb_version(void); +/* Deprecation notice! + * + * The following will be removed in version 3.x (ABI version 3): + * + * TB_256_BLACK (use TB_HI_BLACK) + * TB_OPT_TRUECOLOR (use TB_OPT_ATTR_W) + * TB_TRUECOLOR_BOLD (use TB_BOLD) + * TB_TRUECOLOR_UNDERLINE (use TB_UNDERLINE) + * TB_TRUECOLOR_REVERSE (use TB_REVERSE) + * TB_TRUECOLOR_ITALIC (use TB_ITALICe) + * TB_TRUECOLOR_BLINK (use TB_BLINK) + * TB_TRUECOLOR_BLACK (use TB_HI_BLACK) + * tb_cell_buffer + * tb_set_func + * TB_FUNC_EXTRACT_PRE + * TB_FUNC_EXTRACT_POST + */ + #ifdef __cplusplus } #endif -#endif /* __TERMBOX_H */ +#endif /* TERMBOX_H_INCL */ #ifdef TB_IMPL @@ -1648,6 +1666,7 @@ int tb_present(void) { send_attr(back->fg, back->bg); if (w > 1 && x >= global.front.width - (w - 1)) { + // Not enough room for wide char, send spaces for (i = x; i < global.front.width; i++) { send_char(i, y, ' '); } @@ -1660,12 +1679,20 @@ int tb_present(void) { #endif send_char(x, y, back->ch); } + + // When wcwidth>1, we need to advance the cursor by more + // than 1, thereby skipping some cells. Set these skipped + // cells to an invalid codepoint in the front buffer, so + // that if this cell is later replaced by a wcwidth==1 char, + // we'll get a cell_cmp diff for the skipped cells and + // properly re-render. for (i = 1; i < w; i++) { struct tb_cell *front_wide; + uint32_t invalid = -1; if_err_return(rv, cellbuf_get(&global.front, x + i, y, &front_wide)); if_err_return(rv, - cell_set(front_wide, 0, 1, back->fg, back->bg)); + cell_set(front_wide, &invalid, 1, -1, -1)); } } } From c7f70ac78f84cdf566d30726a849ee92b95c3c22 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Sun, 4 Aug 2024 20:42:00 +0200 Subject: [PATCH 094/530] Handle termbox2 outside of authentication Signed-off-by: AnErrupTion --- src/auth.zig | 7 ------- src/main.zig | 15 +++++++++++---- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/auth.zig b/src/auth.zig index 7b8f136..ed28473 100644 --- a/src/auth.zig +++ b/src/auth.zig @@ -117,10 +117,6 @@ pub fn authenticate(config: Config, current_environment: Session.Environment, lo removeUtmpEntry(&entry); - // Take back control of the TTY - _ = interop.termbox.tb_init(); - _ = interop.termbox.tb_set_output_mode(interop.termbox.TB_OUTPUT_NORMAL); - if (shared_err.readError()) |err| return err; } @@ -159,9 +155,6 @@ fn startSession( // Execute what the user requested std.posix.chdirZ(pwd.pw_dir.?) catch return error.ChangeDirectoryFailed; - // Give up control on the TTY - _ = interop.termbox.tb_shutdown(); - switch (current_environment.display_server) { .wayland => try executeWaylandCmd(pwd.pw_shell.?, config.wayland_cmd, current_environment.cmd), .shell => try executeShellCmd(pwd.pw_shell.?), diff --git a/src/main.zig b/src/main.zig index ec4a2f2..1c412f8 100644 --- a/src/main.zig +++ b/src/main.zig @@ -648,6 +648,11 @@ pub fn main() !void { update = true; }, termbox.TB_KEY_ENTER => { + try info_line.addMessage(lang.authenticating, config.bg, config.fg); + InfoLine.clearRendered(allocator, buffer) catch {}; + info_line.label.draw(); + _ = termbox.tb_present(); + if (config.save) save_last_settings: { var file = std.fs.cwd().createFile(save_path, .{}) catch break :save_last_settings; defer file.close(); @@ -671,10 +676,8 @@ pub fn main() !void { const password_text = try allocator.dupeZ(u8, password.text.items); defer allocator.free(password_text); - try info_line.addMessage(lang.authenticating, config.bg, config.fg); - InfoLine.clearRendered(allocator, buffer) catch {}; - info_line.label.draw(); - _ = termbox.tb_present(); + // Give up control on the TTY + _ = termbox.tb_shutdown(); session_pid = try std.posix.fork(); if (session_pid == 0) { @@ -690,6 +693,10 @@ pub fn main() !void { session_pid = -1; } + // Take back control of the TTY + _ = termbox.tb_init(); + _ = termbox.tb_set_output_mode(termbox.TB_OUTPUT_NORMAL); + const auth_err = shared_err.readError(); if (auth_err) |err| { auth_fails += 1; From 6fbbb4eff0857ae32802ae216f99d4f66813d206 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Mon, 5 Aug 2024 00:58:29 +0200 Subject: [PATCH 095/530] Consolidate xsetup.sh & wsetup.sh into one file Signed-off-by: AnErrupTion --- build.zig | 9 ++-- res/config.ini | 7 +-- res/setup.sh | 107 ++++++++++++++++++++++++++++++++++++++++ src/auth.zig | 8 +-- src/config/Config.zig | 3 +- src/config/migrator.zig | 4 +- 6 files changed, 120 insertions(+), 18 deletions(-) create mode 100755 res/setup.sh diff --git a/build.zig b/build.zig index 2686f09..f0e9074 100644 --- a/build.zig +++ b/build.zig @@ -269,11 +269,8 @@ fn install_ly(allocator: std.mem.Allocator, install_config: bool) !void { try patch_map.put("$CONFIG_DIRECTORY", config_directory); - const patched_xsetup = try patchFile(allocator, "res/xsetup.sh", patch_map); - const patched_wsetup = try patchFile(allocator, "res/wsetup.sh", patch_map); - - try installText(patched_xsetup, config_dir, ly_config_directory, "xsetup.sh", .{ .mode = 0o755 }); - try installText(patched_wsetup, config_dir, ly_config_directory, "wsetup.sh", .{ .mode = 0o755 }); + const patched_setup = try patchFile(allocator, "res/setup.sh", patch_map); + try installText(patched_setup, config_dir, ly_config_directory, "setup.sh", .{ .mode = 0o755 }); } } @@ -317,7 +314,7 @@ fn install_ly(allocator: std.mem.Allocator, install_config: bool) !void { pub fn uninstallall(step: *std.Build.Step, _: ProgressNode) !void { const allocator = step.owner.allocator; - try deleteTree(allocator, config_directory, "/ly", "ly data directory not found"); + try deleteTree(allocator, config_directory, "/ly", "ly config directory not found"); const exe_path = try std.fs.path.join(allocator, &[_][]const u8{ dest_directory, prefix_directory, "/bin/", executable_name }); var success = true; diff --git a/res/config.ini b/res/config.ini index 64dbb3d..8650bf0 100644 --- a/res/config.ini +++ b/res/config.ini @@ -149,8 +149,8 @@ numlock = false # Service name (set to ly to use the provided pam config file) service_name = ly -# Wayland setup command -wayland_cmd = $CONFIG_DIRECTORY/ly/wsetup.sh +# Setup command +setup_cmd = $CONFIG_DIRECTORY/ly/setup.sh # Wayland desktop environments waylandsessions = $PREFIX_DIRECTORY/share/wayland-sessions @@ -162,9 +162,6 @@ xinitrc = ~/.xinitrc # Xorg server command x_cmd = $PREFIX_DIRECTORY/bin/X -# Xorg setup command -x_cmd_setup = $CONFIG_DIRECTORY/ly/xsetup.sh - # Xorg xauthority edition tool xauth_cmd = $PREFIX_DIRECTORY/bin/xauth diff --git a/res/setup.sh b/res/setup.sh new file mode 100755 index 0000000..549994a --- /dev/null +++ b/res/setup.sh @@ -0,0 +1,107 @@ +#!/bin/sh +# Shell environment setup after login +# Copyright (C) 2015-2016 Pier Luigi Fiorini + +# This file is extracted from kde-workspace (kdm/kfrontend/genkdmconf.c) +# Copyright (C) 2001-2005 Oswald Buddenhagen + +# Copyright (C) 2024 The Fairy Glade +# This work is free. You can redistribute it and/or modify it under the +# terms of the Do What The Fuck You Want To Public License, Version 2, +# as published by Sam Hocevar. See the LICENSE file for more details. + +# Note that the respective logout scripts are not sourced. +case $SHELL in +*/bash) + [ -z "$BASH" ] && exec $SHELL "$0" "$@" + set +o posix + [ -f "$CONFIG_DIRECTORY"/profile ] && . "$CONFIG_DIRECTORY"/profile + if [ -f "$HOME"/.bash_profile ]; then + . "$HOME"/.bash_profile + elif [ -f "$HOME"/.bash_login ]; then + . "$HOME"/.bash_login + elif [ -f "$HOME"/.profile ]; then + . "$HOME"/.profile + fi + ;; +*/zsh) + [ -z "$ZSH_NAME" ] && exec $SHELL "$0" "$@" + [ -d "$CONFIG_DIRECTORY"/zsh ] && zdir="$CONFIG_DIRECTORY"/zsh || zdir="$CONFIG_DIRECTORY" + zhome=${ZDOTDIR:-"$HOME"} + # zshenv is always sourced automatically. + [ -f "$zdir"/zprofile ] && . "$zdir"/zprofile + [ -f "$zhome"/.zprofile ] && . "$zhome"/.zprofile + [ -f "$zdir"/zlogin ] && . "$zdir"/zlogin + [ -f "$zhome"/.zlogin ] && . "$zhome"/.zlogin + emulate -R sh + ;; +*/csh|*/tcsh) + # [t]cshrc is always sourced automatically. + # Note that sourcing csh.login after .cshrc is non-standard. + sess_tmp=$(mktemp /tmp/sess-env-XXXXXX) + $SHELL -c "if (-f $CONFIG_DIRECTORY/csh.login) source $CONFIG_DIRECTORY/csh.login; if (-f ~/.login) source ~/.login; /bin/sh -c 'export -p' >! $sess_tmp" + . "$sess_tmp" + rm -f "$sess_tmp" + ;; +*/fish) + [ -f "$CONFIG_DIRECTORY"/profile ] && . "$CONFIG_DIRECTORY"/profile + [ -f "$HOME"/.profile ] && . "$HOME"/.profile + sess_tmp=$(mktemp /tmp/sess-env-XXXXXX) + $SHELL --login -c "/bin/sh -c 'export -p' > $sess_tmp" + . "$sess_tmp" + rm -f "$sess_tmp" + ;; +*) # Plain sh, ksh, and anything we do not know. + [ -f "$CONFIG_DIRECTORY"/profile ] && . "$CONFIG_DIRECTORY"/profile + [ -f "$HOME"/.profile ] && . "$HOME"/.profile + ;; +esac + +if [ "$XDG_SESSION_TYPE" = "x11" ]; then + [ -f "$CONFIG_DIRECTORY"/xprofile ] && . "$CONFIG_DIRECTORY"/xprofile + [ -f "$HOME"/.xprofile ] && . "$HOME"/.xprofile + + # run all system xinitrc shell scripts. + if [ -d "$CONFIG_DIRECTORY"/X11/xinit/xinitrc.d ]; then + for i in "$CONFIG_DIRECTORY"/X11/xinit/xinitrc.d/* ; do + if [ -x "$i" ]; then + . "$i" + fi + done + fi + + # Load Xsession scripts + # OPTIONFILE, USERXSESSION, USERXSESSIONRC and ALTUSERXSESSION are required + # by the scripts to work + xsessionddir="$CONFIG_DIRECTORY"/X11/Xsession.d + export OPTIONFILE="$CONFIG_DIRECTORY"/X11/Xsession.options + export USERXSESSION="$HOME"/.xsession + export USERXSESSIONRC="$HOME"/.xsessionrc + export ALTUSERXSESSION="$HOME"/.Xsession + + if [ -d "$xsessionddir" ]; then + for i in $(ls "$xsessionddir"); do + script="$xsessionddir/$i" + echo "Loading X session script $script" + if [ -r "$script" ] && [ -f "$script" ] && expr "$i" : '^[[:alnum:]_-]\+$' > /dev/null; then + . "$script" + fi + done + fi + + if [ -d "$CONFIG_DIRECTORY"/X11/Xresources ]; then + for i in "$CONFIG_DIRECTORY"/X11/Xresources/*; do + [ -f "$i" ] && xrdb -merge "$i" + done + elif [ -f "$CONFIG_DIRECTORY"/X11/Xresources ]; then + xrdb -merge "$CONFIG_DIRECTORY"/X11/Xresources + fi + [ -f "$HOME"/.Xresources ] && xrdb -merge "$HOME"/.Xresources + [ -f "$XDG_CONFIG_HOME"/X11/Xresources ] && xrdb -merge "$XDG_CONFIG_HOME"/X11/Xresources + + if [ -f "$USERXSESSION" ]; then + . "$USERXSESSION" + fi +fi + +exec "$@" diff --git a/src/auth.zig b/src/auth.zig index ed28473..1abc39c 100644 --- a/src/auth.zig +++ b/src/auth.zig @@ -156,7 +156,7 @@ fn startSession( std.posix.chdirZ(pwd.pw_dir.?) catch return error.ChangeDirectoryFailed; switch (current_environment.display_server) { - .wayland => try executeWaylandCmd(pwd.pw_shell.?, config.wayland_cmd, current_environment.cmd), + .wayland => try executeWaylandCmd(pwd.pw_shell.?, config.setup_cmd, current_environment.cmd), .shell => try executeShellCmd(pwd.pw_shell.?), .xinitrc, .x11 => if (build_options.enable_x11_support) { var vt_buf: [5]u8 = undefined; @@ -377,9 +377,9 @@ fn executeShellCmd(shell: [*:0]const u8) !void { return std.posix.execveZ(shell, &args, std.c.environ); } -fn executeWaylandCmd(shell: [*:0]const u8, wayland_cmd: []const u8, desktop_cmd: []const u8) !void { +fn executeWaylandCmd(shell: [*:0]const u8, setup_cmd: []const u8, desktop_cmd: []const u8) !void { var cmd_buffer: [1024]u8 = undefined; - const cmd_str = try std.fmt.bufPrintZ(&cmd_buffer, "{s} {s}", .{ wayland_cmd, desktop_cmd }); + const cmd_str = try std.fmt.bufPrintZ(&cmd_buffer, "{s} {s}", .{ setup_cmd, desktop_cmd }); const args = [_:null]?[*:0]const u8{ shell, "-c", cmd_str }; return std.posix.execveZ(shell, &args, std.c.environ); } @@ -416,7 +416,7 @@ fn executeX11Cmd(shell: [*:0]const u8, pw_dir: [*:0]const u8, config: Config, de 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}", .{ config.x_cmd_setup, desktop_cmd }) catch std.process.exit(1); + const cmd_str = std.fmt.bufPrintZ(&cmd_buffer, "{s} {s}", .{ config.setup_cmd, desktop_cmd }) catch std.process.exit(1); const args = [_:null]?[*:0]const u8{ shell, "-c", cmd_str }; std.posix.execveZ(shell, &args, std.c.environ) catch {}; std.process.exit(1); diff --git a/src/config/Config.zig b/src/config/Config.zig index beb8e41..55c8f9c 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -36,6 +36,7 @@ restart_cmd: []const u8 = "/sbin/shutdown -r now", restart_key: []const u8 = "F2", save: bool = true, service_name: [:0]const u8 = "ly", +setup_cmd: []const u8 = build_options.config_directory ++ "/ly/setup.sh", shutdown_cmd: []const u8 = "/sbin/shutdown -a now", shutdown_key: []const u8 = "F1", sleep_cmd: ?[]const u8 = null, @@ -43,11 +44,9 @@ sleep_key: []const u8 = "F3", tty: u8 = build_options.tty, vi_mode: bool = false, vi_default_mode: ViMode = .normal, -wayland_cmd: []const u8 = build_options.config_directory ++ "/ly/wsetup.sh", waylandsessions: []const u8 = build_options.prefix_directory ++ "/share/wayland-sessions", x_cmd: []const u8 = build_options.prefix_directory ++ "/bin/X", xinitrc: ?[]const u8 = "~/.xinitrc", -x_cmd_setup: []const u8 = build_options.config_directory ++ "/ly/xsetup.sh", xauth_cmd: []const u8 = build_options.prefix_directory ++ "/bin/xauth", xsessions: []const u8 = build_options.prefix_directory ++ "/share/xsessions", brightness_down_key: []const u8 = "F5", diff --git a/src/config/migrator.zig b/src/config/migrator.zig index 93c470f..df028c4 100644 --- a/src/config/migrator.zig +++ b/src/config/migrator.zig @@ -76,7 +76,9 @@ pub fn configFieldHandler(_: std.mem.Allocator, field: ini.IniField) ?ini.IniFie std.mem.eql(u8, field.key, "max_password_len") or std.mem.eql(u8, field.key, "mcookie_cmd") or std.mem.eql(u8, field.key, "term_reset_cmd") or - std.mem.eql(u8, field.key, "term_restore_cursor_cmd")) + std.mem.eql(u8, field.key, "term_restore_cursor_cmd") or + std.mem.eql(u8, field.key, "x_cmd_setup") or + std.mem.eql(u8, field.key, "wayland_cmd")) { // The options don't exist anymore mapped_config_fields = true; From 391f86f6024c98ecd19835c4aae3c05e8b604628 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Mon, 5 Aug 2024 00:59:29 +0200 Subject: [PATCH 096/530] Delete old setup scripts Signed-off-by: AnErrupTion --- res/wsetup.sh | 55 -------------------------- res/xsetup.sh | 104 -------------------------------------------------- 2 files changed, 159 deletions(-) delete mode 100755 res/wsetup.sh delete mode 100755 res/xsetup.sh diff --git a/res/wsetup.sh b/res/wsetup.sh deleted file mode 100755 index bf8b7e3..0000000 --- a/res/wsetup.sh +++ /dev/null @@ -1,55 +0,0 @@ -#!/bin/sh -# wayland-session - run as user -# Copyright (C) 2015-2016 Pier Luigi Fiorini - -# This file is extracted from kde-workspace (kdm/kfrontend/genkdmconf.c) -# Copyright (C) 2001-2005 Oswald Buddenhagen - -# Note that the respective logout scripts are not sourced. -case $SHELL in - */bash) - [ -z "$BASH" ] && exec $SHELL $0 "$@" - set +o posix - [ -f $CONFIG_DIRECTORY/profile ] && . $CONFIG_DIRECTORY/profile - if [ -f $HOME/.bash_profile ]; then - . $HOME/.bash_profile - elif [ -f $HOME/.bash_login ]; then - . $HOME/.bash_login - elif [ -f $HOME/.profile ]; then - . $HOME/.profile - fi - ;; -*/zsh) - [ -z "$ZSH_NAME" ] && exec $SHELL $0 "$@" - [ -d $CONFIG_DIRECTORY/zsh ] && zdir=$CONFIG_DIRECTORY/zsh || zdir=$CONFIG_DIRECTORY - zhome=${ZDOTDIR:-$HOME} - # zshenv is always sourced automatically. - [ -f $zdir/zprofile ] && . $zdir/zprofile - [ -f $zhome/.zprofile ] && . $zhome/.zprofile - [ -f $zdir/zlogin ] && . $zdir/zlogin - [ -f $zhome/.zlogin ] && . $zhome/.zlogin - emulate -R sh - ;; - */csh|*/tcsh) - # [t]cshrc is always sourced automatically. - # Note that sourcing csh.login after .cshrc is non-standard. - wlsess_tmp=`mktemp /tmp/wlsess-env-XXXXXX` - $SHELL -c "if (-f $CONFIG_DIRECTORY/csh.login) source $CONFIG_DIRECTORY/csh.login; if (-f ~/.login) source ~/.login; /bin/sh -c 'export -p' >! $wlsess_tmp" - . $wlsess_tmp - rm -f $wlsess_tmp - ;; - */fish) - [ -f $CONFIG_DIRECTORY/profile ] && . $CONFIG_DIRECTORY/profile - [ -f $HOME/.profile ] && . $HOME/.profile - xsess_tmp=`mktemp /tmp/xsess-env-XXXXXX` - $SHELL --login -c "/bin/sh -c 'export -p' > $xsess_tmp" - . $xsess_tmp - rm -f $xsess_tmp - ;; - *) # Plain sh, ksh, and anything we do not know. - [ -f $CONFIG_DIRECTORY/profile ] && . $CONFIG_DIRECTORY/profile - [ -f $HOME/.profile ] && . $HOME/.profile - ;; -esac - -exec "$@" diff --git a/res/xsetup.sh b/res/xsetup.sh deleted file mode 100755 index 24d0a4a..0000000 --- a/res/xsetup.sh +++ /dev/null @@ -1,104 +0,0 @@ -#!/bin/sh -# Xsession - run as user -# Copyright (C) 2016 Pier Luigi Fiorini - -# This file is extracted from kde-workspace (kdm/kfrontend/genkdmconf.c) -# Copyright (C) 2001-2005 Oswald Buddenhagen - -# Note that the respective logout scripts are not sourced. -case $SHELL in - */bash) - [ -z "$BASH" ] && exec $SHELL $0 "$@" - set +o posix - [ -f $CONFIG_DIRECTORY/profile ] && . $CONFIG_DIRECTORY/profile - if [ -f $HOME/.bash_profile ]; then - . $HOME/.bash_profile - elif [ -f $HOME/.bash_login ]; then - . $HOME/.bash_login - elif [ -f $HOME/.profile ]; then - . $HOME/.profile - fi - ;; -*/zsh) - [ -z "$ZSH_NAME" ] && exec $SHELL $0 "$@" - [ -d $CONFIG_DIRECTORY/zsh ] && zdir=$CONFIG_DIRECTORY/zsh || zdir=$CONFIG_DIRECTORY - zhome=${ZDOTDIR:-$HOME} - # zshenv is always sourced automatically. - [ -f $zdir/zprofile ] && . $zdir/zprofile - [ -f $zhome/.zprofile ] && . $zhome/.zprofile - [ -f $zdir/zlogin ] && . $zdir/zlogin - [ -f $zhome/.zlogin ] && . $zhome/.zlogin - emulate -R sh - ;; - */csh|*/tcsh) - # [t]cshrc is always sourced automatically. - # Note that sourcing csh.login after .cshrc is non-standard. - xsess_tmp=`mktemp /tmp/xsess-env-XXXXXX` - $SHELL -c "if (-f $CONFIG_DIRECTORY/csh.login) source $CONFIG_DIRECTORY/csh.login; if (-f ~/.login) source ~/.login; /bin/sh -c 'export -p' >! $xsess_tmp" - . $xsess_tmp - rm -f $xsess_tmp - ;; - */fish) - [ -f $CONFIG_DIRECTORY/profile ] && . $CONFIG_DIRECTORY/profile - [ -f $HOME/.profile ] && . $HOME/.profile - xsess_tmp=`mktemp /tmp/xsess-env-XXXXXX` - $SHELL --login -c "/bin/sh -c 'export -p' > $xsess_tmp" - . $xsess_tmp - rm -f $xsess_tmp - ;; - *) # Plain sh, ksh, and anything we do not know. - [ -f $CONFIG_DIRECTORY/profile ] && . $CONFIG_DIRECTORY/profile - [ -f $HOME/.profile ] && . $HOME/.profile - ;; -esac - -[ -f $CONFIG_DIRECTORY/xprofile ] && . $CONFIG_DIRECTORY/xprofile -[ -f $HOME/.xprofile ] && . $HOME/.xprofile - -# run all system xinitrc shell scripts. -if [ -d $CONFIG_DIRECTORY/X11/xinit/xinitrc.d ]; then - for i in $CONFIG_DIRECTORY/X11/xinit/xinitrc.d/* ; do - if [ -x "$i" ]; then - . "$i" - fi - done -fi - -# Load Xsession scripts -# OPTIONFILE, USERXSESSION, USERXSESSIONRC and ALTUSERXSESSION are required -# by the scripts to work -xsessionddir="$CONFIG_DIRECTORY/X11/Xsession.d" -OPTIONFILE=$CONFIG_DIRECTORY/X11/Xsession.options -USERXSESSION=$HOME/.xsession -USERXSESSIONRC=$HOME/.xsessionrc -ALTUSERXSESSION=$HOME/.Xsession - -if [ -d "$xsessionddir" ]; then - for i in `ls $xsessionddir`; do - script="$xsessionddir/$i" - echo "Loading X session script $script" - if [ -r "$script" -a -f "$script" ] && expr "$i" : '^[[:alnum:]_-]\+$' > /dev/null; then - . "$script" - fi - done -fi - -if [ -d $CONFIG_DIRECTORY/X11/Xresources ]; then - for i in $CONFIG_DIRECTORY/X11/Xresources/*; do - [ -f $i ] && xrdb -merge $i - done -elif [ -f $CONFIG_DIRECTORY/X11/Xresources ]; then - xrdb -merge $CONFIG_DIRECTORY/X11/Xresources -fi -[ -f $HOME/.Xresources ] && xrdb -merge $HOME/.Xresources -[ -f $XDG_CONFIG_HOME/X11/Xresources ] && xrdb -merge $XDG_CONFIG_HOME/X11/Xresources - -if [ -f "$USERXSESSION" ]; then - . "$USERXSESSION" -fi - -if [ -z "$*" ]; then - exec xmessage -center -buttons OK:0 -default OK "Sorry, $DESKTOP_SESSION is no valid session." -else - exec $@ -fi From 1075c923efa4e52bbaf3e9014f3e389b232900bf Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Mon, 5 Aug 2024 01:03:11 +0200 Subject: [PATCH 097/530] Make shell login use setup script Signed-off-by: AnErrupTion --- src/auth.zig | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/auth.zig b/src/auth.zig index 1abc39c..f2bcb97 100644 --- a/src/auth.zig +++ b/src/auth.zig @@ -157,7 +157,7 @@ fn startSession( switch (current_environment.display_server) { .wayland => try executeWaylandCmd(pwd.pw_shell.?, config.setup_cmd, current_environment.cmd), - .shell => try executeShellCmd(pwd.pw_shell.?), + .shell => try executeShellCmd(pwd.pw_shell.?, config.setup_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}", .{config.tty}); @@ -372,8 +372,10 @@ fn xauth(display_name: [:0]u8, shell: [*:0]const u8, pw_dir: [*:0]const u8, xaut if (status.status != 0) return error.XauthFailed; } -fn executeShellCmd(shell: [*:0]const u8) !void { - const args = [_:null]?[*:0]const u8{shell}; +fn executeShellCmd(shell: [*:0]const u8, setup_cmd: []const u8) !void { + var cmd_buffer: [1024]u8 = undefined; + const cmd_str = try std.fmt.bufPrintZ(&cmd_buffer, "{s} {s}", .{ setup_cmd, shell }); + const args = [_:null]?[*:0]const u8{ shell, "-c", cmd_str }; return std.posix.execveZ(shell, &args, std.c.environ); } From 071b7a21823bdc03674710150013ca0881b2a691 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Mon, 5 Aug 2024 10:41:34 +0200 Subject: [PATCH 098/530] Make all colors u16 Signed-off-by: AnErrupTion --- src/config/Config.zig | 4 ++-- src/tui/TerminalBuffer.zig | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/config/Config.zig b/src/config/Config.zig index 55c8f9c..cd87bbb 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -11,7 +11,7 @@ asterisk: ?u8 = '*', bg: u16 = 0, bigclock: bool = false, blank_box: bool = true, -border_fg: u8 = 8, +border_fg: u16 = 8, box_title: ?[]const u8 = null, clear_password: bool = false, clock: ?[:0]const u8 = null, @@ -20,7 +20,7 @@ default_input: Input = .login, error_bg: u16 = 0, error_fg: u16 = 258, fg: u16 = 8, -cmatrix_fg: u8 = 3, +cmatrix_fg: u16 = 3, hide_borders: bool = false, hide_key_hints: bool = false, initial_info_text: ?[]const u8 = null, diff --git a/src/tui/TerminalBuffer.zig b/src/tui/TerminalBuffer.zig index 2d36a54..472eadc 100644 --- a/src/tui/TerminalBuffer.zig +++ b/src/tui/TerminalBuffer.zig @@ -16,7 +16,7 @@ height: usize, buffer: [*]termbox.tb_cell, fg: u16, bg: u16, -border_fg: u8, +border_fg: u16, box_chars: struct { left_up: u32, left_down: u32, From 2c428f55371878f1bc10f571bbef9be4a313c61c Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Mon, 5 Aug 2024 11:08:51 +0200 Subject: [PATCH 099/530] Reduce dependence on tb_cell and tb_cell_buffer() Signed-off-by: AnErrupTion --- src/animations/Doom.zig | 14 +++++++++++--- src/bigclock.zig | 8 ++++---- src/main.zig | 2 +- src/tui/TerminalBuffer.zig | 37 +++++++++++++++++++------------------ src/tui/utils.zig | 12 +++++++++--- 5 files changed, 44 insertions(+), 29 deletions(-) diff --git a/src/animations/Doom.zig b/src/animations/Doom.zig index daa203a..9813122 100644 --- a/src/animations/Doom.zig +++ b/src/animations/Doom.zig @@ -9,7 +9,7 @@ const termbox = interop.termbox; const Doom = @This(); pub const STEPS = 13; -pub const FIRE = [_]termbox.tb_cell{ +pub const FIRE = [_]utils.Cell{ utils.initCell(' ', 9, 0), utils.initCell(0x2591, 2, 0), // Red utils.initCell(0x2592, 2, 0), // Red @@ -68,8 +68,8 @@ pub fn draw(self: Doom) void { if (buffer_dest > 12) buffer_dest = 0; self.buffer[dest] = @intCast(buffer_dest); - self.terminal_buffer.buffer[dest] = FIRE[buffer_dest]; - self.terminal_buffer.buffer[source] = FIRE[buffer_source]; + self.terminal_buffer.buffer[dest] = toTermboxCell(FIRE[buffer_dest]); + self.terminal_buffer.buffer[source] = toTermboxCell(FIRE[buffer_source]); } } } @@ -82,3 +82,11 @@ fn initBuffer(buffer: []u8, width: usize) void { @memset(slice_start, 0); @memset(slice_end, STEPS - 1); } + +fn toTermboxCell(cell: utils.Cell) termbox.tb_cell { + return .{ + .ch = cell.ch, + .fg = cell.fg, + .bg = cell.bg, + }; +} diff --git a/src/bigclock.zig b/src/bigclock.zig index bcae4ed..6084569 100644 --- a/src/bigclock.zig +++ b/src/bigclock.zig @@ -99,8 +99,8 @@ const E = [_]u21{ }; // zig fmt: on -pub fn clockCell(animate: bool, char: u8, fg: u16, bg: u16) [SIZE]termbox.tb_cell { - var cells: [SIZE]termbox.tb_cell = undefined; +pub fn clockCell(animate: bool, char: u8, fg: u16, bg: u16) [SIZE]utils.Cell { + var cells: [SIZE]utils.Cell = undefined; var tv: interop.system_time.timeval = undefined; _ = interop.system_time.gettimeofday(&tv, null); @@ -111,13 +111,13 @@ pub fn clockCell(animate: bool, char: u8, fg: u16, bg: u16) [SIZE]termbox.tb_cel return cells; } -pub fn alphaBlit(buffer: [*]termbox.tb_cell, x: usize, y: usize, tb_width: usize, tb_height: usize, cells: [SIZE]termbox.tb_cell) void { +pub fn alphaBlit(x: usize, y: usize, tb_width: usize, tb_height: usize, cells: [SIZE]utils.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]; - if (cell.ch != 0) buffer[(y + yy) * tb_width + (x + xx)] = cell; + if (cell.ch != 0) utils.putCell(x + xx, y + yy, cell); } } } diff --git a/src/main.zig b/src/main.zig index 1c412f8..2ef4965 100644 --- a/src/main.zig +++ b/src/main.zig @@ -400,7 +400,7 @@ pub fn main() !void { for (clock_str, 0..) |c, i| { const clock_cell = bigclock.clockCell(animate, c, buffer.fg, buffer.bg); - bigclock.alphaBlit(buffer.buffer, xo + i * (bigclock.WIDTH + 1), yo, buffer.width, buffer.height, clock_cell); + bigclock.alphaBlit(xo + i * (bigclock.WIDTH + 1), yo, buffer.width, buffer.height, clock_cell); } } diff --git a/src/tui/TerminalBuffer.zig b/src/tui/TerminalBuffer.zig index 472eadc..494ab81 100644 --- a/src/tui/TerminalBuffer.zig +++ b/src/tui/TerminalBuffer.zig @@ -74,27 +74,30 @@ pub fn init(config: Config, labels_max_length: usize, random: Random) TerminalBu } pub fn cascade(self: TerminalBuffer) bool { - var changes = false; - + var changed = false; var y = self.height - 2; + while (y > 0) : (y -= 1) { for (0..self.width) |x| { - const c: u8 = @truncate(self.buffer[(y - 1) * self.width + x].ch); - if (std.ascii.isWhitespace(c)) continue; + const cell = self.buffer[(y - 1) * self.width + x]; + const cell_under = self.buffer[y * self.width + x]; - const c_under: u8 = @truncate(self.buffer[y * self.width + x].ch); - if (!std.ascii.isWhitespace(c_under)) continue; + const char: u8 = @truncate(cell.ch); + if (std.ascii.isWhitespace(char)) continue; - changes = true; + 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; - self.buffer[y * self.width + x] = self.buffer[(y - 1) * self.width + x]; - self.buffer[(y - 1) * self.width + x].ch = ' '; + _ = 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 changes; + return changed; } pub fn drawBoxCenter(self: *TerminalBuffer, show_borders: bool, blank_box: bool) void { @@ -117,16 +120,16 @@ pub fn drawBoxCenter(self: *TerminalBuffer, show_borders: bool, blank_box: bool) var c2 = utils.initCell(self.box_chars.bottom, self.border_fg, self.bg); for (0..self.box_width) |i| { - _ = utils.putCell(@intCast(x1 + i), @intCast(y1 - 1), &c1); - _ = utils.putCell(@intCast(x1 + i), @intCast(y2), &c2); + utils.putCell(x1 + i, y1 - 1, c1); + utils.putCell(x1 + i, y2, c2); } c1.ch = self.box_chars.left; c2.ch = self.box_chars.right; for (0..self.box_height) |i| { - _ = utils.putCell(@intCast(x1 - 1), @intCast(y1 + i), &c1); - _ = utils.putCell(@intCast(x2), @intCast(y1 + i), &c2); + utils.putCell(x1 - 1, y1 + i, c1); + utils.putCell(x2, y1 + i, c2); } } @@ -135,7 +138,7 @@ pub fn drawBoxCenter(self: *TerminalBuffer, show_borders: bool, blank_box: bool) for (0..self.box_height) |y| { for (0..self.box_width) |x| { - _ = utils.putCell(@intCast(x1 + x), @intCast(y1 + y), &blank); + utils.putCell(x1 + x, y1 + y, blank); } } } @@ -191,8 +194,6 @@ pub fn drawConfinedLabel(self: TerminalBuffer, text: []const u8, x: usize, y: us } pub fn drawCharMultiple(self: TerminalBuffer, char: u8, x: usize, y: usize, length: usize) void { - const yc: c_int = @intCast(y); const cell = utils.initCell(char, self.fg, self.bg); - - for (0..length) |xx| _ = utils.putCell(@intCast(x + xx), yc, &cell); + for (0..length) |xx| utils.putCell(x + xx, y, cell); } diff --git a/src/tui/utils.zig b/src/tui/utils.zig index 5b7e067..43c3619 100644 --- a/src/tui/utils.zig +++ b/src/tui/utils.zig @@ -3,7 +3,13 @@ const interop = @import("../interop.zig"); const termbox = interop.termbox; -pub fn initCell(ch: u32, fg: u16, bg: u16) termbox.tb_cell { +pub const Cell = struct { + ch: u32, + fg: u16, + bg: u16, +}; + +pub fn initCell(ch: u32, fg: u16, bg: u16) Cell { return .{ .ch = ch, .fg = fg, @@ -11,8 +17,8 @@ pub fn initCell(ch: u32, fg: u16, bg: u16) termbox.tb_cell { }; } -pub fn putCell(x: i32, y: i32, cell: *const termbox.tb_cell) c_int { - return termbox.tb_set_cell(x, y, cell.ch, cell.fg, cell.bg); +pub fn putCell(x: usize, y: usize, cell: Cell) void { + _ = termbox.tb_set_cell(@intCast(x), @intCast(y), cell.ch, cell.fg, cell.bg); } // Every codepoint is assumed to have a width of 1. From 5e85618730c44f32760912b888d1ee5a0bd0ab33 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Mon, 5 Aug 2024 15:16:58 +0200 Subject: [PATCH 100/530] Conditionally import login_cap.h with pwd.h Signed-off-by: AnErrupTion --- src/auth.zig | 12 ++++++------ src/interop.zig | 14 +++----------- 2 files changed, 9 insertions(+), 17 deletions(-) diff --git a/src/auth.zig b/src/auth.zig index f2bcb97..281777d 100644 --- a/src/auth.zig +++ b/src/auth.zig @@ -66,12 +66,12 @@ pub fn authenticate(config: Config, current_environment: Session.Environment, lo if (status != interop.pam.PAM_SUCCESS) return pamDiagnose(status); defer status = interop.pam.pam_close_session(handle, 0); - var pwd: *interop.passwd = undefined; + var pwd: *interop.pwd.passwd = undefined; { - defer interop.endpwent(); + defer interop.pwd.endpwent(); // Get password structure from username - pwd = interop.getpwnam(login) orelse return error.GetPasswordNameFailed; + pwd = interop.pwd.getpwnam(login) orelse return error.GetPasswordNameFailed; } // Set user shell if it hasn't already been set @@ -122,7 +122,7 @@ pub fn authenticate(config: Config, current_environment: Session.Environment, lo fn startSession( config: Config, - pwd: *interop.passwd, + pwd: *interop.pwd.passwd, handle: ?*interop.pam.pam_handle, current_environment: Session.Environment, ) !void { @@ -132,7 +132,7 @@ fn startSession( if (status != 0) return error.GroupInitializationFailed; // FreeBSD sets the GID and UID with setusercontext() - const result = interop.logincap.setusercontext(null, pwd, pwd.pw_uid, interop.logincap.LOGIN_SETALL); + const result = interop.pwd.setusercontext(null, pwd, pwd.pw_uid, interop.pwd.LOGIN_SETALL); if (result != 0) return error.SetUserUidFailed; } else { const status = interop.grp.initgroups(pwd.pw_name, pwd.pw_gid); @@ -166,7 +166,7 @@ fn startSession( } } -fn initEnv(pwd: *interop.passwd, path_env: ?[:0]const u8) !void { +fn initEnv(pwd: *interop.pwd.passwd, path_env: ?[:0]const u8) !void { _ = interop.stdlib.setenv("HOME", pwd.pw_dir, 1); _ = interop.stdlib.setenv("PWD", pwd.pw_dir, 1); _ = interop.stdlib.setenv("SHELL", pwd.pw_shell, 1); diff --git a/src/interop.zig b/src/interop.zig index aba5d8c..9fc5017 100644 --- a/src/interop.zig +++ b/src/interop.zig @@ -35,18 +35,15 @@ pub const stdlib = @cImport({ pub 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("login_cap.h"); }); pub const grp = @cImport({ @cInclude("grp.h"); }); -// FreeBSD-specific headers (except for pwd.h) -pub const logincap = @cImport({ - @cInclude("pwd.h"); - @cInclude("login_cap.h"); -}); - // BSD-specific headers pub const kbio = @cImport({ @cInclude("sys/kbio.h"); @@ -61,11 +58,6 @@ pub const vt = @cImport({ @cInclude("sys/vt.h"); }); -// On FreeBSD, login_cap.h references the passwd struct directly, so we must use logincap.passwd instead -pub const passwd = if (builtin.os.tag == .freebsd) logincap.passwd else pwd.passwd; -pub const endpwent = if (builtin.os.tag == .freebsd) logincap.endpwent else pwd.endpwent; -pub const getpwnam = if (builtin.os.tag == .freebsd) logincap.getpwnam else pwd.getpwnam; - // Used for getting & setting the lock state const LedState = if (builtin.os.tag.isBSD()) c_int else c_char; const get_led_state = if (builtin.os.tag.isBSD()) kbio.KDGETLED else kd.KDGKBLED; From 4e40e32f5977093bb14c6ceb1cc1105b6731b12a Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Mon, 5 Aug 2024 18:33:22 +0200 Subject: [PATCH 101/530] Slightly refactor resolution check Signed-off-by: AnErrupTion --- src/main.zig | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/src/main.zig b/src/main.zig index 2ef4965..0089ec8 100644 --- a/src/main.zig +++ b/src/main.zig @@ -347,18 +347,11 @@ pub fn main() !void { const width: usize = @intCast(termbox.tb_width()); const height: usize = @intCast(termbox.tb_height()); - if (width != buffer.width) { + 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 + buffer.width = width; - resolution_changed = true; - } - - if (height != buffer.height) { buffer.height = height; - resolution_changed = true; - } - - // If it did change, then update the cell buffer, reallocate the current animation's buffers, and force a draw update - if (resolution_changed) { buffer.buffer = termbox.tb_cell_buffer(); switch (config.animation) { @@ -372,6 +365,7 @@ pub fn main() !void { } update = true; + resolution_changed = true; } } From 2901b408dcf6fbad0a7116b162a30556d96c8857 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Tue, 6 Aug 2024 10:07:23 +0200 Subject: [PATCH 102/530] Arrange config alphabetically Signed-off-by: AnErrupTion --- res/config.ini | 172 ++++++++++++++++++++++-------------------- src/config/Config.zig | 18 ++--- 2 files changed, 99 insertions(+), 91 deletions(-) diff --git a/res/config.ini b/res/config.ini index 8650bf0..a83ee41 100644 --- a/res/config.ini +++ b/res/config.ini @@ -1,6 +1,3 @@ -# The number of failed authentications before a special animation is played... ;) -auth_fails = 10 - # The active animation # none -> Nothing (default) # doom -> PSX DOOM fire @@ -12,28 +9,13 @@ animation = none # 1..2e12 -> Stop the animation after this many seconds animation_timeout_sec = 0 -# Format string for clock in top right corner (see strftime specification). Example: %c -# If null, the clock won't be shown -clock = null - -# Enable/disable big clock -bigclock = false - # The character used to mask the password # If null, the password will be hidden # Note: you can use a # by escaping it like so: \# asterisk = * -# Erase password input on failure -clear_password = false - -# Enable vi keybindings -vi_mode = false - -# Default vi mode -# normal -> normal mode -# insert -> insert mode -vi_default_mode = normal +# The number of failed authentications before a special animation is played... ;) +auth_fails = 10 # The `fg` and `bg` color settings take a digit 0-8 corresponding to: #define TB_DEFAULT 0x00 @@ -57,88 +39,84 @@ vi_default_mode = normal # Background color id bg = 0 -# Foreground color id -fg = 8 +# Enable/disable big clock +bigclock = false -# Background color errors -error_bg = 0 +# Blank main box background +# Setting to false will make it transparent +blank_box = true -# Foreground color errors -# Default is red and bold: TB_RED | TB_BOLD -error_fg = 258 - -# CMatrix animation foreground color id -cmatrix_fg = 3 - -# Border color +# Border foreground color id border_fg = 8 # Title to show at the top of the main box # If set to null, none will be shown box_title = null -# Initial text to show on the info line -# If set to null, the info line defaults to the hostname -initial_info_text = null +# Brightness +/- percentage in one step +brightness_change = 10 -# Blank main box background -# Setting to false will make it transparent -blank_box = true +# Brightness decrease key +brightness_down_key = F5 -# Remove main box borders -hide_borders = false +# Brightness increase key +brightness_up_key = F6 -# Main box margins -margin_box_h = 2 -margin_box_v = 1 +# Brightness control command +brightnessctl = $PREFIX_DIRECTORY/bin/brightnessctl -# Input boxes length -input_len = 34 +# Erase password input on failure +clear_password = false + +# Format string for clock in top right corner (see strftime specification). Example: %c +# If null, the clock won't be shown +clock = null + +# CMatrix animation foreground color id +cmatrix_fg = 3 + +# Console path +console_dev = /dev/console # Input box active by default on startup # Available inputs: info_line, session, login, password default_input = login -# Load the saved desktop and username -load = true +# Error background color id +error_bg = 0 -# Save the current desktop and login as defaults -save = true +# Error foreground color id +# Default is red and bold: TB_RED | TB_BOLD +error_fg = 258 + +# Foreground color id +fg = 8 + +# Remove main box borders +hide_borders = false # Remove power management command hints hide_key_hints = false -# Specifies the key used for shutdown (F1-F12) -shutdown_key = F1 +# Initial text to show on the info line +# If set to null, the info line defaults to the hostname +initial_info_text = null -# Specifies the key used for restart (F1-F12) -restart_key = F2 - -# Specifies the key used for sleep (F1-F12) -sleep_key = F3 - -# Command executed when pressing shutdown_key -shutdown_cmd = /sbin/shutdown -a now - -# Command executed when pressing restart_key -restart_cmd = /sbin/shutdown -r now - -# Command executed when pressing sleep key (can be null) -sleep_cmd = null +# Input boxes length +input_len = 34 # Active language # Available languages are found in $CONFIG_DIRECTORY/ly/lang/ lang = en -# TTY in use -tty = $DEFAULT_TTY +# Load the saved desktop and username +load = true -# Console path -console_dev = /dev/console +# Main box horizontal margin +margin_box_h = 2 -# Default path -# If null, ly doesn't set a path -path = /sbin:/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin +# Main box vertical margin +margin_box_v = 1 # Event timeout in milliseconds min_refresh_delta = 5 @@ -146,30 +124,60 @@ min_refresh_delta = 5 # Set numlock on/off at startup numlock = false +# Default path +# If null, ly doesn't set a path +path = /sbin:/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin + +# Command executed when pressing restart_key +restart_cmd = /sbin/shutdown -r now + +# Specifies the key used for restart (F1-F12) +restart_key = F2 + +# Save the current desktop and login as defaults +save = true + # Service name (set to ly to use the provided pam config file) service_name = ly # Setup command setup_cmd = $CONFIG_DIRECTORY/ly/setup.sh +# Command executed when pressing shutdown_key +shutdown_cmd = /sbin/shutdown -a now + +# Specifies the key used for shutdown (F1-F12) +shutdown_key = F1 + +# Command executed when pressing sleep key (can be null) +sleep_cmd = null + +# Specifies the key used for sleep (F1-F12) +sleep_key = F3 + +# TTY in use +tty = $DEFAULT_TTY + +# Default vi mode +# normal -> normal mode +# insert -> insert mode +vi_default_mode = normal + +# Enable vi keybindings +vi_mode = false + # Wayland desktop environments waylandsessions = $PREFIX_DIRECTORY/share/wayland-sessions -# xinitrc -# If null, the xinitrc session will be hidden -xinitrc = ~/.xinitrc - # Xorg server command x_cmd = $PREFIX_DIRECTORY/bin/X # Xorg xauthority edition tool xauth_cmd = $PREFIX_DIRECTORY/bin/xauth +# xinitrc +# If null, the xinitrc session will be hidden +xinitrc = ~/.xinitrc + # Xorg desktop environments xsessions = $PREFIX_DIRECTORY/share/xsessions - -# Brightness control -brightness_down_key = F5 -brightness_up_key = F6 -brightnessctl = $PREFIX_DIRECTORY/bin/brightnessctl -brightness_change = 10 diff --git a/src/config/Config.zig b/src/config/Config.zig index cd87bbb..f77ea00 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -5,22 +5,27 @@ const Animation = enums.Animation; const Input = enums.Input; const ViMode = enums.ViMode; -auth_fails: u64 = 10, animation: Animation = .none, +animation_timeout_sec: u12 = 0, asterisk: ?u8 = '*', +auth_fails: u64 = 10, bg: u16 = 0, bigclock: bool = false, blank_box: bool = true, border_fg: u16 = 8, box_title: ?[]const u8 = null, +brightness_change: []const u8 = "10", +brightness_down_key: []const u8 = "F5", +brightness_up_key: []const u8 = "F6", +brightnessctl: [:0]const u8 = build_options.prefix_directory ++ "/bin/brightnessctl", clear_password: bool = false, clock: ?[:0]const u8 = null, +cmatrix_fg: u16 = 3, console_dev: []const u8 = "/dev/console", default_input: Input = .login, error_bg: u16 = 0, error_fg: u16 = 258, fg: u16 = 8, -cmatrix_fg: u16 = 3, hide_borders: bool = false, hide_key_hints: bool = false, initial_info_text: ?[]const u8 = null, @@ -42,15 +47,10 @@ shutdown_key: []const u8 = "F1", sleep_cmd: ?[]const u8 = null, sleep_key: []const u8 = "F3", tty: u8 = build_options.tty, -vi_mode: bool = false, 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", -xinitrc: ?[]const u8 = "~/.xinitrc", xauth_cmd: []const u8 = build_options.prefix_directory ++ "/bin/xauth", +xinitrc: ?[]const u8 = "~/.xinitrc", xsessions: []const u8 = build_options.prefix_directory ++ "/share/xsessions", -brightness_down_key: []const u8 = "F5", -brightness_up_key: []const u8 = "F6", -brightnessctl: [:0]const u8 = build_options.prefix_directory ++ "/bin/brightnessctl", -brightness_change: []const u8 = "10", -animation_timeout_sec: u12 = 0, From b5b3317dd84e7922ec75733b10faa2a6acb8bb8e Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Tue, 6 Aug 2024 11:06:20 +0200 Subject: [PATCH 103/530] Add login & logout script support Signed-off-by: AnErrupTion --- res/config.ini | 16 ++++++++++++++-- src/auth.zig | 14 +++++++------- src/config/Config.zig | 2 ++ src/main.zig | 5 +++++ 4 files changed, 28 insertions(+), 9 deletions(-) diff --git a/res/config.ini b/res/config.ini index a83ee41..951b003 100644 --- a/res/config.ini +++ b/res/config.ini @@ -1,11 +1,11 @@ # The active animation -# none -> Nothing (default) +# none -> Nothing # doom -> PSX DOOM fire # matrix -> CMatrix animation = none # Stop the animation after some time -# 0 -> Run forever (default) +# 0 -> Run forever # 1..2e12 -> Stop the animation after this many seconds animation_timeout_sec = 0 @@ -112,6 +112,18 @@ lang = en # Load the saved desktop and username load = true +# Command executed when logging in +# If null, no command will be executed +# Important: the code itself must end with `exec "$@"` in order to launch the session! +# You can also set environment variables in there, they'll persist until logout +login_cmd = null + +# Command executed when logging out +# If null, no command will be executed +# Important: the session will already be terminated when this command is executed, so +# no need to add `exec "$@"` at the end +logout_cmd = null + # Main box horizontal margin margin_box_h = 2 diff --git a/src/auth.zig b/src/auth.zig index 281777d..255144a 100644 --- a/src/auth.zig +++ b/src/auth.zig @@ -156,8 +156,8 @@ fn startSession( std.posix.chdirZ(pwd.pw_dir.?) catch return error.ChangeDirectoryFailed; switch (current_environment.display_server) { - .wayland => try executeWaylandCmd(pwd.pw_shell.?, config.setup_cmd, current_environment.cmd), - .shell => try executeShellCmd(pwd.pw_shell.?, config.setup_cmd), + .wayland => try executeWaylandCmd(pwd.pw_shell.?, config.setup_cmd, config.login_cmd orelse "", current_environment.cmd), + .shell => try executeShellCmd(pwd.pw_shell.?, config.setup_cmd, config.login_cmd orelse ""), .xinitrc, .x11 => if (build_options.enable_x11_support) { var vt_buf: [5]u8 = undefined; const vt = try std.fmt.bufPrint(&vt_buf, "vt{d}", .{config.tty}); @@ -372,16 +372,16 @@ fn xauth(display_name: [:0]u8, shell: [*:0]const u8, pw_dir: [*:0]const u8, xaut if (status.status != 0) return error.XauthFailed; } -fn executeShellCmd(shell: [*:0]const u8, setup_cmd: []const u8) !void { +fn executeShellCmd(shell: [*:0]const u8, setup_cmd: []const u8, login_cmd: []const u8) !void { var cmd_buffer: [1024]u8 = undefined; - const cmd_str = try std.fmt.bufPrintZ(&cmd_buffer, "{s} {s}", .{ setup_cmd, shell }); + const cmd_str = try std.fmt.bufPrintZ(&cmd_buffer, "{s} {s} {s}", .{ setup_cmd, login_cmd, shell }); const args = [_:null]?[*:0]const u8{ shell, "-c", cmd_str }; return std.posix.execveZ(shell, &args, std.c.environ); } -fn executeWaylandCmd(shell: [*:0]const u8, setup_cmd: []const u8, desktop_cmd: []const u8) !void { +fn executeWaylandCmd(shell: [*:0]const u8, setup_cmd: []const u8, login_cmd: []const u8, desktop_cmd: []const u8) !void { var cmd_buffer: [1024]u8 = undefined; - const cmd_str = try std.fmt.bufPrintZ(&cmd_buffer, "{s} {s}", .{ setup_cmd, desktop_cmd }); + const cmd_str = try std.fmt.bufPrintZ(&cmd_buffer, "{s} {s} {s}", .{ setup_cmd, login_cmd, desktop_cmd }); const args = [_:null]?[*:0]const u8{ shell, "-c", cmd_str }; return std.posix.execveZ(shell, &args, std.c.environ); } @@ -418,7 +418,7 @@ fn executeX11Cmd(shell: [*:0]const u8, pw_dir: [*:0]const u8, config: Config, de 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}", .{ config.setup_cmd, desktop_cmd }) catch std.process.exit(1); + const cmd_str = std.fmt.bufPrintZ(&cmd_buffer, "{s} {s} {s}", .{ config.setup_cmd, config.login_cmd orelse "", desktop_cmd }) catch std.process.exit(1); const args = [_:null]?[*:0]const u8{ shell, "-c", cmd_str }; std.posix.execveZ(shell, &args, std.c.environ) catch {}; std.process.exit(1); diff --git a/src/config/Config.zig b/src/config/Config.zig index f77ea00..89a56cd 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -32,6 +32,8 @@ initial_info_text: ?[]const u8 = null, input_len: u8 = 34, lang: []const u8 = "en", load: bool = true, +login_cmd: ?[]const u8 = null, +logout_cmd: ?[]const u8 = null, margin_box_h: u8 = 2, margin_box_v: u8 = 1, min_refresh_delta: u16 = 5, diff --git a/src/main.zig b/src/main.zig index 0089ec8..039c184 100644 --- a/src/main.zig +++ b/src/main.zig @@ -698,6 +698,11 @@ pub fn main() !void { try info_line.addMessage(getAuthErrorMsg(err, lang), config.error_bg, config.error_fg); 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(); try info_line.addMessage(lang.logout, config.bg, config.fg); } From 8562cf4e29b81bd40f9f360ecb2458d280586793 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Tue, 6 Aug 2024 11:11:38 +0200 Subject: [PATCH 104/530] Update feature request template Signed-off-by: AnErrupTion --- .github/ISSUE_TEMPLATE/bug.yml | 2 +- .github/ISSUE_TEMPLATE/feature.yml | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug.yml b/.github/ISSUE_TEMPLATE/bug.yml index 4e9eb61..bccca4d 100644 --- a/.github/ISSUE_TEMPLATE/bug.yml +++ b/.github/ISSUE_TEMPLATE/bug.yml @@ -1,4 +1,4 @@ -name: Bug Report +name: Bug report description: File a bug report. title: "[Bug] " labels: ["bug"] diff --git a/.github/ISSUE_TEMPLATE/feature.yml b/.github/ISSUE_TEMPLATE/feature.yml index 9cb61ea..b9a26af 100644 --- a/.github/ISSUE_TEMPLATE/feature.yml +++ b/.github/ISSUE_TEMPLATE/feature.yml @@ -1,4 +1,4 @@ -name: Feature Request +name: Feature request description: Request a new feature or enhancement. title: "[Feature] " labels: ["feature"] @@ -11,8 +11,10 @@ body: options: - label: I have looked for any other duplicate issues required: true + - label: I have confirmed the requested feature doesn't exist in the latest version in development + required: true - type: textarea - id: observed + id: wanted attributes: label: Wanted behavior description: What do you want to be added? Describe the behavior clearly. From 1ca53f661edcf7126a90cd05556c8689bd87c187 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Tue, 6 Aug 2024 14:44:48 +0200 Subject: [PATCH 105/530] Fix drawn position of sleep key hint Signed-off-by: AnErrupTion --- src/main.zig | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/src/main.zig b/src/main.zig index 039c184..1b1c0e3 100644 --- a/src/main.zig +++ b/src/main.zig @@ -321,6 +321,7 @@ pub fn main() !void { const restart_key = try std.fmt.parseInt(u8, config.restart_key[1..], 10); const restart_len = try utils.strWidth(lang.restart); const sleep_key = try std.fmt.parseInt(u8, config.sleep_key[1..], 10); + const sleep_len = try utils.strWidth(lang.sleep); const brightness_down_key = try std.fmt.parseInt(u8, config.brightness_down_key[1..], 10); const brightness_down_len = try utils.strWidth(lang.brightness_down); const brightness_up_key = try std.fmt.parseInt(u8, config.brightness_up_key[1..], 10); @@ -457,6 +458,15 @@ pub fn main() !void { buffer.drawLabel(lang.restart, length, 0); length += restart_len + 1; + if (config.sleep_cmd != null) { + buffer.drawLabel(config.sleep_key, length, 0); + length += config.sleep_key.len + 1; + buffer.drawLabel(" ", length - 1, 0); + + buffer.drawLabel(lang.sleep, length, 0); + length += sleep_len + 1; + } + buffer.drawLabel(config.brightness_down_key, length, 0); length += config.brightness_down_key.len + 1; buffer.drawLabel(" ", length - 1, 0); @@ -470,14 +480,6 @@ pub fn main() !void { buffer.drawLabel(lang.brightness_up, length, 0); length += brightness_up_len + 1; - - if (config.sleep_cmd != null) { - buffer.drawLabel(config.sleep_key, length, 0); - length += config.sleep_key.len + 1; - buffer.drawLabel(" ", length - 1, 0); - - buffer.drawLabel(lang.sleep, length, 0); - } } if (config.box_title) |title| { From f0869f0e135fc21e83cd7a82492135b782b74809 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Tue, 6 Aug 2024 15:32:31 +0200 Subject: [PATCH 106/530] Add numlock set error + handle 2 more errors Signed-off-by: AnErrupTion --- res/lang/en.ini | 1 + res/lang/fr.ini | 1 + src/config/Lang.zig | 1 + src/config/migrator.zig | 4 ++-- src/main.zig | 10 +++++++--- 5 files changed, 12 insertions(+), 5 deletions(-) diff --git a/res/lang/en.ini b/res/lang/en.ini index 511200e..b2f52ae 100644 --- a/res/lang/en.ini +++ b/res/lang/en.ini @@ -13,6 +13,7 @@ err_envlist = failed to get envlist err_hostname = failed to get hostname err_mlock = failed to lock password memory err_null = null pointer +err_numlock = failed to set numlock err_pam = pam transaction failed err_pam_abort = pam transaction aborted err_pam_acct_expired = account expired diff --git a/res/lang/fr.ini b/res/lang/fr.ini index 32e5598..5685c95 100644 --- a/res/lang/fr.ini +++ b/res/lang/fr.ini @@ -13,6 +13,7 @@ err_envlist = échec de lecture de la liste d'environnement err_hostname = échec de lecture du nom d'hôte err_mlock = échec du verrouillage mémoire err_null = pointeur null +err_numlock = échec de modification du verr.num err_pam = échec de la transaction pam err_pam_abort = transaction pam avortée err_pam_acct_expired = compte expiré diff --git a/src/config/Lang.zig b/src/config/Lang.zig index 3a81108..fafa537 100644 --- a/src/config/Lang.zig +++ b/src/config/Lang.zig @@ -14,6 +14,7 @@ err_envlist: []const u8 = "failed to get envlist", err_hostname: []const u8 = "failed to get hostname", err_mlock: []const u8 = "failed to lock password memory", err_null: []const u8 = "null pointer", +err_numlock: []const u8 = "failed to set numlock", err_pam: []const u8 = "pam transaction failed", err_pam_abort: []const u8 = "pam transaction aborted", err_pam_acct_expired: []const u8 = "account expired", diff --git a/src/config/migrator.zig b/src/config/migrator.zig index df028c4..f003213 100644 --- a/src/config/migrator.zig +++ b/src/config/migrator.zig @@ -108,13 +108,13 @@ pub fn tryMigrateSaveFile(user_buf: *[32]u8) Save { const reader = file.reader(); var user_fbs = std.io.fixedBufferStream(user_buf); - reader.streamUntilDelimiter(user_fbs.writer(), '\n', 32) catch return save; + reader.streamUntilDelimiter(user_fbs.writer(), '\n', user_buf.len) catch return save; const user = user_fbs.getWritten(); if (user.len > 0) save.user = user; var session_buf: [20]u8 = undefined; var session_fbs = std.io.fixedBufferStream(&session_buf); - reader.streamUntilDelimiter(session_fbs.writer(), '\n', 20) catch {}; + reader.streamUntilDelimiter(session_fbs.writer(), '\n', session_buf.len) catch return save; const session_index_str = session_fbs.getWritten(); var session_index: ?usize = null; diff --git a/src/main.zig b/src/main.zig index 1b1c0e3..d0420ab 100644 --- a/src/main.zig +++ b/src/main.zig @@ -182,8 +182,6 @@ pub fn main() !void { shutdown_cmd = try temporary_allocator.dupe(u8, config.shutdown_cmd); restart_cmd = try temporary_allocator.dupe(u8, config.restart_cmd); - interop.setNumlock(config.numlock) catch {}; - // Initialize termbox _ = termbox.tb_init(); defer _ = termbox.tb_shutdown(); @@ -221,6 +219,10 @@ pub fn main() !void { try info_line.addMessage("unable to parse config file", config.error_bg, config.error_fg); } + interop.setNumlock(config.numlock) catch { + try info_line.addMessage(lang.err_numlock, config.error_bg, config.error_fg); + }; + var session = Session.init(allocator, &buffer, lang); defer session.deinit(); @@ -645,7 +647,9 @@ pub fn main() !void { }, termbox.TB_KEY_ENTER => { try info_line.addMessage(lang.authenticating, config.bg, config.fg); - InfoLine.clearRendered(allocator, buffer) catch {}; + InfoLine.clearRendered(allocator, buffer) catch { + try info_line.addMessage(lang.err_alloc, config.error_bg, config.error_fg); + }; info_line.label.draw(); _ = termbox.tb_present(); From 096b1a7d44fa05cb463dfe8ce7df99e08c52c378 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Tue, 6 Aug 2024 18:40:24 +0200 Subject: [PATCH 107/530] Refactor brightness handling code Signed-off-by: AnErrupTion --- res/config.ini | 10 +++++----- src/config/Config.zig | 4 ++-- src/main.zig | 18 ++++-------------- 3 files changed, 11 insertions(+), 21 deletions(-) diff --git a/res/config.ini b/res/config.ini index 951b003..220319f 100644 --- a/res/config.ini +++ b/res/config.ini @@ -53,18 +53,18 @@ border_fg = 8 # If set to null, none will be shown box_title = null -# Brightness +/- percentage in one step -brightness_change = 10 +# Brightness increase command +brightness_down_cmd = $PREFIX_DIRECTORY/bin/brightnessctl -q s 10%- # Brightness decrease key brightness_down_key = F5 +# Brightness increase command +brightness_up_cmd = $PREFIX_DIRECTORY/bin/brightnessctl -q s +10% + # Brightness increase key brightness_up_key = F6 -# Brightness control command -brightnessctl = $PREFIX_DIRECTORY/bin/brightnessctl - # Erase password input on failure clear_password = false diff --git a/src/config/Config.zig b/src/config/Config.zig index 89a56cd..1c4555a 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -14,10 +14,10 @@ bigclock: bool = false, blank_box: bool = true, border_fg: u16 = 8, box_title: ?[]const u8 = null, -brightness_change: []const u8 = "10", +brightness_down_cmd: [:0]const u8 = build_options.prefix_directory ++ "/bin/brightnessctl -q s 10%-", brightness_down_key: []const u8 = "F5", +brightness_up_cmd: [:0]const u8 = build_options.prefix_directory ++ "/bin/brightnessctl -q s +10%", brightness_up_key: []const u8 = "F6", -brightnessctl: [:0]const u8 = build_options.prefix_directory ++ "/bin/brightnessctl", clear_password: bool = false, clock: ?[:0]const u8 = null, cmatrix_fg: u16 = 3, diff --git a/src/main.zig b/src/main.zig index d0420ab..1f8c224 100644 --- a/src/main.zig +++ b/src/main.zig @@ -582,21 +582,11 @@ pub fn main() !void { var sleep = std.process.Child.init(&[_][]const u8{ "/bin/sh", "-c", sleep_cmd }, allocator); _ = sleep.spawnAndWait() catch .{}; } - } else if (pressed_key == brightness_down_key and unistd.access(config.brightnessctl, unistd.X_OK) == 0) brightness_change: { - const brightness_str = std.fmt.allocPrint(allocator, "{s}%-", .{config.brightness_change}) catch { - try info_line.addMessage(lang.err_brightness_change, config.error_bg, config.error_fg); - break :brightness_change; - }; - defer allocator.free(brightness_str); - var brightness = std.process.Child.init(&[_][]const u8{ config.brightnessctl, "-q", "s", brightness_str }, allocator); + } else if (pressed_key == brightness_down_key) { + var brightness = std.process.Child.init(&[_][]const u8{ "/bin/sh", "-c", config.brightness_down_cmd }, allocator); _ = brightness.spawnAndWait() catch .{}; - } else if (pressed_key == brightness_up_key and unistd.access(config.brightnessctl, unistd.X_OK) == 0) brightness_change: { - const brightness_str = std.fmt.allocPrint(allocator, "+{s}%", .{config.brightness_change}) catch { - try info_line.addMessage(lang.err_brightness_change, config.error_bg, config.error_fg); - break :brightness_change; - }; - defer allocator.free(brightness_str); - var brightness = std.process.Child.init(&[_][]const u8{ config.brightnessctl, "-q", "s", brightness_str }, allocator); + } else if (pressed_key == brightness_up_key) { + var brightness = std.process.Child.init(&[_][]const u8{ "/bin/sh", "-c", config.brightness_up_cmd }, allocator); _ = brightness.spawnAndWait() catch .{}; } }, From c033f5bd03bd04c8f4b169cad11f6f029f0d4a5c Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Tue, 6 Aug 2024 19:38:02 +0200 Subject: [PATCH 108/530] Use hexadecimal numbers for colors in config Signed-off-by: AnErrupTion --- res/config.ini | 62 ++++++++++++++++++++++++++++++-------------------- 1 file changed, 37 insertions(+), 25 deletions(-) diff --git a/res/config.ini b/res/config.ini index 220319f..d7b302f 100644 --- a/res/config.ini +++ b/res/config.ini @@ -1,3 +1,34 @@ +# The color settings in Ly take a digit 0-8 corresponding to: +#define TB_DEFAULT 0x00 +#define TB_BLACK 0x01 +#define TB_RED 0x02 +#define TB_GREEN 0x03 +#define TB_YELLOW 0x04 +#define TB_BLUE 0x05 +#define TB_MAGENTA 0x06 +#define TB_CYAN 0x07 +#define TB_WHITE 0x08 +# The default color varies, but usually it makes the background black and the foreground white. +# You can also combine these colors with the following style attributes using bitwise OR: +#define TB_BOLD 0x0100 +#define TB_UNDERLINE 0x0200 +#define TB_REVERSE 0x0400 +#define TB_ITALIC 0x0800 +#define TB_BLINK 0x1000 +#define TB_HI_BLACK 0x2000 +#define TB_BRIGHT 0x4000 +#define TB_DIM 0x8000 +# For example, to set the foreground color to red and bold, you would do 0x02 | 0x0100 = 0x0102. +# Note that you must pre-calculate the value because Ly doesn't parse bitwise OR operations in its config. +# +# Moreover, to set the VT color palette, you are encouraged to use another tool such as +# mkinitcpio-colors (https://github.com/evanpurkhiser/mkinitcpio-colors). Note that the color palette defined with +# mkinitcpio-colors takes 16 colors (0-15), only values 0-8 are valid with Ly and these values do not correspond +# exactly. For instance, in defining palettes with mkinitcpio-colors, the order is black, dark red, dark green, brown, dark +# blue, dark purple, dark cyan, light gray, dark gray, bright red, bright green, yellow, bright blue, bright purple, bright +# cyan, and white, indexed in that order 0 through 15. For example, the color defined for white (indexed at 15 in the mkinitcpio +# config) will be used by Ly for fg = 0x0008. + # The active animation # none -> Nothing # doom -> PSX DOOM fire @@ -17,27 +48,8 @@ asterisk = * # The number of failed authentications before a special animation is played... ;) auth_fails = 10 -# The `fg` and `bg` color settings take a digit 0-8 corresponding to: -#define TB_DEFAULT 0x00 -#define TB_BLACK 0x01 -#define TB_RED 0x02 -#define TB_GREEN 0x03 -#define TB_YELLOW 0x04 -#define TB_BLUE 0x05 -#define TB_MAGENTA 0x06 -#define TB_CYAN 0x07 -#define TB_WHITE 0x08 -# -# Setting both to zero makes `bg` black and `fg` white. To set the actual color palette you are encouraged to use another tool -# such as [mkinitcpio-colors](https://github.com/evanpurkhiser/mkinitcpio-colors). Note that the color palette defined with -# `mkinitcpio-colors` takes 16 colors (0-15), only values 0-8 are valid for `ly` config and these values do not correspond -# exactly. For instance, in defining palettes with `mkinitcpio-colors` the order is black, dark red, dark green, brown, dark -# blue, dark purple, dark cyan, light gray, dark gray, bright red, bright green, yellow, bright blue, bright purple, bright -# cyan, and white, indexed in that order 0 through 15. For example, the color defined for white (indexed at 15 in the mkinitcpio -# config) will be used by `ly` for `fg = 8`. - # Background color id -bg = 0 +bg = 0x0000 # Enable/disable big clock bigclock = false @@ -47,7 +59,7 @@ bigclock = false blank_box = true # Border foreground color id -border_fg = 8 +border_fg = 0x0008 # Title to show at the top of the main box # If set to null, none will be shown @@ -73,7 +85,7 @@ clear_password = false clock = null # CMatrix animation foreground color id -cmatrix_fg = 3 +cmatrix_fg = 0x0003 # Console path console_dev = /dev/console @@ -83,14 +95,14 @@ console_dev = /dev/console default_input = login # Error background color id -error_bg = 0 +error_bg = 0x0000 # Error foreground color id # Default is red and bold: TB_RED | TB_BOLD -error_fg = 258 +error_fg = 0x0102 # Foreground color id -fg = 8 +fg = 0x0008 # Remove main box borders hide_borders = false From 767bdaf166fdd74a1e0a34b239ab972c78aa903f Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Wed, 7 Aug 2024 12:03:34 +0200 Subject: [PATCH 109/530] Add session logging support Signed-off-by: AnErrupTion --- .github/ISSUE_TEMPLATE/bug.yml | 5 ++++- res/config.ini | 6 ++++++ src/auth.zig | 29 ++++++++++++++++++++++------- src/config/Config.zig | 1 + 4 files changed, 33 insertions(+), 8 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug.yml b/.github/ISSUE_TEMPLATE/bug.yml index bccca4d..831248e 100644 --- a/.github/ISSUE_TEMPLATE/bug.yml +++ b/.github/ISSUE_TEMPLATE/bug.yml @@ -50,5 +50,8 @@ body: id: logs attributes: label: Relevant logs - description: Please copy and paste any relevant logs, error messages or any other output. This will be automatically formatted into code, so no need for backticks. Screenshots are accepted if they make life easier for you. + description: | + Please copy and paste any relevant logs, error messages or any other output. This will be automatically formatted into code, so no need for backticks. + Screenshots are accepted if they make life easier for you. + Generally, including your session log (found at /var/log/ly-session.log unless modified) is a good idea. (But make sure it's relevant!) render: shell diff --git a/res/config.ini b/res/config.ini index d7b302f..83fcc91 100644 --- a/res/config.ini +++ b/res/config.ini @@ -164,6 +164,12 @@ save = true # Service name (set to ly to use the provided pam config file) service_name = ly +# Session log file path +# This will contain stdout and stderr of X11 and Wayland sessions +# By default it's saved in the user's home directory +# Note: this file won't be used in a shell session (due to the need of stdout and stderr) +session_log = ly-session.log + # Setup command setup_cmd = $CONFIG_DIRECTORY/ly/setup.sh diff --git a/src/auth.zig b/src/auth.zig index 255144a..29f9843 100644 --- a/src/auth.zig +++ b/src/auth.zig @@ -152,12 +152,13 @@ fn startSession( const env_list = std.mem.span(pam_env_vars.?); for (env_list) |env_var| _ = interop.stdlib.putenv(env_var); - // Execute what the user requested + // Change to the user's home directory std.posix.chdirZ(pwd.pw_dir.?) catch return error.ChangeDirectoryFailed; + // Execute what the user requested switch (current_environment.display_server) { - .wayland => try executeWaylandCmd(pwd.pw_shell.?, config.setup_cmd, config.login_cmd orelse "", current_environment.cmd), - .shell => try executeShellCmd(pwd.pw_shell.?, config.setup_cmd, config.login_cmd orelse ""), + .wayland => try executeWaylandCmd(pwd.pw_shell.?, config, current_environment.cmd), + .shell => try executeShellCmd(pwd.pw_shell.?, config), .xinitrc, .x11 => if (build_options.enable_x11_support) { var vt_buf: [5]u8 = undefined; const vt = try std.fmt.bufPrint(&vt_buf, "vt{d}", .{config.tty}); @@ -372,21 +373,35 @@ fn xauth(display_name: [:0]u8, shell: [*:0]const u8, pw_dir: [*:0]const u8, xaut if (status.status != 0) return error.XauthFailed; } -fn executeShellCmd(shell: [*:0]const u8, setup_cmd: []const u8, login_cmd: []const u8) !void { +fn executeShellCmd(shell: [*:0]const u8, config: Config) !void { + // We don't want to redirect stdout and stderr in a shell session + var cmd_buffer: [1024]u8 = undefined; - const cmd_str = try std.fmt.bufPrintZ(&cmd_buffer, "{s} {s} {s}", .{ setup_cmd, login_cmd, shell }); + const cmd_str = try std.fmt.bufPrintZ(&cmd_buffer, "{s} {s} {s}", .{ config.setup_cmd, config.login_cmd orelse "", shell }); const args = [_:null]?[*:0]const u8{ shell, "-c", cmd_str }; return std.posix.execveZ(shell, &args, std.c.environ); } -fn executeWaylandCmd(shell: [*:0]const u8, setup_cmd: []const u8, login_cmd: []const u8, desktop_cmd: []const u8) !void { +fn executeWaylandCmd(shell: [*:0]const u8, config: Config, desktop_cmd: []const u8) !void { + const log_file = try std.fs.cwd().createFile(config.session_log, .{ .mode = 0o666 }); + defer log_file.close(); + + try std.posix.dup2(std.posix.STDOUT_FILENO, std.posix.STDERR_FILENO); + try std.posix.dup2(log_file.handle, std.posix.STDOUT_FILENO); + var cmd_buffer: [1024]u8 = undefined; - const cmd_str = try std.fmt.bufPrintZ(&cmd_buffer, "{s} {s} {s}", .{ setup_cmd, login_cmd, desktop_cmd }); + const cmd_str = try std.fmt.bufPrintZ(&cmd_buffer, "{s} {s} {s}", .{ config.setup_cmd, config.login_cmd orelse "", desktop_cmd }); const args = [_:null]?[*:0]const u8{ shell, "-c", cmd_str }; return std.posix.execveZ(shell, &args, std.c.environ); } fn executeX11Cmd(shell: [*:0]const u8, pw_dir: [*:0]const u8, config: Config, desktop_cmd: []const u8, vt: []const u8) !void { + const log_file = try std.fs.cwd().createFile(config.session_log, .{ .mode = 0o666 }); + defer log_file.close(); + + try std.posix.dup2(std.posix.STDOUT_FILENO, std.posix.STDERR_FILENO); + try std.posix.dup2(log_file.handle, std.posix.STDOUT_FILENO); + const display_num = try getFreeDisplay(); var buf: [5]u8 = undefined; const display_name = try std.fmt.bufPrintZ(&buf, ":{d}", .{display_num}); diff --git a/src/config/Config.zig b/src/config/Config.zig index 1c4555a..f9c88db 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -43,6 +43,7 @@ restart_cmd: []const u8 = "/sbin/shutdown -r now", 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", shutdown_cmd: []const u8 = "/sbin/shutdown -a now", shutdown_key: []const u8 = "F1", From 2bd0d0d4f3864d57106e4d65e992382dc035e1f4 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Wed, 7 Aug 2024 13:41:28 +0200 Subject: [PATCH 110/530] Don't mention log file yet in issue template Signed-off-by: AnErrupTion --- .github/ISSUE_TEMPLATE/bug.yml | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug.yml b/.github/ISSUE_TEMPLATE/bug.yml index 831248e..bccca4d 100644 --- a/.github/ISSUE_TEMPLATE/bug.yml +++ b/.github/ISSUE_TEMPLATE/bug.yml @@ -50,8 +50,5 @@ body: id: logs attributes: label: Relevant logs - description: | - Please copy and paste any relevant logs, error messages or any other output. This will be automatically formatted into code, so no need for backticks. - Screenshots are accepted if they make life easier for you. - Generally, including your session log (found at /var/log/ly-session.log unless modified) is a good idea. (But make sure it's relevant!) + description: Please copy and paste any relevant logs, error messages or any other output. This will be automatically formatted into code, so no need for backticks. Screenshots are accepted if they make life easier for you. render: shell From 00c94f8ffdeb2213f80139f40c6930388f59f838 Mon Sep 17 00:00:00 2001 From: "Kian A." <91016770+KAYT33N@users.noreply.github.com> Date: Wed, 7 Aug 2024 18:17:27 +0330 Subject: [PATCH 111/530] Add Farsi Bigclock (#673) * Change config type for bigclock * Seprates big clock's hard codes from its logic * Adds Farsi (fa) to big clock langs * Minor changes * Makes requested changes --- res/config.ini | 7 +- src/bigclock.zig | 140 +++++++++------------------------------- src/bigclock/Lang.zig | 23 +++++++ src/bigclock/en.zig | 94 +++++++++++++++++++++++++++ src/bigclock/fa.zig | 94 +++++++++++++++++++++++++++ src/config/Config.zig | 3 +- src/config/migrator.zig | 16 +++++ src/enums.zig | 6 ++ src/main.zig | 6 +- 9 files changed, 272 insertions(+), 117 deletions(-) create mode 100644 src/bigclock/Lang.zig create mode 100644 src/bigclock/en.zig create mode 100644 src/bigclock/fa.zig diff --git a/res/config.ini b/res/config.ini index 83fcc91..596ef36 100644 --- a/res/config.ini +++ b/res/config.ini @@ -51,8 +51,11 @@ auth_fails = 10 # Background color id bg = 0x0000 -# Enable/disable big clock -bigclock = false +# Change the state and language of the big clock +# none -> Disabled (default) +# en -> English +# fa -> Farsi +bigclock = none # Blank main box background # Setting to false will make it transparent diff --git a/src/bigclock.zig b/src/bigclock.zig index 6084569..6f17b72 100644 --- a/src/bigclock.zig +++ b/src/bigclock.zig @@ -1,111 +1,24 @@ const std = @import("std"); -const builtin = @import("builtin"); const interop = @import("interop.zig"); const utils = @import("tui/utils.zig"); +const enums = @import("enums.zig"); +const Lang = @import("bigclock/Lang.zig"); +const en = @import("bigclock/en.zig"); +const fa = @import("bigclock/fa.zig"); -const termbox = interop.termbox; +const termbox = interop.termbox; +const Bigclock = enums.Bigclock; +pub const WIDTH = Lang.WIDTH; +pub const HEIGHT = Lang.HEIGHT; +pub const SIZE = Lang.SIZE; -const X: u32 = if (builtin.os.tag == .linux or builtin.os.tag.isBSD()) 0x2593 else '#'; -const O: u32 = 0; - -pub const WIDTH = 5; -pub const HEIGHT = 5; -pub const SIZE = WIDTH * HEIGHT; - -// zig fmt: off -const ZERO = [_]u21{ - X,X,X,X,X, - X,X,O,X,X, - X,X,O,X,X, - X,X,O,X,X, - X,X,X,X,X, -}; -const ONE = [_]u21{ - O,O,O,X,X, - O,O,O,X,X, - O,O,O,X,X, - O,O,O,X,X, - O,O,O,X,X, -}; -const TWO = [_]u21{ - X,X,X,X,X, - O,O,O,X,X, - X,X,X,X,X, - X,X,O,O,O, - X,X,X,X,X, -}; -const THREE = [_]u21{ - X,X,X,X,X, - O,O,O,X,X, - X,X,X,X,X, - O,O,O,X,X, - X,X,X,X,X, -}; -const FOUR = [_]u21{ - X,X,O,X,X, - X,X,O,X,X, - X,X,X,X,X, - O,O,O,X,X, - O,O,O,X,X, -}; -const FIVE = [_]u21{ - X,X,X,X,X, - X,X,O,O,O, - X,X,X,X,X, - O,O,O,X,X, - X,X,X,X,X, -}; -const SIX = [_]u21{ - X,X,X,X,X, - X,X,O,O,O, - X,X,X,X,X, - X,X,O,X,X, - X,X,X,X,X, -}; -const SEVEN = [_]u21{ - X,X,X,X,X, - O,O,O,X,X, - O,O,O,X,X, - O,O,O,X,X, - O,O,O,X,X, -}; -const EIGHT = [_]u21{ - X,X,X,X,X, - X,X,O,X,X, - X,X,X,X,X, - X,X,O,X,X, - X,X,X,X,X, -}; -const NINE = [_]u21{ - X,X,X,X,X, - X,X,O,X,X, - X,X,X,X,X, - O,O,O,X,X, - X,X,X,X,X, -}; -const S = [_]u21{ - O,O,O,O,O, - O,O,X,O,O, - O,O,O,O,O, - O,O,X,O,O, - O,O,O,O,O, -}; -const E = [_]u21{ - O,O,O,O,O, - O,O,O,O,O, - O,O,O,O,O, - O,O,O,O,O, - O,O,O,O,O, -}; -// zig fmt: on - -pub fn clockCell(animate: bool, char: u8, fg: u16, bg: u16) [SIZE]utils.Cell { +pub fn clockCell(animate: bool, char: u8, fg: u16, bg: u16, bigclock: Bigclock) [SIZE]utils.Cell { var cells: [SIZE]utils.Cell = undefined; var tv: interop.system_time.timeval = undefined; _ = interop.system_time.gettimeofday(&tv, null); - const clock_chars = toBigNumber(if (animate and char == ':' and @divTrunc(tv.tv_usec, 500000) != 0) ' ' else char); + const clock_chars = toBigNumber(if (animate and char == ':' and @divTrunc(tv.tv_usec, 500000) != 0) ' ' else char, bigclock); for (0..cells.len) |i| cells[i] = utils.initCell(clock_chars[i], fg, bg); return cells; @@ -122,19 +35,24 @@ pub fn alphaBlit(x: usize, y: usize, tb_width: usize, tb_height: usize, cells: [ } } -fn toBigNumber(char: u8) []const u21 { +fn toBigNumber(char: u8, bigclock: Bigclock) []const u21 { + const locale_chars = switch (bigclock) { + .fa => fa.locale_chars, + .en => en.locale_chars, + .none => unreachable, + }; return switch (char) { - '0' => &ZERO, - '1' => &ONE, - '2' => &TWO, - '3' => &THREE, - '4' => &FOUR, - '5' => &FIVE, - '6' => &SIX, - '7' => &SEVEN, - '8' => &EIGHT, - '9' => &NINE, - ':' => &S, - else => &E, + '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, + ':' => &locale_chars.S, + else => &locale_chars.E, }; } diff --git a/src/bigclock/Lang.zig b/src/bigclock/Lang.zig new file mode 100644 index 0000000..4229776 --- /dev/null +++ b/src/bigclock/Lang.zig @@ -0,0 +1,23 @@ +const builtin = @import("builtin"); + +pub const WIDTH = 5; +pub const HEIGHT = 5; +pub const SIZE = WIDTH * HEIGHT; + +pub const X: u32 = if (builtin.os.tag == .linux or builtin.os.tag.isBSD()) 0x2593 else '#'; +pub const O: u32 = 0; + +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, +}; \ No newline at end of file diff --git a/src/bigclock/en.zig b/src/bigclock/en.zig new file mode 100644 index 0000000..868656a --- /dev/null +++ b/src/bigclock/en.zig @@ -0,0 +1,94 @@ +const Lang = @import("Lang.zig"); + +const LocaleChars = Lang.LocaleChars; +const X = Lang.X; +const O = Lang.O; + +// zig fmt: off +pub const locale_chars = LocaleChars{ + .ZERO = [_]u21{ + X,X,X,X,X, + X,X,O,X,X, + X,X,O,X,X, + X,X,O,X,X, + X,X,X,X,X, + }, + .ONE = [_]u21{ + O,O,O,X,X, + O,O,O,X,X, + O,O,O,X,X, + O,O,O,X,X, + O,O,O,X,X, + }, + .TWO = [_]u21{ + X,X,X,X,X, + O,O,O,X,X, + X,X,X,X,X, + X,X,O,O,O, + X,X,X,X,X, + }, + .THREE = [_]u21{ + X,X,X,X,X, + O,O,O,X,X, + X,X,X,X,X, + O,O,O,X,X, + X,X,X,X,X, + }, + .FOUR = [_]u21{ + X,X,O,X,X, + X,X,O,X,X, + X,X,X,X,X, + O,O,O,X,X, + O,O,O,X,X, + }, + .FIVE = [_]u21{ + X,X,X,X,X, + X,X,O,O,O, + X,X,X,X,X, + O,O,O,X,X, + X,X,X,X,X, + }, + .SIX = [_]u21{ + X,X,X,X,X, + X,X,O,O,O, + X,X,X,X,X, + X,X,O,X,X, + X,X,X,X,X, + }, + .SEVEN = [_]u21{ + X,X,X,X,X, + O,O,O,X,X, + O,O,O,X,X, + O,O,O,X,X, + O,O,O,X,X, + }, + .EIGHT = [_]u21{ + X,X,X,X,X, + X,X,O,X,X, + X,X,X,X,X, + X,X,O,X,X, + X,X,X,X,X, + }, + .NINE = [_]u21{ + X,X,X,X,X, + X,X,O,X,X, + X,X,X,X,X, + O,O,O,X,X, + X,X,X,X,X, + }, + .S = [_]u21{ + O,O,O,O,O, + O,O,X,O,O, + O,O,O,O,O, + O,O,X,O,O, + O,O,O,O,O, + }, + .E = [_]u21{ + O,O,O,O,O, + O,O,O,O,O, + O,O,O,O,O, + O,O,O,O,O, + O,O,O,O,O, + }, +}; +// zig fmt: on \ No newline at end of file diff --git a/src/bigclock/fa.zig b/src/bigclock/fa.zig new file mode 100644 index 0000000..63a897a --- /dev/null +++ b/src/bigclock/fa.zig @@ -0,0 +1,94 @@ +const Lang = @import("Lang.zig"); + +const LocaleChars = Lang.LocaleChars; +const X = Lang.X; +const O = Lang.O; + +// zig fmt: off +pub const locale_chars = LocaleChars{ + .ZERO = [_]u21{ + O,O,O,O,O, + O,O,X,O,O, + O,X,O,X,O, + O,O,X,O,O, + O,O,O,O,O, + }, + .ONE = [_]u21{ + O,O,X,O,O, + O,X,X,O,O, + O,O,X,O,O, + O,O,X,O,O, + O,O,X,O,O, + }, + .TWO = [_]u21{ + O,X,O,X,O, + O,X,X,X,O, + O,X,O,O,O, + O,X,O,O,O, + O,X,O,O,O, + }, + .THREE = [_]u21{ + X,O,X,O,X, + X,X,X,X,X, + X,O,O,O,O, + X,O,O,O,O, + X,O,O,O,O, + }, + .FOUR = [_]u21{ + O,X,O,X,X, + O,X,X,O,O, + O,X,X,X,X, + O,X,O,O,O, + O,X,O,O,O, + }, + .FIVE = [_]u21{ + O,O,X,X,O, + O,X,O,O,X, + X,O,O,O,X, + X,O,X,O,X, + O,X,O,X,O, + }, + .SIX = [_]u21{ + O,X,X,O,O, + O,X,O,O,X, + O,O,X,O,O, + O,X,O,O,O, + X,O,O,O,O, + }, + .SEVEN = [_]u21{ + X,O,O,O,X, + X,O,O,O,X, + O,X,O,X,O, + O,X,O,X,O, + O,O,X,O,O, + }, + .EIGHT = [_]u21{ + O,O,O,X,O, + O,O,X,O,X, + O,O,X,O,X, + O,X,O,O,X, + O,X,O,O,X, + }, + .NINE = [_]u21{ + O,X,X,X,O, + O,X,O,X,O, + O,X,X,X,O, + O,O,O,X,O, + O,O,O,X,O, + }, + .S = [_]u21{ + O,O,O,O,O, + O,O,X,O,O, + O,O,O,O,O, + O,O,X,O,O, + O,O,O,O,O, + }, + .E = [_]u21{ + O,O,O,O,O, + O,O,O,O,O, + O,O,O,O,O, + O,O,O,O,O, + O,O,O,O,O, + }, +}; +// zig fmt: on \ No newline at end of file diff --git a/src/config/Config.zig b/src/config/Config.zig index f9c88db..2d3cd07 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -4,13 +4,14 @@ const enums = @import("../enums.zig"); const Animation = enums.Animation; const Input = enums.Input; const ViMode = enums.ViMode; +const Bigclock = enums.Bigclock; animation: Animation = .none, animation_timeout_sec: u12 = 0, asterisk: ?u8 = '*', auth_fails: u64 = 10, bg: u16 = 0, -bigclock: bool = false, +bigclock: Bigclock = .none, blank_box: bool = true, border_fg: u16 = 8, box_title: ?[]const u8 = null, diff --git a/src/config/migrator.zig b/src/config/migrator.zig index f003213..ad7c378 100644 --- a/src/config/migrator.zig +++ b/src/config/migrator.zig @@ -85,6 +85,22 @@ pub fn configFieldHandler(_: std.mem.Allocator, field: ini.IniField) ?ini.IniFie return null; } + if (std.mem.eql(u8, field.key, "bigclock")) { + // The option now uses a string (which then gets converted into an enum) instead of an boolean + // It also includes the ability to change active bigclock's language + var mapped_field = field; + + if (std.mem.eql(u8, field.value, "true")){ + mapped_field.value = "en"; + mapped_config_fields = true; + }else if (std.mem.eql(u8, field.value, "false")){ + mapped_field.value = "none"; + mapped_config_fields = true; + } + + return mapped_field; + } + return field; } diff --git a/src/enums.zig b/src/enums.zig index 84b011e..ad0cc47 100644 --- a/src/enums.zig +++ b/src/enums.zig @@ -22,3 +22,9 @@ pub const ViMode = enum { normal, insert, }; + +pub const Bigclock = enum { + none, + en, + fa, +}; \ No newline at end of file diff --git a/src/main.zig b/src/main.zig index 1f8c224..b9cf38a 100644 --- a/src/main.zig +++ b/src/main.zig @@ -385,7 +385,7 @@ pub fn main() !void { } } - if (config.bigclock and buffer.box_height + (bigclock.HEIGHT + 2) * 2 < buffer.height) draw_big_clock: { + if (config.bigclock != .none and buffer.box_height + (bigclock.HEIGHT + 2) * 2 < buffer.height) draw_big_clock: { const format = "%H:%M"; 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; @@ -396,7 +396,7 @@ pub fn main() !void { }; for (clock_str, 0..) |c, i| { - const clock_cell = bigclock.clockCell(animate, c, buffer.fg, buffer.bg); + const clock_cell = bigclock.clockCell(animate, c, buffer.fg, buffer.bg, config.bigclock); bigclock.alphaBlit(xo + i * (bigclock.WIDTH + 1), yo, buffer.width, buffer.height, clock_cell); } } @@ -544,7 +544,7 @@ pub fn main() !void { .matrix => matrix.deinit(), } } - } else if (config.bigclock and config.clock == null) { + } else if (config.bigclock != .none and config.clock == null) { var tv: interop.system_time.timeval = undefined; _ = interop.system_time.gettimeofday(&tv, null); From 028cb9496a71ba535e561c07fc0dbe25e7328ad7 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Wed, 7 Aug 2024 17:06:14 +0200 Subject: [PATCH 112/530] Fix session logging for X11 (somewhat) Signed-off-by: AnErrupTion --- src/auth.zig | 34 ++++++++++++++++++++-------------- 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/src/auth.zig b/src/auth.zig index 29f9843..60f31c4 100644 --- a/src/auth.zig +++ b/src/auth.zig @@ -350,7 +350,7 @@ fn mcookie() [Md5.digest_length * 2]u8 { return std.fmt.bytesToHex(&out, .lower); } -fn xauth(display_name: [:0]u8, shell: [*:0]const u8, pw_dir: [*:0]const u8, xauth_cmd: []const u8) !void { +fn xauth(display_name: [:0]u8, shell: [*:0]const u8, pw_dir: [*:0]const u8, config: Config) !void { var pwd_buf: [100]u8 = undefined; const pwd = try std.fmt.bufPrintZ(&pwd_buf, "{s}", .{pw_dir}); @@ -362,8 +362,11 @@ fn xauth(display_name: [:0]u8, shell: [*:0]const u8, pw_dir: [*:0]const u8, xaut const pid = try std.posix.fork(); if (pid == 0) { + const log_file = try redirectStandardStreams(config.session_log, true); + defer log_file.close(); + var cmd_buffer: [1024]u8 = undefined; - const cmd_str = std.fmt.bufPrintZ(&cmd_buffer, "{s} add {s} . {s}", .{ xauth_cmd, display_name, magic_cookie }) catch std.process.exit(1); + const cmd_str = std.fmt.bufPrintZ(&cmd_buffer, "{s} add {s} . {s}", .{ config.xauth_cmd, display_name, magic_cookie }) catch std.process.exit(1); const args = [_:null]?[*:0]const u8{ shell, "-c", cmd_str }; std.posix.execveZ(shell, &args, std.c.environ) catch {}; std.process.exit(1); @@ -383,12 +386,9 @@ fn executeShellCmd(shell: [*:0]const u8, config: Config) !void { } fn executeWaylandCmd(shell: [*:0]const u8, config: Config, desktop_cmd: []const u8) !void { - const log_file = try std.fs.cwd().createFile(config.session_log, .{ .mode = 0o666 }); + const log_file = try redirectStandardStreams(config.session_log, true); defer log_file.close(); - try std.posix.dup2(std.posix.STDOUT_FILENO, std.posix.STDERR_FILENO); - try std.posix.dup2(log_file.handle, std.posix.STDOUT_FILENO); - var cmd_buffer: [1024]u8 = undefined; const cmd_str = try std.fmt.bufPrintZ(&cmd_buffer, "{s} {s} {s}", .{ config.setup_cmd, config.login_cmd orelse "", desktop_cmd }); const args = [_:null]?[*:0]const u8{ shell, "-c", cmd_str }; @@ -396,21 +396,15 @@ fn executeWaylandCmd(shell: [*:0]const u8, config: Config, desktop_cmd: []const } fn executeX11Cmd(shell: [*:0]const u8, pw_dir: [*:0]const u8, config: Config, desktop_cmd: []const u8, vt: []const u8) !void { - const log_file = try std.fs.cwd().createFile(config.session_log, .{ .mode = 0o666 }); - defer log_file.close(); - - try std.posix.dup2(std.posix.STDOUT_FILENO, std.posix.STDERR_FILENO); - try std.posix.dup2(log_file.handle, std.posix.STDOUT_FILENO); - const display_num = try getFreeDisplay(); var buf: [5]u8 = undefined; const display_name = try std.fmt.bufPrintZ(&buf, ":{d}", .{display_num}); - try xauth(display_name, shell, pw_dir, config.xauth_cmd); + try xauth(display_name, shell, pw_dir, config); 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}", .{ config.x_cmd, display_name, vt }) catch std.process.exit(1); + const cmd_str = std.fmt.bufPrintZ(&cmd_buffer, "{s} {s} {s} -quiet -logfile {s}", .{ config.x_cmd, display_name, vt, config.session_log }) catch std.process.exit(1); const args = [_:null]?[*:0]const u8{ shell, "-c", cmd_str }; std.posix.execveZ(shell, &args, std.c.environ) catch {}; std.process.exit(1); @@ -432,6 +426,9 @@ fn executeX11Cmd(shell: [*:0]const u8, pw_dir: [*:0]const u8, config: Config, de xorg_pid = try std.posix.fork(); if (xorg_pid == 0) { + const log_file = try redirectStandardStreams(config.session_log, false); + defer log_file.close(); + var cmd_buffer: [1024]u8 = undefined; const cmd_str = std.fmt.bufPrintZ(&cmd_buffer, "{s} {s} {s}", .{ config.setup_cmd, config.login_cmd orelse "", desktop_cmd }) catch std.process.exit(1); const args = [_:null]?[*:0]const u8{ shell, "-c", cmd_str }; @@ -457,6 +454,15 @@ fn executeX11Cmd(shell: [*:0]const u8, pw_dir: [*:0]const u8, config: Config, de _ = std.c.waitpid(x_pid, &status, 0); } +fn redirectStandardStreams(session_log: []const u8, create: bool) !std.fs.File { + const log_file = if (create) (try std.fs.cwd().createFile(session_log, .{ .mode = 0o666 })) else (try std.fs.cwd().openFile(session_log, .{ .mode = .read_write })); + + try std.posix.dup2(std.posix.STDOUT_FILENO, std.posix.STDERR_FILENO); + try std.posix.dup2(log_file.handle, std.posix.STDOUT_FILENO); + + return log_file; +} + fn addUtmpEntry(entry: *Utmp, username: [*:0]const u8, pid: c_int) !void { entry.ut_type = utmp.USER_PROCESS; entry.ut_pid = pid; From c87d5b4e7a0fc788edc6dbded9cf076114a4be49 Mon Sep 17 00:00:00 2001 From: DoctorKnowsBetter <111871367+DoctorKnowsBetter@users.noreply.github.com> Date: Wed, 7 Aug 2024 16:50:55 +0100 Subject: [PATCH 113/530] Fix OpenRC service (#682) Co-authored-by: Your Name --- res/ly-openrc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/res/ly-openrc b/res/ly-openrc index 83ac7b1..ac6ae0f 100644 --- a/res/ly-openrc +++ b/res/ly-openrc @@ -29,7 +29,7 @@ TERM=linux BAUD=38400 # If we don't have getty then we should have agetty command=${commandB:-$commandUL} -command_args_foreground="-nl $PREFIX_DIRECTORY/bin/$EXE_NAME $TTY $BAUD $TERM" +command_args_foreground="-nl $PREFIX_DIRECTORY/bin/$EXECUTABLE_NAME $TTY $BAUD $TERM" depend() { after agetty From b84158e1c0a3ca42d74eb64c73f18c989e1a5d38 Mon Sep 17 00:00:00 2001 From: llc0930 <14966910+llc0930@users.noreply.github.com> Date: Fri, 9 Aug 2024 16:44:49 +0000 Subject: [PATCH 114/530] Add option to center env name (#683) --- res/config.ini | 3 +++ src/config/Config.zig | 1 + src/main.zig | 8 ++++---- src/tui/components/InfoLine.zig | 2 +- src/tui/components/Session.zig | 5 ++++- src/tui/components/generic.zig | 7 ++++++- 6 files changed, 19 insertions(+), 7 deletions(-) diff --git a/res/config.ini b/res/config.ini index 596ef36..1d28cb8 100644 --- a/res/config.ini +++ b/res/config.ini @@ -188,6 +188,9 @@ sleep_cmd = null # Specifies the key used for sleep (F1-F12) sleep_key = F3 +# Center the session name. +text_in_center = false + # TTY in use tty = $DEFAULT_TTY diff --git a/src/config/Config.zig b/src/config/Config.zig index 2d3cd07..38c8b4c 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -50,6 +50,7 @@ shutdown_cmd: []const u8 = "/sbin/shutdown -a now", shutdown_key: []const u8 = "F1", sleep_cmd: ?[]const u8 = null, sleep_key: []const u8 = "F3", +text_in_center: bool = false, tty: u8 = build_options.tty, vi_default_mode: ViMode = .normal, vi_mode: bool = false, diff --git a/src/main.zig b/src/main.zig index b9cf38a..00dde46 100644 --- a/src/main.zig +++ b/src/main.zig @@ -283,8 +283,8 @@ pub fn main() !void { buffer.drawBoxCenter(!config.hide_borders, config.blank_box); const coordinates = buffer.calculateComponentCoordinates(); - info_line.label.position(coordinates.start_x, coordinates.y, coordinates.full_visible_length); - session.label.position(coordinates.x, coordinates.y + 2, coordinates.visible_length); + 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.position(coordinates.x, coordinates.y + 4, coordinates.visible_length); password.position(coordinates.x, coordinates.y + 6, coordinates.visible_length); @@ -405,8 +405,8 @@ pub fn main() !void { if (resolution_changed) { const coordinates = buffer.calculateComponentCoordinates(); - info_line.label.position(coordinates.start_x, coordinates.y, coordinates.full_visible_length); - session.label.position(coordinates.x, coordinates.y + 2, coordinates.visible_length); + 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.position(coordinates.x, coordinates.y + 4, coordinates.visible_length); password.position(coordinates.x, coordinates.y + 6, coordinates.visible_length); diff --git a/src/tui/components/InfoLine.zig b/src/tui/components/InfoLine.zig index d4ef7a4..a43b083 100644 --- a/src/tui/components/InfoLine.zig +++ b/src/tui/components/InfoLine.zig @@ -54,7 +54,7 @@ fn drawItem(label: *MessageLabel, message: Message, _: usize, _: usize) bool { if (message.width == 0 or label.buffer.box_width <= message.width) return false; const x = label.buffer.box_x + ((label.buffer.box_width - message.width) / 2); - label.first_char_x = x; + label.first_char_x = x + message.width; TerminalBuffer.drawColorLabel(message.text, x, label.y, message.fg, message.bg); return true; diff --git a/src/tui/components/Session.zig b/src/tui/components/Session.zig index fbc2620..2cc081a 100644 --- a/src/tui/components/Session.zig +++ b/src/tui/components/Session.zig @@ -133,7 +133,10 @@ fn drawItem(label: *EnvironmentLabel, environment: Environment, x: usize, y: usi const length = @min(environment.name.len, label.visible_length - 3); if (length == 0) return false; + const nx = if (label.text_in_center) (label.x + (label.visible_length - environment.name.len) / 2) else (label.x + 2); + label.first_char_x = nx + environment.name.len; + label.buffer.drawLabel(environment.specifier, x, y); - label.buffer.drawLabel(environment.name, label.x + 2, label.y); + label.buffer.drawLabel(environment.name, nx, label.y); return true; } diff --git a/src/tui/components/generic.zig b/src/tui/components/generic.zig index 215a876..126916b 100644 --- a/src/tui/components/generic.zig +++ b/src/tui/components/generic.zig @@ -21,6 +21,7 @@ pub fn CyclableLabel(comptime ItemType: type) type { x: usize, y: usize, first_char_x: usize, + text_in_center: bool, draw_item_fn: DrawItemFn, pub fn init(allocator: Allocator, buffer: *TerminalBuffer, draw_item_fn: DrawItemFn) Self { @@ -33,6 +34,7 @@ pub fn CyclableLabel(comptime ItemType: type) type { .x = 0, .y = 0, .first_char_x = 0, + .text_in_center = false, .draw_item_fn = draw_item_fn, }; } @@ -41,11 +43,14 @@ pub fn CyclableLabel(comptime ItemType: type) type { self.list.deinit(); } - pub fn position(self: *Self, x: usize, y: usize, visible_length: usize) void { + 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 addItem(self: *Self, item: ItemType) !void { From b80c276dad1c16128e5749fe47c0996d93d583f1 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Sun, 11 Aug 2024 18:27:58 +0200 Subject: [PATCH 115/530] Redirect X11 output to file via shell Signed-off-by: AnErrupTion --- src/auth.zig | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/auth.zig b/src/auth.zig index 60f31c4..077fd35 100644 --- a/src/auth.zig +++ b/src/auth.zig @@ -404,7 +404,7 @@ fn executeX11Cmd(shell: [*:0]const u8, pw_dir: [*:0]const u8, config: Config, de 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} -quiet -logfile {s}", .{ config.x_cmd, display_name, vt, config.session_log }) catch std.process.exit(1); + const cmd_str = std.fmt.bufPrintZ(&cmd_buffer, "{s} {s} {s} >{s} 2>&1", .{ config.x_cmd, display_name, vt, config.session_log }) catch std.process.exit(1); const args = [_:null]?[*:0]const u8{ shell, "-c", cmd_str }; std.posix.execveZ(shell, &args, std.c.environ) catch {}; std.process.exit(1); @@ -426,11 +426,8 @@ fn executeX11Cmd(shell: [*:0]const u8, pw_dir: [*:0]const u8, config: Config, de xorg_pid = try std.posix.fork(); if (xorg_pid == 0) { - const log_file = try redirectStandardStreams(config.session_log, false); - defer log_file.close(); - var cmd_buffer: [1024]u8 = undefined; - const cmd_str = std.fmt.bufPrintZ(&cmd_buffer, "{s} {s} {s}", .{ config.setup_cmd, config.login_cmd orelse "", desktop_cmd }) catch std.process.exit(1); + const cmd_str = std.fmt.bufPrintZ(&cmd_buffer, "{s} {s} {s} >{s} 2>&1", .{ config.setup_cmd, config.login_cmd orelse "", desktop_cmd, config.session_log }) catch std.process.exit(1); const args = [_:null]?[*:0]const u8{ shell, "-c", cmd_str }; std.posix.execveZ(shell, &args, std.c.environ) catch {}; std.process.exit(1); From 87ceba4de8cfadcd00567efefce53e2d19c183b7 Mon Sep 17 00:00:00 2001 From: S41G0N <159702116+S41G0N@users.noreply.github.com> Date: Mon, 12 Aug 2024 00:18:53 +0200 Subject: [PATCH 116/530] Add installation instructions for Gentoo (#685) * Adding installation section for Gentoo Linux (ly was recently added into Gentoo's official GURU project) * replace '$ sudo' with '#' --- readme.md | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/readme.md b/readme.md index 8b80b92..33bb805 100644 --- a/readme.md +++ b/readme.md @@ -194,6 +194,29 @@ You can install ly from the [`[extra]` repos](https://archlinux.org/packages/ext $ sudo pacman -S ly ``` +## Gentoo Installation +You can install ly from the GURU repository: + +Note: If the package is masked, you may need to unmask it using ~amd64 keyword: +```bash +# echo 'x11-misc/ly ~amd64' >> /etc/portage/package.accept_keywords +``` + +1. Enable the GURU repository: +```bash +# eselect repository enable guru +``` + +2. Sync the GURU repository: +```bash +# emaint sync -r guru +``` + +3. Install ly from source: +```bash +# emerge --ask x11-misc/ly +``` + ## Configuration You can find all the configuration in `/etc/ly/config.ini`. The file is commented, and includes the default values. @@ -228,3 +251,4 @@ disable the main box borders with `hide_borders = true`. ## Additional Information The name "Ly" is a tribute to the fairy from the game Rayman. Ly was tested by oxodao, who is some seriously awesome dude. + From 022d146f76a1e87418bf15fed5909b2978b79f6a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Claudi=20Lleyda=20Molt=C3=B3?= <41454094+claudi@users.noreply.github.com> Date: Wed, 21 Aug 2024 21:19:33 +0200 Subject: [PATCH 117/530] Update Catalan translation (#689) --- res/lang/cat.ini | 59 ++++++++++++++++++++++++++++++------------------ 1 file changed, 37 insertions(+), 22 deletions(-) diff --git a/res/lang/cat.ini b/res/lang/cat.ini index de598fa..88ca735 100644 --- a/res/lang/cat.ini +++ b/res/lang/cat.ini @@ -1,45 +1,60 @@ +authenticating = autenticant... +brightness_down = abaixar brillantor +brightness_up = apujar brillantor capslock = Bloq Majús -err_alloc = falla d'assignació de memòria -err_bounds = índex fora de límit -err_chdir = error al obrir carpeta home -err_console_dev = error al accedir a la consola +err_alloc = assignació de memòria fallida +err_bounds = índex fora de límits +err_brightness_change = error en canviar la brillantor +err_chdir = error en obrir la carpeta home +err_console_dev = error en accedir a la consola err_dgn_oob = missatge de registre err_domain = domini invàlid -err_hostname = error al obtenir el nom del host -err_mlock = error al bloquejar la clau de memòria +err_envlist = error en obtenir l'envlist +err_hostname = error en obtenir el nom de l'amfitrió +err_mlock = error en bloquejar la memòria de clau err_null = punter nul +err_numlock = error en establir el Bloq num err_pam = error en la transacció pam err_pam_abort = transacció pam avortada err_pam_acct_expired = compte expirat err_pam_auth = error d'autenticació -err_pam_authinfo_unavail = error al obtenir informació de l'usuari +err_pam_authinfo_unavail = error en obtenir la informació de l'usuari err_pam_authok_reqd = token expirat -err_pam_buf = error de la memòria intermitja -err_pam_cred_err = error al establir les credencials +err_pam_buf = error en la memòria intermèdia +err_pam_cred_err = error en establir les credencials err_pam_cred_expired = credencials expirades err_pam_cred_insufficient = credencials insuficients -err_pam_cred_unavail = error al obtenir credencials -err_pam_maxtries = s'ha assolit al màxim nombre d'intents +err_pam_cred_unavail = error en obtenir credencials +err_pam_maxtries = s'ha assolit al nombre màxim d'intents err_pam_perm_denied = permís denegat err_pam_session = error de sessió err_pam_sys = error de sistema err_pam_user_unknown = usuari desconegut -err_path = error al establir la ruta -err_perm_dir = error al canviar de directori actual -err_perm_group = error al degradar els permisos de grup -err_perm_user = error al degradar els permisos de l'usuari -err_pwnam = error al obtenir la informació de l'usuari -err_user_gid = error al establir el GID de l'usuari -err_user_init = error al inicialitzar usuari -err_user_uid = error al establir el UID de l'usuari -err_xsessions_dir = error al cercar la carpeta de sessions -err_xsessions_open = error al obrir la carpeta de sessions +err_path = error en establir la ruta +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_unknown = ha ocorregut un error desconegut +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 +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 +insert = inserir login = iniciar sessió -logout = tancar sessió +logout = sessió tancada +no_x11_support = x11 support disabled at compile-time +no_x11_support = el suport per x11 ha estat desactivat en la compilació +normal = normal numlock = Bloq Num password = Clau restart = reiniciar shell = shell shutdown = aturar +sleep = suspendre wayland = wayland +x11 = x11 xinitrc = xinitrc From 215ca5edc7ba6c3947e9497c9ed7819b9169a499 Mon Sep 17 00:00:00 2001 From: ttsenturk <75989700+widkit@users.noreply.github.com> Date: Tue, 1 Oct 2024 14:29:58 -0400 Subject: [PATCH 118/530] Update readme.md (#698) --- readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/readme.md b/readme.md index 33bb805..246212c 100644 --- a/readme.md +++ b/readme.md @@ -25,7 +25,7 @@ Ly is a lightweight TUI (ncurses-like) display manager for Linux and BSD. It is recommended to add a rule for Ly as it currently does not ship one. ``` -# dnf install kernel-devel pam-devel libxcb-devel +# dnf install kernel-devel pam-devel libxcb-devel zig ``` ## Support From aea95b7724cd1b114b1055089535b5c59bbe9d4e Mon Sep 17 00:00:00 2001 From: Moritz <129004253+moritz-reinel@users.noreply.github.com> Date: Tue, 1 Oct 2024 20:32:30 +0200 Subject: [PATCH 119/530] Prevent output of brightness/sleep command showing on screen (fixing #695) (#699) * Prevent output of brightness cmds showing on screen (fixing #695) * Prevent output of sleep cmd showing on screen --- src/main.zig | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/src/main.zig b/src/main.zig index 00dde46..fa2d47a 100644 --- a/src/main.zig +++ b/src/main.zig @@ -580,14 +580,26 @@ pub fn main() !void { } 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.spawnAndWait() catch .{}; + sleep.stdout_behavior = .Ignore; + sleep.stderr_behavior = .Ignore; + + _ = sleep.spawnAndWait() catch {}; + } + } else if (pressed_key == brightness_down_key or pressed_key == brightness_up_key) { + const cmd = if (pressed_key == brightness_down_key) config.brightness_down_cmd else config.brightness_up_cmd; + + 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) { + try info_line.addMessage(lang.err_brightness_change, config.error_bg, config.error_fg); + } } - } else if (pressed_key == brightness_down_key) { - var brightness = std.process.Child.init(&[_][]const u8{ "/bin/sh", "-c", config.brightness_down_cmd }, allocator); - _ = brightness.spawnAndWait() catch .{}; - } else if (pressed_key == brightness_up_key) { - var brightness = std.process.Child.init(&[_][]const u8{ "/bin/sh", "-c", config.brightness_up_cmd }, allocator); - _ = brightness.spawnAndWait() catch .{}; } }, termbox.TB_KEY_CTRL_C => run = false, From e885a5e7767d0bc9bcd756b3bc828430f620153f Mon Sep 17 00:00:00 2001 From: Ch1llyB1lly <135429057+Ch1llyB1lly@users.noreply.github.com> Date: Wed, 9 Oct 2024 06:17:38 +0100 Subject: [PATCH 120/530] Add Hyprland in README (#701) Adds hyprland to the list of tested desktop environments --- readme.md | 1 + 1 file changed, 1 insertion(+) diff --git a/readme.md b/readme.md index 246212c..d0d137b 100644 --- a/readme.md +++ b/readme.md @@ -40,6 +40,7 @@ The following desktop environments were tested with success: - dwm - enlightenment - gnome + - hyprland - i3 - kde - labwc From e125d8f1aa1544a6a106047c0acd7a7d2e0ff16c Mon Sep 17 00:00:00 2001 From: Moritz <129004253+moritz-reinel@users.noreply.github.com> Date: Sat, 12 Oct 2024 20:13:25 +0200 Subject: [PATCH 121/530] Add error when sleep command fails (#703) --- res/lang/en.ini | 1 + src/config/Lang.zig | 1 + src/main.zig | 9 ++++++++- 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/res/lang/en.ini b/res/lang/en.ini index b2f52ae..d6a42fe 100644 --- a/res/lang/en.ini +++ b/res/lang/en.ini @@ -35,6 +35,7 @@ err_perm_dir = failed to change current directory err_perm_group = failed to downgrade group permissions err_perm_user = failed to downgrade user permissions err_pwnam = failed to get user info +err_sleep = failed to execute sleep command err_unknown = an unknown error occurred err_user_gid = failed to set user GID err_user_init = failed to initialize user diff --git a/src/config/Lang.zig b/src/config/Lang.zig index fafa537..abe3ed0 100644 --- a/src/config/Lang.zig +++ b/src/config/Lang.zig @@ -36,6 +36,7 @@ err_perm_dir: []const u8 = "failed to change current directory", err_perm_group: []const u8 = "failed to downgrade group permissions", err_perm_user: []const u8 = "failed to downgrade user permissions", err_pwnam: []const u8 = "failed to get user info", +err_sleep: []const u8 = "failed to execute sleep command", err_unknown: []const u8 = "an unknown error occurred", err_user_gid: []const u8 = "failed to set user GID", err_user_init: []const u8 = "failed to initialize user", diff --git a/src/main.zig b/src/main.zig index fa2d47a..4afefad 100644 --- a/src/main.zig +++ b/src/main.zig @@ -583,7 +583,14 @@ pub fn main() !void { sleep.stdout_behavior = .Ignore; sleep.stderr_behavior = .Ignore; - _ = sleep.spawnAndWait() catch {}; + 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); + } + } } } else if (pressed_key == brightness_down_key or pressed_key == brightness_up_key) { const cmd = if (pressed_key == brightness_down_key) config.brightness_down_cmd else config.brightness_up_cmd; From 06e283961dbee6122b9bfe6550b40019da6cac07 Mon Sep 17 00:00:00 2001 From: Moritz Reinel <129004253+moritz-reinel@users.noreply.github.com> Date: Sat, 12 Oct 2024 21:26:31 +0200 Subject: [PATCH 122/530] add helper script for keeping lang files in sync with Lang.zig --- res/lang/cat.ini | 4 ++- res/lang/cs.ini | 17 ++++++++++ res/lang/de.ini | 17 ++++++++++ res/lang/en.ini | 6 ++-- res/lang/es.ini | 9 ++++++ res/lang/fix_missing_entries.py | 40 +++++++++++++++++++++++ res/lang/fix_missing_entries.sh | 56 +++++++++++++++++++++++++++++++++ res/lang/fr.ini | 9 ++++-- res/lang/it.ini | 17 ++++++++++ res/lang/pl.ini | 17 ++++++++++ res/lang/pt.ini | 17 ++++++++++ res/lang/pt_BR.ini | 17 ++++++++++ res/lang/ro.ini | 17 ++++++++++ res/lang/ru.ini | 17 ++++++++++ res/lang/sr.ini | 21 +++++++++++-- res/lang/sv.ini | 17 ++++++++++ res/lang/tr.ini | 17 ++++++++++ res/lang/uk.ini | 17 ++++++++++ src/config/Lang.zig | 4 +-- 19 files changed, 326 insertions(+), 10 deletions(-) create mode 100644 res/lang/fix_missing_entries.py create mode 100644 res/lang/fix_missing_entries.sh diff --git a/res/lang/cat.ini b/res/lang/cat.ini index 88ca735..541898b 100644 --- a/res/lang/cat.ini +++ b/res/lang/cat.ini @@ -6,6 +6,7 @@ err_alloc = assignació de memòria fallida err_bounds = índex fora de límits err_brightness_change = error en canviar la brillantor err_chdir = error en obrir la carpeta home + err_console_dev = error en accedir a la consola err_dgn_oob = missatge de registre err_domain = domini invàlid @@ -35,6 +36,7 @@ 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_unknown = ha ocorregut un error desconegut err_user_gid = error en establir el GID de l'usuari err_user_init = error en inicialitzar usuari @@ -46,10 +48,10 @@ err_xsessions_open = error en obrir la carpeta de sessions insert = inserir login = iniciar sessió logout = sessió tancada -no_x11_support = x11 support disabled at compile-time no_x11_support = el suport per x11 ha estat desactivat en la compilació normal = normal numlock = Bloq Num + password = Clau restart = reiniciar shell = shell diff --git a/res/lang/cs.ini b/res/lang/cs.ini index b6a217f..5beac01 100644 --- a/res/lang/cs.ini +++ b/res/lang/cs.ini @@ -1,13 +1,20 @@ + + + capslock = capslock err_alloc = alokace paměti selhala err_bounds = index je mimo hranice pole + err_chdir = nelze otevřít domovský adresář + err_console_dev = chyba při přístupu do konzole err_dgn_oob = zpráva protokolu err_domain = neplatná doména + err_hostname = nelze získat název hostitele err_mlock = uzamčení paměti hesel selhalo err_null = nulový ukazatel + err_pam = pam transakce selhala err_pam_abort = pam transakce přerušena err_pam_acct_expired = platnost účtu vypršela @@ -29,17 +36,27 @@ 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_user_gid = nastavení GID uživatele selhalo err_user_init = inicializace uživatele selhala err_user_uid = nastavení UID uživateli selhalo + + err_xsessions_dir = nepodařilo se najít složku relací err_xsessions_open = nepodařilo se otevřít složku relací + login = uživatel logout = odhlášen + + numlock = numlock + password = heslo 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 e18b9ba..30a8f44 100644 --- a/res/lang/de.ini +++ b/res/lang/de.ini @@ -1,13 +1,20 @@ + + + capslock = Feststelltaste err_alloc = Speicherzuweisung fehlgeschlagen err_bounds = Listenindex ist außerhalb des Bereichs + err_chdir = Fehler beim oeffnen des home-ordners + err_console_dev = Zugriff auf die Konsole fehlgeschlagen err_dgn_oob = Protokoll Nachricht err_domain = Unzulaessige domain + err_hostname = Holen des Hostnames fehlgeschlagen err_mlock = Abschließen des Passwortspeichers fehlgeschlagen err_null = Null Zeiger + err_pam = pam Transaktion fehlgeschlagen err_pam_abort = pam Transaktion abgebrochen err_pam_acct_expired = Benutzerkonto abgelaufen @@ -29,17 +36,27 @@ err_perm_dir = Fehler beim wechseln des Ordners err_perm_group = Fehler beim heruntersetzen der Gruppen Berechtigungen err_perm_user = Fehler beim heruntersetzen der Nutzer Berechtigungen err_pwnam = Holen der Benutzerinformationen fehlgeschlagen + + err_user_gid = Fehler beim setzen der Gruppen Id des Nutzers err_user_init = Initialisierung des Nutzers fehlgeschlagen err_user_uid = Setzen der Benutzer Id fehlgeschlagen + + err_xsessions_dir = Fehler beim finden des Sitzungsordners err_xsessions_open = Fehler beim öffnen des Sitzungsordners + login = Anmelden logout = Abgemeldet + + numlock = Numtaste + password = Passwort restart = Neustarten shell = shell shutdown = Herunterfahren + wayland = wayland + xinitrc = xinitrc diff --git a/res/lang/en.ini b/res/lang/en.ini index d6a42fe..f08cca1 100644 --- a/res/lang/en.ini +++ b/res/lang/en.ini @@ -6,6 +6,7 @@ err_alloc = failed memory allocation err_bounds = out-of-bounds index err_brightness_change = failed to change brightness err_chdir = failed to open home folder + err_console_dev = failed to access console err_dgn_oob = log message err_domain = invalid domain @@ -47,14 +48,15 @@ err_xsessions_open = failed to open sessions folder insert = insert login = login logout = logged out -normal = normal no_x11_support = x11 support disabled at compile-time +normal = normal numlock = numlock + password = password restart = reboot shell = shell shutdown = shutdown sleep = sleep wayland = wayland -xinitrc = xinitrc x11 = x11 +xinitrc = xinitrc diff --git a/res/lang/es.ini b/res/lang/es.ini index 73a9acd..23ec27c 100644 --- a/res/lang/es.ini +++ b/res/lang/es.ini @@ -4,13 +4,17 @@ brightness_up = subir brillo capslock = Bloq Mayús err_alloc = asignación de memoria fallida err_bounds = índice fuera de límites + err_chdir = error al abrir la carpeta home + err_console_dev = error al acceder a la consola err_dgn_oob = mensaje de registro err_domain = dominio inválido + err_hostname = error al obtener el nombre de host err_mlock = error al bloquear la contraseña de memoria err_null = puntero nulo + err_pam = error en la transacción pam err_pam_abort = transacción pam abortada err_pam_acct_expired = cuenta expirada @@ -32,9 +36,13 @@ 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_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_xsessions_dir = error al buscar la carpeta de sesiones err_xsessions_open = error al abrir la carpeta de sesiones insert = insertar @@ -50,4 +58,5 @@ shell = shell shutdown = apagar sleep = suspender wayland = wayland + xinitrc = xinitrc diff --git a/res/lang/fix_missing_entries.py b/res/lang/fix_missing_entries.py new file mode 100644 index 0000000..1df81f9 --- /dev/null +++ b/res/lang/fix_missing_entries.py @@ -0,0 +1,40 @@ +#!/usr/bin/env python3 + +from pathlib import Path +from sys import stderr + + +def process_lang_file(path: str) -> None: + values = {} + with open(path, "r", encoding="UTF-8") as fh: + while line := fh.readline(): + vals = line.split("=") + if len(vals) != 2: + continue + + key = vals[0].strip() + values[key] = vals[1].strip() + + with open(path, "w", encoding="UTF-8") as fh: + for item in lang_strings: + v = values.get(item) + if v is not None: + fh.write(f"{item} = {v}\n") + else: + fh.write("\n") + + +zig_lang_file = Path(__file__).parent.joinpath("../../src/config/Lang.zig").resolve() +if not zig_lang_file.exists(): + print(f"ERROR: File '{zig_lang_file.as_posix()}' does not exist. Exiting.", file=stderr) + exit(1) + +lang_strings = [] +with open(zig_lang_file, "r", encoding="UTF-8") as fh: + while line := fh.readline(): + lang_strings.append(line.split(":")[0]) + +lang_files = [f for f in Path.iterdir(Path(__file__).parent) if f.name.endswith(".ini") and f.is_file()] + +for file in lang_files: + process_lang_file(file.as_posix()) diff --git a/res/lang/fix_missing_entries.sh b/res/lang/fix_missing_entries.sh new file mode 100644 index 0000000..76c6df3 --- /dev/null +++ b/res/lang/fix_missing_entries.sh @@ -0,0 +1,56 @@ +#!/bin/bash + +set -eu + +function process_lang_file() { + local input_file=$1 + local tmp_file=$(mktemp) + local -A lang_strings_in_file + + while read -r line; do + if [[ -z "$line" ]]; then + : + elif [[ "$line" =~ ^([^\ ]*)[\ ]?\=[\ ]?(.*) ]]; then + lang_strings_in_file["${BASH_REMATCH[1]}"]="${BASH_REMATCH[2]}" + else + echo "ERROR: Line '$line' in file '$input_file' does not contain an entry of the pattern ' = '. Exiting." >&2 + exit 1 + fi + done < "$input_file" + + { + for s in "${LANG_STRINGS[@]}"; do + if [[ -v "lang_strings_in_file[\"$s\"]" ]]; then + printf "%s = %s\n" "$s" "${lang_strings_in_file[$s]}" + else + printf "\n" + fi + done + } > $tmp_file + + mv "$tmp_file" "$input_file" +} + +LANG_DIR=$(dirname "$(realpath $0)") + +ZIG_LANG_FILE=$(realpath "$LANG_DIR/../../src/config/Lang.zig") + +if [ ! -f "$ZIG_LANG_FILE" ]; then + echo "ERROR: File '$ZIG_LANG_FILE' does not exist. Exiting." >&2 + exit 1 +fi + +declare -a LANG_STRINGS + +while read -r line; do + if [[ "$line" =~ ^([^:]*): ]]; then + LANG_STRINGS+=("${BASH_REMATCH[1]}") + else + echo "ERROR: Line '$line' in file '$ZIG_LANG_FILE' does not contain an entry of the pattern ': ...'." >&2 + exit 1 + fi +done < "$ZIG_LANG_FILE" + +for file in $LANG_DIR/*.ini; do + process_lang_file "$file" +done diff --git a/res/lang/fr.ini b/res/lang/fr.ini index 5685c95..a2f4566 100644 --- a/res/lang/fr.ini +++ b/res/lang/fr.ini @@ -1,4 +1,4 @@ -authenticating = authentification... + brightness_down = diminuer la luminosité brightness_up = augmenter la luminosité capslock = verr.maj @@ -6,6 +6,7 @@ err_alloc = échec d'allocation mémoire err_bounds = indice hors-limite err_brightness_change = échec du changement de luminosité err_chdir = échec de l'ouverture du répertoire home + err_console_dev = échec d'accès à la console err_dgn_oob = message err_domain = domaine invalide @@ -35,6 +36,7 @@ err_perm_dir = échec de changement de répertoire err_perm_group = échec du déclassement des permissions de groupe err_perm_user = échec du déclassement des permissions utilisateur err_pwnam = échec de lecture des infos utilisateur + err_unknown = une erreur inconnue est survenue err_user_gid = échec de modification du GID err_user_init = échec d'initialisation de l'utilisateur @@ -46,14 +48,15 @@ err_xsessions_open = échec de l'ouverture du dossier de sessions insert = insertion login = identifiant logout = déconnecté -normal = normal no_x11_support = support pour x11 désactivé lors de la compilation +normal = normal numlock = verr.num + password = mot de passe restart = redémarrer shell = shell shutdown = éteindre sleep = veille wayland = wayland -xinitrc = xinitrc x11 = x11 +xinitrc = xinitrc diff --git a/res/lang/it.ini b/res/lang/it.ini index 13eb147..e9ba7c0 100644 --- a/res/lang/it.ini +++ b/res/lang/it.ini @@ -1,13 +1,20 @@ + + + capslock = capslock err_alloc = impossibile allocare memoria err_bounds = indice fuori limite + err_chdir = impossibile aprire home directory + err_console_dev = impossibile aprire console err_dgn_oob = messaggio log err_domain = dominio non valido + err_hostname = impossibile ottenere hostname err_mlock = impossibile ottenere lock per la password in memoria err_null = puntatore nullo + err_pam = transazione PAM fallita err_pam_abort = transazione PAM interrotta err_pam_acct_expired = account scaduto @@ -29,17 +36,27 @@ 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_user_gid = impossibile impostare GID utente err_user_init = impossibile inizializzare utente err_user_uid = impossible impostare UID utente + + err_xsessions_dir = impossibile localizzare cartella sessioni err_xsessions_open = impossibile aprire cartella sessioni + login = username logout = scollegato + + numlock = numlock + password = password restart = riavvio shell = shell shutdown = arresto + wayland = wayland + xinitrc = xinitrc diff --git a/res/lang/pl.ini b/res/lang/pl.ini index f37c4a8..37d4ff2 100644 --- a/res/lang/pl.ini +++ b/res/lang/pl.ini @@ -1,13 +1,20 @@ + + + capslock = capslock err_alloc = nieudana alokacja pamięci err_bounds = indeks poza granicami + err_chdir = nie udało się otworzyć folderu domowego + err_console_dev = nie udało się uzyskać dostępu do konsoli err_dgn_oob = wiadomość loga err_domain = niepoprawna domena + err_hostname = nie udało się uzyskać nazwy hosta err_mlock = nie udało się zablokować pamięci haseł err_null = wskaźnik zerowy + err_pam = transakcja pam nieudana err_pam_abort = transakcja pam przerwana err_pam_acct_expired = konto wygasło @@ -29,17 +36,27 @@ err_perm_dir = nie udało się zmienić obecnego katalogu 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_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 + + err_xsessions_dir = nie udało się znaleźć folderu sesji err_xsessions_open = nie udało się otworzyć folderu sesji + login = login logout = wylogowano + + numlock = numlock + password = hasło restart = uruchom ponownie shell = powłoka shutdown = wyłącz + wayland = wayland + xinitrc = xinitrc diff --git a/res/lang/pt.ini b/res/lang/pt.ini index ad2c397..5d117d7 100644 --- a/res/lang/pt.ini +++ b/res/lang/pt.ini @@ -1,13 +1,20 @@ + + + capslock = capslock err_alloc = erro na atribuição de memória err_bounds = índice fora de limites + err_chdir = erro ao abrir a pasta home + err_console_dev = erro ao aceder à consola err_dgn_oob = mensagem de registo err_domain = domínio inválido + err_hostname = erro ao obter o nome do host err_mlock = erro de bloqueio de memória err_null = ponteiro nulo + err_pam = erro na transação pam err_pam_abort = transação pam abortada err_pam_acct_expired = conta expirada @@ -29,17 +36,27 @@ 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_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_xsessions_dir = erro ao localizar a pasta das sessões err_xsessions_open = erro ao abrir a pasta das sessões + login = iniciar sessão logout = terminar sessão + + numlock = numlock + password = palavra-passe 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 017129a..f47fc00 100644 --- a/res/lang/pt_BR.ini +++ b/res/lang/pt_BR.ini @@ -1,13 +1,20 @@ + + + capslock = caixa alta err_alloc = alocação de memória malsucedida err_bounds = índice fora de limites + err_chdir = não foi possível abrir o diretório home + err_console_dev = não foi possível acessar o console err_dgn_oob = mensagem de log err_domain = domínio inválido + err_hostname = não foi possível obter o nome do host err_mlock = bloqueio da memória de senha malsucedido err_null = ponteiro nulo + err_pam = transação pam malsucedida err_pam_abort = transação pam abortada err_pam_acct_expired = conta expirada @@ -29,17 +36,27 @@ 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_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_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 + login = conectar logout = desconectado + + numlock = numlock + password = senha restart = reiniciar shell = shell shutdown = desligar + wayland = wayland + xinitrc = xinitrc diff --git a/res/lang/ro.ini b/res/lang/ro.ini index 884e9da..89e92be 100644 --- a/res/lang/ro.ini +++ b/res/lang/ro.ini @@ -1,7 +1,12 @@ + + + capslock = capslock + + err_console_dev = nu s-a putut accesa consola @@ -9,6 +14,8 @@ err_console_dev = nu s-a putut accesa consola + + err_pam_abort = tranzacţie pam anulată err_pam_acct_expired = cont expirat err_pam_auth = eroare de autentificare @@ -34,12 +41,22 @@ err_perm_user = nu s-a putut face downgrade permisiunilor de utilizator + + + + + login = utilizator logout = opreşte sesiunea + + numlock = numlock + password = parolă 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 356ce5f..a67568a 100644 --- a/res/lang/ru.ini +++ b/res/lang/ru.ini @@ -1,13 +1,20 @@ + + + capslock = capslock err_alloc = не удалось выделить память err_bounds = за пределами индекса + err_chdir = не удалось открыть домашнюю папку + err_console_dev = не удалось получить доступ к консоли err_dgn_oob = отладочное сообщение (log) err_domain = неверный домен + err_hostname = не удалось получить имя хоста err_mlock = сбой блокировки памяти err_null = нулевой указатель + err_pam = pam транзакция не удалась err_pam_abort = pam транзакция прервана err_pam_acct_expired = срок действия аккаунта истёк @@ -29,17 +36,27 @@ err_perm_dir = не удалось изменить текущий катало err_perm_group = не удалось понизить права доступа группы err_perm_user = не удалось понизить права доступа пользователя err_pwnam = не удалось получить информацию о пользователе + + err_user_gid = не удалось установить GID пользователя err_user_init = не удалось инициализировать пользователя err_user_uid = не удалось установить UID пользователя + + err_xsessions_dir = не удалось найти сессионную папку err_xsessions_open = не удалось открыть сессионную папку + login = логин logout = logged out + + numlock = numlock + password = пароль restart = перезагрузить shell = shell shutdown = выключить + wayland = wayland + xinitrc = xinitrc diff --git a/res/lang/sr.ini b/res/lang/sr.ini index 2f685e3..96b73d8 100644 --- a/res/lang/sr.ini +++ b/res/lang/sr.ini @@ -1,13 +1,20 @@ + + + capslock = capslock err_alloc = neuspijesna alokacija memorije err_bounds = izvan granica indeksa + err_chdir = neuspijesno otvaranje home foldera + err_console_dev = neuspijesno pristupanje konzoli 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_pam_abort = pam transakcija prekinuta err_pam_acct_expired = nalog istekao @@ -25,21 +32,31 @@ err_pam_session = greska sesije err_pam_sys = greska sistema err_pam_user_unknown = nepoznat korisnik err_path = neuspjelo postavljanje path-a -err_perm_dir = neuspjelo mijenjanje foldera +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_xsessions_open = neuspijesno otvaranje foldera sesija + login = korisnik logout = izlogovan + + numlock = numlock + password = lozinka restart = ponovo pokreni shell = shell shutdown = ugasi + wayland = wayland + xinitrc = xinitrc diff --git a/res/lang/sv.ini b/res/lang/sv.ini index 46af681..7ace439 100644 --- a/res/lang/sv.ini +++ b/res/lang/sv.ini @@ -1,13 +1,20 @@ + + + capslock = capslock err_alloc = misslyckad minnesallokering err_bounds = utanför banan index + err_chdir = misslyckades att öppna hemkatalog + err_console_dev = misslyckades att komma åt konsol 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_pam = pam-transaktion misslyckades err_pam_abort = pam-transaktion avbröts err_pam_acct_expired = konto upphört @@ -29,17 +36,27 @@ 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 + login = inloggning logout = utloggad + + numlock = numlock + password = lösenord restart = starta om shell = skal shutdown = stäng av + wayland = wayland + xinitrc = xinitrc diff --git a/res/lang/tr.ini b/res/lang/tr.ini index 9bef617..32dae6e 100644 --- a/res/lang/tr.ini +++ b/res/lang/tr.ini @@ -1,13 +1,20 @@ + + + capslock = capslock err_alloc = basarisiz bellek ayirma err_bounds = sinirlarin disinda dizin + err_chdir = ev klasoru acilamadi + err_console_dev = konsola erisilemedi err_dgn_oob = log mesaji err_domain = gecersiz etki alani + err_hostname = ana bilgisayar adi alinamadi err_mlock = parola bellegi kilitlenemedi err_null = bos isaretci hatasi + err_pam = pam islemi basarisiz oldu err_pam_abort = pam islemi durduruldu err_pam_acct_expired = hesabin suresi dolmus @@ -29,17 +36,27 @@ err_perm_dir = gecerli dizin degistirilemedi err_perm_group = grup izinleri dusurulemedi err_perm_user = kullanici izinleri dusurulemedi err_pwnam = kullanici bilgileri alinamadi + + err_user_gid = kullanici icin GID ayarlanamadi err_user_init = kullanici oturumu baslatilamadi err_user_uid = kullanici icin UID ayarlanamadi + + err_xsessions_dir = oturumlar klasoru bulunamadi err_xsessions_open = oturumlar klasoru acilamadi + login = kullanici logout = oturumdan cikis yapildi + + numlock = numlock + password = sifre restart = yeniden baslat shell = shell shutdown = makineyi kapat + wayland = wayland + xinitrc = xinitrc diff --git a/res/lang/uk.ini b/res/lang/uk.ini index 5f5b113..76d48b5 100644 --- a/res/lang/uk.ini +++ b/res/lang/uk.ini @@ -1,13 +1,20 @@ + + + capslock = capslock err_alloc = невдале виділення пам'яті err_bounds = поза межами індексу + err_chdir = не вдалося відкрити домашній каталог + err_console_dev = невдалий доступ до консолі err_dgn_oob = повідомлення журналу (log) err_domain = недійсний домен + err_hostname = не вдалося отримати ім'я хосту err_mlock = збій блокування пам'яті err_null = нульовий вказівник + err_pam = невдала pam транзакція err_pam_abort = pam транзакція перервана err_pam_acct_expired = термін дії акаунту вичерпано @@ -29,17 +36,27 @@ err_perm_dir = не вдалося змінити поточний катало err_perm_group = не вдалося понизити права доступу групи err_perm_user = не вдалося понизити права доступу користувача err_pwnam = не вдалося отримати дані користувача + + err_user_gid = не вдалося змінити GID користувача err_user_init = не вдалося ініціалізувати користувача err_user_uid = не вдалося змінити UID користувача + + err_xsessions_dir = не вдалося знайти каталог сесій err_xsessions_open = не вдалося відкрити каталог сесій + login = логін logout = вийти + + numlock = numlock + password = пароль restart = перезавантажити shell = оболонка shutdown = вимкнути + wayland = wayland + xinitrc = xinitrc diff --git a/src/config/Lang.zig b/src/config/Lang.zig index abe3ed0..cd5f689 100644 --- a/src/config/Lang.zig +++ b/src/config/Lang.zig @@ -48,8 +48,8 @@ err_xsessions_open: []const u8 = "failed to open sessions folder", insert: []const u8 = "insert", login: []const u8 = "login:", logout: []const u8 = "logged out", -normal: []const u8 = "normal", no_x11_support: []const u8 = "x11 support disabled at compile-time", +normal: []const u8 = "normal", numlock: []const u8 = "numlock", other: []const u8 = "other", password: []const u8 = "password:", @@ -58,5 +58,5 @@ shell: [:0]const u8 = "shell", shutdown: []const u8 = "shutdown", sleep: []const u8 = "sleep", wayland: []const u8 = "wayland", -xinitrc: [:0]const u8 = "xinitrc", x11: []const u8 = "x11", +xinitrc: [:0]const u8 = "xinitrc", From 7a1fce660cf9e45b2b0ebfb90dffe3d307486776 Mon Sep 17 00:00:00 2001 From: Moritz Reinel <129004253+moritz-reinel@users.noreply.github.com> Date: Sat, 12 Oct 2024 21:27:19 +0200 Subject: [PATCH 123/530] remove python version of lang script --- res/lang/fix_missing_entries.py | 40 --------------------------------- 1 file changed, 40 deletions(-) delete mode 100644 res/lang/fix_missing_entries.py diff --git a/res/lang/fix_missing_entries.py b/res/lang/fix_missing_entries.py deleted file mode 100644 index 1df81f9..0000000 --- a/res/lang/fix_missing_entries.py +++ /dev/null @@ -1,40 +0,0 @@ -#!/usr/bin/env python3 - -from pathlib import Path -from sys import stderr - - -def process_lang_file(path: str) -> None: - values = {} - with open(path, "r", encoding="UTF-8") as fh: - while line := fh.readline(): - vals = line.split("=") - if len(vals) != 2: - continue - - key = vals[0].strip() - values[key] = vals[1].strip() - - with open(path, "w", encoding="UTF-8") as fh: - for item in lang_strings: - v = values.get(item) - if v is not None: - fh.write(f"{item} = {v}\n") - else: - fh.write("\n") - - -zig_lang_file = Path(__file__).parent.joinpath("../../src/config/Lang.zig").resolve() -if not zig_lang_file.exists(): - print(f"ERROR: File '{zig_lang_file.as_posix()}' does not exist. Exiting.", file=stderr) - exit(1) - -lang_strings = [] -with open(zig_lang_file, "r", encoding="UTF-8") as fh: - while line := fh.readline(): - lang_strings.append(line.split(":")[0]) - -lang_files = [f for f in Path.iterdir(Path(__file__).parent) if f.name.endswith(".ini") and f.is_file()] - -for file in lang_files: - process_lang_file(file.as_posix()) From 47ebe641d9200850b4f528146c0d4ee9f7fe54f9 Mon Sep 17 00:00:00 2001 From: Moritz Reinel <129004253+moritz-reinel@users.noreply.github.com> Date: Sat, 12 Oct 2024 21:51:50 +0200 Subject: [PATCH 124/530] remove necessity for temp file --- res/lang/fix_missing_entries.sh | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/res/lang/fix_missing_entries.sh b/res/lang/fix_missing_entries.sh index 76c6df3..ab63965 100644 --- a/res/lang/fix_missing_entries.sh +++ b/res/lang/fix_missing_entries.sh @@ -4,7 +4,6 @@ set -eu function process_lang_file() { local input_file=$1 - local tmp_file=$(mktemp) local -A lang_strings_in_file while read -r line; do @@ -26,9 +25,7 @@ function process_lang_file() { printf "\n" fi done - } > $tmp_file - - mv "$tmp_file" "$input_file" + } > "$input_file" } LANG_DIR=$(dirname "$(realpath $0)") From fe36879dfb5f384e0414f24cd8eb62c4a121112c Mon Sep 17 00:00:00 2001 From: Moritz Reinel <129004253+moritz-reinel@users.noreply.github.com> Date: Sat, 12 Oct 2024 22:07:29 +0200 Subject: [PATCH 125/530] added note to Lang.zig and updated lang script to not process the comment --- res/lang/fix_missing_entries.sh | 4 +++- src/config/Lang.zig | 5 +++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/res/lang/fix_missing_entries.sh b/res/lang/fix_missing_entries.sh index ab63965..8cdaae2 100644 --- a/res/lang/fix_missing_entries.sh +++ b/res/lang/fix_missing_entries.sh @@ -40,7 +40,9 @@ fi declare -a LANG_STRINGS while read -r line; do - if [[ "$line" =~ ^([^:]*): ]]; then + if [[ -z "$line" || "$line" =~ ^\/\/ ]]; then + : + elif [[ "$line" =~ ^([^:]*): ]]; then LANG_STRINGS+=("${BASH_REMATCH[1]}") else echo "ERROR: Line '$line' in file '$ZIG_LANG_FILE' does not contain an entry of the pattern ': ...'." >&2 diff --git a/src/config/Lang.zig b/src/config/Lang.zig index cd5f689..0e8d6c3 100644 --- a/src/config/Lang.zig +++ b/src/config/Lang.zig @@ -1,3 +1,8 @@ +// +// NOTE: After editing this file, please run `/res/lang/fix_missing_entries.sh` +// to update all the language files accordingly. +// + authenticating: []const u8 = "authenticating...", brightness_down: []const u8 = "decrease brightness", brightness_up: []const u8 = "increase brightness", From 7a82b51ac558eb22ec16f6db23baf46201ec04e5 Mon Sep 17 00:00:00 2001 From: Moritz Reinel <129004253+moritz-reinel@users.noreply.github.com> Date: Mon, 21 Oct 2024 19:42:57 +0200 Subject: [PATCH 126/530] Revert "remove python version of lang script" This reverts commit 7a1fce660cf9e45b2b0ebfb90dffe3d307486776. --- res/lang/fix_missing_entries.py | 40 +++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 res/lang/fix_missing_entries.py diff --git a/res/lang/fix_missing_entries.py b/res/lang/fix_missing_entries.py new file mode 100644 index 0000000..1df81f9 --- /dev/null +++ b/res/lang/fix_missing_entries.py @@ -0,0 +1,40 @@ +#!/usr/bin/env python3 + +from pathlib import Path +from sys import stderr + + +def process_lang_file(path: str) -> None: + values = {} + with open(path, "r", encoding="UTF-8") as fh: + while line := fh.readline(): + vals = line.split("=") + if len(vals) != 2: + continue + + key = vals[0].strip() + values[key] = vals[1].strip() + + with open(path, "w", encoding="UTF-8") as fh: + for item in lang_strings: + v = values.get(item) + if v is not None: + fh.write(f"{item} = {v}\n") + else: + fh.write("\n") + + +zig_lang_file = Path(__file__).parent.joinpath("../../src/config/Lang.zig").resolve() +if not zig_lang_file.exists(): + print(f"ERROR: File '{zig_lang_file.as_posix()}' does not exist. Exiting.", file=stderr) + exit(1) + +lang_strings = [] +with open(zig_lang_file, "r", encoding="UTF-8") as fh: + while line := fh.readline(): + lang_strings.append(line.split(":")[0]) + +lang_files = [f for f in Path.iterdir(Path(__file__).parent) if f.name.endswith(".ini") and f.is_file()] + +for file in lang_files: + process_lang_file(file.as_posix()) From 0dea19c8db7ee14e0a9ccf3abf7e8194e2db672b Mon Sep 17 00:00:00 2001 From: Moritz Reinel <129004253+moritz-reinel@users.noreply.github.com> Date: Mon, 21 Oct 2024 19:43:33 +0200 Subject: [PATCH 127/530] remove bash version of script --- res/lang/fix_missing_entries.sh | 55 --------------------------------- 1 file changed, 55 deletions(-) delete mode 100644 res/lang/fix_missing_entries.sh diff --git a/res/lang/fix_missing_entries.sh b/res/lang/fix_missing_entries.sh deleted file mode 100644 index 8cdaae2..0000000 --- a/res/lang/fix_missing_entries.sh +++ /dev/null @@ -1,55 +0,0 @@ -#!/bin/bash - -set -eu - -function process_lang_file() { - local input_file=$1 - local -A lang_strings_in_file - - while read -r line; do - if [[ -z "$line" ]]; then - : - elif [[ "$line" =~ ^([^\ ]*)[\ ]?\=[\ ]?(.*) ]]; then - lang_strings_in_file["${BASH_REMATCH[1]}"]="${BASH_REMATCH[2]}" - else - echo "ERROR: Line '$line' in file '$input_file' does not contain an entry of the pattern ' = '. Exiting." >&2 - exit 1 - fi - done < "$input_file" - - { - for s in "${LANG_STRINGS[@]}"; do - if [[ -v "lang_strings_in_file[\"$s\"]" ]]; then - printf "%s = %s\n" "$s" "${lang_strings_in_file[$s]}" - else - printf "\n" - fi - done - } > "$input_file" -} - -LANG_DIR=$(dirname "$(realpath $0)") - -ZIG_LANG_FILE=$(realpath "$LANG_DIR/../../src/config/Lang.zig") - -if [ ! -f "$ZIG_LANG_FILE" ]; then - echo "ERROR: File '$ZIG_LANG_FILE' does not exist. Exiting." >&2 - exit 1 -fi - -declare -a LANG_STRINGS - -while read -r line; do - if [[ -z "$line" || "$line" =~ ^\/\/ ]]; then - : - elif [[ "$line" =~ ^([^:]*): ]]; then - LANG_STRINGS+=("${BASH_REMATCH[1]}") - else - echo "ERROR: Line '$line' in file '$ZIG_LANG_FILE' does not contain an entry of the pattern ': ...'." >&2 - exit 1 - fi -done < "$ZIG_LANG_FILE" - -for file in $LANG_DIR/*.ini; do - process_lang_file "$file" -done From b6726a76c6114d747e5f84adb6e23def79d54915 Mon Sep 17 00:00:00 2001 From: Moritz Reinel <129004253+moritz-reinel@users.noreply.github.com> Date: Sat, 26 Oct 2024 13:09:30 +0200 Subject: [PATCH 128/530] refactored some variables + added comments --- res/lang/fix_missing_entries.py | 55 +++++++++++++++++++-------------- 1 file changed, 32 insertions(+), 23 deletions(-) diff --git a/res/lang/fix_missing_entries.py b/res/lang/fix_missing_entries.py index 1df81f9..3904737 100644 --- a/res/lang/fix_missing_entries.py +++ b/res/lang/fix_missing_entries.py @@ -4,37 +4,46 @@ from pathlib import Path from sys import stderr -def process_lang_file(path: str) -> None: - values = {} +def process_lang_file(path: Path, lang_keys: list[str]) -> None: + # read key-value-pairs from lang file into dict + existing_entries = {} with open(path, "r", encoding="UTF-8") as fh: while line := fh.readline(): - vals = line.split("=") - if len(vals) != 2: + try: + key, value = line.split("=", 1) + existing_entries[key.strip()] = value.strip() + except ValueError: # line does not contain '=' continue - key = vals[0].strip() - values[key] = vals[1].strip() - + # re-write current lang file with entries in order of occurence in `lang_keys` + # and with empty lines for missing translations with open(path, "w", encoding="UTF-8") as fh: - for item in lang_strings: - v = values.get(item) - if v is not None: - fh.write(f"{item} = {v}\n") - else: + for item in lang_keys: + try: + fh.write(f"{item} = {existing_entries[item]}\n") + except KeyError: # no translation for `item` yet fh.write("\n") -zig_lang_file = Path(__file__).parent.joinpath("../../src/config/Lang.zig").resolve() -if not zig_lang_file.exists(): - print(f"ERROR: File '{zig_lang_file.as_posix()}' does not exist. Exiting.", file=stderr) - exit(1) +def main() -> None: + zig_lang_file = Path(__file__).parent.joinpath("../../src/config/Lang.zig").resolve() + if not zig_lang_file.exists(): + print(f"ERROR: File '{zig_lang_file.as_posix()}' does not exist. Exiting.", file=stderr) + exit(1) -lang_strings = [] -with open(zig_lang_file, "r", encoding="UTF-8") as fh: - while line := fh.readline(): - lang_strings.append(line.split(":")[0]) + # read "language keys" from `zig_lang_file` into list + lang_keys = [] + with open(zig_lang_file, "r", encoding="UTF-8") as fh: + while line := fh.readline(): + # only process lines that are not empty or no comments + if not (line.strip() == "" or line.startswith("//")): + lang_keys.append(line.split(":")[0].strip()) -lang_files = [f for f in Path.iterdir(Path(__file__).parent) if f.name.endswith(".ini") and f.is_file()] + lang_files = [f for f in Path.iterdir(Path(__file__).parent) if f.name.endswith(".ini") and f.is_file()] -for file in lang_files: - process_lang_file(file.as_posix()) + for file in lang_files: + process_lang_file(file, lang_keys) + + +if __name__ == "__main__": + main() From 117eccd65a44f67a61a328d559f2c4458d94dc20 Mon Sep 17 00:00:00 2001 From: Moritz Reinel <129004253+moritz-reinel@users.noreply.github.com> Date: Sat, 26 Oct 2024 13:19:33 +0200 Subject: [PATCH 129/530] rename language normalizer script --- res/lang/{fix_missing_entries.py => normalize_lang_files.py} | 0 src/config/Lang.zig | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename res/lang/{fix_missing_entries.py => normalize_lang_files.py} (100%) diff --git a/res/lang/fix_missing_entries.py b/res/lang/normalize_lang_files.py similarity index 100% rename from res/lang/fix_missing_entries.py rename to res/lang/normalize_lang_files.py diff --git a/src/config/Lang.zig b/src/config/Lang.zig index 0e8d6c3..a8ff9fc 100644 --- a/src/config/Lang.zig +++ b/src/config/Lang.zig @@ -1,5 +1,5 @@ // -// NOTE: After editing this file, please run `/res/lang/fix_missing_entries.sh` +// NOTE: After editing this file, please run `/res/lang/normalize_lang_files.py` // to update all the language files accordingly. // From f5f7422d829089c54632c74a7d6cbbfe5872b5e4 Mon Sep 17 00:00:00 2001 From: winlith Date: Fri, 17 Jan 2025 22:37:48 +0100 Subject: [PATCH 130/530] color mix animation --- res/config.ini | 16 ++++++-- src/animations/ColorMix.zig | 76 +++++++++++++++++++++++++++++++++++++ src/config/Config.zig | 3 ++ src/enums.zig | 3 +- src/main.zig | 7 ++++ 5 files changed, 101 insertions(+), 4 deletions(-) create mode 100644 src/animations/ColorMix.zig diff --git a/res/config.ini b/res/config.ini index 1d28cb8..debb6df 100644 --- a/res/config.ini +++ b/res/config.ini @@ -30,9 +30,10 @@ # config) will be used by Ly for fg = 0x0008. # The active animation -# none -> Nothing -# doom -> PSX DOOM fire -# matrix -> CMatrix +# none -> Nothing +# doom -> PSX DOOM fire +# matrix -> CMatrix +# colormix -> Color mixing shader animation = none # Stop the animation after some time @@ -90,6 +91,15 @@ clock = null # CMatrix animation foreground color id cmatrix_fg = 0x0003 +# Color mixing animation first color id +colormix_col1 = 0x0002 + +# Color mixing animation second color id +colormix_col2 = 0x0005 + +# Color mixing animation third color id +colormix_col3 = 0x0001 + # Console path console_dev = /dev/console diff --git a/src/animations/ColorMix.zig b/src/animations/ColorMix.zig new file mode 100644 index 0000000..d9b262a --- /dev/null +++ b/src/animations/ColorMix.zig @@ -0,0 +1,76 @@ +const math = @import("std").math; +const TerminalBuffer = @import("../tui/TerminalBuffer.zig"); +const utils = @import("../tui/utils.zig"); + +const ColorMix = @This(); + +const Vec2 = @Vector(2, f32); + +const time_scale: f32 = 0.01; +const palette_len: usize = 12; + +fn length(vec: Vec2) f32 { + return math.sqrt(vec[0] * vec[0] + vec[1] * vec[1]); +} + +terminal_buffer: *TerminalBuffer, +frames: u64, +pattern_cos_mod: f32, +pattern_sin_mod: f32, +palette: [palette_len]utils.Cell, + +pub fn init(terminal_buffer: *TerminalBuffer, col1: u16, col2: u16, col3: u16) ColorMix { + return .{ + .terminal_buffer = terminal_buffer, + .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, + .palette = [palette_len]utils.Cell{ + utils.initCell(0x2588, col1, col2), + utils.initCell(0x2593, col1, col2), + utils.initCell(0x2592, col1, col2), + utils.initCell(0x2591, col1, col2), + utils.initCell(0x2588, col2, col3), + utils.initCell(0x2593, col2, col3), + utils.initCell(0x2592, col2, col3), + utils.initCell(0x2591, col2, col3), + utils.initCell(0x2588, col3, col1), + utils.initCell(0x2593, col3, col1), + utils.initCell(0x2592, col3, col1), + utils.initCell(0x2591, col3, col1), + }, + }; +} + +pub fn draw(self: *ColorMix) void { + self.frames +%= 1; + const time: f32 = @as(f32, @floatFromInt(self.frames)) * time_scale; + + for (0..self.terminal_buffer.width) |x| { + for (0..self.terminal_buffer.height) |y| { + var uv: Vec2 = .{ + @as(f32, @floatFromInt(@as(i32, @intCast(x)) * 2 - @as(i32, @intCast(self.terminal_buffer.width)))) / @as(f32, @floatFromInt(self.terminal_buffer.height * 2)), + @as(f32, @floatFromInt(@as(i32, @intCast(y)) * 2 - @as(i32, @intCast(self.terminal_buffer.height)))) / @as(f32, @floatFromInt(self.terminal_buffer.height)), + }; + + var uv2: Vec2 = @splat(uv[0] + uv[1]); + + for (0..3) |_| { + uv2 += uv + @as(Vec2, @splat(length(uv))); + uv += @as(Vec2, @splat(0.5)) * Vec2{ + math.cos(self.pattern_cos_mod + uv2[1] * 0.2 + time * 0.1), + math.sin(self.pattern_sin_mod + uv2[0] - time * 0.1), + }; + 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 screen_index: usize = y * self.terminal_buffer.width + x; + self.terminal_buffer.buffer[screen_index] = .{ + .ch = cell.ch, + .fg = cell.fg, + .bg = cell.bg, + }; + } + } +} diff --git a/src/config/Config.zig b/src/config/Config.zig index 38c8b4c..1ab4232 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -22,6 +22,9 @@ brightness_up_key: []const u8 = "F6", clear_password: bool = false, clock: ?[:0]const u8 = null, cmatrix_fg: u16 = 3, +colormix_col1: u16 = 2, +colormix_col2: u16 = 5, +colormix_col3: u16 = 1, console_dev: []const u8 = "/dev/console", default_input: Input = .login, error_bg: u16 = 0, diff --git a/src/enums.zig b/src/enums.zig index ad0cc47..6d1f1f3 100644 --- a/src/enums.zig +++ b/src/enums.zig @@ -2,6 +2,7 @@ pub const Animation = enum { none, doom, matrix, + colormix, }; pub const DisplayServer = enum { @@ -27,4 +28,4 @@ pub const Bigclock = enum { none, en, fa, -}; \ No newline at end of file +}; diff --git a/src/main.zig b/src/main.zig index 4afefad..e58fafe 100644 --- a/src/main.zig +++ b/src/main.zig @@ -8,6 +8,7 @@ const bigclock = @import("bigclock.zig"); const interop = @import("interop.zig"); const Doom = @import("animations/Doom.zig"); const Matrix = @import("animations/Matrix.zig"); +const ColorMix = @import("animations/ColorMix.zig"); const TerminalBuffer = @import("tui/TerminalBuffer.zig"); const Session = @import("tui/components/Session.zig"); const Text = @import("tui/components/Text.zig"); @@ -303,17 +304,20 @@ pub fn main() !void { // Initialize the animation, if any var doom: Doom = undefined; var matrix: Matrix = undefined; + var color_mix: ColorMix = undefined; switch (config.animation) { .none => {}, .doom => doom = try Doom.init(allocator, &buffer), .matrix => matrix = try Matrix.init(allocator, &buffer, config.cmatrix_fg), + .colormix => color_mix = ColorMix.init(&buffer, config.colormix_col1, config.colormix_col2, config.colormix_col3), } defer { switch (config.animation) { .none => {}, .doom => doom.deinit(), .matrix => matrix.deinit(), + .colormix => {}, } } @@ -365,6 +369,7 @@ pub fn main() !void { .matrix => matrix.realloc() catch { try info_line.addMessage(lang.err_alloc, config.error_bg, config.error_fg); }, + .colormix => {}, } update = true; @@ -382,6 +387,7 @@ pub fn main() !void { .none => {}, .doom => doom.draw(), .matrix => matrix.draw(), + .colormix => color_mix.draw(), } } @@ -542,6 +548,7 @@ pub fn main() !void { .none => {}, .doom => doom.deinit(), .matrix => matrix.deinit(), + .colormix => {}, } } } else if (config.bigclock != .none and config.clock == null) { From d171634f9bde29bd62e8a0a5ac1e5bfb7b63c48d Mon Sep 17 00:00:00 2001 From: villamorrd Date: Sat, 1 Feb 2025 20:58:52 +0800 Subject: [PATCH 131/530] fix: don't include dest_dir in config_dir This should fix $CONFIG_DIRECTORY including the installation directory in its path. --- build.zig | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/build.zig b/build.zig index f0e9074..cd2acdc 100644 --- a/build.zig +++ b/build.zig @@ -31,7 +31,6 @@ pub fn build(b: *std.Build) !void { executable_name = b.option([]const u8, "name", "Specify installed executable file name (default is ly)") orelse "ly"; const bin_directory = try b.allocator.dupe(u8, config_directory); - config_directory = try std.fs.path.join(b.allocator, &[_][]const u8{ dest_directory, config_directory }); const build_options = b.addOptions(); const version_str = try getVersionStr(b, "ly", ly_version); @@ -222,22 +221,22 @@ pub fn ServiceInstaller(comptime init_system: InitSystem) type { } fn install_ly(allocator: std.mem.Allocator, install_config: bool) !void { - const ly_config_directory = try std.fs.path.join(allocator, &[_][]const u8{ config_directory, "/ly" }); + const ly_config_directory = try std.fs.path.join(allocator, &[_][]const u8{ dest_directory, config_directory, "/ly" }); std.fs.cwd().makePath(ly_config_directory) catch { std.debug.print("warn: {s} already exists as a directory.\n", .{ly_config_directory}); }; - const ly_lang_path = try std.fs.path.join(allocator, &[_][]const u8{ config_directory, "/ly/lang" }); + 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 { - std.debug.print("warn: {s} already exists as a directory.\n", .{config_directory}); + 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" }); if (!std.mem.eql(u8, dest_directory, "")) { std.fs.cwd().makePath(exe_path) catch { - std.debug.print("warn: {s} already exists as a directory.\n", .{exe_path}); + std.debug.print("warn: {s} already exists as a directory.\n", .{ exe_path }); }; } From 346a614ba063021c35169d62de0cf46ecd3475ad Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Wed, 5 Mar 2025 02:03:40 +0000 Subject: [PATCH 132/530] Mention Codeberg in the README Signed-off-by: AnErrupTion --- readme.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/readme.md b/readme.md index d0d137b..3302da6 100644 --- a/readme.md +++ b/readme.md @@ -1,5 +1,7 @@ - # Ly - a TUI display manager + +## Development is now continuing on [Codeberg](https://codeberg.org/AnErrupTion/ly), with the [GitHub](https://github.com/fairyglade/ly) repository becoming a mirror. Issues & pull requests on GitHub will be ignored from now on. + ![Ly screenshot](.github/screenshot.png "Ly screenshot") Ly is a lightweight TUI (ncurses-like) display manager for Linux and BSD. From bebccf4d5afffb93efdde4c4b8ab09d168765c72 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Wed, 5 Mar 2025 19:42:00 +0100 Subject: [PATCH 133/530] Support Zig 0.14.0 only (with 1 downstream dependency fork) Signed-off-by: AnErrupTion --- build.zig | 10 ++++------ build.zig.zon | 15 ++++++++------- src/SharedError.zig | 2 +- src/animations/Matrix.zig | 2 +- src/auth.zig | 4 ++-- src/main.zig | 34 ++++++++++++++++++++++++++-------- src/tui/components/Session.zig | 5 ++++- 7 files changed, 46 insertions(+), 26 deletions(-) diff --git a/build.zig b/build.zig index f0e9074..f2d05c6 100644 --- a/build.zig +++ b/build.zig @@ -3,7 +3,7 @@ const builtin = @import("builtin"); const PatchMap = std.StringHashMap([]const u8); -const min_zig_string = "0.12.0"; +const min_zig_string = "0.14.0"; const current_zig = builtin.zig_version; // Implementing zig version detection through compile time @@ -22,8 +22,6 @@ var prefix_directory: []const u8 = undefined; var executable_name: []const u8 = undefined; var default_tty_str: []const u8 = undefined; -const ProgressNode = if (current_zig.minor == 12) *std.Progress.Node else std.Progress.Node; - 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"; @@ -123,7 +121,7 @@ pub fn build(b: *std.Build) !void { pub fn ExeInstaller(install_conf: bool) type { return struct { - pub fn make(step: *std.Build.Step, _: ProgressNode) !void { + pub fn make(step: *std.Build.Step, _: std.Build.Step.MakeOptions) !void { try install_ly(step.owner.allocator, install_conf); } }; @@ -139,7 +137,7 @@ const InitSystem = enum { pub fn ServiceInstaller(comptime init_system: InitSystem) type { return struct { - pub fn make(step: *std.Build.Step, _: ProgressNode) !void { + pub fn make(step: *std.Build.Step, _: std.Build.Step.MakeOptions) !void { const allocator = step.owner.allocator; var patch_map = PatchMap.init(allocator); @@ -311,7 +309,7 @@ fn install_ly(allocator: std.mem.Allocator, install_config: bool) !void { } } -pub fn uninstallall(step: *std.Build.Step, _: ProgressNode) !void { +pub fn uninstallall(step: *std.Build.Step, _: std.Build.Step.MakeOptions) !void { const allocator = step.owner.allocator; try deleteTree(allocator, config_directory, "/ly", "ly config directory not found"); diff --git a/build.zig.zon b/build.zig.zon index c4c9cbd..3bfddce 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -1,15 +1,16 @@ .{ - .name = "ly", - .version = "1.0.0", - .minimum_zig_version = "0.12.0", + .name = .ly, + .version = "1.1.0", + .fingerprint = 0xa148ffcc5dc2cb59, + .minimum_zig_version = "0.14.0", .dependencies = .{ .clap = .{ - .url = "https://github.com/Hejsil/zig-clap/archive/refs/tags/0.9.1.tar.gz", - .hash = "122062d301a203d003547b414237229b09a7980095061697349f8bef41be9c30266b", + .url = "https://github.com/Hejsil/zig-clap/archive/refs/tags/0.10.0.tar.gz", + .hash = "clap-0.10.0-oBajB434AQBDh-Ei3YtoKIRxZacVPF1iSwp3IX_ZB8f0", }, .zigini = .{ - .url = "https://github.com/Kawaii-Ash/zigini/archive/0bba97a12582928e097f4074cc746c43351ba4c8.tar.gz", - .hash = "12209b971367b4066d40ecad4728e6fdffc4cc4f19356d424c2de57f5b69ac7a619a", + .url = "https://github.com/AnErrupTion/zigini/archive/e61d31b2b7db3365993a20cc90e491d0cb0b7282.tar.gz", + .hash = "zigini-0.3.1-BSkB7XJGAAB2E-sKyzhTaQCBlYBL8yqzE4E_jmSY99sC", }, }, .paths = .{""}, diff --git a/src/SharedError.zig b/src/SharedError.zig index dcf1f79..9e5de3b 100644 --- a/src/SharedError.zig +++ b/src/SharedError.zig @@ -9,7 +9,7 @@ const ErrorHandler = packed struct { const SharedError = @This(); -data: []align(std.mem.page_size) u8, +data: []align(std.heap.page_size_min) u8, pub fn init() !SharedError { const data = try std.posix.mmap(null, @sizeOf(ErrorHandler), std.posix.PROT.READ | std.posix.PROT.WRITE, .{ .TYPE = .SHARED, .ANONYMOUS = true }, -1, 0); diff --git a/src/animations/Matrix.zig b/src/animations/Matrix.zig index ba7e6e1..42bf89b 100644 --- a/src/animations/Matrix.zig +++ b/src/animations/Matrix.zig @@ -1,6 +1,6 @@ const std = @import("std"); const Allocator = std.mem.Allocator; -const Random = std.rand.Random; +const Random = std.Random; const TerminalBuffer = @import("../tui/TerminalBuffer.zig"); const interop = @import("../interop.zig"); diff --git a/src/auth.zig b/src/auth.zig index 077fd35..a9d4772 100644 --- a/src/auth.zig +++ b/src/auth.zig @@ -108,7 +108,7 @@ pub fn authenticate(config: Config, current_environment: Session.Environment, lo .mask = std.posix.empty_sigset, .flags = 0, }; - try std.posix.sigaction(std.posix.SIG.TERM, &act, null); + std.posix.sigaction(std.posix.SIG.TERM, &act, null); try addUtmpEntry(&entry, pwd.pw_name.?, child_pid); } @@ -439,7 +439,7 @@ fn executeX11Cmd(shell: [*:0]const u8, pw_dir: [*:0]const u8, config: Config, de .mask = std.posix.empty_sigset, .flags = 0, }; - try std.posix.sigaction(std.posix.SIG.TERM, &act, null); + std.posix.sigaction(std.posix.SIG.TERM, &act, null); _ = std.posix.waitpid(xorg_pid, 0); interop.xcb.xcb_disconnect(xcb); diff --git a/src/main.zig b/src/main.zig index 4afefad..915b44e 100644 --- a/src/main.zig +++ b/src/main.zig @@ -127,7 +127,10 @@ pub fn main() !void { 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, comment_characters, migrator.configFieldHandler) catch _config: { + config = config_ini.readFileToStruct(config_path, .{ + .fieldHandler = migrator.configFieldHandler, + .comment_characters = comment_characters, + }) catch _config: { config_load_failed = true; break :_config Config{}; }; @@ -135,21 +138,30 @@ pub fn main() !void { 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, comment_characters, null) catch Lang{}; + lang = lang_ini.readFileToStruct(lang_path, .{ + .fieldHandler = null, + .comment_characters = comment_characters, + }) catch Lang{}; if (config.load) { save_path = try std.fmt.allocPrint(allocator, "{s}{s}save.ini", .{ s, trailing_slash }); save_path_alloc = true; var user_buf: [32]u8 = undefined; - save = save_ini.readFileToStruct(save_path, comment_characters, null) catch migrator.tryMigrateSaveFile(&user_buf); + save = save_ini.readFileToStruct(save_path, .{ + .fieldHandler = null, + .comment_characters = comment_characters, + }) catch migrator.tryMigrateSaveFile(&user_buf); } migrator.lateConfigFieldHandler(&config.animation); } else { const config_path = build_options.config_directory ++ "/ly/config.ini"; - config = config_ini.readFileToStruct(config_path, comment_characters, migrator.configFieldHandler) catch _config: { + config = config_ini.readFileToStruct(config_path, .{ + .fieldHandler = migrator.configFieldHandler, + .comment_characters = comment_characters, + }) catch _config: { config_load_failed = true; break :_config Config{}; }; @@ -157,11 +169,17 @@ pub fn main() !void { 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, comment_characters, null) catch Lang{}; + lang = lang_ini.readFileToStruct(lang_path, .{ + .fieldHandler = null, + .comment_characters = comment_characters, + }) catch Lang{}; if (config.load) { var user_buf: [32]u8 = undefined; - save = save_ini.readFileToStruct(save_path, comment_characters, null) catch migrator.tryMigrateSaveFile(&user_buf); + save = save_ini.readFileToStruct(save_path, .{ + .fieldHandler = null, + .comment_characters = comment_characters, + }) catch migrator.tryMigrateSaveFile(&user_buf); } migrator.lateConfigFieldHandler(&config.animation); @@ -191,7 +209,7 @@ pub fn main() !void { .mask = std.posix.empty_sigset, .flags = 0, }; - try std.posix.sigaction(std.posix.SIG.TERM, &act, null); + std.posix.sigaction(std.posix.SIG.TERM, &act, null); _ = termbox.tb_set_output_mode(termbox.TB_OUTPUT_NORMAL); _ = termbox.tb_clear(); @@ -670,7 +688,7 @@ pub fn main() !void { .user = login.text.items, .session_index = session.label.current, }; - ini.writeFromStruct(save_data, file.writer(), null, true, .{}) catch break :save_last_settings; + ini.writeFromStruct(save_data, file.writer(), null, .{}) catch break :save_last_settings; // Delete previous save file if it exists if (migrator.maybe_save_file) |path| std.fs.cwd().deleteFile(path) catch {}; diff --git a/src/tui/components/Session.zig b/src/tui/components/Session.zig index 2cc081a..d513949 100644 --- a/src/tui/components/Session.zig +++ b/src/tui/components/Session.zig @@ -110,7 +110,10 @@ pub fn crawl(self: *Session, path: []const u8, display_server: DisplayServer) !v const entry_path = try std.fmt.allocPrint(self.label.allocator, "{s}/{s}", .{ path, item.name }); defer self.label.allocator.free(entry_path); var entry_ini = Ini(Entry).init(self.label.allocator); - _ = try entry_ini.readFileToStruct(entry_path, "#", null); + _ = try entry_ini.readFileToStruct(entry_path, .{ + .fieldHandler = null, + .comment_characters = "#", + }); errdefer entry_ini.deinit(); var xdg_session_desktop: []const u8 = undefined; From 895edaf904e653ae7152eda9d6962347d8a114c2 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Wed, 5 Mar 2025 19:43:02 +0100 Subject: [PATCH 134/530] Update README to only include Zig 0.14.0 Signed-off-by: AnErrupTion --- readme.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/readme.md b/readme.md index 3302da6..f9d8cb5 100644 --- a/readme.md +++ b/readme.md @@ -8,7 +8,7 @@ Ly is a lightweight TUI (ncurses-like) display manager for Linux and BSD. ## Dependencies - Compile-time: - - zig >=0.12.0 + - zig 0.14.0 - libc - pam - xcb (optional, required by default; needed for X11 support) @@ -254,4 +254,3 @@ disable the main box borders with `hide_borders = true`. ## Additional Information The name "Ly" is a tribute to the fairy from the game Rayman. Ly was tested by oxodao, who is some seriously awesome dude. - From 24599368dff77b8906d4c8fcb8159802fe6178d2 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Wed, 5 Mar 2025 22:06:56 +0100 Subject: [PATCH 135/530] Minor changes in color mix animation Signed-off-by: AnErrupTion --- src/animations/ColorMix.zig | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/animations/ColorMix.zig b/src/animations/ColorMix.zig index d9b262a..fb22881 100644 --- a/src/animations/ColorMix.zig +++ b/src/animations/ColorMix.zig @@ -1,9 +1,10 @@ -const math = @import("std").math; +const std = @import("std"); const TerminalBuffer = @import("../tui/TerminalBuffer.zig"); const utils = @import("../tui/utils.zig"); const ColorMix = @This(); +const math = std.math; const Vec2 = @Vector(2, f32); const time_scale: f32 = 0.01; @@ -48,9 +49,14 @@ pub fn draw(self: *ColorMix) void { for (0..self.terminal_buffer.width) |x| { for (0..self.terminal_buffer.height) |y| { + const xi: i32 = @intCast(x); + const yi: i32 = @intCast(y); + const wi: i32 = @intCast(self.terminal_buffer.width); + const hi: i32 = @intCast(self.terminal_buffer.height); + var uv: Vec2 = .{ - @as(f32, @floatFromInt(@as(i32, @intCast(x)) * 2 - @as(i32, @intCast(self.terminal_buffer.width)))) / @as(f32, @floatFromInt(self.terminal_buffer.height * 2)), - @as(f32, @floatFromInt(@as(i32, @intCast(y)) * 2 - @as(i32, @intCast(self.terminal_buffer.height)))) / @as(f32, @floatFromInt(self.terminal_buffer.height)), + @as(f32, @floatFromInt(xi * 2 - wi)) / @as(f32, @floatFromInt(self.terminal_buffer.height * 2)), + @as(f32, @floatFromInt(yi * 2 - hi)) / @as(f32, @floatFromInt(self.terminal_buffer.height)), }; var uv2: Vec2 = @splat(uv[0] + uv[1]); From 85e071a60a96681a5dccbaa2bfaad332a800a4ab Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Wed, 5 Mar 2025 22:07:18 +0100 Subject: [PATCH 136/530] Code style changes in build.zig Signed-off-by: AnErrupTion --- build.zig | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.zig b/build.zig index 601cd6e..9ea68bf 100644 --- a/build.zig +++ b/build.zig @@ -227,14 +227,14 @@ fn install_ly(allocator: std.mem.Allocator, install_config: bool) !void { 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 { - std.debug.print("warn: {s} already exists as a directory.\n", .{ ly_lang_path }); + 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" }); if (!std.mem.eql(u8, dest_directory, "")) { std.fs.cwd().makePath(exe_path) catch { - std.debug.print("warn: {s} already exists as a directory.\n", .{ exe_path }); + std.debug.print("warn: {s} already exists as a directory.\n", .{exe_path}); }; } From 87503367e9ca5937ccbf34b4f5c532422657aa50 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Wed, 5 Mar 2025 22:36:17 +0100 Subject: [PATCH 137/530] Try to create /etc/pam.d and /usr/bin everytime when installing Signed-off-by: AnErrupTion --- build.zig | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/build.zig b/build.zig index 9ea68bf..5984a45 100644 --- a/build.zig +++ b/build.zig @@ -232,11 +232,11 @@ fn install_ly(allocator: std.mem.Allocator, install_config: bool) !void { { const exe_path = try std.fs.path.join(allocator, &[_][]const u8{ dest_directory, prefix_directory, "/bin" }); - if (!std.mem.eql(u8, dest_directory, "")) { - std.fs.cwd().makePath(exe_path) catch { + std.fs.cwd().makePath(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(); @@ -295,11 +295,11 @@ fn install_ly(allocator: std.mem.Allocator, install_config: bool) !void { { const pam_path = try std.fs.path.join(allocator, &[_][]const u8{ dest_directory, config_directory, "/pam.d" }); - if (!std.mem.eql(u8, dest_directory, "")) { - std.fs.cwd().makePath(pam_path) catch { + std.fs.cwd().makePath(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(); From f54657432a911258a1b01cb1cc6a35f0f2fc8a28 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Wed, 5 Mar 2025 22:50:09 +0100 Subject: [PATCH 138/530] Don't set XDG_CURRENT_DESKTOP and XDG_SESSION_DESKTOP if they're empty (closes #702) Signed-off-by: AnErrupTion --- src/auth.zig | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/auth.zig b/src/auth.zig index a9d4772..cf9b743 100644 --- a/src/auth.zig +++ b/src/auth.zig @@ -32,7 +32,7 @@ pub fn authenticate(config: Config, current_environment: Session.Environment, lo // Set the XDG environment variables setXdgSessionEnv(current_environment.display_server); - try setXdgEnv(tty_str, current_environment.xdg_session_desktop orelse "", current_environment.xdg_desktop_names orelse ""); + try setXdgEnv(tty_str, current_environment.xdg_session_desktop, current_environment.xdg_desktop_names); // Open the PAM session var credentials = [_:null]?[*:0]const u8{ login, password }; @@ -188,7 +188,7 @@ fn setXdgSessionEnv(display_server: enums.DisplayServer) void { }, 0); } -fn setXdgEnv(tty_str: [:0]u8, desktop_name: [:0]const u8, xdg_desktop_names: [:0]const u8) !void { +fn setXdgEnv(tty_str: [:0]u8, maybe_desktop_name: ?[:0]const u8, maybe_xdg_desktop_names: ?[:0]const u8) !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 @@ -201,10 +201,10 @@ fn setXdgEnv(tty_str: [:0]u8, desktop_name: [:0]const u8, xdg_desktop_names: [:0 _ = interop.stdlib.setenv("XDG_RUNTIME_DIR", uid_str, 0); } - _ = interop.stdlib.setenv("XDG_CURRENT_DESKTOP", xdg_desktop_names, 0); + if (maybe_xdg_desktop_names) |xdg_desktop_names| _ = interop.stdlib.setenv("XDG_CURRENT_DESKTOP", xdg_desktop_names, 0); _ = interop.stdlib.setenv("XDG_SESSION_CLASS", "user", 0); _ = interop.stdlib.setenv("XDG_SESSION_ID", "1", 0); - _ = interop.stdlib.setenv("XDG_SESSION_DESKTOP", desktop_name, 0); + if (maybe_desktop_name) |desktop_name| _ = interop.stdlib.setenv("XDG_SESSION_DESKTOP", desktop_name, 0); _ = interop.stdlib.setenv("XDG_SEAT", "seat0", 0); _ = interop.stdlib.setenv("XDG_VTNR", tty_str, 0); } From a9449742d6392d2d03796093990e6649f3983a62 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Wed, 5 Mar 2025 22:52:34 +0100 Subject: [PATCH 139/530] Backport: Try to create /etc/pam.d and /usr/bin everytime when installing Signed-off-by: AnErrupTion --- build.zig | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/build.zig b/build.zig index 995536d..17b7943 100644 --- a/build.zig +++ b/build.zig @@ -174,11 +174,11 @@ fn install_ly(allocator: std.mem.Allocator, install_config: bool) !void { { const exe_path = try std.fs.path.join(allocator, &[_][]const u8{ dest_directory, "/usr/bin" }); - if (!std.mem.eql(u8, dest_directory, "")) { - std.fs.cwd().makePath(exe_path) catch { + std.fs.cwd().makePath(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(); @@ -221,11 +221,12 @@ fn install_ly(allocator: std.mem.Allocator, install_config: bool) !void { { const pam_path = try std.fs.path.join(allocator, &[_][]const u8{ dest_directory, "/etc/pam.d" }); - if (!std.mem.eql(u8, dest_directory, "")) { - std.fs.cwd().makePath(pam_path) catch { + + std.fs.cwd().makePath(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(); From 83984dc4933bbe0262fa5377eae6c61a9c825525 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Wed, 5 Mar 2025 22:54:34 +0100 Subject: [PATCH 140/530] Backport: Don't set XDG_CURRENT_DESKTOP and XDG_SESSION_DESKTOP if they're empty Signed-off-by: AnErrupTion --- src/auth.zig | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/auth.zig b/src/auth.zig index 1892c25..05ea61f 100644 --- a/src/auth.zig +++ b/src/auth.zig @@ -183,11 +183,11 @@ fn setXdgEnv(tty_str: [:0]u8, desktop_name: [:0]const u8, xdg_desktop_names: [:0 var uid_buffer: [10 + @sizeOf(u32) + 1]u8 = undefined; const uid_str = try std.fmt.bufPrintZ(&uid_buffer, "/run/user/{d}", .{uid}); - _ = interop.setenv("XDG_CURRENT_DESKTOP", xdg_desktop_names.ptr, 0); + if (!std.mem.eql(u8, xdg_desktop_names, "")) _ = interop.setenv("XDG_CURRENT_DESKTOP", xdg_desktop_names.ptr, 0); _ = interop.setenv("XDG_RUNTIME_DIR", uid_str.ptr, 0); _ = interop.setenv("XDG_SESSION_CLASS", "user", 0); _ = interop.setenv("XDG_SESSION_ID", "1", 0); - _ = interop.setenv("XDG_SESSION_DESKTOP", desktop_name.ptr, 0); + if (!std.mem.eql(u8, desktop_name, "")) _ = interop.setenv("XDG_SESSION_DESKTOP", desktop_name.ptr, 0); _ = interop.setenv("XDG_SEAT", "seat0", 0); _ = interop.setenv("XDG_VTNR", tty_str.ptr, 0); } From a766dc2b9c053fb53314778349b30233e5322e8e Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Thu, 6 Mar 2025 11:42:19 +0100 Subject: [PATCH 141/530] Update termbox2 Signed-off-by: AnErrupTion --- include/termbox2.h | 464 +++++++++++++++++++++++++-------------------- 1 file changed, 259 insertions(+), 205 deletions(-) diff --git a/include/termbox2.h b/include/termbox2.h index 5fc791f..406fbac 100644 --- a/include/termbox2.h +++ b/include/termbox2.h @@ -51,6 +51,8 @@ SOFTWARE. #include #include #include +#include +#include #ifdef PATH_MAX #define TB_PATH_MAX PATH_MAX @@ -64,7 +66,7 @@ extern "C" { // __ffi_start -#define TB_VERSION_STR "2.5.0-dev" +#define TB_VERSION_STR "2.5.0" /* The following compile-time options are supported: * @@ -90,7 +92,7 @@ extern "C" { */ #if defined(TB_LIB_OPTS) || 0 // __tb_lib_opts -// Ensure consistent compile-time options when using as a shared library +/* Ensure consistent compile-time options when using as a shared library */ #undef TB_OPT_ATTR_W #undef TB_OPT_EGC #undef TB_OPT_PRINTF_BUF @@ -99,7 +101,7 @@ extern "C" { #define TB_OPT_EGC #endif -// Ensure sane TB_OPT_ATTR_W (16, 32, or 64) +/* Ensure sane `TB_OPT_ATTR_W` (16, 32, or 64) */ #if defined TB_OPT_ATTR_W && TB_OPT_ATTR_W == 16 #elif defined TB_OPT_ATTR_W && TB_OPT_ATTR_W == 32 #elif defined TB_OPT_ATTR_W && TB_OPT_ATTR_W == 64 @@ -112,9 +114,9 @@ extern "C" { #endif #endif -/* ASCII key constants (tb_event.key) */ +/* ASCII key constants (`tb_event.key`) */ #define TB_KEY_CTRL_TILDE 0x00 -#define TB_KEY_CTRL_2 0x00 /* clash with 'CTRL_TILDE' */ +#define TB_KEY_CTRL_2 0x00 // clash with `CTRL_TILDE` #define TB_KEY_CTRL_A 0x01 #define TB_KEY_CTRL_B 0x02 #define TB_KEY_CTRL_C 0x03 @@ -123,14 +125,14 @@ extern "C" { #define TB_KEY_CTRL_F 0x06 #define TB_KEY_CTRL_G 0x07 #define TB_KEY_BACKSPACE 0x08 -#define TB_KEY_CTRL_H 0x08 /* clash with 'CTRL_BACKSPACE' */ +#define TB_KEY_CTRL_H 0x08 // clash with `CTRL_BACKSPACE` #define TB_KEY_TAB 0x09 -#define TB_KEY_CTRL_I 0x09 /* clash with 'TAB' */ +#define TB_KEY_CTRL_I 0x09 // clash with `TAB` #define TB_KEY_CTRL_J 0x0a #define TB_KEY_CTRL_K 0x0b #define TB_KEY_CTRL_L 0x0c #define TB_KEY_ENTER 0x0d -#define TB_KEY_CTRL_M 0x0d /* clash with 'ENTER' */ +#define TB_KEY_CTRL_M 0x0d // clash with `ENTER` #define TB_KEY_CTRL_N 0x0e #define TB_KEY_CTRL_O 0x0f #define TB_KEY_CTRL_P 0x10 @@ -145,24 +147,24 @@ extern "C" { #define TB_KEY_CTRL_Y 0x19 #define TB_KEY_CTRL_Z 0x1a #define TB_KEY_ESC 0x1b -#define TB_KEY_CTRL_LSQ_BRACKET 0x1b /* clash with 'ESC' */ -#define TB_KEY_CTRL_3 0x1b /* clash with 'ESC' */ +#define TB_KEY_CTRL_LSQ_BRACKET 0x1b // clash with 'ESC' +#define TB_KEY_CTRL_3 0x1b // clash with 'ESC' #define TB_KEY_CTRL_4 0x1c -#define TB_KEY_CTRL_BACKSLASH 0x1c /* clash with 'CTRL_4' */ +#define TB_KEY_CTRL_BACKSLASH 0x1c // clash with 'CTRL_4' #define TB_KEY_CTRL_5 0x1d -#define TB_KEY_CTRL_RSQ_BRACKET 0x1d /* clash with 'CTRL_5' */ +#define TB_KEY_CTRL_RSQ_BRACKET 0x1d // clash with 'CTRL_5' #define TB_KEY_CTRL_6 0x1e #define TB_KEY_CTRL_7 0x1f -#define TB_KEY_CTRL_SLASH 0x1f /* clash with 'CTRL_7' */ -#define TB_KEY_CTRL_UNDERSCORE 0x1f /* clash with 'CTRL_7' */ +#define TB_KEY_CTRL_SLASH 0x1f // clash with 'CTRL_7' +#define TB_KEY_CTRL_UNDERSCORE 0x1f // clash with 'CTRL_7' #define TB_KEY_SPACE 0x20 #define TB_KEY_BACKSPACE2 0x7f -#define TB_KEY_CTRL_8 0x7f /* clash with 'BACKSPACE2' */ +#define TB_KEY_CTRL_8 0x7f // clash with 'BACKSPACE2' #define tb_key_i(i) 0xffff - (i) -/* Terminal-dependent key constants (tb_event.key) and terminfo capabilities */ +/* Terminal-dependent key constants (`tb_event.key`) and terminfo caps */ /* BEGIN codegen h */ -/* Produced by ./codegen.sh on Thu, 13 Jul 2023 05:46:13 +0000 */ +/* Produced by ./codegen.sh on Tue, 03 Sep 2024 04:17:47 +0000 */ #define TB_KEY_F1 (0xffff - 0) #define TB_KEY_F2 (0xffff - 1) #define TB_KEY_F3 (0xffff - 2) @@ -242,7 +244,7 @@ extern "C" { #define TB_HARDCAP_UNDERLINE_2 "\x1b[21m" #define TB_HARDCAP_OVERLINE "\x1b[53m" -/* Colors (numeric) and attributes (bitwise) (tb_cell.fg, tb_cell.bg) */ +/* Colors (numeric) and attributes (bitwise) (`tb_cell.fg`, `tb_cell.bg`) */ #define TB_DEFAULT 0x0000 #define TB_BLACK 0x0001 #define TB_RED 0x0002 @@ -262,8 +264,9 @@ extern "C" { #define TB_HI_BLACK 0x2000 #define TB_BRIGHT 0x4000 #define TB_DIM 0x8000 -#define TB_256_BLACK TB_HI_BLACK // TB_256_BLACK is deprecated -#else // 32 or 64 +#define TB_256_BLACK TB_HI_BLACK // `TB_256_BLACK` is deprecated +#else +// `TB_OPT_ATTR_W` is 32 or 64 #define TB_BOLD 0x01000000 #define TB_UNDERLINE 0x02000000 #define TB_REVERSE 0x04000000 @@ -272,7 +275,7 @@ extern "C" { #define TB_HI_BLACK 0x20000000 #define TB_BRIGHT 0x40000000 #define TB_DIM 0x80000000 -#define TB_TRUECOLOR_BOLD TB_BOLD // TB_TRUECOLOR_* is deprecated +#define TB_TRUECOLOR_BOLD TB_BOLD // `TB_TRUECOLOR_*` is deprecated #define TB_TRUECOLOR_UNDERLINE TB_UNDERLINE #define TB_TRUECOLOR_REVERSE TB_REVERSE #define TB_TRUECOLOR_ITALIC TB_ITALIC @@ -287,24 +290,24 @@ extern "C" { #define TB_INVISIBLE 0x0000000800000000 #endif -/* Event types (tb_event.type) */ +/* Event types (`tb_event.type`) */ #define TB_EVENT_KEY 1 #define TB_EVENT_RESIZE 2 #define TB_EVENT_MOUSE 3 -/* Key modifiers (bitwise) (tb_event.mod) */ +/* Key modifiers (bitwise) (`tb_event.mod`) */ #define TB_MOD_ALT 1 #define TB_MOD_CTRL 2 #define TB_MOD_SHIFT 4 #define TB_MOD_MOTION 8 -/* Input modes (bitwise) (tb_set_input_mode) */ +/* Input modes (bitwise) (`tb_set_input_mode`) */ #define TB_INPUT_CURRENT 0 #define TB_INPUT_ESC 1 #define TB_INPUT_ALT 2 #define TB_INPUT_MOUSE 4 -/* Output modes (tb_set_output_mode) */ +/* Output modes (`tb_set_output_mode`) */ #define TB_OUTPUT_CURRENT 0 #define TB_OUTPUT_NORMAL 1 #define TB_OUTPUT_256 2 @@ -316,9 +319,9 @@ extern "C" { /* Common function return values unless otherwise noted. * - * Library behavior is undefined after receiving TB_ERR_MEM. Callers may - * attempt reinitializing by freeing memory, invoking tb_shutdown, then - * tb_init. + * Library behavior is undefined after receiving `TB_ERR_MEM`. Callers may + * attempt reinitializing by freeing memory, invoking `tb_shutdown`, then + * `tb_init`. */ #define TB_OK 0 #define TB_ERR -1 @@ -347,12 +350,12 @@ extern "C" { #define TB_ERR_SELECT TB_ERR_POLL #define TB_ERR_RESIZE_SELECT TB_ERR_RESIZE_POLL -/* Deprecated. Function types to be used with tb_set_func(). */ +/* Deprecated. Function types to be used with `tb_set_func`. */ #define TB_FUNC_EXTRACT_PRE 0 #define TB_FUNC_EXTRACT_POST 1 -/* Define this to set the size of the buffer used in tb_printf() - * and tb_sendf() +/* Define this to set the size of the buffer used in `tb_printf` + * and `tb_sendf` */ #ifndef TB_OPT_PRINTF_BUF #define TB_OPT_PRINTF_BUF 4096 @@ -389,35 +392,39 @@ typedef uint32_t uintattr_t; typedef uint16_t uintattr_t; #endif -/* The terminal screen is represented as 2d array of cells. The structure is - * optimized for dealing with single-width (wcwidth()==1) Unicode codepoints, +/* A cell in a 2d grid representing the terminal screen. + * + * The terminal screen is represented as 2d array of cells. The structure is + * optimized for dealing with single-width (`wcwidth==1`) Unicode codepoints, * however some support for grapheme clusters (e.g., combining diacritical - * marks) and wide codepoints (e.g., Hiragana) is provided through ech, nech, - * cech via tb_set_cell_ex(). ech is only valid when nech>0, otherwise ch is - * used. + * marks) and wide codepoints (e.g., Hiragana) is provided through `ech`, + * `nech`, and `cech` via `tb_set_cell_ex`. `ech` is only valid when `nech>0`, + * otherwise `ch` is used. * - * For non-single-width codepoints, given N=wcwidth(ch)/wcswidth(ech): + * For non-single-width codepoints, given `N=wcwidth(ch)/wcswidth(ech)`: * - * when N==0: termbox forces a single-width cell. Callers should avoid this - * if aiming to render text accurately. + * when `N==0`: termbox forces a single-width cell. Callers should avoid this + * if aiming to render text accurately. Callers may use + * `tb_set_cell_ex` or `tb_print*` to render `N==0` combining + * characters. * - * when N>1: termbox zeroes out the following N-1 cells and skips sending - * them to the tty. So, e.g., if the caller sets x=0,y=0 to an N==2 - * codepoint, the caller's next set should be at x=2,y=0. Anything - * set at x=1,y=0 will be ignored. If there are not enough columns - * remaining on the line to render N width, spaces are sent - * instead. + * when `N>1`: termbox zeroes out the following `N-1` cells and skips sending + * them to the tty. So, e.g., if the caller sets `x=0,y=0` to an + * `N==2` codepoint, the caller's next set should be at `x=2,y=0`. + * Anything set at `x=1,y=0` will be ignored. If there are not + * enough columns remaining on the line to render `N` width, spaces + * are sent instead. * - * See tb_present() for implementation. + * See `tb_present` for implementation. */ struct tb_cell { - uint32_t ch; /* a Unicode codepoint */ - uintattr_t fg; /* bitwise foreground attributes */ - uintattr_t bg; /* bitwise background attributes */ + uint32_t ch; // a Unicode codepoint + uintattr_t fg; // bitwise foreground attributes + uintattr_t bg; // bitwise background attributes #ifdef TB_OPT_EGC - uint32_t *ech; /* a grapheme cluster of Unicode codepoints, 0-terminated */ - size_t nech; /* num elements in ech, 0 means use ch instead of ech */ - size_t cech; /* num elements allocated for ech */ + uint32_t *ech; // a grapheme cluster of Unicode codepoints, 0-terminated + size_t nech; // num elements in ech, 0 means use ch instead of ech + size_t cech; // num elements allocated for ech #endif }; @@ -425,30 +432,29 @@ struct tb_cell { * * Given the event type, the following fields are relevant: * - * when TB_EVENT_KEY: (key XOR ch, one will be zero), mod. Note there is - * overlap between TB_MOD_CTRL and TB_KEY_CTRL_*. - * TB_MOD_CTRL and TB_MOD_SHIFT are only set as - * modifiers to TB_KEY_ARROW_*. + * when `TB_EVENT_KEY`: `key` xor `ch` (one will be zero) and `mod`. Note + * there is overlap between `TB_MOD_CTRL` and + * `TB_KEY_CTRL_*`. `TB_MOD_CTRL` and `TB_MOD_SHIFT` are + * only set as modifiers to `TB_KEY_ARROW_*`. * - * when TB_EVENT_RESIZE: w, h + * when `TB_EVENT_RESIZE`: `w` and `h` * - * when TB_EVENT_MOUSE: key (TB_KEY_MOUSE_*), x, y + * when `TB_EVENT_MOUSE`: `key` (`TB_KEY_MOUSE_*`), `x`, and `y` */ struct tb_event { - uint8_t type; /* one of TB_EVENT_* constants */ - uint8_t mod; /* bitwise TB_MOD_* constants */ - uint16_t key; /* one of TB_KEY_* constants */ - uint32_t ch; /* a Unicode codepoint */ - int32_t w; /* resize width */ - int32_t h; /* resize height */ - int32_t x; /* mouse x */ - int32_t y; /* mouse y */ + uint8_t type; // one of `TB_EVENT_*` constants + uint8_t mod; // bitwise `TB_MOD_*` constants + uint16_t key; // one of `TB_KEY_*` constants + uint32_t ch; // a Unicode codepoint + int32_t w; // resize width + int32_t h; // resize height + int32_t x; // mouse x + int32_t y; // mouse y }; -/* Initializes the termbox library. This function should be called before any - * other functions. tb_init() is equivalent to tb_init_file("/dev/tty"). After - * successful initialization, the library must be finalized using the - * tb_shutdown() function. +/* Initialize the termbox library. This function should be called before any + * other functions. `tb_init` is equivalent to `tb_init_file("/dev/tty")`. After + * successful initialization, the library must be finalized using `tb_shutdown`. */ int tb_init(void); int tb_init_file(const char *path); @@ -456,184 +462,197 @@ int tb_init_fd(int ttyfd); int tb_init_rwfd(int rfd, int wfd); int tb_shutdown(void); -/* Returns the size of the internal back buffer (which is the same as terminal's +/* Return the size of the internal back buffer (which is the same as terminal's * window size in rows and columns). The internal buffer can be resized after - * tb_clear() or tb_present() function calls. Both dimensions have an - * unspecified negative value when called before tb_init() or after - * tb_shutdown(). + * `tb_clear` or `tb_present` calls. Both dimensions have an unspecified + * negative value when called before `tb_init` or after `tb_shutdown`. */ int tb_width(void); int tb_height(void); -/* Clears the internal back buffer using TB_DEFAULT color or the - * color/attributes set by tb_set_clear_attrs() function. +/* Clear the internal back buffer using `TB_DEFAULT` or the attributes set by + * `tb_set_clear_attrs`. */ int tb_clear(void); int tb_set_clear_attrs(uintattr_t fg, uintattr_t bg); -/* Synchronizes the internal back buffer with the terminal by writing to tty. */ +/* Synchronize the internal back buffer with the terminal by writing to tty. */ int tb_present(void); -/* Clears the internal front buffer effectively forcing a complete re-render of +/* Clear the internal front buffer effectively forcing a complete re-render of * the back buffer to the tty. It is not necessary to call this under normal * circumstances. */ int tb_invalidate(void); -/* Sets the position of the cursor. Upper-left character is (0, 0). */ +/* Set the position of the cursor. Upper-left cell is (0, 0). */ int tb_set_cursor(int cx, int cy); int tb_hide_cursor(void); /* Set cell contents in the internal back buffer at the specified position. * - * Use tb_set_cell_ex() for rendering grapheme clusters (e.g., combining + * Use `tb_set_cell_ex` for rendering grapheme clusters (e.g., combining * diacritical marks). * - * Function tb_set_cell(x, y, ch, fg, bg) is equivalent to - * tb_set_cell_ex(x, y, &ch, 1, fg, bg). + * Calling `tb_set_cell(x, y, ch, fg, bg)` is equivalent to + * `tb_set_cell_ex(x, y, &ch, 1, fg, bg)`. * - * Function tb_extend_cell() is a shortcut for appending 1 codepoint to - * cell->ech. + * `tb_extend_cell` is a shortcut for appending 1 codepoint to `tb_cell.ech`. + * + * Non-printable (`iswprint(3)`) codepoints are replaced with `U+FFFD` at render + * time. */ int tb_set_cell(int x, int y, uint32_t ch, uintattr_t fg, uintattr_t bg); int tb_set_cell_ex(int x, int y, uint32_t *ch, size_t nch, uintattr_t fg, uintattr_t bg); int tb_extend_cell(int x, int y, uint32_t ch); -/* Sets the input mode. Termbox has two input modes: +/* Set the input mode. Termbox has two input modes: * - * 1. TB_INPUT_ESC - * When escape (\x1b) is in the buffer and there's no match for an escape - * sequence, a key event for TB_KEY_ESC is returned. + * 1. `TB_INPUT_ESC` + * When escape (`\x1b`) is in the buffer and there's no match for an escape + * sequence, a key event for `TB_KEY_ESC` is returned. * - * 2. TB_INPUT_ALT - * When escape (\x1b) is in the buffer and there's no match for an escape - * sequence, the next keyboard event is returned with a TB_MOD_ALT modifier. + * 2. `TB_INPUT_ALT` + * When escape (`\x1b`) is in the buffer and there's no match for an escape + * sequence, the next keyboard event is returned with a `TB_MOD_ALT` + * modifier. * - * You can also apply TB_INPUT_MOUSE via bitwise OR operation to either of the - * modes (e.g., TB_INPUT_ESC | TB_INPUT_MOUSE) to receive TB_EVENT_MOUSE events. - * If none of the main two modes were set, but the mouse mode was, TB_INPUT_ESC - * mode is used. If for some reason you've decided to use - * (TB_INPUT_ESC | TB_INPUT_ALT) combination, it will behave as if only - * TB_INPUT_ESC was selected. + * You can also apply `TB_INPUT_MOUSE` via bitwise OR operation to either of the + * modes (e.g., `TB_INPUT_ESC | TB_INPUT_MOUSE`) to receive `TB_EVENT_MOUSE` + * events. If none of the main two modes were set, but the mouse mode was, + * `TB_INPUT_ESC` is used. If for some reason you've decided to use + * `TB_INPUT_ESC | TB_INPUT_ALT`, it will behave as if only `TB_INPUT_ESC` was + * selected. * - * If mode is TB_INPUT_CURRENT, the function returns the current input mode. + * If mode is `TB_INPUT_CURRENT`, return the current input mode. * - * The default input mode is TB_INPUT_ESC. + * The default input mode is `TB_INPUT_ESC`. */ int tb_set_input_mode(int mode); -/* Sets the termbox output mode. Termbox has multiple output modes: +/* Set the output mode. Termbox has multiple output modes: * - * 1. TB_OUTPUT_NORMAL => [0..8] + * 1. `TB_OUTPUT_NORMAL` => [0..8] * * This mode provides 8 different colors: - * TB_BLACK, TB_RED, TB_GREEN, TB_YELLOW, - * TB_BLUE, TB_MAGENTA, TB_CYAN, TB_WHITE + * `TB_BLACK`, `TB_RED`, `TB_GREEN`, `TB_YELLOW`, + * `TB_BLUE`, `TB_MAGENTA`, `TB_CYAN`, `TB_WHITE` * - * Plus TB_DEFAULT which skips sending a color code (i.e., uses the + * Plus `TB_DEFAULT` which skips sending a color code (i.e., uses the * terminal's default color). * - * Colors (including TB_DEFAULT) may be bitwise OR'd with attributes: - * TB_BOLD, TB_UNDERLINE, TB_REVERSE, TB_ITALIC, TB_BLINK, TB_BRIGHT, - * TB_DIM + * Colors (including `TB_DEFAULT`) may be bitwise OR'd with attributes: + * `TB_BOLD`, `TB_UNDERLINE`, `TB_REVERSE`, `TB_ITALIC`, `TB_BLINK`, + * `TB_BRIGHT`, `TB_DIM` * * The following style attributes are also available if compiled with - * TB_OPT_ATTR_W set to 64: - * TB_STRIKEOUT, TB_UNDERLINE_2, TB_OVERLINE, TB_INVISIBLE + * `TB_OPT_ATTR_W` set to 64: + * `TB_STRIKEOUT`, `TB_UNDERLINE_2`, `TB_OVERLINE`, `TB_INVISIBLE` * - * As in all modes, the value 0 is interpreted as TB_DEFAULT for + * As in all modes, the value 0 is interpreted as `TB_DEFAULT` for * convenience. * - * Some notes: TB_REVERSE can be applied as either fg or bg attributes for - * the same effect. TB_BRIGHT can be applied to either fg or bg. The rest of - * the attributes apply to fg only and are ignored as bg attributes. + * Some notes: `TB_REVERSE` and `TB_BRIGHT` can be applied as either `fg` or + * `bg` attributes for the same effect. The rest of the attributes apply to + * `fg` only and are ignored as `bg` attributes. * - * Example usage: - * tb_set_cell(x, y, '@', TB_BLACK | TB_BOLD, TB_RED); + * Example usage: `tb_set_cell(x, y, '@', TB_BLACK | TB_BOLD, TB_RED)` * - * 2. TB_OUTPUT_256 => [0..255] + TB_HI_BLACK + * 2. `TB_OUTPUT_256` => [0..255] + `TB_HI_BLACK` * * In this mode you get 256 distinct colors (plus default): - * 0x00 (1): TB_DEFAULT - * TB_HI_BLACK (1): TB_BLACK in TB_OUTPUT_NORMAL - * 0x01..0x07 (7): the next 7 colors as in TB_OUTPUT_NORMAL + * 0x00 (1): `TB_DEFAULT` + * `TB_HI_BLACK` (1): `TB_BLACK` in `TB_OUTPUT_NORMAL` + * 0x01..0x07 (7): the next 7 colors as in `TB_OUTPUT_NORMAL` * 0x08..0x0f (8): bright versions of the above * 0x10..0xe7 (216): 216 different colors * 0xe8..0xff (24): 24 different shades of gray * - * All TB_* style attributes except TB_BRIGHT may be bitwise OR'd as in - * TB_OUTPUT_NORMAL. + * All `TB_*` style attributes except `TB_BRIGHT` may be bitwise OR'd as in + * `TB_OUTPUT_NORMAL`. * - * Note TB_HI_BLACK must be used for black, as 0x00 represents default. + * Note `TB_HI_BLACK` must be used for black, as 0x00 represents default. * - * 3. TB_OUTPUT_216 => [0..216] + * 3. `TB_OUTPUT_216` => [0..216] * - * This mode supports the 216-color range of TB_OUTPUT_256 only, but you + * This mode supports the 216-color range of `TB_OUTPUT_256` only, but you * don't need to provide an offset: - * 0x00 (1): TB_DEFAULT + * 0x00 (1): `TB_DEFAULT` * 0x01..0xd8 (216): 216 different colors * - * 4. TB_OUTPUT_GRAYSCALE => [0..24] + * 4. `TB_OUTPUT_GRAYSCALE` => [0..24] * - * This mode supports the 24-color range of TB_OUTPUT_256 only, but you + * This mode supports the 24-color range of `TB_OUTPUT_256` only, but you * don't need to provide an offset: - * 0x00 (1): TB_DEFAULT + * 0x00 (1): `TB_DEFAULT` * 0x01..0x18 (24): 24 different shades of gray * - * 5. TB_OUTPUT_TRUECOLOR => [0x000000..0xffffff] + TB_HI_BLACK + * 5. `TB_OUTPUT_TRUECOLOR` => [0x000000..0xffffff] + `TB_HI_BLACK` * * This mode provides 24-bit color on supported terminals. The format is * 0xRRGGBB. * - * All TB_* style attributes except TB_BRIGHT may be bitwise OR'd as in - * TB_OUTPUT_NORMAL. + * All `TB_*` style attributes except `TB_BRIGHT` may be bitwise OR'd as in + * `TB_OUTPUT_NORMAL`. * - * Note TB_HI_BLACK must be used for black, as 0x000000 represents default. - * - * If mode is TB_OUTPUT_CURRENT, the function returns the current output mode. - * - * The default output mode is TB_OUTPUT_NORMAL. + * Note `TB_HI_BLACK` must be used for black, as 0x000000 represents default. * * To use the terminal default color (i.e., to not send an escape code), pass - * TB_DEFAULT. For convenience, the value 0 is interpreted as TB_DEFAULT in + * `TB_DEFAULT`. For convenience, the value 0 is interpreted as `TB_DEFAULT` in * all modes. * * Note, cell attributes persist after switching output modes. Any translation - * between, for example, TB_OUTPUT_NORMAL's TB_RED and TB_OUTPUT_TRUECOLOR's - * 0xff0000 must be performed by the caller. Also note that cells previously - * rendered in one mode may persist unchanged until the front buffer is cleared - * (such as after a resize event) at which point it will be re-interpreted and - * flushed according to the current mode. Callers may invoke tb_invalidate if - * it is desirable to immediately re-interpret and flush the entire screen - * according to the current mode. + * between, for example, `TB_OUTPUT_NORMAL`'s `TB_RED` and + * `TB_OUTPUT_TRUECOLOR`'s 0xff0000 must be performed by the caller. Also note + * that cells previously rendered in one mode may persist unchanged until the + * front buffer is cleared (such as after a resize event) at which point it will + * be re-interpreted and flushed according to the current mode. Callers may + * invoke `tb_invalidate` if it is desirable to immediately re-interpret and + * flush the entire screen according to the current mode. * * Note, not all terminals support all output modes, especially beyond - * TB_OUTPUT_NORMAL. There is also no very reliable way to determine color + * `TB_OUTPUT_NORMAL`. There is also no very reliable way to determine color * support dynamically. If portability is desired, callers are recommended to - * use TB_OUTPUT_NORMAL or make output mode end-user configurable. The same + * use `TB_OUTPUT_NORMAL` or make output mode end-user configurable. The same * advice applies to style attributes. + * + * If mode is `TB_OUTPUT_CURRENT`, return the current output mode. + * + * The default output mode is `TB_OUTPUT_NORMAL`. */ int tb_set_output_mode(int mode); -/* Wait for an event up to timeout_ms milliseconds and fill the event structure - * with it. If no event is available within the timeout period, TB_ERR_NO_EVENT - * is returned. On a resize event, the underlying select(2) call may be - * interrupted, yielding a return code of TB_ERR_POLL. In this case, you may - * check errno via tb_last_errno(). If it's EINTR, you can safely ignore that - * and call tb_peek_event() again. +/* Wait for an event up to `timeout_ms` milliseconds and populate `event` with + * it. If no event is available within the timeout period, `TB_ERR_NO_EVENT` + * is returned. On a resize event, the underlying `select(2)` call may be + * interrupted, yielding a return code of `TB_ERR_POLL`. In this case, you may + * check `errno` via `tb_last_errno`. If it's `EINTR`, you may elect to ignore + * that and call `tb_peek_event` again. */ int tb_peek_event(struct tb_event *event, int timeout_ms); -/* Same as tb_peek_event except no timeout. */ +/* Same as `tb_peek_event` except no timeout. */ int tb_poll_event(struct tb_event *event); -/* Internal termbox FDs that can be used with poll() / select(). Must call - * tb_poll_event() / tb_peek_event() if activity is detected. */ +/* Internal termbox fds that can be used with `poll(2)`, `select(2)`, etc. + * externally. Callers must invoke `tb_poll_event` or `tb_peek_event` if + * fds become readable. */ int tb_get_fds(int *ttyfd, int *resizefd); -/* Print and printf functions. Specify param out_w to determine width of printed - * string. Incomplete trailing UTF-8 byte sequences are replaced with U+FFFD. - * For finer control, use tb_set_cell(). +/* Print and printf functions. Specify param `out_w` to determine width of + * printed string. Strings are interpreted as UTF-8. + * + * Non-printable characters (`iswprint(3)`) and truncated UTF-8 byte sequences + * are replaced with U+FFFD. + * + * Newlines (`\n`) are supported with the caveat that `out_w` will return the + * width of the string as if it were on a single line. + * + * If the starting coordinate is out of bounds, `TB_ERR_OUT_OF_BOUNDS` is + * returned. If the starting coordinate is in bounds, but goes out of bounds, + * then the out-of-bounds portions of the string are ignored. + * + * For finer control, use `tb_set_cell`. */ int tb_print(int x, int y, uintattr_t fg, uintattr_t bg, const char *str); int tb_printf(int x, int y, uintattr_t fg, uintattr_t bg, const char *fmt, ...); @@ -646,14 +665,14 @@ int tb_printf_ex(int x, int y, uintattr_t fg, uintattr_t bg, size_t *out_w, int tb_send(const char *buf, size_t nbuf); int tb_sendf(const char *fmt, ...); -/* Deprecated. Set custom functions. fn_type is one of TB_FUNC_* constants, fn - * is a compatible function pointer, or NULL to clear. +/* Deprecated. Set custom callbacks. `fn_type` is one of `TB_FUNC_*` constants, + * `fn` is a compatible function pointer, or NULL to clear. * - * TB_FUNC_EXTRACT_PRE: + * `TB_FUNC_EXTRACT_PRE`: * If specified, invoke this function BEFORE termbox tries to extract any * escape sequences from the input buffer. * - * TB_FUNC_EXTRACT_POST: + * `TB_FUNC_EXTRACT_POST`: * If specified, invoke this function AFTER termbox tries (and fails) to * extract any escape sequences from the input buffer. */ @@ -711,7 +730,7 @@ const char *tb_version(void); } #endif -#endif /* TERMBOX_H_INCL */ +#endif // TERMBOX_H_INCL #ifdef TB_IMPL @@ -799,7 +818,7 @@ struct tb_global_t { static struct tb_global_t global = {0}; /* BEGIN codegen c */ -/* Produced by ./codegen.sh on Thu, 13 Jul 2023 05:46:13 +0000 */ +/* Produced by ./codegen.sh on Tue, 03 Sep 2024 04:17:48 +0000 */ static const int16_t terminfo_cap_indexes[] = { 66, // kf1 (TB_CAP_F1) @@ -1546,6 +1565,7 @@ static int cellbuf_init(struct cellbuf_t *c, int w, int h); static int cellbuf_free(struct cellbuf_t *c); static int cellbuf_clear(struct cellbuf_t *c); static int cellbuf_get(struct cellbuf_t *c, int x, int y, struct tb_cell **out); +static int cellbuf_in_bounds(struct cellbuf_t *c, int x, int y); static int cellbuf_resize(struct cellbuf_t *c, int w, int h); static int bytebuf_puts(struct bytebuf_t *b, const char *str); static int bytebuf_nputs(struct bytebuf_t *b, const char *str, size_t nstr); @@ -1555,13 +1575,12 @@ static int bytebuf_reserve(struct bytebuf_t *b, size_t sz); static int bytebuf_free(struct bytebuf_t *b); int tb_init(void) { + setlocale(LC_CTYPE, "C.UTF-8"); // Required for iswprint(3) to work properly return tb_init_file("/dev/tty"); } int tb_init_file(const char *path) { - if (global.initialized) { - return TB_ERR_INIT_ALREADY; - } + if (global.initialized) return TB_ERR_INIT_ALREADY; int ttyfd = open(path, O_RDWR); if (ttyfd < 0) { global.last_errno = errno; @@ -1635,7 +1654,7 @@ int tb_present(void) { int rv; - // TODO Assert global.back.(width,height) == global.front.(width,height) + // TODO: Assert global.back.(width,height) == global.front.(width,height) global.last_x = -1; global.last_y = -1; @@ -1654,12 +1673,10 @@ int tb_present(void) { w = wcswidth((wchar_t *)back->ech, back->nech); else #endif - /* wcwidth() simply returns -1 on overflow of wchar_t */ + // wcwidth simply returns -1 on overflow of wchar_t w = wcwidth((wchar_t)back->ch); } - if (w < 1) { - w = 1; - } + if (w < 1) w = 1; if (cell_cmp(back, front) != 0) { cell_copy(front, back); @@ -1763,11 +1780,11 @@ int tb_extend_cell(int x, int y, uint32_t ch) { if_err_return(rv, cellbuf_get(&global.back, x, y, &cell)); if (cell->nech > 0) { // append to ech nech = cell->nech + 1; - if_err_return(rv, cell_reserve_ech(cell, nech)); + if_err_return(rv, cell_reserve_ech(cell, nech + 1)); cell->ech[nech - 1] = ch; } else { // make new ech nech = 2; - if_err_return(rv, cell_reserve_ech(cell, nech)); + if_err_return(rv, cell_reserve_ech(cell, nech + 1)); cell->ech[0] = cell->ch; cell->ech[1] = ch; } @@ -1854,14 +1871,22 @@ int tb_print(int x, int y, uintattr_t fg, uintattr_t bg, const char *str) { int tb_print_ex(int x, int y, uintattr_t fg, uintattr_t bg, size_t *out_w, const char *str) { - int rv; + int rv, w, ix, x_prev; uint32_t uni; - int w, ix = x; - if (out_w) { - *out_w = 0; + + if_not_init_return(); + + if (!cellbuf_in_bounds(&global.back, x, y)) { + return TB_ERR_OUT_OF_BOUNDS; } + + ix = x; + x_prev = x; + if (out_w) *out_w = 0; + while (*str) { rv = tb_utf8_char_to_unicode(&uni, str); + if (rv < 0) { uni = 0xfffd; // replace invalid UTF-8 char with U+FFFD str += rv * -1; @@ -1870,18 +1895,33 @@ int tb_print_ex(int x, int y, uintattr_t fg, uintattr_t bg, size_t *out_w, } else { break; // shouldn't get here } - w = wcwidth((wchar_t)uni); - if (w < 0) w = 1; - if (w == 0 && x > ix) { - if_err_return(rv, tb_extend_cell(x - 1, y, uni)); - } else { - if_err_return(rv, tb_set_cell(x, y, uni, fg, bg)); + + if (uni == '\n') { // TODO: \r, \t, \v, \f, etc? + x = ix; + x_prev = x; + y += 1; + continue; + } else if (!iswprint((wint_t)uni)) { + uni = 0xfffd; // replace non-printable with U+FFFD } - x += w; - if (out_w) { - *out_w += w; + + w = wcwidth((wchar_t)uni); + if (w < 0) { + return TB_ERR; // shouldn't happen if iswprint + } else if (w == 0) { // combining character + if (cellbuf_in_bounds(&global.back, x_prev, y)) { + if_err_return(rv, tb_extend_cell(x_prev, y, uni)); + } + } else { + if (cellbuf_in_bounds(&global.back, x, y)) { + if_err_return(rv, tb_set_cell(x, y, uni, fg, bg)); + } + x_prev = x; + x += w; + if (out_w) *out_w += w; } } + return TB_OK; } @@ -2144,7 +2184,7 @@ static int init_cap_trie(void) { // example, att605-pc collides on TB_CAP_F4 and TB_CAP_DELETE.) First cap // in TB_CAP_* index order will win. // - // TODO Reorder TB_CAP_* so more critical caps come first. + // TODO: Reorder TB_CAP_* so more critical caps come first. for (i = 0; i < TB_CAP__COUNT_KEYS; i++) { rv = cap_trie_add(global.caps[i], tb_key_i(i), 0); if (rv != TB_OK && rv != TB_ERR_CAP_COLLISION) return rv; @@ -2184,8 +2224,8 @@ static int cap_trie_add(const char *cap, uint16_t key, uint8_t mod) { if (!next) { // We need to add a new child to node node->nchildren += 1; - node->children = - tb_realloc(node->children, sizeof(*node) * node->nchildren); + node->children = (struct cap_trie_t *)tb_realloc(node->children, + sizeof(*node) * node->nchildren); if (!node->children) { return TB_ERR_MEM; } @@ -2325,7 +2365,7 @@ static int update_term_size_via_esc(void) { #define TB_RESIZE_FALLBACK_MS 1000 #endif - char *move_and_report = "\x1b[9999;9999H\x1b[6n"; + char move_and_report[] = "\x1b[9999;9999H\x1b[6n"; ssize_t write_rv = write(global.wfd, move_and_report, strlen(move_and_report)); if (write_rv != (ssize_t)strlen(move_and_report)) { @@ -2394,7 +2434,10 @@ static int tb_deinit(void) { } } - sigaction(SIGWINCH, &(struct sigaction){.sa_handler = SIG_DFL}, NULL); + struct sigaction sa; + memset(&sa, 0, sizeof(sa)); + sa.sa_handler = SIG_DFL; + sigaction(SIGWINCH, &sa, NULL); if (global.resize_pipefd[0] >= 0) close(global.resize_pipefd[0]); if (global.resize_pipefd[1] >= 0) close(global.resize_pipefd[1]); @@ -2501,7 +2544,7 @@ static int read_terminfo_path(const char *path) { } size_t fsize = st.st_size; - char *data = tb_malloc(fsize); + char *data = (char *)tb_malloc(fsize); if (!data) { fclose(fp); return TB_ERR; @@ -2685,7 +2728,7 @@ static int wait_event(struct tb_event *event, int timeout) { if (resize_has_events) { int ignore = 0; read(global.resize_pipefd[0], &ignore, sizeof(ignore)); - // TODO Harden against errors encountered mid-resize + // TODO: Harden against errors encountered mid-resize if_err_return(rv, update_term_size()); if_err_return(rv, resize_cellbufs()); event->type = TB_EVENT_RESIZE; @@ -2812,7 +2855,7 @@ static int extract_esc_cap(struct tb_event *event) { static int extract_esc_mouse(struct tb_event *event) { struct bytebuf_t *in = &global.in; - enum type { TYPE_VT200 = 0, TYPE_1006, TYPE_1015, TYPE_MAX }; + enum { TYPE_VT200 = 0, TYPE_1006, TYPE_1015, TYPE_MAX }; const char *cmp[TYPE_MAX] = {// // X10 mouse encoding, the simplest one @@ -2824,7 +2867,7 @@ static int extract_esc_mouse(struct tb_event *event) { // urxvt: \x1b [ Cb ; Cx ; Cy M [TYPE_1015] = "\x1b["}; - enum type type = 0; + int type = 0; int ret = TB_ERR; // Unrolled at compile-time (probably) @@ -3224,13 +3267,18 @@ static int send_cluster(int x, int y, uint32_t *ch, size_t nch) { int i; for (i = 0; i < (int)nch; i++) { uint32_t ch32 = *(ch + i); - int chu8_len; + if (!iswprint((wint_t)ch32)) { + ch32 = 0xfffd; // replace non-printable codepoints with U+FFFD + } + int chu8_len = tb_utf8_unicode_to_char(chu8, ch32); + /* if (ch32 == 0) { // replace null with space (from termbox 19dbee5) chu8_len = 1; chu8[0] = ' '; } else { chu8_len = tb_utf8_unicode_to_char(chu8, ch32); } + */ if_err_return(rv, bytebuf_nputs(&global.out, chu8, (size_t)chu8_len)); } @@ -3241,7 +3289,6 @@ static int convert_num(uint32_t num, char *buf) { int i, l = 0; char ch; do { - /* '0' = 48; 48 + num%10 < 58 < MAX_8bitCHAR */ buf[l++] = (char)('0' + (num % 10)); num /= 10; } while (num); @@ -3287,7 +3334,7 @@ static int cell_set(struct tb_cell *cell, uint32_t *ch, size_t nch, } else { int rv; if_err_return(rv, cell_reserve_ech(cell, nch + 1)); - memcpy(cell->ech, ch, sizeof(ch) * nch); + memcpy(cell->ech, ch, sizeof(*ch) * nch); cell->ech[nch] = '\0'; cell->nech = nch; } @@ -3303,7 +3350,7 @@ static int cell_reserve_ech(struct tb_cell *cell, size_t n) { if (cell->cech >= n) { return TB_OK; } - if (!(cell->ech = tb_realloc(cell->ech, n * sizeof(cell->ch)))) { + if (!(cell->ech = (uint32_t*)tb_realloc(cell->ech, n * sizeof(cell->ch)))) { return TB_ERR_MEM; } cell->cech = n; @@ -3326,7 +3373,7 @@ static int cell_free(struct tb_cell *cell) { } static int cellbuf_init(struct cellbuf_t *c, int w, int h) { - c->cells = tb_malloc(sizeof(struct tb_cell) * w * h); + c->cells = (struct tb_cell *)tb_malloc(sizeof(struct tb_cell) * w * h); if (!c->cells) { return TB_ERR_MEM; } @@ -3360,7 +3407,7 @@ static int cellbuf_clear(struct cellbuf_t *c) { static int cellbuf_get(struct cellbuf_t *c, int x, int y, struct tb_cell **out) { - if (x < 0 || x >= c->width || y < 0 || y >= c->height) { + if (!cellbuf_in_bounds(c, x, y)) { *out = NULL; return TB_ERR_OUT_OF_BOUNDS; } @@ -3368,6 +3415,13 @@ static int cellbuf_get(struct cellbuf_t *c, int x, int y, return TB_OK; } +static int cellbuf_in_bounds(struct cellbuf_t *c, int x, int y) { + if (x < 0 || x >= c->width || y < 0 || y >= c->height) { + return 0; + } + return 1; +} + static int cellbuf_resize(struct cellbuf_t *c, int w, int h) { int rv; @@ -3452,9 +3506,9 @@ static int bytebuf_reserve(struct bytebuf_t *b, size_t sz) { } char *newbuf; if (b->buf) { - newbuf = tb_realloc(b->buf, newcap); + newbuf = (char *)tb_realloc(b->buf, newcap); } else { - newbuf = tb_malloc(newcap); + newbuf = (char *)tb_malloc(newcap); } if (!newbuf) { return TB_ERR_MEM; @@ -3472,4 +3526,4 @@ static int bytebuf_free(struct bytebuf_t *b) { return TB_OK; } -#endif /* TB_IMPL */ +#endif // TB_IMPL From d0ccaa4d69044ddabbaf2202347cb26b6dd4b918 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Thu, 6 Mar 2025 12:47:28 +0100 Subject: [PATCH 142/530] Enable true color output (closes #705) Signed-off-by: AnErrupTion --- build.zig | 1 + res/config.ini | 66 ++++++++++-------------- src/animations/ColorMix.zig | 2 +- src/animations/Matrix.zig | 4 +- src/bigclock.zig | 22 ++++---- src/config/Config.zig | 18 +++---- src/config/migrator.zig | 89 ++++++++++++++++++++++++++------- src/main.zig | 4 +- src/tui/TerminalBuffer.zig | 8 +-- src/tui/components/InfoLine.zig | 6 +-- src/tui/utils.zig | 6 +-- 11 files changed, 134 insertions(+), 92 deletions(-) diff --git a/build.zig b/build.zig index 5984a45..34e6adf 100644 --- a/build.zig +++ b/build.zig @@ -72,6 +72,7 @@ pub fn build(b: *std.Build) !void { .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); diff --git a/res/config.ini b/res/config.ini index debb6df..4490e92 100644 --- a/res/config.ini +++ b/res/config.ini @@ -1,33 +1,19 @@ -# The color settings in Ly take a digit 0-8 corresponding to: -#define TB_DEFAULT 0x00 -#define TB_BLACK 0x01 -#define TB_RED 0x02 -#define TB_GREEN 0x03 -#define TB_YELLOW 0x04 -#define TB_BLUE 0x05 -#define TB_MAGENTA 0x06 -#define TB_CYAN 0x07 -#define TB_WHITE 0x08 -# The default color varies, but usually it makes the background black and the foreground white. -# You can also combine these colors with the following style attributes using bitwise OR: -#define TB_BOLD 0x0100 -#define TB_UNDERLINE 0x0200 -#define TB_REVERSE 0x0400 -#define TB_ITALIC 0x0800 -#define TB_BLINK 0x1000 -#define TB_HI_BLACK 0x2000 -#define TB_BRIGHT 0x4000 -#define TB_DIM 0x8000 -# For example, to set the foreground color to red and bold, you would do 0x02 | 0x0100 = 0x0102. -# Note that you must pre-calculate the value because Ly doesn't parse bitwise OR operations in its config. -# -# Moreover, to set the VT color palette, you are encouraged to use another tool such as -# mkinitcpio-colors (https://github.com/evanpurkhiser/mkinitcpio-colors). Note that the color palette defined with -# mkinitcpio-colors takes 16 colors (0-15), only values 0-8 are valid with Ly and these values do not correspond -# exactly. For instance, in defining palettes with mkinitcpio-colors, the order is black, dark red, dark green, brown, dark -# blue, dark purple, dark cyan, light gray, dark gray, bright red, bright green, yellow, bright blue, bright purple, bright -# cyan, and white, indexed in that order 0 through 15. For example, the color defined for white (indexed at 15 in the mkinitcpio -# config) will be used by Ly for fg = 0x0008. +# Ly supports 24-bit true color with styling, which means each color is a 32-bit value. +# The format is 0xSSRRGGBB, where SS is the styling, RR is red, GG is green, and BB is blue. +# Here are the possible styling options: +#define TB_BOLD 0x01000000 +#define TB_UNDERLINE 0x02000000 +#define TB_REVERSE 0x04000000 +#define TB_ITALIC 0x08000000 +#define TB_BLINK 0x10000000 +#define TB_HI_BLACK 0x20000000 +#define TB_BRIGHT 0x40000000 +#define TB_DIM 0x80000000 +# Programmatically, you'd apply them using the bitwise OR operator (|), but because Ly's +# configuration doesn't support using it, you have to manually compute the color value. +# Note that, if you want to use the default color value of the terminal, you can use the +# special value 0x00000000. This means that, if you want to use black, you *must* use +# the styling option TB_HI_BLACK (the RGB values are ignored when using this option). # The active animation # none -> Nothing @@ -50,7 +36,7 @@ asterisk = * auth_fails = 10 # Background color id -bg = 0x0000 +bg = 0x00000000 # Change the state and language of the big clock # none -> Disabled (default) @@ -63,7 +49,7 @@ bigclock = none blank_box = true # Border foreground color id -border_fg = 0x0008 +border_fg = 0x00FFFFFF # Title to show at the top of the main box # If set to null, none will be shown @@ -89,16 +75,16 @@ clear_password = false clock = null # CMatrix animation foreground color id -cmatrix_fg = 0x0003 +cmatrix_fg = 0x0000FF00 # Color mixing animation first color id -colormix_col1 = 0x0002 +colormix_col1 = 0x00FF0000 # Color mixing animation second color id -colormix_col2 = 0x0005 +colormix_col2 = 0x000000FF # Color mixing animation third color id -colormix_col3 = 0x0001 +colormix_col3 = 0x20000000 # Console path console_dev = /dev/console @@ -108,14 +94,14 @@ console_dev = /dev/console default_input = login # Error background color id -error_bg = 0x0000 +error_bg = 0x00000000 # Error foreground color id -# Default is red and bold: TB_RED | TB_BOLD -error_fg = 0x0102 +# Default is red and bold +error_fg = 0x11FF0000 # Foreground color id -fg = 0x0008 +fg = 0x00FFFFFF # Remove main box borders hide_borders = false diff --git a/src/animations/ColorMix.zig b/src/animations/ColorMix.zig index fb22881..11f8f76 100644 --- a/src/animations/ColorMix.zig +++ b/src/animations/ColorMix.zig @@ -20,7 +20,7 @@ pattern_cos_mod: f32, pattern_sin_mod: f32, palette: [palette_len]utils.Cell, -pub fn init(terminal_buffer: *TerminalBuffer, col1: u16, col2: u16, col3: u16) ColorMix { +pub fn init(terminal_buffer: *TerminalBuffer, col1: u32, col2: u32, col3: u32) ColorMix { return .{ .terminal_buffer = terminal_buffer, .frames = 0, diff --git a/src/animations/Matrix.zig b/src/animations/Matrix.zig index 42bf89b..f4b2e27 100644 --- a/src/animations/Matrix.zig +++ b/src/animations/Matrix.zig @@ -34,9 +34,9 @@ dots: []Dot, lines: []Line, frame: u64, count: u64, -fg_ini: u16, +fg_ini: u32, -pub fn init(allocator: Allocator, terminal_buffer: *TerminalBuffer, fg_ini: u16) !Matrix { +pub fn init(allocator: Allocator, terminal_buffer: *TerminalBuffer, fg_ini: u32) !Matrix { const dots = try allocator.alloc(Dot, terminal_buffer.width * (terminal_buffer.height + 1)); const lines = try allocator.alloc(Line, terminal_buffer.width); diff --git a/src/bigclock.zig b/src/bigclock.zig index 6f17b72..fb4baa7 100644 --- a/src/bigclock.zig +++ b/src/bigclock.zig @@ -2,17 +2,17 @@ const std = @import("std"); const interop = @import("interop.zig"); const utils = @import("tui/utils.zig"); const enums = @import("enums.zig"); -const Lang = @import("bigclock/Lang.zig"); -const en = @import("bigclock/en.zig"); -const fa = @import("bigclock/fa.zig"); +const Lang = @import("bigclock/Lang.zig"); +const en = @import("bigclock/en.zig"); +const fa = @import("bigclock/fa.zig"); -const termbox = interop.termbox; -const Bigclock = enums.Bigclock; -pub const WIDTH = Lang.WIDTH; +const termbox = interop.termbox; +const Bigclock = enums.Bigclock; +pub const WIDTH = Lang.WIDTH; pub const HEIGHT = Lang.HEIGHT; -pub const SIZE = Lang.SIZE; +pub const SIZE = Lang.SIZE; -pub fn clockCell(animate: bool, char: u8, fg: u16, bg: u16, bigclock: Bigclock) [SIZE]utils.Cell { +pub fn clockCell(animate: bool, char: u8, fg: u32, bg: u32, bigclock: Bigclock) [SIZE]utils.Cell { var cells: [SIZE]utils.Cell = undefined; var tv: interop.system_time.timeval = undefined; @@ -37,9 +37,9 @@ pub fn alphaBlit(x: usize, y: usize, tb_width: usize, tb_height: usize, cells: [ fn toBigNumber(char: u8, bigclock: Bigclock) []const u21 { const locale_chars = switch (bigclock) { - .fa => fa.locale_chars, - .en => en.locale_chars, - .none => unreachable, + .fa => fa.locale_chars, + .en => en.locale_chars, + .none => unreachable, }; return switch (char) { '0' => &locale_chars.ZERO, diff --git a/src/config/Config.zig b/src/config/Config.zig index 1ab4232..e949ab1 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -10,10 +10,10 @@ animation: Animation = .none, animation_timeout_sec: u12 = 0, asterisk: ?u8 = '*', auth_fails: u64 = 10, -bg: u16 = 0, +bg: u32 = 0x00000000, bigclock: Bigclock = .none, blank_box: bool = true, -border_fg: u16 = 8, +border_fg: u32 = 0x00FFFFFF, box_title: ?[]const u8 = null, brightness_down_cmd: [:0]const u8 = build_options.prefix_directory ++ "/bin/brightnessctl -q s 10%-", brightness_down_key: []const u8 = "F5", @@ -21,15 +21,15 @@ brightness_up_cmd: [:0]const u8 = build_options.prefix_directory ++ "/bin/bright brightness_up_key: []const u8 = "F6", clear_password: bool = false, clock: ?[:0]const u8 = null, -cmatrix_fg: u16 = 3, -colormix_col1: u16 = 2, -colormix_col2: u16 = 5, -colormix_col3: u16 = 1, +cmatrix_fg: u32 = 0x0000FF00, +colormix_col1: u32 = 0x00FF0000, +colormix_col2: u32 = 0x000000FF, +colormix_col3: u32 = 0x20000000, console_dev: []const u8 = "/dev/console", default_input: Input = .login, -error_bg: u16 = 0, -error_fg: u16 = 258, -fg: u16 = 8, +error_bg: u32 = 0x00000000, +error_fg: u32 = 0x11FF0000, +fg: u32 = 0x00FFFFFF, hide_borders: bool = false, hide_key_hints: bool = false, initial_info_text: ?[]const u8 = null, diff --git a/src/config/migrator.zig b/src/config/migrator.zig index ad7c378..7155906 100644 --- a/src/config/migrator.zig +++ b/src/config/migrator.zig @@ -2,9 +2,34 @@ const std = @import("std"); const ini = @import("zigini"); +const interop = @import("../interop.zig"); const Save = @import("Save.zig"); const enums = @import("../enums.zig"); +const termbox = interop.termbox; +const color_properties = [_][]const u8{ + "bg", + "border_fg", + "cmatrix_fg", + "colormix_col1", + "colormix_col2", + "colormix_col3", + "error_bg", + "error_fg", + "fg", +}; +const removed_properties = [_][]const u8{ + "wayland_specifier", + "max_desktop_len", + "max_login_len", + "max_password_len", + "mcookie_cmd", + "term_reset_cmd", + "term_restore_cursor_cmd", + "x_cmd_setup", + "wayland_cmd", +}; + var temporary_allocator = std.heap.page_allocator; pub var maybe_animate: ?bool = null; @@ -12,7 +37,7 @@ pub var maybe_save_file: ?[]const u8 = null; pub var mapped_config_fields = false; -pub fn configFieldHandler(_: std.mem.Allocator, field: ini.IniField) ?ini.IniField { +pub fn configFieldHandler(allocator: std.mem.Allocator, field: ini.IniField) ?ini.IniField { if (std.mem.eql(u8, field.key, "animate")) { // The option doesn't exist anymore, but we save its value for "animation" maybe_animate = std.mem.eql(u8, field.value, "true"); @@ -37,6 +62,18 @@ pub fn configFieldHandler(_: std.mem.Allocator, field: ini.IniField) ?ini.IniFie return mapped_field; } + inline for (color_properties) |property| { + if (std.mem.eql(u8, field.key, property)) { + // These options now uses a 32-bit RGB value instead of an arbitrary 16-bit integer + const color = std.fmt.parseInt(u16, field.value, 0) catch return field; + var mapped_field = field; + + mapped_field.value = mapColor(allocator, color) catch return field; + mapped_config_fields = true; + return mapped_field; + } + } + if (std.mem.eql(u8, field.key, "blank_password")) { // The option has simply been renamed var mapped_field = field; @@ -70,19 +107,12 @@ pub fn configFieldHandler(_: std.mem.Allocator, field: ini.IniField) ?ini.IniFie return null; } - if (std.mem.eql(u8, field.key, "wayland_specifier") or - std.mem.eql(u8, field.key, "max_desktop_len") or - std.mem.eql(u8, field.key, "max_login_len") or - std.mem.eql(u8, field.key, "max_password_len") or - std.mem.eql(u8, field.key, "mcookie_cmd") or - std.mem.eql(u8, field.key, "term_reset_cmd") or - std.mem.eql(u8, field.key, "term_restore_cursor_cmd") or - std.mem.eql(u8, field.key, "x_cmd_setup") or - std.mem.eql(u8, field.key, "wayland_cmd")) - { - // The options don't exist anymore - mapped_config_fields = true; - return null; + inline for (removed_properties) |property| { + if (std.mem.eql(u8, field.key, property)) { + // The options don't exist anymore + mapped_config_fields = true; + return null; + } } if (std.mem.eql(u8, field.key, "bigclock")) { @@ -90,14 +120,14 @@ pub fn configFieldHandler(_: std.mem.Allocator, field: ini.IniField) ?ini.IniFie // It also includes the ability to change active bigclock's language var mapped_field = field; - if (std.mem.eql(u8, field.value, "true")){ + if (std.mem.eql(u8, field.value, "true")) { mapped_field.value = "en"; mapped_config_fields = true; - }else if (std.mem.eql(u8, field.value, "false")){ + } else if (std.mem.eql(u8, field.value, "false")) { mapped_field.value = "none"; mapped_config_fields = true; } - + return mapped_field; } @@ -142,3 +172,28 @@ pub fn tryMigrateSaveFile(user_buf: *[32]u8) Save { return save; } + +fn mapColor(allocator: std.mem.Allocator, color: u16) ![]const u8 { + const color_no_styling = color & 0x00FF; + const styling_only = color & 0xFF00; + + var new_color: u32 = switch (color_no_styling) { + termbox.TB_BLACK => termbox.TB_HI_BLACK, + termbox.TB_RED => 0x00FF0000, + termbox.TB_GREEN => 0x0000FF00, + termbox.TB_YELLOW => 0x00FFFF00, + termbox.TB_BLUE => 0x000000FF, + termbox.TB_MAGENTA => 0x00FF00FF, + termbox.TB_CYAN => 0x0000FFFF, + termbox.TB_WHITE => 0x00FFFFFF, + else => termbox.TB_DEFAULT, + }; + + // Only applying styling if color isn't black and styling isn't also black + if (!(new_color == termbox.TB_HI_BLACK and styling_only == termbox.TB_HI_BLACK)) { + // Shift styling by 16 to the left to apply it to the new 32-bit color + new_color |= @as(u32, @intCast(styling_only)) << 16; + } + + return try std.fmt.allocPrint(allocator, "0x{X}", .{new_color}); +} diff --git a/src/main.zig b/src/main.zig index 582db77..bb54c3e 100644 --- a/src/main.zig +++ b/src/main.zig @@ -212,7 +212,7 @@ pub fn main() !void { }; std.posix.sigaction(std.posix.SIG.TERM, &act, null); - _ = termbox.tb_set_output_mode(termbox.TB_OUTPUT_NORMAL); + _ = termbox.tb_set_output_mode(termbox.TB_OUTPUT_TRUECOLOR); _ = termbox.tb_clear(); // Needed to reset termbox after auth @@ -729,7 +729,7 @@ pub fn main() !void { // Take back control of the TTY _ = termbox.tb_init(); - _ = termbox.tb_set_output_mode(termbox.TB_OUTPUT_NORMAL); + _ = termbox.tb_set_output_mode(termbox.TB_OUTPUT_TRUECOLOR); const auth_err = shared_err.readError(); if (auth_err) |err| { diff --git a/src/tui/TerminalBuffer.zig b/src/tui/TerminalBuffer.zig index 494ab81..cc18100 100644 --- a/src/tui/TerminalBuffer.zig +++ b/src/tui/TerminalBuffer.zig @@ -14,9 +14,9 @@ random: Random, width: usize, height: usize, buffer: [*]termbox.tb_cell, -fg: u16, -bg: u16, -border_fg: u16, +fg: u32, +bg: u32, +border_fg: u32, box_chars: struct { left_up: u32, left_down: u32, @@ -170,7 +170,7 @@ pub fn drawLabel(self: TerminalBuffer, text: []const u8, x: usize, y: usize) voi drawColorLabel(text, x, y, self.fg, self.bg); } -pub fn drawColorLabel(text: []const u8, x: usize, y: usize, fg: u16, bg: u16) void { +pub fn drawColorLabel(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(); diff --git a/src/tui/components/InfoLine.zig b/src/tui/components/InfoLine.zig index a43b083..05d421c 100644 --- a/src/tui/components/InfoLine.zig +++ b/src/tui/components/InfoLine.zig @@ -12,8 +12,8 @@ const InfoLine = @This(); const Message = struct { width: u8, text: []const u8, - bg: u16, - fg: u16, + bg: u32, + fg: u32, }; label: MessageLabel, @@ -28,7 +28,7 @@ pub fn deinit(self: InfoLine) void { self.label.deinit(); } -pub fn addMessage(self: *InfoLine, text: []const u8, bg: u16, fg: u16) !void { +pub fn addMessage(self: *InfoLine, text: []const u8, bg: u32, fg: u32) !void { if (text.len == 0) return; try self.label.addItem(.{ diff --git a/src/tui/utils.zig b/src/tui/utils.zig index 43c3619..ace54a6 100644 --- a/src/tui/utils.zig +++ b/src/tui/utils.zig @@ -5,11 +5,11 @@ const termbox = interop.termbox; pub const Cell = struct { ch: u32, - fg: u16, - bg: u16, + fg: u32, + bg: u32, }; -pub fn initCell(ch: u32, fg: u16, bg: u16) Cell { +pub fn initCell(ch: u32, fg: u32, bg: u32) Cell { return .{ .ch = ch, .fg = fg, From 0c69e0412ce3293c39b03691b27fdf764d5eb49a Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Thu, 6 Mar 2025 13:08:50 +0100 Subject: [PATCH 143/530] Don't make errors blink by default Signed-off-by: AnErrupTion --- res/config.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/res/config.ini b/res/config.ini index 4490e92..cef6040 100644 --- a/res/config.ini +++ b/res/config.ini @@ -98,7 +98,7 @@ error_bg = 0x00000000 # Error foreground color id # Default is red and bold -error_fg = 0x11FF0000 +error_fg = 0x01FF0000 # Foreground color id fg = 0x00FFFFFF From 3e6d7a1b3bc98ab6155616da3f1bc1be6a0fa26e Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Thu, 6 Mar 2025 13:09:19 +0100 Subject: [PATCH 144/530] Forgot a file lol Signed-off-by: AnErrupTion --- src/config/Config.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/config/Config.zig b/src/config/Config.zig index e949ab1..e5f57d1 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -28,7 +28,7 @@ colormix_col3: u32 = 0x20000000, console_dev: []const u8 = "/dev/console", default_input: Input = .login, error_bg: u32 = 0x00000000, -error_fg: u32 = 0x11FF0000, +error_fg: u32 = 0x01FF0000, fg: u32 = 0x00FFFFFF, hide_borders: bool = false, hide_key_hints: bool = false, From 973d8fe1205df26b0d90c6adfe1d4ce776e44fe9 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Thu, 6 Mar 2025 13:23:47 +0100 Subject: [PATCH 145/530] Don't dynamically allocate color strings Signed-off-by: AnErrupTion --- src/config/migrator.zig | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/config/migrator.zig b/src/config/migrator.zig index 7155906..df3f3b7 100644 --- a/src/config/migrator.zig +++ b/src/config/migrator.zig @@ -37,7 +37,7 @@ pub var maybe_save_file: ?[]const u8 = null; pub var mapped_config_fields = false; -pub fn configFieldHandler(allocator: std.mem.Allocator, field: ini.IniField) ?ini.IniField { +pub fn configFieldHandler(_: std.mem.Allocator, field: ini.IniField) ?ini.IniField { if (std.mem.eql(u8, field.key, "animate")) { // The option doesn't exist anymore, but we save its value for "animation" maybe_animate = std.mem.eql(u8, field.value, "true"); @@ -68,7 +68,7 @@ pub fn configFieldHandler(allocator: std.mem.Allocator, field: ini.IniField) ?in const color = std.fmt.parseInt(u16, field.value, 0) catch return field; var mapped_field = field; - mapped_field.value = mapColor(allocator, color) catch return field; + mapped_field.value = mapColor(color) catch return field; mapped_config_fields = true; return mapped_field; } @@ -173,7 +173,7 @@ pub fn tryMigrateSaveFile(user_buf: *[32]u8) Save { return save; } -fn mapColor(allocator: std.mem.Allocator, color: u16) ![]const u8 { +fn mapColor(color: u16) ![]const u8 { const color_no_styling = color & 0x00FF; const styling_only = color & 0xFF00; @@ -195,5 +195,6 @@ fn mapColor(allocator: std.mem.Allocator, color: u16) ![]const u8 { new_color |= @as(u32, @intCast(styling_only)) << 16; } - return try std.fmt.allocPrint(allocator, "0x{X}", .{new_color}); + var buffer = std.mem.zeroes([10]u8); + return try std.fmt.bufPrint(&buffer, "0x{X}", .{new_color}); } From f013af0dde471866160fa9ba86353f43d55afde9 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Thu, 6 Mar 2025 14:31:11 +0100 Subject: [PATCH 146/530] Fix segmentation fault when using color mix after auth Signed-off-by: AnErrupTion --- src/animations/ColorMix.zig | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/animations/ColorMix.zig b/src/animations/ColorMix.zig index 11f8f76..95e7df0 100644 --- a/src/animations/ColorMix.zig +++ b/src/animations/ColorMix.zig @@ -71,12 +71,7 @@ pub fn draw(self: *ColorMix) void { } const cell = self.palette[@as(usize, @intFromFloat(math.floor(length(uv) * 5.0))) % palette_len]; - const screen_index: usize = y * self.terminal_buffer.width + x; - self.terminal_buffer.buffer[screen_index] = .{ - .ch = cell.ch, - .fg = cell.fg, - .bg = cell.bg, - }; + utils.putCell(x, y, cell); } } } From 55abc4d7f135042bb5b8465c27185b3767e04856 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Thu, 6 Mar 2025 14:32:05 +0100 Subject: [PATCH 147/530] Decouple TerminalBuffer and auth from Config Signed-off-by: AnErrupTion --- src/auth.zig | 63 +++++++++++++++++++--------------- src/main.zig | 28 ++++++++++++--- src/tui/TerminalBuffer.zig | 26 +++++++++----- src/tui/components/Text.zig | 1 - src/tui/components/generic.zig | 1 - 5 files changed, 76 insertions(+), 43 deletions(-) diff --git a/src/auth.zig b/src/auth.zig index cf9b743..b60bb87 100644 --- a/src/auth.zig +++ b/src/auth.zig @@ -3,15 +3,24 @@ const build_options = @import("build_options"); const builtin = @import("builtin"); const enums = @import("enums.zig"); const interop = @import("interop.zig"); -const TerminalBuffer = @import("tui/TerminalBuffer.zig"); const Session = @import("tui/components/Session.zig"); -const Text = @import("tui/components/Text.zig"); -const Config = @import("config/Config.zig"); +const SharedError = @import("SharedError.zig"); + const Allocator = std.mem.Allocator; const Md5 = std.crypto.hash.Md5; const utmp = interop.utmp; const Utmp = utmp.utmpx; -const SharedError = @import("SharedError.zig"); + +pub const AuthOptions = struct { + tty: u8, + service_name: [:0]const u8, + path: ?[:0]const u8, + session_log: []const u8, + xauth_cmd: []const u8, + setup_cmd: []const u8, + login_cmd: ?[]const u8, + x_cmd: []const u8, +}; var xorg_pid: std.posix.pid_t = 0; pub fn xorgSignalHandler(i: c_int) callconv(.C) void { @@ -23,12 +32,12 @@ pub fn sessionSignalHandler(i: c_int) callconv(.C) void { if (child_pid > 0) _ = std.c.kill(child_pid, i); } -pub fn authenticate(config: Config, current_environment: Session.Environment, login: [:0]const u8, password: [:0]const u8) !void { +pub fn authenticate(options: AuthOptions, current_environment: Session.Environment, login: [:0]const u8, password: [:0]const u8) !void { var tty_buffer: [3]u8 = undefined; - const tty_str = try std.fmt.bufPrintZ(&tty_buffer, "{d}", .{config.tty}); + const tty_str = try std.fmt.bufPrintZ(&tty_buffer, "{d}", .{options.tty}); var pam_tty_buffer: [6]u8 = undefined; - const pam_tty_str = try std.fmt.bufPrintZ(&pam_tty_buffer, "tty{d}", .{config.tty}); + const pam_tty_str = try std.fmt.bufPrintZ(&pam_tty_buffer, "tty{d}", .{options.tty}); // Set the XDG environment variables setXdgSessionEnv(current_environment.display_server); @@ -43,7 +52,7 @@ pub fn authenticate(config: Config, current_environment: Session.Environment, lo }; var handle: ?*interop.pam.pam_handle = undefined; - var status = interop.pam.pam_start(config.service_name, null, &conv, &handle); + 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); @@ -86,7 +95,7 @@ pub fn authenticate(config: Config, current_environment: Session.Environment, lo child_pid = try std.posix.fork(); if (child_pid == 0) { - startSession(config, pwd, handle, current_environment) catch |e| { + startSession(options, pwd, handle, current_environment) catch |e| { shared_err.writeError(e); std.process.exit(1); }; @@ -121,7 +130,7 @@ pub fn authenticate(config: Config, current_environment: Session.Environment, lo } fn startSession( - config: Config, + options: AuthOptions, pwd: *interop.pwd.passwd, handle: ?*interop.pam.pam_handle, current_environment: Session.Environment, @@ -143,7 +152,7 @@ fn startSession( } // Set up the environment - try initEnv(pwd, config.path); + try initEnv(pwd, options.path); // Set the PAM variables const pam_env_vars: ?[*:null]?[*:0]u8 = interop.pam.pam_getenvlist(handle); @@ -157,12 +166,12 @@ fn startSession( // Execute what the user requested switch (current_environment.display_server) { - .wayland => try executeWaylandCmd(pwd.pw_shell.?, config, current_environment.cmd), - .shell => try executeShellCmd(pwd.pw_shell.?, config), + .wayland => try executeWaylandCmd(pwd.pw_shell.?, options, current_environment.cmd), + .shell => try executeShellCmd(pwd.pw_shell.?, options), .xinitrc, .x11 => if (build_options.enable_x11_support) { var vt_buf: [5]u8 = undefined; - const vt = try std.fmt.bufPrint(&vt_buf, "vt{d}", .{config.tty}); - try executeX11Cmd(pwd.pw_shell.?, pwd.pw_dir.?, config, current_environment.cmd, vt); + const vt = try std.fmt.bufPrint(&vt_buf, "vt{d}", .{options.tty}); + try executeX11Cmd(pwd.pw_shell.?, pwd.pw_dir.?, options, current_environment.cmd, vt); }, } } @@ -350,7 +359,7 @@ fn mcookie() [Md5.digest_length * 2]u8 { return std.fmt.bytesToHex(&out, .lower); } -fn xauth(display_name: [:0]u8, shell: [*:0]const u8, pw_dir: [*:0]const u8, config: Config) !void { +fn xauth(display_name: [:0]u8, shell: [*:0]const u8, pw_dir: [*:0]const u8, options: AuthOptions) !void { var pwd_buf: [100]u8 = undefined; const pwd = try std.fmt.bufPrintZ(&pwd_buf, "{s}", .{pw_dir}); @@ -362,11 +371,11 @@ fn xauth(display_name: [:0]u8, shell: [*:0]const u8, pw_dir: [*:0]const u8, conf const pid = try std.posix.fork(); if (pid == 0) { - const log_file = try redirectStandardStreams(config.session_log, true); + const log_file = try redirectStandardStreams(options.session_log, true); defer log_file.close(); var cmd_buffer: [1024]u8 = undefined; - const cmd_str = std.fmt.bufPrintZ(&cmd_buffer, "{s} add {s} . {s}", .{ config.xauth_cmd, display_name, magic_cookie }) catch std.process.exit(1); + const cmd_str = std.fmt.bufPrintZ(&cmd_buffer, "{s} add {s} . {s}", .{ options.xauth_cmd, display_name, magic_cookie }) catch std.process.exit(1); const args = [_:null]?[*:0]const u8{ shell, "-c", cmd_str }; std.posix.execveZ(shell, &args, std.c.environ) catch {}; std.process.exit(1); @@ -376,35 +385,35 @@ fn xauth(display_name: [:0]u8, shell: [*:0]const u8, pw_dir: [*:0]const u8, conf if (status.status != 0) return error.XauthFailed; } -fn executeShellCmd(shell: [*:0]const u8, config: Config) !void { +fn executeShellCmd(shell: [*:0]const u8, options: AuthOptions) !void { // We don't want to redirect stdout and stderr in a shell session var cmd_buffer: [1024]u8 = undefined; - const cmd_str = try std.fmt.bufPrintZ(&cmd_buffer, "{s} {s} {s}", .{ config.setup_cmd, config.login_cmd orelse "", shell }); + const cmd_str = try std.fmt.bufPrintZ(&cmd_buffer, "{s} {s} {s}", .{ options.setup_cmd, options.login_cmd orelse "", shell }); const args = [_:null]?[*:0]const u8{ shell, "-c", cmd_str }; return std.posix.execveZ(shell, &args, std.c.environ); } -fn executeWaylandCmd(shell: [*:0]const u8, config: Config, desktop_cmd: []const u8) !void { - const log_file = try redirectStandardStreams(config.session_log, true); +fn executeWaylandCmd(shell: [*:0]const u8, options: AuthOptions, desktop_cmd: []const u8) !void { + const log_file = try redirectStandardStreams(options.session_log, true); defer log_file.close(); var cmd_buffer: [1024]u8 = undefined; - const cmd_str = try std.fmt.bufPrintZ(&cmd_buffer, "{s} {s} {s}", .{ config.setup_cmd, config.login_cmd orelse "", desktop_cmd }); + const cmd_str = try std.fmt.bufPrintZ(&cmd_buffer, "{s} {s} {s}", .{ options.setup_cmd, options.login_cmd orelse "", desktop_cmd }); const args = [_:null]?[*:0]const u8{ shell, "-c", cmd_str }; return std.posix.execveZ(shell, &args, std.c.environ); } -fn executeX11Cmd(shell: [*:0]const u8, pw_dir: [*:0]const u8, config: Config, desktop_cmd: []const u8, vt: []const u8) !void { +fn executeX11Cmd(shell: [*:0]const u8, pw_dir: [*:0]const u8, options: AuthOptions, desktop_cmd: []const u8, vt: []const u8) !void { const display_num = try getFreeDisplay(); var buf: [5]u8 = undefined; const display_name = try std.fmt.bufPrintZ(&buf, ":{d}", .{display_num}); - try xauth(display_name, shell, pw_dir, config); + try xauth(display_name, shell, pw_dir, options); 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} >{s} 2>&1", .{ config.x_cmd, display_name, vt, config.session_log }) catch std.process.exit(1); + const cmd_str = std.fmt.bufPrintZ(&cmd_buffer, "{s} {s} {s} >{s} 2>&1", .{ options.x_cmd, display_name, vt, options.session_log }) catch std.process.exit(1); const args = [_:null]?[*:0]const u8{ shell, "-c", cmd_str }; std.posix.execveZ(shell, &args, std.c.environ) catch {}; std.process.exit(1); @@ -427,7 +436,7 @@ fn executeX11Cmd(shell: [*:0]const u8, pw_dir: [*:0]const u8, config: Config, de 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} >{s} 2>&1", .{ config.setup_cmd, config.login_cmd orelse "", desktop_cmd, config.session_log }) catch std.process.exit(1); + const cmd_str = std.fmt.bufPrintZ(&cmd_buffer, "{s} {s} {s} >{s} 2>&1", .{ options.setup_cmd, options.login_cmd orelse "", desktop_cmd, options.session_log }) catch std.process.exit(1); const args = [_:null]?[*:0]const u8{ shell, "-c", cmd_str }; std.posix.execveZ(shell, &args, std.c.environ) catch {}; std.process.exit(1); diff --git a/src/main.zig b/src/main.zig index bb54c3e..d8c21ef 100644 --- a/src/main.zig +++ b/src/main.zig @@ -63,11 +63,10 @@ pub fn main() !void { } } - var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + var gpa = std.heap.DebugAllocator(.{}).init; defer _ = gpa.deinit(); - // to be able to stop the animation after some time - + // Allows stopping an animation after some time var tv_zero: interop.system_time.timeval = undefined; _ = interop.system_time.gettimeofday(&tv_zero, null); var animation_timed_out: bool = false; @@ -227,7 +226,15 @@ pub fn main() !void { var prng = std.Random.DefaultPrng.init(seed); const random = prng.random(); - var buffer = TerminalBuffer.init(config, labels_max_length, random); + const buffer_options = TerminalBuffer.InitOptions{ + .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, + }; + var buffer = TerminalBuffer.init(buffer_options, labels_max_length, random); // Initialize components var info_line = InfoLine.init(allocator, &buffer); @@ -716,7 +723,18 @@ pub fn main() !void { session_pid = try std.posix.fork(); if (session_pid == 0) { const current_environment = session.label.list.items[session.label.current]; - auth.authenticate(config, current_environment, login_text, password_text) catch |err| { + const auth_options = auth.AuthOptions{ + .tty = config.tty, + .service_name = config.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, + }; + + auth.authenticate(auth_options, current_environment, login_text, password_text) catch |err| { shared_err.writeError(err); std.process.exit(1); }; diff --git a/src/tui/TerminalBuffer.zig b/src/tui/TerminalBuffer.zig index cc18100..1fc5d5b 100644 --- a/src/tui/TerminalBuffer.zig +++ b/src/tui/TerminalBuffer.zig @@ -2,7 +2,6 @@ const std = @import("std"); const builtin = @import("builtin"); const interop = @import("../interop.zig"); const utils = @import("utils.zig"); -const Config = @import("../config/Config.zig"); const Random = std.Random; @@ -10,6 +9,15 @@ const termbox = interop.termbox; const TerminalBuffer = @This(); +pub const InitOptions = struct { + fg: u32, + bg: u32, + border_fg: u32, + margin_box_h: u8, + margin_box_v: u8, + input_len: u8, +}; + random: Random, width: usize, height: usize, @@ -35,15 +43,15 @@ box_height: usize, margin_box_v: u8, margin_box_h: u8, -pub fn init(config: Config, labels_max_length: usize, random: Random) TerminalBuffer { +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()), .buffer = termbox.tb_cell_buffer(), - .fg = config.fg, - .bg = config.bg, - .border_fg = config.border_fg, + .fg = options.fg, + .bg = options.bg, + .border_fg = options.border_fg, .box_chars = if (builtin.os.tag == .linux or builtin.os.tag.isBSD()) .{ .left_up = 0x250C, .left_down = 0x2514, @@ -66,10 +74,10 @@ pub fn init(config: Config, labels_max_length: usize, random: Random) TerminalBu .labels_max_length = labels_max_length, .box_x = 0, .box_y = 0, - .box_width = (2 * config.margin_box_h) + config.input_len + 1 + labels_max_length, - .box_height = 7 + (2 * config.margin_box_v), - .margin_box_v = config.margin_box_v, - .margin_box_h = config.margin_box_h, + .box_width = (2 * options.margin_box_h) + options.input_len + 1 + 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, }; } diff --git a/src/tui/components/Text.zig b/src/tui/components/Text.zig index 18223b2..d694eb4 100644 --- a/src/tui/components/Text.zig +++ b/src/tui/components/Text.zig @@ -1,7 +1,6 @@ const std = @import("std"); const interop = @import("../../interop.zig"); const TerminalBuffer = @import("../TerminalBuffer.zig"); -const utils = @import("../utils.zig"); const Allocator = std.mem.Allocator; const DynamicString = std.ArrayList(u8); diff --git a/src/tui/components/generic.zig b/src/tui/components/generic.zig index 126916b..060a3fe 100644 --- a/src/tui/components/generic.zig +++ b/src/tui/components/generic.zig @@ -1,5 +1,4 @@ const std = @import("std"); -const enums = @import("../../enums.zig"); const interop = @import("../../interop.zig"); const TerminalBuffer = @import("../TerminalBuffer.zig"); From 6cb102257cb20ff4e81bf23333159416054d2a74 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Thu, 6 Mar 2025 15:27:38 +0100 Subject: [PATCH 148/530] Fix Doom & Matrix animation + bug in migrator Signed-off-by: AnErrupTion --- src/animations/Doom.zig | 44 +++++++++++++++++---------------------- src/animations/Matrix.zig | 7 ++++--- src/config/migrator.zig | 9 ++++++-- 3 files changed, 30 insertions(+), 30 deletions(-) diff --git a/src/animations/Doom.zig b/src/animations/Doom.zig index 9813122..94df92c 100644 --- a/src/animations/Doom.zig +++ b/src/animations/Doom.zig @@ -3,26 +3,23 @@ const Allocator = std.mem.Allocator; const TerminalBuffer = @import("../tui/TerminalBuffer.zig"); const utils = @import("../tui/utils.zig"); -const interop = @import("../interop.zig"); -const termbox = interop.termbox; - const Doom = @This(); pub const STEPS = 13; pub const FIRE = [_]utils.Cell{ utils.initCell(' ', 9, 0), - utils.initCell(0x2591, 2, 0), // Red - utils.initCell(0x2592, 2, 0), // Red - utils.initCell(0x2593, 2, 0), // Red - utils.initCell(0x2588, 2, 0), // Red - utils.initCell(0x2591, 4, 2), // Yellow - utils.initCell(0x2592, 4, 2), // Yellow - utils.initCell(0x2593, 4, 2), // Yellow - utils.initCell(0x2588, 4, 2), // Yellow - utils.initCell(0x2591, 8, 4), // White - utils.initCell(0x2592, 8, 4), // White - utils.initCell(0x2593, 8, 4), // White - utils.initCell(0x2588, 8, 4), // White + utils.initCell(0x2591, 0x00FF0000, 0), // Red + utils.initCell(0x2592, 0x00FF0000, 0), // Red + utils.initCell(0x2593, 0x00FF0000, 0), // Red + utils.initCell(0x2588, 0x00FF0000, 0), // Red + utils.initCell(0x2591, 0x00FFFF00, 2), // Yellow + utils.initCell(0x2592, 0x00FFFF00, 2), // Yellow + utils.initCell(0x2593, 0x00FFFF00, 2), // Yellow + utils.initCell(0x2588, 0x00FFFF00, 2), // Yellow + utils.initCell(0x2591, 0x00FFFFFF, 4), // White + utils.initCell(0x2592, 0x00FFFFFF, 4), // White + utils.initCell(0x2593, 0x00FFFFFF, 4), // White + utils.initCell(0x2588, 0x00FFFFFF, 4), // White }; allocator: Allocator, @@ -68,8 +65,13 @@ pub fn draw(self: Doom) void { if (buffer_dest > 12) buffer_dest = 0; self.buffer[dest] = @intCast(buffer_dest); - self.terminal_buffer.buffer[dest] = toTermboxCell(FIRE[buffer_dest]); - self.terminal_buffer.buffer[source] = toTermboxCell(FIRE[buffer_source]); + const dest_y = dest / self.terminal_buffer.width; + const dest_x = dest % self.terminal_buffer.width; + utils.putCell(dest_x, dest_y, FIRE[buffer_dest]); + + const source_y = source / self.terminal_buffer.width; + const source_x = source % self.terminal_buffer.width; + utils.putCell(source_x, source_y, FIRE[buffer_source]); } } } @@ -82,11 +84,3 @@ fn initBuffer(buffer: []u8, width: usize) void { @memset(slice_start, 0); @memset(slice_end, STEPS - 1); } - -fn toTermboxCell(cell: utils.Cell) termbox.tb_cell { - return .{ - .ch = cell.ch, - .fg = cell.fg, - .bg = cell.bg, - }; -} diff --git a/src/animations/Matrix.zig b/src/animations/Matrix.zig index f4b2e27..d978f4b 100644 --- a/src/animations/Matrix.zig +++ b/src/animations/Matrix.zig @@ -2,6 +2,7 @@ const std = @import("std"); const Allocator = std.mem.Allocator; const Random = std.Random; const TerminalBuffer = @import("../tui/TerminalBuffer.zig"); +const utils = @import("../tui/utils.zig"); const interop = @import("../interop.zig"); const termbox = interop.termbox; @@ -151,12 +152,12 @@ pub fn draw(self: *Matrix) void { var fg = self.fg_ini; if (dot.value == -1 or dot.value == ' ') { - _ = termbox.tb_set_cell(@intCast(x), @intCast(y - 1), ' ', fg, termbox.TB_DEFAULT); + utils.putCell(x, y - 1, .{ .ch = ' ', .fg = fg, .bg = termbox.TB_DEFAULT }); continue; } - if (dot.is_head) fg = @intCast(termbox.TB_WHITE | termbox.TB_BOLD); - _ = termbox.tb_set_cell(@intCast(x), @intCast(y - 1), @intCast(dot.value), fg, termbox.TB_DEFAULT); + if (dot.is_head) fg = @intCast(0x00FFFFFF | termbox.TB_BOLD); // White and bold + utils.putCell(x, y - 1, .{ .ch = @intCast(dot.value), .fg = fg, .bg = termbox.TB_DEFAULT }); } } } diff --git a/src/config/migrator.zig b/src/config/migrator.zig index df3f3b7..224c55b 100644 --- a/src/config/migrator.zig +++ b/src/config/migrator.zig @@ -31,6 +31,7 @@ const removed_properties = [_][]const u8{ }; var temporary_allocator = std.heap.page_allocator; +var buffer = std.mem.zeroes([10 * color_properties.len]u8); pub var maybe_animate: ?bool = null; pub var maybe_save_file: ?[]const u8 = null; @@ -177,7 +178,12 @@ fn mapColor(color: u16) ![]const u8 { const color_no_styling = color & 0x00FF; const styling_only = color & 0xFF00; + if (color_no_styling > termbox.TB_WHITE or styling_only > 0x8000) { // TB_DIM in 16-bit mode + return error.InvalidColor; + } + var new_color: u32 = switch (color_no_styling) { + termbox.TB_DEFAULT => termbox.TB_DEFAULT, termbox.TB_BLACK => termbox.TB_HI_BLACK, termbox.TB_RED => 0x00FF0000, termbox.TB_GREEN => 0x0000FF00, @@ -186,7 +192,7 @@ fn mapColor(color: u16) ![]const u8 { termbox.TB_MAGENTA => 0x00FF00FF, termbox.TB_CYAN => 0x0000FFFF, termbox.TB_WHITE => 0x00FFFFFF, - else => termbox.TB_DEFAULT, + else => unreachable, }; // Only applying styling if color isn't black and styling isn't also black @@ -195,6 +201,5 @@ fn mapColor(color: u16) ![]const u8 { new_color |= @as(u32, @intCast(styling_only)) << 16; } - var buffer = std.mem.zeroes([10]u8); return try std.fmt.bufPrint(&buffer, "0x{X}", .{new_color}); } From 9168266cca96a4f3e01080d6fb58cec481aa368b Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Thu, 6 Mar 2025 15:29:56 +0100 Subject: [PATCH 149/530] Don't shutdown termbox2 if authentication fails Signed-off-by: AnErrupTion --- src/auth.zig | 4 ++++ src/main.zig | 18 ++++++++++++++++-- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/src/auth.zig b/src/auth.zig index b60bb87..216784d 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, + session_pid: std.posix.pid_t, }; var xorg_pid: std.posix.pid_t = 0; @@ -164,6 +165,9 @@ fn startSession( // Change to the user's home directory std.posix.chdirZ(pwd.pw_dir.?) catch return error.ChangeDirectoryFailed; + // Signal to the session process to give up control on the 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 => try executeWaylandCmd(pwd.pw_shell.?, options, current_environment.cmd), diff --git a/src/main.zig b/src/main.zig index d8c21ef..0185e62 100644 --- a/src/main.zig +++ b/src/main.zig @@ -26,7 +26,7 @@ const unistd = interop.unistd; const temporary_allocator = std.heap.page_allocator; var session_pid: std.posix.pid_t = -1; -pub fn signalHandler(i: c_int) callconv(.C) void { +fn signalHandler(i: c_int) callconv(.C) void { if (session_pid == 0) return; // Forward signal to session to clean up @@ -40,6 +40,10 @@ pub fn signalHandler(i: c_int) callconv(.C) void { std.c.exit(i); } +fn ttyControlTransferSignalHandler(_: c_int) callconv(.C) void { + _ = termbox.tb_shutdown(); +} + pub fn main() !void { var shutdown = false; var restart = false; @@ -718,7 +722,7 @@ pub fn main() !void { defer allocator.free(password_text); // Give up control on the TTY - _ = termbox.tb_shutdown(); + // _ = termbox.tb_shutdown(); session_pid = try std.posix.fork(); if (session_pid == 0) { @@ -732,8 +736,17 @@ pub fn main() !void { .setup_cmd = config.setup_cmd, .login_cmd = config.login_cmd, .x_cmd = config.x_cmd, + .session_pid = session_pid, }; + // Signal action to give up control on the TTY + const tty_control_transfer_act = std.posix.Sigaction{ + .handler = .{ .handler = &ttyControlTransferSignalHandler }, + .mask = std.posix.empty_sigset, + .flags = 0, + }; + std.posix.sigaction(std.posix.SIG.CHLD, &tty_control_transfer_act, null); + auth.authenticate(auth_options, current_environment, login_text, password_text) catch |err| { shared_err.writeError(err); std.process.exit(1); @@ -830,6 +843,7 @@ fn getAuthErrorMsg(err: anyerror, lang: Lang) []const u8 { error.SetUserGidFailed => lang.err_user_gid, error.SetUserUidFailed => lang.err_user_uid, error.ChangeDirectoryFailed => lang.err_perm_dir, + error.TtyControlTransferFailed => "tty control transfer failed", error.SetPathFailed => lang.err_path, error.PamAccountExpired => lang.err_pam_acct_expired, error.PamAuthError => lang.err_pam_auth, From 9c79137c9fe64330b6b9d74af293c48cdbf293b0 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Thu, 6 Mar 2025 15:42:33 +0100 Subject: [PATCH 150/530] Remove all deprecated calls to tb_cell_buffer() Signed-off-by: AnErrupTion --- include/termbox2.h | 32 ++++++++++++++++++++++++-------- src/main.zig | 3 +-- src/tui/TerminalBuffer.zig | 9 +++++---- 3 files changed, 30 insertions(+), 14 deletions(-) diff --git a/include/termbox2.h b/include/termbox2.h index 406fbac..297d0be 100644 --- a/include/termbox2.h +++ b/include/termbox2.h @@ -506,6 +506,21 @@ int tb_set_cell_ex(int x, int y, uint32_t *ch, size_t nch, uintattr_t fg, uintattr_t bg); int tb_extend_cell(int x, int y, uint32_t ch); +/* Get cell at specified position. + * + * If position is valid, function returns TB_OK and cell contents are copied to + * `cell`. Note if `nech>0`, then `ech` will be a pointer to memory which may + * be invalid or freed after subsequent library calls. Callers must copy this + * memory if they need to persist it for some reason. Modifying memory at `ech` + * results in undefined behavior. + * + * If `back` is non-zero, return cells from the internal back buffer. Otherwise, + * return cells from the front buffer. Note the front buffer is updated on each + * call to tb_present(), whereas the back buffer is updated immediately by + * tb_set_cell() and other functions that mutate cell contents. + */ +int tb_get_cell(int x, int y, int back, struct tb_cell *cell); + /* Set the input mode. Termbox has two input modes: * * 1. `TB_INPUT_ESC` @@ -1771,6 +1786,15 @@ int tb_set_cell_ex(int x, int y, uint32_t *ch, size_t nch, uintattr_t fg, return TB_OK; } +int tb_get_cell(int x, int y, int back, struct tb_cell *cell) { + if_not_init_return(); + int rv; + struct tb_cell *cellp = NULL; + rv = cellbuf_get(back ? &global.back : &global.front, x, y, &cellp); + if (cellp) memcpy(cell, cellp, sizeof(*cell)); + return rv; +} + int tb_extend_cell(int x, int y, uint32_t ch) { if_not_init_return(); #ifdef TB_OPT_EGC @@ -3271,14 +3295,6 @@ static int send_cluster(int x, int y, uint32_t *ch, size_t nch) { ch32 = 0xfffd; // replace non-printable codepoints with U+FFFD } int chu8_len = tb_utf8_unicode_to_char(chu8, ch32); - /* - if (ch32 == 0) { // replace null with space (from termbox 19dbee5) - chu8_len = 1; - chu8[0] = ' '; - } else { - chu8_len = tb_utf8_unicode_to_char(chu8, ch32); - } - */ if_err_return(rv, bytebuf_nputs(&global.out, chu8, (size_t)chu8_len)); } diff --git a/src/main.zig b/src/main.zig index 0185e62..39f1bfd 100644 --- a/src/main.zig +++ b/src/main.zig @@ -378,7 +378,7 @@ pub fn main() !void { if (!update or config.animation != .none) { if (!update) std.time.sleep(std.time.ns_per_ms * 100); - _ = termbox.tb_present(); // Required to update tb_width(), tb_height() and tb_cell_buffer() + _ = 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()); @@ -388,7 +388,6 @@ pub fn main() !void { buffer.width = width; buffer.height = height; - buffer.buffer = termbox.tb_cell_buffer(); switch (config.animation) { .none => {}, diff --git a/src/tui/TerminalBuffer.zig b/src/tui/TerminalBuffer.zig index 1fc5d5b..8281814 100644 --- a/src/tui/TerminalBuffer.zig +++ b/src/tui/TerminalBuffer.zig @@ -21,7 +21,6 @@ pub const InitOptions = struct { random: Random, width: usize, height: usize, -buffer: [*]termbox.tb_cell, fg: u32, bg: u32, border_fg: u32, @@ -48,7 +47,6 @@ pub fn init(options: InitOptions, labels_max_length: usize, random: Random) Term .random = random, .width = @intCast(termbox.tb_width()), .height = @intCast(termbox.tb_height()), - .buffer = termbox.tb_cell_buffer(), .fg = options.fg, .bg = options.bg, .border_fg = options.border_fg, @@ -87,8 +85,11 @@ pub fn cascade(self: TerminalBuffer) bool { while (y > 0) : (y -= 1) { for (0..self.width) |x| { - const cell = self.buffer[(y - 1) * self.width + x]; - const cell_under = self.buffer[y * 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); const char: u8 = @truncate(cell.ch); if (std.ascii.isWhitespace(char)) continue; From 6079c01a4bc814c92af3e8f8afe77e3922ebfdec Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Thu, 6 Mar 2025 19:16:47 +0100 Subject: [PATCH 151/530] Allow disabling the brightness control commands (closes #664) Signed-off-by: AnErrupTion --- res/config.ini | 4 +-- src/config/Config.zig | 4 +-- src/main.zig | 66 +++++++++++++++++++++++++------------------ 3 files changed, 43 insertions(+), 31 deletions(-) diff --git a/res/config.ini b/res/config.ini index cef6040..b568697 100644 --- a/res/config.ini +++ b/res/config.ini @@ -58,13 +58,13 @@ box_title = null # Brightness increase command brightness_down_cmd = $PREFIX_DIRECTORY/bin/brightnessctl -q s 10%- -# Brightness decrease key +# Brightness decrease key, or null to disable brightness_down_key = F5 # Brightness increase command brightness_up_cmd = $PREFIX_DIRECTORY/bin/brightnessctl -q s +10% -# Brightness increase key +# Brightness increase key, or null to disable brightness_up_key = F6 # Erase password input on failure diff --git a/src/config/Config.zig b/src/config/Config.zig index e5f57d1..5b8de0f 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -16,9 +16,9 @@ blank_box: bool = true, border_fg: u32 = 0x00FFFFFF, box_title: ?[]const u8 = null, brightness_down_cmd: [:0]const u8 = build_options.prefix_directory ++ "/bin/brightnessctl -q s 10%-", -brightness_down_key: []const u8 = "F5", +brightness_down_key: ?[]const u8 = "F5", brightness_up_cmd: [:0]const u8 = build_options.prefix_directory ++ "/bin/brightnessctl -q s +10%", -brightness_up_key: []const u8 = "F6", +brightness_up_key: ?[]const u8 = "F6", clear_password: bool = false, clock: ?[:0]const u8 = null, cmatrix_fg: u32 = 0x0000FF00, diff --git a/src/main.zig b/src/main.zig index 39f1bfd..a22daa6 100644 --- a/src/main.zig +++ b/src/main.zig @@ -357,9 +357,9 @@ pub fn main() !void { const restart_len = try utils.strWidth(lang.restart); const sleep_key = try std.fmt.parseInt(u8, config.sleep_key[1..], 10); const sleep_len = try utils.strWidth(lang.sleep); - const brightness_down_key = try std.fmt.parseInt(u8, config.brightness_down_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_down_len = try utils.strWidth(lang.brightness_down); - const brightness_up_key = try std.fmt.parseInt(u8, config.brightness_up_key[1..], 10); + const brightness_up_key = if (config.brightness_down_key) |key| try std.fmt.parseInt(u8, key[1..], 10) else null; const brightness_up_len = try utils.strWidth(lang.brightness_up); var event: termbox.tb_event = undefined; @@ -503,19 +503,23 @@ pub fn main() !void { length += sleep_len + 1; } - buffer.drawLabel(config.brightness_down_key, length, 0); - length += config.brightness_down_key.len + 1; - buffer.drawLabel(" ", length - 1, 0); + if (config.brightness_down_key) |key| { + buffer.drawLabel(key, length, 0); + length += key.len + 1; + buffer.drawLabel(" ", length - 1, 0); - buffer.drawLabel(lang.brightness_down, length, 0); - length += brightness_down_len + 1; + buffer.drawLabel(lang.brightness_down, length, 0); + length += brightness_down_len + 1; + } - buffer.drawLabel(config.brightness_up_key, length, 0); - length += config.brightness_up_key.len + 1; - buffer.drawLabel(" ", length - 1, 0); + if (config.brightness_up_key) |key| { + buffer.drawLabel(key, length, 0); + length += key.len + 1; + buffer.drawLabel(" ", length - 1, 0); - buffer.drawLabel(lang.brightness_up, length, 0); - length += brightness_up_len + 1; + buffer.drawLabel(lang.brightness_up, length, 0); + length += brightness_up_len + 1; + } } if (config.box_title) |title| { @@ -627,21 +631,14 @@ pub fn main() !void { } } } - } else if (pressed_key == brightness_down_key or pressed_key == brightness_up_key) { - const cmd = if (pressed_key == brightness_down_key) config.brightness_down_cmd else config.brightness_up_cmd; - - 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) { - try info_line.addMessage(lang.err_brightness_change, config.error_bg, config.error_fg); - } - } + } else if (brightness_down_key != null and pressed_key == brightness_down_key.?) { + adjustBrightness(allocator, config.brightness_down_cmd) catch { + try info_line.addMessage(lang.err_brightness_change, config.error_bg, config.error_fg); + }; + } else if (brightness_up_key != null and pressed_key == brightness_up_key.?) { + adjustBrightness(allocator, config.brightness_up_cmd) catch { + try info_line.addMessage(lang.err_brightness_change, config.error_bg, config.error_fg); + }; } }, termbox.TB_KEY_CTRL_C => run = false, @@ -832,6 +829,21 @@ pub fn main() !void { } } +fn adjustBrightness(allocator: std.mem.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; + } + } +} + fn getAuthErrorMsg(err: anyerror, lang: Lang) []const u8 { return switch (err) { error.GetPasswordNameFailed => lang.err_pwnam, From 593a7751488a4100296c5885a8099f6acb8446a1 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Thu, 6 Mar 2025 19:36:42 +0100 Subject: [PATCH 152/530] Use unsigned integers only in Matrix animation Signed-off-by: AnErrupTion --- src/animations/Matrix.zig | 47 +++++++++++++++++++-------------------- 1 file changed, 23 insertions(+), 24 deletions(-) diff --git a/src/animations/Matrix.zig b/src/animations/Matrix.zig index d978f4b..3576245 100644 --- a/src/animations/Matrix.zig +++ b/src/animations/Matrix.zig @@ -7,11 +7,11 @@ const utils = @import("../tui/utils.zig"); const interop = @import("../interop.zig"); const termbox = interop.termbox; -pub const FRAME_DELAY: u64 = 8; +pub const FRAME_DELAY: usize = 8; // Allowed codepoints -pub const MIN_CODEPOINT: isize = 33; -pub const MAX_CODEPOINT: isize = 123 - MIN_CODEPOINT; +pub const MIN_CODEPOINT: u16 = 33; +pub const MAX_CODEPOINT: u16 = 123 - MIN_CODEPOINT; // Characters change mid-scroll pub const MID_SCROLL_CHANGE = true; @@ -19,22 +19,22 @@ pub const MID_SCROLL_CHANGE = true; const Matrix = @This(); pub const Dot = struct { - value: isize, + value: ?usize, is_head: bool, }; pub const Line = struct { - space: isize, - length: isize, - update: isize, + space: usize, + length: usize, + update: usize, }; allocator: Allocator, terminal_buffer: *TerminalBuffer, dots: []Dot, lines: []Line, -frame: u64, -count: u64, +frame: usize, +count: usize, fg_ini: u32, pub fn init(allocator: Allocator, terminal_buffer: *TerminalBuffer, fg_ini: u32) !Matrix { @@ -84,12 +84,12 @@ pub fn draw(self: *Matrix) void { var line = &self.lines[x]; if (self.frame <= line.update) continue; - if (self.dots[x].value == -1 and self.dots[self.terminal_buffer.width + x].value == ' ') { + if (self.dots[x].value == null and self.dots[self.terminal_buffer.width + x].value == ' ') { if (line.space > 0) { line.space -= 1; } else { - const randint = self.terminal_buffer.random.int(i16); - const h: isize = @intCast(self.terminal_buffer.height); + const randint = self.terminal_buffer.random.int(u16); + const h = self.terminal_buffer.height; line.length = @mod(randint, h - 3) + 3; self.dots[x].value = @mod(randint, MAX_CODEPOINT) + MIN_CODEPOINT; line.space = @mod(randint, h + 1); @@ -102,7 +102,7 @@ pub fn draw(self: *Matrix) void { height_it: while (y <= buf_height) : (y += 1) { var dot = &self.dots[buf_width * y + x]; // Skip over spaces - while (y <= buf_height and (dot.value == ' ' or dot.value == -1)) { + while (y <= buf_height and (dot.value == ' ' or dot.value == null)) { y += 1; if (y > buf_height) break :height_it; dot = &self.dots[buf_width * y + x]; @@ -111,10 +111,10 @@ pub fn draw(self: *Matrix) void { // Find the head of this column tail = y; seg_len = 0; - while (y <= buf_height and dot.value != ' ' and dot.value != -1) { + while (y <= buf_height and dot.value != ' ' and dot.value != null) { dot.is_head = false; if (MID_SCROLL_CHANGE) { - const randint = self.terminal_buffer.random.int(i16); + const randint = self.terminal_buffer.random.int(u16); if (@mod(randint, 8) == 0) { dot.value = @mod(randint, MAX_CODEPOINT) + MIN_CODEPOINT; } @@ -130,13 +130,13 @@ pub fn draw(self: *Matrix) void { dot = &self.dots[buf_width * y + x]; } - const randint = self.terminal_buffer.random.int(i16); + const randint = self.terminal_buffer.random.int(u16); dot.value = @mod(randint, MAX_CODEPOINT) + MIN_CODEPOINT; dot.is_head = true; if (seg_len > line.length or !first_col) { self.dots[buf_width * tail + x].value = ' '; - self.dots[x].value = -1; + self.dots[x].value = null; } first_col = false; } @@ -151,13 +151,13 @@ pub fn draw(self: *Matrix) void { var fg = self.fg_ini; - if (dot.value == -1 or dot.value == ' ') { + if (dot.value == null or dot.value == ' ') { utils.putCell(x, y - 1, .{ .ch = ' ', .fg = fg, .bg = termbox.TB_DEFAULT }); continue; } if (dot.is_head) fg = @intCast(0x00FFFFFF | termbox.TB_BOLD); // White and bold - utils.putCell(x, y - 1, .{ .ch = @intCast(dot.value), .fg = fg, .bg = termbox.TB_DEFAULT }); + utils.putCell(x, y - 1, .{ .ch = @intCast(dot.value.?), .fg = fg, .bg = termbox.TB_DEFAULT }); } } } @@ -167,17 +167,16 @@ fn initBuffers(dots: []Dot, lines: []Line, width: usize, height: usize, random: while (y <= height) : (y += 1) { var x: usize = 0; while (x < width) : (x += 2) { - dots[y * width + x].value = -1; + dots[y * width + x].value = null; } } var x: usize = 0; while (x < width) : (x += 2) { var line = lines[x]; - const h: isize = @intCast(height); - line.space = @mod(random.int(i16), h) + 1; - line.length = @mod(random.int(i16), h - 3) + 3; - line.update = @mod(random.int(i16), 3) + 1; + line.space = @mod(random.int(u16), height) + 1; + line.length = @mod(random.int(u16), height - 3) + 3; + line.update = @mod(random.int(u16), 3) + 1; lines[x] = line; dots[width + x].value = ' '; From d12fa271683b11ac78e5b6807808fd5fe04644ba Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Thu, 6 Mar 2025 19:41:05 +0100 Subject: [PATCH 153/530] Added new error and updated French translation Signed-off-by: AnErrupTion --- res/lang/cat.ini | 1 + res/lang/cs.ini | 1 + res/lang/de.ini | 1 + res/lang/en.ini | 5 +++-- res/lang/es.ini | 1 + res/lang/fr.ini | 9 +++++---- res/lang/it.ini | 1 + res/lang/normalize_lang_files.py | 0 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 | 1 + res/lang/tr.ini | 1 + res/lang/uk.ini | 1 + src/config/Lang.zig | 1 + src/main.zig | 2 +- 19 files changed, 24 insertions(+), 7 deletions(-) mode change 100644 => 100755 res/lang/normalize_lang_files.py diff --git a/res/lang/cat.ini b/res/lang/cat.ini index 541898b..c7c50fb 100644 --- a/res/lang/cat.ini +++ b/res/lang/cat.ini @@ -37,6 +37,7 @@ 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_unknown = ha ocorregut un error desconegut err_user_gid = error en establir el GID de l'usuari err_user_init = error en inicialitzar usuari diff --git a/res/lang/cs.ini b/res/lang/cs.ini index 5beac01..8b2d649 100644 --- a/res/lang/cs.ini +++ b/res/lang/cs.ini @@ -38,6 +38,7 @@ err_perm_user = nepodařilo se snížit uživatelská oprávnění err_pwnam = nelze získat informace o uživateli + err_user_gid = nastavení GID uživatele selhalo err_user_init = inicializace uživatele selhala err_user_uid = nastavení UID uživateli selhalo diff --git a/res/lang/de.ini b/res/lang/de.ini index 30a8f44..f57b042 100644 --- a/res/lang/de.ini +++ b/res/lang/de.ini @@ -38,6 +38,7 @@ err_perm_user = Fehler beim heruntersetzen der Nutzer Berechtigungen err_pwnam = Holen der Benutzerinformationen fehlgeschlagen + err_user_gid = Fehler beim setzen der Gruppen Id des Nutzers err_user_init = Initialisierung des Nutzers fehlgeschlagen err_user_uid = Setzen der Benutzer Id fehlgeschlagen diff --git a/res/lang/en.ini b/res/lang/en.ini index f08cca1..b3ff320 100644 --- a/res/lang/en.ini +++ b/res/lang/en.ini @@ -6,7 +6,7 @@ err_alloc = failed memory allocation err_bounds = out-of-bounds index err_brightness_change = failed to change brightness err_chdir = failed to open home folder - +err_config = unable to parse config file err_console_dev = failed to access console err_dgn_oob = log message err_domain = invalid domain @@ -37,6 +37,7 @@ err_perm_group = failed to downgrade group permissions err_perm_user = failed to downgrade user permissions err_pwnam = failed to get user info err_sleep = failed to execute sleep command +err_tty_ctrl = tty control transfer failed err_unknown = an unknown error occurred err_user_gid = failed to set user GID err_user_init = failed to initialize user @@ -51,7 +52,7 @@ logout = logged out no_x11_support = x11 support disabled at compile-time normal = normal numlock = numlock - +other = other password = password restart = reboot shell = shell diff --git a/res/lang/es.ini b/res/lang/es.ini index 23ec27c..abb5156 100644 --- a/res/lang/es.ini +++ b/res/lang/es.ini @@ -38,6 +38,7 @@ err_perm_user = error al degradar los permisos del usuario err_pwnam = error al obtener la información del usuario + 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 diff --git a/res/lang/fr.ini b/res/lang/fr.ini index a2f4566..852f227 100644 --- a/res/lang/fr.ini +++ b/res/lang/fr.ini @@ -1,4 +1,4 @@ - +authenticating = authentification... brightness_down = diminuer la luminosité brightness_up = augmenter la luminosité capslock = verr.maj @@ -6,7 +6,7 @@ err_alloc = échec d'allocation mémoire err_bounds = indice hors-limite err_brightness_change = échec du changement de luminosité err_chdir = échec de l'ouverture du répertoire home - +err_config = échec de lecture du fichier de configuration err_console_dev = échec d'accès à la console err_dgn_oob = message err_domain = domaine invalide @@ -36,7 +36,8 @@ err_perm_dir = échec de changement de répertoire err_perm_group = échec du déclassement des permissions de groupe err_perm_user = échec du déclassement des permissions utilisateur err_pwnam = échec de lecture des infos utilisateur - +err_sleep = échec de l'exécution de la commande de veille +err_tty_ctrl = échec du transfert de contrôle du terminal err_unknown = une erreur inconnue est survenue err_user_gid = échec de modification du GID err_user_init = échec d'initialisation de l'utilisateur @@ -51,7 +52,7 @@ logout = déconnecté no_x11_support = support pour x11 désactivé lors de la compilation normal = normal numlock = verr.num - +other = autre password = mot de passe restart = redémarrer shell = shell diff --git a/res/lang/it.ini b/res/lang/it.ini index e9ba7c0..d594193 100644 --- a/res/lang/it.ini +++ b/res/lang/it.ini @@ -38,6 +38,7 @@ err_perm_user = impossibile ridurre permessi utente err_pwnam = impossibile ottenere dati utente + err_user_gid = impossibile impostare GID utente err_user_init = impossibile inizializzare utente err_user_uid = impossible impostare UID utente diff --git a/res/lang/normalize_lang_files.py b/res/lang/normalize_lang_files.py old mode 100644 new mode 100755 diff --git a/res/lang/pl.ini b/res/lang/pl.ini index 37d4ff2..9d78b22 100644 --- a/res/lang/pl.ini +++ b/res/lang/pl.ini @@ -38,6 +38,7 @@ err_perm_user = nie udało się obniżyć uprawnień użytkownika err_pwnam = nie udało się uzyskać informacji o użytkowniku + 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 diff --git a/res/lang/pt.ini b/res/lang/pt.ini index 5d117d7..e7dff51 100644 --- a/res/lang/pt.ini +++ b/res/lang/pt.ini @@ -38,6 +38,7 @@ err_perm_user = erro ao reduzir as permissões do utilizador err_pwnam = erro ao obter informação do utilizador + 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 diff --git a/res/lang/pt_BR.ini b/res/lang/pt_BR.ini index f47fc00..ab03864 100644 --- a/res/lang/pt_BR.ini +++ b/res/lang/pt_BR.ini @@ -38,6 +38,7 @@ 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_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 diff --git a/res/lang/ro.ini b/res/lang/ro.ini index 89e92be..43eb1a4 100644 --- a/res/lang/ro.ini +++ b/res/lang/ro.ini @@ -46,6 +46,7 @@ err_perm_user = nu s-a putut face downgrade permisiunilor de utilizator + login = utilizator logout = opreşte sesiunea diff --git a/res/lang/ru.ini b/res/lang/ru.ini index a67568a..9791065 100644 --- a/res/lang/ru.ini +++ b/res/lang/ru.ini @@ -38,6 +38,7 @@ err_perm_user = не удалось понизить права доступа err_pwnam = не удалось получить информацию о пользователе + err_user_gid = не удалось установить GID пользователя err_user_init = не удалось инициализировать пользователя err_user_uid = не удалось установить UID пользователя diff --git a/res/lang/sr.ini b/res/lang/sr.ini index 96b73d8..f5ccea8 100644 --- a/res/lang/sr.ini +++ b/res/lang/sr.ini @@ -38,6 +38,7 @@ 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 diff --git a/res/lang/sv.ini b/res/lang/sv.ini index 7ace439..0a3de58 100644 --- a/res/lang/sv.ini +++ b/res/lang/sv.ini @@ -38,6 +38,7 @@ 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 diff --git a/res/lang/tr.ini b/res/lang/tr.ini index 32dae6e..3e7ce4c 100644 --- a/res/lang/tr.ini +++ b/res/lang/tr.ini @@ -38,6 +38,7 @@ err_perm_user = kullanici izinleri dusurulemedi err_pwnam = kullanici bilgileri alinamadi + err_user_gid = kullanici icin GID ayarlanamadi err_user_init = kullanici oturumu baslatilamadi err_user_uid = kullanici icin UID ayarlanamadi diff --git a/res/lang/uk.ini b/res/lang/uk.ini index 76d48b5..dde7da7 100644 --- a/res/lang/uk.ini +++ b/res/lang/uk.ini @@ -38,6 +38,7 @@ err_perm_user = не вдалося понизити права доступу err_pwnam = не вдалося отримати дані користувача + err_user_gid = не вдалося змінити GID користувача err_user_init = не вдалося ініціалізувати користувача err_user_uid = не вдалося змінити UID користувача diff --git a/src/config/Lang.zig b/src/config/Lang.zig index a8ff9fc..389c4ac 100644 --- a/src/config/Lang.zig +++ b/src/config/Lang.zig @@ -42,6 +42,7 @@ err_perm_group: []const u8 = "failed to downgrade group permissions", err_perm_user: []const u8 = "failed to downgrade user permissions", err_pwnam: []const u8 = "failed to get user info", err_sleep: []const u8 = "failed to execute sleep command", +err_tty_ctrl: []const u8 = "tty control transfer failed", err_unknown: []const u8 = "an unknown error occurred", err_user_gid: []const u8 = "failed to set user GID", err_user_init: []const u8 = "failed to initialize user", diff --git a/src/main.zig b/src/main.zig index a22daa6..16a1e39 100644 --- a/src/main.zig +++ b/src/main.zig @@ -854,7 +854,7 @@ fn getAuthErrorMsg(err: anyerror, lang: Lang) []const u8 { error.SetUserGidFailed => lang.err_user_gid, error.SetUserUidFailed => lang.err_user_uid, error.ChangeDirectoryFailed => lang.err_perm_dir, - error.TtyControlTransferFailed => "tty control transfer failed", + error.TtyControlTransferFailed => lang.err_tty_ctrl, error.SetPathFailed => lang.err_path, error.PamAccountExpired => lang.err_pam_acct_expired, error.PamAuthError => lang.err_pam_auth, From d80ec8fd1fa0ee023054a38114de461a6ad546b1 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Thu, 6 Mar 2025 19:48:11 +0100 Subject: [PATCH 154/530] Allow changing matrix animation min/max codepoints (closes #615) Signed-off-by: AnErrupTion --- res/config.ini | 8 ++++++++ src/animations/Matrix.zig | 16 ++++++++-------- src/config/Config.zig | 2 ++ src/main.zig | 2 +- 4 files changed, 19 insertions(+), 9 deletions(-) diff --git a/res/config.ini b/res/config.ini index b568697..3ff7bb3 100644 --- a/res/config.ini +++ b/res/config.ini @@ -77,6 +77,14 @@ clock = null # CMatrix animation foreground color id cmatrix_fg = 0x0000FF00 +# CMatrix animation minimum codepoint. It uses a 16-bit integer +# For Japanese characters for example, you can use 0x3000 here +cmatrix_min_codepoint = 0x21 + +# CMatrix animation maximum codepoint. It uses a 16-bit integer +# For Japanese characters for example, you can use 0x30FF here +cmatrix_max_codepoint = 0x7B + # Color mixing animation first color id colormix_col1 = 0x00FF0000 diff --git a/src/animations/Matrix.zig b/src/animations/Matrix.zig index 3576245..18636fc 100644 --- a/src/animations/Matrix.zig +++ b/src/animations/Matrix.zig @@ -9,10 +9,6 @@ const termbox = interop.termbox; pub const FRAME_DELAY: usize = 8; -// Allowed codepoints -pub const MIN_CODEPOINT: u16 = 33; -pub const MAX_CODEPOINT: u16 = 123 - MIN_CODEPOINT; - // Characters change mid-scroll pub const MID_SCROLL_CHANGE = true; @@ -36,8 +32,10 @@ lines: []Line, frame: usize, count: usize, fg_ini: u32, +min_codepoint: u16, +max_codepoint: u16, -pub fn init(allocator: Allocator, terminal_buffer: *TerminalBuffer, fg_ini: u32) !Matrix { +pub fn init(allocator: Allocator, terminal_buffer: *TerminalBuffer, fg_ini: u32, min_codepoint: u16, max_codepoint: u16) !Matrix { const dots = try allocator.alloc(Dot, terminal_buffer.width * (terminal_buffer.height + 1)); const lines = try allocator.alloc(Line, terminal_buffer.width); @@ -51,6 +49,8 @@ pub fn init(allocator: Allocator, terminal_buffer: *TerminalBuffer, fg_ini: u32) .frame = 3, .count = 0, .fg_ini = fg_ini, + .min_codepoint = min_codepoint, + .max_codepoint = max_codepoint - min_codepoint, }; } @@ -91,7 +91,7 @@ pub fn draw(self: *Matrix) void { const randint = self.terminal_buffer.random.int(u16); const h = self.terminal_buffer.height; line.length = @mod(randint, h - 3) + 3; - self.dots[x].value = @mod(randint, MAX_CODEPOINT) + MIN_CODEPOINT; + self.dots[x].value = @mod(randint, self.max_codepoint) + self.min_codepoint; line.space = @mod(randint, h + 1); } } @@ -116,7 +116,7 @@ pub fn draw(self: *Matrix) void { if (MID_SCROLL_CHANGE) { const randint = self.terminal_buffer.random.int(u16); if (@mod(randint, 8) == 0) { - dot.value = @mod(randint, MAX_CODEPOINT) + MIN_CODEPOINT; + dot.value = @mod(randint, self.max_codepoint) + self.min_codepoint; } } @@ -131,7 +131,7 @@ pub fn draw(self: *Matrix) void { } const randint = self.terminal_buffer.random.int(u16); - dot.value = @mod(randint, MAX_CODEPOINT) + MIN_CODEPOINT; + dot.value = @mod(randint, self.max_codepoint) + self.min_codepoint; dot.is_head = true; if (seg_len > line.length or !first_col) { diff --git a/src/config/Config.zig b/src/config/Config.zig index 5b8de0f..ec5bd9e 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -22,6 +22,8 @@ brightness_up_key: ?[]const u8 = "F6", clear_password: bool = false, clock: ?[:0]const u8 = null, cmatrix_fg: u32 = 0x0000FF00, +cmatrix_min_codepoint: u16 = 0x21, +cmatrix_max_codepoint: u16 = 0x7B, colormix_col1: u32 = 0x00FF0000, colormix_col2: u32 = 0x000000FF, colormix_col3: u32 = 0x20000000, diff --git a/src/main.zig b/src/main.zig index 16a1e39..69830c0 100644 --- a/src/main.zig +++ b/src/main.zig @@ -338,7 +338,7 @@ pub fn main() !void { switch (config.animation) { .none => {}, .doom => doom = try Doom.init(allocator, &buffer), - .matrix => matrix = try Matrix.init(allocator, &buffer, config.cmatrix_fg), + .matrix => matrix = try Matrix.init(allocator, &buffer, config.cmatrix_fg, config.cmatrix_min_codepoint, config.cmatrix_max_codepoint), .colormix => color_mix = ColorMix.init(&buffer, config.colormix_col1, config.colormix_col2, config.colormix_col3), } defer { From 78d64ad2a72f3c7d49200dce3c0318d76df24d60 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Thu, 6 Mar 2025 20:06:28 +0100 Subject: [PATCH 155/530] Possibly fix .Xresources not being loaded (closes #600) Signed-off-by: AnErrupTion --- res/setup.sh | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/res/setup.sh b/res/setup.sh index 549994a..8ec0f00 100755 --- a/res/setup.sh +++ b/res/setup.sh @@ -89,6 +89,10 @@ if [ "$XDG_SESSION_TYPE" = "x11" ]; then done fi + if [ -f "$USERXSESSION" ]; then + . "$USERXSESSION" + fi + if [ -d "$CONFIG_DIRECTORY"/X11/Xresources ]; then for i in "$CONFIG_DIRECTORY"/X11/Xresources/*; do [ -f "$i" ] && xrdb -merge "$i" @@ -98,10 +102,6 @@ if [ "$XDG_SESSION_TYPE" = "x11" ]; then fi [ -f "$HOME"/.Xresources ] && xrdb -merge "$HOME"/.Xresources [ -f "$XDG_CONFIG_HOME"/X11/Xresources ] && xrdb -merge "$XDG_CONFIG_HOME"/X11/Xresources - - if [ -f "$USERXSESSION" ]; then - . "$USERXSESSION" - fi fi exec "$@" From 4e859e56cb103b40bb5282f3bcf44c319cc4d3d8 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Thu, 6 Mar 2025 20:40:53 +0100 Subject: [PATCH 156/530] Allow modifying DOOM animation fire colors (closes #239) Signed-off-by: AnErrupTion --- res/config.ini | 9 +++++++++ src/animations/Doom.zig | 43 +++++++++++++++++++++-------------------- src/config/Config.zig | 3 +++ src/main.zig | 2 +- 4 files changed, 35 insertions(+), 22 deletions(-) diff --git a/res/config.ini b/res/config.ini index 3ff7bb3..b6def9a 100644 --- a/res/config.ini +++ b/res/config.ini @@ -101,6 +101,15 @@ console_dev = /dev/console # Available inputs: info_line, session, login, password default_input = login +# DOOM animation top color (low intensity flames) +doom_top_color = 0x00FF0000 + +# DOOM animation middle color (medium intensity flames) +doom_middle_color = 0x00FFFF00 + +# DOOM animation bottom color (high intensity flames) +doom_bottom_color = 0x00FFFFFF + # Error background color id error_bg = 0x00000000 diff --git a/src/animations/Doom.zig b/src/animations/Doom.zig index 94df92c..6b0d46a 100644 --- a/src/animations/Doom.zig +++ b/src/animations/Doom.zig @@ -5,28 +5,14 @@ const utils = @import("../tui/utils.zig"); const Doom = @This(); -pub const STEPS = 13; -pub const FIRE = [_]utils.Cell{ - utils.initCell(' ', 9, 0), - utils.initCell(0x2591, 0x00FF0000, 0), // Red - utils.initCell(0x2592, 0x00FF0000, 0), // Red - utils.initCell(0x2593, 0x00FF0000, 0), // Red - utils.initCell(0x2588, 0x00FF0000, 0), // Red - utils.initCell(0x2591, 0x00FFFF00, 2), // Yellow - utils.initCell(0x2592, 0x00FFFF00, 2), // Yellow - utils.initCell(0x2593, 0x00FFFF00, 2), // Yellow - utils.initCell(0x2588, 0x00FFFF00, 2), // Yellow - utils.initCell(0x2591, 0x00FFFFFF, 4), // White - utils.initCell(0x2592, 0x00FFFFFF, 4), // White - utils.initCell(0x2593, 0x00FFFFFF, 4), // White - utils.initCell(0x2588, 0x00FFFFFF, 4), // White -}; +pub const STEPS = 12; allocator: Allocator, terminal_buffer: *TerminalBuffer, buffer: []u8, +fire: [STEPS + 1]utils.Cell, -pub fn init(allocator: Allocator, terminal_buffer: *TerminalBuffer) !Doom { +pub fn init(allocator: Allocator, terminal_buffer: *TerminalBuffer, top_color: u32, middle_color: u32, bottom_color: u32) !Doom { const buffer = try allocator.alloc(u8, terminal_buffer.width * terminal_buffer.height); initBuffer(buffer, terminal_buffer.width); @@ -34,6 +20,21 @@ pub fn init(allocator: Allocator, terminal_buffer: *TerminalBuffer) !Doom { .allocator = allocator, .terminal_buffer = terminal_buffer, .buffer = buffer, + .fire = [_]utils.Cell{ + utils.initCell(' ', 0x00000000, 0), + utils.initCell(0x2591, top_color, 0), + utils.initCell(0x2592, top_color, 0), + utils.initCell(0x2593, top_color, 0), + utils.initCell(0x2588, top_color, 0), + utils.initCell(0x2591, middle_color, 2), + utils.initCell(0x2592, middle_color, 2), + utils.initCell(0x2593, middle_color, 2), + utils.initCell(0x2588, middle_color, 2), + utils.initCell(0x2591, bottom_color, 4), + utils.initCell(0x2592, bottom_color, 4), + utils.initCell(0x2593, bottom_color, 4), + utils.initCell(0x2588, bottom_color, 4), + }, }; } @@ -62,16 +63,16 @@ pub fn draw(self: Doom) void { if (buffer_source < buffer_dest_offset) continue; var buffer_dest = buffer_source - buffer_dest_offset; - if (buffer_dest > 12) buffer_dest = 0; + if (buffer_dest > STEPS) buffer_dest = 0; self.buffer[dest] = @intCast(buffer_dest); const dest_y = dest / self.terminal_buffer.width; const dest_x = dest % self.terminal_buffer.width; - utils.putCell(dest_x, dest_y, FIRE[buffer_dest]); + utils.putCell(dest_x, dest_y, self.fire[buffer_dest]); const source_y = source / self.terminal_buffer.width; const source_x = source % self.terminal_buffer.width; - utils.putCell(source_x, source_y, FIRE[buffer_source]); + utils.putCell(source_x, source_y, self.fire[buffer_source]); } } } @@ -82,5 +83,5 @@ fn initBuffer(buffer: []u8, width: usize) void { const slice_end = buffer[length..]; @memset(slice_start, 0); - @memset(slice_end, STEPS - 1); + @memset(slice_end, STEPS); } diff --git a/src/config/Config.zig b/src/config/Config.zig index ec5bd9e..0478173 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -29,6 +29,9 @@ colormix_col2: u32 = 0x000000FF, colormix_col3: u32 = 0x20000000, console_dev: []const u8 = "/dev/console", default_input: Input = .login, +doom_top_color: u32 = 0x00FF0000, +doom_middle_color: u32 = 0x00FFFF00, +doom_bottom_color: u32 = 0x00FFFFFF, error_bg: u32 = 0x00000000, error_fg: u32 = 0x01FF0000, fg: u32 = 0x00FFFFFF, diff --git a/src/main.zig b/src/main.zig index 69830c0..73f439b 100644 --- a/src/main.zig +++ b/src/main.zig @@ -337,7 +337,7 @@ pub fn main() !void { switch (config.animation) { .none => {}, - .doom => doom = try Doom.init(allocator, &buffer), + .doom => doom = try Doom.init(allocator, &buffer, config.doom_top_color, config.doom_middle_color, config.doom_bottom_color), .matrix => matrix = try Matrix.init(allocator, &buffer, config.cmatrix_fg, config.cmatrix_min_codepoint, config.cmatrix_max_codepoint), .colormix => color_mix = ColorMix.init(&buffer, config.colormix_col1, config.colormix_col2, config.colormix_col3), } From f2ca72eace425cc02b758d04d89cac9a1e43590f Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Thu, 6 Mar 2025 21:08:05 +0100 Subject: [PATCH 157/530] Add (incomplete) Chinese translation, thanks @eonun! (closes #194) Signed-off-by: AnErrupTion --- res/lang/zh_CN.ini | 63 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 res/lang/zh_CN.ini diff --git a/res/lang/zh_CN.ini b/res/lang/zh_CN.ini new file mode 100644 index 0000000..2460209 --- /dev/null +++ b/res/lang/zh_CN.ini @@ -0,0 +1,63 @@ + + + +capslock = 大写锁定 +err_alloc = 内存分配失败 +err_bounds = 索引越界 + +err_chdir = 无法打开home文件夹 + +err_console_dev = 无法访问控制台 +err_dgn_oob = 日志消息 +err_domain = 无效的域 + +err_hostname = 获取主机名失败 +err_mlock = 锁定密码存储器失败 +err_null = 空指针 + +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_user_gid = 设置用户GID失败 +err_user_init = 初始化用户失败 +err_user_uid = 设置用户UID失败 + + +err_xsessions_dir = 找不到会话文件夹 +err_xsessions_open = 无法打开会话文件夹 + +login = 登录 +logout = 注销 + + +numlock = 数字锁定 + +password = 密码 + +shell = shell + + +wayland = wayland +x11 = x11 +xinitrc = xinitrc From e19a23b54c680223df83a47bcc592ad5080dce2f Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Thu, 6 Mar 2025 21:16:24 +0100 Subject: [PATCH 158/530] Allow using up to a UTF-32 codepoint as a password asterisk (closes #715) Signed-off-by: AnErrupTion --- res/config.ini | 2 ++ src/config/Config.zig | 2 +- src/tui/TerminalBuffer.zig | 2 +- src/tui/components/Text.zig | 4 ++-- 4 files changed, 6 insertions(+), 4 deletions(-) diff --git a/res/config.ini b/res/config.ini index b6def9a..6dd7af3 100644 --- a/res/config.ini +++ b/res/config.ini @@ -28,6 +28,8 @@ animation = none animation_timeout_sec = 0 # The character used to mask the password +# You can either type it directly as a UTF-8 character (like *), or use a UTF-32 +# codepoint (for example 0x2022 for a bullet point) # If null, the password will be hidden # Note: you can use a # by escaping it like so: \# asterisk = * diff --git a/src/config/Config.zig b/src/config/Config.zig index 0478173..6e4c6eb 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -8,7 +8,7 @@ const Bigclock = enums.Bigclock; animation: Animation = .none, animation_timeout_sec: u12 = 0, -asterisk: ?u8 = '*', +asterisk: ?u32 = '*', auth_fails: u64 = 10, bg: u32 = 0x00000000, bigclock: Bigclock = .none, diff --git a/src/tui/TerminalBuffer.zig b/src/tui/TerminalBuffer.zig index 8281814..ba75516 100644 --- a/src/tui/TerminalBuffer.zig +++ b/src/tui/TerminalBuffer.zig @@ -202,7 +202,7 @@ pub fn drawConfinedLabel(self: TerminalBuffer, text: []const u8, x: usize, y: us } } -pub fn drawCharMultiple(self: TerminalBuffer, char: u8, x: usize, y: usize, length: usize) void { +pub fn drawCharMultiple(self: TerminalBuffer, char: u32, x: usize, y: usize, length: usize) void { const cell = utils.initCell(char, self.fg, self.bg); for (0..length) |xx| utils.putCell(x + xx, y, cell); } diff --git a/src/tui/components/Text.zig b/src/tui/components/Text.zig index d694eb4..f2502d5 100644 --- a/src/tui/components/Text.zig +++ b/src/tui/components/Text.zig @@ -19,9 +19,9 @@ visible_length: usize, x: usize, y: usize, masked: bool, -maybe_mask: ?u8, +maybe_mask: ?u32, -pub fn init(allocator: Allocator, buffer: *TerminalBuffer, masked: bool, maybe_mask: ?u8) Text { +pub fn init(allocator: Allocator, buffer: *TerminalBuffer, masked: bool, maybe_mask: ?u32) Text { const text = DynamicString.init(allocator); return .{ From 4b9ea3d7cb12678c3e8dd0bad1261bcc66b14a96 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Thu, 6 Mar 2025 21:18:33 +0100 Subject: [PATCH 159/530] Backport: Possibly fix .Xresources not being loaded Signed-off-by: AnErrupTion --- res/xsetup.sh | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/res/xsetup.sh b/res/xsetup.sh index 2c962f5..e73d357 100755 --- a/res/xsetup.sh +++ b/res/xsetup.sh @@ -83,6 +83,10 @@ if [ -d "$xsessionddir" ]; then done fi +if [ -f "$USERXSESSION" ]; then + . "$USERXSESSION" +fi + if [ -d /etc/X11/Xresources ]; then for i in /etc/X11/Xresources/*; do [ -f $i ] && xrdb -merge $i @@ -93,10 +97,6 @@ fi [ -f $HOME/.Xresources ] && xrdb -merge $HOME/.Xresources [ -f $XDG_CONFIG_HOME/X11/Xresources ] && xrdb -merge $XDG_CONFIG_HOME/X11/Xresources -if [ -f "$USERXSESSION" ]; then - . "$USERXSESSION" -fi - if [ -z "$*" ]; then exec xmessage -center -buttons OK:0 -default OK "Sorry, $DESKTOP_SESSION is no valid session." else From 459965439811c3b260fd2479d6a79376afa60aa3 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Thu, 6 Mar 2025 21:41:28 +0100 Subject: [PATCH 160/530] Update issue template for bugs Signed-off-by: AnErrupTion --- .github/ISSUE_TEMPLATE/bug.yml | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug.yml b/.github/ISSUE_TEMPLATE/bug.yml index bccca4d..c0e9165 100644 --- a/.github/ISSUE_TEMPLATE/bug.yml +++ b/.github/ISSUE_TEMPLATE/bug.yml @@ -15,7 +15,7 @@ body: id: version attributes: label: Ly version - description: The output of `ly --version` + description: The output of `ly --version`. Please note that only Ly v1.0.0 and above are supported. placeholder: 1.1.0-dev.12+2b0301c validations: required: true @@ -33,6 +33,13 @@ body: description: What did you expect to happen instead? validations: required: true + - type: textarea + id: expected + attributes: + label: Desktop environment/Window manager + description: Which DE or WM did you use when observing the problem? + validations: + required: true - type: textarea id: reproduction attributes: @@ -50,5 +57,12 @@ body: id: logs attributes: label: Relevant logs - description: Please copy and paste any relevant logs, error messages or any other output. This will be automatically formatted into code, so no need for backticks. Screenshots are accepted if they make life easier for you. + description: | + Please copy and paste any relevant logs, error messages or any other output. This will be automatically formatted into code, so no need for backticks. Screenshots are accepted if they make life easier for you. + If you're using the latest code on master (for v1.1.0), including your session log (found at /var/log/ly-session.log unless modified) is a good idea. (But make sure it's relevant!) render: shell + - type: textarea + id: moreinfo + attributes: + label: Additional information + description: If you have any additional information that might be helpful in reproducing the problem, please provide it here. From 932c751ac2c709b50472234cff38cb8d620ef926 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Thu, 6 Mar 2025 21:46:40 +0100 Subject: [PATCH 161/530] Stop redirecting X11 output to session log (closes #693, #688) Signed-off-by: AnErrupTion --- res/config.ini | 5 +++-- src/auth.zig | 7 ++----- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/res/config.ini b/res/config.ini index 6dd7af3..413110d 100644 --- a/res/config.ini +++ b/res/config.ini @@ -183,9 +183,10 @@ save = true service_name = ly # Session log file path -# This will contain stdout and stderr of X11 and Wayland sessions +# This will contain stdout and stderr of Wayland sessions # By default it's saved in the user's home directory -# Note: this file won't be used in a shell session (due to the need of stdout and stderr) +# Important: due to technical limitations, X11 and shell sessions aren't supported, which +# means you won't get any logs from those sessions session_log = ly-session.log # Setup command diff --git a/src/auth.zig b/src/auth.zig index 216784d..e3d8e32 100644 --- a/src/auth.zig +++ b/src/auth.zig @@ -375,9 +375,6 @@ fn xauth(display_name: [:0]u8, shell: [*:0]const u8, pw_dir: [*:0]const u8, opti const pid = try std.posix.fork(); if (pid == 0) { - const log_file = try redirectStandardStreams(options.session_log, true); - defer log_file.close(); - 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); const args = [_:null]?[*:0]const u8{ shell, "-c", cmd_str }; @@ -417,7 +414,7 @@ fn executeX11Cmd(shell: [*:0]const u8, pw_dir: [*:0]const u8, options: AuthOptio 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} >{s} 2>&1", .{ options.x_cmd, display_name, vt, options.session_log }) catch std.process.exit(1); + const cmd_str = std.fmt.bufPrintZ(&cmd_buffer, "{s} {s} {s}", .{ options.x_cmd, display_name, vt }) catch std.process.exit(1); const args = [_:null]?[*:0]const u8{ shell, "-c", cmd_str }; std.posix.execveZ(shell, &args, std.c.environ) catch {}; std.process.exit(1); @@ -440,7 +437,7 @@ fn executeX11Cmd(shell: [*:0]const u8, pw_dir: [*:0]const u8, options: AuthOptio 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} >{s} 2>&1", .{ options.setup_cmd, options.login_cmd orelse "", desktop_cmd, options.session_log }) catch std.process.exit(1); + 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 args = [_:null]?[*:0]const u8{ shell, "-c", cmd_str }; std.posix.execveZ(shell, &args, std.c.environ) catch {}; std.process.exit(1); From c6db79b8730be10625275bb948f4c6aa6a95b22e Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Thu, 6 Mar 2025 21:48:03 +0100 Subject: [PATCH 162/530] Fix issue template Signed-off-by: AnErrupTion --- .github/ISSUE_TEMPLATE/bug.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/bug.yml b/.github/ISSUE_TEMPLATE/bug.yml index c0e9165..caf63fc 100644 --- a/.github/ISSUE_TEMPLATE/bug.yml +++ b/.github/ISSUE_TEMPLATE/bug.yml @@ -34,7 +34,7 @@ body: validations: required: true - type: textarea - id: expected + id: desktop attributes: label: Desktop environment/Window manager description: Which DE or WM did you use when observing the problem? From a058a81ec99f7f7e0794bc41c56cc5eea7c37683 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Thu, 6 Mar 2025 21:48:40 +0100 Subject: [PATCH 163/530] Use proper input text box in issue template Signed-off-by: AnErrupTion --- .github/ISSUE_TEMPLATE/bug.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/bug.yml b/.github/ISSUE_TEMPLATE/bug.yml index caf63fc..9616dcf 100644 --- a/.github/ISSUE_TEMPLATE/bug.yml +++ b/.github/ISSUE_TEMPLATE/bug.yml @@ -33,7 +33,7 @@ body: description: What did you expect to happen instead? validations: required: true - - type: textarea + - type: input id: desktop attributes: label: Desktop environment/Window manager From 6504cd02092d7b92f25ac175bb42fa77d3e86786 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Thu, 6 Mar 2025 21:51:17 +0100 Subject: [PATCH 164/530] Mention OS in issue template 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 9616dcf..3314a84 100644 --- a/.github/ISSUE_TEMPLATE/bug.yml +++ b/.github/ISSUE_TEMPLATE/bug.yml @@ -36,8 +36,8 @@ body: - type: input id: desktop attributes: - label: Desktop environment/Window manager - description: Which DE or WM did you use when observing the problem? + label: OS + Desktop environment/Window manager + description: Which OS and DE (or WM) did you use when observing the problem? validations: required: true - type: textarea From 92845268af0d4babd17db00e7cc9f32ccb9ed8e4 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Thu, 6 Mar 2025 22:00:46 +0100 Subject: [PATCH 165/530] Add option to allow empty password or not (closes #577) Signed-off-by: AnErrupTion --- res/config.ini | 3 +++ 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/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 | 1 + res/lang/tr.ini | 1 + res/lang/uk.ini | 1 + res/lang/zh_CN.ini | 1 + src/config/Config.zig | 1 + src/config/Lang.zig | 1 + src/main.zig | 12 +++++++++++- 21 files changed, 33 insertions(+), 1 deletion(-) diff --git a/res/config.ini b/res/config.ini index 413110d..c197fb8 100644 --- a/res/config.ini +++ b/res/config.ini @@ -15,6 +15,9 @@ # special value 0x00000000. This means that, if you want to use black, you *must* use # the styling option TB_HI_BLACK (the RGB values are ignored when using this option). +# Allow empty password or not when authenticating +allow_empty_password = true + # The active animation # none -> Nothing # doom -> PSX DOOM fire diff --git a/res/lang/cat.ini b/res/lang/cat.ini index c7c50fb..0351223 100644 --- a/res/lang/cat.ini +++ b/res/lang/cat.ini @@ -10,6 +10,7 @@ err_chdir = error en obrir la carpeta home err_console_dev = error en accedir a la consola err_dgn_oob = missatge de registre err_domain = domini invàlid + err_envlist = error en obtenir l'envlist err_hostname = error en obtenir el nom de l'amfitrió err_mlock = error en bloquejar la memòria de clau diff --git a/res/lang/cs.ini b/res/lang/cs.ini index 8b2d649..b2653d3 100644 --- a/res/lang/cs.ini +++ b/res/lang/cs.ini @@ -11,6 +11,7 @@ err_console_dev = chyba při přístupu do konzole err_dgn_oob = zpráva protokolu err_domain = neplatná doména + err_hostname = nelze získat název hostitele err_mlock = uzamčení paměti hesel selhalo err_null = nulový ukazatel diff --git a/res/lang/de.ini b/res/lang/de.ini index f57b042..d7da11e 100644 --- a/res/lang/de.ini +++ b/res/lang/de.ini @@ -11,6 +11,7 @@ err_console_dev = Zugriff auf die Konsole fehlgeschlagen err_dgn_oob = Protokoll Nachricht err_domain = Unzulaessige domain + err_hostname = Holen des Hostnames fehlgeschlagen err_mlock = Abschließen des Passwortspeichers fehlgeschlagen err_null = Null Zeiger diff --git a/res/lang/en.ini b/res/lang/en.ini index b3ff320..49accc5 100644 --- a/res/lang/en.ini +++ b/res/lang/en.ini @@ -10,6 +10,7 @@ err_config = unable to parse config file err_console_dev = failed to access console err_dgn_oob = log message err_domain = invalid domain +err_empty_password = empty password not allowed err_envlist = failed to get envlist err_hostname = failed to get hostname err_mlock = failed to lock password memory diff --git a/res/lang/es.ini b/res/lang/es.ini index abb5156..e1fbab5 100644 --- a/res/lang/es.ini +++ b/res/lang/es.ini @@ -11,6 +11,7 @@ err_console_dev = error al acceder a la consola err_dgn_oob = mensaje de registro err_domain = dominio inválido + err_hostname = error al obtener el nombre de host err_mlock = error al bloquear la contraseña de memoria err_null = puntero nulo diff --git a/res/lang/fr.ini b/res/lang/fr.ini index 852f227..728d0bd 100644 --- a/res/lang/fr.ini +++ b/res/lang/fr.ini @@ -10,6 +10,7 @@ err_config = échec de lecture du fichier de configuration err_console_dev = échec d'accès à la console err_dgn_oob = message err_domain = domaine invalide +err_empty_password = mot de passe vide non autorisé err_envlist = échec de lecture de la liste d'environnement err_hostname = échec de lecture du nom d'hôte err_mlock = échec du verrouillage mémoire diff --git a/res/lang/it.ini b/res/lang/it.ini index d594193..3527f3e 100644 --- a/res/lang/it.ini +++ b/res/lang/it.ini @@ -11,6 +11,7 @@ err_console_dev = impossibile aprire console err_dgn_oob = messaggio log err_domain = dominio non valido + err_hostname = impossibile ottenere hostname err_mlock = impossibile ottenere lock per la password in memoria err_null = puntatore nullo diff --git a/res/lang/pl.ini b/res/lang/pl.ini index 9d78b22..93ec5ba 100644 --- a/res/lang/pl.ini +++ b/res/lang/pl.ini @@ -11,6 +11,7 @@ err_console_dev = nie udało się uzyskać dostępu do konsoli err_dgn_oob = wiadomość loga err_domain = niepoprawna domena + err_hostname = nie udało się uzyskać nazwy hosta err_mlock = nie udało się zablokować pamięci haseł err_null = wskaźnik zerowy diff --git a/res/lang/pt.ini b/res/lang/pt.ini index e7dff51..eaaf8ff 100644 --- a/res/lang/pt.ini +++ b/res/lang/pt.ini @@ -11,6 +11,7 @@ err_console_dev = erro ao aceder à consola err_dgn_oob = mensagem de registo err_domain = domínio inválido + err_hostname = erro ao obter o nome do host err_mlock = erro de bloqueio de memória err_null = ponteiro nulo diff --git a/res/lang/pt_BR.ini b/res/lang/pt_BR.ini index ab03864..33b4437 100644 --- a/res/lang/pt_BR.ini +++ b/res/lang/pt_BR.ini @@ -11,6 +11,7 @@ err_console_dev = não foi possível acessar o console err_dgn_oob = mensagem de log err_domain = domínio inválido + err_hostname = não foi possível obter o nome do host err_mlock = bloqueio da memória de senha malsucedido err_null = ponteiro nulo diff --git a/res/lang/ro.ini b/res/lang/ro.ini index 43eb1a4..3f79c86 100644 --- a/res/lang/ro.ini +++ b/res/lang/ro.ini @@ -16,6 +16,7 @@ err_console_dev = nu s-a putut accesa consola + err_pam_abort = tranzacţie pam anulată err_pam_acct_expired = cont expirat err_pam_auth = eroare de autentificare diff --git a/res/lang/ru.ini b/res/lang/ru.ini index 9791065..3a15f54 100644 --- a/res/lang/ru.ini +++ b/res/lang/ru.ini @@ -11,6 +11,7 @@ err_console_dev = не удалось получить доступ к конс err_dgn_oob = отладочное сообщение (log) err_domain = неверный домен + err_hostname = не удалось получить имя хоста err_mlock = сбой блокировки памяти err_null = нулевой указатель diff --git a/res/lang/sr.ini b/res/lang/sr.ini index f5ccea8..d6cb896 100644 --- a/res/lang/sr.ini +++ b/res/lang/sr.ini @@ -11,6 +11,7 @@ err_console_dev = neuspijesno pristupanje konzoli 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 diff --git a/res/lang/sv.ini b/res/lang/sv.ini index 0a3de58..92143f0 100644 --- a/res/lang/sv.ini +++ b/res/lang/sv.ini @@ -11,6 +11,7 @@ err_console_dev = misslyckades att komma åt konsol 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 diff --git a/res/lang/tr.ini b/res/lang/tr.ini index 3e7ce4c..048fb64 100644 --- a/res/lang/tr.ini +++ b/res/lang/tr.ini @@ -11,6 +11,7 @@ err_console_dev = konsola erisilemedi err_dgn_oob = log mesaji err_domain = gecersiz etki alani + err_hostname = ana bilgisayar adi alinamadi err_mlock = parola bellegi kilitlenemedi err_null = bos isaretci hatasi diff --git a/res/lang/uk.ini b/res/lang/uk.ini index dde7da7..3e1098c 100644 --- a/res/lang/uk.ini +++ b/res/lang/uk.ini @@ -11,6 +11,7 @@ err_console_dev = невдалий доступ до консолі err_dgn_oob = повідомлення журналу (log) err_domain = недійсний домен + err_hostname = не вдалося отримати ім'я хосту err_mlock = збій блокування пам'яті err_null = нульовий вказівник diff --git a/res/lang/zh_CN.ini b/res/lang/zh_CN.ini index 2460209..be970eb 100644 --- a/res/lang/zh_CN.ini +++ b/res/lang/zh_CN.ini @@ -11,6 +11,7 @@ err_console_dev = 无法访问控制台 err_dgn_oob = 日志消息 err_domain = 无效的域 + err_hostname = 获取主机名失败 err_mlock = 锁定密码存储器失败 err_null = 空指针 diff --git a/src/config/Config.zig b/src/config/Config.zig index 6e4c6eb..c0f3411 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -6,6 +6,7 @@ const Input = enums.Input; const ViMode = enums.ViMode; const Bigclock = enums.Bigclock; +allow_empty_password: bool = true, animation: Animation = .none, animation_timeout_sec: u12 = 0, asterisk: ?u32 = '*', diff --git a/src/config/Lang.zig b/src/config/Lang.zig index 389c4ac..fb71b45 100644 --- a/src/config/Lang.zig +++ b/src/config/Lang.zig @@ -15,6 +15,7 @@ err_config: []const u8 = "unable to parse config file", err_console_dev: []const u8 = "failed to access console", err_dgn_oob: []const u8 = "log message", err_domain: []const u8 = "invalid domain", +err_empty_password: []const u8 = "empty password not allowed", err_envlist: []const u8 = "failed to get envlist", err_hostname: []const u8 = "failed to get hostname", err_mlock: []const u8 = "failed to lock password memory", diff --git a/src/main.zig b/src/main.zig index 73f439b..628d2d7 100644 --- a/src/main.zig +++ b/src/main.zig @@ -686,7 +686,17 @@ pub fn main() !void { update = true; }, - termbox.TB_KEY_ENTER => { + termbox.TB_KEY_ENTER => authenticate: { + if (!config.allow_empty_password and password.text.items.len == 0) { + try info_line.addMessage(lang.err_empty_password, config.error_bg, config.error_fg); + InfoLine.clearRendered(allocator, buffer) catch { + try info_line.addMessage(lang.err_alloc, config.error_bg, config.error_fg); + }; + info_line.label.draw(); + _ = termbox.tb_present(); + break :authenticate; + } + try info_line.addMessage(lang.authenticating, config.bg, config.fg); InfoLine.clearRendered(allocator, buffer) catch { try info_line.addMessage(lang.err_alloc, config.error_bg, config.error_fg); From ac1d828a5f07d5c3e35fc334269ae4c83bd9699e Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Thu, 6 Mar 2025 22:02:01 +0100 Subject: [PATCH 166/530] Stop spamming err_console_dev when first time didn't work Signed-off-by: AnErrupTion --- src/main.zig | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main.zig b/src/main.zig index 628d2d7..02c6082 100644 --- a/src/main.zig +++ b/src/main.zig @@ -367,10 +367,12 @@ pub fn main() !void { var update = true; var resolution_changed = false; var auth_fails: u64 = 0; + var can_access_console_dev = true; // Switch to selected TTY if possible interop.switchTty(config.console_dev, config.tty) catch { try info_line.addMessage(lang.err_console_dev, config.error_bg, config.error_fg); + can_access_console_dev = false; }; while (run) { @@ -531,7 +533,7 @@ pub fn main() !void { buffer.drawLabel(label_txt, buffer.box_x, buffer.box_y + buffer.box_height); } - draw_lock_state: { + if (can_access_console_dev) draw_lock_state: { const lock_state = interop.getLockState(config.console_dev) catch { try info_line.addMessage(lang.err_console_dev, config.error_bg, config.error_fg); break :draw_lock_state; From 1eca889e45f1c6c2de999469a95db260363ec155 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Thu, 6 Mar 2025 22:57:24 +0100 Subject: [PATCH 167/530] Confirm LeftWM support (closes #164) Signed-off-by: AnErrupTion --- readme.md | 1 + 1 file changed, 1 insertion(+) diff --git a/readme.md b/readme.md index f9d8cb5..64450b6 100644 --- a/readme.md +++ b/readme.md @@ -46,6 +46,7 @@ The following desktop environments were tested with success: - i3 - kde - labwc + - leftwm - lxde - lxqt - mate From 4dcef65b1cd4889d552f620c014fb1a6a59b8555 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Thu, 6 Mar 2025 23:32:23 +0100 Subject: [PATCH 168/530] Fix X11 session logout 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 e3d8e32..2d2bbf8 100644 --- a/src/auth.zig +++ b/src/auth.zig @@ -455,7 +455,7 @@ fn executeX11Cmd(shell: [*:0]const u8, pw_dir: [*:0]const u8, options: AuthOptio interop.xcb.xcb_disconnect(xcb); std.posix.kill(x_pid, 0) catch return; - std.posix.kill(x_pid, std.posix.SIG.TERM) catch {}; + std.posix.kill(x_pid, std.posix.SIG.KILL) catch {}; var status: c_int = 0; _ = std.c.waitpid(x_pid, &status, 0); From 8fa6b2cec98af3e0c37ee17892fff0d2aeac4ef0 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Fri, 7 Mar 2025 11:03:45 +0100 Subject: [PATCH 169/530] Improve build system commands Signed-off-by: AnErrupTion --- build.zig | 268 +++++++++++++++++++++++++----------------------------- 1 file changed, 122 insertions(+), 146 deletions(-) diff --git a/build.zig b/build.zig index 34e6adf..36635d8 100644 --- a/build.zig +++ b/build.zig @@ -2,6 +2,13 @@ const std = @import("std"); const builtin = @import("builtin"); const PatchMap = std.StringHashMap([]const u8); +const InitSystem = enum { + systemd, + openrc, + runit, + s6, + dinit, +}; const min_zig_string = "0.14.0"; const current_zig = builtin.zig_version; @@ -20,6 +27,7 @@ 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; pub fn build(b: *std.Build) !void { @@ -27,8 +35,7 @@ pub fn build(b: *std.Build) !void { 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"; - - const bin_directory = try b.allocator.dupe(u8, config_directory); + 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); @@ -37,7 +44,7 @@ pub fn build(b: *std.Build) !void { default_tty_str = try std.fmt.allocPrint(b.allocator, "{d}", .{default_tty}); - build_options.addOption([]const u8, "config_directory", bin_directory); + build_options.addOption([]const u8, "config_directory", config_directory); build_options.addOption([]const u8, "prefix_directory", prefix_directory); build_options.addOption([]const u8, "version", version_str); build_options.addOption(u8, "tty", default_tty); @@ -87,55 +94,22 @@ pub fn build(b: *std.Build) !void { const run_step = b.step("run", "Run the app"); run_step.dependOn(&run_cmd.step); - const installexe_step = b.step("installexe", "Install Ly"); - installexe_step.makeFn = ExeInstaller(true).make; + 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()); - const installnoconf_step = b.step("installnoconf", "Install Ly without its configuration file"); - installnoconf_step.makeFn = ExeInstaller(false).make; + 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()); - const installsystemd_step = b.step("installsystemd", "Install the Ly systemd service"); - installsystemd_step.makeFn = ServiceInstaller(.Systemd).make; - installsystemd_step.dependOn(installexe_step); + const uninstallexe_step = b.step("uninstallexe", "Uninstall Ly and remove the selected init system service"); + uninstallexe_step.makeFn = Uninstaller(true).make; - const installopenrc_step = b.step("installopenrc", "Install the Ly openrc service"); - installopenrc_step.makeFn = ServiceInstaller(.Openrc).make; - installopenrc_step.dependOn(installexe_step); - - const installrunit_step = b.step("installrunit", "Install the Ly runit service"); - installrunit_step.makeFn = ServiceInstaller(.Runit).make; - installrunit_step.dependOn(installexe_step); - - const installs6_step = b.step("installs6", "Install the Ly s6 service"); - installs6_step.makeFn = ServiceInstaller(.S6).make; - installs6_step.dependOn(installexe_step); - - const installdinit_step = b.step("installdinit", "Install the Ly dinit service"); - installdinit_step.makeFn = ServiceInstaller(.Dinit).make; - installdinit_step.dependOn(installexe_step); - - const uninstallall_step = b.step("uninstallall", "Uninstall Ly and all services"); - uninstallall_step.makeFn = uninstallall; + 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 ExeInstaller(install_conf: bool) type { - return struct { - pub fn make(step: *std.Build.Step, _: std.Build.Step.MakeOptions) !void { - try install_ly(step.owner.allocator, install_conf); - } - }; -} - -const InitSystem = enum { - Systemd, - Openrc, - Runit, - S6, - Dinit, -}; - -pub fn ServiceInstaller(comptime init_system: InitSystem) type { +pub fn Installer(install_config: bool) type { return struct { pub fn make(step: *std.Build.Step, _: std.Build.Step.MakeOptions) !void { const allocator = step.owner.allocator; @@ -148,78 +122,13 @@ pub fn ServiceInstaller(comptime init_system: InitSystem) type { try patch_map.put("$PREFIX_DIRECTORY", prefix_directory); try patch_map.put("$EXECUTABLE_NAME", executable_name); - 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 patched_service = try patchFile(allocator, "res/ly.service", patch_map); - try installText(patched_service, service_dir, service_path, "ly.service", .{ .mode = 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 patched_service = try patchFile(allocator, "res/ly-openrc", patch_map); - try installText(patched_service, service_dir, service_path, executable_name, .{ .mode = 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 supervise_path = try std.fs.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", .{}); - - try installFile("res/ly-runit-service/finish", service_dir, service_path, "finish", .{ .override_mode = 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 }); - - try std.fs.cwd().symLink("/run/runit/supervise.ly", supervise_path, .{}); - 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 file = try admin_service_dir.createFile("ly-srv", .{}); - file.close(); - - 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 patched_run = try patchFile(allocator, "res/ly-s6/run", patch_map); - try installText(patched_run, service_dir, service_path, "run", .{ .mode = 0o755 }); - - try installFile("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 patched_service = try patchFile(allocator, "res/ly-dinit", patch_map); - try installText(patched_service, service_dir, service_path, "ly", .{}); - }, - } + try install_ly(allocator, patch_map, install_config); + try install_service(allocator, patch_map); } }; } -fn install_ly(allocator: std.mem.Allocator, install_config: bool) !void { +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" }); std.fs.cwd().makePath(ly_config_directory) catch { @@ -250,26 +159,12 @@ fn install_ly(allocator: std.mem.Allocator, install_config: bool) !void { defer config_dir.close(); if (install_config) { - 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); - const patched_config = try patchFile(allocator, "res/config.ini", patch_map); try installText(patched_config, config_dir, ly_config_directory, "config.ini", .{}); } - { - var patch_map = PatchMap.init(allocator); - defer patch_map.deinit(); - - try patch_map.put("$CONFIG_DIRECTORY", config_directory); - - 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, "res/setup.sh", patch_map); + try installText(patched_setup, config_dir, ly_config_directory, "setup.sh", .{ .mode = 0o755 }); } { @@ -309,26 +204,107 @@ fn install_ly(allocator: std.mem.Allocator, install_config: bool) !void { } } -pub fn uninstallall(step: *std.Build.Step, _: std.Build.Step.MakeOptions) !void { - const allocator = step.owner.allocator; +fn install_service(allocator: std.mem.Allocator, 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(); - try deleteTree(allocator, config_directory, "/ly", "ly config directory not found"); + const patched_service = try patchFile(allocator, "res/ly.service", patch_map); + try installText(patched_service, service_dir, service_path, "ly.service", .{ .mode = 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 exe_path = try std.fs.path.join(allocator, &[_][]const u8{ dest_directory, prefix_directory, "/bin/", executable_name }); - var success = true; - std.fs.cwd().deleteFile(exe_path) catch { - std.debug.print("warn: ly executable not found\n", .{}); - success = false; + const patched_service = try patchFile(allocator, "res/ly-openrc", patch_map); + try installText(patched_service, service_dir, service_path, executable_name, .{ .mode = 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 supervise_path = try std.fs.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", .{}); + + try installFile("res/ly-runit-service/finish", service_dir, service_path, "finish", .{ .override_mode = 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 }); + + try std.fs.cwd().symLink("/run/runit/supervise.ly", supervise_path, .{}); + 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 file = try admin_service_dir.createFile("ly-srv", .{}); + file.close(); + + 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 patched_run = try patchFile(allocator, "res/ly-s6/run", patch_map); + try installText(patched_run, service_dir, service_path, "run", .{ .mode = 0o755 }); + + try installFile("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 patched_service = try patchFile(allocator, "res/ly-dinit", patch_map); + try installText(patched_service, service_dir, service_path, "ly", .{}); + }, + } +} + +pub fn Uninstaller(uninstall_config: bool) type { + return struct { + pub fn make(step: *std.Build.Step, _: std.Build.Step.MakeOptions) !void { + const allocator = step.owner.allocator; + + if (uninstall_config) { + try deleteTree(allocator, config_directory, "/ly", "ly config directory not found"); + } + + const exe_path = try std.fs.path.join(allocator, &[_][]const u8{ prefix_directory, "/bin/", executable_name }); + var success = true; + std.fs.cwd().deleteFile(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"); + + 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"), + .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"); + }, + .dinit => try deleteFile(allocator, config_directory, "/dinit.d/ly", "dinit service not found"), + } + } }; - 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, prefix_directory, "/lib/systemd/system/ly.service", "systemd service not found"); - try deleteFile(allocator, config_directory, "/init.d/ly", "openrc service not found"); - try deleteTree(allocator, config_directory, "/sv/ly", "runit service not found"); - 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 deleteFile(allocator, config_directory, "/dinit.d/ly", "dinit service not found"); } fn getVersionStr(b: *std.Build, name: []const u8, version: std.SemanticVersion) ![]const u8 { From 75975ea301f3e55901a6592210c818049bad26fb Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Wed, 12 Mar 2025 07:48:38 +0100 Subject: [PATCH 170/530] Add Niri to tested WM list Signed-off-by: AnErrupTion --- readme.md | 1 + 1 file changed, 1 insertion(+) diff --git a/readme.md b/readme.md index 64450b6..9342a8c 100644 --- a/readme.md +++ b/readme.md @@ -51,6 +51,7 @@ The following desktop environments were tested with success: - lxqt - mate - maxx + - niri - pantheon - qtile - spectrwm From 548fa210c1652c37cf09433df5279e0c0736bcb3 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Wed, 12 Mar 2025 07:58:37 +0100 Subject: [PATCH 171/530] Update README with new build commands Signed-off-by: AnErrupTion --- readme.md | 53 +++++++++++++++++++++++++++-------------------------- 1 file changed, 27 insertions(+), 26 deletions(-) diff --git a/readme.md b/readme.md index 9342a8c..72adb2c 100644 --- a/readme.md +++ b/readme.md @@ -72,7 +72,7 @@ changing the source code won't be necessary :) ## Cloning and Compiling Clone the repository ``` -$ git clone https://github.com/fairyglade/ly +$ git clone https://codeberg.org/AnErrupTion/ly ``` Change the directory to ly @@ -86,14 +86,24 @@ $ zig build ``` Test in the configured tty (tty2 by default) -or a terminal emulator (but desktop environments won't start) +or a terminal emulator (but authentication won't work) ``` -# zig build run +$ zig build run ``` -Install Ly and the provided systemd service file +**Important**: Running Ly in a terminal emulator as root is *not* recommended. If you +want to properly test Ly, please enable its service (as described below) and reboot +your machine. + +Install Ly for systemd-based systems (the default) ``` -# zig build installsystemd +# zig build installexe +``` + +(You can also install Ly without overriding the current configuration +file) +``` +# zig build installnoconf ``` Enable the service @@ -114,7 +124,7 @@ Clone, compile and test. Install Ly and the provided OpenRC service ``` -# zig build installopenrc +# zig build installexe -Dinit_system=openrc ``` Enable the service @@ -134,7 +144,7 @@ then you have to disable getty, so it doesn't respawn on top of ly ### runit ``` -# zig build installrunit +# zig build installexe -Dinit_system=runit # ln -s /etc/sv/ly /var/service/ ``` @@ -156,7 +166,7 @@ you should disable the agetty-tty2 service like this: ### s6 ``` -# zig build installs6 +# zig build installexe -Dinit_system=s6 ``` Then, edit `/etc/s6/config/ttyX.conf` and set `SPAWN="no"`, where X is the TTY ID (e.g. `2`). @@ -171,7 +181,7 @@ Finally, enable the service: ### dinit ``` -# zig build installdinit +# zig build installexe -Dinit_system=dinit # dinitctl enable ly ``` @@ -180,23 +190,19 @@ In addition to the steps above, you will also have to keep a TTY free within `/e To do that, change `ACTIVE_CONSOLES` so that the tty that ly should use in `/etc/ly/config.ini` is free. ### Updating -You can also install Ly without copying the system service and the configuration file. That's -called *updating*. To update, simply run: +You can also install Ly without overrding the current configuration file. That's called +*updating*. To update, simply run: ``` # zig build installnoconf ``` -If you want to also copy the default config file (but still not the system service), run: - -``` -# zig build installexe -``` +You can, of course, still select the init system of your choice when using this command. ## Arch Linux Installation You can install ly from the [`[extra]` repos](https://archlinux.org/packages/extra/x86_64/ly/): ``` -$ sudo pacman -S ly +# pacman -S ly ``` ## Gentoo Installation @@ -243,15 +249,10 @@ On Arch Linux, the example .xinitrc (/etc/X11/xinit/xinitrc) starts 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 if X doesn't start, as it can interfere -(this file is launched with X to configure the display properly). - -## PSX DOOM fire animation -To enable the famous PSX DOOM fire described by [Fabien Sanglard](http://fabiensanglard.net/doom_fire_psx/index.html), -just set `animation = doom` in `/etc/ly/config.ini`. You may also -disable the main box borders with `hide_borders = true`. +- 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 if X doesn't start, as it can interfere + (this file is launched with X to configure the display properly). ## Additional Information The name "Ly" is a tribute to the fairy from the game Rayman. From 5f5206383516eb0eeea7ab6ef5be863e8f998b9a Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Wed, 12 Mar 2025 08:00:42 +0100 Subject: [PATCH 172/530] Remove duplicate information in README Signed-off-by: AnErrupTion --- readme.md | 6 ------ 1 file changed, 6 deletions(-) diff --git a/readme.md b/readme.md index 72adb2c..0386dd1 100644 --- a/readme.md +++ b/readme.md @@ -100,12 +100,6 @@ Install Ly for systemd-based systems (the default) # zig build installexe ``` -(You can also install Ly without overriding the current configuration -file) -``` -# zig build installnoconf -``` - Enable the service ``` # systemctl enable ly.service From 9efb734fd50453cf0f486cc2724016b86442ffe9 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Thu, 13 Mar 2025 13:27:18 +0100 Subject: [PATCH 173/530] Show Ly version string at top-left Signed-off-by: AnErrupTion --- src/main.zig | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/main.zig b/src/main.zig index 02c6082..2868b42 100644 --- a/src/main.zig +++ b/src/main.zig @@ -24,6 +24,7 @@ const Ini = ini.Ini; const termbox = interop.termbox; const unistd = interop.unistd; const temporary_allocator = std.heap.page_allocator; +const ly_top_str = "Ly version " ++ build_options.version; var session_pid: std.posix.pid_t = -1; fn signalHandler(i: c_int) callconv(.C) void { @@ -421,6 +422,8 @@ pub fn main() !void { } } + buffer.drawLabel(ly_top_str, 0, 0); + if (config.bigclock != .none and buffer.box_height + (bigclock.HEIGHT + 2) * 2 < buffer.height) draw_big_clock: { const format = "%H:%M"; const xo = buffer.width / 2 - @min(buffer.width, (format.len * (bigclock.WIDTH + 1))) / 2; @@ -480,7 +483,7 @@ pub fn main() !void { info_line.label.draw(); if (!config.hide_key_hints) { - var length: usize = 0; + var length: usize = ly_top_str.len + 1; buffer.drawLabel(config.shutdown_key, length, 0); length += config.shutdown_key.len + 1; From 86ea38f460e654bab58d363dd9cc8789db800fc9 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Sun, 16 Mar 2025 00:24:56 +0100 Subject: [PATCH 174/530] Split session crawling from TUI component Signed-off-by: AnErrupTion --- src/Environment.zig | 21 +++++++ src/auth.zig | 6 +- src/main.zig | 93 +++++++++++++++++++++++++-- src/tui/components/Session.zig | 111 ++------------------------------- 4 files changed, 117 insertions(+), 114 deletions(-) create mode 100644 src/Environment.zig diff --git a/src/Environment.zig b/src/Environment.zig new file mode 100644 index 0000000..b21d451 --- /dev/null +++ b/src/Environment.zig @@ -0,0 +1,21 @@ +const enums = @import("enums.zig"); +const ini = @import("zigini"); + +const DisplayServer = enums.DisplayServer; +const Ini = ini.Ini; + +pub const DesktopEntry = struct { + Exec: []const u8 = "", + Name: [:0]const u8 = "", + DesktopNames: ?[:0]u8 = null, +}; + +pub const Entry = struct { @"Desktop Entry": DesktopEntry = .{} }; + +entry_ini: ?Ini(Entry) = null, +name: [:0]const u8 = "", +xdg_session_desktop: ?[:0]const u8 = null, +xdg_desktop_names: ?[:0]const u8 = null, +cmd: []const u8 = "", +specifier: []const u8 = "", +display_server: DisplayServer = .wayland, diff --git a/src/auth.zig b/src/auth.zig index 2d2bbf8..35eee5e 100644 --- a/src/auth.zig +++ b/src/auth.zig @@ -2,8 +2,8 @@ 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 Session = @import("tui/components/Session.zig"); const SharedError = @import("SharedError.zig"); const Allocator = std.mem.Allocator; @@ -33,7 +33,7 @@ pub fn sessionSignalHandler(i: c_int) callconv(.C) void { if (child_pid > 0) _ = std.c.kill(child_pid, i); } -pub fn authenticate(options: AuthOptions, current_environment: Session.Environment, login: [:0]const u8, password: [:0]const u8) !void { +pub fn authenticate(options: AuthOptions, current_environment: Environment, login: [:0]const u8, password: [:0]const u8) !void { var tty_buffer: [3]u8 = undefined; const tty_str = try std.fmt.bufPrintZ(&tty_buffer, "{d}", .{options.tty}); @@ -134,7 +134,7 @@ fn startSession( options: AuthOptions, pwd: *interop.pwd.passwd, handle: ?*interop.pam.pam_handle, - current_environment: Session.Environment, + current_environment: Environment, ) !void { if (builtin.os.tag == .freebsd) { // FreeBSD has initgroups() in unistd diff --git a/src/main.zig b/src/main.zig index 2868b42..e9de028 100644 --- a/src/main.zig +++ b/src/main.zig @@ -5,6 +5,8 @@ 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 Doom = @import("animations/Doom.zig"); const Matrix = @import("animations/Matrix.zig"); @@ -21,6 +23,8 @@ const SharedError = @import("SharedError.zig"); const utils = @import("tui/utils.zig"); const Ini = ini.Ini; +const DisplayServer = enums.DisplayServer; +const Entry = Environment.Entry; const termbox = interop.termbox; const unistd = interop.unistd; const temporary_allocator = std.heap.page_allocator; @@ -254,16 +258,16 @@ pub fn main() !void { try info_line.addMessage(lang.err_numlock, config.error_bg, config.error_fg); }; - var session = Session.init(allocator, &buffer, lang); + var session = Session.init(allocator, &buffer); defer session.deinit(); - session.addEnvironment(.{ .Name = lang.shell }, null, .shell) catch { + addOtherEnvironment(&session, lang, .shell, null) catch { try info_line.addMessage(lang.err_alloc, config.error_bg, config.error_fg); }; if (build_options.enable_x11_support) { - if (config.xinitrc) |xinitrc| { - session.addEnvironment(.{ .Name = lang.xinitrc, .Exec = xinitrc }, null, .xinitrc) catch { + if (config.xinitrc) |xinitrc_cmd| { + addOtherEnvironment(&session, lang, .xinitrc, xinitrc_cmd) catch { try info_line.addMessage(lang.err_alloc, config.error_bg, config.error_fg); }; } @@ -283,8 +287,8 @@ pub fn main() !void { try info_line.addMessage(hostname, config.bg, config.fg); } - try session.crawl(config.waylandsessions, .wayland); - if (build_options.enable_x11_support) try session.crawl(config.xsessions, .x11); + try crawl(&session, lang, config.waylandsessions, .wayland); + if (build_options.enable_x11_support) try crawl(&session, lang, config.xsessions, .x11); var login = Text.init(allocator, &buffer, false, null); defer login.deinit(); @@ -844,6 +848,83 @@ pub fn main() !void { } } +fn addOtherEnvironment(session: *Session, lang: Lang, display_server: DisplayServer, exec: ?[]const u8) !void { + const name = switch (display_server) { + .shell => lang.shell, + .xinitrc => lang.xinitrc, + else => unreachable, + }; + + try session.addEnvironment(.{ + .entry_ini = null, + .name = name, + .xdg_session_desktop = null, + .xdg_desktop_names = null, + .cmd = exec orelse "", + .specifier = switch (display_server) { + .wayland => lang.wayland, + .x11 => lang.x11, + else => lang.other, + }, + .display_server = display_server, + }); +} + +fn crawl(session: *Session, lang: Lang, path: []const u8, display_server: DisplayServer) !void { + var iterable_directory = std.fs.openDirAbsolute(path, .{ .iterate = true }) catch return; + defer iterable_directory.close(); + + var iterator = iterable_directory.iterate(); + while (try iterator.next()) |item| { + if (!std.mem.eql(u8, std.fs.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); + _ = try entry_ini.readFileToStruct(entry_path, .{ + .fieldHandler = null, + .comment_characters = "#", + }); + errdefer entry_ini.deinit(); + + var xdg_session_desktop: []const u8 = undefined; + const maybe_desktop_names = entry_ini.data.@"Desktop Entry".DesktopNames; + if (maybe_desktop_names) |desktop_names| { + xdg_session_desktop = std.mem.sliceTo(desktop_names, ';'); + } else { + // if DesktopNames is empty, we'll take the name of the session file + xdg_session_desktop = std.fs.path.stem(item.name); + } + + // Prepare the XDG_CURRENT_DESKTOP environment variable here + const entry = entry_ini.data.@"Desktop Entry"; + var xdg_desktop_names: ?[:0]const u8 = null; + if (entry.DesktopNames) |desktop_names| { + for (desktop_names) |*c| { + if (c.* == ';') c.* = ':'; + } + xdg_desktop_names = desktop_names; + } + + const session_desktop = try session.label.allocator.dupeZ(u8, xdg_session_desktop); + errdefer session.label.allocator.free(session_desktop); + + try session.addEnvironment(.{ + .entry_ini = entry_ini, + .name = entry.Name, + .xdg_session_desktop = session_desktop, + .xdg_desktop_names = xdg_desktop_names, + .cmd = entry.Exec, + .specifier = switch (display_server) { + .wayland => lang.wayland, + .x11 => lang.x11, + else => lang.other, + }, + .display_server = display_server, + }); + } +} + fn adjustBrightness(allocator: std.mem.Allocator, cmd: []const u8) !void { var brightness = std.process.Child.init(&[_][]const u8{ "/bin/sh", "-c", cmd }, allocator); brightness.stdout_behavior = .Ignore; diff --git a/src/tui/components/Session.zig b/src/tui/components/Session.zig index d513949..4135283 100644 --- a/src/tui/components/Session.zig +++ b/src/tui/components/Session.zig @@ -1,43 +1,22 @@ const std = @import("std"); const TerminalBuffer = @import("../TerminalBuffer.zig"); const enums = @import("../../enums.zig"); +const ini = @import("zigini"); +const Environment = @import("../../Environment.zig"); const generic = @import("generic.zig"); -const Ini = @import("zigini").Ini; -const Lang = @import("../../config/Lang.zig"); const Allocator = std.mem.Allocator; - const DisplayServer = enums.DisplayServer; - +const Ini = ini.Ini; const EnvironmentLabel = generic.CyclableLabel(Environment); const Session = @This(); -pub const Environment = struct { - entry_ini: ?Ini(Entry) = null, - name: [:0]const u8 = "", - xdg_session_desktop: ?[:0]const u8 = null, - xdg_desktop_names: ?[:0]const u8 = null, - cmd: []const u8 = "", - specifier: []const u8 = "", - display_server: DisplayServer = .wayland, -}; - -const DesktopEntry = struct { - Exec: []const u8 = "", - Name: [:0]const u8 = "", - DesktopNames: ?[:0]u8 = null, -}; - -pub const Entry = struct { @"Desktop Entry": DesktopEntry = .{} }; - label: EnvironmentLabel, -lang: Lang, -pub fn init(allocator: Allocator, buffer: *TerminalBuffer, lang: Lang) Session { +pub fn init(allocator: Allocator, buffer: *TerminalBuffer) Session { return .{ .label = EnvironmentLabel.init(allocator, buffer, drawItem), - .lang = lang, }; } @@ -50,86 +29,8 @@ pub fn deinit(self: Session) void { self.label.deinit(); } -pub fn addEnvironment(self: *Session, entry: DesktopEntry, xdg_session_desktop: ?[:0]const u8, display_server: DisplayServer) !void { - var xdg_desktop_names: ?[:0]const u8 = null; - if (entry.DesktopNames) |desktop_names| { - for (desktop_names) |*c| { - if (c.* == ';') c.* = ':'; - } - xdg_desktop_names = desktop_names; - } - - try self.label.addItem(.{ - .entry_ini = null, - .name = entry.Name, - .xdg_session_desktop = xdg_session_desktop, - .xdg_desktop_names = xdg_desktop_names, - .cmd = entry.Exec, - .specifier = switch (display_server) { - .wayland => self.lang.wayland, - .x11 => self.lang.x11, - else => self.lang.other, - }, - .display_server = display_server, - }); -} - -pub fn addEnvironmentWithIni(self: *Session, entry_ini: Ini(Entry), xdg_session_desktop: ?[:0]const u8, display_server: DisplayServer) !void { - const entry = entry_ini.data.@"Desktop Entry"; - var xdg_desktop_names: ?[:0]const u8 = null; - if (entry.DesktopNames) |desktop_names| { - for (desktop_names) |*c| { - if (c.* == ';') c.* = ':'; - } - xdg_desktop_names = desktop_names; - } - - try self.label.addItem(.{ - .entry_ini = entry_ini, - .name = entry.Name, - .xdg_session_desktop = xdg_session_desktop, - .xdg_desktop_names = xdg_desktop_names, - .cmd = entry.Exec, - .specifier = switch (display_server) { - .wayland => self.lang.wayland, - .x11 => self.lang.x11, - else => self.lang.other, - }, - .display_server = display_server, - }); -} - -pub fn crawl(self: *Session, path: []const u8, display_server: DisplayServer) !void { - var iterable_directory = std.fs.openDirAbsolute(path, .{ .iterate = true }) catch return; - defer iterable_directory.close(); - - var iterator = iterable_directory.iterate(); - while (try iterator.next()) |item| { - if (!std.mem.eql(u8, std.fs.path.extension(item.name), ".desktop")) continue; - - const entry_path = try std.fmt.allocPrint(self.label.allocator, "{s}/{s}", .{ path, item.name }); - defer self.label.allocator.free(entry_path); - var entry_ini = Ini(Entry).init(self.label.allocator); - _ = try entry_ini.readFileToStruct(entry_path, .{ - .fieldHandler = null, - .comment_characters = "#", - }); - errdefer entry_ini.deinit(); - - var xdg_session_desktop: []const u8 = undefined; - const maybe_desktop_names = entry_ini.data.@"Desktop Entry".DesktopNames; - if (maybe_desktop_names) |desktop_names| { - xdg_session_desktop = std.mem.sliceTo(desktop_names, ';'); - } else { - // if DesktopNames is empty, we'll take the name of the session file - xdg_session_desktop = std.fs.path.stem(item.name); - } - - const session_desktop = try self.label.allocator.dupeZ(u8, xdg_session_desktop); - errdefer self.label.allocator.free(session_desktop); - - try self.addEnvironmentWithIni(entry_ini, session_desktop, display_server); - } +pub fn addEnvironment(self: *Session, environment: Environment) !void { + try self.label.addItem(environment); } fn drawItem(label: *EnvironmentLabel, environment: Environment, x: usize, y: usize) bool { From fa0748ead263d51ee734910230067408718f8462 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Sun, 16 Mar 2025 09:13:01 +0100 Subject: [PATCH 175/530] Remove unused valgrind file Signed-off-by: AnErrupTion --- res/valgrind.supp | 31 ------------------------------- 1 file changed, 31 deletions(-) delete mode 100644 res/valgrind.supp diff --git a/res/valgrind.supp b/res/valgrind.supp deleted file mode 100644 index 274f2f0..0000000 --- a/res/valgrind.supp +++ /dev/null @@ -1,31 +0,0 @@ -{ - pam - Memcheck:Leak - ... - obj:/usr/lib/libpam.so.0.84.2 - ... -} - -{ - termbox - Memcheck:Leak - ... - fun:tb_init - ... -} - -{ - libc/dynamic - Memcheck:Leak - ... - fun:_dl_catch_exception - ... -} - -{ - libc/groups - Memcheck:Leak - ... - fun:initgroups - ... -} From e0ed1b4eb1cf2f5df262aaff344bbbe449feb969 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Sun, 16 Mar 2025 10:54:06 +0100 Subject: [PATCH 176/530] Add animation framework Signed-off-by: AnErrupTion --- src/animations/ColorMix.zig | 11 ++++++- src/animations/Doom.zig | 11 +++++-- src/animations/Matrix.zig | 11 +++++-- src/main.zig | 63 +++++++++++++------------------------ src/tui/Animation.zig | 61 +++++++++++++++++++++++++++++++++++ 5 files changed, 108 insertions(+), 49 deletions(-) create mode 100644 src/tui/Animation.zig diff --git a/src/animations/ColorMix.zig b/src/animations/ColorMix.zig index 95e7df0..95b454c 100644 --- a/src/animations/ColorMix.zig +++ b/src/animations/ColorMix.zig @@ -1,4 +1,5 @@ const std = @import("std"); +const Animation = @import("../tui/Animation.zig"); const TerminalBuffer = @import("../tui/TerminalBuffer.zig"); const utils = @import("../tui/utils.zig"); @@ -43,7 +44,15 @@ pub fn init(terminal_buffer: *TerminalBuffer, col1: u32, col2: u32, col3: u32) C }; } -pub fn draw(self: *ColorMix) void { +pub fn animation(self: *ColorMix) Animation { + return Animation.init(self, deinit, realloc, draw); +} + +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 6b0d46a..bbc0a86 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 TerminalBuffer = @import("../tui/TerminalBuffer.zig"); const utils = @import("../tui/utils.zig"); @@ -38,17 +39,21 @@ pub fn init(allocator: Allocator, terminal_buffer: *TerminalBuffer, top_color: u }; } -pub fn deinit(self: Doom) void { +pub fn animation(self: *Doom) Animation { + return Animation.init(self, deinit, realloc, draw); +} + +fn deinit(self: *Doom) void { self.allocator.free(self.buffer); } -pub fn realloc(self: *Doom) !void { +fn realloc(self: *Doom) anyerror!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; } -pub fn draw(self: Doom) void { +fn draw(self: *Doom) void { for (0..self.terminal_buffer.width) |x| { for (1..self.terminal_buffer.height) |y| { const source = y * self.terminal_buffer.width + x; diff --git a/src/animations/Matrix.zig b/src/animations/Matrix.zig index 18636fc..c60aa17 100644 --- a/src/animations/Matrix.zig +++ b/src/animations/Matrix.zig @@ -1,6 +1,7 @@ const std = @import("std"); const Allocator = std.mem.Allocator; const Random = std.Random; +const Animation = @import("../tui/Animation.zig"); const TerminalBuffer = @import("../tui/TerminalBuffer.zig"); const utils = @import("../tui/utils.zig"); @@ -54,12 +55,16 @@ pub fn init(allocator: Allocator, terminal_buffer: *TerminalBuffer, fg_ini: u32, }; } -pub fn deinit(self: Matrix) void { +pub fn animation(self: *Matrix) Animation { + return Animation.init(self, deinit, realloc, draw); +} + +fn deinit(self: *Matrix) void { self.allocator.free(self.dots); self.allocator.free(self.lines); } -pub fn realloc(self: *Matrix) !void { +fn realloc(self: *Matrix) anyerror!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); @@ -69,7 +74,7 @@ pub fn realloc(self: *Matrix) !void { self.lines = lines; } -pub fn draw(self: *Matrix) void { +fn draw(self: *Matrix) void { 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 e9de028..54e62aa 100644 --- a/src/main.zig +++ b/src/main.zig @@ -11,6 +11,7 @@ const interop = @import("interop.zig"); const Doom = @import("animations/Doom.zig"); const Matrix = @import("animations/Matrix.zig"); const ColorMix = @import("animations/ColorMix.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"); @@ -336,24 +337,24 @@ pub fn main() !void { } // Initialize the animation, if any - var doom: Doom = undefined; - var matrix: Matrix = undefined; - var color_mix: ColorMix = undefined; + var animation: Animation = undefined; switch (config.animation) { .none => {}, - .doom => doom = try Doom.init(allocator, &buffer, config.doom_top_color, config.doom_middle_color, config.doom_bottom_color), - .matrix => matrix = try Matrix.init(allocator, &buffer, config.cmatrix_fg, config.cmatrix_min_codepoint, config.cmatrix_max_codepoint), - .colormix => color_mix = ColorMix.init(&buffer, config.colormix_col1, config.colormix_col2, config.colormix_col3), - } - defer { - switch (config.animation) { - .none => {}, - .doom => doom.deinit(), - .matrix => matrix.deinit(), - .colormix => {}, - } + .doom => { + var doom = try Doom.init(allocator, &buffer, config.doom_top_color, config.doom_middle_color, config.doom_bottom_color); + animation = doom.animation(); + }, + .matrix => { + var matrix = try Matrix.init(allocator, &buffer, config.cmatrix_fg, 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(); + }, } + defer animation.deinit(); const animate = config.animation != .none; const shutdown_key = try std.fmt.parseInt(u8, config.shutdown_key[1..], 10); @@ -382,7 +383,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 (!update or config.animation != .none) { + if (!update or animate) { if (!update) std.time.sleep(std.time.ns_per_ms * 100); _ = termbox.tb_present(); // Required to update tb_width() and tb_height() @@ -396,16 +397,9 @@ pub fn main() !void { buffer.width = width; buffer.height = height; - switch (config.animation) { - .none => {}, - .doom => doom.realloc() catch { - try info_line.addMessage(lang.err_alloc, config.error_bg, config.error_fg); - }, - .matrix => matrix.realloc() catch { - try info_line.addMessage(lang.err_alloc, config.error_bg, config.error_fg); - }, - .colormix => {}, - } + animation.realloc() catch { + try info_line.addMessage(lang.err_alloc, config.error_bg, config.error_fg); + }; update = true; resolution_changed = true; @@ -417,14 +411,7 @@ pub fn main() !void { if (auth_fails < config.auth_fails) { _ = termbox.tb_clear(); - if (!animation_timed_out) { - switch (config.animation) { - .none => {}, - .doom => doom.draw(), - .matrix => matrix.draw(), - .colormix => color_mix.draw(), - } - } + if (!animation_timed_out) animation.draw(); buffer.drawLabel(ly_top_str, 0, 0); @@ -585,12 +572,7 @@ pub fn main() !void { if (config.animation_timeout_sec > 0 and tv.tv_sec - tv_zero.tv_sec > config.animation_timeout_sec) { animation_timed_out = true; - switch (config.animation) { - .none => {}, - .doom => doom.deinit(), - .matrix => matrix.deinit(), - .colormix => {}, - } + animation.deinit(); } } else if (config.bigclock != .none and config.clock == null) { var tv: interop.system_time.timeval = undefined; @@ -736,9 +718,6 @@ pub fn main() !void { const password_text = try allocator.dupeZ(u8, password.text.items); defer allocator.free(password_text); - // Give up control on the TTY - // _ = termbox.tb_shutdown(); - session_pid = try std.posix.fork(); if (session_pid == 0) { const current_environment = session.label.list.items[session.label.current]; diff --git a/src/tui/Animation.zig b/src/tui/Animation.zig new file mode 100644 index 0000000..2311bba --- /dev/null +++ b/src/tui/Animation.zig @@ -0,0 +1,61 @@ +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}); +} From 1672d4a9ec655a3e208883729ead43733e90313f Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Sun, 16 Mar 2025 11:17:38 +0100 Subject: [PATCH 177/530] Make main code less directly dependent on termbox2 Signed-off-by: AnErrupTion --- src/animations/ColorMix.zig | 32 +++++++++++++-------------- src/animations/Doom.zig | 38 +++++++++++++++++---------------- src/animations/Matrix.zig | 29 +++++++++++++------------ src/bigclock.zig | 13 ++++++----- src/main.zig | 11 +++++----- src/tui/Cell.zig | 23 ++++++++++++++++++++ src/tui/TerminalBuffer.zig | 34 ++++++++++++++++++----------- src/tui/components/InfoLine.zig | 3 +-- src/tui/utils.zig | 32 --------------------------- 9 files changed, 108 insertions(+), 107 deletions(-) create mode 100644 src/tui/Cell.zig delete mode 100644 src/tui/utils.zig diff --git a/src/animations/ColorMix.zig b/src/animations/ColorMix.zig index 95b454c..3db2e46 100644 --- a/src/animations/ColorMix.zig +++ b/src/animations/ColorMix.zig @@ -1,7 +1,7 @@ const std = @import("std"); const Animation = @import("../tui/Animation.zig"); +const Cell = @import("../tui/Cell.zig"); const TerminalBuffer = @import("../tui/TerminalBuffer.zig"); -const utils = @import("../tui/utils.zig"); const ColorMix = @This(); @@ -19,7 +19,7 @@ terminal_buffer: *TerminalBuffer, frames: u64, pattern_cos_mod: f32, pattern_sin_mod: f32, -palette: [palette_len]utils.Cell, +palette: [palette_len]Cell, pub fn init(terminal_buffer: *TerminalBuffer, col1: u32, col2: u32, col3: u32) ColorMix { return .{ @@ -27,19 +27,19 @@ pub fn init(terminal_buffer: *TerminalBuffer, col1: u32, col2: u32, col3: u32) C .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, - .palette = [palette_len]utils.Cell{ - utils.initCell(0x2588, col1, col2), - utils.initCell(0x2593, col1, col2), - utils.initCell(0x2592, col1, col2), - utils.initCell(0x2591, col1, col2), - utils.initCell(0x2588, col2, col3), - utils.initCell(0x2593, col2, col3), - utils.initCell(0x2592, col2, col3), - utils.initCell(0x2591, col2, col3), - utils.initCell(0x2588, col3, col1), - utils.initCell(0x2593, col3, col1), - utils.initCell(0x2592, col3, col1), - utils.initCell(0x2591, col3, col1), + .palette = [palette_len]Cell{ + Cell.init(0x2588, col1, col2), + Cell.init(0x2593, col1, col2), + Cell.init(0x2592, col1, col2), + Cell.init(0x2591, col1, col2), + Cell.init(0x2588, col2, col3), + Cell.init(0x2593, col2, col3), + Cell.init(0x2592, col2, col3), + Cell.init(0x2591, col2, col3), + Cell.init(0x2588, col3, col1), + Cell.init(0x2593, col3, col1), + Cell.init(0x2592, col3, col1), + Cell.init(0x2591, col3, col1), }, }; } @@ -80,7 +80,7 @@ fn draw(self: *ColorMix) void { } const cell = self.palette[@as(usize, @intFromFloat(math.floor(length(uv) * 5.0))) % palette_len]; - utils.putCell(x, y, cell); + cell.put(x, y); } } } diff --git a/src/animations/Doom.zig b/src/animations/Doom.zig index bbc0a86..9e2b24a 100644 --- a/src/animations/Doom.zig +++ b/src/animations/Doom.zig @@ -1,8 +1,8 @@ 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 utils = @import("../tui/utils.zig"); const Doom = @This(); @@ -11,7 +11,7 @@ pub const STEPS = 12; allocator: Allocator, terminal_buffer: *TerminalBuffer, buffer: []u8, -fire: [STEPS + 1]utils.Cell, +fire: [STEPS + 1]Cell, pub fn init(allocator: Allocator, terminal_buffer: *TerminalBuffer, top_color: u32, middle_color: u32, bottom_color: u32) !Doom { const buffer = try allocator.alloc(u8, terminal_buffer.width * terminal_buffer.height); @@ -21,20 +21,20 @@ pub fn init(allocator: Allocator, terminal_buffer: *TerminalBuffer, top_color: u .allocator = allocator, .terminal_buffer = terminal_buffer, .buffer = buffer, - .fire = [_]utils.Cell{ - utils.initCell(' ', 0x00000000, 0), - utils.initCell(0x2591, top_color, 0), - utils.initCell(0x2592, top_color, 0), - utils.initCell(0x2593, top_color, 0), - utils.initCell(0x2588, top_color, 0), - utils.initCell(0x2591, middle_color, 2), - utils.initCell(0x2592, middle_color, 2), - utils.initCell(0x2593, middle_color, 2), - utils.initCell(0x2588, middle_color, 2), - utils.initCell(0x2591, bottom_color, 4), - utils.initCell(0x2592, bottom_color, 4), - utils.initCell(0x2593, bottom_color, 4), - utils.initCell(0x2588, bottom_color, 4), + .fire = [_]Cell{ + Cell.init(' ', 0x00000000, 0), + Cell.init(0x2591, top_color, 0), + Cell.init(0x2592, top_color, 0), + Cell.init(0x2593, top_color, 0), + Cell.init(0x2588, top_color, 0), + Cell.init(0x2591, middle_color, 2), + Cell.init(0x2592, middle_color, 2), + Cell.init(0x2593, middle_color, 2), + Cell.init(0x2588, middle_color, 2), + Cell.init(0x2591, bottom_color, 4), + Cell.init(0x2592, bottom_color, 4), + Cell.init(0x2593, bottom_color, 4), + Cell.init(0x2588, bottom_color, 4), }, }; } @@ -73,11 +73,13 @@ fn draw(self: *Doom) void { const dest_y = dest / self.terminal_buffer.width; const dest_x = dest % self.terminal_buffer.width; - utils.putCell(dest_x, dest_y, self.fire[buffer_dest]); + const dest_cell = self.fire[buffer_dest]; + dest_cell.put(dest_x, dest_y); const source_y = source / self.terminal_buffer.width; const source_x = source % self.terminal_buffer.width; - utils.putCell(source_x, source_y, self.fire[buffer_source]); + const source_cell = self.fire[buffer_source]; + source_cell.put(source_x, source_y); } } } diff --git a/src/animations/Matrix.zig b/src/animations/Matrix.zig index c60aa17..9ebc0fb 100644 --- a/src/animations/Matrix.zig +++ b/src/animations/Matrix.zig @@ -1,11 +1,11 @@ const std = @import("std"); +const interop = @import("../interop.zig"); +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; -const Animation = @import("../tui/Animation.zig"); -const TerminalBuffer = @import("../tui/TerminalBuffer.zig"); -const utils = @import("../tui/utils.zig"); - -const interop = @import("../interop.zig"); const termbox = interop.termbox; pub const FRAME_DELAY: usize = 8; @@ -13,6 +13,8 @@ pub const FRAME_DELAY: usize = 8; // Characters change mid-scroll pub const MID_SCROLL_CHANGE = true; +const DOT_HEAD_COLOR: u32 = @intCast(0x00FFFFFF | termbox.TB_BOLD); // White and bold + const Matrix = @This(); pub const Dot = struct { @@ -35,6 +37,7 @@ count: usize, fg_ini: u32, min_codepoint: u16, max_codepoint: u16, +default_cell: Cell, pub fn init(allocator: Allocator, terminal_buffer: *TerminalBuffer, fg_ini: u32, min_codepoint: u16, max_codepoint: u16) !Matrix { const dots = try allocator.alloc(Dot, terminal_buffer.width * (terminal_buffer.height + 1)); @@ -52,6 +55,7 @@ pub fn init(allocator: Allocator, terminal_buffer: *TerminalBuffer, fg_ini: u32, .fg_ini = fg_ini, .min_codepoint = min_codepoint, .max_codepoint = max_codepoint - min_codepoint, + .default_cell = .{ .ch = ' ', .fg = fg_ini, .bg = termbox.TB_DEFAULT }, }; } @@ -153,16 +157,13 @@ fn draw(self: *Matrix) void { var y: usize = 1; while (y <= self.terminal_buffer.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.?), + .fg = if (dot.is_head) DOT_HEAD_COLOR else self.fg_ini, + .bg = termbox.TB_DEFAULT, + }; - var fg = self.fg_ini; - - if (dot.value == null or dot.value == ' ') { - utils.putCell(x, y - 1, .{ .ch = ' ', .fg = fg, .bg = termbox.TB_DEFAULT }); - continue; - } - - if (dot.is_head) fg = @intCast(0x00FFFFFF | termbox.TB_BOLD); // White and bold - utils.putCell(x, y - 1, .{ .ch = @intCast(dot.value.?), .fg = fg, .bg = termbox.TB_DEFAULT }); + cell.put(x, y - 1); } } } diff --git a/src/bigclock.zig b/src/bigclock.zig index fb4baa7..d63f0fd 100644 --- a/src/bigclock.zig +++ b/src/bigclock.zig @@ -1,36 +1,35 @@ const std = @import("std"); const interop = @import("interop.zig"); -const utils = @import("tui/utils.zig"); 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 termbox = interop.termbox; const Bigclock = enums.Bigclock; pub const WIDTH = Lang.WIDTH; pub const HEIGHT = Lang.HEIGHT; pub const SIZE = Lang.SIZE; -pub fn clockCell(animate: bool, char: u8, fg: u32, bg: u32, bigclock: Bigclock) [SIZE]utils.Cell { - var cells: [SIZE]utils.Cell = undefined; +pub fn clockCell(animate: bool, char: u8, fg: u32, bg: u32, bigclock: Bigclock) [SIZE]Cell { + var cells: [SIZE]Cell = undefined; var tv: interop.system_time.timeval = undefined; _ = interop.system_time.gettimeofday(&tv, null); const clock_chars = toBigNumber(if (animate and char == ':' and @divTrunc(tv.tv_usec, 500000) != 0) ' ' else char, bigclock); - for (0..cells.len) |i| cells[i] = utils.initCell(clock_chars[i], fg, bg); + 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]utils.Cell) void { +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]; - if (cell.ch != 0) utils.putCell(x + xx, y + yy, cell); + cell.put(x + xx, y + yy); } } } diff --git a/src/main.zig b/src/main.zig index 54e62aa..d187010 100644 --- a/src/main.zig +++ b/src/main.zig @@ -21,7 +21,6 @@ const Lang = @import("config/Lang.zig"); const Save = @import("config/Save.zig"); const migrator = @import("config/migrator.zig"); const SharedError = @import("SharedError.zig"); -const utils = @import("tui/utils.zig"); const Ini = ini.Ini; const DisplayServer = enums.DisplayServer; @@ -358,15 +357,15 @@ pub fn main() !void { const animate = config.animation != .none; const shutdown_key = try std.fmt.parseInt(u8, config.shutdown_key[1..], 10); - const shutdown_len = try utils.strWidth(lang.shutdown); + 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 utils.strWidth(lang.restart); + 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 utils.strWidth(lang.sleep); + const sleep_len = try TerminalBuffer.strWidth(lang.sleep); 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 utils.strWidth(lang.brightness_down); + const brightness_down_len = try TerminalBuffer.strWidth(lang.brightness_down); const brightness_up_key = if (config.brightness_down_key) |key| try std.fmt.parseInt(u8, key[1..], 10) else null; - const brightness_up_len = try utils.strWidth(lang.brightness_up); + const brightness_up_len = try TerminalBuffer.strWidth(lang.brightness_up); var event: termbox.tb_event = undefined; var run = true; diff --git a/src/tui/Cell.zig b/src/tui/Cell.zig new file mode 100644 index 0000000..66d06f8 --- /dev/null +++ b/src/tui/Cell.zig @@ -0,0 +1,23 @@ +const interop = @import("../interop.zig"); + +const termbox = interop.termbox; + +const Cell = @This(); + +ch: u32, +fg: u32, +bg: u32, + +pub fn init(ch: u32, fg: u32, bg: u32) Cell { + return .{ + .ch = ch, + .fg = fg, + .bg = bg, + }; +} + +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); +} diff --git a/src/tui/TerminalBuffer.zig b/src/tui/TerminalBuffer.zig index ba75516..4fe2dae 100644 --- a/src/tui/TerminalBuffer.zig +++ b/src/tui/TerminalBuffer.zig @@ -1,7 +1,7 @@ const std = @import("std"); const builtin = @import("builtin"); const interop = @import("../interop.zig"); -const utils = @import("utils.zig"); +const Cell = @import("Cell.zig"); const Random = std.Random; @@ -41,6 +41,7 @@ box_width: usize, box_height: usize, margin_box_v: u8, margin_box_h: u8, +blank_cell: Cell, pub fn init(options: InitOptions, labels_max_length: usize, random: Random) TerminalBuffer { return .{ @@ -76,6 +77,7 @@ pub fn init(options: InitOptions, labels_max_length: usize, random: Random) Term .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), }; } @@ -125,29 +127,27 @@ pub fn drawBoxCenter(self: *TerminalBuffer, show_borders: bool, blank_box: bool) _ = 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 = utils.initCell(self.box_chars.top, self.border_fg, self.bg); - var c2 = utils.initCell(self.box_chars.bottom, 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| { - utils.putCell(x1 + i, y1 - 1, c1); - utils.putCell(x1 + i, y2, c2); + 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| { - utils.putCell(x1 - 1, y1 + i, c1); - utils.putCell(x2, y1 + i, c2); + c1.put(x1 - 1, y1 + i); + c2.put(x2, y1 + i); } } if (blank_box) { - const blank = utils.initCell(' ', self.fg, self.bg); - for (0..self.box_height) |y| { for (0..self.box_width) |x| { - utils.putCell(x1 + x, y1 + y, blank); + self.blank_cell.put(x1 + x, y1 + y); } } } @@ -203,6 +203,16 @@ pub fn drawConfinedLabel(self: TerminalBuffer, text: []const u8, x: usize, y: us } pub fn drawCharMultiple(self: TerminalBuffer, char: u32, x: usize, y: usize, length: usize) void { - const cell = utils.initCell(char, self.fg, self.bg); - for (0..length) |xx| utils.putCell(x + xx, y, cell); + const cell = Cell.init(char, self.fg, self.bg); + for (0..length) |xx| cell.put(x + xx, y); +} + +// 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); + var utf8 = utf8view.iterator(); + var i: u8 = 0; + while (utf8.nextCodepoint()) |_| i += 1; + return i; } diff --git a/src/tui/components/InfoLine.zig b/src/tui/components/InfoLine.zig index 05d421c..42436d9 100644 --- a/src/tui/components/InfoLine.zig +++ b/src/tui/components/InfoLine.zig @@ -1,7 +1,6 @@ const std = @import("std"); const TerminalBuffer = @import("../TerminalBuffer.zig"); const generic = @import("generic.zig"); -const utils = @import("../utils.zig"); const Allocator = std.mem.Allocator; @@ -32,7 +31,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 utils.strWidth(text), + .width = try TerminalBuffer.strWidth(text), .text = text, .bg = bg, .fg = fg, diff --git a/src/tui/utils.zig b/src/tui/utils.zig deleted file mode 100644 index ace54a6..0000000 --- a/src/tui/utils.zig +++ /dev/null @@ -1,32 +0,0 @@ -const std = @import("std"); -const interop = @import("../interop.zig"); - -const termbox = interop.termbox; - -pub const Cell = struct { - ch: u32, - fg: u32, - bg: u32, -}; - -pub fn initCell(ch: u32, fg: u32, bg: u32) Cell { - return .{ - .ch = ch, - .fg = fg, - .bg = bg, - }; -} - -pub fn putCell(x: usize, y: usize, cell: Cell) void { - _ = termbox.tb_set_cell(@intCast(x), @intCast(y), cell.ch, cell.fg, cell.bg); -} - -// Every codepoint is assumed to have a width of 1. -// Since ly should be running in a tty, this should be fine. -pub fn strWidth(str: []const u8) !u8 { - const utf8view = try std.unicode.Utf8View.init(str); - var utf8 = utf8view.iterator(); - var i: u8 = 0; - while (utf8.nextCodepoint()) |_| i += 1; - return i; -} From 13ba52319cd4e307885bdedbeb3c2147a74cdc7e Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Sun, 16 Mar 2025 11:40:27 +0100 Subject: [PATCH 178/530] Clean termbox2 usage + fix animation bug Signed-off-by: AnErrupTion --- src/animations/Doom.zig | 26 +++++++++++++------------- src/animations/Dummy.zig | 14 ++++++++++++++ src/animations/Matrix.zig | 16 +++++++--------- src/config/migrator.zig | 28 +++++++++++++--------------- src/main.zig | 10 +++++++--- src/tui/TerminalBuffer.zig | 23 +++++++++++++++++++++++ 6 files changed, 77 insertions(+), 40 deletions(-) create mode 100644 src/animations/Dummy.zig diff --git a/src/animations/Doom.zig b/src/animations/Doom.zig index 9e2b24a..f651ed4 100644 --- a/src/animations/Doom.zig +++ b/src/animations/Doom.zig @@ -22,19 +22,19 @@ pub fn init(allocator: Allocator, terminal_buffer: *TerminalBuffer, top_color: u .terminal_buffer = terminal_buffer, .buffer = buffer, .fire = [_]Cell{ - Cell.init(' ', 0x00000000, 0), - Cell.init(0x2591, top_color, 0), - Cell.init(0x2592, top_color, 0), - Cell.init(0x2593, top_color, 0), - Cell.init(0x2588, top_color, 0), - Cell.init(0x2591, middle_color, 2), - Cell.init(0x2592, middle_color, 2), - Cell.init(0x2593, middle_color, 2), - Cell.init(0x2588, middle_color, 2), - Cell.init(0x2591, bottom_color, 4), - Cell.init(0x2592, bottom_color, 4), - Cell.init(0x2593, bottom_color, 4), - Cell.init(0x2588, bottom_color, 4), + Cell.init(' ', TerminalBuffer.Color.DEFAULT, TerminalBuffer.Color.DEFAULT), + Cell.init(0x2591, top_color, TerminalBuffer.Color.DEFAULT), + Cell.init(0x2592, top_color, TerminalBuffer.Color.DEFAULT), + Cell.init(0x2593, top_color, TerminalBuffer.Color.DEFAULT), + Cell.init(0x2588, top_color, TerminalBuffer.Color.DEFAULT), + Cell.init(0x2591, middle_color, top_color), + Cell.init(0x2592, middle_color, top_color), + Cell.init(0x2593, middle_color, top_color), + Cell.init(0x2588, middle_color, top_color), + Cell.init(0x2591, bottom_color, middle_color), + Cell.init(0x2592, bottom_color, middle_color), + Cell.init(0x2593, bottom_color, middle_color), + Cell.init(0x2588, bottom_color, middle_color), }, }; } diff --git a/src/animations/Dummy.zig b/src/animations/Dummy.zig new file mode 100644 index 0000000..1dddfeb --- /dev/null +++ b/src/animations/Dummy.zig @@ -0,0 +1,14 @@ +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/animations/Matrix.zig b/src/animations/Matrix.zig index 9ebc0fb..4069b34 100644 --- a/src/animations/Matrix.zig +++ b/src/animations/Matrix.zig @@ -1,19 +1,17 @@ const std = @import("std"); -const interop = @import("../interop.zig"); 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; -const termbox = interop.termbox; pub const FRAME_DELAY: usize = 8; // Characters change mid-scroll pub const MID_SCROLL_CHANGE = true; -const DOT_HEAD_COLOR: u32 = @intCast(0x00FFFFFF | termbox.TB_BOLD); // White and bold +const DOT_HEAD_COLOR: u32 = @intCast(TerminalBuffer.Color.WHITE | TerminalBuffer.Styling.BOLD); const Matrix = @This(); @@ -34,12 +32,12 @@ dots: []Dot, lines: []Line, frame: usize, count: usize, -fg_ini: u32, +fg: u32, min_codepoint: u16, max_codepoint: u16, default_cell: Cell, -pub fn init(allocator: Allocator, terminal_buffer: *TerminalBuffer, fg_ini: u32, min_codepoint: u16, max_codepoint: u16) !Matrix { +pub fn init(allocator: Allocator, terminal_buffer: *TerminalBuffer, fg: u32, min_codepoint: u16, max_codepoint: u16) !Matrix { const dots = try allocator.alloc(Dot, terminal_buffer.width * (terminal_buffer.height + 1)); const lines = try allocator.alloc(Line, terminal_buffer.width); @@ -52,10 +50,10 @@ pub fn init(allocator: Allocator, terminal_buffer: *TerminalBuffer, fg_ini: u32, .lines = lines, .frame = 3, .count = 0, - .fg_ini = fg_ini, + .fg = fg, .min_codepoint = min_codepoint, .max_codepoint = max_codepoint - min_codepoint, - .default_cell = .{ .ch = ' ', .fg = fg_ini, .bg = termbox.TB_DEFAULT }, + .default_cell = .{ .ch = ' ', .fg = fg, .bg = terminal_buffer.bg }, }; } @@ -159,8 +157,8 @@ fn draw(self: *Matrix) void { 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.?), - .fg = if (dot.is_head) DOT_HEAD_COLOR else self.fg_ini, - .bg = termbox.TB_DEFAULT, + .fg = if (dot.is_head) DOT_HEAD_COLOR else self.fg, + .bg = self.terminal_buffer.bg, }; cell.put(x, y - 1); diff --git a/src/config/migrator.zig b/src/config/migrator.zig index 224c55b..2d2e34e 100644 --- a/src/config/migrator.zig +++ b/src/config/migrator.zig @@ -2,11 +2,9 @@ const std = @import("std"); const ini = @import("zigini"); -const interop = @import("../interop.zig"); const Save = @import("Save.zig"); const enums = @import("../enums.zig"); -const termbox = interop.termbox; const color_properties = [_][]const u8{ "bg", "border_fg", @@ -178,25 +176,25 @@ fn mapColor(color: u16) ![]const u8 { const color_no_styling = color & 0x00FF; const styling_only = color & 0xFF00; - if (color_no_styling > termbox.TB_WHITE or styling_only > 0x8000) { // TB_DIM in 16-bit mode - return error.InvalidColor; - } + // If color is "greater" than TB_WHITE, or the styling is "greater" than TB_DIM, + // we have an invalid color, so return an error + if (color_no_styling > 0x0008 or styling_only > 0x8000) return error.InvalidColor; var new_color: u32 = switch (color_no_styling) { - termbox.TB_DEFAULT => termbox.TB_DEFAULT, - termbox.TB_BLACK => termbox.TB_HI_BLACK, - termbox.TB_RED => 0x00FF0000, - termbox.TB_GREEN => 0x0000FF00, - termbox.TB_YELLOW => 0x00FFFF00, - termbox.TB_BLUE => 0x000000FF, - termbox.TB_MAGENTA => 0x00FF00FF, - termbox.TB_CYAN => 0x0000FFFF, - termbox.TB_WHITE => 0x00FFFFFF, + 0x0000 => 0x00000000, // Default + 0x0001 => 0x20000000, // "Hi-black" styling + 0x0002 => 0x00FF0000, // Red + 0x0003 => 0x0000FF00, // Green + 0x0004 => 0x00FFFF00, // Yellow + 0x0005 => 0x000000FF, // Blue + 0x0006 => 0x00FF00FF, // Magenta + 0x0007 => 0x0000FFFF, // Cyan + 0x0008 => 0x00FFFFFF, // White else => unreachable, }; // Only applying styling if color isn't black and styling isn't also black - if (!(new_color == termbox.TB_HI_BLACK and styling_only == termbox.TB_HI_BLACK)) { + if (!(new_color == 0x20000000 and styling_only == 0x20000000)) { // Shift styling by 16 to the left to apply it to the new 32-bit color new_color |= @as(u32, @intCast(styling_only)) << 16; } diff --git a/src/main.zig b/src/main.zig index d187010..93a677a 100644 --- a/src/main.zig +++ b/src/main.zig @@ -8,9 +8,10 @@ const bigclock = @import("bigclock.zig"); const enums = @import("enums.zig"); const Environment = @import("Environment.zig"); const interop = @import("interop.zig"); -const Doom = @import("animations/Doom.zig"); -const Matrix = @import("animations/Matrix.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 Animation = @import("tui/Animation.zig"); const TerminalBuffer = @import("tui/TerminalBuffer.zig"); const Session = @import("tui/components/Session.zig"); @@ -339,7 +340,10 @@ pub fn main() !void { var animation: Animation = undefined; switch (config.animation) { - .none => {}, + .none => { + var dummy = Dummy{}; + animation = dummy.animation(); + }, .doom => { var doom = try Doom.init(allocator, &buffer, config.doom_top_color, config.doom_middle_color, config.doom_bottom_color); animation = doom.animation(); diff --git a/src/tui/TerminalBuffer.zig b/src/tui/TerminalBuffer.zig index 4fe2dae..ca072f6 100644 --- a/src/tui/TerminalBuffer.zig +++ b/src/tui/TerminalBuffer.zig @@ -18,6 +18,29 @@ pub const InitOptions = struct { input_len: u8, }; +pub const Styling = struct { + pub const BOLD = termbox.TB_BOLD; + pub const UNDERLINE = termbox.TB_UNDERLINE; + pub const REVERSE = termbox.TB_REVERSE; + pub const ITALIC = termbox.TB_ITALIC; + pub const BLINK = termbox.TB_BLINK; + pub const HI_BLACK = termbox.TB_HI_BLACK; + pub const BRIGHT = termbox.TB_BRIGHT; + pub const DIM = termbox.TB_DIM; +}; + +pub const Color = struct { + pub const DEFAULT = 0x00000000; + pub const BLACK = Styling.HI_BLACK; + pub const RED = 0x00FF0000; + pub const GREEN = 0x0000FF00; + pub const YELLOW = 0x00FFFF00; + pub const BLUE = 0x000000FF; + pub const MAGENTA = 0x00FF00FF; + pub const CYAN = 0x0000FFFF; + pub const WHITE = 0x00FFFFFF; +}; + random: Random, width: usize, height: usize, From 9ded9fd7653a1aaa7af250522d63e73516975296 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Sun, 16 Mar 2025 22:45:46 +0100 Subject: [PATCH 179/530] Remove use of deprecated aliases/types + use upstream zigini Signed-off-by: AnErrupTion --- build.zig.zon | 2 +- src/main.zig | 8 ++++---- src/tui/components/InfoLine.zig | 2 +- src/tui/components/Session.zig | 2 +- src/tui/components/Text.zig | 10 +++++----- src/tui/components/generic.zig | 10 +++++----- 6 files changed, 17 insertions(+), 17 deletions(-) diff --git a/build.zig.zon b/build.zig.zon index 3bfddce..48c58f3 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -9,7 +9,7 @@ .hash = "clap-0.10.0-oBajB434AQBDh-Ei3YtoKIRxZacVPF1iSwp3IX_ZB8f0", }, .zigini = .{ - .url = "https://github.com/AnErrupTion/zigini/archive/e61d31b2b7db3365993a20cc90e491d0cb0b7282.tar.gz", + .url = "https://github.com/Kawaii-Ash/zigini/archive/2ed3d417f17fab5b0ee8cad8a63c6d62d7ac1042.tar.gz", .hash = "zigini-0.3.1-BSkB7XJGAAB2E-sKyzhTaQCBlYBL8yqzE4E_jmSY99sC", }, }, diff --git a/src/main.zig b/src/main.zig index 93a677a..569fa88 100644 --- a/src/main.zig +++ b/src/main.zig @@ -303,7 +303,7 @@ pub fn main() !void { // Load last saved username and desktop selection, if any if (config.load) { if (save.user) |user| { - try login.text.appendSlice(user); + try login.text.appendSlice(login.allocator, user); login.end = user.len; login.cursor = login.end; active_input = .password; @@ -387,7 +387,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 (!update or animate) { - if (!update) std.time.sleep(std.time.ns_per_ms * 100); + if (!update) std.Thread.sleep(std.time.ns_per_ms * 100); _ = termbox.tb_present(); // Required to update tb_width() and tb_height() @@ -551,11 +551,11 @@ pub fn main() !void { login.draw(); password.draw(); } else { - std.time.sleep(std.time.ns_per_ms * 10); + std.Thread.sleep(std.time.ns_per_ms * 10); update = buffer.cascade(); if (!update) { - std.time.sleep(std.time.ns_per_s * 7); + std.Thread.sleep(std.time.ns_per_s * 7); auth_fails = 0; } } diff --git a/src/tui/components/InfoLine.zig b/src/tui/components/InfoLine.zig index 42436d9..7d588fb 100644 --- a/src/tui/components/InfoLine.zig +++ b/src/tui/components/InfoLine.zig @@ -23,7 +23,7 @@ pub fn init(allocator: Allocator, buffer: *TerminalBuffer) InfoLine { }; } -pub fn deinit(self: InfoLine) void { +pub fn deinit(self: *InfoLine) void { self.label.deinit(); } diff --git a/src/tui/components/Session.zig b/src/tui/components/Session.zig index 4135283..2656025 100644 --- a/src/tui/components/Session.zig +++ b/src/tui/components/Session.zig @@ -20,7 +20,7 @@ pub fn init(allocator: Allocator, buffer: *TerminalBuffer) Session { }; } -pub fn deinit(self: Session) void { +pub fn deinit(self: *Session) void { for (self.label.list.items) |*environment| { if (environment.entry_ini) |*entry_ini| entry_ini.deinit(); if (environment.xdg_session_desktop) |session_desktop| self.label.allocator.free(session_desktop); diff --git a/src/tui/components/Text.zig b/src/tui/components/Text.zig index f2502d5..f205b91 100644 --- a/src/tui/components/Text.zig +++ b/src/tui/components/Text.zig @@ -3,7 +3,7 @@ const interop = @import("../../interop.zig"); const TerminalBuffer = @import("../TerminalBuffer.zig"); const Allocator = std.mem.Allocator; -const DynamicString = std.ArrayList(u8); +const DynamicString = std.ArrayListUnmanaged(u8); const termbox = interop.termbox; @@ -22,7 +22,7 @@ masked: bool, maybe_mask: ?u32, pub fn init(allocator: Allocator, buffer: *TerminalBuffer, masked: bool, maybe_mask: ?u32) Text { - const text = DynamicString.init(allocator); + const text: DynamicString = .empty; return .{ .allocator = allocator, @@ -39,8 +39,8 @@ pub fn init(allocator: Allocator, buffer: *TerminalBuffer, masked: bool, maybe_m }; } -pub fn deinit(self: Text) void { - self.text.deinit(); +pub fn deinit(self: *Text) void { + self.text.deinit(self.allocator); } pub fn position(self: *Text, x: usize, y: usize, visible_length: usize) void { @@ -153,7 +153,7 @@ fn backspace(self: *Text) void { fn write(self: *Text, char: u8) !void { if (char == 0) return; - try self.text.insert(self.cursor, char); + try self.text.insert(self.allocator, self.cursor, char); self.end += 1; self.goRight(); diff --git a/src/tui/components/generic.zig b/src/tui/components/generic.zig index 060a3fe..322bc8f 100644 --- a/src/tui/components/generic.zig +++ b/src/tui/components/generic.zig @@ -5,7 +5,7 @@ const TerminalBuffer = @import("../TerminalBuffer.zig"); pub fn CyclableLabel(comptime ItemType: type) type { return struct { const Allocator = std.mem.Allocator; - const ItemList = std.ArrayList(ItemType); + const ItemList = std.ArrayListUnmanaged(ItemType); const DrawItemFn = *const fn (*Self, ItemType, usize, usize) bool; const termbox = interop.termbox; @@ -27,7 +27,7 @@ pub fn CyclableLabel(comptime ItemType: type) type { return .{ .allocator = allocator, .buffer = buffer, - .list = ItemList.init(allocator), + .list = .empty, .current = 0, .visible_length = 0, .x = 0, @@ -38,8 +38,8 @@ pub fn CyclableLabel(comptime ItemType: type) type { }; } - pub fn deinit(self: Self) void { - self.list.deinit(); + pub fn deinit(self: *Self) void { + self.list.deinit(self.allocator); } pub fn position(self: *Self, x: usize, y: usize, visible_length: usize, text_in_center: ?bool) void { @@ -53,7 +53,7 @@ pub fn CyclableLabel(comptime ItemType: type) type { } pub fn addItem(self: *Self, item: ItemType) !void { - try self.list.append(item); + try self.list.append(self.allocator, item); self.current = self.list.items.len - 1; } From 32793bcfeb35115a868aae64d0839baeb595bfe8 Mon Sep 17 00:00:00 2001 From: Chiron8 Date: Mon, 17 Mar 2025 16:18:51 +0000 Subject: [PATCH 180/530] Update readme.md --- readme.md | 64 +++++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 50 insertions(+), 14 deletions(-) diff --git a/readme.md b/readme.md index 0386dd1..1270870 100644 --- a/readme.md +++ b/readme.md @@ -32,33 +32,69 @@ It is recommended to add a rule for Ly as it currently does not ship one. ## Support The following desktop environments were tested with success: - - awesome - - bspwm +

+Wayland environments +
- budgie - - cinnamon +
- cosmic +
- deepin - - dwl - - dwm +
- enlightenment +
- gnome +
- hyprland - - i3 +
- kde +
- labwc - - leftwm - - lxde - - lxqt - - mate - - maxx - - niri +
- pantheon - - qtile - - spectrwm +
- sway +
+ - weston +
+ +
+X11 environments +
+ - awesome +
+ - bspwm +
+ - budgie +
+ - cinnamon +
+ - enlightenment +
+ - kde +
+ - leftwm +
+ - lxde +
+ - mate +
+ - maxx +
+ - niri +
+ - pantheon +
+ - qwm +
+ - spectrwm +
- windowmaker +
- xfce +
- xmonad +
Ly should work with any X desktop environment, and provides basic wayland support (sway works very well, for example). From beb6d04c582913c76edae6c351a8b6363a78d0bf Mon Sep 17 00:00:00 2001 From: Chiron8 Date: Mon, 17 Mar 2025 16:19:43 +0000 Subject: [PATCH 181/530] Update readme.md --- readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/readme.md b/readme.md index 1270870..ed003f4 100644 --- a/readme.md +++ b/readme.md @@ -59,7 +59,7 @@ The following desktop environments were tested with success:
-X11 environments +X environments
- awesome
From c624c35ac7655ec18b3b52eb79fe0435bfa31c07 Mon Sep 17 00:00:00 2001 From: Chiron8 Date: Mon, 17 Mar 2025 16:25:06 +0000 Subject: [PATCH 182/530] Update readme.md --- readme.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/readme.md b/readme.md index ed003f4..3ec05a7 100644 --- a/readme.md +++ b/readme.md @@ -34,7 +34,6 @@ It is recommended to add a rule for Ly as it currently does not ship one. The following desktop environments were tested with success:
Wayland environments -
- budgie
- cosmic @@ -60,7 +59,6 @@ The following desktop environments were tested with success:
X environments -
- awesome
- bspwm From 585ca5f0da42917ed365d6f8a78105072afc3aba Mon Sep 17 00:00:00 2001 From: Chiron8 Date: Thu, 20 Mar 2025 08:24:25 +0000 Subject: [PATCH 183/530] Update readme.md --- readme.md | 96 +++++++++++++++++++++---------------------------------- 1 file changed, 36 insertions(+), 60 deletions(-) diff --git a/readme.md b/readme.md index 3ec05a7..335243b 100644 --- a/readme.md +++ b/readme.md @@ -32,67 +32,10 @@ It is recommended to add a rule for Ly as it currently does not ship one. ## Support The following desktop environments were tested with success: -
-Wayland environments - - budgie -
- - cosmic -
- - deepin -
- - enlightenment -
- - gnome -
- - hyprland -
- - kde -
- - labwc -
- - pantheon -
- - sway -
- - weston -
-
-X environments - - awesome -
- - bspwm -
- - budgie -
- - cinnamon -
- - enlightenment -
- - kde -
- - leftwm -
- - lxde -
- - mate -
- - maxx -
- - niri -
- - pantheon -
- - qwm -
- - spectrwm -
- - windowmaker -
- - xfce -
- - xmonad -
+[Wayland Environments](#supported-wayland-environments) + +[X11 Environments](#supported-x11-environments) Ly should work with any X desktop environment, and provides basic wayland support (sway works very well, for example). @@ -282,6 +225,39 @@ On Arch Linux, the example .xinitrc (/etc/X11/xinit/xinitrc) starts like this: - Take a look at your .xsession 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 + - pantheon + - sway + - weston + +## Supported X11 Environments + - awesome + - bspwm + - budgie + - cinnamon + - enlightenment + - kde + - leftwm + - lxde + - mate + - maxx + - niri + - pantheon + - qwm + - spectrwm + - windowmaker + - xfce + - xmonad + + ## Additional Information The name "Ly" is a tribute to the fairy from the game Rayman. Ly was tested by oxodao, who is some seriously awesome dude. From 7747b27f9d6ee3ef52f567563c4377c545e3f81b Mon Sep 17 00:00:00 2001 From: Chiron8 Date: Thu, 20 Mar 2025 13:08:13 +0000 Subject: [PATCH 184/530] Update readme.md --- readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/readme.md b/readme.md index 335243b..1ee879a 100644 --- a/readme.md +++ b/readme.md @@ -234,6 +234,7 @@ On Arch Linux, the example .xinitrc (/etc/X11/xinit/xinitrc) starts like this: - hyprland - kde - labwc + - niri - pantheon - sway - weston @@ -249,7 +250,6 @@ On Arch Linux, the example .xinitrc (/etc/X11/xinit/xinitrc) starts like this: - lxde - mate - maxx - - niri - pantheon - qwm - spectrwm From be5a68dd1d33bc0e43749504e5edc83cb0331958 Mon Sep 17 00:00:00 2001 From: jack-avery <47289484+jack-avery@users.noreply.github.com> Date: Fri, 28 Mar 2025 00:47:12 -0400 Subject: [PATCH 185/530] allow specify multiple dirs --- src/main.zig | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/main.zig b/src/main.zig index 569fa88..b27117b 100644 --- a/src/main.zig +++ b/src/main.zig @@ -288,8 +288,16 @@ pub fn main() !void { try info_line.addMessage(hostname, config.bg, config.fg); } - try crawl(&session, lang, config.waylandsessions, .wayland); - if (build_options.enable_x11_support) try crawl(&session, lang, config.xsessions, .x11); + var waylandsessiondirs = std.mem.splitScalar(u8, config.waylandsessions, ':'); + while (waylandsessiondirs.next()) |dir| { + try crawl(&session, lang, dir, .wayland); + } + if (build_options.enable_x11_support) { + var xsessiondirs = std.mem.splitScalar(u8, config.xsessions, ':'); + while (xsessiondirs.next()) |dir| { + try crawl(&session, lang, dir, .x11); + } + } var login = Text.init(allocator, &buffer, false, null); defer login.deinit(); From b7e37ce1b7f300f378eb7cb9253b131ea8127155 Mon Sep 17 00:00:00 2001 From: jack-avery <47289484+jack-avery@users.noreply.github.com> Date: Fri, 28 Mar 2025 11:00:47 -0400 Subject: [PATCH 186/530] update config comments --- res/config.ini | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/res/config.ini b/res/config.ini index c197fb8..53050e4 100644 --- a/res/config.ini +++ b/res/config.ini @@ -222,6 +222,8 @@ vi_default_mode = normal vi_mode = false # Wayland desktop environments +# You can specify multiple directories, +# e.g. /usr/share/wayland-sessions:/usr/local/share/wayland-sessions waylandsessions = $PREFIX_DIRECTORY/share/wayland-sessions # Xorg server command @@ -235,4 +237,6 @@ xauth_cmd = $PREFIX_DIRECTORY/bin/xauth xinitrc = ~/.xinitrc # Xorg desktop environments +# You can specify multiple directories, +# e.g. /usr/share/xsessions:/usr/local/share/xsessions xsessions = $PREFIX_DIRECTORY/share/xsessions From fecc6884189cedf03082e9eeb8a57af6dfb0180f Mon Sep 17 00:00:00 2001 From: jack-avery <47289484+jack-avery@users.noreply.github.com> Date: Fri, 28 Mar 2025 11:01:33 -0400 Subject: [PATCH 187/530] snake case --- src/main.zig | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main.zig b/src/main.zig index b27117b..bd89b15 100644 --- a/src/main.zig +++ b/src/main.zig @@ -288,13 +288,13 @@ pub fn main() !void { try info_line.addMessage(hostname, config.bg, config.fg); } - var waylandsessiondirs = std.mem.splitScalar(u8, config.waylandsessions, ':'); - while (waylandsessiondirs.next()) |dir| { + var wayland_session_dirs = std.mem.splitScalar(u8, config.waylandsessions, ':'); + while (wayland_session_dirs.next()) |dir| { try crawl(&session, lang, dir, .wayland); } if (build_options.enable_x11_support) { - var xsessiondirs = std.mem.splitScalar(u8, config.xsessions, ':'); - while (xsessiondirs.next()) |dir| { + var x_session_dirs = std.mem.splitScalar(u8, config.xsessions, ':'); + while (x_session_dirs.next()) |dir| { try crawl(&session, lang, dir, .x11); } } From d2803194f36a60663b9f010af7844cd3a3549b53 Mon Sep 17 00:00:00 2001 From: "Mohamed A. Abdallah" Date: Fri, 28 Mar 2025 22:57:13 +0200 Subject: [PATCH 188/530] Add Arabic localization file --- res/lang/ar.ini | 64 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 res/lang/ar.ini diff --git a/res/lang/ar.ini b/res/lang/ar.ini new file mode 100644 index 0000000..49accc5 --- /dev/null +++ b/res/lang/ar.ini @@ -0,0 +1,64 @@ +authenticating = authenticating... +brightness_down = decrease brightness +brightness_up = increase brightness +capslock = capslock +err_alloc = failed memory allocation +err_bounds = out-of-bounds index +err_brightness_change = failed to change brightness +err_chdir = failed to open home folder +err_config = unable to parse config file +err_console_dev = failed to access console +err_dgn_oob = log message +err_domain = invalid domain +err_empty_password = empty password not allowed +err_envlist = failed to get envlist +err_hostname = failed to get hostname +err_mlock = failed to lock password memory +err_null = null pointer +err_numlock = failed to set numlock +err_pam = pam transaction failed +err_pam_abort = pam transaction aborted +err_pam_acct_expired = account expired +err_pam_auth = authentication error +err_pam_authinfo_unavail = failed to get user info +err_pam_authok_reqd = token expired +err_pam_buf = memory buffer error +err_pam_cred_err = failed to set credentials +err_pam_cred_expired = credentials expired +err_pam_cred_insufficient = insufficient credentials +err_pam_cred_unavail = failed to get credentials +err_pam_maxtries = reached maximum tries limit +err_pam_perm_denied = permission denied +err_pam_session = session error +err_pam_sys = system error +err_pam_user_unknown = unknown user +err_path = failed to set path +err_perm_dir = failed to change current directory +err_perm_group = failed to downgrade group permissions +err_perm_user = failed to downgrade user permissions +err_pwnam = failed to get user info +err_sleep = failed to execute sleep command +err_tty_ctrl = tty control transfer failed +err_unknown = an unknown error occurred +err_user_gid = failed to set user GID +err_user_init = failed to initialize user +err_user_uid = failed to set user UID +err_xauth = xauth command failed +err_xcb_conn = xcb connection failed +err_xsessions_dir = failed to find sessions folder +err_xsessions_open = failed to open sessions folder +insert = insert +login = login +logout = logged out +no_x11_support = x11 support disabled at compile-time +normal = normal +numlock = numlock +other = other +password = password +restart = reboot +shell = shell +shutdown = shutdown +sleep = sleep +wayland = wayland +x11 = x11 +xinitrc = xinitrc From c6a3223a034eb96f2b8cdcea86133ac311eb75a1 Mon Sep 17 00:00:00 2001 From: "Mohamed A. Abdallah" Date: Fri, 28 Mar 2025 23:10:09 +0200 Subject: [PATCH 189/530] Update Arabic localization for actions --- res/lang/ar.ini | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/res/lang/ar.ini b/res/lang/ar.ini index 49accc5..961806e 100644 --- a/res/lang/ar.ini +++ b/res/lang/ar.ini @@ -1,6 +1,6 @@ -authenticating = authenticating... -brightness_down = decrease brightness -brightness_up = increase brightness +authenticating = جاري المصادقة... +brightness_down = خفض السطوع +brightness_up = رفع السطوع capslock = capslock err_alloc = failed memory allocation err_bounds = out-of-bounds index @@ -47,18 +47,18 @@ err_xauth = xauth command failed err_xcb_conn = xcb connection failed err_xsessions_dir = failed to find sessions folder err_xsessions_open = failed to open sessions folder -insert = insert -login = login -logout = logged out -no_x11_support = x11 support disabled at compile-time -normal = normal +insert = ادخال +login = تسجيل الدخول +logout = تم تسجيل خروجك +no_x11_support = تم تعطيل دعم x11 اثناء وقت الـ compile +normal = عادي numlock = numlock -other = other -password = password -restart = reboot +other = اخر +password = كلمة السر +restart = اعادة التشغيل shell = shell -shutdown = shutdown -sleep = sleep +shutdown = ايقاف التشغيل +sleep = وضع السكون wayland = wayland x11 = x11 xinitrc = xinitrc From 751d31cae271636c8767f421875ddc4eb1878c72 Mon Sep 17 00:00:00 2001 From: "Mohamed A. Abdallah" Date: Fri, 28 Mar 2025 23:26:17 +0200 Subject: [PATCH 190/530] Update Arabic error messages for errors --- res/lang/ar.ini | 88 ++++++++++++++++++++++++------------------------- 1 file changed, 44 insertions(+), 44 deletions(-) diff --git a/res/lang/ar.ini b/res/lang/ar.ini index 961806e..86393df 100644 --- a/res/lang/ar.ini +++ b/res/lang/ar.ini @@ -2,51 +2,51 @@ authenticating = جاري المصادقة... brightness_down = خفض السطوع brightness_up = رفع السطوع capslock = capslock -err_alloc = failed memory allocation +err_alloc = فشل في تخصيص الذاكرة err_bounds = out-of-bounds index -err_brightness_change = failed to change brightness -err_chdir = failed to open home folder -err_config = unable to parse config file -err_console_dev = failed to access console -err_dgn_oob = log message -err_domain = invalid domain -err_empty_password = empty password not allowed -err_envlist = failed to get envlist -err_hostname = failed to get hostname -err_mlock = failed to lock password memory -err_null = null pointer -err_numlock = failed to set numlock -err_pam = pam transaction failed -err_pam_abort = pam transaction aborted -err_pam_acct_expired = account expired -err_pam_auth = authentication error -err_pam_authinfo_unavail = failed to get user info -err_pam_authok_reqd = token expired -err_pam_buf = memory buffer error -err_pam_cred_err = failed to set credentials -err_pam_cred_expired = credentials expired -err_pam_cred_insufficient = insufficient credentials -err_pam_cred_unavail = failed to get credentials -err_pam_maxtries = reached maximum tries limit -err_pam_perm_denied = permission denied -err_pam_session = session error -err_pam_sys = system error -err_pam_user_unknown = unknown user -err_path = failed to set path -err_perm_dir = failed to change current directory -err_perm_group = failed to downgrade group permissions -err_perm_user = failed to downgrade user permissions -err_pwnam = failed to get user info -err_sleep = failed to execute sleep command -err_tty_ctrl = tty control transfer failed -err_unknown = an unknown error occurred -err_user_gid = failed to set user GID -err_user_init = failed to initialize user -err_user_uid = failed to set user UID -err_xauth = xauth command failed -err_xcb_conn = xcb connection failed -err_xsessions_dir = failed to find sessions folder -err_xsessions_open = failed to open sessions folder +err_brightness_change = فشل في تغيير سطوع الشاشة +err_chdir = فشل في فتح مجلد المنزل +err_config = فشل في تفسير ملف الإعدادات +err_console_dev = فشل في الوصول إلى جهاز وحدة التحكم +err_dgn_oob = رسالة سجل (Log) +err_domain = اسم نطاق غير صالح +err_empty_password = لا يُسمح بكلمة مرور فارغة +err_envlist = فشل في جلب قائمة المتغيرات البيئية +err_hostname = فشل في جلب اسم المضيف (Hostname) +err_mlock = فشل في تأمين ذاكرة كلمة المرور (mlock) +err_null = مؤشر فارغ (Null pointer) +err_numlock = فشل في ضبط Num Lock +err_pam = فشل في معاملة PAM +err_pam_abort = تم إلغاء معاملة PAM +err_pam_acct_expired = الحساب منتهي الصلاحية +err_pam_auth = خطأ في المصادقة (Authentication error) +err_pam_authinfo_unavail = فشل في الحصول على معلومات المستخدم +err_pam_authok_reqd = انتهت صلاحية رمز المصادقة (Token) +err_pam_buf = خطأ في ذاكرة التخزين المؤقت (Buffer) +err_pam_cred_err = فشل في تعيين بيانات الاعتماد (Credentials) +err_pam_cred_expired = بيانات الاعتماد منتهية الصلاحية +err_pam_cred_insufficient = بيانات الاعتماد غير كافية +err_pam_cred_unavail = فشل في الحصول على بيانات الاعتماد +err_pam_maxtries = تم بلوغ الحد الأقصى لمحاولات المصادقة +err_pam_perm_denied = تم رفض الوصول (Permission denied) +err_pam_session = خطأ في جلسة المستخدم (Session error) +err_pam_sys = خطأ في النظام (System error) +err_pam_user_unknown = المستخدم غير موجود +err_path = فشل في تعيين متغير PATH +err_perm_dir = فشل في تغيير المجلد الحالي +err_perm_group = فشل في تخفيض صلاحيات المجموعة (Group permissions) +err_perm_user = فشل في تخفيض صلاحيات المستخدم (User permissions) +err_pwnam = فشل في جلب معلومات المستخدم +err_sleep = فشل في تنفيذ أمر sleep +err_tty_ctrl = فشل في نقل تحكم الطرفية (TTY) +err_unknown = حدث خطأ غير معروف +err_user_gid = فشل في تعيين معرّف المجموعة (GID) للمستخدم +err_user_init = فشل في تهيئة بيانات المستخدم +err_user_uid = فشل في تعيين معرّف المستخدم (UID) +err_xauth = فشل في تنفيذ أمر xauth +err_xcb_conn = فشل في الاتصال بمكتبة XCB +err_xsessions_dir = فشل في العثور على مجلد Xsessions +err_xsessions_open = فشل في فتح مجلد Xsessions insert = ادخال login = تسجيل الدخول logout = تم تسجيل خروجك From 555e72a38890f9878ff29cd3758c230b3fa7c07d Mon Sep 17 00:00:00 2001 From: ManogyaDahal Date: Mon, 28 Apr 2025 14:18:12 +0545 Subject: [PATCH 191/530] Updae: Added dwm in Readme --- readme.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/readme.md b/readme.md index 1ee879a..ec3b649 100644 --- a/readme.md +++ b/readme.md @@ -244,7 +244,9 @@ On Arch Linux, the example .xinitrc (/etc/X11/xinit/xinitrc) starts like this: - bspwm - budgie - cinnamon + - dwm - enlightenment + - gnome - kde - leftwm - lxde From f74c4e92a946a73384e6e2a26402849f8216ef04 Mon Sep 17 00:00:00 2001 From: Bluudek Date: Wed, 7 May 2025 23:31:31 +0200 Subject: [PATCH 192/530] Update Polish missing messages --- res/lang/pl.ini | 38 +++++++++++++++++++------------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/res/lang/pl.ini b/res/lang/pl.ini index 93ec5ba..7271c00 100644 --- a/res/lang/pl.ini +++ b/res/lang/pl.ini @@ -1,21 +1,21 @@ - - - +authenticating = uwierzytelnianie... +brightness_down = zmniejsz jasność +brightness_up = zwiększ jasność capslock = capslock err_alloc = nieudana alokacja pamięci err_bounds = indeks poza granicami - +err_brightness_change = nie udało się zmienić jasności err_chdir = nie udało się otworzyć folderu domowego - +err_config = nie można przetworzyć pliku konfiguracyjnego err_console_dev = nie udało się uzyskać dostępu do konsoli 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_hostname = nie udało się uzyskać nazwy hosta err_mlock = nie udało się zablokować pamięci haseł err_null = wskaźnik zerowy - +err_numlock = nie udało się ustawić numlock err_pam = transakcja pam nieudana err_pam_abort = transakcja pam przerwana err_pam_acct_expired = konto wygasło @@ -37,28 +37,28 @@ err_perm_dir = nie udało się zmienić obecnego katalogu 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_tty_ctrl = nie udało się przekazać kontroli tty +err_unknown = wystąpił nieznany błąd 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 - - +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 - +insert = wstaw login = login logout = wylogowano - - +no_x11_support = wsparcie X11 wyłączone podczas kompilacji +normal = normalny numlock = numlock - +other = inny password = hasło restart = uruchom ponownie shell = powłoka shutdown = wyłącz - +sleep = uśpij wayland = wayland - +x11 = x11 xinitrc = xinitrc From 71a06e13a6033649d52a8f15c798e85df7829370 Mon Sep 17 00:00:00 2001 From: Bluudek Date: Thu, 8 May 2025 00:12:50 +0200 Subject: [PATCH 193/530] Update Polish phrases --- res/lang/pl.ini | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/res/lang/pl.ini b/res/lang/pl.ini index 7271c00..3f0d4e8 100644 --- a/res/lang/pl.ini +++ b/res/lang/pl.ini @@ -3,7 +3,7 @@ brightness_down = zmniejsz jasność brightness_up = zwiększ jasność capslock = capslock err_alloc = nieudana alokacja pamięci -err_bounds = indeks poza granicami +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_config = nie można przetworzyć pliku konfiguracyjnego @@ -14,12 +14,12 @@ err_empty_password = puste hasło jest niedozwolone err_envlist = nie udało się pobrać listy zmiennych środowiskowych err_hostname = nie udało się uzyskać nazwy hosta err_mlock = nie udało się zablokować pamięci haseł -err_null = wskaźnik zerowy +err_null = pusty wskaźnik err_numlock = nie udało się ustawić numlock err_pam = transakcja pam nieudana err_pam_abort = transakcja pam przerwana err_pam_acct_expired = konto wygasło -err_pam_auth = błąd autentyfikacji +err_pam_auth = błąd uwierzytelniania err_pam_authinfo_unavail = nie udało się zdobyć informacji o użytkowniku err_pam_authok_reqd = token wygasł err_pam_buf = błąd bufora pamięci @@ -28,7 +28,7 @@ err_pam_cred_expired = uwierzytelnienie wygasło err_pam_cred_insufficient = niewystarczające uwierzytelnienie err_pam_cred_unavail = nie udało się uzyskać uwierzytelnienia err_pam_maxtries = osiągnięto limit prób -err_pam_perm_denied = brak uprawnień +err_pam_perm_denied = odmowa dostępu err_pam_session = błąd sesji err_pam_sys = błąd systemu err_pam_user_unknown = nieznany użytkownik From 732888fd940955d4de5e7538ebf592785faaf282 Mon Sep 17 00:00:00 2001 From: Bluudek Date: Thu, 8 May 2025 00:16:14 +0200 Subject: [PATCH 194/530] Remove invisible Unicode characters --- res/lang/pl.ini | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/res/lang/pl.ini b/res/lang/pl.ini index 3f0d4e8..abef3a8 100644 --- a/res/lang/pl.ini +++ b/res/lang/pl.ini @@ -7,7 +7,7 @@ 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_config = nie można przetworzyć pliku konfiguracyjnego -err_console_dev = nie udało się uzyskać dostępu do konsoli +err_console_dev = nie udało się uzyskać dostępu do konsoli err_dgn_oob = wiadomość loga err_domain = niepoprawna domena err_empty_password = puste hasło jest niedozwolone @@ -23,30 +23,30 @@ err_pam_auth = błąd uwierzytelniania err_pam_authinfo_unavail = nie udało się zdobyć informacji o użytkowniku err_pam_authok_reqd = token wygasł err_pam_buf = błąd bufora pamięci -err_pam_cred_err = nie udało się ustawić uwierzytelnienia +err_pam_cred_err = nie udało się ustawić uwierzytelnienia err_pam_cred_expired = uwierzytelnienie wygasło err_pam_cred_insufficient = niewystarczające uwierzytelnienie -err_pam_cred_unavail = nie udało się uzyskać uwierzytelnienia +err_pam_cred_unavail = nie udało się uzyskać uwierzytelnienia err_pam_maxtries = osiągnięto limit prób err_pam_perm_denied = odmowa dostępu err_pam_session = błąd sesji err_pam_sys = błąd systemu err_pam_user_unknown = nieznany użytkownik -err_path = nie udało się ustawić ścieżki -err_perm_dir = nie udało się zmienić obecnego katalogu -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_path = nie udało się ustawić ścieżki +err_perm_dir = nie udało się zmienić obecnego katalogu +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_tty_ctrl = nie udało się przekazać kontroli tty err_unknown = wystąpił nieznany błąd err_user_gid = nie udało się ustawić GID użytkownika -err_user_init = nie udało się zainicjalizować użytkownika +err_user_init = nie udało się zainicjalizować użytkownika err_user_uid = nie udało się ustawić UID użytkownika 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 +err_xsessions_dir = nie udało się znaleźć folderu sesji +err_xsessions_open = nie udało się otworzyć folderu sesji insert = wstaw login = login logout = wylogowano From 98af3a98c8e2cc05e1739f03becb325956b54994 Mon Sep 17 00:00:00 2001 From: thoxy Date: Tue, 27 May 2025 07:07:37 +0200 Subject: [PATCH 195/530] Add GameOfLife Animation --- src/animations/GameOfLife.zig | 272 ++++++++++++++++++++++++++++++++++ src/enums.zig | 1 + src/main.zig | 5 + 3 files changed, 278 insertions(+) create mode 100644 src/animations/GameOfLife.zig diff --git a/src/animations/GameOfLife.zig b/src/animations/GameOfLife.zig new file mode 100644 index 0000000..1130262 --- /dev/null +++ b/src/animations/GameOfLife.zig @@ -0,0 +1,272 @@ +const std = @import("std"); +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; + +const GameOfLife = @This(); + +pub const FRAME_DELAY: usize = 6; // Slightly faster for smoother animation +pub const INITIAL_DENSITY: f32 = 0.4; // Increased for more activity +pub const COLOR_CYCLE_DELAY: usize = 192; // Change color every N frames + +// Visual styles - using block characters like other animations +const ALIVE_CHAR: u21 = 0x2588; // Full block █ +const DEAD_CHAR: u21 = ' '; + +// ANSI basic colors using TerminalBuffer.Color like other animations +const ANSI_COLORS = [_]u32{ + @intCast(TerminalBuffer.Color.RED), + @intCast(TerminalBuffer.Color.GREEN), + @intCast(TerminalBuffer.Color.YELLOW), + @intCast(TerminalBuffer.Color.BLUE), + @intCast(TerminalBuffer.Color.MAGENTA), + @intCast(TerminalBuffer.Color.CYAN), + @intCast(TerminalBuffer.Color.RED | TerminalBuffer.Styling.BOLD), + @intCast(TerminalBuffer.Color.GREEN | TerminalBuffer.Styling.BOLD), + @intCast(TerminalBuffer.Color.YELLOW | TerminalBuffer.Styling.BOLD), + @intCast(TerminalBuffer.Color.BLUE | TerminalBuffer.Styling.BOLD), + @intCast(TerminalBuffer.Color.MAGENTA | TerminalBuffer.Styling.BOLD), + @intCast(TerminalBuffer.Color.CYAN | TerminalBuffer.Styling.BOLD), +}; +const NUM_COLORS = ANSI_COLORS.len; +const NEIGHBOR_DIRS = [_][2]i8{ + .{ -1, -1 }, .{ -1, 0 }, .{ -1, 1 }, + .{ 0, -1 }, .{ 0, 1 }, .{ 1, -1 }, + .{ 1, 0 }, .{ 1, 1 }, +}; + +allocator: Allocator, +terminal_buffer: *TerminalBuffer, +current_grid: []bool, +next_grid: []bool, +frame_counter: usize, +generation: u64, +color_index: usize, +color_counter: usize, +dead_cell: Cell, +width: usize, +height: usize, + +pub fn init(allocator: Allocator, terminal_buffer: *TerminalBuffer) !GameOfLife { + const width = terminal_buffer.width; + const height = terminal_buffer.height; + const grid_size = width * height; + + const current_grid = try allocator.alloc(bool, grid_size); + const next_grid = try allocator.alloc(bool, grid_size); + + var game = GameOfLife{ + .allocator = allocator, + .terminal_buffer = terminal_buffer, + .current_grid = current_grid, + .next_grid = next_grid, + .frame_counter = 0, + .generation = 0, + .color_index = 0, + .color_counter = 0, + .dead_cell = .{ .ch = DEAD_CHAR, .fg = @intCast(TerminalBuffer.Color.DEFAULT), .bg = terminal_buffer.bg }, + .width = width, + .height = height, + }; + + // Initialize grid + game.initializeGrid(); + + return game; +} + +pub fn animation(self: *GameOfLife) Animation { + return Animation.init(self, deinit, realloc, draw); +} + +fn deinit(self: *GameOfLife) void { + self.allocator.free(self.current_grid); + self.allocator.free(self.next_grid); +} + +fn realloc(self: *GameOfLife) anyerror!void { + const new_width = self.terminal_buffer.width; + const new_height = self.terminal_buffer.height; + const new_size = new_width * new_height; + + // Only reallocate if size changed significantly + if (new_size != self.width * self.height) { + const current_grid = try self.allocator.realloc(self.current_grid, new_size); + const next_grid = try self.allocator.realloc(self.next_grid, new_size); + + self.current_grid = current_grid; + self.next_grid = next_grid; + self.width = new_width; + self.height = new_height; + + self.initializeGrid(); + self.generation = 0; + self.color_index = 0; + self.color_counter = 0; + } +} + +fn draw(self: *GameOfLife) void { + // Update ANSI color cycling at controlled rate + self.color_counter += 1; + if (self.color_counter >= COLOR_CYCLE_DELAY) { + self.color_counter = 0; + self.color_index = (self.color_index + 1) % NUM_COLORS; + } + + // Update game state at controlled frame rate + self.frame_counter += 1; + if (self.frame_counter >= FRAME_DELAY) { + self.frame_counter = 0; + self.updateGeneration(); + self.generation += 1; + + // Add entropy less frequently to reduce computational overhead + if (self.generation % 150 == 0) { + self.addEntropy(); + } + } + + // Render with ANSI color cycling - use current color from the array (same method as Matrix/Doom) + const current_color = ANSI_COLORS[self.color_index]; + const alive_cell = Cell{ .ch = ALIVE_CHAR, .fg = current_color, .bg = self.terminal_buffer.bg }; + + for (0..self.height) |y| { + 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); + } + } +} + +fn updateGeneration(self: *GameOfLife) void { + // Conway's Game of Life rules with optimized neighbor counting + for (0..self.height) |y| { + const row_offset = y * self.width; + for (0..self.width) |x| { + const index = row_offset + x; + const neighbors = self.countNeighborsOptimized(x, y); + const is_alive = self.current_grid[index]; + + // Optimized rule application + self.next_grid[index] = switch (neighbors) { + 2 => is_alive, + 3 => true, + else => false, + }; + } + } + + // Efficient grid swap + std.mem.swap([]bool, &self.current_grid, &self.next_grid); +} + +fn countNeighborsOptimized(self: *GameOfLife, x: usize, y: usize) u8 { + var count: u8 = 0; + + // Use cached dimensions and more efficient bounds checking + for (NEIGHBOR_DIRS) |dir| { + const nx = @as(i32, @intCast(x)) + dir[0]; + const ny = @as(i32, @intCast(y)) + dir[1]; + + // Toroidal wrapping with modular arithmetic + const wx: usize = @intCast(@mod(nx + @as(i32, @intCast(self.width)), @as(i32, @intCast(self.width)))); + const wy: usize = @intCast(@mod(ny + @as(i32, @intCast(self.height)), @as(i32, @intCast(self.height)))); + + if (self.current_grid[wy * self.width + wx]) { + count += 1; + } + } + + return count; +} + +fn initializeGrid(self: *GameOfLife) void { + const total_cells = self.width * self.height; + + // Clear grid + @memset(self.current_grid, false); + @memset(self.next_grid, false); + + // Random initialization with better distribution + for (0..total_cells) |i| { + self.current_grid[i] = self.terminal_buffer.random.float(f32) < INITIAL_DENSITY; + } + + // Add interesting patterns with better positioning + self.addPatterns(); +} + +fn addPatterns(self: *GameOfLife) void { + if (self.width < 8 or self.height < 8) return; + + // Add multiple instances of each pattern for liveliness + for (0..3) |_| { + self.addGlider(); + if (self.width >= 10 and self.height >= 10) { + self.addBlock(); + self.addBlinker(); + } + } +} + +fn addGlider(self: *GameOfLife) void { + const x = self.terminal_buffer.random.intRangeAtMost(usize, 2, self.width - 4); + const y = self.terminal_buffer.random.intRangeAtMost(usize, 2, self.height - 4); + + // Classic glider pattern + const positions = [_][2]usize{ .{ 1, 0 }, .{ 2, 1 }, .{ 0, 2 }, .{ 1, 2 }, .{ 2, 2 } }; + + for (positions) |pos| { + const idx = (y + pos[1]) * self.width + (x + pos[0]); + self.current_grid[idx] = true; + } +} + +fn addBlock(self: *GameOfLife) void { + const x = self.terminal_buffer.random.intRangeAtMost(usize, 1, self.width - 3); + const y = self.terminal_buffer.random.intRangeAtMost(usize, 1, self.height - 3); + + // 2x2 block + const positions = [_][2]usize{ .{ 0, 0 }, .{ 1, 0 }, .{ 0, 1 }, .{ 1, 1 } }; + + for (positions) |pos| { + const idx = (y + pos[1]) * self.width + (x + pos[0]); + self.current_grid[idx] = true; + } +} + +fn addBlinker(self: *GameOfLife) void { + const x = self.terminal_buffer.random.intRangeAtMost(usize, 1, self.width - 4); + const y = self.terminal_buffer.random.intRangeAtMost(usize, 1, self.height - 2); + + // 3-cell horizontal line + for (0..3) |i| { + const idx = y * self.width + (x + i); + self.current_grid[idx] = true; + } +} + +fn addEntropy(self: *GameOfLife) void { + // Add fewer random cells but in clusters for more interesting patterns + const clusters = 2; + for (0..clusters) |_| { + const cx = self.terminal_buffer.random.intRangeAtMost(usize, 1, self.width - 2); + const cy = self.terminal_buffer.random.intRangeAtMost(usize, 1, self.height - 2); + + // Small cluster around center point + for (0..3) |dy| { + for (0..3) |dx| { + if (self.terminal_buffer.random.float(f32) < 0.4) { + const x = (cx + dx) % self.width; + const y = (cy + dy) % self.height; + self.current_grid[y * self.width + x] = true; + } + } + } + } +} diff --git a/src/enums.zig b/src/enums.zig index 6d1f1f3..a70f17c 100644 --- a/src/enums.zig +++ b/src/enums.zig @@ -3,6 +3,7 @@ pub const Animation = enum { doom, matrix, colormix, + gameoflife, }; pub const DisplayServer = enum { diff --git a/src/main.zig b/src/main.zig index bd89b15..259a5e3 100644 --- a/src/main.zig +++ b/src/main.zig @@ -12,6 +12,7 @@ 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 Animation = @import("tui/Animation.zig"); const TerminalBuffer = @import("tui/TerminalBuffer.zig"); const Session = @import("tui/components/Session.zig"); @@ -364,6 +365,10 @@ pub fn main() !void { 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); + animation = game_of_life.animation(); + }, } defer animation.deinit(); From e90cf40e5b9b6d00523dd3c39c01763d7db7a45e Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Thu, 29 May 2025 23:52:58 +0200 Subject: [PATCH 196/530] Update termbox2 Signed-off-by: AnErrupTion --- include/termbox2.h | 1050 ++++++++++++++++++++++++++++++++++++++------ 1 file changed, 913 insertions(+), 137 deletions(-) diff --git a/include/termbox2.h b/include/termbox2.h index 297d0be..831b824 100644 --- a/include/termbox2.h +++ b/include/termbox2.h @@ -2,7 +2,7 @@ MIT License Copyright (c) 2010-2020 nsf - 2015-2024 Adam Saponara + 2015-2025 Adam Saponara Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -52,7 +52,6 @@ SOFTWARE. #include #include #include -#include #ifdef PATH_MAX #define TB_PATH_MAX PATH_MAX @@ -66,28 +65,33 @@ extern "C" { // __ffi_start -#define TB_VERSION_STR "2.5.0" +#define TB_VERSION_STR "2.6.0-dev" /* The following compile-time options are supported: * - * TB_OPT_ATTR_W: Integer width of fg and bg attributes. Valid values + * TB_OPT_ATTR_W: Integer width of `fg` and `bg` attributes. Valid values * (assuming system support) are 16, 32, and 64. (See - * uintattr_t). 32 or 64 enables output mode - * TB_OUTPUT_TRUECOLOR. 64 enables additional style - * attributes. (See tb_set_output_mode.) Larger values + * `uintattr_t`). 32 or 64 enables output mode + * `TB_OUTPUT_TRUECOLOR`. 64 enables additional style + * attributes. (See `tb_set_output_mode`.) Larger values * consume more memory in exchange for more features. * Defaults to 16. * * TB_OPT_EGC: If set, enable extended grapheme cluster support - * (tb_extend_cell, tb_set_cell_ex). Consumes more memory. - * Defaults off. + * (`tb_extend_cell`, `tb_set_cell_ex`). Consumes more + * memory. Defaults off. * * TB_OPT_PRINTF_BUF: Write buffer size for printf operations. Represents the - * largest string that can be sent in one call to tb_print* - * and tb_send* functions. Defaults to 4096. + * largest string that can be sent in one call to + * `tb_print*` and `tb_send*` functions. Defaults to 4096. * * TB_OPT_READ_BUF: Read buffer size for tty reads. Defaults to 64. * + * TB_OPT_LIBC_WCHAR: If set, use libc's `wcwidth(3)`, `iswprint(3)`, etc + * instead of the built-in Unicode-aware versions. Note, + * libc's are locale-dependent and the caller must + * `setlocale(3)` `LC_CTYPE` to UTF-8. Defaults to built-in. + * * TB_OPT_TRUECOLOR: Deprecated. Sets TB_OPT_ATTR_W to 32 if not already set. */ @@ -97,6 +101,7 @@ extern "C" { #undef TB_OPT_EGC #undef TB_OPT_PRINTF_BUF #undef TB_OPT_READ_BUF +#undef TB_OPT_LIBC_WCHAR #define TB_OPT_ATTR_W 64 #define TB_OPT_EGC #endif @@ -481,7 +486,8 @@ int tb_present(void); /* Clear the internal front buffer effectively forcing a complete re-render of * the back buffer to the tty. It is not necessary to call this under normal - * circumstances. */ + * circumstances. + */ int tb_invalidate(void); /* Set the position of the cursor. Upper-left cell is (0, 0). */ @@ -651,7 +657,8 @@ int tb_poll_event(struct tb_event *event); /* Internal termbox fds that can be used with `poll(2)`, `select(2)`, etc. * externally. Callers must invoke `tb_poll_event` or `tb_peek_event` if - * fds become readable. */ + * fds become readable. + */ int tb_get_fds(int *ttyfd, int *resizefd); /* Print and printf functions. Specify param `out_w` to determine width of @@ -722,6 +729,8 @@ int tb_has_truecolor(void); int tb_has_egc(void); int tb_attr_width(void); const char *tb_version(void); +int tb_iswprint(uint32_t ch); +int tb_wcwidth(uint32_t ch); /* Deprecation notice! * @@ -732,7 +741,7 @@ const char *tb_version(void); * TB_TRUECOLOR_BOLD (use TB_BOLD) * TB_TRUECOLOR_UNDERLINE (use TB_UNDERLINE) * TB_TRUECOLOR_REVERSE (use TB_REVERSE) - * TB_TRUECOLOR_ITALIC (use TB_ITALICe) + * TB_TRUECOLOR_ITALIC (use TB_ITALIC) * TB_TRUECOLOR_BLINK (use TB_BLINK) * TB_TRUECOLOR_BLACK (use TB_HI_BLACK) * tb_cell_buffer @@ -1149,7 +1158,7 @@ static struct { const uint16_t key; const uint8_t mod; } builtin_mod_caps[] = { - // xterm arrows + // xterm arrows {"\x1b[1;2A", TB_KEY_ARROW_UP, TB_MOD_SHIFT }, {"\x1b[1;3A", TB_KEY_ARROW_UP, TB_MOD_ALT }, {"\x1b[1;4A", TB_KEY_ARROW_UP, TB_MOD_ALT | TB_MOD_SHIFT }, @@ -1182,7 +1191,7 @@ static struct { {"\x1b[1;7D", TB_KEY_ARROW_LEFT, TB_MOD_CTRL | TB_MOD_ALT }, {"\x1b[1;8D", TB_KEY_ARROW_LEFT, TB_MOD_CTRL | TB_MOD_ALT | TB_MOD_SHIFT}, - // xterm keys + // xterm keys {"\x1b[1;2H", TB_KEY_HOME, TB_MOD_SHIFT }, {"\x1b[1;3H", TB_KEY_HOME, TB_MOD_ALT }, {"\x1b[1;4H", TB_KEY_HOME, TB_MOD_ALT | TB_MOD_SHIFT }, @@ -1327,7 +1336,7 @@ static struct { {"\x1b[24;7~", TB_KEY_F12, TB_MOD_CTRL | TB_MOD_ALT }, {"\x1b[24;8~", TB_KEY_F12, TB_MOD_CTRL | TB_MOD_ALT | TB_MOD_SHIFT}, - // rxvt arrows + // rxvt arrows {"\x1b[a", TB_KEY_ARROW_UP, TB_MOD_SHIFT }, {"\x1b\x1b[A", TB_KEY_ARROW_UP, TB_MOD_ALT }, {"\x1b\x1b[a", TB_KEY_ARROW_UP, TB_MOD_ALT | TB_MOD_SHIFT }, @@ -1352,7 +1361,7 @@ static struct { {"\x1bOd", TB_KEY_ARROW_LEFT, TB_MOD_CTRL }, {"\x1b\x1bOd", TB_KEY_ARROW_LEFT, TB_MOD_CTRL | TB_MOD_ALT }, - // rxvt keys + // rxvt keys {"\x1b[7$", TB_KEY_HOME, TB_MOD_SHIFT }, {"\x1b\x1b[7~", TB_KEY_HOME, TB_MOD_ALT }, {"\x1b\x1b[7$", TB_KEY_HOME, TB_MOD_ALT | TB_MOD_SHIFT }, @@ -1497,13 +1506,13 @@ static struct { {"\x1b[24@", TB_KEY_F12, TB_MOD_CTRL | TB_MOD_SHIFT }, {"\x1b[24$", TB_KEY_F12, TB_MOD_SHIFT }, - // linux console/putty arrows + // linux console/putty arrows {"\x1b[A", TB_KEY_ARROW_UP, TB_MOD_SHIFT }, {"\x1b[B", TB_KEY_ARROW_DOWN, TB_MOD_SHIFT }, {"\x1b[C", TB_KEY_ARROW_RIGHT, TB_MOD_SHIFT }, {"\x1b[D", TB_KEY_ARROW_LEFT, TB_MOD_SHIFT }, - // more putty arrows + // more putty arrows {"\x1bOA", TB_KEY_ARROW_UP, TB_MOD_CTRL }, {"\x1b\x1bOA", TB_KEY_ARROW_UP, TB_MOD_CTRL | TB_MOD_ALT }, {"\x1bOB", TB_KEY_ARROW_DOWN, TB_MOD_CTRL }, @@ -1530,6 +1539,733 @@ static const unsigned char utf8_length[256] = {1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, static const unsigned char utf8_mask[6] = {0x7f, 0x1f, 0x0f, 0x07, 0x03, 0x01}; +#ifndef TB_OPT_LIBC_WCHAR +static struct { + uint32_t range_start; + uint32_t range_end; + int width; // -1 means iswprint==0, otherwise wcwidth value (0, 1, or 2) +} wcwidth_table[] = { + // clang-format off + {0x000001, 0x00001f, -1}, {0x000020, 0x00007e, 1}, {0x00007f, 0x00009f, -1}, + {0x0000a0, 0x0002ff, 1}, {0x000300, 0x00036f, 0}, {0x000370, 0x000377, 1}, + {0x000378, 0x000379, -1}, {0x00037a, 0x00037f, 1}, {0x000380, 0x000383, -1}, + {0x000384, 0x00038a, 1}, {0x00038b, 0x00038b, -1}, {0x00038c, 0x00038c, 1}, + {0x00038d, 0x00038d, -1}, {0x00038e, 0x0003a1, 1}, {0x0003a2, 0x0003a2, -1}, + {0x0003a3, 0x000482, 1}, {0x000483, 0x000489, 0}, {0x00048a, 0x00052f, 1}, + {0x000530, 0x000530, -1}, {0x000531, 0x000556, 1}, {0x000557, 0x000558, -1}, + {0x000559, 0x00058a, 1}, {0x00058b, 0x00058c, -1}, {0x00058d, 0x00058f, 1}, + {0x000590, 0x000590, -1}, {0x000591, 0x0005bd, 0}, {0x0005be, 0x0005be, 1}, + {0x0005bf, 0x0005bf, 0}, {0x0005c0, 0x0005c0, 1}, {0x0005c1, 0x0005c2, 0}, + {0x0005c3, 0x0005c3, 1}, {0x0005c4, 0x0005c5, 0}, {0x0005c6, 0x0005c6, 1}, + {0x0005c7, 0x0005c7, 0}, {0x0005c8, 0x0005cf, -1}, {0x0005d0, 0x0005ea, 1}, + {0x0005eb, 0x0005ee, -1}, {0x0005ef, 0x0005f4, 1}, {0x0005f5, 0x0005ff, -1}, + {0x000600, 0x00060f, 1}, {0x000610, 0x00061a, 0}, {0x00061b, 0x00061b, 1}, + {0x00061c, 0x00061c, 0}, {0x00061d, 0x00064a, 1}, {0x00064b, 0x00065f, 0}, + {0x000660, 0x00066f, 1}, {0x000670, 0x000670, 0}, {0x000671, 0x0006d5, 1}, + {0x0006d6, 0x0006dc, 0}, {0x0006dd, 0x0006de, 1}, {0x0006df, 0x0006e4, 0}, + {0x0006e5, 0x0006e6, 1}, {0x0006e7, 0x0006e8, 0}, {0x0006e9, 0x0006e9, 1}, + {0x0006ea, 0x0006ed, 0}, {0x0006ee, 0x00070d, 1}, {0x00070e, 0x00070e, -1}, + {0x00070f, 0x000710, 1}, {0x000711, 0x000711, 0}, {0x000712, 0x00072f, 1}, + {0x000730, 0x00074a, 0}, {0x00074b, 0x00074c, -1}, {0x00074d, 0x0007a5, 1}, + {0x0007a6, 0x0007b0, 0}, {0x0007b1, 0x0007b1, 1}, {0x0007b2, 0x0007bf, -1}, + {0x0007c0, 0x0007ea, 1}, {0x0007eb, 0x0007f3, 0}, {0x0007f4, 0x0007fa, 1}, + {0x0007fb, 0x0007fc, -1}, {0x0007fd, 0x0007fd, 0}, {0x0007fe, 0x000815, 1}, + {0x000816, 0x000819, 0}, {0x00081a, 0x00081a, 1}, {0x00081b, 0x000823, 0}, + {0x000824, 0x000824, 1}, {0x000825, 0x000827, 0}, {0x000828, 0x000828, 1}, + {0x000829, 0x00082d, 0}, {0x00082e, 0x00082f, -1}, {0x000830, 0x00083e, 1}, + {0x00083f, 0x00083f, -1}, {0x000840, 0x000858, 1}, {0x000859, 0x00085b, 0}, + {0x00085c, 0x00085d, -1}, {0x00085e, 0x00085e, 1}, {0x00085f, 0x00085f, -1}, + {0x000860, 0x00086a, 1}, {0x00086b, 0x00086f, -1}, {0x000870, 0x00088e, 1}, + {0x00088f, 0x00088f, -1}, {0x000890, 0x000891, 1}, {0x000892, 0x000896, -1}, + {0x000897, 0x00089f, 0}, {0x0008a0, 0x0008c9, 1}, {0x0008ca, 0x0008e1, 0}, + {0x0008e2, 0x0008e2, 1}, {0x0008e3, 0x000902, 0}, {0x000903, 0x000939, 1}, + {0x00093a, 0x00093a, 0}, {0x00093b, 0x00093b, 1}, {0x00093c, 0x00093c, 0}, + {0x00093d, 0x000940, 1}, {0x000941, 0x000948, 0}, {0x000949, 0x00094c, 1}, + {0x00094d, 0x00094d, 0}, {0x00094e, 0x000950, 1}, {0x000951, 0x000957, 0}, + {0x000958, 0x000961, 1}, {0x000962, 0x000963, 0}, {0x000964, 0x000980, 1}, + {0x000981, 0x000981, 0}, {0x000982, 0x000983, 1}, {0x000984, 0x000984, -1}, + {0x000985, 0x00098c, 1}, {0x00098d, 0x00098e, -1}, {0x00098f, 0x000990, 1}, + {0x000991, 0x000992, -1}, {0x000993, 0x0009a8, 1}, {0x0009a9, 0x0009a9, -1}, + {0x0009aa, 0x0009b0, 1}, {0x0009b1, 0x0009b1, -1}, {0x0009b2, 0x0009b2, 1}, + {0x0009b3, 0x0009b5, -1}, {0x0009b6, 0x0009b9, 1}, {0x0009ba, 0x0009bb, -1}, + {0x0009bc, 0x0009bc, 0}, {0x0009bd, 0x0009c0, 1}, {0x0009c1, 0x0009c4, 0}, + {0x0009c5, 0x0009c6, -1}, {0x0009c7, 0x0009c8, 1}, {0x0009c9, 0x0009ca, -1}, + {0x0009cb, 0x0009cc, 1}, {0x0009cd, 0x0009cd, 0}, {0x0009ce, 0x0009ce, 1}, + {0x0009cf, 0x0009d6, -1}, {0x0009d7, 0x0009d7, 1}, {0x0009d8, 0x0009db, -1}, + {0x0009dc, 0x0009dd, 1}, {0x0009de, 0x0009de, -1}, {0x0009df, 0x0009e1, 1}, + {0x0009e2, 0x0009e3, 0}, {0x0009e4, 0x0009e5, -1}, {0x0009e6, 0x0009fd, 1}, + {0x0009fe, 0x0009fe, 0}, {0x0009ff, 0x000a00, -1}, {0x000a01, 0x000a02, 0}, + {0x000a03, 0x000a03, 1}, {0x000a04, 0x000a04, -1}, {0x000a05, 0x000a0a, 1}, + {0x000a0b, 0x000a0e, -1}, {0x000a0f, 0x000a10, 1}, {0x000a11, 0x000a12, -1}, + {0x000a13, 0x000a28, 1}, {0x000a29, 0x000a29, -1}, {0x000a2a, 0x000a30, 1}, + {0x000a31, 0x000a31, -1}, {0x000a32, 0x000a33, 1}, {0x000a34, 0x000a34, -1}, + {0x000a35, 0x000a36, 1}, {0x000a37, 0x000a37, -1}, {0x000a38, 0x000a39, 1}, + {0x000a3a, 0x000a3b, -1}, {0x000a3c, 0x000a3c, 0}, {0x000a3d, 0x000a3d, -1}, + {0x000a3e, 0x000a40, 1}, {0x000a41, 0x000a42, 0}, {0x000a43, 0x000a46, -1}, + {0x000a47, 0x000a48, 0}, {0x000a49, 0x000a4a, -1}, {0x000a4b, 0x000a4d, 0}, + {0x000a4e, 0x000a50, -1}, {0x000a51, 0x000a51, 0}, {0x000a52, 0x000a58, -1}, + {0x000a59, 0x000a5c, 1}, {0x000a5d, 0x000a5d, -1}, {0x000a5e, 0x000a5e, 1}, + {0x000a5f, 0x000a65, -1}, {0x000a66, 0x000a6f, 1}, {0x000a70, 0x000a71, 0}, + {0x000a72, 0x000a74, 1}, {0x000a75, 0x000a75, 0}, {0x000a76, 0x000a76, 1}, + {0x000a77, 0x000a80, -1}, {0x000a81, 0x000a82, 0}, {0x000a83, 0x000a83, 1}, + {0x000a84, 0x000a84, -1}, {0x000a85, 0x000a8d, 1}, {0x000a8e, 0x000a8e, -1}, + {0x000a8f, 0x000a91, 1}, {0x000a92, 0x000a92, -1}, {0x000a93, 0x000aa8, 1}, + {0x000aa9, 0x000aa9, -1}, {0x000aaa, 0x000ab0, 1}, {0x000ab1, 0x000ab1, -1}, + {0x000ab2, 0x000ab3, 1}, {0x000ab4, 0x000ab4, -1}, {0x000ab5, 0x000ab9, 1}, + {0x000aba, 0x000abb, -1}, {0x000abc, 0x000abc, 0}, {0x000abd, 0x000ac0, 1}, + {0x000ac1, 0x000ac5, 0}, {0x000ac6, 0x000ac6, -1}, {0x000ac7, 0x000ac8, 0}, + {0x000ac9, 0x000ac9, 1}, {0x000aca, 0x000aca, -1}, {0x000acb, 0x000acc, 1}, + {0x000acd, 0x000acd, 0}, {0x000ace, 0x000acf, -1}, {0x000ad0, 0x000ad0, 1}, + {0x000ad1, 0x000adf, -1}, {0x000ae0, 0x000ae1, 1}, {0x000ae2, 0x000ae3, 0}, + {0x000ae4, 0x000ae5, -1}, {0x000ae6, 0x000af1, 1}, {0x000af2, 0x000af8, -1}, + {0x000af9, 0x000af9, 1}, {0x000afa, 0x000aff, 0}, {0x000b00, 0x000b00, -1}, + {0x000b01, 0x000b01, 0}, {0x000b02, 0x000b03, 1}, {0x000b04, 0x000b04, -1}, + {0x000b05, 0x000b0c, 1}, {0x000b0d, 0x000b0e, -1}, {0x000b0f, 0x000b10, 1}, + {0x000b11, 0x000b12, -1}, {0x000b13, 0x000b28, 1}, {0x000b29, 0x000b29, -1}, + {0x000b2a, 0x000b30, 1}, {0x000b31, 0x000b31, -1}, {0x000b32, 0x000b33, 1}, + {0x000b34, 0x000b34, -1}, {0x000b35, 0x000b39, 1}, {0x000b3a, 0x000b3b, -1}, + {0x000b3c, 0x000b3c, 0}, {0x000b3d, 0x000b3e, 1}, {0x000b3f, 0x000b3f, 0}, + {0x000b40, 0x000b40, 1}, {0x000b41, 0x000b44, 0}, {0x000b45, 0x000b46, -1}, + {0x000b47, 0x000b48, 1}, {0x000b49, 0x000b4a, -1}, {0x000b4b, 0x000b4c, 1}, + {0x000b4d, 0x000b4d, 0}, {0x000b4e, 0x000b54, -1}, {0x000b55, 0x000b56, 0}, + {0x000b57, 0x000b57, 1}, {0x000b58, 0x000b5b, -1}, {0x000b5c, 0x000b5d, 1}, + {0x000b5e, 0x000b5e, -1}, {0x000b5f, 0x000b61, 1}, {0x000b62, 0x000b63, 0}, + {0x000b64, 0x000b65, -1}, {0x000b66, 0x000b77, 1}, {0x000b78, 0x000b81, -1}, + {0x000b82, 0x000b82, 0}, {0x000b83, 0x000b83, 1}, {0x000b84, 0x000b84, -1}, + {0x000b85, 0x000b8a, 1}, {0x000b8b, 0x000b8d, -1}, {0x000b8e, 0x000b90, 1}, + {0x000b91, 0x000b91, -1}, {0x000b92, 0x000b95, 1}, {0x000b96, 0x000b98, -1}, + {0x000b99, 0x000b9a, 1}, {0x000b9b, 0x000b9b, -1}, {0x000b9c, 0x000b9c, 1}, + {0x000b9d, 0x000b9d, -1}, {0x000b9e, 0x000b9f, 1}, {0x000ba0, 0x000ba2, -1}, + {0x000ba3, 0x000ba4, 1}, {0x000ba5, 0x000ba7, -1}, {0x000ba8, 0x000baa, 1}, + {0x000bab, 0x000bad, -1}, {0x000bae, 0x000bb9, 1}, {0x000bba, 0x000bbd, -1}, + {0x000bbe, 0x000bbf, 1}, {0x000bc0, 0x000bc0, 0}, {0x000bc1, 0x000bc2, 1}, + {0x000bc3, 0x000bc5, -1}, {0x000bc6, 0x000bc8, 1}, {0x000bc9, 0x000bc9, -1}, + {0x000bca, 0x000bcc, 1}, {0x000bcd, 0x000bcd, 0}, {0x000bce, 0x000bcf, -1}, + {0x000bd0, 0x000bd0, 1}, {0x000bd1, 0x000bd6, -1}, {0x000bd7, 0x000bd7, 1}, + {0x000bd8, 0x000be5, -1}, {0x000be6, 0x000bfa, 1}, {0x000bfb, 0x000bff, -1}, + {0x000c00, 0x000c00, 0}, {0x000c01, 0x000c03, 1}, {0x000c04, 0x000c04, 0}, + {0x000c05, 0x000c0c, 1}, {0x000c0d, 0x000c0d, -1}, {0x000c0e, 0x000c10, 1}, + {0x000c11, 0x000c11, -1}, {0x000c12, 0x000c28, 1}, {0x000c29, 0x000c29, -1}, + {0x000c2a, 0x000c39, 1}, {0x000c3a, 0x000c3b, -1}, {0x000c3c, 0x000c3c, 0}, + {0x000c3d, 0x000c3d, 1}, {0x000c3e, 0x000c40, 0}, {0x000c41, 0x000c44, 1}, + {0x000c45, 0x000c45, -1}, {0x000c46, 0x000c48, 0}, {0x000c49, 0x000c49, -1}, + {0x000c4a, 0x000c4d, 0}, {0x000c4e, 0x000c54, -1}, {0x000c55, 0x000c56, 0}, + {0x000c57, 0x000c57, -1}, {0x000c58, 0x000c5a, 1}, {0x000c5b, 0x000c5c, -1}, + {0x000c5d, 0x000c5d, 1}, {0x000c5e, 0x000c5f, -1}, {0x000c60, 0x000c61, 1}, + {0x000c62, 0x000c63, 0}, {0x000c64, 0x000c65, -1}, {0x000c66, 0x000c6f, 1}, + {0x000c70, 0x000c76, -1}, {0x000c77, 0x000c80, 1}, {0x000c81, 0x000c81, 0}, + {0x000c82, 0x000c8c, 1}, {0x000c8d, 0x000c8d, -1}, {0x000c8e, 0x000c90, 1}, + {0x000c91, 0x000c91, -1}, {0x000c92, 0x000ca8, 1}, {0x000ca9, 0x000ca9, -1}, + {0x000caa, 0x000cb3, 1}, {0x000cb4, 0x000cb4, -1}, {0x000cb5, 0x000cb9, 1}, + {0x000cba, 0x000cbb, -1}, {0x000cbc, 0x000cbc, 0}, {0x000cbd, 0x000cbe, 1}, + {0x000cbf, 0x000cbf, 0}, {0x000cc0, 0x000cc4, 1}, {0x000cc5, 0x000cc5, -1}, + {0x000cc6, 0x000cc6, 0}, {0x000cc7, 0x000cc8, 1}, {0x000cc9, 0x000cc9, -1}, + {0x000cca, 0x000ccb, 1}, {0x000ccc, 0x000ccd, 0}, {0x000cce, 0x000cd4, -1}, + {0x000cd5, 0x000cd6, 1}, {0x000cd7, 0x000cdc, -1}, {0x000cdd, 0x000cde, 1}, + {0x000cdf, 0x000cdf, -1}, {0x000ce0, 0x000ce1, 1}, {0x000ce2, 0x000ce3, 0}, + {0x000ce4, 0x000ce5, -1}, {0x000ce6, 0x000cef, 1}, {0x000cf0, 0x000cf0, -1}, + {0x000cf1, 0x000cf3, 1}, {0x000cf4, 0x000cff, -1}, {0x000d00, 0x000d01, 0}, + {0x000d02, 0x000d0c, 1}, {0x000d0d, 0x000d0d, -1}, {0x000d0e, 0x000d10, 1}, + {0x000d11, 0x000d11, -1}, {0x000d12, 0x000d3a, 1}, {0x000d3b, 0x000d3c, 0}, + {0x000d3d, 0x000d40, 1}, {0x000d41, 0x000d44, 0}, {0x000d45, 0x000d45, -1}, + {0x000d46, 0x000d48, 1}, {0x000d49, 0x000d49, -1}, {0x000d4a, 0x000d4c, 1}, + {0x000d4d, 0x000d4d, 0}, {0x000d4e, 0x000d4f, 1}, {0x000d50, 0x000d53, -1}, + {0x000d54, 0x000d61, 1}, {0x000d62, 0x000d63, 0}, {0x000d64, 0x000d65, -1}, + {0x000d66, 0x000d7f, 1}, {0x000d80, 0x000d80, -1}, {0x000d81, 0x000d81, 0}, + {0x000d82, 0x000d83, 1}, {0x000d84, 0x000d84, -1}, {0x000d85, 0x000d96, 1}, + {0x000d97, 0x000d99, -1}, {0x000d9a, 0x000db1, 1}, {0x000db2, 0x000db2, -1}, + {0x000db3, 0x000dbb, 1}, {0x000dbc, 0x000dbc, -1}, {0x000dbd, 0x000dbd, 1}, + {0x000dbe, 0x000dbf, -1}, {0x000dc0, 0x000dc6, 1}, {0x000dc7, 0x000dc9, -1}, + {0x000dca, 0x000dca, 0}, {0x000dcb, 0x000dce, -1}, {0x000dcf, 0x000dd1, 1}, + {0x000dd2, 0x000dd4, 0}, {0x000dd5, 0x000dd5, -1}, {0x000dd6, 0x000dd6, 0}, + {0x000dd7, 0x000dd7, -1}, {0x000dd8, 0x000ddf, 1}, {0x000de0, 0x000de5, -1}, + {0x000de6, 0x000def, 1}, {0x000df0, 0x000df1, -1}, {0x000df2, 0x000df4, 1}, + {0x000df5, 0x000e00, -1}, {0x000e01, 0x000e30, 1}, {0x000e31, 0x000e31, 0}, + {0x000e32, 0x000e33, 1}, {0x000e34, 0x000e3a, 0}, {0x000e3b, 0x000e3e, -1}, + {0x000e3f, 0x000e46, 1}, {0x000e47, 0x000e4e, 0}, {0x000e4f, 0x000e5b, 1}, + {0x000e5c, 0x000e80, -1}, {0x000e81, 0x000e82, 1}, {0x000e83, 0x000e83, -1}, + {0x000e84, 0x000e84, 1}, {0x000e85, 0x000e85, -1}, {0x000e86, 0x000e8a, 1}, + {0x000e8b, 0x000e8b, -1}, {0x000e8c, 0x000ea3, 1}, {0x000ea4, 0x000ea4, -1}, + {0x000ea5, 0x000ea5, 1}, {0x000ea6, 0x000ea6, -1}, {0x000ea7, 0x000eb0, 1}, + {0x000eb1, 0x000eb1, 0}, {0x000eb2, 0x000eb3, 1}, {0x000eb4, 0x000ebc, 0}, + {0x000ebd, 0x000ebd, 1}, {0x000ebe, 0x000ebf, -1}, {0x000ec0, 0x000ec4, 1}, + {0x000ec5, 0x000ec5, -1}, {0x000ec6, 0x000ec6, 1}, {0x000ec7, 0x000ec7, -1}, + {0x000ec8, 0x000ece, 0}, {0x000ecf, 0x000ecf, -1}, {0x000ed0, 0x000ed9, 1}, + {0x000eda, 0x000edb, -1}, {0x000edc, 0x000edf, 1}, {0x000ee0, 0x000eff, -1}, + {0x000f00, 0x000f17, 1}, {0x000f18, 0x000f19, 0}, {0x000f1a, 0x000f34, 1}, + {0x000f35, 0x000f35, 0}, {0x000f36, 0x000f36, 1}, {0x000f37, 0x000f37, 0}, + {0x000f38, 0x000f38, 1}, {0x000f39, 0x000f39, 0}, {0x000f3a, 0x000f47, 1}, + {0x000f48, 0x000f48, -1}, {0x000f49, 0x000f6c, 1}, {0x000f6d, 0x000f70, -1}, + {0x000f71, 0x000f7e, 0}, {0x000f7f, 0x000f7f, 1}, {0x000f80, 0x000f84, 0}, + {0x000f85, 0x000f85, 1}, {0x000f86, 0x000f87, 0}, {0x000f88, 0x000f8c, 1}, + {0x000f8d, 0x000f97, 0}, {0x000f98, 0x000f98, -1}, {0x000f99, 0x000fbc, 0}, + {0x000fbd, 0x000fbd, -1}, {0x000fbe, 0x000fc5, 1}, {0x000fc6, 0x000fc6, 0}, + {0x000fc7, 0x000fcc, 1}, {0x000fcd, 0x000fcd, -1}, {0x000fce, 0x000fda, 1}, + {0x000fdb, 0x000fff, -1}, {0x001000, 0x00102c, 1}, {0x00102d, 0x001030, 0}, + {0x001031, 0x001031, 1}, {0x001032, 0x001037, 0}, {0x001038, 0x001038, 1}, + {0x001039, 0x00103a, 0}, {0x00103b, 0x00103c, 1}, {0x00103d, 0x00103e, 0}, + {0x00103f, 0x001057, 1}, {0x001058, 0x001059, 0}, {0x00105a, 0x00105d, 1}, + {0x00105e, 0x001060, 0}, {0x001061, 0x001070, 1}, {0x001071, 0x001074, 0}, + {0x001075, 0x001081, 1}, {0x001082, 0x001082, 0}, {0x001083, 0x001084, 1}, + {0x001085, 0x001086, 0}, {0x001087, 0x00108c, 1}, {0x00108d, 0x00108d, 0}, + {0x00108e, 0x00109c, 1}, {0x00109d, 0x00109d, 0}, {0x00109e, 0x0010c5, 1}, + {0x0010c6, 0x0010c6, -1}, {0x0010c7, 0x0010c7, 1}, {0x0010c8, 0x0010cc, -1}, + {0x0010cd, 0x0010cd, 1}, {0x0010ce, 0x0010cf, -1}, {0x0010d0, 0x0010ff, 1}, + {0x001100, 0x00115f, 2}, {0x001160, 0x0011ff, 0}, {0x001200, 0x001248, 1}, + {0x001249, 0x001249, -1}, {0x00124a, 0x00124d, 1}, {0x00124e, 0x00124f, -1}, + {0x001250, 0x001256, 1}, {0x001257, 0x001257, -1}, {0x001258, 0x001258, 1}, + {0x001259, 0x001259, -1}, {0x00125a, 0x00125d, 1}, {0x00125e, 0x00125f, -1}, + {0x001260, 0x001288, 1}, {0x001289, 0x001289, -1}, {0x00128a, 0x00128d, 1}, + {0x00128e, 0x00128f, -1}, {0x001290, 0x0012b0, 1}, {0x0012b1, 0x0012b1, -1}, + {0x0012b2, 0x0012b5, 1}, {0x0012b6, 0x0012b7, -1}, {0x0012b8, 0x0012be, 1}, + {0x0012bf, 0x0012bf, -1}, {0x0012c0, 0x0012c0, 1}, {0x0012c1, 0x0012c1, -1}, + {0x0012c2, 0x0012c5, 1}, {0x0012c6, 0x0012c7, -1}, {0x0012c8, 0x0012d6, 1}, + {0x0012d7, 0x0012d7, -1}, {0x0012d8, 0x001310, 1}, {0x001311, 0x001311, -1}, + {0x001312, 0x001315, 1}, {0x001316, 0x001317, -1}, {0x001318, 0x00135a, 1}, + {0x00135b, 0x00135c, -1}, {0x00135d, 0x00135f, 0}, {0x001360, 0x00137c, 1}, + {0x00137d, 0x00137f, -1}, {0x001380, 0x001399, 1}, {0x00139a, 0x00139f, -1}, + {0x0013a0, 0x0013f5, 1}, {0x0013f6, 0x0013f7, -1}, {0x0013f8, 0x0013fd, 1}, + {0x0013fe, 0x0013ff, -1}, {0x001400, 0x00169c, 1}, {0x00169d, 0x00169f, -1}, + {0x0016a0, 0x0016f8, 1}, {0x0016f9, 0x0016ff, -1}, {0x001700, 0x001711, 1}, + {0x001712, 0x001714, 0}, {0x001715, 0x001715, 1}, {0x001716, 0x00171e, -1}, + {0x00171f, 0x001731, 1}, {0x001732, 0x001733, 0}, {0x001734, 0x001736, 1}, + {0x001737, 0x00173f, -1}, {0x001740, 0x001751, 1}, {0x001752, 0x001753, 0}, + {0x001754, 0x00175f, -1}, {0x001760, 0x00176c, 1}, {0x00176d, 0x00176d, -1}, + {0x00176e, 0x001770, 1}, {0x001771, 0x001771, -1}, {0x001772, 0x001773, 0}, + {0x001774, 0x00177f, -1}, {0x001780, 0x0017b3, 1}, {0x0017b4, 0x0017b5, 0}, + {0x0017b6, 0x0017b6, 1}, {0x0017b7, 0x0017bd, 0}, {0x0017be, 0x0017c5, 1}, + {0x0017c6, 0x0017c6, 0}, {0x0017c7, 0x0017c8, 1}, {0x0017c9, 0x0017d3, 0}, + {0x0017d4, 0x0017dc, 1}, {0x0017dd, 0x0017dd, 0}, {0x0017de, 0x0017df, -1}, + {0x0017e0, 0x0017e9, 1}, {0x0017ea, 0x0017ef, -1}, {0x0017f0, 0x0017f9, 1}, + {0x0017fa, 0x0017ff, -1}, {0x001800, 0x00180a, 1}, {0x00180b, 0x00180f, 0}, + {0x001810, 0x001819, 1}, {0x00181a, 0x00181f, -1}, {0x001820, 0x001878, 1}, + {0x001879, 0x00187f, -1}, {0x001880, 0x001884, 1}, {0x001885, 0x001886, 0}, + {0x001887, 0x0018a8, 1}, {0x0018a9, 0x0018a9, 0}, {0x0018aa, 0x0018aa, 1}, + {0x0018ab, 0x0018af, -1}, {0x0018b0, 0x0018f5, 1}, {0x0018f6, 0x0018ff, -1}, + {0x001900, 0x00191e, 1}, {0x00191f, 0x00191f, -1}, {0x001920, 0x001922, 0}, + {0x001923, 0x001926, 1}, {0x001927, 0x001928, 0}, {0x001929, 0x00192b, 1}, + {0x00192c, 0x00192f, -1}, {0x001930, 0x001931, 1}, {0x001932, 0x001932, 0}, + {0x001933, 0x001938, 1}, {0x001939, 0x00193b, 0}, {0x00193c, 0x00193f, -1}, + {0x001940, 0x001940, 1}, {0x001941, 0x001943, -1}, {0x001944, 0x00196d, 1}, + {0x00196e, 0x00196f, -1}, {0x001970, 0x001974, 1}, {0x001975, 0x00197f, -1}, + {0x001980, 0x0019ab, 1}, {0x0019ac, 0x0019af, -1}, {0x0019b0, 0x0019c9, 1}, + {0x0019ca, 0x0019cf, -1}, {0x0019d0, 0x0019da, 1}, {0x0019db, 0x0019dd, -1}, + {0x0019de, 0x001a16, 1}, {0x001a17, 0x001a18, 0}, {0x001a19, 0x001a1a, 1}, + {0x001a1b, 0x001a1b, 0}, {0x001a1c, 0x001a1d, -1}, {0x001a1e, 0x001a55, 1}, + {0x001a56, 0x001a56, 0}, {0x001a57, 0x001a57, 1}, {0x001a58, 0x001a5e, 0}, + {0x001a5f, 0x001a5f, -1}, {0x001a60, 0x001a60, 0}, {0x001a61, 0x001a61, 1}, + {0x001a62, 0x001a62, 0}, {0x001a63, 0x001a64, 1}, {0x001a65, 0x001a6c, 0}, + {0x001a6d, 0x001a72, 1}, {0x001a73, 0x001a7c, 0}, {0x001a7d, 0x001a7e, -1}, + {0x001a7f, 0x001a7f, 0}, {0x001a80, 0x001a89, 1}, {0x001a8a, 0x001a8f, -1}, + {0x001a90, 0x001a99, 1}, {0x001a9a, 0x001a9f, -1}, {0x001aa0, 0x001aad, 1}, + {0x001aae, 0x001aaf, -1}, {0x001ab0, 0x001ace, 0}, {0x001acf, 0x001aff, -1}, + {0x001b00, 0x001b03, 0}, {0x001b04, 0x001b33, 1}, {0x001b34, 0x001b34, 0}, + {0x001b35, 0x001b35, 1}, {0x001b36, 0x001b3a, 0}, {0x001b3b, 0x001b3b, 1}, + {0x001b3c, 0x001b3c, 0}, {0x001b3d, 0x001b41, 1}, {0x001b42, 0x001b42, 0}, + {0x001b43, 0x001b4c, 1}, {0x001b4d, 0x001b4d, -1}, {0x001b4e, 0x001b6a, 1}, + {0x001b6b, 0x001b73, 0}, {0x001b74, 0x001b7f, 1}, {0x001b80, 0x001b81, 0}, + {0x001b82, 0x001ba1, 1}, {0x001ba2, 0x001ba5, 0}, {0x001ba6, 0x001ba7, 1}, + {0x001ba8, 0x001ba9, 0}, {0x001baa, 0x001baa, 1}, {0x001bab, 0x001bad, 0}, + {0x001bae, 0x001be5, 1}, {0x001be6, 0x001be6, 0}, {0x001be7, 0x001be7, 1}, + {0x001be8, 0x001be9, 0}, {0x001bea, 0x001bec, 1}, {0x001bed, 0x001bed, 0}, + {0x001bee, 0x001bee, 1}, {0x001bef, 0x001bf1, 0}, {0x001bf2, 0x001bf3, 1}, + {0x001bf4, 0x001bfb, -1}, {0x001bfc, 0x001c2b, 1}, {0x001c2c, 0x001c33, 0}, + {0x001c34, 0x001c35, 1}, {0x001c36, 0x001c37, 0}, {0x001c38, 0x001c3a, -1}, + {0x001c3b, 0x001c49, 1}, {0x001c4a, 0x001c4c, -1}, {0x001c4d, 0x001c8a, 1}, + {0x001c8b, 0x001c8f, -1}, {0x001c90, 0x001cba, 1}, {0x001cbb, 0x001cbc, -1}, + {0x001cbd, 0x001cc7, 1}, {0x001cc8, 0x001ccf, -1}, {0x001cd0, 0x001cd2, 0}, + {0x001cd3, 0x001cd3, 1}, {0x001cd4, 0x001ce0, 0}, {0x001ce1, 0x001ce1, 1}, + {0x001ce2, 0x001ce8, 0}, {0x001ce9, 0x001cec, 1}, {0x001ced, 0x001ced, 0}, + {0x001cee, 0x001cf3, 1}, {0x001cf4, 0x001cf4, 0}, {0x001cf5, 0x001cf7, 1}, + {0x001cf8, 0x001cf9, 0}, {0x001cfa, 0x001cfa, 1}, {0x001cfb, 0x001cff, -1}, + {0x001d00, 0x001dbf, 1}, {0x001dc0, 0x001dff, 0}, {0x001e00, 0x001f15, 1}, + {0x001f16, 0x001f17, -1}, {0x001f18, 0x001f1d, 1}, {0x001f1e, 0x001f1f, -1}, + {0x001f20, 0x001f45, 1}, {0x001f46, 0x001f47, -1}, {0x001f48, 0x001f4d, 1}, + {0x001f4e, 0x001f4f, -1}, {0x001f50, 0x001f57, 1}, {0x001f58, 0x001f58, -1}, + {0x001f59, 0x001f59, 1}, {0x001f5a, 0x001f5a, -1}, {0x001f5b, 0x001f5b, 1}, + {0x001f5c, 0x001f5c, -1}, {0x001f5d, 0x001f5d, 1}, {0x001f5e, 0x001f5e, -1}, + {0x001f5f, 0x001f7d, 1}, {0x001f7e, 0x001f7f, -1}, {0x001f80, 0x001fb4, 1}, + {0x001fb5, 0x001fb5, -1}, {0x001fb6, 0x001fc4, 1}, {0x001fc5, 0x001fc5, -1}, + {0x001fc6, 0x001fd3, 1}, {0x001fd4, 0x001fd5, -1}, {0x001fd6, 0x001fdb, 1}, + {0x001fdc, 0x001fdc, -1}, {0x001fdd, 0x001fef, 1}, {0x001ff0, 0x001ff1, -1}, + {0x001ff2, 0x001ff4, 1}, {0x001ff5, 0x001ff5, -1}, {0x001ff6, 0x001ffe, 1}, + {0x001fff, 0x001fff, -1}, {0x002000, 0x00200a, 1}, {0x00200b, 0x00200f, 0}, + {0x002010, 0x002027, 1}, {0x002028, 0x002029, -1}, {0x00202a, 0x00202e, 0}, + {0x00202f, 0x00205f, 1}, {0x002060, 0x002064, 0}, {0x002065, 0x002065, -1}, + {0x002066, 0x00206f, 0}, {0x002070, 0x002071, 1}, {0x002072, 0x002073, -1}, + {0x002074, 0x00208e, 1}, {0x00208f, 0x00208f, -1}, {0x002090, 0x00209c, 1}, + {0x00209d, 0x00209f, -1}, {0x0020a0, 0x0020c0, 1}, {0x0020c1, 0x0020cf, -1}, + {0x0020d0, 0x0020f0, 0}, {0x0020f1, 0x0020ff, -1}, {0x002100, 0x00218b, 1}, + {0x00218c, 0x00218f, -1}, {0x002190, 0x002319, 1}, {0x00231a, 0x00231b, 2}, + {0x00231c, 0x002328, 1}, {0x002329, 0x00232a, 2}, {0x00232b, 0x0023e8, 1}, + {0x0023e9, 0x0023ec, 2}, {0x0023ed, 0x0023ef, 1}, {0x0023f0, 0x0023f0, 2}, + {0x0023f1, 0x0023f2, 1}, {0x0023f3, 0x0023f3, 2}, {0x0023f4, 0x002429, 1}, + {0x00242a, 0x00243f, -1}, {0x002440, 0x00244a, 1}, {0x00244b, 0x00245f, -1}, + {0x002460, 0x0025fc, 1}, {0x0025fd, 0x0025fe, 2}, {0x0025ff, 0x002613, 1}, + {0x002614, 0x002615, 2}, {0x002616, 0x00262f, 1}, {0x002630, 0x002637, 2}, + {0x002638, 0x002647, 1}, {0x002648, 0x002653, 2}, {0x002654, 0x00267e, 1}, + {0x00267f, 0x00267f, 2}, {0x002680, 0x002689, 1}, {0x00268a, 0x00268f, 2}, + {0x002690, 0x002692, 1}, {0x002693, 0x002693, 2}, {0x002694, 0x0026a0, 1}, + {0x0026a1, 0x0026a1, 2}, {0x0026a2, 0x0026a9, 1}, {0x0026aa, 0x0026ab, 2}, + {0x0026ac, 0x0026bc, 1}, {0x0026bd, 0x0026be, 2}, {0x0026bf, 0x0026c3, 1}, + {0x0026c4, 0x0026c5, 2}, {0x0026c6, 0x0026cd, 1}, {0x0026ce, 0x0026ce, 2}, + {0x0026cf, 0x0026d3, 1}, {0x0026d4, 0x0026d4, 2}, {0x0026d5, 0x0026e9, 1}, + {0x0026ea, 0x0026ea, 2}, {0x0026eb, 0x0026f1, 1}, {0x0026f2, 0x0026f3, 2}, + {0x0026f4, 0x0026f4, 1}, {0x0026f5, 0x0026f5, 2}, {0x0026f6, 0x0026f9, 1}, + {0x0026fa, 0x0026fa, 2}, {0x0026fb, 0x0026fc, 1}, {0x0026fd, 0x0026fd, 2}, + {0x0026fe, 0x002704, 1}, {0x002705, 0x002705, 2}, {0x002706, 0x002709, 1}, + {0x00270a, 0x00270b, 2}, {0x00270c, 0x002727, 1}, {0x002728, 0x002728, 2}, + {0x002729, 0x00274b, 1}, {0x00274c, 0x00274c, 2}, {0x00274d, 0x00274d, 1}, + {0x00274e, 0x00274e, 2}, {0x00274f, 0x002752, 1}, {0x002753, 0x002755, 2}, + {0x002756, 0x002756, 1}, {0x002757, 0x002757, 2}, {0x002758, 0x002794, 1}, + {0x002795, 0x002797, 2}, {0x002798, 0x0027af, 1}, {0x0027b0, 0x0027b0, 2}, + {0x0027b1, 0x0027be, 1}, {0x0027bf, 0x0027bf, 2}, {0x0027c0, 0x002b1a, 1}, + {0x002b1b, 0x002b1c, 2}, {0x002b1d, 0x002b4f, 1}, {0x002b50, 0x002b50, 2}, + {0x002b51, 0x002b54, 1}, {0x002b55, 0x002b55, 2}, {0x002b56, 0x002b73, 1}, + {0x002b74, 0x002b75, -1}, {0x002b76, 0x002b95, 1}, {0x002b96, 0x002b96, -1}, + {0x002b97, 0x002cee, 1}, {0x002cef, 0x002cf1, 0}, {0x002cf2, 0x002cf3, 1}, + {0x002cf4, 0x002cf8, -1}, {0x002cf9, 0x002d25, 1}, {0x002d26, 0x002d26, -1}, + {0x002d27, 0x002d27, 1}, {0x002d28, 0x002d2c, -1}, {0x002d2d, 0x002d2d, 1}, + {0x002d2e, 0x002d2f, -1}, {0x002d30, 0x002d67, 1}, {0x002d68, 0x002d6e, -1}, + {0x002d6f, 0x002d70, 1}, {0x002d71, 0x002d7e, -1}, {0x002d7f, 0x002d7f, 0}, + {0x002d80, 0x002d96, 1}, {0x002d97, 0x002d9f, -1}, {0x002da0, 0x002da6, 1}, + {0x002da7, 0x002da7, -1}, {0x002da8, 0x002dae, 1}, {0x002daf, 0x002daf, -1}, + {0x002db0, 0x002db6, 1}, {0x002db7, 0x002db7, -1}, {0x002db8, 0x002dbe, 1}, + {0x002dbf, 0x002dbf, -1}, {0x002dc0, 0x002dc6, 1}, {0x002dc7, 0x002dc7, -1}, + {0x002dc8, 0x002dce, 1}, {0x002dcf, 0x002dcf, -1}, {0x002dd0, 0x002dd6, 1}, + {0x002dd7, 0x002dd7, -1}, {0x002dd8, 0x002dde, 1}, {0x002ddf, 0x002ddf, -1}, + {0x002de0, 0x002dff, 0}, {0x002e00, 0x002e5d, 1}, {0x002e5e, 0x002e7f, -1}, + {0x002e80, 0x002e99, 2}, {0x002e9a, 0x002e9a, -1}, {0x002e9b, 0x002ef3, 2}, + {0x002ef4, 0x002eff, -1}, {0x002f00, 0x002fd5, 2}, {0x002fd6, 0x002fef, -1}, + {0x002ff0, 0x003029, 2}, {0x00302a, 0x00302d, 0}, {0x00302e, 0x00303e, 2}, + {0x00303f, 0x00303f, 1}, {0x003040, 0x003040, -1}, {0x003041, 0x003096, 2}, + {0x003097, 0x003098, -1}, {0x003099, 0x00309a, 0}, {0x00309b, 0x0030ff, 2}, + {0x003100, 0x003104, -1}, {0x003105, 0x00312f, 2}, {0x003130, 0x003130, -1}, + {0x003131, 0x003163, 2}, {0x003164, 0x003164, 0}, {0x003165, 0x00318e, 2}, + {0x00318f, 0x00318f, -1}, {0x003190, 0x0031e5, 2}, {0x0031e6, 0x0031ee, -1}, + {0x0031ef, 0x00321e, 2}, {0x00321f, 0x00321f, -1}, {0x003220, 0x00a48c, 2}, + {0x00a48d, 0x00a48f, -1}, {0x00a490, 0x00a4c6, 2}, {0x00a4c7, 0x00a4cf, -1}, + {0x00a4d0, 0x00a62b, 1}, {0x00a62c, 0x00a63f, -1}, {0x00a640, 0x00a66e, 1}, + {0x00a66f, 0x00a672, 0}, {0x00a673, 0x00a673, 1}, {0x00a674, 0x00a67d, 0}, + {0x00a67e, 0x00a69d, 1}, {0x00a69e, 0x00a69f, 0}, {0x00a6a0, 0x00a6ef, 1}, + {0x00a6f0, 0x00a6f1, 0}, {0x00a6f2, 0x00a6f7, 1}, {0x00a6f8, 0x00a6ff, -1}, + {0x00a700, 0x00a7cd, 1}, {0x00a7ce, 0x00a7cf, -1}, {0x00a7d0, 0x00a7d1, 1}, + {0x00a7d2, 0x00a7d2, -1}, {0x00a7d3, 0x00a7d3, 1}, {0x00a7d4, 0x00a7d4, -1}, + {0x00a7d5, 0x00a7dc, 1}, {0x00a7dd, 0x00a7f1, -1}, {0x00a7f2, 0x00a801, 1}, + {0x00a802, 0x00a802, 0}, {0x00a803, 0x00a805, 1}, {0x00a806, 0x00a806, 0}, + {0x00a807, 0x00a80a, 1}, {0x00a80b, 0x00a80b, 0}, {0x00a80c, 0x00a824, 1}, + {0x00a825, 0x00a826, 0}, {0x00a827, 0x00a82b, 1}, {0x00a82c, 0x00a82c, 0}, + {0x00a82d, 0x00a82f, -1}, {0x00a830, 0x00a839, 1}, {0x00a83a, 0x00a83f, -1}, + {0x00a840, 0x00a877, 1}, {0x00a878, 0x00a87f, -1}, {0x00a880, 0x00a8c3, 1}, + {0x00a8c4, 0x00a8c5, 0}, {0x00a8c6, 0x00a8cd, -1}, {0x00a8ce, 0x00a8d9, 1}, + {0x00a8da, 0x00a8df, -1}, {0x00a8e0, 0x00a8f1, 0}, {0x00a8f2, 0x00a8fe, 1}, + {0x00a8ff, 0x00a8ff, 0}, {0x00a900, 0x00a925, 1}, {0x00a926, 0x00a92d, 0}, + {0x00a92e, 0x00a946, 1}, {0x00a947, 0x00a951, 0}, {0x00a952, 0x00a953, 1}, + {0x00a954, 0x00a95e, -1}, {0x00a95f, 0x00a95f, 1}, {0x00a960, 0x00a97c, 2}, + {0x00a97d, 0x00a97f, -1}, {0x00a980, 0x00a982, 0}, {0x00a983, 0x00a9b2, 1}, + {0x00a9b3, 0x00a9b3, 0}, {0x00a9b4, 0x00a9b5, 1}, {0x00a9b6, 0x00a9b9, 0}, + {0x00a9ba, 0x00a9bb, 1}, {0x00a9bc, 0x00a9bd, 0}, {0x00a9be, 0x00a9cd, 1}, + {0x00a9ce, 0x00a9ce, -1}, {0x00a9cf, 0x00a9d9, 1}, {0x00a9da, 0x00a9dd, -1}, + {0x00a9de, 0x00a9e4, 1}, {0x00a9e5, 0x00a9e5, 0}, {0x00a9e6, 0x00a9fe, 1}, + {0x00a9ff, 0x00a9ff, -1}, {0x00aa00, 0x00aa28, 1}, {0x00aa29, 0x00aa2e, 0}, + {0x00aa2f, 0x00aa30, 1}, {0x00aa31, 0x00aa32, 0}, {0x00aa33, 0x00aa34, 1}, + {0x00aa35, 0x00aa36, 0}, {0x00aa37, 0x00aa3f, -1}, {0x00aa40, 0x00aa42, 1}, + {0x00aa43, 0x00aa43, 0}, {0x00aa44, 0x00aa4b, 1}, {0x00aa4c, 0x00aa4c, 0}, + {0x00aa4d, 0x00aa4d, 1}, {0x00aa4e, 0x00aa4f, -1}, {0x00aa50, 0x00aa59, 1}, + {0x00aa5a, 0x00aa5b, -1}, {0x00aa5c, 0x00aa7b, 1}, {0x00aa7c, 0x00aa7c, 0}, + {0x00aa7d, 0x00aaaf, 1}, {0x00aab0, 0x00aab0, 0}, {0x00aab1, 0x00aab1, 1}, + {0x00aab2, 0x00aab4, 0}, {0x00aab5, 0x00aab6, 1}, {0x00aab7, 0x00aab8, 0}, + {0x00aab9, 0x00aabd, 1}, {0x00aabe, 0x00aabf, 0}, {0x00aac0, 0x00aac0, 1}, + {0x00aac1, 0x00aac1, 0}, {0x00aac2, 0x00aac2, 1}, {0x00aac3, 0x00aada, -1}, + {0x00aadb, 0x00aaeb, 1}, {0x00aaec, 0x00aaed, 0}, {0x00aaee, 0x00aaf5, 1}, + {0x00aaf6, 0x00aaf6, 0}, {0x00aaf7, 0x00ab00, -1}, {0x00ab01, 0x00ab06, 1}, + {0x00ab07, 0x00ab08, -1}, {0x00ab09, 0x00ab0e, 1}, {0x00ab0f, 0x00ab10, -1}, + {0x00ab11, 0x00ab16, 1}, {0x00ab17, 0x00ab1f, -1}, {0x00ab20, 0x00ab26, 1}, + {0x00ab27, 0x00ab27, -1}, {0x00ab28, 0x00ab2e, 1}, {0x00ab2f, 0x00ab2f, -1}, + {0x00ab30, 0x00ab6b, 1}, {0x00ab6c, 0x00ab6f, -1}, {0x00ab70, 0x00abe4, 1}, + {0x00abe5, 0x00abe5, 0}, {0x00abe6, 0x00abe7, 1}, {0x00abe8, 0x00abe8, 0}, + {0x00abe9, 0x00abec, 1}, {0x00abed, 0x00abed, 0}, {0x00abee, 0x00abef, -1}, + {0x00abf0, 0x00abf9, 1}, {0x00abfa, 0x00abff, -1}, {0x00ac00, 0x00d7a3, 2}, + {0x00d7a4, 0x00d7af, -1}, {0x00d7b0, 0x00d7c6, 0}, {0x00d7c7, 0x00d7ca, -1}, + {0x00d7cb, 0x00d7fb, 0}, {0x00d7fc, 0x00dfff, -1}, {0x00e000, 0x00f8ff, 1}, + {0x00f900, 0x00fa6d, 2}, {0x00fa6e, 0x00fa6f, -1}, {0x00fa70, 0x00fad9, 2}, + {0x00fada, 0x00faff, -1}, {0x00fb00, 0x00fb06, 1}, {0x00fb07, 0x00fb12, -1}, + {0x00fb13, 0x00fb17, 1}, {0x00fb18, 0x00fb1c, -1}, {0x00fb1d, 0x00fb1d, 1}, + {0x00fb1e, 0x00fb1e, 0}, {0x00fb1f, 0x00fb36, 1}, {0x00fb37, 0x00fb37, -1}, + {0x00fb38, 0x00fb3c, 1}, {0x00fb3d, 0x00fb3d, -1}, {0x00fb3e, 0x00fb3e, 1}, + {0x00fb3f, 0x00fb3f, -1}, {0x00fb40, 0x00fb41, 1}, {0x00fb42, 0x00fb42, -1}, + {0x00fb43, 0x00fb44, 1}, {0x00fb45, 0x00fb45, -1}, {0x00fb46, 0x00fbc2, 1}, + {0x00fbc3, 0x00fbd2, -1}, {0x00fbd3, 0x00fd8f, 1}, {0x00fd90, 0x00fd91, -1}, + {0x00fd92, 0x00fdc7, 1}, {0x00fdc8, 0x00fdce, -1}, {0x00fdcf, 0x00fdcf, 1}, + {0x00fdd0, 0x00fdef, -1}, {0x00fdf0, 0x00fdff, 1}, {0x00fe00, 0x00fe0f, 0}, + {0x00fe10, 0x00fe19, 2}, {0x00fe1a, 0x00fe1f, -1}, {0x00fe20, 0x00fe2f, 0}, + {0x00fe30, 0x00fe52, 2}, {0x00fe53, 0x00fe53, -1}, {0x00fe54, 0x00fe66, 2}, + {0x00fe67, 0x00fe67, -1}, {0x00fe68, 0x00fe6b, 2}, {0x00fe6c, 0x00fe6f, -1}, + {0x00fe70, 0x00fe74, 1}, {0x00fe75, 0x00fe75, -1}, {0x00fe76, 0x00fefc, 1}, + {0x00fefd, 0x00fefe, -1}, {0x00feff, 0x00feff, 0}, {0x00ff00, 0x00ff00, -1}, + {0x00ff01, 0x00ff60, 2}, {0x00ff61, 0x00ff9f, 1}, {0x00ffa0, 0x00ffa0, 0}, + {0x00ffa1, 0x00ffbe, 1}, {0x00ffbf, 0x00ffc1, -1}, {0x00ffc2, 0x00ffc7, 1}, + {0x00ffc8, 0x00ffc9, -1}, {0x00ffca, 0x00ffcf, 1}, {0x00ffd0, 0x00ffd1, -1}, + {0x00ffd2, 0x00ffd7, 1}, {0x00ffd8, 0x00ffd9, -1}, {0x00ffda, 0x00ffdc, 1}, + {0x00ffdd, 0x00ffdf, -1}, {0x00ffe0, 0x00ffe6, 2}, {0x00ffe7, 0x00ffe7, -1}, + {0x00ffe8, 0x00ffee, 1}, {0x00ffef, 0x00fff8, -1}, {0x00fff9, 0x00fffd, 1}, + {0x00fffe, 0x00ffff, -1}, {0x010000, 0x01000b, 1}, {0x01000c, 0x01000c, -1}, + {0x01000d, 0x010026, 1}, {0x010027, 0x010027, -1}, {0x010028, 0x01003a, 1}, + {0x01003b, 0x01003b, -1}, {0x01003c, 0x01003d, 1}, {0x01003e, 0x01003e, -1}, + {0x01003f, 0x01004d, 1}, {0x01004e, 0x01004f, -1}, {0x010050, 0x01005d, 1}, + {0x01005e, 0x01007f, -1}, {0x010080, 0x0100fa, 1}, {0x0100fb, 0x0100ff, -1}, + {0x010100, 0x010102, 1}, {0x010103, 0x010106, -1}, {0x010107, 0x010133, 1}, + {0x010134, 0x010136, -1}, {0x010137, 0x01018e, 1}, {0x01018f, 0x01018f, -1}, + {0x010190, 0x01019c, 1}, {0x01019d, 0x01019f, -1}, {0x0101a0, 0x0101a0, 1}, + {0x0101a1, 0x0101cf, -1}, {0x0101d0, 0x0101fc, 1}, {0x0101fd, 0x0101fd, 0}, + {0x0101fe, 0x01027f, -1}, {0x010280, 0x01029c, 1}, {0x01029d, 0x01029f, -1}, + {0x0102a0, 0x0102d0, 1}, {0x0102d1, 0x0102df, -1}, {0x0102e0, 0x0102e0, 0}, + {0x0102e1, 0x0102fb, 1}, {0x0102fc, 0x0102ff, -1}, {0x010300, 0x010323, 1}, + {0x010324, 0x01032c, -1}, {0x01032d, 0x01034a, 1}, {0x01034b, 0x01034f, -1}, + {0x010350, 0x010375, 1}, {0x010376, 0x01037a, 0}, {0x01037b, 0x01037f, -1}, + {0x010380, 0x01039d, 1}, {0x01039e, 0x01039e, -1}, {0x01039f, 0x0103c3, 1}, + {0x0103c4, 0x0103c7, -1}, {0x0103c8, 0x0103d5, 1}, {0x0103d6, 0x0103ff, -1}, + {0x010400, 0x01049d, 1}, {0x01049e, 0x01049f, -1}, {0x0104a0, 0x0104a9, 1}, + {0x0104aa, 0x0104af, -1}, {0x0104b0, 0x0104d3, 1}, {0x0104d4, 0x0104d7, -1}, + {0x0104d8, 0x0104fb, 1}, {0x0104fc, 0x0104ff, -1}, {0x010500, 0x010527, 1}, + {0x010528, 0x01052f, -1}, {0x010530, 0x010563, 1}, {0x010564, 0x01056e, -1}, + {0x01056f, 0x01057a, 1}, {0x01057b, 0x01057b, -1}, {0x01057c, 0x01058a, 1}, + {0x01058b, 0x01058b, -1}, {0x01058c, 0x010592, 1}, {0x010593, 0x010593, -1}, + {0x010594, 0x010595, 1}, {0x010596, 0x010596, -1}, {0x010597, 0x0105a1, 1}, + {0x0105a2, 0x0105a2, -1}, {0x0105a3, 0x0105b1, 1}, {0x0105b2, 0x0105b2, -1}, + {0x0105b3, 0x0105b9, 1}, {0x0105ba, 0x0105ba, -1}, {0x0105bb, 0x0105bc, 1}, + {0x0105bd, 0x0105bf, -1}, {0x0105c0, 0x0105f3, 1}, {0x0105f4, 0x0105ff, -1}, + {0x010600, 0x010736, 1}, {0x010737, 0x01073f, -1}, {0x010740, 0x010755, 1}, + {0x010756, 0x01075f, -1}, {0x010760, 0x010767, 1}, {0x010768, 0x01077f, -1}, + {0x010780, 0x010785, 1}, {0x010786, 0x010786, -1}, {0x010787, 0x0107b0, 1}, + {0x0107b1, 0x0107b1, -1}, {0x0107b2, 0x0107ba, 1}, {0x0107bb, 0x0107ff, -1}, + {0x010800, 0x010805, 1}, {0x010806, 0x010807, -1}, {0x010808, 0x010808, 1}, + {0x010809, 0x010809, -1}, {0x01080a, 0x010835, 1}, {0x010836, 0x010836, -1}, + {0x010837, 0x010838, 1}, {0x010839, 0x01083b, -1}, {0x01083c, 0x01083c, 1}, + {0x01083d, 0x01083e, -1}, {0x01083f, 0x010855, 1}, {0x010856, 0x010856, -1}, + {0x010857, 0x01089e, 1}, {0x01089f, 0x0108a6, -1}, {0x0108a7, 0x0108af, 1}, + {0x0108b0, 0x0108df, -1}, {0x0108e0, 0x0108f2, 1}, {0x0108f3, 0x0108f3, -1}, + {0x0108f4, 0x0108f5, 1}, {0x0108f6, 0x0108fa, -1}, {0x0108fb, 0x01091b, 1}, + {0x01091c, 0x01091e, -1}, {0x01091f, 0x010939, 1}, {0x01093a, 0x01093e, -1}, + {0x01093f, 0x01093f, 1}, {0x010940, 0x01097f, -1}, {0x010980, 0x0109b7, 1}, + {0x0109b8, 0x0109bb, -1}, {0x0109bc, 0x0109cf, 1}, {0x0109d0, 0x0109d1, -1}, + {0x0109d2, 0x010a00, 1}, {0x010a01, 0x010a03, 0}, {0x010a04, 0x010a04, -1}, + {0x010a05, 0x010a06, 0}, {0x010a07, 0x010a0b, -1}, {0x010a0c, 0x010a0f, 0}, + {0x010a10, 0x010a13, 1}, {0x010a14, 0x010a14, -1}, {0x010a15, 0x010a17, 1}, + {0x010a18, 0x010a18, -1}, {0x010a19, 0x010a35, 1}, {0x010a36, 0x010a37, -1}, + {0x010a38, 0x010a3a, 0}, {0x010a3b, 0x010a3e, -1}, {0x010a3f, 0x010a3f, 0}, + {0x010a40, 0x010a48, 1}, {0x010a49, 0x010a4f, -1}, {0x010a50, 0x010a58, 1}, + {0x010a59, 0x010a5f, -1}, {0x010a60, 0x010a9f, 1}, {0x010aa0, 0x010abf, -1}, + {0x010ac0, 0x010ae4, 1}, {0x010ae5, 0x010ae6, 0}, {0x010ae7, 0x010aea, -1}, + {0x010aeb, 0x010af6, 1}, {0x010af7, 0x010aff, -1}, {0x010b00, 0x010b35, 1}, + {0x010b36, 0x010b38, -1}, {0x010b39, 0x010b55, 1}, {0x010b56, 0x010b57, -1}, + {0x010b58, 0x010b72, 1}, {0x010b73, 0x010b77, -1}, {0x010b78, 0x010b91, 1}, + {0x010b92, 0x010b98, -1}, {0x010b99, 0x010b9c, 1}, {0x010b9d, 0x010ba8, -1}, + {0x010ba9, 0x010baf, 1}, {0x010bb0, 0x010bff, -1}, {0x010c00, 0x010c48, 1}, + {0x010c49, 0x010c7f, -1}, {0x010c80, 0x010cb2, 1}, {0x010cb3, 0x010cbf, -1}, + {0x010cc0, 0x010cf2, 1}, {0x010cf3, 0x010cf9, -1}, {0x010cfa, 0x010d23, 1}, + {0x010d24, 0x010d27, 0}, {0x010d28, 0x010d2f, -1}, {0x010d30, 0x010d39, 1}, + {0x010d3a, 0x010d3f, -1}, {0x010d40, 0x010d65, 1}, {0x010d66, 0x010d68, -1}, + {0x010d69, 0x010d6d, 0}, {0x010d6e, 0x010d85, 1}, {0x010d86, 0x010d8d, -1}, + {0x010d8e, 0x010d8f, 1}, {0x010d90, 0x010e5f, -1}, {0x010e60, 0x010e7e, 1}, + {0x010e7f, 0x010e7f, -1}, {0x010e80, 0x010ea9, 1}, {0x010eaa, 0x010eaa, -1}, + {0x010eab, 0x010eac, 0}, {0x010ead, 0x010ead, 1}, {0x010eae, 0x010eaf, -1}, + {0x010eb0, 0x010eb1, 1}, {0x010eb2, 0x010ec1, -1}, {0x010ec2, 0x010ec4, 1}, + {0x010ec5, 0x010efb, -1}, {0x010efc, 0x010eff, 0}, {0x010f00, 0x010f27, 1}, + {0x010f28, 0x010f2f, -1}, {0x010f30, 0x010f45, 1}, {0x010f46, 0x010f50, 0}, + {0x010f51, 0x010f59, 1}, {0x010f5a, 0x010f6f, -1}, {0x010f70, 0x010f81, 1}, + {0x010f82, 0x010f85, 0}, {0x010f86, 0x010f89, 1}, {0x010f8a, 0x010faf, -1}, + {0x010fb0, 0x010fcb, 1}, {0x010fcc, 0x010fdf, -1}, {0x010fe0, 0x010ff6, 1}, + {0x010ff7, 0x010fff, -1}, {0x011000, 0x011000, 1}, {0x011001, 0x011001, 0}, + {0x011002, 0x011037, 1}, {0x011038, 0x011046, 0}, {0x011047, 0x01104d, 1}, + {0x01104e, 0x011051, -1}, {0x011052, 0x01106f, 1}, {0x011070, 0x011070, 0}, + {0x011071, 0x011072, 1}, {0x011073, 0x011074, 0}, {0x011075, 0x011075, 1}, + {0x011076, 0x01107e, -1}, {0x01107f, 0x011081, 0}, {0x011082, 0x0110b2, 1}, + {0x0110b3, 0x0110b6, 0}, {0x0110b7, 0x0110b8, 1}, {0x0110b9, 0x0110ba, 0}, + {0x0110bb, 0x0110c1, 1}, {0x0110c2, 0x0110c2, 0}, {0x0110c3, 0x0110cc, -1}, + {0x0110cd, 0x0110cd, 1}, {0x0110ce, 0x0110cf, -1}, {0x0110d0, 0x0110e8, 1}, + {0x0110e9, 0x0110ef, -1}, {0x0110f0, 0x0110f9, 1}, {0x0110fa, 0x0110ff, -1}, + {0x011100, 0x011102, 0}, {0x011103, 0x011126, 1}, {0x011127, 0x01112b, 0}, + {0x01112c, 0x01112c, 1}, {0x01112d, 0x011134, 0}, {0x011135, 0x011135, -1}, + {0x011136, 0x011147, 1}, {0x011148, 0x01114f, -1}, {0x011150, 0x011172, 1}, + {0x011173, 0x011173, 0}, {0x011174, 0x011176, 1}, {0x011177, 0x01117f, -1}, + {0x011180, 0x011181, 0}, {0x011182, 0x0111b5, 1}, {0x0111b6, 0x0111be, 0}, + {0x0111bf, 0x0111c8, 1}, {0x0111c9, 0x0111cc, 0}, {0x0111cd, 0x0111ce, 1}, + {0x0111cf, 0x0111cf, 0}, {0x0111d0, 0x0111df, 1}, {0x0111e0, 0x0111e0, -1}, + {0x0111e1, 0x0111f4, 1}, {0x0111f5, 0x0111ff, -1}, {0x011200, 0x011211, 1}, + {0x011212, 0x011212, -1}, {0x011213, 0x01122e, 1}, {0x01122f, 0x011231, 0}, + {0x011232, 0x011233, 1}, {0x011234, 0x011234, 0}, {0x011235, 0x011235, 1}, + {0x011236, 0x011237, 0}, {0x011238, 0x01123d, 1}, {0x01123e, 0x01123e, 0}, + {0x01123f, 0x011240, 1}, {0x011241, 0x011241, 0}, {0x011242, 0x01127f, -1}, + {0x011280, 0x011286, 1}, {0x011287, 0x011287, -1}, {0x011288, 0x011288, 1}, + {0x011289, 0x011289, -1}, {0x01128a, 0x01128d, 1}, {0x01128e, 0x01128e, -1}, + {0x01128f, 0x01129d, 1}, {0x01129e, 0x01129e, -1}, {0x01129f, 0x0112a9, 1}, + {0x0112aa, 0x0112af, -1}, {0x0112b0, 0x0112de, 1}, {0x0112df, 0x0112df, 0}, + {0x0112e0, 0x0112e2, 1}, {0x0112e3, 0x0112ea, 0}, {0x0112eb, 0x0112ef, -1}, + {0x0112f0, 0x0112f9, 1}, {0x0112fa, 0x0112ff, -1}, {0x011300, 0x011301, 0}, + {0x011302, 0x011303, 1}, {0x011304, 0x011304, -1}, {0x011305, 0x01130c, 1}, + {0x01130d, 0x01130e, -1}, {0x01130f, 0x011310, 1}, {0x011311, 0x011312, -1}, + {0x011313, 0x011328, 1}, {0x011329, 0x011329, -1}, {0x01132a, 0x011330, 1}, + {0x011331, 0x011331, -1}, {0x011332, 0x011333, 1}, {0x011334, 0x011334, -1}, + {0x011335, 0x011339, 1}, {0x01133a, 0x01133a, -1}, {0x01133b, 0x01133c, 0}, + {0x01133d, 0x01133f, 1}, {0x011340, 0x011340, 0}, {0x011341, 0x011344, 1}, + {0x011345, 0x011346, -1}, {0x011347, 0x011348, 1}, {0x011349, 0x01134a, -1}, + {0x01134b, 0x01134d, 1}, {0x01134e, 0x01134f, -1}, {0x011350, 0x011350, 1}, + {0x011351, 0x011356, -1}, {0x011357, 0x011357, 1}, {0x011358, 0x01135c, -1}, + {0x01135d, 0x011363, 1}, {0x011364, 0x011365, -1}, {0x011366, 0x01136c, 0}, + {0x01136d, 0x01136f, -1}, {0x011370, 0x011374, 0}, {0x011375, 0x01137f, -1}, + {0x011380, 0x011389, 1}, {0x01138a, 0x01138a, -1}, {0x01138b, 0x01138b, 1}, + {0x01138c, 0x01138d, -1}, {0x01138e, 0x01138e, 1}, {0x01138f, 0x01138f, -1}, + {0x011390, 0x0113b5, 1}, {0x0113b6, 0x0113b6, -1}, {0x0113b7, 0x0113ba, 1}, + {0x0113bb, 0x0113c0, 0}, {0x0113c1, 0x0113c1, -1}, {0x0113c2, 0x0113c2, 1}, + {0x0113c3, 0x0113c4, -1}, {0x0113c5, 0x0113c5, 1}, {0x0113c6, 0x0113c6, -1}, + {0x0113c7, 0x0113ca, 1}, {0x0113cb, 0x0113cb, -1}, {0x0113cc, 0x0113cd, 1}, + {0x0113ce, 0x0113ce, 0}, {0x0113cf, 0x0113cf, 1}, {0x0113d0, 0x0113d0, 0}, + {0x0113d1, 0x0113d1, 1}, {0x0113d2, 0x0113d2, 0}, {0x0113d3, 0x0113d5, 1}, + {0x0113d6, 0x0113d6, -1}, {0x0113d7, 0x0113d8, 1}, {0x0113d9, 0x0113e0, -1}, + {0x0113e1, 0x0113e2, 0}, {0x0113e3, 0x0113ff, -1}, {0x011400, 0x011437, 1}, + {0x011438, 0x01143f, 0}, {0x011440, 0x011441, 1}, {0x011442, 0x011444, 0}, + {0x011445, 0x011445, 1}, {0x011446, 0x011446, 0}, {0x011447, 0x01145b, 1}, + {0x01145c, 0x01145c, -1}, {0x01145d, 0x01145d, 1}, {0x01145e, 0x01145e, 0}, + {0x01145f, 0x011461, 1}, {0x011462, 0x01147f, -1}, {0x011480, 0x0114b2, 1}, + {0x0114b3, 0x0114b8, 0}, {0x0114b9, 0x0114b9, 1}, {0x0114ba, 0x0114ba, 0}, + {0x0114bb, 0x0114be, 1}, {0x0114bf, 0x0114c0, 0}, {0x0114c1, 0x0114c1, 1}, + {0x0114c2, 0x0114c3, 0}, {0x0114c4, 0x0114c7, 1}, {0x0114c8, 0x0114cf, -1}, + {0x0114d0, 0x0114d9, 1}, {0x0114da, 0x01157f, -1}, {0x011580, 0x0115b1, 1}, + {0x0115b2, 0x0115b5, 0}, {0x0115b6, 0x0115b7, -1}, {0x0115b8, 0x0115bb, 1}, + {0x0115bc, 0x0115bd, 0}, {0x0115be, 0x0115be, 1}, {0x0115bf, 0x0115c0, 0}, + {0x0115c1, 0x0115db, 1}, {0x0115dc, 0x0115dd, 0}, {0x0115de, 0x0115ff, -1}, + {0x011600, 0x011632, 1}, {0x011633, 0x01163a, 0}, {0x01163b, 0x01163c, 1}, + {0x01163d, 0x01163d, 0}, {0x01163e, 0x01163e, 1}, {0x01163f, 0x011640, 0}, + {0x011641, 0x011644, 1}, {0x011645, 0x01164f, -1}, {0x011650, 0x011659, 1}, + {0x01165a, 0x01165f, -1}, {0x011660, 0x01166c, 1}, {0x01166d, 0x01167f, -1}, + {0x011680, 0x0116aa, 1}, {0x0116ab, 0x0116ab, 0}, {0x0116ac, 0x0116ac, 1}, + {0x0116ad, 0x0116ad, 0}, {0x0116ae, 0x0116af, 1}, {0x0116b0, 0x0116b5, 0}, + {0x0116b6, 0x0116b6, 1}, {0x0116b7, 0x0116b7, 0}, {0x0116b8, 0x0116b9, 1}, + {0x0116ba, 0x0116bf, -1}, {0x0116c0, 0x0116c9, 1}, {0x0116ca, 0x0116cf, -1}, + {0x0116d0, 0x0116e3, 1}, {0x0116e4, 0x0116ff, -1}, {0x011700, 0x01171a, 1}, + {0x01171b, 0x01171c, -1}, {0x01171d, 0x01171d, 0}, {0x01171e, 0x01171e, 1}, + {0x01171f, 0x01171f, 0}, {0x011720, 0x011721, 1}, {0x011722, 0x011725, 0}, + {0x011726, 0x011726, 1}, {0x011727, 0x01172b, 0}, {0x01172c, 0x01172f, -1}, + {0x011730, 0x011746, 1}, {0x011747, 0x0117ff, -1}, {0x011800, 0x01182e, 1}, + {0x01182f, 0x011837, 0}, {0x011838, 0x011838, 1}, {0x011839, 0x01183a, 0}, + {0x01183b, 0x01183b, 1}, {0x01183c, 0x01189f, -1}, {0x0118a0, 0x0118f2, 1}, + {0x0118f3, 0x0118fe, -1}, {0x0118ff, 0x011906, 1}, {0x011907, 0x011908, -1}, + {0x011909, 0x011909, 1}, {0x01190a, 0x01190b, -1}, {0x01190c, 0x011913, 1}, + {0x011914, 0x011914, -1}, {0x011915, 0x011916, 1}, {0x011917, 0x011917, -1}, + {0x011918, 0x011935, 1}, {0x011936, 0x011936, -1}, {0x011937, 0x011938, 1}, + {0x011939, 0x01193a, -1}, {0x01193b, 0x01193c, 0}, {0x01193d, 0x01193d, 1}, + {0x01193e, 0x01193e, 0}, {0x01193f, 0x011942, 1}, {0x011943, 0x011943, 0}, + {0x011944, 0x011946, 1}, {0x011947, 0x01194f, -1}, {0x011950, 0x011959, 1}, + {0x01195a, 0x01199f, -1}, {0x0119a0, 0x0119a7, 1}, {0x0119a8, 0x0119a9, -1}, + {0x0119aa, 0x0119d3, 1}, {0x0119d4, 0x0119d7, 0}, {0x0119d8, 0x0119d9, -1}, + {0x0119da, 0x0119db, 0}, {0x0119dc, 0x0119df, 1}, {0x0119e0, 0x0119e0, 0}, + {0x0119e1, 0x0119e4, 1}, {0x0119e5, 0x0119ff, -1}, {0x011a00, 0x011a00, 1}, + {0x011a01, 0x011a0a, 0}, {0x011a0b, 0x011a32, 1}, {0x011a33, 0x011a38, 0}, + {0x011a39, 0x011a3a, 1}, {0x011a3b, 0x011a3e, 0}, {0x011a3f, 0x011a46, 1}, + {0x011a47, 0x011a47, 0}, {0x011a48, 0x011a4f, -1}, {0x011a50, 0x011a50, 1}, + {0x011a51, 0x011a56, 0}, {0x011a57, 0x011a58, 1}, {0x011a59, 0x011a5b, 0}, + {0x011a5c, 0x011a89, 1}, {0x011a8a, 0x011a96, 0}, {0x011a97, 0x011a97, 1}, + {0x011a98, 0x011a99, 0}, {0x011a9a, 0x011aa2, 1}, {0x011aa3, 0x011aaf, -1}, + {0x011ab0, 0x011af8, 1}, {0x011af9, 0x011aff, -1}, {0x011b00, 0x011b09, 1}, + {0x011b0a, 0x011bbf, -1}, {0x011bc0, 0x011be1, 1}, {0x011be2, 0x011bef, -1}, + {0x011bf0, 0x011bf9, 1}, {0x011bfa, 0x011bff, -1}, {0x011c00, 0x011c08, 1}, + {0x011c09, 0x011c09, -1}, {0x011c0a, 0x011c2f, 1}, {0x011c30, 0x011c36, 0}, + {0x011c37, 0x011c37, -1}, {0x011c38, 0x011c3d, 0}, {0x011c3e, 0x011c3e, 1}, + {0x011c3f, 0x011c3f, 0}, {0x011c40, 0x011c45, 1}, {0x011c46, 0x011c4f, -1}, + {0x011c50, 0x011c6c, 1}, {0x011c6d, 0x011c6f, -1}, {0x011c70, 0x011c8f, 1}, + {0x011c90, 0x011c91, -1}, {0x011c92, 0x011ca7, 0}, {0x011ca8, 0x011ca8, -1}, + {0x011ca9, 0x011ca9, 1}, {0x011caa, 0x011cb0, 0}, {0x011cb1, 0x011cb1, 1}, + {0x011cb2, 0x011cb3, 0}, {0x011cb4, 0x011cb4, 1}, {0x011cb5, 0x011cb6, 0}, + {0x011cb7, 0x011cff, -1}, {0x011d00, 0x011d06, 1}, {0x011d07, 0x011d07, -1}, + {0x011d08, 0x011d09, 1}, {0x011d0a, 0x011d0a, -1}, {0x011d0b, 0x011d30, 1}, + {0x011d31, 0x011d36, 0}, {0x011d37, 0x011d39, -1}, {0x011d3a, 0x011d3a, 0}, + {0x011d3b, 0x011d3b, -1}, {0x011d3c, 0x011d3d, 0}, {0x011d3e, 0x011d3e, -1}, + {0x011d3f, 0x011d45, 0}, {0x011d46, 0x011d46, 1}, {0x011d47, 0x011d47, 0}, + {0x011d48, 0x011d4f, -1}, {0x011d50, 0x011d59, 1}, {0x011d5a, 0x011d5f, -1}, + {0x011d60, 0x011d65, 1}, {0x011d66, 0x011d66, -1}, {0x011d67, 0x011d68, 1}, + {0x011d69, 0x011d69, -1}, {0x011d6a, 0x011d8e, 1}, {0x011d8f, 0x011d8f, -1}, + {0x011d90, 0x011d91, 0}, {0x011d92, 0x011d92, -1}, {0x011d93, 0x011d94, 1}, + {0x011d95, 0x011d95, 0}, {0x011d96, 0x011d96, 1}, {0x011d97, 0x011d97, 0}, + {0x011d98, 0x011d98, 1}, {0x011d99, 0x011d9f, -1}, {0x011da0, 0x011da9, 1}, + {0x011daa, 0x011edf, -1}, {0x011ee0, 0x011ef2, 1}, {0x011ef3, 0x011ef4, 0}, + {0x011ef5, 0x011ef8, 1}, {0x011ef9, 0x011eff, -1}, {0x011f00, 0x011f01, 0}, + {0x011f02, 0x011f10, 1}, {0x011f11, 0x011f11, -1}, {0x011f12, 0x011f35, 1}, + {0x011f36, 0x011f3a, 0}, {0x011f3b, 0x011f3d, -1}, {0x011f3e, 0x011f3f, 1}, + {0x011f40, 0x011f40, 0}, {0x011f41, 0x011f41, 1}, {0x011f42, 0x011f42, 0}, + {0x011f43, 0x011f59, 1}, {0x011f5a, 0x011f5a, 0}, {0x011f5b, 0x011faf, -1}, + {0x011fb0, 0x011fb0, 1}, {0x011fb1, 0x011fbf, -1}, {0x011fc0, 0x011ff1, 1}, + {0x011ff2, 0x011ffe, -1}, {0x011fff, 0x012399, 1}, {0x01239a, 0x0123ff, -1}, + {0x012400, 0x01246e, 1}, {0x01246f, 0x01246f, -1}, {0x012470, 0x012474, 1}, + {0x012475, 0x01247f, -1}, {0x012480, 0x012543, 1}, {0x012544, 0x012f8f, -1}, + {0x012f90, 0x012ff2, 1}, {0x012ff3, 0x012fff, -1}, {0x013000, 0x01343f, 1}, + {0x013440, 0x013440, 0}, {0x013441, 0x013446, 1}, {0x013447, 0x013455, 0}, + {0x013456, 0x01345f, -1}, {0x013460, 0x0143fa, 1}, {0x0143fb, 0x0143ff, -1}, + {0x014400, 0x014646, 1}, {0x014647, 0x0160ff, -1}, {0x016100, 0x01611d, 1}, + {0x01611e, 0x016129, 0}, {0x01612a, 0x01612c, 1}, {0x01612d, 0x01612f, 0}, + {0x016130, 0x016139, 1}, {0x01613a, 0x0167ff, -1}, {0x016800, 0x016a38, 1}, + {0x016a39, 0x016a3f, -1}, {0x016a40, 0x016a5e, 1}, {0x016a5f, 0x016a5f, -1}, + {0x016a60, 0x016a69, 1}, {0x016a6a, 0x016a6d, -1}, {0x016a6e, 0x016abe, 1}, + {0x016abf, 0x016abf, -1}, {0x016ac0, 0x016ac9, 1}, {0x016aca, 0x016acf, -1}, + {0x016ad0, 0x016aed, 1}, {0x016aee, 0x016aef, -1}, {0x016af0, 0x016af4, 0}, + {0x016af5, 0x016af5, 1}, {0x016af6, 0x016aff, -1}, {0x016b00, 0x016b2f, 1}, + {0x016b30, 0x016b36, 0}, {0x016b37, 0x016b45, 1}, {0x016b46, 0x016b4f, -1}, + {0x016b50, 0x016b59, 1}, {0x016b5a, 0x016b5a, -1}, {0x016b5b, 0x016b61, 1}, + {0x016b62, 0x016b62, -1}, {0x016b63, 0x016b77, 1}, {0x016b78, 0x016b7c, -1}, + {0x016b7d, 0x016b8f, 1}, {0x016b90, 0x016d3f, -1}, {0x016d40, 0x016d79, 1}, + {0x016d7a, 0x016e3f, -1}, {0x016e40, 0x016e9a, 1}, {0x016e9b, 0x016eff, -1}, + {0x016f00, 0x016f4a, 1}, {0x016f4b, 0x016f4e, -1}, {0x016f4f, 0x016f4f, 0}, + {0x016f50, 0x016f87, 1}, {0x016f88, 0x016f8e, -1}, {0x016f8f, 0x016f92, 0}, + {0x016f93, 0x016f9f, 1}, {0x016fa0, 0x016fdf, -1}, {0x016fe0, 0x016fe3, 2}, + {0x016fe4, 0x016fe4, 0}, {0x016fe5, 0x016fef, -1}, {0x016ff0, 0x016ff1, 2}, + {0x016ff2, 0x016fff, -1}, {0x017000, 0x0187f7, 2}, {0x0187f8, 0x0187ff, -1}, + {0x018800, 0x018cd5, 2}, {0x018cd6, 0x018cfe, -1}, {0x018cff, 0x018d08, 2}, + {0x018d09, 0x01afef, -1}, {0x01aff0, 0x01aff3, 2}, {0x01aff4, 0x01aff4, -1}, + {0x01aff5, 0x01affb, 2}, {0x01affc, 0x01affc, -1}, {0x01affd, 0x01affe, 2}, + {0x01afff, 0x01afff, -1}, {0x01b000, 0x01b122, 2}, {0x01b123, 0x01b131, -1}, + {0x01b132, 0x01b132, 2}, {0x01b133, 0x01b14f, -1}, {0x01b150, 0x01b152, 2}, + {0x01b153, 0x01b154, -1}, {0x01b155, 0x01b155, 2}, {0x01b156, 0x01b163, -1}, + {0x01b164, 0x01b167, 2}, {0x01b168, 0x01b16f, -1}, {0x01b170, 0x01b2fb, 2}, + {0x01b2fc, 0x01bbff, -1}, {0x01bc00, 0x01bc6a, 1}, {0x01bc6b, 0x01bc6f, -1}, + {0x01bc70, 0x01bc7c, 1}, {0x01bc7d, 0x01bc7f, -1}, {0x01bc80, 0x01bc88, 1}, + {0x01bc89, 0x01bc8f, -1}, {0x01bc90, 0x01bc99, 1}, {0x01bc9a, 0x01bc9b, -1}, + {0x01bc9c, 0x01bc9c, 1}, {0x01bc9d, 0x01bc9e, 0}, {0x01bc9f, 0x01bc9f, 1}, + {0x01bca0, 0x01bca3, 0}, {0x01bca4, 0x01cbff, -1}, {0x01cc00, 0x01ccf9, 1}, + {0x01ccfa, 0x01ccff, -1}, {0x01cd00, 0x01ceb3, 1}, {0x01ceb4, 0x01ceff, -1}, + {0x01cf00, 0x01cf2d, 0}, {0x01cf2e, 0x01cf2f, -1}, {0x01cf30, 0x01cf46, 0}, + {0x01cf47, 0x01cf4f, -1}, {0x01cf50, 0x01cfc3, 1}, {0x01cfc4, 0x01cfff, -1}, + {0x01d000, 0x01d0f5, 1}, {0x01d0f6, 0x01d0ff, -1}, {0x01d100, 0x01d126, 1}, + {0x01d127, 0x01d128, -1}, {0x01d129, 0x01d166, 1}, {0x01d167, 0x01d169, 0}, + {0x01d16a, 0x01d172, 1}, {0x01d173, 0x01d182, 0}, {0x01d183, 0x01d184, 1}, + {0x01d185, 0x01d18b, 0}, {0x01d18c, 0x01d1a9, 1}, {0x01d1aa, 0x01d1ad, 0}, + {0x01d1ae, 0x01d1ea, 1}, {0x01d1eb, 0x01d1ff, -1}, {0x01d200, 0x01d241, 1}, + {0x01d242, 0x01d244, 0}, {0x01d245, 0x01d245, 1}, {0x01d246, 0x01d2bf, -1}, + {0x01d2c0, 0x01d2d3, 1}, {0x01d2d4, 0x01d2df, -1}, {0x01d2e0, 0x01d2f3, 1}, + {0x01d2f4, 0x01d2ff, -1}, {0x01d300, 0x01d356, 2}, {0x01d357, 0x01d35f, -1}, + {0x01d360, 0x01d376, 2}, {0x01d377, 0x01d378, 1}, {0x01d379, 0x01d3ff, -1}, + {0x01d400, 0x01d454, 1}, {0x01d455, 0x01d455, -1}, {0x01d456, 0x01d49c, 1}, + {0x01d49d, 0x01d49d, -1}, {0x01d49e, 0x01d49f, 1}, {0x01d4a0, 0x01d4a1, -1}, + {0x01d4a2, 0x01d4a2, 1}, {0x01d4a3, 0x01d4a4, -1}, {0x01d4a5, 0x01d4a6, 1}, + {0x01d4a7, 0x01d4a8, -1}, {0x01d4a9, 0x01d4ac, 1}, {0x01d4ad, 0x01d4ad, -1}, + {0x01d4ae, 0x01d4b9, 1}, {0x01d4ba, 0x01d4ba, -1}, {0x01d4bb, 0x01d4bb, 1}, + {0x01d4bc, 0x01d4bc, -1}, {0x01d4bd, 0x01d4c3, 1}, {0x01d4c4, 0x01d4c4, -1}, + {0x01d4c5, 0x01d505, 1}, {0x01d506, 0x01d506, -1}, {0x01d507, 0x01d50a, 1}, + {0x01d50b, 0x01d50c, -1}, {0x01d50d, 0x01d514, 1}, {0x01d515, 0x01d515, -1}, + {0x01d516, 0x01d51c, 1}, {0x01d51d, 0x01d51d, -1}, {0x01d51e, 0x01d539, 1}, + {0x01d53a, 0x01d53a, -1}, {0x01d53b, 0x01d53e, 1}, {0x01d53f, 0x01d53f, -1}, + {0x01d540, 0x01d544, 1}, {0x01d545, 0x01d545, -1}, {0x01d546, 0x01d546, 1}, + {0x01d547, 0x01d549, -1}, {0x01d54a, 0x01d550, 1}, {0x01d551, 0x01d551, -1}, + {0x01d552, 0x01d6a5, 1}, {0x01d6a6, 0x01d6a7, -1}, {0x01d6a8, 0x01d7cb, 1}, + {0x01d7cc, 0x01d7cd, -1}, {0x01d7ce, 0x01d9ff, 1}, {0x01da00, 0x01da36, 0}, + {0x01da37, 0x01da3a, 1}, {0x01da3b, 0x01da6c, 0}, {0x01da6d, 0x01da74, 1}, + {0x01da75, 0x01da75, 0}, {0x01da76, 0x01da83, 1}, {0x01da84, 0x01da84, 0}, + {0x01da85, 0x01da8b, 1}, {0x01da8c, 0x01da9a, -1}, {0x01da9b, 0x01da9f, 0}, + {0x01daa0, 0x01daa0, -1}, {0x01daa1, 0x01daaf, 0}, {0x01dab0, 0x01deff, -1}, + {0x01df00, 0x01df1e, 1}, {0x01df1f, 0x01df24, -1}, {0x01df25, 0x01df2a, 1}, + {0x01df2b, 0x01dfff, -1}, {0x01e000, 0x01e006, 0}, {0x01e007, 0x01e007, -1}, + {0x01e008, 0x01e018, 0}, {0x01e019, 0x01e01a, -1}, {0x01e01b, 0x01e021, 0}, + {0x01e022, 0x01e022, -1}, {0x01e023, 0x01e024, 0}, {0x01e025, 0x01e025, -1}, + {0x01e026, 0x01e02a, 0}, {0x01e02b, 0x01e02f, -1}, {0x01e030, 0x01e06d, 1}, + {0x01e06e, 0x01e08e, -1}, {0x01e08f, 0x01e08f, 0}, {0x01e090, 0x01e0ff, -1}, + {0x01e100, 0x01e12c, 1}, {0x01e12d, 0x01e12f, -1}, {0x01e130, 0x01e136, 0}, + {0x01e137, 0x01e13d, 1}, {0x01e13e, 0x01e13f, -1}, {0x01e140, 0x01e149, 1}, + {0x01e14a, 0x01e14d, -1}, {0x01e14e, 0x01e14f, 1}, {0x01e150, 0x01e28f, -1}, + {0x01e290, 0x01e2ad, 1}, {0x01e2ae, 0x01e2ae, 0}, {0x01e2af, 0x01e2bf, -1}, + {0x01e2c0, 0x01e2eb, 1}, {0x01e2ec, 0x01e2ef, 0}, {0x01e2f0, 0x01e2f9, 1}, + {0x01e2fa, 0x01e2fe, -1}, {0x01e2ff, 0x01e2ff, 1}, {0x01e300, 0x01e4cf, -1}, + {0x01e4d0, 0x01e4eb, 1}, {0x01e4ec, 0x01e4ef, 0}, {0x01e4f0, 0x01e4f9, 1}, + {0x01e4fa, 0x01e5cf, -1}, {0x01e5d0, 0x01e5ed, 1}, {0x01e5ee, 0x01e5ef, 0}, + {0x01e5f0, 0x01e5fa, 1}, {0x01e5fb, 0x01e5fe, -1}, {0x01e5ff, 0x01e5ff, 1}, + {0x01e600, 0x01e7df, -1}, {0x01e7e0, 0x01e7e6, 1}, {0x01e7e7, 0x01e7e7, -1}, + {0x01e7e8, 0x01e7eb, 1}, {0x01e7ec, 0x01e7ec, -1}, {0x01e7ed, 0x01e7ee, 1}, + {0x01e7ef, 0x01e7ef, -1}, {0x01e7f0, 0x01e7fe, 1}, {0x01e7ff, 0x01e7ff, -1}, + {0x01e800, 0x01e8c4, 1}, {0x01e8c5, 0x01e8c6, -1}, {0x01e8c7, 0x01e8cf, 1}, + {0x01e8d0, 0x01e8d6, 0}, {0x01e8d7, 0x01e8ff, -1}, {0x01e900, 0x01e943, 1}, + {0x01e944, 0x01e94a, 0}, {0x01e94b, 0x01e94b, 1}, {0x01e94c, 0x01e94f, -1}, + {0x01e950, 0x01e959, 1}, {0x01e95a, 0x01e95d, -1}, {0x01e95e, 0x01e95f, 1}, + {0x01e960, 0x01ec70, -1}, {0x01ec71, 0x01ecb4, 1}, {0x01ecb5, 0x01ed00, -1}, + {0x01ed01, 0x01ed3d, 1}, {0x01ed3e, 0x01edff, -1}, {0x01ee00, 0x01ee03, 1}, + {0x01ee04, 0x01ee04, -1}, {0x01ee05, 0x01ee1f, 1}, {0x01ee20, 0x01ee20, -1}, + {0x01ee21, 0x01ee22, 1}, {0x01ee23, 0x01ee23, -1}, {0x01ee24, 0x01ee24, 1}, + {0x01ee25, 0x01ee26, -1}, {0x01ee27, 0x01ee27, 1}, {0x01ee28, 0x01ee28, -1}, + {0x01ee29, 0x01ee32, 1}, {0x01ee33, 0x01ee33, -1}, {0x01ee34, 0x01ee37, 1}, + {0x01ee38, 0x01ee38, -1}, {0x01ee39, 0x01ee39, 1}, {0x01ee3a, 0x01ee3a, -1}, + {0x01ee3b, 0x01ee3b, 1}, {0x01ee3c, 0x01ee41, -1}, {0x01ee42, 0x01ee42, 1}, + {0x01ee43, 0x01ee46, -1}, {0x01ee47, 0x01ee47, 1}, {0x01ee48, 0x01ee48, -1}, + {0x01ee49, 0x01ee49, 1}, {0x01ee4a, 0x01ee4a, -1}, {0x01ee4b, 0x01ee4b, 1}, + {0x01ee4c, 0x01ee4c, -1}, {0x01ee4d, 0x01ee4f, 1}, {0x01ee50, 0x01ee50, -1}, + {0x01ee51, 0x01ee52, 1}, {0x01ee53, 0x01ee53, -1}, {0x01ee54, 0x01ee54, 1}, + {0x01ee55, 0x01ee56, -1}, {0x01ee57, 0x01ee57, 1}, {0x01ee58, 0x01ee58, -1}, + {0x01ee59, 0x01ee59, 1}, {0x01ee5a, 0x01ee5a, -1}, {0x01ee5b, 0x01ee5b, 1}, + {0x01ee5c, 0x01ee5c, -1}, {0x01ee5d, 0x01ee5d, 1}, {0x01ee5e, 0x01ee5e, -1}, + {0x01ee5f, 0x01ee5f, 1}, {0x01ee60, 0x01ee60, -1}, {0x01ee61, 0x01ee62, 1}, + {0x01ee63, 0x01ee63, -1}, {0x01ee64, 0x01ee64, 1}, {0x01ee65, 0x01ee66, -1}, + {0x01ee67, 0x01ee6a, 1}, {0x01ee6b, 0x01ee6b, -1}, {0x01ee6c, 0x01ee72, 1}, + {0x01ee73, 0x01ee73, -1}, {0x01ee74, 0x01ee77, 1}, {0x01ee78, 0x01ee78, -1}, + {0x01ee79, 0x01ee7c, 1}, {0x01ee7d, 0x01ee7d, -1}, {0x01ee7e, 0x01ee7e, 1}, + {0x01ee7f, 0x01ee7f, -1}, {0x01ee80, 0x01ee89, 1}, {0x01ee8a, 0x01ee8a, -1}, + {0x01ee8b, 0x01ee9b, 1}, {0x01ee9c, 0x01eea0, -1}, {0x01eea1, 0x01eea3, 1}, + {0x01eea4, 0x01eea4, -1}, {0x01eea5, 0x01eea9, 1}, {0x01eeaa, 0x01eeaa, -1}, + {0x01eeab, 0x01eebb, 1}, {0x01eebc, 0x01eeef, -1}, {0x01eef0, 0x01eef1, 1}, + {0x01eef2, 0x01efff, -1}, {0x01f000, 0x01f003, 1}, {0x01f004, 0x01f004, 2}, + {0x01f005, 0x01f02b, 1}, {0x01f02c, 0x01f02f, -1}, {0x01f030, 0x01f093, 1}, + {0x01f094, 0x01f09f, -1}, {0x01f0a0, 0x01f0ae, 1}, {0x01f0af, 0x01f0b0, -1}, + {0x01f0b1, 0x01f0bf, 1}, {0x01f0c0, 0x01f0c0, -1}, {0x01f0c1, 0x01f0ce, 1}, + {0x01f0cf, 0x01f0cf, 2}, {0x01f0d0, 0x01f0d0, -1}, {0x01f0d1, 0x01f0f5, 1}, + {0x01f0f6, 0x01f0ff, -1}, {0x01f100, 0x01f18d, 1}, {0x01f18e, 0x01f18e, 2}, + {0x01f18f, 0x01f190, 1}, {0x01f191, 0x01f19a, 2}, {0x01f19b, 0x01f1ad, 1}, + {0x01f1ae, 0x01f1e5, -1}, {0x01f1e6, 0x01f1ff, 1}, {0x01f200, 0x01f202, 2}, + {0x01f203, 0x01f20f, -1}, {0x01f210, 0x01f23b, 2}, {0x01f23c, 0x01f23f, -1}, + {0x01f240, 0x01f248, 2}, {0x01f249, 0x01f24f, -1}, {0x01f250, 0x01f251, 2}, + {0x01f252, 0x01f25f, -1}, {0x01f260, 0x01f265, 2}, {0x01f266, 0x01f2ff, -1}, + {0x01f300, 0x01f320, 2}, {0x01f321, 0x01f32c, 1}, {0x01f32d, 0x01f335, 2}, + {0x01f336, 0x01f336, 1}, {0x01f337, 0x01f37c, 2}, {0x01f37d, 0x01f37d, 1}, + {0x01f37e, 0x01f393, 2}, {0x01f394, 0x01f39f, 1}, {0x01f3a0, 0x01f3ca, 2}, + {0x01f3cb, 0x01f3ce, 1}, {0x01f3cf, 0x01f3d3, 2}, {0x01f3d4, 0x01f3df, 1}, + {0x01f3e0, 0x01f3f0, 2}, {0x01f3f1, 0x01f3f3, 1}, {0x01f3f4, 0x01f3f4, 2}, + {0x01f3f5, 0x01f3f7, 1}, {0x01f3f8, 0x01f43e, 2}, {0x01f43f, 0x01f43f, 1}, + {0x01f440, 0x01f440, 2}, {0x01f441, 0x01f441, 1}, {0x01f442, 0x01f4fc, 2}, + {0x01f4fd, 0x01f4fe, 1}, {0x01f4ff, 0x01f53d, 2}, {0x01f53e, 0x01f54a, 1}, + {0x01f54b, 0x01f54e, 2}, {0x01f54f, 0x01f54f, 1}, {0x01f550, 0x01f567, 2}, + {0x01f568, 0x01f579, 1}, {0x01f57a, 0x01f57a, 2}, {0x01f57b, 0x01f594, 1}, + {0x01f595, 0x01f596, 2}, {0x01f597, 0x01f5a3, 1}, {0x01f5a4, 0x01f5a4, 2}, + {0x01f5a5, 0x01f5fa, 1}, {0x01f5fb, 0x01f64f, 2}, {0x01f650, 0x01f67f, 1}, + {0x01f680, 0x01f6c5, 2}, {0x01f6c6, 0x01f6cb, 1}, {0x01f6cc, 0x01f6cc, 2}, + {0x01f6cd, 0x01f6cf, 1}, {0x01f6d0, 0x01f6d2, 2}, {0x01f6d3, 0x01f6d4, 1}, + {0x01f6d5, 0x01f6d7, 2}, {0x01f6d8, 0x01f6db, -1}, {0x01f6dc, 0x01f6df, 2}, + {0x01f6e0, 0x01f6ea, 1}, {0x01f6eb, 0x01f6ec, 2}, {0x01f6ed, 0x01f6ef, -1}, + {0x01f6f0, 0x01f6f3, 1}, {0x01f6f4, 0x01f6fc, 2}, {0x01f6fd, 0x01f6ff, -1}, + {0x01f700, 0x01f776, 1}, {0x01f777, 0x01f77a, -1}, {0x01f77b, 0x01f7d9, 1}, + {0x01f7da, 0x01f7df, -1}, {0x01f7e0, 0x01f7eb, 2}, {0x01f7ec, 0x01f7ef, -1}, + {0x01f7f0, 0x01f7f0, 2}, {0x01f7f1, 0x01f7ff, -1}, {0x01f800, 0x01f80b, 1}, + {0x01f80c, 0x01f80f, -1}, {0x01f810, 0x01f847, 1}, {0x01f848, 0x01f84f, -1}, + {0x01f850, 0x01f859, 1}, {0x01f85a, 0x01f85f, -1}, {0x01f860, 0x01f887, 1}, + {0x01f888, 0x01f88f, -1}, {0x01f890, 0x01f8ad, 1}, {0x01f8ae, 0x01f8af, -1}, + {0x01f8b0, 0x01f8bb, 1}, {0x01f8bc, 0x01f8bf, -1}, {0x01f8c0, 0x01f8c1, 1}, + {0x01f8c2, 0x01f8ff, -1}, {0x01f900, 0x01f90b, 1}, {0x01f90c, 0x01f93a, 2}, + {0x01f93b, 0x01f93b, 1}, {0x01f93c, 0x01f945, 2}, {0x01f946, 0x01f946, 1}, + {0x01f947, 0x01f9ff, 2}, {0x01fa00, 0x01fa53, 1}, {0x01fa54, 0x01fa5f, -1}, + {0x01fa60, 0x01fa6d, 1}, {0x01fa6e, 0x01fa6f, -1}, {0x01fa70, 0x01fa7c, 2}, + {0x01fa7d, 0x01fa7f, -1}, {0x01fa80, 0x01fa89, 2}, {0x01fa8a, 0x01fa8e, -1}, + {0x01fa8f, 0x01fac6, 2}, {0x01fac7, 0x01facd, -1}, {0x01face, 0x01fadc, 2}, + {0x01fadd, 0x01fade, -1}, {0x01fadf, 0x01fae9, 2}, {0x01faea, 0x01faef, -1}, + {0x01faf0, 0x01faf8, 2}, {0x01faf9, 0x01faff, -1}, {0x01fb00, 0x01fb92, 1}, + {0x01fb93, 0x01fb93, -1}, {0x01fb94, 0x01fbf9, 1}, {0x01fbfa, 0x01ffff, -1}, + {0x020000, 0x02a6df, 2}, {0x02a6e0, 0x02a6ff, -1}, {0x02a700, 0x02b739, 2}, + {0x02b73a, 0x02b73f, -1}, {0x02b740, 0x02b81d, 2}, {0x02b81e, 0x02b81f, -1}, + {0x02b820, 0x02cea1, 2}, {0x02cea2, 0x02ceaf, -1}, {0x02ceb0, 0x02ebe0, 2}, + {0x02ebe1, 0x02ebef, -1}, {0x02ebf0, 0x02ee5d, 2}, {0x02ee5e, 0x02f7ff, -1}, + {0x02f800, 0x02fa1d, 2}, {0x02fa1e, 0x02ffff, -1}, {0x030000, 0x03134a, 2}, + {0x03134b, 0x03134f, -1}, {0x031350, 0x0323af, 2}, {0x0323b0, 0x0e0000, -1}, + {0x0e0001, 0x0e0001, 0}, {0x0e0002, 0x0e001f, -1}, {0x0e0020, 0x0e007f, 0}, + {0x0e0080, 0x0e00ff, -1}, {0x0e0100, 0x0e01ef, 0}, {0x0e01f0, 0x0effff, -1}, + {0x0f0000, 0x0ffffd, 1}, {0x0ffffe, 0x0fffff, -1}, {0x100000, 0x10fffd, 1}, + {0x10fffe, 0x10ffff, -1}, + // clang-format on +}; +#define WCWIDTH_TABLE_LENGTH 2143 +#endif // ifndef TB_OPT_LIBC_WCHAR + static int tb_reset(void); static int tb_printf_inner(int x, int y, uintattr_t fg, uintattr_t bg, size_t *out_w, const char *fmt, va_list vl); @@ -1552,9 +2288,9 @@ static int load_terminfo_from_path(const char *path, const char *term); static int read_terminfo_path(const char *path); static int parse_terminfo_caps(void); static int load_builtin_caps(void); -static const char *get_terminfo_string(int16_t str_offsets_pos, - int16_t str_offsets_len, int16_t str_table_pos, int16_t str_table_len, - int16_t str_index); +static const char *get_terminfo_string(int16_t offsets_pos, int16_t offsets_len, + int16_t table_pos, int16_t table_size, int16_t index); +static int get_terminfo_int16(int offset, int16_t *val); static int wait_event(struct tb_event *event, int timeout); static int extract_event(struct tb_event *event); static int extract_esc(struct tb_event *event); @@ -1588,9 +2324,10 @@ static int bytebuf_shift(struct bytebuf_t *b, size_t n); static int bytebuf_flush(struct bytebuf_t *b, int fd); static int bytebuf_reserve(struct bytebuf_t *b, size_t sz); static int bytebuf_free(struct bytebuf_t *b); +static int tb_iswprint_ex(uint32_t ch, int *width); +static int tb_wcswidth(uint32_t *ch, size_t nch); int tb_init(void) { - setlocale(LC_CTYPE, "C.UTF-8"); // Required for iswprint(3) to work properly return tb_init_file("/dev/tty"); } @@ -1629,9 +2366,7 @@ int tb_init_rwfd(int rfd, int wfd) { global.initialized = 1; } while (0); - if (rv != TB_OK) { - tb_deinit(); - } + if (rv != TB_OK) tb_deinit(); return rv; } @@ -1685,13 +2420,12 @@ int tb_present(void) { { #ifdef TB_OPT_EGC if (back->nech > 0) - w = wcswidth((wchar_t *)back->ech, back->nech); + w = tb_wcswidth(back->ech, back->nech); else #endif - // wcwidth simply returns -1 on overflow of wchar_t - w = wcwidth((wchar_t)back->ch); + w = tb_wcwidth((wchar_t)back->ch); } - if (w < 1) w = 1; + if (w < 1) w = 1; // wcwidth qreturns -1 for invalid codepoints if (cell_cmp(back, front) != 0) { cell_copy(front, back); @@ -1798,6 +2532,7 @@ int tb_get_cell(int x, int y, int back, struct tb_cell *cell) { int tb_extend_cell(int x, int y, uint32_t ch) { if_not_init_return(); #ifdef TB_OPT_EGC + // TODO: iswprint ch? int rv; struct tb_cell *cell; size_t nech; @@ -1825,16 +2560,15 @@ int tb_extend_cell(int x, int y, uint32_t ch) { int tb_set_input_mode(int mode) { if_not_init_return(); - if (mode == TB_INPUT_CURRENT) { - return global.input_mode; - } - if ((mode & (TB_INPUT_ESC | TB_INPUT_ALT)) == 0) { + if (mode == TB_INPUT_CURRENT) return global.input_mode; + + int esc_or_alt = TB_INPUT_ESC | TB_INPUT_ALT; + if ((mode & esc_or_alt) == 0) { + // neither specified; flip on ESC mode |= TB_INPUT_ESC; - } - - if ((mode & (TB_INPUT_ESC | TB_INPUT_ALT)) == (TB_INPUT_ESC | TB_INPUT_ALT)) - { + } else if ((mode & esc_or_alt) == esc_or_alt) { + // both specified; flip off ALT mode &= ~TB_INPUT_ALT; } @@ -1925,11 +2659,11 @@ int tb_print_ex(int x, int y, uintattr_t fg, uintattr_t bg, size_t *out_w, x_prev = x; y += 1; continue; - } else if (!iswprint((wint_t)uni)) { + } else if (!tb_iswprint_ex(uni, &w)) { uni = 0xfffd; // replace non-printable with U+FFFD + w = 1; } - w = wcwidth((wchar_t)uni); if (w < 0) { return TB_ERR; // shouldn't happen if iswprint } else if (w == 0) { // combining character @@ -2156,9 +2890,7 @@ static int tb_reset(void) { } static int init_term_attrs(void) { - if (global.ttyfd < 0) { - return TB_OK; - } + if (global.ttyfd < 0) return TB_OK; if (tcgetattr(global.ttyfd, &global.orig_tios) != 0) { global.last_errno = errno; @@ -2309,9 +3041,7 @@ static int cap_trie_deinit(struct cap_trie_t *node) { for (j = 0; j < node->nchildren; j++) { cap_trie_deinit(&node->children[j]); } - if (node->children) { - tb_free(node->children); - } + if (node->children) tb_free(node->children); memset(node, 0, sizeof(*node)); return TB_OK; } @@ -2362,9 +3092,7 @@ static int send_clear(void) { static int update_term_size(void) { int rv, ioctl_errno; - if (global.ttyfd < 0) { - return TB_OK; - } + if (global.ttyfd < 0) return TB_OK; struct winsize sz; memset(&sz, 0, sizeof(sz)); @@ -2486,15 +3214,11 @@ static int load_terminfo(void) { // this behavior. Some of these paths are compile-time ncurses options, so // best guesses are used here. const char *term = getenv("TERM"); - if (!term) { - return TB_ERR; - } + if (!term) return TB_ERR; // If TERMINFO is set, try that directory and stop const char *terminfo = getenv("TERMINFO"); - if (terminfo) { - return load_terminfo_from_path(terminfo, term); - } + if (terminfo) return load_terminfo_from_path(terminfo, term); // Next try ~/.terminfo const char *home = getenv("HOME"); @@ -2557,9 +3281,7 @@ static int load_terminfo_from_path(const char *path, const char *term) { static int read_terminfo_path(const char *path) { FILE *fp = fopen(path, "rb"); - if (!fp) { - return TB_ERR; - } + if (!fp) return TB_ERR; struct stat st; if (fstat(fileno(fp), &st) != 0) { @@ -2592,43 +3314,49 @@ static int parse_terminfo_caps(void) { // description of this behavior. // Ensure there's at least a header's worth of data - if (global.nterminfo < 6) { - return TB_ERR; - } + if (global.nterminfo < 6 * (int)sizeof(int16_t)) return TB_ERR; - int16_t *header = (int16_t *)global.terminfo; + int16_t magic_number, nbytes_names, nbytes_bools, num_ints, num_offsets, + nbytes_strings; + size_t nbytes_header = 6 * sizeof(int16_t); // header[0] the magic number (octal 0432 or 01036) // header[1] the size, in bytes, of the names section // header[2] the number of bytes in the boolean section // header[3] the number of short integers in the numbers section // header[4] the number of offsets (short integers) in the strings section // header[5] the size, in bytes, of the string table + get_terminfo_int16(0 * sizeof(int16_t), &magic_number); + get_terminfo_int16(1 * sizeof(int16_t), &nbytes_names); + get_terminfo_int16(2 * sizeof(int16_t), &nbytes_bools); + get_terminfo_int16(3 * sizeof(int16_t), &num_ints); + get_terminfo_int16(4 * sizeof(int16_t), &num_offsets); + get_terminfo_int16(5 * sizeof(int16_t), &nbytes_strings); // Legacy ints are 16-bit, extended ints are 32-bit - const int bytes_per_int = header[0] == 01036 ? 4 // 32-bit - : 2; // 16-bit + const int bytes_per_int = magic_number == 01036 ? 4 // 32-bit + : 2; // 16-bit // > Between the boolean section and the number section, a null byte will be // > inserted, if necessary, to ensure that the number section begins on an // > even byte - const int align_offset = (header[1] + header[2]) % 2 != 0 ? 1 : 0; + const int align_offset = (nbytes_names + nbytes_bools) % 2 != 0 ? 1 : 0; const int pos_str_offsets = - (6 * sizeof(int16_t)) // header (12 bytes) - + header[1] // length of names section - + header[2] // length of boolean section + nbytes_header // header (12 bytes) + + nbytes_names // length of names section + + nbytes_bools // length of boolean section + align_offset + - (header[3] * bytes_per_int); // length of numbers section + (num_ints * bytes_per_int); // length of numbers section const int pos_str_table = pos_str_offsets + - (header[4] * sizeof(int16_t)); // length of string offsets table + (num_offsets * sizeof(int16_t)); // length of string offsets table // Load caps int i; for (i = 0; i < TB_CAP__COUNT; i++) { - const char *cap = get_terminfo_string(pos_str_offsets, header[4], - pos_str_table, header[5], terminfo_cap_indexes[i]); + const char *cap = get_terminfo_string(pos_str_offsets, num_offsets, + pos_str_table, nbytes_strings, terminfo_cap_indexes[i]); if (!cap) { // Something is not right return TB_ERR; @@ -2643,9 +3371,7 @@ static int load_builtin_caps(void) { int i, j; const char *term = getenv("TERM"); - if (!term) { - return TB_ERR_NO_TERM; - } + if (!term) return TB_ERR_NO_TERM; // Check for exact TERM match for (i = 0; builtin_terms[i].name != NULL; i++) { @@ -2673,35 +3399,46 @@ static int load_builtin_caps(void) { return TB_ERR_UNSUPPORTED_TERM; } -static const char *get_terminfo_string(int16_t str_offsets_pos, - int16_t str_offsets_len, int16_t str_table_pos, int16_t str_table_len, - int16_t str_index) { - const int str_byte_index = (int)str_index * (int)sizeof(int16_t); - if (str_byte_index >= (int)str_offsets_len * (int)sizeof(int16_t)) { - // An offset beyond the table indicates absent +static const char *get_terminfo_string(int16_t offsets_pos, int16_t offsets_len, + int16_t table_pos, int16_t table_size, int16_t index) { + if (index >= offsets_len) { + // An index beyond the offset table indicates absent // See `convert_strings` in tinfo `read_entry.c` return ""; } - const int16_t *str_offset = - (int16_t *)(global.terminfo + (int)str_offsets_pos + str_byte_index); - if ((char *)str_offset >= global.terminfo + global.nterminfo) { - // str_offset points beyond end of entry + + int16_t table_offset; + int table_offset_offset = (int)offsets_pos + (index * (int)sizeof(int16_t)); + if (get_terminfo_int16(table_offset_offset, &table_offset) != TB_OK) { + // offset beyond end of terminfo entry // Truncated/corrupt terminfo entry? return NULL; } - if (*str_offset < 0 || *str_offset >= str_table_len) { + + if (table_offset < 0 || table_offset >= table_size) { // A negative offset indicates absent - // An offset beyond the table indicates absent + // An offset beyond the string table indicates absent // See `convert_strings` in tinfo `read_entry.c` return ""; } - if (((size_t)((int)str_table_pos + (int)*str_offset)) >= global.nterminfo) { - // string points beyond end of entry + + int str_offset = (int)table_pos + (int)table_offset; + if (str_offset >= (int)global.nterminfo) { + // string beyond end of terminfo entry // Truncated/corrupt terminfo entry? return NULL; } - return ( - const char *)(global.terminfo + (int)str_table_pos + (int)*str_offset); + + return (const char *)(global.terminfo + str_offset); +} + +static int get_terminfo_int16(int offset, int16_t *val) { + if (offset < 0 || offset >= (int)global.nterminfo) { + *val = -1; + return TB_ERR; + } + memcpy(val, global.terminfo + offset, sizeof(int16_t)); + return TB_OK; } static int wait_event(struct tb_event *event, int timeout) { @@ -2772,9 +3509,7 @@ static int extract_event(struct tb_event *event) { int rv; struct bytebuf_t *in = &global.in; - if (in->len == 0) { - return TB_ERR; - } + if (in->len == 0) return TB_ERR; if (in->buf[0] == '\x1b') { // Escape sequence? @@ -2800,8 +3535,9 @@ static int extract_event(struct tb_event *event) { } // ASCII control key? - if ((uint16_t)in->buf[0] < TB_KEY_SPACE || in->buf[0] == TB_KEY_BACKSPACE2) - { + int is_ctrl = + (uint16_t)in->buf[0] < TB_KEY_SPACE || in->buf[0] == TB_KEY_BACKSPACE2; + if (is_ctrl) { event->type = TB_EVENT_KEY; event->ch = 0; event->key = (uint16_t)in->buf[0]; @@ -2840,14 +3576,10 @@ static int extract_esc_user(struct tb_event *event, int is_post) { fn = is_post ? global.fn_extract_esc_post : global.fn_extract_esc_pre; - if (!fn) { - return TB_ERR; - } + if (!fn) return TB_ERR; rv = fn(event, &consumed); - if (rv == TB_OK) { - bytebuf_shift(in, consumed); - } + if (rv == TB_OK) bytebuf_shift(in, consumed); if_ok_or_need_more_return(rv, rv); return TB_ERR; @@ -3047,13 +3779,9 @@ static int extract_esc_mouse(struct tb_event *event) { ret = TB_ERR; } - if (buf_shift > 0) { - bytebuf_shift(in, buf_shift); - } + if (buf_shift > 0) bytebuf_shift(in, buf_shift); - if (ret == TB_OK) { - event->type = TB_EVENT_MOUSE; - } + if (ret == TB_OK) event->type = TB_EVENT_MOUSE; return ret; } @@ -3291,7 +4019,7 @@ static int send_cluster(int x, int y, uint32_t *ch, size_t nch) { int i; for (i = 0; i < (int)nch; i++) { uint32_t ch32 = *(ch + i); - if (!iswprint((wint_t)ch32)) { + if (!tb_iswprint(ch32)) { ch32 = 0xfffd; // replace non-printable codepoints with U+FFFD } int chu8_len = tb_utf8_unicode_to_char(chu8, ch32); @@ -3341,6 +4069,7 @@ static int cell_copy(struct tb_cell *dst, struct tb_cell *src) { static int cell_set(struct tb_cell *cell, uint32_t *ch, size_t nch, uintattr_t fg, uintattr_t bg) { + // TODO: iswprint ch? cell->ch = ch ? *ch : 0; cell->fg = fg; cell->bg = bg; @@ -3363,12 +4092,9 @@ static int cell_set(struct tb_cell *cell, uint32_t *ch, size_t nch, static int cell_reserve_ech(struct tb_cell *cell, size_t n) { #ifdef TB_OPT_EGC - if (cell->cech >= n) { - return TB_OK; - } - if (!(cell->ech = (uint32_t*)tb_realloc(cell->ech, n * sizeof(cell->ch)))) { - return TB_ERR_MEM; - } + if (cell->cech >= n) return TB_OK; + cell->ech = (uint32_t *)tb_realloc(cell->ech, n * sizeof(cell->ch)); + if (!cell->ech) return TB_ERR_MEM; cell->cech = n; return TB_OK; #else @@ -3380,9 +4106,7 @@ static int cell_reserve_ech(struct tb_cell *cell, size_t n) { static int cell_free(struct tb_cell *cell) { #ifdef TB_OPT_EGC - if (cell->ech) { - tb_free(cell->ech); - } + if (cell->ech) tb_free(cell->ech); #endif memset(cell, 0, sizeof(*cell)); return TB_OK; @@ -3390,9 +4114,7 @@ static int cell_free(struct tb_cell *cell) { static int cellbuf_init(struct cellbuf_t *c, int w, int h) { c->cells = (struct tb_cell *)tb_malloc(sizeof(struct tb_cell) * w * h); - if (!c->cells) { - return TB_ERR_MEM; - } + if (!c->cells) return TB_ERR_MEM; memset(c->cells, 0, sizeof(struct tb_cell) * w * h); c->width = w; c->height = h; @@ -3489,9 +4211,7 @@ static int bytebuf_nputs(struct bytebuf_t *b, const char *str, size_t nstr) { } static int bytebuf_shift(struct bytebuf_t *b, size_t n) { - if (n > b->len) { - n = b->len; - } + if (n > b->len) n = b->len; size_t nmove = b->len - n; memmove(b->buf, b->buf + n, nmove); b->len -= n; @@ -3499,9 +4219,7 @@ static int bytebuf_shift(struct bytebuf_t *b, size_t n) { } static int bytebuf_flush(struct bytebuf_t *b, int fd) { - if (b->len <= 0) { - return TB_OK; - } + if (b->len <= 0) return TB_OK; ssize_t write_rv = write(fd, b->buf, b->len); if (write_rv < 0 || (size_t)write_rv != b->len) { // Note, errno will be 0 on partial write @@ -3513,33 +4231,91 @@ static int bytebuf_flush(struct bytebuf_t *b, int fd) { } static int bytebuf_reserve(struct bytebuf_t *b, size_t sz) { - if (b->cap >= sz) { - return TB_OK; - } + if (b->cap >= sz) return TB_OK; + size_t newcap = b->cap > 0 ? b->cap : 1; while (newcap < sz) { newcap *= 2; } + char *newbuf; if (b->buf) { newbuf = (char *)tb_realloc(b->buf, newcap); } else { newbuf = (char *)tb_malloc(newcap); } - if (!newbuf) { - return TB_ERR_MEM; - } + if (!newbuf) return TB_ERR_MEM; + b->buf = newbuf; b->cap = newcap; return TB_OK; } static int bytebuf_free(struct bytebuf_t *b) { - if (b->buf) { - tb_free(b->buf); - } + if (b->buf) tb_free(b->buf); memset(b, 0, sizeof(*b)); return TB_OK; } +int tb_iswprint(uint32_t ch) { +#ifdef TB_OPT_LIBC_WCHAR + return iswprint((wint_t)ch); +#else + return tb_iswprint_ex(ch, NULL); +#endif +} + +int tb_wcwidth(uint32_t ch) { +#ifdef TB_OPT_LIBC_WCHAR + return wcwidth((wchar_t)ch); +#else + return tb_wcswidth(&ch, 1); +#endif +} + +static int tb_wcswidth(uint32_t *ch, size_t nch) { +#ifdef TB_OPT_LIBC_WCHAR + return wcswidth((wchar_t *)ch, nch); +#else + int sw = 0; + size_t i = 0; + for (i = 0; i < nch; i++) { + int w; + tb_iswprint_ex(ch[i], &w); + if (w < 0) return -1; + sw += w; + } + return sw; +#endif +} + +static int tb_iswprint_ex(uint32_t ch, int *w) { +#ifdef TB_OPT_LIBC_WCHAR + if (w) *w = wcwidth((wint_t)ch); + return iswprint(ch); +#else + int lo = 0, hi = WCWIDTH_TABLE_LENGTH - 1; + if (ch >= 0x20 && ch <= 0x7e) { // fast path for ASCII + if (w) *w = 1; + return 1; + } else if (ch == 0) { // Special case for null, which is not represented in + if (w) *w = 0; // wcwidth_table since it's the only codepoint that is + return 0; // iswprint==0 but not wcwidth==-1. (It's wcwidth==0.) + } + while (lo <= hi) { + int i = (lo + hi) / 2; + if (ch < wcwidth_table[i].range_start) { + hi = i - 1; + } else if (ch > wcwidth_table[i].range_end) { + lo = i + 1; + } else { + if (w) *w = wcwidth_table[i].width; + return wcwidth_table[i].width >= 0 ? 1 : 0; + } + } + if (w) *w = -1; // invalid codepoint + return 0; +#endif +} + #endif // TB_IMPL From d9204131aa986fb4df704d157973cd565383a4f3 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Fri, 30 May 2025 09:24:07 +0200 Subject: [PATCH 197/530] Fix brightness up key not working (closes #763) 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 bd89b15..6ddd1ff 100644 --- a/src/main.zig +++ b/src/main.zig @@ -376,7 +376,7 @@ pub fn main() !void { const sleep_len = try TerminalBuffer.strWidth(lang.sleep); 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_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; const brightness_up_len = try TerminalBuffer.strWidth(lang.brightness_up); var event: termbox.tb_event = undefined; From 02729cce21c52d9bbc7781ae1ae034047fe3bd46 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Fri, 30 May 2025 14:17:41 +0200 Subject: [PATCH 198/530] Add brightnessctl to runtime dependency list Signed-off-by: AnErrupTion --- readme.md | 1 + 1 file changed, 1 insertion(+) diff --git a/readme.md b/readme.md index ec3b649..cf0c095 100644 --- a/readme.md +++ b/readme.md @@ -16,6 +16,7 @@ Ly is a lightweight TUI (ncurses-like) display manager for Linux and BSD. - xorg - xorg-xauth - shutdown + - brightnessctl ### Debian ``` From cedb7a3b026fe19bc4bb6d0be2405b9344a55d6f Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Fri, 30 May 2025 14:18:29 +0200 Subject: [PATCH 199/530] Fix TTY not being cleared sometimes (closes #696) Signed-off-by: AnErrupTion --- src/main.zig | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/main.zig b/src/main.zig index 6ddd1ff..a182d89 100644 --- a/src/main.zig +++ b/src/main.zig @@ -783,6 +783,11 @@ pub fn main() !void { try info_line.addMessage(lang.logout, config.bg, config.fg); } + // Clear the TTY because termbox2 doesn't properly do it + const capability = termbox.global.caps[termbox.TB_CAP_CLEAR_SCREEN]; + const capability_slice = capability[0..std.mem.len(capability)]; + _ = try std.posix.write(termbox.global.ttyfd, capability_slice); + try std.posix.tcsetattr(std.posix.STDIN_FILENO, .FLUSH, tb_termios); if (auth_fails < config.auth_fails) _ = termbox.tb_clear(); From 67a4dd8f9d8ffa1f136712c4ee3cd9a9a9867c58 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Fri, 30 May 2025 16:11:14 +0200 Subject: [PATCH 200/530] Rewrite DOOM animation (fixes #692) Signed-off-by: AnErrupTion --- src/animations/Doom.zig | 34 +++++++++++++--------------------- 1 file changed, 13 insertions(+), 21 deletions(-) diff --git a/src/animations/Doom.zig b/src/animations/Doom.zig index f651ed4..41de5ee 100644 --- a/src/animations/Doom.zig +++ b/src/animations/Doom.zig @@ -55,31 +55,21 @@ fn realloc(self: *Doom) anyerror!void { fn draw(self: *Doom) void { 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| { - const source = y * self.terminal_buffer.width + x; - const random = (self.terminal_buffer.random.int(u16) % 7) & 3; + // Get current cell + const from = y * self.terminal_buffer.width + x; + const cell_index = self.buffer[from]; - var dest = (source - @min(source, random)) + 1; - if (self.terminal_buffer.width > dest) dest = 0 else dest -= self.terminal_buffer.width; + // Spread fire + const propagate = self.terminal_buffer.random.int(u1); + const to = from - self.terminal_buffer.width; // Get the line above - const buffer_source = self.buffer[source]; - const buffer_dest_offset = random & 1; + self.buffer[to] = if (cell_index > 0) cell_index - propagate else cell_index; - if (buffer_source < buffer_dest_offset) continue; - - var buffer_dest = buffer_source - buffer_dest_offset; - if (buffer_dest > STEPS) buffer_dest = 0; - self.buffer[dest] = @intCast(buffer_dest); - - const dest_y = dest / self.terminal_buffer.width; - const dest_x = dest % self.terminal_buffer.width; - const dest_cell = self.fire[buffer_dest]; - dest_cell.put(dest_x, dest_y); - - const source_y = source / self.terminal_buffer.width; - const source_x = source % self.terminal_buffer.width; - const source_cell = self.fire[buffer_source]; - source_cell.put(source_x, source_y); + // Put the cell + const cell = self.fire[cell_index]; + cell.put(x, y); } } } @@ -89,6 +79,8 @@ fn initBuffer(buffer: []u8, width: usize) void { const slice_start = buffer[0..length]; const slice_end = buffer[length..]; + // Initialize the framebuffer in black, except for the "fire source" as the + // last color @memset(slice_start, 0); @memset(slice_end, STEPS); } From 11d9cf8b719b4bd4cf7e018633ab860107b65b54 Mon Sep 17 00:00:00 2001 From: B P Date: Fri, 30 May 2025 18:35:46 +0300 Subject: [PATCH 201/530] Update readme.md --- readme.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/readme.md b/readme.md index cf0c095..be31748 100644 --- a/readme.md +++ b/readme.md @@ -78,6 +78,14 @@ Install Ly for systemd-based systems (the default) # zig build installexe ``` +Instead of DISPLAY_MANAGER you need to add your DM: +- gdm.service +- sddm.service +- lightdm.service +``` +# systemctl disable DISPLAY_MANAGER +``` + Enable the service ``` # systemctl enable ly.service From b42953fd7e96d3d329f0a354c5ea02065d405bad Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Fri, 30 May 2025 17:39:44 +0200 Subject: [PATCH 202/530] Update screenshot Signed-off-by: AnErrupTion --- .github/screenshot.png | Bin 121750 -> 43911 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/.github/screenshot.png b/.github/screenshot.png index 9d0db4bd0bf792c36c63bb21a498497675d3dd08..969417cba9c9bec1ed6413f27ee89ab6cedf140f 100644 GIT binary patch literal 43911 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\E98=BtKFgMP}Q?TyE(GC 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

F|>no4{(d^3M;!CF?;p39QC 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 Date: Fri, 30 May 2025 19:29:04 +0200 Subject: [PATCH 203/530] Start v1.2.0 development cycle Signed-off-by: AnErrupTion --- .github/ISSUE_TEMPLATE/bug.yml | 4 ++-- build.zig | 2 +- build.zig.zon | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug.yml b/.github/ISSUE_TEMPLATE/bug.yml index 3314a84..a94cfc9 100644 --- a/.github/ISSUE_TEMPLATE/bug.yml +++ b/.github/ISSUE_TEMPLATE/bug.yml @@ -15,7 +15,7 @@ body: id: version attributes: label: Ly version - description: The output of `ly --version`. Please note that only Ly v1.0.0 and above are supported. + description: The output of `ly --version`. Please note that only Ly v1.1.0 and above are supported. placeholder: 1.1.0-dev.12+2b0301c validations: required: true @@ -59,7 +59,7 @@ body: label: Relevant logs description: | Please copy and paste any relevant logs, error messages or any other output. This will be automatically formatted into code, so no need for backticks. Screenshots are accepted if they make life easier for you. - If you're using the latest code on master (for v1.1.0), including your session log (found at /var/log/ly-session.log unless modified) is a good idea. (But make sure it's relevant!) + If it exists, ncluding your session log (found at /var/log/ly-session.log unless modified) is a good idea. (But make sure it's relevant!) render: shell - type: textarea id: moreinfo diff --git a/build.zig b/build.zig index 36635d8..80de3e8 100644 --- a/build.zig +++ b/build.zig @@ -21,7 +21,7 @@ comptime { } } -const ly_version = std.SemanticVersion{ .major = 1, .minor = 1, .patch = 0 }; +const ly_version = std.SemanticVersion{ .major = 1, .minor = 2, .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 48c58f3..08d7e95 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -1,6 +1,6 @@ .{ .name = .ly, - .version = "1.1.0", + .version = "1.2.0", .fingerprint = 0xa148ffcc5dc2cb59, .minimum_zig_version = "0.14.0", .dependencies = .{ From fa46155f725ce907f3bc7343bb53ccbcf583906a Mon Sep 17 00:00:00 2001 From: thoxy Date: Fri, 30 May 2025 20:04:52 +0200 Subject: [PATCH 204/530] make gameoflife more configurable and fix pull reviews --- res/config.ini | 26 ++++++ src/animations/GameOfLife.zig | 155 ++++++++++------------------------ src/config/Config.zig | 5 ++ src/main.zig | 2 +- 4 files changed, 76 insertions(+), 112 deletions(-) diff --git a/res/config.ini b/res/config.ini index 53050e4..c358c19 100644 --- a/res/config.ini +++ b/res/config.ini @@ -125,6 +125,32 @@ error_fg = 0x01FF0000 # Foreground color id fg = 0x00FFFFFF +# Game of Life entropy interval (0 = disabled, >0 = add entropy every N generations) +# 0 -> Pure Conway's Game of Life (will eventually stabilize) +# 10 -> Add entropy every 10 generations (recommended for continuous activity) +# 50+ -> Less frequent entropy for more natural evolution +gameoflife_entropy_interval = 10 + +# Game of Life animation foreground color id +gameoflife_fg = 0x0000FF00 + +# Game of Life frame delay (lower = faster animation, higher = slower) +# 1-3 -> Very fast animation +# 6 -> Default smooth animation speed +# 10+ -> Slower, more contemplative speed +gameoflife_frame_delay = 6 + +# Game of Life initial cell density (0.0 to 1.0) +# 0.1 -> Sparse, minimal activity +# 0.4 -> Balanced activity (recommended) +# 0.7+ -> Dense, chaotic patterns +gameoflife_initial_density = 0.4 + +# Game of Life randomize colors (true/false) +# false -> Use the fixed gameoflife_fg color +# true -> Generate one random color at startup and use it for the entire session +gameoflife_randomize_colors = false + # Remove main box borders hide_borders = false diff --git a/src/animations/GameOfLife.zig b/src/animations/GameOfLife.zig index 1130262..f564e52 100644 --- a/src/animations/GameOfLife.zig +++ b/src/animations/GameOfLife.zig @@ -8,30 +8,9 @@ const Random = std.Random; const GameOfLife = @This(); -pub const FRAME_DELAY: usize = 6; // Slightly faster for smoother animation -pub const INITIAL_DENSITY: f32 = 0.4; // Increased for more activity -pub const COLOR_CYCLE_DELAY: usize = 192; // Change color every N frames - // Visual styles - using block characters like other animations const ALIVE_CHAR: u21 = 0x2588; // Full block █ const DEAD_CHAR: u21 = ' '; - -// ANSI basic colors using TerminalBuffer.Color like other animations -const ANSI_COLORS = [_]u32{ - @intCast(TerminalBuffer.Color.RED), - @intCast(TerminalBuffer.Color.GREEN), - @intCast(TerminalBuffer.Color.YELLOW), - @intCast(TerminalBuffer.Color.BLUE), - @intCast(TerminalBuffer.Color.MAGENTA), - @intCast(TerminalBuffer.Color.CYAN), - @intCast(TerminalBuffer.Color.RED | TerminalBuffer.Styling.BOLD), - @intCast(TerminalBuffer.Color.GREEN | TerminalBuffer.Styling.BOLD), - @intCast(TerminalBuffer.Color.YELLOW | TerminalBuffer.Styling.BOLD), - @intCast(TerminalBuffer.Color.BLUE | TerminalBuffer.Styling.BOLD), - @intCast(TerminalBuffer.Color.MAGENTA | TerminalBuffer.Styling.BOLD), - @intCast(TerminalBuffer.Color.CYAN | TerminalBuffer.Styling.BOLD), -}; -const NUM_COLORS = ANSI_COLORS.len; const NEIGHBOR_DIRS = [_][2]i8{ .{ -1, -1 }, .{ -1, 0 }, .{ -1, 1 }, .{ 0, -1 }, .{ 0, 1 }, .{ 1, -1 }, @@ -44,13 +23,16 @@ current_grid: []bool, next_grid: []bool, frame_counter: usize, generation: u64, -color_index: usize, -color_counter: usize, +fg_color: u32, +entropy_interval: usize, +frame_delay: usize, +initial_density: f32, +randomize_colors: bool, dead_cell: Cell, width: usize, height: usize, -pub fn init(allocator: Allocator, terminal_buffer: *TerminalBuffer) !GameOfLife { +pub fn init(allocator: Allocator, terminal_buffer: *TerminalBuffer, fg_color: u32, entropy_interval: usize, frame_delay: usize, initial_density: f32, randomize_colors: bool) !GameOfLife { const width = terminal_buffer.width; const height = terminal_buffer.height; const grid_size = width * height; @@ -65,8 +47,11 @@ pub fn init(allocator: Allocator, terminal_buffer: *TerminalBuffer) !GameOfLife .next_grid = next_grid, .frame_counter = 0, .generation = 0, - .color_index = 0, - .color_counter = 0, + .fg_color = if (randomize_colors) generateRandomColor(terminal_buffer.random) else fg_color, + .entropy_interval = entropy_interval, + .frame_delay = frame_delay, + .initial_density = initial_density, + .randomize_colors = randomize_colors, .dead_cell = .{ .ch = DEAD_CHAR, .fg = @intCast(TerminalBuffer.Color.DEFAULT), .bg = terminal_buffer.bg }, .width = width, .height = height, @@ -92,47 +77,35 @@ fn realloc(self: *GameOfLife) anyerror!void { const new_height = self.terminal_buffer.height; const new_size = new_width * new_height; - // Only reallocate if size changed significantly - if (new_size != self.width * self.height) { - const current_grid = try self.allocator.realloc(self.current_grid, new_size); - const next_grid = try self.allocator.realloc(self.next_grid, new_size); + // Always reallocate to be safe + const current_grid = try self.allocator.realloc(self.current_grid, new_size); + const next_grid = try self.allocator.realloc(self.next_grid, new_size); - self.current_grid = current_grid; - self.next_grid = next_grid; - self.width = new_width; - self.height = new_height; + self.current_grid = current_grid; + self.next_grid = next_grid; + self.width = new_width; + self.height = new_height; - self.initializeGrid(); - self.generation = 0; - self.color_index = 0; - self.color_counter = 0; - } + self.initializeGrid(); + self.generation = 0; } fn draw(self: *GameOfLife) void { - // Update ANSI color cycling at controlled rate - self.color_counter += 1; - if (self.color_counter >= COLOR_CYCLE_DELAY) { - self.color_counter = 0; - self.color_index = (self.color_index + 1) % NUM_COLORS; - } - // Update game state at controlled frame rate self.frame_counter += 1; - if (self.frame_counter >= FRAME_DELAY) { + if (self.frame_counter >= self.frame_delay) { self.frame_counter = 0; self.updateGeneration(); self.generation += 1; - // Add entropy less frequently to reduce computational overhead - if (self.generation % 150 == 0) { + // Add entropy based on configuration (0 = disabled, >0 = interval) + if (self.entropy_interval > 0 and self.generation % self.entropy_interval == 0) { self.addEntropy(); } } - // Render with ANSI color cycling - use current color from the array (same method as Matrix/Doom) - const current_color = ANSI_COLORS[self.color_index]; - const alive_cell = Cell{ .ch = ALIVE_CHAR, .fg = current_color, .bg = self.terminal_buffer.bg }; + // Render with the set color (either configured or randomly generated at startup) + const alive_cell = Cell{ .ch = ALIVE_CHAR, .fg = self.fg_color, .bg = self.terminal_buffer.bg }; for (0..self.height) |y| { const row_offset = y * self.width; @@ -143,6 +116,15 @@ fn draw(self: *GameOfLife) void { } } +fn generateRandomColor(random: Random) u32 { + // Generate a random RGB color with good visibility + // Avoid very dark colors by using range 64-255 for each component + const r = random.intRangeAtMost(u8, 64, 255); + const g = random.intRangeAtMost(u8, 64, 255); + const b = random.intRangeAtMost(u8, 64, 255); + return (@as(u32, r) << 16) | (@as(u32, g) << 8) | @as(u32, b); +} + fn updateGeneration(self: *GameOfLife) void { // Conway's Game of Life rules with optimized neighbor counting for (0..self.height) |y| { @@ -170,12 +152,16 @@ fn countNeighborsOptimized(self: *GameOfLife, x: usize, y: usize) u8 { // Use cached dimensions and more efficient bounds checking for (NEIGHBOR_DIRS) |dir| { - const nx = @as(i32, @intCast(x)) + dir[0]; - const ny = @as(i32, @intCast(y)) + dir[1]; + const nx: i32 = @intCast(x); + const ny: i32 = @intCast(y); + const neighbor_x: i32 = nx + dir[0]; + const neighbor_y: i32 = ny + dir[1]; + const width_i32: i32 = @intCast(self.width); + const height_i32: i32 = @intCast(self.height); // Toroidal wrapping with modular arithmetic - const wx: usize = @intCast(@mod(nx + @as(i32, @intCast(self.width)), @as(i32, @intCast(self.width)))); - const wy: usize = @intCast(@mod(ny + @as(i32, @intCast(self.height)), @as(i32, @intCast(self.height)))); + const wx: usize = @intCast(@mod(neighbor_x + width_i32, width_i32)); + const wy: usize = @intCast(@mod(neighbor_y + height_i32, height_i32)); if (self.current_grid[wy * self.width + wx]) { count += 1; @@ -192,62 +178,9 @@ fn initializeGrid(self: *GameOfLife) void { @memset(self.current_grid, false); @memset(self.next_grid, false); - // Random initialization with better distribution + // Random initialization with configurable density for (0..total_cells) |i| { - self.current_grid[i] = self.terminal_buffer.random.float(f32) < INITIAL_DENSITY; - } - - // Add interesting patterns with better positioning - self.addPatterns(); -} - -fn addPatterns(self: *GameOfLife) void { - if (self.width < 8 or self.height < 8) return; - - // Add multiple instances of each pattern for liveliness - for (0..3) |_| { - self.addGlider(); - if (self.width >= 10 and self.height >= 10) { - self.addBlock(); - self.addBlinker(); - } - } -} - -fn addGlider(self: *GameOfLife) void { - const x = self.terminal_buffer.random.intRangeAtMost(usize, 2, self.width - 4); - const y = self.terminal_buffer.random.intRangeAtMost(usize, 2, self.height - 4); - - // Classic glider pattern - const positions = [_][2]usize{ .{ 1, 0 }, .{ 2, 1 }, .{ 0, 2 }, .{ 1, 2 }, .{ 2, 2 } }; - - for (positions) |pos| { - const idx = (y + pos[1]) * self.width + (x + pos[0]); - self.current_grid[idx] = true; - } -} - -fn addBlock(self: *GameOfLife) void { - const x = self.terminal_buffer.random.intRangeAtMost(usize, 1, self.width - 3); - const y = self.terminal_buffer.random.intRangeAtMost(usize, 1, self.height - 3); - - // 2x2 block - const positions = [_][2]usize{ .{ 0, 0 }, .{ 1, 0 }, .{ 0, 1 }, .{ 1, 1 } }; - - for (positions) |pos| { - const idx = (y + pos[1]) * self.width + (x + pos[0]); - self.current_grid[idx] = true; - } -} - -fn addBlinker(self: *GameOfLife) void { - const x = self.terminal_buffer.random.intRangeAtMost(usize, 1, self.width - 4); - const y = self.terminal_buffer.random.intRangeAtMost(usize, 1, self.height - 2); - - // 3-cell horizontal line - for (0..3) |i| { - const idx = y * self.width + (x + i); - self.current_grid[idx] = true; + self.current_grid[i] = self.terminal_buffer.random.float(f32) < self.initial_density; } } diff --git a/src/config/Config.zig b/src/config/Config.zig index c0f3411..60dad89 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -36,6 +36,11 @@ doom_bottom_color: u32 = 0x00FFFFFF, error_bg: u32 = 0x00000000, error_fg: u32 = 0x01FF0000, fg: u32 = 0x00FFFFFF, +gameoflife_fg: u32 = 0x0000FF00, +gameoflife_entropy_interval: usize = 10, +gameoflife_frame_delay: usize = 6, +gameoflife_initial_density: f32 = 0.4, +gameoflife_randomize_colors: bool = false, hide_borders: bool = false, hide_key_hints: bool = false, initial_info_text: ?[]const u8 = null, diff --git a/src/main.zig b/src/main.zig index 259a5e3..c70d5aa 100644 --- a/src/main.zig +++ b/src/main.zig @@ -366,7 +366,7 @@ pub fn main() !void { animation = color_mix.animation(); }, .gameoflife => { - var game_of_life = try GameOfLife.init(allocator, &buffer); + var game_of_life = try GameOfLife.init(allocator, &buffer, config.gameoflife_fg, config.gameoflife_entropy_interval, config.gameoflife_frame_delay, config.gameoflife_initial_density, config.gameoflife_randomize_colors); animation = game_of_life.animation(); }, } From 9d4c4a3a59d0790edfaa9943849c391e5b0d4a15 Mon Sep 17 00:00:00 2001 From: thoxy Date: Fri, 30 May 2025 20:11:53 +0200 Subject: [PATCH 205/530] add mention of the new animation in the config file --- res/config.ini | 1 + 1 file changed, 1 insertion(+) diff --git a/res/config.ini b/res/config.ini index c358c19..b405f2f 100644 --- a/res/config.ini +++ b/res/config.ini @@ -23,6 +23,7 @@ allow_empty_password = true # doom -> PSX DOOM fire # matrix -> CMatrix # colormix -> Color mixing shader +# gameoflife -> John Conway's Game of Life animation = none # Stop the animation after some time From 36a27f6167fab7daa8d7f79c770fd3027307774a Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Sat, 31 May 2025 10:32:03 +0200 Subject: [PATCH 206/530] Fix big clock UB in ReleaseSafe Signed-off-by: AnErrupTion --- src/bigclock.zig | 26 +++++++++++++------------- src/bigclock/Lang.zig | 4 +++- 2 files changed, 16 insertions(+), 14 deletions(-) diff --git a/src/bigclock.zig b/src/bigclock.zig index d63f0fd..4fae3a4 100644 --- a/src/bigclock.zig +++ b/src/bigclock.zig @@ -34,24 +34,24 @@ pub fn alphaBlit(x: usize, y: usize, tb_width: usize, tb_height: usize, cells: [ } } -fn toBigNumber(char: u8, bigclock: Bigclock) []const u21 { +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, - ':' => &locale_chars.S, - else => &locale_chars.E, + '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, + ':' => locale_chars.S, + else => locale_chars.E, }; } diff --git a/src/bigclock/Lang.zig b/src/bigclock/Lang.zig index 4229776..6ba8cf1 100644 --- a/src/bigclock/Lang.zig +++ b/src/bigclock/Lang.zig @@ -7,6 +7,7 @@ pub const SIZE = WIDTH * HEIGHT; pub const X: u32 = if (builtin.os.tag == .linux or builtin.os.tag.isBSD()) 0x2593 else '#'; pub const O: u32 = 0; +// zig fmt: off pub const LocaleChars = struct { ZERO: [SIZE]u21, ONE: [SIZE]u21, @@ -20,4 +21,5 @@ pub const LocaleChars = struct { NINE: [SIZE]u21, S: [SIZE]u21, E: [SIZE]u21, -}; \ No newline at end of file +}; +// zig fmt: on From a8b82923188ad22c8a9f98d91b5d2772e50b5b65 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Sat, 31 May 2025 11:04:46 +0200 Subject: [PATCH 207/530] Add name of unknown error instead of generic string Signed-off-by: AnErrupTion --- res/lang/ar.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/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 | 1 - res/lang/tr.ini | 1 - res/lang/uk.ini | 1 - res/lang/zh_CN.ini | 1 - src/config/Lang.zig | 1 - src/main.zig | 2 +- 20 files changed, 1 insertion(+), 20 deletions(-) diff --git a/res/lang/ar.ini b/res/lang/ar.ini index 86393df..835fd66 100644 --- a/res/lang/ar.ini +++ b/res/lang/ar.ini @@ -39,7 +39,6 @@ err_perm_user = فشل في تخفيض صلاحيات المستخدم (User per err_pwnam = فشل في جلب معلومات المستخدم err_sleep = فشل في تنفيذ أمر sleep err_tty_ctrl = فشل في نقل تحكم الطرفية (TTY) -err_unknown = حدث خطأ غير معروف err_user_gid = فشل في تعيين معرّف المجموعة (GID) للمستخدم err_user_init = فشل في تهيئة بيانات المستخدم err_user_uid = فشل في تعيين معرّف المستخدم (UID) diff --git a/res/lang/cat.ini b/res/lang/cat.ini index 0351223..01d9232 100644 --- a/res/lang/cat.ini +++ b/res/lang/cat.ini @@ -39,7 +39,6 @@ err_perm_user = error en degradar els permisos de l'usuari err_pwnam = error en obtenir la informació de l'usuari -err_unknown = ha ocorregut un error desconegut 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 diff --git a/res/lang/cs.ini b/res/lang/cs.ini index b2653d3..2adc297 100644 --- a/res/lang/cs.ini +++ b/res/lang/cs.ini @@ -39,7 +39,6 @@ err_perm_user = nepodařilo se snížit uživatelská oprávnění err_pwnam = nelze získat informace o uživateli - err_user_gid = nastavení GID uživatele selhalo err_user_init = inicializace uživatele selhala err_user_uid = nastavení UID uživateli selhalo diff --git a/res/lang/de.ini b/res/lang/de.ini index d7da11e..af8090d 100644 --- a/res/lang/de.ini +++ b/res/lang/de.ini @@ -39,7 +39,6 @@ err_perm_user = Fehler beim heruntersetzen der Nutzer Berechtigungen err_pwnam = Holen der Benutzerinformationen fehlgeschlagen - err_user_gid = Fehler beim setzen der Gruppen Id des Nutzers err_user_init = Initialisierung des Nutzers fehlgeschlagen err_user_uid = Setzen der Benutzer Id fehlgeschlagen diff --git a/res/lang/en.ini b/res/lang/en.ini index 49accc5..a0eae31 100644 --- a/res/lang/en.ini +++ b/res/lang/en.ini @@ -39,7 +39,6 @@ err_perm_user = failed to downgrade user permissions err_pwnam = failed to get user info err_sleep = failed to execute sleep command err_tty_ctrl = tty control transfer failed -err_unknown = an unknown error occurred err_user_gid = failed to set user GID err_user_init = failed to initialize user err_user_uid = failed to set user UID diff --git a/res/lang/es.ini b/res/lang/es.ini index e1fbab5..5004d12 100644 --- a/res/lang/es.ini +++ b/res/lang/es.ini @@ -39,7 +39,6 @@ err_perm_user = error al degradar los permisos del usuario err_pwnam = error al obtener la información del usuario - 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 diff --git a/res/lang/fr.ini b/res/lang/fr.ini index 728d0bd..75c7c6f 100644 --- a/res/lang/fr.ini +++ b/res/lang/fr.ini @@ -39,7 +39,6 @@ err_perm_user = échec du déclassement des permissions utilisateur err_pwnam = échec de lecture des infos utilisateur err_sleep = échec de l'exécution de la commande de veille err_tty_ctrl = échec du transfert de contrôle du terminal -err_unknown = une erreur inconnue est survenue err_user_gid = échec de modification du GID err_user_init = échec d'initialisation de l'utilisateur err_user_uid = échec de modification du UID diff --git a/res/lang/it.ini b/res/lang/it.ini index 3527f3e..8d749c6 100644 --- a/res/lang/it.ini +++ b/res/lang/it.ini @@ -39,7 +39,6 @@ err_perm_user = impossibile ridurre permessi utente err_pwnam = impossibile ottenere dati utente - err_user_gid = impossibile impostare GID utente err_user_init = impossibile inizializzare utente err_user_uid = impossible impostare UID utente diff --git a/res/lang/pl.ini b/res/lang/pl.ini index abef3a8..cdd3f29 100644 --- a/res/lang/pl.ini +++ b/res/lang/pl.ini @@ -39,7 +39,6 @@ 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_tty_ctrl = nie udało się przekazać kontroli tty -err_unknown = wystąpił nieznany błąd 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 diff --git a/res/lang/pt.ini b/res/lang/pt.ini index eaaf8ff..25361b6 100644 --- a/res/lang/pt.ini +++ b/res/lang/pt.ini @@ -39,7 +39,6 @@ err_perm_user = erro ao reduzir as permissões do utilizador err_pwnam = erro ao obter informação do utilizador - 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 diff --git a/res/lang/pt_BR.ini b/res/lang/pt_BR.ini index 33b4437..8148e27 100644 --- a/res/lang/pt_BR.ini +++ b/res/lang/pt_BR.ini @@ -39,7 +39,6 @@ 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_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 diff --git a/res/lang/ro.ini b/res/lang/ro.ini index 3f79c86..f4928cf 100644 --- a/res/lang/ro.ini +++ b/res/lang/ro.ini @@ -47,7 +47,6 @@ err_perm_user = nu s-a putut face downgrade permisiunilor de utilizator - login = utilizator logout = opreşte sesiunea diff --git a/res/lang/ru.ini b/res/lang/ru.ini index 3a15f54..8e9b1a0 100644 --- a/res/lang/ru.ini +++ b/res/lang/ru.ini @@ -39,7 +39,6 @@ err_perm_user = не удалось понизить права доступа err_pwnam = не удалось получить информацию о пользователе - err_user_gid = не удалось установить GID пользователя err_user_init = не удалось инициализировать пользователя err_user_uid = не удалось установить UID пользователя diff --git a/res/lang/sr.ini b/res/lang/sr.ini index d6cb896..bedc690 100644 --- a/res/lang/sr.ini +++ b/res/lang/sr.ini @@ -39,7 +39,6 @@ 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 diff --git a/res/lang/sv.ini b/res/lang/sv.ini index 92143f0..40b8cbd 100644 --- a/res/lang/sv.ini +++ b/res/lang/sv.ini @@ -39,7 +39,6 @@ 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 diff --git a/res/lang/tr.ini b/res/lang/tr.ini index 048fb64..77a22ae 100644 --- a/res/lang/tr.ini +++ b/res/lang/tr.ini @@ -39,7 +39,6 @@ err_perm_user = kullanici izinleri dusurulemedi err_pwnam = kullanici bilgileri alinamadi - err_user_gid = kullanici icin GID ayarlanamadi err_user_init = kullanici oturumu baslatilamadi err_user_uid = kullanici icin UID ayarlanamadi diff --git a/res/lang/uk.ini b/res/lang/uk.ini index 3e1098c..a24731e 100644 --- a/res/lang/uk.ini +++ b/res/lang/uk.ini @@ -39,7 +39,6 @@ err_perm_user = не вдалося понизити права доступу err_pwnam = не вдалося отримати дані користувача - err_user_gid = не вдалося змінити GID користувача err_user_init = не вдалося ініціалізувати користувача err_user_uid = не вдалося змінити UID користувача diff --git a/res/lang/zh_CN.ini b/res/lang/zh_CN.ini index be970eb..9960d07 100644 --- a/res/lang/zh_CN.ini +++ b/res/lang/zh_CN.ini @@ -39,7 +39,6 @@ err_perm_user = 用户权限降级失败 err_pwnam = 获取用户信息失败 - err_user_gid = 设置用户GID失败 err_user_init = 初始化用户失败 err_user_uid = 设置用户UID失败 diff --git a/src/config/Lang.zig b/src/config/Lang.zig index fb71b45..3360b71 100644 --- a/src/config/Lang.zig +++ b/src/config/Lang.zig @@ -44,7 +44,6 @@ err_perm_user: []const u8 = "failed to downgrade user permissions", err_pwnam: []const u8 = "failed to get user info", err_sleep: []const u8 = "failed to execute sleep command", err_tty_ctrl: []const u8 = "tty control transfer failed", -err_unknown: []const u8 = "an unknown error occurred", err_user_gid: []const u8 = "failed to set user GID", err_user_init: []const u8 = "failed to initialize user", err_user_uid: []const u8 = "failed to set user UID", diff --git a/src/main.zig b/src/main.zig index a182d89..70d0191 100644 --- a/src/main.zig +++ b/src/main.zig @@ -962,6 +962,6 @@ fn getAuthErrorMsg(err: anyerror, lang: Lang) []const u8 { error.PamSystemError => lang.err_pam_sys, error.PamUserUnknown => lang.err_pam_user_unknown, error.PamAbort => lang.err_pam_abort, - else => lang.err_unknown, + else => @errorName(err), }; } From 5e8e0af59c111f6c48ac3ef2387c4fbdce924ffc Mon Sep 17 00:00:00 2001 From: thoxy Date: Sat, 31 May 2025 17:56:16 +0200 Subject: [PATCH 208/530] Remove redundant comments and type annotations --- src/animations/GameOfLife.zig | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/animations/GameOfLife.zig b/src/animations/GameOfLife.zig index f564e52..785c236 100644 --- a/src/animations/GameOfLife.zig +++ b/src/animations/GameOfLife.zig @@ -77,7 +77,6 @@ fn realloc(self: *GameOfLife) anyerror!void { const new_height = self.terminal_buffer.height; const new_size = new_width * new_height; - // Always reallocate to be safe const current_grid = try self.allocator.realloc(self.current_grid, new_size); const next_grid = try self.allocator.realloc(self.next_grid, new_size); @@ -150,12 +149,9 @@ fn updateGeneration(self: *GameOfLife) void { fn countNeighborsOptimized(self: *GameOfLife, x: usize, y: usize) u8 { var count: u8 = 0; - // Use cached dimensions and more efficient bounds checking for (NEIGHBOR_DIRS) |dir| { - const nx: i32 = @intCast(x); - const ny: i32 = @intCast(y); - const neighbor_x: i32 = nx + dir[0]; - const neighbor_y: i32 = ny + dir[1]; + const neighbor_x = @as(i32, @intCast(x)) + dir[0]; + const neighbor_y = @as(i32, @intCast(y)) + dir[1]; const width_i32: i32 = @intCast(self.width); const height_i32: i32 = @intCast(self.height); From 3504180e955b8c680aa591a139bd025e4689a555 Mon Sep 17 00:00:00 2001 From: Dusan Date: Tue, 10 Jun 2025 06:12:48 +0200 Subject: [PATCH 209/530] Option to hide version string --- res/config.ini | 3 +++ src/config/Config.zig | 1 + src/main.zig | 9 ++++++--- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/res/config.ini b/res/config.ini index 53050e4..7e2c7eb 100644 --- a/res/config.ini +++ b/res/config.ini @@ -128,6 +128,9 @@ fg = 0x00FFFFFF # Remove main box borders hide_borders = false +# Remove version number from the top left corner +hide_version_string = false + # Remove power management command hints hide_key_hints = false diff --git a/src/config/Config.zig b/src/config/Config.zig index c0f3411..99d7162 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -37,6 +37,7 @@ error_bg: u32 = 0x00000000, error_fg: u32 = 0x01FF0000, fg: u32 = 0x00FFFFFF, hide_borders: bool = false, +hide_version_string: bool = false, hide_key_hints: bool = false, initial_info_text: ?[]const u8 = null, input_len: u8 = 34, diff --git a/src/main.zig b/src/main.zig index 70d0191..2906297 100644 --- a/src/main.zig +++ b/src/main.zig @@ -422,9 +422,14 @@ pub fn main() !void { if (auth_fails < config.auth_fails) { _ = termbox.tb_clear(); + var length: usize = 0; + if (!animation_timed_out) animation.draw(); - buffer.drawLabel(ly_top_str, 0, 0); + if (!config.hide_version_string) { + buffer.drawLabel(ly_top_str, 0, 0); + length += ly_top_str.len + 1; + } if (config.bigclock != .none and buffer.box_height + (bigclock.HEIGHT + 2) * 2 < buffer.height) draw_big_clock: { const format = "%H:%M"; @@ -485,8 +490,6 @@ pub fn main() !void { info_line.label.draw(); if (!config.hide_key_hints) { - var length: usize = ly_top_str.len + 1; - buffer.drawLabel(config.shutdown_key, length, 0); length += config.shutdown_key.len + 1; buffer.drawLabel(" ", length - 1, 0); From 14aae40fda912b33f059c1bc293135b01ecd16e4 Mon Sep 17 00:00:00 2001 From: Dusan Date: Tue, 10 Jun 2025 14:34:20 +0200 Subject: [PATCH 210/530] Use existing label value for x --- src/main.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main.zig b/src/main.zig index 2906297..9e41dc2 100644 --- a/src/main.zig +++ b/src/main.zig @@ -427,7 +427,7 @@ pub fn main() !void { if (!animation_timed_out) animation.draw(); if (!config.hide_version_string) { - buffer.drawLabel(ly_top_str, 0, 0); + buffer.drawLabel(ly_top_str, length, 0); length += ly_top_str.len + 1; } From a5e38e2ce5c830576431700c177707dcab3598ec Mon Sep 17 00:00:00 2001 From: thoxy Date: Tue, 17 Jun 2025 12:35:22 +0200 Subject: [PATCH 211/530] Remove color randomization from Game of Life animation --- res/config.ini | 5 ----- src/animations/GameOfLife.zig | 18 +++--------------- src/config/Config.zig | 1 - 3 files changed, 3 insertions(+), 21 deletions(-) diff --git a/res/config.ini b/res/config.ini index b405f2f..df6d721 100644 --- a/res/config.ini +++ b/res/config.ini @@ -147,11 +147,6 @@ gameoflife_frame_delay = 6 # 0.7+ -> Dense, chaotic patterns gameoflife_initial_density = 0.4 -# Game of Life randomize colors (true/false) -# false -> Use the fixed gameoflife_fg color -# true -> Generate one random color at startup and use it for the entire session -gameoflife_randomize_colors = false - # Remove main box borders hide_borders = false diff --git a/src/animations/GameOfLife.zig b/src/animations/GameOfLife.zig index 785c236..9cbefaa 100644 --- a/src/animations/GameOfLife.zig +++ b/src/animations/GameOfLife.zig @@ -4,7 +4,6 @@ const Cell = @import("../tui/Cell.zig"); const TerminalBuffer = @import("../tui/TerminalBuffer.zig"); const Allocator = std.mem.Allocator; -const Random = std.Random; const GameOfLife = @This(); @@ -27,12 +26,11 @@ fg_color: u32, entropy_interval: usize, frame_delay: usize, initial_density: f32, -randomize_colors: 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, randomize_colors: bool) !GameOfLife { +pub fn init(allocator: Allocator, terminal_buffer: *TerminalBuffer, fg_color: u32, entropy_interval: usize, frame_delay: usize, initial_density: f32) !GameOfLife { const width = terminal_buffer.width; const height = terminal_buffer.height; const grid_size = width * height; @@ -47,11 +45,10 @@ pub fn init(allocator: Allocator, terminal_buffer: *TerminalBuffer, fg_color: u3 .next_grid = next_grid, .frame_counter = 0, .generation = 0, - .fg_color = if (randomize_colors) generateRandomColor(terminal_buffer.random) else fg_color, + .fg_color = fg_color, .entropy_interval = entropy_interval, .frame_delay = frame_delay, .initial_density = initial_density, - .randomize_colors = randomize_colors, .dead_cell = .{ .ch = DEAD_CHAR, .fg = @intCast(TerminalBuffer.Color.DEFAULT), .bg = terminal_buffer.bg }, .width = width, .height = height, @@ -103,7 +100,7 @@ fn draw(self: *GameOfLife) void { } } - // Render with the set color (either configured or randomly generated at startup) + // Render with the configured color const alive_cell = Cell{ .ch = ALIVE_CHAR, .fg = self.fg_color, .bg = self.terminal_buffer.bg }; for (0..self.height) |y| { @@ -115,15 +112,6 @@ fn draw(self: *GameOfLife) void { } } -fn generateRandomColor(random: Random) u32 { - // Generate a random RGB color with good visibility - // Avoid very dark colors by using range 64-255 for each component - const r = random.intRangeAtMost(u8, 64, 255); - const g = random.intRangeAtMost(u8, 64, 255); - const b = random.intRangeAtMost(u8, 64, 255); - return (@as(u32, r) << 16) | (@as(u32, g) << 8) | @as(u32, b); -} - fn updateGeneration(self: *GameOfLife) void { // Conway's Game of Life rules with optimized neighbor counting for (0..self.height) |y| { diff --git a/src/config/Config.zig b/src/config/Config.zig index 60dad89..e4030ce 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -40,7 +40,6 @@ gameoflife_fg: u32 = 0x0000FF00, gameoflife_entropy_interval: usize = 10, gameoflife_frame_delay: usize = 6, gameoflife_initial_density: f32 = 0.4, -gameoflife_randomize_colors: bool = false, hide_borders: bool = false, hide_key_hints: bool = false, initial_info_text: ?[]const u8 = null, From e7aad8de888d3d53c9870bd53992ec8e1b26f895 Mon Sep 17 00:00:00 2001 From: thoxy Date: Tue, 17 Jun 2025 12:40:49 +0200 Subject: [PATCH 212/530] remove gameoflife_randomize_colors argument in main for gameoflife --- src/main.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main.zig b/src/main.zig index 6eed2a2..17309df 100644 --- a/src/main.zig +++ b/src/main.zig @@ -366,7 +366,7 @@ pub fn main() !void { 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, config.gameoflife_randomize_colors); + 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(); }, } From ef78ac28a48be8d152656c51b8c88b03b6ec55be Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Tue, 17 Jun 2025 22:28:00 +0200 Subject: [PATCH 213/530] Remove big header about Codeberg migration Signed-off-by: AnErrupTion --- readme.md | 105 ++++++++++++++++++++++++++++++++++++------------------ 1 file changed, 71 insertions(+), 34 deletions(-) diff --git a/readme.md b/readme.md index be31748..57ca447 100644 --- a/readme.md +++ b/readme.md @@ -1,12 +1,13 @@ # Ly - a TUI display manager -## Development is now continuing on [Codeberg](https://codeberg.org/AnErrupTion/ly), with the [GitHub](https://github.com/fairyglade/ly) repository becoming a mirror. Issues & pull requests on GitHub will be ignored from now on. - ![Ly screenshot](.github/screenshot.png "Ly screenshot") Ly is a lightweight TUI (ncurses-like) display manager for Linux and BSD. +**Note**: Development happens on [Codeberg](https://codeberg.org/AnErrupTion/ly) with a mirror on [GitHub](https://github.com/fairyglade/ly). + ## Dependencies + - Compile-time: - zig 0.14.0 - libc @@ -19,11 +20,13 @@ Ly is a lightweight TUI (ncurses-like) display manager for Linux and BSD. - brightnessctl ### Debian + ``` # apt install build-essential libpam0g-dev libxcb-xkb-dev ``` ### 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. @@ -32,6 +35,7 @@ It is recommended to add a rule for Ly as it currently does not ship one. ``` ## Support + The following desktop environments were tested with success: [Wayland Environments](#supported-wayland-environments) @@ -42,72 +46,86 @@ Ly should work with any X desktop environment, and provides basic wayland support (sway works very well, for example). ## systemd? + Unlike what you may have heard, Ly does not require `systemd`, and was even specifically designed not to depend on `logind`. You should be able to make it work easily with a better init, changing the source code won't be necessary :) ## Cloning and Compiling + Clone the repository + ``` $ git clone https://codeberg.org/AnErrupTion/ly ``` Change the directory to ly + ``` $ cd ly ``` Compile + ``` $ zig build ``` Test in the configured tty (tty2 by default) or a terminal emulator (but authentication won't work) + ``` $ zig build run ``` -**Important**: Running Ly in a terminal emulator as root is *not* recommended. If you +**Important**: Running Ly in a terminal emulator as root is _not_ recommended. If you want to properly test Ly, please enable its service (as described below) and reboot your machine. Install Ly for systemd-based systems (the default) + ``` # zig build installexe ``` Instead of DISPLAY_MANAGER you need to add your DM: + - gdm.service - sddm.service - lightdm.service + ``` # systemctl disable DISPLAY_MANAGER ``` Enable the service + ``` # systemctl enable ly.service ``` If you need to switch between ttys after Ly's start you also have to disable getty on Ly's tty to prevent "login" from spawning on top of it + ``` # systemctl disable getty@tty2.service ``` ### OpenRC + **NOTE 1**: On Gentoo, Ly will disable the `display-manager-init` service in order to run. Clone, compile and test. Install Ly and the provided OpenRC service + ``` # zig build installexe -Dinit_system=openrc ``` Enable the service + ``` # rc-update add ly ``` @@ -116,6 +134,7 @@ You can edit which tty Ly will start on by editing the `tty` option in the confi If you choose a tty that already has a login/getty running (has a basic login prompt), then you have to disable getty, so it doesn't respawn on top of ly + ``` # rc-update del agetty.tty2 ``` @@ -123,6 +142,7 @@ then you have to disable getty, so it doesn't respawn on top of ly **NOTE 2**: To avoid a console spawning on top on Ly, comment out the appropriate line from /etc/inittab (default is 2). ### runit + ``` # zig build installexe -Dinit_system=runit # ln -s /etc/sv/ly /var/service/ @@ -145,6 +165,7 @@ you should disable the agetty-tty2 service like this: ``` ### s6 + ``` # zig build installexe -Dinit_system=s6 ``` @@ -160,6 +181,7 @@ Finally, enable the service: ``` ### dinit + ``` # zig build installexe -Dinit_system=dinit # dinitctl enable ly @@ -170,8 +192,9 @@ In addition to the steps above, you will also have to keep a TTY free within `/e To do that, change `ACTIVE_CONSOLES` so that the tty that ly should use in `/etc/ly/config.ini` is free. ### Updating + You can also install Ly without overrding the current configuration file. That's called -*updating*. To update, simply run: +_updating_. To update, simply run: ``` # zig build installnoconf @@ -180,95 +203,109 @@ You can also install Ly without overrding the current configuration file. That's You can, of course, still select the init system of your choice when using this command. ## Arch Linux Installation + You can install ly from the [`[extra]` repos](https://archlinux.org/packages/extra/x86_64/ly/): + ``` # pacman -S ly ``` ## Gentoo Installation + You can install ly from the GURU repository: Note: If the package is masked, you may need to unmask it using ~amd64 keyword: + ```bash # echo 'x11-misc/ly ~amd64' >> /etc/portage/package.accept_keywords ``` 1. Enable the GURU repository: + ```bash # eselect repository enable guru ``` 2. Sync the GURU repository: + ```bash # emaint sync -r guru ``` 3. Install ly from source: + ```bash # emerge --ask x11-misc/ly ``` ## Configuration + You can find all the configuration in `/etc/ly/config.ini`. The file is commented, and includes the default values. ## Controls + Use the up and down arrow keys to change the current field, and the left and right arrow keys to change the target desktop environment while on the desktop field (above the login field). ## .xinitrc + If your .xinitrc 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. On Arch Linux, the example .xinitrc (/etc/X11/xinit/xinitrc) starts like this: + ``` #!/bin/sh ``` ## 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 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 + +- 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 +- awesome +- bspwm +- budgie +- cinnamon +- dwm +- enlightenment +- gnome +- kde +- leftwm +- lxde +- mate +- maxx +- pantheon +- qwm +- spectrwm +- windowmaker +- xfce +- xmonad ## Additional Information + The name "Ly" is a tribute to the fairy from the game Rayman. Ly was tested by oxodao, who is some seriously awesome dude. From de11ac8972edcf519ee83de144b2a218046b3fa6 Mon Sep 17 00:00:00 2001 From: Ireozar Date: Tue, 24 Jun 2025 12:34:41 +0200 Subject: [PATCH 214/530] improved/added German localization --- res/lang/de.ini | 96 ++++++++++++++++++++++++------------------------- 1 file changed, 48 insertions(+), 48 deletions(-) diff --git a/res/lang/de.ini b/res/lang/de.ini index af8090d..0859297 100644 --- a/res/lang/de.ini +++ b/res/lang/de.ini @@ -1,63 +1,63 @@ - - - +authenticating = authentifizierend... +brightness_down = Helligkeit- +brightness_up = Helligkeit+ capslock = Feststelltaste err_alloc = Speicherzuweisung fehlgeschlagen -err_bounds = Listenindex ist außerhalb des Bereichs - -err_chdir = Fehler beim oeffnen des home-ordners - +err_bounds = Index außerhalb des Bereichs +err_brightness_change = Helligkeitsänderung fehlgeschlagen +err_chdir = Fehler beim Oeffnen des Home-Ordners +err_config = Fehler beim Verarbeiten der Konfigurationsdatei err_console_dev = Zugriff auf die Konsole fehlgeschlagen -err_dgn_oob = Protokoll Nachricht -err_domain = Unzulaessige domain - - -err_hostname = Holen des Hostnames fehlgeschlagen -err_mlock = Abschließen des Passwortspeichers fehlgeschlagen -err_null = Null Zeiger - -err_pam = pam Transaktion fehlgeschlagen -err_pam_abort = pam Transaktion abgebrochen +err_dgn_oob = Diagnose-Nachricht +err_domain = Ungültige Domain +err_empty_password = Leeres Passwort nicht zugelassen +err_envlist = Fehler beim Abrufen der Umgebungs-Variablen +err_hostname = Abrufen des Hostnames fehlgeschlagen +err_mlock = Sperren des Passwortspeichers fehlgeschlagen +err_null = Null Pointer +err_numlock = Numlock konnte nicht aktiviert werden +err_pam = PAM-Transaktion fehlgeschlagen +err_pam_abort = PAM-Transaktion abgebrochen err_pam_acct_expired = Benutzerkonto abgelaufen -err_pam_auth = Authentifizierungs Fehler -err_pam_authinfo_unavail = holen der Benutzerinformationen fehlgeschlagen -err_pam_authok_reqd = Schluessel abgelaufen +err_pam_auth = Authentifizierungsfehler +err_pam_authinfo_unavail = Abrufen der Benutzerinformationen fehlgeschlagen +err_pam_authok_reqd = Passwort abgelaufen err_pam_buf = Speicherpufferfehler -err_pam_cred_err = Fehler beim setzen der Anmeldedaten +err_pam_cred_err = Fehler beim Setzen der Anmeldedaten err_pam_cred_expired = Anmeldedaten abgelaufen err_pam_cred_insufficient = Anmeldedaten unzureichend -err_pam_cred_unavail = Fehler beim holen der Anmeldedaten -err_pam_maxtries = Maximale Versuche erreicht -err_pam_perm_denied = Zugriff Verweigert +err_pam_cred_unavail = Fehler beim Abrufen der Anmeldedaten +err_pam_maxtries = Maximale Versuchsanzahl erreicht +err_pam_perm_denied = Zugriff verweigert err_pam_session = Sitzungsfehler err_pam_sys = Systemfehler err_pam_user_unknown = Unbekannter Nutzer -err_path = Fehler beim setzen des Pfades -err_perm_dir = Fehler beim wechseln des Ordners -err_perm_group = Fehler beim heruntersetzen der Gruppen Berechtigungen -err_perm_user = Fehler beim heruntersetzen der Nutzer Berechtigungen -err_pwnam = Holen der Benutzerinformationen fehlgeschlagen - - -err_user_gid = Fehler beim setzen der Gruppen Id des Nutzers -err_user_init = Initialisierung des Nutzers fehlgeschlagen -err_user_uid = Setzen der Benutzer Id fehlgeschlagen - - -err_xsessions_dir = Fehler beim finden des Sitzungsordners -err_xsessions_open = Fehler beim öffnen des Sitzungsordners - -login = Anmelden -logout = Abgemeldet - - -numlock = Numtaste - +err_path = Fehler beim Setzen des Pfades +err_perm_dir = Ordnerwechsel fehlgeschlagen +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_tty_ctrl = Fehler bei der TTY-Übergabe +err_user_gid = Fehler beim Setzen der Gruppen-ID +err_user_init = Nutzer-Initialisierung fehlgeschlagen +err_user_uid = Setzen der Benutzer-ID fehlgeschlagen +err_xauth = Xauth-Befehl fehlgeschlagen +err_xcb_conn = xcb-Verbindung fehlgeschlagen +err_xsessions_dir = Fehler beim Finden des Sitzungsordners +err_xsessions_open = Fehler beim Öffnen des Sitzungsordners +insert = Einfügen +login = Nutzer +logout = Abmelden +no_x11_support = X11-Support bei Kompilierung deaktiviert +normal = Normal +numlock = Numlock +other = Andere password = Passwort restart = Neustarten -shell = shell +shell = Shell shutdown = Herunterfahren - +sleep = Sleep wayland = wayland - +x11 = X11 xinitrc = xinitrc From 7182d91b37f9b865dae2855f1476968dbbc7e237 Mon Sep 17 00:00:00 2001 From: Ireozar Date: Tue, 24 Jun 2025 13:25:45 +0200 Subject: [PATCH 215/530] removed some special characters --- res/lang/de.ini | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/res/lang/de.ini b/res/lang/de.ini index 0859297..47c44aa 100644 --- a/res/lang/de.ini +++ b/res/lang/de.ini @@ -1,15 +1,15 @@ -authenticating = authentifizierend... +authenticating = authentifizieren... brightness_down = Helligkeit- brightness_up = Helligkeit+ capslock = Feststelltaste err_alloc = Speicherzuweisung fehlgeschlagen -err_bounds = Index außerhalb des Bereichs +err_bounds = Index ausserhalb des Bereichs err_brightness_change = Helligkeitsänderung fehlgeschlagen err_chdir = Fehler beim Oeffnen des Home-Ordners err_config = Fehler beim Verarbeiten der Konfigurationsdatei err_console_dev = Zugriff auf die Konsole fehlgeschlagen err_dgn_oob = Diagnose-Nachricht -err_domain = Ungültige Domain +err_domain = Ungueltige Domain err_empty_password = Leeres Passwort nicht zugelassen err_envlist = Fehler beim Abrufen der Umgebungs-Variablen err_hostname = Abrufen des Hostnames fehlgeschlagen @@ -38,14 +38,14 @@ 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_tty_ctrl = Fehler bei der TTY-Übergabe +err_tty_ctrl = Fehler bei der TTY-Uebergabe err_user_gid = Fehler beim Setzen der Gruppen-ID err_user_init = Nutzer-Initialisierung fehlgeschlagen err_user_uid = Setzen der Benutzer-ID fehlgeschlagen err_xauth = Xauth-Befehl fehlgeschlagen err_xcb_conn = xcb-Verbindung fehlgeschlagen err_xsessions_dir = Fehler beim Finden des Sitzungsordners -err_xsessions_open = Fehler beim Öffnen des Sitzungsordners +err_xsessions_open = Fehler beim Oeffnen des Sitzungsordners insert = Einfügen login = Nutzer logout = Abmelden From 7b81336761f96a26070aa60e5ff06b87aad31e51 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Lucas?= Date: Fri, 4 Jul 2025 08:32:49 -0300 Subject: [PATCH 216/530] refactor: Use zig fetch for termbox2 dependency Replaced the local `termbox2.h` header file with a proper dependency managed by the Zig package manager. This improves maintainability and makes it easier to track and update the library in the future. The previous `termbox2.h` contained a custom `tb_get_cell` function that is not present in the upstream repository. This function has been re-implemented in Zig (`src/tui/termbox_extras.zig`) to maintain compatibility, especially for the failed-login "cascade" animation. This change also involved updating `build.zig` and `build.zig.zon` to use the new dependency. --- build.zig | 8 +- build.zig.zon | 4 + include/termbox2.h | 4321 ------------------------------------ src/tui/TerminalBuffer.zig | 5 +- src/tui/termbox_extras.zig | 26 + 5 files changed, 39 insertions(+), 4325 deletions(-) delete mode 100644 include/termbox2.h create mode 100644 src/tui/termbox_extras.zig diff --git a/build.zig b/build.zig index 80de3e8..36a78c6 100644 --- a/build.zig +++ b/build.zig @@ -68,13 +68,17 @@ 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.addIncludePath(b.path("include")); + 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 = b.path("include/termbox2.h"), + .root_source_file = termbox_dep.path("termbox2.h"), .target = target, .optimize = optimize, }); diff --git a/build.zig.zon b/build.zig.zon index 08d7e95..5d8e4e4 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -12,6 +12,10 @@ .url = "https://github.com/Kawaii-Ash/zigini/archive/2ed3d417f17fab5b0ee8cad8a63c6d62d7ac1042.tar.gz", .hash = "zigini-0.3.1-BSkB7XJGAAB2E-sKyzhTaQCBlYBL8yqzE4E_jmSY99sC", }, + .termbox2 = .{ + .url = "git+https://github.com/termbox/termbox2#8ee9dc17e1ca61c630f91db0aa7f81fa29a32040", + .hash = "N-V-__8AAKvjBAAUF2KVdkHsNs7L5EEZYzBnrJTBvj-baBMZ", + }, }, .paths = .{""}, } diff --git a/include/termbox2.h b/include/termbox2.h deleted file mode 100644 index 831b824..0000000 --- a/include/termbox2.h +++ /dev/null @@ -1,4321 +0,0 @@ -/* -MIT License - -Copyright (c) 2010-2020 nsf - 2015-2025 Adam Saponara - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. -*/ - -#ifndef TERMBOX_H_INCL -#define TERMBOX_H_INCL - -#ifndef _XOPEN_SOURCE -#define _XOPEN_SOURCE -#endif - -#ifndef _DEFAULT_SOURCE -#define _DEFAULT_SOURCE -#endif - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#ifdef PATH_MAX -#define TB_PATH_MAX PATH_MAX -#else -#define TB_PATH_MAX 4096 -#endif - -#ifdef __cplusplus -extern "C" { -#endif - -// __ffi_start - -#define TB_VERSION_STR "2.6.0-dev" - -/* The following compile-time options are supported: - * - * TB_OPT_ATTR_W: Integer width of `fg` and `bg` attributes. Valid values - * (assuming system support) are 16, 32, and 64. (See - * `uintattr_t`). 32 or 64 enables output mode - * `TB_OUTPUT_TRUECOLOR`. 64 enables additional style - * attributes. (See `tb_set_output_mode`.) Larger values - * consume more memory in exchange for more features. - * Defaults to 16. - * - * TB_OPT_EGC: If set, enable extended grapheme cluster support - * (`tb_extend_cell`, `tb_set_cell_ex`). Consumes more - * memory. Defaults off. - * - * TB_OPT_PRINTF_BUF: Write buffer size for printf operations. Represents the - * largest string that can be sent in one call to - * `tb_print*` and `tb_send*` functions. Defaults to 4096. - * - * TB_OPT_READ_BUF: Read buffer size for tty reads. Defaults to 64. - * - * TB_OPT_LIBC_WCHAR: If set, use libc's `wcwidth(3)`, `iswprint(3)`, etc - * instead of the built-in Unicode-aware versions. Note, - * libc's are locale-dependent and the caller must - * `setlocale(3)` `LC_CTYPE` to UTF-8. Defaults to built-in. - * - * TB_OPT_TRUECOLOR: Deprecated. Sets TB_OPT_ATTR_W to 32 if not already set. - */ - -#if defined(TB_LIB_OPTS) || 0 // __tb_lib_opts -/* Ensure consistent compile-time options when using as a shared library */ -#undef TB_OPT_ATTR_W -#undef TB_OPT_EGC -#undef TB_OPT_PRINTF_BUF -#undef TB_OPT_READ_BUF -#undef TB_OPT_LIBC_WCHAR -#define TB_OPT_ATTR_W 64 -#define TB_OPT_EGC -#endif - -/* Ensure sane `TB_OPT_ATTR_W` (16, 32, or 64) */ -#if defined TB_OPT_ATTR_W && TB_OPT_ATTR_W == 16 -#elif defined TB_OPT_ATTR_W && TB_OPT_ATTR_W == 32 -#elif defined TB_OPT_ATTR_W && TB_OPT_ATTR_W == 64 -#else -#undef TB_OPT_ATTR_W -#if defined TB_OPT_TRUECOLOR // Deprecated. Back-compat for old flag. -#define TB_OPT_ATTR_W 32 -#else -#define TB_OPT_ATTR_W 16 -#endif -#endif - -/* ASCII key constants (`tb_event.key`) */ -#define TB_KEY_CTRL_TILDE 0x00 -#define TB_KEY_CTRL_2 0x00 // clash with `CTRL_TILDE` -#define TB_KEY_CTRL_A 0x01 -#define TB_KEY_CTRL_B 0x02 -#define TB_KEY_CTRL_C 0x03 -#define TB_KEY_CTRL_D 0x04 -#define TB_KEY_CTRL_E 0x05 -#define TB_KEY_CTRL_F 0x06 -#define TB_KEY_CTRL_G 0x07 -#define TB_KEY_BACKSPACE 0x08 -#define TB_KEY_CTRL_H 0x08 // clash with `CTRL_BACKSPACE` -#define TB_KEY_TAB 0x09 -#define TB_KEY_CTRL_I 0x09 // clash with `TAB` -#define TB_KEY_CTRL_J 0x0a -#define TB_KEY_CTRL_K 0x0b -#define TB_KEY_CTRL_L 0x0c -#define TB_KEY_ENTER 0x0d -#define TB_KEY_CTRL_M 0x0d // clash with `ENTER` -#define TB_KEY_CTRL_N 0x0e -#define TB_KEY_CTRL_O 0x0f -#define TB_KEY_CTRL_P 0x10 -#define TB_KEY_CTRL_Q 0x11 -#define TB_KEY_CTRL_R 0x12 -#define TB_KEY_CTRL_S 0x13 -#define TB_KEY_CTRL_T 0x14 -#define TB_KEY_CTRL_U 0x15 -#define TB_KEY_CTRL_V 0x16 -#define TB_KEY_CTRL_W 0x17 -#define TB_KEY_CTRL_X 0x18 -#define TB_KEY_CTRL_Y 0x19 -#define TB_KEY_CTRL_Z 0x1a -#define TB_KEY_ESC 0x1b -#define TB_KEY_CTRL_LSQ_BRACKET 0x1b // clash with 'ESC' -#define TB_KEY_CTRL_3 0x1b // clash with 'ESC' -#define TB_KEY_CTRL_4 0x1c -#define TB_KEY_CTRL_BACKSLASH 0x1c // clash with 'CTRL_4' -#define TB_KEY_CTRL_5 0x1d -#define TB_KEY_CTRL_RSQ_BRACKET 0x1d // clash with 'CTRL_5' -#define TB_KEY_CTRL_6 0x1e -#define TB_KEY_CTRL_7 0x1f -#define TB_KEY_CTRL_SLASH 0x1f // clash with 'CTRL_7' -#define TB_KEY_CTRL_UNDERSCORE 0x1f // clash with 'CTRL_7' -#define TB_KEY_SPACE 0x20 -#define TB_KEY_BACKSPACE2 0x7f -#define TB_KEY_CTRL_8 0x7f // clash with 'BACKSPACE2' - -#define tb_key_i(i) 0xffff - (i) -/* Terminal-dependent key constants (`tb_event.key`) and terminfo caps */ -/* BEGIN codegen h */ -/* Produced by ./codegen.sh on Tue, 03 Sep 2024 04:17:47 +0000 */ -#define TB_KEY_F1 (0xffff - 0) -#define TB_KEY_F2 (0xffff - 1) -#define TB_KEY_F3 (0xffff - 2) -#define TB_KEY_F4 (0xffff - 3) -#define TB_KEY_F5 (0xffff - 4) -#define TB_KEY_F6 (0xffff - 5) -#define TB_KEY_F7 (0xffff - 6) -#define TB_KEY_F8 (0xffff - 7) -#define TB_KEY_F9 (0xffff - 8) -#define TB_KEY_F10 (0xffff - 9) -#define TB_KEY_F11 (0xffff - 10) -#define TB_KEY_F12 (0xffff - 11) -#define TB_KEY_INSERT (0xffff - 12) -#define TB_KEY_DELETE (0xffff - 13) -#define TB_KEY_HOME (0xffff - 14) -#define TB_KEY_END (0xffff - 15) -#define TB_KEY_PGUP (0xffff - 16) -#define TB_KEY_PGDN (0xffff - 17) -#define TB_KEY_ARROW_UP (0xffff - 18) -#define TB_KEY_ARROW_DOWN (0xffff - 19) -#define TB_KEY_ARROW_LEFT (0xffff - 20) -#define TB_KEY_ARROW_RIGHT (0xffff - 21) -#define TB_KEY_BACK_TAB (0xffff - 22) -#define TB_KEY_MOUSE_LEFT (0xffff - 23) -#define TB_KEY_MOUSE_RIGHT (0xffff - 24) -#define TB_KEY_MOUSE_MIDDLE (0xffff - 25) -#define TB_KEY_MOUSE_RELEASE (0xffff - 26) -#define TB_KEY_MOUSE_WHEEL_UP (0xffff - 27) -#define TB_KEY_MOUSE_WHEEL_DOWN (0xffff - 28) - -#define TB_CAP_F1 0 -#define TB_CAP_F2 1 -#define TB_CAP_F3 2 -#define TB_CAP_F4 3 -#define TB_CAP_F5 4 -#define TB_CAP_F6 5 -#define TB_CAP_F7 6 -#define TB_CAP_F8 7 -#define TB_CAP_F9 8 -#define TB_CAP_F10 9 -#define TB_CAP_F11 10 -#define TB_CAP_F12 11 -#define TB_CAP_INSERT 12 -#define TB_CAP_DELETE 13 -#define TB_CAP_HOME 14 -#define TB_CAP_END 15 -#define TB_CAP_PGUP 16 -#define TB_CAP_PGDN 17 -#define TB_CAP_ARROW_UP 18 -#define TB_CAP_ARROW_DOWN 19 -#define TB_CAP_ARROW_LEFT 20 -#define TB_CAP_ARROW_RIGHT 21 -#define TB_CAP_BACK_TAB 22 -#define TB_CAP__COUNT_KEYS 23 -#define TB_CAP_ENTER_CA 23 -#define TB_CAP_EXIT_CA 24 -#define TB_CAP_SHOW_CURSOR 25 -#define TB_CAP_HIDE_CURSOR 26 -#define TB_CAP_CLEAR_SCREEN 27 -#define TB_CAP_SGR0 28 -#define TB_CAP_UNDERLINE 29 -#define TB_CAP_BOLD 30 -#define TB_CAP_BLINK 31 -#define TB_CAP_ITALIC 32 -#define TB_CAP_REVERSE 33 -#define TB_CAP_ENTER_KEYPAD 34 -#define TB_CAP_EXIT_KEYPAD 35 -#define TB_CAP_DIM 36 -#define TB_CAP_INVISIBLE 37 -#define TB_CAP__COUNT 38 -/* END codegen h */ - -/* Some hard-coded caps */ -#define TB_HARDCAP_ENTER_MOUSE "\x1b[?1000h\x1b[?1002h\x1b[?1015h\x1b[?1006h" -#define TB_HARDCAP_EXIT_MOUSE "\x1b[?1006l\x1b[?1015l\x1b[?1002l\x1b[?1000l" -#define TB_HARDCAP_STRIKEOUT "\x1b[9m" -#define TB_HARDCAP_UNDERLINE_2 "\x1b[21m" -#define TB_HARDCAP_OVERLINE "\x1b[53m" - -/* Colors (numeric) and attributes (bitwise) (`tb_cell.fg`, `tb_cell.bg`) */ -#define TB_DEFAULT 0x0000 -#define TB_BLACK 0x0001 -#define TB_RED 0x0002 -#define TB_GREEN 0x0003 -#define TB_YELLOW 0x0004 -#define TB_BLUE 0x0005 -#define TB_MAGENTA 0x0006 -#define TB_CYAN 0x0007 -#define TB_WHITE 0x0008 - -#if TB_OPT_ATTR_W == 16 -#define TB_BOLD 0x0100 -#define TB_UNDERLINE 0x0200 -#define TB_REVERSE 0x0400 -#define TB_ITALIC 0x0800 -#define TB_BLINK 0x1000 -#define TB_HI_BLACK 0x2000 -#define TB_BRIGHT 0x4000 -#define TB_DIM 0x8000 -#define TB_256_BLACK TB_HI_BLACK // `TB_256_BLACK` is deprecated -#else -// `TB_OPT_ATTR_W` is 32 or 64 -#define TB_BOLD 0x01000000 -#define TB_UNDERLINE 0x02000000 -#define TB_REVERSE 0x04000000 -#define TB_ITALIC 0x08000000 -#define TB_BLINK 0x10000000 -#define TB_HI_BLACK 0x20000000 -#define TB_BRIGHT 0x40000000 -#define TB_DIM 0x80000000 -#define TB_TRUECOLOR_BOLD TB_BOLD // `TB_TRUECOLOR_*` is deprecated -#define TB_TRUECOLOR_UNDERLINE TB_UNDERLINE -#define TB_TRUECOLOR_REVERSE TB_REVERSE -#define TB_TRUECOLOR_ITALIC TB_ITALIC -#define TB_TRUECOLOR_BLINK TB_BLINK -#define TB_TRUECOLOR_BLACK TB_HI_BLACK -#endif - -#if TB_OPT_ATTR_W == 64 -#define TB_STRIKEOUT 0x0000000100000000 -#define TB_UNDERLINE_2 0x0000000200000000 -#define TB_OVERLINE 0x0000000400000000 -#define TB_INVISIBLE 0x0000000800000000 -#endif - -/* Event types (`tb_event.type`) */ -#define TB_EVENT_KEY 1 -#define TB_EVENT_RESIZE 2 -#define TB_EVENT_MOUSE 3 - -/* Key modifiers (bitwise) (`tb_event.mod`) */ -#define TB_MOD_ALT 1 -#define TB_MOD_CTRL 2 -#define TB_MOD_SHIFT 4 -#define TB_MOD_MOTION 8 - -/* Input modes (bitwise) (`tb_set_input_mode`) */ -#define TB_INPUT_CURRENT 0 -#define TB_INPUT_ESC 1 -#define TB_INPUT_ALT 2 -#define TB_INPUT_MOUSE 4 - -/* Output modes (`tb_set_output_mode`) */ -#define TB_OUTPUT_CURRENT 0 -#define TB_OUTPUT_NORMAL 1 -#define TB_OUTPUT_256 2 -#define TB_OUTPUT_216 3 -#define TB_OUTPUT_GRAYSCALE 4 -#if TB_OPT_ATTR_W >= 32 -#define TB_OUTPUT_TRUECOLOR 5 -#endif - -/* Common function return values unless otherwise noted. - * - * Library behavior is undefined after receiving `TB_ERR_MEM`. Callers may - * attempt reinitializing by freeing memory, invoking `tb_shutdown`, then - * `tb_init`. - */ -#define TB_OK 0 -#define TB_ERR -1 -#define TB_ERR_NEED_MORE -2 -#define TB_ERR_INIT_ALREADY -3 -#define TB_ERR_INIT_OPEN -4 -#define TB_ERR_MEM -5 -#define TB_ERR_NO_EVENT -6 -#define TB_ERR_NO_TERM -7 -#define TB_ERR_NOT_INIT -8 -#define TB_ERR_OUT_OF_BOUNDS -9 -#define TB_ERR_READ -10 -#define TB_ERR_RESIZE_IOCTL -11 -#define TB_ERR_RESIZE_PIPE -12 -#define TB_ERR_RESIZE_SIGACTION -13 -#define TB_ERR_POLL -14 -#define TB_ERR_TCGETATTR -15 -#define TB_ERR_TCSETATTR -16 -#define TB_ERR_UNSUPPORTED_TERM -17 -#define TB_ERR_RESIZE_WRITE -18 -#define TB_ERR_RESIZE_POLL -19 -#define TB_ERR_RESIZE_READ -20 -#define TB_ERR_RESIZE_SSCANF -21 -#define TB_ERR_CAP_COLLISION -22 - -#define TB_ERR_SELECT TB_ERR_POLL -#define TB_ERR_RESIZE_SELECT TB_ERR_RESIZE_POLL - -/* Deprecated. Function types to be used with `tb_set_func`. */ -#define TB_FUNC_EXTRACT_PRE 0 -#define TB_FUNC_EXTRACT_POST 1 - -/* Define this to set the size of the buffer used in `tb_printf` - * and `tb_sendf` - */ -#ifndef TB_OPT_PRINTF_BUF -#define TB_OPT_PRINTF_BUF 4096 -#endif - -/* Define this to set the size of the read buffer used when reading - * from the tty - */ -#ifndef TB_OPT_READ_BUF -#define TB_OPT_READ_BUF 64 -#endif - -/* Define this for limited back compat with termbox v1 */ -#ifdef TB_OPT_V1_COMPAT -#define tb_change_cell tb_set_cell -#define tb_put_cell(x, y, c) tb_set_cell((x), (y), (c)->ch, (c)->fg, (c)->bg) -#define tb_set_clear_attributes tb_set_clear_attrs -#define tb_select_input_mode tb_set_input_mode -#define tb_select_output_mode tb_set_output_mode -#endif - -/* Define these to swap in a different allocator */ -#ifndef tb_malloc -#define tb_malloc malloc -#define tb_realloc realloc -#define tb_free free -#endif - -#if TB_OPT_ATTR_W == 64 -typedef uint64_t uintattr_t; -#elif TB_OPT_ATTR_W == 32 -typedef uint32_t uintattr_t; -#else // 16 -typedef uint16_t uintattr_t; -#endif - -/* A cell in a 2d grid representing the terminal screen. - * - * The terminal screen is represented as 2d array of cells. The structure is - * optimized for dealing with single-width (`wcwidth==1`) Unicode codepoints, - * however some support for grapheme clusters (e.g., combining diacritical - * marks) and wide codepoints (e.g., Hiragana) is provided through `ech`, - * `nech`, and `cech` via `tb_set_cell_ex`. `ech` is only valid when `nech>0`, - * otherwise `ch` is used. - * - * For non-single-width codepoints, given `N=wcwidth(ch)/wcswidth(ech)`: - * - * when `N==0`: termbox forces a single-width cell. Callers should avoid this - * if aiming to render text accurately. Callers may use - * `tb_set_cell_ex` or `tb_print*` to render `N==0` combining - * characters. - * - * when `N>1`: termbox zeroes out the following `N-1` cells and skips sending - * them to the tty. So, e.g., if the caller sets `x=0,y=0` to an - * `N==2` codepoint, the caller's next set should be at `x=2,y=0`. - * Anything set at `x=1,y=0` will be ignored. If there are not - * enough columns remaining on the line to render `N` width, spaces - * are sent instead. - * - * See `tb_present` for implementation. - */ -struct tb_cell { - uint32_t ch; // a Unicode codepoint - uintattr_t fg; // bitwise foreground attributes - uintattr_t bg; // bitwise background attributes -#ifdef TB_OPT_EGC - uint32_t *ech; // a grapheme cluster of Unicode codepoints, 0-terminated - size_t nech; // num elements in ech, 0 means use ch instead of ech - size_t cech; // num elements allocated for ech -#endif -}; - -/* An incoming event from the tty. - * - * Given the event type, the following fields are relevant: - * - * when `TB_EVENT_KEY`: `key` xor `ch` (one will be zero) and `mod`. Note - * there is overlap between `TB_MOD_CTRL` and - * `TB_KEY_CTRL_*`. `TB_MOD_CTRL` and `TB_MOD_SHIFT` are - * only set as modifiers to `TB_KEY_ARROW_*`. - * - * when `TB_EVENT_RESIZE`: `w` and `h` - * - * when `TB_EVENT_MOUSE`: `key` (`TB_KEY_MOUSE_*`), `x`, and `y` - */ -struct tb_event { - uint8_t type; // one of `TB_EVENT_*` constants - uint8_t mod; // bitwise `TB_MOD_*` constants - uint16_t key; // one of `TB_KEY_*` constants - uint32_t ch; // a Unicode codepoint - int32_t w; // resize width - int32_t h; // resize height - int32_t x; // mouse x - int32_t y; // mouse y -}; - -/* Initialize the termbox library. This function should be called before any - * other functions. `tb_init` is equivalent to `tb_init_file("/dev/tty")`. After - * successful initialization, the library must be finalized using `tb_shutdown`. - */ -int tb_init(void); -int tb_init_file(const char *path); -int tb_init_fd(int ttyfd); -int tb_init_rwfd(int rfd, int wfd); -int tb_shutdown(void); - -/* Return the size of the internal back buffer (which is the same as terminal's - * window size in rows and columns). The internal buffer can be resized after - * `tb_clear` or `tb_present` calls. Both dimensions have an unspecified - * negative value when called before `tb_init` or after `tb_shutdown`. - */ -int tb_width(void); -int tb_height(void); - -/* Clear the internal back buffer using `TB_DEFAULT` or the attributes set by - * `tb_set_clear_attrs`. - */ -int tb_clear(void); -int tb_set_clear_attrs(uintattr_t fg, uintattr_t bg); - -/* Synchronize the internal back buffer with the terminal by writing to tty. */ -int tb_present(void); - -/* Clear the internal front buffer effectively forcing a complete re-render of - * the back buffer to the tty. It is not necessary to call this under normal - * circumstances. - */ -int tb_invalidate(void); - -/* Set the position of the cursor. Upper-left cell is (0, 0). */ -int tb_set_cursor(int cx, int cy); -int tb_hide_cursor(void); - -/* Set cell contents in the internal back buffer at the specified position. - * - * Use `tb_set_cell_ex` for rendering grapheme clusters (e.g., combining - * diacritical marks). - * - * Calling `tb_set_cell(x, y, ch, fg, bg)` is equivalent to - * `tb_set_cell_ex(x, y, &ch, 1, fg, bg)`. - * - * `tb_extend_cell` is a shortcut for appending 1 codepoint to `tb_cell.ech`. - * - * Non-printable (`iswprint(3)`) codepoints are replaced with `U+FFFD` at render - * time. - */ -int tb_set_cell(int x, int y, uint32_t ch, uintattr_t fg, uintattr_t bg); -int tb_set_cell_ex(int x, int y, uint32_t *ch, size_t nch, uintattr_t fg, - uintattr_t bg); -int tb_extend_cell(int x, int y, uint32_t ch); - -/* Get cell at specified position. - * - * If position is valid, function returns TB_OK and cell contents are copied to - * `cell`. Note if `nech>0`, then `ech` will be a pointer to memory which may - * be invalid or freed after subsequent library calls. Callers must copy this - * memory if they need to persist it for some reason. Modifying memory at `ech` - * results in undefined behavior. - * - * If `back` is non-zero, return cells from the internal back buffer. Otherwise, - * return cells from the front buffer. Note the front buffer is updated on each - * call to tb_present(), whereas the back buffer is updated immediately by - * tb_set_cell() and other functions that mutate cell contents. - */ -int tb_get_cell(int x, int y, int back, struct tb_cell *cell); - -/* Set the input mode. Termbox has two input modes: - * - * 1. `TB_INPUT_ESC` - * When escape (`\x1b`) is in the buffer and there's no match for an escape - * sequence, a key event for `TB_KEY_ESC` is returned. - * - * 2. `TB_INPUT_ALT` - * When escape (`\x1b`) is in the buffer and there's no match for an escape - * sequence, the next keyboard event is returned with a `TB_MOD_ALT` - * modifier. - * - * You can also apply `TB_INPUT_MOUSE` via bitwise OR operation to either of the - * modes (e.g., `TB_INPUT_ESC | TB_INPUT_MOUSE`) to receive `TB_EVENT_MOUSE` - * events. If none of the main two modes were set, but the mouse mode was, - * `TB_INPUT_ESC` is used. If for some reason you've decided to use - * `TB_INPUT_ESC | TB_INPUT_ALT`, it will behave as if only `TB_INPUT_ESC` was - * selected. - * - * If mode is `TB_INPUT_CURRENT`, return the current input mode. - * - * The default input mode is `TB_INPUT_ESC`. - */ -int tb_set_input_mode(int mode); - -/* Set the output mode. Termbox has multiple output modes: - * - * 1. `TB_OUTPUT_NORMAL` => [0..8] - * - * This mode provides 8 different colors: - * `TB_BLACK`, `TB_RED`, `TB_GREEN`, `TB_YELLOW`, - * `TB_BLUE`, `TB_MAGENTA`, `TB_CYAN`, `TB_WHITE` - * - * Plus `TB_DEFAULT` which skips sending a color code (i.e., uses the - * terminal's default color). - * - * Colors (including `TB_DEFAULT`) may be bitwise OR'd with attributes: - * `TB_BOLD`, `TB_UNDERLINE`, `TB_REVERSE`, `TB_ITALIC`, `TB_BLINK`, - * `TB_BRIGHT`, `TB_DIM` - * - * The following style attributes are also available if compiled with - * `TB_OPT_ATTR_W` set to 64: - * `TB_STRIKEOUT`, `TB_UNDERLINE_2`, `TB_OVERLINE`, `TB_INVISIBLE` - * - * As in all modes, the value 0 is interpreted as `TB_DEFAULT` for - * convenience. - * - * Some notes: `TB_REVERSE` and `TB_BRIGHT` can be applied as either `fg` or - * `bg` attributes for the same effect. The rest of the attributes apply to - * `fg` only and are ignored as `bg` attributes. - * - * Example usage: `tb_set_cell(x, y, '@', TB_BLACK | TB_BOLD, TB_RED)` - * - * 2. `TB_OUTPUT_256` => [0..255] + `TB_HI_BLACK` - * - * In this mode you get 256 distinct colors (plus default): - * 0x00 (1): `TB_DEFAULT` - * `TB_HI_BLACK` (1): `TB_BLACK` in `TB_OUTPUT_NORMAL` - * 0x01..0x07 (7): the next 7 colors as in `TB_OUTPUT_NORMAL` - * 0x08..0x0f (8): bright versions of the above - * 0x10..0xe7 (216): 216 different colors - * 0xe8..0xff (24): 24 different shades of gray - * - * All `TB_*` style attributes except `TB_BRIGHT` may be bitwise OR'd as in - * `TB_OUTPUT_NORMAL`. - * - * Note `TB_HI_BLACK` must be used for black, as 0x00 represents default. - * - * 3. `TB_OUTPUT_216` => [0..216] - * - * This mode supports the 216-color range of `TB_OUTPUT_256` only, but you - * don't need to provide an offset: - * 0x00 (1): `TB_DEFAULT` - * 0x01..0xd8 (216): 216 different colors - * - * 4. `TB_OUTPUT_GRAYSCALE` => [0..24] - * - * This mode supports the 24-color range of `TB_OUTPUT_256` only, but you - * don't need to provide an offset: - * 0x00 (1): `TB_DEFAULT` - * 0x01..0x18 (24): 24 different shades of gray - * - * 5. `TB_OUTPUT_TRUECOLOR` => [0x000000..0xffffff] + `TB_HI_BLACK` - * - * This mode provides 24-bit color on supported terminals. The format is - * 0xRRGGBB. - * - * All `TB_*` style attributes except `TB_BRIGHT` may be bitwise OR'd as in - * `TB_OUTPUT_NORMAL`. - * - * Note `TB_HI_BLACK` must be used for black, as 0x000000 represents default. - * - * To use the terminal default color (i.e., to not send an escape code), pass - * `TB_DEFAULT`. For convenience, the value 0 is interpreted as `TB_DEFAULT` in - * all modes. - * - * Note, cell attributes persist after switching output modes. Any translation - * between, for example, `TB_OUTPUT_NORMAL`'s `TB_RED` and - * `TB_OUTPUT_TRUECOLOR`'s 0xff0000 must be performed by the caller. Also note - * that cells previously rendered in one mode may persist unchanged until the - * front buffer is cleared (such as after a resize event) at which point it will - * be re-interpreted and flushed according to the current mode. Callers may - * invoke `tb_invalidate` if it is desirable to immediately re-interpret and - * flush the entire screen according to the current mode. - * - * Note, not all terminals support all output modes, especially beyond - * `TB_OUTPUT_NORMAL`. There is also no very reliable way to determine color - * support dynamically. If portability is desired, callers are recommended to - * use `TB_OUTPUT_NORMAL` or make output mode end-user configurable. The same - * advice applies to style attributes. - * - * If mode is `TB_OUTPUT_CURRENT`, return the current output mode. - * - * The default output mode is `TB_OUTPUT_NORMAL`. - */ -int tb_set_output_mode(int mode); - -/* Wait for an event up to `timeout_ms` milliseconds and populate `event` with - * it. If no event is available within the timeout period, `TB_ERR_NO_EVENT` - * is returned. On a resize event, the underlying `select(2)` call may be - * interrupted, yielding a return code of `TB_ERR_POLL`. In this case, you may - * check `errno` via `tb_last_errno`. If it's `EINTR`, you may elect to ignore - * that and call `tb_peek_event` again. - */ -int tb_peek_event(struct tb_event *event, int timeout_ms); - -/* Same as `tb_peek_event` except no timeout. */ -int tb_poll_event(struct tb_event *event); - -/* Internal termbox fds that can be used with `poll(2)`, `select(2)`, etc. - * externally. Callers must invoke `tb_poll_event` or `tb_peek_event` if - * fds become readable. - */ -int tb_get_fds(int *ttyfd, int *resizefd); - -/* Print and printf functions. Specify param `out_w` to determine width of - * printed string. Strings are interpreted as UTF-8. - * - * Non-printable characters (`iswprint(3)`) and truncated UTF-8 byte sequences - * are replaced with U+FFFD. - * - * Newlines (`\n`) are supported with the caveat that `out_w` will return the - * width of the string as if it were on a single line. - * - * If the starting coordinate is out of bounds, `TB_ERR_OUT_OF_BOUNDS` is - * returned. If the starting coordinate is in bounds, but goes out of bounds, - * then the out-of-bounds portions of the string are ignored. - * - * For finer control, use `tb_set_cell`. - */ -int tb_print(int x, int y, uintattr_t fg, uintattr_t bg, const char *str); -int tb_printf(int x, int y, uintattr_t fg, uintattr_t bg, const char *fmt, ...); -int tb_print_ex(int x, int y, uintattr_t fg, uintattr_t bg, size_t *out_w, - const char *str); -int tb_printf_ex(int x, int y, uintattr_t fg, uintattr_t bg, size_t *out_w, - const char *fmt, ...); - -/* Send raw bytes to terminal. */ -int tb_send(const char *buf, size_t nbuf); -int tb_sendf(const char *fmt, ...); - -/* Deprecated. Set custom callbacks. `fn_type` is one of `TB_FUNC_*` constants, - * `fn` is a compatible function pointer, or NULL to clear. - * - * `TB_FUNC_EXTRACT_PRE`: - * If specified, invoke this function BEFORE termbox tries to extract any - * escape sequences from the input buffer. - * - * `TB_FUNC_EXTRACT_POST`: - * If specified, invoke this function AFTER termbox tries (and fails) to - * extract any escape sequences from the input buffer. - */ -int tb_set_func(int fn_type, int (*fn)(struct tb_event *, size_t *)); - -/* Return byte length of codepoint given first byte of UTF-8 sequence (1-6). */ -int tb_utf8_char_length(char c); - -/* Convert UTF-8 null-terminated byte sequence to UTF-32 codepoint. - * - * If `c` is an empty C string, return 0. `out` is left unchanged. - * - * If a null byte is encountered in the middle of the codepoint, return a - * negative number indicating how many bytes were processed. `out` is left - * unchanged. - * - * Otherwise, return byte length of codepoint (1-6). - */ -int tb_utf8_char_to_unicode(uint32_t *out, const char *c); - -/* Convert UTF-32 codepoint to UTF-8 null-terminated byte sequence. - * - * `out` must be char[7] or greater. Return byte length of codepoint (1-6). - */ -int tb_utf8_unicode_to_char(char *out, uint32_t c); - -/* Library utility functions */ -int tb_last_errno(void); -const char *tb_strerror(int err); -struct tb_cell *tb_cell_buffer(void); // Deprecated -int tb_has_truecolor(void); -int tb_has_egc(void); -int tb_attr_width(void); -const char *tb_version(void); -int tb_iswprint(uint32_t ch); -int tb_wcwidth(uint32_t ch); - -/* Deprecation notice! - * - * The following will be removed in version 3.x (ABI version 3): - * - * TB_256_BLACK (use TB_HI_BLACK) - * TB_OPT_TRUECOLOR (use TB_OPT_ATTR_W) - * TB_TRUECOLOR_BOLD (use TB_BOLD) - * TB_TRUECOLOR_UNDERLINE (use TB_UNDERLINE) - * TB_TRUECOLOR_REVERSE (use TB_REVERSE) - * TB_TRUECOLOR_ITALIC (use TB_ITALIC) - * TB_TRUECOLOR_BLINK (use TB_BLINK) - * TB_TRUECOLOR_BLACK (use TB_HI_BLACK) - * tb_cell_buffer - * tb_set_func - * TB_FUNC_EXTRACT_PRE - * TB_FUNC_EXTRACT_POST - */ - -#ifdef __cplusplus -} -#endif - -#endif // TERMBOX_H_INCL - -#ifdef TB_IMPL - -#define if_err_return(rv, expr) \ - if (((rv) = (expr)) != TB_OK) return (rv) -#define if_err_break(rv, expr) \ - if (((rv) = (expr)) != TB_OK) break -#define if_ok_return(rv, expr) \ - if (((rv) = (expr)) == TB_OK) return (rv) -#define if_ok_or_need_more_return(rv, expr) \ - if (((rv) = (expr)) == TB_OK || (rv) == TB_ERR_NEED_MORE) return (rv) - -#define send_literal(rv, a) \ - if_err_return((rv), bytebuf_nputs(&global.out, (a), sizeof(a) - 1)) - -#define send_num(rv, nbuf, n) \ - if_err_return((rv), \ - bytebuf_nputs(&global.out, (nbuf), convert_num((n), (nbuf)))) - -#define snprintf_or_return(rv, str, sz, fmt, ...) \ - do { \ - (rv) = snprintf((str), (sz), (fmt), __VA_ARGS__); \ - if ((rv) < 0 || (rv) >= (int)(sz)) return TB_ERR; \ - } while (0) - -#define if_not_init_return() \ - if (!global.initialized) return TB_ERR_NOT_INIT - -struct bytebuf_t { - char *buf; - size_t len; - size_t cap; -}; - -struct cellbuf_t { - int width; - int height; - struct tb_cell *cells; -}; - -struct cap_trie_t { - char c; - struct cap_trie_t *children; - size_t nchildren; - int is_leaf; - uint16_t key; - uint8_t mod; -}; - -struct tb_global_t { - int ttyfd; - int rfd; - int wfd; - int ttyfd_open; - int resize_pipefd[2]; - int width; - int height; - int cursor_x; - int cursor_y; - int last_x; - int last_y; - uintattr_t fg; - uintattr_t bg; - uintattr_t last_fg; - uintattr_t last_bg; - int input_mode; - int output_mode; - char *terminfo; - size_t nterminfo; - const char *caps[TB_CAP__COUNT]; - struct cap_trie_t cap_trie; - struct bytebuf_t in; - struct bytebuf_t out; - struct cellbuf_t back; - struct cellbuf_t front; - struct termios orig_tios; - int has_orig_tios; - int last_errno; - int initialized; - int (*fn_extract_esc_pre)(struct tb_event *, size_t *); - int (*fn_extract_esc_post)(struct tb_event *, size_t *); - char errbuf[1024]; -}; - -static struct tb_global_t global = {0}; - -/* BEGIN codegen c */ -/* Produced by ./codegen.sh on Tue, 03 Sep 2024 04:17:48 +0000 */ - -static const int16_t terminfo_cap_indexes[] = { - 66, // kf1 (TB_CAP_F1) - 68, // kf2 (TB_CAP_F2) - 69, // kf3 (TB_CAP_F3) - 70, // kf4 (TB_CAP_F4) - 71, // kf5 (TB_CAP_F5) - 72, // kf6 (TB_CAP_F6) - 73, // kf7 (TB_CAP_F7) - 74, // kf8 (TB_CAP_F8) - 75, // kf9 (TB_CAP_F9) - 67, // kf10 (TB_CAP_F10) - 216, // kf11 (TB_CAP_F11) - 217, // kf12 (TB_CAP_F12) - 77, // kich1 (TB_CAP_INSERT) - 59, // kdch1 (TB_CAP_DELETE) - 76, // khome (TB_CAP_HOME) - 164, // kend (TB_CAP_END) - 82, // kpp (TB_CAP_PGUP) - 81, // knp (TB_CAP_PGDN) - 87, // kcuu1 (TB_CAP_ARROW_UP) - 61, // kcud1 (TB_CAP_ARROW_DOWN) - 79, // kcub1 (TB_CAP_ARROW_LEFT) - 83, // kcuf1 (TB_CAP_ARROW_RIGHT) - 148, // kcbt (TB_CAP_BACK_TAB) - 28, // smcup (TB_CAP_ENTER_CA) - 40, // rmcup (TB_CAP_EXIT_CA) - 16, // cnorm (TB_CAP_SHOW_CURSOR) - 13, // civis (TB_CAP_HIDE_CURSOR) - 5, // clear (TB_CAP_CLEAR_SCREEN) - 39, // sgr0 (TB_CAP_SGR0) - 36, // smul (TB_CAP_UNDERLINE) - 27, // bold (TB_CAP_BOLD) - 26, // blink (TB_CAP_BLINK) - 311, // sitm (TB_CAP_ITALIC) - 34, // rev (TB_CAP_REVERSE) - 89, // smkx (TB_CAP_ENTER_KEYPAD) - 88, // rmkx (TB_CAP_EXIT_KEYPAD) - 30, // dim (TB_CAP_DIM) - 32, // invis (TB_CAP_INVISIBLE) -}; - -// xterm -static const char *xterm_caps[] = { - "\033OP", // kf1 (TB_CAP_F1) - "\033OQ", // kf2 (TB_CAP_F2) - "\033OR", // kf3 (TB_CAP_F3) - "\033OS", // kf4 (TB_CAP_F4) - "\033[15~", // kf5 (TB_CAP_F5) - "\033[17~", // kf6 (TB_CAP_F6) - "\033[18~", // kf7 (TB_CAP_F7) - "\033[19~", // kf8 (TB_CAP_F8) - "\033[20~", // kf9 (TB_CAP_F9) - "\033[21~", // kf10 (TB_CAP_F10) - "\033[23~", // kf11 (TB_CAP_F11) - "\033[24~", // kf12 (TB_CAP_F12) - "\033[2~", // kich1 (TB_CAP_INSERT) - "\033[3~", // kdch1 (TB_CAP_DELETE) - "\033OH", // khome (TB_CAP_HOME) - "\033OF", // kend (TB_CAP_END) - "\033[5~", // kpp (TB_CAP_PGUP) - "\033[6~", // knp (TB_CAP_PGDN) - "\033OA", // kcuu1 (TB_CAP_ARROW_UP) - "\033OB", // kcud1 (TB_CAP_ARROW_DOWN) - "\033OD", // kcub1 (TB_CAP_ARROW_LEFT) - "\033OC", // kcuf1 (TB_CAP_ARROW_RIGHT) - "\033[Z", // kcbt (TB_CAP_BACK_TAB) - "\033[?1049h\033[22;0;0t", // smcup (TB_CAP_ENTER_CA) - "\033[?1049l\033[23;0;0t", // rmcup (TB_CAP_EXIT_CA) - "\033[?12l\033[?25h", // cnorm (TB_CAP_SHOW_CURSOR) - "\033[?25l", // civis (TB_CAP_HIDE_CURSOR) - "\033[H\033[2J", // clear (TB_CAP_CLEAR_SCREEN) - "\033(B\033[m", // sgr0 (TB_CAP_SGR0) - "\033[4m", // smul (TB_CAP_UNDERLINE) - "\033[1m", // bold (TB_CAP_BOLD) - "\033[5m", // blink (TB_CAP_BLINK) - "\033[3m", // sitm (TB_CAP_ITALIC) - "\033[7m", // rev (TB_CAP_REVERSE) - "\033[?1h\033=", // smkx (TB_CAP_ENTER_KEYPAD) - "\033[?1l\033>", // rmkx (TB_CAP_EXIT_KEYPAD) - "\033[2m", // dim (TB_CAP_DIM) - "\033[8m", // invis (TB_CAP_INVISIBLE) -}; - -// linux -static const char *linux_caps[] = { - "\033[[A", // kf1 (TB_CAP_F1) - "\033[[B", // kf2 (TB_CAP_F2) - "\033[[C", // kf3 (TB_CAP_F3) - "\033[[D", // kf4 (TB_CAP_F4) - "\033[[E", // kf5 (TB_CAP_F5) - "\033[17~", // kf6 (TB_CAP_F6) - "\033[18~", // kf7 (TB_CAP_F7) - "\033[19~", // kf8 (TB_CAP_F8) - "\033[20~", // kf9 (TB_CAP_F9) - "\033[21~", // kf10 (TB_CAP_F10) - "\033[23~", // kf11 (TB_CAP_F11) - "\033[24~", // kf12 (TB_CAP_F12) - "\033[2~", // kich1 (TB_CAP_INSERT) - "\033[3~", // kdch1 (TB_CAP_DELETE) - "\033[1~", // khome (TB_CAP_HOME) - "\033[4~", // kend (TB_CAP_END) - "\033[5~", // kpp (TB_CAP_PGUP) - "\033[6~", // knp (TB_CAP_PGDN) - "\033[A", // kcuu1 (TB_CAP_ARROW_UP) - "\033[B", // kcud1 (TB_CAP_ARROW_DOWN) - "\033[D", // kcub1 (TB_CAP_ARROW_LEFT) - "\033[C", // kcuf1 (TB_CAP_ARROW_RIGHT) - "\033\011", // kcbt (TB_CAP_BACK_TAB) - "", // smcup (TB_CAP_ENTER_CA) - "", // rmcup (TB_CAP_EXIT_CA) - "\033[?25h\033[?0c", // cnorm (TB_CAP_SHOW_CURSOR) - "\033[?25l\033[?1c", // civis (TB_CAP_HIDE_CURSOR) - "\033[H\033[J", // clear (TB_CAP_CLEAR_SCREEN) - "\033[m\017", // sgr0 (TB_CAP_SGR0) - "\033[4m", // smul (TB_CAP_UNDERLINE) - "\033[1m", // bold (TB_CAP_BOLD) - "\033[5m", // blink (TB_CAP_BLINK) - "", // sitm (TB_CAP_ITALIC) - "\033[7m", // rev (TB_CAP_REVERSE) - "", // smkx (TB_CAP_ENTER_KEYPAD) - "", // rmkx (TB_CAP_EXIT_KEYPAD) - "\033[2m", // dim (TB_CAP_DIM) - "", // invis (TB_CAP_INVISIBLE) -}; - -// screen -static const char *screen_caps[] = { - "\033OP", // kf1 (TB_CAP_F1) - "\033OQ", // kf2 (TB_CAP_F2) - "\033OR", // kf3 (TB_CAP_F3) - "\033OS", // kf4 (TB_CAP_F4) - "\033[15~", // kf5 (TB_CAP_F5) - "\033[17~", // kf6 (TB_CAP_F6) - "\033[18~", // kf7 (TB_CAP_F7) - "\033[19~", // kf8 (TB_CAP_F8) - "\033[20~", // kf9 (TB_CAP_F9) - "\033[21~", // kf10 (TB_CAP_F10) - "\033[23~", // kf11 (TB_CAP_F11) - "\033[24~", // kf12 (TB_CAP_F12) - "\033[2~", // kich1 (TB_CAP_INSERT) - "\033[3~", // kdch1 (TB_CAP_DELETE) - "\033[1~", // khome (TB_CAP_HOME) - "\033[4~", // kend (TB_CAP_END) - "\033[5~", // kpp (TB_CAP_PGUP) - "\033[6~", // knp (TB_CAP_PGDN) - "\033OA", // kcuu1 (TB_CAP_ARROW_UP) - "\033OB", // kcud1 (TB_CAP_ARROW_DOWN) - "\033OD", // kcub1 (TB_CAP_ARROW_LEFT) - "\033OC", // kcuf1 (TB_CAP_ARROW_RIGHT) - "\033[Z", // kcbt (TB_CAP_BACK_TAB) - "\033[?1049h", // smcup (TB_CAP_ENTER_CA) - "\033[?1049l", // rmcup (TB_CAP_EXIT_CA) - "\033[34h\033[?25h", // cnorm (TB_CAP_SHOW_CURSOR) - "\033[?25l", // civis (TB_CAP_HIDE_CURSOR) - "\033[H\033[J", // clear (TB_CAP_CLEAR_SCREEN) - "\033[m\017", // sgr0 (TB_CAP_SGR0) - "\033[4m", // smul (TB_CAP_UNDERLINE) - "\033[1m", // bold (TB_CAP_BOLD) - "\033[5m", // blink (TB_CAP_BLINK) - "", // sitm (TB_CAP_ITALIC) - "\033[7m", // rev (TB_CAP_REVERSE) - "\033[?1h\033=", // smkx (TB_CAP_ENTER_KEYPAD) - "\033[?1l\033>", // rmkx (TB_CAP_EXIT_KEYPAD) - "\033[2m", // dim (TB_CAP_DIM) - "", // invis (TB_CAP_INVISIBLE) -}; - -// rxvt-256color -static const char *rxvt_256color_caps[] = { - "\033[11~", // kf1 (TB_CAP_F1) - "\033[12~", // kf2 (TB_CAP_F2) - "\033[13~", // kf3 (TB_CAP_F3) - "\033[14~", // kf4 (TB_CAP_F4) - "\033[15~", // kf5 (TB_CAP_F5) - "\033[17~", // kf6 (TB_CAP_F6) - "\033[18~", // kf7 (TB_CAP_F7) - "\033[19~", // kf8 (TB_CAP_F8) - "\033[20~", // kf9 (TB_CAP_F9) - "\033[21~", // kf10 (TB_CAP_F10) - "\033[23~", // kf11 (TB_CAP_F11) - "\033[24~", // kf12 (TB_CAP_F12) - "\033[2~", // kich1 (TB_CAP_INSERT) - "\033[3~", // kdch1 (TB_CAP_DELETE) - "\033[7~", // khome (TB_CAP_HOME) - "\033[8~", // kend (TB_CAP_END) - "\033[5~", // kpp (TB_CAP_PGUP) - "\033[6~", // knp (TB_CAP_PGDN) - "\033[A", // kcuu1 (TB_CAP_ARROW_UP) - "\033[B", // kcud1 (TB_CAP_ARROW_DOWN) - "\033[D", // kcub1 (TB_CAP_ARROW_LEFT) - "\033[C", // kcuf1 (TB_CAP_ARROW_RIGHT) - "\033[Z", // kcbt (TB_CAP_BACK_TAB) - "\0337\033[?47h", // smcup (TB_CAP_ENTER_CA) - "\033[2J\033[?47l\0338", // rmcup (TB_CAP_EXIT_CA) - "\033[?25h", // cnorm (TB_CAP_SHOW_CURSOR) - "\033[?25l", // civis (TB_CAP_HIDE_CURSOR) - "\033[H\033[2J", // clear (TB_CAP_CLEAR_SCREEN) - "\033[m\017", // sgr0 (TB_CAP_SGR0) - "\033[4m", // smul (TB_CAP_UNDERLINE) - "\033[1m", // bold (TB_CAP_BOLD) - "\033[5m", // blink (TB_CAP_BLINK) - "", // sitm (TB_CAP_ITALIC) - "\033[7m", // rev (TB_CAP_REVERSE) - "\033=", // smkx (TB_CAP_ENTER_KEYPAD) - "\033>", // rmkx (TB_CAP_EXIT_KEYPAD) - "", // dim (TB_CAP_DIM) - "", // invis (TB_CAP_INVISIBLE) -}; - -// rxvt-unicode -static const char *rxvt_unicode_caps[] = { - "\033[11~", // kf1 (TB_CAP_F1) - "\033[12~", // kf2 (TB_CAP_F2) - "\033[13~", // kf3 (TB_CAP_F3) - "\033[14~", // kf4 (TB_CAP_F4) - "\033[15~", // kf5 (TB_CAP_F5) - "\033[17~", // kf6 (TB_CAP_F6) - "\033[18~", // kf7 (TB_CAP_F7) - "\033[19~", // kf8 (TB_CAP_F8) - "\033[20~", // kf9 (TB_CAP_F9) - "\033[21~", // kf10 (TB_CAP_F10) - "\033[23~", // kf11 (TB_CAP_F11) - "\033[24~", // kf12 (TB_CAP_F12) - "\033[2~", // kich1 (TB_CAP_INSERT) - "\033[3~", // kdch1 (TB_CAP_DELETE) - "\033[7~", // khome (TB_CAP_HOME) - "\033[8~", // kend (TB_CAP_END) - "\033[5~", // kpp (TB_CAP_PGUP) - "\033[6~", // knp (TB_CAP_PGDN) - "\033[A", // kcuu1 (TB_CAP_ARROW_UP) - "\033[B", // kcud1 (TB_CAP_ARROW_DOWN) - "\033[D", // kcub1 (TB_CAP_ARROW_LEFT) - "\033[C", // kcuf1 (TB_CAP_ARROW_RIGHT) - "\033[Z", // kcbt (TB_CAP_BACK_TAB) - "\033[?1049h", // smcup (TB_CAP_ENTER_CA) - "\033[r\033[?1049l", // rmcup (TB_CAP_EXIT_CA) - "\033[?12l\033[?25h", // cnorm (TB_CAP_SHOW_CURSOR) - "\033[?25l", // civis (TB_CAP_HIDE_CURSOR) - "\033[H\033[2J", // clear (TB_CAP_CLEAR_SCREEN) - "\033[m\033(B", // sgr0 (TB_CAP_SGR0) - "\033[4m", // smul (TB_CAP_UNDERLINE) - "\033[1m", // bold (TB_CAP_BOLD) - "\033[5m", // blink (TB_CAP_BLINK) - "\033[3m", // sitm (TB_CAP_ITALIC) - "\033[7m", // rev (TB_CAP_REVERSE) - "\033=", // smkx (TB_CAP_ENTER_KEYPAD) - "\033>", // rmkx (TB_CAP_EXIT_KEYPAD) - "", // dim (TB_CAP_DIM) - "", // invis (TB_CAP_INVISIBLE) -}; - -// Eterm -static const char *eterm_caps[] = { - "\033[11~", // kf1 (TB_CAP_F1) - "\033[12~", // kf2 (TB_CAP_F2) - "\033[13~", // kf3 (TB_CAP_F3) - "\033[14~", // kf4 (TB_CAP_F4) - "\033[15~", // kf5 (TB_CAP_F5) - "\033[17~", // kf6 (TB_CAP_F6) - "\033[18~", // kf7 (TB_CAP_F7) - "\033[19~", // kf8 (TB_CAP_F8) - "\033[20~", // kf9 (TB_CAP_F9) - "\033[21~", // kf10 (TB_CAP_F10) - "\033[23~", // kf11 (TB_CAP_F11) - "\033[24~", // kf12 (TB_CAP_F12) - "\033[2~", // kich1 (TB_CAP_INSERT) - "\033[3~", // kdch1 (TB_CAP_DELETE) - "\033[7~", // khome (TB_CAP_HOME) - "\033[8~", // kend (TB_CAP_END) - "\033[5~", // kpp (TB_CAP_PGUP) - "\033[6~", // knp (TB_CAP_PGDN) - "\033[A", // kcuu1 (TB_CAP_ARROW_UP) - "\033[B", // kcud1 (TB_CAP_ARROW_DOWN) - "\033[D", // kcub1 (TB_CAP_ARROW_LEFT) - "\033[C", // kcuf1 (TB_CAP_ARROW_RIGHT) - "", // kcbt (TB_CAP_BACK_TAB) - "\0337\033[?47h", // smcup (TB_CAP_ENTER_CA) - "\033[2J\033[?47l\0338", // rmcup (TB_CAP_EXIT_CA) - "\033[?25h", // cnorm (TB_CAP_SHOW_CURSOR) - "\033[?25l", // civis (TB_CAP_HIDE_CURSOR) - "\033[H\033[2J", // clear (TB_CAP_CLEAR_SCREEN) - "\033[m\017", // sgr0 (TB_CAP_SGR0) - "\033[4m", // smul (TB_CAP_UNDERLINE) - "\033[1m", // bold (TB_CAP_BOLD) - "\033[5m", // blink (TB_CAP_BLINK) - "", // sitm (TB_CAP_ITALIC) - "\033[7m", // rev (TB_CAP_REVERSE) - "", // smkx (TB_CAP_ENTER_KEYPAD) - "", // rmkx (TB_CAP_EXIT_KEYPAD) - "", // dim (TB_CAP_DIM) - "", // invis (TB_CAP_INVISIBLE) -}; - -static struct { - const char *name; - const char **caps; - const char *alias; -} builtin_terms[] = { - {"xterm", xterm_caps, "" }, - {"linux", linux_caps, "" }, - {"screen", screen_caps, "tmux"}, - {"rxvt-256color", rxvt_256color_caps, "" }, - {"rxvt-unicode", rxvt_unicode_caps, "rxvt"}, - {"Eterm", eterm_caps, "" }, - {NULL, NULL, NULL }, -}; - -/* END codegen c */ - -static struct { - const char *cap; - const uint16_t key; - const uint8_t mod; -} builtin_mod_caps[] = { - // xterm arrows - {"\x1b[1;2A", TB_KEY_ARROW_UP, TB_MOD_SHIFT }, - {"\x1b[1;3A", TB_KEY_ARROW_UP, TB_MOD_ALT }, - {"\x1b[1;4A", TB_KEY_ARROW_UP, TB_MOD_ALT | TB_MOD_SHIFT }, - {"\x1b[1;5A", TB_KEY_ARROW_UP, TB_MOD_CTRL }, - {"\x1b[1;6A", TB_KEY_ARROW_UP, TB_MOD_CTRL | TB_MOD_SHIFT }, - {"\x1b[1;7A", TB_KEY_ARROW_UP, TB_MOD_CTRL | TB_MOD_ALT }, - {"\x1b[1;8A", TB_KEY_ARROW_UP, TB_MOD_CTRL | TB_MOD_ALT | TB_MOD_SHIFT}, - - {"\x1b[1;2B", TB_KEY_ARROW_DOWN, TB_MOD_SHIFT }, - {"\x1b[1;3B", TB_KEY_ARROW_DOWN, TB_MOD_ALT }, - {"\x1b[1;4B", TB_KEY_ARROW_DOWN, TB_MOD_ALT | TB_MOD_SHIFT }, - {"\x1b[1;5B", TB_KEY_ARROW_DOWN, TB_MOD_CTRL }, - {"\x1b[1;6B", TB_KEY_ARROW_DOWN, TB_MOD_CTRL | TB_MOD_SHIFT }, - {"\x1b[1;7B", TB_KEY_ARROW_DOWN, TB_MOD_CTRL | TB_MOD_ALT }, - {"\x1b[1;8B", TB_KEY_ARROW_DOWN, TB_MOD_CTRL | TB_MOD_ALT | TB_MOD_SHIFT}, - - {"\x1b[1;2C", TB_KEY_ARROW_RIGHT, TB_MOD_SHIFT }, - {"\x1b[1;3C", TB_KEY_ARROW_RIGHT, TB_MOD_ALT }, - {"\x1b[1;4C", TB_KEY_ARROW_RIGHT, TB_MOD_ALT | TB_MOD_SHIFT }, - {"\x1b[1;5C", TB_KEY_ARROW_RIGHT, TB_MOD_CTRL }, - {"\x1b[1;6C", TB_KEY_ARROW_RIGHT, TB_MOD_CTRL | TB_MOD_SHIFT }, - {"\x1b[1;7C", TB_KEY_ARROW_RIGHT, TB_MOD_CTRL | TB_MOD_ALT }, - {"\x1b[1;8C", TB_KEY_ARROW_RIGHT, TB_MOD_CTRL | TB_MOD_ALT | TB_MOD_SHIFT}, - - {"\x1b[1;2D", TB_KEY_ARROW_LEFT, TB_MOD_SHIFT }, - {"\x1b[1;3D", TB_KEY_ARROW_LEFT, TB_MOD_ALT }, - {"\x1b[1;4D", TB_KEY_ARROW_LEFT, TB_MOD_ALT | TB_MOD_SHIFT }, - {"\x1b[1;5D", TB_KEY_ARROW_LEFT, TB_MOD_CTRL }, - {"\x1b[1;6D", TB_KEY_ARROW_LEFT, TB_MOD_CTRL | TB_MOD_SHIFT }, - {"\x1b[1;7D", TB_KEY_ARROW_LEFT, TB_MOD_CTRL | TB_MOD_ALT }, - {"\x1b[1;8D", TB_KEY_ARROW_LEFT, TB_MOD_CTRL | TB_MOD_ALT | TB_MOD_SHIFT}, - - // xterm keys - {"\x1b[1;2H", TB_KEY_HOME, TB_MOD_SHIFT }, - {"\x1b[1;3H", TB_KEY_HOME, TB_MOD_ALT }, - {"\x1b[1;4H", TB_KEY_HOME, TB_MOD_ALT | TB_MOD_SHIFT }, - {"\x1b[1;5H", TB_KEY_HOME, TB_MOD_CTRL }, - {"\x1b[1;6H", TB_KEY_HOME, TB_MOD_CTRL | TB_MOD_SHIFT }, - {"\x1b[1;7H", TB_KEY_HOME, TB_MOD_CTRL | TB_MOD_ALT }, - {"\x1b[1;8H", TB_KEY_HOME, TB_MOD_CTRL | TB_MOD_ALT | TB_MOD_SHIFT}, - - {"\x1b[1;2F", TB_KEY_END, TB_MOD_SHIFT }, - {"\x1b[1;3F", TB_KEY_END, TB_MOD_ALT }, - {"\x1b[1;4F", TB_KEY_END, TB_MOD_ALT | TB_MOD_SHIFT }, - {"\x1b[1;5F", TB_KEY_END, TB_MOD_CTRL }, - {"\x1b[1;6F", TB_KEY_END, TB_MOD_CTRL | TB_MOD_SHIFT }, - {"\x1b[1;7F", TB_KEY_END, TB_MOD_CTRL | TB_MOD_ALT }, - {"\x1b[1;8F", TB_KEY_END, TB_MOD_CTRL | TB_MOD_ALT | TB_MOD_SHIFT}, - - {"\x1b[2;2~", TB_KEY_INSERT, TB_MOD_SHIFT }, - {"\x1b[2;3~", TB_KEY_INSERT, TB_MOD_ALT }, - {"\x1b[2;4~", TB_KEY_INSERT, TB_MOD_ALT | TB_MOD_SHIFT }, - {"\x1b[2;5~", TB_KEY_INSERT, TB_MOD_CTRL }, - {"\x1b[2;6~", TB_KEY_INSERT, TB_MOD_CTRL | TB_MOD_SHIFT }, - {"\x1b[2;7~", TB_KEY_INSERT, TB_MOD_CTRL | TB_MOD_ALT }, - {"\x1b[2;8~", TB_KEY_INSERT, TB_MOD_CTRL | TB_MOD_ALT | TB_MOD_SHIFT}, - - {"\x1b[3;2~", TB_KEY_DELETE, TB_MOD_SHIFT }, - {"\x1b[3;3~", TB_KEY_DELETE, TB_MOD_ALT }, - {"\x1b[3;4~", TB_KEY_DELETE, TB_MOD_ALT | TB_MOD_SHIFT }, - {"\x1b[3;5~", TB_KEY_DELETE, TB_MOD_CTRL }, - {"\x1b[3;6~", TB_KEY_DELETE, TB_MOD_CTRL | TB_MOD_SHIFT }, - {"\x1b[3;7~", TB_KEY_DELETE, TB_MOD_CTRL | TB_MOD_ALT }, - {"\x1b[3;8~", TB_KEY_DELETE, TB_MOD_CTRL | TB_MOD_ALT | TB_MOD_SHIFT}, - - {"\x1b[5;2~", TB_KEY_PGUP, TB_MOD_SHIFT }, - {"\x1b[5;3~", TB_KEY_PGUP, TB_MOD_ALT }, - {"\x1b[5;4~", TB_KEY_PGUP, TB_MOD_ALT | TB_MOD_SHIFT }, - {"\x1b[5;5~", TB_KEY_PGUP, TB_MOD_CTRL }, - {"\x1b[5;6~", TB_KEY_PGUP, TB_MOD_CTRL | TB_MOD_SHIFT }, - {"\x1b[5;7~", TB_KEY_PGUP, TB_MOD_CTRL | TB_MOD_ALT }, - {"\x1b[5;8~", TB_KEY_PGUP, TB_MOD_CTRL | TB_MOD_ALT | TB_MOD_SHIFT}, - - {"\x1b[6;2~", TB_KEY_PGDN, TB_MOD_SHIFT }, - {"\x1b[6;3~", TB_KEY_PGDN, TB_MOD_ALT }, - {"\x1b[6;4~", TB_KEY_PGDN, TB_MOD_ALT | TB_MOD_SHIFT }, - {"\x1b[6;5~", TB_KEY_PGDN, TB_MOD_CTRL }, - {"\x1b[6;6~", TB_KEY_PGDN, TB_MOD_CTRL | TB_MOD_SHIFT }, - {"\x1b[6;7~", TB_KEY_PGDN, TB_MOD_CTRL | TB_MOD_ALT }, - {"\x1b[6;8~", TB_KEY_PGDN, TB_MOD_CTRL | TB_MOD_ALT | TB_MOD_SHIFT}, - - {"\x1b[1;2P", TB_KEY_F1, TB_MOD_SHIFT }, - {"\x1b[1;3P", TB_KEY_F1, TB_MOD_ALT }, - {"\x1b[1;4P", TB_KEY_F1, TB_MOD_ALT | TB_MOD_SHIFT }, - {"\x1b[1;5P", TB_KEY_F1, TB_MOD_CTRL }, - {"\x1b[1;6P", TB_KEY_F1, TB_MOD_CTRL | TB_MOD_SHIFT }, - {"\x1b[1;7P", TB_KEY_F1, TB_MOD_CTRL | TB_MOD_ALT }, - {"\x1b[1;8P", TB_KEY_F1, TB_MOD_CTRL | TB_MOD_ALT | TB_MOD_SHIFT}, - - {"\x1b[1;2Q", TB_KEY_F2, TB_MOD_SHIFT }, - {"\x1b[1;3Q", TB_KEY_F2, TB_MOD_ALT }, - {"\x1b[1;4Q", TB_KEY_F2, TB_MOD_ALT | TB_MOD_SHIFT }, - {"\x1b[1;5Q", TB_KEY_F2, TB_MOD_CTRL }, - {"\x1b[1;6Q", TB_KEY_F2, TB_MOD_CTRL | TB_MOD_SHIFT }, - {"\x1b[1;7Q", TB_KEY_F2, TB_MOD_CTRL | TB_MOD_ALT }, - {"\x1b[1;8Q", TB_KEY_F2, TB_MOD_CTRL | TB_MOD_ALT | TB_MOD_SHIFT}, - - {"\x1b[1;2R", TB_KEY_F3, TB_MOD_SHIFT }, - {"\x1b[1;3R", TB_KEY_F3, TB_MOD_ALT }, - {"\x1b[1;4R", TB_KEY_F3, TB_MOD_ALT | TB_MOD_SHIFT }, - {"\x1b[1;5R", TB_KEY_F3, TB_MOD_CTRL }, - {"\x1b[1;6R", TB_KEY_F3, TB_MOD_CTRL | TB_MOD_SHIFT }, - {"\x1b[1;7R", TB_KEY_F3, TB_MOD_CTRL | TB_MOD_ALT }, - {"\x1b[1;8R", TB_KEY_F3, TB_MOD_CTRL | TB_MOD_ALT | TB_MOD_SHIFT}, - - {"\x1b[1;2S", TB_KEY_F4, TB_MOD_SHIFT }, - {"\x1b[1;3S", TB_KEY_F4, TB_MOD_ALT }, - {"\x1b[1;4S", TB_KEY_F4, TB_MOD_ALT | TB_MOD_SHIFT }, - {"\x1b[1;5S", TB_KEY_F4, TB_MOD_CTRL }, - {"\x1b[1;6S", TB_KEY_F4, TB_MOD_CTRL | TB_MOD_SHIFT }, - {"\x1b[1;7S", TB_KEY_F4, TB_MOD_CTRL | TB_MOD_ALT }, - {"\x1b[1;8S", TB_KEY_F4, TB_MOD_CTRL | TB_MOD_ALT | TB_MOD_SHIFT}, - - {"\x1b[15;2~", TB_KEY_F5, TB_MOD_SHIFT }, - {"\x1b[15;3~", TB_KEY_F5, TB_MOD_ALT }, - {"\x1b[15;4~", TB_KEY_F5, TB_MOD_ALT | TB_MOD_SHIFT }, - {"\x1b[15;5~", TB_KEY_F5, TB_MOD_CTRL }, - {"\x1b[15;6~", TB_KEY_F5, TB_MOD_CTRL | TB_MOD_SHIFT }, - {"\x1b[15;7~", TB_KEY_F5, TB_MOD_CTRL | TB_MOD_ALT }, - {"\x1b[15;8~", TB_KEY_F5, TB_MOD_CTRL | TB_MOD_ALT | TB_MOD_SHIFT}, - - {"\x1b[17;2~", TB_KEY_F6, TB_MOD_SHIFT }, - {"\x1b[17;3~", TB_KEY_F6, TB_MOD_ALT }, - {"\x1b[17;4~", TB_KEY_F6, TB_MOD_ALT | TB_MOD_SHIFT }, - {"\x1b[17;5~", TB_KEY_F6, TB_MOD_CTRL }, - {"\x1b[17;6~", TB_KEY_F6, TB_MOD_CTRL | TB_MOD_SHIFT }, - {"\x1b[17;7~", TB_KEY_F6, TB_MOD_CTRL | TB_MOD_ALT }, - {"\x1b[17;8~", TB_KEY_F6, TB_MOD_CTRL | TB_MOD_ALT | TB_MOD_SHIFT}, - - {"\x1b[18;2~", TB_KEY_F7, TB_MOD_SHIFT }, - {"\x1b[18;3~", TB_KEY_F7, TB_MOD_ALT }, - {"\x1b[18;4~", TB_KEY_F7, TB_MOD_ALT | TB_MOD_SHIFT }, - {"\x1b[18;5~", TB_KEY_F7, TB_MOD_CTRL }, - {"\x1b[18;6~", TB_KEY_F7, TB_MOD_CTRL | TB_MOD_SHIFT }, - {"\x1b[18;7~", TB_KEY_F7, TB_MOD_CTRL | TB_MOD_ALT }, - {"\x1b[18;8~", TB_KEY_F7, TB_MOD_CTRL | TB_MOD_ALT | TB_MOD_SHIFT}, - - {"\x1b[19;2~", TB_KEY_F8, TB_MOD_SHIFT }, - {"\x1b[19;3~", TB_KEY_F8, TB_MOD_ALT }, - {"\x1b[19;4~", TB_KEY_F8, TB_MOD_ALT | TB_MOD_SHIFT }, - {"\x1b[19;5~", TB_KEY_F8, TB_MOD_CTRL }, - {"\x1b[19;6~", TB_KEY_F8, TB_MOD_CTRL | TB_MOD_SHIFT }, - {"\x1b[19;7~", TB_KEY_F8, TB_MOD_CTRL | TB_MOD_ALT }, - {"\x1b[19;8~", TB_KEY_F8, TB_MOD_CTRL | TB_MOD_ALT | TB_MOD_SHIFT}, - - {"\x1b[20;2~", TB_KEY_F9, TB_MOD_SHIFT }, - {"\x1b[20;3~", TB_KEY_F9, TB_MOD_ALT }, - {"\x1b[20;4~", TB_KEY_F9, TB_MOD_ALT | TB_MOD_SHIFT }, - {"\x1b[20;5~", TB_KEY_F9, TB_MOD_CTRL }, - {"\x1b[20;6~", TB_KEY_F9, TB_MOD_CTRL | TB_MOD_SHIFT }, - {"\x1b[20;7~", TB_KEY_F9, TB_MOD_CTRL | TB_MOD_ALT }, - {"\x1b[20;8~", TB_KEY_F9, TB_MOD_CTRL | TB_MOD_ALT | TB_MOD_SHIFT}, - - {"\x1b[21;2~", TB_KEY_F10, TB_MOD_SHIFT }, - {"\x1b[21;3~", TB_KEY_F10, TB_MOD_ALT }, - {"\x1b[21;4~", TB_KEY_F10, TB_MOD_ALT | TB_MOD_SHIFT }, - {"\x1b[21;5~", TB_KEY_F10, TB_MOD_CTRL }, - {"\x1b[21;6~", TB_KEY_F10, TB_MOD_CTRL | TB_MOD_SHIFT }, - {"\x1b[21;7~", TB_KEY_F10, TB_MOD_CTRL | TB_MOD_ALT }, - {"\x1b[21;8~", TB_KEY_F10, TB_MOD_CTRL | TB_MOD_ALT | TB_MOD_SHIFT}, - - {"\x1b[23;2~", TB_KEY_F11, TB_MOD_SHIFT }, - {"\x1b[23;3~", TB_KEY_F11, TB_MOD_ALT }, - {"\x1b[23;4~", TB_KEY_F11, TB_MOD_ALT | TB_MOD_SHIFT }, - {"\x1b[23;5~", TB_KEY_F11, TB_MOD_CTRL }, - {"\x1b[23;6~", TB_KEY_F11, TB_MOD_CTRL | TB_MOD_SHIFT }, - {"\x1b[23;7~", TB_KEY_F11, TB_MOD_CTRL | TB_MOD_ALT }, - {"\x1b[23;8~", TB_KEY_F11, TB_MOD_CTRL | TB_MOD_ALT | TB_MOD_SHIFT}, - - {"\x1b[24;2~", TB_KEY_F12, TB_MOD_SHIFT }, - {"\x1b[24;3~", TB_KEY_F12, TB_MOD_ALT }, - {"\x1b[24;4~", TB_KEY_F12, TB_MOD_ALT | TB_MOD_SHIFT }, - {"\x1b[24;5~", TB_KEY_F12, TB_MOD_CTRL }, - {"\x1b[24;6~", TB_KEY_F12, TB_MOD_CTRL | TB_MOD_SHIFT }, - {"\x1b[24;7~", TB_KEY_F12, TB_MOD_CTRL | TB_MOD_ALT }, - {"\x1b[24;8~", TB_KEY_F12, TB_MOD_CTRL | TB_MOD_ALT | TB_MOD_SHIFT}, - - // rxvt arrows - {"\x1b[a", TB_KEY_ARROW_UP, TB_MOD_SHIFT }, - {"\x1b\x1b[A", TB_KEY_ARROW_UP, TB_MOD_ALT }, - {"\x1b\x1b[a", TB_KEY_ARROW_UP, TB_MOD_ALT | TB_MOD_SHIFT }, - {"\x1bOa", TB_KEY_ARROW_UP, TB_MOD_CTRL }, - {"\x1b\x1bOa", TB_KEY_ARROW_UP, TB_MOD_CTRL | TB_MOD_ALT }, - - {"\x1b[b", TB_KEY_ARROW_DOWN, TB_MOD_SHIFT }, - {"\x1b\x1b[B", TB_KEY_ARROW_DOWN, TB_MOD_ALT }, - {"\x1b\x1b[b", TB_KEY_ARROW_DOWN, TB_MOD_ALT | TB_MOD_SHIFT }, - {"\x1bOb", TB_KEY_ARROW_DOWN, TB_MOD_CTRL }, - {"\x1b\x1bOb", TB_KEY_ARROW_DOWN, TB_MOD_CTRL | TB_MOD_ALT }, - - {"\x1b[c", TB_KEY_ARROW_RIGHT, TB_MOD_SHIFT }, - {"\x1b\x1b[C", TB_KEY_ARROW_RIGHT, TB_MOD_ALT }, - {"\x1b\x1b[c", TB_KEY_ARROW_RIGHT, TB_MOD_ALT | TB_MOD_SHIFT }, - {"\x1bOc", TB_KEY_ARROW_RIGHT, TB_MOD_CTRL }, - {"\x1b\x1bOc", TB_KEY_ARROW_RIGHT, TB_MOD_CTRL | TB_MOD_ALT }, - - {"\x1b[d", TB_KEY_ARROW_LEFT, TB_MOD_SHIFT }, - {"\x1b\x1b[D", TB_KEY_ARROW_LEFT, TB_MOD_ALT }, - {"\x1b\x1b[d", TB_KEY_ARROW_LEFT, TB_MOD_ALT | TB_MOD_SHIFT }, - {"\x1bOd", TB_KEY_ARROW_LEFT, TB_MOD_CTRL }, - {"\x1b\x1bOd", TB_KEY_ARROW_LEFT, TB_MOD_CTRL | TB_MOD_ALT }, - - // rxvt keys - {"\x1b[7$", TB_KEY_HOME, TB_MOD_SHIFT }, - {"\x1b\x1b[7~", TB_KEY_HOME, TB_MOD_ALT }, - {"\x1b\x1b[7$", TB_KEY_HOME, TB_MOD_ALT | TB_MOD_SHIFT }, - {"\x1b[7^", TB_KEY_HOME, TB_MOD_CTRL }, - {"\x1b[7@", TB_KEY_HOME, TB_MOD_CTRL | TB_MOD_SHIFT }, - {"\x1b\x1b[7^", TB_KEY_HOME, TB_MOD_CTRL | TB_MOD_ALT }, - {"\x1b\x1b[7@", TB_KEY_HOME, TB_MOD_CTRL | TB_MOD_ALT | TB_MOD_SHIFT}, - - {"\x1b\x1b[8~", TB_KEY_END, TB_MOD_ALT }, - {"\x1b\x1b[8$", TB_KEY_END, TB_MOD_ALT | TB_MOD_SHIFT }, - {"\x1b[8^", TB_KEY_END, TB_MOD_CTRL }, - {"\x1b\x1b[8^", TB_KEY_END, TB_MOD_CTRL | TB_MOD_ALT }, - {"\x1b\x1b[8@", TB_KEY_END, TB_MOD_CTRL | TB_MOD_ALT | TB_MOD_SHIFT}, - {"\x1b[8@", TB_KEY_END, TB_MOD_CTRL | TB_MOD_SHIFT }, - {"\x1b[8$", TB_KEY_END, TB_MOD_SHIFT }, - - {"\x1b\x1b[2~", TB_KEY_INSERT, TB_MOD_ALT }, - {"\x1b\x1b[2$", TB_KEY_INSERT, TB_MOD_ALT | TB_MOD_SHIFT }, - {"\x1b[2^", TB_KEY_INSERT, TB_MOD_CTRL }, - {"\x1b\x1b[2^", TB_KEY_INSERT, TB_MOD_CTRL | TB_MOD_ALT }, - {"\x1b\x1b[2@", TB_KEY_INSERT, TB_MOD_CTRL | TB_MOD_ALT | TB_MOD_SHIFT}, - {"\x1b[2@", TB_KEY_INSERT, TB_MOD_CTRL | TB_MOD_SHIFT }, - {"\x1b[2$", TB_KEY_INSERT, TB_MOD_SHIFT }, - - {"\x1b\x1b[3~", TB_KEY_DELETE, TB_MOD_ALT }, - {"\x1b\x1b[3$", TB_KEY_DELETE, TB_MOD_ALT | TB_MOD_SHIFT }, - {"\x1b[3^", TB_KEY_DELETE, TB_MOD_CTRL }, - {"\x1b\x1b[3^", TB_KEY_DELETE, TB_MOD_CTRL | TB_MOD_ALT }, - {"\x1b\x1b[3@", TB_KEY_DELETE, TB_MOD_CTRL | TB_MOD_ALT | TB_MOD_SHIFT}, - {"\x1b[3@", TB_KEY_DELETE, TB_MOD_CTRL | TB_MOD_SHIFT }, - {"\x1b[3$", TB_KEY_DELETE, TB_MOD_SHIFT }, - - {"\x1b\x1b[5~", TB_KEY_PGUP, TB_MOD_ALT }, - {"\x1b\x1b[5$", TB_KEY_PGUP, TB_MOD_ALT | TB_MOD_SHIFT }, - {"\x1b[5^", TB_KEY_PGUP, TB_MOD_CTRL }, - {"\x1b\x1b[5^", TB_KEY_PGUP, TB_MOD_CTRL | TB_MOD_ALT }, - {"\x1b\x1b[5@", TB_KEY_PGUP, TB_MOD_CTRL | TB_MOD_ALT | TB_MOD_SHIFT}, - {"\x1b[5@", TB_KEY_PGUP, TB_MOD_CTRL | TB_MOD_SHIFT }, - {"\x1b[5$", TB_KEY_PGUP, TB_MOD_SHIFT }, - - {"\x1b\x1b[6~", TB_KEY_PGDN, TB_MOD_ALT }, - {"\x1b\x1b[6$", TB_KEY_PGDN, TB_MOD_ALT | TB_MOD_SHIFT }, - {"\x1b[6^", TB_KEY_PGDN, TB_MOD_CTRL }, - {"\x1b\x1b[6^", TB_KEY_PGDN, TB_MOD_CTRL | TB_MOD_ALT }, - {"\x1b\x1b[6@", TB_KEY_PGDN, TB_MOD_CTRL | TB_MOD_ALT | TB_MOD_SHIFT}, - {"\x1b[6@", TB_KEY_PGDN, TB_MOD_CTRL | TB_MOD_SHIFT }, - {"\x1b[6$", TB_KEY_PGDN, TB_MOD_SHIFT }, - - {"\x1b\x1b[11~", TB_KEY_F1, TB_MOD_ALT }, - {"\x1b\x1b[23~", TB_KEY_F1, TB_MOD_ALT | TB_MOD_SHIFT }, - {"\x1b[11^", TB_KEY_F1, TB_MOD_CTRL }, - {"\x1b\x1b[11^", TB_KEY_F1, TB_MOD_CTRL | TB_MOD_ALT }, - {"\x1b\x1b[23^", TB_KEY_F1, TB_MOD_CTRL | TB_MOD_ALT | TB_MOD_SHIFT}, - {"\x1b[23^", TB_KEY_F1, TB_MOD_CTRL | TB_MOD_SHIFT }, - {"\x1b[23~", TB_KEY_F1, TB_MOD_SHIFT }, - - {"\x1b\x1b[12~", TB_KEY_F2, TB_MOD_ALT }, - {"\x1b\x1b[24~", TB_KEY_F2, TB_MOD_ALT | TB_MOD_SHIFT }, - {"\x1b[12^", TB_KEY_F2, TB_MOD_CTRL }, - {"\x1b\x1b[12^", TB_KEY_F2, TB_MOD_CTRL | TB_MOD_ALT }, - {"\x1b\x1b[24^", TB_KEY_F2, TB_MOD_CTRL | TB_MOD_ALT | TB_MOD_SHIFT}, - {"\x1b[24^", TB_KEY_F2, TB_MOD_CTRL | TB_MOD_SHIFT }, - {"\x1b[24~", TB_KEY_F2, TB_MOD_SHIFT }, - - {"\x1b\x1b[13~", TB_KEY_F3, TB_MOD_ALT }, - {"\x1b\x1b[25~", TB_KEY_F3, TB_MOD_ALT | TB_MOD_SHIFT }, - {"\x1b[13^", TB_KEY_F3, TB_MOD_CTRL }, - {"\x1b\x1b[13^", TB_KEY_F3, TB_MOD_CTRL | TB_MOD_ALT }, - {"\x1b\x1b[25^", TB_KEY_F3, TB_MOD_CTRL | TB_MOD_ALT | TB_MOD_SHIFT}, - {"\x1b[25^", TB_KEY_F3, TB_MOD_CTRL | TB_MOD_SHIFT }, - {"\x1b[25~", TB_KEY_F3, TB_MOD_SHIFT }, - - {"\x1b\x1b[14~", TB_KEY_F4, TB_MOD_ALT }, - {"\x1b\x1b[26~", TB_KEY_F4, TB_MOD_ALT | TB_MOD_SHIFT }, - {"\x1b[14^", TB_KEY_F4, TB_MOD_CTRL }, - {"\x1b\x1b[14^", TB_KEY_F4, TB_MOD_CTRL | TB_MOD_ALT }, - {"\x1b\x1b[26^", TB_KEY_F4, TB_MOD_CTRL | TB_MOD_ALT | TB_MOD_SHIFT}, - {"\x1b[26^", TB_KEY_F4, TB_MOD_CTRL | TB_MOD_SHIFT }, - {"\x1b[26~", TB_KEY_F4, TB_MOD_SHIFT }, - - {"\x1b\x1b[15~", TB_KEY_F5, TB_MOD_ALT }, - {"\x1b\x1b[28~", TB_KEY_F5, TB_MOD_ALT | TB_MOD_SHIFT }, - {"\x1b[15^", TB_KEY_F5, TB_MOD_CTRL }, - {"\x1b\x1b[15^", TB_KEY_F5, TB_MOD_CTRL | TB_MOD_ALT }, - {"\x1b\x1b[28^", TB_KEY_F5, TB_MOD_CTRL | TB_MOD_ALT | TB_MOD_SHIFT}, - {"\x1b[28^", TB_KEY_F5, TB_MOD_CTRL | TB_MOD_SHIFT }, - {"\x1b[28~", TB_KEY_F5, TB_MOD_SHIFT }, - - {"\x1b\x1b[17~", TB_KEY_F6, TB_MOD_ALT }, - {"\x1b\x1b[29~", TB_KEY_F6, TB_MOD_ALT | TB_MOD_SHIFT }, - {"\x1b[17^", TB_KEY_F6, TB_MOD_CTRL }, - {"\x1b\x1b[17^", TB_KEY_F6, TB_MOD_CTRL | TB_MOD_ALT }, - {"\x1b\x1b[29^", TB_KEY_F6, TB_MOD_CTRL | TB_MOD_ALT | TB_MOD_SHIFT}, - {"\x1b[29^", TB_KEY_F6, TB_MOD_CTRL | TB_MOD_SHIFT }, - {"\x1b[29~", TB_KEY_F6, TB_MOD_SHIFT }, - - {"\x1b\x1b[18~", TB_KEY_F7, TB_MOD_ALT }, - {"\x1b\x1b[31~", TB_KEY_F7, TB_MOD_ALT | TB_MOD_SHIFT }, - {"\x1b[18^", TB_KEY_F7, TB_MOD_CTRL }, - {"\x1b\x1b[18^", TB_KEY_F7, TB_MOD_CTRL | TB_MOD_ALT }, - {"\x1b\x1b[31^", TB_KEY_F7, TB_MOD_CTRL | TB_MOD_ALT | TB_MOD_SHIFT}, - {"\x1b[31^", TB_KEY_F7, TB_MOD_CTRL | TB_MOD_SHIFT }, - {"\x1b[31~", TB_KEY_F7, TB_MOD_SHIFT }, - - {"\x1b\x1b[19~", TB_KEY_F8, TB_MOD_ALT }, - {"\x1b\x1b[32~", TB_KEY_F8, TB_MOD_ALT | TB_MOD_SHIFT }, - {"\x1b[19^", TB_KEY_F8, TB_MOD_CTRL }, - {"\x1b\x1b[19^", TB_KEY_F8, TB_MOD_CTRL | TB_MOD_ALT }, - {"\x1b\x1b[32^", TB_KEY_F8, TB_MOD_CTRL | TB_MOD_ALT | TB_MOD_SHIFT}, - {"\x1b[32^", TB_KEY_F8, TB_MOD_CTRL | TB_MOD_SHIFT }, - {"\x1b[32~", TB_KEY_F8, TB_MOD_SHIFT }, - - {"\x1b\x1b[20~", TB_KEY_F9, TB_MOD_ALT }, - {"\x1b\x1b[33~", TB_KEY_F9, TB_MOD_ALT | TB_MOD_SHIFT }, - {"\x1b[20^", TB_KEY_F9, TB_MOD_CTRL }, - {"\x1b\x1b[20^", TB_KEY_F9, TB_MOD_CTRL | TB_MOD_ALT }, - {"\x1b\x1b[33^", TB_KEY_F9, TB_MOD_CTRL | TB_MOD_ALT | TB_MOD_SHIFT}, - {"\x1b[33^", TB_KEY_F9, TB_MOD_CTRL | TB_MOD_SHIFT }, - {"\x1b[33~", TB_KEY_F9, TB_MOD_SHIFT }, - - {"\x1b\x1b[21~", TB_KEY_F10, TB_MOD_ALT }, - {"\x1b\x1b[34~", TB_KEY_F10, TB_MOD_ALT | TB_MOD_SHIFT }, - {"\x1b[21^", TB_KEY_F10, TB_MOD_CTRL }, - {"\x1b\x1b[21^", TB_KEY_F10, TB_MOD_CTRL | TB_MOD_ALT }, - {"\x1b\x1b[34^", TB_KEY_F10, TB_MOD_CTRL | TB_MOD_ALT | TB_MOD_SHIFT}, - {"\x1b[34^", TB_KEY_F10, TB_MOD_CTRL | TB_MOD_SHIFT }, - {"\x1b[34~", TB_KEY_F10, TB_MOD_SHIFT }, - - {"\x1b\x1b[23~", TB_KEY_F11, TB_MOD_ALT }, - {"\x1b\x1b[23$", TB_KEY_F11, TB_MOD_ALT | TB_MOD_SHIFT }, - {"\x1b[23^", TB_KEY_F11, TB_MOD_CTRL }, - {"\x1b\x1b[23^", TB_KEY_F11, TB_MOD_CTRL | TB_MOD_ALT }, - {"\x1b\x1b[23@", TB_KEY_F11, TB_MOD_CTRL | TB_MOD_ALT | TB_MOD_SHIFT}, - {"\x1b[23@", TB_KEY_F11, TB_MOD_CTRL | TB_MOD_SHIFT }, - {"\x1b[23$", TB_KEY_F11, TB_MOD_SHIFT }, - - {"\x1b\x1b[24~", TB_KEY_F12, TB_MOD_ALT }, - {"\x1b\x1b[24$", TB_KEY_F12, TB_MOD_ALT | TB_MOD_SHIFT }, - {"\x1b[24^", TB_KEY_F12, TB_MOD_CTRL }, - {"\x1b\x1b[24^", TB_KEY_F12, TB_MOD_CTRL | TB_MOD_ALT }, - {"\x1b\x1b[24@", TB_KEY_F12, TB_MOD_CTRL | TB_MOD_ALT | TB_MOD_SHIFT}, - {"\x1b[24@", TB_KEY_F12, TB_MOD_CTRL | TB_MOD_SHIFT }, - {"\x1b[24$", TB_KEY_F12, TB_MOD_SHIFT }, - - // linux console/putty arrows - {"\x1b[A", TB_KEY_ARROW_UP, TB_MOD_SHIFT }, - {"\x1b[B", TB_KEY_ARROW_DOWN, TB_MOD_SHIFT }, - {"\x1b[C", TB_KEY_ARROW_RIGHT, TB_MOD_SHIFT }, - {"\x1b[D", TB_KEY_ARROW_LEFT, TB_MOD_SHIFT }, - - // more putty arrows - {"\x1bOA", TB_KEY_ARROW_UP, TB_MOD_CTRL }, - {"\x1b\x1bOA", TB_KEY_ARROW_UP, TB_MOD_CTRL | TB_MOD_ALT }, - {"\x1bOB", TB_KEY_ARROW_DOWN, TB_MOD_CTRL }, - {"\x1b\x1bOB", TB_KEY_ARROW_DOWN, TB_MOD_CTRL | TB_MOD_ALT }, - {"\x1bOC", TB_KEY_ARROW_RIGHT, TB_MOD_CTRL }, - {"\x1b\x1bOC", TB_KEY_ARROW_RIGHT, TB_MOD_CTRL | TB_MOD_ALT }, - {"\x1bOD", TB_KEY_ARROW_LEFT, TB_MOD_CTRL }, - {"\x1b\x1bOD", TB_KEY_ARROW_LEFT, TB_MOD_CTRL | TB_MOD_ALT }, - - {NULL, 0, 0 }, -}; - -static const unsigned char utf8_length[256] = {1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, - 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, - 3, 3, 3, 3, 4, 4, 4, 4, 4, 4, 4, 4, 5, 5, 5, 5, 6, 6, 1, 1}; - -static const unsigned char utf8_mask[6] = {0x7f, 0x1f, 0x0f, 0x07, 0x03, 0x01}; - -#ifndef TB_OPT_LIBC_WCHAR -static struct { - uint32_t range_start; - uint32_t range_end; - int width; // -1 means iswprint==0, otherwise wcwidth value (0, 1, or 2) -} wcwidth_table[] = { - // clang-format off - {0x000001, 0x00001f, -1}, {0x000020, 0x00007e, 1}, {0x00007f, 0x00009f, -1}, - {0x0000a0, 0x0002ff, 1}, {0x000300, 0x00036f, 0}, {0x000370, 0x000377, 1}, - {0x000378, 0x000379, -1}, {0x00037a, 0x00037f, 1}, {0x000380, 0x000383, -1}, - {0x000384, 0x00038a, 1}, {0x00038b, 0x00038b, -1}, {0x00038c, 0x00038c, 1}, - {0x00038d, 0x00038d, -1}, {0x00038e, 0x0003a1, 1}, {0x0003a2, 0x0003a2, -1}, - {0x0003a3, 0x000482, 1}, {0x000483, 0x000489, 0}, {0x00048a, 0x00052f, 1}, - {0x000530, 0x000530, -1}, {0x000531, 0x000556, 1}, {0x000557, 0x000558, -1}, - {0x000559, 0x00058a, 1}, {0x00058b, 0x00058c, -1}, {0x00058d, 0x00058f, 1}, - {0x000590, 0x000590, -1}, {0x000591, 0x0005bd, 0}, {0x0005be, 0x0005be, 1}, - {0x0005bf, 0x0005bf, 0}, {0x0005c0, 0x0005c0, 1}, {0x0005c1, 0x0005c2, 0}, - {0x0005c3, 0x0005c3, 1}, {0x0005c4, 0x0005c5, 0}, {0x0005c6, 0x0005c6, 1}, - {0x0005c7, 0x0005c7, 0}, {0x0005c8, 0x0005cf, -1}, {0x0005d0, 0x0005ea, 1}, - {0x0005eb, 0x0005ee, -1}, {0x0005ef, 0x0005f4, 1}, {0x0005f5, 0x0005ff, -1}, - {0x000600, 0x00060f, 1}, {0x000610, 0x00061a, 0}, {0x00061b, 0x00061b, 1}, - {0x00061c, 0x00061c, 0}, {0x00061d, 0x00064a, 1}, {0x00064b, 0x00065f, 0}, - {0x000660, 0x00066f, 1}, {0x000670, 0x000670, 0}, {0x000671, 0x0006d5, 1}, - {0x0006d6, 0x0006dc, 0}, {0x0006dd, 0x0006de, 1}, {0x0006df, 0x0006e4, 0}, - {0x0006e5, 0x0006e6, 1}, {0x0006e7, 0x0006e8, 0}, {0x0006e9, 0x0006e9, 1}, - {0x0006ea, 0x0006ed, 0}, {0x0006ee, 0x00070d, 1}, {0x00070e, 0x00070e, -1}, - {0x00070f, 0x000710, 1}, {0x000711, 0x000711, 0}, {0x000712, 0x00072f, 1}, - {0x000730, 0x00074a, 0}, {0x00074b, 0x00074c, -1}, {0x00074d, 0x0007a5, 1}, - {0x0007a6, 0x0007b0, 0}, {0x0007b1, 0x0007b1, 1}, {0x0007b2, 0x0007bf, -1}, - {0x0007c0, 0x0007ea, 1}, {0x0007eb, 0x0007f3, 0}, {0x0007f4, 0x0007fa, 1}, - {0x0007fb, 0x0007fc, -1}, {0x0007fd, 0x0007fd, 0}, {0x0007fe, 0x000815, 1}, - {0x000816, 0x000819, 0}, {0x00081a, 0x00081a, 1}, {0x00081b, 0x000823, 0}, - {0x000824, 0x000824, 1}, {0x000825, 0x000827, 0}, {0x000828, 0x000828, 1}, - {0x000829, 0x00082d, 0}, {0x00082e, 0x00082f, -1}, {0x000830, 0x00083e, 1}, - {0x00083f, 0x00083f, -1}, {0x000840, 0x000858, 1}, {0x000859, 0x00085b, 0}, - {0x00085c, 0x00085d, -1}, {0x00085e, 0x00085e, 1}, {0x00085f, 0x00085f, -1}, - {0x000860, 0x00086a, 1}, {0x00086b, 0x00086f, -1}, {0x000870, 0x00088e, 1}, - {0x00088f, 0x00088f, -1}, {0x000890, 0x000891, 1}, {0x000892, 0x000896, -1}, - {0x000897, 0x00089f, 0}, {0x0008a0, 0x0008c9, 1}, {0x0008ca, 0x0008e1, 0}, - {0x0008e2, 0x0008e2, 1}, {0x0008e3, 0x000902, 0}, {0x000903, 0x000939, 1}, - {0x00093a, 0x00093a, 0}, {0x00093b, 0x00093b, 1}, {0x00093c, 0x00093c, 0}, - {0x00093d, 0x000940, 1}, {0x000941, 0x000948, 0}, {0x000949, 0x00094c, 1}, - {0x00094d, 0x00094d, 0}, {0x00094e, 0x000950, 1}, {0x000951, 0x000957, 0}, - {0x000958, 0x000961, 1}, {0x000962, 0x000963, 0}, {0x000964, 0x000980, 1}, - {0x000981, 0x000981, 0}, {0x000982, 0x000983, 1}, {0x000984, 0x000984, -1}, - {0x000985, 0x00098c, 1}, {0x00098d, 0x00098e, -1}, {0x00098f, 0x000990, 1}, - {0x000991, 0x000992, -1}, {0x000993, 0x0009a8, 1}, {0x0009a9, 0x0009a9, -1}, - {0x0009aa, 0x0009b0, 1}, {0x0009b1, 0x0009b1, -1}, {0x0009b2, 0x0009b2, 1}, - {0x0009b3, 0x0009b5, -1}, {0x0009b6, 0x0009b9, 1}, {0x0009ba, 0x0009bb, -1}, - {0x0009bc, 0x0009bc, 0}, {0x0009bd, 0x0009c0, 1}, {0x0009c1, 0x0009c4, 0}, - {0x0009c5, 0x0009c6, -1}, {0x0009c7, 0x0009c8, 1}, {0x0009c9, 0x0009ca, -1}, - {0x0009cb, 0x0009cc, 1}, {0x0009cd, 0x0009cd, 0}, {0x0009ce, 0x0009ce, 1}, - {0x0009cf, 0x0009d6, -1}, {0x0009d7, 0x0009d7, 1}, {0x0009d8, 0x0009db, -1}, - {0x0009dc, 0x0009dd, 1}, {0x0009de, 0x0009de, -1}, {0x0009df, 0x0009e1, 1}, - {0x0009e2, 0x0009e3, 0}, {0x0009e4, 0x0009e5, -1}, {0x0009e6, 0x0009fd, 1}, - {0x0009fe, 0x0009fe, 0}, {0x0009ff, 0x000a00, -1}, {0x000a01, 0x000a02, 0}, - {0x000a03, 0x000a03, 1}, {0x000a04, 0x000a04, -1}, {0x000a05, 0x000a0a, 1}, - {0x000a0b, 0x000a0e, -1}, {0x000a0f, 0x000a10, 1}, {0x000a11, 0x000a12, -1}, - {0x000a13, 0x000a28, 1}, {0x000a29, 0x000a29, -1}, {0x000a2a, 0x000a30, 1}, - {0x000a31, 0x000a31, -1}, {0x000a32, 0x000a33, 1}, {0x000a34, 0x000a34, -1}, - {0x000a35, 0x000a36, 1}, {0x000a37, 0x000a37, -1}, {0x000a38, 0x000a39, 1}, - {0x000a3a, 0x000a3b, -1}, {0x000a3c, 0x000a3c, 0}, {0x000a3d, 0x000a3d, -1}, - {0x000a3e, 0x000a40, 1}, {0x000a41, 0x000a42, 0}, {0x000a43, 0x000a46, -1}, - {0x000a47, 0x000a48, 0}, {0x000a49, 0x000a4a, -1}, {0x000a4b, 0x000a4d, 0}, - {0x000a4e, 0x000a50, -1}, {0x000a51, 0x000a51, 0}, {0x000a52, 0x000a58, -1}, - {0x000a59, 0x000a5c, 1}, {0x000a5d, 0x000a5d, -1}, {0x000a5e, 0x000a5e, 1}, - {0x000a5f, 0x000a65, -1}, {0x000a66, 0x000a6f, 1}, {0x000a70, 0x000a71, 0}, - {0x000a72, 0x000a74, 1}, {0x000a75, 0x000a75, 0}, {0x000a76, 0x000a76, 1}, - {0x000a77, 0x000a80, -1}, {0x000a81, 0x000a82, 0}, {0x000a83, 0x000a83, 1}, - {0x000a84, 0x000a84, -1}, {0x000a85, 0x000a8d, 1}, {0x000a8e, 0x000a8e, -1}, - {0x000a8f, 0x000a91, 1}, {0x000a92, 0x000a92, -1}, {0x000a93, 0x000aa8, 1}, - {0x000aa9, 0x000aa9, -1}, {0x000aaa, 0x000ab0, 1}, {0x000ab1, 0x000ab1, -1}, - {0x000ab2, 0x000ab3, 1}, {0x000ab4, 0x000ab4, -1}, {0x000ab5, 0x000ab9, 1}, - {0x000aba, 0x000abb, -1}, {0x000abc, 0x000abc, 0}, {0x000abd, 0x000ac0, 1}, - {0x000ac1, 0x000ac5, 0}, {0x000ac6, 0x000ac6, -1}, {0x000ac7, 0x000ac8, 0}, - {0x000ac9, 0x000ac9, 1}, {0x000aca, 0x000aca, -1}, {0x000acb, 0x000acc, 1}, - {0x000acd, 0x000acd, 0}, {0x000ace, 0x000acf, -1}, {0x000ad0, 0x000ad0, 1}, - {0x000ad1, 0x000adf, -1}, {0x000ae0, 0x000ae1, 1}, {0x000ae2, 0x000ae3, 0}, - {0x000ae4, 0x000ae5, -1}, {0x000ae6, 0x000af1, 1}, {0x000af2, 0x000af8, -1}, - {0x000af9, 0x000af9, 1}, {0x000afa, 0x000aff, 0}, {0x000b00, 0x000b00, -1}, - {0x000b01, 0x000b01, 0}, {0x000b02, 0x000b03, 1}, {0x000b04, 0x000b04, -1}, - {0x000b05, 0x000b0c, 1}, {0x000b0d, 0x000b0e, -1}, {0x000b0f, 0x000b10, 1}, - {0x000b11, 0x000b12, -1}, {0x000b13, 0x000b28, 1}, {0x000b29, 0x000b29, -1}, - {0x000b2a, 0x000b30, 1}, {0x000b31, 0x000b31, -1}, {0x000b32, 0x000b33, 1}, - {0x000b34, 0x000b34, -1}, {0x000b35, 0x000b39, 1}, {0x000b3a, 0x000b3b, -1}, - {0x000b3c, 0x000b3c, 0}, {0x000b3d, 0x000b3e, 1}, {0x000b3f, 0x000b3f, 0}, - {0x000b40, 0x000b40, 1}, {0x000b41, 0x000b44, 0}, {0x000b45, 0x000b46, -1}, - {0x000b47, 0x000b48, 1}, {0x000b49, 0x000b4a, -1}, {0x000b4b, 0x000b4c, 1}, - {0x000b4d, 0x000b4d, 0}, {0x000b4e, 0x000b54, -1}, {0x000b55, 0x000b56, 0}, - {0x000b57, 0x000b57, 1}, {0x000b58, 0x000b5b, -1}, {0x000b5c, 0x000b5d, 1}, - {0x000b5e, 0x000b5e, -1}, {0x000b5f, 0x000b61, 1}, {0x000b62, 0x000b63, 0}, - {0x000b64, 0x000b65, -1}, {0x000b66, 0x000b77, 1}, {0x000b78, 0x000b81, -1}, - {0x000b82, 0x000b82, 0}, {0x000b83, 0x000b83, 1}, {0x000b84, 0x000b84, -1}, - {0x000b85, 0x000b8a, 1}, {0x000b8b, 0x000b8d, -1}, {0x000b8e, 0x000b90, 1}, - {0x000b91, 0x000b91, -1}, {0x000b92, 0x000b95, 1}, {0x000b96, 0x000b98, -1}, - {0x000b99, 0x000b9a, 1}, {0x000b9b, 0x000b9b, -1}, {0x000b9c, 0x000b9c, 1}, - {0x000b9d, 0x000b9d, -1}, {0x000b9e, 0x000b9f, 1}, {0x000ba0, 0x000ba2, -1}, - {0x000ba3, 0x000ba4, 1}, {0x000ba5, 0x000ba7, -1}, {0x000ba8, 0x000baa, 1}, - {0x000bab, 0x000bad, -1}, {0x000bae, 0x000bb9, 1}, {0x000bba, 0x000bbd, -1}, - {0x000bbe, 0x000bbf, 1}, {0x000bc0, 0x000bc0, 0}, {0x000bc1, 0x000bc2, 1}, - {0x000bc3, 0x000bc5, -1}, {0x000bc6, 0x000bc8, 1}, {0x000bc9, 0x000bc9, -1}, - {0x000bca, 0x000bcc, 1}, {0x000bcd, 0x000bcd, 0}, {0x000bce, 0x000bcf, -1}, - {0x000bd0, 0x000bd0, 1}, {0x000bd1, 0x000bd6, -1}, {0x000bd7, 0x000bd7, 1}, - {0x000bd8, 0x000be5, -1}, {0x000be6, 0x000bfa, 1}, {0x000bfb, 0x000bff, -1}, - {0x000c00, 0x000c00, 0}, {0x000c01, 0x000c03, 1}, {0x000c04, 0x000c04, 0}, - {0x000c05, 0x000c0c, 1}, {0x000c0d, 0x000c0d, -1}, {0x000c0e, 0x000c10, 1}, - {0x000c11, 0x000c11, -1}, {0x000c12, 0x000c28, 1}, {0x000c29, 0x000c29, -1}, - {0x000c2a, 0x000c39, 1}, {0x000c3a, 0x000c3b, -1}, {0x000c3c, 0x000c3c, 0}, - {0x000c3d, 0x000c3d, 1}, {0x000c3e, 0x000c40, 0}, {0x000c41, 0x000c44, 1}, - {0x000c45, 0x000c45, -1}, {0x000c46, 0x000c48, 0}, {0x000c49, 0x000c49, -1}, - {0x000c4a, 0x000c4d, 0}, {0x000c4e, 0x000c54, -1}, {0x000c55, 0x000c56, 0}, - {0x000c57, 0x000c57, -1}, {0x000c58, 0x000c5a, 1}, {0x000c5b, 0x000c5c, -1}, - {0x000c5d, 0x000c5d, 1}, {0x000c5e, 0x000c5f, -1}, {0x000c60, 0x000c61, 1}, - {0x000c62, 0x000c63, 0}, {0x000c64, 0x000c65, -1}, {0x000c66, 0x000c6f, 1}, - {0x000c70, 0x000c76, -1}, {0x000c77, 0x000c80, 1}, {0x000c81, 0x000c81, 0}, - {0x000c82, 0x000c8c, 1}, {0x000c8d, 0x000c8d, -1}, {0x000c8e, 0x000c90, 1}, - {0x000c91, 0x000c91, -1}, {0x000c92, 0x000ca8, 1}, {0x000ca9, 0x000ca9, -1}, - {0x000caa, 0x000cb3, 1}, {0x000cb4, 0x000cb4, -1}, {0x000cb5, 0x000cb9, 1}, - {0x000cba, 0x000cbb, -1}, {0x000cbc, 0x000cbc, 0}, {0x000cbd, 0x000cbe, 1}, - {0x000cbf, 0x000cbf, 0}, {0x000cc0, 0x000cc4, 1}, {0x000cc5, 0x000cc5, -1}, - {0x000cc6, 0x000cc6, 0}, {0x000cc7, 0x000cc8, 1}, {0x000cc9, 0x000cc9, -1}, - {0x000cca, 0x000ccb, 1}, {0x000ccc, 0x000ccd, 0}, {0x000cce, 0x000cd4, -1}, - {0x000cd5, 0x000cd6, 1}, {0x000cd7, 0x000cdc, -1}, {0x000cdd, 0x000cde, 1}, - {0x000cdf, 0x000cdf, -1}, {0x000ce0, 0x000ce1, 1}, {0x000ce2, 0x000ce3, 0}, - {0x000ce4, 0x000ce5, -1}, {0x000ce6, 0x000cef, 1}, {0x000cf0, 0x000cf0, -1}, - {0x000cf1, 0x000cf3, 1}, {0x000cf4, 0x000cff, -1}, {0x000d00, 0x000d01, 0}, - {0x000d02, 0x000d0c, 1}, {0x000d0d, 0x000d0d, -1}, {0x000d0e, 0x000d10, 1}, - {0x000d11, 0x000d11, -1}, {0x000d12, 0x000d3a, 1}, {0x000d3b, 0x000d3c, 0}, - {0x000d3d, 0x000d40, 1}, {0x000d41, 0x000d44, 0}, {0x000d45, 0x000d45, -1}, - {0x000d46, 0x000d48, 1}, {0x000d49, 0x000d49, -1}, {0x000d4a, 0x000d4c, 1}, - {0x000d4d, 0x000d4d, 0}, {0x000d4e, 0x000d4f, 1}, {0x000d50, 0x000d53, -1}, - {0x000d54, 0x000d61, 1}, {0x000d62, 0x000d63, 0}, {0x000d64, 0x000d65, -1}, - {0x000d66, 0x000d7f, 1}, {0x000d80, 0x000d80, -1}, {0x000d81, 0x000d81, 0}, - {0x000d82, 0x000d83, 1}, {0x000d84, 0x000d84, -1}, {0x000d85, 0x000d96, 1}, - {0x000d97, 0x000d99, -1}, {0x000d9a, 0x000db1, 1}, {0x000db2, 0x000db2, -1}, - {0x000db3, 0x000dbb, 1}, {0x000dbc, 0x000dbc, -1}, {0x000dbd, 0x000dbd, 1}, - {0x000dbe, 0x000dbf, -1}, {0x000dc0, 0x000dc6, 1}, {0x000dc7, 0x000dc9, -1}, - {0x000dca, 0x000dca, 0}, {0x000dcb, 0x000dce, -1}, {0x000dcf, 0x000dd1, 1}, - {0x000dd2, 0x000dd4, 0}, {0x000dd5, 0x000dd5, -1}, {0x000dd6, 0x000dd6, 0}, - {0x000dd7, 0x000dd7, -1}, {0x000dd8, 0x000ddf, 1}, {0x000de0, 0x000de5, -1}, - {0x000de6, 0x000def, 1}, {0x000df0, 0x000df1, -1}, {0x000df2, 0x000df4, 1}, - {0x000df5, 0x000e00, -1}, {0x000e01, 0x000e30, 1}, {0x000e31, 0x000e31, 0}, - {0x000e32, 0x000e33, 1}, {0x000e34, 0x000e3a, 0}, {0x000e3b, 0x000e3e, -1}, - {0x000e3f, 0x000e46, 1}, {0x000e47, 0x000e4e, 0}, {0x000e4f, 0x000e5b, 1}, - {0x000e5c, 0x000e80, -1}, {0x000e81, 0x000e82, 1}, {0x000e83, 0x000e83, -1}, - {0x000e84, 0x000e84, 1}, {0x000e85, 0x000e85, -1}, {0x000e86, 0x000e8a, 1}, - {0x000e8b, 0x000e8b, -1}, {0x000e8c, 0x000ea3, 1}, {0x000ea4, 0x000ea4, -1}, - {0x000ea5, 0x000ea5, 1}, {0x000ea6, 0x000ea6, -1}, {0x000ea7, 0x000eb0, 1}, - {0x000eb1, 0x000eb1, 0}, {0x000eb2, 0x000eb3, 1}, {0x000eb4, 0x000ebc, 0}, - {0x000ebd, 0x000ebd, 1}, {0x000ebe, 0x000ebf, -1}, {0x000ec0, 0x000ec4, 1}, - {0x000ec5, 0x000ec5, -1}, {0x000ec6, 0x000ec6, 1}, {0x000ec7, 0x000ec7, -1}, - {0x000ec8, 0x000ece, 0}, {0x000ecf, 0x000ecf, -1}, {0x000ed0, 0x000ed9, 1}, - {0x000eda, 0x000edb, -1}, {0x000edc, 0x000edf, 1}, {0x000ee0, 0x000eff, -1}, - {0x000f00, 0x000f17, 1}, {0x000f18, 0x000f19, 0}, {0x000f1a, 0x000f34, 1}, - {0x000f35, 0x000f35, 0}, {0x000f36, 0x000f36, 1}, {0x000f37, 0x000f37, 0}, - {0x000f38, 0x000f38, 1}, {0x000f39, 0x000f39, 0}, {0x000f3a, 0x000f47, 1}, - {0x000f48, 0x000f48, -1}, {0x000f49, 0x000f6c, 1}, {0x000f6d, 0x000f70, -1}, - {0x000f71, 0x000f7e, 0}, {0x000f7f, 0x000f7f, 1}, {0x000f80, 0x000f84, 0}, - {0x000f85, 0x000f85, 1}, {0x000f86, 0x000f87, 0}, {0x000f88, 0x000f8c, 1}, - {0x000f8d, 0x000f97, 0}, {0x000f98, 0x000f98, -1}, {0x000f99, 0x000fbc, 0}, - {0x000fbd, 0x000fbd, -1}, {0x000fbe, 0x000fc5, 1}, {0x000fc6, 0x000fc6, 0}, - {0x000fc7, 0x000fcc, 1}, {0x000fcd, 0x000fcd, -1}, {0x000fce, 0x000fda, 1}, - {0x000fdb, 0x000fff, -1}, {0x001000, 0x00102c, 1}, {0x00102d, 0x001030, 0}, - {0x001031, 0x001031, 1}, {0x001032, 0x001037, 0}, {0x001038, 0x001038, 1}, - {0x001039, 0x00103a, 0}, {0x00103b, 0x00103c, 1}, {0x00103d, 0x00103e, 0}, - {0x00103f, 0x001057, 1}, {0x001058, 0x001059, 0}, {0x00105a, 0x00105d, 1}, - {0x00105e, 0x001060, 0}, {0x001061, 0x001070, 1}, {0x001071, 0x001074, 0}, - {0x001075, 0x001081, 1}, {0x001082, 0x001082, 0}, {0x001083, 0x001084, 1}, - {0x001085, 0x001086, 0}, {0x001087, 0x00108c, 1}, {0x00108d, 0x00108d, 0}, - {0x00108e, 0x00109c, 1}, {0x00109d, 0x00109d, 0}, {0x00109e, 0x0010c5, 1}, - {0x0010c6, 0x0010c6, -1}, {0x0010c7, 0x0010c7, 1}, {0x0010c8, 0x0010cc, -1}, - {0x0010cd, 0x0010cd, 1}, {0x0010ce, 0x0010cf, -1}, {0x0010d0, 0x0010ff, 1}, - {0x001100, 0x00115f, 2}, {0x001160, 0x0011ff, 0}, {0x001200, 0x001248, 1}, - {0x001249, 0x001249, -1}, {0x00124a, 0x00124d, 1}, {0x00124e, 0x00124f, -1}, - {0x001250, 0x001256, 1}, {0x001257, 0x001257, -1}, {0x001258, 0x001258, 1}, - {0x001259, 0x001259, -1}, {0x00125a, 0x00125d, 1}, {0x00125e, 0x00125f, -1}, - {0x001260, 0x001288, 1}, {0x001289, 0x001289, -1}, {0x00128a, 0x00128d, 1}, - {0x00128e, 0x00128f, -1}, {0x001290, 0x0012b0, 1}, {0x0012b1, 0x0012b1, -1}, - {0x0012b2, 0x0012b5, 1}, {0x0012b6, 0x0012b7, -1}, {0x0012b8, 0x0012be, 1}, - {0x0012bf, 0x0012bf, -1}, {0x0012c0, 0x0012c0, 1}, {0x0012c1, 0x0012c1, -1}, - {0x0012c2, 0x0012c5, 1}, {0x0012c6, 0x0012c7, -1}, {0x0012c8, 0x0012d6, 1}, - {0x0012d7, 0x0012d7, -1}, {0x0012d8, 0x001310, 1}, {0x001311, 0x001311, -1}, - {0x001312, 0x001315, 1}, {0x001316, 0x001317, -1}, {0x001318, 0x00135a, 1}, - {0x00135b, 0x00135c, -1}, {0x00135d, 0x00135f, 0}, {0x001360, 0x00137c, 1}, - {0x00137d, 0x00137f, -1}, {0x001380, 0x001399, 1}, {0x00139a, 0x00139f, -1}, - {0x0013a0, 0x0013f5, 1}, {0x0013f6, 0x0013f7, -1}, {0x0013f8, 0x0013fd, 1}, - {0x0013fe, 0x0013ff, -1}, {0x001400, 0x00169c, 1}, {0x00169d, 0x00169f, -1}, - {0x0016a0, 0x0016f8, 1}, {0x0016f9, 0x0016ff, -1}, {0x001700, 0x001711, 1}, - {0x001712, 0x001714, 0}, {0x001715, 0x001715, 1}, {0x001716, 0x00171e, -1}, - {0x00171f, 0x001731, 1}, {0x001732, 0x001733, 0}, {0x001734, 0x001736, 1}, - {0x001737, 0x00173f, -1}, {0x001740, 0x001751, 1}, {0x001752, 0x001753, 0}, - {0x001754, 0x00175f, -1}, {0x001760, 0x00176c, 1}, {0x00176d, 0x00176d, -1}, - {0x00176e, 0x001770, 1}, {0x001771, 0x001771, -1}, {0x001772, 0x001773, 0}, - {0x001774, 0x00177f, -1}, {0x001780, 0x0017b3, 1}, {0x0017b4, 0x0017b5, 0}, - {0x0017b6, 0x0017b6, 1}, {0x0017b7, 0x0017bd, 0}, {0x0017be, 0x0017c5, 1}, - {0x0017c6, 0x0017c6, 0}, {0x0017c7, 0x0017c8, 1}, {0x0017c9, 0x0017d3, 0}, - {0x0017d4, 0x0017dc, 1}, {0x0017dd, 0x0017dd, 0}, {0x0017de, 0x0017df, -1}, - {0x0017e0, 0x0017e9, 1}, {0x0017ea, 0x0017ef, -1}, {0x0017f0, 0x0017f9, 1}, - {0x0017fa, 0x0017ff, -1}, {0x001800, 0x00180a, 1}, {0x00180b, 0x00180f, 0}, - {0x001810, 0x001819, 1}, {0x00181a, 0x00181f, -1}, {0x001820, 0x001878, 1}, - {0x001879, 0x00187f, -1}, {0x001880, 0x001884, 1}, {0x001885, 0x001886, 0}, - {0x001887, 0x0018a8, 1}, {0x0018a9, 0x0018a9, 0}, {0x0018aa, 0x0018aa, 1}, - {0x0018ab, 0x0018af, -1}, {0x0018b0, 0x0018f5, 1}, {0x0018f6, 0x0018ff, -1}, - {0x001900, 0x00191e, 1}, {0x00191f, 0x00191f, -1}, {0x001920, 0x001922, 0}, - {0x001923, 0x001926, 1}, {0x001927, 0x001928, 0}, {0x001929, 0x00192b, 1}, - {0x00192c, 0x00192f, -1}, {0x001930, 0x001931, 1}, {0x001932, 0x001932, 0}, - {0x001933, 0x001938, 1}, {0x001939, 0x00193b, 0}, {0x00193c, 0x00193f, -1}, - {0x001940, 0x001940, 1}, {0x001941, 0x001943, -1}, {0x001944, 0x00196d, 1}, - {0x00196e, 0x00196f, -1}, {0x001970, 0x001974, 1}, {0x001975, 0x00197f, -1}, - {0x001980, 0x0019ab, 1}, {0x0019ac, 0x0019af, -1}, {0x0019b0, 0x0019c9, 1}, - {0x0019ca, 0x0019cf, -1}, {0x0019d0, 0x0019da, 1}, {0x0019db, 0x0019dd, -1}, - {0x0019de, 0x001a16, 1}, {0x001a17, 0x001a18, 0}, {0x001a19, 0x001a1a, 1}, - {0x001a1b, 0x001a1b, 0}, {0x001a1c, 0x001a1d, -1}, {0x001a1e, 0x001a55, 1}, - {0x001a56, 0x001a56, 0}, {0x001a57, 0x001a57, 1}, {0x001a58, 0x001a5e, 0}, - {0x001a5f, 0x001a5f, -1}, {0x001a60, 0x001a60, 0}, {0x001a61, 0x001a61, 1}, - {0x001a62, 0x001a62, 0}, {0x001a63, 0x001a64, 1}, {0x001a65, 0x001a6c, 0}, - {0x001a6d, 0x001a72, 1}, {0x001a73, 0x001a7c, 0}, {0x001a7d, 0x001a7e, -1}, - {0x001a7f, 0x001a7f, 0}, {0x001a80, 0x001a89, 1}, {0x001a8a, 0x001a8f, -1}, - {0x001a90, 0x001a99, 1}, {0x001a9a, 0x001a9f, -1}, {0x001aa0, 0x001aad, 1}, - {0x001aae, 0x001aaf, -1}, {0x001ab0, 0x001ace, 0}, {0x001acf, 0x001aff, -1}, - {0x001b00, 0x001b03, 0}, {0x001b04, 0x001b33, 1}, {0x001b34, 0x001b34, 0}, - {0x001b35, 0x001b35, 1}, {0x001b36, 0x001b3a, 0}, {0x001b3b, 0x001b3b, 1}, - {0x001b3c, 0x001b3c, 0}, {0x001b3d, 0x001b41, 1}, {0x001b42, 0x001b42, 0}, - {0x001b43, 0x001b4c, 1}, {0x001b4d, 0x001b4d, -1}, {0x001b4e, 0x001b6a, 1}, - {0x001b6b, 0x001b73, 0}, {0x001b74, 0x001b7f, 1}, {0x001b80, 0x001b81, 0}, - {0x001b82, 0x001ba1, 1}, {0x001ba2, 0x001ba5, 0}, {0x001ba6, 0x001ba7, 1}, - {0x001ba8, 0x001ba9, 0}, {0x001baa, 0x001baa, 1}, {0x001bab, 0x001bad, 0}, - {0x001bae, 0x001be5, 1}, {0x001be6, 0x001be6, 0}, {0x001be7, 0x001be7, 1}, - {0x001be8, 0x001be9, 0}, {0x001bea, 0x001bec, 1}, {0x001bed, 0x001bed, 0}, - {0x001bee, 0x001bee, 1}, {0x001bef, 0x001bf1, 0}, {0x001bf2, 0x001bf3, 1}, - {0x001bf4, 0x001bfb, -1}, {0x001bfc, 0x001c2b, 1}, {0x001c2c, 0x001c33, 0}, - {0x001c34, 0x001c35, 1}, {0x001c36, 0x001c37, 0}, {0x001c38, 0x001c3a, -1}, - {0x001c3b, 0x001c49, 1}, {0x001c4a, 0x001c4c, -1}, {0x001c4d, 0x001c8a, 1}, - {0x001c8b, 0x001c8f, -1}, {0x001c90, 0x001cba, 1}, {0x001cbb, 0x001cbc, -1}, - {0x001cbd, 0x001cc7, 1}, {0x001cc8, 0x001ccf, -1}, {0x001cd0, 0x001cd2, 0}, - {0x001cd3, 0x001cd3, 1}, {0x001cd4, 0x001ce0, 0}, {0x001ce1, 0x001ce1, 1}, - {0x001ce2, 0x001ce8, 0}, {0x001ce9, 0x001cec, 1}, {0x001ced, 0x001ced, 0}, - {0x001cee, 0x001cf3, 1}, {0x001cf4, 0x001cf4, 0}, {0x001cf5, 0x001cf7, 1}, - {0x001cf8, 0x001cf9, 0}, {0x001cfa, 0x001cfa, 1}, {0x001cfb, 0x001cff, -1}, - {0x001d00, 0x001dbf, 1}, {0x001dc0, 0x001dff, 0}, {0x001e00, 0x001f15, 1}, - {0x001f16, 0x001f17, -1}, {0x001f18, 0x001f1d, 1}, {0x001f1e, 0x001f1f, -1}, - {0x001f20, 0x001f45, 1}, {0x001f46, 0x001f47, -1}, {0x001f48, 0x001f4d, 1}, - {0x001f4e, 0x001f4f, -1}, {0x001f50, 0x001f57, 1}, {0x001f58, 0x001f58, -1}, - {0x001f59, 0x001f59, 1}, {0x001f5a, 0x001f5a, -1}, {0x001f5b, 0x001f5b, 1}, - {0x001f5c, 0x001f5c, -1}, {0x001f5d, 0x001f5d, 1}, {0x001f5e, 0x001f5e, -1}, - {0x001f5f, 0x001f7d, 1}, {0x001f7e, 0x001f7f, -1}, {0x001f80, 0x001fb4, 1}, - {0x001fb5, 0x001fb5, -1}, {0x001fb6, 0x001fc4, 1}, {0x001fc5, 0x001fc5, -1}, - {0x001fc6, 0x001fd3, 1}, {0x001fd4, 0x001fd5, -1}, {0x001fd6, 0x001fdb, 1}, - {0x001fdc, 0x001fdc, -1}, {0x001fdd, 0x001fef, 1}, {0x001ff0, 0x001ff1, -1}, - {0x001ff2, 0x001ff4, 1}, {0x001ff5, 0x001ff5, -1}, {0x001ff6, 0x001ffe, 1}, - {0x001fff, 0x001fff, -1}, {0x002000, 0x00200a, 1}, {0x00200b, 0x00200f, 0}, - {0x002010, 0x002027, 1}, {0x002028, 0x002029, -1}, {0x00202a, 0x00202e, 0}, - {0x00202f, 0x00205f, 1}, {0x002060, 0x002064, 0}, {0x002065, 0x002065, -1}, - {0x002066, 0x00206f, 0}, {0x002070, 0x002071, 1}, {0x002072, 0x002073, -1}, - {0x002074, 0x00208e, 1}, {0x00208f, 0x00208f, -1}, {0x002090, 0x00209c, 1}, - {0x00209d, 0x00209f, -1}, {0x0020a0, 0x0020c0, 1}, {0x0020c1, 0x0020cf, -1}, - {0x0020d0, 0x0020f0, 0}, {0x0020f1, 0x0020ff, -1}, {0x002100, 0x00218b, 1}, - {0x00218c, 0x00218f, -1}, {0x002190, 0x002319, 1}, {0x00231a, 0x00231b, 2}, - {0x00231c, 0x002328, 1}, {0x002329, 0x00232a, 2}, {0x00232b, 0x0023e8, 1}, - {0x0023e9, 0x0023ec, 2}, {0x0023ed, 0x0023ef, 1}, {0x0023f0, 0x0023f0, 2}, - {0x0023f1, 0x0023f2, 1}, {0x0023f3, 0x0023f3, 2}, {0x0023f4, 0x002429, 1}, - {0x00242a, 0x00243f, -1}, {0x002440, 0x00244a, 1}, {0x00244b, 0x00245f, -1}, - {0x002460, 0x0025fc, 1}, {0x0025fd, 0x0025fe, 2}, {0x0025ff, 0x002613, 1}, - {0x002614, 0x002615, 2}, {0x002616, 0x00262f, 1}, {0x002630, 0x002637, 2}, - {0x002638, 0x002647, 1}, {0x002648, 0x002653, 2}, {0x002654, 0x00267e, 1}, - {0x00267f, 0x00267f, 2}, {0x002680, 0x002689, 1}, {0x00268a, 0x00268f, 2}, - {0x002690, 0x002692, 1}, {0x002693, 0x002693, 2}, {0x002694, 0x0026a0, 1}, - {0x0026a1, 0x0026a1, 2}, {0x0026a2, 0x0026a9, 1}, {0x0026aa, 0x0026ab, 2}, - {0x0026ac, 0x0026bc, 1}, {0x0026bd, 0x0026be, 2}, {0x0026bf, 0x0026c3, 1}, - {0x0026c4, 0x0026c5, 2}, {0x0026c6, 0x0026cd, 1}, {0x0026ce, 0x0026ce, 2}, - {0x0026cf, 0x0026d3, 1}, {0x0026d4, 0x0026d4, 2}, {0x0026d5, 0x0026e9, 1}, - {0x0026ea, 0x0026ea, 2}, {0x0026eb, 0x0026f1, 1}, {0x0026f2, 0x0026f3, 2}, - {0x0026f4, 0x0026f4, 1}, {0x0026f5, 0x0026f5, 2}, {0x0026f6, 0x0026f9, 1}, - {0x0026fa, 0x0026fa, 2}, {0x0026fb, 0x0026fc, 1}, {0x0026fd, 0x0026fd, 2}, - {0x0026fe, 0x002704, 1}, {0x002705, 0x002705, 2}, {0x002706, 0x002709, 1}, - {0x00270a, 0x00270b, 2}, {0x00270c, 0x002727, 1}, {0x002728, 0x002728, 2}, - {0x002729, 0x00274b, 1}, {0x00274c, 0x00274c, 2}, {0x00274d, 0x00274d, 1}, - {0x00274e, 0x00274e, 2}, {0x00274f, 0x002752, 1}, {0x002753, 0x002755, 2}, - {0x002756, 0x002756, 1}, {0x002757, 0x002757, 2}, {0x002758, 0x002794, 1}, - {0x002795, 0x002797, 2}, {0x002798, 0x0027af, 1}, {0x0027b0, 0x0027b0, 2}, - {0x0027b1, 0x0027be, 1}, {0x0027bf, 0x0027bf, 2}, {0x0027c0, 0x002b1a, 1}, - {0x002b1b, 0x002b1c, 2}, {0x002b1d, 0x002b4f, 1}, {0x002b50, 0x002b50, 2}, - {0x002b51, 0x002b54, 1}, {0x002b55, 0x002b55, 2}, {0x002b56, 0x002b73, 1}, - {0x002b74, 0x002b75, -1}, {0x002b76, 0x002b95, 1}, {0x002b96, 0x002b96, -1}, - {0x002b97, 0x002cee, 1}, {0x002cef, 0x002cf1, 0}, {0x002cf2, 0x002cf3, 1}, - {0x002cf4, 0x002cf8, -1}, {0x002cf9, 0x002d25, 1}, {0x002d26, 0x002d26, -1}, - {0x002d27, 0x002d27, 1}, {0x002d28, 0x002d2c, -1}, {0x002d2d, 0x002d2d, 1}, - {0x002d2e, 0x002d2f, -1}, {0x002d30, 0x002d67, 1}, {0x002d68, 0x002d6e, -1}, - {0x002d6f, 0x002d70, 1}, {0x002d71, 0x002d7e, -1}, {0x002d7f, 0x002d7f, 0}, - {0x002d80, 0x002d96, 1}, {0x002d97, 0x002d9f, -1}, {0x002da0, 0x002da6, 1}, - {0x002da7, 0x002da7, -1}, {0x002da8, 0x002dae, 1}, {0x002daf, 0x002daf, -1}, - {0x002db0, 0x002db6, 1}, {0x002db7, 0x002db7, -1}, {0x002db8, 0x002dbe, 1}, - {0x002dbf, 0x002dbf, -1}, {0x002dc0, 0x002dc6, 1}, {0x002dc7, 0x002dc7, -1}, - {0x002dc8, 0x002dce, 1}, {0x002dcf, 0x002dcf, -1}, {0x002dd0, 0x002dd6, 1}, - {0x002dd7, 0x002dd7, -1}, {0x002dd8, 0x002dde, 1}, {0x002ddf, 0x002ddf, -1}, - {0x002de0, 0x002dff, 0}, {0x002e00, 0x002e5d, 1}, {0x002e5e, 0x002e7f, -1}, - {0x002e80, 0x002e99, 2}, {0x002e9a, 0x002e9a, -1}, {0x002e9b, 0x002ef3, 2}, - {0x002ef4, 0x002eff, -1}, {0x002f00, 0x002fd5, 2}, {0x002fd6, 0x002fef, -1}, - {0x002ff0, 0x003029, 2}, {0x00302a, 0x00302d, 0}, {0x00302e, 0x00303e, 2}, - {0x00303f, 0x00303f, 1}, {0x003040, 0x003040, -1}, {0x003041, 0x003096, 2}, - {0x003097, 0x003098, -1}, {0x003099, 0x00309a, 0}, {0x00309b, 0x0030ff, 2}, - {0x003100, 0x003104, -1}, {0x003105, 0x00312f, 2}, {0x003130, 0x003130, -1}, - {0x003131, 0x003163, 2}, {0x003164, 0x003164, 0}, {0x003165, 0x00318e, 2}, - {0x00318f, 0x00318f, -1}, {0x003190, 0x0031e5, 2}, {0x0031e6, 0x0031ee, -1}, - {0x0031ef, 0x00321e, 2}, {0x00321f, 0x00321f, -1}, {0x003220, 0x00a48c, 2}, - {0x00a48d, 0x00a48f, -1}, {0x00a490, 0x00a4c6, 2}, {0x00a4c7, 0x00a4cf, -1}, - {0x00a4d0, 0x00a62b, 1}, {0x00a62c, 0x00a63f, -1}, {0x00a640, 0x00a66e, 1}, - {0x00a66f, 0x00a672, 0}, {0x00a673, 0x00a673, 1}, {0x00a674, 0x00a67d, 0}, - {0x00a67e, 0x00a69d, 1}, {0x00a69e, 0x00a69f, 0}, {0x00a6a0, 0x00a6ef, 1}, - {0x00a6f0, 0x00a6f1, 0}, {0x00a6f2, 0x00a6f7, 1}, {0x00a6f8, 0x00a6ff, -1}, - {0x00a700, 0x00a7cd, 1}, {0x00a7ce, 0x00a7cf, -1}, {0x00a7d0, 0x00a7d1, 1}, - {0x00a7d2, 0x00a7d2, -1}, {0x00a7d3, 0x00a7d3, 1}, {0x00a7d4, 0x00a7d4, -1}, - {0x00a7d5, 0x00a7dc, 1}, {0x00a7dd, 0x00a7f1, -1}, {0x00a7f2, 0x00a801, 1}, - {0x00a802, 0x00a802, 0}, {0x00a803, 0x00a805, 1}, {0x00a806, 0x00a806, 0}, - {0x00a807, 0x00a80a, 1}, {0x00a80b, 0x00a80b, 0}, {0x00a80c, 0x00a824, 1}, - {0x00a825, 0x00a826, 0}, {0x00a827, 0x00a82b, 1}, {0x00a82c, 0x00a82c, 0}, - {0x00a82d, 0x00a82f, -1}, {0x00a830, 0x00a839, 1}, {0x00a83a, 0x00a83f, -1}, - {0x00a840, 0x00a877, 1}, {0x00a878, 0x00a87f, -1}, {0x00a880, 0x00a8c3, 1}, - {0x00a8c4, 0x00a8c5, 0}, {0x00a8c6, 0x00a8cd, -1}, {0x00a8ce, 0x00a8d9, 1}, - {0x00a8da, 0x00a8df, -1}, {0x00a8e0, 0x00a8f1, 0}, {0x00a8f2, 0x00a8fe, 1}, - {0x00a8ff, 0x00a8ff, 0}, {0x00a900, 0x00a925, 1}, {0x00a926, 0x00a92d, 0}, - {0x00a92e, 0x00a946, 1}, {0x00a947, 0x00a951, 0}, {0x00a952, 0x00a953, 1}, - {0x00a954, 0x00a95e, -1}, {0x00a95f, 0x00a95f, 1}, {0x00a960, 0x00a97c, 2}, - {0x00a97d, 0x00a97f, -1}, {0x00a980, 0x00a982, 0}, {0x00a983, 0x00a9b2, 1}, - {0x00a9b3, 0x00a9b3, 0}, {0x00a9b4, 0x00a9b5, 1}, {0x00a9b6, 0x00a9b9, 0}, - {0x00a9ba, 0x00a9bb, 1}, {0x00a9bc, 0x00a9bd, 0}, {0x00a9be, 0x00a9cd, 1}, - {0x00a9ce, 0x00a9ce, -1}, {0x00a9cf, 0x00a9d9, 1}, {0x00a9da, 0x00a9dd, -1}, - {0x00a9de, 0x00a9e4, 1}, {0x00a9e5, 0x00a9e5, 0}, {0x00a9e6, 0x00a9fe, 1}, - {0x00a9ff, 0x00a9ff, -1}, {0x00aa00, 0x00aa28, 1}, {0x00aa29, 0x00aa2e, 0}, - {0x00aa2f, 0x00aa30, 1}, {0x00aa31, 0x00aa32, 0}, {0x00aa33, 0x00aa34, 1}, - {0x00aa35, 0x00aa36, 0}, {0x00aa37, 0x00aa3f, -1}, {0x00aa40, 0x00aa42, 1}, - {0x00aa43, 0x00aa43, 0}, {0x00aa44, 0x00aa4b, 1}, {0x00aa4c, 0x00aa4c, 0}, - {0x00aa4d, 0x00aa4d, 1}, {0x00aa4e, 0x00aa4f, -1}, {0x00aa50, 0x00aa59, 1}, - {0x00aa5a, 0x00aa5b, -1}, {0x00aa5c, 0x00aa7b, 1}, {0x00aa7c, 0x00aa7c, 0}, - {0x00aa7d, 0x00aaaf, 1}, {0x00aab0, 0x00aab0, 0}, {0x00aab1, 0x00aab1, 1}, - {0x00aab2, 0x00aab4, 0}, {0x00aab5, 0x00aab6, 1}, {0x00aab7, 0x00aab8, 0}, - {0x00aab9, 0x00aabd, 1}, {0x00aabe, 0x00aabf, 0}, {0x00aac0, 0x00aac0, 1}, - {0x00aac1, 0x00aac1, 0}, {0x00aac2, 0x00aac2, 1}, {0x00aac3, 0x00aada, -1}, - {0x00aadb, 0x00aaeb, 1}, {0x00aaec, 0x00aaed, 0}, {0x00aaee, 0x00aaf5, 1}, - {0x00aaf6, 0x00aaf6, 0}, {0x00aaf7, 0x00ab00, -1}, {0x00ab01, 0x00ab06, 1}, - {0x00ab07, 0x00ab08, -1}, {0x00ab09, 0x00ab0e, 1}, {0x00ab0f, 0x00ab10, -1}, - {0x00ab11, 0x00ab16, 1}, {0x00ab17, 0x00ab1f, -1}, {0x00ab20, 0x00ab26, 1}, - {0x00ab27, 0x00ab27, -1}, {0x00ab28, 0x00ab2e, 1}, {0x00ab2f, 0x00ab2f, -1}, - {0x00ab30, 0x00ab6b, 1}, {0x00ab6c, 0x00ab6f, -1}, {0x00ab70, 0x00abe4, 1}, - {0x00abe5, 0x00abe5, 0}, {0x00abe6, 0x00abe7, 1}, {0x00abe8, 0x00abe8, 0}, - {0x00abe9, 0x00abec, 1}, {0x00abed, 0x00abed, 0}, {0x00abee, 0x00abef, -1}, - {0x00abf0, 0x00abf9, 1}, {0x00abfa, 0x00abff, -1}, {0x00ac00, 0x00d7a3, 2}, - {0x00d7a4, 0x00d7af, -1}, {0x00d7b0, 0x00d7c6, 0}, {0x00d7c7, 0x00d7ca, -1}, - {0x00d7cb, 0x00d7fb, 0}, {0x00d7fc, 0x00dfff, -1}, {0x00e000, 0x00f8ff, 1}, - {0x00f900, 0x00fa6d, 2}, {0x00fa6e, 0x00fa6f, -1}, {0x00fa70, 0x00fad9, 2}, - {0x00fada, 0x00faff, -1}, {0x00fb00, 0x00fb06, 1}, {0x00fb07, 0x00fb12, -1}, - {0x00fb13, 0x00fb17, 1}, {0x00fb18, 0x00fb1c, -1}, {0x00fb1d, 0x00fb1d, 1}, - {0x00fb1e, 0x00fb1e, 0}, {0x00fb1f, 0x00fb36, 1}, {0x00fb37, 0x00fb37, -1}, - {0x00fb38, 0x00fb3c, 1}, {0x00fb3d, 0x00fb3d, -1}, {0x00fb3e, 0x00fb3e, 1}, - {0x00fb3f, 0x00fb3f, -1}, {0x00fb40, 0x00fb41, 1}, {0x00fb42, 0x00fb42, -1}, - {0x00fb43, 0x00fb44, 1}, {0x00fb45, 0x00fb45, -1}, {0x00fb46, 0x00fbc2, 1}, - {0x00fbc3, 0x00fbd2, -1}, {0x00fbd3, 0x00fd8f, 1}, {0x00fd90, 0x00fd91, -1}, - {0x00fd92, 0x00fdc7, 1}, {0x00fdc8, 0x00fdce, -1}, {0x00fdcf, 0x00fdcf, 1}, - {0x00fdd0, 0x00fdef, -1}, {0x00fdf0, 0x00fdff, 1}, {0x00fe00, 0x00fe0f, 0}, - {0x00fe10, 0x00fe19, 2}, {0x00fe1a, 0x00fe1f, -1}, {0x00fe20, 0x00fe2f, 0}, - {0x00fe30, 0x00fe52, 2}, {0x00fe53, 0x00fe53, -1}, {0x00fe54, 0x00fe66, 2}, - {0x00fe67, 0x00fe67, -1}, {0x00fe68, 0x00fe6b, 2}, {0x00fe6c, 0x00fe6f, -1}, - {0x00fe70, 0x00fe74, 1}, {0x00fe75, 0x00fe75, -1}, {0x00fe76, 0x00fefc, 1}, - {0x00fefd, 0x00fefe, -1}, {0x00feff, 0x00feff, 0}, {0x00ff00, 0x00ff00, -1}, - {0x00ff01, 0x00ff60, 2}, {0x00ff61, 0x00ff9f, 1}, {0x00ffa0, 0x00ffa0, 0}, - {0x00ffa1, 0x00ffbe, 1}, {0x00ffbf, 0x00ffc1, -1}, {0x00ffc2, 0x00ffc7, 1}, - {0x00ffc8, 0x00ffc9, -1}, {0x00ffca, 0x00ffcf, 1}, {0x00ffd0, 0x00ffd1, -1}, - {0x00ffd2, 0x00ffd7, 1}, {0x00ffd8, 0x00ffd9, -1}, {0x00ffda, 0x00ffdc, 1}, - {0x00ffdd, 0x00ffdf, -1}, {0x00ffe0, 0x00ffe6, 2}, {0x00ffe7, 0x00ffe7, -1}, - {0x00ffe8, 0x00ffee, 1}, {0x00ffef, 0x00fff8, -1}, {0x00fff9, 0x00fffd, 1}, - {0x00fffe, 0x00ffff, -1}, {0x010000, 0x01000b, 1}, {0x01000c, 0x01000c, -1}, - {0x01000d, 0x010026, 1}, {0x010027, 0x010027, -1}, {0x010028, 0x01003a, 1}, - {0x01003b, 0x01003b, -1}, {0x01003c, 0x01003d, 1}, {0x01003e, 0x01003e, -1}, - {0x01003f, 0x01004d, 1}, {0x01004e, 0x01004f, -1}, {0x010050, 0x01005d, 1}, - {0x01005e, 0x01007f, -1}, {0x010080, 0x0100fa, 1}, {0x0100fb, 0x0100ff, -1}, - {0x010100, 0x010102, 1}, {0x010103, 0x010106, -1}, {0x010107, 0x010133, 1}, - {0x010134, 0x010136, -1}, {0x010137, 0x01018e, 1}, {0x01018f, 0x01018f, -1}, - {0x010190, 0x01019c, 1}, {0x01019d, 0x01019f, -1}, {0x0101a0, 0x0101a0, 1}, - {0x0101a1, 0x0101cf, -1}, {0x0101d0, 0x0101fc, 1}, {0x0101fd, 0x0101fd, 0}, - {0x0101fe, 0x01027f, -1}, {0x010280, 0x01029c, 1}, {0x01029d, 0x01029f, -1}, - {0x0102a0, 0x0102d0, 1}, {0x0102d1, 0x0102df, -1}, {0x0102e0, 0x0102e0, 0}, - {0x0102e1, 0x0102fb, 1}, {0x0102fc, 0x0102ff, -1}, {0x010300, 0x010323, 1}, - {0x010324, 0x01032c, -1}, {0x01032d, 0x01034a, 1}, {0x01034b, 0x01034f, -1}, - {0x010350, 0x010375, 1}, {0x010376, 0x01037a, 0}, {0x01037b, 0x01037f, -1}, - {0x010380, 0x01039d, 1}, {0x01039e, 0x01039e, -1}, {0x01039f, 0x0103c3, 1}, - {0x0103c4, 0x0103c7, -1}, {0x0103c8, 0x0103d5, 1}, {0x0103d6, 0x0103ff, -1}, - {0x010400, 0x01049d, 1}, {0x01049e, 0x01049f, -1}, {0x0104a0, 0x0104a9, 1}, - {0x0104aa, 0x0104af, -1}, {0x0104b0, 0x0104d3, 1}, {0x0104d4, 0x0104d7, -1}, - {0x0104d8, 0x0104fb, 1}, {0x0104fc, 0x0104ff, -1}, {0x010500, 0x010527, 1}, - {0x010528, 0x01052f, -1}, {0x010530, 0x010563, 1}, {0x010564, 0x01056e, -1}, - {0x01056f, 0x01057a, 1}, {0x01057b, 0x01057b, -1}, {0x01057c, 0x01058a, 1}, - {0x01058b, 0x01058b, -1}, {0x01058c, 0x010592, 1}, {0x010593, 0x010593, -1}, - {0x010594, 0x010595, 1}, {0x010596, 0x010596, -1}, {0x010597, 0x0105a1, 1}, - {0x0105a2, 0x0105a2, -1}, {0x0105a3, 0x0105b1, 1}, {0x0105b2, 0x0105b2, -1}, - {0x0105b3, 0x0105b9, 1}, {0x0105ba, 0x0105ba, -1}, {0x0105bb, 0x0105bc, 1}, - {0x0105bd, 0x0105bf, -1}, {0x0105c0, 0x0105f3, 1}, {0x0105f4, 0x0105ff, -1}, - {0x010600, 0x010736, 1}, {0x010737, 0x01073f, -1}, {0x010740, 0x010755, 1}, - {0x010756, 0x01075f, -1}, {0x010760, 0x010767, 1}, {0x010768, 0x01077f, -1}, - {0x010780, 0x010785, 1}, {0x010786, 0x010786, -1}, {0x010787, 0x0107b0, 1}, - {0x0107b1, 0x0107b1, -1}, {0x0107b2, 0x0107ba, 1}, {0x0107bb, 0x0107ff, -1}, - {0x010800, 0x010805, 1}, {0x010806, 0x010807, -1}, {0x010808, 0x010808, 1}, - {0x010809, 0x010809, -1}, {0x01080a, 0x010835, 1}, {0x010836, 0x010836, -1}, - {0x010837, 0x010838, 1}, {0x010839, 0x01083b, -1}, {0x01083c, 0x01083c, 1}, - {0x01083d, 0x01083e, -1}, {0x01083f, 0x010855, 1}, {0x010856, 0x010856, -1}, - {0x010857, 0x01089e, 1}, {0x01089f, 0x0108a6, -1}, {0x0108a7, 0x0108af, 1}, - {0x0108b0, 0x0108df, -1}, {0x0108e0, 0x0108f2, 1}, {0x0108f3, 0x0108f3, -1}, - {0x0108f4, 0x0108f5, 1}, {0x0108f6, 0x0108fa, -1}, {0x0108fb, 0x01091b, 1}, - {0x01091c, 0x01091e, -1}, {0x01091f, 0x010939, 1}, {0x01093a, 0x01093e, -1}, - {0x01093f, 0x01093f, 1}, {0x010940, 0x01097f, -1}, {0x010980, 0x0109b7, 1}, - {0x0109b8, 0x0109bb, -1}, {0x0109bc, 0x0109cf, 1}, {0x0109d0, 0x0109d1, -1}, - {0x0109d2, 0x010a00, 1}, {0x010a01, 0x010a03, 0}, {0x010a04, 0x010a04, -1}, - {0x010a05, 0x010a06, 0}, {0x010a07, 0x010a0b, -1}, {0x010a0c, 0x010a0f, 0}, - {0x010a10, 0x010a13, 1}, {0x010a14, 0x010a14, -1}, {0x010a15, 0x010a17, 1}, - {0x010a18, 0x010a18, -1}, {0x010a19, 0x010a35, 1}, {0x010a36, 0x010a37, -1}, - {0x010a38, 0x010a3a, 0}, {0x010a3b, 0x010a3e, -1}, {0x010a3f, 0x010a3f, 0}, - {0x010a40, 0x010a48, 1}, {0x010a49, 0x010a4f, -1}, {0x010a50, 0x010a58, 1}, - {0x010a59, 0x010a5f, -1}, {0x010a60, 0x010a9f, 1}, {0x010aa0, 0x010abf, -1}, - {0x010ac0, 0x010ae4, 1}, {0x010ae5, 0x010ae6, 0}, {0x010ae7, 0x010aea, -1}, - {0x010aeb, 0x010af6, 1}, {0x010af7, 0x010aff, -1}, {0x010b00, 0x010b35, 1}, - {0x010b36, 0x010b38, -1}, {0x010b39, 0x010b55, 1}, {0x010b56, 0x010b57, -1}, - {0x010b58, 0x010b72, 1}, {0x010b73, 0x010b77, -1}, {0x010b78, 0x010b91, 1}, - {0x010b92, 0x010b98, -1}, {0x010b99, 0x010b9c, 1}, {0x010b9d, 0x010ba8, -1}, - {0x010ba9, 0x010baf, 1}, {0x010bb0, 0x010bff, -1}, {0x010c00, 0x010c48, 1}, - {0x010c49, 0x010c7f, -1}, {0x010c80, 0x010cb2, 1}, {0x010cb3, 0x010cbf, -1}, - {0x010cc0, 0x010cf2, 1}, {0x010cf3, 0x010cf9, -1}, {0x010cfa, 0x010d23, 1}, - {0x010d24, 0x010d27, 0}, {0x010d28, 0x010d2f, -1}, {0x010d30, 0x010d39, 1}, - {0x010d3a, 0x010d3f, -1}, {0x010d40, 0x010d65, 1}, {0x010d66, 0x010d68, -1}, - {0x010d69, 0x010d6d, 0}, {0x010d6e, 0x010d85, 1}, {0x010d86, 0x010d8d, -1}, - {0x010d8e, 0x010d8f, 1}, {0x010d90, 0x010e5f, -1}, {0x010e60, 0x010e7e, 1}, - {0x010e7f, 0x010e7f, -1}, {0x010e80, 0x010ea9, 1}, {0x010eaa, 0x010eaa, -1}, - {0x010eab, 0x010eac, 0}, {0x010ead, 0x010ead, 1}, {0x010eae, 0x010eaf, -1}, - {0x010eb0, 0x010eb1, 1}, {0x010eb2, 0x010ec1, -1}, {0x010ec2, 0x010ec4, 1}, - {0x010ec5, 0x010efb, -1}, {0x010efc, 0x010eff, 0}, {0x010f00, 0x010f27, 1}, - {0x010f28, 0x010f2f, -1}, {0x010f30, 0x010f45, 1}, {0x010f46, 0x010f50, 0}, - {0x010f51, 0x010f59, 1}, {0x010f5a, 0x010f6f, -1}, {0x010f70, 0x010f81, 1}, - {0x010f82, 0x010f85, 0}, {0x010f86, 0x010f89, 1}, {0x010f8a, 0x010faf, -1}, - {0x010fb0, 0x010fcb, 1}, {0x010fcc, 0x010fdf, -1}, {0x010fe0, 0x010ff6, 1}, - {0x010ff7, 0x010fff, -1}, {0x011000, 0x011000, 1}, {0x011001, 0x011001, 0}, - {0x011002, 0x011037, 1}, {0x011038, 0x011046, 0}, {0x011047, 0x01104d, 1}, - {0x01104e, 0x011051, -1}, {0x011052, 0x01106f, 1}, {0x011070, 0x011070, 0}, - {0x011071, 0x011072, 1}, {0x011073, 0x011074, 0}, {0x011075, 0x011075, 1}, - {0x011076, 0x01107e, -1}, {0x01107f, 0x011081, 0}, {0x011082, 0x0110b2, 1}, - {0x0110b3, 0x0110b6, 0}, {0x0110b7, 0x0110b8, 1}, {0x0110b9, 0x0110ba, 0}, - {0x0110bb, 0x0110c1, 1}, {0x0110c2, 0x0110c2, 0}, {0x0110c3, 0x0110cc, -1}, - {0x0110cd, 0x0110cd, 1}, {0x0110ce, 0x0110cf, -1}, {0x0110d0, 0x0110e8, 1}, - {0x0110e9, 0x0110ef, -1}, {0x0110f0, 0x0110f9, 1}, {0x0110fa, 0x0110ff, -1}, - {0x011100, 0x011102, 0}, {0x011103, 0x011126, 1}, {0x011127, 0x01112b, 0}, - {0x01112c, 0x01112c, 1}, {0x01112d, 0x011134, 0}, {0x011135, 0x011135, -1}, - {0x011136, 0x011147, 1}, {0x011148, 0x01114f, -1}, {0x011150, 0x011172, 1}, - {0x011173, 0x011173, 0}, {0x011174, 0x011176, 1}, {0x011177, 0x01117f, -1}, - {0x011180, 0x011181, 0}, {0x011182, 0x0111b5, 1}, {0x0111b6, 0x0111be, 0}, - {0x0111bf, 0x0111c8, 1}, {0x0111c9, 0x0111cc, 0}, {0x0111cd, 0x0111ce, 1}, - {0x0111cf, 0x0111cf, 0}, {0x0111d0, 0x0111df, 1}, {0x0111e0, 0x0111e0, -1}, - {0x0111e1, 0x0111f4, 1}, {0x0111f5, 0x0111ff, -1}, {0x011200, 0x011211, 1}, - {0x011212, 0x011212, -1}, {0x011213, 0x01122e, 1}, {0x01122f, 0x011231, 0}, - {0x011232, 0x011233, 1}, {0x011234, 0x011234, 0}, {0x011235, 0x011235, 1}, - {0x011236, 0x011237, 0}, {0x011238, 0x01123d, 1}, {0x01123e, 0x01123e, 0}, - {0x01123f, 0x011240, 1}, {0x011241, 0x011241, 0}, {0x011242, 0x01127f, -1}, - {0x011280, 0x011286, 1}, {0x011287, 0x011287, -1}, {0x011288, 0x011288, 1}, - {0x011289, 0x011289, -1}, {0x01128a, 0x01128d, 1}, {0x01128e, 0x01128e, -1}, - {0x01128f, 0x01129d, 1}, {0x01129e, 0x01129e, -1}, {0x01129f, 0x0112a9, 1}, - {0x0112aa, 0x0112af, -1}, {0x0112b0, 0x0112de, 1}, {0x0112df, 0x0112df, 0}, - {0x0112e0, 0x0112e2, 1}, {0x0112e3, 0x0112ea, 0}, {0x0112eb, 0x0112ef, -1}, - {0x0112f0, 0x0112f9, 1}, {0x0112fa, 0x0112ff, -1}, {0x011300, 0x011301, 0}, - {0x011302, 0x011303, 1}, {0x011304, 0x011304, -1}, {0x011305, 0x01130c, 1}, - {0x01130d, 0x01130e, -1}, {0x01130f, 0x011310, 1}, {0x011311, 0x011312, -1}, - {0x011313, 0x011328, 1}, {0x011329, 0x011329, -1}, {0x01132a, 0x011330, 1}, - {0x011331, 0x011331, -1}, {0x011332, 0x011333, 1}, {0x011334, 0x011334, -1}, - {0x011335, 0x011339, 1}, {0x01133a, 0x01133a, -1}, {0x01133b, 0x01133c, 0}, - {0x01133d, 0x01133f, 1}, {0x011340, 0x011340, 0}, {0x011341, 0x011344, 1}, - {0x011345, 0x011346, -1}, {0x011347, 0x011348, 1}, {0x011349, 0x01134a, -1}, - {0x01134b, 0x01134d, 1}, {0x01134e, 0x01134f, -1}, {0x011350, 0x011350, 1}, - {0x011351, 0x011356, -1}, {0x011357, 0x011357, 1}, {0x011358, 0x01135c, -1}, - {0x01135d, 0x011363, 1}, {0x011364, 0x011365, -1}, {0x011366, 0x01136c, 0}, - {0x01136d, 0x01136f, -1}, {0x011370, 0x011374, 0}, {0x011375, 0x01137f, -1}, - {0x011380, 0x011389, 1}, {0x01138a, 0x01138a, -1}, {0x01138b, 0x01138b, 1}, - {0x01138c, 0x01138d, -1}, {0x01138e, 0x01138e, 1}, {0x01138f, 0x01138f, -1}, - {0x011390, 0x0113b5, 1}, {0x0113b6, 0x0113b6, -1}, {0x0113b7, 0x0113ba, 1}, - {0x0113bb, 0x0113c0, 0}, {0x0113c1, 0x0113c1, -1}, {0x0113c2, 0x0113c2, 1}, - {0x0113c3, 0x0113c4, -1}, {0x0113c5, 0x0113c5, 1}, {0x0113c6, 0x0113c6, -1}, - {0x0113c7, 0x0113ca, 1}, {0x0113cb, 0x0113cb, -1}, {0x0113cc, 0x0113cd, 1}, - {0x0113ce, 0x0113ce, 0}, {0x0113cf, 0x0113cf, 1}, {0x0113d0, 0x0113d0, 0}, - {0x0113d1, 0x0113d1, 1}, {0x0113d2, 0x0113d2, 0}, {0x0113d3, 0x0113d5, 1}, - {0x0113d6, 0x0113d6, -1}, {0x0113d7, 0x0113d8, 1}, {0x0113d9, 0x0113e0, -1}, - {0x0113e1, 0x0113e2, 0}, {0x0113e3, 0x0113ff, -1}, {0x011400, 0x011437, 1}, - {0x011438, 0x01143f, 0}, {0x011440, 0x011441, 1}, {0x011442, 0x011444, 0}, - {0x011445, 0x011445, 1}, {0x011446, 0x011446, 0}, {0x011447, 0x01145b, 1}, - {0x01145c, 0x01145c, -1}, {0x01145d, 0x01145d, 1}, {0x01145e, 0x01145e, 0}, - {0x01145f, 0x011461, 1}, {0x011462, 0x01147f, -1}, {0x011480, 0x0114b2, 1}, - {0x0114b3, 0x0114b8, 0}, {0x0114b9, 0x0114b9, 1}, {0x0114ba, 0x0114ba, 0}, - {0x0114bb, 0x0114be, 1}, {0x0114bf, 0x0114c0, 0}, {0x0114c1, 0x0114c1, 1}, - {0x0114c2, 0x0114c3, 0}, {0x0114c4, 0x0114c7, 1}, {0x0114c8, 0x0114cf, -1}, - {0x0114d0, 0x0114d9, 1}, {0x0114da, 0x01157f, -1}, {0x011580, 0x0115b1, 1}, - {0x0115b2, 0x0115b5, 0}, {0x0115b6, 0x0115b7, -1}, {0x0115b8, 0x0115bb, 1}, - {0x0115bc, 0x0115bd, 0}, {0x0115be, 0x0115be, 1}, {0x0115bf, 0x0115c0, 0}, - {0x0115c1, 0x0115db, 1}, {0x0115dc, 0x0115dd, 0}, {0x0115de, 0x0115ff, -1}, - {0x011600, 0x011632, 1}, {0x011633, 0x01163a, 0}, {0x01163b, 0x01163c, 1}, - {0x01163d, 0x01163d, 0}, {0x01163e, 0x01163e, 1}, {0x01163f, 0x011640, 0}, - {0x011641, 0x011644, 1}, {0x011645, 0x01164f, -1}, {0x011650, 0x011659, 1}, - {0x01165a, 0x01165f, -1}, {0x011660, 0x01166c, 1}, {0x01166d, 0x01167f, -1}, - {0x011680, 0x0116aa, 1}, {0x0116ab, 0x0116ab, 0}, {0x0116ac, 0x0116ac, 1}, - {0x0116ad, 0x0116ad, 0}, {0x0116ae, 0x0116af, 1}, {0x0116b0, 0x0116b5, 0}, - {0x0116b6, 0x0116b6, 1}, {0x0116b7, 0x0116b7, 0}, {0x0116b8, 0x0116b9, 1}, - {0x0116ba, 0x0116bf, -1}, {0x0116c0, 0x0116c9, 1}, {0x0116ca, 0x0116cf, -1}, - {0x0116d0, 0x0116e3, 1}, {0x0116e4, 0x0116ff, -1}, {0x011700, 0x01171a, 1}, - {0x01171b, 0x01171c, -1}, {0x01171d, 0x01171d, 0}, {0x01171e, 0x01171e, 1}, - {0x01171f, 0x01171f, 0}, {0x011720, 0x011721, 1}, {0x011722, 0x011725, 0}, - {0x011726, 0x011726, 1}, {0x011727, 0x01172b, 0}, {0x01172c, 0x01172f, -1}, - {0x011730, 0x011746, 1}, {0x011747, 0x0117ff, -1}, {0x011800, 0x01182e, 1}, - {0x01182f, 0x011837, 0}, {0x011838, 0x011838, 1}, {0x011839, 0x01183a, 0}, - {0x01183b, 0x01183b, 1}, {0x01183c, 0x01189f, -1}, {0x0118a0, 0x0118f2, 1}, - {0x0118f3, 0x0118fe, -1}, {0x0118ff, 0x011906, 1}, {0x011907, 0x011908, -1}, - {0x011909, 0x011909, 1}, {0x01190a, 0x01190b, -1}, {0x01190c, 0x011913, 1}, - {0x011914, 0x011914, -1}, {0x011915, 0x011916, 1}, {0x011917, 0x011917, -1}, - {0x011918, 0x011935, 1}, {0x011936, 0x011936, -1}, {0x011937, 0x011938, 1}, - {0x011939, 0x01193a, -1}, {0x01193b, 0x01193c, 0}, {0x01193d, 0x01193d, 1}, - {0x01193e, 0x01193e, 0}, {0x01193f, 0x011942, 1}, {0x011943, 0x011943, 0}, - {0x011944, 0x011946, 1}, {0x011947, 0x01194f, -1}, {0x011950, 0x011959, 1}, - {0x01195a, 0x01199f, -1}, {0x0119a0, 0x0119a7, 1}, {0x0119a8, 0x0119a9, -1}, - {0x0119aa, 0x0119d3, 1}, {0x0119d4, 0x0119d7, 0}, {0x0119d8, 0x0119d9, -1}, - {0x0119da, 0x0119db, 0}, {0x0119dc, 0x0119df, 1}, {0x0119e0, 0x0119e0, 0}, - {0x0119e1, 0x0119e4, 1}, {0x0119e5, 0x0119ff, -1}, {0x011a00, 0x011a00, 1}, - {0x011a01, 0x011a0a, 0}, {0x011a0b, 0x011a32, 1}, {0x011a33, 0x011a38, 0}, - {0x011a39, 0x011a3a, 1}, {0x011a3b, 0x011a3e, 0}, {0x011a3f, 0x011a46, 1}, - {0x011a47, 0x011a47, 0}, {0x011a48, 0x011a4f, -1}, {0x011a50, 0x011a50, 1}, - {0x011a51, 0x011a56, 0}, {0x011a57, 0x011a58, 1}, {0x011a59, 0x011a5b, 0}, - {0x011a5c, 0x011a89, 1}, {0x011a8a, 0x011a96, 0}, {0x011a97, 0x011a97, 1}, - {0x011a98, 0x011a99, 0}, {0x011a9a, 0x011aa2, 1}, {0x011aa3, 0x011aaf, -1}, - {0x011ab0, 0x011af8, 1}, {0x011af9, 0x011aff, -1}, {0x011b00, 0x011b09, 1}, - {0x011b0a, 0x011bbf, -1}, {0x011bc0, 0x011be1, 1}, {0x011be2, 0x011bef, -1}, - {0x011bf0, 0x011bf9, 1}, {0x011bfa, 0x011bff, -1}, {0x011c00, 0x011c08, 1}, - {0x011c09, 0x011c09, -1}, {0x011c0a, 0x011c2f, 1}, {0x011c30, 0x011c36, 0}, - {0x011c37, 0x011c37, -1}, {0x011c38, 0x011c3d, 0}, {0x011c3e, 0x011c3e, 1}, - {0x011c3f, 0x011c3f, 0}, {0x011c40, 0x011c45, 1}, {0x011c46, 0x011c4f, -1}, - {0x011c50, 0x011c6c, 1}, {0x011c6d, 0x011c6f, -1}, {0x011c70, 0x011c8f, 1}, - {0x011c90, 0x011c91, -1}, {0x011c92, 0x011ca7, 0}, {0x011ca8, 0x011ca8, -1}, - {0x011ca9, 0x011ca9, 1}, {0x011caa, 0x011cb0, 0}, {0x011cb1, 0x011cb1, 1}, - {0x011cb2, 0x011cb3, 0}, {0x011cb4, 0x011cb4, 1}, {0x011cb5, 0x011cb6, 0}, - {0x011cb7, 0x011cff, -1}, {0x011d00, 0x011d06, 1}, {0x011d07, 0x011d07, -1}, - {0x011d08, 0x011d09, 1}, {0x011d0a, 0x011d0a, -1}, {0x011d0b, 0x011d30, 1}, - {0x011d31, 0x011d36, 0}, {0x011d37, 0x011d39, -1}, {0x011d3a, 0x011d3a, 0}, - {0x011d3b, 0x011d3b, -1}, {0x011d3c, 0x011d3d, 0}, {0x011d3e, 0x011d3e, -1}, - {0x011d3f, 0x011d45, 0}, {0x011d46, 0x011d46, 1}, {0x011d47, 0x011d47, 0}, - {0x011d48, 0x011d4f, -1}, {0x011d50, 0x011d59, 1}, {0x011d5a, 0x011d5f, -1}, - {0x011d60, 0x011d65, 1}, {0x011d66, 0x011d66, -1}, {0x011d67, 0x011d68, 1}, - {0x011d69, 0x011d69, -1}, {0x011d6a, 0x011d8e, 1}, {0x011d8f, 0x011d8f, -1}, - {0x011d90, 0x011d91, 0}, {0x011d92, 0x011d92, -1}, {0x011d93, 0x011d94, 1}, - {0x011d95, 0x011d95, 0}, {0x011d96, 0x011d96, 1}, {0x011d97, 0x011d97, 0}, - {0x011d98, 0x011d98, 1}, {0x011d99, 0x011d9f, -1}, {0x011da0, 0x011da9, 1}, - {0x011daa, 0x011edf, -1}, {0x011ee0, 0x011ef2, 1}, {0x011ef3, 0x011ef4, 0}, - {0x011ef5, 0x011ef8, 1}, {0x011ef9, 0x011eff, -1}, {0x011f00, 0x011f01, 0}, - {0x011f02, 0x011f10, 1}, {0x011f11, 0x011f11, -1}, {0x011f12, 0x011f35, 1}, - {0x011f36, 0x011f3a, 0}, {0x011f3b, 0x011f3d, -1}, {0x011f3e, 0x011f3f, 1}, - {0x011f40, 0x011f40, 0}, {0x011f41, 0x011f41, 1}, {0x011f42, 0x011f42, 0}, - {0x011f43, 0x011f59, 1}, {0x011f5a, 0x011f5a, 0}, {0x011f5b, 0x011faf, -1}, - {0x011fb0, 0x011fb0, 1}, {0x011fb1, 0x011fbf, -1}, {0x011fc0, 0x011ff1, 1}, - {0x011ff2, 0x011ffe, -1}, {0x011fff, 0x012399, 1}, {0x01239a, 0x0123ff, -1}, - {0x012400, 0x01246e, 1}, {0x01246f, 0x01246f, -1}, {0x012470, 0x012474, 1}, - {0x012475, 0x01247f, -1}, {0x012480, 0x012543, 1}, {0x012544, 0x012f8f, -1}, - {0x012f90, 0x012ff2, 1}, {0x012ff3, 0x012fff, -1}, {0x013000, 0x01343f, 1}, - {0x013440, 0x013440, 0}, {0x013441, 0x013446, 1}, {0x013447, 0x013455, 0}, - {0x013456, 0x01345f, -1}, {0x013460, 0x0143fa, 1}, {0x0143fb, 0x0143ff, -1}, - {0x014400, 0x014646, 1}, {0x014647, 0x0160ff, -1}, {0x016100, 0x01611d, 1}, - {0x01611e, 0x016129, 0}, {0x01612a, 0x01612c, 1}, {0x01612d, 0x01612f, 0}, - {0x016130, 0x016139, 1}, {0x01613a, 0x0167ff, -1}, {0x016800, 0x016a38, 1}, - {0x016a39, 0x016a3f, -1}, {0x016a40, 0x016a5e, 1}, {0x016a5f, 0x016a5f, -1}, - {0x016a60, 0x016a69, 1}, {0x016a6a, 0x016a6d, -1}, {0x016a6e, 0x016abe, 1}, - {0x016abf, 0x016abf, -1}, {0x016ac0, 0x016ac9, 1}, {0x016aca, 0x016acf, -1}, - {0x016ad0, 0x016aed, 1}, {0x016aee, 0x016aef, -1}, {0x016af0, 0x016af4, 0}, - {0x016af5, 0x016af5, 1}, {0x016af6, 0x016aff, -1}, {0x016b00, 0x016b2f, 1}, - {0x016b30, 0x016b36, 0}, {0x016b37, 0x016b45, 1}, {0x016b46, 0x016b4f, -1}, - {0x016b50, 0x016b59, 1}, {0x016b5a, 0x016b5a, -1}, {0x016b5b, 0x016b61, 1}, - {0x016b62, 0x016b62, -1}, {0x016b63, 0x016b77, 1}, {0x016b78, 0x016b7c, -1}, - {0x016b7d, 0x016b8f, 1}, {0x016b90, 0x016d3f, -1}, {0x016d40, 0x016d79, 1}, - {0x016d7a, 0x016e3f, -1}, {0x016e40, 0x016e9a, 1}, {0x016e9b, 0x016eff, -1}, - {0x016f00, 0x016f4a, 1}, {0x016f4b, 0x016f4e, -1}, {0x016f4f, 0x016f4f, 0}, - {0x016f50, 0x016f87, 1}, {0x016f88, 0x016f8e, -1}, {0x016f8f, 0x016f92, 0}, - {0x016f93, 0x016f9f, 1}, {0x016fa0, 0x016fdf, -1}, {0x016fe0, 0x016fe3, 2}, - {0x016fe4, 0x016fe4, 0}, {0x016fe5, 0x016fef, -1}, {0x016ff0, 0x016ff1, 2}, - {0x016ff2, 0x016fff, -1}, {0x017000, 0x0187f7, 2}, {0x0187f8, 0x0187ff, -1}, - {0x018800, 0x018cd5, 2}, {0x018cd6, 0x018cfe, -1}, {0x018cff, 0x018d08, 2}, - {0x018d09, 0x01afef, -1}, {0x01aff0, 0x01aff3, 2}, {0x01aff4, 0x01aff4, -1}, - {0x01aff5, 0x01affb, 2}, {0x01affc, 0x01affc, -1}, {0x01affd, 0x01affe, 2}, - {0x01afff, 0x01afff, -1}, {0x01b000, 0x01b122, 2}, {0x01b123, 0x01b131, -1}, - {0x01b132, 0x01b132, 2}, {0x01b133, 0x01b14f, -1}, {0x01b150, 0x01b152, 2}, - {0x01b153, 0x01b154, -1}, {0x01b155, 0x01b155, 2}, {0x01b156, 0x01b163, -1}, - {0x01b164, 0x01b167, 2}, {0x01b168, 0x01b16f, -1}, {0x01b170, 0x01b2fb, 2}, - {0x01b2fc, 0x01bbff, -1}, {0x01bc00, 0x01bc6a, 1}, {0x01bc6b, 0x01bc6f, -1}, - {0x01bc70, 0x01bc7c, 1}, {0x01bc7d, 0x01bc7f, -1}, {0x01bc80, 0x01bc88, 1}, - {0x01bc89, 0x01bc8f, -1}, {0x01bc90, 0x01bc99, 1}, {0x01bc9a, 0x01bc9b, -1}, - {0x01bc9c, 0x01bc9c, 1}, {0x01bc9d, 0x01bc9e, 0}, {0x01bc9f, 0x01bc9f, 1}, - {0x01bca0, 0x01bca3, 0}, {0x01bca4, 0x01cbff, -1}, {0x01cc00, 0x01ccf9, 1}, - {0x01ccfa, 0x01ccff, -1}, {0x01cd00, 0x01ceb3, 1}, {0x01ceb4, 0x01ceff, -1}, - {0x01cf00, 0x01cf2d, 0}, {0x01cf2e, 0x01cf2f, -1}, {0x01cf30, 0x01cf46, 0}, - {0x01cf47, 0x01cf4f, -1}, {0x01cf50, 0x01cfc3, 1}, {0x01cfc4, 0x01cfff, -1}, - {0x01d000, 0x01d0f5, 1}, {0x01d0f6, 0x01d0ff, -1}, {0x01d100, 0x01d126, 1}, - {0x01d127, 0x01d128, -1}, {0x01d129, 0x01d166, 1}, {0x01d167, 0x01d169, 0}, - {0x01d16a, 0x01d172, 1}, {0x01d173, 0x01d182, 0}, {0x01d183, 0x01d184, 1}, - {0x01d185, 0x01d18b, 0}, {0x01d18c, 0x01d1a9, 1}, {0x01d1aa, 0x01d1ad, 0}, - {0x01d1ae, 0x01d1ea, 1}, {0x01d1eb, 0x01d1ff, -1}, {0x01d200, 0x01d241, 1}, - {0x01d242, 0x01d244, 0}, {0x01d245, 0x01d245, 1}, {0x01d246, 0x01d2bf, -1}, - {0x01d2c0, 0x01d2d3, 1}, {0x01d2d4, 0x01d2df, -1}, {0x01d2e0, 0x01d2f3, 1}, - {0x01d2f4, 0x01d2ff, -1}, {0x01d300, 0x01d356, 2}, {0x01d357, 0x01d35f, -1}, - {0x01d360, 0x01d376, 2}, {0x01d377, 0x01d378, 1}, {0x01d379, 0x01d3ff, -1}, - {0x01d400, 0x01d454, 1}, {0x01d455, 0x01d455, -1}, {0x01d456, 0x01d49c, 1}, - {0x01d49d, 0x01d49d, -1}, {0x01d49e, 0x01d49f, 1}, {0x01d4a0, 0x01d4a1, -1}, - {0x01d4a2, 0x01d4a2, 1}, {0x01d4a3, 0x01d4a4, -1}, {0x01d4a5, 0x01d4a6, 1}, - {0x01d4a7, 0x01d4a8, -1}, {0x01d4a9, 0x01d4ac, 1}, {0x01d4ad, 0x01d4ad, -1}, - {0x01d4ae, 0x01d4b9, 1}, {0x01d4ba, 0x01d4ba, -1}, {0x01d4bb, 0x01d4bb, 1}, - {0x01d4bc, 0x01d4bc, -1}, {0x01d4bd, 0x01d4c3, 1}, {0x01d4c4, 0x01d4c4, -1}, - {0x01d4c5, 0x01d505, 1}, {0x01d506, 0x01d506, -1}, {0x01d507, 0x01d50a, 1}, - {0x01d50b, 0x01d50c, -1}, {0x01d50d, 0x01d514, 1}, {0x01d515, 0x01d515, -1}, - {0x01d516, 0x01d51c, 1}, {0x01d51d, 0x01d51d, -1}, {0x01d51e, 0x01d539, 1}, - {0x01d53a, 0x01d53a, -1}, {0x01d53b, 0x01d53e, 1}, {0x01d53f, 0x01d53f, -1}, - {0x01d540, 0x01d544, 1}, {0x01d545, 0x01d545, -1}, {0x01d546, 0x01d546, 1}, - {0x01d547, 0x01d549, -1}, {0x01d54a, 0x01d550, 1}, {0x01d551, 0x01d551, -1}, - {0x01d552, 0x01d6a5, 1}, {0x01d6a6, 0x01d6a7, -1}, {0x01d6a8, 0x01d7cb, 1}, - {0x01d7cc, 0x01d7cd, -1}, {0x01d7ce, 0x01d9ff, 1}, {0x01da00, 0x01da36, 0}, - {0x01da37, 0x01da3a, 1}, {0x01da3b, 0x01da6c, 0}, {0x01da6d, 0x01da74, 1}, - {0x01da75, 0x01da75, 0}, {0x01da76, 0x01da83, 1}, {0x01da84, 0x01da84, 0}, - {0x01da85, 0x01da8b, 1}, {0x01da8c, 0x01da9a, -1}, {0x01da9b, 0x01da9f, 0}, - {0x01daa0, 0x01daa0, -1}, {0x01daa1, 0x01daaf, 0}, {0x01dab0, 0x01deff, -1}, - {0x01df00, 0x01df1e, 1}, {0x01df1f, 0x01df24, -1}, {0x01df25, 0x01df2a, 1}, - {0x01df2b, 0x01dfff, -1}, {0x01e000, 0x01e006, 0}, {0x01e007, 0x01e007, -1}, - {0x01e008, 0x01e018, 0}, {0x01e019, 0x01e01a, -1}, {0x01e01b, 0x01e021, 0}, - {0x01e022, 0x01e022, -1}, {0x01e023, 0x01e024, 0}, {0x01e025, 0x01e025, -1}, - {0x01e026, 0x01e02a, 0}, {0x01e02b, 0x01e02f, -1}, {0x01e030, 0x01e06d, 1}, - {0x01e06e, 0x01e08e, -1}, {0x01e08f, 0x01e08f, 0}, {0x01e090, 0x01e0ff, -1}, - {0x01e100, 0x01e12c, 1}, {0x01e12d, 0x01e12f, -1}, {0x01e130, 0x01e136, 0}, - {0x01e137, 0x01e13d, 1}, {0x01e13e, 0x01e13f, -1}, {0x01e140, 0x01e149, 1}, - {0x01e14a, 0x01e14d, -1}, {0x01e14e, 0x01e14f, 1}, {0x01e150, 0x01e28f, -1}, - {0x01e290, 0x01e2ad, 1}, {0x01e2ae, 0x01e2ae, 0}, {0x01e2af, 0x01e2bf, -1}, - {0x01e2c0, 0x01e2eb, 1}, {0x01e2ec, 0x01e2ef, 0}, {0x01e2f0, 0x01e2f9, 1}, - {0x01e2fa, 0x01e2fe, -1}, {0x01e2ff, 0x01e2ff, 1}, {0x01e300, 0x01e4cf, -1}, - {0x01e4d0, 0x01e4eb, 1}, {0x01e4ec, 0x01e4ef, 0}, {0x01e4f0, 0x01e4f9, 1}, - {0x01e4fa, 0x01e5cf, -1}, {0x01e5d0, 0x01e5ed, 1}, {0x01e5ee, 0x01e5ef, 0}, - {0x01e5f0, 0x01e5fa, 1}, {0x01e5fb, 0x01e5fe, -1}, {0x01e5ff, 0x01e5ff, 1}, - {0x01e600, 0x01e7df, -1}, {0x01e7e0, 0x01e7e6, 1}, {0x01e7e7, 0x01e7e7, -1}, - {0x01e7e8, 0x01e7eb, 1}, {0x01e7ec, 0x01e7ec, -1}, {0x01e7ed, 0x01e7ee, 1}, - {0x01e7ef, 0x01e7ef, -1}, {0x01e7f0, 0x01e7fe, 1}, {0x01e7ff, 0x01e7ff, -1}, - {0x01e800, 0x01e8c4, 1}, {0x01e8c5, 0x01e8c6, -1}, {0x01e8c7, 0x01e8cf, 1}, - {0x01e8d0, 0x01e8d6, 0}, {0x01e8d7, 0x01e8ff, -1}, {0x01e900, 0x01e943, 1}, - {0x01e944, 0x01e94a, 0}, {0x01e94b, 0x01e94b, 1}, {0x01e94c, 0x01e94f, -1}, - {0x01e950, 0x01e959, 1}, {0x01e95a, 0x01e95d, -1}, {0x01e95e, 0x01e95f, 1}, - {0x01e960, 0x01ec70, -1}, {0x01ec71, 0x01ecb4, 1}, {0x01ecb5, 0x01ed00, -1}, - {0x01ed01, 0x01ed3d, 1}, {0x01ed3e, 0x01edff, -1}, {0x01ee00, 0x01ee03, 1}, - {0x01ee04, 0x01ee04, -1}, {0x01ee05, 0x01ee1f, 1}, {0x01ee20, 0x01ee20, -1}, - {0x01ee21, 0x01ee22, 1}, {0x01ee23, 0x01ee23, -1}, {0x01ee24, 0x01ee24, 1}, - {0x01ee25, 0x01ee26, -1}, {0x01ee27, 0x01ee27, 1}, {0x01ee28, 0x01ee28, -1}, - {0x01ee29, 0x01ee32, 1}, {0x01ee33, 0x01ee33, -1}, {0x01ee34, 0x01ee37, 1}, - {0x01ee38, 0x01ee38, -1}, {0x01ee39, 0x01ee39, 1}, {0x01ee3a, 0x01ee3a, -1}, - {0x01ee3b, 0x01ee3b, 1}, {0x01ee3c, 0x01ee41, -1}, {0x01ee42, 0x01ee42, 1}, - {0x01ee43, 0x01ee46, -1}, {0x01ee47, 0x01ee47, 1}, {0x01ee48, 0x01ee48, -1}, - {0x01ee49, 0x01ee49, 1}, {0x01ee4a, 0x01ee4a, -1}, {0x01ee4b, 0x01ee4b, 1}, - {0x01ee4c, 0x01ee4c, -1}, {0x01ee4d, 0x01ee4f, 1}, {0x01ee50, 0x01ee50, -1}, - {0x01ee51, 0x01ee52, 1}, {0x01ee53, 0x01ee53, -1}, {0x01ee54, 0x01ee54, 1}, - {0x01ee55, 0x01ee56, -1}, {0x01ee57, 0x01ee57, 1}, {0x01ee58, 0x01ee58, -1}, - {0x01ee59, 0x01ee59, 1}, {0x01ee5a, 0x01ee5a, -1}, {0x01ee5b, 0x01ee5b, 1}, - {0x01ee5c, 0x01ee5c, -1}, {0x01ee5d, 0x01ee5d, 1}, {0x01ee5e, 0x01ee5e, -1}, - {0x01ee5f, 0x01ee5f, 1}, {0x01ee60, 0x01ee60, -1}, {0x01ee61, 0x01ee62, 1}, - {0x01ee63, 0x01ee63, -1}, {0x01ee64, 0x01ee64, 1}, {0x01ee65, 0x01ee66, -1}, - {0x01ee67, 0x01ee6a, 1}, {0x01ee6b, 0x01ee6b, -1}, {0x01ee6c, 0x01ee72, 1}, - {0x01ee73, 0x01ee73, -1}, {0x01ee74, 0x01ee77, 1}, {0x01ee78, 0x01ee78, -1}, - {0x01ee79, 0x01ee7c, 1}, {0x01ee7d, 0x01ee7d, -1}, {0x01ee7e, 0x01ee7e, 1}, - {0x01ee7f, 0x01ee7f, -1}, {0x01ee80, 0x01ee89, 1}, {0x01ee8a, 0x01ee8a, -1}, - {0x01ee8b, 0x01ee9b, 1}, {0x01ee9c, 0x01eea0, -1}, {0x01eea1, 0x01eea3, 1}, - {0x01eea4, 0x01eea4, -1}, {0x01eea5, 0x01eea9, 1}, {0x01eeaa, 0x01eeaa, -1}, - {0x01eeab, 0x01eebb, 1}, {0x01eebc, 0x01eeef, -1}, {0x01eef0, 0x01eef1, 1}, - {0x01eef2, 0x01efff, -1}, {0x01f000, 0x01f003, 1}, {0x01f004, 0x01f004, 2}, - {0x01f005, 0x01f02b, 1}, {0x01f02c, 0x01f02f, -1}, {0x01f030, 0x01f093, 1}, - {0x01f094, 0x01f09f, -1}, {0x01f0a0, 0x01f0ae, 1}, {0x01f0af, 0x01f0b0, -1}, - {0x01f0b1, 0x01f0bf, 1}, {0x01f0c0, 0x01f0c0, -1}, {0x01f0c1, 0x01f0ce, 1}, - {0x01f0cf, 0x01f0cf, 2}, {0x01f0d0, 0x01f0d0, -1}, {0x01f0d1, 0x01f0f5, 1}, - {0x01f0f6, 0x01f0ff, -1}, {0x01f100, 0x01f18d, 1}, {0x01f18e, 0x01f18e, 2}, - {0x01f18f, 0x01f190, 1}, {0x01f191, 0x01f19a, 2}, {0x01f19b, 0x01f1ad, 1}, - {0x01f1ae, 0x01f1e5, -1}, {0x01f1e6, 0x01f1ff, 1}, {0x01f200, 0x01f202, 2}, - {0x01f203, 0x01f20f, -1}, {0x01f210, 0x01f23b, 2}, {0x01f23c, 0x01f23f, -1}, - {0x01f240, 0x01f248, 2}, {0x01f249, 0x01f24f, -1}, {0x01f250, 0x01f251, 2}, - {0x01f252, 0x01f25f, -1}, {0x01f260, 0x01f265, 2}, {0x01f266, 0x01f2ff, -1}, - {0x01f300, 0x01f320, 2}, {0x01f321, 0x01f32c, 1}, {0x01f32d, 0x01f335, 2}, - {0x01f336, 0x01f336, 1}, {0x01f337, 0x01f37c, 2}, {0x01f37d, 0x01f37d, 1}, - {0x01f37e, 0x01f393, 2}, {0x01f394, 0x01f39f, 1}, {0x01f3a0, 0x01f3ca, 2}, - {0x01f3cb, 0x01f3ce, 1}, {0x01f3cf, 0x01f3d3, 2}, {0x01f3d4, 0x01f3df, 1}, - {0x01f3e0, 0x01f3f0, 2}, {0x01f3f1, 0x01f3f3, 1}, {0x01f3f4, 0x01f3f4, 2}, - {0x01f3f5, 0x01f3f7, 1}, {0x01f3f8, 0x01f43e, 2}, {0x01f43f, 0x01f43f, 1}, - {0x01f440, 0x01f440, 2}, {0x01f441, 0x01f441, 1}, {0x01f442, 0x01f4fc, 2}, - {0x01f4fd, 0x01f4fe, 1}, {0x01f4ff, 0x01f53d, 2}, {0x01f53e, 0x01f54a, 1}, - {0x01f54b, 0x01f54e, 2}, {0x01f54f, 0x01f54f, 1}, {0x01f550, 0x01f567, 2}, - {0x01f568, 0x01f579, 1}, {0x01f57a, 0x01f57a, 2}, {0x01f57b, 0x01f594, 1}, - {0x01f595, 0x01f596, 2}, {0x01f597, 0x01f5a3, 1}, {0x01f5a4, 0x01f5a4, 2}, - {0x01f5a5, 0x01f5fa, 1}, {0x01f5fb, 0x01f64f, 2}, {0x01f650, 0x01f67f, 1}, - {0x01f680, 0x01f6c5, 2}, {0x01f6c6, 0x01f6cb, 1}, {0x01f6cc, 0x01f6cc, 2}, - {0x01f6cd, 0x01f6cf, 1}, {0x01f6d0, 0x01f6d2, 2}, {0x01f6d3, 0x01f6d4, 1}, - {0x01f6d5, 0x01f6d7, 2}, {0x01f6d8, 0x01f6db, -1}, {0x01f6dc, 0x01f6df, 2}, - {0x01f6e0, 0x01f6ea, 1}, {0x01f6eb, 0x01f6ec, 2}, {0x01f6ed, 0x01f6ef, -1}, - {0x01f6f0, 0x01f6f3, 1}, {0x01f6f4, 0x01f6fc, 2}, {0x01f6fd, 0x01f6ff, -1}, - {0x01f700, 0x01f776, 1}, {0x01f777, 0x01f77a, -1}, {0x01f77b, 0x01f7d9, 1}, - {0x01f7da, 0x01f7df, -1}, {0x01f7e0, 0x01f7eb, 2}, {0x01f7ec, 0x01f7ef, -1}, - {0x01f7f0, 0x01f7f0, 2}, {0x01f7f1, 0x01f7ff, -1}, {0x01f800, 0x01f80b, 1}, - {0x01f80c, 0x01f80f, -1}, {0x01f810, 0x01f847, 1}, {0x01f848, 0x01f84f, -1}, - {0x01f850, 0x01f859, 1}, {0x01f85a, 0x01f85f, -1}, {0x01f860, 0x01f887, 1}, - {0x01f888, 0x01f88f, -1}, {0x01f890, 0x01f8ad, 1}, {0x01f8ae, 0x01f8af, -1}, - {0x01f8b0, 0x01f8bb, 1}, {0x01f8bc, 0x01f8bf, -1}, {0x01f8c0, 0x01f8c1, 1}, - {0x01f8c2, 0x01f8ff, -1}, {0x01f900, 0x01f90b, 1}, {0x01f90c, 0x01f93a, 2}, - {0x01f93b, 0x01f93b, 1}, {0x01f93c, 0x01f945, 2}, {0x01f946, 0x01f946, 1}, - {0x01f947, 0x01f9ff, 2}, {0x01fa00, 0x01fa53, 1}, {0x01fa54, 0x01fa5f, -1}, - {0x01fa60, 0x01fa6d, 1}, {0x01fa6e, 0x01fa6f, -1}, {0x01fa70, 0x01fa7c, 2}, - {0x01fa7d, 0x01fa7f, -1}, {0x01fa80, 0x01fa89, 2}, {0x01fa8a, 0x01fa8e, -1}, - {0x01fa8f, 0x01fac6, 2}, {0x01fac7, 0x01facd, -1}, {0x01face, 0x01fadc, 2}, - {0x01fadd, 0x01fade, -1}, {0x01fadf, 0x01fae9, 2}, {0x01faea, 0x01faef, -1}, - {0x01faf0, 0x01faf8, 2}, {0x01faf9, 0x01faff, -1}, {0x01fb00, 0x01fb92, 1}, - {0x01fb93, 0x01fb93, -1}, {0x01fb94, 0x01fbf9, 1}, {0x01fbfa, 0x01ffff, -1}, - {0x020000, 0x02a6df, 2}, {0x02a6e0, 0x02a6ff, -1}, {0x02a700, 0x02b739, 2}, - {0x02b73a, 0x02b73f, -1}, {0x02b740, 0x02b81d, 2}, {0x02b81e, 0x02b81f, -1}, - {0x02b820, 0x02cea1, 2}, {0x02cea2, 0x02ceaf, -1}, {0x02ceb0, 0x02ebe0, 2}, - {0x02ebe1, 0x02ebef, -1}, {0x02ebf0, 0x02ee5d, 2}, {0x02ee5e, 0x02f7ff, -1}, - {0x02f800, 0x02fa1d, 2}, {0x02fa1e, 0x02ffff, -1}, {0x030000, 0x03134a, 2}, - {0x03134b, 0x03134f, -1}, {0x031350, 0x0323af, 2}, {0x0323b0, 0x0e0000, -1}, - {0x0e0001, 0x0e0001, 0}, {0x0e0002, 0x0e001f, -1}, {0x0e0020, 0x0e007f, 0}, - {0x0e0080, 0x0e00ff, -1}, {0x0e0100, 0x0e01ef, 0}, {0x0e01f0, 0x0effff, -1}, - {0x0f0000, 0x0ffffd, 1}, {0x0ffffe, 0x0fffff, -1}, {0x100000, 0x10fffd, 1}, - {0x10fffe, 0x10ffff, -1}, - // clang-format on -}; -#define WCWIDTH_TABLE_LENGTH 2143 -#endif // ifndef TB_OPT_LIBC_WCHAR - -static int tb_reset(void); -static int tb_printf_inner(int x, int y, uintattr_t fg, uintattr_t bg, - size_t *out_w, const char *fmt, va_list vl); -static int init_term_attrs(void); -static int init_term_caps(void); -static int init_cap_trie(void); -static int cap_trie_add(const char *cap, uint16_t key, uint8_t mod); -static int cap_trie_find(const char *buf, size_t nbuf, struct cap_trie_t **last, - size_t *depth); -static int cap_trie_deinit(struct cap_trie_t *node); -static int init_resize_handler(void); -static int send_init_escape_codes(void); -static int send_clear(void); -static int update_term_size(void); -static int update_term_size_via_esc(void); -static int init_cellbuf(void); -static int tb_deinit(void); -static int load_terminfo(void); -static int load_terminfo_from_path(const char *path, const char *term); -static int read_terminfo_path(const char *path); -static int parse_terminfo_caps(void); -static int load_builtin_caps(void); -static const char *get_terminfo_string(int16_t offsets_pos, int16_t offsets_len, - int16_t table_pos, int16_t table_size, int16_t index); -static int get_terminfo_int16(int offset, int16_t *val); -static int wait_event(struct tb_event *event, int timeout); -static int extract_event(struct tb_event *event); -static int extract_esc(struct tb_event *event); -static int extract_esc_user(struct tb_event *event, int is_post); -static int extract_esc_cap(struct tb_event *event); -static int extract_esc_mouse(struct tb_event *event); -static int resize_cellbufs(void); -static void handle_resize(int sig); -static int send_attr(uintattr_t fg, uintattr_t bg); -static int send_sgr(uint32_t fg, uint32_t bg, int fg_is_default, - int bg_is_default); -static int send_cursor_if(int x, int y); -static int send_char(int x, int y, uint32_t ch); -static int send_cluster(int x, int y, uint32_t *ch, size_t nch); -static int convert_num(uint32_t num, char *buf); -static int cell_cmp(struct tb_cell *a, struct tb_cell *b); -static int cell_copy(struct tb_cell *dst, struct tb_cell *src); -static int cell_set(struct tb_cell *cell, uint32_t *ch, size_t nch, - uintattr_t fg, uintattr_t bg); -static int cell_reserve_ech(struct tb_cell *cell, size_t n); -static int cell_free(struct tb_cell *cell); -static int cellbuf_init(struct cellbuf_t *c, int w, int h); -static int cellbuf_free(struct cellbuf_t *c); -static int cellbuf_clear(struct cellbuf_t *c); -static int cellbuf_get(struct cellbuf_t *c, int x, int y, struct tb_cell **out); -static int cellbuf_in_bounds(struct cellbuf_t *c, int x, int y); -static int cellbuf_resize(struct cellbuf_t *c, int w, int h); -static int bytebuf_puts(struct bytebuf_t *b, const char *str); -static int bytebuf_nputs(struct bytebuf_t *b, const char *str, size_t nstr); -static int bytebuf_shift(struct bytebuf_t *b, size_t n); -static int bytebuf_flush(struct bytebuf_t *b, int fd); -static int bytebuf_reserve(struct bytebuf_t *b, size_t sz); -static int bytebuf_free(struct bytebuf_t *b); -static int tb_iswprint_ex(uint32_t ch, int *width); -static int tb_wcswidth(uint32_t *ch, size_t nch); - -int tb_init(void) { - return tb_init_file("/dev/tty"); -} - -int tb_init_file(const char *path) { - if (global.initialized) return TB_ERR_INIT_ALREADY; - int ttyfd = open(path, O_RDWR); - if (ttyfd < 0) { - global.last_errno = errno; - return TB_ERR_INIT_OPEN; - } - global.ttyfd_open = 1; - return tb_init_fd(ttyfd); -} - -int tb_init_fd(int ttyfd) { - return tb_init_rwfd(ttyfd, ttyfd); -} - -int tb_init_rwfd(int rfd, int wfd) { - int rv; - - tb_reset(); - global.ttyfd = rfd == wfd && isatty(rfd) ? rfd : -1; - global.rfd = rfd; - global.wfd = wfd; - - do { - if_err_break(rv, init_term_attrs()); - if_err_break(rv, init_term_caps()); - if_err_break(rv, init_cap_trie()); - if_err_break(rv, init_resize_handler()); - if_err_break(rv, send_init_escape_codes()); - if_err_break(rv, send_clear()); - if_err_break(rv, update_term_size()); - if_err_break(rv, init_cellbuf()); - global.initialized = 1; - } while (0); - - if (rv != TB_OK) tb_deinit(); - - return rv; -} - -int tb_shutdown(void) { - if_not_init_return(); - tb_deinit(); - return TB_OK; -} - -int tb_width(void) { - if_not_init_return(); - return global.width; -} - -int tb_height(void) { - if_not_init_return(); - return global.height; -} - -int tb_clear(void) { - if_not_init_return(); - return cellbuf_clear(&global.back); -} - -int tb_set_clear_attrs(uintattr_t fg, uintattr_t bg) { - if_not_init_return(); - global.fg = fg; - global.bg = bg; - return TB_OK; -} - -int tb_present(void) { - if_not_init_return(); - - int rv; - - // TODO: Assert global.back.(width,height) == global.front.(width,height) - - global.last_x = -1; - global.last_y = -1; - - int x, y, i; - for (y = 0; y < global.front.height; y++) { - for (x = 0; x < global.front.width;) { - struct tb_cell *back, *front; - if_err_return(rv, cellbuf_get(&global.back, x, y, &back)); - if_err_return(rv, cellbuf_get(&global.front, x, y, &front)); - - int w; - { -#ifdef TB_OPT_EGC - if (back->nech > 0) - w = tb_wcswidth(back->ech, back->nech); - else -#endif - w = tb_wcwidth((wchar_t)back->ch); - } - if (w < 1) w = 1; // wcwidth qreturns -1 for invalid codepoints - - if (cell_cmp(back, front) != 0) { - cell_copy(front, back); - - send_attr(back->fg, back->bg); - if (w > 1 && x >= global.front.width - (w - 1)) { - // Not enough room for wide char, send spaces - for (i = x; i < global.front.width; i++) { - send_char(i, y, ' '); - } - } else { - { -#ifdef TB_OPT_EGC - if (back->nech > 0) - send_cluster(x, y, back->ech, back->nech); - else -#endif - send_char(x, y, back->ch); - } - - // When wcwidth>1, we need to advance the cursor by more - // than 1, thereby skipping some cells. Set these skipped - // cells to an invalid codepoint in the front buffer, so - // that if this cell is later replaced by a wcwidth==1 char, - // we'll get a cell_cmp diff for the skipped cells and - // properly re-render. - for (i = 1; i < w; i++) { - struct tb_cell *front_wide; - uint32_t invalid = -1; - if_err_return(rv, - cellbuf_get(&global.front, x + i, y, &front_wide)); - if_err_return(rv, - cell_set(front_wide, &invalid, 1, -1, -1)); - } - } - } - x += w; - } - } - - if_err_return(rv, send_cursor_if(global.cursor_x, global.cursor_y)); - if_err_return(rv, bytebuf_flush(&global.out, global.wfd)); - - return TB_OK; -} - -int tb_invalidate(void) { - int rv; - if_not_init_return(); - if_err_return(rv, resize_cellbufs()); - return TB_OK; -} - -int tb_set_cursor(int cx, int cy) { - if_not_init_return(); - int rv; - if (cx < 0) cx = 0; - if (cy < 0) cy = 0; - if (global.cursor_x == -1) { - if_err_return(rv, - bytebuf_puts(&global.out, global.caps[TB_CAP_SHOW_CURSOR])); - } - if_err_return(rv, send_cursor_if(cx, cy)); - global.cursor_x = cx; - global.cursor_y = cy; - return TB_OK; -} - -int tb_hide_cursor(void) { - if_not_init_return(); - int rv; - if (global.cursor_x >= 0) { - if_err_return(rv, - bytebuf_puts(&global.out, global.caps[TB_CAP_HIDE_CURSOR])); - } - global.cursor_x = -1; - global.cursor_y = -1; - return TB_OK; -} - -int tb_set_cell(int x, int y, uint32_t ch, uintattr_t fg, uintattr_t bg) { - return tb_set_cell_ex(x, y, &ch, 1, fg, bg); -} - -int tb_set_cell_ex(int x, int y, uint32_t *ch, size_t nch, uintattr_t fg, - uintattr_t bg) { - if_not_init_return(); - int rv; - struct tb_cell *cell; - if_err_return(rv, cellbuf_get(&global.back, x, y, &cell)); - if_err_return(rv, cell_set(cell, ch, nch, fg, bg)); - return TB_OK; -} - -int tb_get_cell(int x, int y, int back, struct tb_cell *cell) { - if_not_init_return(); - int rv; - struct tb_cell *cellp = NULL; - rv = cellbuf_get(back ? &global.back : &global.front, x, y, &cellp); - if (cellp) memcpy(cell, cellp, sizeof(*cell)); - return rv; -} - -int tb_extend_cell(int x, int y, uint32_t ch) { - if_not_init_return(); -#ifdef TB_OPT_EGC - // TODO: iswprint ch? - int rv; - struct tb_cell *cell; - size_t nech; - if_err_return(rv, cellbuf_get(&global.back, x, y, &cell)); - if (cell->nech > 0) { // append to ech - nech = cell->nech + 1; - if_err_return(rv, cell_reserve_ech(cell, nech + 1)); - cell->ech[nech - 1] = ch; - } else { // make new ech - nech = 2; - if_err_return(rv, cell_reserve_ech(cell, nech + 1)); - cell->ech[0] = cell->ch; - cell->ech[1] = ch; - } - cell->ech[nech] = '\0'; - cell->nech = nech; - return TB_OK; -#else - (void)x; - (void)y; - (void)ch; - return TB_ERR; -#endif -} - -int tb_set_input_mode(int mode) { - if_not_init_return(); - - if (mode == TB_INPUT_CURRENT) return global.input_mode; - - int esc_or_alt = TB_INPUT_ESC | TB_INPUT_ALT; - if ((mode & esc_or_alt) == 0) { - // neither specified; flip on ESC - mode |= TB_INPUT_ESC; - } else if ((mode & esc_or_alt) == esc_or_alt) { - // both specified; flip off ALT - mode &= ~TB_INPUT_ALT; - } - - if (mode & TB_INPUT_MOUSE) { - bytebuf_puts(&global.out, TB_HARDCAP_ENTER_MOUSE); - bytebuf_flush(&global.out, global.wfd); - } else { - bytebuf_puts(&global.out, TB_HARDCAP_EXIT_MOUSE); - bytebuf_flush(&global.out, global.wfd); - } - - global.input_mode = mode; - return TB_OK; -} - -int tb_set_output_mode(int mode) { - if_not_init_return(); - switch (mode) { - case TB_OUTPUT_CURRENT: - return global.output_mode; - case TB_OUTPUT_NORMAL: - case TB_OUTPUT_256: - case TB_OUTPUT_216: - case TB_OUTPUT_GRAYSCALE: -#if TB_OPT_ATTR_W >= 32 - case TB_OUTPUT_TRUECOLOR: -#endif - global.last_fg = ~global.fg; - global.last_bg = ~global.bg; - global.output_mode = mode; - return TB_OK; - } - return TB_ERR; -} - -int tb_peek_event(struct tb_event *event, int timeout_ms) { - if_not_init_return(); - return wait_event(event, timeout_ms); -} - -int tb_poll_event(struct tb_event *event) { - if_not_init_return(); - return wait_event(event, -1); -} - -int tb_get_fds(int *ttyfd, int *resizefd) { - if_not_init_return(); - - *ttyfd = global.rfd; - *resizefd = global.resize_pipefd[0]; - - return TB_OK; -} - -int tb_print(int x, int y, uintattr_t fg, uintattr_t bg, const char *str) { - return tb_print_ex(x, y, fg, bg, NULL, str); -} - -int tb_print_ex(int x, int y, uintattr_t fg, uintattr_t bg, size_t *out_w, - const char *str) { - int rv, w, ix, x_prev; - uint32_t uni; - - if_not_init_return(); - - if (!cellbuf_in_bounds(&global.back, x, y)) { - return TB_ERR_OUT_OF_BOUNDS; - } - - ix = x; - x_prev = x; - if (out_w) *out_w = 0; - - while (*str) { - rv = tb_utf8_char_to_unicode(&uni, str); - - if (rv < 0) { - uni = 0xfffd; // replace invalid UTF-8 char with U+FFFD - str += rv * -1; - } else if (rv > 0) { - str += rv; - } else { - break; // shouldn't get here - } - - if (uni == '\n') { // TODO: \r, \t, \v, \f, etc? - x = ix; - x_prev = x; - y += 1; - continue; - } else if (!tb_iswprint_ex(uni, &w)) { - uni = 0xfffd; // replace non-printable with U+FFFD - w = 1; - } - - if (w < 0) { - return TB_ERR; // shouldn't happen if iswprint - } else if (w == 0) { // combining character - if (cellbuf_in_bounds(&global.back, x_prev, y)) { - if_err_return(rv, tb_extend_cell(x_prev, y, uni)); - } - } else { - if (cellbuf_in_bounds(&global.back, x, y)) { - if_err_return(rv, tb_set_cell(x, y, uni, fg, bg)); - } - x_prev = x; - x += w; - if (out_w) *out_w += w; - } - } - - return TB_OK; -} - -int tb_printf(int x, int y, uintattr_t fg, uintattr_t bg, const char *fmt, - ...) { - int rv; - va_list vl; - va_start(vl, fmt); - rv = tb_printf_inner(x, y, fg, bg, NULL, fmt, vl); - va_end(vl); - return rv; -} - -int tb_printf_ex(int x, int y, uintattr_t fg, uintattr_t bg, size_t *out_w, - const char *fmt, ...) { - int rv; - va_list vl; - va_start(vl, fmt); - rv = tb_printf_inner(x, y, fg, bg, out_w, fmt, vl); - va_end(vl); - return rv; -} - -int tb_send(const char *buf, size_t nbuf) { - return bytebuf_nputs(&global.out, buf, nbuf); -} - -int tb_sendf(const char *fmt, ...) { - int rv; - char buf[TB_OPT_PRINTF_BUF]; - va_list vl; - va_start(vl, fmt); - rv = vsnprintf(buf, sizeof(buf), fmt, vl); - va_end(vl); - if (rv < 0 || rv >= (int)sizeof(buf)) { - return TB_ERR; - } - return tb_send(buf, (size_t)rv); -} - -int tb_set_func(int fn_type, int (*fn)(struct tb_event *, size_t *)) { - switch (fn_type) { - case TB_FUNC_EXTRACT_PRE: - global.fn_extract_esc_pre = fn; - return TB_OK; - case TB_FUNC_EXTRACT_POST: - global.fn_extract_esc_post = fn; - return TB_OK; - } - return TB_ERR; -} - -struct tb_cell *tb_cell_buffer(void) { - if (!global.initialized) return NULL; - return global.back.cells; -} - -int tb_utf8_char_length(char c) { - return utf8_length[(unsigned char)c]; -} - -int tb_utf8_char_to_unicode(uint32_t *out, const char *c) { - if (*c == '\0') return 0; - - int i; - unsigned char len = tb_utf8_char_length(*c); - unsigned char mask = utf8_mask[len - 1]; - uint32_t result = c[0] & mask; - for (i = 1; i < len && c[i] != '\0'; ++i) { - result <<= 6; - result |= c[i] & 0x3f; - } - - if (i != len) return i * -1; - - *out = result; - return (int)len; -} - -int tb_utf8_unicode_to_char(char *out, uint32_t c) { - int len = 0; - int first; - int i; - - if (c < 0x80) { - first = 0; - len = 1; - } else if (c < 0x800) { - first = 0xc0; - len = 2; - } else if (c < 0x10000) { - first = 0xe0; - len = 3; - } else if (c < 0x200000) { - first = 0xf0; - len = 4; - } else if (c < 0x4000000) { - first = 0xf8; - len = 5; - } else { - first = 0xfc; - len = 6; - } - - for (i = len - 1; i > 0; --i) { - out[i] = (c & 0x3f) | 0x80; - c >>= 6; - } - out[0] = c | first; - out[len] = '\0'; - - return len; -} - -int tb_last_errno(void) { - return global.last_errno; -} - -const char *tb_strerror(int err) { - switch (err) { - case TB_OK: - return "Success"; - case TB_ERR_NEED_MORE: - return "Not enough input"; - case TB_ERR_INIT_ALREADY: - return "Termbox initialized already"; - case TB_ERR_MEM: - return "Out of memory"; - case TB_ERR_NO_EVENT: - return "No event"; - case TB_ERR_NO_TERM: - return "No TERM in environment"; - case TB_ERR_NOT_INIT: - return "Termbox not initialized"; - case TB_ERR_OUT_OF_BOUNDS: - return "Out of bounds"; - case TB_ERR_UNSUPPORTED_TERM: - return "Unsupported terminal"; - case TB_ERR_CAP_COLLISION: - return "Termcaps collision"; - case TB_ERR_RESIZE_SSCANF: - return "Terminal width/height not received by sscanf() after " - "resize"; - case TB_ERR: - case TB_ERR_INIT_OPEN: - case TB_ERR_READ: - case TB_ERR_RESIZE_IOCTL: - case TB_ERR_RESIZE_PIPE: - case TB_ERR_RESIZE_SIGACTION: - case TB_ERR_POLL: - case TB_ERR_TCGETATTR: - case TB_ERR_TCSETATTR: - case TB_ERR_RESIZE_WRITE: - case TB_ERR_RESIZE_POLL: - case TB_ERR_RESIZE_READ: - default: - strerror_r(global.last_errno, global.errbuf, sizeof(global.errbuf)); - return (const char *)global.errbuf; - } -} - -int tb_has_truecolor(void) { -#if TB_OPT_ATTR_W >= 32 - return 1; -#else - return 0; -#endif -} - -int tb_has_egc(void) { -#ifdef TB_OPT_EGC - return 1; -#else - return 0; -#endif -} - -int tb_attr_width(void) { - return TB_OPT_ATTR_W; -} - -const char *tb_version(void) { - return TB_VERSION_STR; -} - -static int tb_reset(void) { - int ttyfd_open = global.ttyfd_open; - memset(&global, 0, sizeof(global)); - global.ttyfd = -1; - global.rfd = -1; - global.wfd = -1; - global.ttyfd_open = ttyfd_open; - global.resize_pipefd[0] = -1; - global.resize_pipefd[1] = -1; - global.width = -1; - global.height = -1; - global.cursor_x = -1; - global.cursor_y = -1; - global.last_x = -1; - global.last_y = -1; - global.fg = TB_DEFAULT; - global.bg = TB_DEFAULT; - global.last_fg = ~global.fg; - global.last_bg = ~global.bg; - global.input_mode = TB_INPUT_ESC; - global.output_mode = TB_OUTPUT_NORMAL; - return TB_OK; -} - -static int init_term_attrs(void) { - if (global.ttyfd < 0) return TB_OK; - - if (tcgetattr(global.ttyfd, &global.orig_tios) != 0) { - global.last_errno = errno; - return TB_ERR_TCGETATTR; - } - - struct termios tios; - memcpy(&tios, &global.orig_tios, sizeof(tios)); - global.has_orig_tios = 1; - - cfmakeraw(&tios); - tios.c_cc[VMIN] = 1; - tios.c_cc[VTIME] = 0; - - if (tcsetattr(global.ttyfd, TCSAFLUSH, &tios) != 0) { - global.last_errno = errno; - return TB_ERR_TCSETATTR; - } - - return TB_OK; -} - -int tb_printf_inner(int x, int y, uintattr_t fg, uintattr_t bg, size_t *out_w, - const char *fmt, va_list vl) { - int rv; - char buf[TB_OPT_PRINTF_BUF]; - rv = vsnprintf(buf, sizeof(buf), fmt, vl); - if (rv < 0 || rv >= (int)sizeof(buf)) { - return TB_ERR; - } - return tb_print_ex(x, y, fg, bg, out_w, buf); -} - -static int init_term_caps(void) { - if (load_terminfo() == TB_OK) { - return parse_terminfo_caps(); - } - return load_builtin_caps(); -} - -static int init_cap_trie(void) { - int rv, i; - - // Add caps from terminfo or built-in - // - // Collisions are expected as some terminfo entries have dupes. (For - // example, att605-pc collides on TB_CAP_F4 and TB_CAP_DELETE.) First cap - // in TB_CAP_* index order will win. - // - // TODO: Reorder TB_CAP_* so more critical caps come first. - for (i = 0; i < TB_CAP__COUNT_KEYS; i++) { - rv = cap_trie_add(global.caps[i], tb_key_i(i), 0); - if (rv != TB_OK && rv != TB_ERR_CAP_COLLISION) return rv; - } - - // Add built-in mod caps - // - // Collisions are OK here as well. This can happen if global.caps collides - // with builtin_mod_caps. It is desirable to give precedence to global.caps - // here. - for (i = 0; builtin_mod_caps[i].cap != NULL; i++) { - rv = cap_trie_add(builtin_mod_caps[i].cap, builtin_mod_caps[i].key, - builtin_mod_caps[i].mod); - if (rv != TB_OK && rv != TB_ERR_CAP_COLLISION) return rv; - } - - return TB_OK; -} - -static int cap_trie_add(const char *cap, uint16_t key, uint8_t mod) { - struct cap_trie_t *next, *node = &global.cap_trie; - size_t i, j; - - if (!cap || strlen(cap) <= 0) return TB_OK; // Nothing to do for empty caps - - for (i = 0; cap[i] != '\0'; i++) { - char c = cap[i]; - next = NULL; - - // Check if c is already a child of node - for (j = 0; j < node->nchildren; j++) { - if (node->children[j].c == c) { - next = &node->children[j]; - break; - } - } - if (!next) { - // We need to add a new child to node - node->nchildren += 1; - node->children = (struct cap_trie_t *)tb_realloc(node->children, - sizeof(*node) * node->nchildren); - if (!node->children) { - return TB_ERR_MEM; - } - next = &node->children[node->nchildren - 1]; - memset(next, 0, sizeof(*next)); - next->c = c; - } - - // Continue - node = next; - } - - if (node->is_leaf) { - // Already a leaf here - return TB_ERR_CAP_COLLISION; - } - - node->is_leaf = 1; - node->key = key; - node->mod = mod; - return TB_OK; -} - -static int cap_trie_find(const char *buf, size_t nbuf, struct cap_trie_t **last, - size_t *depth) { - struct cap_trie_t *next, *node = &global.cap_trie; - size_t i, j; - *last = node; - *depth = 0; - for (i = 0; i < nbuf; i++) { - char c = buf[i]; - next = NULL; - - // Find c in node.children - for (j = 0; j < node->nchildren; j++) { - if (node->children[j].c == c) { - next = &node->children[j]; - break; - } - } - if (!next) { - // Not found - return TB_OK; - } - node = next; - *last = node; - *depth += 1; - if (node->is_leaf && node->nchildren < 1) { - break; - } - } - return TB_OK; -} - -static int cap_trie_deinit(struct cap_trie_t *node) { - size_t j; - for (j = 0; j < node->nchildren; j++) { - cap_trie_deinit(&node->children[j]); - } - if (node->children) tb_free(node->children); - memset(node, 0, sizeof(*node)); - return TB_OK; -} - -static int init_resize_handler(void) { - if (pipe(global.resize_pipefd) != 0) { - global.last_errno = errno; - return TB_ERR_RESIZE_PIPE; - } - - struct sigaction sa; - memset(&sa, 0, sizeof(sa)); - sa.sa_handler = handle_resize; - if (sigaction(SIGWINCH, &sa, NULL) != 0) { - global.last_errno = errno; - return TB_ERR_RESIZE_SIGACTION; - } - - return TB_OK; -} - -static int send_init_escape_codes(void) { - int rv; - if_err_return(rv, bytebuf_puts(&global.out, global.caps[TB_CAP_ENTER_CA])); - if_err_return(rv, - bytebuf_puts(&global.out, global.caps[TB_CAP_ENTER_KEYPAD])); - if_err_return(rv, - bytebuf_puts(&global.out, global.caps[TB_CAP_HIDE_CURSOR])); - return TB_OK; -} - -static int send_clear(void) { - int rv; - - if_err_return(rv, send_attr(global.fg, global.bg)); - if_err_return(rv, - bytebuf_puts(&global.out, global.caps[TB_CAP_CLEAR_SCREEN])); - - if_err_return(rv, send_cursor_if(global.cursor_x, global.cursor_y)); - if_err_return(rv, bytebuf_flush(&global.out, global.wfd)); - - global.last_x = -1; - global.last_y = -1; - - return TB_OK; -} - -static int update_term_size(void) { - int rv, ioctl_errno; - - if (global.ttyfd < 0) return TB_OK; - - struct winsize sz; - memset(&sz, 0, sizeof(sz)); - - // Try ioctl TIOCGWINSZ - if (ioctl(global.ttyfd, TIOCGWINSZ, &sz) == 0) { - global.width = sz.ws_col; - global.height = sz.ws_row; - return TB_OK; - } - ioctl_errno = errno; - - // Try >cursor(9999,9999), >u7, = 0) { - bytebuf_puts(&global.out, global.caps[TB_CAP_SHOW_CURSOR]); - bytebuf_puts(&global.out, global.caps[TB_CAP_SGR0]); - bytebuf_puts(&global.out, global.caps[TB_CAP_CLEAR_SCREEN]); - bytebuf_puts(&global.out, global.caps[TB_CAP_EXIT_CA]); - bytebuf_puts(&global.out, global.caps[TB_CAP_EXIT_KEYPAD]); - bytebuf_puts(&global.out, TB_HARDCAP_EXIT_MOUSE); - bytebuf_flush(&global.out, global.wfd); - } - if (global.ttyfd >= 0) { - if (global.has_orig_tios) { - tcsetattr(global.ttyfd, TCSAFLUSH, &global.orig_tios); - } - if (global.ttyfd_open) { - close(global.ttyfd); - global.ttyfd_open = 0; - } - } - - struct sigaction sa; - memset(&sa, 0, sizeof(sa)); - sa.sa_handler = SIG_DFL; - sigaction(SIGWINCH, &sa, NULL); - if (global.resize_pipefd[0] >= 0) close(global.resize_pipefd[0]); - if (global.resize_pipefd[1] >= 0) close(global.resize_pipefd[1]); - - cellbuf_free(&global.back); - cellbuf_free(&global.front); - bytebuf_free(&global.in); - bytebuf_free(&global.out); - - if (global.terminfo) tb_free(global.terminfo); - - cap_trie_deinit(&global.cap_trie); - - tb_reset(); - return TB_OK; -} - -static int load_terminfo(void) { - int rv; - char tmp[TB_PATH_MAX]; - - // See terminfo(5) "Fetching Compiled Descriptions" for a description of - // this behavior. Some of these paths are compile-time ncurses options, so - // best guesses are used here. - const char *term = getenv("TERM"); - if (!term) return TB_ERR; - - // If TERMINFO is set, try that directory and stop - const char *terminfo = getenv("TERMINFO"); - if (terminfo) return load_terminfo_from_path(terminfo, term); - - // Next try ~/.terminfo - const char *home = getenv("HOME"); - if (home) { - snprintf_or_return(rv, tmp, sizeof(tmp), "%s/.terminfo", home); - if_ok_return(rv, load_terminfo_from_path(tmp, term)); - } - - // Next try TERMINFO_DIRS - // - // Note, empty entries are supposed to be interpretted as the "compiled-in - // default", which is of course system-dependent. Previously /etc/terminfo - // was used here. Let's skip empty entries altogether rather than give - // precedence to a guess, and check common paths after this loop. - const char *dirs = getenv("TERMINFO_DIRS"); - if (dirs) { - snprintf_or_return(rv, tmp, sizeof(tmp), "%s", dirs); - char *dir = strtok(tmp, ":"); - while (dir) { - const char *cdir = dir; - if (*cdir != '\0') { - if_ok_return(rv, load_terminfo_from_path(cdir, term)); - } - dir = strtok(NULL, ":"); - } - } - -#ifdef TB_TERMINFO_DIR - if_ok_return(rv, load_terminfo_from_path(TB_TERMINFO_DIR, term)); -#endif - if_ok_return(rv, load_terminfo_from_path("/usr/local/etc/terminfo", term)); - if_ok_return(rv, - load_terminfo_from_path("/usr/local/share/terminfo", term)); - if_ok_return(rv, load_terminfo_from_path("/usr/local/lib/terminfo", term)); - if_ok_return(rv, load_terminfo_from_path("/etc/terminfo", term)); - if_ok_return(rv, load_terminfo_from_path("/usr/share/terminfo", term)); - if_ok_return(rv, load_terminfo_from_path("/usr/lib/terminfo", term)); - if_ok_return(rv, load_terminfo_from_path("/usr/share/lib/terminfo", term)); - if_ok_return(rv, load_terminfo_from_path("/lib/terminfo", term)); - - return TB_ERR; -} - -static int load_terminfo_from_path(const char *path, const char *term) { - int rv; - char tmp[TB_PATH_MAX]; - - // Look for term at this terminfo location, e.g., /x/xterm - snprintf_or_return(rv, tmp, sizeof(tmp), "%s/%c/%s", path, term[0], term); - if_ok_return(rv, read_terminfo_path(tmp)); - -#ifdef __APPLE__ - // Try the Darwin equivalent path, e.g., /78/xterm - snprintf_or_return(rv, tmp, sizeof(tmp), "%s/%x/%s", path, term[0], term); - return read_terminfo_path(tmp); -#endif - - return TB_ERR; -} - -static int read_terminfo_path(const char *path) { - FILE *fp = fopen(path, "rb"); - if (!fp) return TB_ERR; - - struct stat st; - if (fstat(fileno(fp), &st) != 0) { - fclose(fp); - return TB_ERR; - } - - size_t fsize = st.st_size; - char *data = (char *)tb_malloc(fsize); - if (!data) { - fclose(fp); - return TB_ERR; - } - - if (fread(data, 1, fsize, fp) != fsize) { - fclose(fp); - tb_free(data); - return TB_ERR; - } - - global.terminfo = data; - global.nterminfo = fsize; - - fclose(fp); - return TB_OK; -} - -static int parse_terminfo_caps(void) { - // See term(5) "LEGACY STORAGE FORMAT" and "EXTENDED STORAGE FORMAT" for a - // description of this behavior. - - // Ensure there's at least a header's worth of data - if (global.nterminfo < 6 * (int)sizeof(int16_t)) return TB_ERR; - - int16_t magic_number, nbytes_names, nbytes_bools, num_ints, num_offsets, - nbytes_strings; - size_t nbytes_header = 6 * sizeof(int16_t); - // header[0] the magic number (octal 0432 or 01036) - // header[1] the size, in bytes, of the names section - // header[2] the number of bytes in the boolean section - // header[3] the number of short integers in the numbers section - // header[4] the number of offsets (short integers) in the strings section - // header[5] the size, in bytes, of the string table - get_terminfo_int16(0 * sizeof(int16_t), &magic_number); - get_terminfo_int16(1 * sizeof(int16_t), &nbytes_names); - get_terminfo_int16(2 * sizeof(int16_t), &nbytes_bools); - get_terminfo_int16(3 * sizeof(int16_t), &num_ints); - get_terminfo_int16(4 * sizeof(int16_t), &num_offsets); - get_terminfo_int16(5 * sizeof(int16_t), &nbytes_strings); - - // Legacy ints are 16-bit, extended ints are 32-bit - const int bytes_per_int = magic_number == 01036 ? 4 // 32-bit - : 2; // 16-bit - - // > Between the boolean section and the number section, a null byte will be - // > inserted, if necessary, to ensure that the number section begins on an - // > even byte - const int align_offset = (nbytes_names + nbytes_bools) % 2 != 0 ? 1 : 0; - - const int pos_str_offsets = - nbytes_header // header (12 bytes) - + nbytes_names // length of names section - + nbytes_bools // length of boolean section - + align_offset + - (num_ints * bytes_per_int); // length of numbers section - - const int pos_str_table = - pos_str_offsets + - (num_offsets * sizeof(int16_t)); // length of string offsets table - - // Load caps - int i; - for (i = 0; i < TB_CAP__COUNT; i++) { - const char *cap = get_terminfo_string(pos_str_offsets, num_offsets, - pos_str_table, nbytes_strings, terminfo_cap_indexes[i]); - if (!cap) { - // Something is not right - return TB_ERR; - } - global.caps[i] = cap; - } - - return TB_OK; -} - -static int load_builtin_caps(void) { - int i, j; - const char *term = getenv("TERM"); - - if (!term) return TB_ERR_NO_TERM; - - // Check for exact TERM match - for (i = 0; builtin_terms[i].name != NULL; i++) { - if (strcmp(term, builtin_terms[i].name) == 0) { - for (j = 0; j < TB_CAP__COUNT; j++) { - global.caps[j] = builtin_terms[i].caps[j]; - } - return TB_OK; - } - } - - // Check for partial TERM or alias match - for (i = 0; builtin_terms[i].name != NULL; i++) { - if (strstr(term, builtin_terms[i].name) != NULL || - (*(builtin_terms[i].alias) != '\0' && - strstr(term, builtin_terms[i].alias) != NULL)) - { - for (j = 0; j < TB_CAP__COUNT; j++) { - global.caps[j] = builtin_terms[i].caps[j]; - } - return TB_OK; - } - } - - return TB_ERR_UNSUPPORTED_TERM; -} - -static const char *get_terminfo_string(int16_t offsets_pos, int16_t offsets_len, - int16_t table_pos, int16_t table_size, int16_t index) { - if (index >= offsets_len) { - // An index beyond the offset table indicates absent - // See `convert_strings` in tinfo `read_entry.c` - return ""; - } - - int16_t table_offset; - int table_offset_offset = (int)offsets_pos + (index * (int)sizeof(int16_t)); - if (get_terminfo_int16(table_offset_offset, &table_offset) != TB_OK) { - // offset beyond end of terminfo entry - // Truncated/corrupt terminfo entry? - return NULL; - } - - if (table_offset < 0 || table_offset >= table_size) { - // A negative offset indicates absent - // An offset beyond the string table indicates absent - // See `convert_strings` in tinfo `read_entry.c` - return ""; - } - - int str_offset = (int)table_pos + (int)table_offset; - if (str_offset >= (int)global.nterminfo) { - // string beyond end of terminfo entry - // Truncated/corrupt terminfo entry? - return NULL; - } - - return (const char *)(global.terminfo + str_offset); -} - -static int get_terminfo_int16(int offset, int16_t *val) { - if (offset < 0 || offset >= (int)global.nterminfo) { - *val = -1; - return TB_ERR; - } - memcpy(val, global.terminfo + offset, sizeof(int16_t)); - return TB_OK; -} - -static int wait_event(struct tb_event *event, int timeout) { - int rv; - char buf[TB_OPT_READ_BUF]; - - memset(event, 0, sizeof(*event)); - if_ok_return(rv, extract_event(event)); - - fd_set fds; - struct timeval tv; - tv.tv_sec = timeout / 1000; - tv.tv_usec = (timeout - (tv.tv_sec * 1000)) * 1000; - - do { - FD_ZERO(&fds); - FD_SET(global.rfd, &fds); - FD_SET(global.resize_pipefd[0], &fds); - - int maxfd = global.resize_pipefd[0] > global.rfd - ? global.resize_pipefd[0] - : global.rfd; - - int select_rv = - select(maxfd + 1, &fds, NULL, NULL, (timeout < 0) ? NULL : &tv); - - if (select_rv < 0) { - // Let EINTR/EAGAIN bubble up - global.last_errno = errno; - return TB_ERR_POLL; - } else if (select_rv == 0) { - return TB_ERR_NO_EVENT; - } - - int tty_has_events = (FD_ISSET(global.rfd, &fds)); - int resize_has_events = (FD_ISSET(global.resize_pipefd[0], &fds)); - - if (tty_has_events) { - ssize_t read_rv = read(global.rfd, buf, sizeof(buf)); - if (read_rv < 0) { - global.last_errno = errno; - return TB_ERR_READ; - } else if (read_rv > 0) { - bytebuf_nputs(&global.in, buf, read_rv); - } - } - - if (resize_has_events) { - int ignore = 0; - read(global.resize_pipefd[0], &ignore, sizeof(ignore)); - // TODO: Harden against errors encountered mid-resize - if_err_return(rv, update_term_size()); - if_err_return(rv, resize_cellbufs()); - event->type = TB_EVENT_RESIZE; - event->w = global.width; - event->h = global.height; - return TB_OK; - } - - memset(event, 0, sizeof(*event)); - if_ok_return(rv, extract_event(event)); - } while (timeout == -1); - - return rv; -} - -static int extract_event(struct tb_event *event) { - int rv; - struct bytebuf_t *in = &global.in; - - if (in->len == 0) return TB_ERR; - - if (in->buf[0] == '\x1b') { - // Escape sequence? - // In TB_INPUT_ESC, skip if the buffer is a single escape char - if (!((global.input_mode & TB_INPUT_ESC) && in->len == 1)) { - if_ok_or_need_more_return(rv, extract_esc(event)); - } - - // Escape key? - if (global.input_mode & TB_INPUT_ESC) { - event->type = TB_EVENT_KEY; - event->ch = 0; - event->key = TB_KEY_ESC; - event->mod = 0; - bytebuf_shift(in, 1); - return TB_OK; - } - - // Recurse for alt key - event->mod |= TB_MOD_ALT; - bytebuf_shift(in, 1); - return extract_event(event); - } - - // ASCII control key? - int is_ctrl = - (uint16_t)in->buf[0] < TB_KEY_SPACE || in->buf[0] == TB_KEY_BACKSPACE2; - if (is_ctrl) { - event->type = TB_EVENT_KEY; - event->ch = 0; - event->key = (uint16_t)in->buf[0]; - event->mod |= TB_MOD_CTRL; - bytebuf_shift(in, 1); - return TB_OK; - } - - // UTF-8? - if (in->len >= (size_t)tb_utf8_char_length(in->buf[0])) { - event->type = TB_EVENT_KEY; - tb_utf8_char_to_unicode(&event->ch, in->buf); - event->key = 0; - bytebuf_shift(in, tb_utf8_char_length(in->buf[0])); - return TB_OK; - } - - // Need more input - return TB_ERR; -} - -static int extract_esc(struct tb_event *event) { - int rv; - if_ok_or_need_more_return(rv, extract_esc_user(event, 0)); - if_ok_or_need_more_return(rv, extract_esc_cap(event)); - if_ok_or_need_more_return(rv, extract_esc_mouse(event)); - if_ok_or_need_more_return(rv, extract_esc_user(event, 1)); - return TB_ERR; -} - -static int extract_esc_user(struct tb_event *event, int is_post) { - int rv; - size_t consumed = 0; - struct bytebuf_t *in = &global.in; - int (*fn)(struct tb_event *, size_t *); - - fn = is_post ? global.fn_extract_esc_post : global.fn_extract_esc_pre; - - if (!fn) return TB_ERR; - - rv = fn(event, &consumed); - if (rv == TB_OK) bytebuf_shift(in, consumed); - - if_ok_or_need_more_return(rv, rv); - return TB_ERR; -} - -static int extract_esc_cap(struct tb_event *event) { - int rv; - struct bytebuf_t *in = &global.in; - struct cap_trie_t *node; - size_t depth; - - if_err_return(rv, cap_trie_find(in->buf, in->len, &node, &depth)); - if (node->is_leaf) { - // Found a leaf node - event->type = TB_EVENT_KEY; - event->ch = 0; - event->key = node->key; - event->mod = node->mod; - bytebuf_shift(in, depth); - return TB_OK; - } else if (node->nchildren > 0 && in->len <= depth) { - // Found a branch node (not enough input) - return TB_ERR_NEED_MORE; - } - - return TB_ERR; -} - -static int extract_esc_mouse(struct tb_event *event) { - struct bytebuf_t *in = &global.in; - - enum { TYPE_VT200 = 0, TYPE_1006, TYPE_1015, TYPE_MAX }; - - const char *cmp[TYPE_MAX] = {// - // X10 mouse encoding, the simplest one - // \x1b [ M Cb Cx Cy - [TYPE_VT200] = "\x1b[M", - // xterm 1006 extended mode or urxvt 1015 extended mode - // xterm: \x1b [ < Cb ; Cx ; Cy (M or m) - [TYPE_1006] = "\x1b[<", - // urxvt: \x1b [ Cb ; Cx ; Cy M - [TYPE_1015] = "\x1b["}; - - int type = 0; - int ret = TB_ERR; - - // Unrolled at compile-time (probably) - for (; type < TYPE_MAX; type++) { - size_t size = strlen(cmp[type]); - - if (in->len >= size && (strncmp(cmp[type], in->buf, size)) == 0) { - break; - } - } - - if (type == TYPE_MAX) { - ret = TB_ERR; // No match - return ret; - } - - size_t buf_shift = 0; - - switch (type) { - case TYPE_VT200: - if (in->len >= 6) { - int b = in->buf[3] - 0x20; - int fail = 0; - - switch (b & 3) { - case 0: - event->key = ((b & 64) != 0) ? TB_KEY_MOUSE_WHEEL_UP - : TB_KEY_MOUSE_LEFT; - break; - case 1: - event->key = ((b & 64) != 0) ? TB_KEY_MOUSE_WHEEL_DOWN - : TB_KEY_MOUSE_MIDDLE; - break; - case 2: - event->key = TB_KEY_MOUSE_RIGHT; - break; - case 3: - event->key = TB_KEY_MOUSE_RELEASE; - break; - default: - ret = TB_ERR; - fail = 1; - break; - } - - if (!fail) { - if ((b & 32) != 0) { - event->mod |= TB_MOD_MOTION; - } - - // the coord is 1,1 for upper left - event->x = ((uint8_t)in->buf[4]) - 0x21; - event->y = ((uint8_t)in->buf[5]) - 0x21; - - ret = TB_OK; - } - - buf_shift = 6; - } - break; - case TYPE_1006: - // fallthrough - case TYPE_1015: { - size_t index_fail = (size_t)-1; - - enum { - FIRST_M = 0, - FIRST_SEMICOLON, - LAST_SEMICOLON, - FIRST_LAST_MAX - }; - - size_t indices[FIRST_LAST_MAX] = {index_fail, index_fail, - index_fail}; - int m_is_capital = 0; - - for (size_t i = 0; i < in->len; i++) { - if (in->buf[i] == ';') { - if (indices[FIRST_SEMICOLON] == index_fail) { - indices[FIRST_SEMICOLON] = i; - } else { - indices[LAST_SEMICOLON] = i; - } - } else if (indices[FIRST_M] == index_fail) { - if (in->buf[i] == 'm' || in->buf[i] == 'M') { - m_is_capital = (in->buf[i] == 'M'); - indices[FIRST_M] = i; - } - } - } - - if (indices[FIRST_M] == index_fail || - indices[FIRST_SEMICOLON] == index_fail || - indices[LAST_SEMICOLON] == index_fail) - { - ret = TB_ERR; - } else { - int start = (type == TYPE_1015 ? 2 : 3); - - unsigned n1 = strtoul(&in->buf[start], NULL, 10); - unsigned n2 = - strtoul(&in->buf[indices[FIRST_SEMICOLON] + 1], NULL, 10); - unsigned n3 = - strtoul(&in->buf[indices[LAST_SEMICOLON] + 1], NULL, 10); - - if (type == TYPE_1015) { - n1 -= 0x20; - } - - int fail = 0; - - switch (n1 & 3) { - case 0: - event->key = ((n1 & 64) != 0) ? TB_KEY_MOUSE_WHEEL_UP - : TB_KEY_MOUSE_LEFT; - break; - case 1: - event->key = ((n1 & 64) != 0) ? TB_KEY_MOUSE_WHEEL_DOWN - : TB_KEY_MOUSE_MIDDLE; - break; - case 2: - event->key = TB_KEY_MOUSE_RIGHT; - break; - case 3: - event->key = TB_KEY_MOUSE_RELEASE; - break; - default: - ret = TB_ERR; - fail = 1; - break; - } - - buf_shift = in->len; - - if (!fail) { - if (!m_is_capital) { - // on xterm mouse release is signaled by lowercase m - event->key = TB_KEY_MOUSE_RELEASE; - } - - if ((n1 & 32) != 0) { - event->mod |= TB_MOD_MOTION; - } - - event->x = ((uint8_t)n2) - 1; - event->y = ((uint8_t)n3) - 1; - - ret = TB_OK; - } - } - } break; - case TYPE_MAX: - ret = TB_ERR; - } - - if (buf_shift > 0) bytebuf_shift(in, buf_shift); - - if (ret == TB_OK) event->type = TB_EVENT_MOUSE; - - return ret; -} - -static int resize_cellbufs(void) { - int rv; - if_err_return(rv, - cellbuf_resize(&global.back, global.width, global.height)); - if_err_return(rv, - cellbuf_resize(&global.front, global.width, global.height)); - if_err_return(rv, cellbuf_clear(&global.front)); - if_err_return(rv, send_clear()); - return TB_OK; -} - -static void handle_resize(int sig) { - int errno_copy = errno; - write(global.resize_pipefd[1], &sig, sizeof(sig)); - errno = errno_copy; -} - -static int send_attr(uintattr_t fg, uintattr_t bg) { - int rv; - - if (fg == global.last_fg && bg == global.last_bg) { - return TB_OK; - } - - if_err_return(rv, bytebuf_puts(&global.out, global.caps[TB_CAP_SGR0])); - - uint32_t cfg, cbg; - switch (global.output_mode) { - default: - case TB_OUTPUT_NORMAL: - // The minus 1 below is because our colors are 1-indexed starting - // from black. Black is represented by a 30, 40, 90, or 100 for fg, - // bg, bright fg, or bright bg respectively. Red is 31, 41, 91, - // 101, etc. - cfg = (fg & TB_BRIGHT ? 90 : 30) + (fg & 0x0f) - 1; - cbg = (bg & TB_BRIGHT ? 100 : 40) + (bg & 0x0f) - 1; - break; - - case TB_OUTPUT_256: - cfg = fg & 0xff; - cbg = bg & 0xff; - if (fg & TB_HI_BLACK) cfg = 0; - if (bg & TB_HI_BLACK) cbg = 0; - break; - - case TB_OUTPUT_216: - cfg = fg & 0xff; - cbg = bg & 0xff; - if (cfg > 216) cfg = 216; - if (cbg > 216) cbg = 216; - cfg += 0x0f; - cbg += 0x0f; - break; - - case TB_OUTPUT_GRAYSCALE: - cfg = fg & 0xff; - cbg = bg & 0xff; - if (cfg > 24) cfg = 24; - if (cbg > 24) cbg = 24; - cfg += 0xe7; - cbg += 0xe7; - break; - -#if TB_OPT_ATTR_W >= 32 - case TB_OUTPUT_TRUECOLOR: - cfg = fg & 0xffffff; - cbg = bg & 0xffffff; - if (fg & TB_HI_BLACK) cfg = 0; - if (bg & TB_HI_BLACK) cbg = 0; - break; -#endif - } - - if (fg & TB_BOLD) - if_err_return(rv, bytebuf_puts(&global.out, global.caps[TB_CAP_BOLD])); - - if (fg & TB_BLINK) - if_err_return(rv, bytebuf_puts(&global.out, global.caps[TB_CAP_BLINK])); - - if (fg & TB_UNDERLINE) - if_err_return(rv, - bytebuf_puts(&global.out, global.caps[TB_CAP_UNDERLINE])); - - if (fg & TB_ITALIC) - if_err_return(rv, - bytebuf_puts(&global.out, global.caps[TB_CAP_ITALIC])); - - if (fg & TB_DIM) - if_err_return(rv, bytebuf_puts(&global.out, global.caps[TB_CAP_DIM])); - -#if TB_OPT_ATTR_W == 64 - if (fg & TB_STRIKEOUT) - if_err_return(rv, bytebuf_puts(&global.out, TB_HARDCAP_STRIKEOUT)); - - if (fg & TB_UNDERLINE_2) - if_err_return(rv, bytebuf_puts(&global.out, TB_HARDCAP_UNDERLINE_2)); - - if (fg & TB_OVERLINE) - if_err_return(rv, bytebuf_puts(&global.out, TB_HARDCAP_OVERLINE)); - - if (fg & TB_INVISIBLE) - if_err_return(rv, - bytebuf_puts(&global.out, global.caps[TB_CAP_INVISIBLE])); -#endif - - if ((fg & TB_REVERSE) || (bg & TB_REVERSE)) - if_err_return(rv, - bytebuf_puts(&global.out, global.caps[TB_CAP_REVERSE])); - - int fg_is_default = (fg & 0xff) == 0; - int bg_is_default = (bg & 0xff) == 0; - if (global.output_mode == TB_OUTPUT_256) { - if (fg & TB_HI_BLACK) fg_is_default = 0; - if (bg & TB_HI_BLACK) bg_is_default = 0; - } -#if TB_OPT_ATTR_W >= 32 - if (global.output_mode == TB_OUTPUT_TRUECOLOR) { - fg_is_default = ((fg & 0xffffff) == 0) && ((fg & TB_HI_BLACK) == 0); - bg_is_default = ((bg & 0xffffff) == 0) && ((bg & TB_HI_BLACK) == 0); - } -#endif - - if_err_return(rv, send_sgr(cfg, cbg, fg_is_default, bg_is_default)); - - global.last_fg = fg; - global.last_bg = bg; - - return TB_OK; -} - -static int send_sgr(uint32_t cfg, uint32_t cbg, int fg_is_default, - int bg_is_default) { - int rv; - char nbuf[32]; - - if (fg_is_default && bg_is_default) { - return TB_OK; - } - - switch (global.output_mode) { - default: - case TB_OUTPUT_NORMAL: - send_literal(rv, "\x1b["); - if (!fg_is_default) { - send_num(rv, nbuf, cfg); - if (!bg_is_default) { - send_literal(rv, ";"); - } - } - if (!bg_is_default) { - send_num(rv, nbuf, cbg); - } - send_literal(rv, "m"); - break; - - case TB_OUTPUT_256: - case TB_OUTPUT_216: - case TB_OUTPUT_GRAYSCALE: - send_literal(rv, "\x1b["); - if (!fg_is_default) { - send_literal(rv, "38;5;"); - send_num(rv, nbuf, cfg); - if (!bg_is_default) { - send_literal(rv, ";"); - } - } - if (!bg_is_default) { - send_literal(rv, "48;5;"); - send_num(rv, nbuf, cbg); - } - send_literal(rv, "m"); - break; - -#if TB_OPT_ATTR_W >= 32 - case TB_OUTPUT_TRUECOLOR: - send_literal(rv, "\x1b["); - if (!fg_is_default) { - send_literal(rv, "38;2;"); - send_num(rv, nbuf, (cfg >> 16) & 0xff); - send_literal(rv, ";"); - send_num(rv, nbuf, (cfg >> 8) & 0xff); - send_literal(rv, ";"); - send_num(rv, nbuf, cfg & 0xff); - if (!bg_is_default) { - send_literal(rv, ";"); - } - } - if (!bg_is_default) { - send_literal(rv, "48;2;"); - send_num(rv, nbuf, (cbg >> 16) & 0xff); - send_literal(rv, ";"); - send_num(rv, nbuf, (cbg >> 8) & 0xff); - send_literal(rv, ";"); - send_num(rv, nbuf, cbg & 0xff); - } - send_literal(rv, "m"); - break; -#endif - } - return TB_OK; -} - -static int send_cursor_if(int x, int y) { - int rv; - char nbuf[32]; - if (x < 0 || y < 0) { - return TB_OK; - } - send_literal(rv, "\x1b["); - send_num(rv, nbuf, y + 1); - send_literal(rv, ";"); - send_num(rv, nbuf, x + 1); - send_literal(rv, "H"); - return TB_OK; -} - -static int send_char(int x, int y, uint32_t ch) { - return send_cluster(x, y, &ch, 1); -} - -static int send_cluster(int x, int y, uint32_t *ch, size_t nch) { - int rv; - char chu8[8]; - - if (global.last_x != x - 1 || global.last_y != y) { - if_err_return(rv, send_cursor_if(x, y)); - } - global.last_x = x; - global.last_y = y; - - int i; - for (i = 0; i < (int)nch; i++) { - uint32_t ch32 = *(ch + i); - if (!tb_iswprint(ch32)) { - ch32 = 0xfffd; // replace non-printable codepoints with U+FFFD - } - int chu8_len = tb_utf8_unicode_to_char(chu8, ch32); - if_err_return(rv, bytebuf_nputs(&global.out, chu8, (size_t)chu8_len)); - } - - return TB_OK; -} - -static int convert_num(uint32_t num, char *buf) { - int i, l = 0; - char ch; - do { - buf[l++] = (char)('0' + (num % 10)); - num /= 10; - } while (num); - for (i = 0; i < l / 2; i++) { - ch = buf[i]; - buf[i] = buf[l - 1 - i]; - buf[l - 1 - i] = ch; - } - return l; -} - -static int cell_cmp(struct tb_cell *a, struct tb_cell *b) { - if (a->ch != b->ch || a->fg != b->fg || a->bg != b->bg) { - return 1; - } -#ifdef TB_OPT_EGC - if (a->nech != b->nech) { - return 1; - } else if (a->nech > 0) { // a->nech == b->nech - return memcmp(a->ech, b->ech, a->nech); - } -#endif - return 0; -} - -static int cell_copy(struct tb_cell *dst, struct tb_cell *src) { -#ifdef TB_OPT_EGC - if (src->nech > 0) { - return cell_set(dst, src->ech, src->nech, src->fg, src->bg); - } -#endif - return cell_set(dst, &src->ch, 1, src->fg, src->bg); -} - -static int cell_set(struct tb_cell *cell, uint32_t *ch, size_t nch, - uintattr_t fg, uintattr_t bg) { - // TODO: iswprint ch? - cell->ch = ch ? *ch : 0; - cell->fg = fg; - cell->bg = bg; -#ifdef TB_OPT_EGC - if (nch <= 1) { - cell->nech = 0; - } else { - int rv; - if_err_return(rv, cell_reserve_ech(cell, nch + 1)); - memcpy(cell->ech, ch, sizeof(*ch) * nch); - cell->ech[nch] = '\0'; - cell->nech = nch; - } -#else - (void)nch; - (void)cell_reserve_ech; -#endif - return TB_OK; -} - -static int cell_reserve_ech(struct tb_cell *cell, size_t n) { -#ifdef TB_OPT_EGC - if (cell->cech >= n) return TB_OK; - cell->ech = (uint32_t *)tb_realloc(cell->ech, n * sizeof(cell->ch)); - if (!cell->ech) return TB_ERR_MEM; - cell->cech = n; - return TB_OK; -#else - (void)cell; - (void)n; - return TB_ERR; -#endif -} - -static int cell_free(struct tb_cell *cell) { -#ifdef TB_OPT_EGC - if (cell->ech) tb_free(cell->ech); -#endif - memset(cell, 0, sizeof(*cell)); - return TB_OK; -} - -static int cellbuf_init(struct cellbuf_t *c, int w, int h) { - c->cells = (struct tb_cell *)tb_malloc(sizeof(struct tb_cell) * w * h); - if (!c->cells) return TB_ERR_MEM; - memset(c->cells, 0, sizeof(struct tb_cell) * w * h); - c->width = w; - c->height = h; - return TB_OK; -} - -static int cellbuf_free(struct cellbuf_t *c) { - if (c->cells) { - int i; - for (i = 0; i < c->width * c->height; i++) { - cell_free(&c->cells[i]); - } - tb_free(c->cells); - } - memset(c, 0, sizeof(*c)); - return TB_OK; -} - -static int cellbuf_clear(struct cellbuf_t *c) { - int rv, i; - uint32_t space = (uint32_t)' '; - for (i = 0; i < c->width * c->height; i++) { - if_err_return(rv, - cell_set(&c->cells[i], &space, 1, global.fg, global.bg)); - } - return TB_OK; -} - -static int cellbuf_get(struct cellbuf_t *c, int x, int y, - struct tb_cell **out) { - if (!cellbuf_in_bounds(c, x, y)) { - *out = NULL; - return TB_ERR_OUT_OF_BOUNDS; - } - *out = &c->cells[(y * c->width) + x]; - return TB_OK; -} - -static int cellbuf_in_bounds(struct cellbuf_t *c, int x, int y) { - if (x < 0 || x >= c->width || y < 0 || y >= c->height) { - return 0; - } - return 1; -} - -static int cellbuf_resize(struct cellbuf_t *c, int w, int h) { - int rv; - - int ow = c->width; - int oh = c->height; - - if (ow == w && oh == h) { - return TB_OK; - } - - w = w < 1 ? 1 : w; - h = h < 1 ? 1 : h; - - int minw = (w < ow) ? w : ow; - int minh = (h < oh) ? h : oh; - - struct tb_cell *prev = c->cells; - - if_err_return(rv, cellbuf_init(c, w, h)); - if_err_return(rv, cellbuf_clear(c)); - - int x, y; - for (x = 0; x < minw; x++) { - for (y = 0; y < minh; y++) { - struct tb_cell *src, *dst; - src = &prev[(y * ow) + x]; - if_err_return(rv, cellbuf_get(c, x, y, &dst)); - if_err_return(rv, cell_copy(dst, src)); - } - } - - tb_free(prev); - - return TB_OK; -} - -static int bytebuf_puts(struct bytebuf_t *b, const char *str) { - if (!str || strlen(str) <= 0) return TB_OK; // Nothing to do for empty caps - return bytebuf_nputs(b, str, (size_t)strlen(str)); -} - -static int bytebuf_nputs(struct bytebuf_t *b, const char *str, size_t nstr) { - int rv; - if_err_return(rv, bytebuf_reserve(b, b->len + nstr + 1)); - memcpy(b->buf + b->len, str, nstr); - b->len += nstr; - b->buf[b->len] = '\0'; - return TB_OK; -} - -static int bytebuf_shift(struct bytebuf_t *b, size_t n) { - if (n > b->len) n = b->len; - size_t nmove = b->len - n; - memmove(b->buf, b->buf + n, nmove); - b->len -= n; - return TB_OK; -} - -static int bytebuf_flush(struct bytebuf_t *b, int fd) { - if (b->len <= 0) return TB_OK; - ssize_t write_rv = write(fd, b->buf, b->len); - if (write_rv < 0 || (size_t)write_rv != b->len) { - // Note, errno will be 0 on partial write - global.last_errno = errno; - return TB_ERR; - } - b->len = 0; - return TB_OK; -} - -static int bytebuf_reserve(struct bytebuf_t *b, size_t sz) { - if (b->cap >= sz) return TB_OK; - - size_t newcap = b->cap > 0 ? b->cap : 1; - while (newcap < sz) { - newcap *= 2; - } - - char *newbuf; - if (b->buf) { - newbuf = (char *)tb_realloc(b->buf, newcap); - } else { - newbuf = (char *)tb_malloc(newcap); - } - if (!newbuf) return TB_ERR_MEM; - - b->buf = newbuf; - b->cap = newcap; - return TB_OK; -} - -static int bytebuf_free(struct bytebuf_t *b) { - if (b->buf) tb_free(b->buf); - memset(b, 0, sizeof(*b)); - return TB_OK; -} - -int tb_iswprint(uint32_t ch) { -#ifdef TB_OPT_LIBC_WCHAR - return iswprint((wint_t)ch); -#else - return tb_iswprint_ex(ch, NULL); -#endif -} - -int tb_wcwidth(uint32_t ch) { -#ifdef TB_OPT_LIBC_WCHAR - return wcwidth((wchar_t)ch); -#else - return tb_wcswidth(&ch, 1); -#endif -} - -static int tb_wcswidth(uint32_t *ch, size_t nch) { -#ifdef TB_OPT_LIBC_WCHAR - return wcswidth((wchar_t *)ch, nch); -#else - int sw = 0; - size_t i = 0; - for (i = 0; i < nch; i++) { - int w; - tb_iswprint_ex(ch[i], &w); - if (w < 0) return -1; - sw += w; - } - return sw; -#endif -} - -static int tb_iswprint_ex(uint32_t ch, int *w) { -#ifdef TB_OPT_LIBC_WCHAR - if (w) *w = wcwidth((wint_t)ch); - return iswprint(ch); -#else - int lo = 0, hi = WCWIDTH_TABLE_LENGTH - 1; - if (ch >= 0x20 && ch <= 0x7e) { // fast path for ASCII - if (w) *w = 1; - return 1; - } else if (ch == 0) { // Special case for null, which is not represented in - if (w) *w = 0; // wcwidth_table since it's the only codepoint that is - return 0; // iswprint==0 but not wcwidth==-1. (It's wcwidth==0.) - } - while (lo <= hi) { - int i = (lo + hi) / 2; - if (ch < wcwidth_table[i].range_start) { - hi = i - 1; - } else if (ch > wcwidth_table[i].range_end) { - lo = i + 1; - } else { - if (w) *w = wcwidth_table[i].width; - return wcwidth_table[i].width >= 0 ? 1 : 0; - } - } - if (w) *w = -1; // invalid codepoint - return 0; -#endif -} - -#endif // TB_IMPL diff --git a/src/tui/TerminalBuffer.zig b/src/tui/TerminalBuffer.zig index ca072f6..ee72f4f 100644 --- a/src/tui/TerminalBuffer.zig +++ b/src/tui/TerminalBuffer.zig @@ -2,6 +2,7 @@ const std = @import("std"); const builtin = @import("builtin"); const interop = @import("../interop.zig"); const Cell = @import("Cell.zig"); +const termbox_extras = @import("termbox_extras.zig"); const Random = std.Random; @@ -113,8 +114,8 @@ pub fn cascade(self: TerminalBuffer) bool { 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); + _ = termbox_extras.tb_get_cell(@intCast(x), @intCast(y - 1), 1, &cell); + _ = termbox_extras.tb_get_cell(@intCast(x), @intCast(y), 1, &cell_under); const char: u8 = @truncate(cell.ch); if (std.ascii.isWhitespace(char)) continue; diff --git a/src/tui/termbox_extras.zig b/src/tui/termbox_extras.zig new file mode 100644 index 0000000..314a1d8 --- /dev/null +++ b/src/tui/termbox_extras.zig @@ -0,0 +1,26 @@ +const std = @import("std"); +const interop = @import("../interop.zig"); +const termbox = interop.termbox; + +pub fn tb_get_cell(x: c_int, y: c_int, back: c_int, cell: *termbox.tb_cell) c_int { + if (back == 0) { + return termbox.TB_ERR; + } + + const width = termbox.tb_width(); + const height = termbox.tb_height(); + + if (x < 0 or x >= width or y < 0 or y >= height) { + return termbox.TB_ERR_OUT_OF_BOUNDS; + } + + const buffer = termbox.tb_cell_buffer(); + if (buffer == null) { + return termbox.TB_ERR_NOT_INIT; + } + + const index = y * width + x; + cell.* = buffer[@intCast(index)]; + + return termbox.TB_OK; +} From 41f4378bfe2a4b7d3f9e2dbd7a24d67088cf6832 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Sun, 6 Jul 2025 09:25:06 +0200 Subject: [PATCH 217/530] Fix XDG_RUNTIME_DIR not being set properly (fixes #781) Signed-off-by: AnErrupTion --- src/auth.zig | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/src/auth.zig b/src/auth.zig index 35eee5e..aa853c9 100644 --- a/src/auth.zig +++ b/src/auth.zig @@ -41,8 +41,7 @@ pub fn authenticate(options: AuthOptions, current_environment: Environment, logi const pam_tty_str = try std.fmt.bufPrintZ(&pam_tty_buffer, "tty{d}", .{options.tty}); // Set the XDG environment variables - setXdgSessionEnv(current_environment.display_server); - try setXdgEnv(tty_str, current_environment.xdg_session_desktop, current_environment.xdg_desktop_names); + try setXdgEnv(tty_str, current_environment); // Open the PAM session var credentials = [_:null]?[*:0]const u8{ login, password }; @@ -96,7 +95,7 @@ pub fn authenticate(options: AuthOptions, current_environment: Environment, logi child_pid = try std.posix.fork(); if (child_pid == 0) { - startSession(options, pwd, handle, current_environment) catch |e| { + startSession(options, tty_str, pwd, handle, current_environment) catch |e| { shared_err.writeError(e); std.process.exit(1); }; @@ -132,6 +131,7 @@ pub fn authenticate(options: AuthOptions, current_environment: Environment, logi fn startSession( options: AuthOptions, + tty_str: [:0]u8, pwd: *interop.pwd.passwd, handle: ?*interop.pam.pam_handle, current_environment: Environment, @@ -155,6 +155,9 @@ fn startSession( // Set up the environment try initEnv(pwd, options.path); + // Reset the XDG environment variables + try setXdgEnv(tty_str, current_environment); + // Set the PAM variables const pam_env_vars: ?[*:null]?[*:0]u8 = interop.pam.pam_getenvlist(handle); if (pam_env_vars == null) return error.GetEnvListFailed; @@ -193,15 +196,13 @@ fn initEnv(pwd: *interop.pwd.passwd, path_env: ?[:0]const u8) !void { } } -fn setXdgSessionEnv(display_server: enums.DisplayServer) void { - _ = interop.stdlib.setenv("XDG_SESSION_TYPE", switch (display_server) { +fn setXdgEnv(tty_str: [:0]u8, environment: Environment) !void { + _ = interop.stdlib.setenv("XDG_SESSION_TYPE", switch (environment.display_server) { .wayland => "wayland", .shell => "tty", .xinitrc, .x11 => "x11", }, 0); -} -fn setXdgEnv(tty_str: [:0]u8, maybe_desktop_name: ?[:0]const u8, maybe_xdg_desktop_names: ?[:0]const u8) !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 @@ -214,10 +215,10 @@ fn setXdgEnv(tty_str: [:0]u8, maybe_desktop_name: ?[:0]const u8, maybe_xdg_deskt _ = interop.stdlib.setenv("XDG_RUNTIME_DIR", uid_str, 0); } - if (maybe_xdg_desktop_names) |xdg_desktop_names| _ = interop.stdlib.setenv("XDG_CURRENT_DESKTOP", xdg_desktop_names, 0); + if (environment.xdg_desktop_names) |xdg_desktop_names| _ = interop.stdlib.setenv("XDG_CURRENT_DESKTOP", xdg_desktop_names, 0); _ = interop.stdlib.setenv("XDG_SESSION_CLASS", "user", 0); _ = interop.stdlib.setenv("XDG_SESSION_ID", "1", 0); - if (maybe_desktop_name) |desktop_name| _ = interop.stdlib.setenv("XDG_SESSION_DESKTOP", desktop_name, 0); + if (environment.xdg_session_desktop) |desktop_name| _ = interop.stdlib.setenv("XDG_SESSION_DESKTOP", desktop_name, 0); _ = interop.stdlib.setenv("XDG_SEAT", "seat0", 0); _ = interop.stdlib.setenv("XDG_VTNR", tty_str, 0); } From d08b9a916e32357df3c9d559f844edbab36d70b0 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Mon, 7 Jul 2025 21:45:54 +0200 Subject: [PATCH 218/530] Remove config.console_dev option + handle ioctl errors Signed-off-by: AnErrupTion --- res/config.ini | 3 --- res/lang/ar.ini | 2 ++ res/lang/cat.ini | 2 ++ res/lang/cs.ini | 2 ++ res/lang/de.ini | 2 ++ res/lang/en.ini | 2 ++ res/lang/es.ini | 2 ++ res/lang/fr.ini | 2 ++ res/lang/it.ini | 2 ++ res/lang/pl.ini | 2 ++ res/lang/pt.ini | 2 ++ res/lang/pt_BR.ini | 2 ++ res/lang/ro.ini | 2 ++ res/lang/ru.ini | 2 ++ res/lang/sr.ini | 2 ++ res/lang/sv.ini | 2 ++ res/lang/tr.ini | 2 ++ res/lang/uk.ini | 2 ++ res/lang/zh_CN.ini | 2 ++ src/config/Config.zig | 1 - src/config/Lang.zig | 2 ++ src/interop.zig | 25 ++++++++++++------------- src/main.zig | 14 ++++++-------- 23 files changed, 56 insertions(+), 25 deletions(-) diff --git a/res/config.ini b/res/config.ini index a74cd5c..526c6f9 100644 --- a/res/config.ini +++ b/res/config.ini @@ -100,9 +100,6 @@ colormix_col2 = 0x000000FF # Color mixing animation third color id colormix_col3 = 0x20000000 -# Console path -console_dev = /dev/console - # Input box active by default on startup # Available inputs: info_line, session, login, password default_input = login diff --git a/res/lang/ar.ini b/res/lang/ar.ini index 835fd66..95644b7 100644 --- a/res/lang/ar.ini +++ b/res/lang/ar.ini @@ -13,6 +13,7 @@ err_domain = اسم نطاق غير صالح err_empty_password = لا يُسمح بكلمة مرور فارغة err_envlist = فشل في جلب قائمة المتغيرات البيئية err_hostname = فشل في جلب اسم المضيف (Hostname) + err_mlock = فشل في تأمين ذاكرة كلمة المرور (mlock) err_null = مؤشر فارغ (Null pointer) err_numlock = فشل في ضبط Num Lock @@ -38,6 +39,7 @@ err_perm_group = فشل في تخفيض صلاحيات المجموعة (Group p err_perm_user = فشل في تخفيض صلاحيات المستخدم (User permissions) err_pwnam = فشل في جلب معلومات المستخدم err_sleep = فشل في تنفيذ أمر sleep + err_tty_ctrl = فشل في نقل تحكم الطرفية (TTY) err_user_gid = فشل في تعيين معرّف المجموعة (GID) للمستخدم err_user_init = فشل في تهيئة بيانات المستخدم diff --git a/res/lang/cat.ini b/res/lang/cat.ini index 01d9232..f1adf55 100644 --- a/res/lang/cat.ini +++ b/res/lang/cat.ini @@ -13,6 +13,7 @@ err_domain = domini invàlid err_envlist = error en obtenir l'envlist err_hostname = error en obtenir el nom de l'amfitrió + err_mlock = error en bloquejar la memòria de clau err_null = punter nul err_numlock = error en establir el Bloq num @@ -39,6 +40,7 @@ err_perm_user = error en degradar els permisos de l'usuari err_pwnam = error en obtenir la informació de l'usuari + 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 diff --git a/res/lang/cs.ini b/res/lang/cs.ini index 2adc297..05c2e4d 100644 --- a/res/lang/cs.ini +++ b/res/lang/cs.ini @@ -13,6 +13,7 @@ err_domain = neplatná doména err_hostname = nelze získat název hostitele + err_mlock = uzamčení paměti hesel selhalo err_null = nulový ukazatel @@ -39,6 +40,7 @@ err_perm_user = nepodařilo se snížit uživatelská oprávnění err_pwnam = nelze získat informace o uživateli + err_user_gid = nastavení GID uživatele selhalo err_user_init = inicializace uživatele selhala err_user_uid = nastavení UID uživateli selhalo diff --git a/res/lang/de.ini b/res/lang/de.ini index 47c44aa..7277510 100644 --- a/res/lang/de.ini +++ b/res/lang/de.ini @@ -13,6 +13,7 @@ err_domain = Ungueltige Domain err_empty_password = Leeres Passwort nicht zugelassen err_envlist = Fehler beim Abrufen der Umgebungs-Variablen err_hostname = Abrufen des Hostnames fehlgeschlagen + err_mlock = Sperren des Passwortspeichers fehlgeschlagen err_null = Null Pointer err_numlock = Numlock konnte nicht aktiviert werden @@ -38,6 +39,7 @@ 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_tty_ctrl = Fehler bei der TTY-Uebergabe err_user_gid = Fehler beim Setzen der Gruppen-ID err_user_init = Nutzer-Initialisierung fehlgeschlagen diff --git a/res/lang/en.ini b/res/lang/en.ini index a0eae31..3e1b7d7 100644 --- a/res/lang/en.ini +++ b/res/lang/en.ini @@ -13,6 +13,7 @@ err_domain = invalid domain err_empty_password = empty password not allowed err_envlist = failed to get envlist err_hostname = failed to get hostname +err_lock_state = failed to get lock state err_mlock = failed to lock password memory err_null = null pointer err_numlock = failed to set numlock @@ -38,6 +39,7 @@ err_perm_group = failed to downgrade group permissions err_perm_user = failed to downgrade user permissions err_pwnam = failed to get user info err_sleep = failed to execute sleep command +err_switch_tty = failed to switch tty err_tty_ctrl = tty control transfer failed err_user_gid = failed to set user GID err_user_init = failed to initialize user diff --git a/res/lang/es.ini b/res/lang/es.ini index 5004d12..f9780ec 100644 --- a/res/lang/es.ini +++ b/res/lang/es.ini @@ -13,6 +13,7 @@ err_domain = dominio inválido err_hostname = error al obtener el nombre de host + err_mlock = error al bloquear la contraseña de memoria err_null = puntero nulo @@ -39,6 +40,7 @@ err_perm_user = error al degradar los permisos del usuario err_pwnam = error al obtener la información del usuario + 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 diff --git a/res/lang/fr.ini b/res/lang/fr.ini index 75c7c6f..3d73ef1 100644 --- a/res/lang/fr.ini +++ b/res/lang/fr.ini @@ -13,6 +13,7 @@ err_domain = domaine invalide err_empty_password = mot de passe vide non autorisé err_envlist = échec de lecture de la liste d'environnement err_hostname = échec de lecture du nom d'hôte +err_lock_state = échec de lecture de l'état de verrouillage err_mlock = échec du verrouillage mémoire err_null = pointeur null err_numlock = échec de modification du verr.num @@ -38,6 +39,7 @@ err_perm_group = échec du déclassement des permissions de groupe err_perm_user = échec du déclassement des permissions utilisateur err_pwnam = échec de lecture des infos utilisateur err_sleep = échec de l'exécution de la commande de veille +err_switch_tty = échec du changement de terminal err_tty_ctrl = échec du transfert de contrôle du terminal err_user_gid = échec de modification du GID err_user_init = échec d'initialisation de l'utilisateur diff --git a/res/lang/it.ini b/res/lang/it.ini index 8d749c6..1781c1e 100644 --- a/res/lang/it.ini +++ b/res/lang/it.ini @@ -13,6 +13,7 @@ err_domain = dominio non valido err_hostname = impossibile ottenere hostname + err_mlock = impossibile ottenere lock per la password in memoria err_null = puntatore nullo @@ -39,6 +40,7 @@ err_perm_user = impossibile ridurre permessi utente err_pwnam = impossibile ottenere dati utente + err_user_gid = impossibile impostare GID utente err_user_init = impossibile inizializzare utente err_user_uid = impossible impostare UID utente diff --git a/res/lang/pl.ini b/res/lang/pl.ini index cdd3f29..4995ef0 100644 --- a/res/lang/pl.ini +++ b/res/lang/pl.ini @@ -13,6 +13,7 @@ err_domain = niepoprawna domena err_empty_password = puste hasło jest niedozwolone err_envlist = nie udało się pobrać listy zmiennych środowiskowych err_hostname = nie udało się uzyskać nazwy hosta + err_mlock = nie udało się zablokować pamięci haseł err_null = pusty wskaźnik err_numlock = nie udało się ustawić numlock @@ -38,6 +39,7 @@ 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_tty_ctrl = nie udało się przekazać kontroli tty err_user_gid = nie udało się ustawić GID użytkownika err_user_init = nie udało się zainicjalizować użytkownika diff --git a/res/lang/pt.ini b/res/lang/pt.ini index 25361b6..a4f0fa8 100644 --- a/res/lang/pt.ini +++ b/res/lang/pt.ini @@ -13,6 +13,7 @@ err_domain = domínio inválido err_hostname = erro ao obter o nome do host + err_mlock = erro de bloqueio de memória err_null = ponteiro nulo @@ -39,6 +40,7 @@ err_perm_user = erro ao reduzir as permissões do utilizador err_pwnam = erro ao obter informação do utilizador + 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 diff --git a/res/lang/pt_BR.ini b/res/lang/pt_BR.ini index 8148e27..2d2bb89 100644 --- a/res/lang/pt_BR.ini +++ b/res/lang/pt_BR.ini @@ -13,6 +13,7 @@ err_domain = domínio inválido err_hostname = não foi possível obter o nome do host + err_mlock = bloqueio da memória de senha malsucedido err_null = ponteiro nulo @@ -39,6 +40,7 @@ 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_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 diff --git a/res/lang/ro.ini b/res/lang/ro.ini index f4928cf..9f50c8f 100644 --- a/res/lang/ro.ini +++ b/res/lang/ro.ini @@ -17,6 +17,7 @@ err_console_dev = nu s-a putut accesa consola + err_pam_abort = tranzacţie pam anulată err_pam_acct_expired = cont expirat err_pam_auth = eroare de autentificare @@ -47,6 +48,7 @@ err_perm_user = nu s-a putut face downgrade permisiunilor de utilizator + login = utilizator logout = opreşte sesiunea diff --git a/res/lang/ru.ini b/res/lang/ru.ini index 8e9b1a0..fe7315e 100644 --- a/res/lang/ru.ini +++ b/res/lang/ru.ini @@ -13,6 +13,7 @@ err_domain = неверный домен err_hostname = не удалось получить имя хоста + err_mlock = сбой блокировки памяти err_null = нулевой указатель @@ -39,6 +40,7 @@ err_perm_user = не удалось понизить права доступа err_pwnam = не удалось получить информацию о пользователе + err_user_gid = не удалось установить GID пользователя err_user_init = не удалось инициализировать пользователя err_user_uid = не удалось установить UID пользователя diff --git a/res/lang/sr.ini b/res/lang/sr.ini index bedc690..9251011 100644 --- a/res/lang/sr.ini +++ b/res/lang/sr.ini @@ -13,6 +13,7 @@ err_domain = nevazeci domen err_hostname = neuspijesno trazenje hostname-a + err_mlock = neuspijesno zakljucavanje memorije lozinke err_null = null pokazivac @@ -39,6 +40,7 @@ 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 diff --git a/res/lang/sv.ini b/res/lang/sv.ini index 40b8cbd..aad2244 100644 --- a/res/lang/sv.ini +++ b/res/lang/sv.ini @@ -13,6 +13,7 @@ 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 @@ -39,6 +40,7 @@ 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 diff --git a/res/lang/tr.ini b/res/lang/tr.ini index 77a22ae..6ad0e94 100644 --- a/res/lang/tr.ini +++ b/res/lang/tr.ini @@ -13,6 +13,7 @@ err_domain = gecersiz etki alani err_hostname = ana bilgisayar adi alinamadi + err_mlock = parola bellegi kilitlenemedi err_null = bos isaretci hatasi @@ -39,6 +40,7 @@ err_perm_user = kullanici izinleri dusurulemedi err_pwnam = kullanici bilgileri alinamadi + err_user_gid = kullanici icin GID ayarlanamadi err_user_init = kullanici oturumu baslatilamadi err_user_uid = kullanici icin UID ayarlanamadi diff --git a/res/lang/uk.ini b/res/lang/uk.ini index a24731e..df925df 100644 --- a/res/lang/uk.ini +++ b/res/lang/uk.ini @@ -13,6 +13,7 @@ err_domain = недійсний домен err_hostname = не вдалося отримати ім'я хосту + err_mlock = збій блокування пам'яті err_null = нульовий вказівник @@ -39,6 +40,7 @@ err_perm_user = не вдалося понизити права доступу err_pwnam = не вдалося отримати дані користувача + err_user_gid = не вдалося змінити GID користувача err_user_init = не вдалося ініціалізувати користувача err_user_uid = не вдалося змінити UID користувача diff --git a/res/lang/zh_CN.ini b/res/lang/zh_CN.ini index 9960d07..d1ccc57 100644 --- a/res/lang/zh_CN.ini +++ b/res/lang/zh_CN.ini @@ -13,6 +13,7 @@ err_domain = 无效的域 err_hostname = 获取主机名失败 + err_mlock = 锁定密码存储器失败 err_null = 空指针 @@ -39,6 +40,7 @@ err_perm_user = 用户权限降级失败 err_pwnam = 获取用户信息失败 + err_user_gid = 设置用户GID失败 err_user_init = 初始化用户失败 err_user_uid = 设置用户UID失败 diff --git a/src/config/Config.zig b/src/config/Config.zig index 558668b..874a75e 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -28,7 +28,6 @@ cmatrix_max_codepoint: u16 = 0x7B, colormix_col1: u32 = 0x00FF0000, colormix_col2: u32 = 0x000000FF, colormix_col3: u32 = 0x20000000, -console_dev: []const u8 = "/dev/console", default_input: Input = .login, doom_top_color: u32 = 0x00FF0000, doom_middle_color: u32 = 0x00FFFF00, diff --git a/src/config/Lang.zig b/src/config/Lang.zig index 3360b71..8334d6c 100644 --- a/src/config/Lang.zig +++ b/src/config/Lang.zig @@ -18,6 +18,7 @@ err_domain: []const u8 = "invalid domain", err_empty_password: []const u8 = "empty password not allowed", err_envlist: []const u8 = "failed to get envlist", err_hostname: []const u8 = "failed to get hostname", +err_lock_state: []const u8 = "failed to get lock state", err_mlock: []const u8 = "failed to lock password memory", err_null: []const u8 = "null pointer", err_numlock: []const u8 = "failed to set numlock", @@ -43,6 +44,7 @@ err_perm_group: []const u8 = "failed to downgrade group permissions", err_perm_user: []const u8 = "failed to downgrade user permissions", err_pwnam: []const u8 = "failed to get user info", err_sleep: []const u8 = "failed to execute sleep command", +err_switch_tty: []const u8 = "failed to switch tty", err_tty_ctrl: []const u8 = "tty control transfer failed", err_user_gid: []const u8 = "failed to set user GID", err_user_init: []const u8 = "failed to initialize user", diff --git a/src/interop.zig b/src/interop.zig index 9fc5017..8540c21 100644 --- a/src/interop.zig +++ b/src/interop.zig @@ -36,7 +36,7 @@ pub const stdlib = @cImport({ pub 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' + // the passwd struct directly, so we can't import it separately if (builtin.os.tag == .freebsd) @cInclude("login_cap.h"); }); @@ -75,23 +75,21 @@ pub fn timeAsString(buf: [:0]u8, format: [:0]const u8) ![]u8 { return buf[0..len]; } -pub fn switchTty(console_dev: []const u8, tty: u8) !void { - const fd = try std.posix.open(console_dev, .{ .ACCMODE = .WRONLY }, 0); - defer std.posix.close(fd); +pub fn switchTty(tty: u8) !void { + var status = std.c.ioctl(std.c.STDIN_FILENO, vt.VT_ACTIVATE, tty); + if (status != 0) return error.FailedToActivateTty; - _ = std.c.ioctl(fd, vt.VT_ACTIVATE, tty); - _ = std.c.ioctl(fd, vt.VT_WAITACTIVE, tty); + status = std.c.ioctl(std.c.STDIN_FILENO, vt.VT_WAITACTIVE, tty); + if (status != 0) return error.FailedToWaitForActiveTty; } -pub fn getLockState(console_dev: []const u8) !struct { +pub fn getLockState() !struct { numlock: bool, capslock: bool, } { - const fd = try std.posix.open(console_dev, .{ .ACCMODE = .RDONLY }, 0); - defer std.posix.close(fd); - var led: LedState = undefined; - _ = std.c.ioctl(fd, get_led_state, &led); + const status = std.c.ioctl(std.c.STDIN_FILENO, get_led_state, &led); + if (status != 0) return error.FailedToGetLockState; return .{ .numlock = (led & numlock_led) != 0, @@ -101,11 +99,12 @@ pub fn getLockState(console_dev: []const u8) !struct { pub fn setNumlock(val: bool) !void { var led: LedState = undefined; - _ = std.c.ioctl(0, get_led_state, &led); + var status = std.c.ioctl(std.c.STDIN_FILENO, get_led_state, &led); + if (status != 0) return error.FailedToGetNumlock; const numlock = (led & numlock_led) != 0; if (numlock != val) { - const status = std.c.ioctl(std.posix.STDIN_FILENO, set_led_state, led ^ numlock_led); + status = std.c.ioctl(std.posix.STDIN_FILENO, set_led_state, led ^ numlock_led); if (status != 0) return error.FailedToSetNumlock; } } diff --git a/src/main.zig b/src/main.zig index 17309df..9e51691 100644 --- a/src/main.zig +++ b/src/main.zig @@ -389,12 +389,10 @@ pub fn main() !void { var update = true; var resolution_changed = false; var auth_fails: u64 = 0; - var can_access_console_dev = true; - // Switch to selected TTY if possible - interop.switchTty(config.console_dev, config.tty) catch { - try info_line.addMessage(lang.err_console_dev, config.error_bg, config.error_fg); - can_access_console_dev = false; + // Switch to selected TTY + interop.switchTty(config.tty) catch { + try info_line.addMessage(lang.err_switch_tty, config.error_bg, config.error_fg); }; while (run) { @@ -546,9 +544,9 @@ pub fn main() !void { buffer.drawLabel(label_txt, buffer.box_x, buffer.box_y + buffer.box_height); } - if (can_access_console_dev) draw_lock_state: { - const lock_state = interop.getLockState(config.console_dev) catch { - try info_line.addMessage(lang.err_console_dev, config.error_bg, config.error_fg); + draw_lock_state: { + const lock_state = interop.getLockState() catch { + try info_line.addMessage(lang.err_lock_state, config.error_bg, config.error_fg); break :draw_lock_state; }; From 1bcbb08202da5fcb5f0e3b6d58128e437dc95220 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Mon, 7 Jul 2025 21:46:49 +0200 Subject: [PATCH 219/530] Add config.console_dev as a removed property in migrator Signed-off-by: AnErrupTion --- src/config/migrator.zig | 1 + 1 file changed, 1 insertion(+) diff --git a/src/config/migrator.zig b/src/config/migrator.zig index 2d2e34e..5f1269b 100644 --- a/src/config/migrator.zig +++ b/src/config/migrator.zig @@ -26,6 +26,7 @@ const removed_properties = [_][]const u8{ "term_restore_cursor_cmd", "x_cmd_setup", "wayland_cmd", + "console_dev", }; var temporary_allocator = std.heap.page_allocator; From ab23631e66b224976274262b850f2cfa9b102c69 Mon Sep 17 00:00:00 2001 From: Matthew Rothlisberger Date: Wed, 18 Jun 2025 09:59:20 -0400 Subject: [PATCH 220/530] reimplements PSX Doom fire animation; adds flame height control --- res/config.ini | 13 +++++++-- src/animations/Doom.zig | 64 ++++++++++++++++++++++++++++++----------- src/config/Config.zig | 2 ++ src/main.zig | 2 +- 4 files changed, 61 insertions(+), 20 deletions(-) diff --git a/res/config.ini b/res/config.ini index 526c6f9..cabc070 100644 --- a/res/config.ini +++ b/res/config.ini @@ -104,13 +104,20 @@ colormix_col3 = 0x20000000 # Available inputs: info_line, session, login, password default_input = login -# DOOM animation top color (low intensity flames) +# DOOM animation fire height (1 thru 9) +doom_fire_height = 6 + +# DOOM animation use natural fire colors +# If false, below custom colors used +doom_default_colors = true + +# DOOM animation custom top color (low intensity flames) doom_top_color = 0x00FF0000 -# DOOM animation middle color (medium intensity flames) +# DOOM animation custom middle color (medium intensity flames) doom_middle_color = 0x00FFFF00 -# DOOM animation bottom color (high intensity flames) +# DOOM animation custom bottom color (high intensity flames) doom_bottom_color = 0x00FFFFFF # Error background color id diff --git a/src/animations/Doom.zig b/src/animations/Doom.zig index 41de5ee..fe90ad4 100644 --- a/src/animations/Doom.zig +++ b/src/animations/Doom.zig @@ -11,17 +11,31 @@ pub const STEPS = 12; allocator: Allocator, terminal_buffer: *TerminalBuffer, buffer: []u8, +height: u8, fire: [STEPS + 1]Cell, -pub fn init(allocator: Allocator, terminal_buffer: *TerminalBuffer, top_color: u32, middle_color: u32, bottom_color: u32) !Doom { +pub fn init(allocator: Allocator, terminal_buffer: *TerminalBuffer, fire_height: u8, default_colors: bool, top_color: u32, middle_color: u32, bottom_color: u32) !Doom { const buffer = try allocator.alloc(u8, terminal_buffer.width * terminal_buffer.height); initBuffer(buffer, terminal_buffer.width); - return .{ - .allocator = allocator, - .terminal_buffer = terminal_buffer, - .buffer = buffer, - .fire = [_]Cell{ + const levels = if (default_colors) + [_]Cell{ + Cell.init(' ', TerminalBuffer.Color.DEFAULT, TerminalBuffer.Color.DEFAULT), + Cell.init(0x2591, 0x070707, TerminalBuffer.Color.DEFAULT), + Cell.init(0x2592, 0x470F07, TerminalBuffer.Color.DEFAULT), + Cell.init(0x2593, 0x771F07, TerminalBuffer.Color.DEFAULT), + Cell.init(0x2588, 0xAF3F07, TerminalBuffer.Color.DEFAULT), + Cell.init(0x2591, 0xC74707, 0xAF3F07), + Cell.init(0x2592, 0xDF5707, 0xAF3F07), + Cell.init(0x2593, 0xCF6F0F, 0xAF3F07), + Cell.init(0x2588, 0xC78F17, 0xAF3F07), + Cell.init(0x2591, 0xBF9F1F, 0xAF3F07), + Cell.init(0x2592, 0xBFAF2F, 0xAF3F07), + Cell.init(0x2593, 0xCFCF6F, 0xAF3F07), + Cell.init(0x2588, 0xFFFFFF, 0xAF3F07), + } + else + [_]Cell{ Cell.init(' ', TerminalBuffer.Color.DEFAULT, TerminalBuffer.Color.DEFAULT), Cell.init(0x2591, top_color, TerminalBuffer.Color.DEFAULT), Cell.init(0x2592, top_color, TerminalBuffer.Color.DEFAULT), @@ -35,7 +49,14 @@ pub fn init(allocator: Allocator, terminal_buffer: *TerminalBuffer, top_color: u Cell.init(0x2592, bottom_color, middle_color), Cell.init(0x2593, bottom_color, middle_color), Cell.init(0x2588, bottom_color, middle_color), - }, + }; + + return .{ + .allocator = allocator, + .terminal_buffer = terminal_buffer, + .buffer = buffer, + .height = @min(9, fire_height), + .fire = levels, }; } @@ -57,20 +78,31 @@ fn draw(self: *Doom) void { 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| { - // Get current cell + // Get index of current cell in fire level buffer const from = y * self.terminal_buffer.width + x; - const cell_index = self.buffer[from]; - // Spread fire - const propagate = self.terminal_buffer.random.int(u1); - const to = from - self.terminal_buffer.width; // Get the line above + // Generate random datum for fire propagation + const random = (self.terminal_buffer.random.int(u16) % 10); - self.buffer[to] = if (cell_index > 0) cell_index - propagate else cell_index; + // Select semi-random target cell + const to = from -| self.terminal_buffer.width -| (random & 3) + 1; - // Put the cell - const cell = self.fire[cell_index]; - cell.put(x, y); + // Get fire level of current cell + const level_buf_from = self.buffer[from]; + + // Choose new fire level and store in level buffer + var level_buf_to = level_buf_from; + if (random >= self.height) level_buf_to -|= 1; + self.buffer[to] = @intCast(level_buf_to); + + // Send fire level to terminal buffer + const to_cell = self.fire[level_buf_to]; + to_cell.put(x, y); } + + // Draw bottom line (fire source) + const src_cell = self.fire[STEPS]; + src_cell.put(x, self.terminal_buffer.height - 1); } } diff --git a/src/config/Config.zig b/src/config/Config.zig index 874a75e..613c55a 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -29,6 +29,8 @@ colormix_col1: u32 = 0x00FF0000, colormix_col2: u32 = 0x000000FF, colormix_col3: u32 = 0x20000000, default_input: Input = .login, +doom_fire_height: u8 = 6, +doom_default_colors: bool = true, doom_top_color: u32 = 0x00FF0000, doom_middle_color: u32 = 0x00FFFF00, doom_bottom_color: u32 = 0x00FFFFFF, diff --git a/src/main.zig b/src/main.zig index 9e51691..b14a462 100644 --- a/src/main.zig +++ b/src/main.zig @@ -354,7 +354,7 @@ pub fn main() !void { animation = dummy.animation(); }, .doom => { - var doom = try Doom.init(allocator, &buffer, config.doom_top_color, config.doom_middle_color, config.doom_bottom_color); + var doom = try Doom.init(allocator, &buffer, config.doom_fire_height, config.doom_default_colors, config.doom_top_color, config.doom_middle_color, config.doom_bottom_color); animation = doom.animation(); }, .matrix => { From 1c5686ea545fa19ca58a57f672ddcd91eee7ecb3 Mon Sep 17 00:00:00 2001 From: Matthew Rothlisberger Date: Fri, 20 Jun 2025 10:23:17 -0400 Subject: [PATCH 221/530] further improves fire behavior --- src/animations/Doom.zig | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/src/animations/Doom.zig b/src/animations/Doom.zig index fe90ad4..f988495 100644 --- a/src/animations/Doom.zig +++ b/src/animations/Doom.zig @@ -24,14 +24,14 @@ pub fn init(allocator: Allocator, terminal_buffer: *TerminalBuffer, fire_height: Cell.init(0x2591, 0x070707, TerminalBuffer.Color.DEFAULT), Cell.init(0x2592, 0x470F07, TerminalBuffer.Color.DEFAULT), Cell.init(0x2593, 0x771F07, TerminalBuffer.Color.DEFAULT), - Cell.init(0x2588, 0xAF3F07, TerminalBuffer.Color.DEFAULT), - Cell.init(0x2591, 0xC74707, 0xAF3F07), - Cell.init(0x2592, 0xDF5707, 0xAF3F07), - Cell.init(0x2593, 0xCF6F0F, 0xAF3F07), - Cell.init(0x2588, 0xC78F17, 0xAF3F07), - Cell.init(0x2591, 0xBF9F1F, 0xAF3F07), - Cell.init(0x2592, 0xBFAF2F, 0xAF3F07), - Cell.init(0x2593, 0xCFCF6F, 0xAF3F07), + Cell.init(0x2588, 0x9F2F07, TerminalBuffer.Color.DEFAULT), + Cell.init(0x2591, 0xBF4707, 0xAF3F07), + Cell.init(0x2592, 0xC74707, 0xAF3F07), + Cell.init(0x2593, 0xDF5707, 0xAF3F07), + Cell.init(0x2588, 0xCF6F0F, 0xAF3F07), + Cell.init(0x2591, 0xC78F17, 0xAF3F07), + Cell.init(0x2592, 0xBF9F1F, 0xAF3F07), + Cell.init(0x2593, 0xBFAF2F, 0xAF3F07), Cell.init(0x2588, 0xFFFFFF, 0xAF3F07), } else @@ -82,10 +82,12 @@ fn draw(self: *Doom) void { const from = y * self.terminal_buffer.width + x; // Generate random datum for fire propagation - const random = (self.terminal_buffer.random.int(u16) % 10); + const random = (self.terminal_buffer.random.int(u8) % 10); // Select semi-random target cell const to = from -| self.terminal_buffer.width -| (random & 3) + 1; + const to_x = to % self.terminal_buffer.width; + const to_y = to / self.terminal_buffer.width; // Get fire level of current cell const level_buf_from = self.buffer[from]; @@ -95,9 +97,11 @@ fn draw(self: *Doom) void { if (random >= self.height) level_buf_to -|= 1; self.buffer[to] = @intCast(level_buf_to); - // Send fire level to terminal buffer + // Send known fire levels to terminal buffer + const from_cell = self.fire[level_buf_from]; const to_cell = self.fire[level_buf_to]; - to_cell.put(x, y); + from_cell.put(x, y); + to_cell.put(to_x, to_y); } // Draw bottom line (fire source) From 2a8e221e80240c690c7cdbf9b35cf4ca3736e5d7 Mon Sep 17 00:00:00 2001 From: Matthew Rothlisberger Date: Fri, 20 Jun 2025 22:22:29 -0400 Subject: [PATCH 222/530] improves fire gradient in true color mode --- src/animations/Doom.zig | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/animations/Doom.zig b/src/animations/Doom.zig index f988495..721eead 100644 --- a/src/animations/Doom.zig +++ b/src/animations/Doom.zig @@ -21,18 +21,18 @@ pub fn init(allocator: Allocator, terminal_buffer: *TerminalBuffer, fire_height: const levels = if (default_colors) [_]Cell{ Cell.init(' ', TerminalBuffer.Color.DEFAULT, TerminalBuffer.Color.DEFAULT), - Cell.init(0x2591, 0x070707, TerminalBuffer.Color.DEFAULT), - Cell.init(0x2592, 0x470F07, TerminalBuffer.Color.DEFAULT), - Cell.init(0x2593, 0x771F07, TerminalBuffer.Color.DEFAULT), - Cell.init(0x2588, 0x9F2F07, TerminalBuffer.Color.DEFAULT), - Cell.init(0x2591, 0xBF4707, 0xAF3F07), - Cell.init(0x2592, 0xC74707, 0xAF3F07), - Cell.init(0x2593, 0xDF5707, 0xAF3F07), - Cell.init(0x2588, 0xCF6F0F, 0xAF3F07), - Cell.init(0x2591, 0xC78F17, 0xAF3F07), - Cell.init(0x2592, 0xBF9F1F, 0xAF3F07), - Cell.init(0x2593, 0xBFAF2F, 0xAF3F07), - Cell.init(0x2588, 0xFFFFFF, 0xAF3F07), + Cell.init(0x2591, 0x009F2707, TerminalBuffer.Color.DEFAULT), + Cell.init(0x2592, 0x009F2707, TerminalBuffer.Color.DEFAULT), + Cell.init(0x2593, 0x009F2707, TerminalBuffer.Color.DEFAULT), + Cell.init(0x2588, 0x009F2707, TerminalBuffer.Color.DEFAULT), + Cell.init(0x2591, 0x00C78F17, 0x009F2707), + Cell.init(0x2592, 0x00C78F17, 0x009F2707), + Cell.init(0x2593, 0x00C78F17, 0x009F2707), + Cell.init(0x2588, 0x00C78F17, 0x009F2707), + Cell.init(0x2591, 0x00FFFFFF, 0x00C78F17), + Cell.init(0x2592, 0x00FFFFFF, 0x00C78F17), + Cell.init(0x2593, 0x00FFFFFF, 0x00C78F17), + Cell.init(0x2588, 0x00FFFFFF, 0x00C78F17), } else [_]Cell{ From 99f3ab96ba04554d300ba5db90d276e2913a3d81 Mon Sep 17 00:00:00 2001 From: Matthew Rothlisberger Date: Mon, 7 Jul 2025 21:01:36 -0400 Subject: [PATCH 223/530] changes fire parameters --- res/config.ini | 9 ++++----- src/animations/Doom.zig | 38 +++++++++++++------------------------- src/config/Config.zig | 2 +- src/main.zig | 2 +- 4 files changed, 19 insertions(+), 32 deletions(-) diff --git a/res/config.ini b/res/config.ini index cabc070..492319c 100644 --- a/res/config.ini +++ b/res/config.ini @@ -107,15 +107,14 @@ default_input = login # DOOM animation fire height (1 thru 9) doom_fire_height = 6 -# DOOM animation use natural fire colors -# If false, below custom colors used -doom_default_colors = true +# DOOM animation fire spread (0 thru 4) +doom_fire_spread = 2 # DOOM animation custom top color (low intensity flames) -doom_top_color = 0x00FF0000 +doom_top_color = 0x009F2707 # DOOM animation custom middle color (medium intensity flames) -doom_middle_color = 0x00FFFF00 +doom_middle_color = 0x00C78F17 # DOOM animation custom bottom color (high intensity flames) doom_bottom_color = 0x00FFFFFF diff --git a/src/animations/Doom.zig b/src/animations/Doom.zig index 721eead..76850fa 100644 --- a/src/animations/Doom.zig +++ b/src/animations/Doom.zig @@ -7,34 +7,21 @@ const TerminalBuffer = @import("../tui/TerminalBuffer.zig"); const Doom = @This(); pub const STEPS = 12; +pub const HEIGHT_MAX = 9; +pub const SPREAD_MAX = 4; allocator: Allocator, terminal_buffer: *TerminalBuffer, buffer: []u8, height: u8, +spread: u8, fire: [STEPS + 1]Cell, -pub fn init(allocator: Allocator, terminal_buffer: *TerminalBuffer, fire_height: u8, default_colors: bool, top_color: u32, middle_color: u32, bottom_color: u32) !Doom { +pub fn init(allocator: Allocator, terminal_buffer: *TerminalBuffer, top_color: u32, middle_color: u32, bottom_color: u32, fire_height: u8, fire_spread: u8) !Doom { const buffer = try allocator.alloc(u8, terminal_buffer.width * terminal_buffer.height); initBuffer(buffer, terminal_buffer.width); - const levels = if (default_colors) - [_]Cell{ - Cell.init(' ', TerminalBuffer.Color.DEFAULT, TerminalBuffer.Color.DEFAULT), - Cell.init(0x2591, 0x009F2707, TerminalBuffer.Color.DEFAULT), - Cell.init(0x2592, 0x009F2707, TerminalBuffer.Color.DEFAULT), - Cell.init(0x2593, 0x009F2707, TerminalBuffer.Color.DEFAULT), - Cell.init(0x2588, 0x009F2707, TerminalBuffer.Color.DEFAULT), - Cell.init(0x2591, 0x00C78F17, 0x009F2707), - Cell.init(0x2592, 0x00C78F17, 0x009F2707), - Cell.init(0x2593, 0x00C78F17, 0x009F2707), - Cell.init(0x2588, 0x00C78F17, 0x009F2707), - Cell.init(0x2591, 0x00FFFFFF, 0x00C78F17), - Cell.init(0x2592, 0x00FFFFFF, 0x00C78F17), - Cell.init(0x2593, 0x00FFFFFF, 0x00C78F17), - Cell.init(0x2588, 0x00FFFFFF, 0x00C78F17), - } - else + const levels = [_]Cell{ Cell.init(' ', TerminalBuffer.Color.DEFAULT, TerminalBuffer.Color.DEFAULT), Cell.init(0x2591, top_color, TerminalBuffer.Color.DEFAULT), @@ -55,7 +42,8 @@ pub fn init(allocator: Allocator, terminal_buffer: *TerminalBuffer, fire_height: .allocator = allocator, .terminal_buffer = terminal_buffer, .buffer = buffer, - .height = @min(9, fire_height), + .height = @min(HEIGHT_MAX, fire_height), + .spread = @min(SPREAD_MAX, fire_spread), .fire = levels, }; } @@ -81,11 +69,12 @@ fn draw(self: *Doom) void { // Get index of current cell in fire level buffer const from = y * self.terminal_buffer.width + x; - // Generate random datum for fire propagation - const random = (self.terminal_buffer.random.int(u8) % 10); + // Generate random data for fire propagation + const rand_loss = self.terminal_buffer.random.intRangeAtMost(u8, 0, HEIGHT_MAX); + const rand_spread = self.terminal_buffer.random.intRangeAtMost(u8, 0, self.spread * 2); // Select semi-random target cell - const to = from -| self.terminal_buffer.width -| (random & 3) + 1; + const to = from -| self.terminal_buffer.width + self.spread -| rand_spread; const to_x = to % self.terminal_buffer.width; const to_y = to / self.terminal_buffer.width; @@ -93,9 +82,8 @@ fn draw(self: *Doom) void { const level_buf_from = self.buffer[from]; // Choose new fire level and store in level buffer - var level_buf_to = level_buf_from; - if (random >= self.height) level_buf_to -|= 1; - self.buffer[to] = @intCast(level_buf_to); + const level_buf_to = level_buf_from -| @intFromBool(rand_loss >= self.height); + self.buffer[to] = level_buf_to; // Send known fire levels to terminal buffer const from_cell = self.fire[level_buf_from]; diff --git a/src/config/Config.zig b/src/config/Config.zig index 613c55a..8b020f0 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -30,7 +30,7 @@ colormix_col2: u32 = 0x000000FF, colormix_col3: u32 = 0x20000000, default_input: Input = .login, doom_fire_height: u8 = 6, -doom_default_colors: bool = true, +doom_fire_spread: u8 = 2, doom_top_color: u32 = 0x00FF0000, doom_middle_color: u32 = 0x00FFFF00, doom_bottom_color: u32 = 0x00FFFFFF, diff --git a/src/main.zig b/src/main.zig index b14a462..fb42923 100644 --- a/src/main.zig +++ b/src/main.zig @@ -354,7 +354,7 @@ pub fn main() !void { animation = dummy.animation(); }, .doom => { - var doom = try Doom.init(allocator, &buffer, config.doom_fire_height, config.doom_default_colors, config.doom_top_color, config.doom_middle_color, config.doom_bottom_color); + 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 => { From ce17d346e873c67161e2fe3aa418dfb7c967a652 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Tue, 8 Jul 2025 12:45:47 +0200 Subject: [PATCH 224/530] Remove lang.err_console_dev Signed-off-by: AnErrupTion --- res/lang/ar.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/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 | 1 - res/lang/tr.ini | 1 - res/lang/uk.ini | 1 - res/lang/zh_CN.ini | 1 - src/config/Lang.zig | 1 - 19 files changed, 19 deletions(-) diff --git a/res/lang/ar.ini b/res/lang/ar.ini index 95644b7..ba1871f 100644 --- a/res/lang/ar.ini +++ b/res/lang/ar.ini @@ -7,7 +7,6 @@ err_bounds = out-of-bounds index err_brightness_change = فشل في تغيير سطوع الشاشة err_chdir = فشل في فتح مجلد المنزل err_config = فشل في تفسير ملف الإعدادات -err_console_dev = فشل في الوصول إلى جهاز وحدة التحكم err_dgn_oob = رسالة سجل (Log) err_domain = اسم نطاق غير صالح err_empty_password = لا يُسمح بكلمة مرور فارغة diff --git a/res/lang/cat.ini b/res/lang/cat.ini index f1adf55..40d877f 100644 --- a/res/lang/cat.ini +++ b/res/lang/cat.ini @@ -7,7 +7,6 @@ err_bounds = índex fora de límits err_brightness_change = error en canviar la brillantor err_chdir = error en obrir la carpeta home -err_console_dev = error en accedir a la consola err_dgn_oob = missatge de registre err_domain = domini invàlid diff --git a/res/lang/cs.ini b/res/lang/cs.ini index 05c2e4d..9052663 100644 --- a/res/lang/cs.ini +++ b/res/lang/cs.ini @@ -7,7 +7,6 @@ err_bounds = index je mimo hranice pole err_chdir = nelze otevřít domovský adresář -err_console_dev = chyba při přístupu do konzole err_dgn_oob = zpráva protokolu err_domain = neplatná doména diff --git a/res/lang/de.ini b/res/lang/de.ini index 7277510..3f76cc6 100644 --- a/res/lang/de.ini +++ b/res/lang/de.ini @@ -7,7 +7,6 @@ err_bounds = Index ausserhalb des Bereichs err_brightness_change = Helligkeitsänderung fehlgeschlagen err_chdir = Fehler beim Oeffnen des Home-Ordners err_config = Fehler beim Verarbeiten der Konfigurationsdatei -err_console_dev = Zugriff auf die Konsole fehlgeschlagen err_dgn_oob = Diagnose-Nachricht err_domain = Ungueltige Domain err_empty_password = Leeres Passwort nicht zugelassen diff --git a/res/lang/en.ini b/res/lang/en.ini index 3e1b7d7..423020e 100644 --- a/res/lang/en.ini +++ b/res/lang/en.ini @@ -7,7 +7,6 @@ err_bounds = out-of-bounds index err_brightness_change = failed to change brightness err_chdir = failed to open home folder err_config = unable to parse config file -err_console_dev = failed to access console err_dgn_oob = log message err_domain = invalid domain err_empty_password = empty password not allowed diff --git a/res/lang/es.ini b/res/lang/es.ini index f9780ec..5ec48af 100644 --- a/res/lang/es.ini +++ b/res/lang/es.ini @@ -7,7 +7,6 @@ err_bounds = índice fuera de límites err_chdir = error al abrir la carpeta home -err_console_dev = error al acceder a la consola err_dgn_oob = mensaje de registro err_domain = dominio inválido diff --git a/res/lang/fr.ini b/res/lang/fr.ini index 3d73ef1..bda901b 100644 --- a/res/lang/fr.ini +++ b/res/lang/fr.ini @@ -7,7 +7,6 @@ err_bounds = indice hors-limite err_brightness_change = échec du changement de luminosité err_chdir = échec de l'ouverture du répertoire home err_config = échec de lecture du fichier de configuration -err_console_dev = échec d'accès à la console err_dgn_oob = message err_domain = domaine invalide err_empty_password = mot de passe vide non autorisé diff --git a/res/lang/it.ini b/res/lang/it.ini index 1781c1e..e7e0d17 100644 --- a/res/lang/it.ini +++ b/res/lang/it.ini @@ -7,7 +7,6 @@ err_bounds = indice fuori limite err_chdir = impossibile aprire home directory -err_console_dev = impossibile aprire console err_dgn_oob = messaggio log err_domain = dominio non valido diff --git a/res/lang/pl.ini b/res/lang/pl.ini index 4995ef0..eeb43e4 100644 --- a/res/lang/pl.ini +++ b/res/lang/pl.ini @@ -7,7 +7,6 @@ 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_config = nie można przetworzyć pliku konfiguracyjnego -err_console_dev = nie udało się uzyskać dostępu do konsoli err_dgn_oob = wiadomość loga err_domain = niepoprawna domena err_empty_password = puste hasło jest niedozwolone diff --git a/res/lang/pt.ini b/res/lang/pt.ini index a4f0fa8..97afee3 100644 --- a/res/lang/pt.ini +++ b/res/lang/pt.ini @@ -7,7 +7,6 @@ err_bounds = índice fora de limites err_chdir = erro ao abrir a pasta home -err_console_dev = erro ao aceder à consola err_dgn_oob = mensagem de registo err_domain = domínio inválido diff --git a/res/lang/pt_BR.ini b/res/lang/pt_BR.ini index 2d2bb89..c89b8aa 100644 --- a/res/lang/pt_BR.ini +++ b/res/lang/pt_BR.ini @@ -7,7 +7,6 @@ err_bounds = índice fora de limites err_chdir = não foi possível abrir o diretório home -err_console_dev = não foi possível acessar o console err_dgn_oob = mensagem de log err_domain = domínio inválido diff --git a/res/lang/ro.ini b/res/lang/ro.ini index 9f50c8f..fe7ee38 100644 --- a/res/lang/ro.ini +++ b/res/lang/ro.ini @@ -7,7 +7,6 @@ capslock = capslock -err_console_dev = nu s-a putut accesa consola diff --git a/res/lang/ru.ini b/res/lang/ru.ini index fe7315e..25dc553 100644 --- a/res/lang/ru.ini +++ b/res/lang/ru.ini @@ -7,7 +7,6 @@ err_bounds = за пределами индекса err_chdir = не удалось открыть домашнюю папку -err_console_dev = не удалось получить доступ к консоли err_dgn_oob = отладочное сообщение (log) err_domain = неверный домен diff --git a/res/lang/sr.ini b/res/lang/sr.ini index 9251011..a24e4b2 100644 --- a/res/lang/sr.ini +++ b/res/lang/sr.ini @@ -7,7 +7,6 @@ err_bounds = izvan granica indeksa err_chdir = neuspijesno otvaranje home foldera -err_console_dev = neuspijesno pristupanje konzoli err_dgn_oob = log poruka err_domain = nevazeci domen diff --git a/res/lang/sv.ini b/res/lang/sv.ini index aad2244..668f431 100644 --- a/res/lang/sv.ini +++ b/res/lang/sv.ini @@ -7,7 +7,6 @@ err_bounds = utanför banan index err_chdir = misslyckades att öppna hemkatalog -err_console_dev = misslyckades att komma åt konsol err_dgn_oob = loggmeddelande err_domain = okänd domän diff --git a/res/lang/tr.ini b/res/lang/tr.ini index 6ad0e94..ce69231 100644 --- a/res/lang/tr.ini +++ b/res/lang/tr.ini @@ -7,7 +7,6 @@ err_bounds = sinirlarin disinda dizin err_chdir = ev klasoru acilamadi -err_console_dev = konsola erisilemedi err_dgn_oob = log mesaji err_domain = gecersiz etki alani diff --git a/res/lang/uk.ini b/res/lang/uk.ini index df925df..af8f247 100644 --- a/res/lang/uk.ini +++ b/res/lang/uk.ini @@ -7,7 +7,6 @@ err_bounds = поза межами індексу err_chdir = не вдалося відкрити домашній каталог -err_console_dev = невдалий доступ до консолі err_dgn_oob = повідомлення журналу (log) err_domain = недійсний домен diff --git a/res/lang/zh_CN.ini b/res/lang/zh_CN.ini index d1ccc57..ecc19be 100644 --- a/res/lang/zh_CN.ini +++ b/res/lang/zh_CN.ini @@ -7,7 +7,6 @@ err_bounds = 索引越界 err_chdir = 无法打开home文件夹 -err_console_dev = 无法访问控制台 err_dgn_oob = 日志消息 err_domain = 无效的域 diff --git a/src/config/Lang.zig b/src/config/Lang.zig index 8334d6c..f673090 100644 --- a/src/config/Lang.zig +++ b/src/config/Lang.zig @@ -12,7 +12,6 @@ err_bounds: []const u8 = "out-of-bounds index", err_brightness_change: []const u8 = "failed to change brightness", err_chdir: []const u8 = "failed to open home folder", err_config: []const u8 = "unable to parse config file", -err_console_dev: []const u8 = "failed to access console", err_dgn_oob: []const u8 = "log message", err_domain: []const u8 = "invalid domain", err_empty_password: []const u8 = "empty password not allowed", From 918e9ad5acf13a9dc7df8c3253c99290f8c719e3 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Tue, 8 Jul 2025 12:47:24 +0200 Subject: [PATCH 225/530] Only show lang.err_lock_state once Signed-off-by: AnErrupTion --- src/main.zig | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main.zig b/src/main.zig index fb42923..aed67e5 100644 --- a/src/main.zig +++ b/src/main.zig @@ -102,6 +102,7 @@ pub fn main() !void { var lang: Lang = undefined; var save: Save = undefined; var config_load_failed = false; + var can_get_lock_state = true; if (res.args.help != 0) { try clap.help(stderr, clap.Help, ¶ms, .{}); @@ -544,9 +545,10 @@ pub fn main() !void { buffer.drawLabel(label_txt, buffer.box_x, buffer.box_y + buffer.box_height); } - draw_lock_state: { + if (can_get_lock_state) draw_lock_state: { const lock_state = interop.getLockState() catch { try info_line.addMessage(lang.err_lock_state, config.error_bg, config.error_fg); + can_get_lock_state = false; break :draw_lock_state; }; From 48e5369f564db06ff2eebd6bbf5ceaf8dc55373a Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Tue, 8 Jul 2025 16:32:20 +0200 Subject: [PATCH 226/530] Fix character width calculation Signed-off-by: AnErrupTion --- src/tui/TerminalBuffer.zig | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/src/tui/TerminalBuffer.zig b/src/tui/TerminalBuffer.zig index ca072f6..7d608c1 100644 --- a/src/tui/TerminalBuffer.zig +++ b/src/tui/TerminalBuffer.zig @@ -207,9 +207,9 @@ pub fn drawColorLabel(text: []const u8, x: usize, y: usize, fg: u32, bg: u32) vo const utf8view = std.unicode.Utf8View.init(text) catch return; var utf8 = utf8view.iterator(); - var i = x; - while (utf8.nextCodepoint()) |codepoint| : (i += 1) { - _ = termbox.tb_set_cell(@intCast(i), yc, codepoint, fg, bg); + var i: c_int = @intCast(x); + while (utf8.nextCodepoint()) |codepoint| : (i += termbox.tb_wcwidth(codepoint)) { + _ = termbox.tb_set_cell(i, yc, codepoint, fg, bg); } } @@ -218,10 +218,10 @@ pub fn drawConfinedLabel(self: TerminalBuffer, text: []const u8, x: usize, y: us const utf8view = std.unicode.Utf8View.init(text) catch return; var utf8 = utf8view.iterator(); - var i: usize = 0; - while (utf8.nextCodepoint()) |codepoint| : (i += 1) { + var i: c_int = @intCast(x); + while (utf8.nextCodepoint()) |codepoint| : (i += termbox.tb_wcwidth(codepoint)) { if (i >= max_length) break; - _ = termbox.tb_set_cell(@intCast(i + x), yc, codepoint, self.fg, self.bg); + _ = termbox.tb_set_cell(i, yc, codepoint, self.fg, self.bg); } } @@ -235,7 +235,8 @@ pub fn drawCharMultiple(self: TerminalBuffer, char: u32, x: usize, y: usize, len pub fn strWidth(str: []const u8) !u8 { const utf8view = try std.unicode.Utf8View.init(str); var utf8 = utf8view.iterator(); - var i: u8 = 0; - while (utf8.nextCodepoint()) |_| i += 1; - return i; + var i: c_int = 0; + while (utf8.nextCodepoint()) |codepoint| i += termbox.tb_wcwidth(codepoint); + + return @intCast(i); } From 04920e1b1ba5c50cbc3f69227b04867dd60ece72 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Tue, 8 Jul 2025 22:39:14 +0200 Subject: [PATCH 227/530] Implement custom session support (fixes #757) Signed-off-by: AnErrupTion --- build.zig | 14 ++++++++++++++ res/config.ini | 9 +++++++-- res/custom-sessions/README | 22 ++++++++++++++++++++++ res/lang/ar.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/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 | 1 + res/lang/tr.ini | 1 + res/lang/uk.ini | 1 + res/lang/zh_CN.ini | 1 + src/Environment.zig | 2 ++ src/auth.zig | 18 ++++++++++++++++++ src/config/Config.zig | 1 + src/config/Lang.zig | 1 + src/enums.zig | 1 + src/main.zig | 38 ++++++++++++++++++++++---------------- 27 files changed, 106 insertions(+), 18 deletions(-) create mode 100644 res/custom-sessions/README diff --git a/build.zig b/build.zig index 80de3e8..128357c 100644 --- a/build.zig +++ b/build.zig @@ -135,6 +135,12 @@ fn install_ly(allocator: std.mem.Allocator, patch_map: PatchMap, install_config: 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" }); + + std.fs.cwd().makePath(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 { std.debug.print("warn: {s} already exists as a directory.\n", .{ly_lang_path}); @@ -167,6 +173,14 @@ fn install_ly(allocator: std.mem.Allocator, patch_map: PatchMap, install_config: try installText(patched_setup, config_dir, ly_config_directory, "setup.sh", .{ .mode = 0o755 }); } + { + var custom_sessions_dir = std.fs.cwd().openDir(ly_custom_sessions_directory, .{}) catch unreachable; + defer custom_sessions_dir.close(); + + const patched_readme = try patchFile(allocator, "res/custom-sessions/README", patch_map); + try installText(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(); diff --git a/res/config.ini b/res/config.ini index 492319c..8b4a4f9 100644 --- a/res/config.ini +++ b/res/config.ini @@ -100,6 +100,11 @@ colormix_col2 = 0x000000FF # Color mixing animation third color id colormix_col3 = 0x20000000 +# Custom sessions directory +# You can specify multiple directories, +# e.g. $CONFIG_DIRECTORY/ly/custom-sessions:$PREFIX_DIRECTORY/share/custom-sessions +waylandsessions = $CONFIG_DIRECTORY/share/custom-sessions + # Input box active by default on startup # Available inputs: info_line, session, login, password default_input = login @@ -251,7 +256,7 @@ vi_mode = false # Wayland desktop environments # You can specify multiple directories, -# e.g. /usr/share/wayland-sessions:/usr/local/share/wayland-sessions +# e.g. $PREFIX_DIRECTORY/share/wayland-sessions:$PREFIX_DIRECTORY/local/share/wayland-sessions waylandsessions = $PREFIX_DIRECTORY/share/wayland-sessions # Xorg server command @@ -266,5 +271,5 @@ xinitrc = ~/.xinitrc # Xorg desktop environments # You can specify multiple directories, -# e.g. /usr/share/xsessions:/usr/local/share/xsessions +# e.g. $PREFIX_DIRECTORY/share/xsessions:$PREFIX_DIRECTORY/local/share/xsessions xsessions = $PREFIX_DIRECTORY/share/xsessions diff --git a/res/custom-sessions/README b/res/custom-sessions/README new file mode 100644 index 0000000..23c1ed1 --- /dev/null +++ b/res/custom-sessions/README @@ -0,0 +1,22 @@ +A custom session is just a desktop entry file, like for X11 and Wayland +sessions. For example: + +[Desktop Entry] +Name=Fish shell +Exec=$PREFIX_DIRECTORY/bin/fish +DesktopNames=null +Terminal=true + +The DesktopNames value is optional and sets the XDG_SESSION_DESKTOP and +XDG_CURRENT_DESKTOP environment variables. If equal to null or if not present, +XDG_SESSION_DESKTOP and XDG_CURRENT_DESKTOP will not be set. Otherwise, the +syntax is the same as described in the Freedesktop Desktop Entry Specification. + +The Terminal value specifies if standard output and standard error should be +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. + +Finally, do note that the XDG_SESSION_TYPE environment variable is set to +"unspecified" (without quotes), which is behavior that at least systemd +recognizes (see pam_systemd's man page) diff --git a/res/lang/ar.ini b/res/lang/ar.ini index ba1871f..9c2f9be 100644 --- a/res/lang/ar.ini +++ b/res/lang/ar.ini @@ -2,6 +2,7 @@ authenticating = جاري المصادقة... brightness_down = خفض السطوع brightness_up = رفع السطوع capslock = capslock + err_alloc = فشل في تخصيص الذاكرة err_bounds = out-of-bounds index err_brightness_change = فشل في تغيير سطوع الشاشة diff --git a/res/lang/cat.ini b/res/lang/cat.ini index 40d877f..7aa4261 100644 --- a/res/lang/cat.ini +++ b/res/lang/cat.ini @@ -2,6 +2,7 @@ authenticating = autenticant... brightness_down = abaixar brillantor brightness_up = apujar brillantor capslock = Bloq Majús + err_alloc = assignació de memòria fallida err_bounds = índex fora de límits err_brightness_change = error en canviar la brillantor diff --git a/res/lang/cs.ini b/res/lang/cs.ini index 9052663..cca457a 100644 --- a/res/lang/cs.ini +++ b/res/lang/cs.ini @@ -2,6 +2,7 @@ capslock = capslock + err_alloc = alokace paměti selhala err_bounds = index je mimo hranice pole diff --git a/res/lang/de.ini b/res/lang/de.ini index 3f76cc6..32e00a6 100644 --- a/res/lang/de.ini +++ b/res/lang/de.ini @@ -2,6 +2,7 @@ authenticating = authentifizieren... brightness_down = Helligkeit- brightness_up = Helligkeit+ capslock = Feststelltaste + err_alloc = Speicherzuweisung fehlgeschlagen err_bounds = Index ausserhalb des Bereichs err_brightness_change = Helligkeitsänderung fehlgeschlagen diff --git a/res/lang/en.ini b/res/lang/en.ini index 423020e..d7076db 100644 --- a/res/lang/en.ini +++ b/res/lang/en.ini @@ -2,6 +2,7 @@ authenticating = authenticating... brightness_down = decrease brightness brightness_up = increase brightness capslock = capslock +custom = custom err_alloc = failed memory allocation err_bounds = out-of-bounds index err_brightness_change = failed to change brightness diff --git a/res/lang/es.ini b/res/lang/es.ini index 5ec48af..fe3e6d9 100644 --- a/res/lang/es.ini +++ b/res/lang/es.ini @@ -2,6 +2,7 @@ authenticating = autenticando... brightness_down = bajar brillo brightness_up = subir brillo capslock = Bloq Mayús + err_alloc = asignación de memoria fallida err_bounds = índice fuera de límites diff --git a/res/lang/fr.ini b/res/lang/fr.ini index bda901b..6d46a1d 100644 --- a/res/lang/fr.ini +++ b/res/lang/fr.ini @@ -2,6 +2,7 @@ authenticating = authentification... brightness_down = diminuer la luminosité brightness_up = augmenter la luminosité capslock = verr.maj +custom = customisé err_alloc = échec d'allocation mémoire err_bounds = indice hors-limite err_brightness_change = échec du changement de luminosité diff --git a/res/lang/it.ini b/res/lang/it.ini index e7e0d17..4bff87e 100644 --- a/res/lang/it.ini +++ b/res/lang/it.ini @@ -2,6 +2,7 @@ capslock = capslock + err_alloc = impossibile allocare memoria err_bounds = indice fuori limite diff --git a/res/lang/pl.ini b/res/lang/pl.ini index eeb43e4..cfc5722 100644 --- a/res/lang/pl.ini +++ b/res/lang/pl.ini @@ -2,6 +2,7 @@ authenticating = uwierzytelnianie... brightness_down = zmniejsz jasność brightness_up = zwiększ jasność capslock = capslock + err_alloc = nieudana alokacja pamięci err_bounds = indeks poza zakresem err_brightness_change = nie udało się zmienić jasności diff --git a/res/lang/pt.ini b/res/lang/pt.ini index 97afee3..df1db13 100644 --- a/res/lang/pt.ini +++ b/res/lang/pt.ini @@ -2,6 +2,7 @@ capslock = capslock + err_alloc = erro na atribuição de memória err_bounds = índice fora de limites diff --git a/res/lang/pt_BR.ini b/res/lang/pt_BR.ini index c89b8aa..a7cee7b 100644 --- a/res/lang/pt_BR.ini +++ b/res/lang/pt_BR.ini @@ -2,6 +2,7 @@ capslock = caixa alta + err_alloc = alocação de memória malsucedida err_bounds = índice fora de limites diff --git a/res/lang/ro.ini b/res/lang/ro.ini index fe7ee38..4289b7f 100644 --- a/res/lang/ro.ini +++ b/res/lang/ro.ini @@ -17,6 +17,7 @@ capslock = capslock + err_pam_abort = tranzacţie pam anulată err_pam_acct_expired = cont expirat err_pam_auth = eroare de autentificare diff --git a/res/lang/ru.ini b/res/lang/ru.ini index 25dc553..b9cfb1a 100644 --- a/res/lang/ru.ini +++ b/res/lang/ru.ini @@ -2,6 +2,7 @@ capslock = capslock + err_alloc = не удалось выделить память err_bounds = за пределами индекса diff --git a/res/lang/sr.ini b/res/lang/sr.ini index a24e4b2..61ec4c0 100644 --- a/res/lang/sr.ini +++ b/res/lang/sr.ini @@ -2,6 +2,7 @@ capslock = capslock + err_alloc = neuspijesna alokacija memorije err_bounds = izvan granica indeksa diff --git a/res/lang/sv.ini b/res/lang/sv.ini index 668f431..9ccd0f3 100644 --- a/res/lang/sv.ini +++ b/res/lang/sv.ini @@ -2,6 +2,7 @@ capslock = capslock + err_alloc = misslyckad minnesallokering err_bounds = utanför banan index diff --git a/res/lang/tr.ini b/res/lang/tr.ini index ce69231..d553495 100644 --- a/res/lang/tr.ini +++ b/res/lang/tr.ini @@ -2,6 +2,7 @@ capslock = capslock + err_alloc = basarisiz bellek ayirma err_bounds = sinirlarin disinda dizin diff --git a/res/lang/uk.ini b/res/lang/uk.ini index af8f247..ffae75c 100644 --- a/res/lang/uk.ini +++ b/res/lang/uk.ini @@ -2,6 +2,7 @@ capslock = capslock + err_alloc = невдале виділення пам'яті err_bounds = поза межами індексу diff --git a/res/lang/zh_CN.ini b/res/lang/zh_CN.ini index ecc19be..6f7ba41 100644 --- a/res/lang/zh_CN.ini +++ b/res/lang/zh_CN.ini @@ -2,6 +2,7 @@ capslock = 大写锁定 + err_alloc = 内存分配失败 err_bounds = 索引越界 diff --git a/src/Environment.zig b/src/Environment.zig index b21d451..47d4d7f 100644 --- a/src/Environment.zig +++ b/src/Environment.zig @@ -8,6 +8,7 @@ pub const DesktopEntry = struct { Exec: []const u8 = "", Name: [:0]const u8 = "", DesktopNames: ?[:0]u8 = null, + Terminal: ?bool = null, }; pub const Entry = struct { @"Desktop Entry": DesktopEntry = .{} }; @@ -19,3 +20,4 @@ xdg_desktop_names: ?[:0]const u8 = null, cmd: []const u8 = "", specifier: []const u8 = "", display_server: DisplayServer = .wayland, +is_terminal: bool = false, diff --git a/src/auth.zig b/src/auth.zig index aa853c9..0b468da 100644 --- a/src/auth.zig +++ b/src/auth.zig @@ -180,6 +180,7 @@ fn startSession( const vt = try std.fmt.bufPrint(&vt_buf, "vt{d}", .{options.tty}); try executeX11Cmd(pwd.pw_shell.?, pwd.pw_dir.?, options, current_environment.cmd, vt); }, + .custom => try executeCustomCmd(pwd.pw_shell.?, options, current_environment.is_terminal, current_environment.cmd), } } @@ -201,6 +202,7 @@ fn setXdgEnv(tty_str: [:0]u8, environment: Environment) !void { .wayland => "wayland", .shell => "tty", .xinitrc, .x11 => "x11", + .custom => "unspecified", }, 0); // The "/run/user/%d" directory is not available on FreeBSD. It is much @@ -462,6 +464,22 @@ fn executeX11Cmd(shell: [*:0]const u8, pw_dir: [*:0]const u8, options: AuthOptio _ = std.c.waitpid(x_pid, &status, 0); } +fn executeCustomCmd(shell: [*:0]const u8, options: AuthOptions, is_terminal: bool, exec_cmd: []const u8) !void { + 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). + maybe_log_file = try redirectStandardStreams(options.session_log, true); + } + defer if (maybe_log_file) |log_file| log_file.close(); + + 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 }); + const args = [_:null]?[*:0]const u8{ shell, "-c", cmd_str }; + return std.posix.execveZ(shell, &args, std.c.environ); +} + fn redirectStandardStreams(session_log: []const u8, create: bool) !std.fs.File { const log_file = if (create) (try std.fs.cwd().createFile(session_log, .{ .mode = 0o666 })) else (try std.fs.cwd().openFile(session_log, .{ .mode = .read_write })); diff --git a/src/config/Config.zig b/src/config/Config.zig index 8b020f0..4a95b40 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -28,6 +28,7 @@ cmatrix_max_codepoint: u16 = 0x7B, colormix_col1: u32 = 0x00FF0000, colormix_col2: u32 = 0x000000FF, colormix_col3: u32 = 0x20000000, +custom_sessions: []const u8 = build_options.config_directory ++ "/ly/custom-sessions", default_input: Input = .login, doom_fire_height: u8 = 6, doom_fire_spread: u8 = 2, diff --git a/src/config/Lang.zig b/src/config/Lang.zig index f673090..3761fa7 100644 --- a/src/config/Lang.zig +++ b/src/config/Lang.zig @@ -7,6 +7,7 @@ authenticating: []const u8 = "authenticating...", brightness_down: []const u8 = "decrease brightness", brightness_up: []const u8 = "increase brightness", capslock: []const u8 = "capslock", +custom: []const u8 = "custom", err_alloc: []const u8 = "failed memory allocation", err_bounds: []const u8 = "out-of-bounds index", err_brightness_change: []const u8 = "failed to change brightness", diff --git a/src/enums.zig b/src/enums.zig index a70f17c..82c478d 100644 --- a/src/enums.zig +++ b/src/enums.zig @@ -11,6 +11,7 @@ pub const DisplayServer = enum { shell, xinitrc, x11, + custom, }; pub const Input = enum { diff --git a/src/main.zig b/src/main.zig index aed67e5..b52e779 100644 --- a/src/main.zig +++ b/src/main.zig @@ -290,10 +290,12 @@ pub fn main() !void { try info_line.addMessage(hostname, config.bg, config.fg); } + // Crawl session directories (Wayland, X11 and custom respectively) var wayland_session_dirs = std.mem.splitScalar(u8, config.waylandsessions, ':'); while (wayland_session_dirs.next()) |dir| { try crawl(&session, lang, dir, .wayland); } + if (build_options.enable_x11_support) { var x_session_dirs = std.mem.splitScalar(u8, config.xsessions, ':'); while (x_session_dirs.next()) |dir| { @@ -301,6 +303,11 @@ pub fn main() !void { } } + var custom_session_dirs = std.mem.splitScalar(u8, config.custom_sessions, ':'); + while (custom_session_dirs.next()) |dir| { + try crawl(&session, lang, dir, .custom); + } + var login = Text.init(allocator, &buffer, false, null); defer login.deinit(); @@ -864,11 +871,7 @@ fn addOtherEnvironment(session: *Session, lang: Lang, display_server: DisplaySer .xdg_session_desktop = null, .xdg_desktop_names = null, .cmd = exec orelse "", - .specifier = switch (display_server) { - .wayland => lang.wayland, - .x11 => lang.x11, - else => lang.other, - }, + .specifier = lang.other, .display_server = display_server, }); } @@ -890,40 +893,43 @@ fn crawl(session: *Session, lang: Lang, path: []const u8, display_server: Displa }); errdefer entry_ini.deinit(); - var xdg_session_desktop: []const u8 = undefined; + var maybe_xdg_session_desktop: ?[]const u8 = null; const maybe_desktop_names = entry_ini.data.@"Desktop Entry".DesktopNames; if (maybe_desktop_names) |desktop_names| { - xdg_session_desktop = std.mem.sliceTo(desktop_names, ';'); - } else { - // if DesktopNames is empty, we'll take the name of the session file - xdg_session_desktop = std.fs.path.stem(item.name); + maybe_xdg_session_desktop = std.mem.sliceTo(desktop_names, ';'); + } 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 + maybe_xdg_session_desktop = std.fs.path.stem(item.name); } // Prepare the XDG_CURRENT_DESKTOP environment variable here const entry = entry_ini.data.@"Desktop Entry"; - var xdg_desktop_names: ?[:0]const u8 = null; + var maybe_xdg_desktop_names: ?[:0]const u8 = null; if (entry.DesktopNames) |desktop_names| { for (desktop_names) |*c| { if (c.* == ';') c.* = ':'; } - xdg_desktop_names = desktop_names; + maybe_xdg_desktop_names = desktop_names; } - const session_desktop = try session.label.allocator.dupeZ(u8, xdg_session_desktop); - errdefer session.label.allocator.free(session_desktop); + const maybe_session_desktop = if (maybe_xdg_session_desktop) |xdg_session_desktop| try session.label.allocator.dupeZ(u8, xdg_session_desktop) else null; + errdefer if (maybe_session_desktop) |session_desktop| session.label.allocator.free(session_desktop); try session.addEnvironment(.{ .entry_ini = entry_ini, .name = entry.Name, - .xdg_session_desktop = session_desktop, - .xdg_desktop_names = xdg_desktop_names, + .xdg_session_desktop = maybe_session_desktop, + .xdg_desktop_names = maybe_xdg_desktop_names, .cmd = entry.Exec, .specifier = switch (display_server) { .wayland => lang.wayland, .x11 => lang.x11, + .custom => lang.custom, else => lang.other, }, .display_server = display_server, + .is_terminal = entry.Terminal orelse false, }); } } From 97efac0cd11cf44461089724408f9b3405c66bef Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Wed, 9 Jul 2025 21:38:45 +0200 Subject: [PATCH 228/530] Add Matrix space link to README Signed-off-by: AnErrupTion --- readme.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/readme.md b/readme.md index 57ca447..c81d6ef 100644 --- a/readme.md +++ b/readme.md @@ -4,6 +4,8 @@ Ly is a lightweight TUI (ncurses-like) display manager for Linux and BSD. +Join us on Matrix over at [#ly:envs.net](https://matrix.to/#/#ly:envs.net)! + **Note**: Development happens on [Codeberg](https://codeberg.org/AnErrupTion/ly) with a mirror on [GitHub](https://github.com/fairyglade/ly). ## Dependencies From 1d4e32ba82829038b58c0b01904eb5ab6ad331df Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Thu, 10 Jul 2025 10:06:19 +0200 Subject: [PATCH 229/530] List all users in the system (fixes #373) Signed-off-by: AnErrupTion --- res/config.ini | 3 + res/lang/ar.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/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 | 1 + res/lang/tr.ini | 1 + res/lang/uk.ini | 1 + res/lang/zh_CN.ini | 1 + src/UidRange.zig | 6 ++ src/config/Config.zig | 1 + src/config/Lang.zig | 1 + src/main.zig | 124 ++++++++++++++++++++++++++------ src/tui/components/UserList.zig | 45 ++++++++++++ 24 files changed, 176 insertions(+), 22 deletions(-) create mode 100644 src/UidRange.zig create mode 100644 src/tui/components/UserList.zig diff --git a/res/config.ini b/res/config.ini index 8b4a4f9..866b6b2 100644 --- a/res/config.ini +++ b/res/config.ini @@ -184,6 +184,9 @@ load = true # You can also set environment variables in there, they'll persist until logout login_cmd = null +# Path for login.defs file (used for listing all local users on the system) +login_defs_path = /etc/login.defs + # Command executed when logging out # If null, no command will be executed # Important: the session will already be terminated when this command is executed, so diff --git a/res/lang/ar.ini b/res/lang/ar.ini index 9c2f9be..b06f253 100644 --- a/res/lang/ar.ini +++ b/res/lang/ar.ini @@ -41,6 +41,7 @@ err_pwnam = فشل في جلب معلومات المستخدم err_sleep = فشل في تنفيذ أمر sleep err_tty_ctrl = فشل في نقل تحكم الطرفية (TTY) + err_user_gid = فشل في تعيين معرّف المجموعة (GID) للمستخدم err_user_init = فشل في تهيئة بيانات المستخدم err_user_uid = فشل في تعيين معرّف المستخدم (UID) diff --git a/res/lang/cat.ini b/res/lang/cat.ini index 7aa4261..d0ce33e 100644 --- a/res/lang/cat.ini +++ b/res/lang/cat.ini @@ -41,6 +41,7 @@ err_pwnam = error en obtenir la informació de l'usuari + 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 diff --git a/res/lang/cs.ini b/res/lang/cs.ini index cca457a..7c87ea6 100644 --- a/res/lang/cs.ini +++ b/res/lang/cs.ini @@ -41,6 +41,7 @@ err_pwnam = nelze získat informace o uživateli + err_user_gid = nastavení GID uživatele selhalo err_user_init = inicializace uživatele selhala err_user_uid = nastavení UID uživateli selhalo diff --git a/res/lang/de.ini b/res/lang/de.ini index 32e00a6..44bbc53 100644 --- a/res/lang/de.ini +++ b/res/lang/de.ini @@ -41,6 +41,7 @@ err_pwnam = Abrufen der Benutzerinformationen fehlgeschlagen err_sleep = Sleep-Befehl fehlgeschlagen err_tty_ctrl = Fehler bei der TTY-Uebergabe + err_user_gid = Fehler beim Setzen der Gruppen-ID err_user_init = Nutzer-Initialisierung fehlgeschlagen err_user_uid = Setzen der Benutzer-ID fehlgeschlagen diff --git a/res/lang/en.ini b/res/lang/en.ini index d7076db..9554658 100644 --- a/res/lang/en.ini +++ b/res/lang/en.ini @@ -41,6 +41,7 @@ err_pwnam = failed to get user info err_sleep = failed to execute sleep command err_switch_tty = failed to switch tty err_tty_ctrl = tty control transfer failed +err_no_users = no users found err_user_gid = failed to set user GID err_user_init = failed to initialize user err_user_uid = failed to set user UID diff --git a/res/lang/es.ini b/res/lang/es.ini index fe3e6d9..2bd51a7 100644 --- a/res/lang/es.ini +++ b/res/lang/es.ini @@ -41,6 +41,7 @@ err_pwnam = error al obtener la información del usuario + 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 diff --git a/res/lang/fr.ini b/res/lang/fr.ini index 6d46a1d..e024820 100644 --- a/res/lang/fr.ini +++ b/res/lang/fr.ini @@ -41,6 +41,7 @@ err_pwnam = échec de lecture des infos utilisateur err_sleep = échec de l'exécution de la commande de veille err_switch_tty = échec du changement de terminal err_tty_ctrl = échec du transfert de contrôle du terminal +err_no_users = aucun utilisateur trouvé err_user_gid = échec de modification du GID err_user_init = échec d'initialisation de l'utilisateur err_user_uid = échec de modification du UID diff --git a/res/lang/it.ini b/res/lang/it.ini index 4bff87e..3a7f5a8 100644 --- a/res/lang/it.ini +++ b/res/lang/it.ini @@ -41,6 +41,7 @@ err_pwnam = impossibile ottenere dati utente + err_user_gid = impossibile impostare GID utente err_user_init = impossibile inizializzare utente err_user_uid = impossible impostare UID utente diff --git a/res/lang/pl.ini b/res/lang/pl.ini index cfc5722..b3eee3d 100644 --- a/res/lang/pl.ini +++ b/res/lang/pl.ini @@ -41,6 +41,7 @@ err_pwnam = nie udało się uzyskać informacji o użytkowniku err_sleep = nie udało się wykonać polecenia sleep err_tty_ctrl = nie udało się przekazać kontroli tty + 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 diff --git a/res/lang/pt.ini b/res/lang/pt.ini index df1db13..587d3b4 100644 --- a/res/lang/pt.ini +++ b/res/lang/pt.ini @@ -41,6 +41,7 @@ err_pwnam = erro ao obter informação do utilizador + 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 diff --git a/res/lang/pt_BR.ini b/res/lang/pt_BR.ini index a7cee7b..2485522 100644 --- a/res/lang/pt_BR.ini +++ b/res/lang/pt_BR.ini @@ -41,6 +41,7 @@ err_pwnam = não foi possível obter informações do usuário + 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 diff --git a/res/lang/ro.ini b/res/lang/ro.ini index 4289b7f..74c0461 100644 --- a/res/lang/ro.ini +++ b/res/lang/ro.ini @@ -49,6 +49,7 @@ err_perm_user = nu s-a putut face downgrade permisiunilor de utilizator + login = utilizator logout = opreşte sesiunea diff --git a/res/lang/ru.ini b/res/lang/ru.ini index b9cfb1a..103f135 100644 --- a/res/lang/ru.ini +++ b/res/lang/ru.ini @@ -41,6 +41,7 @@ err_pwnam = не удалось получить информацию о пол + err_user_gid = не удалось установить GID пользователя err_user_init = не удалось инициализировать пользователя err_user_uid = не удалось установить UID пользователя diff --git a/res/lang/sr.ini b/res/lang/sr.ini index 61ec4c0..c63fe28 100644 --- a/res/lang/sr.ini +++ b/res/lang/sr.ini @@ -41,6 +41,7 @@ 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 diff --git a/res/lang/sv.ini b/res/lang/sv.ini index 9ccd0f3..17359cc 100644 --- a/res/lang/sv.ini +++ b/res/lang/sv.ini @@ -41,6 +41,7 @@ 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 diff --git a/res/lang/tr.ini b/res/lang/tr.ini index d553495..535015c 100644 --- a/res/lang/tr.ini +++ b/res/lang/tr.ini @@ -41,6 +41,7 @@ err_pwnam = kullanici bilgileri alinamadi + err_user_gid = kullanici icin GID ayarlanamadi err_user_init = kullanici oturumu baslatilamadi err_user_uid = kullanici icin UID ayarlanamadi diff --git a/res/lang/uk.ini b/res/lang/uk.ini index ffae75c..9e6de2d 100644 --- a/res/lang/uk.ini +++ b/res/lang/uk.ini @@ -41,6 +41,7 @@ err_pwnam = не вдалося отримати дані користувача + err_user_gid = не вдалося змінити GID користувача err_user_init = не вдалося ініціалізувати користувача err_user_uid = не вдалося змінити UID користувача diff --git a/res/lang/zh_CN.ini b/res/lang/zh_CN.ini index 6f7ba41..9afdc66 100644 --- a/res/lang/zh_CN.ini +++ b/res/lang/zh_CN.ini @@ -41,6 +41,7 @@ err_pwnam = 获取用户信息失败 + err_user_gid = 设置用户GID失败 err_user_init = 初始化用户失败 err_user_uid = 设置用户UID失败 diff --git a/src/UidRange.zig b/src/UidRange.zig new file mode 100644 index 0000000..13eb979 --- /dev/null +++ b/src/UidRange.zig @@ -0,0 +1,6 @@ +const std = @import("std"); + +// We set both values to 0 by default so that, in case they aren't present in +// the login.defs for some reason, then only the root username will be shown +uid_min: std.c.uid_t = 0, +uid_max: std.c.uid_t = 0, diff --git a/src/config/Config.zig b/src/config/Config.zig index 4a95b40..92e0552 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -50,6 +50,7 @@ input_len: u8 = 34, lang: []const u8 = "en", load: bool = true, login_cmd: ?[]const u8 = null, +login_defs_path: []const u8 = "/etc/login.defs", logout_cmd: ?[]const u8 = null, margin_box_h: u8 = 2, margin_box_v: u8 = 1, diff --git a/src/config/Lang.zig b/src/config/Lang.zig index 3761fa7..7175e0f 100644 --- a/src/config/Lang.zig +++ b/src/config/Lang.zig @@ -46,6 +46,7 @@ err_pwnam: []const u8 = "failed to get user info", err_sleep: []const u8 = "failed to execute sleep command", err_switch_tty: []const u8 = "failed to switch tty", err_tty_ctrl: []const u8 = "tty control transfer failed", +err_no_users: []const u8 = "no users found", err_user_gid: []const u8 = "failed to set user GID", err_user_init: []const u8 = "failed to initialize user", err_user_uid: []const u8 = "failed to set user UID", diff --git a/src/main.zig b/src/main.zig index b52e779..0cc2c6e 100644 --- a/src/main.zig +++ b/src/main.zig @@ -18,12 +18,15 @@ 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 Save = @import("config/Save.zig"); const migrator = @import("config/migrator.zig"); const SharedError = @import("SharedError.zig"); +const UidRange = @import("UidRange.zig"); +const StringList = std.ArrayListUnmanaged([]const u8); const Ini = ini.Ini; const DisplayServer = enums.DisplayServer; const Entry = Environment.Entry; @@ -308,7 +311,21 @@ pub fn main() !void { try crawl(&session, lang, dir, .custom); } - var login = Text.init(allocator, &buffer, false, null); + var usernames = try getAllUsernames(allocator, config.login_defs_path); + defer { + for (usernames.items) |username| allocator.free(username); + usernames.deinit(allocator); + } + + if (usernames.items.len == 0) { + // If we have no usernames, simply add an error to the info line. + // 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); + } + + var login = try UserList.init(allocator, &buffer, usernames); defer login.deinit(); var password = Text.init(allocator, &buffer, true, config.asterisk); @@ -320,9 +337,17 @@ pub fn main() !void { // Load last saved username and desktop selection, if any if (config.load) { if (save.user) |user| { - try login.text.appendSlice(login.allocator, user); - login.end = user.len; - login.cursor = login.end; + // Find user with saved name, and switch over to it + // If it doesn't exist (anymore), we don't change the value + // Note that we could instead save the username index, but migrating + // from the raw username to an index is non-trivial and I'm lazy :P + for (usernames.items, 0..) |username, i| { + if (std.mem.eql(u8, username, user)) { + login.label.current = i; + break; + } + } + active_input = .password; } @@ -338,15 +363,13 @@ pub fn main() !void { 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.position(coordinates.x, coordinates.y + 4, coordinates.visible_length); + 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.handle(null, insert_mode) catch { - try info_line.addMessage(lang.err_alloc, config.error_bg, config.error_fg); - }, + .login => login.label.handle(null, insert_mode), .password => password.handle(null, insert_mode) catch { try info_line.addMessage(lang.err_alloc, config.error_bg, config.error_fg); }, @@ -464,7 +487,7 @@ pub fn main() !void { 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.position(coordinates.x, coordinates.y + 4, coordinates.visible_length); + 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; @@ -473,9 +496,7 @@ pub fn main() !void { switch (active_input) { .info_line => info_line.label.handle(null, insert_mode), .session => session.label.handle(null, insert_mode), - .login => login.handle(null, insert_mode) catch { - try info_line.addMessage(lang.err_alloc, config.error_bg, config.error_fg); - }, + .login => login.label.handle(null, insert_mode), .password => password.handle(null, insert_mode) catch { try info_line.addMessage(lang.err_alloc, config.error_bg, config.error_fg); }, @@ -571,7 +592,7 @@ pub fn main() !void { } session.label.draw(); - login.draw(); + login.label.draw(); password.draw(); } else { std.Thread.sleep(std.time.ns_per_ms * 10); @@ -660,10 +681,7 @@ pub fn main() !void { }, termbox.TB_KEY_CTRL_C => run = false, termbox.TB_KEY_CTRL_U => { - if (active_input == .login) { - login.clear(); - update = true; - } else if (active_input == .password) { + if (active_input == .password) { password.clear(); update = true; } @@ -726,7 +744,7 @@ pub fn main() !void { defer file.close(); const save_data = Save{ - .user = login.text.items, + .user = login.getCurrentUser(), .session_index = session.label.current, }; ini.writeFromStruct(save_data, file.writer(), null, .{}) catch break :save_last_settings; @@ -739,7 +757,7 @@ pub fn main() !void { defer shared_err.deinit(); { - const login_text = try allocator.dupeZ(u8, login.text.items); + const login_text = try allocator.dupeZ(u8, login.getCurrentUser()); defer allocator.free(login_text); const password_text = try allocator.dupeZ(u8, password.text.items); defer allocator.free(password_text); @@ -845,9 +863,7 @@ pub fn main() !void { switch (active_input) { .info_line => info_line.label.handle(&event, insert_mode), .session => session.label.handle(&event, insert_mode), - .login => login.handle(&event, insert_mode) catch { - try info_line.addMessage(lang.err_alloc, config.error_bg, config.error_fg); - }, + .login => login.label.handle(&event, insert_mode), .password => password.handle(&event, insert_mode) catch { try info_line.addMessage(lang.err_alloc, config.error_bg, config.error_fg); }, @@ -934,6 +950,70 @@ fn crawl(session: *Session, lang: Lang, path: []const u8, display_server: Displa } } +fn getAllUsernames(allocator: std.mem.Allocator, login_defs_path: []const u8) !StringList { + const uid_range = try getUserIdRange(allocator, login_defs_path); + + var usernames: StringList = .empty; + var maybe_entry = interop.pwd.getpwent(); + + while (maybe_entry != null) { + const entry = maybe_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.pw_uid >= uid_range.uid_min and entry.pw_uid <= uid_range.uid_max or entry.pw_uid == 0) { + const pw_name_slice = entry.pw_name[0..std.mem.len(entry.pw_name)]; + const username = try allocator.dupe(u8, pw_name_slice); + + try usernames.append(allocator, username); + } + + maybe_entry = interop.pwd.getpwent(); + } + + interop.pwd.endpwent(); + return usernames; +} + +// 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... +fn getUserIdRange(allocator: std.mem.Allocator, login_defs_path: []const u8) !UidRange { + const login_defs_file = try std.fs.cwd().openFile(login_defs_path, .{}); + defer login_defs_file.close(); + + const login_defs_buffer = try login_defs_file.readToEndAlloc(allocator, std.math.maxInt(u16)); + defer allocator.free(login_defs_buffer); + + var iterator = std.mem.splitScalar(u8, login_defs_buffer, '\n'); + var uid_range = UidRange{}; + + 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.c.uid_t, "UID_MIN", trimmed_line); + } else if (std.mem.startsWith(u8, trimmed_line, "UID_MAX")) { + uid_range.uid_max = try parseValue(std.c.uid_t, "UID_MAX", trimmed_line); + } + } + + return uid_range; +} + +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; + + while (iterator.next()) |slice| { + // Skip the slice if it's empty (whitespace) or is the name of the + // property (e.g. UID_MIN or UID_MAX) + if (slice.len == 0 or std.mem.eql(u8, slice, name)) continue; + maybe_value = std.fmt.parseInt(T, slice, 10) catch continue; + } + + return maybe_value orelse error.ValueNotFound; +} + fn adjustBrightness(allocator: std.mem.Allocator, cmd: []const u8) !void { var brightness = std.process.Child.init(&[_][]const u8{ "/bin/sh", "-c", cmd }, allocator); brightness.stdout_behavior = .Ignore; diff --git a/src/tui/components/UserList.zig b/src/tui/components/UserList.zig new file mode 100644 index 0000000..41eff39 --- /dev/null +++ b/src/tui/components/UserList.zig @@ -0,0 +1,45 @@ +const std = @import("std"); +const TerminalBuffer = @import("../TerminalBuffer.zig"); +const generic = @import("generic.zig"); + +const StringList = std.ArrayListUnmanaged([]const u8); +const Allocator = std.mem.Allocator; + +const UsernameText = generic.CyclableLabel([]const u8); + +const UserList = @This(); + +label: UsernameText, + +pub fn init(allocator: Allocator, buffer: *TerminalBuffer, usernames: StringList) !UserList { + var userList = UserList{ + .label = UsernameText.init(allocator, buffer, drawItem), + }; + + for (usernames.items) |username| { + if (username.len == 0) continue; + + try userList.label.addItem(username); + } + + return userList; +} + +pub fn deinit(self: *UserList) void { + self.label.deinit(); +} + +pub fn getCurrentUser(self: UserList) []const u8 { + return self.label.list.items[self.label.current]; +} + +fn drawItem(label: *UsernameText, username: []const u8, _: usize, _: usize) bool { + const length = @min(username.len, label.visible_length - 3); + if (length == 0) return false; + + const x = if (label.text_in_center) (label.x + (label.visible_length - username.len) / 2) else (label.x + 2); + label.first_char_x = x + username.len; + + label.buffer.drawLabel(username, x, label.y); + return true; +} From 5c3da103869f56bf21e6155e4876083bbb80b31a Mon Sep 17 00:00:00 2001 From: RadsammyT Date: Wed, 16 Jul 2025 22:03:51 -0400 Subject: [PATCH 230/530] fix: confined labels cutting off `drawConfinedLabel` didn't take into account the starting x axis when checking to break for exceeding `max_length`. This should fix the box title not appearing on terminals with larger column counts. --- src/tui/TerminalBuffer.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tui/TerminalBuffer.zig b/src/tui/TerminalBuffer.zig index 7d608c1..555f26b 100644 --- a/src/tui/TerminalBuffer.zig +++ b/src/tui/TerminalBuffer.zig @@ -220,7 +220,7 @@ 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 >= max_length) break; + if (i - @as(c_int, @intCast(x)) >= max_length) break; _ = termbox.tb_set_cell(i, yc, codepoint, self.fg, self.bg); } } From c11194332ca4874b37559e9ec5276421578d6468 Mon Sep 17 00:00:00 2001 From: darallium Date: Thu, 19 Jun 2025 12:45:13 +0900 Subject: [PATCH 231/530] Add Japanese lang file --- res/lang/ja_JP.ini | 126 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 126 insertions(+) create mode 100644 res/lang/ja_JP.ini diff --git a/res/lang/ja_JP.ini b/res/lang/ja_JP.ini new file mode 100644 index 0000000..c5f5596 --- /dev/null +++ b/res/lang/ja_JP.ini @@ -0,0 +1,126 @@ +authenticating=認証中... +brightness_down=明るさを下げる +brightness_up=明るさを上げる +capslock=CapsLock +err_alloc=メモリ割り当て失敗 +err_bounds=境界外インデックス +err_brightness_change=明るさの変更に失敗しました +err_chdir=ホームフォルダを開けませんでした +err_config=設定ファイルを解析できません +err_console_dev=コンソールへのアクセスに失敗しました +err_dgn_oob=ログメッセージ +err_domain=無効なドメイン +err_empty_password=空のパスワードは許可されていません +err_envlist=環境変数リストの取得に失敗しました +err_hostname=ホスト名の取得に失敗しました +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_tty_ctrl=TTY制御の転送に失敗しました +err_user_gid=ユーザーGIDの設定に失敗しました +err_user_init=ユーザーの初期化に失敗しました +err_user_uid=ユーザーUIDの設定に失敗しました +err_xauth=xauthコマンドの実行に失敗しました +err_xcb_conn=XCB接続に失敗しました +err_xsessions_dir=セッションフォルダが見つかりませんでした +err_xsessions_open=セッションフォルダを開けませんでした +insert=挿入 +login=ログイン +logout=ログアウト済み +no_x11_support=X11サポートはコンパイル時に無効化されています +normal=通常 +numlock=NumLock +other=その他 +password=パスワード +restart=再起動 +shell=シェル +shutdown=シャットダウン +sleep=スリープ +wayland=Wayland +x11=X11 +xinitrc=xinitrc +authenticating=認証中... +brightness_down=明るさを下げる +brightness_up=明るさを上げる +capslock=CapsLock +err_alloc=メモリ割り当て失敗 +err_bounds=境界外インデックス +err_brightness_change=明るさの変更に失敗しました +err_chdir=ホームフォルダを開けませんでした +err_config=設定ファイルを解析できません +err_console_dev=コンソールへのアクセスに失敗しました +err_dgn_oob=ログメッセージ +err_domain=無効なドメイン +err_empty_password=空のパスワードは許可されていません +err_envlist=環境変数リストの取得に失敗しました +err_hostname=ホスト名の取得に失敗しました +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_tty_ctrl=TTY制御の転送に失敗しました +err_user_gid=ユーザーGIDの設定に失敗しました +err_user_init=ユーザーの初期化に失敗しました +err_user_uid=ユーザーUIDの設定に失敗しました +err_xauth=xauthコマンドの実行に失敗しました +err_xcb_conn=XCB接続に失敗しました +err_xsessions_dir=セッションフォルダが見つかりませんでした +err_xsessions_open=セッションフォルダを開けませんでした +insert=挿入 +login=ログイン +logout=ログアウト済み +no_x11_support=X11サポートはコンパイル時に無効化されています +normal=通常 +numlock=NumLock +other=その他 +password=パスワード +restart=再起動 +shell=シェル +shutdown=シャットダウン +sleep=スリープ +wayland=Wayland +x11=X11 +xinitrc=xinitrc From c3d0864e6257d6bf7d10f40b707445bfdb828cc9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Lucas?= Date: Fri, 25 Jul 2025 01:29:57 -0300 Subject: [PATCH 232/530] fix: update termbox2 dependency URL --- build.zig.zon | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.zig.zon b/build.zig.zon index 5d8e4e4..1acc19d 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -13,8 +13,8 @@ .hash = "zigini-0.3.1-BSkB7XJGAAB2E-sKyzhTaQCBlYBL8yqzE4E_jmSY99sC", }, .termbox2 = .{ - .url = "git+https://github.com/termbox/termbox2#8ee9dc17e1ca61c630f91db0aa7f81fa29a32040", - .hash = "N-V-__8AAKvjBAAUF2KVdkHsNs7L5EEZYzBnrJTBvj-baBMZ", + .url = "git+https://github.com/AnErrupTion/termbox2?ref=get_cell#e975d250ee6567773400e9d5b0b5c2f175349c57", + .hash = "N-V-__8AAMruBACJ7xT-O64hnS7lNeTiZQMketHdkHKrR1A8", }, }, .paths = .{""}, From 5fb40899e5c6ed839302b93f965e04d77a03945a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Lucas?= Date: Fri, 25 Jul 2025 01:33:57 -0300 Subject: [PATCH 233/530] refactor: remove termbox_extras in favor of the fork --- src/tui/TerminalBuffer.zig | 5 ++--- src/tui/termbox_extras.zig | 26 -------------------------- 2 files changed, 2 insertions(+), 29 deletions(-) delete mode 100644 src/tui/termbox_extras.zig diff --git a/src/tui/TerminalBuffer.zig b/src/tui/TerminalBuffer.zig index 86240b9..555f26b 100644 --- a/src/tui/TerminalBuffer.zig +++ b/src/tui/TerminalBuffer.zig @@ -2,7 +2,6 @@ const std = @import("std"); const builtin = @import("builtin"); const interop = @import("../interop.zig"); const Cell = @import("Cell.zig"); -const termbox_extras = @import("termbox_extras.zig"); const Random = std.Random; @@ -114,8 +113,8 @@ pub fn cascade(self: TerminalBuffer) bool { var cell: termbox.tb_cell = undefined; var cell_under: termbox.tb_cell = undefined; - _ = termbox_extras.tb_get_cell(@intCast(x), @intCast(y - 1), 1, &cell); - _ = termbox_extras.tb_get_cell(@intCast(x), @intCast(y), 1, &cell_under); + _ = termbox.tb_get_cell(@intCast(x), @intCast(y - 1), 1, &cell); + _ = termbox.tb_get_cell(@intCast(x), @intCast(y), 1, &cell_under); const char: u8 = @truncate(cell.ch); if (std.ascii.isWhitespace(char)) continue; diff --git a/src/tui/termbox_extras.zig b/src/tui/termbox_extras.zig deleted file mode 100644 index 314a1d8..0000000 --- a/src/tui/termbox_extras.zig +++ /dev/null @@ -1,26 +0,0 @@ -const std = @import("std"); -const interop = @import("../interop.zig"); -const termbox = interop.termbox; - -pub fn tb_get_cell(x: c_int, y: c_int, back: c_int, cell: *termbox.tb_cell) c_int { - if (back == 0) { - return termbox.TB_ERR; - } - - const width = termbox.tb_width(); - const height = termbox.tb_height(); - - if (x < 0 or x >= width or y < 0 or y >= height) { - return termbox.TB_ERR_OUT_OF_BOUNDS; - } - - const buffer = termbox.tb_cell_buffer(); - if (buffer == null) { - return termbox.TB_ERR_NOT_INIT; - } - - const index = y * width + x; - cell.* = buffer[@intCast(index)]; - - return termbox.TB_OK; -} From 19c879a20144dd3f1d97ccbc9c2c3da7fe51a34f Mon Sep 17 00:00:00 2001 From: tyusha Date: Fri, 25 Jul 2025 16:36:06 +0300 Subject: [PATCH 234/530] update russian translation --- res/lang/ru.ini | 38 +++++++++++++++++++------------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/res/lang/ru.ini b/res/lang/ru.ini index 103f135..2448b40 100644 --- a/res/lang/ru.ini +++ b/res/lang/ru.ini @@ -1,17 +1,17 @@ - - - +authenticating = аутентификация... +brightness_down = уменьшить яркость +brightness_up = увеличить яркость capslock = capslock err_alloc = не удалось выделить память err_bounds = за пределами индекса - +err_brightness_change = не удалось изменить яркость err_chdir = не удалось открыть домашнюю папку - +err_config = не удалось разобрать файл конфигурации err_dgn_oob = отладочное сообщение (log) err_domain = неверный домен - - +err_empty_password = пустой пароль не допустим +err_envlist = не удалось получить envlist err_hostname = не удалось получить имя хоста err_mlock = сбой блокировки памяти @@ -38,29 +38,29 @@ err_perm_dir = не удалось изменить текущий катало err_perm_group = не удалось понизить права доступа группы err_perm_user = не удалось понизить права доступа пользователя err_pwnam = не удалось получить информацию о пользователе - - - - +err_sleep = не удалось выполнить команду sleep +err_switch_tty = не удалось переключить tty +err_tty_ctrl = передача управления tty не удалась +err_no_users = пользователи не найдены err_user_gid = не удалось установить GID пользователя err_user_init = не удалось инициализировать пользователя err_user_uid = не удалось установить UID пользователя - - +err_xauth = команда xauth не выполнена +err_xcb_conn = ошибка подключения xcb err_xsessions_dir = не удалось найти сессионную папку err_xsessions_open = не удалось открыть сессионную папку login = логин -logout = logged out - +logout = вышел из системы +no_x11_support = поддержка x11 отключена во время компиляции numlock = numlock - +other = прочие password = пароль restart = перезагрузить -shell = shell +shell = оболочка shutdown = выключить - +sleep = сон wayland = wayland - +x11 = x11 xinitrc = xinitrc From 3d3cf84292daf72e673ab4cf1267c61a056e164e Mon Sep 17 00:00:00 2001 From: tyusha Date: Fri, 25 Jul 2025 16:41:06 +0300 Subject: [PATCH 235/530] fix --- 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 2448b40..131e602 100644 --- a/res/lang/ru.ini +++ b/res/lang/ru.ini @@ -11,7 +11,7 @@ err_config = не удалось разобрать файл конфигура err_dgn_oob = отладочное сообщение (log) err_domain = неверный домен err_empty_password = пустой пароль не допустим -err_envlist = не удалось получить envlist +err_envlist = не удалось получить список переменных среды err_hostname = не удалось получить имя хоста err_mlock = сбой блокировки памяти From a9d85a6925183ca997f552053303eb95a6539cf2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Lucas?= Date: Sat, 26 Jul 2025 13:55:48 -0300 Subject: [PATCH 236/530] fix: duplicated entry waylandsessions --- res/config.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/res/config.ini b/res/config.ini index 866b6b2..7114a2c 100644 --- a/res/config.ini +++ b/res/config.ini @@ -103,7 +103,7 @@ colormix_col3 = 0x20000000 # Custom sessions directory # You can specify multiple directories, # e.g. $CONFIG_DIRECTORY/ly/custom-sessions:$PREFIX_DIRECTORY/share/custom-sessions -waylandsessions = $CONFIG_DIRECTORY/share/custom-sessions +custom_sessions = $CONFIG_DIRECTORY/ly/custom-sessions # Input box active by default on startup # Available inputs: info_line, session, login, password From 8030cf524408ac5a5f75ab02d74bb3a9edb3b79e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Lucas?= Date: Sat, 26 Jul 2025 14:02:49 -0300 Subject: [PATCH 237/530] fix: Reorder default PATH to prioritize /usr/local directories to solve problem with archlinux --- res/config.ini | 2 +- src/config/Config.zig | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/res/config.ini b/res/config.ini index 7114a2c..02a242e 100644 --- a/res/config.ini +++ b/res/config.ini @@ -207,7 +207,7 @@ numlock = false # Default path # If null, ly doesn't set a path -path = /sbin:/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin +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 diff --git a/src/config/Config.zig b/src/config/Config.zig index 92e0552..6a8ba18 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -56,7 +56,7 @@ margin_box_h: u8 = 2, margin_box_v: u8 = 1, min_refresh_delta: u16 = 5, numlock: bool = false, -path: ?[:0]const u8 = "/sbin:/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin", +path: ?[:0]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, From 1f0274e797095899acd8f8d236fb7a86d8e85af4 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Sat, 2 Aug 2025 00:02:30 +0200 Subject: [PATCH 238/530] Add packaging status in readme.md through Repology Signed-off-by: AnErrupTion --- readme.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/readme.md b/readme.md index c81d6ef..98595a6 100644 --- a/readme.md +++ b/readme.md @@ -21,6 +21,10 @@ Join us on Matrix over at [#ly:envs.net](https://matrix.to/#/#ly:envs.net)! - shutdown - brightnessctl +## Packaging status + +[![Packaging status](https://repology.org/badge/vertical-allrepos/ly.svg)](https://repology.org/project/ly/versions) + ### Debian ``` From c05c32c5beb5ee94d7de6e1129add52024492a70 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Sat, 2 Aug 2025 00:57:22 +0200 Subject: [PATCH 239/530] Fix possible overflow with 5-digit+ UIDs (c.f. #684) 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 0b468da..370a2be 100644 --- a/src/auth.zig +++ b/src/auth.zig @@ -211,7 +211,7 @@ fn setXdgEnv(tty_str: [:0]u8, environment: Environment) !void { // directory. if (builtin.os.tag != .freebsd) { const uid = interop.unistd.getuid(); - var uid_buffer: [10 + @sizeOf(u32) + 1]u8 = undefined; + var uid_buffer: [32]u8 = undefined; // No UID can be larger than this const uid_str = try std.fmt.bufPrintZ(&uid_buffer, "/run/user/{d}", .{uid}); _ = interop.stdlib.setenv("XDG_RUNTIME_DIR", uid_str, 0); From b35c055e7bb0c1c8b84a5b37b87d18f3edfa6e8d Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Sat, 2 Aug 2025 13:08:07 +0200 Subject: [PATCH 240/530] Fix clock string length issues (fixes #716) Co-authored-by: Plash Signed-off-by: AnErrupTion --- res/lang/ar.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 | 193 +++++++++++++++----------------------------- 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 | 1 + res/lang/tr.ini | 1 + res/lang/uk.ini | 1 + res/lang/zh_CN.ini | 1 + src/config/Lang.zig | 1 + src/interop.zig | 4 +- src/main.zig | 21 ++--- 22 files changed, 99 insertions(+), 138 deletions(-) diff --git a/res/lang/ar.ini b/res/lang/ar.ini index b06f253..eaa78b3 100644 --- a/res/lang/ar.ini +++ b/res/lang/ar.ini @@ -7,6 +7,7 @@ err_alloc = فشل في تخصيص الذاكرة err_bounds = out-of-bounds index err_brightness_change = فشل في تغيير سطوع الشاشة err_chdir = فشل في فتح مجلد المنزل + err_config = فشل في تفسير ملف الإعدادات err_dgn_oob = رسالة سجل (Log) err_domain = اسم نطاق غير صالح diff --git a/res/lang/cat.ini b/res/lang/cat.ini index d0ce33e..71eabd6 100644 --- a/res/lang/cat.ini +++ b/res/lang/cat.ini @@ -8,6 +8,7 @@ err_bounds = índex fora de límits err_brightness_change = error en canviar la brillantor err_chdir = error en obrir la carpeta home + err_dgn_oob = missatge de registre err_domain = domini invàlid diff --git a/res/lang/cs.ini b/res/lang/cs.ini index 7c87ea6..e91dfbd 100644 --- a/res/lang/cs.ini +++ b/res/lang/cs.ini @@ -8,6 +8,7 @@ err_bounds = index je mimo hranice pole err_chdir = nelze otevřít domovský adresář + err_dgn_oob = zpráva protokolu err_domain = neplatná doména diff --git a/res/lang/de.ini b/res/lang/de.ini index 44bbc53..34c352e 100644 --- a/res/lang/de.ini +++ b/res/lang/de.ini @@ -7,6 +7,7 @@ err_alloc = Speicherzuweisung fehlgeschlagen err_bounds = Index ausserhalb des Bereichs err_brightness_change = Helligkeitsänderung fehlgeschlagen err_chdir = Fehler beim Oeffnen des Home-Ordners + err_config = Fehler beim Verarbeiten der Konfigurationsdatei err_dgn_oob = Diagnose-Nachricht err_domain = Ungueltige Domain diff --git a/res/lang/en.ini b/res/lang/en.ini index 9554658..9bcfeb0 100644 --- a/res/lang/en.ini +++ b/res/lang/en.ini @@ -7,6 +7,7 @@ err_alloc = failed memory allocation err_bounds = out-of-bounds index err_brightness_change = failed to change brightness err_chdir = failed to open home folder +err_clock_too_long = clock string too long err_config = unable to parse config file err_dgn_oob = log message err_domain = invalid domain diff --git a/res/lang/es.ini b/res/lang/es.ini index 2bd51a7..d4c79d5 100644 --- a/res/lang/es.ini +++ b/res/lang/es.ini @@ -8,6 +8,7 @@ err_bounds = índice fuera de límites err_chdir = error al abrir la carpeta home + err_dgn_oob = mensaje de registro err_domain = dominio inválido diff --git a/res/lang/fr.ini b/res/lang/fr.ini index e024820..9553985 100644 --- a/res/lang/fr.ini +++ b/res/lang/fr.ini @@ -7,6 +7,7 @@ err_alloc = échec d'allocation mémoire err_bounds = indice hors-limite err_brightness_change = échec du changement de luminosité err_chdir = échec de l'ouverture du répertoire home +err_clock_too_long = chaîne de formattage de l'horloge trop longue err_config = échec de lecture du fichier de configuration err_dgn_oob = message err_domain = domaine invalide diff --git a/res/lang/it.ini b/res/lang/it.ini index 3a7f5a8..27f6e46 100644 --- a/res/lang/it.ini +++ b/res/lang/it.ini @@ -8,6 +8,7 @@ err_bounds = indice fuori limite err_chdir = impossibile aprire home directory + err_dgn_oob = messaggio log err_domain = dominio non valido diff --git a/res/lang/ja_JP.ini b/res/lang/ja_JP.ini index c5f5596..1bb02fc 100644 --- a/res/lang/ja_JP.ini +++ b/res/lang/ja_JP.ini @@ -1,126 +1,67 @@ -authenticating=認証中... -brightness_down=明るさを下げる -brightness_up=明るさを上げる -capslock=CapsLock -err_alloc=メモリ割り当て失敗 -err_bounds=境界外インデックス -err_brightness_change=明るさの変更に失敗しました -err_chdir=ホームフォルダを開けませんでした -err_config=設定ファイルを解析できません -err_console_dev=コンソールへのアクセスに失敗しました -err_dgn_oob=ログメッセージ -err_domain=無効なドメイン -err_empty_password=空のパスワードは許可されていません -err_envlist=環境変数リストの取得に失敗しました -err_hostname=ホスト名の取得に失敗しました -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_tty_ctrl=TTY制御の転送に失敗しました -err_user_gid=ユーザーGIDの設定に失敗しました -err_user_init=ユーザーの初期化に失敗しました -err_user_uid=ユーザーUIDの設定に失敗しました -err_xauth=xauthコマンドの実行に失敗しました -err_xcb_conn=XCB接続に失敗しました -err_xsessions_dir=セッションフォルダが見つかりませんでした -err_xsessions_open=セッションフォルダを開けませんでした -insert=挿入 -login=ログイン -logout=ログアウト済み -no_x11_support=X11サポートはコンパイル時に無効化されています -normal=通常 -numlock=NumLock -other=その他 -password=パスワード -restart=再起動 -shell=シェル -shutdown=シャットダウン -sleep=スリープ -wayland=Wayland -x11=X11 -xinitrc=xinitrc -authenticating=認証中... -brightness_down=明るさを下げる -brightness_up=明るさを上げる -capslock=CapsLock -err_alloc=メモリ割り当て失敗 -err_bounds=境界外インデックス -err_brightness_change=明るさの変更に失敗しました -err_chdir=ホームフォルダを開けませんでした -err_config=設定ファイルを解析できません -err_console_dev=コンソールへのアクセスに失敗しました -err_dgn_oob=ログメッセージ -err_domain=無効なドメイン -err_empty_password=空のパスワードは許可されていません -err_envlist=環境変数リストの取得に失敗しました -err_hostname=ホスト名の取得に失敗しました -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_tty_ctrl=TTY制御の転送に失敗しました -err_user_gid=ユーザーGIDの設定に失敗しました -err_user_init=ユーザーの初期化に失敗しました -err_user_uid=ユーザーUIDの設定に失敗しました -err_xauth=xauthコマンドの実行に失敗しました -err_xcb_conn=XCB接続に失敗しました -err_xsessions_dir=セッションフォルダが見つかりませんでした -err_xsessions_open=セッションフォルダを開けませんでした -insert=挿入 -login=ログイン -logout=ログアウト済み -no_x11_support=X11サポートはコンパイル時に無効化されています -normal=通常 -numlock=NumLock -other=その他 -password=パスワード -restart=再起動 -shell=シェル -shutdown=シャットダウン -sleep=スリープ -wayland=Wayland -x11=X11 -xinitrc=xinitrc +authenticating = 認証中... +brightness_down = 明るさを下げる +brightness_up = 明るさを上げる +capslock = CapsLock + +err_alloc = メモリ割り当て失敗 +err_bounds = 境界外インデックス +err_brightness_change = 明るさの変更に失敗しました +err_chdir = ホームフォルダを開けませんでした + +err_config = 設定ファイルを解析できません +err_dgn_oob = ログメッセージ +err_domain = 無効なドメイン +err_empty_password = 空のパスワードは許可されていません +err_envlist = 環境変数リストの取得に失敗しました +err_hostname = ホスト名の取得に失敗しました + +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_tty_ctrl = TTY制御の転送に失敗しました + +err_user_gid = ユーザーGIDの設定に失敗しました +err_user_init = ユーザーの初期化に失敗しました +err_user_uid = ユーザーUIDの設定に失敗しました +err_xauth = xauthコマンドの実行に失敗しました +err_xcb_conn = XCB接続に失敗しました +err_xsessions_dir = セッションフォルダが見つかりませんでした +err_xsessions_open = セッションフォルダを開けませんでした +insert = 挿入 +login = ログイン +logout = ログアウト済み +no_x11_support = X11サポートはコンパイル時に無効化されています +normal = 通常 +numlock = NumLock +other = その他 +password = パスワード +restart = 再起動 +shell = シェル +shutdown = シャットダウン +sleep = スリープ +wayland = Wayland +x11 = X11 +xinitrc = xinitrc diff --git a/res/lang/pl.ini b/res/lang/pl.ini index b3eee3d..e94d8c0 100644 --- a/res/lang/pl.ini +++ b/res/lang/pl.ini @@ -7,6 +7,7 @@ err_alloc = nieudana alokacja pamięci 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_config = nie można przetworzyć pliku konfiguracyjnego err_dgn_oob = wiadomość loga err_domain = niepoprawna domena diff --git a/res/lang/pt.ini b/res/lang/pt.ini index 587d3b4..ef566de 100644 --- a/res/lang/pt.ini +++ b/res/lang/pt.ini @@ -8,6 +8,7 @@ err_bounds = índice fora de limites err_chdir = erro ao abrir a pasta home + err_dgn_oob = mensagem de registo err_domain = domínio inválido diff --git a/res/lang/pt_BR.ini b/res/lang/pt_BR.ini index 2485522..a15f0c9 100644 --- a/res/lang/pt_BR.ini +++ b/res/lang/pt_BR.ini @@ -8,6 +8,7 @@ err_bounds = índice fora de limites err_chdir = não foi possível abrir o diretório home + err_dgn_oob = mensagem de log err_domain = domínio inválido diff --git a/res/lang/ro.ini b/res/lang/ro.ini index 74c0461..53a7432 100644 --- a/res/lang/ro.ini +++ b/res/lang/ro.ini @@ -18,6 +18,7 @@ capslock = capslock + err_pam_abort = tranzacţie pam anulată err_pam_acct_expired = cont expirat err_pam_auth = eroare de autentificare diff --git a/res/lang/ru.ini b/res/lang/ru.ini index 131e602..07d164c 100644 --- a/res/lang/ru.ini +++ b/res/lang/ru.ini @@ -7,6 +7,7 @@ err_alloc = не удалось выделить память err_bounds = за пределами индекса err_brightness_change = не удалось изменить яркость err_chdir = не удалось открыть домашнюю папку + err_config = не удалось разобрать файл конфигурации err_dgn_oob = отладочное сообщение (log) err_domain = неверный домен diff --git a/res/lang/sr.ini b/res/lang/sr.ini index c63fe28..fb8c26c 100644 --- a/res/lang/sr.ini +++ b/res/lang/sr.ini @@ -8,6 +8,7 @@ err_bounds = izvan granica indeksa err_chdir = neuspijesno otvaranje home foldera + err_dgn_oob = log poruka err_domain = nevazeci domen diff --git a/res/lang/sv.ini b/res/lang/sv.ini index 17359cc..7aaff8d 100644 --- a/res/lang/sv.ini +++ b/res/lang/sv.ini @@ -8,6 +8,7 @@ err_bounds = utanför banan index err_chdir = misslyckades att öppna hemkatalog + err_dgn_oob = loggmeddelande err_domain = okänd domän diff --git a/res/lang/tr.ini b/res/lang/tr.ini index 535015c..aee7fbd 100644 --- a/res/lang/tr.ini +++ b/res/lang/tr.ini @@ -8,6 +8,7 @@ err_bounds = sinirlarin disinda dizin err_chdir = ev klasoru acilamadi + err_dgn_oob = log mesaji err_domain = gecersiz etki alani diff --git a/res/lang/uk.ini b/res/lang/uk.ini index 9e6de2d..16d740c 100644 --- a/res/lang/uk.ini +++ b/res/lang/uk.ini @@ -8,6 +8,7 @@ err_bounds = поза межами індексу err_chdir = не вдалося відкрити домашній каталог + err_dgn_oob = повідомлення журналу (log) err_domain = недійсний домен diff --git a/res/lang/zh_CN.ini b/res/lang/zh_CN.ini index 9afdc66..971cb1c 100644 --- a/res/lang/zh_CN.ini +++ b/res/lang/zh_CN.ini @@ -8,6 +8,7 @@ err_bounds = 索引越界 err_chdir = 无法打开home文件夹 + err_dgn_oob = 日志消息 err_domain = 无效的域 diff --git a/src/config/Lang.zig b/src/config/Lang.zig index 7175e0f..0649619 100644 --- a/src/config/Lang.zig +++ b/src/config/Lang.zig @@ -12,6 +12,7 @@ err_alloc: []const u8 = "failed memory allocation", err_bounds: []const u8 = "out-of-bounds index", err_brightness_change: []const u8 = "failed to change brightness", err_chdir: []const u8 = "failed to open home folder", +err_clock_too_long: []const u8 = "clock string too long", err_config: []const u8 = "unable to parse config file", err_dgn_oob: []const u8 = "log message", err_domain: []const u8 = "invalid domain", diff --git a/src/interop.zig b/src/interop.zig index 8540c21..68d4332 100644 --- a/src/interop.zig +++ b/src/interop.zig @@ -65,12 +65,10 @@ const set_led_state = if (builtin.os.tag.isBSD()) kbio.KDSETLED else kd.KDSKBLED const numlock_led = if (builtin.os.tag.isBSD()) kbio.LED_NUM else kd.K_NUMLOCK; const capslock_led = if (builtin.os.tag.isBSD()) kbio.LED_CAP else kd.K_CAPSLOCK; -pub fn timeAsString(buf: [:0]u8, format: [:0]const u8) ![]u8 { +pub fn timeAsString(buf: [:0]u8, format: [:0]const u8) []u8 { const timer = std.time.timestamp(); const tm_info = time.localtime(&timer); - const len = time.strftime(buf, buf.len, format, tm_info); - if (len < 0) return error.CannotGetFormattedTime; return buf[0..len]; } diff --git a/src/main.zig b/src/main.zig index 0cc2c6e..ddd6cfb 100644 --- a/src/main.zig +++ b/src/main.zig @@ -106,6 +106,7 @@ pub fn main() !void { var save: Save = undefined; var config_load_failed = false; var can_get_lock_state = true; + var can_draw_clock = true; if (res.args.help != 0) { try clap.help(stderr, clap.Help, ¶ms, .{}); @@ -465,15 +466,13 @@ pub fn main() !void { length += ly_top_str.len + 1; } - if (config.bigclock != .none and buffer.box_height + (bigclock.HEIGHT + 2) * 2 < buffer.height) draw_big_clock: { + if (config.bigclock != .none and buffer.box_height + (bigclock.HEIGHT + 2) * 2 < buffer.height) { const format = "%H:%M"; 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; var clock_buf: [format.len + 1:0]u8 = undefined; - const clock_str = interop.timeAsString(&clock_buf, format) catch { - break :draw_big_clock; - }; + const clock_str = interop.timeAsString(&clock_buf, format); for (clock_str, 0..) |c, i| { const clock_cell = bigclock.clockCell(animate, c, buffer.fg, buffer.bg, config.bigclock); @@ -503,12 +502,16 @@ pub fn main() !void { } if (config.clock) |clock| draw_clock: { - var clock_buf: [32:0]u8 = undefined; - const clock_str = interop.timeAsString(&clock_buf, clock) catch { - break :draw_clock; - }; + if (!can_draw_clock) break :draw_clock; - if (clock_str.len == 0) return error.FormattedTimeEmpty; + 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; + break :draw_clock; + } buffer.drawLabel(clock_str, buffer.width - @min(buffer.width, clock_str.len), 0); } From 8377f145095dc495f2fd68535387549dae703927 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Sat, 2 Aug 2025 19:45:40 +0200 Subject: [PATCH 241/530] Exclude unsupported distributions in packaging status Signed-off-by: AnErrupTion --- readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/readme.md b/readme.md index 98595a6..aa0da4a 100644 --- a/readme.md +++ b/readme.md @@ -23,7 +23,7 @@ Join us on Matrix over at [#ly:envs.net](https://matrix.to/#/#ly:envs.net)! ## Packaging status -[![Packaging status](https://repology.org/badge/vertical-allrepos/ly.svg)](https://repology.org/project/ly/versions) +[![Packaging status](https://repology.org/badge/vertical-allrepos/ly.svg?exclude_unsupported=1)](https://repology.org/project/ly/versions) ### Debian From c37aa6957a527cbb8f91ffaf0bfa4e20907149a4 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Sat, 2 Aug 2025 20:26:31 +0200 Subject: [PATCH 242/530] Improve README Signed-off-by: AnErrupTion --- readme.md | 229 +++++++++++++++++++----------------------------------- 1 file changed, 79 insertions(+), 150 deletions(-) diff --git a/readme.md b/readme.md index aa0da4a..c6ca77d 100644 --- a/readme.md +++ b/readme.md @@ -1,17 +1,19 @@ -# Ly - a TUI display manager +# The Ly display manager ![Ly screenshot](.github/screenshot.png "Ly screenshot") -Ly is a lightweight TUI (ncurses-like) display manager for Linux and BSD. +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)! -**Note**: Development happens on [Codeberg](https://codeberg.org/AnErrupTion/ly) with a mirror on [GitHub](https://github.com/fairyglade/ly). +**Note**: Development happens on [Codeberg](https://codeberg.org/AnErrupTion/ly) +with a mirror on [GitHub](https://github.com/fairyglade/ly). ## Dependencies - Compile-time: - - zig 0.14.0 + - zig 0.14.x - libc - pam - xcb (optional, required by default; needed for X11 support) @@ -21,14 +23,10 @@ Join us on Matrix over at [#ly:envs.net](https://matrix.to/#/#ly:envs.net)! - shutdown - brightnessctl -## Packaging status - -[![Packaging status](https://repology.org/badge/vertical-allrepos/ly.svg?exclude_unsupported=1)](https://repology.org/project/ly/versions) - ### Debian ``` -# apt install build-essential libpam0g-dev libxcb-xkb-dev +# apt install build-essential libpam0g-dev libxcb-xkb-dev xauth xserver-xorg brightnessctl ``` ### Fedora @@ -37,136 +35,102 @@ Join us on Matrix over at [#ly:envs.net](https://matrix.to/#/#ly:envs.net)! 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 +# dnf install kernel-devel pam-devel libxcb-devel zig xorg-x11-xauth xorg-x11-server brightnessctl ``` +## Packaging status + +[![Packaging status](https://repology.org/badge/vertical-allrepos/ly.svg?exclude_unsupported=1)](https://repology.org/project/ly/versions) + ## Support -The following desktop environments were tested with success: +Ly has been tested with a wide variety of desktop environments and window +managers, all of which you can find in the sections below: -[Wayland Environments](#supported-wayland-environments) +[Wayland environments](#supported-wayland-environments) -[X11 Environments](#supported-x11-environments) +[X11 environments](#supported-x11-environments) -Ly should work with any X desktop environment, and provides -basic wayland support (sway works very well, for example). +## Manually building -## systemd? - -Unlike what you may have heard, Ly does not require `systemd`, -and was even specifically designed not to depend on `logind`. -You should be able to make it work easily with a better init, -changing the source code won't be necessary :) - -## Cloning and Compiling - -Clone the repository +The procedure for manually building Ly is pretty standard: ``` $ git clone https://codeberg.org/AnErrupTion/ly -``` - -Change the directory to ly - -``` $ cd ly -``` - -Compile - -``` $ zig build ``` -Test in the configured tty (tty2 by default) -or a terminal emulator (but authentication won't work) +After building, you can (optionally) test Ly in a terminal emulator, although +authentication will **not** work: ``` $ zig build run ``` -**Important**: Running Ly in a terminal emulator as root is _not_ recommended. 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. -Install Ly for systemd-based systems (the default) +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. + +### systemd + +Now, you can install Ly on your system: ``` -# zig build installexe +# zig build installexe -Dinit_system=systemd ``` -Instead of DISPLAY_MANAGER you need to add your DM: +**Note**: The `init_system` parameter is optional and defaults to `systemd`. -- gdm.service -- sddm.service -- lightdm.service +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 DISPLAY_MANAGER +# systemctl disable lightdm.service ``` -Enable the service +Then, similarly to the previous command, you need to enable the Ly service: ``` # systemctl enable ly.service ``` -If you need to switch between ttys after Ly's start you also have to -disable getty on Ly's tty to prevent "login" from spawning on top of it +**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 (the default TTY on which Ly spawns), you need to +execute the following command: ``` # systemctl disable getty@tty2.service ``` +You can change the TTY Ly will run on by editing the `tty` option in the configuration file. + ### OpenRC -**NOTE 1**: On Gentoo, Ly will disable the `display-manager-init` service in order to run. - -Clone, compile and test. - -Install Ly and the provided OpenRC service - ``` # zig build installexe -Dinit_system=openrc -``` - -Enable the service - -``` +# rc-update del lightdm # rc-update add ly -``` - -You can edit which tty Ly will start on by editing the `tty` option in the configuration file. - -If you choose a tty that already has a login/getty running (has a basic login prompt), -then you have to disable getty, so it doesn't respawn on top of ly - -``` # rc-update del agetty.tty2 ``` -**NOTE 2**: To avoid a console spawning on top on Ly, comment out the appropriate line from /etc/inittab (default is 2). +**Note**: On Gentoo specifically, you also **must** comment out the appropriate +line for the TTY in /etc/inittab. ### runit ``` # zig build installexe -Dinit_system=runit +# rm /var/service/lightdm # ln -s /etc/sv/ly /var/service/ -``` - -By default, ly will run on tty2. To change the tty it must be set in `/etc/ly/config.ini` - -You should as well disable your existing display manager service if needed, e.g.: - -``` -# rm /var/service/lxdm -``` - -The agetty service for the tty console where you are running ly should be disabled. -For instance, if you are running ly on tty2 (that's the default, check your `/etc/ly/config.ini`) -you should disable the agetty-tty2 service like this: - -``` # rm /var/service/agetty-tty2 ``` @@ -174,95 +138,60 @@ you should disable the agetty-tty2 service like this: ``` # zig build installexe -Dinit_system=s6 -``` - -Then, edit `/etc/s6/config/ttyX.conf` and set `SPAWN="no"`, where X is the TTY ID (e.g. `2`). - -Finally, enable the service: - -``` +# s6-rc -d change lightdm # s6-service add default ly-srv # s6-db-reload # s6-rc -u change ly-srv ``` +To disable TTY 2, edit `/etc/s6/config/tty2.conf` and set `SPAWN="no"`. + ### dinit ``` # zig build installexe -Dinit_system=dinit +# dinitctl disable lightdm # dinitctl enable ly ``` -In addition to the steps above, you will also have to keep a TTY free within `/etc/dinit.d/config/console.conf`. - -To do that, change `ACTIVE_CONSOLES` so that the tty that ly should use in `/etc/ly/config.ini` is free. +To disable TTY 2, go to `/etc/dinit.d/config/console.conf` and modify +`ACTIVE_CONSOLES`. ### Updating -You can also install Ly without overrding the current configuration file. That's 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. - -## Arch Linux Installation - -You can install ly from the [`[extra]` repos](https://archlinux.org/packages/extra/x86_64/ly/): - -``` -# pacman -S ly -``` - -## Gentoo Installation - -You can install ly from the GURU repository: - -Note: If the package is masked, you may need to unmask it using ~amd64 keyword: - -```bash -# echo 'x11-misc/ly ~amd64' >> /etc/portage/package.accept_keywords -``` - -1. Enable the GURU repository: - -```bash -# eselect repository enable guru -``` - -2. Sync the GURU repository: - -```bash -# emaint sync -r guru -``` - -3. Install ly from source: - -```bash -# emerge --ask x11-misc/ly -``` +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 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 and down arrow keys to change the current field, and the -left and right arrow keys to change the target desktop environment -while on the desktop field (above the login field). +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. -## .xinitrc +## A note on .xinitrc -If your .xinitrc 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. -On Arch Linux, the example .xinitrc (/etc/X11/xinit/xinitrc) starts like this: +A typical shebang for a shell script looks like this: ``` #!/bin/sh @@ -272,10 +201,10 @@ On Arch Linux, the example .xinitrc (/etc/X11/xinit/xinitrc) starts like this: - 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 if X doesn't start, as it can interfere +- 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 +## Supported Wayland environments - budgie - cosmic @@ -290,7 +219,7 @@ On Arch Linux, the example .xinitrc (/etc/X11/xinit/xinitrc) starts like this: - sway - weston -## Supported X11 Environments +## Supported X11 environments - awesome - bspwm @@ -311,7 +240,7 @@ On Arch Linux, the example .xinitrc (/etc/X11/xinit/xinitrc) starts like this: - xfce - xmonad -## Additional Information +## 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. From 71c694e575b855126099a854c5a46f8885f68d41 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Sat, 2 Aug 2025 22:22:18 +0200 Subject: [PATCH 243/530] Correct mention of TTY modification in README Signed-off-by: AnErrupTion --- readme.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/readme.md b/readme.md index c6ca77d..f41e0e0 100644 --- a/readme.md +++ b/readme.md @@ -111,7 +111,9 @@ execute the following command: # systemctl disable getty@tty2.service ``` -You can change the TTY Ly will run on by editing the `tty` option in the configuration file. +You can change the TTY Ly will run on by editing the `tty` option in the +configuration file **and** change which TTY is used in the corresponding +service file.. ### OpenRC From ef6402979595335f6f3251ec7fb415004e8c2610 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Sat, 2 Aug 2025 22:57:47 +0200 Subject: [PATCH 244/530] Only clear TTY under certain circumstances Signed-off-by: AnErrupTion --- src/main.zig | 32 +++++++++++++++++++------------- 1 file changed, 19 insertions(+), 13 deletions(-) diff --git a/src/main.zig b/src/main.zig index ddd6cfb..1fe4d44 100644 --- a/src/main.zig +++ b/src/main.zig @@ -230,6 +230,9 @@ pub fn main() !void { _ = termbox.tb_set_output_mode(termbox.TB_OUTPUT_TRUECOLOR); _ = 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); @@ -683,11 +686,9 @@ pub fn main() !void { } }, termbox.TB_KEY_CTRL_C => run = false, - termbox.TB_KEY_CTRL_U => { - if (active_input == .password) { - password.clear(); - update = true; - } + termbox.TB_KEY_CTRL_U => if (active_input == .password) { + password.clear(); + update = true; }, termbox.TB_KEY_CTRL_K, termbox.TB_KEY_ARROW_UP => { active_input = switch (active_input) { @@ -721,7 +722,6 @@ pub fn main() !void { .login => .session, .password => .login, }; - update = true; }, termbox.TB_KEY_ENTER => authenticate: { @@ -819,15 +819,14 @@ pub fn main() !void { try info_line.addMessage(lang.logout, config.bg, config.fg); } - // Clear the TTY because termbox2 doesn't properly do it - const capability = termbox.global.caps[termbox.TB_CAP_CLEAR_SCREEN]; - const capability_slice = capability[0..std.mem.len(capability)]; - _ = try std.posix.write(termbox.global.ttyfd, capability_slice); - try std.posix.tcsetattr(std.posix.STDIN_FILENO, .FLUSH, tb_termios); - if (auth_fails < config.auth_fails) _ = termbox.tb_clear(); - update = true; + if (auth_fails < config.auth_fails) { + _ = termbox.tb_clear(); + try ttyClearScreen(); + + update = true; + } // Restore the cursor _ = termbox.tb_set_cursor(0, 0); @@ -877,6 +876,13 @@ pub fn main() !void { } } +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 = capability[0..std.mem.len(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, From 4fbbb6f0f2bc9cb8db55ba2681de22792d738018 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Sat, 2 Aug 2025 22:59:34 +0200 Subject: [PATCH 245/530] Reduce nesting a bit Signed-off-by: AnErrupTion --- src/main.zig | 291 ++++++++++++++++++++++++++------------------------- 1 file changed, 147 insertions(+), 144 deletions(-) diff --git a/src/main.zig b/src/main.zig index 1fe4d44..f70f93b 100644 --- a/src/main.zig +++ b/src/main.zig @@ -457,150 +457,7 @@ pub fn main() !void { if (update) { // If the user entered a wrong password 10 times in a row, play a cascade animation, else update normally - if (auth_fails < config.auth_fails) { - _ = termbox.tb_clear(); - - var length: usize = 0; - - if (!animation_timed_out) animation.draw(); - - if (!config.hide_version_string) { - buffer.drawLabel(ly_top_str, length, 0); - length += ly_top_str.len + 1; - } - - if (config.bigclock != .none and buffer.box_height + (bigclock.HEIGHT + 2) * 2 < buffer.height) { - const format = "%H:%M"; - 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; - - var clock_buf: [format.len + 1:0]u8 = undefined; - const clock_str = interop.timeAsString(&clock_buf, format); - - for (clock_str, 0..) |c, i| { - const clock_cell = 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 { - try info_line.addMessage(lang.err_alloc, config.error_bg, config.error_fg); - }, - } - - 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; - break :draw_clock; - } - - buffer.drawLabel(clock_str, buffer.width - @min(buffer.width, clock_str.len), 0); - } - - 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, 0); - length += config.shutdown_key.len + 1; - buffer.drawLabel(" ", length - 1, 0); - - buffer.drawLabel(lang.shutdown, length, 0); - length += shutdown_len + 1; - - buffer.drawLabel(config.restart_key, length, 0); - length += config.restart_key.len + 1; - buffer.drawLabel(" ", length - 1, 0); - - buffer.drawLabel(lang.restart, length, 0); - length += restart_len + 1; - - if (config.sleep_cmd != null) { - buffer.drawLabel(config.sleep_key, length, 0); - length += config.sleep_key.len + 1; - buffer.drawLabel(" ", length - 1, 0); - - buffer.drawLabel(lang.sleep, length, 0); - length += sleep_len + 1; - } - - if (config.brightness_down_key) |key| { - buffer.drawLabel(key, length, 0); - length += key.len + 1; - buffer.drawLabel(" ", length - 1, 0); - - buffer.drawLabel(lang.brightness_down, length, 0); - length += brightness_down_len + 1; - } - - if (config.brightness_up_key) |key| { - buffer.drawLabel(key, length, 0); - length += key.len + 1; - buffer.drawLabel(" ", length - 1, 0); - - buffer.drawLabel(lang.brightness_up, length, 0); - 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 (can_get_lock_state) draw_lock_state: { - const lock_state = interop.getLockState() catch { - try info_line.addMessage(lang.err_lock_state, config.error_bg, config.error_fg); - can_get_lock_state = false; - break :draw_lock_state; - }; - - var lock_state_x = buffer.width - @min(buffer.width, lang.numlock.len); - const lock_state_y: usize = if (config.clock != null) 1 else 0; - - 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(); - } else { + if (auth_fails >= config.auth_fails) { std.Thread.sleep(std.time.ns_per_ms * 10); update = buffer.cascade(); @@ -608,8 +465,154 @@ pub fn main() !void { std.Thread.sleep(std.time.ns_per_s * 7); auth_fails = 0; } + + _ = termbox.tb_present(); + continue; } + _ = termbox.tb_clear(); + + var length: usize = 0; + + if (!animation_timed_out) animation.draw(); + + if (!config.hide_version_string) { + buffer.drawLabel(ly_top_str, length, 0); + length += ly_top_str.len + 1; + } + + if (config.bigclock != .none and buffer.box_height + (bigclock.HEIGHT + 2) * 2 < buffer.height) { + const format = "%H:%M"; + 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; + + var clock_buf: [format.len + 1:0]u8 = undefined; + const clock_str = interop.timeAsString(&clock_buf, format); + + for (clock_str, 0..) |c, i| { + const clock_cell = 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 { + try info_line.addMessage(lang.err_alloc, config.error_bg, config.error_fg); + }, + } + + 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; + break :draw_clock; + } + + buffer.drawLabel(clock_str, buffer.width - @min(buffer.width, clock_str.len), 0); + } + + 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, 0); + length += config.shutdown_key.len + 1; + buffer.drawLabel(" ", length - 1, 0); + + buffer.drawLabel(lang.shutdown, length, 0); + length += shutdown_len + 1; + + buffer.drawLabel(config.restart_key, length, 0); + length += config.restart_key.len + 1; + buffer.drawLabel(" ", length - 1, 0); + + buffer.drawLabel(lang.restart, length, 0); + length += restart_len + 1; + + if (config.sleep_cmd != null) { + buffer.drawLabel(config.sleep_key, length, 0); + length += config.sleep_key.len + 1; + buffer.drawLabel(" ", length - 1, 0); + + buffer.drawLabel(lang.sleep, length, 0); + length += sleep_len + 1; + } + + if (config.brightness_down_key) |key| { + buffer.drawLabel(key, length, 0); + length += key.len + 1; + buffer.drawLabel(" ", length - 1, 0); + + buffer.drawLabel(lang.brightness_down, length, 0); + length += brightness_down_len + 1; + } + + if (config.brightness_up_key) |key| { + buffer.drawLabel(key, length, 0); + length += key.len + 1; + buffer.drawLabel(" ", length - 1, 0); + + buffer.drawLabel(lang.brightness_up, length, 0); + 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 (can_get_lock_state) draw_lock_state: { + const lock_state = interop.getLockState() catch { + try info_line.addMessage(lang.err_lock_state, config.error_bg, config.error_fg); + can_get_lock_state = false; + break :draw_lock_state; + }; + + var lock_state_x = buffer.width - @min(buffer.width, lang.numlock.len); + const lock_state_y: usize = if (config.clock != null) 1 else 0; + + 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(); + _ = termbox.tb_present(); } From b382d7496980424eedab10c98adaa321ab30f7a0 Mon Sep 17 00:00:00 2001 From: RadsammyT Date: Sun, 3 Aug 2025 11:16:04 +0200 Subject: [PATCH 246/530] bigclock: add 12-hour & seconds support (#805) Added P,A,M characters to bigclock and added 12hr and seconds support to bigclock via `bigclock_12hr` and `bigclock_seconds` in the config. ![image](/attachments/e95accff-4822-4801-8159-94411a6c644f) Image has bigclock_12hr and bigclock_seconds enabled. Farsi characters for P,A,M are blank since I don't know what it would look like in their language. (should i have just used the english characters as a placeholder?) Reviewed-on: https://codeberg.org/fairyglade/ly/pulls/805 Reviewed-by: AnErrupTion Co-authored-by: RadsammyT Co-committed-by: RadsammyT --- res/config.ini | 6 +++++- src/bigclock.zig | 3 +++ src/bigclock/Lang.zig | 3 +++ src/bigclock/en.zig | 23 ++++++++++++++++++++++- src/bigclock/fa.zig | 23 ++++++++++++++++++++++- src/config/Config.zig | 2 ++ src/main.zig | 12 ++++++++++-- 7 files changed, 67 insertions(+), 5 deletions(-) diff --git a/res/config.ini b/res/config.ini index 02a242e..2522b40 100644 --- a/res/config.ini +++ b/res/config.ini @@ -48,7 +48,11 @@ bg = 0x00000000 # none -> Disabled (default) # en -> English # fa -> Farsi -bigclock = none +bigclock = en +# Set bigclock to 12-hour notation. +bigclock_12hr = false +# Set bigclock to show the seconds. +bigclock_seconds = false # Blank main box background # Setting to false will make it transparent diff --git a/src/bigclock.zig b/src/bigclock.zig index 4fae3a4..a1c860a 100644 --- a/src/bigclock.zig +++ b/src/bigclock.zig @@ -51,6 +51,9 @@ fn toBigNumber(char: u8, bigclock: Bigclock) [SIZE]u21 { '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 index 6ba8cf1..d11aac9 100644 --- a/src/bigclock/Lang.zig +++ b/src/bigclock/Lang.zig @@ -21,5 +21,8 @@ pub const LocaleChars = struct { 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/bigclock/en.zig b/src/bigclock/en.zig index 868656a..42a45f9 100644 --- a/src/bigclock/en.zig +++ b/src/bigclock/en.zig @@ -90,5 +90,26 @@ pub const locale_chars = LocaleChars{ O,O,O,O,O, O,O,O,O,O, }, + .P = [_]u21{ + X,X,X,X,X, + X,X,O,X,X, + X,X,X,X,X, + X,X,O,O,O, + X,X,O,O,O, + }, + .A = [_]u21{ + X,X,X,X,X, + X,X,O,X,X, + X,X,X,X,X, + X,X,O,X,X, + X,X,O,X,X, + }, + .M = [_]u21{ + X,X,X,X,X, + X,O,X,O,X, + X,O,X,O,X, + X,O,O,O,X, + X,O,O,O,X, + }, }; -// zig fmt: on \ No newline at end of file +// zig fmt: on diff --git a/src/bigclock/fa.zig b/src/bigclock/fa.zig index 63a897a..acfde42 100644 --- a/src/bigclock/fa.zig +++ b/src/bigclock/fa.zig @@ -76,6 +76,27 @@ pub const locale_chars = LocaleChars{ O,O,O,X,O, O,O,O,X,O, }, + .P = [_]u21{ + O,O,O,O,O, + O,O,O,O,O, + O,O,O,O,O, + O,O,O,O,O, + O,O,O,O,O, + }, + .A = [_]u21{ + O,O,O,O,O, + O,O,O,O,O, + O,O,O,O,O, + O,O,O,O,O, + O,O,O,O,O, + }, + .M = [_]u21{ + O,O,O,O,O, + O,O,O,O,O, + O,O,O,O,O, + O,O,O,O,O, + O,O,O,O,O, + }, .S = [_]u21{ O,O,O,O,O, O,O,X,O,O, @@ -91,4 +112,4 @@ pub const locale_chars = LocaleChars{ O,O,O,O,O, }, }; -// zig fmt: on \ No newline at end of file +// zig fmt: on diff --git a/src/config/Config.zig b/src/config/Config.zig index 6a8ba18..2399898 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -13,6 +13,8 @@ asterisk: ?u32 = '*', auth_fails: u64 = 10, bg: u32 = 0x00000000, bigclock: Bigclock = .none, +bigclock_12hr: bool = false, +bigclock_seconds: bool = false, blank_box: bool = true, border_fg: u32 = 0x00FFFFFF, box_title: ?[]const u8 = null, diff --git a/src/main.zig b/src/main.zig index f70f93b..e288e0a 100644 --- a/src/main.zig +++ b/src/main.zig @@ -482,11 +482,19 @@ pub fn main() !void { } if (config.bigclock != .none and buffer.box_height + (bigclock.HEIGHT + 2) * 2 < buffer.height) { - const format = "%H:%M"; + 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: [:0]const u8 = 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; - var clock_buf: [format.len + 1:0]u8 = undefined; const clock_str = interop.timeAsString(&clock_buf, format); for (clock_str, 0..) |c, i| { From bd2d1142b2581262d0e6151b5e6769d5646e1577 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Sun, 3 Aug 2025 13:05:28 +0200 Subject: [PATCH 247/530] Don't enable bigclock by default Signed-off-by: AnErrupTion --- res/config.ini | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/res/config.ini b/res/config.ini index 2522b40..eef5f9f 100644 --- a/res/config.ini +++ b/res/config.ini @@ -48,9 +48,11 @@ bg = 0x00000000 # none -> Disabled (default) # en -> English # fa -> Farsi -bigclock = en +bigclock = none + # Set bigclock to 12-hour notation. -bigclock_12hr = false +bigclock_12hr = false + # Set bigclock to show the seconds. bigclock_seconds = false From dee055748c440e3c072e180360ad5a2b7b65082c Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Sun, 3 Aug 2025 13:47:53 +0200 Subject: [PATCH 248/530] Format code properly Signed-off-by: AnErrupTion --- src/main.zig | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/src/main.zig b/src/main.zig index e288e0a..28b9e9a 100644 --- a/src/main.zig +++ b/src/main.zig @@ -482,16 +482,15 @@ pub fn main() !void { } if (config.bigclock != .none and buffer.box_height + (bigclock.HEIGHT + 2) * 2 < buffer.height) { - var format_buf: [16:0]u8 = undefined; + 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: [:0]const u8 = 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 format: [:0]const u8 = 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; From e404d5bdb30973e431eeb9cecec3fdaa4e529efb Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Sun, 3 Aug 2025 14:55:50 +0200 Subject: [PATCH 249/530] Clean-up: std.posix.kill() returns nothing Signed-off-by: AnErrupTion --- src/auth.zig | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/auth.zig b/src/auth.zig index 370a2be..d1e3a0a 100644 --- a/src/auth.zig +++ b/src/auth.zig @@ -107,7 +107,7 @@ pub fn authenticate(options: AuthOptions, current_environment: Environment, logi { // 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.kill(child_pid, std.posix.SIG.TERM) catch break :cleanup; _ = std.posix.waitpid(child_pid, 0); } @@ -169,7 +169,7 @@ fn startSession( std.posix.chdirZ(pwd.pw_dir.?) catch return error.ChangeDirectoryFailed; // Signal to the session process to give up control on the TTY - _ = std.posix.kill(options.session_pid, std.posix.SIG.CHLD) catch return error.TtyControlTransferFailed; + std.posix.kill(options.session_pid, std.posix.SIG.CHLD) catch return error.TtyControlTransferFailed; // Execute what the user requested switch (current_environment.display_server) { From 3f891d7f0ddc375ecde9b71afeeaaa30f4e5b5b7 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Sun, 3 Aug 2025 14:56:16 +0200 Subject: [PATCH 250/530] Workaround for session process not exiting immediately Signed-off-by: AnErrupTion --- src/main.zig | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/main.zig b/src/main.zig index 28b9e9a..47d733d 100644 --- a/src/main.zig +++ b/src/main.zig @@ -806,6 +806,9 @@ pub fn main() !void { } _ = 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; } From c3d180c213e55ebf84e65613f6a431cc09c9b3e1 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Sun, 3 Aug 2025 15:59:47 +0200 Subject: [PATCH 251/530] Add basic general log file Signed-off-by: AnErrupTion --- .github/ISSUE_TEMPLATE/bug.yml | 4 +- res/config.ini | 3 ++ res/lang/ar.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/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 | 1 + res/lang/tr.ini | 1 + res/lang/uk.ini | 1 + res/lang/zh_CN.ini | 1 + src/config/Config.zig | 1 + src/config/Lang.zig | 1 + src/main.zig | 84 ++++++++++++++++++++++++++++------ 24 files changed, 96 insertions(+), 16 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug.yml b/.github/ISSUE_TEMPLATE/bug.yml index a94cfc9..2b8bb33 100644 --- a/.github/ISSUE_TEMPLATE/bug.yml +++ b/.github/ISSUE_TEMPLATE/bug.yml @@ -58,8 +58,8 @@ body: attributes: label: Relevant logs description: | - Please copy and paste any relevant logs, error messages or any other output. This will be automatically formatted into code, so no need for backticks. Screenshots are accepted if they make life easier for you. - If it exists, ncluding your session log (found at /var/log/ly-session.log unless modified) is a good idea. (But make sure it's relevant!) + Please copy and paste (or attach) any relevant logs, error messages or any other output. This will be automatically formatted into code, so no need for backticks. Screenshots are accepted if they make life easier for you. + Moreover, it is almost always a good idea to include your session log and your general log files (found at ~/ly-session.log and /var/log/ly.log respectively by default) as it usually contains relevant information about the problem. render: shell - type: textarea id: moreinfo diff --git a/res/config.ini b/res/config.ini index eef5f9f..17db32c 100644 --- a/res/config.ini +++ b/res/config.ini @@ -199,6 +199,9 @@ login_defs_path = /etc/login.defs # no need to add `exec "$@"` at the end logout_cmd = null +# General log file path +ly_log = /var/log/ly.log + # Main box horizontal margin margin_box_h = 2 diff --git a/res/lang/ar.ini b/res/lang/ar.ini index eaa78b3..3a0ae3b 100644 --- a/res/lang/ar.ini +++ b/res/lang/ar.ini @@ -15,6 +15,7 @@ err_empty_password = لا يُسمح بكلمة مرور فارغة err_envlist = فشل في جلب قائمة المتغيرات البيئية err_hostname = فشل في جلب اسم المضيف (Hostname) + err_mlock = فشل في تأمين ذاكرة كلمة المرور (mlock) err_null = مؤشر فارغ (Null pointer) err_numlock = فشل في ضبط Num Lock diff --git a/res/lang/cat.ini b/res/lang/cat.ini index 71eabd6..985cb55 100644 --- a/res/lang/cat.ini +++ b/res/lang/cat.ini @@ -15,6 +15,7 @@ err_domain = domini invàlid err_envlist = error en obtenir l'envlist err_hostname = error en obtenir el nom de l'amfitrió + err_mlock = error en bloquejar la memòria de clau err_null = punter nul err_numlock = error en establir el Bloq num diff --git a/res/lang/cs.ini b/res/lang/cs.ini index e91dfbd..2798e18 100644 --- a/res/lang/cs.ini +++ b/res/lang/cs.ini @@ -15,6 +15,7 @@ err_domain = neplatná doména err_hostname = nelze získat název hostitele + err_mlock = uzamčení paměti hesel selhalo err_null = nulový ukazatel diff --git a/res/lang/de.ini b/res/lang/de.ini index 34c352e..bf70ace 100644 --- a/res/lang/de.ini +++ b/res/lang/de.ini @@ -15,6 +15,7 @@ err_empty_password = Leeres Passwort nicht zugelassen err_envlist = Fehler beim Abrufen der Umgebungs-Variablen err_hostname = Abrufen des Hostnames fehlgeschlagen + err_mlock = Sperren des Passwortspeichers fehlgeschlagen err_null = Null Pointer err_numlock = Numlock konnte nicht aktiviert werden diff --git a/res/lang/en.ini b/res/lang/en.ini index 9bcfeb0..11219b5 100644 --- a/res/lang/en.ini +++ b/res/lang/en.ini @@ -15,6 +15,7 @@ err_empty_password = empty password not allowed err_envlist = failed to get envlist err_hostname = failed to get hostname err_lock_state = failed to get lock state +err_log = failed to open log file err_mlock = failed to lock password memory err_null = null pointer err_numlock = failed to set numlock diff --git a/res/lang/es.ini b/res/lang/es.ini index d4c79d5..9676c0e 100644 --- a/res/lang/es.ini +++ b/res/lang/es.ini @@ -15,6 +15,7 @@ err_domain = dominio inválido err_hostname = error al obtener el nombre de host + err_mlock = error al bloquear la contraseña de memoria err_null = puntero nulo diff --git a/res/lang/fr.ini b/res/lang/fr.ini index 9553985..4ee547e 100644 --- a/res/lang/fr.ini +++ b/res/lang/fr.ini @@ -15,6 +15,7 @@ err_empty_password = mot de passe vide non autorisé err_envlist = échec de lecture de la liste d'environnement err_hostname = échec de lecture du nom d'hôte err_lock_state = échec de lecture de l'état de verrouillage +err_log = échec de l'ouverture du fichier de journal err_mlock = échec du verrouillage mémoire err_null = pointeur null err_numlock = échec de modification du verr.num diff --git a/res/lang/it.ini b/res/lang/it.ini index 27f6e46..ada3999 100644 --- a/res/lang/it.ini +++ b/res/lang/it.ini @@ -15,6 +15,7 @@ err_domain = dominio non valido err_hostname = impossibile ottenere hostname + err_mlock = impossibile ottenere lock per la password in memoria err_null = puntatore nullo diff --git a/res/lang/ja_JP.ini b/res/lang/ja_JP.ini index 1bb02fc..27f6083 100644 --- a/res/lang/ja_JP.ini +++ b/res/lang/ja_JP.ini @@ -15,6 +15,7 @@ err_empty_password = 空のパスワードは許可されていません err_envlist = 環境変数リストの取得に失敗しました err_hostname = ホスト名の取得に失敗しました + err_mlock = パスワードメモリのロックに失敗しました err_null = ヌルポインタ err_numlock = NumLockの設定に失敗しました diff --git a/res/lang/pl.ini b/res/lang/pl.ini index e94d8c0..a35a38e 100644 --- a/res/lang/pl.ini +++ b/res/lang/pl.ini @@ -15,6 +15,7 @@ err_empty_password = puste hasło jest niedozwolone err_envlist = nie udało się pobrać listy zmiennych środowiskowych err_hostname = nie udało się uzyskać nazwy hosta + err_mlock = nie udało się zablokować pamięci haseł err_null = pusty wskaźnik err_numlock = nie udało się ustawić numlock diff --git a/res/lang/pt.ini b/res/lang/pt.ini index ef566de..a94257e 100644 --- a/res/lang/pt.ini +++ b/res/lang/pt.ini @@ -15,6 +15,7 @@ err_domain = domínio inválido err_hostname = erro ao obter o nome do host + err_mlock = erro de bloqueio de memória err_null = ponteiro nulo diff --git a/res/lang/pt_BR.ini b/res/lang/pt_BR.ini index a15f0c9..66d990c 100644 --- a/res/lang/pt_BR.ini +++ b/res/lang/pt_BR.ini @@ -15,6 +15,7 @@ err_domain = domínio inválido err_hostname = não foi possível obter o nome do host + err_mlock = bloqueio da memória de senha malsucedido err_null = ponteiro nulo diff --git a/res/lang/ro.ini b/res/lang/ro.ini index 53a7432..008def0 100644 --- a/res/lang/ro.ini +++ b/res/lang/ro.ini @@ -19,6 +19,7 @@ capslock = capslock + err_pam_abort = tranzacţie pam anulată err_pam_acct_expired = cont expirat err_pam_auth = eroare de autentificare diff --git a/res/lang/ru.ini b/res/lang/ru.ini index 07d164c..bb6f98e 100644 --- a/res/lang/ru.ini +++ b/res/lang/ru.ini @@ -15,6 +15,7 @@ err_empty_password = пустой пароль не допустим err_envlist = не удалось получить список переменных среды err_hostname = не удалось получить имя хоста + err_mlock = сбой блокировки памяти err_null = нулевой указатель diff --git a/res/lang/sr.ini b/res/lang/sr.ini index fb8c26c..7dc54bf 100644 --- a/res/lang/sr.ini +++ b/res/lang/sr.ini @@ -15,6 +15,7 @@ err_domain = nevazeci domen err_hostname = neuspijesno trazenje hostname-a + err_mlock = neuspijesno zakljucavanje memorije lozinke err_null = null pokazivac diff --git a/res/lang/sv.ini b/res/lang/sv.ini index 7aaff8d..6b72ada 100644 --- a/res/lang/sv.ini +++ b/res/lang/sv.ini @@ -15,6 +15,7 @@ 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 diff --git a/res/lang/tr.ini b/res/lang/tr.ini index aee7fbd..9c7b7a5 100644 --- a/res/lang/tr.ini +++ b/res/lang/tr.ini @@ -15,6 +15,7 @@ err_domain = gecersiz etki alani err_hostname = ana bilgisayar adi alinamadi + err_mlock = parola bellegi kilitlenemedi err_null = bos isaretci hatasi diff --git a/res/lang/uk.ini b/res/lang/uk.ini index 16d740c..20ad6c4 100644 --- a/res/lang/uk.ini +++ b/res/lang/uk.ini @@ -15,6 +15,7 @@ err_domain = недійсний домен err_hostname = не вдалося отримати ім'я хосту + err_mlock = збій блокування пам'яті err_null = нульовий вказівник diff --git a/res/lang/zh_CN.ini b/res/lang/zh_CN.ini index 971cb1c..639c224 100644 --- a/res/lang/zh_CN.ini +++ b/res/lang/zh_CN.ini @@ -15,6 +15,7 @@ err_domain = 无效的域 err_hostname = 获取主机名失败 + err_mlock = 锁定密码存储器失败 err_null = 空指针 diff --git a/src/config/Config.zig b/src/config/Config.zig index 2399898..1aea18a 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -54,6 +54,7 @@ load: bool = true, 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", margin_box_h: u8 = 2, margin_box_v: u8 = 1, min_refresh_delta: u16 = 5, diff --git a/src/config/Lang.zig b/src/config/Lang.zig index 0649619..cbeee18 100644 --- a/src/config/Lang.zig +++ b/src/config/Lang.zig @@ -20,6 +20,7 @@ err_empty_password: []const u8 = "empty password not allowed", err_envlist: []const u8 = "failed to get envlist", err_hostname: []const u8 = "failed to get hostname", err_lock_state: []const u8 = "failed to get lock state", +err_log: []const u8 = "failed to open log file", err_mlock: []const u8 = "failed to lock password memory", err_null: []const u8 = "null pointer", err_numlock: []const u8 = "failed to set numlock", diff --git a/src/main.zig b/src/main.zig index 47d733d..aeab079 100644 --- a/src/main.zig +++ b/src/main.zig @@ -201,6 +201,25 @@ pub fn main() !void { migrator.lateConfigFieldHandler(&config.animation); } + var log_file: std.fs.File = undefined; + defer log_file.close(); + + var could_open_log_file = true; + open_log_file: { + log_file = std.fs.cwd().openFile(config.ly_log, .{ .mode = .write_only }) catch std.fs.cwd().createFile(config.ly_log, .{ .mode = 0o666 }) catch { + // If we could neither open an existing log file nor create a new + // one, abort. + could_open_log_file = false; + break :open_log_file; + }; + } + + if (!could_open_log_file) { + log_file = try std.fs.openFileAbsolute("/dev/null", .{ .mode = .write_only }); + } + + const log_writer = log_file.writer(); + // if (migrator.mapped_config_fields) save_migrated_config: { // var file = try std.fs.cwd().createFile(config_path, .{}); // defer file.close(); @@ -217,8 +236,12 @@ pub fn main() !void { restart_cmd = try temporary_allocator.dupe(u8, config.restart_cmd); // Initialize termbox + try log_writer.writeAll("initializing termbox2\n"); _ = termbox.tb_init(); - defer _ = termbox.tb_shutdown(); + defer { + log_writer.writeAll("shutting down termbox2\n") catch {}; + _ = termbox.tb_shutdown(); + } const act = std.posix.Sigaction{ .handler = .{ .handler = &signalHandler }, @@ -255,6 +278,8 @@ pub fn main() !void { }; var buffer = TerminalBuffer.init(buffer_options, labels_max_length, random); + try log_writer.print("screen resolution is {d}x{d}\n", .{ buffer.width, buffer.height }); + // Initialize components var info_line = InfoLine.init(allocator, &buffer); defer info_line.deinit(); @@ -262,27 +287,37 @@ pub fn main() !void { if (config_load_failed) { // 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.writeAll("unable to parse config file\n"); } - interop.setNumlock(config.numlock) catch { + if (!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"); + } + + 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)}); }; var session = Session.init(allocator, &buffer); defer session.deinit(); - addOtherEnvironment(&session, lang, .shell, null) catch { + 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)}); }; if (build_options.enable_x11_support) { if (config.xinitrc) |xinitrc_cmd| { - addOtherEnvironment(&session, lang, .xinitrc, xinitrc_cmd) catch { + 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)}); }; } } else { try info_line.addMessage(lang.no_x11_support, config.bg, config.fg); + try log_writer.writeAll("x11 support disabled at compile-time\n"); } if (config.initial_info_text) |text| { @@ -290,8 +325,9 @@ pub fn main() !void { } 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 { + 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)}); break :get_host_name; }; try info_line.addMessage(hostname, config.bg, config.fg); @@ -327,6 +363,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"); } var login = try UserList.init(allocator, &buffer, usernames); @@ -374,8 +411,9 @@ pub fn main() !void { .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 { + .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)}); }, } } @@ -426,8 +464,9 @@ pub fn main() !void { var auth_fails: u64 = 0; // Switch to selected TTY - interop.switchTty(config.tty) catch { + interop.switchTty(config.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)}); }; while (run) { @@ -442,12 +481,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 }); buffer.width = width; buffer.height = height; - animation.realloc() catch { + 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)}); }; update = true; @@ -518,8 +559,9 @@ pub fn main() !void { .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 { + .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)}); }, } @@ -532,6 +574,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"); break :draw_clock; } @@ -599,9 +642,10 @@ pub fn main() !void { } if (can_get_lock_state) draw_lock_state: { - const lock_state = interop.getLockState() catch { + 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)}); break :draw_lock_state; }; @@ -682,16 +726,19 @@ 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}); } } } } else if (brightness_down_key != null and pressed_key == brightness_down_key.?) { - adjustBrightness(allocator, config.brightness_down_cmd) catch { + 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)}); }; } else if (brightness_up_key != null and pressed_key == brightness_up_key.?) { - adjustBrightness(allocator, config.brightness_up_cmd) catch { + 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)}); }; } }, @@ -735,10 +782,14 @@ pub fn main() !void { update = true; }, termbox.TB_KEY_ENTER => authenticate: { + try log_writer.writeAll("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 { + 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)}); }; info_line.label.draw(); _ = termbox.tb_present(); @@ -746,8 +797,9 @@ pub fn main() !void { } try info_line.addMessage(lang.authenticating, config.bg, config.fg); - InfoLine.clearRendered(allocator, buffer) catch { + 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)}); }; info_line.label.draw(); _ = termbox.tb_present(); @@ -820,7 +872,10 @@ pub fn main() !void { if (auth_err) |err| { auth_fails += 1; 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)}); + if (config.clear_password or err != error.PamAuthError) password.clear(); } else { if (config.logout_cmd) |logout_cmd| { @@ -830,6 +885,7 @@ pub fn main() !void { password.clear(); try info_line.addMessage(lang.logout, config.bg, config.fg); + try log_writer.writeAll("logged out"); } try std.posix.tcsetattr(std.posix.STDIN_FILENO, .FLUSH, tb_termios); From a7ff18aa163912c8569790bf35b242e2f2db2d3a Mon Sep 17 00:00:00 2001 From: Matthew Rothlisberger Date: Sun, 3 Aug 2025 23:37:53 +0200 Subject: [PATCH 252/530] Add option for eight-color terminal output (#802) Reviewed-on: https://codeberg.org/fairyglade/ly/pulls/802 Reviewed-by: AnErrupTion Co-authored-by: Matthew Rothlisberger Co-committed-by: Matthew Rothlisberger --- res/config.ini | 35 +++++++++--- src/animations/Doom.zig | 10 ++-- src/animations/Matrix.zig | 10 ++-- src/config/Config.zig | 2 + src/config/migrator.zig | 106 +++++++++++++++++++++++-------------- src/main.zig | 23 ++++++-- src/tui/TerminalBuffer.zig | 24 ++++++--- 7 files changed, 139 insertions(+), 71 deletions(-) diff --git a/res/config.ini b/res/config.ini index 17db32c..b987e94 100644 --- a/res/config.ini +++ b/res/config.ini @@ -1,14 +1,14 @@ # Ly supports 24-bit true color with styling, which means each color is a 32-bit value. # The format is 0xSSRRGGBB, where SS is the styling, RR is red, GG is green, and BB is blue. # Here are the possible styling options: -#define TB_BOLD 0x01000000 -#define TB_UNDERLINE 0x02000000 -#define TB_REVERSE 0x04000000 -#define TB_ITALIC 0x08000000 -#define TB_BLINK 0x10000000 -#define TB_HI_BLACK 0x20000000 -#define TB_BRIGHT 0x40000000 -#define TB_DIM 0x80000000 +# TB_BOLD 0x01000000 +# TB_UNDERLINE 0x02000000 +# TB_REVERSE 0x04000000 +# TB_ITALIC 0x08000000 +# TB_BLINK 0x10000000 +# TB_HI_BLACK 0x20000000 +# TB_BRIGHT 0x40000000 +# TB_DIM 0x80000000 # Programmatically, you'd apply them using the bitwise OR operator (|), but because Ly's # configuration doesn't support using it, you have to manually compute the color value. # Note that, if you want to use the default color value of the terminal, you can use the @@ -89,6 +89,9 @@ clock = null # CMatrix animation foreground color id cmatrix_fg = 0x0000FF00 +# CMatrix animation character string head color id +cmatrix_head_col = 0x01FFFFFF + # CMatrix animation minimum codepoint. It uses a 16-bit integer # For Japanese characters for example, you can use 0x3000 here cmatrix_min_codepoint = 0x21 @@ -140,6 +143,22 @@ error_fg = 0x01FF0000 # Foreground color id fg = 0x00FFFFFF +# Render true colors (if supported) +# If false, output will be in eight-color mode +# All eight-color mode color codes: +# TB_DEFAULT 0x0000 +# TB_BLACK 0x0001 +# TB_RED 0x0002 +# TB_GREEN 0x0003 +# TB_YELLOW 0x0004 +# TB_BLUE 0x0005 +# TB_MAGENTA 0x0006 +# TB_CYAN 0x0007 +# 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. +full_color = true + # Game of Life entropy interval (0 = disabled, >0 = add entropy every N generations) # 0 -> Pure Conway's Game of Life (will eventually stabilize) # 10 -> Add entropy every 10 generations (recommended for continuous activity) diff --git a/src/animations/Doom.zig b/src/animations/Doom.zig index 76850fa..9917bcd 100644 --- a/src/animations/Doom.zig +++ b/src/animations/Doom.zig @@ -23,11 +23,11 @@ pub fn init(allocator: Allocator, terminal_buffer: *TerminalBuffer, top_color: u const levels = [_]Cell{ - Cell.init(' ', TerminalBuffer.Color.DEFAULT, TerminalBuffer.Color.DEFAULT), - Cell.init(0x2591, top_color, TerminalBuffer.Color.DEFAULT), - Cell.init(0x2592, top_color, TerminalBuffer.Color.DEFAULT), - Cell.init(0x2593, top_color, TerminalBuffer.Color.DEFAULT), - Cell.init(0x2588, top_color, TerminalBuffer.Color.DEFAULT), + Cell.init(' ', terminal_buffer.bg, terminal_buffer.bg), + Cell.init(0x2591, top_color, terminal_buffer.bg), + Cell.init(0x2592, top_color, terminal_buffer.bg), + Cell.init(0x2593, top_color, terminal_buffer.bg), + Cell.init(0x2588, top_color, terminal_buffer.bg), Cell.init(0x2591, middle_color, top_color), Cell.init(0x2592, middle_color, top_color), Cell.init(0x2593, middle_color, top_color), diff --git a/src/animations/Matrix.zig b/src/animations/Matrix.zig index 4069b34..5def466 100644 --- a/src/animations/Matrix.zig +++ b/src/animations/Matrix.zig @@ -11,8 +11,6 @@ pub const FRAME_DELAY: usize = 8; // Characters change mid-scroll pub const MID_SCROLL_CHANGE = true; -const DOT_HEAD_COLOR: u32 = @intCast(TerminalBuffer.Color.WHITE | TerminalBuffer.Styling.BOLD); - const Matrix = @This(); pub const Dot = struct { @@ -33,11 +31,12 @@ lines: []Line, frame: usize, count: usize, fg: u32, +head_col: u32, min_codepoint: u16, max_codepoint: u16, default_cell: Cell, -pub fn init(allocator: Allocator, terminal_buffer: *TerminalBuffer, fg: 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) !Matrix { const dots = try allocator.alloc(Dot, terminal_buffer.width * (terminal_buffer.height + 1)); const lines = try allocator.alloc(Line, terminal_buffer.width); @@ -51,6 +50,7 @@ pub fn init(allocator: Allocator, terminal_buffer: *TerminalBuffer, fg: u32, min .frame = 3, .count = 0, .fg = fg, + .head_col = head_col, .min_codepoint = min_codepoint, .max_codepoint = max_codepoint - min_codepoint, .default_cell = .{ .ch = ' ', .fg = fg, .bg = terminal_buffer.bg }, @@ -157,11 +157,13 @@ fn draw(self: *Matrix) void { 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.?), - .fg = if (dot.is_head) DOT_HEAD_COLOR else self.fg, + .fg = if (dot.is_head) self.head_col else self.fg, .bg = self.terminal_buffer.bg, }; cell.put(x, y - 1); + // Fill background in between columns + self.default_cell.put(x + 1, y - 1); } } } diff --git a/src/config/Config.zig b/src/config/Config.zig index 1aea18a..4fc0af7 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -25,6 +25,7 @@ brightness_up_key: ?[]const u8 = "F6", clear_password: bool = false, clock: ?[:0]const u8 = null, cmatrix_fg: u32 = 0x0000FF00, +cmatrix_head_col: u32 = 0x01FFFFFF, cmatrix_min_codepoint: u16 = 0x21, cmatrix_max_codepoint: u16 = 0x7B, colormix_col1: u32 = 0x00FF0000, @@ -40,6 +41,7 @@ doom_bottom_color: u32 = 0x00FFFFFF, error_bg: u32 = 0x00000000, error_fg: u32 = 0x01FF0000, fg: u32 = 0x00FFFFFF, +full_color: bool = true, gameoflife_fg: u32 = 0x0000FF00, gameoflife_entropy_interval: usize = 10, gameoflife_frame_delay: usize = 6, diff --git a/src/config/migrator.zig b/src/config/migrator.zig index 5f1269b..6d3f73b 100644 --- a/src/config/migrator.zig +++ b/src/config/migrator.zig @@ -1,9 +1,15 @@ -// The migrator ensures compatibility with <=0.6.0 configuration files +// The migrator ensures compatibility with older configuration files +// Properties removed or changed since 0.6.0 +// Color codes interpreted differently since 1.1.0 const std = @import("std"); const ini = @import("zigini"); +const Config = @import("Config.zig"); const Save = @import("Save.zig"); -const enums = @import("../enums.zig"); +const TerminalBuffer = @import("../tui/TerminalBuffer.zig"); + +const Color = TerminalBuffer.Color; +const Styling = TerminalBuffer.Styling; const color_properties = [_][]const u8{ "bg", @@ -16,6 +22,10 @@ const color_properties = [_][]const u8{ "error_fg", "fg", }; + +var set_color_properties = + [_]bool{ false, false, false, false, false, false, false, false, false }; + const removed_properties = [_][]const u8{ "wayland_specifier", "max_desktop_len", @@ -32,6 +42,8 @@ const removed_properties = [_][]const u8{ var temporary_allocator = std.heap.page_allocator; var buffer = std.mem.zeroes([10 * color_properties.len]u8); +pub var auto_eight_colors: bool = true; + pub var maybe_animate: ?bool = null; pub var maybe_save_file: ?[]const u8 = null; @@ -62,15 +74,26 @@ pub fn configFieldHandler(_: std.mem.Allocator, field: ini.IniField) ?ini.IniFie return mapped_field; } - inline for (color_properties) |property| { + inline for (color_properties, &set_color_properties) |property, *status| { if (std.mem.eql(u8, field.key, property)) { - // These options now uses a 32-bit RGB value instead of an arbitrary 16-bit integer - const color = std.fmt.parseInt(u16, field.value, 0) catch return field; - var mapped_field = field; + // Color has been set; it won't be overwritten if we default to eight-color output + status.* = true; - mapped_field.value = mapColor(color) catch return field; - mapped_config_fields = true; - return mapped_field; + // These options now uses a 32-bit RGB value instead of an arbitrary 16-bit integer + // If they're all using eight-color codes, we start in eight-color mode + const color = std.fmt.parseInt(u16, field.value, 0) catch { + auto_eight_colors = false; + return field; + }; + + const color_no_styling = color & 0x00FF; + const styling_only = color & 0xFF00; + + // If color is "greater" than TB_WHITE, or the styling is "greater" than TB_DIM, + // we have an invalid color, so do not use eight-color mode + if (color_no_styling > 0x0008 or styling_only > 0x8000) auto_eight_colors = false; + + return field; } } @@ -131,14 +154,45 @@ pub fn configFieldHandler(_: std.mem.Allocator, field: ini.IniField) ?ini.IniFie return mapped_field; } + if (std.mem.eql(u8, field.key, "full_color")) { + // If color mode is defined, definitely don't set it automatically + auto_eight_colors = false; + return field; + } + return field; } // This is the stuff we only handle after reading the config. // For example, the "animate" field could come after "animation" -pub fn lateConfigFieldHandler(animation: *enums.Animation) void { +pub fn lateConfigFieldHandler(config: *Config) void { if (maybe_animate) |animate| { - if (!animate) animation.* = .none; + if (!animate) config.*.animation = .none; + } + + if (auto_eight_colors) { + // Valid config file predates true-color mode + // Will use eight-color output instead + config.full_color = false; + + // We cannot rely on Config defaults when in eight-color mode, + // because they will appear as undesired colors. + // Instead set color properties to matching eight-color codes + config.doom_top_color = Color.ECOL_RED; + config.doom_middle_color = Color.ECOL_YELLOW; + config.doom_bottom_color = Color.ECOL_WHITE; + config.cmatrix_head_col = Styling.BOLD | Color.ECOL_WHITE; + + // These may be in the config, so only change those which were not set + if (!set_color_properties[0]) config.bg = Color.DEFAULT; + if (!set_color_properties[1]) config.border_fg = Color.ECOL_WHITE; + if (!set_color_properties[2]) config.cmatrix_fg = Color.ECOL_GREEN; + if (!set_color_properties[3]) config.colormix_col1 = Color.ECOL_RED; + if (!set_color_properties[4]) config.colormix_col2 = Color.ECOL_BLUE; + if (!set_color_properties[5]) config.colormix_col3 = Color.ECOL_BLACK; + if (!set_color_properties[6]) config.error_bg = Color.DEFAULT; + if (!set_color_properties[7]) config.error_fg = Styling.BOLD | Color.ECOL_RED; + if (!set_color_properties[8]) config.fg = Color.ECOL_WHITE; } } @@ -172,33 +226,3 @@ pub fn tryMigrateSaveFile(user_buf: *[32]u8) Save { return save; } - -fn mapColor(color: u16) ![]const u8 { - const color_no_styling = color & 0x00FF; - const styling_only = color & 0xFF00; - - // If color is "greater" than TB_WHITE, or the styling is "greater" than TB_DIM, - // we have an invalid color, so return an error - if (color_no_styling > 0x0008 or styling_only > 0x8000) return error.InvalidColor; - - var new_color: u32 = switch (color_no_styling) { - 0x0000 => 0x00000000, // Default - 0x0001 => 0x20000000, // "Hi-black" styling - 0x0002 => 0x00FF0000, // Red - 0x0003 => 0x0000FF00, // Green - 0x0004 => 0x00FFFF00, // Yellow - 0x0005 => 0x000000FF, // Blue - 0x0006 => 0x00FF00FF, // Magenta - 0x0007 => 0x0000FFFF, // Cyan - 0x0008 => 0x00FFFFFF, // White - else => unreachable, - }; - - // Only applying styling if color isn't black and styling isn't also black - if (!(new_color == 0x20000000 and styling_only == 0x20000000)) { - // Shift styling by 16 to the left to apply it to the new 32-bit color - new_color |= @as(u32, @intCast(styling_only)) << 16; - } - - return try std.fmt.bufPrint(&buffer, "0x{X}", .{new_color}); -} diff --git a/src/main.zig b/src/main.zig index aeab079..710d67b 100644 --- a/src/main.zig +++ b/src/main.zig @@ -170,7 +170,9 @@ pub fn main() !void { }) catch migrator.tryMigrateSaveFile(&user_buf); } - migrator.lateConfigFieldHandler(&config.animation); + if (!config_load_failed) { + migrator.lateConfigFieldHandler(&config); + } } else { const config_path = build_options.config_directory ++ "/ly/config.ini"; @@ -198,7 +200,9 @@ pub fn main() !void { }) catch migrator.tryMigrateSaveFile(&user_buf); } - migrator.lateConfigFieldHandler(&config.animation); + if (!config_load_failed) { + migrator.lateConfigFieldHandler(&config); + } } var log_file: std.fs.File = undefined; @@ -250,7 +254,13 @@ pub fn main() !void { }; std.posix.sigaction(std.posix.SIG.TERM, &act, null); - _ = termbox.tb_set_output_mode(termbox.TB_OUTPUT_TRUECOLOR); + 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 @@ -431,7 +441,7 @@ pub fn main() !void { animation = doom.animation(); }, .matrix => { - var matrix = try Matrix.init(allocator, &buffer, config.cmatrix_fg, config.cmatrix_min_codepoint, config.cmatrix_max_codepoint); + 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 => { @@ -866,7 +876,10 @@ pub fn main() !void { // Take back control of the TTY _ = termbox.tb_init(); - _ = termbox.tb_set_output_mode(termbox.TB_OUTPUT_TRUECOLOR); + + if (config.full_color) { + _ = termbox.tb_set_output_mode(termbox.TB_OUTPUT_TRUECOLOR); + } const auth_err = shared_err.readError(); if (auth_err) |err| { diff --git a/src/tui/TerminalBuffer.zig b/src/tui/TerminalBuffer.zig index 555f26b..4987208 100644 --- a/src/tui/TerminalBuffer.zig +++ b/src/tui/TerminalBuffer.zig @@ -31,14 +31,22 @@ pub const Styling = struct { pub const Color = struct { pub const DEFAULT = 0x00000000; - pub const BLACK = Styling.HI_BLACK; - pub const RED = 0x00FF0000; - pub const GREEN = 0x0000FF00; - pub const YELLOW = 0x00FFFF00; - pub const BLUE = 0x000000FF; - pub const MAGENTA = 0x00FF00FF; - pub const CYAN = 0x0000FFFF; - pub const WHITE = 0x00FFFFFF; + pub const TRUE_BLACK = Styling.HI_BLACK; + pub const TRUE_RED = 0x00FF0000; + pub const TRUE_GREEN = 0x0000FF00; + pub const TRUE_YELLOW = 0x00FFFF00; + pub const TRUE_BLUE = 0x000000FF; + pub const TRUE_MAGENTA = 0x00FF00FF; + pub const TRUE_CYAN = 0x0000FFFF; + pub const TRUE_WHITE = 0x00FFFFFF; + pub const ECOL_BLACK = 1; + pub const ECOL_RED = 2; + pub const ECOL_GREEN = 3; + pub const ECOL_YELLOW = 4; + pub const ECOL_BLUE = 5; + pub const ECOL_MAGENTA = 6; + pub const ECOL_CYAN = 7; + pub const ECOL_WHITE = 8; }; random: Random, From 5bacc8870b126d199e87756fd80af9e2aab3767a Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Sun, 3 Aug 2025 23:42:35 +0200 Subject: [PATCH 253/530] Update repository link in README Signed-off-by: AnErrupTion --- readme.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/readme.md b/readme.md index f41e0e0..973a10c 100644 --- a/readme.md +++ b/readme.md @@ -7,7 +7,7 @@ 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)! -**Note**: Development happens on [Codeberg](https://codeberg.org/AnErrupTion/ly) +**Note**: Development happens on [Codeberg](https://codeberg.org/fairyglade/ly) with a mirror on [GitHub](https://github.com/fairyglade/ly). ## Dependencies @@ -56,7 +56,7 @@ managers, all of which you can find in the sections below: The procedure for manually building Ly is pretty standard: ``` -$ git clone https://codeberg.org/AnErrupTion/ly +$ git clone https://codeberg.org/fairyglade/ly.git $ cd ly $ zig build ``` From b71789912dc55375cc43c9738492f6e6c7f5328e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Lucas?= Date: Mon, 4 Aug 2025 00:00:18 +0200 Subject: [PATCH 254/530] Add enable_session_log option to control session logging (#809) (fixes #808) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reviewed-on: https://codeberg.org/fairyglade/ly/pulls/809 Reviewed-by: AnErrupTion Co-authored-by: João Lucas Co-committed-by: João Lucas --- res/config.ini | 3 ++- src/auth.zig | 13 +++++++++---- src/config/Config.zig | 2 +- 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/res/config.ini b/res/config.ini index b987e94..15844e5 100644 --- a/res/config.ini +++ b/res/config.ini @@ -253,7 +253,8 @@ service_name = ly # 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 +# means you won't get any logs from those sessions. +# If null, no session log will be created session_log = ly-session.log # Setup command diff --git a/src/auth.zig b/src/auth.zig index d1e3a0a..e57e237 100644 --- a/src/auth.zig +++ b/src/auth.zig @@ -15,7 +15,7 @@ pub const AuthOptions = struct { tty: u8, service_name: [:0]const u8, path: ?[:0]const u8, - session_log: []const u8, + session_log: ?[]const u8, xauth_cmd: []const u8, setup_cmd: []const u8, login_cmd: ?[]const u8, @@ -399,8 +399,11 @@ fn executeShellCmd(shell: [*:0]const u8, options: AuthOptions) !void { } fn executeWaylandCmd(shell: [*:0]const u8, options: AuthOptions, desktop_cmd: []const u8) !void { - const log_file = try redirectStandardStreams(options.session_log, true); - defer log_file.close(); + var maybe_log_file: ?std.fs.File = null; + if (options.session_log) |log_path| { + maybe_log_file = try redirectStandardStreams(log_path, true); + } + defer if (maybe_log_file) |log_file| log_file.close(); 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 "", desktop_cmd }); @@ -470,7 +473,9 @@ fn executeCustomCmd(shell: [*:0]const u8, options: AuthOptions, is_terminal: boo // 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). - maybe_log_file = try redirectStandardStreams(options.session_log, true); + if (options.session_log) |log_path| { + maybe_log_file = try redirectStandardStreams(log_path, true); + } } defer if (maybe_log_file) |log_file| log_file.close(); diff --git a/src/config/Config.zig b/src/config/Config.zig index 4fc0af7..ef91cb1 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -66,7 +66,7 @@ restart_cmd: []const u8 = "/sbin/shutdown -r now", restart_key: []const u8 = "F2", save: bool = true, service_name: [:0]const u8 = "ly", -session_log: []const u8 = "ly-session.log", +session_log: ?[]const u8 = "ly-session.log", setup_cmd: []const u8 = build_options.config_directory ++ "/ly/setup.sh", shutdown_cmd: []const u8 = "/sbin/shutdown -a now", shutdown_key: []const u8 = "F1", From cd0accfb287445d08b2d7967344a1155f808de08 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Tue, 5 Aug 2025 08:26:13 +0200 Subject: [PATCH 255/530] Show error name instead of error if shutdown/reboot fails 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 710d67b..6fc758c 100644 --- a/src/main.zig +++ b/src/main.zig @@ -66,10 +66,10 @@ pub fn main() !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.execv(temporary_allocator, &[_][]const u8{ "/bin/sh", "-c", shutdown_cmd }); - stderr.print("error: couldn't shutdown: {any}\n", .{shutdown_error}) catch std.process.exit(1); + stderr.print("error: couldn't shutdown: {s}\n", .{@errorName(shutdown_error)}) catch std.process.exit(1); } else if (restart) { const restart_error = std.process.execv(temporary_allocator, &[_][]const u8{ "/bin/sh", "-c", restart_cmd }); - stderr.print("error: couldn't restart: {any}\n", .{restart_error}) catch std.process.exit(1); + stderr.print("error: couldn't restart: {s}\n", .{@errorName(restart_error)}) catch std.process.exit(1); } else { // The user has quit Ly using Ctrl+C temporary_allocator.free(shutdown_cmd); From 73ecac67bfbd819dad34cfdb30e16ca1c868bfdf Mon Sep 17 00:00:00 2001 From: djsigmann Date: Tue, 5 Aug 2025 21:45:09 +0200 Subject: [PATCH 256/530] Prevent Ly from zombifying when X.org is terminated (#807) (fixes #787) If a compositor is running when exiting Xorg, the user is met with an unresponsive black screen without the ability to switch to a different TTY (the usual `Ctrl+Alt+F{1..6}` chord doesn't do anything). In addition, ly is displayed as a zombie process under `ps` and cannot be killed (observed by utilizing a preexisting ssh connection to the host). Reviewed-on: https://codeberg.org/fairyglade/ly/pulls/807 Reviewed-by: AnErrupTion Co-authored-by: djsigmann Co-committed-by: djsigmann --- src/auth.zig | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/auth.zig b/src/auth.zig index e57e237..f60f05e 100644 --- a/src/auth.zig +++ b/src/auth.zig @@ -460,8 +460,10 @@ fn executeX11Cmd(shell: [*:0]const u8, pw_dir: [*:0]const u8, options: AuthOptio _ = std.posix.waitpid(xorg_pid, 0); interop.xcb.xcb_disconnect(xcb); - std.posix.kill(x_pid, 0) catch return; - std.posix.kill(x_pid, std.posix.SIG.KILL) catch {}; + // 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 + std.posix.kill(x_pid, std.posix.SIG.KILL) catch return; var status: c_int = 0; _ = std.c.waitpid(x_pid, &status, 0); From d7a4535007bb98e78a21ccf4f57dfbbe3a145e9d Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Mon, 11 Aug 2025 12:32:10 +0200 Subject: [PATCH 257/530] Update screenshot Signed-off-by: AnErrupTion --- .github/screenshot.png | Bin 43911 -> 39461 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/.github/screenshot.png b/.github/screenshot.png index 969417cba9c9bec1ed6413f27ee89ab6cedf140f..1cd5ff4555c55f58d3df7662946aa505a316ab13 100644 GIT binary patch literal 39461 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 Date: Tue, 26 Aug 2025 18:59:43 +0200 Subject: [PATCH 258/530] [dinit] fix deprecated loginready, replaced by login.target (#828) Fixes [corresponding issue](https://codeberg.org/fairyglade/ly/issues/827#issue-2234461) Reviewed-on: https://codeberg.org/fairyglade/ly/pulls/828 Reviewed-by: AnErrupTion Co-authored-by: KaiJan57 Co-committed-by: KaiJan57 --- res/ly-dinit | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/res/ly-dinit b/res/ly-dinit index a20a71f..016929a 100644 --- a/res/ly-dinit +++ b/res/ly-dinit @@ -2,7 +2,7 @@ type = process restart = true smooth-recovery = true command = $PREFIX_DIRECTORY/bin/$EXE_NAME -depends-on = loginready +depends-on = login.target termsignal = HUP # ly needs access to the console while loginready already occupies it options = shares-console From ff9b6279d3ad3c9b36983d0068fc0fec0e38c731 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Wed, 27 Aug 2025 23:44:39 +0200 Subject: [PATCH 259/530] Update to Zig 0.15.0 (closes #829) Signed-off-by: AnErrupTion --- build.zig | 25 ++++++++++++++++--------- build.zig.zon | 10 +++++----- readme.md | 2 +- src/auth.zig | 22 ++++++++++++---------- src/config/migrator.zig | 21 ++++++++++----------- src/main.zig | 38 +++++++++++++++++++++----------------- 6 files changed, 65 insertions(+), 53 deletions(-) diff --git a/build.zig b/build.zig index a227e07..d241ebb 100644 --- a/build.zig +++ b/build.zig @@ -10,7 +10,7 @@ const InitSystem = enum { dinit, }; -const min_zig_string = "0.14.0"; +const min_zig_string = "0.15.0"; const current_zig = builtin.zig_version; // Implementing zig version detection through compile time @@ -55,9 +55,11 @@ pub fn build(b: *std.Build) !void { const exe = b.addExecutable(.{ .name = "ly", - .root_source_file = b.path("src/main.zig"), - .target = target, - .optimize = optimize, + .root_module = b.createModule(.{ + .root_source_file = b.path("src/main.zig"), + .target = target, + .optimize = optimize, + }), }); const zigini = b.dependency("zigini", .{ .target = target, .optimize = optimize }); @@ -360,7 +362,7 @@ fn getVersionStr(b: *std.Build, name: []const u8, version: std.SemanticVersion) const ancestor_ver = try std.SemanticVersion.parse(tagged_ancestor); if (version.order(ancestor_ver) != .gt) { - std.debug.print("{s} version '{}' must be greater than tagged ancestor '{}'\n", .{ name, version, ancestor_ver }); + std.debug.print("{s} version '{f}' must be greater than tagged ancestor '{f}'\n", .{ name, version, ancestor_ver }); std.process.exit(1); } @@ -395,8 +397,11 @@ fn patchFile(allocator: std.mem.Allocator, source_file: []const u8, patch_map: P var file = try std.fs.cwd().openFile(source_file, .{}); defer file.close(); - const reader = file.reader(); - var text = try reader.readAllAlloc(allocator, std.math.maxInt(u16)); + const stat = try file.stat(); + + var buffer: [4096]u8 = undefined; + var reader = file.reader(&buffer); + var text = try reader.interface.readAlloc(allocator, stat.size); var iterator = patch_map.iterator(); while (iterator.next()) |kv| { @@ -418,8 +423,10 @@ fn installText( var file = try destination_directory.createFile(destination_file, options); defer file.close(); - const writer = file.writer(); - try writer.writeAll(text); + var buffer: [1024]u8 = undefined; + var writer = file.writer(&buffer); + try writer.interface.writeAll(text); + try writer.interface.flush(); std.debug.print("info: installed {s}/{s}\n", .{ destination_directory_path, destination_file }); } diff --git a/build.zig.zon b/build.zig.zon index 1acc19d..dad2165 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -2,15 +2,15 @@ .name = .ly, .version = "1.2.0", .fingerprint = 0xa148ffcc5dc2cb59, - .minimum_zig_version = "0.14.0", + .minimum_zig_version = "0.15.0", .dependencies = .{ .clap = .{ - .url = "https://github.com/Hejsil/zig-clap/archive/refs/tags/0.10.0.tar.gz", - .hash = "clap-0.10.0-oBajB434AQBDh-Ei3YtoKIRxZacVPF1iSwp3IX_ZB8f0", + .url = "https://github.com/Hejsil/zig-clap/archive/refs/tags/0.11.0.tar.gz", + .hash = "clap-0.11.0-oBajB-HnAQDPCKYzwF7rO3qDFwRcD39Q0DALlTSz5H7e", }, .zigini = .{ - .url = "https://github.com/Kawaii-Ash/zigini/archive/2ed3d417f17fab5b0ee8cad8a63c6d62d7ac1042.tar.gz", - .hash = "zigini-0.3.1-BSkB7XJGAAB2E-sKyzhTaQCBlYBL8yqzE4E_jmSY99sC", + .url = "https://github.com/AnErrupTion/zigini/archive/d580d42f1b1051c0a35d63ab0f5704c6340e0bd3.tar.gz", + .hash = "zigini-0.3.2-BSkB7aVHAADhxwo0aEdWtNzaVXer3d8RwXMuZd-q-spO", }, .termbox2 = .{ .url = "git+https://github.com/AnErrupTion/termbox2?ref=get_cell#e975d250ee6567773400e9d5b0b5c2f175349c57", diff --git a/readme.md b/readme.md index 973a10c..4690b02 100644 --- a/readme.md +++ b/readme.md @@ -13,7 +13,7 @@ with a mirror on [GitHub](https://github.com/fairyglade/ly). ## Dependencies - Compile-time: - - zig 0.14.x + - zig 0.15.x - libc - pam - xcb (optional, required by default; needed for X11 support) diff --git a/src/auth.zig b/src/auth.zig index f60f05e..bbe00c3 100644 --- a/src/auth.zig +++ b/src/auth.zig @@ -24,12 +24,12 @@ pub const AuthOptions = struct { }; var xorg_pid: std.posix.pid_t = 0; -pub fn xorgSignalHandler(i: c_int) callconv(.C) void { +pub fn xorgSignalHandler(i: c_int) callconv(.c) void { if (xorg_pid > 0) _ = std.c.kill(xorg_pid, i); } var child_pid: std.posix.pid_t = 0; -pub fn sessionSignalHandler(i: c_int) callconv(.C) void { +pub fn sessionSignalHandler(i: c_int) callconv(.c) void { if (child_pid > 0) _ = std.c.kill(child_pid, i); } @@ -114,7 +114,7 @@ pub fn authenticate(options: AuthOptions, current_environment: Environment, logi // If we receive SIGTERM, forward it to child_pid const act = std.posix.Sigaction{ .handler = .{ .handler = &sessionSignalHandler }, - .mask = std.posix.empty_sigset, + .mask = std.posix.sigemptyset(), .flags = 0, }; std.posix.sigaction(std.posix.SIG.TERM, &act, null); @@ -230,7 +230,7 @@ fn loginConv( msg: ?[*]?*const interop.pam.pam_message, resp: ?*?[*]interop.pam.pam_response, appdata_ptr: ?*anyopaque, -) callconv(.C) c_int { +) callconv(.c) c_int { const message_count: u32 = @intCast(num_msg); const messages = msg.?; @@ -299,13 +299,15 @@ fn getXPid(display_num: u8) !i32 { const file = try std.fs.openFileAbsolute(file_name, .{}); defer file.close(); - var file_buf: [20]u8 = undefined; - var fbs = std.io.fixedBufferStream(&file_buf); + var file_buffer: [32]u8 = undefined; + var file_reader = file.reader(&file_buffer); + var reader = &file_reader.interface; - _ = try file.reader().streamUntilDelimiter(fbs.writer(), '\n', 20); - const line = fbs.getWritten(); + var buffer: [20]u8 = undefined; + var writer = std.Io.Writer.fixed(&buffer); - return std.fmt.parseInt(i32, std.mem.trim(u8, line, " "), 10); + const written = try reader.streamDelimiter(&writer, '\n'); + return std.fmt.parseInt(i32, std.mem.trim(u8, buffer[0..written], " "), 10); } fn createXauthFile(pwd: [:0]const u8) ![:0]const u8 { @@ -452,7 +454,7 @@ fn executeX11Cmd(shell: [*:0]const u8, pw_dir: [*:0]const u8, options: AuthOptio // If we receive SIGTERM, clean up by killing the xorg_pid process const act = std.posix.Sigaction{ .handler = .{ .handler = &xorgSignalHandler }, - .mask = std.posix.empty_sigset, + .mask = std.posix.sigemptyset(), .flags = 0, }; std.posix.sigaction(std.posix.SIG.TERM, &act, null); diff --git a/src/config/migrator.zig b/src/config/migrator.zig index 6d3f73b..c3b7266 100644 --- a/src/config/migrator.zig +++ b/src/config/migrator.zig @@ -40,7 +40,6 @@ const removed_properties = [_][]const u8{ }; var temporary_allocator = std.heap.page_allocator; -var buffer = std.mem.zeroes([10 * color_properties.len]u8); pub var auto_eight_colors: bool = true; @@ -205,21 +204,21 @@ pub fn tryMigrateSaveFile(user_buf: *[32]u8) Save { var file = std.fs.openFileAbsolute(path, .{}) catch return save; defer file.close(); - const reader = file.reader(); + var file_buffer: [64]u8 = undefined; + var file_reader = file.reader(&file_buffer); + var reader = &file_reader.interface; - var user_fbs = std.io.fixedBufferStream(user_buf); - reader.streamUntilDelimiter(user_fbs.writer(), '\n', user_buf.len) catch return save; - const user = user_fbs.getWritten(); - if (user.len > 0) save.user = user; + var user_writer = std.Io.Writer.fixed(user_buf); + var written = reader.streamDelimiter(&user_writer, '\n') catch return save; + if (written > 0) save.user = user_buf[0..written]; var session_buf: [20]u8 = undefined; - var session_fbs = std.io.fixedBufferStream(&session_buf); - reader.streamUntilDelimiter(session_fbs.writer(), '\n', session_buf.len) catch return save; + var session_writer = std.Io.Writer.fixed(&session_buf); + written = reader.streamDelimiter(&session_writer, '\n') catch return save; - const session_index_str = session_fbs.getWritten(); var session_index: ?usize = null; - if (session_index_str.len > 0) { - session_index = std.fmt.parseUnsigned(usize, session_index_str, 10) catch return save; + if (written > 0) { + session_index = std.fmt.parseUnsigned(usize, session_buf[0..written], 10) catch return save; } save.session_index = session_index; } diff --git a/src/main.zig b/src/main.zig index 6fc758c..424a3a2 100644 --- a/src/main.zig +++ b/src/main.zig @@ -36,7 +36,7 @@ const temporary_allocator = std.heap.page_allocator; const ly_top_str = "Ly version " ++ build_options.version; var session_pid: std.posix.pid_t = -1; -fn signalHandler(i: c_int) callconv(.C) void { +fn signalHandler(i: c_int) callconv(.c) void { if (session_pid == 0) return; // Forward signal to session to clean up @@ -50,7 +50,7 @@ fn signalHandler(i: c_int) callconv(.C) void { std.c.exit(i); } -fn ttyControlTransferSignalHandler(_: c_int) callconv(.C) void { +fn ttyControlTransferSignalHandler(_: c_int) callconv(.c) void { _ = termbox.tb_shutdown(); } @@ -60,16 +60,20 @@ pub fn main() !void { var shutdown_cmd: []const u8 = undefined; var restart_cmd: []const u8 = undefined; - const stderr = std.io.getStdErr().writer(); + var stderr_buffer: [128]u8 = undefined; + var stderr_writer = std.fs.File.stderr().writer(&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 }); 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 }); stderr.print("error: couldn't restart: {s}\n", .{@errorName(restart_error)}) catch std.process.exit(1); + stderr.flush() catch std.process.exit(1); } else { // The user has quit Ly using Ctrl+C temporary_allocator.free(shutdown_cmd); @@ -97,6 +101,7 @@ pub fn main() !void { var diag = clap.Diagnostic{}; var res = clap.parse(clap.Help, ¶ms, clap.parsers.default, .{ .diagnostic = &diag, .allocator = allocator }) catch |err| { diag.report(stderr, err) catch {}; + try stderr.flush(); return err; }; defer res.deinit(); @@ -112,10 +117,12 @@ pub fn main() !void { 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.process.exit(0); } if (res.args.version != 0) { _ = try stderr.write("Ly version " ++ build_options.version ++ "\n"); + try stderr.flush(); std.process.exit(0); } @@ -222,17 +229,9 @@ pub fn main() !void { log_file = try std.fs.openFileAbsolute("/dev/null", .{ .mode = .write_only }); } - const log_writer = log_file.writer(); - - // if (migrator.mapped_config_fields) save_migrated_config: { - // var file = try std.fs.cwd().createFile(config_path, .{}); - // defer file.close(); - - // const writer = file.writer(); - // ini.writeFromStruct(config, writer, null, true, .{}) catch { - // break :save_migrated_config; - // }; - // } + var log_buffer: [1024]u8 = undefined; + var log_file_writer = log_file.writer(&log_buffer); + var log_writer = &log_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 @@ -249,7 +248,7 @@ pub fn main() !void { const act = std.posix.Sigaction{ .handler = .{ .handler = &signalHandler }, - .mask = std.posix.empty_sigset, + .mask = std.posix.sigemptyset(), .flags = 0, }; std.posix.sigaction(std.posix.SIG.TERM, &act, null); @@ -818,11 +817,16 @@ pub fn main() !void { var file = std.fs.cwd().createFile(save_path, .{}) catch break :save_last_settings; defer file.close(); + var file_buffer: [64]u8 = undefined; + var file_writer = file.writer(&file_buffer); + var writer = &file_writer.interface; + const save_data = Save{ .user = login.getCurrentUser(), .session_index = session.label.current, }; - ini.writeFromStruct(save_data, file.writer(), null, .{}) catch break :save_last_settings; + ini.writeFromStruct(save_data, writer, null, .{}) catch break :save_last_settings; + try writer.flush(); // Delete previous save file if it exists if (migrator.maybe_save_file) |path| std.fs.cwd().deleteFile(path) catch {}; @@ -855,7 +859,7 @@ pub fn main() !void { // Signal action to give up control on the TTY const tty_control_transfer_act = std.posix.Sigaction{ .handler = .{ .handler = &ttyControlTransferSignalHandler }, - .mask = std.posix.empty_sigset, + .mask = std.posix.sigemptyset(), .flags = 0, }; std.posix.sigaction(std.posix.SIG.CHLD, &tty_control_transfer_act, null); From 7cfb94718743561d93931aa4c93204a9ea3338a7 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Thu, 28 Aug 2025 01:39:19 +0200 Subject: [PATCH 260/530] Reduce libc usage & move more stuff to interop Signed-off-by: AnErrupTion --- src/Environment.zig | 10 +- src/UidRange.zig | 4 +- src/auth.zig | 180 ++++++++++++++++----------------- src/bigclock.zig | 8 +- src/bigclock/Lang.zig | 4 +- src/config/Config.zig | 2 +- src/interop.zig | 156 +++++++++++++++++++++++----- src/main.zig | 53 ++++------ src/tui/TerminalBuffer.zig | 3 +- src/tui/components/Session.zig | 1 - 10 files changed, 256 insertions(+), 165 deletions(-) diff --git a/src/Environment.zig b/src/Environment.zig index 47d4d7f..05ab149 100644 --- a/src/Environment.zig +++ b/src/Environment.zig @@ -6,17 +6,17 @@ const Ini = ini.Ini; pub const DesktopEntry = struct { Exec: []const u8 = "", - Name: [:0]const u8 = "", - DesktopNames: ?[:0]u8 = null, + Name: []const u8 = "", + DesktopNames: ?[]u8 = null, Terminal: ?bool = null, }; pub const Entry = struct { @"Desktop Entry": DesktopEntry = .{} }; entry_ini: ?Ini(Entry) = null, -name: [:0]const u8 = "", -xdg_session_desktop: ?[:0]const u8 = null, -xdg_desktop_names: ?[:0]const u8 = null, +name: []const u8 = "", +xdg_session_desktop: ?[]const u8 = null, +xdg_desktop_names: ?[]const u8 = null, cmd: []const u8 = "", specifier: []const u8 = "", display_server: DisplayServer = .wayland, diff --git a/src/UidRange.zig b/src/UidRange.zig index 13eb979..da78988 100644 --- a/src/UidRange.zig +++ b/src/UidRange.zig @@ -2,5 +2,5 @@ const std = @import("std"); // We set both values to 0 by default so that, in case they aren't present in // the login.defs for some reason, then only the root username will be shown -uid_min: std.c.uid_t = 0, -uid_max: std.c.uid_t = 0, +uid_min: std.posix.uid_t = 0, +uid_max: std.posix.uid_t = 0, diff --git a/src/auth.zig b/src/auth.zig index bbe00c3..ebb0505 100644 --- a/src/auth.zig +++ b/src/auth.zig @@ -14,7 +14,7 @@ const Utmp = utmp.utmpx; pub const AuthOptions = struct { tty: u8, service_name: [:0]const u8, - path: ?[:0]const u8, + path: ?[]const u8, session_log: ?[]const u8, xauth_cmd: []const u8, setup_cmd: []const u8, @@ -33,15 +33,15 @@ pub fn sessionSignalHandler(i: c_int) callconv(.c) void { if (child_pid > 0) _ = std.c.kill(child_pid, i); } -pub fn authenticate(options: AuthOptions, current_environment: Environment, login: [:0]const u8, password: [:0]const u8) !void { +pub fn authenticate(allocator: std.mem.Allocator, options: AuthOptions, current_environment: Environment, login: [:0]const u8, password: [:0]const u8) !void { var tty_buffer: [3]u8 = undefined; - const tty_str = try std.fmt.bufPrintZ(&tty_buffer, "{d}", .{options.tty}); + const tty_str = try std.fmt.bufPrint(&tty_buffer, "{d}", .{options.tty}); var pam_tty_buffer: [6]u8 = undefined; const pam_tty_str = try std.fmt.bufPrintZ(&pam_tty_buffer, "tty{d}", .{options.tty}); // Set the XDG environment variables - try setXdgEnv(tty_str, current_environment); + try setXdgEnv(allocator, tty_str, current_environment); // Open the PAM session var credentials = [_:null]?[*:0]const u8{ login, password }; @@ -75,27 +75,23 @@ pub fn authenticate(options: AuthOptions, current_environment: Environment, logi if (status != interop.pam.PAM_SUCCESS) return pamDiagnose(status); defer status = interop.pam.pam_close_session(handle, 0); - var pwd: *interop.pwd.passwd = undefined; + var user_entry: interop.UsernameEntry = undefined; { - defer interop.pwd.endpwent(); + defer interop.closePasswordDatabase(); // Get password structure from username - pwd = interop.pwd.getpwnam(login) orelse return error.GetPasswordNameFailed; + user_entry = interop.getUsernameEntry(login) orelse return error.GetPasswordNameFailed; } // Set user shell if it hasn't already been set - if (pwd.pw_shell == null) { - interop.unistd.setusershell(); - pwd.pw_shell = interop.unistd.getusershell(); - interop.unistd.endusershell(); - } + if (user_entry.shell == null) interop.setUserShell(&user_entry); var shared_err = try SharedError.init(); defer shared_err.deinit(); child_pid = try std.posix.fork(); if (child_pid == 0) { - startSession(options, tty_str, pwd, handle, current_environment) catch |e| { + startSession(allocator, options, tty_str, user_entry, handle, current_environment) catch |e| { shared_err.writeError(e); std.process.exit(1); }; @@ -119,7 +115,7 @@ pub fn authenticate(options: AuthOptions, current_environment: Environment, logi }; std.posix.sigaction(std.posix.SIG.TERM, &act, null); - try addUtmpEntry(&entry, pwd.pw_name.?, child_pid); + try addUtmpEntry(&entry, user_entry.username.?, child_pid); } // Wait for the session to stop _ = std.posix.waitpid(child_pid, 0); @@ -130,99 +126,89 @@ pub fn authenticate(options: AuthOptions, current_environment: Environment, logi } fn startSession( + allocator: std.mem.Allocator, options: AuthOptions, - tty_str: [:0]u8, - pwd: *interop.pwd.passwd, + tty_str: []u8, + user_entry: interop.UsernameEntry, handle: ?*interop.pam.pam_handle, current_environment: Environment, ) !void { - if (builtin.os.tag == .freebsd) { - // FreeBSD has initgroups() in unistd - const status = interop.unistd.initgroups(pwd.pw_name, pwd.pw_gid); - if (status != 0) return error.GroupInitializationFailed; - - // FreeBSD sets the GID and UID with setusercontext() - const result = interop.pwd.setusercontext(null, pwd, pwd.pw_uid, interop.pwd.LOGIN_SETALL); - if (result != 0) return error.SetUserUidFailed; - } else { - const status = interop.grp.initgroups(pwd.pw_name, pwd.pw_gid); - if (status != 0) return error.GroupInitializationFailed; - - std.posix.setgid(pwd.pw_gid) catch return error.SetUserGidFailed; - std.posix.setuid(pwd.pw_uid) catch return error.SetUserUidFailed; - } + // Set the user's GID & PID + try interop.setUserContext(allocator, user_entry); // Set up the environment - try initEnv(pwd, options.path); + try initEnv(allocator, user_entry, options.path); // Reset the XDG environment variables - try setXdgEnv(tty_str, current_environment); + try setXdgEnv(allocator, tty_str, current_environment); // Set the PAM variables const pam_env_vars: ?[*:null]?[*:0]u8 = interop.pam.pam_getenvlist(handle); if (pam_env_vars == null) return error.GetEnvListFailed; const env_list = std.mem.span(pam_env_vars.?); - for (env_list) |env_var| _ = interop.stdlib.putenv(env_var); + for (env_list) |env_var| try interop.putEnvironmentVariable(env_var); // Change to the user's home directory - std.posix.chdirZ(pwd.pw_dir.?) catch return error.ChangeDirectoryFailed; + std.posix.chdir(user_entry.home.?) catch return error.ChangeDirectoryFailed; // Signal to the session process to give up control on the 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 => try executeWaylandCmd(pwd.pw_shell.?, options, current_environment.cmd), - .shell => try executeShellCmd(pwd.pw_shell.?, options), + .wayland => try executeWaylandCmd(allocator, user_entry.shell.?, options, current_environment.cmd), + .shell => try executeShellCmd(allocator, user_entry.shell.?, options), .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}); - try executeX11Cmd(pwd.pw_shell.?, pwd.pw_dir.?, options, current_environment.cmd, vt); + try executeX11Cmd(allocator, user_entry.shell.?, user_entry.home.?, options, current_environment.cmd, vt); }, - .custom => try executeCustomCmd(pwd.pw_shell.?, options, current_environment.is_terminal, current_environment.cmd), + .custom => try executeCustomCmd(allocator, user_entry.shell.?, options, current_environment.is_terminal, current_environment.cmd), } } -fn initEnv(pwd: *interop.pwd.passwd, path_env: ?[:0]const u8) !void { - _ = interop.stdlib.setenv("HOME", pwd.pw_dir, 1); - _ = interop.stdlib.setenv("PWD", pwd.pw_dir, 1); - _ = interop.stdlib.setenv("SHELL", pwd.pw_shell, 1); - _ = interop.stdlib.setenv("USER", pwd.pw_name, 1); - _ = interop.stdlib.setenv("LOGNAME", pwd.pw_name, 1); +fn initEnv(allocator: std.mem.Allocator, entry: interop.UsernameEntry, path_env: ?[]const u8) !void { + if (entry.home) |home| { + try interop.setEnvironmentVariable(allocator, "HOME", home, true); + try interop.setEnvironmentVariable(allocator, "PWD", home, true); + } else return error.NoHomeDirectory; + + try interop.setEnvironmentVariable(allocator, "SHELL", entry.shell.?, true); + try interop.setEnvironmentVariable(allocator, "USER", entry.username.?, true); + try interop.setEnvironmentVariable(allocator, "LOGNAME", entry.username.?, true); if (path_env) |path| { - const status = interop.stdlib.setenv("PATH", path, 1); - if (status != 0) return error.SetPathFailed; + interop.setEnvironmentVariable(allocator, "PATH", path, true) catch return error.SetPathFailed; } } -fn setXdgEnv(tty_str: [:0]u8, environment: Environment) !void { - _ = interop.stdlib.setenv("XDG_SESSION_TYPE", switch (environment.display_server) { +fn setXdgEnv(allocator: std.mem.Allocator, tty_str: []u8, environment: Environment) !void { + try interop.setEnvironmentVariable(allocator, "XDG_SESSION_TYPE", switch (environment.display_server) { .wayland => "wayland", .shell => "tty", .xinitrc, .x11 => "x11", .custom => "unspecified", - }, 0); + }, false); // 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 = interop.unistd.getuid(); + const uid = std.posix.getuid(); var uid_buffer: [32]u8 = undefined; // No UID can be larger than this - const uid_str = try std.fmt.bufPrintZ(&uid_buffer, "/run/user/{d}", .{uid}); + const uid_str = try std.fmt.bufPrint(&uid_buffer, "/run/user/{d}", .{uid}); - _ = interop.stdlib.setenv("XDG_RUNTIME_DIR", uid_str, 0); + try interop.setEnvironmentVariable(allocator, "XDG_RUNTIME_DIR", uid_str, false); } - if (environment.xdg_desktop_names) |xdg_desktop_names| _ = interop.stdlib.setenv("XDG_CURRENT_DESKTOP", xdg_desktop_names, 0); - _ = interop.stdlib.setenv("XDG_SESSION_CLASS", "user", 0); - _ = interop.stdlib.setenv("XDG_SESSION_ID", "1", 0); - if (environment.xdg_session_desktop) |desktop_name| _ = interop.stdlib.setenv("XDG_SESSION_DESKTOP", desktop_name, 0); - _ = interop.stdlib.setenv("XDG_SEAT", "seat0", 0); - _ = interop.stdlib.setenv("XDG_VTNR", tty_str, 0); + 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( @@ -274,8 +260,8 @@ fn loginConv( if (status != interop.pam.PAM_SUCCESS) { // Memory is freed by pam otherwise allocator.free(response); - if (username != null) allocator.free(username.?); - if (password != null) allocator.free(password.?); + if (username) |str| allocator.free(str); + if (password) |str| allocator.free(str); } else { resp.?.* = response.ptr; } @@ -310,7 +296,7 @@ fn getXPid(display_num: u8) !i32 { return std.fmt.parseInt(i32, std.mem.trim(u8, buffer[0..written], " "), 10); } -fn createXauthFile(pwd: [:0]const u8) ![:0]const u8 { +fn createXauthFile(pwd: [:0]const u8) ![]const u8 { var xauth_buf: [100]u8 = undefined; var xauth_dir: [:0]const u8 = undefined; const xdg_rt_dir = std.posix.getenv("XDG_RUNTIME_DIR"); @@ -351,8 +337,8 @@ fn createXauthFile(pwd: [:0]const u8) ![:0]const u8 { const trimmed_xauth_dir = xauth_dir[0 .. i + 1]; var buf: [256]u8 = undefined; - const xauthority: [:0]u8 = try std.fmt.bufPrintZ(&buf, "{s}/{s}", .{ trimmed_xauth_dir, xauth_file }); - const file = try std.fs.createFileAbsoluteZ(xauthority, .{}); + const xauthority: []u8 = try std.fmt.bufPrint(&buf, "{s}/{s}", .{ trimmed_xauth_dir, xauth_file }); + const file = try std.fs.createFileAbsolute(xauthority, .{}); file.close(); return xauthority; @@ -368,13 +354,13 @@ fn mcookie() [Md5.digest_length * 2]u8 { return std.fmt.bytesToHex(&out, .lower); } -fn xauth(display_name: [:0]u8, shell: [*:0]const u8, pw_dir: [*:0]const u8, options: AuthOptions) !void { +fn xauth(allocator: std.mem.Allocator, display_name: []u8, shell: [*:0]const u8, pw_dir: [*:0]const u8, options: AuthOptions) !void { var pwd_buf: [100]u8 = undefined; const pwd = try std.fmt.bufPrintZ(&pwd_buf, "{s}", .{pw_dir}); const xauthority = try createXauthFile(pwd); - _ = interop.stdlib.setenv("XAUTHORITY", xauthority, 1); - _ = interop.stdlib.setenv("DISPLAY", display_name, 1); + try interop.setEnvironmentVariable(allocator, "XAUTHORITY", xauthority, true); + try interop.setEnvironmentVariable(allocator, "DISPLAY", display_name, true); const magic_cookie = mcookie(); @@ -391,40 +377,53 @@ fn xauth(display_name: [:0]u8, shell: [*:0]const u8, pw_dir: [*:0]const u8, opti if (status.status != 0) return error.XauthFailed; } -fn executeShellCmd(shell: [*:0]const u8, options: AuthOptions) !void { +fn executeShellCmd(allocator: std.mem.Allocator, shell: []const u8, options: AuthOptions) !void { // We don't want to redirect stdout and stderr in a shell session + const shell_z = try allocator.dupeZ(u8, 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 "", shell }); - const args = [_:null]?[*:0]const u8{ shell, "-c", cmd_str }; - return std.posix.execveZ(shell, &args, std.c.environ); + const args = [_:null]?[*:0]const u8{ shell_z, "-c", cmd_str }; + return std.posix.execveZ(shell_z, &args, std.c.environ); } -fn executeWaylandCmd(shell: [*:0]const u8, options: AuthOptions, desktop_cmd: []const u8) !void { +fn executeWaylandCmd(allocator: std.mem.Allocator, shell: []const u8, options: AuthOptions, desktop_cmd: []const u8) !void { var maybe_log_file: ?std.fs.File = null; if (options.session_log) |log_path| { maybe_log_file = try redirectStandardStreams(log_path, true); } defer if (maybe_log_file) |log_file| log_file.close(); + const shell_z = try allocator.dupeZ(u8, 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 "", desktop_cmd }); - const args = [_:null]?[*:0]const u8{ shell, "-c", cmd_str }; - return std.posix.execveZ(shell, &args, std.c.environ); + const args = [_:null]?[*:0]const u8{ shell_z, "-c", cmd_str }; + return std.posix.execveZ(shell_z, &args, std.c.environ); } -fn executeX11Cmd(shell: [*:0]const u8, pw_dir: [*:0]const u8, options: AuthOptions, desktop_cmd: []const u8, vt: []const u8) !void { +fn executeX11Cmd(allocator: std.mem.Allocator, shell: []const u8, home: []const u8, options: AuthOptions, desktop_cmd: []const u8, vt: []const u8) !void { const display_num = try getFreeDisplay(); var buf: [5]u8 = undefined; - const display_name = try std.fmt.bufPrintZ(&buf, ":{d}", .{display_num}); - try xauth(display_name, shell, pw_dir, options); + const display_name = try std.fmt.bufPrint(&buf, ":{d}", .{display_num}); + + const shell_z = try allocator.dupeZ(u8, shell); + defer allocator.free(shell_z); + + const home_z = try allocator.dupeZ(u8, home); + defer allocator.free(home_z); + + try xauth(allocator, display_name, shell_z, home_z, options); 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); - const args = [_:null]?[*:0]const u8{ shell, "-c", cmd_str }; - std.posix.execveZ(shell, &args, std.c.environ) catch {}; + 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); } @@ -446,8 +445,8 @@ fn executeX11Cmd(shell: [*:0]const u8, pw_dir: [*:0]const u8, options: AuthOptio 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 args = [_:null]?[*:0]const u8{ shell, "-c", cmd_str }; - std.posix.execveZ(shell, &args, std.c.environ) catch {}; + 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); } @@ -467,11 +466,10 @@ fn executeX11Cmd(shell: [*:0]const u8, pw_dir: [*:0]const u8, options: AuthOptio std.Thread.sleep(std.time.ns_per_s * 1); // Wait 1 second before sending SIGKILL std.posix.kill(x_pid, std.posix.SIG.KILL) catch return; - var status: c_int = 0; - _ = std.c.waitpid(x_pid, &status, 0); + _ = std.posix.waitpid(x_pid, 0); } -fn executeCustomCmd(shell: [*:0]const u8, options: AuthOptions, is_terminal: bool, exec_cmd: []const u8) !void { +fn executeCustomCmd(allocator: std.mem.Allocator, shell: []const u8, options: AuthOptions, is_terminal: bool, exec_cmd: []const u8) !void { var maybe_log_file: ?std.fs.File = null; if (!is_terminal) { // For custom desktop entries, the "Terminal" value here determines if @@ -483,10 +481,13 @@ fn executeCustomCmd(shell: [*:0]const u8, options: AuthOptions, is_terminal: boo } defer if (maybe_log_file) |log_file| log_file.close(); + const shell_z = try allocator.dupeZ(u8, 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 }); - const args = [_:null]?[*:0]const u8{ shell, "-c", cmd_str }; - return std.posix.execveZ(shell, &args, std.c.environ); + const args = [_:null]?[*:0]const u8{ shell_z, "-c", cmd_str }; + return std.posix.execveZ(shell_z, &args, std.c.environ); } fn redirectStandardStreams(session_log: []const u8, create: bool) !std.fs.File { @@ -498,7 +499,7 @@ fn redirectStandardStreams(session_log: []const u8, create: bool) !std.fs.File { return log_file; } -fn addUtmpEntry(entry: *Utmp, username: [*:0]const u8, pid: c_int) !void { +fn addUtmpEntry(entry: *Utmp, username: []const u8, pid: c_int) !void { entry.ut_type = utmp.USER_PROCESS; entry.ut_pid = pid; @@ -520,12 +521,11 @@ fn addUtmpEntry(entry: *Utmp, username: [*:0]const u8, pid: c_int) !void { host[0] = 0; entry.ut_host = host; - var tv: interop.system_time.timeval = undefined; - _ = interop.system_time.gettimeofday(&tv, null); + const time = try interop.getTimeOfDay(); entry.ut_tv = .{ - .tv_sec = @intCast(tv.tv_sec), - .tv_usec = @intCast(tv.tv_usec), + .tv_sec = @intCast(time.seconds), + .tv_usec = @intCast(time.microseconds), }; entry.ut_addr_v6[0] = 0; diff --git a/src/bigclock.zig b/src/bigclock.zig index a1c860a..63aa02a 100644 --- a/src/bigclock.zig +++ b/src/bigclock.zig @@ -11,13 +11,11 @@ pub const WIDTH = Lang.WIDTH; pub const HEIGHT = Lang.HEIGHT; pub const SIZE = Lang.SIZE; -pub fn clockCell(animate: bool, char: u8, fg: u32, bg: u32, bigclock: Bigclock) [SIZE]Cell { +pub fn clockCell(animate: bool, char: u8, fg: u32, bg: u32, bigclock: Bigclock) ![SIZE]Cell { var cells: [SIZE]Cell = undefined; - var tv: interop.system_time.timeval = undefined; - _ = interop.system_time.gettimeofday(&tv, null); - - const clock_chars = toBigNumber(if (animate and char == ':' and @divTrunc(tv.tv_usec, 500000) != 0) ' ' else char, bigclock); + 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; diff --git a/src/bigclock/Lang.zig b/src/bigclock/Lang.zig index d11aac9..3e3be4e 100644 --- a/src/bigclock/Lang.zig +++ b/src/bigclock/Lang.zig @@ -1,10 +1,10 @@ -const builtin = @import("builtin"); +const interop = @import("../interop.zig"); pub const WIDTH = 5; pub const HEIGHT = 5; pub const SIZE = WIDTH * HEIGHT; -pub const X: u32 = if (builtin.os.tag == .linux or builtin.os.tag.isBSD()) 0x2593 else '#'; +pub const X: u32 = if (interop.supportsUnicode()) 0x2593 else '#'; pub const O: u32 = 0; // zig fmt: off diff --git a/src/config/Config.zig b/src/config/Config.zig index ef91cb1..89919a6 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -61,7 +61,7 @@ margin_box_h: u8 = 2, margin_box_v: u8 = 1, min_refresh_delta: u16 = 5, numlock: bool = false, -path: ?[:0]const u8 = "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", +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, diff --git a/src/interop.zig b/src/interop.zig index 68d4332..c04f458 100644 --- a/src/interop.zig +++ b/src/interop.zig @@ -2,6 +2,19 @@ const std = @import("std"); const builtin = @import("builtin"); const Allocator = std.mem.Allocator; +pub const TimeOfDay = struct { + seconds: i64, + microseconds: i64, +}; + +pub const UsernameEntry = struct { + username: ?[]const u8, + uid: std.posix.uid_t, + gid: std.posix.gid_t, + home: ?[]const u8, + shell: ?[]const u8, +}; + pub const termbox = @import("termbox2"); pub const pam = @cImport({ @@ -17,44 +30,44 @@ pub const xcb = @cImport({ @cInclude("xcb/xcb.h"); }); -pub const unistd = @cImport({ - @cInclude("unistd.h"); -}); - -pub const time = @cImport({ - @cInclude("time.h"); -}); - -pub const system_time = @cImport({ - @cInclude("sys/time.h"); -}); - -pub const stdlib = @cImport({ - @cInclude("stdlib.h"); -}); - -pub const pwd = @cImport({ +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("login_cap.h"); }); -pub const grp = @cImport({ +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"); +}); + // BSD-specific headers -pub const kbio = @cImport({ +const kbio = @cImport({ @cInclude("sys/kbio.h"); }); // Linux-specific headers -pub const kd = @cImport({ +const kd = @cImport({ @cInclude("sys/kd.h"); }); -pub const vt = @cImport({ +const vt = @cImport({ @cInclude("sys/vt.h"); }); @@ -65,6 +78,10 @@ const set_led_state = if (builtin.os.tag.isBSD()) kbio.KDSETLED else kd.KDSKBLED const numlock_led = if (builtin.os.tag.isBSD()) kbio.LED_NUM else kd.K_NUMLOCK; const capslock_led = if (builtin.os.tag.isBSD()) kbio.LED_CAP else kd.K_CAPSLOCK; +pub fn supportsUnicode() bool { + return builtin.os.tag == .linux or builtin.os.tag.isBSD(); +} + pub fn timeAsString(buf: [:0]u8, format: [:0]const u8) []u8 { const timer = std.time.timestamp(); const tm_info = time.localtime(&timer); @@ -73,11 +90,23 @@ pub fn timeAsString(buf: [:0]u8, format: [:0]const u8) []u8 { return buf[0..len]; } +pub fn getTimeOfDay() !TimeOfDay { + var tv: system_time.timeval = undefined; + const status = system_time.gettimeofday(&tv, null); + + if (status != 0) return error.FailedToGetTimeOfDay; + + return .{ + .seconds = @intCast(tv.tv_sec), + .microseconds = @intCast(tv.tv_usec), + }; +} + pub fn switchTty(tty: u8) !void { - var status = std.c.ioctl(std.c.STDIN_FILENO, vt.VT_ACTIVATE, tty); + var status = std.c.ioctl(std.posix.STDIN_FILENO, vt.VT_ACTIVATE, tty); if (status != 0) return error.FailedToActivateTty; - status = std.c.ioctl(std.c.STDIN_FILENO, vt.VT_WAITACTIVE, tty); + status = std.c.ioctl(std.posix.STDIN_FILENO, vt.VT_WAITACTIVE, tty); if (status != 0) return error.FailedToWaitForActiveTty; } @@ -86,7 +115,7 @@ pub fn getLockState() !struct { capslock: bool, } { var led: LedState = undefined; - const status = std.c.ioctl(std.c.STDIN_FILENO, get_led_state, &led); + const status = std.c.ioctl(std.posix.STDIN_FILENO, get_led_state, &led); if (status != 0) return error.FailedToGetLockState; return .{ @@ -97,7 +126,7 @@ pub fn getLockState() !struct { pub fn setNumlock(val: bool) !void { var led: LedState = undefined; - var status = std.c.ioctl(std.c.STDIN_FILENO, get_led_state, &led); + var status = std.c.ioctl(std.posix.STDIN_FILENO, get_led_state, &led); if (status != 0) return error.FailedToGetNumlock; const numlock = (led & numlock_led) != 0; @@ -106,3 +135,80 @@ pub fn setNumlock(val: bool) !void { if (status != 0) return error.FailedToSetNumlock; } } + +pub fn setUserContext(allocator: std.mem.Allocator, entry: UsernameEntry) !void { + const username_z = try allocator.dupeZ(u8, entry.username.?); + defer allocator.free(username_z); + + if (builtin.os.tag == .freebsd) { + // FreeBSD has initgroups() in unistd + const status = unistd.initgroups(username_z.ptr, @intCast(entry.gid)); + if (status != 0) return error.GroupInitializationFailed; + + // FreeBSD sets the GID and UID with setusercontext() + // TODO + const result = pwd.setusercontext(null, entry, @intCast(entry.uid), pwd.LOGIN_SETALL); + if (result != 0) return error.SetUserUidFailed; + } else { + const status = grp.initgroups(username_z.ptr, @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; + } +} + +pub fn setUserShell(entry: *UsernameEntry) void { + unistd.setusershell(); + + const shell = unistd.getusershell(); + entry.shell = shell[0..std.mem.len(shell)]; + + unistd.endusershell(); +} + +pub fn setEnvironmentVariable(allocator: std.mem.Allocator, name: []const u8, value: []const u8, replace: bool) !void { + const name_z = try allocator.dupeZ(u8, name); + defer allocator.free(name_z); + + const value_z = try allocator.dupeZ(u8, value); + defer allocator.free(value_z); + + const status = stdlib.setenv(name_z.ptr, value_z.ptr, @intFromBool(replace)); + if (status != 0) return error.SetEnvironmentVariableFailed; +} + +pub fn putEnvironmentVariable(name_and_value: [*c]u8) !void { + const status = stdlib.putenv(name_and_value); + if (status != 0) return error.PutEnvironmentVariableFailed; +} + +pub fn getNextUsernameEntry() ?UsernameEntry { + const entry = pwd.getpwent(); + if (entry == null) return null; + + return .{ + .username = if (entry.*.pw_name) |name| name[0..std.mem.len(name)] else null, + .uid = @intCast(entry.*.pw_uid), + .gid = @intCast(entry.*.pw_gid), + .home = if (entry.*.pw_dir) |dir| dir[0..std.mem.len(dir)] else null, + .shell = if (entry.*.pw_shell) |shell| shell[0..std.mem.len(shell)] else null, + }; +} + +pub fn getUsernameEntry(username: [:0]const u8) ?UsernameEntry { + const entry = pwd.getpwnam(username); + if (entry == null) return null; + + return .{ + .username = if (entry.*.pw_name) |name| name[0..std.mem.len(name)] else null, + .uid = @intCast(entry.*.pw_uid), + .gid = @intCast(entry.*.pw_gid), + .home = if (entry.*.pw_dir) |dir| dir[0..std.mem.len(dir)] else null, + .shell = if (entry.*.pw_shell) |shell| shell[0..std.mem.len(shell)] else null, + }; +} + +pub fn closePasswordDatabase() void { + pwd.endpwent(); +} diff --git a/src/main.zig b/src/main.zig index 424a3a2..e91ac04 100644 --- a/src/main.zig +++ b/src/main.zig @@ -31,7 +31,6 @@ const Ini = ini.Ini; const DisplayServer = enums.DisplayServer; const Entry = Environment.Entry; const termbox = interop.termbox; -const unistd = interop.unistd; const temporary_allocator = std.heap.page_allocator; const ly_top_str = "Ly version " ++ build_options.version; @@ -85,8 +84,7 @@ pub fn main() !void { defer _ = gpa.deinit(); // Allows stopping an animation after some time - var tv_zero: interop.system_time.timeval = undefined; - _ = interop.system_time.gettimeofday(&tv_zero, null); + const time_start = try interop.getTimeOfDay(); var animation_timed_out: bool = false; const allocator = gpa.allocator(); @@ -547,7 +545,8 @@ pub fn main() !void { const clock_str = interop.timeAsString(&clock_buf, format); for (clock_str, 0..) |c, i| { - const clock_cell = bigclock.clockCell(animate, c, buffer.fg, buffer.bg, config.bigclock); + // 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); } } @@ -682,24 +681,21 @@ pub fn main() !void { if (animate and !animation_timed_out) { timeout = config.min_refresh_delta; - // check how long we have been running so we can turn off the animation - var tv: interop.system_time.timeval = undefined; - _ = interop.system_time.gettimeofday(&tv, null); + // 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 tv.tv_sec - tv_zero.tv_sec > config.animation_timeout_sec) { + if (config.animation_timeout_sec > 0 and time.seconds - time_start.seconds > config.animation_timeout_sec) { animation_timed_out = true; animation.deinit(); } } else if (config.bigclock != .none and config.clock == null) { - var tv: interop.system_time.timeval = undefined; - _ = interop.system_time.gettimeofday(&tv, null); + const time = try interop.getTimeOfDay(); - timeout = @intCast((60 - @rem(tv.tv_sec, 60)) * 1000 - @divTrunc(tv.tv_usec, 1000) + 1); + timeout = @intCast((60 - @rem(time.seconds, 60)) * 1000 - @divTrunc(time.microseconds, 1000) + 1); } else if (config.clock != null or auth_fails >= config.auth_fails) { - var tv: interop.system_time.timeval = undefined; - _ = interop.system_time.gettimeofday(&tv, null); + const time = try interop.getTimeOfDay(); - timeout = @intCast(1000 - @divTrunc(tv.tv_usec, 1000) + 1); + timeout = @intCast(1000 - @divTrunc(time.microseconds, 1000) + 1); } const event_error = if (timeout == -1) termbox.tb_poll_event(&event) else termbox.tb_peek_event(&event, timeout); @@ -864,7 +860,7 @@ pub fn main() !void { }; std.posix.sigaction(std.posix.SIG.CHLD, &tty_control_transfer_act, null); - auth.authenticate(auth_options, current_environment, login_text, password_text) catch |err| { + auth.authenticate(allocator, auth_options, current_environment, login_text, password_text) catch |err| { shared_err.writeError(err); std.process.exit(1); }; @@ -1016,7 +1012,7 @@ fn crawl(session: *Session, lang: Lang, path: []const u8, display_server: Displa // Prepare the XDG_CURRENT_DESKTOP environment variable here const entry = entry_ini.data.@"Desktop Entry"; - var maybe_xdg_desktop_names: ?[:0]const u8 = null; + var maybe_xdg_desktop_names: ?[]const u8 = null; if (entry.DesktopNames) |desktop_names| { for (desktop_names) |*c| { if (c.* == ';') c.* = ':'; @@ -1024,13 +1020,10 @@ fn crawl(session: *Session, lang: Lang, path: []const u8, display_server: Displa maybe_xdg_desktop_names = desktop_names; } - const maybe_session_desktop = if (maybe_xdg_session_desktop) |xdg_session_desktop| try session.label.allocator.dupeZ(u8, xdg_session_desktop) else null; - errdefer if (maybe_session_desktop) |session_desktop| session.label.allocator.free(session_desktop); - try session.addEnvironment(.{ .entry_ini = entry_ini, .name = entry.Name, - .xdg_session_desktop = maybe_session_desktop, + .xdg_session_desktop = maybe_xdg_session_desktop, .xdg_desktop_names = maybe_xdg_desktop_names, .cmd = entry.Exec, .specifier = switch (display_server) { @@ -1049,24 +1042,20 @@ fn getAllUsernames(allocator: std.mem.Allocator, login_defs_path: []const u8) !S const uid_range = try getUserIdRange(allocator, login_defs_path); var usernames: StringList = .empty; - var maybe_entry = interop.pwd.getpwent(); - - while (maybe_entry != null) { - const entry = maybe_entry.*; + 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.pw_uid >= uid_range.uid_min and entry.pw_uid <= uid_range.uid_max or entry.pw_uid == 0) { - const pw_name_slice = entry.pw_name[0..std.mem.len(entry.pw_name)]; - const username = try allocator.dupe(u8, pw_name_slice); - + if (entry.uid >= uid_range.uid_min and entry.uid <= uid_range.uid_max or entry.uid == 0 and entry.username != null) { + const username = try allocator.dupe(u8, entry.username.?); try usernames.append(allocator, username); } - maybe_entry = interop.pwd.getpwent(); + maybe_entry = interop.getNextUsernameEntry(); } - interop.pwd.endpwent(); + interop.closePasswordDatabase(); return usernames; } @@ -1086,9 +1075,9 @@ fn getUserIdRange(allocator: std.mem.Allocator, login_defs_path: []const u8) !Ui 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.c.uid_t, "UID_MIN", trimmed_line); + uid_range.uid_min = try parseValue(std.posix.uid_t, "UID_MIN", trimmed_line); } else if (std.mem.startsWith(u8, trimmed_line, "UID_MAX")) { - uid_range.uid_max = try parseValue(std.c.uid_t, "UID_MAX", trimmed_line); + uid_range.uid_max = try parseValue(std.posix.uid_t, "UID_MAX", trimmed_line); } } diff --git a/src/tui/TerminalBuffer.zig b/src/tui/TerminalBuffer.zig index 4987208..c5e15d9 100644 --- a/src/tui/TerminalBuffer.zig +++ b/src/tui/TerminalBuffer.zig @@ -1,5 +1,4 @@ const std = @import("std"); -const builtin = @import("builtin"); const interop = @import("../interop.zig"); const Cell = @import("Cell.zig"); @@ -82,7 +81,7 @@ pub fn init(options: InitOptions, labels_max_length: usize, random: Random) Term .fg = options.fg, .bg = options.bg, .border_fg = options.border_fg, - .box_chars = if (builtin.os.tag == .linux or builtin.os.tag.isBSD()) .{ + .box_chars = if (interop.supportsUnicode()) .{ .left_up = 0x250C, .left_down = 0x2514, .right_up = 0x2510, diff --git a/src/tui/components/Session.zig b/src/tui/components/Session.zig index 2656025..3c39a29 100644 --- a/src/tui/components/Session.zig +++ b/src/tui/components/Session.zig @@ -23,7 +23,6 @@ pub fn init(allocator: Allocator, buffer: *TerminalBuffer) Session { pub fn deinit(self: *Session) void { for (self.label.list.items) |*environment| { if (environment.entry_ini) |*entry_ini| entry_ini.deinit(); - if (environment.xdg_session_desktop) |session_desktop| self.label.allocator.free(session_desktop); } self.label.deinit(); From 6d7dbb9f277e13ed93f6357fc61aea08344aa450 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Fri, 29 Aug 2025 00:07:32 +0200 Subject: [PATCH 261/530] Fix typo & remove unused import Signed-off-by: AnErrupTion --- res/config.ini | 2 +- src/tui/components/Session.zig | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/res/config.ini b/res/config.ini index 15844e5..dc37004 100644 --- a/res/config.ini +++ b/res/config.ini @@ -67,7 +67,7 @@ border_fg = 0x00FFFFFF # If set to null, none will be shown box_title = null -# Brightness increase command +# Brightness decrease command brightness_down_cmd = $PREFIX_DIRECTORY/bin/brightnessctl -q s 10%- # Brightness decrease key, or null to disable diff --git a/src/tui/components/Session.zig b/src/tui/components/Session.zig index 3c39a29..527b978 100644 --- a/src/tui/components/Session.zig +++ b/src/tui/components/Session.zig @@ -1,13 +1,11 @@ const std = @import("std"); const TerminalBuffer = @import("../TerminalBuffer.zig"); const enums = @import("../../enums.zig"); -const ini = @import("zigini"); const Environment = @import("../../Environment.zig"); const generic = @import("generic.zig"); const Allocator = std.mem.Allocator; const DisplayServer = enums.DisplayServer; -const Ini = ini.Ini; const EnvironmentLabel = generic.CyclableLabel(Environment); const Session = @This(); From 69d39dc035de58ec43db76c3f9c58aa8f4f62a52 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Fri, 29 Aug 2025 00:32:49 +0200 Subject: [PATCH 262/530] Remove config.load config.save already makes it redundant. Besides, who would want to save the current username & session, but not want to load it at the next boot? Signed-off-by: AnErrupTion --- res/config.ini | 5 +---- src/config/Config.zig | 1 - src/main.zig | 6 +++--- 3 files changed, 4 insertions(+), 8 deletions(-) diff --git a/res/config.ini b/res/config.ini index dc37004..3efc716 100644 --- a/res/config.ini +++ b/res/config.ini @@ -200,9 +200,6 @@ input_len = 34 # Available languages are found in $CONFIG_DIRECTORY/ly/lang/ lang = en -# Load the saved desktop and username -load = true - # Command executed when logging in # If null, no command will be executed # Important: the code itself must end with `exec "$@"` in order to launch the session! @@ -243,7 +240,7 @@ restart_cmd = /sbin/shutdown -r now # Specifies the key used for restart (F1-F12) restart_key = F2 -# Save the current desktop and login as defaults +# Save the current desktop and login as defaults, and load them on startup save = true # Service name (set to ly to use the provided pam config file) diff --git a/src/config/Config.zig b/src/config/Config.zig index 89919a6..26eb29a 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -52,7 +52,6 @@ hide_key_hints: bool = false, initial_info_text: ?[]const u8 = null, input_len: u8 = 34, lang: []const u8 = "en", -load: bool = true, login_cmd: ?[]const u8 = null, login_defs_path: []const u8 = "/etc/login.defs", logout_cmd: ?[]const u8 = null, diff --git a/src/main.zig b/src/main.zig index e91ac04..76ebde3 100644 --- a/src/main.zig +++ b/src/main.zig @@ -164,7 +164,7 @@ pub fn main() !void { .comment_characters = comment_characters, }) catch Lang{}; - if (config.load) { + if (config.save) { save_path = try std.fmt.allocPrint(allocator, "{s}{s}save.ini", .{ s, trailing_slash }); save_path_alloc = true; @@ -197,7 +197,7 @@ pub fn main() !void { .comment_characters = comment_characters, }) catch Lang{}; - if (config.load) { + if (config.save) { var user_buf: [32]u8 = undefined; save = save_ini.readFileToStruct(save_path, .{ .fieldHandler = null, @@ -383,7 +383,7 @@ pub fn main() !void { var insert_mode = !config.vi_mode or config.vi_default_mode == .insert; // Load last saved username and desktop selection, if any - if (config.load) { + if (config.save) { if (save.user) |user| { // Find user with saved name, and switch over to it // If it doesn't exist (anymore), we don't change the value From aa0222948a5819b6493c1f03ea958625afc5a768 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Fri, 29 Aug 2025 00:35:11 +0200 Subject: [PATCH 263/530] Update config migrator Signed-off-by: AnErrupTion --- src/config/migrator.zig | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/src/config/migrator.zig b/src/config/migrator.zig index c3b7266..bdbfe67 100644 --- a/src/config/migrator.zig +++ b/src/config/migrator.zig @@ -37,6 +37,7 @@ const removed_properties = [_][]const u8{ "x_cmd_setup", "wayland_cmd", "console_dev", + "load", }; var temporary_allocator = std.heap.page_allocator; @@ -46,14 +47,11 @@ pub var auto_eight_colors: bool = true; pub var maybe_animate: ?bool = null; pub var maybe_save_file: ?[]const u8 = null; -pub var mapped_config_fields = false; - pub fn configFieldHandler(_: std.mem.Allocator, field: ini.IniField) ?ini.IniField { if (std.mem.eql(u8, field.key, "animate")) { // The option doesn't exist anymore, but we save its value for "animation" maybe_animate = std.mem.eql(u8, field.value, "true"); - mapped_config_fields = true; return null; } @@ -69,7 +67,6 @@ pub fn configFieldHandler(_: std.mem.Allocator, field: ini.IniField) ?ini.IniFie else => "none", }; - mapped_config_fields = true; return mapped_field; } @@ -101,7 +98,6 @@ pub fn configFieldHandler(_: std.mem.Allocator, field: ini.IniField) ?ini.IniFie var mapped_field = field; mapped_field.key = "clear_password"; - mapped_config_fields = true; return mapped_field; } @@ -117,7 +113,6 @@ pub fn configFieldHandler(_: std.mem.Allocator, field: ini.IniField) ?ini.IniFie else => "login", }; - mapped_config_fields = true; return mapped_field; } @@ -125,14 +120,12 @@ pub fn configFieldHandler(_: std.mem.Allocator, field: ini.IniField) ?ini.IniFie // 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; - mapped_config_fields = true; return null; } inline for (removed_properties) |property| { if (std.mem.eql(u8, field.key, property)) { // The options don't exist anymore - mapped_config_fields = true; return null; } } @@ -144,10 +137,8 @@ pub fn configFieldHandler(_: std.mem.Allocator, field: ini.IniField) ?ini.IniFie if (std.mem.eql(u8, field.value, "true")) { mapped_field.value = "en"; - mapped_config_fields = true; } else if (std.mem.eql(u8, field.value, "false")) { mapped_field.value = "none"; - mapped_config_fields = true; } return mapped_field; From f988bd334b3894141053b64deacea792b4f18dce Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Fri, 29 Aug 2025 00:45:28 +0200 Subject: [PATCH 264/530] Update termbox2 Signed-off-by: AnErrupTion --- build.zig.zon | 4 ++-- src/tui/TerminalBuffer.zig | 16 ++++++++++------ 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/build.zig.zon b/build.zig.zon index dad2165..138fbda 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -13,8 +13,8 @@ .hash = "zigini-0.3.2-BSkB7aVHAADhxwo0aEdWtNzaVXer3d8RwXMuZd-q-spO", }, .termbox2 = .{ - .url = "git+https://github.com/AnErrupTion/termbox2?ref=get_cell#e975d250ee6567773400e9d5b0b5c2f175349c57", - .hash = "N-V-__8AAMruBACJ7xT-O64hnS7lNeTiZQMketHdkHKrR1A8", + .url = "git+https://github.com/AnErrupTion/termbox2?ref=master#290ac6b8225aacfd16851224682b851b65fcb918", + .hash = "N-V-__8AAGcUBQAa5vov1Yi_9AXEffFQ1e2KsXaK4dgygRKq", }, }, .paths = .{""}, diff --git a/src/tui/TerminalBuffer.zig b/src/tui/TerminalBuffer.zig index c5e15d9..f09877d 100644 --- a/src/tui/TerminalBuffer.zig +++ b/src/tui/TerminalBuffer.zig @@ -117,24 +117,28 @@ pub fn cascade(self: TerminalBuffer) bool { while (y > 0) : (y -= 1) { for (0..self.width) |x| { - var cell: termbox.tb_cell = undefined; - var cell_under: termbox.tb_cell = undefined; + 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); - const char: u8 = @truncate(cell.ch); + // 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); + 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); + _ = 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); } } From 1ee8010c2439194212b1b23fbfd96d818d7a5499 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Fri, 29 Aug 2025 02:18:11 +0200 Subject: [PATCH 265/530] Fix dinit & s6 service + don't hardcode paths in runit service Signed-off-by: AnErrupTion --- res/ly-dinit | 4 ++-- res/ly-runit-service/conf | 2 +- res/ly-s6/run | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/res/ly-dinit b/res/ly-dinit index 016929a..53ef1dd 100644 --- a/res/ly-dinit +++ b/res/ly-dinit @@ -1,8 +1,8 @@ type = process restart = true smooth-recovery = true -command = $PREFIX_DIRECTORY/bin/$EXE_NAME +command = $PREFIX_DIRECTORY/bin/$EXECUTABLE_NAME depends-on = login.target termsignal = HUP -# ly needs access to the console while loginready already occupies it +# ly needs access to the console while login.target already occupies it options = shares-console diff --git a/res/ly-runit-service/conf b/res/ly-runit-service/conf index 76ceb87..fca1c76 100644 --- a/res/ly-runit-service/conf +++ b/res/ly-runit-service/conf @@ -8,5 +8,5 @@ fi BAUD_RATE=38400 TERM_NAME=linux -auxtty=$(/bin/cat $CONFIG_DIRECTORY/ly/config.ini 2>/dev/null 1| /bin/sed -n 's/\(^[[:space:]]*tty[[:space:]]*=[[:space:]]*\)\([[:digit:]][[:digit:]]*\)\(.*\)/\2/p') +auxtty=$(cat $CONFIG_DIRECTORY/ly/config.ini 2>/dev/null 1| sed -n 's/\(^[[:space:]]*tty[[:space:]]*=[[:space:]]*\)\([[:digit:]][[:digit:]]*\)\(.*\)/\2/p') TTY=tty${auxtty:-$DEFAULT_TTY} diff --git a/res/ly-s6/run b/res/ly-s6/run index e63826f..1b8aa0d 100644 --- a/res/ly-s6/run +++ b/res/ly-s6/run @@ -1,2 +1,2 @@ #!/bin/execlineb -P -exec agetty -L -8 -n -l $PREFIX_DIRECTORY/bin/$EXE_NAME tty$DEFAULT_TTY 115200 +exec agetty -L -8 -n -l $PREFIX_DIRECTORY/bin/$EXECUTABLE_NAME tty$DEFAULT_TTY 115200 From a7d6b06d2105f3240373870fe300fffd93bec3ba Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Fri, 29 Aug 2025 09:28:29 +0200 Subject: [PATCH 266/530] Add partial logging in authentication process (closes #822) Signed-off-by: AnErrupTion --- src/auth.zig | 44 ++++++++++++++++++++++++++++---------------- src/main.zig | 2 +- 2 files changed, 29 insertions(+), 17 deletions(-) diff --git a/src/auth.zig b/src/auth.zig index ebb0505..0c5d5a3 100644 --- a/src/auth.zig +++ b/src/auth.zig @@ -33,7 +33,7 @@ pub fn sessionSignalHandler(i: c_int) callconv(.c) void { if (child_pid > 0) _ = std.c.kill(child_pid, i); } -pub fn authenticate(allocator: std.mem.Allocator, options: AuthOptions, current_environment: Environment, login: [:0]const u8, password: [:0]const u8) !void { +pub fn authenticate(allocator: std.mem.Allocator, log_writer: *std.Io.Writer, options: AuthOptions, current_environment: Environment, login: [:0]const u8, password: [:0]const u8) !void { var tty_buffer: [3]u8 = undefined; const tty_str = try std.fmt.bufPrint(&tty_buffer, "{d}", .{options.tty}); @@ -91,7 +91,9 @@ pub fn authenticate(allocator: std.mem.Allocator, options: AuthOptions, current_ child_pid = try std.posix.fork(); if (child_pid == 0) { - startSession(allocator, options, tty_str, user_entry, handle, current_environment) catch |e| { + try log_writer.writeAll("starting session"); + + startSession(log_writer, allocator, options, tty_str, user_entry, handle, current_environment) catch |e| { shared_err.writeError(e); std.process.exit(1); }; @@ -126,6 +128,7 @@ pub fn authenticate(allocator: std.mem.Allocator, options: AuthOptions, current_ } fn startSession( + log_writer: *std.Io.Writer, allocator: std.mem.Allocator, options: AuthOptions, tty_str: []u8, @@ -157,14 +160,14 @@ fn startSession( // Execute what the user requested switch (current_environment.display_server) { - .wayland => try executeWaylandCmd(allocator, user_entry.shell.?, options, current_environment.cmd), + .wayland => try executeWaylandCmd(log_writer, allocator, user_entry.shell.?, options, current_environment.cmd), .shell => try executeShellCmd(allocator, user_entry.shell.?, options), .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}); - try executeX11Cmd(allocator, user_entry.shell.?, user_entry.home.?, options, current_environment.cmd, vt); + try executeX11Cmd(log_writer, allocator, user_entry.shell.?, user_entry.home.?, options, current_environment.cmd, vt); }, - .custom => try executeCustomCmd(allocator, user_entry.shell.?, options, current_environment.is_terminal, current_environment.cmd), + .custom => try executeCustomCmd(log_writer, allocator, user_entry.shell.?, options, current_environment.is_terminal, current_environment.cmd), } } @@ -354,7 +357,7 @@ fn mcookie() [Md5.digest_length * 2]u8 { return std.fmt.bytesToHex(&out, .lower); } -fn xauth(allocator: std.mem.Allocator, display_name: []u8, shell: [*:0]const u8, pw_dir: [*:0]const u8, options: AuthOptions) !void { +fn xauth(log_writer: *std.Io.Writer, allocator: std.mem.Allocator, display_name: []u8, shell: [*:0]const u8, pw_dir: [*:0]const u8, options: AuthOptions) !void { var pwd_buf: [100]u8 = undefined; const pwd = try std.fmt.bufPrintZ(&pwd_buf, "{s}", .{pw_dir}); @@ -374,7 +377,10 @@ fn xauth(allocator: std.mem.Allocator, display_name: []u8, shell: [*:0]const u8, } const status = std.posix.waitpid(pid, 0); - if (status.status != 0) return error.XauthFailed; + if (status.status != 0) { + try log_writer.print("xauth command failed with status {d}", .{status.status}); + return error.XauthFailed; + } } fn executeShellCmd(allocator: std.mem.Allocator, shell: []const u8, options: AuthOptions) !void { @@ -389,10 +395,10 @@ fn executeShellCmd(allocator: std.mem.Allocator, shell: []const u8, options: Aut return std.posix.execveZ(shell_z, &args, std.c.environ); } -fn executeWaylandCmd(allocator: std.mem.Allocator, shell: []const u8, options: AuthOptions, desktop_cmd: []const u8) !void { +fn executeWaylandCmd(log_writer: *std.Io.Writer, allocator: std.mem.Allocator, shell: []const u8, options: AuthOptions, desktop_cmd: []const u8) !void { var maybe_log_file: ?std.fs.File = null; if (options.session_log) |log_path| { - maybe_log_file = try redirectStandardStreams(log_path, true); + maybe_log_file = try redirectStandardStreams(log_writer, log_path, true); } defer if (maybe_log_file) |log_file| log_file.close(); @@ -405,9 +411,9 @@ fn executeWaylandCmd(allocator: std.mem.Allocator, shell: []const u8, options: A return std.posix.execveZ(shell_z, &args, std.c.environ); } -fn executeX11Cmd(allocator: std.mem.Allocator, shell: []const u8, home: []const u8, options: AuthOptions, desktop_cmd: []const u8, vt: []const u8) !void { +fn executeX11Cmd(log_writer: *std.Io.Writer, allocator: std.mem.Allocator, shell: []const u8, home: []const u8, options: AuthOptions, desktop_cmd: []const u8, vt: []const u8) !void { const display_num = try getFreeDisplay(); - var buf: [5]u8 = undefined; + var buf: [4]u8 = undefined; const display_name = try std.fmt.bufPrint(&buf, ":{d}", .{display_num}); const shell_z = try allocator.dupeZ(u8, shell); @@ -416,7 +422,7 @@ fn executeX11Cmd(allocator: std.mem.Allocator, shell: []const u8, home: []const const home_z = try allocator.dupeZ(u8, home); defer allocator.free(home_z); - try xauth(allocator, display_name, shell_z, home_z, options); + try xauth(log_writer, allocator, display_name, shell_z, home_z, options); const pid = try std.posix.fork(); if (pid == 0) { @@ -469,14 +475,14 @@ fn executeX11Cmd(allocator: std.mem.Allocator, shell: []const u8, home: []const _ = std.posix.waitpid(x_pid, 0); } -fn executeCustomCmd(allocator: std.mem.Allocator, shell: []const u8, options: AuthOptions, is_terminal: bool, exec_cmd: []const u8) !void { +fn executeCustomCmd(log_writer: *std.Io.Writer, allocator: std.mem.Allocator, shell: []const u8, options: AuthOptions, is_terminal: bool, exec_cmd: []const u8) !void { 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| { - maybe_log_file = try redirectStandardStreams(log_path, true); + maybe_log_file = try redirectStandardStreams(log_writer, log_path, true); } } defer if (maybe_log_file) |log_file| log_file.close(); @@ -490,8 +496,14 @@ fn executeCustomCmd(allocator: std.mem.Allocator, shell: []const u8, options: Au return std.posix.execveZ(shell_z, &args, std.c.environ); } -fn redirectStandardStreams(session_log: []const u8, create: bool) !std.fs.File { - const log_file = if (create) (try std.fs.cwd().createFile(session_log, .{ .mode = 0o666 })) else (try std.fs.cwd().openFile(session_log, .{ .mode = .read_write })); +fn redirectStandardStreams(log_writer: *std.Io.Writer, session_log: []const u8, create: bool) !std.fs.File { + const log_file = if (create) (std.fs.cwd().createFile(session_log, .{ .mode = 0o666 }) catch |err| { + try log_writer.print("failed to create new session log file: {s}\n", .{@errorName(err)}); + return err; + }) else (std.fs.cwd().openFile(session_log, .{ .mode = .read_write }) catch |err| { + try log_writer.print("failed to open existing session log file: {s}\n", .{@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); diff --git a/src/main.zig b/src/main.zig index 76ebde3..0733d1f 100644 --- a/src/main.zig +++ b/src/main.zig @@ -860,7 +860,7 @@ pub fn main() !void { }; std.posix.sigaction(std.posix.SIG.CHLD, &tty_control_transfer_act, null); - auth.authenticate(allocator, auth_options, current_environment, login_text, password_text) catch |err| { + auth.authenticate(allocator, log_writer, auth_options, current_environment, login_text, password_text) catch |err| { shared_err.writeError(err); std.process.exit(1); }; From fec0815161ebf25b21d03685148d41e700bbd130 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Fri, 29 Aug 2025 22:18:50 +0200 Subject: [PATCH 267/530] Always copy an example config file (partially addresses #801) Signed-off-by: AnErrupTion --- build.zig | 3 +++ 1 file changed, 3 insertions(+) diff --git a/build.zig b/build.zig index d241ebb..c017e6a 100644 --- a/build.zig +++ b/build.zig @@ -175,6 +175,9 @@ fn install_ly(allocator: std.mem.Allocator, patch_map: PatchMap, install_config: try installText(patched_config, config_dir, ly_config_directory, "config.ini", .{}); } + 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_setup = try patchFile(allocator, "res/setup.sh", patch_map); try installText(patched_setup, config_dir, ly_config_directory, "setup.sh", .{ .mode = 0o755 }); } From 38c3ecd089047bdfa4f5e0e63ab3788f6aabc858 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Fri, 29 Aug 2025 22:42:34 +0200 Subject: [PATCH 268/530] Remove unused import Signed-off-by: AnErrupTion --- src/auth.zig | 1 - 1 file changed, 1 deletion(-) diff --git a/src/auth.zig b/src/auth.zig index 0c5d5a3..86301a2 100644 --- a/src/auth.zig +++ b/src/auth.zig @@ -6,7 +6,6 @@ const Environment = @import("Environment.zig"); const interop = @import("interop.zig"); const SharedError = @import("SharedError.zig"); -const Allocator = std.mem.Allocator; const Md5 = std.crypto.hash.Md5; const utmp = interop.utmp; const Utmp = utmp.utmpx; From 230874abd1a7a9051ae38a801111a4e1c68822fe Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Fri, 29 Aug 2025 22:54:05 +0200 Subject: [PATCH 269/530] Don't forget to flush... :) Signed-off-by: AnErrupTion --- src/auth.zig | 5 +++-- src/main.zig | 13 +++++++++++-- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/src/auth.zig b/src/auth.zig index 86301a2..e7d6296 100644 --- a/src/auth.zig +++ b/src/auth.zig @@ -90,7 +90,8 @@ pub fn authenticate(allocator: std.mem.Allocator, log_writer: *std.Io.Writer, op child_pid = try std.posix.fork(); if (child_pid == 0) { - try log_writer.writeAll("starting session"); + try log_writer.writeAll("starting session\n"); + try log_writer.flush(); startSession(log_writer, allocator, options, tty_str, user_entry, handle, current_environment) catch |e| { shared_err.writeError(e); @@ -377,7 +378,7 @@ fn xauth(log_writer: *std.Io.Writer, allocator: std.mem.Allocator, display_name: const status = std.posix.waitpid(pid, 0); if (status.status != 0) { - try log_writer.print("xauth command failed with status {d}", .{status.status}); + try log_writer.print("xauth command failed with status {d}\n", .{status.status}); return error.XauthFailed; } } diff --git a/src/main.zig b/src/main.zig index 0733d1f..89f99d7 100644 --- a/src/main.zig +++ b/src/main.zig @@ -229,6 +229,13 @@ pub fn main() !void { var log_buffer: [1024]u8 = undefined; var log_file_writer = log_file.writer(&log_buffer); + + // Seek to the end of the log file + if (could_open_log_file) { + const stat = try log_file.stat(); + try log_file_writer.seekTo(stat.size); + } + var log_writer = &log_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 @@ -787,7 +794,7 @@ pub fn main() !void { update = true; }, termbox.TB_KEY_ENTER => authenticate: { - try log_writer.writeAll("authenticating..."); + try log_writer.writeAll("authenticating...\n"); if (!config.allow_empty_password and password.text.items.len == 0) { // Let's not log this message for security reasons @@ -898,7 +905,7 @@ pub fn main() !void { password.clear(); try info_line.addMessage(lang.logout, config.bg, config.fg); - try log_writer.writeAll("logged out"); + try log_writer.writeAll("logged out\n"); } try std.posix.tcsetattr(std.posix.STDIN_FILENO, .FLUSH, tb_termios); @@ -955,6 +962,8 @@ pub fn main() !void { update = true; }, } + + try log_writer.flush(); } } From f9553655a30388725cc09e1277754f75d3dd7110 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Sat, 30 Aug 2025 00:27:04 +0200 Subject: [PATCH 270/530] Separate platform code from C interop code Also, don't use isBSD() because we'll only support FreeBSD for now. Other BSDs may not necessarily support Unicode characters or the same ioctl constants as we do (or even ioctl at all). Signed-off-by: AnErrupTion --- src/interop.zig | 143 ++++++++++++++++++++++++++++-------------------- 1 file changed, 85 insertions(+), 58 deletions(-) diff --git a/src/interop.zig b/src/interop.zig index c04f458..d58eeec 100644 --- a/src/interop.zig +++ b/src/interop.zig @@ -1,19 +1,5 @@ const std = @import("std"); const builtin = @import("builtin"); -const Allocator = std.mem.Allocator; - -pub const TimeOfDay = struct { - seconds: i64, - microseconds: i64, -}; - -pub const UsernameEntry = struct { - username: ?[]const u8, - uid: std.posix.uid_t, - gid: std.posix.gid_t, - home: ?[]const u8, - shell: ?[]const u8, -}; pub const termbox = @import("termbox2"); @@ -57,29 +43,83 @@ const time = @cImport({ @cInclude("time.h"); }); -// BSD-specific headers -const kbio = @cImport({ - @cInclude("sys/kbio.h"); -}); +pub const TimeOfDay = struct { + seconds: i64, + microseconds: i64, +}; -// Linux-specific headers -const kd = @cImport({ - @cInclude("sys/kd.h"); -}); +pub const UsernameEntry = struct { + username: ?[]const u8, + uid: std.posix.uid_t, + gid: std.posix.gid_t, + home: ?[]const u8, + shell: ?[]const u8, + passwd_struct: [*c]pwd.passwd, +}; -const vt = @cImport({ - @cInclude("sys/vt.h"); -}); +// Contains the platform-specific code +fn PlatformStruct() type { + return switch (builtin.os.tag) { + .linux => struct { + pub const kd = @cImport({ + @cInclude("sys/kd.h"); + }); -// Used for getting & setting the lock state -const LedState = if (builtin.os.tag.isBSD()) c_int else c_char; -const get_led_state = if (builtin.os.tag.isBSD()) kbio.KDGETLED else kd.KDGKBLED; -const set_led_state = if (builtin.os.tag.isBSD()) kbio.KDSETLED else kd.KDSKBLED; -const numlock_led = if (builtin.os.tag.isBSD()) kbio.LED_NUM else kd.K_NUMLOCK; -const capslock_led = if (builtin.os.tag.isBSD()) kbio.LED_CAP else kd.K_CAPSLOCK; + pub const vt = @cImport({ + @cInclude("sys/vt.h"); + }); + + pub const LedState = c_char; + pub const get_led_state = kd.KDGKBLED; + pub const set_led_state = kd.KDSKBLED; + pub const numlock_led = kd.K_NUMLOCK; + pub const capslock_led = kd.K_CAPSLOCK; + pub const vt_activate = vt.VT_ACTIVATE; + pub const vt_waitactive = vt.VT_WAITACTIVE; + + pub fn setUserContextImpl(username: [*:0]const u8, entry: UsernameEntry) !void { + 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; + } + }, + .freebsd => struct { + pub const kbio = @cImport({ + @cInclude("sys/kbio.h"); + }); + + pub const consio = @cImport({ + @cInclude("sys/consio.h"); + }); + + pub const LedState = c_int; + pub const get_led_state = kbio.KDGETLED; + pub const set_led_state = kbio.KDSETLED; + pub const numlock_led = kbio.LED_NUM; + pub const capslock_led = kbio.LED_CAP; + pub const vt_activate = consio.VT_ACTIVATE; + pub const vt_waitactive = consio.VT_WAITACTIVE; + + pub fn setUserContextImpl(username: [*:0]const u8, entry: UsernameEntry) !void { + // FreeBSD has initgroups() in unistd + const status = unistd.initgroups(username, @intCast(entry.gid)); + if (status != 0) return error.GroupInitializationFailed; + + // FreeBSD sets the GID and UID with setusercontext() + const result = pwd.setusercontext(null, entry.passwd_struct, @intCast(entry.uid), pwd.LOGIN_SETALL); + if (result != 0) return error.SetUserUidFailed; + } + }, + else => @compileError("Unsupported target: " ++ builtin.os.tag), + }; +} + +const platform_struct = PlatformStruct(); pub fn supportsUnicode() bool { - return builtin.os.tag == .linux or builtin.os.tag.isBSD(); + return builtin.os.tag == .linux or builtin.os.tag == .freebsd; } pub fn timeAsString(buf: [:0]u8, format: [:0]const u8) []u8 { @@ -103,10 +143,10 @@ pub fn getTimeOfDay() !TimeOfDay { } pub fn switchTty(tty: u8) !void { - var status = std.c.ioctl(std.posix.STDIN_FILENO, vt.VT_ACTIVATE, tty); + var status = std.c.ioctl(std.posix.STDIN_FILENO, platform_struct.vt_activate, tty); if (status != 0) return error.FailedToActivateTty; - status = std.c.ioctl(std.posix.STDIN_FILENO, vt.VT_WAITACTIVE, tty); + status = std.c.ioctl(std.posix.STDIN_FILENO, platform_struct.vt_waitactive, tty); if (status != 0) return error.FailedToWaitForActiveTty; } @@ -114,24 +154,24 @@ pub fn getLockState() !struct { numlock: bool, capslock: bool, } { - var led: LedState = undefined; - const status = std.c.ioctl(std.posix.STDIN_FILENO, get_led_state, &led); + var led: platform_struct.LedState = undefined; + const status = std.c.ioctl(std.posix.STDIN_FILENO, platform_struct.get_led_state, &led); if (status != 0) return error.FailedToGetLockState; return .{ - .numlock = (led & numlock_led) != 0, - .capslock = (led & capslock_led) != 0, + .numlock = (led & platform_struct.numlock_led) != 0, + .capslock = (led & platform_struct.capslock_led) != 0, }; } pub fn setNumlock(val: bool) !void { - var led: LedState = undefined; - var status = std.c.ioctl(std.posix.STDIN_FILENO, get_led_state, &led); + var led: platform_struct.LedState = undefined; + var status = std.c.ioctl(std.posix.STDIN_FILENO, platform_struct.get_led_state, &led); if (status != 0) return error.FailedToGetNumlock; - const numlock = (led & numlock_led) != 0; + const numlock = (led & platform_struct.numlock_led) != 0; if (numlock != val) { - status = std.c.ioctl(std.posix.STDIN_FILENO, set_led_state, led ^ numlock_led); + status = std.c.ioctl(std.posix.STDIN_FILENO, platform_struct.set_led_state, led ^ platform_struct.numlock_led); if (status != 0) return error.FailedToSetNumlock; } } @@ -140,22 +180,7 @@ pub fn setUserContext(allocator: std.mem.Allocator, entry: UsernameEntry) !void const username_z = try allocator.dupeZ(u8, entry.username.?); defer allocator.free(username_z); - if (builtin.os.tag == .freebsd) { - // FreeBSD has initgroups() in unistd - const status = unistd.initgroups(username_z.ptr, @intCast(entry.gid)); - if (status != 0) return error.GroupInitializationFailed; - - // FreeBSD sets the GID and UID with setusercontext() - // TODO - const result = pwd.setusercontext(null, entry, @intCast(entry.uid), pwd.LOGIN_SETALL); - if (result != 0) return error.SetUserUidFailed; - } else { - const status = grp.initgroups(username_z.ptr, @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; - } + return platform_struct.setUserContextImpl(username_z.ptr, entry); } pub fn setUserShell(entry: *UsernameEntry) void { @@ -193,6 +218,7 @@ pub fn getNextUsernameEntry() ?UsernameEntry { .gid = @intCast(entry.*.pw_gid), .home = if (entry.*.pw_dir) |dir| dir[0..std.mem.len(dir)] else null, .shell = if (entry.*.pw_shell) |shell| shell[0..std.mem.len(shell)] else null, + .passwd_struct = entry, }; } @@ -206,6 +232,7 @@ pub fn getUsernameEntry(username: [:0]const u8) ?UsernameEntry { .gid = @intCast(entry.*.pw_gid), .home = if (entry.*.pw_dir) |dir| dir[0..std.mem.len(dir)] else null, .shell = if (entry.*.pw_shell) |shell| shell[0..std.mem.len(shell)] else null, + .passwd_struct = entry, }; } From 0a9ceca822e872520a798767a0d7b25cfbf7ef92 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Sat, 30 Aug 2025 01:09:12 +0200 Subject: [PATCH 271/530] Don't dupeZ() in main Signed-off-by: AnErrupTion --- src/auth.zig | 12 +++++++++--- src/main.zig | 7 +------ 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/src/auth.zig b/src/auth.zig index e7d6296..5621d21 100644 --- a/src/auth.zig +++ b/src/auth.zig @@ -32,7 +32,7 @@ pub fn sessionSignalHandler(i: c_int) callconv(.c) void { if (child_pid > 0) _ = std.c.kill(child_pid, i); } -pub fn authenticate(allocator: std.mem.Allocator, log_writer: *std.Io.Writer, options: AuthOptions, current_environment: Environment, login: [:0]const u8, password: [:0]const u8) !void { +pub fn authenticate(allocator: std.mem.Allocator, log_writer: *std.Io.Writer, 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}); @@ -43,7 +43,13 @@ pub fn authenticate(allocator: std.mem.Allocator, log_writer: *std.Io.Writer, op try setXdgEnv(allocator, tty_str, current_environment); // Open the PAM session - var credentials = [_:null]?[*:0]const u8{ login, password }; + const login_z = try allocator.dupeZ(u8, login); + defer allocator.free(login_z); + + const password_z = try allocator.dupeZ(u8, password); + defer allocator.free(password_z); + + var credentials = [_:null]?[*:0]const u8{ login_z, password_z }; const conv = interop.pam.pam_conv{ .conv = loginConv, @@ -79,7 +85,7 @@ pub fn authenticate(allocator: std.mem.Allocator, log_writer: *std.Io.Writer, op defer interop.closePasswordDatabase(); // Get password structure from username - user_entry = interop.getUsernameEntry(login) orelse return error.GetPasswordNameFailed; + user_entry = interop.getUsernameEntry(login_z) orelse return error.GetPasswordNameFailed; } // Set user shell if it hasn't already been set diff --git a/src/main.zig b/src/main.zig index 89f99d7..d3f3b70 100644 --- a/src/main.zig +++ b/src/main.zig @@ -839,11 +839,6 @@ pub fn main() !void { defer shared_err.deinit(); { - const login_text = try allocator.dupeZ(u8, login.getCurrentUser()); - defer allocator.free(login_text); - const password_text = try allocator.dupeZ(u8, password.text.items); - defer allocator.free(password_text); - session_pid = try std.posix.fork(); if (session_pid == 0) { const current_environment = session.label.list.items[session.label.current]; @@ -867,7 +862,7 @@ pub fn main() !void { }; std.posix.sigaction(std.posix.SIG.CHLD, &tty_control_transfer_act, null); - auth.authenticate(allocator, log_writer, auth_options, current_environment, login_text, password_text) catch |err| { + auth.authenticate(allocator, log_writer, auth_options, current_environment, login.getCurrentUser(), password.text.items) catch |err| { shared_err.writeError(err); std.process.exit(1); }; From 36e220e2ffcb35100c11568cb49800a718a8b338 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Sat, 30 Aug 2025 02:09:51 +0200 Subject: [PATCH 272/530] Remove usage of std.c.stat() for xauth code Signed-off-by: AnErrupTion --- src/auth.zig | 42 ++++++++++++++++++++++-------------------- src/main.zig | 2 +- 2 files changed, 23 insertions(+), 21 deletions(-) diff --git a/src/auth.zig b/src/auth.zig index 5621d21..22b5abe 100644 --- a/src/auth.zig +++ b/src/auth.zig @@ -307,35 +307,37 @@ fn getXPid(display_num: u8) !i32 { fn createXauthFile(pwd: [:0]const u8) ![]const u8 { var xauth_buf: [100]u8 = undefined; - var xauth_dir: [:0]const u8 = undefined; + var xauth_dir: []const u8 = undefined; const xdg_rt_dir = std.posix.getenv("XDG_RUNTIME_DIR"); var xauth_file: []const u8 = "lyxauth"; - if (xdg_rt_dir == null) { + if (xdg_rt_dir == null) no_rt_dir: { const xdg_cfg_home = std.posix.getenv("XDG_CONFIG_HOME"); - var sb: std.c.Stat = undefined; - if (xdg_cfg_home == null) { - xauth_dir = try std.fmt.bufPrintZ(&xauth_buf, "{s}/.config", .{pwd}); - _ = std.c.stat(xauth_dir, &sb); - const mode = sb.mode & std.posix.S.IFMT; - if (mode == std.posix.S.IFDIR) { - xauth_dir = try std.fmt.bufPrintZ(&xauth_buf, "{s}/ly", .{xauth_dir}); - } else { + 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 { + // xauth_dir isn't a directory xauth_dir = pwd; xauth_file = ".lyxauth"; - } + break :no_cfg_home; + }; + dir.close(); + + // xauth_dir is a directory, use it to store Xauthority + xauth_dir = try std.fmt.bufPrint(&xauth_buf, "{s}/ly", .{xauth_dir}); } else { - xauth_dir = try std.fmt.bufPrintZ(&xauth_buf, "{s}/ly", .{xdg_cfg_home.?}); + xauth_dir = try std.fmt.bufPrint(&xauth_buf, "{s}/ly", .{xdg_cfg_home.?}); } - _ = std.c.stat(xauth_dir, &sb); - const mode = sb.mode & std.posix.S.IFMT; - if (mode != std.posix.S.IFDIR) { - std.posix.mkdir(xauth_dir, 777) catch { - xauth_dir = pwd; - xauth_file = ".lyxauth"; - }; - } + const file = std.fs.cwd().openFile(xauth_dir, .{}) catch break :no_rt_dir; + file.close(); + + // xauth_dir is a file, create the parent directory + std.posix.mkdir(xauth_dir, 777) catch { + xauth_dir = pwd; + xauth_file = ".lyxauth"; + }; } else { xauth_dir = xdg_rt_dir.?; } diff --git a/src/main.zig b/src/main.zig index d3f3b70..ef02fa7 100644 --- a/src/main.zig +++ b/src/main.zig @@ -540,7 +540,7 @@ pub fn main() !void { 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: [:0]const u8 = try std.fmt.bufPrintZ(&format_buf, "{s}{s}{s}{s}", .{ + 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 "", From 5924db58e1cbebcf6736a8709753da8f7c8f2b3b Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Sat, 30 Aug 2025 13:17:19 +0200 Subject: [PATCH 273/530] Use std.mem.span + remove useless dupeZ() Signed-off-by: AnErrupTion --- src/auth.zig | 20 ++++++++++---------- src/interop.zig | 14 +++++++------- src/main.zig | 2 +- 3 files changed, 18 insertions(+), 18 deletions(-) diff --git a/src/auth.zig b/src/auth.zig index 22b5abe..7544352 100644 --- a/src/auth.zig +++ b/src/auth.zig @@ -305,7 +305,7 @@ fn getXPid(display_num: u8) !i32 { return std.fmt.parseInt(i32, std.mem.trim(u8, buffer[0..written], " "), 10); } -fn createXauthFile(pwd: [:0]const u8) ![]const u8 { +fn createXauthFile(pwd: []const 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"); @@ -365,11 +365,8 @@ fn mcookie() [Md5.digest_length * 2]u8 { return std.fmt.bytesToHex(&out, .lower); } -fn xauth(log_writer: *std.Io.Writer, allocator: std.mem.Allocator, display_name: []u8, shell: [*:0]const u8, pw_dir: [*:0]const u8, options: AuthOptions) !void { - var pwd_buf: [100]u8 = undefined; - const pwd = try std.fmt.bufPrintZ(&pwd_buf, "{s}", .{pw_dir}); - - const xauthority = try createXauthFile(pwd); +fn xauth(log_writer: *std.Io.Writer, allocator: std.mem.Allocator, display_name: []u8, shell: [*:0]const u8, home: []const u8, options: AuthOptions) !void { + const xauthority = try createXauthFile(home); try interop.setEnvironmentVariable(allocator, "XAUTHORITY", xauthority, true); try interop.setEnvironmentVariable(allocator, "DISPLAY", display_name, true); @@ -379,6 +376,7 @@ fn xauth(log_writer: *std.Io.Writer, allocator: std.mem.Allocator, display_name: 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); + const args = [_:null]?[*:0]const u8{ shell, "-c", cmd_str }; std.posix.execveZ(shell, &args, std.c.environ) catch {}; std.process.exit(1); @@ -399,6 +397,7 @@ fn executeShellCmd(allocator: std.mem.Allocator, shell: []const u8, options: Aut 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 "", shell }); + const args = [_:null]?[*:0]const u8{ shell_z, "-c", cmd_str }; return std.posix.execveZ(shell_z, &args, std.c.environ); } @@ -415,6 +414,7 @@ fn executeWaylandCmd(log_writer: *std.Io.Writer, allocator: std.mem.Allocator, s 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 "", desktop_cmd }); + const args = [_:null]?[*:0]const u8{ shell_z, "-c", cmd_str }; return std.posix.execveZ(shell_z, &args, std.c.environ); } @@ -427,15 +427,13 @@ fn executeX11Cmd(log_writer: *std.Io.Writer, allocator: std.mem.Allocator, shell const shell_z = try allocator.dupeZ(u8, shell); defer allocator.free(shell_z); - const home_z = try allocator.dupeZ(u8, home); - defer allocator.free(home_z); - - try xauth(log_writer, allocator, display_name, shell_z, home_z, options); + try xauth(log_writer, allocator, display_name, shell_z, home, options); 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); + 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); @@ -459,6 +457,7 @@ fn executeX11Cmd(log_writer: *std.Io.Writer, allocator: std.mem.Allocator, shell 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 args = [_:null]?[*:0]const u8{ shell_z, "-c", cmd_str }; std.posix.execveZ(shell_z, &args, std.c.environ) catch {}; std.process.exit(1); @@ -500,6 +499,7 @@ fn executeCustomCmd(log_writer: *std.Io.Writer, allocator: std.mem.Allocator, sh 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 }); + const args = [_:null]?[*:0]const u8{ shell_z, "-c", cmd_str }; return std.posix.execveZ(shell_z, &args, std.c.environ); } diff --git a/src/interop.zig b/src/interop.zig index d58eeec..940c297 100644 --- a/src/interop.zig +++ b/src/interop.zig @@ -187,7 +187,7 @@ pub fn setUserShell(entry: *UsernameEntry) void { unistd.setusershell(); const shell = unistd.getusershell(); - entry.shell = shell[0..std.mem.len(shell)]; + entry.shell = std.mem.span(shell); unistd.endusershell(); } @@ -213,11 +213,11 @@ pub fn getNextUsernameEntry() ?UsernameEntry { if (entry == null) return null; return .{ - .username = if (entry.*.pw_name) |name| name[0..std.mem.len(name)] else null, + .username = if (entry.*.pw_name) |name| std.mem.span(name) else null, .uid = @intCast(entry.*.pw_uid), .gid = @intCast(entry.*.pw_gid), - .home = if (entry.*.pw_dir) |dir| dir[0..std.mem.len(dir)] else null, - .shell = if (entry.*.pw_shell) |shell| shell[0..std.mem.len(shell)] else null, + .home = if (entry.*.pw_dir) |dir| std.mem.span(dir) else null, + .shell = if (entry.*.pw_shell) |shell| std.mem.span(shell) else null, .passwd_struct = entry, }; } @@ -227,11 +227,11 @@ pub fn getUsernameEntry(username: [:0]const u8) ?UsernameEntry { if (entry == null) return null; return .{ - .username = if (entry.*.pw_name) |name| name[0..std.mem.len(name)] else null, + .username = if (entry.*.pw_name) |name| std.mem.span(name) else null, .uid = @intCast(entry.*.pw_uid), .gid = @intCast(entry.*.pw_gid), - .home = if (entry.*.pw_dir) |dir| dir[0..std.mem.len(dir)] else null, - .shell = if (entry.*.pw_shell) |shell| shell[0..std.mem.len(shell)] else null, + .home = if (entry.*.pw_dir) |dir| std.mem.span(dir) else null, + .shell = if (entry.*.pw_shell) |shell| std.mem.span(shell) else null, .passwd_struct = entry, }; } diff --git a/src/main.zig b/src/main.zig index ef02fa7..2abce0b 100644 --- a/src/main.zig +++ b/src/main.zig @@ -965,7 +965,7 @@ pub fn main() !void { 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 = capability[0..std.mem.len(capability)]; + const capability_slice = std.mem.span(capability); _ = try std.posix.write(termbox.global.ttyfd, capability_slice); } From ee97f3b5e1c8caf8622662fb85fbbea9d16d67a6 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Sun, 7 Sep 2025 17:44:50 +0200 Subject: [PATCH 274/530] Automatically detect TTY (closes #795) Signed-off-by: AnErrupTion --- res/config.ini | 3 -- src/config/Config.zig | 1 - src/interop.zig | 71 +++++++++++++++++++++++++++++++++++++++++++ src/main.zig | 14 ++++++--- 4 files changed, 81 insertions(+), 8 deletions(-) diff --git a/res/config.ini b/res/config.ini index 3efc716..539a425 100644 --- a/res/config.ini +++ b/res/config.ini @@ -272,9 +272,6 @@ sleep_key = F3 # Center the session name. text_in_center = false -# TTY in use -tty = $DEFAULT_TTY - # Default vi mode # normal -> normal mode # insert -> insert mode diff --git a/src/config/Config.zig b/src/config/Config.zig index 26eb29a..fb782ac 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -72,7 +72,6 @@ shutdown_key: []const u8 = "F1", sleep_cmd: ?[]const u8 = null, sleep_key: []const u8 = "F3", text_in_center: bool = false, -tty: u8 = build_options.tty, vi_default_mode: ViMode = .normal, vi_mode: bool = false, waylandsessions: []const u8 = build_options.prefix_directory ++ "/share/wayland-sessions", diff --git a/src/interop.zig b/src/interop.zig index 940c297..0c30be7 100644 --- a/src/interop.zig +++ b/src/interop.zig @@ -84,6 +84,73 @@ fn PlatformStruct() type { std.posix.setgid(@intCast(entry.gid)) catch return error.SetUserGidFailed; std.posix.setuid(@intCast(entry.uid)) catch return error.SetUserUidFailed; } + + // Procedure: + // 1. Open /proc/self/stat to retrieve the tty_nr field + // 2. Parse the tty_nr field to extract the major and minor device + // numbers + // 3. Then, read every /sys/class/tty/[dir]/dev, where [dir] is every + // sub-directory + // 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 { + var file_buffer: [256]u8 = undefined; + var tty_major: u16 = undefined; + var tty_minor: u16 = undefined; + + { + var file = try std.fs.openFileAbsolute("/proc/self/stat", .{}); + defer file.close(); + + var reader = file.reader(&file_buffer); + var buffer: [1024]u8 = undefined; + const read = try reader.read(&buffer); + + var iterator = std.mem.splitScalar(u8, buffer[0..read], ' '); + var fields: [52][]const u8 = undefined; + var index: usize = 0; + + while (iterator.next()) |field| { + fields[index] = field; + index += 1; + } + + const tty_nr = try std.fmt.parseInt(u16, fields[6], 10); + tty_major = tty_nr / 256; + tty_minor = tty_nr % 256; + } + + var directory = try std.fs.openDirAbsolute("/sys/class/tty", .{ .iterate = true }); + defer directory.close(); + + var iterator = directory.iterate(); + while (try iterator.next()) |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 reader = file.reader(&file_buffer); + var buffer: [16]u8 = undefined; + const read = try reader.read(&buffer); + + var device_iterator = std.mem.splitScalar(u8, buffer[0..(read - 1)], ':'); + const device_major_str = device_iterator.next() orelse continue; + const device_minor_str = device_iterator.next() orelse continue; + + const device_major = try std.fmt.parseInt(u8, device_major_str, 10); + const device_minor = try std.fmt.parseInt(u8, device_minor_str, 10); + + if (device_major == tty_major and device_minor == tty_minor) { + const tty_id_str = entry.name["tty".len..]; + return try std.fmt.parseInt(u8, tty_id_str, 10); + } + } + + return error.NoTtyFound; + } }, .freebsd => struct { pub const kbio = @cImport({ @@ -142,6 +209,10 @@ pub fn getTimeOfDay() !TimeOfDay { }; } +pub fn getActiveTty(allocator: std.mem.Allocator) !u8 { + return platform_struct.getActiveTtyImpl(allocator); +} + pub fn switchTty(tty: u8) !void { var status = std.c.ioctl(std.posix.STDIN_FILENO, platform_struct.vt_activate, tty); if (status != 0) return error.FailedToActivateTty; diff --git a/src/main.zig b/src/main.zig index 2abce0b..65298ac 100644 --- a/src/main.zig +++ b/src/main.zig @@ -58,6 +58,7 @@ pub fn main() !void { var restart = false; var shutdown_cmd: []const u8 = undefined; var restart_cmd: []const u8 = undefined; + var commands_allocated = false; var stderr_buffer: [128]u8 = undefined; var stderr_writer = std.fs.File.stderr().writer(&stderr_buffer); @@ -75,8 +76,11 @@ pub fn main() !void { stderr.flush() catch std.process.exit(1); } else { // The user has quit Ly using Ctrl+C - temporary_allocator.free(shutdown_cmd); - temporary_allocator.free(restart_cmd); + if (commands_allocated) { + // Necessary if we error out before allocating + temporary_allocator.free(shutdown_cmd); + temporary_allocator.free(restart_cmd); + } } } @@ -242,6 +246,7 @@ pub fn main() !void { // 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); + commands_allocated = true; // Initialize termbox try log_writer.writeAll("initializing termbox2\n"); @@ -478,7 +483,8 @@ pub fn main() !void { var auth_fails: u64 = 0; // Switch to selected TTY - interop.switchTty(config.tty) catch |err| { + const active_tty = try interop.getActiveTty(allocator); + 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)}); }; @@ -843,7 +849,7 @@ pub fn main() !void { if (session_pid == 0) { const current_environment = session.label.list.items[session.label.current]; const auth_options = auth.AuthOptions{ - .tty = config.tty, + .tty = active_tty, .service_name = config.service_name, .path = config.path, .session_log = config.session_log, From a6535b9152933b1aba96ec17ab317517b6eff75d Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Thu, 25 Sep 2025 08:29:36 +0200 Subject: [PATCH 275/530] Update custom sessions' README Signed-off-by: AnErrupTion --- res/custom-sessions/README | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/res/custom-sessions/README b/res/custom-sessions/README index 23c1ed1..2911923 100644 --- a/res/custom-sessions/README +++ b/res/custom-sessions/README @@ -15,8 +15,9 @@ syntax is the same as described in the Freedesktop Desktop Entry Specification. The Terminal value specifies if standard output and standard error should be 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. +redirect standard output & error. It is optional and defaults to false. -Finally, do note that the XDG_SESSION_TYPE environment variable is set to -"unspecified" (without quotes), which is behavior that at least systemd -recognizes (see pam_systemd's man page) +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 d8b0ae34f3891a26274cb1e073413dfebd2cfafd Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Thu, 25 Sep 2025 08:30:13 +0200 Subject: [PATCH 276/530] Remove semi-colons in default locale Signed-off-by: AnErrupTion --- src/config/Lang.zig | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/config/Lang.zig b/src/config/Lang.zig index cbeee18..2e664f2 100644 --- a/src/config/Lang.zig +++ b/src/config/Lang.zig @@ -57,13 +57,13 @@ err_xcb_conn: []const u8 = "xcb connection failed", err_xsessions_dir: []const u8 = "failed to find sessions folder", err_xsessions_open: []const u8 = "failed to open sessions folder", insert: []const u8 = "insert", -login: []const u8 = "login:", +login: []const u8 = "login", logout: []const u8 = "logged out", no_x11_support: []const u8 = "x11 support disabled at compile-time", normal: []const u8 = "normal", numlock: []const u8 = "numlock", other: []const u8 = "other", -password: []const u8 = "password:", +password: []const u8 = "password", restart: []const u8 = "reboot", shell: [:0]const u8 = "shell", shutdown: []const u8 = "shutdown", From 145ad5142c9ca9dba3e8613b7a6aabf9cc5cceb4 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Thu, 25 Sep 2025 08:32:30 +0200 Subject: [PATCH 277/530] Clean up redundant authentication & session code Signed-off-by: AnErrupTion --- src/auth.zig | 38 +++----------------------------------- src/main.zig | 22 +++++++++++----------- 2 files changed, 14 insertions(+), 46 deletions(-) diff --git a/src/auth.zig b/src/auth.zig index 7544352..b27f96c 100644 --- a/src/auth.zig +++ b/src/auth.zig @@ -166,14 +166,12 @@ fn startSession( // Execute what the user requested switch (current_environment.display_server) { - .wayland => try executeWaylandCmd(log_writer, allocator, user_entry.shell.?, options, current_environment.cmd), - .shell => try executeShellCmd(allocator, user_entry.shell.?, options), + .wayland, .shell, .custom => try executeCmd(log_writer, 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}); try executeX11Cmd(log_writer, allocator, user_entry.shell.?, user_entry.home.?, options, current_environment.cmd, vt); }, - .custom => try executeCustomCmd(log_writer, allocator, user_entry.shell.?, options, current_environment.is_terminal, current_environment.cmd), } } @@ -197,7 +195,7 @@ fn setXdgEnv(allocator: std.mem.Allocator, tty_str: []u8, environment: Environme .wayland => "wayland", .shell => "tty", .xinitrc, .x11 => "x11", - .custom => "unspecified", + .custom => if (environment.is_terminal) "tty" else "unspecified", }, false); // The "/run/user/%d" directory is not available on FreeBSD. It is much @@ -389,36 +387,6 @@ fn xauth(log_writer: *std.Io.Writer, allocator: std.mem.Allocator, display_name: } } -fn executeShellCmd(allocator: std.mem.Allocator, shell: []const u8, options: AuthOptions) !void { - // We don't want to redirect stdout and stderr in a shell session - - const shell_z = try allocator.dupeZ(u8, 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 "", shell }); - - const args = [_:null]?[*:0]const u8{ shell_z, "-c", cmd_str }; - return std.posix.execveZ(shell_z, &args, std.c.environ); -} - -fn executeWaylandCmd(log_writer: *std.Io.Writer, allocator: std.mem.Allocator, shell: []const u8, options: AuthOptions, desktop_cmd: []const u8) !void { - var maybe_log_file: ?std.fs.File = null; - if (options.session_log) |log_path| { - maybe_log_file = try redirectStandardStreams(log_writer, log_path, true); - } - defer if (maybe_log_file) |log_file| log_file.close(); - - const shell_z = try allocator.dupeZ(u8, 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 "", desktop_cmd }); - - const args = [_:null]?[*:0]const u8{ shell_z, "-c", cmd_str }; - return std.posix.execveZ(shell_z, &args, std.c.environ); -} - fn executeX11Cmd(log_writer: *std.Io.Writer, allocator: std.mem.Allocator, shell: []const u8, home: []const u8, options: AuthOptions, desktop_cmd: []const u8, vt: []const u8) !void { const display_num = try getFreeDisplay(); var buf: [4]u8 = undefined; @@ -482,7 +450,7 @@ fn executeX11Cmd(log_writer: *std.Io.Writer, allocator: std.mem.Allocator, shell _ = std.posix.waitpid(x_pid, 0); } -fn executeCustomCmd(log_writer: *std.Io.Writer, allocator: std.mem.Allocator, shell: []const u8, options: AuthOptions, is_terminal: bool, exec_cmd: []const u8) !void { +fn executeCmd(log_writer: *std.Io.Writer, allocator: std.mem.Allocator, shell: []const u8, options: AuthOptions, is_terminal: bool, exec_cmd: []const u8) !void { var maybe_log_file: ?std.fs.File = null; if (!is_terminal) { // For custom desktop entries, the "Terminal" value here determines if diff --git a/src/main.zig b/src/main.zig index 65298ac..2a7e8cb 100644 --- a/src/main.zig +++ b/src/main.zig @@ -990,6 +990,7 @@ fn addOtherEnvironment(session: *Session, lang: Lang, display_server: DisplaySer .cmd = exec orelse "", .specifier = lang.other, .display_server = display_server, + .is_terminal = display_server == .shell, }); } @@ -1010,24 +1011,23 @@ fn crawl(session: *Session, lang: Lang, path: []const u8, display_server: Displa }); errdefer entry_ini.deinit(); - var maybe_xdg_session_desktop: ?[]const u8 = null; - const maybe_desktop_names = entry_ini.data.@"Desktop Entry".DesktopNames; - if (maybe_desktop_names) |desktop_names| { - maybe_xdg_session_desktop = std.mem.sliceTo(desktop_names, ';'); - } 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 - maybe_xdg_session_desktop = std.fs.path.stem(item.name); - } - - // Prepare the XDG_CURRENT_DESKTOP environment variable here const entry = entry_ini.data.@"Desktop Entry"; + var maybe_xdg_session_desktop: ?[]const u8 = null; var maybe_xdg_desktop_names: ?[]const u8 = null; + + // Prepare the XDG_SESSION_DESKTOP and XDG_CURRENT_DESKTOP environment + // variables here if (entry.DesktopNames) |desktop_names| { + maybe_xdg_session_desktop = std.mem.sliceTo(desktop_names, ';'); + for (desktop_names) |*c| { if (c.* == ';') c.* = ':'; } maybe_xdg_desktop_names = desktop_names; + } 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 + maybe_xdg_session_desktop = std.fs.path.stem(item.name); } try session.addEnvironment(.{ From 3edd1ff1be3d7b342e365ddae944c75662bd2052 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Thu, 25 Sep 2025 13:35:28 +0200 Subject: [PATCH 278/530] Log error name when zigini fails to parse config Signed-off-by: AnErrupTion --- src/main.zig | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/main.zig b/src/main.zig index 2a7e8cb..28207ab 100644 --- a/src/main.zig +++ b/src/main.zig @@ -111,7 +111,7 @@ pub fn main() !void { var config: Config = undefined; var lang: Lang = undefined; var save: Save = undefined; - var config_load_failed = false; + var maybe_config_load_error: ?anyerror = null; var can_get_lock_state = true; var can_draw_clock = true; @@ -155,9 +155,9 @@ pub fn main() !void { config = config_ini.readFileToStruct(config_path, .{ .fieldHandler = migrator.configFieldHandler, .comment_characters = comment_characters, - }) catch _config: { - config_load_failed = true; - break :_config Config{}; + }) 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 }); @@ -179,7 +179,7 @@ pub fn main() !void { }) catch migrator.tryMigrateSaveFile(&user_buf); } - if (!config_load_failed) { + if (maybe_config_load_error == null) { migrator.lateConfigFieldHandler(&config); } } else { @@ -188,9 +188,9 @@ pub fn main() !void { config = config_ini.readFileToStruct(config_path, .{ .fieldHandler = migrator.configFieldHandler, .comment_characters = comment_characters, - }) catch _config: { - config_load_failed = true; - break :_config Config{}; + }) 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 }); @@ -209,7 +209,7 @@ pub fn main() !void { }) catch migrator.tryMigrateSaveFile(&user_buf); } - if (!config_load_failed) { + if (maybe_config_load_error == null) { migrator.lateConfigFieldHandler(&config); } } @@ -303,10 +303,10 @@ pub fn main() !void { var info_line = InfoLine.init(allocator, &buffer); defer info_line.deinit(); - if (config_load_failed) { + 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.writeAll("unable to parse config file\n"); + try log_writer.print("unable to parse config file: {s}\n", .{@errorName(err)}); } if (!could_open_log_file) { From cee0e0ca4b8488f814fec1b9c3a731dc6d773648 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Fri, 26 Sep 2025 09:54:38 +0200 Subject: [PATCH 279/530] Log more detailed config error messages (closes #801) Signed-off-by: AnErrupTion --- build.zig.zon | 4 ++-- src/main.zig | 35 +++++++++++++++++++++++++++++++++++ 2 files changed, 37 insertions(+), 2 deletions(-) diff --git a/build.zig.zon b/build.zig.zon index 138fbda..735ddc2 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -9,8 +9,8 @@ .hash = "clap-0.11.0-oBajB-HnAQDPCKYzwF7rO3qDFwRcD39Q0DALlTSz5H7e", }, .zigini = .{ - .url = "https://github.com/AnErrupTion/zigini/archive/d580d42f1b1051c0a35d63ab0f5704c6340e0bd3.tar.gz", - .hash = "zigini-0.3.2-BSkB7aVHAADhxwo0aEdWtNzaVXer3d8RwXMuZd-q-spO", + .url = "https://github.com/AnErrupTion/zigini/archive/96ca1d9f1a7ec741f07ceb104dae2b3a7bdfd48a.tar.gz", + .hash = "zigini-0.3.2-BSkB7WJJAADybd5DGd9MLCp6ikGGUq9wicxsjv0HF1Qc", }, .termbox2 = .{ .url = "git+https://github.com/AnErrupTion/termbox2?ref=master#290ac6b8225aacfd16851224682b851b65fcb918", diff --git a/src/main.zig b/src/main.zig index 28207ab..01047ff 100644 --- a/src/main.zig +++ b/src/main.zig @@ -53,6 +53,14 @@ fn ttyControlTransferSignalHandler(_: c_int) callconv(.c) void { _ = termbox.tb_shutdown(); } +const ConfigError = struct { + type_name: []const u8, + key: []const u8, + value: []const u8, + error_name: []const u8, +}; +var config_errors: std.ArrayList(ConfigError) = .empty; + pub fn main() !void { var shutdown = false; var restart = false; @@ -154,6 +162,7 @@ pub fn main() !void { config = config_ini.readFileToStruct(config_path, .{ .fieldHandler = migrator.configFieldHandler, + .errorHandler = configErrorHandler, .comment_characters = comment_characters, }) catch |err| load_error: { maybe_config_load_error = err; @@ -187,6 +196,7 @@ pub fn main() !void { config = config_ini.readFileToStruct(config_path, .{ .fieldHandler = migrator.configFieldHandler, + .errorHandler = configErrorHandler, .comment_characters = comment_characters, }) catch |err| load_error: { maybe_config_load_error = err; @@ -307,6 +317,22 @@ pub fn main() !void { // 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)}); + + 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_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(); + } } if (!could_open_log_file) { @@ -968,6 +994,15 @@ pub fn main() !void { } } +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 ttyClearScreen() !void { // Clear the TTY because termbox2 doesn't seem to do it properly const capability = termbox.global.caps[termbox.TB_CAP_CLEAR_SCREEN]; From 7a0520687db61a72618f0c042bde58f23bfabbf2 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Mon, 29 Sep 2025 21:30:51 +0200 Subject: [PATCH 280/530] Add fallback TTY option (closes #838) Signed-off-by: AnErrupTion --- build.zig | 2 ++ res/lang/ar.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/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 | 1 + res/lang/tr.ini | 1 + res/lang/uk.ini | 1 + res/lang/zh_CN.ini | 1 + src/config/Lang.zig | 1 + src/main.zig | 6 +++++- 22 files changed, 27 insertions(+), 1 deletion(-) diff --git a/build.zig b/build.zig index c017e6a..0f446d5 100644 --- a/build.zig +++ b/build.zig @@ -41,6 +41,7 @@ pub fn build(b: *std.Build) !void { const version_str = try getVersionStr(b, "ly", ly_version); const enable_x11_support = b.option(bool, "enable_x11_support", "Enable X11 support (default is on)") orelse true; const default_tty = b.option(u8, "default_tty", "Set the TTY (default is 2)") orelse 2; + const fallback_tty = b.option(u8, "fallback_tty", "Set the fallback TTY (default is 1). This value gets embedded into the binary") orelse 1; default_tty_str = try std.fmt.allocPrint(b.allocator, "{d}", .{default_tty}); @@ -48,6 +49,7 @@ pub fn build(b: *std.Build) !void { build_options.addOption([]const u8, "prefix_directory", prefix_directory); build_options.addOption([]const u8, "version", version_str); build_options.addOption(u8, "tty", default_tty); + build_options.addOption(u8, "fallback_tty", fallback_tty); build_options.addOption(bool, "enable_x11_support", enable_x11_support); const target = b.standardTargetOptions(.{}); diff --git a/res/lang/ar.ini b/res/lang/ar.ini index 3a0ae3b..6e1f3f5 100644 --- a/res/lang/ar.ini +++ b/res/lang/ar.ini @@ -13,6 +13,7 @@ err_dgn_oob = رسالة سجل (Log) err_domain = اسم نطاق غير صالح err_empty_password = لا يُسمح بكلمة مرور فارغة err_envlist = فشل في جلب قائمة المتغيرات البيئية + err_hostname = فشل في جلب اسم المضيف (Hostname) diff --git a/res/lang/cat.ini b/res/lang/cat.ini index 985cb55..c736153 100644 --- a/res/lang/cat.ini +++ b/res/lang/cat.ini @@ -13,6 +13,7 @@ err_dgn_oob = missatge de registre err_domain = domini invàlid err_envlist = error en obtenir l'envlist + err_hostname = error en obtenir el nom de l'amfitrió diff --git a/res/lang/cs.ini b/res/lang/cs.ini index 2798e18..d9b9ae8 100644 --- a/res/lang/cs.ini +++ b/res/lang/cs.ini @@ -13,6 +13,7 @@ err_dgn_oob = zpráva protokolu err_domain = neplatná doména + err_hostname = nelze získat název hostitele diff --git a/res/lang/de.ini b/res/lang/de.ini index bf70ace..7d5e405 100644 --- a/res/lang/de.ini +++ b/res/lang/de.ini @@ -13,6 +13,7 @@ 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_hostname = Abrufen des Hostnames fehlgeschlagen diff --git a/res/lang/en.ini b/res/lang/en.ini index 11219b5..dfe60f3 100644 --- a/res/lang/en.ini +++ b/res/lang/en.ini @@ -13,6 +13,7 @@ err_dgn_oob = log message err_domain = invalid domain err_empty_password = empty password not allowed err_envlist = failed to get envlist +err_get_active_tty = failed to get active tty err_hostname = failed to get hostname err_lock_state = failed to get lock state err_log = failed to open log file diff --git a/res/lang/es.ini b/res/lang/es.ini index 9676c0e..c249c20 100644 --- a/res/lang/es.ini +++ b/res/lang/es.ini @@ -13,6 +13,7 @@ err_dgn_oob = mensaje de registro err_domain = dominio inválido + err_hostname = error al obtener el nombre de host diff --git a/res/lang/fr.ini b/res/lang/fr.ini index 4ee547e..1a5faba 100644 --- a/res/lang/fr.ini +++ b/res/lang/fr.ini @@ -13,6 +13,7 @@ err_dgn_oob = message err_domain = domaine invalide err_empty_password = mot de passe vide non autorisé err_envlist = échec de lecture de la liste d'environnement +err_get_active_tty = échec de lecture du terminal actif err_hostname = échec de lecture du nom d'hôte err_lock_state = échec de lecture de l'état de verrouillage err_log = échec de l'ouverture du fichier de journal diff --git a/res/lang/it.ini b/res/lang/it.ini index ada3999..b366c4f 100644 --- a/res/lang/it.ini +++ b/res/lang/it.ini @@ -13,6 +13,7 @@ err_dgn_oob = messaggio log err_domain = dominio non valido + err_hostname = impossibile ottenere hostname diff --git a/res/lang/ja_JP.ini b/res/lang/ja_JP.ini index 27f6083..9a9555e 100644 --- a/res/lang/ja_JP.ini +++ b/res/lang/ja_JP.ini @@ -13,6 +13,7 @@ err_dgn_oob = ログメッセージ err_domain = 無効なドメイン err_empty_password = 空のパスワードは許可されていません err_envlist = 環境変数リストの取得に失敗しました + err_hostname = ホスト名の取得に失敗しました diff --git a/res/lang/pl.ini b/res/lang/pl.ini index a35a38e..7bf0a33 100644 --- a/res/lang/pl.ini +++ b/res/lang/pl.ini @@ -13,6 +13,7 @@ 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_hostname = nie udało się uzyskać nazwy hosta diff --git a/res/lang/pt.ini b/res/lang/pt.ini index a94257e..f8c9bd7 100644 --- a/res/lang/pt.ini +++ b/res/lang/pt.ini @@ -13,6 +13,7 @@ err_dgn_oob = mensagem de registo err_domain = domínio inválido + err_hostname = erro ao obter o nome do host diff --git a/res/lang/pt_BR.ini b/res/lang/pt_BR.ini index 66d990c..0c64ca4 100644 --- a/res/lang/pt_BR.ini +++ b/res/lang/pt_BR.ini @@ -13,6 +13,7 @@ err_dgn_oob = mensagem de log err_domain = domínio inválido + err_hostname = não foi possível obter o nome do host diff --git a/res/lang/ro.ini b/res/lang/ro.ini index 008def0..a90c54d 100644 --- a/res/lang/ro.ini +++ b/res/lang/ro.ini @@ -20,6 +20,7 @@ capslock = capslock + err_pam_abort = tranzacţie pam anulată err_pam_acct_expired = cont expirat err_pam_auth = eroare de autentificare diff --git a/res/lang/ru.ini b/res/lang/ru.ini index bb6f98e..95116f1 100644 --- a/res/lang/ru.ini +++ b/res/lang/ru.ini @@ -13,6 +13,7 @@ err_dgn_oob = отладочное сообщение (log) err_domain = неверный домен err_empty_password = пустой пароль не допустим err_envlist = не удалось получить список переменных среды + err_hostname = не удалось получить имя хоста diff --git a/res/lang/sr.ini b/res/lang/sr.ini index 7dc54bf..c99bf9e 100644 --- a/res/lang/sr.ini +++ b/res/lang/sr.ini @@ -13,6 +13,7 @@ err_dgn_oob = log poruka err_domain = nevazeci domen + err_hostname = neuspijesno trazenje hostname-a diff --git a/res/lang/sv.ini b/res/lang/sv.ini index 6b72ada..81233fe 100644 --- a/res/lang/sv.ini +++ b/res/lang/sv.ini @@ -13,6 +13,7 @@ err_dgn_oob = loggmeddelande err_domain = okänd domän + err_hostname = misslyckades att hämta värdnamn diff --git a/res/lang/tr.ini b/res/lang/tr.ini index 9c7b7a5..6759701 100644 --- a/res/lang/tr.ini +++ b/res/lang/tr.ini @@ -13,6 +13,7 @@ err_dgn_oob = log mesaji err_domain = gecersiz etki alani + err_hostname = ana bilgisayar adi alinamadi diff --git a/res/lang/uk.ini b/res/lang/uk.ini index 20ad6c4..af96e1f 100644 --- a/res/lang/uk.ini +++ b/res/lang/uk.ini @@ -13,6 +13,7 @@ err_dgn_oob = повідомлення журналу (log) err_domain = недійсний домен + err_hostname = не вдалося отримати ім'я хосту diff --git a/res/lang/zh_CN.ini b/res/lang/zh_CN.ini index 639c224..2d70f3f 100644 --- a/res/lang/zh_CN.ini +++ b/res/lang/zh_CN.ini @@ -13,6 +13,7 @@ err_dgn_oob = 日志消息 err_domain = 无效的域 + err_hostname = 获取主机名失败 diff --git a/src/config/Lang.zig b/src/config/Lang.zig index 2e664f2..0e9b840 100644 --- a/src/config/Lang.zig +++ b/src/config/Lang.zig @@ -18,6 +18,7 @@ err_dgn_oob: []const u8 = "log message", err_domain: []const u8 = "invalid domain", err_empty_password: []const u8 = "empty password not allowed", err_envlist: []const u8 = "failed to get envlist", +err_get_active_tty: []const u8 = "failed to get active tty", err_hostname: []const u8 = "failed to get hostname", err_lock_state: []const u8 = "failed to get lock state", err_log: []const u8 = "failed to open log file", diff --git a/src/main.zig b/src/main.zig index 01047ff..fff3857 100644 --- a/src/main.zig +++ b/src/main.zig @@ -509,7 +509,11 @@ pub fn main() !void { var auth_fails: u64 = 0; // Switch to selected TTY - const active_tty = try interop.getActiveTty(allocator); + 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)}); + 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)}); From 44faa263b19d3e97977709db37fbc5599f29a6df Mon Sep 17 00:00:00 2001 From: mctaylors Date: Sun, 5 Oct 2025 19:26:09 +0200 Subject: [PATCH 281/530] change(config): add -n flag to prevent accidental backlight disabling (#840) Reviewed-on: https://codeberg.org/fairyglade/ly/pulls/840 Reviewed-by: AnErrupTion Co-authored-by: mctaylors Co-committed-by: mctaylors --- res/config.ini | 4 ++-- src/config/Config.zig | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/res/config.ini b/res/config.ini index 539a425..d4d1e6c 100644 --- a/res/config.ini +++ b/res/config.ini @@ -68,13 +68,13 @@ border_fg = 0x00FFFFFF box_title = null # Brightness decrease command -brightness_down_cmd = $PREFIX_DIRECTORY/bin/brightnessctl -q s 10%- +brightness_down_cmd = $PREFIX_DIRECTORY/bin/brightnessctl -q -n s 10%- # Brightness decrease key, or null to disable brightness_down_key = F5 # Brightness increase command -brightness_up_cmd = $PREFIX_DIRECTORY/bin/brightnessctl -q s +10% +brightness_up_cmd = $PREFIX_DIRECTORY/bin/brightnessctl -q -n s +10% # Brightness increase key, or null to disable brightness_up_key = F6 diff --git a/src/config/Config.zig b/src/config/Config.zig index fb782ac..296a9c7 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -18,9 +18,9 @@ bigclock_seconds: bool = false, blank_box: bool = true, border_fg: u32 = 0x00FFFFFF, box_title: ?[]const u8 = null, -brightness_down_cmd: [:0]const u8 = build_options.prefix_directory ++ "/bin/brightnessctl -q s 10%-", +brightness_down_cmd: [:0]const u8 = build_options.prefix_directory ++ "/bin/brightnessctl -q -n s 10%-", brightness_down_key: ?[]const u8 = "F5", -brightness_up_cmd: [:0]const u8 = build_options.prefix_directory ++ "/bin/brightnessctl -q s +10%", +brightness_up_cmd: [:0]const u8 = build_options.prefix_directory ++ "/bin/brightnessctl -q -n s +10%", brightness_up_key: ?[]const u8 = "F6", clear_password: bool = false, clock: ?[:0]const u8 = null, From 339e39d4962d5bdc9c38a9334a9c50e7b4d2184f Mon Sep 17 00:00:00 2001 From: ebits Date: Thu, 9 Oct 2025 18:48:46 +0200 Subject: [PATCH 282/530] Adding the battery status for the top bar alongside brightness controls (closes #821) (#826) Reviewed-on: https://codeberg.org/fairyglade/ly/pulls/826 Reviewed-by: AnErrupTion Co-authored-by: ebits Co-committed-by: ebits --- res/config.ini | 4 ++++ res/lang/ar.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/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 | 1 + res/lang/tr.ini | 1 + res/lang/uk.ini | 1 + res/lang/zh_CN.ini | 1 + src/config/Config.zig | 1 + src/config/Lang.zig | 1 + src/main.zig | 32 ++++++++++++++++++++++++++++++++ 23 files changed, 57 insertions(+) diff --git a/res/config.ini b/res/config.ini index d4d1e6c..5d2d03e 100644 --- a/res/config.ini +++ b/res/config.ini @@ -189,6 +189,10 @@ hide_version_string = false # Remove power management command hints hide_key_hints = false +# Set to null to disable battery status display +# Default is BAT0, the typical identifier for the primary battery +battery_id = null + # Initial text to show on the info line # If set to null, the info line defaults to the hostname initial_info_text = null diff --git a/res/lang/ar.ini b/res/lang/ar.ini index 6e1f3f5..78d9e93 100644 --- a/res/lang/ar.ini +++ b/res/lang/ar.ini @@ -43,6 +43,7 @@ err_perm_user = فشل في تخفيض صلاحيات المستخدم (User per err_pwnam = فشل في جلب معلومات المستخدم err_sleep = فشل في تنفيذ أمر sleep + err_tty_ctrl = فشل في نقل تحكم الطرفية (TTY) err_user_gid = فشل في تعيين معرّف المجموعة (GID) للمستخدم diff --git a/res/lang/cat.ini b/res/lang/cat.ini index c736153..fb829a6 100644 --- a/res/lang/cat.ini +++ b/res/lang/cat.ini @@ -45,6 +45,7 @@ err_pwnam = error en obtenir la informació de l'usuari + 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 diff --git a/res/lang/cs.ini b/res/lang/cs.ini index d9b9ae8..c1206fe 100644 --- a/res/lang/cs.ini +++ b/res/lang/cs.ini @@ -45,6 +45,7 @@ err_pwnam = nelze získat informace o uživateli + err_user_gid = nastavení GID uživatele selhalo err_user_init = inicializace uživatele selhala err_user_uid = nastavení UID uživateli selhalo diff --git a/res/lang/de.ini b/res/lang/de.ini index 7d5e405..5512d5e 100644 --- a/res/lang/de.ini +++ b/res/lang/de.ini @@ -43,6 +43,7 @@ err_perm_user = Fehler beim Heruntersetzen der Nutzerberechtigungen err_pwnam = Abrufen der Benutzerinformationen fehlgeschlagen err_sleep = Sleep-Befehl fehlgeschlagen + err_tty_ctrl = Fehler bei der TTY-Uebergabe err_user_gid = Fehler beim Setzen der Gruppen-ID diff --git a/res/lang/en.ini b/res/lang/en.ini index dfe60f3..1a8c16c 100644 --- a/res/lang/en.ini +++ b/res/lang/en.ini @@ -42,6 +42,7 @@ err_perm_group = failed to downgrade group permissions err_perm_user = failed to downgrade user permissions err_pwnam = failed to get user info err_sleep = failed to execute sleep command +err_battery = failed to load battery status err_switch_tty = failed to switch tty err_tty_ctrl = tty control transfer failed err_no_users = no users found diff --git a/res/lang/es.ini b/res/lang/es.ini index c249c20..776c998 100644 --- a/res/lang/es.ini +++ b/res/lang/es.ini @@ -45,6 +45,7 @@ err_pwnam = error al obtener la información del usuario + 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 diff --git a/res/lang/fr.ini b/res/lang/fr.ini index 1a5faba..87b7a93 100644 --- a/res/lang/fr.ini +++ b/res/lang/fr.ini @@ -42,6 +42,7 @@ err_perm_group = échec du déclassement des permissions de groupe err_perm_user = échec du déclassement des permissions utilisateur err_pwnam = échec de lecture des infos utilisateur err_sleep = échec de l'exécution de la commande de veille + err_switch_tty = échec du changement de terminal err_tty_ctrl = échec du transfert de contrôle du terminal err_no_users = aucun utilisateur trouvé diff --git a/res/lang/it.ini b/res/lang/it.ini index b366c4f..4ba807c 100644 --- a/res/lang/it.ini +++ b/res/lang/it.ini @@ -45,6 +45,7 @@ err_pwnam = impossibile ottenere dati utente + err_user_gid = impossibile impostare GID utente err_user_init = impossibile inizializzare utente err_user_uid = impossible impostare UID utente diff --git a/res/lang/ja_JP.ini b/res/lang/ja_JP.ini index 9a9555e..200422a 100644 --- a/res/lang/ja_JP.ini +++ b/res/lang/ja_JP.ini @@ -43,6 +43,7 @@ err_perm_user = ユーザー権限のダウングレードに失敗しました err_pwnam = ユーザー情報の取得に失敗しました err_sleep = スリープコマンドの実行に失敗しました + err_tty_ctrl = TTY制御の転送に失敗しました err_user_gid = ユーザーGIDの設定に失敗しました diff --git a/res/lang/pl.ini b/res/lang/pl.ini index 7bf0a33..065fb22 100644 --- a/res/lang/pl.ini +++ b/res/lang/pl.ini @@ -43,6 +43,7 @@ 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_tty_ctrl = nie udało się przekazać kontroli tty err_user_gid = nie udało się ustawić GID użytkownika diff --git a/res/lang/pt.ini b/res/lang/pt.ini index f8c9bd7..a1951fd 100644 --- a/res/lang/pt.ini +++ b/res/lang/pt.ini @@ -45,6 +45,7 @@ err_pwnam = erro ao obter informação do utilizador + 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 diff --git a/res/lang/pt_BR.ini b/res/lang/pt_BR.ini index 0c64ca4..f886816 100644 --- a/res/lang/pt_BR.ini +++ b/res/lang/pt_BR.ini @@ -45,6 +45,7 @@ err_pwnam = não foi possível obter informações do usuário + 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 diff --git a/res/lang/ro.ini b/res/lang/ro.ini index a90c54d..8503245 100644 --- a/res/lang/ro.ini +++ b/res/lang/ro.ini @@ -53,6 +53,7 @@ err_perm_user = nu s-a putut face downgrade permisiunilor de utilizator + login = utilizator logout = opreşte sesiunea diff --git a/res/lang/ru.ini b/res/lang/ru.ini index 95116f1..dab497c 100644 --- a/res/lang/ru.ini +++ b/res/lang/ru.ini @@ -42,6 +42,7 @@ err_perm_group = не удалось понизить права доступа err_perm_user = не удалось понизить права доступа пользователя err_pwnam = не удалось получить информацию о пользователе err_sleep = не удалось выполнить команду sleep + err_switch_tty = не удалось переключить tty err_tty_ctrl = передача управления tty не удалась err_no_users = пользователи не найдены diff --git a/res/lang/sr.ini b/res/lang/sr.ini index c99bf9e..1e3c6f7 100644 --- a/res/lang/sr.ini +++ b/res/lang/sr.ini @@ -45,6 +45,7 @@ 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 diff --git a/res/lang/sv.ini b/res/lang/sv.ini index 81233fe..3fc2bfd 100644 --- a/res/lang/sv.ini +++ b/res/lang/sv.ini @@ -45,6 +45,7 @@ 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 diff --git a/res/lang/tr.ini b/res/lang/tr.ini index 6759701..78de8c3 100644 --- a/res/lang/tr.ini +++ b/res/lang/tr.ini @@ -45,6 +45,7 @@ err_pwnam = kullanici bilgileri alinamadi + err_user_gid = kullanici icin GID ayarlanamadi err_user_init = kullanici oturumu baslatilamadi err_user_uid = kullanici icin UID ayarlanamadi diff --git a/res/lang/uk.ini b/res/lang/uk.ini index af96e1f..de41076 100644 --- a/res/lang/uk.ini +++ b/res/lang/uk.ini @@ -45,6 +45,7 @@ err_pwnam = не вдалося отримати дані користувача + err_user_gid = не вдалося змінити GID користувача err_user_init = не вдалося ініціалізувати користувача err_user_uid = не вдалося змінити UID користувача diff --git a/res/lang/zh_CN.ini b/res/lang/zh_CN.ini index 2d70f3f..5e77f8e 100644 --- a/res/lang/zh_CN.ini +++ b/res/lang/zh_CN.ini @@ -45,6 +45,7 @@ err_pwnam = 获取用户信息失败 + err_user_gid = 设置用户GID失败 err_user_init = 初始化用户失败 err_user_uid = 设置用户UID失败 diff --git a/src/config/Config.zig b/src/config/Config.zig index 296a9c7..4dfab4d 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -49,6 +49,7 @@ gameoflife_initial_density: f32 = 0.4, hide_borders: bool = false, hide_version_string: bool = false, hide_key_hints: bool = false, +battery_id: ?[]const u8 = "BAT0", initial_info_text: ?[]const u8 = null, input_len: u8 = 34, lang: []const u8 = "en", diff --git a/src/config/Lang.zig b/src/config/Lang.zig index 0e9b840..886644d 100644 --- a/src/config/Lang.zig +++ b/src/config/Lang.zig @@ -47,6 +47,7 @@ err_perm_group: []const u8 = "failed to downgrade group permissions", err_perm_user: []const u8 = "failed to downgrade user permissions", err_pwnam: []const u8 = "failed to get user info", err_sleep: []const u8 = "failed to execute sleep command", +err_battery: []const u8 = "failed to load battery status", err_switch_tty: []const u8 = "failed to switch tty", err_tty_ctrl: []const u8 = "tty control transfer failed", err_no_users: []const u8 = "no users found", diff --git a/src/main.zig b/src/main.zig index fff3857..3b71540 100644 --- a/src/main.zig +++ b/src/main.zig @@ -572,6 +572,22 @@ pub fn main() !void { length += ly_top_str.len + 1; } + var battery_bar_shown = false; + if (config.battery_id) |id| draw_battery: { + const battery_percentage = getBatteryPercentage(id) catch |err| { + try log_writer.print("failed to get battery percentage: {s}\n", .{@errorName(err)}); + try info_line.addMessage(lang.err_battery, config.error_bg, config.error_fg); + 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; + + const battery_y: usize = 1; + buffer.drawLabel(battery_str, 0, battery_y); + battery_bar_shown = 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; @@ -1162,6 +1178,22 @@ fn adjustBrightness(allocator: std.mem.Allocator, cmd: []const u8) !void { } } +fn getBatteryPercentage(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(); + + var buffer: [8]u8 = undefined; + const bytes_read = try battery_file.read(&buffer); + const capacity_str = buffer[0..bytes_read]; + + const trimmed = std.mem.trimRight(u8, capacity_str, "\n\r"); + + return try std.fmt.parseInt(u8, trimmed, 10); +} + fn getAuthErrorMsg(err: anyerror, lang: Lang) []const u8 { return switch (err) { error.GetPasswordNameFailed => lang.err_pwnam, From 81a17f2904808bfc9516541a956b7f6d9e95099b Mon Sep 17 00:00:00 2001 From: ebits Date: Sat, 11 Oct 2025 08:47:13 +0200 Subject: [PATCH 283/530] Fix: Default battery status to row 1 when hide_key_hints and hide_version_string is true (fixes #844) (#845) Default battery status behaviour to usize 0 when hide_key_hints and hide_version_string is true as a fix to issue [#844](https://codeberg.org/fairyglade/ly/issues/844) Reviewed-on: https://codeberg.org/fairyglade/ly/pulls/845 Reviewed-by: AnErrupTion Co-authored-by: ebits Co-committed-by: ebits --- src/main.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main.zig b/src/main.zig index 3b71540..9060222 100644 --- a/src/main.zig +++ b/src/main.zig @@ -583,7 +583,7 @@ pub fn main() !void { var battery_buf: [16:0]u8 = undefined; const battery_str = std.fmt.bufPrintZ(&battery_buf, "BAT: {d}%", .{battery_percentage}) catch break :draw_battery; - const battery_y: usize = 1; + const battery_y: usize = if (config.hide_key_hints and config.hide_version_string) 0 else 1; buffer.drawLabel(battery_str, 0, battery_y); battery_bar_shown = true; } From cb4f1952cdf12bff799ff8c9856501118e8f421e Mon Sep 17 00:00:00 2001 From: nyraa Date: Sat, 11 Oct 2025 09:24:15 +0200 Subject: [PATCH 284/530] Move version string to bottom-left corner (#846) In #834, it was decided to move the version string to the bottom-left corner, and a new PR was opened to address it. Co-authored-by: nyraa <112930946+nyraa@users.noreply.github.com> Reviewed-on: https://codeberg.org/fairyglade/ly/pulls/846 Reviewed-by: AnErrupTion Co-authored-by: nyraa Co-committed-by: nyraa --- src/main.zig | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/main.zig b/src/main.zig index 9060222..b82f8d5 100644 --- a/src/main.zig +++ b/src/main.zig @@ -32,7 +32,7 @@ const DisplayServer = enums.DisplayServer; const Entry = Environment.Entry; const termbox = interop.termbox; const temporary_allocator = std.heap.page_allocator; -const ly_top_str = "Ly version " ++ build_options.version; +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 { @@ -568,8 +568,7 @@ pub fn main() !void { if (!animation_timed_out) animation.draw(); if (!config.hide_version_string) { - buffer.drawLabel(ly_top_str, length, 0); - length += ly_top_str.len + 1; + buffer.drawLabel(ly_version_str, 0, buffer.height - 1); } var battery_bar_shown = false; @@ -583,7 +582,7 @@ pub fn main() !void { var battery_buf: [16:0]u8 = undefined; const battery_str = std.fmt.bufPrintZ(&battery_buf, "BAT: {d}%", .{battery_percentage}) catch break :draw_battery; - const battery_y: usize = if (config.hide_key_hints and config.hide_version_string) 0 else 1; + const battery_y: usize = if (config.hide_key_hints) 0 else 1; buffer.drawLabel(battery_str, 0, battery_y); battery_bar_shown = true; } From 1e2faad0f851c3b762b054f5045d77df0d06ec84 Mon Sep 17 00:00:00 2001 From: Galtrhan Date: Sun, 12 Oct 2025 20:57:28 +0200 Subject: [PATCH 285/530] Add Latvian language translation (#847) Reviewed-on: https://codeberg.org/fairyglade/ly/pulls/847 Reviewed-by: AnErrupTion Co-authored-by: Galtrhan Co-committed-by: Galtrhan --- res/lang/lv.ini | 70 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 res/lang/lv.ini diff --git a/res/lang/lv.ini b/res/lang/lv.ini new file mode 100644 index 0000000..a61c929 --- /dev/null +++ b/res/lang/lv.ini @@ -0,0 +1,70 @@ +authenticating = autentificējas... +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 +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_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_hostname = neizdevās iegūt hostname +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 +err_null = null rādītājs +err_numlock = neizdevās iestatīt numlock +err_pam = pam transakcija neizdevās +err_pam_abort = pam transakcija pārtraukta +err_pam_acct_expired = konts novecojis +err_pam_auth = autentifikācijas kļūda +err_pam_authinfo_unavail = neizdevās iegūt lietotāja informāciju +err_pam_authok_reqd = žetons beidzies +err_pam_buf = atmiņas bufera kļūda +err_pam_cred_err = neizdevās iestatīt akreditācijas datus +err_pam_cred_expired = akreditācijas dati novecojuši +err_pam_cred_insufficient = nepietiekami akreditācijas dati +err_pam_cred_unavail = neizdevās iegūt akreditācijas datus +err_pam_maxtries = sasniegts maksimālais mēģinājumu skaits +err_pam_perm_denied = piekļuve liegta +err_pam_session = sesijas kļūda +err_pam_sys = sistēmas kļūda +err_pam_user_unknown = nezināms lietotājs +err_path = neizdevās iestatīt ceļu +err_perm_dir = neizdevās mainīt pašreizējo mapi +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_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_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 +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 +insert = ievietot +login = pieteikties +logout = iziet +no_x11_support = x11 atbalsts atspējots kompilācijas laikā +normal = parastais +numlock = numlock +other = cits +password = parole +restart = restartēt +shell = terminālis +shutdown = izslēgt +sleep = snauda +wayland = wayland +x11 = x11 +xinitrc = xinitrc From 4f4855b5e98bb5523e4cde2a5d58b8ac83368577 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Sun, 12 Oct 2025 22:47:03 +0200 Subject: [PATCH 286/530] Implement dummy active TTY getter for FreeBSD Signed-off-by: AnErrupTion --- src/interop.zig | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/interop.zig b/src/interop.zig index 0c30be7..bd4ed65 100644 --- a/src/interop.zig +++ b/src/interop.zig @@ -89,8 +89,8 @@ fn PlatformStruct() type { // 1. Open /proc/self/stat to retrieve the tty_nr field // 2. Parse the tty_nr field to extract the major and minor device // numbers - // 3. Then, read every /sys/class/tty/[dir]/dev, where [dir] is every - // sub-directory + // 3. Then, read every /sys/class/tty/[dir]/dev, where [dir] is + // every sub-directory // 4. Finally, compare the major and minor device numbers with the // extracted values. If they correspond, parse [dir] to get the // TTY ID @@ -178,6 +178,10 @@ fn PlatformStruct() type { const result = pwd.setusercontext(null, entry.passwd_struct, @intCast(entry.uid), pwd.LOGIN_SETALL); if (result != 0) return error.SetUserUidFailed; } + + pub fn getActiveTtyImpl(_: std.mem.Allocator) !u8 { + return error.FeatureUnimplemented; + } }, else => @compileError("Unsupported target: " ++ builtin.os.tag), }; From bd335c8c91cd9e41cea893dd314131b1d8a5f6d4 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Mon, 13 Oct 2025 21:39:44 +0200 Subject: [PATCH 287/530] Add missing locales in build.zig Signed-off-by: AnErrupTion --- build.zig | 41 +++++++++++++++++++++++++---------------- 1 file changed, 25 insertions(+), 16 deletions(-) diff --git a/build.zig b/build.zig index 0f446d5..fd3715c 100644 --- a/build.zig +++ b/build.zig @@ -196,22 +196,31 @@ fn install_ly(allocator: std.mem.Allocator, patch_map: PatchMap, install_config: var lang_dir = std.fs.cwd().openDir(ly_lang_path, .{}) catch unreachable; defer lang_dir.close(); - try installFile("res/lang/cat.ini", lang_dir, ly_lang_path, "cat.ini", .{}); - try installFile("res/lang/cs.ini", lang_dir, ly_lang_path, "cs.ini", .{}); - try installFile("res/lang/de.ini", lang_dir, ly_lang_path, "de.ini", .{}); - try installFile("res/lang/en.ini", lang_dir, ly_lang_path, "en.ini", .{}); - try installFile("res/lang/es.ini", lang_dir, ly_lang_path, "es.ini", .{}); - try installFile("res/lang/fr.ini", lang_dir, ly_lang_path, "fr.ini", .{}); - try installFile("res/lang/it.ini", lang_dir, ly_lang_path, "it.ini", .{}); - try installFile("res/lang/pl.ini", lang_dir, ly_lang_path, "pl.ini", .{}); - try installFile("res/lang/pt.ini", lang_dir, ly_lang_path, "pt.ini", .{}); - try installFile("res/lang/pt_BR.ini", lang_dir, ly_lang_path, "pt_BR.ini", .{}); - try installFile("res/lang/ro.ini", lang_dir, ly_lang_path, "ro.ini", .{}); - try installFile("res/lang/ru.ini", lang_dir, ly_lang_path, "ru.ini", .{}); - try installFile("res/lang/sr.ini", lang_dir, ly_lang_path, "sr.ini", .{}); - try installFile("res/lang/sv.ini", lang_dir, ly_lang_path, "sv.ini", .{}); - try installFile("res/lang/tr.ini", lang_dir, ly_lang_path, "tr.ini", .{}); - try installFile("res/lang/uk.ini", lang_dir, ly_lang_path, "uk.ini", .{}); + const languages = [_][]const u8{ + "ar.ini", + "cat.ini", + "cs.ini", + "de.ini", + "en.ini", + "es.ini", + "fr.ini", + "it.ini", + "ja_JP.ini", + "pl.ini", + "pt.ini", + "pt_BR.ini", + "ro.ini", + "ru.ini", + "sr.ini", + "sv.ini", + "tr.ini", + "uk.ini", + "zh_CN.ini", + }; + + inline for (languages) |language| { + try installFile("res/lang/" ++ language, lang_dir, ly_lang_path, language, .{}); + } } { From b2f51e5bc84dcd810f4f1bad0ee6a288688376a1 Mon Sep 17 00:00:00 2001 From: Galtrhan Date: Mon, 13 Oct 2025 21:43:46 +0200 Subject: [PATCH 288/530] Adjust Latvian translation & add missing file entries to build.zig (#850) Change Latvian literal translation that did not quite fit the role of username to more appropriate. Added missing locale files to build.zig Reviewed-on: https://codeberg.org/fairyglade/ly/pulls/850 Reviewed-by: AnErrupTion Co-authored-by: Galtrhan Co-committed-by: Galtrhan --- build.zig | 4 ++++ res/lang/lv.ini | 3 ++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/build.zig b/build.zig index 0f446d5..c111bf8 100644 --- a/build.zig +++ b/build.zig @@ -196,6 +196,7 @@ fn install_ly(allocator: std.mem.Allocator, patch_map: PatchMap, install_config: var lang_dir = std.fs.cwd().openDir(ly_lang_path, .{}) catch unreachable; defer lang_dir.close(); + try installFile("res/lang/ar.ini", lang_dir, ly_lang_path, "ar.ini", .{}); try installFile("res/lang/cat.ini", lang_dir, ly_lang_path, "cat.ini", .{}); try installFile("res/lang/cs.ini", lang_dir, ly_lang_path, "cs.ini", .{}); try installFile("res/lang/de.ini", lang_dir, ly_lang_path, "de.ini", .{}); @@ -203,6 +204,8 @@ fn install_ly(allocator: std.mem.Allocator, patch_map: PatchMap, install_config: try installFile("res/lang/es.ini", lang_dir, ly_lang_path, "es.ini", .{}); try installFile("res/lang/fr.ini", lang_dir, ly_lang_path, "fr.ini", .{}); try installFile("res/lang/it.ini", lang_dir, ly_lang_path, "it.ini", .{}); + try installFile("res/lang/ja_JP.ini", lang_dir, ly_lang_path, "ja_JP.ini", .{}); + try installFile("res/lang/lv.ini", lang_dir, ly_lang_path, "lv.ini", .{}); try installFile("res/lang/pl.ini", lang_dir, ly_lang_path, "pl.ini", .{}); try installFile("res/lang/pt.ini", lang_dir, ly_lang_path, "pt.ini", .{}); try installFile("res/lang/pt_BR.ini", lang_dir, ly_lang_path, "pt_BR.ini", .{}); @@ -212,6 +215,7 @@ fn install_ly(allocator: std.mem.Allocator, patch_map: PatchMap, install_config: try installFile("res/lang/sv.ini", lang_dir, ly_lang_path, "sv.ini", .{}); try installFile("res/lang/tr.ini", lang_dir, ly_lang_path, "tr.ini", .{}); try installFile("res/lang/uk.ini", lang_dir, ly_lang_path, "uk.ini", .{}); + try installFile("res/lang/zh_CN.ini", lang_dir, ly_lang_path, "zh_CN.ini", .{}); } { diff --git a/res/lang/lv.ini b/res/lang/lv.ini index a61c929..dd15b05 100644 --- a/res/lang/lv.ini +++ b/res/lang/lv.ini @@ -54,7 +54,7 @@ err_xcb_conn = xcb savienojums neizdevās err_xsessions_dir = neizdevās atrast sesiju mapi err_xsessions_open = neizdevās atvērt sesiju mapi insert = ievietot -login = pieteikties +login = lietotājs logout = iziet no_x11_support = x11 atbalsts atspējots kompilācijas laikā normal = parastais @@ -68,3 +68,4 @@ sleep = snauda wayland = wayland x11 = x11 xinitrc = xinitrc + From 1839e4cb4437a212290704a86f0b2973e9b3cfa0 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Tue, 14 Oct 2025 19:42:16 +0200 Subject: [PATCH 289/530] Use LLVM in Debug (closes #832) Signed-off-by: AnErrupTion --- build.zig | 2 ++ 1 file changed, 2 insertions(+) diff --git a/build.zig b/build.zig index fd3715c..7014956 100644 --- a/build.zig +++ b/build.zig @@ -62,6 +62,8 @@ pub fn build(b: *std.Build) !void { .target = target, .optimize = optimize, }), + // Here until the native backend matures in terms of performance + .use_llvm = true, }); const zigini = b.dependency("zigini", .{ .target = target, .optimize = optimize }); From a34a5a97bd4258f6241772bd79f369d133bd4e45 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Tue, 14 Oct 2025 20:02:55 +0200 Subject: [PATCH 290/530] Execute shell in case exec_cmd is null Signed-off-by: AnErrupTion --- src/Environment.zig | 2 +- src/auth.zig | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Environment.zig b/src/Environment.zig index 05ab149..849cb08 100644 --- a/src/Environment.zig +++ b/src/Environment.zig @@ -17,7 +17,7 @@ entry_ini: ?Ini(Entry) = null, name: []const u8 = "", xdg_session_desktop: ?[]const u8 = null, xdg_desktop_names: ?[]const u8 = null, -cmd: []const u8 = "", +cmd: ?[]const u8 = null, specifier: []const u8 = "", display_server: DisplayServer = .wayland, is_terminal: bool = false, diff --git a/src/auth.zig b/src/auth.zig index b27f96c..ef62a6b 100644 --- a/src/auth.zig +++ b/src/auth.zig @@ -170,7 +170,7 @@ 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.tty}); - try executeX11Cmd(log_writer, allocator, user_entry.shell.?, user_entry.home.?, options, current_environment.cmd, vt); + try executeX11Cmd(log_writer, allocator, user_entry.shell.?, user_entry.home.?, options, current_environment.cmd orelse "", vt); }, } } @@ -450,7 +450,7 @@ fn executeX11Cmd(log_writer: *std.Io.Writer, allocator: std.mem.Allocator, shell _ = std.posix.waitpid(x_pid, 0); } -fn executeCmd(log_writer: *std.Io.Writer, allocator: std.mem.Allocator, shell: []const u8, options: AuthOptions, is_terminal: bool, exec_cmd: []const u8) !void { +fn executeCmd(log_writer: *std.Io.Writer, allocator: std.mem.Allocator, shell: []const u8, options: AuthOptions, is_terminal: bool, exec_cmd: ?[]const u8) !void { var maybe_log_file: ?std.fs.File = null; if (!is_terminal) { // For custom desktop entries, the "Terminal" value here determines if @@ -466,7 +466,7 @@ fn executeCmd(log_writer: *std.Io.Writer, 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 }); + const cmd_str = try std.fmt.bufPrintZ(&cmd_buffer, "{s} {s} {s}", .{ options.setup_cmd, options.login_cmd orelse "", exec_cmd orelse shell }); const args = [_:null]?[*:0]const u8{ shell_z, "-c", cmd_str }; return std.posix.execveZ(shell_z, &args, std.c.environ); From 3d977d2ff7846a33ccec6312346875328b051683 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Tue, 14 Oct 2025 20:05:50 +0200 Subject: [PATCH 291/530] Fix compatibility with Zig 0.15.2 Signed-off-by: AnErrupTion --- src/interop.zig | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/src/interop.zig b/src/interop.zig index bd4ed65..1a77fa9 100644 --- a/src/interop.zig +++ b/src/interop.zig @@ -105,7 +105,7 @@ fn PlatformStruct() type { var reader = file.reader(&file_buffer); var buffer: [1024]u8 = undefined; - const read = try reader.read(&buffer); + const read = try readBuffer(&reader.interface, &buffer); var iterator = std.mem.splitScalar(u8, buffer[0..read], ' '); var fields: [52][]const u8 = undefined; @@ -134,7 +134,7 @@ fn PlatformStruct() type { var reader = file.reader(&file_buffer); var buffer: [16]u8 = undefined; - const read = try reader.read(&buffer); + const read = try readBuffer(&reader.interface, &buffer); var device_iterator = std.mem.splitScalar(u8, buffer[0..(read - 1)], ':'); const device_major_str = device_iterator.next() orelse continue; @@ -151,6 +151,19 @@ fn PlatformStruct() type { return error.NoTtyFound; } + + fn readBuffer(reader: *std.Io.Reader, buffer: []u8) !usize { + var bytes_read: usize = 0; + var byte: u8 = try reader.takeByte(); + + while (byte != 0 and bytes_read < buffer.len) { + buffer[bytes_read] = byte; + bytes_read += 1; + byte = reader.takeByte() catch break; + } + + return bytes_read; + } }, .freebsd => struct { pub const kbio = @cImport({ From aef1dd9c1a90d2eff591031d5d813b05d36c0979 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Tue, 14 Oct 2025 20:09:39 +0200 Subject: [PATCH 292/530] Add more logs when logging into an X11 session Signed-off-by: AnErrupTion --- src/auth.zig | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/auth.zig b/src/auth.zig index ef62a6b..9b01597 100644 --- a/src/auth.zig +++ b/src/auth.zig @@ -388,6 +388,9 @@ fn xauth(log_writer: *std.Io.Writer, allocator: std.mem.Allocator, display_name: } fn executeX11Cmd(log_writer: *std.Io.Writer, allocator: std.mem.Allocator, shell: []const u8, home: []const u8, options: AuthOptions, desktop_cmd: []const u8, vt: []const u8) !void { + try log_writer.writeAll("[x11] getting free display\n"); + try log_writer.flush(); + const display_num = try getFreeDisplay(); var buf: [4]u8 = undefined; const display_name = try std.fmt.bufPrint(&buf, ":{d}", .{display_num}); @@ -395,8 +398,14 @@ fn executeX11Cmd(log_writer: *std.Io.Writer, allocator: std.mem.Allocator, shell const shell_z = try allocator.dupeZ(u8, shell); defer allocator.free(shell_z); + try log_writer.writeAll("[x11] creating xauth file\n"); + try log_writer.flush(); + try xauth(log_writer, allocator, display_name, shell_z, home, options); + try log_writer.writeAll("[x11] starting x server\n"); + try log_writer.flush(); + const pid = try std.posix.fork(); if (pid == 0) { var cmd_buffer: [1024]u8 = undefined; @@ -417,10 +426,16 @@ fn executeX11Cmd(log_writer: *std.Io.Writer, allocator: std.mem.Allocator, shell }; } + try log_writer.writeAll("[x11] getting x server pid\n"); + try log_writer.flush(); + // X Server detaches from the process. // PID can be fetched from /tmp/X{d}.lock const x_pid = try getXPid(display_num); + try log_writer.writeAll("[x11] launching environment\n"); + try log_writer.flush(); + xorg_pid = try std.posix.fork(); if (xorg_pid == 0) { var cmd_buffer: [1024]u8 = undefined; From b3f1e91cf688274c869b2cda0cd6be7214ef4945 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Tue, 14 Oct 2025 21:05:54 +0200 Subject: [PATCH 293/530] Remember last session for each user (closes #619) Signed-off-by: AnErrupTion --- res/init.sh | 98 ++++++++++++++++++++ src/config/{Save.zig => OldSave.zig} | 0 src/config/SavedUsers.zig | 22 +++++ src/config/migrator.zig | 33 ++++++- src/main.zig | 134 +++++++++++++++++---------- src/tui/components/InfoLine.zig | 4 +- src/tui/components/Session.zig | 36 ++++--- src/tui/components/UserList.zig | 62 ++++++++++--- src/tui/components/generic.zig | 27 +++--- 9 files changed, 324 insertions(+), 92 deletions(-) create mode 100644 res/init.sh rename src/config/{Save.zig => OldSave.zig} (100%) create mode 100644 src/config/SavedUsers.zig diff --git a/res/init.sh b/res/init.sh new file mode 100644 index 0000000..1f442f2 --- /dev/null +++ b/res/init.sh @@ -0,0 +1,98 @@ +#!/bin/sh +# Styling options +local BOLD = 0x01000000 +local UNDERLINE = 0x02000000 +local REVERSE = 0x04000000 +local ITALIC = 0x08000000 +local BLINK = 0x10000000 +local HI_BLACK = 0x20000000 +local BRIGHT = 0x40000000 +local DIM = 0x80000000 + +# Common colors +local DEFAULT = 0x00000000 +local RED = 0x00FF0000 +local GREEN = 0x0000FF00 +local YELLOW = 0x00FFFF00 +local BLUE = 0x000000FF +local MAGENTA = 0x00FF00FF +local CYAN = 0x0000FFFF +local WHITE = 0x00FFFFFF + +source lang/en.sh # From lang + +# It'd be a good idea to condense multiple options into 1 command (e.g. +# animation settings) +ly set-config allow_empty_password true +ly set-config animation none +ly set-config animation_timeout_sec 0 +ly set-config asterisk "*" +ly set-config auth_fails 10 +ly set-config bg $DEFAULT +ly set-config bigclock_12hr false +ly set-config bigclock_seconds false +ly set-config blank_box true +ly set-config border_fg $WHITE +ly set-config box_title null +ly set-config clear_password false +ly set-config clock null +ly set-config cmatrix_fg $GREEN +ly set-config cmatrix_head_col $(($WHITE | $BOLD)) +ly set-config cmatrix_min_codepoint 0x21 +ly set-config cmatrix_max_codepoint = 0x7B +ly set-config colormix_col1 $RED +ly set-config colormix_col2 $BLUE +ly set-config colormix_col3 $HI_BLACK +ly set-config default_input login +ly set-config doom_fire_height 6 +ly set-config doom_fire_spread 2 +ly set-config doom_top_color 0x009F2707 +ly set-config doom_middle_color 0x00C78F17 +ly set-config doom_bottom_color $WHITE +ly set-config error_bg $DEFAULT +ly set-config error_fg $(($RED | $BOLD)) +ly set-config fg $WHITE +ly set-config full_color true +ly set-config gameoflife_entropy_interval 10 +ly set-config gameoflife_fg $GREEN +ly set-config gameoflife_frame_delay 6 +ly set-config gameoflife_initial_density 0.4 +ly set-config hide_borders false +ly set-config initial_info_text null +ly set-config input_len 34 +ly set-config login_cmd null +ly set-config login_defs_path "/etc/login.defs" +ly set-config logout_cmd null +ly set-config ly_log "/var/log/ly.log" +ly set-config margin_box_h 2 +ly set-config margin_box_v 1 +ly set-config min_refresh_delta 5 +ly set-config numlock false +ly set-config path "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" +ly set-config save true +ly set-config service_name ly +ly set-config session_log "ly-session.log" +ly set-config setup_cmd "$CONFIG_DIRECTORY/ly/setup.sh" +ly set-config text_in_center false +ly set-config vi_default_mode normal +ly set-config vi_mode false +ly set-config x_cmd "$PREFIX_DIRECTORY/bin/X" +ly set-config xauth_cmd "$PREFIX_DIRECTORY/bin/xauth" + +# Replaces respective options +# X11 requires special support from the display manager, which is why a separate +# command is required +ly add-session "System shell" shell "/bin/sh" +ly add-x11-session xinitrc x11 "~/.xinitrc" + +# ly add-session-dir custom "$CONFIG_DIRECTORY/ly/custom-sessions" +ly add-session-dir wayland "$PREFIX_DIRECTORY/share/wayland-sessions" +ly add-x11-session-dir x11 "$PREFIX_DIRECTORY/share/xsessions" + +ly add-hud 0 0 "Ly version %VERSION" null null top left +ly add-hud 1 0 "F1 shutdown" F1 "/sbin/shutdown -a now" top left +ly add-hud 2 0 "F2 reboot" F2 "/sbin/shutdown -r now" top left +#ly add-hud 0 0 "F3 sleep" F3 null top left +ly add-hud 3 0 "F5 decrease brightness" F5 "$PREFIX_DIRECTORY/bin/brightnessctl -q s 10%-" top left +ly add-hud 4 0 "F6 increase brightness" F6 "$PREFIX_DIRECTORY/bin/brightnessctl -q s +10%" top left +ly add-hud 5 0 "%CLOCK" null null top right diff --git a/src/config/Save.zig b/src/config/OldSave.zig similarity index 100% rename from src/config/Save.zig rename to src/config/OldSave.zig diff --git a/src/config/SavedUsers.zig b/src/config/SavedUsers.zig new file mode 100644 index 0000000..dd08eb4 --- /dev/null +++ b/src/config/SavedUsers.zig @@ -0,0 +1,22 @@ +const std = @import("std"); + +const SavedUsers = @This(); + +const User = struct { + username: []const u8, + session_index: usize, +}; + +user_list: std.ArrayList(User), +last_username_index: ?usize, + +pub fn init() SavedUsers { + return .{ + .user_list = .empty, + .last_username_index = null, + }; +} + +pub fn deinit(self: *SavedUsers, allocator: std.mem.Allocator) void { + self.user_list.deinit(allocator); +} diff --git a/src/config/migrator.zig b/src/config/migrator.zig index bdbfe67..bfba870 100644 --- a/src/config/migrator.zig +++ b/src/config/migrator.zig @@ -5,7 +5,8 @@ const std = @import("std"); const ini = @import("zigini"); const Config = @import("Config.zig"); -const Save = @import("Save.zig"); +const OldSave = @import("OldSave.zig"); +const SavedUsers = @import("SavedUsers.zig"); const TerminalBuffer = @import("../tui/TerminalBuffer.zig"); const Color = TerminalBuffer.Color; @@ -186,8 +187,8 @@ pub fn lateConfigFieldHandler(config: *Config) void { } } -pub fn tryMigrateSaveFile(user_buf: *[32]u8) Save { - var save = Save{}; +pub fn tryMigrateFirstSaveFile(user_buf: *[32]u8) OldSave { + var save = OldSave{}; if (maybe_save_file) |path| { defer temporary_allocator.free(path); @@ -216,3 +217,29 @@ pub fn tryMigrateSaveFile(user_buf: *[32]u8) 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 }); + } + + return true; +} diff --git a/src/main.zig b/src/main.zig index fff3857..102e53d 100644 --- a/src/main.zig +++ b/src/main.zig @@ -21,7 +21,8 @@ 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 Save = @import("config/Save.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 UidRange = @import("UidRange.zig"); @@ -118,11 +119,14 @@ pub fn main() !void { var config: Config = undefined; var lang: Lang = undefined; - var save: Save = undefined; + var old_save_file_exists = false; var maybe_config_load_error: ?anyerror = null; var can_get_lock_state = true; var can_draw_clock = true; + var saved_users = SavedUsers.init(); + defer saved_users.deinit(allocator); + if (res.args.help != 0) { try clap.help(stderr, clap.Help, ¶ms, .{}); @@ -143,13 +147,15 @@ pub fn main() !void { var lang_ini = Ini(Lang).init(allocator); defer lang_ini.deinit(); - var save_ini = Ini(Save).init(allocator); - defer save_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.ini"; + 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); } const comment_characters = "#"; @@ -178,18 +184,9 @@ pub fn main() !void { }) catch Lang{}; if (config.save) { - save_path = try std.fmt.allocPrint(allocator, "{s}{s}save.ini", .{ s, trailing_slash }); + 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; - - var user_buf: [32]u8 = undefined; - save = save_ini.readFileToStruct(save_path, .{ - .fieldHandler = null, - .comment_characters = comment_characters, - }) catch migrator.tryMigrateSaveFile(&user_buf); - } - - if (maybe_config_load_error == null) { - migrator.lateConfigFieldHandler(&config); } } else { const config_path = build_options.config_directory ++ "/ly/config.ini"; @@ -210,17 +207,47 @@ pub fn main() !void { .fieldHandler = null, .comment_characters = comment_characters, }) catch Lang{}; + } - if (config.save) { - var user_buf: [32]u8 = undefined; - save = save_ini.readFileToStruct(save_path, .{ - .fieldHandler = null, - .comment_characters = comment_characters, - }) catch migrator.tryMigrateSaveFile(&user_buf); - } + if (maybe_config_load_error == null) { + migrator.lateConfigFieldHandler(&config); + } - if (maybe_config_load_error == null) { - migrator.lateConfigFieldHandler(&config); + var usernames = try getAllUsernames(allocator, config.login_defs_path); + defer { + for (usernames.items) |username| allocator.free(username); + usernames.deinit(allocator); + } + + 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; + + // Don't read the new save file if the old one still exists + if (old_save_file_exists) break :read_save_file; + + var save_file = std.fs.cwd().openFile(save_path, .{}) catch break :read_save_file; + defer save_file.close(); + + var file_buffer: [256]u8 = undefined; + var file_reader = save_file.reader(&file_buffer); + 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; + + while (reader.seek < reader.buffer.len) { + const line = reader.takeDelimiterInclusive('\n') catch break; + + var user = std.mem.splitScalar(u8, line[0..(line.len - 1)], ':'); + const username = user.next() orelse continue; + const session_index_str = user.next() orelse continue; + + const session_index = std.fmt.parseInt(usize, session_index_str, 10) catch continue; + + try saved_users.user_list.append(allocator, .{ + .username = username, + .session_index = session_index, + }); } } @@ -345,7 +372,9 @@ pub fn main() !void { try log_writer.print("failed to set numlock: {s}\n", .{@errorName(err)}); }; - var session = Session.init(allocator, &buffer); + var login: UserList = undefined; + + var session = Session.init(allocator, &buffer, &login); defer session.deinit(); addOtherEnvironment(&session, lang, .shell, null) catch |err| { @@ -396,12 +425,6 @@ pub fn main() !void { try crawl(&session, lang, dir, .custom); } - var usernames = try getAllUsernames(allocator, config.login_defs_path); - defer { - for (usernames.items) |username| allocator.free(username); - usernames.deinit(allocator); - } - if (usernames.items.len == 0) { // If we have no usernames, simply add an error to the info line. // This effectively means you can't login, since there would be no local @@ -411,7 +434,7 @@ pub fn main() !void { try log_writer.writeAll("no users found\n"); } - var login = try UserList.init(allocator, &buffer, usernames); + login = try UserList.init(allocator, &buffer, usernames, &saved_users, &session); defer login.deinit(); var password = Text.init(allocator, &buffer, true, config.asterisk); @@ -422,23 +445,21 @@ pub fn main() !void { // Load last saved username and desktop selection, if any if (config.save) { - if (save.user) |user| { + if (saved_users.last_username_index) |index| { + const user = 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 - // Note that we could instead save the username index, but migrating - // from the raw username to an index is non-trivial and I'm lazy :P for (usernames.items, 0..) |username, i| { - if (std.mem.eql(u8, username, user)) { + if (std.mem.eql(u8, username, user.username)) { login.label.current = i; break; } } active_input = .password; - } - if (save.session_index) |session_index| { - if (session_index < session.label.list.items.len) session.label.current = session_index; + if (user.session_index < session.label.list.items.len) session.label.current = user.session_index; } } @@ -853,22 +874,33 @@ pub fn main() !void { _ = termbox.tb_present(); if (config.save) save_last_settings: { - var file = std.fs.cwd().createFile(save_path, .{}) catch break :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_writer.writeAll("failed to save current user data\n") 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; + break :save_last_settings; + }; defer file.close(); - var file_buffer: [64]u8 = undefined; + var file_buffer: [256]u8 = undefined; var file_writer = file.writer(&file_buffer); var writer = &file_writer.interface; - const save_data = Save{ - .user = login.getCurrentUser(), - .session_index = session.label.current, - }; - ini.writeFromStruct(save_data, writer, null, .{}) catch break :save_last_settings; + 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 {}; + if (migrator.maybe_save_file) |path| { + std.fs.cwd().deleteFile(path) catch {}; + } else if (old_save_file_exists) { + std.fs.cwd().deleteFile(old_save_path) catch {}; + } } var shared_err = try SharedError.init(); @@ -877,7 +909,7 @@ pub fn main() !void { { session_pid = try std.posix.fork(); if (session_pid == 0) { - const current_environment = session.label.list.items[session.label.current]; + const current_environment = session.label.list.items[session.label.current].environment; const auth_options = auth.AuthOptions{ .tty = active_tty, .service_name = config.service_name, @@ -898,7 +930,7 @@ pub fn main() !void { }; std.posix.sigaction(std.posix.SIG.CHLD, &tty_control_transfer_act, null); - auth.authenticate(allocator, log_writer, auth_options, current_environment, login.getCurrentUser(), password.text.items) catch |err| { + auth.authenticate(allocator, log_writer, auth_options, current_environment, login.getCurrentUsername(), password.text.items) catch |err| { shared_err.writeError(err); std.process.exit(1); }; @@ -1026,7 +1058,7 @@ fn addOtherEnvironment(session: *Session, lang: Lang, display_server: DisplaySer .name = name, .xdg_session_desktop = null, .xdg_desktop_names = null, - .cmd = exec orelse "", + .cmd = exec, .specifier = lang.other, .display_server = display_server, .is_terminal = display_server == .shell, diff --git a/src/tui/components/InfoLine.zig b/src/tui/components/InfoLine.zig index 7d588fb..f31fc11 100644 --- a/src/tui/components/InfoLine.zig +++ b/src/tui/components/InfoLine.zig @@ -4,7 +4,7 @@ const generic = @import("generic.zig"); const Allocator = std.mem.Allocator; -const MessageLabel = generic.CyclableLabel(Message); +const MessageLabel = generic.CyclableLabel(Message, Message); const InfoLine = @This(); @@ -19,7 +19,7 @@ label: MessageLabel, pub fn init(allocator: Allocator, buffer: *TerminalBuffer) InfoLine { return .{ - .label = MessageLabel.init(allocator, buffer, drawItem), + .label = MessageLabel.init(allocator, buffer, drawItem, null, null), }; } diff --git a/src/tui/components/Session.zig b/src/tui/components/Session.zig index 527b978..53a6c90 100644 --- a/src/tui/components/Session.zig +++ b/src/tui/components/Session.zig @@ -3,41 +3,53 @@ const TerminalBuffer = @import("../TerminalBuffer.zig"); const enums = @import("../../enums.zig"); const Environment = @import("../../Environment.zig"); const generic = @import("generic.zig"); +const UserList = @import("UserList.zig"); const Allocator = std.mem.Allocator; const DisplayServer = enums.DisplayServer; -const EnvironmentLabel = generic.CyclableLabel(Environment); + +const Env = struct { + environment: Environment, + index: usize, +}; +const EnvironmentLabel = generic.CyclableLabel(Env, *UserList); const Session = @This(); label: EnvironmentLabel, -pub fn init(allocator: Allocator, buffer: *TerminalBuffer) Session { +pub fn init(allocator: Allocator, buffer: *TerminalBuffer, user_list: *UserList) Session { return .{ - .label = EnvironmentLabel.init(allocator, buffer, drawItem), + .label = EnvironmentLabel.init(allocator, buffer, drawItem, sessionChanged, user_list), }; } pub fn deinit(self: *Session) void { - for (self.label.list.items) |*environment| { - if (environment.entry_ini) |*entry_ini| entry_ini.deinit(); + for (self.label.list.items) |*env| { + if (env.environment.entry_ini) |*entry_ini| entry_ini.deinit(); } self.label.deinit(); } pub fn addEnvironment(self: *Session, environment: Environment) !void { - try self.label.addItem(environment); + try self.label.addItem(.{ .environment = environment, .index = self.label.list.items.len }); } -fn drawItem(label: *EnvironmentLabel, environment: Environment, x: usize, y: usize) bool { - const length = @min(environment.name.len, label.visible_length - 3); +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; + } +} + +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; - const nx = if (label.text_in_center) (label.x + (label.visible_length - environment.name.len) / 2) else (label.x + 2); - label.first_char_x = nx + environment.name.len; + 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; - label.buffer.drawLabel(environment.specifier, x, y); - label.buffer.drawLabel(environment.name, nx, label.y); + label.buffer.drawLabel(env.environment.specifier, x, y); + label.buffer.drawLabel(env.environment.name, nx, label.y); return true; } diff --git a/src/tui/components/UserList.zig b/src/tui/components/UserList.zig index 41eff39..6939298 100644 --- a/src/tui/components/UserList.zig +++ b/src/tui/components/UserList.zig @@ -1,45 +1,83 @@ const std = @import("std"); 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; -const UsernameText = generic.CyclableLabel([]const u8); +pub const User = struct { + name: []const u8, + session_index: *usize, + allocated_index: bool, +}; +const UserLabel = generic.CyclableLabel(User, *Session); const UserList = @This(); -label: UsernameText, +label: UserLabel, -pub fn init(allocator: Allocator, buffer: *TerminalBuffer, usernames: StringList) !UserList { +pub fn init(allocator: Allocator, buffer: *TerminalBuffer, usernames: StringList, saved_users: *SavedUsers, session: *Session) !UserList { var userList = UserList{ - .label = UsernameText.init(allocator, buffer, drawItem), + .label = UserLabel.init(allocator, buffer, drawItem, usernameChanged, session), }; for (usernames.items) |username| { if (username.len == 0) continue; - try userList.label.addItem(username); + var maybe_session_index: ?*usize = null; + for (saved_users.user_list.items) |*saved_user| { + if (std.mem.eql(u8, username, saved_user.username)) { + maybe_session_index = &saved_user.session_index; + break; + } + } + + var allocated_index = false; + if (maybe_session_index == null) { + maybe_session_index = try allocator.create(usize); + maybe_session_index.?.* = 0; + allocated_index = true; + } + + try userList.label.addItem(.{ + .name = username, + .session_index = maybe_session_index.?, + .allocated_index = allocated_index, + }); } return userList; } pub fn deinit(self: *UserList) void { + for (self.label.list.items) |user| { + if (user.allocated_index) { + self.label.allocator.destroy(user.session_index); + } + } + self.label.deinit(); } -pub fn getCurrentUser(self: UserList) []const u8 { - return self.label.list.items[self.label.current]; +pub fn getCurrentUsername(self: UserList) []const u8 { + return self.label.list.items[self.label.current].name; } -fn drawItem(label: *UsernameText, username: []const u8, _: usize, _: usize) bool { - const length = @min(username.len, label.visible_length - 3); +fn usernameChanged(user: User, maybe_session: ?*Session) void { + if (maybe_session) |session| { + session.label.current = user.session_index.*; + } +} + +fn drawItem(label: *UserLabel, user: User, _: usize, _: usize) bool { + const length = @min(user.name.len, label.visible_length - 3); if (length == 0) return false; - const x = if (label.text_in_center) (label.x + (label.visible_length - username.len) / 2) else (label.x + 2); - label.first_char_x = x + username.len; + 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; - label.buffer.drawLabel(username, x, label.y); + label.buffer.drawLabel(user.name, x, label.y); return true; } diff --git a/src/tui/components/generic.zig b/src/tui/components/generic.zig index 322bc8f..2f3c25a 100644 --- a/src/tui/components/generic.zig +++ b/src/tui/components/generic.zig @@ -2,11 +2,12 @@ const std = @import("std"); const interop = @import("../../interop.zig"); const TerminalBuffer = @import("../TerminalBuffer.zig"); -pub fn CyclableLabel(comptime ItemType: type) type { +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 ChangeItemFn = *const fn (ItemType, ?ChangeItemType) void; const termbox = interop.termbox; @@ -22,8 +23,10 @@ pub fn CyclableLabel(comptime ItemType: type) type { first_char_x: usize, text_in_center: bool, draw_item_fn: DrawItemFn, + change_item_fn: ?ChangeItemFn, + change_item_arg: ?ChangeItemType, - pub fn init(allocator: Allocator, buffer: *TerminalBuffer, draw_item_fn: DrawItemFn) Self { + pub fn init(allocator: Allocator, buffer: *TerminalBuffer, draw_item_fn: DrawItemFn, change_item_fn: ?ChangeItemFn, change_item_arg: ?ChangeItemType) Self { return .{ .allocator = allocator, .buffer = buffer, @@ -35,6 +38,8 @@ pub fn CyclableLabel(comptime ItemType: type) type { .first_char_x = 0, .text_in_center = false, .draw_item_fn = draw_item_fn, + .change_item_fn = change_item_fn, + .change_item_arg = change_item_arg, }; } @@ -94,21 +99,19 @@ pub fn CyclableLabel(comptime ItemType: type) type { } fn goLeft(self: *Self) void { - if (self.current == 0) { - self.current = self.list.items.len - 1; - return; - } + self.current = if (self.current == 0) self.list.items.len - 1 else self.current - 1; - 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 }); + } } fn goRight(self: *Self) void { - if (self.current == self.list.items.len - 1) { - self.current = 0; - return; - } + self.current = if (self.current == self.list.items.len - 1) 0 else self.current + 1; - 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 }); + } } }; } From 09c2cfb74d9e9672d389b2dd4cbee3c48cf4900f Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Tue, 14 Oct 2025 21:07:42 +0200 Subject: [PATCH 294/530] Remove file that was added by mistake Signed-off-by: AnErrupTion --- res/init.sh | 98 ----------------------------------------------------- 1 file changed, 98 deletions(-) delete mode 100644 res/init.sh diff --git a/res/init.sh b/res/init.sh deleted file mode 100644 index 1f442f2..0000000 --- a/res/init.sh +++ /dev/null @@ -1,98 +0,0 @@ -#!/bin/sh -# Styling options -local BOLD = 0x01000000 -local UNDERLINE = 0x02000000 -local REVERSE = 0x04000000 -local ITALIC = 0x08000000 -local BLINK = 0x10000000 -local HI_BLACK = 0x20000000 -local BRIGHT = 0x40000000 -local DIM = 0x80000000 - -# Common colors -local DEFAULT = 0x00000000 -local RED = 0x00FF0000 -local GREEN = 0x0000FF00 -local YELLOW = 0x00FFFF00 -local BLUE = 0x000000FF -local MAGENTA = 0x00FF00FF -local CYAN = 0x0000FFFF -local WHITE = 0x00FFFFFF - -source lang/en.sh # From lang - -# It'd be a good idea to condense multiple options into 1 command (e.g. -# animation settings) -ly set-config allow_empty_password true -ly set-config animation none -ly set-config animation_timeout_sec 0 -ly set-config asterisk "*" -ly set-config auth_fails 10 -ly set-config bg $DEFAULT -ly set-config bigclock_12hr false -ly set-config bigclock_seconds false -ly set-config blank_box true -ly set-config border_fg $WHITE -ly set-config box_title null -ly set-config clear_password false -ly set-config clock null -ly set-config cmatrix_fg $GREEN -ly set-config cmatrix_head_col $(($WHITE | $BOLD)) -ly set-config cmatrix_min_codepoint 0x21 -ly set-config cmatrix_max_codepoint = 0x7B -ly set-config colormix_col1 $RED -ly set-config colormix_col2 $BLUE -ly set-config colormix_col3 $HI_BLACK -ly set-config default_input login -ly set-config doom_fire_height 6 -ly set-config doom_fire_spread 2 -ly set-config doom_top_color 0x009F2707 -ly set-config doom_middle_color 0x00C78F17 -ly set-config doom_bottom_color $WHITE -ly set-config error_bg $DEFAULT -ly set-config error_fg $(($RED | $BOLD)) -ly set-config fg $WHITE -ly set-config full_color true -ly set-config gameoflife_entropy_interval 10 -ly set-config gameoflife_fg $GREEN -ly set-config gameoflife_frame_delay 6 -ly set-config gameoflife_initial_density 0.4 -ly set-config hide_borders false -ly set-config initial_info_text null -ly set-config input_len 34 -ly set-config login_cmd null -ly set-config login_defs_path "/etc/login.defs" -ly set-config logout_cmd null -ly set-config ly_log "/var/log/ly.log" -ly set-config margin_box_h 2 -ly set-config margin_box_v 1 -ly set-config min_refresh_delta 5 -ly set-config numlock false -ly set-config path "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" -ly set-config save true -ly set-config service_name ly -ly set-config session_log "ly-session.log" -ly set-config setup_cmd "$CONFIG_DIRECTORY/ly/setup.sh" -ly set-config text_in_center false -ly set-config vi_default_mode normal -ly set-config vi_mode false -ly set-config x_cmd "$PREFIX_DIRECTORY/bin/X" -ly set-config xauth_cmd "$PREFIX_DIRECTORY/bin/xauth" - -# Replaces respective options -# X11 requires special support from the display manager, which is why a separate -# command is required -ly add-session "System shell" shell "/bin/sh" -ly add-x11-session xinitrc x11 "~/.xinitrc" - -# ly add-session-dir custom "$CONFIG_DIRECTORY/ly/custom-sessions" -ly add-session-dir wayland "$PREFIX_DIRECTORY/share/wayland-sessions" -ly add-x11-session-dir x11 "$PREFIX_DIRECTORY/share/xsessions" - -ly add-hud 0 0 "Ly version %VERSION" null null top left -ly add-hud 1 0 "F1 shutdown" F1 "/sbin/shutdown -a now" top left -ly add-hud 2 0 "F2 reboot" F2 "/sbin/shutdown -r now" top left -#ly add-hud 0 0 "F3 sleep" F3 null top left -ly add-hud 3 0 "F5 decrease brightness" F5 "$PREFIX_DIRECTORY/bin/brightnessctl -q s 10%-" top left -ly add-hud 4 0 "F6 increase brightness" F6 "$PREFIX_DIRECTORY/bin/brightnessctl -q s +10%" top left -ly add-hud 5 0 "%CLOCK" null null top right From 76da16904f2efd6006ee6a2839c187a341bc53d3 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Tue, 14 Oct 2025 21:17:58 +0200 Subject: [PATCH 295/530] Update French translation Signed-off-by: AnErrupTion --- res/lang/fr.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/res/lang/fr.ini b/res/lang/fr.ini index 87b7a93..31c0971 100644 --- a/res/lang/fr.ini +++ b/res/lang/fr.ini @@ -42,7 +42,7 @@ err_perm_group = échec du déclassement des permissions de groupe err_perm_user = échec du déclassement des permissions utilisateur err_pwnam = échec de lecture des infos utilisateur err_sleep = échec de l'exécution de la commande de veille - +err_battery = échec de lecture de l'état de la batterie err_switch_tty = échec du changement de terminal err_tty_ctrl = échec du transfert de contrôle du terminal err_no_users = aucun utilisateur trouvé From 1f2453f0fb0a5fea005625112eb9df4aa80bace4 Mon Sep 17 00:00:00 2001 From: Matthew Rothlisberger Date: Wed, 15 Oct 2025 13:58:04 +0200 Subject: [PATCH 296/530] Disable battery status display by default (#852) A couple things to fix in the new battery status display configuration. I think this should be disabled by default. My reasoning: - Historically a conservative approach is taken with new capabilities in Ly; even the clock is disabled by default - The existing default creates a regression (error message) for anyone without `/sys/class/power_supply/BAT0` on their system (all non-portable PCs, and laptops that use a different identifier) - The battery status check causes animations to momentarily hang at a regular interval Other changes: - Comment for `battery_id` aligned with similar config switch comments (description / useful information / effect of null setting) - `battery_id` moved to its correct alphabetical position in the config file - Setting aligned between `Config.zig` and `config.ini` (the prototypical config file should reflect the actual default) - Configurations prefixed with `hide_` alphabetized Reviewed-on: https://codeberg.org/fairyglade/ly/pulls/852 Reviewed-by: AnErrupTion Co-authored-by: Matthew Rothlisberger Co-committed-by: Matthew Rothlisberger --- res/config.ini | 13 +++++++------ src/config/Config.zig | 4 ++-- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/res/config.ini b/res/config.ini index 5d2d03e..17503fe 100644 --- a/res/config.ini +++ b/res/config.ini @@ -41,6 +41,11 @@ asterisk = * # The number of failed authentications before a special animation is 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 +battery_id = null + # Background color id bg = 0x00000000 @@ -183,15 +188,11 @@ gameoflife_initial_density = 0.4 # Remove main box borders hide_borders = false -# Remove version number from the top left corner -hide_version_string = false - # Remove power management command hints hide_key_hints = false -# Set to null to disable battery status display -# Default is BAT0, the typical identifier for the primary battery -battery_id = null +# Remove version number from the top left corner +hide_version_string = false # Initial text to show on the info line # If set to null, the info line defaults to the hostname diff --git a/src/config/Config.zig b/src/config/Config.zig index 4dfab4d..53cc3c8 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -11,6 +11,7 @@ animation: Animation = .none, animation_timeout_sec: u12 = 0, asterisk: ?u32 = '*', auth_fails: u64 = 10, +battery_id: ?[]const u8 = null, bg: u32 = 0x00000000, bigclock: Bigclock = .none, bigclock_12hr: bool = false, @@ -47,9 +48,8 @@ gameoflife_entropy_interval: usize = 10, gameoflife_frame_delay: usize = 6, gameoflife_initial_density: f32 = 0.4, hide_borders: bool = false, -hide_version_string: bool = false, hide_key_hints: bool = false, -battery_id: ?[]const u8 = "BAT0", +hide_version_string: bool = false, initial_info_text: ?[]const u8 = null, input_len: u8 = 34, lang: []const u8 = "en", From e36872baa6519390c4c983891ebf7f68771e4565 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Wed, 15 Oct 2025 16:32:57 +0200 Subject: [PATCH 297/530] Don't spam battery status error if already tried once 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 2f1189d..ab04dfc 100644 --- a/src/main.zig +++ b/src/main.zig @@ -123,6 +123,7 @@ pub fn main() !void { var maybe_config_load_error: ?anyerror = null; 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); @@ -592,11 +593,13 @@ pub fn main() !void { buffer.drawLabel(ly_version_str, 0, buffer.height - 1); } - var battery_bar_shown = false; if (config.battery_id) |id| draw_battery: { + 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 info_line.addMessage(lang.err_battery, config.error_bg, config.error_fg); + can_draw_battery = false; break :draw_battery; }; @@ -605,7 +608,7 @@ pub fn main() !void { const battery_y: usize = if (config.hide_key_hints) 0 else 1; buffer.drawLabel(battery_str, 0, battery_y); - battery_bar_shown = true; + can_draw_battery = true; } if (config.bigclock != .none and buffer.box_height + (bigclock.HEIGHT + 2) * 2 < buffer.height) { From 657daafec8ebd7d3d03e1109b4d1ec49577bac17 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Fri, 17 Oct 2025 22:58:50 +0200 Subject: [PATCH 298/530] Fix crash after reading saved credentials for first time Signed-off-by: AnErrupTion --- src/main.zig | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/main.zig b/src/main.zig index ab04dfc..5962aac 100644 --- a/src/main.zig +++ b/src/main.zig @@ -250,6 +250,16 @@ pub fn main() !void { .session_index = session_index, }); } + + // If no save file previously existed, fill it up with all usernames + if (saved_users.user_list.items.len > 0) break :read_save_file; + + for (usernames.items) |user| { + try saved_users.user_list.append(allocator, .{ + .username = user, + .session_index = 0, + }); + } } var log_file: std.fs.File = undefined; @@ -446,7 +456,10 @@ pub fn main() !void { // Load last saved username and desktop selection, if any if (config.save) { - if (saved_users.last_username_index) |index| { + if (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; + const user = saved_users.user_list.items[index]; // Find user with saved name, and switch over to it From bb669c239ca0e3bce434ad50809159f1646712fb Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Fri, 17 Oct 2025 23:08:15 +0200 Subject: [PATCH 299/530] Remove config TTY options in OpenRC & runit services Signed-off-by: AnErrupTion --- res/ly-openrc | 6 +----- res/ly-runit-service/conf | 4 +--- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/res/ly-openrc b/res/ly-openrc index ac6ae0f..f6d222c 100644 --- a/res/ly-openrc +++ b/res/ly-openrc @@ -19,12 +19,8 @@ then commandUL="/sbin/agetty" fi -## Get the tty from the conf file -CONFTTY=$(cat $CONFIG_DIRECTORY/ly/config.ini | sed -n 's/^tty.*=[^1-9]*// p') - ## The execution vars -# If CONFTTY is empty then default to $DEFAULT_TTY -TTY="tty${CONFTTY:-$DEFAULT_TTY}" +TTY="tty$DEFAULT_TTY" TERM=linux BAUD=38400 # If we don't have getty then we should have agetty diff --git a/res/ly-runit-service/conf b/res/ly-runit-service/conf index fca1c76..ded7001 100644 --- a/res/ly-runit-service/conf +++ b/res/ly-runit-service/conf @@ -7,6 +7,4 @@ fi BAUD_RATE=38400 TERM_NAME=linux - -auxtty=$(cat $CONFIG_DIRECTORY/ly/config.ini 2>/dev/null 1| sed -n 's/\(^[[:space:]]*tty[[:space:]]*=[[:space:]]*\)\([[:digit:]][[:digit:]]*\)\(.*\)/\2/p') -TTY=tty${auxtty:-$DEFAULT_TTY} +TTY=tty$DEFAULT_TTY From 412994775bd37542d135a972968439e48ee4b44f Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Fri, 17 Oct 2025 23:10:52 +0200 Subject: [PATCH 300/530] Add SysVinit service (closes #224) Signed-off-by: AnErrupTion --- build.zig | 11 +++++++++ readme.md | 10 ++++++++ res/ly-sysvinit | 65 +++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 86 insertions(+) create mode 100755 res/ly-sysvinit diff --git a/build.zig b/build.zig index b2864d5..1373f00 100644 --- a/build.zig +++ b/build.zig @@ -8,6 +8,7 @@ const InitSystem = enum { runit, s6, dinit, + sysvinit, }; const min_zig_string = "0.15.0"; @@ -308,6 +309,15 @@ fn install_service(allocator: std.mem.Allocator, patch_map: PatchMap) !void { const patched_service = try patchFile(allocator, "res/ly-dinit", patch_map); try installText(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 patched_service = try patchFile(allocator, "res/ly-sysvinit", patch_map); + try installText(patched_service, service_dir, service_path, "ly", .{}); + }, } } @@ -339,6 +349,7 @@ pub fn Uninstaller(uninstall_config: bool) type { try deleteFile(allocator, 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"), } } }; diff --git a/readme.md b/readme.md index 4690b02..1919f91 100644 --- a/readme.md +++ b/readme.md @@ -159,6 +159,16 @@ To disable TTY 2, edit `/etc/s6/config/tty2.conf` and set `SPAWN="no"`. To disable TTY 2, go to `/etc/dinit.d/config/console.conf` and modify `ACTIVE_CONSOLES`. +### sysvinit + +``` +# zig build installexe -Dinit_system=sysvinit +# update-rc.d lightdm disable +# update-rc.d ly defaults +``` + +To disable TTY 2, go to `/etc/inittab` and comment out the line containing `tty2`. + ### Updating You can also install Ly without overrding the current configuration file. This diff --git a/res/ly-sysvinit b/res/ly-sysvinit new file mode 100755 index 0000000..f24dfd4 --- /dev/null +++ b/res/ly-sysvinit @@ -0,0 +1,65 @@ +#!/bin/sh +### BEGIN INIT INFO +# Provides: ly +# Required-Start: $remote_fs $syslog +# Required-Stop: $remote_fs $syslog +# Default-Start: 2 3 4 5 +# Default-Stop: 0 1 6 +# Short-Description: Ly display manager +# Description: Starts and stops the Ly display manager +### END INIT INFO +# +# Author: AnErrupTion +# + +PATH=/sbin:/usr/sbin:/bin:/usr/bin +DAEMON=/usr/bin/ly +TTY=/dev/tty$DEFAULT_TTY +PIDFILE=/var/run/ly.pid +NAME=ly +DESC="Ly display manager" + +. /lib/lsb/init-functions + +case "$1" in + start) + log_daemon_msg "Starting $DESC on $TTY..." + if [ -f "$PIDFILE" ]; then + log_progress_msg "$DESC is already running" + log_end_msg 0 + return 0 + fi + + # Ensure TTY exists + [ -c "$TTY" ] || { + log_failure_msg "$TTY does not exist" + return 1 + } + + start-stop-daemon --start --background --make-pidfile --pidfile $PIDFILE \ + --chdir / --exec /bin/sh -- -c "exec setsid sh -c 'exec <$TTY >$TTY 2>&1 $DAEMON'" + log_end_msg $? + ;; + stop) + log_daemon_msg "Stopping $DESC..." + start-stop-daemon --stop --pidfile $PIDFILE --retry 5 + RETVAL=$? + [ $RETVAL -eq 0 ] && rm -f "$PIDFILE" + log_end_msg $RETVAL + ;; + restart) + echo "Restarting $DESC..." + $0 stop + sleep 1 + $0 start + ;; + status) + status_of_proc -p $PIDFILE $DAEMON $NAME && exit 0 || exit $? + ;; + *) + echo "Usage: /etc/init.d/$NAME {start|stop|restart|status}" + exit 1 + ;; +esac + +exit 0 From 8df9603188b25c886037b96c2ebe5a0edf7ff885 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Fri, 17 Oct 2025 23:12:21 +0200 Subject: [PATCH 301/530] Install SysVinit service as an executable Signed-off-by: AnErrupTion --- build.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.zig b/build.zig index 1373f00..06b6d54 100644 --- a/build.zig +++ b/build.zig @@ -316,7 +316,7 @@ fn install_service(allocator: std.mem.Allocator, patch_map: PatchMap) !void { defer service_dir.close(); const patched_service = try patchFile(allocator, "res/ly-sysvinit", patch_map); - try installText(patched_service, service_dir, service_path, "ly", .{}); + try installText(patched_service, service_dir, service_path, "ly", .{ .mode = 0o755 }); }, } } From ed88458efdd789ece9e29d5e9be24289aec75248 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Sat, 18 Oct 2025 00:05:53 +0200 Subject: [PATCH 302/530] Fix platform-specific bugs for FreeBSD compilation Signed-off-by: AnErrupTion --- src/auth.zig | 17 ++++++++++++----- src/interop.zig | 5 ++++- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/src/auth.zig b/src/auth.zig index 9b01597..d0867fa 100644 --- a/src/auth.zig +++ b/src/auth.zig @@ -506,14 +506,17 @@ fn addUtmpEntry(entry: *Utmp, username: []const u8, pid: c_int) !void { entry.ut_type = utmp.USER_PROCESS; entry.ut_pid = pid; - var buf: [4096]u8 = undefined; - const ttyname = try std.os.getFdPath(std.posix.STDIN_FILENO, &buf); + var buf: [std.fs.max_path_bytes]u8 = undefined; + const tty_path = try std.os.getFdPath(std.posix.STDIN_FILENO, &buf); + // Get the TTY name (i.e. without the /dev/ prefix) var ttyname_buf: [@sizeOf(@TypeOf(entry.ut_line))]u8 = undefined; - _ = try std.fmt.bufPrintZ(&ttyname_buf, "{s}", .{ttyname["/dev/".len..]}); + const ttyname = try std.fmt.bufPrintZ(&ttyname_buf, "{s}", .{tty_path["/dev/".len..]}); entry.ut_line = ttyname_buf; - entry.ut_id = ttyname_buf["tty".len..7].*; + // Get the TTY ID (i.e. without the tty prefix) and truncate it to the size + // of ut_id if necessary + entry.ut_id = ttyname["tty".len..(@sizeOf(@TypeOf(entry.ut_id)) + "tty".len)].*; var username_buf: [@sizeOf(@TypeOf(entry.ut_user))]u8 = undefined; _ = try std.fmt.bufPrintZ(&username_buf, "{s}", .{username}); @@ -530,7 +533,11 @@ fn addUtmpEntry(entry: *Utmp, username: []const u8, pid: c_int) !void { .tv_sec = @intCast(time.seconds), .tv_usec = @intCast(time.microseconds), }; - entry.ut_addr_v6[0] = 0; + + // FreeBSD doesn't have this field + if (builtin.os.tag == .linux) { + entry.ut_addr_v6[0] = 0; + } utmp.setutxent(); _ = utmp.pututxline(entry); diff --git a/src/interop.zig b/src/interop.zig index 1a77fa9..2cdd746 100644 --- a/src/interop.zig +++ b/src/interop.zig @@ -20,7 +20,10 @@ 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("login_cap.h"); + if (builtin.os.tag == .freebsd) { + @cInclude("sys/types.h"); + @cInclude("login_cap.h"); + } }); const stdlib = @cImport({ From 02f5aa702d5e6e86aac63192f49503c5c818e31e Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Sat, 18 Oct 2025 00:28:12 +0200 Subject: [PATCH 303/530] Implement /etc/login.defs in interop, TODO for FreeBSD We should be able to parse the "minuid" and "maxuid" values in /etc/rc.conf to get the UID range of the system, with default values of 1000 to 32000 (as they don't seem to be present by default). Signed-off-by: AnErrupTion --- res/config.ini | 3 ++- src/interop.zig | 56 +++++++++++++++++++++++++++++++++++++++++++++++++ src/main.zig | 42 +------------------------------------ 3 files changed, 59 insertions(+), 42 deletions(-) diff --git a/res/config.ini b/res/config.ini index 17503fe..cb229f7 100644 --- a/res/config.ini +++ b/res/config.ini @@ -211,7 +211,8 @@ lang = en # You can also set environment variables in there, they'll persist until logout login_cmd = null -# Path for login.defs file (used for listing all local users on the system) +# Path for login.defs file (used for listing all local users on the system on +# Linux) login_defs_path = /etc/login.defs # Command executed when logging out diff --git a/src/interop.zig b/src/interop.zig index 2cdd746..af7a571 100644 --- a/src/interop.zig +++ b/src/interop.zig @@ -1,5 +1,6 @@ const std = @import("std"); const builtin = @import("builtin"); +const UidRange = @import("UidRange.zig"); pub const termbox = @import("termbox2"); @@ -155,6 +156,46 @@ fn PlatformStruct() type { return error.NoTtyFound; } + // 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(); + + const login_defs_buffer = try login_defs_file.readToEndAlloc(allocator, std.math.maxInt(u16)); + defer allocator.free(login_defs_buffer); + + var iterator = std.mem.splitScalar(u8, login_defs_buffer, '\n'); + var uid_range = UidRange{}; + + 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); + } else if (std.mem.startsWith(u8, trimmed_line, "UID_MAX")) { + uid_range.uid_max = try parseValue(std.posix.uid_t, "UID_MAX", trimmed_line); + } + } + + return uid_range; + } + + 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; + + while (iterator.next()) |slice| { + // Skip the slice if it's empty (whitespace) or is the name of the + // property (e.g. UID_MIN or UID_MAX) + if (slice.len == 0 or std.mem.eql(u8, slice, name)) continue; + maybe_value = std.fmt.parseInt(T, slice, 10) catch continue; + } + + return maybe_value orelse error.ValueNotFound; + } + fn readBuffer(reader: *std.Io.Reader, buffer: []u8) !usize { var bytes_read: usize = 0; var byte: u8 = try reader.takeByte(); @@ -198,6 +239,15 @@ fn PlatformStruct() type { pub fn getActiveTtyImpl(_: std.mem.Allocator) !u8 { return error.FeatureUnimplemented; } + + pub fn getUserIdRange(_: std.mem.Allocator, _: []const u8) !UidRange { + return .{ + // Hardcoded default values chosen from + // /usr/src/usr.sbin/pw/pw_conf.c + .uid_min = 1000, + .uid_max = 32000, + }; + } }, else => @compileError("Unsupported target: " ++ builtin.os.tag), }; @@ -330,3 +380,9 @@ pub fn getUsernameEntry(username: [:0]const u8) ?UsernameEntry { pub fn closePasswordDatabase() void { pwd.endpwent(); } + +// 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); +} diff --git a/src/main.zig b/src/main.zig index 5962aac..b69c669 100644 --- a/src/main.zig +++ b/src/main.zig @@ -25,7 +25,6 @@ const OldSave = @import("config/OldSave.zig"); const SavedUsers = @import("config/SavedUsers.zig"); const migrator = @import("config/migrator.zig"); const SharedError = @import("SharedError.zig"); -const UidRange = @import("UidRange.zig"); const StringList = std.ArrayListUnmanaged([]const u8); const Ini = ini.Ini; @@ -1151,7 +1150,7 @@ fn crawl(session: *Session, lang: Lang, path: []const u8, display_server: Displa } fn getAllUsernames(allocator: std.mem.Allocator, login_defs_path: []const u8) !StringList { - const uid_range = try getUserIdRange(allocator, login_defs_path); + const uid_range = try interop.getUserIdRange(allocator, login_defs_path); var usernames: StringList = .empty; var maybe_entry = interop.getNextUsernameEntry(); @@ -1171,45 +1170,6 @@ fn getAllUsernames(allocator: std.mem.Allocator, login_defs_path: []const u8) !S return usernames; } -// 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... -fn getUserIdRange(allocator: std.mem.Allocator, login_defs_path: []const u8) !UidRange { - const login_defs_file = try std.fs.cwd().openFile(login_defs_path, .{}); - defer login_defs_file.close(); - - const login_defs_buffer = try login_defs_file.readToEndAlloc(allocator, std.math.maxInt(u16)); - defer allocator.free(login_defs_buffer); - - var iterator = std.mem.splitScalar(u8, login_defs_buffer, '\n'); - var uid_range = UidRange{}; - - 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); - } else if (std.mem.startsWith(u8, trimmed_line, "UID_MAX")) { - uid_range.uid_max = try parseValue(std.posix.uid_t, "UID_MAX", trimmed_line); - } - } - - return uid_range; -} - -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; - - while (iterator.next()) |slice| { - // Skip the slice if it's empty (whitespace) or is the name of the - // property (e.g. UID_MIN or UID_MAX) - if (slice.len == 0 or std.mem.eql(u8, slice, name)) continue; - maybe_value = std.fmt.parseInt(T, slice, 10) catch continue; - } - - return maybe_value orelse error.ValueNotFound; -} - fn adjustBrightness(allocator: std.mem.Allocator, cmd: []const u8) !void { var brightness = std.process.Child.init(&[_][]const u8{ "/bin/sh", "-c", cmd }, allocator); brightness.stdout_behavior = .Ignore; From a3a8f1157558c235f28b743d447e88a0f3aab79c Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Sat, 18 Oct 2025 00:46:09 +0200 Subject: [PATCH 304/530] Add basic FreeBSD service, change default fallback TTY to 2 Signed-off-by: AnErrupTion --- build.zig | 13 ++++++++++++- readme.md | 16 ++++++++++++++++ res/ly-freebsd | 23 +++++++++++++++++++++++ 3 files changed, 51 insertions(+), 1 deletion(-) create mode 100644 res/ly-freebsd diff --git a/build.zig b/build.zig index 06b6d54..1a986f0 100644 --- a/build.zig +++ b/build.zig @@ -9,6 +9,7 @@ const InitSystem = enum { s6, dinit, sysvinit, + freebsd, }; const min_zig_string = "0.15.0"; @@ -42,7 +43,7 @@ pub fn build(b: *std.Build) !void { const version_str = try getVersionStr(b, "ly", ly_version); const enable_x11_support = b.option(bool, "enable_x11_support", "Enable X11 support (default is on)") orelse true; const default_tty = b.option(u8, "default_tty", "Set the TTY (default is 2)") orelse 2; - const fallback_tty = b.option(u8, "fallback_tty", "Set the fallback TTY (default is 1). This value gets embedded into the binary") orelse 1; + const fallback_tty = b.option(u8, "fallback_tty", "Set the fallback TTY (default is 2). This value gets embedded into the binary") orelse 2; default_tty_str = try std.fmt.allocPrint(b.allocator, "{d}", .{default_tty}); @@ -318,6 +319,15 @@ fn install_service(allocator: std.mem.Allocator, patch_map: PatchMap) !void { const patched_service = try patchFile(allocator, "res/ly-sysvinit", patch_map); try installText(patched_service, service_dir, service_path, "ly", .{ .mode = 0o755 }); }, + .freebsd => { + const service_path = try std.fs.path.join(allocator, &[_][]const u8{ dest_directory, config_directory, "/rc.d" }); + std.fs.cwd().makePath(service_path) catch {}; + var service_dir = std.fs.cwd().openDir(service_path, .{}) catch unreachable; + defer service_dir.close(); + + const patched_service = try patchFile(allocator, "res/ly-freebsd", patch_map); + try installText(patched_service, service_dir, service_path, "ly", .{ .mode = 0o755 }); + }, } } @@ -350,6 +360,7 @@ pub fn Uninstaller(uninstall_config: bool) type { }, .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, config_directory, "/rc.d/ly", "freebsd service not found"), } } }; diff --git a/readme.md b/readme.md index 1919f91..e19ead1 100644 --- a/readme.md +++ b/readme.md @@ -38,6 +38,12 @@ 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 ``` +### FreeBSD + +``` +# pkg install ca_root_nss libxcb git +``` + ## Packaging status [![Packaging status](https://repology.org/badge/vertical-allrepos/ly.svg?exclude_unsupported=1)](https://repology.org/project/ly/versions) @@ -169,6 +175,16 @@ To disable TTY 2, go to `/etc/dinit.d/config/console.conf` and modify To disable TTY 2, go to `/etc/inittab` and comment out the line containing `tty2`. +### FreeBSD + +``` +# zig build installexe -Dprefix_directory=/usr/local +# sysrc lightdm_enable="NO" +# sysrc ly_enable="YES" +``` + +To disable TTY 2, go to `/etc/ttys` and comment out the line starting with `ttyv2`. + ### Updating You can also install Ly without overrding the current configuration file. This diff --git a/res/ly-freebsd b/res/ly-freebsd new file mode 100644 index 0000000..9dfcb9f --- /dev/null +++ b/res/ly-freebsd @@ -0,0 +1,23 @@ +#!/bin/sh +# +# PROVIDE: ly +# REQUIRE: DAEMON +# KEYWORD: shutdown + +. /etc/rc.subr + +name=ly +rcvar=ly_enable + +command="/usr/local/bin/ly" + +load_rc_config $name + +# +# DO NOT CHANGE THESE DEFAULT VALUES HERE +# SET THEM IN THE /etc/rc.conf FILE +# +ly_enable=${ly_enable-"NO"} +pidfile=${ly_pidfile-"/var/run/ly.pid"} + +run_rc_command "$1" From 52d29bbd47ee00d01415bc40c2d2b0fd91bb1e48 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Sat, 18 Oct 2025 00:46:56 +0200 Subject: [PATCH 305/530] Fix FreeBSD installation in README Signed-off-by: AnErrupTion --- readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/readme.md b/readme.md index e19ead1..86c1ffb 100644 --- a/readme.md +++ b/readme.md @@ -178,7 +178,7 @@ To disable TTY 2, go to `/etc/inittab` and comment out the line containing `tty2 ### FreeBSD ``` -# zig build installexe -Dprefix_directory=/usr/local +# zig build installexe -Dprefix_directory=/usr/local -Dinit_system=freebsd # sysrc lightdm_enable="NO" # sysrc ly_enable="YES" ``` From 3faf3dec42493da2479dfc713cab7a4442027f05 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Sat, 18 Oct 2025 08:41:36 +0200 Subject: [PATCH 306/530] Fix login issue Signed-off-by: AnErrupTion --- readme.md | 2 +- src/auth.zig | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/readme.md b/readme.md index 86c1ffb..a02bcda 100644 --- a/readme.md +++ b/readme.md @@ -183,7 +183,7 @@ To disable TTY 2, go to `/etc/inittab` and comment out the line containing `tty2 # sysrc ly_enable="YES" ``` -To disable TTY 2, go to `/etc/ttys` and comment out the line starting with `ttyv2`. +To disable TTY 2, go to `/etc/ttys` and comment out the line starting with `ttyv1` (TTYs start at 0 in FreeBSD). ### Updating diff --git a/src/auth.zig b/src/auth.zig index d0867fa..6e6a2a9 100644 --- a/src/auth.zig +++ b/src/auth.zig @@ -511,12 +511,12 @@ fn addUtmpEntry(entry: *Utmp, username: []const u8, pid: c_int) !void { // Get the TTY name (i.e. without the /dev/ prefix) var ttyname_buf: [@sizeOf(@TypeOf(entry.ut_line))]u8 = undefined; - const ttyname = try std.fmt.bufPrintZ(&ttyname_buf, "{s}", .{tty_path["/dev/".len..]}); + _ = try std.fmt.bufPrintZ(&ttyname_buf, "{s}", .{tty_path["/dev/".len..]}); entry.ut_line = ttyname_buf; // Get the TTY ID (i.e. without the tty prefix) and truncate it to the size // of ut_id if necessary - entry.ut_id = ttyname["tty".len..(@sizeOf(@TypeOf(entry.ut_id)) + "tty".len)].*; + entry.ut_id = ttyname_buf["tty".len..(@sizeOf(@TypeOf(entry.ut_id)) + "tty".len)].*; var username_buf: [@sizeOf(@TypeOf(entry.ut_user))]u8 = undefined; _ = try std.fmt.bufPrintZ(&username_buf, "{s}", .{username}); From 541eae531134373d0d421ba7e7fecc12ce189337 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Sat, 18 Oct 2025 11:23:57 +0200 Subject: [PATCH 307/530] Update init service for FreeBSD Signed-off-by: AnErrupTion --- build.zig | 11 +++++------ readme.md | 16 ++++++++++++++-- res/ly-freebsd | 23 ----------------------- res/ly-freebsd-wrapper | 7 +++++++ 4 files changed, 26 insertions(+), 31 deletions(-) delete mode 100644 res/ly-freebsd create mode 100644 res/ly-freebsd-wrapper diff --git a/build.zig b/build.zig index 1a986f0..74c59cb 100644 --- a/build.zig +++ b/build.zig @@ -320,13 +320,12 @@ fn install_service(allocator: std.mem.Allocator, patch_map: PatchMap) !void { try installText(patched_service, service_dir, service_path, "ly", .{ .mode = 0o755 }); }, .freebsd => { - const service_path = try std.fs.path.join(allocator, &[_][]const u8{ dest_directory, config_directory, "/rc.d" }); - std.fs.cwd().makePath(service_path) catch {}; - var service_dir = std.fs.cwd().openDir(service_path, .{}) catch unreachable; - defer service_dir.close(); + 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 patched_service = try patchFile(allocator, "res/ly-freebsd", patch_map); - try installText(patched_service, service_dir, service_path, "ly", .{ .mode = 0o755 }); + const patched_wrapper = try patchFile(allocator, "res/ly-freebsd-wrapper", patch_map); + try installText(patched_wrapper, executable_dir, exe_path, "ly_wrapper", .{ .mode = 0o755 }); }, } } diff --git a/readme.md b/readme.md index a02bcda..c9daae7 100644 --- a/readme.md +++ b/readme.md @@ -180,10 +180,22 @@ To disable TTY 2, go to `/etc/inittab` and comment out the line containing `tty2 ``` # zig build installexe -Dprefix_directory=/usr/local -Dinit_system=freebsd # sysrc lightdm_enable="NO" -# sysrc ly_enable="YES" ``` -To disable TTY 2, go to `/etc/ttys` and comment out the line starting with `ttyv1` (TTYs start at 0 in FreeBSD). +To enable Ly, add the following entry to `/etc/gettytab`: + +``` +Ly:\ + :lo=/usr/local/bin/ly_wrapper:\ + :al=root: +``` + +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 +``` ### Updating diff --git a/res/ly-freebsd b/res/ly-freebsd deleted file mode 100644 index 9dfcb9f..0000000 --- a/res/ly-freebsd +++ /dev/null @@ -1,23 +0,0 @@ -#!/bin/sh -# -# PROVIDE: ly -# REQUIRE: DAEMON -# KEYWORD: shutdown - -. /etc/rc.subr - -name=ly -rcvar=ly_enable - -command="/usr/local/bin/ly" - -load_rc_config $name - -# -# DO NOT CHANGE THESE DEFAULT VALUES HERE -# SET THEM IN THE /etc/rc.conf FILE -# -ly_enable=${ly_enable-"NO"} -pidfile=${ly_pidfile-"/var/run/ly.pid"} - -run_rc_command "$1" diff --git a/res/ly-freebsd-wrapper b/res/ly-freebsd-wrapper new file mode 100644 index 0000000..23544b7 --- /dev/null +++ b/res/ly-freebsd-wrapper @@ -0,0 +1,7 @@ +#!/bin/sh + +# On FreeBSD, even if we override the default login program, getty will still +# try to append "login -fp root" as arguments to Ly, which is not supported. +# To avoid this, we use a wrapper script that ignores these arguments before +# actually executing Ly. +exec $PREFIX_DIRECTORY/bin/$EXECUTABLE_NAME From 44c8acff166bafea8d4283a7b945c9786b69849c Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Sat, 18 Oct 2025 12:03:48 +0200 Subject: [PATCH 308/530] Open new log file handle after fork() This would also need to be done in the nested fork() calls. Signed-off-by: AnErrupTion --- src/auth.zig | 6 +++++ src/main.zig | 64 ++++++++++++++++++++++++++++++++++------------------ 2 files changed, 48 insertions(+), 22 deletions(-) diff --git a/src/auth.zig b/src/auth.zig index 6e6a2a9..62edbfa 100644 --- a/src/auth.zig +++ b/src/auth.zig @@ -57,25 +57,31 @@ pub fn authenticate(allocator: std.mem.Allocator, log_writer: *std.Io.Writer, op }; var handle: ?*interop.pam.pam_handle = undefined; + try log_writer.writeAll("[pam] starting session\n"); 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"); 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"); status = interop.pam.pam_authenticate(handle, 0); if (status != interop.pam.PAM_SUCCESS) return pamDiagnose(status); + try log_writer.writeAll("[pam] validating account\n"); 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"); 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"); 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); diff --git a/src/main.zig b/src/main.zig index b69c669..decc6d3 100644 --- a/src/main.zig +++ b/src/main.zig @@ -264,29 +264,9 @@ pub fn main() !void { var log_file: std.fs.File = undefined; defer log_file.close(); - var could_open_log_file = true; - open_log_file: { - log_file = std.fs.cwd().openFile(config.ly_log, .{ .mode = .write_only }) catch std.fs.cwd().createFile(config.ly_log, .{ .mode = 0o666 }) catch { - // If we could neither open an existing log file nor create a new - // one, abort. - could_open_log_file = false; - break :open_log_file; - }; - } - - if (!could_open_log_file) { - log_file = try std.fs.openFileAbsolute("/dev/null", .{ .mode = .write_only }); - } - var log_buffer: [1024]u8 = undefined; - var log_file_writer = log_file.writer(&log_buffer); - - // Seek to the end of the log file - if (could_open_log_file) { - const stat = try log_file.stat(); - try log_file_writer.seekTo(stat.size); - } - + var log_file_writer: std.fs.File.Writer = undefined; + var could_open_log_file = try openLogFile(config.ly_log, &log_file, &log_buffer, &log_file_writer); var log_writer = &log_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 @@ -937,6 +917,8 @@ pub fn main() !void { defer shared_err.deinit(); { + log_file.close(); + session_pid = try std.posix.fork(); if (session_pid == 0) { const current_environment = session.label.list.items[session.label.current].environment; @@ -960,10 +942,18 @@ pub fn main() !void { }; std.posix.sigaction(std.posix.SIG.CHLD, &tty_control_transfer_act, null); + could_open_log_file = try openLogFile(config.ly_log, &log_file, &log_buffer, &log_file_writer); + log_writer = &log_file_writer.interface; + defer log_file.close(); + auth.authenticate(allocator, log_writer, auth_options, current_environment, login.getCurrentUsername(), password.text.items) catch |err| { shared_err.writeError(err); + + try log_writer.flush(); std.process.exit(1); }; + + try log_writer.flush(); std.process.exit(0); } @@ -972,6 +962,9 @@ pub fn main() !void { // 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; + + could_open_log_file = try openLogFile(config.ly_log, &log_file, &log_buffer, &log_file_writer); + log_writer = &log_file_writer.interface; } // Take back control of the TTY @@ -1060,6 +1053,33 @@ pub fn main() !void { } } +fn openLogFile(path: []const u8, log_file: *std.fs.File, buffer: []u8, writer: *std.fs.File.Writer) !bool { + var could_open_log_file = true; + open_log_file: { + log_file.* = std.fs.cwd().openFile(path, .{ .mode = .write_only }) catch std.fs.cwd().createFile(path, .{ .mode = 0o666 }) catch { + // If we could neither open an existing log file nor create a new + // one, abort. + could_open_log_file = false; + break :open_log_file; + }; + } + + if (!could_open_log_file) { + log_file.* = try std.fs.openFileAbsolute("/dev/null", .{ .mode = .write_only }); + } + + var log_file_writer = log_file.writer(buffer); + + // Seek to the end of the log file + if (could_open_log_file) { + const stat = try log_file.stat(); + try log_file_writer.seekTo(stat.size); + } + + writer.* = log_file_writer; + return could_open_log_file; +} + 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 bfb3f925d9b003c5ca68c95c5e9770fe13ca241e Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Sat, 18 Oct 2025 13:47:52 +0200 Subject: [PATCH 309/530] Use shutdown -p now on FreeBSD, update instructions Signed-off-by: AnErrupTion --- build.zig | 6 +++++- readme.md | 2 +- res/config.ini | 2 +- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/build.zig b/build.zig index 74c59cb..3b9e833 100644 --- a/build.zig +++ b/build.zig @@ -134,6 +134,10 @@ 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, patch_map, install_config); try install_service(allocator, patch_map); } @@ -359,7 +363,7 @@ pub fn Uninstaller(uninstall_config: bool) type { }, .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, config_directory, "/rc.d/ly", "freebsd service not found"), + .freebsd => try deleteFile(allocator, prefix_directory, "/bin/ly_wrapper", "freebsd wrapper not found"), } } }; diff --git a/readme.md b/readme.md index c9daae7..5a46679 100644 --- a/readme.md +++ b/readme.md @@ -178,7 +178,7 @@ To disable TTY 2, go to `/etc/inittab` and comment out the line containing `tty2 ### FreeBSD ``` -# zig build installexe -Dprefix_directory=/usr/local -Dinit_system=freebsd +# zig build installexe -Dprefix_directory=/usr/local -Dconfig_directory=/usr/local/etc -Dinit_system=freebsd # sysrc lightdm_enable="NO" ``` diff --git a/res/config.ini b/res/config.ini index cb229f7..e67c609 100644 --- a/res/config.ini +++ b/res/config.ini @@ -264,7 +264,7 @@ session_log = ly-session.log setup_cmd = $CONFIG_DIRECTORY/ly/setup.sh # Command executed when pressing shutdown_key -shutdown_cmd = /sbin/shutdown -a now +shutdown_cmd = /sbin/shutdown $PLATFORM_SHUTDOWN_ARG now # Specifies the key used for shutdown (F1-F12) shutdown_key = F1 From 1fbcb10110a593936bb38053eab431aa5703a289 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Sat, 18 Oct 2025 15:29:47 +0200 Subject: [PATCH 310/530] Open new log file after every fork() where necessary Signed-off-by: AnErrupTion --- src/LogFile.zig | 51 ++++++++++++++++++++++++++++++++++++++++++ src/auth.zig | 59 +++++++++++++++++++++++++++---------------------- src/main.zig | 56 +++++++++++----------------------------------- 3 files changed, 96 insertions(+), 70 deletions(-) create mode 100644 src/LogFile.zig diff --git a/src/LogFile.zig b/src/LogFile.zig new file mode 100644 index 0000000..46b353a --- /dev/null +++ b/src/LogFile.zig @@ -0,0 +1,51 @@ +const std = @import("std"); + +const LogFile = @This(); + +path: []const u8, +could_open_log_file: bool = undefined, +file: std.fs.File = undefined, +buffer: []u8, +file_writer: std.fs.File.Writer = undefined, + +pub fn init(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); + return log_file; +} + +pub fn reinit(self: *LogFile) !void { + self.could_open_log_file = try openLogFile(self.path, self); +} + +pub fn deinit(self: *LogFile) void { + self.file_writer.interface.flush() catch {}; + self.file.close(); +} + +fn openLogFile(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 { + // If we could neither open an existing log file nor create a new + // one, abort. + could_open_log_file = false; + break :open_log_file; + }; + } + + if (!could_open_log_file) { + log_file.file = try std.fs.openFileAbsolute("/dev/null", .{ .mode = .write_only }); + } + + var log_file_writer = log_file.file.writer(log_file.buffer); + + // Seek to the end of the log file + if (could_open_log_file) { + const stat = try log_file.file.stat(); + try log_file_writer.seekTo(stat.size); + } + + log_file.file_writer = log_file_writer; + return could_open_log_file; +} diff --git a/src/auth.zig b/src/auth.zig index 62edbfa..394b432 100644 --- a/src/auth.zig +++ b/src/auth.zig @@ -5,6 +5,7 @@ const enums = @import("enums.zig"); 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 utmp = interop.utmp; @@ -32,7 +33,7 @@ pub fn sessionSignalHandler(i: c_int) callconv(.c) void { if (child_pid > 0) _ = std.c.kill(child_pid, i); } -pub fn authenticate(allocator: std.mem.Allocator, log_writer: *std.Io.Writer, options: AuthOptions, current_environment: Environment, login: []const u8, password: []const u8) !void { +pub fn authenticate(allocator: std.mem.Allocator, 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}); @@ -57,6 +58,8 @@ pub fn authenticate(allocator: std.mem.Allocator, log_writer: *std.Io.Writer, op }; var handle: ?*interop.pam.pam_handle = undefined; + var log_writer = &log_file.file_writer.interface; + try log_writer.writeAll("[pam] starting session\n"); var status = interop.pam.pam_start(options.service_name, null, &conv, &handle); if (status != interop.pam.PAM_SUCCESS) return pamDiagnose(status); @@ -100,15 +103,23 @@ pub fn authenticate(allocator: std.mem.Allocator, log_writer: *std.Io.Writer, op var shared_err = try SharedError.init(); defer shared_err.deinit(); + log_file.deinit(); + child_pid = try std.posix.fork(); if (child_pid == 0) { - try log_writer.writeAll("starting session\n"); - try log_writer.flush(); + try log_file.reinit(); + log_writer = &log_file.file_writer.interface; - startSession(log_writer, allocator, options, tty_str, user_entry, handle, current_environment) catch |e| { + try log_writer.writeAll("starting session\n"); + + startSession(log_file, allocator, options, tty_str, user_entry, handle, current_environment) catch |e| { shared_err.writeError(e); + + log_file.deinit(); std.process.exit(1); }; + + log_file.deinit(); std.process.exit(0); } @@ -134,13 +145,15 @@ pub fn authenticate(allocator: std.mem.Allocator, log_writer: *std.Io.Writer, op // Wait for the session to stop _ = std.posix.waitpid(child_pid, 0); + try log_file.reinit(); + removeUtmpEntry(&entry); if (shared_err.readError()) |err| return err; } fn startSession( - log_writer: *std.Io.Writer, + log_file: *LogFile, allocator: std.mem.Allocator, options: AuthOptions, tty_str: []u8, @@ -172,11 +185,11 @@ fn startSession( // Execute what the user requested switch (current_environment.display_server) { - .wayland, .shell, .custom => try executeCmd(log_writer, allocator, user_entry.shell.?, options, current_environment.is_terminal, current_environment.cmd), + .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}); - try executeX11Cmd(log_writer, allocator, user_entry.shell.?, user_entry.home.?, options, current_environment.cmd orelse "", vt); + try executeX11Cmd(log_file, allocator, user_entry.shell.?, user_entry.home.?, options, current_environment.cmd orelse "", vt); }, } } @@ -369,7 +382,7 @@ fn mcookie() [Md5.digest_length * 2]u8 { return std.fmt.bytesToHex(&out, .lower); } -fn xauth(log_writer: *std.Io.Writer, allocator: std.mem.Allocator, display_name: []u8, shell: [*:0]const u8, home: []const u8, options: AuthOptions) !void { +fn xauth(log_file: *LogFile, allocator: std.mem.Allocator, display_name: []u8, shell: [*:0]const u8, home: []const u8, options: AuthOptions) !void { const xauthority = try createXauthFile(home); try interop.setEnvironmentVariable(allocator, "XAUTHORITY", xauthority, true); try interop.setEnvironmentVariable(allocator, "DISPLAY", display_name, true); @@ -388,15 +401,15 @@ fn xauth(log_writer: *std.Io.Writer, allocator: std.mem.Allocator, display_name: const status = std.posix.waitpid(pid, 0); if (status.status != 0) { - try log_writer.print("xauth command failed with status {d}\n", .{status.status}); + try log_file.file_writer.interface.print("xauth command failed with status {d}\n", .{status.status}); return error.XauthFailed; } } -fn executeX11Cmd(log_writer: *std.Io.Writer, allocator: std.mem.Allocator, shell: []const u8, home: []const u8, options: AuthOptions, desktop_cmd: []const u8, vt: []const u8) !void { - try log_writer.writeAll("[x11] getting free display\n"); - try log_writer.flush(); +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; + try log_writer.writeAll("[x11] getting free display\n"); const display_num = try getFreeDisplay(); var buf: [4]u8 = undefined; const display_name = try std.fmt.bufPrint(&buf, ":{d}", .{display_num}); @@ -405,13 +418,9 @@ fn executeX11Cmd(log_writer: *std.Io.Writer, allocator: std.mem.Allocator, shell defer allocator.free(shell_z); try log_writer.writeAll("[x11] creating xauth file\n"); - try log_writer.flush(); - - try xauth(log_writer, allocator, display_name, shell_z, home, options); + try xauth(log_file, allocator, display_name, shell_z, home, options); try log_writer.writeAll("[x11] starting x server\n"); - try log_writer.flush(); - const pid = try std.posix.fork(); if (pid == 0) { var cmd_buffer: [1024]u8 = undefined; @@ -432,16 +441,12 @@ fn executeX11Cmd(log_writer: *std.Io.Writer, allocator: std.mem.Allocator, shell }; } - try log_writer.writeAll("[x11] getting x server pid\n"); - try log_writer.flush(); - // 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"); const x_pid = try getXPid(display_num); try log_writer.writeAll("[x11] launching environment\n"); - try log_writer.flush(); - xorg_pid = try std.posix.fork(); if (xorg_pid == 0) { var cmd_buffer: [1024]u8 = undefined; @@ -471,14 +476,14 @@ fn executeX11Cmd(log_writer: *std.Io.Writer, allocator: std.mem.Allocator, shell _ = std.posix.waitpid(x_pid, 0); } -fn executeCmd(log_writer: *std.Io.Writer, allocator: std.mem.Allocator, shell: []const u8, options: AuthOptions, is_terminal: bool, exec_cmd: ?[]const u8) !void { +fn executeCmd(global_log_file: *LogFile, allocator: std.mem.Allocator, shell: []const u8, options: AuthOptions, is_terminal: bool, exec_cmd: ?[]const u8) !void { 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| { - maybe_log_file = try redirectStandardStreams(log_writer, log_path, true); + maybe_log_file = try redirectStandardStreams(global_log_file, log_path, true); } } defer if (maybe_log_file) |log_file| log_file.close(); @@ -493,12 +498,12 @@ fn executeCmd(log_writer: *std.Io.Writer, allocator: std.mem.Allocator, shell: [ return std.posix.execveZ(shell_z, &args, std.c.environ); } -fn redirectStandardStreams(log_writer: *std.Io.Writer, session_log: []const u8, create: bool) !std.fs.File { +fn redirectStandardStreams(global_log_file: *LogFile, session_log: []const u8, create: bool) !std.fs.File { const log_file = if (create) (std.fs.cwd().createFile(session_log, .{ .mode = 0o666 }) catch |err| { - try log_writer.print("failed to create new session log file: {s}\n", .{@errorName(err)}); + try global_log_file.file_writer.interface.print("failed to create new session log file: {s}\n", .{@errorName(err)}); return err; }) else (std.fs.cwd().openFile(session_log, .{ .mode = .read_write }) catch |err| { - try log_writer.print("failed to open existing session log file: {s}\n", .{@errorName(err)}); + try global_log_file.file_writer.interface.print("failed to open existing session log file: {s}\n", .{@errorName(err)}); return err; }); diff --git a/src/main.zig b/src/main.zig index decc6d3..afe51f2 100644 --- a/src/main.zig +++ b/src/main.zig @@ -25,6 +25,7 @@ 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 StringList = std.ArrayListUnmanaged([]const u8); const Ini = ini.Ini; @@ -261,13 +262,12 @@ pub fn main() !void { } } - var log_file: std.fs.File = undefined; - defer log_file.close(); + var log_file_buffer: [1024]u8 = undefined; - var log_buffer: [1024]u8 = undefined; - var log_file_writer: std.fs.File.Writer = undefined; - var could_open_log_file = try openLogFile(config.ly_log, &log_file, &log_buffer, &log_file_writer); - var log_writer = &log_file_writer.interface; + 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 @@ -352,7 +352,7 @@ pub fn main() !void { } } - if (!could_open_log_file) { + 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"); } @@ -917,7 +917,7 @@ pub fn main() !void { defer shared_err.deinit(); { - log_file.close(); + log_file.deinit(); session_pid = try std.posix.fork(); if (session_pid == 0) { @@ -942,18 +942,16 @@ pub fn main() !void { }; std.posix.sigaction(std.posix.SIG.CHLD, &tty_control_transfer_act, null); - could_open_log_file = try openLogFile(config.ly_log, &log_file, &log_buffer, &log_file_writer); - log_writer = &log_file_writer.interface; - defer log_file.close(); + try log_file.reinit(); - auth.authenticate(allocator, log_writer, auth_options, current_environment, login.getCurrentUsername(), password.text.items) catch |err| { + auth.authenticate(allocator, &log_file, auth_options, current_environment, login.getCurrentUsername(), password.text.items) catch |err| { shared_err.writeError(err); - try log_writer.flush(); + log_file.deinit(); std.process.exit(1); }; - try log_writer.flush(); + log_file.deinit(); std.process.exit(0); } @@ -963,8 +961,7 @@ pub fn main() !void { std.Thread.sleep(std.time.ns_per_s * 1); session_pid = -1; - could_open_log_file = try openLogFile(config.ly_log, &log_file, &log_buffer, &log_file_writer); - log_writer = &log_file_writer.interface; + try log_file.reinit(); } // Take back control of the TTY @@ -1053,33 +1050,6 @@ pub fn main() !void { } } -fn openLogFile(path: []const u8, log_file: *std.fs.File, buffer: []u8, writer: *std.fs.File.Writer) !bool { - var could_open_log_file = true; - open_log_file: { - log_file.* = std.fs.cwd().openFile(path, .{ .mode = .write_only }) catch std.fs.cwd().createFile(path, .{ .mode = 0o666 }) catch { - // If we could neither open an existing log file nor create a new - // one, abort. - could_open_log_file = false; - break :open_log_file; - }; - } - - if (!could_open_log_file) { - log_file.* = try std.fs.openFileAbsolute("/dev/null", .{ .mode = .write_only }); - } - - var log_file_writer = log_file.writer(buffer); - - // Seek to the end of the log file - if (could_open_log_file) { - const stat = try log_file.stat(); - try log_file_writer.seekTo(stat.size); - } - - writer.* = log_file_writer; - return could_open_log_file; -} - 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 1c05664c85260e31fe2ed8cbe51ed499f0590d11 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Sat, 18 Oct 2025 16:18:05 +0200 Subject: [PATCH 311/530] Fix xauth file name UB Signed-off-by: AnErrupTion --- readme.md | 2 +- src/auth.zig | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/readme.md b/readme.md index 5a46679..eab20fd 100644 --- a/readme.md +++ b/readme.md @@ -41,7 +41,7 @@ It is recommended to add a rule for Ly as it currently does not ship one. ### FreeBSD ``` -# pkg install ca_root_nss libxcb git +# pkg install ca_root_nss libxcb git xorg xauth ``` ## Packaging status diff --git a/src/auth.zig b/src/auth.zig index 394b432..cb3230c 100644 --- a/src/auth.zig +++ b/src/auth.zig @@ -322,7 +322,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) ![]const u8 { +fn createXauthFile(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"); @@ -364,8 +364,7 @@ fn createXauthFile(pwd: []const u8) ![]const u8 { while (xauth_dir[i] == '/') i -= 1; const trimmed_xauth_dir = xauth_dir[0 .. i + 1]; - var buf: [256]u8 = undefined; - const xauthority: []u8 = try std.fmt.bufPrint(&buf, "{s}/{s}", .{ trimmed_xauth_dir, xauth_file }); + const xauthority: []u8 = try std.fmt.bufPrint(buffer, "{s}/{s}", .{ trimmed_xauth_dir, xauth_file }); const file = try std.fs.createFileAbsolute(xauthority, .{}); file.close(); @@ -382,8 +381,8 @@ 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, options: AuthOptions) !void { - const xauthority = try createXauthFile(home); +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); try interop.setEnvironmentVariable(allocator, "XAUTHORITY", xauthority, true); try interop.setEnvironmentVariable(allocator, "DISPLAY", display_name, true); @@ -408,6 +407,7 @@ 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"); const display_num = try getFreeDisplay(); @@ -418,7 +418,7 @@ fn executeX11Cmd(log_file: *LogFile, allocator: std.mem.Allocator, shell: []cons defer allocator.free(shell_z); try log_writer.writeAll("[x11] creating xauth file\n"); - try xauth(log_file, allocator, display_name, shell_z, home, options); + try xauth(log_file, allocator, display_name, shell_z, home, &xauth_buffer, options); try log_writer.writeAll("[x11] starting x server\n"); const pid = try std.posix.fork(); From c4b68364efedda842c159081c12cb9bc64720302 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Sat, 18 Oct 2025 17:34:44 +0200 Subject: [PATCH 312/530] Add FreeBSD-specific PAM file Signed-off-by: AnErrupTion --- build.zig | 2 +- res/pam.d/ly-freebsd | 8 ++++++++ res/pam.d/{ly => ly-linux} | 0 3 files changed, 9 insertions(+), 1 deletion(-) create mode 100644 res/pam.d/ly-freebsd rename res/pam.d/{ly => ly-linux} (100%) diff --git a/build.zig b/build.zig index 3b9e833..bd1f432 100644 --- a/build.zig +++ b/build.zig @@ -243,7 +243,7 @@ fn install_ly(allocator: std.mem.Allocator, patch_map: PatchMap, install_config: var pam_dir = std.fs.cwd().openDir(pam_path, .{}) catch unreachable; defer pam_dir.close(); - try installFile("res/pam.d/ly", pam_dir, pam_path, "ly", .{ .override_mode = 0o644 }); + 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 }); } } diff --git a/res/pam.d/ly-freebsd b/res/pam.d/ly-freebsd new file mode 100644 index 0000000..1cefe07 --- /dev/null +++ b/res/pam.d/ly-freebsd @@ -0,0 +1,8 @@ +#%PAM-1.0 + +# OpenPAM (used in FreeBSD) doesn't support prepending "-" for ignoring missing +# modules. +auth include login +account include login +password include login +session include login diff --git a/res/pam.d/ly b/res/pam.d/ly-linux similarity index 100% rename from res/pam.d/ly rename to res/pam.d/ly-linux From 4bc405f2397006e643c5be128443518ae3b8e96a Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Sat, 18 Oct 2025 18:12:16 +0200 Subject: [PATCH 313/530] Start Ly v1.3.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 bd1f432..cb54f55 100644 --- a/build.zig +++ b/build.zig @@ -23,7 +23,7 @@ comptime { } } -const ly_version = std.SemanticVersion{ .major = 1, .minor = 2, .patch = 0 }; +const ly_version = std.SemanticVersion{ .major = 1, .minor = 3, .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 735ddc2..bd8e8a7 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -1,6 +1,6 @@ .{ .name = .ly, - .version = "1.2.0", + .version = "1.3.0", .fingerprint = 0xa148ffcc5dc2cb59, .minimum_zig_version = "0.15.0", .dependencies = .{ From 5f22173b91f880c127caf96448e92839adabb295 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Sat, 18 Oct 2025 19:10:44 +0200 Subject: [PATCH 314/530] Mention only Ly v1.2.0 and above are supported in issue template Signed-off-by: AnErrupTion --- .github/ISSUE_TEMPLATE/bug.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/bug.yml b/.github/ISSUE_TEMPLATE/bug.yml index 2b8bb33..a0c37ce 100644 --- a/.github/ISSUE_TEMPLATE/bug.yml +++ b/.github/ISSUE_TEMPLATE/bug.yml @@ -15,7 +15,7 @@ body: id: version attributes: label: Ly version - description: The output of `ly --version`. Please note that only Ly v1.1.0 and above are supported. + description: The output of `ly --version`. Please note that only Ly v1.2.0 and above are supported. placeholder: 1.1.0-dev.12+2b0301c validations: required: true From ec16ad5dfcf0c173032a6edf2338c15593a65ad3 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Tue, 21 Oct 2025 17:36:42 +0200 Subject: [PATCH 315/530] Remove mention of config.tty option in README (fixes #854) Signed-off-by: AnErrupTion --- readme.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/readme.md b/readme.md index eab20fd..05b74e3 100644 --- a/readme.md +++ b/readme.md @@ -117,9 +117,8 @@ execute the following command: # systemctl disable getty@tty2.service ``` -You can change the TTY Ly will run on by editing the `tty` option in the -configuration file **and** change which TTY is used in the corresponding -service file.. +You can change the TTY Ly will run on by editing the corresponding +service file for your platform. ### OpenRC From 106f157a2cf27ce6d25b5a3596543d25f43975bf Mon Sep 17 00:00:00 2001 From: ebits Date: Thu, 23 Oct 2025 19:07:54 +0200 Subject: [PATCH 316/530] [Feature] Add edge margin option (#856) (closes #848) Allows setting a balanced margin on all sides. Reviewed-on: https://codeberg.org/fairyglade/ly/pulls/856 Reviewed-by: AnErrupTion Co-authored-by: ebits Co-committed-by: ebits --- res/config.ini | 3 +++ src/config/Config.zig | 1 + src/main.zig | 41 ++++++++++++++++++++++------------------- 3 files changed, 26 insertions(+), 19 deletions(-) diff --git a/res/config.ini b/res/config.ini index e67c609..9b67388 100644 --- a/res/config.ini +++ b/res/config.ini @@ -138,6 +138,9 @@ doom_middle_color = 0x00C78F17 # DOOM animation custom bottom color (high intensity flames) doom_bottom_color = 0x00FFFFFF +# Set margin to the edges of the DM (useful for curved monitors) +edge_margin = 0 + # Error background color id error_bg = 0x00000000 diff --git a/src/config/Config.zig b/src/config/Config.zig index 53cc3c8..8f95627 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -39,6 +39,7 @@ doom_fire_spread: u8 = 2, doom_top_color: u32 = 0x00FF0000, doom_middle_color: u32 = 0x00FFFF00, doom_bottom_color: u32 = 0x00FFFFFF, +edge_margin: u8 = 0, error_bg: u32 = 0x00000000, error_fg: u32 = 0x01FF0000, fg: u32 = 0x00FFFFFF, diff --git a/src/main.zig b/src/main.zig index afe51f2..49b66d0 100644 --- a/src/main.zig +++ b/src/main.zig @@ -577,12 +577,12 @@ pub fn main() !void { _ = termbox.tb_clear(); - var length: usize = 0; + var length: usize = config.edge_margin; if (!animation_timed_out) animation.draw(); if (!config.hide_version_string) { - buffer.drawLabel(ly_version_str, 0, buffer.height - 1); + buffer.drawLabel(ly_version_str, config.edge_margin, buffer.height - 1 - config.edge_margin); } if (config.battery_id) |id| draw_battery: { @@ -598,8 +598,11 @@ pub fn main() !void { var battery_buf: [16:0]u8 = undefined; const battery_str = std.fmt.bufPrintZ(&battery_buf, "BAT: {d}%", .{battery_percentage}) catch break :draw_battery; - const battery_y: usize = if (config.hide_key_hints) 0 else 1; - buffer.drawLabel(battery_str, 0, battery_y); + 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; } @@ -672,44 +675,44 @@ pub fn main() !void { info_line.label.draw(); if (!config.hide_key_hints) { - buffer.drawLabel(config.shutdown_key, length, 0); + buffer.drawLabel(config.shutdown_key, length, config.edge_margin); length += config.shutdown_key.len + 1; - buffer.drawLabel(" ", length - 1, 0); + buffer.drawLabel(" ", length - 1, config.edge_margin); - buffer.drawLabel(lang.shutdown, length, 0); + buffer.drawLabel(lang.shutdown, length, config.edge_margin); length += shutdown_len + 1; - buffer.drawLabel(config.restart_key, length, 0); + buffer.drawLabel(config.restart_key, length, config.edge_margin); length += config.restart_key.len + 1; - buffer.drawLabel(" ", length - 1, 0); + buffer.drawLabel(" ", length - 1, config.edge_margin); - buffer.drawLabel(lang.restart, length, 0); + buffer.drawLabel(lang.restart, length, config.edge_margin); length += restart_len + 1; if (config.sleep_cmd != null) { - buffer.drawLabel(config.sleep_key, length, 0); + buffer.drawLabel(config.sleep_key, length, config.edge_margin); length += config.sleep_key.len + 1; - buffer.drawLabel(" ", length - 1, 0); + buffer.drawLabel(" ", length - 1, config.edge_margin); - buffer.drawLabel(lang.sleep, length, 0); + buffer.drawLabel(lang.sleep, length, config.edge_margin); length += sleep_len + 1; } if (config.brightness_down_key) |key| { - buffer.drawLabel(key, length, 0); + buffer.drawLabel(key, length, config.edge_margin); length += key.len + 1; - buffer.drawLabel(" ", length - 1, 0); + buffer.drawLabel(" ", length - 1, config.edge_margin); - buffer.drawLabel(lang.brightness_down, length, 0); + buffer.drawLabel(lang.brightness_down, length, config.edge_margin); length += brightness_down_len + 1; } if (config.brightness_up_key) |key| { - buffer.drawLabel(key, length, 0); + buffer.drawLabel(key, length, config.edge_margin); length += key.len + 1; - buffer.drawLabel(" ", length - 1, 0); + buffer.drawLabel(" ", length - 1, config.edge_margin); - buffer.drawLabel(lang.brightness_up, length, 0); + buffer.drawLabel(lang.brightness_up, length, config.edge_margin); length += brightness_up_len + 1; } } From 80c27224e9d65689fb81215a22fdb934b361d7e0 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Thu, 23 Oct 2025 22:15:16 +0200 Subject: [PATCH 317/530] Create xauth directory if it doesn't exist Signed-off-by: AnErrupTion --- src/auth.zig | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/auth.zig b/src/auth.zig index cb3230c..8bd7621 100644 --- a/src/auth.zig +++ b/src/auth.zig @@ -342,7 +342,7 @@ fn createXauthFile(pwd: []const u8, buffer: []u8) ![]const u8 { dir.close(); // xauth_dir is a directory, use it to store Xauthority - xauth_dir = try std.fmt.bufPrint(&xauth_buf, "{s}/ly", .{xauth_dir}); + 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.?}); } @@ -365,6 +365,9 @@ fn createXauthFile(pwd: []const u8, buffer: []u8) ![]const u8 { const trimmed_xauth_dir = xauth_dir[0 .. i + 1]; const xauthority: []u8 = try std.fmt.bufPrint(buffer, "{s}/{s}", .{ trimmed_xauth_dir, xauth_file }); + + std.fs.makeDirAbsolute(trimmed_xauth_dir) catch {}; + const file = try std.fs.createFileAbsolute(xauthority, .{}); file.close(); From 4171e2999505a42f70f2dc07e8b2573d5c3f84eb Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Thu, 23 Oct 2025 22:15:43 +0200 Subject: [PATCH 318/530] Fix build error when runit service symlink already exists Signed-off-by: AnErrupTion --- build.zig | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/build.zig b/build.zig index cb54f55..b8e1b8d 100644 --- a/build.zig +++ b/build.zig @@ -283,7 +283,13 @@ fn install_service(allocator: std.mem.Allocator, patch_map: PatchMap) !void { const patched_run = try patchFile(allocator, "res/ly-runit-service/run", patch_map); try installText(patched_run, service_dir, service_path, "run", .{ .mode = 0o755 }); - try std.fs.cwd().symLink("/run/runit/supervise.ly", supervise_path, .{}); + std.fs.cwd().symLink("/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 => { From 95d1d9378cc0b93d58dc90eb8e3a69d510e5d9c9 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Thu, 23 Oct 2025 22:53:58 +0200 Subject: [PATCH 319/530] Add edge margin to top-right clock 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 49b66d0..504fec1 100644 --- a/src/main.zig +++ b/src/main.zig @@ -663,7 +663,7 @@ pub fn main() !void { break :draw_clock; } - buffer.drawLabel(clock_str, buffer.width - @min(buffer.width, clock_str.len), 0); + 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; From 38173d855705101bbb0edec374252b0d54e69dc5 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Sat, 25 Oct 2025 20:39:34 +0200 Subject: [PATCH 320/530] Add edge margin to numlock & capslock Signed-off-by: AnErrupTion --- src/main.zig | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/main.zig b/src/main.zig index 504fec1..80c1ab2 100644 --- a/src/main.zig +++ b/src/main.zig @@ -734,8 +734,10 @@ pub fn main() !void { break :draw_lock_state; }; - var lock_state_x = buffer.width - @min(buffer.width, lang.numlock.len); - const lock_state_y: usize = if (config.clock != null) 1 else 0; + 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); From 4a486bd876aa9e53372c81115f92b51969c1ffc6 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Sat, 25 Oct 2025 20:49:56 +0200 Subject: [PATCH 321/530] Update screenshot Signed-off-by: AnErrupTion --- .github/screenshot.png | Bin 39461 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/.github/screenshot.png b/.github/screenshot.png index 1cd5ff4555c55f58d3df7662946aa505a316ab13..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 100644 GIT binary patch literal 0 HcmV?d00001 literal 39461 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 Date: Sat, 25 Oct 2025 20:51:51 +0200 Subject: [PATCH 322/530] Update screenshot (for real this time) Signed-off-by: AnErrupTion --- .github/screenshot.png | Bin 0 -> 34754 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/.github/screenshot.png b/.github/screenshot.png index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..748feccf62e1f157b825977c0d5a56a90eacec6b 100644 GIT binary patch literal 34754 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 literal 0 HcmV?d00001 From 0cf752f3b850d16283e28853bca63e994d8c5e7b Mon Sep 17 00:00:00 2001 From: ferreo Date: Sat, 25 Oct 2025 23:09:24 +0200 Subject: [PATCH 323/530] [Feature] Add autologin support (#841) (closes #200) This is the simplest change I could come up with to add working autologin, only bit I really dislike is the event but it seemed like the cleanest way without refactoring. Co-authored-by: ferreo Reviewed-on: https://codeberg.org/fairyglade/ly/pulls/841 Reviewed-by: AnErrupTion Co-authored-by: ferreo Co-committed-by: ferreo --- build.zig | 1 + res/config.ini | 24 +++++++++ res/lang/ar.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/lv.ini | 2 +- 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 | 1 + res/lang/tr.ini | 1 + res/lang/uk.ini | 1 + res/lang/zh_CN.ini | 1 + res/pam.d/ly-freebsd-autologin | 9 ++++ res/pam.d/ly-linux-autologin | 16 ++++++ src/Environment.zig | 1 + src/config/Config.zig | 3 ++ src/config/Lang.zig | 1 + src/main.zig | 89 +++++++++++++++++++++++++++++++--- src/tui/components/Session.zig | 3 ++ 29 files changed, 160 insertions(+), 8 deletions(-) create mode 100644 res/pam.d/ly-freebsd-autologin create mode 100644 res/pam.d/ly-linux-autologin diff --git a/build.zig b/build.zig index b8e1b8d..bf23051 100644 --- a/build.zig +++ b/build.zig @@ -244,6 +244,7 @@ fn install_ly(allocator: std.mem.Allocator, patch_map: PatchMap, install_config: defer pam_dir.close(); 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 }); } } diff --git a/res/config.ini b/res/config.ini index 9b67388..c431280 100644 --- a/res/config.ini +++ b/res/config.ini @@ -46,6 +46,30 @@ auth_fails = 10 # If set to null, battery status won't be shown 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. +# Autologin only happens once at startup - it won't re-trigger after logout. + +# PAM service name to use for automatic login +# The default service (ly-autologin) uses pam_permit to allow login without password +# The appropriate platform-specific PAM configuration (ly-autologin) will be used automatically +auto_login_service = ly-autologin + +# Session name to launch automatically +# 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 +# Examples: "i3", "sway", "gnome", "plasma", "xfce" +# If null, automatic login is disabled +auto_login_session = null + +# Username to automatically log in +# Must be a valid user on the system +# If null, automatic login is disabled +auto_login_user = null + # Background color id bg = 0x00000000 diff --git a/res/lang/ar.ini b/res/lang/ar.ini index 78d9e93..9b371f8 100644 --- a/res/lang/ar.ini +++ b/res/lang/ar.ini @@ -4,6 +4,7 @@ brightness_up = رفع السطوع capslock = capslock err_alloc = فشل في تخصيص الذاكرة + err_bounds = out-of-bounds index err_brightness_change = فشل في تغيير سطوع الشاشة err_chdir = فشل في فتح مجلد المنزل diff --git a/res/lang/cat.ini b/res/lang/cat.ini index fb829a6..284b14a 100644 --- a/res/lang/cat.ini +++ b/res/lang/cat.ini @@ -4,6 +4,7 @@ brightness_up = apujar brillantor capslock = Bloq Majús err_alloc = assignació de memòria fallida + err_bounds = índex fora de límits err_brightness_change = error en canviar la brillantor err_chdir = error en obrir la carpeta home diff --git a/res/lang/cs.ini b/res/lang/cs.ini index c1206fe..cac7884 100644 --- a/res/lang/cs.ini +++ b/res/lang/cs.ini @@ -4,6 +4,7 @@ capslock = capslock err_alloc = alokace paměti selhala + err_bounds = index je mimo hranice pole err_chdir = nelze otevřít domovský adresář diff --git a/res/lang/de.ini b/res/lang/de.ini index 5512d5e..12cce5f 100644 --- a/res/lang/de.ini +++ b/res/lang/de.ini @@ -4,6 +4,7 @@ brightness_up = Helligkeit+ capslock = Feststelltaste err_alloc = Speicherzuweisung fehlgeschlagen + err_bounds = Index ausserhalb des Bereichs err_brightness_change = Helligkeitsänderung fehlgeschlagen err_chdir = Fehler beim Oeffnen des Home-Ordners diff --git a/res/lang/en.ini b/res/lang/en.ini index 1a8c16c..7c95a09 100644 --- a/res/lang/en.ini +++ b/res/lang/en.ini @@ -4,6 +4,7 @@ brightness_up = increase brightness capslock = capslock custom = custom err_alloc = failed memory allocation +err_autologin_session = autologin session not found err_bounds = out-of-bounds index err_brightness_change = failed to change brightness err_chdir = failed to open home folder diff --git a/res/lang/es.ini b/res/lang/es.ini index 776c998..0878bf2 100644 --- a/res/lang/es.ini +++ b/res/lang/es.ini @@ -4,6 +4,7 @@ brightness_up = subir brillo capslock = Bloq Mayús err_alloc = asignación de memoria fallida + err_bounds = índice fuera de límites err_chdir = error al abrir la carpeta home diff --git a/res/lang/fr.ini b/res/lang/fr.ini index 31c0971..7774a16 100644 --- a/res/lang/fr.ini +++ b/res/lang/fr.ini @@ -4,6 +4,7 @@ brightness_up = augmenter la luminosité capslock = verr.maj custom = customisé err_alloc = échec d'allocation mémoire + err_bounds = indice hors-limite err_brightness_change = échec du changement de luminosité err_chdir = échec de l'ouverture du répertoire home diff --git a/res/lang/it.ini b/res/lang/it.ini index 4ba807c..84d0d39 100644 --- a/res/lang/it.ini +++ b/res/lang/it.ini @@ -4,6 +4,7 @@ capslock = capslock err_alloc = impossibile allocare memoria + err_bounds = indice fuori limite err_chdir = impossibile aprire home directory diff --git a/res/lang/ja_JP.ini b/res/lang/ja_JP.ini index 200422a..0f4c34a 100644 --- a/res/lang/ja_JP.ini +++ b/res/lang/ja_JP.ini @@ -4,6 +4,7 @@ brightness_up = 明るさを上げる capslock = CapsLock err_alloc = メモリ割り当て失敗 + err_bounds = 境界外インデックス err_brightness_change = 明るさの変更に失敗しました err_chdir = ホームフォルダを開けませんでした diff --git a/res/lang/lv.ini b/res/lang/lv.ini index dd15b05..024b130 100644 --- a/res/lang/lv.ini +++ b/res/lang/lv.ini @@ -4,6 +4,7 @@ brightness_up = palielināt spilgtumu capslock = caps lock custom = pielāgots err_alloc = neizdevās atmiņas piešķiršana + err_bounds = indekss ārpus robežām err_brightness_change = neizdevās mainīt spilgtumu err_chdir = neizdevās atvērt mājas mapi @@ -68,4 +69,3 @@ sleep = snauda wayland = wayland x11 = x11 xinitrc = xinitrc - diff --git a/res/lang/pl.ini b/res/lang/pl.ini index 065fb22..b159a1d 100644 --- a/res/lang/pl.ini +++ b/res/lang/pl.ini @@ -4,6 +4,7 @@ brightness_up = zwiększ jasność capslock = capslock err_alloc = nieudana alokacja pamięci + err_bounds = indeks poza zakresem err_brightness_change = nie udało się zmienić jasności err_chdir = nie udało się otworzyć folderu domowego diff --git a/res/lang/pt.ini b/res/lang/pt.ini index a1951fd..d47112c 100644 --- a/res/lang/pt.ini +++ b/res/lang/pt.ini @@ -4,6 +4,7 @@ capslock = capslock err_alloc = erro na atribuição de memória + err_bounds = índice fora de limites err_chdir = erro ao abrir a pasta home diff --git a/res/lang/pt_BR.ini b/res/lang/pt_BR.ini index f886816..94312fd 100644 --- a/res/lang/pt_BR.ini +++ b/res/lang/pt_BR.ini @@ -4,6 +4,7 @@ capslock = caixa alta err_alloc = alocação de memória malsucedida + err_bounds = índice fora de limites err_chdir = não foi possível abrir o diretório home diff --git a/res/lang/ro.ini b/res/lang/ro.ini index 8503245..78ffcc0 100644 --- a/res/lang/ro.ini +++ b/res/lang/ro.ini @@ -21,6 +21,7 @@ capslock = capslock + err_pam_abort = tranzacţie pam anulată err_pam_acct_expired = cont expirat err_pam_auth = eroare de autentificare diff --git a/res/lang/ru.ini b/res/lang/ru.ini index dab497c..3715250 100644 --- a/res/lang/ru.ini +++ b/res/lang/ru.ini @@ -4,6 +4,7 @@ brightness_up = увеличить яркость capslock = capslock err_alloc = не удалось выделить память + err_bounds = за пределами индекса err_brightness_change = не удалось изменить яркость err_chdir = не удалось открыть домашнюю папку diff --git a/res/lang/sr.ini b/res/lang/sr.ini index 1e3c6f7..6f8be10 100644 --- a/res/lang/sr.ini +++ b/res/lang/sr.ini @@ -4,6 +4,7 @@ capslock = capslock err_alloc = neuspijesna alokacija memorije + err_bounds = izvan granica indeksa err_chdir = neuspijesno otvaranje home foldera diff --git a/res/lang/sv.ini b/res/lang/sv.ini index 3fc2bfd..030a66a 100644 --- a/res/lang/sv.ini +++ b/res/lang/sv.ini @@ -4,6 +4,7 @@ capslock = capslock err_alloc = misslyckad minnesallokering + err_bounds = utanför banan index err_chdir = misslyckades att öppna hemkatalog diff --git a/res/lang/tr.ini b/res/lang/tr.ini index 78de8c3..0722574 100644 --- a/res/lang/tr.ini +++ b/res/lang/tr.ini @@ -4,6 +4,7 @@ capslock = capslock err_alloc = basarisiz bellek ayirma + err_bounds = sinirlarin disinda dizin err_chdir = ev klasoru acilamadi diff --git a/res/lang/uk.ini b/res/lang/uk.ini index de41076..bbc6993 100644 --- a/res/lang/uk.ini +++ b/res/lang/uk.ini @@ -4,6 +4,7 @@ capslock = capslock err_alloc = невдале виділення пам'яті + err_bounds = поза межами індексу err_chdir = не вдалося відкрити домашній каталог diff --git a/res/lang/zh_CN.ini b/res/lang/zh_CN.ini index 5e77f8e..0c4e79e 100644 --- a/res/lang/zh_CN.ini +++ b/res/lang/zh_CN.ini @@ -4,6 +4,7 @@ capslock = 大写锁定 err_alloc = 内存分配失败 + err_bounds = 索引越界 err_chdir = 无法打开home文件夹 diff --git a/res/pam.d/ly-freebsd-autologin b/res/pam.d/ly-freebsd-autologin new file mode 100644 index 0000000..e2448ad --- /dev/null +++ b/res/pam.d/ly-freebsd-autologin @@ -0,0 +1,9 @@ +#%PAM-1.0 + +# 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 diff --git a/res/pam.d/ly-linux-autologin b/res/pam.d/ly-linux-autologin new file mode 100644 index 0000000..31a51a1 --- /dev/null +++ b/res/pam.d/ly-linux-autologin @@ -0,0 +1,16 @@ +#%PAM-1.0 + +auth required pam_permit.so +-auth optional pam_gnome_keyring.so +-auth optional pam_kwallet5.so + +account include login + +password include login +-password optional pam_gnome_keyring.so use_authtok + +-session optional pam_systemd.so class=greeter +-session optional pam_elogind.so +session include login +-session optional pam_gnome_keyring.so auto_start +-session optional pam_kwallet5.so auto_start diff --git a/src/Environment.zig b/src/Environment.zig index 849cb08..eab875d 100644 --- a/src/Environment.zig +++ b/src/Environment.zig @@ -16,6 +16,7 @@ pub const Entry = struct { @"Desktop Entry": DesktopEntry = .{} }; entry_ini: ?Ini(Entry) = null, 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/config/Config.zig b/src/config/Config.zig index 8f95627..371f256 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -12,6 +12,9 @@ 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, bg: u32 = 0x00000000, bigclock: Bigclock = .none, bigclock_12hr: bool = false, diff --git a/src/config/Lang.zig b/src/config/Lang.zig index 886644d..359feec 100644 --- a/src/config/Lang.zig +++ b/src/config/Lang.zig @@ -9,6 +9,7 @@ brightness_up: []const u8 = "increase brightness", capslock: []const u8 = "capslock", custom: []const u8 = "custom", err_alloc: []const u8 = "failed memory allocation", +err_autologin_session: []const u8 = "autologin session not found", err_bounds: []const u8 = "out-of-bounds index", err_brightness_change: []const u8 = "failed to change brightness", err_chdir: []const u8 = "failed to open home folder", diff --git a/src/main.zig b/src/main.zig index 80c1ab2..8f6d434 100644 --- a/src/main.zig +++ b/src/main.zig @@ -432,9 +432,38 @@ pub fn main() !void { var active_input = config.default_input; var insert_mode = !config.vi_mode or config.vi_default_mode == .insert; + var 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; + + 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}); + 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 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 }); + + session.label.current = session_index; + for (login.label.list.items, 0..) |username, i| { + if (std.mem.eql(u8, username.name, auto_user)) { + login.label.current = i; + break; + } + } + is_autologin = true; + } // Load last saved username and desktop selection, if any - if (config.save) { + // 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 the saved index isn't valid, bail out if (index >= saved_users.user_list.items.len) break :load_last_user; @@ -777,11 +806,25 @@ pub fn main() !void { timeout = @intCast(1000 - @divTrunc(time.microseconds, 1000) + 1); } - const event_error = if (timeout == -1) termbox.tb_poll_event(&event) else termbox.tb_peek_event(&event, timeout); + // Skip event polling if autologin is set, use simulated Enter key press instead + if (is_autologin) { + event = termbox.tb_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); - update = timeout != -1; + update = timeout != -1; - if (event_error < 0 or event.type != termbox.TB_EVENT_KEY) continue; + if (event_error < 0 or event.type != termbox.TB_EVENT_KEY) continue; + } switch (event.key) { termbox.TB_KEY_ESC => { @@ -927,9 +970,14 @@ pub fn main() !void { 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 = config.service_name, + .service_name = service_name, .path = config.path, .session_log = config.session_log, .xauth_cmd = config.xauth_cmd, @@ -949,7 +997,7 @@ pub fn main() !void { try log_file.reinit(); - auth.authenticate(allocator, &log_file, auth_options, current_environment, login.getCurrentUsername(), password.text.items) catch |err| { + auth.authenticate(allocator, &log_file, auth_options, current_environment, login.getCurrentUsername(), password_text) catch |err| { shared_err.writeError(err); log_file.deinit(); @@ -992,6 +1040,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"); } @@ -1110,6 +1159,7 @@ fn crawl(session: *Session, lang: Lang, path: []const u8, display_server: Displa 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 @@ -1123,13 +1173,18 @@ 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 - maybe_xdg_session_desktop = std.fs.path.stem(item.name); + const stem = std.fs.path.stem(item.name); + if (stem.len > 0) { + maybe_xdg_session_desktop = try session.label.allocator.dupe(u8, stem); + xdg_session_desktop_owned = true; + } } try session.addEnvironment(.{ .entry_ini = entry_ini, .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) { @@ -1144,6 +1199,26 @@ fn crawl(session: *Session, lang: Lang, path: []const u8, display_server: Displa } } +fn isValidUsername(username: []const u8, usernames: StringList) bool { + for (usernames.items) |valid_username| { + if (std.mem.eql(u8, username, valid_username)) return true; + } + return false; +} + +fn findSessionByName(session: *Session, name: []const u8) ?usize { + for (session.label.list.items, 0..) |env, 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; + } + return null; +} + fn getAllUsernames(allocator: std.mem.Allocator, login_defs_path: []const u8) !StringList { const uid_range = try interop.getUserIdRange(allocator, login_defs_path); diff --git a/src/tui/components/Session.zig b/src/tui/components/Session.zig index 53a6c90..4417e06 100644 --- a/src/tui/components/Session.zig +++ b/src/tui/components/Session.zig @@ -27,6 +27,9 @@ 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.deinit(); From 2da364817916abca956626a2d4adba44627210b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Gina=C5=82?= Date: Mon, 3 Nov 2025 19:45:59 +0100 Subject: [PATCH 324/530] Add missing Polish translations (#861) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reviewed-on: https://codeberg.org/fairyglade/ly/pulls/861 Reviewed-by: AnErrupTion Co-authored-by: Piotr Ginał Co-committed-by: Piotr Ginał --- res/lang/pl.ini | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/res/lang/pl.ini b/res/lang/pl.ini index b159a1d..f48d38f 100644 --- a/res/lang/pl.ini +++ b/res/lang/pl.ini @@ -2,22 +2,22 @@ authenticating = uwierzytelnianie... 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 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_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_hostname = nie udało się uzyskać nazwy hosta - - +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ł err_null = pusty wskaźnik err_numlock = nie udało się ustawić numlock @@ -43,10 +43,10 @@ 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_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_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 From 1537addd6787720f5afc0e793a5b21102c022756 Mon Sep 17 00:00:00 2001 From: Corey Newton Date: Wed, 5 Nov 2025 21:57:10 +0100 Subject: [PATCH 325/530] Change default session log directory to ~/.local/state (#859) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This addresses #808 and #823 conclusively for all parties. - It requires no additional directory creation. - It does not impact users who do not have a problem. - It adds additional documentation detail in both the bug template and the readme to help users who *are* having a problem. - Fulfills the spirit of the [XDG Spec(https://specifications.freedesktop.org/basedir-spec/latest) but not in practice as `$XDG_CONFIG_HOME` and `$HOME` and the associated logic are not implemented. `~/.local/state` is the fallback location. In particular, the spec indicates: > It may contain: actions history (**logs**, history, …) Reviewed-on: https://codeberg.org/fairyglade/ly/pulls/859 Reviewed-by: AnErrupTion Co-authored-by: Corey Newton Co-committed-by: Corey Newton --- .github/ISSUE_TEMPLATE/bug.yml | 5 +++-- readme.md | 4 ++++ res/config.ini | 2 +- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug.yml b/.github/ISSUE_TEMPLATE/bug.yml index a0c37ce..101a459 100644 --- a/.github/ISSUE_TEMPLATE/bug.yml +++ b/.github/ISSUE_TEMPLATE/bug.yml @@ -58,8 +58,9 @@ body: attributes: label: Relevant logs description: | - Please copy and paste (or attach) any relevant logs, error messages or any other output. This will be automatically formatted into code, so no need for backticks. Screenshots are accepted if they make life easier for you. - Moreover, it is almost always a good idea to include your session log and your general log files (found at ~/ly-session.log and /var/log/ly.log respectively by default) as it usually contains relevant information about the problem. + Please copy and paste (or attach) any relevant logs, error messages or any other output. This will be automatically formatted into code, so no need for backticks. Screenshots are accepted if they make life easier for you. The log files (located as specified by `/etc/ly/config.ini`) usually contain relevant information about the problem: + - 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. render: shell - type: textarea id: moreinfo diff --git a/readme.md b/readme.md index 05b74e3..491efc0 100644 --- a/readme.md +++ b/readme.md @@ -57,6 +57,10 @@ managers, all of which you can find in the sections below: [X11 environments](#supported-x11-environments) +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 The procedure for manually building Ly is pretty standard: diff --git a/res/config.ini b/res/config.ini index c431280..96006a4 100644 --- a/res/config.ini +++ b/res/config.ini @@ -285,7 +285,7 @@ service_name = ly # Important: due to technical limitations, X11 and shell sessions aren't supported, which # means you won't get any logs from those sessions. # If null, no session log will be created -session_log = ly-session.log +session_log = .local/state/ly-session.log # Setup command setup_cmd = $CONFIG_DIRECTORY/ly/setup.sh From 68ec85f412846d3e2ae778380f77d5a140ebeb6c Mon Sep 17 00:00:00 2001 From: RomanPro100 Date: Sun, 9 Nov 2025 17:35:26 +0100 Subject: [PATCH 326/530] Add missing Russian translations (#863) Reviewed-on: https://codeberg.org/fairyglade/ly/pulls/863 Reviewed-by: AnErrupTion Co-authored-by: RomanPro100 Co-committed-by: RomanPro100 --- res/lang/ru.ini | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/res/lang/ru.ini b/res/lang/ru.ini index 3715250..63d0824 100644 --- a/res/lang/ru.ini +++ b/res/lang/ru.ini @@ -2,25 +2,25 @@ authenticating = аутентификация... brightness_down = уменьшить яркость brightness_up = увеличить яркость capslock = capslock - +custom = пользовательский err_alloc = не удалось выделить память - +err_autologin_session = не найдена сессия с автологином err_bounds = за пределами индекса err_brightness_change = не удалось изменить яркость err_chdir = не удалось открыть домашнюю папку - +err_clock_too_long = строка часов слишком длинная err_config = не удалось разобрать файл конфигурации err_dgn_oob = отладочное сообщение (log) err_domain = неверный домен err_empty_password = пустой пароль не допустим err_envlist = не удалось получить список переменных среды - +err_get_active_tty = не удалось получить активный tty err_hostname = не удалось получить имя хоста - - +err_lock_state = не удалось получить состояние lock +err_log = не удалось открыть файл log err_mlock = сбой блокировки памяти err_null = нулевой указатель - +err_numlock = не удалось установить numlock err_pam = pam транзакция не удалась err_pam_abort = pam транзакция прервана err_pam_acct_expired = срок действия аккаунта истёк @@ -43,7 +43,7 @@ err_perm_group = не удалось понизить права доступа err_perm_user = не удалось понизить права доступа пользователя err_pwnam = не удалось получить информацию о пользователе err_sleep = не удалось выполнить команду sleep - +err_battery = не удалось получить статус батареи err_switch_tty = не удалось переключить tty err_tty_ctrl = передача управления tty не удалась err_no_users = пользователи не найдены @@ -54,11 +54,11 @@ err_xauth = команда xauth не выполнена err_xcb_conn = ошибка подключения xcb err_xsessions_dir = не удалось найти сессионную папку err_xsessions_open = не удалось открыть сессионную папку - +insert = вставка login = логин logout = вышел из системы no_x11_support = поддержка x11 отключена во время компиляции - +normal = обычный numlock = numlock other = прочие password = пароль From cc07c4870a1c4d6b293544bef2dbfa082906c253 Mon Sep 17 00:00:00 2001 From: notiant Date: Sat, 15 Nov 2025 16:32:42 +0100 Subject: [PATCH 327/530] Add option to hide CapsLock and NumLock states (#864) Reviewed-on: https://codeberg.org/fairyglade/ly/pulls/864 Reviewed-by: AnErrupTion Co-authored-by: notiant Co-committed-by: notiant --- res/config.ini | 3 +++ src/config/Config.zig | 1 + src/main.zig | 2 +- 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/res/config.ini b/res/config.ini index 96006a4..f090397 100644 --- a/res/config.ini +++ b/res/config.ini @@ -218,6 +218,9 @@ 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 diff --git a/src/config/Config.zig b/src/config/Config.zig index 371f256..fc1a07f 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -53,6 +53,7 @@ gameoflife_frame_delay: usize = 6, gameoflife_initial_density: f32 = 0.4, hide_borders: bool = false, hide_key_hints: bool = false, +hide_keyboard_locks: bool = false, hide_version_string: bool = false, initial_info_text: ?[]const u8 = null, input_len: u8 = 34, diff --git a/src/main.zig b/src/main.zig index 8f6d434..a4108e4 100644 --- a/src/main.zig +++ b/src/main.zig @@ -755,7 +755,7 @@ pub fn main() !void { buffer.drawLabel(label_txt, buffer.box_x, buffer.box_y + buffer.box_height); } - if (can_get_lock_state) draw_lock_state: { + if (!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; From 3c0c84d067342af700f1cc6e017cdaf68a57b452 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Sat, 15 Nov 2025 20:02:59 +0100 Subject: [PATCH 328/530] Fix build error I really should test PRs before merging them, shouldn't I? --- src/main.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main.zig b/src/main.zig index a4108e4..cc69921 100644 --- a/src/main.zig +++ b/src/main.zig @@ -755,7 +755,7 @@ pub fn main() !void { buffer.drawLabel(label_txt, buffer.box_x, buffer.box_y + buffer.box_height); } - if (!hide_keyboard_locks and can_get_lock_state) draw_lock_state: { + 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; From f0758d812ea4f1674968657a717d49ffc7a63e89 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Wed, 19 Nov 2025 22:15:07 +0100 Subject: [PATCH 329/530] Fix potential out of bounds issue when automatically changing session Signed-off-by: AnErrupTion --- src/tui/components/UserList.zig | 1 + 1 file changed, 1 insertion(+) diff --git a/src/tui/components/UserList.zig b/src/tui/components/UserList.zig index 6939298..83819bb 100644 --- a/src/tui/components/UserList.zig +++ b/src/tui/components/UserList.zig @@ -67,6 +67,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.*; } } From 816be7449f9b3cce63d424cb89e8e8b9feccfe23 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Wed, 19 Nov 2025 22:19:12 +0100 Subject: [PATCH 330/530] Update Repology URL Signed-off-by: AnErrupTion --- readme.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/readme.md b/readme.md index 491efc0..8e9b5b2 100644 --- a/readme.md +++ b/readme.md @@ -44,9 +44,9 @@ It is recommended to add a rule for Ly as it currently does not ship one. # pkg install ca_root_nss libxcb git xorg xauth ``` -## Packaging status +## Availability -[![Packaging status](https://repology.org/badge/vertical-allrepos/ly.svg?exclude_unsupported=1)](https://repology.org/project/ly/versions) +[![Packaging status](https://repology.org/badge/vertical-allrepos/ly-display-manager.svg?exclude_unsupported=1)](https://repology.org/project/ly-display-manager/versions) ## Support @@ -58,8 +58,9 @@ managers, all of which you can find in the sections below: [X11 environments](#supported-x11-environments) 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. + +- 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 From 1980b2e479dd06ca270096b6ad98ac3b5b8d24ee Mon Sep 17 00:00:00 2001 From: ebits Date: Fri, 28 Nov 2025 19:05:17 +0100 Subject: [PATCH 331/530] Feature: Added option for hibernate between sleep and brightness down (#867) (closes #866) Reviewed-on: https://codeberg.org/fairyglade/ly/pulls/867 Reviewed-by: AnErrupTion Co-authored-by: ebits Co-committed-by: ebits --- res/config.ini | 6 ++++++ res/lang/ar.ini | 2 ++ res/lang/cat.ini | 2 ++ res/lang/cs.ini | 2 ++ res/lang/de.ini | 2 ++ res/lang/en.ini | 4 +++- res/lang/es.ini | 2 ++ res/lang/fr.ini | 2 ++ res/lang/it.ini | 2 ++ res/lang/ja_JP.ini | 2 ++ res/lang/lv.ini | 2 ++ res/lang/pl.ini | 2 ++ res/lang/pt.ini | 2 ++ res/lang/pt_BR.ini | 2 ++ res/lang/ro.ini | 2 ++ res/lang/ru.ini | 2 ++ res/lang/sr.ini | 2 ++ res/lang/sv.ini | 2 ++ res/lang/tr.ini | 2 ++ res/lang/uk.ini | 2 ++ res/lang/zh_CN.ini | 2 ++ src/config/Config.zig | 2 ++ src/config/Lang.zig | 2 ++ src/main.zig | 27 +++++++++++++++++++++++++++ 24 files changed, 78 insertions(+), 1 deletion(-) diff --git a/res/config.ini b/res/config.ini index f090397..1757303 100644 --- a/res/config.ini +++ b/res/config.ini @@ -212,6 +212,12 @@ 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 used for hibernate (F1-F12) +hibernate_key = F4 + # Remove main box borders hide_borders = false diff --git a/res/lang/ar.ini b/res/lang/ar.ini index 9b371f8..b70dbe0 100644 --- a/res/lang/ar.ini +++ b/res/lang/ar.ini @@ -42,6 +42,7 @@ err_perm_dir = فشل في تغيير المجلد الحالي err_perm_group = فشل في تخفيض صلاحيات المجموعة (Group permissions) err_perm_user = فشل في تخفيض صلاحيات المستخدم (User permissions) err_pwnam = فشل في جلب معلومات المستخدم + err_sleep = فشل في تنفيذ أمر sleep @@ -66,6 +67,7 @@ restart = اعادة التشغيل shell = shell shutdown = ايقاف التشغيل sleep = وضع السكون + wayland = wayland x11 = x11 xinitrc = xinitrc diff --git a/res/lang/cat.ini b/res/lang/cat.ini index 284b14a..302ddbd 100644 --- a/res/lang/cat.ini +++ b/res/lang/cat.ini @@ -47,6 +47,7 @@ err_pwnam = error en obtenir la informació de l'usuari + 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 @@ -66,6 +67,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 cac7884..50af836 100644 --- a/res/lang/cs.ini +++ b/res/lang/cs.ini @@ -47,6 +47,7 @@ err_pwnam = nelze získat informace o uživateli + err_user_gid = nastavení GID uživatele selhalo err_user_init = inicializace uživatele selhala err_user_uid = nastavení UID uživateli selhalo @@ -66,6 +67,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 12cce5f..68cc4a6 100644 --- a/res/lang/de.ini +++ b/res/lang/de.ini @@ -42,6 +42,7 @@ err_perm_dir = Ordnerwechsel fehlgeschlagen 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 @@ -66,6 +67,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 7c95a09..513ee72 100644 --- a/res/lang/en.ini +++ b/res/lang/en.ini @@ -42,7 +42,8 @@ err_perm_dir = failed to change current directory err_perm_group = failed to downgrade group permissions err_perm_user = failed to downgrade user permissions err_pwnam = failed to get user info -err_sleep = failed to execute sleep command + + err_battery = failed to load battery status err_switch_tty = failed to switch tty err_tty_ctrl = tty control transfer failed @@ -66,6 +67,7 @@ restart = reboot shell = shell shutdown = shutdown sleep = sleep + wayland = wayland x11 = x11 xinitrc = xinitrc diff --git a/res/lang/es.ini b/res/lang/es.ini index 0878bf2..a00d866 100644 --- a/res/lang/es.ini +++ b/res/lang/es.ini @@ -47,6 +47,7 @@ err_pwnam = error al obtener la información del usuario + 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 @@ -66,6 +67,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 7774a16..82083c7 100644 --- a/res/lang/fr.ini +++ b/res/lang/fr.ini @@ -42,6 +42,7 @@ err_perm_dir = échec de changement de répertoire err_perm_group = échec du déclassement des permissions de groupe err_perm_user = échec du déclassement des permissions utilisateur err_pwnam = échec de lecture des infos utilisateur + err_sleep = échec de l'exécution de la commande de veille err_battery = échec de lecture de l'état de la batterie err_switch_tty = échec du changement de terminal @@ -66,6 +67,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 84d0d39..35dcb6a 100644 --- a/res/lang/it.ini +++ b/res/lang/it.ini @@ -47,6 +47,7 @@ err_pwnam = impossibile ottenere dati utente + err_user_gid = impossibile impostare GID utente err_user_init = impossibile inizializzare utente err_user_uid = impossible impostare UID utente @@ -66,6 +67,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 0f4c34a..ef126d6 100644 --- a/res/lang/ja_JP.ini +++ b/res/lang/ja_JP.ini @@ -42,6 +42,7 @@ err_perm_dir = カレントディレクトリの変更に失敗しました err_perm_group = グループ権限のダウングレードに失敗しました err_perm_user = ユーザー権限のダウングレードに失敗しました err_pwnam = ユーザー情報の取得に失敗しました + err_sleep = スリープコマンドの実行に失敗しました @@ -66,6 +67,7 @@ restart = 再起動 shell = シェル shutdown = シャットダウン sleep = スリープ + wayland = Wayland x11 = X11 xinitrc = xinitrc diff --git a/res/lang/lv.ini b/res/lang/lv.ini index 024b130..40992bc 100644 --- a/res/lang/lv.ini +++ b/res/lang/lv.ini @@ -42,6 +42,7 @@ err_perm_dir = neizdevās mainīt pašreizējo mapi 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_battery = neizdevās ielādēt akumulatora stāvokli err_switch_tty = neizdevās pārslēgt tty @@ -66,6 +67,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 f48d38f..791b968 100644 --- a/res/lang/pl.ini +++ b/res/lang/pl.ini @@ -42,6 +42,7 @@ err_perm_dir = nie udało się zmienić obecnego katalogu 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_battery = nie udało się sprawdzić statusu baterii err_switch_tty = nie można przełączyć tty @@ -66,6 +67,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 d47112c..25d3690 100644 --- a/res/lang/pt.ini +++ b/res/lang/pt.ini @@ -47,6 +47,7 @@ err_pwnam = erro ao obter informação do utilizador + 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 @@ -66,6 +67,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 94312fd..144e26f 100644 --- a/res/lang/pt_BR.ini +++ b/res/lang/pt_BR.ini @@ -47,6 +47,7 @@ err_pwnam = não foi possível obter informações do usuário + 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 @@ -66,6 +67,7 @@ restart = reiniciar shell = shell shutdown = desligar + wayland = wayland xinitrc = xinitrc diff --git a/res/lang/ro.ini b/res/lang/ro.ini index 78ffcc0..0dd5ab7 100644 --- a/res/lang/ro.ini +++ b/res/lang/ro.ini @@ -55,6 +55,7 @@ err_perm_user = nu s-a putut face downgrade permisiunilor de utilizator + login = utilizator logout = opreşte sesiunea @@ -66,6 +67,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 63d0824..46e7d98 100644 --- a/res/lang/ru.ini +++ b/res/lang/ru.ini @@ -42,6 +42,7 @@ err_perm_dir = не удалось изменить текущий катало err_perm_group = не удалось понизить права доступа группы err_perm_user = не удалось понизить права доступа пользователя err_pwnam = не удалось получить информацию о пользователе + err_sleep = не удалось выполнить команду sleep err_battery = не удалось получить статус батареи err_switch_tty = не удалось переключить tty @@ -66,6 +67,7 @@ restart = перезагрузить shell = оболочка shutdown = выключить sleep = сон + wayland = wayland x11 = x11 xinitrc = xinitrc diff --git a/res/lang/sr.ini b/res/lang/sr.ini index 6f8be10..236803c 100644 --- a/res/lang/sr.ini +++ b/res/lang/sr.ini @@ -47,6 +47,7 @@ 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 @@ -66,6 +67,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 030a66a..1b2efb6 100644 --- a/res/lang/sv.ini +++ b/res/lang/sv.ini @@ -47,6 +47,7 @@ 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 @@ -66,6 +67,7 @@ restart = starta om shell = skal shutdown = stäng av + wayland = wayland xinitrc = xinitrc diff --git a/res/lang/tr.ini b/res/lang/tr.ini index 0722574..07ef320 100644 --- a/res/lang/tr.ini +++ b/res/lang/tr.ini @@ -47,6 +47,7 @@ err_pwnam = kullanici bilgileri alinamadi + err_user_gid = kullanici icin GID ayarlanamadi err_user_init = kullanici oturumu baslatilamadi err_user_uid = kullanici icin UID ayarlanamadi @@ -66,6 +67,7 @@ restart = yeniden baslat shell = shell shutdown = makineyi kapat + wayland = wayland xinitrc = xinitrc diff --git a/res/lang/uk.ini b/res/lang/uk.ini index bbc6993..09320cd 100644 --- a/res/lang/uk.ini +++ b/res/lang/uk.ini @@ -47,6 +47,7 @@ err_pwnam = не вдалося отримати дані користувача + err_user_gid = не вдалося змінити GID користувача err_user_init = не вдалося ініціалізувати користувача err_user_uid = не вдалося змінити UID користувача @@ -66,6 +67,7 @@ restart = перезавантажити shell = оболонка shutdown = вимкнути + wayland = wayland xinitrc = xinitrc diff --git a/res/lang/zh_CN.ini b/res/lang/zh_CN.ini index 0c4e79e..9f2c0a8 100644 --- a/res/lang/zh_CN.ini +++ b/res/lang/zh_CN.ini @@ -47,6 +47,7 @@ err_pwnam = 获取用户信息失败 + err_user_gid = 设置用户GID失败 err_user_init = 初始化用户失败 err_user_uid = 设置用户UID失败 @@ -66,6 +67,7 @@ password = 密码 shell = shell + wayland = wayland x11 = x11 xinitrc = xinitrc diff --git a/src/config/Config.zig b/src/config/Config.zig index fc1a07f..5ddfe80 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -51,6 +51,8 @@ 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, hide_key_hints: bool = false, hide_keyboard_locks: bool = false, diff --git a/src/config/Lang.zig b/src/config/Lang.zig index 359feec..ba391b4 100644 --- a/src/config/Lang.zig +++ b/src/config/Lang.zig @@ -20,6 +20,7 @@ err_domain: []const u8 = "invalid domain", err_empty_password: []const u8 = "empty password not allowed", err_envlist: []const u8 = "failed to get envlist", err_get_active_tty: []const u8 = "failed to get active tty", +err_hibernate: []const u8 = "failed to execute hibernate command", err_hostname: []const u8 = "failed to get hostname", err_lock_state: []const u8 = "failed to get lock state", err_log: []const u8 = "failed to open log file", @@ -59,6 +60,7 @@ err_xauth: []const u8 = "xauth command failed", err_xcb_conn: []const u8 = "xcb connection failed", err_xsessions_dir: []const u8 = "failed to find sessions folder", err_xsessions_open: []const u8 = "failed to open sessions folder", +hibernate: []const u8 = "hibernate", insert: []const u8 = "insert", login: []const u8 = "login", logout: []const u8 = "logged out", diff --git a/src/main.zig b/src/main.zig index cc69921..28bbdca 100644 --- a/src/main.zig +++ b/src/main.zig @@ -540,6 +540,8 @@ pub fn main() !void { 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; @@ -727,6 +729,15 @@ pub fn main() !void { 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; @@ -857,6 +868,22 @@ pub fn main() !void { } } } + } 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_writer.print("failed to execute hibernate command: exit code {d}\n", .{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); From 10854e643a1748cfcf303423caf09e0fdc45f39a Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Sun, 30 Nov 2025 09:14:10 +0100 Subject: [PATCH 332/530] Don't crash when failing to parse arguments Signed-off-by: AnErrupTion --- res/lang/ar.ini | 5 +++-- res/lang/cat.ini | 5 +++-- res/lang/cs.ini | 5 +++-- res/lang/de.ini | 5 +++-- res/lang/en.ini | 7 ++++--- res/lang/es.ini | 5 +++-- res/lang/fr.ini | 5 +++-- res/lang/it.ini | 5 +++-- res/lang/ja_JP.ini | 5 +++-- res/lang/lv.ini | 5 +++-- res/lang/pl.ini | 5 +++-- res/lang/pt.ini | 5 +++-- res/lang/pt_BR.ini | 5 +++-- res/lang/ro.ini | 3 ++- res/lang/ru.ini | 5 +++-- res/lang/sr.ini | 5 +++-- res/lang/sv.ini | 5 +++-- res/lang/tr.ini | 5 +++-- res/lang/uk.ini | 5 +++-- res/lang/zh_CN.ini | 5 +++-- src/config/Lang.zig | 1 + src/main.zig | 42 ++++++++++++++++++++++++++++-------------- 22 files changed, 89 insertions(+), 54 deletions(-) diff --git a/res/lang/ar.ini b/res/lang/ar.ini index b70dbe0..6f48471 100644 --- a/res/lang/ar.ini +++ b/res/lang/ar.ini @@ -5,6 +5,7 @@ capslock = capslock err_alloc = فشل في تخصيص الذاكرة + err_bounds = out-of-bounds index err_brightness_change = فشل في تغيير سطوع الشاشة err_chdir = فشل في فتح مجلد المنزل @@ -15,6 +16,7 @@ err_domain = اسم نطاق غير صالح err_empty_password = لا يُسمح بكلمة مرور فارغة err_envlist = فشل في جلب قائمة المتغيرات البيئية + err_hostname = فشل في جلب اسم المضيف (Hostname) @@ -42,7 +44,6 @@ err_perm_dir = فشل في تغيير المجلد الحالي err_perm_group = فشل في تخفيض صلاحيات المجموعة (Group permissions) err_perm_user = فشل في تخفيض صلاحيات المستخدم (User permissions) err_pwnam = فشل في جلب معلومات المستخدم - err_sleep = فشل في تنفيذ أمر sleep @@ -55,6 +56,7 @@ err_xauth = فشل في تنفيذ أمر xauth err_xcb_conn = فشل في الاتصال بمكتبة XCB err_xsessions_dir = فشل في العثور على مجلد Xsessions err_xsessions_open = فشل في فتح مجلد Xsessions + insert = ادخال login = تسجيل الدخول logout = تم تسجيل خروجك @@ -67,7 +69,6 @@ restart = اعادة التشغيل shell = shell shutdown = ايقاف التشغيل sleep = وضع السكون - wayland = wayland x11 = x11 xinitrc = xinitrc diff --git a/res/lang/cat.ini b/res/lang/cat.ini index 302ddbd..f7e4343 100644 --- a/res/lang/cat.ini +++ b/res/lang/cat.ini @@ -5,6 +5,7 @@ capslock = Bloq Majús err_alloc = assignació de memòria fallida + err_bounds = índex fora de límits err_brightness_change = error en canviar la brillantor err_chdir = error en obrir la carpeta home @@ -15,6 +16,7 @@ err_domain = domini invàlid err_envlist = error en obtenir l'envlist + err_hostname = error en obtenir el nom de l'amfitrió @@ -47,7 +49,6 @@ err_pwnam = error en obtenir la informació de l'usuari - 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 @@ -55,6 +56,7 @@ 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 + insert = inserir login = iniciar sessió logout = sessió tancada @@ -67,7 +69,6 @@ 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 50af836..c25ace1 100644 --- a/res/lang/cs.ini +++ b/res/lang/cs.ini @@ -5,6 +5,7 @@ capslock = capslock err_alloc = alokace paměti selhala + err_bounds = index je mimo hranice pole err_chdir = nelze otevřít domovský adresář @@ -15,6 +16,7 @@ err_domain = neplatná doména + err_hostname = nelze získat název hostitele @@ -47,7 +49,6 @@ err_pwnam = nelze získat informace o uživateli - err_user_gid = nastavení GID uživatele selhalo err_user_init = inicializace uživatele selhala err_user_uid = nastavení UID uživateli selhalo @@ -56,6 +57,7 @@ err_user_uid = nastavení UID uživateli selhalo err_xsessions_dir = nepodařilo se najít složku relací err_xsessions_open = nepodařilo se otevřít složku relací + login = uživatel logout = odhlášen @@ -67,7 +69,6 @@ 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 68cc4a6..ac8f654 100644 --- a/res/lang/de.ini +++ b/res/lang/de.ini @@ -5,6 +5,7 @@ capslock = Feststelltaste err_alloc = Speicherzuweisung fehlgeschlagen + err_bounds = Index ausserhalb des Bereichs err_brightness_change = Helligkeitsänderung fehlgeschlagen err_chdir = Fehler beim Oeffnen des Home-Ordners @@ -15,6 +16,7 @@ err_domain = Ungueltige Domain err_empty_password = Leeres Passwort nicht zugelassen err_envlist = Fehler beim Abrufen der Umgebungs-Variablen + err_hostname = Abrufen des Hostnames fehlgeschlagen @@ -42,7 +44,6 @@ err_perm_dir = Ordnerwechsel fehlgeschlagen 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 @@ -55,6 +56,7 @@ 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 + insert = Einfügen login = Nutzer logout = Abmelden @@ -67,7 +69,6 @@ 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 513ee72..325e9b9 100644 --- a/res/lang/en.ini +++ b/res/lang/en.ini @@ -4,6 +4,7 @@ brightness_up = increase brightness capslock = capslock custom = custom err_alloc = failed memory allocation +err_args = unable to parse command line arguments err_autologin_session = autologin session not found err_bounds = out-of-bounds index err_brightness_change = failed to change brightness @@ -15,6 +16,7 @@ err_domain = invalid domain err_empty_password = empty password not allowed err_envlist = failed to get envlist err_get_active_tty = failed to get active tty +err_hibernate = failed to execute hibernate command err_hostname = failed to get hostname err_lock_state = failed to get lock state err_log = failed to open log file @@ -42,8 +44,7 @@ err_perm_dir = failed to change current directory err_perm_group = failed to downgrade group permissions err_perm_user = failed to downgrade user permissions err_pwnam = failed to get user info - - +err_sleep = failed to execute sleep command err_battery = failed to load battery status err_switch_tty = failed to switch tty err_tty_ctrl = tty control transfer failed @@ -55,6 +56,7 @@ err_xauth = xauth command failed err_xcb_conn = xcb connection failed err_xsessions_dir = failed to find sessions folder err_xsessions_open = failed to open sessions folder +hibernate = hibernate insert = insert login = login logout = logged out @@ -67,7 +69,6 @@ restart = reboot shell = shell shutdown = shutdown sleep = sleep - wayland = wayland x11 = x11 xinitrc = xinitrc diff --git a/res/lang/es.ini b/res/lang/es.ini index a00d866..fe720c7 100644 --- a/res/lang/es.ini +++ b/res/lang/es.ini @@ -5,6 +5,7 @@ capslock = Bloq Mayús err_alloc = asignación de memoria fallida + err_bounds = índice fuera de límites err_chdir = error al abrir la carpeta home @@ -15,6 +16,7 @@ err_domain = dominio inválido + err_hostname = error al obtener el nombre de host @@ -47,7 +49,6 @@ err_pwnam = error al obtener la información del usuario - 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 @@ -55,6 +56,7 @@ err_user_uid = error al establecer el UID del usuario err_xsessions_dir = error al buscar la carpeta de sesiones err_xsessions_open = error al abrir la carpeta de sesiones + insert = insertar login = usuario logout = cerrar sesión @@ -67,7 +69,6 @@ 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 82083c7..1ed64e4 100644 --- a/res/lang/fr.ini +++ b/res/lang/fr.ini @@ -5,6 +5,7 @@ capslock = verr.maj custom = customisé err_alloc = échec d'allocation mémoire + err_bounds = indice hors-limite err_brightness_change = échec du changement de luminosité err_chdir = échec de l'ouverture du répertoire home @@ -15,6 +16,7 @@ err_domain = domaine invalide err_empty_password = mot de passe vide non autorisé err_envlist = échec de lecture de la liste d'environnement err_get_active_tty = échec de lecture du terminal actif + err_hostname = échec de lecture du nom d'hôte err_lock_state = échec de lecture de l'état de verrouillage err_log = échec de l'ouverture du fichier de journal @@ -42,7 +44,6 @@ err_perm_dir = échec de changement de répertoire err_perm_group = échec du déclassement des permissions de groupe err_perm_user = échec du déclassement des permissions utilisateur err_pwnam = échec de lecture des infos utilisateur - err_sleep = échec de l'exécution de la commande de veille err_battery = échec de lecture de l'état de la batterie err_switch_tty = échec du changement de terminal @@ -55,6 +56,7 @@ err_xauth = échec de la commande xauth err_xcb_conn = échec de la connexion xcb err_xsessions_dir = échec de la recherche du dossier de sessions err_xsessions_open = échec de l'ouverture du dossier de sessions + insert = insertion login = identifiant logout = déconnecté @@ -67,7 +69,6 @@ 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 35dcb6a..5cab01d 100644 --- a/res/lang/it.ini +++ b/res/lang/it.ini @@ -5,6 +5,7 @@ capslock = capslock err_alloc = impossibile allocare memoria + err_bounds = indice fuori limite err_chdir = impossibile aprire home directory @@ -15,6 +16,7 @@ err_domain = dominio non valido + err_hostname = impossibile ottenere hostname @@ -47,7 +49,6 @@ err_pwnam = impossibile ottenere dati utente - err_user_gid = impossibile impostare GID utente err_user_init = impossibile inizializzare utente err_user_uid = impossible impostare UID utente @@ -56,6 +57,7 @@ err_user_uid = impossible impostare UID utente err_xsessions_dir = impossibile localizzare cartella sessioni err_xsessions_open = impossibile aprire cartella sessioni + login = username logout = scollegato @@ -67,7 +69,6 @@ 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 ef126d6..44fff53 100644 --- a/res/lang/ja_JP.ini +++ b/res/lang/ja_JP.ini @@ -5,6 +5,7 @@ capslock = CapsLock err_alloc = メモリ割り当て失敗 + err_bounds = 境界外インデックス err_brightness_change = 明るさの変更に失敗しました err_chdir = ホームフォルダを開けませんでした @@ -15,6 +16,7 @@ err_domain = 無効なドメイン err_empty_password = 空のパスワードは許可されていません err_envlist = 環境変数リストの取得に失敗しました + err_hostname = ホスト名の取得に失敗しました @@ -42,7 +44,6 @@ err_perm_dir = カレントディレクトリの変更に失敗しました err_perm_group = グループ権限のダウングレードに失敗しました err_perm_user = ユーザー権限のダウングレードに失敗しました err_pwnam = ユーザー情報の取得に失敗しました - err_sleep = スリープコマンドの実行に失敗しました @@ -55,6 +56,7 @@ err_xauth = xauthコマンドの実行に失敗しました err_xcb_conn = XCB接続に失敗しました err_xsessions_dir = セッションフォルダが見つかりませんでした err_xsessions_open = セッションフォルダを開けませんでした + insert = 挿入 login = ログイン logout = ログアウト済み @@ -67,7 +69,6 @@ restart = 再起動 shell = シェル shutdown = シャットダウン sleep = スリープ - wayland = Wayland x11 = X11 xinitrc = xinitrc diff --git a/res/lang/lv.ini b/res/lang/lv.ini index 40992bc..7d0947f 100644 --- a/res/lang/lv.ini +++ b/res/lang/lv.ini @@ -5,6 +5,7 @@ capslock = caps lock custom = pielāgots err_alloc = neizdevās atmiņas piešķiršana + err_bounds = indekss ārpus robežām err_brightness_change = neizdevās mainīt spilgtumu err_chdir = neizdevās atvērt mājas mapi @@ -15,6 +16,7 @@ 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_hostname = neizdevās iegūt hostname err_lock_state = neizdevās iegūt bloķēšanas stāvokli err_log = neizdevās atvērt žurnāla failu @@ -42,7 +44,6 @@ err_perm_dir = neizdevās mainīt pašreizējo mapi 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_battery = neizdevās ielādēt akumulatora stāvokli err_switch_tty = neizdevās pārslēgt tty @@ -55,6 +56,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 + insert = ievietot login = lietotājs logout = iziet @@ -67,7 +69,6 @@ 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 791b968..ad57810 100644 --- a/res/lang/pl.ini +++ b/res/lang/pl.ini @@ -4,6 +4,7 @@ brightness_up = zwiększ jasność capslock = capslock custom = własny err_alloc = nieudana alokacja pamięci + err_autologin_session = nie znaleziono sesji autologowania err_bounds = indeks poza zakresem err_brightness_change = nie udało się zmienić jasności @@ -15,6 +16,7 @@ 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_hostname = nie udało się uzyskać nazwy hosta err_lock_state = nie udało się uzyskać stanu blokady err_log = nie udało się otworzyć pliku logu @@ -42,7 +44,6 @@ err_perm_dir = nie udało się zmienić obecnego katalogu 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_battery = nie udało się sprawdzić statusu baterii err_switch_tty = nie można przełączyć tty @@ -55,6 +56,7 @@ 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 + insert = wstaw login = login logout = wylogowano @@ -67,7 +69,6 @@ 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 25d3690..2b4fe45 100644 --- a/res/lang/pt.ini +++ b/res/lang/pt.ini @@ -5,6 +5,7 @@ capslock = capslock err_alloc = erro na atribuição de memória + err_bounds = índice fora de limites err_chdir = erro ao abrir a pasta home @@ -15,6 +16,7 @@ err_domain = domínio inválido + err_hostname = erro ao obter o nome do host @@ -47,7 +49,6 @@ err_pwnam = erro ao obter informação do utilizador - 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 @@ -56,6 +57,7 @@ err_user_uid = erro ao definir o UID do utilizador err_xsessions_dir = erro ao localizar a pasta das sessões err_xsessions_open = erro ao abrir a pasta das sessões + login = iniciar sessão logout = terminar sessão @@ -67,7 +69,6 @@ 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 144e26f..0f53481 100644 --- a/res/lang/pt_BR.ini +++ b/res/lang/pt_BR.ini @@ -5,6 +5,7 @@ capslock = caixa alta err_alloc = alocação de memória malsucedida + err_bounds = índice fora de limites err_chdir = não foi possível abrir o diretório home @@ -15,6 +16,7 @@ err_domain = domínio inválido + err_hostname = não foi possível obter o nome do host @@ -47,7 +49,6 @@ err_pwnam = não foi possível obter informações do usuário - 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 @@ -56,6 +57,7 @@ err_user_uid = não foi possível definir o UID do usuário 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 + login = conectar logout = desconectado @@ -67,7 +69,6 @@ restart = reiniciar shell = shell shutdown = desligar - wayland = wayland xinitrc = xinitrc diff --git a/res/lang/ro.ini b/res/lang/ro.ini index 0dd5ab7..a045ffd 100644 --- a/res/lang/ro.ini +++ b/res/lang/ro.ini @@ -20,6 +20,8 @@ capslock = capslock + + err_pam_abort = tranzacţie pam anulată @@ -67,7 +69,6 @@ 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 46e7d98..5af0923 100644 --- a/res/lang/ru.ini +++ b/res/lang/ru.ini @@ -4,6 +4,7 @@ brightness_up = увеличить яркость capslock = capslock custom = пользовательский err_alloc = не удалось выделить память + err_autologin_session = не найдена сессия с автологином err_bounds = за пределами индекса err_brightness_change = не удалось изменить яркость @@ -15,6 +16,7 @@ err_domain = неверный домен err_empty_password = пустой пароль не допустим err_envlist = не удалось получить список переменных среды err_get_active_tty = не удалось получить активный tty + err_hostname = не удалось получить имя хоста err_lock_state = не удалось получить состояние lock err_log = не удалось открыть файл log @@ -42,7 +44,6 @@ err_perm_dir = не удалось изменить текущий катало err_perm_group = не удалось понизить права доступа группы err_perm_user = не удалось понизить права доступа пользователя err_pwnam = не удалось получить информацию о пользователе - err_sleep = не удалось выполнить команду sleep err_battery = не удалось получить статус батареи err_switch_tty = не удалось переключить tty @@ -55,6 +56,7 @@ err_xauth = команда xauth не выполнена err_xcb_conn = ошибка подключения xcb err_xsessions_dir = не удалось найти сессионную папку err_xsessions_open = не удалось открыть сессионную папку + insert = вставка login = логин logout = вышел из системы @@ -67,7 +69,6 @@ restart = перезагрузить shell = оболочка shutdown = выключить sleep = сон - wayland = wayland x11 = x11 xinitrc = xinitrc diff --git a/res/lang/sr.ini b/res/lang/sr.ini index 236803c..b9159cc 100644 --- a/res/lang/sr.ini +++ b/res/lang/sr.ini @@ -5,6 +5,7 @@ capslock = capslock err_alloc = neuspijesna alokacija memorije + err_bounds = izvan granica indeksa err_chdir = neuspijesno otvaranje home foldera @@ -15,6 +16,7 @@ err_domain = nevazeci domen + err_hostname = neuspijesno trazenje hostname-a @@ -47,7 +49,6 @@ 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 @@ -56,6 +57,7 @@ err_user_uid = neuspijesno postavljanje UID-a korisnika err_xsessions_dir = neuspijesno pronalazenje foldera sesija err_xsessions_open = neuspijesno otvaranje foldera sesija + login = korisnik logout = izlogovan @@ -67,7 +69,6 @@ restart = ponovo pokreni shell = shell shutdown = ugasi - wayland = wayland xinitrc = xinitrc diff --git a/res/lang/sv.ini b/res/lang/sv.ini index 1b2efb6..2fb7f91 100644 --- a/res/lang/sv.ini +++ b/res/lang/sv.ini @@ -5,6 +5,7 @@ capslock = capslock err_alloc = misslyckad minnesallokering + err_bounds = utanför banan index err_chdir = misslyckades att öppna hemkatalog @@ -15,6 +16,7 @@ err_domain = okänd domän + err_hostname = misslyckades att hämta värdnamn @@ -47,7 +49,6 @@ 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 @@ -56,6 +57,7 @@ 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 + login = inloggning logout = utloggad @@ -67,7 +69,6 @@ restart = starta om shell = skal shutdown = stäng av - wayland = wayland xinitrc = xinitrc diff --git a/res/lang/tr.ini b/res/lang/tr.ini index 07ef320..a145152 100644 --- a/res/lang/tr.ini +++ b/res/lang/tr.ini @@ -5,6 +5,7 @@ capslock = capslock err_alloc = basarisiz bellek ayirma + err_bounds = sinirlarin disinda dizin err_chdir = ev klasoru acilamadi @@ -15,6 +16,7 @@ err_domain = gecersiz etki alani + err_hostname = ana bilgisayar adi alinamadi @@ -47,7 +49,6 @@ err_pwnam = kullanici bilgileri alinamadi - err_user_gid = kullanici icin GID ayarlanamadi err_user_init = kullanici oturumu baslatilamadi err_user_uid = kullanici icin UID ayarlanamadi @@ -56,6 +57,7 @@ err_user_uid = kullanici icin UID ayarlanamadi err_xsessions_dir = oturumlar klasoru bulunamadi err_xsessions_open = oturumlar klasoru acilamadi + login = kullanici logout = oturumdan cikis yapildi @@ -67,7 +69,6 @@ restart = yeniden baslat shell = shell shutdown = makineyi kapat - wayland = wayland xinitrc = xinitrc diff --git a/res/lang/uk.ini b/res/lang/uk.ini index 09320cd..0cbb714 100644 --- a/res/lang/uk.ini +++ b/res/lang/uk.ini @@ -5,6 +5,7 @@ capslock = capslock err_alloc = невдале виділення пам'яті + err_bounds = поза межами індексу err_chdir = не вдалося відкрити домашній каталог @@ -15,6 +16,7 @@ err_domain = недійсний домен + err_hostname = не вдалося отримати ім'я хосту @@ -47,7 +49,6 @@ err_pwnam = не вдалося отримати дані користувача - err_user_gid = не вдалося змінити GID користувача err_user_init = не вдалося ініціалізувати користувача err_user_uid = не вдалося змінити UID користувача @@ -56,6 +57,7 @@ err_user_uid = не вдалося змінити UID користувача err_xsessions_dir = не вдалося знайти каталог сесій err_xsessions_open = не вдалося відкрити каталог сесій + login = логін logout = вийти @@ -67,7 +69,6 @@ restart = перезавантажити shell = оболонка shutdown = вимкнути - wayland = wayland xinitrc = xinitrc diff --git a/res/lang/zh_CN.ini b/res/lang/zh_CN.ini index 9f2c0a8..7b30e9e 100644 --- a/res/lang/zh_CN.ini +++ b/res/lang/zh_CN.ini @@ -5,6 +5,7 @@ capslock = 大写锁定 err_alloc = 内存分配失败 + err_bounds = 索引越界 err_chdir = 无法打开home文件夹 @@ -15,6 +16,7 @@ err_domain = 无效的域 + err_hostname = 获取主机名失败 @@ -47,7 +49,6 @@ err_pwnam = 获取用户信息失败 - err_user_gid = 设置用户GID失败 err_user_init = 初始化用户失败 err_user_uid = 设置用户UID失败 @@ -56,6 +57,7 @@ err_user_uid = 设置用户UID失败 err_xsessions_dir = 找不到会话文件夹 err_xsessions_open = 无法打开会话文件夹 + login = 登录 logout = 注销 @@ -67,7 +69,6 @@ password = 密码 shell = shell - wayland = wayland x11 = x11 xinitrc = xinitrc diff --git a/src/config/Lang.zig b/src/config/Lang.zig index ba391b4..84c8a6f 100644 --- a/src/config/Lang.zig +++ b/src/config/Lang.zig @@ -9,6 +9,7 @@ brightness_up: []const u8 = "increase brightness", capslock: []const u8 = "capslock", custom: []const u8 = "custom", 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", err_bounds: []const u8 = "out-of-bounds index", err_brightness_change: []const u8 = "failed to change brightness", diff --git a/src/main.zig b/src/main.zig index 28bbdca..d06a46d 100644 --- a/src/main.zig +++ b/src/main.zig @@ -110,12 +110,14 @@ pub fn main() !void { ); var diag = clap.Diagnostic{}; - var res = clap.parse(clap.Help, ¶ms, clap.parsers.default, .{ .diagnostic = &diag, .allocator = allocator }) catch |err| { + 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: { + arg_parse_error = err; diag.report(stderr, err) catch {}; try stderr.flush(); - return err; + break :parse_error null; }; - defer res.deinit(); + defer if (maybe_res) |*res| res.deinit(); var config: Config = undefined; var lang: Lang = undefined; @@ -128,17 +130,19 @@ pub fn main() !void { var saved_users = SavedUsers.init(); defer saved_users.deinit(allocator); - if (res.args.help != 0) { - try clap.help(stderr, clap.Help, ¶ms, .{}); + if (maybe_res) |*res| { + 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.process.exit(0); - } - if (res.args.version != 0) { - _ = try stderr.write("Ly version " ++ build_options.version ++ "\n"); - try stderr.flush(); - std.process.exit(0); + _ = 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.process.exit(0); + } + if (res.args.version != 0) { + _ = try stderr.write("Ly version " ++ build_options.version ++ "\n"); + try stderr.flush(); + std.process.exit(0); + } } // Load configuration file @@ -161,7 +165,8 @@ pub fn main() !void { const comment_characters = "#"; - if (res.args.config) |s| { + 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 }); @@ -330,6 +335,15 @@ pub fn main() !void { var info_line = InfoLine.init(allocator, &buffer); defer 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_writer.print("unable to parse argument '{s}{s}': {s}\n", .{ longest.kind.prefix(), longest.name, @errorName(arg_parse_error) }); + } + 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); From 392ea6ea6382548f6bed9d2727b4b1428476d204 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Sun, 30 Nov 2025 09:18:29 +0100 Subject: [PATCH 333/530] Update French locale 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 1ed64e4..ebc6589 100644 --- a/res/lang/fr.ini +++ b/res/lang/fr.ini @@ -4,8 +4,8 @@ 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 err_bounds = indice hors-limite err_brightness_change = échec du changement de luminosité err_chdir = échec de l'ouverture du répertoire home @@ -16,7 +16,7 @@ err_domain = domaine invalide err_empty_password = mot de passe vide non autorisé err_envlist = échec de lecture de la liste d'environnement err_get_active_tty = échec de lecture du terminal actif - +err_hibernate = échec de l'exécution de la commande de veille prolongée err_hostname = échec de lecture du nom d'hôte err_lock_state = échec de lecture de l'état de verrouillage err_log = échec de l'ouverture du fichier de journal @@ -56,7 +56,7 @@ err_xauth = échec de la commande xauth err_xcb_conn = échec de la connexion xcb err_xsessions_dir = échec de la recherche du dossier de sessions err_xsessions_open = échec de l'ouverture du dossier de sessions - +hibernate = veille prolongée insert = insertion login = identifiant logout = déconnecté From 8c964d9ce5e073edc27761c232b0a3fb1268d347 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Sun, 30 Nov 2025 09:30:33 +0100 Subject: [PATCH 334/530] Don't crash when failing to crawl a session directory (closes #870) Signed-off-by: AnErrupTion --- res/lang/ar.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/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 | 1 + res/lang/tr.ini | 1 + res/lang/uk.ini | 1 + res/lang/zh_CN.ini | 1 + src/config/Lang.zig | 1 + src/main.zig | 25 +++++++++++++++++++++---- 22 files changed, 42 insertions(+), 4 deletions(-) diff --git a/res/lang/ar.ini b/res/lang/ar.ini index 6f48471..f98c911 100644 --- a/res/lang/ar.ini +++ b/res/lang/ar.ini @@ -11,6 +11,7 @@ err_brightness_change = فشل في تغيير سطوع الشاشة err_chdir = فشل في فتح مجلد المنزل err_config = فشل في تفسير ملف الإعدادات + err_dgn_oob = رسالة سجل (Log) err_domain = اسم نطاق غير صالح err_empty_password = لا يُسمح بكلمة مرور فارغة diff --git a/res/lang/cat.ini b/res/lang/cat.ini index f7e4343..a0bd809 100644 --- a/res/lang/cat.ini +++ b/res/lang/cat.ini @@ -11,6 +11,7 @@ err_brightness_change = error en canviar la brillantor err_chdir = error en obrir la carpeta home + err_dgn_oob = missatge de registre err_domain = domini invàlid diff --git a/res/lang/cs.ini b/res/lang/cs.ini index c25ace1..a84757b 100644 --- a/res/lang/cs.ini +++ b/res/lang/cs.ini @@ -11,6 +11,7 @@ err_bounds = index je mimo hranice pole err_chdir = nelze otevřít domovský adresář + err_dgn_oob = zpráva protokolu err_domain = neplatná doména diff --git a/res/lang/de.ini b/res/lang/de.ini index ac8f654..4f52bea 100644 --- a/res/lang/de.ini +++ b/res/lang/de.ini @@ -11,6 +11,7 @@ err_brightness_change = Helligkeitsänderung fehlgeschlagen err_chdir = Fehler beim Oeffnen des Home-Ordners err_config = Fehler beim Verarbeiten der Konfigurationsdatei + err_dgn_oob = Diagnose-Nachricht err_domain = Ungueltige Domain err_empty_password = Leeres Passwort nicht zugelassen diff --git a/res/lang/en.ini b/res/lang/en.ini index 325e9b9..60368ac 100644 --- a/res/lang/en.ini +++ b/res/lang/en.ini @@ -11,6 +11,7 @@ err_brightness_change = failed to change brightness err_chdir = failed to open home folder err_clock_too_long = clock string too long err_config = unable to parse config file + err_dgn_oob = log message err_domain = invalid domain err_empty_password = empty password not allowed diff --git a/res/lang/es.ini b/res/lang/es.ini index fe720c7..62d0513 100644 --- a/res/lang/es.ini +++ b/res/lang/es.ini @@ -11,6 +11,7 @@ err_bounds = índice fuera de límites err_chdir = error al abrir la carpeta home + err_dgn_oob = mensaje de registro err_domain = dominio inválido diff --git a/res/lang/fr.ini b/res/lang/fr.ini index ebc6589..cc6801e 100644 --- a/res/lang/fr.ini +++ b/res/lang/fr.ini @@ -11,6 +11,7 @@ err_brightness_change = échec du changement de luminosité err_chdir = échec de l'ouverture du répertoire home err_clock_too_long = chaîne de formattage de l'horloge trop longue err_config = échec de lecture du fichier de configuration +err_crawl = échec de la navigation des répertoires de session err_dgn_oob = message err_domain = domaine invalide err_empty_password = mot de passe vide non autorisé diff --git a/res/lang/it.ini b/res/lang/it.ini index 5cab01d..bc69e7c 100644 --- a/res/lang/it.ini +++ b/res/lang/it.ini @@ -11,6 +11,7 @@ err_bounds = indice fuori limite err_chdir = impossibile aprire home directory + err_dgn_oob = messaggio log err_domain = dominio non valido diff --git a/res/lang/ja_JP.ini b/res/lang/ja_JP.ini index 44fff53..23c3f84 100644 --- a/res/lang/ja_JP.ini +++ b/res/lang/ja_JP.ini @@ -11,6 +11,7 @@ err_brightness_change = 明るさの変更に失敗しました err_chdir = ホームフォルダを開けませんでした err_config = 設定ファイルを解析できません + err_dgn_oob = ログメッセージ err_domain = 無効なドメイン err_empty_password = 空のパスワードは許可されていません diff --git a/res/lang/lv.ini b/res/lang/lv.ini index 7d0947f..fcac1aa 100644 --- a/res/lang/lv.ini +++ b/res/lang/lv.ini @@ -11,6 +11,7 @@ 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_dgn_oob = žurnāla ziņojums err_domain = nederīgs domēns err_empty_password = tukša parole nav atļauta diff --git a/res/lang/pl.ini b/res/lang/pl.ini index ad57810..42e2660 100644 --- a/res/lang/pl.ini +++ b/res/lang/pl.ini @@ -11,6 +11,7 @@ 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_dgn_oob = wiadomość loga err_domain = niepoprawna domena err_empty_password = puste hasło jest niedozwolone diff --git a/res/lang/pt.ini b/res/lang/pt.ini index 2b4fe45..a0618c9 100644 --- a/res/lang/pt.ini +++ b/res/lang/pt.ini @@ -11,6 +11,7 @@ err_bounds = índice fora de limites err_chdir = erro ao abrir a pasta home + err_dgn_oob = mensagem de registo err_domain = domínio inválido diff --git a/res/lang/pt_BR.ini b/res/lang/pt_BR.ini index 0f53481..62eae98 100644 --- a/res/lang/pt_BR.ini +++ b/res/lang/pt_BR.ini @@ -11,6 +11,7 @@ err_bounds = índice fora de limites err_chdir = não foi possível abrir o diretório home + err_dgn_oob = mensagem de log err_domain = domínio inválido diff --git a/res/lang/ro.ini b/res/lang/ro.ini index a045ffd..aa59318 100644 --- a/res/lang/ro.ini +++ b/res/lang/ro.ini @@ -22,6 +22,7 @@ capslock = capslock + err_pam_abort = tranzacţie pam anulată diff --git a/res/lang/ru.ini b/res/lang/ru.ini index 5af0923..fd5d4ed 100644 --- a/res/lang/ru.ini +++ b/res/lang/ru.ini @@ -11,6 +11,7 @@ err_brightness_change = не удалось изменить яркость err_chdir = не удалось открыть домашнюю папку err_clock_too_long = строка часов слишком длинная err_config = не удалось разобрать файл конфигурации + err_dgn_oob = отладочное сообщение (log) err_domain = неверный домен err_empty_password = пустой пароль не допустим diff --git a/res/lang/sr.ini b/res/lang/sr.ini index b9159cc..96b0686 100644 --- a/res/lang/sr.ini +++ b/res/lang/sr.ini @@ -11,6 +11,7 @@ err_bounds = izvan granica indeksa err_chdir = neuspijesno otvaranje home foldera + err_dgn_oob = log poruka err_domain = nevazeci domen diff --git a/res/lang/sv.ini b/res/lang/sv.ini index 2fb7f91..9439a07 100644 --- a/res/lang/sv.ini +++ b/res/lang/sv.ini @@ -11,6 +11,7 @@ err_bounds = utanför banan index err_chdir = misslyckades att öppna hemkatalog + err_dgn_oob = loggmeddelande err_domain = okänd domän diff --git a/res/lang/tr.ini b/res/lang/tr.ini index a145152..d551399 100644 --- a/res/lang/tr.ini +++ b/res/lang/tr.ini @@ -11,6 +11,7 @@ err_bounds = sinirlarin disinda dizin err_chdir = ev klasoru acilamadi + err_dgn_oob = log mesaji err_domain = gecersiz etki alani diff --git a/res/lang/uk.ini b/res/lang/uk.ini index 0cbb714..531f311 100644 --- a/res/lang/uk.ini +++ b/res/lang/uk.ini @@ -11,6 +11,7 @@ err_bounds = поза межами індексу err_chdir = не вдалося відкрити домашній каталог + err_dgn_oob = повідомлення журналу (log) err_domain = недійсний домен diff --git a/res/lang/zh_CN.ini b/res/lang/zh_CN.ini index 7b30e9e..f73093b 100644 --- a/res/lang/zh_CN.ini +++ b/res/lang/zh_CN.ini @@ -11,6 +11,7 @@ err_bounds = 索引越界 err_chdir = 无法打开home文件夹 + err_dgn_oob = 日志消息 err_domain = 无效的域 diff --git a/src/config/Lang.zig b/src/config/Lang.zig index 84c8a6f..e116f39 100644 --- a/src/config/Lang.zig +++ b/src/config/Lang.zig @@ -16,6 +16,7 @@ err_brightness_change: []const u8 = "failed to change brightness", err_chdir: []const u8 = "failed to open home folder", err_clock_too_long: []const u8 = "clock string too long", err_config: []const u8 = "unable to parse config file", +err_crawl: []const u8 = "failed to crawl session directories", err_dgn_oob: []const u8 = "log message", err_domain: []const u8 = "invalid domain", err_empty_password: []const u8 = "empty password not allowed", diff --git a/src/main.zig b/src/main.zig index d06a46d..5fc3545 100644 --- a/src/main.zig +++ b/src/main.zig @@ -411,22 +411,37 @@ pub fn main() !void { try info_line.addMessage(hostname, config.bg, config.fg); } + var has_crawl_error = false; + // Crawl session directories (Wayland, X11 and custom respectively) var wayland_session_dirs = std.mem.splitScalar(u8, config.waylandsessions, ':'); while (wayland_session_dirs.next()) |dir| { - try crawl(&session, lang, dir, .wayland); + 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) }); + }; } if (build_options.enable_x11_support) { var x_session_dirs = std.mem.splitScalar(u8, config.xsessions, ':'); while (x_session_dirs.next()) |dir| { - try crawl(&session, lang, dir, .x11); + 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) }); + }; } } var custom_session_dirs = std.mem.splitScalar(u8, config.custom_sessions, ':'); while (custom_session_dirs.next()) |dir| { - try crawl(&session, lang, dir, .custom); + 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) }); + }; + } + + if (has_crawl_error) { + try info_line.addMessage(lang.err_crawl, config.error_bg, config.error_fg); } if (usernames.items.len == 0) { @@ -1181,7 +1196,9 @@ fn addOtherEnvironment(session: *Session, lang: Lang, display_server: DisplaySer } fn crawl(session: *Session, lang: Lang, path: []const u8, display_server: DisplayServer) !void { - var iterable_directory = std.fs.openDirAbsolute(path, .{ .iterate = true }) catch return; + if (!std.fs.path.isAbsolute(path)) return error.PathNotAbsolute; + + var iterable_directory = try std.fs.openDirAbsolute(path, .{ .iterate = true }); defer iterable_directory.close(); var iterator = iterable_directory.iterate(); From fe354a480967719c5decb097ce9401553369ddb0 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Sun, 30 Nov 2025 09:47:50 +0100 Subject: [PATCH 335/530] Add fallback UID range options at compile-time Signed-off-by: AnErrupTion --- build.zig | 4 ++++ res/lang/ar.ini | 1 + res/lang/cat.ini | 1 + res/lang/cs.ini | 1 + res/lang/de.ini | 1 + res/lang/en.ini | 3 ++- res/lang/es.ini | 1 + res/lang/fr.ini | 1 + res/lang/it.ini | 1 + res/lang/ja_JP.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 | 1 + res/lang/tr.ini | 1 + res/lang/uk.ini | 1 + res/lang/zh_CN.ini | 1 + src/config/Lang.zig | 1 + src/main.zig | 19 ++++++++++++++++--- 23 files changed, 42 insertions(+), 4 deletions(-) diff --git a/build.zig b/build.zig index bf23051..fe73229 100644 --- a/build.zig +++ b/build.zig @@ -44,6 +44,8 @@ pub fn build(b: *std.Build) !void { const enable_x11_support = b.option(bool, "enable_x11_support", "Enable X11 support (default is on)") orelse true; const default_tty = b.option(u8, "default_tty", "Set the TTY (default is 2)") orelse 2; const fallback_tty = b.option(u8, "fallback_tty", "Set the fallback TTY (default is 2). This value gets embedded into the binary") orelse 2; + 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 2; + 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 2; default_tty_str = try std.fmt.allocPrint(b.allocator, "{d}", .{default_tty}); @@ -52,6 +54,8 @@ pub fn build(b: *std.Build) !void { build_options.addOption([]const u8, "version", version_str); build_options.addOption(u8, "tty", default_tty); build_options.addOption(u8, "fallback_tty", fallback_tty); + 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); build_options.addOption(bool, "enable_x11_support", enable_x11_support); const target = b.standardTargetOptions(.{}); diff --git a/res/lang/ar.ini b/res/lang/ar.ini index f98c911..a35d449 100644 --- a/res/lang/ar.ini +++ b/res/lang/ar.ini @@ -50,6 +50,7 @@ err_sleep = فشل في تنفيذ أمر sleep err_tty_ctrl = فشل في نقل تحكم الطرفية (TTY) + err_user_gid = فشل في تعيين معرّف المجموعة (GID) للمستخدم err_user_init = فشل في تهيئة بيانات المستخدم err_user_uid = فشل في تعيين معرّف المستخدم (UID) diff --git a/res/lang/cat.ini b/res/lang/cat.ini index a0bd809..f11930e 100644 --- a/res/lang/cat.ini +++ b/res/lang/cat.ini @@ -50,6 +50,7 @@ err_pwnam = error en obtenir la informació de l'usuari + 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 diff --git a/res/lang/cs.ini b/res/lang/cs.ini index a84757b..ee52e2a 100644 --- a/res/lang/cs.ini +++ b/res/lang/cs.ini @@ -50,6 +50,7 @@ err_pwnam = nelze získat informace o uživateli + err_user_gid = nastavení GID uživatele selhalo err_user_init = inicializace uživatele selhala err_user_uid = nastavení UID uživateli selhalo diff --git a/res/lang/de.ini b/res/lang/de.ini index 4f52bea..0715006 100644 --- a/res/lang/de.ini +++ b/res/lang/de.ini @@ -50,6 +50,7 @@ err_sleep = Sleep-Befehl fehlgeschlagen err_tty_ctrl = Fehler bei der TTY-Uebergabe + err_user_gid = Fehler beim Setzen der Gruppen-ID err_user_init = Nutzer-Initialisierung fehlgeschlagen err_user_uid = Setzen der Benutzer-ID fehlgeschlagen diff --git a/res/lang/en.ini b/res/lang/en.ini index 60368ac..6de93fb 100644 --- a/res/lang/en.ini +++ b/res/lang/en.ini @@ -11,7 +11,7 @@ err_brightness_change = failed to change brightness err_chdir = failed to open home folder err_clock_too_long = clock string too long err_config = unable to parse config file - +err_crawl = failed to crawl session directories err_dgn_oob = log message err_domain = invalid domain err_empty_password = empty password not allowed @@ -50,6 +50,7 @@ err_battery = failed to load battery status err_switch_tty = failed to switch tty err_tty_ctrl = tty control transfer failed err_no_users = no users found +err_uid_range = failed to dynamically get uid range err_user_gid = failed to set user GID err_user_init = failed to initialize user err_user_uid = failed to set user UID diff --git a/res/lang/es.ini b/res/lang/es.ini index 62d0513..2fbba01 100644 --- a/res/lang/es.ini +++ b/res/lang/es.ini @@ -50,6 +50,7 @@ err_pwnam = error al obtener la información del usuario + 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 diff --git a/res/lang/fr.ini b/res/lang/fr.ini index cc6801e..046a74d 100644 --- a/res/lang/fr.ini +++ b/res/lang/fr.ini @@ -50,6 +50,7 @@ err_battery = échec de lecture de l'état de la batterie err_switch_tty = échec du changement de terminal err_tty_ctrl = échec du transfert de contrôle du terminal err_no_users = aucun utilisateur trouvé +err_uid_range = échec de récupération dynamique de la plage d'UID err_user_gid = échec de modification du GID err_user_init = échec d'initialisation de l'utilisateur err_user_uid = échec de modification du UID diff --git a/res/lang/it.ini b/res/lang/it.ini index bc69e7c..9eac717 100644 --- a/res/lang/it.ini +++ b/res/lang/it.ini @@ -50,6 +50,7 @@ err_pwnam = impossibile ottenere dati utente + err_user_gid = impossibile impostare GID utente err_user_init = impossibile inizializzare utente err_user_uid = impossible impostare UID utente diff --git a/res/lang/ja_JP.ini b/res/lang/ja_JP.ini index 23c3f84..6cb303e 100644 --- a/res/lang/ja_JP.ini +++ b/res/lang/ja_JP.ini @@ -50,6 +50,7 @@ err_sleep = スリープコマンドの実行に失敗しました err_tty_ctrl = TTY制御の転送に失敗しました + err_user_gid = ユーザーGIDの設定に失敗しました err_user_init = ユーザーの初期化に失敗しました err_user_uid = ユーザーUIDの設定に失敗しました diff --git a/res/lang/lv.ini b/res/lang/lv.ini index fcac1aa..cfaddb5 100644 --- a/res/lang/lv.ini +++ b/res/lang/lv.ini @@ -50,6 +50,7 @@ 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_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 diff --git a/res/lang/pl.ini b/res/lang/pl.ini index 42e2660..13bbc4f 100644 --- a/res/lang/pl.ini +++ b/res/lang/pl.ini @@ -50,6 +50,7 @@ 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_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 diff --git a/res/lang/pt.ini b/res/lang/pt.ini index a0618c9..f33c363 100644 --- a/res/lang/pt.ini +++ b/res/lang/pt.ini @@ -50,6 +50,7 @@ err_pwnam = erro ao obter informação do utilizador + 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 diff --git a/res/lang/pt_BR.ini b/res/lang/pt_BR.ini index 62eae98..bf3c329 100644 --- a/res/lang/pt_BR.ini +++ b/res/lang/pt_BR.ini @@ -50,6 +50,7 @@ err_pwnam = não foi possível obter informações do usuário + 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 diff --git a/res/lang/ro.ini b/res/lang/ro.ini index aa59318..18c35b7 100644 --- a/res/lang/ro.ini +++ b/res/lang/ro.ini @@ -59,6 +59,7 @@ err_perm_user = nu s-a putut face downgrade permisiunilor de utilizator + login = utilizator logout = opreşte sesiunea diff --git a/res/lang/ru.ini b/res/lang/ru.ini index fd5d4ed..ac1c302 100644 --- a/res/lang/ru.ini +++ b/res/lang/ru.ini @@ -50,6 +50,7 @@ err_battery = не удалось получить статус батареи err_switch_tty = не удалось переключить tty err_tty_ctrl = передача управления tty не удалась err_no_users = пользователи не найдены + err_user_gid = не удалось установить GID пользователя err_user_init = не удалось инициализировать пользователя err_user_uid = не удалось установить UID пользователя diff --git a/res/lang/sr.ini b/res/lang/sr.ini index 96b0686..f49cbdf 100644 --- a/res/lang/sr.ini +++ b/res/lang/sr.ini @@ -50,6 +50,7 @@ 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 diff --git a/res/lang/sv.ini b/res/lang/sv.ini index 9439a07..ec9a372 100644 --- a/res/lang/sv.ini +++ b/res/lang/sv.ini @@ -50,6 +50,7 @@ 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 diff --git a/res/lang/tr.ini b/res/lang/tr.ini index d551399..c4e0b1b 100644 --- a/res/lang/tr.ini +++ b/res/lang/tr.ini @@ -50,6 +50,7 @@ err_pwnam = kullanici bilgileri alinamadi + err_user_gid = kullanici icin GID ayarlanamadi err_user_init = kullanici oturumu baslatilamadi err_user_uid = kullanici icin UID ayarlanamadi diff --git a/res/lang/uk.ini b/res/lang/uk.ini index 531f311..cddce01 100644 --- a/res/lang/uk.ini +++ b/res/lang/uk.ini @@ -50,6 +50,7 @@ err_pwnam = не вдалося отримати дані користувача + err_user_gid = не вдалося змінити GID користувача err_user_init = не вдалося ініціалізувати користувача err_user_uid = не вдалося змінити UID користувача diff --git a/res/lang/zh_CN.ini b/res/lang/zh_CN.ini index f73093b..c7d51b9 100644 --- a/res/lang/zh_CN.ini +++ b/res/lang/zh_CN.ini @@ -50,6 +50,7 @@ err_pwnam = 获取用户信息失败 + err_user_gid = 设置用户GID失败 err_user_init = 初始化用户失败 err_user_uid = 设置用户UID失败 diff --git a/src/config/Lang.zig b/src/config/Lang.zig index e116f39..55589f2 100644 --- a/src/config/Lang.zig +++ b/src/config/Lang.zig @@ -55,6 +55,7 @@ err_battery: []const u8 = "failed to load battery status", err_switch_tty: []const u8 = "failed to switch tty", err_tty_ctrl: []const u8 = "tty control transfer failed", err_no_users: []const u8 = "no users found", +err_uid_range: []const u8 = "failed to dynamically get uid range", err_user_gid: []const u8 = "failed to set user GID", err_user_init: []const u8 = "failed to initialize user", err_user_uid: []const u8 = "failed to set user UID", diff --git a/src/main.zig b/src/main.zig index 5fc3545..4b882a9 100644 --- a/src/main.zig +++ b/src/main.zig @@ -26,6 +26,7 @@ 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; @@ -219,7 +220,8 @@ pub fn main() !void { migrator.lateConfigFieldHandler(&config); } - var usernames = try getAllUsernames(allocator, config.login_defs_path); + var maybe_uid_range_error: ?anyerror = null; + var usernames = try getAllUsernames(allocator, config.login_defs_path, &maybe_uid_range_error); defer { for (usernames.items) |username| allocator.free(username); usernames.deinit(allocator); @@ -344,6 +346,11 @@ pub fn main() !void { try log_writer.print("unable to parse argument '{s}{s}': {s}\n", .{ 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)}); + } + 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); @@ -1277,8 +1284,14 @@ fn findSessionByName(session: *Session, name: []const u8) ?usize { return null; } -fn getAllUsernames(allocator: std.mem.Allocator, login_defs_path: []const u8) !StringList { - const uid_range = try interop.getUserIdRange(allocator, login_defs_path); +fn getAllUsernames(allocator: std.mem.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{ + .uid_min = build_options.fallback_uid_min, + .uid_max = build_options.fallback_uid_max, + }; + }; var usernames: StringList = .empty; var maybe_entry = interop.getNextUsernameEntry(); From a94abf2e69413ce5952bc34d4af3a57664f16bac Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Sun, 30 Nov 2025 12:08:10 +0100 Subject: [PATCH 336/530] Improve bug report template & add PR template Signed-off-by: AnErrupTion --- .github/ISSUE_TEMPLATE/bug.yml | 2 ++ .github/pull_request_template.md | 11 +++++++++++ 2 files changed, 13 insertions(+) create mode 100644 .github/pull_request_template.md diff --git a/.github/ISSUE_TEMPLATE/bug.yml b/.github/ISSUE_TEMPLATE/bug.yml index 101a459..d40aaf3 100644 --- a/.github/ISSUE_TEMPLATE/bug.yml +++ b/.github/ISSUE_TEMPLATE/bug.yml @@ -11,6 +11,8 @@ 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 + required: true - type: input id: version attributes: diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..0cc851f --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,11 @@ +## What are the changes about? + +_Replace this with a brief description of your changes_ + +## What existing issue does this resolve? + +_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 From 5235ca47c5ac783c72a875220c0881487c310195 Mon Sep 17 00:00:00 2001 From: RacerBG Date: Sun, 30 Nov 2025 21:52:46 +0100 Subject: [PATCH 337/530] Add Bulgarian Translation (#872) Reviewed-on: https://codeberg.org/fairyglade/ly/pulls/872 Reviewed-by: AnErrupTion Co-authored-by: RacerBG Co-committed-by: RacerBG --- res/lang/bg.ini | 76 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 res/lang/bg.ini diff --git a/res/lang/bg.ini b/res/lang/bg.ini new file mode 100644 index 0000000..06cca36 --- /dev/null +++ b/res/lang/bg.ini @@ -0,0 +1,76 @@ +authenticating = удостоверяване... +brightness_down = намаляване на яркостта +brightness_up = увеличаване на яркостта +capslock = caps lock +custom = персонализирано +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_lock_state = неуспешно получаване на състоянието на заключване +err_log = неуспешно отваряне на файла с дневника +err_mlock = неуспешно заключване на паметта за паролата +err_null = нулев указател +err_numlock = неуспешно задаване на num lock +err_pam = неуспешна транзакция +err_pam_abort = прекратена транзакция +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_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 = num lock +other = друго +password = парола +restart = рестартиране +shell = обвивка +shutdown = изключване +sleep = заспиване +wayland = wayland +x11 = x11 +xinitrc = xinitrc From f9a001b160eba8518aa6bbf43f175a0681bd0ce6 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Sun, 30 Nov 2025 21:57:42 +0100 Subject: [PATCH 338/530] Update zigini dependency & use same Git URL format everywhere Signed-off-by: AnErrupTion --- build.zig.zon | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/build.zig.zon b/build.zig.zon index bd8e8a7..8da9e91 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -5,12 +5,12 @@ .minimum_zig_version = "0.15.0", .dependencies = .{ .clap = .{ - .url = "https://github.com/Hejsil/zig-clap/archive/refs/tags/0.11.0.tar.gz", + .url = "git+https://github.com/Hejsil/zig-clap?ref=0.11.0", .hash = "clap-0.11.0-oBajB-HnAQDPCKYzwF7rO3qDFwRcD39Q0DALlTSz5H7e", }, .zigini = .{ - .url = "https://github.com/AnErrupTion/zigini/archive/96ca1d9f1a7ec741f07ceb104dae2b3a7bdfd48a.tar.gz", - .hash = "zigini-0.3.2-BSkB7WJJAADybd5DGd9MLCp6ikGGUq9wicxsjv0HF1Qc", + .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#290ac6b8225aacfd16851224682b851b65fcb918", From 4df2382698e21eb67da609ceecc2c9dccf69b7f4 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Sun, 30 Nov 2025 22:19:38 +0100 Subject: [PATCH 339/530] Add possibility to disable auth_fails animation (closes #835) Signed-off-by: AnErrupTion --- res/config.ini | 5 +++-- src/main.zig | 6 +++--- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/res/config.ini b/res/config.ini index 1757303..2e3579e 100644 --- a/res/config.ini +++ b/res/config.ini @@ -39,6 +39,7 @@ animation_timeout_sec = 0 asterisk = * # The number of failed authentications before a special animation is played... ;) +# If set to 0, the animation will never be played auth_fails = 10 # Identifier for battery whose charge to display at top left @@ -162,7 +163,7 @@ doom_middle_color = 0x00C78F17 # DOOM animation custom bottom color (high intensity flames) doom_bottom_color = 0x00FFFFFF -# Set margin to the edges of the DM (useful for curved monitors) +# Set margin to the edges of the DM (useful for curved monitors) edge_margin = 0 # Error background color id @@ -213,7 +214,7 @@ gameoflife_frame_delay = 6 gameoflife_initial_density = 0.4 # Command executed when pressing hibernate key (can be null) -hibernate_cmd = null +hibernate_cmd = null # Specifies the key used for hibernate (F1-F12) hibernate_key = F4 diff --git a/src/main.zig b/src/main.zig index 4b882a9..dffd8fe 100644 --- a/src/main.zig +++ b/src/main.zig @@ -629,7 +629,7 @@ pub fn main() !void { if (update) { // If the user entered a wrong password 10 times in a row, play a cascade animation, else update normally - if (auth_fails >= config.auth_fails) { + if (config.auth_fails > 0 and auth_fails >= config.auth_fails) { std.Thread.sleep(std.time.ns_per_ms * 10); update = buffer.cascade(); @@ -847,7 +847,7 @@ pub fn main() !void { const time = try interop.getTimeOfDay(); timeout = @intCast((60 - @rem(time.seconds, 60)) * 1000 - @divTrunc(time.microseconds, 1000) + 1); - } else if (config.clock != null or auth_fails >= config.auth_fails) { + } else if (config.clock != null or (config.auth_fails > 0 and auth_fails >= config.auth_fails)) { const time = try interop.getTimeOfDay(); timeout = @intCast(1000 - @divTrunc(time.microseconds, 1000) + 1); @@ -1110,7 +1110,7 @@ pub fn main() !void { try std.posix.tcsetattr(std.posix.STDIN_FILENO, .FLUSH, tb_termios); - if (auth_fails < config.auth_fails) { + if (config.auth_fails == 0 or auth_fails < config.auth_fails) { _ = termbox.tb_clear(); try ttyClearScreen(); From e29bda3250010b41e23726b80847003ad41d3561 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Sun, 30 Nov 2025 23:02:23 +0100 Subject: [PATCH 340/530] Add systemd-homed UID range Signed-off-by: AnErrupTion --- src/interop.zig | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/src/interop.zig b/src/interop.zig index af7a571..2f8483a 100644 --- a/src/interop.zig +++ b/src/interop.zig @@ -81,6 +81,9 @@ 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; @@ -179,6 +182,19 @@ fn PlatformStruct() type { } } + // 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; } @@ -226,6 +242,9 @@ fn PlatformStruct() type { pub const vt_activate = consio.VT_ACTIVATE; pub const vt_waitactive = consio.VT_WAITACTIVE; + const FREEBSD_UID_MIN = 1000; + const FREEBSD_UID_MAX = 32000; + pub fn setUserContextImpl(username: [*:0]const u8, entry: UsernameEntry) !void { // FreeBSD has initgroups() in unistd const status = unistd.initgroups(username, @intCast(entry.gid)); @@ -244,8 +263,8 @@ fn PlatformStruct() type { return .{ // Hardcoded default values chosen from // /usr/src/usr.sbin/pw/pw_conf.c - .uid_min = 1000, - .uid_max = 32000, + .uid_min = FREEBSD_UID_MIN, + .uid_max = FREEBSD_UID_MAX, }; } }, From c2b3d794e88c96f19a12cc7b3c7a960beb355294 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Sun, 30 Nov 2025 23:36:47 +0100 Subject: [PATCH 341/530] Fix fallback UID range + add error if UID range not found Signed-off-by: AnErrupTion --- build.zig | 4 ++-- src/interop.zig | 5 +++++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/build.zig b/build.zig index fe73229..12ababb 100644 --- a/build.zig +++ b/build.zig @@ -44,8 +44,8 @@ pub fn build(b: *std.Build) !void { const enable_x11_support = b.option(bool, "enable_x11_support", "Enable X11 support (default is on)") orelse true; const default_tty = b.option(u8, "default_tty", "Set the TTY (default is 2)") orelse 2; const fallback_tty = b.option(u8, "fallback_tty", "Set the fallback TTY (default is 2). This value gets embedded into the binary") orelse 2; - 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 2; - 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 2; + 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}); diff --git a/src/interop.zig b/src/interop.zig index 2f8483a..4d52f49 100644 --- a/src/interop.zig +++ b/src/interop.zig @@ -171,17 +171,22 @@ fn PlatformStruct() type { var iterator = std.mem.splitScalar(u8, login_defs_buffer, '\n'); var uid_range = UidRange{}; + var nameFound = 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; } 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; } } + 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. From d82fa82a878e1b6973a508e6a2d2f042d1bf0ba2 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Sun, 30 Nov 2025 23:50:39 +0100 Subject: [PATCH 342/530] Always add hostname last in the info line Signed-off-by: AnErrupTion --- src/main.zig | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/src/main.zig b/src/main.zig index dffd8fe..276cb0b 100644 --- a/src/main.zig +++ b/src/main.zig @@ -405,19 +405,6 @@ pub fn main() !void { try log_writer.writeAll("x11 support disabled at compile-time\n"); } - if (config.initial_info_text) |text| { - try info_line.addMessage(text, config.bg, 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_writer.print("failed to get hostname: {s}\n", .{@errorName(err)}); - break :get_host_name; - }; - try info_line.addMessage(hostname, config.bg, config.fg); - } - var has_crawl_error = false; // Crawl session directories (Wayland, X11 and custom respectively) @@ -600,6 +587,19 @@ pub fn main() !void { try log_writer.print("failed to switch tty: {s}\n", .{@errorName(err)}); }; + if (config.initial_info_text) |text| { + try info_line.addMessage(text, config.bg, 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_writer.print("failed to get hostname: {s}\n", .{@errorName(err)}); + break :get_host_name; + }; + try info_line.addMessage(hostname, config.bg, config.fg); + } + while (run) { // If there's no input or there's an animation, a resolution change needs to be checked if (!update or animate) { From 6cb53b6e3867c56f5171f43238a94b5c348456de Mon Sep 17 00:00:00 2001 From: radsammyt Date: Mon, 1 Dec 2025 20:07:59 +0100 Subject: [PATCH 343/530] Refactor active_input field-jumping logic (#873) Reviewed-on: https://codeberg.org/fairyglade/ly/pulls/873 Reviewed-by: AnErrupTion Co-authored-by: radsammyt Co-committed-by: radsammyt --- src/enums.zig | 21 +++++++++++++++++++++ src/main.zig | 38 ++++++-------------------------------- 2 files changed, 27 insertions(+), 32 deletions(-) diff --git a/src/enums.zig b/src/enums.zig index 82c478d..07dc3eb 100644 --- a/src/enums.zig +++ b/src/enums.zig @@ -1,3 +1,4 @@ +const std = @import("std"); pub const Animation = enum { none, doom, @@ -19,6 +20,26 @@ pub const Input = enum { session, login, password, + + /// Moves the current Input forwards by one entry. If `reverse`, then the Input + /// moves backwards. If `wrap` is true, then the entry will wrap back around + pub fn move(self: *Input, reverse: bool, wrap: bool) void { + const maxNum = @typeInfo(Input).@"enum".fields.len - 1; + const selfNum = @intFromEnum(self.*); + if (reverse) { + if (wrap) { + self.* = @enumFromInt(selfNum -% 1); + } else if (selfNum != 0) { + self.* = @enumFromInt(selfNum - 1); + } + } else { + if (wrap) { + self.* = @enumFromInt(selfNum +% 1); + } else if (selfNum != maxNum) { + self.* = @enumFromInt(selfNum + 1); + } + } + } }; pub const ViMode = enum { diff --git a/src/main.zig b/src/main.zig index 276cb0b..935cf1d 100644 --- a/src/main.zig +++ b/src/main.zig @@ -938,37 +938,19 @@ pub fn main() !void { update = true; }, termbox.TB_KEY_CTRL_K, termbox.TB_KEY_ARROW_UP => { - active_input = switch (active_input) { - .session, .info_line => .info_line, - .login => .session, - .password => .login, - }; + active_input.move(true, false); update = true; }, termbox.TB_KEY_CTRL_J, termbox.TB_KEY_ARROW_DOWN => { - active_input = switch (active_input) { - .info_line => .session, - .session => .login, - .login, .password => .password, - }; + active_input.move(false, false); update = true; }, termbox.TB_KEY_TAB => { - active_input = switch (active_input) { - .info_line => .session, - .session => .login, - .login => .password, - .password => .info_line, - }; + active_input.move(false, true); update = true; }, termbox.TB_KEY_BACK_TAB => { - active_input = switch (active_input) { - .info_line => .password, - .session => .info_line, - .login => .session, - .password => .login, - }; + active_input.move(true, true); update = true; }, termbox.TB_KEY_ENTER => authenticate: { @@ -1125,20 +1107,12 @@ pub fn main() !void { if (!insert_mode) { switch (event.ch) { 'k' => { - active_input = switch (active_input) { - .session, .info_line => .info_line, - .login => .session, - .password => .login, - }; + active_input.move(true, false); update = true; continue; }, 'j' => { - active_input = switch (active_input) { - .info_line => .session, - .session => .login, - .login, .password => .password, - }; + active_input.move(false, false); update = true; continue; }, From 3365b33d6d92db63d0b8132dcfc8503e08d5cdae Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Mon, 1 Dec 2025 22:09:41 +0100 Subject: [PATCH 344/530] Fix zig-clap dependency URL Signed-off-by: AnErrupTion --- build.zig.zon | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.zig.zon b/build.zig.zon index 8da9e91..3c6a247 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -5,7 +5,7 @@ .minimum_zig_version = "0.15.0", .dependencies = .{ .clap = .{ - .url = "git+https://github.com/Hejsil/zig-clap?ref=0.11.0", + .url = "git+https://github.com/Hejsil/zig-clap#5289e0753cd274d65344bef1c114284c633536ea", .hash = "clap-0.11.0-oBajB-HnAQDPCKYzwF7rO3qDFwRcD39Q0DALlTSz5H7e", }, .zigini = .{ From b249dba0924ad439ea81f14242e4ea018f2aa511 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Tue, 2 Dec 2025 22:11:39 +0100 Subject: [PATCH 345/530] Support multiple TTYs with systemd service (closes #102) Signed-off-by: AnErrupTion --- build.zig | 6 +++--- readme.md | 8 ++++---- res/{ly.service => ly@.service} | 8 ++++---- 3 files changed, 11 insertions(+), 11 deletions(-) rename res/{ly.service => ly@.service} (62%) diff --git a/build.zig b/build.zig index 12ababb..d7dd3b4 100644 --- a/build.zig +++ b/build.zig @@ -260,8 +260,8 @@ fn install_service(allocator: std.mem.Allocator, patch_map: PatchMap) !void { var service_dir = std.fs.cwd().openDir(service_path, .{}) catch unreachable; defer service_dir.close(); - 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, "res/ly@.service", patch_map); + try installText(patched_service, service_dir, service_path, "ly@.service", .{ .mode = 0o644 }); }, .openrc => { const service_path = try std.fs.path.join(allocator, &[_][]const u8{ dest_directory, config_directory, "/init.d" }); @@ -365,7 +365,7 @@ pub fn Uninstaller(uninstall_config: bool) type { try deleteFile(allocator, 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"), + .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"), .s6 => { diff --git a/readme.md b/readme.md index 8e9b5b2..e0308e3 100644 --- a/readme.md +++ b/readme.md @@ -111,19 +111,19 @@ command: Then, similarly to the previous command, you need to enable the Ly service: ``` -# systemctl enable 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 (the default TTY on which Ly spawns), you need to -execute the following command: +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 ``` You can change the TTY Ly will run on by editing the corresponding -service file for your platform. +service file for your platform, or on systemd, by enabling the service on +different TTYs, as is done above. ### OpenRC diff --git a/res/ly.service b/res/ly@.service similarity index 62% rename from res/ly.service rename to res/ly@.service index 0b72699..21d20df 100644 --- a/res/ly.service +++ b/res/ly@.service @@ -1,16 +1,16 @@ [Unit] Description=TUI display manager After=systemd-user-sessions.service plymouth-quit-wait.service -After=getty@tty$DEFAULT_TTY.service -Conflicts=getty@tty$DEFAULT_TTY.service +After=getty@%I.service +Conflicts=getty@%I.service [Service] Type=idle ExecStart=$PREFIX_DIRECTORY/bin/$EXECUTABLE_NAME StandardInput=tty -TTYPath=/dev/tty$DEFAULT_TTY +TTYPath=/dev/%I TTYReset=yes TTYVHangup=yes [Install] -Alias=display-manager.service +WantedBy=multi-user.target From 1c99574f7381db3c4eb41fa1d9fd59a50a1222cb Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Tue, 2 Dec 2025 22:23:10 +0100 Subject: [PATCH 346/530] Add option to run command before UI is initialised (closes #798) Signed-off-by: AnErrupTion --- res/config.ini | 4 ++++ 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/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 | 1 + res/lang/tr.ini | 1 + res/lang/uk.ini | 1 + res/lang/zh_CN.ini | 1 + src/config/Config.zig | 1 + src/config/Lang.zig | 1 + src/main.zig | 19 +++++++++++++++++++ 25 files changed, 46 insertions(+) diff --git a/res/config.ini b/res/config.ini index 2e3579e..2e015c1 100644 --- a/res/config.ini +++ b/res/config.ini @@ -312,6 +312,10 @@ sleep_cmd = null # Specifies the key used for sleep (F1-F12) 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 + # Center the session name. text_in_center = false diff --git a/res/lang/ar.ini b/res/lang/ar.ini index a35d449..0d733c5 100644 --- a/res/lang/ar.ini +++ b/res/lang/ar.ini @@ -48,6 +48,7 @@ err_pwnam = فشل في جلب معلومات المستخدم err_sleep = فشل في تنفيذ أمر sleep + err_tty_ctrl = فشل في نقل تحكم الطرفية (TTY) diff --git a/res/lang/bg.ini b/res/lang/bg.ini index 06cca36..984d44d 100644 --- a/res/lang/bg.ini +++ b/res/lang/bg.ini @@ -46,6 +46,7 @@ err_perm_group = неуспешно понижаване на правата н err_perm_user = неуспешно понижаване на правата на потребителя err_pwnam = неуспешно получаване на информация за потребителя err_sleep = неуспешно изпълнение на командата за заспиване + err_battery = неуспешно зареждане на състоянието на батерията err_switch_tty = неуспешна смяна на TTY err_tty_ctrl = неуспешно прехвърляне на контрола над TTY diff --git a/res/lang/cat.ini b/res/lang/cat.ini index f11930e..526e858 100644 --- a/res/lang/cat.ini +++ b/res/lang/cat.ini @@ -51,6 +51,7 @@ err_pwnam = error en obtenir la informació de l'usuari + 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 diff --git a/res/lang/cs.ini b/res/lang/cs.ini index ee52e2a..b42a52c 100644 --- a/res/lang/cs.ini +++ b/res/lang/cs.ini @@ -51,6 +51,7 @@ err_pwnam = nelze získat informace o uživateli + err_user_gid = nastavení GID uživatele selhalo err_user_init = inicializace uživatele selhala err_user_uid = nastavení UID uživateli selhalo diff --git a/res/lang/de.ini b/res/lang/de.ini index 0715006..02034b9 100644 --- a/res/lang/de.ini +++ b/res/lang/de.ini @@ -48,6 +48,7 @@ err_pwnam = Abrufen der Benutzerinformationen fehlgeschlagen err_sleep = Sleep-Befehl fehlgeschlagen + err_tty_ctrl = Fehler bei der TTY-Uebergabe diff --git a/res/lang/en.ini b/res/lang/en.ini index 6de93fb..38ad8f5 100644 --- a/res/lang/en.ini +++ b/res/lang/en.ini @@ -46,6 +46,7 @@ err_perm_group = failed to downgrade group permissions err_perm_user = failed to downgrade user permissions err_pwnam = failed to get user info err_sleep = failed to execute sleep command +err_start = failed to execute start command err_battery = failed to load battery status err_switch_tty = failed to switch tty err_tty_ctrl = tty control transfer failed diff --git a/res/lang/es.ini b/res/lang/es.ini index 2fbba01..e14aea9 100644 --- a/res/lang/es.ini +++ b/res/lang/es.ini @@ -51,6 +51,7 @@ err_pwnam = error al obtener la información del usuario + 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 diff --git a/res/lang/fr.ini b/res/lang/fr.ini index 046a74d..8236184 100644 --- a/res/lang/fr.ini +++ b/res/lang/fr.ini @@ -46,6 +46,7 @@ err_perm_group = échec du déclassement des permissions de groupe err_perm_user = échec du déclassement des permissions utilisateur err_pwnam = échec de lecture des infos utilisateur err_sleep = échec de l'exécution de la commande de veille +err_start = échec de l'exécution de la commande de démarrage err_battery = échec de lecture de l'état de la batterie err_switch_tty = échec du changement de terminal err_tty_ctrl = échec du transfert de contrôle du terminal diff --git a/res/lang/it.ini b/res/lang/it.ini index 9eac717..7c90a8f 100644 --- a/res/lang/it.ini +++ b/res/lang/it.ini @@ -51,6 +51,7 @@ err_pwnam = impossibile ottenere dati utente + err_user_gid = impossibile impostare GID utente err_user_init = impossibile inizializzare utente err_user_uid = impossible impostare UID utente diff --git a/res/lang/ja_JP.ini b/res/lang/ja_JP.ini index 6cb303e..cb299df 100644 --- a/res/lang/ja_JP.ini +++ b/res/lang/ja_JP.ini @@ -48,6 +48,7 @@ err_pwnam = ユーザー情報の取得に失敗しました err_sleep = スリープコマンドの実行に失敗しました + err_tty_ctrl = TTY制御の転送に失敗しました diff --git a/res/lang/lv.ini b/res/lang/lv.ini index cfaddb5..9b6f6cf 100644 --- a/res/lang/lv.ini +++ b/res/lang/lv.ini @@ -46,6 +46,7 @@ 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_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 diff --git a/res/lang/pl.ini b/res/lang/pl.ini index 13bbc4f..40c6895 100644 --- a/res/lang/pl.ini +++ b/res/lang/pl.ini @@ -46,6 +46,7 @@ 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_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 diff --git a/res/lang/pt.ini b/res/lang/pt.ini index f33c363..f0cebd8 100644 --- a/res/lang/pt.ini +++ b/res/lang/pt.ini @@ -51,6 +51,7 @@ err_pwnam = erro ao obter informação do utilizador + 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 diff --git a/res/lang/pt_BR.ini b/res/lang/pt_BR.ini index bf3c329..a8eb90f 100644 --- a/res/lang/pt_BR.ini +++ b/res/lang/pt_BR.ini @@ -51,6 +51,7 @@ err_pwnam = não foi possível obter informações do usuário + 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 diff --git a/res/lang/ro.ini b/res/lang/ro.ini index 18c35b7..8e5c6cf 100644 --- a/res/lang/ro.ini +++ b/res/lang/ro.ini @@ -60,6 +60,7 @@ err_perm_user = nu s-a putut face downgrade permisiunilor de utilizator + login = utilizator logout = opreşte sesiunea diff --git a/res/lang/ru.ini b/res/lang/ru.ini index ac1c302..26e9fbf 100644 --- a/res/lang/ru.ini +++ b/res/lang/ru.ini @@ -46,6 +46,7 @@ err_perm_group = не удалось понизить права доступа err_perm_user = не удалось понизить права доступа пользователя err_pwnam = не удалось получить информацию о пользователе err_sleep = не удалось выполнить команду sleep + err_battery = не удалось получить статус батареи err_switch_tty = не удалось переключить tty err_tty_ctrl = передача управления tty не удалась diff --git a/res/lang/sr.ini b/res/lang/sr.ini index f49cbdf..5c9cb64 100644 --- a/res/lang/sr.ini +++ b/res/lang/sr.ini @@ -51,6 +51,7 @@ 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 diff --git a/res/lang/sv.ini b/res/lang/sv.ini index ec9a372..de329f1 100644 --- a/res/lang/sv.ini +++ b/res/lang/sv.ini @@ -51,6 +51,7 @@ 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 diff --git a/res/lang/tr.ini b/res/lang/tr.ini index c4e0b1b..0bf1ffe 100644 --- a/res/lang/tr.ini +++ b/res/lang/tr.ini @@ -51,6 +51,7 @@ err_pwnam = kullanici bilgileri alinamadi + err_user_gid = kullanici icin GID ayarlanamadi err_user_init = kullanici oturumu baslatilamadi err_user_uid = kullanici icin UID ayarlanamadi diff --git a/res/lang/uk.ini b/res/lang/uk.ini index cddce01..36e4ce3 100644 --- a/res/lang/uk.ini +++ b/res/lang/uk.ini @@ -51,6 +51,7 @@ err_pwnam = не вдалося отримати дані користувача + err_user_gid = не вдалося змінити GID користувача err_user_init = не вдалося ініціалізувати користувача err_user_uid = не вдалося змінити UID користувача diff --git a/res/lang/zh_CN.ini b/res/lang/zh_CN.ini index c7d51b9..fd7b4ea 100644 --- a/res/lang/zh_CN.ini +++ b/res/lang/zh_CN.ini @@ -51,6 +51,7 @@ err_pwnam = 获取用户信息失败 + err_user_gid = 设置用户GID失败 err_user_init = 初始化用户失败 err_user_uid = 设置用户UID失败 diff --git a/src/config/Config.zig b/src/config/Config.zig index 5ddfe80..a952be8 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -79,6 +79,7 @@ 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, vi_default_mode: ViMode = .normal, vi_mode: bool = false, diff --git a/src/config/Lang.zig b/src/config/Lang.zig index 55589f2..4134933 100644 --- a/src/config/Lang.zig +++ b/src/config/Lang.zig @@ -51,6 +51,7 @@ err_perm_group: []const u8 = "failed to downgrade group permissions", err_perm_user: []const u8 = "failed to downgrade user permissions", err_pwnam: []const u8 = "failed to get user info", err_sleep: []const u8 = "failed to execute sleep command", +err_start: []const u8 = "failed to execute start command", err_battery: []const u8 = "failed to load battery status", err_switch_tty: []const u8 = "failed to switch tty", err_tty_ctrl: []const u8 = "tty control transfer failed", diff --git a/src/main.zig b/src/main.zig index 935cf1d..7addd74 100644 --- a/src/main.zig +++ b/src/main.zig @@ -124,6 +124,7 @@ pub fn main() !void { var lang: Lang = undefined; 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; @@ -282,6 +283,19 @@ pub fn main() !void { restart_cmd = try temporary_allocator.dupe(u8, config.restart_cmd); commands_allocated = true; + if (config.start_cmd) |start_cmd| { + var sleep = std.process.Child.init(&[_][]const u8{ "/bin/sh", "-c", start_cmd }, allocator); + sleep.stdout_behavior = .Ignore; + sleep.stderr_behavior = .Ignore; + + handle_start_cmd: { + const process_result = sleep.spawnAndWait() catch { + break :handle_start_cmd; + }; + start_cmd_exit_code = process_result.Exited; + } + } + // Initialize termbox try log_writer.writeAll("initializing termbox2\n"); _ = termbox.tb_init(); @@ -351,6 +365,11 @@ pub fn main() !void { try log_writer.print("failed to get uid range: {s}; falling back to default\n", .{@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}); + } + 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); From 6cce221cbfddd94935f5bc414931a70aebb530ae Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Tue, 2 Dec 2025 22:37:03 +0100 Subject: [PATCH 347/530] Improve issue template Signed-off-by: AnErrupTion --- .github/ISSUE_TEMPLATE/bug.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/ISSUE_TEMPLATE/bug.yml b/.github/ISSUE_TEMPLATE/bug.yml index d40aaf3..909ba6c 100644 --- a/.github/ISSUE_TEMPLATE/bug.yml +++ b/.github/ISSUE_TEMPLATE/bug.yml @@ -13,6 +13,8 @@ body: 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: true - type: input id: version attributes: From 3bfdc75a70b0dadf3b1f4d067ae4ff830cedcc08 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Wed, 3 Dec 2025 20:39:27 +0100 Subject: [PATCH 348/530] Add option to run command after inactivity delay (closes #747) Signed-off-by: AnErrupTion --- res/config.ini | 7 +++++++ 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/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 | 1 + res/lang/tr.ini | 1 + res/lang/uk.ini | 1 + res/lang/zh_CN.ini | 1 + src/config/Config.zig | 2 ++ src/config/Lang.zig | 1 + src/main.zig | 39 +++++++++++++++++++++++++++++++++------ 25 files changed, 64 insertions(+), 6 deletions(-) diff --git a/res/config.ini b/res/config.ini index 2e015c1..f56bcc6 100644 --- a/res/config.ini +++ b/res/config.ini @@ -231,6 +231,13 @@ 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 + +# Executes a command after a certain amount of seconds +inactivity_delay = 0 + # Initial text to show on the info line # If set to null, the info line defaults to the hostname initial_info_text = null diff --git a/res/lang/ar.ini b/res/lang/ar.ini index 0d733c5..6971326 100644 --- a/res/lang/ar.ini +++ b/res/lang/ar.ini @@ -21,6 +21,7 @@ err_envlist = فشل في جلب قائمة المتغيرات البيئية err_hostname = فشل في جلب اسم المضيف (Hostname) + err_mlock = فشل في تأمين ذاكرة كلمة المرور (mlock) err_null = مؤشر فارغ (Null pointer) err_numlock = فشل في ضبط Num Lock diff --git a/res/lang/bg.ini b/res/lang/bg.ini index 984d44d..aa24e02 100644 --- a/res/lang/bg.ini +++ b/res/lang/bg.ini @@ -19,6 +19,7 @@ err_envlist = неуспешно получаване на списъка с п err_get_active_tty = неуспешно откриване на активния TTY err_hibernate = неуспешно изпълнение на командата за хибернация err_hostname = неуспешно получаване на името на хоста + err_lock_state = неуспешно получаване на състоянието на заключване err_log = неуспешно отваряне на файла с дневника err_mlock = неуспешно заключване на паметта за паролата diff --git a/res/lang/cat.ini b/res/lang/cat.ini index 526e858..85dbe53 100644 --- a/res/lang/cat.ini +++ b/res/lang/cat.ini @@ -21,6 +21,7 @@ err_envlist = error en obtenir l'envlist err_hostname = error en obtenir el nom de l'amfitrió + err_mlock = error en bloquejar la memòria de clau err_null = punter nul err_numlock = error en establir el Bloq num diff --git a/res/lang/cs.ini b/res/lang/cs.ini index b42a52c..ff6943e 100644 --- a/res/lang/cs.ini +++ b/res/lang/cs.ini @@ -21,6 +21,7 @@ err_domain = neplatná doména err_hostname = nelze získat název hostitele + err_mlock = uzamčení paměti hesel selhalo err_null = nulový ukazatel diff --git a/res/lang/de.ini b/res/lang/de.ini index 02034b9..60ca3bd 100644 --- a/res/lang/de.ini +++ b/res/lang/de.ini @@ -21,6 +21,7 @@ err_envlist = Fehler beim Abrufen der Umgebungs-Variablen err_hostname = Abrufen des Hostnames fehlgeschlagen + err_mlock = Sperren des Passwortspeichers fehlgeschlagen err_null = Null Pointer err_numlock = Numlock konnte nicht aktiviert werden diff --git a/res/lang/en.ini b/res/lang/en.ini index 38ad8f5..b840816 100644 --- a/res/lang/en.ini +++ b/res/lang/en.ini @@ -19,6 +19,7 @@ err_envlist = failed to get envlist err_get_active_tty = failed to get active tty err_hibernate = failed to execute hibernate command err_hostname = failed to get hostname +err_inactivity = failed to execute inactivity command err_lock_state = failed to get lock state err_log = failed to open log file err_mlock = failed to lock password memory diff --git a/res/lang/es.ini b/res/lang/es.ini index e14aea9..fd3450a 100644 --- a/res/lang/es.ini +++ b/res/lang/es.ini @@ -21,6 +21,7 @@ err_domain = dominio inválido err_hostname = error al obtener el nombre de host + err_mlock = error al bloquear la contraseña de memoria err_null = puntero nulo diff --git a/res/lang/fr.ini b/res/lang/fr.ini index 8236184..17258de 100644 --- a/res/lang/fr.ini +++ b/res/lang/fr.ini @@ -19,6 +19,7 @@ err_envlist = échec de lecture de la liste d'environnement err_get_active_tty = échec de lecture du terminal actif err_hibernate = échec de l'exécution de la commande de veille prolongée err_hostname = échec de lecture du nom d'hôte +err_inactivity = échec de l'exécution de la commande d'inactivité err_lock_state = échec de lecture de l'état de verrouillage err_log = échec de l'ouverture du fichier de journal err_mlock = échec du verrouillage mémoire diff --git a/res/lang/it.ini b/res/lang/it.ini index 7c90a8f..d909609 100644 --- a/res/lang/it.ini +++ b/res/lang/it.ini @@ -21,6 +21,7 @@ err_domain = dominio non valido err_hostname = impossibile ottenere hostname + err_mlock = impossibile ottenere lock per la password in memoria err_null = puntatore nullo diff --git a/res/lang/ja_JP.ini b/res/lang/ja_JP.ini index cb299df..f77abd3 100644 --- a/res/lang/ja_JP.ini +++ b/res/lang/ja_JP.ini @@ -21,6 +21,7 @@ err_envlist = 環境変数リストの取得に失敗しました err_hostname = ホスト名の取得に失敗しました + err_mlock = パスワードメモリのロックに失敗しました err_null = ヌルポインタ err_numlock = NumLockの設定に失敗しました diff --git a/res/lang/lv.ini b/res/lang/lv.ini index 9b6f6cf..f572a99 100644 --- a/res/lang/lv.ini +++ b/res/lang/lv.ini @@ -19,6 +19,7 @@ err_envlist = neizdevās iegūt vides mainīgo sarakstu err_get_active_tty = neizdevās iegūt aktīvo tty err_hostname = neizdevās iegūt hostname + 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 diff --git a/res/lang/pl.ini b/res/lang/pl.ini index 40c6895..aadc9ff 100644 --- a/res/lang/pl.ini +++ b/res/lang/pl.ini @@ -19,6 +19,7 @@ err_envlist = nie udało się pobrać listy zmiennych środowiskowych err_get_active_tty = nie udało się uzyskać aktywnego tty err_hostname = nie udało się uzyskać nazwy hosta + 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ł diff --git a/res/lang/pt.ini b/res/lang/pt.ini index f0cebd8..16d25a3 100644 --- a/res/lang/pt.ini +++ b/res/lang/pt.ini @@ -21,6 +21,7 @@ err_domain = domínio inválido err_hostname = erro ao obter o nome do host + err_mlock = erro de bloqueio de memória err_null = ponteiro nulo diff --git a/res/lang/pt_BR.ini b/res/lang/pt_BR.ini index a8eb90f..f8d0e26 100644 --- a/res/lang/pt_BR.ini +++ b/res/lang/pt_BR.ini @@ -21,6 +21,7 @@ err_domain = domínio inválido err_hostname = não foi possível obter o nome do host + err_mlock = bloqueio da memória de senha malsucedido err_null = ponteiro nulo diff --git a/res/lang/ro.ini b/res/lang/ro.ini index 8e5c6cf..9dfd75a 100644 --- a/res/lang/ro.ini +++ b/res/lang/ro.ini @@ -23,6 +23,7 @@ capslock = capslock + err_pam_abort = tranzacţie pam anulată diff --git a/res/lang/ru.ini b/res/lang/ru.ini index 26e9fbf..e47a41e 100644 --- a/res/lang/ru.ini +++ b/res/lang/ru.ini @@ -19,6 +19,7 @@ err_envlist = не удалось получить список переменн err_get_active_tty = не удалось получить активный tty err_hostname = не удалось получить имя хоста + err_lock_state = не удалось получить состояние lock err_log = не удалось открыть файл log err_mlock = сбой блокировки памяти diff --git a/res/lang/sr.ini b/res/lang/sr.ini index 5c9cb64..71c7ea1 100644 --- a/res/lang/sr.ini +++ b/res/lang/sr.ini @@ -21,6 +21,7 @@ err_domain = nevazeci domen err_hostname = neuspijesno trazenje hostname-a + err_mlock = neuspijesno zakljucavanje memorije lozinke err_null = null pokazivac diff --git a/res/lang/sv.ini b/res/lang/sv.ini index de329f1..661eb62 100644 --- a/res/lang/sv.ini +++ b/res/lang/sv.ini @@ -21,6 +21,7 @@ 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 diff --git a/res/lang/tr.ini b/res/lang/tr.ini index 0bf1ffe..e351076 100644 --- a/res/lang/tr.ini +++ b/res/lang/tr.ini @@ -21,6 +21,7 @@ err_domain = gecersiz etki alani err_hostname = ana bilgisayar adi alinamadi + err_mlock = parola bellegi kilitlenemedi err_null = bos isaretci hatasi diff --git a/res/lang/uk.ini b/res/lang/uk.ini index 36e4ce3..f1dcf54 100644 --- a/res/lang/uk.ini +++ b/res/lang/uk.ini @@ -21,6 +21,7 @@ err_domain = недійсний домен err_hostname = не вдалося отримати ім'я хосту + err_mlock = збій блокування пам'яті err_null = нульовий вказівник diff --git a/res/lang/zh_CN.ini b/res/lang/zh_CN.ini index fd7b4ea..493ceac 100644 --- a/res/lang/zh_CN.ini +++ b/res/lang/zh_CN.ini @@ -21,6 +21,7 @@ err_domain = 无效的域 err_hostname = 获取主机名失败 + err_mlock = 锁定密码存储器失败 err_null = 空指针 diff --git a/src/config/Config.zig b/src/config/Config.zig index a952be8..05a14df 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -57,6 +57,8 @@ 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, input_len: u8 = 34, lang: []const u8 = "en", diff --git a/src/config/Lang.zig b/src/config/Lang.zig index 4134933..79145e5 100644 --- a/src/config/Lang.zig +++ b/src/config/Lang.zig @@ -24,6 +24,7 @@ err_envlist: []const u8 = "failed to get envlist", err_get_active_tty: []const u8 = "failed to get active tty", err_hibernate: []const u8 = "failed to execute hibernate command", err_hostname: []const u8 = "failed to get hostname", +err_inactivity: []const u8 = "failed to execute inactivity command", err_lock_state: []const u8 = "failed to get lock state", err_log: []const u8 = "failed to open log file", err_mlock: []const u8 = "failed to lock password memory", diff --git a/src/main.zig b/src/main.zig index 7addd74..fe85067 100644 --- a/src/main.zig +++ b/src/main.zig @@ -98,7 +98,7 @@ pub fn main() !void { defer _ = gpa.deinit(); // Allows stopping an animation after some time - const time_start = try interop.getTimeOfDay(); + const animation_time_start = try interop.getTimeOfDay(); var animation_timed_out: bool = false; const allocator = gpa.allocator(); @@ -284,12 +284,12 @@ pub fn main() !void { commands_allocated = true; if (config.start_cmd) |start_cmd| { - var sleep = std.process.Child.init(&[_][]const u8{ "/bin/sh", "-c", start_cmd }, allocator); - sleep.stdout_behavior = .Ignore; - sleep.stderr_behavior = .Ignore; + var start = std.process.Child.init(&[_][]const u8{ "/bin/sh", "-c", start_cmd }, allocator); + start.stdout_behavior = .Ignore; + start.stderr_behavior = .Ignore; handle_start_cmd: { - const process_result = sleep.spawnAndWait() catch { + const process_result = start.spawnAndWait() catch { break :handle_start_cmd; }; start_cmd_exit_code = process_result.Exited; @@ -594,6 +594,8 @@ pub fn main() !void { var update = true; var resolution_changed = false; var auth_fails: u64 = 0; + var inactivity_time_start = try interop.getTimeOfDay(); + var inactivity_cmd_ran = false; // Switch to selected TTY const active_tty = interop.getActiveTty(allocator) catch |err| no_tty_found: { @@ -858,7 +860,7 @@ pub fn main() !void { // 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 - time_start.seconds > config.animation_timeout_sec) { + if (config.animation_timeout_sec > 0 and time.seconds - animation_time_start.seconds > config.animation_timeout_sec) { animation_timed_out = true; animation.deinit(); } @@ -872,6 +874,28 @@ pub fn main() !void { timeout = @intCast(1000 - @divTrunc(time.microseconds, 1000) + 1); } + if (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); + 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 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}); + } + } + + inactivity_cmd_ran = true; + } + } + // Skip event polling if autologin is set, use simulated Enter key press instead if (is_autologin) { event = termbox.tb_event{ @@ -892,6 +916,9 @@ pub fn main() !void { if (event_error < 0 or event.type != termbox.TB_EVENT_KEY) continue; } + // Input of some kind was detected, so reset the inactivity timer + inactivity_time_start = try interop.getTimeOfDay(); + switch (event.key) { termbox.TB_KEY_ESC => { if (config.vi_mode and insert_mode) { From 7e18d906c483997a39394025227fc5a1b2c112d1 Mon Sep 17 00:00:00 2001 From: hynak Date: Fri, 5 Dec 2025 19:46:42 +0100 Subject: [PATCH 349/530] [Feature] Add support for .dur file format and animations (closes #719) (#833) Adds support for durdraw's .dur file format. Supports ascii, animations, and 16/256 color display. Reviewed-on: https://codeberg.org/fairyglade/ly/pulls/833 Reviewed-by: AnErrupTion Co-authored-by: hynak Co-committed-by: hynak --- res/config.ini | 12 ++ src/animations/DurFile.zig | 425 +++++++++++++++++++++++++++++++++++++ src/config/Config.zig | 3 + src/enums.zig | 1 + src/main.zig | 5 + src/tui/TerminalBuffer.zig | 7 + 6 files changed, 453 insertions(+) create mode 100644 src/animations/DurFile.zig diff --git a/res/config.ini b/res/config.ini index f56bcc6..52fb15d 100644 --- a/res/config.ini +++ b/res/config.ini @@ -24,6 +24,7 @@ allow_empty_password = true # matrix -> CMatrix # colormix -> Color mixing shader # gameoflife -> John Conway's Game of Life +# dur_file -> .dur file format (https://github.com/cmang/durdraw/tree/master) animation = none # Stop the animation after some time @@ -163,6 +164,15 @@ doom_middle_color = 0x00C78F17 # DOOM animation custom bottom color (high intensity flames) doom_bottom_color = 0x00FFFFFF +# Dur file path +dur_file_path = $CONFIG_DIRECTORY/ly/example.dur + +# Dur offset x direction +dur_x_offset = 0 + +# Dur offset y direction +dur_y_offset = 0 + # Set margin to the edges of the DM (useful for curved monitors) edge_margin = 0 @@ -190,6 +200,8 @@ 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 +# is saved as 256 with this option disabled, the file will not be drawn. full_color = true # Game of Life entropy interval (0 = disabled, >0 = add entropy every N generations) diff --git a/src/animations/DurFile.zig b/src/animations/DurFile.zig new file mode 100644 index 0000000..81b6f42 --- /dev/null +++ b/src/animations/DurFile.zig @@ -0,0 +1,425 @@ +const std = @import("std"); +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 Allocator = std.mem.Allocator; +const Json = std.json; +const eql = std.mem.eql; +const flate = std.compress.flate; + +fn read_decompress_file(allocator: Allocator, file_path: []const u8) ![]u8 { + const file_buffer = std.fs.cwd().openFile(file_path, .{}) catch { + return error.FileNotFound; + }; + defer file_buffer.close(); + + 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 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: i32, + contents: [][]u8, + colorMap: [][][]i32, + + // allocator must be outside of struct as it will fail the json parser + pub fn deinit(self: *const Frame, allocator: Allocator) void { + for (self.contents) |con| { + allocator.free(con); + } + allocator.free(self.contents); + + for (self.colorMap) |cm| { + for (cm) |int2| { + allocator.free(int2); + } + allocator.free(cm); + } + allocator.free(self.colorMap); + } +}; + +// https://github.com/cmang/durdraw/blob/0.29.0/durformat.md +const DurFormat = struct { + allocator: Allocator, + formatVersion: ?i64 = null, + colorFormat: ?[]const u8 = null, + encoding: ?[]const u8 = null, + framerate: ?f64 = null, + columns: ?i64 = null, + 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) { + + // Oldest example in dur repo was 5 so unsure if older changes json layout + if (self.formatVersion.? < 5) return false; + // v8 may have breaking changes like changing the colormap xy direction + // (https://github.com/cmang/durdraw/issues/24) + if (self.formatVersion.? > 7) return false; + + // Code currently only supports 16 and 256 color format only + if (!(eql(u8, "16", self.colorFormat.?) or eql(u8, "256", self.colorFormat.?))) + return false; + + // Code currently supports only utf-8 encoding + if (!eql(u8, self.encoding.?, "utf-8")) return false; + + // Sanity check on file + if (self.columns.? <= 0) return false; + if (self.lines.? <= 0) return false; + if (self.framerate.? < 0) return false; + + return true; + } + + return false; + } + + fn parse_dur_from_json(self: *DurFormat, 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) + self.formatVersion = if (dur_movie.get("formatVersion"))|x| x.integer else null; + self.colorFormat = if (dur_movie.get("colorFormat")) |x| try allocator.dupe(u8, x.string) else null; + self.encoding = if (dur_movie.get("encoding")) |x| try allocator.dupe(u8, x.string) else null; + self.framerate = if (dur_movie.get("framerate")) |x| x.float else null; + self.columns = if (dur_movie.get("columns")) |x| x.integer + else if (dur_movie.get("sizeX")) |x| x.integer else null; + + self.lines = if (dur_movie.get("lines")) |x| x.integer + else if (dur_movie.get("sizeY")) |x| x.integer else null; + + const frames = dur_movie.get("frames") orelse return error.NotValidFile; + + self.frames = try .initCapacity(allocator, frames.array.items.len); + + for (frames.array.items) |json_frame| { + var parsed_frame = try Json.parseFromValue(Frame, allocator, json_frame, .{}); + defer parsed_frame.deinit(); + + const frame_val = parsed_frame.value; + + // copy all fields to own the ptrs for deallocation, the parsed_frame has some other + // allocated memory making it difficult to deallocate without leaks + const frame: Frame = .{ + .frameNumber = frame_val.frameNumber, + .delay = frame_val.delay, + .contents = try allocator.alloc([]u8, frame_val.contents.len), + .colorMap = try allocator.alloc([][]i32, frame_val.colorMap.len) + }; + + for (0..frame.contents.len) |i| { + frame.contents[i] = try allocator.dupe(u8, frame_val.contents[i]); + } + + // colorMap is stored as an 3d array where: + // the outer (i) most array is the horizontal position of the color + // the middle (j) is the vertical position of the color + // the inner (0/1) is the foreground/background color + for (0..frame.colorMap.len) |i| { + frame.colorMap[i] = try allocator.alloc([]i32, frame_val.colorMap[i].len); + for (0..frame.colorMap[i].len) |j| { + frame.colorMap[i][j] = try allocator.alloc(i32, 2); + frame.colorMap[i][j][0] = frame_val.colorMap[i][j][0]; + frame.colorMap[i][j][1] = frame_val.colorMap[i][j][1]; + } + } + + try self.frames.append(allocator, frame); + } + } + + pub fn create_from_file(self: *DurFormat, allocator: Allocator, file_path: [] const u8) !void { + const file_decompressed = try read_decompress_file(allocator, 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); + + if (!self.valid()) { return error.NotValidFile; } + } + + pub fn init(allocator: Allocator) DurFormat { + return .{ .allocator = allocator }; + } + + pub fn deinit(self: *DurFormat) void { + if (self.colorFormat) |str| self.allocator.free(str); + if (self.encoding) |str| self.allocator.free(str); + + for (self.frames.items) |frame| { + frame.deinit(self.allocator); + } + self.frames.deinit(self.allocator); + } +}; + +const tb_color_16 = [16]u32{ + Color.ECOL_BLACK, + Color.ECOL_RED, + Color.ECOL_GREEN, + Color.ECOL_YELLOW, + Color.ECOL_BLUE, + Color.ECOL_MAGENTA, + Color.ECOL_CYAN, + Color.ECOL_WHITE, + Color.ECOL_BLACK | Styling.BOLD, + Color.ECOL_RED | Styling.BOLD, + Color.ECOL_GREEN | Styling.BOLD, + Color.ECOL_YELLOW | Styling.BOLD, + Color.ECOL_BLUE | Styling.BOLD, + Color.ECOL_MAGENTA | Styling.BOLD, + Color.ECOL_CYAN | Styling.BOLD, + Color.ECOL_WHITE | Styling.BOLD, +}; + +// Using bold for bright colors allows for all 16 colors to be rendered on tty term +const rgb_color_16 = [16]u32{ + Color.DEFAULT, // DEFAULT instead of TRUE_BLACK to not break compositors (the latter ignores transparency) + Color.TRUE_DIM_RED, + Color.TRUE_DIM_GREEN, + Color.TRUE_DIM_YELLOW, + Color.TRUE_DIM_BLUE, + Color.TRUE_DIM_MAGENTA, + Color.TRUE_DIM_CYAN, + Color.TRUE_DIM_WHITE, + Color.DEFAULT | Styling.BOLD, + Color.TRUE_RED | Styling.BOLD, + Color.TRUE_GREEN | Styling.BOLD, + Color.TRUE_YELLOW | Styling.BOLD, + Color.TRUE_BLUE | Styling.BOLD, + Color.TRUE_MAGENTA | Styling.BOLD, + Color.TRUE_CYAN | Styling.BOLD, + Color.TRUE_WHITE | Styling.BOLD, +}; + +// Made this table from looking at colormapping in dur source, not sure whats going on with the mapping logic +// Array indexes are dur colormappings which value maps to indexes in table above. Only needed for dur 16 color +const durcolor_table_to_color16 = [17]u32{ + 0, // 0 black + 0, // 1 nothing?? dur source did not say why 1 is unused + 4, // 2 blue + 2, // 3 green + 6, // 4 cyan + 1, // 5 red + 5, // 6 magenta + 3, // 7 yellow + 7, // 8 light gray + 8, // 9 gray + 12, // 10 bright blue + 10, // 11 bright green + 14, // 12 bright cyan + 9, // 13 bright red + 13, // 14 bright magenta + 11, // 15 bright yellow + 15, // 16 bright white +}; + +fn sixcube_to_channel(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; + + // Since the range is to 0xFF but 6 isn't divisible, we must add a scaler to get it to 0xFF at the last index (5) + const scaler = 0xFF - (equal_divisions * 5); + + return if (sixcube > 0) (sixcube * equal_divisions) + scaler else 0; +} + +fn convert_256_to_rgb(color_256: u32) u32 { + var rgb_color: u32 = 0; + + // 0 - 15 is the standard color range, map to array table + if (color_256 < 16) { + rgb_color = rgb_color_16[color_256]; + } + // 16 - 231 is the extended range + else if (color_256 < 232) { + + // For extended term range we subtract by 16 to get it in a 0..(6x6x6) cube (range of 216) + // divide by 36 gets the depth of the cube (6x6x1) + // divide by 6 gets the width of the cube (6x1) + // 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); + } + // 232 - 255 is the grayscale range + else { + + // For grayscale we have a space of 232 - 255 (24) + // subtract by 232 to get it into the 0..23 range + // standard colors will contain white and black, so we do not use them in the grayscale range (0 is 0x08, 23 is 0xEE) + // this results in a skip of 0x08 for the first color and divisions of 0x0A + // example: term_col 232 = scaler + equal_divisions * (232 - 232) which becomes (scaler + 0x00) == 0x08 + // example: term_col 255 = scaler + equal_divisions * (255 - 232) which becomes (scaler + 0xE6) == 0xEE + const scaler = 0x08; + + // to get equal parts, the equation is: + // 0xEE = equal_divisions * 23 + scaler | top of range is 0xEE, 23 is last element value (255 minus 232) + // reordered to solve for equal_divisions: + const equal_divisions = (0xEE - scaler) / 23; // evals to 0x0A + + const channel = scaler + equal_divisions * (color_256 - 232); + + // gray is equal value of same channel color in rgb + rgb_color = channel | (channel << 8) | (channel << 16); + } + + return rgb_color; +} + + +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, +frame_time: u32, +is_color_format_16 : bool, + +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 { + 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}); + return err; + }, + error.NotValidFile => { + try log_writer.print("error: dur_file loaded was invalid or not a dur file!\n", .{}); + return err; + }, + else => return err, + }; + + // 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", .{}); + dur_movie.deinit(); + return error.InvalidColorFormat; + } + + 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.?); + + // 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; + + // Convert dur fps to frames per ms + const frame_time: u32 = @intFromFloat(1000 / dur_movie.framerate.?); + + return .{ + .allocator = allocator, + .terminal_buffer = terminal_buffer, + .frames = 0, + .time_previous = std.time.milliTimestamp(), + .x_offset = x_offset_clamped, + .y_offset = y_offset_clamped, + .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") + }; +} + +pub fn animation(self: *DurFile) Animation { + return Animation.init(self, deinit, realloc, draw); +} + +fn deinit(self: *DurFile) void { + self.dur_movie.deinit(); +} + +fn realloc(_: *DurFile) anyerror!void {} + +fn draw(self: *DurFile) void { + const current_frame = self.dur_movie.frames.items[self.frames]; + + for (0..self.frame_height) |y| { + var iter = std.unicode.Utf8View.initUnchecked(current_frame.contents[y]).iterator(); + + for (0..self.frame_width) |x| { + const codepoint: u21 = iter.nextCodepoint().?; + + var color_map_0: u32 = @intCast(current_frame.colorMap[x][y][0]); + var color_map_1: u32 = @intCast(current_frame.colorMap[x][y][1]); + + if (self.is_color_format_16) { + color_map_0 = durcolor_table_to_color16[color_map_0]; + 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 cell = Cell { + .ch = @intCast(codepoint), + .fg = fg_color, + .bg = bg_color + }; + + cell.put(x + self.x_offset, y + self.y_offset); + } + } + + const time_current = std.time.milliTimestamp(); + const delta_time = time_current - self.time_previous; + + // Convert delay from sec to ms + const delay_time: u32 = @intCast(current_frame.delay * 1000); + if (delta_time > (self.frame_time + delay_time)) { + self.time_previous = time_current; + + const frame_count = self.dur_movie.frames.items.len; + self.frames = (self.frames + 1) % frame_count; + } +} diff --git a/src/config/Config.zig b/src/config/Config.zig index 05a14df..e9169bd 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -42,6 +42,9 @@ doom_fire_spread: u8 = 2, 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, edge_margin: u8 = 0, error_bg: u32 = 0x00000000, error_fg: u32 = 0x01FF0000, diff --git a/src/enums.zig b/src/enums.zig index 07dc3eb..337d6bf 100644 --- a/src/enums.zig +++ b/src/enums.zig @@ -5,6 +5,7 @@ pub const Animation = enum { matrix, colormix, gameoflife, + dur_file, }; pub const DisplayServer = enum { diff --git a/src/main.zig b/src/main.zig index fe85067..608f4e2 100644 --- a/src/main.zig +++ b/src/main.zig @@ -13,6 +13,7 @@ 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"); const Animation = @import("tui/Animation.zig"); const TerminalBuffer = @import("tui/TerminalBuffer.zig"); const Session = @import("tui/components/Session.zig"); @@ -572,6 +573,10 @@ pub fn main() !void { 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_writer, config.dur_file_path, config.dur_x_offset, config.dur_y_offset, config.full_color); + animation = dur.animation(); + }, } defer animation.deinit(); diff --git a/src/tui/TerminalBuffer.zig b/src/tui/TerminalBuffer.zig index f09877d..999dfca 100644 --- a/src/tui/TerminalBuffer.zig +++ b/src/tui/TerminalBuffer.zig @@ -38,6 +38,13 @@ pub const Color = struct { pub const TRUE_MAGENTA = 0x00FF00FF; pub const TRUE_CYAN = 0x0000FFFF; pub const TRUE_WHITE = 0x00FFFFFF; + pub const TRUE_DIM_RED = 0x00800000; + pub const TRUE_DIM_GREEN = 0x00008000; + pub const TRUE_DIM_YELLOW = 0x00808000; + pub const TRUE_DIM_BLUE = 0x00000080; + pub const TRUE_DIM_MAGENTA = 0x00800080; + pub const TRUE_DIM_CYAN = 0x00008080; + pub const TRUE_DIM_WHITE = 0x00C0C0C0; pub const ECOL_BLACK = 1; pub const ECOL_RED = 2; pub const ECOL_GREEN = 3; From 92beb24c8010c6e45b75c3f2d914e988ed33bab4 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Fri, 5 Dec 2025 19:49:27 +0100 Subject: [PATCH 350/530] Fix save file initialisation & incorrect saved session index Signed-off-by: AnErrupTion --- src/main.zig | 12 ++++++------ src/tui/components/Session.zig | 7 ++++++- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/src/main.zig b/src/main.zig index 608f4e2..d1e429f 100644 --- a/src/main.zig +++ b/src/main.zig @@ -259,10 +259,10 @@ pub fn main() !void { .session_index = session_index, }); } + } - // If no save file previously existed, fill it up with all usernames - if (saved_users.user_list.items.len > 0) break :read_save_file; - + // If no save file previously existed, fill it up with all usernames + if (config.save and saved_users.user_list.items.len == 0) { for (usernames.items) |user| { try saved_users.user_list.append(allocator, .{ .username = user, @@ -408,6 +408,9 @@ pub fn main() !void { var session = Session.init(allocator, &buffer, &login); defer session.deinit(); + login = try UserList.init(allocator, &buffer, usernames, &saved_users, &session); + defer login.deinit(); + 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)}); @@ -467,9 +470,6 @@ pub fn main() !void { try log_writer.writeAll("no users found\n"); } - login = try UserList.init(allocator, &buffer, usernames, &saved_users, &session); - defer login.deinit(); - var password = Text.init(allocator, &buffer, true, config.asterisk); defer password.deinit(); diff --git a/src/tui/components/Session.zig b/src/tui/components/Session.zig index 4417e06..ac6188b 100644 --- a/src/tui/components/Session.zig +++ b/src/tui/components/Session.zig @@ -17,10 +17,12 @@ const EnvironmentLabel = generic.CyclableLabel(Env, *UserList); const Session = @This(); label: EnvironmentLabel, +user_list: *UserList, pub fn init(allocator: Allocator, buffer: *TerminalBuffer, user_list: *UserList) Session { return .{ .label = EnvironmentLabel.init(allocator, buffer, drawItem, sessionChanged, user_list), + .user_list = user_list, }; } @@ -36,7 +38,10 @@ pub fn deinit(self: *Session) void { } pub fn addEnvironment(self: *Session, environment: Environment) !void { - try self.label.addItem(.{ .environment = environment, .index = self.label.list.items.len }); + const env = Env{ .environment = environment, .index = self.label.list.items.len }; + + try self.label.addItem(env); + sessionChanged(env, self.user_list); } fn sessionChanged(env: Env, maybe_user_list: ?*UserList) void { From a9ff0a6d07d197fbcdb0eae5b6d97814e5ef6e5d Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Sat, 6 Dec 2025 10:05:41 +0100 Subject: [PATCH 351/530] Only support dur format v7, set -1 color to black Signed-off-by: AnErrupTion --- src/animations/DurFile.zig | 149 ++++++++++++++++--------------------- 1 file changed, 66 insertions(+), 83 deletions(-) diff --git a/src/animations/DurFile.zig b/src/animations/DurFile.zig index 81b6f42..a79d653 100644 --- a/src/animations/DurFile.zig +++ b/src/animations/DurFile.zig @@ -17,14 +17,14 @@ fn read_decompress_file(allocator: Allocator, file_path: []const u8) ![]u8 { 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 decompress: flate.Decompress = .init(&file_reader.interface, .gzip, &decompress_buffer); - const file_decompressed = decompress.reader.allocRemaining(allocator, .unlimited) catch { + const file_decompressed = decompress.reader.allocRemaining(allocator, .unlimited) catch { return error.NotValidFile; }; - + return file_decompressed; } @@ -36,7 +36,7 @@ const Frame = struct { // allocator must be outside of struct as it will fail the json parser pub fn deinit(self: *const Frame, allocator: Allocator) void { - for (self.contents) |con| { + for (self.contents) |con| { allocator.free(con); } allocator.free(self.contents); @@ -63,22 +63,20 @@ const DurFormat = struct { frames: std.ArrayList(Frame) = undefined, pub fn valid(self: *DurFormat) bool { - if (self.formatVersion != null and + 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) { - - // Oldest example in dur repo was 5 so unsure if older changes json layout - if (self.formatVersion.? < 5) return false; - // v8 may have breaking changes like changing the colormap xy direction + 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; + if (self.formatVersion.? != 7) return false; // Code currently only supports 16 and 256 color format only - if (!(eql(u8, "16", self.colorFormat.?) or eql(u8, "256", self.colorFormat.?))) + if (!(eql(u8, "16", self.colorFormat.?) or eql(u8, "256", self.colorFormat.?))) return false; // Code currently supports only utf-8 encoding @@ -96,18 +94,16 @@ const DurFormat = struct { } fn parse_dur_from_json(self: *DurFormat, 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; + 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) - self.formatVersion = if (dur_movie.get("formatVersion"))|x| x.integer else null; - self.colorFormat = if (dur_movie.get("colorFormat")) |x| try allocator.dupe(u8, x.string) else null; - self.encoding = if (dur_movie.get("encoding")) |x| try allocator.dupe(u8, x.string) else null; - self.framerate = if (dur_movie.get("framerate")) |x| x.float else null; - self.columns = if (dur_movie.get("columns")) |x| x.integer - else if (dur_movie.get("sizeX")) |x| x.integer else null; + self.formatVersion = if (dur_movie.get("formatVersion")) |x| x.integer else null; + self.colorFormat = if (dur_movie.get("colorFormat")) |x| try allocator.dupe(u8, x.string) else null; + self.encoding = if (dur_movie.get("encoding")) |x| try allocator.dupe(u8, x.string) else null; + self.framerate = if (dur_movie.get("framerate")) |x| x.float else null; + self.columns = if (dur_movie.get("columns")) |x| x.integer else if (dur_movie.get("sizeX")) |x| x.integer else null; - self.lines = if (dur_movie.get("lines")) |x| x.integer - else if (dur_movie.get("sizeY")) |x| x.integer else null; + self.lines = if (dur_movie.get("lines")) |x| x.integer else if (dur_movie.get("sizeY")) |x| x.integer else null; const frames = dur_movie.get("frames") orelse return error.NotValidFile; @@ -117,21 +113,16 @@ const DurFormat = struct { var parsed_frame = try Json.parseFromValue(Frame, allocator, json_frame, .{}); defer parsed_frame.deinit(); - const frame_val = parsed_frame.value; - - // copy all fields to own the ptrs for deallocation, the parsed_frame has some other + const frame_val = parsed_frame.value; + + // copy all fields to own the ptrs for deallocation, the parsed_frame has some other // allocated memory making it difficult to deallocate without leaks - const frame: Frame = .{ - .frameNumber = frame_val.frameNumber, - .delay = frame_val.delay, - .contents = try allocator.alloc([]u8, frame_val.contents.len), - .colorMap = try allocator.alloc([][]i32, frame_val.colorMap.len) - }; - + const frame: Frame = .{ .frameNumber = frame_val.frameNumber, .delay = frame_val.delay, .contents = try allocator.alloc([]u8, frame_val.contents.len), .colorMap = try allocator.alloc([][]i32, frame_val.colorMap.len) }; + for (0..frame.contents.len) |i| { frame.contents[i] = try allocator.dupe(u8, frame_val.contents[i]); } - + // colorMap is stored as an 3d array where: // the outer (i) most array is the horizontal position of the color // the middle (j) is the vertical position of the color @@ -143,22 +134,24 @@ const DurFormat = struct { frame.colorMap[i][j][0] = frame_val.colorMap[i][j][0]; frame.colorMap[i][j][1] = frame_val.colorMap[i][j][1]; } - } - + } + try self.frames.append(allocator, frame); } } - pub fn create_from_file(self: *DurFormat, allocator: Allocator, file_path: [] const u8) !void { + pub fn create_from_file(self: *DurFormat, allocator: Allocator, file_path: []const u8) !void { const file_decompressed = try read_decompress_file(allocator, 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); - if (!self.valid()) { return error.NotValidFile; } + if (!self.valid()) { + return error.NotValidFile; + } } pub fn init(allocator: Allocator) DurFormat { @@ -169,8 +162,8 @@ const DurFormat = struct { if (self.colorFormat) |str| self.allocator.free(str); if (self.encoding) |str| self.allocator.free(str); - for (self.frames.items) |frame| { - frame.deinit(self.allocator); + for (self.frames.items) |frame| { + frame.deinit(self.allocator); } self.frames.deinit(self.allocator); } @@ -218,30 +211,30 @@ const rgb_color_16 = [16]u32{ // Made this table from looking at colormapping in dur source, not sure whats going on with the mapping logic // Array indexes are dur colormappings which value maps to indexes in table above. Only needed for dur 16 color const durcolor_table_to_color16 = [17]u32{ - 0, // 0 black - 0, // 1 nothing?? dur source did not say why 1 is unused - 4, // 2 blue - 2, // 3 green - 6, // 4 cyan - 1, // 5 red - 5, // 6 magenta - 3, // 7 yellow - 7, // 8 light gray - 8, // 9 gray - 12, // 10 bright blue + 0, // 0 black + 0, // 1 nothing?? dur source did not say why 1 is unused + 4, // 2 blue + 2, // 3 green + 6, // 4 cyan + 1, // 5 red + 5, // 6 magenta + 3, // 7 yellow + 7, // 8 light gray + 8, // 9 gray + 12, // 10 bright blue 10, // 11 bright green 14, // 12 bright cyan - 9, // 13 bright red + 9, // 13 bright red 13, // 14 bright magenta 11, // 15 bright yellow 15, // 16 bright white }; fn sixcube_to_channel(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 + // 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; - + // Since the range is to 0xFF but 6 isn't divisible, we must add a scaler to get it to 0xFF at the last index (5) const scaler = 0xFF - (equal_divisions * 5); @@ -285,7 +278,7 @@ fn convert_256_to_rgb(color_256: u32) u32 { const equal_divisions = (0xEE - scaler) / 23; // evals to 0x0A const channel = scaler + equal_divisions * (color_256 - 232); - + // gray is equal value of same channel color in rgb rgb_color = channel | (channel << 8) | (channel << 16); } @@ -293,7 +286,6 @@ fn convert_256_to_rgb(color_256: u32) u32 { return rgb_color; } - const DurFile = @This(); allocator: Allocator, @@ -307,20 +299,14 @@ dur_movie: DurFormat, frame_width: u32, frame_height: u32, frame_time: u32, -is_color_format_16 : bool, +is_color_format_16: bool, -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 { +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 { 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 => { + error.FileNotFound => { try log_writer.print("error: dur_file was not found at: {s}\n", .{file_path}); return err; }, @@ -340,18 +326,18 @@ pub fn init(allocator: Allocator, 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.?); // 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; - + // Convert dur fps to frames per ms const frame_time: u32 = @intFromFloat(1000 / dur_movie.framerate.?); @@ -367,7 +353,7 @@ pub fn init(allocator: Allocator, .frame_width = frame_width, .frame_height = frame_height, .frame_time = frame_time, - .is_color_format_16 = eql(u8, dur_movie.colorFormat.?, "16") + .is_color_format_16 = eql(u8, dur_movie.colorFormat.?, "16"), }; } @@ -383,37 +369,34 @@ fn realloc(_: *DurFile) anyerror!void {} fn draw(self: *DurFile) void { const current_frame = self.dur_movie.frames.items[self.frames]; - + for (0..self.frame_height) |y| { var iter = std.unicode.Utf8View.initUnchecked(current_frame.contents[y]).iterator(); - + for (0..self.frame_width) |x| { const codepoint: u21 = iter.nextCodepoint().?; + const color_map = current_frame.colorMap[x][y]; - var color_map_0: u32 = @intCast(current_frame.colorMap[x][y][0]); - var color_map_1: u32 = @intCast(current_frame.colorMap[x][y][1]); + var color_map_0: u32 = @intCast(if (color_map[0] == -1) 0 else color_map[0]); + var color_map_1: u32 = @intCast(if (color_map[1] == -1) 0 else color_map[1]); if (self.is_color_format_16) { - color_map_0 = durcolor_table_to_color16[color_map_0]; + color_map_0 = durcolor_table_to_color16[color_map_0]; 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 cell = Cell { - .ch = @intCast(codepoint), - .fg = fg_color, - .bg = bg_color - }; + const cell = Cell{ .ch = @intCast(codepoint), .fg = fg_color, .bg = bg_color }; cell.put(x + self.x_offset, y + self.y_offset); } } - + const time_current = std.time.milliTimestamp(); const delta_time = time_current - self.time_previous; - + // Convert delay from sec to ms const delay_time: u32 = @intCast(current_frame.delay * 1000); if (delta_time > (self.frame_time + delay_time)) { From cab3f1bfb57c00b0587d4f8bc2989e2c1c07e1d9 Mon Sep 17 00:00:00 2001 From: RacerBG Date: Sat, 6 Dec 2025 13:39:52 +0100 Subject: [PATCH 352/530] Added the Latest Changes to the Bulgarian Translation (#879) Signed-off-by: RacerBG ## What are the changes about? N/A ## 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/879 Reviewed-by: AnErrupTion Co-authored-by: RacerBG Co-committed-by: RacerBG --- res/lang/bg.ini | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/res/lang/bg.ini b/res/lang/bg.ini index aa24e02..8557023 100644 --- a/res/lang/bg.ini +++ b/res/lang/bg.ini @@ -19,7 +19,7 @@ err_envlist = неуспешно получаване на списъка с п err_get_active_tty = неуспешно откриване на активния TTY err_hibernate = неуспешно изпълнение на командата за хибернация err_hostname = неуспешно получаване на името на хоста - +err_inactivity = неуспешно изпълнение на командата за неактивност err_lock_state = неуспешно получаване на състоянието на заключване err_log = неуспешно отваряне на файла с дневника err_mlock = неуспешно заключване на паметта за паролата @@ -47,7 +47,7 @@ err_perm_group = неуспешно понижаване на правата н err_perm_user = неуспешно понижаване на правата на потребителя err_pwnam = неуспешно получаване на информация за потребителя err_sleep = неуспешно изпълнение на командата за заспиване - +err_start = неуспешно изпълнение на командата за стартиране err_battery = неуспешно зареждане на състоянието на батерията err_switch_tty = неуспешна смяна на TTY err_tty_ctrl = неуспешно прехвърляне на контрола над TTY From e6966a628c64a7ac347f1df088f2a0fde537e799 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Sat, 6 Dec 2025 14:25:01 +0100 Subject: [PATCH 353/530] Fix wrong session index + save file corruption Signed-off-by: AnErrupTion --- src/config/SavedUsers.zig | 6 ++++++ src/config/migrator.zig | 7 ++++++- src/main.zig | 7 ++++++- src/tui/components/Session.zig | 5 ++++- src/tui/components/UserList.zig | 4 ++++ 5 files changed, 26 insertions(+), 3 deletions(-) diff --git a/src/config/SavedUsers.zig b/src/config/SavedUsers.zig index dd08eb4..4891f27 100644 --- a/src/config/SavedUsers.zig +++ b/src/config/SavedUsers.zig @@ -5,6 +5,8 @@ const SavedUsers = @This(); const User = struct { username: []const u8, session_index: usize, + first_run: bool, + allocated_username: bool, }; user_list: std.ArrayList(User), @@ -18,5 +20,9 @@ pub fn init() SavedUsers { } pub fn deinit(self: *SavedUsers, allocator: std.mem.Allocator) void { + for (self.user_list.items) |user| { + if (user.allocated_username) allocator.free(user.username); + } + self.user_list.deinit(allocator); } diff --git a/src/config/migrator.zig b/src/config/migrator.zig index bfba870..124ec89 100644 --- a/src/config/migrator.zig +++ b/src/config/migrator.zig @@ -238,7 +238,12 @@ pub fn tryMigrateIniSaveFile(allocator: std.mem.Allocator, save_ini: *ini.Ini(Ol 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 }); + try saved_users.user_list.append(allocator, .{ + .username = username, + .session_index = save.session_index orelse 0, + .first_run = false, + .allocated_username = false, + }); } return true; diff --git a/src/main.zig b/src/main.zig index d1e429f..8565c9a 100644 --- a/src/main.zig +++ b/src/main.zig @@ -255,18 +255,23 @@ 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 = username, + .username = try allocator.dupe(u8, username), .session_index = session_index, + .first_run = false, + .allocated_username = true, }); } } // 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) { for (usernames.items) |user| { try saved_users.user_list.append(allocator, .{ .username = user, .session_index = 0, + .first_run = true, + .allocated_username = false, }); } } diff --git a/src/tui/components/Session.zig b/src/tui/components/Session.zig index ac6188b..74ab7a2 100644 --- a/src/tui/components/Session.zig +++ b/src/tui/components/Session.zig @@ -46,7 +46,10 @@ pub fn addEnvironment(self: *Session, environment: Environment) !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; + const user = user_list.label.list.items[user_list.label.current]; + if (!user.first_run) return; + + user.session_index.* = env.index; } } diff --git a/src/tui/components/UserList.zig b/src/tui/components/UserList.zig index 83819bb..d9ed846 100644 --- a/src/tui/components/UserList.zig +++ b/src/tui/components/UserList.zig @@ -11,6 +11,7 @@ pub const User = struct { name: []const u8, session_index: *usize, allocated_index: bool, + first_run: bool, }; const UserLabel = generic.CyclableLabel(User, *Session); @@ -27,9 +28,11 @@ pub fn init(allocator: Allocator, buffer: *TerminalBuffer, usernames: StringList if (username.len == 0) continue; var maybe_session_index: ?*usize = null; + var first_run = true; for (saved_users.user_list.items) |*saved_user| { if (std.mem.eql(u8, username, saved_user.username)) { maybe_session_index = &saved_user.session_index; + first_run = saved_user.first_run; break; } } @@ -45,6 +48,7 @@ pub fn init(allocator: Allocator, buffer: *TerminalBuffer, usernames: StringList .name = username, .session_index = maybe_session_index.?, .allocated_index = allocated_index, + .first_run = first_run, }); } From e0692885c5c8d535115338b797565bffa85811e6 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Sat, 6 Dec 2025 14:34:47 +0100 Subject: [PATCH 354/530] Make delay floating point in dur format Signed-off-by: AnErrupTion --- src/animations/DurFile.zig | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/animations/DurFile.zig b/src/animations/DurFile.zig index a79d653..86b951d 100644 --- a/src/animations/DurFile.zig +++ b/src/animations/DurFile.zig @@ -30,7 +30,7 @@ fn read_decompress_file(allocator: Allocator, file_path: []const u8) ![]u8 { const Frame = struct { frameNumber: i32, - delay: i32, + delay: f32, contents: [][]u8, colorMap: [][][]i32, @@ -398,7 +398,7 @@ fn draw(self: *DurFile) void { const delta_time = time_current - self.time_previous; // Convert delay from sec to ms - const delay_time: u32 = @intCast(current_frame.delay * 1000); + const delay_time: u32 = @intFromFloat(current_frame.delay * 1000); if (delta_time > (self.frame_time + delay_time)) { self.time_previous = time_current; From c6446db3e202bb7774bd24a0330eb2438a03b78d Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Sat, 6 Dec 2025 15:58:39 +0100 Subject: [PATCH 355/530] 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 356/530] 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 357/530] 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 358/530] 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 359/530] 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 360/530] 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 361/530] 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 362/530] 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 363/530] 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 364/530] 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 365/530] 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 366/530] 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 367/530] 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 368/530] 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 369/530] 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 370/530] 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 371/530] 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 372/530] 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 373/530] 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 374/530] [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 375/530] 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 376/530] 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 377/530] 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 378/530] 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 379/530] 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 380/530] 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 381/530] 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 382/530] 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 383/530] 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 384/530] 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 385/530] 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 386/530] 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 387/530] 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 388/530] 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 389/530] 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 390/530] 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 391/530] 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 392/530] 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 393/530] 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 394/530] 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 395/530] 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 396/530] 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 397/530] 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 398/530] 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 399/530] 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 400/530] 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 401/530] 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 402/530] 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 403/530] 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 404/530] 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 405/530] 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 406/530] 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 407/530] 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 408/530] 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 409/530] 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 410/530] 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 411/530] 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 412/530] 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 413/530] 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 414/530] 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 415/530] 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 416/530] 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 417/530] 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 418/530] 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 419/530] 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 420/530] 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 421/530] 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 422/530] 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 423/530] 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 424/530] 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 425/530] 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 426/530] 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 427/530] 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 428/530] 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 429/530] 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 430/530] 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 431/530] 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 432/530] 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 433/530] 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 434/530] 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 435/530] 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 436/530] 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 437/530] 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 438/530] 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 439/530] 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 440/530] 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 441/530] 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 442/530] 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 443/530] 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 444/530] 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 445/530] 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 446/530] 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 447/530] 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 448/530] 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 449/530] 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 450/530] 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 451/530] 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 452/530] 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 453/530] 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 454/530] 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 455/530] 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 456/530] 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 457/530] 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 458/530] 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 459/530] 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 460/530] 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 461/530] 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 462/530] 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 463/530] 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 464/530] 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 465/530] 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 466/530] 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 467/530] 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 468/530] 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 469/530] 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 470/530] 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 471/530] 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 472/530] 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 473/530] 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 474/530] 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 475/530] 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 476/530] 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 477/530] 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 478/530] 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 479/530] 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 480/530] 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 481/530] 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 482/530] 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 483/530] 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 484/530] 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 485/530] 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 486/530] 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 487/530] 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 488/530] 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 489/530] 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 490/530] 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 491/530] 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 492/530] 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 493/530] 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 494/530] 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 495/530] 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 496/530] 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 497/530] 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 498/530] 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 499/530] 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 500/530] 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 501/530] 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 502/530] 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 503/530] 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 504/530] 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 505/530] 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 506/530] 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 507/530] 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 508/530] 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 509/530] 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 510/530] 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 511/530] 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 512/530] 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 513/530] 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 514/530] 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 515/530] 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 516/530] 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 517/530] 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 518/530] 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 519/530] 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 520/530] 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 521/530] 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 522/530] 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 523/530] 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 524/530] 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 525/530] 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 526/530] 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 527/530] 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 528/530] 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 529/530] 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 530/530] 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; };