From 5755550e84879cf9eb0aa405451309c91e81bb2d Mon Sep 17 00:00:00 2001 From: senchpimy Date: Sat, 6 Sep 2025 16:03:00 -0600 Subject: [PATCH] Metaballs animation --- src/animations/Metaballs.zig | 129 +++++++++++++++++++++++++++++++++++ src/enums.zig | 1 + src/main.zig | 120 +++++++++++++++++--------------- 3 files changed, 194 insertions(+), 56 deletions(-) create mode 100644 src/animations/Metaballs.zig diff --git a/src/animations/Metaballs.zig b/src/animations/Metaballs.zig new file mode 100644 index 0000000..1633703 --- /dev/null +++ b/src/animations/Metaballs.zig @@ -0,0 +1,129 @@ +const std = @import("std"); +const Allocator = std.mem.Allocator; +const Animation = @import("../tui/Animation.zig"); +const Cell = @import("../tui/Cell.zig"); +const TerminalBuffer = @import("../tui/TerminalBuffer.zig"); + +const Metaballs = @This(); + +const math = std.math; +const Vec2 = @Vector(2, f32); + +const num_metaballs = 5; +const min_radius: f32 = 5.0; +const max_radius: f32 = 12.0; +const max_speed: f32 = 0.2; + +const threshold: f32 = 0.7; + +const Metaball = struct { + pos: Vec2, + vel: Vec2, + radius: f32, +}; + +allocator: Allocator, +terminal_buffer: *TerminalBuffer, +balls: [num_metaballs]Metaball, +palette: [5]Cell, + +pub fn init(allocator: Allocator, terminal_buffer: *TerminalBuffer) !Metaballs { + var self = Metaballs{ + .allocator = allocator, + .terminal_buffer = terminal_buffer, + .balls = undefined, + .palette = [_]Cell{ + Cell.init(' ', 0x2c0000, 0x4f0000), + Cell.init(0x2591, 0x8b0000, 0xae0000), + Cell.init(0x2592, 0xff4500, 0xff6347), + Cell.init(0x2593, 0xffa500, 0xffd700), + Cell.init(0x2588, 0xffff00, 0xffffe0), + }, + }; + + self.initBalls(); + return self; +} + +fn initBalls(self: *Metaballs) void { + const width_f = @as(f32, @floatFromInt(self.terminal_buffer.width)); + const height_f = @as(f32, @floatFromInt(self.terminal_buffer.height)); + const rand = self.terminal_buffer.random; + + for (&self.balls) |*ball| { + ball.* = .{ + .pos = .{ + rand.float(f32) * width_f, + rand.float(f32) * height_f, + }, + .vel = .{ + (rand.float(f32) - 0.5) * 2.0 * max_speed, + (rand.float(f32) - 0.5) * 2.0 * max_speed, + }, + .radius = min_radius + (rand.float(f32) * (max_radius - min_radius)), + }; + } +} + +pub fn animation(self: *Metaballs) Animation { + return Animation.init(self, deinit, realloc, draw); +} + +fn deinit(_: *Metaballs) void {} + +fn realloc(self: *Metaballs) anyerror!void { + self.initBalls(); +} + +fn draw(self: *Metaballs) void { + const width = self.terminal_buffer.width; + const height = self.terminal_buffer.height; + const width_f = @as(f32, @floatFromInt(width)); + const height_f = @as(f32, @floatFromInt(height)); + + for (&self.balls) |*ball| { + ball.pos += ball.vel; + + if (ball.pos[0] < 0 or ball.pos[0] > width_f) { + ball.vel[0] *= -1.0; + ball.pos[0] = math.clamp(ball.pos[0], 0, width_f); + } + if (ball.pos[1] < 0 or ball.pos[1] > height_f) { + ball.vel[1] *= -1.0; + ball.pos[1] = math.clamp(ball.pos[1], 0, height_f); + } + } + + for (0..height) |y| { + for (0..width) |x| { + const cell_pos = Vec2{ + @as(f32, @floatFromInt(x)) + 0.5, + @as(f32, @floatFromInt(y)) + 0.5, + }; + + var sum_influence: f32 = 0.0; + for (self.balls) |ball| { + const dist_vec = cell_pos - ball.pos; + const dist_sq = (dist_vec[0] * dist_vec[0]) + (dist_vec[1] * dist_vec[1]); + + if (dist_sq == 0) { + sum_influence += 1000.0; + } else { + sum_influence += (ball.radius * ball.radius) / dist_sq; + } + } + + if (sum_influence > threshold * 1.1) { + self.palette[4].put(x, y); + } else if (sum_influence > threshold * 1.0) { + self.palette[3].put(x, y); + } else if (sum_influence > threshold * 0.9) { + self.palette[2].put(x, y); + } else if (sum_influence > threshold * 0.8) { + self.palette[1].put(x, y); + } else { + self.palette[0].put(x, y); + } + } + } +} diff --git a/src/enums.zig b/src/enums.zig index 82c478d..1c51be2 100644 --- a/src/enums.zig +++ b/src/enums.zig @@ -4,6 +4,7 @@ pub const Animation = enum { matrix, colormix, gameoflife, + metaballs, }; pub const DisplayServer = enum { diff --git a/src/main.zig b/src/main.zig index 2abce0b..d3bd1ec 100644 --- a/src/main.zig +++ b/src/main.zig @@ -10,6 +10,7 @@ const Environment = @import("Environment.zig"); const interop = @import("interop.zig"); const ColorMix = @import("animations/ColorMix.zig"); const Doom = @import("animations/Doom.zig"); +const Metaballs = @import("animations/Metaballs.zig"); const Dummy = @import("animations/Dummy.zig"); const Matrix = @import("animations/Matrix.zig"); const GameOfLife = @import("animations/GameOfLife.zig"); @@ -31,11 +32,12 @@ const Ini = ini.Ini; const DisplayServer = enums.DisplayServer; const Entry = Environment.Entry; const termbox = interop.termbox; +const unistd = interop.unistd; const temporary_allocator = std.heap.page_allocator; const ly_top_str = "Ly version " ++ build_options.version; var session_pid: std.posix.pid_t = -1; -fn signalHandler(i: c_int) callconv(.c) void { +fn signalHandler(i: c_int) callconv(.C) void { if (session_pid == 0) return; // Forward signal to session to clean up @@ -49,7 +51,7 @@ fn signalHandler(i: c_int) callconv(.c) void { std.c.exit(i); } -fn ttyControlTransferSignalHandler(_: c_int) callconv(.c) void { +fn ttyControlTransferSignalHandler(_: c_int) callconv(.C) void { _ = termbox.tb_shutdown(); } @@ -59,20 +61,16 @@ pub fn main() !void { var shutdown_cmd: []const u8 = undefined; var restart_cmd: []const u8 = undefined; - var stderr_buffer: [128]u8 = undefined; - var stderr_writer = std.fs.File.stderr().writer(&stderr_buffer); - var stderr = &stderr_writer.interface; + const stderr = std.io.getStdErr().writer(); defer { // If we can't shutdown or restart due to an error, we print it to standard error. If that fails, just bail out if (shutdown) { const shutdown_error = std.process.execv(temporary_allocator, &[_][]const u8{ "/bin/sh", "-c", shutdown_cmd }); stderr.print("error: couldn't shutdown: {s}\n", .{@errorName(shutdown_error)}) catch std.process.exit(1); - stderr.flush() catch std.process.exit(1); } else if (restart) { const restart_error = std.process.execv(temporary_allocator, &[_][]const u8{ "/bin/sh", "-c", restart_cmd }); stderr.print("error: couldn't restart: {s}\n", .{@errorName(restart_error)}) catch std.process.exit(1); - stderr.flush() catch std.process.exit(1); } else { // The user has quit Ly using Ctrl+C temporary_allocator.free(shutdown_cmd); @@ -84,7 +82,8 @@ pub fn main() !void { defer _ = gpa.deinit(); // Allows stopping an animation after some time - const time_start = try interop.getTimeOfDay(); + var tv_zero: interop.system_time.timeval = undefined; + _ = interop.system_time.gettimeofday(&tv_zero, null); var animation_timed_out: bool = false; const allocator = gpa.allocator(); @@ -99,7 +98,6 @@ pub fn main() !void { var diag = clap.Diagnostic{}; var res = clap.parse(clap.Help, ¶ms, clap.parsers.default, .{ .diagnostic = &diag, .allocator = allocator }) catch |err| { diag.report(stderr, err) catch {}; - try stderr.flush(); return err; }; defer res.deinit(); @@ -115,12 +113,10 @@ pub fn main() !void { try clap.help(stderr, clap.Help, ¶ms, .{}); _ = try stderr.write("Note: if you want to configure Ly, please check the config file, which is located at " ++ build_options.config_directory ++ "/ly/config.ini.\n"); - try stderr.flush(); std.process.exit(0); } if (res.args.version != 0) { _ = try stderr.write("Ly version " ++ build_options.version ++ "\n"); - try stderr.flush(); std.process.exit(0); } @@ -164,7 +160,7 @@ pub fn main() !void { .comment_characters = comment_characters, }) catch Lang{}; - if (config.save) { + if (config.load) { save_path = try std.fmt.allocPrint(allocator, "{s}{s}save.ini", .{ s, trailing_slash }); save_path_alloc = true; @@ -197,7 +193,7 @@ pub fn main() !void { .comment_characters = comment_characters, }) catch Lang{}; - if (config.save) { + if (config.load) { var user_buf: [32]u8 = undefined; save = save_ini.readFileToStruct(save_path, .{ .fieldHandler = null, @@ -227,16 +223,17 @@ pub fn main() !void { log_file = try std.fs.openFileAbsolute("/dev/null", .{ .mode = .write_only }); } - var log_buffer: [1024]u8 = undefined; - var log_file_writer = log_file.writer(&log_buffer); + const log_writer = log_file.writer(); - // Seek to the end of the log file - if (could_open_log_file) { - const stat = try log_file.stat(); - try log_file_writer.seekTo(stat.size); - } + // if (migrator.mapped_config_fields) save_migrated_config: { + // var file = try std.fs.cwd().createFile(config_path, .{}); + // defer file.close(); - var log_writer = &log_file_writer.interface; + // const writer = file.writer(); + // ini.writeFromStruct(config, writer, null, true, .{}) catch { + // break :save_migrated_config; + // }; + // } // These strings only end up getting freed if the user quits Ly using Ctrl+C, which is fine since in the other cases // we end up shutting down or restarting the system @@ -253,7 +250,7 @@ pub fn main() !void { const act = std.posix.Sigaction{ .handler = .{ .handler = &signalHandler }, - .mask = std.posix.sigemptyset(), + .mask = std.posix.empty_sigset, .flags = 0, }; std.posix.sigaction(std.posix.SIG.TERM, &act, null); @@ -390,7 +387,7 @@ pub fn main() !void { var insert_mode = !config.vi_mode or config.vi_default_mode == .insert; // Load last saved username and desktop selection, if any - if (config.save) { + if (config.load) { if (save.user) |user| { // Find user with saved name, and switch over to it // If it doesn't exist (anymore), we don't change the value @@ -456,6 +453,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(); }, + .metaballs => { + var metaballs = try Metaballs.init(allocator, &buffer); + animation = metaballs.animation(); + }, } defer animation.deinit(); @@ -540,7 +541,7 @@ pub fn main() !void { var format_buf: [16:0]u8 = undefined; var clock_buf: [32:0]u8 = undefined; // We need the slice/c-string returned by `bufPrintZ`. - const format = try std.fmt.bufPrintZ(&format_buf, "{s}{s}{s}{s}", .{ + const format: [:0]const u8 = try std.fmt.bufPrintZ(&format_buf, "{s}{s}{s}{s}", .{ if (config.bigclock_12hr) "%I" else "%H", ":%M", if (config.bigclock_seconds) ":%S" else "", @@ -552,8 +553,7 @@ pub fn main() !void { const clock_str = interop.timeAsString(&clock_buf, format); for (clock_str, 0..) |c, i| { - // TODO: Show error - const clock_cell = try bigclock.clockCell(animate, c, buffer.fg, buffer.bg, config.bigclock); + const clock_cell = bigclock.clockCell(animate, c, buffer.fg, buffer.bg, config.bigclock); bigclock.alphaBlit(xo + i * (bigclock.WIDTH + 1), yo, buffer.width, buffer.height, clock_cell); } } @@ -688,21 +688,24 @@ pub fn main() !void { if (animate and !animation_timed_out) { timeout = config.min_refresh_delta; - // Check how long we've been running so we can turn off the animation - const time = try interop.getTimeOfDay(); + // check how long we have been running so we can turn off the animation + var tv: interop.system_time.timeval = undefined; + _ = interop.system_time.gettimeofday(&tv, null); - if (config.animation_timeout_sec > 0 and time.seconds - time_start.seconds > config.animation_timeout_sec) { + if (config.animation_timeout_sec > 0 and tv.tv_sec - tv_zero.tv_sec > config.animation_timeout_sec) { animation_timed_out = true; animation.deinit(); } } else if (config.bigclock != .none and config.clock == null) { - const time = try interop.getTimeOfDay(); + var tv: interop.system_time.timeval = undefined; + _ = interop.system_time.gettimeofday(&tv, null); - timeout = @intCast((60 - @rem(time.seconds, 60)) * 1000 - @divTrunc(time.microseconds, 1000) + 1); + timeout = @intCast((60 - @rem(tv.tv_sec, 60)) * 1000 - @divTrunc(tv.tv_usec, 1000) + 1); } else if (config.clock != null or auth_fails >= config.auth_fails) { - const time = try interop.getTimeOfDay(); + var tv: interop.system_time.timeval = undefined; + _ = interop.system_time.gettimeofday(&tv, null); - timeout = @intCast(1000 - @divTrunc(time.microseconds, 1000) + 1); + timeout = @intCast(1000 - @divTrunc(tv.tv_usec, 1000) + 1); } const event_error = if (timeout == -1) termbox.tb_poll_event(&event) else termbox.tb_peek_event(&event, timeout); @@ -794,7 +797,7 @@ pub fn main() !void { update = true; }, termbox.TB_KEY_ENTER => authenticate: { - try log_writer.writeAll("authenticating...\n"); + try log_writer.writeAll("authenticating..."); if (!config.allow_empty_password and password.text.items.len == 0) { // Let's not log this message for security reasons @@ -820,16 +823,11 @@ pub fn main() !void { var file = std.fs.cwd().createFile(save_path, .{}) catch break :save_last_settings; defer file.close(); - var file_buffer: [64]u8 = undefined; - var file_writer = file.writer(&file_buffer); - var writer = &file_writer.interface; - const save_data = Save{ .user = login.getCurrentUser(), .session_index = session.label.current, }; - ini.writeFromStruct(save_data, writer, null, .{}) catch break :save_last_settings; - try writer.flush(); + ini.writeFromStruct(save_data, file.writer(), null, .{}) catch break :save_last_settings; // Delete previous save file if it exists if (migrator.maybe_save_file) |path| std.fs.cwd().deleteFile(path) catch {}; @@ -839,6 +837,11 @@ pub fn main() !void { defer shared_err.deinit(); { + const login_text = try allocator.dupeZ(u8, login.getCurrentUser()); + defer allocator.free(login_text); + const password_text = try allocator.dupeZ(u8, password.text.items); + defer allocator.free(password_text); + session_pid = try std.posix.fork(); if (session_pid == 0) { const current_environment = session.label.list.items[session.label.current]; @@ -857,12 +860,12 @@ pub fn main() !void { // Signal action to give up control on the TTY const tty_control_transfer_act = std.posix.Sigaction{ .handler = .{ .handler = &ttyControlTransferSignalHandler }, - .mask = std.posix.sigemptyset(), + .mask = std.posix.empty_sigset, .flags = 0, }; std.posix.sigaction(std.posix.SIG.CHLD, &tty_control_transfer_act, null); - auth.authenticate(allocator, log_writer, auth_options, current_environment, login.getCurrentUser(), password.text.items) catch |err| { + auth.authenticate(auth_options, current_environment, login_text, password_text) catch |err| { shared_err.writeError(err); std.process.exit(1); }; @@ -900,7 +903,7 @@ pub fn main() !void { password.clear(); try info_line.addMessage(lang.logout, config.bg, config.fg); - try log_writer.writeAll("logged out\n"); + try log_writer.writeAll("logged out"); } try std.posix.tcsetattr(std.posix.STDIN_FILENO, .FLUSH, tb_termios); @@ -957,15 +960,13 @@ pub fn main() !void { update = true; }, } - - try log_writer.flush(); } } fn ttyClearScreen() !void { // Clear the TTY because termbox2 doesn't seem to do it properly const capability = termbox.global.caps[termbox.TB_CAP_CLEAR_SCREEN]; - const capability_slice = std.mem.span(capability); + const capability_slice = capability[0..std.mem.len(capability)]; _ = try std.posix.write(termbox.global.ttyfd, capability_slice); } @@ -1016,7 +1017,7 @@ fn crawl(session: *Session, lang: Lang, path: []const u8, display_server: Displa // Prepare the XDG_CURRENT_DESKTOP environment variable here const entry = entry_ini.data.@"Desktop Entry"; - var maybe_xdg_desktop_names: ?[]const u8 = null; + var maybe_xdg_desktop_names: ?[:0]const u8 = null; if (entry.DesktopNames) |desktop_names| { for (desktop_names) |*c| { if (c.* == ';') c.* = ':'; @@ -1024,10 +1025,13 @@ fn crawl(session: *Session, lang: Lang, path: []const u8, display_server: Displa maybe_xdg_desktop_names = desktop_names; } + const maybe_session_desktop = if (maybe_xdg_session_desktop) |xdg_session_desktop| try session.label.allocator.dupeZ(u8, xdg_session_desktop) else null; + errdefer if (maybe_session_desktop) |session_desktop| session.label.allocator.free(session_desktop); + try session.addEnvironment(.{ .entry_ini = entry_ini, .name = entry.Name, - .xdg_session_desktop = maybe_xdg_session_desktop, + .xdg_session_desktop = maybe_session_desktop, .xdg_desktop_names = maybe_xdg_desktop_names, .cmd = entry.Exec, .specifier = switch (display_server) { @@ -1046,20 +1050,24 @@ fn getAllUsernames(allocator: std.mem.Allocator, login_defs_path: []const u8) !S const uid_range = try getUserIdRange(allocator, login_defs_path); var usernames: StringList = .empty; - var maybe_entry = interop.getNextUsernameEntry(); + var maybe_entry = interop.pwd.getpwent(); + + while (maybe_entry != null) { + const entry = maybe_entry.*; - while (maybe_entry) |entry| { // We check if the UID is equal to 0 because we always want to add root // as a username (even if you can't log into it) - if (entry.uid >= uid_range.uid_min and entry.uid <= uid_range.uid_max or entry.uid == 0 and entry.username != null) { - const username = try allocator.dupe(u8, entry.username.?); + if (entry.pw_uid >= uid_range.uid_min and entry.pw_uid <= uid_range.uid_max or entry.pw_uid == 0) { + const pw_name_slice = entry.pw_name[0..std.mem.len(entry.pw_name)]; + const username = try allocator.dupe(u8, pw_name_slice); + try usernames.append(allocator, username); } - maybe_entry = interop.getNextUsernameEntry(); + maybe_entry = interop.pwd.getpwent(); } - interop.closePasswordDatabase(); + interop.pwd.endpwent(); return usernames; } @@ -1079,9 +1087,9 @@ fn getUserIdRange(allocator: std.mem.Allocator, login_defs_path: []const u8) !Ui const trimmed_line = std.mem.trim(u8, line, " \n\r\t"); if (std.mem.startsWith(u8, trimmed_line, "UID_MIN")) { - uid_range.uid_min = try parseValue(std.posix.uid_t, "UID_MIN", trimmed_line); + uid_range.uid_min = try parseValue(std.c.uid_t, "UID_MIN", trimmed_line); } else if (std.mem.startsWith(u8, trimmed_line, "UID_MAX")) { - uid_range.uid_max = try parseValue(std.posix.uid_t, "UID_MAX", trimmed_line); + uid_range.uid_max = try parseValue(std.c.uid_t, "UID_MAX", trimmed_line); } }