diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..ba1041c --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,2 @@ +github: AnErrupTion +liberapay: ShiningLea diff --git a/.github/ISSUE_TEMPLATE/bug.yml b/.github/ISSUE_TEMPLATE/bug.yml index bccca4d..64eef9c 100644 --- a/.github/ISSUE_TEMPLATE/bug.yml +++ b/.github/ISSUE_TEMPLATE/bug.yml @@ -11,11 +11,19 @@ body: options: - label: I have looked for any other duplicate issues required: true + - label: I have reproduced the issue with the default config of Ly + required: true + - label: I have reproduced the issue on a fresh install of Ly **as well as** my OS and my desktop environment/window manager, all with **default settings**, except ones I will mention + required: false + - label: I have confirmed this issue also occurs on the latest development version (found in the `master` branch) + required: true + - label: I understand that this issue **will** be closed if requirements are not or not properly met + required: true - type: input id: version attributes: label: Ly version - description: The output of `ly --version` + description: The output of `ly --version`. Please note that only the latest release and the current version in development are supported. placeholder: 1.1.0-dev.12+2b0301c validations: required: true @@ -33,6 +41,20 @@ body: description: What did you expect to happen instead? validations: required: true + - type: input + id: desktop + attributes: + label: OS + Desktop environment/Window manager + description: Which operating system and DE (or WM) did you use when observing the problem? + validations: + required: true + - type: input + id: osimage + attributes: + label: OS version/snapshot used to reproduce + description: Which version (or snapshot if rolling release) of the OS did you use to reproduce the issue? This is only required if you have reproduced the issue on a fresh copy of the OS. + validations: + required: false - type: textarea id: reproduction attributes: @@ -50,5 +72,13 @@ body: id: logs attributes: label: Relevant logs - description: Please copy and paste 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. + 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. diff --git a/.github/ISSUE_TEMPLATE/feature.yml b/.github/ISSUE_TEMPLATE/feature.yml index b9a26af..b4cc299 100644 --- a/.github/ISSUE_TEMPLATE/feature.yml +++ b/.github/ISSUE_TEMPLATE/feature.yml @@ -20,3 +20,10 @@ body: description: What do you want to be added? Describe the behavior clearly. validations: required: true + - type: textarea + id: alternatives + attributes: + label: Alternatives + description: Are there any alternatives to the new behavior you wish to see being added? + validations: + required: true diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..606d00f --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,12 @@ +## What are the changes about? + +_Replace this with a brief description of your changes_ + +## What existing issue(s) does this resolve? + +_Replace this with a reference to (an) existing issue(s), or N/A if there is none_ + +## Pre-requisites + +- [ ] I have tested & confirmed the changes work locally +- [ ] I have read and fully adhere to the rules set in the contributing guidelines found in `CONTRIBUTING.md` diff --git a/.github/screenshot.png b/.github/screenshot.png index 9d0db4b..396a251 100644 Binary files a/.github/screenshot.png and b/.github/screenshot.png differ diff --git a/.gitignore b/.gitignore index de08f4f..fa23ae8 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ zig-cache/ zig-out/ valgrind.log .zig-cache +zig-pkg diff --git a/build.zig b/build.zig index f0e9074..cdb0d1f 100644 --- a/build.zig +++ b/build.zig @@ -1,9 +1,23 @@ 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 InstallType = enum { + installexe, + installnoconf, + uninstallexe, + uninstallnoconf, +}; -const min_zig_string = "0.12.0"; +const min_zig_string = "0.16.0"; const current_zig = builtin.zig_version; // Implementing zig version detection through compile time @@ -14,36 +28,65 @@ comptime { } } -const ly_version = std.SemanticVersion{ .major = 1, .minor = 1, .patch = 0 }; +const ly_version = std.SemanticVersion{ .major = 1, .minor = 5, .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 default_tty_str: []const u8 = undefined; +fn InstallStep( + b: *std.Build, + target: std.Build.ResolvedTarget, + comptime install_type: InstallType, + dest_directory: []const u8, + config_directory: []const u8, + prefix_directory: []const u8, + executable_name: []const u8, + init_system: InitSystem, + default_tty_str: []const u8, +) *std.Build.Step.Run { + const step = b.addRunArtifact(b.addExecutable(.{ + .name = "install", + .root_module = b.createModule(.{ + .root_source_file = b.path("install.zig"), + .target = target, + }), + })); + step.step.dependOn(b.getInstallStep()); -const ProgressNode = if (current_zig.minor == 12) *std.Progress.Node else std.Progress.Node; + step.addArgs(&.{ + std.enums.tagName(InstallType, install_type).?, + dest_directory, + config_directory, + prefix_directory, + executable_name, + std.enums.tagName(InitSystem, init_system).?, + default_tty_str, + }); + + return step; +} 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"; - - const bin_directory = try b.allocator.dupe(u8, config_directory); - config_directory = try std.fs.path.join(b.allocator, &[_][]const u8{ dest_directory, config_directory }); + const dest_directory = b.option([]const u8, "dest_directory", "Specify a destination directory for installation") orelse ""; + const 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"; + const prefix_directory = b.option([]const u8, "prefix_directory", "Specify a default prefix directory (default is /usr)") orelse "/usr"; + const executable_name = b.option([]const u8, "name", "Specify installed executable file name (default is ly)") orelse "ly"; + const 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}); + const default_tty_str = try std.fmt.allocPrint(b.allocator, "{d}", .{default_tty}); - build_options.addOption([]const u8, "config_directory", bin_directory); + 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(.{}); @@ -51,286 +94,98 @@ pub fn build(b: *std.Build) !void { const exe = b.addExecutable(.{ .name = "ly", - .root_source_file = b.path("src/main.zig"), - .target = target, - .optimize = optimize, + .root_module = b.createModule(.{ + .root_source_file = b.path("src/main.zig"), + .target = target, + .optimize = optimize, + .link_libc = true, + }), }); - const zigini = b.dependency("zigini", .{ .target = target, .optimize = optimize }); - exe.root_module.addImport("zigini", zigini.module("zigini")); + const zlua = b.dependency("zlua", .{ + .target = target, + .optimize = optimize, + .lang = .luajit, + }); + exe.root_module.addImport("zlua", zlua.module("zlua")); + + const ly_ui = b.dependency("ly_ui", .{ + .target = target, + .optimize = optimize, + .enable_x11_support = enable_x11_support, + .fallback_uid_min = fallback_uid_min, + .fallback_uid_max = fallback_uid_max, + }); + 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.addIncludePath(b.path("include")); - exe.linkSystemLibrary("pam"); - if (enable_x11_support) exe.linkSystemLibrary("xcb"); - exe.linkLibC(); - - const translate_c = b.addTranslateC(.{ - .root_source_file = b.path("include/termbox2.h"), - .target = target, - .optimize = optimize, - }); - translate_c.defineCMacroRaw("TB_IMPL"); - const termbox2 = translate_c.addModule("termbox2"); - exe.root_module.addImport("termbox2", termbox2); + exe.root_module.linkSystemLibrary("pam", .{}); + if (enable_x11_support) exe.root_module.linkSystemLibrary("xcb", .{}); 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"); - installexe_step.makeFn = ExeInstaller(true).make; - installexe_step.dependOn(b.getInstallStep()); + const installexe_step = b.step("installexe", "Install Ly and the selected init system service"); + installexe_step.dependOn(&InstallStep( + b, + target, + .installexe, + dest_directory, + config_directory, + prefix_directory, + executable_name, + init_system, + default_tty_str, + ).step); - const installnoconf_step = b.step("installnoconf", "Install Ly without its configuration file"); - installnoconf_step.makeFn = ExeInstaller(false).make; - installnoconf_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.dependOn(&InstallStep( + b, + target, + .installnoconf, + dest_directory, + config_directory, + prefix_directory, + executable_name, + init_system, + default_tty_str, + ).step); - const installsystemd_step = b.step("installsystemd", "Install the Ly systemd service"); - installsystemd_step.makeFn = ServiceInstaller(.Systemd).make; - installsystemd_step.dependOn(installexe_step); + const uninstallexe_step = b.step("uninstallexe", "Uninstall Ly and remove the selected init system service"); + uninstallexe_step.dependOn(&InstallStep( + b, + target, + .uninstallexe, + dest_directory, + config_directory, + prefix_directory, + executable_name, + init_system, + default_tty_str, + ).step); - const installopenrc_step = b.step("installopenrc", "Install the Ly openrc service"); - installopenrc_step.makeFn = ServiceInstaller(.Openrc).make; - installopenrc_step.dependOn(installexe_step); - - const installrunit_step = b.step("installrunit", "Install the Ly runit service"); - installrunit_step.makeFn = ServiceInstaller(.Runit).make; - installrunit_step.dependOn(installexe_step); - - const installs6_step = b.step("installs6", "Install the Ly s6 service"); - installs6_step.makeFn = ServiceInstaller(.S6).make; - installs6_step.dependOn(installexe_step); - - const installdinit_step = b.step("installdinit", "Install the Ly dinit service"); - installdinit_step.makeFn = ServiceInstaller(.Dinit).make; - installdinit_step.dependOn(installexe_step); - - const uninstallall_step = b.step("uninstallall", "Uninstall Ly and all services"); - uninstallall_step.makeFn = uninstallall; -} - -pub fn ExeInstaller(install_conf: bool) type { - return struct { - pub fn make(step: *std.Build.Step, _: ProgressNode) !void { - try install_ly(step.owner.allocator, install_conf); - } - }; -} - -const InitSystem = enum { - Systemd, - Openrc, - Runit, - S6, - Dinit, -}; - -pub fn ServiceInstaller(comptime init_system: InitSystem) type { - return struct { - pub fn make(step: *std.Build.Step, _: ProgressNode) !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); - - 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 }); - }, - .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 }); - - try std.fs.cwd().symLink("/run/runit/supervise.ly", supervise_path, .{}); - 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", .{}); - }, - } - } - }; -} - -fn install_ly(allocator: std.mem.Allocator, install_config: bool) !void { - const ly_config_directory = try std.fs.path.join(allocator, &[_][]const u8{ 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_lang_path = try std.fs.path.join(allocator, &[_][]const u8{ config_directory, "/ly/lang" }); - std.fs.cwd().makePath(ly_lang_path) catch { - std.debug.print("warn: {s} already exists as a directory.\n", .{config_directory}); - }; - - { - const exe_path = try std.fs.path.join(allocator, &[_][]const u8{ dest_directory, prefix_directory, "/bin" }); - if (!std.mem.eql(u8, dest_directory, "")) { - std.fs.cwd().makePath(exe_path) catch { - 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) { - 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); - - const patched_config = try patchFile(allocator, "res/config.ini", patch_map); - try installText(patched_config, config_dir, ly_config_directory, "config.ini", .{}); - } - - { - var patch_map = PatchMap.init(allocator); - defer patch_map.deinit(); - - try patch_map.put("$CONFIG_DIRECTORY", config_directory); - - const patched_setup = try patchFile(allocator, "res/setup.sh", patch_map); - try installText(patched_setup, config_dir, ly_config_directory, "setup.sh", .{ .mode = 0o755 }); - } - } - - { - var lang_dir = std.fs.cwd().openDir(ly_lang_path, .{}) catch unreachable; - defer lang_dir.close(); - - try installFile("res/lang/cat.ini", lang_dir, ly_lang_path, "cat.ini", .{}); - try installFile("res/lang/cs.ini", lang_dir, ly_lang_path, "cs.ini", .{}); - try installFile("res/lang/de.ini", lang_dir, ly_lang_path, "de.ini", .{}); - try installFile("res/lang/en.ini", lang_dir, ly_lang_path, "en.ini", .{}); - try installFile("res/lang/es.ini", lang_dir, ly_lang_path, "es.ini", .{}); - try installFile("res/lang/fr.ini", lang_dir, ly_lang_path, "fr.ini", .{}); - try installFile("res/lang/it.ini", lang_dir, ly_lang_path, "it.ini", .{}); - try installFile("res/lang/pl.ini", lang_dir, ly_lang_path, "pl.ini", .{}); - try installFile("res/lang/pt.ini", lang_dir, ly_lang_path, "pt.ini", .{}); - try installFile("res/lang/pt_BR.ini", lang_dir, ly_lang_path, "pt_BR.ini", .{}); - try installFile("res/lang/ro.ini", lang_dir, ly_lang_path, "ro.ini", .{}); - try installFile("res/lang/ru.ini", lang_dir, ly_lang_path, "ru.ini", .{}); - try installFile("res/lang/sr.ini", lang_dir, ly_lang_path, "sr.ini", .{}); - try installFile("res/lang/sv.ini", lang_dir, ly_lang_path, "sv.ini", .{}); - try installFile("res/lang/tr.ini", lang_dir, ly_lang_path, "tr.ini", .{}); - try installFile("res/lang/uk.ini", lang_dir, ly_lang_path, "uk.ini", .{}); - } - - { - const pam_path = try std.fs.path.join(allocator, &[_][]const u8{ dest_directory, config_directory, "/pam.d" }); - if (!std.mem.eql(u8, dest_directory, "")) { - std.fs.cwd().makePath(pam_path) catch { - 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("res/pam.d/ly", pam_dir, pam_path, "ly", .{ .override_mode = 0o644 }); - } -} - -pub fn uninstallall(step: *std.Build.Step, _: ProgressNode) !void { - const allocator = step.owner.allocator; - - try deleteTree(allocator, config_directory, "/ly", "ly config directory not found"); - - const exe_path = try std.fs.path.join(allocator, &[_][]const u8{ dest_directory, 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"); - try deleteFile(allocator, prefix_directory, "/lib/systemd/system/ly.service", "systemd service not found"); - try deleteFile(allocator, config_directory, "/init.d/ly", "openrc service not found"); - try deleteTree(allocator, config_directory, "/sv/ly", "runit service not found"); - 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"); - try deleteFile(allocator, config_directory, "/dinit.d/ly", "dinit service not found"); + const uninstallnoconf_step = b.step("uninstallnoconf", "Uninstall Ly and remove the selected init system service, but keep the configuration directory"); + uninstallnoconf_step.dependOn(&InstallStep( + b, + target, + .uninstallnoconf, + dest_directory, + config_directory, + prefix_directory, + executable_name, + init_system, + default_tty_str, + ).step); } fn getVersionStr(b: *std.Build, name: []const u8, version: std.SemanticVersion) ![]const u8 { @@ -345,11 +200,11 @@ fn getVersionStr(b: *std.Build, name: []const u8, version: std.SemanticVersion) "--match", "*.*.*", "--tags", - }, &status, .Ignore) catch { + }, &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"); + git_describe = std.mem.trimStart(u8, git_describe, "v"); switch (std.mem.count(u8, git_describe, "-")) { 0 => { @@ -362,13 +217,13 @@ fn getVersionStr(b: *std.Build, name: []const u8, version: std.SemanticVersion) 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 tagged_ancestor = std.mem.trimStart(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 '{}' must be greater than tagged ancestor '{}'\n", .{ name, version, ancestor_ver }); + std.debug.print("{s} version '{f}' must be greater than tagged ancestor '{f}'\n", .{ name, version, ancestor_ver }); std.process.exit(1); } @@ -387,90 +242,3 @@ fn getVersionStr(b: *std.Build, name: []const u8, version: std.SemanticVersion) }, } } - -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 reader = file.reader(); - var text = try reader.readAllAlloc(allocator, std.math.maxInt(u16)); - - 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(); - - const writer = file.writer(); - try writer.writeAll(text); - - 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}); -} diff --git a/build.zig.zon b/build.zig.zon index c4c9cbd..1d19b69 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -1,16 +1,24 @@ .{ - .name = "ly", - .version = "1.0.0", - .minimum_zig_version = "0.12.0", + .name = .ly, + .version = "1.5.0", + .fingerprint = 0xa148ffcc5dc2cb59, + .minimum_zig_version = "0.16.0", .dependencies = .{ - .clap = .{ - .url = "https://github.com/Hejsil/zig-clap/archive/refs/tags/0.9.1.tar.gz", - .hash = "122062d301a203d003547b414237229b09a7980095061697349f8bef41be9c30266b", + .ly_ui = .{ + .path = "ly-ui", }, - .zigini = .{ - .url = "https://github.com/Kawaii-Ash/zigini/archive/0bba97a12582928e097f4074cc746c43351ba4c8.tar.gz", - .hash = "12209b971367b4066d40ecad4728e6fdffc4cc4f19356d424c2de57f5b69ac7a619a", + .clap = .{ + .url = "git+https://github.com/Hejsil/zig-clap#fc1e5cc3f6d9d3001112385ee6256d694e959d2f", + .hash = "clap-0.11.0-oBajB7foAQC3Iyn4IVCkUdYaOVVng5IZkSncySTjNig1", + }, + .zlua = .{ + .url = "git+https://github.com/natecraddock/ziglua?ref=zig-0.16#8f271c82baa5fc43aa02a72f6da020c2025d9436", + .hash = "zlua-0.1.0-hGRpC2aABQD4D9PBVH3wAW8k32-I4969MRQ0CpOwoley", }, }, - .paths = .{""}, + .paths = .{ + "build.zig", + "build.zig.zon", + "src", + }, } diff --git a/contributing.md b/contributing.md new file mode 100644 index 0000000..eddaffb --- /dev/null +++ b/contributing.md @@ -0,0 +1,29 @@ +# Contributing to Ly + +To contribute to Ly, you have to open a pull request on [Codeberg](https://codeberg.org/fairyglade/ly), as [GitHub](https://github.com/fairyglade/ly) is just a mirror. However, you also have to respect the following rules, otherwise your PR may end up being rejected: + +## AI usage + +While we cannot control your usage of any LLM whatsoever, we do heavily discourage their use for environmental, ethical & moral reasons that you can learn more about on the Internet. However, if you do end up still using them, here are a couple rules you **must absolutely** respect for your PR not to get instantly shot down. Of course, it goes without saying that **all** the other rules in the other sections below **must** be adhered to, even more so when AI is used. + +1. **Communicate through you**, not the AI; as in, responses to reviews or other comments in the PR must be written by you and not an LLM. If English isn't your native language and you have a lot of trouble speaking it, you _may_ use AI, but responses must not be your typical, alienating AI-generated text: https://github.com/realrossmanngroup/no_ai_slop_writing_rules +2. Control the code, as in, **don't vibecode**, especially if you don't know Zig. Don't waste the maintainers's time, and **heavily test your code**, as AI-generated code is typically more error-prone in subtle ways (even if they can be better than humans in some other aspects). Finally, don't generate any "summary" or whatever the AI may come up with during the process. **If you cannot understand the generated code, do not contribute it**. Maintainers are busy and are often simple volunteers, so the less work they have to do, the better. +3. **Be transparent about AI usage**. Precise what AI model was used in the process, and preferably, how you went about creating your changes (i.e. how did you use AI to make the pull request). The more honest you are about it, the more likely your PR will be accepted and merged into the code base. + +If all the above rules are respected, your changes have much more likely to be accepted into the code base. + +## Code style + +You must follow Zig's [style guide](https://ziglang.org/documentation/master/#Style-Guide). In most cases, all you'll have to do is run `zig fmt` after you have completed your changes, though it does not fix everything, notably variable, function & field naming, as well as the maximum length of aline. + +For the former, please refer to the aforementioned style guide in that case. For the latter, you must respect a maximum line length of **80 characters**. For function calls with many parameters or anything similar that may overflow this limit, consider adding a trailing comma to the list so that `zig fmt` can split it into multiple lines. Ideally, few or no lines end up having to get soft wrapped by an editor having this limit set. + +## Commit names & descriptions + +1. **Commit names must be descriptive**, resorting to the commit description if they are too long. In such cases, the commit name **must** be shortened while still making sense. With that said, no particular convention (emojis, prefixes & suffixes, etc.) is mandated for them, and you are free to adopt any one you think is best or that you are used to. +2. **Do not force push**, as PRs will get squashed into a single commit anyway. This erases history and makes it impossible to see the changes done over time and by a particular commit. +3. While not a requirement per se, **consider signing your commits** with an SSH or GPG key for security purposes. This ensures to a lesser extent that you are the person who you claim to be, and reduces the potential amount of malicious contributions that could infiltrate the code base. + +## Additional requirements + +Other requirements (such as testing your code) for merging the changes will be available when you open your pull request via the template (which you **must** use), however, these are not stated here because they may not be mandatory in some cases (e.g. if the changes are still work-in-progress), so they may temporarily be ommitted for a certain period of time until the changes are fully ready to be reviewed, for instance. diff --git a/create_vendor_tarball.sh b/create_vendor_tarball.sh new file mode 100755 index 0000000..b42f58c --- /dev/null +++ b/create_vendor_tarball.sh @@ -0,0 +1,2 @@ +#!/bin/sh +tar --zstd -cvf vendor.tar.zst zig-pkg ly-ui/zig-pkg ly-core/zig-pkg diff --git a/include/termbox2.h b/include/termbox2.h deleted file mode 100644 index 5fc791f..0000000 --- a/include/termbox2.h +++ /dev/null @@ -1,3475 +0,0 @@ -/* -MIT License - -Copyright (c) 2010-2020 nsf - 2015-2024 Adam Saponara - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. -*/ - -#ifndef TERMBOX_H_INCL -#define TERMBOX_H_INCL - -#ifndef _XOPEN_SOURCE -#define _XOPEN_SOURCE -#endif - -#ifndef _DEFAULT_SOURCE -#define _DEFAULT_SOURCE -#endif - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#ifdef PATH_MAX -#define TB_PATH_MAX PATH_MAX -#else -#define TB_PATH_MAX 4096 -#endif - -#ifdef __cplusplus -extern "C" { -#endif - -// __ffi_start - -#define TB_VERSION_STR "2.5.0-dev" - -/* The following compile-time options are supported: - * - * TB_OPT_ATTR_W: Integer width of fg and bg attributes. Valid values - * (assuming system support) are 16, 32, and 64. (See - * uintattr_t). 32 or 64 enables output mode - * TB_OUTPUT_TRUECOLOR. 64 enables additional style - * attributes. (See tb_set_output_mode.) Larger values - * consume more memory in exchange for more features. - * Defaults to 16. - * - * TB_OPT_EGC: If set, enable extended grapheme cluster support - * (tb_extend_cell, tb_set_cell_ex). Consumes more memory. - * Defaults off. - * - * TB_OPT_PRINTF_BUF: Write buffer size for printf operations. Represents the - * largest string that can be sent in one call to tb_print* - * and tb_send* functions. Defaults to 4096. - * - * TB_OPT_READ_BUF: Read buffer size for tty reads. Defaults to 64. - * - * TB_OPT_TRUECOLOR: Deprecated. Sets TB_OPT_ATTR_W to 32 if not already set. - */ - -#if defined(TB_LIB_OPTS) || 0 // __tb_lib_opts -// Ensure consistent compile-time options when using as a shared library -#undef TB_OPT_ATTR_W -#undef TB_OPT_EGC -#undef TB_OPT_PRINTF_BUF -#undef TB_OPT_READ_BUF -#define TB_OPT_ATTR_W 64 -#define TB_OPT_EGC -#endif - -// Ensure sane TB_OPT_ATTR_W (16, 32, or 64) -#if defined TB_OPT_ATTR_W && TB_OPT_ATTR_W == 16 -#elif defined TB_OPT_ATTR_W && TB_OPT_ATTR_W == 32 -#elif defined TB_OPT_ATTR_W && TB_OPT_ATTR_W == 64 -#else -#undef TB_OPT_ATTR_W -#if defined TB_OPT_TRUECOLOR // Deprecated. Back-compat for old flag. -#define TB_OPT_ATTR_W 32 -#else -#define TB_OPT_ATTR_W 16 -#endif -#endif - -/* ASCII key constants (tb_event.key) */ -#define TB_KEY_CTRL_TILDE 0x00 -#define TB_KEY_CTRL_2 0x00 /* clash with 'CTRL_TILDE' */ -#define TB_KEY_CTRL_A 0x01 -#define TB_KEY_CTRL_B 0x02 -#define TB_KEY_CTRL_C 0x03 -#define TB_KEY_CTRL_D 0x04 -#define TB_KEY_CTRL_E 0x05 -#define TB_KEY_CTRL_F 0x06 -#define TB_KEY_CTRL_G 0x07 -#define TB_KEY_BACKSPACE 0x08 -#define TB_KEY_CTRL_H 0x08 /* clash with 'CTRL_BACKSPACE' */ -#define TB_KEY_TAB 0x09 -#define TB_KEY_CTRL_I 0x09 /* clash with 'TAB' */ -#define TB_KEY_CTRL_J 0x0a -#define TB_KEY_CTRL_K 0x0b -#define TB_KEY_CTRL_L 0x0c -#define TB_KEY_ENTER 0x0d -#define TB_KEY_CTRL_M 0x0d /* clash with 'ENTER' */ -#define TB_KEY_CTRL_N 0x0e -#define TB_KEY_CTRL_O 0x0f -#define TB_KEY_CTRL_P 0x10 -#define TB_KEY_CTRL_Q 0x11 -#define TB_KEY_CTRL_R 0x12 -#define TB_KEY_CTRL_S 0x13 -#define TB_KEY_CTRL_T 0x14 -#define TB_KEY_CTRL_U 0x15 -#define TB_KEY_CTRL_V 0x16 -#define TB_KEY_CTRL_W 0x17 -#define TB_KEY_CTRL_X 0x18 -#define TB_KEY_CTRL_Y 0x19 -#define TB_KEY_CTRL_Z 0x1a -#define TB_KEY_ESC 0x1b -#define TB_KEY_CTRL_LSQ_BRACKET 0x1b /* clash with 'ESC' */ -#define TB_KEY_CTRL_3 0x1b /* clash with 'ESC' */ -#define TB_KEY_CTRL_4 0x1c -#define TB_KEY_CTRL_BACKSLASH 0x1c /* clash with 'CTRL_4' */ -#define TB_KEY_CTRL_5 0x1d -#define TB_KEY_CTRL_RSQ_BRACKET 0x1d /* clash with 'CTRL_5' */ -#define TB_KEY_CTRL_6 0x1e -#define TB_KEY_CTRL_7 0x1f -#define TB_KEY_CTRL_SLASH 0x1f /* clash with 'CTRL_7' */ -#define TB_KEY_CTRL_UNDERSCORE 0x1f /* clash with 'CTRL_7' */ -#define TB_KEY_SPACE 0x20 -#define TB_KEY_BACKSPACE2 0x7f -#define TB_KEY_CTRL_8 0x7f /* clash with 'BACKSPACE2' */ - -#define tb_key_i(i) 0xffff - (i) -/* Terminal-dependent key constants (tb_event.key) and terminfo capabilities */ -/* BEGIN codegen h */ -/* Produced by ./codegen.sh on Thu, 13 Jul 2023 05:46:13 +0000 */ -#define TB_KEY_F1 (0xffff - 0) -#define TB_KEY_F2 (0xffff - 1) -#define TB_KEY_F3 (0xffff - 2) -#define TB_KEY_F4 (0xffff - 3) -#define TB_KEY_F5 (0xffff - 4) -#define TB_KEY_F6 (0xffff - 5) -#define TB_KEY_F7 (0xffff - 6) -#define TB_KEY_F8 (0xffff - 7) -#define TB_KEY_F9 (0xffff - 8) -#define TB_KEY_F10 (0xffff - 9) -#define TB_KEY_F11 (0xffff - 10) -#define TB_KEY_F12 (0xffff - 11) -#define TB_KEY_INSERT (0xffff - 12) -#define TB_KEY_DELETE (0xffff - 13) -#define TB_KEY_HOME (0xffff - 14) -#define TB_KEY_END (0xffff - 15) -#define TB_KEY_PGUP (0xffff - 16) -#define TB_KEY_PGDN (0xffff - 17) -#define TB_KEY_ARROW_UP (0xffff - 18) -#define TB_KEY_ARROW_DOWN (0xffff - 19) -#define TB_KEY_ARROW_LEFT (0xffff - 20) -#define TB_KEY_ARROW_RIGHT (0xffff - 21) -#define TB_KEY_BACK_TAB (0xffff - 22) -#define TB_KEY_MOUSE_LEFT (0xffff - 23) -#define TB_KEY_MOUSE_RIGHT (0xffff - 24) -#define TB_KEY_MOUSE_MIDDLE (0xffff - 25) -#define TB_KEY_MOUSE_RELEASE (0xffff - 26) -#define TB_KEY_MOUSE_WHEEL_UP (0xffff - 27) -#define TB_KEY_MOUSE_WHEEL_DOWN (0xffff - 28) - -#define TB_CAP_F1 0 -#define TB_CAP_F2 1 -#define TB_CAP_F3 2 -#define TB_CAP_F4 3 -#define TB_CAP_F5 4 -#define TB_CAP_F6 5 -#define TB_CAP_F7 6 -#define TB_CAP_F8 7 -#define TB_CAP_F9 8 -#define TB_CAP_F10 9 -#define TB_CAP_F11 10 -#define TB_CAP_F12 11 -#define TB_CAP_INSERT 12 -#define TB_CAP_DELETE 13 -#define TB_CAP_HOME 14 -#define TB_CAP_END 15 -#define TB_CAP_PGUP 16 -#define TB_CAP_PGDN 17 -#define TB_CAP_ARROW_UP 18 -#define TB_CAP_ARROW_DOWN 19 -#define TB_CAP_ARROW_LEFT 20 -#define TB_CAP_ARROW_RIGHT 21 -#define TB_CAP_BACK_TAB 22 -#define TB_CAP__COUNT_KEYS 23 -#define TB_CAP_ENTER_CA 23 -#define TB_CAP_EXIT_CA 24 -#define TB_CAP_SHOW_CURSOR 25 -#define TB_CAP_HIDE_CURSOR 26 -#define TB_CAP_CLEAR_SCREEN 27 -#define TB_CAP_SGR0 28 -#define TB_CAP_UNDERLINE 29 -#define TB_CAP_BOLD 30 -#define TB_CAP_BLINK 31 -#define TB_CAP_ITALIC 32 -#define TB_CAP_REVERSE 33 -#define TB_CAP_ENTER_KEYPAD 34 -#define TB_CAP_EXIT_KEYPAD 35 -#define TB_CAP_DIM 36 -#define TB_CAP_INVISIBLE 37 -#define TB_CAP__COUNT 38 -/* END codegen h */ - -/* Some hard-coded caps */ -#define TB_HARDCAP_ENTER_MOUSE "\x1b[?1000h\x1b[?1002h\x1b[?1015h\x1b[?1006h" -#define TB_HARDCAP_EXIT_MOUSE "\x1b[?1006l\x1b[?1015l\x1b[?1002l\x1b[?1000l" -#define TB_HARDCAP_STRIKEOUT "\x1b[9m" -#define TB_HARDCAP_UNDERLINE_2 "\x1b[21m" -#define TB_HARDCAP_OVERLINE "\x1b[53m" - -/* Colors (numeric) and attributes (bitwise) (tb_cell.fg, tb_cell.bg) */ -#define TB_DEFAULT 0x0000 -#define TB_BLACK 0x0001 -#define TB_RED 0x0002 -#define TB_GREEN 0x0003 -#define TB_YELLOW 0x0004 -#define TB_BLUE 0x0005 -#define TB_MAGENTA 0x0006 -#define TB_CYAN 0x0007 -#define TB_WHITE 0x0008 - -#if TB_OPT_ATTR_W == 16 -#define TB_BOLD 0x0100 -#define TB_UNDERLINE 0x0200 -#define TB_REVERSE 0x0400 -#define TB_ITALIC 0x0800 -#define TB_BLINK 0x1000 -#define TB_HI_BLACK 0x2000 -#define TB_BRIGHT 0x4000 -#define TB_DIM 0x8000 -#define TB_256_BLACK TB_HI_BLACK // TB_256_BLACK is deprecated -#else // 32 or 64 -#define TB_BOLD 0x01000000 -#define TB_UNDERLINE 0x02000000 -#define TB_REVERSE 0x04000000 -#define TB_ITALIC 0x08000000 -#define TB_BLINK 0x10000000 -#define TB_HI_BLACK 0x20000000 -#define TB_BRIGHT 0x40000000 -#define TB_DIM 0x80000000 -#define TB_TRUECOLOR_BOLD TB_BOLD // TB_TRUECOLOR_* is deprecated -#define TB_TRUECOLOR_UNDERLINE TB_UNDERLINE -#define TB_TRUECOLOR_REVERSE TB_REVERSE -#define TB_TRUECOLOR_ITALIC TB_ITALIC -#define TB_TRUECOLOR_BLINK TB_BLINK -#define TB_TRUECOLOR_BLACK TB_HI_BLACK -#endif - -#if TB_OPT_ATTR_W == 64 -#define TB_STRIKEOUT 0x0000000100000000 -#define TB_UNDERLINE_2 0x0000000200000000 -#define TB_OVERLINE 0x0000000400000000 -#define TB_INVISIBLE 0x0000000800000000 -#endif - -/* Event types (tb_event.type) */ -#define TB_EVENT_KEY 1 -#define TB_EVENT_RESIZE 2 -#define TB_EVENT_MOUSE 3 - -/* Key modifiers (bitwise) (tb_event.mod) */ -#define TB_MOD_ALT 1 -#define TB_MOD_CTRL 2 -#define TB_MOD_SHIFT 4 -#define TB_MOD_MOTION 8 - -/* Input modes (bitwise) (tb_set_input_mode) */ -#define TB_INPUT_CURRENT 0 -#define TB_INPUT_ESC 1 -#define TB_INPUT_ALT 2 -#define TB_INPUT_MOUSE 4 - -/* Output modes (tb_set_output_mode) */ -#define TB_OUTPUT_CURRENT 0 -#define TB_OUTPUT_NORMAL 1 -#define TB_OUTPUT_256 2 -#define TB_OUTPUT_216 3 -#define TB_OUTPUT_GRAYSCALE 4 -#if TB_OPT_ATTR_W >= 32 -#define TB_OUTPUT_TRUECOLOR 5 -#endif - -/* Common function return values unless otherwise noted. - * - * Library behavior is undefined after receiving TB_ERR_MEM. Callers may - * attempt reinitializing by freeing memory, invoking tb_shutdown, then - * tb_init. - */ -#define TB_OK 0 -#define TB_ERR -1 -#define TB_ERR_NEED_MORE -2 -#define TB_ERR_INIT_ALREADY -3 -#define TB_ERR_INIT_OPEN -4 -#define TB_ERR_MEM -5 -#define TB_ERR_NO_EVENT -6 -#define TB_ERR_NO_TERM -7 -#define TB_ERR_NOT_INIT -8 -#define TB_ERR_OUT_OF_BOUNDS -9 -#define TB_ERR_READ -10 -#define TB_ERR_RESIZE_IOCTL -11 -#define TB_ERR_RESIZE_PIPE -12 -#define TB_ERR_RESIZE_SIGACTION -13 -#define TB_ERR_POLL -14 -#define TB_ERR_TCGETATTR -15 -#define TB_ERR_TCSETATTR -16 -#define TB_ERR_UNSUPPORTED_TERM -17 -#define TB_ERR_RESIZE_WRITE -18 -#define TB_ERR_RESIZE_POLL -19 -#define TB_ERR_RESIZE_READ -20 -#define TB_ERR_RESIZE_SSCANF -21 -#define TB_ERR_CAP_COLLISION -22 - -#define TB_ERR_SELECT TB_ERR_POLL -#define TB_ERR_RESIZE_SELECT TB_ERR_RESIZE_POLL - -/* Deprecated. Function types to be used with tb_set_func(). */ -#define TB_FUNC_EXTRACT_PRE 0 -#define TB_FUNC_EXTRACT_POST 1 - -/* Define this to set the size of the buffer used in tb_printf() - * and tb_sendf() - */ -#ifndef TB_OPT_PRINTF_BUF -#define TB_OPT_PRINTF_BUF 4096 -#endif - -/* Define this to set the size of the read buffer used when reading - * from the tty - */ -#ifndef TB_OPT_READ_BUF -#define TB_OPT_READ_BUF 64 -#endif - -/* Define this for limited back compat with termbox v1 */ -#ifdef TB_OPT_V1_COMPAT -#define tb_change_cell tb_set_cell -#define tb_put_cell(x, y, c) tb_set_cell((x), (y), (c)->ch, (c)->fg, (c)->bg) -#define tb_set_clear_attributes tb_set_clear_attrs -#define tb_select_input_mode tb_set_input_mode -#define tb_select_output_mode tb_set_output_mode -#endif - -/* Define these to swap in a different allocator */ -#ifndef tb_malloc -#define tb_malloc malloc -#define tb_realloc realloc -#define tb_free free -#endif - -#if TB_OPT_ATTR_W == 64 -typedef uint64_t uintattr_t; -#elif TB_OPT_ATTR_W == 32 -typedef uint32_t uintattr_t; -#else // 16 -typedef uint16_t uintattr_t; -#endif - -/* The terminal screen is represented as 2d array of cells. The structure is - * optimized for dealing with single-width (wcwidth()==1) Unicode codepoints, - * however some support for grapheme clusters (e.g., combining diacritical - * marks) and wide codepoints (e.g., Hiragana) is provided through ech, nech, - * cech via tb_set_cell_ex(). ech is only valid when nech>0, otherwise ch is - * used. - * - * For non-single-width codepoints, given N=wcwidth(ch)/wcswidth(ech): - * - * when N==0: termbox forces a single-width cell. Callers should avoid this - * if aiming to render text accurately. - * - * when N>1: termbox zeroes out the following N-1 cells and skips sending - * them to the tty. So, e.g., if the caller sets x=0,y=0 to an N==2 - * codepoint, the caller's next set should be at x=2,y=0. Anything - * set at x=1,y=0 will be ignored. If there are not enough columns - * remaining on the line to render N width, spaces are sent - * instead. - * - * See tb_present() for implementation. - */ -struct tb_cell { - uint32_t ch; /* a Unicode codepoint */ - uintattr_t fg; /* bitwise foreground attributes */ - uintattr_t bg; /* bitwise background attributes */ -#ifdef TB_OPT_EGC - uint32_t *ech; /* a grapheme cluster of Unicode codepoints, 0-terminated */ - size_t nech; /* num elements in ech, 0 means use ch instead of ech */ - size_t cech; /* num elements allocated for ech */ -#endif -}; - -/* An incoming event from the tty. - * - * Given the event type, the following fields are relevant: - * - * when TB_EVENT_KEY: (key XOR ch, one will be zero), mod. Note there is - * overlap between TB_MOD_CTRL and TB_KEY_CTRL_*. - * TB_MOD_CTRL and TB_MOD_SHIFT are only set as - * modifiers to TB_KEY_ARROW_*. - * - * when TB_EVENT_RESIZE: w, h - * - * when TB_EVENT_MOUSE: key (TB_KEY_MOUSE_*), x, y - */ -struct tb_event { - uint8_t type; /* one of TB_EVENT_* constants */ - uint8_t mod; /* bitwise TB_MOD_* constants */ - uint16_t key; /* one of TB_KEY_* constants */ - uint32_t ch; /* a Unicode codepoint */ - int32_t w; /* resize width */ - int32_t h; /* resize height */ - int32_t x; /* mouse x */ - int32_t y; /* mouse y */ -}; - -/* Initializes the termbox library. This function should be called before any - * other functions. tb_init() is equivalent to tb_init_file("/dev/tty"). After - * successful initialization, the library must be finalized using the - * tb_shutdown() function. - */ -int tb_init(void); -int tb_init_file(const char *path); -int tb_init_fd(int ttyfd); -int tb_init_rwfd(int rfd, int wfd); -int tb_shutdown(void); - -/* Returns the size of the internal back buffer (which is the same as terminal's - * window size in rows and columns). The internal buffer can be resized after - * tb_clear() or tb_present() function calls. Both dimensions have an - * unspecified negative value when called before tb_init() or after - * tb_shutdown(). - */ -int tb_width(void); -int tb_height(void); - -/* Clears the internal back buffer using TB_DEFAULT color or the - * color/attributes set by tb_set_clear_attrs() function. - */ -int tb_clear(void); -int tb_set_clear_attrs(uintattr_t fg, uintattr_t bg); - -/* Synchronizes the internal back buffer with the terminal by writing to tty. */ -int tb_present(void); - -/* Clears the internal front buffer effectively forcing a complete re-render of - * the back buffer to the tty. It is not necessary to call this under normal - * circumstances. */ -int tb_invalidate(void); - -/* Sets the position of the cursor. Upper-left character is (0, 0). */ -int tb_set_cursor(int cx, int cy); -int tb_hide_cursor(void); - -/* Set cell contents in the internal back buffer at the specified position. - * - * Use tb_set_cell_ex() for rendering grapheme clusters (e.g., combining - * diacritical marks). - * - * Function tb_set_cell(x, y, ch, fg, bg) is equivalent to - * tb_set_cell_ex(x, y, &ch, 1, fg, bg). - * - * Function tb_extend_cell() is a shortcut for appending 1 codepoint to - * cell->ech. - */ -int tb_set_cell(int x, int y, uint32_t ch, uintattr_t fg, uintattr_t bg); -int tb_set_cell_ex(int x, int y, uint32_t *ch, size_t nch, uintattr_t fg, - uintattr_t bg); -int tb_extend_cell(int x, int y, uint32_t ch); - -/* Sets the input mode. Termbox has two input modes: - * - * 1. TB_INPUT_ESC - * When escape (\x1b) is in the buffer and there's no match for an escape - * sequence, a key event for TB_KEY_ESC is returned. - * - * 2. TB_INPUT_ALT - * When escape (\x1b) is in the buffer and there's no match for an escape - * sequence, the next keyboard event is returned with a TB_MOD_ALT modifier. - * - * You can also apply TB_INPUT_MOUSE via bitwise OR operation to either of the - * modes (e.g., TB_INPUT_ESC | TB_INPUT_MOUSE) to receive TB_EVENT_MOUSE events. - * If none of the main two modes were set, but the mouse mode was, TB_INPUT_ESC - * mode is used. If for some reason you've decided to use - * (TB_INPUT_ESC | TB_INPUT_ALT) combination, it will behave as if only - * TB_INPUT_ESC was selected. - * - * If mode is TB_INPUT_CURRENT, the function returns the current input mode. - * - * The default input mode is TB_INPUT_ESC. - */ -int tb_set_input_mode(int mode); - -/* Sets the termbox output mode. Termbox has multiple output modes: - * - * 1. TB_OUTPUT_NORMAL => [0..8] - * - * This mode provides 8 different colors: - * TB_BLACK, TB_RED, TB_GREEN, TB_YELLOW, - * TB_BLUE, TB_MAGENTA, TB_CYAN, TB_WHITE - * - * Plus TB_DEFAULT which skips sending a color code (i.e., uses the - * terminal's default color). - * - * Colors (including TB_DEFAULT) may be bitwise OR'd with attributes: - * TB_BOLD, TB_UNDERLINE, TB_REVERSE, TB_ITALIC, TB_BLINK, TB_BRIGHT, - * TB_DIM - * - * The following style attributes are also available if compiled with - * TB_OPT_ATTR_W set to 64: - * TB_STRIKEOUT, TB_UNDERLINE_2, TB_OVERLINE, TB_INVISIBLE - * - * As in all modes, the value 0 is interpreted as TB_DEFAULT for - * convenience. - * - * Some notes: TB_REVERSE can be applied as either fg or bg attributes for - * the same effect. TB_BRIGHT can be applied to either fg or bg. The rest of - * the attributes apply to fg only and are ignored as bg attributes. - * - * Example usage: - * tb_set_cell(x, y, '@', TB_BLACK | TB_BOLD, TB_RED); - * - * 2. TB_OUTPUT_256 => [0..255] + TB_HI_BLACK - * - * In this mode you get 256 distinct colors (plus default): - * 0x00 (1): TB_DEFAULT - * TB_HI_BLACK (1): TB_BLACK in TB_OUTPUT_NORMAL - * 0x01..0x07 (7): the next 7 colors as in TB_OUTPUT_NORMAL - * 0x08..0x0f (8): bright versions of the above - * 0x10..0xe7 (216): 216 different colors - * 0xe8..0xff (24): 24 different shades of gray - * - * All TB_* style attributes except TB_BRIGHT may be bitwise OR'd as in - * TB_OUTPUT_NORMAL. - * - * Note TB_HI_BLACK must be used for black, as 0x00 represents default. - * - * 3. TB_OUTPUT_216 => [0..216] - * - * This mode supports the 216-color range of TB_OUTPUT_256 only, but you - * don't need to provide an offset: - * 0x00 (1): TB_DEFAULT - * 0x01..0xd8 (216): 216 different colors - * - * 4. TB_OUTPUT_GRAYSCALE => [0..24] - * - * This mode supports the 24-color range of TB_OUTPUT_256 only, but you - * don't need to provide an offset: - * 0x00 (1): TB_DEFAULT - * 0x01..0x18 (24): 24 different shades of gray - * - * 5. TB_OUTPUT_TRUECOLOR => [0x000000..0xffffff] + TB_HI_BLACK - * - * This mode provides 24-bit color on supported terminals. The format is - * 0xRRGGBB. - * - * All TB_* style attributes except TB_BRIGHT may be bitwise OR'd as in - * TB_OUTPUT_NORMAL. - * - * Note TB_HI_BLACK must be used for black, as 0x000000 represents default. - * - * If mode is TB_OUTPUT_CURRENT, the function returns the current output mode. - * - * The default output mode is TB_OUTPUT_NORMAL. - * - * To use the terminal default color (i.e., to not send an escape code), pass - * TB_DEFAULT. For convenience, the value 0 is interpreted as TB_DEFAULT in - * all modes. - * - * Note, cell attributes persist after switching output modes. Any translation - * between, for example, TB_OUTPUT_NORMAL's TB_RED and TB_OUTPUT_TRUECOLOR's - * 0xff0000 must be performed by the caller. Also note that cells previously - * rendered in one mode may persist unchanged until the front buffer is cleared - * (such as after a resize event) at which point it will be re-interpreted and - * flushed according to the current mode. Callers may invoke tb_invalidate if - * it is desirable to immediately re-interpret and flush the entire screen - * according to the current mode. - * - * Note, not all terminals support all output modes, especially beyond - * TB_OUTPUT_NORMAL. There is also no very reliable way to determine color - * support dynamically. If portability is desired, callers are recommended to - * use TB_OUTPUT_NORMAL or make output mode end-user configurable. The same - * advice applies to style attributes. - */ -int tb_set_output_mode(int mode); - -/* Wait for an event up to timeout_ms milliseconds and fill the event structure - * with it. If no event is available within the timeout period, TB_ERR_NO_EVENT - * is returned. On a resize event, the underlying select(2) call may be - * interrupted, yielding a return code of TB_ERR_POLL. In this case, you may - * check errno via tb_last_errno(). If it's EINTR, you can safely ignore that - * and call tb_peek_event() again. - */ -int tb_peek_event(struct tb_event *event, int timeout_ms); - -/* Same as tb_peek_event except no timeout. */ -int tb_poll_event(struct tb_event *event); - -/* Internal termbox FDs that can be used with poll() / select(). Must call - * tb_poll_event() / tb_peek_event() if activity is detected. */ -int tb_get_fds(int *ttyfd, int *resizefd); - -/* Print and printf functions. Specify param out_w to determine width of printed - * string. Incomplete trailing UTF-8 byte sequences are replaced with U+FFFD. - * For finer control, use tb_set_cell(). - */ -int tb_print(int x, int y, uintattr_t fg, uintattr_t bg, const char *str); -int tb_printf(int x, int y, uintattr_t fg, uintattr_t bg, const char *fmt, ...); -int tb_print_ex(int x, int y, uintattr_t fg, uintattr_t bg, size_t *out_w, - const char *str); -int tb_printf_ex(int x, int y, uintattr_t fg, uintattr_t bg, size_t *out_w, - const char *fmt, ...); - -/* Send raw bytes to terminal. */ -int tb_send(const char *buf, size_t nbuf); -int tb_sendf(const char *fmt, ...); - -/* Deprecated. Set custom functions. fn_type is one of TB_FUNC_* constants, fn - * is a compatible function pointer, or NULL to clear. - * - * TB_FUNC_EXTRACT_PRE: - * If specified, invoke this function BEFORE termbox tries to extract any - * escape sequences from the input buffer. - * - * TB_FUNC_EXTRACT_POST: - * If specified, invoke this function AFTER termbox tries (and fails) to - * extract any escape sequences from the input buffer. - */ -int tb_set_func(int fn_type, int (*fn)(struct tb_event *, size_t *)); - -/* Return byte length of codepoint given first byte of UTF-8 sequence (1-6). */ -int tb_utf8_char_length(char c); - -/* Convert UTF-8 null-terminated byte sequence to UTF-32 codepoint. - * - * If `c` is an empty C string, return 0. `out` is left unchanged. - * - * If a null byte is encountered in the middle of the codepoint, return a - * negative number indicating how many bytes were processed. `out` is left - * unchanged. - * - * Otherwise, return byte length of codepoint (1-6). - */ -int tb_utf8_char_to_unicode(uint32_t *out, const char *c); - -/* Convert UTF-32 codepoint to UTF-8 null-terminated byte sequence. - * - * `out` must be char[7] or greater. Return byte length of codepoint (1-6). - */ -int tb_utf8_unicode_to_char(char *out, uint32_t c); - -/* Library utility functions */ -int tb_last_errno(void); -const char *tb_strerror(int err); -struct tb_cell *tb_cell_buffer(void); // Deprecated -int tb_has_truecolor(void); -int tb_has_egc(void); -int tb_attr_width(void); -const char *tb_version(void); - -/* Deprecation notice! - * - * The following will be removed in version 3.x (ABI version 3): - * - * TB_256_BLACK (use TB_HI_BLACK) - * TB_OPT_TRUECOLOR (use TB_OPT_ATTR_W) - * TB_TRUECOLOR_BOLD (use TB_BOLD) - * TB_TRUECOLOR_UNDERLINE (use TB_UNDERLINE) - * TB_TRUECOLOR_REVERSE (use TB_REVERSE) - * TB_TRUECOLOR_ITALIC (use TB_ITALICe) - * TB_TRUECOLOR_BLINK (use TB_BLINK) - * TB_TRUECOLOR_BLACK (use TB_HI_BLACK) - * tb_cell_buffer - * tb_set_func - * TB_FUNC_EXTRACT_PRE - * TB_FUNC_EXTRACT_POST - */ - -#ifdef __cplusplus -} -#endif - -#endif /* TERMBOX_H_INCL */ - -#ifdef TB_IMPL - -#define if_err_return(rv, expr) \ - if (((rv) = (expr)) != TB_OK) return (rv) -#define if_err_break(rv, expr) \ - if (((rv) = (expr)) != TB_OK) break -#define if_ok_return(rv, expr) \ - if (((rv) = (expr)) == TB_OK) return (rv) -#define if_ok_or_need_more_return(rv, expr) \ - if (((rv) = (expr)) == TB_OK || (rv) == TB_ERR_NEED_MORE) return (rv) - -#define send_literal(rv, a) \ - if_err_return((rv), bytebuf_nputs(&global.out, (a), sizeof(a) - 1)) - -#define send_num(rv, nbuf, n) \ - if_err_return((rv), \ - bytebuf_nputs(&global.out, (nbuf), convert_num((n), (nbuf)))) - -#define snprintf_or_return(rv, str, sz, fmt, ...) \ - do { \ - (rv) = snprintf((str), (sz), (fmt), __VA_ARGS__); \ - if ((rv) < 0 || (rv) >= (int)(sz)) return TB_ERR; \ - } while (0) - -#define if_not_init_return() \ - if (!global.initialized) return TB_ERR_NOT_INIT - -struct bytebuf_t { - char *buf; - size_t len; - size_t cap; -}; - -struct cellbuf_t { - int width; - int height; - struct tb_cell *cells; -}; - -struct cap_trie_t { - char c; - struct cap_trie_t *children; - size_t nchildren; - int is_leaf; - uint16_t key; - uint8_t mod; -}; - -struct tb_global_t { - int ttyfd; - int rfd; - int wfd; - int ttyfd_open; - int resize_pipefd[2]; - int width; - int height; - int cursor_x; - int cursor_y; - int last_x; - int last_y; - uintattr_t fg; - uintattr_t bg; - uintattr_t last_fg; - uintattr_t last_bg; - int input_mode; - int output_mode; - char *terminfo; - size_t nterminfo; - const char *caps[TB_CAP__COUNT]; - struct cap_trie_t cap_trie; - struct bytebuf_t in; - struct bytebuf_t out; - struct cellbuf_t back; - struct cellbuf_t front; - struct termios orig_tios; - int has_orig_tios; - int last_errno; - int initialized; - int (*fn_extract_esc_pre)(struct tb_event *, size_t *); - int (*fn_extract_esc_post)(struct tb_event *, size_t *); - char errbuf[1024]; -}; - -static struct tb_global_t global = {0}; - -/* BEGIN codegen c */ -/* Produced by ./codegen.sh on Thu, 13 Jul 2023 05:46:13 +0000 */ - -static const int16_t terminfo_cap_indexes[] = { - 66, // kf1 (TB_CAP_F1) - 68, // kf2 (TB_CAP_F2) - 69, // kf3 (TB_CAP_F3) - 70, // kf4 (TB_CAP_F4) - 71, // kf5 (TB_CAP_F5) - 72, // kf6 (TB_CAP_F6) - 73, // kf7 (TB_CAP_F7) - 74, // kf8 (TB_CAP_F8) - 75, // kf9 (TB_CAP_F9) - 67, // kf10 (TB_CAP_F10) - 216, // kf11 (TB_CAP_F11) - 217, // kf12 (TB_CAP_F12) - 77, // kich1 (TB_CAP_INSERT) - 59, // kdch1 (TB_CAP_DELETE) - 76, // khome (TB_CAP_HOME) - 164, // kend (TB_CAP_END) - 82, // kpp (TB_CAP_PGUP) - 81, // knp (TB_CAP_PGDN) - 87, // kcuu1 (TB_CAP_ARROW_UP) - 61, // kcud1 (TB_CAP_ARROW_DOWN) - 79, // kcub1 (TB_CAP_ARROW_LEFT) - 83, // kcuf1 (TB_CAP_ARROW_RIGHT) - 148, // kcbt (TB_CAP_BACK_TAB) - 28, // smcup (TB_CAP_ENTER_CA) - 40, // rmcup (TB_CAP_EXIT_CA) - 16, // cnorm (TB_CAP_SHOW_CURSOR) - 13, // civis (TB_CAP_HIDE_CURSOR) - 5, // clear (TB_CAP_CLEAR_SCREEN) - 39, // sgr0 (TB_CAP_SGR0) - 36, // smul (TB_CAP_UNDERLINE) - 27, // bold (TB_CAP_BOLD) - 26, // blink (TB_CAP_BLINK) - 311, // sitm (TB_CAP_ITALIC) - 34, // rev (TB_CAP_REVERSE) - 89, // smkx (TB_CAP_ENTER_KEYPAD) - 88, // rmkx (TB_CAP_EXIT_KEYPAD) - 30, // dim (TB_CAP_DIM) - 32, // invis (TB_CAP_INVISIBLE) -}; - -// xterm -static const char *xterm_caps[] = { - "\033OP", // kf1 (TB_CAP_F1) - "\033OQ", // kf2 (TB_CAP_F2) - "\033OR", // kf3 (TB_CAP_F3) - "\033OS", // kf4 (TB_CAP_F4) - "\033[15~", // kf5 (TB_CAP_F5) - "\033[17~", // kf6 (TB_CAP_F6) - "\033[18~", // kf7 (TB_CAP_F7) - "\033[19~", // kf8 (TB_CAP_F8) - "\033[20~", // kf9 (TB_CAP_F9) - "\033[21~", // kf10 (TB_CAP_F10) - "\033[23~", // kf11 (TB_CAP_F11) - "\033[24~", // kf12 (TB_CAP_F12) - "\033[2~", // kich1 (TB_CAP_INSERT) - "\033[3~", // kdch1 (TB_CAP_DELETE) - "\033OH", // khome (TB_CAP_HOME) - "\033OF", // kend (TB_CAP_END) - "\033[5~", // kpp (TB_CAP_PGUP) - "\033[6~", // knp (TB_CAP_PGDN) - "\033OA", // kcuu1 (TB_CAP_ARROW_UP) - "\033OB", // kcud1 (TB_CAP_ARROW_DOWN) - "\033OD", // kcub1 (TB_CAP_ARROW_LEFT) - "\033OC", // kcuf1 (TB_CAP_ARROW_RIGHT) - "\033[Z", // kcbt (TB_CAP_BACK_TAB) - "\033[?1049h\033[22;0;0t", // smcup (TB_CAP_ENTER_CA) - "\033[?1049l\033[23;0;0t", // rmcup (TB_CAP_EXIT_CA) - "\033[?12l\033[?25h", // cnorm (TB_CAP_SHOW_CURSOR) - "\033[?25l", // civis (TB_CAP_HIDE_CURSOR) - "\033[H\033[2J", // clear (TB_CAP_CLEAR_SCREEN) - "\033(B\033[m", // sgr0 (TB_CAP_SGR0) - "\033[4m", // smul (TB_CAP_UNDERLINE) - "\033[1m", // bold (TB_CAP_BOLD) - "\033[5m", // blink (TB_CAP_BLINK) - "\033[3m", // sitm (TB_CAP_ITALIC) - "\033[7m", // rev (TB_CAP_REVERSE) - "\033[?1h\033=", // smkx (TB_CAP_ENTER_KEYPAD) - "\033[?1l\033>", // rmkx (TB_CAP_EXIT_KEYPAD) - "\033[2m", // dim (TB_CAP_DIM) - "\033[8m", // invis (TB_CAP_INVISIBLE) -}; - -// linux -static const char *linux_caps[] = { - "\033[[A", // kf1 (TB_CAP_F1) - "\033[[B", // kf2 (TB_CAP_F2) - "\033[[C", // kf3 (TB_CAP_F3) - "\033[[D", // kf4 (TB_CAP_F4) - "\033[[E", // kf5 (TB_CAP_F5) - "\033[17~", // kf6 (TB_CAP_F6) - "\033[18~", // kf7 (TB_CAP_F7) - "\033[19~", // kf8 (TB_CAP_F8) - "\033[20~", // kf9 (TB_CAP_F9) - "\033[21~", // kf10 (TB_CAP_F10) - "\033[23~", // kf11 (TB_CAP_F11) - "\033[24~", // kf12 (TB_CAP_F12) - "\033[2~", // kich1 (TB_CAP_INSERT) - "\033[3~", // kdch1 (TB_CAP_DELETE) - "\033[1~", // khome (TB_CAP_HOME) - "\033[4~", // kend (TB_CAP_END) - "\033[5~", // kpp (TB_CAP_PGUP) - "\033[6~", // knp (TB_CAP_PGDN) - "\033[A", // kcuu1 (TB_CAP_ARROW_UP) - "\033[B", // kcud1 (TB_CAP_ARROW_DOWN) - "\033[D", // kcub1 (TB_CAP_ARROW_LEFT) - "\033[C", // kcuf1 (TB_CAP_ARROW_RIGHT) - "\033\011", // kcbt (TB_CAP_BACK_TAB) - "", // smcup (TB_CAP_ENTER_CA) - "", // rmcup (TB_CAP_EXIT_CA) - "\033[?25h\033[?0c", // cnorm (TB_CAP_SHOW_CURSOR) - "\033[?25l\033[?1c", // civis (TB_CAP_HIDE_CURSOR) - "\033[H\033[J", // clear (TB_CAP_CLEAR_SCREEN) - "\033[m\017", // sgr0 (TB_CAP_SGR0) - "\033[4m", // smul (TB_CAP_UNDERLINE) - "\033[1m", // bold (TB_CAP_BOLD) - "\033[5m", // blink (TB_CAP_BLINK) - "", // sitm (TB_CAP_ITALIC) - "\033[7m", // rev (TB_CAP_REVERSE) - "", // smkx (TB_CAP_ENTER_KEYPAD) - "", // rmkx (TB_CAP_EXIT_KEYPAD) - "\033[2m", // dim (TB_CAP_DIM) - "", // invis (TB_CAP_INVISIBLE) -}; - -// screen -static const char *screen_caps[] = { - "\033OP", // kf1 (TB_CAP_F1) - "\033OQ", // kf2 (TB_CAP_F2) - "\033OR", // kf3 (TB_CAP_F3) - "\033OS", // kf4 (TB_CAP_F4) - "\033[15~", // kf5 (TB_CAP_F5) - "\033[17~", // kf6 (TB_CAP_F6) - "\033[18~", // kf7 (TB_CAP_F7) - "\033[19~", // kf8 (TB_CAP_F8) - "\033[20~", // kf9 (TB_CAP_F9) - "\033[21~", // kf10 (TB_CAP_F10) - "\033[23~", // kf11 (TB_CAP_F11) - "\033[24~", // kf12 (TB_CAP_F12) - "\033[2~", // kich1 (TB_CAP_INSERT) - "\033[3~", // kdch1 (TB_CAP_DELETE) - "\033[1~", // khome (TB_CAP_HOME) - "\033[4~", // kend (TB_CAP_END) - "\033[5~", // kpp (TB_CAP_PGUP) - "\033[6~", // knp (TB_CAP_PGDN) - "\033OA", // kcuu1 (TB_CAP_ARROW_UP) - "\033OB", // kcud1 (TB_CAP_ARROW_DOWN) - "\033OD", // kcub1 (TB_CAP_ARROW_LEFT) - "\033OC", // kcuf1 (TB_CAP_ARROW_RIGHT) - "\033[Z", // kcbt (TB_CAP_BACK_TAB) - "\033[?1049h", // smcup (TB_CAP_ENTER_CA) - "\033[?1049l", // rmcup (TB_CAP_EXIT_CA) - "\033[34h\033[?25h", // cnorm (TB_CAP_SHOW_CURSOR) - "\033[?25l", // civis (TB_CAP_HIDE_CURSOR) - "\033[H\033[J", // clear (TB_CAP_CLEAR_SCREEN) - "\033[m\017", // sgr0 (TB_CAP_SGR0) - "\033[4m", // smul (TB_CAP_UNDERLINE) - "\033[1m", // bold (TB_CAP_BOLD) - "\033[5m", // blink (TB_CAP_BLINK) - "", // sitm (TB_CAP_ITALIC) - "\033[7m", // rev (TB_CAP_REVERSE) - "\033[?1h\033=", // smkx (TB_CAP_ENTER_KEYPAD) - "\033[?1l\033>", // rmkx (TB_CAP_EXIT_KEYPAD) - "\033[2m", // dim (TB_CAP_DIM) - "", // invis (TB_CAP_INVISIBLE) -}; - -// rxvt-256color -static const char *rxvt_256color_caps[] = { - "\033[11~", // kf1 (TB_CAP_F1) - "\033[12~", // kf2 (TB_CAP_F2) - "\033[13~", // kf3 (TB_CAP_F3) - "\033[14~", // kf4 (TB_CAP_F4) - "\033[15~", // kf5 (TB_CAP_F5) - "\033[17~", // kf6 (TB_CAP_F6) - "\033[18~", // kf7 (TB_CAP_F7) - "\033[19~", // kf8 (TB_CAP_F8) - "\033[20~", // kf9 (TB_CAP_F9) - "\033[21~", // kf10 (TB_CAP_F10) - "\033[23~", // kf11 (TB_CAP_F11) - "\033[24~", // kf12 (TB_CAP_F12) - "\033[2~", // kich1 (TB_CAP_INSERT) - "\033[3~", // kdch1 (TB_CAP_DELETE) - "\033[7~", // khome (TB_CAP_HOME) - "\033[8~", // kend (TB_CAP_END) - "\033[5~", // kpp (TB_CAP_PGUP) - "\033[6~", // knp (TB_CAP_PGDN) - "\033[A", // kcuu1 (TB_CAP_ARROW_UP) - "\033[B", // kcud1 (TB_CAP_ARROW_DOWN) - "\033[D", // kcub1 (TB_CAP_ARROW_LEFT) - "\033[C", // kcuf1 (TB_CAP_ARROW_RIGHT) - "\033[Z", // kcbt (TB_CAP_BACK_TAB) - "\0337\033[?47h", // smcup (TB_CAP_ENTER_CA) - "\033[2J\033[?47l\0338", // rmcup (TB_CAP_EXIT_CA) - "\033[?25h", // cnorm (TB_CAP_SHOW_CURSOR) - "\033[?25l", // civis (TB_CAP_HIDE_CURSOR) - "\033[H\033[2J", // clear (TB_CAP_CLEAR_SCREEN) - "\033[m\017", // sgr0 (TB_CAP_SGR0) - "\033[4m", // smul (TB_CAP_UNDERLINE) - "\033[1m", // bold (TB_CAP_BOLD) - "\033[5m", // blink (TB_CAP_BLINK) - "", // sitm (TB_CAP_ITALIC) - "\033[7m", // rev (TB_CAP_REVERSE) - "\033=", // smkx (TB_CAP_ENTER_KEYPAD) - "\033>", // rmkx (TB_CAP_EXIT_KEYPAD) - "", // dim (TB_CAP_DIM) - "", // invis (TB_CAP_INVISIBLE) -}; - -// rxvt-unicode -static const char *rxvt_unicode_caps[] = { - "\033[11~", // kf1 (TB_CAP_F1) - "\033[12~", // kf2 (TB_CAP_F2) - "\033[13~", // kf3 (TB_CAP_F3) - "\033[14~", // kf4 (TB_CAP_F4) - "\033[15~", // kf5 (TB_CAP_F5) - "\033[17~", // kf6 (TB_CAP_F6) - "\033[18~", // kf7 (TB_CAP_F7) - "\033[19~", // kf8 (TB_CAP_F8) - "\033[20~", // kf9 (TB_CAP_F9) - "\033[21~", // kf10 (TB_CAP_F10) - "\033[23~", // kf11 (TB_CAP_F11) - "\033[24~", // kf12 (TB_CAP_F12) - "\033[2~", // kich1 (TB_CAP_INSERT) - "\033[3~", // kdch1 (TB_CAP_DELETE) - "\033[7~", // khome (TB_CAP_HOME) - "\033[8~", // kend (TB_CAP_END) - "\033[5~", // kpp (TB_CAP_PGUP) - "\033[6~", // knp (TB_CAP_PGDN) - "\033[A", // kcuu1 (TB_CAP_ARROW_UP) - "\033[B", // kcud1 (TB_CAP_ARROW_DOWN) - "\033[D", // kcub1 (TB_CAP_ARROW_LEFT) - "\033[C", // kcuf1 (TB_CAP_ARROW_RIGHT) - "\033[Z", // kcbt (TB_CAP_BACK_TAB) - "\033[?1049h", // smcup (TB_CAP_ENTER_CA) - "\033[r\033[?1049l", // rmcup (TB_CAP_EXIT_CA) - "\033[?12l\033[?25h", // cnorm (TB_CAP_SHOW_CURSOR) - "\033[?25l", // civis (TB_CAP_HIDE_CURSOR) - "\033[H\033[2J", // clear (TB_CAP_CLEAR_SCREEN) - "\033[m\033(B", // sgr0 (TB_CAP_SGR0) - "\033[4m", // smul (TB_CAP_UNDERLINE) - "\033[1m", // bold (TB_CAP_BOLD) - "\033[5m", // blink (TB_CAP_BLINK) - "\033[3m", // sitm (TB_CAP_ITALIC) - "\033[7m", // rev (TB_CAP_REVERSE) - "\033=", // smkx (TB_CAP_ENTER_KEYPAD) - "\033>", // rmkx (TB_CAP_EXIT_KEYPAD) - "", // dim (TB_CAP_DIM) - "", // invis (TB_CAP_INVISIBLE) -}; - -// Eterm -static const char *eterm_caps[] = { - "\033[11~", // kf1 (TB_CAP_F1) - "\033[12~", // kf2 (TB_CAP_F2) - "\033[13~", // kf3 (TB_CAP_F3) - "\033[14~", // kf4 (TB_CAP_F4) - "\033[15~", // kf5 (TB_CAP_F5) - "\033[17~", // kf6 (TB_CAP_F6) - "\033[18~", // kf7 (TB_CAP_F7) - "\033[19~", // kf8 (TB_CAP_F8) - "\033[20~", // kf9 (TB_CAP_F9) - "\033[21~", // kf10 (TB_CAP_F10) - "\033[23~", // kf11 (TB_CAP_F11) - "\033[24~", // kf12 (TB_CAP_F12) - "\033[2~", // kich1 (TB_CAP_INSERT) - "\033[3~", // kdch1 (TB_CAP_DELETE) - "\033[7~", // khome (TB_CAP_HOME) - "\033[8~", // kend (TB_CAP_END) - "\033[5~", // kpp (TB_CAP_PGUP) - "\033[6~", // knp (TB_CAP_PGDN) - "\033[A", // kcuu1 (TB_CAP_ARROW_UP) - "\033[B", // kcud1 (TB_CAP_ARROW_DOWN) - "\033[D", // kcub1 (TB_CAP_ARROW_LEFT) - "\033[C", // kcuf1 (TB_CAP_ARROW_RIGHT) - "", // kcbt (TB_CAP_BACK_TAB) - "\0337\033[?47h", // smcup (TB_CAP_ENTER_CA) - "\033[2J\033[?47l\0338", // rmcup (TB_CAP_EXIT_CA) - "\033[?25h", // cnorm (TB_CAP_SHOW_CURSOR) - "\033[?25l", // civis (TB_CAP_HIDE_CURSOR) - "\033[H\033[2J", // clear (TB_CAP_CLEAR_SCREEN) - "\033[m\017", // sgr0 (TB_CAP_SGR0) - "\033[4m", // smul (TB_CAP_UNDERLINE) - "\033[1m", // bold (TB_CAP_BOLD) - "\033[5m", // blink (TB_CAP_BLINK) - "", // sitm (TB_CAP_ITALIC) - "\033[7m", // rev (TB_CAP_REVERSE) - "", // smkx (TB_CAP_ENTER_KEYPAD) - "", // rmkx (TB_CAP_EXIT_KEYPAD) - "", // dim (TB_CAP_DIM) - "", // invis (TB_CAP_INVISIBLE) -}; - -static struct { - const char *name; - const char **caps; - const char *alias; -} builtin_terms[] = { - {"xterm", xterm_caps, "" }, - {"linux", linux_caps, "" }, - {"screen", screen_caps, "tmux"}, - {"rxvt-256color", rxvt_256color_caps, "" }, - {"rxvt-unicode", rxvt_unicode_caps, "rxvt"}, - {"Eterm", eterm_caps, "" }, - {NULL, NULL, NULL }, -}; - -/* END codegen c */ - -static struct { - const char *cap; - const uint16_t key; - const uint8_t mod; -} builtin_mod_caps[] = { - // xterm arrows - {"\x1b[1;2A", TB_KEY_ARROW_UP, TB_MOD_SHIFT }, - {"\x1b[1;3A", TB_KEY_ARROW_UP, TB_MOD_ALT }, - {"\x1b[1;4A", TB_KEY_ARROW_UP, TB_MOD_ALT | TB_MOD_SHIFT }, - {"\x1b[1;5A", TB_KEY_ARROW_UP, TB_MOD_CTRL }, - {"\x1b[1;6A", TB_KEY_ARROW_UP, TB_MOD_CTRL | TB_MOD_SHIFT }, - {"\x1b[1;7A", TB_KEY_ARROW_UP, TB_MOD_CTRL | TB_MOD_ALT }, - {"\x1b[1;8A", TB_KEY_ARROW_UP, TB_MOD_CTRL | TB_MOD_ALT | TB_MOD_SHIFT}, - - {"\x1b[1;2B", TB_KEY_ARROW_DOWN, TB_MOD_SHIFT }, - {"\x1b[1;3B", TB_KEY_ARROW_DOWN, TB_MOD_ALT }, - {"\x1b[1;4B", TB_KEY_ARROW_DOWN, TB_MOD_ALT | TB_MOD_SHIFT }, - {"\x1b[1;5B", TB_KEY_ARROW_DOWN, TB_MOD_CTRL }, - {"\x1b[1;6B", TB_KEY_ARROW_DOWN, TB_MOD_CTRL | TB_MOD_SHIFT }, - {"\x1b[1;7B", TB_KEY_ARROW_DOWN, TB_MOD_CTRL | TB_MOD_ALT }, - {"\x1b[1;8B", TB_KEY_ARROW_DOWN, TB_MOD_CTRL | TB_MOD_ALT | TB_MOD_SHIFT}, - - {"\x1b[1;2C", TB_KEY_ARROW_RIGHT, TB_MOD_SHIFT }, - {"\x1b[1;3C", TB_KEY_ARROW_RIGHT, TB_MOD_ALT }, - {"\x1b[1;4C", TB_KEY_ARROW_RIGHT, TB_MOD_ALT | TB_MOD_SHIFT }, - {"\x1b[1;5C", TB_KEY_ARROW_RIGHT, TB_MOD_CTRL }, - {"\x1b[1;6C", TB_KEY_ARROW_RIGHT, TB_MOD_CTRL | TB_MOD_SHIFT }, - {"\x1b[1;7C", TB_KEY_ARROW_RIGHT, TB_MOD_CTRL | TB_MOD_ALT }, - {"\x1b[1;8C", TB_KEY_ARROW_RIGHT, TB_MOD_CTRL | TB_MOD_ALT | TB_MOD_SHIFT}, - - {"\x1b[1;2D", TB_KEY_ARROW_LEFT, TB_MOD_SHIFT }, - {"\x1b[1;3D", TB_KEY_ARROW_LEFT, TB_MOD_ALT }, - {"\x1b[1;4D", TB_KEY_ARROW_LEFT, TB_MOD_ALT | TB_MOD_SHIFT }, - {"\x1b[1;5D", TB_KEY_ARROW_LEFT, TB_MOD_CTRL }, - {"\x1b[1;6D", TB_KEY_ARROW_LEFT, TB_MOD_CTRL | TB_MOD_SHIFT }, - {"\x1b[1;7D", TB_KEY_ARROW_LEFT, TB_MOD_CTRL | TB_MOD_ALT }, - {"\x1b[1;8D", TB_KEY_ARROW_LEFT, TB_MOD_CTRL | TB_MOD_ALT | TB_MOD_SHIFT}, - - // xterm keys - {"\x1b[1;2H", TB_KEY_HOME, TB_MOD_SHIFT }, - {"\x1b[1;3H", TB_KEY_HOME, TB_MOD_ALT }, - {"\x1b[1;4H", TB_KEY_HOME, TB_MOD_ALT | TB_MOD_SHIFT }, - {"\x1b[1;5H", TB_KEY_HOME, TB_MOD_CTRL }, - {"\x1b[1;6H", TB_KEY_HOME, TB_MOD_CTRL | TB_MOD_SHIFT }, - {"\x1b[1;7H", TB_KEY_HOME, TB_MOD_CTRL | TB_MOD_ALT }, - {"\x1b[1;8H", TB_KEY_HOME, TB_MOD_CTRL | TB_MOD_ALT | TB_MOD_SHIFT}, - - {"\x1b[1;2F", TB_KEY_END, TB_MOD_SHIFT }, - {"\x1b[1;3F", TB_KEY_END, TB_MOD_ALT }, - {"\x1b[1;4F", TB_KEY_END, TB_MOD_ALT | TB_MOD_SHIFT }, - {"\x1b[1;5F", TB_KEY_END, TB_MOD_CTRL }, - {"\x1b[1;6F", TB_KEY_END, TB_MOD_CTRL | TB_MOD_SHIFT }, - {"\x1b[1;7F", TB_KEY_END, TB_MOD_CTRL | TB_MOD_ALT }, - {"\x1b[1;8F", TB_KEY_END, TB_MOD_CTRL | TB_MOD_ALT | TB_MOD_SHIFT}, - - {"\x1b[2;2~", TB_KEY_INSERT, TB_MOD_SHIFT }, - {"\x1b[2;3~", TB_KEY_INSERT, TB_MOD_ALT }, - {"\x1b[2;4~", TB_KEY_INSERT, TB_MOD_ALT | TB_MOD_SHIFT }, - {"\x1b[2;5~", TB_KEY_INSERT, TB_MOD_CTRL }, - {"\x1b[2;6~", TB_KEY_INSERT, TB_MOD_CTRL | TB_MOD_SHIFT }, - {"\x1b[2;7~", TB_KEY_INSERT, TB_MOD_CTRL | TB_MOD_ALT }, - {"\x1b[2;8~", TB_KEY_INSERT, TB_MOD_CTRL | TB_MOD_ALT | TB_MOD_SHIFT}, - - {"\x1b[3;2~", TB_KEY_DELETE, TB_MOD_SHIFT }, - {"\x1b[3;3~", TB_KEY_DELETE, TB_MOD_ALT }, - {"\x1b[3;4~", TB_KEY_DELETE, TB_MOD_ALT | TB_MOD_SHIFT }, - {"\x1b[3;5~", TB_KEY_DELETE, TB_MOD_CTRL }, - {"\x1b[3;6~", TB_KEY_DELETE, TB_MOD_CTRL | TB_MOD_SHIFT }, - {"\x1b[3;7~", TB_KEY_DELETE, TB_MOD_CTRL | TB_MOD_ALT }, - {"\x1b[3;8~", TB_KEY_DELETE, TB_MOD_CTRL | TB_MOD_ALT | TB_MOD_SHIFT}, - - {"\x1b[5;2~", TB_KEY_PGUP, TB_MOD_SHIFT }, - {"\x1b[5;3~", TB_KEY_PGUP, TB_MOD_ALT }, - {"\x1b[5;4~", TB_KEY_PGUP, TB_MOD_ALT | TB_MOD_SHIFT }, - {"\x1b[5;5~", TB_KEY_PGUP, TB_MOD_CTRL }, - {"\x1b[5;6~", TB_KEY_PGUP, TB_MOD_CTRL | TB_MOD_SHIFT }, - {"\x1b[5;7~", TB_KEY_PGUP, TB_MOD_CTRL | TB_MOD_ALT }, - {"\x1b[5;8~", TB_KEY_PGUP, TB_MOD_CTRL | TB_MOD_ALT | TB_MOD_SHIFT}, - - {"\x1b[6;2~", TB_KEY_PGDN, TB_MOD_SHIFT }, - {"\x1b[6;3~", TB_KEY_PGDN, TB_MOD_ALT }, - {"\x1b[6;4~", TB_KEY_PGDN, TB_MOD_ALT | TB_MOD_SHIFT }, - {"\x1b[6;5~", TB_KEY_PGDN, TB_MOD_CTRL }, - {"\x1b[6;6~", TB_KEY_PGDN, TB_MOD_CTRL | TB_MOD_SHIFT }, - {"\x1b[6;7~", TB_KEY_PGDN, TB_MOD_CTRL | TB_MOD_ALT }, - {"\x1b[6;8~", TB_KEY_PGDN, TB_MOD_CTRL | TB_MOD_ALT | TB_MOD_SHIFT}, - - {"\x1b[1;2P", TB_KEY_F1, TB_MOD_SHIFT }, - {"\x1b[1;3P", TB_KEY_F1, TB_MOD_ALT }, - {"\x1b[1;4P", TB_KEY_F1, TB_MOD_ALT | TB_MOD_SHIFT }, - {"\x1b[1;5P", TB_KEY_F1, TB_MOD_CTRL }, - {"\x1b[1;6P", TB_KEY_F1, TB_MOD_CTRL | TB_MOD_SHIFT }, - {"\x1b[1;7P", TB_KEY_F1, TB_MOD_CTRL | TB_MOD_ALT }, - {"\x1b[1;8P", TB_KEY_F1, TB_MOD_CTRL | TB_MOD_ALT | TB_MOD_SHIFT}, - - {"\x1b[1;2Q", TB_KEY_F2, TB_MOD_SHIFT }, - {"\x1b[1;3Q", TB_KEY_F2, TB_MOD_ALT }, - {"\x1b[1;4Q", TB_KEY_F2, TB_MOD_ALT | TB_MOD_SHIFT }, - {"\x1b[1;5Q", TB_KEY_F2, TB_MOD_CTRL }, - {"\x1b[1;6Q", TB_KEY_F2, TB_MOD_CTRL | TB_MOD_SHIFT }, - {"\x1b[1;7Q", TB_KEY_F2, TB_MOD_CTRL | TB_MOD_ALT }, - {"\x1b[1;8Q", TB_KEY_F2, TB_MOD_CTRL | TB_MOD_ALT | TB_MOD_SHIFT}, - - {"\x1b[1;2R", TB_KEY_F3, TB_MOD_SHIFT }, - {"\x1b[1;3R", TB_KEY_F3, TB_MOD_ALT }, - {"\x1b[1;4R", TB_KEY_F3, TB_MOD_ALT | TB_MOD_SHIFT }, - {"\x1b[1;5R", TB_KEY_F3, TB_MOD_CTRL }, - {"\x1b[1;6R", TB_KEY_F3, TB_MOD_CTRL | TB_MOD_SHIFT }, - {"\x1b[1;7R", TB_KEY_F3, TB_MOD_CTRL | TB_MOD_ALT }, - {"\x1b[1;8R", TB_KEY_F3, TB_MOD_CTRL | TB_MOD_ALT | TB_MOD_SHIFT}, - - {"\x1b[1;2S", TB_KEY_F4, TB_MOD_SHIFT }, - {"\x1b[1;3S", TB_KEY_F4, TB_MOD_ALT }, - {"\x1b[1;4S", TB_KEY_F4, TB_MOD_ALT | TB_MOD_SHIFT }, - {"\x1b[1;5S", TB_KEY_F4, TB_MOD_CTRL }, - {"\x1b[1;6S", TB_KEY_F4, TB_MOD_CTRL | TB_MOD_SHIFT }, - {"\x1b[1;7S", TB_KEY_F4, TB_MOD_CTRL | TB_MOD_ALT }, - {"\x1b[1;8S", TB_KEY_F4, TB_MOD_CTRL | TB_MOD_ALT | TB_MOD_SHIFT}, - - {"\x1b[15;2~", TB_KEY_F5, TB_MOD_SHIFT }, - {"\x1b[15;3~", TB_KEY_F5, TB_MOD_ALT }, - {"\x1b[15;4~", TB_KEY_F5, TB_MOD_ALT | TB_MOD_SHIFT }, - {"\x1b[15;5~", TB_KEY_F5, TB_MOD_CTRL }, - {"\x1b[15;6~", TB_KEY_F5, TB_MOD_CTRL | TB_MOD_SHIFT }, - {"\x1b[15;7~", TB_KEY_F5, TB_MOD_CTRL | TB_MOD_ALT }, - {"\x1b[15;8~", TB_KEY_F5, TB_MOD_CTRL | TB_MOD_ALT | TB_MOD_SHIFT}, - - {"\x1b[17;2~", TB_KEY_F6, TB_MOD_SHIFT }, - {"\x1b[17;3~", TB_KEY_F6, TB_MOD_ALT }, - {"\x1b[17;4~", TB_KEY_F6, TB_MOD_ALT | TB_MOD_SHIFT }, - {"\x1b[17;5~", TB_KEY_F6, TB_MOD_CTRL }, - {"\x1b[17;6~", TB_KEY_F6, TB_MOD_CTRL | TB_MOD_SHIFT }, - {"\x1b[17;7~", TB_KEY_F6, TB_MOD_CTRL | TB_MOD_ALT }, - {"\x1b[17;8~", TB_KEY_F6, TB_MOD_CTRL | TB_MOD_ALT | TB_MOD_SHIFT}, - - {"\x1b[18;2~", TB_KEY_F7, TB_MOD_SHIFT }, - {"\x1b[18;3~", TB_KEY_F7, TB_MOD_ALT }, - {"\x1b[18;4~", TB_KEY_F7, TB_MOD_ALT | TB_MOD_SHIFT }, - {"\x1b[18;5~", TB_KEY_F7, TB_MOD_CTRL }, - {"\x1b[18;6~", TB_KEY_F7, TB_MOD_CTRL | TB_MOD_SHIFT }, - {"\x1b[18;7~", TB_KEY_F7, TB_MOD_CTRL | TB_MOD_ALT }, - {"\x1b[18;8~", TB_KEY_F7, TB_MOD_CTRL | TB_MOD_ALT | TB_MOD_SHIFT}, - - {"\x1b[19;2~", TB_KEY_F8, TB_MOD_SHIFT }, - {"\x1b[19;3~", TB_KEY_F8, TB_MOD_ALT }, - {"\x1b[19;4~", TB_KEY_F8, TB_MOD_ALT | TB_MOD_SHIFT }, - {"\x1b[19;5~", TB_KEY_F8, TB_MOD_CTRL }, - {"\x1b[19;6~", TB_KEY_F8, TB_MOD_CTRL | TB_MOD_SHIFT }, - {"\x1b[19;7~", TB_KEY_F8, TB_MOD_CTRL | TB_MOD_ALT }, - {"\x1b[19;8~", TB_KEY_F8, TB_MOD_CTRL | TB_MOD_ALT | TB_MOD_SHIFT}, - - {"\x1b[20;2~", TB_KEY_F9, TB_MOD_SHIFT }, - {"\x1b[20;3~", TB_KEY_F9, TB_MOD_ALT }, - {"\x1b[20;4~", TB_KEY_F9, TB_MOD_ALT | TB_MOD_SHIFT }, - {"\x1b[20;5~", TB_KEY_F9, TB_MOD_CTRL }, - {"\x1b[20;6~", TB_KEY_F9, TB_MOD_CTRL | TB_MOD_SHIFT }, - {"\x1b[20;7~", TB_KEY_F9, TB_MOD_CTRL | TB_MOD_ALT }, - {"\x1b[20;8~", TB_KEY_F9, TB_MOD_CTRL | TB_MOD_ALT | TB_MOD_SHIFT}, - - {"\x1b[21;2~", TB_KEY_F10, TB_MOD_SHIFT }, - {"\x1b[21;3~", TB_KEY_F10, TB_MOD_ALT }, - {"\x1b[21;4~", TB_KEY_F10, TB_MOD_ALT | TB_MOD_SHIFT }, - {"\x1b[21;5~", TB_KEY_F10, TB_MOD_CTRL }, - {"\x1b[21;6~", TB_KEY_F10, TB_MOD_CTRL | TB_MOD_SHIFT }, - {"\x1b[21;7~", TB_KEY_F10, TB_MOD_CTRL | TB_MOD_ALT }, - {"\x1b[21;8~", TB_KEY_F10, TB_MOD_CTRL | TB_MOD_ALT | TB_MOD_SHIFT}, - - {"\x1b[23;2~", TB_KEY_F11, TB_MOD_SHIFT }, - {"\x1b[23;3~", TB_KEY_F11, TB_MOD_ALT }, - {"\x1b[23;4~", TB_KEY_F11, TB_MOD_ALT | TB_MOD_SHIFT }, - {"\x1b[23;5~", TB_KEY_F11, TB_MOD_CTRL }, - {"\x1b[23;6~", TB_KEY_F11, TB_MOD_CTRL | TB_MOD_SHIFT }, - {"\x1b[23;7~", TB_KEY_F11, TB_MOD_CTRL | TB_MOD_ALT }, - {"\x1b[23;8~", TB_KEY_F11, TB_MOD_CTRL | TB_MOD_ALT | TB_MOD_SHIFT}, - - {"\x1b[24;2~", TB_KEY_F12, TB_MOD_SHIFT }, - {"\x1b[24;3~", TB_KEY_F12, TB_MOD_ALT }, - {"\x1b[24;4~", TB_KEY_F12, TB_MOD_ALT | TB_MOD_SHIFT }, - {"\x1b[24;5~", TB_KEY_F12, TB_MOD_CTRL }, - {"\x1b[24;6~", TB_KEY_F12, TB_MOD_CTRL | TB_MOD_SHIFT }, - {"\x1b[24;7~", TB_KEY_F12, TB_MOD_CTRL | TB_MOD_ALT }, - {"\x1b[24;8~", TB_KEY_F12, TB_MOD_CTRL | TB_MOD_ALT | TB_MOD_SHIFT}, - - // rxvt arrows - {"\x1b[a", TB_KEY_ARROW_UP, TB_MOD_SHIFT }, - {"\x1b\x1b[A", TB_KEY_ARROW_UP, TB_MOD_ALT }, - {"\x1b\x1b[a", TB_KEY_ARROW_UP, TB_MOD_ALT | TB_MOD_SHIFT }, - {"\x1bOa", TB_KEY_ARROW_UP, TB_MOD_CTRL }, - {"\x1b\x1bOa", TB_KEY_ARROW_UP, TB_MOD_CTRL | TB_MOD_ALT }, - - {"\x1b[b", TB_KEY_ARROW_DOWN, TB_MOD_SHIFT }, - {"\x1b\x1b[B", TB_KEY_ARROW_DOWN, TB_MOD_ALT }, - {"\x1b\x1b[b", TB_KEY_ARROW_DOWN, TB_MOD_ALT | TB_MOD_SHIFT }, - {"\x1bOb", TB_KEY_ARROW_DOWN, TB_MOD_CTRL }, - {"\x1b\x1bOb", TB_KEY_ARROW_DOWN, TB_MOD_CTRL | TB_MOD_ALT }, - - {"\x1b[c", TB_KEY_ARROW_RIGHT, TB_MOD_SHIFT }, - {"\x1b\x1b[C", TB_KEY_ARROW_RIGHT, TB_MOD_ALT }, - {"\x1b\x1b[c", TB_KEY_ARROW_RIGHT, TB_MOD_ALT | TB_MOD_SHIFT }, - {"\x1bOc", TB_KEY_ARROW_RIGHT, TB_MOD_CTRL }, - {"\x1b\x1bOc", TB_KEY_ARROW_RIGHT, TB_MOD_CTRL | TB_MOD_ALT }, - - {"\x1b[d", TB_KEY_ARROW_LEFT, TB_MOD_SHIFT }, - {"\x1b\x1b[D", TB_KEY_ARROW_LEFT, TB_MOD_ALT }, - {"\x1b\x1b[d", TB_KEY_ARROW_LEFT, TB_MOD_ALT | TB_MOD_SHIFT }, - {"\x1bOd", TB_KEY_ARROW_LEFT, TB_MOD_CTRL }, - {"\x1b\x1bOd", TB_KEY_ARROW_LEFT, TB_MOD_CTRL | TB_MOD_ALT }, - - // rxvt keys - {"\x1b[7$", TB_KEY_HOME, TB_MOD_SHIFT }, - {"\x1b\x1b[7~", TB_KEY_HOME, TB_MOD_ALT }, - {"\x1b\x1b[7$", TB_KEY_HOME, TB_MOD_ALT | TB_MOD_SHIFT }, - {"\x1b[7^", TB_KEY_HOME, TB_MOD_CTRL }, - {"\x1b[7@", TB_KEY_HOME, TB_MOD_CTRL | TB_MOD_SHIFT }, - {"\x1b\x1b[7^", TB_KEY_HOME, TB_MOD_CTRL | TB_MOD_ALT }, - {"\x1b\x1b[7@", TB_KEY_HOME, TB_MOD_CTRL | TB_MOD_ALT | TB_MOD_SHIFT}, - - {"\x1b\x1b[8~", TB_KEY_END, TB_MOD_ALT }, - {"\x1b\x1b[8$", TB_KEY_END, TB_MOD_ALT | TB_MOD_SHIFT }, - {"\x1b[8^", TB_KEY_END, TB_MOD_CTRL }, - {"\x1b\x1b[8^", TB_KEY_END, TB_MOD_CTRL | TB_MOD_ALT }, - {"\x1b\x1b[8@", TB_KEY_END, TB_MOD_CTRL | TB_MOD_ALT | TB_MOD_SHIFT}, - {"\x1b[8@", TB_KEY_END, TB_MOD_CTRL | TB_MOD_SHIFT }, - {"\x1b[8$", TB_KEY_END, TB_MOD_SHIFT }, - - {"\x1b\x1b[2~", TB_KEY_INSERT, TB_MOD_ALT }, - {"\x1b\x1b[2$", TB_KEY_INSERT, TB_MOD_ALT | TB_MOD_SHIFT }, - {"\x1b[2^", TB_KEY_INSERT, TB_MOD_CTRL }, - {"\x1b\x1b[2^", TB_KEY_INSERT, TB_MOD_CTRL | TB_MOD_ALT }, - {"\x1b\x1b[2@", TB_KEY_INSERT, TB_MOD_CTRL | TB_MOD_ALT | TB_MOD_SHIFT}, - {"\x1b[2@", TB_KEY_INSERT, TB_MOD_CTRL | TB_MOD_SHIFT }, - {"\x1b[2$", TB_KEY_INSERT, TB_MOD_SHIFT }, - - {"\x1b\x1b[3~", TB_KEY_DELETE, TB_MOD_ALT }, - {"\x1b\x1b[3$", TB_KEY_DELETE, TB_MOD_ALT | TB_MOD_SHIFT }, - {"\x1b[3^", TB_KEY_DELETE, TB_MOD_CTRL }, - {"\x1b\x1b[3^", TB_KEY_DELETE, TB_MOD_CTRL | TB_MOD_ALT }, - {"\x1b\x1b[3@", TB_KEY_DELETE, TB_MOD_CTRL | TB_MOD_ALT | TB_MOD_SHIFT}, - {"\x1b[3@", TB_KEY_DELETE, TB_MOD_CTRL | TB_MOD_SHIFT }, - {"\x1b[3$", TB_KEY_DELETE, TB_MOD_SHIFT }, - - {"\x1b\x1b[5~", TB_KEY_PGUP, TB_MOD_ALT }, - {"\x1b\x1b[5$", TB_KEY_PGUP, TB_MOD_ALT | TB_MOD_SHIFT }, - {"\x1b[5^", TB_KEY_PGUP, TB_MOD_CTRL }, - {"\x1b\x1b[5^", TB_KEY_PGUP, TB_MOD_CTRL | TB_MOD_ALT }, - {"\x1b\x1b[5@", TB_KEY_PGUP, TB_MOD_CTRL | TB_MOD_ALT | TB_MOD_SHIFT}, - {"\x1b[5@", TB_KEY_PGUP, TB_MOD_CTRL | TB_MOD_SHIFT }, - {"\x1b[5$", TB_KEY_PGUP, TB_MOD_SHIFT }, - - {"\x1b\x1b[6~", TB_KEY_PGDN, TB_MOD_ALT }, - {"\x1b\x1b[6$", TB_KEY_PGDN, TB_MOD_ALT | TB_MOD_SHIFT }, - {"\x1b[6^", TB_KEY_PGDN, TB_MOD_CTRL }, - {"\x1b\x1b[6^", TB_KEY_PGDN, TB_MOD_CTRL | TB_MOD_ALT }, - {"\x1b\x1b[6@", TB_KEY_PGDN, TB_MOD_CTRL | TB_MOD_ALT | TB_MOD_SHIFT}, - {"\x1b[6@", TB_KEY_PGDN, TB_MOD_CTRL | TB_MOD_SHIFT }, - {"\x1b[6$", TB_KEY_PGDN, TB_MOD_SHIFT }, - - {"\x1b\x1b[11~", TB_KEY_F1, TB_MOD_ALT }, - {"\x1b\x1b[23~", TB_KEY_F1, TB_MOD_ALT | TB_MOD_SHIFT }, - {"\x1b[11^", TB_KEY_F1, TB_MOD_CTRL }, - {"\x1b\x1b[11^", TB_KEY_F1, TB_MOD_CTRL | TB_MOD_ALT }, - {"\x1b\x1b[23^", TB_KEY_F1, TB_MOD_CTRL | TB_MOD_ALT | TB_MOD_SHIFT}, - {"\x1b[23^", TB_KEY_F1, TB_MOD_CTRL | TB_MOD_SHIFT }, - {"\x1b[23~", TB_KEY_F1, TB_MOD_SHIFT }, - - {"\x1b\x1b[12~", TB_KEY_F2, TB_MOD_ALT }, - {"\x1b\x1b[24~", TB_KEY_F2, TB_MOD_ALT | TB_MOD_SHIFT }, - {"\x1b[12^", TB_KEY_F2, TB_MOD_CTRL }, - {"\x1b\x1b[12^", TB_KEY_F2, TB_MOD_CTRL | TB_MOD_ALT }, - {"\x1b\x1b[24^", TB_KEY_F2, TB_MOD_CTRL | TB_MOD_ALT | TB_MOD_SHIFT}, - {"\x1b[24^", TB_KEY_F2, TB_MOD_CTRL | TB_MOD_SHIFT }, - {"\x1b[24~", TB_KEY_F2, TB_MOD_SHIFT }, - - {"\x1b\x1b[13~", TB_KEY_F3, TB_MOD_ALT }, - {"\x1b\x1b[25~", TB_KEY_F3, TB_MOD_ALT | TB_MOD_SHIFT }, - {"\x1b[13^", TB_KEY_F3, TB_MOD_CTRL }, - {"\x1b\x1b[13^", TB_KEY_F3, TB_MOD_CTRL | TB_MOD_ALT }, - {"\x1b\x1b[25^", TB_KEY_F3, TB_MOD_CTRL | TB_MOD_ALT | TB_MOD_SHIFT}, - {"\x1b[25^", TB_KEY_F3, TB_MOD_CTRL | TB_MOD_SHIFT }, - {"\x1b[25~", TB_KEY_F3, TB_MOD_SHIFT }, - - {"\x1b\x1b[14~", TB_KEY_F4, TB_MOD_ALT }, - {"\x1b\x1b[26~", TB_KEY_F4, TB_MOD_ALT | TB_MOD_SHIFT }, - {"\x1b[14^", TB_KEY_F4, TB_MOD_CTRL }, - {"\x1b\x1b[14^", TB_KEY_F4, TB_MOD_CTRL | TB_MOD_ALT }, - {"\x1b\x1b[26^", TB_KEY_F4, TB_MOD_CTRL | TB_MOD_ALT | TB_MOD_SHIFT}, - {"\x1b[26^", TB_KEY_F4, TB_MOD_CTRL | TB_MOD_SHIFT }, - {"\x1b[26~", TB_KEY_F4, TB_MOD_SHIFT }, - - {"\x1b\x1b[15~", TB_KEY_F5, TB_MOD_ALT }, - {"\x1b\x1b[28~", TB_KEY_F5, TB_MOD_ALT | TB_MOD_SHIFT }, - {"\x1b[15^", TB_KEY_F5, TB_MOD_CTRL }, - {"\x1b\x1b[15^", TB_KEY_F5, TB_MOD_CTRL | TB_MOD_ALT }, - {"\x1b\x1b[28^", TB_KEY_F5, TB_MOD_CTRL | TB_MOD_ALT | TB_MOD_SHIFT}, - {"\x1b[28^", TB_KEY_F5, TB_MOD_CTRL | TB_MOD_SHIFT }, - {"\x1b[28~", TB_KEY_F5, TB_MOD_SHIFT }, - - {"\x1b\x1b[17~", TB_KEY_F6, TB_MOD_ALT }, - {"\x1b\x1b[29~", TB_KEY_F6, TB_MOD_ALT | TB_MOD_SHIFT }, - {"\x1b[17^", TB_KEY_F6, TB_MOD_CTRL }, - {"\x1b\x1b[17^", TB_KEY_F6, TB_MOD_CTRL | TB_MOD_ALT }, - {"\x1b\x1b[29^", TB_KEY_F6, TB_MOD_CTRL | TB_MOD_ALT | TB_MOD_SHIFT}, - {"\x1b[29^", TB_KEY_F6, TB_MOD_CTRL | TB_MOD_SHIFT }, - {"\x1b[29~", TB_KEY_F6, TB_MOD_SHIFT }, - - {"\x1b\x1b[18~", TB_KEY_F7, TB_MOD_ALT }, - {"\x1b\x1b[31~", TB_KEY_F7, TB_MOD_ALT | TB_MOD_SHIFT }, - {"\x1b[18^", TB_KEY_F7, TB_MOD_CTRL }, - {"\x1b\x1b[18^", TB_KEY_F7, TB_MOD_CTRL | TB_MOD_ALT }, - {"\x1b\x1b[31^", TB_KEY_F7, TB_MOD_CTRL | TB_MOD_ALT | TB_MOD_SHIFT}, - {"\x1b[31^", TB_KEY_F7, TB_MOD_CTRL | TB_MOD_SHIFT }, - {"\x1b[31~", TB_KEY_F7, TB_MOD_SHIFT }, - - {"\x1b\x1b[19~", TB_KEY_F8, TB_MOD_ALT }, - {"\x1b\x1b[32~", TB_KEY_F8, TB_MOD_ALT | TB_MOD_SHIFT }, - {"\x1b[19^", TB_KEY_F8, TB_MOD_CTRL }, - {"\x1b\x1b[19^", TB_KEY_F8, TB_MOD_CTRL | TB_MOD_ALT }, - {"\x1b\x1b[32^", TB_KEY_F8, TB_MOD_CTRL | TB_MOD_ALT | TB_MOD_SHIFT}, - {"\x1b[32^", TB_KEY_F8, TB_MOD_CTRL | TB_MOD_SHIFT }, - {"\x1b[32~", TB_KEY_F8, TB_MOD_SHIFT }, - - {"\x1b\x1b[20~", TB_KEY_F9, TB_MOD_ALT }, - {"\x1b\x1b[33~", TB_KEY_F9, TB_MOD_ALT | TB_MOD_SHIFT }, - {"\x1b[20^", TB_KEY_F9, TB_MOD_CTRL }, - {"\x1b\x1b[20^", TB_KEY_F9, TB_MOD_CTRL | TB_MOD_ALT }, - {"\x1b\x1b[33^", TB_KEY_F9, TB_MOD_CTRL | TB_MOD_ALT | TB_MOD_SHIFT}, - {"\x1b[33^", TB_KEY_F9, TB_MOD_CTRL | TB_MOD_SHIFT }, - {"\x1b[33~", TB_KEY_F9, TB_MOD_SHIFT }, - - {"\x1b\x1b[21~", TB_KEY_F10, TB_MOD_ALT }, - {"\x1b\x1b[34~", TB_KEY_F10, TB_MOD_ALT | TB_MOD_SHIFT }, - {"\x1b[21^", TB_KEY_F10, TB_MOD_CTRL }, - {"\x1b\x1b[21^", TB_KEY_F10, TB_MOD_CTRL | TB_MOD_ALT }, - {"\x1b\x1b[34^", TB_KEY_F10, TB_MOD_CTRL | TB_MOD_ALT | TB_MOD_SHIFT}, - {"\x1b[34^", TB_KEY_F10, TB_MOD_CTRL | TB_MOD_SHIFT }, - {"\x1b[34~", TB_KEY_F10, TB_MOD_SHIFT }, - - {"\x1b\x1b[23~", TB_KEY_F11, TB_MOD_ALT }, - {"\x1b\x1b[23$", TB_KEY_F11, TB_MOD_ALT | TB_MOD_SHIFT }, - {"\x1b[23^", TB_KEY_F11, TB_MOD_CTRL }, - {"\x1b\x1b[23^", TB_KEY_F11, TB_MOD_CTRL | TB_MOD_ALT }, - {"\x1b\x1b[23@", TB_KEY_F11, TB_MOD_CTRL | TB_MOD_ALT | TB_MOD_SHIFT}, - {"\x1b[23@", TB_KEY_F11, TB_MOD_CTRL | TB_MOD_SHIFT }, - {"\x1b[23$", TB_KEY_F11, TB_MOD_SHIFT }, - - {"\x1b\x1b[24~", TB_KEY_F12, TB_MOD_ALT }, - {"\x1b\x1b[24$", TB_KEY_F12, TB_MOD_ALT | TB_MOD_SHIFT }, - {"\x1b[24^", TB_KEY_F12, TB_MOD_CTRL }, - {"\x1b\x1b[24^", TB_KEY_F12, TB_MOD_CTRL | TB_MOD_ALT }, - {"\x1b\x1b[24@", TB_KEY_F12, TB_MOD_CTRL | TB_MOD_ALT | TB_MOD_SHIFT}, - {"\x1b[24@", TB_KEY_F12, TB_MOD_CTRL | TB_MOD_SHIFT }, - {"\x1b[24$", TB_KEY_F12, TB_MOD_SHIFT }, - - // linux console/putty arrows - {"\x1b[A", TB_KEY_ARROW_UP, TB_MOD_SHIFT }, - {"\x1b[B", TB_KEY_ARROW_DOWN, TB_MOD_SHIFT }, - {"\x1b[C", TB_KEY_ARROW_RIGHT, TB_MOD_SHIFT }, - {"\x1b[D", TB_KEY_ARROW_LEFT, TB_MOD_SHIFT }, - - // more putty arrows - {"\x1bOA", TB_KEY_ARROW_UP, TB_MOD_CTRL }, - {"\x1b\x1bOA", TB_KEY_ARROW_UP, TB_MOD_CTRL | TB_MOD_ALT }, - {"\x1bOB", TB_KEY_ARROW_DOWN, TB_MOD_CTRL }, - {"\x1b\x1bOB", TB_KEY_ARROW_DOWN, TB_MOD_CTRL | TB_MOD_ALT }, - {"\x1bOC", TB_KEY_ARROW_RIGHT, TB_MOD_CTRL }, - {"\x1b\x1bOC", TB_KEY_ARROW_RIGHT, TB_MOD_CTRL | TB_MOD_ALT }, - {"\x1bOD", TB_KEY_ARROW_LEFT, TB_MOD_CTRL }, - {"\x1b\x1bOD", TB_KEY_ARROW_LEFT, TB_MOD_CTRL | TB_MOD_ALT }, - - {NULL, 0, 0 }, -}; - -static const unsigned char utf8_length[256] = {1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, - 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, - 3, 3, 3, 3, 4, 4, 4, 4, 4, 4, 4, 4, 5, 5, 5, 5, 6, 6, 1, 1}; - -static const unsigned char utf8_mask[6] = {0x7f, 0x1f, 0x0f, 0x07, 0x03, 0x01}; - -static int tb_reset(void); -static int tb_printf_inner(int x, int y, uintattr_t fg, uintattr_t bg, - size_t *out_w, const char *fmt, va_list vl); -static int init_term_attrs(void); -static int init_term_caps(void); -static int init_cap_trie(void); -static int cap_trie_add(const char *cap, uint16_t key, uint8_t mod); -static int cap_trie_find(const char *buf, size_t nbuf, struct cap_trie_t **last, - size_t *depth); -static int cap_trie_deinit(struct cap_trie_t *node); -static int init_resize_handler(void); -static int send_init_escape_codes(void); -static int send_clear(void); -static int update_term_size(void); -static int update_term_size_via_esc(void); -static int init_cellbuf(void); -static int tb_deinit(void); -static int load_terminfo(void); -static int load_terminfo_from_path(const char *path, const char *term); -static int read_terminfo_path(const char *path); -static int parse_terminfo_caps(void); -static int load_builtin_caps(void); -static const char *get_terminfo_string(int16_t str_offsets_pos, - int16_t str_offsets_len, int16_t str_table_pos, int16_t str_table_len, - int16_t str_index); -static int wait_event(struct tb_event *event, int timeout); -static int extract_event(struct tb_event *event); -static int extract_esc(struct tb_event *event); -static int extract_esc_user(struct tb_event *event, int is_post); -static int extract_esc_cap(struct tb_event *event); -static int extract_esc_mouse(struct tb_event *event); -static int resize_cellbufs(void); -static void handle_resize(int sig); -static int send_attr(uintattr_t fg, uintattr_t bg); -static int send_sgr(uint32_t fg, uint32_t bg, int fg_is_default, - int bg_is_default); -static int send_cursor_if(int x, int y); -static int send_char(int x, int y, uint32_t ch); -static int send_cluster(int x, int y, uint32_t *ch, size_t nch); -static int convert_num(uint32_t num, char *buf); -static int cell_cmp(struct tb_cell *a, struct tb_cell *b); -static int cell_copy(struct tb_cell *dst, struct tb_cell *src); -static int cell_set(struct tb_cell *cell, uint32_t *ch, size_t nch, - uintattr_t fg, uintattr_t bg); -static int cell_reserve_ech(struct tb_cell *cell, size_t n); -static int cell_free(struct tb_cell *cell); -static int cellbuf_init(struct cellbuf_t *c, int w, int h); -static int cellbuf_free(struct cellbuf_t *c); -static int cellbuf_clear(struct cellbuf_t *c); -static int cellbuf_get(struct cellbuf_t *c, int x, int y, struct tb_cell **out); -static int cellbuf_resize(struct cellbuf_t *c, int w, int h); -static int bytebuf_puts(struct bytebuf_t *b, const char *str); -static int bytebuf_nputs(struct bytebuf_t *b, const char *str, size_t nstr); -static int bytebuf_shift(struct bytebuf_t *b, size_t n); -static int bytebuf_flush(struct bytebuf_t *b, int fd); -static int bytebuf_reserve(struct bytebuf_t *b, size_t sz); -static int bytebuf_free(struct bytebuf_t *b); - -int tb_init(void) { - return tb_init_file("/dev/tty"); -} - -int tb_init_file(const char *path) { - if (global.initialized) { - return TB_ERR_INIT_ALREADY; - } - int ttyfd = open(path, O_RDWR); - if (ttyfd < 0) { - global.last_errno = errno; - return TB_ERR_INIT_OPEN; - } - global.ttyfd_open = 1; - return tb_init_fd(ttyfd); -} - -int tb_init_fd(int ttyfd) { - return tb_init_rwfd(ttyfd, ttyfd); -} - -int tb_init_rwfd(int rfd, int wfd) { - int rv; - - tb_reset(); - global.ttyfd = rfd == wfd && isatty(rfd) ? rfd : -1; - global.rfd = rfd; - global.wfd = wfd; - - do { - if_err_break(rv, init_term_attrs()); - if_err_break(rv, init_term_caps()); - if_err_break(rv, init_cap_trie()); - if_err_break(rv, init_resize_handler()); - if_err_break(rv, send_init_escape_codes()); - if_err_break(rv, send_clear()); - if_err_break(rv, update_term_size()); - if_err_break(rv, init_cellbuf()); - global.initialized = 1; - } while (0); - - if (rv != TB_OK) { - tb_deinit(); - } - - return rv; -} - -int tb_shutdown(void) { - if_not_init_return(); - tb_deinit(); - return TB_OK; -} - -int tb_width(void) { - if_not_init_return(); - return global.width; -} - -int tb_height(void) { - if_not_init_return(); - return global.height; -} - -int tb_clear(void) { - if_not_init_return(); - return cellbuf_clear(&global.back); -} - -int tb_set_clear_attrs(uintattr_t fg, uintattr_t bg) { - if_not_init_return(); - global.fg = fg; - global.bg = bg; - return TB_OK; -} - -int tb_present(void) { - if_not_init_return(); - - int rv; - - // TODO Assert global.back.(width,height) == global.front.(width,height) - - global.last_x = -1; - global.last_y = -1; - - int x, y, i; - for (y = 0; y < global.front.height; y++) { - for (x = 0; x < global.front.width;) { - struct tb_cell *back, *front; - if_err_return(rv, cellbuf_get(&global.back, x, y, &back)); - if_err_return(rv, cellbuf_get(&global.front, x, y, &front)); - - int w; - { -#ifdef TB_OPT_EGC - if (back->nech > 0) - w = wcswidth((wchar_t *)back->ech, back->nech); - else -#endif - /* wcwidth() simply returns -1 on overflow of wchar_t */ - w = wcwidth((wchar_t)back->ch); - } - if (w < 1) { - w = 1; - } - - if (cell_cmp(back, front) != 0) { - cell_copy(front, back); - - send_attr(back->fg, back->bg); - if (w > 1 && x >= global.front.width - (w - 1)) { - // Not enough room for wide char, send spaces - for (i = x; i < global.front.width; i++) { - send_char(i, y, ' '); - } - } else { - { -#ifdef TB_OPT_EGC - if (back->nech > 0) - send_cluster(x, y, back->ech, back->nech); - else -#endif - send_char(x, y, back->ch); - } - - // When wcwidth>1, we need to advance the cursor by more - // than 1, thereby skipping some cells. Set these skipped - // cells to an invalid codepoint in the front buffer, so - // that if this cell is later replaced by a wcwidth==1 char, - // we'll get a cell_cmp diff for the skipped cells and - // properly re-render. - for (i = 1; i < w; i++) { - struct tb_cell *front_wide; - uint32_t invalid = -1; - if_err_return(rv, - cellbuf_get(&global.front, x + i, y, &front_wide)); - if_err_return(rv, - cell_set(front_wide, &invalid, 1, -1, -1)); - } - } - } - x += w; - } - } - - if_err_return(rv, send_cursor_if(global.cursor_x, global.cursor_y)); - if_err_return(rv, bytebuf_flush(&global.out, global.wfd)); - - return TB_OK; -} - -int tb_invalidate(void) { - int rv; - if_not_init_return(); - if_err_return(rv, resize_cellbufs()); - return TB_OK; -} - -int tb_set_cursor(int cx, int cy) { - if_not_init_return(); - int rv; - if (cx < 0) cx = 0; - if (cy < 0) cy = 0; - if (global.cursor_x == -1) { - if_err_return(rv, - bytebuf_puts(&global.out, global.caps[TB_CAP_SHOW_CURSOR])); - } - if_err_return(rv, send_cursor_if(cx, cy)); - global.cursor_x = cx; - global.cursor_y = cy; - return TB_OK; -} - -int tb_hide_cursor(void) { - if_not_init_return(); - int rv; - if (global.cursor_x >= 0) { - if_err_return(rv, - bytebuf_puts(&global.out, global.caps[TB_CAP_HIDE_CURSOR])); - } - global.cursor_x = -1; - global.cursor_y = -1; - return TB_OK; -} - -int tb_set_cell(int x, int y, uint32_t ch, uintattr_t fg, uintattr_t bg) { - return tb_set_cell_ex(x, y, &ch, 1, fg, bg); -} - -int tb_set_cell_ex(int x, int y, uint32_t *ch, size_t nch, uintattr_t fg, - uintattr_t bg) { - if_not_init_return(); - int rv; - struct tb_cell *cell; - if_err_return(rv, cellbuf_get(&global.back, x, y, &cell)); - if_err_return(rv, cell_set(cell, ch, nch, fg, bg)); - return TB_OK; -} - -int tb_extend_cell(int x, int y, uint32_t ch) { - if_not_init_return(); -#ifdef TB_OPT_EGC - int rv; - struct tb_cell *cell; - size_t nech; - if_err_return(rv, cellbuf_get(&global.back, x, y, &cell)); - if (cell->nech > 0) { // append to ech - nech = cell->nech + 1; - if_err_return(rv, cell_reserve_ech(cell, nech)); - cell->ech[nech - 1] = ch; - } else { // make new ech - nech = 2; - if_err_return(rv, cell_reserve_ech(cell, nech)); - cell->ech[0] = cell->ch; - cell->ech[1] = ch; - } - cell->ech[nech] = '\0'; - cell->nech = nech; - return TB_OK; -#else - (void)x; - (void)y; - (void)ch; - return TB_ERR; -#endif -} - -int tb_set_input_mode(int mode) { - if_not_init_return(); - if (mode == TB_INPUT_CURRENT) { - return global.input_mode; - } - - if ((mode & (TB_INPUT_ESC | TB_INPUT_ALT)) == 0) { - mode |= TB_INPUT_ESC; - } - - if ((mode & (TB_INPUT_ESC | TB_INPUT_ALT)) == (TB_INPUT_ESC | TB_INPUT_ALT)) - { - mode &= ~TB_INPUT_ALT; - } - - if (mode & TB_INPUT_MOUSE) { - bytebuf_puts(&global.out, TB_HARDCAP_ENTER_MOUSE); - bytebuf_flush(&global.out, global.wfd); - } else { - bytebuf_puts(&global.out, TB_HARDCAP_EXIT_MOUSE); - bytebuf_flush(&global.out, global.wfd); - } - - global.input_mode = mode; - return TB_OK; -} - -int tb_set_output_mode(int mode) { - if_not_init_return(); - switch (mode) { - case TB_OUTPUT_CURRENT: - return global.output_mode; - case TB_OUTPUT_NORMAL: - case TB_OUTPUT_256: - case TB_OUTPUT_216: - case TB_OUTPUT_GRAYSCALE: -#if TB_OPT_ATTR_W >= 32 - case TB_OUTPUT_TRUECOLOR: -#endif - global.last_fg = ~global.fg; - global.last_bg = ~global.bg; - global.output_mode = mode; - return TB_OK; - } - return TB_ERR; -} - -int tb_peek_event(struct tb_event *event, int timeout_ms) { - if_not_init_return(); - return wait_event(event, timeout_ms); -} - -int tb_poll_event(struct tb_event *event) { - if_not_init_return(); - return wait_event(event, -1); -} - -int tb_get_fds(int *ttyfd, int *resizefd) { - if_not_init_return(); - - *ttyfd = global.rfd; - *resizefd = global.resize_pipefd[0]; - - return TB_OK; -} - -int tb_print(int x, int y, uintattr_t fg, uintattr_t bg, const char *str) { - return tb_print_ex(x, y, fg, bg, NULL, str); -} - -int tb_print_ex(int x, int y, uintattr_t fg, uintattr_t bg, size_t *out_w, - const char *str) { - int rv; - uint32_t uni; - int w, ix = x; - if (out_w) { - *out_w = 0; - } - while (*str) { - rv = tb_utf8_char_to_unicode(&uni, str); - if (rv < 0) { - uni = 0xfffd; // replace invalid UTF-8 char with U+FFFD - str += rv * -1; - } else if (rv > 0) { - str += rv; - } else { - break; // shouldn't get here - } - w = wcwidth((wchar_t)uni); - if (w < 0) w = 1; - if (w == 0 && x > ix) { - if_err_return(rv, tb_extend_cell(x - 1, y, uni)); - } else { - if_err_return(rv, tb_set_cell(x, y, uni, fg, bg)); - } - x += w; - if (out_w) { - *out_w += w; - } - } - return TB_OK; -} - -int tb_printf(int x, int y, uintattr_t fg, uintattr_t bg, const char *fmt, - ...) { - int rv; - va_list vl; - va_start(vl, fmt); - rv = tb_printf_inner(x, y, fg, bg, NULL, fmt, vl); - va_end(vl); - return rv; -} - -int tb_printf_ex(int x, int y, uintattr_t fg, uintattr_t bg, size_t *out_w, - const char *fmt, ...) { - int rv; - va_list vl; - va_start(vl, fmt); - rv = tb_printf_inner(x, y, fg, bg, out_w, fmt, vl); - va_end(vl); - return rv; -} - -int tb_send(const char *buf, size_t nbuf) { - return bytebuf_nputs(&global.out, buf, nbuf); -} - -int tb_sendf(const char *fmt, ...) { - int rv; - char buf[TB_OPT_PRINTF_BUF]; - va_list vl; - va_start(vl, fmt); - rv = vsnprintf(buf, sizeof(buf), fmt, vl); - va_end(vl); - if (rv < 0 || rv >= (int)sizeof(buf)) { - return TB_ERR; - } - return tb_send(buf, (size_t)rv); -} - -int tb_set_func(int fn_type, int (*fn)(struct tb_event *, size_t *)) { - switch (fn_type) { - case TB_FUNC_EXTRACT_PRE: - global.fn_extract_esc_pre = fn; - return TB_OK; - case TB_FUNC_EXTRACT_POST: - global.fn_extract_esc_post = fn; - return TB_OK; - } - return TB_ERR; -} - -struct tb_cell *tb_cell_buffer(void) { - if (!global.initialized) return NULL; - return global.back.cells; -} - -int tb_utf8_char_length(char c) { - return utf8_length[(unsigned char)c]; -} - -int tb_utf8_char_to_unicode(uint32_t *out, const char *c) { - if (*c == '\0') return 0; - - int i; - unsigned char len = tb_utf8_char_length(*c); - unsigned char mask = utf8_mask[len - 1]; - uint32_t result = c[0] & mask; - for (i = 1; i < len && c[i] != '\0'; ++i) { - result <<= 6; - result |= c[i] & 0x3f; - } - - if (i != len) return i * -1; - - *out = result; - return (int)len; -} - -int tb_utf8_unicode_to_char(char *out, uint32_t c) { - int len = 0; - int first; - int i; - - if (c < 0x80) { - first = 0; - len = 1; - } else if (c < 0x800) { - first = 0xc0; - len = 2; - } else if (c < 0x10000) { - first = 0xe0; - len = 3; - } else if (c < 0x200000) { - first = 0xf0; - len = 4; - } else if (c < 0x4000000) { - first = 0xf8; - len = 5; - } else { - first = 0xfc; - len = 6; - } - - for (i = len - 1; i > 0; --i) { - out[i] = (c & 0x3f) | 0x80; - c >>= 6; - } - out[0] = c | first; - out[len] = '\0'; - - return len; -} - -int tb_last_errno(void) { - return global.last_errno; -} - -const char *tb_strerror(int err) { - switch (err) { - case TB_OK: - return "Success"; - case TB_ERR_NEED_MORE: - return "Not enough input"; - case TB_ERR_INIT_ALREADY: - return "Termbox initialized already"; - case TB_ERR_MEM: - return "Out of memory"; - case TB_ERR_NO_EVENT: - return "No event"; - case TB_ERR_NO_TERM: - return "No TERM in environment"; - case TB_ERR_NOT_INIT: - return "Termbox not initialized"; - case TB_ERR_OUT_OF_BOUNDS: - return "Out of bounds"; - case TB_ERR_UNSUPPORTED_TERM: - return "Unsupported terminal"; - case TB_ERR_CAP_COLLISION: - return "Termcaps collision"; - case TB_ERR_RESIZE_SSCANF: - return "Terminal width/height not received by sscanf() after " - "resize"; - case TB_ERR: - case TB_ERR_INIT_OPEN: - case TB_ERR_READ: - case TB_ERR_RESIZE_IOCTL: - case TB_ERR_RESIZE_PIPE: - case TB_ERR_RESIZE_SIGACTION: - case TB_ERR_POLL: - case TB_ERR_TCGETATTR: - case TB_ERR_TCSETATTR: - case TB_ERR_RESIZE_WRITE: - case TB_ERR_RESIZE_POLL: - case TB_ERR_RESIZE_READ: - default: - strerror_r(global.last_errno, global.errbuf, sizeof(global.errbuf)); - return (const char *)global.errbuf; - } -} - -int tb_has_truecolor(void) { -#if TB_OPT_ATTR_W >= 32 - return 1; -#else - return 0; -#endif -} - -int tb_has_egc(void) { -#ifdef TB_OPT_EGC - return 1; -#else - return 0; -#endif -} - -int tb_attr_width(void) { - return TB_OPT_ATTR_W; -} - -const char *tb_version(void) { - return TB_VERSION_STR; -} - -static int tb_reset(void) { - int ttyfd_open = global.ttyfd_open; - memset(&global, 0, sizeof(global)); - global.ttyfd = -1; - global.rfd = -1; - global.wfd = -1; - global.ttyfd_open = ttyfd_open; - global.resize_pipefd[0] = -1; - global.resize_pipefd[1] = -1; - global.width = -1; - global.height = -1; - global.cursor_x = -1; - global.cursor_y = -1; - global.last_x = -1; - global.last_y = -1; - global.fg = TB_DEFAULT; - global.bg = TB_DEFAULT; - global.last_fg = ~global.fg; - global.last_bg = ~global.bg; - global.input_mode = TB_INPUT_ESC; - global.output_mode = TB_OUTPUT_NORMAL; - return TB_OK; -} - -static int init_term_attrs(void) { - if (global.ttyfd < 0) { - return TB_OK; - } - - if (tcgetattr(global.ttyfd, &global.orig_tios) != 0) { - global.last_errno = errno; - return TB_ERR_TCGETATTR; - } - - struct termios tios; - memcpy(&tios, &global.orig_tios, sizeof(tios)); - global.has_orig_tios = 1; - - cfmakeraw(&tios); - tios.c_cc[VMIN] = 1; - tios.c_cc[VTIME] = 0; - - if (tcsetattr(global.ttyfd, TCSAFLUSH, &tios) != 0) { - global.last_errno = errno; - return TB_ERR_TCSETATTR; - } - - return TB_OK; -} - -int tb_printf_inner(int x, int y, uintattr_t fg, uintattr_t bg, size_t *out_w, - const char *fmt, va_list vl) { - int rv; - char buf[TB_OPT_PRINTF_BUF]; - rv = vsnprintf(buf, sizeof(buf), fmt, vl); - if (rv < 0 || rv >= (int)sizeof(buf)) { - return TB_ERR; - } - return tb_print_ex(x, y, fg, bg, out_w, buf); -} - -static int init_term_caps(void) { - if (load_terminfo() == TB_OK) { - return parse_terminfo_caps(); - } - return load_builtin_caps(); -} - -static int init_cap_trie(void) { - int rv, i; - - // Add caps from terminfo or built-in - // - // Collisions are expected as some terminfo entries have dupes. (For - // example, att605-pc collides on TB_CAP_F4 and TB_CAP_DELETE.) First cap - // in TB_CAP_* index order will win. - // - // TODO Reorder TB_CAP_* so more critical caps come first. - for (i = 0; i < TB_CAP__COUNT_KEYS; i++) { - rv = cap_trie_add(global.caps[i], tb_key_i(i), 0); - if (rv != TB_OK && rv != TB_ERR_CAP_COLLISION) return rv; - } - - // Add built-in mod caps - // - // Collisions are OK here as well. This can happen if global.caps collides - // with builtin_mod_caps. It is desirable to give precedence to global.caps - // here. - for (i = 0; builtin_mod_caps[i].cap != NULL; i++) { - rv = cap_trie_add(builtin_mod_caps[i].cap, builtin_mod_caps[i].key, - builtin_mod_caps[i].mod); - if (rv != TB_OK && rv != TB_ERR_CAP_COLLISION) return rv; - } - - return TB_OK; -} - -static int cap_trie_add(const char *cap, uint16_t key, uint8_t mod) { - struct cap_trie_t *next, *node = &global.cap_trie; - size_t i, j; - - if (!cap || strlen(cap) <= 0) return TB_OK; // Nothing to do for empty caps - - for (i = 0; cap[i] != '\0'; i++) { - char c = cap[i]; - next = NULL; - - // Check if c is already a child of node - for (j = 0; j < node->nchildren; j++) { - if (node->children[j].c == c) { - next = &node->children[j]; - break; - } - } - if (!next) { - // We need to add a new child to node - node->nchildren += 1; - node->children = - tb_realloc(node->children, sizeof(*node) * node->nchildren); - if (!node->children) { - return TB_ERR_MEM; - } - next = &node->children[node->nchildren - 1]; - memset(next, 0, sizeof(*next)); - next->c = c; - } - - // Continue - node = next; - } - - if (node->is_leaf) { - // Already a leaf here - return TB_ERR_CAP_COLLISION; - } - - node->is_leaf = 1; - node->key = key; - node->mod = mod; - return TB_OK; -} - -static int cap_trie_find(const char *buf, size_t nbuf, struct cap_trie_t **last, - size_t *depth) { - struct cap_trie_t *next, *node = &global.cap_trie; - size_t i, j; - *last = node; - *depth = 0; - for (i = 0; i < nbuf; i++) { - char c = buf[i]; - next = NULL; - - // Find c in node.children - for (j = 0; j < node->nchildren; j++) { - if (node->children[j].c == c) { - next = &node->children[j]; - break; - } - } - if (!next) { - // Not found - return TB_OK; - } - node = next; - *last = node; - *depth += 1; - if (node->is_leaf && node->nchildren < 1) { - break; - } - } - return TB_OK; -} - -static int cap_trie_deinit(struct cap_trie_t *node) { - size_t j; - for (j = 0; j < node->nchildren; j++) { - cap_trie_deinit(&node->children[j]); - } - if (node->children) { - tb_free(node->children); - } - memset(node, 0, sizeof(*node)); - return TB_OK; -} - -static int init_resize_handler(void) { - if (pipe(global.resize_pipefd) != 0) { - global.last_errno = errno; - return TB_ERR_RESIZE_PIPE; - } - - struct sigaction sa; - memset(&sa, 0, sizeof(sa)); - sa.sa_handler = handle_resize; - if (sigaction(SIGWINCH, &sa, NULL) != 0) { - global.last_errno = errno; - return TB_ERR_RESIZE_SIGACTION; - } - - return TB_OK; -} - -static int send_init_escape_codes(void) { - int rv; - if_err_return(rv, bytebuf_puts(&global.out, global.caps[TB_CAP_ENTER_CA])); - if_err_return(rv, - bytebuf_puts(&global.out, global.caps[TB_CAP_ENTER_KEYPAD])); - if_err_return(rv, - bytebuf_puts(&global.out, global.caps[TB_CAP_HIDE_CURSOR])); - return TB_OK; -} - -static int send_clear(void) { - int rv; - - if_err_return(rv, send_attr(global.fg, global.bg)); - if_err_return(rv, - bytebuf_puts(&global.out, global.caps[TB_CAP_CLEAR_SCREEN])); - - if_err_return(rv, send_cursor_if(global.cursor_x, global.cursor_y)); - if_err_return(rv, bytebuf_flush(&global.out, global.wfd)); - - global.last_x = -1; - global.last_y = -1; - - return TB_OK; -} - -static int update_term_size(void) { - int rv, ioctl_errno; - - if (global.ttyfd < 0) { - return TB_OK; - } - - struct winsize sz; - memset(&sz, 0, sizeof(sz)); - - // Try ioctl TIOCGWINSZ - if (ioctl(global.ttyfd, TIOCGWINSZ, &sz) == 0) { - global.width = sz.ws_col; - global.height = sz.ws_row; - return TB_OK; - } - ioctl_errno = errno; - - // Try >cursor(9999,9999), >u7, = 0) { - bytebuf_puts(&global.out, global.caps[TB_CAP_SHOW_CURSOR]); - bytebuf_puts(&global.out, global.caps[TB_CAP_SGR0]); - bytebuf_puts(&global.out, global.caps[TB_CAP_CLEAR_SCREEN]); - bytebuf_puts(&global.out, global.caps[TB_CAP_EXIT_CA]); - bytebuf_puts(&global.out, global.caps[TB_CAP_EXIT_KEYPAD]); - bytebuf_puts(&global.out, TB_HARDCAP_EXIT_MOUSE); - bytebuf_flush(&global.out, global.wfd); - } - if (global.ttyfd >= 0) { - if (global.has_orig_tios) { - tcsetattr(global.ttyfd, TCSAFLUSH, &global.orig_tios); - } - if (global.ttyfd_open) { - close(global.ttyfd); - global.ttyfd_open = 0; - } - } - - sigaction(SIGWINCH, &(struct sigaction){.sa_handler = SIG_DFL}, NULL); - if (global.resize_pipefd[0] >= 0) close(global.resize_pipefd[0]); - if (global.resize_pipefd[1] >= 0) close(global.resize_pipefd[1]); - - cellbuf_free(&global.back); - cellbuf_free(&global.front); - bytebuf_free(&global.in); - bytebuf_free(&global.out); - - if (global.terminfo) tb_free(global.terminfo); - - cap_trie_deinit(&global.cap_trie); - - tb_reset(); - return TB_OK; -} - -static int load_terminfo(void) { - int rv; - char tmp[TB_PATH_MAX]; - - // See terminfo(5) "Fetching Compiled Descriptions" for a description of - // this behavior. Some of these paths are compile-time ncurses options, so - // best guesses are used here. - const char *term = getenv("TERM"); - if (!term) { - return TB_ERR; - } - - // If TERMINFO is set, try that directory and stop - const char *terminfo = getenv("TERMINFO"); - if (terminfo) { - return load_terminfo_from_path(terminfo, term); - } - - // Next try ~/.terminfo - const char *home = getenv("HOME"); - if (home) { - snprintf_or_return(rv, tmp, sizeof(tmp), "%s/.terminfo", home); - if_ok_return(rv, load_terminfo_from_path(tmp, term)); - } - - // Next try TERMINFO_DIRS - // - // Note, empty entries are supposed to be interpretted as the "compiled-in - // default", which is of course system-dependent. Previously /etc/terminfo - // was used here. Let's skip empty entries altogether rather than give - // precedence to a guess, and check common paths after this loop. - const char *dirs = getenv("TERMINFO_DIRS"); - if (dirs) { - snprintf_or_return(rv, tmp, sizeof(tmp), "%s", dirs); - char *dir = strtok(tmp, ":"); - while (dir) { - const char *cdir = dir; - if (*cdir != '\0') { - if_ok_return(rv, load_terminfo_from_path(cdir, term)); - } - dir = strtok(NULL, ":"); - } - } - -#ifdef TB_TERMINFO_DIR - if_ok_return(rv, load_terminfo_from_path(TB_TERMINFO_DIR, term)); -#endif - if_ok_return(rv, load_terminfo_from_path("/usr/local/etc/terminfo", term)); - if_ok_return(rv, - load_terminfo_from_path("/usr/local/share/terminfo", term)); - if_ok_return(rv, load_terminfo_from_path("/usr/local/lib/terminfo", term)); - if_ok_return(rv, load_terminfo_from_path("/etc/terminfo", term)); - if_ok_return(rv, load_terminfo_from_path("/usr/share/terminfo", term)); - if_ok_return(rv, load_terminfo_from_path("/usr/lib/terminfo", term)); - if_ok_return(rv, load_terminfo_from_path("/usr/share/lib/terminfo", term)); - if_ok_return(rv, load_terminfo_from_path("/lib/terminfo", term)); - - return TB_ERR; -} - -static int load_terminfo_from_path(const char *path, const char *term) { - int rv; - char tmp[TB_PATH_MAX]; - - // Look for term at this terminfo location, e.g., /x/xterm - snprintf_or_return(rv, tmp, sizeof(tmp), "%s/%c/%s", path, term[0], term); - if_ok_return(rv, read_terminfo_path(tmp)); - -#ifdef __APPLE__ - // Try the Darwin equivalent path, e.g., /78/xterm - snprintf_or_return(rv, tmp, sizeof(tmp), "%s/%x/%s", path, term[0], term); - return read_terminfo_path(tmp); -#endif - - return TB_ERR; -} - -static int read_terminfo_path(const char *path) { - FILE *fp = fopen(path, "rb"); - if (!fp) { - return TB_ERR; - } - - struct stat st; - if (fstat(fileno(fp), &st) != 0) { - fclose(fp); - return TB_ERR; - } - - size_t fsize = st.st_size; - char *data = tb_malloc(fsize); - if (!data) { - fclose(fp); - return TB_ERR; - } - - if (fread(data, 1, fsize, fp) != fsize) { - fclose(fp); - tb_free(data); - return TB_ERR; - } - - global.terminfo = data; - global.nterminfo = fsize; - - fclose(fp); - return TB_OK; -} - -static int parse_terminfo_caps(void) { - // See term(5) "LEGACY STORAGE FORMAT" and "EXTENDED STORAGE FORMAT" for a - // description of this behavior. - - // Ensure there's at least a header's worth of data - if (global.nterminfo < 6) { - return TB_ERR; - } - - int16_t *header = (int16_t *)global.terminfo; - // header[0] the magic number (octal 0432 or 01036) - // header[1] the size, in bytes, of the names section - // header[2] the number of bytes in the boolean section - // header[3] the number of short integers in the numbers section - // header[4] the number of offsets (short integers) in the strings section - // header[5] the size, in bytes, of the string table - - // Legacy ints are 16-bit, extended ints are 32-bit - const int bytes_per_int = header[0] == 01036 ? 4 // 32-bit - : 2; // 16-bit - - // > Between the boolean section and the number section, a null byte will be - // > inserted, if necessary, to ensure that the number section begins on an - // > even byte - const int align_offset = (header[1] + header[2]) % 2 != 0 ? 1 : 0; - - const int pos_str_offsets = - (6 * sizeof(int16_t)) // header (12 bytes) - + header[1] // length of names section - + header[2] // length of boolean section - + align_offset + - (header[3] * bytes_per_int); // length of numbers section - - const int pos_str_table = - pos_str_offsets + - (header[4] * sizeof(int16_t)); // length of string offsets table - - // Load caps - int i; - for (i = 0; i < TB_CAP__COUNT; i++) { - const char *cap = get_terminfo_string(pos_str_offsets, header[4], - pos_str_table, header[5], terminfo_cap_indexes[i]); - if (!cap) { - // Something is not right - return TB_ERR; - } - global.caps[i] = cap; - } - - return TB_OK; -} - -static int load_builtin_caps(void) { - int i, j; - const char *term = getenv("TERM"); - - if (!term) { - return TB_ERR_NO_TERM; - } - - // Check for exact TERM match - for (i = 0; builtin_terms[i].name != NULL; i++) { - if (strcmp(term, builtin_terms[i].name) == 0) { - for (j = 0; j < TB_CAP__COUNT; j++) { - global.caps[j] = builtin_terms[i].caps[j]; - } - return TB_OK; - } - } - - // Check for partial TERM or alias match - for (i = 0; builtin_terms[i].name != NULL; i++) { - if (strstr(term, builtin_terms[i].name) != NULL || - (*(builtin_terms[i].alias) != '\0' && - strstr(term, builtin_terms[i].alias) != NULL)) - { - for (j = 0; j < TB_CAP__COUNT; j++) { - global.caps[j] = builtin_terms[i].caps[j]; - } - return TB_OK; - } - } - - return TB_ERR_UNSUPPORTED_TERM; -} - -static const char *get_terminfo_string(int16_t str_offsets_pos, - int16_t str_offsets_len, int16_t str_table_pos, int16_t str_table_len, - int16_t str_index) { - const int str_byte_index = (int)str_index * (int)sizeof(int16_t); - if (str_byte_index >= (int)str_offsets_len * (int)sizeof(int16_t)) { - // An offset beyond the table indicates absent - // See `convert_strings` in tinfo `read_entry.c` - return ""; - } - const int16_t *str_offset = - (int16_t *)(global.terminfo + (int)str_offsets_pos + str_byte_index); - if ((char *)str_offset >= global.terminfo + global.nterminfo) { - // str_offset points beyond end of entry - // Truncated/corrupt terminfo entry? - return NULL; - } - if (*str_offset < 0 || *str_offset >= str_table_len) { - // A negative offset indicates absent - // An offset beyond the table indicates absent - // See `convert_strings` in tinfo `read_entry.c` - return ""; - } - if (((size_t)((int)str_table_pos + (int)*str_offset)) >= global.nterminfo) { - // string points beyond end of entry - // Truncated/corrupt terminfo entry? - return NULL; - } - return ( - const char *)(global.terminfo + (int)str_table_pos + (int)*str_offset); -} - -static int wait_event(struct tb_event *event, int timeout) { - int rv; - char buf[TB_OPT_READ_BUF]; - - memset(event, 0, sizeof(*event)); - if_ok_return(rv, extract_event(event)); - - fd_set fds; - struct timeval tv; - tv.tv_sec = timeout / 1000; - tv.tv_usec = (timeout - (tv.tv_sec * 1000)) * 1000; - - do { - FD_ZERO(&fds); - FD_SET(global.rfd, &fds); - FD_SET(global.resize_pipefd[0], &fds); - - int maxfd = global.resize_pipefd[0] > global.rfd - ? global.resize_pipefd[0] - : global.rfd; - - int select_rv = - select(maxfd + 1, &fds, NULL, NULL, (timeout < 0) ? NULL : &tv); - - if (select_rv < 0) { - // Let EINTR/EAGAIN bubble up - global.last_errno = errno; - return TB_ERR_POLL; - } else if (select_rv == 0) { - return TB_ERR_NO_EVENT; - } - - int tty_has_events = (FD_ISSET(global.rfd, &fds)); - int resize_has_events = (FD_ISSET(global.resize_pipefd[0], &fds)); - - if (tty_has_events) { - ssize_t read_rv = read(global.rfd, buf, sizeof(buf)); - if (read_rv < 0) { - global.last_errno = errno; - return TB_ERR_READ; - } else if (read_rv > 0) { - bytebuf_nputs(&global.in, buf, read_rv); - } - } - - if (resize_has_events) { - int ignore = 0; - read(global.resize_pipefd[0], &ignore, sizeof(ignore)); - // TODO Harden against errors encountered mid-resize - if_err_return(rv, update_term_size()); - if_err_return(rv, resize_cellbufs()); - event->type = TB_EVENT_RESIZE; - event->w = global.width; - event->h = global.height; - return TB_OK; - } - - memset(event, 0, sizeof(*event)); - if_ok_return(rv, extract_event(event)); - } while (timeout == -1); - - return rv; -} - -static int extract_event(struct tb_event *event) { - int rv; - struct bytebuf_t *in = &global.in; - - if (in->len == 0) { - return TB_ERR; - } - - if (in->buf[0] == '\x1b') { - // Escape sequence? - // In TB_INPUT_ESC, skip if the buffer is a single escape char - if (!((global.input_mode & TB_INPUT_ESC) && in->len == 1)) { - if_ok_or_need_more_return(rv, extract_esc(event)); - } - - // Escape key? - if (global.input_mode & TB_INPUT_ESC) { - event->type = TB_EVENT_KEY; - event->ch = 0; - event->key = TB_KEY_ESC; - event->mod = 0; - bytebuf_shift(in, 1); - return TB_OK; - } - - // Recurse for alt key - event->mod |= TB_MOD_ALT; - bytebuf_shift(in, 1); - return extract_event(event); - } - - // ASCII control key? - if ((uint16_t)in->buf[0] < TB_KEY_SPACE || in->buf[0] == TB_KEY_BACKSPACE2) - { - event->type = TB_EVENT_KEY; - event->ch = 0; - event->key = (uint16_t)in->buf[0]; - event->mod |= TB_MOD_CTRL; - bytebuf_shift(in, 1); - return TB_OK; - } - - // UTF-8? - if (in->len >= (size_t)tb_utf8_char_length(in->buf[0])) { - event->type = TB_EVENT_KEY; - tb_utf8_char_to_unicode(&event->ch, in->buf); - event->key = 0; - bytebuf_shift(in, tb_utf8_char_length(in->buf[0])); - return TB_OK; - } - - // Need more input - return TB_ERR; -} - -static int extract_esc(struct tb_event *event) { - int rv; - if_ok_or_need_more_return(rv, extract_esc_user(event, 0)); - if_ok_or_need_more_return(rv, extract_esc_cap(event)); - if_ok_or_need_more_return(rv, extract_esc_mouse(event)); - if_ok_or_need_more_return(rv, extract_esc_user(event, 1)); - return TB_ERR; -} - -static int extract_esc_user(struct tb_event *event, int is_post) { - int rv; - size_t consumed = 0; - struct bytebuf_t *in = &global.in; - int (*fn)(struct tb_event *, size_t *); - - fn = is_post ? global.fn_extract_esc_post : global.fn_extract_esc_pre; - - if (!fn) { - return TB_ERR; - } - - rv = fn(event, &consumed); - if (rv == TB_OK) { - bytebuf_shift(in, consumed); - } - - if_ok_or_need_more_return(rv, rv); - return TB_ERR; -} - -static int extract_esc_cap(struct tb_event *event) { - int rv; - struct bytebuf_t *in = &global.in; - struct cap_trie_t *node; - size_t depth; - - if_err_return(rv, cap_trie_find(in->buf, in->len, &node, &depth)); - if (node->is_leaf) { - // Found a leaf node - event->type = TB_EVENT_KEY; - event->ch = 0; - event->key = node->key; - event->mod = node->mod; - bytebuf_shift(in, depth); - return TB_OK; - } else if (node->nchildren > 0 && in->len <= depth) { - // Found a branch node (not enough input) - return TB_ERR_NEED_MORE; - } - - return TB_ERR; -} - -static int extract_esc_mouse(struct tb_event *event) { - struct bytebuf_t *in = &global.in; - - enum type { TYPE_VT200 = 0, TYPE_1006, TYPE_1015, TYPE_MAX }; - - const char *cmp[TYPE_MAX] = {// - // X10 mouse encoding, the simplest one - // \x1b [ M Cb Cx Cy - [TYPE_VT200] = "\x1b[M", - // xterm 1006 extended mode or urxvt 1015 extended mode - // xterm: \x1b [ < Cb ; Cx ; Cy (M or m) - [TYPE_1006] = "\x1b[<", - // urxvt: \x1b [ Cb ; Cx ; Cy M - [TYPE_1015] = "\x1b["}; - - enum type type = 0; - int ret = TB_ERR; - - // Unrolled at compile-time (probably) - for (; type < TYPE_MAX; type++) { - size_t size = strlen(cmp[type]); - - if (in->len >= size && (strncmp(cmp[type], in->buf, size)) == 0) { - break; - } - } - - if (type == TYPE_MAX) { - ret = TB_ERR; // No match - return ret; - } - - size_t buf_shift = 0; - - switch (type) { - case TYPE_VT200: - if (in->len >= 6) { - int b = in->buf[3] - 0x20; - int fail = 0; - - switch (b & 3) { - case 0: - event->key = ((b & 64) != 0) ? TB_KEY_MOUSE_WHEEL_UP - : TB_KEY_MOUSE_LEFT; - break; - case 1: - event->key = ((b & 64) != 0) ? TB_KEY_MOUSE_WHEEL_DOWN - : TB_KEY_MOUSE_MIDDLE; - break; - case 2: - event->key = TB_KEY_MOUSE_RIGHT; - break; - case 3: - event->key = TB_KEY_MOUSE_RELEASE; - break; - default: - ret = TB_ERR; - fail = 1; - break; - } - - if (!fail) { - if ((b & 32) != 0) { - event->mod |= TB_MOD_MOTION; - } - - // the coord is 1,1 for upper left - event->x = ((uint8_t)in->buf[4]) - 0x21; - event->y = ((uint8_t)in->buf[5]) - 0x21; - - ret = TB_OK; - } - - buf_shift = 6; - } - break; - case TYPE_1006: - // fallthrough - case TYPE_1015: { - size_t index_fail = (size_t)-1; - - enum { - FIRST_M = 0, - FIRST_SEMICOLON, - LAST_SEMICOLON, - FIRST_LAST_MAX - }; - - size_t indices[FIRST_LAST_MAX] = {index_fail, index_fail, - index_fail}; - int m_is_capital = 0; - - for (size_t i = 0; i < in->len; i++) { - if (in->buf[i] == ';') { - if (indices[FIRST_SEMICOLON] == index_fail) { - indices[FIRST_SEMICOLON] = i; - } else { - indices[LAST_SEMICOLON] = i; - } - } else if (indices[FIRST_M] == index_fail) { - if (in->buf[i] == 'm' || in->buf[i] == 'M') { - m_is_capital = (in->buf[i] == 'M'); - indices[FIRST_M] = i; - } - } - } - - if (indices[FIRST_M] == index_fail || - indices[FIRST_SEMICOLON] == index_fail || - indices[LAST_SEMICOLON] == index_fail) - { - ret = TB_ERR; - } else { - int start = (type == TYPE_1015 ? 2 : 3); - - unsigned n1 = strtoul(&in->buf[start], NULL, 10); - unsigned n2 = - strtoul(&in->buf[indices[FIRST_SEMICOLON] + 1], NULL, 10); - unsigned n3 = - strtoul(&in->buf[indices[LAST_SEMICOLON] + 1], NULL, 10); - - if (type == TYPE_1015) { - n1 -= 0x20; - } - - int fail = 0; - - switch (n1 & 3) { - case 0: - event->key = ((n1 & 64) != 0) ? TB_KEY_MOUSE_WHEEL_UP - : TB_KEY_MOUSE_LEFT; - break; - case 1: - event->key = ((n1 & 64) != 0) ? TB_KEY_MOUSE_WHEEL_DOWN - : TB_KEY_MOUSE_MIDDLE; - break; - case 2: - event->key = TB_KEY_MOUSE_RIGHT; - break; - case 3: - event->key = TB_KEY_MOUSE_RELEASE; - break; - default: - ret = TB_ERR; - fail = 1; - break; - } - - buf_shift = in->len; - - if (!fail) { - if (!m_is_capital) { - // on xterm mouse release is signaled by lowercase m - event->key = TB_KEY_MOUSE_RELEASE; - } - - if ((n1 & 32) != 0) { - event->mod |= TB_MOD_MOTION; - } - - event->x = ((uint8_t)n2) - 1; - event->y = ((uint8_t)n3) - 1; - - ret = TB_OK; - } - } - } break; - case TYPE_MAX: - ret = TB_ERR; - } - - if (buf_shift > 0) { - bytebuf_shift(in, buf_shift); - } - - if (ret == TB_OK) { - event->type = TB_EVENT_MOUSE; - } - - return ret; -} - -static int resize_cellbufs(void) { - int rv; - if_err_return(rv, - cellbuf_resize(&global.back, global.width, global.height)); - if_err_return(rv, - cellbuf_resize(&global.front, global.width, global.height)); - if_err_return(rv, cellbuf_clear(&global.front)); - if_err_return(rv, send_clear()); - return TB_OK; -} - -static void handle_resize(int sig) { - int errno_copy = errno; - write(global.resize_pipefd[1], &sig, sizeof(sig)); - errno = errno_copy; -} - -static int send_attr(uintattr_t fg, uintattr_t bg) { - int rv; - - if (fg == global.last_fg && bg == global.last_bg) { - return TB_OK; - } - - if_err_return(rv, bytebuf_puts(&global.out, global.caps[TB_CAP_SGR0])); - - uint32_t cfg, cbg; - switch (global.output_mode) { - default: - case TB_OUTPUT_NORMAL: - // The minus 1 below is because our colors are 1-indexed starting - // from black. Black is represented by a 30, 40, 90, or 100 for fg, - // bg, bright fg, or bright bg respectively. Red is 31, 41, 91, - // 101, etc. - cfg = (fg & TB_BRIGHT ? 90 : 30) + (fg & 0x0f) - 1; - cbg = (bg & TB_BRIGHT ? 100 : 40) + (bg & 0x0f) - 1; - break; - - case TB_OUTPUT_256: - cfg = fg & 0xff; - cbg = bg & 0xff; - if (fg & TB_HI_BLACK) cfg = 0; - if (bg & TB_HI_BLACK) cbg = 0; - break; - - case TB_OUTPUT_216: - cfg = fg & 0xff; - cbg = bg & 0xff; - if (cfg > 216) cfg = 216; - if (cbg > 216) cbg = 216; - cfg += 0x0f; - cbg += 0x0f; - break; - - case TB_OUTPUT_GRAYSCALE: - cfg = fg & 0xff; - cbg = bg & 0xff; - if (cfg > 24) cfg = 24; - if (cbg > 24) cbg = 24; - cfg += 0xe7; - cbg += 0xe7; - break; - -#if TB_OPT_ATTR_W >= 32 - case TB_OUTPUT_TRUECOLOR: - cfg = fg & 0xffffff; - cbg = bg & 0xffffff; - if (fg & TB_HI_BLACK) cfg = 0; - if (bg & TB_HI_BLACK) cbg = 0; - break; -#endif - } - - if (fg & TB_BOLD) - if_err_return(rv, bytebuf_puts(&global.out, global.caps[TB_CAP_BOLD])); - - if (fg & TB_BLINK) - if_err_return(rv, bytebuf_puts(&global.out, global.caps[TB_CAP_BLINK])); - - if (fg & TB_UNDERLINE) - if_err_return(rv, - bytebuf_puts(&global.out, global.caps[TB_CAP_UNDERLINE])); - - if (fg & TB_ITALIC) - if_err_return(rv, - bytebuf_puts(&global.out, global.caps[TB_CAP_ITALIC])); - - if (fg & TB_DIM) - if_err_return(rv, bytebuf_puts(&global.out, global.caps[TB_CAP_DIM])); - -#if TB_OPT_ATTR_W == 64 - if (fg & TB_STRIKEOUT) - if_err_return(rv, bytebuf_puts(&global.out, TB_HARDCAP_STRIKEOUT)); - - if (fg & TB_UNDERLINE_2) - if_err_return(rv, bytebuf_puts(&global.out, TB_HARDCAP_UNDERLINE_2)); - - if (fg & TB_OVERLINE) - if_err_return(rv, bytebuf_puts(&global.out, TB_HARDCAP_OVERLINE)); - - if (fg & TB_INVISIBLE) - if_err_return(rv, - bytebuf_puts(&global.out, global.caps[TB_CAP_INVISIBLE])); -#endif - - if ((fg & TB_REVERSE) || (bg & TB_REVERSE)) - if_err_return(rv, - bytebuf_puts(&global.out, global.caps[TB_CAP_REVERSE])); - - int fg_is_default = (fg & 0xff) == 0; - int bg_is_default = (bg & 0xff) == 0; - if (global.output_mode == TB_OUTPUT_256) { - if (fg & TB_HI_BLACK) fg_is_default = 0; - if (bg & TB_HI_BLACK) bg_is_default = 0; - } -#if TB_OPT_ATTR_W >= 32 - if (global.output_mode == TB_OUTPUT_TRUECOLOR) { - fg_is_default = ((fg & 0xffffff) == 0) && ((fg & TB_HI_BLACK) == 0); - bg_is_default = ((bg & 0xffffff) == 0) && ((bg & TB_HI_BLACK) == 0); - } -#endif - - if_err_return(rv, send_sgr(cfg, cbg, fg_is_default, bg_is_default)); - - global.last_fg = fg; - global.last_bg = bg; - - return TB_OK; -} - -static int send_sgr(uint32_t cfg, uint32_t cbg, int fg_is_default, - int bg_is_default) { - int rv; - char nbuf[32]; - - if (fg_is_default && bg_is_default) { - return TB_OK; - } - - switch (global.output_mode) { - default: - case TB_OUTPUT_NORMAL: - send_literal(rv, "\x1b["); - if (!fg_is_default) { - send_num(rv, nbuf, cfg); - if (!bg_is_default) { - send_literal(rv, ";"); - } - } - if (!bg_is_default) { - send_num(rv, nbuf, cbg); - } - send_literal(rv, "m"); - break; - - case TB_OUTPUT_256: - case TB_OUTPUT_216: - case TB_OUTPUT_GRAYSCALE: - send_literal(rv, "\x1b["); - if (!fg_is_default) { - send_literal(rv, "38;5;"); - send_num(rv, nbuf, cfg); - if (!bg_is_default) { - send_literal(rv, ";"); - } - } - if (!bg_is_default) { - send_literal(rv, "48;5;"); - send_num(rv, nbuf, cbg); - } - send_literal(rv, "m"); - break; - -#if TB_OPT_ATTR_W >= 32 - case TB_OUTPUT_TRUECOLOR: - send_literal(rv, "\x1b["); - if (!fg_is_default) { - send_literal(rv, "38;2;"); - send_num(rv, nbuf, (cfg >> 16) & 0xff); - send_literal(rv, ";"); - send_num(rv, nbuf, (cfg >> 8) & 0xff); - send_literal(rv, ";"); - send_num(rv, nbuf, cfg & 0xff); - if (!bg_is_default) { - send_literal(rv, ";"); - } - } - if (!bg_is_default) { - send_literal(rv, "48;2;"); - send_num(rv, nbuf, (cbg >> 16) & 0xff); - send_literal(rv, ";"); - send_num(rv, nbuf, (cbg >> 8) & 0xff); - send_literal(rv, ";"); - send_num(rv, nbuf, cbg & 0xff); - } - send_literal(rv, "m"); - break; -#endif - } - return TB_OK; -} - -static int send_cursor_if(int x, int y) { - int rv; - char nbuf[32]; - if (x < 0 || y < 0) { - return TB_OK; - } - send_literal(rv, "\x1b["); - send_num(rv, nbuf, y + 1); - send_literal(rv, ";"); - send_num(rv, nbuf, x + 1); - send_literal(rv, "H"); - return TB_OK; -} - -static int send_char(int x, int y, uint32_t ch) { - return send_cluster(x, y, &ch, 1); -} - -static int send_cluster(int x, int y, uint32_t *ch, size_t nch) { - int rv; - char chu8[8]; - - if (global.last_x != x - 1 || global.last_y != y) { - if_err_return(rv, send_cursor_if(x, y)); - } - global.last_x = x; - global.last_y = y; - - int i; - for (i = 0; i < (int)nch; i++) { - uint32_t ch32 = *(ch + i); - int chu8_len; - if (ch32 == 0) { // replace null with space (from termbox 19dbee5) - chu8_len = 1; - chu8[0] = ' '; - } else { - chu8_len = tb_utf8_unicode_to_char(chu8, ch32); - } - if_err_return(rv, bytebuf_nputs(&global.out, chu8, (size_t)chu8_len)); - } - - return TB_OK; -} - -static int convert_num(uint32_t num, char *buf) { - int i, l = 0; - char ch; - do { - /* '0' = 48; 48 + num%10 < 58 < MAX_8bitCHAR */ - buf[l++] = (char)('0' + (num % 10)); - num /= 10; - } while (num); - for (i = 0; i < l / 2; i++) { - ch = buf[i]; - buf[i] = buf[l - 1 - i]; - buf[l - 1 - i] = ch; - } - return l; -} - -static int cell_cmp(struct tb_cell *a, struct tb_cell *b) { - if (a->ch != b->ch || a->fg != b->fg || a->bg != b->bg) { - return 1; - } -#ifdef TB_OPT_EGC - if (a->nech != b->nech) { - return 1; - } else if (a->nech > 0) { // a->nech == b->nech - return memcmp(a->ech, b->ech, a->nech); - } -#endif - return 0; -} - -static int cell_copy(struct tb_cell *dst, struct tb_cell *src) { -#ifdef TB_OPT_EGC - if (src->nech > 0) { - return cell_set(dst, src->ech, src->nech, src->fg, src->bg); - } -#endif - return cell_set(dst, &src->ch, 1, src->fg, src->bg); -} - -static int cell_set(struct tb_cell *cell, uint32_t *ch, size_t nch, - uintattr_t fg, uintattr_t bg) { - cell->ch = ch ? *ch : 0; - cell->fg = fg; - cell->bg = bg; -#ifdef TB_OPT_EGC - if (nch <= 1) { - cell->nech = 0; - } else { - int rv; - if_err_return(rv, cell_reserve_ech(cell, nch + 1)); - memcpy(cell->ech, ch, sizeof(ch) * nch); - cell->ech[nch] = '\0'; - cell->nech = nch; - } -#else - (void)nch; - (void)cell_reserve_ech; -#endif - return TB_OK; -} - -static int cell_reserve_ech(struct tb_cell *cell, size_t n) { -#ifdef TB_OPT_EGC - if (cell->cech >= n) { - return TB_OK; - } - if (!(cell->ech = tb_realloc(cell->ech, n * sizeof(cell->ch)))) { - return TB_ERR_MEM; - } - cell->cech = n; - return TB_OK; -#else - (void)cell; - (void)n; - return TB_ERR; -#endif -} - -static int cell_free(struct tb_cell *cell) { -#ifdef TB_OPT_EGC - if (cell->ech) { - tb_free(cell->ech); - } -#endif - memset(cell, 0, sizeof(*cell)); - return TB_OK; -} - -static int cellbuf_init(struct cellbuf_t *c, int w, int h) { - c->cells = tb_malloc(sizeof(struct tb_cell) * w * h); - if (!c->cells) { - return TB_ERR_MEM; - } - memset(c->cells, 0, sizeof(struct tb_cell) * w * h); - c->width = w; - c->height = h; - return TB_OK; -} - -static int cellbuf_free(struct cellbuf_t *c) { - if (c->cells) { - int i; - for (i = 0; i < c->width * c->height; i++) { - cell_free(&c->cells[i]); - } - tb_free(c->cells); - } - memset(c, 0, sizeof(*c)); - return TB_OK; -} - -static int cellbuf_clear(struct cellbuf_t *c) { - int rv, i; - uint32_t space = (uint32_t)' '; - for (i = 0; i < c->width * c->height; i++) { - if_err_return(rv, - cell_set(&c->cells[i], &space, 1, global.fg, global.bg)); - } - return TB_OK; -} - -static int cellbuf_get(struct cellbuf_t *c, int x, int y, - struct tb_cell **out) { - if (x < 0 || x >= c->width || y < 0 || y >= c->height) { - *out = NULL; - return TB_ERR_OUT_OF_BOUNDS; - } - *out = &c->cells[(y * c->width) + x]; - return TB_OK; -} - -static int cellbuf_resize(struct cellbuf_t *c, int w, int h) { - int rv; - - int ow = c->width; - int oh = c->height; - - if (ow == w && oh == h) { - return TB_OK; - } - - w = w < 1 ? 1 : w; - h = h < 1 ? 1 : h; - - int minw = (w < ow) ? w : ow; - int minh = (h < oh) ? h : oh; - - struct tb_cell *prev = c->cells; - - if_err_return(rv, cellbuf_init(c, w, h)); - if_err_return(rv, cellbuf_clear(c)); - - int x, y; - for (x = 0; x < minw; x++) { - for (y = 0; y < minh; y++) { - struct tb_cell *src, *dst; - src = &prev[(y * ow) + x]; - if_err_return(rv, cellbuf_get(c, x, y, &dst)); - if_err_return(rv, cell_copy(dst, src)); - } - } - - tb_free(prev); - - return TB_OK; -} - -static int bytebuf_puts(struct bytebuf_t *b, const char *str) { - if (!str || strlen(str) <= 0) return TB_OK; // Nothing to do for empty caps - return bytebuf_nputs(b, str, (size_t)strlen(str)); -} - -static int bytebuf_nputs(struct bytebuf_t *b, const char *str, size_t nstr) { - int rv; - if_err_return(rv, bytebuf_reserve(b, b->len + nstr + 1)); - memcpy(b->buf + b->len, str, nstr); - b->len += nstr; - b->buf[b->len] = '\0'; - return TB_OK; -} - -static int bytebuf_shift(struct bytebuf_t *b, size_t n) { - if (n > b->len) { - n = b->len; - } - size_t nmove = b->len - n; - memmove(b->buf, b->buf + n, nmove); - b->len -= n; - return TB_OK; -} - -static int bytebuf_flush(struct bytebuf_t *b, int fd) { - if (b->len <= 0) { - return TB_OK; - } - ssize_t write_rv = write(fd, b->buf, b->len); - if (write_rv < 0 || (size_t)write_rv != b->len) { - // Note, errno will be 0 on partial write - global.last_errno = errno; - return TB_ERR; - } - b->len = 0; - return TB_OK; -} - -static int bytebuf_reserve(struct bytebuf_t *b, size_t sz) { - if (b->cap >= sz) { - return TB_OK; - } - size_t newcap = b->cap > 0 ? b->cap : 1; - while (newcap < sz) { - newcap *= 2; - } - char *newbuf; - if (b->buf) { - newbuf = tb_realloc(b->buf, newcap); - } else { - newbuf = tb_malloc(newcap); - } - if (!newbuf) { - return TB_ERR_MEM; - } - b->buf = newbuf; - b->cap = newcap; - return TB_OK; -} - -static int bytebuf_free(struct bytebuf_t *b) { - if (b->buf) { - tb_free(b->buf); - } - memset(b, 0, sizeof(*b)); - return TB_OK; -} - -#endif /* TB_IMPL */ diff --git a/install.zig b/install.zig new file mode 100644 index 0000000..b5649cd --- /dev/null +++ b/install.zig @@ -0,0 +1,445 @@ +const std = @import("std"); + +const PatchMap = std.StringHashMap([]const u8); +const InitSystem = enum { + systemd, + openrc, + runit, + s6, + dinit, + sysvinit, + freebsd, +}; +const InstallType = enum { + installexe, + installnoconf, + uninstallexe, + uninstallnoconf, +}; + +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; + +pub fn main(init: std.process.Init) !void { + const io = init.io; + const allocator = init.gpa; + + var args = init.minimal.args.iterate(); + if (!args.skip()) return error.NoProgramName; + + const install_type = std.meta.stringToEnum(InstallType, args.next().?).?; + dest_directory = args.next().?; + config_directory = args.next().?; + prefix_directory = args.next().?; + executable_name = args.next().?; + init_system = std.meta.stringToEnum(InitSystem, args.next().?).?; + const default_tty_str = args.next().?; + + switch (install_type) { + .installexe, .installnoconf => { + 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); + + try installLy(allocator, io, patch_map, install_type == .installexe); + try installService(allocator, io, patch_map); + }, + .uninstallexe, .uninstallnoconf => { + if (install_type == .uninstallexe) { + try deleteTree(allocator, io, config_directory, "/ly", "ly config directory not found"); + } + + const exe_path = try std.Io.Dir.path.join(allocator, &[_][]const u8{ prefix_directory, "/bin/", executable_name }); + defer allocator.free(exe_path); + + var success = true; + std.Io.Dir.cwd().deleteFile(io, 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, io, config_directory, "/pam.d/ly", "ly pam file not found"); + + switch (init_system) { + .systemd => try deleteFile(allocator, io, prefix_directory, "/lib/systemd/system/ly@.service", "systemd service not found"), + .openrc => try deleteFile(allocator, io, config_directory, "/init.d/ly", "openrc service not found"), + .runit => try deleteTree(allocator, io, config_directory, "/sv/ly", "runit service not found"), + .s6 => { + try deleteTree(allocator, io, config_directory, "/s6/sv/ly-srv", "s6 service not found"); + try deleteFile(allocator, io, config_directory, "/s6/adminsv/default/contents.d/ly-srv", "s6 admin service not found"); + }, + .dinit => try deleteFile(allocator, io, config_directory, "/dinit.d/ly", "dinit service not found"), + .sysvinit => try deleteFile(allocator, io, config_directory, "/init.d/ly", "sysvinit service not found"), + .freebsd => try deleteFile(allocator, io, prefix_directory, "/bin/ly_wrapper", "freebsd wrapper not found"), + } + }, + } +} + +fn installLy(allocator: std.mem.Allocator, io: std.Io, patch_map: PatchMap, install_config: bool) !void { + const ly_config_directory = try std.Io.Dir.path.join(allocator, &[_][]const u8{ dest_directory, config_directory, "/ly" }); + defer allocator.free(ly_config_directory); + + std.Io.Dir.cwd().createDirPath(io, 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.Io.Dir.path.join(allocator, &[_][]const u8{ dest_directory, config_directory, "/ly/custom-sessions" }); + defer allocator.free(ly_custom_sessions_directory); + + std.Io.Dir.cwd().createDirPath(io, 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.Io.Dir.path.join(allocator, &[_][]const u8{ dest_directory, config_directory, "/ly/lang" }); + defer allocator.free(ly_lang_path); + + std.Io.Dir.cwd().createDirPath(io, ly_lang_path) catch { + std.debug.print("warn: {s} already exists as a directory.\n", .{ly_lang_path}); + }; + + { + const exe_path = try std.Io.Dir.path.join(allocator, &[_][]const u8{ dest_directory, prefix_directory, "/bin" }); + defer allocator.free(exe_path); + + std.Io.Dir.cwd().createDirPath(io, 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.Io.Dir.cwd().openDir(io, exe_path, .{}) catch unreachable; + defer executable_dir.close(io); + + try installFile(io, "zig-out/bin/ly", executable_dir, exe_path, executable_name, .{}); + } + + { + var config_dir = std.Io.Dir.cwd().openDir(io, ly_config_directory, .{}) catch unreachable; + defer config_dir.close(io); + + if (install_config) { + const patched_config = try patchFile(allocator, io, "res/config.ini", patch_map); + defer allocator.free(patched_config); + + try installText(io, patched_config, config_dir, ly_config_directory, "config.ini", .{}); + + try installFile(io, "res/startup.sh", config_dir, ly_config_directory, "startup.sh", .{ .permissions = .fromMode(0o755) }); + } + + const patched_example_config = try patchFile(allocator, io, "res/config.ini", patch_map); + defer allocator.free(patched_example_config); + + try installText(io, patched_example_config, config_dir, ly_config_directory, "config.ini.example", .{}); + + const patched_setup = try patchFile(allocator, io, "res/setup.sh", patch_map); + defer allocator.free(patched_setup); + + try installText(io, patched_setup, config_dir, ly_config_directory, "setup.sh", .{ .permissions = .fromMode(0o755) }); + + try installFile(io, "res/example.dur", config_dir, ly_config_directory, "example.dur", .{ .permissions = .fromMode(0o755) }); + + try installFile(io, "res/example.lua", config_dir, ly_config_directory, "example.lua", .{ .permissions = .fromMode(0o755) }); + } + + { + var custom_sessions_dir = std.Io.Dir.cwd().openDir(io, ly_custom_sessions_directory, .{}) catch unreachable; + defer custom_sessions_dir.close(io); + + const patched_readme = try patchFile(allocator, io, "res/custom-sessions/README", patch_map); + defer allocator.free(patched_readme); + + try installText(io, patched_readme, custom_sessions_dir, ly_custom_sessions_directory, "README", .{}); + } + + { + var lang_dir = std.Io.Dir.cwd().openDir(io, ly_lang_path, .{}) catch unreachable; + defer lang_dir.close(io); + + const languages = [_][]const u8{ + "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", + "pl.ini", + "pt.ini", + "pt_BR.ini", + "ro.ini", + "ru.ini", + "sr.ini", + "sr_Cyrl.ini", + "sv.ini", + "tr.ini", + "uk.ini", + "zh_CN.ini", + "zh_TW.ini", + }; + + inline for (languages) |language| { + try installFile(io, "res/lang/" ++ language, lang_dir, ly_lang_path, language, .{}); + } + } + + { + const pam_path = try std.Io.Dir.path.join(allocator, &[_][]const u8{ dest_directory, config_directory, "/pam.d" }); + defer allocator.free(pam_path); + + std.Io.Dir.cwd().createDirPath(io, 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.Io.Dir.cwd().openDir(io, pam_path, .{}) catch unreachable; + defer pam_dir.close(io); + + try installFile(io, if (init_system == .freebsd) "res/pam.d/ly-freebsd" else "res/pam.d/ly-linux", pam_dir, pam_path, "ly", .{ .permissions = .fromMode(0o644) }); + try installFile(io, if (init_system == .freebsd) "res/pam.d/ly-freebsd-autologin" else "res/pam.d/ly-linux-autologin", pam_dir, pam_path, "ly-autologin", .{ .permissions = .fromMode(0o644) }); + } +} + +fn installService(allocator: std.mem.Allocator, io: std.Io, patch_map: PatchMap) !void { + switch (init_system) { + .systemd => { + const service_path = try std.Io.Dir.path.join(allocator, &[_][]const u8{ dest_directory, prefix_directory, "/lib/systemd/system" }); + defer allocator.free(service_path); + + std.Io.Dir.cwd().createDirPath(io, service_path) catch {}; + var service_dir = std.Io.Dir.cwd().openDir(io, service_path, .{}) catch unreachable; + defer service_dir.close(io); + + const patched_service = try patchFile(allocator, io, "res/ly@.service", patch_map); + defer allocator.free(patched_service); + + try installText(io, patched_service, service_dir, service_path, "ly@.service", .{ .permissions = .fromMode(0o644) }); + + const patched_kmsconvt_service = try patchFile(allocator, io, "res/ly-kmsconvt@.service", patch_map); + defer allocator.free(patched_kmsconvt_service); + + try installText(io, patched_kmsconvt_service, service_dir, service_path, "ly-kmsconvt@.service", .{ .permissions = .fromMode(0o644) }); + }, + .openrc => { + const service_path = try std.Io.Dir.path.join(allocator, &[_][]const u8{ dest_directory, config_directory, "/init.d" }); + defer allocator.free(service_path); + + std.Io.Dir.cwd().createDirPath(io, service_path) catch {}; + var service_dir = std.Io.Dir.cwd().openDir(io, service_path, .{}) catch unreachable; + defer service_dir.close(io); + + const patched_service = try patchFile(allocator, io, "res/ly-openrc", patch_map); + defer allocator.free(patched_service); + + try installText(io, patched_service, service_dir, service_path, executable_name, .{ .permissions = .fromMode(0o755) }); + }, + .runit => { + const service_path = try std.Io.Dir.path.join(allocator, &[_][]const u8{ dest_directory, config_directory, "/sv/ly" }); + defer allocator.free(service_path); + + std.Io.Dir.cwd().createDirPath(io, service_path) catch {}; + var service_dir = std.Io.Dir.cwd().openDir(io, service_path, .{}) catch unreachable; + defer service_dir.close(io); + + const supervise_path = try std.Io.Dir.path.join(allocator, &[_][]const u8{ service_path, "supervise" }); + defer allocator.free(supervise_path); + + const patched_conf = try patchFile(allocator, io, "res/ly-runit-service/conf", patch_map); + defer allocator.free(patched_conf); + + try installText(io, patched_conf, service_dir, service_path, "conf", .{}); + + try installFile(io, "res/ly-runit-service/finish", service_dir, service_path, "finish", .{ .permissions = .fromMode(0o755) }); + + const patched_run = try patchFile(allocator, io, "res/ly-runit-service/run", patch_map); + defer allocator.free(patched_run); + + try installText(io, patched_run, service_dir, service_path, "run", .{ .permissions = .fromMode(0o755) }); + + std.Io.Dir.cwd().symLink(io, "/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.Io.Dir.path.join(allocator, &[_][]const u8{ dest_directory, config_directory, "/s6/adminsv/default/contents.d" }); + std.Io.Dir.cwd().createDirPath(io, admin_service_path) catch {}; + defer allocator.free(admin_service_path); + + var admin_service_dir = std.Io.Dir.cwd().openDir(io, admin_service_path, .{}) catch unreachable; + defer admin_service_dir.close(io); + + const file = try admin_service_dir.createFile(io, "ly-srv", .{}); + file.close(io); + + const service_path = try std.Io.Dir.path.join(allocator, &[_][]const u8{ dest_directory, config_directory, "/s6/sv/ly-srv" }); + defer allocator.free(service_path); + + std.Io.Dir.cwd().createDirPath(io, service_path) catch {}; + var service_dir = std.Io.Dir.cwd().openDir(io, service_path, .{}) catch unreachable; + defer service_dir.close(io); + + const patched_run = try patchFile(allocator, io, "res/ly-s6/run", patch_map); + defer allocator.free(patched_run); + + try installText(io, patched_run, service_dir, service_path, "run", .{ .permissions = .fromMode(0o755) }); + + try installFile(io, "res/ly-s6/type", service_dir, service_path, "type", .{}); + }, + .dinit => { + const service_path = try std.Io.Dir.path.join(allocator, &[_][]const u8{ dest_directory, config_directory, "/dinit.d" }); + defer allocator.free(service_path); + + std.Io.Dir.cwd().createDirPath(io, service_path) catch {}; + var service_dir = std.Io.Dir.cwd().openDir(io, service_path, .{}) catch unreachable; + defer service_dir.close(io); + + const patched_service = try patchFile(allocator, io, "res/ly-dinit", patch_map); + defer allocator.free(patched_service); + + try installText(io, patched_service, service_dir, service_path, "ly", .{}); + }, + .sysvinit => { + const service_path = try std.Io.Dir.path.join(allocator, &[_][]const u8{ dest_directory, config_directory, "/init.d" }); + defer allocator.free(service_path); + + std.Io.Dir.cwd().createDirPath(io, service_path) catch {}; + var service_dir = std.Io.Dir.cwd().openDir(io, service_path, .{}) catch unreachable; + defer service_dir.close(io); + + const patched_service = try patchFile(allocator, io, "res/ly-sysvinit", patch_map); + defer allocator.free(patched_service); + + try installText(io, patched_service, service_dir, service_path, "ly", .{ .permissions = .fromMode(0o755) }); + }, + .freebsd => { + const exe_path = try std.Io.Dir.path.join(allocator, &[_][]const u8{ dest_directory, prefix_directory, "/bin" }); + defer allocator.free(exe_path); + + var executable_dir = std.Io.Dir.cwd().openDir(io, exe_path, .{}) catch unreachable; + defer executable_dir.close(io); + + const patched_wrapper = try patchFile(allocator, io, "res/ly-freebsd-wrapper", patch_map); + defer allocator.free(patched_wrapper); + + try installText(io, patched_wrapper, executable_dir, exe_path, "ly_wrapper", .{ .permissions = .fromMode(0o755) }); + }, + } +} + +fn installFile( + io: std.Io, + source_file: []const u8, + destination_directory: std.Io.Dir, + destination_directory_path: []const u8, + destination_file: []const u8, + options: std.Io.Dir.CopyFileOptions, +) !void { + try std.Io.Dir.cwd().copyFile(source_file, destination_directory, destination_file, io, options); + std.debug.print("info: installed {s}/{s}\n", .{ destination_directory_path, destination_file }); +} + +fn patchFile(allocator: std.mem.Allocator, io: std.Io, source_file: []const u8, patch_map: PatchMap) ![]const u8 { + var file = try std.Io.Dir.cwd().openFile(io, source_file, .{}); + defer file.close(io); + + const stat = try file.stat(io); + + var buffer: [4096]u8 = undefined; + var reader = file.reader(io, &buffer); + var text = try reader.interface.readAlloc(allocator, @intCast(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( + io: std.Io, + text: []const u8, + destination_directory: std.Io.Dir, + destination_directory_path: []const u8, + destination_file: []const u8, + options: std.Io.File.CreateFlags, +) !void { + var file = try destination_directory.createFile(io, destination_file, options); + defer file.close(io); + + var buffer: [1024]u8 = undefined; + var writer = file.writer(io, &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, + io: std.Io, + prefix: []const u8, + file: []const u8, + warning: []const u8, +) !void { + const path = try std.Io.Dir.path.join(allocator, &[_][]const u8{ dest_directory, prefix, file }); + defer allocator.free(path); + + std.Io.Dir.cwd().deleteFile(io, 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, + io: std.Io, + prefix: []const u8, + directory: []const u8, + warning: []const u8, +) !void { + const path = try std.Io.Dir.path.join(allocator, &[_][]const u8{ dest_directory, prefix, directory }); + defer allocator.free(path); + + var dir = std.Io.Dir.cwd().openDir(io, path, .{}) catch |err| { + if (err == error.FileNotFound) { + std.debug.print("warn: {s}\n", .{warning}); + return; + } + + return err; + }; + dir.close(io); + + try std.Io.Dir.cwd().deleteTree(io, path); + + std.debug.print("info: deleted {s}\n", .{path}); +} diff --git a/ly-core/build.zig b/ly-core/build.zig new file mode 100644 index 0000000..f62fb2e --- /dev/null +++ b/ly-core/build.zig @@ -0,0 +1,82 @@ +const std = @import("std"); +const Translator = @import("translate_c").Translator; + +pub fn build(b: *std.Build) void { + const target = b.standardTargetOptions(.{}); + const optimize = b.standardOptimizeOption(.{}); + const enable_x11_support = b.option(bool, "enable_x11_support", "Enable X11 support") orelse true; + const mod = b.addModule("ly-core", .{ + .root_source_file = b.path("src/root.zig"), + .target = target, + .optimize = optimize, + }); + + 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").?; + 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").?; + const build_options = b.addOptions(); + 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); + mod.addOptions("build_options", build_options); + + const zigini = b.dependency("zigini", .{ .target = target, .optimize = optimize }); + mod.addImport("zigini", zigini.module("zigini")); + + const translate_c = b.dependency("translate_c", .{ + .target = target, + }); + + addCImport(b, mod, translate_c, target, optimize, "pam", "#include "); + addCImport(b, mod, translate_c, target, optimize, "utmp", "#include "); + if (enable_x11_support) { + addCImport(b, mod, translate_c, target, optimize, "xcb", "#include "); + } + if (target.result.os.tag == .freebsd) { + addCImport(b, mod, translate_c, target, optimize, "pwd", + \\#include + \\#include + \\#include + ); + } else { + addCImport(b, mod, translate_c, target, optimize, "pwd", "#include "); + } + addCImport(b, mod, translate_c, target, optimize, "stdlib", "#include "); + addCImport(b, mod, translate_c, target, optimize, "unistd", "#include "); + addCImport(b, mod, translate_c, target, optimize, "grp", "#include "); + addCImport(b, mod, translate_c, target, optimize, "system_time", "#include "); + addCImport(b, mod, translate_c, target, optimize, "time", "#include "); + + if (target.result.os.tag == .linux) { + addCImport(b, mod, translate_c, target, optimize, "kd", "#include "); + addCImport(b, mod, translate_c, target, optimize, "vt", "#include "); + } else if (target.result.os.tag == .freebsd) { + addCImport(b, mod, translate_c, target, optimize, "kbio", "#include "); + addCImport(b, mod, translate_c, target, optimize, "consio", "#include "); + addCImport(b, mod, translate_c, target, optimize, "sysctl", "#include "); + addCImport(b, mod, translate_c, target, optimize, "reboot", "#include "); + } + + 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); +} + +fn addCImport( + b: *std.Build, + mod: *std.Build.Module, + translate_c: *std.Build.Dependency, + target: std.Build.ResolvedTarget, + optimize: std.builtin.OptimizeMode, + comptime name: []const u8, + comptime bytes: []const u8, +) void { + const pam: Translator = .init(translate_c, .{ + .c_source_file = b.addWriteFiles().add(name ++ ".h", bytes), + .target = target, + .optimize = optimize, + }); + mod.addImport(name, pam.mod); +} diff --git a/ly-core/build.zig.zon b/ly-core/build.zig.zon new file mode 100644 index 0000000..0d84326 --- /dev/null +++ b/ly-core/build.zig.zon @@ -0,0 +1,21 @@ +.{ + .name = .ly_core, + .version = "1.1.0", + .fingerprint = 0xddda7afda795472, + .minimum_zig_version = "0.16.0", + .dependencies = .{ + .zigini = .{ + .url = "git+https://github.com/AshAmetrine/zigini?ref=master#a665d081dda42664a96da2840ea09c5ccf9d0692", + .hash = "zigini-0.5.0-BSkB7e9WAACfyCBABNZiWL3gFMw18GKn3qBcPs8L1Ec1", + }, + .translate_c = .{ + .url = "git+https://codeberg.org/ziglang/translate-c?ref=zig-0.16.x#6fe0ffc4549f15c5f2d9432c2b4460ba90ff85ac", + .hash = "translate_c-1.0.0-Q_BUWo_5BgD4flHdUhA31zOz0XvZk9k7lQv1ouzyNXj2", + }, + }, + .paths = .{ + "build.zig", + "build.zig.zon", + "src", + }, +} diff --git a/ly-core/src/LogFile.zig b/ly-core/src/LogFile.zig new file mode 100644 index 0000000..192f338 --- /dev/null +++ b/ly-core/src/LogFile.zig @@ -0,0 +1,106 @@ +const std = @import("std"); +const interop = @import("interop.zig"); + +const LogFile = @This(); + +maybe_path: ?[]const u8, +could_open_log_file: bool = undefined, +maybe_file: ?std.Io.File = null, +buffer: []u8, +maybe_file_writer: ?std.Io.File.Writer = null, + +pub fn init(io: std.Io, path: ?[]const u8, buffer: []u8) !LogFile { + var log_file = LogFile{ + .maybe_path = path, + .buffer = buffer, + }; + + if (path) |p| { + log_file.could_open_log_file = try openLogFile(io, p, &log_file); + } else { + std.posix.system.openlog("ly", 0, 0); + log_file.could_open_log_file = true; + } + + return log_file; +} + +pub fn reinit(self: *LogFile, io: std.Io) !void { + if (self.maybe_path) |path| { + self.could_open_log_file = try openLogFile(io, path, self); + } else { + std.posix.system.openlog("ly", 0, 0); + self.could_open_log_file = true; + } +} + +pub fn deinit(self: *LogFile, io: std.Io) void { + if (self.maybe_file) |file| { + file.close(io); + } else { + std.posix.system.closelog(); + } +} + +pub fn info(self: *LogFile, io: std.Io, category: []const u8, comptime message: []const u8, args: anytype) !void { + if (self.maybe_file_writer) |*writer| { + var buffer: [128:0]u8 = undefined; + const time = interop.timeAsString(io, &buffer, "%Y-%m-%d %H:%M:%S"); + + try writer.interface.print("{s} [info/{s}] ", .{ time, category }); + try writer.interface.print(message, args); + try writer.interface.writeByte('\n'); + try writer.interface.flush(); + } else { + var buffer: [1024]u8 = undefined; + const slice = try std.fmt.bufPrint(&buffer, message, args); + const msg = try std.fmt.bufPrintZ(buffer[slice.len..], "[info/{s}] {s}", .{ category, slice }); + + std.posix.system.syslog(std.posix.LOG.INFO, msg.ptr); + } +} + +pub fn err(self: *LogFile, io: std.Io, category: []const u8, comptime message: []const u8, args: anytype) !void { + if (self.maybe_file_writer) |*writer| { + var buffer: [128:0]u8 = undefined; + const time = interop.timeAsString(io, &buffer, "%Y-%m-%d %H:%M:%S"); + + try writer.interface.print("{s} [err/{s}] ", .{ time, category }); + try writer.interface.print(message, args); + try writer.interface.writeByte('\n'); + try writer.interface.flush(); + } else { + var buffer: [1024]u8 = undefined; + const slice = try std.fmt.bufPrint(&buffer, message, args); + const msg = try std.fmt.bufPrintZ(buffer[slice.len..], "[info/{s}] {s}", .{ category, slice }); + + std.posix.system.syslog(std.posix.LOG.ERR, msg.ptr); + } +} + +fn openLogFile(io: std.Io, path: []const u8, log_file: *LogFile) !bool { + var could_open_log_file = true; + open_log_file: { + log_file.maybe_file = std.Io.Dir.cwd().openFile(io, path, .{ .mode = .write_only }) catch std.Io.Dir.cwd().createFile(io, path, .{ .permissions = .fromMode(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.maybe_file = try std.Io.Dir.openFileAbsolute(io, "/dev/null", .{ .mode = .write_only }); + } + + var log_file_writer = log_file.maybe_file.?.writer(io, log_file.buffer); + + // Seek to the end of the log file + if (could_open_log_file) { + const stat = try log_file.maybe_file.?.stat(io); + try log_file_writer.seekTo(stat.size); + } + + log_file.maybe_file_writer = log_file_writer; + return could_open_log_file; +} diff --git a/ly-core/src/SharedError.zig b/ly-core/src/SharedError.zig new file mode 100644 index 0000000..c3ce088 --- /dev/null +++ b/ly-core/src/SharedError.zig @@ -0,0 +1,52 @@ +const std = @import("std"); + +const ErrInt = std.meta.Int(.unsigned, @bitSizeOf(anyerror)); +const PaddingInt = std.meta.Int(.unsigned, 8 - (@bitSizeOf(ErrInt) + @bitSizeOf(bool)) % 8); + +const ErrorHandler = packed struct { + has_error: bool = false, + err_int: ErrInt = 0, + padding: PaddingInt = 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), .{ .READ = true, .WRITE = true }, .{ .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 writer: std.Io.Writer = .fixed(self.data); + writer.writeStruct(ErrorHandler{ .has_error = true, .err_int = @intFromError(err) }, .native) 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 reader: std.Io.Reader = .fixed(self.data); + const err_handler = try reader.takeStruct(ErrorHandler, .native); + + if (err_handler.has_error) + return @errorFromInt(err_handler.err_int); + + return null; +} diff --git a/ly-core/src/UidRange.zig b/ly-core/src/UidRange.zig new file mode 100644 index 0000000..da78988 --- /dev/null +++ b/ly-core/src/UidRange.zig @@ -0,0 +1,6 @@ +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, diff --git a/ly-core/src/interop.zig b/ly-core/src/interop.zig new file mode 100644 index 0000000..d894485 --- /dev/null +++ b/ly-core/src/interop.zig @@ -0,0 +1,433 @@ +const std = @import("std"); +const builtin = @import("builtin"); +const build_options = @import("build_options"); +const UidRange = @import("UidRange.zig"); +const pwd = @import("pwd"); +const stdlib = @import("stdlib"); +const unistd = @import("unistd"); +const grp = @import("grp"); +const system_time = @import("system_time"); +const time = @import("time"); + +pub const pam = @import("pam"); +pub const utmp = @import("utmp"); +// Exists for X11 support only +pub const xcb = @import("xcb"); + +// FreeBSD only +pub const sysctl = @import("sysctl"); + +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: [*]pwd.passwd, +}; + +// Contains the platform-specific code +fn PlatformStruct() type { + return switch (builtin.os.tag) { + .linux => struct { + pub const kd = @import("kd"); + pub const vt = @import("vt"); + + 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; + + if (isError(std.posix.system.setgid(@intCast(entry.gid)))) return error.SetUserGidFailed; + if (isError(std.posix.system.setuid(@intCast(entry.uid)))) 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, io: std.Io, use_kmscon_vt: bool) !u8 { + var file_buffer: [256]u8 = undefined; + + if (use_kmscon_vt) { + var file = try std.Io.Dir.openFileAbsolute(io, "/sys/class/tty/tty0/active", .{}); + defer file.close(io); + + var reader = file.reader(io, &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.Io.Dir.openFileAbsolute(io, "/proc/self/stat", .{}); + defer file.close(io); + + var reader = file.reader(io, &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.Io.Dir.openDirAbsolute(io, "/sys/class/tty", .{ .iterate = true }); + defer directory.close(io); + + var iterator = directory.iterate(); + while (try iterator.next(io)) |entry| { + const path = try std.fmt.allocPrint(allocator, "/sys/class/tty/{s}/dev", .{entry.name}); + defer allocator.free(path); + + var file = try std.Io.Dir.openFileAbsolute(io, path, .{}); + defer file.close(io); + + var reader = file.reader(io, &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, io: std.Io, file_path: []const u8) !UidRange { + const login_defs_file = try std.Io.Dir.cwd().openFile(io, file_path, .{}); + defer login_defs_file.close(io); + + var buffer: [4096]u8 = undefined; + var reader = login_defs_file.reader(io, &buffer); + + const login_defs_buffer = try reader.interface.allocRemaining(allocator, .unlimited); + defer allocator.free(login_defs_buffer); + + var iterator = std.mem.splitScalar(u8, login_defs_buffer, '\n'); + var uid_range = UidRange{}; + var uid_min_Found = false; + var uid_max_Found = 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); + uid_min_Found = 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); + uid_max_Found = true; + } + } + + if (!(uid_min_Found or uid_max_Found)) { + return error.UidNameNotFound; + } + + if (!uid_min_Found) { + uid_range.uid_min = build_options.fallback_uid_min; + } + + if (!uid_max_Found) { + uid_range.uid_max = build_options.fallback_uid_max; + } + + return uid_range; + } + + pub fn shutdownSystemImpl() !void { + std.posix.system.sync(); + if (isError(std.os.linux.reboot(.MAGIC1, .MAGIC2, .POWER_OFF, null))) { + return error.CouldntShutdown; + } + } + + pub fn rebootSystemImpl() !void { + std.posix.system.sync(); + if (isError(std.os.linux.reboot(.MAGIC1, .MAGIC2, .RESTART, null))) { + return error.CouldntShutdown; + } + } + + 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 = @import("kbio"); + pub const consio = @import("consio"); + pub const reboot = @import("reboot"); + + 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, _: std.Io, _: bool) !u8 { + return error.FeatureUnimplemented; + } + + pub fn getUserIdRange(_: std.mem.Allocator, _: std.Io, _: []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, + }; + } + + pub fn shutdownSystemImpl() !void { + if (isError(unistd.reboot(reboot.RB_POWEROFF))) { + return error.CouldntShutdown; + } + } + + pub fn rebootSystemImpl() !void { + if (isError(unistd.reboot(reboot.RB_AUTOBOOT))) { + return error.CouldntReboot; + } + } + }, + else => @compileError("Unsupported target: " ++ builtin.os.tag), + }; +} + +const platform_struct = PlatformStruct(); + +pub fn isError(result: anytype) bool { + if (@typeInfo(@TypeOf(result)).int.signedness == .signed) { + return result < 0; + } + + if (@typeInfo(@TypeOf(result)).int.signedness == .unsigned) { + return switch (builtin.os.tag) { + .linux => std.os.linux.errno(result) != .SUCCESS, + else => @compileError("interop.isError() not implemented for current target!"), + }; + } + + unreachable; +} + +pub fn supportsUnicode() bool { + return builtin.os.tag == .linux or builtin.os.tag == .freebsd; +} + +pub fn timeAsString(io: std.Io, buf: [:0]u8, format: [:0]const u8) []u8 { + const timer: isize = @intCast(std.Io.Timestamp.now(io, .real).toSeconds()); + 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, io: std.Io, use_kmscon_vt: bool) !u8 { + return platform_struct.getActiveTtyImpl(allocator, io, 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: []u8) !void { + const status = stdlib.putenv(name_and_value.ptr); + 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, io: std.Io, file_path: []const u8) !UidRange { + return platform_struct.getUserIdRange(allocator, io, file_path); +} + +pub fn shutdownSystem() !void { + try platform_struct.shutdownSystemImpl(); +} + +pub fn rebootSystem() !void { + try platform_struct.rebootSystemImpl(); +} diff --git a/ly-core/src/root.zig b/ly-core/src/root.zig new file mode 100644 index 0000000..42198f5 --- /dev/null +++ b/ly-core/src/root.zig @@ -0,0 +1,78 @@ +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, + io: std.Io, + 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(io, 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; + } + }; +} diff --git a/ly-ui/build.zig b/ly-ui/build.zig new file mode 100644 index 0000000..b13633f --- /dev/null +++ b/ly-ui/build.zig @@ -0,0 +1,62 @@ +const std = @import("std"); +const Translator = @import("translate_c").Translator; + +pub fn build(b: *std.Build) void { + const target = b.standardTargetOptions(.{}); + const optimize = b.standardOptimizeOption(.{}); + const enable_x11_support = b.option(bool, "enable_x11_support", "Enable X11 support") orelse true; + const mod = b.addModule("ly-ui", .{ + .root_source_file = b.path("src/root.zig"), + .target = target, + .optimize = optimize, + }); + + 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"); + 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"); + + const ly_core = b.dependency("ly_core", .{ + .target = target, + .optimize = optimize, + .enable_x11_support = enable_x11_support, + .fallback_uid_min = fallback_uid_min, + .fallback_uid_max = fallback_uid_max, + }); + mod.addImport("ly-core", ly_core.module("ly-core")); + + const termbox_dep = b.dependency("termbox2", .{ + .target = target, + .optimize = optimize, + }); + + const translate_c_dep = b.dependency("translate_c", .{ + .target = target, + }); + + const termbox2: Translator = .init(translate_c_dep, .{ + .c_source_file = termbox_dep.path("termbox2.h"), + .target = target, + .optimize = optimize, + }); + termbox2.defineCMacro("TB_IMPL", null); + // TODO 0.16.0: Workaround until Aro gets better... + // https://codeberg.org/ziglang/translate-c/issues/319 + termbox2.defineCMacro("_XOPEN_SOURCE", "700"); + termbox2.defineCMacro("TB_OPT_ATTR_W", "32"); // Enable 24-bit color support + styling (32-bit) + // TODO 0.16.0: Including with -OReleaseSafe causes + // __attribute__(__error__()) to be called. Below + // is the workaround. + termbox2.defineCMacro("_FORTIFY_SOURCE", "0"); + // TODO 0.16.0: Needed for now + if (target.result.os.tag == .freebsd) { + termbox2.defineCMacro("__BSD_VISIBLE", "1"); + } + mod.addImport("termbox2", termbox2.mod); + + 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); +} diff --git a/ly-ui/build.zig.zon b/ly-ui/build.zig.zon new file mode 100644 index 0000000..68131d1 --- /dev/null +++ b/ly-ui/build.zig.zon @@ -0,0 +1,24 @@ +.{ + .name = .ly_ui, + .version = "1.1.0", + .fingerprint = 0x8d11bf85a74ec803, + .minimum_zig_version = "0.16.0", + .dependencies = .{ + .ly_core = .{ + .path = "../ly-core", + }, + .termbox2 = .{ + .url = "git+https://github.com/AnErrupTion/termbox2?ref=master#c7f241e8888ce243e1748b05c26a42fcfaaad936", + .hash = "N-V-__8AAAUXBQD6Fwpi9m0MBqWXFFaqW5l1lVrJC2Ynj7a-", + }, + .translate_c = .{ + .url = "git+https://codeberg.org/ziglang/translate-c?ref=zig-0.16.x#6fe0ffc4549f15c5f2d9432c2b4460ba90ff85ac", + .hash = "translate_c-1.0.0-Q_BUWo_5BgD4flHdUhA31zOz0XvZk9k7lQv1ouzyNXj2", + }, + }, + .paths = .{ + "build.zig", + "build.zig.zon", + "src", + }, +} diff --git a/ly-ui/src/Cell.zig b/ly-ui/src/Cell.zig new file mode 100644 index 0000000..59eb4aa --- /dev/null +++ b/ly-ui/src/Cell.zig @@ -0,0 +1,21 @@ +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; + + try TerminalBuffer.setCell(x, y, self); +} diff --git a/ly-ui/src/Position.zig b/ly-ui/src/Position.zig new file mode 100644 index 0000000..7073cba --- /dev/null +++ b/ly-ui/src/Position.zig @@ -0,0 +1,221 @@ +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, + }; +} diff --git a/ly-ui/src/TerminalBuffer.zig b/ly-ui/src/TerminalBuffer.zig new file mode 100644 index 0000000..4881a2e --- /dev/null +++ b/ly-ui/src/TerminalBuffer.zig @@ -0,0 +1,680 @@ +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, + io: std.Io, + options: InitOptions, + log_file: *LogFile, + random: Random, +) !TerminalBuffer { + // Initialize termbox + var err = termbox.tb_init(); + if (err != 0) { + try log_file.err( + io, + "tui", + "failed to initialise termbox2: {s}, term: {s}", + .{ termbox.tb_strerror(err), std.c.getenv("TERM").? }, + ); + return error.TermboxInitFailed; + } + + if (options.full_color) { + err = termbox.tb_set_output_mode(termbox.TB_OUTPUT_TRUECOLOR); + if (err != 0) { + try log_file.err( + io, + "tui", + "failed to set termbox2 output mode to 24-bit color: {s}", + .{termbox.tb_strerror(err)}, + ); + return error.TermboxSetOutputModeFailed; + } + try log_file.info( + io, + "tui", + "termbox2 set to 24-bit color output mode", + .{}, + ); + } else { + try log_file.info( + io, + "tui", + "termbox2 set to eight-color output mode", + .{}, + ); + } + + // Let's take some precautions here and clear the back buffer as well + try clearScreen(true); + + const width = getWidth(); + const height = getHeight(); + + try log_file.info( + io, + "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() catch {}; +} + +pub fn runEventLoop( + self: *TerminalBuffer, + allocator: Allocator, + io: std.Io, + 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(io, "Ctrl+K", &moveCursorUp, self); + try self.registerGlobalKeybind(io, "Up", &moveCursorUp, self); + + try self.registerGlobalKeybind(io, "Ctrl+J", &moveCursorDown, self); + try self.registerGlobalKeybind(io, "Down", &moveCursorDown, self); + + try self.registerGlobalKeybind(io, "Tab", &wrapCursor, self); + try self.registerGlobalKeybind(io, "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) { + var maybe_timeout: ?usize = null; + + if (self.update) { + try TerminalBuffer.clearScreen(false); + + // Reset cursor + const current_widget = self.getActiveWidget(); + current_widget.handle(null) catch |err| { + shared_error.writeError(error.SetCursorFailed); + try self.log_file.err( + io, + "tui", + "failed to set cursor in active widget '{s}': {s}", + .{ current_widget.display_name, @errorName(err) }, + ); + }; + + for (layers) |layer| { + for (layer) |widget| { + try widget.update(context); + widget.draw(); + + if (try widget.calculateTimeout(context)) |widget_timeout| { + if (maybe_timeout == null or widget_timeout < maybe_timeout.?) maybe_timeout = widget_timeout; + } + } + } + + // We don't care about present errors here + TerminalBuffer.presentBuffer() catch {}; + } + + 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 or event_error >= 0; + + 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( + io, + "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( + io, + "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( + io, + "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 { + if (termbox.tb_set_cursor(@intCast(x), @intCast(y)) != 0) { + return error.TermboxSetCursorFailed; + } +} + +pub fn clearScreen(clear_back_buffer: bool) !void { + if (termbox.tb_clear() != 0) return error.TermboxClearFailed; + if (clear_back_buffer) try clearBackBuffer(); +} + +pub fn shutdown() !void { + if (termbox.tb_shutdown() != 0) return error.TermboxShutdownFailed; +} + +pub fn presentBuffer() !void { + if (termbox.tb_present() != 0) return error.TermboxPresentFailed; +} + +pub fn getCell(x: usize, y: usize) ?Cell { + var maybe_cell: ?*termbox.tb_cell = undefined; + if (termbox.tb_get_cell( + @intCast(x), + @intCast(y), + 1, + &maybe_cell, + ) != 0) { + return null; + } + + 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 { + if (termbox.tb_set_cell( + @intCast(x), + @intCast(y), + cell.ch, + cell.fg, + cell.bg, + ) != 0) { + return error.TermboxSetCellFailed; + } +} + +pub fn setCellBoundsChecked(self: *TerminalBuffer, x: isize, y: isize, cell: Cell) !void { + if (0 <= x and x < self.width and 0 <= y and y < self.height) { + try cell.put(@intCast(x), @intCast(y)); + } +} + +pub fn reclaim(self: TerminalBuffer) !void { + if (self.termios) |termios| { + // Take back control of the TTY + const err = termbox.tb_init(); + if (err != 0 and err != termbox.TB_ERR_INIT_ALREADY) return error.TermboxReinitFailed; + + if (self.full_color and termbox.tb_set_output_mode(termbox.TB_OUTPUT_TRUECOLOR) != 0) { + return error.TermboxSetOutputModeFailed; + } + + try std.posix.tcsetattr(std.posix.STDIN_FILENO, .FLUSH, termios); + } +} + +pub fn registerKeybind( + self: *TerminalBuffer, + io: std.Io, + keybinds: *KeybindMap, + keybind: []const u8, + callback: KeybindCallbackFn, + context: *anyopaque, +) !void { + const key = try self.parseKeybind(io, keybind); + + keybinds.put(key, .{ + .callback = callback, + .context = context, + }) catch |err| { + try self.log_file.err( + io, + "tui", + "failed to register keybind {s}: {s}", + .{ keybind, @errorName(err) }, + ); + }; +} + +pub fn registerGlobalKeybind( + self: *TerminalBuffer, + io: std.Io, + keybind: []const u8, + callback: KeybindCallbackFn, + context: *anyopaque, +) !void { + try self.registerKeybind(io, &self.keybinds, keybind, callback, context); +} + +pub fn simulateKeybind(self: *TerminalBuffer, io: std.Io, keybind: []const u8) !bool { + const key = try self.parseKeybind(io, 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 utf8view = std.unicode.Utf8View.init(text) catch return; + var utf8 = utf8view.iterator(); + + var i = x; + while (utf8.nextCodepoint()) |codepoint| : (i += @intCast(termbox.tb_wcwidth(codepoint))) { + const cell = Cell.init(codepoint, fg, bg); + try cell.put(i, y); + } +} + +pub fn drawConfinedText( + text: []const u8, + x: usize, + y: usize, + max_length: usize, + fg: u32, + bg: u32, +) !void { + const utf8view = std.unicode.Utf8View.init(text) catch return; + var utf8 = utf8view.iterator(); + + var i = x; + while (utf8.nextCodepoint()) |codepoint| : (i += @intCast(termbox.tb_wcwidth(codepoint))) { + if (i - x >= max_length) break; + + const cell = Cell.init(codepoint, fg, bg); + try cell.put(i, y); + } +} + +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| try 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 { + if (termbox.global.initialized == 0) return; + + // Clear the TTY because termbox2 doesn't seem to do it properly + const capability = termbox.global.caps[termbox.TB_CAP_CLEAR_SCREEN]; + const capability_slice = std.mem.span(capability); + const result = std.posix.system.write(termbox.global.ttyfd, capability_slice.ptr, capability_slice.len); + + if (result != capability_slice.len) return error.PartialClearBackBuffer; + if (result < 0) return error.ClearBackBufferFailed; +} + +fn parseKeybind(self: *TerminalBuffer, io: std.Io, 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 (comptime std.meta.fieldNames(keyboard.Key)) |name| { + if (std.ascii.eqlIgnoreCase(name, item)) { + @field(key, name) = true; + found = true; + break; + } + } + + if (!found) { + try self.log_file.err( + io, + "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; +} diff --git a/ly-ui/src/Widget.zig b/ly-ui/src/Widget.zig new file mode 100644 index 0000000..07f76c3 --- /dev/null +++ b/ly-ui/src/Widget.zig @@ -0,0 +1,186 @@ +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, +}; + +pub var idCounter: u64 = 0; + +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, + }; + }; + + idCounter += 1; + return .{ + .id = idCounter, + .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; +} diff --git a/ly-ui/src/components/BigLabel.zig b/ly-ui/src/components/BigLabel.zig new file mode 100644 index 0000000..cb2f6b5 --- /dev/null +++ b/ly-ui/src/components/BigLabel.zig @@ -0,0 +1,237 @@ +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, +}; + +instance: ?Widget = null, +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 .{ + .instance = null, + .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 { + if (self.instance) |*instance| return instance; + self.instance = Widget.init( + "BigLabel", + null, + self, + deinit, + null, + draw, + update, + null, + calculateTimeout, + ); + return &self.instance.?; +} + +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) catch {}; + } + } +} + +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, + }; +} diff --git a/ly-ui/src/components/Box.zig b/ly-ui/src/components/Box.zig new file mode 100644 index 0000000..af3a687 --- /dev/null +++ b/ly-ui/src/components/Box.zig @@ -0,0 +1,190 @@ +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 Box = @This(); + +instance: ?Widget = null, +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 (*Box, *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 (*Box, *anyopaque) anyerror!void, +) Box { + return .{ + .instance = null, + .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: *Box) *Widget { + if (self.instance) |*instance| return instance; + self.instance = Widget.init( + "Box", + null, + self, + null, + null, + draw, + update, + null, + null, + ); + return &self.instance.?; +} + +pub fn positionXY(self: *Box, original_pos: Position) void { + if (self.buffer.width < 2 or self.buffer.height < 2) return; + + self.left_pos = original_pos; + self.right_pos = Position.init( + @min(self.buffer.width, self.width), + @min(self.buffer.height, self.height), + ).add(self.left_pos); + + self.children_pos = Position.init( + self.horizontal_margin, + self.vertical_margin, + ).add(self.left_pos); +} + +pub fn childrenPosition(self: Box) Position { + return self.children_pos; +} + +fn draw(self: *Box) 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) catch {}; + right_up.put(self.right_pos.x, self.left_pos.y - 1) catch {}; + left_down.put(self.left_pos.x - 1, self.right_pos.y) catch {}; + right_down.put(self.right_pos.x, self.right_pos.y) catch {}; + + for (0..self.width) |i| { + top.put(self.left_pos.x + i, self.left_pos.y - 1) catch {}; + bottom.put(self.left_pos.x + i, self.right_pos.y) catch {}; + } + + 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) catch {}; + bottom.put(self.right_pos.x, self.left_pos.y + i) catch {}; + } + } + + 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) catch {}; + } + } + } + + if (self.top_title) |title| { + TerminalBuffer.drawConfinedText( + title, + self.left_pos.x, + self.left_pos.y - 1, + self.width, + self.title_fg, + self.bg, + ) catch {}; + } + + 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, + ) catch {}; + } +} + +fn update(self: *Box, ctx: *anyopaque) !void { + if (self.update_fn) |update_fn| { + return @call( + .auto, + update_fn, + .{ self, ctx }, + ); + } +} diff --git a/ly-ui/src/components/Label.zig b/ly-ui/src/components/Label.zig new file mode 100644 index 0000000..d54cb9c --- /dev/null +++ b/ly-ui/src/components/Label.zig @@ -0,0 +1,153 @@ +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"); + +instance: ?Widget, +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 .{ + .instance = null, + .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 { + if (self.instance) |*instance| return instance; + self.instance = Widget.init( + "Label", + null, + self, + deinit, + null, + draw, + update, + null, + calculateTimeout, + ); + return &self.instance.?; +} + +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, + ) catch {}; + return; + } + + TerminalBuffer.drawText( + self.text, + self.component_pos.x, + self.component_pos.y, + self.fg, + self.bg, + ) catch {}; +} + +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; +} diff --git a/ly-ui/src/components/Text.zig b/ly-ui/src/components/Text.zig new file mode 100644 index 0000000..875021a --- /dev/null +++ b/ly-ui/src/components/Text.zig @@ -0,0 +1,250 @@ +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.ArrayList(u8); + +const Text = @This(); + +instance: ?Widget, +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, + io: std.Io, + 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{ + .instance = null, + .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(io, &self.keybinds, "Left", &goLeft, self); + try buffer.registerKeybind(io, &self.keybinds, "Right", &goRight, self); + try buffer.registerKeybind(io, &self.keybinds, "Delete", &delete, self); + try buffer.registerKeybind(io, &self.keybinds, "Backspace", &backspace, self); + try buffer.registerKeybind(io, &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 { + if (self.instance) |*instance| return instance; + self.instance = Widget.init( + "Text", + self.keybinds, + self, + deinit, + null, + draw, + null, + handle, + null, + ); + return &self.instance.?; +} + +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) { + try TerminalBuffer.setCursor( + self.component_pos.x, + self.component_pos.y, + ); + return; + } + + try TerminalBuffer.setCursor( + self.component_pos.x + (self.cursor - self.visible_start), + self.component_pos.y, + ); +} + +pub fn writeText(self: *Text, str: []const u8) !void { + for (str) |c| try self.write(c); +} + +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, + ) catch {}; + } + 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, + ) catch {}; +} + +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; +} diff --git a/src/bigclock/en.zig b/ly-ui/src/components/bigLabelLocales/en.zig similarity index 75% rename from src/bigclock/en.zig rename to ly-ui/src/components/bigLabelLocales/en.zig index 868656a..0593eed 100644 --- a/src/bigclock/en.zig +++ b/ly-ui/src/components/bigLabelLocales/en.zig @@ -1,8 +1,7 @@ -const Lang = @import("Lang.zig"); - -const LocaleChars = Lang.LocaleChars; -const X = Lang.X; -const O = Lang.O; +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{ @@ -90,5 +89,26 @@ pub const locale_chars = LocaleChars{ 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 \ No newline at end of file +// zig fmt: on diff --git a/src/bigclock/fa.zig b/ly-ui/src/components/bigLabelLocales/fa.zig similarity index 75% rename from src/bigclock/fa.zig rename to ly-ui/src/components/bigLabelLocales/fa.zig index 63a897a..06e7036 100644 --- a/src/bigclock/fa.zig +++ b/ly-ui/src/components/bigLabelLocales/fa.zig @@ -1,8 +1,7 @@ -const Lang = @import("Lang.zig"); - -const LocaleChars = Lang.LocaleChars; -const X = Lang.X; -const O = Lang.O; +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{ @@ -76,6 +75,27 @@ pub const locale_chars = LocaleChars{ 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, @@ -91,4 +111,4 @@ pub const locale_chars = LocaleChars{ O,O,O,O,O, }, }; -// zig fmt: on \ No newline at end of file +// zig fmt: on diff --git a/ly-ui/src/components/generic.zig b/ly-ui/src/components/generic.zig new file mode 100644 index 0000000..2d0b3c4 --- /dev/null +++ b/ly-ui/src/components/generic.zig @@ -0,0 +1,172 @@ +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.ArrayList(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, + io: std.Io, + 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(io, &self.keybinds, "Left", &goLeft, self); + try buffer.registerKeybind(io, &self.keybinds, "Ctrl+H", &goLeft, self); + try buffer.registerKeybind(io, &self.keybinds, "Right", &goRight, self); + try buffer.registerKeybind(io, &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 { + try 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) catch {}; + right_arrow.put( + self.component_pos.x + self.width - 1, + self.component_pos.y, + ) catch {}; + + 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; + } + }; +} diff --git a/ly-ui/src/keyboard.zig b/ly-ui/src/keyboard.zig new file mode 100644 index 0000000..16b9e77 --- /dev/null +++ b/ly-ui/src/keyboard.zig @@ -0,0 +1,717 @@ +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 (comptime std.meta.fieldNames(Key)) |name| { + if (name.len == 1 and std.ascii.isPrint(name[0]) and @field(self, name)) { + if (self.shift) { + if (!std.ascii.isAlphanumeric(name[0])) return null; + return std.ascii.toUpper(name[0]); + } + + return 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 => {}, + } + + if (code >= 18 and code <= 21) { + // https://github.com/termbox/termbox2/blob/605398fa79108412976191e062ea14bd4bd30213/termbox2.h#L446 + key.ctrl = false; + key.shift = false; + } + } 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) { + // Non-standard control codes + 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; + }, + // Standard ASCII characters + 32 => { + key = std.mem.zeroes(Key); + 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 = std.mem.zeroes(Key); + 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 = std.mem.zeroes(Key); + key.@"," = true; + }, + 45 => { + key = std.mem.zeroes(Key); + key.@"-" = true; + }, + 46 => { + key = std.mem.zeroes(Key); + key.@"." = true; + }, + 47 => { + key = std.mem.zeroes(Key); + key.@"/" = true; + }, + 48 => { + key = std.mem.zeroes(Key); + key.@"0" = true; + }, + 49 => { + key = std.mem.zeroes(Key); + key.@"1" = true; + }, + 50 => { + key = std.mem.zeroes(Key); + key.@"2" = true; + }, + 51 => { + key = std.mem.zeroes(Key); + key.@"3" = true; + }, + 52 => { + key = std.mem.zeroes(Key); + key.@"4" = true; + }, + 53 => { + key = std.mem.zeroes(Key); + key.@"5" = true; + }, + 54 => { + key = std.mem.zeroes(Key); + key.@"6" = true; + }, + 55 => { + key = std.mem.zeroes(Key); + key.@"7" = true; + }, + 56 => { + key = std.mem.zeroes(Key); + key.@"8" = true; + }, + 57 => { + key = std.mem.zeroes(Key); + key.@"9" = true; + }, + 58 => { + key = std.mem.zeroes(Key); + key.@":" = true; + }, + 59 => { + key = std.mem.zeroes(Key); + key.@";" = true; + }, + 60 => { + key = std.mem.zeroes(Key); + key.@"<" = true; + }, + 61 => { + key = std.mem.zeroes(Key); + key.@"=" = true; + }, + 62 => { + key = std.mem.zeroes(Key); + key.@">" = true; + }, + 63 => { + key = std.mem.zeroes(Key); + key.@"?" = true; + }, + 64 => { + 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 = std.mem.zeroes(Key); + key.@"[" = true; + }, + 92 => { + key = std.mem.zeroes(Key); + key.@"\\" = true; + }, + 93 => { + key = std.mem.zeroes(Key); + key.@"]" = true; + }, + 94 => { + key = std.mem.zeroes(Key); + key.@"^" = true; + }, + 95 => { + key = std.mem.zeroes(Key); + key._ = true; + }, + 96 => { + key = std.mem.zeroes(Key); + 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.@"{" = true; + }, + 124 => { + key = std.mem.zeroes(Key); + key.@"|" = true; + }, + 125 => { + key = std.mem.zeroes(Key); + key.@"}" = true; + }, + 126 => { + key = std.mem.zeroes(Key); + key.@"~" = true; + }, + 127 => { + key = std.mem.zeroes(Key); + key.backspace = true; + }, + else => {}, + } + } + + try keys.append(allocator, key); + + return keys; +} diff --git a/ly-ui/src/root.zig b/ly-ui/src/root.zig new file mode 100644 index 0000000..dba4740 --- /dev/null +++ b/ly-ui/src/root.zig @@ -0,0 +1,13 @@ +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 Box = @import("components/Box.zig"); +pub const CyclableLabel = @import("components/generic.zig").CyclableLabel; +pub const Label = @import("components/Label.zig"); +pub const Text = @import("components/Text.zig"); diff --git a/readme.md b/readme.md index 33bb805..2d26c31 100644 --- a/readme.md +++ b/readme.md @@ -1,254 +1,294 @@ +# The Ly display manager -# Ly - a TUI display manager ![Ly screenshot](.github/screenshot.png "Ly screenshot") -Ly is a lightweight TUI (ncurses-like) display manager for Linux and BSD. +_Note: the above animation can be found [here](https://codeberg.org/fairyglade/ly-community/src/branch/main/animations/dur/blackhole-smooth-240x67.dur)!_ + +Ly is a lightweight TUI (ncurses-like) display manager for Linux and BSD, designed with portability in mind and doesn't 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.12.0 + - zig 0.16.x (you must use a **release version** of zig; check that `zig version` does not have a `-dev*` suffix) + - 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 +# 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 +# dnf install kernel-devel pam-devel libxcb-devel zig xorg-x11-xauth xorg-x11-server brightnessctl ``` +> [!WARNING] +> Distributions using SELinux such as Fedora and openSUSE Tumbleweed may encounter issues. If you encounter such issues, such as session launch failures, proceed with the following steps: +> +> ``` +> # ausearch -m avc -ts recent +> ``` +> +> If SELinux is denying process context transition, you will see this output: +> +> ``` +> denied { transition } for pid=XXXX comm="ly" path="/usr/bin/bash" +> scontext=system_u:system_r:unconfined_service_t:s0 +> tcontext=unconfined_u:unconfined_r:unconfined_t:s0 +> tclass=process permissive=0 +> ``` +> +> Pipe this output into `audit2allow` to generate a security module package. This will persist regardless of changes to filesystem permissions: +> +> ``` +> # ausearch -m avc -ts recent | audit2allow -M ly-local +> ``` +> +> ``` +> # semodule -i ly-local.pp +> ``` +> +> _This fix has been confirmed on Fedora 39 and 44 and openSUSE Tumbleweed. (#494)_ + +### 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) + +## Custom animations & user scripts + +If you want to pimp your Ly installation even further than what the traditional configuration file allows you to, a [community repository](https://codeberg.org/fairyglade/ly-community) exists containing custom animations & scripts, so go check it out! + ## Support -The following desktop environments were tested with success: - - awesome - - bspwm - - budgie - - cinnamon - - cosmic - - deepin - - dwl - - dwm - - enlightenment - - gnome - - i3 - - kde - - labwc - - lxde - - lxqt - - mate - - maxx - - pantheon - - qtile - - spectrwm - - sway - - windowmaker - - xfce - - xmonad -Ly should work with any X desktop environment, and provides -basic wayland support (sway works very well, for example). +Every environment that works on other login managers also should work on Ly. -## systemd? -Unlike what you may have heard, Ly does not require `systemd`, -and was even specifically designed not to depend on `logind`. -You should be able to make it work easily with a better init, -changing the source code won't be necessary :) +- Unlike most login managers Ly has an xinitrc and shell entry. + +- 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 Xorg sessions don't work then check if your distro compiles Ly with Xorg. + +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: -## Cloning and Compiling -Clone the repository -``` -$ git clone https://github.com/fairyglade/ly -``` - -Change the directory to ly ``` +$ git clone https://codeberg.org/fairyglade/ly.git $ cd ly -``` - -Compile -``` $ zig build ``` -Test in the configured tty (tty2 by default) -or a terminal emulator (but desktop environments won't start) +After building, you can (optionally) test Ly in a terminal emulator, although authentication will **not** work: + ``` -# zig build run +$ zig build run ``` -Install Ly and the provided systemd service file +> [!IMPORTANT] +> While you can run Ly in a terminal emulator as root, it is **not** recommended. If you want to test Ly, please enable its service (as described below) and reboot your machine. + +> [!NOTE] +> You can, however, test your configuration file changes like that. Note that you must do Ctrl+C in order to exit Ly. + +The next sections will explain how to use Ly with a variety of init systems. Detailed explanation is only given for systemd, but should be applicable for all. + +> [!NOTE] +> All following sections will assume you are using LightDM for convenience sake. + +### systemd + +Now, you can install Ly on your system: + ``` -# zig build installsystemd +# zig build installexe -Dinit_system=systemd ``` -Enable the service +> [!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 you are using LightDM, you can execute the following command: + ``` -# systemctl enable ly.service +# systemctl disable lightdm.service ``` -If you need to switch between ttys after Ly's start you also have to -disable getty on Ly's tty to prevent "login" from spawning on top of it +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 non-systemd systems, you can change the TTY Ly will run on by editing the corresponding service file for your platform. + ### OpenRC -**NOTE 1**: On Gentoo, Ly will disable the `display-manager-init` service in order to run. -Clone, compile and test. - -Install Ly and the provided OpenRC service -``` -# zig build installopenrc -``` - -Enable the service ``` +# zig build installexe -Dinit_system=openrc +# rc-update del lightdm # rc-update add ly -``` - -You can edit which tty Ly will start on by editing the `tty` option in the configuration file. - -If you choose a tty that already has a login/getty running (has a basic login prompt), -then you have to disable getty, so it doesn't respawn on top of ly -``` # rc-update del agetty.tty2 ``` -**NOTE 2**: To avoid a console spawning on top on Ly, comment out the appropriate line from /etc/inittab (default is 2). +> [!NOTE] +> On Gentoo, Alpine and potentially others, you also **must** comment out the appropriate line for the TTY in /etc/inittab. ### runit + ``` -# zig build installrunit +# zig build installexe -Dinit_system=runit +# rm /var/service/lightdm # ln -s /etc/sv/ly /var/service/ -``` - -By default, ly will run on tty2. To change the tty it must be set in `/etc/ly/config.ini` - -You should as well disable your existing display manager service if needed, e.g.: - -``` -# rm /var/service/lxdm -``` - -The agetty service for the tty console where you are running ly should be disabled. -For instance, if you are running ly on tty2 (that's the default, check your `/etc/ly/config.ini`) -you should disable the agetty-tty2 service like this: - -``` # rm /var/service/agetty-tty2 ``` ### s6 -``` -# zig build installs6 -``` - -Then, edit `/etc/s6/config/ttyX.conf` and set `SPAWN="no"`, where X is the TTY ID (e.g. `2`). - -Finally, enable the service: ``` +# 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 installdinit +# zig build installexe -Dinit_system=dinit +# dinitctl disable lightdm # dinitctl enable ly ``` -In addition to the steps above, you will also have to keep a TTY free within `/etc/dinit.d/config/console.conf`. +To disable TTY 2, go to `/etc/dinit.d/config/console.conf` and modify `ACTIVE_CONSOLES`. -To do that, change `ACTIVE_CONSOLES` so that the tty that ly should use in `/etc/ly/config.ini` is free. +### 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 copying the system service and the configuration file. That's -called *updating*. To update, simply run: + +You can also install Ly without overriding the current configuration file. This is called **updating**. To update, simply run: ``` # zig build installnoconf ``` -If you want to also copy the default config file (but still not the system service), run: - -``` -# zig build installexe -``` - -## Arch Linux Installation -You can install ly from the [`[extra]` repos](https://archlinux.org/packages/extra/x86_64/ly/): -``` -$ sudo pacman -S ly -``` - -## Gentoo Installation -You can install ly from the GURU repository: - -Note: If the package is masked, you may need to unmask it using ~amd64 keyword: -```bash -# echo 'x11-misc/ly ~amd64' >> /etc/portage/package.accept_keywords -``` - -1. Enable the GURU repository: -```bash -# eselect repository enable guru -``` - -2. Sync the GURU repository: -```bash -# emaint sync -r guru -``` - -3. Install ly from source: -```bash -# emerge --ask x11-misc/ly -``` +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 commented, and includes the default values. + +You can find all the configuration in `/etc/ly/config.ini`. The file is fully commented, and includes the default values. + +You may also check the validity of your configuration file (i.e. if there are any errors in it) with the following command: + +``` +$ ly --validate-config /etc/ly/config.ini +``` ## Controls -Use the up and down arrow keys to change the current field, and the -left and right arrow keys to change the target desktop environment -while on the desktop field (above the login field). -## .xinitrc -If your .xinitrc 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: +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. -On Arch Linux, the example .xinitrc (/etc/X11/xinit/xinitrc) starts like this: +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 if X doesn't start, as it can interfere -(this file is launched with X to configure the display properly). -## PSX DOOM fire animation -To enable the famous PSX DOOM fire described by [Fabien Sanglard](http://fabiensanglard.net/doom_fire_psx/index.html), -just set `animation = doom` in `/etc/ly/config.ini`. You may also -disable the main box borders with `hide_borders = true`. +- The numlock and capslock states are printed in the top-right corner. -## Additional Information -The name "Ly" is a tribute to the fairy from the game Rayman. -Ly was tested by oxodao, who is some seriously awesome dude. +- 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 [ashametrine](https://github.com/ashametrine), 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)! diff --git a/res/config.ini b/res/config.ini index 1d28cb8..2bd2624 100644 --- a/res/config.ini +++ b/res/config.ini @@ -1,55 +1,84 @@ -# The color settings in Ly take a digit 0-8 corresponding to: -#define TB_DEFAULT 0x00 -#define TB_BLACK 0x01 -#define TB_RED 0x02 -#define TB_GREEN 0x03 -#define TB_YELLOW 0x04 -#define TB_BLUE 0x05 -#define TB_MAGENTA 0x06 -#define TB_CYAN 0x07 -#define TB_WHITE 0x08 -# The default color varies, but usually it makes the background black and the foreground white. -# You can also combine these colors with the following style attributes using bitwise OR: -#define TB_BOLD 0x0100 -#define TB_UNDERLINE 0x0200 -#define TB_REVERSE 0x0400 -#define TB_ITALIC 0x0800 -#define TB_BLINK 0x1000 -#define TB_HI_BLACK 0x2000 -#define TB_BRIGHT 0x4000 -#define TB_DIM 0x8000 -# For example, to set the foreground color to red and bold, you would do 0x02 | 0x0100 = 0x0102. -# Note that you must pre-calculate the value because Ly doesn't parse bitwise OR operations in its config. -# -# Moreover, to set the VT color palette, you are encouraged to use another tool such as -# mkinitcpio-colors (https://github.com/evanpurkhiser/mkinitcpio-colors). Note that the color palette defined with -# mkinitcpio-colors takes 16 colors (0-15), only values 0-8 are valid with Ly and these values do not correspond -# exactly. For instance, in defining palettes with mkinitcpio-colors, the order is black, dark red, dark green, brown, dark -# blue, dark purple, dark cyan, light gray, dark gray, bright red, bright green, yellow, bright blue, bright purple, bright -# cyan, and white, indexed in that order 0 through 15. For example, the color defined for white (indexed at 15 in the mkinitcpio -# config) will be used by Ly for fg = 0x0008. +# 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 +# none -> Nothing +# doom -> PSX DOOM fire +# matrix -> CMatrix +# colormix -> Color mixing shader +# gameoflife -> John Conway's Game of Life +# dur_file -> .dur file format (https://github.com/cmang/durdraw/tree/master) +# lua -> user-made animation written in LuaJIT animation = none +# Delay between each animation frame in milliseconds +animation_frame_delay = 5 + # Stop the animation after some time # 0 -> Run forever # 1..2e12 -> Stop the animation after this many seconds animation_timeout_sec = 0 # The character used to mask the password +# You can either type it directly as a UTF-8 character (like *), or use a UTF-32 +# codepoint (for example 0x2022 for a bullet point) # If null, the password will be hidden # Note: you can use a # by escaping it like so: \# asterisk = * # The number of failed authentications before a special animation is played... ;) +# If set to 0, the animation will never be played auth_fails = 10 +# Automatic login configuration +# This feature allows Ly to automatically log in a user without password prompt. +# IMPORTANT: Both auto_login_user and auto_login_session must be set for this to work. +# Autologin only happens once at startup - it won't re-trigger after logout. + +# PAM service name to use for automatic login +# The default service (ly-autologin) uses pam_permit to allow login without password +# The appropriate platform-specific PAM configuration (ly-autologin) will be used automatically +auto_login_service = ly-autologin + +# Session name to launch automatically +# To find available session names, check the .desktop files in: +# - /usr/share/xsessions/ (for X11 sessions) +# - /usr/share/wayland-sessions/ (for Wayland sessions) +# Use the filename without .desktop extension, the Name field inside the file or the value of the DesktopNames field +# Examples: "i3", "sway", "gnome", "plasma", "xfce" +# If null, automatic login is disabled +auto_login_session = null + +# Username to automatically log in +# Must be a valid user on the system +# If null, automatic login is disabled +auto_login_user = null + +# Identifier for battery whose charge to display at top left +# Primary battery is usually BAT0 or BAT1 +# If set to null, battery status won't be shown +# Unused on FreeBSD (a sysctl is used there) +battery_id = null + # Background color id -bg = 0x0000 +bg = 0x00000000 # Change the state and language of the big clock # none -> Disabled (default) @@ -57,27 +86,43 @@ bg = 0x0000 # 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 = 0x0008 +border_fg = 0x00FFFFFF + +# Relative horizontal position from the end of the screen +# default: 0.5 +box_position_h = 0.5 + +# Relative vertical position from the bottom of the screen +# default: 0.5 +box_position_v = 0.5 # Title to show at the top of the main box # If set to null, none will be shown box_title = null -# Brightness increase command -brightness_down_cmd = $PREFIX_DIRECTORY/bin/brightnessctl -q s 10%- +# Brightness decrease command +brightness_down_cmd = $PREFIX_DIRECTORY/bin/brightnessctl -q -n s 10%- -# Brightness decrease key +# Brightness decrease key combination +# If null, the keybind is disabled and isn't shown brightness_down_key = F5 # Brightness increase command -brightness_up_cmd = $PREFIX_DIRECTORY/bin/brightnessctl -q s +10% +brightness_up_cmd = $PREFIX_DIRECTORY/bin/brightnessctl -q -n s +10% -# Brightness increase key +# Brightness increase key combination +# If null, the keybind is disabled and isn't shown brightness_up_key = F6 # Erase password input on failure @@ -88,30 +133,169 @@ clear_password = false clock = null # CMatrix animation foreground color id -cmatrix_fg = 0x0003 +cmatrix_fg = 0x0000FF00 -# Console path -console_dev = /dev/console +# CMatrix animation character string head color id +cmatrix_head_col = 0x01FFFFFF + +# CMatrix animation minimum codepoint. It uses a 16-bit integer +# For Japanese characters for example, you can use 0x3000 here +cmatrix_min_codepoint = 0x21 + +# CMatrix animation maximum codepoint. It uses a 16-bit integer +# For Japanese characters for example, you can use 0x30FF here +cmatrix_max_codepoint = 0x7B + +# Color mixing animation first color id +colormix_col1 = 0x00FF0000 + +# Color mixing animation second color id +colormix_col2 = 0x000000FF + +# Color mixing animation third color id +colormix_col3 = 0x20000000 + +# Screen corners customization +# Keywords: +# shutdown -> Shutdown key +# restart -> Restart key +# britup -> Brightness up key +# britdown -> Brightness down key +# password -> Toggle password key +# clock -> Clock (format defined by 'clock' option) +# tty -> Active TTY number +# battery -> Battery percentage +# version -> Ly version string +# numlock -> Numlock state +# capslock -> Capslock state +# labels -> All custom info labels (lbl:) +# binds -> All custom keybind hints (cmd:) +# lbl:name -> Specific custom info label +# cmd:key -> Specific custom keybind hint +# +# If using a keyword that groups multiple labels into one (e.g. labels, binds), +# they'll be placed horizontally +# +# Also, the order defines the vertical stack (first item is at the edge) +# If items are separted by commas, they'll be placed horizontally +# It is possible to have both horizontal and vertical items on the same corner + +# Bottom left +corner_bottom_left = version + +# Bottom right +corner_bottom_right = labels + +# Top left +corner_top_left = shutdown,restart,britup,britdown,password battery + +# Top right +corner_top_right = clock numlock,capslock + +# For custom binds: the horizontal limit in characters for each +# line of custom binds before moving on to the next. +# If null, defaults to the width of the terminal instead. +custom_bind_width = null + +# 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 = 0x0000 +error_bg = 0x00000000 # Error foreground color id -# Default is red and bold: TB_RED | TB_BOLD -error_fg = 0x0102 +# Default is red and bold +error_fg = 0x01FF0000 # Foreground color id -fg = 0x0008 +fg = 0x00FFFFFF + +# Render true colors (if supported) +# If false, output will be in eight-color mode +# All eight-color mode color codes: +# TB_DEFAULT 0x0000 +# TB_BLACK 0x0001 +# TB_RED 0x0002 +# TB_GREEN 0x0003 +# TB_YELLOW 0x0004 +# TB_BLUE 0x0005 +# TB_MAGENTA 0x0006 +# TB_CYAN 0x0007 +# TB_WHITE 0x0008 +# If full color is off, the styling options still work. The colors are +# always 32-bit values with the styling in the most significant byte. +# Note: If using the dur_file animation option and the dur file's color range +# is saved as 256 with this option disabled, the file will not be drawn. +full_color = true + +# Game of Life entropy interval (0 = disabled, >0 = add entropy every N generations) +# 0 -> Pure Conway's Game of Life (will eventually stabilize) +# 10 -> Add entropy every 10 generations (recommended for continuous activity) +# 50+ -> Less frequent entropy for more natural evolution +gameoflife_entropy_interval = 10 + +# Game of Life animation foreground color id +gameoflife_fg = 0x0000FF00 + +# Game of Life frame delay (lower = faster animation, higher = slower) +# 1-3 -> Very fast animation +# 6 -> Default smooth animation speed +# 10+ -> Slower, more contemplative speed +gameoflife_frame_delay = 6 + +# Game of Life initial cell density (0.0 to 1.0) +# 0.1 -> Sparse, minimal activity +# 0.4 -> Balanced activity (recommended) +# 0.7+ -> Dense, chaotic patterns +gameoflife_initial_density = 0.4 # Remove main box borders hide_borders = false -# Remove power management command hints -hide_key_hints = 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 @@ -124,75 +308,87 @@ input_len = 34 # Available languages are found in $CONFIG_DIRECTORY/ly/lang/ lang = en -# Load the saved desktop and username -load = true - # 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 +# The file pointing to the Lua file to be used when using the Lua animation option +lua_animation_file = $CONFIG_DIRECTORY/ly/example.lua + +# General log file path +# If null, syslog will be used instead +ly_log = /var/log/ly.log + # Main box horizontal margin margin_box_h = 2 # Main box vertical margin margin_box_v = 1 -# Event timeout in milliseconds -min_refresh_delta = 5 - # Set numlock on/off at startup numlock = false # Default path # If null, ly doesn't set a path -path = /sbin:/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin +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 used for restart (F1-F12) +# Specifies the key combination used for restart +# If null, the keybind is disabled and isn't shown restart_key = F2 -# Save the current desktop and login as defaults -save = true +# Absolute directory for the save file +# If null, current desktop & login won't be saved nor loaded +save_file_dir = $CONFIG_DIRECTORY/ly # Service name (set to ly to use the provided pam config file) service_name = ly # Session log file path -# This will contain stdout and stderr of X11 and Wayland sessions +# This will contain stdout and stderr of Wayland sessions # By default it's saved in the user's home directory -# Note: this file won't be used in a shell session (due to the need of stdout and stderr) -session_log = ly-session.log +# 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 -# Command executed when pressing shutdown_key -shutdown_cmd = /sbin/shutdown -a now +# Show the shell session in the session list +# If false, the shell session will be hidden +shell = true -# Specifies the key used for shutdown (F1-F12) +# Specifies the key combination used for showing the password +# If null, the keybind is disabled and isn't shown +show_password_key = F7 + +# Specifies the key combination used for shutdown +# If null, the keybind is disabled and isn't shown shutdown_key = F1 -# Command executed when pressing sleep key (can be null) -sleep_cmd = null - -# Specifies the key used for sleep (F1-F12) -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 -# TTY in use -tty = $DEFAULT_TTY +# If true, user will need to manually type username instead of selecting from the list +# of discovered users +type_username = false # Default vi mode # normal -> normal mode @@ -203,11 +399,20 @@ vi_default_mode = normal vi_mode = false # Wayland desktop environments +# You can specify multiple directories, +# e.g. $PREFIX_DIRECTORY/share/wayland-sessions:$PREFIX_DIRECTORY/local/share/wayland-sessions +# If null, Wayland sessions will not be shown waylandsessions = $PREFIX_DIRECTORY/share/wayland-sessions # Xorg server command +# Add the -quiet argument to hide startup logs from the server x_cmd = $PREFIX_DIRECTORY/bin/X +# Xorg virtual terminal number +# Mostly useful for FreeBSD where choosing the current TTY causes issues +# If null, the current TTY will be chosen +x_vt = null + # Xorg xauthority edition tool xauth_cmd = $PREFIX_DIRECTORY/bin/xauth @@ -216,4 +421,31 @@ xauth_cmd = $PREFIX_DIRECTORY/bin/xauth xinitrc = ~/.xinitrc # Xorg desktop environments +# You can specify multiple directories, +# e.g. $PREFIX_DIRECTORY/share/xsessions:$PREFIX_DIRECTORY/local/share/xsessions +# If null, X11 sessions will not be shown xsessions = $PREFIX_DIRECTORY/share/xsessions + +# Custom Commands and Labels: +# The following examples below give an outline for setting up custom commands and labels. +# Unless specified as optional, an option is mandatory. + +# Comments preceding with '##' are for documentation. +# Comments preceding with '#' comment out the example INI. + +## Declare a command with the F8 binding. +#[cmd:F8] +## The name of the command to show up in Ly. +## Note: "$" in "$brightness_up" fetches the appropriate string from the specified locale file +## and is replaced with the value representing "brightness_up". +## You can see the list of keys in any locale file in $CONFIG_DIRECTORY/ly/lang. +#cmd = touch /tmp/ly.gaming +#name = custom command $brightness_up + +## Declare a label with an ID. This ID should be unique across all labels. +#[lbl:kernel] +#cmd = uname -srn +## Optional, defaulting to 0. +## In frames, the time to re-run the command and update the label. +## If 0, only run once and do not refresh afterwards +#refresh = 0 diff --git a/res/custom-sessions/README b/res/custom-sessions/README new file mode 100644 index 0000000..b706118 --- /dev/null +++ b/res/custom-sessions/README @@ -0,0 +1,23 @@ +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). diff --git a/res/example.dur b/res/example.dur new file mode 100644 index 0000000..00e8206 Binary files /dev/null and b/res/example.dur differ diff --git a/res/example.lua b/res/example.lua new file mode 100644 index 0000000..81f9bc9 --- /dev/null +++ b/res/example.lua @@ -0,0 +1,119 @@ +-- [[ +-- This is an example of using LuaJIT to create a custom animation in Ly, in this case +-- bouncing squares that change colors. +-- +-- You are given the following `ly` table: +-- { +-- height: number -- The height of the terminal +-- width: number -- The width of the terminal +-- putCell(byte, fg, bg, x, y) -- Draw a cell. +-- All arguments to this function are integers, and +-- must be in the unsigned 32-bit integer range: 0 to 2^32-1. +-- If an argument cannot be converted to this range, it will throw +-- an error. +-- +-- For reference, the XY coordinates (0,0) draw a cell on the top-left +-- of the terminal, where the positive-X axis moves right and the +-- positive-Y axis moves down. +-- +-- For arguments fg and bg: they are colors in the format +-- 0xSSRRGGBB, where SS is for styling. See your +-- config.ini for more details. +-- +-- For the byte argument, you may use string.byte to fill this argument. +-- +-- putRect(byte, fg, bg, x, y, w, h) -- Draw a rectangle. +-- Arguments are the same as putCell except for w and h, which are also +-- unsigned integers. The rectangle will be drawn from the top-left, with +-- argument w extending it to the right and argument h extending downwards. +-- +-- +-- putLabel(str, fg, bg, x, y) -- Draw text in argument str. See putCell() +-- for info on the rest of the arguments. +-- +-- clock() -- The time, in microseconds. +-- } +-- +-- A function named `draw()` must be declared in the script. This is ran every +-- frame. +-- +-- In addition to the base library, you are also given the following standard +-- libraries: +-- bit (A library exclusive to LuaJIT, see https://bitop.luajit.org/api.html) +-- math +-- string +-- table +-- +-- The std libraries io and debug are NOT included. +-- +-- ]] + +-- You should probably copy FPS and FPS_COUNT into any future LuaJIT animations +-- you create. +local FPS_COUNT = 40 +local function FPS() + return (1 / FPS_COUNT) * 1000000 +end + + +local SQUARE_WIDTH = 10 +local SQUARE_HEIGHT = 5 + +local SQUARE_COUNT = 25 + +local squares = {} + +for i = 1, SQUARE_COUNT do + local vx = 1 + local vy = 1 + if math.random(1, 2) == 2 then vx = -vx end + if math.random(1, 2) == 2 then vy = -vy end + squares[#squares + 1] = { + x = math.random(1, ly.width - SQUARE_WIDTH), + y = math.random(1, ly.height - SQUARE_HEIGHT), + vx = vx, + vy = vy, + color = math.random(0xFFFFFF) + } +end + +local timer = ly.clock() +local perf = ly.clock() + +function draw() + -- Rather than progressing the animation by frame, do it based on + -- seconds, via ly.clock(). In this timeframe, you can update the animation + -- state. + -- DO NOT DRAW CELLS IN THIS TIMEFRAME. You will get flickering. + + -- if this check passes, we can update the animation + if timer + FPS() < ly.clock() then + for i, v in ipairs(squares) do + v.x = v.x + v.vx + v.y = v.y + v.vy + if v.x == 0 then + v.vx = 1; v.color = math.random(0xFFFFFF) + end + if v.x + SQUARE_WIDTH >= ly.width - 1 then + v.vx = -1; v.color = math.random(0xFFFFFF) + end + if v.y == 0 then + v.vy = 1; v.color = math.random(0xFFFFFF) + end + if v.y + SQUARE_HEIGHT >= ly.height - 1 then + v.vy = -1; v.color = math.random(0xFFFFFF) + end + end + timer = ly.clock() + end + + + for i, v in ipairs(squares) do + ly.putRect(string.byte(' '), 0, v.color, v.x, v.y, SQUARE_WIDTH, SQUARE_HEIGHT) + end + + local new_perf = ly.clock() + local str = "FT: " .. ((new_perf - perf) / 1000) .. "ms" + ly.putLabel(str, 0x00FFFFFF, 0, (ly.width / 2) - (string.len(str) / 2), ly.height - 1) + perf = new_perf +end diff --git a/res/lang/ar.ini b/res/lang/ar.ini new file mode 100644 index 0000000..fd51b51 --- /dev/null +++ b/res/lang/ar.ini @@ -0,0 +1,82 @@ +authenticating = جاري المصادقة... +brightness_down = خفض السطوع +brightness_up = رفع السطوع +capslock = capslock +custom = مخصص +custom_info_err_output_long = الإخراج طويل جداً +custom_info_err_no_output = لا يوجد إخراج +custom_info_err_no_output_error = ، خطأ محتمل +err_alloc = فشل في تخصيص الذاكرة +err_args = تعذر تحليل وسيطات سطر الأوامر +err_autologin_session = لم يتم العثور على جلسة تسجيل الدخول التلقائي +err_bounds = out-of-bounds index +err_brightness_change = فشل في تغيير سطوع الشاشة +err_chdir = فشل في فتح مجلد المنزل +err_clock_too_long = نص الساعة طويل جداً +err_config = فشل في تفسير ملف الإعدادات +err_crawl = فشل الزحف في أدلة الجلسة +err_dgn_oob = رسالة سجل (Log) +err_domain = اسم نطاق غير صالح +err_empty_password = لا يُسمح بكلمة مرور فارغة +err_envlist = فشل في جلب قائمة المتغيرات البيئية +err_get_active_tty = فشل الحصول على tty النشط +err_hibernate = فشل تنفيذ أمر الإسبات +err_hostname = فشل في جلب اسم المضيف (Hostname) +err_inactivity = فشل تنفيذ أمر عدم النشاط +err_lock_state = فشل الحصول على حالة القفل +err_log = فشل فتح ملف السجل +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_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 = فشل في العثور على مجلد Xsessions +err_xsessions_open = فشل في فتح مجلد Xsessions +hibernate = إسبات +insert = ادخال +login = تسجيل الدخول +logout = تم تسجيل خروجك +no_x11_support = دعم x11 معطّل في وقت الترجمة +normal = عادي +numlock = numlock +other = اخر +password = كلمة السر +restart = اعادة التشغيل +shell = shell +shutdown = ايقاف التشغيل +sleep = وضع السكون +toggle_password = إظهار/إخفاء كلمة المرور +wayland = wayland +x11 = x11 +xinitrc = xinitrc diff --git a/res/lang/bg.ini b/res/lang/bg.ini new file mode 100644 index 0000000..1ac9246 --- /dev/null +++ b/res/lang/bg.ini @@ -0,0 +1,82 @@ +authenticating = удостоверяване... +brightness_down = намаляване на яркостта +brightness_up = увеличаване на яркостта +capslock = caps lock +custom = персонализирано +custom_info_err_output_long = резултатът е твърде дълъг +custom_info_err_no_output = няма резултат +custom_info_err_no_output_error = , възможна грешка +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 = заспиване +toggle_password = превключване на паролата +wayland = wayland +x11 = x11 +xinitrc = xinitrc diff --git a/res/lang/cat.ini b/res/lang/cat.ini index de598fa..3bf271b 100644 --- a/res/lang/cat.ini +++ b/res/lang/cat.ini @@ -1,45 +1,82 @@ +authenticating = autenticant... +brightness_down = abaixar brillantor +brightness_up = apujar brillantor capslock = Bloq Majús -err_alloc = falla d'assignació de memòria -err_bounds = índex fora de límit -err_chdir = error al obrir carpeta home -err_console_dev = error al accedir a la consola +custom = personalitzat +custom_info_err_output_long = sortida massa llarga +custom_info_err_no_output = sense sortida +custom_info_err_no_output_error = , possible error +err_alloc = assignació de memòria fallida +err_args = no s'han pogut analitzar els arguments de la línia d'ordres +err_autologin_session = no s'ha trobat la sessió d'inici de sessió automàtic +err_bounds = índex fora de límits +err_brightness_change = error en canviar la brillantor +err_chdir = error en obrir la carpeta home +err_clock_too_long = la cadena del rellotge és massa llarga +err_config = no s'ha pogut analitzar el fitxer de configuració +err_crawl = no s'han pogut explorar els directoris de sessió err_dgn_oob = missatge de registre err_domain = domini invàlid -err_hostname = error al obtenir el nom del host -err_mlock = error al bloquejar la clau de memòria +err_empty_password = no es permet la contrasenya buida +err_envlist = error en obtenir l'envlist +err_get_active_tty = no s'ha pogut obtenir el tty actiu +err_hibernate = no s'ha pogut executar l'ordre d'hibernació +err_hostname = error en obtenir el nom de l'amfitrió +err_inactivity = no s'ha pogut executar l'ordre d'inactivitat +err_lock_state = no s'ha pogut obtenir l'estat de bloqueig +err_log = no s'ha pogut obrir el fitxer de registre +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 al obtenir informació de l'usuari +err_pam_authinfo_unavail = error en obtenir la informació de l'usuari err_pam_authok_reqd = token expirat -err_pam_buf = error de la memòria intermitja -err_pam_cred_err = error al establir les credencials +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 al obtenir credencials -err_pam_maxtries = s'ha assolit al màxim nombre d'intents +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 al establir la ruta -err_perm_dir = error al canviar de directori actual -err_perm_group = error al degradar els permisos de grup -err_perm_user = error al degradar els permisos de l'usuari -err_pwnam = error al obtenir la informació de l'usuari -err_user_gid = error al establir el GID de l'usuari -err_user_init = error al inicialitzar usuari -err_user_uid = error al establir el UID de l'usuari -err_xsessions_dir = error al cercar la carpeta de sessions -err_xsessions_open = error al obrir la carpeta de sessions +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_sleep = no s'ha pogut executar l'ordre de suspensió +err_start = no s'ha pogut executar l'ordre d'inici +err_battery = no s'ha pogut carregar l'estat de la bateria +err_switch_tty = no s'ha pogut canviar de tty +err_tty_ctrl = ha fallat la transferència del control tty +err_no_users = no s'han trobat usuaris +err_uid_range = no s'ha pogut obtenir dinàmicament el rang d'uid +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 +hibernate = hibernar +insert = inserir login = iniciar sessió -logout = tancar sessió +logout = sessió tancada +no_x11_support = suport x11 desactivat en temps de compilació +normal = normal numlock = Bloq Num +other = altres password = Clau restart = reiniciar shell = shell shutdown = aturar +sleep = suspendre +toggle_password = mostrar/amagar contrasenya wayland = wayland +x11 = x11 xinitrc = xinitrc diff --git a/res/lang/cs.ini b/res/lang/cs.ini index b6a217f..4f9456c 100644 --- a/res/lang/cs.ini +++ b/res/lang/cs.ini @@ -1,13 +1,33 @@ +authenticating = ověřování... +brightness_down = snížit jas +brightness_up = zvýšit jas capslock = capslock +custom = vlastní +custom_info_err_output_long = výstup je příliš dlouhý +custom_info_err_no_output = žádný výstup +custom_info_err_no_output_error = , možná chyba err_alloc = alokace paměti selhala +err_args = nelze analyzovat argumenty příkazového řádku +err_autologin_session = relace automatického přihlášení nebyla nalezena err_bounds = index je mimo hranice pole +err_brightness_change = nepodařilo se změnit jas err_chdir = nelze otevřít domovský adresář -err_console_dev = chyba při přístupu do konzole +err_clock_too_long = řetězec hodin je příliš dlouhý +err_config = nelze analyzovat konfigurační soubor +err_crawl = nepodařilo se prohledat adresáře relací err_dgn_oob = zpráva protokolu err_domain = neplatná doména +err_empty_password = prázdné heslo není povoleno +err_envlist = nepodařilo se získat seznam proměnných prostředí +err_get_active_tty = nepodařilo se získat aktivní tty +err_hibernate = nepodařilo se spustit příkaz hibernace err_hostname = nelze získat název hostitele +err_inactivity = nepodařilo se spustit příkaz nečinnosti +err_lock_state = nepodařilo se získat stav zámku +err_log = nepodařilo se otevřít soubor protokolu err_mlock = uzamčení paměti hesel selhalo err_null = nulový ukazatel +err_numlock = nepodařilo se nastavit numlock err_pam = pam transakce selhala err_pam_abort = pam transakce přerušena err_pam_acct_expired = platnost účtu vypršela @@ -29,17 +49,34 @@ 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_sleep = nepodařilo se spustit příkaz spánku +err_start = nepodařilo se spustit příkaz spuštění +err_battery = nepodařilo se načíst stav baterie +err_switch_tty = nepodařilo se přepnout tty +err_tty_ctrl = přenos řízení tty selhal +err_no_users = nebyli nalezeni žádní uživatelé +err_uid_range = nepodařilo se dynamicky získat rozsah uid err_user_gid = nastavení GID uživatele selhalo err_user_init = inicializace uživatele selhala err_user_uid = nastavení UID uživateli selhalo +err_xauth = příkaz xauth selhal +err_xcb_conn = připojení xcb selhalo err_xsessions_dir = nepodařilo se najít složku relací err_xsessions_open = nepodařilo se otevřít složku relací +hibernate = hibernace +insert = vložit login = uživatel logout = odhlášen +no_x11_support = podpora x11 zakázána při kompilaci +normal = normální numlock = numlock +other = jiné password = heslo restart = restartovat shell = příkazový řádek shutdown = vypnout +sleep = uspat +toggle_password = zobrazit/skrýt heslo wayland = wayland +x11 = x11 xinitrc = xinitrc diff --git a/res/lang/de.ini b/res/lang/de.ini index e18b9ba..ca3a0ef 100644 --- a/res/lang/de.ini +++ b/res/lang/de.ini @@ -1,45 +1,82 @@ +authenticating = authentifizieren... +brightness_down = Helligkeit- +brightness_up = Helligkeit+ capslock = Feststelltaste +custom = benutzerdefiniert +custom_info_err_output_long = Ausgabe zu lang +custom_info_err_no_output = keine Ausgabe +custom_info_err_no_output_error = , möglicher Fehler err_alloc = Speicherzuweisung fehlgeschlagen -err_bounds = Listenindex ist außerhalb des Bereichs -err_chdir = Fehler beim oeffnen des home-ordners -err_console_dev = Zugriff auf die Konsole fehlgeschlagen -err_dgn_oob = Protokoll Nachricht -err_domain = Unzulaessige domain -err_hostname = Holen des Hostnames fehlgeschlagen -err_mlock = Abschließen des Passwortspeichers fehlgeschlagen -err_null = Null Zeiger -err_pam = pam Transaktion fehlgeschlagen -err_pam_abort = pam Transaktion abgebrochen +err_args = Kommandozeilenargumente konnten nicht verarbeitet werden +err_autologin_session = Autologin-Sitzung nicht gefunden +err_bounds = Index ausserhalb des Bereichs +err_brightness_change = Helligkeitsänderung fehlgeschlagen +err_chdir = Fehler beim Oeffnen des Home-Ordners +err_clock_too_long = Uhrzeitzeichenkette zu lang +err_config = Fehler beim Verarbeiten der Konfigurationsdatei +err_crawl = Sitzungsverzeichnisse konnten nicht durchsucht werden +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_get_active_tty = Aktives tty konnte nicht ermittelt werden +err_hibernate = Ruhezustand-Befehl konnte nicht ausgeführt werden +err_hostname = Abrufen des Hostnames fehlgeschlagen +err_inactivity = Inaktivitätsbefehl konnte nicht ausgeführt werden +err_lock_state = Sperrstatus konnte nicht ermittelt werden +err_log = Protokolldatei konnte nicht geöffnet werden +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 = Authentifizierungs Fehler -err_pam_authinfo_unavail = holen der Benutzerinformationen fehlgeschlagen -err_pam_authok_reqd = Schluessel 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_err = Fehler beim Setzen der Anmeldedaten err_pam_cred_expired = Anmeldedaten abgelaufen err_pam_cred_insufficient = Anmeldedaten unzureichend -err_pam_cred_unavail = Fehler beim holen der Anmeldedaten -err_pam_maxtries = Maximale Versuche erreicht -err_pam_perm_denied = Zugriff Verweigert +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 = Fehler beim wechseln des Ordners -err_perm_group = Fehler beim heruntersetzen der Gruppen Berechtigungen -err_perm_user = Fehler beim heruntersetzen der Nutzer Berechtigungen -err_pwnam = Holen der Benutzerinformationen fehlgeschlagen -err_user_gid = Fehler beim setzen der Gruppen Id des Nutzers -err_user_init = Initialisierung des Nutzers fehlgeschlagen -err_user_uid = Setzen der Benutzer Id fehlgeschlagen -err_xsessions_dir = Fehler beim finden des Sitzungsordners -err_xsessions_open = Fehler beim öffnen des Sitzungsordners -login = Anmelden -logout = Abgemeldet -numlock = Numtaste +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_start = Startbefehl konnte nicht ausgeführt werden +err_battery = Akkustand konnte nicht geladen werden +err_switch_tty = tty konnte nicht gewechselt werden +err_tty_ctrl = Fehler bei der TTY-Uebergabe +err_no_users = Keine Benutzer gefunden +err_uid_range = uid-Bereich konnte nicht dynamisch ermittelt werden +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 +hibernate = Ruhezustand +insert = Einfügen +login = Nutzer +logout = Abmelden +no_x11_support = x11-Unterstützung zur Kompilierzeit deaktiviert +normal = Normal +numlock = Numlock +other = Andere password = Passwort restart = Neustarten -shell = shell +shell = Shell shutdown = Herunterfahren +sleep = Sleep +toggle_password = Passwort anzeigen/verbergen wayland = wayland +x11 = x11 xinitrc = xinitrc diff --git a/res/lang/en.ini b/res/lang/en.ini index b2f52ae..b3d40f2 100644 --- a/res/lang/en.ini +++ b/res/lang/en.ini @@ -2,15 +2,29 @@ authenticating = authenticating... brightness_down = decrease brightness brightness_up = increase brightness capslock = capslock +custom = custom +custom_info_err_output_long = output too long +custom_info_err_no_output = no output +custom_info_err_no_output_error = , possible error 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_console_dev = failed to access console +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 @@ -35,7 +49,13 @@ 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_unknown = an unknown error occurred +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 @@ -43,17 +63,20 @@ 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 -normal = normal 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 -xinitrc = xinitrc x11 = x11 +xinitrc = xinitrc diff --git a/res/lang/eo.ini b/res/lang/eo.ini new file mode 100644 index 0000000..c8cbb96 --- /dev/null +++ b/res/lang/eo.ini @@ -0,0 +1,82 @@ +authenticating = aŭtentigado... +brightness_down = malpliigi helecon +brightness_up = pliigi helecon +capslock = majuskla baskulo +custom = propra +custom_info_err_output_long = eligo tro longa +custom_info_err_no_output = neniu eligo +custom_info_err_no_output_error = , ebla eraro +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 +toggle_password = montri/kaŝi pasvorton +wayland = wayland +x11 = x11 +xinitrc = xinitrc diff --git a/res/lang/es.ini b/res/lang/es.ini index 73a9acd..f587fe7 100644 --- a/res/lang/es.ini +++ b/res/lang/es.ini @@ -2,15 +2,32 @@ authenticating = autenticando... brightness_down = bajar brillo brightness_up = subir brillo capslock = Bloq Mayús +custom = personalizado +custom_info_err_output_long = salida demasiado larga +custom_info_err_no_output = sin salida +custom_info_err_no_output_error = , posible error err_alloc = asignación de memoria fallida +err_args = no se pudieron analizar los argumentos de la línea de comandos +err_autologin_session = no se encontró la sesión de inicio de sesión automático err_bounds = índice fuera de límites +err_brightness_change = no se pudo cambiar el brillo err_chdir = error al abrir la carpeta home -err_console_dev = error al acceder a la consola +err_clock_too_long = la cadena del reloj es demasiado larga +err_config = no se pudo analizar el archivo de configuración +err_crawl = no se pudieron explorar los directorios de sesión err_dgn_oob = mensaje de registro err_domain = dominio inválido +err_empty_password = no se permite contraseña vacía +err_envlist = no se pudo obtener la lista de variables de entorno +err_get_active_tty = no se pudo obtener el tty activo +err_hibernate = no se pudo ejecutar el comando de hibernación err_hostname = error al obtener el nombre de host +err_inactivity = no se pudo ejecutar el comando de inactividad +err_lock_state = no se pudo obtener el estado de bloqueo +err_log = no se pudo abrir el archivo de registro err_mlock = error al bloquear la contraseña de memoria err_null = puntero nulo +err_numlock = no se pudo configurar numlock err_pam = error en la transacción pam err_pam_abort = transacción pam abortada err_pam_acct_expired = cuenta expirada @@ -32,15 +49,25 @@ 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_sleep = no se pudo ejecutar el comando de suspensión +err_start = no se pudo ejecutar el comando de inicio +err_battery = no se pudo cargar el estado de la batería +err_switch_tty = no se pudo cambiar de tty +err_tty_ctrl = falló la transferencia de control tty +err_no_users = no se encontraron usuarios +err_uid_range = no se pudo obtener dinámicamente el rango de uid 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_xauth = falló el comando xauth +err_xcb_conn = falló la conexión xcb err_xsessions_dir = error al buscar la carpeta de sesiones err_xsessions_open = error al abrir la carpeta de sesiones +hibernate = hibernar insert = insertar login = usuario logout = cerrar sesión -no_x11_support = soporte para x11 deshabilitado en tiempo de compilación +no_x11_support = soporte x11 desactivado en tiempo de compilación normal = normal numlock = Bloq Num other = otro @@ -49,5 +76,7 @@ restart = reiniciar shell = shell shutdown = apagar sleep = suspender +toggle_password = mostrar/ocultar contraseña wayland = wayland +x11 = x11 xinitrc = xinitrc diff --git a/res/lang/fr.ini b/res/lang/fr.ini index 5685c95..472115e 100644 --- a/res/lang/fr.ini +++ b/res/lang/fr.ini @@ -1,16 +1,30 @@ -authenticating = authentification... +authenticating = authentification... brightness_down = diminuer la luminosité brightness_up = augmenter la luminosité capslock = verr.maj +custom = customisé +custom_info_err_output_long = sortie trop longue +custom_info_err_no_output = pas de sortie +custom_info_err_no_output_error = , erreur possible 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_console_dev = échec d'accès à la console +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 @@ -35,7 +49,13 @@ 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_unknown = une erreur inconnue est survenue +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 @@ -43,17 +63,20 @@ 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é -normal = normal 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 +toggle_password = afficher le mot de passe wayland = wayland -xinitrc = xinitrc x11 = x11 +xinitrc = xinitrc diff --git a/res/lang/it.ini b/res/lang/it.ini index 13eb147..9c9679d 100644 --- a/res/lang/it.ini +++ b/res/lang/it.ini @@ -1,13 +1,33 @@ +authenticating = autenticazione in corso... +brightness_down = diminuisci luminosità +brightness_up = aumenta luminosità capslock = capslock +custom = personalizzato +custom_info_err_output_long = output troppo lungo +custom_info_err_no_output = nessun output +custom_info_err_no_output_error = , possibile errore err_alloc = impossibile allocare memoria +err_args = impossibile analizzare gli argomenti della riga di comando +err_autologin_session = sessione di accesso automatico non trovata err_bounds = indice fuori limite +err_brightness_change = impossibile modificare la luminosità err_chdir = impossibile aprire home directory -err_console_dev = impossibile aprire console +err_clock_too_long = stringa dell'orologio troppo lunga +err_config = impossibile analizzare il file di configurazione +err_crawl = impossibile esplorare le directory delle sessioni err_dgn_oob = messaggio log err_domain = dominio non valido +err_empty_password = password vuota non consentita +err_envlist = impossibile ottenere la lista delle variabili d'ambiente +err_get_active_tty = impossibile ottenere il tty attivo +err_hibernate = impossibile eseguire il comando di ibernazione err_hostname = impossibile ottenere hostname +err_inactivity = impossibile eseguire il comando di inattività +err_lock_state = impossibile ottenere lo stato di blocco +err_log = impossibile aprire il file di log err_mlock = impossibile ottenere lock per la password in memoria err_null = puntatore nullo +err_numlock = impossibile impostare il numlock err_pam = transazione PAM fallita err_pam_abort = transazione PAM interrotta err_pam_acct_expired = account scaduto @@ -29,17 +49,34 @@ 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_sleep = impossibile eseguire il comando di sospensione +err_start = impossibile eseguire il comando di avvio +err_battery = impossibile caricare lo stato della batteria +err_switch_tty = impossibile cambiare tty +err_tty_ctrl = trasferimento del controllo tty fallito +err_no_users = nessun utente trovato +err_uid_range = impossibile ottenere dinamicamente l'intervallo uid err_user_gid = impossibile impostare GID utente err_user_init = impossibile inizializzare utente err_user_uid = impossible impostare UID utente +err_xauth = comando xauth fallito +err_xcb_conn = connessione xcb fallita err_xsessions_dir = impossibile localizzare cartella sessioni err_xsessions_open = impossibile aprire cartella sessioni +hibernate = ibernazione +insert = inserisci login = username logout = scollegato +no_x11_support = supporto x11 disabilitato in fase di compilazione +normal = normale numlock = numlock +other = altro password = password restart = riavvio shell = shell shutdown = arresto +sleep = sospendi +toggle_password = mostra/nascondi password wayland = wayland +x11 = x11 xinitrc = xinitrc diff --git a/res/lang/ja_JP.ini b/res/lang/ja_JP.ini new file mode 100644 index 0000000..a14feab --- /dev/null +++ b/res/lang/ja_JP.ini @@ -0,0 +1,82 @@ +authenticating = 認証中... +brightness_down = 明るさを下げる +brightness_up = 明るさを上げる +capslock = CapsLock +custom = カスタム +custom_info_err_output_long = 出力が長すぎます +custom_info_err_no_output = 出力なし +custom_info_err_no_output_error = 、エラーの可能性あり +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 = 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_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 = NumLock +other = その他 +password = パスワード +restart = 再起動 +shell = シェル +shutdown = シャットダウン +sleep = スリープ +toggle_password = パスワードの表示/非表示 +wayland = Wayland +x11 = x11 +xinitrc = xinitrc diff --git a/res/lang/ku.ini b/res/lang/ku.ini new file mode 100644 index 0000000..0c39030 --- /dev/null +++ b/res/lang/ku.ini @@ -0,0 +1,82 @@ +authenticating = tê piştrastkirin... +brightness_down = ronahiyê kêm bike +brightness_up = ronahiyê bilind bike +capslock = tîpên girdek (capslock) +custom = kesane +custom_info_err_output_long = encam pir dirêj e +custom_info_err_no_output = encam tune +custom_info_err_no_output_error = , xeletiya mimkun +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ê +toggle_password = şîfre nîşan bide/veşêre +wayland = wayland +x11 = x11 +xinitrc = xinitrc diff --git a/res/lang/lv.ini b/res/lang/lv.ini new file mode 100644 index 0000000..8f54d89 --- /dev/null +++ b/res/lang/lv.ini @@ -0,0 +1,82 @@ +authenticating = autentificējas... +brightness_down = samazināt spilgtumu +brightness_up = palielināt spilgtumu +capslock = caps lock +custom = pielāgots +custom_info_err_output_long = izvade pārāk gara +custom_info_err_no_output = nav izvades +custom_info_err_no_output_error = , iespējama kļūda +err_alloc = neizdevās atmiņas piešķiršana +err_args = nevar parsēt komandrindas argumentus +err_autologin_session = automātiskās pieteikšanās sesija nav atrasta +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_crawl = neizdevās pārlūkot sesiju direktorijus +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_hibernate = neizdevās izpildīt hibernācijas komandu +err_hostname = neizdevās iegūt hostname +err_inactivity = neizdevās izpildīt neaktivitātes komandu +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_start = neizdevās izpildīt startēšanas 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_uid_range = neizdevās dinamiski iegūt uid diapazonu +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 +hibernate = hibernācija +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 +toggle_password = rādīt/slēpt paroli +wayland = wayland +x11 = x11 +xinitrc = xinitrc diff --git a/res/lang/normalize_lang_files.py b/res/lang/normalize_lang_files.py new file mode 100755 index 0000000..3904737 --- /dev/null +++ b/res/lang/normalize_lang_files.py @@ -0,0 +1,49 @@ +#!/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() diff --git a/res/lang/pl.ini b/res/lang/pl.ini index f37c4a8..536680d 100644 --- a/res/lang/pl.ini +++ b/res/lang/pl.ini @@ -1,45 +1,82 @@ +authenticating = uwierzytelnianie... +brightness_down = zmniejsz jasność +brightness_up = zwiększ jasność capslock = capslock +custom = własny +custom_info_err_output_long = wyjście zbyt długie +custom_info_err_no_output = brak wyjścia +custom_info_err_no_output_error = , możliwy błąd err_alloc = nieudana alokacja pamięci -err_bounds = indeks poza granicami +err_args = nie można przetworzyć argumentów wiersza poleceń +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_console_dev = nie udało się uzyskać dostępu do konsoli +err_clock_too_long = ciąg znaków zegara jest za długi +err_config = nie można przetworzyć pliku konfiguracyjnego +err_crawl = nie udało się przeszukać katalogów sesji 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_hibernate = nie udało się wykonać polecenia hibernacji err_hostname = nie udało się uzyskać nazwy hosta +err_inactivity = nie udało się wykonać polecenia nieaktywności +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 = wskaźnik zerowy +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 autentyfikacji +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_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_cred_unavail = nie udało się uzyskać uwierzytelnienia err_pam_maxtries = osiągnięto limit prób -err_pam_perm_denied = brak uprawnień +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_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_start = nie udało się wykonać polecenia startowego +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_uid_range = nie udało się dynamicznie pobrać zakresu uid err_user_gid = nie udało się ustawić GID użytkownika -err_user_init = nie udało się zainicjalizować użytkownika +err_user_init = nie udało się zainicjalizować użytkownika err_user_uid = nie udało się ustawić UID użytkownika -err_xsessions_dir = nie udało się znaleźć folderu sesji -err_xsessions_open = nie udało się otworzyć folderu sesji +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 +hibernate = hibernuj +insert = wstaw login = login logout = wylogowano +no_x11_support = obsługa x11 wyłączona podczas kompilacji +normal = normalny numlock = numlock +other = inny password = hasło restart = uruchom ponownie shell = powłoka shutdown = wyłącz +sleep = uśpij +toggle_password = Pokaż/ukryj hasło wayland = wayland +x11 = x11 xinitrc = xinitrc diff --git a/res/lang/pt.ini b/res/lang/pt.ini index ad2c397..204a9f1 100644 --- a/res/lang/pt.ini +++ b/res/lang/pt.ini @@ -1,13 +1,33 @@ +authenticating = a autenticar... +brightness_down = diminuir brilho +brightness_up = aumentar brilho capslock = capslock +custom = personalizado +custom_info_err_output_long = saída demasiado longa +custom_info_err_no_output = sem saída +custom_info_err_no_output_error = , possível erro err_alloc = erro na atribuição de memória +err_args = não foi possível analisar os argumentos da linha de comandos +err_autologin_session = sessão de início de sessão automático não encontrada err_bounds = índice fora de limites +err_brightness_change = não foi possível alterar o brilho err_chdir = erro ao abrir a pasta home -err_console_dev = erro ao aceder à consola +err_clock_too_long = a cadeia de caracteres do relógio é demasiado longa +err_config = não foi possível analisar o ficheiro de configuração +err_crawl = não foi possível explorar os diretórios de sessão err_dgn_oob = mensagem de registo err_domain = domínio inválido +err_empty_password = palavra-passe vazia não é permitida +err_envlist = não foi possível obter a lista de variáveis de ambiente +err_get_active_tty = não foi possível obter o tty ativo +err_hibernate = não foi possível executar o comando de hibernação err_hostname = erro ao obter o nome do host +err_inactivity = não foi possível executar o comando de inatividade +err_lock_state = não foi possível obter o estado de bloqueio +err_log = não foi possível abrir o ficheiro de registo err_mlock = erro de bloqueio de memória err_null = ponteiro nulo +err_numlock = não foi possível definir o numlock err_pam = erro na transação pam err_pam_abort = transação pam abortada err_pam_acct_expired = conta expirada @@ -29,17 +49,34 @@ 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_sleep = não foi possível executar o comando de suspensão +err_start = não foi possível executar o comando de início +err_battery = não foi possível carregar o estado da bateria +err_switch_tty = não foi possível mudar de tty +err_tty_ctrl = falhou a transferência de controlo do tty +err_no_users = nenhum utilizador encontrado +err_uid_range = não foi possível obter dinamicamente o intervalo de uid 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_xauth = o comando xauth falhou +err_xcb_conn = a ligação xcb falhou err_xsessions_dir = erro ao localizar a pasta das sessões err_xsessions_open = erro ao abrir a pasta das sessões +hibernate = hibernar +insert = inserir login = iniciar sessão logout = terminar sessão +no_x11_support = suporte a x11 desativado em tempo de compilação +normal = normal numlock = numlock +other = outro password = palavra-passe restart = reiniciar shell = shell shutdown = encerrar +sleep = suspender +toggle_password = mostrar/ocultar palavra-passe wayland = wayland +x11 = x11 xinitrc = xinitrc diff --git a/res/lang/pt_BR.ini b/res/lang/pt_BR.ini index 017129a..af8c970 100644 --- a/res/lang/pt_BR.ini +++ b/res/lang/pt_BR.ini @@ -1,19 +1,39 @@ +authenticating = autenticando... +brightness_down = diminuir brilho +brightness_up = aumentar brilho capslock = caixa alta +custom = personalizado +custom_info_err_output_long = saída muito longa +custom_info_err_no_output = sem saída +custom_info_err_no_output_error = , possível erro err_alloc = alocação de memória malsucedida +err_args = não foi possível analisar os argumentos da linha de comando +err_autologin_session = sessão de login automático não encontrada err_bounds = índice fora de limites +err_brightness_change = não foi possível alterar o brilho err_chdir = não foi possível abrir o diretório home -err_console_dev = não foi possível acessar o console +err_clock_too_long = a string do relógio é muito longa +err_config = não foi possível analisar o arquivo de configuração +err_crawl = não foi possível explorar os diretórios de sessão err_dgn_oob = mensagem de log err_domain = domínio inválido +err_empty_password = senha vazia não é permitida +err_envlist = não foi possível obter a lista de variáveis de ambiente +err_get_active_tty = não foi possível obter o tty ativo +err_hibernate = não foi possível executar o comando de hibernação err_hostname = não foi possível obter o nome do host +err_inactivity = não foi possível executar o comando de inatividade +err_lock_state = não foi possível obter o estado de bloqueio +err_log = não foi possível abrir o arquivo de log err_mlock = bloqueio da memória de senha malsucedido err_null = ponteiro nulo +err_numlock = não foi possível definir o numlock 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_authok_reqd = token expirado err_pam_buf = erro de buffer de memória err_pam_cred_err = erro para definir credenciais err_pam_cred_expired = credenciais expiradas @@ -29,17 +49,34 @@ 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_sleep = não foi possível executar o comando de suspensão +err_start = não foi possível executar o comando de início +err_battery = não foi possível carregar o status da bateria +err_switch_tty = não foi possível mudar de tty +err_tty_ctrl = a transferência de controle do tty falhou +err_no_users = nenhum usuário encontrado +err_uid_range = não foi possível obter dinamicamente o intervalo de uid 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_xauth = o comando xauth falhou +err_xcb_conn = a conexão xcb falhou 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 +hibernate = hibernar +insert = inserir login = conectar logout = desconectado +no_x11_support = suporte a x11 desativado em tempo de compilação +normal = normal numlock = numlock +other = outro password = senha restart = reiniciar shell = shell shutdown = desligar +sleep = suspender +toggle_password = mostrar/ocultar senha wayland = wayland +x11 = x11 xinitrc = xinitrc diff --git a/res/lang/ro.ini b/res/lang/ro.ini index 884e9da..3b771a8 100644 --- a/res/lang/ro.ini +++ b/res/lang/ro.ini @@ -1,14 +1,34 @@ +authenticating = autentificare... +brightness_down = scade luminozitatea +brightness_up = crește luminozitatea capslock = capslock - - - -err_console_dev = nu s-a putut accesa consola - - - - - - +custom = personalizat +custom_info_err_output_long = ieșire prea lungă +custom_info_err_no_output = fără ieșire +custom_info_err_no_output_error = , posibil eroare +err_alloc = alocare de memorie eșuată +err_args = imposibil de analizat argumentele liniei de comandă +err_autologin_session = sesiunea de autentificare automată nu a fost găsită +err_bounds = index în afara limitelor +err_brightness_change = imposibil de schimbat luminozitatea +err_chdir = imposibil de deschis folderul de acasă +err_clock_too_long = șirul de ceas este prea lung +err_config = imposibil de analizat fișierul de configurare +err_crawl = imposibil de explorat directoarele de sesiune +err_dgn_oob = mesaj jurnal +err_domain = domeniu invalid +err_empty_password = parola goală nu este permisă +err_envlist = imposibil de obținut lista de variabile de mediu +err_get_active_tty = imposibil de obținut tty-ul activ +err_hibernate = imposibil de executat comanda de hibernare +err_hostname = imposibil de obținut numele gazdei +err_inactivity = imposibil de executat comanda de inactivitate +err_lock_state = imposibil de obținut starea de blocare +err_log = imposibil de deschis fișierul jurnal +err_mlock = imposibil de blocat memoria parolei +err_null = pointer nul +err_numlock = imposibil de setat numlock +err_pam = tranzacție pam eșuată err_pam_abort = tranzacţie pam anulată err_pam_acct_expired = cont expirat err_pam_auth = eroare de autentificare @@ -24,22 +44,39 @@ 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_path = imposibil de setat calea 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 - - - - - - +err_pwnam = imposibil de obținut informații despre utilizator +err_sleep = imposibil de executat comanda de repaus +err_start = imposibil de executat comanda de pornire +err_battery = imposibil de încărcat starea bateriei +err_switch_tty = imposibil de comutat tty +err_tty_ctrl = transferul controlului tty a eșuat +err_no_users = niciun utilizator găsit +err_uid_range = imposibil de obținut dinamic intervalul uid +err_user_gid = imposibil de setat GID-ul utilizatorului +err_user_init = imposibil de inițializa utilizatorul +err_user_uid = imposibil de setat UID-ul utilizatorului +err_xauth = comanda xauth a eșuat +err_xcb_conn = conexiunea xcb a eșuat +err_xsessions_dir = imposibil de găsit folderul de sesiuni +err_xsessions_open = imposibil de deschis folderul de sesiuni +hibernate = hibernare +insert = inserare login = utilizator logout = opreşte sesiunea +no_x11_support = suportul x11 dezactivat la compilare +normal = normal numlock = numlock +other = altul password = parolă restart = resetează shell = shell shutdown = opreşte sistemul +sleep = repaus +toggle_password = afișare/ascundere parolă wayland = wayland +x11 = x11 xinitrc = xinitrc diff --git a/res/lang/ru.ini b/res/lang/ru.ini index 356ce5f..014c170 100644 --- a/res/lang/ru.ini +++ b/res/lang/ru.ini @@ -1,13 +1,33 @@ +authenticating = аутентификация... +brightness_down = уменьшить яркость +brightness_up = увеличить яркость capslock = capslock +custom = пользовательский +custom_info_err_output_long = вывод слишком длинный +custom_info_err_no_output = нет вывода +custom_info_err_no_output_error = , возможная ошибка err_alloc = не удалось выделить память +err_args = не удалось разобрать аргументы командной строки +err_autologin_session = не найдена сессия с автологином err_bounds = за пределами индекса +err_brightness_change = не удалось изменить яркость err_chdir = не удалось открыть домашнюю папку -err_console_dev = не удалось получить доступ к консоли +err_clock_too_long = строка часов слишком длинная +err_config = не удалось разобрать файл конфигурации +err_crawl = не удалось просканировать каталоги сессии err_dgn_oob = отладочное сообщение (log) err_domain = неверный домен +err_empty_password = пустой пароль не допустим +err_envlist = не удалось получить список переменных среды +err_get_active_tty = не удалось получить активный tty +err_hibernate = не удалось выполнить команду гибернации err_hostname = не удалось получить имя хоста +err_inactivity = не удалось выполнить команду бездействия +err_lock_state = не удалось получить состояние lock +err_log = не удалось открыть файл log err_mlock = сбой блокировки памяти err_null = нулевой указатель +err_numlock = не удалось установить numlock err_pam = pam транзакция не удалась err_pam_abort = pam транзакция прервана err_pam_acct_expired = срок действия аккаунта истёк @@ -29,17 +49,34 @@ err_perm_dir = не удалось изменить текущий катало err_perm_group = не удалось понизить права доступа группы err_perm_user = не удалось понизить права доступа пользователя err_pwnam = не удалось получить информацию о пользователе +err_sleep = не удалось выполнить команду 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 = logged out +logout = вышел из системы +no_x11_support = поддержка x11 отключена при компиляции +normal = обычный numlock = numlock +other = прочие password = пароль restart = перезагрузить -shell = shell +shell = оболочка shutdown = выключить +sleep = сон +toggle_password = показать/скрыть пароль wayland = wayland +x11 = x11 xinitrc = xinitrc diff --git a/res/lang/sr.ini b/res/lang/sr.ini index 2f685e3..a2678bf 100644 --- a/res/lang/sr.ini +++ b/res/lang/sr.ini @@ -1,45 +1,82 @@ +authenticating = autentifikacija... +brightness_down = smanji osvetljenost +brightness_up = povećaj osvetljenost capslock = capslock -err_alloc = neuspijesna alokacija memorije +custom = prilagođeno +custom_info_err_output_long = izlaz predugačak +custom_info_err_no_output = nema izlaza +custom_info_err_no_output_error = , moguća greška +err_alloc = neuspješna alokacija memorije +err_args = nije moguće raščlaniti argumente komandne linije +err_autologin_session = sesija automatske prijave nije pronađena err_bounds = izvan granica indeksa -err_chdir = neuspijesno otvaranje home foldera -err_console_dev = neuspijesno pristupanje konzoli +err_brightness_change = nije uspela promena osvetljenosti +err_chdir = neuspješno otvaranje home foldera +err_clock_too_long = niska sata je preduga +err_config = nije moguće raščlaniti konfiguracioni fajl +err_crawl = nije uspelo skeniranje direktorijuma sesija err_dgn_oob = log poruka -err_domain = nevazeci domen -err_hostname = neuspijesno trazenje hostname-a -err_mlock = neuspijesno zakljucavanje memorije lozinke -err_null = null pokazivac -err_pam = pam transakcija neuspijesna +err_domain = nevažeći domen +err_empty_password = prazna lozinka nije dozvoljena +err_envlist = nije uspelo dobijanje liste promenljivih okruženja +err_get_active_tty = nije uspelo dobijanje aktivnog tty +err_hibernate = nije uspelo izvršavanje komande hibernacije +err_hostname = neuspješno traženje hostname-a +err_inactivity = nije uspelo izvršavanje komande neaktivnosti +err_lock_state = nije uspelo dobijanje stanja zaključavanja +err_log = nije uspelo otvaranje fajla dnevnika +err_mlock = neuspješno zaključavanje memorije lozinke +err_null = null pokazivač +err_numlock = nije uspelo postavljanje numlock +err_pam = pam transakcija neuspješna err_pam_abort = pam transakcija prekinuta err_pam_acct_expired = nalog istekao -err_pam_auth = greska pri autentikaciji -err_pam_authinfo_unavail = neuspjelo uzimanje informacija o korisniku +err_pam_auth = greška pri autentikaciji +err_pam_authinfo_unavail = neuspješno uzimanje informacija o korisniku err_pam_authok_reqd = token istekao -err_pam_buf = greska bafera memorije -err_pam_cred_err = neuspjelo postavljanje kredencijala +err_pam_buf = greška bafera memorije +err_pam_cred_err = neuspješno postavljanje kredencijala err_pam_cred_expired = kredencijali istekli err_pam_cred_insufficient = nedovoljni kredencijali -err_pam_cred_unavail = neuspjelo uzimanje kredencijala -err_pam_maxtries = dostignut maksimalan broj pokusaja -err_pam_perm_denied = nedozovoljeno -err_pam_session = greska sesije -err_pam_sys = greska sistema +err_pam_cred_unavail = neuspješno uzimanje kredencijala +err_pam_maxtries = dostignut maksimalan broj pokušaja +err_pam_perm_denied = nedozvoljeno +err_pam_session = greška sesije +err_pam_sys = greška sistema err_pam_user_unknown = nepoznat korisnik -err_path = neuspjelo postavljanje path-a -err_perm_dir = neuspjelo mijenjanje foldera -err_perm_group = neuspjesno snizavanje dozvola grupe -err_perm_user = neuspijesno snizavanje dozvola korisnika -err_pwnam = neuspijesno skupljanje informacija o korisniku -err_user_gid = neuspijesno postavljanje korisničkog GID-a -err_user_init = neuspijensa inicijalizacija korisnika -err_user_uid = neuspijesno postavljanje UID-a korisnika -err_xsessions_dir = neuspijesno pronalazenje foldera sesija -err_xsessions_open = neuspijesno otvaranje foldera sesija +err_path = neuspješno postavljanje path-a +err_perm_dir = neuspješno mijenjanje foldera +err_perm_group = neuspješno snižavanje dozvola grupe +err_perm_user = neuspješno snižavanje dozvola korisnika +err_pwnam = neuspješno skupljanje informacija o korisniku +err_sleep = nije uspelo izvršavanje komande spavanja +err_start = nije uspelo izvršavanje komande pokretanja +err_battery = nije uspelo učitavanje statusa baterije +err_switch_tty = nije uspelo prebacivanje tty +err_tty_ctrl = prenos kontrole tty nije uspeo +err_no_users = nisu pronađeni korisnici +err_uid_range = nije uspelo dinamičko dobijanje opsega uid +err_user_gid = neuspješno postavljanje korisničkog GID-a +err_user_init = neuspješna inicijalizacija korisnika +err_user_uid = neuspješno postavljanje UID-a korisnika +err_xauth = komanda xauth nije uspela +err_xcb_conn = xcb veza nije uspela +err_xsessions_dir = neuspješno pronalaženje foldera sesija +err_xsessions_open = neuspješno otvaranje foldera sesija +hibernate = hibernacija +insert = umetni login = korisnik logout = izlogovan +no_x11_support = x11 podrška onemogućena tokom prevođenja +normal = normalno numlock = numlock +other = ostalo password = lozinka restart = ponovo pokreni shell = shell shutdown = ugasi +sleep = uspavaj +toggle_password = prikaži/sakrij lozinku wayland = wayland +x11 = x11 xinitrc = xinitrc diff --git a/res/lang/sr_Cyrl.ini b/res/lang/sr_Cyrl.ini new file mode 100644 index 0000000..6222b3d --- /dev/null +++ b/res/lang/sr_Cyrl.ini @@ -0,0 +1,82 @@ +authenticating = аутентификација... +brightness_down = смањи осветљеност +brightness_up = повећај осветљеност +capslock = capslock +custom = прилагођено +custom_info_err_output_long = излаз предугачак +custom_info_err_no_output = нема излаза +custom_info_err_no_output_error = , могућа грешка +err_alloc = неуспешна алокација меморије +err_args = није могуће рашчланити аргументе командне линије +err_autologin_session = сесија аутоматске пријаве није пронађена +err_bounds = изван граница индекса +err_brightness_change = није успела промена осветљености +err_chdir = неуспешно отварање home фолдера +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 = неуспешно тражење hostname-а +err_inactivity = није успело извршавање команде неактивности +err_lock_state = није успело добијање стања закључавања +err_log = није успело отварање фајла дневника +err_mlock = неуспешно закључавање меморије лозинке +err_null = 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_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 = numlock +other = остало +password = лозинка +restart = поново покрени +shell = shell +shutdown = угаси +sleep = успавај +toggle_password = прикажи/сакриј лозинку +wayland = wayland +x11 = x11 +xinitrc = xinitrc diff --git a/res/lang/sv.ini b/res/lang/sv.ini index 46af681..45e36d4 100644 --- a/res/lang/sv.ini +++ b/res/lang/sv.ini @@ -1,45 +1,82 @@ +authenticating = autentiserar... +brightness_down = minska ljusstyrka +brightness_up = öka ljusstyrka capslock = capslock -err_alloc = misslyckad minnesallokering -err_bounds = utanför banan index +custom = anpassad +custom_info_err_output_long = utdata för lång +custom_info_err_no_output = ingen utdata +custom_info_err_no_output_error = , möjligt fel +err_alloc = minnesallokering misslyckades +err_args = tolkning av kommandoargument misslyckades +err_autologin_session = autologin-session hittades inte +err_bounds = index-värde utanför intervallet +err_brightness_change = ändring av ljusstyrka misslyckades err_chdir = misslyckades att öppna hemkatalog -err_console_dev = misslyckades att komma åt konsol +err_clock_too_long = klocksträng för lång +err_config = tolkning av konfigfil misslyckades +err_crawl = genomsökning av sessionskataloger misslyckades err_dgn_oob = loggmeddelande -err_domain = okänd domän -err_hostname = misslyckades att hämta värdnamn -err_mlock = misslyckades att låsa lösenordsminne +err_domain = ogitlig domän +err_empty_password = tomt lösenord godtas ej +err_envlist = hämtning av env-lista misslyckades +err_get_active_tty = hämtning av aktiv tty misslyckades +err_hibernate = vilolägets kommando misslyckades +err_hostname = hämtning av hostname misslyckades +err_inactivity = inaktivitetslägets kommando misslyckades +err_lock_state = hämtning av låsningsstatus misslyckades +err_log = öppning av loggfil misslyckades +err_mlock = låsning av lösenordsminne misslyckades err_null = nullpekare +err_numlock = inställning av numlock misslyckades err_pam = pam-transaktion misslyckades err_pam_abort = pam-transaktion avbröts -err_pam_acct_expired = konto upphört -err_pam_auth = autentiseringsfel -err_pam_authinfo_unavail = misslyckades att hämta användarinfo -err_pam_authok_reqd = token utgången -err_pam_buf = minnesbuffer fel -err_pam_cred_err = misslyckades att ställa in inloggningsuppgifter -err_pam_cred_expired = inloggningsuppgifter upphörda +err_pam_acct_expired = kontot har löpt ut +err_pam_auth = autentisering misslyckades +err_pam_authinfo_unavail = hämtning av användarinformation misslyckades +err_pam_authok_reqd = token har löpt ut +err_pam_buf = minnesbufferfel +err_pam_cred_err = inställning av inloggningsuppgifter misslyckades +err_pam_cred_expired = inloggningsuppgifterna har löpt ut err_pam_cred_insufficient = otillräckliga inloggningsuppgifter -err_pam_cred_unavail = misslyckades att hämta inloggningsuppgifter -err_pam_maxtries = nådde maximal försöksgräns -err_pam_perm_denied = åtkomst nekad +err_pam_cred_unavail = hämtning av inloggningsuppgifter misslyckades +err_pam_maxtries = gränsen för antal försök nådd +err_pam_perm_denied = tillstånd nekas err_pam_session = sessionsfel err_pam_sys = systemfel err_pam_user_unknown = okänd användare -err_path = misslyckades att ställa in sökväg -err_perm_dir = misslyckades att ändra aktuell katalog -err_perm_group = misslyckades att nergradera gruppbehörigheter -err_perm_user = misslyckades att nergradera användarbehörigheter -err_pwnam = misslyckades att hämta användarinfo -err_user_gid = misslyckades att ställa in användar-GID -err_user_init = misslyckades att initialisera användaren -err_user_uid = misslyckades att ställa in användar-UID -err_xsessions_dir = misslyckades att hitta sessionskatalog -err_xsessions_open = misslyckades att öppna sessionskatalog +err_path = inställning av sökväg misslyckades +err_perm_dir = byte av nuvarande katalog misslyckades +err_perm_group = nedgradering av grupptillstånd misslyckades +err_perm_user = nedgradering av användartillstånd misslyckades +err_pwnam = hämtning av användarinformation misslyckades +err_sleep = strömsparlägets kommando misslyckades +err_start = startkommando misslyckades +err_battery = hämtning av batteristatus misslyckades +err_switch_tty = byte av tty misslyckades +err_tty_ctrl = överföring av tty-kontroll misslyckades +err_no_users = inga användare hittades +err_uid_range = dynamisk hämtning av uid-intervall misslyckades +err_user_gid = inställning av användarens GID misslyckades +err_user_init = initiering av användare misslyckades +err_user_uid = inställning av användarens UID misslyckades +err_xauth = xauth-kommando misslyckades +err_xcb_conn = xcb-anslutning misslyckades +err_xsessions_dir = sessionskatalog hittades inte +err_xsessions_open = öppning av sessionskatalog misslyckades +hibernate = viloläge +insert = infoga login = inloggning logout = utloggad +no_x11_support = x11-stöd inaktiverat vid kompilering +normal = normal numlock = numlock +other = övrig password = lösenord restart = starta om -shell = skal +shell = shell shutdown = stäng av +sleep = viloläge +toggle_password = visa/dölj lösenord wayland = wayland +x11 = x11 xinitrc = xinitrc diff --git a/res/lang/tr.ini b/res/lang/tr.ini index 9bef617..414f4d5 100644 --- a/res/lang/tr.ini +++ b/res/lang/tr.ini @@ -1,13 +1,33 @@ +authenticating = kimlik doğrulanıyor... +brightness_down = parlakligi azalt +brightness_up = parlakligi arttir capslock = capslock +custom = özel +custom_info_err_output_long = çıktı çok uzun +custom_info_err_no_output = çıktı yok +custom_info_err_no_output_error = , olası hata err_alloc = basarisiz bellek ayirma +err_args = komut satırı argümanları ayrıştırılamıyor +err_autologin_session = otomatik oturum açma oturumu bulunamadı err_bounds = sinirlarin disinda dizin +err_brightness_change = parlaklık değiştirilemedi err_chdir = ev klasoru acilamadi -err_console_dev = konsola erisilemedi +err_clock_too_long = saat dizesi çok uzun +err_config = yapılandırma dosyası ayrıştırılamıyor +err_crawl = oturum dizinleri taranamadı err_dgn_oob = log mesaji err_domain = gecersiz etki alani +err_empty_password = boş parola kullanılamaz +err_envlist = ortam değişkenleri listesi alınamadı +err_get_active_tty = aktif tty alınamadı +err_hibernate = hazırda bekletme komutu çalıştırılamadı err_hostname = ana bilgisayar adi alinamadi +err_inactivity = hareketsizlik komutu çalıştırılamadı +err_lock_state = kilit durumu alınamadı +err_log = günlük dosyası açılamadı err_mlock = parola bellegi kilitlenemedi err_null = bos isaretci hatasi +err_numlock = numlock ayarlanamadı err_pam = pam islemi basarisiz oldu err_pam_abort = pam islemi durduruldu err_pam_acct_expired = hesabin suresi dolmus @@ -29,17 +49,34 @@ err_perm_dir = gecerli dizin degistirilemedi err_perm_group = grup izinleri dusurulemedi err_perm_user = kullanici izinleri dusurulemedi err_pwnam = kullanici bilgileri alinamadi +err_sleep = uyku komutu çalıştırılamadı +err_start = başlatma komutu çalıştırılamadı +err_battery = pil durumu yüklenemedi +err_switch_tty = tty değiştirilemedi +err_tty_ctrl = tty kontrol aktarımı başarısız oldu +err_no_users = kullanıcı bulunamadı +err_uid_range = uid aralığı dinamik olarak alınamadı err_user_gid = kullanici icin GID ayarlanamadi err_user_init = kullanici oturumu baslatilamadi err_user_uid = kullanici icin UID ayarlanamadi +err_xauth = xauth komutu başarısız oldu +err_xcb_conn = xcb bağlantısı başarısız oldu err_xsessions_dir = oturumlar klasoru bulunamadi err_xsessions_open = oturumlar klasoru acilamadi +hibernate = askiya al +insert = ekle login = kullanici logout = oturumdan cikis yapildi +no_x11_support = x11 desteği derleme zamanında devre dışı bırakıldı +normal = normal numlock = numlock +other = baska password = sifre restart = yeniden baslat shell = shell shutdown = makineyi kapat +sleep = uykuya al +toggle_password = parolayı göster/gizle wayland = wayland +x11 = x11 xinitrc = xinitrc diff --git a/res/lang/uk.ini b/res/lang/uk.ini index 5f5b113..6baa469 100644 --- a/res/lang/uk.ini +++ b/res/lang/uk.ini @@ -1,13 +1,33 @@ +authenticating = автентифікація... +brightness_down = зменшити яскравість +brightness_up = збільшити яскравість capslock = capslock +custom = власний +custom_info_err_output_long = вивід занадто довгий +custom_info_err_no_output = немає виводу +custom_info_err_no_output_error = , можлива помилка err_alloc = невдале виділення пам'яті +err_args = не вдалося розібрати аргументи командного рядка +err_autologin_session = сеанс автоматичного входу не знайдено err_bounds = поза межами індексу +err_brightness_change = не вдалося змінити яскравість err_chdir = не вдалося відкрити домашній каталог -err_console_dev = невдалий доступ до консолі +err_clock_too_long = рядок годинника занадто довгий +err_config = не вдалося розібрати файл конфігурації +err_crawl = не вдалося сканувати каталоги сесій err_dgn_oob = повідомлення журналу (log) 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 = не вдалося встановити numlock err_pam = невдала pam транзакція err_pam_abort = pam транзакція перервана err_pam_acct_expired = термін дії акаунту вичерпано @@ -29,17 +49,34 @@ 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 = numlock +other = інший password = пароль restart = перезавантажити shell = оболонка shutdown = вимкнути +sleep = сплячий режим +toggle_password = показати/приховати пароль wayland = wayland +x11 = x11 xinitrc = xinitrc diff --git a/res/lang/zh_CN.ini b/res/lang/zh_CN.ini new file mode 100644 index 0000000..4562624 --- /dev/null +++ b/res/lang/zh_CN.ini @@ -0,0 +1,82 @@ +authenticating = 正在认证... +brightness_down = 降低亮度 +brightness_up = 提高亮度 +capslock = 大写锁定 +custom = 自定义 +custom_info_err_output_long = 输出过长 +custom_info_err_no_output = 无输出 +custom_info_err_no_output_error = ,可能有错误 +err_alloc = 内存分配失败 +err_args = 无法解析命令行参数 +err_autologin_session = 未找到自动登录会话 +err_bounds = 索引越界 +err_brightness_change = 无法更改亮度 +err_chdir = 无法打开home文件夹 +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 = 无法设置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_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 = 数字锁定 +other = 其他 +password = 密码 +restart = 重启 +shell = shell +shutdown = 关机 +sleep = 睡眠 +toggle_password = 显示/隐藏密码 +wayland = wayland +x11 = x11 +xinitrc = xinitrc diff --git a/res/lang/zh_TW.ini b/res/lang/zh_TW.ini new file mode 100644 index 0000000..50d5f68 --- /dev/null +++ b/res/lang/zh_TW.ini @@ -0,0 +1,82 @@ +authenticating = 正在驗證... +brightness_down = 降低亮度 +brightness_up = 提高亮度 +capslock = 大寫鎖定 +custom = 自訂 +custom_info_err_output_long = 輸出過長 +custom_info_err_no_output = 無輸出 +custom_info_err_no_output_error = ,可能有錯誤 +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 = 無法設定 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_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 = 數字鎖定 +other = 其他 +password = 密碼 +restart = 重新啟動 +shell = shell +shutdown = 關機 +sleep = 睡眠 +toggle_password = 顯示/隱藏密碼 +wayland = wayland +x11 = x11 +xinitrc = xinitrc diff --git a/res/ly-dinit b/res/ly-dinit index a20a71f..53ef1dd 100644 --- a/res/ly-dinit +++ b/res/ly-dinit @@ -1,8 +1,8 @@ type = process restart = true smooth-recovery = true -command = $PREFIX_DIRECTORY/bin/$EXE_NAME -depends-on = loginready +command = $PREFIX_DIRECTORY/bin/$EXECUTABLE_NAME +depends-on = login.target termsignal = HUP -# ly needs access to the console while loginready already occupies it +# ly needs access to the console while login.target already occupies it options = shares-console diff --git a/res/ly-freebsd-wrapper b/res/ly-freebsd-wrapper new file mode 100644 index 0000000..23544b7 --- /dev/null +++ b/res/ly-freebsd-wrapper @@ -0,0 +1,7 @@ +#!/bin/sh + +# On FreeBSD, even if we override the default login program, getty will still +# try to append "login -fp root" as arguments to Ly, which is not supported. +# To avoid this, we use a wrapper script that ignores these arguments before +# actually executing Ly. +exec $PREFIX_DIRECTORY/bin/$EXECUTABLE_NAME diff --git a/res/ly-kmsconvt@.service b/res/ly-kmsconvt@.service new file mode 100644 index 0000000..9384bdf --- /dev/null +++ b/res/ly-kmsconvt@.service @@ -0,0 +1,28 @@ +# Default kmsconvt@.service +# Modified for Ly by AnErrupTion + +[Unit] +Description=TUI display manager on %I using KMSCON +After=systemd-user-sessions.service plymouth-quit-wait.service rc-local.service kmsconvt@%i.service +Conflicts=getty@%i.service kmsconvt@%i.service ly@%i.service +OnFailure=ly@%i.service + +[Service] +ExecStart=$PREFIX_DIRECTORY/bin/kmscon --term=linux --font-engine unifont --vt=%I --login -- $PREFIX_DIRECTORY/bin/$EXECUTABLE_NAME --use-kmscon-vt +Type=idle +Restart=always +RestartSec=0 +UtmpIdentifier=%I +StandardInput=tty +StandardOutput=tty +TTYPath=/dev/%I +TTYReset=yes +TTYVHangup=yes +TTYVTDisallocate=yes +IgnoreSIGPIPE=no +SendSIGHUP=yes + +[Install] +Alias=autovt@.service +WantedBy=multi-user.target +DefaultInstance=tty2 diff --git a/res/ly-openrc b/res/ly-openrc index ac6ae0f..f6d222c 100644 --- a/res/ly-openrc +++ b/res/ly-openrc @@ -19,12 +19,8 @@ then commandUL="/sbin/agetty" fi -## Get the tty from the conf file -CONFTTY=$(cat $CONFIG_DIRECTORY/ly/config.ini | sed -n 's/^tty.*=[^1-9]*// p') - ## The execution vars -# If CONFTTY is empty then default to $DEFAULT_TTY -TTY="tty${CONFTTY:-$DEFAULT_TTY}" +TTY="tty$DEFAULT_TTY" TERM=linux BAUD=38400 # If we don't have getty then we should have agetty diff --git a/res/ly-runit-service/conf b/res/ly-runit-service/conf index 76ceb87..ded7001 100644 --- a/res/ly-runit-service/conf +++ b/res/ly-runit-service/conf @@ -7,6 +7,4 @@ fi BAUD_RATE=38400 TERM_NAME=linux - -auxtty=$(/bin/cat $CONFIG_DIRECTORY/ly/config.ini 2>/dev/null 1| /bin/sed -n 's/\(^[[:space:]]*tty[[:space:]]*=[[:space:]]*\)\([[:digit:]][[:digit:]]*\)\(.*\)/\2/p') -TTY=tty${auxtty:-$DEFAULT_TTY} +TTY=tty$DEFAULT_TTY diff --git a/res/ly-s6/run b/res/ly-s6/run index e63826f..1b8aa0d 100644 --- a/res/ly-s6/run +++ b/res/ly-s6/run @@ -1,2 +1,2 @@ #!/bin/execlineb -P -exec agetty -L -8 -n -l $PREFIX_DIRECTORY/bin/$EXE_NAME tty$DEFAULT_TTY 115200 +exec agetty -L -8 -n -l $PREFIX_DIRECTORY/bin/$EXECUTABLE_NAME tty$DEFAULT_TTY 115200 diff --git a/res/ly-sysvinit b/res/ly-sysvinit new file mode 100755 index 0000000..f24dfd4 --- /dev/null +++ b/res/ly-sysvinit @@ -0,0 +1,65 @@ +#!/bin/sh +### BEGIN INIT INFO +# Provides: ly +# Required-Start: $remote_fs $syslog +# Required-Stop: $remote_fs $syslog +# Default-Start: 2 3 4 5 +# Default-Stop: 0 1 6 +# Short-Description: Ly display manager +# Description: Starts and stops the Ly display manager +### END INIT INFO +# +# Author: AnErrupTion +# + +PATH=/sbin:/usr/sbin:/bin:/usr/bin +DAEMON=/usr/bin/ly +TTY=/dev/tty$DEFAULT_TTY +PIDFILE=/var/run/ly.pid +NAME=ly +DESC="Ly display manager" + +. /lib/lsb/init-functions + +case "$1" in + start) + log_daemon_msg "Starting $DESC on $TTY..." + if [ -f "$PIDFILE" ]; then + log_progress_msg "$DESC is already running" + log_end_msg 0 + return 0 + fi + + # Ensure TTY exists + [ -c "$TTY" ] || { + log_failure_msg "$TTY does not exist" + return 1 + } + + start-stop-daemon --start --background --make-pidfile --pidfile $PIDFILE \ + --chdir / --exec /bin/sh -- -c "exec setsid sh -c 'exec <$TTY >$TTY 2>&1 $DAEMON'" + log_end_msg $? + ;; + stop) + log_daemon_msg "Stopping $DESC..." + start-stop-daemon --stop --pidfile $PIDFILE --retry 5 + RETVAL=$? + [ $RETVAL -eq 0 ] && rm -f "$PIDFILE" + log_end_msg $RETVAL + ;; + restart) + echo "Restarting $DESC..." + $0 stop + sleep 1 + $0 start + ;; + status) + status_of_proc -p $PIDFILE $DAEMON $NAME && exit 0 || exit $? + ;; + *) + echo "Usage: /etc/init.d/$NAME {start|stop|restart|status}" + exit 1 + ;; +esac + +exit 0 diff --git a/res/ly.service b/res/ly.service deleted file mode 100644 index 0b72699..0000000 --- a/res/ly.service +++ /dev/null @@ -1,16 +0,0 @@ -[Unit] -Description=TUI display manager -After=systemd-user-sessions.service plymouth-quit-wait.service -After=getty@tty$DEFAULT_TTY.service -Conflicts=getty@tty$DEFAULT_TTY.service - -[Service] -Type=idle -ExecStart=$PREFIX_DIRECTORY/bin/$EXECUTABLE_NAME -StandardInput=tty -TTYPath=/dev/tty$DEFAULT_TTY -TTYReset=yes -TTYVHangup=yes - -[Install] -Alias=display-manager.service diff --git a/res/ly@.service b/res/ly@.service new file mode 100644 index 0000000..b2fb5ff --- /dev/null +++ b/res/ly@.service @@ -0,0 +1,46 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +# +# This file is part of systemd. +# +# systemd is free software; you can redistribute it and/or modify it +# under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation; either version 2.1 of the License, or +# (at your option) any later version. +# +# Modified for Ly by AnErrupTion + +[Unit] +Description=TUI display manager on %I +After=systemd-user-sessions.service plymouth-quit-wait.service getty@%i.service +Conflicts=getty@%i.service kmsconvt@%i.service ly-kmsconvt@%i.service + +[Service] +ExecStart=$PREFIX_DIRECTORY/bin/agetty -nl $PREFIX_DIRECTORY/bin/$EXECUTABLE_NAME %I ${TERM} +Type=idle +Restart=always +RestartSec=0 +UtmpIdentifier=%I +StandardInput=tty +StandardOutput=tty +TTYPath=/dev/%I +TTYReset=yes +TTYVHangup=yes +TTYVTDisallocate=yes +IgnoreSIGPIPE=no +SendSIGHUP=yes + +ImportCredential=tty.virtual.%I.agetty.*:agetty. +ImportCredential=tty.virtual.%I.login.*:login. +ImportCredential=agetty.* +ImportCredential=login.* +ImportCredential=shell.* + +# Unset locale for the console getty since the console has problems +# displaying some internationalized messages. +UnsetEnvironment=LANG LANGUAGE LC_CTYPE LC_NUMERIC LC_TIME LC_COLLATE LC_MONETARY LC_MESSAGES LC_PAPER LC_NAME LC_ADDRESS LC_TELEPHONE LC_MEASUREMENT LC_IDENTIFICATION + +[Install] +Alias=autovt@.service + +WantedBy=multi-user.target +DefaultInstance=tty2 diff --git a/res/pam.d/ly-freebsd b/res/pam.d/ly-freebsd new file mode 100644 index 0000000..1cefe07 --- /dev/null +++ b/res/pam.d/ly-freebsd @@ -0,0 +1,8 @@ +#%PAM-1.0 + +# OpenPAM (used in FreeBSD) doesn't support prepending "-" for ignoring missing +# modules. +auth include login +account include login +password include login +session include login diff --git a/res/pam.d/ly-freebsd-autologin b/res/pam.d/ly-freebsd-autologin new file mode 100644 index 0000000..b6f56e3 --- /dev/null +++ b/res/pam.d/ly-freebsd-autologin @@ -0,0 +1,8 @@ +#%PAM-1.0 + +# OpenPAM (used in FreeBSD) doesn't support prepending "-" for ignoring missing +# modules. +auth required pam_permit.so +account include login +password include login +session include login diff --git a/res/pam.d/ly b/res/pam.d/ly-linux similarity index 100% rename from res/pam.d/ly rename to res/pam.d/ly-linux diff --git a/res/pam.d/ly-linux-autologin b/res/pam.d/ly-linux-autologin new file mode 100644 index 0000000..31a51a1 --- /dev/null +++ b/res/pam.d/ly-linux-autologin @@ -0,0 +1,16 @@ +#%PAM-1.0 + +auth required pam_permit.so +-auth optional pam_gnome_keyring.so +-auth optional pam_kwallet5.so + +account include login + +password include login +-password optional pam_gnome_keyring.so use_authtok + +-session optional pam_systemd.so class=greeter +-session optional pam_elogind.so +session include login +-session optional pam_gnome_keyring.so auto_start +-session optional pam_kwallet5.so auto_start diff --git a/res/setup.sh b/res/setup.sh index 549994a..8ec0f00 100755 --- a/res/setup.sh +++ b/res/setup.sh @@ -89,6 +89,10 @@ if [ "$XDG_SESSION_TYPE" = "x11" ]; then done fi + if [ -f "$USERXSESSION" ]; then + . "$USERXSESSION" + fi + if [ -d "$CONFIG_DIRECTORY"/X11/Xresources ]; then for i in "$CONFIG_DIRECTORY"/X11/Xresources/*; do [ -f "$i" ] && xrdb -merge "$i" @@ -98,10 +102,6 @@ if [ "$XDG_SESSION_TYPE" = "x11" ]; then fi [ -f "$HOME"/.Xresources ] && xrdb -merge "$HOME"/.Xresources [ -f "$XDG_CONFIG_HOME"/X11/Xresources ] && xrdb -merge "$XDG_CONFIG_HOME"/X11/Xresources - - if [ -f "$USERXSESSION" ]; then - . "$USERXSESSION" - fi fi exec "$@" diff --git a/res/startup.sh b/res/startup.sh new file mode 100755 index 0000000..c1e88c1 --- /dev/null +++ b/res/startup.sh @@ -0,0 +1,37 @@ +#!/bin/sh +# This file is executed when starting Ly (before the TTY is taken control of) +# Custom startup code can be placed in this file or the start_cmd var can be pointed to a different file + + +# Uncomment the example below for an example of changing the default TTY colors to an alternitive palette on linux +# Colors are in red/green/blue hex (the current colors are a brighter palette than default) +# +# if [ "$TERM" = "linux" ]; then +# BLACK="232323" +# DARK_RED="D75F5F" +# DARK_GREEN="87AF5F" +# DARK_YELLOW="D7AF87" +# DARK_BLUE="8787AF" +# DARK_MAGENTA="BD53A5" +# DARK_CYAN="5FAFAF" +# LIGHT_GRAY="E5E5E5" +# DARK_GRAY="2B2B2B" +# RED="E33636" +# GREEN="98E34D" +# YELLOW="FFD75F" +# BLUE="7373C9" +# MAGENTA="D633B2" +# CYAN="44C9C9" +# WHITE="FFFFFF" + +# COLORS="${BLACK} ${DARK_RED} ${DARK_GREEN} ${DARK_YELLOW} ${DARK_BLUE} ${DARK_MAGENTA} ${DARK_CYAN} ${LIGHT_GRAY} ${DARK_GRAY} ${RED} ${GREEN} ${YELLOW} ${BLUE} ${MAGENTA} ${CYAN} ${WHITE}" + +# i=0 +# while [ $i -lt 16 ]; do +# printf "\033]P%x%s" ${i} "$(echo "$COLORS" | cut -d ' ' -f$(( i + 1)))" + +# i=$(( i + 1 )) +# done + +# clear # for fixing background artifacting after changing color +# fi diff --git a/res/valgrind.supp b/res/valgrind.supp deleted file mode 100644 index 274f2f0..0000000 --- a/res/valgrind.supp +++ /dev/null @@ -1,31 +0,0 @@ -{ - pam - Memcheck:Leak - ... - obj:/usr/lib/libpam.so.0.84.2 - ... -} - -{ - termbox - Memcheck:Leak - ... - fun:tb_init - ... -} - -{ - libc/dynamic - Memcheck:Leak - ... - fun:_dl_catch_exception - ... -} - -{ - libc/groups - Memcheck:Leak - ... - fun:initgroups - ... -} diff --git a/src/Environment.zig b/src/Environment.zig new file mode 100644 index 0000000..b661c14 --- /dev/null +++ b/src/Environment.zig @@ -0,0 +1,24 @@ +const ini = @import("ly-ui").ly_core.ini; +const Ini = ini.Ini; + +const enums = @import("enums.zig"); +const DisplayServer = enums.DisplayServer; + +pub const DesktopEntry = struct { + Exec: []const u8 = "", + Name: []const u8 = "", + DesktopNames: ?[]u8 = null, + Terminal: ?bool = null, +}; + +pub const Entry = struct { @"Desktop Entry": DesktopEntry = .{} }; + +entry_ini: ?Ini(Entry) = null, +file_name: []const u8 = "", +name: []const u8 = "", +xdg_session_desktop: ?[]const u8 = null, +xdg_desktop_names: ?[]const u8 = null, +cmd: ?[]const u8 = null, +specifier: []const u8 = "", +display_server: DisplayServer = .wayland, +is_terminal: bool = false, diff --git a/src/SharedError.zig b/src/SharedError.zig deleted file mode 100644 index dcf1f79..0000000 --- a/src/SharedError.zig +++ /dev/null @@ -1,39 +0,0 @@ -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.mem.page_size) u8, - -pub fn init() !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 }; -} - -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 {}; -} - -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; -} diff --git a/src/animations/Cascade.zig b/src/animations/Cascade.zig new file mode 100644 index 0000000..c9b7769 --- /dev/null +++ b/src/animations/Cascade.zig @@ -0,0 +1,92 @@ +const std = @import("std"); +const math = std.math; + +const ly_ui = @import("ly-ui"); +const Cell = ly_ui.Cell; +const TerminalBuffer = ly_ui.TerminalBuffer; +const Widget = ly_ui.Widget; + +const Cascade = @This(); + +io: std.Io, +instance: ?Widget = null, +buffer: *TerminalBuffer, +current_auth_fails: *u64, +max_auth_fails: u64, + +pub fn init( + io: std.Io, + buffer: *TerminalBuffer, + current_auth_fails: *u64, + max_auth_fails: u64, +) Cascade { + return .{ + .io = io, + .instance = null, + .buffer = buffer, + .current_auth_fails = current_auth_fails, + .max_auth_fails = max_auth_fails, + }; +} + +pub fn widget(self: *Cascade) *Widget { + if (self.instance) |*instance| return instance; + self.instance = Widget.init( + "Cascade", + null, + self, + null, + null, + draw, + null, + null, + null, + ); + return &self.instance.?; +} + +fn draw(self: *Cascade) void { + while (self.current_auth_fails.* >= self.max_auth_fails) { + self.io.sleep(.fromMilliseconds(10), .real) catch {}; + + var changed = false; + var y = self.buffer.height - 2; + + while (y > 0) : (y -= 1) { + for (0..self.buffer.width) |x| { + const cell = TerminalBuffer.getCell(x, y - 1); + const cell_under = TerminalBuffer.getCell(x, y); + + // This shouldn't happen under normal circumstances, but because + // this is a *secret* animation, there's no need to care that much + if (cell == null or cell_under == null) continue; + + const char: u8 = @truncate(cell.?.ch); + if (std.ascii.isWhitespace(char)) continue; + + const char_under: u8 = @truncate(cell_under.?.ch); + if (!std.ascii.isWhitespace(char_under)) continue; + + changed = true; + + if ((self.buffer.random.int(u16) % 10) > 7) continue; + + cell.?.put(x, y) catch {}; + + var space = Cell.init( + ' ', + cell_under.?.fg, + cell_under.?.bg, + ); + space.put(x, y - 1) catch {}; + } + } + + if (!changed) { + self.io.sleep(.fromSeconds(7), .real) catch {}; + self.current_auth_fails.* = 0; + } + + TerminalBuffer.presentBuffer() catch {}; + } +} diff --git a/src/animations/ColorMix.zig b/src/animations/ColorMix.zig new file mode 100644 index 0000000..e9063a3 --- /dev/null +++ b/src/animations/ColorMix.zig @@ -0,0 +1,132 @@ +const std = @import("std"); +const math = std.math; + +const ly_ui = @import("ly-ui"); +const Cell = ly_ui.Cell; +const TerminalBuffer = ly_ui.TerminalBuffer; +const Widget = ly_ui.Widget; + +const ly_core = ly_ui.ly_core; +const interop = ly_core.interop; +const TimeOfDay = interop.TimeOfDay; + +const ColorMix = @This(); + +const Vec2 = @Vector(2, f32); + +const time_scale: f32 = 0.01; +const palette_len: usize = 12; + +fn length(vec: Vec2) f32 { + return math.sqrt(vec[0] * vec[0] + vec[1] * vec[1]); +} + +instance: ?Widget = null, +start_time: TimeOfDay, +terminal_buffer: *TerminalBuffer, +animate: *bool, +timeout_sec: u12, +frame_delay: u16, +frames: u64, +pattern_cos_mod: f32, +pattern_sin_mod: f32, +palette: [palette_len]Cell, + +pub fn init( + terminal_buffer: *TerminalBuffer, + col1: u32, + col2: u32, + col3: u32, + animate: *bool, + timeout_sec: u12, + frame_delay: u16, +) !ColorMix { + return .{ + .instance = null, + .start_time = try interop.getTimeOfDay(), + .terminal_buffer = terminal_buffer, + .animate = animate, + .timeout_sec = timeout_sec, + .frame_delay = frame_delay, + .frames = 0, + .pattern_cos_mod = terminal_buffer.random.float(f32) * math.pi * 2.0, + .pattern_sin_mod = terminal_buffer.random.float(f32) * math.pi * 2.0, + .palette = [palette_len]Cell{ + Cell.init(0x2588, col1, col2), + Cell.init(0x2593, col1, col2), + Cell.init(0x2592, col1, col2), + Cell.init(0x2591, col1, col2), + Cell.init(0x2588, col2, col3), + Cell.init(0x2593, col2, col3), + Cell.init(0x2592, col2, col3), + Cell.init(0x2591, col2, col3), + Cell.init(0x2588, col3, col1), + Cell.init(0x2593, col3, col1), + Cell.init(0x2592, col3, col1), + Cell.init(0x2591, col3, col1), + }, + }; +} + +pub fn widget(self: *ColorMix) *Widget { + if (self.instance) |*instance| return instance; + self.instance = Widget.init( + "ColorMix", + null, + self, + null, + null, + draw, + update, + null, + calculateTimeout, + ); + return &self.instance.?; +} + +fn draw(self: *ColorMix) void { + if (!self.animate.*) return; + + self.frames +%= 1; + const time: f32 = @as(f32, @floatFromInt(self.frames)) * time_scale; + + for (0..self.terminal_buffer.width) |x| { + for (0..self.terminal_buffer.height) |y| { + const xi: i32 = @intCast(x); + const yi: i32 = @intCast(y); + const wi: i32 = @intCast(self.terminal_buffer.width); + const hi: i32 = @intCast(self.terminal_buffer.height); + + var uv: Vec2 = .{ + @as(f32, @floatFromInt(xi * 2 - wi)) / @as(f32, @floatFromInt(self.terminal_buffer.height * 2)), + @as(f32, @floatFromInt(yi * 2 - hi)) / @as(f32, @floatFromInt(self.terminal_buffer.height)), + }; + + var uv2: Vec2 = @splat(uv[0] + uv[1]); + + for (0..3) |_| { + uv2 += uv + @as(Vec2, @splat(length(uv))); + uv += @as(Vec2, @splat(0.5)) * Vec2{ + math.cos(self.pattern_cos_mod + uv2[1] * 0.2 + time * 0.1), + math.sin(self.pattern_sin_mod + uv2[0] - time * 0.1), + }; + uv -= @splat(1.0 * math.cos(uv[0] + uv[1]) - math.sin(uv[0] * 0.7 - uv[1])); + } + + const cell = self.palette[@as(usize, @trunc(math.floor(length(uv) * 5.0))) % palette_len]; + cell.put(x, y) catch {}; + } + } +} + +fn update(self: *ColorMix, _: *anyopaque) !void { + const time = try interop.getTimeOfDay(); + + if (self.timeout_sec > 0 and time.seconds - self.start_time.seconds > self.timeout_sec) { + self.animate.* = false; + } +} + +fn calculateTimeout(self: *ColorMix, _: *anyopaque) !?usize { + return self.frame_delay; +} diff --git a/src/animations/Doom.zig b/src/animations/Doom.zig index 9813122..504831a 100644 --- a/src/animations/Doom.zig +++ b/src/animations/Doom.zig @@ -1,76 +1,141 @@ const std = @import("std"); const Allocator = std.mem.Allocator; -const TerminalBuffer = @import("../tui/TerminalBuffer.zig"); -const utils = @import("../tui/utils.zig"); -const interop = @import("../interop.zig"); -const termbox = interop.termbox; +const ly_ui = @import("ly-ui"); +const Cell = ly_ui.Cell; +const TerminalBuffer = ly_ui.TerminalBuffer; +const Widget = ly_ui.Widget; + +const ly_core = ly_ui.ly_core; +const interop = ly_core.interop; +const TimeOfDay = interop.TimeOfDay; const Doom = @This(); -pub const STEPS = 13; -pub const FIRE = [_]utils.Cell{ - utils.initCell(' ', 9, 0), - utils.initCell(0x2591, 2, 0), // Red - utils.initCell(0x2592, 2, 0), // Red - utils.initCell(0x2593, 2, 0), // Red - utils.initCell(0x2588, 2, 0), // Red - utils.initCell(0x2591, 4, 2), // Yellow - utils.initCell(0x2592, 4, 2), // Yellow - utils.initCell(0x2593, 4, 2), // Yellow - utils.initCell(0x2588, 4, 2), // Yellow - utils.initCell(0x2591, 8, 4), // White - utils.initCell(0x2592, 8, 4), // White - utils.initCell(0x2593, 8, 4), // White - utils.initCell(0x2588, 8, 4), // White -}; +pub const STEPS = 12; +pub const HEIGHT_MAX = 9; +pub const SPREAD_MAX = 4; +instance: ?Widget = null, +start_time: TimeOfDay, allocator: Allocator, terminal_buffer: *TerminalBuffer, +animate: *bool, +timeout_sec: u12, +frame_delay: u16, buffer: []u8, +height: u8, +spread: u8, +fire: [STEPS + 1]Cell, -pub fn init(allocator: Allocator, terminal_buffer: *TerminalBuffer) !Doom { +pub fn init( + allocator: Allocator, + terminal_buffer: *TerminalBuffer, + top_color: u32, + middle_color: u32, + bottom_color: u32, + fire_height: u8, + fire_spread: u8, + animate: *bool, + timeout_sec: u12, + frame_delay: u16, +) !Doom { const buffer = try allocator.alloc(u8, terminal_buffer.width * terminal_buffer.height); initBuffer(buffer, terminal_buffer.width); + const levels = + [_]Cell{ + Cell.init(' ', terminal_buffer.bg, terminal_buffer.bg), + Cell.init(0x2591, top_color, terminal_buffer.bg), + Cell.init(0x2592, top_color, terminal_buffer.bg), + Cell.init(0x2593, top_color, terminal_buffer.bg), + Cell.init(0x2588, top_color, terminal_buffer.bg), + Cell.init(0x2591, middle_color, top_color), + Cell.init(0x2592, middle_color, top_color), + Cell.init(0x2593, middle_color, top_color), + Cell.init(0x2588, middle_color, top_color), + Cell.init(0x2591, bottom_color, middle_color), + Cell.init(0x2592, bottom_color, middle_color), + Cell.init(0x2593, bottom_color, middle_color), + Cell.init(0x2588, bottom_color, middle_color), + }; + return .{ + .instance = null, + .start_time = try interop.getTimeOfDay(), .allocator = allocator, .terminal_buffer = terminal_buffer, + .animate = animate, + .timeout_sec = timeout_sec, + .frame_delay = frame_delay, .buffer = buffer, + .height = @min(HEIGHT_MAX, fire_height), + .spread = @min(SPREAD_MAX, fire_spread), + .fire = levels, }; } -pub fn deinit(self: Doom) void { +pub fn widget(self: *Doom) *Widget { + if (self.instance) |*instance| return instance; + self.instance = Widget.init( + "Doom", + null, + self, + deinit, + realloc, + draw, + update, + null, + calculateTimeout, + ); + return &self.instance.?; +} + +fn deinit(self: *Doom) void { self.allocator.free(self.buffer); } -pub fn realloc(self: *Doom) !void { +fn realloc(self: *Doom) !void { const buffer = try self.allocator.realloc(self.buffer, self.terminal_buffer.width * self.terminal_buffer.height); initBuffer(buffer, self.terminal_buffer.width); self.buffer = buffer; } -pub fn draw(self: Doom) void { +fn draw(self: *Doom) void { + if (!self.animate.*) return; + for (0..self.terminal_buffer.width) |x| { + // We start from 1 so that we always have the topmost line when spreading fire for (1..self.terminal_buffer.height) |y| { - const source = y * self.terminal_buffer.width + x; - const random = (self.terminal_buffer.random.int(u16) % 7) & 3; + // Get index of current cell in fire level buffer + const from = y * self.terminal_buffer.width + x; - var dest = (source - @min(source, random)) + 1; - if (self.terminal_buffer.width > dest) dest = 0 else dest -= self.terminal_buffer.width; + // Generate random data for fire propagation + const rand_loss = self.terminal_buffer.random.intRangeAtMost(u8, 0, HEIGHT_MAX); + const rand_spread = self.terminal_buffer.random.intRangeAtMost(u8, 0, self.spread * 2); - const buffer_source = self.buffer[source]; - const buffer_dest_offset = random & 1; + // Select semi-random target cell + const to = from -| self.terminal_buffer.width + self.spread -| rand_spread; + const to_x = to % self.terminal_buffer.width; + const to_y = to / self.terminal_buffer.width; - if (buffer_source < buffer_dest_offset) continue; + // Get fire level of current cell + const level_buf_from = self.buffer[from]; - var buffer_dest = buffer_source - buffer_dest_offset; - if (buffer_dest > 12) buffer_dest = 0; - self.buffer[dest] = @intCast(buffer_dest); + // Choose new fire level and store in level buffer + const level_buf_to = level_buf_from -| @intFromBool(rand_loss >= self.height); + self.buffer[to] = level_buf_to; - self.terminal_buffer.buffer[dest] = toTermboxCell(FIRE[buffer_dest]); - self.terminal_buffer.buffer[source] = toTermboxCell(FIRE[buffer_source]); + // Send known fire levels to terminal buffer + const from_cell = self.fire[level_buf_from]; + const to_cell = self.fire[level_buf_to]; + from_cell.put(x, y) catch {}; + to_cell.put(to_x, to_y) catch {}; } + + // Draw bottom line (fire source) + const src_cell = self.fire[STEPS]; + src_cell.put(x, self.terminal_buffer.height - 1) catch {}; } } @@ -79,14 +144,20 @@ fn initBuffer(buffer: []u8, width: usize) void { const slice_start = buffer[0..length]; const slice_end = buffer[length..]; + // Initialize the framebuffer in black, except for the "fire source" as the + // last color @memset(slice_start, 0); - @memset(slice_end, STEPS - 1); + @memset(slice_end, STEPS); } -fn toTermboxCell(cell: utils.Cell) termbox.tb_cell { - return .{ - .ch = cell.ch, - .fg = cell.fg, - .bg = cell.bg, - }; +fn update(self: *Doom, _: *anyopaque) !void { + const time = try interop.getTimeOfDay(); + + if (self.timeout_sec > 0 and time.seconds - self.start_time.seconds > self.timeout_sec) { + self.animate.* = false; + } +} + +fn calculateTimeout(self: *Doom, _: *anyopaque) !?usize { + return self.frame_delay; } diff --git a/src/animations/DurFile.zig b/src/animations/DurFile.zig new file mode 100644 index 0000000..95677f8 --- /dev/null +++ b/src/animations/DurFile.zig @@ -0,0 +1,565 @@ +const std = @import("std"); +const Allocator = std.mem.Allocator; +const Json = std.json; +const eql = std.mem.eql; +const flate = std.compress.flate; + +const ly_ui = @import("ly-ui"); +const Cell = ly_ui.Cell; +const TerminalBuffer = ly_ui.TerminalBuffer; +const Color = TerminalBuffer.Color; +const Styling = TerminalBuffer.Styling; +const Widget = ly_ui.Widget; + +const ly_core = ly_ui.ly_core; +const interop = ly_core.interop; +const TimeOfDay = interop.TimeOfDay; +const LogFile = ly_core.LogFile; + +const enums = @import("../enums.zig"); +const DurOffsetAlignment = enums.DurOffsetAlignment; + +const Frame = struct { + frameNumber: i32, + delay: f32, + contents: [][]u8, + colorMap: [][][]i32, + + // allocator must be outside of struct as it will fail the json parser + pub fn deinit(self: *const Frame, allocator: Allocator) void { + for (self.contents) |con| { + allocator.free(con); + } + allocator.free(self.contents); + + for (self.colorMap) |cm| { + for (cm) |int2| { + allocator.free(int2); + } + allocator.free(cm); + } + allocator.free(self.colorMap); + } +}; + +// https://github.com/cmang/durdraw/blob/0.29.0/durformat.md +const DurFormatRaw = struct { + allocator: Allocator, + formatVersion: ?i64 = null, + colorFormat: ?[]const u8 = null, + encoding: ?[]const u8 = null, + framerate: ?f64 = null, + columns: ?i64 = null, + lines: ?i64 = null, + frames: std.ArrayList(Frame) = undefined, + + // Validate data and return a valid DurFormat + // Consumes `self`, making it unusable after + pub fn validate(self: *DurFormatRaw) !DurFormat { + // v8 may have breaking changes like changing the colormap xy direction + // (https://github.com/cmang/durdraw/issues/24) + const format_version = self.formatVersion orelse return error.MissingFieldVersion; + if (format_version != 7) return error.UnsupportedVersion; + + const color_format_str = self.colorFormat orelse return error.MissingFieldColorFormat; + // Code currently only supports 16 and 256 color format only + const color_format: DurColorFormat = + if (eql(u8, color_format_str, "16")) .@"16" else if (eql(u8, color_format_str, "256")) .@"256" else return error.UnsupportedColorFormat; + + const encoding_str = self.encoding orelse return error.MissingFieldEncoding; + // Code currently supports only utf-8 encoding + const encoding: DurEncoding = if (eql(u8, encoding_str, "utf-8")) .utf_8 else return error.UnsupportedEncoding; + + if (self.framerate == null) return error.MissingFieldFramerate; + if (self.framerate.? <= 0) return error.InvalidFramerate; + const framerate: f64 = self.framerate.?; + + // Sanity check on file + if (self.columns == null or self.lines == null) return error.MissingDimensions; + const columns = std.math.cast(u32, self.columns.?) orelse return error.InvalidColumnCount; + const lines = std.math.cast(u32, self.lines.?) orelse return error.InvalidLineCount; + + if (self.frames.items.len == 0) return error.NoFrames; + const frames = self.frames; + + return .{ + .allocator = self.allocator, + .formatVersion = format_version, + .colorFormat = color_format, + .encoding = encoding, + .framerate = framerate, + .columns = columns, + .lines = lines, + .frames = frames, + }; + } + + fn parseFromJson(self: *DurFormatRaw, allocator: Allocator, dur_json_root: Json.Value) !void { + var dur_movie = if (dur_json_root.object.get("DurMovie")) |dm| dm.object else return error.NotValidFile; + + // Depending on the version, a dur file can have different json object names (ie: columns vs sizeX) + self.formatVersion = if (dur_movie.get("formatVersion")) |x| x.integer else null; + self.colorFormat = if (dur_movie.get("colorFormat")) |x| try allocator.dupe(u8, x.string) else null; + self.encoding = if (dur_movie.get("encoding")) |x| try allocator.dupe(u8, x.string) else null; + self.framerate = if (dur_movie.get("framerate")) |x| x.float else null; + self.columns = if (dur_movie.get("columns")) |x| x.integer else if (dur_movie.get("sizeX")) |x| x.integer else null; + + self.lines = if (dur_movie.get("lines")) |x| x.integer else if (dur_movie.get("sizeY")) |x| x.integer else null; + + const frames = dur_movie.get("frames") orelse return error.NotValidFile; + + self.frames = try .initCapacity(allocator, frames.array.items.len); + + for (frames.array.items) |json_frame| { + var parsed_frame = try Json.parseFromValue(Frame, allocator, json_frame, .{}); + defer parsed_frame.deinit(); + + const frame_val = parsed_frame.value; + + // copy all fields to own the ptrs for deallocation, the parsed_frame has some other + // allocated memory making it difficult to deallocate without leaks + const frame: Frame = .{ .frameNumber = frame_val.frameNumber, .delay = frame_val.delay, .contents = try allocator.alloc([]u8, frame_val.contents.len), .colorMap = try allocator.alloc([][]i32, frame_val.colorMap.len) }; + + for (0..frame.contents.len) |i| { + frame.contents[i] = try allocator.dupe(u8, frame_val.contents[i]); + } + + // colorMap is stored as an 3d array where: + // the outer (i) most array is the horizontal position of the color + // the middle (j) is the vertical position of the color + // the inner (0/1) is the foreground/background color + for (0..frame.colorMap.len) |i| { + frame.colorMap[i] = try allocator.alloc([]i32, frame_val.colorMap[i].len); + for (0..frame.colorMap[i].len) |j| { + frame.colorMap[i][j] = try allocator.alloc(i32, 2); + frame.colorMap[i][j][0] = frame_val.colorMap[i][j][0]; + frame.colorMap[i][j][1] = frame_val.colorMap[i][j][1]; + } + } + + try self.frames.append(allocator, frame); + } + } + + pub fn createFromFile(self: *DurFormatRaw, allocator: Allocator, io: std.Io, file_path: []const u8) !void { + const file = try std.Io.Dir.cwd().openFile(io, file_path, .{}); + defer file.close(io); + + var reader_buffer: [4096]u8 = undefined; + var decompress_buffer: [flate.max_window_len]u8 = undefined; + + var file_reader = file.reader(io, &reader_buffer); + var decompress: flate.Decompress = .init(&file_reader.interface, .gzip, &decompress_buffer); + + var json_reader = Json.Reader.init(allocator, &decompress.reader); + defer json_reader.deinit(); + + const json = try Json.parseFromTokenSource(Json.Value, allocator, &json_reader, .{}); + defer json.deinit(); + + try parseFromJson(self, allocator, json.value); + } + + pub fn init(allocator: Allocator) DurFormatRaw { + return .{ .allocator = allocator }; + } + + pub fn deinit(self: *DurFormatRaw) void { + if (self.colorFormat) |str| self.allocator.free(str); + if (self.encoding) |str| self.allocator.free(str); + } +}; + +const DurColorFormat = enum { + @"16", + @"256", +}; + +const DurEncoding = enum { utf_8 }; + +const DurFormat = struct { + allocator: Allocator, + formatVersion: i64, + colorFormat: DurColorFormat, + encoding: DurEncoding, + framerate: f64, + columns: u32, + lines: u32, + frames: std.ArrayList(Frame), + + pub fn deinit(self: *DurFormat) void { + for (self.frames.items) |frame| { + frame.deinit(self.allocator); + } + self.frames.deinit(self.allocator); + } +}; + +const tb_color_16 = [16]u32{ + Color.ECOL_BLACK, + Color.ECOL_RED, + Color.ECOL_GREEN, + Color.ECOL_YELLOW, + Color.ECOL_BLUE, + Color.ECOL_MAGENTA, + Color.ECOL_CYAN, + Color.ECOL_WHITE, + Color.ECOL_BLACK | Styling.BOLD, + Color.ECOL_RED | Styling.BOLD, + Color.ECOL_GREEN | Styling.BOLD, + Color.ECOL_YELLOW | Styling.BOLD, + Color.ECOL_BLUE | Styling.BOLD, + Color.ECOL_MAGENTA | Styling.BOLD, + Color.ECOL_CYAN | Styling.BOLD, + Color.ECOL_WHITE | Styling.BOLD, +}; + +// Using bold for bright colors allows for all 16 colors to be rendered on tty term +const rgb_color_16 = [16]u32{ + Color.DEFAULT, // DEFAULT instead of TRUE_BLACK to not break compositors (the latter ignores transparency) + Color.TRUE_DIM_RED, + Color.TRUE_DIM_GREEN, + Color.TRUE_DIM_YELLOW, + Color.TRUE_DIM_BLUE, + Color.TRUE_DIM_MAGENTA, + Color.TRUE_DIM_CYAN, + Color.TRUE_DIM_WHITE, + Color.DEFAULT | Styling.BOLD, + Color.TRUE_RED | Styling.BOLD, + Color.TRUE_GREEN | Styling.BOLD, + Color.TRUE_YELLOW | Styling.BOLD, + Color.TRUE_BLUE | Styling.BOLD, + Color.TRUE_MAGENTA | Styling.BOLD, + Color.TRUE_CYAN | Styling.BOLD, + Color.TRUE_WHITE | Styling.BOLD, +}; + +// Made this table from looking at colormapping in dur source, not sure whats going on with the mapping logic +// Array indexes are dur colormappings which value maps to indexes in table above. Only needed for dur 16 color +const durcolor_table_to_color16 = [17]u32{ + 0, // 0 black + 0, // 1 nothing?? dur source did not say why 1 is unused + 4, // 2 blue + 2, // 3 green + 6, // 4 cyan + 1, // 5 red + 5, // 6 magenta + 3, // 7 yellow + 7, // 8 light gray + 8, // 9 gray + 12, // 10 bright blue + 10, // 11 bright green + 14, // 12 bright cyan + 9, // 13 bright red + 13, // 14 bright magenta + 11, // 15 bright yellow + 15, // 16 bright white +}; + +fn sixCubeToChannel(sixcube: u32) u32 { + // Although the range top for the extended range is 0xFF, 6 is not divisible into 0xFF, + // so we use 0xF0 instead with a scaler + const equal_divisions = 0xF0 / 6; + + // Since the range is to 0xFF but 6 isn't divisible, we must add a scaler to get it to 0xFF at the last index (5) + const scaler = 0xFF - (equal_divisions * 5); + + return if (sixcube > 0) (sixcube * equal_divisions) + scaler else 0; +} + +fn convert256ToRgb(color_256: u32) u32 { + var rgb_color: u32 = 0; + + // 0 - 15 is the standard color range, map to array table + if (color_256 < 16) { + rgb_color = rgb_color_16[color_256]; + } + // 16 - 231 is the extended range + else if (color_256 < 232) { + + // For extended term range we subtract by 16 to get it in a 0..(6x6x6) cube (range of 216) + // divide by 36 gets the depth of the cube (6x6x1) + // divide by 6 gets the width of the cube (6x1) + // divide by 1 gets the height of the cube (divide 1 for clarity for what we are doing) + // each channel can be 6 levels of brightness hence remander operation of 6 + // finally bitshift to correct rgb channel (16 for red, 8 for green, 0 for blue) + rgb_color |= sixCubeToChannel(((color_256 - 16) / 36) % 6) << 16; + rgb_color |= sixCubeToChannel(((color_256 - 16) / 6) % 6) << 8; + rgb_color |= sixCubeToChannel(((color_256 - 16) / 1) % 6); + } + // 232 - 255 is the grayscale range + else { + + // For grayscale we have a space of 232 - 255 (24) + // subtract by 232 to get it into the 0..23 range + // standard colors will contain white and black, so we do not use them in the grayscale range (0 is 0x08, 23 is 0xEE) + // this results in a skip of 0x08 for the first color and divisions of 0x0A + // example: term_col 232 = scaler + equal_divisions * (232 - 232) which becomes (scaler + 0x00) == 0x08 + // example: term_col 255 = scaler + equal_divisions * (255 - 232) which becomes (scaler + 0xE6) == 0xEE + const scaler = 0x08; + + // to get equal parts, the equation is: + // 0xEE = equal_divisions * 23 + scaler | top of range is 0xEE, 23 is last element value (255 minus 232) + // reordered to solve for equal_divisions: + const equal_divisions = (0xEE - scaler) / 23; // evals to 0x0A + + const channel = scaler + equal_divisions * (color_256 - 232); + + // gray is equal value of same channel color in rgb + rgb_color = channel | (channel << 8) | (channel << 16); + } + + return rgb_color; +} + +const UVec2 = @Vector(2, u32); +const IVec2 = @Vector(2, i64); + +const VEC_X = 0; +const VEC_Y = 1; + +const DurFile = @This(); + +instance: ?Widget = null, +start_time: TimeOfDay, +allocator: Allocator, +io: std.Io, +terminal_buffer: *TerminalBuffer, +dur_movie: DurFormat, +frames: usize, +start_pos: IVec2, +full_color: bool, +animate: *bool, +timeout_sec: u12, +frame_delay: u16, +frame_time: u32, +time_previous: i64, +is_color_format_16: bool, +offset_alignment: DurOffsetAlignment, +offset: IVec2, + +// if the user has an even number of columns or rows, we will default to the left or higher position (e.g. 4 columns center = .x..) +fn center(v: i64) i64 { + return @intCast(@divTrunc(v, 2) + @mod(v, 2)); +} + +fn calculateStartPos(terminal_buffer: *TerminalBuffer, dur_movie: *DurFormat, offset_alignment: DurOffsetAlignment, offset: IVec2) IVec2 { + const buf_width: i64 = @intCast(terminal_buffer.width); + const buf_height: i64 = @intCast(terminal_buffer.height); + + const movie_width: i64 = @intCast(dur_movie.columns); + const movie_height: i64 = @intCast(dur_movie.lines); + + const start_pos: IVec2 = switch (offset_alignment) { + DurOffsetAlignment.center => .{ center(buf_width) - center(movie_width), center(buf_height) - center(movie_height) }, + DurOffsetAlignment.topleft => .{ 0, 0 }, + DurOffsetAlignment.topcenter => .{ center(buf_width) - center(movie_width), 0 }, + DurOffsetAlignment.topright => .{ buf_width - movie_width, 0 }, + DurOffsetAlignment.centerleft => .{ 0, center(buf_height) - center(movie_height) }, + DurOffsetAlignment.centerright => .{ buf_width - movie_width, center(buf_height) - center(movie_height) }, + DurOffsetAlignment.bottomleft => .{ 0, buf_height - movie_height }, + DurOffsetAlignment.bottomcenter => .{ center(buf_width) - center(movie_width), buf_height - movie_height }, + DurOffsetAlignment.bottomright => .{ buf_width - movie_width, buf_height - movie_height }, + }; + + return start_pos + offset; +} + +pub fn init( + allocator: Allocator, + io: std.Io, + terminal_buffer: *TerminalBuffer, + log_file: *LogFile, + file_path: []const u8, + offset_alignment: DurOffsetAlignment, + x_offset: i32, + y_offset: i32, + full_color: bool, + animate: *bool, + timeout_sec: u12, + frame_delay: u16, +) !DurFile { + var dur_movie_raw: DurFormatRaw = .init(allocator); + defer dur_movie_raw.deinit(); + + dur_movie_raw.createFromFile(allocator, io, file_path) catch |err| switch (err) { + error.FileNotFound => { + try log_file.err(io, "tui", "dur_file was not found at: {s}", .{file_path}); + return err; + }, + error.NotValidFile => { + try log_file.err(io, "tui", "dur_file loaded was invalid or not a dur file!", .{}); + return err; + }, + else => return err, + }; + + var dur_movie = dur_movie_raw.validate() catch |err| switch (err) { + error.MissingFieldVersion => { + try log_file.err(io, "tui", "dur_file loaded was invalid: missing field formatVersion!", .{}); + return err; + }, + error.UnsupportedVersion => { + try log_file.err(io, "tui", "dur_file loaded was invalid: unsupported version ({d})!", .{dur_movie_raw.formatVersion.?}); + return err; + }, + error.MissingFieldColorFormat => { + try log_file.err(io, "tui", "dur_file loaded was invalid: missing field colorFormat!", .{}); + return err; + }, + error.UnsupportedColorFormat => { + try log_file.err(io, "tui", "dur_file loaded was invalid: unsupported colorFormat ({s})!", .{dur_movie_raw.colorFormat.?}); + return err; + }, + error.MissingFieldEncoding => { + try log_file.err(io, "tui", "dur_file loaded was invalid: missing field encoding!", .{}); + return err; + }, + error.UnsupportedEncoding => { + try log_file.err(io, "tui", "dur_file loaded was invalid: unsupported encoding ({s})!", .{dur_movie_raw.encoding.?}); + return err; + }, + error.MissingFieldFramerate => { + try log_file.err(io, "tui", "dur_file loaded was invalid: missing field framerate!", .{}); + return err; + }, + error.InvalidFramerate => { + try log_file.err(io, "tui", "dur_file loaded was invalid: negative framerate value found!", .{}); + return err; + }, + error.MissingDimensions => { + try log_file.err(io, "tui", "dur_file loaded was invalid: missing field(s) lines and/or columns!", .{}); + return err; + }, + error.InvalidColumnCount => { + try log_file.err(io, "tui", "dur_file loaded was invalid: columns value falls outside of supported range ({d})!", .{dur_movie_raw.columns.?}); + return err; + }, + error.InvalidLineCount => { + try log_file.err(io, "tui", "dur_file loaded was invalid: lines value falls outside of supported range ({d})!", .{dur_movie_raw.lines.?}); + return err; + }, + error.NoFrames => { + try log_file.err(io, "tui", "dur_file loaded was invalid: animation has no frames!", .{}); + return err; + }, + }; + + // 4 bit mode with 256 color is unsupported + if (!full_color and dur_movie.colorFormat == .@"256") { + try log_file.err(io, "tui", "dur_file can not be 256 color encoded when not using full_color option!", .{}); + dur_movie.deinit(); + return error.NotFullColor; + } + + const offset: IVec2 = .{ x_offset, y_offset }; + + const start_pos = calculateStartPos(terminal_buffer, &dur_movie, offset_alignment, offset); + + // Convert dur fps to frames per ms + const frame_time: u32 = @trunc(1000 / dur_movie.framerate); + + return .{ + .instance = null, + .start_time = try interop.getTimeOfDay(), + .allocator = allocator, + .io = io, + .terminal_buffer = terminal_buffer, + .frames = 0, + .time_previous = std.Io.Timestamp.now(io, .real).toMilliseconds(), + .start_pos = start_pos, + .full_color = full_color, + .animate = animate, + .timeout_sec = timeout_sec, + .frame_delay = frame_delay, + .dur_movie = dur_movie, + .frame_time = frame_time, + .is_color_format_16 = dur_movie.colorFormat == .@"16", + .offset_alignment = offset_alignment, + .offset = offset, + }; +} + +pub fn widget(self: *DurFile) *Widget { + if (self.instance) |*instance| return instance; + self.instance = Widget.init( + "DurFile", + null, + self, + deinit, + realloc, + draw, + update, + null, + calculateTimeout, + ); + return &self.instance.?; +} + +fn deinit(self: *DurFile) void { + self.dur_movie.deinit(); +} + +fn realloc(self: *DurFile) !void { + // when terminal size changes, we need to recalculate the start_pos based on the new size + self.start_pos = calculateStartPos(self.terminal_buffer, &self.dur_movie, self.offset_alignment, self.offset); +} + +fn draw(self: *DurFile) void { + if (!self.animate.*) return; + + const current_frame = self.dur_movie.frames.items[self.frames]; + + // y is used as an iterator in the durformat, while cell_y gives us the correct placement for the cell (same for x) + for (0..@intCast(self.dur_movie.lines)) |y| { + const cell_y = @as(i32, @intCast(y)) + self.start_pos[VEC_Y]; + + var iter = std.unicode.Utf8View.initUnchecked(current_frame.contents[y]).iterator(); + + for (0..@intCast(self.dur_movie.columns)) |x| { + const cell_x = @as(i32, @intCast(x)) + self.start_pos[VEC_X]; + + const codepoint: u21 = iter.nextCodepoint().?; + const color_map = current_frame.colorMap[x][y]; + + var color_map_0: u32 = @intCast(if (color_map[0] == -1) 0 else color_map[0]); + var color_map_1: u32 = @intCast(if (color_map[1] == -1) 0 else color_map[1]); + + if (self.is_color_format_16) { + color_map_0 = durcolor_table_to_color16[color_map_0]; + color_map_1 = durcolor_table_to_color16[color_map_1 + 1]; // Add 1, dur source stores it like this for some reason + } + + const fg_color = if (self.full_color) convert256ToRgb(color_map_0) else tb_color_16[color_map_0]; + const bg_color = if (self.full_color) convert256ToRgb(color_map_1) else tb_color_16[color_map_1]; + + const cell = Cell{ .ch = @intCast(codepoint), .fg = fg_color, .bg = bg_color }; + + self.terminal_buffer.setCellBoundsChecked(cell_x, cell_y, cell) catch {}; + } + } + + const time_current = std.Io.Timestamp.now(self.io, .real).toMilliseconds(); + const delta_time = time_current - self.time_previous; + + // Convert delay from sec to ms + const delay_time: u32 = @trunc(current_frame.delay * 1000); + if (delta_time > (self.frame_time + delay_time)) { + self.time_previous = time_current; + + const frame_count = self.dur_movie.frames.items.len; + self.frames = (self.frames + 1) % frame_count; + } +} + +fn update(self: *DurFile, _: *anyopaque) !void { + const time = try interop.getTimeOfDay(); + + if (self.timeout_sec > 0 and time.seconds - self.start_time.seconds > self.timeout_sec) { + self.animate.* = false; + } +} + +fn calculateTimeout(self: *DurFile, _: *anyopaque) !?usize { + return self.frame_delay; +} diff --git a/src/animations/GameOfLife.zig b/src/animations/GameOfLife.zig new file mode 100644 index 0000000..6e9a16c --- /dev/null +++ b/src/animations/GameOfLife.zig @@ -0,0 +1,240 @@ +const std = @import("std"); +const Allocator = std.mem.Allocator; + +const ly_ui = @import("ly-ui"); +const Cell = ly_ui.Cell; +const TerminalBuffer = ly_ui.TerminalBuffer; +const Widget = ly_ui.Widget; + +const ly_core = ly_ui.ly_core; +const interop = ly_core.interop; +const TimeOfDay = interop.TimeOfDay; + +const GameOfLife = @This(); + +// Visual styles - using block characters like other animations +const ALIVE_CHAR: u21 = 0x2588; // Full block █ +const DEAD_CHAR: u21 = ' '; +const NEIGHBOR_DIRS = [_][2]i8{ + .{ -1, -1 }, .{ -1, 0 }, .{ -1, 1 }, + .{ 0, -1 }, .{ 0, 1 }, .{ 1, -1 }, + .{ 1, 0 }, .{ 1, 1 }, +}; + +instance: ?Widget = null, +start_time: TimeOfDay, +allocator: Allocator, +terminal_buffer: *TerminalBuffer, +current_grid: []bool, +next_grid: []bool, +frame_counter: usize, +generation: u64, +fg_color: u32, +entropy_interval: usize, +frame_delay: usize, +initial_density: f32, +animate: *bool, +timeout_sec: u12, +animation_frame_delay: u16, +dead_cell: Cell, +width: usize, +height: usize, + +pub fn init( + allocator: Allocator, + terminal_buffer: *TerminalBuffer, + fg_color: u32, + entropy_interval: usize, + frame_delay: usize, + initial_density: f32, + animate: *bool, + timeout_sec: u12, + animation_frame_delay: u16, +) !GameOfLife { + const width = terminal_buffer.width; + const height = terminal_buffer.height; + const grid_size = width * height; + + const current_grid = try allocator.alloc(bool, grid_size); + const next_grid = try allocator.alloc(bool, grid_size); + + var game = GameOfLife{ + .instance = null, + .start_time = try interop.getTimeOfDay(), + .allocator = allocator, + .terminal_buffer = terminal_buffer, + .current_grid = current_grid, + .next_grid = next_grid, + .frame_counter = 0, + .generation = 0, + .fg_color = fg_color, + .entropy_interval = entropy_interval, + .frame_delay = frame_delay, + .initial_density = initial_density, + .animate = animate, + .timeout_sec = timeout_sec, + .animation_frame_delay = animation_frame_delay, + .dead_cell = .{ .ch = DEAD_CHAR, .fg = @intCast(TerminalBuffer.Color.DEFAULT), .bg = terminal_buffer.bg }, + .width = width, + .height = height, + }; + + // Initialize grid + game.initializeGrid(); + + return game; +} + +pub fn widget(self: *GameOfLife) *Widget { + if (self.instance) |*instance| return instance; + self.instance = Widget.init( + "GameOfLife", + null, + self, + deinit, + realloc, + draw, + update, + null, + calculateTimeout, + ); + return &self.instance.?; +} + +fn deinit(self: *GameOfLife) void { + self.allocator.free(self.current_grid); + self.allocator.free(self.next_grid); +} + +fn realloc(self: *GameOfLife) !void { + const new_width = self.terminal_buffer.width; + const new_height = self.terminal_buffer.height; + const new_size = new_width * new_height; + + const current_grid = try self.allocator.realloc(self.current_grid, new_size); + const next_grid = try self.allocator.realloc(self.next_grid, new_size); + + self.current_grid = current_grid; + self.next_grid = next_grid; + self.width = new_width; + self.height = new_height; + + self.initializeGrid(); + self.generation = 0; +} + +fn draw(self: *GameOfLife) void { + if (!self.animate.*) return; + + // Update game state at controlled frame rate + self.frame_counter += 1; + if (self.frame_counter >= self.frame_delay) { + self.frame_counter = 0; + self.updateGeneration(); + self.generation += 1; + + // Add entropy based on configuration (0 = disabled, >0 = interval) + if (self.entropy_interval > 0 and self.generation % self.entropy_interval == 0) { + self.addEntropy(); + } + } + + // Render with the configured color + const alive_cell = Cell{ .ch = ALIVE_CHAR, .fg = self.fg_color, .bg = self.terminal_buffer.bg }; + + for (0..self.height) |y| { + const row_offset = y * self.width; + for (0..self.width) |x| { + const cell = if (self.current_grid[row_offset + x]) alive_cell else self.dead_cell; + cell.put(x, y) catch {}; + } + } +} + +fn update(self: *GameOfLife, _: *anyopaque) !void { + const time = try interop.getTimeOfDay(); + + if (self.timeout_sec > 0 and time.seconds - self.start_time.seconds > self.timeout_sec) { + self.animate.* = false; + } +} + +fn calculateTimeout(self: *GameOfLife, _: *anyopaque) !?usize { + return self.animation_frame_delay; +} + +fn updateGeneration(self: *GameOfLife) void { + // Conway's Game of Life rules with optimized neighbor counting + for (0..self.height) |y| { + const row_offset = y * self.width; + for (0..self.width) |x| { + const index = row_offset + x; + const neighbors = self.countNeighborsOptimized(x, y); + const is_alive = self.current_grid[index]; + + // Optimized rule application + self.next_grid[index] = switch (neighbors) { + 2 => is_alive, + 3 => true, + else => false, + }; + } + } + + // Efficient grid swap + std.mem.swap([]bool, &self.current_grid, &self.next_grid); +} + +fn countNeighborsOptimized(self: *GameOfLife, x: usize, y: usize) u8 { + var count: u8 = 0; + + for (NEIGHBOR_DIRS) |dir| { + const neighbor_x = @as(i32, @intCast(x)) + dir[0]; + const neighbor_y = @as(i32, @intCast(y)) + dir[1]; + const width_i32: i32 = @intCast(self.width); + const height_i32: i32 = @intCast(self.height); + + // Toroidal wrapping with modular arithmetic + const wx: usize = @intCast(@mod(neighbor_x + width_i32, width_i32)); + const wy: usize = @intCast(@mod(neighbor_y + height_i32, height_i32)); + + if (self.current_grid[wy * self.width + wx]) { + count += 1; + } + } + + return count; +} + +fn initializeGrid(self: *GameOfLife) void { + const total_cells = self.width * self.height; + + // Clear grid + @memset(self.current_grid, false); + @memset(self.next_grid, false); + + // Random initialization with configurable density + for (0..total_cells) |i| { + self.current_grid[i] = self.terminal_buffer.random.float(f32) < self.initial_density; + } +} + +fn addEntropy(self: *GameOfLife) void { + // Add fewer random cells but in clusters for more interesting patterns + const clusters = 2; + for (0..clusters) |_| { + const cx = self.terminal_buffer.random.intRangeAtMost(usize, 1, self.width - 2); + const cy = self.terminal_buffer.random.intRangeAtMost(usize, 1, self.height - 2); + + // Small cluster around center point + for (0..3) |dy| { + for (0..3) |dx| { + if (self.terminal_buffer.random.float(f32) < 0.4) { + const x = (cx + dx) % self.width; + const y = (cy + dy) % self.height; + self.current_grid[y * self.width + x] = true; + } + } + } + } +} diff --git a/src/animations/Lua.zig b/src/animations/Lua.zig new file mode 100644 index 0000000..2d2956f --- /dev/null +++ b/src/animations/Lua.zig @@ -0,0 +1,304 @@ +const std = @import("std"); +const ly_ui = @import("ly-ui"); +const LogFile = ly_ui.ly_core.LogFile; +const Widget = ly_ui.Widget; +const TerminalBuffer = ly_ui.TerminalBuffer; +const Cell = ly_ui.Cell; +const Allocator = std.mem.Allocator; +const InfoLine = @import("../components/InfoLine.zig"); +const Lang = @import("../config/Lang.zig"); + +const zlua = @import("zlua"); + +const ly_lua = @embedFile("ly.lua"); + +const Lua = @This(); + +allocator: Allocator, +instance: ?Widget = null, +lua: *zlua.Lua, +log: *LogFile, +terminal_buffer: *TerminalBuffer, +width: usize, +height: usize, +margin: usize, +io: std.Io, +animation_delay: u16, + +info_line: *InfoLine, +fg: u32, +bg: u32, + +lang: Lang, +full_color: bool, + +lua_error: bool = false, +lua_error_logged: bool = false, +lua_str: ?[:0]const u8 = null, + +pub fn init( + io: std.Io, + alloc: Allocator, + log: *LogFile, + buf: *TerminalBuffer, + file: []const u8, + margin: u8, + animation_delay: u16, + info_line: *InfoLine, + fg: u32, + bg: u32, + lang: Lang, + full_color: bool, +) !Lua { + var self: Lua = .{ + .lua = try zlua.Lua.init(alloc), + .allocator = alloc, + .terminal_buffer = buf, + .instance = null, + .log = log, + .width = 0, + .height = 0, + .margin = margin, + .io = io, + .animation_delay = animation_delay, + .info_line = info_line, + .fg = fg, + .bg = bg, + .lang = lang, + .full_color = full_color, + }; + + // exclude IO and debug libraries + self.lua.openBase(); + self.lua.openBit(); + self.lua.openMath(); + self.lua.openString(); + self.lua.openTable(); + + file_loading: { + const zf = std.mem.concatWithSentinel(alloc, u8, &[1][]const u8{file}, 0) catch |e| { + try self.log.err(self.io, "lua", "failed to allocate file path: {}", .{e}); + self.lua_str = "failed to allocate file path!"; + self.info_line.addMessage(lang.err_alloc, self.bg, self.fg) catch {}; + return e; + }; + defer alloc.free(zf); + + // create the ly table + self.lua.newTable(); + self.lua.setGlobal("ly"); + + // create ly.width and ly.height from TerminalBuffer width/height + self.propagateTerminalBounds(); + + _ = self.lua.getGlobal("ly"); + _ = self.lua.pushString("clock"); + self.lua.pushFunction(luaLyClock); + self.lua.setTable(-3); + _ = self.lua.pushString("putCell"); + self.lua.pushFunction(luaPutCell); + self.lua.setTable(-3); + _ = self.lua.pushString("putLabel"); + self.lua.pushFunction(luaPutLabel); + self.lua.setTable(-3); + _ = self.lua.pushString("putRect"); + self.lua.pushFunction(luaPutRect); + self.lua.setTable(-3); + self.lua.setGlobal("ly"); + + self.lua.doFile(zf) catch { + const errorStr = self.lua.toString(-1) catch unreachable; + self.lua_str = try self.allocator.dupeSentinel(u8, errorStr, 0); + try self.log.err(self.io, "lua", "lua error: {s}", .{errorStr}); + self.lua_error = true; + break :file_loading; + }; + } + + return self; +} + +fn draw(self: *Lua) void { + self.propagateTerminalBounds(); + if (self.lua_error) { + // Ly's Red Screen of Omega-Death:tm: + const RED: u32 = if (self.full_color) TerminalBuffer.Color.TRUE_RED else TerminalBuffer.Color.ECOL_RED; + const cell = Cell.init(0x2588, RED, RED); + for (0..self.terminal_buffer.height) |y| + for (0..self.terminal_buffer.width) |x| + cell.put(x, y) catch {}; + if (self.lua_str) |str| + for (str, 0..) |c, i| { + const dwidth = @divFloor(self.width, 2); + const dlen = @divFloor(str.len, 2); + Cell.init(c, 0x00FFFFFF, 0).put( + (if (dlen > dwidth) 0 else dwidth - dlen) + i, + self.margin + 5, + ) catch {}; + }; + if (!self.lua_error_logged) { + self.info_line.addMessage("lua animation failed", self.bg, self.fg) catch {}; + self.lua_error_logged = true; + } + return; + } + + _ = self.lua.getGlobal("draw"); + self.lua.protectedCall(.{}) catch { + const errorStr = self.lua.toString(-1) catch unreachable; + self.lua_str = std.mem.concatWithSentinel( + self.allocator, + u8, + &.{ "cannot call draw(): ", errorStr }, + 0, + ) catch unreachable; + self.log.err(self.io, "lua", "error (cannot call draw()): {s}", .{errorStr}) catch unreachable; + self.lua_error = true; + }; +} + +fn calculateTimeout(self: *Lua, _: *anyopaque) !?usize { + return self.animation_delay; +} + +fn deinit(self: *Lua) void { + if (self.lua_str) |str| self.allocator.free(str); + self.lua.deinit(); +} + +pub fn widget(self: *Lua) *Widget { + if (self.instance) |*inst| return inst; + self.instance = Widget.init( + "Lua", + null, + self, + deinit, + null, + draw, + null, + null, + calculateTimeout, + ); + return &self.instance.?; +} + +fn propagateTerminalBounds(self: *Lua) void { + if (self.terminal_buffer.height == self.height and + self.terminal_buffer.width == self.width) + return; + self.width = self.terminal_buffer.width; + self.height = self.terminal_buffer.height; + _ = self.lua.getGlobal("ly"); + _ = self.lua.pushString("width"); + self.lua.pushInteger(@intCast(self.terminal_buffer.width)); + self.lua.setTable(-3); + _ = self.lua.pushString("height"); + self.lua.pushInteger(@intCast(self.terminal_buffer.height)); + self.lua.setTable(-3); + self.lua.setGlobal("ly"); +} + +fn luaLyClock(state: ?*zlua.LuaState) callconv(.c) c_int { + var threaded = std.Io.Threaded.init_single_threaded; + const lua: *zlua.Lua = @ptrCast(@alignCast(state orelse unreachable)); + lua.pushInteger(std.Io.Timestamp.now(threaded.io(), .real).toMicroseconds()); + return 1; +} + +fn luaPutCell(state: ?*zlua.LuaState) callconv(.c) c_int { + const lua: *zlua.Lua = @ptrCast(@alignCast(state orelse unreachable)); + const MSG = "ly.putCell: cannot convert %s-typed "; + const byte = lua.toNumeric(u32, 1) catch { + const t = lua.typeName(lua.typeOf(1)); + lua.raiseErrorStr(MSG ++ "byte to u32", .{t.ptr}); + }; + const fg = lua.toNumeric(u32, 2) catch { + const t = lua.typeName(lua.typeOf(2)); + lua.raiseErrorStr(MSG ++ "fg to u32", .{t.ptr}); + }; + const bg = lua.toNumeric(u32, 3) catch { + const t = lua.typeName(lua.typeOf(3)); + lua.raiseErrorStr(MSG ++ "bg to u32", .{t.ptr}); + }; + const x = lua.toNumeric(usize, 4) catch { + const t = lua.typeName(lua.typeOf(4)); + lua.raiseErrorStr(MSG ++ "x to usize", .{t.ptr}); + }; + const y = lua.toNumeric(usize, 5) catch { + const t = lua.typeName(lua.typeOf(5)); + lua.raiseErrorStr(MSG ++ "y to usize", .{t.ptr}); + }; + TerminalBuffer.setCell(x, y, .{ + .fg = fg, + .bg = bg, + .ch = byte, + }) catch {}; + return 0; +} + +fn luaPutRect(state: ?*zlua.LuaState) callconv(.c) c_int { + const lua: *zlua.Lua = @ptrCast(@alignCast(state orelse unreachable)); + const MSG = "ly.putRect: cannot convert %s-typed "; + const byte = lua.toNumeric(u32, 1) catch { + const t = lua.typeName(lua.typeOf(1)); + lua.raiseErrorStr(MSG ++ "byte to u32", .{t.ptr}); + }; + const fg = lua.toNumeric(u32, 2) catch { + const t = lua.typeName(lua.typeOf(2)); + lua.raiseErrorStr(MSG ++ "fg to u32", .{t.ptr}); + }; + const bg = lua.toNumeric(u32, 3) catch { + const t = lua.typeName(lua.typeOf(3)); + lua.raiseErrorStr(MSG ++ "bg to u32", .{t.ptr}); + }; + const x = lua.toNumeric(usize, 4) catch { + const t = lua.typeName(lua.typeOf(4)); + lua.raiseErrorStr(MSG ++ "x to usize", .{t.ptr}); + }; + const y = lua.toNumeric(usize, 5) catch { + const t = lua.typeName(lua.typeOf(5)); + lua.raiseErrorStr(MSG ++ "y to usize", .{t.ptr}); + }; + const w = lua.toNumeric(usize, 6) catch { + const t = lua.typeName(lua.typeOf(5)); + lua.raiseErrorStr(MSG ++ "w to usize", .{t.ptr}); + }; + const h = lua.toNumeric(usize, 7) catch { + const t = lua.typeName(lua.typeOf(5)); + lua.raiseErrorStr(MSG ++ "h to usize", .{t.ptr}); + }; + for (0..w) |wx| for (0..h) |hy| + TerminalBuffer.setCell(x + wx, y + hy, .{ + .fg = fg, + .bg = bg, + .ch = byte, + }) catch {}; + return 0; +} + +fn luaPutLabel(state: ?*zlua.LuaState) callconv(.c) c_int { + const lua: *zlua.Lua = @ptrCast(@alignCast(state orelse unreachable)); + const MSG = "ly.putLabel: cannot convert %s-typed "; + const str = lua.toString(1) catch { + const t = lua.typeName(lua.typeOf(2)); + lua.raiseErrorStr(MSG ++ "str to string", .{t.ptr}); + }; + const fg = lua.toNumeric(u32, 2) catch { + const t = lua.typeName(lua.typeOf(2)); + lua.raiseErrorStr(MSG ++ "fg to u32", .{t.ptr}); + }; + const bg = lua.toNumeric(u32, 3) catch { + const t = lua.typeName(lua.typeOf(3)); + lua.raiseErrorStr(MSG ++ "bg to u32", .{t.ptr}); + }; + const x = lua.toNumeric(usize, 4) catch { + const t = lua.typeName(lua.typeOf(4)); + lua.raiseErrorStr(MSG ++ "x to usize", .{t.ptr}); + }; + const y = lua.toNumeric(usize, 5) catch { + const t = lua.typeName(lua.typeOf(5)); + lua.raiseErrorStr(MSG ++ "y to usize", .{t.ptr}); + }; + TerminalBuffer.drawText(str, x, y, fg, bg) catch {}; + return 0; +} diff --git a/src/animations/Matrix.zig b/src/animations/Matrix.zig index ba7e6e1..3d43118 100644 --- a/src/animations/Matrix.zig +++ b/src/animations/Matrix.zig @@ -1,16 +1,17 @@ const std = @import("std"); const Allocator = std.mem.Allocator; -const Random = std.rand.Random; -const TerminalBuffer = @import("../tui/TerminalBuffer.zig"); +const Random = std.Random; -const interop = @import("../interop.zig"); -const termbox = interop.termbox; +const ly_ui = @import("ly-ui"); +const Cell = ly_ui.Cell; +const TerminalBuffer = ly_ui.TerminalBuffer; +const Widget = ly_ui.Widget; -pub const FRAME_DELAY: u64 = 8; +const ly_core = ly_ui.ly_core; +const interop = ly_core.interop; +const TimeOfDay = interop.TimeOfDay; -// Allowed codepoints -pub const MIN_CODEPOINT: isize = 33; -pub const MAX_CODEPOINT: isize = 123 - MIN_CODEPOINT; +pub const FRAME_DELAY: usize = 8; // Characters change mid-scroll pub const MID_SCROLL_CHANGE = true; @@ -18,47 +19,91 @@ pub const MID_SCROLL_CHANGE = true; const Matrix = @This(); pub const Dot = struct { - value: isize, + value: ?usize, is_head: bool, }; pub const Line = struct { - space: isize, - length: isize, - update: isize, + space: usize, + length: usize, + update: usize, }; +instance: ?Widget = null, +start_time: TimeOfDay, allocator: Allocator, terminal_buffer: *TerminalBuffer, dots: []Dot, lines: []Line, -frame: u64, -count: u64, -fg_ini: u16, +frame: usize, +count: usize, +fg: u32, +head_col: u32, +min_codepoint: u16, +max_codepoint: u16, +animate: *bool, +timeout_sec: u12, +frame_delay: u16, +default_cell: Cell, -pub fn init(allocator: Allocator, terminal_buffer: *TerminalBuffer, fg_ini: u16) !Matrix { +pub fn init( + allocator: Allocator, + terminal_buffer: *TerminalBuffer, + fg: u32, + head_col: u32, + min_codepoint: u16, + max_codepoint: u16, + animate: *bool, + timeout_sec: u12, + frame_delay: u16, +) !Matrix { const dots = try allocator.alloc(Dot, terminal_buffer.width * (terminal_buffer.height + 1)); const lines = try allocator.alloc(Line, terminal_buffer.width); initBuffers(dots, lines, terminal_buffer.width, terminal_buffer.height, terminal_buffer.random); return .{ + .instance = null, + .start_time = try interop.getTimeOfDay(), .allocator = allocator, .terminal_buffer = terminal_buffer, .dots = dots, .lines = lines, .frame = 3, .count = 0, - .fg_ini = fg_ini, + .fg = fg, + .head_col = head_col, + .min_codepoint = min_codepoint, + .max_codepoint = max_codepoint - min_codepoint, + .animate = animate, + .timeout_sec = timeout_sec, + .frame_delay = frame_delay, + .default_cell = .{ .ch = ' ', .fg = fg, .bg = terminal_buffer.bg }, }; } -pub fn deinit(self: Matrix) void { +pub fn widget(self: *Matrix) *Widget { + if (self.instance) |*instance| return instance; + self.instance = Widget.init( + "Matrix", + null, + self, + deinit, + realloc, + draw, + update, + null, + calculateTimeout, + ); + return &self.instance.?; +} + +fn deinit(self: *Matrix) void { self.allocator.free(self.dots); self.allocator.free(self.lines); } -pub fn realloc(self: *Matrix) !void { +fn realloc(self: *Matrix) !void { const dots = try self.allocator.realloc(self.dots, self.terminal_buffer.width * (self.terminal_buffer.height + 1)); const lines = try self.allocator.realloc(self.lines, self.terminal_buffer.width); @@ -68,7 +113,9 @@ pub fn realloc(self: *Matrix) !void { self.lines = lines; } -pub fn draw(self: *Matrix) void { +fn draw(self: *Matrix) void { + if (!self.animate.*) return; + const buf_height = self.terminal_buffer.height; const buf_width = self.terminal_buffer.width; self.count += 1; @@ -78,19 +125,19 @@ pub fn draw(self: *Matrix) void { self.count = 0; var x: usize = 0; - while (x < self.terminal_buffer.width) : (x += 2) { + while (x < buf_width) : (x += 2) { var tail: usize = 0; var line = &self.lines[x]; if (self.frame <= line.update) continue; - if (self.dots[x].value == -1 and self.dots[self.terminal_buffer.width + x].value == ' ') { + if (self.dots[x].value == null and self.dots[buf_width + x].value == ' ') { if (line.space > 0) { line.space -= 1; } else { - const randint = self.terminal_buffer.random.int(i16); - const h: isize = @intCast(self.terminal_buffer.height); + const randint = self.terminal_buffer.random.int(u16); + const h = buf_height; line.length = @mod(randint, h - 3) + 3; - self.dots[x].value = @mod(randint, MAX_CODEPOINT) + MIN_CODEPOINT; + self.dots[x].value = @mod(randint, self.max_codepoint) + self.min_codepoint; line.space = @mod(randint, h + 1); } } @@ -101,7 +148,7 @@ pub fn draw(self: *Matrix) void { height_it: while (y <= buf_height) : (y += 1) { var dot = &self.dots[buf_width * y + x]; // Skip over spaces - while (y <= buf_height and (dot.value == ' ' or dot.value == -1)) { + while (y <= buf_height and (dot.value == ' ' or dot.value == null)) { y += 1; if (y > buf_height) break :height_it; dot = &self.dots[buf_width * y + x]; @@ -110,12 +157,12 @@ pub fn draw(self: *Matrix) void { // Find the head of this column tail = y; seg_len = 0; - while (y <= buf_height and dot.value != ' ' and dot.value != -1) { + while (y <= buf_height and dot.value != ' ' and dot.value != null) { dot.is_head = false; if (MID_SCROLL_CHANGE) { - const randint = self.terminal_buffer.random.int(i16); + const randint = self.terminal_buffer.random.int(u16); if (@mod(randint, 8) == 0) { - dot.value = @mod(randint, MAX_CODEPOINT) + MIN_CODEPOINT; + dot.value = @mod(randint, self.max_codepoint) + self.min_codepoint; } } @@ -129,13 +176,13 @@ pub fn draw(self: *Matrix) void { dot = &self.dots[buf_width * y + x]; } - const randint = self.terminal_buffer.random.int(i16); - dot.value = @mod(randint, MAX_CODEPOINT) + MIN_CODEPOINT; + const randint = self.terminal_buffer.random.int(u16); + dot.value = @mod(randint, self.max_codepoint) + self.min_codepoint; dot.is_head = true; if (seg_len > line.length or !first_col) { self.dots[buf_width * tail + x].value = ' '; - self.dots[x].value = -1; + self.dots[x].value = null; } first_col = false; } @@ -145,38 +192,48 @@ pub fn draw(self: *Matrix) void { var x: usize = 0; while (x < buf_width) : (x += 2) { var y: usize = 1; - while (y <= self.terminal_buffer.height) : (y += 1) { + while (y <= buf_height) : (y += 1) { const dot = self.dots[buf_width * y + x]; + const cell = if (dot.value == null or dot.value == ' ') self.default_cell else Cell{ + .ch = @intCast(dot.value.?), + .fg = if (dot.is_head) self.head_col else self.fg, + .bg = self.terminal_buffer.bg, + }; - var fg = self.fg_ini; - - if (dot.value == -1 or dot.value == ' ') { - _ = termbox.tb_set_cell(@intCast(x), @intCast(y - 1), ' ', fg, termbox.TB_DEFAULT); - continue; - } - - if (dot.is_head) fg = @intCast(termbox.TB_WHITE | termbox.TB_BOLD); - _ = termbox.tb_set_cell(@intCast(x), @intCast(y - 1), @intCast(dot.value), fg, termbox.TB_DEFAULT); + cell.put(x, y - 1) catch {}; + // Fill background in between columns + self.default_cell.put(x + 1, y - 1) catch {}; } } } +fn update(self: *Matrix, _: *anyopaque) !void { + const time = try interop.getTimeOfDay(); + + if (self.timeout_sec > 0 and time.seconds - self.start_time.seconds > self.timeout_sec) { + self.animate.* = false; + } +} + +fn calculateTimeout(self: *Matrix, _: *anyopaque) !?usize { + return self.frame_delay; +} + fn initBuffers(dots: []Dot, lines: []Line, width: usize, height: usize, random: Random) void { var y: usize = 0; while (y <= height) : (y += 1) { var x: usize = 0; while (x < width) : (x += 2) { - dots[y * width + x].value = -1; + dots[y * width + x].value = null; } } var x: usize = 0; while (x < width) : (x += 2) { var line = lines[x]; - const h: isize = @intCast(height); - line.space = @mod(random.int(i16), h) + 1; - line.length = @mod(random.int(i16), h - 3) + 3; - line.update = @mod(random.int(i16), 3) + 1; + line.space = @mod(random.int(u16), height) + 1; + line.length = @mod(random.int(u16), height - 3) + 3; + line.update = @mod(random.int(u16), 3) + 1; lines[x] = line; dots[width + x].value = ' '; diff --git a/src/auth.zig b/src/auth.zig index 077fd35..7d9c66c 100644 --- a/src/auth.zig +++ b/src/auth.zig @@ -1,41 +1,61 @@ const std = @import("std"); -const build_options = @import("build_options"); -const builtin = @import("builtin"); -const enums = @import("enums.zig"); -const interop = @import("interop.zig"); -const TerminalBuffer = @import("tui/TerminalBuffer.zig"); -const Session = @import("tui/components/Session.zig"); -const Text = @import("tui/components/Text.zig"); -const Config = @import("config/Config.zig"); -const Allocator = std.mem.Allocator; const Md5 = std.crypto.hash.Md5; +const builtin = @import("builtin"); +const build_options = @import("build_options"); + +const ly_core = @import("ly-ui").ly_core; +const interop = ly_core.interop; +const SharedError = ly_core.SharedError; +const LogFile = ly_core.LogFile; const utmp = interop.utmp; const Utmp = utmp.utmpx; -const SharedError = @import("SharedError.zig"); + +const Environment = @import("Environment.zig"); + +pub const AuthOptions = struct { + tty: u8, + service_name: [:0]const u8, + path: ?[]const u8, + session_log: ?[]const u8, + xauth_cmd: []const u8, + setup_cmd: []const u8, + login_cmd: ?[]const u8, + x_cmd: []const u8, + x_vt: ?u8, + session_pid: std.posix.pid_t, + use_kmscon_vt: bool, +}; var xorg_pid: std.posix.pid_t = 0; -pub fn xorgSignalHandler(i: c_int) callconv(.C) void { - if (xorg_pid > 0) _ = std.c.kill(xorg_pid, i); +pub fn xorgSignalHandler(sig: std.posix.SIG) callconv(.c) void { + if (xorg_pid > 0) _ = std.c.kill(xorg_pid, sig); } var child_pid: std.posix.pid_t = 0; -pub fn sessionSignalHandler(i: c_int) callconv(.C) void { - if (child_pid > 0) _ = std.c.kill(child_pid, i); +pub fn sessionSignalHandler(sig: std.posix.SIG) callconv(.c) void { + if (child_pid > 0) _ = std.c.kill(child_pid, sig); } -pub fn authenticate(config: Config, current_environment: Session.Environment, login: [:0]const u8, password: [:0]const u8) !void { +pub fn authenticate(allocator: std.mem.Allocator, io: std.Io, log_file: *LogFile, options: AuthOptions, current_environment: Environment, login: []const u8, password: []const u8) !void { var tty_buffer: [3]u8 = undefined; - const tty_str = try std.fmt.bufPrintZ(&tty_buffer, "{d}", .{config.tty}); + const tty_str = try std.fmt.bufPrint(&tty_buffer, "{d}", .{options.tty}); var pam_tty_buffer: [6]u8 = undefined; - const pam_tty_str = try std.fmt.bufPrintZ(&pam_tty_buffer, "tty{d}", .{config.tty}); + const pam_tty_str = try std.fmt.bufPrintZ(&pam_tty_buffer, "tty{d}", .{options.tty}); // Set the XDG environment variables - setXdgSessionEnv(current_environment.display_server); - try setXdgEnv(tty_str, current_environment.xdg_session_desktop orelse "", current_environment.xdg_desktop_names orelse ""); + try log_file.info(io, "auth/env", "setting xdg environment variables", .{}); + try setXdgEnv(allocator, tty_str, current_environment); // Open the PAM session - var credentials = [_:null]?[*:0]const u8{ login, password }; + try log_file.info(io, "auth/pam", "encoding credentials", .{}); + const login_z = try allocator.dupeZ(u8, login); + defer allocator.free(login_z); + + const password_z = try allocator.dupeZ(u8, password); + defer allocator.free(password_z); + + var credentials = [_:null]?[*:0]const u8{ login_z, password_z }; const conv = interop.pam.pam_conv{ .conv = loginConv, @@ -43,53 +63,71 @@ pub fn authenticate(config: Config, current_environment: Session.Environment, lo }; var handle: ?*interop.pam.pam_handle = undefined; - var status = interop.pam.pam_start(config.service_name, null, &conv, &handle); + try log_file.info(io, "auth/pam", "starting session", .{}); + var status = interop.pam.pam_start(options.service_name, null, &conv, &handle); if (status != interop.pam.PAM_SUCCESS) return pamDiagnose(status); defer _ = interop.pam.pam_end(handle, status); // Set PAM_TTY as the current TTY. This is required in case it isn't being set by another PAM module + try log_file.info(io, "auth/pam", "setting tty", .{}); status = interop.pam.pam_set_item(handle, interop.pam.PAM_TTY, pam_tty_str.ptr); if (status != interop.pam.PAM_SUCCESS) return pamDiagnose(status); // Do the PAM routine + try log_file.info(io, "auth/pam", "authenticating", .{}); status = interop.pam.pam_authenticate(handle, 0); if (status != interop.pam.PAM_SUCCESS) return pamDiagnose(status); + try log_file.info(io, "auth/pam", "validating account", .{}); status = interop.pam.pam_acct_mgmt(handle, 0); if (status != interop.pam.PAM_SUCCESS) return pamDiagnose(status); + try log_file.info(io, "auth/pam", "setting credentials", .{}); status = interop.pam.pam_setcred(handle, interop.pam.PAM_ESTABLISH_CRED); if (status != interop.pam.PAM_SUCCESS) return pamDiagnose(status); defer status = interop.pam.pam_setcred(handle, interop.pam.PAM_DELETE_CRED); + try log_file.info(io, "auth/pam", "opening session", .{}); status = interop.pam.pam_open_session(handle, 0); if (status != interop.pam.PAM_SUCCESS) return pamDiagnose(status); defer status = interop.pam.pam_close_session(handle, 0); - var pwd: *interop.pwd.passwd = undefined; + try log_file.info(io, "auth/passwd", "getting struct", .{}); + var user_entry: interop.UsernameEntry = undefined; { - defer interop.pwd.endpwent(); + defer interop.closePasswordDatabase(); // Get password structure from username - pwd = interop.pwd.getpwnam(login) orelse return error.GetPasswordNameFailed; + user_entry = interop.getUsernameEntry(login_z) orelse return error.GetPasswordNameFailed; } // Set user shell if it hasn't already been set - if (pwd.pw_shell == null) { - interop.unistd.setusershell(); - pwd.pw_shell = interop.unistd.getusershell(); - interop.unistd.endusershell(); - } + try log_file.info(io, "auth/passwd", "setting user shell", .{}); + if (user_entry.shell == null) interop.setUserShell(&user_entry); - var shared_err = try SharedError.init(); + // chown & chmod stdin (which is the TTY) + // https://github.com/mirror/busybox/blob/371fe9f71d445d18be28c82a2a6d82115c8af19d/loginutils/login.c#L558 + if (interop.isError(std.posix.system.fchown(std.posix.STDIN_FILENO, user_entry.uid, user_entry.gid))) return error.TtyChownFailed; + if (interop.isError(std.posix.system.fchmod(std.posix.STDIN_FILENO, 0o600))) return error.TtyChmodFailed; + + var shared_err = try SharedError.init(null, null); defer shared_err.deinit(); - child_pid = try std.posix.fork(); + log_file.deinit(io); + + child_pid = std.posix.system.fork(); if (child_pid == 0) { - startSession(config, pwd, handle, current_environment) catch |e| { + try log_file.reinit(io); + try log_file.info(io, "auth/sys", "starting session", .{}); + + startSession(log_file, allocator, io, options, tty_str, user_entry, handle, current_environment) catch |e| { shared_err.writeError(e); + + log_file.deinit(io); std.process.exit(1); }; + + log_file.deinit(io); std.process.exit(0); } @@ -98,115 +136,138 @@ pub fn authenticate(config: Config, current_environment: Session.Environment, lo { // If an error occurs here, we can send SIGTERM to the session errdefer cleanup: { - _ = std.posix.kill(child_pid, std.posix.SIG.TERM) catch break :cleanup; - _ = std.posix.waitpid(child_pid, 0); + std.posix.kill(child_pid, std.posix.SIG.TERM) catch break :cleanup; + var child_status: c_int = undefined; + _ = std.posix.system.waitpid(child_pid, &child_status, 0); } // If we receive SIGTERM, forward it to child_pid const act = std.posix.Sigaction{ .handler = .{ .handler = &sessionSignalHandler }, - .mask = std.posix.empty_sigset, + .mask = std.posix.sigemptyset(), .flags = 0, }; - try std.posix.sigaction(std.posix.SIG.TERM, &act, null); + std.posix.sigaction(std.posix.SIG.TERM, &act, null); - try addUtmpEntry(&entry, pwd.pw_name.?, child_pid); + try addUtmpEntry(io, &entry, user_entry.username.?, child_pid); } // Wait for the session to stop - _ = std.posix.waitpid(child_pid, 0); + var child_status: c_int = undefined; + _ = std.posix.system.waitpid(child_pid, &child_status, 0); + try log_file.reinit(io); + + try log_file.info(io, "auth/utmp", "removing utmp entry", .{}); removeUtmpEntry(&entry); if (shared_err.readError()) |err| return err; } fn startSession( - config: Config, - pwd: *interop.pwd.passwd, + log_file: *LogFile, + allocator: std.mem.Allocator, + io: std.Io, + options: AuthOptions, + tty_str: []u8, + user_entry: interop.UsernameEntry, handle: ?*interop.pam.pam_handle, - current_environment: Session.Environment, + current_environment: Environment, ) !void { - if (builtin.os.tag == .freebsd) { - // FreeBSD has initgroups() in unistd - const status = interop.unistd.initgroups(pwd.pw_name, pwd.pw_gid); - if (status != 0) return error.GroupInitializationFailed; - - // FreeBSD sets the GID and UID with setusercontext() - const result = interop.pwd.setusercontext(null, pwd, pwd.pw_uid, interop.pwd.LOGIN_SETALL); - if (result != 0) return error.SetUserUidFailed; - } else { - const status = interop.grp.initgroups(pwd.pw_name, pwd.pw_gid); - if (status != 0) return error.GroupInitializationFailed; - - std.posix.setgid(pwd.pw_gid) catch return error.SetUserGidFailed; - std.posix.setuid(pwd.pw_uid) catch return error.SetUserUidFailed; - } + // Set the user's GID & PID + try log_file.info(io, "auth/passwd", "setting user context", .{}); + try interop.setUserContext(allocator, user_entry); // Set up the environment - try initEnv(pwd, config.path); + try log_file.info(io, "auth/env", "setting environment variables", .{}); + try initEnv(allocator, user_entry, options.path); + + // Reset the XDG environment variables + try log_file.info(io, "auth/env", "resetting xdg environment variables", .{}); + try setXdgEnv(allocator, tty_str, current_environment); + try setXdgRuntimeDir(allocator, io); // Set the PAM variables const pam_env_vars: ?[*:null]?[*:0]u8 = interop.pam.pam_getenvlist(handle); if (pam_env_vars == null) return error.GetEnvListFailed; const env_list = std.mem.span(pam_env_vars.?); - for (env_list) |env_var| _ = interop.stdlib.putenv(env_var); + for (env_list) |maybe_env_var| { + if (maybe_env_var) |env_var| { + const env_var_slice = std.mem.span(env_var); + try log_file.info(io, "auth/env", "setting pam environment variable: {s}", .{env_var_slice}); + try interop.putEnvironmentVariable(env_var_slice); + } + } + + const home_z = try allocator.dupeZ(u8, user_entry.home.?); + defer allocator.free(home_z); // Change to the user's home directory - std.posix.chdirZ(pwd.pw_dir.?) catch return error.ChangeDirectoryFailed; + try log_file.info(io, "auth/sys", "changing cwd to user home", .{}); + if (std.posix.system.chdir(home_z.ptr) < 0) return error.ChangeDirectoryFailed; + + // Signal to the session process to give up control on the TTY + try log_file.info(io, "auth/sys", "releasing tty", .{}); + std.posix.kill(options.session_pid, std.posix.SIG.INT) catch return error.TtyControlTransferFailed; // Execute what the user requested switch (current_environment.display_server) { - .wayland => try executeWaylandCmd(pwd.pw_shell.?, config, current_environment.cmd), - .shell => try executeShellCmd(pwd.pw_shell.?, config), + .wayland, .shell, .custom => try executeCmd(log_file, allocator, io, user_entry.shell.?, options, current_environment.is_terminal, current_environment.cmd), .xinitrc, .x11 => if (build_options.enable_x11_support) { var vt_buf: [5]u8 = undefined; - const vt = try std.fmt.bufPrint(&vt_buf, "vt{d}", .{config.tty}); - try executeX11Cmd(pwd.pw_shell.?, pwd.pw_dir.?, config, current_environment.cmd, vt); + const vt = try std.fmt.bufPrint(&vt_buf, "vt{d}", .{options.x_vt orelse options.tty}); + + try log_file.info(io, "auth/x11", "setting vt to {s}", .{vt}); + try executeX11Cmd(log_file, allocator, io, user_entry.shell.?, user_entry.home.?, options, current_environment.cmd orelse "", vt); }, } } -fn initEnv(pwd: *interop.pwd.passwd, path_env: ?[:0]const u8) !void { - _ = interop.stdlib.setenv("HOME", pwd.pw_dir, 1); - _ = interop.stdlib.setenv("PWD", pwd.pw_dir, 1); - _ = interop.stdlib.setenv("SHELL", pwd.pw_shell, 1); - _ = interop.stdlib.setenv("USER", pwd.pw_name, 1); - _ = interop.stdlib.setenv("LOGNAME", pwd.pw_name, 1); +fn initEnv(allocator: std.mem.Allocator, entry: interop.UsernameEntry, path_env: ?[]const u8) !void { + if (entry.home) |home| { + try interop.setEnvironmentVariable(allocator, "HOME", home, true); + try interop.setEnvironmentVariable(allocator, "PWD", home, true); + } else return error.NoHomeDirectory; + + try interop.setEnvironmentVariable(allocator, "SHELL", entry.shell.?, true); + try interop.setEnvironmentVariable(allocator, "USER", entry.username.?, true); + try interop.setEnvironmentVariable(allocator, "LOGNAME", entry.username.?, true); if (path_env) |path| { - const status = interop.stdlib.setenv("PATH", path, 1); - if (status != 0) return error.SetPathFailed; + interop.setEnvironmentVariable(allocator, "PATH", path, true) catch return error.SetPathFailed; } } -fn setXdgSessionEnv(display_server: enums.DisplayServer) void { - _ = interop.stdlib.setenv("XDG_SESSION_TYPE", switch (display_server) { +fn setXdgEnv(allocator: std.mem.Allocator, tty_str: []u8, environment: Environment) !void { + try interop.setEnvironmentVariable(allocator, "XDG_SESSION_TYPE", switch (environment.display_server) { .wayland => "wayland", .shell => "tty", .xinitrc, .x11 => "x11", - }, 0); + .custom => if (environment.is_terminal) "tty" else "unspecified", + }, false); + + if (environment.xdg_desktop_names) |xdg_desktop_names| try interop.setEnvironmentVariable(allocator, "XDG_CURRENT_DESKTOP", xdg_desktop_names, false); + try interop.setEnvironmentVariable(allocator, "XDG_SESSION_CLASS", "user", false); + try interop.setEnvironmentVariable(allocator, "XDG_SESSION_ID", "1", false); + if (environment.xdg_session_desktop) |desktop_name| try interop.setEnvironmentVariable(allocator, "XDG_SESSION_DESKTOP", desktop_name, false); + try interop.setEnvironmentVariable(allocator, "XDG_SEAT", "seat0", false); + try interop.setEnvironmentVariable(allocator, "XDG_VTNR", tty_str, false); } -fn setXdgEnv(tty_str: [:0]u8, desktop_name: [:0]const u8, xdg_desktop_names: [:0]const u8) !void { - // The "/run/user/%d" directory is not available on FreeBSD. It is much - // better to stick to the defaults and let applications using - // XDG_RUNTIME_DIR to fall back to directories inside user's home - // directory. - if (builtin.os.tag != .freebsd) { - const uid = interop.unistd.getuid(); - var uid_buffer: [10 + @sizeOf(u32) + 1]u8 = undefined; - const uid_str = try std.fmt.bufPrintZ(&uid_buffer, "/run/user/{d}", .{uid}); +fn setXdgRuntimeDir(allocator: std.mem.Allocator, io: std.Io) !void { + // The "/run/user/%d" directory is not available on some operating systems, + // like FreeBSD and Alpine + const uid = std.posix.system.getuid(); + var uid_buffer: [32]u8 = undefined; // No UID can be larger than this + const uid_str = try std.fmt.bufPrint(&uid_buffer, "/run/user/{d}", .{uid}); - _ = interop.stdlib.setenv("XDG_RUNTIME_DIR", uid_str, 0); - } + var xdg_dir = std.Io.Dir.openDirAbsolute(io, uid_str, .{}) catch |err| { + if (err == error.FileNotFound) return; + return err; + }; + xdg_dir.close(io); - _ = interop.stdlib.setenv("XDG_CURRENT_DESKTOP", xdg_desktop_names, 0); - _ = interop.stdlib.setenv("XDG_SESSION_CLASS", "user", 0); - _ = interop.stdlib.setenv("XDG_SESSION_ID", "1", 0); - _ = interop.stdlib.setenv("XDG_SESSION_DESKTOP", desktop_name, 0); - _ = interop.stdlib.setenv("XDG_SEAT", "seat0", 0); - _ = interop.stdlib.setenv("XDG_VTNR", tty_str, 0); + try interop.setEnvironmentVariable(allocator, "XDG_RUNTIME_DIR", uid_str, false); } fn loginConv( @@ -214,7 +275,7 @@ fn loginConv( msg: ?[*]?*const interop.pam.pam_message, resp: ?*?[*]interop.pam.pam_response, appdata_ptr: ?*anyopaque, -) callconv(.C) c_int { +) callconv(.c) c_int { const message_count: u32 = @intCast(num_msg); const messages = msg.?; @@ -258,8 +319,8 @@ fn loginConv( if (status != interop.pam.PAM_SUCCESS) { // Memory is freed by pam otherwise allocator.free(response); - if (username != null) allocator.free(username.?); - if (password != null) allocator.free(password.?); + if (username) |str| allocator.free(str); + if (password) |str| allocator.free(str); } else { resp.?.* = response.ptr; } @@ -271,60 +332,64 @@ fn getFreeDisplay() !u8 { var buf: [15]u8 = undefined; var i: u8 = 0; while (i < 200) : (i += 1) { - const xlock = try std.fmt.bufPrint(&buf, "/tmp/.X{d}-lock", .{i}); - std.posix.access(xlock, std.posix.F_OK) catch break; + const xlock = try std.fmt.bufPrintZ(&buf, "/tmp/.X{d}-lock", .{i}); + if (interop.isError(std.posix.system.access(xlock.ptr, std.posix.F_OK))) break; } return i; } -fn getXPid(display_num: u8) !i32 { +fn getXPid(io: std.Io, display_num: u8) !i32 { var buf: [15]u8 = undefined; const file_name = try std.fmt.bufPrint(&buf, "/tmp/.X{d}-lock", .{display_num}); - const file = try std.fs.openFileAbsolute(file_name, .{}); - defer file.close(); + const file = try std.Io.Dir.openFileAbsolute(io, file_name, .{}); + defer file.close(io); - var file_buf: [20]u8 = undefined; - var fbs = std.io.fixedBufferStream(&file_buf); + var file_buffer: [32]u8 = undefined; + var file_reader = file.reader(io, &file_buffer); + var reader = &file_reader.interface; - _ = try file.reader().streamUntilDelimiter(fbs.writer(), '\n', 20); - const line = fbs.getWritten(); + var buffer: [20]u8 = undefined; + var writer = std.Io.Writer.fixed(&buffer); - return std.fmt.parseInt(i32, std.mem.trim(u8, line, " "), 10); + const written = try reader.streamDelimiter(&writer, '\n'); + return std.fmt.parseInt(i32, std.mem.trim(u8, buffer[0..written], " "), 10); } -fn createXauthFile(pwd: [:0]const u8) ![:0]const u8 { +fn createXauthFile(log_file: *LogFile, io: std.Io, pwd: []const u8, buffer: []u8) ![]const u8 { var xauth_buf: [100]u8 = undefined; - var xauth_dir: [:0]const u8 = undefined; - const xdg_rt_dir = std.posix.getenv("XDG_RUNTIME_DIR"); + var xauth_dir: []const u8 = undefined; + const xdg_rt_dir = std.posix.system.getenv("XDG_RUNTIME_DIR"); var xauth_file: []const u8 = "lyxauth"; - if (xdg_rt_dir == null) { - const xdg_cfg_home = std.posix.getenv("XDG_CONFIG_HOME"); - var sb: std.c.Stat = undefined; - if (xdg_cfg_home == null) { - xauth_dir = try std.fmt.bufPrintZ(&xauth_buf, "{s}/.config", .{pwd}); - _ = std.c.stat(xauth_dir, &sb); - const mode = sb.mode & std.posix.S.IFMT; - if (mode == std.posix.S.IFDIR) { - xauth_dir = try std.fmt.bufPrintZ(&xauth_buf, "{s}/ly", .{xauth_dir}); - } else { + if (xdg_rt_dir == null) no_rt_dir: { + const xdg_cfg_home = std.posix.system.getenv("XDG_CONFIG_HOME"); + if (xdg_cfg_home == null) no_cfg_home: { + xauth_dir = try std.fmt.bufPrint(&xauth_buf, "{s}/.config", .{pwd}); + + var dir = std.Io.Dir.cwd().openDir(io, xauth_dir, .{}) catch { + // xauth_dir isn't a directory xauth_dir = pwd; xauth_file = ".lyxauth"; - } + break :no_cfg_home; + }; + dir.close(io); + + // xauth_dir is a directory, use it to store Xauthority + xauth_dir = try std.fmt.bufPrint(&xauth_buf, "{s}/.config/ly", .{pwd}); } else { - xauth_dir = try std.fmt.bufPrintZ(&xauth_buf, "{s}/ly", .{xdg_cfg_home.?}); + xauth_dir = try std.fmt.bufPrint(&xauth_buf, "{s}/ly", .{std.mem.span(xdg_cfg_home.?)}); } - _ = std.c.stat(xauth_dir, &sb); - const mode = sb.mode & std.posix.S.IFMT; - if (mode != std.posix.S.IFDIR) { - std.posix.mkdir(xauth_dir, 777) catch { - xauth_dir = pwd; - xauth_file = ".lyxauth"; - }; - } + const file = std.Io.Dir.cwd().openFile(io, xauth_dir, .{}) catch break :no_rt_dir; + file.close(io); + + // xauth_dir is a file, create the parent directory + std.Io.Dir.createDirAbsolute(io, xauth_dir, .fromMode(777)) catch { + xauth_dir = pwd; + xauth_file = ".lyxauth"; + }; } else { - xauth_dir = xdg_rt_dir.?; + xauth_dir = std.mem.span(xdg_rt_dir.?); } // Trim trailing slashes @@ -332,17 +397,21 @@ fn createXauthFile(pwd: [:0]const u8) ![:0]const u8 { while (xauth_dir[i] == '/') i -= 1; const trimmed_xauth_dir = xauth_dir[0 .. i + 1]; - var buf: [256]u8 = undefined; - const xauthority: [:0]u8 = try std.fmt.bufPrintZ(&buf, "{s}/{s}", .{ trimmed_xauth_dir, xauth_file }); - const file = try std.fs.createFileAbsoluteZ(xauthority, .{}); - file.close(); + const xauthority: []u8 = try std.fmt.bufPrint(buffer, "{s}/{s}", .{ trimmed_xauth_dir, xauth_file }); + + try std.Io.Dir.cwd().createDirPath(io, trimmed_xauth_dir); + + try log_file.info(io, "auth/x11", "creating xauth file: {s}", .{xauthority}); + + const file = try std.Io.Dir.createFileAbsolute(io, xauthority, .{}); + file.close(io); return xauthority; } -fn mcookie() [Md5.digest_length * 2]u8 { +fn mcookie(io: std.Io) [Md5.digest_length * 2]u8 { var buf: [4096]u8 = undefined; - std.crypto.random.bytes(&buf); + io.random(&buf); var out: [Md5.digest_length]u8 = undefined; Md5.hash(&buf, &out, .{}); @@ -350,128 +419,205 @@ fn mcookie() [Md5.digest_length * 2]u8 { return std.fmt.bytesToHex(&out, .lower); } -fn xauth(display_name: [:0]u8, shell: [*:0]const u8, pw_dir: [*:0]const u8, config: Config) !void { - var pwd_buf: [100]u8 = undefined; - const pwd = try std.fmt.bufPrintZ(&pwd_buf, "{s}", .{pw_dir}); +fn xauth(log_file: *LogFile, allocator: std.mem.Allocator, io: std.Io, display_name: []u8, shell: [*:0]const u8, home: []const u8, xauth_buffer: []u8, options: AuthOptions) ![]const u8 { + const xauthority = try createXauthFile(log_file, io, home, xauth_buffer); + try interop.setEnvironmentVariable(allocator, "XAUTHORITY", xauthority, true); + try interop.setEnvironmentVariable(allocator, "DISPLAY", display_name, true); - const xauthority = try createXauthFile(pwd); - _ = interop.stdlib.setenv("XAUTHORITY", xauthority, 1); - _ = interop.stdlib.setenv("DISPLAY", display_name, 1); + const magic_cookie = mcookie(io); - const magic_cookie = mcookie(); + log_file.deinit(io); - const pid = try std.posix.fork(); + const pid = std.posix.system.fork(); if (pid == 0) { - const log_file = try redirectStandardStreams(config.session_log, true); - defer log_file.close(); + try log_file.reinit(io); var cmd_buffer: [1024]u8 = undefined; - const cmd_str = std.fmt.bufPrintZ(&cmd_buffer, "{s} add {s} . {s}", .{ config.xauth_cmd, display_name, magic_cookie }) catch std.process.exit(1); + const cmd_str = std.fmt.bufPrintZ(&cmd_buffer, "{s} add {s} . {s}", .{ options.xauth_cmd, display_name, magic_cookie }) catch std.process.exit(1); + + try log_file.info(io, "auth/x11", "executing: {s} -c {s}", .{ shell, cmd_str }); const args = [_:null]?[*:0]const u8{ shell, "-c", cmd_str }; - std.posix.execveZ(shell, &args, std.c.environ) catch {}; + _ = std.posix.system.execve(shell, &args, std.c.environ); + + log_file.deinit(io); std.process.exit(1); } - const status = std.posix.waitpid(pid, 0); - if (status.status != 0) return error.XauthFailed; + var status: c_int = undefined; + const result = std.posix.system.waitpid(pid, &status, 0); + + try log_file.reinit(io); + if (interop.isError(result) or status != 0) { + try log_file.err( + io, + "auth/x11", + "xauth command failed with status: {d}", + .{status}, + ); + return error.XauthFailed; + } + + return xauthority; } -fn executeShellCmd(shell: [*:0]const u8, config: Config) !void { - // We don't want to redirect stdout and stderr in a shell session +fn executeX11Cmd(log_file: *LogFile, allocator: std.mem.Allocator, io: std.Io, shell: []const u8, home: []const u8, options: AuthOptions, desktop_cmd: []const u8, vt: []const u8) !void { + var xauth_buffer: [256]u8 = undefined; - var cmd_buffer: [1024]u8 = undefined; - const cmd_str = try std.fmt.bufPrintZ(&cmd_buffer, "{s} {s} {s}", .{ config.setup_cmd, config.login_cmd orelse "", shell }); - const args = [_:null]?[*:0]const u8{ shell, "-c", cmd_str }; - return std.posix.execveZ(shell, &args, std.c.environ); -} - -fn executeWaylandCmd(shell: [*:0]const u8, config: Config, desktop_cmd: []const u8) !void { - const log_file = try redirectStandardStreams(config.session_log, true); - defer log_file.close(); - - var cmd_buffer: [1024]u8 = undefined; - const cmd_str = try std.fmt.bufPrintZ(&cmd_buffer, "{s} {s} {s}", .{ config.setup_cmd, config.login_cmd orelse "", desktop_cmd }); - const args = [_:null]?[*:0]const u8{ shell, "-c", cmd_str }; - return std.posix.execveZ(shell, &args, std.c.environ); -} - -fn executeX11Cmd(shell: [*:0]const u8, pw_dir: [*:0]const u8, config: Config, desktop_cmd: []const u8, vt: []const u8) !void { + try log_file.info(io, "auth/x11", "getting free display", .{}); const display_num = try getFreeDisplay(); - var buf: [5]u8 = undefined; - const display_name = try std.fmt.bufPrintZ(&buf, ":{d}", .{display_num}); - try xauth(display_name, shell, pw_dir, config); + var buf: [4]u8 = undefined; + const display_name = try std.fmt.bufPrint(&buf, ":{d}", .{display_num}); + try log_file.info(io, "auth/x11", "got free display: {d}", .{display_num}); - const pid = try std.posix.fork(); + const shell_z = try allocator.dupeZ(u8, shell); + defer allocator.free(shell_z); + + try log_file.info(io, "auth/x11", "creating xauth file", .{}); + const xauthority = try xauth(log_file, allocator, io, display_name, shell_z, home, &xauth_buffer, options); + + try log_file.info(io, "auth/x11", "starting x server", .{}); + const pid = std.posix.system.fork(); if (pid == 0) { var cmd_buffer: [1024]u8 = undefined; - const cmd_str = std.fmt.bufPrintZ(&cmd_buffer, "{s} {s} {s} >{s} 2>&1", .{ config.x_cmd, display_name, vt, config.session_log }) catch std.process.exit(1); - const args = [_:null]?[*:0]const u8{ shell, "-c", cmd_str }; - std.posix.execveZ(shell, &args, std.c.environ) catch {}; + const cmd_str = std.fmt.bufPrintZ(&cmd_buffer, "{s} {s} {s} -auth {s}", .{ options.x_cmd, display_name, vt, xauthority }) catch std.process.exit(1); + try log_file.info(io, "auth/x11", "executing: {s} -c {s} -auth {s}", .{ shell, cmd_str, xauthority }); + + const args = [_:null]?[*:0]const u8{ shell_z, "-c", cmd_str }; + _ = std.posix.system.execve(shell_z, &args, std.c.environ); std.process.exit(1); } - var ok: c_int = undefined; + try log_file.info(io, "auth/x11", "waiting for xcb connection", .{}); + var ok: c_int = -1; var xcb: ?*interop.xcb.xcb_connection_t = null; while (ok != 0) { xcb = interop.xcb.xcb_connect(null, null); ok = interop.xcb.xcb_connection_has_error(xcb); - std.posix.kill(pid, 0) catch |e| { + std.posix.kill(pid, @enumFromInt(0)) catch |e| { if (e == error.ProcessNotFound and ok != 0) return error.XcbConnectionFailed; }; } // X Server detaches from the process. // PID can be fetched from /tmp/X{d}.lock - const x_pid = try getXPid(display_num); + try log_file.info(io, "auth/x11", "getting x server pid", .{}); + const x_pid = try getXPid(io, display_num); + try log_file.info(io, "auth/x11", "got x server pid: {d}", .{x_pid}); - xorg_pid = try std.posix.fork(); + try log_file.info(io, "auth/x11", "launching environment", .{}); + xorg_pid = std.posix.system.fork(); if (xorg_pid == 0) { var cmd_buffer: [1024]u8 = undefined; - const cmd_str = std.fmt.bufPrintZ(&cmd_buffer, "{s} {s} {s} >{s} 2>&1", .{ config.setup_cmd, config.login_cmd orelse "", desktop_cmd, config.session_log }) catch std.process.exit(1); - const args = [_:null]?[*:0]const u8{ shell, "-c", cmd_str }; - std.posix.execveZ(shell, &args, std.c.environ) catch {}; + const cmd_str = std.fmt.bufPrintZ(&cmd_buffer, "{s} {s} {s} {s}", .{ if (options.use_kmscon_vt) "kmscon-launch-gui" else "", options.setup_cmd, options.login_cmd orelse "", desktop_cmd }) catch std.process.exit(1); + try log_file.info(io, "auth/x11", "executing: {s} -c {s}", .{ shell, cmd_str }); + + const args = [_:null]?[*:0]const u8{ shell_z, "-c", cmd_str }; + _ = std.posix.system.execve(shell_z, &args, std.c.environ); std.process.exit(1); } // If we receive SIGTERM, clean up by killing the xorg_pid process const act = std.posix.Sigaction{ .handler = .{ .handler = &xorgSignalHandler }, - .mask = std.posix.empty_sigset, + .mask = std.posix.sigemptyset(), .flags = 0, }; - try std.posix.sigaction(std.posix.SIG.TERM, &act, null); + std.posix.sigaction(std.posix.SIG.TERM, &act, null); - _ = std.posix.waitpid(xorg_pid, 0); + var xorg_status: c_int = undefined; + _ = std.posix.system.waitpid(xorg_pid, &xorg_status, 0); + + try log_file.info(io, "auth/x11", "disconnecting xcb", .{}); interop.xcb.xcb_disconnect(xcb); - std.posix.kill(x_pid, 0) catch return; + // TODO: Find a more robust way to ensure that X has been terminated (pidfds?) std.posix.kill(x_pid, std.posix.SIG.TERM) catch {}; + io.sleep(.fromSeconds(1), .real) catch {}; // Wait 1 second before sending SIGKILL + std.posix.kill(x_pid, std.posix.SIG.KILL) catch return; - var status: c_int = 0; - _ = std.c.waitpid(x_pid, &status, 0); + var x_status: c_int = undefined; + _ = std.posix.system.waitpid(x_pid, &x_status, 0); } -fn redirectStandardStreams(session_log: []const u8, create: bool) !std.fs.File { - const log_file = if (create) (try std.fs.cwd().createFile(session_log, .{ .mode = 0o666 })) else (try std.fs.cwd().openFile(session_log, .{ .mode = .read_write })); +fn executeCmd(global_log_file: *LogFile, allocator: std.mem.Allocator, io: std.Io, shell: []const u8, options: AuthOptions, is_terminal: bool, exec_cmd: ?[]const u8) !void { + try global_log_file.info(io, "auth/sys", "launching wayland/shell/custom session", .{}); - try std.posix.dup2(std.posix.STDOUT_FILENO, std.posix.STDERR_FILENO); - try std.posix.dup2(log_file.handle, std.posix.STDOUT_FILENO); + var maybe_log_file: ?std.Io.File = null; + if (!is_terminal) redirect_streams: { + if (options.use_kmscon_vt) { + try global_log_file.err(io, "auth/sys", "cannot redirect stdio & stderr with kmscon", .{}); + break :redirect_streams; + } + + // For custom desktop entries, the "Terminal" value here determines if + // we redirect standard output & error or not. That is, we redirect only + // if it's equal to false (so if it's not running in a TTY). + if (options.session_log) |log_path| { + try global_log_file.info(io, "auth/sys", "setting up stdio & stderr redirection", .{}); + maybe_log_file = try redirectStandardStreams(global_log_file, io, log_path, true); + } + } + defer if (maybe_log_file) |log_file| log_file.close(io); + + const shell_z = try allocator.dupeZ(u8, shell); + defer allocator.free(shell_z); + + var cmd_buffer: [1024]u8 = undefined; + const cmd_str = try std.fmt.bufPrintZ(&cmd_buffer, "{s} {s} {s} {s}", .{ if (!is_terminal and options.use_kmscon_vt) "kmscon-launch-gui" else "", options.setup_cmd, options.login_cmd orelse "", exec_cmd orelse shell }); + + try global_log_file.info(io, "auth/sys", "executing: {s} -c {s}", .{ shell, cmd_str }); + const args = [_:null]?[*:0]const u8{ shell_z, "-c", cmd_str }; + _ = std.posix.system.execve(shell_z, &args, std.c.environ); + return error.CmdExecveFailed; +} + +fn redirectStandardStreams(global_log_file: *LogFile, io: std.Io, session_log: []const u8, create: bool) !std.Io.File { + create_session_log_dir: { + const session_log_dir = std.Io.Dir.path.dirname(session_log) orelse break :create_session_log_dir; + + var buffer = std.mem.zeroes([std.Io.Dir.max_path_bytes]u8); + const len = std.Io.Dir.cwd().realPathFile(io, session_log_dir, &buffer) catch |err| { + try global_log_file.err(io, "auth/sys", "failed to resolve path for session log file directory: {s}", .{@errorName(err)}); + return err; + }; + const resolved_path = buffer[0..len]; + + std.Io.Dir.cwd().createDirPath(io, resolved_path) catch |err| { + try global_log_file.err(io, "auth/sys", "failed to create session log file directory: {s}", .{@errorName(err)}); + return err; + }; + } + + const log_file = if (create) (std.Io.Dir.cwd().createFile(io, session_log, .{ .permissions = .fromMode(0o666) }) catch |err| { + try global_log_file.err(io, "auth/sys", "failed to create new session log file: {s}", .{@errorName(err)}); + return err; + }) else (std.Io.Dir.cwd().openFile(io, session_log, .{ .mode = .read_write }) catch |err| { + try global_log_file.err(io, "auth/sys", "failed to open existing session log file: {s}", .{@errorName(err)}); + return err; + }); + + if (interop.isError(std.posix.system.dup2(std.posix.STDOUT_FILENO, std.posix.STDERR_FILENO))) return error.StdoutDup2Failed; + if (interop.isError(std.posix.system.dup2(log_file.handle, std.posix.STDOUT_FILENO))) return error.LogFileDup2Failed; return log_file; } -fn addUtmpEntry(entry: *Utmp, username: [*:0]const u8, pid: c_int) !void { +fn addUtmpEntry(io: std.Io, entry: *Utmp, username: []const u8, pid: c_int) !void { entry.ut_type = utmp.USER_PROCESS; entry.ut_pid = pid; - var buf: [4096]u8 = undefined; - const ttyname = try std.os.getFdPath(std.posix.STDIN_FILENO, &buf); + var buf: [std.Io.Dir.max_path_bytes]u8 = undefined; + const length = try std.Io.File.stdin().realPath(io, &buf); + const tty_path = buf[0..length]; + // Get the TTY name (i.e. without the /dev/ prefix) var ttyname_buf: [@sizeOf(@TypeOf(entry.ut_line))]u8 = undefined; - _ = try std.fmt.bufPrintZ(&ttyname_buf, "{s}", .{ttyname["/dev/".len..]}); + _ = try std.fmt.bufPrintZ(&ttyname_buf, "{s}", .{tty_path["/dev/".len..]}); entry.ut_line = ttyname_buf; - entry.ut_id = ttyname_buf["tty".len..7].*; + // Get the TTY ID (i.e. without the tty prefix) and truncate it to the size + // of ut_id if necessary + entry.ut_id = ttyname_buf["tty".len..(@sizeOf(@TypeOf(entry.ut_id)) + "tty".len)].*; var username_buf: [@sizeOf(@TypeOf(entry.ut_user))]u8 = undefined; _ = try std.fmt.bufPrintZ(&username_buf, "{s}", .{username}); @@ -482,14 +628,17 @@ fn addUtmpEntry(entry: *Utmp, username: [*:0]const u8, pid: c_int) !void { host[0] = 0; entry.ut_host = host; - var tv: interop.system_time.timeval = undefined; - _ = interop.system_time.gettimeofday(&tv, null); + const time = try interop.getTimeOfDay(); entry.ut_tv = .{ - .tv_sec = @intCast(tv.tv_sec), - .tv_usec = @intCast(tv.tv_usec), + .tv_sec = @intCast(time.seconds), + .tv_usec = @intCast(time.microseconds), }; - entry.ut_addr_v6[0] = 0; + + // FreeBSD doesn't have this field + if (builtin.os.tag == .linux) { + entry.ut_addr_v6[0] = 0; + } utmp.setutxent(); _ = utmp.pututxline(entry); diff --git a/src/bigclock.zig b/src/bigclock.zig deleted file mode 100644 index 6f17b72..0000000 --- a/src/bigclock.zig +++ /dev/null @@ -1,58 +0,0 @@ -const std = @import("std"); -const interop = @import("interop.zig"); -const utils = @import("tui/utils.zig"); -const enums = @import("enums.zig"); -const Lang = @import("bigclock/Lang.zig"); -const en = @import("bigclock/en.zig"); -const fa = @import("bigclock/fa.zig"); - -const termbox = interop.termbox; -const Bigclock = enums.Bigclock; -pub const WIDTH = Lang.WIDTH; -pub const HEIGHT = Lang.HEIGHT; -pub const SIZE = Lang.SIZE; - -pub fn clockCell(animate: bool, char: u8, fg: u16, bg: u16, bigclock: Bigclock) [SIZE]utils.Cell { - var cells: [SIZE]utils.Cell = undefined; - - var tv: interop.system_time.timeval = undefined; - _ = interop.system_time.gettimeofday(&tv, null); - - const clock_chars = toBigNumber(if (animate and char == ':' and @divTrunc(tv.tv_usec, 500000) != 0) ' ' else char, bigclock); - for (0..cells.len) |i| cells[i] = utils.initCell(clock_chars[i], fg, bg); - - return cells; -} - -pub fn alphaBlit(x: usize, y: usize, tb_width: usize, tb_height: usize, cells: [SIZE]utils.Cell) void { - if (x + WIDTH >= tb_width or y + HEIGHT >= tb_height) return; - - for (0..HEIGHT) |yy| { - for (0..WIDTH) |xx| { - const cell = cells[yy * WIDTH + xx]; - if (cell.ch != 0) utils.putCell(x + xx, y + yy, cell); - } - } -} - -fn toBigNumber(char: u8, bigclock: Bigclock) []const u21 { - const locale_chars = switch (bigclock) { - .fa => fa.locale_chars, - .en => en.locale_chars, - .none => unreachable, - }; - 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, - ':' => &locale_chars.S, - else => &locale_chars.E, - }; -} diff --git a/src/bigclock/Lang.zig b/src/bigclock/Lang.zig deleted file mode 100644 index 4229776..0000000 --- a/src/bigclock/Lang.zig +++ /dev/null @@ -1,23 +0,0 @@ -const builtin = @import("builtin"); - -pub const WIDTH = 5; -pub const HEIGHT = 5; -pub const SIZE = WIDTH * HEIGHT; - -pub const X: u32 = if (builtin.os.tag == .linux or builtin.os.tag.isBSD()) 0x2593 else '#'; -pub const O: u32 = 0; - -pub const LocaleChars = struct { - ZERO: [SIZE]u21, - ONE: [SIZE]u21, - TWO: [SIZE]u21, - THREE: [SIZE]u21, - FOUR: [SIZE]u21, - FIVE: [SIZE]u21, - SIX: [SIZE]u21, - SEVEN: [SIZE]u21, - EIGHT: [SIZE]u21, - NINE: [SIZE]u21, - S: [SIZE]u21, - E: [SIZE]u21, -}; \ No newline at end of file diff --git a/src/components/InfoLine.zig b/src/components/InfoLine.zig new file mode 100644 index 0000000..45b5da3 --- /dev/null +++ b/src/components/InfoLine.zig @@ -0,0 +1,118 @@ +const std = @import("std"); +const Allocator = std.mem.Allocator; + +const ly_ui = @import("ly-ui"); +const keyboard = ly_ui.keyboard; +const TerminalBuffer = ly_ui.TerminalBuffer; +const Widget = ly_ui.Widget; +const CyclableLabel = ly_ui.CyclableLabel; + +const MessageLabel = CyclableLabel(Message, Message); + +const InfoLine = @This(); + +const Message = struct { + width: usize, + text: []const u8, + bg: u32, + fg: u32, +}; + +instance: ?Widget = null, +label: *MessageLabel, + +pub fn init( + allocator: Allocator, + io: std.Io, + buffer: *TerminalBuffer, + width: usize, + arrow_fg: u32, + arrow_bg: u32, +) !InfoLine { + return .{ + .instance = null, + .label = try MessageLabel.init( + allocator, + io, + buffer, + drawItem, + null, + null, + width, + true, + arrow_fg, + arrow_bg, + ), + }; +} + +pub fn deinit(self: *InfoLine) void { + self.label.deinit(); +} + +pub fn widget(self: *InfoLine) *Widget { + if (self.instance) |*instance| return instance; + self.instance = Widget.init( + "InfoLine", + self.label.keybinds, + self, + deinit, + null, + draw, + null, + handle, + null, + ); + return &self.instance.?; +} + +pub fn addMessage(self: *InfoLine, text: []const u8, bg: u32, fg: u32) !void { + if (text.len == 0) return; + + try self.label.addItem(.{ + .width = TerminalBuffer.strWidth(text), + .text = text, + .bg = bg, + .fg = fg, + }); +} + +pub fn clearRendered(self: InfoLine, allocator: Allocator) !void { + // Draw over the area + const spaces = try allocator.alloc(u8, self.label.width - 2); + defer allocator.free(spaces); + + @memset(spaces, ' '); + + try TerminalBuffer.drawText( + spaces, + self.label.component_pos.x + 2, + self.label.component_pos.y, + TerminalBuffer.Color.DEFAULT, + TerminalBuffer.Color.DEFAULT, + ); +} + +fn draw(self: *InfoLine) void { + self.label.draw(); +} + +fn handle(self: *InfoLine, maybe_key: ?keyboard.Key) !void { + try self.label.handle(maybe_key); +} + +fn drawItem(label: *MessageLabel, message: Message, x: usize, y: usize, width: usize) void { + if (message.width == 0) return; + + const x_offset = if (label.text_in_center and width >= message.width) (width - message.width) / 2 else 0; + + label.cursor = message.width + x_offset; + TerminalBuffer.drawConfinedText( + message.text, + x + x_offset, + y, + width, + message.fg, + message.bg, + ) catch {}; +} diff --git a/src/components/Session.zig b/src/components/Session.zig new file mode 100644 index 0000000..fe8c498 --- /dev/null +++ b/src/components/Session.zig @@ -0,0 +1,123 @@ +const std = @import("std"); +const Allocator = std.mem.Allocator; + +const ly_ui = @import("ly-ui"); +const keyboard = ly_ui.keyboard; +const TerminalBuffer = ly_ui.TerminalBuffer; +const Widget = ly_ui.Widget; +const CyclableLabel = ly_ui.CyclableLabel; + +const UserList = @import("UserList.zig"); +const Environment = @import("../Environment.zig"); + +const Env = struct { + environment: Environment, + index: usize, +}; +const EnvironmentLabel = CyclableLabel(Env, *?UserList); + +const Session = @This(); + +instance: ?Widget = null, +label: *EnvironmentLabel, +user_list: *?UserList, + +pub fn init( + allocator: Allocator, + io: std.Io, + buffer: *TerminalBuffer, + user_list: *?UserList, + width: usize, + text_in_center: bool, + fg: u32, + bg: u32, +) !Session { + return .{ + .instance = null, + .label = try EnvironmentLabel.init( + allocator, + io, + buffer, + drawItem, + sessionChanged, + user_list, + width, + text_in_center, + fg, + bg, + ), + .user_list = user_list, + }; +} + +pub fn deinit(self: *Session) void { + for (self.label.list.items) |*env| { + if (env.environment.entry_ini) |*entry_ini| entry_ini.deinit(); + self.label.allocator.free(env.environment.file_name); + } + + self.label.deinit(); +} + +pub fn widget(self: *Session) *Widget { + if (self.instance) |*instance| return instance; + self.instance = Widget.init( + "Session", + self.label.keybinds, + self, + deinit, + null, + draw, + null, + handle, + null, + ); + return &self.instance.?; +} + +pub fn addEnvironment(self: *Session, environment: Environment) !void { + const env = Env{ .environment = environment, .index = self.label.list.items.len }; + + try self.label.addItem(env); + if (self.user_list.*) |w| addedSession(env, w); +} + +fn draw(self: *Session) void { + self.label.draw(); +} + +fn handle(self: *Session, maybe_key: ?keyboard.Key) !void { + try self.label.handle(maybe_key); +} + +fn addedSession(env: Env, user_list: UserList) void { + const user = user_list.label.list.items[user_list.label.current]; + if (!user.first_run) return; + + user.session_index.* = env.index; +} + +fn sessionChanged(env: Env, maybe_user_list: ?*?UserList) void { + if (maybe_user_list) |user_list| { + if (user_list.*) |w| w.label.list.items[w.label.current].session_index.* = env.index; + } +} + +fn drawItem(label: *EnvironmentLabel, env: Env, x: usize, y: usize, width: usize) void { + if (width < 3) return; + + const length = @min(TerminalBuffer.strWidth(env.environment.name), width - 3); + if (length == 0) return; + + const x_offset = if (label.text_in_center and width >= length) (width - length) / 2 else 0; + + label.cursor = length + x_offset; + TerminalBuffer.drawConfinedText( + env.environment.name, + x + x_offset, + y, + width, + label.fg, + label.bg, + ) catch {}; +} diff --git a/src/components/UserList.zig b/src/components/UserList.zig new file mode 100644 index 0000000..1be513c --- /dev/null +++ b/src/components/UserList.zig @@ -0,0 +1,147 @@ +const std = @import("std"); +const Allocator = std.mem.Allocator; + +const ly_ui = @import("ly-ui"); +const keyboard = ly_ui.keyboard; +const TerminalBuffer = ly_ui.TerminalBuffer; +const Widget = ly_ui.Widget; +const CyclableLabel = ly_ui.CyclableLabel; + +const Session = @import("Session.zig"); +const SavedUsers = @import("../config/SavedUsers.zig"); + +const StringList = std.ArrayList([]const u8); +pub const User = struct { + name: []const u8, + session_index: *usize, + allocated_index: bool, + first_run: bool, +}; +const UserLabel = CyclableLabel(User, *Session); + +const UserList = @This(); + +instance: ?Widget = null, +label: *UserLabel, + +pub fn init( + allocator: Allocator, + io: std.Io, + buffer: *TerminalBuffer, + usernames: StringList, + saved_users: *SavedUsers, + session: *Session, + width: usize, + text_in_center: bool, + fg: u32, + bg: u32, +) !UserList { + var user_list = UserList{ + .instance = null, + .label = try UserLabel.init( + allocator, + io, + buffer, + drawItem, + usernameChanged, + session, + width, + text_in_center, + fg, + bg, + ), + }; + + for (usernames.items) |username| { + if (username.len == 0) continue; + + var maybe_session_index: ?*usize = null; + var first_run = true; + for (saved_users.user_list.items) |*saved_user| { + if (std.mem.eql(u8, username, saved_user.username)) { + maybe_session_index = &saved_user.session_index; + first_run = saved_user.first_run; + break; + } + } + + var allocated_index = false; + if (maybe_session_index == null) { + maybe_session_index = try allocator.create(usize); + maybe_session_index.?.* = 0; + allocated_index = true; + } + + try user_list.label.addItem(.{ + .name = username, + .session_index = maybe_session_index.?, + .allocated_index = allocated_index, + .first_run = first_run, + }); + } + + return user_list; +} + +pub fn deinit(self: *UserList) void { + for (self.label.list.items) |user| { + if (user.allocated_index) { + self.label.allocator.destroy(user.session_index); + } + } + + self.label.deinit(); +} + +pub fn widget(self: *UserList) *Widget { + if (self.instance) |*instance| return instance; + self.instance = Widget.init( + "UserList", + self.label.keybinds, + self, + deinit, + null, + draw, + null, + handle, + null, + ); + return &self.instance.?; +} + +pub fn getCurrentUsername(self: UserList) []const u8 { + return self.label.list.items[self.label.current].name; +} + +fn draw(self: *UserList) void { + self.label.draw(); +} + +fn handle(self: *UserList, maybe_key: ?keyboard.Key) !void { + try self.label.handle(maybe_key); +} + +fn usernameChanged(user: User, maybe_session: ?*Session) void { + if (maybe_session) |session| { + session.label.current = @min(user.session_index.*, session.label.list.items.len - 1); + } +} + +fn drawItem(label: *UserLabel, user: User, x: usize, y: usize, width: usize) void { + if (width < 3) return; + + const length = @min(TerminalBuffer.strWidth(user.name), width - 3); + if (length == 0) return; + + const x_offset = if (label.text_in_center and width >= length) (width - length) / 2 else 0; + + label.cursor = length + x_offset; + TerminalBuffer.drawConfinedText( + user.name, + x + x_offset, + y, + width, + label.fg, + label.bg, + ) catch {}; +} diff --git a/src/config/Config.zig b/src/config/Config.zig index 38c8b4c..2ba8444 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -1,61 +1,100 @@ const build_options = @import("build_options"); -const enums = @import("../enums.zig"); +const enums = @import("../enums.zig"); const Animation = enums.Animation; const Input = enums.Input; const ViMode = enums.ViMode; const Bigclock = enums.Bigclock; +const DurOffsetAlignment = enums.DurOffsetAlignment; +allow_empty_password: bool = true, animation: Animation = .none, +animation_frame_delay: u16 = 5, animation_timeout_sec: u12 = 0, -asterisk: ?u8 = '*', +asterisk: ?u32 = '*', auth_fails: u64 = 10, -bg: u16 = 0, +auto_login_service: [:0]const u8 = "ly-autologin", +auto_login_session: ?[]const u8 = null, +auto_login_user: ?[]const u8 = null, +battery_id: ?[]const u8 = null, +bg: u32 = 0x00000000, bigclock: Bigclock = .none, +bigclock_12hr: bool = false, +bigclock_seconds: bool = false, blank_box: bool = true, -border_fg: u16 = 8, +border_fg: u32 = 0x00FFFFFF, +box_position_h: f32 = 0.5, +box_position_v: f32 = 0.5, box_title: ?[]const u8 = null, -brightness_down_cmd: [:0]const u8 = build_options.prefix_directory ++ "/bin/brightnessctl -q s 10%-", -brightness_down_key: []const u8 = "F5", -brightness_up_cmd: [:0]const u8 = build_options.prefix_directory ++ "/bin/brightnessctl -q s +10%", -brightness_up_key: []const u8 = "F6", +brightness_down_cmd: [:0]const u8 = build_options.prefix_directory ++ "/bin/brightnessctl -q -n s 10%-", +brightness_down_key: ?[]const u8 = "F5", +brightness_up_cmd: [:0]const u8 = build_options.prefix_directory ++ "/bin/brightnessctl -q -n s +10%", +brightness_up_key: ?[]const u8 = "F6", clear_password: bool = false, clock: ?[:0]const u8 = null, -cmatrix_fg: u16 = 3, -console_dev: []const u8 = "/dev/console", +cmatrix_fg: u32 = 0x0000FF00, +cmatrix_head_col: u32 = 0x01FFFFFF, +cmatrix_min_codepoint: u16 = 0x21, +cmatrix_max_codepoint: u16 = 0x7B, +colormix_col1: u32 = 0x00FF0000, +colormix_col2: u32 = 0x000000FF, +colormix_col3: u32 = 0x20000000, +corner_bottom_left: []const u8 = "version", +corner_bottom_right: []const u8 = "labels", +corner_top_left: []const u8 = "shutdown,restart,britup,britdown,password battery", +corner_top_right: []const u8 = "clock numlock,capslock", +custom_bind_width: ?u32 = null, +custom_sessions: []const u8 = build_options.config_directory ++ "/ly/custom-sessions", default_input: Input = .login, -error_bg: u16 = 0, -error_fg: u16 = 258, -fg: u16 = 8, +doom_fire_height: u8 = 6, +doom_fire_spread: u8 = 2, +doom_top_color: u32 = 0x00FF0000, +doom_middle_color: u32 = 0x00FFFF00, +doom_bottom_color: u32 = 0x00FFFFFF, +dur_file_path: []const u8 = build_options.config_directory ++ "/ly/example.dur", +dur_offset_alignment: DurOffsetAlignment = .center, +dur_x_offset: i32 = 0, +dur_y_offset: i32 = 0, +edge_margin: u8 = 0, +error_bg: u32 = 0x00000000, +error_fg: u32 = 0x01FF0000, +fg: u32 = 0x00FFFFFF, +full_color: bool = true, +gameoflife_fg: u32 = 0x0000FF00, +gameoflife_entropy_interval: usize = 10, +gameoflife_frame_delay: usize = 6, +gameoflife_initial_density: f32 = 0.4, hide_borders: bool = false, -hide_key_hints: bool = false, +inactivity_cmd: ?[]const u8 = null, +inactivity_delay: u16 = 0, initial_info_text: ?[]const u8 = null, input_len: u8 = 34, lang: []const u8 = "en", -load: bool = true, login_cmd: ?[]const u8 = null, +login_defs_path: []const u8 = "/etc/login.defs", logout_cmd: ?[]const u8 = null, +lua_animation_file: []const u8 = build_options.config_directory ++ "/ly/example.lua", +ly_log: ?[]const u8 = "/var/log/ly.log", margin_box_h: u8 = 2, margin_box_v: u8 = 1, -min_refresh_delta: u16 = 5, numlock: bool = false, -path: ?[:0]const u8 = "/sbin:/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin", -restart_cmd: []const u8 = "/sbin/shutdown -r now", -restart_key: []const u8 = "F2", -save: bool = true, +path: ?[]const u8 = "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", +restart_key: ?[]const u8 = "F2", +save_file_dir: ?[]const u8 = build_options.config_directory ++ "/ly", service_name: [:0]const u8 = "ly", -session_log: []const u8 = "ly-session.log", +session_log: ?[]const u8 = "ly-session.log", setup_cmd: []const u8 = build_options.config_directory ++ "/ly/setup.sh", -shutdown_cmd: []const u8 = "/sbin/shutdown -a now", -shutdown_key: []const u8 = "F1", -sleep_cmd: ?[]const u8 = null, -sleep_key: []const u8 = "F3", +shell: bool = true, +show_password_key: ?[]const u8 = "F7", +shutdown_key: ?[]const u8 = "F1", +start_cmd: ?[]const u8 = null, text_in_center: bool = false, -tty: u8 = build_options.tty, +type_username: bool = false, vi_default_mode: ViMode = .normal, vi_mode: bool = false, -waylandsessions: []const u8 = build_options.prefix_directory ++ "/share/wayland-sessions", +waylandsessions: ?[]const u8 = build_options.prefix_directory ++ "/share/wayland-sessions", x_cmd: []const u8 = build_options.prefix_directory ++ "/bin/X", +x_vt: ?u8 = null, xauth_cmd: []const u8 = build_options.prefix_directory ++ "/bin/xauth", xinitrc: ?[]const u8 = "~/.xinitrc", -xsessions: []const u8 = build_options.prefix_directory ++ "/share/xsessions", +xsessions: ?[]const u8 = build_options.prefix_directory ++ "/share/xsessions", diff --git a/src/config/Lang.zig b/src/config/Lang.zig index fafa537..c01cff0 100644 --- a/src/config/Lang.zig +++ b/src/config/Lang.zig @@ -1,17 +1,35 @@ +// +// NOTE: After editing this file, please run `/res/lang/normalize_lang_files.py` +// to update all the language files accordingly. +// + authenticating: []const u8 = "authenticating...", brightness_down: []const u8 = "decrease brightness", brightness_up: []const u8 = "increase brightness", capslock: []const u8 = "capslock", +custom: []const u8 = "custom", +custom_info_err_output_long: []const u8 = "output too long", +custom_info_err_no_output: []const u8 = "no output", +custom_info_err_no_output_error: []const u8 = ", possible error", err_alloc: []const u8 = "failed memory allocation", +err_args: []const u8 = "unable to parse command line arguments", +err_autologin_session: []const u8 = "autologin session not found", err_bounds: []const u8 = "out-of-bounds index", err_brightness_change: []const u8 = "failed to change brightness", err_chdir: []const u8 = "failed to open home folder", +err_clock_too_long: []const u8 = "clock string too long", err_config: []const u8 = "unable to parse config file", -err_console_dev: []const u8 = "failed to access console", +err_crawl: []const u8 = "failed to crawl session directories", err_dgn_oob: []const u8 = "log message", err_domain: []const u8 = "invalid domain", +err_empty_password: []const u8 = "empty password not allowed", err_envlist: []const u8 = "failed to get envlist", +err_get_active_tty: []const u8 = "failed to get active tty", +err_hibernate: []const u8 = "failed to execute hibernate command", err_hostname: []const u8 = "failed to get hostname", +err_inactivity: []const u8 = "failed to execute inactivity command", +err_lock_state: []const u8 = "failed to get lock state", +err_log: []const u8 = "failed to open log file", err_mlock: []const u8 = "failed to lock password memory", err_null: []const u8 = "null pointer", err_numlock: []const u8 = "failed to set numlock", @@ -36,7 +54,13 @@ err_perm_dir: []const u8 = "failed to change current directory", err_perm_group: []const u8 = "failed to downgrade group permissions", err_perm_user: []const u8 = "failed to downgrade user permissions", err_pwnam: []const u8 = "failed to get user info", -err_unknown: []const u8 = "an unknown error occurred", +err_sleep: []const u8 = "failed to execute sleep command", +err_start: []const u8 = "failed to execute start command", +err_battery: []const u8 = "failed to load battery status", +err_switch_tty: []const u8 = "failed to switch tty", +err_tty_ctrl: []const u8 = "tty control transfer failed", +err_no_users: []const u8 = "no users found", +err_uid_range: []const u8 = "failed to dynamically get uid range", err_user_gid: []const u8 = "failed to set user GID", err_user_init: []const u8 = "failed to initialize user", err_user_uid: []const u8 = "failed to set user UID", @@ -44,18 +68,20 @@ err_xauth: []const u8 = "xauth command failed", err_xcb_conn: []const u8 = "xcb connection failed", err_xsessions_dir: []const u8 = "failed to find sessions folder", err_xsessions_open: []const u8 = "failed to open sessions folder", +hibernate: []const u8 = "hibernate", insert: []const u8 = "insert", -login: []const u8 = "login:", +login: []const u8 = "login", logout: []const u8 = "logged out", -normal: []const u8 = "normal", no_x11_support: []const u8 = "x11 support disabled at compile-time", +normal: []const u8 = "normal", numlock: []const u8 = "numlock", other: []const u8 = "other", -password: []const u8 = "password:", +password: []const u8 = "password", restart: []const u8 = "reboot", shell: [:0]const u8 = "shell", shutdown: []const u8 = "shutdown", sleep: []const u8 = "sleep", +toggle_password: []const u8 = "toggle password", wayland: []const u8 = "wayland", -xinitrc: [:0]const u8 = "xinitrc", x11: []const u8 = "x11", +xinitrc: [:0]const u8 = "xinitrc", diff --git a/src/config/Save.zig b/src/config/OldSave.zig similarity index 100% rename from src/config/Save.zig rename to src/config/OldSave.zig diff --git a/src/config/SavedUsers.zig b/src/config/SavedUsers.zig new file mode 100644 index 0000000..4891f27 --- /dev/null +++ b/src/config/SavedUsers.zig @@ -0,0 +1,28 @@ +const std = @import("std"); + +const SavedUsers = @This(); + +const User = struct { + username: []const u8, + session_index: usize, + first_run: bool, + allocated_username: bool, +}; + +user_list: std.ArrayList(User), +last_username_index: ?usize, + +pub fn init() SavedUsers { + return .{ + .user_list = .empty, + .last_username_index = null, + }; +} + +pub fn deinit(self: *SavedUsers, allocator: std.mem.Allocator) void { + for (self.user_list.items) |user| { + if (user.allocated_username) allocator.free(user.username); + } + + self.user_list.deinit(allocator); +} diff --git a/src/config/custom.zig b/src/config/custom.zig new file mode 100644 index 0000000..464316d --- /dev/null +++ b/src/config/custom.zig @@ -0,0 +1,25 @@ +const std = @import("std"); + +const custom = @This(); + +pub const CustomCommandBind = struct { + name: []const u8 = "", + cmd: []const u8 = "", +}; + +pub const UNDEFINED_CMD: []const u8 = "echo \"You forgot to define 'cmd'!\""; + +pub const CustomCommandInfo = struct { + name: []const u8 = "", + cmd: ?[]const u8 = null, + /// To be set to the label's widget ID + id: u64 = 0, + + /// In frames, the refresh rate for the `cmd` to run again + /// If 0, only run once. + refresh: u32 = 0, + counter: u32 = 0, +}; + +pub var binds: std.array_hash_map.String(CustomCommandBind) = undefined; +pub var labels: std.array_hash_map.String(CustomCommandInfo) = undefined; diff --git a/src/config/migrator.zig b/src/config/migrator.zig index ad7c378..4ed4d2e 100644 --- a/src/config/migrator.zig +++ b/src/config/migrator.zig @@ -1,23 +1,75 @@ -// The migrator ensures compatibility with <=0.6.0 configuration files +// The migrator ensures compatibility with older configuration files +// Properties removed or changed since 0.6.0 +// Color codes interpreted differently since 1.1.0 +const build_options = @import("build_options"); const std = @import("std"); -const ini = @import("zigini"); -const Save = @import("Save.zig"); -const enums = @import("../enums.zig"); - var temporary_allocator = std.heap.page_allocator; +const ly_ui = @import("ly-ui"); +const TerminalBuffer = ly_ui.TerminalBuffer; +const Color = TerminalBuffer.Color; +const Styling = TerminalBuffer.Styling; +const ly_core = ly_ui.ly_core; +const IniParser = ly_core.IniParser; +const ini = ly_core.ini; + +const Config = @import("Config.zig"); +const Lang = @import("Lang.zig"); +const OldSave = @import("OldSave.zig"); +const SavedUsers = @import("SavedUsers.zig"); +const custom = @import("custom.zig"); + +const color_properties = [_][]const u8{ + "bg", + "border_fg", + "cmatrix_fg", + "colormix_col1", + "colormix_col2", + "colormix_col3", + "error_bg", + "error_fg", + "fg", +}; + +var set_color_properties = + [_]bool{ false, false, false, false, false, false, false, false, false }; + +const removed_properties = [_][]const u8{ + "wayland_specifier", + "max_desktop_len", + "max_login_len", + "max_password_len", + "mcookie_cmd", + "term_reset_cmd", + "term_restore_cursor_cmd", + "x_cmd_setup", + "wayland_cmd", + "console_dev", + "load", + "shutdown_cmd", + "restart_cmd", + // Migrating these isn't worth the effort so just say we removed them + "hide_key_hints", + "hide_keyboard_locks", + "hide_version_string", + "show_tty", +}; + +pub var auto_eight_colors: bool = true; + pub var maybe_animate: ?bool = null; pub var maybe_save_file: ?[]const u8 = null; - -pub var mapped_config_fields = false; +pub var maybe_sleep_key: ?[]const u8 = null; +pub var maybe_sleep_cmd: ?[]const u8 = null; +pub var maybe_hibernate_key: ?[]const u8 = null; +pub var maybe_hibernate_cmd: ?[]const u8 = null; pub fn configFieldHandler(_: std.mem.Allocator, field: ini.IniField) ?ini.IniField { if (std.mem.eql(u8, field.key, "animate")) { // The option doesn't exist anymore, but we save its value for "animation" maybe_animate = std.mem.eql(u8, field.value, "true"); - mapped_config_fields = true; return null; } @@ -33,16 +85,37 @@ pub fn configFieldHandler(_: std.mem.Allocator, field: ini.IniField) ?ini.IniFie else => "none", }; - mapped_config_fields = true; return mapped_field; } + inline for (color_properties, &set_color_properties) |property, *status| { + if (std.mem.eql(u8, field.key, property)) { + // Color has been set; it won't be overwritten if we default to eight-color output + status.* = true; + + // These options now uses a 32-bit RGB value instead of an arbitrary 16-bit integer + // If they're all using eight-color codes, we start in eight-color mode + const color = std.fmt.parseInt(u16, field.value, 0) catch { + auto_eight_colors = false; + return field; + }; + + const color_no_styling = color & 0x00FF; + const styling_only = color & 0xFF00; + + // If color is "greater" than TB_WHITE, or the styling is "greater" than TB_DIM, + // we have an invalid color, so do not use eight-color mode + if (color_no_styling > 0x0008 or styling_only > 0x8000) auto_eight_colors = false; + + return field; + } + } + if (std.mem.eql(u8, field.key, "blank_password")) { // The option has simply been renamed var mapped_field = field; mapped_field.key = "clear_password"; - mapped_config_fields = true; return mapped_field; } @@ -58,31 +131,20 @@ pub fn configFieldHandler(_: std.mem.Allocator, field: ini.IniField) ?ini.IniFie else => "login", }; - mapped_config_fields = true; return mapped_field; } if (std.mem.eql(u8, field.key, "save_file")) { // The option doesn't exist anymore, but we save its value for migration later on maybe_save_file = temporary_allocator.dupe(u8, field.value) catch return null; - - mapped_config_fields = true; return null; } - if (std.mem.eql(u8, field.key, "wayland_specifier") or - std.mem.eql(u8, field.key, "max_desktop_len") or - std.mem.eql(u8, field.key, "max_login_len") or - std.mem.eql(u8, field.key, "max_password_len") or - std.mem.eql(u8, field.key, "mcookie_cmd") or - std.mem.eql(u8, field.key, "term_reset_cmd") or - std.mem.eql(u8, field.key, "term_restore_cursor_cmd") or - std.mem.eql(u8, field.key, "x_cmd_setup") or - std.mem.eql(u8, field.key, "wayland_cmd")) - { - // The options don't exist anymore - mapped_config_fields = true; - return null; + inline for (removed_properties) |property| { + if (std.mem.eql(u8, field.key, property)) { + // The options don't exist anymore + return null; + } } if (std.mem.eql(u8, field.key, "bigclock")) { @@ -90,55 +152,225 @@ pub fn configFieldHandler(_: std.mem.Allocator, field: ini.IniField) ?ini.IniFie // It also includes the ability to change active bigclock's language var mapped_field = field; - if (std.mem.eql(u8, field.value, "true")){ + if (std.mem.eql(u8, field.value, "true")) { mapped_field.value = "en"; - mapped_config_fields = true; - }else if (std.mem.eql(u8, field.value, "false")){ + } else if (std.mem.eql(u8, field.value, "false")) { mapped_field.value = "none"; - mapped_config_fields = true; } - + return mapped_field; } + if (std.mem.eql(u8, field.key, "full_color")) { + // If color mode is defined, definitely don't set it automatically + auto_eight_colors = false; + return field; + } + + if (std.mem.eql(u8, field.key, "min_refresh_delta")) { + // The option has simply been renamed + var mapped_field = field; + mapped_field.key = "animation_frame_delay"; + + return mapped_field; + } + + if (std.mem.eql(u8, field.key, "sleep_key")) { + maybe_sleep_key = temporary_allocator.dupe(u8, field.value) catch return null; + return null; + } + + if (std.mem.eql(u8, field.key, "sleep_cmd")) { + maybe_sleep_cmd = temporary_allocator.dupe(u8, field.value) catch return null; + return null; + } + + if (std.mem.eql(u8, field.key, "hibernate_key")) { + maybe_hibernate_key = temporary_allocator.dupe(u8, field.value) catch return null; + return null; + } + + if (std.mem.eql(u8, field.key, "hibernate_cmd")) { + maybe_hibernate_cmd = temporary_allocator.dupe(u8, field.value) catch return null; + return null; + } + + if (std.mem.eql(u8, field.key, "save")) { + // The option now uses a string for the file's parent directory + var mapped_field = field; + + if (std.mem.eql(u8, field.value, "true")) { + mapped_field.value = build_options.config_directory ++ "/ly"; + } else if (std.mem.eql(u8, field.value, "false")) { + mapped_field.value = "null"; + } + + return mapped_field; + } + + // TODO: Dearest Melpert, + // I pray this message finds you well, as daylight dwindles and the witching hour + // approaches, I find it more and more imperative as time continues that I place + // this reminder here in such a format that you cannot ignore. + // Do you know how long I have been waiting for this petition to be authorized + // in regards to this particular segment of computerized instructions? + // It has been many a moon since this particular audit has been + // posted regarding the position of handling configurable literature + // apparatuses and plans for a new feature to the configuration + // interface and as time continues onwards I grow more restless + // on the progress of said interface, only to find out afterwards + // that you have PROCRASTINATED on the efforts meant to enhance + // configuration. Thus the requirement for this reminder larger + // compared to the two reminders regarding better methods of + // X termination detection and new usernames with existing + // save files. + // + // Thus is my que to leave this TODO at thy request, + // + // Forever Sullied, + // + // Ly Contributor. + // + if (std.mem.startsWith(u8, field.header, "cmd:")) { + const key = field.header["cmd:".len..]; + const keyZ = temporary_allocator.dupe(u8, key) catch ""; + if (!custom.binds.contains(key)) { + custom.binds.put(temporary_allocator, keyZ, .{}) catch {}; + } + if (custom.binds.getPtr(keyZ)) |command| { + if (std.mem.eql(u8, field.key, "name")) { + command.name = temporary_allocator.dupe(u8, field.value) catch ""; + } + if (std.mem.eql(u8, field.key, "cmd")) { + command.cmd = temporary_allocator.dupe(u8, field.value) catch ""; + } + } + } + + if (std.mem.startsWith(u8, field.header, "lbl:")) { + const key = field.header["lbl:".len..]; + const keyZ = temporary_allocator.dupe(u8, key) catch ""; + if (!custom.labels.contains(keyZ)) { + custom.labels.put(temporary_allocator, keyZ, .{ .name = keyZ }) catch {}; + } + if (custom.labels.getPtr(keyZ)) |label| { + if (std.mem.eql(u8, field.key, "cmd")) { + label.cmd = temporary_allocator.dupe(u8, field.value) catch ""; + } + if (std.mem.eql(u8, field.key, "refresh")) { + label.refresh = std.fmt.parseInt(u32, field.value, 10) catch 0; + } + } + } + return field; } // This is the stuff we only handle after reading the config. // For example, the "animate" field could come after "animation" -pub fn lateConfigFieldHandler(animation: *enums.Animation) void { +pub fn lateConfigFieldHandler(config: *Config, lang: Lang) void { if (maybe_animate) |animate| { - if (!animate) animation.* = .none; + if (!animate) config.*.animation = .none; + } + + if (auto_eight_colors) { + // Valid config file predates true-color mode + // Will use eight-color output instead + config.full_color = false; + + // We cannot rely on Config defaults when in eight-color mode, + // because they will appear as undesired colors. + // Instead set color properties to matching eight-color codes + config.doom_top_color = Color.ECOL_RED; + config.doom_middle_color = Color.ECOL_YELLOW; + config.doom_bottom_color = Color.ECOL_WHITE; + config.cmatrix_head_col = Styling.BOLD | Color.ECOL_WHITE; + + // These may be in the config, so only change those which were not set + if (!set_color_properties[0]) config.bg = Color.DEFAULT; + if (!set_color_properties[1]) config.border_fg = Color.ECOL_WHITE; + if (!set_color_properties[2]) config.cmatrix_fg = Color.ECOL_GREEN; + if (!set_color_properties[3]) config.colormix_col1 = Color.ECOL_RED; + if (!set_color_properties[4]) config.colormix_col2 = Color.ECOL_BLUE; + if (!set_color_properties[5]) config.colormix_col3 = Color.ECOL_BLACK; + if (!set_color_properties[6]) config.error_bg = Color.DEFAULT; + if (!set_color_properties[7]) config.error_fg = Styling.BOLD | Color.ECOL_RED; + if (!set_color_properties[8]) config.fg = Color.ECOL_WHITE; + } + + if (maybe_sleep_key) |key| { + if (maybe_sleep_cmd) |cmd| { + custom.binds.put(temporary_allocator, key, .{ + .name = temporary_allocator.dupe(u8, lang.sleep) catch "", + .cmd = cmd, + }) catch {}; + } + } + + if (maybe_hibernate_key) |key| { + if (maybe_hibernate_cmd) |cmd| { + custom.binds.put(temporary_allocator, key, .{ + .name = temporary_allocator.dupe(u8, lang.hibernate) catch "", + .cmd = cmd, + }) catch {}; + } } } -pub fn tryMigrateSaveFile(user_buf: *[32]u8) Save { - var save = Save{}; +pub fn tryMigrateIniSaveFile(allocator: std.mem.Allocator, io: std.Io, path: []const u8, saved_users: *SavedUsers, usernames: [][]const u8) !?IniParser(OldSave) { + var save_parser = try IniParser(OldSave).init(allocator, io, path, null); + errdefer save_parser.deinit(); + var user_buf: [32]u8 = undefined; + const maybe_save = if (save_parser.maybe_load_error == null) save_parser.structure else tryMigrateFirstSaveFile(io, &user_buf); + + if (maybe_save) |save| { + // Add all other users to the list + for (usernames, 0..) |username, i| { + if (save.user) |user| { + if (std.mem.eql(u8, user, username)) saved_users.last_username_index = i; + } + + try saved_users.user_list.append(allocator, .{ + .username = username, + .session_index = save.session_index orelse 0, + .first_run = false, + .allocated_username = false, + }); + } + + return save_parser; + } + + return null; +} + +fn tryMigrateFirstSaveFile(io: std.Io, user_buf: *[32]u8) ?OldSave { if (maybe_save_file) |path| { - defer temporary_allocator.free(path); + var save = OldSave{}; + var file = std.Io.Dir.openFileAbsolute(io, path, .{}) catch return null; + defer file.close(io); - var file = std.fs.openFileAbsolute(path, .{}) catch return save; - defer file.close(); + var file_buffer: [64]u8 = undefined; + var file_reader = file.reader(io, &file_buffer); + var reader = &file_reader.interface; - const reader = file.reader(); - - var user_fbs = std.io.fixedBufferStream(user_buf); - reader.streamUntilDelimiter(user_fbs.writer(), '\n', user_buf.len) catch return save; - const user = user_fbs.getWritten(); - if (user.len > 0) save.user = user; + var user_writer = std.Io.Writer.fixed(user_buf); + var written = reader.streamDelimiter(&user_writer, '\n') catch return null; + if (written > 0) save.user = user_buf[0..written]; var session_buf: [20]u8 = undefined; - var session_fbs = std.io.fixedBufferStream(&session_buf); - reader.streamUntilDelimiter(session_fbs.writer(), '\n', session_buf.len) catch return save; + var session_writer = std.Io.Writer.fixed(&session_buf); + written = reader.streamDelimiter(&session_writer, '\n') catch return null; - const session_index_str = session_fbs.getWritten(); var session_index: ?usize = null; - if (session_index_str.len > 0) { - session_index = std.fmt.parseUnsigned(usize, session_index_str, 10) catch return save; + if (written > 0) { + session_index = std.fmt.parseUnsigned(usize, session_buf[0..written], 10) catch return null; } save.session_index = session_index; + + return save; } - return save; + return null; } diff --git a/src/enums.zig b/src/enums.zig index ad0cc47..ff69f84 100644 --- a/src/enums.zig +++ b/src/enums.zig @@ -1,7 +1,13 @@ +const std = @import("std"); + pub const Animation = enum { none, doom, matrix, + colormix, + gameoflife, + dur_file, + lua, }; pub const DisplayServer = enum { @@ -9,6 +15,7 @@ pub const DisplayServer = enum { shell, xinitrc, x11, + custom, }; pub const Input = enum { @@ -16,6 +23,26 @@ pub const Input = enum { session, login, password, + + /// Moves the current Input forwards by one entry. If `reverse`, then the Input + /// moves backwards. If `wrap` is true, then the entry will wrap back around + pub fn move(self: *Input, reverse: bool, wrap: bool) void { + const maxNum = @typeInfo(Input).@"enum".fields.len - 1; + const selfNum = @intFromEnum(self.*); + if (reverse) { + if (wrap) { + self.* = @enumFromInt(selfNum -% 1); + } else if (selfNum != 0) { + self.* = @enumFromInt(selfNum - 1); + } + } else { + if (wrap) { + self.* = @enumFromInt(selfNum +% 1); + } else if (selfNum != maxNum) { + self.* = @enumFromInt(selfNum + 1); + } + } + } }; pub const ViMode = enum { @@ -27,4 +54,16 @@ pub const Bigclock = enum { none, en, fa, -}; \ No newline at end of file +}; + +pub const DurOffsetAlignment = enum { + topleft, + topcenter, + topright, + centerleft, + center, + centerright, + bottomleft, + bottomcenter, + bottomright, +}; diff --git a/src/interop.zig b/src/interop.zig deleted file mode 100644 index 9fc5017..0000000 --- a/src/interop.zig +++ /dev/null @@ -1,111 +0,0 @@ -const std = @import("std"); -const builtin = @import("builtin"); -const Allocator = std.mem.Allocator; - -pub const termbox = @import("termbox2"); - -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"); -}); - -pub const unistd = @cImport({ - @cInclude("unistd.h"); -}); - -pub const time = @cImport({ - @cInclude("time.h"); -}); - -pub const system_time = @cImport({ - @cInclude("sys/time.h"); -}); - -pub const stdlib = @cImport({ - @cInclude("stdlib.h"); -}); - -pub 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("login_cap.h"); -}); - -pub const grp = @cImport({ - @cInclude("grp.h"); -}); - -// BSD-specific headers -pub const kbio = @cImport({ - @cInclude("sys/kbio.h"); -}); - -// Linux-specific headers -pub const kd = @cImport({ - @cInclude("sys/kd.h"); -}); - -pub const vt = @cImport({ - @cInclude("sys/vt.h"); -}); - -// Used for getting & setting the lock state -const LedState = if (builtin.os.tag.isBSD()) c_int else c_char; -const get_led_state = if (builtin.os.tag.isBSD()) kbio.KDGETLED else kd.KDGKBLED; -const set_led_state = if (builtin.os.tag.isBSD()) kbio.KDSETLED else kd.KDSKBLED; -const numlock_led = if (builtin.os.tag.isBSD()) kbio.LED_NUM else kd.K_NUMLOCK; -const capslock_led = if (builtin.os.tag.isBSD()) kbio.LED_CAP else kd.K_CAPSLOCK; - -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); - if (len < 0) return error.CannotGetFormattedTime; - - return buf[0..len]; -} - -pub fn switchTty(console_dev: []const u8, tty: u8) !void { - const fd = try std.posix.open(console_dev, .{ .ACCMODE = .WRONLY }, 0); - defer std.posix.close(fd); - - _ = std.c.ioctl(fd, vt.VT_ACTIVATE, tty); - _ = std.c.ioctl(fd, vt.VT_WAITACTIVE, tty); -} - -pub fn getLockState(console_dev: []const u8) !struct { - numlock: bool, - capslock: bool, -} { - const fd = try std.posix.open(console_dev, .{ .ACCMODE = .RDONLY }, 0); - defer std.posix.close(fd); - - var led: LedState = undefined; - _ = std.c.ioctl(fd, get_led_state, &led); - - return .{ - .numlock = (led & numlock_led) != 0, - .capslock = (led & capslock_led) != 0, - }; -} - -pub fn setNumlock(val: bool) !void { - var led: LedState = undefined; - _ = std.c.ioctl(0, get_led_state, &led); - - const numlock = (led & numlock_led) != 0; - if (numlock != val) { - const status = std.c.ioctl(std.posix.STDIN_FILENO, set_led_state, led ^ numlock_led); - if (status != 0) return error.FailedToSetNumlock; - } -} diff --git a/src/main.zig b/src/main.zig index 00dde46..1d0db99 100644 --- a/src/main.zig +++ b/src/main.zig @@ -1,760 +1,2548 @@ const std = @import("std"); -const build_options = @import("build_options"); +const Allocator = std.mem.Allocator; +const StringList = std.ArrayList([]const u8); +const temporary_allocator = std.heap.page_allocator; const builtin = @import("builtin"); +const build_options = @import("build_options"); + const clap = @import("clap"); -const ini = @import("zigini"); -const auth = @import("auth.zig"); -const bigclock = @import("bigclock.zig"); -const interop = @import("interop.zig"); +const ly_ui = @import("ly-ui"); +const Position = ly_ui.Position; +const BigLabel = ly_ui.BigLabel; +const Box = ly_ui.Box; +const Label = ly_ui.Label; +const Text = ly_ui.Text; +const TerminalBuffer = ly_ui.TerminalBuffer; +const Widget = ly_ui.Widget; +const ly_core = ly_ui.ly_core; +const interop = ly_core.interop; +const UidRange = ly_core.UidRange; +const LogFile = ly_core.LogFile; +const SharedError = ly_core.SharedError; +const IniParser = ly_core.IniParser; +const ini = ly_core.ini; +const Ini = ini.Ini; + +const Cascade = @import("animations/Cascade.zig"); +const ColorMix = @import("animations/ColorMix.zig"); const Doom = @import("animations/Doom.zig"); +const DurFile = @import("animations/DurFile.zig"); +const GameOfLife = @import("animations/GameOfLife.zig"); const Matrix = @import("animations/Matrix.zig"); -const TerminalBuffer = @import("tui/TerminalBuffer.zig"); -const Session = @import("tui/components/Session.zig"); -const Text = @import("tui/components/Text.zig"); -const InfoLine = @import("tui/components/InfoLine.zig"); +const Lua = @import("animations/Lua.zig"); +const auth = @import("auth.zig"); +const InfoLine = @import("components/InfoLine.zig"); +const Session = @import("components/Session.zig"); +const UserList = @import("components/UserList.zig"); const Config = @import("config/Config.zig"); const Lang = @import("config/Lang.zig"); -const Save = @import("config/Save.zig"); const migrator = @import("config/migrator.zig"); -const SharedError = @import("SharedError.zig"); -const utils = @import("tui/utils.zig"); +const OldSave = @import("config/OldSave.zig"); +const SavedUsers = @import("config/SavedUsers.zig"); +const custom = @import("config/custom.zig"); +const DisplayServer = @import("enums.zig").DisplayServer; +const Environment = @import("Environment.zig"); +const Entry = Environment.Entry; -const Ini = ini.Ini; -const termbox = interop.termbox; -const unistd = interop.unistd; -const temporary_allocator = std.heap.page_allocator; +const ly_version_str = "Ly version " ++ build_options.version; var session_pid: std.posix.pid_t = -1; -pub fn signalHandler(i: c_int) callconv(.C) void { +fn signalHandler(sig: std.posix.SIG) callconv(.c) void { if (session_pid == 0) return; // Forward signal to session to clean up if (session_pid > 0) { - _ = std.c.kill(session_pid, i); + _ = std.c.kill(session_pid, sig); var status: c_int = 0; _ = std.c.waitpid(session_pid, &status, 0); } - _ = termbox.tb_shutdown(); - std.c.exit(i); + TerminalBuffer.shutdown() catch {}; + std.c.exit(@intCast(@intFromEnum(sig))); } -pub fn main() !void { - var shutdown = false; - var restart = false; - var shutdown_cmd: []const u8 = undefined; - var restart_cmd: []const u8 = undefined; +fn ttyControlTransferSignalHandler(_: std.posix.SIG) callconv(.c) void { + TerminalBuffer.shutdown() catch {}; +} - const stderr = std.io.getStdErr().writer(); +const CustomBindLabel = struct { + cmd: custom.CustomCommandBind, + key: []const u8, + lbl: Label, + io: std.Io, +}; +const CustomInfoLabel = struct { + info: custom.CustomCommandInfo, + lbl: Label, +}; + +const UiState = struct { + allocator: Allocator, + io: std.Io, + auth_fails: u64, + is_autologin: bool, + use_kmscon_vt: bool, + active_tty: u8, + buffer: TerminalBuffer, + labels_max_length: usize, + shutdown_label: Label, + restart_label: Label, + toggle_password_label: Label, + brightness_down_label: Label, + brightness_up_label: Label, + numlock_label: Label, + capslock_label: Label, + battery_label: Label, + clock_label: Label, + tty_label: Label, + session_specifier_label: Label, + login_label: Label, + password_label: Label, + version_label: Label, + bigclock_label: BigLabel, + box: Box, + info_line: InfoLine, + animate: bool, + session: Session, + saved_username: ?[]const u8, + saved_users: SavedUsers, + login: ?UserList, + login_text: ?*Text, + password: *Text, + password_widget: *Widget, + insert_mode: bool, + edge_margin: Position, + config: Config, + lang: Lang, + log_file: LogFile, + save_path: []const u8, + old_save_path: []const u8, + has_old_save: bool, + battery_buf: [16:0]u8, + bigclock_format_buf: [16:0]u8, + clock_buf: [64:0]u8, + tty_buf: [8:0]u8, + bigclock_buf: [32:0]u8, + custom_binds: std.ArrayList(CustomBindLabel), + custom_info: std.ArrayList(CustomInfoLabel), +}; + +var shutdown = false; +var restart = false; + +pub fn main(init: std.process.Init) !void { + var state: UiState = undefined; + + state.io = init.io; + + var stderr_buffer: [128]u8 = undefined; + var stderr_writer = std.Io.File.stderr().writer(state.io, &stderr_buffer); + var stderr = &stderr_writer.interface; + + // If we can't shutdown or restart due to an error, we print it to standard error. If that fails, just bail out defer { - // If we can't shutdown or restart due to an error, we print it to standard error. If that fails, just bail out if (shutdown) { - const shutdown_error = std.process.execv(temporary_allocator, &[_][]const u8{ "/bin/sh", "-c", shutdown_cmd }); - stderr.print("error: couldn't shutdown: {any}\n", .{shutdown_error}) catch std.process.exit(1); + interop.shutdownSystem() catch std.log.err("whoopsie doodle, couldn't shutdown system!", .{}); } else if (restart) { - const restart_error = std.process.execv(temporary_allocator, &[_][]const u8{ "/bin/sh", "-c", restart_cmd }); - stderr.print("error: couldn't restart: {any}\n", .{restart_error}) catch std.process.exit(1); - } else { - // The user has quit Ly using Ctrl+C - temporary_allocator.free(shutdown_cmd); - temporary_allocator.free(restart_cmd); + interop.rebootSystem() catch std.log.err("whoopsie doodle, couldn't restart system!", .{}); } } - var gpa = std.heap.GeneralPurposeAllocator(.{}){}; - defer _ = gpa.deinit(); + var gpa: std.heap.DebugAllocator(.{ + .never_unmap = builtin.mode == .Debug, + .retain_metadata = builtin.mode == .Debug, + }) = .init; + defer if (gpa.deinit() == .leak) std.log.err("attention please, memory has been leaked!", .{}); - // to be able to stop the animation after some time - - var tv_zero: interop.system_time.timeval = undefined; - _ = interop.system_time.gettimeofday(&tv_zero, null); - var animation_timed_out: bool = false; - - const allocator = gpa.allocator(); + state.allocator = gpa.allocator(); // Load arguments const params = comptime clap.parseParamsComptime( \\-h, --help Shows all commands. \\-v, --version Shows the version of Ly. \\-c, --config Overrides the default configuration path. Example: --config /usr/share/ly + \\--use-kmscon-vt Uses KMSCON instead of the kernel VT. + \\--validate-config Validates the given configuration file. ); var diag = clap.Diagnostic{}; - var res = clap.parse(clap.Help, ¶ms, clap.parsers.default, .{ .diagnostic = &diag, .allocator = allocator }) catch |err| { + var arg_parse_error: anyerror = undefined; + var maybe_res = clap.parse(clap.Help, ¶ms, clap.parsers.default, init.minimal.args, .{ .diagnostic = &diag, .allocator = state.allocator }) catch |err| parse_error: { + arg_parse_error = err; diag.report(stderr, err) catch {}; - return err; + try stderr.flush(); + break :parse_error null; }; - defer res.deinit(); + defer if (maybe_res) |*res| res.deinit(); - var config: Config = undefined; - var lang: Lang = undefined; - var save: Save = undefined; - var config_load_failed = false; + var old_save_parser: ?IniParser(OldSave) = null; + defer if (old_save_parser) |*str| str.deinit(); - if (res.args.help != 0) { - try clap.help(stderr, clap.Help, ¶ms, .{}); + state.use_kmscon_vt = false; - _ = try stderr.write("Note: if you want to configure Ly, please check the config file, which is located at " ++ build_options.config_directory ++ "/ly/config.ini.\n"); - std.process.exit(0); - } - if (res.args.version != 0) { - _ = try stderr.write("Ly version " ++ build_options.version ++ "\n"); - std.process.exit(0); + var start_cmd_exit_code: u8 = 0; + + state.saved_users = SavedUsers.init(); + defer state.saved_users.deinit(state.allocator); + + var config_parent_path: []const u8 = build_options.config_directory ++ "/ly"; + if (maybe_res) |*res| { + if (res.args.help != 0) { + try clap.help(stderr, clap.Help, ¶ms, .{}); + + std.log.info("note: if you want to configure Ly, please check the config file, which is located at " ++ build_options.config_directory ++ "/ly/config.ini.", .{}); + std.process.exit(0); + } + if (res.args.version != 0) { + std.log.info("ly version " ++ build_options.version, .{}); + std.process.exit(0); + } + if (res.args.config) |path| config_parent_path = path; + if (res.args.@"use-kmscon-vt" != 0) state.use_kmscon_vt = true; + if (res.args.@"validate-config") |path| { + var parser = try IniParser(Config).init( + state.allocator, + state.io, + path, + migrator.configFieldHandler, + ); + defer parser.deinit(); + + for (parser.errors.items) |err| { + std.log.err( + "failed to convert value '{s}' of option '{s}' to type '{s}': {s}", + .{ err.value, err.key, err.type_name, err.error_name }, + ); + } + + if (parser.maybe_load_error) |err| { + std.log.err("failed to load config file: {s}", .{@errorName(err)}); + std.process.exit(1); + } + + std.log.info("no errors detected!", .{}); + std.process.exit(0); + } } // Load configuration file - var config_ini = Ini(Config).init(allocator); - defer config_ini.deinit(); - - var lang_ini = Ini(Lang).init(allocator); - defer lang_ini.deinit(); - - var save_ini = Ini(Save).init(allocator); - defer save_ini.deinit(); - - var save_path: []const u8 = build_options.config_directory ++ "/ly/save.ini"; - var save_path_alloc = false; - defer { - if (save_path_alloc) allocator.free(save_path); - } - - const comment_characters = "#"; - - if (res.args.config) |s| { - const trailing_slash = if (s[s.len - 1] != '/') "/" else ""; - - const config_path = try std.fmt.allocPrint(allocator, "{s}{s}config.ini", .{ s, trailing_slash }); - defer allocator.free(config_path); - - config = config_ini.readFileToStruct(config_path, comment_characters, migrator.configFieldHandler) catch _config: { - config_load_failed = true; - break :_config Config{}; - }; - - const lang_path = try std.fmt.allocPrint(allocator, "{s}{s}lang/{s}.ini", .{ s, trailing_slash, config.lang }); - defer allocator.free(lang_path); - - lang = lang_ini.readFileToStruct(lang_path, comment_characters, null) catch Lang{}; - - if (config.load) { - save_path = try std.fmt.allocPrint(allocator, "{s}{s}save.ini", .{ s, trailing_slash }); - save_path_alloc = true; - - var user_buf: [32]u8 = undefined; - save = save_ini.readFileToStruct(save_path, comment_characters, null) catch migrator.tryMigrateSaveFile(&user_buf); - } - - migrator.lateConfigFieldHandler(&config.animation); - } else { - const config_path = build_options.config_directory ++ "/ly/config.ini"; - - config = config_ini.readFileToStruct(config_path, comment_characters, migrator.configFieldHandler) catch _config: { - config_load_failed = true; - break :_config Config{}; - }; - - const lang_path = try std.fmt.allocPrint(allocator, "{s}/ly/lang/{s}.ini", .{ build_options.config_directory, config.lang }); - defer allocator.free(lang_path); - - lang = lang_ini.readFileToStruct(lang_path, comment_characters, null) catch Lang{}; - - if (config.load) { - var user_buf: [32]u8 = undefined; - save = save_ini.readFileToStruct(save_path, comment_characters, null) catch migrator.tryMigrateSaveFile(&user_buf); - } - - migrator.lateConfigFieldHandler(&config.animation); - } - - // if (migrator.mapped_config_fields) save_migrated_config: { - // var file = try std.fs.cwd().createFile(config_path, .{}); - // defer file.close(); - - // const writer = file.writer(); - // ini.writeFromStruct(config, writer, null, true, .{}) catch { - // break :save_migrated_config; - // }; - // } - - // These strings only end up getting freed if the user quits Ly using Ctrl+C, which is fine since in the other cases - // we end up shutting down or restarting the system - shutdown_cmd = try temporary_allocator.dupe(u8, config.shutdown_cmd); - restart_cmd = try temporary_allocator.dupe(u8, config.restart_cmd); - - // Initialize termbox - _ = termbox.tb_init(); - defer _ = termbox.tb_shutdown(); - - const act = std.posix.Sigaction{ - .handler = .{ .handler = &signalHandler }, - .mask = std.posix.empty_sigset, - .flags = 0, + defer if (state.config.save_file_dir != null) { + state.allocator.free(state.save_path); + state.allocator.free(state.old_save_path); }; - try std.posix.sigaction(std.posix.SIG.TERM, &act, null); - _ = termbox.tb_set_output_mode(termbox.TB_OUTPUT_NORMAL); - _ = termbox.tb_clear(); + const config_path = try std.Io.Dir.path.join(state.allocator, &[_][]const u8{ config_parent_path, "config.ini" }); + defer state.allocator.free(config_path); - // Needed to reset termbox after auth - const tb_termios = try std.posix.tcgetattr(std.posix.STDIN_FILENO); + custom.binds = .empty; + custom.labels = .empty; + var config_parser = try IniParser(Config).init(state.allocator, state.io, config_path, migrator.configFieldHandler); + defer config_parser.deinit(); + defer if (!shutdown or !restart) { + var iter = custom.binds.iterator(); + while (iter.next()) |i| { + temporary_allocator.free(i.key_ptr.*); + temporary_allocator.free(i.value_ptr.*.cmd); + temporary_allocator.free(i.value_ptr.*.name); + } + custom.binds.deinit(temporary_allocator); + var labelIter = custom.labels.iterator(); + while (labelIter.next()) |i| { + temporary_allocator.free(i.key_ptr.*); + if (i.value_ptr.cmd) |cmd| + temporary_allocator.free(cmd); + } + custom.labels.deinit(temporary_allocator); + }; + + state.config = config_parser.structure; + + var lang_buffer: [16]u8 = undefined; + const lang_file = try std.fmt.bufPrint(&lang_buffer, "{s}.ini", .{state.config.lang}); + + const lang_path = try std.Io.Dir.path.join(state.allocator, &[_][]const u8{ config_parent_path, "lang", lang_file }); + defer state.allocator.free(lang_path); + + var lang_parser = try IniParser(Lang).init(state.allocator, state.io, lang_path, null); + defer lang_parser.deinit(); + + state.lang = lang_parser.structure; + + if (state.config.save_file_dir) |dir| { + state.save_path = try std.Io.Dir.path.join(state.allocator, &[_][]const u8{ dir, "save.txt" }); + state.old_save_path = try std.Io.Dir.path.join(state.allocator, &[_][]const u8{ config_parent_path, "save.ini" }); + } + + if (config_parser.maybe_load_error == null) { + migrator.lateConfigFieldHandler(&state.config, state.lang); + } + + var maybe_uid_range_error: ?anyerror = null; + var usernames = try getAllUsernames(state.allocator, state.io, state.config.login_defs_path, &maybe_uid_range_error); + defer { + for (usernames.items) |username| state.allocator.free(username); + usernames.deinit(state.allocator); + } + + state.has_old_save = false; + state.saved_username = null; + + if (state.config.save_file_dir != null) read_save_file: { + old_save_parser = migrator.tryMigrateIniSaveFile(state.allocator, state.io, state.old_save_path, &state.saved_users, usernames.items) catch break :read_save_file; + + // Don't read the new save file if the old one still exists + if (old_save_parser != null) { + state.has_old_save = true; + break :read_save_file; + } + + var save_file = std.Io.Dir.cwd().openFile(state.io, state.save_path, .{}) catch break :read_save_file; + defer save_file.close(state.io); + + var file_buffer: [256]u8 = undefined; + var file_reader = save_file.reader(state.io, &file_buffer); + var reader = &file_reader.interface; + + const username_line = reader.takeDelimiterInclusive('\n') catch break :read_save_file; + + if (std.mem.containsAtLeastScalar2(u8, username_line, '-', 1)) read_username: { + var iterator = std.mem.splitScalar(u8, username_line[0..(username_line.len - 1)], '-'); + if (iterator.next() == null) break :read_username; // Would be index + + const maybe_username = iterator.next(); + if (maybe_username) |username| { + state.saved_username = try state.allocator.dupe(u8, username); + } + } else if (!state.config.type_username) { + state.saved_users.last_username_index = std.fmt.parseInt(usize, username_line[0..(username_line.len - 1)], 10) catch break :read_save_file; + } + + while (reader.seek < reader.buffer.len) { + const line = reader.takeDelimiterInclusive('\n') catch break; + + var user = std.mem.splitScalar(u8, line[0..(line.len - 1)], ':'); + const username = user.next() orelse continue; + const session_index_str = user.next() orelse continue; + + const session_index = std.fmt.parseInt(usize, session_index_str, 10) catch continue; + + try state.saved_users.user_list.append(state.allocator, .{ + .username = try state.allocator.dupe(u8, username), + .session_index = session_index, + .first_run = false, + .allocated_username = true, + }); + } + } + + // If no save file previously existed, fill it up with all usernames + // TODO: Add new username with existing save file + if (state.config.save_file_dir != null and state.saved_users.user_list.items.len == 0) { + for (usernames.items) |user| { + try state.saved_users.user_list.append(state.allocator, .{ + .username = user, + .session_index = 0, + .first_run = true, + .allocated_username = false, + }); + } + } + + var log_file_buffer: [1024]u8 = undefined; + + state.log_file = try LogFile.init(state.io, state.config.ly_log, &log_file_buffer); + defer state.log_file.deinit(state.io); + + try state.log_file.info(state.io, "tui", "using {s} vt", .{if (state.use_kmscon_vt) "kmscon" else "default"}); + + if (state.config.start_cmd) |start_cmd| handle_start_cmd: { + var process = std.process.spawn(state.io, .{ + .argv = &[_][]const u8{ "/bin/sh", "-c", start_cmd }, + .stdout = .inherit, + .stderr = .ignore, + }) catch { + break :handle_start_cmd; + }; + + const process_result = process.wait(state.io) catch { + break :handle_start_cmd; + }; + start_cmd_exit_code = process_result.exited; + } // Initialize terminal buffer - const labels_max_length = @max(lang.login.len, lang.password.len); + try state.log_file.info(state.io, "tui", "initializing terminal buffer", .{}); + var labels = [_][]const u8{ + state.lang.login, + state.lang.password, + state.lang.wayland, + state.lang.x11, + state.lang.shell, + state.lang.xinitrc, + state.lang.custom, + }; + state.labels_max_length = maxWidths(&labels); var seed: u64 = undefined; - std.crypto.random.bytes(std.mem.asBytes(&seed)); // Get a random seed for the PRNG (used by animations) + state.io.random(std.mem.asBytes(&seed)); // Get a random seed for the PRNG (used by animations) var prng = std.Random.DefaultPrng.init(seed); const random = prng.random(); - var buffer = TerminalBuffer.init(config, labels_max_length, random); - - // Initialize components - var info_line = InfoLine.init(allocator, &buffer); - defer info_line.deinit(); - - if (config_load_failed) { - // We can't localize this since the config failed to load so we'd fallback to the default language anyway - try info_line.addMessage("unable to parse config file", config.error_bg, config.error_fg); + const buffer_options = TerminalBuffer.InitOptions{ + .fg = state.config.fg, + .bg = state.config.bg, + .border_fg = state.config.border_fg, + .full_color = state.config.full_color, + .is_tty = true, + }; + state.buffer = try TerminalBuffer.init( + state.allocator, + state.io, + buffer_options, + &state.log_file, + random, + ); + defer { + state.log_file.info(state.io, "tui", "shutting down terminal buffer", .{}) catch {}; + state.buffer.deinit(); } - interop.setNumlock(config.numlock) catch { - try info_line.addMessage(lang.err_numlock, config.error_bg, config.error_fg); + const act = std.posix.Sigaction{ + .handler = .{ .handler = &signalHandler }, + .mask = std.posix.sigemptyset(), + .flags = 0, + }; + std.posix.sigaction(std.posix.SIG.TERM, &act, null); + + // Initialize components + state.shutdown_label = Label.init( + "", + null, + state.buffer.fg, + state.buffer.bg, + null, + null, + ); + defer state.shutdown_label.deinit(); + + state.restart_label = Label.init( + "", + null, + state.buffer.fg, + state.buffer.bg, + null, + null, + ); + defer state.restart_label.deinit(); + + state.toggle_password_label = Label.init( + "", + null, + state.buffer.fg, + state.buffer.bg, + null, + null, + ); + defer state.toggle_password_label.deinit(); + + state.brightness_down_label = Label.init( + "", + null, + state.buffer.fg, + state.buffer.bg, + null, + null, + ); + defer state.brightness_down_label.deinit(); + + state.brightness_up_label = Label.init( + "", + null, + state.buffer.fg, + state.buffer.bg, + null, + null, + ); + defer state.brightness_up_label.deinit(); + + if (state.config.shutdown_key) |key| { + try state.shutdown_label.setTextAlloc( + state.allocator, + "{s} {s}", + .{ key, state.lang.shutdown }, + ); + } + if (state.config.restart_key) |key| { + try state.restart_label.setTextAlloc( + state.allocator, + "{s} {s}", + .{ key, state.lang.restart }, + ); + } + if (state.config.show_password_key) |key| { + try state.toggle_password_label.setTextAlloc( + state.allocator, + "{s} {s}", + .{ key, state.lang.toggle_password }, + ); + } + if (state.config.brightness_down_key) |key| { + try state.brightness_down_label.setTextAlloc( + state.allocator, + "{s} {s}", + .{ key, state.lang.brightness_down }, + ); + } + if (state.config.brightness_up_key) |key| { + try state.brightness_up_label.setTextAlloc( + state.allocator, + "{s} {s}", + .{ key, state.lang.brightness_up }, + ); + } + + state.numlock_label = Label.init( + "", + null, + state.buffer.fg, + state.buffer.bg, + &updateNumlock, + null, + ); + defer state.numlock_label.deinit(); + + state.capslock_label = Label.init( + "", + null, + state.buffer.fg, + state.buffer.bg, + &updateCapslock, + null, + ); + defer state.capslock_label.deinit(); + + state.battery_label = Label.init( + "", + null, + state.buffer.fg, + state.buffer.bg, + &updateBattery, + null, + ); + defer state.battery_label.deinit(); + + state.clock_label = Label.init( + "", + null, + state.buffer.fg, + state.buffer.bg, + &updateClock, + &calculateClockTimeout, + ); + defer state.clock_label.deinit(); + + state.tty_label = Label.init( + "", + null, + state.buffer.fg, + state.buffer.bg, + null, + null, + ); + defer state.tty_label.deinit(); + + state.bigclock_label = BigLabel.init( + &state.buffer, + "", + null, + state.buffer.fg, + state.buffer.bg, + switch (state.config.bigclock) { + .none, .en => .en, + .fa => .fa, + }, + &updateBigClock, + &calculateBigClockTimeout, + ); + defer state.bigclock_label.deinit(); + + state.box = Box.init( + &state.buffer, + state.config.margin_box_h, + state.config.margin_box_v, + (2 * state.config.margin_box_h) + state.config.input_len + 1 + state.labels_max_length, + 7 + (2 * state.config.margin_box_v), + !state.config.hide_borders, + state.config.blank_box, + state.config.box_title, + null, + state.buffer.border_fg, + state.buffer.fg, + state.buffer.bg, + &updateBox, + ); + + state.info_line = try InfoLine.init( + state.allocator, + state.io, + &state.buffer, + state.box.width - 2 * state.box.horizontal_margin, + state.buffer.fg, + state.buffer.bg, + ); + defer state.info_line.deinit(); + + try state.buffer.registerKeybind(state.io, &state.info_line.label.keybinds, "H", &viGoLeft, &state); + try state.buffer.registerKeybind(state.io, &state.info_line.label.keybinds, "L", &viGoRight, &state); + + if (maybe_res == null) { + var longest = diag.name.longest(); + if (longest.kind == .positional) + longest.name = diag.arg; + + try state.info_line.addMessage( + state.lang.err_args, + state.config.error_bg, + state.config.error_fg, + ); + try state.log_file.err( + state.io, + "cli", + "unable to parse argument '{s}{s}': {s}", + .{ longest.kind.prefix(), longest.name, @errorName(arg_parse_error) }, + ); + } + + if (maybe_uid_range_error) |err| { + try state.info_line.addMessage( + state.lang.err_uid_range, + state.config.error_bg, + state.config.error_fg, + ); + try state.log_file.err( + state.io, + "sys", + "failed to get uid range: {s}; falling back to default", + .{@errorName(err)}, + ); + } + + if (start_cmd_exit_code != 0) { + try state.info_line.addMessage( + state.lang.err_start, + state.config.error_bg, + state.config.error_fg, + ); + try state.log_file.err( + state.io, + "sys", + "failed to execute start command: exit code {d}", + .{start_cmd_exit_code}, + ); + } + + if (config_parser.maybe_load_error) |load_error| { + // We can't localize this since the config failed to load so we'd fallback to the default language anyway + try state.info_line.addMessage( + "unable to parse config file", + state.config.error_bg, + state.config.error_fg, + ); + try state.log_file.err( + state.io, + "conf", + "unable to parse config file: {s}", + .{@errorName(load_error)}, + ); + + for (config_parser.errors.items) |err| { + try state.log_file.err( + state.io, + "conf", + "failed to convert value '{s}' of option '{s}' to type '{s}': {s}", + .{ err.value, err.key, err.type_name, err.error_name }, + ); + } + } + + if (!state.log_file.could_open_log_file) { + try state.info_line.addMessage( + state.lang.err_log, + state.config.error_bg, + state.config.error_fg, + ); + try state.log_file.err( + state.io, + "sys", + "failed to open log file", + .{}, + ); + } + + interop.setNumlock(state.config.numlock) catch |err| { + try state.info_line.addMessage( + state.lang.err_numlock, + state.config.error_bg, + state.config.error_fg, + ); + try state.log_file.err( + state.io, + "sys", + "failed to set numlock: {s}", + .{@errorName(err)}, + ); }; - var session = Session.init(allocator, &buffer, lang); - defer session.deinit(); + state.session_specifier_label = Label.init( + "", + null, + state.buffer.fg, + state.buffer.bg, + &updateSessionSpecifier, + null, + ); + defer state.session_specifier_label.deinit(); - session.addEnvironment(.{ .Name = lang.shell }, null, .shell) catch { - try info_line.addMessage(lang.err_alloc, config.error_bg, config.error_fg); - }; + state.session = try Session.init( + state.allocator, + state.io, + &state.buffer, + &state.login, + state.box.width - 2 * state.box.horizontal_margin - state.labels_max_length - 1, + state.config.text_in_center, + state.buffer.fg, + state.buffer.bg, + ); + defer state.session.deinit(); + + try state.buffer.registerKeybind(state.io, &state.session.label.keybinds, "H", &viGoLeft, &state); + try state.buffer.registerKeybind(state.io, &state.session.label.keybinds, "L", &viGoRight, &state); + + state.login_label = Label.init( + state.lang.login, + null, + state.buffer.fg, + state.buffer.bg, + null, + null, + ); + defer state.login_label.deinit(); + + state.login = null; + if (!state.config.type_username) { + state.login = try UserList.init( + state.allocator, + state.io, + &state.buffer, + usernames, + &state.saved_users, + &state.session, + state.box.width - 2 * state.box.horizontal_margin - state.labels_max_length - 1, + state.config.text_in_center, + state.buffer.fg, + state.buffer.bg, + ); + + try state.buffer.registerKeybind(state.io, &state.login.?.label.keybinds, "H", &viGoLeft, &state); + try state.buffer.registerKeybind(state.io, &state.login.?.label.keybinds, "L", &viGoRight, &state); + } + defer if (state.login) |*w| w.deinit(); + + if (state.config.shell) { + addOtherEnvironment(&state.session, state.lang, .shell, null) catch |err| { + try state.info_line.addMessage( + state.lang.err_alloc, + state.config.error_bg, + state.config.error_fg, + ); + try state.log_file.err( + state.io, + "sys", + "failed to add shell environment: {s}", + .{@errorName(err)}, + ); + }; + } if (build_options.enable_x11_support) { - if (config.xinitrc) |xinitrc| { - session.addEnvironment(.{ .Name = lang.xinitrc, .Exec = xinitrc }, null, .xinitrc) catch { - try info_line.addMessage(lang.err_alloc, config.error_bg, config.error_fg); + if (state.config.xinitrc) |xinitrc_cmd| { + addOtherEnvironment(&state.session, state.lang, .xinitrc, xinitrc_cmd) catch |err| { + try state.info_line.addMessage( + state.lang.err_alloc, + state.config.error_bg, + state.config.error_fg, + ); + try state.log_file.err( + state.io, + "sys", + "failed to add xinitrc environment: {s}", + .{@errorName(err)}, + ); }; } } else { - try info_line.addMessage(lang.no_x11_support, config.bg, config.fg); + try state.info_line.addMessage( + state.lang.no_x11_support, + state.config.bg, + state.config.fg, + ); + try state.log_file.info( + state.io, + "comp", + "x11 support disabled at compile-time", + .{}, + ); } - if (config.initial_info_text) |text| { - try info_line.addMessage(text, config.bg, config.fg); + var has_crawl_error = false; + + // Crawl session directories (Wayland, X11 and custom respectively) + if (state.config.waylandsessions) |waylandsessions| { + var wayland_session_dirs = std.mem.splitScalar(u8, waylandsessions, ':'); + while (wayland_session_dirs.next()) |dir| { + crawl(&state.session, state.io, state.lang, dir, .wayland) catch |err| { + has_crawl_error = true; + try state.log_file.err( + state.io, + "sys", + "failed to crawl wayland session directory '{s}': {s}", + .{ dir, @errorName(err) }, + ); + }; + } + } + + if (build_options.enable_x11_support) { + if (state.config.xsessions) |xsessions| { + var x_session_dirs = std.mem.splitScalar(u8, xsessions, ':'); + while (x_session_dirs.next()) |dir| { + crawl(&state.session, state.io, state.lang, dir, .x11) catch |err| { + has_crawl_error = true; + try state.log_file.err( + state.io, + "sys", + "failed to crawl x11 session directory '{s}': {s}", + .{ dir, @errorName(err) }, + ); + }; + } + } + } + + var custom_session_dirs = std.mem.splitScalar(u8, state.config.custom_sessions, ':'); + while (custom_session_dirs.next()) |dir| { + crawl(&state.session, state.io, state.lang, dir, .custom) catch |err| { + has_crawl_error = true; + try state.log_file.err( + state.io, + "sys", + "failed to crawl custom session directory '{s}': {s}", + .{ dir, @errorName(err) }, + ); + }; + } + + if (has_crawl_error) { + try state.info_line.addMessage( + state.lang.err_crawl, + state.config.error_bg, + state.config.error_fg, + ); + } + + if (usernames.items.len == 0) { + // If we have no usernames, simply add an error to the info line. + // This effectively means you can't login, since there would be no local + // accounts *and* no root account...but at this point, if that's the + // case, you have bigger problems to deal with in the first place. :D + if (!state.config.type_username) try state.info_line.addMessage(state.lang.err_no_users, state.config.error_bg, state.config.error_fg); + try state.log_file.err(state.io, "sys", "no users found", .{}); + } + + state.password_label = Label.init( + state.lang.password, + null, + state.buffer.fg, + state.buffer.bg, + null, + null, + ); + defer state.password_label.deinit(); + + state.insert_mode = !state.config.vi_mode or state.config.vi_default_mode == .insert; + + state.password = try Text.init( + state.allocator, + state.io, + &state.buffer, + state.insert_mode, + true, + state.config.asterisk, + state.box.width - 2 * state.box.horizontal_margin - state.labels_max_length - 1, + state.buffer.fg, + state.buffer.bg, + ); + defer state.password.deinit(); + + try state.buffer.registerKeybind(state.io, &state.password.keybinds, "H", &viGoLeft, &state); + try state.buffer.registerKeybind(state.io, &state.password.keybinds, "L", &viGoRight, &state); + + state.password_widget = state.password.widget(); + state.login_text = null; + + if (state.config.type_username) { + state.login_text = try Text.init( + state.allocator, + state.io, + &state.buffer, + state.insert_mode, + false, + null, + state.box.width - 2 * state.box.horizontal_margin - state.labels_max_length - 1, + state.buffer.fg, + state.buffer.bg, + ); + } + defer if (state.login_text) |lt| lt.deinit(); + + state.version_label = Label.init( + ly_version_str, + null, + state.buffer.fg, + state.buffer.bg, + null, + null, + ); + defer state.version_label.deinit(); + + state.is_autologin = false; + + check_autologin: { + const auto_user = state.config.auto_login_user orelse break :check_autologin; + const auto_session = state.config.auto_login_session orelse break :check_autologin; + + if (!isValidUsername(auto_user, usernames)) { + try state.info_line.addMessage( + state.lang.err_pam_user_unknown, + state.config.error_bg, + state.config.error_fg, + ); + try state.log_file.err( + state.io, + "auth", + "autologin failed: username '{s}' not found", + .{auto_user}, + ); + break :check_autologin; + } + + const session_index = findSessionByName(&state.session, auto_session) orelse { + try state.log_file.err( + state.io, + "auth", + "autologin failed: session '{s}' not found", + .{auto_session}, + ); + try state.info_line.addMessage( + state.lang.err_autologin_session, + state.config.error_bg, + state.config.error_fg, + ); + break :check_autologin; + }; + try state.log_file.info( + state.io, + "auth", + "attempting autologin for user '{s}' with session '{s}'", + .{ auto_user, auto_session }, + ); + + state.session.label.current = session_index; + state.is_autologin = true; + + if (state.login_text) |box| { + try box.writeText(auto_user); + } else { + for (state.login.?.label.list.items, 0..) |username, i| { + if (std.mem.eql(u8, username.name, auto_user)) { + state.login.?.label.current = i; + break; + } + } + } + } + + // Switch to selected TTY + state.active_tty = interop.getActiveTty(state.allocator, state.io, state.use_kmscon_vt) catch |err| no_tty_found: { + try state.info_line.addMessage( + state.lang.err_get_active_tty, + state.config.error_bg, + state.config.error_fg, + ); + try state.log_file.err( + state.io, + "sys", + "failed to get active tty: {s}", + .{@errorName(err)}, + ); + break :no_tty_found build_options.fallback_tty; + }; + if (!state.use_kmscon_vt) { + interop.switchTty(state.active_tty) catch |err| { + try state.info_line.addMessage( + state.lang.err_switch_tty, + state.config.error_bg, + state.config.error_fg, + ); + try state.log_file.err( + state.io, + "sys", + "failed to switch to tty {d}: {s}", + .{ state.active_tty, @errorName(err) }, + ); + }; + } + + try state.tty_label.setTextBuf(&state.tty_buf, "tty{d}", .{state.active_tty}); + + // Initialize the animation, if any + var animation: ?*Widget = null; + switch (state.config.animation) { + .none => {}, + .doom => { + var doom = try Doom.init( + state.allocator, + &state.buffer, + state.config.doom_top_color, + state.config.doom_middle_color, + state.config.doom_bottom_color, + state.config.doom_fire_height, + state.config.doom_fire_spread, + &state.animate, + state.config.animation_timeout_sec, + state.config.animation_frame_delay, + ); + animation = doom.widget(); + }, + .matrix => { + var matrix = try Matrix.init( + state.allocator, + &state.buffer, + state.config.cmatrix_fg, + state.config.cmatrix_head_col, + state.config.cmatrix_min_codepoint, + state.config.cmatrix_max_codepoint, + &state.animate, + state.config.animation_timeout_sec, + state.config.animation_frame_delay, + ); + animation = matrix.widget(); + }, + .colormix => { + var color_mix = try ColorMix.init( + &state.buffer, + state.config.colormix_col1, + state.config.colormix_col2, + state.config.colormix_col3, + &state.animate, + state.config.animation_timeout_sec, + state.config.animation_frame_delay, + ); + animation = color_mix.widget(); + }, + .gameoflife => { + var game_of_life = try GameOfLife.init( + state.allocator, + &state.buffer, + state.config.gameoflife_fg, + state.config.gameoflife_entropy_interval, + state.config.gameoflife_frame_delay, + state.config.gameoflife_initial_density, + &state.animate, + state.config.animation_timeout_sec, + state.config.animation_frame_delay, + ); + animation = game_of_life.widget(); + }, + .dur_file => { + var dur = try DurFile.init( + state.allocator, + state.io, + &state.buffer, + &state.log_file, + state.config.dur_file_path, + state.config.dur_offset_alignment, + state.config.dur_x_offset, + state.config.dur_y_offset, + state.config.full_color, + &state.animate, + state.config.animation_timeout_sec, + state.config.animation_frame_delay, + ); + animation = dur.widget(); + }, + .lua => { + var lua = try Lua.init( + state.io, + state.allocator, + &state.log_file, + &state.buffer, + state.config.lua_animation_file, + state.config.edge_margin, + state.config.animation_frame_delay, + &state.info_line, + state.config.error_fg, + state.config.error_bg, + state.lang, + state.config.full_color, + ); + animation = lua.widget(); + }, + } + defer if (animation) |a| a.deinit(); + + var cascade = Cascade.init( + state.io, + &state.buffer, + &state.auth_fails, + state.config.auth_fails, + ); + + state.auth_fails = 0; + state.animate = state.config.animation != .none; + state.edge_margin = Position.init( + state.config.edge_margin, + state.config.edge_margin, + ); + + // Load last saved username and desktop selection, if any + // Skip if autologin is active to prevent overriding autologin session + var default_input = state.config.default_input; + + if (state.config.save_file_dir != null and !state.is_autologin) { + if (state.login_text) |box| { + if (state.saved_username) |username| { + defer state.allocator.free(username); + + try box.writeText(username); + + default_input = .password; + + for (state.saved_users.user_list.items) |user| { + if (std.mem.eql(u8, username, user.username)) { + state.session.label.current = @min(user.session_index, state.session.label.list.items.len - 1); + break; + } + } + } + } else if (state.saved_users.last_username_index) |index| load_last_user: { + // If the saved index isn't valid, bail out + if (index >= state.saved_users.user_list.items.len) break :load_last_user; + + const user = state.saved_users.user_list.items[index]; + + // Find user with saved name, and switch over to it + // If it doesn't exist (anymore), we don't change the value + for (usernames.items, 0..) |username, i| { + if (std.mem.eql(u8, username, user.username)) { + state.login.?.label.current = i; + break; + } + } + + default_input = .password; + + state.session.label.current = @min(user.session_index, state.session.label.list.items.len - 1); + } + } + + const info_line_widget = state.info_line.widget(); + const session_widget = state.session.widget(); + const login_widget = if (state.config.type_username) state.login_text.?.widget() else state.login.?.widget(); + + var widgets: std.ArrayList([]*Widget) = .empty; + defer widgets.deinit(state.allocator); + + // Layer 1 + if (animation) |a| { + var layer1 = [_]*Widget{a}; + try widgets.append(state.allocator, &layer1); + } + + // Layer 2 + var layer2: std.ArrayList(*Widget) = .empty; + defer layer2.deinit(state.allocator); + + state.custom_binds = .empty; + defer state.custom_binds.deinit(state.allocator); + defer for (state.custom_binds.items) |*i| { + i.lbl.deinit(); + }; + + state.custom_info = .empty; + defer state.custom_info.deinit(state.allocator); + defer for (state.custom_info.items) |*item| { + item.lbl.deinit(); + }; + + var lblIter = custom.labels.iterator(); + // NOTE: Because widgets have a pointer to the underlying Label, we have to ensure + // that the ArrayList doesn't allocate more memory than what we ensured. Otherwise + // the pointer to the Label becomes invalid. + try state.custom_info.ensureTotalCapacity(state.allocator, @intCast(custom.labels.count())); + while (lblIter.next()) |i| { + try state.custom_info.append(state.allocator, .{ + .info = i.value_ptr.*, + .lbl = .init("", null, state.buffer.fg, state.buffer.bg, updateCustomInfo, null), + }); + var latest = &state.custom_info.items[state.custom_info.items.len - 1]; + latest.info.id = latest.lbl.widget().id; + latest.info.counter = 1; + } + + var iter = custom.binds.iterator(); + while (iter.next()) |i| { + var concat = try std.mem.concat(state.allocator, u8, &[_][]const u8{ i.key_ptr.*, " ", i.value_ptr.name }); + inline for (@typeInfo(Lang).@"struct".fields) |lang_key| { + const new = try std.mem.replaceOwned(u8, state.allocator, concat, "$" ++ lang_key.name, @field(state.lang, lang_key.name)); + state.allocator.free(concat); + concat = new; + } + try state.custom_binds.append(state.allocator, .{ + .lbl = .init( + concat, + null, + state.buffer.fg, + state.buffer.bg, + null, + null, + ), + .cmd = i.value_ptr.*, + .key = i.key_ptr.*, + .io = state.io, + }); + state.custom_binds.items[state.custom_binds.items.len - 1].lbl.allocator = state.allocator; + } + try layer2.append(state.allocator, state.shutdown_label.widget()); + try layer2.append(state.allocator, state.restart_label.widget()); + try layer2.append(state.allocator, state.toggle_password_label.widget()); + try layer2.append(state.allocator, state.brightness_down_label.widget()); + try layer2.append(state.allocator, state.brightness_up_label.widget()); + try layer2.append(state.allocator, state.battery_label.widget()); + try layer2.append(state.allocator, state.clock_label.widget()); + try layer2.append(state.allocator, state.tty_label.widget()); + try layer2.append(state.allocator, state.bigclock_label.widget()); + try layer2.append(state.allocator, state.numlock_label.widget()); + try layer2.append(state.allocator, state.capslock_label.widget()); + try layer2.append(state.allocator, state.box.widget()); + try layer2.append(state.allocator, info_line_widget); + try layer2.append(state.allocator, state.session_specifier_label.widget()); + try layer2.append(state.allocator, session_widget); + try layer2.append(state.allocator, state.login_label.widget()); + try layer2.append(state.allocator, login_widget); + try layer2.append(state.allocator, state.password_label.widget()); + try layer2.append(state.allocator, state.password_widget); + try layer2.append(state.allocator, state.version_label.widget()); + + for (state.custom_binds.items) |*item| { + try layer2.append(state.allocator, item.lbl.widget()); + } + for (state.custom_info.items) |*item| { + try layer2.append(state.allocator, item.lbl.widget()); + } + + try widgets.append(state.allocator, layer2.items); + + // Layer 3 + if (state.config.auth_fails > 0) { + var layer3 = [_]*Widget{cascade.widget()}; + try widgets.append(state.allocator, &layer3); + } + + for (state.custom_binds.items) |*item| { + try state.buffer.registerGlobalKeybind(state.io, item.key, &customCommand, item); + } + + try state.buffer.registerGlobalKeybind(state.io, "Esc", &disableInsertMode, &state); + try state.buffer.registerGlobalKeybind(state.io, "I", &enableInsertMode, &state); + + try state.buffer.registerGlobalKeybind(state.io, "Ctrl+C", &quit, &state); + + try state.buffer.registerGlobalKeybind(state.io, "K", &viMoveCursorUp, &state); + try state.buffer.registerGlobalKeybind(state.io, "J", &viMoveCursorDown, &state); + + try state.buffer.registerGlobalKeybind(state.io, "Enter", &authenticate, &state); + + if (state.config.shutdown_key) |key| try state.buffer.registerGlobalKeybind(state.io, key, &shutdownCmd, &state); + if (state.config.restart_key) |key| try state.buffer.registerGlobalKeybind(state.io, key, &restartCmd, &state); + if (state.config.show_password_key) |key| try state.buffer.registerGlobalKeybind(state.io, key, &togglePasswordMask, &state); + if (state.config.brightness_down_key) |key| try state.buffer.registerGlobalKeybind(state.io, key, &decreaseBrightnessCmd, &state); + if (state.config.brightness_up_key) |key| try state.buffer.registerGlobalKeybind(state.io, key, &increaseBrightnessCmd, &state); + + if (state.config.initial_info_text) |text| { + try state.info_line.addMessage(text, state.config.bg, state.config.fg); } else get_host_name: { // Initialize information line with host name var name_buf: [std.posix.HOST_NAME_MAX]u8 = undefined; - const hostname = std.posix.gethostname(&name_buf) catch { - try info_line.addMessage(lang.err_hostname, config.error_bg, config.error_fg); + const hostname = std.posix.gethostname(&name_buf) catch |err| { + try state.info_line.addMessage( + state.lang.err_hostname, + state.config.error_bg, + state.config.error_fg, + ); + try state.log_file.err( + state.io, + "sys", + "failed to get hostname: {s}", + .{@errorName(err)}, + ); break :get_host_name; }; - try info_line.addMessage(hostname, config.bg, config.fg); + try state.info_line.addMessage( + hostname, + state.config.bg, + state.config.fg, + ); } - try session.crawl(config.waylandsessions, .wayland); - if (build_options.enable_x11_support) try session.crawl(config.xsessions, .x11); + if (state.is_autologin) _ = try authenticate(&state); - var login = Text.init(allocator, &buffer, false, null); - defer login.deinit(); - - var password = Text.init(allocator, &buffer, true, config.asterisk); - defer password.deinit(); - - var active_input = config.default_input; - var insert_mode = !config.vi_mode or config.vi_default_mode == .insert; - - // Load last saved username and desktop selection, if any - if (config.load) { - if (save.user) |user| { - try login.text.appendSlice(user); - login.end = user.len; - login.cursor = login.end; - active_input = .password; - } - - if (save.session_index) |session_index| { - if (session_index < session.label.list.items.len) session.label.current = session_index; - } - } - - // Place components on the screen - { - buffer.drawBoxCenter(!config.hide_borders, config.blank_box); - - const coordinates = buffer.calculateComponentCoordinates(); - info_line.label.position(coordinates.start_x, coordinates.y, coordinates.full_visible_length, null); - session.label.position(coordinates.x, coordinates.y + 2, coordinates.visible_length, config.text_in_center); - login.position(coordinates.x, coordinates.y + 4, coordinates.visible_length); - password.position(coordinates.x, coordinates.y + 6, coordinates.visible_length); - - switch (active_input) { - .info_line => info_line.label.handle(null, insert_mode), - .session => session.label.handle(null, insert_mode), - .login => login.handle(null, insert_mode) catch { - try info_line.addMessage(lang.err_alloc, config.error_bg, config.error_fg); - }, - .password => password.handle(null, insert_mode) catch { - try info_line.addMessage(lang.err_alloc, config.error_bg, config.error_fg); - }, - } - } - - // Initialize the animation, if any - var doom: Doom = undefined; - var matrix: Matrix = undefined; - - switch (config.animation) { - .none => {}, - .doom => doom = try Doom.init(allocator, &buffer), - .matrix => matrix = try Matrix.init(allocator, &buffer, config.cmatrix_fg), - } - defer { - switch (config.animation) { - .none => {}, - .doom => doom.deinit(), - .matrix => matrix.deinit(), - } - } - - const animate = config.animation != .none; - const shutdown_key = try std.fmt.parseInt(u8, config.shutdown_key[1..], 10); - const shutdown_len = try utils.strWidth(lang.shutdown); - const restart_key = try std.fmt.parseInt(u8, config.restart_key[1..], 10); - const restart_len = try utils.strWidth(lang.restart); - const sleep_key = try std.fmt.parseInt(u8, config.sleep_key[1..], 10); - const sleep_len = try utils.strWidth(lang.sleep); - const brightness_down_key = try std.fmt.parseInt(u8, config.brightness_down_key[1..], 10); - const brightness_down_len = try utils.strWidth(lang.brightness_down); - const brightness_up_key = try std.fmt.parseInt(u8, config.brightness_up_key[1..], 10); - const brightness_up_len = try utils.strWidth(lang.brightness_up); - - var event: termbox.tb_event = undefined; - var run = true; - var update = true; - var resolution_changed = false; - var auth_fails: u64 = 0; - - // Switch to selected TTY if possible - interop.switchTty(config.console_dev, config.tty) catch { - try info_line.addMessage(lang.err_console_dev, config.error_bg, config.error_fg); + const active_widget = switch (default_input) { + .info_line => info_line_widget, + .session => session_widget, + .login => login_widget, + .password => state.password_widget, }; - while (run) { - // If there's no input or there's an animation, a resolution change needs to be checked - if (!update or config.animation != .none) { - if (!update) std.time.sleep(std.time.ns_per_ms * 100); + var shared_error = try SharedError.init(&uiErrorHandler, &state); + defer shared_error.deinit(); - _ = termbox.tb_present(); // Required to update tb_width(), tb_height() and tb_cell_buffer() + try state.buffer.runEventLoop( + state.allocator, + state.io, + shared_error, + widgets.items, + active_widget, + state.config.inactivity_delay, + positionWidgets, + handleInactivity, + &state, + ); +} - const width: usize = @intCast(termbox.tb_width()); - const height: usize = @intCast(termbox.tb_height()); +fn maxWidths(labels: [][]const u8) usize { + var max_width: usize = 0; - if (width != buffer.width or height != buffer.height) { - // If it did change, then update the cell buffer, reallocate the current animation's buffers, and force a draw update + for (labels) |label| { + max_width = @max(max_width, TerminalBuffer.strWidth(label)); + } - buffer.width = width; - buffer.height = height; - buffer.buffer = termbox.tb_cell_buffer(); + return max_width; +} - switch (config.animation) { - .none => {}, - .doom => doom.realloc() catch { - try info_line.addMessage(lang.err_alloc, config.error_bg, config.error_fg); - }, - .matrix => matrix.realloc() catch { - try info_line.addMessage(lang.err_alloc, config.error_bg, config.error_fg); - }, - } +fn uiErrorHandler(err: anyerror, ctx: *anyopaque) anyerror!void { + var state: *UiState = @ptrCast(@alignCast(ctx)); - update = true; - resolution_changed = true; - } + switch (err) { + error.SetCursorFailed => { + try state.info_line.addMessage( + state.lang.err_alloc, + state.config.error_bg, + state.config.error_fg, + ); + }, + error.WidgetReallocationFailed => { + try state.info_line.addMessage( + state.lang.err_alloc, + state.config.error_bg, + state.config.error_fg, + ); + }, + error.CurrentWidgetHandlingFailed => { + try state.info_line.addMessage( + state.lang.err_alloc, + state.config.error_bg, + state.config.error_fg, + ); + }, + else => unreachable, + } +} + +fn disableInsertMode(ptr: *anyopaque) !bool { + var state: *UiState = @ptrCast(@alignCast(ptr)); + + if (state.config.vi_mode and state.insert_mode) { + state.insert_mode = false; + state.password.should_insert = false; + if (state.login_text) |lt| lt.should_insert = false; + state.buffer.drawNextFrame(true); + } + return false; +} + +fn enableInsertMode(ptr: *anyopaque) !bool { + var state: *UiState = @ptrCast(@alignCast(ptr)); + if (state.insert_mode) return true; + + state.insert_mode = true; + state.password.should_insert = true; + if (state.login_text) |lt| lt.should_insert = true; + state.buffer.drawNextFrame(true); + return false; +} + +fn viGoLeft(ptr: *anyopaque) !bool { + var state: *UiState = @ptrCast(@alignCast(ptr)); + if (state.insert_mode) return true; + + return try state.buffer.simulateKeybind(state.io, "Left"); +} + +fn viGoRight(ptr: *anyopaque) !bool { + var state: *UiState = @ptrCast(@alignCast(ptr)); + if (state.insert_mode) return true; + + return try state.buffer.simulateKeybind(state.io, "Right"); +} + +fn viMoveCursorUp(ptr: *anyopaque) !bool { + var state: *UiState = @ptrCast(@alignCast(ptr)); + if (state.insert_mode) return true; + + return try state.buffer.simulateKeybind(state.io, "Up"); +} + +fn viMoveCursorDown(ptr: *anyopaque) !bool { + var state: *UiState = @ptrCast(@alignCast(ptr)); + if (state.insert_mode) return true; + + return try state.buffer.simulateKeybind(state.io, "Down"); +} + +fn togglePasswordMask(ptr: *anyopaque) !bool { + var state: *UiState = @ptrCast(@alignCast(ptr)); + + state.password.toggleMask(); + state.buffer.drawNextFrame(true); + return false; +} + +fn quit(ptr: *anyopaque) !bool { + var state: *UiState = @ptrCast(@alignCast(ptr)); + + state.buffer.stopEventLoop(); + return false; +} + +fn customCommand(ptr: *anyopaque) !bool { + const lbl: *CustomBindLabel = @ptrCast(@alignCast(ptr)); + var proc = std.process.spawn(lbl.io, .{ + .argv = &[_][]const u8{ "/bin/sh", "-c", lbl.cmd.cmd }, + .stdout = .ignore, + .stderr = .ignore, + }) catch return false; + + const res = proc.wait(lbl.io) catch return false; + if (res.exited != 0) return error.CommandFailed; + return false; +} + +fn authenticate(ptr: *anyopaque) !bool { + var state: *UiState = @ptrCast(@alignCast(ptr)); + + try state.log_file.info(state.io, "auth", "starting authentication", .{}); + + if (!state.config.allow_empty_password and state.password.text.items.len == 0) { + // Let's not log this message for security reasons + try state.info_line.addMessage( + state.lang.err_empty_password, + state.config.error_bg, + state.config.error_fg, + ); + if (!state.is_autologin) { + state.info_line.clearRendered(state.allocator) catch |err| { + try state.info_line.addMessage( + state.lang.err_alloc, + state.config.error_bg, + state.config.error_fg, + ); + try state.log_file.err( + state.io, + "tui", + "failed to clear info line: {s}", + .{@errorName(err)}, + ); + }; + state.info_line.label.draw(); + try TerminalBuffer.presentBuffer(); + } + return false; + } + + try state.info_line.addMessage( + state.lang.authenticating, + state.config.bg, + state.config.fg, + ); + if (!state.is_autologin) { + state.info_line.clearRendered(state.allocator) catch |err| { + try state.info_line.addMessage( + state.lang.err_alloc, + state.config.error_bg, + state.config.error_fg, + ); + try state.log_file.err( + state.io, + "tui", + "failed to clear info line: {s}", + .{@errorName(err)}, + ); + }; + state.info_line.label.draw(); + try TerminalBuffer.presentBuffer(); + } + + if (state.config.save_file_dir != null) save_last_settings: { + // It isn't worth cluttering the code with precise error + // handling, so let's just report a generic error message, + // that should be good enough for debugging anyway. + errdefer state.log_file.err( + state.io, + "conf", + "failed to save current user data", + .{}, + ) catch {}; + + var file = std.Io.Dir.cwd().createFile(state.io, state.save_path, .{}) catch |err| { + state.log_file.err( + state.io, + "sys", + "failed to create save file: {s}", + .{@errorName(err)}, + ) catch break :save_last_settings; + break :save_last_settings; + }; + defer file.close(state.io); + + var file_buffer: [256]u8 = undefined; + var file_writer = file.writer(state.io, &file_buffer); + var writer = &file_writer.interface; + + if (state.login_text) |box| { + try writer.print("0-{s}\n", .{box.text.items}); + } else { + try writer.print("{d}\n", .{state.login.?.label.current}); + } + for (state.saved_users.user_list.items) |user| { + try writer.print("{s}:{d}\n", .{ user.username, user.session_index }); + } + try writer.flush(); + + // Delete previous save file if it exists + if (migrator.maybe_save_file) |path| { + defer temporary_allocator.free(path); + std.Io.Dir.cwd().deleteFile(state.io, path) catch {}; + } else if (state.has_old_save) { + std.Io.Dir.cwd().deleteFile(state.io, state.old_save_path) catch {}; + } + } + + var shared_err = try SharedError.init(null, null); + defer shared_err.deinit(); + + { + state.log_file.deinit(state.io); + + session_pid = std.posix.system.fork(); + if (session_pid == 0) { + const current_environment = state.session.label.list.items[state.session.label.current].environment; + + // Use auto_login_service for autologin, otherwise use configured service + const service_name = if (state.is_autologin) state.config.auto_login_service else state.config.service_name; + const password_text = if (state.is_autologin) "" else state.password.text.items; + + const auth_options = auth.AuthOptions{ + .tty = state.active_tty, + .service_name = service_name, + .path = state.config.path, + .session_log = state.config.session_log, + .xauth_cmd = state.config.xauth_cmd, + .setup_cmd = state.config.setup_cmd, + .login_cmd = state.config.login_cmd, + .x_cmd = state.config.x_cmd, + .x_vt = state.config.x_vt, + .session_pid = session_pid, + .use_kmscon_vt = state.use_kmscon_vt, + }; + + // Signal action to give up control on the TTY + const tty_control_transfer_act = std.posix.Sigaction{ + .handler = .{ .handler = &ttyControlTransferSignalHandler }, + .mask = std.posix.sigemptyset(), + .flags = 0, + }; + std.posix.sigaction(std.posix.SIG.INT, &tty_control_transfer_act, null); + + try state.log_file.reinit(state.io); + + auth.authenticate( + state.allocator, + state.io, + &state.log_file, + auth_options, + current_environment, + if (state.login_text) |box| box.text.items else state.login.?.getCurrentUsername(), + password_text, + ) catch |err| { + shared_err.writeError(err); + + state.log_file.deinit(state.io); + std.process.exit(1); + }; + + state.log_file.deinit(state.io); + std.process.exit(0); } - if (update) { - // If the user entered a wrong password 10 times in a row, play a cascade animation, else update normally - if (auth_fails < config.auth_fails) { - _ = termbox.tb_clear(); + var session_status: c_int = undefined; + _ = std.posix.system.waitpid(session_pid, &session_status, 0); + // HACK: It seems like the session process is not exiting immediately after the waitpid call. + // This is a workaround to ensure the session process has exited before re-initializing the TTY. + state.io.sleep(.fromSeconds(1), .real) catch {}; + session_pid = -1; - if (!animation_timed_out) { - switch (config.animation) { - .none => {}, - .doom => doom.draw(), - .matrix => matrix.draw(), - } - } + try state.log_file.reinit(state.io); + } - if (config.bigclock != .none and buffer.box_height + (bigclock.HEIGHT + 2) * 2 < buffer.height) draw_big_clock: { - const format = "%H:%M"; - const xo = buffer.width / 2 - @min(buffer.width, (format.len * (bigclock.WIDTH + 1))) / 2; - const yo = (buffer.height - buffer.box_height) / 2 - bigclock.HEIGHT - 2; + try state.buffer.reclaim(); - var clock_buf: [format.len + 1:0]u8 = undefined; - const clock_str = interop.timeAsString(&clock_buf, format) catch { - break :draw_big_clock; - }; + const auth_err = shared_err.readError(); + if (auth_err) |err| { + state.auth_fails += 1; + state.buffer.setActiveWidget(state.password_widget); - for (clock_str, 0..) |c, i| { - const clock_cell = bigclock.clockCell(animate, c, buffer.fg, buffer.bg, config.bigclock); - bigclock.alphaBlit(xo + i * (bigclock.WIDTH + 1), yo, buffer.width, buffer.height, clock_cell); - } - } + try state.info_line.addMessage( + getAuthErrorMsg(err, state.lang), + state.config.error_bg, + state.config.error_fg, + ); + try state.log_file.err( + state.io, + "auth", + "failed to authenticate: {s}", + .{@errorName(err)}, + ); - buffer.drawBoxCenter(!config.hide_borders, config.blank_box); + if (state.config.clear_password or err != error.PamAuthError) state.password.clear(); + } else { + if (state.config.logout_cmd) |logout_cmd| execute_cmd: { + var process = std.process.spawn(state.io, .{ + .argv = &[_][]const u8{ "/bin/sh", "-c", logout_cmd }, + }) catch break :execute_cmd; + _ = process.wait(state.io) catch {}; + } - if (resolution_changed) { - const coordinates = buffer.calculateComponentCoordinates(); - info_line.label.position(coordinates.start_x, coordinates.y, coordinates.full_visible_length, null); - session.label.position(coordinates.x, coordinates.y + 2, coordinates.visible_length, config.text_in_center); - login.position(coordinates.x, coordinates.y + 4, coordinates.visible_length); - password.position(coordinates.x, coordinates.y + 6, coordinates.visible_length); + state.password.clear(); + state.is_autologin = false; + try state.info_line.addMessage( + state.lang.logout, + state.config.bg, + state.config.fg, + ); + try state.log_file.info(state.io, "auth", "logged out", .{}); + } - resolution_changed = false; - } + if (state.config.auth_fails == 0 or state.auth_fails < state.config.auth_fails) { + try TerminalBuffer.clearScreen(true); + state.buffer.drawNextFrame(true); + } - switch (active_input) { - .info_line => info_line.label.handle(null, insert_mode), - .session => session.label.handle(null, insert_mode), - .login => login.handle(null, insert_mode) catch { - try info_line.addMessage(lang.err_alloc, config.error_bg, config.error_fg); - }, - .password => password.handle(null, insert_mode) catch { - try info_line.addMessage(lang.err_alloc, config.error_bg, config.error_fg); - }, - } + // Restore the cursor + try TerminalBuffer.setCursor(0, 0); + try TerminalBuffer.presentBuffer(); + return false; +} - if (config.clock) |clock| draw_clock: { - var clock_buf: [32:0]u8 = undefined; - const clock_str = interop.timeAsString(&clock_buf, clock) catch { - break :draw_clock; - }; +fn shutdownCmd(ptr: *anyopaque) !bool { + var state: *UiState = @ptrCast(@alignCast(ptr)); - if (clock_str.len == 0) return error.FormattedTimeEmpty; + shutdown = true; + state.buffer.stopEventLoop(); + return false; +} - buffer.drawLabel(clock_str, buffer.width - @min(buffer.width, clock_str.len), 0); - } +fn restartCmd(ptr: *anyopaque) !bool { + var state: *UiState = @ptrCast(@alignCast(ptr)); - const label_x = buffer.box_x + buffer.margin_box_h; - const label_y = buffer.box_y + buffer.margin_box_v; + restart = true; + state.buffer.stopEventLoop(); + return false; +} - buffer.drawLabel(lang.login, label_x, label_y + 4); - buffer.drawLabel(lang.password, label_x, label_y + 6); +fn decreaseBrightnessCmd(ptr: *anyopaque) !bool { + var state: *UiState = @ptrCast(@alignCast(ptr)); - info_line.label.draw(); + adjustBrightness(state.io, state.config.brightness_down_cmd) catch |err| { + try state.info_line.addMessage( + state.lang.err_brightness_change, + state.config.error_bg, + state.config.error_fg, + ); + try state.log_file.err( + state.io, + "sys", + "failed to decrease brightness: {s}", + .{@errorName(err)}, + ); + }; + return false; +} - if (!config.hide_key_hints) { - var length: usize = 0; +fn increaseBrightnessCmd(ptr: *anyopaque) !bool { + var state: *UiState = @ptrCast(@alignCast(ptr)); - buffer.drawLabel(config.shutdown_key, length, 0); - length += config.shutdown_key.len + 1; - buffer.drawLabel(" ", length - 1, 0); + adjustBrightness(state.io, state.config.brightness_up_cmd) catch |err| { + try state.info_line.addMessage( + state.lang.err_brightness_change, + state.config.error_bg, + state.config.error_fg, + ); + try state.log_file.err( + state.io, + "sys", + "failed to increase brightness: {s}", + .{@errorName(err)}, + ); + }; + return false; +} - buffer.drawLabel(lang.shutdown, length, 0); - length += shutdown_len + 1; +fn updateNumlock(self: *Label, ptr: *anyopaque) !void { + var state: *UiState = @ptrCast(@alignCast(ptr)); - buffer.drawLabel(config.restart_key, length, 0); - length += config.restart_key.len + 1; - buffer.drawLabel(" ", length - 1, 0); + const lock_state = interop.getLockState() catch |err| { + self.update_fn = null; + try state.info_line.addMessage( + state.lang.err_lock_state, + state.config.error_bg, + state.config.error_fg, + ); + try state.log_file.err( + state.io, + "sys", + "failed to get lock state for numlock: {s}", + .{@errorName(err)}, + ); + return; + }; - buffer.drawLabel(lang.restart, length, 0); - length += restart_len + 1; + self.setText(if (lock_state.numlock) state.lang.numlock else ""); +} - if (config.sleep_cmd != null) { - buffer.drawLabel(config.sleep_key, length, 0); - length += config.sleep_key.len + 1; - buffer.drawLabel(" ", length - 1, 0); +fn updateCapslock(self: *Label, ptr: *anyopaque) !void { + var state: *UiState = @ptrCast(@alignCast(ptr)); - buffer.drawLabel(lang.sleep, length, 0); - length += sleep_len + 1; - } + const lock_state = interop.getLockState() catch |err| { + self.update_fn = null; + try state.info_line.addMessage( + state.lang.err_lock_state, + state.config.error_bg, + state.config.error_fg, + ); + try state.log_file.err( + state.io, + "sys", + "failed to get lock state for capslock: {s}", + .{@errorName(err)}, + ); + return; + }; - buffer.drawLabel(config.brightness_down_key, length, 0); - length += config.brightness_down_key.len + 1; - buffer.drawLabel(" ", length - 1, 0); + self.setText(if (lock_state.capslock) state.lang.capslock else ""); +} - buffer.drawLabel(lang.brightness_down, length, 0); - length += brightness_down_len + 1; +fn updateBattery(self: *Label, ptr: *anyopaque) !void { + var state: *UiState = @ptrCast(@alignCast(ptr)); - buffer.drawLabel(config.brightness_up_key, length, 0); - length += config.brightness_up_key.len + 1; - buffer.drawLabel(" ", length - 1, 0); + if (state.config.battery_id) |id| { + const battery_percentage = getBatteryPercentage(state.io, id) catch |err| { + self.update_fn = null; + try state.log_file.err( + state.io, + "sys", + "failed to get battery percentage: {s}", + .{@errorName(err)}, + ); + try state.info_line.addMessage( + state.lang.err_battery, + state.config.error_bg, + state.config.error_fg, + ); + return; + }; - buffer.drawLabel(lang.brightness_up, length, 0); - length += brightness_up_len + 1; - } + try self.setTextBuf( + &state.battery_buf, + "BAT: {d}%", + .{battery_percentage}, + ); + } +} - if (config.box_title) |title| { - buffer.drawConfinedLabel(title, buffer.box_x, buffer.box_y - 1, buffer.box_width); - } +fn updateClock(self: *Label, ptr: *anyopaque) !void { + var state: *UiState = @ptrCast(@alignCast(ptr)); - if (config.vi_mode) { - const label_txt = if (insert_mode) lang.insert else lang.normal; - buffer.drawLabel(label_txt, buffer.box_x, buffer.box_y + buffer.box_height); - } + if (state.config.clock) |clock| draw_clock: { + const clock_str = interop.timeAsString(state.io, &state.clock_buf, clock); - draw_lock_state: { - const lock_state = interop.getLockState(config.console_dev) catch { - try info_line.addMessage(lang.err_console_dev, config.error_bg, config.error_fg); - break :draw_lock_state; - }; + if (clock_str.len == 0) { + self.update_fn = null; + try state.info_line.addMessage( + state.lang.err_clock_too_long, + state.config.error_bg, + state.config.error_fg, + ); + try state.log_file.err( + state.io, + "tui", + "clock string too long", + .{}, + ); + break :draw_clock; + } - var lock_state_x = buffer.width - @min(buffer.width, lang.numlock.len); - const lock_state_y: usize = if (config.clock != null) 1 else 0; + self.setText(clock_str); + } +} - if (lock_state.numlock) buffer.drawLabel(lang.numlock, lock_state_x, lock_state_y); +fn updateCustomInfo(lbl: *Label, ptr: *anyopaque) !void { + const state: *UiState = @ptrCast(@alignCast(ptr)); + const wid = lbl.widget().id; - if (lock_state_x >= lang.capslock.len + 1) { - lock_state_x -= lang.capslock.len + 1; - if (lock_state.capslock) buffer.drawLabel(lang.capslock, lock_state_x, lock_state_y); - } - } + for (state.custom_info.items) |*i| { + if (i.info.id != wid) continue; + // Here, a counter ticks down every time `updateCustomInfo` runs on that + // particular label. It will only run the command and update the label + // once it reaches to 1. If a refresh value is defined it's then reset to + // that refresh value. + if (i.info.counter == 1) { + var c = try std.process.spawn(state.io, .{ + .argv = &[_][]const u8{ "/bin/sh", "-c", i.info.cmd orelse custom.UNDEFINED_CMD }, + .stdout = .pipe, + .stderr = .pipe, + }); - session.label.draw(); - login.draw(); - password.draw(); + var stdout_buffer: [1024]u8 = undefined; + var stdout_file_reader = c.stdout.?.reader(state.io, &stdout_buffer); + + const stdout = stdout_file_reader.interface.allocRemaining(state.allocator, .limited(state.buffer.width)) catch alloc_error: { + break :alloc_error try std.fmt.allocPrint(state.allocator, "{s}: [{s}]", .{ i.info.name, state.lang.custom_info_err_output_long }); + }; + defer state.allocator.free(stdout); + + var cur_stdout = stdout; + const newline_index = std.mem.indexOfAny(u8, stdout, "\n"); + if (newline_index) |idx| cur_stdout = stdout[0..idx]; + + _ = try c.wait(state.io); + + // Sometimes, the output of a command would have an unprintable character at + // the end of its output, causing '�' (U+FFFD) to appear in its place. Here, we check + // if this is the case and remove it. + if (cur_stdout.len != 0 and !std.ascii.isPrint(cur_stdout[cur_stdout.len - 1])) { + cur_stdout = cur_stdout[0 .. cur_stdout.len - 1]; + } + + state.allocator.free(lbl.text); + if (cur_stdout.len == 0) { + const stderr_length = try c.stderr.?.length(state.io); + try lbl.setTextAlloc(state.allocator, "{s}: [{s}{s}]", .{ i.info.name, state.lang.custom_info_err_no_output, if (stderr_length > 0) state.lang.custom_info_err_no_output_error else "" }); } else { - std.time.sleep(std.time.ns_per_ms * 10); - update = buffer.cascade(); - - if (!update) { - std.time.sleep(std.time.ns_per_s * 7); - auth_fails = 0; - } + try lbl.setTextAlloc(state.allocator, "{s}", .{cur_stdout}); } - _ = termbox.tb_present(); + // Called to re-position the widgets after they receive their output. + try positionWidgets(state); + if (i.info.refresh != 0) + i.info.counter = i.info.refresh; } + if (i.info.counter != 0) + i.info.counter -= 1; + } +} - var timeout: i32 = -1; +fn calculateClockTimeout(_: *Label, _: *anyopaque) !?usize { + const time = try interop.getTimeOfDay(); - // Calculate the maximum timeout based on current animations, or the (big) clock. If there's none, we wait for the event indefinitely instead - if (animate and !animation_timed_out) { - timeout = config.min_refresh_delta; + return @intCast(1000 - @divTrunc(time.microseconds, 1000) + 1); +} - // check how long we have been running so we can turn off the animation - var tv: interop.system_time.timeval = undefined; - _ = interop.system_time.gettimeofday(&tv, null); +fn updateBigClock(self: *BigLabel, ptr: *anyopaque) !void { + var state: *UiState = @ptrCast(@alignCast(ptr)); - if (config.animation_timeout_sec > 0 and tv.tv_sec - tv_zero.tv_sec > config.animation_timeout_sec) { - animation_timed_out = true; - switch (config.animation) { - .none => {}, - .doom => doom.deinit(), - .matrix => matrix.deinit(), - } + if (state.box.height + (BigLabel.CHAR_HEIGHT + 2) * 2 >= state.buffer.height) return; + + const time = try interop.getTimeOfDay(); + const animate_time = @divTrunc(time.microseconds, 500_000); + const separator = if (state.animate and animate_time != 0) " " else ":"; + const format = try std.fmt.bufPrintZ( + &state.bigclock_format_buf, + "{s}{s}{s}{s}{s}{s}", + .{ + if (state.config.bigclock_12hr) "%I" else "%H", + separator, + "%M", + if (state.config.bigclock_seconds) separator else "", + if (state.config.bigclock_seconds) "%S" else "", + if (state.config.bigclock_12hr) "%P" else "", + }, + ); + + const clock_str = interop.timeAsString(state.io, &state.bigclock_buf, format); + self.setText(clock_str); +} + +fn calculateBigClockTimeout(_: *BigLabel, ptr: *anyopaque) !?usize { + const state: *UiState = @ptrCast(@alignCast(ptr)); + const time = try interop.getTimeOfDay(); + + if (state.config.bigclock_seconds) { + return @intCast(1000 - @divTrunc(time.microseconds, 1000) + 1); + } + + return @intCast((60 - @rem(time.seconds, 60)) * 1000 - @divTrunc(time.microseconds, 1000) + 1); +} + +fn updateBox(self: *Box, ptr: *anyopaque) !void { + const state: *UiState = @ptrCast(@alignCast(ptr)); + + if (state.config.vi_mode) { + self.bottom_title = if (state.insert_mode) state.lang.insert else state.lang.normal; + } +} + +fn updateSessionSpecifier(self: *Label, ptr: *anyopaque) !void { + const state: *UiState = @ptrCast(@alignCast(ptr)); + + const env = state.session.label.list.items[state.session.label.current]; + self.setText(env.environment.specifier); +} + +const Corner = enum { + bottomLeft, + bottomRight, + topLeft, + topRight, +}; + +const PositionedWidgets = struct { + binds: []bool, + labels: []bool, +}; + +fn positionSingleWidget(state: *UiState, item: []const u8, current_x: *usize, current_y: usize, is_left: bool, is_top: bool, positioned: *PositionedWidgets) !bool { + const base_x = state.edge_margin.x; + + if (std.mem.eql(u8, item, "shutdown")) { + if (state.config.shutdown_key == null) return false; + const width = TerminalBuffer.strWidth(state.shutdown_label.text); + if (is_left) { + state.shutdown_label.positionXY(Position.init(current_x.*, current_y)); + current_x.* += width + 1; + } else { + const dwidth = if (width > current_x.*) 0 else current_x.* - width; + state.shutdown_label.positionXY(Position.init(dwidth, current_y)); + if (width + 1 <= current_x.*) current_x.* -= width + 1; + } + return true; + } else if (std.mem.eql(u8, item, "restart")) { + if (state.config.restart_key == null) return false; + const width = TerminalBuffer.strWidth(state.restart_label.text); + if (is_left) { + state.restart_label.positionXY(Position.init(current_x.*, current_y)); + current_x.* += width + 1; + } else { + const dwidth = if (width > current_x.*) 0 else current_x.* - width; + state.restart_label.positionXY(Position.init(dwidth, current_y)); + if (width + 1 <= current_x.*) current_x.* -= width + 1; + } + return true; + } else if (std.mem.eql(u8, item, "britup")) { + if (state.config.brightness_up_key == null) return false; + const width = TerminalBuffer.strWidth(state.brightness_up_label.text); + if (is_left) { + state.brightness_up_label.positionXY(Position.init(current_x.*, current_y)); + current_x.* += width + 1; + } else { + const dwidth = if (width > current_x.*) 0 else current_x.* - width; + state.brightness_up_label.positionXY(Position.init(dwidth, current_y)); + if (width + 1 <= current_x.*) current_x.* -= width + 1; + } + return true; + } else if (std.mem.eql(u8, item, "britdown")) { + if (state.config.brightness_down_key == null) return false; + const width = TerminalBuffer.strWidth(state.brightness_down_label.text); + if (is_left) { + state.brightness_down_label.positionXY(Position.init(current_x.*, current_y)); + current_x.* += width + 1; + } else { + const dwidth = if (width > current_x.*) 0 else current_x.* - width; + state.brightness_down_label.positionXY(Position.init(dwidth, current_y)); + if (width + 1 <= current_x.*) current_x.* -= width + 1; + } + return true; + } else if (std.mem.eql(u8, item, "password")) { + if (state.config.show_password_key == null) return false; + const width = TerminalBuffer.strWidth(state.toggle_password_label.text); + if (is_left) { + state.toggle_password_label.positionXY(Position.init(current_x.*, current_y)); + current_x.* += width + 1; + } else { + const dwidth = if (width > current_x.*) 0 else current_x.* - width; + state.toggle_password_label.positionXY(Position.init(dwidth, current_y)); + if (width + 1 <= current_x.*) current_x.* -= width + 1; + } + return true; + } else if (std.mem.eql(u8, item, "clock") or std.mem.eql(u8, item, "time")) { + if (state.config.clock == null) return false; + const width = TerminalBuffer.strWidth(state.clock_label.text); + if (is_left) { + state.clock_label.positionXY(Position.init(current_x.*, current_y)); + current_x.* += width + 1; + } else { + const dwidth = if (width > current_x.*) 0 else current_x.* - width; + state.clock_label.positionXY(Position.init(dwidth, current_y)); + if (width + 1 <= current_x.*) current_x.* -= width + 1; + } + return true; + } else if (std.mem.eql(u8, item, "tty")) { + const width = TerminalBuffer.strWidth(state.tty_label.text); + if (is_left) { + state.tty_label.positionXY(Position.init(current_x.*, current_y)); + current_x.* += width + 1; + } else { + const dwidth = if (width > current_x.*) 0 else current_x.* - width; + state.tty_label.positionXY(Position.init(dwidth, current_y)); + if (width + 1 <= current_x.*) current_x.* -= width + 1; + } + return true; + } else if (std.mem.eql(u8, item, "battery")) { + if (state.config.battery_id == null) return false; + const width = TerminalBuffer.strWidth(state.battery_label.text); + if (is_left) { + state.battery_label.positionXY(Position.init(current_x.*, current_y)); + current_x.* += width + 1; + } else { + const dwidth = if (width > current_x.*) 0 else current_x.* - width; + state.battery_label.positionXY(Position.init(dwidth, current_y)); + if (width + 1 <= current_x.*) current_x.* -= width + 1; + } + return true; + } else if (std.mem.eql(u8, item, "version")) { + const width = TerminalBuffer.strWidth(state.version_label.text); + if (is_left) { + state.version_label.positionXY(Position.init(current_x.*, current_y)); + current_x.* += width + 1; + } else { + const dwidth = if (width > current_x.*) 0 else current_x.* - width; + state.version_label.positionXY(Position.init(dwidth, current_y)); + if (width + 1 <= current_x.*) current_x.* -= width + 1; + } + return true; + } else if (std.mem.eql(u8, item, "numlock")) { + const width = TerminalBuffer.strWidth(state.lang.numlock); + if (is_left) { + state.numlock_label.positionXY(Position.init(current_x.*, current_y)); + current_x.* += width + 1; + } else { + const dwidth = if (width > current_x.*) 0 else current_x.* - width; + state.numlock_label.positionXY(Position.init(dwidth, current_y)); + if (width + 1 <= current_x.*) current_x.* -= width + 1; + } + return true; + } else if (std.mem.eql(u8, item, "capslock")) { + const width = TerminalBuffer.strWidth(state.lang.capslock); + if (is_left) { + state.capslock_label.positionXY(Position.init(current_x.*, current_y)); + current_x.* += width + 1; + } else { + const dwidth = if (width > current_x.*) 0 else current_x.* - width; + state.capslock_label.positionXY(Position.init(dwidth, current_y)); + if (width + 1 <= current_x.*) current_x.* -= width + 1; + } + return true; + } else if (std.mem.eql(u8, item, "labels")) { + for (state.custom_info.items, 0..) |*info, i| { + if (positioned.labels[i]) continue; + const width = TerminalBuffer.strWidth(info.lbl.text); + if (is_left) { + info.lbl.positionXY(Position.init(current_x.*, current_y)); + current_x.* += width + 1; + } else { + const dwidth = if (width > current_x.*) 0 else current_x.* - width; + info.lbl.positionXY(Position.init(dwidth, current_y)); + if (width + 1 <= current_x.*) current_x.* -= width + 1; } - } else if (config.bigclock != .none and config.clock == null) { - var tv: interop.system_time.timeval = undefined; - _ = interop.system_time.gettimeofday(&tv, null); - - timeout = @intCast((60 - @rem(tv.tv_sec, 60)) * 1000 - @divTrunc(tv.tv_usec, 1000) + 1); - } else if (config.clock != null or auth_fails >= config.auth_fails) { - var tv: interop.system_time.timeval = undefined; - _ = interop.system_time.gettimeofday(&tv, null); - - timeout = @intCast(1000 - @divTrunc(tv.tv_usec, 1000) + 1); + positioned.labels[i] = true; } + return true; + } else if (std.mem.eql(u8, item, "binds")) { + var local_x = current_x.*; + var local_y = current_y; - const event_error = if (timeout == -1) termbox.tb_poll_event(&event) else termbox.tb_peek_event(&event, timeout); + for (state.custom_binds.items, 0..) |*bind, i| { + if (positioned.binds[i]) continue; + const width = TerminalBuffer.strWidth(bind.lbl.text); - update = timeout != -1; - - if (event_error < 0 or event.type != termbox.TB_EVENT_KEY) continue; - - switch (event.key) { - termbox.TB_KEY_ESC => { - if (config.vi_mode and insert_mode) { - insert_mode = false; - update = true; + if (is_left) { + if (local_x + width > (state.config.custom_bind_width orelse state.buffer.width) - state.edge_margin.x) { + local_x = base_x; + if (is_top) local_y += 1 else local_y -= 1; } - }, - termbox.TB_KEY_F12...termbox.TB_KEY_F1 => { - const pressed_key = 0xFFFF - event.key + 1; - if (pressed_key == shutdown_key) { - shutdown = true; - run = false; - } else if (pressed_key == restart_key) { - restart = true; - run = false; - } else if (pressed_key == sleep_key) { - if (config.sleep_cmd) |sleep_cmd| { - var sleep = std.process.Child.init(&[_][]const u8{ "/bin/sh", "-c", sleep_cmd }, allocator); - _ = sleep.spawnAndWait() catch .{}; - } - } else if (pressed_key == brightness_down_key) { - var brightness = std.process.Child.init(&[_][]const u8{ "/bin/sh", "-c", config.brightness_down_cmd }, allocator); - _ = brightness.spawnAndWait() catch .{}; - } else if (pressed_key == brightness_up_key) { - var brightness = std.process.Child.init(&[_][]const u8{ "/bin/sh", "-c", config.brightness_up_cmd }, allocator); - _ = brightness.spawnAndWait() catch .{}; + bind.lbl.positionXY(Position.init(local_x, local_y)); + local_x += width + 1; + } else { + if (width + state.edge_margin.x > local_x) { + local_x = state.buffer.width - state.edge_margin.x; + if (is_top) local_y += 1 else local_y -= 1; } - }, - termbox.TB_KEY_CTRL_C => run = false, - termbox.TB_KEY_CTRL_U => { - if (active_input == .login) { - login.clear(); - update = true; - } else if (active_input == .password) { - password.clear(); - update = true; - } - }, - termbox.TB_KEY_CTRL_K, termbox.TB_KEY_ARROW_UP => { - active_input = switch (active_input) { - .session, .info_line => .info_line, - .login => .session, - .password => .login, - }; - update = true; - }, - termbox.TB_KEY_CTRL_J, termbox.TB_KEY_ARROW_DOWN => { - active_input = switch (active_input) { - .info_line => .session, - .session => .login, - .login, .password => .password, - }; - update = true; - }, - termbox.TB_KEY_TAB => { - active_input = switch (active_input) { - .info_line => .session, - .session => .login, - .login => .password, - .password => .info_line, - }; - update = true; - }, - termbox.TB_KEY_BACK_TAB => { - active_input = switch (active_input) { - .info_line => .password, - .session => .info_line, - .login => .session, - .password => .login, - }; - - update = true; - }, - termbox.TB_KEY_ENTER => { - try info_line.addMessage(lang.authenticating, config.bg, config.fg); - InfoLine.clearRendered(allocator, buffer) catch { - try info_line.addMessage(lang.err_alloc, config.error_bg, config.error_fg); - }; - info_line.label.draw(); - _ = termbox.tb_present(); - - if (config.save) save_last_settings: { - var file = std.fs.cwd().createFile(save_path, .{}) catch break :save_last_settings; - defer file.close(); - - const save_data = Save{ - .user = login.text.items, - .session_index = session.label.current, - }; - ini.writeFromStruct(save_data, file.writer(), null, true, .{}) catch break :save_last_settings; - - // Delete previous save file if it exists - if (migrator.maybe_save_file) |path| std.fs.cwd().deleteFile(path) catch {}; - } - - var shared_err = try SharedError.init(); - defer shared_err.deinit(); - - { - const login_text = try allocator.dupeZ(u8, login.text.items); - defer allocator.free(login_text); - const password_text = try allocator.dupeZ(u8, password.text.items); - defer allocator.free(password_text); - - // Give up control on the TTY - _ = termbox.tb_shutdown(); - - session_pid = try std.posix.fork(); - if (session_pid == 0) { - const current_environment = session.label.list.items[session.label.current]; - auth.authenticate(config, current_environment, login_text, password_text) catch |err| { - shared_err.writeError(err); - std.process.exit(1); - }; - std.process.exit(0); - } - - _ = std.posix.waitpid(session_pid, 0); - session_pid = -1; - } - - // Take back control of the TTY - _ = termbox.tb_init(); - _ = termbox.tb_set_output_mode(termbox.TB_OUTPUT_NORMAL); - - const auth_err = shared_err.readError(); - if (auth_err) |err| { - auth_fails += 1; - active_input = .password; - try info_line.addMessage(getAuthErrorMsg(err, lang), config.error_bg, config.error_fg); - if (config.clear_password or err != error.PamAuthError) password.clear(); + const dwidth = if (width > local_x) 0 else local_x - width; + bind.lbl.positionXY(Position.init(dwidth, current_y)); + if (width + 1 <= local_x) local_x -= width + 1; + } + positioned.binds[i] = true; + } + current_x.* = local_x; + return true; + } else if (std.mem.startsWith(u8, item, "lbl:")) { + const name = item["lbl:".len..]; + for (state.custom_info.items, 0..) |*info, i| { + if (std.mem.eql(u8, info.info.name, name)) { + if (positioned.labels[i]) return false; + const width = TerminalBuffer.strWidth(info.lbl.text); + if (is_left) { + info.lbl.positionXY(Position.init(current_x.*, current_y)); + current_x.* += width + 1; } else { - if (config.logout_cmd) |logout_cmd| { - var logout_process = std.process.Child.init(&[_][]const u8{ "/bin/sh", "-c", logout_cmd }, allocator); - _ = logout_process.spawnAndWait() catch .{}; - } - - password.clear(); - try info_line.addMessage(lang.logout, config.bg, config.fg); + const dwidth = if (width > current_x.*) 0 else current_x.* - width; + info.lbl.positionXY(Position.init(dwidth, current_y)); + if (width + 1 <= current_x.*) current_x.* -= width + 1; } - - try std.posix.tcsetattr(std.posix.STDIN_FILENO, .FLUSH, tb_termios); - if (auth_fails < config.auth_fails) _ = termbox.tb_clear(); - - update = true; - - // Restore the cursor - _ = termbox.tb_set_cursor(0, 0); - _ = termbox.tb_present(); - }, - else => { - if (!insert_mode) { - switch (event.ch) { - 'k' => { - active_input = switch (active_input) { - .session, .info_line => .info_line, - .login => .session, - .password => .login, - }; - update = true; - continue; - }, - 'j' => { - active_input = switch (active_input) { - .info_line => .session, - .session => .login, - .login, .password => .password, - }; - update = true; - continue; - }, - 'i' => { - insert_mode = true; - update = true; - continue; - }, - else => {}, - } - } - - switch (active_input) { - .info_line => info_line.label.handle(&event, insert_mode), - .session => session.label.handle(&event, insert_mode), - .login => login.handle(&event, insert_mode) catch { - try info_line.addMessage(lang.err_alloc, config.error_bg, config.error_fg); - }, - .password => password.handle(&event, insert_mode) catch { - try info_line.addMessage(lang.err_alloc, config.error_bg, config.error_fg); - }, - } - update = true; - }, + positioned.labels[i] = true; + return true; + } } + } else if (std.mem.startsWith(u8, item, "cmd:")) { + const key = item["cmd:".len..]; + for (state.custom_binds.items, 0..) |*bind, i| { + if (std.mem.eql(u8, bind.key, key)) { + if (positioned.binds[i]) return false; + const width = TerminalBuffer.strWidth(bind.lbl.text); + if (is_left) { + bind.lbl.positionXY(Position.init(current_x.*, current_y)); + current_x.* += width + 1; + } else { + const dwidth = if (width > current_x.*) 0 else current_x.* - width; + bind.lbl.positionXY(Position.init(dwidth, current_y)); + if (width + 1 <= current_x.*) current_x.* -= width + 1; + } + positioned.binds[i] = true; + return true; + } + } + } + return false; +} + +fn positionItem(state: *UiState, item: []const u8, current_x: usize, current_y: usize, is_left: bool, is_top: bool, positioned: *PositionedWidgets) !bool { + // Check if item contains a comma for compound widgets + if (std.mem.indexOf(u8, item, ",") == null) { + var line_x = current_x; + return positionSingleWidget(state, item, &line_x, current_y, is_left, is_top, positioned); + } + + var it = std.mem.tokenizeAny(u8, item, ","); + var success = false; + + // Check if we need to wrap to next line + var line_x = current_x; + + // Now position each subitem + while (it.next()) |subitem| { + const trimmed = std.mem.trim(u8, subitem, " "); + + if (try positionSingleWidget(state, trimmed, &line_x, current_y, is_left, is_top, positioned)) { + success = true; + } + } + + return success; +} + +fn positionCorner(state: *UiState, config_str: []const u8, corner: Corner, positioned: *PositionedWidgets) !void { + const is_left = corner == .topLeft or corner == .bottomLeft; + const is_top = corner == .topLeft or corner == .topRight; + + var y_offset: usize = 0; + var i: usize = 0; + const len = config_str.len; + + while (i < len) { + // Skip whitespace and brackets + while (i < len and (config_str[i] == ' ' or config_str[i] == '\t' or config_str[i] == '[' or config_str[i] == ']')) { + i += 1; + } + if (i >= len) break; + + const start = i; + while (i < len and config_str[i] != ' ' and config_str[i] != '\t' and config_str[i] != '[' and config_str[i] != ']') { + i += 1; + } + const token = config_str[start..i]; + + const current_x = if (is_left) state.edge_margin.x else state.buffer.width - state.edge_margin.x; + const current_y = if (is_top) state.edge_margin.y + y_offset else state.buffer.height - 1 - state.edge_margin.y - y_offset; + + if (try positionItem(state, token, current_x, current_y, is_left, is_top, positioned)) { + y_offset += 1; + } + } +} + +fn positionWidgets(ptr: *anyopaque) !void { + var state: *UiState = @ptrCast(@alignCast(ptr)); + + const offscreen = Position.init(state.buffer.width + 1, state.buffer.height + 1); + + // Reset all potential corner widgets to offscreen + state.shutdown_label.positionXY(offscreen); + state.restart_label.positionXY(offscreen); + state.toggle_password_label.positionXY(offscreen); + state.brightness_down_label.positionXY(offscreen); + state.brightness_up_label.positionXY(offscreen); + state.clock_label.positionXY(offscreen); + state.bigclock_label.positionXY(offscreen); + state.tty_label.positionXY(offscreen); + state.battery_label.positionXY(offscreen); + state.version_label.positionXY(offscreen); + state.numlock_label.positionXY(offscreen); + state.capslock_label.positionXY(offscreen); + + for (state.custom_binds.items) |*bind| { + bind.lbl.positionXY(offscreen); + } + for (state.custom_info.items) |*info| { + info.lbl.positionXY(offscreen); + } + + var positioned = PositionedWidgets{ + .binds = try state.allocator.alloc(bool, state.custom_binds.items.len), + .labels = try state.allocator.alloc(bool, state.custom_info.items.len), + }; + defer state.allocator.free(positioned.binds); + defer state.allocator.free(positioned.labels); + + @memset(positioned.binds, false); + @memset(positioned.labels, false); + + try positionCorner(state, state.config.corner_top_left, .topLeft, &positioned); + try positionCorner(state, state.config.corner_top_right, .topRight, &positioned); + try positionCorner(state, state.config.corner_bottom_left, .bottomLeft, &positioned); + try positionCorner(state, state.config.corner_bottom_right, .bottomRight, &positioned); + + var bb_height = state.box.height; + var bb_width = state.box.width; + const clock_text_len = TerminalBuffer.strWidth(state.bigclock_label.text) * (BigLabel.CHAR_WIDTH + 1); + + if (state.config.bigclock != .none) { + bb_height += BigLabel.CHAR_HEIGHT + 2; + bb_width = @max(bb_width, clock_text_len); + } + + const dheight = if (bb_height + 1 > state.buffer.height) 1 else state.buffer.height - bb_height - 1; + const dwidth = if (bb_width + 1 > state.buffer.width) 1 else state.buffer.width - bb_width - 1; + + const max_v_position: f32 = @floatFromInt(dheight); + const max_h_position: f32 = @floatFromInt(dwidth); + + bb_height = @min(bb_height, state.buffer.height - 2); + bb_width = @min(bb_width, state.buffer.width - 2); + + const v_space: f32 = @floatFromInt(state.buffer.height - bb_height); + const v_position: usize = @intFromFloat(if (max_v_position < 1.0) 1.0 else std.math.clamp(v_space * state.config.box_position_v, 1.0, max_v_position)); + const h_space: f32 = @floatFromInt(state.buffer.width - bb_width); + const h_position: usize = @intFromFloat(if (max_h_position < 1.0) 1.0 else std.math.clamp(h_space * state.config.box_position_h, 1.0, max_h_position)); + + if (state.config.bigclock != .none) { + const cwidth = if (clock_text_len > bb_width) 0 else bb_width - clock_text_len; + + state.bigclock_label.positionXY(TerminalBuffer.START_POSITION + .addX(h_position + cwidth / 2) + .addY(v_position)); + } + + const bwidth = if (state.box.width > bb_width) 0 else bb_width - state.box.width; + const bheight = if (state.box.height > bb_height) 0 else bb_height - state.box.height; + + state.box.positionXY(TerminalBuffer.START_POSITION + .addX(h_position + bwidth / 2) + .addY(v_position + bheight)); + + state.info_line.label.positionY(state.box + .childrenPosition()); + + state.session_specifier_label.positionX(state.info_line.label + .childrenPosition() + .addY(1)); + state.session.label.positionY(state.session_specifier_label + .childrenPosition() + .addX(state.labels_max_length - TerminalBuffer.strWidth(state.session_specifier_label.text) + 1)); + + state.login_label.positionX(state.session.label + .childrenPosition() + .resetXFrom(state.info_line.label.childrenPosition()) + .addY(1)); + if (state.login_text) |box| { + box.positionY(state.login_label + .childrenPosition() + .addX(state.labels_max_length - TerminalBuffer.strWidth(state.login_label.text) + 1)); + } else { + state.login.?.label.positionY(state.login_label + .childrenPosition() + .addX(state.labels_max_length - TerminalBuffer.strWidth(state.login_label.text) + 1)); + } + + const login_children_pos = if (state.login_text) |box| + box.childrenPosition() + else + state.login.?.label.childrenPosition(); + + state.password_label.positionX(login_children_pos + .resetXFrom(state.info_line.label.childrenPosition()) + .addY(1)); + state.password.positionY(state.password_label + .childrenPosition() + .addX(state.labels_max_length - TerminalBuffer.strWidth(state.password_label.text) + 1)); +} + +fn handleInactivity(ptr: *anyopaque) !void { + var state: *UiState = @ptrCast(@alignCast(ptr)); + + if (state.config.inactivity_cmd) |inactivity_cmd| handle_inactivity_cmd: { + var process = std.process.spawn(state.io, .{ + .argv = &[_][]const u8{ "/bin/sh", "-c", inactivity_cmd }, + .stdout = .ignore, + .stderr = .ignore, + }) catch break :handle_inactivity_cmd; + + const process_result = process.wait(state.io) catch { + break :handle_inactivity_cmd; + }; + if (process_result.exited != 0) { + try state.info_line.addMessage( + state.lang.err_inactivity, + state.config.error_bg, + state.config.error_fg, + ); + try state.log_file.err( + state.io, + "sys", + "failed to execute inactivity command: exit code {d}", + .{process_result.exited}, + ); + } + } +} + +fn addOtherEnvironment(session: *Session, lang: Lang, display_server: DisplayServer, exec: ?[]const u8) !void { + const name = switch (display_server) { + .shell => lang.shell, + .xinitrc => lang.xinitrc, + else => unreachable, + }; + + try session.addEnvironment(.{ + .entry_ini = null, + .name = name, + .xdg_session_desktop = null, + .xdg_desktop_names = null, + .cmd = exec, + .specifier = lang.other, + .display_server = display_server, + .is_terminal = display_server == .shell, + }); +} + +fn crawl(session: *Session, io: std.Io, lang: Lang, path: []const u8, display_server: DisplayServer) !void { + if (!std.Io.Dir.path.isAbsolute(path)) return error.PathNotAbsolute; + + var iterable_directory = try std.Io.Dir.openDirAbsolute(io, path, .{ .iterate = true }); + defer iterable_directory.close(io); + + var iterator = iterable_directory.iterate(); + while (try iterator.next(io)) |item| { + if (!std.mem.eql(u8, std.Io.Dir.path.extension(item.name), ".desktop")) continue; + + const entry_path = try std.fmt.allocPrint(session.label.allocator, "{s}/{s}", .{ path, item.name }); + defer session.label.allocator.free(entry_path); + var entry_ini = Ini(Entry).init(session.label.allocator); + const data = try entry_ini.readFileToStruct(io, entry_path, .{ + .fieldHandler = null, + .comment_characters = "#", + }); + errdefer entry_ini.deinit(); + + const file_name = try session.label.allocator.dupe(u8, std.Io.Dir.path.stem(item.name)); + const entry = data.@"Desktop Entry"; + var maybe_xdg_session_desktop: ?[]const u8 = null; + var maybe_xdg_desktop_names: ?[]const u8 = null; + + // Prepare the XDG_SESSION_DESKTOP and XDG_CURRENT_DESKTOP environment + // variables here + if (entry.DesktopNames) |desktop_names| { + maybe_xdg_session_desktop = std.mem.sliceTo(desktop_names, ';'); + + for (desktop_names) |*c| { + if (c.* == ';') c.* = ':'; + } + maybe_xdg_desktop_names = desktop_names; + } else if (display_server != .custom) { + // If DesktopNames is empty, and this isn't a custom session entry, + // we'll take the name of the session file + if (file_name.len > 0) maybe_xdg_session_desktop = file_name; + } + + try session.addEnvironment(.{ + .entry_ini = entry_ini, + .file_name = file_name, + .name = entry.Name, + .xdg_session_desktop = maybe_xdg_session_desktop, + .xdg_desktop_names = maybe_xdg_desktop_names, + .cmd = entry.Exec, + .specifier = switch (display_server) { + .wayland => lang.wayland, + .x11 => lang.x11, + .custom => lang.custom, + else => lang.other, + }, + .display_server = display_server, + .is_terminal = entry.Terminal orelse false, + }); + } +} + +fn isValidUsername(username: []const u8, usernames: StringList) bool { + for (usernames.items) |valid_username| { + if (std.mem.eql(u8, username, valid_username)) return true; + } + return false; +} + +fn findSessionByName(session: *Session, name: []const u8) ?usize { + for (session.label.list.items, 0..) |env, i| { + if (std.ascii.eqlIgnoreCase(env.environment.file_name, name)) return i; + if (std.ascii.eqlIgnoreCase(env.environment.name, name)) return i; + if (env.environment.xdg_session_desktop) |session_desktop| { + if (session_desktop.len > 0 and std.ascii.eqlIgnoreCase(session_desktop, name)) return i; + } + if (env.environment.xdg_desktop_names) |session_desktop_name| { + if (std.ascii.eqlIgnoreCase(session_desktop_name, name)) return i; + } + } + return null; +} + +fn getAllUsernames(allocator: Allocator, io: std.Io, login_defs_path: []const u8, uid_range_error: *?anyerror) !StringList { + const uid_range = interop.getUserIdRange(allocator, io, login_defs_path) catch |err| no_uid_range: { + uid_range_error.* = err; + break :no_uid_range UidRange{ + .uid_min = build_options.fallback_uid_min, + .uid_max = build_options.fallback_uid_max, + }; + }; + + // There's no reliable (and clean) way to check for systemd support, so + // let's just define a range and check if a user is within it + const SYSTEMD_HOMED_UID_MIN = 60001; + const SYSTEMD_HOMED_UID_MAX = 60513; + const homed_uid_range = UidRange{ + .uid_min = SYSTEMD_HOMED_UID_MIN, + .uid_max = SYSTEMD_HOMED_UID_MAX, + }; + + var usernames: StringList = .empty; + var maybe_entry = interop.getNextUsernameEntry(); + + while (maybe_entry) |entry| { + // We check if the UID is equal to 0 because we always want to add root + // as a username (even if you can't log into it) + const is_within_range = + entry.uid >= uid_range.uid_min and + entry.uid <= uid_range.uid_max; + const is_within_homed_range = + builtin.os.tag == .linux and + entry.uid >= homed_uid_range.uid_min and + entry.uid <= homed_uid_range.uid_max; + const is_root = + entry.uid == 0 and + entry.username != null; + + if (is_within_range or is_within_homed_range or is_root) { + const username = try allocator.dupe(u8, entry.username.?); + try usernames.append(allocator, username); + } + + maybe_entry = interop.getNextUsernameEntry(); + } + + interop.closePasswordDatabase(); + return usernames; +} + +fn adjustBrightness(io: std.Io, cmd: []const u8) !void { + var process = std.process.spawn(io, .{ + .argv = &[_][]const u8{ "/bin/sh", "-c", cmd }, + .stdout = .ignore, + .stderr = .ignore, + }) catch return; + + const process_result = process.wait(io) catch return; + if (process_result.exited != 0) { + return error.BrightnessChangeFailed; + } +} + +fn getBatteryPercentage(io: std.Io, battery_id: []const u8) !u8 { + if (builtin.os.tag == .freebsd) { + // battery_id is unused on FreeBSD; sysctl exposes a single aggregate value. + // For multi-battery systems the MIB would be hw.acpi.battery.N.life, + // but hw.acpi.battery.life is the standard single-battery interface. + var capacity: c_int = -1; + var size: usize = @sizeOf(c_int); + + const ret = interop.sysctl.sysctlbyname("hw.acpi.battery.life", &capacity, &size, null, 0); + + if (ret != 0) return error.SysctlFailed; + if (capacity < 0 or capacity > 100) return error.InvalidBatteryCapacity; + + return @intCast(capacity); + } else { + const path = try std.fmt.allocPrint(temporary_allocator, "/sys/class/power_supply/{s}/capacity", .{battery_id}); + defer temporary_allocator.free(path); + + const battery_file = try std.Io.Dir.cwd().openFile(io, path, .{}); + defer battery_file.close(io); + + var buffer: [8]u8 = undefined; + const bytes_read = try battery_file.readStreaming(io, &.{&buffer}); + const capacity_str = buffer[0..bytes_read]; + + const trimmed = std.mem.trimEnd(u8, capacity_str, "\n\r"); + + return try std.fmt.parseInt(u8, trimmed, 10); } } @@ -768,6 +2556,7 @@ fn getAuthErrorMsg(err: anyerror, lang: Lang) []const u8 { error.SetUserGidFailed => lang.err_user_gid, error.SetUserUidFailed => lang.err_user_uid, error.ChangeDirectoryFailed => lang.err_perm_dir, + error.TtyControlTransferFailed => lang.err_tty_ctrl, error.SetPathFailed => lang.err_path, error.PamAccountExpired => lang.err_pam_acct_expired, error.PamAuthError => lang.err_pam_auth, @@ -784,6 +2573,6 @@ fn getAuthErrorMsg(err: anyerror, lang: Lang) []const u8 { error.PamSystemError => lang.err_pam_sys, error.PamUserUnknown => lang.err_pam_user_unknown, error.PamAbort => lang.err_pam_abort, - else => lang.err_unknown, + else => @errorName(err), }; } diff --git a/src/tui/TerminalBuffer.zig b/src/tui/TerminalBuffer.zig deleted file mode 100644 index 494ab81..0000000 --- a/src/tui/TerminalBuffer.zig +++ /dev/null @@ -1,199 +0,0 @@ -const std = @import("std"); -const builtin = @import("builtin"); -const interop = @import("../interop.zig"); -const utils = @import("utils.zig"); -const Config = @import("../config/Config.zig"); - -const Random = std.Random; - -const termbox = interop.termbox; - -const TerminalBuffer = @This(); - -random: Random, -width: usize, -height: usize, -buffer: [*]termbox.tb_cell, -fg: u16, -bg: u16, -border_fg: u16, -box_chars: struct { - left_up: u32, - left_down: u32, - right_up: u32, - right_down: u32, - top: u32, - bottom: u32, - left: u32, - right: u32, -}, -labels_max_length: usize, -box_x: usize, -box_y: usize, -box_width: usize, -box_height: usize, -margin_box_v: u8, -margin_box_h: u8, - -pub fn init(config: Config, labels_max_length: usize, random: Random) TerminalBuffer { - return .{ - .random = random, - .width = @intCast(termbox.tb_width()), - .height = @intCast(termbox.tb_height()), - .buffer = termbox.tb_cell_buffer(), - .fg = config.fg, - .bg = config.bg, - .border_fg = config.border_fg, - .box_chars = if (builtin.os.tag == .linux or builtin.os.tag.isBSD()) .{ - .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 = '|', - }, - .labels_max_length = labels_max_length, - .box_x = 0, - .box_y = 0, - .box_width = (2 * config.margin_box_h) + config.input_len + 1 + labels_max_length, - .box_height = 7 + (2 * config.margin_box_v), - .margin_box_v = config.margin_box_v, - .margin_box_h = config.margin_box_h, - }; -} - -pub fn cascade(self: TerminalBuffer) bool { - var changed = false; - var y = self.height - 2; - - while (y > 0) : (y -= 1) { - for (0..self.width) |x| { - const cell = self.buffer[(y - 1) * self.width + x]; - const cell_under = self.buffer[y * self.width + x]; - - const char: u8 = @truncate(cell.ch); - if (std.ascii.isWhitespace(char)) continue; - - const char_under: u8 = @truncate(cell_under.ch); - if (!std.ascii.isWhitespace(char_under)) continue; - - changed = true; - - if ((self.random.int(u16) % 10) > 7) continue; - - _ = termbox.tb_set_cell(@intCast(x), @intCast(y), cell.ch, cell.fg, cell.bg); - _ = termbox.tb_set_cell(@intCast(x), @intCast(y - 1), ' ', cell_under.fg, cell_under.bg); - } - } - - return changed; -} - -pub fn drawBoxCenter(self: *TerminalBuffer, show_borders: bool, blank_box: bool) void { - if (self.width < 2 or self.height < 2) return; - const x1 = (self.width - @min(self.width - 2, self.box_width)) / 2; - const y1 = (self.height - @min(self.height - 2, self.box_height)) / 2; - const x2 = (self.width + @min(self.width, self.box_width)) / 2; - const y2 = (self.height + @min(self.height, self.box_height)) / 2; - - self.box_x = x1; - self.box_y = y1; - - if (show_borders) { - _ = termbox.tb_set_cell(@intCast(x1 - 1), @intCast(y1 - 1), self.box_chars.left_up, self.border_fg, self.bg); - _ = termbox.tb_set_cell(@intCast(x2), @intCast(y1 - 1), self.box_chars.right_up, self.border_fg, self.bg); - _ = termbox.tb_set_cell(@intCast(x1 - 1), @intCast(y2), self.box_chars.left_down, self.border_fg, self.bg); - _ = termbox.tb_set_cell(@intCast(x2), @intCast(y2), self.box_chars.right_down, self.border_fg, self.bg); - - var c1 = utils.initCell(self.box_chars.top, self.border_fg, self.bg); - var c2 = utils.initCell(self.box_chars.bottom, self.border_fg, self.bg); - - for (0..self.box_width) |i| { - utils.putCell(x1 + i, y1 - 1, c1); - utils.putCell(x1 + i, y2, c2); - } - - c1.ch = self.box_chars.left; - c2.ch = self.box_chars.right; - - for (0..self.box_height) |i| { - utils.putCell(x1 - 1, y1 + i, c1); - utils.putCell(x2, y1 + i, c2); - } - } - - if (blank_box) { - const blank = utils.initCell(' ', self.fg, self.bg); - - for (0..self.box_height) |y| { - for (0..self.box_width) |x| { - utils.putCell(x1 + x, y1 + y, blank); - } - } - } -} - -pub fn calculateComponentCoordinates(self: TerminalBuffer) struct { - start_x: usize, - x: usize, - y: usize, - full_visible_length: usize, - visible_length: usize, -} { - const start_x = self.box_x + self.margin_box_h; - const x = start_x + self.labels_max_length + 1; - const y = self.box_y + self.margin_box_v; - const full_visible_length = self.box_x + self.box_width - self.margin_box_h - start_x; - const visible_length = self.box_x + self.box_width - self.margin_box_h - x; - - return .{ - .start_x = start_x, - .x = x, - .y = y, - .full_visible_length = full_visible_length, - .visible_length = visible_length, - }; -} - -pub fn drawLabel(self: TerminalBuffer, text: []const u8, x: usize, y: usize) void { - drawColorLabel(text, x, y, self.fg, self.bg); -} - -pub fn drawColorLabel(text: []const u8, x: usize, y: usize, fg: u16, bg: u16) void { - const yc: c_int = @intCast(y); - const utf8view = std.unicode.Utf8View.init(text) catch return; - var utf8 = utf8view.iterator(); - - var i = x; - while (utf8.nextCodepoint()) |codepoint| : (i += 1) { - _ = termbox.tb_set_cell(@intCast(i), yc, codepoint, fg, bg); - } -} - -pub fn drawConfinedLabel(self: TerminalBuffer, text: []const u8, x: usize, y: usize, max_length: usize) void { - const yc: c_int = @intCast(y); - const utf8view = std.unicode.Utf8View.init(text) catch return; - var utf8 = utf8view.iterator(); - - var i: usize = 0; - while (utf8.nextCodepoint()) |codepoint| : (i += 1) { - if (i >= max_length) break; - _ = termbox.tb_set_cell(@intCast(i + x), yc, codepoint, self.fg, self.bg); - } -} - -pub fn drawCharMultiple(self: TerminalBuffer, char: u8, x: usize, y: usize, length: usize) void { - const cell = utils.initCell(char, self.fg, self.bg); - for (0..length) |xx| utils.putCell(x + xx, y, cell); -} diff --git a/src/tui/components/InfoLine.zig b/src/tui/components/InfoLine.zig deleted file mode 100644 index a43b083..0000000 --- a/src/tui/components/InfoLine.zig +++ /dev/null @@ -1,61 +0,0 @@ -const std = @import("std"); -const TerminalBuffer = @import("../TerminalBuffer.zig"); -const generic = @import("generic.zig"); -const utils = @import("../utils.zig"); - -const Allocator = std.mem.Allocator; - -const MessageLabel = generic.CyclableLabel(Message); - -const InfoLine = @This(); - -const Message = struct { - width: u8, - text: []const u8, - bg: u16, - fg: u16, -}; - -label: MessageLabel, - -pub fn init(allocator: Allocator, buffer: *TerminalBuffer) InfoLine { - return .{ - .label = MessageLabel.init(allocator, buffer, drawItem), - }; -} - -pub fn deinit(self: InfoLine) void { - self.label.deinit(); -} - -pub fn addMessage(self: *InfoLine, text: []const u8, bg: u16, fg: u16) !void { - if (text.len == 0) return; - - try self.label.addItem(.{ - .width = try utils.strWidth(text), - .text = text, - .bg = bg, - .fg = fg, - }); -} - -pub fn clearRendered(allocator: Allocator, buffer: TerminalBuffer) !void { - // Draw over the area - const y = buffer.box_y + buffer.margin_box_v; - const spaces = try allocator.alloc(u8, buffer.box_width); - defer allocator.free(spaces); - - @memset(spaces, ' '); - - buffer.drawLabel(spaces, buffer.box_x, y); -} - -fn drawItem(label: *MessageLabel, message: Message, _: usize, _: usize) bool { - if (message.width == 0 or label.buffer.box_width <= message.width) return false; - - const x = label.buffer.box_x + ((label.buffer.box_width - message.width) / 2); - label.first_char_x = x + message.width; - - TerminalBuffer.drawColorLabel(message.text, x, label.y, message.fg, message.bg); - return true; -} diff --git a/src/tui/components/Session.zig b/src/tui/components/Session.zig deleted file mode 100644 index 2cc081a..0000000 --- a/src/tui/components/Session.zig +++ /dev/null @@ -1,142 +0,0 @@ -const std = @import("std"); -const TerminalBuffer = @import("../TerminalBuffer.zig"); -const enums = @import("../../enums.zig"); -const generic = @import("generic.zig"); -const Ini = @import("zigini").Ini; -const Lang = @import("../../config/Lang.zig"); - -const Allocator = std.mem.Allocator; - -const DisplayServer = enums.DisplayServer; - -const EnvironmentLabel = generic.CyclableLabel(Environment); - -const Session = @This(); - -pub const Environment = struct { - entry_ini: ?Ini(Entry) = null, - name: [:0]const u8 = "", - xdg_session_desktop: ?[:0]const u8 = null, - xdg_desktop_names: ?[:0]const u8 = null, - cmd: []const u8 = "", - specifier: []const u8 = "", - display_server: DisplayServer = .wayland, -}; - -const DesktopEntry = struct { - Exec: []const u8 = "", - Name: [:0]const u8 = "", - DesktopNames: ?[:0]u8 = null, -}; - -pub const Entry = struct { @"Desktop Entry": DesktopEntry = .{} }; - -label: EnvironmentLabel, -lang: Lang, - -pub fn init(allocator: Allocator, buffer: *TerminalBuffer, lang: Lang) Session { - return .{ - .label = EnvironmentLabel.init(allocator, buffer, drawItem), - .lang = lang, - }; -} - -pub fn deinit(self: Session) void { - for (self.label.list.items) |*environment| { - if (environment.entry_ini) |*entry_ini| entry_ini.deinit(); - if (environment.xdg_session_desktop) |session_desktop| self.label.allocator.free(session_desktop); - } - - self.label.deinit(); -} - -pub fn addEnvironment(self: *Session, entry: DesktopEntry, xdg_session_desktop: ?[:0]const u8, display_server: DisplayServer) !void { - var xdg_desktop_names: ?[:0]const u8 = null; - if (entry.DesktopNames) |desktop_names| { - for (desktop_names) |*c| { - if (c.* == ';') c.* = ':'; - } - xdg_desktop_names = desktop_names; - } - - try self.label.addItem(.{ - .entry_ini = null, - .name = entry.Name, - .xdg_session_desktop = xdg_session_desktop, - .xdg_desktop_names = xdg_desktop_names, - .cmd = entry.Exec, - .specifier = switch (display_server) { - .wayland => self.lang.wayland, - .x11 => self.lang.x11, - else => self.lang.other, - }, - .display_server = display_server, - }); -} - -pub fn addEnvironmentWithIni(self: *Session, entry_ini: Ini(Entry), xdg_session_desktop: ?[:0]const u8, display_server: DisplayServer) !void { - const entry = entry_ini.data.@"Desktop Entry"; - var xdg_desktop_names: ?[:0]const u8 = null; - if (entry.DesktopNames) |desktop_names| { - for (desktop_names) |*c| { - if (c.* == ';') c.* = ':'; - } - xdg_desktop_names = desktop_names; - } - - try self.label.addItem(.{ - .entry_ini = entry_ini, - .name = entry.Name, - .xdg_session_desktop = xdg_session_desktop, - .xdg_desktop_names = xdg_desktop_names, - .cmd = entry.Exec, - .specifier = switch (display_server) { - .wayland => self.lang.wayland, - .x11 => self.lang.x11, - else => self.lang.other, - }, - .display_server = display_server, - }); -} - -pub fn crawl(self: *Session, path: []const u8, display_server: DisplayServer) !void { - var iterable_directory = std.fs.openDirAbsolute(path, .{ .iterate = true }) catch return; - defer iterable_directory.close(); - - var iterator = iterable_directory.iterate(); - while (try iterator.next()) |item| { - if (!std.mem.eql(u8, std.fs.path.extension(item.name), ".desktop")) continue; - - const entry_path = try std.fmt.allocPrint(self.label.allocator, "{s}/{s}", .{ path, item.name }); - defer self.label.allocator.free(entry_path); - var entry_ini = Ini(Entry).init(self.label.allocator); - _ = try entry_ini.readFileToStruct(entry_path, "#", null); - errdefer entry_ini.deinit(); - - var xdg_session_desktop: []const u8 = undefined; - const maybe_desktop_names = entry_ini.data.@"Desktop Entry".DesktopNames; - if (maybe_desktop_names) |desktop_names| { - xdg_session_desktop = std.mem.sliceTo(desktop_names, ';'); - } else { - // if DesktopNames is empty, we'll take the name of the session file - xdg_session_desktop = std.fs.path.stem(item.name); - } - - const session_desktop = try self.label.allocator.dupeZ(u8, xdg_session_desktop); - errdefer self.label.allocator.free(session_desktop); - - try self.addEnvironmentWithIni(entry_ini, session_desktop, display_server); - } -} - -fn drawItem(label: *EnvironmentLabel, environment: Environment, x: usize, y: usize) bool { - const length = @min(environment.name.len, label.visible_length - 3); - if (length == 0) return false; - - const nx = if (label.text_in_center) (label.x + (label.visible_length - environment.name.len) / 2) else (label.x + 2); - label.first_char_x = nx + environment.name.len; - - label.buffer.drawLabel(environment.specifier, x, y); - label.buffer.drawLabel(environment.name, nx, label.y); - return true; -} diff --git a/src/tui/components/Text.zig b/src/tui/components/Text.zig deleted file mode 100644 index 18223b2..0000000 --- a/src/tui/components/Text.zig +++ /dev/null @@ -1,161 +0,0 @@ -const std = @import("std"); -const interop = @import("../../interop.zig"); -const TerminalBuffer = @import("../TerminalBuffer.zig"); -const utils = @import("../utils.zig"); - -const Allocator = std.mem.Allocator; -const DynamicString = std.ArrayList(u8); - -const termbox = interop.termbox; - -const Text = @This(); - -allocator: Allocator, -buffer: *TerminalBuffer, -text: DynamicString, -end: usize, -cursor: usize, -visible_start: usize, -visible_length: usize, -x: usize, -y: usize, -masked: bool, -maybe_mask: ?u8, - -pub fn init(allocator: Allocator, buffer: *TerminalBuffer, masked: bool, maybe_mask: ?u8) Text { - const text = DynamicString.init(allocator); - - return .{ - .allocator = allocator, - .buffer = buffer, - .text = text, - .end = 0, - .cursor = 0, - .visible_start = 0, - .visible_length = 0, - .x = 0, - .y = 0, - .masked = masked, - .maybe_mask = maybe_mask, - }; -} - -pub fn deinit(self: Text) void { - self.text.deinit(); -} - -pub fn position(self: *Text, x: usize, y: usize, visible_length: usize) void { - self.x = x; - self.y = y; - self.visible_length = visible_length; -} - -pub fn handle(self: *Text, maybe_event: ?*termbox.tb_event, insert_mode: bool) !void { - if (maybe_event) |event| blk: { - if (event.type != termbox.TB_EVENT_KEY) break :blk; - - switch (event.key) { - termbox.TB_KEY_ARROW_LEFT => self.goLeft(), - termbox.TB_KEY_ARROW_RIGHT => self.goRight(), - termbox.TB_KEY_DELETE => self.delete(), - termbox.TB_KEY_BACKSPACE, termbox.TB_KEY_BACKSPACE2 => { - if (insert_mode) { - self.backspace(); - } else { - self.goLeft(); - } - }, - termbox.TB_KEY_SPACE => try self.write(' '), - else => { - if (event.ch > 31 and event.ch < 127) { - if (insert_mode) { - try self.write(@intCast(event.ch)); - } else { - switch (event.ch) { - 'h' => self.goLeft(), - 'l' => self.goRight(), - else => {}, - } - } - } - }, - } - } - - if (self.masked and self.maybe_mask == null) { - _ = termbox.tb_set_cursor(@intCast(self.x), @intCast(self.y)); - return; - } - - _ = termbox.tb_set_cursor(@intCast(self.x + (self.cursor - self.visible_start)), @intCast(self.y)); -} - -pub fn draw(self: Text) void { - if (self.masked) { - if (self.maybe_mask) |mask| { - const length = @min(self.text.items.len, self.visible_length - 1); - if (length == 0) return; - - self.buffer.drawCharMultiple(mask, self.x, self.y, length); - } - return; - } - - const length = @min(self.text.items.len, self.visible_length); - if (length == 0) return; - - const visible_slice = vs: { - if (self.text.items.len > self.visible_length and self.cursor < self.text.items.len) { - break :vs self.text.items[self.visible_start..(self.visible_length + self.visible_start)]; - } else { - break :vs self.text.items[self.visible_start..]; - } - }; - - self.buffer.drawLabel(visible_slice, self.x, self.y); -} - -pub fn clear(self: *Text) void { - self.text.clearRetainingCapacity(); - self.end = 0; - self.cursor = 0; - self.visible_start = 0; -} - -fn goLeft(self: *Text) void { - if (self.cursor == 0) return; - if (self.visible_start > 0) self.visible_start -= 1; - - self.cursor -= 1; -} - -fn goRight(self: *Text) void { - if (self.cursor >= self.end) return; - if (self.cursor - self.visible_start == self.visible_length - 1) self.visible_start += 1; - - self.cursor += 1; -} - -fn delete(self: *Text) void { - if (self.cursor >= self.end) return; - - _ = self.text.orderedRemove(self.cursor); - - self.end -= 1; -} - -fn backspace(self: *Text) void { - if (self.cursor == 0) return; - - self.goLeft(); - self.delete(); -} - -fn write(self: *Text, char: u8) !void { - if (char == 0) return; - - try self.text.insert(self.cursor, char); - - self.end += 1; - self.goRight(); -} diff --git a/src/tui/components/generic.zig b/src/tui/components/generic.zig deleted file mode 100644 index 126916b..0000000 --- a/src/tui/components/generic.zig +++ /dev/null @@ -1,115 +0,0 @@ -const std = @import("std"); -const enums = @import("../../enums.zig"); -const interop = @import("../../interop.zig"); -const TerminalBuffer = @import("../TerminalBuffer.zig"); - -pub fn CyclableLabel(comptime ItemType: type) type { - return struct { - const Allocator = std.mem.Allocator; - const ItemList = std.ArrayList(ItemType); - const DrawItemFn = *const fn (*Self, ItemType, usize, usize) bool; - - const termbox = interop.termbox; - - const Self = @This(); - - allocator: Allocator, - buffer: *TerminalBuffer, - list: ItemList, - current: usize, - visible_length: usize, - x: usize, - y: usize, - first_char_x: usize, - text_in_center: bool, - draw_item_fn: DrawItemFn, - - pub fn init(allocator: Allocator, buffer: *TerminalBuffer, draw_item_fn: DrawItemFn) Self { - return .{ - .allocator = allocator, - .buffer = buffer, - .list = ItemList.init(allocator), - .current = 0, - .visible_length = 0, - .x = 0, - .y = 0, - .first_char_x = 0, - .text_in_center = false, - .draw_item_fn = draw_item_fn, - }; - } - - pub fn deinit(self: Self) void { - self.list.deinit(); - } - - pub fn position(self: *Self, x: usize, y: usize, visible_length: usize, text_in_center: ?bool) void { - self.x = x; - self.y = y; - self.visible_length = visible_length; - self.first_char_x = x + 2; - if (text_in_center) |value| { - self.text_in_center = value; - } - } - - pub fn addItem(self: *Self, item: ItemType) !void { - try self.list.append(item); - self.current = self.list.items.len - 1; - } - - pub fn handle(self: *Self, maybe_event: ?*termbox.tb_event, insert_mode: bool) void { - if (maybe_event) |event| blk: { - if (event.type != termbox.TB_EVENT_KEY) break :blk; - - switch (event.key) { - termbox.TB_KEY_ARROW_LEFT, termbox.TB_KEY_CTRL_H => self.goLeft(), - termbox.TB_KEY_ARROW_RIGHT, termbox.TB_KEY_CTRL_L => self.goRight(), - else => { - if (!insert_mode) { - switch (event.ch) { - 'h' => self.goLeft(), - 'l' => self.goRight(), - else => {}, - } - } - }, - } - } - - _ = termbox.tb_set_cursor(@intCast(self.first_char_x), @intCast(self.y)); - } - - pub fn draw(self: *Self) void { - if (self.list.items.len == 0) return; - - const current_item = self.list.items[self.current]; - const x = self.buffer.box_x + self.buffer.margin_box_h; - const y = self.buffer.box_y + self.buffer.margin_box_v + 2; - - const continue_drawing = @call(.auto, self.draw_item_fn, .{ self, current_item, x, y }); - if (!continue_drawing) return; - - _ = termbox.tb_set_cell(@intCast(self.x), @intCast(self.y), '<', self.buffer.fg, self.buffer.bg); - _ = termbox.tb_set_cell(@intCast(self.x + self.visible_length - 1), @intCast(self.y), '>', self.buffer.fg, self.buffer.bg); - } - - fn goLeft(self: *Self) void { - if (self.current == 0) { - self.current = self.list.items.len - 1; - return; - } - - self.current -= 1; - } - - fn goRight(self: *Self) void { - if (self.current == self.list.items.len - 1) { - self.current = 0; - return; - } - - self.current += 1; - } - }; -} diff --git a/src/tui/utils.zig b/src/tui/utils.zig deleted file mode 100644 index 43c3619..0000000 --- a/src/tui/utils.zig +++ /dev/null @@ -1,32 +0,0 @@ -const std = @import("std"); -const interop = @import("../interop.zig"); - -const termbox = interop.termbox; - -pub const Cell = struct { - ch: u32, - fg: u16, - bg: u16, -}; - -pub fn initCell(ch: u32, fg: u16, bg: u16) Cell { - return .{ - .ch = ch, - .fg = fg, - .bg = bg, - }; -} - -pub fn putCell(x: usize, y: usize, cell: Cell) void { - _ = termbox.tb_set_cell(@intCast(x), @intCast(y), cell.ch, cell.fg, cell.bg); -} - -// Every codepoint is assumed to have a width of 1. -// Since ly should be running in a tty, this should be fine. -pub fn strWidth(str: []const u8) !u8 { - const utf8view = try std.unicode.Utf8View.init(str); - var utf8 = utf8view.iterator(); - var i: u8 = 0; - while (utf8.nextCodepoint()) |_| i += 1; - return i; -}