├── .github ├── ISSUE_TEMPLATE │ ├── bug.yml │ └── feature.yml └── screenshot.png ├── .gitignore ├── build.zig ├── build.zig.zon ├── include └── termbox2.h ├── license.md ├── readme.md ├── res ├── config.ini ├── lang │ ├── ar.ini │ ├── cat.ini │ ├── cs.ini │ ├── de.ini │ ├── en.ini │ ├── es.ini │ ├── fr.ini │ ├── it.ini │ ├── normalize_lang_files.py │ ├── pl.ini │ ├── pt.ini │ ├── pt_BR.ini │ ├── ro.ini │ ├── ru.ini │ ├── sr.ini │ ├── sv.ini │ ├── tr.ini │ ├── uk.ini │ └── zh_CN.ini ├── ly-dinit ├── ly-openrc ├── ly-runit-service │ ├── conf │ ├── finish │ └── run ├── ly-s6 │ ├── run │ └── type ├── ly.service ├── pam.d │ └── ly └── setup.sh └── src ├── Environment.zig ├── SharedError.zig ├── animations ├── ColorMix.zig ├── Doom.zig ├── Dummy.zig └── Matrix.zig ├── auth.zig ├── bigclock.zig ├── bigclock ├── Lang.zig ├── en.zig └── fa.zig ├── config ├── Config.zig ├── Lang.zig ├── Save.zig └── migrator.zig ├── enums.zig ├── interop.zig ├── main.zig └── tui ├── Animation.zig ├── Cell.zig ├── TerminalBuffer.zig └── components ├── InfoLine.zig ├── Session.zig ├── Text.zig └── generic.zig /.github/ISSUE_TEMPLATE/bug.yml: -------------------------------------------------------------------------------- 1 | name: Bug report 2 | description: File a bug report. 3 | title: "[Bug] " 4 | labels: ["bug"] 5 | body: 6 | - type: checkboxes 7 | id: prerequisites 8 | attributes: 9 | label: Pre-requisites 10 | description: By submitting this issue, you agree to have done the following. 11 | options: 12 | - label: I have looked for any other duplicate issues 13 | required: true 14 | - type: input 15 | id: version 16 | attributes: 17 | label: Ly version 18 | description: The output of `ly --version`. Please note that only Ly v1.1.0 and above are supported. 19 | placeholder: 1.1.0-dev.12+2b0301c 20 | validations: 21 | required: true 22 | - type: textarea 23 | id: observed 24 | attributes: 25 | label: Observed behavior 26 | description: What happened? 27 | validations: 28 | required: true 29 | - type: textarea 30 | id: expected 31 | attributes: 32 | label: Expected behavior 33 | description: What did you expect to happen instead? 34 | validations: 35 | required: true 36 | - type: input 37 | id: desktop 38 | attributes: 39 | label: OS + Desktop environment/Window manager 40 | description: Which OS and DE (or WM) did you use when observing the problem? 41 | validations: 42 | required: true 43 | - type: textarea 44 | id: reproduction 45 | attributes: 46 | label: Steps to reproduce 47 | description: What **exactly** can someone else do in order to observe the problem you observed? 48 | placeholder: | 49 | 1. Authenticate with ... 50 | 2. Go to ... 51 | 3. Create file ... 52 | 4. Log out and log back in 53 | 5. Observe error 54 | validations: 55 | required: true 56 | - type: textarea 57 | id: logs 58 | attributes: 59 | label: Relevant logs 60 | description: | 61 | 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. 62 | If it exists, ncluding your session log (found at /var/log/ly-session.log unless modified) is a good idea. (But make sure it's relevant!) 63 | render: shell 64 | - type: textarea 65 | id: moreinfo 66 | attributes: 67 | label: Additional information 68 | description: If you have any additional information that might be helpful in reproducing the problem, please provide it here. 69 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature.yml: -------------------------------------------------------------------------------- 1 | name: Feature request 2 | description: Request a new feature or enhancement. 3 | title: "[Feature] " 4 | labels: ["feature"] 5 | body: 6 | - type: checkboxes 7 | id: prerequisites 8 | attributes: 9 | label: Pre-requisites 10 | description: By submitting this issue, you agree to have done the following. 11 | options: 12 | - label: I have looked for any other duplicate issues 13 | required: true 14 | - label: I have confirmed the requested feature doesn't exist in the latest version in development 15 | required: true 16 | - type: textarea 17 | id: wanted 18 | attributes: 19 | label: Wanted behavior 20 | description: What do you want to be added? Describe the behavior clearly. 21 | validations: 22 | required: true 23 | -------------------------------------------------------------------------------- /.github/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fairyglade/ly/a8b82923188ad22c8a9f98d91b5d2772e50b5b65/.github/screenshot.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | zig-cache/ 3 | zig-out/ 4 | valgrind.log 5 | .zig-cache 6 | -------------------------------------------------------------------------------- /build.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const builtin = @import("builtin"); 3 | 4 | const PatchMap = std.StringHashMap([]const u8); 5 | const InitSystem = enum { 6 | systemd, 7 | openrc, 8 | runit, 9 | s6, 10 | dinit, 11 | }; 12 | 13 | const min_zig_string = "0.14.0"; 14 | const current_zig = builtin.zig_version; 15 | 16 | // Implementing zig version detection through compile time 17 | comptime { 18 | const min_zig = std.SemanticVersion.parse(min_zig_string) catch unreachable; 19 | if (current_zig.order(min_zig) == .lt) { 20 | @compileError(std.fmt.comptimePrint("Your Zig version v{} does not meet the minimum build requirement of v{}", .{ current_zig, min_zig })); 21 | } 22 | } 23 | 24 | const ly_version = std.SemanticVersion{ .major = 1, .minor = 2, .patch = 0 }; 25 | 26 | var dest_directory: []const u8 = undefined; 27 | var config_directory: []const u8 = undefined; 28 | var prefix_directory: []const u8 = undefined; 29 | var executable_name: []const u8 = undefined; 30 | var init_system: InitSystem = undefined; 31 | var default_tty_str: []const u8 = undefined; 32 | 33 | pub fn build(b: *std.Build) !void { 34 | dest_directory = b.option([]const u8, "dest_directory", "Specify a destination directory for installation") orelse ""; 35 | 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"; 36 | prefix_directory = b.option([]const u8, "prefix_directory", "Specify a default prefix directory (default is /usr)") orelse "/usr"; 37 | executable_name = b.option([]const u8, "name", "Specify installed executable file name (default is ly)") orelse "ly"; 38 | init_system = b.option(InitSystem, "init_system", "Specify the target init system (default is systemd)") orelse .systemd; 39 | 40 | const build_options = b.addOptions(); 41 | const version_str = try getVersionStr(b, "ly", ly_version); 42 | const enable_x11_support = b.option(bool, "enable_x11_support", "Enable X11 support (default is on)") orelse true; 43 | const default_tty = b.option(u8, "default_tty", "Set the TTY (default is 2)") orelse 2; 44 | 45 | default_tty_str = try std.fmt.allocPrint(b.allocator, "{d}", .{default_tty}); 46 | 47 | build_options.addOption([]const u8, "config_directory", config_directory); 48 | build_options.addOption([]const u8, "prefix_directory", prefix_directory); 49 | build_options.addOption([]const u8, "version", version_str); 50 | build_options.addOption(u8, "tty", default_tty); 51 | build_options.addOption(bool, "enable_x11_support", enable_x11_support); 52 | 53 | const target = b.standardTargetOptions(.{}); 54 | const optimize = b.standardOptimizeOption(.{}); 55 | 56 | const exe = b.addExecutable(.{ 57 | .name = "ly", 58 | .root_source_file = b.path("src/main.zig"), 59 | .target = target, 60 | .optimize = optimize, 61 | }); 62 | 63 | const zigini = b.dependency("zigini", .{ .target = target, .optimize = optimize }); 64 | exe.root_module.addImport("zigini", zigini.module("zigini")); 65 | 66 | exe.root_module.addOptions("build_options", build_options); 67 | 68 | const clap = b.dependency("clap", .{ .target = target, .optimize = optimize }); 69 | exe.root_module.addImport("clap", clap.module("clap")); 70 | 71 | exe.addIncludePath(b.path("include")); 72 | exe.linkSystemLibrary("pam"); 73 | if (enable_x11_support) exe.linkSystemLibrary("xcb"); 74 | exe.linkLibC(); 75 | 76 | const translate_c = b.addTranslateC(.{ 77 | .root_source_file = b.path("include/termbox2.h"), 78 | .target = target, 79 | .optimize = optimize, 80 | }); 81 | translate_c.defineCMacroRaw("TB_IMPL"); 82 | translate_c.defineCMacro("TB_OPT_ATTR_W", "32"); // Enable 24-bit color support + styling (32-bit) 83 | const termbox2 = translate_c.addModule("termbox2"); 84 | exe.root_module.addImport("termbox2", termbox2); 85 | 86 | b.installArtifact(exe); 87 | 88 | const run_cmd = b.addRunArtifact(exe); 89 | 90 | run_cmd.step.dependOn(b.getInstallStep()); 91 | 92 | if (b.args) |args| run_cmd.addArgs(args); 93 | 94 | const run_step = b.step("run", "Run the app"); 95 | run_step.dependOn(&run_cmd.step); 96 | 97 | const installexe_step = b.step("installexe", "Install Ly and the selected init system service"); 98 | installexe_step.makeFn = Installer(true).make; 99 | installexe_step.dependOn(b.getInstallStep()); 100 | 101 | const installnoconf_step = b.step("installnoconf", "Install Ly and the selected init system service, but not the configuration file"); 102 | installnoconf_step.makeFn = Installer(false).make; 103 | installnoconf_step.dependOn(b.getInstallStep()); 104 | 105 | const uninstallexe_step = b.step("uninstallexe", "Uninstall Ly and remove the selected init system service"); 106 | uninstallexe_step.makeFn = Uninstaller(true).make; 107 | 108 | const uninstallnoconf_step = b.step("uninstallnoconf", "Uninstall Ly and remove the selected init system service, but keep the configuration directory"); 109 | uninstallnoconf_step.makeFn = Uninstaller(false).make; 110 | } 111 | 112 | pub fn Installer(install_config: bool) type { 113 | return struct { 114 | pub fn make(step: *std.Build.Step, _: std.Build.Step.MakeOptions) !void { 115 | const allocator = step.owner.allocator; 116 | 117 | var patch_map = PatchMap.init(allocator); 118 | defer patch_map.deinit(); 119 | 120 | try patch_map.put("$DEFAULT_TTY", default_tty_str); 121 | try patch_map.put("$CONFIG_DIRECTORY", config_directory); 122 | try patch_map.put("$PREFIX_DIRECTORY", prefix_directory); 123 | try patch_map.put("$EXECUTABLE_NAME", executable_name); 124 | 125 | try install_ly(allocator, patch_map, install_config); 126 | try install_service(allocator, patch_map); 127 | } 128 | }; 129 | } 130 | 131 | fn install_ly(allocator: std.mem.Allocator, patch_map: PatchMap, install_config: bool) !void { 132 | const ly_config_directory = try std.fs.path.join(allocator, &[_][]const u8{ dest_directory, config_directory, "/ly" }); 133 | 134 | std.fs.cwd().makePath(ly_config_directory) catch { 135 | std.debug.print("warn: {s} already exists as a directory.\n", .{ly_config_directory}); 136 | }; 137 | 138 | const ly_lang_path = try std.fs.path.join(allocator, &[_][]const u8{ dest_directory, config_directory, "/ly/lang" }); 139 | std.fs.cwd().makePath(ly_lang_path) catch { 140 | std.debug.print("warn: {s} already exists as a directory.\n", .{ly_lang_path}); 141 | }; 142 | 143 | { 144 | const exe_path = try std.fs.path.join(allocator, &[_][]const u8{ dest_directory, prefix_directory, "/bin" }); 145 | std.fs.cwd().makePath(exe_path) catch { 146 | if (!std.mem.eql(u8, dest_directory, "")) { 147 | std.debug.print("warn: {s} already exists as a directory.\n", .{exe_path}); 148 | } 149 | }; 150 | 151 | var executable_dir = std.fs.cwd().openDir(exe_path, .{}) catch unreachable; 152 | defer executable_dir.close(); 153 | 154 | try installFile("zig-out/bin/ly", executable_dir, exe_path, executable_name, .{}); 155 | } 156 | 157 | { 158 | var config_dir = std.fs.cwd().openDir(ly_config_directory, .{}) catch unreachable; 159 | defer config_dir.close(); 160 | 161 | if (install_config) { 162 | const patched_config = try patchFile(allocator, "res/config.ini", patch_map); 163 | try installText(patched_config, config_dir, ly_config_directory, "config.ini", .{}); 164 | } 165 | 166 | const patched_setup = try patchFile(allocator, "res/setup.sh", patch_map); 167 | try installText(patched_setup, config_dir, ly_config_directory, "setup.sh", .{ .mode = 0o755 }); 168 | } 169 | 170 | { 171 | var lang_dir = std.fs.cwd().openDir(ly_lang_path, .{}) catch unreachable; 172 | defer lang_dir.close(); 173 | 174 | try installFile("res/lang/cat.ini", lang_dir, ly_lang_path, "cat.ini", .{}); 175 | try installFile("res/lang/cs.ini", lang_dir, ly_lang_path, "cs.ini", .{}); 176 | try installFile("res/lang/de.ini", lang_dir, ly_lang_path, "de.ini", .{}); 177 | try installFile("res/lang/en.ini", lang_dir, ly_lang_path, "en.ini", .{}); 178 | try installFile("res/lang/es.ini", lang_dir, ly_lang_path, "es.ini", .{}); 179 | try installFile("res/lang/fr.ini", lang_dir, ly_lang_path, "fr.ini", .{}); 180 | try installFile("res/lang/it.ini", lang_dir, ly_lang_path, "it.ini", .{}); 181 | try installFile("res/lang/pl.ini", lang_dir, ly_lang_path, "pl.ini", .{}); 182 | try installFile("res/lang/pt.ini", lang_dir, ly_lang_path, "pt.ini", .{}); 183 | try installFile("res/lang/pt_BR.ini", lang_dir, ly_lang_path, "pt_BR.ini", .{}); 184 | try installFile("res/lang/ro.ini", lang_dir, ly_lang_path, "ro.ini", .{}); 185 | try installFile("res/lang/ru.ini", lang_dir, ly_lang_path, "ru.ini", .{}); 186 | try installFile("res/lang/sr.ini", lang_dir, ly_lang_path, "sr.ini", .{}); 187 | try installFile("res/lang/sv.ini", lang_dir, ly_lang_path, "sv.ini", .{}); 188 | try installFile("res/lang/tr.ini", lang_dir, ly_lang_path, "tr.ini", .{}); 189 | try installFile("res/lang/uk.ini", lang_dir, ly_lang_path, "uk.ini", .{}); 190 | } 191 | 192 | { 193 | const pam_path = try std.fs.path.join(allocator, &[_][]const u8{ dest_directory, config_directory, "/pam.d" }); 194 | std.fs.cwd().makePath(pam_path) catch { 195 | if (!std.mem.eql(u8, dest_directory, "")) { 196 | std.debug.print("warn: {s} already exists as a directory.\n", .{pam_path}); 197 | } 198 | }; 199 | 200 | var pam_dir = std.fs.cwd().openDir(pam_path, .{}) catch unreachable; 201 | defer pam_dir.close(); 202 | 203 | try installFile("res/pam.d/ly", pam_dir, pam_path, "ly", .{ .override_mode = 0o644 }); 204 | } 205 | } 206 | 207 | fn install_service(allocator: std.mem.Allocator, patch_map: PatchMap) !void { 208 | switch (init_system) { 209 | .systemd => { 210 | const service_path = try std.fs.path.join(allocator, &[_][]const u8{ dest_directory, prefix_directory, "/lib/systemd/system" }); 211 | std.fs.cwd().makePath(service_path) catch {}; 212 | var service_dir = std.fs.cwd().openDir(service_path, .{}) catch unreachable; 213 | defer service_dir.close(); 214 | 215 | const patched_service = try patchFile(allocator, "res/ly.service", patch_map); 216 | try installText(patched_service, service_dir, service_path, "ly.service", .{ .mode = 0o644 }); 217 | }, 218 | .openrc => { 219 | const service_path = try std.fs.path.join(allocator, &[_][]const u8{ dest_directory, config_directory, "/init.d" }); 220 | std.fs.cwd().makePath(service_path) catch {}; 221 | var service_dir = std.fs.cwd().openDir(service_path, .{}) catch unreachable; 222 | defer service_dir.close(); 223 | 224 | const patched_service = try patchFile(allocator, "res/ly-openrc", patch_map); 225 | try installText(patched_service, service_dir, service_path, executable_name, .{ .mode = 0o755 }); 226 | }, 227 | .runit => { 228 | const service_path = try std.fs.path.join(allocator, &[_][]const u8{ dest_directory, config_directory, "/sv/ly" }); 229 | std.fs.cwd().makePath(service_path) catch {}; 230 | var service_dir = std.fs.cwd().openDir(service_path, .{}) catch unreachable; 231 | defer service_dir.close(); 232 | 233 | const supervise_path = try std.fs.path.join(allocator, &[_][]const u8{ service_path, "supervise" }); 234 | 235 | const patched_conf = try patchFile(allocator, "res/ly-runit-service/conf", patch_map); 236 | try installText(patched_conf, service_dir, service_path, "conf", .{}); 237 | 238 | try installFile("res/ly-runit-service/finish", service_dir, service_path, "finish", .{ .override_mode = 0o755 }); 239 | 240 | const patched_run = try patchFile(allocator, "res/ly-runit-service/run", patch_map); 241 | try installText(patched_run, service_dir, service_path, "run", .{ .mode = 0o755 }); 242 | 243 | try std.fs.cwd().symLink("/run/runit/supervise.ly", supervise_path, .{}); 244 | std.debug.print("info: installed symlink /run/runit/supervise.ly\n", .{}); 245 | }, 246 | .s6 => { 247 | const admin_service_path = try std.fs.path.join(allocator, &[_][]const u8{ dest_directory, config_directory, "/s6/adminsv/default/contents.d" }); 248 | std.fs.cwd().makePath(admin_service_path) catch {}; 249 | var admin_service_dir = std.fs.cwd().openDir(admin_service_path, .{}) catch unreachable; 250 | defer admin_service_dir.close(); 251 | 252 | const file = try admin_service_dir.createFile("ly-srv", .{}); 253 | file.close(); 254 | 255 | const service_path = try std.fs.path.join(allocator, &[_][]const u8{ dest_directory, config_directory, "/s6/sv/ly-srv" }); 256 | std.fs.cwd().makePath(service_path) catch {}; 257 | var service_dir = std.fs.cwd().openDir(service_path, .{}) catch unreachable; 258 | defer service_dir.close(); 259 | 260 | const patched_run = try patchFile(allocator, "res/ly-s6/run", patch_map); 261 | try installText(patched_run, service_dir, service_path, "run", .{ .mode = 0o755 }); 262 | 263 | try installFile("res/ly-s6/type", service_dir, service_path, "type", .{}); 264 | }, 265 | .dinit => { 266 | const service_path = try std.fs.path.join(allocator, &[_][]const u8{ dest_directory, config_directory, "/dinit.d" }); 267 | std.fs.cwd().makePath(service_path) catch {}; 268 | var service_dir = std.fs.cwd().openDir(service_path, .{}) catch unreachable; 269 | defer service_dir.close(); 270 | 271 | const patched_service = try patchFile(allocator, "res/ly-dinit", patch_map); 272 | try installText(patched_service, service_dir, service_path, "ly", .{}); 273 | }, 274 | } 275 | } 276 | 277 | pub fn Uninstaller(uninstall_config: bool) type { 278 | return struct { 279 | pub fn make(step: *std.Build.Step, _: std.Build.Step.MakeOptions) !void { 280 | const allocator = step.owner.allocator; 281 | 282 | if (uninstall_config) { 283 | try deleteTree(allocator, config_directory, "/ly", "ly config directory not found"); 284 | } 285 | 286 | const exe_path = try std.fs.path.join(allocator, &[_][]const u8{ prefix_directory, "/bin/", executable_name }); 287 | var success = true; 288 | std.fs.cwd().deleteFile(exe_path) catch { 289 | std.debug.print("warn: ly executable not found\n", .{}); 290 | success = false; 291 | }; 292 | if (success) std.debug.print("info: deleted {s}\n", .{exe_path}); 293 | 294 | try deleteFile(allocator, config_directory, "/pam.d/ly", "ly pam file not found"); 295 | 296 | switch (init_system) { 297 | .systemd => try deleteFile(allocator, prefix_directory, "/lib/systemd/system/ly.service", "systemd service not found"), 298 | .openrc => try deleteFile(allocator, config_directory, "/init.d/ly", "openrc service not found"), 299 | .runit => try deleteTree(allocator, config_directory, "/sv/ly", "runit service not found"), 300 | .s6 => { 301 | try deleteTree(allocator, config_directory, "/s6/sv/ly-srv", "s6 service not found"); 302 | try deleteFile(allocator, config_directory, "/s6/adminsv/default/contents.d/ly-srv", "s6 admin service not found"); 303 | }, 304 | .dinit => try deleteFile(allocator, config_directory, "/dinit.d/ly", "dinit service not found"), 305 | } 306 | } 307 | }; 308 | } 309 | 310 | fn getVersionStr(b: *std.Build, name: []const u8, version: std.SemanticVersion) ![]const u8 { 311 | const version_str = b.fmt("{d}.{d}.{d}", .{ version.major, version.minor, version.patch }); 312 | 313 | var status: u8 = undefined; 314 | const git_describe_raw = b.runAllowFail(&[_][]const u8{ 315 | "git", 316 | "-C", 317 | b.build_root.path orelse ".", 318 | "describe", 319 | "--match", 320 | "*.*.*", 321 | "--tags", 322 | }, &status, .Ignore) catch { 323 | return version_str; 324 | }; 325 | var git_describe = std.mem.trim(u8, git_describe_raw, " \n\r"); 326 | git_describe = std.mem.trimLeft(u8, git_describe, "v"); 327 | 328 | switch (std.mem.count(u8, git_describe, "-")) { 329 | 0 => { 330 | if (!std.mem.eql(u8, version_str, git_describe)) { 331 | std.debug.print("{s} version '{s}' does not match git tag: '{s}'\n", .{ name, version_str, git_describe }); 332 | std.process.exit(1); 333 | } 334 | return version_str; 335 | }, 336 | 2 => { 337 | // Untagged development build (e.g. 0.10.0-dev.2025+ecf0050a9). 338 | var it = std.mem.splitScalar(u8, git_describe, '-'); 339 | const tagged_ancestor = std.mem.trimLeft(u8, it.first(), "v"); 340 | const commit_height = it.next().?; 341 | const commit_id = it.next().?; 342 | 343 | const ancestor_ver = try std.SemanticVersion.parse(tagged_ancestor); 344 | if (version.order(ancestor_ver) != .gt) { 345 | std.debug.print("{s} version '{}' must be greater than tagged ancestor '{}'\n", .{ name, version, ancestor_ver }); 346 | std.process.exit(1); 347 | } 348 | 349 | // Check that the commit hash is prefixed with a 'g' (a Git convention). 350 | if (commit_id.len < 1 or commit_id[0] != 'g') { 351 | std.debug.print("Unexpected `git describe` output: {s}\n", .{git_describe}); 352 | return version_str; 353 | } 354 | 355 | // The version is reformatted in accordance with the https://semver.org specification. 356 | return b.fmt("{s}-dev.{s}+{s}", .{ version_str, commit_height, commit_id[1..] }); 357 | }, 358 | else => { 359 | std.debug.print("Unexpected `git describe` output: {s}\n", .{git_describe}); 360 | return version_str; 361 | }, 362 | } 363 | } 364 | 365 | fn installFile( 366 | source_file: []const u8, 367 | destination_directory: std.fs.Dir, 368 | destination_directory_path: []const u8, 369 | destination_file: []const u8, 370 | options: std.fs.Dir.CopyFileOptions, 371 | ) !void { 372 | try std.fs.cwd().copyFile(source_file, destination_directory, destination_file, options); 373 | std.debug.print("info: installed {s}/{s}\n", .{ destination_directory_path, destination_file }); 374 | } 375 | 376 | fn patchFile(allocator: std.mem.Allocator, source_file: []const u8, patch_map: PatchMap) ![]const u8 { 377 | var file = try std.fs.cwd().openFile(source_file, .{}); 378 | defer file.close(); 379 | 380 | const reader = file.reader(); 381 | var text = try reader.readAllAlloc(allocator, std.math.maxInt(u16)); 382 | 383 | var iterator = patch_map.iterator(); 384 | while (iterator.next()) |kv| { 385 | const new_text = try std.mem.replaceOwned(u8, allocator, text, kv.key_ptr.*, kv.value_ptr.*); 386 | allocator.free(text); 387 | text = new_text; 388 | } 389 | 390 | return text; 391 | } 392 | 393 | fn installText( 394 | text: []const u8, 395 | destination_directory: std.fs.Dir, 396 | destination_directory_path: []const u8, 397 | destination_file: []const u8, 398 | options: std.fs.File.CreateFlags, 399 | ) !void { 400 | var file = try destination_directory.createFile(destination_file, options); 401 | defer file.close(); 402 | 403 | const writer = file.writer(); 404 | try writer.writeAll(text); 405 | 406 | std.debug.print("info: installed {s}/{s}\n", .{ destination_directory_path, destination_file }); 407 | } 408 | 409 | fn deleteFile( 410 | allocator: std.mem.Allocator, 411 | prefix: []const u8, 412 | file: []const u8, 413 | warning: []const u8, 414 | ) !void { 415 | const path = try std.fs.path.join(allocator, &[_][]const u8{ dest_directory, prefix, file }); 416 | 417 | std.fs.cwd().deleteFile(path) catch |err| { 418 | if (err == error.FileNotFound) { 419 | std.debug.print("warn: {s}\n", .{warning}); 420 | return; 421 | } 422 | 423 | return err; 424 | }; 425 | 426 | std.debug.print("info: deleted {s}\n", .{path}); 427 | } 428 | 429 | fn deleteTree( 430 | allocator: std.mem.Allocator, 431 | prefix: []const u8, 432 | directory: []const u8, 433 | warning: []const u8, 434 | ) !void { 435 | const path = try std.fs.path.join(allocator, &[_][]const u8{ dest_directory, prefix, directory }); 436 | 437 | var dir = std.fs.cwd().openDir(path, .{}) catch |err| { 438 | if (err == error.FileNotFound) { 439 | std.debug.print("warn: {s}\n", .{warning}); 440 | return; 441 | } 442 | 443 | return err; 444 | }; 445 | dir.close(); 446 | 447 | try std.fs.cwd().deleteTree(path); 448 | 449 | std.debug.print("info: deleted {s}\n", .{path}); 450 | } 451 | -------------------------------------------------------------------------------- /build.zig.zon: -------------------------------------------------------------------------------- 1 | .{ 2 | .name = .ly, 3 | .version = "1.2.0", 4 | .fingerprint = 0xa148ffcc5dc2cb59, 5 | .minimum_zig_version = "0.14.0", 6 | .dependencies = .{ 7 | .clap = .{ 8 | .url = "https://github.com/Hejsil/zig-clap/archive/refs/tags/0.10.0.tar.gz", 9 | .hash = "clap-0.10.0-oBajB434AQBDh-Ei3YtoKIRxZacVPF1iSwp3IX_ZB8f0", 10 | }, 11 | .zigini = .{ 12 | .url = "https://github.com/Kawaii-Ash/zigini/archive/2ed3d417f17fab5b0ee8cad8a63c6d62d7ac1042.tar.gz", 13 | .hash = "zigini-0.3.1-BSkB7XJGAAB2E-sKyzhTaQCBlYBL8yqzE4E_jmSY99sC", 14 | }, 15 | }, 16 | .paths = .{""}, 17 | } 18 | -------------------------------------------------------------------------------- /license.md: -------------------------------------------------------------------------------- 1 | DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE 2 | Version 2, December 2004 3 | 4 | Copyright (C) 2004 Sam Hocevar 5 | 6 | Everyone is permitted to copy and distribute verbatim or modified 7 | copies of this license document, and changing it is allowed as long 8 | as the name is changed. 9 | 10 | DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE 11 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 12 | 13 | 0. You just DO WHAT THE FUCK YOU WANT TO. 14 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Ly - a TUI display manager 2 | 3 | ## Development is now continuing on [Codeberg](https://codeberg.org/AnErrupTion/ly), with the [GitHub](https://github.com/fairyglade/ly) repository becoming a mirror. Issues & pull requests on GitHub will be ignored from now on. 4 | 5 | ![Ly screenshot](.github/screenshot.png "Ly screenshot") 6 | 7 | Ly is a lightweight TUI (ncurses-like) display manager for Linux and BSD. 8 | 9 | ## Dependencies 10 | - Compile-time: 11 | - zig 0.14.0 12 | - libc 13 | - pam 14 | - xcb (optional, required by default; needed for X11 support) 15 | - Runtime (with default config): 16 | - xorg 17 | - xorg-xauth 18 | - shutdown 19 | - brightnessctl 20 | 21 | ### Debian 22 | ``` 23 | # apt install build-essential libpam0g-dev libxcb-xkb-dev 24 | ``` 25 | 26 | ### Fedora 27 | **Warning**: You may encounter issues with SELinux on Fedora. 28 | It is recommended to add a rule for Ly as it currently does not ship one. 29 | 30 | ``` 31 | # dnf install kernel-devel pam-devel libxcb-devel zig 32 | ``` 33 | 34 | ## Support 35 | The following desktop environments were tested with success: 36 | 37 | [Wayland Environments](#supported-wayland-environments) 38 | 39 | [X11 Environments](#supported-x11-environments) 40 | 41 | Ly should work with any X desktop environment, and provides 42 | basic wayland support (sway works very well, for example). 43 | 44 | ## systemd? 45 | Unlike what you may have heard, Ly does not require `systemd`, 46 | and was even specifically designed not to depend on `logind`. 47 | You should be able to make it work easily with a better init, 48 | changing the source code won't be necessary :) 49 | 50 | ## Cloning and Compiling 51 | Clone the repository 52 | ``` 53 | $ git clone https://codeberg.org/AnErrupTion/ly 54 | ``` 55 | 56 | Change the directory to ly 57 | ``` 58 | $ cd ly 59 | ``` 60 | 61 | Compile 62 | ``` 63 | $ zig build 64 | ``` 65 | 66 | Test in the configured tty (tty2 by default) 67 | or a terminal emulator (but authentication won't work) 68 | ``` 69 | $ zig build run 70 | ``` 71 | 72 | **Important**: Running Ly in a terminal emulator as root is *not* recommended. If you 73 | want to properly test Ly, please enable its service (as described below) and reboot 74 | your machine. 75 | 76 | Install Ly for systemd-based systems (the default) 77 | ``` 78 | # zig build installexe 79 | ``` 80 | 81 | Instead of DISPLAY_MANAGER you need to add your DM: 82 | - gdm.service 83 | - sddm.service 84 | - lightdm.service 85 | ``` 86 | # systemctl disable DISPLAY_MANAGER 87 | ``` 88 | 89 | Enable the service 90 | ``` 91 | # systemctl enable ly.service 92 | ``` 93 | 94 | If you need to switch between ttys after Ly's start you also have to 95 | disable getty on Ly's tty to prevent "login" from spawning on top of it 96 | ``` 97 | # systemctl disable getty@tty2.service 98 | ``` 99 | 100 | ### OpenRC 101 | **NOTE 1**: On Gentoo, Ly will disable the `display-manager-init` service in order to run. 102 | 103 | Clone, compile and test. 104 | 105 | Install Ly and the provided OpenRC service 106 | ``` 107 | # zig build installexe -Dinit_system=openrc 108 | ``` 109 | 110 | Enable the service 111 | ``` 112 | # rc-update add ly 113 | ``` 114 | 115 | You can edit which tty Ly will start on by editing the `tty` option in the configuration file. 116 | 117 | If you choose a tty that already has a login/getty running (has a basic login prompt), 118 | then you have to disable getty, so it doesn't respawn on top of ly 119 | ``` 120 | # rc-update del agetty.tty2 121 | ``` 122 | 123 | **NOTE 2**: To avoid a console spawning on top on Ly, comment out the appropriate line from /etc/inittab (default is 2). 124 | 125 | ### runit 126 | ``` 127 | # zig build installexe -Dinit_system=runit 128 | # ln -s /etc/sv/ly /var/service/ 129 | ``` 130 | 131 | By default, ly will run on tty2. To change the tty it must be set in `/etc/ly/config.ini` 132 | 133 | You should as well disable your existing display manager service if needed, e.g.: 134 | 135 | ``` 136 | # rm /var/service/lxdm 137 | ``` 138 | 139 | The agetty service for the tty console where you are running ly should be disabled. 140 | For instance, if you are running ly on tty2 (that's the default, check your `/etc/ly/config.ini`) 141 | you should disable the agetty-tty2 service like this: 142 | 143 | ``` 144 | # rm /var/service/agetty-tty2 145 | ``` 146 | 147 | ### s6 148 | ``` 149 | # zig build installexe -Dinit_system=s6 150 | ``` 151 | 152 | Then, edit `/etc/s6/config/ttyX.conf` and set `SPAWN="no"`, where X is the TTY ID (e.g. `2`). 153 | 154 | Finally, enable the service: 155 | 156 | ``` 157 | # s6-service add default ly-srv 158 | # s6-db-reload 159 | # s6-rc -u change ly-srv 160 | ``` 161 | 162 | ### dinit 163 | ``` 164 | # zig build installexe -Dinit_system=dinit 165 | # dinitctl enable ly 166 | ``` 167 | 168 | In addition to the steps above, you will also have to keep a TTY free within `/etc/dinit.d/config/console.conf`. 169 | 170 | To do that, change `ACTIVE_CONSOLES` so that the tty that ly should use in `/etc/ly/config.ini` is free. 171 | 172 | ### Updating 173 | You can also install Ly without overrding the current configuration file. That's called 174 | *updating*. To update, simply run: 175 | 176 | ``` 177 | # zig build installnoconf 178 | ``` 179 | 180 | You can, of course, still select the init system of your choice when using this command. 181 | 182 | ## Arch Linux Installation 183 | You can install ly from the [`[extra]` repos](https://archlinux.org/packages/extra/x86_64/ly/): 184 | ``` 185 | # pacman -S ly 186 | ``` 187 | 188 | ## Gentoo Installation 189 | You can install ly from the GURU repository: 190 | 191 | Note: If the package is masked, you may need to unmask it using ~amd64 keyword: 192 | ```bash 193 | # echo 'x11-misc/ly ~amd64' >> /etc/portage/package.accept_keywords 194 | ``` 195 | 196 | 1. Enable the GURU repository: 197 | ```bash 198 | # eselect repository enable guru 199 | ``` 200 | 201 | 2. Sync the GURU repository: 202 | ```bash 203 | # emaint sync -r guru 204 | ``` 205 | 206 | 3. Install ly from source: 207 | ```bash 208 | # emerge --ask x11-misc/ly 209 | ``` 210 | 211 | ## Configuration 212 | You can find all the configuration in `/etc/ly/config.ini`. 213 | The file is commented, and includes the default values. 214 | 215 | ## Controls 216 | Use the up and down arrow keys to change the current field, and the 217 | left and right arrow keys to change the target desktop environment 218 | while on the desktop field (above the login field). 219 | 220 | ## .xinitrc 221 | If your .xinitrc doesn't work make sure it is executable and includes a shebang. 222 | This file is supposed to be a shell script! Quoting from xinit's man page: 223 | 224 | > 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. 225 | 226 | On Arch Linux, the example .xinitrc (/etc/X11/xinit/xinitrc) starts like this: 227 | ``` 228 | #!/bin/sh 229 | ``` 230 | 231 | ## Tips 232 | - The numlock and capslock state is printed in the top-right corner. 233 | - Use the F1 and F2 keys to respectively shutdown and reboot. 234 | - Take a look at your .xsession if X doesn't start, as it can interfere 235 | (this file is launched with X to configure the display properly). 236 | 237 | ## Supported Wayland Environments 238 | - budgie 239 | - cosmic 240 | - deepin 241 | - enlightenment 242 | - gnome 243 | - hyprland 244 | - kde 245 | - labwc 246 | - niri 247 | - pantheon 248 | - sway 249 | - weston 250 | 251 | ## Supported X11 Environments 252 | - awesome 253 | - bspwm 254 | - budgie 255 | - cinnamon 256 | - dwm 257 | - enlightenment 258 | - gnome 259 | - kde 260 | - leftwm 261 | - lxde 262 | - mate 263 | - maxx 264 | - pantheon 265 | - qwm 266 | - spectrwm 267 | - windowmaker 268 | - xfce 269 | - xmonad 270 | 271 | 272 | ## Additional Information 273 | The name "Ly" is a tribute to the fairy from the game Rayman. 274 | Ly was tested by oxodao, who is some seriously awesome dude. 275 | -------------------------------------------------------------------------------- /res/config.ini: -------------------------------------------------------------------------------- 1 | # Ly supports 24-bit true color with styling, which means each color is a 32-bit value. 2 | # The format is 0xSSRRGGBB, where SS is the styling, RR is red, GG is green, and BB is blue. 3 | # Here are the possible styling options: 4 | #define TB_BOLD 0x01000000 5 | #define TB_UNDERLINE 0x02000000 6 | #define TB_REVERSE 0x04000000 7 | #define TB_ITALIC 0x08000000 8 | #define TB_BLINK 0x10000000 9 | #define TB_HI_BLACK 0x20000000 10 | #define TB_BRIGHT 0x40000000 11 | #define TB_DIM 0x80000000 12 | # Programmatically, you'd apply them using the bitwise OR operator (|), but because Ly's 13 | # configuration doesn't support using it, you have to manually compute the color value. 14 | # Note that, if you want to use the default color value of the terminal, you can use the 15 | # special value 0x00000000. This means that, if you want to use black, you *must* use 16 | # the styling option TB_HI_BLACK (the RGB values are ignored when using this option). 17 | 18 | # Allow empty password or not when authenticating 19 | allow_empty_password = true 20 | 21 | # The active animation 22 | # none -> Nothing 23 | # doom -> PSX DOOM fire 24 | # matrix -> CMatrix 25 | # colormix -> Color mixing shader 26 | animation = none 27 | 28 | # Stop the animation after some time 29 | # 0 -> Run forever 30 | # 1..2e12 -> Stop the animation after this many seconds 31 | animation_timeout_sec = 0 32 | 33 | # The character used to mask the password 34 | # You can either type it directly as a UTF-8 character (like *), or use a UTF-32 35 | # codepoint (for example 0x2022 for a bullet point) 36 | # If null, the password will be hidden 37 | # Note: you can use a # by escaping it like so: \# 38 | asterisk = * 39 | 40 | # The number of failed authentications before a special animation is played... ;) 41 | auth_fails = 10 42 | 43 | # Background color id 44 | bg = 0x00000000 45 | 46 | # Change the state and language of the big clock 47 | # none -> Disabled (default) 48 | # en -> English 49 | # fa -> Farsi 50 | bigclock = none 51 | 52 | # Blank main box background 53 | # Setting to false will make it transparent 54 | blank_box = true 55 | 56 | # Border foreground color id 57 | border_fg = 0x00FFFFFF 58 | 59 | # Title to show at the top of the main box 60 | # If set to null, none will be shown 61 | box_title = null 62 | 63 | # Brightness increase command 64 | brightness_down_cmd = $PREFIX_DIRECTORY/bin/brightnessctl -q s 10%- 65 | 66 | # Brightness decrease key, or null to disable 67 | brightness_down_key = F5 68 | 69 | # Brightness increase command 70 | brightness_up_cmd = $PREFIX_DIRECTORY/bin/brightnessctl -q s +10% 71 | 72 | # Brightness increase key, or null to disable 73 | brightness_up_key = F6 74 | 75 | # Erase password input on failure 76 | clear_password = false 77 | 78 | # Format string for clock in top right corner (see strftime specification). Example: %c 79 | # If null, the clock won't be shown 80 | clock = null 81 | 82 | # CMatrix animation foreground color id 83 | cmatrix_fg = 0x0000FF00 84 | 85 | # CMatrix animation minimum codepoint. It uses a 16-bit integer 86 | # For Japanese characters for example, you can use 0x3000 here 87 | cmatrix_min_codepoint = 0x21 88 | 89 | # CMatrix animation maximum codepoint. It uses a 16-bit integer 90 | # For Japanese characters for example, you can use 0x30FF here 91 | cmatrix_max_codepoint = 0x7B 92 | 93 | # Color mixing animation first color id 94 | colormix_col1 = 0x00FF0000 95 | 96 | # Color mixing animation second color id 97 | colormix_col2 = 0x000000FF 98 | 99 | # Color mixing animation third color id 100 | colormix_col3 = 0x20000000 101 | 102 | # Console path 103 | console_dev = /dev/console 104 | 105 | # Input box active by default on startup 106 | # Available inputs: info_line, session, login, password 107 | default_input = login 108 | 109 | # DOOM animation top color (low intensity flames) 110 | doom_top_color = 0x00FF0000 111 | 112 | # DOOM animation middle color (medium intensity flames) 113 | doom_middle_color = 0x00FFFF00 114 | 115 | # DOOM animation bottom color (high intensity flames) 116 | doom_bottom_color = 0x00FFFFFF 117 | 118 | # Error background color id 119 | error_bg = 0x00000000 120 | 121 | # Error foreground color id 122 | # Default is red and bold 123 | error_fg = 0x01FF0000 124 | 125 | # Foreground color id 126 | fg = 0x00FFFFFF 127 | 128 | # Remove main box borders 129 | hide_borders = false 130 | 131 | # Remove power management command hints 132 | hide_key_hints = false 133 | 134 | # Initial text to show on the info line 135 | # If set to null, the info line defaults to the hostname 136 | initial_info_text = null 137 | 138 | # Input boxes length 139 | input_len = 34 140 | 141 | # Active language 142 | # Available languages are found in $CONFIG_DIRECTORY/ly/lang/ 143 | lang = en 144 | 145 | # Load the saved desktop and username 146 | load = true 147 | 148 | # Command executed when logging in 149 | # If null, no command will be executed 150 | # Important: the code itself must end with `exec "$@"` in order to launch the session! 151 | # You can also set environment variables in there, they'll persist until logout 152 | login_cmd = null 153 | 154 | # Command executed when logging out 155 | # If null, no command will be executed 156 | # Important: the session will already be terminated when this command is executed, so 157 | # no need to add `exec "$@"` at the end 158 | logout_cmd = null 159 | 160 | # Main box horizontal margin 161 | margin_box_h = 2 162 | 163 | # Main box vertical margin 164 | margin_box_v = 1 165 | 166 | # Event timeout in milliseconds 167 | min_refresh_delta = 5 168 | 169 | # Set numlock on/off at startup 170 | numlock = false 171 | 172 | # Default path 173 | # If null, ly doesn't set a path 174 | path = /sbin:/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin 175 | 176 | # Command executed when pressing restart_key 177 | restart_cmd = /sbin/shutdown -r now 178 | 179 | # Specifies the key used for restart (F1-F12) 180 | restart_key = F2 181 | 182 | # Save the current desktop and login as defaults 183 | save = true 184 | 185 | # Service name (set to ly to use the provided pam config file) 186 | service_name = ly 187 | 188 | # Session log file path 189 | # This will contain stdout and stderr of Wayland sessions 190 | # By default it's saved in the user's home directory 191 | # Important: due to technical limitations, X11 and shell sessions aren't supported, which 192 | # means you won't get any logs from those sessions 193 | session_log = ly-session.log 194 | 195 | # Setup command 196 | setup_cmd = $CONFIG_DIRECTORY/ly/setup.sh 197 | 198 | # Command executed when pressing shutdown_key 199 | shutdown_cmd = /sbin/shutdown -a now 200 | 201 | # Specifies the key used for shutdown (F1-F12) 202 | shutdown_key = F1 203 | 204 | # Command executed when pressing sleep key (can be null) 205 | sleep_cmd = null 206 | 207 | # Specifies the key used for sleep (F1-F12) 208 | sleep_key = F3 209 | 210 | # Center the session name. 211 | text_in_center = false 212 | 213 | # TTY in use 214 | tty = $DEFAULT_TTY 215 | 216 | # Default vi mode 217 | # normal -> normal mode 218 | # insert -> insert mode 219 | vi_default_mode = normal 220 | 221 | # Enable vi keybindings 222 | vi_mode = false 223 | 224 | # Wayland desktop environments 225 | # You can specify multiple directories, 226 | # e.g. /usr/share/wayland-sessions:/usr/local/share/wayland-sessions 227 | waylandsessions = $PREFIX_DIRECTORY/share/wayland-sessions 228 | 229 | # Xorg server command 230 | x_cmd = $PREFIX_DIRECTORY/bin/X 231 | 232 | # Xorg xauthority edition tool 233 | xauth_cmd = $PREFIX_DIRECTORY/bin/xauth 234 | 235 | # xinitrc 236 | # If null, the xinitrc session will be hidden 237 | xinitrc = ~/.xinitrc 238 | 239 | # Xorg desktop environments 240 | # You can specify multiple directories, 241 | # e.g. /usr/share/xsessions:/usr/local/share/xsessions 242 | xsessions = $PREFIX_DIRECTORY/share/xsessions 243 | -------------------------------------------------------------------------------- /res/lang/ar.ini: -------------------------------------------------------------------------------- 1 | authenticating = جاري المصادقة... 2 | brightness_down = خفض السطوع 3 | brightness_up = رفع السطوع 4 | capslock = capslock 5 | err_alloc = فشل في تخصيص الذاكرة 6 | err_bounds = out-of-bounds index 7 | err_brightness_change = فشل في تغيير سطوع الشاشة 8 | err_chdir = فشل في فتح مجلد المنزل 9 | err_config = فشل في تفسير ملف الإعدادات 10 | err_console_dev = فشل في الوصول إلى جهاز وحدة التحكم 11 | err_dgn_oob = رسالة سجل (Log) 12 | err_domain = اسم نطاق غير صالح 13 | err_empty_password = لا يُسمح بكلمة مرور فارغة 14 | err_envlist = فشل في جلب قائمة المتغيرات البيئية 15 | err_hostname = فشل في جلب اسم المضيف (Hostname) 16 | err_mlock = فشل في تأمين ذاكرة كلمة المرور (mlock) 17 | err_null = مؤشر فارغ (Null pointer) 18 | err_numlock = فشل في ضبط Num Lock 19 | err_pam = فشل في معاملة PAM 20 | err_pam_abort = تم إلغاء معاملة PAM 21 | err_pam_acct_expired = الحساب منتهي الصلاحية 22 | err_pam_auth = خطأ في المصادقة (Authentication error) 23 | err_pam_authinfo_unavail = فشل في الحصول على معلومات المستخدم 24 | err_pam_authok_reqd = انتهت صلاحية رمز المصادقة (Token) 25 | err_pam_buf = خطأ في ذاكرة التخزين المؤقت (Buffer) 26 | err_pam_cred_err = فشل في تعيين بيانات الاعتماد (Credentials) 27 | err_pam_cred_expired = بيانات الاعتماد منتهية الصلاحية 28 | err_pam_cred_insufficient = بيانات الاعتماد غير كافية 29 | err_pam_cred_unavail = فشل في الحصول على بيانات الاعتماد 30 | err_pam_maxtries = تم بلوغ الحد الأقصى لمحاولات المصادقة 31 | err_pam_perm_denied = تم رفض الوصول (Permission denied) 32 | err_pam_session = خطأ في جلسة المستخدم (Session error) 33 | err_pam_sys = خطأ في النظام (System error) 34 | err_pam_user_unknown = المستخدم غير موجود 35 | err_path = فشل في تعيين متغير PATH 36 | err_perm_dir = فشل في تغيير المجلد الحالي 37 | err_perm_group = فشل في تخفيض صلاحيات المجموعة (Group permissions) 38 | err_perm_user = فشل في تخفيض صلاحيات المستخدم (User permissions) 39 | err_pwnam = فشل في جلب معلومات المستخدم 40 | err_sleep = فشل في تنفيذ أمر sleep 41 | err_tty_ctrl = فشل في نقل تحكم الطرفية (TTY) 42 | err_user_gid = فشل في تعيين معرّف المجموعة (GID) للمستخدم 43 | err_user_init = فشل في تهيئة بيانات المستخدم 44 | err_user_uid = فشل في تعيين معرّف المستخدم (UID) 45 | err_xauth = فشل في تنفيذ أمر xauth 46 | err_xcb_conn = فشل في الاتصال بمكتبة XCB 47 | err_xsessions_dir = فشل في العثور على مجلد Xsessions 48 | err_xsessions_open = فشل في فتح مجلد Xsessions 49 | insert = ادخال 50 | login = تسجيل الدخول 51 | logout = تم تسجيل خروجك 52 | no_x11_support = تم تعطيل دعم x11 اثناء وقت الـ compile 53 | normal = عادي 54 | numlock = numlock 55 | other = اخر 56 | password = كلمة السر 57 | restart = اعادة التشغيل 58 | shell = shell 59 | shutdown = ايقاف التشغيل 60 | sleep = وضع السكون 61 | wayland = wayland 62 | x11 = x11 63 | xinitrc = xinitrc 64 | -------------------------------------------------------------------------------- /res/lang/cat.ini: -------------------------------------------------------------------------------- 1 | authenticating = autenticant... 2 | brightness_down = abaixar brillantor 3 | brightness_up = apujar brillantor 4 | capslock = Bloq Majús 5 | err_alloc = assignació de memòria fallida 6 | err_bounds = índex fora de límits 7 | err_brightness_change = error en canviar la brillantor 8 | err_chdir = error en obrir la carpeta home 9 | 10 | err_console_dev = error en accedir a la consola 11 | err_dgn_oob = missatge de registre 12 | err_domain = domini invàlid 13 | 14 | err_envlist = error en obtenir l'envlist 15 | err_hostname = error en obtenir el nom de l'amfitrió 16 | err_mlock = error en bloquejar la memòria de clau 17 | err_null = punter nul 18 | err_numlock = error en establir el Bloq num 19 | err_pam = error en la transacció pam 20 | err_pam_abort = transacció pam avortada 21 | err_pam_acct_expired = compte expirat 22 | err_pam_auth = error d'autenticació 23 | err_pam_authinfo_unavail = error en obtenir la informació de l'usuari 24 | err_pam_authok_reqd = token expirat 25 | err_pam_buf = error en la memòria intermèdia 26 | err_pam_cred_err = error en establir les credencials 27 | err_pam_cred_expired = credencials expirades 28 | err_pam_cred_insufficient = credencials insuficients 29 | err_pam_cred_unavail = error en obtenir credencials 30 | err_pam_maxtries = s'ha assolit al nombre màxim d'intents 31 | err_pam_perm_denied = permís denegat 32 | err_pam_session = error de sessió 33 | err_pam_sys = error de sistema 34 | err_pam_user_unknown = usuari desconegut 35 | err_path = error en establir la ruta 36 | err_perm_dir = error en canviar el directori actual 37 | err_perm_group = error en degradar els permisos de grup 38 | err_perm_user = error en degradar els permisos de l'usuari 39 | err_pwnam = error en obtenir la informació de l'usuari 40 | 41 | 42 | err_user_gid = error en establir el GID de l'usuari 43 | err_user_init = error en inicialitzar usuari 44 | err_user_uid = error en establir l'UID de l'usuari 45 | err_xauth = error en la comanda xauth 46 | err_xcb_conn = error en la connexió xcb 47 | err_xsessions_dir = error en trobar la carpeta de sessions 48 | err_xsessions_open = error en obrir la carpeta de sessions 49 | insert = inserir 50 | login = iniciar sessió 51 | logout = sessió tancada 52 | no_x11_support = el suport per x11 ha estat desactivat en la compilació 53 | normal = normal 54 | numlock = Bloq Num 55 | 56 | password = Clau 57 | restart = reiniciar 58 | shell = shell 59 | shutdown = aturar 60 | sleep = suspendre 61 | wayland = wayland 62 | x11 = x11 63 | xinitrc = xinitrc 64 | -------------------------------------------------------------------------------- /res/lang/cs.ini: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | capslock = capslock 5 | err_alloc = alokace paměti selhala 6 | err_bounds = index je mimo hranice pole 7 | 8 | err_chdir = nelze otevřít domovský adresář 9 | 10 | err_console_dev = chyba při přístupu do konzole 11 | err_dgn_oob = zpráva protokolu 12 | err_domain = neplatná doména 13 | 14 | 15 | err_hostname = nelze získat název hostitele 16 | err_mlock = uzamčení paměti hesel selhalo 17 | err_null = nulový ukazatel 18 | 19 | err_pam = pam transakce selhala 20 | err_pam_abort = pam transakce přerušena 21 | err_pam_acct_expired = platnost účtu vypršela 22 | err_pam_auth = chyba autentizace 23 | err_pam_authinfo_unavail = nelze získat informace o uživateli 24 | err_pam_authok_reqd = platnost tokenu vypršela 25 | err_pam_buf = chyba vyrovnávací paměti 26 | err_pam_cred_err = nelze nastavit pověření 27 | err_pam_cred_expired = platnost pověření vypršela 28 | err_pam_cred_insufficient = nedostatečné pověření 29 | err_pam_cred_unavail = nepodařilo se získat pověření 30 | err_pam_maxtries = byl dosažen maximální počet pokusů 31 | err_pam_perm_denied = přístup odepřen 32 | err_pam_session = chyba relace 33 | err_pam_sys = systemová chyba 34 | err_pam_user_unknown = neznámý uživatel 35 | err_path = nepodařilo se nastavit cestu 36 | err_perm_dir = nepodařilo se změnit adresář 37 | err_perm_group = nepodařilo se snížit skupinová oprávnění 38 | err_perm_user = nepodařilo se snížit uživatelská oprávnění 39 | err_pwnam = nelze získat informace o uživateli 40 | 41 | 42 | err_user_gid = nastavení GID uživatele selhalo 43 | err_user_init = inicializace uživatele selhala 44 | err_user_uid = nastavení UID uživateli selhalo 45 | 46 | 47 | err_xsessions_dir = nepodařilo se najít složku relací 48 | err_xsessions_open = nepodařilo se otevřít složku relací 49 | 50 | login = uživatel 51 | logout = odhlášen 52 | 53 | 54 | numlock = numlock 55 | 56 | password = heslo 57 | restart = restartovat 58 | shell = příkazový řádek 59 | shutdown = vypnout 60 | 61 | wayland = wayland 62 | 63 | xinitrc = xinitrc 64 | -------------------------------------------------------------------------------- /res/lang/de.ini: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | capslock = Feststelltaste 5 | err_alloc = Speicherzuweisung fehlgeschlagen 6 | err_bounds = Listenindex ist außerhalb des Bereichs 7 | 8 | err_chdir = Fehler beim oeffnen des home-ordners 9 | 10 | err_console_dev = Zugriff auf die Konsole fehlgeschlagen 11 | err_dgn_oob = Protokoll Nachricht 12 | err_domain = Unzulaessige domain 13 | 14 | 15 | err_hostname = Holen des Hostnames fehlgeschlagen 16 | err_mlock = Abschließen des Passwortspeichers fehlgeschlagen 17 | err_null = Null Zeiger 18 | 19 | err_pam = pam Transaktion fehlgeschlagen 20 | err_pam_abort = pam Transaktion abgebrochen 21 | err_pam_acct_expired = Benutzerkonto abgelaufen 22 | err_pam_auth = Authentifizierungs Fehler 23 | err_pam_authinfo_unavail = holen der Benutzerinformationen fehlgeschlagen 24 | err_pam_authok_reqd = Schluessel abgelaufen 25 | err_pam_buf = Speicherpufferfehler 26 | err_pam_cred_err = Fehler beim setzen der Anmeldedaten 27 | err_pam_cred_expired = Anmeldedaten abgelaufen 28 | err_pam_cred_insufficient = Anmeldedaten unzureichend 29 | err_pam_cred_unavail = Fehler beim holen der Anmeldedaten 30 | err_pam_maxtries = Maximale Versuche erreicht 31 | err_pam_perm_denied = Zugriff Verweigert 32 | err_pam_session = Sitzungsfehler 33 | err_pam_sys = Systemfehler 34 | err_pam_user_unknown = Unbekannter Nutzer 35 | err_path = Fehler beim setzen des Pfades 36 | err_perm_dir = Fehler beim wechseln des Ordners 37 | err_perm_group = Fehler beim heruntersetzen der Gruppen Berechtigungen 38 | err_perm_user = Fehler beim heruntersetzen der Nutzer Berechtigungen 39 | err_pwnam = Holen der Benutzerinformationen fehlgeschlagen 40 | 41 | 42 | err_user_gid = Fehler beim setzen der Gruppen Id des Nutzers 43 | err_user_init = Initialisierung des Nutzers fehlgeschlagen 44 | err_user_uid = Setzen der Benutzer Id fehlgeschlagen 45 | 46 | 47 | err_xsessions_dir = Fehler beim finden des Sitzungsordners 48 | err_xsessions_open = Fehler beim öffnen des Sitzungsordners 49 | 50 | login = Anmelden 51 | logout = Abgemeldet 52 | 53 | 54 | numlock = Numtaste 55 | 56 | password = Passwort 57 | restart = Neustarten 58 | shell = shell 59 | shutdown = Herunterfahren 60 | 61 | wayland = wayland 62 | 63 | xinitrc = xinitrc 64 | -------------------------------------------------------------------------------- /res/lang/en.ini: -------------------------------------------------------------------------------- 1 | authenticating = authenticating... 2 | brightness_down = decrease brightness 3 | brightness_up = increase brightness 4 | capslock = capslock 5 | err_alloc = failed memory allocation 6 | err_bounds = out-of-bounds index 7 | err_brightness_change = failed to change brightness 8 | err_chdir = failed to open home folder 9 | err_config = unable to parse config file 10 | err_console_dev = failed to access console 11 | err_dgn_oob = log message 12 | err_domain = invalid domain 13 | err_empty_password = empty password not allowed 14 | err_envlist = failed to get envlist 15 | err_hostname = failed to get hostname 16 | err_mlock = failed to lock password memory 17 | err_null = null pointer 18 | err_numlock = failed to set numlock 19 | err_pam = pam transaction failed 20 | err_pam_abort = pam transaction aborted 21 | err_pam_acct_expired = account expired 22 | err_pam_auth = authentication error 23 | err_pam_authinfo_unavail = failed to get user info 24 | err_pam_authok_reqd = token expired 25 | err_pam_buf = memory buffer error 26 | err_pam_cred_err = failed to set credentials 27 | err_pam_cred_expired = credentials expired 28 | err_pam_cred_insufficient = insufficient credentials 29 | err_pam_cred_unavail = failed to get credentials 30 | err_pam_maxtries = reached maximum tries limit 31 | err_pam_perm_denied = permission denied 32 | err_pam_session = session error 33 | err_pam_sys = system error 34 | err_pam_user_unknown = unknown user 35 | err_path = failed to set path 36 | err_perm_dir = failed to change current directory 37 | err_perm_group = failed to downgrade group permissions 38 | err_perm_user = failed to downgrade user permissions 39 | err_pwnam = failed to get user info 40 | err_sleep = failed to execute sleep command 41 | err_tty_ctrl = tty control transfer failed 42 | err_user_gid = failed to set user GID 43 | err_user_init = failed to initialize user 44 | err_user_uid = failed to set user UID 45 | err_xauth = xauth command failed 46 | err_xcb_conn = xcb connection failed 47 | err_xsessions_dir = failed to find sessions folder 48 | err_xsessions_open = failed to open sessions folder 49 | insert = insert 50 | login = login 51 | logout = logged out 52 | no_x11_support = x11 support disabled at compile-time 53 | normal = normal 54 | numlock = numlock 55 | other = other 56 | password = password 57 | restart = reboot 58 | shell = shell 59 | shutdown = shutdown 60 | sleep = sleep 61 | wayland = wayland 62 | x11 = x11 63 | xinitrc = xinitrc 64 | -------------------------------------------------------------------------------- /res/lang/es.ini: -------------------------------------------------------------------------------- 1 | authenticating = autenticando... 2 | brightness_down = bajar brillo 3 | brightness_up = subir brillo 4 | capslock = Bloq Mayús 5 | err_alloc = asignación de memoria fallida 6 | err_bounds = índice fuera de límites 7 | 8 | err_chdir = error al abrir la carpeta home 9 | 10 | err_console_dev = error al acceder a la consola 11 | err_dgn_oob = mensaje de registro 12 | err_domain = dominio inválido 13 | 14 | 15 | err_hostname = error al obtener el nombre de host 16 | err_mlock = error al bloquear la contraseña de memoria 17 | err_null = puntero nulo 18 | 19 | err_pam = error en la transacción pam 20 | err_pam_abort = transacción pam abortada 21 | err_pam_acct_expired = cuenta expirada 22 | err_pam_auth = error de autenticación 23 | err_pam_authinfo_unavail = error al obtener información del usuario 24 | err_pam_authok_reqd = token expirado 25 | err_pam_buf = error de la memoria intermedia 26 | err_pam_cred_err = error al establecer las credenciales 27 | err_pam_cred_expired = credenciales expiradas 28 | err_pam_cred_insufficient = credenciales insuficientes 29 | err_pam_cred_unavail = error al obtener credenciales 30 | err_pam_maxtries = se ha alcanzado el límite de intentos 31 | err_pam_perm_denied = permiso denegado 32 | err_pam_session = error de sesión 33 | err_pam_sys = error de sistema 34 | err_pam_user_unknown = usuario desconocido 35 | err_path = error al establecer la ruta 36 | err_perm_dir = error al cambiar el directorio actual 37 | err_perm_group = error al degradar los permisos del grupo 38 | err_perm_user = error al degradar los permisos del usuario 39 | err_pwnam = error al obtener la información del usuario 40 | 41 | 42 | err_user_gid = error al establecer el GID del usuario 43 | err_user_init = error al inicializar usuario 44 | err_user_uid = error al establecer el UID del usuario 45 | 46 | 47 | err_xsessions_dir = error al buscar la carpeta de sesiones 48 | err_xsessions_open = error al abrir la carpeta de sesiones 49 | insert = insertar 50 | login = usuario 51 | logout = cerrar sesión 52 | no_x11_support = soporte para x11 deshabilitado en tiempo de compilación 53 | normal = normal 54 | numlock = Bloq Num 55 | other = otro 56 | password = contraseña 57 | restart = reiniciar 58 | shell = shell 59 | shutdown = apagar 60 | sleep = suspender 61 | wayland = wayland 62 | 63 | xinitrc = xinitrc 64 | -------------------------------------------------------------------------------- /res/lang/fr.ini: -------------------------------------------------------------------------------- 1 | authenticating = authentification... 2 | brightness_down = diminuer la luminosité 3 | brightness_up = augmenter la luminosité 4 | capslock = verr.maj 5 | err_alloc = échec d'allocation mémoire 6 | err_bounds = indice hors-limite 7 | err_brightness_change = échec du changement de luminosité 8 | err_chdir = échec de l'ouverture du répertoire home 9 | err_config = échec de lecture du fichier de configuration 10 | err_console_dev = échec d'accès à la console 11 | err_dgn_oob = message 12 | err_domain = domaine invalide 13 | err_empty_password = mot de passe vide non autorisé 14 | err_envlist = échec de lecture de la liste d'environnement 15 | err_hostname = échec de lecture du nom d'hôte 16 | err_mlock = échec du verrouillage mémoire 17 | err_null = pointeur null 18 | err_numlock = échec de modification du verr.num 19 | err_pam = échec de la transaction pam 20 | err_pam_abort = transaction pam avortée 21 | err_pam_acct_expired = compte expiré 22 | err_pam_auth = erreur d'authentification 23 | err_pam_authinfo_unavail = échec de l'obtention des infos utilisateur 24 | err_pam_authok_reqd = tiquet expiré 25 | err_pam_buf = erreur de mémoire tampon 26 | err_pam_cred_err = échec de la modification des identifiants 27 | err_pam_cred_expired = identifiants expirés 28 | err_pam_cred_insufficient = identifiants insuffisants 29 | err_pam_cred_unavail = échec de l'obtention des identifiants 30 | err_pam_maxtries = limite d'essais atteinte 31 | err_pam_perm_denied = permission refusée 32 | err_pam_session = erreur de session 33 | err_pam_sys = erreur système 34 | err_pam_user_unknown = utilisateur inconnu 35 | err_path = échec de la modification du path 36 | err_perm_dir = échec de changement de répertoire 37 | err_perm_group = échec du déclassement des permissions de groupe 38 | err_perm_user = échec du déclassement des permissions utilisateur 39 | err_pwnam = échec de lecture des infos utilisateur 40 | err_sleep = échec de l'exécution de la commande de veille 41 | err_tty_ctrl = échec du transfert de contrôle du terminal 42 | err_user_gid = échec de modification du GID 43 | err_user_init = échec d'initialisation de l'utilisateur 44 | err_user_uid = échec de modification du UID 45 | err_xauth = échec de la commande xauth 46 | err_xcb_conn = échec de la connexion xcb 47 | err_xsessions_dir = échec de la recherche du dossier de sessions 48 | err_xsessions_open = échec de l'ouverture du dossier de sessions 49 | insert = insertion 50 | login = identifiant 51 | logout = déconnecté 52 | no_x11_support = support pour x11 désactivé lors de la compilation 53 | normal = normal 54 | numlock = verr.num 55 | other = autre 56 | password = mot de passe 57 | restart = redémarrer 58 | shell = shell 59 | shutdown = éteindre 60 | sleep = veille 61 | wayland = wayland 62 | x11 = x11 63 | xinitrc = xinitrc 64 | -------------------------------------------------------------------------------- /res/lang/it.ini: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | capslock = capslock 5 | err_alloc = impossibile allocare memoria 6 | err_bounds = indice fuori limite 7 | 8 | err_chdir = impossibile aprire home directory 9 | 10 | err_console_dev = impossibile aprire console 11 | err_dgn_oob = messaggio log 12 | err_domain = dominio non valido 13 | 14 | 15 | err_hostname = impossibile ottenere hostname 16 | err_mlock = impossibile ottenere lock per la password in memoria 17 | err_null = puntatore nullo 18 | 19 | err_pam = transazione PAM fallita 20 | err_pam_abort = transazione PAM interrotta 21 | err_pam_acct_expired = account scaduto 22 | err_pam_auth = errore di autenticazione 23 | err_pam_authinfo_unavail = impossibile ottenere informazioni utente 24 | err_pam_authok_reqd = token scaduto 25 | err_pam_buf = errore buffer memoria 26 | err_pam_cred_err = impossibile impostare credenziali 27 | err_pam_cred_expired = credenziali scadute 28 | err_pam_cred_insufficient = credenziali insufficienti 29 | err_pam_cred_unavail = impossibile ottenere credenziali 30 | err_pam_maxtries = raggiunto limite tentativi 31 | err_pam_perm_denied = permesso negato 32 | err_pam_session = errore di sessione 33 | err_pam_sys = errore di sistema 34 | err_pam_user_unknown = utente sconosciuto 35 | err_path = impossibile impostare percorso 36 | err_perm_dir = impossibile cambiare directory corrente 37 | err_perm_group = impossibile ridurre permessi gruppo 38 | err_perm_user = impossibile ridurre permessi utente 39 | err_pwnam = impossibile ottenere dati utente 40 | 41 | 42 | err_user_gid = impossibile impostare GID utente 43 | err_user_init = impossibile inizializzare utente 44 | err_user_uid = impossible impostare UID utente 45 | 46 | 47 | err_xsessions_dir = impossibile localizzare cartella sessioni 48 | err_xsessions_open = impossibile aprire cartella sessioni 49 | 50 | login = username 51 | logout = scollegato 52 | 53 | 54 | numlock = numlock 55 | 56 | password = password 57 | restart = riavvio 58 | shell = shell 59 | shutdown = arresto 60 | 61 | wayland = wayland 62 | 63 | xinitrc = xinitrc 64 | -------------------------------------------------------------------------------- /res/lang/normalize_lang_files.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from pathlib import Path 4 | from sys import stderr 5 | 6 | 7 | def process_lang_file(path: Path, lang_keys: list[str]) -> None: 8 | # read key-value-pairs from lang file into dict 9 | existing_entries = {} 10 | with open(path, "r", encoding="UTF-8") as fh: 11 | while line := fh.readline(): 12 | try: 13 | key, value = line.split("=", 1) 14 | existing_entries[key.strip()] = value.strip() 15 | except ValueError: # line does not contain '=' 16 | continue 17 | 18 | # re-write current lang file with entries in order of occurence in `lang_keys` 19 | # and with empty lines for missing translations 20 | with open(path, "w", encoding="UTF-8") as fh: 21 | for item in lang_keys: 22 | try: 23 | fh.write(f"{item} = {existing_entries[item]}\n") 24 | except KeyError: # no translation for `item` yet 25 | fh.write("\n") 26 | 27 | 28 | def main() -> None: 29 | zig_lang_file = Path(__file__).parent.joinpath("../../src/config/Lang.zig").resolve() 30 | if not zig_lang_file.exists(): 31 | print(f"ERROR: File '{zig_lang_file.as_posix()}' does not exist. Exiting.", file=stderr) 32 | exit(1) 33 | 34 | # read "language keys" from `zig_lang_file` into list 35 | lang_keys = [] 36 | with open(zig_lang_file, "r", encoding="UTF-8") as fh: 37 | while line := fh.readline(): 38 | # only process lines that are not empty or no comments 39 | if not (line.strip() == "" or line.startswith("//")): 40 | lang_keys.append(line.split(":")[0].strip()) 41 | 42 | lang_files = [f for f in Path.iterdir(Path(__file__).parent) if f.name.endswith(".ini") and f.is_file()] 43 | 44 | for file in lang_files: 45 | process_lang_file(file, lang_keys) 46 | 47 | 48 | if __name__ == "__main__": 49 | main() 50 | -------------------------------------------------------------------------------- /res/lang/pl.ini: -------------------------------------------------------------------------------- 1 | authenticating = uwierzytelnianie... 2 | brightness_down = zmniejsz jasność 3 | brightness_up = zwiększ jasność 4 | capslock = capslock 5 | err_alloc = nieudana alokacja pamięci 6 | err_bounds = indeks poza zakresem 7 | err_brightness_change = nie udało się zmienić jasności 8 | err_chdir = nie udało się otworzyć folderu domowego 9 | err_config = nie można przetworzyć pliku konfiguracyjnego 10 | err_console_dev = nie udało się uzyskać dostępu do konsoli 11 | err_dgn_oob = wiadomość loga 12 | err_domain = niepoprawna domena 13 | err_empty_password = puste hasło jest niedozwolone 14 | err_envlist = nie udało się pobrać listy zmiennych środowiskowych 15 | err_hostname = nie udało się uzyskać nazwy hosta 16 | err_mlock = nie udało się zablokować pamięci haseł 17 | err_null = pusty wskaźnik 18 | err_numlock = nie udało się ustawić numlock 19 | err_pam = transakcja pam nieudana 20 | err_pam_abort = transakcja pam przerwana 21 | err_pam_acct_expired = konto wygasło 22 | err_pam_auth = błąd uwierzytelniania 23 | err_pam_authinfo_unavail = nie udało się zdobyć informacji o użytkowniku 24 | err_pam_authok_reqd = token wygasł 25 | err_pam_buf = błąd bufora pamięci 26 | err_pam_cred_err = nie udało się ustawić uwierzytelnienia 27 | err_pam_cred_expired = uwierzytelnienie wygasło 28 | err_pam_cred_insufficient = niewystarczające uwierzytelnienie 29 | err_pam_cred_unavail = nie udało się uzyskać uwierzytelnienia 30 | err_pam_maxtries = osiągnięto limit prób 31 | err_pam_perm_denied = odmowa dostępu 32 | err_pam_session = błąd sesji 33 | err_pam_sys = błąd systemu 34 | err_pam_user_unknown = nieznany użytkownik 35 | err_path = nie udało się ustawić ścieżki 36 | err_perm_dir = nie udało się zmienić obecnego katalogu 37 | err_perm_group = nie udało się obniżyć uprawnień grupy 38 | err_perm_user = nie udało się obniżyć uprawnień użytkownika 39 | err_pwnam = nie udało się uzyskać informacji o użytkowniku 40 | err_sleep = nie udało się wykonać polecenia sleep 41 | err_tty_ctrl = nie udało się przekazać kontroli tty 42 | err_user_gid = nie udało się ustawić GID użytkownika 43 | err_user_init = nie udało się zainicjalizować użytkownika 44 | err_user_uid = nie udało się ustawić UID użytkownika 45 | err_xauth = polecenie xauth nie powiodło się 46 | err_xcb_conn = połączenie xcb nie powiodło się 47 | err_xsessions_dir = nie udało się znaleźć folderu sesji 48 | err_xsessions_open = nie udało się otworzyć folderu sesji 49 | insert = wstaw 50 | login = login 51 | logout = wylogowano 52 | no_x11_support = wsparcie X11 wyłączone podczas kompilacji 53 | normal = normalny 54 | numlock = numlock 55 | other = inny 56 | password = hasło 57 | restart = uruchom ponownie 58 | shell = powłoka 59 | shutdown = wyłącz 60 | sleep = uśpij 61 | wayland = wayland 62 | x11 = x11 63 | xinitrc = xinitrc 64 | -------------------------------------------------------------------------------- /res/lang/pt.ini: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | capslock = capslock 5 | err_alloc = erro na atribuição de memória 6 | err_bounds = índice fora de limites 7 | 8 | err_chdir = erro ao abrir a pasta home 9 | 10 | err_console_dev = erro ao aceder à consola 11 | err_dgn_oob = mensagem de registo 12 | err_domain = domínio inválido 13 | 14 | 15 | err_hostname = erro ao obter o nome do host 16 | err_mlock = erro de bloqueio de memória 17 | err_null = ponteiro nulo 18 | 19 | err_pam = erro na transação pam 20 | err_pam_abort = transação pam abortada 21 | err_pam_acct_expired = conta expirada 22 | err_pam_auth = erro de autenticação 23 | err_pam_authinfo_unavail = erro ao obter informação do utilizador 24 | err_pam_authok_reqd = token expirado 25 | err_pam_buf = erro de buffer de memória 26 | err_pam_cred_err = erro ao definir credenciais 27 | err_pam_cred_expired = credenciais expiradas 28 | err_pam_cred_insufficient = credenciais insuficientes 29 | err_pam_cred_unavail = erro ao obter credenciais 30 | err_pam_maxtries = limite máximo de tentativas atingido 31 | err_pam_perm_denied = permissão negada 32 | err_pam_session = erro de sessão 33 | err_pam_sys = erro de sistema 34 | err_pam_user_unknown = utilizador desconhecido 35 | err_path = erro ao definir o caminho de acesso 36 | err_perm_dir = erro ao alterar o diretório atual 37 | err_perm_group = erro ao reduzir as permissões do grupo 38 | err_perm_user = erro ao reduzir as permissões do utilizador 39 | err_pwnam = erro ao obter informação do utilizador 40 | 41 | 42 | err_user_gid = erro ao definir o GID do utilizador 43 | err_user_init = erro ao iniciar o utilizador 44 | err_user_uid = erro ao definir o UID do utilizador 45 | 46 | 47 | err_xsessions_dir = erro ao localizar a pasta das sessões 48 | err_xsessions_open = erro ao abrir a pasta das sessões 49 | 50 | login = iniciar sessão 51 | logout = terminar sessão 52 | 53 | 54 | numlock = numlock 55 | 56 | password = palavra-passe 57 | restart = reiniciar 58 | shell = shell 59 | shutdown = encerrar 60 | 61 | wayland = wayland 62 | 63 | xinitrc = xinitrc 64 | -------------------------------------------------------------------------------- /res/lang/pt_BR.ini: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | capslock = caixa alta 5 | err_alloc = alocação de memória malsucedida 6 | err_bounds = índice fora de limites 7 | 8 | err_chdir = não foi possível abrir o diretório home 9 | 10 | err_console_dev = não foi possível acessar o console 11 | err_dgn_oob = mensagem de log 12 | err_domain = domínio inválido 13 | 14 | 15 | err_hostname = não foi possível obter o nome do host 16 | err_mlock = bloqueio da memória de senha malsucedido 17 | err_null = ponteiro nulo 18 | 19 | err_pam = transação pam malsucedida 20 | err_pam_abort = transação pam abortada 21 | err_pam_acct_expired = conta expirada 22 | err_pam_auth = erro de autenticação 23 | err_pam_authinfo_unavail = não foi possível obter informações do usuário 24 | err_pam_authok_reqd = token expirada 25 | err_pam_buf = erro de buffer de memória 26 | err_pam_cred_err = erro para definir credenciais 27 | err_pam_cred_expired = credenciais expiradas 28 | err_pam_cred_insufficient = credenciais insuficientes 29 | err_pam_cred_unavail = não foi possível obter credenciais 30 | err_pam_maxtries = limite máximo de tentativas atingido 31 | err_pam_perm_denied = permissão negada 32 | err_pam_session = erro de sessão 33 | err_pam_sys = erro de sistema 34 | err_pam_user_unknown = usuário desconhecido 35 | err_path = não foi possível definir o caminho 36 | err_perm_dir = não foi possível alterar o diretório atual 37 | err_perm_group = não foi possível reduzir as permissões de grupo 38 | err_perm_user = não foi possível reduzir as permissões de usuário 39 | err_pwnam = não foi possível obter informações do usuário 40 | 41 | 42 | err_user_gid = não foi possível definir o GID do usuário 43 | err_user_init = não foi possível iniciar o usuário 44 | err_user_uid = não foi possível definir o UID do usuário 45 | 46 | 47 | err_xsessions_dir = não foi possível encontrar a pasta das sessões 48 | err_xsessions_open = não foi possível abrir a pasta das sessões 49 | 50 | login = conectar 51 | logout = desconectado 52 | 53 | 54 | numlock = numlock 55 | 56 | password = senha 57 | restart = reiniciar 58 | shell = shell 59 | shutdown = desligar 60 | 61 | wayland = wayland 62 | 63 | xinitrc = xinitrc 64 | -------------------------------------------------------------------------------- /res/lang/ro.ini: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | capslock = capslock 5 | 6 | 7 | 8 | 9 | 10 | err_console_dev = nu s-a putut accesa consola 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | err_pam_abort = tranzacţie pam anulată 21 | err_pam_acct_expired = cont expirat 22 | err_pam_auth = eroare de autentificare 23 | err_pam_authinfo_unavail = nu s-au putut obţine informaţii despre utilizator 24 | err_pam_authok_reqd = token expirat 25 | err_pam_buf = eroare de memorie (buffer) 26 | err_pam_cred_err = nu s-au putut seta date de identificare (credentials) 27 | err_pam_cred_expired = datele de identificare (credentials) au expirat 28 | err_pam_cred_insufficient = date de identificare (credentials) insuficiente 29 | err_pam_cred_unavail = nu s-au putut obţine date de indentificare (credentials) 30 | err_pam_maxtries = s-a atins numărul maxim de încercări 31 | err_pam_perm_denied = acces interzis 32 | err_pam_session = eroare de sesiune 33 | err_pam_sys = eroare de sistem 34 | err_pam_user_unknown = utilizator necunoscut 35 | 36 | err_perm_dir = nu s-a putut schimba dosarul (folder-ul) curent 37 | err_perm_group = nu s-a putut face downgrade permisiunilor de grup 38 | err_perm_user = nu s-a putut face downgrade permisiunilor de utilizator 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | login = utilizator 51 | logout = opreşte sesiunea 52 | 53 | 54 | numlock = numlock 55 | 56 | password = parolă 57 | restart = resetează 58 | shell = shell 59 | shutdown = opreşte sistemul 60 | 61 | wayland = wayland 62 | 63 | xinitrc = xinitrc 64 | -------------------------------------------------------------------------------- /res/lang/ru.ini: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | capslock = capslock 5 | err_alloc = не удалось выделить память 6 | err_bounds = за пределами индекса 7 | 8 | err_chdir = не удалось открыть домашнюю папку 9 | 10 | err_console_dev = не удалось получить доступ к консоли 11 | err_dgn_oob = отладочное сообщение (log) 12 | err_domain = неверный домен 13 | 14 | 15 | err_hostname = не удалось получить имя хоста 16 | err_mlock = сбой блокировки памяти 17 | err_null = нулевой указатель 18 | 19 | err_pam = pam транзакция не удалась 20 | err_pam_abort = pam транзакция прервана 21 | err_pam_acct_expired = срок действия аккаунта истёк 22 | err_pam_auth = ошибка аутентификации 23 | err_pam_authinfo_unavail = не удалось получить информацию о пользователе 24 | err_pam_authok_reqd = токен истёк 25 | err_pam_buf = ошибка буфера памяти 26 | err_pam_cred_err = не удалось установить полномочия 27 | err_pam_cred_expired = полномочия истекли 28 | err_pam_cred_insufficient = недостаточно полномочий 29 | err_pam_cred_unavail = не удалось получить полномочия 30 | err_pam_maxtries = лимит попыток исчерпан 31 | err_pam_perm_denied = доступ запрещён 32 | err_pam_session = ошибка сессии 33 | err_pam_sys = системная ошибка 34 | err_pam_user_unknown = неизвестный пользователь 35 | err_path = не удалось установить путь 36 | err_perm_dir = не удалось изменить текущий каталог 37 | err_perm_group = не удалось понизить права доступа группы 38 | err_perm_user = не удалось понизить права доступа пользователя 39 | err_pwnam = не удалось получить информацию о пользователе 40 | 41 | 42 | err_user_gid = не удалось установить GID пользователя 43 | err_user_init = не удалось инициализировать пользователя 44 | err_user_uid = не удалось установить UID пользователя 45 | 46 | 47 | err_xsessions_dir = не удалось найти сессионную папку 48 | err_xsessions_open = не удалось открыть сессионную папку 49 | 50 | login = логин 51 | logout = logged out 52 | 53 | 54 | numlock = numlock 55 | 56 | password = пароль 57 | restart = перезагрузить 58 | shell = shell 59 | shutdown = выключить 60 | 61 | wayland = wayland 62 | 63 | xinitrc = xinitrc 64 | -------------------------------------------------------------------------------- /res/lang/sr.ini: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | capslock = capslock 5 | err_alloc = neuspijesna alokacija memorije 6 | err_bounds = izvan granica indeksa 7 | 8 | err_chdir = neuspijesno otvaranje home foldera 9 | 10 | err_console_dev = neuspijesno pristupanje konzoli 11 | err_dgn_oob = log poruka 12 | err_domain = nevazeci domen 13 | 14 | 15 | err_hostname = neuspijesno trazenje hostname-a 16 | err_mlock = neuspijesno zakljucavanje memorije lozinke 17 | err_null = null pokazivac 18 | 19 | err_pam = pam transakcija neuspijesna 20 | err_pam_abort = pam transakcija prekinuta 21 | err_pam_acct_expired = nalog istekao 22 | err_pam_auth = greska pri autentikaciji 23 | err_pam_authinfo_unavail = neuspjelo uzimanje informacija o korisniku 24 | err_pam_authok_reqd = token istekao 25 | err_pam_buf = greska bafera memorije 26 | err_pam_cred_err = neuspjelo postavljanje kredencijala 27 | err_pam_cred_expired = kredencijali istekli 28 | err_pam_cred_insufficient = nedovoljni kredencijali 29 | err_pam_cred_unavail = neuspjelo uzimanje kredencijala 30 | err_pam_maxtries = dostignut maksimalan broj pokusaja 31 | err_pam_perm_denied = nedozovoljeno 32 | err_pam_session = greska sesije 33 | err_pam_sys = greska sistema 34 | err_pam_user_unknown = nepoznat korisnik 35 | err_path = neuspjelo postavljanje path-a 36 | err_perm_dir = neuspjelo mijenjanje foldera 37 | err_perm_group = neuspjesno snizavanje dozvola grupe 38 | err_perm_user = neuspijesno snizavanje dozvola korisnika 39 | err_pwnam = neuspijesno skupljanje informacija o korisniku 40 | 41 | 42 | err_user_gid = neuspijesno postavljanje korisničkog GID-a 43 | err_user_init = neuspijensa inicijalizacija korisnika 44 | err_user_uid = neuspijesno postavljanje UID-a korisnika 45 | 46 | 47 | err_xsessions_dir = neuspijesno pronalazenje foldera sesija 48 | err_xsessions_open = neuspijesno otvaranje foldera sesija 49 | 50 | login = korisnik 51 | logout = izlogovan 52 | 53 | 54 | numlock = numlock 55 | 56 | password = lozinka 57 | restart = ponovo pokreni 58 | shell = shell 59 | shutdown = ugasi 60 | 61 | wayland = wayland 62 | 63 | xinitrc = xinitrc 64 | -------------------------------------------------------------------------------- /res/lang/sv.ini: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | capslock = capslock 5 | err_alloc = misslyckad minnesallokering 6 | err_bounds = utanför banan index 7 | 8 | err_chdir = misslyckades att öppna hemkatalog 9 | 10 | err_console_dev = misslyckades att komma åt konsol 11 | err_dgn_oob = loggmeddelande 12 | err_domain = okänd domän 13 | 14 | 15 | err_hostname = misslyckades att hämta värdnamn 16 | err_mlock = misslyckades att låsa lösenordsminne 17 | err_null = nullpekare 18 | 19 | err_pam = pam-transaktion misslyckades 20 | err_pam_abort = pam-transaktion avbröts 21 | err_pam_acct_expired = konto upphört 22 | err_pam_auth = autentiseringsfel 23 | err_pam_authinfo_unavail = misslyckades att hämta användarinfo 24 | err_pam_authok_reqd = token utgången 25 | err_pam_buf = minnesbuffer fel 26 | err_pam_cred_err = misslyckades att ställa in inloggningsuppgifter 27 | err_pam_cred_expired = inloggningsuppgifter upphörda 28 | err_pam_cred_insufficient = otillräckliga inloggningsuppgifter 29 | err_pam_cred_unavail = misslyckades att hämta inloggningsuppgifter 30 | err_pam_maxtries = nådde maximal försöksgräns 31 | err_pam_perm_denied = åtkomst nekad 32 | err_pam_session = sessionsfel 33 | err_pam_sys = systemfel 34 | err_pam_user_unknown = okänd användare 35 | err_path = misslyckades att ställa in sökväg 36 | err_perm_dir = misslyckades att ändra aktuell katalog 37 | err_perm_group = misslyckades att nergradera gruppbehörigheter 38 | err_perm_user = misslyckades att nergradera användarbehörigheter 39 | err_pwnam = misslyckades att hämta användarinfo 40 | 41 | 42 | err_user_gid = misslyckades att ställa in användar-GID 43 | err_user_init = misslyckades att initialisera användaren 44 | err_user_uid = misslyckades att ställa in användar-UID 45 | 46 | 47 | err_xsessions_dir = misslyckades att hitta sessionskatalog 48 | err_xsessions_open = misslyckades att öppna sessionskatalog 49 | 50 | login = inloggning 51 | logout = utloggad 52 | 53 | 54 | numlock = numlock 55 | 56 | password = lösenord 57 | restart = starta om 58 | shell = skal 59 | shutdown = stäng av 60 | 61 | wayland = wayland 62 | 63 | xinitrc = xinitrc 64 | -------------------------------------------------------------------------------- /res/lang/tr.ini: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | capslock = capslock 5 | err_alloc = basarisiz bellek ayirma 6 | err_bounds = sinirlarin disinda dizin 7 | 8 | err_chdir = ev klasoru acilamadi 9 | 10 | err_console_dev = konsola erisilemedi 11 | err_dgn_oob = log mesaji 12 | err_domain = gecersiz etki alani 13 | 14 | 15 | err_hostname = ana bilgisayar adi alinamadi 16 | err_mlock = parola bellegi kilitlenemedi 17 | err_null = bos isaretci hatasi 18 | 19 | err_pam = pam islemi basarisiz oldu 20 | err_pam_abort = pam islemi durduruldu 21 | err_pam_acct_expired = hesabin suresi dolmus 22 | err_pam_auth = kimlik dogrulama hatasi 23 | err_pam_authinfo_unavail = kullanici bilgileri getirilirken hata olustu 24 | err_pam_authok_reqd = suresi dolmus token 25 | err_pam_buf = bellek arabellegi hatasi 26 | err_pam_cred_err = kimlik bilgileri ayarlanamadi 27 | err_pam_cred_expired = kimlik bilgilerinin suresi dolmus 28 | err_pam_cred_insufficient = yetersiz kimlik bilgileri 29 | err_pam_cred_unavail = kimlik bilgileri alinamadi 30 | err_pam_maxtries = en fazla deneme sinirina ulasildi 31 | err_pam_perm_denied = izin reddedildi 32 | err_pam_session = oturum hatasi 33 | err_pam_sys = sistem hatasi 34 | err_pam_user_unknown = bilinmeyen kullanici 35 | err_path = yol ayarlanamadi 36 | err_perm_dir = gecerli dizin degistirilemedi 37 | err_perm_group = grup izinleri dusurulemedi 38 | err_perm_user = kullanici izinleri dusurulemedi 39 | err_pwnam = kullanici bilgileri alinamadi 40 | 41 | 42 | err_user_gid = kullanici icin GID ayarlanamadi 43 | err_user_init = kullanici oturumu baslatilamadi 44 | err_user_uid = kullanici icin UID ayarlanamadi 45 | 46 | 47 | err_xsessions_dir = oturumlar klasoru bulunamadi 48 | err_xsessions_open = oturumlar klasoru acilamadi 49 | 50 | login = kullanici 51 | logout = oturumdan cikis yapildi 52 | 53 | 54 | numlock = numlock 55 | 56 | password = sifre 57 | restart = yeniden baslat 58 | shell = shell 59 | shutdown = makineyi kapat 60 | 61 | wayland = wayland 62 | 63 | xinitrc = xinitrc 64 | -------------------------------------------------------------------------------- /res/lang/uk.ini: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | capslock = capslock 5 | err_alloc = невдале виділення пам'яті 6 | err_bounds = поза межами індексу 7 | 8 | err_chdir = не вдалося відкрити домашній каталог 9 | 10 | err_console_dev = невдалий доступ до консолі 11 | err_dgn_oob = повідомлення журналу (log) 12 | err_domain = недійсний домен 13 | 14 | 15 | err_hostname = не вдалося отримати ім'я хосту 16 | err_mlock = збій блокування пам'яті 17 | err_null = нульовий вказівник 18 | 19 | err_pam = невдала pam транзакція 20 | err_pam_abort = pam транзакція перервана 21 | err_pam_acct_expired = термін дії акаунту вичерпано 22 | err_pam_auth = помилка автентифікації 23 | err_pam_authinfo_unavail = не вдалося отримати дані користувача 24 | err_pam_authok_reqd = термін дії токена вичерпано 25 | err_pam_buf = помилка буферу пам'яті 26 | err_pam_cred_err = не вдалося змінити облікові дані 27 | err_pam_cred_expired = термін дії повноважень вичерпано 28 | err_pam_cred_insufficient = недостатньо облікових даних 29 | err_pam_cred_unavail = не вдалося отримати облікові дані 30 | err_pam_maxtries = вичерпано ліміт спроб 31 | err_pam_perm_denied = відмовлено у доступі 32 | err_pam_session = помилка сесії 33 | err_pam_sys = системна помилка 34 | err_pam_user_unknown = невідомий користувач 35 | err_path = не вдалося змінити шлях 36 | err_perm_dir = не вдалося змінити поточний каталог 37 | err_perm_group = не вдалося понизити права доступу групи 38 | err_perm_user = не вдалося понизити права доступу користувача 39 | err_pwnam = не вдалося отримати дані користувача 40 | 41 | 42 | err_user_gid = не вдалося змінити GID користувача 43 | err_user_init = не вдалося ініціалізувати користувача 44 | err_user_uid = не вдалося змінити UID користувача 45 | 46 | 47 | err_xsessions_dir = не вдалося знайти каталог сесій 48 | err_xsessions_open = не вдалося відкрити каталог сесій 49 | 50 | login = логін 51 | logout = вийти 52 | 53 | 54 | numlock = numlock 55 | 56 | password = пароль 57 | restart = перезавантажити 58 | shell = оболонка 59 | shutdown = вимкнути 60 | 61 | wayland = wayland 62 | 63 | xinitrc = xinitrc 64 | -------------------------------------------------------------------------------- /res/lang/zh_CN.ini: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | capslock = 大写锁定 5 | err_alloc = 内存分配失败 6 | err_bounds = 索引越界 7 | 8 | err_chdir = 无法打开home文件夹 9 | 10 | err_console_dev = 无法访问控制台 11 | err_dgn_oob = 日志消息 12 | err_domain = 无效的域 13 | 14 | 15 | err_hostname = 获取主机名失败 16 | err_mlock = 锁定密码存储器失败 17 | err_null = 空指针 18 | 19 | err_pam = PAM事件失败 20 | err_pam_abort = PAM事务已中止 21 | err_pam_acct_expired = 帐户已过期 22 | err_pam_auth = 身份验证错误 23 | err_pam_authinfo_unavail = 获取用户信息失败 24 | err_pam_authok_reqd = 口令已过期 25 | err_pam_buf = 内存缓冲区错误 26 | err_pam_cred_err = 设置凭据失败 27 | err_pam_cred_expired = 凭据已过期 28 | err_pam_cred_insufficient = 凭据不足 29 | err_pam_cred_unavail = 无法获取凭据 30 | err_pam_maxtries = 已达到最大尝试次数限制 31 | err_pam_perm_denied = 拒绝访问 32 | err_pam_session = 会话错误 33 | err_pam_sys = 系统错误 34 | err_pam_user_unknown = 未知用户 35 | err_path = 无法设置路径 36 | err_perm_dir = 更改当前目录失败 37 | err_perm_group = 组权限降级失败 38 | err_perm_user = 用户权限降级失败 39 | err_pwnam = 获取用户信息失败 40 | 41 | 42 | err_user_gid = 设置用户GID失败 43 | err_user_init = 初始化用户失败 44 | err_user_uid = 设置用户UID失败 45 | 46 | 47 | err_xsessions_dir = 找不到会话文件夹 48 | err_xsessions_open = 无法打开会话文件夹 49 | 50 | login = 登录 51 | logout = 注销 52 | 53 | 54 | numlock = 数字锁定 55 | 56 | password = 密码 57 | 58 | shell = shell 59 | 60 | 61 | wayland = wayland 62 | x11 = x11 63 | xinitrc = xinitrc 64 | -------------------------------------------------------------------------------- /res/ly-dinit: -------------------------------------------------------------------------------- 1 | type = process 2 | restart = true 3 | smooth-recovery = true 4 | command = $PREFIX_DIRECTORY/bin/$EXE_NAME 5 | depends-on = loginready 6 | termsignal = HUP 7 | # ly needs access to the console while loginready already occupies it 8 | options = shares-console 9 | -------------------------------------------------------------------------------- /res/ly-openrc: -------------------------------------------------------------------------------- 1 | #!/sbin/openrc-run 2 | 3 | name="ly" 4 | description="TUI Display Manager" 5 | 6 | ## Supervisor daemon 7 | supervisor=supervise-daemon 8 | respawn_period=60 9 | pidfile=/run/"${RC_SVCNAME}.pid" 10 | 11 | ## Check for getty or agetty 12 | if [ -x /sbin/getty ] || [ -x /bin/getty ]; 13 | then 14 | # busybox 15 | commandB="/sbin/getty" 16 | elif [ -x /sbin/agetty ] || [ -x /bin/agetty ]; 17 | then 18 | # util-linux 19 | commandUL="/sbin/agetty" 20 | fi 21 | 22 | ## Get the tty from the conf file 23 | CONFTTY=$(cat $CONFIG_DIRECTORY/ly/config.ini | sed -n 's/^tty.*=[^1-9]*// p') 24 | 25 | ## The execution vars 26 | # If CONFTTY is empty then default to $DEFAULT_TTY 27 | TTY="tty${CONFTTY:-$DEFAULT_TTY}" 28 | TERM=linux 29 | BAUD=38400 30 | # If we don't have getty then we should have agetty 31 | command=${commandB:-$commandUL} 32 | command_args_foreground="-nl $PREFIX_DIRECTORY/bin/$EXECUTABLE_NAME $TTY $BAUD $TERM" 33 | 34 | depend() { 35 | after agetty 36 | provide display-manager 37 | want elogind 38 | } 39 | -------------------------------------------------------------------------------- /res/ly-runit-service/conf: -------------------------------------------------------------------------------- 1 | if [ -x /sbin/agetty -o -x /bin/agetty ]; then 2 | # util-linux specific settings 3 | if [ "${tty}" = "tty1" ]; then 4 | GETTY_ARGS="--noclear" 5 | fi 6 | fi 7 | 8 | BAUD_RATE=38400 9 | TERM_NAME=linux 10 | 11 | auxtty=$(/bin/cat $CONFIG_DIRECTORY/ly/config.ini 2>/dev/null 1| /bin/sed -n 's/\(^[[:space:]]*tty[[:space:]]*=[[:space:]]*\)\([[:digit:]][[:digit:]]*\)\(.*\)/\2/p') 12 | TTY=tty${auxtty:-$DEFAULT_TTY} 13 | -------------------------------------------------------------------------------- /res/ly-runit-service/finish: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | [ -r conf ] && . ./conf 3 | 4 | exec utmpset -w ${TTY} 5 | -------------------------------------------------------------------------------- /res/ly-runit-service/run: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | [ -r conf ] && . ./conf 4 | 5 | if [ -x /sbin/getty -o -x /bin/getty ]; then 6 | # busybox 7 | GETTY=getty 8 | elif [ -x /sbin/agetty -o -x /bin/agetty ]; then 9 | # util-linux 10 | GETTY=agetty 11 | fi 12 | 13 | exec setsid ${GETTY} ${GETTY_ARGS} -nl $PREFIX_DIRECTORY/bin/$EXECUTABLE_NAME "${TTY}" "${BAUD_RATE}" "${TERM_NAME}" 14 | -------------------------------------------------------------------------------- /res/ly-s6/run: -------------------------------------------------------------------------------- 1 | #!/bin/execlineb -P 2 | exec agetty -L -8 -n -l $PREFIX_DIRECTORY/bin/$EXE_NAME tty$DEFAULT_TTY 115200 3 | -------------------------------------------------------------------------------- /res/ly-s6/type: -------------------------------------------------------------------------------- 1 | longrun 2 | -------------------------------------------------------------------------------- /res/ly.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=TUI display manager 3 | After=systemd-user-sessions.service plymouth-quit-wait.service 4 | After=getty@tty$DEFAULT_TTY.service 5 | Conflicts=getty@tty$DEFAULT_TTY.service 6 | 7 | [Service] 8 | Type=idle 9 | ExecStart=$PREFIX_DIRECTORY/bin/$EXECUTABLE_NAME 10 | StandardInput=tty 11 | TTYPath=/dev/tty$DEFAULT_TTY 12 | TTYReset=yes 13 | TTYVHangup=yes 14 | 15 | [Install] 16 | Alias=display-manager.service 17 | -------------------------------------------------------------------------------- /res/pam.d/ly: -------------------------------------------------------------------------------- 1 | #%PAM-1.0 2 | 3 | auth include login 4 | -auth optional pam_gnome_keyring.so 5 | -auth optional pam_kwallet5.so 6 | 7 | account include login 8 | 9 | password include login 10 | -password optional pam_gnome_keyring.so use_authtok 11 | 12 | -session optional pam_systemd.so class=greeter 13 | -session optional pam_elogind.so 14 | session include login 15 | -session optional pam_gnome_keyring.so auto_start 16 | -session optional pam_kwallet5.so auto_start 17 | -------------------------------------------------------------------------------- /res/setup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # Shell environment setup after login 3 | # Copyright (C) 2015-2016 Pier Luigi Fiorini 4 | 5 | # This file is extracted from kde-workspace (kdm/kfrontend/genkdmconf.c) 6 | # Copyright (C) 2001-2005 Oswald Buddenhagen 7 | 8 | # Copyright (C) 2024 The Fairy Glade 9 | # This work is free. You can redistribute it and/or modify it under the 10 | # terms of the Do What The Fuck You Want To Public License, Version 2, 11 | # as published by Sam Hocevar. See the LICENSE file for more details. 12 | 13 | # Note that the respective logout scripts are not sourced. 14 | case $SHELL in 15 | */bash) 16 | [ -z "$BASH" ] && exec $SHELL "$0" "$@" 17 | set +o posix 18 | [ -f "$CONFIG_DIRECTORY"/profile ] && . "$CONFIG_DIRECTORY"/profile 19 | if [ -f "$HOME"/.bash_profile ]; then 20 | . "$HOME"/.bash_profile 21 | elif [ -f "$HOME"/.bash_login ]; then 22 | . "$HOME"/.bash_login 23 | elif [ -f "$HOME"/.profile ]; then 24 | . "$HOME"/.profile 25 | fi 26 | ;; 27 | */zsh) 28 | [ -z "$ZSH_NAME" ] && exec $SHELL "$0" "$@" 29 | [ -d "$CONFIG_DIRECTORY"/zsh ] && zdir="$CONFIG_DIRECTORY"/zsh || zdir="$CONFIG_DIRECTORY" 30 | zhome=${ZDOTDIR:-"$HOME"} 31 | # zshenv is always sourced automatically. 32 | [ -f "$zdir"/zprofile ] && . "$zdir"/zprofile 33 | [ -f "$zhome"/.zprofile ] && . "$zhome"/.zprofile 34 | [ -f "$zdir"/zlogin ] && . "$zdir"/zlogin 35 | [ -f "$zhome"/.zlogin ] && . "$zhome"/.zlogin 36 | emulate -R sh 37 | ;; 38 | */csh|*/tcsh) 39 | # [t]cshrc is always sourced automatically. 40 | # Note that sourcing csh.login after .cshrc is non-standard. 41 | sess_tmp=$(mktemp /tmp/sess-env-XXXXXX) 42 | $SHELL -c "if (-f $CONFIG_DIRECTORY/csh.login) source $CONFIG_DIRECTORY/csh.login; if (-f ~/.login) source ~/.login; /bin/sh -c 'export -p' >! $sess_tmp" 43 | . "$sess_tmp" 44 | rm -f "$sess_tmp" 45 | ;; 46 | */fish) 47 | [ -f "$CONFIG_DIRECTORY"/profile ] && . "$CONFIG_DIRECTORY"/profile 48 | [ -f "$HOME"/.profile ] && . "$HOME"/.profile 49 | sess_tmp=$(mktemp /tmp/sess-env-XXXXXX) 50 | $SHELL --login -c "/bin/sh -c 'export -p' > $sess_tmp" 51 | . "$sess_tmp" 52 | rm -f "$sess_tmp" 53 | ;; 54 | *) # Plain sh, ksh, and anything we do not know. 55 | [ -f "$CONFIG_DIRECTORY"/profile ] && . "$CONFIG_DIRECTORY"/profile 56 | [ -f "$HOME"/.profile ] && . "$HOME"/.profile 57 | ;; 58 | esac 59 | 60 | if [ "$XDG_SESSION_TYPE" = "x11" ]; then 61 | [ -f "$CONFIG_DIRECTORY"/xprofile ] && . "$CONFIG_DIRECTORY"/xprofile 62 | [ -f "$HOME"/.xprofile ] && . "$HOME"/.xprofile 63 | 64 | # run all system xinitrc shell scripts. 65 | if [ -d "$CONFIG_DIRECTORY"/X11/xinit/xinitrc.d ]; then 66 | for i in "$CONFIG_DIRECTORY"/X11/xinit/xinitrc.d/* ; do 67 | if [ -x "$i" ]; then 68 | . "$i" 69 | fi 70 | done 71 | fi 72 | 73 | # Load Xsession scripts 74 | # OPTIONFILE, USERXSESSION, USERXSESSIONRC and ALTUSERXSESSION are required 75 | # by the scripts to work 76 | xsessionddir="$CONFIG_DIRECTORY"/X11/Xsession.d 77 | export OPTIONFILE="$CONFIG_DIRECTORY"/X11/Xsession.options 78 | export USERXSESSION="$HOME"/.xsession 79 | export USERXSESSIONRC="$HOME"/.xsessionrc 80 | export ALTUSERXSESSION="$HOME"/.Xsession 81 | 82 | if [ -d "$xsessionddir" ]; then 83 | for i in $(ls "$xsessionddir"); do 84 | script="$xsessionddir/$i" 85 | echo "Loading X session script $script" 86 | if [ -r "$script" ] && [ -f "$script" ] && expr "$i" : '^[[:alnum:]_-]\+$' > /dev/null; then 87 | . "$script" 88 | fi 89 | done 90 | fi 91 | 92 | if [ -f "$USERXSESSION" ]; then 93 | . "$USERXSESSION" 94 | fi 95 | 96 | if [ -d "$CONFIG_DIRECTORY"/X11/Xresources ]; then 97 | for i in "$CONFIG_DIRECTORY"/X11/Xresources/*; do 98 | [ -f "$i" ] && xrdb -merge "$i" 99 | done 100 | elif [ -f "$CONFIG_DIRECTORY"/X11/Xresources ]; then 101 | xrdb -merge "$CONFIG_DIRECTORY"/X11/Xresources 102 | fi 103 | [ -f "$HOME"/.Xresources ] && xrdb -merge "$HOME"/.Xresources 104 | [ -f "$XDG_CONFIG_HOME"/X11/Xresources ] && xrdb -merge "$XDG_CONFIG_HOME"/X11/Xresources 105 | fi 106 | 107 | exec "$@" 108 | -------------------------------------------------------------------------------- /src/Environment.zig: -------------------------------------------------------------------------------- 1 | const enums = @import("enums.zig"); 2 | const ini = @import("zigini"); 3 | 4 | const DisplayServer = enums.DisplayServer; 5 | const Ini = ini.Ini; 6 | 7 | pub const DesktopEntry = struct { 8 | Exec: []const u8 = "", 9 | Name: [:0]const u8 = "", 10 | DesktopNames: ?[:0]u8 = null, 11 | }; 12 | 13 | pub const Entry = struct { @"Desktop Entry": DesktopEntry = .{} }; 14 | 15 | entry_ini: ?Ini(Entry) = null, 16 | name: [:0]const u8 = "", 17 | xdg_session_desktop: ?[:0]const u8 = null, 18 | xdg_desktop_names: ?[:0]const u8 = null, 19 | cmd: []const u8 = "", 20 | specifier: []const u8 = "", 21 | display_server: DisplayServer = .wayland, 22 | -------------------------------------------------------------------------------- /src/SharedError.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | const ErrInt = std.meta.Int(.unsigned, @bitSizeOf(anyerror)); 4 | 5 | const ErrorHandler = packed struct { 6 | has_error: bool = false, 7 | err_int: ErrInt = 0, 8 | }; 9 | 10 | const SharedError = @This(); 11 | 12 | data: []align(std.heap.page_size_min) u8, 13 | 14 | pub fn init() !SharedError { 15 | const data = try std.posix.mmap(null, @sizeOf(ErrorHandler), std.posix.PROT.READ | std.posix.PROT.WRITE, .{ .TYPE = .SHARED, .ANONYMOUS = true }, -1, 0); 16 | 17 | return .{ .data = data }; 18 | } 19 | 20 | pub fn deinit(self: *SharedError) void { 21 | std.posix.munmap(self.data); 22 | } 23 | 24 | pub fn writeError(self: SharedError, err: anyerror) void { 25 | var buf_stream = std.io.fixedBufferStream(self.data); 26 | const writer = buf_stream.writer(); 27 | writer.writeStruct(ErrorHandler{ .has_error = true, .err_int = @intFromError(err) }) catch {}; 28 | } 29 | 30 | pub fn readError(self: SharedError) ?anyerror { 31 | var buf_stream = std.io.fixedBufferStream(self.data); 32 | const reader = buf_stream.reader(); 33 | const err_handler = try reader.readStruct(ErrorHandler); 34 | 35 | if (err_handler.has_error) 36 | return @errorFromInt(err_handler.err_int); 37 | 38 | return null; 39 | } 40 | -------------------------------------------------------------------------------- /src/animations/ColorMix.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const Animation = @import("../tui/Animation.zig"); 3 | const Cell = @import("../tui/Cell.zig"); 4 | const TerminalBuffer = @import("../tui/TerminalBuffer.zig"); 5 | 6 | const ColorMix = @This(); 7 | 8 | const math = std.math; 9 | const Vec2 = @Vector(2, f32); 10 | 11 | const time_scale: f32 = 0.01; 12 | const palette_len: usize = 12; 13 | 14 | fn length(vec: Vec2) f32 { 15 | return math.sqrt(vec[0] * vec[0] + vec[1] * vec[1]); 16 | } 17 | 18 | terminal_buffer: *TerminalBuffer, 19 | frames: u64, 20 | pattern_cos_mod: f32, 21 | pattern_sin_mod: f32, 22 | palette: [palette_len]Cell, 23 | 24 | pub fn init(terminal_buffer: *TerminalBuffer, col1: u32, col2: u32, col3: u32) ColorMix { 25 | return .{ 26 | .terminal_buffer = terminal_buffer, 27 | .frames = 0, 28 | .pattern_cos_mod = terminal_buffer.random.float(f32) * math.pi * 2.0, 29 | .pattern_sin_mod = terminal_buffer.random.float(f32) * math.pi * 2.0, 30 | .palette = [palette_len]Cell{ 31 | Cell.init(0x2588, col1, col2), 32 | Cell.init(0x2593, col1, col2), 33 | Cell.init(0x2592, col1, col2), 34 | Cell.init(0x2591, col1, col2), 35 | Cell.init(0x2588, col2, col3), 36 | Cell.init(0x2593, col2, col3), 37 | Cell.init(0x2592, col2, col3), 38 | Cell.init(0x2591, col2, col3), 39 | Cell.init(0x2588, col3, col1), 40 | Cell.init(0x2593, col3, col1), 41 | Cell.init(0x2592, col3, col1), 42 | Cell.init(0x2591, col3, col1), 43 | }, 44 | }; 45 | } 46 | 47 | pub fn animation(self: *ColorMix) Animation { 48 | return Animation.init(self, deinit, realloc, draw); 49 | } 50 | 51 | fn deinit(_: *ColorMix) void {} 52 | 53 | fn realloc(_: *ColorMix) anyerror!void {} 54 | 55 | fn draw(self: *ColorMix) void { 56 | self.frames +%= 1; 57 | const time: f32 = @as(f32, @floatFromInt(self.frames)) * time_scale; 58 | 59 | for (0..self.terminal_buffer.width) |x| { 60 | for (0..self.terminal_buffer.height) |y| { 61 | const xi: i32 = @intCast(x); 62 | const yi: i32 = @intCast(y); 63 | const wi: i32 = @intCast(self.terminal_buffer.width); 64 | const hi: i32 = @intCast(self.terminal_buffer.height); 65 | 66 | var uv: Vec2 = .{ 67 | @as(f32, @floatFromInt(xi * 2 - wi)) / @as(f32, @floatFromInt(self.terminal_buffer.height * 2)), 68 | @as(f32, @floatFromInt(yi * 2 - hi)) / @as(f32, @floatFromInt(self.terminal_buffer.height)), 69 | }; 70 | 71 | var uv2: Vec2 = @splat(uv[0] + uv[1]); 72 | 73 | for (0..3) |_| { 74 | uv2 += uv + @as(Vec2, @splat(length(uv))); 75 | uv += @as(Vec2, @splat(0.5)) * Vec2{ 76 | math.cos(self.pattern_cos_mod + uv2[1] * 0.2 + time * 0.1), 77 | math.sin(self.pattern_sin_mod + uv2[0] - time * 0.1), 78 | }; 79 | uv -= @splat(1.0 * math.cos(uv[0] + uv[1]) - math.sin(uv[0] * 0.7 - uv[1])); 80 | } 81 | 82 | const cell = self.palette[@as(usize, @intFromFloat(math.floor(length(uv) * 5.0))) % palette_len]; 83 | cell.put(x, y); 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/animations/Doom.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const Allocator = std.mem.Allocator; 3 | const Animation = @import("../tui/Animation.zig"); 4 | const Cell = @import("../tui/Cell.zig"); 5 | const TerminalBuffer = @import("../tui/TerminalBuffer.zig"); 6 | 7 | const Doom = @This(); 8 | 9 | pub const STEPS = 12; 10 | 11 | allocator: Allocator, 12 | terminal_buffer: *TerminalBuffer, 13 | buffer: []u8, 14 | fire: [STEPS + 1]Cell, 15 | 16 | pub fn init(allocator: Allocator, terminal_buffer: *TerminalBuffer, top_color: u32, middle_color: u32, bottom_color: u32) !Doom { 17 | const buffer = try allocator.alloc(u8, terminal_buffer.width * terminal_buffer.height); 18 | initBuffer(buffer, terminal_buffer.width); 19 | 20 | return .{ 21 | .allocator = allocator, 22 | .terminal_buffer = terminal_buffer, 23 | .buffer = buffer, 24 | .fire = [_]Cell{ 25 | Cell.init(' ', TerminalBuffer.Color.DEFAULT, TerminalBuffer.Color.DEFAULT), 26 | Cell.init(0x2591, top_color, TerminalBuffer.Color.DEFAULT), 27 | Cell.init(0x2592, top_color, TerminalBuffer.Color.DEFAULT), 28 | Cell.init(0x2593, top_color, TerminalBuffer.Color.DEFAULT), 29 | Cell.init(0x2588, top_color, TerminalBuffer.Color.DEFAULT), 30 | Cell.init(0x2591, middle_color, top_color), 31 | Cell.init(0x2592, middle_color, top_color), 32 | Cell.init(0x2593, middle_color, top_color), 33 | Cell.init(0x2588, middle_color, top_color), 34 | Cell.init(0x2591, bottom_color, middle_color), 35 | Cell.init(0x2592, bottom_color, middle_color), 36 | Cell.init(0x2593, bottom_color, middle_color), 37 | Cell.init(0x2588, bottom_color, middle_color), 38 | }, 39 | }; 40 | } 41 | 42 | pub fn animation(self: *Doom) Animation { 43 | return Animation.init(self, deinit, realloc, draw); 44 | } 45 | 46 | fn deinit(self: *Doom) void { 47 | self.allocator.free(self.buffer); 48 | } 49 | 50 | fn realloc(self: *Doom) anyerror!void { 51 | const buffer = try self.allocator.realloc(self.buffer, self.terminal_buffer.width * self.terminal_buffer.height); 52 | initBuffer(buffer, self.terminal_buffer.width); 53 | self.buffer = buffer; 54 | } 55 | 56 | fn draw(self: *Doom) void { 57 | for (0..self.terminal_buffer.width) |x| { 58 | // We start from 1 so that we always have the topmost line when spreading fire 59 | for (1..self.terminal_buffer.height) |y| { 60 | // Get current cell 61 | const from = y * self.terminal_buffer.width + x; 62 | const cell_index = self.buffer[from]; 63 | 64 | // Spread fire 65 | const propagate = self.terminal_buffer.random.int(u1); 66 | const to = from - self.terminal_buffer.width; // Get the line above 67 | 68 | self.buffer[to] = if (cell_index > 0) cell_index - propagate else cell_index; 69 | 70 | // Put the cell 71 | const cell = self.fire[cell_index]; 72 | cell.put(x, y); 73 | } 74 | } 75 | } 76 | 77 | fn initBuffer(buffer: []u8, width: usize) void { 78 | const length = buffer.len - width; 79 | const slice_start = buffer[0..length]; 80 | const slice_end = buffer[length..]; 81 | 82 | // Initialize the framebuffer in black, except for the "fire source" as the 83 | // last color 84 | @memset(slice_start, 0); 85 | @memset(slice_end, STEPS); 86 | } 87 | -------------------------------------------------------------------------------- /src/animations/Dummy.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const Animation = @import("../tui/Animation.zig"); 3 | 4 | const Dummy = @This(); 5 | 6 | pub fn animation(self: *Dummy) Animation { 7 | return Animation.init(self, deinit, realloc, draw); 8 | } 9 | 10 | fn deinit(_: *Dummy) void {} 11 | 12 | fn realloc(_: *Dummy) anyerror!void {} 13 | 14 | fn draw(_: *Dummy) void {} 15 | -------------------------------------------------------------------------------- /src/animations/Matrix.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const Animation = @import("../tui/Animation.zig"); 3 | const Cell = @import("../tui/Cell.zig"); 4 | const TerminalBuffer = @import("../tui/TerminalBuffer.zig"); 5 | 6 | const Allocator = std.mem.Allocator; 7 | const Random = std.Random; 8 | 9 | pub const FRAME_DELAY: usize = 8; 10 | 11 | // Characters change mid-scroll 12 | pub const MID_SCROLL_CHANGE = true; 13 | 14 | const DOT_HEAD_COLOR: u32 = @intCast(TerminalBuffer.Color.WHITE | TerminalBuffer.Styling.BOLD); 15 | 16 | const Matrix = @This(); 17 | 18 | pub const Dot = struct { 19 | value: ?usize, 20 | is_head: bool, 21 | }; 22 | 23 | pub const Line = struct { 24 | space: usize, 25 | length: usize, 26 | update: usize, 27 | }; 28 | 29 | allocator: Allocator, 30 | terminal_buffer: *TerminalBuffer, 31 | dots: []Dot, 32 | lines: []Line, 33 | frame: usize, 34 | count: usize, 35 | fg: u32, 36 | min_codepoint: u16, 37 | max_codepoint: u16, 38 | default_cell: Cell, 39 | 40 | pub fn init(allocator: Allocator, terminal_buffer: *TerminalBuffer, fg: u32, min_codepoint: u16, max_codepoint: u16) !Matrix { 41 | const dots = try allocator.alloc(Dot, terminal_buffer.width * (terminal_buffer.height + 1)); 42 | const lines = try allocator.alloc(Line, terminal_buffer.width); 43 | 44 | initBuffers(dots, lines, terminal_buffer.width, terminal_buffer.height, terminal_buffer.random); 45 | 46 | return .{ 47 | .allocator = allocator, 48 | .terminal_buffer = terminal_buffer, 49 | .dots = dots, 50 | .lines = lines, 51 | .frame = 3, 52 | .count = 0, 53 | .fg = fg, 54 | .min_codepoint = min_codepoint, 55 | .max_codepoint = max_codepoint - min_codepoint, 56 | .default_cell = .{ .ch = ' ', .fg = fg, .bg = terminal_buffer.bg }, 57 | }; 58 | } 59 | 60 | pub fn animation(self: *Matrix) Animation { 61 | return Animation.init(self, deinit, realloc, draw); 62 | } 63 | 64 | fn deinit(self: *Matrix) void { 65 | self.allocator.free(self.dots); 66 | self.allocator.free(self.lines); 67 | } 68 | 69 | fn realloc(self: *Matrix) anyerror!void { 70 | const dots = try self.allocator.realloc(self.dots, self.terminal_buffer.width * (self.terminal_buffer.height + 1)); 71 | const lines = try self.allocator.realloc(self.lines, self.terminal_buffer.width); 72 | 73 | initBuffers(dots, lines, self.terminal_buffer.width, self.terminal_buffer.height, self.terminal_buffer.random); 74 | 75 | self.dots = dots; 76 | self.lines = lines; 77 | } 78 | 79 | fn draw(self: *Matrix) void { 80 | const buf_height = self.terminal_buffer.height; 81 | const buf_width = self.terminal_buffer.width; 82 | self.count += 1; 83 | if (self.count > FRAME_DELAY) { 84 | self.frame += 1; 85 | if (self.frame > 4) self.frame = 1; 86 | self.count = 0; 87 | 88 | var x: usize = 0; 89 | while (x < self.terminal_buffer.width) : (x += 2) { 90 | var tail: usize = 0; 91 | var line = &self.lines[x]; 92 | if (self.frame <= line.update) continue; 93 | 94 | if (self.dots[x].value == null and self.dots[self.terminal_buffer.width + x].value == ' ') { 95 | if (line.space > 0) { 96 | line.space -= 1; 97 | } else { 98 | const randint = self.terminal_buffer.random.int(u16); 99 | const h = self.terminal_buffer.height; 100 | line.length = @mod(randint, h - 3) + 3; 101 | self.dots[x].value = @mod(randint, self.max_codepoint) + self.min_codepoint; 102 | line.space = @mod(randint, h + 1); 103 | } 104 | } 105 | 106 | var y: usize = 0; 107 | var first_col = true; 108 | var seg_len: u64 = 0; 109 | height_it: while (y <= buf_height) : (y += 1) { 110 | var dot = &self.dots[buf_width * y + x]; 111 | // Skip over spaces 112 | while (y <= buf_height and (dot.value == ' ' or dot.value == null)) { 113 | y += 1; 114 | if (y > buf_height) break :height_it; 115 | dot = &self.dots[buf_width * y + x]; 116 | } 117 | 118 | // Find the head of this column 119 | tail = y; 120 | seg_len = 0; 121 | while (y <= buf_height and dot.value != ' ' and dot.value != null) { 122 | dot.is_head = false; 123 | if (MID_SCROLL_CHANGE) { 124 | const randint = self.terminal_buffer.random.int(u16); 125 | if (@mod(randint, 8) == 0) { 126 | dot.value = @mod(randint, self.max_codepoint) + self.min_codepoint; 127 | } 128 | } 129 | 130 | y += 1; 131 | seg_len += 1; 132 | // Head's down offscreen 133 | if (y > buf_height) { 134 | self.dots[buf_width * tail + x].value = ' '; 135 | break :height_it; 136 | } 137 | dot = &self.dots[buf_width * y + x]; 138 | } 139 | 140 | const randint = self.terminal_buffer.random.int(u16); 141 | dot.value = @mod(randint, self.max_codepoint) + self.min_codepoint; 142 | dot.is_head = true; 143 | 144 | if (seg_len > line.length or !first_col) { 145 | self.dots[buf_width * tail + x].value = ' '; 146 | self.dots[x].value = null; 147 | } 148 | first_col = false; 149 | } 150 | } 151 | } 152 | 153 | var x: usize = 0; 154 | while (x < buf_width) : (x += 2) { 155 | var y: usize = 1; 156 | while (y <= self.terminal_buffer.height) : (y += 1) { 157 | const dot = self.dots[buf_width * y + x]; 158 | const cell = if (dot.value == null or dot.value == ' ') self.default_cell else Cell{ 159 | .ch = @intCast(dot.value.?), 160 | .fg = if (dot.is_head) DOT_HEAD_COLOR else self.fg, 161 | .bg = self.terminal_buffer.bg, 162 | }; 163 | 164 | cell.put(x, y - 1); 165 | } 166 | } 167 | } 168 | 169 | fn initBuffers(dots: []Dot, lines: []Line, width: usize, height: usize, random: Random) void { 170 | var y: usize = 0; 171 | while (y <= height) : (y += 1) { 172 | var x: usize = 0; 173 | while (x < width) : (x += 2) { 174 | dots[y * width + x].value = null; 175 | } 176 | } 177 | 178 | var x: usize = 0; 179 | while (x < width) : (x += 2) { 180 | var line = lines[x]; 181 | line.space = @mod(random.int(u16), height) + 1; 182 | line.length = @mod(random.int(u16), height - 3) + 3; 183 | line.update = @mod(random.int(u16), 3) + 1; 184 | lines[x] = line; 185 | 186 | dots[width + x].value = ' '; 187 | } 188 | } 189 | -------------------------------------------------------------------------------- /src/auth.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const build_options = @import("build_options"); 3 | const builtin = @import("builtin"); 4 | const enums = @import("enums.zig"); 5 | const Environment = @import("Environment.zig"); 6 | const interop = @import("interop.zig"); 7 | const SharedError = @import("SharedError.zig"); 8 | 9 | const Allocator = std.mem.Allocator; 10 | const Md5 = std.crypto.hash.Md5; 11 | const utmp = interop.utmp; 12 | const Utmp = utmp.utmpx; 13 | 14 | pub const AuthOptions = struct { 15 | tty: u8, 16 | service_name: [:0]const u8, 17 | path: ?[:0]const u8, 18 | session_log: []const u8, 19 | xauth_cmd: []const u8, 20 | setup_cmd: []const u8, 21 | login_cmd: ?[]const u8, 22 | x_cmd: []const u8, 23 | session_pid: std.posix.pid_t, 24 | }; 25 | 26 | var xorg_pid: std.posix.pid_t = 0; 27 | pub fn xorgSignalHandler(i: c_int) callconv(.C) void { 28 | if (xorg_pid > 0) _ = std.c.kill(xorg_pid, i); 29 | } 30 | 31 | var child_pid: std.posix.pid_t = 0; 32 | pub fn sessionSignalHandler(i: c_int) callconv(.C) void { 33 | if (child_pid > 0) _ = std.c.kill(child_pid, i); 34 | } 35 | 36 | pub fn authenticate(options: AuthOptions, current_environment: Environment, login: [:0]const u8, password: [:0]const u8) !void { 37 | var tty_buffer: [3]u8 = undefined; 38 | const tty_str = try std.fmt.bufPrintZ(&tty_buffer, "{d}", .{options.tty}); 39 | 40 | var pam_tty_buffer: [6]u8 = undefined; 41 | const pam_tty_str = try std.fmt.bufPrintZ(&pam_tty_buffer, "tty{d}", .{options.tty}); 42 | 43 | // Set the XDG environment variables 44 | setXdgSessionEnv(current_environment.display_server); 45 | try setXdgEnv(tty_str, current_environment.xdg_session_desktop, current_environment.xdg_desktop_names); 46 | 47 | // Open the PAM session 48 | var credentials = [_:null]?[*:0]const u8{ login, password }; 49 | 50 | const conv = interop.pam.pam_conv{ 51 | .conv = loginConv, 52 | .appdata_ptr = @ptrCast(&credentials), 53 | }; 54 | var handle: ?*interop.pam.pam_handle = undefined; 55 | 56 | var status = interop.pam.pam_start(options.service_name, null, &conv, &handle); 57 | if (status != interop.pam.PAM_SUCCESS) return pamDiagnose(status); 58 | defer _ = interop.pam.pam_end(handle, status); 59 | 60 | // Set PAM_TTY as the current TTY. This is required in case it isn't being set by another PAM module 61 | status = interop.pam.pam_set_item(handle, interop.pam.PAM_TTY, pam_tty_str.ptr); 62 | if (status != interop.pam.PAM_SUCCESS) return pamDiagnose(status); 63 | 64 | // Do the PAM routine 65 | status = interop.pam.pam_authenticate(handle, 0); 66 | if (status != interop.pam.PAM_SUCCESS) return pamDiagnose(status); 67 | 68 | status = interop.pam.pam_acct_mgmt(handle, 0); 69 | if (status != interop.pam.PAM_SUCCESS) return pamDiagnose(status); 70 | 71 | status = interop.pam.pam_setcred(handle, interop.pam.PAM_ESTABLISH_CRED); 72 | if (status != interop.pam.PAM_SUCCESS) return pamDiagnose(status); 73 | defer status = interop.pam.pam_setcred(handle, interop.pam.PAM_DELETE_CRED); 74 | 75 | status = interop.pam.pam_open_session(handle, 0); 76 | if (status != interop.pam.PAM_SUCCESS) return pamDiagnose(status); 77 | defer status = interop.pam.pam_close_session(handle, 0); 78 | 79 | var pwd: *interop.pwd.passwd = undefined; 80 | { 81 | defer interop.pwd.endpwent(); 82 | 83 | // Get password structure from username 84 | pwd = interop.pwd.getpwnam(login) orelse return error.GetPasswordNameFailed; 85 | } 86 | 87 | // Set user shell if it hasn't already been set 88 | if (pwd.pw_shell == null) { 89 | interop.unistd.setusershell(); 90 | pwd.pw_shell = interop.unistd.getusershell(); 91 | interop.unistd.endusershell(); 92 | } 93 | 94 | var shared_err = try SharedError.init(); 95 | defer shared_err.deinit(); 96 | 97 | child_pid = try std.posix.fork(); 98 | if (child_pid == 0) { 99 | startSession(options, pwd, handle, current_environment) catch |e| { 100 | shared_err.writeError(e); 101 | std.process.exit(1); 102 | }; 103 | std.process.exit(0); 104 | } 105 | 106 | var entry = std.mem.zeroes(Utmp); 107 | 108 | { 109 | // If an error occurs here, we can send SIGTERM to the session 110 | errdefer cleanup: { 111 | _ = std.posix.kill(child_pid, std.posix.SIG.TERM) catch break :cleanup; 112 | _ = std.posix.waitpid(child_pid, 0); 113 | } 114 | 115 | // If we receive SIGTERM, forward it to child_pid 116 | const act = std.posix.Sigaction{ 117 | .handler = .{ .handler = &sessionSignalHandler }, 118 | .mask = std.posix.empty_sigset, 119 | .flags = 0, 120 | }; 121 | std.posix.sigaction(std.posix.SIG.TERM, &act, null); 122 | 123 | try addUtmpEntry(&entry, pwd.pw_name.?, child_pid); 124 | } 125 | // Wait for the session to stop 126 | _ = std.posix.waitpid(child_pid, 0); 127 | 128 | removeUtmpEntry(&entry); 129 | 130 | if (shared_err.readError()) |err| return err; 131 | } 132 | 133 | fn startSession( 134 | options: AuthOptions, 135 | pwd: *interop.pwd.passwd, 136 | handle: ?*interop.pam.pam_handle, 137 | current_environment: Environment, 138 | ) !void { 139 | if (builtin.os.tag == .freebsd) { 140 | // FreeBSD has initgroups() in unistd 141 | const status = interop.unistd.initgroups(pwd.pw_name, pwd.pw_gid); 142 | if (status != 0) return error.GroupInitializationFailed; 143 | 144 | // FreeBSD sets the GID and UID with setusercontext() 145 | const result = interop.pwd.setusercontext(null, pwd, pwd.pw_uid, interop.pwd.LOGIN_SETALL); 146 | if (result != 0) return error.SetUserUidFailed; 147 | } else { 148 | const status = interop.grp.initgroups(pwd.pw_name, pwd.pw_gid); 149 | if (status != 0) return error.GroupInitializationFailed; 150 | 151 | std.posix.setgid(pwd.pw_gid) catch return error.SetUserGidFailed; 152 | std.posix.setuid(pwd.pw_uid) catch return error.SetUserUidFailed; 153 | } 154 | 155 | // Set up the environment 156 | try initEnv(pwd, options.path); 157 | 158 | // Set the PAM variables 159 | const pam_env_vars: ?[*:null]?[*:0]u8 = interop.pam.pam_getenvlist(handle); 160 | if (pam_env_vars == null) return error.GetEnvListFailed; 161 | 162 | const env_list = std.mem.span(pam_env_vars.?); 163 | for (env_list) |env_var| _ = interop.stdlib.putenv(env_var); 164 | 165 | // Change to the user's home directory 166 | std.posix.chdirZ(pwd.pw_dir.?) catch return error.ChangeDirectoryFailed; 167 | 168 | // Signal to the session process to give up control on the TTY 169 | _ = std.posix.kill(options.session_pid, std.posix.SIG.CHLD) catch return error.TtyControlTransferFailed; 170 | 171 | // Execute what the user requested 172 | switch (current_environment.display_server) { 173 | .wayland => try executeWaylandCmd(pwd.pw_shell.?, options, current_environment.cmd), 174 | .shell => try executeShellCmd(pwd.pw_shell.?, options), 175 | .xinitrc, .x11 => if (build_options.enable_x11_support) { 176 | var vt_buf: [5]u8 = undefined; 177 | const vt = try std.fmt.bufPrint(&vt_buf, "vt{d}", .{options.tty}); 178 | try executeX11Cmd(pwd.pw_shell.?, pwd.pw_dir.?, options, current_environment.cmd, vt); 179 | }, 180 | } 181 | } 182 | 183 | fn initEnv(pwd: *interop.pwd.passwd, path_env: ?[:0]const u8) !void { 184 | _ = interop.stdlib.setenv("HOME", pwd.pw_dir, 1); 185 | _ = interop.stdlib.setenv("PWD", pwd.pw_dir, 1); 186 | _ = interop.stdlib.setenv("SHELL", pwd.pw_shell, 1); 187 | _ = interop.stdlib.setenv("USER", pwd.pw_name, 1); 188 | _ = interop.stdlib.setenv("LOGNAME", pwd.pw_name, 1); 189 | 190 | if (path_env) |path| { 191 | const status = interop.stdlib.setenv("PATH", path, 1); 192 | if (status != 0) return error.SetPathFailed; 193 | } 194 | } 195 | 196 | fn setXdgSessionEnv(display_server: enums.DisplayServer) void { 197 | _ = interop.stdlib.setenv("XDG_SESSION_TYPE", switch (display_server) { 198 | .wayland => "wayland", 199 | .shell => "tty", 200 | .xinitrc, .x11 => "x11", 201 | }, 0); 202 | } 203 | 204 | fn setXdgEnv(tty_str: [:0]u8, maybe_desktop_name: ?[:0]const u8, maybe_xdg_desktop_names: ?[:0]const u8) !void { 205 | // The "/run/user/%d" directory is not available on FreeBSD. It is much 206 | // better to stick to the defaults and let applications using 207 | // XDG_RUNTIME_DIR to fall back to directories inside user's home 208 | // directory. 209 | if (builtin.os.tag != .freebsd) { 210 | const uid = interop.unistd.getuid(); 211 | var uid_buffer: [10 + @sizeOf(u32) + 1]u8 = undefined; 212 | const uid_str = try std.fmt.bufPrintZ(&uid_buffer, "/run/user/{d}", .{uid}); 213 | 214 | _ = interop.stdlib.setenv("XDG_RUNTIME_DIR", uid_str, 0); 215 | } 216 | 217 | if (maybe_xdg_desktop_names) |xdg_desktop_names| _ = interop.stdlib.setenv("XDG_CURRENT_DESKTOP", xdg_desktop_names, 0); 218 | _ = interop.stdlib.setenv("XDG_SESSION_CLASS", "user", 0); 219 | _ = interop.stdlib.setenv("XDG_SESSION_ID", "1", 0); 220 | if (maybe_desktop_name) |desktop_name| _ = interop.stdlib.setenv("XDG_SESSION_DESKTOP", desktop_name, 0); 221 | _ = interop.stdlib.setenv("XDG_SEAT", "seat0", 0); 222 | _ = interop.stdlib.setenv("XDG_VTNR", tty_str, 0); 223 | } 224 | 225 | fn loginConv( 226 | num_msg: c_int, 227 | msg: ?[*]?*const interop.pam.pam_message, 228 | resp: ?*?[*]interop.pam.pam_response, 229 | appdata_ptr: ?*anyopaque, 230 | ) callconv(.C) c_int { 231 | const message_count: u32 = @intCast(num_msg); 232 | const messages = msg.?; 233 | 234 | const allocator = std.heap.c_allocator; 235 | const response = allocator.alloc(interop.pam.pam_response, message_count) catch return interop.pam.PAM_BUF_ERR; 236 | 237 | // Initialise allocated memory to 0 238 | // This ensures memory can be freed by pam on success 239 | @memset(response, std.mem.zeroes(interop.pam.pam_response)); 240 | 241 | var username: ?[:0]u8 = null; 242 | var password: ?[:0]u8 = null; 243 | var status: c_int = interop.pam.PAM_SUCCESS; 244 | 245 | for (0..message_count) |i| set_credentials: { 246 | switch (messages[i].?.msg_style) { 247 | interop.pam.PAM_PROMPT_ECHO_ON => { 248 | const data: [*][*:0]u8 = @ptrCast(@alignCast(appdata_ptr)); 249 | username = allocator.dupeZ(u8, std.mem.span(data[0])) catch { 250 | status = interop.pam.PAM_BUF_ERR; 251 | break :set_credentials; 252 | }; 253 | response[i].resp = username.?; 254 | }, 255 | interop.pam.PAM_PROMPT_ECHO_OFF => { 256 | const data: [*][*:0]u8 = @ptrCast(@alignCast(appdata_ptr)); 257 | password = allocator.dupeZ(u8, std.mem.span(data[1])) catch { 258 | status = interop.pam.PAM_BUF_ERR; 259 | break :set_credentials; 260 | }; 261 | response[i].resp = password.?; 262 | }, 263 | interop.pam.PAM_ERROR_MSG => { 264 | status = interop.pam.PAM_CONV_ERR; 265 | break :set_credentials; 266 | }, 267 | else => {}, 268 | } 269 | } 270 | 271 | if (status != interop.pam.PAM_SUCCESS) { 272 | // Memory is freed by pam otherwise 273 | allocator.free(response); 274 | if (username != null) allocator.free(username.?); 275 | if (password != null) allocator.free(password.?); 276 | } else { 277 | resp.?.* = response.ptr; 278 | } 279 | 280 | return status; 281 | } 282 | 283 | fn getFreeDisplay() !u8 { 284 | var buf: [15]u8 = undefined; 285 | var i: u8 = 0; 286 | while (i < 200) : (i += 1) { 287 | const xlock = try std.fmt.bufPrint(&buf, "/tmp/.X{d}-lock", .{i}); 288 | std.posix.access(xlock, std.posix.F_OK) catch break; 289 | } 290 | return i; 291 | } 292 | 293 | fn getXPid(display_num: u8) !i32 { 294 | var buf: [15]u8 = undefined; 295 | const file_name = try std.fmt.bufPrint(&buf, "/tmp/.X{d}-lock", .{display_num}); 296 | const file = try std.fs.openFileAbsolute(file_name, .{}); 297 | defer file.close(); 298 | 299 | var file_buf: [20]u8 = undefined; 300 | var fbs = std.io.fixedBufferStream(&file_buf); 301 | 302 | _ = try file.reader().streamUntilDelimiter(fbs.writer(), '\n', 20); 303 | const line = fbs.getWritten(); 304 | 305 | return std.fmt.parseInt(i32, std.mem.trim(u8, line, " "), 10); 306 | } 307 | 308 | fn createXauthFile(pwd: [:0]const u8) ![:0]const u8 { 309 | var xauth_buf: [100]u8 = undefined; 310 | var xauth_dir: [:0]const u8 = undefined; 311 | const xdg_rt_dir = std.posix.getenv("XDG_RUNTIME_DIR"); 312 | var xauth_file: []const u8 = "lyxauth"; 313 | 314 | if (xdg_rt_dir == null) { 315 | const xdg_cfg_home = std.posix.getenv("XDG_CONFIG_HOME"); 316 | var sb: std.c.Stat = undefined; 317 | if (xdg_cfg_home == null) { 318 | xauth_dir = try std.fmt.bufPrintZ(&xauth_buf, "{s}/.config", .{pwd}); 319 | _ = std.c.stat(xauth_dir, &sb); 320 | const mode = sb.mode & std.posix.S.IFMT; 321 | if (mode == std.posix.S.IFDIR) { 322 | xauth_dir = try std.fmt.bufPrintZ(&xauth_buf, "{s}/ly", .{xauth_dir}); 323 | } else { 324 | xauth_dir = pwd; 325 | xauth_file = ".lyxauth"; 326 | } 327 | } else { 328 | xauth_dir = try std.fmt.bufPrintZ(&xauth_buf, "{s}/ly", .{xdg_cfg_home.?}); 329 | } 330 | 331 | _ = std.c.stat(xauth_dir, &sb); 332 | const mode = sb.mode & std.posix.S.IFMT; 333 | if (mode != std.posix.S.IFDIR) { 334 | std.posix.mkdir(xauth_dir, 777) catch { 335 | xauth_dir = pwd; 336 | xauth_file = ".lyxauth"; 337 | }; 338 | } 339 | } else { 340 | xauth_dir = xdg_rt_dir.?; 341 | } 342 | 343 | // Trim trailing slashes 344 | var i = xauth_dir.len - 1; 345 | while (xauth_dir[i] == '/') i -= 1; 346 | const trimmed_xauth_dir = xauth_dir[0 .. i + 1]; 347 | 348 | var buf: [256]u8 = undefined; 349 | const xauthority: [:0]u8 = try std.fmt.bufPrintZ(&buf, "{s}/{s}", .{ trimmed_xauth_dir, xauth_file }); 350 | const file = try std.fs.createFileAbsoluteZ(xauthority, .{}); 351 | file.close(); 352 | 353 | return xauthority; 354 | } 355 | 356 | fn mcookie() [Md5.digest_length * 2]u8 { 357 | var buf: [4096]u8 = undefined; 358 | std.crypto.random.bytes(&buf); 359 | 360 | var out: [Md5.digest_length]u8 = undefined; 361 | Md5.hash(&buf, &out, .{}); 362 | 363 | return std.fmt.bytesToHex(&out, .lower); 364 | } 365 | 366 | fn xauth(display_name: [:0]u8, shell: [*:0]const u8, pw_dir: [*:0]const u8, options: AuthOptions) !void { 367 | var pwd_buf: [100]u8 = undefined; 368 | const pwd = try std.fmt.bufPrintZ(&pwd_buf, "{s}", .{pw_dir}); 369 | 370 | const xauthority = try createXauthFile(pwd); 371 | _ = interop.stdlib.setenv("XAUTHORITY", xauthority, 1); 372 | _ = interop.stdlib.setenv("DISPLAY", display_name, 1); 373 | 374 | const magic_cookie = mcookie(); 375 | 376 | const pid = try std.posix.fork(); 377 | if (pid == 0) { 378 | var cmd_buffer: [1024]u8 = undefined; 379 | const cmd_str = std.fmt.bufPrintZ(&cmd_buffer, "{s} add {s} . {s}", .{ options.xauth_cmd, display_name, magic_cookie }) catch std.process.exit(1); 380 | const args = [_:null]?[*:0]const u8{ shell, "-c", cmd_str }; 381 | std.posix.execveZ(shell, &args, std.c.environ) catch {}; 382 | std.process.exit(1); 383 | } 384 | 385 | const status = std.posix.waitpid(pid, 0); 386 | if (status.status != 0) return error.XauthFailed; 387 | } 388 | 389 | fn executeShellCmd(shell: [*:0]const u8, options: AuthOptions) !void { 390 | // We don't want to redirect stdout and stderr in a shell session 391 | 392 | var cmd_buffer: [1024]u8 = undefined; 393 | const cmd_str = try std.fmt.bufPrintZ(&cmd_buffer, "{s} {s} {s}", .{ options.setup_cmd, options.login_cmd orelse "", shell }); 394 | const args = [_:null]?[*:0]const u8{ shell, "-c", cmd_str }; 395 | return std.posix.execveZ(shell, &args, std.c.environ); 396 | } 397 | 398 | fn executeWaylandCmd(shell: [*:0]const u8, options: AuthOptions, desktop_cmd: []const u8) !void { 399 | const log_file = try redirectStandardStreams(options.session_log, true); 400 | defer log_file.close(); 401 | 402 | var cmd_buffer: [1024]u8 = undefined; 403 | const cmd_str = try std.fmt.bufPrintZ(&cmd_buffer, "{s} {s} {s}", .{ options.setup_cmd, options.login_cmd orelse "", desktop_cmd }); 404 | const args = [_:null]?[*:0]const u8{ shell, "-c", cmd_str }; 405 | return std.posix.execveZ(shell, &args, std.c.environ); 406 | } 407 | 408 | fn executeX11Cmd(shell: [*:0]const u8, pw_dir: [*:0]const u8, options: AuthOptions, desktop_cmd: []const u8, vt: []const u8) !void { 409 | const display_num = try getFreeDisplay(); 410 | var buf: [5]u8 = undefined; 411 | const display_name = try std.fmt.bufPrintZ(&buf, ":{d}", .{display_num}); 412 | try xauth(display_name, shell, pw_dir, options); 413 | 414 | const pid = try std.posix.fork(); 415 | if (pid == 0) { 416 | var cmd_buffer: [1024]u8 = undefined; 417 | const cmd_str = std.fmt.bufPrintZ(&cmd_buffer, "{s} {s} {s}", .{ options.x_cmd, display_name, vt }) catch std.process.exit(1); 418 | const args = [_:null]?[*:0]const u8{ shell, "-c", cmd_str }; 419 | std.posix.execveZ(shell, &args, std.c.environ) catch {}; 420 | std.process.exit(1); 421 | } 422 | 423 | var ok: c_int = undefined; 424 | var xcb: ?*interop.xcb.xcb_connection_t = null; 425 | while (ok != 0) { 426 | xcb = interop.xcb.xcb_connect(null, null); 427 | ok = interop.xcb.xcb_connection_has_error(xcb); 428 | std.posix.kill(pid, 0) catch |e| { 429 | if (e == error.ProcessNotFound and ok != 0) return error.XcbConnectionFailed; 430 | }; 431 | } 432 | 433 | // X Server detaches from the process. 434 | // PID can be fetched from /tmp/X{d}.lock 435 | const x_pid = try getXPid(display_num); 436 | 437 | xorg_pid = try std.posix.fork(); 438 | if (xorg_pid == 0) { 439 | var cmd_buffer: [1024]u8 = undefined; 440 | const cmd_str = std.fmt.bufPrintZ(&cmd_buffer, "{s} {s} {s}", .{ options.setup_cmd, options.login_cmd orelse "", desktop_cmd }) catch std.process.exit(1); 441 | const args = [_:null]?[*:0]const u8{ shell, "-c", cmd_str }; 442 | std.posix.execveZ(shell, &args, std.c.environ) catch {}; 443 | std.process.exit(1); 444 | } 445 | 446 | // If we receive SIGTERM, clean up by killing the xorg_pid process 447 | const act = std.posix.Sigaction{ 448 | .handler = .{ .handler = &xorgSignalHandler }, 449 | .mask = std.posix.empty_sigset, 450 | .flags = 0, 451 | }; 452 | std.posix.sigaction(std.posix.SIG.TERM, &act, null); 453 | 454 | _ = std.posix.waitpid(xorg_pid, 0); 455 | interop.xcb.xcb_disconnect(xcb); 456 | 457 | std.posix.kill(x_pid, 0) catch return; 458 | std.posix.kill(x_pid, std.posix.SIG.KILL) catch {}; 459 | 460 | var status: c_int = 0; 461 | _ = std.c.waitpid(x_pid, &status, 0); 462 | } 463 | 464 | fn redirectStandardStreams(session_log: []const u8, create: bool) !std.fs.File { 465 | 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 })); 466 | 467 | try std.posix.dup2(std.posix.STDOUT_FILENO, std.posix.STDERR_FILENO); 468 | try std.posix.dup2(log_file.handle, std.posix.STDOUT_FILENO); 469 | 470 | return log_file; 471 | } 472 | 473 | fn addUtmpEntry(entry: *Utmp, username: [*:0]const u8, pid: c_int) !void { 474 | entry.ut_type = utmp.USER_PROCESS; 475 | entry.ut_pid = pid; 476 | 477 | var buf: [4096]u8 = undefined; 478 | const ttyname = try std.os.getFdPath(std.posix.STDIN_FILENO, &buf); 479 | 480 | var ttyname_buf: [@sizeOf(@TypeOf(entry.ut_line))]u8 = undefined; 481 | _ = try std.fmt.bufPrintZ(&ttyname_buf, "{s}", .{ttyname["/dev/".len..]}); 482 | 483 | entry.ut_line = ttyname_buf; 484 | entry.ut_id = ttyname_buf["tty".len..7].*; 485 | 486 | var username_buf: [@sizeOf(@TypeOf(entry.ut_user))]u8 = undefined; 487 | _ = try std.fmt.bufPrintZ(&username_buf, "{s}", .{username}); 488 | 489 | entry.ut_user = username_buf; 490 | 491 | var host: [@sizeOf(@TypeOf(entry.ut_host))]u8 = undefined; 492 | host[0] = 0; 493 | entry.ut_host = host; 494 | 495 | var tv: interop.system_time.timeval = undefined; 496 | _ = interop.system_time.gettimeofday(&tv, null); 497 | 498 | entry.ut_tv = .{ 499 | .tv_sec = @intCast(tv.tv_sec), 500 | .tv_usec = @intCast(tv.tv_usec), 501 | }; 502 | entry.ut_addr_v6[0] = 0; 503 | 504 | utmp.setutxent(); 505 | _ = utmp.pututxline(entry); 506 | utmp.endutxent(); 507 | } 508 | 509 | fn removeUtmpEntry(entry: *Utmp) void { 510 | entry.ut_type = utmp.DEAD_PROCESS; 511 | entry.ut_line[0] = 0; 512 | entry.ut_user[0] = 0; 513 | utmp.setutxent(); 514 | _ = utmp.pututxline(entry); 515 | utmp.endutxent(); 516 | } 517 | 518 | fn pamDiagnose(status: c_int) anyerror { 519 | return switch (status) { 520 | interop.pam.PAM_ACCT_EXPIRED => return error.PamAccountExpired, 521 | interop.pam.PAM_AUTH_ERR => return error.PamAuthError, 522 | interop.pam.PAM_AUTHINFO_UNAVAIL => return error.PamAuthInfoUnavailable, 523 | interop.pam.PAM_BUF_ERR => return error.PamBufferError, 524 | interop.pam.PAM_CRED_ERR => return error.PamCredentialsError, 525 | interop.pam.PAM_CRED_EXPIRED => return error.PamCredentialsExpired, 526 | interop.pam.PAM_CRED_INSUFFICIENT => return error.PamCredentialsInsufficient, 527 | interop.pam.PAM_CRED_UNAVAIL => return error.PamCredentialsUnavailable, 528 | interop.pam.PAM_MAXTRIES => return error.PamMaximumTries, 529 | interop.pam.PAM_NEW_AUTHTOK_REQD => return error.PamNewAuthTokenRequired, 530 | interop.pam.PAM_PERM_DENIED => return error.PamPermissionDenied, 531 | interop.pam.PAM_SESSION_ERR => return error.PamSessionError, 532 | interop.pam.PAM_SYSTEM_ERR => return error.PamSystemError, 533 | interop.pam.PAM_USER_UNKNOWN => return error.PamUserUnknown, 534 | else => return error.PamAbort, 535 | }; 536 | } 537 | -------------------------------------------------------------------------------- /src/bigclock.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const interop = @import("interop.zig"); 3 | const enums = @import("enums.zig"); 4 | const Lang = @import("bigclock/Lang.zig"); 5 | const en = @import("bigclock/en.zig"); 6 | const fa = @import("bigclock/fa.zig"); 7 | const Cell = @import("tui/Cell.zig"); 8 | 9 | const Bigclock = enums.Bigclock; 10 | pub const WIDTH = Lang.WIDTH; 11 | pub const HEIGHT = Lang.HEIGHT; 12 | pub const SIZE = Lang.SIZE; 13 | 14 | pub fn clockCell(animate: bool, char: u8, fg: u32, bg: u32, bigclock: Bigclock) [SIZE]Cell { 15 | var cells: [SIZE]Cell = undefined; 16 | 17 | var tv: interop.system_time.timeval = undefined; 18 | _ = interop.system_time.gettimeofday(&tv, null); 19 | 20 | const clock_chars = toBigNumber(if (animate and char == ':' and @divTrunc(tv.tv_usec, 500000) != 0) ' ' else char, bigclock); 21 | for (0..cells.len) |i| cells[i] = Cell.init(clock_chars[i], fg, bg); 22 | 23 | return cells; 24 | } 25 | 26 | pub fn alphaBlit(x: usize, y: usize, tb_width: usize, tb_height: usize, cells: [SIZE]Cell) void { 27 | if (x + WIDTH >= tb_width or y + HEIGHT >= tb_height) return; 28 | 29 | for (0..HEIGHT) |yy| { 30 | for (0..WIDTH) |xx| { 31 | const cell = cells[yy * WIDTH + xx]; 32 | cell.put(x + xx, y + yy); 33 | } 34 | } 35 | } 36 | 37 | fn toBigNumber(char: u8, bigclock: Bigclock) [SIZE]u21 { 38 | const locale_chars = switch (bigclock) { 39 | .fa => fa.locale_chars, 40 | .en => en.locale_chars, 41 | .none => unreachable, 42 | }; 43 | return switch (char) { 44 | '0' => locale_chars.ZERO, 45 | '1' => locale_chars.ONE, 46 | '2' => locale_chars.TWO, 47 | '3' => locale_chars.THREE, 48 | '4' => locale_chars.FOUR, 49 | '5' => locale_chars.FIVE, 50 | '6' => locale_chars.SIX, 51 | '7' => locale_chars.SEVEN, 52 | '8' => locale_chars.EIGHT, 53 | '9' => locale_chars.NINE, 54 | ':' => locale_chars.S, 55 | else => locale_chars.E, 56 | }; 57 | } 58 | -------------------------------------------------------------------------------- /src/bigclock/Lang.zig: -------------------------------------------------------------------------------- 1 | const builtin = @import("builtin"); 2 | 3 | pub const WIDTH = 5; 4 | pub const HEIGHT = 5; 5 | pub const SIZE = WIDTH * HEIGHT; 6 | 7 | pub const X: u32 = if (builtin.os.tag == .linux or builtin.os.tag.isBSD()) 0x2593 else '#'; 8 | pub const O: u32 = 0; 9 | 10 | // zig fmt: off 11 | pub const LocaleChars = struct { 12 | ZERO: [SIZE]u21, 13 | ONE: [SIZE]u21, 14 | TWO: [SIZE]u21, 15 | THREE: [SIZE]u21, 16 | FOUR: [SIZE]u21, 17 | FIVE: [SIZE]u21, 18 | SIX: [SIZE]u21, 19 | SEVEN: [SIZE]u21, 20 | EIGHT: [SIZE]u21, 21 | NINE: [SIZE]u21, 22 | S: [SIZE]u21, 23 | E: [SIZE]u21, 24 | }; 25 | // zig fmt: on 26 | -------------------------------------------------------------------------------- /src/bigclock/en.zig: -------------------------------------------------------------------------------- 1 | const Lang = @import("Lang.zig"); 2 | 3 | const LocaleChars = Lang.LocaleChars; 4 | const X = Lang.X; 5 | const O = Lang.O; 6 | 7 | // zig fmt: off 8 | pub const locale_chars = LocaleChars{ 9 | .ZERO = [_]u21{ 10 | X,X,X,X,X, 11 | X,X,O,X,X, 12 | X,X,O,X,X, 13 | X,X,O,X,X, 14 | X,X,X,X,X, 15 | }, 16 | .ONE = [_]u21{ 17 | O,O,O,X,X, 18 | O,O,O,X,X, 19 | O,O,O,X,X, 20 | O,O,O,X,X, 21 | O,O,O,X,X, 22 | }, 23 | .TWO = [_]u21{ 24 | X,X,X,X,X, 25 | O,O,O,X,X, 26 | X,X,X,X,X, 27 | X,X,O,O,O, 28 | X,X,X,X,X, 29 | }, 30 | .THREE = [_]u21{ 31 | X,X,X,X,X, 32 | O,O,O,X,X, 33 | X,X,X,X,X, 34 | O,O,O,X,X, 35 | X,X,X,X,X, 36 | }, 37 | .FOUR = [_]u21{ 38 | X,X,O,X,X, 39 | X,X,O,X,X, 40 | X,X,X,X,X, 41 | O,O,O,X,X, 42 | O,O,O,X,X, 43 | }, 44 | .FIVE = [_]u21{ 45 | X,X,X,X,X, 46 | X,X,O,O,O, 47 | X,X,X,X,X, 48 | O,O,O,X,X, 49 | X,X,X,X,X, 50 | }, 51 | .SIX = [_]u21{ 52 | X,X,X,X,X, 53 | X,X,O,O,O, 54 | X,X,X,X,X, 55 | X,X,O,X,X, 56 | X,X,X,X,X, 57 | }, 58 | .SEVEN = [_]u21{ 59 | X,X,X,X,X, 60 | O,O,O,X,X, 61 | O,O,O,X,X, 62 | O,O,O,X,X, 63 | O,O,O,X,X, 64 | }, 65 | .EIGHT = [_]u21{ 66 | X,X,X,X,X, 67 | X,X,O,X,X, 68 | X,X,X,X,X, 69 | X,X,O,X,X, 70 | X,X,X,X,X, 71 | }, 72 | .NINE = [_]u21{ 73 | X,X,X,X,X, 74 | X,X,O,X,X, 75 | X,X,X,X,X, 76 | O,O,O,X,X, 77 | X,X,X,X,X, 78 | }, 79 | .S = [_]u21{ 80 | O,O,O,O,O, 81 | O,O,X,O,O, 82 | O,O,O,O,O, 83 | O,O,X,O,O, 84 | O,O,O,O,O, 85 | }, 86 | .E = [_]u21{ 87 | O,O,O,O,O, 88 | O,O,O,O,O, 89 | O,O,O,O,O, 90 | O,O,O,O,O, 91 | O,O,O,O,O, 92 | }, 93 | }; 94 | // zig fmt: on -------------------------------------------------------------------------------- /src/bigclock/fa.zig: -------------------------------------------------------------------------------- 1 | const Lang = @import("Lang.zig"); 2 | 3 | const LocaleChars = Lang.LocaleChars; 4 | const X = Lang.X; 5 | const O = Lang.O; 6 | 7 | // zig fmt: off 8 | pub const locale_chars = LocaleChars{ 9 | .ZERO = [_]u21{ 10 | O,O,O,O,O, 11 | O,O,X,O,O, 12 | O,X,O,X,O, 13 | O,O,X,O,O, 14 | O,O,O,O,O, 15 | }, 16 | .ONE = [_]u21{ 17 | O,O,X,O,O, 18 | O,X,X,O,O, 19 | O,O,X,O,O, 20 | O,O,X,O,O, 21 | O,O,X,O,O, 22 | }, 23 | .TWO = [_]u21{ 24 | O,X,O,X,O, 25 | O,X,X,X,O, 26 | O,X,O,O,O, 27 | O,X,O,O,O, 28 | O,X,O,O,O, 29 | }, 30 | .THREE = [_]u21{ 31 | X,O,X,O,X, 32 | X,X,X,X,X, 33 | X,O,O,O,O, 34 | X,O,O,O,O, 35 | X,O,O,O,O, 36 | }, 37 | .FOUR = [_]u21{ 38 | O,X,O,X,X, 39 | O,X,X,O,O, 40 | O,X,X,X,X, 41 | O,X,O,O,O, 42 | O,X,O,O,O, 43 | }, 44 | .FIVE = [_]u21{ 45 | O,O,X,X,O, 46 | O,X,O,O,X, 47 | X,O,O,O,X, 48 | X,O,X,O,X, 49 | O,X,O,X,O, 50 | }, 51 | .SIX = [_]u21{ 52 | O,X,X,O,O, 53 | O,X,O,O,X, 54 | O,O,X,O,O, 55 | O,X,O,O,O, 56 | X,O,O,O,O, 57 | }, 58 | .SEVEN = [_]u21{ 59 | X,O,O,O,X, 60 | X,O,O,O,X, 61 | O,X,O,X,O, 62 | O,X,O,X,O, 63 | O,O,X,O,O, 64 | }, 65 | .EIGHT = [_]u21{ 66 | O,O,O,X,O, 67 | O,O,X,O,X, 68 | O,O,X,O,X, 69 | O,X,O,O,X, 70 | O,X,O,O,X, 71 | }, 72 | .NINE = [_]u21{ 73 | O,X,X,X,O, 74 | O,X,O,X,O, 75 | O,X,X,X,O, 76 | O,O,O,X,O, 77 | O,O,O,X,O, 78 | }, 79 | .S = [_]u21{ 80 | O,O,O,O,O, 81 | O,O,X,O,O, 82 | O,O,O,O,O, 83 | O,O,X,O,O, 84 | O,O,O,O,O, 85 | }, 86 | .E = [_]u21{ 87 | O,O,O,O,O, 88 | O,O,O,O,O, 89 | O,O,O,O,O, 90 | O,O,O,O,O, 91 | O,O,O,O,O, 92 | }, 93 | }; 94 | // zig fmt: on -------------------------------------------------------------------------------- /src/config/Config.zig: -------------------------------------------------------------------------------- 1 | const build_options = @import("build_options"); 2 | const enums = @import("../enums.zig"); 3 | 4 | const Animation = enums.Animation; 5 | const Input = enums.Input; 6 | const ViMode = enums.ViMode; 7 | const Bigclock = enums.Bigclock; 8 | 9 | allow_empty_password: bool = true, 10 | animation: Animation = .none, 11 | animation_timeout_sec: u12 = 0, 12 | asterisk: ?u32 = '*', 13 | auth_fails: u64 = 10, 14 | bg: u32 = 0x00000000, 15 | bigclock: Bigclock = .none, 16 | blank_box: bool = true, 17 | border_fg: u32 = 0x00FFFFFF, 18 | box_title: ?[]const u8 = null, 19 | brightness_down_cmd: [:0]const u8 = build_options.prefix_directory ++ "/bin/brightnessctl -q s 10%-", 20 | brightness_down_key: ?[]const u8 = "F5", 21 | brightness_up_cmd: [:0]const u8 = build_options.prefix_directory ++ "/bin/brightnessctl -q s +10%", 22 | brightness_up_key: ?[]const u8 = "F6", 23 | clear_password: bool = false, 24 | clock: ?[:0]const u8 = null, 25 | cmatrix_fg: u32 = 0x0000FF00, 26 | cmatrix_min_codepoint: u16 = 0x21, 27 | cmatrix_max_codepoint: u16 = 0x7B, 28 | colormix_col1: u32 = 0x00FF0000, 29 | colormix_col2: u32 = 0x000000FF, 30 | colormix_col3: u32 = 0x20000000, 31 | console_dev: []const u8 = "/dev/console", 32 | default_input: Input = .login, 33 | doom_top_color: u32 = 0x00FF0000, 34 | doom_middle_color: u32 = 0x00FFFF00, 35 | doom_bottom_color: u32 = 0x00FFFFFF, 36 | error_bg: u32 = 0x00000000, 37 | error_fg: u32 = 0x01FF0000, 38 | fg: u32 = 0x00FFFFFF, 39 | hide_borders: bool = false, 40 | hide_key_hints: bool = false, 41 | initial_info_text: ?[]const u8 = null, 42 | input_len: u8 = 34, 43 | lang: []const u8 = "en", 44 | load: bool = true, 45 | login_cmd: ?[]const u8 = null, 46 | logout_cmd: ?[]const u8 = null, 47 | margin_box_h: u8 = 2, 48 | margin_box_v: u8 = 1, 49 | min_refresh_delta: u16 = 5, 50 | numlock: bool = false, 51 | path: ?[:0]const u8 = "/sbin:/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin", 52 | restart_cmd: []const u8 = "/sbin/shutdown -r now", 53 | restart_key: []const u8 = "F2", 54 | save: bool = true, 55 | service_name: [:0]const u8 = "ly", 56 | session_log: []const u8 = "ly-session.log", 57 | setup_cmd: []const u8 = build_options.config_directory ++ "/ly/setup.sh", 58 | shutdown_cmd: []const u8 = "/sbin/shutdown -a now", 59 | shutdown_key: []const u8 = "F1", 60 | sleep_cmd: ?[]const u8 = null, 61 | sleep_key: []const u8 = "F3", 62 | text_in_center: bool = false, 63 | tty: u8 = build_options.tty, 64 | vi_default_mode: ViMode = .normal, 65 | vi_mode: bool = false, 66 | waylandsessions: []const u8 = build_options.prefix_directory ++ "/share/wayland-sessions", 67 | x_cmd: []const u8 = build_options.prefix_directory ++ "/bin/X", 68 | xauth_cmd: []const u8 = build_options.prefix_directory ++ "/bin/xauth", 69 | xinitrc: ?[]const u8 = "~/.xinitrc", 70 | xsessions: []const u8 = build_options.prefix_directory ++ "/share/xsessions", 71 | -------------------------------------------------------------------------------- /src/config/Lang.zig: -------------------------------------------------------------------------------- 1 | // 2 | // NOTE: After editing this file, please run `/res/lang/normalize_lang_files.py` 3 | // to update all the language files accordingly. 4 | // 5 | 6 | authenticating: []const u8 = "authenticating...", 7 | brightness_down: []const u8 = "decrease brightness", 8 | brightness_up: []const u8 = "increase brightness", 9 | capslock: []const u8 = "capslock", 10 | err_alloc: []const u8 = "failed memory allocation", 11 | err_bounds: []const u8 = "out-of-bounds index", 12 | err_brightness_change: []const u8 = "failed to change brightness", 13 | err_chdir: []const u8 = "failed to open home folder", 14 | err_config: []const u8 = "unable to parse config file", 15 | err_console_dev: []const u8 = "failed to access console", 16 | err_dgn_oob: []const u8 = "log message", 17 | err_domain: []const u8 = "invalid domain", 18 | err_empty_password: []const u8 = "empty password not allowed", 19 | err_envlist: []const u8 = "failed to get envlist", 20 | err_hostname: []const u8 = "failed to get hostname", 21 | err_mlock: []const u8 = "failed to lock password memory", 22 | err_null: []const u8 = "null pointer", 23 | err_numlock: []const u8 = "failed to set numlock", 24 | err_pam: []const u8 = "pam transaction failed", 25 | err_pam_abort: []const u8 = "pam transaction aborted", 26 | err_pam_acct_expired: []const u8 = "account expired", 27 | err_pam_auth: []const u8 = "authentication error", 28 | err_pam_authinfo_unavail: []const u8 = "failed to get user info", 29 | err_pam_authok_reqd: []const u8 = "token expired", 30 | err_pam_buf: []const u8 = "memory buffer error", 31 | err_pam_cred_err: []const u8 = "failed to set credentials", 32 | err_pam_cred_expired: []const u8 = "credentials expired", 33 | err_pam_cred_insufficient: []const u8 = "insufficient credentials", 34 | err_pam_cred_unavail: []const u8 = "failed to get credentials", 35 | err_pam_maxtries: []const u8 = "reached maximum tries limit", 36 | err_pam_perm_denied: []const u8 = "permission denied", 37 | err_pam_session: []const u8 = "session error", 38 | err_pam_sys: []const u8 = "system error", 39 | err_pam_user_unknown: []const u8 = "unknown user", 40 | err_path: []const u8 = "failed to set path", 41 | err_perm_dir: []const u8 = "failed to change current directory", 42 | err_perm_group: []const u8 = "failed to downgrade group permissions", 43 | err_perm_user: []const u8 = "failed to downgrade user permissions", 44 | err_pwnam: []const u8 = "failed to get user info", 45 | err_sleep: []const u8 = "failed to execute sleep command", 46 | err_tty_ctrl: []const u8 = "tty control transfer failed", 47 | err_user_gid: []const u8 = "failed to set user GID", 48 | err_user_init: []const u8 = "failed to initialize user", 49 | err_user_uid: []const u8 = "failed to set user UID", 50 | err_xauth: []const u8 = "xauth command failed", 51 | err_xcb_conn: []const u8 = "xcb connection failed", 52 | err_xsessions_dir: []const u8 = "failed to find sessions folder", 53 | err_xsessions_open: []const u8 = "failed to open sessions folder", 54 | insert: []const u8 = "insert", 55 | login: []const u8 = "login:", 56 | logout: []const u8 = "logged out", 57 | no_x11_support: []const u8 = "x11 support disabled at compile-time", 58 | normal: []const u8 = "normal", 59 | numlock: []const u8 = "numlock", 60 | other: []const u8 = "other", 61 | password: []const u8 = "password:", 62 | restart: []const u8 = "reboot", 63 | shell: [:0]const u8 = "shell", 64 | shutdown: []const u8 = "shutdown", 65 | sleep: []const u8 = "sleep", 66 | wayland: []const u8 = "wayland", 67 | x11: []const u8 = "x11", 68 | xinitrc: [:0]const u8 = "xinitrc", 69 | -------------------------------------------------------------------------------- /src/config/Save.zig: -------------------------------------------------------------------------------- 1 | user: ?[]const u8 = null, 2 | session_index: ?usize = null, 3 | -------------------------------------------------------------------------------- /src/config/migrator.zig: -------------------------------------------------------------------------------- 1 | // The migrator ensures compatibility with <=0.6.0 configuration files 2 | 3 | const std = @import("std"); 4 | const ini = @import("zigini"); 5 | const Save = @import("Save.zig"); 6 | const enums = @import("../enums.zig"); 7 | 8 | const color_properties = [_][]const u8{ 9 | "bg", 10 | "border_fg", 11 | "cmatrix_fg", 12 | "colormix_col1", 13 | "colormix_col2", 14 | "colormix_col3", 15 | "error_bg", 16 | "error_fg", 17 | "fg", 18 | }; 19 | const removed_properties = [_][]const u8{ 20 | "wayland_specifier", 21 | "max_desktop_len", 22 | "max_login_len", 23 | "max_password_len", 24 | "mcookie_cmd", 25 | "term_reset_cmd", 26 | "term_restore_cursor_cmd", 27 | "x_cmd_setup", 28 | "wayland_cmd", 29 | }; 30 | 31 | var temporary_allocator = std.heap.page_allocator; 32 | var buffer = std.mem.zeroes([10 * color_properties.len]u8); 33 | 34 | pub var maybe_animate: ?bool = null; 35 | pub var maybe_save_file: ?[]const u8 = null; 36 | 37 | pub var mapped_config_fields = false; 38 | 39 | pub fn configFieldHandler(_: std.mem.Allocator, field: ini.IniField) ?ini.IniField { 40 | if (std.mem.eql(u8, field.key, "animate")) { 41 | // The option doesn't exist anymore, but we save its value for "animation" 42 | maybe_animate = std.mem.eql(u8, field.value, "true"); 43 | 44 | mapped_config_fields = true; 45 | return null; 46 | } 47 | 48 | if (std.mem.eql(u8, field.key, "animation")) { 49 | // The option now uses a string (which then gets converted into an enum) instead of an integer 50 | // It also combines the previous "animate" and "animation" options 51 | const animation = std.fmt.parseInt(u8, field.value, 10) catch return field; 52 | var mapped_field = field; 53 | 54 | mapped_field.value = switch (animation) { 55 | 0 => "doom", 56 | 1 => "matrix", 57 | else => "none", 58 | }; 59 | 60 | mapped_config_fields = true; 61 | return mapped_field; 62 | } 63 | 64 | inline for (color_properties) |property| { 65 | if (std.mem.eql(u8, field.key, property)) { 66 | // These options now uses a 32-bit RGB value instead of an arbitrary 16-bit integer 67 | const color = std.fmt.parseInt(u16, field.value, 0) catch return field; 68 | var mapped_field = field; 69 | 70 | mapped_field.value = mapColor(color) catch return field; 71 | mapped_config_fields = true; 72 | return mapped_field; 73 | } 74 | } 75 | 76 | if (std.mem.eql(u8, field.key, "blank_password")) { 77 | // The option has simply been renamed 78 | var mapped_field = field; 79 | mapped_field.key = "clear_password"; 80 | 81 | mapped_config_fields = true; 82 | return mapped_field; 83 | } 84 | 85 | if (std.mem.eql(u8, field.key, "default_input")) { 86 | // The option now uses a string (which then gets converted into an enum) instead of an integer 87 | const default_input = std.fmt.parseInt(u8, field.value, 10) catch return field; 88 | var mapped_field = field; 89 | 90 | mapped_field.value = switch (default_input) { 91 | 0 => "session", 92 | 1 => "login", 93 | 2 => "password", 94 | else => "login", 95 | }; 96 | 97 | mapped_config_fields = true; 98 | return mapped_field; 99 | } 100 | 101 | if (std.mem.eql(u8, field.key, "save_file")) { 102 | // The option doesn't exist anymore, but we save its value for migration later on 103 | maybe_save_file = temporary_allocator.dupe(u8, field.value) catch return null; 104 | 105 | mapped_config_fields = true; 106 | return null; 107 | } 108 | 109 | inline for (removed_properties) |property| { 110 | if (std.mem.eql(u8, field.key, property)) { 111 | // The options don't exist anymore 112 | mapped_config_fields = true; 113 | return null; 114 | } 115 | } 116 | 117 | if (std.mem.eql(u8, field.key, "bigclock")) { 118 | // The option now uses a string (which then gets converted into an enum) instead of an boolean 119 | // It also includes the ability to change active bigclock's language 120 | var mapped_field = field; 121 | 122 | if (std.mem.eql(u8, field.value, "true")) { 123 | mapped_field.value = "en"; 124 | mapped_config_fields = true; 125 | } else if (std.mem.eql(u8, field.value, "false")) { 126 | mapped_field.value = "none"; 127 | mapped_config_fields = true; 128 | } 129 | 130 | return mapped_field; 131 | } 132 | 133 | return field; 134 | } 135 | 136 | // This is the stuff we only handle after reading the config. 137 | // For example, the "animate" field could come after "animation" 138 | pub fn lateConfigFieldHandler(animation: *enums.Animation) void { 139 | if (maybe_animate) |animate| { 140 | if (!animate) animation.* = .none; 141 | } 142 | } 143 | 144 | pub fn tryMigrateSaveFile(user_buf: *[32]u8) Save { 145 | var save = Save{}; 146 | 147 | if (maybe_save_file) |path| { 148 | defer temporary_allocator.free(path); 149 | 150 | var file = std.fs.openFileAbsolute(path, .{}) catch return save; 151 | defer file.close(); 152 | 153 | const reader = file.reader(); 154 | 155 | var user_fbs = std.io.fixedBufferStream(user_buf); 156 | reader.streamUntilDelimiter(user_fbs.writer(), '\n', user_buf.len) catch return save; 157 | const user = user_fbs.getWritten(); 158 | if (user.len > 0) save.user = user; 159 | 160 | var session_buf: [20]u8 = undefined; 161 | var session_fbs = std.io.fixedBufferStream(&session_buf); 162 | reader.streamUntilDelimiter(session_fbs.writer(), '\n', session_buf.len) catch return save; 163 | 164 | const session_index_str = session_fbs.getWritten(); 165 | var session_index: ?usize = null; 166 | if (session_index_str.len > 0) { 167 | session_index = std.fmt.parseUnsigned(usize, session_index_str, 10) catch return save; 168 | } 169 | save.session_index = session_index; 170 | } 171 | 172 | return save; 173 | } 174 | 175 | fn mapColor(color: u16) ![]const u8 { 176 | const color_no_styling = color & 0x00FF; 177 | const styling_only = color & 0xFF00; 178 | 179 | // If color is "greater" than TB_WHITE, or the styling is "greater" than TB_DIM, 180 | // we have an invalid color, so return an error 181 | if (color_no_styling > 0x0008 or styling_only > 0x8000) return error.InvalidColor; 182 | 183 | var new_color: u32 = switch (color_no_styling) { 184 | 0x0000 => 0x00000000, // Default 185 | 0x0001 => 0x20000000, // "Hi-black" styling 186 | 0x0002 => 0x00FF0000, // Red 187 | 0x0003 => 0x0000FF00, // Green 188 | 0x0004 => 0x00FFFF00, // Yellow 189 | 0x0005 => 0x000000FF, // Blue 190 | 0x0006 => 0x00FF00FF, // Magenta 191 | 0x0007 => 0x0000FFFF, // Cyan 192 | 0x0008 => 0x00FFFFFF, // White 193 | else => unreachable, 194 | }; 195 | 196 | // Only applying styling if color isn't black and styling isn't also black 197 | if (!(new_color == 0x20000000 and styling_only == 0x20000000)) { 198 | // Shift styling by 16 to the left to apply it to the new 32-bit color 199 | new_color |= @as(u32, @intCast(styling_only)) << 16; 200 | } 201 | 202 | return try std.fmt.bufPrint(&buffer, "0x{X}", .{new_color}); 203 | } 204 | -------------------------------------------------------------------------------- /src/enums.zig: -------------------------------------------------------------------------------- 1 | pub const Animation = enum { 2 | none, 3 | doom, 4 | matrix, 5 | colormix, 6 | }; 7 | 8 | pub const DisplayServer = enum { 9 | wayland, 10 | shell, 11 | xinitrc, 12 | x11, 13 | }; 14 | 15 | pub const Input = enum { 16 | info_line, 17 | session, 18 | login, 19 | password, 20 | }; 21 | 22 | pub const ViMode = enum { 23 | normal, 24 | insert, 25 | }; 26 | 27 | pub const Bigclock = enum { 28 | none, 29 | en, 30 | fa, 31 | }; 32 | -------------------------------------------------------------------------------- /src/interop.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const builtin = @import("builtin"); 3 | const Allocator = std.mem.Allocator; 4 | 5 | pub const termbox = @import("termbox2"); 6 | 7 | pub const pam = @cImport({ 8 | @cInclude("security/pam_appl.h"); 9 | }); 10 | 11 | pub const utmp = @cImport({ 12 | @cInclude("utmpx.h"); 13 | }); 14 | 15 | // Exists for X11 support only 16 | pub const xcb = @cImport({ 17 | @cInclude("xcb/xcb.h"); 18 | }); 19 | 20 | pub const unistd = @cImport({ 21 | @cInclude("unistd.h"); 22 | }); 23 | 24 | pub const time = @cImport({ 25 | @cInclude("time.h"); 26 | }); 27 | 28 | pub const system_time = @cImport({ 29 | @cInclude("sys/time.h"); 30 | }); 31 | 32 | pub const stdlib = @cImport({ 33 | @cInclude("stdlib.h"); 34 | }); 35 | 36 | pub const pwd = @cImport({ 37 | @cInclude("pwd.h"); 38 | // We include a FreeBSD-specific header here since login_cap.h references 39 | // the passwd struct directly, so we can't import it separately' 40 | if (builtin.os.tag == .freebsd) @cInclude("login_cap.h"); 41 | }); 42 | 43 | pub const grp = @cImport({ 44 | @cInclude("grp.h"); 45 | }); 46 | 47 | // BSD-specific headers 48 | pub const kbio = @cImport({ 49 | @cInclude("sys/kbio.h"); 50 | }); 51 | 52 | // Linux-specific headers 53 | pub const kd = @cImport({ 54 | @cInclude("sys/kd.h"); 55 | }); 56 | 57 | pub const vt = @cImport({ 58 | @cInclude("sys/vt.h"); 59 | }); 60 | 61 | // Used for getting & setting the lock state 62 | const LedState = if (builtin.os.tag.isBSD()) c_int else c_char; 63 | const get_led_state = if (builtin.os.tag.isBSD()) kbio.KDGETLED else kd.KDGKBLED; 64 | const set_led_state = if (builtin.os.tag.isBSD()) kbio.KDSETLED else kd.KDSKBLED; 65 | const numlock_led = if (builtin.os.tag.isBSD()) kbio.LED_NUM else kd.K_NUMLOCK; 66 | const capslock_led = if (builtin.os.tag.isBSD()) kbio.LED_CAP else kd.K_CAPSLOCK; 67 | 68 | pub fn timeAsString(buf: [:0]u8, format: [:0]const u8) ![]u8 { 69 | const timer = std.time.timestamp(); 70 | const tm_info = time.localtime(&timer); 71 | 72 | const len = time.strftime(buf, buf.len, format, tm_info); 73 | if (len < 0) return error.CannotGetFormattedTime; 74 | 75 | return buf[0..len]; 76 | } 77 | 78 | pub fn switchTty(console_dev: []const u8, tty: u8) !void { 79 | const fd = try std.posix.open(console_dev, .{ .ACCMODE = .WRONLY }, 0); 80 | defer std.posix.close(fd); 81 | 82 | _ = std.c.ioctl(fd, vt.VT_ACTIVATE, tty); 83 | _ = std.c.ioctl(fd, vt.VT_WAITACTIVE, tty); 84 | } 85 | 86 | pub fn getLockState(console_dev: []const u8) !struct { 87 | numlock: bool, 88 | capslock: bool, 89 | } { 90 | const fd = try std.posix.open(console_dev, .{ .ACCMODE = .RDONLY }, 0); 91 | defer std.posix.close(fd); 92 | 93 | var led: LedState = undefined; 94 | _ = std.c.ioctl(fd, get_led_state, &led); 95 | 96 | return .{ 97 | .numlock = (led & numlock_led) != 0, 98 | .capslock = (led & capslock_led) != 0, 99 | }; 100 | } 101 | 102 | pub fn setNumlock(val: bool) !void { 103 | var led: LedState = undefined; 104 | _ = std.c.ioctl(0, get_led_state, &led); 105 | 106 | const numlock = (led & numlock_led) != 0; 107 | if (numlock != val) { 108 | const status = std.c.ioctl(std.posix.STDIN_FILENO, set_led_state, led ^ numlock_led); 109 | if (status != 0) return error.FailedToSetNumlock; 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/main.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const build_options = @import("build_options"); 3 | const builtin = @import("builtin"); 4 | const clap = @import("clap"); 5 | const ini = @import("zigini"); 6 | const auth = @import("auth.zig"); 7 | const bigclock = @import("bigclock.zig"); 8 | const enums = @import("enums.zig"); 9 | const Environment = @import("Environment.zig"); 10 | const interop = @import("interop.zig"); 11 | const ColorMix = @import("animations/ColorMix.zig"); 12 | const Doom = @import("animations/Doom.zig"); 13 | const Dummy = @import("animations/Dummy.zig"); 14 | const Matrix = @import("animations/Matrix.zig"); 15 | const Animation = @import("tui/Animation.zig"); 16 | const TerminalBuffer = @import("tui/TerminalBuffer.zig"); 17 | const Session = @import("tui/components/Session.zig"); 18 | const Text = @import("tui/components/Text.zig"); 19 | const InfoLine = @import("tui/components/InfoLine.zig"); 20 | const Config = @import("config/Config.zig"); 21 | const Lang = @import("config/Lang.zig"); 22 | const Save = @import("config/Save.zig"); 23 | const migrator = @import("config/migrator.zig"); 24 | const SharedError = @import("SharedError.zig"); 25 | 26 | const Ini = ini.Ini; 27 | const DisplayServer = enums.DisplayServer; 28 | const Entry = Environment.Entry; 29 | const termbox = interop.termbox; 30 | const unistd = interop.unistd; 31 | const temporary_allocator = std.heap.page_allocator; 32 | const ly_top_str = "Ly version " ++ build_options.version; 33 | 34 | var session_pid: std.posix.pid_t = -1; 35 | fn signalHandler(i: c_int) callconv(.C) void { 36 | if (session_pid == 0) return; 37 | 38 | // Forward signal to session to clean up 39 | if (session_pid > 0) { 40 | _ = std.c.kill(session_pid, i); 41 | var status: c_int = 0; 42 | _ = std.c.waitpid(session_pid, &status, 0); 43 | } 44 | 45 | _ = termbox.tb_shutdown(); 46 | std.c.exit(i); 47 | } 48 | 49 | fn ttyControlTransferSignalHandler(_: c_int) callconv(.C) void { 50 | _ = termbox.tb_shutdown(); 51 | } 52 | 53 | pub fn main() !void { 54 | var shutdown = false; 55 | var restart = false; 56 | var shutdown_cmd: []const u8 = undefined; 57 | var restart_cmd: []const u8 = undefined; 58 | 59 | const stderr = std.io.getStdErr().writer(); 60 | 61 | defer { 62 | // If we can't shutdown or restart due to an error, we print it to standard error. If that fails, just bail out 63 | if (shutdown) { 64 | const shutdown_error = std.process.execv(temporary_allocator, &[_][]const u8{ "/bin/sh", "-c", shutdown_cmd }); 65 | stderr.print("error: couldn't shutdown: {any}\n", .{shutdown_error}) catch std.process.exit(1); 66 | } else if (restart) { 67 | const restart_error = std.process.execv(temporary_allocator, &[_][]const u8{ "/bin/sh", "-c", restart_cmd }); 68 | stderr.print("error: couldn't restart: {any}\n", .{restart_error}) catch std.process.exit(1); 69 | } else { 70 | // The user has quit Ly using Ctrl+C 71 | temporary_allocator.free(shutdown_cmd); 72 | temporary_allocator.free(restart_cmd); 73 | } 74 | } 75 | 76 | var gpa = std.heap.DebugAllocator(.{}).init; 77 | defer _ = gpa.deinit(); 78 | 79 | // Allows stopping an animation after some time 80 | var tv_zero: interop.system_time.timeval = undefined; 81 | _ = interop.system_time.gettimeofday(&tv_zero, null); 82 | var animation_timed_out: bool = false; 83 | 84 | const allocator = gpa.allocator(); 85 | 86 | // Load arguments 87 | const params = comptime clap.parseParamsComptime( 88 | \\-h, --help Shows all commands. 89 | \\-v, --version Shows the version of Ly. 90 | \\-c, --config Overrides the default configuration path. Example: --config /usr/share/ly 91 | ); 92 | 93 | var diag = clap.Diagnostic{}; 94 | var res = clap.parse(clap.Help, ¶ms, clap.parsers.default, .{ .diagnostic = &diag, .allocator = allocator }) catch |err| { 95 | diag.report(stderr, err) catch {}; 96 | return err; 97 | }; 98 | defer res.deinit(); 99 | 100 | var config: Config = undefined; 101 | var lang: Lang = undefined; 102 | var save: Save = undefined; 103 | var config_load_failed = false; 104 | 105 | if (res.args.help != 0) { 106 | try clap.help(stderr, clap.Help, ¶ms, .{}); 107 | 108 | _ = 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"); 109 | std.process.exit(0); 110 | } 111 | if (res.args.version != 0) { 112 | _ = try stderr.write("Ly version " ++ build_options.version ++ "\n"); 113 | std.process.exit(0); 114 | } 115 | 116 | // Load configuration file 117 | var config_ini = Ini(Config).init(allocator); 118 | defer config_ini.deinit(); 119 | 120 | var lang_ini = Ini(Lang).init(allocator); 121 | defer lang_ini.deinit(); 122 | 123 | var save_ini = Ini(Save).init(allocator); 124 | defer save_ini.deinit(); 125 | 126 | var save_path: []const u8 = build_options.config_directory ++ "/ly/save.ini"; 127 | var save_path_alloc = false; 128 | defer { 129 | if (save_path_alloc) allocator.free(save_path); 130 | } 131 | 132 | const comment_characters = "#"; 133 | 134 | if (res.args.config) |s| { 135 | const trailing_slash = if (s[s.len - 1] != '/') "/" else ""; 136 | 137 | const config_path = try std.fmt.allocPrint(allocator, "{s}{s}config.ini", .{ s, trailing_slash }); 138 | defer allocator.free(config_path); 139 | 140 | config = config_ini.readFileToStruct(config_path, .{ 141 | .fieldHandler = migrator.configFieldHandler, 142 | .comment_characters = comment_characters, 143 | }) catch _config: { 144 | config_load_failed = true; 145 | break :_config Config{}; 146 | }; 147 | 148 | const lang_path = try std.fmt.allocPrint(allocator, "{s}{s}lang/{s}.ini", .{ s, trailing_slash, config.lang }); 149 | defer allocator.free(lang_path); 150 | 151 | lang = lang_ini.readFileToStruct(lang_path, .{ 152 | .fieldHandler = null, 153 | .comment_characters = comment_characters, 154 | }) catch Lang{}; 155 | 156 | if (config.load) { 157 | save_path = try std.fmt.allocPrint(allocator, "{s}{s}save.ini", .{ s, trailing_slash }); 158 | save_path_alloc = true; 159 | 160 | var user_buf: [32]u8 = undefined; 161 | save = save_ini.readFileToStruct(save_path, .{ 162 | .fieldHandler = null, 163 | .comment_characters = comment_characters, 164 | }) catch migrator.tryMigrateSaveFile(&user_buf); 165 | } 166 | 167 | migrator.lateConfigFieldHandler(&config.animation); 168 | } else { 169 | const config_path = build_options.config_directory ++ "/ly/config.ini"; 170 | 171 | config = config_ini.readFileToStruct(config_path, .{ 172 | .fieldHandler = migrator.configFieldHandler, 173 | .comment_characters = comment_characters, 174 | }) catch _config: { 175 | config_load_failed = true; 176 | break :_config Config{}; 177 | }; 178 | 179 | const lang_path = try std.fmt.allocPrint(allocator, "{s}/ly/lang/{s}.ini", .{ build_options.config_directory, config.lang }); 180 | defer allocator.free(lang_path); 181 | 182 | lang = lang_ini.readFileToStruct(lang_path, .{ 183 | .fieldHandler = null, 184 | .comment_characters = comment_characters, 185 | }) catch Lang{}; 186 | 187 | if (config.load) { 188 | var user_buf: [32]u8 = undefined; 189 | save = save_ini.readFileToStruct(save_path, .{ 190 | .fieldHandler = null, 191 | .comment_characters = comment_characters, 192 | }) catch migrator.tryMigrateSaveFile(&user_buf); 193 | } 194 | 195 | migrator.lateConfigFieldHandler(&config.animation); 196 | } 197 | 198 | // if (migrator.mapped_config_fields) save_migrated_config: { 199 | // var file = try std.fs.cwd().createFile(config_path, .{}); 200 | // defer file.close(); 201 | 202 | // const writer = file.writer(); 203 | // ini.writeFromStruct(config, writer, null, true, .{}) catch { 204 | // break :save_migrated_config; 205 | // }; 206 | // } 207 | 208 | // These strings only end up getting freed if the user quits Ly using Ctrl+C, which is fine since in the other cases 209 | // we end up shutting down or restarting the system 210 | shutdown_cmd = try temporary_allocator.dupe(u8, config.shutdown_cmd); 211 | restart_cmd = try temporary_allocator.dupe(u8, config.restart_cmd); 212 | 213 | // Initialize termbox 214 | _ = termbox.tb_init(); 215 | defer _ = termbox.tb_shutdown(); 216 | 217 | const act = std.posix.Sigaction{ 218 | .handler = .{ .handler = &signalHandler }, 219 | .mask = std.posix.empty_sigset, 220 | .flags = 0, 221 | }; 222 | std.posix.sigaction(std.posix.SIG.TERM, &act, null); 223 | 224 | _ = termbox.tb_set_output_mode(termbox.TB_OUTPUT_TRUECOLOR); 225 | _ = termbox.tb_clear(); 226 | 227 | // Needed to reset termbox after auth 228 | const tb_termios = try std.posix.tcgetattr(std.posix.STDIN_FILENO); 229 | 230 | // Initialize terminal buffer 231 | const labels_max_length = @max(lang.login.len, lang.password.len); 232 | 233 | var seed: u64 = undefined; 234 | std.crypto.random.bytes(std.mem.asBytes(&seed)); // Get a random seed for the PRNG (used by animations) 235 | 236 | var prng = std.Random.DefaultPrng.init(seed); 237 | const random = prng.random(); 238 | 239 | const buffer_options = TerminalBuffer.InitOptions{ 240 | .fg = config.fg, 241 | .bg = config.bg, 242 | .border_fg = config.border_fg, 243 | .margin_box_h = config.margin_box_h, 244 | .margin_box_v = config.margin_box_v, 245 | .input_len = config.input_len, 246 | }; 247 | var buffer = TerminalBuffer.init(buffer_options, labels_max_length, random); 248 | 249 | // Initialize components 250 | var info_line = InfoLine.init(allocator, &buffer); 251 | defer info_line.deinit(); 252 | 253 | if (config_load_failed) { 254 | // We can't localize this since the config failed to load so we'd fallback to the default language anyway 255 | try info_line.addMessage("unable to parse config file", config.error_bg, config.error_fg); 256 | } 257 | 258 | interop.setNumlock(config.numlock) catch { 259 | try info_line.addMessage(lang.err_numlock, config.error_bg, config.error_fg); 260 | }; 261 | 262 | var session = Session.init(allocator, &buffer); 263 | defer session.deinit(); 264 | 265 | addOtherEnvironment(&session, lang, .shell, null) catch { 266 | try info_line.addMessage(lang.err_alloc, config.error_bg, config.error_fg); 267 | }; 268 | 269 | if (build_options.enable_x11_support) { 270 | if (config.xinitrc) |xinitrc_cmd| { 271 | addOtherEnvironment(&session, lang, .xinitrc, xinitrc_cmd) catch { 272 | try info_line.addMessage(lang.err_alloc, config.error_bg, config.error_fg); 273 | }; 274 | } 275 | } else { 276 | try info_line.addMessage(lang.no_x11_support, config.bg, config.fg); 277 | } 278 | 279 | if (config.initial_info_text) |text| { 280 | try info_line.addMessage(text, config.bg, config.fg); 281 | } else get_host_name: { 282 | // Initialize information line with host name 283 | var name_buf: [std.posix.HOST_NAME_MAX]u8 = undefined; 284 | const hostname = std.posix.gethostname(&name_buf) catch { 285 | try info_line.addMessage(lang.err_hostname, config.error_bg, config.error_fg); 286 | break :get_host_name; 287 | }; 288 | try info_line.addMessage(hostname, config.bg, config.fg); 289 | } 290 | 291 | var wayland_session_dirs = std.mem.splitScalar(u8, config.waylandsessions, ':'); 292 | while (wayland_session_dirs.next()) |dir| { 293 | try crawl(&session, lang, dir, .wayland); 294 | } 295 | if (build_options.enable_x11_support) { 296 | var x_session_dirs = std.mem.splitScalar(u8, config.xsessions, ':'); 297 | while (x_session_dirs.next()) |dir| { 298 | try crawl(&session, lang, dir, .x11); 299 | } 300 | } 301 | 302 | var login = Text.init(allocator, &buffer, false, null); 303 | defer login.deinit(); 304 | 305 | var password = Text.init(allocator, &buffer, true, config.asterisk); 306 | defer password.deinit(); 307 | 308 | var active_input = config.default_input; 309 | var insert_mode = !config.vi_mode or config.vi_default_mode == .insert; 310 | 311 | // Load last saved username and desktop selection, if any 312 | if (config.load) { 313 | if (save.user) |user| { 314 | try login.text.appendSlice(login.allocator, user); 315 | login.end = user.len; 316 | login.cursor = login.end; 317 | active_input = .password; 318 | } 319 | 320 | if (save.session_index) |session_index| { 321 | if (session_index < session.label.list.items.len) session.label.current = session_index; 322 | } 323 | } 324 | 325 | // Place components on the screen 326 | { 327 | buffer.drawBoxCenter(!config.hide_borders, config.blank_box); 328 | 329 | const coordinates = buffer.calculateComponentCoordinates(); 330 | info_line.label.position(coordinates.start_x, coordinates.y, coordinates.full_visible_length, null); 331 | session.label.position(coordinates.x, coordinates.y + 2, coordinates.visible_length, config.text_in_center); 332 | login.position(coordinates.x, coordinates.y + 4, coordinates.visible_length); 333 | password.position(coordinates.x, coordinates.y + 6, coordinates.visible_length); 334 | 335 | switch (active_input) { 336 | .info_line => info_line.label.handle(null, insert_mode), 337 | .session => session.label.handle(null, insert_mode), 338 | .login => login.handle(null, insert_mode) catch { 339 | try info_line.addMessage(lang.err_alloc, config.error_bg, config.error_fg); 340 | }, 341 | .password => password.handle(null, insert_mode) catch { 342 | try info_line.addMessage(lang.err_alloc, config.error_bg, config.error_fg); 343 | }, 344 | } 345 | } 346 | 347 | // Initialize the animation, if any 348 | var animation: Animation = undefined; 349 | 350 | switch (config.animation) { 351 | .none => { 352 | var dummy = Dummy{}; 353 | animation = dummy.animation(); 354 | }, 355 | .doom => { 356 | var doom = try Doom.init(allocator, &buffer, config.doom_top_color, config.doom_middle_color, config.doom_bottom_color); 357 | animation = doom.animation(); 358 | }, 359 | .matrix => { 360 | var matrix = try Matrix.init(allocator, &buffer, config.cmatrix_fg, config.cmatrix_min_codepoint, config.cmatrix_max_codepoint); 361 | animation = matrix.animation(); 362 | }, 363 | .colormix => { 364 | var color_mix = ColorMix.init(&buffer, config.colormix_col1, config.colormix_col2, config.colormix_col3); 365 | animation = color_mix.animation(); 366 | }, 367 | } 368 | defer animation.deinit(); 369 | 370 | const animate = config.animation != .none; 371 | const shutdown_key = try std.fmt.parseInt(u8, config.shutdown_key[1..], 10); 372 | const shutdown_len = try TerminalBuffer.strWidth(lang.shutdown); 373 | const restart_key = try std.fmt.parseInt(u8, config.restart_key[1..], 10); 374 | const restart_len = try TerminalBuffer.strWidth(lang.restart); 375 | const sleep_key = try std.fmt.parseInt(u8, config.sleep_key[1..], 10); 376 | const sleep_len = try TerminalBuffer.strWidth(lang.sleep); 377 | const brightness_down_key = if (config.brightness_down_key) |key| try std.fmt.parseInt(u8, key[1..], 10) else null; 378 | const brightness_down_len = try TerminalBuffer.strWidth(lang.brightness_down); 379 | const brightness_up_key = if (config.brightness_up_key) |key| try std.fmt.parseInt(u8, key[1..], 10) else null; 380 | const brightness_up_len = try TerminalBuffer.strWidth(lang.brightness_up); 381 | 382 | var event: termbox.tb_event = undefined; 383 | var run = true; 384 | var update = true; 385 | var resolution_changed = false; 386 | var auth_fails: u64 = 0; 387 | var can_access_console_dev = true; 388 | 389 | // Switch to selected TTY if possible 390 | interop.switchTty(config.console_dev, config.tty) catch { 391 | try info_line.addMessage(lang.err_console_dev, config.error_bg, config.error_fg); 392 | can_access_console_dev = false; 393 | }; 394 | 395 | while (run) { 396 | // If there's no input or there's an animation, a resolution change needs to be checked 397 | if (!update or animate) { 398 | if (!update) std.Thread.sleep(std.time.ns_per_ms * 100); 399 | 400 | _ = termbox.tb_present(); // Required to update tb_width() and tb_height() 401 | 402 | const width: usize = @intCast(termbox.tb_width()); 403 | const height: usize = @intCast(termbox.tb_height()); 404 | 405 | if (width != buffer.width or height != buffer.height) { 406 | // If it did change, then update the cell buffer, reallocate the current animation's buffers, and force a draw update 407 | 408 | buffer.width = width; 409 | buffer.height = height; 410 | 411 | animation.realloc() catch { 412 | try info_line.addMessage(lang.err_alloc, config.error_bg, config.error_fg); 413 | }; 414 | 415 | update = true; 416 | resolution_changed = true; 417 | } 418 | } 419 | 420 | if (update) { 421 | // If the user entered a wrong password 10 times in a row, play a cascade animation, else update normally 422 | if (auth_fails < config.auth_fails) { 423 | _ = termbox.tb_clear(); 424 | 425 | if (!animation_timed_out) animation.draw(); 426 | 427 | buffer.drawLabel(ly_top_str, 0, 0); 428 | 429 | if (config.bigclock != .none and buffer.box_height + (bigclock.HEIGHT + 2) * 2 < buffer.height) draw_big_clock: { 430 | const format = "%H:%M"; 431 | const xo = buffer.width / 2 - @min(buffer.width, (format.len * (bigclock.WIDTH + 1))) / 2; 432 | const yo = (buffer.height - buffer.box_height) / 2 - bigclock.HEIGHT - 2; 433 | 434 | var clock_buf: [format.len + 1:0]u8 = undefined; 435 | const clock_str = interop.timeAsString(&clock_buf, format) catch { 436 | break :draw_big_clock; 437 | }; 438 | 439 | for (clock_str, 0..) |c, i| { 440 | const clock_cell = bigclock.clockCell(animate, c, buffer.fg, buffer.bg, config.bigclock); 441 | bigclock.alphaBlit(xo + i * (bigclock.WIDTH + 1), yo, buffer.width, buffer.height, clock_cell); 442 | } 443 | } 444 | 445 | buffer.drawBoxCenter(!config.hide_borders, config.blank_box); 446 | 447 | if (resolution_changed) { 448 | const coordinates = buffer.calculateComponentCoordinates(); 449 | info_line.label.position(coordinates.start_x, coordinates.y, coordinates.full_visible_length, null); 450 | session.label.position(coordinates.x, coordinates.y + 2, coordinates.visible_length, config.text_in_center); 451 | login.position(coordinates.x, coordinates.y + 4, coordinates.visible_length); 452 | password.position(coordinates.x, coordinates.y + 6, coordinates.visible_length); 453 | 454 | resolution_changed = false; 455 | } 456 | 457 | switch (active_input) { 458 | .info_line => info_line.label.handle(null, insert_mode), 459 | .session => session.label.handle(null, insert_mode), 460 | .login => login.handle(null, insert_mode) catch { 461 | try info_line.addMessage(lang.err_alloc, config.error_bg, config.error_fg); 462 | }, 463 | .password => password.handle(null, insert_mode) catch { 464 | try info_line.addMessage(lang.err_alloc, config.error_bg, config.error_fg); 465 | }, 466 | } 467 | 468 | if (config.clock) |clock| draw_clock: { 469 | var clock_buf: [32:0]u8 = undefined; 470 | const clock_str = interop.timeAsString(&clock_buf, clock) catch { 471 | break :draw_clock; 472 | }; 473 | 474 | if (clock_str.len == 0) return error.FormattedTimeEmpty; 475 | 476 | buffer.drawLabel(clock_str, buffer.width - @min(buffer.width, clock_str.len), 0); 477 | } 478 | 479 | const label_x = buffer.box_x + buffer.margin_box_h; 480 | const label_y = buffer.box_y + buffer.margin_box_v; 481 | 482 | buffer.drawLabel(lang.login, label_x, label_y + 4); 483 | buffer.drawLabel(lang.password, label_x, label_y + 6); 484 | 485 | info_line.label.draw(); 486 | 487 | if (!config.hide_key_hints) { 488 | var length: usize = ly_top_str.len + 1; 489 | 490 | buffer.drawLabel(config.shutdown_key, length, 0); 491 | length += config.shutdown_key.len + 1; 492 | buffer.drawLabel(" ", length - 1, 0); 493 | 494 | buffer.drawLabel(lang.shutdown, length, 0); 495 | length += shutdown_len + 1; 496 | 497 | buffer.drawLabel(config.restart_key, length, 0); 498 | length += config.restart_key.len + 1; 499 | buffer.drawLabel(" ", length - 1, 0); 500 | 501 | buffer.drawLabel(lang.restart, length, 0); 502 | length += restart_len + 1; 503 | 504 | if (config.sleep_cmd != null) { 505 | buffer.drawLabel(config.sleep_key, length, 0); 506 | length += config.sleep_key.len + 1; 507 | buffer.drawLabel(" ", length - 1, 0); 508 | 509 | buffer.drawLabel(lang.sleep, length, 0); 510 | length += sleep_len + 1; 511 | } 512 | 513 | if (config.brightness_down_key) |key| { 514 | buffer.drawLabel(key, length, 0); 515 | length += key.len + 1; 516 | buffer.drawLabel(" ", length - 1, 0); 517 | 518 | buffer.drawLabel(lang.brightness_down, length, 0); 519 | length += brightness_down_len + 1; 520 | } 521 | 522 | if (config.brightness_up_key) |key| { 523 | buffer.drawLabel(key, length, 0); 524 | length += key.len + 1; 525 | buffer.drawLabel(" ", length - 1, 0); 526 | 527 | buffer.drawLabel(lang.brightness_up, length, 0); 528 | length += brightness_up_len + 1; 529 | } 530 | } 531 | 532 | if (config.box_title) |title| { 533 | buffer.drawConfinedLabel(title, buffer.box_x, buffer.box_y - 1, buffer.box_width); 534 | } 535 | 536 | if (config.vi_mode) { 537 | const label_txt = if (insert_mode) lang.insert else lang.normal; 538 | buffer.drawLabel(label_txt, buffer.box_x, buffer.box_y + buffer.box_height); 539 | } 540 | 541 | if (can_access_console_dev) draw_lock_state: { 542 | const lock_state = interop.getLockState(config.console_dev) catch { 543 | try info_line.addMessage(lang.err_console_dev, config.error_bg, config.error_fg); 544 | break :draw_lock_state; 545 | }; 546 | 547 | var lock_state_x = buffer.width - @min(buffer.width, lang.numlock.len); 548 | const lock_state_y: usize = if (config.clock != null) 1 else 0; 549 | 550 | if (lock_state.numlock) buffer.drawLabel(lang.numlock, lock_state_x, lock_state_y); 551 | 552 | if (lock_state_x >= lang.capslock.len + 1) { 553 | lock_state_x -= lang.capslock.len + 1; 554 | if (lock_state.capslock) buffer.drawLabel(lang.capslock, lock_state_x, lock_state_y); 555 | } 556 | } 557 | 558 | session.label.draw(); 559 | login.draw(); 560 | password.draw(); 561 | } else { 562 | std.Thread.sleep(std.time.ns_per_ms * 10); 563 | update = buffer.cascade(); 564 | 565 | if (!update) { 566 | std.Thread.sleep(std.time.ns_per_s * 7); 567 | auth_fails = 0; 568 | } 569 | } 570 | 571 | _ = termbox.tb_present(); 572 | } 573 | 574 | var timeout: i32 = -1; 575 | 576 | // Calculate the maximum timeout based on current animations, or the (big) clock. If there's none, we wait for the event indefinitely instead 577 | if (animate and !animation_timed_out) { 578 | timeout = config.min_refresh_delta; 579 | 580 | // check how long we have been running so we can turn off the animation 581 | var tv: interop.system_time.timeval = undefined; 582 | _ = interop.system_time.gettimeofday(&tv, null); 583 | 584 | if (config.animation_timeout_sec > 0 and tv.tv_sec - tv_zero.tv_sec > config.animation_timeout_sec) { 585 | animation_timed_out = true; 586 | animation.deinit(); 587 | } 588 | } else if (config.bigclock != .none and config.clock == null) { 589 | var tv: interop.system_time.timeval = undefined; 590 | _ = interop.system_time.gettimeofday(&tv, null); 591 | 592 | timeout = @intCast((60 - @rem(tv.tv_sec, 60)) * 1000 - @divTrunc(tv.tv_usec, 1000) + 1); 593 | } else if (config.clock != null or auth_fails >= config.auth_fails) { 594 | var tv: interop.system_time.timeval = undefined; 595 | _ = interop.system_time.gettimeofday(&tv, null); 596 | 597 | timeout = @intCast(1000 - @divTrunc(tv.tv_usec, 1000) + 1); 598 | } 599 | 600 | const event_error = if (timeout == -1) termbox.tb_poll_event(&event) else termbox.tb_peek_event(&event, timeout); 601 | 602 | update = timeout != -1; 603 | 604 | if (event_error < 0 or event.type != termbox.TB_EVENT_KEY) continue; 605 | 606 | switch (event.key) { 607 | termbox.TB_KEY_ESC => { 608 | if (config.vi_mode and insert_mode) { 609 | insert_mode = false; 610 | update = true; 611 | } 612 | }, 613 | termbox.TB_KEY_F12...termbox.TB_KEY_F1 => { 614 | const pressed_key = 0xFFFF - event.key + 1; 615 | if (pressed_key == shutdown_key) { 616 | shutdown = true; 617 | run = false; 618 | } else if (pressed_key == restart_key) { 619 | restart = true; 620 | run = false; 621 | } else if (pressed_key == sleep_key) { 622 | if (config.sleep_cmd) |sleep_cmd| { 623 | var sleep = std.process.Child.init(&[_][]const u8{ "/bin/sh", "-c", sleep_cmd }, allocator); 624 | sleep.stdout_behavior = .Ignore; 625 | sleep.stderr_behavior = .Ignore; 626 | 627 | handle_sleep_cmd: { 628 | const process_result = sleep.spawnAndWait() catch { 629 | break :handle_sleep_cmd; 630 | }; 631 | if (process_result.Exited != 0) { 632 | try info_line.addMessage(lang.err_sleep, config.error_bg, config.error_fg); 633 | } 634 | } 635 | } 636 | } else if (brightness_down_key != null and pressed_key == brightness_down_key.?) { 637 | adjustBrightness(allocator, config.brightness_down_cmd) catch { 638 | try info_line.addMessage(lang.err_brightness_change, config.error_bg, config.error_fg); 639 | }; 640 | } else if (brightness_up_key != null and pressed_key == brightness_up_key.?) { 641 | adjustBrightness(allocator, config.brightness_up_cmd) catch { 642 | try info_line.addMessage(lang.err_brightness_change, config.error_bg, config.error_fg); 643 | }; 644 | } 645 | }, 646 | termbox.TB_KEY_CTRL_C => run = false, 647 | termbox.TB_KEY_CTRL_U => { 648 | if (active_input == .login) { 649 | login.clear(); 650 | update = true; 651 | } else if (active_input == .password) { 652 | password.clear(); 653 | update = true; 654 | } 655 | }, 656 | termbox.TB_KEY_CTRL_K, termbox.TB_KEY_ARROW_UP => { 657 | active_input = switch (active_input) { 658 | .session, .info_line => .info_line, 659 | .login => .session, 660 | .password => .login, 661 | }; 662 | update = true; 663 | }, 664 | termbox.TB_KEY_CTRL_J, termbox.TB_KEY_ARROW_DOWN => { 665 | active_input = switch (active_input) { 666 | .info_line => .session, 667 | .session => .login, 668 | .login, .password => .password, 669 | }; 670 | update = true; 671 | }, 672 | termbox.TB_KEY_TAB => { 673 | active_input = switch (active_input) { 674 | .info_line => .session, 675 | .session => .login, 676 | .login => .password, 677 | .password => .info_line, 678 | }; 679 | update = true; 680 | }, 681 | termbox.TB_KEY_BACK_TAB => { 682 | active_input = switch (active_input) { 683 | .info_line => .password, 684 | .session => .info_line, 685 | .login => .session, 686 | .password => .login, 687 | }; 688 | 689 | update = true; 690 | }, 691 | termbox.TB_KEY_ENTER => authenticate: { 692 | if (!config.allow_empty_password and password.text.items.len == 0) { 693 | try info_line.addMessage(lang.err_empty_password, config.error_bg, config.error_fg); 694 | InfoLine.clearRendered(allocator, buffer) catch { 695 | try info_line.addMessage(lang.err_alloc, config.error_bg, config.error_fg); 696 | }; 697 | info_line.label.draw(); 698 | _ = termbox.tb_present(); 699 | break :authenticate; 700 | } 701 | 702 | try info_line.addMessage(lang.authenticating, config.bg, config.fg); 703 | InfoLine.clearRendered(allocator, buffer) catch { 704 | try info_line.addMessage(lang.err_alloc, config.error_bg, config.error_fg); 705 | }; 706 | info_line.label.draw(); 707 | _ = termbox.tb_present(); 708 | 709 | if (config.save) save_last_settings: { 710 | var file = std.fs.cwd().createFile(save_path, .{}) catch break :save_last_settings; 711 | defer file.close(); 712 | 713 | const save_data = Save{ 714 | .user = login.text.items, 715 | .session_index = session.label.current, 716 | }; 717 | ini.writeFromStruct(save_data, file.writer(), null, .{}) catch break :save_last_settings; 718 | 719 | // Delete previous save file if it exists 720 | if (migrator.maybe_save_file) |path| std.fs.cwd().deleteFile(path) catch {}; 721 | } 722 | 723 | var shared_err = try SharedError.init(); 724 | defer shared_err.deinit(); 725 | 726 | { 727 | const login_text = try allocator.dupeZ(u8, login.text.items); 728 | defer allocator.free(login_text); 729 | const password_text = try allocator.dupeZ(u8, password.text.items); 730 | defer allocator.free(password_text); 731 | 732 | session_pid = try std.posix.fork(); 733 | if (session_pid == 0) { 734 | const current_environment = session.label.list.items[session.label.current]; 735 | const auth_options = auth.AuthOptions{ 736 | .tty = config.tty, 737 | .service_name = config.service_name, 738 | .path = config.path, 739 | .session_log = config.session_log, 740 | .xauth_cmd = config.xauth_cmd, 741 | .setup_cmd = config.setup_cmd, 742 | .login_cmd = config.login_cmd, 743 | .x_cmd = config.x_cmd, 744 | .session_pid = session_pid, 745 | }; 746 | 747 | // Signal action to give up control on the TTY 748 | const tty_control_transfer_act = std.posix.Sigaction{ 749 | .handler = .{ .handler = &ttyControlTransferSignalHandler }, 750 | .mask = std.posix.empty_sigset, 751 | .flags = 0, 752 | }; 753 | std.posix.sigaction(std.posix.SIG.CHLD, &tty_control_transfer_act, null); 754 | 755 | auth.authenticate(auth_options, current_environment, login_text, password_text) catch |err| { 756 | shared_err.writeError(err); 757 | std.process.exit(1); 758 | }; 759 | std.process.exit(0); 760 | } 761 | 762 | _ = std.posix.waitpid(session_pid, 0); 763 | session_pid = -1; 764 | } 765 | 766 | // Take back control of the TTY 767 | _ = termbox.tb_init(); 768 | _ = termbox.tb_set_output_mode(termbox.TB_OUTPUT_TRUECOLOR); 769 | 770 | const auth_err = shared_err.readError(); 771 | if (auth_err) |err| { 772 | auth_fails += 1; 773 | active_input = .password; 774 | try info_line.addMessage(getAuthErrorMsg(err, lang), config.error_bg, config.error_fg); 775 | if (config.clear_password or err != error.PamAuthError) password.clear(); 776 | } else { 777 | if (config.logout_cmd) |logout_cmd| { 778 | var logout_process = std.process.Child.init(&[_][]const u8{ "/bin/sh", "-c", logout_cmd }, allocator); 779 | _ = logout_process.spawnAndWait() catch .{}; 780 | } 781 | 782 | password.clear(); 783 | try info_line.addMessage(lang.logout, config.bg, config.fg); 784 | } 785 | 786 | // Clear the TTY because termbox2 doesn't properly do it 787 | const capability = termbox.global.caps[termbox.TB_CAP_CLEAR_SCREEN]; 788 | const capability_slice = capability[0..std.mem.len(capability)]; 789 | _ = try std.posix.write(termbox.global.ttyfd, capability_slice); 790 | 791 | try std.posix.tcsetattr(std.posix.STDIN_FILENO, .FLUSH, tb_termios); 792 | if (auth_fails < config.auth_fails) _ = termbox.tb_clear(); 793 | 794 | update = true; 795 | 796 | // Restore the cursor 797 | _ = termbox.tb_set_cursor(0, 0); 798 | _ = termbox.tb_present(); 799 | }, 800 | else => { 801 | if (!insert_mode) { 802 | switch (event.ch) { 803 | 'k' => { 804 | active_input = switch (active_input) { 805 | .session, .info_line => .info_line, 806 | .login => .session, 807 | .password => .login, 808 | }; 809 | update = true; 810 | continue; 811 | }, 812 | 'j' => { 813 | active_input = switch (active_input) { 814 | .info_line => .session, 815 | .session => .login, 816 | .login, .password => .password, 817 | }; 818 | update = true; 819 | continue; 820 | }, 821 | 'i' => { 822 | insert_mode = true; 823 | update = true; 824 | continue; 825 | }, 826 | else => {}, 827 | } 828 | } 829 | 830 | switch (active_input) { 831 | .info_line => info_line.label.handle(&event, insert_mode), 832 | .session => session.label.handle(&event, insert_mode), 833 | .login => login.handle(&event, insert_mode) catch { 834 | try info_line.addMessage(lang.err_alloc, config.error_bg, config.error_fg); 835 | }, 836 | .password => password.handle(&event, insert_mode) catch { 837 | try info_line.addMessage(lang.err_alloc, config.error_bg, config.error_fg); 838 | }, 839 | } 840 | update = true; 841 | }, 842 | } 843 | } 844 | } 845 | 846 | fn addOtherEnvironment(session: *Session, lang: Lang, display_server: DisplayServer, exec: ?[]const u8) !void { 847 | const name = switch (display_server) { 848 | .shell => lang.shell, 849 | .xinitrc => lang.xinitrc, 850 | else => unreachable, 851 | }; 852 | 853 | try session.addEnvironment(.{ 854 | .entry_ini = null, 855 | .name = name, 856 | .xdg_session_desktop = null, 857 | .xdg_desktop_names = null, 858 | .cmd = exec orelse "", 859 | .specifier = switch (display_server) { 860 | .wayland => lang.wayland, 861 | .x11 => lang.x11, 862 | else => lang.other, 863 | }, 864 | .display_server = display_server, 865 | }); 866 | } 867 | 868 | fn crawl(session: *Session, lang: Lang, path: []const u8, display_server: DisplayServer) !void { 869 | var iterable_directory = std.fs.openDirAbsolute(path, .{ .iterate = true }) catch return; 870 | defer iterable_directory.close(); 871 | 872 | var iterator = iterable_directory.iterate(); 873 | while (try iterator.next()) |item| { 874 | if (!std.mem.eql(u8, std.fs.path.extension(item.name), ".desktop")) continue; 875 | 876 | const entry_path = try std.fmt.allocPrint(session.label.allocator, "{s}/{s}", .{ path, item.name }); 877 | defer session.label.allocator.free(entry_path); 878 | var entry_ini = Ini(Entry).init(session.label.allocator); 879 | _ = try entry_ini.readFileToStruct(entry_path, .{ 880 | .fieldHandler = null, 881 | .comment_characters = "#", 882 | }); 883 | errdefer entry_ini.deinit(); 884 | 885 | var xdg_session_desktop: []const u8 = undefined; 886 | const maybe_desktop_names = entry_ini.data.@"Desktop Entry".DesktopNames; 887 | if (maybe_desktop_names) |desktop_names| { 888 | xdg_session_desktop = std.mem.sliceTo(desktop_names, ';'); 889 | } else { 890 | // if DesktopNames is empty, we'll take the name of the session file 891 | xdg_session_desktop = std.fs.path.stem(item.name); 892 | } 893 | 894 | // Prepare the XDG_CURRENT_DESKTOP environment variable here 895 | const entry = entry_ini.data.@"Desktop Entry"; 896 | var xdg_desktop_names: ?[:0]const u8 = null; 897 | if (entry.DesktopNames) |desktop_names| { 898 | for (desktop_names) |*c| { 899 | if (c.* == ';') c.* = ':'; 900 | } 901 | xdg_desktop_names = desktop_names; 902 | } 903 | 904 | const session_desktop = try session.label.allocator.dupeZ(u8, xdg_session_desktop); 905 | errdefer session.label.allocator.free(session_desktop); 906 | 907 | try session.addEnvironment(.{ 908 | .entry_ini = entry_ini, 909 | .name = entry.Name, 910 | .xdg_session_desktop = session_desktop, 911 | .xdg_desktop_names = xdg_desktop_names, 912 | .cmd = entry.Exec, 913 | .specifier = switch (display_server) { 914 | .wayland => lang.wayland, 915 | .x11 => lang.x11, 916 | else => lang.other, 917 | }, 918 | .display_server = display_server, 919 | }); 920 | } 921 | } 922 | 923 | fn adjustBrightness(allocator: std.mem.Allocator, cmd: []const u8) !void { 924 | var brightness = std.process.Child.init(&[_][]const u8{ "/bin/sh", "-c", cmd }, allocator); 925 | brightness.stdout_behavior = .Ignore; 926 | brightness.stderr_behavior = .Ignore; 927 | 928 | handle_brightness_cmd: { 929 | const process_result = brightness.spawnAndWait() catch { 930 | break :handle_brightness_cmd; 931 | }; 932 | if (process_result.Exited != 0) { 933 | return error.BrightnessChangeFailed; 934 | } 935 | } 936 | } 937 | 938 | fn getAuthErrorMsg(err: anyerror, lang: Lang) []const u8 { 939 | return switch (err) { 940 | error.GetPasswordNameFailed => lang.err_pwnam, 941 | error.GetEnvListFailed => lang.err_envlist, 942 | error.XauthFailed => lang.err_xauth, 943 | error.XcbConnectionFailed => lang.err_xcb_conn, 944 | error.GroupInitializationFailed => lang.err_user_init, 945 | error.SetUserGidFailed => lang.err_user_gid, 946 | error.SetUserUidFailed => lang.err_user_uid, 947 | error.ChangeDirectoryFailed => lang.err_perm_dir, 948 | error.TtyControlTransferFailed => lang.err_tty_ctrl, 949 | error.SetPathFailed => lang.err_path, 950 | error.PamAccountExpired => lang.err_pam_acct_expired, 951 | error.PamAuthError => lang.err_pam_auth, 952 | error.PamAuthInfoUnavailable => lang.err_pam_authinfo_unavail, 953 | error.PamBufferError => lang.err_pam_buf, 954 | error.PamCredentialsError => lang.err_pam_cred_err, 955 | error.PamCredentialsExpired => lang.err_pam_cred_expired, 956 | error.PamCredentialsInsufficient => lang.err_pam_cred_insufficient, 957 | error.PamCredentialsUnavailable => lang.err_pam_cred_unavail, 958 | error.PamMaximumTries => lang.err_pam_maxtries, 959 | error.PamNewAuthTokenRequired => lang.err_pam_authok_reqd, 960 | error.PamPermissionDenied => lang.err_pam_perm_denied, 961 | error.PamSessionError => lang.err_pam_session, 962 | error.PamSystemError => lang.err_pam_sys, 963 | error.PamUserUnknown => lang.err_pam_user_unknown, 964 | error.PamAbort => lang.err_pam_abort, 965 | else => @errorName(err), 966 | }; 967 | } 968 | -------------------------------------------------------------------------------- /src/tui/Animation.zig: -------------------------------------------------------------------------------- 1 | const Animation = @This(); 2 | 3 | const VTable = struct { 4 | deinit_fn: *const fn (ptr: *anyopaque) void, 5 | realloc_fn: *const fn (ptr: *anyopaque) anyerror!void, 6 | draw_fn: *const fn (ptr: *anyopaque) void, 7 | }; 8 | 9 | pointer: *anyopaque, 10 | vtable: VTable, 11 | 12 | pub fn init( 13 | pointer: anytype, 14 | comptime deinit_fn: fn (ptr: @TypeOf(pointer)) void, 15 | comptime realloc_fn: fn (ptr: @TypeOf(pointer)) anyerror!void, 16 | comptime draw_fn: fn (ptr: @TypeOf(pointer)) void, 17 | ) Animation { 18 | const Pointer = @TypeOf(pointer); 19 | const Impl = struct { 20 | pub fn deinitImpl(ptr: *anyopaque) void { 21 | const impl: Pointer = @ptrCast(@alignCast(ptr)); 22 | return @call(.always_inline, deinit_fn, .{impl}); 23 | } 24 | 25 | pub fn reallocImpl(ptr: *anyopaque) anyerror!void { 26 | const impl: Pointer = @ptrCast(@alignCast(ptr)); 27 | return @call(.always_inline, realloc_fn, .{impl}); 28 | } 29 | 30 | pub fn drawImpl(ptr: *anyopaque) void { 31 | const impl: Pointer = @ptrCast(@alignCast(ptr)); 32 | return @call(.always_inline, draw_fn, .{impl}); 33 | } 34 | 35 | const vtable = VTable{ 36 | .deinit_fn = deinitImpl, 37 | .realloc_fn = reallocImpl, 38 | .draw_fn = drawImpl, 39 | }; 40 | }; 41 | 42 | return .{ 43 | .pointer = pointer, 44 | .vtable = Impl.vtable, 45 | }; 46 | } 47 | 48 | pub fn deinit(self: *Animation) void { 49 | const impl: @TypeOf(self.pointer) = @ptrCast(@alignCast(self.pointer)); 50 | return @call(.auto, self.vtable.deinit_fn, .{impl}); 51 | } 52 | 53 | pub fn realloc(self: *Animation) anyerror!void { 54 | const impl: @TypeOf(self.pointer) = @ptrCast(@alignCast(self.pointer)); 55 | return @call(.auto, self.vtable.realloc_fn, .{impl}); 56 | } 57 | 58 | pub fn draw(self: *Animation) void { 59 | const impl: @TypeOf(self.pointer) = @ptrCast(@alignCast(self.pointer)); 60 | return @call(.auto, self.vtable.draw_fn, .{impl}); 61 | } 62 | -------------------------------------------------------------------------------- /src/tui/Cell.zig: -------------------------------------------------------------------------------- 1 | const interop = @import("../interop.zig"); 2 | 3 | const termbox = interop.termbox; 4 | 5 | const Cell = @This(); 6 | 7 | ch: u32, 8 | fg: u32, 9 | bg: u32, 10 | 11 | pub fn init(ch: u32, fg: u32, bg: u32) Cell { 12 | return .{ 13 | .ch = ch, 14 | .fg = fg, 15 | .bg = bg, 16 | }; 17 | } 18 | 19 | pub fn put(self: Cell, x: usize, y: usize) void { 20 | if (self.ch == 0) return; 21 | 22 | _ = termbox.tb_set_cell(@intCast(x), @intCast(y), self.ch, self.fg, self.bg); 23 | } 24 | -------------------------------------------------------------------------------- /src/tui/TerminalBuffer.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const builtin = @import("builtin"); 3 | const interop = @import("../interop.zig"); 4 | const Cell = @import("Cell.zig"); 5 | 6 | const Random = std.Random; 7 | 8 | const termbox = interop.termbox; 9 | 10 | const TerminalBuffer = @This(); 11 | 12 | pub const InitOptions = struct { 13 | fg: u32, 14 | bg: u32, 15 | border_fg: u32, 16 | margin_box_h: u8, 17 | margin_box_v: u8, 18 | input_len: u8, 19 | }; 20 | 21 | pub const Styling = struct { 22 | pub const BOLD = termbox.TB_BOLD; 23 | pub const UNDERLINE = termbox.TB_UNDERLINE; 24 | pub const REVERSE = termbox.TB_REVERSE; 25 | pub const ITALIC = termbox.TB_ITALIC; 26 | pub const BLINK = termbox.TB_BLINK; 27 | pub const HI_BLACK = termbox.TB_HI_BLACK; 28 | pub const BRIGHT = termbox.TB_BRIGHT; 29 | pub const DIM = termbox.TB_DIM; 30 | }; 31 | 32 | pub const Color = struct { 33 | pub const DEFAULT = 0x00000000; 34 | pub const BLACK = Styling.HI_BLACK; 35 | pub const RED = 0x00FF0000; 36 | pub const GREEN = 0x0000FF00; 37 | pub const YELLOW = 0x00FFFF00; 38 | pub const BLUE = 0x000000FF; 39 | pub const MAGENTA = 0x00FF00FF; 40 | pub const CYAN = 0x0000FFFF; 41 | pub const WHITE = 0x00FFFFFF; 42 | }; 43 | 44 | random: Random, 45 | width: usize, 46 | height: usize, 47 | fg: u32, 48 | bg: u32, 49 | border_fg: u32, 50 | box_chars: struct { 51 | left_up: u32, 52 | left_down: u32, 53 | right_up: u32, 54 | right_down: u32, 55 | top: u32, 56 | bottom: u32, 57 | left: u32, 58 | right: u32, 59 | }, 60 | labels_max_length: usize, 61 | box_x: usize, 62 | box_y: usize, 63 | box_width: usize, 64 | box_height: usize, 65 | margin_box_v: u8, 66 | margin_box_h: u8, 67 | blank_cell: Cell, 68 | 69 | pub fn init(options: InitOptions, labels_max_length: usize, random: Random) TerminalBuffer { 70 | return .{ 71 | .random = random, 72 | .width = @intCast(termbox.tb_width()), 73 | .height = @intCast(termbox.tb_height()), 74 | .fg = options.fg, 75 | .bg = options.bg, 76 | .border_fg = options.border_fg, 77 | .box_chars = if (builtin.os.tag == .linux or builtin.os.tag.isBSD()) .{ 78 | .left_up = 0x250C, 79 | .left_down = 0x2514, 80 | .right_up = 0x2510, 81 | .right_down = 0x2518, 82 | .top = 0x2500, 83 | .bottom = 0x2500, 84 | .left = 0x2502, 85 | .right = 0x2502, 86 | } else .{ 87 | .left_up = '+', 88 | .left_down = '+', 89 | .right_up = '+', 90 | .right_down = '+', 91 | .top = '-', 92 | .bottom = '-', 93 | .left = '|', 94 | .right = '|', 95 | }, 96 | .labels_max_length = labels_max_length, 97 | .box_x = 0, 98 | .box_y = 0, 99 | .box_width = (2 * options.margin_box_h) + options.input_len + 1 + labels_max_length, 100 | .box_height = 7 + (2 * options.margin_box_v), 101 | .margin_box_v = options.margin_box_v, 102 | .margin_box_h = options.margin_box_h, 103 | .blank_cell = Cell.init(' ', options.fg, options.bg), 104 | }; 105 | } 106 | 107 | pub fn cascade(self: TerminalBuffer) bool { 108 | var changed = false; 109 | var y = self.height - 2; 110 | 111 | while (y > 0) : (y -= 1) { 112 | for (0..self.width) |x| { 113 | var cell: termbox.tb_cell = undefined; 114 | var cell_under: termbox.tb_cell = undefined; 115 | 116 | _ = termbox.tb_get_cell(@intCast(x), @intCast(y - 1), 1, &cell); 117 | _ = termbox.tb_get_cell(@intCast(x), @intCast(y), 1, &cell_under); 118 | 119 | const char: u8 = @truncate(cell.ch); 120 | if (std.ascii.isWhitespace(char)) continue; 121 | 122 | const char_under: u8 = @truncate(cell_under.ch); 123 | if (!std.ascii.isWhitespace(char_under)) continue; 124 | 125 | changed = true; 126 | 127 | if ((self.random.int(u16) % 10) > 7) continue; 128 | 129 | _ = termbox.tb_set_cell(@intCast(x), @intCast(y), cell.ch, cell.fg, cell.bg); 130 | _ = termbox.tb_set_cell(@intCast(x), @intCast(y - 1), ' ', cell_under.fg, cell_under.bg); 131 | } 132 | } 133 | 134 | return changed; 135 | } 136 | 137 | pub fn drawBoxCenter(self: *TerminalBuffer, show_borders: bool, blank_box: bool) void { 138 | if (self.width < 2 or self.height < 2) return; 139 | const x1 = (self.width - @min(self.width - 2, self.box_width)) / 2; 140 | const y1 = (self.height - @min(self.height - 2, self.box_height)) / 2; 141 | const x2 = (self.width + @min(self.width, self.box_width)) / 2; 142 | const y2 = (self.height + @min(self.height, self.box_height)) / 2; 143 | 144 | self.box_x = x1; 145 | self.box_y = y1; 146 | 147 | if (show_borders) { 148 | _ = termbox.tb_set_cell(@intCast(x1 - 1), @intCast(y1 - 1), self.box_chars.left_up, self.border_fg, self.bg); 149 | _ = termbox.tb_set_cell(@intCast(x2), @intCast(y1 - 1), self.box_chars.right_up, self.border_fg, self.bg); 150 | _ = termbox.tb_set_cell(@intCast(x1 - 1), @intCast(y2), self.box_chars.left_down, self.border_fg, self.bg); 151 | _ = termbox.tb_set_cell(@intCast(x2), @intCast(y2), self.box_chars.right_down, self.border_fg, self.bg); 152 | 153 | var c1 = Cell.init(self.box_chars.top, self.border_fg, self.bg); 154 | var c2 = Cell.init(self.box_chars.bottom, self.border_fg, self.bg); 155 | 156 | for (0..self.box_width) |i| { 157 | c1.put(x1 + i, y1 - 1); 158 | c2.put(x1 + i, y2); 159 | } 160 | 161 | c1.ch = self.box_chars.left; 162 | c2.ch = self.box_chars.right; 163 | 164 | for (0..self.box_height) |i| { 165 | c1.put(x1 - 1, y1 + i); 166 | c2.put(x2, y1 + i); 167 | } 168 | } 169 | 170 | if (blank_box) { 171 | for (0..self.box_height) |y| { 172 | for (0..self.box_width) |x| { 173 | self.blank_cell.put(x1 + x, y1 + y); 174 | } 175 | } 176 | } 177 | } 178 | 179 | pub fn calculateComponentCoordinates(self: TerminalBuffer) struct { 180 | start_x: usize, 181 | x: usize, 182 | y: usize, 183 | full_visible_length: usize, 184 | visible_length: usize, 185 | } { 186 | const start_x = self.box_x + self.margin_box_h; 187 | const x = start_x + self.labels_max_length + 1; 188 | const y = self.box_y + self.margin_box_v; 189 | const full_visible_length = self.box_x + self.box_width - self.margin_box_h - start_x; 190 | const visible_length = self.box_x + self.box_width - self.margin_box_h - x; 191 | 192 | return .{ 193 | .start_x = start_x, 194 | .x = x, 195 | .y = y, 196 | .full_visible_length = full_visible_length, 197 | .visible_length = visible_length, 198 | }; 199 | } 200 | 201 | pub fn drawLabel(self: TerminalBuffer, text: []const u8, x: usize, y: usize) void { 202 | drawColorLabel(text, x, y, self.fg, self.bg); 203 | } 204 | 205 | pub fn drawColorLabel(text: []const u8, x: usize, y: usize, fg: u32, bg: u32) void { 206 | const yc: c_int = @intCast(y); 207 | const utf8view = std.unicode.Utf8View.init(text) catch return; 208 | var utf8 = utf8view.iterator(); 209 | 210 | var i = x; 211 | while (utf8.nextCodepoint()) |codepoint| : (i += 1) { 212 | _ = termbox.tb_set_cell(@intCast(i), yc, codepoint, fg, bg); 213 | } 214 | } 215 | 216 | pub fn drawConfinedLabel(self: TerminalBuffer, text: []const u8, x: usize, y: usize, max_length: usize) void { 217 | const yc: c_int = @intCast(y); 218 | const utf8view = std.unicode.Utf8View.init(text) catch return; 219 | var utf8 = utf8view.iterator(); 220 | 221 | var i: usize = 0; 222 | while (utf8.nextCodepoint()) |codepoint| : (i += 1) { 223 | if (i >= max_length) break; 224 | _ = termbox.tb_set_cell(@intCast(i + x), yc, codepoint, self.fg, self.bg); 225 | } 226 | } 227 | 228 | pub fn drawCharMultiple(self: TerminalBuffer, char: u32, x: usize, y: usize, length: usize) void { 229 | const cell = Cell.init(char, self.fg, self.bg); 230 | for (0..length) |xx| cell.put(x + xx, y); 231 | } 232 | 233 | // Every codepoint is assumed to have a width of 1. 234 | // Since Ly is normally running in a TTY, this should be fine. 235 | pub fn strWidth(str: []const u8) !u8 { 236 | const utf8view = try std.unicode.Utf8View.init(str); 237 | var utf8 = utf8view.iterator(); 238 | var i: u8 = 0; 239 | while (utf8.nextCodepoint()) |_| i += 1; 240 | return i; 241 | } 242 | -------------------------------------------------------------------------------- /src/tui/components/InfoLine.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const TerminalBuffer = @import("../TerminalBuffer.zig"); 3 | const generic = @import("generic.zig"); 4 | 5 | const Allocator = std.mem.Allocator; 6 | 7 | const MessageLabel = generic.CyclableLabel(Message); 8 | 9 | const InfoLine = @This(); 10 | 11 | const Message = struct { 12 | width: u8, 13 | text: []const u8, 14 | bg: u32, 15 | fg: u32, 16 | }; 17 | 18 | label: MessageLabel, 19 | 20 | pub fn init(allocator: Allocator, buffer: *TerminalBuffer) InfoLine { 21 | return .{ 22 | .label = MessageLabel.init(allocator, buffer, drawItem), 23 | }; 24 | } 25 | 26 | pub fn deinit(self: *InfoLine) void { 27 | self.label.deinit(); 28 | } 29 | 30 | pub fn addMessage(self: *InfoLine, text: []const u8, bg: u32, fg: u32) !void { 31 | if (text.len == 0) return; 32 | 33 | try self.label.addItem(.{ 34 | .width = try TerminalBuffer.strWidth(text), 35 | .text = text, 36 | .bg = bg, 37 | .fg = fg, 38 | }); 39 | } 40 | 41 | pub fn clearRendered(allocator: Allocator, buffer: TerminalBuffer) !void { 42 | // Draw over the area 43 | const y = buffer.box_y + buffer.margin_box_v; 44 | const spaces = try allocator.alloc(u8, buffer.box_width); 45 | defer allocator.free(spaces); 46 | 47 | @memset(spaces, ' '); 48 | 49 | buffer.drawLabel(spaces, buffer.box_x, y); 50 | } 51 | 52 | fn drawItem(label: *MessageLabel, message: Message, _: usize, _: usize) bool { 53 | if (message.width == 0 or label.buffer.box_width <= message.width) return false; 54 | 55 | const x = label.buffer.box_x + ((label.buffer.box_width - message.width) / 2); 56 | label.first_char_x = x + message.width; 57 | 58 | TerminalBuffer.drawColorLabel(message.text, x, label.y, message.fg, message.bg); 59 | return true; 60 | } 61 | -------------------------------------------------------------------------------- /src/tui/components/Session.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const TerminalBuffer = @import("../TerminalBuffer.zig"); 3 | const enums = @import("../../enums.zig"); 4 | const ini = @import("zigini"); 5 | const Environment = @import("../../Environment.zig"); 6 | const generic = @import("generic.zig"); 7 | 8 | const Allocator = std.mem.Allocator; 9 | const DisplayServer = enums.DisplayServer; 10 | const Ini = ini.Ini; 11 | const EnvironmentLabel = generic.CyclableLabel(Environment); 12 | 13 | const Session = @This(); 14 | 15 | label: EnvironmentLabel, 16 | 17 | pub fn init(allocator: Allocator, buffer: *TerminalBuffer) Session { 18 | return .{ 19 | .label = EnvironmentLabel.init(allocator, buffer, drawItem), 20 | }; 21 | } 22 | 23 | pub fn deinit(self: *Session) void { 24 | for (self.label.list.items) |*environment| { 25 | if (environment.entry_ini) |*entry_ini| entry_ini.deinit(); 26 | if (environment.xdg_session_desktop) |session_desktop| self.label.allocator.free(session_desktop); 27 | } 28 | 29 | self.label.deinit(); 30 | } 31 | 32 | pub fn addEnvironment(self: *Session, environment: Environment) !void { 33 | try self.label.addItem(environment); 34 | } 35 | 36 | fn drawItem(label: *EnvironmentLabel, environment: Environment, x: usize, y: usize) bool { 37 | const length = @min(environment.name.len, label.visible_length - 3); 38 | if (length == 0) return false; 39 | 40 | const nx = if (label.text_in_center) (label.x + (label.visible_length - environment.name.len) / 2) else (label.x + 2); 41 | label.first_char_x = nx + environment.name.len; 42 | 43 | label.buffer.drawLabel(environment.specifier, x, y); 44 | label.buffer.drawLabel(environment.name, nx, label.y); 45 | return true; 46 | } 47 | -------------------------------------------------------------------------------- /src/tui/components/Text.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const interop = @import("../../interop.zig"); 3 | const TerminalBuffer = @import("../TerminalBuffer.zig"); 4 | 5 | const Allocator = std.mem.Allocator; 6 | const DynamicString = std.ArrayListUnmanaged(u8); 7 | 8 | const termbox = interop.termbox; 9 | 10 | const Text = @This(); 11 | 12 | allocator: Allocator, 13 | buffer: *TerminalBuffer, 14 | text: DynamicString, 15 | end: usize, 16 | cursor: usize, 17 | visible_start: usize, 18 | visible_length: usize, 19 | x: usize, 20 | y: usize, 21 | masked: bool, 22 | maybe_mask: ?u32, 23 | 24 | pub fn init(allocator: Allocator, buffer: *TerminalBuffer, masked: bool, maybe_mask: ?u32) Text { 25 | const text: DynamicString = .empty; 26 | 27 | return .{ 28 | .allocator = allocator, 29 | .buffer = buffer, 30 | .text = text, 31 | .end = 0, 32 | .cursor = 0, 33 | .visible_start = 0, 34 | .visible_length = 0, 35 | .x = 0, 36 | .y = 0, 37 | .masked = masked, 38 | .maybe_mask = maybe_mask, 39 | }; 40 | } 41 | 42 | pub fn deinit(self: *Text) void { 43 | self.text.deinit(self.allocator); 44 | } 45 | 46 | pub fn position(self: *Text, x: usize, y: usize, visible_length: usize) void { 47 | self.x = x; 48 | self.y = y; 49 | self.visible_length = visible_length; 50 | } 51 | 52 | pub fn handle(self: *Text, maybe_event: ?*termbox.tb_event, insert_mode: bool) !void { 53 | if (maybe_event) |event| blk: { 54 | if (event.type != termbox.TB_EVENT_KEY) break :blk; 55 | 56 | switch (event.key) { 57 | termbox.TB_KEY_ARROW_LEFT => self.goLeft(), 58 | termbox.TB_KEY_ARROW_RIGHT => self.goRight(), 59 | termbox.TB_KEY_DELETE => self.delete(), 60 | termbox.TB_KEY_BACKSPACE, termbox.TB_KEY_BACKSPACE2 => { 61 | if (insert_mode) { 62 | self.backspace(); 63 | } else { 64 | self.goLeft(); 65 | } 66 | }, 67 | termbox.TB_KEY_SPACE => try self.write(' '), 68 | else => { 69 | if (event.ch > 31 and event.ch < 127) { 70 | if (insert_mode) { 71 | try self.write(@intCast(event.ch)); 72 | } else { 73 | switch (event.ch) { 74 | 'h' => self.goLeft(), 75 | 'l' => self.goRight(), 76 | else => {}, 77 | } 78 | } 79 | } 80 | }, 81 | } 82 | } 83 | 84 | if (self.masked and self.maybe_mask == null) { 85 | _ = termbox.tb_set_cursor(@intCast(self.x), @intCast(self.y)); 86 | return; 87 | } 88 | 89 | _ = termbox.tb_set_cursor(@intCast(self.x + (self.cursor - self.visible_start)), @intCast(self.y)); 90 | } 91 | 92 | pub fn draw(self: Text) void { 93 | if (self.masked) { 94 | if (self.maybe_mask) |mask| { 95 | const length = @min(self.text.items.len, self.visible_length - 1); 96 | if (length == 0) return; 97 | 98 | self.buffer.drawCharMultiple(mask, self.x, self.y, length); 99 | } 100 | return; 101 | } 102 | 103 | const length = @min(self.text.items.len, self.visible_length); 104 | if (length == 0) return; 105 | 106 | const visible_slice = vs: { 107 | if (self.text.items.len > self.visible_length and self.cursor < self.text.items.len) { 108 | break :vs self.text.items[self.visible_start..(self.visible_length + self.visible_start)]; 109 | } else { 110 | break :vs self.text.items[self.visible_start..]; 111 | } 112 | }; 113 | 114 | self.buffer.drawLabel(visible_slice, self.x, self.y); 115 | } 116 | 117 | pub fn clear(self: *Text) void { 118 | self.text.clearRetainingCapacity(); 119 | self.end = 0; 120 | self.cursor = 0; 121 | self.visible_start = 0; 122 | } 123 | 124 | fn goLeft(self: *Text) void { 125 | if (self.cursor == 0) return; 126 | if (self.visible_start > 0) self.visible_start -= 1; 127 | 128 | self.cursor -= 1; 129 | } 130 | 131 | fn goRight(self: *Text) void { 132 | if (self.cursor >= self.end) return; 133 | if (self.cursor - self.visible_start == self.visible_length - 1) self.visible_start += 1; 134 | 135 | self.cursor += 1; 136 | } 137 | 138 | fn delete(self: *Text) void { 139 | if (self.cursor >= self.end) return; 140 | 141 | _ = self.text.orderedRemove(self.cursor); 142 | 143 | self.end -= 1; 144 | } 145 | 146 | fn backspace(self: *Text) void { 147 | if (self.cursor == 0) return; 148 | 149 | self.goLeft(); 150 | self.delete(); 151 | } 152 | 153 | fn write(self: *Text, char: u8) !void { 154 | if (char == 0) return; 155 | 156 | try self.text.insert(self.allocator, self.cursor, char); 157 | 158 | self.end += 1; 159 | self.goRight(); 160 | } 161 | -------------------------------------------------------------------------------- /src/tui/components/generic.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const interop = @import("../../interop.zig"); 3 | const TerminalBuffer = @import("../TerminalBuffer.zig"); 4 | 5 | pub fn CyclableLabel(comptime ItemType: type) type { 6 | return struct { 7 | const Allocator = std.mem.Allocator; 8 | const ItemList = std.ArrayListUnmanaged(ItemType); 9 | const DrawItemFn = *const fn (*Self, ItemType, usize, usize) bool; 10 | 11 | const termbox = interop.termbox; 12 | 13 | const Self = @This(); 14 | 15 | allocator: Allocator, 16 | buffer: *TerminalBuffer, 17 | list: ItemList, 18 | current: usize, 19 | visible_length: usize, 20 | x: usize, 21 | y: usize, 22 | first_char_x: usize, 23 | text_in_center: bool, 24 | draw_item_fn: DrawItemFn, 25 | 26 | pub fn init(allocator: Allocator, buffer: *TerminalBuffer, draw_item_fn: DrawItemFn) Self { 27 | return .{ 28 | .allocator = allocator, 29 | .buffer = buffer, 30 | .list = .empty, 31 | .current = 0, 32 | .visible_length = 0, 33 | .x = 0, 34 | .y = 0, 35 | .first_char_x = 0, 36 | .text_in_center = false, 37 | .draw_item_fn = draw_item_fn, 38 | }; 39 | } 40 | 41 | pub fn deinit(self: *Self) void { 42 | self.list.deinit(self.allocator); 43 | } 44 | 45 | pub fn position(self: *Self, x: usize, y: usize, visible_length: usize, text_in_center: ?bool) void { 46 | self.x = x; 47 | self.y = y; 48 | self.visible_length = visible_length; 49 | self.first_char_x = x + 2; 50 | if (text_in_center) |value| { 51 | self.text_in_center = value; 52 | } 53 | } 54 | 55 | pub fn addItem(self: *Self, item: ItemType) !void { 56 | try self.list.append(self.allocator, item); 57 | self.current = self.list.items.len - 1; 58 | } 59 | 60 | pub fn handle(self: *Self, maybe_event: ?*termbox.tb_event, insert_mode: bool) void { 61 | if (maybe_event) |event| blk: { 62 | if (event.type != termbox.TB_EVENT_KEY) break :blk; 63 | 64 | switch (event.key) { 65 | termbox.TB_KEY_ARROW_LEFT, termbox.TB_KEY_CTRL_H => self.goLeft(), 66 | termbox.TB_KEY_ARROW_RIGHT, termbox.TB_KEY_CTRL_L => self.goRight(), 67 | else => { 68 | if (!insert_mode) { 69 | switch (event.ch) { 70 | 'h' => self.goLeft(), 71 | 'l' => self.goRight(), 72 | else => {}, 73 | } 74 | } 75 | }, 76 | } 77 | } 78 | 79 | _ = termbox.tb_set_cursor(@intCast(self.first_char_x), @intCast(self.y)); 80 | } 81 | 82 | pub fn draw(self: *Self) void { 83 | if (self.list.items.len == 0) return; 84 | 85 | const current_item = self.list.items[self.current]; 86 | const x = self.buffer.box_x + self.buffer.margin_box_h; 87 | const y = self.buffer.box_y + self.buffer.margin_box_v + 2; 88 | 89 | const continue_drawing = @call(.auto, self.draw_item_fn, .{ self, current_item, x, y }); 90 | if (!continue_drawing) return; 91 | 92 | _ = termbox.tb_set_cell(@intCast(self.x), @intCast(self.y), '<', self.buffer.fg, self.buffer.bg); 93 | _ = termbox.tb_set_cell(@intCast(self.x + self.visible_length - 1), @intCast(self.y), '>', self.buffer.fg, self.buffer.bg); 94 | } 95 | 96 | fn goLeft(self: *Self) void { 97 | if (self.current == 0) { 98 | self.current = self.list.items.len - 1; 99 | return; 100 | } 101 | 102 | self.current -= 1; 103 | } 104 | 105 | fn goRight(self: *Self) void { 106 | if (self.current == self.list.items.len - 1) { 107 | self.current = 0; 108 | return; 109 | } 110 | 111 | self.current += 1; 112 | } 113 | }; 114 | } 115 | --------------------------------------------------------------------------------