Repository: fairyglade/ly Branch: master Commit: abe72c74ff12 Files: 93 Total size: 375.0 KB Directory structure: gitextract__4waf0wh/ ├── .github/ │ ├── FUNDING.yml │ ├── ISSUE_TEMPLATE/ │ │ ├── bug.yml │ │ └── feature.yml │ └── pull_request_template.md ├── .gitignore ├── build.zig ├── build.zig.zon ├── license.md ├── ly-core/ │ ├── build.zig │ ├── build.zig.zon │ └── src/ │ ├── LogFile.zig │ ├── SharedError.zig │ ├── UidRange.zig │ ├── interop.zig │ └── root.zig ├── ly-ui/ │ ├── build.zig │ ├── build.zig.zon │ └── src/ │ ├── Cell.zig │ ├── Position.zig │ ├── TerminalBuffer.zig │ ├── Widget.zig │ ├── components/ │ │ ├── BigLabel.zig │ │ ├── CenteredBox.zig │ │ ├── Label.zig │ │ ├── Text.zig │ │ ├── bigLabelLocales/ │ │ │ ├── en.zig │ │ │ └── fa.zig │ │ └── generic.zig │ ├── keyboard.zig │ └── root.zig ├── readme.md ├── res/ │ ├── config.ini │ ├── custom-sessions/ │ │ └── README │ ├── example.dur │ ├── lang/ │ │ ├── ar.ini │ │ ├── bg.ini │ │ ├── cat.ini │ │ ├── cs.ini │ │ ├── de.ini │ │ ├── en.ini │ │ ├── eo.ini │ │ ├── es.ini │ │ ├── fr.ini │ │ ├── it.ini │ │ ├── ja_JP.ini │ │ ├── ku.ini │ │ ├── lv.ini │ │ ├── normalize_lang_files.py │ │ ├── pl.ini │ │ ├── pt.ini │ │ ├── pt_BR.ini │ │ ├── ro.ini │ │ ├── ru.ini │ │ ├── sr.ini │ │ ├── sv.ini │ │ ├── tr.ini │ │ ├── uk.ini │ │ └── zh_CN.ini │ ├── ly-dinit │ ├── ly-freebsd-wrapper │ ├── ly-kmsconvt@.service │ ├── ly-openrc │ ├── ly-runit-service/ │ │ ├── conf │ │ ├── finish │ │ └── run │ ├── ly-s6/ │ │ ├── run │ │ └── type │ ├── ly-sysvinit │ ├── ly@.service │ ├── pam.d/ │ │ ├── ly-freebsd │ │ ├── ly-freebsd-autologin │ │ ├── ly-linux │ │ └── ly-linux-autologin │ ├── setup.sh │ └── startup.sh └── src/ ├── Environment.zig ├── animations/ │ ├── Cascade.zig │ ├── ColorMix.zig │ ├── Doom.zig │ ├── DurFile.zig │ ├── GameOfLife.zig │ └── Matrix.zig ├── auth.zig ├── components/ │ ├── InfoLine.zig │ ├── Session.zig │ └── UserList.zig ├── config/ │ ├── Config.zig │ ├── Lang.zig │ ├── OldSave.zig │ ├── SavedUsers.zig │ └── migrator.zig ├── enums.zig └── main.zig ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/FUNDING.yml ================================================ github: AnErrupTion liberapay: ShiningLea ================================================ FILE: .github/ISSUE_TEMPLATE/bug.yml ================================================ name: Bug report description: File a bug report. title: "[Bug] " labels: ["bug"] body: - type: checkboxes id: prerequisites attributes: label: Pre-requisites description: By submitting this issue, you agree to have done the following. options: - label: I have looked for any other duplicate issues required: true - label: I have reproduced the issue on a fresh install of my OS & Ly with default settings, except ones I will mention required: true - label: I have confirmed this issue also occurs on the latest development version required: true - type: input id: version attributes: label: Ly version description: The output of `ly --version`. Please note that only Ly v1.2.0 and above are supported. placeholder: 1.1.0-dev.12+2b0301c validations: required: true - type: textarea id: observed attributes: label: Observed behavior description: What happened? validations: required: true - type: textarea id: expected attributes: label: Expected behavior description: What did you expect to happen instead? validations: required: true - type: input id: desktop attributes: label: OS + Desktop environment/Window manager description: Which OS and DE (or WM) did you use when observing the problem? validations: required: true - type: textarea id: reproduction attributes: label: Steps to reproduce description: What **exactly** can someone else do in order to observe the problem you observed? placeholder: | 1. Authenticate with ... 2. Go to ... 3. Create file ... 4. Log out and log back in 5. Observe error validations: required: true - type: textarea id: logs attributes: label: Relevant logs description: | Please copy and paste (or attach) any relevant logs, error messages or any other output. This will be automatically formatted into code, so no need for backticks. Screenshots are accepted if they make life easier for you. The log files (located as specified by `/etc/ly/config.ini`) usually contain relevant information about the problem: - The session log is located at `~/.local/state/ly-session.log` by default. - The system log is located at `/var/log/ly.log` by default. render: shell - type: textarea id: moreinfo attributes: label: Additional information description: If you have any additional information that might be helpful in reproducing the problem, please provide it here. ================================================ FILE: .github/ISSUE_TEMPLATE/feature.yml ================================================ name: Feature request description: Request a new feature or enhancement. title: "[Feature] " labels: ["feature"] body: - type: checkboxes id: prerequisites attributes: label: Pre-requisites description: By submitting this issue, you agree to have done the following. options: - label: I have looked for any other duplicate issues required: true - label: I have confirmed the requested feature doesn't exist in the latest version in development required: true - type: textarea id: wanted attributes: label: Wanted behavior description: What do you want to be added? Describe the behavior clearly. validations: required: true ================================================ FILE: .github/pull_request_template.md ================================================ ## What are the changes about? _Replace this with a brief description of your changes_ ## What existing issue does this resolve? _Replace this with a reference to an existing issue, or N/A if there is none_ ## Pre-requisites - [ ] I have tested & confirmed the changes work locally ================================================ FILE: .gitignore ================================================ .idea/ zig-cache/ zig-out/ valgrind.log .zig-cache ================================================ FILE: build.zig ================================================ const std = @import("std"); const builtin = @import("builtin"); const PatchMap = std.StringHashMap([]const u8); const InitSystem = enum { systemd, openrc, runit, s6, dinit, sysvinit, freebsd, }; const min_zig_string = "0.15.0"; const current_zig = builtin.zig_version; // Implementing zig version detection through compile time comptime { const min_zig = std.SemanticVersion.parse(min_zig_string) catch unreachable; if (current_zig.order(min_zig) == .lt) { @compileError(std.fmt.comptimePrint("Your Zig version v{} does not meet the minimum build requirement of v{}", .{ current_zig, min_zig })); } } const ly_version = std.SemanticVersion{ .major = 1, .minor = 4, .patch = 0 }; var dest_directory: []const u8 = undefined; var config_directory: []const u8 = undefined; var prefix_directory: []const u8 = undefined; var executable_name: []const u8 = undefined; var init_system: InitSystem = undefined; var default_tty_str: []const u8 = undefined; pub fn build(b: *std.Build) !void { dest_directory = b.option([]const u8, "dest_directory", "Specify a destination directory for installation") orelse ""; config_directory = b.option([]const u8, "config_directory", "Specify a default config directory (default is /etc). This path gets embedded into the binary") orelse "/etc"; prefix_directory = b.option([]const u8, "prefix_directory", "Specify a default prefix directory (default is /usr)") orelse "/usr"; executable_name = b.option([]const u8, "name", "Specify installed executable file name (default is ly)") orelse "ly"; init_system = b.option(InitSystem, "init_system", "Specify the target init system (default is systemd)") orelse .systemd; const build_options = b.addOptions(); const version_str = try getVersionStr(b, "ly", ly_version); const enable_x11_support = b.option(bool, "enable_x11_support", "Enable X11 support (default is on)") orelse true; const default_tty = b.option(u8, "default_tty", "Set the TTY (default is 2)") orelse 2; const fallback_tty = b.option(u8, "fallback_tty", "Set the fallback TTY (default is 2). This value gets embedded into the binary") orelse 2; const fallback_uid_min = b.option(std.posix.uid_t, "fallback_uid_min", "Set the fallback minimum UID (default is 1000). This value gets embedded into the binary") orelse 1000; const fallback_uid_max = b.option(std.posix.uid_t, "fallback_uid_max", "Set the fallback maximum UID (default is 60000). This value gets embedded into the binary") orelse 60000; default_tty_str = try std.fmt.allocPrint(b.allocator, "{d}", .{default_tty}); build_options.addOption([]const u8, "config_directory", config_directory); build_options.addOption([]const u8, "prefix_directory", prefix_directory); build_options.addOption([]const u8, "version", version_str); build_options.addOption(u8, "tty", default_tty); build_options.addOption(u8, "fallback_tty", fallback_tty); build_options.addOption(std.posix.uid_t, "fallback_uid_min", fallback_uid_min); build_options.addOption(std.posix.uid_t, "fallback_uid_max", fallback_uid_max); build_options.addOption(bool, "enable_x11_support", enable_x11_support); const target = b.standardTargetOptions(.{}); const optimize = b.standardOptimizeOption(.{}); const exe = b.addExecutable(.{ .name = "ly", .root_module = b.createModule(.{ .root_source_file = b.path("src/main.zig"), .target = target, .optimize = optimize, }), // Here until the native backend matures in terms of performance .use_llvm = true, }); const ly_ui = b.dependency("ly_ui", .{ .target = target, .optimize = optimize }); exe.root_module.addImport("ly-ui", ly_ui.module("ly-ui")); exe.root_module.addOptions("build_options", build_options); const clap = b.dependency("clap", .{ .target = target, .optimize = optimize }); exe.root_module.addImport("clap", clap.module("clap")); exe.linkSystemLibrary("pam"); if (enable_x11_support) exe.linkSystemLibrary("xcb"); exe.linkLibC(); b.installArtifact(exe); const run_cmd = b.addRunArtifact(exe); run_cmd.step.dependOn(b.getInstallStep()); if (b.args) |args| run_cmd.addArgs(args); const run_step = b.step("run", "Run the app"); run_step.dependOn(&run_cmd.step); const installexe_step = b.step("installexe", "Install Ly and the selected init system service"); installexe_step.makeFn = Installer(true).make; installexe_step.dependOn(b.getInstallStep()); const installnoconf_step = b.step("installnoconf", "Install Ly and the selected init system service, but not the configuration file"); installnoconf_step.makeFn = Installer(false).make; installnoconf_step.dependOn(b.getInstallStep()); const uninstallexe_step = b.step("uninstallexe", "Uninstall Ly and remove the selected init system service"); uninstallexe_step.makeFn = Uninstaller(true).make; const uninstallnoconf_step = b.step("uninstallnoconf", "Uninstall Ly and remove the selected init system service, but keep the configuration directory"); uninstallnoconf_step.makeFn = Uninstaller(false).make; } pub fn Installer(install_config: bool) type { return struct { pub fn make(step: *std.Build.Step, _: std.Build.Step.MakeOptions) !void { const allocator = step.owner.allocator; var patch_map = PatchMap.init(allocator); defer patch_map.deinit(); try patch_map.put("$DEFAULT_TTY", default_tty_str); try patch_map.put("$CONFIG_DIRECTORY", config_directory); try patch_map.put("$PREFIX_DIRECTORY", prefix_directory); try patch_map.put("$EXECUTABLE_NAME", executable_name); // The "-a" argument doesn't exist on FreeBSD, so we use "-p" // instead to shutdown the system. try patch_map.put("$PLATFORM_SHUTDOWN_ARG", if (init_system == .freebsd) "-p" else "-a"); try install_ly(allocator, patch_map, install_config); try install_service(allocator, patch_map); } }; } fn install_ly(allocator: std.mem.Allocator, patch_map: PatchMap, install_config: bool) !void { const ly_config_directory = try std.fs.path.join(allocator, &[_][]const u8{ dest_directory, config_directory, "/ly" }); std.fs.cwd().makePath(ly_config_directory) catch { std.debug.print("warn: {s} already exists as a directory.\n", .{ly_config_directory}); }; const ly_custom_sessions_directory = try std.fs.path.join(allocator, &[_][]const u8{ dest_directory, config_directory, "/ly/custom-sessions" }); std.fs.cwd().makePath(ly_custom_sessions_directory) catch { std.debug.print("warn: {s} already exists as a directory.\n", .{ly_custom_sessions_directory}); }; const ly_lang_path = try std.fs.path.join(allocator, &[_][]const u8{ dest_directory, config_directory, "/ly/lang" }); std.fs.cwd().makePath(ly_lang_path) catch { std.debug.print("warn: {s} already exists as a directory.\n", .{ly_lang_path}); }; { const exe_path = try std.fs.path.join(allocator, &[_][]const u8{ dest_directory, prefix_directory, "/bin" }); std.fs.cwd().makePath(exe_path) catch { if (!std.mem.eql(u8, dest_directory, "")) { std.debug.print("warn: {s} already exists as a directory.\n", .{exe_path}); } }; var executable_dir = std.fs.cwd().openDir(exe_path, .{}) catch unreachable; defer executable_dir.close(); try installFile("zig-out/bin/ly", executable_dir, exe_path, executable_name, .{}); } { var config_dir = std.fs.cwd().openDir(ly_config_directory, .{}) catch unreachable; defer config_dir.close(); if (install_config) { const patched_config = try patchFile(allocator, "res/config.ini", patch_map); try installText(patched_config, config_dir, ly_config_directory, "config.ini", .{}); try installFile("res/startup.sh", config_dir, ly_config_directory, "startup.sh", .{ .override_mode = 0o755 }); } const patched_example_config = try patchFile(allocator, "res/config.ini", patch_map); try installText(patched_example_config, config_dir, ly_config_directory, "config.ini.example", .{}); const patched_setup = try patchFile(allocator, "res/setup.sh", patch_map); try installText(patched_setup, config_dir, ly_config_directory, "setup.sh", .{ .mode = 0o755 }); try installFile("res/example.dur", config_dir, ly_config_directory, "example.dur", .{ .override_mode = 0o755 }); } { var custom_sessions_dir = std.fs.cwd().openDir(ly_custom_sessions_directory, .{}) catch unreachable; defer custom_sessions_dir.close(); const patched_readme = try patchFile(allocator, "res/custom-sessions/README", patch_map); try installText(patched_readme, custom_sessions_dir, ly_custom_sessions_directory, "README", .{}); } { var lang_dir = std.fs.cwd().openDir(ly_lang_path, .{}) catch unreachable; defer lang_dir.close(); const languages = [_][]const u8{ "ar.ini", "cat.ini", "cs.ini", "de.ini", "en.ini", "es.ini", "fr.ini", "it.ini", "ja_JP.ini", "lv.ini", "pl.ini", "pt.ini", "pt_BR.ini", "ro.ini", "ru.ini", "sr.ini", "sv.ini", "tr.ini", "uk.ini", "zh_CN.ini", }; inline for (languages) |language| { try installFile("res/lang/" ++ language, lang_dir, ly_lang_path, language, .{}); } } { const pam_path = try std.fs.path.join(allocator, &[_][]const u8{ dest_directory, config_directory, "/pam.d" }); std.fs.cwd().makePath(pam_path) catch { if (!std.mem.eql(u8, dest_directory, "")) { std.debug.print("warn: {s} already exists as a directory.\n", .{pam_path}); } }; var pam_dir = std.fs.cwd().openDir(pam_path, .{}) catch unreachable; defer pam_dir.close(); try installFile(if (init_system == .freebsd) "res/pam.d/ly-freebsd" else "res/pam.d/ly-linux", pam_dir, pam_path, "ly", .{ .override_mode = 0o644 }); try installFile(if (init_system == .freebsd) "res/pam.d/ly-freebsd-autologin" else "res/pam.d/ly-linux-autologin", pam_dir, pam_path, "ly-autologin", .{ .override_mode = 0o644 }); } } fn install_service(allocator: std.mem.Allocator, patch_map: PatchMap) !void { switch (init_system) { .systemd => { const service_path = try std.fs.path.join(allocator, &[_][]const u8{ dest_directory, prefix_directory, "/lib/systemd/system" }); std.fs.cwd().makePath(service_path) catch {}; var service_dir = std.fs.cwd().openDir(service_path, .{}) catch unreachable; defer service_dir.close(); const patched_service = try patchFile(allocator, "res/ly@.service", patch_map); try installText(patched_service, service_dir, service_path, "ly@.service", .{ .mode = 0o644 }); const patched_kmsconvt_service = try patchFile(allocator, "res/ly-kmsconvt@.service", patch_map); try installText(patched_kmsconvt_service, service_dir, service_path, "ly-kmsconvt@.service", .{ .mode = 0o644 }); }, .openrc => { const service_path = try std.fs.path.join(allocator, &[_][]const u8{ dest_directory, config_directory, "/init.d" }); std.fs.cwd().makePath(service_path) catch {}; var service_dir = std.fs.cwd().openDir(service_path, .{}) catch unreachable; defer service_dir.close(); const patched_service = try patchFile(allocator, "res/ly-openrc", patch_map); try installText(patched_service, service_dir, service_path, executable_name, .{ .mode = 0o755 }); }, .runit => { const service_path = try std.fs.path.join(allocator, &[_][]const u8{ dest_directory, config_directory, "/sv/ly" }); std.fs.cwd().makePath(service_path) catch {}; var service_dir = std.fs.cwd().openDir(service_path, .{}) catch unreachable; defer service_dir.close(); const supervise_path = try std.fs.path.join(allocator, &[_][]const u8{ service_path, "supervise" }); const patched_conf = try patchFile(allocator, "res/ly-runit-service/conf", patch_map); try installText(patched_conf, service_dir, service_path, "conf", .{}); try installFile("res/ly-runit-service/finish", service_dir, service_path, "finish", .{ .override_mode = 0o755 }); const patched_run = try patchFile(allocator, "res/ly-runit-service/run", patch_map); try installText(patched_run, service_dir, service_path, "run", .{ .mode = 0o755 }); std.fs.cwd().symLink("/run/runit/supervise.ly", supervise_path, .{}) catch |err| { if (err == error.PathAlreadyExists) { std.debug.print("warn: /run/runit/supervise.ly already exists as a symbolic link.\n", .{}); } else { return err; } }; std.debug.print("info: installed symlink /run/runit/supervise.ly\n", .{}); }, .s6 => { const admin_service_path = try std.fs.path.join(allocator, &[_][]const u8{ dest_directory, config_directory, "/s6/adminsv/default/contents.d" }); std.fs.cwd().makePath(admin_service_path) catch {}; var admin_service_dir = std.fs.cwd().openDir(admin_service_path, .{}) catch unreachable; defer admin_service_dir.close(); const file = try admin_service_dir.createFile("ly-srv", .{}); file.close(); const service_path = try std.fs.path.join(allocator, &[_][]const u8{ dest_directory, config_directory, "/s6/sv/ly-srv" }); std.fs.cwd().makePath(service_path) catch {}; var service_dir = std.fs.cwd().openDir(service_path, .{}) catch unreachable; defer service_dir.close(); const patched_run = try patchFile(allocator, "res/ly-s6/run", patch_map); try installText(patched_run, service_dir, service_path, "run", .{ .mode = 0o755 }); try installFile("res/ly-s6/type", service_dir, service_path, "type", .{}); }, .dinit => { const service_path = try std.fs.path.join(allocator, &[_][]const u8{ dest_directory, config_directory, "/dinit.d" }); std.fs.cwd().makePath(service_path) catch {}; var service_dir = std.fs.cwd().openDir(service_path, .{}) catch unreachable; defer service_dir.close(); const patched_service = try patchFile(allocator, "res/ly-dinit", patch_map); try installText(patched_service, service_dir, service_path, "ly", .{}); }, .sysvinit => { const service_path = try std.fs.path.join(allocator, &[_][]const u8{ dest_directory, config_directory, "/init.d" }); std.fs.cwd().makePath(service_path) catch {}; var service_dir = std.fs.cwd().openDir(service_path, .{}) catch unreachable; defer service_dir.close(); const patched_service = try patchFile(allocator, "res/ly-sysvinit", patch_map); try installText(patched_service, service_dir, service_path, "ly", .{ .mode = 0o755 }); }, .freebsd => { const exe_path = try std.fs.path.join(allocator, &[_][]const u8{ dest_directory, prefix_directory, "/bin" }); var executable_dir = std.fs.cwd().openDir(exe_path, .{}) catch unreachable; defer executable_dir.close(); const patched_wrapper = try patchFile(allocator, "res/ly-freebsd-wrapper", patch_map); try installText(patched_wrapper, executable_dir, exe_path, "ly_wrapper", .{ .mode = 0o755 }); }, } } pub fn Uninstaller(uninstall_config: bool) type { return struct { pub fn make(step: *std.Build.Step, _: std.Build.Step.MakeOptions) !void { const allocator = step.owner.allocator; if (uninstall_config) { try deleteTree(allocator, config_directory, "/ly", "ly config directory not found"); } const exe_path = try std.fs.path.join(allocator, &[_][]const u8{ prefix_directory, "/bin/", executable_name }); var success = true; std.fs.cwd().deleteFile(exe_path) catch { std.debug.print("warn: ly executable not found\n", .{}); success = false; }; if (success) std.debug.print("info: deleted {s}\n", .{exe_path}); try deleteFile(allocator, config_directory, "/pam.d/ly", "ly pam file not found"); switch (init_system) { .systemd => try deleteFile(allocator, prefix_directory, "/lib/systemd/system/ly@.service", "systemd service not found"), .openrc => try deleteFile(allocator, config_directory, "/init.d/ly", "openrc service not found"), .runit => try deleteTree(allocator, config_directory, "/sv/ly", "runit service not found"), .s6 => { try deleteTree(allocator, config_directory, "/s6/sv/ly-srv", "s6 service not found"); try deleteFile(allocator, config_directory, "/s6/adminsv/default/contents.d/ly-srv", "s6 admin service not found"); }, .dinit => try deleteFile(allocator, config_directory, "/dinit.d/ly", "dinit service not found"), .sysvinit => try deleteFile(allocator, config_directory, "/init.d/ly", "sysvinit service not found"), .freebsd => try deleteFile(allocator, prefix_directory, "/bin/ly_wrapper", "freebsd wrapper not found"), } } }; } fn getVersionStr(b: *std.Build, name: []const u8, version: std.SemanticVersion) ![]const u8 { const version_str = b.fmt("{d}.{d}.{d}", .{ version.major, version.minor, version.patch }); var status: u8 = undefined; const git_describe_raw = b.runAllowFail(&[_][]const u8{ "git", "-C", b.build_root.path orelse ".", "describe", "--match", "*.*.*", "--tags", }, &status, .Ignore) catch { return version_str; }; var git_describe = std.mem.trim(u8, git_describe_raw, " \n\r"); git_describe = std.mem.trimLeft(u8, git_describe, "v"); switch (std.mem.count(u8, git_describe, "-")) { 0 => { if (!std.mem.eql(u8, version_str, git_describe)) { std.debug.print("{s} version '{s}' does not match git tag: '{s}'\n", .{ name, version_str, git_describe }); std.process.exit(1); } return version_str; }, 2 => { // Untagged development build (e.g. 0.10.0-dev.2025+ecf0050a9). var it = std.mem.splitScalar(u8, git_describe, '-'); const tagged_ancestor = std.mem.trimLeft(u8, it.first(), "v"); const commit_height = it.next().?; const commit_id = it.next().?; const ancestor_ver = try std.SemanticVersion.parse(tagged_ancestor); if (version.order(ancestor_ver) != .gt) { std.debug.print("{s} version '{f}' must be greater than tagged ancestor '{f}'\n", .{ name, version, ancestor_ver }); std.process.exit(1); } // Check that the commit hash is prefixed with a 'g' (a Git convention). if (commit_id.len < 1 or commit_id[0] != 'g') { std.debug.print("Unexpected `git describe` output: {s}\n", .{git_describe}); return version_str; } // The version is reformatted in accordance with the https://semver.org specification. return b.fmt("{s}-dev.{s}+{s}", .{ version_str, commit_height, commit_id[1..] }); }, else => { std.debug.print("Unexpected `git describe` output: {s}\n", .{git_describe}); return version_str; }, } } fn installFile( source_file: []const u8, destination_directory: std.fs.Dir, destination_directory_path: []const u8, destination_file: []const u8, options: std.fs.Dir.CopyFileOptions, ) !void { try std.fs.cwd().copyFile(source_file, destination_directory, destination_file, options); std.debug.print("info: installed {s}/{s}\n", .{ destination_directory_path, destination_file }); } fn patchFile(allocator: std.mem.Allocator, source_file: []const u8, patch_map: PatchMap) ![]const u8 { var file = try std.fs.cwd().openFile(source_file, .{}); defer file.close(); const stat = try file.stat(); var buffer: [4096]u8 = undefined; var reader = file.reader(&buffer); var text = try reader.interface.readAlloc(allocator, stat.size); var iterator = patch_map.iterator(); while (iterator.next()) |kv| { const new_text = try std.mem.replaceOwned(u8, allocator, text, kv.key_ptr.*, kv.value_ptr.*); allocator.free(text); text = new_text; } return text; } fn installText( text: []const u8, destination_directory: std.fs.Dir, destination_directory_path: []const u8, destination_file: []const u8, options: std.fs.File.CreateFlags, ) !void { var file = try destination_directory.createFile(destination_file, options); defer file.close(); var buffer: [1024]u8 = undefined; var writer = file.writer(&buffer); try writer.interface.writeAll(text); try writer.interface.flush(); std.debug.print("info: installed {s}/{s}\n", .{ destination_directory_path, destination_file }); } fn deleteFile( allocator: std.mem.Allocator, prefix: []const u8, file: []const u8, warning: []const u8, ) !void { const path = try std.fs.path.join(allocator, &[_][]const u8{ dest_directory, prefix, file }); std.fs.cwd().deleteFile(path) catch |err| { if (err == error.FileNotFound) { std.debug.print("warn: {s}\n", .{warning}); return; } return err; }; std.debug.print("info: deleted {s}\n", .{path}); } fn deleteTree( allocator: std.mem.Allocator, prefix: []const u8, directory: []const u8, warning: []const u8, ) !void { const path = try std.fs.path.join(allocator, &[_][]const u8{ dest_directory, prefix, directory }); var dir = std.fs.cwd().openDir(path, .{}) catch |err| { if (err == error.FileNotFound) { std.debug.print("warn: {s}\n", .{warning}); return; } return err; }; dir.close(); try std.fs.cwd().deleteTree(path); std.debug.print("info: deleted {s}\n", .{path}); } ================================================ FILE: build.zig.zon ================================================ .{ .name = .ly, .version = "1.4.0", .fingerprint = 0xa148ffcc5dc2cb59, .minimum_zig_version = "0.15.0", .dependencies = .{ .ly_ui = .{ .path = "ly-ui", }, .clap = .{ .url = "git+https://github.com/Hejsil/zig-clap#5289e0753cd274d65344bef1c114284c633536ea", .hash = "clap-0.11.0-oBajB-HnAQDPCKYzwF7rO3qDFwRcD39Q0DALlTSz5H7e", }, }, .paths = .{""}, } ================================================ FILE: license.md ================================================ DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE Version 2, December 2004 Copyright (C) 2004 Sam Hocevar Everyone is permitted to copy and distribute verbatim or modified copies of this license document, and changing it is allowed as long as the name is changed. DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 0. You just DO WHAT THE FUCK YOU WANT TO. ================================================ FILE: ly-core/build.zig ================================================ const std = @import("std"); pub fn build(b: *std.Build) void { const target = b.standardTargetOptions(.{}); const optimize = b.standardOptimizeOption(.{}); const mod = b.addModule("ly-core", .{ .root_source_file = b.path("src/root.zig"), .target = target, .optimize = optimize, }); const zigini = b.dependency("zigini", .{ .target = target, .optimize = optimize }); mod.addImport("zigini", zigini.module("zigini")); const mod_tests = b.addTest(.{ .root_module = mod, }); const run_mod_tests = b.addRunArtifact(mod_tests); const test_step = b.step("test", "Run tests"); test_step.dependOn(&run_mod_tests.step); } ================================================ FILE: ly-core/build.zig.zon ================================================ .{ .name = .ly_core, .version = "1.0.0", .fingerprint = 0xddda7afda795472, .minimum_zig_version = "0.15.0", .dependencies = .{ .zigini = .{ .url = "git+https://github.com/AnErrupTion/zigini?ref=zig-0.15.0#9281f47702b57779e831d7618e158abb8eb4d4a2", .hash = "zigini-0.3.3-36M0FRJJAADZVq5HPm-hYKMpFFTr0OgjbEYcK2ijKZ5n", }, }, .paths = .{ "build.zig", "build.zig.zon", "src", }, } ================================================ FILE: ly-core/src/LogFile.zig ================================================ const std = @import("std"); const interop = @import("interop.zig"); const LogFile = @This(); path: []const u8, could_open_log_file: bool = undefined, file: std.fs.File = undefined, buffer: []u8, file_writer: std.fs.File.Writer = undefined, pub fn init(path: []const u8, buffer: []u8) !LogFile { var log_file = LogFile{ .path = path, .buffer = buffer }; log_file.could_open_log_file = try openLogFile(path, &log_file); return log_file; } pub fn reinit(self: *LogFile) !void { self.could_open_log_file = try openLogFile(self.path, self); } pub fn deinit(self: *LogFile) void { self.file.close(); } pub fn info(self: *LogFile, category: []const u8, comptime message: []const u8, args: anytype) !void { var buffer: [128:0]u8 = undefined; const time = interop.timeAsString(&buffer, "%Y-%m-%d %H:%M:%S"); try self.file_writer.interface.print("{s} [info/{s}] ", .{ time, category }); try self.file_writer.interface.print(message, args); try self.file_writer.interface.writeByte('\n'); try self.file_writer.interface.flush(); } pub fn err(self: *LogFile, category: []const u8, comptime message: []const u8, args: anytype) !void { var buffer: [128:0]u8 = undefined; const time = interop.timeAsString(&buffer, "%Y-%m-%d %H:%M:%S"); try self.file_writer.interface.print("{s} [err/{s}] ", .{ time, category }); try self.file_writer.interface.print(message, args); try self.file_writer.interface.writeByte('\n'); try self.file_writer.interface.flush(); } fn openLogFile(path: []const u8, log_file: *LogFile) !bool { var could_open_log_file = true; open_log_file: { log_file.file = std.fs.cwd().openFile(path, .{ .mode = .write_only }) catch std.fs.cwd().createFile(path, .{ .mode = 0o666 }) catch { // If we could neither open an existing log file nor create a new // one, abort. could_open_log_file = false; break :open_log_file; }; } if (!could_open_log_file) { log_file.file = try std.fs.openFileAbsolute("/dev/null", .{ .mode = .write_only }); } var log_file_writer = log_file.file.writer(log_file.buffer); // Seek to the end of the log file if (could_open_log_file) { const stat = try log_file.file.stat(); try log_file_writer.seekTo(stat.size); } log_file.file_writer = log_file_writer; return could_open_log_file; } ================================================ FILE: ly-core/src/SharedError.zig ================================================ const std = @import("std"); const ErrInt = std.meta.Int(.unsigned, @bitSizeOf(anyerror)); const ErrorHandler = packed struct { has_error: bool = false, err_int: ErrInt = 0, }; const SharedError = @This(); data: []align(std.heap.page_size_min) u8, write_error_event_fn: ?*const fn (anyerror, *anyopaque) anyerror!void, ctx: ?*anyopaque, pub fn init( write_error_event_fn: ?*const fn (anyerror, *anyopaque) anyerror!void, ctx: ?*anyopaque, ) !SharedError { const data = try std.posix.mmap(null, @sizeOf(ErrorHandler), std.posix.PROT.READ | std.posix.PROT.WRITE, .{ .TYPE = .SHARED, .ANONYMOUS = true }, -1, 0); return .{ .data = data, .write_error_event_fn = write_error_event_fn, .ctx = ctx, }; } pub fn deinit(self: *SharedError) void { std.posix.munmap(self.data); } pub fn writeError(self: SharedError, err: anyerror) void { var buf_stream = std.io.fixedBufferStream(self.data); const writer = buf_stream.writer(); writer.writeStruct(ErrorHandler{ .has_error = true, .err_int = @intFromError(err) }) catch {}; if (self.write_error_event_fn) |write_error_event_fn| { @call(.auto, write_error_event_fn, .{ err, self.ctx.? }) catch {}; } } pub fn readError(self: SharedError) ?anyerror { var buf_stream = std.io.fixedBufferStream(self.data); const reader = buf_stream.reader(); const err_handler = try reader.readStruct(ErrorHandler); if (err_handler.has_error) return @errorFromInt(err_handler.err_int); return null; } ================================================ FILE: ly-core/src/UidRange.zig ================================================ const std = @import("std"); // We set both values to 0 by default so that, in case they aren't present in // the login.defs for some reason, then only the root username will be shown uid_min: std.posix.uid_t = 0, uid_max: std.posix.uid_t = 0, ================================================ FILE: ly-core/src/interop.zig ================================================ const std = @import("std"); const builtin = @import("builtin"); const UidRange = @import("UidRange.zig"); pub const pam = @cImport({ @cInclude("security/pam_appl.h"); }); pub const utmp = @cImport({ @cInclude("utmpx.h"); }); // Exists for X11 support only pub const xcb = @cImport({ @cInclude("xcb/xcb.h"); }); const pwd = @cImport({ @cInclude("pwd.h"); // We include a FreeBSD-specific header here since login_cap.h references // the passwd struct directly, so we can't import it separately if (builtin.os.tag == .freebsd) { @cInclude("sys/types.h"); @cInclude("login_cap.h"); } }); const stdlib = @cImport({ @cInclude("stdlib.h"); }); const unistd = @cImport({ @cInclude("unistd.h"); }); const grp = @cImport({ @cInclude("grp.h"); }); const system_time = @cImport({ @cInclude("sys/time.h"); }); const time = @cImport({ @cInclude("time.h"); }); pub const TimeOfDay = struct { seconds: i64, microseconds: i64, }; pub const UsernameEntry = struct { username: ?[]const u8, uid: std.posix.uid_t, gid: std.posix.gid_t, home: ?[]const u8, shell: ?[]const u8, passwd_struct: [*c]pwd.passwd, }; // Contains the platform-specific code fn PlatformStruct() type { return switch (builtin.os.tag) { .linux => struct { pub const kd = @cImport({ @cInclude("sys/kd.h"); }); pub const vt = @cImport({ @cInclude("sys/vt.h"); }); pub const LedState = c_char; pub const get_led_state = kd.KDGKBLED; pub const set_led_state = kd.KDSKBLED; pub const numlock_led = kd.K_NUMLOCK; pub const capslock_led = kd.K_CAPSLOCK; pub const vt_activate = vt.VT_ACTIVATE; pub const vt_waitactive = vt.VT_WAITACTIVE; pub fn setUserContextImpl(username: [*:0]const u8, entry: UsernameEntry) !void { const status = grp.initgroups(username, @intCast(entry.gid)); if (status != 0) return error.GroupInitializationFailed; std.posix.setgid(@intCast(entry.gid)) catch return error.SetUserGidFailed; std.posix.setuid(@intCast(entry.uid)) catch return error.SetUserUidFailed; } // Procedure: // 1. Open /proc/self/stat to retrieve the tty_nr field // 2. Parse the tty_nr field to extract the major and minor device // numbers // 3. Then, read every /sys/class/tty/[dir]/dev, where [dir] is // every sub-directory // 4. Finally, compare the major and minor device numbers with the // extracted values. If they correspond, parse [dir] to get the // TTY ID pub fn getActiveTtyImpl(allocator: std.mem.Allocator, use_kmscon_vt: bool) !u8 { var file_buffer: [256]u8 = undefined; if (use_kmscon_vt) { var file = try std.fs.openFileAbsolute("/sys/class/tty/tty0/active", .{}); defer file.close(); var reader = file.reader(&file_buffer); var buffer: [16]u8 = undefined; const read = try readBuffer(&reader.interface, &buffer); const tty = buffer[0..(read - 1)]; return std.fmt.parseInt(u8, tty["tty".len..], 10); } var tty_major: u16 = undefined; var tty_minor: u16 = undefined; { var file = try std.fs.openFileAbsolute("/proc/self/stat", .{}); defer file.close(); var reader = file.reader(&file_buffer); var buffer: [1024]u8 = undefined; const read = try readBuffer(&reader.interface, &buffer); var iterator = std.mem.splitScalar(u8, buffer[0..read], ' '); var fields: [52][]const u8 = undefined; var index: usize = 0; while (iterator.next()) |field| { fields[index] = field; index += 1; } const tty_nr = try std.fmt.parseInt(u16, fields[6], 10); tty_major = tty_nr / 256; tty_minor = tty_nr % 256; } var directory = try std.fs.openDirAbsolute("/sys/class/tty", .{ .iterate = true }); defer directory.close(); var iterator = directory.iterate(); while (try iterator.next()) |entry| { const path = try std.fmt.allocPrint(allocator, "/sys/class/tty/{s}/dev", .{entry.name}); defer allocator.free(path); var file = try std.fs.openFileAbsolute(path, .{}); defer file.close(); var reader = file.reader(&file_buffer); var buffer: [16]u8 = undefined; const read = try readBuffer(&reader.interface, &buffer); var device_iterator = std.mem.splitScalar(u8, buffer[0..(read - 1)], ':'); const device_major_str = device_iterator.next() orelse continue; const device_minor_str = device_iterator.next() orelse continue; const device_major = try std.fmt.parseInt(u8, device_major_str, 10); const device_minor = try std.fmt.parseInt(u8, device_minor_str, 10); if (device_major == tty_major and device_minor == tty_minor) { const tty_id_str = entry.name["tty".len..]; return try std.fmt.parseInt(u8, tty_id_str, 10); } } return error.NoTtyFound; } // This is very bad parsing, but we only need to get 2 values.. // and the format of the file seems to be standard? So this should // be fine... pub fn getUserIdRange(allocator: std.mem.Allocator, file_path: []const u8) !UidRange { const login_defs_file = try std.fs.cwd().openFile(file_path, .{}); defer login_defs_file.close(); const login_defs_buffer = try login_defs_file.readToEndAlloc(allocator, std.math.maxInt(u16)); defer allocator.free(login_defs_buffer); var iterator = std.mem.splitScalar(u8, login_defs_buffer, '\n'); var uid_range = UidRange{}; var nameFound = false; while (iterator.next()) |line| { 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); nameFound = true; } else if (std.mem.startsWith(u8, trimmed_line, "UID_MAX")) { uid_range.uid_max = try parseValue(std.posix.uid_t, "UID_MAX", trimmed_line); nameFound = true; } } if (!nameFound) return error.UidNameNotFound; return uid_range; } fn parseValue(comptime T: type, name: []const u8, buffer: []const u8) !T { var iterator = std.mem.splitAny(u8, buffer, " \t"); var maybe_value: ?T = null; while (iterator.next()) |slice| { // Skip the slice if it's empty (whitespace) or is the name of the // property (e.g. UID_MIN or UID_MAX) if (slice.len == 0 or std.mem.eql(u8, slice, name)) continue; maybe_value = std.fmt.parseInt(T, slice, 10) catch continue; } return maybe_value orelse error.ValueNotFound; } fn readBuffer(reader: *std.Io.Reader, buffer: []u8) !usize { var bytes_read: usize = 0; var byte: u8 = try reader.takeByte(); while (byte != 0 and bytes_read < buffer.len) { buffer[bytes_read] = byte; bytes_read += 1; byte = reader.takeByte() catch break; } return bytes_read; } }, .freebsd => struct { pub const kbio = @cImport({ @cInclude("sys/kbio.h"); }); pub const consio = @cImport({ @cInclude("sys/consio.h"); }); pub const LedState = c_int; pub const get_led_state = kbio.KDGETLED; pub const set_led_state = kbio.KDSETLED; pub const numlock_led = kbio.LED_NUM; pub const capslock_led = kbio.LED_CAP; pub const vt_activate = consio.VT_ACTIVATE; pub const vt_waitactive = consio.VT_WAITACTIVE; const FREEBSD_UID_MIN = 1000; const FREEBSD_UID_MAX = 32000; pub fn setUserContextImpl(username: [*:0]const u8, entry: UsernameEntry) !void { // FreeBSD has initgroups() in unistd const status = unistd.initgroups(username, @intCast(entry.gid)); if (status != 0) return error.GroupInitializationFailed; // FreeBSD sets the GID and UID with setusercontext() const result = pwd.setusercontext(null, entry.passwd_struct, @intCast(entry.uid), pwd.LOGIN_SETALL); if (result != 0) return error.SetUserUidFailed; } pub fn getActiveTtyImpl(_: std.mem.Allocator, _: bool) !u8 { return error.FeatureUnimplemented; } pub fn getUserIdRange(_: std.mem.Allocator, _: []const u8) !UidRange { return .{ // Hardcoded default values chosen from // /usr/src/usr.sbin/pw/pw_conf.c .uid_min = FREEBSD_UID_MIN, .uid_max = FREEBSD_UID_MAX, }; } }, else => @compileError("Unsupported target: " ++ builtin.os.tag), }; } const platform_struct = PlatformStruct(); pub fn supportsUnicode() bool { return builtin.os.tag == .linux or builtin.os.tag == .freebsd; } pub fn timeAsString(buf: [:0]u8, format: [:0]const u8) []u8 { const timer = std.time.timestamp(); const tm_info = time.localtime(&timer); const len = time.strftime(buf, buf.len, format, tm_info); return buf[0..len]; } pub fn getTimeOfDay() !TimeOfDay { var tv: system_time.timeval = undefined; const status = system_time.gettimeofday(&tv, null); if (status != 0) return error.FailedToGetTimeOfDay; return .{ .seconds = @intCast(tv.tv_sec), .microseconds = @intCast(tv.tv_usec), }; } pub fn getActiveTty(allocator: std.mem.Allocator, use_kmscon_vt: bool) !u8 { return platform_struct.getActiveTtyImpl(allocator, use_kmscon_vt); } pub fn switchTty(tty: u8) !void { var status = std.c.ioctl(std.posix.STDIN_FILENO, platform_struct.vt_activate, tty); if (status != 0) return error.FailedToActivateTty; status = std.c.ioctl(std.posix.STDIN_FILENO, platform_struct.vt_waitactive, tty); if (status != 0) return error.FailedToWaitForActiveTty; } pub fn getLockState() !struct { numlock: bool, capslock: bool, } { var led: platform_struct.LedState = undefined; const status = std.c.ioctl(std.posix.STDIN_FILENO, platform_struct.get_led_state, &led); if (status != 0) return error.FailedToGetLockState; return .{ .numlock = (led & platform_struct.numlock_led) != 0, .capslock = (led & platform_struct.capslock_led) != 0, }; } pub fn setNumlock(val: bool) !void { var led: platform_struct.LedState = undefined; var status = std.c.ioctl(std.posix.STDIN_FILENO, platform_struct.get_led_state, &led); if (status != 0) return error.FailedToGetNumlock; const numlock = (led & platform_struct.numlock_led) != 0; if (numlock != val) { status = std.c.ioctl(std.posix.STDIN_FILENO, platform_struct.set_led_state, led ^ platform_struct.numlock_led); if (status != 0) return error.FailedToSetNumlock; } } pub fn setUserContext(allocator: std.mem.Allocator, entry: UsernameEntry) !void { const username_z = try allocator.dupeZ(u8, entry.username.?); defer allocator.free(username_z); return platform_struct.setUserContextImpl(username_z.ptr, entry); } pub fn setUserShell(entry: *UsernameEntry) void { unistd.setusershell(); const shell = unistd.getusershell(); entry.shell = std.mem.span(shell); unistd.endusershell(); } pub fn setEnvironmentVariable(allocator: std.mem.Allocator, name: []const u8, value: []const u8, replace: bool) !void { const name_z = try allocator.dupeZ(u8, name); defer allocator.free(name_z); const value_z = try allocator.dupeZ(u8, value); defer allocator.free(value_z); const status = stdlib.setenv(name_z.ptr, value_z.ptr, @intFromBool(replace)); if (status != 0) return error.SetEnvironmentVariableFailed; } pub fn putEnvironmentVariable(name_and_value: [*c]u8) !void { const status = stdlib.putenv(name_and_value); if (status != 0) return error.PutEnvironmentVariableFailed; } pub fn getNextUsernameEntry() ?UsernameEntry { const entry = pwd.getpwent(); if (entry == null) return null; return .{ .username = if (entry.*.pw_name) |name| std.mem.span(name) else null, .uid = @intCast(entry.*.pw_uid), .gid = @intCast(entry.*.pw_gid), .home = if (entry.*.pw_dir) |dir| std.mem.span(dir) else null, .shell = if (entry.*.pw_shell) |shell| std.mem.span(shell) else null, .passwd_struct = entry, }; } pub fn getUsernameEntry(username: [:0]const u8) ?UsernameEntry { const entry = pwd.getpwnam(username); if (entry == null) return null; return .{ .username = if (entry.*.pw_name) |name| std.mem.span(name) else null, .uid = @intCast(entry.*.pw_uid), .gid = @intCast(entry.*.pw_gid), .home = if (entry.*.pw_dir) |dir| std.mem.span(dir) else null, .shell = if (entry.*.pw_shell) |shell| std.mem.span(shell) else null, .passwd_struct = entry, }; } pub fn closePasswordDatabase() void { pwd.endpwent(); } // This is very bad parsing, but we only need to get 2 values... and the format // of the file doesn't seem to be standard? So this should be fine... pub fn getUserIdRange(allocator: std.mem.Allocator, file_path: []const u8) !UidRange { return platform_struct.getUserIdRange(allocator, file_path); } ================================================ FILE: ly-core/src/root.zig ================================================ const std = @import("std"); pub const ini = @import("zigini"); 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 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), structure: Struct, maybe_load_error: ?anyerror, errors: std.ArrayList(Error), pub fn init( allocator: std.mem.Allocator, path: []const u8, field_handler: ?fn (allocator: std.mem.Allocator, field: ini.IniField) ?ini.IniField, ) !Self { var ini_struct = ini.Ini(Struct).init(allocator); errdefer ini_struct.deinit(); var maybe_load_error: ?anyerror = null; const structure = ini_struct.readFileToStruct(path, .{ .fieldHandler = field_handler, .errorHandler = errorHandler, .comment_characters = "#", }) catch |err| load_error: { maybe_load_error = err; break :load_error Struct{}; }; return .{ .ini_struct = ini_struct, .structure = structure, .maybe_load_error = maybe_load_error, .errors = global_errors, }; } pub fn deinit(self: *Self) void { self.ini_struct.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; } }; } ================================================ FILE: ly-ui/build.zig ================================================ const std = @import("std"); pub fn build(b: *std.Build) void { const target = b.standardTargetOptions(.{}); const optimize = b.standardOptimizeOption(.{}); const mod = b.addModule("ly-ui", .{ .root_source_file = b.path("src/root.zig"), .target = target, .optimize = optimize, }); const ly_core = b.dependency("ly_core", .{ .target = target, .optimize = optimize }); mod.addImport("ly-core", ly_core.module("ly-core")); const termbox_dep = b.dependency("termbox2", .{ .target = target, .optimize = optimize, }); const translate_c = b.addTranslateC(.{ .root_source_file = termbox_dep.path("termbox2.h"), .target = target, .optimize = optimize, }); translate_c.defineCMacroRaw("TB_IMPL"); translate_c.defineCMacro("TB_OPT_ATTR_W", "32"); // Enable 24-bit color support + styling (32-bit) const termbox2 = translate_c.addModule("termbox2"); mod.addImport("termbox2", termbox2); const mod_tests = b.addTest(.{ .root_module = mod, }); const run_mod_tests = b.addRunArtifact(mod_tests); const test_step = b.step("test", "Run tests"); test_step.dependOn(&run_mod_tests.step); } ================================================ FILE: ly-ui/build.zig.zon ================================================ .{ .name = .ly_ui, .version = "1.0.0", .fingerprint = 0x8d11bf85a74ec803, .minimum_zig_version = "0.15.0", .dependencies = .{ .ly_core = .{ .path = "../ly-core", }, .termbox2 = .{ .url = "git+https://github.com/AnErrupTion/termbox2?ref=master#496730697c662893eec43192f48ff616c2539da6", .hash = "N-V-__8AAOEWBQDt5tNdIzIFY6n8DdZsCP-6MyLoNS20wgpA", }, }, .paths = .{ "build.zig", "build.zig.zon", "src", }, } ================================================ FILE: ly-ui/src/Cell.zig ================================================ const TerminalBuffer = @import("TerminalBuffer.zig"); const Cell = @This(); ch: u32, fg: u32, bg: u32, pub fn init(ch: u32, fg: u32, bg: u32) Cell { return .{ .ch = ch, .fg = fg, .bg = bg, }; } pub fn put(self: Cell, x: usize, y: usize) void { if (self.ch == 0) return; TerminalBuffer.setCell(x, y, self); } ================================================ FILE: ly-ui/src/Position.zig ================================================ const Position = @This(); x: usize, y: usize, pub fn init(x: usize, y: usize) Position { return .{ .x = x, .y = y, }; } pub fn add(self: Position, other: Position) Position { return .{ .x = self.x + other.x, .y = self.y + other.y, }; } pub fn addIf(self: Position, other: Position, condition: bool) Position { return .{ .x = self.x + if (condition) other.x else 0, .y = self.y + if (condition) other.y else 0, }; } pub fn addX(self: Position, x: usize) Position { return .{ .x = self.x + x, .y = self.y, }; } pub fn addY(self: Position, y: usize) Position { return .{ .x = self.x, .y = self.y + y, }; } pub fn addXIf(self: Position, x: usize, condition: bool) Position { return .{ .x = self.x + if (condition) x else 0, .y = self.y, }; } pub fn addYIf(self: Position, y: usize, condition: bool) Position { return .{ .x = self.x, .y = self.y + if (condition) y else 0, }; } pub fn addXFrom(self: Position, other: Position) Position { return .{ .x = self.x + other.x, .y = self.y, }; } pub fn addYFrom(self: Position, other: Position) Position { return .{ .x = self.x, .y = self.y + other.y, }; } pub fn addXFromIf(self: Position, other: Position, condition: bool) Position { return .{ .x = self.x + if (condition) other.x else 0, .y = self.y, }; } pub fn addYFromIf(self: Position, other: Position, condition: bool) Position { return .{ .x = self.x, .y = self.y + if (condition) other.y else 0, }; } pub fn remove(self: Position, other: Position) Position { return .{ .x = self.x - other.x, .y = self.y - other.y, }; } pub fn removeIf(self: Position, other: Position, condition: bool) Position { return .{ .x = self.x - if (condition) other.x else 0, .y = self.y - if (condition) other.y else 0, }; } pub fn removeX(self: Position, x: usize) Position { return .{ .x = self.x - x, .y = self.y, }; } pub fn removeY(self: Position, y: usize) Position { return .{ .x = self.x, .y = self.y - y, }; } pub fn removeXIf(self: Position, x: usize, condition: bool) Position { return .{ .x = self.x - if (condition) x else 0, .y = self.y, }; } pub fn removeYIf(self: Position, y: usize, condition: bool) Position { return .{ .x = self.x, .y = self.y - if (condition) y else 0, }; } pub fn removeXFrom(self: Position, other: Position) Position { return .{ .x = self.x - other.x, .y = self.y, }; } pub fn removeYFrom(self: Position, other: Position) Position { return .{ .x = self.x, .y = self.y - other.y, }; } pub fn removeXFromIf(self: Position, other: Position, condition: bool) Position { return .{ .x = self.x - if (condition) other.x else 0, .y = self.y, }; } pub fn removeYFromIf(self: Position, other: Position, condition: bool) Position { return .{ .x = self.x, .y = self.y - if (condition) other.y else 0, }; } pub fn invert(self: Position, other: Position) Position { return .{ .x = other.x - self.x, .y = other.y - self.y, }; } pub fn invertIf(self: Position, other: Position, condition: bool) Position { return .{ .x = if (condition) other.x - self.x else self.x, .y = if (condition) other.y - self.y else self.y, }; } pub fn invertX(self: Position, width: usize) Position { return .{ .x = width - self.x, .y = self.y, }; } pub fn invertY(self: Position, height: usize) Position { return .{ .x = self.x, .y = height - self.y, }; } pub fn invertXIf(self: Position, width: usize, condition: bool) Position { return .{ .x = if (condition) width - self.x else self.x, .y = self.y, }; } pub fn invertYIf(self: Position, height: usize, condition: bool) Position { return .{ .x = self.x, .y = if (condition) height - self.y else self.y, }; } pub fn resetXFrom(self: Position, other: Position) Position { return .{ .x = other.x, .y = self.y, }; } pub fn resetYFrom(self: Position, other: Position) Position { return .{ .x = self.x, .y = other.y, }; } pub fn resetXFromIf(self: Position, other: Position, condition: bool) Position { return .{ .x = if (condition) other.x else self.x, .y = self.y, }; } pub fn resetYFromIf(self: Position, other: Position, condition: bool) Position { return .{ .x = self.x, .y = if (condition) other.y else self.y, }; } ================================================ FILE: ly-ui/src/TerminalBuffer.zig ================================================ const std = @import("std"); const Allocator = std.mem.Allocator; const Random = std.Random; const ly_core = @import("ly-core"); const interop = ly_core.interop; const LogFile = ly_core.LogFile; const SharedError = ly_core.SharedError; pub const termbox = @import("termbox2"); const Cell = @import("Cell.zig"); const keyboard = @import("keyboard.zig"); const Position = @import("Position.zig"); const Widget = @import("Widget.zig"); const TerminalBuffer = @This(); pub const KeybindCallbackFn = *const fn (*anyopaque) anyerror!bool; pub const KeybindMap = std.AutoHashMap(keyboard.Key, struct { callback: KeybindCallbackFn, context: *anyopaque, }); pub const InitOptions = struct { fg: u32, bg: u32, border_fg: u32, full_color: bool, is_tty: bool, }; pub const Styling = struct { pub const BOLD = termbox.TB_BOLD; pub const UNDERLINE = termbox.TB_UNDERLINE; pub const REVERSE = termbox.TB_REVERSE; pub const ITALIC = termbox.TB_ITALIC; pub const BLINK = termbox.TB_BLINK; pub const HI_BLACK = termbox.TB_HI_BLACK; pub const BRIGHT = termbox.TB_BRIGHT; pub const DIM = termbox.TB_DIM; }; pub const Color = struct { pub const DEFAULT = 0x00000000; pub const TRUE_BLACK = Styling.HI_BLACK; pub const TRUE_RED = 0x00FF0000; pub const TRUE_GREEN = 0x0000FF00; pub const TRUE_YELLOW = 0x00FFFF00; pub const TRUE_BLUE = 0x000000FF; pub const TRUE_MAGENTA = 0x00FF00FF; pub const TRUE_CYAN = 0x0000FFFF; pub const TRUE_WHITE = 0x00FFFFFF; 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; pub const ECOL_YELLOW = 4; pub const ECOL_BLUE = 5; pub const ECOL_MAGENTA = 6; pub const ECOL_CYAN = 7; pub const ECOL_WHITE = 8; }; pub const START_POSITION = Position.init(0, 0); log_file: *LogFile, random: Random, width: usize, height: usize, fg: u32, bg: u32, border_fg: u32, box_chars: struct { left_up: u32, left_down: u32, right_up: u32, right_down: u32, top: u32, bottom: u32, left: u32, right: u32, }, blank_cell: Cell, full_color: bool, termios: ?std.posix.termios, keybinds: KeybindMap, handlable_widgets: std.ArrayList(*Widget), run: bool, update: bool, active_widget_index: usize, pub fn init( allocator: Allocator, options: InitOptions, log_file: *LogFile, random: Random, ) !TerminalBuffer { // Initialize termbox _ = termbox.tb_init(); if (options.full_color) { _ = termbox.tb_set_output_mode(termbox.TB_OUTPUT_TRUECOLOR); try log_file.info("tui", "termbox2 set to 24-bit color output mode", .{}); } else { try log_file.info("tui", "termbox2 set to eight-color output mode", .{}); } _ = termbox.tb_clear(); // Let's take some precautions here and clear the back buffer as well try clearBackBuffer(); const width: usize = @intCast(termbox.tb_width()); const height: usize = @intCast(termbox.tb_height()); try log_file.info("tui", "screen resolution is {d}x{d}", .{ width, height }); return .{ .log_file = log_file, .random = random, .width = width, .height = height, .fg = options.fg, .bg = options.bg, .border_fg = options.border_fg, .box_chars = if (interop.supportsUnicode()) .{ .left_up = 0x250C, .left_down = 0x2514, .right_up = 0x2510, .right_down = 0x2518, .top = 0x2500, .bottom = 0x2500, .left = 0x2502, .right = 0x2502, } else .{ .left_up = '+', .left_down = '+', .right_up = '+', .right_down = '+', .top = '-', .bottom = '-', .left = '|', .right = '|', }, .blank_cell = Cell.init(' ', options.fg, options.bg), .full_color = options.full_color, // Needed to reclaim the TTY after giving up its control .termios = try std.posix.tcgetattr(std.posix.STDIN_FILENO), .keybinds = KeybindMap.init(allocator), .handlable_widgets = .empty, .run = true, .update = true, .active_widget_index = 0, }; } pub fn deinit(self: *TerminalBuffer) void { self.keybinds.deinit(); TerminalBuffer.shutdown(); } pub fn runEventLoop( self: *TerminalBuffer, allocator: Allocator, shared_error: SharedError, layers: [][]Widget, active_widget: Widget, inactivity_delay: u16, position_widgets_fn: *const fn (*anyopaque) anyerror!void, inactivity_event_fn: ?*const fn (*anyopaque) anyerror!void, context: *anyopaque, ) !void { try self.registerGlobalKeybind("Ctrl+K", &moveCursorUp, self); try self.registerGlobalKeybind("Up", &moveCursorUp, self); try self.registerGlobalKeybind("Ctrl+J", &moveCursorDown, self); try self.registerGlobalKeybind("Down", &moveCursorDown, self); try self.registerGlobalKeybind("Tab", &wrapCursor, self); try self.registerGlobalKeybind("Shift+Tab", &wrapCursorReverse, self); defer self.handlable_widgets.deinit(allocator); var i: usize = 0; for (layers) |layer| { for (layer) |*widget| { try widget.update(context); if (widget.vtable.handle_fn != null) { try self.handlable_widgets.append(allocator, widget); if (widget.id == active_widget.id) self.active_widget_index = i; i += 1; } } } try @call(.auto, position_widgets_fn, .{context}); var event: termbox.tb_event = undefined; var inactivity_cmd_ran = false; var inactivity_time_start = try interop.getTimeOfDay(); while (self.run) { if (self.update) { for (layers) |layer| { for (layer) |*widget| { try widget.update(context); } } // Reset cursor const current_widget = self.getActiveWidget(); current_widget.handle(null) catch |err| { shared_error.writeError(error.SetCursorFailed); try self.log_file.err( "tui", "failed to set cursor in active widget '{s}': {s}", .{ current_widget.display_name, @errorName(err) }, ); }; try TerminalBuffer.clearScreen(false); for (layers) |layer| { for (layer) |*widget| { widget.draw(); } } TerminalBuffer.presentBuffer(); } var maybe_timeout: ?usize = null; for (layers) |layer| { for (layer) |*widget| { if (try widget.calculateTimeout(context)) |widget_timeout| { if (maybe_timeout == null or widget_timeout < maybe_timeout.?) maybe_timeout = widget_timeout; } } } if (inactivity_event_fn) |inactivity_fn| { const time = try interop.getTimeOfDay(); if (!inactivity_cmd_ran and time.seconds - inactivity_time_start.seconds > inactivity_delay) { try @call(.auto, inactivity_fn, .{context}); inactivity_cmd_ran = true; } } const event_error = if (maybe_timeout) |timeout| termbox.tb_peek_event(&event, @intCast(timeout)) else termbox.tb_poll_event(&event); self.update = maybe_timeout != null; if (event_error < 0) continue; // Input of some kind was detected, so reset the inactivity timer inactivity_time_start = try interop.getTimeOfDay(); if (event.type == termbox.TB_EVENT_RESIZE) { self.width = TerminalBuffer.getWidth(); self.height = TerminalBuffer.getHeight(); try self.log_file.info( "tui", "screen resolution updated to {d}x{d}", .{ self.width, self.height }, ); for (layers) |layer| { for (layer) |*widget| { widget.realloc() catch |err| { shared_error.writeError(error.WidgetReallocationFailed); try self.log_file.err( "tui", "failed to reallocate widget '{s}': {s}", .{ widget.display_name, @errorName(err) }, ); }; } } try @call(.auto, position_widgets_fn, .{context}); self.update = true; continue; } var maybe_keys = try self.handleKeybind(allocator, event); if (maybe_keys) |*keys| { defer keys.deinit(allocator); const current_widget = self.getActiveWidget(); for (keys.items) |key| { current_widget.handle(key) catch |err| { shared_error.writeError(error.CurrentWidgetHandlingFailed); try self.log_file.err( "tui", "failed to handle active widget '{s}': {s}", .{ current_widget.display_name, @errorName(err) }, ); }; } self.update = true; } } } pub fn stopEventLoop(self: *TerminalBuffer) void { self.run = false; } pub fn drawNextFrame(self: *TerminalBuffer, value: bool) void { self.update = value; } pub fn getActiveWidget(self: *TerminalBuffer) *Widget { return self.handlable_widgets.items[self.active_widget_index]; } pub fn setActiveWidget(self: *TerminalBuffer, widget: Widget) void { for (self.handlable_widgets.items, 0..) |widg, i| { if (widg.id == widget.id) self.active_widget_index = i; } } pub fn getWidth() usize { return @intCast(termbox.tb_width()); } pub fn getHeight() usize { return @intCast(termbox.tb_height()); } pub fn setCursor(x: usize, y: usize) void { _ = termbox.tb_set_cursor(@intCast(x), @intCast(y)); } pub fn clearScreen(clear_back_buffer: bool) !void { _ = termbox.tb_clear(); if (clear_back_buffer) try clearBackBuffer(); } pub fn shutdown() void { _ = termbox.tb_shutdown(); } pub fn presentBuffer() void { _ = termbox.tb_present(); } pub fn getCell(x: usize, y: usize) ?Cell { var maybe_cell: ?*termbox.tb_cell = undefined; _ = termbox.tb_get_cell( @intCast(x), @intCast(y), 1, &maybe_cell, ); if (maybe_cell) |cell| { return Cell.init(cell.ch, cell.fg, cell.bg); } return null; } pub fn setCell(x: usize, y: usize, cell: Cell) void { _ = termbox.tb_set_cell( @intCast(x), @intCast(y), cell.ch, cell.fg, cell.bg, ); } pub fn reclaim(self: TerminalBuffer) !void { if (self.termios) |termios| { // Take back control of the TTY _ = termbox.tb_init(); if (self.full_color) { _ = termbox.tb_set_output_mode(termbox.TB_OUTPUT_TRUECOLOR); } try std.posix.tcsetattr(std.posix.STDIN_FILENO, .FLUSH, termios); } } pub fn registerKeybind( self: *TerminalBuffer, keybinds: *KeybindMap, keybind: []const u8, callback: KeybindCallbackFn, context: *anyopaque, ) !void { const key = try self.parseKeybind(keybind); keybinds.put(key, .{ .callback = callback, .context = context, }) catch |err| { try self.log_file.err( "tui", "failed to register keybind {s}: {s}", .{ keybind, @errorName(err) }, ); }; } pub fn registerGlobalKeybind( self: *TerminalBuffer, keybind: []const u8, callback: KeybindCallbackFn, context: *anyopaque, ) !void { try self.registerKeybind(&self.keybinds, keybind, callback, context); } pub fn simulateKeybind(self: *TerminalBuffer, keybind: []const u8) !bool { const key = try self.parseKeybind(keybind); if (self.keybinds.get(key)) |binding| { return try @call( .auto, binding.callback, .{binding.context}, ); } const current_widget = self.getActiveWidget(); if (current_widget.keybinds) |keybinds| { if (keybinds.get(key)) |binding| { return try @call( .auto, binding.callback, .{binding.context}, ); } } return true; } pub fn drawText( text: []const u8, x: usize, y: usize, fg: u32, bg: u32, ) void { const yc: c_int = @intCast(y); const utf8view = std.unicode.Utf8View.init(text) catch return; var utf8 = utf8view.iterator(); var i: c_int = @intCast(x); while (utf8.nextCodepoint()) |codepoint| : (i += termbox.tb_wcwidth(codepoint)) { _ = termbox.tb_set_cell(i, yc, codepoint, fg, bg); } } pub fn drawConfinedText( text: []const u8, x: usize, y: usize, max_length: usize, fg: u32, bg: u32, ) void { const yc: c_int = @intCast(y); const utf8view = std.unicode.Utf8View.init(text) catch return; var utf8 = utf8view.iterator(); var i: c_int = @intCast(x); while (utf8.nextCodepoint()) |codepoint| : (i += termbox.tb_wcwidth(codepoint)) { if (i - @as(c_int, @intCast(x)) >= max_length) break; _ = termbox.tb_set_cell(i, yc, codepoint, fg, bg); } } pub fn drawCharMultiple( char: u32, x: usize, y: usize, length: usize, fg: u32, bg: u32, ) void { const cell = Cell.init(char, fg, bg); for (0..length) |xx| cell.put(x + xx, y); } // Every codepoint is assumed to have a width of 1. // Since Ly is normally running in a TTY, this should be fine. pub fn strWidth(str: []const u8) usize { const utf8view = std.unicode.Utf8View.init(str) catch return str.len; var utf8 = utf8view.iterator(); var length: c_int = 0; while (utf8.nextCodepoint()) |codepoint| { length += termbox.tb_wcwidth(codepoint); } return @intCast(length); } fn clearBackBuffer() !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); _ = try std.posix.write(termbox.global.ttyfd, capability_slice); } fn parseKeybind(self: *TerminalBuffer, keybind: []const u8) !keyboard.Key { var key = std.mem.zeroes(keyboard.Key); var iterator = std.mem.splitScalar(u8, keybind, '+'); while (iterator.next()) |item| { var found = false; inline for (std.meta.fields(keyboard.Key)) |field| { if (std.ascii.eqlIgnoreCase(field.name, item)) { @field(key, field.name) = true; found = true; break; } } if (!found) { try self.log_file.err( "tui", "failed to parse key {s} of keybind {s}", .{ item, keybind }, ); } } return key; } fn handleKeybind( self: *TerminalBuffer, allocator: Allocator, tb_event: termbox.tb_event, ) !?std.ArrayList(keyboard.Key) { var keys = try keyboard.getKeyList(allocator, tb_event); for (keys.items) |key| { if (self.keybinds.get(key)) |binding| { const passthrough_event = try @call( .auto, binding.callback, .{binding.context}, ); if (!passthrough_event) { keys.deinit(allocator); return null; } return keys; } const current_widget = self.getActiveWidget(); if (current_widget.keybinds) |keybinds| { if (keybinds.get(key)) |binding| { const passthrough_event = try @call( .auto, binding.callback, .{binding.context}, ); if (!passthrough_event) { keys.deinit(allocator); return null; } return keys; } } } return keys; } fn moveCursorUp(ptr: *anyopaque) !bool { var state: *TerminalBuffer = @ptrCast(@alignCast(ptr)); if (state.active_widget_index == 0) return false; state.active_widget_index -= 1; state.update = true; return false; } fn moveCursorDown(ptr: *anyopaque) !bool { var state: *TerminalBuffer = @ptrCast(@alignCast(ptr)); if (state.active_widget_index == state.handlable_widgets.items.len - 1) return false; state.active_widget_index += 1; state.update = true; return false; } fn wrapCursor(ptr: *anyopaque) !bool { var state: *TerminalBuffer = @ptrCast(@alignCast(ptr)); state.active_widget_index = (state.active_widget_index + 1) % state.handlable_widgets.items.len; state.update = true; return false; } fn wrapCursorReverse(ptr: *anyopaque) !bool { var state: *TerminalBuffer = @ptrCast(@alignCast(ptr)); state.active_widget_index = if (state.active_widget_index == 0) state.handlable_widgets.items.len - 1 else state.active_widget_index - 1; state.update = true; return false; } ================================================ FILE: ly-ui/src/Widget.zig ================================================ const Widget = @This(); const keyboard = @import("keyboard.zig"); const TerminalBuffer = @import("TerminalBuffer.zig"); const VTable = struct { deinit_fn: ?*const fn (ptr: *anyopaque) void, realloc_fn: ?*const fn (ptr: *anyopaque) anyerror!void, draw_fn: *const fn (ptr: *anyopaque) void, update_fn: ?*const fn (ptr: *anyopaque, ctx: *anyopaque) anyerror!void, handle_fn: ?*const fn (ptr: *anyopaque, maybe_key: ?keyboard.Key) anyerror!void, calculate_timeout_fn: ?*const fn (ptr: *anyopaque, ctx: *anyopaque) anyerror!?usize, }; id: u64, display_name: []const u8, keybinds: ?TerminalBuffer.KeybindMap, pointer: *anyopaque, vtable: VTable, pub fn init( display_name: []const u8, keybinds: ?TerminalBuffer.KeybindMap, pointer: anytype, comptime deinit_fn: ?fn (ptr: @TypeOf(pointer)) void, comptime realloc_fn: ?fn (ptr: @TypeOf(pointer)) anyerror!void, comptime draw_fn: fn (ptr: @TypeOf(pointer)) void, comptime update_fn: ?fn (ptr: @TypeOf(pointer), ctx: *anyopaque) anyerror!void, comptime handle_fn: ?fn (ptr: @TypeOf(pointer), maybe_key: ?keyboard.Key) anyerror!void, comptime calculate_timeout_fn: ?fn (ptr: @TypeOf(pointer), ctx: *anyopaque) anyerror!?usize, ) Widget { const Pointer = @TypeOf(pointer); const Impl = struct { pub fn deinitImpl(ptr: *anyopaque) void { const impl: Pointer = @ptrCast(@alignCast(ptr)); return @call( .always_inline, deinit_fn.?, .{impl}, ); } pub fn reallocImpl(ptr: *anyopaque) !void { const impl: Pointer = @ptrCast(@alignCast(ptr)); return @call( .always_inline, realloc_fn.?, .{impl}, ); } pub fn drawImpl(ptr: *anyopaque) void { const impl: Pointer = @ptrCast(@alignCast(ptr)); return @call( .always_inline, draw_fn, .{impl}, ); } pub fn updateImpl(ptr: *anyopaque, ctx: *anyopaque) !void { const impl: Pointer = @ptrCast(@alignCast(ptr)); return @call( .always_inline, update_fn.?, .{ impl, ctx }, ); } pub fn handleImpl(ptr: *anyopaque, maybe_key: ?keyboard.Key) !void { const impl: Pointer = @ptrCast(@alignCast(ptr)); return @call( .always_inline, handle_fn.?, .{ impl, maybe_key }, ); } pub fn calculateTimeoutImpl(ptr: *anyopaque, ctx: *anyopaque) !?usize { const impl: Pointer = @ptrCast(@alignCast(ptr)); return @call( .always_inline, calculate_timeout_fn.?, .{ impl, ctx }, ); } const vtable = VTable{ .deinit_fn = if (deinit_fn != null) deinitImpl else null, .realloc_fn = if (realloc_fn != null) reallocImpl else null, .draw_fn = drawImpl, .update_fn = if (update_fn != null) updateImpl else null, .handle_fn = if (handle_fn != null) handleImpl else null, .calculate_timeout_fn = if (calculate_timeout_fn != null) calculateTimeoutImpl else null, }; }; return .{ .id = @intFromPtr(Impl.vtable.draw_fn), .display_name = display_name, .keybinds = keybinds, .pointer = pointer, .vtable = Impl.vtable, }; } pub fn deinit(self: *Widget) void { const impl: @TypeOf(self.pointer) = @ptrCast(@alignCast(self.pointer)); if (self.vtable.deinit_fn) |deinit_fn| { return @call( .auto, deinit_fn, .{impl}, ); } } pub fn realloc(self: *Widget) !void { const impl: @TypeOf(self.pointer) = @ptrCast(@alignCast(self.pointer)); if (self.vtable.realloc_fn) |realloc_fn| { return @call( .auto, realloc_fn, .{impl}, ); } } pub fn draw(self: *Widget) void { const impl: @TypeOf(self.pointer) = @ptrCast(@alignCast(self.pointer)); @call( .auto, self.vtable.draw_fn, .{impl}, ); } pub fn update(self: *Widget, ctx: *anyopaque) !void { const impl: @TypeOf(self.pointer) = @ptrCast(@alignCast(self.pointer)); if (self.vtable.update_fn) |update_fn| { return @call( .auto, update_fn, .{ impl, ctx }, ); } } pub fn handle(self: *Widget, maybe_key: ?keyboard.Key) !void { const impl: @TypeOf(self.pointer) = @ptrCast(@alignCast(self.pointer)); if (self.vtable.handle_fn) |handle_fn| { return @call( .auto, handle_fn, .{ impl, maybe_key }, ); } } pub fn calculateTimeout(self: *Widget, ctx: *anyopaque) !?usize { const impl: @TypeOf(self.pointer) = @ptrCast(@alignCast(self.pointer)); if (self.vtable.calculate_timeout_fn) |calculate_timeout_fn| { return @call( .auto, calculate_timeout_fn, .{ impl, ctx }, ); } return null; } ================================================ FILE: ly-ui/src/components/BigLabel.zig ================================================ const BigLabel = @This(); const std = @import("std"); const Allocator = std.mem.Allocator; const ly_core = @import("ly-core"); const interop = ly_core.interop; const en = @import("bigLabelLocales/en.zig"); const fa = @import("bigLabelLocales/fa.zig"); const Cell = @import("../Cell.zig"); const Position = @import("../Position.zig"); const TerminalBuffer = @import("../TerminalBuffer.zig"); const Widget = @import("../Widget.zig"); pub const CHAR_WIDTH = 5; pub const CHAR_HEIGHT = 5; pub const CHAR_SIZE = CHAR_WIDTH * CHAR_HEIGHT; pub const X: u32 = if (ly_core.interop.supportsUnicode()) 0x2593 else '#'; pub const O: u32 = 0; // zig fmt: off pub const LocaleChars = struct { ZERO: [CHAR_SIZE]u21, ONE: [CHAR_SIZE]u21, TWO: [CHAR_SIZE]u21, THREE: [CHAR_SIZE]u21, FOUR: [CHAR_SIZE]u21, FIVE: [CHAR_SIZE]u21, SIX: [CHAR_SIZE]u21, SEVEN: [CHAR_SIZE]u21, EIGHT: [CHAR_SIZE]u21, NINE: [CHAR_SIZE]u21, S: [CHAR_SIZE]u21, E: [CHAR_SIZE]u21, P: [CHAR_SIZE]u21, A: [CHAR_SIZE]u21, M: [CHAR_SIZE]u21, }; // zig fmt: on pub const BigLabelLocale = enum { en, fa, }; allocator: ?Allocator = null, buffer: *TerminalBuffer, text: []const u8, max_width: ?usize, fg: u32, bg: u32, locale: BigLabelLocale, update_fn: ?*const fn (*BigLabel, *anyopaque) anyerror!void, calculate_timeout_fn: ?*const fn (*BigLabel, *anyopaque) anyerror!?usize, component_pos: Position, children_pos: Position, pub fn init( buffer: *TerminalBuffer, text: []const u8, max_width: ?usize, fg: u32, bg: u32, locale: BigLabelLocale, update_fn: ?*const fn (*BigLabel, *anyopaque) anyerror!void, calculate_timeout_fn: ?*const fn (*BigLabel, *anyopaque) anyerror!?usize, ) BigLabel { return .{ .allocator = null, .buffer = buffer, .text = text, .max_width = max_width, .fg = fg, .bg = bg, .locale = locale, .update_fn = update_fn, .calculate_timeout_fn = calculate_timeout_fn, .component_pos = TerminalBuffer.START_POSITION, .children_pos = TerminalBuffer.START_POSITION, }; } pub fn deinit(self: *BigLabel) void { if (self.allocator) |allocator| allocator.free(self.text); } pub fn widget(self: *BigLabel) Widget { return Widget.init( "BigLabel", null, self, deinit, null, draw, update, null, calculateTimeout, ); } pub fn setTextAlloc( self: *BigLabel, allocator: Allocator, comptime fmt: []const u8, args: anytype, ) !void { self.text = try std.fmt.allocPrint(allocator, fmt, args); self.allocator = allocator; } pub fn setTextBuf( self: *BigLabel, buffer: []u8, comptime fmt: []const u8, args: anytype, ) !void { self.text = try std.fmt.bufPrint(buffer, fmt, args); self.allocator = null; } pub fn setText(self: *BigLabel, text: []const u8) void { self.text = text; self.allocator = null; } pub fn positionX(self: *BigLabel, original_pos: Position) void { self.component_pos = original_pos; self.children_pos = original_pos.addX(TerminalBuffer.strWidth(self.text) * CHAR_WIDTH); } pub fn positionY(self: *BigLabel, original_pos: Position) void { self.component_pos = original_pos; self.children_pos = original_pos.addY(CHAR_HEIGHT); } pub fn positionXY(self: *BigLabel, original_pos: Position) void { self.component_pos = original_pos; self.children_pos = Position.init( TerminalBuffer.strWidth(self.text) * CHAR_WIDTH, CHAR_HEIGHT, ).add(original_pos); } pub fn childrenPosition(self: BigLabel) Position { return self.children_pos; } fn draw(self: *BigLabel) void { for (self.text, 0..) |c, i| { const clock_cell = clockCell( c, self.fg, self.bg, self.locale, ); alphaBlit( self.component_pos.x + i * (CHAR_WIDTH + 1), self.component_pos.y, self.buffer.width, self.buffer.height, clock_cell, ); } } fn update(self: *BigLabel, context: *anyopaque) !void { if (self.update_fn) |update_fn| { return @call( .auto, update_fn, .{ self, context }, ); } } fn calculateTimeout(self: *BigLabel, ctx: *anyopaque) !?usize { if (self.calculate_timeout_fn) |calculate_timeout_fn| { return @call( .auto, calculate_timeout_fn, .{ self, ctx }, ); } return null; } fn clockCell(char: u8, fg: u32, bg: u32, locale: BigLabelLocale) [CHAR_SIZE]Cell { var cells: [CHAR_SIZE]Cell = undefined; //@divTrunc(time.microseconds, 500000) != 0) const clock_chars = toBigNumber(char, locale); for (0..cells.len) |i| cells[i] = Cell.init(clock_chars[i], fg, bg); return cells; } fn alphaBlit(x: usize, y: usize, tb_width: usize, tb_height: usize, cells: [CHAR_SIZE]Cell) void { if (x + CHAR_WIDTH >= tb_width or y + CHAR_HEIGHT >= tb_height) return; for (0..CHAR_HEIGHT) |yy| { for (0..CHAR_WIDTH) |xx| { const cell = cells[yy * CHAR_WIDTH + xx]; cell.put(x + xx, y + yy); } } } fn toBigNumber(char: u8, locale: BigLabelLocale) [CHAR_SIZE]u21 { const locale_chars = switch (locale) { .fa => fa.locale_chars, .en => en.locale_chars, }; return switch (char) { '0' => locale_chars.ZERO, '1' => locale_chars.ONE, '2' => locale_chars.TWO, '3' => locale_chars.THREE, '4' => locale_chars.FOUR, '5' => locale_chars.FIVE, '6' => locale_chars.SIX, '7' => locale_chars.SEVEN, '8' => locale_chars.EIGHT, '9' => locale_chars.NINE, 'p', 'P' => locale_chars.P, 'a', 'A' => locale_chars.A, 'm', 'M' => locale_chars.M, ':' => locale_chars.S, else => locale_chars.E, }; } ================================================ FILE: ly-ui/src/components/CenteredBox.zig ================================================ const std = @import("std"); const Cell = @import("../Cell.zig"); const Position = @import("../Position.zig"); const TerminalBuffer = @import("../TerminalBuffer.zig"); const Widget = @import("../Widget.zig"); const CenteredBox = @This(); buffer: *TerminalBuffer, horizontal_margin: usize, vertical_margin: usize, width: usize, height: usize, show_borders: bool, blank_box: bool, top_title: ?[]const u8, bottom_title: ?[]const u8, border_fg: u32, title_fg: u32, bg: u32, update_fn: ?*const fn (*CenteredBox, *anyopaque) anyerror!void, left_pos: Position, right_pos: Position, children_pos: Position, pub fn init( buffer: *TerminalBuffer, horizontal_margin: usize, vertical_margin: usize, width: usize, height: usize, show_borders: bool, blank_box: bool, top_title: ?[]const u8, bottom_title: ?[]const u8, border_fg: u32, title_fg: u32, bg: u32, update_fn: ?*const fn (*CenteredBox, *anyopaque) anyerror!void, ) CenteredBox { return .{ .buffer = buffer, .horizontal_margin = horizontal_margin, .vertical_margin = vertical_margin, .width = width, .height = height, .show_borders = show_borders, .blank_box = blank_box, .top_title = top_title, .bottom_title = bottom_title, .border_fg = border_fg, .title_fg = title_fg, .bg = bg, .update_fn = update_fn, .left_pos = TerminalBuffer.START_POSITION, .right_pos = TerminalBuffer.START_POSITION, .children_pos = TerminalBuffer.START_POSITION, }; } pub fn widget(self: *CenteredBox) Widget { return Widget.init( "CenteredBox", null, self, null, null, draw, update, null, null, ); } pub fn positionXY(self: *CenteredBox, original_pos: Position) void { if (self.buffer.width < 2 or self.buffer.height < 2) return; self.left_pos = Position.init( (self.buffer.width - @min(self.buffer.width - 2, self.width)) / 2, (self.buffer.height - @min(self.buffer.height - 2, self.height)) / 2, ).add(original_pos); self.right_pos = Position.init( (self.buffer.width + @min(self.buffer.width, self.width)) / 2, (self.buffer.height + @min(self.buffer.height, self.height)) / 2, ).add(original_pos); self.children_pos = Position.init( self.left_pos.x + self.horizontal_margin, self.left_pos.y + self.vertical_margin, ).add(original_pos); } pub fn childrenPosition(self: CenteredBox) Position { return self.children_pos; } fn draw(self: *CenteredBox) void { if (self.show_borders) { var left_up = Cell.init( self.buffer.box_chars.left_up, self.border_fg, self.bg, ); var right_up = Cell.init( self.buffer.box_chars.right_up, self.border_fg, self.bg, ); var left_down = Cell.init( self.buffer.box_chars.left_down, self.border_fg, self.bg, ); var right_down = Cell.init( self.buffer.box_chars.right_down, self.border_fg, self.bg, ); var top = Cell.init( self.buffer.box_chars.top, self.border_fg, self.bg, ); var bottom = Cell.init( self.buffer.box_chars.bottom, self.border_fg, self.bg, ); left_up.put(self.left_pos.x - 1, self.left_pos.y - 1); right_up.put(self.right_pos.x, self.left_pos.y - 1); left_down.put(self.left_pos.x - 1, self.right_pos.y); right_down.put(self.right_pos.x, self.right_pos.y); for (0..self.width) |i| { top.put(self.left_pos.x + i, self.left_pos.y - 1); bottom.put(self.left_pos.x + i, self.right_pos.y); } top.ch = self.buffer.box_chars.left; bottom.ch = self.buffer.box_chars.right; for (0..self.height) |i| { top.put(self.left_pos.x - 1, self.left_pos.y + i); bottom.put(self.right_pos.x, self.left_pos.y + i); } } if (self.blank_box) { for (0..self.height) |y| { for (0..self.width) |x| { self.buffer.blank_cell.put(self.left_pos.x + x, self.left_pos.y + y); } } } if (self.top_title) |title| { TerminalBuffer.drawConfinedText( title, self.left_pos.x, self.left_pos.y - 1, self.width, self.title_fg, self.bg, ); } if (self.bottom_title) |title| { TerminalBuffer.drawConfinedText( title, self.left_pos.x, self.left_pos.y + self.height, self.width, self.title_fg, self.bg, ); } } fn update(self: *CenteredBox, ctx: *anyopaque) !void { if (self.update_fn) |update_fn| { return @call( .auto, update_fn, .{ self, ctx }, ); } } ================================================ FILE: ly-ui/src/components/Label.zig ================================================ const Label = @This(); const std = @import("std"); const Allocator = std.mem.Allocator; const Cell = @import("../Cell.zig"); const Position = @import("../Position.zig"); const TerminalBuffer = @import("../TerminalBuffer.zig"); const Widget = @import("../Widget.zig"); allocator: ?Allocator, text: []const u8, max_width: ?usize, fg: u32, bg: u32, update_fn: ?*const fn (*Label, *anyopaque) anyerror!void, calculate_timeout_fn: ?*const fn (*Label, *anyopaque) anyerror!?usize, component_pos: Position, children_pos: Position, pub fn init( text: []const u8, max_width: ?usize, fg: u32, bg: u32, update_fn: ?*const fn (*Label, *anyopaque) anyerror!void, calculate_timeout_fn: ?*const fn (*Label, *anyopaque) anyerror!?usize, ) Label { return .{ .allocator = null, .text = text, .max_width = max_width, .fg = fg, .bg = bg, .update_fn = update_fn, .calculate_timeout_fn = calculate_timeout_fn, .component_pos = TerminalBuffer.START_POSITION, .children_pos = TerminalBuffer.START_POSITION, }; } pub fn deinit(self: *Label) void { if (self.allocator) |allocator| allocator.free(self.text); } pub fn widget(self: *Label) Widget { return Widget.init( "Label", null, self, deinit, null, draw, update, null, calculateTimeout, ); } pub fn setTextAlloc( self: *Label, allocator: Allocator, comptime fmt: []const u8, args: anytype, ) !void { self.text = try std.fmt.allocPrint(allocator, fmt, args); self.allocator = allocator; } pub fn setTextBuf( self: *Label, buffer: []u8, comptime fmt: []const u8, args: anytype, ) !void { self.text = try std.fmt.bufPrint(buffer, fmt, args); self.allocator = null; } pub fn setText(self: *Label, text: []const u8) void { self.text = text; self.allocator = null; } pub fn positionX(self: *Label, original_pos: Position) void { self.component_pos = original_pos; self.children_pos = original_pos.addX(TerminalBuffer.strWidth(self.text)); } pub fn positionY(self: *Label, original_pos: Position) void { self.component_pos = original_pos; self.children_pos = original_pos.addY(1); } pub fn positionXY(self: *Label, original_pos: Position) void { self.component_pos = original_pos; self.children_pos = Position.init( TerminalBuffer.strWidth(self.text), 1, ).add(original_pos); } pub fn childrenPosition(self: Label) Position { return self.children_pos; } fn draw(self: *Label) void { if (self.max_width) |width| { TerminalBuffer.drawConfinedText( self.text, self.component_pos.x, self.component_pos.y, width, self.fg, self.bg, ); return; } TerminalBuffer.drawText( self.text, self.component_pos.x, self.component_pos.y, self.fg, self.bg, ); } fn update(self: *Label, ctx: *anyopaque) !void { if (self.update_fn) |update_fn| { return @call( .auto, update_fn, .{ self, ctx }, ); } } fn calculateTimeout(self: *Label, ctx: *anyopaque) !?usize { if (self.calculate_timeout_fn) |calculate_timeout_fn| { return @call( .auto, calculate_timeout_fn, .{ self, ctx }, ); } return null; } ================================================ FILE: ly-ui/src/components/Text.zig ================================================ const std = @import("std"); const Allocator = std.mem.Allocator; const keyboard = @import("../keyboard.zig"); const TerminalBuffer = @import("../TerminalBuffer.zig"); const Position = @import("../Position.zig"); const Widget = @import("../Widget.zig"); const DynamicString = std.ArrayListUnmanaged(u8); const Text = @This(); allocator: Allocator, buffer: *TerminalBuffer, text: DynamicString, end: usize, cursor: usize, visible_start: usize, width: usize, component_pos: Position, children_pos: Position, should_insert: bool, masked: bool, maybe_mask: ?u32, fg: u32, bg: u32, keybinds: TerminalBuffer.KeybindMap, pub fn init( allocator: Allocator, buffer: *TerminalBuffer, should_insert: bool, masked: bool, maybe_mask: ?u32, width: usize, fg: u32, bg: u32, ) !*Text { var self = try allocator.create(Text); self.* = Text{ .allocator = allocator, .buffer = buffer, .text = .empty, .end = 0, .cursor = 0, .visible_start = 0, .width = width, .component_pos = TerminalBuffer.START_POSITION, .children_pos = TerminalBuffer.START_POSITION, .should_insert = should_insert, .masked = masked, .maybe_mask = maybe_mask, .fg = fg, .bg = bg, .keybinds = .init(allocator), }; try buffer.registerKeybind(&self.keybinds, "Left", &goLeft, self); try buffer.registerKeybind(&self.keybinds, "Right", &goRight, self); try buffer.registerKeybind(&self.keybinds, "Delete", &delete, self); try buffer.registerKeybind(&self.keybinds, "Backspace", &backspace, self); try buffer.registerKeybind(&self.keybinds, "Ctrl+U", &clearTextEntry, self); return self; } pub fn deinit(self: *Text) void { self.text.deinit(self.allocator); self.keybinds.deinit(); self.allocator.destroy(self); } pub fn widget(self: *Text) Widget { return Widget.init( "Text", self.keybinds, self, deinit, null, draw, null, handle, null, ); } pub fn positionX(self: *Text, original_pos: Position) void { self.component_pos = original_pos; self.children_pos = original_pos.addX(self.width); } pub fn positionY(self: *Text, original_pos: Position) void { self.component_pos = original_pos; self.children_pos = original_pos.addY(1); } pub fn positionXY(self: *Text, original_pos: Position) void { self.component_pos = original_pos; self.children_pos = Position.init( self.width, 1, ).add(original_pos); } pub fn childrenPosition(self: Text) Position { return self.children_pos; } pub fn clear(self: *Text) void { self.text.clearRetainingCapacity(); self.end = 0; self.cursor = 0; self.visible_start = 0; } pub fn toggleMask(self: *Text) void { self.masked = !self.masked; } pub fn handle(self: *Text, maybe_key: ?keyboard.Key) !void { if (maybe_key) |key| { if (self.should_insert) { const maybe_character = key.getEnabledPrintableAscii(); if (maybe_character) |character| try self.write(character); } } if (self.masked and self.maybe_mask == null) { TerminalBuffer.setCursor( self.component_pos.x, self.component_pos.y, ); return; } TerminalBuffer.setCursor( self.component_pos.x + (self.cursor - self.visible_start), self.component_pos.y, ); } fn draw(self: *Text) void { if (self.masked) { if (self.maybe_mask) |mask| { if (self.width < 1) return; const length = @min(TerminalBuffer.strWidth(self.text.items), self.width - 1); if (length == 0) return; TerminalBuffer.drawCharMultiple( mask, self.component_pos.x, self.component_pos.y, length, self.fg, self.bg, ); } return; } const str_length = TerminalBuffer.strWidth(self.text.items); const length = @min(str_length, self.width); if (length == 0) return; const visible_slice = vs: { if (str_length > self.width and self.cursor < str_length) { break :vs self.text.items[self.visible_start..(self.width + self.visible_start)]; } else { break :vs self.text.items[self.visible_start..]; } }; TerminalBuffer.drawText( visible_slice, self.component_pos.x, self.component_pos.y, self.fg, self.bg, ); } fn goLeft(ptr: *anyopaque) !bool { var self: *Text = @ptrCast(@alignCast(ptr)); if (self.cursor == 0) return false; if (self.visible_start > 0) self.visible_start -= 1; self.cursor -= 1; return false; } fn goRight(ptr: *anyopaque) !bool { var self: *Text = @ptrCast(@alignCast(ptr)); if (self.cursor >= self.end) return false; if (self.cursor - self.visible_start == self.width - 1) self.visible_start += 1; self.cursor += 1; return false; } fn delete(ptr: *anyopaque) !bool { var self: *Text = @ptrCast(@alignCast(ptr)); if (self.cursor >= self.end or !self.should_insert) return false; _ = self.text.orderedRemove(self.cursor); self.end -= 1; return false; } fn backspace(ptr: *anyopaque) !bool { const self: *Text = @ptrCast(@alignCast(ptr)); if (self.cursor == 0 or !self.should_insert) return false; _ = try goLeft(ptr); _ = try delete(ptr); return false; } fn write(self: *Text, char: u8) !void { if (char == 0) return; try self.text.insert(self.allocator, self.cursor, char); self.end += 1; _ = try goRight(self); } fn clearTextEntry(ptr: *anyopaque) !bool { var self: *Text = @ptrCast(@alignCast(ptr)); if (!self.should_insert) return false; self.clear(); self.buffer.drawNextFrame(true); return false; } ================================================ FILE: ly-ui/src/components/bigLabelLocales/en.zig ================================================ const BigLabel = @import("../BigLabel.zig"); const LocaleChars = BigLabel.LocaleChars; const X = BigLabel.X; const O = BigLabel.O; // zig fmt: off pub const locale_chars = LocaleChars{ .ZERO = [_]u21{ X,X,X,X,X, X,X,O,X,X, X,X,O,X,X, X,X,O,X,X, X,X,X,X,X, }, .ONE = [_]u21{ O,O,O,X,X, O,O,O,X,X, O,O,O,X,X, O,O,O,X,X, O,O,O,X,X, }, .TWO = [_]u21{ X,X,X,X,X, O,O,O,X,X, X,X,X,X,X, X,X,O,O,O, X,X,X,X,X, }, .THREE = [_]u21{ X,X,X,X,X, O,O,O,X,X, X,X,X,X,X, O,O,O,X,X, X,X,X,X,X, }, .FOUR = [_]u21{ X,X,O,X,X, X,X,O,X,X, X,X,X,X,X, O,O,O,X,X, O,O,O,X,X, }, .FIVE = [_]u21{ X,X,X,X,X, X,X,O,O,O, X,X,X,X,X, O,O,O,X,X, X,X,X,X,X, }, .SIX = [_]u21{ X,X,X,X,X, X,X,O,O,O, X,X,X,X,X, X,X,O,X,X, X,X,X,X,X, }, .SEVEN = [_]u21{ X,X,X,X,X, O,O,O,X,X, O,O,O,X,X, O,O,O,X,X, O,O,O,X,X, }, .EIGHT = [_]u21{ X,X,X,X,X, X,X,O,X,X, X,X,X,X,X, X,X,O,X,X, X,X,X,X,X, }, .NINE = [_]u21{ X,X,X,X,X, X,X,O,X,X, X,X,X,X,X, O,O,O,X,X, X,X,X,X,X, }, .S = [_]u21{ O,O,O,O,O, O,O,X,O,O, O,O,O,O,O, O,O,X,O,O, O,O,O,O,O, }, .E = [_]u21{ O,O,O,O,O, O,O,O,O,O, O,O,O,O,O, O,O,O,O,O, O,O,O,O,O, }, .P = [_]u21{ X,X,X,X,X, X,X,O,X,X, X,X,X,X,X, X,X,O,O,O, X,X,O,O,O, }, .A = [_]u21{ X,X,X,X,X, X,X,O,X,X, X,X,X,X,X, X,X,O,X,X, X,X,O,X,X, }, .M = [_]u21{ X,X,X,X,X, X,O,X,O,X, X,O,X,O,X, X,O,O,O,X, X,O,O,O,X, }, }; // zig fmt: on ================================================ FILE: ly-ui/src/components/bigLabelLocales/fa.zig ================================================ const BigLabel = @import("../BigLabel.zig"); const LocaleChars = BigLabel.LocaleChars; const X = BigLabel.X; const O = BigLabel.O; // zig fmt: off pub const locale_chars = LocaleChars{ .ZERO = [_]u21{ O,O,O,O,O, O,O,X,O,O, O,X,O,X,O, O,O,X,O,O, O,O,O,O,O, }, .ONE = [_]u21{ O,O,X,O,O, O,X,X,O,O, O,O,X,O,O, O,O,X,O,O, O,O,X,O,O, }, .TWO = [_]u21{ O,X,O,X,O, O,X,X,X,O, O,X,O,O,O, O,X,O,O,O, O,X,O,O,O, }, .THREE = [_]u21{ X,O,X,O,X, X,X,X,X,X, X,O,O,O,O, X,O,O,O,O, X,O,O,O,O, }, .FOUR = [_]u21{ O,X,O,X,X, O,X,X,O,O, O,X,X,X,X, O,X,O,O,O, O,X,O,O,O, }, .FIVE = [_]u21{ O,O,X,X,O, O,X,O,O,X, X,O,O,O,X, X,O,X,O,X, O,X,O,X,O, }, .SIX = [_]u21{ O,X,X,O,O, O,X,O,O,X, O,O,X,O,O, O,X,O,O,O, X,O,O,O,O, }, .SEVEN = [_]u21{ X,O,O,O,X, X,O,O,O,X, O,X,O,X,O, O,X,O,X,O, O,O,X,O,O, }, .EIGHT = [_]u21{ O,O,O,X,O, O,O,X,O,X, O,O,X,O,X, O,X,O,O,X, O,X,O,O,X, }, .NINE = [_]u21{ O,X,X,X,O, O,X,O,X,O, O,X,X,X,O, O,O,O,X,O, O,O,O,X,O, }, .P = [_]u21{ O,O,O,O,O, O,O,O,O,O, O,O,O,O,O, O,O,O,O,O, O,O,O,O,O, }, .A = [_]u21{ O,O,O,O,O, O,O,O,O,O, O,O,O,O,O, O,O,O,O,O, O,O,O,O,O, }, .M = [_]u21{ O,O,O,O,O, O,O,O,O,O, O,O,O,O,O, O,O,O,O,O, O,O,O,O,O, }, .S = [_]u21{ O,O,O,O,O, O,O,X,O,O, O,O,O,O,O, O,O,X,O,O, O,O,O,O,O, }, .E = [_]u21{ O,O,O,O,O, O,O,O,O,O, O,O,O,O,O, O,O,O,O,O, O,O,O,O,O, }, }; // zig fmt: on ================================================ FILE: ly-ui/src/components/generic.zig ================================================ const std = @import("std"); const Cell = @import("../Cell.zig"); const keyboard = @import("../keyboard.zig"); const TerminalBuffer = @import("../TerminalBuffer.zig"); const Position = @import("../Position.zig"); pub fn CyclableLabel(comptime ItemType: type, comptime ChangeItemType: type) type { return struct { const Allocator = std.mem.Allocator; const ItemList = std.ArrayListUnmanaged(ItemType); const DrawItemFn = *const fn (*Self, ItemType, usize, usize, usize) void; const ChangeItemFn = *const fn (ItemType, ?ChangeItemType) void; const Self = @This(); allocator: Allocator, buffer: *TerminalBuffer, list: ItemList, current: usize, width: usize, component_pos: Position, children_pos: Position, text_in_center: bool, fg: u32, bg: u32, cursor: usize, draw_item_fn: DrawItemFn, change_item_fn: ?ChangeItemFn, change_item_arg: ?ChangeItemType, keybinds: TerminalBuffer.KeybindMap, pub fn init( allocator: Allocator, buffer: *TerminalBuffer, draw_item_fn: DrawItemFn, change_item_fn: ?ChangeItemFn, change_item_arg: ?ChangeItemType, width: usize, text_in_center: bool, fg: u32, bg: u32, ) !*Self { var self = try allocator.create(Self); self.* = .{ .allocator = allocator, .buffer = buffer, .list = .empty, .current = 0, .width = width, .component_pos = TerminalBuffer.START_POSITION, .children_pos = TerminalBuffer.START_POSITION, .text_in_center = text_in_center, .fg = fg, .bg = bg, .cursor = 0, .draw_item_fn = draw_item_fn, .change_item_fn = change_item_fn, .change_item_arg = change_item_arg, .keybinds = .init(allocator), }; try buffer.registerKeybind(&self.keybinds, "Left", &goLeft, self); try buffer.registerKeybind(&self.keybinds, "Ctrl+H", &goLeft, self); try buffer.registerKeybind(&self.keybinds, "Right", &goRight, self); try buffer.registerKeybind(&self.keybinds, "Ctrl+L", &goRight, self); return self; } pub fn deinit(self: *Self) void { self.list.deinit(self.allocator); self.keybinds.deinit(); self.allocator.destroy(self); } pub fn positionX(self: *Self, original_pos: Position) void { self.component_pos = original_pos; self.cursor = self.component_pos.x + 2; self.children_pos = original_pos.addX(self.width); } pub fn positionY(self: *Self, original_pos: Position) void { self.component_pos = original_pos; self.cursor = self.component_pos.x + 2; self.children_pos = original_pos.addY(1); } pub fn positionXY(self: *Self, original_pos: Position) void { self.component_pos = original_pos; self.cursor = self.component_pos.x + 2; self.children_pos = Position.init( self.width, 1, ).add(original_pos); } pub fn childrenPosition(self: Self) Position { return self.children_pos; } pub fn addItem(self: *Self, item: ItemType) !void { try self.list.append(self.allocator, item); self.current = self.list.items.len - 1; } pub fn handle(self: *Self, _: ?keyboard.Key) void { TerminalBuffer.setCursor( self.component_pos.x + self.cursor + 2, self.component_pos.y, ); } pub fn draw(self: *Self) void { if (self.list.items.len == 0) return; if (self.width < 2) return; var left_arrow = Cell.init('<', self.fg, self.bg); var right_arrow = Cell.init('>', self.fg, self.bg); left_arrow.put(self.component_pos.x, self.component_pos.y); right_arrow.put( self.component_pos.x + self.width - 1, self.component_pos.y, ); const current_item = self.list.items[self.current]; const x = self.component_pos.x + 2; const y = self.component_pos.y; const width = self.width - 2; @call( .auto, self.draw_item_fn, .{ self, current_item, x, y, width }, ); } fn goLeft(ptr: *anyopaque) !bool { var self: *Self = @ptrCast(@alignCast(ptr)); self.current = if (self.current == 0) self.list.items.len - 1 else self.current - 1; if (self.change_item_fn) |change_item_fn| { @call( .auto, change_item_fn, .{ self.list.items[self.current], self.change_item_arg }, ); } return false; } fn goRight(ptr: *anyopaque) !bool { var self: *Self = @ptrCast(@alignCast(ptr)); self.current = if (self.current == self.list.items.len - 1) 0 else self.current + 1; if (self.change_item_fn) |change_item_fn| { @call( .auto, change_item_fn, .{ self.list.items[self.current], self.change_item_arg }, ); } return false; } }; } ================================================ FILE: ly-ui/src/keyboard.zig ================================================ const std = @import("std"); const Allocator = std.mem.Allocator; const KeyList = std.ArrayList(Key); const TerminalBuffer = @import("TerminalBuffer.zig"); const termbox = TerminalBuffer.termbox; pub const Key = packed struct { ctrl: bool, shift: bool, alt: bool, f1: bool, f2: bool, f3: bool, f4: bool, f5: bool, f6: bool, f7: bool, f8: bool, f9: bool, f10: bool, f11: bool, f12: bool, insert: bool, delete: bool, home: bool, end: bool, pageup: bool, pagedown: bool, up: bool, down: bool, left: bool, right: bool, tab: bool, backspace: bool, enter: bool, @" ": bool, @"!": bool, @"`": bool, esc: bool, @"[": bool, @"\\": bool, @"]": bool, @"/": bool, _: bool, @"'": bool, @"\"": bool, @",": bool, @"-": bool, @".": bool, @"#": bool, @"$": bool, @"%": bool, @"&": bool, @"*": bool, @"(": bool, @")": bool, @"+": bool, @"=": bool, @":": bool, @";": bool, @"<": bool, @">": bool, @"?": bool, @"@": bool, @"^": bool, @"~": bool, @"{": bool, @"}": bool, @"|": bool, @"0": bool, @"1": bool, @"2": bool, @"3": bool, @"4": bool, @"5": bool, @"6": bool, @"7": bool, @"8": bool, @"9": bool, a: bool, b: bool, c: bool, d: bool, e: bool, f: bool, g: bool, h: bool, i: bool, j: bool, k: bool, l: bool, m: bool, n: bool, o: bool, p: bool, q: bool, r: bool, s: bool, t: bool, u: bool, v: bool, w: bool, x: bool, y: bool, z: bool, pub fn getEnabledPrintableAscii(self: Key) ?u8 { if (self.ctrl or self.alt) return null; inline for (std.meta.fields(Key)) |field| { if (field.name.len == 1 and std.ascii.isPrint(field.name[0]) and @field(self, field.name)) { if (self.shift) { if (!std.ascii.isAlphanumeric(field.name[0])) return null; return std.ascii.toUpper(field.name[0]); } return field.name[0]; } } return null; } }; pub fn getKeyList(allocator: Allocator, tb_event: termbox.tb_event) !KeyList { var keys: KeyList = .empty; var key = std.mem.zeroes(Key); if (tb_event.mod & termbox.TB_MOD_CTRL != 0) key.ctrl = true; if (tb_event.mod & termbox.TB_MOD_SHIFT != 0) key.shift = true; if (tb_event.mod & termbox.TB_MOD_ALT != 0) key.alt = true; if (tb_event.key == termbox.TB_KEY_BACK_TAB) { key.shift = true; key.tab = true; } else if (tb_event.key > termbox.TB_KEY_BACK_TAB) { const code = 0xFFFF - tb_event.key; switch (code) { 0 => key.f1 = true, 1 => key.f2 = true, 2 => key.f3 = true, 3 => key.f4 = true, 4 => key.f5 = true, 5 => key.f6 = true, 6 => key.f7 = true, 7 => key.f8 = true, 8 => key.f9 = true, 9 => key.f10 = true, 10 => key.f11 = true, 11 => key.f12 = true, 12 => key.insert = true, 13 => key.delete = true, 14 => key.home = true, 15 => key.end = true, 16 => key.pageup = true, 17 => key.pagedown = true, 18 => key.up = true, 19 => key.down = true, 20 => key.left = true, 21 => key.right = true, else => {}, } } else if (tb_event.ch < 128) { const code = if (tb_event.ch == 0 and tb_event.key < 128) tb_event.key else tb_event.ch; switch (code) { 0 => { key.ctrl = true; key.@"2" = true; try keys.append(allocator, key); key = std.mem.zeroes(Key); key.@"`" = true; }, 1 => { key.ctrl = true; key.a = true; }, 2 => { key.ctrl = true; key.b = true; }, 3 => { key.ctrl = true; key.c = true; }, 4 => { key.ctrl = true; key.d = true; }, 5 => { key.ctrl = true; key.e = true; }, 6 => { key.ctrl = true; key.f = true; }, 7 => { key.ctrl = true; key.g = true; }, 8 => { key.ctrl = true; key.h = true; try keys.append(allocator, key); key = std.mem.zeroes(Key); key.backspace = true; }, 9 => { key.ctrl = true; key.i = true; try keys.append(allocator, key); key = std.mem.zeroes(Key); key.tab = true; }, 10 => { key.ctrl = true; key.j = true; }, 11 => { key.ctrl = true; key.k = true; }, 12 => { key.ctrl = true; key.l = true; }, 13 => { key.ctrl = true; key.m = true; try keys.append(allocator, key); key = std.mem.zeroes(Key); key.enter = true; }, 14 => { key.ctrl = true; key.n = true; }, 15 => { key.ctrl = true; key.o = true; }, 16 => { key.ctrl = true; key.p = true; }, 17 => { key.ctrl = true; key.q = true; }, 18 => { key.ctrl = true; key.r = true; }, 19 => { key.ctrl = true; key.s = true; }, 20 => { key.ctrl = true; key.t = true; }, 21 => { key.ctrl = true; key.u = true; }, 22 => { key.ctrl = true; key.v = true; }, 23 => { key.ctrl = true; key.w = true; }, 24 => { key.ctrl = true; key.x = true; }, 25 => { key.ctrl = true; key.y = true; }, 26 => { key.ctrl = true; key.z = true; }, 27 => { key.ctrl = true; key.@"3" = true; try keys.append(allocator, key); key = std.mem.zeroes(Key); key.esc = true; try keys.append(allocator, key); key = std.mem.zeroes(Key); key.@"[" = true; }, 28 => { key.ctrl = true; key.@"4" = true; try keys.append(allocator, key); key = std.mem.zeroes(Key); key.@"\\" = true; }, 29 => { key.ctrl = true; key.@"5" = true; try keys.append(allocator, key); key = std.mem.zeroes(Key); key.@"]" = true; }, 30 => { key.ctrl = true; try keys.append(allocator, key); key = std.mem.zeroes(Key); key.@"6" = true; }, 31 => { key.ctrl = true; key.@"7" = true; try keys.append(allocator, key); key = std.mem.zeroes(Key); key.@"/" = true; try keys.append(allocator, key); key = std.mem.zeroes(Key); key._ = true; }, 32 => { key.@" " = true; }, 33 => { key = std.mem.zeroes(Key); key.@"!" = true; }, 34 => { key = std.mem.zeroes(Key); key.@"\"" = true; }, 35 => { key = std.mem.zeroes(Key); key.@"#" = true; }, 36 => { key = std.mem.zeroes(Key); key.@"$" = true; }, 37 => { key = std.mem.zeroes(Key); key.@"%" = true; }, 38 => { key = std.mem.zeroes(Key); key.@"&" = true; }, 39 => { key.@"'" = true; }, 40 => { key = std.mem.zeroes(Key); key.@"(" = true; }, 41 => { key = std.mem.zeroes(Key); key.@")" = true; }, 42 => { key = std.mem.zeroes(Key); key.@"*" = true; }, 43 => { key = std.mem.zeroes(Key); key.@"+" = true; }, 44 => { key.@"," = true; }, 45 => { key.@"-" = true; }, 46 => { key.@"." = true; }, 47 => { key.@"/" = true; }, 48 => { key.@"0" = true; }, 49 => { key.@"1" = true; }, 50 => { key.@"2" = true; }, 51 => { key.@"3" = true; }, 52 => { key.@"4" = true; }, 53 => { key.@"5" = true; }, 54 => { key.@"6" = true; }, 55 => { key.@"7" = true; }, 56 => { key.@"8" = true; }, 57 => { key.@"9" = true; }, 58 => { key.shift = true; key.@":" = true; }, 59 => { key.@";" = true; }, 60 => { key.shift = true; key.@"<" = true; }, 61 => { key.@"=" = true; }, 62 => { key.shift = true; key.@">" = true; }, 63 => { key.shift = true; key.@"?" = true; }, 64 => { key.shift = true; key.@"2" = true; try keys.append(allocator, key); key = std.mem.zeroes(Key); key.@"@" = true; }, 65 => { key.shift = true; key.a = true; }, 66 => { key.shift = true; key.b = true; }, 67 => { key.shift = true; key.c = true; }, 68 => { key.shift = true; key.d = true; }, 69 => { key.shift = true; key.e = true; }, 70 => { key.shift = true; key.f = true; }, 71 => { key.shift = true; key.g = true; }, 72 => { key.shift = true; key.h = true; }, 73 => { key.shift = true; key.i = true; }, 74 => { key.shift = true; key.j = true; }, 75 => { key.shift = true; key.k = true; }, 76 => { key.shift = true; key.l = true; }, 77 => { key.shift = true; key.m = true; }, 78 => { key.shift = true; key.n = true; }, 79 => { key.shift = true; key.o = true; }, 80 => { key.shift = true; key.p = true; }, 81 => { key.shift = true; key.q = true; }, 82 => { key.shift = true; key.r = true; }, 83 => { key.shift = true; key.s = true; }, 84 => { key.shift = true; key.t = true; }, 85 => { key.shift = true; key.u = true; }, 86 => { key.shift = true; key.v = true; }, 87 => { key.shift = true; key.w = true; }, 88 => { key.shift = true; key.x = true; }, 89 => { key.shift = true; key.y = true; }, 90 => { key.shift = true; key.z = true; }, 91 => { key.@"[" = true; }, 92 => { key.@"\\" = true; }, 93 => { key.@"]" = true; }, 94 => { key = std.mem.zeroes(Key); key.@"^" = true; }, 95 => { key.shift = true; key.@"-" = true; try keys.append(allocator, key); key = std.mem.zeroes(Key); key._ = true; }, 96 => { key.@"`" = true; }, 97 => { key.a = true; }, 98 => { key.b = true; }, 99 => { key.c = true; }, 100 => { key.d = true; }, 101 => { key.e = true; }, 102 => { key.f = true; }, 103 => { key.g = true; }, 104 => { key.h = true; }, 105 => { key.i = true; }, 106 => { key.j = true; }, 107 => { key.k = true; }, 108 => { key.l = true; }, 109 => { key.m = true; }, 110 => { key.n = true; }, 111 => { key.o = true; }, 112 => { key.p = true; }, 113 => { key.q = true; }, 114 => { key.r = true; }, 115 => { key.s = true; }, 116 => { key.t = true; }, 117 => { key.u = true; }, 118 => { key.v = true; }, 119 => { key.w = true; }, 120 => { key.x = true; }, 121 => { key.y = true; }, 122 => { key.z = true; }, 123 => { key.shift = true; key.@"{" = true; }, 124 => { key.shift = true; key.@"\\" = true; try keys.append(allocator, key); key = std.mem.zeroes(Key); key.@"|" = true; }, 125 => { key.shift = true; key.@"}" = true; }, 126 => { key.shift = true; key.@"`" = true; try keys.append(allocator, key); key = std.mem.zeroes(Key); key.@"~" = true; }, 127 => { key.ctrl = true; key.@"8" = true; try keys.append(allocator, key); key = std.mem.zeroes(Key); key.backspace = true; }, else => {}, } } try keys.append(allocator, key); return keys; } ================================================ FILE: ly-ui/src/root.zig ================================================ pub const ly_core = @import("ly-core"); pub const Cell = @import("Cell.zig"); pub const keyboard = @import("keyboard.zig"); pub const Position = @import("Position.zig"); pub const TerminalBuffer = @import("TerminalBuffer.zig"); pub const Widget = @import("Widget.zig"); pub const BigLabel = @import("components/BigLabel.zig"); pub const CenteredBox = @import("components/CenteredBox.zig"); pub const CyclableLabel = @import("components/generic.zig").CyclableLabel; pub const Label = @import("components/Label.zig"); pub const Text = @import("components/Text.zig"); ================================================ FILE: readme.md ================================================ # The Ly display manager ![Ly screenshot](.github/screenshot.png "Ly screenshot") Ly is a lightweight TUI (ncurses-like) display manager for Linux and BSD, designed with portability in mind (e.g. it does not require systemd to run). Join us on Matrix over at [#ly-dm:matrix.org](https://matrix.to/#/#ly-dm:matrix.org)! **Note**: Development happens on [Codeberg](https://codeberg.org/fairyglade/ly) with a mirror on [GitHub](https://github.com/fairyglade/ly). ## Dependencies - Compile-time: - zig 0.15.x - libc - pam - xcb (optional, required by default; needed for X11 support) - Runtime (with default config): - xorg - xorg-xauth - shutdown - brightnessctl ### Debian ``` # apt install build-essential libpam0g-dev libxcb-xkb-dev xauth xserver-xorg brightnessctl ``` ### Fedora **Warning**: You may encounter issues with SELinux on Fedora. It is recommended to add a rule for Ly as it currently does not ship one. ``` # dnf install kernel-devel pam-devel libxcb-devel zig xorg-x11-xauth xorg-x11-server brightnessctl ``` ### FreeBSD ``` # pkg install ca_root_nss libxcb git xorg xauth ``` ## Availability [![Packaging status](https://repology.org/badge/vertical-allrepos/ly-display-manager.svg?exclude_unsupported=1)](https://repology.org/project/ly-display-manager/versions) ## Support Every environment that works on other login managers also should work on Ly. - Unlike most login managers Ly has xinitrc entry and it also supports shell. - If you installed your favorite environment and you don't see it, that's because Ly doesn't automatically refresh itself. To fix this you should restart Ly service (depends on your init system) or the easy way is to reboot your system. - If your environment is still missing then check at `/usr/share/xsessions` or `/usr/share/wayland-sessions` to see if a .desktop file is present. - If there isn't a .desktop file then create a new one at `/etc/ly/custom-sessions` that launches your favorite environment. These .desktop files can be only seen by Ly and if you want them system-wide you also can create at those directories instead. - If only Xorg sessions doesn't work then check if your distro compiles Ly with Xorg support as it can be compiled with Xorg support disabled. Logs are defined by `/etc/ly/config.ini`: - The session log is located at `~/.local/state/ly-session.log` by default. - The system log is located at `/var/log/ly.log` by default. ## Manually building The procedure for manually building Ly is pretty standard: ``` $ git clone https://codeberg.org/fairyglade/ly.git $ cd ly $ zig build ``` After building, you can (optionally) test Ly in a terminal emulator, although authentication will **not** work: ``` $ zig build run ``` **Important**: While you can also run Ly in a terminal emulator as root, it is **not** recommended either. If you want to properly test Ly, please enable its service (as described below) and reboot your machine. The following sections show how to install Ly for a particular init system. Because the procedure is very similar for all of them, the commands will only be detailed for the first section (which is about systemd). **Note**: All following sections will assume you are using LightDM for convenience sake. ### systemd Now, you can install Ly on your system: ``` # zig build installexe -Dinit_system=systemd ``` **Note**: The `init_system` parameter is optional and defaults to `systemd`. Note that you also need to disable your current display manager. For example, if LightDM is the current display manager, you can execute the following command: ``` # systemctl disable lightdm.service ``` Then, similarly to the previous command, you need to enable the Ly service: ``` # systemctl enable ly@tty2.service ``` **Important**: Because Ly runs in a TTY, you **must** disable the TTY service that Ly will run on, otherwise bad things will happen. For example, to disable `getty` spawning on TTY 2, you need to execute the following command: ``` # systemctl disable getty@tty2.service ``` On platforms that use systemd-logind to dynamically start `autovt@.service` instances when the switch to a new tty occurs, any ly instances for ttys _except the default tty_ need to be enabled using a different mechanism: To autostart ly on switch to `tty2`, do not enable any `ly` unit directly, instead symlink `autovt@tty2.service` to `ly@tty2.service` within `/usr/lib/systemd/system/` (analogous for every other tty you want to enable ly on). The target of the symlink, `ly@ttyN.service`, does not actually exist, but systemd nevertheless recognizes that the instanciation of `autovt@.service` with `%I` equal to `ttyN` now points to an instanciation of `ly@.service` with `%I` set to `ttyN`. Compare to `man 5 logind.conf`, especially regarding the `NAutoVTs=` and `ReserveVT=` parameters. On non-systemd systems, you can change the TTY Ly will run on by editing the corresponding service file for your platform. ### OpenRC ``` # zig build installexe -Dinit_system=openrc # rc-update del lightdm # rc-update add ly # rc-update del agetty.tty2 ``` **Note**: On Gentoo specifically, you also **must** comment out the appropriate line for the TTY in /etc/inittab. ### runit ``` # zig build installexe -Dinit_system=runit # rm /var/service/lightdm # ln -s /etc/sv/ly /var/service/ # rm /var/service/agetty-tty2 ``` ### s6 ``` # zig build installexe -Dinit_system=s6 # s6-rc -d change lightdm # s6-service add default ly-srv # s6-db-reload # s6-rc -u change ly-srv ``` To disable TTY 2, edit `/etc/s6/config/tty2.conf` and set `SPAWN="no"`. ### dinit ``` # zig build installexe -Dinit_system=dinit # dinitctl disable lightdm # dinitctl enable ly ``` To disable TTY 2, go to `/etc/dinit.d/config/console.conf` and modify `ACTIVE_CONSOLES`. ### sysvinit ``` # zig build installexe -Dinit_system=sysvinit # update-rc.d lightdm disable # update-rc.d ly defaults ``` To disable TTY 2, go to `/etc/inittab` and comment out the line containing `tty2`. ### FreeBSD ``` # zig build installexe -Dprefix_directory=/usr/local -Dconfig_directory=/usr/local/etc -Dinit_system=freebsd # sysrc lightdm_enable="NO" ``` To enable Ly, add the following entry to `/etc/gettytab`: ``` Ly:\ :lo=/usr/local/bin/ly_wrapper:\ :al=root: ``` Then, modify the command field of the `ttyv1` terminal entry in `/etc/ttys` (TTYs in FreeBSD start at 0): ``` ttyv1 "/usr/libexec/getty Ly" xterm on secure ``` ### Updating You can also install Ly without overrding the current configuration file. This is called **updating**. To update, simply run: ``` # zig build installnoconf ``` You can, of course, still select the init system of your choice when using this command. ## Configuration You can find all the configuration in `/etc/ly/config.ini`. The file is fully commented, and includes the default values. ## Controls Use the Up/Down arrow keys to change the current field, and the Left/Right arrow keys to scroll through the different fields (whether it be the info line, the desktop environment, or the username). The info line is where messages and errors are displayed. ## A note on .xinitrc If your `.xinitrc` file doesn't work ,make sure it is executable and includes a shebang. This file is supposed to be a shell script! Quoting from `xinit`'s man page: > If no specific client program is given on the command line, xinit will look for a file in the user's home directory called .xinitrc to run as a shell script to start up client programs. A typical shebang for a shell script looks like this: ``` #!/bin/sh ``` ## Tips - The numlock and capslock state is printed in the top-right corner. - Use the F1 and F2 keys to respectively shutdown and reboot. - Take a look at your `.xsession` file if X doesn't start, as it can interfere (this file is launched with X to configure the display properly). ## A final note The name "Ly" is a tribute to the fairy from the game Rayman. Ly was tested by oxodao, who is some seriously awesome dude. Also, Ly wouldn't be there today without [Kawaii-Ash](https://github.com/Kawaii-Ash), who has done significant contributions to the project for the Zig rewrite, which lead to the release of Ly v1.0.0. Massive thanks, and sorry for not crediting you enough beforehand! ### Donate If you like Ly and wish to support my work further, feel free to donate via my [Liberapay link](https://liberapay.com/ShiningLea)! ================================================ FILE: res/config.ini ================================================ # 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) 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 # 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 battery_id = null # 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 = null # Username to automatically log in # Must be a valid user on the system # If null, automatic login is disabled auto_login_user = null # 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 # Title to show at the top of the main box # If set to null, none will be shown box_title = null # Brightness decrease command brightness_down_cmd = $PREFIX_DIRECTORY/bin/brightnessctl -q -n s 10%- # Brightness decrease key combination, or null to disable brightness_down_key = F5 # Brightness increase command brightness_up_cmd = $PREFIX_DIRECTORY/bin/brightnessctl -q -n s +10% # Brightness increase key combination, or null to disable 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 = null # 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 # 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 # Command executed when pressing hibernate key (can be null) hibernate_cmd = null # Specifies the key combination used for hibernate hibernate_key = F4 # Remove main box borders hide_borders = false # Remove power management command hints hide_key_hints = false # Remove keyboard lock states from the top right corner hide_keyboard_locks = false # Remove version number from the top left corner hide_version_string = false # Command executed when no input is detected for a certain time # If null, no command will be executed inactivity_cmd = null # 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 = null # 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 = null # 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 = null # General log file path 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 # Command executed when pressing restart_key restart_cmd = /sbin/shutdown -r now # Specifies the key combination used for restart restart_key = F2 # Save the current desktop and login as defaults, and load them on startup save = true # 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 # Specifies the key combination used for showing the password show_password_key = F7 # Command executed when pressing shutdown_key shutdown_cmd = /sbin/shutdown $PLATFORM_SHUTDOWN_ARG now # Specifies the key combination used for shutdown shutdown_key = F1 # Command executed when pressing sleep key (can be null) sleep_cmd = null # Specifies the key combination used for sleep sleep_key = F3 # 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 # 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 waylandsessions = $PREFIX_DIRECTORY/share/wayland-sessions # Xorg server command 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 = null # 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 xsessions = $PREFIX_DIRECTORY/share/xsessions ================================================ FILE: res/custom-sessions/README ================================================ A custom session is just a desktop entry file, like for X11 and Wayland sessions. For example: [Desktop Entry] Name=Fish shell Exec=$PREFIX_DIRECTORY/bin/fish DesktopNames=null Terminal=true The DesktopNames value is optional and sets the XDG_SESSION_DESKTOP and XDG_CURRENT_DESKTOP environment variables. If equal to null or if not present, XDG_SESSION_DESKTOP and XDG_CURRENT_DESKTOP will not be set. Otherwise, the syntax is the same as described in the Freedesktop Desktop Entry Specification. The Terminal value specifies if standard output and standard error should be redirected to the session log file found in Ly's configuration file. If set to true, Ly will consider the program is going to run in a TTY, and thus will not redirect standard output & error. It is optional and defaults to false. Finally, do note that if the Terminal value is set to true, the XDG_SESSION_TYPE environment variable will be set to "tty". Otherwise, it will be set to "unspecified" (without quotes), which is behavior that at least systemd recognizes (see pam_systemd's man page). ================================================ FILE: res/lang/ar.ini ================================================ authenticating = جاري المصادقة... brightness_down = خفض السطوع brightness_up = رفع السطوع capslock = capslock err_alloc = فشل في تخصيص الذاكرة err_bounds = out-of-bounds index err_brightness_change = فشل في تغيير سطوع الشاشة err_chdir = فشل في فتح مجلد المنزل err_config = فشل في تفسير ملف الإعدادات err_dgn_oob = رسالة سجل (Log) err_domain = اسم نطاق غير صالح err_empty_password = لا يُسمح بكلمة مرور فارغة err_envlist = فشل في جلب قائمة المتغيرات البيئية err_hostname = فشل في جلب اسم المضيف (Hostname) err_mlock = فشل في تأمين ذاكرة كلمة المرور (mlock) err_null = مؤشر فارغ (Null pointer) err_numlock = فشل في ضبط Num Lock err_pam = فشل في معاملة PAM err_pam_abort = تم إلغاء معاملة PAM err_pam_acct_expired = الحساب منتهي الصلاحية err_pam_auth = خطأ في المصادقة (Authentication error) err_pam_authinfo_unavail = فشل في الحصول على معلومات المستخدم err_pam_authok_reqd = انتهت صلاحية رمز المصادقة (Token) err_pam_buf = خطأ في ذاكرة التخزين المؤقت (Buffer) err_pam_cred_err = فشل في تعيين بيانات الاعتماد (Credentials) err_pam_cred_expired = بيانات الاعتماد منتهية الصلاحية err_pam_cred_insufficient = بيانات الاعتماد غير كافية err_pam_cred_unavail = فشل في الحصول على بيانات الاعتماد err_pam_maxtries = تم بلوغ الحد الأقصى لمحاولات المصادقة err_pam_perm_denied = تم رفض الوصول (Permission denied) err_pam_session = خطأ في جلسة المستخدم (Session error) err_pam_sys = خطأ في النظام (System error) err_pam_user_unknown = المستخدم غير موجود err_path = فشل في تعيين متغير PATH err_perm_dir = فشل في تغيير المجلد الحالي err_perm_group = فشل في تخفيض صلاحيات المجموعة (Group permissions) err_perm_user = فشل في تخفيض صلاحيات المستخدم (User permissions) err_pwnam = فشل في جلب معلومات المستخدم err_sleep = فشل في تنفيذ أمر sleep err_tty_ctrl = فشل في نقل تحكم الطرفية (TTY) err_user_gid = فشل في تعيين معرّف المجموعة (GID) للمستخدم err_user_init = فشل في تهيئة بيانات المستخدم err_user_uid = فشل في تعيين معرّف المستخدم (UID) err_xauth = فشل في تنفيذ أمر xauth err_xcb_conn = فشل في الاتصال بمكتبة XCB err_xsessions_dir = فشل في العثور على مجلد Xsessions err_xsessions_open = فشل في فتح مجلد Xsessions insert = ادخال login = تسجيل الدخول logout = تم تسجيل خروجك no_x11_support = تم تعطيل دعم x11 اثناء وقت الـ compile normal = عادي numlock = numlock other = اخر password = كلمة السر restart = اعادة التشغيل shell = shell shutdown = ايقاف التشغيل sleep = وضع السكون wayland = wayland x11 = x11 xinitrc = xinitrc ================================================ FILE: res/lang/bg.ini ================================================ authenticating = удостоверяване... brightness_down = намаляване на яркостта brightness_up = увеличаване на яркостта capslock = caps lock custom = персонализирано err_alloc = неуспешно заделяне на памет err_args = неуспешен анализ на аргументите от командния ред err_autologin_session = сесията за автоматично влизане не е намерена err_bounds = индексът е извън границите err_brightness_change = неуспешна промяна на яркостта err_chdir = неуспешно отваряне на домашната папка err_clock_too_long = низът на часовника е твърде дълъг err_config = неуспешен анализ на конфигурационния файл err_crawl = неуспешно обхождане на папките със сесии err_dgn_oob = съобщение в дневника err_domain = невалиден домейн err_empty_password = не е позволена празна парола err_envlist = неуспешно получаване на списъка с променливи на средата err_get_active_tty = неуспешно откриване на активния TTY err_hibernate = неуспешно изпълнение на командата за хибернация err_hostname = неуспешно получаване на името на хоста err_inactivity = неуспешно изпълнение на командата за неактивност err_lock_state = неуспешно получаване на състоянието на заключване err_log = неуспешно отваряне на файла с дневника err_mlock = неуспешно заключване на паметта за паролата err_null = нулев указател err_numlock = неуспешно задаване на num lock err_pam = неуспешна транзакция err_pam_abort = прекратена транзакция err_pam_acct_expired = изтекъл профил err_pam_auth = грешка при удостоверяването err_pam_authinfo_unavail = неуспешно получаване на информация за потребителя err_pam_authok_reqd = изтекъл жетон err_pam_buf = грешка в буфера на паметта err_pam_cred_err = неуспешно задаване на удостоверения err_pam_cred_expired = изтекли удостоверения err_pam_cred_insufficient = недостатъчни удостоверения err_pam_cred_unavail = неуспешно получаване на удостоверения err_pam_maxtries = достигнат е максималният лимит на опитите err_pam_perm_denied = достъпът е отказан err_pam_session = грешка в сесията err_pam_sys = системна грешка err_pam_user_unknown = непознат потребител err_path = неуспешно задаване на пътя err_perm_dir = неуспешна смяна на текущата папка err_perm_group = неуспешно понижаване на правата на групата err_perm_user = неуспешно понижаване на правата на потребителя err_pwnam = неуспешно получаване на информация за потребителя err_sleep = неуспешно изпълнение на командата за заспиване err_start = неуспешно изпълнение на командата за стартиране err_battery = неуспешно зареждане на състоянието на батерията err_switch_tty = неуспешна смяна на TTY err_tty_ctrl = неуспешно прехвърляне на контрола над TTY err_no_users = не са намерени потребители err_uid_range = неуспешно динамично получаване на UID обхват err_user_gid = неуспешно задаване на потребителския GID err_user_init = неуспешна стартиране на потребителя err_user_uid = неуспешно задаване на потребителския UID err_xauth = неуспешна команда xauth err_xcb_conn = неуспешна xcb връзка err_xsessions_dir = папката със сесии не е намерена err_xsessions_open = неуспешно отваряне на папката със сесии hibernate = хибернация insert = вмъкване login = вход logout = излизане no_x11_support = поддръжката на x11 е изключена при компилирането normal = нормално numlock = num lock other = друго password = парола restart = рестартиране shell = обвивка shutdown = изключване sleep = заспиване wayland = wayland x11 = x11 xinitrc = xinitrc ================================================ FILE: res/lang/cat.ini ================================================ authenticating = autenticant... brightness_down = abaixar brillantor brightness_up = apujar brillantor capslock = Bloq Majús err_alloc = assignació de memòria fallida err_bounds = índex fora de límits err_brightness_change = error en canviar la brillantor err_chdir = error en obrir la carpeta home err_dgn_oob = missatge de registre err_domain = domini invàlid err_envlist = error en obtenir l'envlist err_hostname = error en obtenir el nom de l'amfitrió err_mlock = error en bloquejar la memòria de clau err_null = punter nul err_numlock = error en establir el Bloq num err_pam = error en la transacció pam err_pam_abort = transacció pam avortada err_pam_acct_expired = compte expirat err_pam_auth = error d'autenticació err_pam_authinfo_unavail = error en obtenir la informació de l'usuari err_pam_authok_reqd = token expirat err_pam_buf = error en la memòria intermèdia err_pam_cred_err = error en establir les credencials err_pam_cred_expired = credencials expirades err_pam_cred_insufficient = credencials insuficients err_pam_cred_unavail = error en obtenir credencials err_pam_maxtries = s'ha assolit al nombre màxim d'intents err_pam_perm_denied = permís denegat err_pam_session = error de sessió err_pam_sys = error de sistema err_pam_user_unknown = usuari desconegut err_path = error en establir la ruta err_perm_dir = error en canviar el directori actual err_perm_group = error en degradar els permisos de grup err_perm_user = error en degradar els permisos de l'usuari err_pwnam = error en obtenir la informació de l'usuari err_user_gid = error en establir el GID de l'usuari err_user_init = error en inicialitzar usuari err_user_uid = error en establir l'UID de l'usuari err_xauth = error en la comanda xauth err_xcb_conn = error en la connexió xcb err_xsessions_dir = error en trobar la carpeta de sessions err_xsessions_open = error en obrir la carpeta de sessions insert = inserir login = iniciar sessió logout = sessió tancada no_x11_support = el suport per x11 ha estat desactivat en la compilació normal = normal numlock = Bloq Num password = Clau restart = reiniciar shell = shell shutdown = aturar sleep = suspendre wayland = wayland x11 = x11 xinitrc = xinitrc ================================================ FILE: res/lang/cs.ini ================================================ capslock = capslock err_alloc = alokace paměti selhala err_bounds = index je mimo hranice pole err_chdir = nelze otevřít domovský adresář err_dgn_oob = zpráva protokolu err_domain = neplatná doména err_hostname = nelze získat název hostitele err_mlock = uzamčení paměti hesel selhalo err_null = nulový ukazatel err_pam = pam transakce selhala err_pam_abort = pam transakce přerušena err_pam_acct_expired = platnost účtu vypršela err_pam_auth = chyba autentizace err_pam_authinfo_unavail = nelze získat informace o uživateli err_pam_authok_reqd = platnost tokenu vypršela err_pam_buf = chyba vyrovnávací paměti err_pam_cred_err = nelze nastavit pověření err_pam_cred_expired = platnost pověření vypršela err_pam_cred_insufficient = nedostatečné pověření err_pam_cred_unavail = nepodařilo se získat pověření err_pam_maxtries = byl dosažen maximální počet pokusů err_pam_perm_denied = přístup odepřen err_pam_session = chyba relace err_pam_sys = systemová chyba err_pam_user_unknown = neznámý uživatel err_path = nepodařilo se nastavit cestu err_perm_dir = nepodařilo se změnit adresář err_perm_group = nepodařilo se snížit skupinová oprávnění err_perm_user = nepodařilo se snížit uživatelská oprávnění err_pwnam = nelze získat informace o uživateli err_user_gid = nastavení GID uživatele selhalo err_user_init = inicializace uživatele selhala err_user_uid = nastavení UID uživateli selhalo err_xsessions_dir = nepodařilo se najít složku relací err_xsessions_open = nepodařilo se otevřít složku relací login = uživatel logout = odhlášen numlock = numlock password = heslo restart = restartovat shell = příkazový řádek shutdown = vypnout wayland = wayland xinitrc = xinitrc ================================================ FILE: res/lang/de.ini ================================================ authenticating = authentifizieren... brightness_down = Helligkeit- brightness_up = Helligkeit+ capslock = Feststelltaste err_alloc = Speicherzuweisung fehlgeschlagen err_bounds = Index ausserhalb des Bereichs err_brightness_change = Helligkeitsänderung fehlgeschlagen err_chdir = Fehler beim Oeffnen des Home-Ordners err_config = Fehler beim Verarbeiten der Konfigurationsdatei err_dgn_oob = Diagnose-Nachricht err_domain = Ungueltige Domain err_empty_password = Leeres Passwort nicht zugelassen err_envlist = Fehler beim Abrufen der Umgebungs-Variablen err_hostname = Abrufen des Hostnames fehlgeschlagen err_mlock = Sperren des Passwortspeichers fehlgeschlagen err_null = Null Pointer err_numlock = Numlock konnte nicht aktiviert werden err_pam = PAM-Transaktion fehlgeschlagen err_pam_abort = PAM-Transaktion abgebrochen err_pam_acct_expired = Benutzerkonto abgelaufen err_pam_auth = Authentifizierungsfehler err_pam_authinfo_unavail = Abrufen der Benutzerinformationen fehlgeschlagen err_pam_authok_reqd = Passwort abgelaufen err_pam_buf = Speicherpufferfehler err_pam_cred_err = Fehler beim Setzen der Anmeldedaten err_pam_cred_expired = Anmeldedaten abgelaufen err_pam_cred_insufficient = Anmeldedaten unzureichend err_pam_cred_unavail = Fehler beim Abrufen der Anmeldedaten err_pam_maxtries = Maximale Versuchsanzahl erreicht err_pam_perm_denied = Zugriff verweigert err_pam_session = Sitzungsfehler err_pam_sys = Systemfehler err_pam_user_unknown = Unbekannter Nutzer err_path = Fehler beim Setzen des Pfades err_perm_dir = Ordnerwechsel fehlgeschlagen err_perm_group = Fehler beim Heruntersetzen der Gruppenberechtigungen err_perm_user = Fehler beim Heruntersetzen der Nutzerberechtigungen err_pwnam = Abrufen der Benutzerinformationen fehlgeschlagen err_sleep = Sleep-Befehl fehlgeschlagen err_tty_ctrl = Fehler bei der TTY-Uebergabe err_user_gid = Fehler beim Setzen der Gruppen-ID err_user_init = Nutzer-Initialisierung fehlgeschlagen err_user_uid = Setzen der Benutzer-ID fehlgeschlagen err_xauth = Xauth-Befehl fehlgeschlagen err_xcb_conn = xcb-Verbindung fehlgeschlagen err_xsessions_dir = Fehler beim Finden des Sitzungsordners err_xsessions_open = Fehler beim Oeffnen des Sitzungsordners insert = Einfügen login = Nutzer logout = Abmelden no_x11_support = X11-Support bei Kompilierung deaktiviert normal = Normal numlock = Numlock other = Andere password = Passwort restart = Neustarten shell = Shell shutdown = Herunterfahren sleep = Sleep wayland = wayland x11 = X11 xinitrc = xinitrc ================================================ FILE: res/lang/en.ini ================================================ authenticating = authenticating... brightness_down = decrease brightness brightness_up = increase brightness capslock = capslock custom = custom err_alloc = failed memory allocation err_args = unable to parse command line arguments err_autologin_session = autologin session not found err_bounds = out-of-bounds index err_brightness_change = failed to change brightness err_chdir = failed to open home folder err_clock_too_long = clock string too long err_config = unable to parse config file err_crawl = failed to crawl session directories err_dgn_oob = log message err_domain = invalid domain err_empty_password = empty password not allowed err_envlist = failed to get envlist err_get_active_tty = failed to get active tty err_hibernate = failed to execute hibernate command err_hostname = failed to get hostname err_inactivity = failed to execute inactivity command err_lock_state = failed to get lock state err_log = failed to open log file err_mlock = failed to lock password memory err_null = null pointer err_numlock = failed to set numlock err_pam = pam transaction failed err_pam_abort = pam transaction aborted err_pam_acct_expired = account expired err_pam_auth = authentication error err_pam_authinfo_unavail = failed to get user info err_pam_authok_reqd = token expired err_pam_buf = memory buffer error err_pam_cred_err = failed to set credentials err_pam_cred_expired = credentials expired err_pam_cred_insufficient = insufficient credentials err_pam_cred_unavail = failed to get credentials err_pam_maxtries = reached maximum tries limit err_pam_perm_denied = permission denied err_pam_session = session error err_pam_sys = system error err_pam_user_unknown = unknown user err_path = failed to set path err_perm_dir = failed to change current directory err_perm_group = failed to downgrade group permissions err_perm_user = failed to downgrade user permissions err_pwnam = failed to get user info err_sleep = failed to execute sleep command err_start = failed to execute start command err_battery = failed to load battery status err_switch_tty = failed to switch tty err_tty_ctrl = tty control transfer failed err_no_users = no users found err_uid_range = failed to dynamically get uid range err_user_gid = failed to set user GID err_user_init = failed to initialize user err_user_uid = failed to set user UID err_xauth = xauth command failed err_xcb_conn = xcb connection failed err_xsessions_dir = failed to find sessions folder err_xsessions_open = failed to open sessions folder hibernate = hibernate insert = insert login = login logout = logged out no_x11_support = x11 support disabled at compile-time normal = normal numlock = numlock other = other password = password restart = reboot shell = shell shutdown = shutdown sleep = sleep toggle_password = toggle password wayland = wayland x11 = x11 xinitrc = xinitrc ================================================ FILE: res/lang/eo.ini ================================================ authenticating = aŭtentigado... brightness_down = malpliigi helecon brightness_up = pliigi helecon capslock = majuskla baskulo custom = propra err_alloc = malsukcesis memorasignon err_args = ne povas analizi argumentojn de komanda linio err_autologin_session = aŭtomatan ensalutan seancon ne trovis err_bounds = indico estas ekster-intervala err_brightness_change = malsukcesis ŝanĝi la helecon err_chdir = malsukcesis malfermi hejman dosierujon err_clock_too_long = horloĝa ĉeno estas tro longa err_config = ne povas analizi agordan dosieron err_crawl = malsukcesis dum serĉado de seancaj dosierujoj err_dgn_oob = protokola mesaĝo err_domain = malvalida domajno err_empty_password = ne akceptas malplenan pasvorton err_envlist = malsukcesis preni la medivariablojn err_get_active_tty = malsukcesis preni la aktivan TTY-on err_hibernate = malsukcesis ruli la komandon por diskodormo err_hostname = malsukcesis preni la sistemnomon err_inactivity = malsukcesis ruli la agorditan komandon por malaktiveco err_lock_state = malsukcesis preni la ŝlosan staton err_log = malsukcesis malfermi la protokolan dosieron err_mlock = malsukcesis ŝlosi pasvortan memoron err_null = nula memorloko err_numlock = malsukcesis agordi numeran baskulon err_pam = PAM-a transakcio malsukcesis err_pam_abort = PAM-a transakcio malsukcesis err_pam_acct_expired = konto eksvalidiĝis err_pam_auth = aŭtentiga eraro err_pam_authinfo_unavail = malsukcesis preni uzantajn informojn err_pam_authok_reqd = memorsigno eksvalidiĝis err_pam_buf = bufra eraro err_pam_cred_err = malsukcesis agordi akreditaĵon err_pam_cred_expired = akreditaĵo eksvalidiĝis err_pam_cred_insufficient = nesufiĉa akreditaĵo err_pam_cred_unavail = malsukcesis preni akreditaĵon err_pam_maxtries = atingis maksimuman kvanton da provoj err_pam_perm_denied = permeso negis err_pam_session = seancan eraron err_pam_sys = sisteman eraron err_pam_user_unknown = ne konas uzanton err_path = malsukcesis agordi la median dosierindikon err_perm_dir = malsukcesis ŝanĝi la nunan dosierujon err_perm_group = malsukcesis redukti grupajn permesojn err_perm_user = malsukcesis redukti uzantajn permesojn err_pwnam = malsukcesis preni uzantajn informojn err_sleep = malsukcesis ruli memordorman komandon err_start = malsukcesis ruli startan komandon err_battery = malsukcesis ŝargi baterian staton err_switch_tty = malsukcesis ŝanĝi TTY-on err_tty_ctrl = TTY-an stiran transigon malsukcesis err_no_users = nul uzantojn trovas err_uid_range = malsukcesis dinamike preni UID-an intervalon err_user_gid = malsukcesis agordi uzantan GID-on err_user_init = malsukcesis iniciĝi uzanto err_user_uid = malsukcesis agordi uzantan UID-on err_xauth = malsukcesis plenumi je xauth err_xcb_conn = malsukcesis dum konectado al xcb err_xsessions_dir = malsukcesis trovi seancan dosierujon err_xsessions_open = malsukcesis malfermi seancan dosierujon hibernate = diskodormi insert = enmeti login = uzanto logout = elsalutis no_x11_support = x11 estas foriĝita de kompil-tempo normal = normala numlock = numera baskulo other = alia password = pasvorto restart = restartigi shell = ŝelo shutdown = malŝalti sleep = memordormi wayland = wayland x11 = x11 xinitrc = xinitrc ================================================ FILE: res/lang/es.ini ================================================ authenticating = autenticando... brightness_down = bajar brillo brightness_up = subir brillo capslock = Bloq Mayús err_alloc = asignación de memoria fallida err_bounds = índice fuera de límites err_chdir = error al abrir la carpeta home err_dgn_oob = mensaje de registro err_domain = dominio inválido err_hostname = error al obtener el nombre de host err_mlock = error al bloquear la contraseña de memoria err_null = puntero nulo err_pam = error en la transacción pam err_pam_abort = transacción pam abortada err_pam_acct_expired = cuenta expirada err_pam_auth = error de autenticación err_pam_authinfo_unavail = error al obtener información del usuario err_pam_authok_reqd = token expirado err_pam_buf = error de la memoria intermedia err_pam_cred_err = error al establecer las credenciales err_pam_cred_expired = credenciales expiradas err_pam_cred_insufficient = credenciales insuficientes err_pam_cred_unavail = error al obtener credenciales err_pam_maxtries = se ha alcanzado el límite de intentos err_pam_perm_denied = permiso denegado err_pam_session = error de sesión err_pam_sys = error de sistema err_pam_user_unknown = usuario desconocido err_path = error al establecer la ruta err_perm_dir = error al cambiar el directorio actual err_perm_group = error al degradar los permisos del grupo err_perm_user = error al degradar los permisos del usuario err_pwnam = error al obtener la información del usuario err_user_gid = error al establecer el GID del usuario err_user_init = error al inicializar usuario err_user_uid = error al establecer el UID del usuario err_xsessions_dir = error al buscar la carpeta de sesiones err_xsessions_open = error al abrir la carpeta de sesiones insert = insertar login = usuario logout = cerrar sesión no_x11_support = soporte para x11 deshabilitado en tiempo de compilación normal = normal numlock = Bloq Num other = otro password = contraseña restart = reiniciar shell = shell shutdown = apagar sleep = suspender wayland = wayland xinitrc = xinitrc ================================================ FILE: res/lang/fr.ini ================================================ authenticating = authentification... brightness_down = diminuer la luminosité brightness_up = augmenter la luminosité capslock = verr.maj custom = customisé err_alloc = échec d'allocation mémoire err_args = échec de l'analyse des arguments en lignes de commande err_autologin_session = session de connexion automatique introuvable err_bounds = indice hors-limite err_brightness_change = échec du changement de luminosité err_chdir = échec de l'ouverture du répertoire home err_clock_too_long = chaîne de formattage de l'horloge trop longue err_config = échec de lecture du fichier de configuration err_crawl = échec de la navigation des répertoires de session err_dgn_oob = message err_domain = domaine invalide err_empty_password = mot de passe vide non autorisé err_envlist = échec de lecture de la liste d'environnement err_get_active_tty = échec de lecture du terminal actif err_hibernate = échec de l'exécution de la commande de veille prolongée err_hostname = échec de lecture du nom d'hôte err_inactivity = échec de l'exécution de la commande d'inactivité err_lock_state = échec de lecture de l'état de verrouillage err_log = échec de l'ouverture du fichier de journal err_mlock = échec du verrouillage mémoire err_null = pointeur null err_numlock = échec de modification du verr.num err_pam = échec de la transaction pam err_pam_abort = transaction pam avortée err_pam_acct_expired = compte expiré err_pam_auth = erreur d'authentification err_pam_authinfo_unavail = échec de l'obtention des infos utilisateur err_pam_authok_reqd = tiquet expiré err_pam_buf = erreur de mémoire tampon err_pam_cred_err = échec de la modification des identifiants err_pam_cred_expired = identifiants expirés err_pam_cred_insufficient = identifiants insuffisants err_pam_cred_unavail = échec de l'obtention des identifiants err_pam_maxtries = limite d'essais atteinte err_pam_perm_denied = permission refusée err_pam_session = erreur de session err_pam_sys = erreur système err_pam_user_unknown = utilisateur inconnu err_path = échec de la modification du path err_perm_dir = échec de changement de répertoire err_perm_group = échec du déclassement des permissions de groupe err_perm_user = échec du déclassement des permissions utilisateur err_pwnam = échec de lecture des infos utilisateur err_sleep = échec de l'exécution de la commande de veille err_start = échec de l'exécution de la commande de démarrage err_battery = échec de lecture de l'état de la batterie err_switch_tty = échec du changement de terminal err_tty_ctrl = échec du transfert de contrôle du terminal err_no_users = aucun utilisateur trouvé err_uid_range = échec de récupération dynamique de la plage d'UID err_user_gid = échec de modification du GID err_user_init = échec d'initialisation de l'utilisateur err_user_uid = échec de modification du UID err_xauth = échec de la commande xauth err_xcb_conn = échec de la connexion xcb err_xsessions_dir = échec de la recherche du dossier de sessions err_xsessions_open = échec de l'ouverture du dossier de sessions hibernate = veille prolongée insert = insertion login = identifiant logout = déconnecté no_x11_support = support pour x11 désactivé lors de la compilation normal = normal numlock = verr.num other = autre password = mot de passe restart = redémarrer shell = shell shutdown = éteindre sleep = veille wayland = wayland x11 = x11 xinitrc = xinitrc ================================================ FILE: res/lang/it.ini ================================================ capslock = capslock err_alloc = impossibile allocare memoria err_bounds = indice fuori limite err_chdir = impossibile aprire home directory err_dgn_oob = messaggio log err_domain = dominio non valido err_hostname = impossibile ottenere hostname err_mlock = impossibile ottenere lock per la password in memoria err_null = puntatore nullo err_pam = transazione PAM fallita err_pam_abort = transazione PAM interrotta err_pam_acct_expired = account scaduto err_pam_auth = errore di autenticazione err_pam_authinfo_unavail = impossibile ottenere informazioni utente err_pam_authok_reqd = token scaduto err_pam_buf = errore buffer memoria err_pam_cred_err = impossibile impostare credenziali err_pam_cred_expired = credenziali scadute err_pam_cred_insufficient = credenziali insufficienti err_pam_cred_unavail = impossibile ottenere credenziali err_pam_maxtries = raggiunto limite tentativi err_pam_perm_denied = permesso negato err_pam_session = errore di sessione err_pam_sys = errore di sistema err_pam_user_unknown = utente sconosciuto err_path = impossibile impostare percorso err_perm_dir = impossibile cambiare directory corrente err_perm_group = impossibile ridurre permessi gruppo err_perm_user = impossibile ridurre permessi utente err_pwnam = impossibile ottenere dati utente err_user_gid = impossibile impostare GID utente err_user_init = impossibile inizializzare utente err_user_uid = impossible impostare UID utente err_xsessions_dir = impossibile localizzare cartella sessioni err_xsessions_open = impossibile aprire cartella sessioni login = username logout = scollegato numlock = numlock password = password restart = riavvio shell = shell shutdown = arresto wayland = wayland xinitrc = xinitrc ================================================ FILE: res/lang/ja_JP.ini ================================================ authenticating = 認証中... brightness_down = 明るさを下げる brightness_up = 明るさを上げる capslock = CapsLock err_alloc = メモリ割り当て失敗 err_bounds = 境界外インデックス err_brightness_change = 明るさの変更に失敗しました err_chdir = ホームフォルダを開けませんでした err_config = 設定ファイルを解析できません err_dgn_oob = ログメッセージ err_domain = 無効なドメイン err_empty_password = 空のパスワードは許可されていません err_envlist = 環境変数リストの取得に失敗しました err_hostname = ホスト名の取得に失敗しました err_mlock = パスワードメモリのロックに失敗しました err_null = ヌルポインタ err_numlock = NumLockの設定に失敗しました err_pam = PAMトランザクション失敗 err_pam_abort = PAMトランザクションが中断されました err_pam_acct_expired = アカウントの有効期限が切れています err_pam_auth = 認証エラー err_pam_authinfo_unavail = ユーザー情報の取得に失敗しました err_pam_authok_reqd = トークンの有効期限が切れています err_pam_buf = メモリバッファエラー err_pam_cred_err = 認証情報の設定に失敗しました err_pam_cred_expired = 認証情報の有効期限が切れています err_pam_cred_insufficient = 認証情報が不十分です err_pam_cred_unavail = 認証情報の取得に失敗しました err_pam_maxtries = 最大試行回数に到達しました err_pam_perm_denied = アクセスが拒否されました err_pam_session = セッションエラー err_pam_sys = システムエラー err_pam_user_unknown = 不明なユーザー err_path = パスの設定に失敗しました err_perm_dir = カレントディレクトリの変更に失敗しました err_perm_group = グループ権限のダウングレードに失敗しました err_perm_user = ユーザー権限のダウングレードに失敗しました err_pwnam = ユーザー情報の取得に失敗しました err_sleep = スリープコマンドの実行に失敗しました err_tty_ctrl = TTY制御の転送に失敗しました err_user_gid = ユーザーGIDの設定に失敗しました err_user_init = ユーザーの初期化に失敗しました err_user_uid = ユーザーUIDの設定に失敗しました err_xauth = xauthコマンドの実行に失敗しました err_xcb_conn = XCB接続に失敗しました err_xsessions_dir = セッションフォルダが見つかりませんでした err_xsessions_open = セッションフォルダを開けませんでした insert = 挿入 login = ログイン logout = ログアウト済み no_x11_support = X11サポートはコンパイル時に無効化されています normal = 通常 numlock = NumLock other = その他 password = パスワード restart = 再起動 shell = シェル shutdown = シャットダウン sleep = スリープ wayland = Wayland x11 = X11 xinitrc = xinitrc ================================================ FILE: res/lang/ku.ini ================================================ authenticating = tê piştrastkirin... brightness_down = ronahiyê kêm bike brightness_up = ronahiyê bilind bike capslock = tîpên girdek (capslock) custom = kesane err_alloc = veqetandina bîrê têk çû err_args = argumanên rêzika fermanê nehatin analîzkirin err_autologin_session = danişîna têketina xweber nehate dîtin err_bounds = îndeksa derveyî sînor err_brightness_change = guherandina ronahiyê têk çû err_chdir = vekirina peldanka malê têk çû err_clock_too_long = rêzika demjimêrê pir dirêj e err_config = pela rêkxistinê nehat analîzkirin err_crawl = gerandina pelrêçên danişînê têk çû err_dgn_oob = peyama têketinê err_domain = navpara nederbasdar err_empty_password = borînpeyv nabe ku vala be err_envlist = girtina lîsteya jîngehê (envlist) têk çû err_get_active_tty = girtina tty ya çalak têk çû err_hibernate = fermana cemidaninê nehat xebitandin err_hostname = girtina navê mêvandar têk çû err_inactivity = fermana neçalaktiyê nehat xebitandin err_lock_state = girtina rewşa kilîtkirinê têk çû err_log = vekirina pelê têkeinê têk çû err_mlock = kilîtkirina bîra borînpeyvê têk çû err_null = nîşandera null err_numlock = sazkirina numlock têk çû err_pam = danûstendina pam têk çû err_pam_abort = danûstendina pam hate têkbirin err_pam_acct_expired = dema jimarê derbas bûye err_pam_auth = şaşetiya piştrastkirinê err_pam_authinfo_unavail = zanyariyên bikarhêner nehatin girtin err_pam_authok_reqd = dema nîşandanê derbas bûye err_pam_buf = şaşetiya bîra demkî err_pam_cred_err = sazkirina rastkitinê têk çû err_pam_cred_expired = dema rastkitinê derbas bûye err_pam_cred_insufficient = rastkitinê kêm err_pam_cred_unavail = girtina rastkitinê têk çû err_pam_maxtries = sînorê hewldanên herî bilind hat gihîştin err_pam_perm_denied = mafdayîn hat paşguhkirin err_pam_session = şaşetiya danişînê err_pam_sys = şaşetiya pergalê err_pam_user_unknown = bikarhênerê nenas err_path = sazkirina rêgehê têk çû err_perm_dir = guhertina pelrêçê heyî têk çû err_perm_group = kêmkirina mafdayînên komê têk çû err_perm_user = kêmkirina mafdayînên bikarhêner têk çû err_pwnam = girtina zanyariyên bikarhêner têk çû err_sleep = fermana cemidaninê nehat xebitandin err_start = fermana destpêkirinê nehat xebitandin err_battery = barkirina rewşa betariyê têk çû err_switch_tty = guhertina tty têk çû err_tty_ctrl = guhertina kontrola tty têk çû err_no_users = tu bikarhêner nehatin dîtin err_uid_range = girtina rêjeya dînamîk a sînorê uid têk çû err_user_gid = sazkirina GID a bikarhêner têk çû err_user_init = destpêkirina bikarhêner têk çû err_user_uid = sazkirina UID a bikarhêner têk çû err_xauth = fermana xauth têk çû err_xcb_conn = girêdana xcb têk çû err_xsessions_dir = dîtina peldanka danişînan têk çû err_xsessions_open = vekirina peldanka danişînan têk çû hibernate = bicemidîne insert = têxîne login = têketin logout = derkeve no_x11_support = piştgiriya x11 di dema berhevkirinê de hatiye girtin normal = normal numlock = numlock other = ên din password = borînpeyv restart = ji nû ve bide destpêkirin shell = shell shutdown = vemirîne sleep = têxîne xewê wayland = wayland x11 = x11 xinitrc = xinitrc ================================================ FILE: res/lang/lv.ini ================================================ authenticating = autentificējas... brightness_down = samazināt spilgtumu brightness_up = palielināt spilgtumu capslock = caps lock custom = pielāgots err_alloc = neizdevās atmiņas piešķiršana err_bounds = indekss ārpus robežām err_brightness_change = neizdevās mainīt spilgtumu err_chdir = neizdevās atvērt mājas mapi err_clock_too_long = pulksteņa virkne pārāk gara err_config = neizdevās parsēt konfigurācijas failu err_dgn_oob = žurnāla ziņojums err_domain = nederīgs domēns err_empty_password = tukša parole nav atļauta err_envlist = neizdevās iegūt vides mainīgo sarakstu err_get_active_tty = neizdevās iegūt aktīvo tty err_hostname = neizdevās iegūt hostname err_lock_state = neizdevās iegūt bloķēšanas stāvokli err_log = neizdevās atvērt žurnāla failu err_mlock = neizdevās bloķēt paroles atmiņu err_null = null rādītājs err_numlock = neizdevās iestatīt numlock err_pam = pam transakcija neizdevās err_pam_abort = pam transakcija pārtraukta err_pam_acct_expired = konts novecojis err_pam_auth = autentifikācijas kļūda err_pam_authinfo_unavail = neizdevās iegūt lietotāja informāciju err_pam_authok_reqd = žetons beidzies err_pam_buf = atmiņas bufera kļūda err_pam_cred_err = neizdevās iestatīt akreditācijas datus err_pam_cred_expired = akreditācijas dati novecojuši err_pam_cred_insufficient = nepietiekami akreditācijas dati err_pam_cred_unavail = neizdevās iegūt akreditācijas datus err_pam_maxtries = sasniegts maksimālais mēģinājumu skaits err_pam_perm_denied = piekļuve liegta err_pam_session = sesijas kļūda err_pam_sys = sistēmas kļūda err_pam_user_unknown = nezināms lietotājs err_path = neizdevās iestatīt ceļu err_perm_dir = neizdevās mainīt pašreizējo mapi err_perm_group = neizdevās pazemināt grupas atļaujas err_perm_user = neizdevās pazemināt lietotāja atļaujas err_pwnam = neizdevās iegūt lietotāja informāciju err_sleep = neizdevās izpildīt miega komandu err_battery = neizdevās ielādēt akumulatora stāvokli err_switch_tty = neizdevās pārslēgt tty err_tty_ctrl = tty vadības nodošana neizdevās err_no_users = lietotāji nav atrasti err_user_gid = neizdevās iestatīt lietotāja GID err_user_init = neizdevās inicializēt lietotāju err_user_uid = neizdevās iestatīt lietotāja UID err_xauth = xauth komanda neizdevās err_xcb_conn = xcb savienojums neizdevās err_xsessions_dir = neizdevās atrast sesiju mapi err_xsessions_open = neizdevās atvērt sesiju mapi insert = ievietot login = lietotājs logout = iziet no_x11_support = x11 atbalsts atspējots kompilācijas laikā normal = parastais numlock = numlock other = cits password = parole restart = restartēt shell = terminālis shutdown = izslēgt sleep = snauda wayland = wayland x11 = x11 xinitrc = xinitrc ================================================ FILE: res/lang/normalize_lang_files.py ================================================ #!/usr/bin/env python3 from pathlib import Path from sys import stderr def process_lang_file(path: Path, lang_keys: list[str]) -> None: # read key-value-pairs from lang file into dict existing_entries = {} with open(path, "r", encoding="UTF-8") as fh: while line := fh.readline(): try: key, value = line.split("=", 1) existing_entries[key.strip()] = value.strip() except ValueError: # line does not contain '=' continue # re-write current lang file with entries in order of occurence in `lang_keys` # and with empty lines for missing translations with open(path, "w", encoding="UTF-8") as fh: for item in lang_keys: try: fh.write(f"{item} = {existing_entries[item]}\n") except KeyError: # no translation for `item` yet fh.write("\n") def main() -> None: zig_lang_file = Path(__file__).parent.joinpath("../../src/config/Lang.zig").resolve() if not zig_lang_file.exists(): print(f"ERROR: File '{zig_lang_file.as_posix()}' does not exist. Exiting.", file=stderr) exit(1) # read "language keys" from `zig_lang_file` into list lang_keys = [] with open(zig_lang_file, "r", encoding="UTF-8") as fh: while line := fh.readline(): # only process lines that are not empty or no comments if not (line.strip() == "" or line.startswith("//")): lang_keys.append(line.split(":")[0].strip()) lang_files = [f for f in Path.iterdir(Path(__file__).parent) if f.name.endswith(".ini") and f.is_file()] for file in lang_files: process_lang_file(file, lang_keys) if __name__ == "__main__": main() ================================================ FILE: res/lang/pl.ini ================================================ authenticating = uwierzytelnianie... brightness_down = zmniejsz jasność brightness_up = zwiększ jasność capslock = capslock custom = własny err_alloc = nieudana alokacja pamięci err_autologin_session = nie znaleziono sesji autologowania err_bounds = indeks poza zakresem err_brightness_change = nie udało się zmienić jasności err_chdir = nie udało się otworzyć folderu domowego err_clock_too_long = ciąg znaków zegara jest za długi err_config = nie można przetworzyć pliku konfiguracyjnego err_dgn_oob = wiadomość loga err_domain = niepoprawna domena err_empty_password = puste hasło jest niedozwolone err_envlist = nie udało się pobrać listy zmiennych środowiskowych err_get_active_tty = nie udało się uzyskać aktywnego tty err_hostname = nie udało się uzyskać nazwy hosta err_lock_state = nie udało się uzyskać stanu blokady err_log = nie udało się otworzyć pliku logu err_mlock = nie udało się zablokować pamięci haseł err_null = pusty wskaźnik err_numlock = nie udało się ustawić numlock err_pam = transakcja pam nieudana err_pam_abort = transakcja pam przerwana err_pam_acct_expired = konto wygasło err_pam_auth = błąd uwierzytelniania err_pam_authinfo_unavail = nie udało się zdobyć informacji o użytkowniku err_pam_authok_reqd = token wygasł err_pam_buf = błąd bufora pamięci err_pam_cred_err = nie udało się ustawić uwierzytelnienia err_pam_cred_expired = uwierzytelnienie wygasło err_pam_cred_insufficient = niewystarczające uwierzytelnienie err_pam_cred_unavail = nie udało się uzyskać uwierzytelnienia err_pam_maxtries = osiągnięto limit prób err_pam_perm_denied = odmowa dostępu err_pam_session = błąd sesji err_pam_sys = błąd systemu err_pam_user_unknown = nieznany użytkownik err_path = nie udało się ustawić ścieżki err_perm_dir = nie udało się zmienić obecnego katalogu err_perm_group = nie udało się obniżyć uprawnień grupy err_perm_user = nie udało się obniżyć uprawnień użytkownika err_pwnam = nie udało się uzyskać informacji o użytkowniku err_sleep = nie udało się wykonać polecenia sleep err_battery = nie udało się sprawdzić statusu baterii err_switch_tty = nie można przełączyć tty err_tty_ctrl = nie udało się przekazać kontroli tty err_no_users = nie znaleziono żadnego użytkownika err_user_gid = nie udało się ustawić GID użytkownika err_user_init = nie udało się zainicjalizować użytkownika err_user_uid = nie udało się ustawić UID użytkownika err_xauth = polecenie xauth nie powiodło się err_xcb_conn = połączenie xcb nie powiodło się err_xsessions_dir = nie udało się znaleźć folderu sesji err_xsessions_open = nie udało się otworzyć folderu sesji insert = wstaw login = login logout = wylogowano no_x11_support = wsparcie X11 wyłączone podczas kompilacji normal = normalny numlock = numlock other = inny password = hasło restart = uruchom ponownie shell = powłoka shutdown = wyłącz sleep = uśpij wayland = wayland x11 = x11 xinitrc = xinitrc ================================================ FILE: res/lang/pt.ini ================================================ capslock = capslock err_alloc = erro na atribuição de memória err_bounds = índice fora de limites err_chdir = erro ao abrir a pasta home err_dgn_oob = mensagem de registo err_domain = domínio inválido err_hostname = erro ao obter o nome do host err_mlock = erro de bloqueio de memória err_null = ponteiro nulo err_pam = erro na transação pam err_pam_abort = transação pam abortada err_pam_acct_expired = conta expirada err_pam_auth = erro de autenticação err_pam_authinfo_unavail = erro ao obter informação do utilizador err_pam_authok_reqd = token expirado err_pam_buf = erro de buffer de memória err_pam_cred_err = erro ao definir credenciais err_pam_cred_expired = credenciais expiradas err_pam_cred_insufficient = credenciais insuficientes err_pam_cred_unavail = erro ao obter credenciais err_pam_maxtries = limite máximo de tentativas atingido err_pam_perm_denied = permissão negada err_pam_session = erro de sessão err_pam_sys = erro de sistema err_pam_user_unknown = utilizador desconhecido err_path = erro ao definir o caminho de acesso err_perm_dir = erro ao alterar o diretório atual err_perm_group = erro ao reduzir as permissões do grupo err_perm_user = erro ao reduzir as permissões do utilizador err_pwnam = erro ao obter informação do utilizador err_user_gid = erro ao definir o GID do utilizador err_user_init = erro ao iniciar o utilizador err_user_uid = erro ao definir o UID do utilizador err_xsessions_dir = erro ao localizar a pasta das sessões err_xsessions_open = erro ao abrir a pasta das sessões login = iniciar sessão logout = terminar sessão numlock = numlock password = palavra-passe restart = reiniciar shell = shell shutdown = encerrar wayland = wayland xinitrc = xinitrc ================================================ FILE: res/lang/pt_BR.ini ================================================ capslock = caixa alta err_alloc = alocação de memória malsucedida err_bounds = índice fora de limites err_chdir = não foi possível abrir o diretório home err_dgn_oob = mensagem de log err_domain = domínio inválido err_hostname = não foi possível obter o nome do host err_mlock = bloqueio da memória de senha malsucedido err_null = ponteiro nulo err_pam = transação pam malsucedida err_pam_abort = transação pam abortada err_pam_acct_expired = conta expirada err_pam_auth = erro de autenticação err_pam_authinfo_unavail = não foi possível obter informações do usuário err_pam_authok_reqd = token expirada err_pam_buf = erro de buffer de memória err_pam_cred_err = erro para definir credenciais err_pam_cred_expired = credenciais expiradas err_pam_cred_insufficient = credenciais insuficientes err_pam_cred_unavail = não foi possível obter credenciais err_pam_maxtries = limite máximo de tentativas atingido err_pam_perm_denied = permissão negada err_pam_session = erro de sessão err_pam_sys = erro de sistema err_pam_user_unknown = usuário desconhecido err_path = não foi possível definir o caminho err_perm_dir = não foi possível alterar o diretório atual err_perm_group = não foi possível reduzir as permissões de grupo err_perm_user = não foi possível reduzir as permissões de usuário err_pwnam = não foi possível obter informações do usuário err_user_gid = não foi possível definir o GID do usuário err_user_init = não foi possível iniciar o usuário err_user_uid = não foi possível definir o UID do usuário err_xsessions_dir = não foi possível encontrar a pasta das sessões err_xsessions_open = não foi possível abrir a pasta das sessões login = conectar logout = desconectado numlock = numlock password = senha restart = reiniciar shell = shell shutdown = desligar wayland = wayland xinitrc = xinitrc ================================================ FILE: res/lang/ro.ini ================================================ capslock = capslock err_pam_abort = tranzacţie pam anulată err_pam_acct_expired = cont expirat err_pam_auth = eroare de autentificare err_pam_authinfo_unavail = nu s-au putut obţine informaţii despre utilizator err_pam_authok_reqd = token expirat err_pam_buf = eroare de memorie (buffer) err_pam_cred_err = nu s-au putut seta date de identificare (credentials) err_pam_cred_expired = datele de identificare (credentials) au expirat err_pam_cred_insufficient = date de identificare (credentials) insuficiente err_pam_cred_unavail = nu s-au putut obţine date de indentificare (credentials) err_pam_maxtries = s-a atins numărul maxim de încercări err_pam_perm_denied = acces interzis err_pam_session = eroare de sesiune err_pam_sys = eroare de sistem err_pam_user_unknown = utilizator necunoscut err_perm_dir = nu s-a putut schimba dosarul (folder-ul) curent err_perm_group = nu s-a putut face downgrade permisiunilor de grup err_perm_user = nu s-a putut face downgrade permisiunilor de utilizator login = utilizator logout = opreşte sesiunea numlock = numlock password = parolă restart = resetează shell = shell shutdown = opreşte sistemul wayland = wayland xinitrc = xinitrc ================================================ FILE: res/lang/ru.ini ================================================ authenticating = аутентификация... brightness_down = уменьшить яркость brightness_up = увеличить яркость capslock = capslock custom = пользовательский err_alloc = не удалось выделить память err_autologin_session = не найдена сессия с автологином err_bounds = за пределами индекса err_brightness_change = не удалось изменить яркость err_chdir = не удалось открыть домашнюю папку err_clock_too_long = строка часов слишком длинная err_config = не удалось разобрать файл конфигурации err_dgn_oob = отладочное сообщение (log) err_domain = неверный домен err_empty_password = пустой пароль не допустим err_envlist = не удалось получить список переменных среды err_get_active_tty = не удалось получить активный tty err_hostname = не удалось получить имя хоста err_lock_state = не удалось получить состояние lock err_log = не удалось открыть файл log err_mlock = сбой блокировки памяти err_null = нулевой указатель err_numlock = не удалось установить numlock err_pam = pam транзакция не удалась err_pam_abort = pam транзакция прервана err_pam_acct_expired = срок действия аккаунта истёк err_pam_auth = ошибка аутентификации err_pam_authinfo_unavail = не удалось получить информацию о пользователе err_pam_authok_reqd = токен истёк err_pam_buf = ошибка буфера памяти err_pam_cred_err = не удалось установить полномочия err_pam_cred_expired = полномочия истекли err_pam_cred_insufficient = недостаточно полномочий err_pam_cred_unavail = не удалось получить полномочия err_pam_maxtries = лимит попыток исчерпан err_pam_perm_denied = доступ запрещён err_pam_session = ошибка сессии err_pam_sys = системная ошибка err_pam_user_unknown = неизвестный пользователь err_path = не удалось установить путь err_perm_dir = не удалось изменить текущий каталог err_perm_group = не удалось понизить права доступа группы err_perm_user = не удалось понизить права доступа пользователя err_pwnam = не удалось получить информацию о пользователе err_sleep = не удалось выполнить команду sleep err_battery = не удалось получить статус батареи err_switch_tty = не удалось переключить tty err_tty_ctrl = передача управления tty не удалась err_no_users = пользователи не найдены err_user_gid = не удалось установить GID пользователя err_user_init = не удалось инициализировать пользователя err_user_uid = не удалось установить UID пользователя err_xauth = команда xauth не выполнена err_xcb_conn = ошибка подключения xcb err_xsessions_dir = не удалось найти сессионную папку err_xsessions_open = не удалось открыть сессионную папку insert = вставка login = логин logout = вышел из системы no_x11_support = поддержка x11 отключена во время компиляции normal = обычный numlock = numlock other = прочие password = пароль restart = перезагрузить shell = оболочка shutdown = выключить sleep = сон wayland = wayland x11 = x11 xinitrc = xinitrc ================================================ FILE: res/lang/sr.ini ================================================ capslock = capslock err_alloc = neuspijesna alokacija memorije err_bounds = izvan granica indeksa err_chdir = neuspijesno otvaranje home foldera err_dgn_oob = log poruka err_domain = nevazeci domen err_hostname = neuspijesno trazenje hostname-a err_mlock = neuspijesno zakljucavanje memorije lozinke err_null = null pokazivac err_pam = pam transakcija neuspijesna err_pam_abort = pam transakcija prekinuta err_pam_acct_expired = nalog istekao err_pam_auth = greska pri autentikaciji err_pam_authinfo_unavail = neuspjelo uzimanje informacija o korisniku err_pam_authok_reqd = token istekao err_pam_buf = greska bafera memorije err_pam_cred_err = neuspjelo postavljanje kredencijala err_pam_cred_expired = kredencijali istekli err_pam_cred_insufficient = nedovoljni kredencijali err_pam_cred_unavail = neuspjelo uzimanje kredencijala err_pam_maxtries = dostignut maksimalan broj pokusaja err_pam_perm_denied = nedozovoljeno err_pam_session = greska sesije err_pam_sys = greska sistema err_pam_user_unknown = nepoznat korisnik err_path = neuspjelo postavljanje path-a err_perm_dir = neuspjelo mijenjanje foldera err_perm_group = neuspjesno snizavanje dozvola grupe err_perm_user = neuspijesno snizavanje dozvola korisnika err_pwnam = neuspijesno skupljanje informacija o korisniku err_user_gid = neuspijesno postavljanje korisničkog GID-a err_user_init = neuspijensa inicijalizacija korisnika err_user_uid = neuspijesno postavljanje UID-a korisnika err_xsessions_dir = neuspijesno pronalazenje foldera sesija err_xsessions_open = neuspijesno otvaranje foldera sesija login = korisnik logout = izlogovan numlock = numlock password = lozinka restart = ponovo pokreni shell = shell shutdown = ugasi wayland = wayland xinitrc = xinitrc ================================================ FILE: res/lang/sv.ini ================================================ authenticating = autentiserar... brightness_down = minska ljusstyrka brightness_up = öka ljusstyrka capslock = capslock custom = anpassad err_alloc = minnesallokering misslyckades err_args = tolkning av kommandoargument misslyckades err_autologin_session = autologin-session hittades inte err_bounds = index-värde utanför intervallet err_brightness_change = ändring av ljusstyrka misslyckades err_chdir = misslyckades att öppna hemkatalog err_clock_too_long = klocksträng för lång err_config = tolkning av konfigfil misslyckades err_crawl = genomsökning av sessionskataloger misslyckades err_dgn_oob = loggmeddelande err_domain = ogitlig domän err_empty_password = tomt lösenord godtas ej err_envlist = hämtning av env-lista misslyckades err_get_active_tty = hämtning av aktiv tty misslyckades err_hibernate = vilolägets kommando misslyckades err_hostname = hämtning av hostname misslyckades err_inactivity = inaktivitetslägets kommando misslyckades err_lock_state = hämtning av låsningsstatus misslyckades err_log = öppning av loggfil misslyckades err_mlock = låsning av lösenordsminne misslyckades err_null = null pointer err_numlock = inställning av numlock misslyckades err_pam = pam-transaktion misslyckades err_pam_abort = pam-transaktion avbröts err_pam_acct_expired = kontot har löpt ut err_pam_auth = autentisering misslyckades err_pam_authinfo_unavail = hämtning av användarinformation misslyckades err_pam_authok_reqd = token har löpt ut err_pam_buf = minnesbufferfel err_pam_cred_err = inställning av inloggningsuppgifter misslyckades err_pam_cred_expired = inloggningsuppgifterna har löpt ut err_pam_cred_insufficient = otillräckliga inloggningsuppgifter err_pam_cred_unavail = hämtning av inloggningsuppgifter misslyckades err_pam_maxtries = gränsen för antal försök nådd err_pam_perm_denied = tillstånd nekas err_pam_session = sessionsfel err_pam_sys = systemfel err_pam_user_unknown = okänd användare err_path = inställning av sökväg misslyckades err_perm_dir = byte av nuvarande katalog misslyckades err_perm_group = nedgradering av grupptillstånd misslyckades err_perm_user = nedgradering av användartillstånd misslyckades err_pwnam = hämtning av användarinformation misslyckades err_sleep = strömsparlägets kommando misslyckades err_start = startkommando misslyckades err_battery = hämtning av batteristatus misslyckades err_switch_tty = byte av tty misslyckades err_tty_ctrl = överföring av tty-kontroll misslyckades err_no_users = inga användare hittades err_uid_range = dynamisk hämtning av uid-intervall misslyckades err_user_gid = inställning av användarens GID misslyckades err_user_init = initiering av användare misslyckades err_user_uid = inställning av användarens UID misslyckades err_xauth = xauth-kommando misslyckades err_xcb_conn = xcb-anslutning misslyckades err_xsessions_dir = sessionskatalog hittades inte err_xsessions_open = öppning av sessionskatalog misslyckades hibernate = viloläge insert = infoga login = inloggning logout = utloggad no_x11_support = x11-stöd inaktiverat vid kompilering normal = normal numlock = numlock other = övrig password = lösenord restart = starta om shell = shell shutdown = stäng av sleep = viloläge wayland = wayland x11 = x11 xinitrc = xinitrc ================================================ FILE: res/lang/tr.ini ================================================ brightness_down = parlakligi azalt brightness_up = parlakligi arttir capslock = capslock err_alloc = basarisiz bellek ayirma err_bounds = sinirlarin disinda dizin err_chdir = ev klasoru acilamadi err_dgn_oob = log mesaji err_domain = gecersiz etki alani err_hostname = ana bilgisayar adi alinamadi err_mlock = parola bellegi kilitlenemedi err_null = bos isaretci hatasi err_pam = pam islemi basarisiz oldu err_pam_abort = pam islemi durduruldu err_pam_acct_expired = hesabin suresi dolmus err_pam_auth = kimlik dogrulama hatasi err_pam_authinfo_unavail = kullanici bilgileri getirilirken hata olustu err_pam_authok_reqd = suresi dolmus token err_pam_buf = bellek arabellegi hatasi err_pam_cred_err = kimlik bilgileri ayarlanamadi err_pam_cred_expired = kimlik bilgilerinin suresi dolmus err_pam_cred_insufficient = yetersiz kimlik bilgileri err_pam_cred_unavail = kimlik bilgileri alinamadi err_pam_maxtries = en fazla deneme sinirina ulasildi err_pam_perm_denied = izin reddedildi err_pam_session = oturum hatasi err_pam_sys = sistem hatasi err_pam_user_unknown = bilinmeyen kullanici err_path = yol ayarlanamadi err_perm_dir = gecerli dizin degistirilemedi err_perm_group = grup izinleri dusurulemedi err_perm_user = kullanici izinleri dusurulemedi err_pwnam = kullanici bilgileri alinamadi err_user_gid = kullanici icin GID ayarlanamadi err_user_init = kullanici oturumu baslatilamadi err_user_uid = kullanici icin UID ayarlanamadi err_xsessions_dir = oturumlar klasoru bulunamadi err_xsessions_open = oturumlar klasoru acilamadi hibernate = askiya al login = kullanici logout = oturumdan cikis yapildi numlock = numlock other = baska password = sifre restart = yeniden baslat shell = shell shutdown = makineyi kapat sleep = uykuya al wayland = wayland xinitrc = xinitrc ================================================ FILE: res/lang/uk.ini ================================================ capslock = capslock err_alloc = невдале виділення пам'яті err_bounds = поза межами індексу err_chdir = не вдалося відкрити домашній каталог err_dgn_oob = повідомлення журналу (log) err_domain = недійсний домен err_hostname = не вдалося отримати ім'я хосту err_mlock = збій блокування пам'яті err_null = нульовий вказівник err_pam = невдала pam транзакція err_pam_abort = pam транзакція перервана err_pam_acct_expired = термін дії акаунту вичерпано err_pam_auth = помилка автентифікації err_pam_authinfo_unavail = не вдалося отримати дані користувача err_pam_authok_reqd = термін дії токена вичерпано err_pam_buf = помилка буферу пам'яті err_pam_cred_err = не вдалося змінити облікові дані err_pam_cred_expired = термін дії повноважень вичерпано err_pam_cred_insufficient = недостатньо облікових даних err_pam_cred_unavail = не вдалося отримати облікові дані err_pam_maxtries = вичерпано ліміт спроб err_pam_perm_denied = відмовлено у доступі err_pam_session = помилка сесії err_pam_sys = системна помилка err_pam_user_unknown = невідомий користувач err_path = не вдалося змінити шлях err_perm_dir = не вдалося змінити поточний каталог err_perm_group = не вдалося понизити права доступу групи err_perm_user = не вдалося понизити права доступу користувача err_pwnam = не вдалося отримати дані користувача err_user_gid = не вдалося змінити GID користувача err_user_init = не вдалося ініціалізувати користувача err_user_uid = не вдалося змінити UID користувача err_xsessions_dir = не вдалося знайти каталог сесій err_xsessions_open = не вдалося відкрити каталог сесій login = логін logout = вийти numlock = numlock password = пароль restart = перезавантажити shell = оболонка shutdown = вимкнути wayland = wayland xinitrc = xinitrc ================================================ FILE: res/lang/zh_CN.ini ================================================ capslock = 大写锁定 err_alloc = 内存分配失败 err_bounds = 索引越界 err_chdir = 无法打开home文件夹 err_dgn_oob = 日志消息 err_domain = 无效的域 err_hostname = 获取主机名失败 err_mlock = 锁定密码存储器失败 err_null = 空指针 err_pam = PAM事件失败 err_pam_abort = PAM事务已中止 err_pam_acct_expired = 帐户已过期 err_pam_auth = 身份验证错误 err_pam_authinfo_unavail = 获取用户信息失败 err_pam_authok_reqd = 口令已过期 err_pam_buf = 内存缓冲区错误 err_pam_cred_err = 设置凭据失败 err_pam_cred_expired = 凭据已过期 err_pam_cred_insufficient = 凭据不足 err_pam_cred_unavail = 无法获取凭据 err_pam_maxtries = 已达到最大尝试次数限制 err_pam_perm_denied = 拒绝访问 err_pam_session = 会话错误 err_pam_sys = 系统错误 err_pam_user_unknown = 未知用户 err_path = 无法设置路径 err_perm_dir = 更改当前目录失败 err_perm_group = 组权限降级失败 err_perm_user = 用户权限降级失败 err_pwnam = 获取用户信息失败 err_user_gid = 设置用户GID失败 err_user_init = 初始化用户失败 err_user_uid = 设置用户UID失败 err_xsessions_dir = 找不到会话文件夹 err_xsessions_open = 无法打开会话文件夹 login = 登录 logout = 注销 numlock = 数字锁定 password = 密码 shell = shell wayland = wayland x11 = x11 xinitrc = xinitrc ================================================ FILE: res/ly-dinit ================================================ type = process restart = true smooth-recovery = true command = $PREFIX_DIRECTORY/bin/$EXECUTABLE_NAME depends-on = login.target termsignal = HUP # ly needs access to the console while login.target already occupies it options = shares-console ================================================ FILE: res/ly-freebsd-wrapper ================================================ #!/bin/sh # On FreeBSD, even if we override the default login program, getty will still # try to append "login -fp root" as arguments to Ly, which is not supported. # To avoid this, we use a wrapper script that ignores these arguments before # actually executing Ly. exec $PREFIX_DIRECTORY/bin/$EXECUTABLE_NAME ================================================ FILE: res/ly-kmsconvt@.service ================================================ [Unit] Description=TUI display manager using KMSCON After=systemd-user-sessions.service plymouth-quit-wait.service After=kmsconvt@%i.service Conflicts=kmsconvt@%i.service [Service] ExecStart=$PREFIX_DIRECTORY/bin/kmscon --font-engine unifont --vt=%I --seats=seat0 --login -- $PREFIX_DIRECTORY/bin/ly --use-kmscon-vt StandardInput=tty UtmpIdentifier=%I TTYPath=/dev/%I TTYReset=yes TTYVHangup=yes TTYVTDisallocate=yes [Install] WantedBy=multi-user.target ================================================ FILE: res/ly-openrc ================================================ #!/sbin/openrc-run name="ly" description="TUI Display Manager" ## Supervisor daemon supervisor=supervise-daemon respawn_period=60 pidfile=/run/"${RC_SVCNAME}.pid" ## Check for getty or agetty if [ -x /sbin/getty ] || [ -x /bin/getty ]; then # busybox commandB="/sbin/getty" elif [ -x /sbin/agetty ] || [ -x /bin/agetty ]; then # util-linux commandUL="/sbin/agetty" fi ## The execution vars TTY="tty$DEFAULT_TTY" TERM=linux BAUD=38400 # If we don't have getty then we should have agetty command=${commandB:-$commandUL} command_args_foreground="-nl $PREFIX_DIRECTORY/bin/$EXECUTABLE_NAME $TTY $BAUD $TERM" depend() { after agetty provide display-manager want elogind } ================================================ FILE: res/ly-runit-service/conf ================================================ if [ -x /sbin/agetty -o -x /bin/agetty ]; then # util-linux specific settings if [ "${tty}" = "tty1" ]; then GETTY_ARGS="--noclear" fi fi BAUD_RATE=38400 TERM_NAME=linux TTY=tty$DEFAULT_TTY ================================================ FILE: res/ly-runit-service/finish ================================================ #!/bin/sh [ -r conf ] && . ./conf exec utmpset -w ${TTY} ================================================ FILE: res/ly-runit-service/run ================================================ #!/bin/sh [ -r conf ] && . ./conf if [ -x /sbin/getty -o -x /bin/getty ]; then # busybox GETTY=getty elif [ -x /sbin/agetty -o -x /bin/agetty ]; then # util-linux GETTY=agetty fi exec setsid ${GETTY} ${GETTY_ARGS} -nl $PREFIX_DIRECTORY/bin/$EXECUTABLE_NAME "${TTY}" "${BAUD_RATE}" "${TERM_NAME}" ================================================ FILE: res/ly-s6/run ================================================ #!/bin/execlineb -P exec agetty -L -8 -n -l $PREFIX_DIRECTORY/bin/$EXECUTABLE_NAME tty$DEFAULT_TTY 115200 ================================================ FILE: res/ly-s6/type ================================================ longrun ================================================ FILE: res/ly-sysvinit ================================================ #!/bin/sh ### BEGIN INIT INFO # Provides: ly # Required-Start: $remote_fs $syslog # Required-Stop: $remote_fs $syslog # Default-Start: 2 3 4 5 # Default-Stop: 0 1 6 # Short-Description: Ly display manager # Description: Starts and stops the Ly display manager ### END INIT INFO # # Author: AnErrupTion # PATH=/sbin:/usr/sbin:/bin:/usr/bin DAEMON=/usr/bin/ly TTY=/dev/tty$DEFAULT_TTY PIDFILE=/var/run/ly.pid NAME=ly DESC="Ly display manager" . /lib/lsb/init-functions case "$1" in start) log_daemon_msg "Starting $DESC on $TTY..." if [ -f "$PIDFILE" ]; then log_progress_msg "$DESC is already running" log_end_msg 0 return 0 fi # Ensure TTY exists [ -c "$TTY" ] || { log_failure_msg "$TTY does not exist" return 1 } start-stop-daemon --start --background --make-pidfile --pidfile $PIDFILE \ --chdir / --exec /bin/sh -- -c "exec setsid sh -c 'exec <$TTY >$TTY 2>&1 $DAEMON'" log_end_msg $? ;; stop) log_daemon_msg "Stopping $DESC..." start-stop-daemon --stop --pidfile $PIDFILE --retry 5 RETVAL=$? [ $RETVAL -eq 0 ] && rm -f "$PIDFILE" log_end_msg $RETVAL ;; restart) echo "Restarting $DESC..." $0 stop sleep 1 $0 start ;; status) status_of_proc -p $PIDFILE $DAEMON $NAME && exit 0 || exit $? ;; *) echo "Usage: /etc/init.d/$NAME {start|stop|restart|status}" exit 1 ;; esac exit 0 ================================================ FILE: res/ly@.service ================================================ [Unit] Description=TUI display manager After=systemd-user-sessions.service plymouth-quit-wait.service After=getty@%i.service Conflicts=getty@%i.service [Service] Type=idle ExecStart=$PREFIX_DIRECTORY/bin/$EXECUTABLE_NAME StandardInput=tty TTYPath=/dev/%I TTYReset=yes TTYVHangup=yes [Install] WantedBy=multi-user.target ================================================ FILE: res/pam.d/ly-freebsd ================================================ #%PAM-1.0 # OpenPAM (used in FreeBSD) doesn't support prepending "-" for ignoring missing # modules. auth include login account include login password include login session include login ================================================ FILE: res/pam.d/ly-freebsd-autologin ================================================ #%PAM-1.0 # OpenPAM (used in FreeBSD) doesn't support prepending "-" for ignoring missing # modules. auth required pam_permit.so auth include login account include login password include login session include login ================================================ FILE: res/pam.d/ly-linux ================================================ #%PAM-1.0 auth include login -auth optional pam_gnome_keyring.so -auth optional pam_kwallet5.so account include login password include login -password optional pam_gnome_keyring.so use_authtok -session optional pam_systemd.so class=greeter -session optional pam_elogind.so session include login -session optional pam_gnome_keyring.so auto_start -session optional pam_kwallet5.so auto_start ================================================ FILE: res/pam.d/ly-linux-autologin ================================================ #%PAM-1.0 auth required pam_permit.so -auth optional pam_gnome_keyring.so -auth optional pam_kwallet5.so account include login password include login -password optional pam_gnome_keyring.so use_authtok -session optional pam_systemd.so class=greeter -session optional pam_elogind.so session include login -session optional pam_gnome_keyring.so auto_start -session optional pam_kwallet5.so auto_start ================================================ FILE: res/setup.sh ================================================ #!/bin/sh # Shell environment setup after login # Copyright (C) 2015-2016 Pier Luigi Fiorini # This file is extracted from kde-workspace (kdm/kfrontend/genkdmconf.c) # Copyright (C) 2001-2005 Oswald Buddenhagen # Copyright (C) 2024 The Fairy Glade # This work is free. You can redistribute it and/or modify it under the # terms of the Do What The Fuck You Want To Public License, Version 2, # as published by Sam Hocevar. See the LICENSE file for more details. # Note that the respective logout scripts are not sourced. case $SHELL in */bash) [ -z "$BASH" ] && exec $SHELL "$0" "$@" set +o posix [ -f "$CONFIG_DIRECTORY"/profile ] && . "$CONFIG_DIRECTORY"/profile if [ -f "$HOME"/.bash_profile ]; then . "$HOME"/.bash_profile elif [ -f "$HOME"/.bash_login ]; then . "$HOME"/.bash_login elif [ -f "$HOME"/.profile ]; then . "$HOME"/.profile fi ;; */zsh) [ -z "$ZSH_NAME" ] && exec $SHELL "$0" "$@" [ -d "$CONFIG_DIRECTORY"/zsh ] && zdir="$CONFIG_DIRECTORY"/zsh || zdir="$CONFIG_DIRECTORY" zhome=${ZDOTDIR:-"$HOME"} # zshenv is always sourced automatically. [ -f "$zdir"/zprofile ] && . "$zdir"/zprofile [ -f "$zhome"/.zprofile ] && . "$zhome"/.zprofile [ -f "$zdir"/zlogin ] && . "$zdir"/zlogin [ -f "$zhome"/.zlogin ] && . "$zhome"/.zlogin emulate -R sh ;; */csh|*/tcsh) # [t]cshrc is always sourced automatically. # Note that sourcing csh.login after .cshrc is non-standard. sess_tmp=$(mktemp /tmp/sess-env-XXXXXX) $SHELL -c "if (-f $CONFIG_DIRECTORY/csh.login) source $CONFIG_DIRECTORY/csh.login; if (-f ~/.login) source ~/.login; /bin/sh -c 'export -p' >! $sess_tmp" . "$sess_tmp" rm -f "$sess_tmp" ;; */fish) [ -f "$CONFIG_DIRECTORY"/profile ] && . "$CONFIG_DIRECTORY"/profile [ -f "$HOME"/.profile ] && . "$HOME"/.profile sess_tmp=$(mktemp /tmp/sess-env-XXXXXX) $SHELL --login -c "/bin/sh -c 'export -p' > $sess_tmp" . "$sess_tmp" rm -f "$sess_tmp" ;; *) # Plain sh, ksh, and anything we do not know. [ -f "$CONFIG_DIRECTORY"/profile ] && . "$CONFIG_DIRECTORY"/profile [ -f "$HOME"/.profile ] && . "$HOME"/.profile ;; esac if [ "$XDG_SESSION_TYPE" = "x11" ]; then [ -f "$CONFIG_DIRECTORY"/xprofile ] && . "$CONFIG_DIRECTORY"/xprofile [ -f "$HOME"/.xprofile ] && . "$HOME"/.xprofile # run all system xinitrc shell scripts. if [ -d "$CONFIG_DIRECTORY"/X11/xinit/xinitrc.d ]; then for i in "$CONFIG_DIRECTORY"/X11/xinit/xinitrc.d/* ; do if [ -x "$i" ]; then . "$i" fi done fi # Load Xsession scripts # OPTIONFILE, USERXSESSION, USERXSESSIONRC and ALTUSERXSESSION are required # by the scripts to work xsessionddir="$CONFIG_DIRECTORY"/X11/Xsession.d export OPTIONFILE="$CONFIG_DIRECTORY"/X11/Xsession.options export USERXSESSION="$HOME"/.xsession export USERXSESSIONRC="$HOME"/.xsessionrc export ALTUSERXSESSION="$HOME"/.Xsession if [ -d "$xsessionddir" ]; then for i in $(ls "$xsessionddir"); do script="$xsessionddir/$i" echo "Loading X session script $script" if [ -r "$script" ] && [ -f "$script" ] && expr "$i" : '^[[:alnum:]_-]\+$' > /dev/null; then . "$script" fi done fi if [ -f "$USERXSESSION" ]; then . "$USERXSESSION" fi if [ -d "$CONFIG_DIRECTORY"/X11/Xresources ]; then for i in "$CONFIG_DIRECTORY"/X11/Xresources/*; do [ -f "$i" ] && xrdb -merge "$i" done elif [ -f "$CONFIG_DIRECTORY"/X11/Xresources ]; then xrdb -merge "$CONFIG_DIRECTORY"/X11/Xresources fi [ -f "$HOME"/.Xresources ] && xrdb -merge "$HOME"/.Xresources [ -f "$XDG_CONFIG_HOME"/X11/Xresources ] && xrdb -merge "$XDG_CONFIG_HOME"/X11/Xresources fi exec "$@" ================================================ FILE: res/startup.sh ================================================ #!/bin/sh # This file is executed when starting Ly (before the TTY is taken control of) # Custom startup code can be placed in this file or the start_cmd var can be pointed to a different file # Uncomment the example below for an example of changing the default TTY colors to an alternitive palette on linux # Colors are in red/green/blue hex (the current colors are a brighter palette than default) # # if [ "$TERM" = "linux" ]; then # BLACK="232323" # DARK_RED="D75F5F" # DARK_GREEN="87AF5F" # DARK_YELLOW="D7AF87" # DARK_BLUE="8787AF" # DARK_MAGENTA="BD53A5" # DARK_CYAN="5FAFAF" # LIGHT_GRAY="E5E5E5" # DARK_GRAY="2B2B2B" # RED="E33636" # GREEN="98E34D" # YELLOW="FFD75F" # BLUE="7373C9" # MAGENTA="D633B2" # CYAN="44C9C9" # WHITE="FFFFFF" # COLORS="${BLACK} ${DARK_RED} ${DARK_GREEN} ${DARK_YELLOW} ${DARK_BLUE} ${DARK_MAGENTA} ${DARK_CYAN} ${LIGHT_GRAY} ${DARK_GRAY} ${RED} ${GREEN} ${YELLOW} ${BLUE} ${MAGENTA} ${CYAN} ${WHITE}" # i=0 # while [ $i -lt 16 ]; do # printf "\033]P%x%s" ${i} "$(echo "$COLORS" | cut -d ' ' -f$(( i + 1)))" # i=$(( i + 1 )) # done # clear # for fixing background artifacting after changing color # fi ================================================ FILE: src/Environment.zig ================================================ const ini = @import("ly-ui").ly_core.ini; const Ini = ini.Ini; const enums = @import("enums.zig"); const DisplayServer = enums.DisplayServer; pub const DesktopEntry = struct { Exec: []const u8 = "", Name: []const u8 = "", DesktopNames: ?[]u8 = null, Terminal: ?bool = null, }; pub const Entry = struct { @"Desktop Entry": DesktopEntry = .{} }; entry_ini: ?Ini(Entry) = null, file_name: []const u8 = "", name: []const u8 = "", xdg_session_desktop: ?[]const u8 = null, xdg_desktop_names: ?[]const u8 = null, cmd: ?[]const u8 = null, specifier: []const u8 = "", display_server: DisplayServer = .wayland, is_terminal: bool = false, ================================================ FILE: src/animations/Cascade.zig ================================================ const std = @import("std"); const math = std.math; const ly_ui = @import("ly-ui"); const Cell = ly_ui.Cell; const TerminalBuffer = ly_ui.TerminalBuffer; const Widget = ly_ui.Widget; const Cascade = @This(); buffer: *TerminalBuffer, current_auth_fails: *usize, max_auth_fails: usize, pub fn init( buffer: *TerminalBuffer, current_auth_fails: *usize, max_auth_fails: usize, ) Cascade { return .{ .buffer = buffer, .current_auth_fails = current_auth_fails, .max_auth_fails = max_auth_fails, }; } pub fn widget(self: *Cascade) Widget { return Widget.init( "Cascade", null, self, null, null, draw, null, null, null, ); } fn draw(self: *Cascade) void { while (self.current_auth_fails.* >= self.max_auth_fails) { std.Thread.sleep(std.time.ns_per_ms * 10); var changed = false; var y = self.buffer.height - 2; while (y > 0) : (y -= 1) { for (0..self.buffer.width) |x| { const cell = TerminalBuffer.getCell(x, y - 1); const cell_under = TerminalBuffer.getCell(x, y); // This shouldn't happen under normal circumstances, but because // this is a *secret* animation, there's no need to care that much if (cell == null or cell_under == null) continue; const char: u8 = @truncate(cell.?.ch); if (std.ascii.isWhitespace(char)) continue; const char_under: u8 = @truncate(cell_under.?.ch); if (!std.ascii.isWhitespace(char_under)) continue; changed = true; if ((self.buffer.random.int(u16) % 10) > 7) continue; cell.?.put(x, y); var space = Cell.init( ' ', cell_under.?.fg, cell_under.?.bg, ); space.put(x, y - 1); } } if (!changed) { std.Thread.sleep(std.time.ns_per_s * 7); self.current_auth_fails.* = 0; } TerminalBuffer.presentBuffer(); } } ================================================ FILE: src/animations/ColorMix.zig ================================================ const std = @import("std"); const math = std.math; const ly_ui = @import("ly-ui"); const Cell = ly_ui.Cell; const TerminalBuffer = ly_ui.TerminalBuffer; const Widget = ly_ui.Widget; const ly_core = ly_ui.ly_core; const interop = ly_core.interop; const TimeOfDay = interop.TimeOfDay; const ColorMix = @This(); const Vec2 = @Vector(2, f32); const time_scale: f32 = 0.01; const palette_len: usize = 12; fn length(vec: Vec2) f32 { return math.sqrt(vec[0] * vec[0] + vec[1] * vec[1]); } start_time: TimeOfDay, terminal_buffer: *TerminalBuffer, animate: *bool, timeout_sec: u12, frame_delay: u16, frames: u64, pattern_cos_mod: f32, pattern_sin_mod: f32, palette: [palette_len]Cell, pub fn init( terminal_buffer: *TerminalBuffer, col1: u32, col2: u32, col3: u32, animate: *bool, timeout_sec: u12, frame_delay: u16, ) !ColorMix { return .{ .start_time = try interop.getTimeOfDay(), .terminal_buffer = terminal_buffer, .animate = animate, .timeout_sec = timeout_sec, .frame_delay = frame_delay, .frames = 0, .pattern_cos_mod = terminal_buffer.random.float(f32) * math.pi * 2.0, .pattern_sin_mod = terminal_buffer.random.float(f32) * math.pi * 2.0, .palette = [palette_len]Cell{ Cell.init(0x2588, col1, col2), Cell.init(0x2593, col1, col2), Cell.init(0x2592, col1, col2), Cell.init(0x2591, col1, col2), Cell.init(0x2588, col2, col3), Cell.init(0x2593, col2, col3), Cell.init(0x2592, col2, col3), Cell.init(0x2591, col2, col3), Cell.init(0x2588, col3, col1), Cell.init(0x2593, col3, col1), Cell.init(0x2592, col3, col1), Cell.init(0x2591, col3, col1), }, }; } pub fn widget(self: *ColorMix) Widget { return Widget.init( "ColorMix", null, self, null, null, draw, update, null, calculateTimeout, ); } fn draw(self: *ColorMix) void { if (!self.animate.*) return; self.frames +%= 1; const time: f32 = @as(f32, @floatFromInt(self.frames)) * time_scale; for (0..self.terminal_buffer.width) |x| { for (0..self.terminal_buffer.height) |y| { const xi: i32 = @intCast(x); const yi: i32 = @intCast(y); const wi: i32 = @intCast(self.terminal_buffer.width); const hi: i32 = @intCast(self.terminal_buffer.height); var uv: Vec2 = .{ @as(f32, @floatFromInt(xi * 2 - wi)) / @as(f32, @floatFromInt(self.terminal_buffer.height * 2)), @as(f32, @floatFromInt(yi * 2 - hi)) / @as(f32, @floatFromInt(self.terminal_buffer.height)), }; var uv2: Vec2 = @splat(uv[0] + uv[1]); for (0..3) |_| { uv2 += uv + @as(Vec2, @splat(length(uv))); uv += @as(Vec2, @splat(0.5)) * Vec2{ math.cos(self.pattern_cos_mod + uv2[1] * 0.2 + time * 0.1), math.sin(self.pattern_sin_mod + uv2[0] - time * 0.1), }; uv -= @splat(1.0 * math.cos(uv[0] + uv[1]) - math.sin(uv[0] * 0.7 - uv[1])); } const cell = self.palette[@as(usize, @intFromFloat(math.floor(length(uv) * 5.0))) % palette_len]; cell.put(x, y); } } } fn update(self: *ColorMix, _: *anyopaque) !void { const time = try interop.getTimeOfDay(); if (self.timeout_sec > 0 and time.seconds - self.start_time.seconds > self.timeout_sec) { self.animate.* = false; } } fn calculateTimeout(self: *ColorMix, _: *anyopaque) !?usize { return self.frame_delay; } ================================================ FILE: src/animations/Doom.zig ================================================ const std = @import("std"); const Allocator = std.mem.Allocator; const ly_ui = @import("ly-ui"); const Cell = ly_ui.Cell; const TerminalBuffer = ly_ui.TerminalBuffer; const Widget = ly_ui.Widget; const ly_core = ly_ui.ly_core; const interop = ly_core.interop; const TimeOfDay = interop.TimeOfDay; const Doom = @This(); pub const STEPS = 12; pub const HEIGHT_MAX = 9; pub const SPREAD_MAX = 4; start_time: TimeOfDay, allocator: Allocator, terminal_buffer: *TerminalBuffer, animate: *bool, timeout_sec: u12, frame_delay: u16, buffer: []u8, height: u8, spread: u8, fire: [STEPS + 1]Cell, pub fn init( allocator: Allocator, terminal_buffer: *TerminalBuffer, top_color: u32, middle_color: u32, bottom_color: u32, fire_height: u8, fire_spread: u8, animate: *bool, timeout_sec: u12, frame_delay: u16, ) !Doom { const buffer = try allocator.alloc(u8, terminal_buffer.width * terminal_buffer.height); initBuffer(buffer, terminal_buffer.width); const levels = [_]Cell{ Cell.init(' ', terminal_buffer.bg, terminal_buffer.bg), Cell.init(0x2591, top_color, terminal_buffer.bg), Cell.init(0x2592, top_color, terminal_buffer.bg), Cell.init(0x2593, top_color, terminal_buffer.bg), Cell.init(0x2588, top_color, terminal_buffer.bg), Cell.init(0x2591, middle_color, top_color), Cell.init(0x2592, middle_color, top_color), Cell.init(0x2593, middle_color, top_color), Cell.init(0x2588, middle_color, top_color), Cell.init(0x2591, bottom_color, middle_color), Cell.init(0x2592, bottom_color, middle_color), Cell.init(0x2593, bottom_color, middle_color), Cell.init(0x2588, bottom_color, middle_color), }; return .{ .start_time = try interop.getTimeOfDay(), .allocator = allocator, .terminal_buffer = terminal_buffer, .animate = animate, .timeout_sec = timeout_sec, .frame_delay = frame_delay, .buffer = buffer, .height = @min(HEIGHT_MAX, fire_height), .spread = @min(SPREAD_MAX, fire_spread), .fire = levels, }; } pub fn widget(self: *Doom) Widget { return Widget.init( "Doom", null, self, deinit, realloc, draw, update, null, calculateTimeout, ); } fn deinit(self: *Doom) void { self.allocator.free(self.buffer); } fn realloc(self: *Doom) !void { const buffer = try self.allocator.realloc(self.buffer, self.terminal_buffer.width * self.terminal_buffer.height); initBuffer(buffer, self.terminal_buffer.width); self.buffer = buffer; } fn draw(self: *Doom) void { if (!self.animate.*) return; for (0..self.terminal_buffer.width) |x| { // We start from 1 so that we always have the topmost line when spreading fire for (1..self.terminal_buffer.height) |y| { // Get index of current cell in fire level buffer const from = y * self.terminal_buffer.width + x; // Generate random data for fire propagation const rand_loss = self.terminal_buffer.random.intRangeAtMost(u8, 0, HEIGHT_MAX); const rand_spread = self.terminal_buffer.random.intRangeAtMost(u8, 0, self.spread * 2); // Select semi-random target cell const to = from -| self.terminal_buffer.width + self.spread -| rand_spread; const to_x = to % self.terminal_buffer.width; const to_y = to / self.terminal_buffer.width; // Get fire level of current cell const level_buf_from = self.buffer[from]; // Choose new fire level and store in level buffer const level_buf_to = level_buf_from -| @intFromBool(rand_loss >= self.height); self.buffer[to] = level_buf_to; // Send known fire levels to terminal buffer const from_cell = self.fire[level_buf_from]; const to_cell = self.fire[level_buf_to]; from_cell.put(x, y); to_cell.put(to_x, to_y); } // Draw bottom line (fire source) const src_cell = self.fire[STEPS]; src_cell.put(x, self.terminal_buffer.height - 1); } } fn initBuffer(buffer: []u8, width: usize) void { const length = buffer.len - width; const slice_start = buffer[0..length]; const slice_end = buffer[length..]; // Initialize the framebuffer in black, except for the "fire source" as the // last color @memset(slice_start, 0); @memset(slice_end, STEPS); } fn update(self: *Doom, _: *anyopaque) !void { const time = try interop.getTimeOfDay(); if (self.timeout_sec > 0 and time.seconds - self.start_time.seconds > self.timeout_sec) { self.animate.* = false; } } fn calculateTimeout(self: *Doom, _: *anyopaque) !?usize { return self.frame_delay; } ================================================ FILE: src/animations/DurFile.zig ================================================ const std = @import("std"); const Allocator = std.mem.Allocator; const Json = std.json; const eql = std.mem.eql; const flate = std.compress.flate; const ly_ui = @import("ly-ui"); const Cell = ly_ui.Cell; const TerminalBuffer = ly_ui.TerminalBuffer; const Color = TerminalBuffer.Color; const Styling = TerminalBuffer.Styling; const Widget = ly_ui.Widget; const ly_core = ly_ui.ly_core; const interop = ly_core.interop; const TimeOfDay = interop.TimeOfDay; const LogFile = ly_core.LogFile; const enums = @import("../enums.zig"); const DurOffsetAlignment = enums.DurOffsetAlignment; 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, delay: f32, contents: [][]u8, colorMap: [][][]i32, // allocator must be outside of struct as it will fail the json parser pub fn deinit(self: *const Frame, allocator: Allocator) void { for (self.contents) |con| { allocator.free(con); } allocator.free(self.contents); for (self.colorMap) |cm| { for (cm) |int2| { allocator.free(int2); } allocator.free(cm); } allocator.free(self.colorMap); } }; // https://github.com/cmang/durdraw/blob/0.29.0/durformat.md const DurFormat = struct { allocator: Allocator, formatVersion: ?i64 = null, colorFormat: ?[]const u8 = null, encoding: ?[]const u8 = null, framerate: ?f64 = null, columns: ?i64 = null, lines: ?i64 = null, frames: std.ArrayList(Frame) = undefined, pub fn valid(self: *DurFormat) bool { if (self.formatVersion != null and self.colorFormat != null and self.encoding != null and self.framerate != null and self.columns != null and self.lines != null and self.frames.items.len >= 1) { // v8 may have breaking changes like changing the colormap xy direction // (https://github.com/cmang/durdraw/issues/24) if (self.formatVersion.? != 7) return false; // Code currently only supports 16 and 256 color format only if (!(eql(u8, "16", self.colorFormat.?) or eql(u8, "256", self.colorFormat.?))) return false; // Code currently supports only utf-8 encoding if (!eql(u8, self.encoding.?, "utf-8")) return false; // Sanity check on file if (self.columns.? <= 0) return false; if (self.lines.? <= 0) return false; if (self.framerate.? < 0) return false; return true; } return false; } 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; // 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; self.colorFormat = if (dur_movie.get("colorFormat")) |x| try allocator.dupe(u8, x.string) else null; self.encoding = if (dur_movie.get("encoding")) |x| try allocator.dupe(u8, x.string) else null; self.framerate = if (dur_movie.get("framerate")) |x| x.float else null; self.columns = if (dur_movie.get("columns")) |x| x.integer else if (dur_movie.get("sizeX")) |x| x.integer else null; self.lines = if (dur_movie.get("lines")) |x| x.integer else if (dur_movie.get("sizeY")) |x| x.integer else null; const frames = dur_movie.get("frames") orelse return error.NotValidFile; self.frames = try .initCapacity(allocator, frames.array.items.len); 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 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..frame.contents.len) |i| { frame.contents[i] = try allocator.dupe(u8, frame_val.contents[i]); } // 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, frame); } } pub fn create_from_file(self: *DurFormat, allocator: Allocator, file_path: []const u8) !void { 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, .{}); defer parsed.deinit(); try parse_dur_from_json(self, allocator, parsed.value); if (!self.valid()) { return error.NotValidFile; } } pub fn init(allocator: Allocator) DurFormat { return .{ .allocator = allocator }; } pub fn deinit(self: *DurFormat) void { 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); } self.frames.deinit(self.allocator); } }; const tb_color_16 = [16]u32{ 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{ Color.DEFAULT, // DEFAULT instead of TRUE_BLACK to not break compositors (the latter 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.DEFAULT | Styling.BOLD, 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 // Array indexes are dur colormappings which value maps to indexes in table above. Only needed for dur 16 color const durcolor_table_to_color16 = [17]u32{ 0, // 0 black 0, // 1 nothing?? dur source did not say why 1 is unused 4, // 2 blue 2, // 3 green 6, // 4 cyan 1, // 5 red 5, // 6 magenta 3, // 7 yellow 7, // 8 light gray 8, // 9 gray 12, // 10 bright blue 10, // 11 bright green 14, // 12 bright cyan 9, // 13 bright red 13, // 14 bright magenta 11, // 15 bright yellow 15, // 16 bright white }; fn sixcube_to_channel(sixcube: u32) u32 { // 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) { // 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 { // 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); } return rgb_color; } const UVec2 = @Vector(2, u32); const IVec2 = @Vector(2, i64); const VEC_X = 0; const VEC_Y = 1; const DurFile = @This(); start_time: TimeOfDay, allocator: Allocator, terminal_buffer: *TerminalBuffer, dur_movie: DurFormat, frames: u64, frame_size: UVec2, start_pos: IVec2, full_color: bool, animate: *bool, timeout_sec: u12, frame_delay: u16, frame_time: u32, time_previous: i64, is_color_format_16: bool, offset_alignment: DurOffsetAlignment, offset: IVec2, // if the user has an even number of columns or rows, we will default to the left or higher position (e.g. 4 columns center = .x..) fn center(v: u32) i64 { return @intCast((v / 2) + (v % 2)); } fn calc_start_position(terminal_buffer: *TerminalBuffer, dur_movie: *DurFormat, offset_alignment: DurOffsetAlignment, offset: IVec2) IVec2 { const buf_width: u32 = @intCast(terminal_buffer.width); const buf_height: u32 = @intCast(terminal_buffer.height); var movie_width: u32 = @intCast(dur_movie.columns.?); var movie_height: u32 = @intCast(dur_movie.lines.?); if (movie_width > buf_width) movie_width = buf_width; if (movie_height > buf_height) movie_height = buf_height; const start_pos: IVec2 = switch (offset_alignment) { DurOffsetAlignment.center => .{ center(buf_width) - center(movie_width), center(buf_height) - center(movie_height) }, DurOffsetAlignment.topleft => .{ 0, 0 }, DurOffsetAlignment.topcenter => .{ center(buf_width) - center(movie_width), 0 }, DurOffsetAlignment.topright => .{ buf_width - movie_width, 0 }, DurOffsetAlignment.centerleft => .{ 0, center(buf_height) - center(movie_height) }, DurOffsetAlignment.centerright => .{ buf_width - movie_width, center(buf_height) - center(movie_height) }, DurOffsetAlignment.bottomleft => .{ 0, buf_height - movie_height }, DurOffsetAlignment.bottomcenter => .{ center(buf_width) - center(movie_width), buf_height - movie_height }, DurOffsetAlignment.bottomright => .{ buf_width - movie_width, buf_height - movie_height }, }; return start_pos + offset; } fn calc_frame_size(terminal_buffer: *TerminalBuffer, dur_movie: *DurFormat) UVec2 { 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.?); // Draw only the needed amount if movie smaller than screen. If movie is bigger, we will just draw entire screen const frame_width = if (movie_width < buf_width) movie_width else buf_width; const frame_height = if (movie_height < buf_height) movie_height else buf_height; return .{ frame_width, frame_height }; } pub fn init( allocator: Allocator, terminal_buffer: *TerminalBuffer, log_file: *LogFile, file_path: []const u8, offset_alignment: DurOffsetAlignment, x_offset: i32, y_offset: i32, full_color: bool, animate: *bool, timeout_sec: u12, frame_delay: u16, ) !DurFile { var dur_movie: DurFormat = .init(allocator); dur_movie.create_from_file(allocator, file_path) catch |err| switch (err) { error.FileNotFound => { try log_file.err("tui", "dur_file was not found at: {s}", .{file_path}); return err; }, error.NotValidFile => { try log_file.err("tui", "dur_file loaded was invalid or not a dur file!", .{}); 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_file.err("tui", "dur_file can not be 256 color encoded when not using full_color option!", .{}); dur_movie.deinit(); return error.InvalidColorFormat; } const offset: IVec2 = .{ x_offset, y_offset }; const start_pos = calc_start_position(terminal_buffer, &dur_movie, offset_alignment, offset); const frame_size = calc_frame_size(terminal_buffer, &dur_movie); // Convert dur fps to frames per ms const frame_time: u32 = @intFromFloat(1000 / dur_movie.framerate.?); return .{ .start_time = try interop.getTimeOfDay(), .allocator = allocator, .terminal_buffer = terminal_buffer, .frames = 0, .time_previous = std.time.milliTimestamp(), .frame_size = frame_size, .start_pos = start_pos, .full_color = full_color, .animate = animate, .timeout_sec = timeout_sec, .frame_delay = frame_delay, .dur_movie = dur_movie, .frame_time = frame_time, .is_color_format_16 = eql(u8, dur_movie.colorFormat.?, "16"), .offset_alignment = offset_alignment, .offset = offset, }; } pub fn widget(self: *DurFile) Widget { return Widget.init( "DurFile", null, self, deinit, realloc, draw, update, null, calculateTimeout, ); } fn deinit(self: *DurFile) void { self.dur_movie.deinit(); } fn realloc(self: *DurFile) !void { // when terminal size changes, we need to recalculate the start_pos and frame_size based on the new size self.start_pos = calc_start_position(self.terminal_buffer, &self.dur_movie, self.offset_alignment, self.offset); self.frame_size = calc_frame_size(self.terminal_buffer, &self.dur_movie); } fn draw(self: *DurFile) void { if (!self.animate.*) return; const current_frame = self.dur_movie.frames.items[self.frames]; const buf_width: u32 = @intCast(self.terminal_buffer.width); const buf_height: u32 = @intCast(self.terminal_buffer.height); // y is used as an iterator in the durformat, while cell_y gives us the correct placement for the cell (same for x) for (0..self.frame_size[VEC_Y]) |y| { const y_offset_i = @as(i32, @intCast(y)) + self.start_pos[VEC_Y]; // we skip the pass if it falls outside of the draw window (ensure no int underflow) const cell_y: u32 = if (y_offset_i >= 0 and y_offset_i < buf_height) @intCast(y_offset_i) else continue; var iter = std.unicode.Utf8View.initUnchecked(current_frame.contents[y]).iterator(); for (0..self.frame_size[VEC_X]) |x| { const x_offset_i = @as(i32, @intCast(x)) + self.start_pos[VEC_X]; // skip pass, same as y but also increment the codepoint iter to fetch correct values in later passes const cell_x: u32 = if (x_offset_i >= 0 and x_offset_i < buf_width) @intCast(x_offset_i) else { _ = iter.nextCodepoint().?; continue; }; const codepoint: u21 = iter.nextCodepoint().?; const color_map = current_frame.colorMap[x][y]; var color_map_0: u32 = @intCast(if (color_map[0] == -1) 0 else color_map[0]); var color_map_1: u32 = @intCast(if (color_map[1] == -1) 0 else color_map[1]); 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 cell = Cell{ .ch = @intCast(codepoint), .fg = fg_color, .bg = bg_color }; cell.put(cell_x, cell_y); } } const time_current = std.time.milliTimestamp(); const delta_time = time_current - self.time_previous; // Convert delay from sec to ms const delay_time: u32 = @intFromFloat(current_frame.delay * 1000); if (delta_time > (self.frame_time + delay_time)) { self.time_previous = time_current; const frame_count = self.dur_movie.frames.items.len; self.frames = (self.frames + 1) % frame_count; } } fn update(self: *DurFile, _: *anyopaque) !void { const time = try interop.getTimeOfDay(); if (self.timeout_sec > 0 and time.seconds - self.start_time.seconds > self.timeout_sec) { self.animate.* = false; } } fn calculateTimeout(self: *DurFile, _: *anyopaque) !?usize { return self.frame_delay; } ================================================ FILE: src/animations/GameOfLife.zig ================================================ const std = @import("std"); const Allocator = std.mem.Allocator; const ly_ui = @import("ly-ui"); const Cell = ly_ui.Cell; const TerminalBuffer = ly_ui.TerminalBuffer; const Widget = ly_ui.Widget; const ly_core = ly_ui.ly_core; const interop = ly_core.interop; const TimeOfDay = interop.TimeOfDay; const GameOfLife = @This(); // Visual styles - using block characters like other animations const ALIVE_CHAR: u21 = 0x2588; // Full block █ const DEAD_CHAR: u21 = ' '; const NEIGHBOR_DIRS = [_][2]i8{ .{ -1, -1 }, .{ -1, 0 }, .{ -1, 1 }, .{ 0, -1 }, .{ 0, 1 }, .{ 1, -1 }, .{ 1, 0 }, .{ 1, 1 }, }; start_time: TimeOfDay, allocator: Allocator, terminal_buffer: *TerminalBuffer, current_grid: []bool, next_grid: []bool, frame_counter: usize, generation: u64, fg_color: u32, entropy_interval: usize, frame_delay: usize, initial_density: f32, animate: *bool, timeout_sec: u12, animation_frame_delay: u16, dead_cell: Cell, width: usize, height: usize, pub fn init( allocator: Allocator, terminal_buffer: *TerminalBuffer, fg_color: u32, entropy_interval: usize, frame_delay: usize, initial_density: f32, animate: *bool, timeout_sec: u12, animation_frame_delay: u16, ) !GameOfLife { const width = terminal_buffer.width; const height = terminal_buffer.height; const grid_size = width * height; const current_grid = try allocator.alloc(bool, grid_size); const next_grid = try allocator.alloc(bool, grid_size); var game = GameOfLife{ .start_time = try interop.getTimeOfDay(), .allocator = allocator, .terminal_buffer = terminal_buffer, .current_grid = current_grid, .next_grid = next_grid, .frame_counter = 0, .generation = 0, .fg_color = fg_color, .entropy_interval = entropy_interval, .frame_delay = frame_delay, .initial_density = initial_density, .animate = animate, .timeout_sec = timeout_sec, .animation_frame_delay = animation_frame_delay, .dead_cell = .{ .ch = DEAD_CHAR, .fg = @intCast(TerminalBuffer.Color.DEFAULT), .bg = terminal_buffer.bg }, .width = width, .height = height, }; // Initialize grid game.initializeGrid(); return game; } pub fn widget(self: *GameOfLife) Widget { return Widget.init( "GameOfLife", null, self, deinit, realloc, draw, update, null, calculateTimeout, ); } fn deinit(self: *GameOfLife) void { self.allocator.free(self.current_grid); self.allocator.free(self.next_grid); } fn realloc(self: *GameOfLife) !void { const new_width = self.terminal_buffer.width; const new_height = self.terminal_buffer.height; const new_size = new_width * new_height; const current_grid = try self.allocator.realloc(self.current_grid, new_size); const next_grid = try self.allocator.realloc(self.next_grid, new_size); self.current_grid = current_grid; self.next_grid = next_grid; self.width = new_width; self.height = new_height; self.initializeGrid(); self.generation = 0; } fn draw(self: *GameOfLife) void { if (!self.animate.*) return; // Update game state at controlled frame rate self.frame_counter += 1; if (self.frame_counter >= self.frame_delay) { self.frame_counter = 0; self.updateGeneration(); self.generation += 1; // Add entropy based on configuration (0 = disabled, >0 = interval) if (self.entropy_interval > 0 and self.generation % self.entropy_interval == 0) { self.addEntropy(); } } // Render with the configured color const alive_cell = Cell{ .ch = ALIVE_CHAR, .fg = self.fg_color, .bg = self.terminal_buffer.bg }; for (0..self.height) |y| { const row_offset = y * self.width; for (0..self.width) |x| { const cell = if (self.current_grid[row_offset + x]) alive_cell else self.dead_cell; cell.put(x, y); } } } fn update(self: *GameOfLife, _: *anyopaque) !void { const time = try interop.getTimeOfDay(); if (self.timeout_sec > 0 and time.seconds - self.start_time.seconds > self.timeout_sec) { self.animate.* = false; } } fn calculateTimeout(self: *GameOfLife, _: *anyopaque) !?usize { return self.animation_frame_delay; } fn updateGeneration(self: *GameOfLife) void { // Conway's Game of Life rules with optimized neighbor counting for (0..self.height) |y| { const row_offset = y * self.width; for (0..self.width) |x| { const index = row_offset + x; const neighbors = self.countNeighborsOptimized(x, y); const is_alive = self.current_grid[index]; // Optimized rule application self.next_grid[index] = switch (neighbors) { 2 => is_alive, 3 => true, else => false, }; } } // Efficient grid swap std.mem.swap([]bool, &self.current_grid, &self.next_grid); } fn countNeighborsOptimized(self: *GameOfLife, x: usize, y: usize) u8 { var count: u8 = 0; for (NEIGHBOR_DIRS) |dir| { const neighbor_x = @as(i32, @intCast(x)) + dir[0]; const neighbor_y = @as(i32, @intCast(y)) + dir[1]; const width_i32: i32 = @intCast(self.width); const height_i32: i32 = @intCast(self.height); // Toroidal wrapping with modular arithmetic const wx: usize = @intCast(@mod(neighbor_x + width_i32, width_i32)); const wy: usize = @intCast(@mod(neighbor_y + height_i32, height_i32)); if (self.current_grid[wy * self.width + wx]) { count += 1; } } return count; } fn initializeGrid(self: *GameOfLife) void { const total_cells = self.width * self.height; // Clear grid @memset(self.current_grid, false); @memset(self.next_grid, false); // Random initialization with configurable density for (0..total_cells) |i| { self.current_grid[i] = self.terminal_buffer.random.float(f32) < self.initial_density; } } fn addEntropy(self: *GameOfLife) void { // Add fewer random cells but in clusters for more interesting patterns const clusters = 2; for (0..clusters) |_| { const cx = self.terminal_buffer.random.intRangeAtMost(usize, 1, self.width - 2); const cy = self.terminal_buffer.random.intRangeAtMost(usize, 1, self.height - 2); // Small cluster around center point for (0..3) |dy| { for (0..3) |dx| { if (self.terminal_buffer.random.float(f32) < 0.4) { const x = (cx + dx) % self.width; const y = (cy + dy) % self.height; self.current_grid[y * self.width + x] = true; } } } } } ================================================ FILE: src/animations/Matrix.zig ================================================ const std = @import("std"); const Allocator = std.mem.Allocator; const Random = std.Random; const ly_ui = @import("ly-ui"); const Cell = ly_ui.Cell; const TerminalBuffer = ly_ui.TerminalBuffer; const Widget = ly_ui.Widget; const ly_core = ly_ui.ly_core; const interop = ly_core.interop; const TimeOfDay = interop.TimeOfDay; pub const FRAME_DELAY: usize = 8; // Characters change mid-scroll pub const MID_SCROLL_CHANGE = true; const Matrix = @This(); pub const Dot = struct { value: ?usize, is_head: bool, }; pub const Line = struct { space: usize, length: usize, update: usize, }; start_time: TimeOfDay, allocator: Allocator, terminal_buffer: *TerminalBuffer, dots: []Dot, lines: []Line, frame: usize, count: usize, fg: u32, head_col: u32, min_codepoint: u16, max_codepoint: u16, animate: *bool, timeout_sec: u12, frame_delay: u16, default_cell: Cell, pub fn init( allocator: Allocator, terminal_buffer: *TerminalBuffer, fg: u32, head_col: u32, min_codepoint: u16, max_codepoint: u16, animate: *bool, timeout_sec: u12, frame_delay: u16, ) !Matrix { const dots = try allocator.alloc(Dot, terminal_buffer.width * (terminal_buffer.height + 1)); const lines = try allocator.alloc(Line, terminal_buffer.width); initBuffers(dots, lines, terminal_buffer.width, terminal_buffer.height, terminal_buffer.random); return .{ .start_time = try interop.getTimeOfDay(), .allocator = allocator, .terminal_buffer = terminal_buffer, .dots = dots, .lines = lines, .frame = 3, .count = 0, .fg = fg, .head_col = head_col, .min_codepoint = min_codepoint, .max_codepoint = max_codepoint - min_codepoint, .animate = animate, .timeout_sec = timeout_sec, .frame_delay = frame_delay, .default_cell = .{ .ch = ' ', .fg = fg, .bg = terminal_buffer.bg }, }; } pub fn widget(self: *Matrix) Widget { return Widget.init( "Matrix", null, self, deinit, realloc, draw, update, null, calculateTimeout, ); } fn deinit(self: *Matrix) void { self.allocator.free(self.dots); self.allocator.free(self.lines); } fn realloc(self: *Matrix) !void { const dots = try self.allocator.realloc(self.dots, self.terminal_buffer.width * (self.terminal_buffer.height + 1)); const lines = try self.allocator.realloc(self.lines, self.terminal_buffer.width); initBuffers(dots, lines, self.terminal_buffer.width, self.terminal_buffer.height, self.terminal_buffer.random); self.dots = dots; self.lines = lines; } fn draw(self: *Matrix) void { if (!self.animate.*) return; const buf_height = self.terminal_buffer.height; const buf_width = self.terminal_buffer.width; self.count += 1; if (self.count > FRAME_DELAY) { self.frame += 1; if (self.frame > 4) self.frame = 1; self.count = 0; var x: usize = 0; while (x < buf_width) : (x += 2) { var tail: usize = 0; var line = &self.lines[x]; if (self.frame <= line.update) continue; if (self.dots[x].value == null and self.dots[buf_width + x].value == ' ') { if (line.space > 0) { line.space -= 1; } else { const randint = self.terminal_buffer.random.int(u16); const h = buf_height; line.length = @mod(randint, h - 3) + 3; self.dots[x].value = @mod(randint, self.max_codepoint) + self.min_codepoint; line.space = @mod(randint, h + 1); } } var y: usize = 0; var first_col = true; var seg_len: u64 = 0; height_it: while (y <= buf_height) : (y += 1) { var dot = &self.dots[buf_width * y + x]; // Skip over spaces while (y <= buf_height and (dot.value == ' ' or dot.value == null)) { y += 1; if (y > buf_height) break :height_it; dot = &self.dots[buf_width * y + x]; } // Find the head of this column tail = y; seg_len = 0; while (y <= buf_height and dot.value != ' ' and dot.value != null) { dot.is_head = false; if (MID_SCROLL_CHANGE) { const randint = self.terminal_buffer.random.int(u16); if (@mod(randint, 8) == 0) { dot.value = @mod(randint, self.max_codepoint) + self.min_codepoint; } } y += 1; seg_len += 1; // Head's down offscreen if (y > buf_height) { self.dots[buf_width * tail + x].value = ' '; break :height_it; } dot = &self.dots[buf_width * y + x]; } const randint = self.terminal_buffer.random.int(u16); dot.value = @mod(randint, self.max_codepoint) + self.min_codepoint; dot.is_head = true; if (seg_len > line.length or !first_col) { self.dots[buf_width * tail + x].value = ' '; self.dots[x].value = null; } first_col = false; } } } var x: usize = 0; while (x < buf_width) : (x += 2) { var y: usize = 1; while (y <= buf_height) : (y += 1) { const dot = self.dots[buf_width * y + x]; const cell = if (dot.value == null or dot.value == ' ') self.default_cell else Cell{ .ch = @intCast(dot.value.?), .fg = if (dot.is_head) self.head_col else self.fg, .bg = self.terminal_buffer.bg, }; cell.put(x, y - 1); // Fill background in between columns self.default_cell.put(x + 1, y - 1); } } } fn update(self: *Matrix, _: *anyopaque) !void { const time = try interop.getTimeOfDay(); if (self.timeout_sec > 0 and time.seconds - self.start_time.seconds > self.timeout_sec) { self.animate.* = false; } } fn calculateTimeout(self: *Matrix, _: *anyopaque) !?usize { return self.frame_delay; } fn initBuffers(dots: []Dot, lines: []Line, width: usize, height: usize, random: Random) void { var y: usize = 0; while (y <= height) : (y += 1) { var x: usize = 0; while (x < width) : (x += 2) { dots[y * width + x].value = null; } } var x: usize = 0; while (x < width) : (x += 2) { var line = lines[x]; line.space = @mod(random.int(u16), height) + 1; line.length = @mod(random.int(u16), height - 3) + 3; line.update = @mod(random.int(u16), 3) + 1; lines[x] = line; dots[width + x].value = ' '; } } ================================================ FILE: src/auth.zig ================================================ const std = @import("std"); const Md5 = std.crypto.hash.Md5; const builtin = @import("builtin"); const build_options = @import("build_options"); const ly_core = @import("ly-ui").ly_core; const interop = ly_core.interop; const SharedError = ly_core.SharedError; const LogFile = ly_core.LogFile; const utmp = interop.utmp; const Utmp = utmp.utmpx; const Environment = @import("Environment.zig"); pub const AuthOptions = struct { tty: u8, service_name: [:0]const u8, path: ?[]const u8, session_log: ?[]const u8, xauth_cmd: []const u8, setup_cmd: []const u8, login_cmd: ?[]const u8, x_cmd: []const u8, x_vt: ?u8, session_pid: std.posix.pid_t, use_kmscon_vt: bool, }; var xorg_pid: std.posix.pid_t = 0; pub fn xorgSignalHandler(i: c_int) callconv(.c) void { if (xorg_pid > 0) _ = std.c.kill(xorg_pid, i); } var child_pid: std.posix.pid_t = 0; pub fn sessionSignalHandler(i: c_int) callconv(.c) void { if (child_pid > 0) _ = std.c.kill(child_pid, i); } pub fn authenticate(allocator: std.mem.Allocator, log_file: *LogFile, options: AuthOptions, current_environment: Environment, login: []const u8, password: []const u8) !void { var tty_buffer: [3]u8 = undefined; const tty_str = try std.fmt.bufPrint(&tty_buffer, "{d}", .{options.tty}); var pam_tty_buffer: [6]u8 = undefined; const pam_tty_str = try std.fmt.bufPrintZ(&pam_tty_buffer, "tty{d}", .{options.tty}); // Set the XDG environment variables try log_file.info("auth/env", "setting xdg environment variables", .{}); try setXdgEnv(allocator, tty_str, current_environment); // Open the PAM session try log_file.info("auth/pam", "encoding credentials", .{}); const login_z = try allocator.dupeZ(u8, login); defer allocator.free(login_z); const password_z = try allocator.dupeZ(u8, password); defer allocator.free(password_z); var credentials = [_:null]?[*:0]const u8{ login_z, password_z }; const conv = interop.pam.pam_conv{ .conv = loginConv, .appdata_ptr = @ptrCast(&credentials), }; var handle: ?*interop.pam.pam_handle = undefined; try log_file.info("auth/pam", "starting session", .{}); var status = interop.pam.pam_start(options.service_name, null, &conv, &handle); if (status != interop.pam.PAM_SUCCESS) return pamDiagnose(status); defer _ = interop.pam.pam_end(handle, status); // Set PAM_TTY as the current TTY. This is required in case it isn't being set by another PAM module try log_file.info("auth/pam", "setting tty", .{}); status = interop.pam.pam_set_item(handle, interop.pam.PAM_TTY, pam_tty_str.ptr); if (status != interop.pam.PAM_SUCCESS) return pamDiagnose(status); // Do the PAM routine try log_file.info("auth/pam", "authenticating", .{}); status = interop.pam.pam_authenticate(handle, 0); if (status != interop.pam.PAM_SUCCESS) return pamDiagnose(status); try log_file.info("auth/pam", "validating account", .{}); status = interop.pam.pam_acct_mgmt(handle, 0); if (status != interop.pam.PAM_SUCCESS) return pamDiagnose(status); try log_file.info("auth/pam", "setting credentials", .{}); status = interop.pam.pam_setcred(handle, interop.pam.PAM_ESTABLISH_CRED); if (status != interop.pam.PAM_SUCCESS) return pamDiagnose(status); defer status = interop.pam.pam_setcred(handle, interop.pam.PAM_DELETE_CRED); try log_file.info("auth/pam", "opening session", .{}); status = interop.pam.pam_open_session(handle, 0); if (status != interop.pam.PAM_SUCCESS) return pamDiagnose(status); defer status = interop.pam.pam_close_session(handle, 0); try log_file.info("auth/passwd", "getting struct", .{}); var user_entry: interop.UsernameEntry = undefined; { defer interop.closePasswordDatabase(); // Get password structure from username user_entry = interop.getUsernameEntry(login_z) orelse return error.GetPasswordNameFailed; } // Set user shell if it hasn't already been set try log_file.info("auth/passwd", "setting user shell", .{}); if (user_entry.shell == null) interop.setUserShell(&user_entry); var shared_err = try SharedError.init(null, null); defer shared_err.deinit(); log_file.deinit(); child_pid = try std.posix.fork(); if (child_pid == 0) { try log_file.reinit(); try log_file.info("auth/sys", "starting session", .{}); startSession(log_file, allocator, options, tty_str, user_entry, handle, current_environment) catch |e| { shared_err.writeError(e); log_file.deinit(); std.process.exit(1); }; log_file.deinit(); std.process.exit(0); } var entry = std.mem.zeroes(Utmp); { // If an error occurs here, we can send SIGTERM to the session errdefer cleanup: { std.posix.kill(child_pid, std.posix.SIG.TERM) catch break :cleanup; _ = std.posix.waitpid(child_pid, 0); } // If we receive SIGTERM, forward it to child_pid const act = std.posix.Sigaction{ .handler = .{ .handler = &sessionSignalHandler }, .mask = std.posix.sigemptyset(), .flags = 0, }; std.posix.sigaction(std.posix.SIG.TERM, &act, null); try addUtmpEntry(&entry, user_entry.username.?, child_pid); } // Wait for the session to stop _ = std.posix.waitpid(child_pid, 0); try log_file.reinit(); try log_file.info("auth/utmp", "removing utmp entry", .{}); removeUtmpEntry(&entry); if (shared_err.readError()) |err| return err; } fn startSession( log_file: *LogFile, allocator: std.mem.Allocator, options: AuthOptions, tty_str: []u8, user_entry: interop.UsernameEntry, handle: ?*interop.pam.pam_handle, current_environment: Environment, ) !void { // Set the user's GID & PID try log_file.info("auth/passwd", "setting user context", .{}); try interop.setUserContext(allocator, user_entry); // Set up the environment try log_file.info("auth/env", "setting environment variables", .{}); try initEnv(allocator, user_entry, options.path); // Reset the XDG environment variables try log_file.info("auth/env", "resetting xdg environment variables", .{}); try setXdgEnv(allocator, tty_str, current_environment); try setXdgRuntimeDir(allocator); // Set the PAM variables const pam_env_vars: ?[*:null]?[*:0]u8 = interop.pam.pam_getenvlist(handle); if (pam_env_vars == null) return error.GetEnvListFailed; const env_list = std.mem.span(pam_env_vars.?); for (env_list) |env_var| { if (env_var == null) continue; try log_file.info("auth/env", "setting pam environment variable: {s}", .{std.mem.span(env_var.?)}); try interop.putEnvironmentVariable(env_var); } // Change to the user's home directory try log_file.info("auth/sys", "changing cwd to user home", .{}); std.posix.chdir(user_entry.home.?) catch return error.ChangeDirectoryFailed; // Signal to the session process to give up control on the TTY try log_file.info("auth/sys", "releasing tty", .{}); std.posix.kill(options.session_pid, std.posix.SIG.CHLD) catch return error.TtyControlTransferFailed; // Execute what the user requested switch (current_environment.display_server) { .wayland, .shell, .custom => try executeCmd(log_file, allocator, user_entry.shell.?, options, current_environment.is_terminal, current_environment.cmd), .xinitrc, .x11 => if (build_options.enable_x11_support) { var vt_buf: [5]u8 = undefined; const vt = try std.fmt.bufPrint(&vt_buf, "vt{d}", .{options.x_vt orelse options.tty}); try log_file.info("auth/x11", "setting vt to {s}", .{vt}); try executeX11Cmd(log_file, allocator, user_entry.shell.?, user_entry.home.?, options, current_environment.cmd orelse "", vt); }, } } fn initEnv(allocator: std.mem.Allocator, entry: interop.UsernameEntry, path_env: ?[]const u8) !void { if (entry.home) |home| { try interop.setEnvironmentVariable(allocator, "HOME", home, true); try interop.setEnvironmentVariable(allocator, "PWD", home, true); } else return error.NoHomeDirectory; try interop.setEnvironmentVariable(allocator, "SHELL", entry.shell.?, true); try interop.setEnvironmentVariable(allocator, "USER", entry.username.?, true); try interop.setEnvironmentVariable(allocator, "LOGNAME", entry.username.?, true); if (path_env) |path| { interop.setEnvironmentVariable(allocator, "PATH", path, true) catch return error.SetPathFailed; } } fn setXdgEnv(allocator: std.mem.Allocator, tty_str: []u8, environment: Environment) !void { try interop.setEnvironmentVariable(allocator, "XDG_SESSION_TYPE", switch (environment.display_server) { .wayland => "wayland", .shell => "tty", .xinitrc, .x11 => "x11", .custom => if (environment.is_terminal) "tty" else "unspecified", }, false); if (environment.xdg_desktop_names) |xdg_desktop_names| try interop.setEnvironmentVariable(allocator, "XDG_CURRENT_DESKTOP", xdg_desktop_names, false); try interop.setEnvironmentVariable(allocator, "XDG_SESSION_CLASS", "user", false); try interop.setEnvironmentVariable(allocator, "XDG_SESSION_ID", "1", false); if (environment.xdg_session_desktop) |desktop_name| try interop.setEnvironmentVariable(allocator, "XDG_SESSION_DESKTOP", desktop_name, false); try interop.setEnvironmentVariable(allocator, "XDG_SEAT", "seat0", false); try interop.setEnvironmentVariable(allocator, "XDG_VTNR", tty_str, false); } fn setXdgRuntimeDir(allocator: std.mem.Allocator) !void { // The "/run/user/%d" directory is not available on FreeBSD. It is much // better to stick to the defaults and let applications using // XDG_RUNTIME_DIR to fall back to directories inside user's home // directory. if (builtin.os.tag != .freebsd) { const uid = std.posix.getuid(); var uid_buffer: [32]u8 = undefined; // No UID can be larger than this const uid_str = try std.fmt.bufPrint(&uid_buffer, "/run/user/{d}", .{uid}); try interop.setEnvironmentVariable(allocator, "XDG_RUNTIME_DIR", uid_str, false); } } fn loginConv( num_msg: c_int, msg: ?[*]?*const interop.pam.pam_message, resp: ?*?[*]interop.pam.pam_response, appdata_ptr: ?*anyopaque, ) callconv(.c) c_int { const message_count: u32 = @intCast(num_msg); const messages = msg.?; const allocator = std.heap.c_allocator; const response = allocator.alloc(interop.pam.pam_response, message_count) catch return interop.pam.PAM_BUF_ERR; // Initialise allocated memory to 0 // This ensures memory can be freed by pam on success @memset(response, std.mem.zeroes(interop.pam.pam_response)); var username: ?[:0]u8 = null; var password: ?[:0]u8 = null; var status: c_int = interop.pam.PAM_SUCCESS; for (0..message_count) |i| set_credentials: { switch (messages[i].?.msg_style) { interop.pam.PAM_PROMPT_ECHO_ON => { const data: [*][*:0]u8 = @ptrCast(@alignCast(appdata_ptr)); username = allocator.dupeZ(u8, std.mem.span(data[0])) catch { status = interop.pam.PAM_BUF_ERR; break :set_credentials; }; response[i].resp = username.?; }, interop.pam.PAM_PROMPT_ECHO_OFF => { const data: [*][*:0]u8 = @ptrCast(@alignCast(appdata_ptr)); password = allocator.dupeZ(u8, std.mem.span(data[1])) catch { status = interop.pam.PAM_BUF_ERR; break :set_credentials; }; response[i].resp = password.?; }, interop.pam.PAM_ERROR_MSG => { status = interop.pam.PAM_CONV_ERR; break :set_credentials; }, else => {}, } } if (status != interop.pam.PAM_SUCCESS) { // Memory is freed by pam otherwise allocator.free(response); if (username) |str| allocator.free(str); if (password) |str| allocator.free(str); } else { resp.?.* = response.ptr; } return status; } fn getFreeDisplay() !u8 { var buf: [15]u8 = undefined; var i: u8 = 0; while (i < 200) : (i += 1) { const xlock = try std.fmt.bufPrint(&buf, "/tmp/.X{d}-lock", .{i}); std.posix.access(xlock, std.posix.F_OK) catch break; } return i; } fn getXPid(display_num: u8) !i32 { var buf: [15]u8 = undefined; const file_name = try std.fmt.bufPrint(&buf, "/tmp/.X{d}-lock", .{display_num}); const file = try std.fs.openFileAbsolute(file_name, .{}); defer file.close(); var file_buffer: [32]u8 = undefined; var file_reader = file.reader(&file_buffer); var reader = &file_reader.interface; var buffer: [20]u8 = undefined; var writer = std.Io.Writer.fixed(&buffer); const written = try reader.streamDelimiter(&writer, '\n'); return std.fmt.parseInt(i32, std.mem.trim(u8, buffer[0..written], " "), 10); } fn createXauthFile(log_file: *LogFile, pwd: []const u8, buffer: []u8) ![]const u8 { var xauth_buf: [100]u8 = undefined; var xauth_dir: []const u8 = undefined; const xdg_rt_dir = std.posix.getenv("XDG_RUNTIME_DIR"); var xauth_file: []const u8 = "lyxauth"; if (xdg_rt_dir == null) no_rt_dir: { const xdg_cfg_home = std.posix.getenv("XDG_CONFIG_HOME"); if (xdg_cfg_home == null) no_cfg_home: { xauth_dir = try std.fmt.bufPrint(&xauth_buf, "{s}/.config", .{pwd}); var dir = std.fs.cwd().openDir(xauth_dir, .{}) catch { // xauth_dir isn't a directory xauth_dir = pwd; xauth_file = ".lyxauth"; break :no_cfg_home; }; dir.close(); // xauth_dir is a directory, use it to store Xauthority xauth_dir = try std.fmt.bufPrint(&xauth_buf, "{s}/.config/ly", .{pwd}); } else { xauth_dir = try std.fmt.bufPrint(&xauth_buf, "{s}/ly", .{xdg_cfg_home.?}); } const file = std.fs.cwd().openFile(xauth_dir, .{}) catch break :no_rt_dir; file.close(); // xauth_dir is a file, create the parent directory std.posix.mkdir(xauth_dir, 777) catch { xauth_dir = pwd; xauth_file = ".lyxauth"; }; } else { xauth_dir = xdg_rt_dir.?; } // Trim trailing slashes var i = xauth_dir.len - 1; while (xauth_dir[i] == '/') i -= 1; const trimmed_xauth_dir = xauth_dir[0 .. i + 1]; const xauthority: []u8 = try std.fmt.bufPrint(buffer, "{s}/{s}", .{ trimmed_xauth_dir, xauth_file }); std.fs.cwd().makePath(trimmed_xauth_dir) catch {}; try log_file.info("auth/x11", "creating xauth file: {s}", .{xauthority}); const file = try std.fs.createFileAbsolute(xauthority, .{}); file.close(); return xauthority; } fn mcookie() [Md5.digest_length * 2]u8 { var buf: [4096]u8 = undefined; std.crypto.random.bytes(&buf); var out: [Md5.digest_length]u8 = undefined; Md5.hash(&buf, &out, .{}); return std.fmt.bytesToHex(&out, .lower); } fn xauth(log_file: *LogFile, allocator: std.mem.Allocator, display_name: []u8, shell: [*:0]const u8, home: []const u8, xauth_buffer: []u8, options: AuthOptions) !void { const xauthority = try createXauthFile(log_file, home, xauth_buffer); try interop.setEnvironmentVariable(allocator, "XAUTHORITY", xauthority, true); try interop.setEnvironmentVariable(allocator, "DISPLAY", display_name, true); const magic_cookie = mcookie(); const pid = try std.posix.fork(); if (pid == 0) { var cmd_buffer: [1024]u8 = undefined; const cmd_str = std.fmt.bufPrintZ(&cmd_buffer, "{s} add {s} . {s}", .{ options.xauth_cmd, display_name, magic_cookie }) catch std.process.exit(1); try log_file.info("auth/x11", "executing: {s} -c {s}", .{ shell, cmd_str }); const args = [_:null]?[*:0]const u8{ shell, "-c", cmd_str }; std.posix.execveZ(shell, &args, std.c.environ) catch {}; std.process.exit(1); } const status = std.posix.waitpid(pid, 0); if (status.status != 0) { try log_file.file_writer.interface.print("xauth command failed with status {d}\n", .{status.status}); return error.XauthFailed; } } fn executeX11Cmd(log_file: *LogFile, allocator: std.mem.Allocator, shell: []const u8, home: []const u8, options: AuthOptions, desktop_cmd: []const u8, vt: []const u8) !void { var xauth_buffer: [256]u8 = undefined; try log_file.info("auth/x11", "getting free display", .{}); const display_num = try getFreeDisplay(); var buf: [4]u8 = undefined; const display_name = try std.fmt.bufPrint(&buf, ":{d}", .{display_num}); try log_file.info("auth/x11", "got free display: {d}", .{display_num}); const shell_z = try allocator.dupeZ(u8, shell); defer allocator.free(shell_z); try log_file.info("auth/x11", "creating xauth file", .{}); try xauth(log_file, allocator, display_name, shell_z, home, &xauth_buffer, options); try log_file.info("auth/x11", "starting x server", .{}); const pid = try std.posix.fork(); if (pid == 0) { var cmd_buffer: [1024]u8 = undefined; const cmd_str = std.fmt.bufPrintZ(&cmd_buffer, "{s} {s} {s}", .{ options.x_cmd, display_name, vt }) catch std.process.exit(1); try log_file.info("auth/x11", "executing: {s} -c {s}", .{ shell, cmd_str }); const args = [_:null]?[*:0]const u8{ shell_z, "-c", cmd_str }; std.posix.execveZ(shell_z, &args, std.c.environ) catch {}; std.process.exit(1); } try log_file.info("auth/x11", "waiting for xcb connection", .{}); var ok: c_int = -1; var xcb: ?*interop.xcb.xcb_connection_t = null; while (ok != 0) { xcb = interop.xcb.xcb_connect(null, null); ok = interop.xcb.xcb_connection_has_error(xcb); std.posix.kill(pid, 0) catch |e| { if (e == error.ProcessNotFound and ok != 0) return error.XcbConnectionFailed; }; } // X Server detaches from the process. // PID can be fetched from /tmp/X{d}.lock try log_file.info("auth/x11", "getting x server pid", .{}); const x_pid = try getXPid(display_num); try log_file.info("auth/x11", "got x server pid: {d}", .{x_pid}); try log_file.info("auth/x11", "launching environment", .{}); xorg_pid = try std.posix.fork(); if (xorg_pid == 0) { var cmd_buffer: [1024]u8 = undefined; const cmd_str = std.fmt.bufPrintZ(&cmd_buffer, "{s} {s} {s} {s}", .{ if (options.use_kmscon_vt) "kmscon-launch-gui" else "", options.setup_cmd, options.login_cmd orelse "", desktop_cmd }) catch std.process.exit(1); try log_file.info("auth/x11", "executing: {s} -c {s}", .{ shell, cmd_str }); const args = [_:null]?[*:0]const u8{ shell_z, "-c", cmd_str }; std.posix.execveZ(shell_z, &args, std.c.environ) catch {}; std.process.exit(1); } // If we receive SIGTERM, clean up by killing the xorg_pid process const act = std.posix.Sigaction{ .handler = .{ .handler = &xorgSignalHandler }, .mask = std.posix.sigemptyset(), .flags = 0, }; std.posix.sigaction(std.posix.SIG.TERM, &act, null); _ = std.posix.waitpid(xorg_pid, 0); try log_file.info("auth/x11", "disconnecting xcb", .{}); interop.xcb.xcb_disconnect(xcb); // TODO: Find a more robust way to ensure that X has been terminated (pidfds?) std.posix.kill(x_pid, std.posix.SIG.TERM) catch {}; std.Thread.sleep(std.time.ns_per_s * 1); // Wait 1 second before sending SIGKILL std.posix.kill(x_pid, std.posix.SIG.KILL) catch return; _ = std.posix.waitpid(x_pid, 0); } fn executeCmd(global_log_file: *LogFile, allocator: std.mem.Allocator, shell: []const u8, options: AuthOptions, is_terminal: bool, exec_cmd: ?[]const u8) !void { try global_log_file.info("auth/sys", "launching wayland/shell/custom session", .{}); var maybe_log_file: ?std.fs.File = null; if (!is_terminal) redirect_streams: { if (options.use_kmscon_vt) { try global_log_file.err("auth/sys", "cannot redirect stdio & stderr with kmscon", .{}); break :redirect_streams; } // For custom desktop entries, the "Terminal" value here determines if // we redirect standard output & error or not. That is, we redirect only // if it's equal to false (so if it's not running in a TTY). if (options.session_log) |log_path| { try global_log_file.info("auth/sys", "setting up stdio & stderr redirection", .{}); maybe_log_file = try redirectStandardStreams(global_log_file, log_path, true); } } defer if (maybe_log_file) |log_file| log_file.close(); const shell_z = try allocator.dupeZ(u8, shell); defer allocator.free(shell_z); var cmd_buffer: [1024]u8 = undefined; const cmd_str = try std.fmt.bufPrintZ(&cmd_buffer, "{s} {s} {s} {s}", .{ if (!is_terminal and options.use_kmscon_vt) "kmscon-launch-gui" else "", options.setup_cmd, options.login_cmd orelse "", exec_cmd orelse shell }); try global_log_file.info("auth/sys", "executing: {s} -c {s}", .{ shell, cmd_str }); const args = [_:null]?[*:0]const u8{ shell_z, "-c", cmd_str }; return std.posix.execveZ(shell_z, &args, std.c.environ); } fn redirectStandardStreams(global_log_file: *LogFile, session_log: []const u8, create: bool) !std.fs.File { create_session_log_dir: { const session_log_dir = std.fs.path.dirname(session_log) orelse break :create_session_log_dir; std.fs.cwd().makePath(session_log_dir) catch |err| { try global_log_file.err("auth/sys", "failed to create session log file directory: {s}", .{@errorName(err)}); return err; }; } const log_file = if (create) (std.fs.cwd().createFile(session_log, .{ .mode = 0o666 }) catch |err| { try global_log_file.err("auth/sys", "failed to create new session log file: {s}", .{@errorName(err)}); return err; }) else (std.fs.cwd().openFile(session_log, .{ .mode = .read_write }) catch |err| { try global_log_file.err("auth/sys", "failed to open existing session log file: {s}", .{@errorName(err)}); return err; }); try std.posix.dup2(std.posix.STDOUT_FILENO, std.posix.STDERR_FILENO); try std.posix.dup2(log_file.handle, std.posix.STDOUT_FILENO); return log_file; } fn addUtmpEntry(entry: *Utmp, username: []const u8, pid: c_int) !void { entry.ut_type = utmp.USER_PROCESS; entry.ut_pid = pid; var buf: [std.fs.max_path_bytes]u8 = undefined; const tty_path = try std.os.getFdPath(std.posix.STDIN_FILENO, &buf); // Get the TTY name (i.e. without the /dev/ prefix) var ttyname_buf: [@sizeOf(@TypeOf(entry.ut_line))]u8 = undefined; _ = try std.fmt.bufPrintZ(&ttyname_buf, "{s}", .{tty_path["/dev/".len..]}); entry.ut_line = ttyname_buf; // Get the TTY ID (i.e. without the tty prefix) and truncate it to the size // of ut_id if necessary entry.ut_id = ttyname_buf["tty".len..(@sizeOf(@TypeOf(entry.ut_id)) + "tty".len)].*; var username_buf: [@sizeOf(@TypeOf(entry.ut_user))]u8 = undefined; _ = try std.fmt.bufPrintZ(&username_buf, "{s}", .{username}); entry.ut_user = username_buf; var host: [@sizeOf(@TypeOf(entry.ut_host))]u8 = undefined; host[0] = 0; entry.ut_host = host; const time = try interop.getTimeOfDay(); entry.ut_tv = .{ .tv_sec = @intCast(time.seconds), .tv_usec = @intCast(time.microseconds), }; // FreeBSD doesn't have this field if (builtin.os.tag == .linux) { entry.ut_addr_v6[0] = 0; } utmp.setutxent(); _ = utmp.pututxline(entry); utmp.endutxent(); } fn removeUtmpEntry(entry: *Utmp) void { entry.ut_type = utmp.DEAD_PROCESS; entry.ut_line[0] = 0; entry.ut_user[0] = 0; utmp.setutxent(); _ = utmp.pututxline(entry); utmp.endutxent(); } fn pamDiagnose(status: c_int) anyerror { return switch (status) { interop.pam.PAM_ACCT_EXPIRED => return error.PamAccountExpired, interop.pam.PAM_AUTH_ERR => return error.PamAuthError, interop.pam.PAM_AUTHINFO_UNAVAIL => return error.PamAuthInfoUnavailable, interop.pam.PAM_BUF_ERR => return error.PamBufferError, interop.pam.PAM_CRED_ERR => return error.PamCredentialsError, interop.pam.PAM_CRED_EXPIRED => return error.PamCredentialsExpired, interop.pam.PAM_CRED_INSUFFICIENT => return error.PamCredentialsInsufficient, interop.pam.PAM_CRED_UNAVAIL => return error.PamCredentialsUnavailable, interop.pam.PAM_MAXTRIES => return error.PamMaximumTries, interop.pam.PAM_NEW_AUTHTOK_REQD => return error.PamNewAuthTokenRequired, interop.pam.PAM_PERM_DENIED => return error.PamPermissionDenied, interop.pam.PAM_SESSION_ERR => return error.PamSessionError, interop.pam.PAM_SYSTEM_ERR => return error.PamSystemError, interop.pam.PAM_USER_UNKNOWN => return error.PamUserUnknown, else => return error.PamAbort, }; } ================================================ FILE: src/components/InfoLine.zig ================================================ const std = @import("std"); const Allocator = std.mem.Allocator; const ly_ui = @import("ly-ui"); const keyboard = ly_ui.keyboard; const TerminalBuffer = ly_ui.TerminalBuffer; const Widget = ly_ui.Widget; const CyclableLabel = ly_ui.CyclableLabel; const MessageLabel = CyclableLabel(Message, Message); const InfoLine = @This(); const Message = struct { width: usize, text: []const u8, bg: u32, fg: u32, }; label: *MessageLabel, pub fn init( allocator: Allocator, buffer: *TerminalBuffer, width: usize, arrow_fg: u32, arrow_bg: u32, ) !InfoLine { return .{ .label = try MessageLabel.init( allocator, buffer, drawItem, null, null, width, true, arrow_fg, arrow_bg, ), }; } pub fn deinit(self: *InfoLine) void { self.label.deinit(); } pub fn widget(self: *InfoLine) Widget { return Widget.init( "InfoLine", self.label.keybinds, self, deinit, null, draw, null, handle, null, ); } pub fn addMessage(self: *InfoLine, text: []const u8, bg: u32, fg: u32) !void { if (text.len == 0) return; try self.label.addItem(.{ .width = TerminalBuffer.strWidth(text), .text = text, .bg = bg, .fg = fg, }); } pub fn clearRendered(self: InfoLine, allocator: Allocator) !void { // Draw over the area const spaces = try allocator.alloc(u8, self.label.width - 2); defer allocator.free(spaces); @memset(spaces, ' '); TerminalBuffer.drawText( spaces, self.label.component_pos.x + 2, self.label.component_pos.y, TerminalBuffer.Color.DEFAULT, TerminalBuffer.Color.DEFAULT, ); } fn draw(self: *InfoLine) void { self.label.draw(); } fn handle(self: *InfoLine, maybe_key: ?keyboard.Key) !void { self.label.handle(maybe_key); } fn drawItem(label: *MessageLabel, message: Message, x: usize, y: usize, width: usize) void { if (message.width == 0) return; const x_offset = if (label.text_in_center and width >= message.width) (width - message.width) / 2 else 0; label.cursor = message.width + x_offset; TerminalBuffer.drawConfinedText( message.text, x + x_offset, y, width, message.fg, message.bg, ); } ================================================ FILE: src/components/Session.zig ================================================ const std = @import("std"); const Allocator = std.mem.Allocator; const ly_ui = @import("ly-ui"); const keyboard = ly_ui.keyboard; const TerminalBuffer = ly_ui.TerminalBuffer; const Widget = ly_ui.Widget; const CyclableLabel = ly_ui.CyclableLabel; const UserList = @import("UserList.zig"); const Environment = @import("../Environment.zig"); const Env = struct { environment: Environment, index: usize, }; const EnvironmentLabel = CyclableLabel(Env, *UserList); const Session = @This(); label: *EnvironmentLabel, user_list: *UserList, pub fn init( allocator: Allocator, buffer: *TerminalBuffer, user_list: *UserList, width: usize, text_in_center: bool, fg: u32, bg: u32, ) !Session { return .{ .label = try EnvironmentLabel.init( allocator, buffer, drawItem, sessionChanged, user_list, width, text_in_center, fg, bg, ), .user_list = user_list, }; } pub fn deinit(self: *Session) void { for (self.label.list.items) |*env| { if (env.environment.entry_ini) |*entry_ini| entry_ini.deinit(); self.label.allocator.free(env.environment.file_name); } self.label.deinit(); } pub fn widget(self: *Session) Widget { return Widget.init( "Session", self.label.keybinds, self, deinit, null, draw, null, handle, null, ); } pub fn addEnvironment(self: *Session, environment: Environment) !void { const env = Env{ .environment = environment, .index = self.label.list.items.len }; try self.label.addItem(env); addedSession(env, self.user_list); } fn draw(self: *Session) void { self.label.draw(); } fn handle(self: *Session, maybe_key: ?keyboard.Key) !void { self.label.handle(maybe_key); } fn addedSession(env: Env, user_list: *UserList) void { const user = user_list.label.list.items[user_list.label.current]; if (!user.first_run) return; user.session_index.* = env.index; } fn sessionChanged(env: Env, maybe_user_list: ?*UserList) void { if (maybe_user_list) |user_list| { user_list.label.list.items[user_list.label.current].session_index.* = env.index; } } fn drawItem(label: *EnvironmentLabel, env: Env, x: usize, y: usize, width: usize) void { if (width < 3) return; const length = @min(TerminalBuffer.strWidth(env.environment.name), width - 3); if (length == 0) return; const x_offset = if (label.text_in_center and width >= length) (width - length) / 2 else 0; label.cursor = length + x_offset; TerminalBuffer.drawConfinedText( env.environment.name, x + x_offset, y, width, label.fg, label.bg, ); } ================================================ FILE: src/components/UserList.zig ================================================ const std = @import("std"); const Allocator = std.mem.Allocator; const ly_ui = @import("ly-ui"); const keyboard = ly_ui.keyboard; const TerminalBuffer = ly_ui.TerminalBuffer; const Widget = ly_ui.Widget; const CyclableLabel = ly_ui.CyclableLabel; const Session = @import("Session.zig"); const SavedUsers = @import("../config/SavedUsers.zig"); const StringList = std.ArrayListUnmanaged([]const u8); pub const User = struct { name: []const u8, session_index: *usize, allocated_index: bool, first_run: bool, }; const UserLabel = CyclableLabel(User, *Session); const UserList = @This(); label: *UserLabel, pub fn init( allocator: Allocator, buffer: *TerminalBuffer, usernames: StringList, saved_users: *SavedUsers, session: *Session, width: usize, text_in_center: bool, fg: u32, bg: u32, ) !UserList { var user_list = UserList{ .label = try UserLabel.init( allocator, buffer, drawItem, usernameChanged, session, width, text_in_center, fg, bg, ), }; for (usernames.items) |username| { if (username.len == 0) continue; var maybe_session_index: ?*usize = null; var first_run = true; for (saved_users.user_list.items) |*saved_user| { if (std.mem.eql(u8, username, saved_user.username)) { maybe_session_index = &saved_user.session_index; first_run = saved_user.first_run; break; } } var allocated_index = false; if (maybe_session_index == null) { maybe_session_index = try allocator.create(usize); maybe_session_index.?.* = 0; allocated_index = true; } try user_list.label.addItem(.{ .name = username, .session_index = maybe_session_index.?, .allocated_index = allocated_index, .first_run = first_run, }); } return user_list; } pub fn deinit(self: *UserList) void { for (self.label.list.items) |user| { if (user.allocated_index) { self.label.allocator.destroy(user.session_index); } } self.label.deinit(); } pub fn widget(self: *UserList) Widget { return Widget.init( "UserList", self.label.keybinds, self, deinit, null, draw, null, handle, null, ); } pub fn getCurrentUsername(self: UserList) []const u8 { return self.label.list.items[self.label.current].name; } fn draw(self: *UserList) void { self.label.draw(); } fn handle(self: *UserList, maybe_key: ?keyboard.Key) !void { self.label.handle(maybe_key); } fn usernameChanged(user: User, maybe_session: ?*Session) void { if (maybe_session) |session| { session.label.current = @min(user.session_index.*, session.label.list.items.len - 1); } } fn drawItem(label: *UserLabel, user: User, x: usize, y: usize, width: usize) void { if (width < 3) return; const length = @min(TerminalBuffer.strWidth(user.name), width - 3); if (length == 0) return; const x_offset = if (label.text_in_center and width >= length) (width - length) / 2 else 0; label.cursor = length + x_offset; TerminalBuffer.drawConfinedText( user.name, x + x_offset, y, width, label.fg, label.bg, ); } ================================================ FILE: src/config/Config.zig ================================================ const build_options = @import("build_options"); const enums = @import("../enums.zig"); const Animation = enums.Animation; const Input = enums.Input; const ViMode = enums.ViMode; const Bigclock = enums.Bigclock; const DurOffsetAlignment = enums.DurOffsetAlignment; allow_empty_password: bool = true, animation: Animation = .none, animation_frame_delay: u16 = 5, animation_timeout_sec: u12 = 0, asterisk: ?u32 = '*', auth_fails: u64 = 10, battery_id: ?[]const u8 = null, auto_login_service: [:0]const u8 = "ly-autologin", auto_login_session: ?[]const u8 = null, auto_login_user: ?[]const u8 = null, bg: u32 = 0x00000000, bigclock: Bigclock = .none, bigclock_12hr: bool = false, bigclock_seconds: bool = false, blank_box: bool = true, border_fg: u32 = 0x00FFFFFF, box_title: ?[]const u8 = null, brightness_down_cmd: [:0]const u8 = build_options.prefix_directory ++ "/bin/brightnessctl -q -n s 10%-", brightness_down_key: ?[]const u8 = "F5", brightness_up_cmd: [:0]const u8 = build_options.prefix_directory ++ "/bin/brightnessctl -q -n s +10%", brightness_up_key: ?[]const u8 = "F6", clear_password: bool = false, clock: ?[:0]const u8 = null, cmatrix_fg: u32 = 0x0000FF00, cmatrix_head_col: u32 = 0x01FFFFFF, cmatrix_min_codepoint: u16 = 0x21, cmatrix_max_codepoint: u16 = 0x7B, colormix_col1: u32 = 0x00FF0000, colormix_col2: u32 = 0x000000FF, colormix_col3: u32 = 0x20000000, custom_sessions: []const u8 = build_options.config_directory ++ "/ly/custom-sessions", default_input: Input = .login, doom_fire_height: u8 = 6, doom_fire_spread: u8 = 2, doom_top_color: u32 = 0x00FF0000, doom_middle_color: u32 = 0x00FFFF00, doom_bottom_color: u32 = 0x00FFFFFF, dur_file_path: []const u8 = build_options.config_directory ++ "/ly/example.dur", dur_offset_alignment: DurOffsetAlignment = .center, dur_x_offset: i32 = 0, dur_y_offset: i32 = 0, edge_margin: u8 = 0, error_bg: u32 = 0x00000000, error_fg: u32 = 0x01FF0000, fg: u32 = 0x00FFFFFF, full_color: bool = true, gameoflife_fg: u32 = 0x0000FF00, gameoflife_entropy_interval: usize = 10, gameoflife_frame_delay: usize = 6, gameoflife_initial_density: f32 = 0.4, hibernate_cmd: ?[]const u8 = null, hibernate_key: []const u8 = "F4", hide_borders: bool = false, hide_key_hints: bool = false, hide_keyboard_locks: bool = false, hide_version_string: bool = false, inactivity_cmd: ?[]const u8 = null, inactivity_delay: u16 = 0, initial_info_text: ?[]const u8 = null, input_len: u8 = 34, lang: []const u8 = "en", login_cmd: ?[]const u8 = null, login_defs_path: []const u8 = "/etc/login.defs", logout_cmd: ?[]const u8 = null, ly_log: []const u8 = "/var/log/ly.log", margin_box_h: u8 = 2, margin_box_v: u8 = 1, numlock: bool = false, path: ?[]const u8 = "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", restart_cmd: []const u8 = "/sbin/shutdown -r now", restart_key: []const u8 = "F2", save: bool = true, service_name: [:0]const u8 = "ly", session_log: ?[]const u8 = "ly-session.log", setup_cmd: []const u8 = build_options.config_directory ++ "/ly/setup.sh", show_password_key: []const u8 = "F7", shutdown_cmd: []const u8 = "/sbin/shutdown -a now", shutdown_key: []const u8 = "F1", sleep_cmd: ?[]const u8 = null, sleep_key: []const u8 = "F3", start_cmd: ?[]const u8 = null, text_in_center: bool = false, vi_default_mode: ViMode = .normal, vi_mode: bool = false, waylandsessions: []const u8 = build_options.prefix_directory ++ "/share/wayland-sessions", x_cmd: []const u8 = build_options.prefix_directory ++ "/bin/X", x_vt: ?u8 = null, xauth_cmd: []const u8 = build_options.prefix_directory ++ "/bin/xauth", xinitrc: ?[]const u8 = "~/.xinitrc", xsessions: []const u8 = build_options.prefix_directory ++ "/share/xsessions", ================================================ FILE: src/config/Lang.zig ================================================ // // NOTE: After editing this file, please run `/res/lang/normalize_lang_files.py` // to update all the language files accordingly. // authenticating: []const u8 = "authenticating...", brightness_down: []const u8 = "decrease brightness", brightness_up: []const u8 = "increase brightness", capslock: []const u8 = "capslock", custom: []const u8 = "custom", err_alloc: []const u8 = "failed memory allocation", err_args: []const u8 = "unable to parse command line arguments", err_autologin_session: []const u8 = "autologin session not found", err_bounds: []const u8 = "out-of-bounds index", err_brightness_change: []const u8 = "failed to change brightness", err_chdir: []const u8 = "failed to open home folder", err_clock_too_long: []const u8 = "clock string too long", err_config: []const u8 = "unable to parse config file", err_crawl: []const u8 = "failed to crawl session directories", err_dgn_oob: []const u8 = "log message", err_domain: []const u8 = "invalid domain", err_empty_password: []const u8 = "empty password not allowed", err_envlist: []const u8 = "failed to get envlist", err_get_active_tty: []const u8 = "failed to get active tty", err_hibernate: []const u8 = "failed to execute hibernate command", err_hostname: []const u8 = "failed to get hostname", err_inactivity: []const u8 = "failed to execute inactivity command", err_lock_state: []const u8 = "failed to get lock state", err_log: []const u8 = "failed to open log file", err_mlock: []const u8 = "failed to lock password memory", err_null: []const u8 = "null pointer", err_numlock: []const u8 = "failed to set numlock", err_pam: []const u8 = "pam transaction failed", err_pam_abort: []const u8 = "pam transaction aborted", err_pam_acct_expired: []const u8 = "account expired", err_pam_auth: []const u8 = "authentication error", err_pam_authinfo_unavail: []const u8 = "failed to get user info", err_pam_authok_reqd: []const u8 = "token expired", err_pam_buf: []const u8 = "memory buffer error", err_pam_cred_err: []const u8 = "failed to set credentials", err_pam_cred_expired: []const u8 = "credentials expired", err_pam_cred_insufficient: []const u8 = "insufficient credentials", err_pam_cred_unavail: []const u8 = "failed to get credentials", err_pam_maxtries: []const u8 = "reached maximum tries limit", err_pam_perm_denied: []const u8 = "permission denied", err_pam_session: []const u8 = "session error", err_pam_sys: []const u8 = "system error", err_pam_user_unknown: []const u8 = "unknown user", err_path: []const u8 = "failed to set path", err_perm_dir: []const u8 = "failed to change current directory", err_perm_group: []const u8 = "failed to downgrade group permissions", err_perm_user: []const u8 = "failed to downgrade user permissions", err_pwnam: []const u8 = "failed to get user info", err_sleep: []const u8 = "failed to execute sleep command", err_start: []const u8 = "failed to execute start command", err_battery: []const u8 = "failed to load battery status", err_switch_tty: []const u8 = "failed to switch tty", err_tty_ctrl: []const u8 = "tty control transfer failed", err_no_users: []const u8 = "no users found", err_uid_range: []const u8 = "failed to dynamically get uid range", err_user_gid: []const u8 = "failed to set user GID", err_user_init: []const u8 = "failed to initialize user", err_user_uid: []const u8 = "failed to set user UID", err_xauth: []const u8 = "xauth command failed", err_xcb_conn: []const u8 = "xcb connection failed", err_xsessions_dir: []const u8 = "failed to find sessions folder", err_xsessions_open: []const u8 = "failed to open sessions folder", hibernate: []const u8 = "hibernate", insert: []const u8 = "insert", login: []const u8 = "login", logout: []const u8 = "logged out", no_x11_support: []const u8 = "x11 support disabled at compile-time", normal: []const u8 = "normal", numlock: []const u8 = "numlock", other: []const u8 = "other", password: []const u8 = "password", restart: []const u8 = "reboot", shell: [:0]const u8 = "shell", shutdown: []const u8 = "shutdown", sleep: []const u8 = "sleep", toggle_password: []const u8 = "toggle password", wayland: []const u8 = "wayland", x11: []const u8 = "x11", xinitrc: [:0]const u8 = "xinitrc", ================================================ FILE: src/config/OldSave.zig ================================================ user: ?[]const u8 = null, session_index: ?usize = null, ================================================ FILE: src/config/SavedUsers.zig ================================================ const std = @import("std"); const SavedUsers = @This(); const User = struct { username: []const u8, session_index: usize, first_run: bool, allocated_username: bool, }; user_list: std.ArrayList(User), last_username_index: ?usize, pub fn init() SavedUsers { return .{ .user_list = .empty, .last_username_index = null, }; } pub fn deinit(self: *SavedUsers, allocator: std.mem.Allocator) void { for (self.user_list.items) |user| { if (user.allocated_username) allocator.free(user.username); } self.user_list.deinit(allocator); } ================================================ FILE: src/config/migrator.zig ================================================ // The migrator ensures compatibility with older configuration files // Properties removed or changed since 0.6.0 // Color codes interpreted differently since 1.1.0 const std = @import("std"); var temporary_allocator = std.heap.page_allocator; const ly_ui = @import("ly-ui"); const TerminalBuffer = ly_ui.TerminalBuffer; const Color = TerminalBuffer.Color; const Styling = TerminalBuffer.Styling; const ly_core = ly_ui.ly_core; const IniParser = ly_core.IniParser; const ini = ly_core.ini; const Config = @import("Config.zig"); const OldSave = @import("OldSave.zig"); const SavedUsers = @import("SavedUsers.zig"); const color_properties = [_][]const u8{ "bg", "border_fg", "cmatrix_fg", "colormix_col1", "colormix_col2", "colormix_col3", "error_bg", "error_fg", "fg", }; var set_color_properties = [_]bool{ false, false, false, false, false, false, false, false, false }; const removed_properties = [_][]const u8{ "wayland_specifier", "max_desktop_len", "max_login_len", "max_password_len", "mcookie_cmd", "term_reset_cmd", "term_restore_cursor_cmd", "x_cmd_setup", "wayland_cmd", "console_dev", "load", }; pub var auto_eight_colors: bool = true; pub var maybe_animate: ?bool = null; pub var maybe_save_file: ?[]const u8 = null; pub fn configFieldHandler(_: std.mem.Allocator, field: ini.IniField) ?ini.IniField { if (std.mem.eql(u8, field.key, "animate")) { // The option doesn't exist anymore, but we save its value for "animation" maybe_animate = std.mem.eql(u8, field.value, "true"); return null; } if (std.mem.eql(u8, field.key, "animation")) { // The option now uses a string (which then gets converted into an enum) instead of an integer // It also combines the previous "animate" and "animation" options const animation = std.fmt.parseInt(u8, field.value, 10) catch return field; var mapped_field = field; mapped_field.value = switch (animation) { 0 => "doom", 1 => "matrix", else => "none", }; return mapped_field; } inline for (color_properties, &set_color_properties) |property, *status| { if (std.mem.eql(u8, field.key, property)) { // Color has been set; it won't be overwritten if we default to eight-color output status.* = true; // These options now uses a 32-bit RGB value instead of an arbitrary 16-bit integer // If they're all using eight-color codes, we start in eight-color mode const color = std.fmt.parseInt(u16, field.value, 0) catch { auto_eight_colors = false; return field; }; const color_no_styling = color & 0x00FF; const styling_only = color & 0xFF00; // If color is "greater" than TB_WHITE, or the styling is "greater" than TB_DIM, // we have an invalid color, so do not use eight-color mode if (color_no_styling > 0x0008 or styling_only > 0x8000) auto_eight_colors = false; return field; } } if (std.mem.eql(u8, field.key, "blank_password")) { // The option has simply been renamed var mapped_field = field; mapped_field.key = "clear_password"; return mapped_field; } if (std.mem.eql(u8, field.key, "default_input")) { // The option now uses a string (which then gets converted into an enum) instead of an integer const default_input = std.fmt.parseInt(u8, field.value, 10) catch return field; var mapped_field = field; mapped_field.value = switch (default_input) { 0 => "session", 1 => "login", 2 => "password", else => "login", }; return mapped_field; } if (std.mem.eql(u8, field.key, "save_file")) { // The option doesn't exist anymore, but we save its value for migration later on maybe_save_file = temporary_allocator.dupe(u8, field.value) catch return null; return null; } inline for (removed_properties) |property| { if (std.mem.eql(u8, field.key, property)) { // The options don't exist anymore return null; } } if (std.mem.eql(u8, field.key, "bigclock")) { // The option now uses a string (which then gets converted into an enum) instead of an boolean // It also includes the ability to change active bigclock's language var mapped_field = field; if (std.mem.eql(u8, field.value, "true")) { mapped_field.value = "en"; } else if (std.mem.eql(u8, field.value, "false")) { mapped_field.value = "none"; } return mapped_field; } if (std.mem.eql(u8, field.key, "full_color")) { // If color mode is defined, definitely don't set it automatically auto_eight_colors = false; return field; } if (std.mem.eql(u8, field.key, "min_refresh_delta")) { // The option has simply been renamed var mapped_field = field; mapped_field.key = "animation_frame_delay"; return mapped_field; } return field; } // This is the stuff we only handle after reading the config. // For example, the "animate" field could come after "animation" pub fn lateConfigFieldHandler(config: *Config) void { if (maybe_animate) |animate| { if (!animate) config.*.animation = .none; } if (auto_eight_colors) { // Valid config file predates true-color mode // Will use eight-color output instead config.full_color = false; // We cannot rely on Config defaults when in eight-color mode, // because they will appear as undesired colors. // Instead set color properties to matching eight-color codes config.doom_top_color = Color.ECOL_RED; config.doom_middle_color = Color.ECOL_YELLOW; config.doom_bottom_color = Color.ECOL_WHITE; config.cmatrix_head_col = Styling.BOLD | Color.ECOL_WHITE; // These may be in the config, so only change those which were not set if (!set_color_properties[0]) config.bg = Color.DEFAULT; if (!set_color_properties[1]) config.border_fg = Color.ECOL_WHITE; if (!set_color_properties[2]) config.cmatrix_fg = Color.ECOL_GREEN; if (!set_color_properties[3]) config.colormix_col1 = Color.ECOL_RED; if (!set_color_properties[4]) config.colormix_col2 = Color.ECOL_BLUE; if (!set_color_properties[5]) config.colormix_col3 = Color.ECOL_BLACK; if (!set_color_properties[6]) config.error_bg = Color.DEFAULT; if (!set_color_properties[7]) config.error_fg = Styling.BOLD | Color.ECOL_RED; if (!set_color_properties[8]) config.fg = Color.ECOL_WHITE; } } pub fn tryMigrateIniSaveFile(allocator: std.mem.Allocator, path: []const u8, saved_users: *SavedUsers, usernames: [][]const u8) !?IniParser(OldSave) { var save_parser = try IniParser(OldSave).init(allocator, path, null); errdefer save_parser.deinit(); var user_buf: [32]u8 = undefined; const maybe_save = if (save_parser.maybe_load_error == null) save_parser.structure else tryMigrateFirstSaveFile(&user_buf); if (maybe_save) |save| { // Add all other users to the list for (usernames, 0..) |username, i| { if (save.user) |user| { if (std.mem.eql(u8, user, username)) saved_users.last_username_index = i; } try saved_users.user_list.append(allocator, .{ .username = username, .session_index = save.session_index orelse 0, .first_run = false, .allocated_username = false, }); } return save_parser; } return null; } fn tryMigrateFirstSaveFile(user_buf: *[32]u8) ?OldSave { if (maybe_save_file) |path| { defer temporary_allocator.free(path); var save = OldSave{}; var file = std.fs.openFileAbsolute(path, .{}) catch return null; defer file.close(); var file_buffer: [64]u8 = undefined; var file_reader = file.reader(&file_buffer); var reader = &file_reader.interface; var user_writer = std.Io.Writer.fixed(user_buf); var written = reader.streamDelimiter(&user_writer, '\n') catch return null; if (written > 0) save.user = user_buf[0..written]; var session_buf: [20]u8 = undefined; var session_writer = std.Io.Writer.fixed(&session_buf); written = reader.streamDelimiter(&session_writer, '\n') catch return null; var session_index: ?usize = null; if (written > 0) { session_index = std.fmt.parseUnsigned(usize, session_buf[0..written], 10) catch return null; } save.session_index = session_index; return save; } return null; } ================================================ FILE: src/enums.zig ================================================ const std = @import("std"); pub const Animation = enum { none, doom, matrix, colormix, gameoflife, dur_file, }; pub const DisplayServer = enum { wayland, shell, xinitrc, x11, custom, }; pub const Input = enum { info_line, session, login, password, /// Moves the current Input forwards by one entry. If `reverse`, then the Input /// moves backwards. If `wrap` is true, then the entry will wrap back around pub fn move(self: *Input, reverse: bool, wrap: bool) void { const maxNum = @typeInfo(Input).@"enum".fields.len - 1; const selfNum = @intFromEnum(self.*); if (reverse) { if (wrap) { self.* = @enumFromInt(selfNum -% 1); } else if (selfNum != 0) { self.* = @enumFromInt(selfNum - 1); } } else { if (wrap) { self.* = @enumFromInt(selfNum +% 1); } else if (selfNum != maxNum) { self.* = @enumFromInt(selfNum + 1); } } } }; pub const ViMode = enum { normal, insert, }; pub const Bigclock = enum { none, en, fa, }; pub const DurOffsetAlignment = enum { topleft, topcenter, topright, centerleft, center, centerright, bottomleft, bottomcenter, bottomright, }; ================================================ FILE: src/main.zig ================================================ const std = @import("std"); const Allocator = std.mem.Allocator; const StringList = std.ArrayListUnmanaged([]const u8); const temporary_allocator = std.heap.page_allocator; const builtin = @import("builtin"); const build_options = @import("build_options"); const clap = @import("clap"); const ly_ui = @import("ly-ui"); const Position = ly_ui.Position; const BigLabel = ly_ui.BigLabel; const CenteredBox = ly_ui.CenteredBox; const Label = ly_ui.Label; const Text = ly_ui.Text; const TerminalBuffer = ly_ui.TerminalBuffer; const Widget = ly_ui.Widget; const ly_core = ly_ui.ly_core; const interop = ly_core.interop; const UidRange = ly_core.UidRange; const LogFile = ly_core.LogFile; const SharedError = ly_core.SharedError; const IniParser = ly_core.IniParser; const ini = ly_core.ini; const Ini = ini.Ini; const Cascade = @import("animations/Cascade.zig"); const ColorMix = @import("animations/ColorMix.zig"); 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 auth = @import("auth.zig"); const InfoLine = @import("components/InfoLine.zig"); const Session = @import("components/Session.zig"); const UserList = @import("components/UserList.zig"); const Config = @import("config/Config.zig"); 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 DisplayServer = @import("enums.zig").DisplayServer; const Environment = @import("Environment.zig"); const Entry = Environment.Entry; const ly_version_str = "Ly version " ++ build_options.version; var session_pid: std.posix.pid_t = -1; fn signalHandler(i: c_int) callconv(.c) void { if (session_pid == 0) return; // Forward signal to session to clean up if (session_pid > 0) { _ = std.c.kill(session_pid, i); var status: c_int = 0; _ = std.c.waitpid(session_pid, &status, 0); } TerminalBuffer.shutdown(); std.c.exit(i); } fn ttyControlTransferSignalHandler(_: c_int) callconv(.c) void { TerminalBuffer.shutdown(); } const UiState = struct { allocator: Allocator, auth_fails: u64, is_autologin: bool, use_kmscon_vt: bool, active_tty: u8, buffer: TerminalBuffer, labels_max_length: usize, shutdown_label: Label, restart_label: Label, sleep_label: Label, hibernate_label: Label, toggle_password_label: Label, brightness_down_label: Label, brightness_up_label: Label, numlock_label: Label, capslock_label: Label, battery_label: Label, clock_label: Label, session_specifier_label: Label, login_label: Label, password_label: Label, version_label: Label, bigclock_label: BigLabel, box: CenteredBox, info_line: InfoLine, animate: bool, session: Session, saved_users: SavedUsers, login: UserList, password: *Text, password_widget: Widget, insert_mode: bool, edge_margin: Position, config: Config, lang: Lang, log_file: LogFile, save_path: []const u8, old_save_path: []const u8, has_old_save: bool, battery_buf: [16:0]u8, bigclock_format_buf: [16:0]u8, clock_buf: [64:0]u8, bigclock_buf: [32:0]u8, }; var shutdown = false; var restart = false; pub fn main() !void { var shutdown_cmd: []const u8 = undefined; var restart_cmd: []const u8 = undefined; var commands_allocated = false; var state: UiState = undefined; var stderr_buffer: [128]u8 = undefined; var stderr_writer = std.fs.File.stderr().writer(&stderr_buffer); var stderr = &stderr_writer.interface; 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 if (commands_allocated) { // Necessary if we error out before allocating temporary_allocator.free(shutdown_cmd); temporary_allocator.free(restart_cmd); } } } var gpa = std.heap.DebugAllocator(.{}).init; defer _ = gpa.deinit(); state.allocator = gpa.allocator(); // Load arguments const params = comptime clap.parseParamsComptime( \\-h, --help Shows all commands. \\-v, --version Shows the version of Ly. \\-c, --config Overrides the default configuration path. Example: --config /usr/share/ly \\--use-kmscon-vt Use KMSCON instead of kernel VT ); var diag = clap.Diagnostic{}; var arg_parse_error: anyerror = undefined; var maybe_res = clap.parse(clap.Help, ¶ms, clap.parsers.default, .{ .diagnostic = &diag, .allocator = state.allocator }) catch |err| parse_error: { arg_parse_error = err; diag.report(stderr, err) catch {}; try stderr.flush(); break :parse_error null; }; defer if (maybe_res) |*res| res.deinit(); var old_save_parser: ?IniParser(OldSave) = null; defer if (old_save_parser) |*str| str.deinit(); state.use_kmscon_vt = false; var start_cmd_exit_code: u8 = 0; state.saved_users = SavedUsers.init(); defer state.saved_users.deinit(state.allocator); var config_parent_path: []const u8 = build_options.config_directory ++ "/ly"; if (maybe_res) |*res| { if (res.args.help != 0) { 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); } if (res.args.config) |path| config_parent_path = path; if (res.args.@"use-kmscon-vt" != 0) state.use_kmscon_vt = true; } // Load configuration file var save_path_alloc = false; state.save_path = build_options.config_directory ++ "/ly/save.txt"; state.old_save_path = build_options.config_directory ++ "/ly/save.ini"; defer if (save_path_alloc) { state.allocator.free(state.save_path); state.allocator.free(state.old_save_path); }; const config_path = try std.fs.path.join(state.allocator, &[_][]const u8{ config_parent_path, "config.ini" }); defer state.allocator.free(config_path); var config_parser = try IniParser(Config).init(state.allocator, config_path, migrator.configFieldHandler); defer config_parser.deinit(); 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}); const lang_path = try std.fs.path.join(state.allocator, &[_][]const u8{ config_parent_path, "lang", lang_file }); defer state.allocator.free(lang_path); var lang_parser = try IniParser(Lang).init(state.allocator, lang_path, null); defer lang_parser.deinit(); state.lang = lang_parser.structure; if (state.config.save) { state.save_path = try std.fs.path.join(state.allocator, &[_][]const u8{ config_parent_path, "save.txt" }); state.old_save_path = try std.fs.path.join(state.allocator, &[_][]const u8{ config_parent_path, "save.ini" }); save_path_alloc = true; } if (config_parser.maybe_load_error == null) { migrator.lateConfigFieldHandler(&state.config); } var maybe_uid_range_error: ?anyerror = null; var usernames = try getAllUsernames(state.allocator, state.config.login_defs_path, &maybe_uid_range_error); defer { for (usernames.items) |username| state.allocator.free(username); usernames.deinit(state.allocator); } state.has_old_save = false; if (state.config.save) read_save_file: { old_save_parser = migrator.tryMigrateIniSaveFile(state.allocator, state.old_save_path, &state.saved_users, usernames.items) catch break :read_save_file; // Don't read the new save file if the old one still exists if (old_save_parser != null) { state.has_old_save = true; break :read_save_file; } var save_file = std.fs.cwd().openFile(state.save_path, .{}) catch break :read_save_file; defer save_file.close(); var file_buffer: [256]u8 = undefined; var file_reader = save_file.reader(&file_buffer); var reader = &file_reader.interface; const last_username_index_str = reader.takeDelimiterInclusive('\n') catch break :read_save_file; state.saved_users.last_username_index = std.fmt.parseInt(usize, last_username_index_str[0..(last_username_index_str.len - 1)], 10) catch break :read_save_file; while (reader.seek < reader.buffer.len) { const line = reader.takeDelimiterInclusive('\n') catch break; var user = std.mem.splitScalar(u8, line[0..(line.len - 1)], ':'); const username = user.next() orelse continue; const session_index_str = user.next() orelse continue; const session_index = std.fmt.parseInt(usize, session_index_str, 10) catch continue; try state.saved_users.user_list.append(state.allocator, .{ .username = try state.allocator.dupe(u8, username), .session_index = session_index, .first_run = false, .allocated_username = true, }); } } // If no save file previously existed, fill it up with all usernames // TODO: Add new username with existing save file if (state.config.save and state.saved_users.user_list.items.len == 0) { for (usernames.items) |user| { try state.saved_users.user_list.append(state.allocator, .{ .username = user, .session_index = 0, .first_run = true, .allocated_username = false, }); } } var log_file_buffer: [1024]u8 = undefined; state.log_file = try LogFile.init(state.config.ly_log, &log_file_buffer); defer state.log_file.deinit(); try state.log_file.info("tui", "using {s} vt", .{if (state.use_kmscon_vt) "kmscon" else "default"}); // 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 shutdown_cmd = try temporary_allocator.dupe(u8, state.config.shutdown_cmd); restart_cmd = try temporary_allocator.dupe(u8, state.config.restart_cmd); commands_allocated = true; if (state.config.start_cmd) |start_cmd| { var start = std.process.Child.init(&[_][]const u8{ "/bin/sh", "-c", start_cmd }, state.allocator); start.stdout_behavior = .Inherit; start.stderr_behavior = .Ignore; handle_start_cmd: { const process_result = start.spawnAndWait() catch { break :handle_start_cmd; }; start_cmd_exit_code = process_result.Exited; } } // Initialize terminal buffer try state.log_file.info("tui", "initializing terminal buffer", .{}); state.labels_max_length = @max(TerminalBuffer.strWidth(state.lang.login), TerminalBuffer.strWidth(state.lang.password)); var seed: u64 = undefined; std.crypto.random.bytes(std.mem.asBytes(&seed)); // Get a random seed for the PRNG (used by animations) var prng = std.Random.DefaultPrng.init(seed); const random = prng.random(); const buffer_options = TerminalBuffer.InitOptions{ .fg = state.config.fg, .bg = state.config.bg, .border_fg = state.config.border_fg, .full_color = state.config.full_color, .is_tty = true, }; state.buffer = try TerminalBuffer.init( state.allocator, buffer_options, &state.log_file, random, ); defer { state.log_file.info("tui", "shutting down terminal buffer", .{}) catch {}; state.buffer.deinit(); } const act = std.posix.Sigaction{ .handler = .{ .handler = &signalHandler }, .mask = std.posix.sigemptyset(), .flags = 0, }; std.posix.sigaction(std.posix.SIG.TERM, &act, null); // Initialize components state.shutdown_label = Label.init( "", null, state.buffer.fg, state.buffer.bg, null, null, ); defer state.shutdown_label.deinit(); state.restart_label = Label.init( "", null, state.buffer.fg, state.buffer.bg, null, null, ); defer state.restart_label.deinit(); state.sleep_label = Label.init( "", null, state.buffer.fg, state.buffer.bg, null, null, ); defer state.sleep_label.deinit(); state.hibernate_label = Label.init( "", null, state.buffer.fg, state.buffer.bg, null, null, ); defer state.hibernate_label.deinit(); state.toggle_password_label = Label.init( "", null, state.buffer.fg, state.buffer.bg, null, null, ); defer state.toggle_password_label.deinit(); state.brightness_down_label = Label.init( "", null, state.buffer.fg, state.buffer.bg, null, null, ); defer state.brightness_down_label.deinit(); state.brightness_up_label = Label.init( "", null, state.buffer.fg, state.buffer.bg, null, null, ); defer state.brightness_up_label.deinit(); if (!state.config.hide_key_hints) { try state.shutdown_label.setTextAlloc( state.allocator, "{s} {s}", .{ state.config.shutdown_key, state.lang.shutdown }, ); try state.restart_label.setTextAlloc( state.allocator, "{s} {s}", .{ state.config.restart_key, state.lang.restart }, ); try state.toggle_password_label.setTextAlloc( state.allocator, "{s} {s}", .{ state.config.show_password_key, state.lang.toggle_password }, ); if (state.config.sleep_cmd != null) { try state.sleep_label.setTextAlloc( state.allocator, "{s} {s}", .{ state.config.sleep_key, state.lang.sleep }, ); } if (state.config.hibernate_cmd != null) { try state.hibernate_label.setTextAlloc( state.allocator, "{s} {s}", .{ state.config.hibernate_key, state.lang.hibernate }, ); } if (state.config.brightness_down_key) |key| { try state.brightness_down_label.setTextAlloc( state.allocator, "{s} {s}", .{ key, state.lang.brightness_down }, ); } if (state.config.brightness_up_key) |key| { try state.brightness_up_label.setTextAlloc( state.allocator, "{s} {s}", .{ key, state.lang.brightness_up }, ); } } state.numlock_label = Label.init( "", null, state.buffer.fg, state.buffer.bg, &updateNumlock, null, ); defer state.numlock_label.deinit(); state.capslock_label = Label.init( "", null, state.buffer.fg, state.buffer.bg, &updateCapslock, null, ); defer state.capslock_label.deinit(); state.battery_label = Label.init( "", null, state.buffer.fg, state.buffer.bg, &updateBattery, null, ); defer state.battery_label.deinit(); state.clock_label = Label.init( "", null, state.buffer.fg, state.buffer.bg, &updateClock, &calculateClockTimeout, ); defer state.clock_label.deinit(); state.bigclock_label = BigLabel.init( &state.buffer, "", null, state.buffer.fg, state.buffer.bg, switch (state.config.bigclock) { .none, .en => .en, .fa => .fa, }, &updateBigClock, &calculateBigClockTimeout, ); defer state.bigclock_label.deinit(); state.box = CenteredBox.init( &state.buffer, state.config.margin_box_h, state.config.margin_box_v, (2 * state.config.margin_box_h) + state.config.input_len + 1 + state.labels_max_length, 7 + (2 * state.config.margin_box_v), !state.config.hide_borders, state.config.blank_box, state.config.box_title, null, state.buffer.border_fg, state.buffer.fg, state.buffer.bg, &updateBox, ); state.info_line = try InfoLine.init( state.allocator, &state.buffer, state.box.width - 2 * state.box.horizontal_margin, state.buffer.fg, state.buffer.bg, ); defer state.info_line.deinit(); try state.buffer.registerKeybind(&state.info_line.label.keybinds, "H", &viGoLeft, &state); try state.buffer.registerKeybind(&state.info_line.label.keybinds, "L", &viGoRight, &state); if (maybe_res == null) { var longest = diag.name.longest(); if (longest.kind == .positional) longest.name = diag.arg; try state.info_line.addMessage( state.lang.err_args, state.config.error_bg, state.config.error_fg, ); try state.log_file.err( "cli", "unable to parse argument '{s}{s}': {s}", .{ longest.kind.prefix(), longest.name, @errorName(arg_parse_error) }, ); } if (maybe_uid_range_error) |err| { try state.info_line.addMessage( state.lang.err_uid_range, state.config.error_bg, state.config.error_fg, ); try state.log_file.err( "sys", "failed to get uid range: {s}; falling back to default", .{@errorName(err)}, ); } if (start_cmd_exit_code != 0) { try state.info_line.addMessage( state.lang.err_start, state.config.error_bg, state.config.error_fg, ); try state.log_file.err( "sys", "failed to execute start command: exit code {d}", .{start_cmd_exit_code}, ); } 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", state.config.error_bg, state.config.error_fg, ); try state.log_file.err( "conf", "unable to parse config file: {s}", .{@errorName(load_error)}, ); for (config_parser.errors.items) |err| { try state.log_file.err( "conf", "failed to convert value '{s}' of option '{s}' to type '{s}': {s}", .{ err.value, err.key, err.type_name, err.error_name }, ); } } if (!state.log_file.could_open_log_file) { try state.info_line.addMessage( state.lang.err_log, state.config.error_bg, state.config.error_fg, ); try state.log_file.err( "sys", "failed to open log file", .{}, ); } interop.setNumlock(state.config.numlock) catch |err| { try state.info_line.addMessage( state.lang.err_numlock, state.config.error_bg, state.config.error_fg, ); try state.log_file.err( "sys", "failed to set numlock: {s}", .{@errorName(err)}, ); }; state.session_specifier_label = Label.init( "", null, state.buffer.fg, state.buffer.bg, &updateSessionSpecifier, null, ); defer state.session_specifier_label.deinit(); state.session = try Session.init( state.allocator, &state.buffer, &state.login, state.box.width - 2 * state.box.horizontal_margin - state.labels_max_length - 1, state.config.text_in_center, state.buffer.fg, state.buffer.bg, ); defer state.session.deinit(); try state.buffer.registerKeybind(&state.session.label.keybinds, "H", &viGoLeft, &state); try state.buffer.registerKeybind(&state.session.label.keybinds, "L", &viGoRight, &state); state.login_label = Label.init( state.lang.login, null, state.buffer.fg, state.buffer.bg, null, null, ); defer state.login_label.deinit(); state.login = try UserList.init( state.allocator, &state.buffer, usernames, &state.saved_users, &state.session, state.box.width - 2 * state.box.horizontal_margin - state.labels_max_length - 1, state.config.text_in_center, state.buffer.fg, state.buffer.bg, ); defer state.login.deinit(); try state.buffer.registerKeybind(&state.login.label.keybinds, "H", &viGoLeft, &state); try state.buffer.registerKeybind(&state.login.label.keybinds, "L", &viGoRight, &state); addOtherEnvironment(&state.session, state.lang, .shell, null) catch |err| { try state.info_line.addMessage( state.lang.err_alloc, state.config.error_bg, state.config.error_fg, ); try state.log_file.err( "sys", "failed to add shell environment: {s}", .{@errorName(err)}, ); }; if (build_options.enable_x11_support) { if (state.config.xinitrc) |xinitrc_cmd| { addOtherEnvironment(&state.session, state.lang, .xinitrc, xinitrc_cmd) catch |err| { try state.info_line.addMessage( state.lang.err_alloc, state.config.error_bg, state.config.error_fg, ); try state.log_file.err( "sys", "failed to add xinitrc environment: {s}", .{@errorName(err)}, ); }; } } else { try state.info_line.addMessage( state.lang.no_x11_support, state.config.bg, state.config.fg, ); try state.log_file.info( "comp", "x11 support disabled at compile-time", .{}, ); } var has_crawl_error = false; // Crawl session directories (Wayland, X11 and custom respectively) var wayland_session_dirs = std.mem.splitScalar(u8, state.config.waylandsessions, ':'); while (wayland_session_dirs.next()) |dir| { crawl(&state.session, state.lang, dir, .wayland) catch |err| { has_crawl_error = true; try state.log_file.err( "sys", "failed to crawl wayland session directory '{s}': {s}", .{ dir, @errorName(err) }, ); }; } if (build_options.enable_x11_support) { var x_session_dirs = std.mem.splitScalar(u8, state.config.xsessions, ':'); while (x_session_dirs.next()) |dir| { crawl(&state.session, state.lang, dir, .x11) catch |err| { has_crawl_error = true; try state.log_file.err( "sys", "failed to crawl x11 session directory '{s}': {s}", .{ dir, @errorName(err) }, ); }; } } var custom_session_dirs = std.mem.splitScalar(u8, state.config.custom_sessions, ':'); while (custom_session_dirs.next()) |dir| { crawl(&state.session, state.lang, dir, .custom) catch |err| { has_crawl_error = true; try state.log_file.err( "sys", "failed to crawl custom session directory '{s}': {s}", .{ dir, @errorName(err) }, ); }; } if (has_crawl_error) { try state.info_line.addMessage( state.lang.err_crawl, state.config.error_bg, state.config.error_fg, ); } if (usernames.items.len == 0) { // If we have no usernames, simply add an error to the info line. // This effectively means you can't login, since there would be no local // accounts *and* no root account...but at this point, if that's the // case, you have bigger problems to deal with in the first place. :D try state.info_line.addMessage(state.lang.err_no_users, state.config.error_bg, state.config.error_fg); try state.log_file.err("sys", "no users found", .{}); } state.password_label = Label.init( state.lang.password, null, state.buffer.fg, state.buffer.bg, null, null, ); defer state.password_label.deinit(); state.insert_mode = !state.config.vi_mode or state.config.vi_default_mode == .insert; state.password = try Text.init( state.allocator, &state.buffer, state.insert_mode, true, state.config.asterisk, state.box.width - 2 * state.box.horizontal_margin - state.labels_max_length - 1, state.buffer.fg, state.buffer.bg, ); defer state.password.deinit(); try state.buffer.registerKeybind(&state.password.keybinds, "H", &viGoLeft, &state); try state.buffer.registerKeybind(&state.password.keybinds, "L", &viGoRight, &state); state.password_widget = state.password.widget(); state.version_label = Label.init( ly_version_str, null, state.buffer.fg, state.buffer.bg, null, null, ); defer state.version_label.deinit(); state.is_autologin = false; check_autologin: { const auto_user = state.config.auto_login_user orelse break :check_autologin; const auto_session = state.config.auto_login_session orelse break :check_autologin; if (!isValidUsername(auto_user, usernames)) { try state.info_line.addMessage( state.lang.err_pam_user_unknown, state.config.error_bg, state.config.error_fg, ); try state.log_file.err( "auth", "autologin failed: username '{s}' not found", .{auto_user}, ); break :check_autologin; } const session_index = findSessionByName(&state.session, auto_session) orelse { try state.log_file.err( "auth", "autologin failed: session '{s}' not found", .{auto_session}, ); try state.info_line.addMessage( state.lang.err_autologin_session, state.config.error_bg, state.config.error_fg, ); break :check_autologin; }; try state.log_file.info( "auth", "attempting autologin for user '{s}' with session '{s}'", .{ auto_user, auto_session }, ); state.session.label.current = session_index; for (state.login.label.list.items, 0..) |username, i| { if (std.mem.eql(u8, username.name, auto_user)) { state.login.label.current = i; break; } } state.is_autologin = true; } // Switch to selected TTY state.active_tty = interop.getActiveTty(state.allocator, state.use_kmscon_vt) catch |err| no_tty_found: { try state.info_line.addMessage( state.lang.err_get_active_tty, state.config.error_bg, state.config.error_fg, ); try state.log_file.err( "sys", "failed to get active tty: {s}", .{@errorName(err)}, ); break :no_tty_found build_options.fallback_tty; }; if (!state.use_kmscon_vt) { interop.switchTty(state.active_tty) catch |err| { try state.info_line.addMessage( state.lang.err_switch_tty, state.config.error_bg, state.config.error_fg, ); try state.log_file.err( "sys", "failed to switch to tty {d}: {s}", .{ state.active_tty, @errorName(err) }, ); }; } // Initialize the animation, if any var animation: ?Widget = null; switch (state.config.animation) { .none => {}, .doom => { var doom = try Doom.init( state.allocator, &state.buffer, state.config.doom_top_color, state.config.doom_middle_color, state.config.doom_bottom_color, state.config.doom_fire_height, state.config.doom_fire_spread, &state.animate, state.config.animation_timeout_sec, state.config.animation_frame_delay, ); animation = doom.widget(); }, .matrix => { var matrix = try Matrix.init( state.allocator, &state.buffer, state.config.cmatrix_fg, state.config.cmatrix_head_col, state.config.cmatrix_min_codepoint, state.config.cmatrix_max_codepoint, &state.animate, state.config.animation_timeout_sec, state.config.animation_frame_delay, ); animation = matrix.widget(); }, .colormix => { var color_mix = try ColorMix.init( &state.buffer, state.config.colormix_col1, state.config.colormix_col2, state.config.colormix_col3, &state.animate, state.config.animation_timeout_sec, state.config.animation_frame_delay, ); animation = color_mix.widget(); }, .gameoflife => { var game_of_life = try GameOfLife.init( state.allocator, &state.buffer, state.config.gameoflife_fg, state.config.gameoflife_entropy_interval, state.config.gameoflife_frame_delay, state.config.gameoflife_initial_density, &state.animate, state.config.animation_timeout_sec, state.config.animation_frame_delay, ); animation = game_of_life.widget(); }, .dur_file => { var dur = try DurFile.init( state.allocator, &state.buffer, &state.log_file, state.config.dur_file_path, state.config.dur_offset_alignment, state.config.dur_x_offset, state.config.dur_y_offset, state.config.full_color, &state.animate, state.config.animation_timeout_sec, state.config.animation_frame_delay, ); animation = dur.widget(); }, } defer if (animation) |*a| a.deinit(); var cascade = Cascade.init( &state.buffer, &state.auth_fails, state.config.auth_fails, ); state.auth_fails = 0; state.animate = state.config.animation != .none; state.edge_margin = Position.init( state.config.edge_margin, state.config.edge_margin, ); // Load last saved username and desktop selection, if any // Skip if autologin is active to prevent overriding autologin session var default_input = state.config.default_input; if (state.config.save and !state.is_autologin) { if (state.saved_users.last_username_index) |index| load_last_user: { // If the saved index isn't valid, bail out if (index >= state.saved_users.user_list.items.len) break :load_last_user; const user = state.saved_users.user_list.items[index]; // Find user with saved name, and switch over to it // If it doesn't exist (anymore), we don't change the value for (usernames.items, 0..) |username, i| { if (std.mem.eql(u8, username, user.username)) { state.login.label.current = i; break; } } default_input = .password; state.session.label.current = @min(user.session_index, state.session.label.list.items.len - 1); } } const info_line_widget = state.info_line.widget(); const session_widget = state.session.widget(); const login_widget = state.login.widget(); var widgets: std.ArrayList([]Widget) = .empty; defer widgets.deinit(state.allocator); // Layer 1 if (animation) |a| { var layer1 = [_]Widget{a}; try widgets.append(state.allocator, &layer1); } // Layer 2 var layer2: std.ArrayList(Widget) = .empty; defer layer2.deinit(state.allocator); if (!state.config.hide_key_hints) { try layer2.append(state.allocator, state.shutdown_label.widget()); try layer2.append(state.allocator, state.restart_label.widget()); if (state.config.sleep_cmd != null) { try layer2.append(state.allocator, state.sleep_label.widget()); } if (state.config.hibernate_cmd != null) { try layer2.append(state.allocator, state.hibernate_label.widget()); } try layer2.append(state.allocator, state.toggle_password_label.widget()); if (state.config.brightness_down_key != null) { try layer2.append(state.allocator, state.brightness_down_label.widget()); } if (state.config.brightness_up_key != null) { try layer2.append(state.allocator, state.brightness_up_label.widget()); } } if (state.config.battery_id != null) { try layer2.append(state.allocator, state.battery_label.widget()); } if (state.config.clock != null) { try layer2.append(state.allocator, state.clock_label.widget()); } if (state.config.bigclock != .none) { try layer2.append(state.allocator, state.bigclock_label.widget()); } if (!state.config.hide_keyboard_locks) { try layer2.append(state.allocator, state.numlock_label.widget()); try layer2.append(state.allocator, state.capslock_label.widget()); } try layer2.append(state.allocator, state.box.widget()); try layer2.append(state.allocator, info_line_widget); try layer2.append(state.allocator, state.session_specifier_label.widget()); try layer2.append(state.allocator, session_widget); try layer2.append(state.allocator, state.login_label.widget()); try layer2.append(state.allocator, login_widget); try layer2.append(state.allocator, state.password_label.widget()); try layer2.append(state.allocator, state.password_widget); if (!state.config.hide_version_string) { try layer2.append(state.allocator, state.version_label.widget()); } try widgets.append(state.allocator, layer2.items); // Layer 3 if (state.config.auth_fails > 0) { var layer3 = [_]Widget{cascade.widget()}; try widgets.append(state.allocator, &layer3); } try state.buffer.registerGlobalKeybind("Esc", &disableInsertMode, &state); try state.buffer.registerGlobalKeybind("I", &enableInsertMode, &state); try state.buffer.registerGlobalKeybind("Ctrl+C", &quit, &state); try state.buffer.registerGlobalKeybind("K", &viMoveCursorUp, &state); try state.buffer.registerGlobalKeybind("J", &viMoveCursorDown, &state); try state.buffer.registerGlobalKeybind("Enter", &authenticate, &state); try state.buffer.registerGlobalKeybind(state.config.shutdown_key, &shutdownCmd, &state); try state.buffer.registerGlobalKeybind(state.config.restart_key, &restartCmd, &state); try state.buffer.registerGlobalKeybind(state.config.show_password_key, &togglePasswordMask, &state); if (state.config.sleep_cmd != null) try state.buffer.registerGlobalKeybind(state.config.sleep_key, &sleepCmd, &state); if (state.config.hibernate_cmd != null) try state.buffer.registerGlobalKeybind(state.config.hibernate_key, &hibernateCmd, &state); if (state.config.brightness_down_key) |key| try state.buffer.registerGlobalKeybind(key, &decreaseBrightnessCmd, &state); if (state.config.brightness_up_key) |key| try state.buffer.registerGlobalKeybind(key, &increaseBrightnessCmd, &state); if (state.config.initial_info_text) |text| { try state.info_line.addMessage(text, state.config.bg, state.config.fg); } else get_host_name: { // Initialize information line with host name var name_buf: [std.posix.HOST_NAME_MAX]u8 = undefined; const hostname = std.posix.gethostname(&name_buf) catch |err| { try state.info_line.addMessage( state.lang.err_hostname, state.config.error_bg, state.config.error_fg, ); try state.log_file.err( "sys", "failed to get hostname: {s}", .{@errorName(err)}, ); break :get_host_name; }; try state.info_line.addMessage( hostname, state.config.bg, state.config.fg, ); } if (state.is_autologin) _ = try authenticate(&state); const active_widget = switch (default_input) { .info_line => info_line_widget, .session => session_widget, .login => login_widget, .password => state.password_widget, }; var shared_error = try SharedError.init(&uiErrorHandler, &state); defer shared_error.deinit(); try state.buffer.runEventLoop( state.allocator, shared_error, widgets.items, active_widget, state.config.inactivity_delay, positionWidgets, handleInactivity, &state, ); } fn uiErrorHandler(err: anyerror, ctx: *anyopaque) anyerror!void { var state: *UiState = @ptrCast(@alignCast(ctx)); switch (err) { error.SetCursorFailed => { try state.info_line.addMessage( state.lang.err_alloc, state.config.error_bg, state.config.error_fg, ); }, error.WidgetReallocationFailed => { try state.info_line.addMessage( state.lang.err_alloc, state.config.error_bg, state.config.error_fg, ); }, error.CurrentWidgetHandlingFailed => { try state.info_line.addMessage( state.lang.err_alloc, state.config.error_bg, state.config.error_fg, ); }, else => unreachable, } } fn disableInsertMode(ptr: *anyopaque) !bool { var state: *UiState = @ptrCast(@alignCast(ptr)); if (state.config.vi_mode and state.insert_mode) { state.insert_mode = false; state.password.should_insert = false; state.buffer.drawNextFrame(true); } return false; } fn enableInsertMode(ptr: *anyopaque) !bool { var state: *UiState = @ptrCast(@alignCast(ptr)); if (state.insert_mode) return true; state.insert_mode = true; state.password.should_insert = true; state.buffer.drawNextFrame(true); return false; } fn viGoLeft(ptr: *anyopaque) !bool { var self: *UiState = @ptrCast(@alignCast(ptr)); if (self.insert_mode) return true; return try self.buffer.simulateKeybind("Left"); } fn viGoRight(ptr: *anyopaque) !bool { var state: *UiState = @ptrCast(@alignCast(ptr)); if (state.insert_mode) return true; return try state.buffer.simulateKeybind("Right"); } fn viMoveCursorUp(ptr: *anyopaque) !bool { var state: *UiState = @ptrCast(@alignCast(ptr)); if (state.insert_mode) return true; return try state.buffer.simulateKeybind("Up"); } fn viMoveCursorDown(ptr: *anyopaque) !bool { var state: *UiState = @ptrCast(@alignCast(ptr)); if (state.insert_mode) return true; return try state.buffer.simulateKeybind("Down"); } fn togglePasswordMask(ptr: *anyopaque) !bool { var state: *UiState = @ptrCast(@alignCast(ptr)); state.password.toggleMask(); state.buffer.drawNextFrame(true); return false; } fn quit(ptr: *anyopaque) !bool { var state: *UiState = @ptrCast(@alignCast(ptr)); state.buffer.stopEventLoop(); return false; } fn authenticate(ptr: *anyopaque) !bool { var state: *UiState = @ptrCast(@alignCast(ptr)); try state.log_file.info("auth", "starting authentication", .{}); if (!state.config.allow_empty_password and state.password.text.items.len == 0) { // Let's not log this message for security reasons try state.info_line.addMessage( state.lang.err_empty_password, state.config.error_bg, state.config.error_fg, ); state.info_line.clearRendered(state.allocator) catch |err| { try state.info_line.addMessage( state.lang.err_alloc, state.config.error_bg, state.config.error_fg, ); try state.log_file.err( "tui", "failed to clear info line: {s}", .{@errorName(err)}, ); }; state.info_line.label.draw(); TerminalBuffer.presentBuffer(); return false; } try state.info_line.addMessage( state.lang.authenticating, state.config.bg, state.config.fg, ); state.info_line.clearRendered(state.allocator) catch |err| { try state.info_line.addMessage( state.lang.err_alloc, state.config.error_bg, state.config.error_fg, ); try state.log_file.err( "tui", "failed to clear info line: {s}", .{@errorName(err)}, ); }; state.info_line.label.draw(); TerminalBuffer.presentBuffer(); if (state.config.save) save_last_settings: { // It isn't worth cluttering the code with precise error // handling, so let's just report a generic error message, // that should be good enough for debugging anyway. errdefer state.log_file.err( "conf", "failed to save current user data", .{}, ) catch {}; var file = std.fs.cwd().createFile(state.save_path, .{}) catch |err| { state.log_file.err( "sys", "failed to create save file: {s}", .{@errorName(err)}, ) catch break :save_last_settings; break :save_last_settings; }; defer file.close(); var file_buffer: [256]u8 = undefined; var file_writer = file.writer(&file_buffer); var writer = &file_writer.interface; try writer.print("{d}\n", .{state.login.label.current}); for (state.saved_users.user_list.items) |user| { try writer.print("{s}:{d}\n", .{ user.username, user.session_index }); } try writer.flush(); // Delete previous save file if it exists if (migrator.maybe_save_file) |path| { std.fs.cwd().deleteFile(path) catch {}; } else if (state.has_old_save) { std.fs.cwd().deleteFile(state.old_save_path) catch {}; } } var shared_err = try SharedError.init(null, null); defer shared_err.deinit(); { state.log_file.deinit(); session_pid = try std.posix.fork(); if (session_pid == 0) { const current_environment = state.session.label.list.items[state.session.label.current].environment; // Use auto_login_service for autologin, otherwise use configured service const service_name = if (state.is_autologin) state.config.auto_login_service else state.config.service_name; const password_text = if (state.is_autologin) "" else state.password.text.items; const auth_options = auth.AuthOptions{ .tty = state.active_tty, .service_name = service_name, .path = state.config.path, .session_log = state.config.session_log, .xauth_cmd = state.config.xauth_cmd, .setup_cmd = state.config.setup_cmd, .login_cmd = state.config.login_cmd, .x_cmd = state.config.x_cmd, .x_vt = state.config.x_vt, .session_pid = session_pid, .use_kmscon_vt = state.use_kmscon_vt, }; // Signal action to give up control on the TTY const tty_control_transfer_act = std.posix.Sigaction{ .handler = .{ .handler = &ttyControlTransferSignalHandler }, .mask = std.posix.sigemptyset(), .flags = 0, }; std.posix.sigaction(std.posix.SIG.CHLD, &tty_control_transfer_act, null); try state.log_file.reinit(); auth.authenticate( state.allocator, &state.log_file, auth_options, current_environment, state.login.getCurrentUsername(), password_text, ) catch |err| { shared_err.writeError(err); state.log_file.deinit(); std.process.exit(1); }; state.log_file.deinit(); std.process.exit(0); } _ = std.posix.waitpid(session_pid, 0); // HACK: It seems like the session process is not exiting immediately after the waitpid call. // This is a workaround to ensure the session process has exited before re-initializing the TTY. std.Thread.sleep(std.time.ns_per_s * 1); session_pid = -1; try state.log_file.reinit(); } try state.buffer.reclaim(); const auth_err = shared_err.readError(); if (auth_err) |err| { state.auth_fails += 1; state.buffer.setActiveWidget(state.password_widget); try state.info_line.addMessage( getAuthErrorMsg(err, state.lang), state.config.error_bg, state.config.error_fg, ); try state.log_file.err( "auth", "failed to authenticate: {s}", .{@errorName(err)}, ); if (state.config.clear_password or err != error.PamAuthError) state.password.clear(); } else { if (state.config.logout_cmd) |logout_cmd| { var logout_process = std.process.Child.init(&[_][]const u8{ "/bin/sh", "-c", logout_cmd }, state.allocator); _ = logout_process.spawnAndWait() catch .{}; } state.password.clear(); state.is_autologin = false; try state.info_line.addMessage( state.lang.logout, state.config.bg, state.config.fg, ); try state.log_file.info("auth", "logged out", .{}); } if (state.config.auth_fails == 0 or state.auth_fails < state.config.auth_fails) { try TerminalBuffer.clearScreen(true); state.buffer.drawNextFrame(true); } // Restore the cursor TerminalBuffer.setCursor(0, 0); TerminalBuffer.presentBuffer(); return false; } fn shutdownCmd(ptr: *anyopaque) !bool { var state: *UiState = @ptrCast(@alignCast(ptr)); shutdown = true; state.buffer.stopEventLoop(); return false; } fn restartCmd(ptr: *anyopaque) !bool { var state: *UiState = @ptrCast(@alignCast(ptr)); restart = true; state.buffer.stopEventLoop(); return false; } fn sleepCmd(ptr: *anyopaque) !bool { var state: *UiState = @ptrCast(@alignCast(ptr)); if (state.config.sleep_cmd) |sleep_cmd| { var sleep = std.process.Child.init(&[_][]const u8{ "/bin/sh", "-c", sleep_cmd }, state.allocator); sleep.stdout_behavior = .Ignore; sleep.stderr_behavior = .Ignore; const process_result = sleep.spawnAndWait() catch return false; if (process_result.Exited != 0) { try state.info_line.addMessage( state.lang.err_sleep, state.config.error_bg, state.config.error_fg, ); try state.log_file.err( "sys", "failed to execute sleep command: exit code {d}", .{process_result.Exited}, ); } } return false; } fn hibernateCmd(ptr: *anyopaque) !bool { var state: *UiState = @ptrCast(@alignCast(ptr)); if (state.config.hibernate_cmd) |hibernate_cmd| { var hibernate = std.process.Child.init(&[_][]const u8{ "/bin/sh", "-c", hibernate_cmd }, state.allocator); hibernate.stdout_behavior = .Ignore; hibernate.stderr_behavior = .Ignore; const process_result = hibernate.spawnAndWait() catch return false; if (process_result.Exited != 0) { try state.info_line.addMessage( state.lang.err_hibernate, state.config.error_bg, state.config.error_fg, ); try state.log_file.err( "sys", "failed to execute hibernate command: exit code {d}", .{process_result.Exited}, ); } } return false; } fn decreaseBrightnessCmd(ptr: *anyopaque) !bool { var state: *UiState = @ptrCast(@alignCast(ptr)); adjustBrightness(state.allocator, state.config.brightness_down_cmd) catch |err| { try state.info_line.addMessage( state.lang.err_brightness_change, state.config.error_bg, state.config.error_fg, ); try state.log_file.err( "sys", "failed to decrease brightness: {s}", .{@errorName(err)}, ); }; return false; } fn increaseBrightnessCmd(ptr: *anyopaque) !bool { var state: *UiState = @ptrCast(@alignCast(ptr)); adjustBrightness(state.allocator, state.config.brightness_up_cmd) catch |err| { try state.info_line.addMessage( state.lang.err_brightness_change, state.config.error_bg, state.config.error_fg, ); try state.log_file.err( "sys", "failed to increase brightness: {s}", .{@errorName(err)}, ); }; return false; } fn updateNumlock(self: *Label, ptr: *anyopaque) !void { var state: *UiState = @ptrCast(@alignCast(ptr)); const lock_state = interop.getLockState() catch |err| { self.update_fn = null; try state.info_line.addMessage( state.lang.err_lock_state, state.config.error_bg, state.config.error_fg, ); try state.log_file.err( "sys", "failed to get lock state: {s}", .{@errorName(err)}, ); return; }; self.setText(if (lock_state.numlock) state.lang.numlock else ""); } fn updateCapslock(self: *Label, ptr: *anyopaque) !void { var state: *UiState = @ptrCast(@alignCast(ptr)); const lock_state = interop.getLockState() catch |err| { self.update_fn = null; try state.info_line.addMessage(state.lang.err_lock_state, state.config.error_bg, state.config.error_fg); try state.log_file.err("sys", "failed to get lock state: {s}", .{@errorName(err)}); return; }; self.setText(if (lock_state.capslock) state.lang.capslock else ""); } fn updateBattery(self: *Label, ptr: *anyopaque) !void { var state: *UiState = @ptrCast(@alignCast(ptr)); if (state.config.battery_id) |id| { const battery_percentage = getBatteryPercentage(id) catch |err| { self.update_fn = null; try state.log_file.err( "sys", "failed to get battery percentage: {s}", .{@errorName(err)}, ); try state.info_line.addMessage( state.lang.err_battery, state.config.error_bg, state.config.error_fg, ); return; }; try self.setTextBuf( &state.battery_buf, "BAT: {d}%", .{battery_percentage}, ); } } fn updateClock(self: *Label, ptr: *anyopaque) !void { var state: *UiState = @ptrCast(@alignCast(ptr)); if (state.config.clock) |clock| draw_clock: { const clock_str = interop.timeAsString(&state.clock_buf, clock); if (clock_str.len == 0) { self.update_fn = null; try state.info_line.addMessage( state.lang.err_clock_too_long, state.config.error_bg, state.config.error_fg, ); try state.log_file.err( "tui", "clock string too long", .{}, ); break :draw_clock; } self.setText(clock_str); } } fn calculateClockTimeout(_: *Label, _: *anyopaque) !?usize { const time = try interop.getTimeOfDay(); return @intCast(1000 - @divTrunc(time.microseconds, 1000) + 1); } fn updateBigClock(self: *BigLabel, ptr: *anyopaque) !void { var state: *UiState = @ptrCast(@alignCast(ptr)); if (state.box.height + (BigLabel.CHAR_HEIGHT + 2) * 2 >= state.buffer.height) return; const time = try interop.getTimeOfDay(); const animate_time = @divTrunc(time.microseconds, 500_000); const separator = if (state.animate and animate_time != 0) " " else ":"; const format = try std.fmt.bufPrintZ( &state.bigclock_format_buf, "{s}{s}{s}{s}{s}{s}", .{ if (state.config.bigclock_12hr) "%I" else "%H", separator, "%M", if (state.config.bigclock_seconds) separator else "", if (state.config.bigclock_seconds) "%S" else "", if (state.config.bigclock_12hr) "%P" else "", }, ); const clock_str = interop.timeAsString(&state.bigclock_buf, format); self.setText(clock_str); } fn calculateBigClockTimeout(_: *BigLabel, ptr: *anyopaque) !?usize { const state: *UiState = @ptrCast(@alignCast(ptr)); const time = try interop.getTimeOfDay(); if (state.config.bigclock_seconds) { return @intCast(1000 - @divTrunc(time.microseconds, 1000) + 1); } return @intCast((60 - @rem(time.seconds, 60)) * 1000 - @divTrunc(time.microseconds, 1000) + 1); } fn updateBox(self: *CenteredBox, ptr: *anyopaque) !void { const state: *UiState = @ptrCast(@alignCast(ptr)); if (state.config.vi_mode) { self.bottom_title = if (state.insert_mode) state.lang.insert else state.lang.normal; } } fn updateSessionSpecifier(self: *Label, ptr: *anyopaque) !void { const state: *UiState = @ptrCast(@alignCast(ptr)); const env = state.session.label.list.items[state.session.label.current]; self.setText(env.environment.specifier); } fn positionWidgets(ptr: *anyopaque) !void { var state: *UiState = @ptrCast(@alignCast(ptr)); if (!state.config.hide_key_hints) { state.shutdown_label.positionX(state.edge_margin .add(TerminalBuffer.START_POSITION)); var last_label = state.shutdown_label; state.restart_label.positionX(last_label .childrenPosition() .addX(1)); last_label = state.restart_label; state.sleep_label.positionX(last_label .childrenPosition() .addX(1)); if (state.config.sleep_cmd != null) { last_label = state.sleep_label; } state.hibernate_label.positionX(last_label .childrenPosition() .addX(1)); if (state.config.hibernate_cmd != null) { last_label = state.hibernate_label; } state.toggle_password_label.positionX(last_label .childrenPosition() .addX(1)); last_label = state.toggle_password_label; state.brightness_down_label.positionX(last_label .childrenPosition() .addX(1)); if (state.config.brightness_down_key != null) { last_label = state.brightness_down_label; } state.brightness_up_label.positionXY(last_label .childrenPosition() .addX(1)); } state.battery_label.positionXY(state.edge_margin .add(TerminalBuffer.START_POSITION) .addYFromIf(state.brightness_up_label.childrenPosition(), !state.config.hide_key_hints) .removeYFromIf(state.edge_margin, !state.config.hide_key_hints)); state.clock_label.positionXY(state.edge_margin .add(TerminalBuffer.START_POSITION) .invertX(state.buffer.width) .removeXIf(TerminalBuffer.strWidth(state.clock_label.text), state.buffer.width > TerminalBuffer.strWidth(state.clock_label.text) + state.edge_margin.x)); state.numlock_label.positionX(state.edge_margin .add(TerminalBuffer.START_POSITION) .addYFromIf(state.clock_label.childrenPosition(), state.config.clock != null) .removeYFromIf(state.edge_margin, state.config.clock != null) .invertX(state.buffer.width) .removeXIf(TerminalBuffer.strWidth(state.lang.numlock), state.buffer.width > TerminalBuffer.strWidth(state.lang.numlock) + state.edge_margin.x)); state.capslock_label.positionX(state.numlock_label .childrenPosition() .removeX(TerminalBuffer.strWidth(state.lang.numlock) + TerminalBuffer.strWidth(state.lang.capslock) + 1)); state.box.positionXY(TerminalBuffer.START_POSITION); if (state.config.bigclock != .none) { const half_width = state.buffer.width / 2; const half_label_width = (TerminalBuffer.strWidth(state.bigclock_label.text) * (BigLabel.CHAR_WIDTH + 1)) / 2; const half_height = (if (state.buffer.height > state.box.height) state.buffer.height - state.box.height else state.buffer.height) / 2; state.bigclock_label.positionXY(TerminalBuffer.START_POSITION .addX(half_width) .removeXIf(half_label_width, half_width > half_label_width) .addY(half_height) .removeYIf(BigLabel.CHAR_HEIGHT + 2, half_height > BigLabel.CHAR_HEIGHT + 2)); } state.info_line.label.positionY(state.box .childrenPosition()); state.session_specifier_label.positionX(state.info_line.label .childrenPosition() .addY(1)); state.session.label.positionY(state.session_specifier_label .childrenPosition() .addX(state.labels_max_length - TerminalBuffer.strWidth(state.session_specifier_label.text) + 1)); state.login_label.positionX(state.session.label .childrenPosition() .resetXFrom(state.info_line.label.childrenPosition()) .addY(1)); state.login.label.positionY(state.login_label .childrenPosition() .addX(state.labels_max_length - TerminalBuffer.strWidth(state.login_label.text) + 1)); state.password_label.positionX(state.login.label .childrenPosition() .resetXFrom(state.info_line.label.childrenPosition()) .addY(1)); state.password.positionY(state.password_label .childrenPosition() .addX(state.labels_max_length - TerminalBuffer.strWidth(state.password_label.text) + 1)); state.version_label.positionXY(state.edge_margin .add(TerminalBuffer.START_POSITION) .invertY(state.buffer.height - 1)); } fn handleInactivity(ptr: *anyopaque) !void { var state: *UiState = @ptrCast(@alignCast(ptr)); if (state.config.inactivity_cmd) |inactivity_cmd| { var inactivity = std.process.Child.init(&[_][]const u8{ "/bin/sh", "-c", inactivity_cmd }, state.allocator); inactivity.stdout_behavior = .Ignore; inactivity.stderr_behavior = .Ignore; handle_inactivity_cmd: { const process_result = inactivity.spawnAndWait() catch { break :handle_inactivity_cmd; }; if (process_result.Exited != 0) { try state.info_line.addMessage( state.lang.err_inactivity, state.config.error_bg, state.config.error_fg, ); try state.log_file.err( "sys", "failed to execute inactivity command: exit code {d}", .{process_result.Exited}, ); } } } } fn addOtherEnvironment(session: *Session, lang: Lang, display_server: DisplayServer, exec: ?[]const u8) !void { const name = switch (display_server) { .shell => lang.shell, .xinitrc => lang.xinitrc, else => unreachable, }; try session.addEnvironment(.{ .entry_ini = null, .name = name, .xdg_session_desktop = null, .xdg_desktop_names = null, .cmd = exec, .specifier = lang.other, .display_server = display_server, .is_terminal = display_server == .shell, }); } fn crawl(session: *Session, lang: Lang, path: []const u8, display_server: DisplayServer) !void { if (!std.fs.path.isAbsolute(path)) return error.PathNotAbsolute; var iterable_directory = try std.fs.openDirAbsolute(path, .{ .iterate = true }); defer iterable_directory.close(); var iterator = iterable_directory.iterate(); while (try iterator.next()) |item| { if (!std.mem.eql(u8, std.fs.path.extension(item.name), ".desktop")) continue; const entry_path = try std.fmt.allocPrint(session.label.allocator, "{s}/{s}", .{ path, item.name }); defer session.label.allocator.free(entry_path); var entry_ini = Ini(Entry).init(session.label.allocator); _ = try entry_ini.readFileToStruct(entry_path, .{ .fieldHandler = null, .comment_characters = "#", }); errdefer entry_ini.deinit(); const file_name = try session.label.allocator.dupe(u8, std.fs.path.stem(item.name)); const entry = entry_ini.data.@"Desktop Entry"; var maybe_xdg_session_desktop: ?[]const u8 = null; var maybe_xdg_desktop_names: ?[]const u8 = null; // Prepare the XDG_SESSION_DESKTOP and XDG_CURRENT_DESKTOP environment // variables here if (entry.DesktopNames) |desktop_names| { maybe_xdg_session_desktop = std.mem.sliceTo(desktop_names, ';'); for (desktop_names) |*c| { if (c.* == ';') c.* = ':'; } maybe_xdg_desktop_names = desktop_names; } else if (display_server != .custom) { // If DesktopNames is empty, and this isn't a custom session entry, // we'll take the name of the session file if (file_name.len > 0) maybe_xdg_session_desktop = file_name; } try session.addEnvironment(.{ .entry_ini = entry_ini, .file_name = file_name, .name = entry.Name, .xdg_session_desktop = maybe_xdg_session_desktop, .xdg_desktop_names = maybe_xdg_desktop_names, .cmd = entry.Exec, .specifier = switch (display_server) { .wayland => lang.wayland, .x11 => lang.x11, .custom => lang.custom, else => lang.other, }, .display_server = display_server, .is_terminal = entry.Terminal orelse false, }); } } fn isValidUsername(username: []const u8, usernames: StringList) bool { for (usernames.items) |valid_username| { if (std.mem.eql(u8, username, valid_username)) return true; } return false; } fn findSessionByName(session: *Session, name: []const u8) ?usize { for (session.label.list.items, 0..) |env, i| { if (std.ascii.eqlIgnoreCase(env.environment.file_name, name)) return i; if (std.ascii.eqlIgnoreCase(env.environment.name, name)) return i; if (env.environment.xdg_session_desktop) |session_desktop| { if (session_desktop.len > 0 and std.ascii.eqlIgnoreCase(session_desktop, name)) return i; } if (env.environment.xdg_desktop_names) |session_desktop_name| { if (std.ascii.eqlIgnoreCase(session_desktop_name, name)) return i; } } return null; } fn getAllUsernames(allocator: Allocator, login_defs_path: []const u8, uid_range_error: *?anyerror) !StringList { const uid_range = interop.getUserIdRange(allocator, login_defs_path) catch |err| no_uid_range: { uid_range_error.* = err; break :no_uid_range UidRange{ .uid_min = build_options.fallback_uid_min, .uid_max = build_options.fallback_uid_max, }; }; // There's no reliable (and clean) way to check for systemd support, so // let's just define a range and check if a user is within it const SYSTEMD_HOMED_UID_MIN = 60001; const SYSTEMD_HOMED_UID_MAX = 60513; const homed_uid_range = UidRange{ .uid_min = SYSTEMD_HOMED_UID_MIN, .uid_max = SYSTEMD_HOMED_UID_MAX, }; var usernames: StringList = .empty; var maybe_entry = interop.getNextUsernameEntry(); 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) const is_within_range = entry.uid >= uid_range.uid_min and entry.uid <= uid_range.uid_max; const is_within_homed_range = builtin.os.tag == .linux and entry.uid >= homed_uid_range.uid_min and entry.uid <= homed_uid_range.uid_max; const is_root = entry.uid == 0 and entry.username != null; if (is_within_range or is_within_homed_range or is_root) { const username = try allocator.dupe(u8, entry.username.?); try usernames.append(allocator, username); } maybe_entry = interop.getNextUsernameEntry(); } interop.closePasswordDatabase(); return usernames; } fn adjustBrightness(allocator: Allocator, cmd: []const u8) !void { var brightness = std.process.Child.init(&[_][]const u8{ "/bin/sh", "-c", cmd }, allocator); brightness.stdout_behavior = .Ignore; brightness.stderr_behavior = .Ignore; const process_result = brightness.spawnAndWait() catch return; if (process_result.Exited != 0) { return error.BrightnessChangeFailed; } } fn getBatteryPercentage(battery_id: []const u8) !u8 { const path = try std.fmt.allocPrint(temporary_allocator, "/sys/class/power_supply/{s}/capacity", .{battery_id}); defer temporary_allocator.free(path); const battery_file = try std.fs.cwd().openFile(path, .{}); defer battery_file.close(); var buffer: [8]u8 = undefined; const bytes_read = try battery_file.read(&buffer); const capacity_str = buffer[0..bytes_read]; const trimmed = std.mem.trimRight(u8, capacity_str, "\n\r"); return try std.fmt.parseInt(u8, trimmed, 10); } fn getAuthErrorMsg(err: anyerror, lang: Lang) []const u8 { return switch (err) { error.GetPasswordNameFailed => lang.err_pwnam, error.GetEnvListFailed => lang.err_envlist, error.XauthFailed => lang.err_xauth, error.XcbConnectionFailed => lang.err_xcb_conn, error.GroupInitializationFailed => lang.err_user_init, error.SetUserGidFailed => lang.err_user_gid, error.SetUserUidFailed => lang.err_user_uid, error.ChangeDirectoryFailed => lang.err_perm_dir, error.TtyControlTransferFailed => lang.err_tty_ctrl, error.SetPathFailed => lang.err_path, error.PamAccountExpired => lang.err_pam_acct_expired, error.PamAuthError => lang.err_pam_auth, error.PamAuthInfoUnavailable => lang.err_pam_authinfo_unavail, error.PamBufferError => lang.err_pam_buf, error.PamCredentialsError => lang.err_pam_cred_err, error.PamCredentialsExpired => lang.err_pam_cred_expired, error.PamCredentialsInsufficient => lang.err_pam_cred_insufficient, error.PamCredentialsUnavailable => lang.err_pam_cred_unavail, error.PamMaximumTries => lang.err_pam_maxtries, error.PamNewAuthTokenRequired => lang.err_pam_authok_reqd, error.PamPermissionDenied => lang.err_pam_perm_denied, error.PamSessionError => lang.err_pam_session, error.PamSystemError => lang.err_pam_sys, error.PamUserUnknown => lang.err_pam_user_unknown, error.PamAbort => lang.err_pam_abort, else => @errorName(err), }; }