From cc45ac848d526f2369a78b009072407d9a9fd0cc Mon Sep 17 00:00:00 2001 From: hynak Date: Tue, 2 Dec 2025 20:27:11 -0500 Subject: [PATCH] add changes from code review --- src/animations/DurFile.zig | 284 +++++++++++++++++++++---------------- src/main.zig | 10 +- src/tui/TerminalBuffer.zig | 8 ++ 3 files changed, 176 insertions(+), 126 deletions(-) diff --git a/src/animations/DurFile.zig b/src/animations/DurFile.zig index 6d75871..601cdfe 100644 --- a/src/animations/DurFile.zig +++ b/src/animations/DurFile.zig @@ -2,12 +2,31 @@ const std = @import("std"); const Animation = @import("../tui/Animation.zig"); const Cell = @import("../tui/Cell.zig"); const TerminalBuffer = @import("../tui/TerminalBuffer.zig"); +const Color = TerminalBuffer.Color; +const Styling = TerminalBuffer.Styling; const Allocator = std.mem.Allocator; const Json = std.json; const eql = std.mem.eql; -const flate = std.compress.flate; // compress.gzip is moved to flate with zig 15 +const flate = std.compress.flate; -const DurParseError = error{ FileNotFound, NotValidFile }; +fn read_decompress_file(allocator: Allocator, file_path: []const u8) ![]u8 { + const file_buffer = std.fs.cwd().openFile(file_path, .{}) catch { + return error.FileNotFound; + }; + defer file_buffer.close(); + + var file_reader_buffer: [4096]u8 = undefined; + var decompress_buffer: [flate.max_window_len]u8 = undefined; + + var file_reader = file_buffer.reader(&file_reader_buffer); + var decompress: flate.Decompress = .init(&file_reader.interface, .gzip, &decompress_buffer); + + const file_decompressed = decompress.reader.allocRemaining(allocator, .unlimited) catch { + return error.NotValidFile; + }; + + return file_decompressed; +} const Frame = struct { frameNumber: i32, @@ -76,27 +95,8 @@ const DurFormat = struct { return false; } - fn read_decompress_dur(allocator: Allocator, file_path: []const u8) ![]u8 { - const file_buffer = std.fs.cwd().openFile(file_path, .{}) catch { - return error.FileNotFound; - }; - defer file_buffer.close(); - - var file_reader_buffer: [4096]u8 = undefined; - var decompress_buffer: [flate.max_window_len]u8 = undefined; - - var file_reader = file_buffer.reader(&file_reader_buffer); - var decompress: flate.Decompress = .init(&file_reader.interface, .gzip, &decompress_buffer); - - const file_decompressed = decompress.reader.allocRemaining(allocator, .unlimited) catch { - return error.NotValidFile; - }; - - return file_decompressed; - } - fn parse_dur_from_json(self: *DurFormat, allocator: Allocator, dur_json_root: Json.Value) !void { - var dur_movie = if (dur_json_root.object.get("DurMovie")) |dm| dm.object else { return error.NotValidFile; }; + var dur_movie = if (dur_json_root.object.get("DurMovie")) |dm| dm.object else return error.NotValidFile; // Depending on the version, a dur file can have different json object names (ie: columns vs sizeX) self.formatVersion = if (dur_movie.get("formatVersion"))|x| x.integer else null; @@ -113,40 +113,44 @@ const DurFormat = struct { self.frames = try .initCapacity(allocator, frames.array.items.len); - for (frames.array.items) |frame| { - var parsed_frame = try Json.parseFromValue(Frame, allocator, frame, .{}); + for (frames.array.items) |json_frame| { + var parsed_frame = try Json.parseFromValue(Frame, allocator, json_frame, .{}); defer parsed_frame.deinit(); const frame_val = parsed_frame.value; // copy all fields to own the ptrs for deallocation, the parsed_frame has some other // allocated memory making it difficult to deallocate without leaks - const tmp_frame: Frame = .{ + const frame: Frame = .{ .frameNumber = frame_val.frameNumber, .delay = frame_val.delay, .contents = try allocator.alloc([]u8, frame_val.contents.len), .colorMap = try allocator.alloc([][]i32, frame_val.colorMap.len) }; - for (0..tmp_frame.contents.len) |i| { - tmp_frame.contents[i] = try allocator.dupe(u8, frame_val.contents[i]); + for (0..frame.contents.len) |i| { + frame.contents[i] = try allocator.dupe(u8, frame_val.contents[i]); } - - for (0..tmp_frame.colorMap.len) |i| { - tmp_frame.colorMap[i] = try allocator.alloc([]i32, frame_val.colorMap[i].len); - for (0..tmp_frame.colorMap[i].len) |j| { - tmp_frame.colorMap[i][j] = try allocator.alloc(i32, 2); - tmp_frame.colorMap[i][j][0] = frame_val.colorMap[i][j][0]; - tmp_frame.colorMap[i][j][1] = frame_val.colorMap[i][j][1]; + + // colorMap is stored as an 3d array where: + // the outer (i) most array is the horizontal position of the color + // the middle (j) is the vertical position of the color + // the inner (0/1) is the foreground/background color + for (0..frame.colorMap.len) |i| { + frame.colorMap[i] = try allocator.alloc([]i32, frame_val.colorMap[i].len); + for (0..frame.colorMap[i].len) |j| { + frame.colorMap[i][j] = try allocator.alloc(i32, 2); + frame.colorMap[i][j][0] = frame_val.colorMap[i][j][0]; + frame.colorMap[i][j][1] = frame_val.colorMap[i][j][1]; } } - try self.frames.append(allocator, tmp_frame); + try self.frames.append(allocator, frame); } } pub fn create_from_file(self: *DurFormat, allocator: Allocator, file_path: [] const u8) !void { - const file_decompressed = try read_decompress_dur(allocator, file_path); + const file_decompressed = try read_decompress_file(allocator, file_path); defer allocator.free(file_decompressed); const parsed = try Json.parseFromSlice(Json.Value, allocator, file_decompressed, .{}); @@ -162,8 +166,8 @@ const DurFormat = struct { } pub fn deinit(self: *DurFormat) void { - if (self.colorFormat != null) self.allocator.free(self.colorFormat.?); - if (self.encoding != null) self.allocator.free(self.encoding.?); + if (self.colorFormat) |str| self.allocator.free(str); + if (self.encoding) |str| self.allocator.free(str); for (self.frames.items) |frame| { frame.deinit(self.allocator); @@ -173,42 +177,42 @@ const DurFormat = struct { }; const tb_color_16 = [16]u32{ - 0x00000001, // black - 0x00000002, // red - 0x00000003, // green - 0x00000004, // yellow - 0x00000005, // blue - 0x00000006, // magenta - 0x00000007, // cyan - 0x00000008, // light gray - 0x01000001, // gray (bright black) - 0x01000002, // bright red - 0x01000003, // bright green - 0x01000004, // bright yellow - 0x01000005, // bright blue - 0x01000006, // bright magenta - 0x01000007, // bright cyan - 0x01000008 // white + Color.ECOL_BLACK, + Color.ECOL_RED, + Color.ECOL_GREEN, + Color.ECOL_YELLOW, + Color.ECOL_BLUE, + Color.ECOL_MAGENTA, + Color.ECOL_CYAN, + Color.ECOL_WHITE, + Color.ECOL_BLACK | Styling.BOLD, + Color.ECOL_RED | Styling.BOLD, + Color.ECOL_GREEN | Styling.BOLD, + Color.ECOL_YELLOW | Styling.BOLD, + Color.ECOL_BLUE | Styling.BOLD, + Color.ECOL_MAGENTA | Styling.BOLD, + Color.ECOL_CYAN | Styling.BOLD, + Color.ECOL_WHITE | Styling.BOLD, }; // Using bold for bright colors allows for all 16 colors to be rendered on tty term const rgb_color_16 = [16]u32{ - 0x00000000, // black - 0x00800000, // red - 0x00008000, // green - 0x00808000, // yellow - 0x00000080, // blue - 0x00800080, // magenta - 0x00008080, // cyan - 0x00C0C0C0, // light gray - 0x00808080, // gray - 0x01FF5555, // bright red - 0x0100FF00, // bright green - 0x01FFFF00, // bright yellow - 0x010000FF, // bright blue - 0x01FF00FF, // bright magenta - 0x0100FFFF, // bright cyan - 0x01FFFFFF, // bright white + Color.DEFAULT, // DEFAULT instead of TRUE_BLACK to not break compositors (the ladder ignores transparency) + Color.TRUE_DIM_RED, + Color.TRUE_DIM_GREEN, + Color.TRUE_DIM_YELLOW, + Color.TRUE_DIM_BLUE, + Color.TRUE_DIM_MAGENTA, + Color.TRUE_DIM_CYAN, + Color.TRUE_DIM_WHITE, + Color.TRUE_GRAY, + Color.TRUE_RED | Styling.BOLD, + Color.TRUE_GREEN | Styling.BOLD, + Color.TRUE_YELLOW | Styling.BOLD, + Color.TRUE_BLUE | Styling.BOLD, + Color.TRUE_MAGENTA | Styling.BOLD, + Color.TRUE_CYAN | Styling.BOLD, + Color.TRUE_WHITE | Styling.BOLD, }; // Made this table from looking at colormapping in dur source, not sure whats going on with the mapping logic @@ -233,34 +237,56 @@ const durcolor_table_to_color16 = [17]u32{ 15, // 16 bright white }; -// 256 to rgb math -// For extended term range we subtract by 16 to get it in a 0..(6x6x6) cube range -// divide by 36 gets the depth of the cube -// divide by 6 gets the width of the cube -// divide by 1 gets the height of the cube (divide 1 for clarity for what we are doing) -// -// Each channel can be 6 levels of brightness hence remander operation of 6 -// -// 0x28 = 0xF0 / 6 # This gets us channel color in rgb, 6 equal steps -// 0x37 = 0xFF - (0x28 * 5) # 0x37 is the remander scaler - fn sixcube_to_channel(sixcube: u32) u32 { - return if (sixcube > 0) (sixcube * 0x28) + 0x37 else 0; + // Although the range top for the extended range is 0xFF, 6 is not divisible into 0xFF, + // so we use 0xF0 instead with a scaler + const equal_divisions = 0xF0 / 6; + + // Since the range is to 0xFF but 6 isn't divisible, we must add a scaler to get it to 0xFF at the last index (5) + const scaler = 0xFF - (equal_divisions * 5); + + return if (sixcube > 0) (sixcube * equal_divisions) + scaler else 0; } fn convert_256_to_rgb(color_256: u32) u32 { var rgb_color: u32 = 0; + // 0 - 15 is the standard color range, map to array table if (color_256 < 16) { rgb_color = rgb_color_16[color_256]; } + // 16 - 231 is the extended range else if (color_256 < 232) { - rgb_color |= sixcube_to_channel(((color_256 - 16) / 36) % 6 ) << 16; - rgb_color |= sixcube_to_channel(((color_256 - 16) / 6) % 6 ) << 8; - rgb_color |= sixcube_to_channel(((color_256 - 16) / 1) % 6 ); + + // For extended term range we subtract by 16 to get it in a 0..(6x6x6) cube (range of 216) + // divide by 36 gets the depth of the cube (6x6x1) + // divide by 6 gets the width of the cube (6x1) + // divide by 1 gets the height of the cube (divide 1 for clarity for what we are doing) + // each channel can be 6 levels of brightness hence remander operation of 6 + // finally bitshift to correct rgb channel (16 for red, 8 for green, 0 for blue) + rgb_color |= sixcube_to_channel(((color_256 - 16) / 36) % 6) << 16; + rgb_color |= sixcube_to_channel(((color_256 - 16) / 6) % 6) << 8; + rgb_color |= sixcube_to_channel(((color_256 - 16) / 1) % 6); } + // 232 - 255 is the grayscale range else { - const channel = 0x08 + 0x0a * (color_256 - 232); + + // For grayscale we have a space of 232 - 255 (24) + // subtract by 232 to get it into the 0..23 range + // standard colors will contain white and black, so we do not use them in the grayscale range (0 is 0x08, 23 is 0xEE) + // this results in a skip of 0x08 for the first color and divisions of 0x0A + // example: term_col 232 = scaler + equal_divisions * (232 - 232) which becomes (scaler + 0x00) == 0x08 + // example: term_col 255 = scaler + equal_divisions * (255 - 232) which becomes (scaler + 0xE6) == 0xEE + const scaler = 0x08; + + // to get equal parts, the equation is: + // 0xEE = equal_divisions * 23 + scaler | top of range is 0xEE, 23 is last element value (255 minus 232) + // reordered to solve for equal_divisions: + const equal_divisions = (0xEE - scaler) / 23; // evals to 0x0A + + const channel = scaler + equal_divisions * (color_256 - 232); + + // gray is equal value of same channel color in rgb rgb_color = channel | (channel << 8) | (channel << 16); } @@ -278,6 +304,10 @@ x_offset: u32, y_offset: u32, full_color: bool, dur_movie: DurFormat, +frame_width: u32, +frame_height: u32, +frame_time: u32, +is_color_format_16 : bool, pub fn init(allocator: Allocator, terminal_buffer: *TerminalBuffer, @@ -287,32 +317,57 @@ pub fn init(allocator: Allocator, y_offset: u32, full_color: bool) !DurFile { var dur_movie: DurFormat = .init(allocator); - - // error state when DurParseError is recoverable and results in no background + + // error state is recoverable when thrown to main and results in no background with Dummy in main dur_movie.create_from_file(allocator, file_path) catch |err| switch (err) { - error.FileNotFound => { - try log_writer.print("DurFile was not found at {s}", .{file_path}); + error.FileNotFound => { + try log_writer.print("error: dur_file was not found at: {s}\n", .{file_path}); + return err; }, error.NotValidFile => { - try log_writer.print("DurFile was invalid or not a dur file!", .{}); - }, - else => { - return err; + try log_writer.print("error: dur_file loaded was invalid or not a dur file!\n", .{}); + return err; }, + else => return err, }; + // 4 bit mode with 256 color is unsupported + if (!full_color and eql(u8, dur_movie.colorFormat.?, "256")) { + try log_writer.print("error: dur_file can not be 256 color encoded when not using full_color option!\n", .{}); + dur_movie.deinit(); + return error.InvalidColorFormat; + } + const buf_width: u32 = @intCast(terminal_buffer.width); const buf_height: u32 = @intCast(terminal_buffer.height); + const movie_width: u32 = @intCast(dur_movie.columns.?); + const movie_height: u32 = @intCast(dur_movie.lines.?); + + // Clamp to prevent user from exceeding draw window + const x_offset_clamped = std.math.clamp(x_offset, 0, buf_width - 1); + const y_offset_clamped = std.math.clamp(y_offset, 0, buf_height - 1); + + // Ensure if user offsets and frame goes offscreen, it will not overflow draw + const frame_width = if ((movie_width + x_offset_clamped) < buf_width) movie_width else buf_width - x_offset_clamped; + const frame_height = if ((movie_height + y_offset_clamped) < buf_height) movie_height else buf_height - y_offset_clamped; + + // Convert dur fps to frames per ms + const frame_time: u32 = @intFromFloat(1000 / dur_movie.framerate.?); + return .{ .allocator = allocator, .terminal_buffer = terminal_buffer, .frames = 0, .time_previous = std.time.milliTimestamp(), - .x_offset = std.math.clamp(x_offset, 0, buf_width - 1), - .y_offset = std.math.clamp(y_offset, 0, buf_height - 1), + .x_offset = x_offset_clamped, + .y_offset = y_offset_clamped, .full_color = full_color, .dur_movie = dur_movie, + .frame_width = frame_width, + .frame_height = frame_height, + .frame_time = frame_time, + .is_color_format_16 = eql(u8, dur_movie.colorFormat.?, "16") }; } @@ -327,41 +382,24 @@ fn deinit(self: *DurFile) void { fn realloc(_: *DurFile) anyerror!void {} fn draw(self: *DurFile) void { - // dur_movie will be invalid if DurParseError errored out in init - if (!self.dur_movie.valid()) return; - // 4 bit mode with 256 color is unsupported - if (!self.full_color and eql(u8, self.dur_movie.colorFormat.?, "256")) return; - - const buf_width: u32 = @intCast(self.terminal_buffer.width); - const buf_height: u32 = @intCast(self.terminal_buffer.height); - - const movie_width:u32 = @intCast(self.dur_movie.columns.?); - const movie_height:u32 = @intCast(self.dur_movie.lines.?); - - // Ensure if user offsets and frame goes offscreen, it will not overflow draw - const frame_width = if ((movie_width + self.x_offset) < buf_width) movie_width else buf_width - self.x_offset; - const frame_height = if ((movie_height + self.y_offset) < buf_height) movie_height else buf_height - self.y_offset; - const current_frame = self.dur_movie.frames.items[self.frames]; - for (0..frame_width) |x| { - for (0..frame_height) |y| { - var iter = std.unicode.Utf8View.initUnchecked(current_frame.contents[y]).iterator(); - - // Peak programming here, could not find a better way to get specific codepoints from an offset - for (0..x) |_| {_ = iter.nextCodepoint().?;} + for (0..self.frame_height) |y| { + var iter = std.unicode.Utf8View.initUnchecked(current_frame.contents[y]).iterator(); + + for (0..self.frame_width) |x| { const codepoint: u21 = iter.nextCodepoint().?; var color_map_0: u32 = @intCast(current_frame.colorMap[x][y][0]); var color_map_1: u32 = @intCast(current_frame.colorMap[x][y][1]); - if (eql(u8, self.dur_movie.colorFormat.?, "16")) { + if (self.is_color_format_16) { color_map_0 = durcolor_table_to_color16[color_map_0]; color_map_1 = durcolor_table_to_color16[color_map_1 + 1]; // Add 1, dur source stores it like this for some reason } - const fg_color = if(self.full_color) convert_256_to_rgb(color_map_0) else tb_color_16[color_map_0]; - const bg_color = if(self.full_color) convert_256_to_rgb(color_map_1) else tb_color_16[color_map_1]; + const fg_color = if (self.full_color) convert_256_to_rgb(color_map_0) else tb_color_16[color_map_0]; + const bg_color = if (self.full_color) convert_256_to_rgb(color_map_1) else tb_color_16[color_map_1]; const cell = Cell { .ch = @intCast(codepoint), @@ -376,11 +414,9 @@ fn draw(self: *DurFile) void { const time_current = std.time.milliTimestamp(); const delta_time = time_current - self.time_previous; - // Convert fps to time in ms, and delay from sec to ms - const frame_time: u32 = @intFromFloat(1000 / self.dur_movie.framerate.?); + // Convert delay from sec to ms const delay_time: u32 = @intCast(current_frame.delay * 1000); - - if (delta_time > (frame_time + delay_time)) { + if (delta_time > (self.frame_time + delay_time)) { self.time_previous = time_current; const frame_count = self.dur_movie.frames.items.len; diff --git a/src/main.zig b/src/main.zig index 608f4e2..0c68c94 100644 --- a/src/main.zig +++ b/src/main.zig @@ -574,8 +574,14 @@ pub fn main() !void { animation = game_of_life.animation(); }, .dur_file => { - var dur = try DurFile.init(allocator, &buffer, log_writer, config.dur_file_path, config.dur_x_offset, config.dur_y_offset, config.full_color); - animation = dur.animation(); + var dur : ?DurFile = DurFile.init(allocator, &buffer, log_writer, config.dur_file_path, config.dur_x_offset, config.dur_y_offset, config.full_color) catch null; + + if (dur) |*d| { + animation = d.animation(); + } else { + var dummy = Dummy{}; + animation = dummy.animation(); + } }, } defer animation.deinit(); diff --git a/src/tui/TerminalBuffer.zig b/src/tui/TerminalBuffer.zig index f09877d..f72d4e2 100644 --- a/src/tui/TerminalBuffer.zig +++ b/src/tui/TerminalBuffer.zig @@ -38,6 +38,14 @@ pub const Color = struct { pub const TRUE_MAGENTA = 0x00FF00FF; pub const TRUE_CYAN = 0x0000FFFF; pub const TRUE_WHITE = 0x00FFFFFF; + pub const TRUE_GRAY = 0x00808080; + pub const TRUE_DIM_RED = 0x00800000; + pub const TRUE_DIM_GREEN = 0x00008000; + pub const TRUE_DIM_YELLOW = 0x00808000; + pub const TRUE_DIM_BLUE = 0x00000080; + pub const TRUE_DIM_MAGENTA = 0x00800080; + pub const TRUE_DIM_CYAN = 0x00008080; + pub const TRUE_DIM_WHITE = 0x00C0C0C0; pub const ECOL_BLACK = 1; pub const ECOL_RED = 2; pub const ECOL_GREEN = 3;