From 7c7aed9cb2740fc6a4e04f3bc4f3bdc2adfeb8e6 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Wed, 11 Feb 2026 23:51:32 +0100 Subject: [PATCH] Make animation timeout independent of event loop Signed-off-by: AnErrupTion --- src/animations/ColorMix.zig | 29 +++++++++++++++++++++++------ src/animations/Doom.zig | 27 ++++++++++++++++++++++----- src/animations/DurFile.zig | 25 ++++++++++++++++++++----- src/animations/GameOfLife.zig | 27 ++++++++++++++++++++++----- src/animations/Matrix.zig | 27 ++++++++++++++++++++++----- src/main.zig | 31 ++++++++++++------------------- 6 files changed, 121 insertions(+), 45 deletions(-) diff --git a/src/animations/ColorMix.zig b/src/animations/ColorMix.zig index 01c9cbe..a6ebd6d 100644 --- a/src/animations/ColorMix.zig +++ b/src/animations/ColorMix.zig @@ -1,6 +1,10 @@ const std = @import("std"); const math = std.math; +const ly_core = @import("ly-core"); +const interop = ly_core.interop; +const TimeOfDay = interop.TimeOfDay; + const Cell = @import("../tui/Cell.zig"); const TerminalBuffer = @import("../tui/TerminalBuffer.zig"); const Widget = @import("../tui/Widget.zig"); @@ -16,8 +20,10 @@ fn length(vec: Vec2) f32 { return math.sqrt(vec[0] * vec[0] + vec[1] * vec[1]); } +start_time: TimeOfDay, terminal_buffer: *TerminalBuffer, -timeout: *bool, +animate: *bool, +timeout_sec: u12, frames: u64, pattern_cos_mod: f32, pattern_sin_mod: f32, @@ -28,11 +34,14 @@ pub fn init( col1: u32, col2: u32, col3: u32, - timeout: *bool, -) ColorMix { + animate: *bool, + timeout_sec: u12, +) !ColorMix { return .{ + .start_time = try interop.getTimeOfDay(), .terminal_buffer = terminal_buffer, - .timeout = timeout, + .animate = animate, + .timeout_sec = timeout_sec, .frames = 0, .pattern_cos_mod = terminal_buffer.random.float(f32) * math.pi * 2.0, .pattern_sin_mod = terminal_buffer.random.float(f32) * math.pi * 2.0, @@ -60,13 +69,13 @@ pub fn widget(self: *ColorMix) Widget { null, null, draw, - null, + update, null, ); } fn draw(self: *ColorMix) void { - if (self.timeout.*) return; + if (!self.animate.*) return; self.frames +%= 1; const time: f32 = @as(f32, @floatFromInt(self.frames)) * time_scale; @@ -99,3 +108,11 @@ fn draw(self: *ColorMix) void { } } } + +fn update(self: *ColorMix, _: *anyopaque) !void { + const time = try interop.getTimeOfDay(); + + if (self.timeout_sec > 0 and time.seconds - self.start_time.seconds > self.timeout_sec) { + self.animate.* = false; + } +} diff --git a/src/animations/Doom.zig b/src/animations/Doom.zig index 1bd4dba..bb017ef 100644 --- a/src/animations/Doom.zig +++ b/src/animations/Doom.zig @@ -1,6 +1,10 @@ const std = @import("std"); const Allocator = std.mem.Allocator; +const ly_core = @import("ly-core"); +const interop = ly_core.interop; +const TimeOfDay = interop.TimeOfDay; + const Cell = @import("../tui/Cell.zig"); const TerminalBuffer = @import("../tui/TerminalBuffer.zig"); const Widget = @import("../tui/Widget.zig"); @@ -11,9 +15,11 @@ pub const STEPS = 12; pub const HEIGHT_MAX = 9; pub const SPREAD_MAX = 4; +start_time: TimeOfDay, allocator: Allocator, terminal_buffer: *TerminalBuffer, -timeout: *bool, +animate: *bool, +timeout_sec: u12, buffer: []u8, height: u8, spread: u8, @@ -27,7 +33,8 @@ pub fn init( bottom_color: u32, fire_height: u8, fire_spread: u8, - timeout: *bool, + animate: *bool, + timeout_sec: u12, ) !Doom { const buffer = try allocator.alloc(u8, terminal_buffer.width * terminal_buffer.height); initBuffer(buffer, terminal_buffer.width); @@ -50,9 +57,11 @@ pub fn init( }; return .{ + .start_time = try interop.getTimeOfDay(), .allocator = allocator, .terminal_buffer = terminal_buffer, - .timeout = timeout, + .animate = animate, + .timeout_sec = timeout_sec, .buffer = buffer, .height = @min(HEIGHT_MAX, fire_height), .spread = @min(SPREAD_MAX, fire_spread), @@ -67,7 +76,7 @@ pub fn widget(self: *Doom) Widget { deinit, realloc, draw, - null, + update, null, ); } @@ -83,7 +92,7 @@ fn realloc(self: *Doom) !void { } fn draw(self: *Doom) void { - if (self.timeout.*) return; + if (!self.animate.*) return; for (0..self.terminal_buffer.width) |x| { // We start from 1 so that we always have the topmost line when spreading fire @@ -130,3 +139,11 @@ fn initBuffer(buffer: []u8, width: usize) void { @memset(slice_start, 0); @memset(slice_end, STEPS); } + +fn update(self: *Doom, _: *anyopaque) !void { + const time = try interop.getTimeOfDay(); + + if (self.timeout_sec > 0 and time.seconds - self.start_time.seconds > self.timeout_sec) { + self.animate.* = false; + } +} diff --git a/src/animations/DurFile.zig b/src/animations/DurFile.zig index 24a268e..4f425bd 100644 --- a/src/animations/DurFile.zig +++ b/src/animations/DurFile.zig @@ -5,6 +5,8 @@ const eql = std.mem.eql; const flate = std.compress.flate; const ly_core = @import("ly-core"); +const interop = ly_core.interop; +const TimeOfDay = interop.TimeOfDay; const LogFile = ly_core.LogFile; const enums = @import("../enums.zig"); @@ -300,6 +302,7 @@ const VEC_Y = 1; const DurFile = @This(); +start_time: TimeOfDay, allocator: Allocator, terminal_buffer: *TerminalBuffer, dur_movie: DurFormat, @@ -307,7 +310,8 @@ frames: u64, frame_size: UVec2, start_pos: IVec2, full_color: bool, -timeout: *bool, +animate: *bool, +timeout_sec: u12, frame_time: u32, time_previous: i64, is_color_format_16: bool, @@ -367,7 +371,8 @@ pub fn init( x_offset: i32, y_offset: i32, full_color: bool, - timeout: *bool, + animate: *bool, + timeout_sec: u12, ) !DurFile { var dur_movie: DurFormat = .init(allocator); @@ -399,6 +404,7 @@ pub fn init( const frame_time: u32 = @intFromFloat(1000 / dur_movie.framerate.?); return .{ + .start_time = try interop.getTimeOfDay(), .allocator = allocator, .terminal_buffer = terminal_buffer, .frames = 0, @@ -406,7 +412,8 @@ pub fn init( .frame_size = frame_size, .start_pos = start_pos, .full_color = full_color, - .timeout = timeout, + .animate = animate, + .timeout_sec = timeout_sec, .dur_movie = dur_movie, .frame_time = frame_time, .is_color_format_16 = eql(u8, dur_movie.colorFormat.?, "16"), @@ -422,7 +429,7 @@ pub fn widget(self: *DurFile) Widget { deinit, realloc, draw, - null, + update, null, ); } @@ -438,7 +445,7 @@ fn realloc(self: *DurFile) !void { } fn draw(self: *DurFile) void { - if (self.timeout.*) return; + if (!self.animate.*) return; const current_frame = self.dur_movie.frames.items[self.frames]; @@ -493,3 +500,11 @@ fn draw(self: *DurFile) void { self.frames = (self.frames + 1) % frame_count; } } + +fn update(self: *DurFile, _: *anyopaque) !void { + const time = try interop.getTimeOfDay(); + + if (self.timeout_sec > 0 and time.seconds - self.start_time.seconds > self.timeout_sec) { + self.animate.* = false; + } +} diff --git a/src/animations/GameOfLife.zig b/src/animations/GameOfLife.zig index 32390c1..7ccfd49 100644 --- a/src/animations/GameOfLife.zig +++ b/src/animations/GameOfLife.zig @@ -1,6 +1,10 @@ const std = @import("std"); const Allocator = std.mem.Allocator; +const ly_core = @import("ly-core"); +const interop = ly_core.interop; +const TimeOfDay = interop.TimeOfDay; + const Cell = @import("../tui/Cell.zig"); const TerminalBuffer = @import("../tui/TerminalBuffer.zig"); const Widget = @import("../tui/Widget.zig"); @@ -16,6 +20,7 @@ const NEIGHBOR_DIRS = [_][2]i8{ .{ 1, 0 }, .{ 1, 1 }, }; +start_time: TimeOfDay, allocator: Allocator, terminal_buffer: *TerminalBuffer, current_grid: []bool, @@ -26,7 +31,8 @@ fg_color: u32, entropy_interval: usize, frame_delay: usize, initial_density: f32, -timeout: *bool, +animate: *bool, +timeout_sec: u12, dead_cell: Cell, width: usize, height: usize, @@ -38,7 +44,8 @@ pub fn init( entropy_interval: usize, frame_delay: usize, initial_density: f32, - timeout: *bool, + animate: *bool, + timeout_sec: u12, ) !GameOfLife { const width = terminal_buffer.width; const height = terminal_buffer.height; @@ -48,6 +55,7 @@ pub fn init( const next_grid = try allocator.alloc(bool, grid_size); var game = GameOfLife{ + .start_time = try interop.getTimeOfDay(), .allocator = allocator, .terminal_buffer = terminal_buffer, .current_grid = current_grid, @@ -58,7 +66,8 @@ pub fn init( .entropy_interval = entropy_interval, .frame_delay = frame_delay, .initial_density = initial_density, - .timeout = timeout, + .animate = animate, + .timeout_sec = timeout_sec, .dead_cell = .{ .ch = DEAD_CHAR, .fg = @intCast(TerminalBuffer.Color.DEFAULT), .bg = terminal_buffer.bg }, .width = width, .height = height, @@ -77,7 +86,7 @@ pub fn widget(self: *GameOfLife) Widget { deinit, realloc, draw, - null, + update, null, ); } @@ -105,7 +114,7 @@ fn realloc(self: *GameOfLife) !void { } fn draw(self: *GameOfLife) void { - if (self.timeout.*) return; + if (!self.animate.*) return; // Update game state at controlled frame rate self.frame_counter += 1; @@ -132,6 +141,14 @@ fn draw(self: *GameOfLife) void { } } +fn update(self: *GameOfLife, _: *anyopaque) !void { + const time = try interop.getTimeOfDay(); + + if (self.timeout_sec > 0 and time.seconds - self.start_time.seconds > self.timeout_sec) { + self.animate.* = false; + } +} + fn updateGeneration(self: *GameOfLife) void { // Conway's Game of Life rules with optimized neighbor counting for (0..self.height) |y| { diff --git a/src/animations/Matrix.zig b/src/animations/Matrix.zig index 1cb9c29..4a6ef95 100644 --- a/src/animations/Matrix.zig +++ b/src/animations/Matrix.zig @@ -2,6 +2,10 @@ const std = @import("std"); const Allocator = std.mem.Allocator; const Random = std.Random; +const ly_core = @import("ly-core"); +const interop = ly_core.interop; +const TimeOfDay = interop.TimeOfDay; + const Cell = @import("../tui/Cell.zig"); const TerminalBuffer = @import("../tui/TerminalBuffer.zig"); const Widget = @import("../tui/Widget.zig"); @@ -24,6 +28,7 @@ pub const Line = struct { update: usize, }; +start_time: TimeOfDay, allocator: Allocator, terminal_buffer: *TerminalBuffer, dots: []Dot, @@ -34,7 +39,8 @@ fg: u32, head_col: u32, min_codepoint: u16, max_codepoint: u16, -timeout: *bool, +animate: *bool, +timeout_sec: u12, default_cell: Cell, pub fn init( @@ -44,7 +50,8 @@ pub fn init( head_col: u32, min_codepoint: u16, max_codepoint: u16, - timeout: *bool, + animate: *bool, + timeout_sec: u12, ) !Matrix { const dots = try allocator.alloc(Dot, terminal_buffer.width * (terminal_buffer.height + 1)); const lines = try allocator.alloc(Line, terminal_buffer.width); @@ -52,6 +59,7 @@ pub fn init( initBuffers(dots, lines, terminal_buffer.width, terminal_buffer.height, terminal_buffer.random); return .{ + .start_time = try interop.getTimeOfDay(), .allocator = allocator, .terminal_buffer = terminal_buffer, .dots = dots, @@ -62,7 +70,8 @@ pub fn init( .head_col = head_col, .min_codepoint = min_codepoint, .max_codepoint = max_codepoint - min_codepoint, - .timeout = timeout, + .animate = animate, + .timeout_sec = timeout_sec, .default_cell = .{ .ch = ' ', .fg = fg, .bg = terminal_buffer.bg }, }; } @@ -74,7 +83,7 @@ pub fn widget(self: *Matrix) Widget { deinit, realloc, draw, - null, + update, null, ); } @@ -95,7 +104,7 @@ fn realloc(self: *Matrix) !void { } fn draw(self: *Matrix) void { - if (self.timeout.*) return; + if (!self.animate.*) return; const buf_height = self.terminal_buffer.height; const buf_width = self.terminal_buffer.width; @@ -188,6 +197,14 @@ fn draw(self: *Matrix) void { } } +fn update(self: *Matrix, _: *anyopaque) !void { + const time = try interop.getTimeOfDay(); + + if (self.timeout_sec > 0 and time.seconds - self.start_time.seconds > self.timeout_sec) { + self.animate.* = false; + } +} + fn initBuffers(dots: []Dot, lines: []Line, width: usize, height: usize, random: Random) void { var y: usize = 0; while (y <= height) : (y += 1) { diff --git a/src/main.zig b/src/main.zig index b74200b..dcd44ab 100644 --- a/src/main.zig +++ b/src/main.zig @@ -76,7 +76,6 @@ const UiState = struct { active_tty: u8, buffer: TerminalBuffer, labels_max_length: usize, - animation_timed_out: bool, shutdown_label: Label, restart_label: Label, sleep_label: Label, @@ -152,9 +151,6 @@ pub fn main() !void { state.allocator = gpa.allocator(); - // Allows stopping an animation after some time - const animation_time_start = try interop.getTimeOfDay(); - // Load arguments const params = comptime clap.parseParamsComptime( \\-h, --help Shows all commands. @@ -884,7 +880,8 @@ pub fn main() !void { state.config.doom_bottom_color, state.config.doom_fire_height, state.config.doom_fire_spread, - &state.animation_timed_out, + &state.animate, + state.config.animation_timeout_sec, ); animation = doom.widget(); }, @@ -896,17 +893,19 @@ pub fn main() !void { state.config.cmatrix_head_col, state.config.cmatrix_min_codepoint, state.config.cmatrix_max_codepoint, - &state.animation_timed_out, + &state.animate, + state.config.animation_timeout_sec, ); animation = matrix.widget(); }, .colormix => { - var color_mix = ColorMix.init( + var color_mix = try ColorMix.init( &state.buffer, state.config.colormix_col1, state.config.colormix_col2, state.config.colormix_col3, - &state.animation_timed_out, + &state.animate, + state.config.animation_timeout_sec, ); animation = color_mix.widget(); }, @@ -918,7 +917,8 @@ pub fn main() !void { state.config.gameoflife_entropy_interval, state.config.gameoflife_frame_delay, state.config.gameoflife_initial_density, - &state.animation_timed_out, + &state.animate, + state.config.animation_timeout_sec, ); animation = game_of_life.widget(); }, @@ -932,7 +932,8 @@ pub fn main() !void { state.config.dur_x_offset, state.config.dur_y_offset, state.config.full_color, - &state.animation_timed_out, + &state.animate, + state.config.animation_timeout_sec, ); animation = dur.widget(); }, @@ -949,7 +950,6 @@ pub fn main() !void { state.auth_fails = 0; state.run = true; state.update = true; - state.animation_timed_out = false; state.animate = state.config.animation != .none; state.insert_mode = !state.config.vi_mode or state.config.vi_default_mode == .insert; state.edge_margin = Position.init( @@ -1148,15 +1148,8 @@ pub fn main() !void { var timeout: i32 = -1; // Calculate the maximum timeout based on current animations, or the (big) clock. If there's none, we wait for the event indefinitely instead - if (state.animate and !state.animation_timed_out) { + if (state.animate) { timeout = state.config.animation_frame_delay; - - // Check how long we've been running so we can turn off the animation - const time = try interop.getTimeOfDay(); - - if (state.config.animation_timeout_sec > 0 and time.seconds - animation_time_start.seconds > state.config.animation_timeout_sec) { - state.animation_timed_out = true; - } } else if (state.config.bigclock != .none and state.config.clock == null) { const time = try interop.getTimeOfDay();