Full Code of fairyglade/ly for AI

master abe72c74ff12 cached
93 files
375.0 KB
102.3k tokens
2 symbols
1 requests
Download .txt
Showing preview only (398K chars total). Download the full file or copy to clipboard to get everything.
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 <sam@hocevar.net>

 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 транзакция н
Download .txt
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
Download .txt
SYMBOL INDEX (2 symbols across 1 files)

FILE: res/lang/normalize_lang_files.py
  function process_lang_file (line 7) | def process_lang_file(path: Path, lang_keys: list[str]) -> None:
  function main (line 28) | def main() -> None:
Condensed preview — 93 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (403K chars).
[
  {
    "path": ".github/FUNDING.yml",
    "chars": 42,
    "preview": "github: AnErrupTion\nliberapay: ShiningLea\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug.yml",
    "chars": 2668,
    "preview": "name: Bug report\ndescription: File a bug report.\ntitle: \"[Bug] \"\nlabels: [\"bug\"]\nbody:\n  - type: checkboxes\n    id: prer"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature.yml",
    "chars": 720,
    "preview": "name: Feature request\ndescription: Request a new feature or enhancement.\ntitle: \"[Feature] \"\nlabels: [\"feature\"]\nbody:\n "
  },
  {
    "path": ".github/pull_request_template.md",
    "chars": 287,
    "preview": "## What are the changes about?\n\n_Replace this with a brief description of your changes_\n\n## What existing issue does thi"
  },
  {
    "path": ".gitignore",
    "chars": 51,
    "preview": ".idea/\nzig-cache/\nzig-out/\nvalgrind.log\n.zig-cache\n"
  },
  {
    "path": "build.zig",
    "chars": 23018,
    "preview": "const std = @import(\"std\");\nconst builtin = @import(\"builtin\");\n\nconst PatchMap = std.StringHashMap([]const u8);\nconst I"
  },
  {
    "path": "build.zig.zon",
    "chars": 444,
    "preview": ".{\n    .name = .ly,\n    .version = \"1.4.0\",\n    .fingerprint = 0xa148ffcc5dc2cb59,\n    .minimum_zig_version = \"0.15.0\",\n"
  },
  {
    "path": "license.md",
    "chars": 483,
    "preview": "            DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE\n                    Version 2, December 2004\n\n Copyright (C) 200"
  },
  {
    "path": "ly-core/build.zig",
    "chars": 692,
    "preview": "const std = @import(\"std\");\n\npub fn build(b: *std.Build) void {\n    const target = b.standardTargetOptions(.{});\n    con"
  },
  {
    "path": "ly-core/build.zig.zon",
    "chars": 473,
    "preview": ".{\n    .name = .ly_core,\n    .version = \"1.0.0\",\n    .fingerprint = 0xddda7afda795472,\n    .minimum_zig_version = \"0.15."
  },
  {
    "path": "ly-core/src/LogFile.zig",
    "chars": 2424,
    "preview": "const std = @import(\"std\");\nconst interop = @import(\"interop.zig\");\n\nconst LogFile = @This();\n\npath: []const u8,\ncould_o"
  },
  {
    "path": "ly-core/src/SharedError.zig",
    "chars": 1543,
    "preview": "const std = @import(\"std\");\n\nconst ErrInt = std.meta.Int(.unsigned, @bitSizeOf(anyerror));\n\nconst ErrorHandler = packed "
  },
  {
    "path": "ly-core/src/UidRange.zig",
    "chars": 244,
    "preview": "const std = @import(\"std\");\n\n// We set both values to 0 by default so that, in case they aren't present in\n// the login."
  },
  {
    "path": "ly-core/src/interop.zig",
    "chars": 14840,
    "preview": "const std = @import(\"std\");\nconst builtin = @import(\"builtin\");\nconst UidRange = @import(\"UidRange.zig\");\n\npub const pam"
  },
  {
    "path": "ly-core/src/root.zig",
    "chars": 2639,
    "preview": "const std = @import(\"std\");\n\npub const ini = @import(\"zigini\");\n\npub const interop = @import(\"interop.zig\");\npub const U"
  },
  {
    "path": "ly-ui/build.zig",
    "chars": 1224,
    "preview": "const std = @import(\"std\");\n\npub fn build(b: *std.Build) void {\n    const target = b.standardTargetOptions(.{});\n    con"
  },
  {
    "path": "ly-ui/build.zig.zon",
    "chars": 530,
    "preview": ".{\n    .name = .ly_ui,\n    .version = \"1.0.0\",\n    .fingerprint = 0x8d11bf85a74ec803,\n    .minimum_zig_version = \"0.15.0"
  },
  {
    "path": "ly-ui/src/Cell.zig",
    "chars": 353,
    "preview": "const TerminalBuffer = @import(\"TerminalBuffer.zig\");\n\nconst Cell = @This();\n\nch: u32,\nfg: u32,\nbg: u32,\n\npub fn init(ch"
  },
  {
    "path": "ly-ui/src/Position.zig",
    "chars": 4819,
    "preview": "const Position = @This();\n\nx: usize,\ny: usize,\n\npub fn init(x: usize, y: usize) Position {\n    return .{\n        .x = x,"
  },
  {
    "path": "ly-ui/src/TerminalBuffer.zig",
    "chars": 17625,
    "preview": "const std = @import(\"std\");\nconst Allocator = std.mem.Allocator;\nconst Random = std.Random;\n\nconst ly_core = @import(\"ly"
  },
  {
    "path": "ly-ui/src/Widget.zig",
    "chars": 5247,
    "preview": "const Widget = @This();\n\nconst keyboard = @import(\"keyboard.zig\");\nconst TerminalBuffer = @import(\"TerminalBuffer.zig\");"
  },
  {
    "path": "ly-ui/src/components/BigLabel.zig",
    "chars": 6036,
    "preview": "const BigLabel = @This();\n\nconst std = @import(\"std\");\nconst Allocator = std.mem.Allocator;\n\nconst ly_core = @import(\"ly"
  },
  {
    "path": "ly-ui/src/components/CenteredBox.zig",
    "chars": 5104,
    "preview": "const std = @import(\"std\");\n\nconst Cell = @import(\"../Cell.zig\");\nconst Position = @import(\"../Position.zig\");\nconst Ter"
  },
  {
    "path": "ly-ui/src/components/Label.zig",
    "chars": 3482,
    "preview": "const Label = @This();\n\nconst std = @import(\"std\");\nconst Allocator = std.mem.Allocator;\n\nconst Cell = @import(\"../Cell."
  },
  {
    "path": "ly-ui/src/components/Text.zig",
    "chars": 5946,
    "preview": "const std = @import(\"std\");\nconst Allocator = std.mem.Allocator;\n\nconst keyboard = @import(\"../keyboard.zig\");\nconst Ter"
  },
  {
    "path": "ly-ui/src/components/bigLabelLocales/en.zig",
    "chars": 2019,
    "preview": "const BigLabel = @import(\"../BigLabel.zig\");\nconst LocaleChars = BigLabel.LocaleChars;\nconst X = BigLabel.X;\nconst O = B"
  },
  {
    "path": "ly-ui/src/components/bigLabelLocales/fa.zig",
    "chars": 2019,
    "preview": "const BigLabel = @import(\"../BigLabel.zig\");\nconst LocaleChars = BigLabel.LocaleChars;\nconst X = BigLabel.X;\nconst O = B"
  },
  {
    "path": "ly-ui/src/components/generic.zig",
    "chars": 5740,
    "preview": "const std = @import(\"std\");\n\nconst Cell = @import(\"../Cell.zig\");\nconst keyboard = @import(\"../keyboard.zig\");\nconst Ter"
  },
  {
    "path": "ly-ui/src/keyboard.zig",
    "chars": 17135,
    "preview": "const std = @import(\"std\");\nconst Allocator = std.mem.Allocator;\nconst KeyList = std.ArrayList(Key);\n\nconst TerminalBuff"
  },
  {
    "path": "ly-ui/src/root.zig",
    "chars": 567,
    "preview": "pub const ly_core = @import(\"ly-core\");\n\npub const Cell = @import(\"Cell.zig\");\npub const keyboard = @import(\"keyboard.zi"
  },
  {
    "path": "readme.md",
    "chars": 8434,
    "preview": "# The Ly display manager\n\n![Ly screenshot](.github/screenshot.png \"Ly screenshot\")\n\nLy is a lightweight TUI (ncurses-lik"
  },
  {
    "path": "res/config.ini",
    "chars": 12131,
    "preview": "# Ly supports 24-bit true color with styling, which means each color is a 32-bit value.\n# The format is 0xSSRRGGBB, wher"
  },
  {
    "path": "res/custom-sessions/README",
    "chars": 1075,
    "preview": "A custom session is just a desktop entry file, like for X11 and Wayland\nsessions. For example:\n\n[Desktop Entry]\nName=Fis"
  },
  {
    "path": "res/lang/ar.ini",
    "chars": 2422,
    "preview": "authenticating = جاري المصادقة...\nbrightness_down = خفض السطوع\nbrightness_up = رفع السطوع\ncapslock = capslock\n\nerr_alloc"
  },
  {
    "path": "res/lang/bg.ini",
    "chars": 3372,
    "preview": "authenticating = удостоверяване...\nbrightness_down = намаляване на яркостта\nbrightness_up = увеличаване на яркостта\ncaps"
  },
  {
    "path": "res/lang/cat.ini",
    "chars": 2206,
    "preview": "authenticating = autenticant...\nbrightness_down = abaixar brillantor\nbrightness_up = apujar brillantor\ncapslock = Bloq M"
  },
  {
    "path": "res/lang/cs.ini",
    "chars": 1705,
    "preview": "\n\n\ncapslock = capslock\n\nerr_alloc = alokace paměti selhala\n\n\nerr_bounds = index je mimo hranice pole\n\nerr_chdir = nelze "
  },
  {
    "path": "res/lang/de.ini",
    "chars": 2521,
    "preview": "authenticating = authentifizieren...\nbrightness_down = Helligkeit-\nbrightness_up = Helligkeit+\ncapslock = Feststelltaste"
  },
  {
    "path": "res/lang/en.ini",
    "chars": 2837,
    "preview": "authenticating = authenticating...\nbrightness_down = decrease brightness\nbrightness_up = increase brightness\ncapslock = "
  },
  {
    "path": "res/lang/eo.ini",
    "chars": 3185,
    "preview": "authenticating = aŭtentigado...\nbrightness_down = malpliigi helecon\nbrightness_up = pliigi helecon\ncapslock = majuskla b"
  },
  {
    "path": "res/lang/es.ini",
    "chars": 2019,
    "preview": "authenticating = autenticando...\nbrightness_down = bajar brillo\nbrightness_up = subir brillo\ncapslock = Bloq Mayús\n\nerr_"
  },
  {
    "path": "res/lang/fr.ini",
    "chars": 3367,
    "preview": "authenticating = authentification...\nbrightness_down = diminuer la luminosité\nbrightness_up = augmenter la luminosité\nca"
  },
  {
    "path": "res/lang/it.ini",
    "chars": 1742,
    "preview": "\n\n\ncapslock = capslock\n\nerr_alloc = impossibile allocare memoria\n\n\nerr_bounds = indice fuori limite\n\nerr_chdir = impossi"
  },
  {
    "path": "res/lang/ja_JP.ini",
    "chars": 1720,
    "preview": "authenticating = 認証中...\nbrightness_down = 明るさを下げる\nbrightness_up = 明るさを上げる\ncapslock = CapsLock\n\nerr_alloc = メモリ割り当て失敗\n\n\ne"
  },
  {
    "path": "res/lang/ku.ini",
    "chars": 3107,
    "preview": "authenticating = tê piştrastkirin...\nbrightness_down = ronahiyê kêm bike\nbrightness_up = ronahiyê bilind bike\ncapslock ="
  },
  {
    "path": "res/lang/lv.ini",
    "chars": 2681,
    "preview": "authenticating = autentificējas...\nbrightness_down = samazināt spilgtumu\nbrightness_up = palielināt spilgtumu\ncapslock ="
  },
  {
    "path": "res/lang/normalize_lang_files.py",
    "chars": 1758,
    "preview": "#!/usr/bin/env python3\n\nfrom pathlib import Path\nfrom sys import stderr\n\n\ndef process_lang_file(path: Path, lang_keys: l"
  },
  {
    "path": "res/lang/pl.ini",
    "chars": 2881,
    "preview": "authenticating = uwierzytelnianie...\nbrightness_down = zmniejsz jasność\nbrightness_up = zwiększ jasność\ncapslock = capsl"
  },
  {
    "path": "res/lang/pt.ini",
    "chars": 1737,
    "preview": "\n\n\ncapslock = capslock\n\nerr_alloc = erro na atribuição de memória\n\n\nerr_bounds = índice fora de limites\n\nerr_chdir = err"
  },
  {
    "path": "res/lang/pt_BR.ini",
    "chars": 1840,
    "preview": "\n\n\ncapslock = caixa alta\n\nerr_alloc = alocação de memória malsucedida\n\n\nerr_bounds = índice fora de limites\n\nerr_chdir ="
  },
  {
    "path": "res/lang/ro.ini",
    "chars": 1225,
    "preview": "\n\n\ncapslock = capslock\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\nerr_pam_abort = tranzacţie pam anulată\nerr_pam_acct_expired = cont expira"
  },
  {
    "path": "res/lang/ru.ini",
    "chars": 2813,
    "preview": "authenticating = аутентификация...\nbrightness_down = уменьшить яркость\nbrightness_up = увеличить яркость\ncapslock = caps"
  },
  {
    "path": "res/lang/sr.ini",
    "chars": 1764,
    "preview": "\n\n\ncapslock = capslock\n\nerr_alloc = neuspijesna alokacija memorije\n\n\nerr_bounds = izvan granica indeksa\n\nerr_chdir = neu"
  },
  {
    "path": "res/lang/sv.ini",
    "chars": 3219,
    "preview": "authenticating = autentiserar...\nbrightness_down = minska ljusstyrka\nbrightness_up = öka ljusstyrka\ncapslock = capslock\n"
  },
  {
    "path": "res/lang/tr.ini",
    "chars": 1804,
    "preview": "\nbrightness_down = parlakligi azalt\nbrightness_up = parlakligi arttir\ncapslock = capslock\n\nerr_alloc = basarisiz bellek "
  },
  {
    "path": "res/lang/uk.ini",
    "chars": 1763,
    "preview": "\n\n\ncapslock = capslock\n\nerr_alloc = невдале виділення пам'яті\n\n\nerr_bounds = поза межами індексу\n\nerr_chdir = не вдалося"
  },
  {
    "path": "res/lang/zh_CN.ini",
    "chars": 995,
    "preview": "\n\n\ncapslock = 大写锁定\n\nerr_alloc = 内存分配失败\n\n\nerr_bounds = 索引越界\n\nerr_chdir = 无法打开home文件夹\n\n\n\nerr_dgn_oob = 日志消息\nerr_domain = 无"
  },
  {
    "path": "res/ly-dinit",
    "chars": 287,
    "preview": "type            = process\nrestart         = true\nsmooth-recovery = true\ncommand         = $PREFIX_DIRECTORY/bin/$EXECUTA"
  },
  {
    "path": "res/ly-freebsd-wrapper",
    "chars": 312,
    "preview": "#!/bin/sh\n\n# On FreeBSD, even if we override the default login program, getty will still\n# try to append \"login -fp root"
  },
  {
    "path": "res/ly-kmsconvt@.service",
    "chars": 456,
    "preview": "[Unit]\nDescription=TUI display manager using KMSCON\nAfter=systemd-user-sessions.service plymouth-quit-wait.service\nAfter"
  },
  {
    "path": "res/ly-openrc",
    "chars": 715,
    "preview": "#!/sbin/openrc-run\n\nname=\"ly\"\ndescription=\"TUI Display Manager\"\n\n## Supervisor daemon\nsupervisor=supervise-daemon\nrespaw"
  },
  {
    "path": "res/ly-runit-service/conf",
    "chars": 196,
    "preview": "if [ -x /sbin/agetty -o -x /bin/agetty ]; then\n\t# util-linux specific settings\n\tif [ \"${tty}\" = \"tty1\" ]; then\n\t\tGETTY_A"
  },
  {
    "path": "res/ly-runit-service/finish",
    "chars": 58,
    "preview": "#!/bin/sh\n[ -r conf ] && . ./conf\n\nexec utmpset -w ${TTY}\n"
  },
  {
    "path": "res/ly-runit-service/run",
    "chars": 303,
    "preview": "#!/bin/sh\n\n[ -r conf ] && . ./conf\n\nif [ -x /sbin/getty -o -x /bin/getty ]; then\n\t# busybox\n\tGETTY=getty\nelif [ -x /sbin"
  },
  {
    "path": "res/ly-s6/run",
    "chars": 106,
    "preview": "#!/bin/execlineb -P\nexec agetty -L -8 -n -l $PREFIX_DIRECTORY/bin/$EXECUTABLE_NAME tty$DEFAULT_TTY 115200\n"
  },
  {
    "path": "res/ly-s6/type",
    "chars": 8,
    "preview": "longrun\n"
  },
  {
    "path": "res/ly-sysvinit",
    "chars": 1504,
    "preview": "#!/bin/sh\n### BEGIN INIT INFO\n# Provides:          ly\n# Required-Start:    $remote_fs $syslog\n# Required-Stop:     $remo"
  },
  {
    "path": "res/ly@.service",
    "chars": 322,
    "preview": "[Unit]\nDescription=TUI display manager\nAfter=systemd-user-sessions.service plymouth-quit-wait.service\nAfter=getty@%i.ser"
  },
  {
    "path": "res/pam.d/ly-freebsd",
    "chars": 222,
    "preview": "#%PAM-1.0\n\n# OpenPAM (used in FreeBSD) doesn't support prepending \"-\" for ignoring missing\n# modules.\nauth       include"
  },
  {
    "path": "res/pam.d/ly-freebsd-autologin",
    "chars": 260,
    "preview": "#%PAM-1.0\n\n# OpenPAM (used in FreeBSD) doesn't support prepending \"-\" for ignoring missing\n# modules.\nauth       require"
  },
  {
    "path": "res/pam.d/ly-linux",
    "chars": 486,
    "preview": "#%PAM-1.0\n\nauth       include      login\n-auth      optional     pam_gnome_keyring.so\n-auth      optional     pam_kwalle"
  },
  {
    "path": "res/pam.d/ly-linux-autologin",
    "chars": 494,
    "preview": "#%PAM-1.0\n\nauth       required     pam_permit.so\n-auth      optional     pam_gnome_keyring.so\n-auth      optional     pa"
  },
  {
    "path": "res/setup.sh",
    "chars": 3926,
    "preview": "#!/bin/sh\n# Shell environment setup after login\n# Copyright (C) 2015-2016 Pier Luigi Fiorini <pierluigi.fiorini@gmail.co"
  },
  {
    "path": "res/startup.sh",
    "chars": 1167,
    "preview": "#!/bin/sh\n# This file is executed when starting Ly (before the TTY is taken control of)\n# Custom startup code can be pla"
  },
  {
    "path": "src/Environment.zig",
    "chars": 648,
    "preview": "const ini = @import(\"ly-ui\").ly_core.ini;\nconst Ini = ini.Ini;\n\nconst enums = @import(\"enums.zig\");\nconst DisplayServer "
  },
  {
    "path": "src/animations/Cascade.zig",
    "chars": 2190,
    "preview": "const std = @import(\"std\");\nconst math = std.math;\n\nconst ly_ui = @import(\"ly-ui\");\nconst Cell = ly_ui.Cell;\nconst Termi"
  },
  {
    "path": "src/animations/ColorMix.zig",
    "chars": 3741,
    "preview": "const std = @import(\"std\");\nconst math = std.math;\n\nconst ly_ui = @import(\"ly-ui\");\nconst Cell = ly_ui.Cell;\nconst Termi"
  },
  {
    "path": "src/animations/Doom.zig",
    "chars": 4952,
    "preview": "const std = @import(\"std\");\nconst Allocator = std.mem.Allocator;\n\nconst ly_ui = @import(\"ly-ui\");\nconst Cell = ly_ui.Cel"
  },
  {
    "path": "src/animations/DurFile.zig",
    "chars": 19735,
    "preview": "const std = @import(\"std\");\nconst Allocator = std.mem.Allocator;\nconst Json = std.json;\nconst eql = std.mem.eql;\nconst f"
  },
  {
    "path": "src/animations/GameOfLife.zig",
    "chars": 6894,
    "preview": "const std = @import(\"std\");\nconst Allocator = std.mem.Allocator;\n\nconst ly_ui = @import(\"ly-ui\");\nconst Cell = ly_ui.Cel"
  },
  {
    "path": "src/animations/Matrix.zig",
    "chars": 7125,
    "preview": "const std = @import(\"std\");\nconst Allocator = std.mem.Allocator;\nconst Random = std.Random;\n\nconst ly_ui = @import(\"ly-u"
  },
  {
    "path": "src/auth.zig",
    "chars": 25414,
    "preview": "const std = @import(\"std\");\nconst Md5 = std.crypto.hash.Md5;\nconst builtin = @import(\"builtin\");\nconst build_options = @"
  },
  {
    "path": "src/components/InfoLine.zig",
    "chars": 2419,
    "preview": "const std = @import(\"std\");\nconst Allocator = std.mem.Allocator;\n\nconst ly_ui = @import(\"ly-ui\");\nconst keyboard = ly_ui"
  },
  {
    "path": "src/components/Session.zig",
    "chars": 2815,
    "preview": "const std = @import(\"std\");\nconst Allocator = std.mem.Allocator;\n\nconst ly_ui = @import(\"ly-ui\");\nconst keyboard = ly_ui"
  },
  {
    "path": "src/components/UserList.zig",
    "chars": 3483,
    "preview": "const std = @import(\"std\");\nconst Allocator = std.mem.Allocator;\n\nconst ly_ui = @import(\"ly-ui\");\nconst keyboard = ly_ui"
  },
  {
    "path": "src/config/Config.zig",
    "chars": 3671,
    "preview": "const build_options = @import(\"build_options\");\n\nconst enums = @import(\"../enums.zig\");\nconst Animation = enums.Animatio"
  },
  {
    "path": "src/config/Lang.zig",
    "chars": 4169,
    "preview": "//\n// NOTE: After editing this file, please run `/res/lang/normalize_lang_files.py`\n//       to update all the language "
  },
  {
    "path": "src/config/OldSave.zig",
    "chars": 56,
    "preview": "user: ?[]const u8 = null,\nsession_index: ?usize = null,\n"
  },
  {
    "path": "src/config/SavedUsers.zig",
    "chars": 591,
    "preview": "const std = @import(\"std\");\n\nconst SavedUsers = @This();\n\nconst User = struct {\n    username: []const u8,\n    session_in"
  },
  {
    "path": "src/config/migrator.zig",
    "chars": 8980,
    "preview": "// The migrator ensures compatibility with older configuration files\n// Properties removed or changed since 0.6.0\n// Col"
  },
  {
    "path": "src/enums.zig",
    "chars": 1371,
    "preview": "const std = @import(\"std\");\n\npub const Animation = enum {\n    none,\n    doom,\n    matrix,\n    colormix,\n    gameoflife,\n"
  },
  {
    "path": "src/main.zig",
    "chars": 70189,
    "preview": "const std = @import(\"std\");\nconst Allocator = std.mem.Allocator;\nconst StringList = std.ArrayListUnmanaged([]const u8);\n"
  }
]

// ... and 1 more files (download for full content)

About this extraction

This page contains the full source code of the fairyglade/ly GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 93 files (375.0 KB), approximately 102.3k tokens, and a symbol index with 2 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!