From 0380b8d32ba1b69474dac088f1220c9af876adc7 Mon Sep 17 00:00:00 2001 From: Frederick Ziola Date: Tue, 29 Jul 2025 20:02:13 -0500 Subject: [PATCH] Add ASCII Background Animation --- res/config.ini | 12 ++++++ src/animations/Ascii.zig | 91 ++++++++++++++++++++++++++++++++++++++++ src/config/Config.zig | 4 ++ src/enums.zig | 1 + src/main.zig | 5 +++ 5 files changed, 113 insertions(+) create mode 100644 src/animations/Ascii.zig diff --git a/res/config.ini b/res/config.ini index 7114a2c..713ae76 100644 --- a/res/config.ini +++ b/res/config.ini @@ -24,6 +24,7 @@ allow_empty_password = true # matrix -> CMatrix # colormix -> Color mixing shader # gameoflife -> John Conway's Game of Life +# ascii -> Custom ASCII art background loaded from a file animation = none # Stop the animation after some time @@ -155,6 +156,17 @@ gameoflife_frame_delay = 6 # 0.7+ -> Dense, chaotic patterns gameoflife_initial_density = 0.4 +# ASCII background file path (absolute) +ascii_filename = /etc/ly/config.ini + +# ASCII background text foreground color id +ascii_fg = 0x00FFFFFF + +# ASCII background offset (top left = 0,0; specified in characters) +# These must be >= 0 +ascii_x = 0 +ascii_y = 0 + # Remove main box borders hide_borders = false diff --git a/src/animations/Ascii.zig b/src/animations/Ascii.zig new file mode 100644 index 0000000..b5a72ef --- /dev/null +++ b/src/animations/Ascii.zig @@ -0,0 +1,91 @@ +const std = @import("std"); +const Animation = @import("../tui/Animation.zig"); +const Cell = @import("../tui/Cell.zig"); +const TerminalBuffer = @import("../tui/TerminalBuffer.zig"); + +const Ascii = @This(); + +const MAX_WIDTH = 512; +const MAX_HEIGHT = 256; + +terminal_buffer: *TerminalBuffer, +ascii_art: [MAX_HEIGHT][MAX_WIDTH]u8 = undefined, +line_len: [MAX_HEIGHT]usize = undefined, +line_count: usize = undefined, +fg: u32 = undefined, +x: u32 = undefined, +y: u32 = undefined, + +pub fn init(terminal_buffer: *TerminalBuffer, filename: []const u8, fg: u32, x: u32, y: u32) !Ascii { + var ascii_art: [MAX_HEIGHT][MAX_WIDTH]u8 = undefined; + var line_len: [MAX_HEIGHT]usize = [_]usize{0} ** MAX_HEIGHT; + var line_count: usize = 0; + + var file = std.fs.openFileAbsolute(filename, .{}) catch { + std.log.err("ASCII background file not found: {s}", .{filename}); + return .{ + .terminal_buffer = terminal_buffer, + .ascii_art = undefined, // Undefined buffers here are safe, as line_count + .line_len = undefined, // is set to 0 so nothing will be drawn + .line_count = 0, + .fg = 0x00000000, + .x = 0, .y = 0, + }; + }; + var reader = file.reader(); + defer file.close(); + + while (line_count < MAX_HEIGHT) { + const line = reader.readUntilDelimiterOrEof(&ascii_art[line_count], '\n') catch |err| switch (err) { + error.StreamTooLong => { + _ = try reader.skipUntilDelimiterOrEof('\n'); // consume remainder of line + line_len[line_count] = MAX_WIDTH; + line_count += 1; + continue; + }, + else => return err, + } orelse break; + + line_len[line_count] = line.len; + line_count += 1; + } + + return .{ + .terminal_buffer = terminal_buffer, + .ascii_art = ascii_art, + .line_len = line_len, + .line_count = line_count, + .fg = fg, .x = x, .y = y, + }; +} +pub fn animation(self: *Ascii) Animation { + return Animation.init(self, deinit, realloc, draw); +} + +fn deinit(_: *Ascii) void {} + +fn realloc(_: *Ascii) anyerror!void {} + +fn min(a: usize, b: usize) usize { + if (a < b) return a; + return b; +} + +fn draw(self: *Ascii) void { + const buf_width = self.terminal_buffer.width; + const buf_height = self.terminal_buffer.height; + + var y: usize = 0; + while (y < min(buf_height, self.line_count)) : (y += 1) { + const line_width = self.line_len[y]; + var x: usize = 0; + while (x < min(buf_width, line_width)) : (x += 1) { + const cell = Cell { + .ch = self.ascii_art[y][x], + .fg = self.fg, + .bg = self.terminal_buffer.bg, + }; + cell.put(x + self.x, y + self.y); + } + } +} diff --git a/src/config/Config.zig b/src/config/Config.zig index 92e0552..b5ebb9a 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -42,6 +42,10 @@ gameoflife_fg: u32 = 0x0000FF00, gameoflife_entropy_interval: usize = 10, gameoflife_frame_delay: usize = 6, gameoflife_initial_density: f32 = 0.4, +ascii_filename: []const u8 = "/etc/ly/config.ini", +ascii_fg: u32 = 0x00FF0000, +ascii_x: u32 = 0, +ascii_y: u32 = 0, hide_borders: bool = false, hide_version_string: bool = false, hide_key_hints: bool = false, diff --git a/src/enums.zig b/src/enums.zig index 82c478d..17662f7 100644 --- a/src/enums.zig +++ b/src/enums.zig @@ -4,6 +4,7 @@ pub const Animation = enum { matrix, colormix, gameoflife, + ascii, }; pub const DisplayServer = enum { diff --git a/src/main.zig b/src/main.zig index 0cc2c6e..168b229 100644 --- a/src/main.zig +++ b/src/main.zig @@ -13,6 +13,7 @@ const Doom = @import("animations/Doom.zig"); const Dummy = @import("animations/Dummy.zig"); const Matrix = @import("animations/Matrix.zig"); const GameOfLife = @import("animations/GameOfLife.zig"); +const Ascii = @import("animations/Ascii.zig"); const Animation = @import("tui/Animation.zig"); const TerminalBuffer = @import("tui/TerminalBuffer.zig"); const Session = @import("tui/components/Session.zig"); @@ -400,6 +401,10 @@ pub fn main() !void { var game_of_life = try GameOfLife.init(allocator, &buffer, config.gameoflife_fg, config.gameoflife_entropy_interval, config.gameoflife_frame_delay, config.gameoflife_initial_density); animation = game_of_life.animation(); }, + .ascii => { + var ascii = try Ascii.init(&buffer, config.ascii_filename, config.ascii_fg, config.ascii_x, config.ascii_y); + animation = ascii.animation(); + }, } defer animation.deinit();