diff --git a/build.zig b/build.zig index 8c604e1..6822e3c 100644 --- a/build.zig +++ b/build.zig @@ -102,13 +102,6 @@ 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, diff --git a/build.zig.zon b/build.zig.zon index a340291..f103091 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -11,10 +11,6 @@ .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/install.zig b/install.zig index b5649cd..29d2cdd 100644 --- a/install.zig +++ b/install.zig @@ -127,7 +127,7 @@ fn installLy(allocator: std.mem.Allocator, io: std.Io, patch_map: PatchMap, inst defer config_dir.close(io); if (install_config) { - const patched_config = try patchFile(allocator, io, "res/config.ini", patch_map); + const patched_config = try patchFile(allocator, io, "res/config.lua", patch_map); defer allocator.free(patched_config); try installText(io, patched_config, config_dir, ly_config_directory, "config.ini", .{}); @@ -135,10 +135,10 @@ fn installLy(allocator: std.mem.Allocator, io: std.Io, patch_map: PatchMap, inst try installFile(io, "res/startup.sh", config_dir, ly_config_directory, "startup.sh", .{ .permissions = .fromMode(0o755) }); } - const patched_example_config = try patchFile(allocator, io, "res/config.ini", patch_map); + const patched_example_config = try patchFile(allocator, io, "res/config.lua", patch_map); defer allocator.free(patched_example_config); - try installText(io, patched_example_config, config_dir, ly_config_directory, "config.ini.example", .{}); + try installText(io, patched_example_config, config_dir, ly_config_directory, "config.lua.example", .{}); const patched_setup = try patchFile(allocator, io, "res/setup.sh", patch_map); defer allocator.free(patched_setup); diff --git a/ly-core/build.zig b/ly-core/build.zig index f62fb2e..37b8668 100644 --- a/ly-core/build.zig +++ b/ly-core/build.zig @@ -21,6 +21,13 @@ pub fn build(b: *std.Build) void { const zigini = b.dependency("zigini", .{ .target = target, .optimize = optimize }); mod.addImport("zigini", zigini.module("zigini")); + const zlua = b.dependency("zlua", .{ + .target = target, + .optimize = optimize, + .lang = .luajit, + }); + mod.addImport("zlua", zlua.module("zlua")); + const translate_c = b.dependency("translate_c", .{ .target = target, }); diff --git a/ly-core/build.zig.zon b/ly-core/build.zig.zon index ddf0b9b..4896199 100644 --- a/ly-core/build.zig.zon +++ b/ly-core/build.zig.zon @@ -12,6 +12,10 @@ .url = "git+https://codeberg.org/ziglang/translate-c?ref=zig-0.16.x#6fe0ffc4549f15c5f2d9432c2b4460ba90ff85ac", .hash = "translate_c-1.0.0-Q_BUWo_5BgD4flHdUhA31zOz0XvZk9k7lQv1ouzyNXj2", }, + .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/src/config/custom.zig b/ly-core/src/custom.zig similarity index 100% rename from src/config/custom.zig rename to ly-core/src/custom.zig diff --git a/ly-core/src/root.zig b/ly-core/src/root.zig index 42198f5..c52d7d4 100644 --- a/ly-core/src/root.zig +++ b/ly-core/src/root.zig @@ -1,23 +1,57 @@ const std = @import("std"); pub const ini = @import("zigini"); +pub const zlua = @import("zlua"); +pub const Lua = zlua.Lua; pub const interop = @import("interop.zig"); pub const UidRange = @import("UidRange.zig"); pub const LogFile = @import("LogFile.zig"); pub const SharedError = @import("SharedError.zig"); +pub const custom = @import("custom.zig"); + +pub fn Parser(comptime T: type) type { + return union(enum) { + ini: IniParser(T), + lua: LuaParser(T), + + pub fn errors(self: *const @This()) std.ArrayList(Error) { + return switch (self.*) { + inline else => |p| p.errors, + }; + } + + pub fn maybe_load_error(self: *const @This()) ?anyerror { + return switch (self.*) { + inline else => |p| p.maybe_load_error, + }; + } + pub fn structure(self: *const @This()) T { + return switch (self.*) { + inline else => |p| p.structure, + }; + } + + pub fn deinit(self: *@This()) void { + switch (self.*) { + inline else => |*p| p.deinit(), + } + } + }; +} + +pub const Error = struct { + type_name: []const u8, + key: []const u8, + value: []const u8, + error_name: []const u8, +}; pub fn IniParser(comptime Struct: type) type { return struct { const Self = @This(); const temporary_allocator = std.heap.page_allocator; - pub const Error = struct { - type_name: []const u8, - key: []const u8, - value: []const u8, - error_name: []const u8, - }; pub var global_errors: std.ArrayList(Error) = .empty; ini_struct: ini.Ini(Struct), @@ -76,3 +110,283 @@ pub fn IniParser(comptime Struct: type) type { } }; } + +pub fn LuaParser(comptime Struct: type) type { + return struct { + const Self = @This(); + const temporary_allocator = std.heap.page_allocator; + + pub var global_errors: std.ArrayList(Error) = .empty; + + structure: Struct, + errors: std.ArrayList(Error), + maybe_load_error: ?anyerror, + allocator: std.mem.Allocator, + arena: std.heap.ArenaAllocator, + + pub fn init( + allocator: std.mem.Allocator, + path: []const u8, + ) !Self { + var arena = std.heap.ArenaAllocator.init(allocator); + const arena_alloc = arena.allocator(); + + var maybe_load_error: ?anyerror = null; + errdefer |err| maybe_load_error = err; + + const data = parseLua(arena_alloc, path) catch load_error: { + break :load_error Struct{}; + }; + + if (global_errors.items.len != 0) { + maybe_load_error = error.InvalidConfig; + } + + return .{ + .structure = data, + .errors = global_errors, + .maybe_load_error = maybe_load_error, + .allocator = allocator, + .arena = arena, + }; + } + + fn parseLua( + allocator: std.mem.Allocator, + path: []const u8, + ) !Struct { + var lua: *Lua = try .init(allocator); + defer lua.deinit(); + + lua.openBase(); + lua.openBit(); + lua.openMath(); + lua.openString(); + lua.openTable(); + + // convert to sentinel terminated slice + const spath: [:0]const u8 = try allocator.dupeSentinel(u8, path, 0); + defer allocator.free(spath); + lua.doFile(spath) catch return error.LuaError; + + var data: Struct = .{}; + switch (@typeInfo(Struct)) { + .@"struct" => |struc| { + const ly_type = lua.getGlobal("ly"); + defer lua.pop(1); // pop ly table + if (ly_type == .nil) return error.MissingLyTable; + + inline for (struc.fields) |field| { + try setField(allocator, lua, field, &data); + } + }, + else => @compileError("Expected a struct."), + } + + // Parse custom binds and labels + try parseCustom(lua); + return data; + } + + pub fn setField(allocator: std.mem.Allocator, lua: *Lua, comptime field: std.builtin.Type.StructField, data: *Struct) !void { + const type_info = @typeInfo(field.type); + const actual_type, const is_optional = blk: { + if (type_info == .optional) { + break :blk .{ type_info.optional.child, true }; + } + break :blk .{ field.type, false }; + }; + // push value to top of stack + _ = lua.getField(-1, field.name); + defer lua.pop(1); + + // handle null, i.e. undefined fields + if (is_optional and lua.isNil(-1)) { + @field(data, field.name) = null; + return; + } + + // handle missing required fields + if (lua.isNil(-1)) { + return error.MissingRequiredField; + } + const actual_type_info = @typeInfo(actual_type); + + errdefer |err| { + const value = lua.toString(-1) catch ""; + const duped = allocator.dupe(u8, value) catch ""; + errorHandler(@typeName(field.type), field.name, duped, err); + } + + // dispatch depending on type + if (actual_type_info == .int and is_optional) { + if (lua.isNumber(-1)) { + const value = try lua.toNumber(-1); + @field(data, field.name) = @trunc(value); + } else { + const str = try lua.toString(-1); + + var view = try std.unicode.Utf8View.init(str); + var iter = view.iterator(); + + const codepoint = iter.nextCodepoint(); + + if (iter.nextCodepoint() != null) return error.ExpectedSingleCharacter; + + @field(data, field.name) = if (codepoint) |cp| @intCast(cp) else null; + } + // non null integer + } else if (actual_type_info == .int) { + if (lua.isNumber(-1)) { + const value = try lua.toNumber(-1); + @field(data, field.name) = @trunc(value); + } else { + const str = try lua.toString(-1); + + var view = try std.unicode.Utf8View.init(str); + var iter = view.iterator(); + + const codepoint = iter.nextCodepoint() orelse return error.EmptyString; + + if (iter.nextCodepoint() != null) return error.ExpectedSingleCharacter; + + @field(data, field.name) = @intCast(codepoint); + } + } else if (actual_type_info == .float) { // all floats + const value = try lua.toNumber(-1); + @field(data, field.name) = @floatCast(value); + } else if (actual_type_info == .bool) { + if (!lua.isBoolean(-1)) return error.ExpectedBoolean; + const value = lua.toBoolean(-1); + @field(data, field.name) = value; + } else if (actual_type == []const u8) { + const value = try lua.toString(-1); + const duped = try allocator.dupe(u8, value); + @field(data, field.name) = duped; + } else if (actual_type == [:0]const u8) { + const value = try lua.toString(-1); + const duped = try allocator.dupeSentinel(u8, value, 0); + @field(data, field.name) = duped; + } else if (actual_type_info == .@"enum") { + const value = try lua.toString(-1); + const variant = std.meta.stringToEnum(actual_type, value) orelse return error.InvalidVariant; + @field(data, field.name) = variant; + } else unreachable; + } + + pub fn parseCustom(lua: *Lua) !void { + _ = lua.getGlobal("ly"); + defer lua.pop(1); // pop ly table + if (!lua.isTable(-1)) return error.MissingLyTable; + + _ = lua.getField(-1, "custom_commands"); + // custom_commands can be omitted or empty, so we just skip instead of erroring + + if (lua.isTable(-1)) binds: { + const len: usize = @intCast(lua.objectLen(-1)); + if (len == 0) break :binds; + + for (1..len + 1) |i| { + // push i-th table to stack + lua.pushInteger(@intCast(i)); + const ith_table_type = lua.getTable(-2); + defer lua.pop(1); // i-th table in custom_commands + if (ith_table_type != .table) continue; + + // skip command if binding isn't set or not a string + const binding_type = lua.getField(-1, "binding"); + if (binding_type != .string) continue; + const binding = lua.toString(-1) catch continue; + const bindingZ = temporary_allocator.dupe(u8, binding) catch ""; + std.debug.print("{s}", .{bindingZ}); + lua.pop(1); // binding value + + if (!custom.binds.contains(bindingZ)) { + custom.binds.put(temporary_allocator, bindingZ, .{}) catch {}; + } + if (custom.binds.getPtr(bindingZ)) |command| { + // binding name + const name_type = lua.getField(-1, "name"); + if (name_type != .string) continue; + const binding_name = lua.toString(-1) catch ""; + command.name = temporary_allocator.dupe(u8, binding_name) catch ""; + lua.pop(1); // name value + + // binding command + const cmd_type = lua.getField(-1, "cmd"); + if (cmd_type != .string) continue; + const binding_cmd = lua.toString(-1) catch ""; + command.cmd = temporary_allocator.dupe(u8, binding_cmd) catch ""; + lua.pop(1); // cmd value + } + } + } + lua.pop(1); + + _ = lua.getField(-1, "custom_labels"); + // custom_labels can be omitted, so we just skip instead of erroring + + if (lua.isTable(-1)) labels: { + const len: usize = @intCast(lua.objectLen(-1)); + if (len == 0) break :labels; + + for (1..len + 1) |i| { + // push i-th table to stack + lua.pushInteger(@intCast(i)); + const ith_table_type = lua.getTable(-2); + defer lua.pop(1); // i-th table in custom_labels + if (ith_table_type != .table) continue; + + // skip command if binding isn't set or not a string + const label_type = lua.getField(-1, "label"); + if (label_type != .string) continue; + const label = lua.toString(-1) catch continue; + const labelZ = temporary_allocator.dupe(u8, label) catch ""; + lua.pop(1); // label value + + if (!custom.labels.contains(labelZ)) { + custom.labels.put(temporary_allocator, labelZ, .{ .name = labelZ }) catch {}; + } + if (custom.labels.getPtr(labelZ)) |label_ptr| { + // label command + const cmd_type = lua.getField(-1, "cmd"); + if (cmd_type != .string) continue; + const label_cmd = lua.toString(-1) catch ""; + label_ptr.cmd = temporary_allocator.dupe(u8, label_cmd) catch ""; + lua.pop(1); // cmd value + + // label refresh + const name_type = lua.getField(-1, "refresh"); + if (name_type != .number) continue; + const label_refresh: u32 = @intCast(lua.toInteger(-1) catch 0); + label_ptr.refresh = label_refresh; + lua.pop(1); // name value + } + } + } + + lua.pop(1); + } + + pub fn deinit(self: *Self) void { + self.arena.deinit(); + for (0..global_errors.items.len) |i| { + const err = global_errors.items[i]; + temporary_allocator.free(err.type_name); + temporary_allocator.free(err.key); + temporary_allocator.free(err.value); + } + + global_errors.deinit(temporary_allocator); + } + + fn errorHandler(type_name: []const u8, key: []const u8, value: []const u8, err: anyerror) void { + global_errors.append(temporary_allocator, .{ + .type_name = temporary_allocator.dupe(u8, type_name) catch return, + .key = temporary_allocator.dupe(u8, key) catch return, + .value = temporary_allocator.dupe(u8, value) catch return, + .error_name = @errorName(err), + }) catch return; + } + }; +} diff --git a/readme.md b/readme.md index 8cacd1a..51368db 100644 --- a/readme.md +++ b/readme.md @@ -249,12 +249,14 @@ You can, of course, still select the init system of your choice when using this ## Configuration -You can find all the configuration in `/etc/ly/config.ini`. The file is fully commented, and includes the default values. +You can find all the configuration in `/etc/ly/config.lua`. The file is fully commented, and includes the default values. + +It uses the Lua language, which means you can make the configuration dynamic. You could, for example, choose a random animation each time Ly starts up. You may also check the validity of your configuration file (i.e. if there are any errors in it) with the following command: ``` -$ ly --validate-config /etc/ly/config.ini +$ ly --validate-config /etc/ly/config.lua ``` ## Controls diff --git a/res/config.lua b/res/config.lua new file mode 100644 index 0000000..1449149 --- /dev/null +++ b/res/config.lua @@ -0,0 +1,453 @@ +ly = { + -- Ly supports 24-bit true color with styling, which means each color is a 32-bit value. + -- The format is 0xSSRRGGBB, where SS is the styling, RR is red, GG is green, and BB is blue. + -- Here are the possible styling options: + -- TB_BOLD 0x01000000 + -- TB_UNDERLINE 0x02000000 + -- TB_REVERSE 0x04000000 + -- TB_ITALIC 0x08000000 + -- TB_BLINK 0x10000000 + -- TB_HI_BLACK 0x20000000 + -- TB_BRIGHT 0x40000000 + -- TB_DIM 0x80000000 + -- Programmatically, you'd apply them using the bitwise OR operator (|), but because Ly's + -- configuration doesn't support using it, you have to manually compute the color value. + -- Note that, if you want to use the default color value of the terminal, you can use the + -- special value 0x00000000. This means that, if you want to use black, you *must* use + -- the styling option TB_HI_BLACK (the RGB values are ignored when using this option). + + -- Allow empty password or not when authenticating + allow_empty_password = true, + + -- The active animation + -- none -> Nothing + -- doom -> PSX DOOM fire + -- matrix -> CMatrix + -- 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 + animation_frame_delay = 5, + + -- Stop the animation after some time + -- 0 -> Run forever + -- 1..2e12 -> Stop the animation after this many seconds + animation_timeout_sec = 0, + + -- The character used to mask the password + -- You can either type it directly as a UTF-8 character (like *), or use a UTF-32 + -- codepoint (for example 0x2022 for a bullet point) + -- If null, the password will be hidden + -- Note: you can use a # by escaping it like so: \# + asterisk = '*', + + -- The number of failed authentications before a special animation is played... ;) + -- If set to 0, the animation will never be played + auth_fails = 10, + + -- Automatic login configuration + -- This feature allows Ly to automatically log in a user without password prompt. + -- IMPORTANT: Both auto_login_user and auto_login_session must be set for this to work. + -- Autologin only happens once at startup - it won't re-trigger after logout. + + -- PAM service name to use for automatic login + -- The default service (ly-autologin) uses pam_permit to allow login without password + -- The appropriate platform-specific PAM configuration (ly-autologin) will be used automatically + auto_login_service = "ly-autologin", + + -- Session name to launch automatically + -- To find available session names, check the .desktop files in: + -- - /usr/share/xsessions/ (for X11 sessions) + -- - /usr/share/wayland-sessions/ (for Wayland sessions) + -- Use the filename without .desktop extension, the Name field inside the file or the value of the DesktopNames field + -- Examples: "i3", "sway", "gnome", "plasma", "xfce" + -- If null, automatic login is disabled + auto_login_session = nil, + + -- Username to automatically log in + -- Must be a valid user on the system + -- If null, automatic login is disabled + auto_login_user = nil, + + -- Identifier for battery whose charge to display at top left + -- Primary battery is usually BAT0 or BAT1 + -- If set to null, battery status won't be shown + -- Unused on FreeBSD (a sysctl is used there) + battery_id = nil, + + -- Background color id + bg = 0x00000000, + + -- Change the state and language of the big clock + -- none -> Disabled (default) + -- en -> English + -- fa -> Farsi + bigclock = "none", + + -- Set bigclock to 12-hour notation. + bigclock_12hr = false, + + -- Set bigclock to show the seconds. + bigclock_seconds = false, + + -- Blank main box background + -- Setting to false will make it transparent + blank_box = true, + + -- Border foreground color id + border_fg = 0x00FFFFFF, + + -- Relative horizontal position from the end of the screen + -- default: 0.5 + box_position_h = 0.5, + + -- Relative vertical position from the bottom of the screen + -- default: 0.5 + box_position_v = 0.5, + + -- Title to show at the top of the main box + -- If set to null, none will be shown + box_title = nil, + + -- Brightness decrease command + brightness_down_cmd = "$PREFIX_DIRECTORY/bin/brightnessctl -q -n s 10%-", + + -- Brightness decrease key combination + -- If null, the keybind is disabled and isn't shown + brightness_down_key = "F5", + + -- Brightness increase command + brightness_up_cmd = "$PREFIX_DIRECTORY/bin/brightnessctl -q -n s +10%", + + -- Brightness increase key combination + -- If null, the keybind is disabled and isn't shown + brightness_up_key = "F6", + + -- Erase password input on failure + clear_password = false, + + -- Format string for clock in top right corner (see strftime specification). Example: %c + -- If null, the clock won't be shown + clock = nil, + + -- CMatrix animation foreground color id + cmatrix_fg = 0x0000FF00, + + -- CMatrix animation character string head color id + cmatrix_head_col = 0x01FFFFFF, + + -- CMatrix animation minimum codepoint. It uses a 16-bit integer + -- For Japanese characters for example, you can use 0x3000 here + cmatrix_min_codepoint = 0x21, + + -- CMatrix animation maximum codepoint. It uses a 16-bit integer + -- For Japanese characters for example, you can use 0x30FF here + cmatrix_max_codepoint = 0x7B, + + -- Color mixing animation first color id + colormix_col1 = 0x00FF0000, + + -- Color mixing animation second color id + colormix_col2 = 0x000000FF, + + -- Color mixing animation third color id + colormix_col3 = 0x20000000, + + -- Screen corners customization + -- Keywords: + -- shutdown -> Shutdown key + -- restart -> Restart key + -- britup -> Brightness up key + -- britdown -> Brightness down key + -- password -> Toggle password key + -- clock -> Clock (format defined by 'clock' option) + -- tty -> Active TTY number + -- battery -> Battery percentage + -- version -> Ly version string + -- numlock -> Numlock state + -- capslock -> Capslock state + -- labels -> All custom info labels (lbl:) + -- binds -> All custom keybind hints (cmd:) + -- lbl:name -> Specific custom info label + -- cmd:key -> Specific custom keybind hint + -- + -- If using a keyword that groups multiple labels into one (e.g. labels, binds), + -- they'll be placed horizontally + -- + -- Also, the order defines the vertical stack (first item is at the edge) + -- If items are separted by commas, they'll be placed horizontally + -- It is possible to have both horizontal and vertical items on the same corner + + -- Bottom left + corner_bottom_left = "version", + + -- Bottom right + corner_bottom_right = "labels", + + -- Top left + corner_top_left = "shutdown,restart,britup,britdown,password battery", + + -- Top right + corner_top_right = "clock numlock,capslock", + + -- For custom binds: the horizontal limit in characters for each + -- line of custom binds before moving on to the next. + -- If null, defaults to the width of the terminal instead. + custom_bind_width = nil, + + -- Custom sessions directory + -- You can specify multiple directories, + -- e.g. $CONFIG_DIRECTORY/ly/custom-sessions:$PREFIX_DIRECTORY/share/custom-sessions + custom_sessions = "$CONFIG_DIRECTORY/ly/custom-sessions", + + -- Input box active by default on startup + -- Available inputs: info_line, session, login, password + default_input = "login", + + -- DOOM animation fire height (1 thru 9) + doom_fire_height = 6, + + -- DOOM animation fire spread (0 thru 4) + doom_fire_spread = 2, + + -- DOOM animation custom top color (low intensity flames) + doom_top_color = 0x009F2707, + + -- DOOM animation custom middle color (medium intensity flames) + doom_middle_color = 0x00C78F17, + + -- DOOM animation custom bottom color (high intensity flames) + doom_bottom_color = 0x00FFFFFF, + + -- Dur file path + dur_file_path = "$CONFIG_DIRECTORY/ly/example.dur", + + -- Dur file alignment + -- The dur file can be aligned with a direction and centered easily with the flags below + -- Available inputs: topleft, topcenter, topright, centerleft, center, centerright, bottomleft, bottomcenter, bottomright + dur_offset_alignment = "center", + + -- Dur offset x direction (value is added to the current position determined by alignment, negatives are supported) + dur_x_offset = 0, + + -- Dur offset y direction (value is added to the current position determined by alignment, negatives are supported) + dur_y_offset = 0, + + -- Set margin to the edges of the DM (useful for curved monitors) + edge_margin = 0, + + -- Error background color id + error_bg = 0x00000000, + + -- Error foreground color id + -- Default is red and bold + error_fg = 0x01FF0000, + + -- Foreground color id + fg = 0x00FFFFFF, + + -- Render true colors (if supported) + -- If false, output will be in eight-color mode + -- All eight-color mode color codes: + -- TB_DEFAULT 0x0000 + -- TB_BLACK 0x0001 + -- TB_RED 0x0002 + -- TB_GREEN 0x0003 + -- TB_YELLOW 0x0004 + -- TB_BLUE 0x0005 + -- TB_MAGENTA 0x0006 + -- TB_CYAN 0x0007 + -- TB_WHITE 0x0008 + -- If full color is off, the styling options still work. The colors are + -- always 32-bit values with the styling in the most significant byte. + -- Note: If using the dur_file animation option and the dur file's color range + -- is saved as 256 with this option disabled, the file will not be drawn. + full_color = true, + + -- Game of Life entropy interval (0 = disabled, >0 = add entropy every N generations), + -- 0 -> Pure Conway's Game of Life (will eventually stabilize) + -- 10 -> Add entropy every 10 generations (recommended for continuous activity) + -- 50+ -> Less frequent entropy for more natural evolution + gameoflife_entropy_interval = 10, + + -- Game of Life animation foreground color id + gameoflife_fg = 0x0000FF00, + + -- Game of Life frame delay (lower = faster animation, higher = slower), + -- 1-3 -> Very fast animation + -- 6 -> Default smooth animation speed + -- 10+ -> Slower, more contemplative speed + gameoflife_frame_delay = 6, + + -- Game of Life initial cell density (0.0 to 1.0) + -- 0.1 -> Sparse, minimal activity + -- 0.4 -> Balanced activity (recommended) + -- 0.7+ -> Dense, chaotic patterns + gameoflife_initial_density = 0.4, + + -- Remove main box borders + hide_borders = false, + + -- Command executed when no input is detected for a certain time + -- If null, no command will be executed + inactivity_cmd = nil, + + -- Executes a command after a certain amount of seconds + inactivity_delay = 0, + + -- Initial text to show on the info line + -- If set to null, the info line defaults to the hostname + initial_info_text = nil, + + -- Input boxes length + input_len = 34, + + -- Active language + -- Available languages are found in $CONFIG_DIRECTORY/ly/lang/ + lang = "en", + + -- Command executed when logging in + -- If null, no command will be executed + -- Important: the code itself must end with `exec "$@"` in order to launch the session! + -- You can also set environment variables in there, they'll persist until logout + login_cmd = nil, + + -- Path for login.defs file (used for listing all local users on the system on + -- Linux) + login_defs_path = "/etc/login.defs", + + -- Command executed when logging out + -- If null, no command will be executed + -- Important: the session will already be terminated when this command is executed, so + -- no need to add `exec "$@"` at the end + logout_cmd = nil, + + -- 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", + + -- Main box horizontal margin + margin_box_h = 2, + + -- Main box vertical margin + margin_box_v = 1, + + -- Set numlock on/off at startup + numlock = false, + + -- Default path + -- If null, ly doesn't set a path + path = "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", + + -- Specifies the key combination used for restart + -- If null, the keybind is disabled and isn't shown + restart_key = "F2", + + -- Absolute directory for the save file + -- If null, current desktop & login won't be saved nor loaded + save_file_dir = "$CONFIG_DIRECTORY/ly", + + -- Service name (set to ly to use the provided pam config file) + service_name = "ly", + + -- Session log file path + -- This will contain stdout and stderr of Wayland sessions + -- By default it's saved in the user's home directory + -- Important: due to technical limitations, X11, shell sessions as well as + -- launching session via KMSCON aren't supported, which means you won't get any + -- logs from those sessions. + -- If null, no session log will be created + session_log = ".local/state/ly-session.log", + + -- Setup command + setup_cmd = "$CONFIG_DIRECTORY/ly/setup.sh", + + -- Show the shell session in the session list + -- If false, the shell session will be hidden + shell = true, + + -- Specifies the key combination used for showing the password + -- If null, the keybind is disabled and isn't shown + show_password_key = "F7", + + -- Specifies the key combination used for shutdown + -- If null, the keybind is disabled and isn't shown + shutdown_key = "F1", + + -- Command executed when starting Ly (before the TTY is taken control of) + -- See file at path below for an example of changing the default TTY colors + start_cmd = "$CONFIG_DIRECTORY/ly/startup.sh", + + -- Center the session name. + text_in_center = false, + + -- If true, user will need to manually type username instead of selecting from the list + -- of discovered users + type_username = false, + + -- Default vi mode + -- normal -> normal mode + -- insert -> insert mode + vi_default_mode = "normal", + + -- Enable vi keybindings + vi_mode = false, + + -- Wayland desktop environments + -- You can specify multiple directories, + -- e.g. $PREFIX_DIRECTORY/share/wayland-sessions:$PREFIX_DIRECTORY/local/share/wayland-sessions + -- If null, Wayland sessions will not be shown + waylandsessions = "$PREFIX_DIRECTORY/share/wayland-sessions", + + -- Xorg server command + -- Add the -quiet argument to hide startup logs from the server + x_cmd = "$PREFIX_DIRECTORY/bin/X", + + -- Xorg virtual terminal number + -- Mostly useful for FreeBSD where choosing the current TTY causes issues + -- If null, the current TTY will be chosen + x_vt = nil, + + -- Xorg xauthority edition tool + xauth_cmd = "$PREFIX_DIRECTORY/bin/xauth", + + -- xinitrc + -- If null, the xinitrc session will be hidden + xinitrc = "~/.xinitrc", + + -- Xorg desktop environments + -- You can specify multiple directories, + -- e.g. $PREFIX_DIRECTORY/share/xsessions:$PREFIX_DIRECTORY/local/share/xsessions + -- If null, X11 sessions will not be shown + xsessions = "$PREFIX_DIRECTORY/share/xsessions", + + -- Custom Commands and Labels: + -- The following examples below give an outline for setting up custom commands and labels. + -- Unless specified as optional, an option is mandatory. + + -- Comments preceding with '##' are for documentation. + -- Comments preceding with '#' comment out the example INI. + + --# Declare a command with the F8 binding. + --[cmd:F8] + --# The name of the command to show up in Ly. + --# Note: "$" in "$brightness_up" fetches the appropriate string from the specified locale file + --# and is replaced with the value representing "brightness_up". + --# You can see the list of keys in any locale file in $CONFIG_DIRECTORY/ly/lang. + --cmd = touch /tmp/ly.gaming, + --name = custom command $brightness_up, + + --# Declare a label with an ID. This ID should be unique across all labels. + --[lbl:kernel] + --cmd = uname -srn, + --# Optional, defaulting to 0. + --# In frames, the time to re-run the command and update the label. + --# If 0, only run once and do not refresh afterwards + --refresh = 0, +} diff --git a/src/animations/Lua.zig b/src/animations/Lua.zig index 2d2956f..bb4d406 100644 --- a/src/animations/Lua.zig +++ b/src/animations/Lua.zig @@ -8,7 +8,7 @@ const Allocator = std.mem.Allocator; const InfoLine = @import("../components/InfoLine.zig"); const Lang = @import("../config/Lang.zig"); -const zlua = @import("zlua"); +const zlua = ly_ui.ly_core.zlua; const ly_lua = @embedFile("ly.lua"); diff --git a/src/config/migrator.zig b/src/config/migrator.zig index 4ed4d2e..fc25768 100644 --- a/src/config/migrator.zig +++ b/src/config/migrator.zig @@ -18,7 +18,7 @@ const Config = @import("Config.zig"); const Lang = @import("Lang.zig"); const OldSave = @import("OldSave.zig"); const SavedUsers = @import("SavedUsers.zig"); -const custom = @import("custom.zig"); +const custom = ly_core.custom; const color_properties = [_][]const u8{ "bg", diff --git a/src/main.zig b/src/main.zig index 6a94ec7..9508c41 100644 --- a/src/main.zig +++ b/src/main.zig @@ -19,9 +19,12 @@ const interop = ly_core.interop; const UidRange = ly_core.UidRange; const LogFile = ly_core.LogFile; const SharedError = ly_core.SharedError; +const Parser = ly_core.Parser; const IniParser = ly_core.IniParser; +const LuaParser = ly_core.LuaParser; const ini = ly_core.ini; const Ini = ini.Ini; +const custom = ly_core.custom; const Cascade = @import("animations/Cascade.zig"); const ColorMix = @import("animations/ColorMix.zig"); @@ -39,7 +42,6 @@ const Lang = @import("config/Lang.zig"); const migrator = @import("config/migrator.zig"); const OldSave = @import("config/OldSave.zig"); const SavedUsers = @import("config/SavedUsers.zig"); -const custom = @import("config/custom.zig"); const DisplayServer = @import("enums.zig").DisplayServer; const Environment = @import("Environment.zig"); const Entry = Environment.Entry; @@ -203,22 +205,28 @@ pub fn main(init: std.process.Init) !void { if (res.args.config) |path| config_parent_path = path; if (res.args.@"use-kmscon-vt" != 0) state.use_kmscon_vt = true; if (res.args.@"validate-config") |path| { - var parser = try IniParser(Config).init( - state.allocator, - state.io, - path, - migrator.configFieldHandler, - ); + var parser: Parser(Config) = blk: { + if (std.mem.endsWith(u8, path, ".ini")) { + break :blk .{ .ini = try IniParser(Config).init( + state.allocator, + state.io, + path, + migrator.configFieldHandler, + ) }; + } else { + break :blk .{ .lua = try LuaParser(Config).init(state.allocator, path) }; + } + }; defer parser.deinit(); - for (parser.errors.items) |err| { + for (parser.errors().items) |err| { std.log.err( "failed to convert value '{s}' of option '{s}' to type '{s}': {s}", .{ err.value, err.key, err.type_name, err.error_name }, ); } - if (parser.maybe_load_error) |err| { + if (parser.maybe_load_error()) |err| { std.log.err("failed to load config file: {s}", .{@errorName(err)}); std.process.exit(1); } @@ -234,12 +242,29 @@ pub fn main(init: std.process.Init) !void { state.allocator.free(state.old_save_path); }; - const config_path = try std.Io.Dir.path.join(state.allocator, &[_][]const u8{ config_parent_path, "config.ini" }); + // Test for presence of Lua config file first + // If it fails, fall back to ini + var config_path = try std.Io.Dir.path.join(state.allocator, &[_][]const u8{ config_parent_path, "config.lua" }); + std.Io.Dir.accessAbsolute(state.io, config_path, .{}) catch { + state.allocator.free(config_path); + config_path = try std.Io.Dir.path.join(state.allocator, &[_][]const u8{ config_parent_path, "config.ini" }); + }; defer state.allocator.free(config_path); custom.binds = .empty; custom.labels = .empty; - var config_parser = try IniParser(Config).init(state.allocator, state.io, config_path, migrator.configFieldHandler); + var config_parser: Parser(Config) = blk: { + if (std.mem.endsWith(u8, config_path, ".ini")) { + break :blk .{ .ini = try IniParser(Config).init( + state.allocator, + state.io, + config_path, + migrator.configFieldHandler, + ) }; + } else { + break :blk .{ .lua = try LuaParser(Config).init(state.allocator, config_path) }; + } + }; defer config_parser.deinit(); defer if (!shutdown or !restart) { var iter = custom.binds.iterator(); @@ -258,7 +283,7 @@ pub fn main(init: std.process.Init) !void { custom.labels.deinit(temporary_allocator); }; - state.config = config_parser.structure; + state.config = config_parser.structure(); var lang_buffer: [16]u8 = undefined; const lang_file = try std.fmt.bufPrint(&lang_buffer, "{s}.ini", .{state.config.lang}); @@ -276,7 +301,7 @@ pub fn main(init: std.process.Init) !void { state.old_save_path = try std.Io.Dir.path.join(state.allocator, &[_][]const u8{ config_parent_path, "save.ini" }); } - if (config_parser.maybe_load_error == null) { + if (config_parser.maybe_load_error() == null and config_parser == .ini) { migrator.lateConfigFieldHandler(&state.config, state.lang); } @@ -636,7 +661,7 @@ pub fn main(init: std.process.Init) !void { ); } - if (config_parser.maybe_load_error) |load_error| { + if (config_parser.maybe_load_error()) |load_error| { // We can't localize this since the config failed to load so we'd fallback to the default language anyway try state.info_line.addMessage( "unable to parse config file", @@ -650,7 +675,7 @@ pub fn main(init: std.process.Init) !void { .{@errorName(load_error)}, ); - for (config_parser.errors.items) |err| { + for (config_parser.errors().items) |err| { try state.log_file.err( state.io, "conf",