diff --git a/build.zig b/build.zig index 2de19f7..a3a5f22 100644 --- a/build.zig +++ b/build.zig @@ -71,12 +71,19 @@ pub fn build(b: *std.Build) !void { }), }); + const zlua = b.dependency("zlua", .{ + .target = target, + .optimize = optimize, + .lang = .luajit, + }); + exe.root_module.addImport("zlua", zlua.module("zlua")); + const ly_ui = b.dependency("ly_ui", .{ .target = target, .optimize = optimize, .enable_x11_support = enable_x11_support, .fallback_uid_min = fallback_uid_min, - .fallback_uid_max = fallback_uid_max + .fallback_uid_max = fallback_uid_max, }); exe.root_module.addImport("ly-ui", ly_ui.module("ly-ui")); @@ -189,6 +196,8 @@ fn install_ly(allocator: std.mem.Allocator, io: std.Io, patch_map: PatchMap, ins try installText(io, patched_setup, config_dir, ly_config_directory, "setup.sh", .{ .permissions = .fromMode(0o755) }); try installFile(io, "res/example.dur", config_dir, ly_config_directory, "example.dur", .{ .permissions = .fromMode(0o755) }); + + try installFile(io, "res/example.lua", config_dir, ly_config_directory, "example.lua", .{ .permissions = .fromMode(0o755) }); } { diff --git a/build.zig.zon b/build.zig.zon index 3c091cb..1d19b69 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -11,6 +11,10 @@ .url = "git+https://github.com/Hejsil/zig-clap#fc1e5cc3f6d9d3001112385ee6256d694e959d2f", .hash = "clap-0.11.0-oBajB7foAQC3Iyn4IVCkUdYaOVVng5IZkSncySTjNig1", }, + .zlua = .{ + .url = "git+https://github.com/natecraddock/ziglua?ref=zig-0.16#8f271c82baa5fc43aa02a72f6da020c2025d9436", + .hash = "zlua-0.1.0-hGRpC2aABQD4D9PBVH3wAW8k32-I4969MRQ0CpOwoley", + }, }, .paths = .{ "build.zig", diff --git a/res/config.ini b/res/config.ini index a290773..169f5ea 100644 --- a/res/config.ini +++ b/res/config.ini @@ -25,6 +25,7 @@ allow_empty_password = true # colormix -> Color mixing shader # gameoflife -> John Conway's Game of Life # dur_file -> .dur file format (https://github.com/cmang/durdraw/tree/master) +# lua -> user-made animation written in LuaJIT animation = none # Delay between each animation frame in milliseconds @@ -298,6 +299,9 @@ login_defs_path = /etc/login.defs # no need to add `exec "$@"` at the end logout_cmd = null +# The file pointing to the Lua file to be used when using the Lua animation option +lua_animation_file = $CONFIG_DIRECTORY/ly/example.lua + # General log file path # If null, syslog will be used instead ly_log = /var/log/ly.log diff --git a/res/example.lua b/res/example.lua new file mode 100644 index 0000000..7309da6 --- /dev/null +++ b/res/example.lua @@ -0,0 +1,119 @@ +-- [[ +-- This is an example of using LuaJIT to create a custom animation in Ly, in this case +-- bouncing squares that change colors. +-- +-- You are given the following `ly` table: +-- { +-- height: number -- The height of the terminal +-- width: number -- The width of the terminal +-- putCell(byte, fg, bg, x, y) -- Draw a cell. +-- All arguments to this function are integers, and +-- must be in the unsigned 32-bit integer range: 0 to 2^32-1. +-- If an argument cannot be converted to this range, it will throw +-- an error. +-- +-- For reference, the XY coordinates (0,0) draw a cell on the top-left +-- of the terminal, where the positive-X axis moves right and the +-- positive-Y axis moves down. +-- +-- For arguments fg and bg: they are colors in the format +-- 0xSSRRGGBB, where SS is for styling. See your +-- config.ini for more details. +-- +-- For the byte argument, you may use string.byte to fill this argument. +-- +-- putCell(byte, fg, bg, x, y, w, h) -- Draw a rectangle. +-- Arguments are the same as putCell except for w and h, which are also +-- unsigned integers. The rectangle will be drawn from the top-left, with +-- argument w extending it to the right and argument h extending downwards. +-- +-- +-- putLabel(str, fg, bg, x, y) -- Draw text in argument str. See putCell() +-- for info on the rest of the arguments. +-- +-- clock() -- The time, in microseconds. +-- } +-- +-- A function named `draw()` must be declared in the script. This is ran every +-- frame. +-- +-- In addition to the base library, you are also given the following standard +-- libraries: +-- bit (A library exclusive to LuaJIT, see https://bitop.luajit.org/api.html) +-- math +-- string +-- table +-- +-- The std libraries io and debug are NOT included. +-- +-- ]] + +-- You should probably copy FPS and FPS_COUNT into any future LuaJIT animations +-- you create. +local FPS_COUNT = 40 +local function FPS() + return (1/FPS_COUNT)*1000000 +end + + +local SQUARE_WIDTH = 10 +local SQUARE_HEIGHT = 5 + +local SQUARE_COUNT = 25 + +local squares = {} + +for i = 1, SQUARE_COUNT do + local vx = 1 + local vy = 1 + if math.random(1, 2) == 2 then vx = -vx end + if math.random(1, 2) == 2 then vy = -vy end + squares[#squares+1] = { + x = math.random(1, ly.width - SQUARE_WIDTH), + y = math.random(1, ly.height - SQUARE_HEIGHT), + vx = vx, + vy = vy, + color = math.random(0xFFFFFF) + } +end + +local timer = ly.clock() +local perf = ly.clock() + +function draw() + -- Rather than progressing the animation by frame, do it based on + -- seconds, via ly.clock(). In this timeframe, you can update the animation + -- state. + -- DO NOT DRAW CELLS IN THIS TIMEFRAME. You will get flickering. + + -- if this check passes, we can update the animation + if timer + FPS() < ly.clock() then + for i, v in ipairs(squares) do + v.x = v.x + v.vx + v.y = v.y + v.vy + if v.x == 0 then + v.vx = 1; v.color = math.random(0xFFFFFF) + end + if v.x + SQUARE_WIDTH >= ly.width-1 then + v.vx = -1; v.color = math.random(0xFFFFFF) + end + if v.y == 0 then + v.vy = 1; v.color = math.random(0xFFFFFF) + end + if v.y + SQUARE_HEIGHT >= ly.height-1 then + v.vy = -1; v.color = math.random(0xFFFFFF) + end + end + timer = ly.clock() + end + + + for i, v in ipairs(squares) do + ly.putRect(string.byte(' '), 0, v.color, v.x, v.y, SQUARE_WIDTH, SQUARE_HEIGHT) + end + + local new_perf = ly.clock() + local str = "FT: "..((new_perf - perf) / 1000).."ms" + ly.putLabel(str , 0x00FFFFFF, 0, (ly.width/2) - (string.len(str)/2), ly.height-1) + perf = new_perf +end diff --git a/src/animations/Lua.zig b/src/animations/Lua.zig new file mode 100644 index 0000000..d212e03 --- /dev/null +++ b/src/animations/Lua.zig @@ -0,0 +1,302 @@ +const std = @import("std"); +const ly_ui = @import("ly-ui"); +const LogFile = ly_ui.ly_core.LogFile; +const Widget = ly_ui.Widget; +const TerminalBuffer = ly_ui.TerminalBuffer; +const Cell = ly_ui.Cell; +const Allocator = std.mem.Allocator; +const InfoLine = @import("../components/InfoLine.zig"); +const Lang = @import("../config/Lang.zig"); + +const zlua = @import("zlua"); + +const ly_lua = @embedFile("ly.lua"); + +const Lua = @This(); + +allocator: Allocator, +instance: ?Widget = null, +lua: *zlua.Lua, +log: *LogFile, +terminal_buffer: *TerminalBuffer, +width: usize, +height: usize, +margin: usize, +io: std.Io, +animation_delay: u16, + +info_line: *InfoLine, +fg: u32, +bg: u32, + +lang: Lang, +full_color: bool, + +lua_error: bool = false, +lua_error_logged: bool = false, +lua_str: ?[:0]const u8 = null, + +pub fn init( + io: std.Io, + alloc: Allocator, + log: *LogFile, + buf: *TerminalBuffer, + file: []const u8, + margin: u8, + animation_delay: u16, + info_line: *InfoLine, + fg: u32, + bg: u32, + lang: Lang, + full_color: bool, +) !Lua { + var self: Lua = .{ + .lua = try zlua.Lua.init(alloc), + .allocator = alloc, + .terminal_buffer = buf, + .instance = null, + .log = log, + .width = 0, + .height = 0, + .margin = margin, + .io = io, + .animation_delay = animation_delay, + .info_line = info_line, + .fg = fg, + .bg = bg, + .lang = lang, + .full_color = full_color, + }; + + // exclude IO and debug libraries + self.lua.openBase(); + self.lua.openBit(); + self.lua.openMath(); + self.lua.openString(); + self.lua.openTable(); + + file_loading: { + const zf = std.mem.concatWithSentinel(alloc, u8, &[1][]const u8{file}, 0) catch |e| { + try self.log.err(self.io, "lua", "failed to allocate file path: {}", .{e}); + self.lua_str = "failed to allocate file path!"; + self.info_line.addMessage(lang.err_alloc, self.bg, self.fg) catch {}; + return e; + }; + defer alloc.free(zf); + + // create the ly table + self.lua.newTable(); + self.lua.setGlobal("ly"); + + // create ly.width and ly.height from TerminalBuffer width/height + self.propagateTerminalBounds(); + + _ = self.lua.getGlobal("ly"); + _ = self.lua.pushString("clock"); + self.lua.pushFunction(luaLyClock); + self.lua.setTable(-3); + _ = self.lua.pushString("putCell"); + self.lua.pushFunction(luaPutCell); + self.lua.setTable(-3); + _ = self.lua.pushString("putLabel"); + self.lua.pushFunction(luaPutLabel); + self.lua.setTable(-3); + _ = self.lua.pushString("putRect"); + self.lua.pushFunction(luaPutRect); + self.lua.setTable(-3); + self.lua.setGlobal("ly"); + + self.lua.doFile(zf) catch { + const errorStr = self.lua.toString(-1) catch unreachable; + self.lua_str = try self.allocator.dupeSentinel(u8, errorStr, 0); + try self.log.err(self.io, "lua", "lua error: {s}", .{errorStr}); + self.lua_error = true; + break :file_loading; + }; + } + + return self; +} + +fn draw(self: *Lua) void { + self.propagateTerminalBounds(); + if (self.lua_error) { + // Ly's Red Screen of Omega-Death:tm: + const RED: u32 = if (self.full_color) TerminalBuffer.Color.TRUE_RED else TerminalBuffer.Color.ECOL_RED; + const cell = Cell.init(0x2588, RED, RED); + for (0..self.terminal_buffer.height) |y| + for (0..self.terminal_buffer.width) |x| + cell.put(x, y) catch {}; + if (self.lua_str) |str| + for (str, 0..) |c, i| { + Cell.init(c, 0x00FFFFFF, 0).put( + @divFloor(self.width, 2) - @divFloor(str.len, 2) + i, + self.margin + 5, + ) catch {}; + }; + if (!self.lua_error_logged) { + self.info_line.addMessage("lua animation failed", self.bg, self.fg) catch {}; + self.lua_error_logged = true; + } + return; + } + + _ = self.lua.getGlobal("draw"); + self.lua.protectedCall(.{}) catch { + const errorStr = self.lua.toString(-1) catch unreachable; + self.lua_str = std.mem.concatWithSentinel( + self.allocator, + u8, + &.{ "cannot call draw(): ", errorStr }, + 0, + ) catch unreachable; + self.log.err(self.io, "lua", "error (cannot call draw()): {s}", .{errorStr}) catch unreachable; + self.lua_error = true; + }; +} + +fn calculateTimeout(self: *Lua, _: *anyopaque) !?usize { + return self.animation_delay; +} + +fn deinit(self: *Lua) void { + if (self.lua_str) |str| self.allocator.free(str); + self.lua.deinit(); +} + +pub fn widget(self: *Lua) *Widget { + if (self.instance) |*inst| return inst; + self.instance = Widget.init( + "Lua", + null, + self, + deinit, + null, + draw, + null, + null, + calculateTimeout, + ); + return &self.instance.?; +} + +fn propagateTerminalBounds(self: *Lua) void { + if (self.terminal_buffer.height == self.height and + self.terminal_buffer.width == self.width) + return; + self.width = self.terminal_buffer.width; + self.height = self.terminal_buffer.height; + _ = self.lua.getGlobal("ly"); + _ = self.lua.pushString("width"); + self.lua.pushInteger(@intCast(self.terminal_buffer.width)); + self.lua.setTable(-3); + _ = self.lua.pushString("height"); + self.lua.pushInteger(@intCast(self.terminal_buffer.height)); + self.lua.setTable(-3); + self.lua.setGlobal("ly"); +} + +fn luaLyClock(state: ?*zlua.LuaState) callconv(.c) c_int { + var threaded = std.Io.Threaded.init_single_threaded; + const lua: *zlua.Lua = @ptrCast(@alignCast(state orelse unreachable)); + lua.pushInteger(std.Io.Timestamp.now(threaded.io(), .real).toMicroseconds()); + return 1; +} + +fn luaPutCell(state: ?*zlua.LuaState) callconv(.c) c_int { + const lua: *zlua.Lua = @ptrCast(@alignCast(state orelse unreachable)); + const MSG = "ly.putCell: cannot convert %s-typed "; + const byte = lua.toNumeric(u32, 1) catch { + const t = lua.typeName(lua.typeOf(1)); + lua.raiseErrorStr(MSG ++ "byte to u32", .{t.ptr}); + }; + const fg = lua.toNumeric(u32, 2) catch { + const t = lua.typeName(lua.typeOf(2)); + lua.raiseErrorStr(MSG ++ "fg to u32", .{t.ptr}); + }; + const bg = lua.toNumeric(u32, 3) catch { + const t = lua.typeName(lua.typeOf(3)); + lua.raiseErrorStr(MSG ++ "bg to u32", .{t.ptr}); + }; + const x = lua.toNumeric(usize, 4) catch { + const t = lua.typeName(lua.typeOf(4)); + lua.raiseErrorStr(MSG ++ "x to usize", .{t.ptr}); + }; + const y = lua.toNumeric(usize, 5) catch { + const t = lua.typeName(lua.typeOf(5)); + lua.raiseErrorStr(MSG ++ "y to usize", .{t.ptr}); + }; + TerminalBuffer.setCell(x, y, .{ + .fg = fg, + .bg = bg, + .ch = byte, + }) catch {}; + return 0; +} + +fn luaPutRect(state: ?*zlua.LuaState) callconv(.c) c_int { + const lua: *zlua.Lua = @ptrCast(@alignCast(state orelse unreachable)); + const MSG = "ly.putRect: cannot convert %s-typed "; + const byte = lua.toNumeric(u32, 1) catch { + const t = lua.typeName(lua.typeOf(1)); + lua.raiseErrorStr(MSG ++ "byte to u32", .{t.ptr}); + }; + const fg = lua.toNumeric(u32, 2) catch { + const t = lua.typeName(lua.typeOf(2)); + lua.raiseErrorStr(MSG ++ "fg to u32", .{t.ptr}); + }; + const bg = lua.toNumeric(u32, 3) catch { + const t = lua.typeName(lua.typeOf(3)); + lua.raiseErrorStr(MSG ++ "bg to u32", .{t.ptr}); + }; + const x = lua.toNumeric(usize, 4) catch { + const t = lua.typeName(lua.typeOf(4)); + lua.raiseErrorStr(MSG ++ "x to usize", .{t.ptr}); + }; + const y = lua.toNumeric(usize, 5) catch { + const t = lua.typeName(lua.typeOf(5)); + lua.raiseErrorStr(MSG ++ "y to usize", .{t.ptr}); + }; + const w = lua.toNumeric(usize, 6) catch { + const t = lua.typeName(lua.typeOf(5)); + lua.raiseErrorStr(MSG ++ "w to usize", .{t.ptr}); + }; + const h = lua.toNumeric(usize, 7) catch { + const t = lua.typeName(lua.typeOf(5)); + lua.raiseErrorStr(MSG ++ "h to usize", .{t.ptr}); + }; + for (0..w) |wx| for (0..h) |hy| + TerminalBuffer.setCell(x + wx, y + hy, .{ + .fg = fg, + .bg = bg, + .ch = byte, + }) catch {}; + return 0; +} + +fn luaPutLabel(state: ?*zlua.LuaState) callconv(.c) c_int { + const lua: *zlua.Lua = @ptrCast(@alignCast(state orelse unreachable)); + const MSG = "ly.putLabel: cannot convert %s-typed "; + const str = lua.toString(1) catch { + const t = lua.typeName(lua.typeOf(2)); + lua.raiseErrorStr(MSG ++ "str to string", .{t.ptr}); + }; + const fg = lua.toNumeric(u32, 2) catch { + const t = lua.typeName(lua.typeOf(2)); + lua.raiseErrorStr(MSG ++ "fg to u32", .{t.ptr}); + }; + const bg = lua.toNumeric(u32, 3) catch { + const t = lua.typeName(lua.typeOf(3)); + lua.raiseErrorStr(MSG ++ "bg to u32", .{t.ptr}); + }; + const x = lua.toNumeric(usize, 4) catch { + const t = lua.typeName(lua.typeOf(4)); + lua.raiseErrorStr(MSG ++ "x to usize", .{t.ptr}); + }; + const y = lua.toNumeric(usize, 5) catch { + const t = lua.typeName(lua.typeOf(5)); + lua.raiseErrorStr(MSG ++ "y to usize", .{t.ptr}); + }; + TerminalBuffer.drawText(str, x, y, fg, bg) catch {}; + return 0; +} diff --git a/src/config/Config.zig b/src/config/Config.zig index a592faa..b7b258b 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -74,6 +74,7 @@ lang: []const u8 = "en", login_cmd: ?[]const u8 = null, login_defs_path: []const u8 = "/etc/login.defs", logout_cmd: ?[]const u8 = null, +lua_animation_file: []const u8 = build_options.config_directory ++ "/ly/example.lua", ly_log: ?[]const u8 = "/var/log/ly.log", margin_box_h: u8 = 2, margin_box_v: u8 = 1, diff --git a/src/enums.zig b/src/enums.zig index 47771da..ff69f84 100644 --- a/src/enums.zig +++ b/src/enums.zig @@ -7,6 +7,7 @@ pub const Animation = enum { colormix, gameoflife, dur_file, + lua, }; pub const DisplayServer = enum { diff --git a/src/main.zig b/src/main.zig index 887ec41..41ba15d 100644 --- a/src/main.zig +++ b/src/main.zig @@ -29,6 +29,7 @@ const Doom = @import("animations/Doom.zig"); const DurFile = @import("animations/DurFile.zig"); const GameOfLife = @import("animations/GameOfLife.zig"); const Matrix = @import("animations/Matrix.zig"); +const Lua = @import("animations/Lua.zig"); const auth = @import("auth.zig"); const InfoLine = @import("components/InfoLine.zig"); const Session = @import("components/Session.zig"); @@ -1093,6 +1094,23 @@ pub fn main(init: std.process.Init) !void { ); animation = dur.widget(); }, + .lua => { + var lua = try Lua.init( + state.io, + state.allocator, + &state.log_file, + &state.buffer, + state.config.lua_animation_file, + state.config.edge_margin, + state.config.animation_frame_delay, + &state.info_line, + state.config.error_fg, + state.config.error_bg, + state.lang, + state.config.full_color, + ); + animation = lua.widget(); + }, } defer if (animation) |a| a.deinit();