From b6fba46b087cd27fefa4d67c0a3b0eb084c7b016 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Tue, 28 Jul 2026 01:38:48 +0200 Subject: [PATCH 01/26] 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 02/26] 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; }; From db7f8ac5d39ba1d6f0cd9988607cb9ef70ae494f Mon Sep 17 00:00:00 2001 From: dragsbruh Date: Fri, 21 Aug 2026 19:10:29 +0200 Subject: [PATCH 03/26] [Docs] Fix instructions to disable agetty-tty2 for runit in readme (#1043) ## What are the changes about? updated instructions in readme.md for runit to properly disable `agetty-tty2` and unlink services ## What existing issue(s) does this resolve? on void linux, when `runit-void` package is upgraded it re-adds the symlinks for agetty, including `agetty-tty2`, if its not properly disabled (`down` file in svdir) https://github.com/void-linux/void-packages/blob/master/srcpkgs/runit-void/INSTALL so everytime that package is updated, both `agetty-tty2` and `ly` start making `ly` unusable until you manually unlink it ## 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/1043 Reviewed-by: AnErrupTion --- readme.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/readme.md b/readme.md index 2d26c31..fa1c38d 100644 --- a/readme.md +++ b/readme.md @@ -180,9 +180,10 @@ On non-systemd systems, you can change the TTY Ly will run on by editing the cor ``` # zig build installexe -Dinit_system=runit -# rm /var/service/lightdm +# unlink /var/service/lightdm # ln -s /etc/sv/ly /var/service/ -# rm /var/service/agetty-tty2 +# touch /etc/sv/agetty-tty2/down +# unlink /var/service/agetty-tty2 ``` ### s6 From 1d071223abeea843394cfa4f9d2d560e50e4b682 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Sun, 23 Aug 2026 15:35:38 +0200 Subject: [PATCH 04/26] README: Remove runtime `shutdown` dependency Signed-off-by: AnErrupTion --- readme.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/readme.md b/readme.md index fa1c38d..8cacd1a 100644 --- a/readme.md +++ b/readme.md @@ -27,8 +27,6 @@ Join us on Matrix over at [#ly-dm:matrix.org](https://matrix.to/#/#ly-dm:matrix. - xorg-xauth - - shutdown - - brightnessctl ### Debian From 11da43d7d9341572261d9a41a6c59d1a02a9855a Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Mon, 24 Aug 2026 15:20:13 +0200 Subject: [PATCH 05/26] Start Ly v1.6.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 cdb0d1f..71bece1 100644 --- a/build.zig +++ b/build.zig @@ -28,7 +28,7 @@ comptime { } } -const ly_version = std.SemanticVersion{ .major = 1, .minor = 5, .patch = 0 }; +const ly_version = std.SemanticVersion{ .major = 1, .minor = 6, .patch = 0 }; fn InstallStep( b: *std.Build, diff --git a/build.zig.zon b/build.zig.zon index 1d19b69..a340291 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -1,6 +1,6 @@ .{ .name = .ly, - .version = "1.5.0", + .version = "1.6.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 0d84326..ddf0b9b 100644 --- a/ly-core/build.zig.zon +++ b/ly-core/build.zig.zon @@ -1,6 +1,6 @@ .{ .name = .ly_core, - .version = "1.1.0", + .version = "1.2.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 68131d1..2875b7c 100644 --- a/ly-ui/build.zig.zon +++ b/ly-ui/build.zig.zon @@ -1,6 +1,6 @@ .{ .name = .ly_ui, - .version = "1.1.0", + .version = "1.2.0", .fingerprint = 0x8d11bf85a74ec803, .minimum_zig_version = "0.16.0", .dependencies = .{ From f17ccf9ea7c9de45889436c9dc3f9095faff86ac Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Mon, 24 Aug 2026 15:29:17 +0200 Subject: [PATCH 06/26] build.zig: Support rc build descriptions Signed-off-by: AnErrupTion --- build.zig | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/build.zig b/build.zig index 71bece1..8c604e1 100644 --- a/build.zig +++ b/build.zig @@ -214,6 +214,20 @@ fn getVersionStr(b: *std.Build, name: []const u8, version: std.SemanticVersion) } return version_str; }, + 1 => { + // Release candidate build (e.g. v1.5.0-rc1) + var it = std.mem.splitScalar(u8, git_describe, '-'); + const tagged_ancestor = std.mem.trimStart(u8, it.first(), "v"); + + const ancestor_ver = try std.SemanticVersion.parse(tagged_ancestor); + if (version.order(ancestor_ver) != .gt) { + std.debug.print("{s} version '{f}' must be greater than tagged ancestor '{f}'\n", .{ name, version, ancestor_ver }); + std.process.exit(1); + } + + // The version is reformatted in accordance with the https://semver.org specification. + return version_str; + }, 2 => { // Untagged development build (e.g. 0.10.0-dev.2025+ecf0050a9). var it = std.mem.splitScalar(u8, git_describe, '-'); @@ -236,6 +250,29 @@ fn getVersionStr(b: *std.Build, name: []const u8, version: std.SemanticVersion) // The version is reformatted in accordance with the https://semver.org specification. return b.fmt("{s}-dev.{s}+{s}", .{ version_str, commit_height, commit_id[1..] }); }, + 3 => { + // Untagged development build (e.g. 0.10.0-dev.2025+ecf0050a9). + var it = std.mem.splitScalar(u8, git_describe, '-'); + const tagged_ancestor = std.mem.trimStart(u8, it.first(), "v"); + _ = it.next().?; + const commit_height = it.next().?; + const commit_id = it.next().?; + + const ancestor_ver = try std.SemanticVersion.parse(tagged_ancestor); + if (version.order(ancestor_ver) != .gt) { + std.debug.print("{s} version '{f}' must be greater than tagged ancestor '{f}'\n", .{ name, version, ancestor_ver }); + std.process.exit(1); + } + + // Check that the commit hash is prefixed with a 'g' (a Git convention). + if (commit_id.len < 1 or commit_id[0] != 'g') { + std.debug.print("Unexpected `git describe` output: {s}\n", .{git_describe}); + return version_str; + } + + // The version is reformatted in accordance with the https://semver.org specification. + return b.fmt("{s}-dev.{s}+{s}", .{ version_str, commit_height, commit_id[1..] }); + }, else => { std.debug.print("Unexpected `git describe` output: {s}\n", .{git_describe}); return version_str; From fa8332dddda854602048126c981a4654f98057b5 Mon Sep 17 00:00:00 2001 From: urly3 Date: Mon, 24 Aug 2026 15:30:46 +0200 Subject: [PATCH 07/26] feature: getActiveTerminal for FreeBSD (#1047) ## What are the changes about? Implements getActiveTerminal for FreeBSD via fstat ## What existing issue(s) does this resolve? #1046 ## 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/1047 Reviewed-by: AnErrupTion --- ly-core/src/interop.zig | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/ly-core/src/interop.zig b/ly-core/src/interop.zig index d894485..b269513 100644 --- a/ly-core/src/interop.zig +++ b/ly-core/src/interop.zig @@ -246,8 +246,29 @@ fn PlatformStruct() type { if (result != 0) return error.SetUserUidFailed; } + // FreeBSD implementation using sysctlbyname + // https://man.freebsd.org/cgi/man.cgi?sysctlbyname pub fn getActiveTtyImpl(_: std.mem.Allocator, _: std.Io, _: bool) !u8 { - return error.FeatureUnimplemented; + const tty_fd = std.posix.system.open("/dev/tty", .{}); + if (tty_fd < 0) return error.NoTtyFound; + defer _ = std.posix.system.close(tty_fd); + + var tty_stat: std.posix.Stat = undefined; + if (std.posix.system.fstat(tty_fd, &tty_stat) < 0) return error.FstatTty; + + var buf: ["ttyvx".len:0]u8 = undefined; + var len: usize = buf.len + 1; + + if (std.posix.system.sysctlbyname("kern.devname", buf[0..].ptr, &len, &tty_stat.rdev, @sizeOf(u64)) < 0) + return error.SysctlTty; + + const dev_name = buf[0 .. len - 1]; + + if (std.mem.startsWith(u8, dev_name, "ttyv")) { + return try std.fmt.parseInt(u8, dev_name[dev_name.len - 1 ..], 16); + } + + return error.NoTtyFound; } pub fn getUserIdRange(_: std.mem.Allocator, _: std.Io, _: []const u8) !UidRange { From bd28c0e679e71bb1e7507a28191a4fa0db80db2d Mon Sep 17 00:00:00 2001 From: jR4dh3y Date: Mon, 24 Aug 2026 15:31:52 +0200 Subject: [PATCH 08/26] Add optional bigclock digit outline (closes #1030) (#1031) ## What are the changes about? Add optional `bigclock_outline_fg`. When set, each big-clock digit gets a 1-cell outline drawn under the glyph so it stays readable over busy animations. Default is `null` (previous behavior). When the outline is enabled, the gap under the clock is slightly larger so the stroke is not glued to the login box. ## What existing issue(s) does this resolve? Fixes #1030 ## 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` ### Testing - `zig build -Doptimize=ReleaseSafe` (zig 0.16.0) - Greeter with `bigclock = en` and `bigclock_outline_fg = 0x20000000` over a `dur_file` animation - Default `null` leaves stock look unchanged ### AI usage (per contributing.md) Assisted by an LLM (xAI Grok) for exploration and drafting. I directed the design (optional outline only, no panel/box, stock layout otherwise), reviewed the Zig, fixed an `i2` overflow in the outline loop, verified the build/local greeter, and wrote this PR text. Reviewed-on: https://codeberg.org/fairyglade/ly/pulls/1031 Reviewed-by: AnErrupTion --- ly-ui/src/components/BigLabel.zig | 75 +++++++++++++++++++++++++++---- res/config.ini | 3 ++ src/config/Config.zig | 1 + src/main.zig | 4 +- 4 files changed, 73 insertions(+), 10 deletions(-) diff --git a/ly-ui/src/components/BigLabel.zig b/ly-ui/src/components/BigLabel.zig index cb2f6b5..977a3cf 100644 --- a/ly-ui/src/components/BigLabel.zig +++ b/ly-ui/src/components/BigLabel.zig @@ -19,6 +19,12 @@ pub const CHAR_SIZE = CHAR_WIDTH * CHAR_HEIGHT; pub const X: u32 = if (ly_core.interop.supportsUnicode()) 0x2593 else '#'; pub const O: u32 = 0; +const OUTLINE_DIRS = [_][2]i8{ + .{ -1, -1 }, .{ -1, 0 }, .{ -1, 1 }, + .{ 0, -1 }, .{ 0, 1 }, .{ 1, -1 }, + .{ 1, 0 }, .{ 1, 1 }, +}; + // zig fmt: off pub const LocaleChars = struct { ZERO: [CHAR_SIZE]u21, @@ -51,6 +57,7 @@ text: []const u8, max_width: ?usize, fg: u32, bg: u32, +outline_fg: ?u32 = null, locale: BigLabelLocale, update_fn: ?*const fn (*BigLabel, *anyopaque) anyerror!void, calculate_timeout_fn: ?*const fn (*BigLabel, *anyopaque) anyerror!?usize, @@ -63,6 +70,7 @@ pub fn init( max_width: ?usize, fg: u32, bg: u32, + outline_fg: ?u32, locale: BigLabelLocale, update_fn: ?*const fn (*BigLabel, *anyopaque) anyerror!void, calculate_timeout_fn: ?*const fn (*BigLabel, *anyopaque) anyerror!?usize, @@ -75,6 +83,7 @@ pub fn init( .max_width = max_width, .fg = fg, .bg = bg, + .outline_fg = outline_fg, .locale = locale, .update_fn = update_fn, .calculate_timeout_fn = calculate_timeout_fn, @@ -152,23 +161,71 @@ pub fn childrenPosition(self: BigLabel) Position { fn draw(self: *BigLabel) void { for (self.text, 0..) |c, i| { - const clock_cell = clockCell( - c, - self.fg, - self.bg, - self.locale, - ); + const x = self.component_pos.x + i * (CHAR_WIDTH + 1); + const y = self.component_pos.y; + + if (self.outline_fg) |outline_fg| { + drawDigitOutline(self, c, x, y, outline_fg); + } alphaBlit( - self.component_pos.x + i * (CHAR_WIDTH + 1), - self.component_pos.y, + x, + y, self.buffer.width, self.buffer.height, - clock_cell, + clockCell(c, self.fg, self.bg, self.locale), ); } } +fn drawDigitOutline( + self: *BigLabel, + char: u8, + base_x: usize, + base_y: usize, + outline_fg: u32, +) void { + const pattern = toBigNumber(char, self.locale); + const stroke = Cell.init(X, outline_fg, TerminalBuffer.Color.DEFAULT); + + for (0..CHAR_HEIGHT) |y| { + for (0..CHAR_WIDTH) |x| { + if (pattern[y * CHAR_WIDTH + x] == O) continue; + + for (OUTLINE_DIRS) |dir| { + if (offsetBy(x, dir[0])) |nx| { + if (offsetBy(y, dir[1])) |ny| { + if (nx < CHAR_WIDTH and ny < CHAR_HEIGHT and + pattern[ny * CHAR_WIDTH + nx] != O) + { + continue; + } + } + } + + const sx = offsetBy(base_x + x, dir[0]) orelse continue; + const sy = offsetBy(base_y + y, dir[1]) orelse continue; + if (sx >= self.buffer.width or + sy >= self.buffer.height) + { + continue; + } + + stroke.put(sx, sy) catch {}; + } + } + } +} + +fn offsetBy(pos: usize, delta: i8) ?usize { + if (delta < 0) { + const abs: usize = @intCast(-delta); + if (pos < abs) return null; + return pos - abs; + } + return pos + @as(usize, @intCast(delta)); +} + fn update(self: *BigLabel, context: *anyopaque) !void { if (self.update_fn) |update_fn| { return @call( diff --git a/res/config.ini b/res/config.ini index 2bd2624..55de99d 100644 --- a/res/config.ini +++ b/res/config.ini @@ -92,6 +92,9 @@ bigclock_12hr = false # Set bigclock to show the seconds. bigclock_seconds = false +# Bigclock digit outline color (null = none) +bigclock_outline_fg = null + # Blank main box background # Setting to false will make it transparent blank_box = true diff --git a/src/config/Config.zig b/src/config/Config.zig index 2ba8444..2f90a8b 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -21,6 +21,7 @@ bg: u32 = 0x00000000, bigclock: Bigclock = .none, bigclock_12hr: bool = false, bigclock_seconds: bool = false, +bigclock_outline_fg: ?u32 = null, blank_box: bool = true, border_fg: u32 = 0x00FFFFFF, box_position_h: f32 = 0.5, diff --git a/src/main.zig b/src/main.zig index 1d0db99..ca95467 100644 --- a/src/main.zig +++ b/src/main.zig @@ -559,6 +559,7 @@ pub fn main(init: std.process.Init) !void { null, state.buffer.fg, state.buffer.bg, + state.config.bigclock_outline_fg, switch (state.config.bigclock) { .none, .en => .en, .fa => .fa, @@ -2258,7 +2259,8 @@ fn positionWidgets(ptr: *anyopaque) !void { const clock_text_len = TerminalBuffer.strWidth(state.bigclock_label.text) * (BigLabel.CHAR_WIDTH + 1); if (state.config.bigclock != .none) { - bb_height += BigLabel.CHAR_HEIGHT + 2; + const gap: usize = if (state.config.bigclock_outline_fg != null) 3 else 2; + bb_height += BigLabel.CHAR_HEIGHT + gap; bb_width = @max(bb_width, clock_text_len); } From 0a29dc2fef502a524e9ef0a6f58aca4df7943d71 Mon Sep 17 00:00:00 2001 From: RadsammyT Date: Mon, 24 Aug 2026 15:32:16 +0200 Subject: [PATCH 09/26] feat(gameoflife): allow for variations to game of life (closes #1023) (#1041) ## What are the changes about? Allows the game of life animation to be ran with configurable rules, specifically: the neighbors required for birth and survival. ### Example: [B3/S12345 (Maze)](https://conwaylife.com/wiki/OCA:Maze) (where numbers after B are neighbors required for birth, and S for survival) Introduces two keys to the config: ```ini # Sets the multiple amounts of neighbors needed for a cell to survive. # Each numerical digit from 0 to 8 inclusive in this string becomes # one of the targets required for this cell to survive. # Example: a string of "23" makes the cell survive on 2 or 3 neighbors gameoflife_param_survival = "23" # Sets the multiple amounts of neighbors needed for a cell to be born. # Each numerical digit from 0 to 8 inclusive in this string becomes # one of the targets required for this cell to be born. # Example: a string of "3" makes the cell be born on 3 neighbors gameoflife_param_birth = "3" ``` ## What existing issue(s) does this resolve? Solves !1023 ## 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/1041 Reviewed-by: AnErrupTion --- res/config.ini | 12 ++++++++++++ src/animations/GameOfLife.zig | 26 +++++++++++++++++++++----- src/config/Config.zig | 2 ++ src/main.zig | 2 ++ 4 files changed, 37 insertions(+), 5 deletions(-) diff --git a/res/config.ini b/res/config.ini index 55de99d..9907610 100644 --- a/res/config.ini +++ b/res/config.ini @@ -290,6 +290,18 @@ gameoflife_frame_delay = 6 # 0.7+ -> Dense, chaotic patterns gameoflife_initial_density = 0.4 +# Sets the multiple amounts of neighbors needed for a cell to be born. +# Each numerical digit from 0 to 8 inclusive in this string becomes +# one of the targets required for this cell to be born. +# Example: a string of "3" makes the cell be born on 3 neighbors +gameoflife_param_birth = "3" + +# Sets the multiple amounts of neighbors needed for a cell to survive. +# Each numerical digit from 0 to 8 inclusive in this string becomes +# one of the targets required for this cell to survive. +# Example: a string of "23" makes the cell survive on 2 or 3 neighbors +gameoflife_param_survival = "23" + # Remove main box borders hide_borders = false diff --git a/src/animations/GameOfLife.zig b/src/animations/GameOfLife.zig index 6e9a16c..9541e78 100644 --- a/src/animations/GameOfLife.zig +++ b/src/animations/GameOfLife.zig @@ -39,6 +39,11 @@ animation_frame_delay: u16, dead_cell: Cell, width: usize, height: usize, +// Where N is the amount of neighbors, determines if the cell can survive/be born +// by checking if the Nth bit (where if N == 0, it is the least significant bit, +// or 2 to the power of 0) is set to 1. +cell_survival: u9, +cell_birth: u9, pub fn init( allocator: Allocator, @@ -50,6 +55,8 @@ pub fn init( animate: *bool, timeout_sec: u12, animation_frame_delay: u16, + string_cell_survival: []const u8, + string_cell_birth: []const u8, ) !GameOfLife { const width = terminal_buffer.width; const height = terminal_buffer.height; @@ -77,6 +84,16 @@ pub fn init( .dead_cell = .{ .ch = DEAD_CHAR, .fg = @intCast(TerminalBuffer.Color.DEFAULT), .bg = terminal_buffer.bg }, .width = width, .height = height, + .cell_birth = 0, + .cell_survival = 0, + }; + for (string_cell_survival) |c| switch (c) { + '0'...'8' => game.cell_survival |= (@as(u9, 1) << @truncate(@as(u9, c - '0'))), + else => {}, + }; + for (string_cell_birth) |c| switch (c) { + '0'...'8' => game.cell_birth |= (@as(u9, 1) << @truncate(@as(u9, c - '0'))), + else => {}, }; // Initialize grid @@ -173,11 +190,10 @@ fn updateGeneration(self: *GameOfLife) void { const is_alive = self.current_grid[index]; // Optimized rule application - self.next_grid[index] = switch (neighbors) { - 2 => is_alive, - 3 => true, - else => false, - }; + self.next_grid[index] = if (is_alive) + self.cell_survival & (@as(u9, 1) << @truncate(neighbors)) != 0 + else + self.cell_birth & (@as(u9, 1) << @truncate(neighbors)) != 0; } } diff --git a/src/config/Config.zig b/src/config/Config.zig index 2f90a8b..ff23cad 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -65,6 +65,8 @@ gameoflife_fg: u32 = 0x0000FF00, gameoflife_entropy_interval: usize = 10, gameoflife_frame_delay: usize = 6, gameoflife_initial_density: f32 = 0.4, +gameoflife_param_birth: []const u8 = "3", +gameoflife_param_survival: []const u8 = "23", hide_borders: bool = false, inactivity_cmd: ?[]const u8 = null, inactivity_delay: u16 = 0, diff --git a/src/main.zig b/src/main.zig index ca95467..7cd49fb 100644 --- a/src/main.zig +++ b/src/main.zig @@ -1064,6 +1064,8 @@ pub fn main(init: std.process.Init) !void { &state.animate, state.config.animation_timeout_sec, state.config.animation_frame_delay, + state.config.gameoflife_param_survival, + state.config.gameoflife_param_birth, ); animation = game_of_life.widget(); }, From 882815343fdc594baab25301344d573c002df459 Mon Sep 17 00:00:00 2001 From: urly3 Date: Mon, 24 Aug 2026 15:32:41 +0200 Subject: [PATCH 10/26] feat: per tty last user caching (closes #1040) (#1044) An update to #1042 ## What are the changes about? This no longer creates a new file and instead appends the new save data to the existing file in a backwards compatible way. It can differentiate by using a prefix to the string (which would usually be the username) of "ly/tty". Usernames on supported systems aren't able to include a forward slash, and if they could, the chances of those colliding.. The rest follows the last PR: Fix using saved user list where it shouldn't Now saves users that weren't previously in the save file (fixes TODO) Since we save only valid state, the save file is self-cleaning of bad/outdated data ## What existing issue(s) does this resolve? #1040 ## Pre-requisites - [ x ] I have tested & confirmed the changes work locally (freebsd, void linux) - [ 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/1044 Reviewed-by: AnErrupTion --- src/main.zig | 119 ++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 93 insertions(+), 26 deletions(-) diff --git a/src/main.zig b/src/main.zig index 7cd49fb..6a94ec7 100644 --- a/src/main.zig +++ b/src/main.zig @@ -126,6 +126,7 @@ const UiState = struct { bigclock_buf: [32:0]u8, custom_binds: std.ArrayList(CustomBindLabel), custom_info: std.ArrayList(CustomInfoLabel), + tty_cache: [std.math.maxInt(u8)]?u8, }; var shutdown = false; @@ -134,6 +135,7 @@ var restart = false; pub fn main(init: std.process.Init) !void { var state: UiState = undefined; + state.tty_cache = @splat(null); state.io = init.io; var stderr_buffer: [128]u8 = undefined; @@ -319,7 +321,8 @@ pub fn main(init: std.process.Init) !void { } while (reader.seek < reader.buffer.len) { - const line = reader.takeDelimiterInclusive('\n') catch break; + var line = reader.takeDelimiterInclusive('\n') catch break; + if (std.mem.startsWith(u8, line, "ly/tty")) continue; var user = std.mem.splitScalar(u8, line[0..(line.len - 1)], ':'); const username = user.next() orelse continue; @@ -334,19 +337,8 @@ pub fn main(init: std.process.Init) !void { .allocated_username = true, }); } - } - // If no save file previously existed, fill it up with all usernames - // TODO: Add new username with existing save file - 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, - .session_index = 0, - .first_run = true, - .allocated_username = false, - }); - } + updateTtyCache(&state, .{ .usernames = usernames.items }) catch break :read_save_file; } var log_file_buffer: [1024]u8 = undefined; @@ -1124,6 +1116,8 @@ 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; + const min_session_index = state.session.label.list.items.len - 1; + if (state.config.save_file_dir != null and !state.is_autologin) { if (state.login_text) |box| { if (state.saved_username) |username| { @@ -1135,16 +1129,23 @@ pub fn main(init: std.process.Init) !void { 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); + state.session.label.current = @min(user.session_index, min_session_index); 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; + } else if (state.tty_cache[state.active_tty]) |user_index| { + const user_session_index = state.login.?.label.list.items[user_index].session_index.*; - const user = state.saved_users.user_list.items[index]; + state.login.?.label.current = user_index; + state.session.label.current = @min(user_session_index, min_session_index); + } else if (state.saved_users.last_username_index) |last_user_index| load_last_user: { + const saved_users = state.saved_users.user_list.items; + + // If the saved index isn't valid, bail out + if (last_user_index >= saved_users.len) break :load_last_user; + + const user = saved_users[last_user_index]; // Find user with saved name, and switch over to it // If it doesn't exist (anymore), we don't change the value @@ -1157,7 +1158,7 @@ pub fn main(init: std.process.Init) !void { default_input = .password; - state.session.label.current = @min(user.session_index, state.session.label.list.items.len - 1); + state.session.label.current = @min(user.session_index, min_session_index); } } @@ -1519,28 +1520,51 @@ fn authenticate(ptr: *anyopaque) !bool { .{}, ) catch {}; - var file = std.Io.Dir.cwd().createFile(state.io, state.save_path, .{}) catch |err| { + const current: u8 = @intCast(state.login.?.label.current); + const login_users = state.login.?.label.list.items; + + // Try to update the local tty cache before overwriting, + // since multiple instances can be running + updateTtyCache(state, .{ .user_list = login_users }) catch |err| { state.log_file.err( state.io, "sys", - "failed to create save file: {s}", + "failed to update cache: {s}", .{@errorName(err)}, + ) catch {}; + }; + + var save_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} {s}", + .{ @errorName(err), state.save_path }, ) catch break :save_last_settings; break :save_last_settings; }; - defer file.close(state.io); + defer save_file.close(state.io); var file_buffer: [256]u8 = undefined; - var file_writer = file.writer(state.io, &file_buffer); + var file_writer = save_file.writer(state.io, &file_buffer); var writer = &file_writer.interface; if (state.login_text) |box| { try writer.print("0-{s}\n", .{box.text.items}); } else { - try writer.print("{d}\n", .{state.login.?.label.current}); + try writer.print("{d}\n", .{current}); } - for (state.saved_users.user_list.items) |user| { - try writer.print("{s}:{d}\n", .{ user.username, user.session_index }); + for (login_users) |user| { + try writer.print("{s}:{d}\n", .{ user.name, user.session_index.* }); + } + + state.tty_cache[state.active_tty] = current; + for (state.tty_cache, 0..) |maybe_user_index, tty_num| { + if (maybe_user_index) |user_index| { + // Posix usernames can't contain a '/' + // And, well, if your username is this string... + try writer.print("ly/tty{d}:{s}\n", .{ tty_num, login_users[user_index].name }); + } } try writer.flush(); @@ -2580,3 +2604,46 @@ fn getAuthErrorMsg(err: anyerror, lang: Lang) []const u8 { else => @errorName(err), }; } + +// Updates the UiState's tty_cache using the save file. +// Matched against the current truthful user list to ensure it's a valid index. +fn updateTtyCache(state: *UiState, real_users: union(enum) { user_list: []UserList.User, usernames: [][]const u8 }) !void { + var save_file = try std.Io.Dir.cwd().openFile(state.io, state.save_path, .{}); + defer save_file.close(state.io); + + var file_buffer: [256]u8 = undefined; + var file_reader = save_file.reader(state.io, &file_buffer); + var reader = &file_reader.interface; + + while (reader.seek < reader.buffer.len) { + var line = reader.takeDelimiterInclusive('\n') catch break; + if (!std.mem.startsWith(u8, line, "ly/tty")) continue; + + line = line["ly/tty".len..]; + var entry = std.mem.splitScalar(u8, line[0..(line.len - 1)], ':'); + const tty_num_str = entry.next() orelse continue; + const saved_username = entry.next() orelse continue; + + const tty_num = std.fmt.parseInt(usize, tty_num_str, 10) catch continue; + if (tty_num >= std.math.maxInt(u8)) continue; + + switch (real_users) { + .usernames => |usernames| { + for (usernames, 0..) |username, u_index| { + if (std.mem.eql(u8, username, saved_username)) { + state.tty_cache[tty_num] = @intCast(u_index); + break; + } + } + }, + .user_list => |user_list| { + for (user_list, 0..) |user, u_index| { + if (std.mem.eql(u8, user.name, saved_username)) { + state.tty_cache[tty_num] = @intCast(u_index); + break; + } + } + }, + } + } +} From b9742203ee2d56104e6f504317f57c220e3415a6 Mon Sep 17 00:00:00 2001 From: Titanium Brain Date: Mon, 24 Aug 2026 15:35:01 +0200 Subject: [PATCH 11/26] Use Lua as a config file language (#1010) ## What are the changes about? Adds zlua for running Lua code as the configuration file. Missing as of this time: - ~~Parsing custom binds and labels~~ - ~~Fix memory leaks~~ - ~~Investigate issue with animation colours~~ - ~~Add examples and default config~~ ## What existing issue does this resolve? [#976](https://codeberg.org/fairyglade/ly/issues/976) ## Pre-requisites - [ ] I have tested & confirmed the changes work locally - [ ] I have run `zig fmt` throughout my changes Reviewed-on: https://codeberg.org/fairyglade/ly/pulls/1010 Reviewed-by: AnErrupTion --- build.zig | 7 - build.zig.zon | 4 - install.zig | 6 +- ly-core/build.zig | 7 + ly-core/build.zig.zon | 4 + {src/config => ly-core/src}/custom.zig | 0 ly-core/src/root.zig | 326 +++++++++++++++++- readme.md | 6 +- res/config.lua | 453 +++++++++++++++++++++++++ src/animations/Lua.zig | 2 +- src/config/migrator.zig | 2 +- src/main.zig | 55 ++- 12 files changed, 833 insertions(+), 39 deletions(-) rename {src/config => ly-core/src}/custom.zig (100%) create mode 100644 res/config.lua diff --git a/build.zig b/build.zig index 8c604e1..6822e3c 100644 --- a/build.zig +++ b/build.zig @@ -102,13 +102,6 @@ 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, diff --git a/build.zig.zon b/build.zig.zon index a340291..f103091 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -11,10 +11,6 @@ .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/install.zig b/install.zig index b5649cd..29d2cdd 100644 --- a/install.zig +++ b/install.zig @@ -127,7 +127,7 @@ fn installLy(allocator: std.mem.Allocator, io: std.Io, patch_map: PatchMap, inst defer config_dir.close(io); if (install_config) { - const patched_config = try patchFile(allocator, io, "res/config.ini", patch_map); + const patched_config = try patchFile(allocator, io, "res/config.lua", patch_map); defer allocator.free(patched_config); try installText(io, patched_config, config_dir, ly_config_directory, "config.ini", .{}); @@ -135,10 +135,10 @@ fn installLy(allocator: std.mem.Allocator, io: std.Io, patch_map: PatchMap, inst 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); + const patched_example_config = try patchFile(allocator, io, "res/config.lua", patch_map); defer allocator.free(patched_example_config); - try installText(io, patched_example_config, config_dir, ly_config_directory, "config.ini.example", .{}); + try installText(io, patched_example_config, config_dir, ly_config_directory, "config.lua.example", .{}); const patched_setup = try patchFile(allocator, io, "res/setup.sh", patch_map); defer allocator.free(patched_setup); diff --git a/ly-core/build.zig b/ly-core/build.zig index f62fb2e..37b8668 100644 --- a/ly-core/build.zig +++ b/ly-core/build.zig @@ -21,6 +21,13 @@ pub fn build(b: *std.Build) void { const zigini = b.dependency("zigini", .{ .target = target, .optimize = optimize }); mod.addImport("zigini", zigini.module("zigini")); + const zlua = b.dependency("zlua", .{ + .target = target, + .optimize = optimize, + .lang = .luajit, + }); + mod.addImport("zlua", zlua.module("zlua")); + const translate_c = b.dependency("translate_c", .{ .target = target, }); diff --git a/ly-core/build.zig.zon b/ly-core/build.zig.zon index ddf0b9b..4896199 100644 --- a/ly-core/build.zig.zon +++ b/ly-core/build.zig.zon @@ -12,6 +12,10 @@ .url = "git+https://codeberg.org/ziglang/translate-c?ref=zig-0.16.x#6fe0ffc4549f15c5f2d9432c2b4460ba90ff85ac", .hash = "translate_c-1.0.0-Q_BUWo_5BgD4flHdUhA31zOz0XvZk9k7lQv1ouzyNXj2", }, + .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/src/config/custom.zig b/ly-core/src/custom.zig similarity index 100% rename from src/config/custom.zig rename to ly-core/src/custom.zig diff --git a/ly-core/src/root.zig b/ly-core/src/root.zig index 42198f5..c52d7d4 100644 --- a/ly-core/src/root.zig +++ b/ly-core/src/root.zig @@ -1,23 +1,57 @@ const std = @import("std"); pub const ini = @import("zigini"); +pub const zlua = @import("zlua"); +pub const Lua = zlua.Lua; 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 custom = @import("custom.zig"); + +pub fn Parser(comptime T: type) type { + return union(enum) { + ini: IniParser(T), + lua: LuaParser(T), + + pub fn errors(self: *const @This()) std.ArrayList(Error) { + return switch (self.*) { + inline else => |p| p.errors, + }; + } + + pub fn maybe_load_error(self: *const @This()) ?anyerror { + return switch (self.*) { + inline else => |p| p.maybe_load_error, + }; + } + pub fn structure(self: *const @This()) T { + return switch (self.*) { + inline else => |p| p.structure, + }; + } + + pub fn deinit(self: *@This()) void { + switch (self.*) { + inline else => |*p| p.deinit(), + } + } + }; +} + +pub const Error = struct { + type_name: []const u8, + key: []const u8, + value: []const u8, + error_name: []const u8, +}; 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), @@ -76,3 +110,283 @@ pub fn IniParser(comptime Struct: type) type { } }; } + +pub fn LuaParser(comptime Struct: type) type { + return struct { + const Self = @This(); + const temporary_allocator = std.heap.page_allocator; + + pub var global_errors: std.ArrayList(Error) = .empty; + + structure: Struct, + errors: std.ArrayList(Error), + maybe_load_error: ?anyerror, + allocator: std.mem.Allocator, + arena: std.heap.ArenaAllocator, + + pub fn init( + allocator: std.mem.Allocator, + path: []const u8, + ) !Self { + var arena = std.heap.ArenaAllocator.init(allocator); + const arena_alloc = arena.allocator(); + + var maybe_load_error: ?anyerror = null; + errdefer |err| maybe_load_error = err; + + const data = parseLua(arena_alloc, path) catch load_error: { + break :load_error Struct{}; + }; + + if (global_errors.items.len != 0) { + maybe_load_error = error.InvalidConfig; + } + + return .{ + .structure = data, + .errors = global_errors, + .maybe_load_error = maybe_load_error, + .allocator = allocator, + .arena = arena, + }; + } + + fn parseLua( + allocator: std.mem.Allocator, + path: []const u8, + ) !Struct { + var lua: *Lua = try .init(allocator); + defer lua.deinit(); + + lua.openBase(); + lua.openBit(); + lua.openMath(); + lua.openString(); + lua.openTable(); + + // convert to sentinel terminated slice + const spath: [:0]const u8 = try allocator.dupeSentinel(u8, path, 0); + defer allocator.free(spath); + lua.doFile(spath) catch return error.LuaError; + + var data: Struct = .{}; + switch (@typeInfo(Struct)) { + .@"struct" => |struc| { + const ly_type = lua.getGlobal("ly"); + defer lua.pop(1); // pop ly table + if (ly_type == .nil) return error.MissingLyTable; + + inline for (struc.fields) |field| { + try setField(allocator, lua, field, &data); + } + }, + else => @compileError("Expected a struct."), + } + + // Parse custom binds and labels + try parseCustom(lua); + return data; + } + + pub fn setField(allocator: std.mem.Allocator, lua: *Lua, comptime field: std.builtin.Type.StructField, data: *Struct) !void { + const type_info = @typeInfo(field.type); + const actual_type, const is_optional = blk: { + if (type_info == .optional) { + break :blk .{ type_info.optional.child, true }; + } + break :blk .{ field.type, false }; + }; + // push value to top of stack + _ = lua.getField(-1, field.name); + defer lua.pop(1); + + // handle null, i.e. undefined fields + if (is_optional and lua.isNil(-1)) { + @field(data, field.name) = null; + return; + } + + // handle missing required fields + if (lua.isNil(-1)) { + return error.MissingRequiredField; + } + const actual_type_info = @typeInfo(actual_type); + + errdefer |err| { + const value = lua.toString(-1) catch ""; + const duped = allocator.dupe(u8, value) catch ""; + errorHandler(@typeName(field.type), field.name, duped, err); + } + + // dispatch depending on type + if (actual_type_info == .int and is_optional) { + if (lua.isNumber(-1)) { + const value = try lua.toNumber(-1); + @field(data, field.name) = @trunc(value); + } else { + const str = try lua.toString(-1); + + var view = try std.unicode.Utf8View.init(str); + var iter = view.iterator(); + + const codepoint = iter.nextCodepoint(); + + if (iter.nextCodepoint() != null) return error.ExpectedSingleCharacter; + + @field(data, field.name) = if (codepoint) |cp| @intCast(cp) else null; + } + // non null integer + } else if (actual_type_info == .int) { + if (lua.isNumber(-1)) { + const value = try lua.toNumber(-1); + @field(data, field.name) = @trunc(value); + } else { + const str = try lua.toString(-1); + + var view = try std.unicode.Utf8View.init(str); + var iter = view.iterator(); + + const codepoint = iter.nextCodepoint() orelse return error.EmptyString; + + if (iter.nextCodepoint() != null) return error.ExpectedSingleCharacter; + + @field(data, field.name) = @intCast(codepoint); + } + } else if (actual_type_info == .float) { // all floats + const value = try lua.toNumber(-1); + @field(data, field.name) = @floatCast(value); + } else if (actual_type_info == .bool) { + if (!lua.isBoolean(-1)) return error.ExpectedBoolean; + const value = lua.toBoolean(-1); + @field(data, field.name) = value; + } else if (actual_type == []const u8) { + const value = try lua.toString(-1); + const duped = try allocator.dupe(u8, value); + @field(data, field.name) = duped; + } else if (actual_type == [:0]const u8) { + const value = try lua.toString(-1); + const duped = try allocator.dupeSentinel(u8, value, 0); + @field(data, field.name) = duped; + } else if (actual_type_info == .@"enum") { + const value = try lua.toString(-1); + const variant = std.meta.stringToEnum(actual_type, value) orelse return error.InvalidVariant; + @field(data, field.name) = variant; + } else unreachable; + } + + pub fn parseCustom(lua: *Lua) !void { + _ = lua.getGlobal("ly"); + defer lua.pop(1); // pop ly table + if (!lua.isTable(-1)) return error.MissingLyTable; + + _ = lua.getField(-1, "custom_commands"); + // custom_commands can be omitted or empty, so we just skip instead of erroring + + if (lua.isTable(-1)) binds: { + const len: usize = @intCast(lua.objectLen(-1)); + if (len == 0) break :binds; + + for (1..len + 1) |i| { + // push i-th table to stack + lua.pushInteger(@intCast(i)); + const ith_table_type = lua.getTable(-2); + defer lua.pop(1); // i-th table in custom_commands + if (ith_table_type != .table) continue; + + // skip command if binding isn't set or not a string + const binding_type = lua.getField(-1, "binding"); + if (binding_type != .string) continue; + const binding = lua.toString(-1) catch continue; + const bindingZ = temporary_allocator.dupe(u8, binding) catch ""; + std.debug.print("{s}", .{bindingZ}); + lua.pop(1); // binding value + + if (!custom.binds.contains(bindingZ)) { + custom.binds.put(temporary_allocator, bindingZ, .{}) catch {}; + } + if (custom.binds.getPtr(bindingZ)) |command| { + // binding name + const name_type = lua.getField(-1, "name"); + if (name_type != .string) continue; + const binding_name = lua.toString(-1) catch ""; + command.name = temporary_allocator.dupe(u8, binding_name) catch ""; + lua.pop(1); // name value + + // binding command + const cmd_type = lua.getField(-1, "cmd"); + if (cmd_type != .string) continue; + const binding_cmd = lua.toString(-1) catch ""; + command.cmd = temporary_allocator.dupe(u8, binding_cmd) catch ""; + lua.pop(1); // cmd value + } + } + } + lua.pop(1); + + _ = lua.getField(-1, "custom_labels"); + // custom_labels can be omitted, so we just skip instead of erroring + + if (lua.isTable(-1)) labels: { + const len: usize = @intCast(lua.objectLen(-1)); + if (len == 0) break :labels; + + for (1..len + 1) |i| { + // push i-th table to stack + lua.pushInteger(@intCast(i)); + const ith_table_type = lua.getTable(-2); + defer lua.pop(1); // i-th table in custom_labels + if (ith_table_type != .table) continue; + + // skip command if binding isn't set or not a string + const label_type = lua.getField(-1, "label"); + if (label_type != .string) continue; + const label = lua.toString(-1) catch continue; + const labelZ = temporary_allocator.dupe(u8, label) catch ""; + lua.pop(1); // label value + + if (!custom.labels.contains(labelZ)) { + custom.labels.put(temporary_allocator, labelZ, .{ .name = labelZ }) catch {}; + } + if (custom.labels.getPtr(labelZ)) |label_ptr| { + // label command + const cmd_type = lua.getField(-1, "cmd"); + if (cmd_type != .string) continue; + const label_cmd = lua.toString(-1) catch ""; + label_ptr.cmd = temporary_allocator.dupe(u8, label_cmd) catch ""; + lua.pop(1); // cmd value + + // label refresh + const name_type = lua.getField(-1, "refresh"); + if (name_type != .number) continue; + const label_refresh: u32 = @intCast(lua.toInteger(-1) catch 0); + label_ptr.refresh = label_refresh; + lua.pop(1); // name value + } + } + } + + lua.pop(1); + } + + pub fn deinit(self: *Self) void { + self.arena.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/readme.md b/readme.md index 8cacd1a..51368db 100644 --- a/readme.md +++ b/readme.md @@ -249,12 +249,14 @@ You can, of course, still select the init system of your choice when using this ## 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.lua`. The file is fully commented, and includes the default values. + +It uses the Lua language, which means you can make the configuration dynamic. You could, for example, choose a random animation each time Ly starts up. 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 +$ ly --validate-config /etc/ly/config.lua ``` ## Controls diff --git a/res/config.lua b/res/config.lua new file mode 100644 index 0000000..1449149 --- /dev/null +++ b/res/config.lua @@ -0,0 +1,453 @@ +ly = { + -- 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: + -- 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 + -- 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 + -- 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) + -- lua -> user-made animation written in LuaJIT + 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 + 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 = '*', + + -- The number of failed authentications before a special animation is played... ;) + -- If set to 0, the animation will never be played + auth_fails = 10, + + -- 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, 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 = nil, + + -- Username to automatically log in + -- Must be a valid user on the system + -- If null, automatic login is disabled + auto_login_user = nil, + + -- 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 = nil, + + -- Background color id + bg = 0x00000000, + + -- Change the state and language of the big clock + -- none -> Disabled (default) + -- en -> English + -- fa -> Farsi + bigclock = "none", + + -- 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 + 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.5 + box_position_v = 0.5, + + -- Title to show at the top of the main box + -- If set to null, none will be shown + box_title = nil, + + -- Brightness decrease command + brightness_down_cmd = "$PREFIX_DIRECTORY/bin/brightnessctl -q -n s 10%-", + + -- 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 + -- If null, the keybind is disabled and isn't shown + brightness_up_key = "F6", + + -- 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 = nil, + + -- 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, + + -- 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, + + -- Color mixing animation second color id + colormix_col2 = 0x000000FF, + + -- Color mixing animation third color id + colormix_col3 = 0x20000000, + + -- Screen corners customization + -- Keywords: + -- 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 + -- 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 + -- + -- 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 + -- It is possible to have both horizontal and vertical items on the same corner + + -- Bottom left + corner_bottom_left = "version", + + -- Bottom right + corner_bottom_right = "labels", + + -- Top left + corner_top_left = "shutdown,restart,britup,britdown,password battery", + + -- Top right + 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. + -- If null, defaults to the width of the terminal instead. + custom_bind_width = nil, + + -- Custom sessions directory + -- You can specify multiple directories, + -- e.g. $CONFIG_DIRECTORY/ly/custom-sessions:$PREFIX_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 + default_input = "login", + + -- DOOM animation fire height (1 thru 9) + doom_fire_height = 6, + + -- DOOM animation fire spread (0 thru 4) + doom_fire_spread = 2, + + -- DOOM animation custom top color (low intensity flames) + doom_top_color = 0x009F2707, + + -- DOOM animation custom middle color (medium intensity flames) + 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 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 (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) + edge_margin = 0, + + -- Error background color id + error_bg = 0x00000000, + + -- Error foreground color id + -- Default is red and bold + 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. + -- 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), + -- 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, + + -- Remove main box borders + hide_borders = false, + + -- Command executed when no input is detected for a certain time + -- If null, no command will be executed + inactivity_cmd = nil, + + -- 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 = nil, + + -- Input boxes length + input_len = 34, + + -- Active language + -- Available languages are found in $CONFIG_DIRECTORY/ly/lang/ + lang = "en", + + -- 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 = nil, + + -- 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 + -- 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 = nil, + + -- 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", + + -- Main box horizontal margin + margin_box_h = 2, + + -- Main box vertical margin + margin_box_v = 1, + + -- Set numlock on/off at startup + numlock = false, + + -- Default path + -- If null, ly doesn't set a path + 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", + + -- 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", + + -- 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, 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", + + -- 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 + -- 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) + -- 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, + + -- 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 + vi_default_mode = "normal", + + -- Enable vi keybindings + 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 + -- Add the -quiet argument to hide startup logs from the server + 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 = nil, + + -- Xorg xauthority edition tool + xauth_cmd = "$PREFIX_DIRECTORY/bin/xauth", + + -- xinitrc + -- If null, the xinitrc session will be hidden + 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: + -- 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. + --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, + --# 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, +} diff --git a/src/animations/Lua.zig b/src/animations/Lua.zig index 2d2956f..bb4d406 100644 --- a/src/animations/Lua.zig +++ b/src/animations/Lua.zig @@ -8,7 +8,7 @@ const Allocator = std.mem.Allocator; const InfoLine = @import("../components/InfoLine.zig"); const Lang = @import("../config/Lang.zig"); -const zlua = @import("zlua"); +const zlua = ly_ui.ly_core.zlua; const ly_lua = @embedFile("ly.lua"); diff --git a/src/config/migrator.zig b/src/config/migrator.zig index 4ed4d2e..fc25768 100644 --- a/src/config/migrator.zig +++ b/src/config/migrator.zig @@ -18,7 +18,7 @@ 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"); +const custom = ly_core.custom; const color_properties = [_][]const u8{ "bg", diff --git a/src/main.zig b/src/main.zig index 6a94ec7..9508c41 100644 --- a/src/main.zig +++ b/src/main.zig @@ -19,9 +19,12 @@ const interop = ly_core.interop; const UidRange = ly_core.UidRange; const LogFile = ly_core.LogFile; const SharedError = ly_core.SharedError; +const Parser = ly_core.Parser; const IniParser = ly_core.IniParser; +const LuaParser = ly_core.LuaParser; const ini = ly_core.ini; const Ini = ini.Ini; +const custom = ly_core.custom; const Cascade = @import("animations/Cascade.zig"); const ColorMix = @import("animations/ColorMix.zig"); @@ -39,7 +42,6 @@ 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; @@ -203,22 +205,28 @@ 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, - ); + var parser: Parser(Config) = blk: { + if (std.mem.endsWith(u8, path, ".ini")) { + break :blk .{ .ini = try IniParser(Config).init( + state.allocator, + state.io, + path, + migrator.configFieldHandler, + ) }; + } else { + break :blk .{ .lua = try LuaParser(Config).init(state.allocator, path) }; + } + }; defer parser.deinit(); - for (parser.errors.items) |err| { + 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| { + if (parser.maybe_load_error()) |err| { std.log.err("failed to load config file: {s}", .{@errorName(err)}); std.process.exit(1); } @@ -234,12 +242,29 @@ pub fn main(init: std.process.Init) !void { state.allocator.free(state.old_save_path); }; - const config_path = try std.Io.Dir.path.join(state.allocator, &[_][]const u8{ config_parent_path, "config.ini" }); + // Test for presence of Lua config file first + // If it fails, fall back to ini + var config_path = try std.Io.Dir.path.join(state.allocator, &[_][]const u8{ config_parent_path, "config.lua" }); + std.Io.Dir.accessAbsolute(state.io, config_path, .{}) catch { + state.allocator.free(config_path); + 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 = .empty; custom.labels = .empty; - var config_parser = try IniParser(Config).init(state.allocator, state.io, config_path, migrator.configFieldHandler); + var config_parser: Parser(Config) = blk: { + if (std.mem.endsWith(u8, config_path, ".ini")) { + break :blk .{ .ini = try IniParser(Config).init( + state.allocator, + state.io, + config_path, + migrator.configFieldHandler, + ) }; + } else { + break :blk .{ .lua = try LuaParser(Config).init(state.allocator, config_path) }; + } + }; defer config_parser.deinit(); defer if (!shutdown or !restart) { var iter = custom.binds.iterator(); @@ -258,7 +283,7 @@ pub fn main(init: std.process.Init) !void { custom.labels.deinit(temporary_allocator); }; - state.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", .{state.config.lang}); @@ -276,7 +301,7 @@ pub fn main(init: std.process.Init) !void { state.old_save_path = try std.Io.Dir.path.join(state.allocator, &[_][]const u8{ config_parent_path, "save.ini" }); } - if (config_parser.maybe_load_error == null) { + if (config_parser.maybe_load_error() == null and config_parser == .ini) { migrator.lateConfigFieldHandler(&state.config, state.lang); } @@ -636,7 +661,7 @@ pub fn main(init: std.process.Init) !void { ); } - if (config_parser.maybe_load_error) |load_error| { + 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 state.info_line.addMessage( "unable to parse config file", @@ -650,7 +675,7 @@ pub fn main(init: std.process.Init) !void { .{@errorName(load_error)}, ); - for (config_parser.errors.items) |err| { + for (config_parser.errors().items) |err| { try state.log_file.err( state.io, "conf", From 4e7a599c8adc70949f5310bbdef0068d6ac8c2f1 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Mon, 24 Aug 2026 15:44:21 +0200 Subject: [PATCH 12/26] config: Rename `dur_file` to `dur` (closes #1032) Signed-off-by: AnErrupTion --- src/config/migrator.zig | 27 +++++++++++++++++++-------- src/enums.zig | 2 +- src/main.zig | 2 +- 3 files changed, 21 insertions(+), 10 deletions(-) diff --git a/src/config/migrator.zig b/src/config/migrator.zig index fc25768..9c3fc10 100644 --- a/src/config/migrator.zig +++ b/src/config/migrator.zig @@ -74,16 +74,27 @@ pub fn configFieldHandler(_: std.mem.Allocator, field: ini.IniField) ?ini.IniFie } 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; + string_conversion: { + // 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 break :string_conversion; + var mapped_field = field; + + mapped_field.value = switch (animation) { + 0 => "doom", + 1 => "matrix", + else => "none", + }; + + return mapped_field; + } + + // 'dur_file' was renamed to 'dur' var mapped_field = field; - mapped_field.value = switch (animation) { - 0 => "doom", - 1 => "matrix", - else => "none", - }; + if (std.mem.eql(u8, mapped_field.value, "dur_file")) { + mapped_field.value = "dur"; + } return mapped_field; } diff --git a/src/enums.zig b/src/enums.zig index ff69f84..5208f09 100644 --- a/src/enums.zig +++ b/src/enums.zig @@ -6,7 +6,7 @@ pub const Animation = enum { matrix, colormix, gameoflife, - dur_file, + dur, lua, }; diff --git a/src/main.zig b/src/main.zig index 9508c41..811ca25 100644 --- a/src/main.zig +++ b/src/main.zig @@ -1086,7 +1086,7 @@ pub fn main(init: std.process.Init) !void { ); animation = game_of_life.widget(); }, - .dur_file => { + .dur => { var dur = try DurFile.init( state.allocator, state.io, From 76a5f73ee54cf842414fa986555029b12a1db854 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Mon, 24 Aug 2026 16:09:10 +0200 Subject: [PATCH 13/26] config: Update config.lua Signed-off-by: AnErrupTion --- res/config.lua | 905 +++++++++++++++++++++++++------------------------ 1 file changed, 454 insertions(+), 451 deletions(-) diff --git a/res/config.lua b/res/config.lua index 1449149..d989ffa 100644 --- a/res/config.lua +++ b/res/config.lua @@ -1,453 +1,456 @@ ly = { - -- 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: - -- 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 - -- 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 - -- 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) - -- lua -> user-made animation written in LuaJIT - 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 - 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 = '*', - - -- The number of failed authentications before a special animation is played... ;) - -- If set to 0, the animation will never be played - auth_fails = 10, - - -- 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, 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 = nil, - - -- Username to automatically log in - -- Must be a valid user on the system - -- If null, automatic login is disabled - auto_login_user = nil, - - -- 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 = nil, - - -- Background color id - bg = 0x00000000, - - -- Change the state and language of the big clock - -- none -> Disabled (default) - -- en -> English - -- fa -> Farsi - bigclock = "none", - - -- 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 - 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.5 - box_position_v = 0.5, - - -- Title to show at the top of the main box - -- If set to null, none will be shown - box_title = nil, - - -- Brightness decrease command - brightness_down_cmd = "$PREFIX_DIRECTORY/bin/brightnessctl -q -n s 10%-", - - -- 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 - -- If null, the keybind is disabled and isn't shown - brightness_up_key = "F6", - - -- 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 = nil, - - -- 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, - - -- 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, - - -- Color mixing animation second color id - colormix_col2 = 0x000000FF, - - -- Color mixing animation third color id - colormix_col3 = 0x20000000, - - -- Screen corners customization - -- Keywords: - -- 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 - -- 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 - -- - -- 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 - -- It is possible to have both horizontal and vertical items on the same corner - - -- Bottom left - corner_bottom_left = "version", - - -- Bottom right - corner_bottom_right = "labels", - - -- Top left - corner_top_left = "shutdown,restart,britup,britdown,password battery", - - -- Top right - 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. - -- If null, defaults to the width of the terminal instead. - custom_bind_width = nil, - - -- Custom sessions directory - -- You can specify multiple directories, - -- e.g. $CONFIG_DIRECTORY/ly/custom-sessions:$PREFIX_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 - default_input = "login", - - -- DOOM animation fire height (1 thru 9) - doom_fire_height = 6, - - -- DOOM animation fire spread (0 thru 4) - doom_fire_spread = 2, - - -- DOOM animation custom top color (low intensity flames) - doom_top_color = 0x009F2707, - - -- DOOM animation custom middle color (medium intensity flames) - 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 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 (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) - edge_margin = 0, - - -- Error background color id - error_bg = 0x00000000, - - -- Error foreground color id - -- Default is red and bold - 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. - -- 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), - -- 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, - - -- Remove main box borders - hide_borders = false, - - -- Command executed when no input is detected for a certain time - -- If null, no command will be executed - inactivity_cmd = nil, - - -- 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 = nil, - - -- Input boxes length - input_len = 34, - - -- Active language - -- Available languages are found in $CONFIG_DIRECTORY/ly/lang/ - lang = "en", - - -- 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 = nil, - - -- 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 - -- 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 = nil, - - -- 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", - - -- Main box horizontal margin - margin_box_h = 2, - - -- Main box vertical margin - margin_box_v = 1, - - -- Set numlock on/off at startup - numlock = false, - - -- Default path - -- If null, ly doesn't set a path - 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", - - -- 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", - - -- 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, 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", - - -- 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 - -- 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) - -- 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, - - -- 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 - vi_default_mode = "normal", - - -- Enable vi keybindings - 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 - -- Add the -quiet argument to hide startup logs from the server - 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 = nil, - - -- Xorg xauthority edition tool - xauth_cmd = "$PREFIX_DIRECTORY/bin/xauth", - - -- xinitrc - -- If null, the xinitrc session will be hidden - 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: - -- 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. - --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, - --# 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, + -- 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: + -- 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 + -- 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 + -- 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) + -- lua -> user-made animation written in LuaJIT + 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 + 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 = '*', + + -- The number of failed authentications before a special animation is played... ;) + -- If set to 0, the animation will never be played + auth_fails = 10, + + -- 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, 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 = nil, + + -- Username to automatically log in + -- Must be a valid user on the system + -- If null, automatic login is disabled + auto_login_user = nil, + + -- 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 = nil, + + -- Background color id + bg = 0x00000000, + + -- Change the state and language of the big clock + -- none -> Disabled (default) + -- en -> English + -- fa -> Farsi + bigclock = "none", + + -- 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 + 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.5 + box_position_v = 0.5, + + -- Title to show at the top of the main box + -- If set to null, none will be shown + box_title = nil, + + -- Brightness decrease command + brightness_down_cmd = "$PREFIX_DIRECTORY/bin/brightnessctl -q -n s 10%-", + + -- 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 + -- If null, the keybind is disabled and isn't shown + brightness_up_key = "F6", + + -- 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 = nil, + + -- 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, + + -- 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, + + -- Color mixing animation second color id + colormix_col2 = 0x000000FF, + + -- Color mixing animation third color id + colormix_col3 = 0x20000000, + + -- Screen corners customization + -- Keywords: + -- 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 + -- 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 + -- + -- 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 + -- It is possible to have both horizontal and vertical items on the same corner + + -- Bottom left + corner_bottom_left = "version", + + -- Bottom right + corner_bottom_right = "labels", + + -- Top left + corner_top_left = "shutdown,restart,britup,britdown,password battery", + + -- Top right + 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. + -- If null, defaults to the width of the terminal instead. + custom_bind_width = nil, + + -- Custom sessions directory + -- You can specify multiple directories, + -- e.g. $CONFIG_DIRECTORY/ly/custom-sessions:$PREFIX_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 + default_input = "login", + + -- DOOM animation fire height (1 thru 9) + doom_fire_height = 6, + + -- DOOM animation fire spread (0 thru 4) + doom_fire_spread = 2, + + -- DOOM animation custom top color (low intensity flames) + doom_top_color = 0x009F2707, + + -- DOOM animation custom middle color (medium intensity flames) + 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 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 (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) + edge_margin = 0, + + -- Error background color id + error_bg = 0x00000000, + + -- Error foreground color id + -- Default is red and bold + 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. + -- 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), + -- 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, + + -- Remove main box borders + hide_borders = false, + + -- Command executed when no input is detected for a certain time + -- If null, no command will be executed + inactivity_cmd = nil, + + -- 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 = nil, + + -- Input boxes length + input_len = 34, + + -- Active language + -- Available languages are found in $CONFIG_DIRECTORY/ly/lang/ + lang = "en", + + -- 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 = nil, + + -- 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 + -- 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 = nil, + + -- 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", + + -- Main box horizontal margin + margin_box_h = 2, + + -- Main box vertical margin + margin_box_v = 1, + + -- Set numlock on/off at startup + numlock = false, + + -- Default path + -- If null, ly doesn't set a path + 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", + + -- 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", + + -- 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, 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", + + -- 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 + -- 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) + -- 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, + + -- 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 + vi_default_mode = "normal", + + -- Enable vi keybindings + 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 + -- Add the -quiet argument to hide startup logs from the server + 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 = nil, + + -- Xorg xauthority edition tool + xauth_cmd = "$PREFIX_DIRECTORY/bin/xauth", + + -- xinitrc + -- If null, the xinitrc session will be hidden + 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: + -- 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 code. + + -- custom_commands = { + -- -- Declare a command with the F8 binding. + -- binding = "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", + -- }, + + -- custom_labels = { + -- -- Declare a label with an ID. This ID should be unique across all labels. + -- label = "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, + -- } } From 5618446377cc3f9e39adfbb75ab2b7b750635cdd Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Mon, 24 Aug 2026 16:11:57 +0200 Subject: [PATCH 14/26] systemd: Use agetty from sbin (closes #1045) Signed-off-by: AnErrupTion --- res/ly@.service | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/res/ly@.service b/res/ly@.service index b2fb5ff..6d5c424 100644 --- a/res/ly@.service +++ b/res/ly@.service @@ -15,7 +15,7 @@ After=systemd-user-sessions.service plymouth-quit-wait.service 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} +ExecStart=$PREFIX_DIRECTORY/sbin/agetty -nl $PREFIX_DIRECTORY/bin/$EXECUTABLE_NAME %I ${TERM} Type=idle Restart=always RestartSec=0 From b68a41b1ea74cdc08176e4b35e892de5b2a27dcc Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Mon, 24 Aug 2026 16:12:48 +0200 Subject: [PATCH 15/26] sysvinit: Use $PREFIX_DIRECTORY Signed-off-by: AnErrupTion --- res/ly-sysvinit | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/res/ly-sysvinit b/res/ly-sysvinit index f24dfd4..28791a3 100755 --- a/res/ly-sysvinit +++ b/res/ly-sysvinit @@ -13,7 +13,7 @@ # PATH=/sbin:/usr/sbin:/bin:/usr/bin -DAEMON=/usr/bin/ly +DAEMON=$PREFIX_DIRECTORY/bin/ly TTY=/dev/tty$DEFAULT_TTY PIDFILE=/var/run/ly.pid NAME=ly From 6ef332d73e29bedeb673103881ccb942c9433d06 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Mon, 24 Aug 2026 16:41:28 +0200 Subject: [PATCH 16/26] config: Add getRandomColor(), fix crash (closes #791) Signed-off-by: AnErrupTion --- res/config.lua | 32 ++++++++++++++++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/res/config.lua b/res/config.lua index d989ffa..c0ff749 100644 --- a/res/config.lua +++ b/res/config.lua @@ -1,3 +1,19 @@ +-- This is an example function you can use on any option supporting colors +-- to use a random color instead. +function getRandomColor() + math.randomseed() + + local r = math.random(0, 255) + local g = math.random(0, 255) + local b = math.random(0, 255) + + local col = b + col = bit.bor(col, bit.lshift(g, 8)) + col = bit.bor(col, bit.lshift(r, 16)) + + return col +end + ly = { -- 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. @@ -10,8 +26,8 @@ ly = { -- 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. + -- Programmatically, you'd apply them using the bitwise OR operator (|), and + -- in Lua, you can use the bit.bor(x, y) function to compute x | y. -- 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). @@ -288,6 +304,18 @@ ly = { -- 0.7+ -> Dense, chaotic patterns gameoflife_initial_density = 0.4, + -- Sets the multiple amounts of neighbors needed for a cell to be born. + -- Each numerical digit from 0 to 8 inclusive in this string becomes + -- one of the targets required for this cell to be born. + -- Example: a string of "3" makes the cell be born on 3 neighbors + gameoflife_param_birth = "3", + + -- Sets the multiple amounts of neighbors needed for a cell to survive. + -- Each numerical digit from 0 to 8 inclusive in this string becomes + -- one of the targets required for this cell to survive. + -- Example: a string of "23" makes the cell survive on 2 or 3 neighbors + gameoflife_param_survival = "23", + -- Remove main box borders hide_borders = false, From 1117ef5a3bde630cecd6821d42400bbce8f48f3c Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Mon, 24 Aug 2026 16:50:29 +0200 Subject: [PATCH 17/26] Reference Lua config + fix install bug Signed-off-by: AnErrupTion --- .github/ISSUE_TEMPLATE/bug.yml | 2 +- install.zig | 13 +++++-------- readme.md | 2 +- res/example.lua | 2 +- src/main.zig | 2 +- 5 files changed, 9 insertions(+), 12 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug.yml b/.github/ISSUE_TEMPLATE/bug.yml index 64eef9c..d9458ae 100644 --- a/.github/ISSUE_TEMPLATE/bug.yml +++ b/.github/ISSUE_TEMPLATE/bug.yml @@ -73,7 +73,7 @@ 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. The log files (located as specified by `/etc/ly/config.ini`) usually contain 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.lua` or `/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 diff --git a/install.zig b/install.zig index 29d2cdd..5489f3c 100644 --- a/install.zig +++ b/install.zig @@ -126,19 +126,16 @@ fn installLy(allocator: std.mem.Allocator, io: std.Io, patch_map: PatchMap, inst 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.lua", patch_map); - defer allocator.free(patched_config); + const patched_config = try patchFile(allocator, io, "res/config.lua", patch_map); + defer allocator.free(patched_config); - try installText(io, patched_config, config_dir, ly_config_directory, "config.ini", .{}); + if (install_config) { + try installText(io, patched_config, config_dir, ly_config_directory, "config.lua", .{}); 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.lua", patch_map); - defer allocator.free(patched_example_config); - - try installText(io, patched_example_config, config_dir, ly_config_directory, "config.lua.example", .{}); + try installText(io, patched_config, config_dir, ly_config_directory, "config.lua.example", .{}); const patched_setup = try patchFile(allocator, io, "res/setup.sh", patch_map); defer allocator.free(patched_setup); diff --git a/readme.md b/readme.md index 51368db..6073179 100644 --- a/readme.md +++ b/readme.md @@ -97,7 +97,7 @@ Every environment that works on other login managers also should work on Ly. - If Xorg sessions don't work then check if your distro compiles Ly with Xorg. -Logs are defined by `/etc/ly/config.ini`: +Logs are defined by `/etc/ly/config.lua` or `/etc/ly/config.ini`: - The session log is located at `~/.local/state/ly-session.log` by default. diff --git a/res/example.lua b/res/example.lua index 81f9bc9..ac5fb87 100644 --- a/res/example.lua +++ b/res/example.lua @@ -18,7 +18,7 @@ -- -- For arguments fg and bg: they are colors in the format -- 0xSSRRGGBB, where SS is for styling. See your --- config.ini for more details. +-- config.ini or config.lua for more details. -- -- For the byte argument, you may use string.byte to fill this argument. -- diff --git a/src/main.zig b/src/main.zig index 811ca25..d5f728a 100644 --- a/src/main.zig +++ b/src/main.zig @@ -195,7 +195,7 @@ pub fn main(init: std.process.Init) !void { if (res.args.help != 0) { try clap.help(stderr, clap.Help, ¶ms, .{}); - 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.log.info("note: if you want to configure Ly, please check the config file, which is located at " ++ build_options.config_directory ++ "/ly/config.lua or " ++ build_options.config_directory ++ "/ly/config.ini.", .{}); std.process.exit(0); } if (res.args.version != 0) { From 0a0d6704c8f2f68089bcc6e4e67c9787dfbd3230 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Mon, 24 Aug 2026 16:51:01 +0200 Subject: [PATCH 18/26] Delete config.ini Signed-off-by: AnErrupTion --- res/config.ini | 466 ------------------------------------------------- 1 file changed, 466 deletions(-) delete mode 100644 res/config.ini diff --git a/res/config.ini b/res/config.ini deleted file mode 100644 index 9907610..0000000 --- a/res/config.ini +++ /dev/null @@ -1,466 +0,0 @@ -# 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: -# 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 -# 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 -# 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) -# lua -> user-made animation written in LuaJIT -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 -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 = * - -# The number of failed authentications before a special animation is played... ;) -# If set to 0, the animation will never be played -auth_fails = 10 - -# 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, 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 - -# Username to automatically log in -# Must be a valid user on the system -# 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 - -# Change the state and language of the big clock -# none -> Disabled (default) -# en -> English -# fa -> Farsi -bigclock = none - -# Set bigclock to 12-hour notation. -bigclock_12hr = false - -# Set bigclock to show the seconds. -bigclock_seconds = false - -# Bigclock digit outline color (null = none) -bigclock_outline_fg = null - -# Blank main box background -# Setting to false will make it transparent -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.5 -box_position_v = 0.5 - -# Title to show at the top of the main box -# If set to null, none will be shown -box_title = null - -# Brightness decrease command -brightness_down_cmd = $PREFIX_DIRECTORY/bin/brightnessctl -q -n s 10%- - -# 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 -# If null, the keybind is disabled and isn't shown -brightness_up_key = F6 - -# 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 = 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 - -# 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 - -# Color mixing animation second color id -colormix_col2 = 0x000000FF - -# Color mixing animation third color id -colormix_col3 = 0x20000000 - -# Screen corners customization -# Keywords: -# 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 -# 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 -# -# 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 -# It is possible to have both horizontal and vertical items on the same corner - -# Bottom left -corner_bottom_left = version - -# Bottom right -corner_bottom_right = labels - -# Top left -corner_top_left = shutdown,restart,britup,britdown,password battery - -# Top right -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. -# 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 -custom_sessions = $CONFIG_DIRECTORY/ly/custom-sessions - -# Input box active by default on startup -# Available inputs: info_line, session, login, password -default_input = login - -# DOOM animation fire height (1 thru 9) -doom_fire_height = 6 - -# DOOM animation fire spread (0 thru 4) -doom_fire_spread = 2 - -# DOOM animation custom top color (low intensity flames) -doom_top_color = 0x009F2707 - -# DOOM animation custom middle color (medium intensity flames) -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 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 (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) -edge_margin = 0 - -# Error background color id -error_bg = 0x00000000 - -# Error foreground color id -# Default is red and bold -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. -# 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) -# 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 - -# Sets the multiple amounts of neighbors needed for a cell to be born. -# Each numerical digit from 0 to 8 inclusive in this string becomes -# one of the targets required for this cell to be born. -# Example: a string of "3" makes the cell be born on 3 neighbors -gameoflife_param_birth = "3" - -# Sets the multiple amounts of neighbors needed for a cell to survive. -# Each numerical digit from 0 to 8 inclusive in this string becomes -# one of the targets required for this cell to survive. -# Example: a string of "23" makes the cell survive on 2 or 3 neighbors -gameoflife_param_survival = "23" - -# Remove main box borders -hide_borders = 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 - -# Input boxes length -input_len = 34 - -# Active language -# Available languages are found in $CONFIG_DIRECTORY/ly/lang/ -lang = en - -# 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 - -# 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 -# 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 - -# 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 - -# Main box horizontal margin -margin_box_h = 2 - -# Main box vertical margin -margin_box_v = 1 - -# Set numlock on/off at startup -numlock = false - -# Default path -# If null, ly doesn't set a path -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 - -# 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 - -# 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, 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 - -# 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 -# 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) -# 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 - -# 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 -vi_default_mode = normal - -# Enable vi keybindings -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 -# Add the -quiet argument to hide startup logs from the server -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 - -# xinitrc -# If null, the xinitrc session will be hidden -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: -# 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. -#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 -## 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 From 95a1bca7deddc815fd7e4937a769242f201e32fe Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Mon, 24 Aug 2026 17:58:21 +0200 Subject: [PATCH 19/26] config: Fix dur file comment Signed-off-by: AnErrupTion --- res/config.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/res/config.lua b/res/config.lua index c0ff749..b96de8b 100644 --- a/res/config.lua +++ b/res/config.lua @@ -41,7 +41,7 @@ ly = { -- 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) + -- dur -> .dur file format (https://github.com/cmang/durdraw/tree/master) -- lua -> user-made animation written in LuaJIT animation = "none", From 32dd0967ba9786ce8edf5e6eaaf14f865b5c24dd Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Mon, 24 Aug 2026 21:32:11 +0200 Subject: [PATCH 20/26] config: Add grab_focus_tty (closes #1039) Signed-off-by: AnErrupTion --- res/config.lua | 7 +++++++ src/config/Config.zig | 1 + src/main.zig | 5 ++++- 3 files changed, 12 insertions(+), 1 deletion(-) diff --git a/res/config.lua b/res/config.lua index b96de8b..e0fc47a 100644 --- a/res/config.lua +++ b/res/config.lua @@ -316,6 +316,13 @@ ly = { -- Example: a string of "23" makes the cell survive on 2 or 3 neighbors gameoflife_param_survival = "23", + -- Sets the TTY on which to always grab focus on. + -- This is useful if you want to have deterministic behavior when starting + -- Ly on multiple TTYs simultaneously. + -- If null, Ly will always try to grab focus on the TTY it is launched on, + -- even if multiple instances are launched. + grab_focus_tty = nil, + -- Remove main box borders hide_borders = false, diff --git a/src/config/Config.zig b/src/config/Config.zig index ff23cad..18d4abe 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -67,6 +67,7 @@ gameoflife_frame_delay: usize = 6, gameoflife_initial_density: f32 = 0.4, gameoflife_param_birth: []const u8 = "3", gameoflife_param_survival: []const u8 = "23", +grab_focus_tty: ?u8 = null, hide_borders: bool = false, inactivity_cmd: ?[]const u8 = null, inactivity_delay: u16 = 0, diff --git a/src/main.zig b/src/main.zig index d5f728a..0bcb2db 100644 --- a/src/main.zig +++ b/src/main.zig @@ -1007,7 +1007,10 @@ pub fn main(init: std.process.Init) !void { ); break :no_tty_found build_options.fallback_tty; }; - if (!state.use_kmscon_vt) { + if (!state.use_kmscon_vt) switch_tty: { + if (state.config.grab_focus_tty) |tty| { + if (tty != state.active_tty) break :switch_tty; + } interop.switchTty(state.active_tty) catch |err| { try state.info_line.addMessage( state.lang.err_switch_tty, From c7591a15eb44a8358e8a65ab357203cbd807a6a1 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Mon, 24 Aug 2026 22:47:10 +0200 Subject: [PATCH 21/26] auth: Simplify PAM appdata Signed-off-by: AnErrupTion --- ly-core/src/interop.zig | 7 +++++-- src/auth.zig | 43 ++++++++++++++++++++++------------------- 2 files changed, 28 insertions(+), 22 deletions(-) diff --git a/ly-core/src/interop.zig b/ly-core/src/interop.zig index b269513..0cf5f09 100644 --- a/ly-core/src/interop.zig +++ b/ly-core/src/interop.zig @@ -421,8 +421,11 @@ pub fn getNextUsernameEntry() ?UsernameEntry { }; } -pub fn getUsernameEntry(username: [:0]const u8) ?UsernameEntry { - const entry = pwd.getpwnam(username); +pub fn getUsernameEntry(allocator: std.mem.Allocator, username: []const u8) ?UsernameEntry { + const username_z = allocator.dupeZ(u8, username) catch return null; + defer allocator.free(username_z); + + const entry = pwd.getpwnam(username_z); if (entry == null) return null; return .{ diff --git a/src/auth.zig b/src/auth.zig index 7d9c66c..219e4ca 100644 --- a/src/auth.zig +++ b/src/auth.zig @@ -26,6 +26,11 @@ pub const AuthOptions = struct { use_kmscon_vt: bool, }; +const PamAppdata = struct { + username: []const u8, + password: []const u8, +}; + var xorg_pid: std.posix.pid_t = 0; pub fn xorgSignalHandler(sig: std.posix.SIG) callconv(.c) void { if (xorg_pid > 0) _ = std.c.kill(xorg_pid, sig); @@ -49,13 +54,11 @@ pub fn authenticate(allocator: std.mem.Allocator, io: std.Io, log_file: *LogFile // Open the PAM session try log_file.info(io, "auth/pam", "encoding credentials", .{}); - 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 }; + var credentials: PamAppdata = .{ + .username = login, + .password = password, + }; const conv = interop.pam.pam_conv{ .conv = loginConv, @@ -98,7 +101,7 @@ pub fn authenticate(allocator: std.mem.Allocator, io: std.Io, log_file: *LogFile defer interop.closePasswordDatabase(); // Get password structure from username - user_entry = interop.getUsernameEntry(login_z) orelse return error.GetPasswordNameFailed; + user_entry = interop.getUsernameEntry(allocator, login) orelse return error.GetPasswordNameFailed; } // Set user shell if it hasn't already been set @@ -276,6 +279,7 @@ fn loginConv( resp: ?*?[*]interop.pam.pam_response, appdata_ptr: ?*anyopaque, ) callconv(.c) c_int { + const data: *PamAppdata = @ptrCast(@alignCast(appdata_ptr)); const message_count: u32 = @intCast(num_msg); const messages = msg.?; @@ -289,20 +293,28 @@ fn loginConv( var username: ?[:0]u8 = null; var password: ?[:0]u8 = null; var status: c_int = interop.pam.PAM_SUCCESS; + defer { + if (status != interop.pam.PAM_SUCCESS) { + // Memory is freed by pam otherwise + allocator.free(response); + if (username) |str| allocator.free(str); + if (password) |str| allocator.free(str); + } else { + resp.?.* = response.ptr; + } + } for (0..message_count) |i| set_credentials: { switch (messages[i].?.msg_style) { interop.pam.PAM_PROMPT_ECHO_ON => { - const data: [*][*:0]u8 = @ptrCast(@alignCast(appdata_ptr)); - username = allocator.dupeZ(u8, std.mem.span(data[0])) catch { + username = allocator.dupeZ(u8, data.username) catch { status = interop.pam.PAM_BUF_ERR; break :set_credentials; }; response[i].resp = username.?; }, interop.pam.PAM_PROMPT_ECHO_OFF => { - const data: [*][*:0]u8 = @ptrCast(@alignCast(appdata_ptr)); - password = allocator.dupeZ(u8, std.mem.span(data[1])) catch { + password = allocator.dupeZ(u8, data.password) catch { status = interop.pam.PAM_BUF_ERR; break :set_credentials; }; @@ -316,15 +328,6 @@ fn loginConv( } } - if (status != interop.pam.PAM_SUCCESS) { - // Memory is freed by pam otherwise - allocator.free(response); - if (username) |str| allocator.free(str); - if (password) |str| allocator.free(str); - } else { - resp.?.* = response.ptr; - } - return status; } From 432560a1cd79156965874b414bde9a243bf7d3b1 Mon Sep 17 00:00:00 2001 From: urly3 Date: Tue, 25 Aug 2026 11:29:52 +0200 Subject: [PATCH 22/26] fix: freebsd still requires 1-indexed tty (#1048) ## What are the changes about? I thought it was working as intended but appears I just got lucky when testing. VT_ACTIVATE still wants them 1-indexed, so increase the parsed value respectively. My apologies! ## What existing issue(s) 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/1048 Reviewed-by: AnErrupTion --- ly-core/src/interop.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ly-core/src/interop.zig b/ly-core/src/interop.zig index 0cf5f09..1b0eea1 100644 --- a/ly-core/src/interop.zig +++ b/ly-core/src/interop.zig @@ -265,7 +265,7 @@ fn PlatformStruct() type { const dev_name = buf[0 .. len - 1]; if (std.mem.startsWith(u8, dev_name, "ttyv")) { - return try std.fmt.parseInt(u8, dev_name[dev_name.len - 1 ..], 16); + return try std.fmt.parseInt(u8, dev_name[dev_name.len - 1 ..], 16) + 1; } return error.NoTtyFound; From 39ea43c3cb4eef7c7801e32857a0e508e6ff3fd9 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Tue, 25 Aug 2026 19:54:34 +0200 Subject: [PATCH 23/26] ui: Let the info line render after empty password error Signed-off-by: AnErrupTion --- src/main.zig | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/src/main.zig b/src/main.zig index 0bcb2db..f9a42b5 100644 --- a/src/main.zig +++ b/src/main.zig @@ -1494,23 +1494,6 @@ 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; } From 00440cca2ed000131ed15ab1bd3002a1110e00f7 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Tue, 25 Aug 2026 19:55:54 +0200 Subject: [PATCH 24/26] crawler: remove any trailing colon for XDG_CURRENT_DESKTOP Signed-off-by: AnErrupTion --- src/main.zig | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/main.zig b/src/main.zig index f9a42b5..1090d52 100644 --- a/src/main.zig +++ b/src/main.zig @@ -2447,7 +2447,12 @@ fn crawl(session: *Session, io: std.Io, lang: Lang, path: []const u8, display_se for (desktop_names) |*c| { if (c.* == ';') c.* = ':'; } - maybe_xdg_desktop_names = desktop_names; + + if (desktop_names[desktop_names.len - 1] == ':') { + maybe_xdg_desktop_names = desktop_names[0 .. desktop_names.len - 1]; + } else { + 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 From 95641008fec17b409b843fac9b69b382df41681e Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Tue, 25 Aug 2026 20:32:36 +0200 Subject: [PATCH 25/26] setup.sh: Set STARTUP, late load Xsession (closes #1049) Signed-off-by: AnErrupTion --- res/setup.sh | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/res/setup.sh b/res/setup.sh index 8ec0f00..c82bcd9 100755 --- a/res/setup.sh +++ b/res/setup.sh @@ -70,6 +70,16 @@ if [ "$XDG_SESSION_TYPE" = "x11" ]; then 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 + # Load Xsession scripts # OPTIONFILE, USERXSESSION, USERXSESSIONRC and ALTUSERXSESSION are required # by the scripts to work @@ -78,6 +88,9 @@ if [ "$XDG_SESSION_TYPE" = "x11" ]; then export USERXSESSION="$HOME"/.xsession export USERXSESSIONRC="$HOME"/.xsessionrc export ALTUSERXSESSION="$HOME"/.Xsession + # Some distributions have an Xsession script containing "exec $STARTUP", + # this makes sure to set the variable to what we actually want to launch + export STARTUP="$@" if [ -d "$xsessionddir" ]; then for i in $(ls "$xsessionddir"); do @@ -92,16 +105,6 @@ if [ "$XDG_SESSION_TYPE" = "x11" ]; then 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" - 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 fi exec "$@" From d10df78886ffa9ec5fc42a7cb3d2d2bb02fb46c3 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Tue, 25 Aug 2026 23:10:37 +0200 Subject: [PATCH 26/26] auth: Add password reset prerequisites Signed-off-by: AnErrupTion --- src/auth.zig | 31 +++++++++++++++++++++++++++++-- 1 file changed, 29 insertions(+), 2 deletions(-) diff --git a/src/auth.zig b/src/auth.zig index 219e4ca..ef6af30 100644 --- a/src/auth.zig +++ b/src/auth.zig @@ -29,6 +29,9 @@ pub const AuthOptions = struct { const PamAppdata = struct { username: []const u8, password: []const u8, + authreq_requested: bool, + authreq_responded: bool, + new_password: []const u8, }; var xorg_pid: std.posix.pid_t = 0; @@ -41,7 +44,15 @@ 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, io: std.Io, 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}); @@ -58,6 +69,9 @@ pub fn authenticate(allocator: std.mem.Allocator, io: std.Io, log_file: *LogFile var credentials: PamAppdata = .{ .username = login, .password = password, + .authreq_requested = false, + .authreq_responded = false, + .new_password = "", }; const conv = interop.pam.pam_conv{ @@ -83,6 +97,11 @@ pub fn authenticate(allocator: std.mem.Allocator, io: std.Io, log_file: *LogFile try log_file.info(io, "auth/pam", "validating account", .{}); status = interop.pam.pam_acct_mgmt(handle, 0); + if (status == interop.pam.PAM_NEW_AUTHTOK_REQD) { + // credentials.authreq_requested = true; + // credentials.new_password = ""; + // status = interop.pam.pam_chauthtok(handle, interop.pam.PAM_CHANGE_EXPIRED_AUTHTOK); + } if (status != interop.pam.PAM_SUCCESS) return pamDiagnose(status); try log_file.info(io, "auth/pam", "setting credentials", .{}); @@ -314,7 +333,15 @@ fn loginConv( response[i].resp = username.?; }, interop.pam.PAM_PROMPT_ECHO_OFF => { - password = allocator.dupeZ(u8, data.password) catch { + var pass = data.password; + if (data.authreq_requested) { + if (data.authreq_responded) { + pass = data.new_password; + } + data.authreq_responded = true; + } + + password = allocator.dupeZ(u8, pass) catch { status = interop.pam.PAM_BUF_ERR; break :set_credentials; };