8 Commits

Author SHA1 Message Date
AnErrupTion
afb1dc62a0 Fix merge conflict issues
Signed-off-by: AnErrupTion <anerruption@disroot.org>
2026-05-11 21:15:18 +02:00
AnErrupTion
efa56ae770 Use $EXECUTABLE_NAME in kmscon service
Signed-off-by: AnErrupTion <anerruption@disroot.org>
2026-05-11 21:11:07 +02:00
AnErrupTion
2a41391764 Fix labels_max_length calculation (closes #984)
Signed-off-by: AnErrupTion <anerruption@disroot.org>
2026-05-11 21:10:59 +02:00
AnErrupTion
e0f915d440 Improve keyboard handling (closes #982)
Signed-off-by: AnErrupTion <anerruption@disroot.org>
2026-05-11 21:10:52 +02:00
Titanium Brain
692ca9f7b5 Resolve merge conflict
Signed-off-by: AnErrupTion <anerruption@disroot.org>
2026-05-11 21:10:44 +02:00
AnErrupTion
f6c44d5e57 Fix building without X11
Signed-off-by: AnErrupTion <anerruption@disroot.org>
2026-05-11 21:10:11 +02:00
AnErrupTion
1080583233 Fix log file race condition
Signed-off-by: AnErrupTion <anerruption@disroot.org>
2026-05-11 21:10:02 +02:00
AnErrupTion
cd426bb3df Start Ly v1.4.1 development cycle
Signed-off-by: AnErrupTion <anerruption@disroot.org>
2026-05-11 21:09:46 +02:00
12 changed files with 95 additions and 78 deletions

View File

@@ -23,7 +23,7 @@ comptime {
} }
} }
const ly_version = std.SemanticVersion{ .major = 1, .minor = 4, .patch = 0 }; const ly_version = std.SemanticVersion{ .major = 1, .minor = 4, .patch = 1 };
var dest_directory: []const u8 = undefined; var dest_directory: []const u8 = undefined;
var config_directory: []const u8 = undefined; var config_directory: []const u8 = undefined;
@@ -72,7 +72,11 @@ pub fn build(b: *std.Build) !void {
.use_llvm = true, .use_llvm = true,
}); });
const ly_ui = b.dependency("ly_ui", .{ .target = target, .optimize = optimize }); const ly_ui = b.dependency("ly_ui", .{
.target = target,
.optimize = optimize,
.enable_x11_support = enable_x11_support,
});
exe.root_module.addImport("ly-ui", ly_ui.module("ly-ui")); exe.root_module.addImport("ly-ui", ly_ui.module("ly-ui"));
exe.root_module.addOptions("build_options", build_options); exe.root_module.addOptions("build_options", build_options);

View File

