add changes from code review

This commit is contained in:
hynak 2025-12-02 20:27:11 -05:00
commit cc45ac848d
3 changed files with 176 additions and 126 deletions

View file

@ -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;

View file

@ -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();

View file

@ -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;