@@ -1,6 +1,6 @@
.{ .{
.name = .ly, .name = .ly,
.version = "1.4.0", .version = "1.4.1",
.fingerprint = 0xa148ffcc5dc2cb59, .fingerprint = 0xa148ffcc5dc2cb59,
.minimum_zig_version = "0.16.0", .minimum_zig_version = "0.16.0",
.dependencies = .{ .dependencies = .{

View File

@@ -4,6 +4,7 @@ const Translator = @import("translate_c").Translator;
pub fn build(b: *std.Build) void { pub fn build(b: *std.Build) void {
const target = b.standardTargetOptions(.{}); const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{}); const optimize = b.standardOptimizeOption(.{});
const enable_x11_support = b.option(bool, "enable_x11_support", "Enable X11 support") orelse true;
const mod = b.addModule("ly-core", .{ const mod = b.addModule("ly-core", .{
.root_source_file = b.path("src/root.zig"), .root_source_file = b.path("src/root.zig"),
.target = target, .target = target,
@@ -20,7 +21,9 @@ pub fn build(b: *std.Build) void {
addCImport(b, mod, translate_c, target, optimize, "pam", "#include <security/pam_appl.h>"); addCImport(b, mod, translate_c, target, optimize, "pam", "#include <security/pam_appl.h>");
addCImport(b, mod, translate_c, target, optimize, "utmp", "#include <utmpx.h>"); addCImport(b, mod, translate_c, target, optimize, "utmp", "#include <utmpx.h>");
addCImport(b, mod, translate_c, target, optimize, "xcb", "#include <xcb/xcb.h>"); if (enable_x11_support) {
addCImport(b, mod, translate_c, target, optimize, "xcb", "#include <xcb/xcb.h>");
}
if (target.result.os.tag == .freebsd) { if (target.result.os.tag == .freebsd) {
addCImport(b, mod, translate_c, target, optimize, "pwd", addCImport(b, mod, translate_c, target, optimize, "pwd",
\\#include <pwd.h> \\#include <pwd.h>

View File

@@ -1,6 +1,6 @@
.{ .{
.name = .ly_core, .name = .ly_core,
.version = "1.0.0", .version = "1.0.1",
.fingerprint = 0xddda7afda795472, .fingerprint = 0xddda7afda795472,
.minimum_zig_version = "0.16.0", .minimum_zig_version = "0.16.0",
.dependencies = .{ .dependencies = .{

View File

@@ -4,13 +4,18 @@ const Translator = @import("translate_c").Translator;
pub fn build(b: *std.Build) void { pub fn build(b: *std.Build) void {
const target = b.standardTargetOptions(.{}); const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{}); const optimize = b.standardOptimizeOption(.{});
const enable_x11_support = b.option(bool, "enable_x11_support", "Enable X11 support") orelse true;
const mod = b.addModule("ly-ui", .{ const mod = b.addModule("ly-ui", .{
.root_source_file = b.path("src/root.zig"), .root_source_file = b.path("src/root.zig"),
.target = target, .target = target,
.optimize = optimize, .optimize = optimize,
}); });
const ly_core = b.dependency("ly_core", .{ .target = target, .optimize = optimize }); const ly_core = b.dependency("ly_core", .{
.target = target,
.optimize = optimize,
.enable_x11_support = enable_x11_support,
});
mod.addImport("ly-core", ly_core.module("ly-core")); mod.addImport("ly-core", ly_core.module("ly-core"));
const termbox_dep = b.dependency("termbox2", .{ const termbox_dep = b.dependency("termbox2", .{

View File

@@ -1,6 +1,6 @@
.{ .{
.name = .ly_ui, .name = .ly_ui,
.version = "1.0.0", .version = "1.0.1",
.fingerprint = 0x8d11bf85a74ec803, .fingerprint = 0x8d11bf85a74ec803,
.minimum_zig_version = "0.16.0", .minimum_zig_version = "0.16.0",
.dependencies = .{ .dependencies = .{

View File

@@ -381,6 +381,12 @@ pub fn setCell(x: usize, y: usize, cell: Cell) void {
); );
} }
pub fn setCellBoundsChecked(self: *TerminalBuffer, x: isize, y: isize, cell: Cell) void {
if (0 <= x and x < self.width and 0 <= y and y < self.height) {
cell.put(@intCast(x), @intCast(y));
}
}
pub fn reclaim(self: TerminalBuffer) !void { pub fn reclaim(self: TerminalBuffer) !void {
if (self.termios) |termios| { if (self.termios) |termios| {
// Take back control of the TTY // Take back control of the TTY

View File

@@ -171,6 +171,7 @@ pub fn getKeyList(allocator: Allocator, tb_event: termbox.tb_event) !KeyList {
const code = if (tb_event.ch == 0 and tb_event.key < 128) tb_event.key else tb_event.ch; const code = if (tb_event.ch == 0 and tb_event.key < 128) tb_event.key else tb_event.ch;
switch (code) { switch (code) {
// Non-standard control codes
0 => { 0 => {
key.ctrl = true; key.ctrl = true;
key.@"2" = true; key.@"2" = true;
@@ -342,7 +343,9 @@ pub fn getKeyList(allocator: Allocator, tb_event: termbox.tb_event) !KeyList {
key = std.mem.zeroes(Key); key = std.mem.zeroes(Key);
key._ = true; key._ = true;
}, },
// Standard ASCII characters
32 => { 32 => {
key = std.mem.zeroes(Key);
key.@" " = true; key.@" " = true;
}, },
33 => { 33 => {
@@ -370,6 +373,7 @@ pub fn getKeyList(allocator: Allocator, tb_event: termbox.tb_event) !KeyList {
key.@"&" = true; key.@"&" = true;
}, },
39 => { 39 => {
key = std.mem.zeroes(Key);
key.@"'" = true; key.@"'" = true;
}, },
40 => { 40 => {
@@ -389,74 +393,86 @@ pub fn getKeyList(allocator: Allocator, tb_event: termbox.tb_event) !KeyList {
key.@"+" = true; key.@"+" = true;
}, },
44 => { 44 => {
key = std.mem.zeroes(Key);
key.@"," = true; key.@"," = true;
}, },
45 => { 45 => {
key = std.mem.zeroes(Key);
key.@"-" = true; key.@"-" = true;
}, },
46 => { 46 => {
key = std.mem.zeroes(Key);
key.@"." = true; key.@"." = true;
}, },
47 => { 47 => {
key = std.mem.zeroes(Key);
key.@"/" = true; key.@"/" = true;
}, },
48 => { 48 => {
key = std.mem.zeroes(Key);
key.@"0" = true; key.@"0" = true;
}, },
49 => { 49 => {
key = std.mem.zeroes(Key);
key.@"1" = true; key.@"1" = true;
}, },
50 => { 50 => {
key = std.mem.zeroes(Key);
key.@"2" = true; key.@"2" = true;
}, },
51 => { 51 => {
key = std.mem.zeroes(Key);
key.@"3" = true; key.@"3" = true;
}, },
52 => { 52 => {
key = std.mem.zeroes(Key);
key.@"4" = true; key.@"4" = true;
}, },
53 => { 53 => {
key = std.mem.zeroes(Key);
key.@"5" = true; key.@"5" = true;
}, },
54 => { 54 => {
key = std.mem.zeroes(Key);
key.@"6" = true; key.@"6" = true;
}, },
55 => { 55 => {
key = std.mem.zeroes(Key);
key.@"7" = true; key.@"7" = true;
}, },
56 => { 56 => {
key = std.mem.zeroes(Key);
key.@"8" = true; key.@"8" = true;
}, },
57 => { 57 => {
key = std.mem.zeroes(Key);
key.@"9" = true; key.@"9" = true;
}, },
58 => { 58 => {
key.shift = true; key = std.mem.zeroes(Key);
key.@":" = true; key.@":" = true;
}, },
59 => { 59 => {
key = std.mem.zeroes(Key);
key.@";" = true; key.@";" = true;
}, },
60 => { 60 => {
key.shift = true; key = std.mem.zeroes(Key);
key.@"<" = true; key.@"<" = true;
}, },
61 => { 61 => {
key = std.mem.zeroes(Key);
key.@"=" = true; key.@"=" = true;
}, },
62 => { 62 => {
key.shift = true; key = std.mem.zeroes(Key);
key.@">" = true; key.@">" = true;
}, },
63 => { 63 => {
key.shift = true; key = std.mem.zeroes(Key);
key.@"?" = true; key.@"?" = true;
}, },
64 => { 64 => {
key.shift = true;
key.@"2" = true;
try keys.append(allocator, key);
key = std.mem.zeroes(Key); key = std.mem.zeroes(Key);
key.@"@" = true; key.@"@" = true;
}, },
@@ -565,12 +581,15 @@ pub fn getKeyList(allocator: Allocator, tb_event: termbox.tb_event) !KeyList {
key.z = true; key.z = true;
}, },
91 => { 91 => {
key = std.mem.zeroes(Key);
key.@"[" = true; key.@"[" = true;
}, },
92 => { 92 => {
key = std.mem.zeroes(Key);
key.@"\\" = true; key.@"\\" = true;
}, },
93 => { 93 => {
key = std.mem.zeroes(Key);
key.@"]" = true; key.@"]" = true;
}, },
94 => { 94 => {
@@ -578,14 +597,11 @@ pub fn getKeyList(allocator: Allocator, tb_event: termbox.tb_event) !KeyList {
key.@"^" = true; key.@"^" = true;
}, },
95 => { 95 => {
key.shift = true;
key.@"-" = true;
try keys.append(allocator, key);
key = std.mem.zeroes(Key); key = std.mem.zeroes(Key);
key._ = true; key._ = true;
}, },
96 => { 96 => {
key = std.mem.zeroes(Key);
key.@"`" = true; key.@"`" = true;
}, },
97 => { 97 => {
@@ -667,34 +683,21 @@ pub fn getKeyList(allocator: Allocator, tb_event: termbox.tb_event) !KeyList {
key.z = true; key.z = true;
}, },
123 => { 123 => {
key.shift = true;
key.@"{" = true; key.@"{" = true;
}, },
124 => { 124 => {
key.shift = true;
key.@"\\" = true;
try keys.append(allocator, key);
key = std.mem.zeroes(Key); key = std.mem.zeroes(Key);
key.@"|" = true; key.@"|" = true;
}, },
125 => { 125 => {
key.shift = true; key = std.mem.zeroes(Key);
key.@"}" = true; key.@"}" = true;
}, },
126 => { 126 => {
key.shift = true;
key.@"`" = true;
try keys.append(allocator, key);
key = std.mem.zeroes(Key); key = std.mem.zeroes(Key);
key.@"~" = true; key.@"~" = true;
}, },
127 => { 127 => {
key.ctrl = true;
key.@"8" = true;
try keys.append(allocator, key);
key = std.mem.zeroes(Key); key = std.mem.zeroes(Key);
key.backspace = true; key.backspace = true;
}, },

View File

@@ -5,7 +5,7 @@ After=kmsconvt@%i.service
Conflicts=kmsconvt@%i.service Conflicts=kmsconvt@%i.service
[Service] [Service]
ExecStart=$PREFIX_DIRECTORY/bin/kmscon --font-engine unifont --vt=%I --seats=seat0 --login -- $PREFIX_DIRECTORY/bin/ly --use-kmscon-vt ExecStart=$PREFIX_DIRECTORY/bin/kmscon --font-engine unifont --vt=%I --seats=seat0 --login -- $PREFIX_DIRECTORY/bin/$EXECUTABLE_NAME --use-kmscon-vt
StandardInput=tty StandardInput=tty
UtmpIdentifier=%I UtmpIdentifier=%I
TTYPath=/dev/%I TTYPath=/dev/%I

View File

@@ -311,7 +311,6 @@ io: std.Io,
terminal_buffer: *TerminalBuffer, terminal_buffer: *TerminalBuffer,
dur_movie: DurFormat, dur_movie: DurFormat,
frames: usize, frames: usize,
frame_size: UVec2,
start_pos: IVec2, start_pos: IVec2,
full_color: bool, full_color: bool,
animate: *bool, animate: *bool,
@@ -324,19 +323,16 @@ offset_alignment: DurOffsetAlignment,
offset: IVec2, offset: IVec2,
// if the user has an even number of columns or rows, we will default to the left or higher position (e.g. 4 columns center = .x..) // if the user has an even number of columns or rows, we will default to the left or higher position (e.g. 4 columns center = .x..)
fn center(v: u32) i64 { fn center(v: i64) i64 {
return @intCast((v / 2) + (v % 2)); return @intCast(@divTrunc(v, 2) + @mod(v, 2));
} }
fn calc_start_position(terminal_buffer: *TerminalBuffer, dur_movie: *DurFormat, offset_alignment: DurOffsetAlignment, offset: IVec2) IVec2 { fn calc_start_position(terminal_buffer: *TerminalBuffer, dur_movie: *DurFormat, offset_alignment: DurOffsetAlignment, offset: IVec2) IVec2 {
const buf_width: u32 = @intCast(terminal_buffer.width); const buf_width: i64 = @intCast(terminal_buffer.width);
const buf_height: u32 = @intCast(terminal_buffer.height); const buf_height: i64 = @intCast(terminal_buffer.height);
var movie_width: u32 = @intCast(dur_movie.columns.?); const movie_width: i64 = @intCast(dur_movie.columns.?);
var movie_height: u32 = @intCast(dur_movie.lines.?); const movie_height: i64 = @intCast(dur_movie.lines.?);
if (movie_width > buf_width) movie_width = buf_width;
if (movie_height > buf_height) movie_height = buf_height;
const start_pos: IVec2 = switch (offset_alignment) { const start_pos: IVec2 = switch (offset_alignment) {
DurOffsetAlignment.center => .{ center(buf_width) - center(movie_width), center(buf_height) - center(movie_height) }, DurOffsetAlignment.center => .{ center(buf_width) - center(movie_width), center(buf_height) - center(movie_height) },
@@ -353,20 +349,6 @@ fn calc_start_position(terminal_buffer: *TerminalBuffer, dur_movie: *DurFormat,
return start_pos + offset; return start_pos + offset;
} }
fn calc_frame_size(terminal_buffer: *TerminalBuffer, dur_movie: *DurFormat) UVec2 {
const buf_width: u32 = @intCast(terminal_buffer.width);
const buf_height: u32 = @intCast(terminal_buffer.height);
const movie_width: u32 = @intCast(dur_movie.columns.?);
const movie_height: u32 = @intCast(dur_movie.lines.?);
// Draw only the needed amount if movie smaller than screen. If movie is bigger, we will just draw entire screen
const frame_width = if (movie_width < buf_width) movie_width else buf_width;
const frame_height = if (movie_height < buf_height) movie_height else buf_height;
return .{ frame_width, frame_height };
}
pub fn init( pub fn init(
allocator: Allocator, allocator: Allocator,
io: std.Io, io: std.Io,
@@ -405,7 +387,6 @@ pub fn init(
const offset: IVec2 = .{ x_offset, y_offset }; const offset: IVec2 = .{ x_offset, y_offset };
const start_pos = calc_start_position(terminal_buffer, &dur_movie, offset_alignment, offset); const start_pos = calc_start_position(terminal_buffer, &dur_movie, offset_alignment, offset);
const frame_size = calc_frame_size(terminal_buffer, &dur_movie);
// Convert dur fps to frames per ms // Convert dur fps to frames per ms
const frame_time: u32 = @trunc(1000 / dur_movie.framerate.?); const frame_time: u32 = @trunc(1000 / dur_movie.framerate.?);
@@ -418,7 +399,6 @@ pub fn init(
.terminal_buffer = terminal_buffer, .terminal_buffer = terminal_buffer,
.frames = 0, .frames = 0,
.time_previous = std.Io.Timestamp.now(io, .real).toMilliseconds(), .time_previous = std.Io.Timestamp.now(io, .real).toMilliseconds(),
.frame_size = frame_size,
.start_pos = start_pos, .start_pos = start_pos,
.full_color = full_color, .full_color = full_color,
.animate = animate, .animate = animate,
@@ -453,9 +433,8 @@ fn deinit(self: *DurFile) void {
} }
fn realloc(self: *DurFile) !void { fn realloc(self: *DurFile) !void {
// when terminal size changes, we need to recalculate the start_pos and frame_size based on the new size // when terminal size changes, we need to recalculate the start_pos based on the new size
self.start_pos = calc_start_position(self.terminal_buffer, &self.dur_movie, self.offset_alignment, self.offset); self.start_pos = calc_start_position(self.terminal_buffer, &self.dur_movie, self.offset_alignment, self.offset);
self.frame_size = calc_frame_size(self.terminal_buffer, &self.dur_movie);
} }
fn draw(self: *DurFile) void { fn draw(self: *DurFile) void {
@@ -463,24 +442,14 @@ fn draw(self: *DurFile) void {
const current_frame = self.dur_movie.frames.items[self.frames]; const current_frame = self.dur_movie.frames.items[self.frames];
const buf_width: u32 = @intCast(self.terminal_buffer.width);
const buf_height: u32 = @intCast(self.terminal_buffer.height);
// y is used as an iterator in the durformat, while cell_y gives us the correct placement for the cell (same for x) // y is used as an iterator in the durformat, while cell_y gives us the correct placement for the cell (same for x)
for (0..self.frame_size[VEC_Y]) |y| { for (0..@intCast(self.dur_movie.lines.?)) |y| {
const y_offset_i = @as(i32, @intCast(y)) + self.start_pos[VEC_Y]; const cell_y = @as(i32, @intCast(y)) + self.start_pos[VEC_Y];
// we skip the pass if it falls outside of the draw window (ensure no int underflow)
const cell_y: u32 = if (y_offset_i >= 0 and y_offset_i < buf_height) @intCast(y_offset_i) else continue;
var iter = std.unicode.Utf8View.initUnchecked(current_frame.contents[y]).iterator(); var iter = std.unicode.Utf8View.initUnchecked(current_frame.contents[y]).iterator();
for (0..self.frame_size[VEC_X]) |x| { for (0..@intCast(self.dur_movie.columns.?)) |x| {
const x_offset_i = @as(i32, @intCast(x)) + self.start_pos[VEC_X]; const cell_x = @as(i32, @intCast(x)) + self.start_pos[VEC_X];
// skip pass, same as y but also increment the codepoint iter to fetch correct values in later passes
const cell_x: u32 = if (x_offset_i >= 0 and x_offset_i < buf_width) @intCast(x_offset_i) else {
_ = iter.nextCodepoint().?;
continue;
};
const codepoint: u21 = iter.nextCodepoint().?; const codepoint: u21 = iter.nextCodepoint().?;
const color_map = current_frame.colorMap[x][y]; const color_map = current_frame.colorMap[x][y];
@@ -498,7 +467,7 @@ fn draw(self: *DurFile) void {
const cell = Cell{ .ch = @intCast(codepoint), .fg = fg_color, .bg = bg_color }; const cell = Cell{ .ch = @intCast(codepoint), .fg = fg_color, .bg = bg_color };
cell.put(cell_x, cell_y); self.terminal_buffer.setCellBoundsChecked(cell_x, cell_y, cell);
} }
} }

View File

@@ -417,19 +417,27 @@ fn xauth(log_file: *LogFile, allocator: std.mem.Allocator, io: std.Io, display_n
const magic_cookie = mcookie(io); const magic_cookie = mcookie(io);
log_file.deinit(io);
const pid = std.posix.system.fork(); const pid = std.posix.system.fork();
if (pid == 0) { if (pid == 0) {
try log_file.reinit(io);
var cmd_buffer: [1024]u8 = undefined; var cmd_buffer: [1024]u8 = undefined;
const cmd_str = std.fmt.bufPrintZ(&cmd_buffer, "{s} add {s} . {s}", .{ options.xauth_cmd, display_name, magic_cookie }) catch std.process.exit(1); const cmd_str = std.fmt.bufPrintZ(&cmd_buffer, "{s} add {s} . {s}", .{ options.xauth_cmd, display_name, magic_cookie }) catch std.process.exit(1);
try log_file.info(io, "auth/x11", "executing: {s} -c {s}", .{ shell, cmd_str }); try log_file.info(io, "auth/x11", "executing: {s} -c {s}", .{ shell, cmd_str });
const args = [_:null]?[*:0]const u8{ shell, "-c", cmd_str }; const args = [_:null]?[*:0]const u8{ shell, "-c", cmd_str };
_ = std.posix.system.execve(shell, &args, std.c.environ); _ = std.posix.system.execve(shell, &args, std.c.environ);
log_file.deinit(io);
std.process.exit(1); std.process.exit(1);
} }
var status: c_int = undefined; var status: c_int = undefined;
const result = std.posix.system.waitpid(pid, &status, 0); const result = std.posix.system.waitpid(pid, &status, 0);
try log_file.reinit(io);
if (interop.isError(result) or status != 0) { if (interop.isError(result) or status != 0) {
try log_file.err( try log_file.err(
io, io,

View File

@@ -359,7 +359,16 @@ pub fn main(init: std.process.Init) !void {
// Initialize terminal buffer // Initialize terminal buffer
try state.log_file.info(state.io, "tui", "initializing terminal buffer", .{}); try state.log_file.info(state.io, "tui", "initializing terminal buffer", .{});
state.labels_max_length = @max(TerminalBuffer.strWidth(state.lang.login), TerminalBuffer.strWidth(state.lang.password)); var labels = [_][]const u8{
state.lang.login,
state.lang.password,
state.lang.wayland,
state.lang.x11,
state.lang.shell,
state.lang.xinitrc,
state.lang.custom,
};
state.labels_max_length = maxWidths(&labels);
var seed: u64 = undefined; var seed: u64 = undefined;
state.io.random(std.mem.asBytes(&seed)); // Get a random seed for the PRNG (used by animations) state.io.random(std.mem.asBytes(&seed)); // Get a random seed for the PRNG (used by animations)
@@ -1312,6 +1321,16 @@ pub fn main(init: std.process.Init) !void {
); );
} }
fn maxWidths(labels: [][]const u8) usize {
var max_width: usize = 0;
for (labels) |label| {
max_width = @max(max_width, TerminalBuffer.strWidth(label));
}
return max_width;
}
fn uiErrorHandler(err: anyerror, ctx: *anyopaque) anyerror!void { fn uiErrorHandler(err: anyerror, ctx: *anyopaque) anyerror!void {
var state: *UiState = @ptrCast(@alignCast(ctx)); var state: *UiState = @ptrCast(@alignCast(ctx));