8 Commits

Author SHA1 Message Date
AnErrupTion
edbb982c91 [Backport] Recursively create xauth file directory if non-existent
Signed-off-by: AnErrupTion <anerruption@disroot.org>
2025-12-30 20:55:41 +01:00
AnErrupTion
80fb07daa8 [Backport] Fix invalid XDG_RUNTIME_DIR if D-Bus isn't used
Signed-off-by: AnErrupTion <anerruption@disroot.org>
2025-12-30 20:54:47 +01:00
AnErrupTion
93388159bc [Backport] Clamp user session index if invalid
Signed-off-by: AnErrupTion <anerruption@disroot.org>
2025-12-30 18:12:28 +01:00
AnErrupTion
08b4a49729 [Backport] Check for session file name in autologin
Signed-off-by: AnErrupTion <anerruption@disroot.org>
2025-12-30 17:45:29 +01:00
AnErrupTion
066acf91e6 [Backport] Create session log directory if non-existent
Signed-off-by: AnErrupTion <anerruption@disroot.org>
2025-12-30 17:29:31 +01:00
AnErrupTion
2660486fde [Backport] Escape TTY in systemd service
Signed-off-by: AnErrupTion <anerruption@disroot.org>
2025-12-30 17:28:50 +01:00
AnErrupTion
a3b249ded8 [Backport] Fix session not being saved correctly
Signed-off-by: AnErrupTion <anerruption@disroot.org>
2025-12-30 17:27:04 +01:00
AnErrupTion
3c22597054 Start Ly v1.3.1 development cycle
Signed-off-by: AnErrupTion <anerruption@disroot.org>
2025-12-30 17:26:53 +01:00
88 changed files with 2660 additions and 6952 deletions

2
.github/FUNDING.yml vendored
View File

@@ -1,2 +0,0 @@
github: AnErrupTion
liberapay: ShiningLea

View File

@@ -12,8 +12,8 @@ body:
- label: I have looked for any other duplicate issues - label: I have looked for any other duplicate issues
required: true required: true
- label: I have reproduced the issue on a fresh install of my OS & Ly with default settings, except ones I will mention - label: I have reproduced the issue on a fresh install of my OS & Ly with default settings, except ones I will mention
required: false required: true
- label: I have confirmed this issue also occurs on the latest development version (found in the `master` branch) - label: I have confirmed this issue also occurs on the latest development version
required: true required: true
- type: input - type: input
id: version id: version

View File

@@ -9,4 +9,3 @@ _Replace this with a reference to an existing issue, or N/A if there is none_
## Pre-requisites ## Pre-requisites
- [ ] I have tested & confirmed the changes work locally - [ ] I have tested & confirmed the changes work locally
- [ ] I have run `zig fmt` throughout my changes

BIN
.github/screenshot.png vendored

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

After

Width:  |  Height:  |  Size: 34 KiB

1
.gitignore vendored
View File

@@ -3,4 +3,3 @@ zig-cache/
zig-out/ zig-out/
valgrind.log valgrind.log
.zig-cache .zig-cache
zig-pkg

293
build.zig
View File

@@ -12,7 +12,7 @@ const InitSystem = enum {
freebsd, freebsd,
}; };
const min_zig_string = "0.16.0"; const min_zig_string = "0.15.0";
const current_zig = builtin.zig_version; const current_zig = builtin.zig_version;
// Implementing zig version detection through compile time // Implementing zig version detection through compile time
@@ -23,7 +23,7 @@ comptime {
} }
} }
const ly_version = std.SemanticVersion{ .major = 1, .minor = 4, .patch = 0 }; const ly_version = std.SemanticVersion{ .major = 1, .minor = 3, .patch = 1 };
var dest_directory: []const u8 = undefined; var dest_directory: []const u8 = undefined;
var config_directory: []const u8 = undefined; var config_directory: []const u8 = undefined;
@@ -67,21 +67,37 @@ pub fn build(b: *std.Build) !void {
.root_source_file = b.path("src/main.zig"), .root_source_file = b.path("src/main.zig"),
.target = target, .target = target,
.optimize = optimize, .optimize = optimize,
.link_libc = true,
}), }),
// Here until the native backend matures in terms of performance
.use_llvm = true, .use_llvm = true,
}); });
const ly_ui = b.dependency("ly_ui", .{ .target = target, .optimize = optimize }); const zigini = b.dependency("zigini", .{ .target = target, .optimize = optimize });
exe.root_module.addImport("ly-ui", ly_ui.module("ly-ui")); exe.root_module.addImport("zigini", zigini.module("zigini"));
exe.root_module.addOptions("build_options", build_options); exe.root_module.addOptions("build_options", build_options);
const clap = b.dependency("clap", .{ .target = target, .optimize = optimize }); const clap = b.dependency("clap", .{ .target = target, .optimize = optimize });
exe.root_module.addImport("clap", clap.module("clap")); exe.root_module.addImport("clap", clap.module("clap"));
exe.root_module.linkSystemLibrary("pam", .{}); const termbox_dep = b.dependency("termbox2", .{
if (enable_x11_support) exe.root_module.linkSystemLibrary("xcb", .{}); .target = target,
.optimize = optimize,
});
exe.linkSystemLibrary("pam");
if (enable_x11_support) exe.linkSystemLibrary("xcb");
exe.linkLibC();
const translate_c = b.addTranslateC(.{
.root_source_file = termbox_dep.path("termbox2.h"),
.target = target,
.optimize = optimize,
});
translate_c.defineCMacroRaw("TB_IMPL");
translate_c.defineCMacro("TB_OPT_ATTR_W", "32"); // Enable 24-bit color support + styling (32-bit)
const termbox2 = translate_c.addModule("termbox2");
exe.root_module.addImport("termbox2", termbox2);
b.installArtifact(exe); b.installArtifact(exe);
@@ -112,8 +128,6 @@ pub fn build(b: *std.Build) !void {
pub fn Installer(install_config: bool) type { pub fn Installer(install_config: bool) type {
return struct { return struct {
pub fn make(step: *std.Build.Step, _: std.Build.Step.MakeOptions) !void { pub fn make(step: *std.Build.Step, _: std.Build.Step.MakeOptions) !void {
var threaded: std.Io.Threaded = .init_single_threaded;
const io = threaded.io();
const allocator = step.owner.allocator; const allocator = step.owner.allocator;
var patch_map = PatchMap.init(allocator); var patch_map = PatchMap.init(allocator);
@@ -128,75 +142,71 @@ pub fn Installer(install_config: bool) type {
// instead to shutdown the system. // instead to shutdown the system.
try patch_map.put("$PLATFORM_SHUTDOWN_ARG", if (init_system == .freebsd) "-p" else "-a"); try patch_map.put("$PLATFORM_SHUTDOWN_ARG", if (init_system == .freebsd) "-p" else "-a");
try install_ly(allocator, io, patch_map, install_config); try install_ly(allocator, patch_map, install_config);
try install_service(allocator, io, patch_map); try install_service(allocator, patch_map);
} }
}; };
} }
fn install_ly(allocator: std.mem.Allocator, io: std.Io, patch_map: PatchMap, install_config: bool) !void { fn install_ly(allocator: std.mem.Allocator, patch_map: PatchMap, install_config: bool) !void {
const ly_config_directory = try std.Io.Dir.path.join(allocator, &[_][]const u8{ dest_directory, config_directory, "/ly" }); const ly_config_directory = try std.fs.path.join(allocator, &[_][]const u8{ dest_directory, config_directory, "/ly" });
std.Io.Dir.cwd().createDirPath(io, ly_config_directory) catch { std.fs.cwd().makePath(ly_config_directory) catch {
std.debug.print("warn: {s} already exists as a directory.\n", .{ly_config_directory}); std.debug.print("warn: {s} already exists as a directory.\n", .{ly_config_directory});
}; };
const ly_custom_sessions_directory = try std.Io.Dir.path.join(allocator, &[_][]const u8{ dest_directory, config_directory, "/ly/custom-sessions" }); const ly_custom_sessions_directory = try std.fs.path.join(allocator, &[_][]const u8{ dest_directory, config_directory, "/ly/custom-sessions" });
std.Io.Dir.cwd().createDirPath(io, ly_custom_sessions_directory) catch { std.fs.cwd().makePath(ly_custom_sessions_directory) catch {
std.debug.print("warn: {s} already exists as a directory.\n", .{ly_custom_sessions_directory}); std.debug.print("warn: {s} already exists as a directory.\n", .{ly_custom_sessions_directory});
}; };
const ly_lang_path = try std.Io.Dir.path.join(allocator, &[_][]const u8{ dest_directory, config_directory, "/ly/lang" }); const ly_lang_path = try std.fs.path.join(allocator, &[_][]const u8{ dest_directory, config_directory, "/ly/lang" });
std.Io.Dir.cwd().createDirPath(io, ly_lang_path) catch { std.fs.cwd().makePath(ly_lang_path) catch {
std.debug.print("warn: {s} already exists as a directory.\n", .{ly_lang_path}); std.debug.print("warn: {s} already exists as a directory.\n", .{ly_lang_path});
}; };
{ {
const exe_path = try std.Io.Dir.path.join(allocator, &[_][]const u8{ dest_directory, prefix_directory, "/bin" }); const exe_path = try std.fs.path.join(allocator, &[_][]const u8{ dest_directory, prefix_directory, "/bin" });
std.Io.Dir.cwd().createDirPath(io, exe_path) catch { std.fs.cwd().makePath(exe_path) catch {
if (!std.mem.eql(u8, dest_directory, "")) { if (!std.mem.eql(u8, dest_directory, "")) {
std.debug.print("warn: {s} already exists as a directory.\n", .{exe_path}); std.debug.print("warn: {s} already exists as a directory.\n", .{exe_path});
} }
}; };
var executable_dir = std.Io.Dir.cwd().openDir(io, exe_path, .{}) catch unreachable; var executable_dir = std.fs.cwd().openDir(exe_path, .{}) catch unreachable;
defer executable_dir.close(io); defer executable_dir.close();
try installFile(io, "zig-out/bin/ly", executable_dir, exe_path, executable_name, .{}); try installFile("zig-out/bin/ly", executable_dir, exe_path, executable_name, .{});
} }
{ {
var config_dir = std.Io.Dir.cwd().openDir(io, ly_config_directory, .{}) catch unreachable; var config_dir = std.fs.cwd().openDir(ly_config_directory, .{}) catch unreachable;
defer config_dir.close(io); defer config_dir.close();
if (install_config) { if (install_config) {
const patched_config = try patchFile(allocator, io, "res/config.ini", patch_map); const patched_config = try patchFile(allocator, "res/config.ini", patch_map);
try installText(io, patched_config, config_dir, ly_config_directory, "config.ini", .{}); try installText(patched_config, config_dir, ly_config_directory, "config.ini", .{});
try installFile(io, "res/startup.sh", config_dir, ly_config_directory, "startup.sh", .{ .permissions = .fromMode(0o755) });
} }
const patched_example_config = try patchFile(allocator, io, "res/config.ini", patch_map); const patched_example_config = try patchFile(allocator, "res/config.ini", patch_map);
try installText(io, patched_example_config, config_dir, ly_config_directory, "config.ini.example", .{}); try installText(patched_example_config, config_dir, ly_config_directory, "config.ini.example", .{});
const patched_setup = try patchFile(allocator, io, "res/setup.sh", patch_map); const patched_setup = try patchFile(allocator, "res/setup.sh", patch_map);
try installText(io, patched_setup, config_dir, ly_config_directory, "setup.sh", .{ .permissions = .fromMode(0o755) }); try installText(patched_setup, config_dir, ly_config_directory, "setup.sh", .{ .mode = 0o755 });
try installFile(io, "res/example.dur", config_dir, ly_config_directory, "example.dur", .{ .permissions = .fromMode(0o755) });
} }
{ {
var custom_sessions_dir = std.Io.Dir.cwd().openDir(io, ly_custom_sessions_directory, .{}) catch unreachable; var custom_sessions_dir = std.fs.cwd().openDir(ly_custom_sessions_directory, .{}) catch unreachable;
defer custom_sessions_dir.close(io); defer custom_sessions_dir.close();
const patched_readme = try patchFile(allocator, io, "res/custom-sessions/README", patch_map); const patched_readme = try patchFile(allocator, "res/custom-sessions/README", patch_map);
try installText(io, patched_readme, custom_sessions_dir, ly_custom_sessions_directory, "README", .{}); try installText(patched_readme, custom_sessions_dir, ly_custom_sessions_directory, "README", .{});
} }
{ {
var lang_dir = std.Io.Dir.cwd().openDir(io, ly_lang_path, .{}) catch unreachable; var lang_dir = std.fs.cwd().openDir(ly_lang_path, .{}) catch unreachable;
defer lang_dir.close(io); defer lang_dir.close();
const languages = [_][]const u8{ const languages = [_][]const u8{
"ar.ini", "ar.ini",
@@ -222,66 +232,63 @@ fn install_ly(allocator: std.mem.Allocator, io: std.Io, patch_map: PatchMap, ins
}; };
inline for (languages) |language| { inline for (languages) |language| {
try installFile(io, "res/lang/" ++ language, lang_dir, ly_lang_path, language, .{}); try installFile("res/lang/" ++ language, lang_dir, ly_lang_path, language, .{});
} }
} }
{ {
const pam_path = try std.Io.Dir.path.join(allocator, &[_][]const u8{ dest_directory, config_directory, "/pam.d" }); const pam_path = try std.fs.path.join(allocator, &[_][]const u8{ dest_directory, config_directory, "/pam.d" });
std.Io.Dir.cwd().createDirPath(io, pam_path) catch { std.fs.cwd().makePath(pam_path) catch {
if (!std.mem.eql(u8, dest_directory, "")) { if (!std.mem.eql(u8, dest_directory, "")) {
std.debug.print("warn: {s} already exists as a directory.\n", .{pam_path}); std.debug.print("warn: {s} already exists as a directory.\n", .{pam_path});
} }
}; };
var pam_dir = std.Io.Dir.cwd().openDir(io, pam_path, .{}) catch unreachable; var pam_dir = std.fs.cwd().openDir(pam_path, .{}) catch unreachable;
defer pam_dir.close(io); defer pam_dir.close();
try installFile(io, if (init_system == .freebsd) "res/pam.d/ly-freebsd" else "res/pam.d/ly-linux", pam_dir, pam_path, "ly", .{ .permissions = .fromMode(0o644) }); try installFile(if (init_system == .freebsd) "res/pam.d/ly-freebsd" else "res/pam.d/ly-linux", pam_dir, pam_path, "ly", .{ .override_mode = 0o644 });
try installFile(io, if (init_system == .freebsd) "res/pam.d/ly-freebsd-autologin" else "res/pam.d/ly-linux-autologin", pam_dir, pam_path, "ly-autologin", .{ .permissions = .fromMode(0o644) }); try installFile(if (init_system == .freebsd) "res/pam.d/ly-freebsd-autologin" else "res/pam.d/ly-linux-autologin", pam_dir, pam_path, "ly-autologin", .{ .override_mode = 0o644 });
} }
} }
fn install_service(allocator: std.mem.Allocator, io: std.Io, patch_map: PatchMap) !void { fn install_service(allocator: std.mem.Allocator, patch_map: PatchMap) !void {
switch (init_system) { switch (init_system) {
.systemd => { .systemd => {
const service_path = try std.Io.Dir.path.join(allocator, &[_][]const u8{ dest_directory, prefix_directory, "/lib/systemd/system" }); const service_path = try std.fs.path.join(allocator, &[_][]const u8{ dest_directory, prefix_directory, "/lib/systemd/system" });
std.Io.Dir.cwd().createDirPath(io, service_path) catch {}; std.fs.cwd().makePath(service_path) catch {};
var service_dir = std.Io.Dir.cwd().openDir(io, service_path, .{}) catch unreachable; var service_dir = std.fs.cwd().openDir(service_path, .{}) catch unreachable;
defer service_dir.close(io); defer service_dir.close();
const patched_service = try patchFile(allocator, io, "res/ly@.service", patch_map); const patched_service = try patchFile(allocator, "res/ly@.service", patch_map);
try installText(io, patched_service, service_dir, service_path, "ly@.service", .{ .permissions = .fromMode(0o644) }); try installText(patched_service, service_dir, service_path, "ly@.service", .{ .mode = 0o644 });
const patched_kmsconvt_service = try patchFile(allocator, io, "res/ly-kmsconvt@.service", patch_map);
try installText(io, patched_kmsconvt_service, service_dir, service_path, "ly-kmsconvt@.service", .{ .permissions = .fromMode(0o644) });
}, },
.openrc => { .openrc => {
const service_path = try std.Io.Dir.path.join(allocator, &[_][]const u8{ dest_directory, config_directory, "/init.d" }); const service_path = try std.fs.path.join(allocator, &[_][]const u8{ dest_directory, config_directory, "/init.d" });
std.Io.Dir.cwd().createDirPath(io, service_path) catch {}; std.fs.cwd().makePath(service_path) catch {};
var service_dir = std.Io.Dir.cwd().openDir(io, service_path, .{}) catch unreachable; var service_dir = std.fs.cwd().openDir(service_path, .{}) catch unreachable;
defer service_dir.close(io); defer service_dir.close();
const patched_service = try patchFile(allocator, io, "res/ly-openrc", patch_map); const patched_service = try patchFile(allocator, "res/ly-openrc", patch_map);
try installText(io, patched_service, service_dir, service_path, executable_name, .{ .permissions = .fromMode(0o755) }); try installText(patched_service, service_dir, service_path, executable_name, .{ .mode = 0o755 });
}, },
.runit => { .runit => {
const service_path = try std.Io.Dir.path.join(allocator, &[_][]const u8{ dest_directory, config_directory, "/sv/ly" }); const service_path = try std.fs.path.join(allocator, &[_][]const u8{ dest_directory, config_directory, "/sv/ly" });
std.Io.Dir.cwd().createDirPath(io, service_path) catch {}; std.fs.cwd().makePath(service_path) catch {};
var service_dir = std.Io.Dir.cwd().openDir(io, service_path, .{}) catch unreachable; var service_dir = std.fs.cwd().openDir(service_path, .{}) catch unreachable;
defer service_dir.close(io); defer service_dir.close();
const supervise_path = try std.Io.Dir.path.join(allocator, &[_][]const u8{ service_path, "supervise" }); const supervise_path = try std.fs.path.join(allocator, &[_][]const u8{ service_path, "supervise" });
const patched_conf = try patchFile(allocator, io, "res/ly-runit-service/conf", patch_map); const patched_conf = try patchFile(allocator, "res/ly-runit-service/conf", patch_map);
try installText(io, patched_conf, service_dir, service_path, "conf", .{}); try installText(patched_conf, service_dir, service_path, "conf", .{});
try installFile(io, "res/ly-runit-service/finish", service_dir, service_path, "finish", .{ .permissions = .fromMode(0o755) }); try installFile("res/ly-runit-service/finish", service_dir, service_path, "finish", .{ .override_mode = 0o755 });
const patched_run = try patchFile(allocator, io, "res/ly-runit-service/run", patch_map); const patched_run = try patchFile(allocator, "res/ly-runit-service/run", patch_map);
try installText(io, patched_run, service_dir, service_path, "run", .{ .permissions = .fromMode(0o755) }); try installText(patched_run, service_dir, service_path, "run", .{ .mode = 0o755 });
std.Io.Dir.cwd().symLink(io, "/run/runit/supervise.ly", supervise_path, .{}) catch |err| { std.fs.cwd().symLink("/run/runit/supervise.ly", supervise_path, .{}) catch |err| {
if (err == error.PathAlreadyExists) { if (err == error.PathAlreadyExists) {
std.debug.print("warn: /run/runit/supervise.ly already exists as a symbolic link.\n", .{}); std.debug.print("warn: /run/runit/supervise.ly already exists as a symbolic link.\n", .{});
} else { } else {
@@ -291,49 +298,49 @@ fn install_service(allocator: std.mem.Allocator, io: std.Io, patch_map: PatchMap
std.debug.print("info: installed symlink /run/runit/supervise.ly\n", .{}); std.debug.print("info: installed symlink /run/runit/supervise.ly\n", .{});
}, },
.s6 => { .s6 => {
const admin_service_path = try std.Io.Dir.path.join(allocator, &[_][]const u8{ dest_directory, config_directory, "/s6/adminsv/default/contents.d" }); const admin_service_path = try std.fs.path.join(allocator, &[_][]const u8{ dest_directory, config_directory, "/s6/adminsv/default/contents.d" });
std.Io.Dir.cwd().createDirPath(io, admin_service_path) catch {}; std.fs.cwd().makePath(admin_service_path) catch {};
var admin_service_dir = std.Io.Dir.cwd().openDir(io, admin_service_path, .{}) catch unreachable; var admin_service_dir = std.fs.cwd().openDir(admin_service_path, .{}) catch unreachable;
defer admin_service_dir.close(io); defer admin_service_dir.close();
const file = try admin_service_dir.createFile(io, "ly-srv", .{}); const file = try admin_service_dir.createFile("ly-srv", .{});
file.close(io); file.close();
const service_path = try std.Io.Dir.path.join(allocator, &[_][]const u8{ dest_directory, config_directory, "/s6/sv/ly-srv" }); const service_path = try std.fs.path.join(allocator, &[_][]const u8{ dest_directory, config_directory, "/s6/sv/ly-srv" });
std.Io.Dir.cwd().createDirPath(io, service_path) catch {}; std.fs.cwd().makePath(service_path) catch {};
var service_dir = std.Io.Dir.cwd().openDir(io, service_path, .{}) catch unreachable; var service_dir = std.fs.cwd().openDir(service_path, .{}) catch unreachable;
defer service_dir.close(io); defer service_dir.close();
const patched_run = try patchFile(allocator, io, "res/ly-s6/run", patch_map); const patched_run = try patchFile(allocator, "res/ly-s6/run", patch_map);
try installText(io, patched_run, service_dir, service_path, "run", .{ .permissions = .fromMode(0o755) }); try installText(patched_run, service_dir, service_path, "run", .{ .mode = 0o755 });
try installFile(io, "res/ly-s6/type", service_dir, service_path, "type", .{}); try installFile("res/ly-s6/type", service_dir, service_path, "type", .{});
}, },
.dinit => { .dinit => {
const service_path = try std.Io.Dir.path.join(allocator, &[_][]const u8{ dest_directory, config_directory, "/dinit.d" }); const service_path = try std.fs.path.join(allocator, &[_][]const u8{ dest_directory, config_directory, "/dinit.d" });
std.Io.Dir.cwd().createDirPath(io, service_path) catch {}; std.fs.cwd().makePath(service_path) catch {};
var service_dir = std.Io.Dir.cwd().openDir(io, service_path, .{}) catch unreachable; var service_dir = std.fs.cwd().openDir(service_path, .{}) catch unreachable;
defer service_dir.close(io); defer service_dir.close();
const patched_service = try patchFile(allocator, io, "res/ly-dinit", patch_map); const patched_service = try patchFile(allocator, "res/ly-dinit", patch_map);
try installText(io, patched_service, service_dir, service_path, "ly", .{}); try installText(patched_service, service_dir, service_path, "ly", .{});
}, },
.sysvinit => { .sysvinit => {
const service_path = try std.Io.Dir.path.join(allocator, &[_][]const u8{ dest_directory, config_directory, "/init.d" }); const service_path = try std.fs.path.join(allocator, &[_][]const u8{ dest_directory, config_directory, "/init.d" });
std.Io.Dir.cwd().createDirPath(io, service_path) catch {}; std.fs.cwd().makePath(service_path) catch {};
var service_dir = std.Io.Dir.cwd().openDir(io, service_path, .{}) catch unreachable; var service_dir = std.fs.cwd().openDir(service_path, .{}) catch unreachable;
defer service_dir.close(io); defer service_dir.close();
const patched_service = try patchFile(allocator, io, "res/ly-sysvinit", patch_map); const patched_service = try patchFile(allocator, "res/ly-sysvinit", patch_map);
try installText(io, patched_service, service_dir, service_path, "ly", .{ .permissions = .fromMode(0o755) }); try installText(patched_service, service_dir, service_path, "ly", .{ .mode = 0o755 });
}, },
.freebsd => { .freebsd => {
const exe_path = try std.Io.Dir.path.join(allocator, &[_][]const u8{ dest_directory, prefix_directory, "/bin" }); const exe_path = try std.fs.path.join(allocator, &[_][]const u8{ dest_directory, prefix_directory, "/bin" });
var executable_dir = std.Io.Dir.cwd().openDir(io, exe_path, .{}) catch unreachable; var executable_dir = std.fs.cwd().openDir(exe_path, .{}) catch unreachable;
defer executable_dir.close(io); defer executable_dir.close();
const patched_wrapper = try patchFile(allocator, io, "res/ly-freebsd-wrapper", patch_map); const patched_wrapper = try patchFile(allocator, "res/ly-freebsd-wrapper", patch_map);
try installText(io, patched_wrapper, executable_dir, exe_path, "ly_wrapper", .{ .permissions = .fromMode(0o755) }); try installText(patched_wrapper, executable_dir, exe_path, "ly_wrapper", .{ .mode = 0o755 });
}, },
} }
} }
@@ -341,35 +348,33 @@ fn install_service(allocator: std.mem.Allocator, io: std.Io, patch_map: PatchMap
pub fn Uninstaller(uninstall_config: bool) type { pub fn Uninstaller(uninstall_config: bool) type {
return struct { return struct {
pub fn make(step: *std.Build.Step, _: std.Build.Step.MakeOptions) !void { pub fn make(step: *std.Build.Step, _: std.Build.Step.MakeOptions) !void {
var threaded: std.Io.Threaded = .init_single_threaded;
const io = threaded.io();
const allocator = step.owner.allocator; const allocator = step.owner.allocator;
if (uninstall_config) { if (uninstall_config) {
try deleteTree(allocator, io, config_directory, "/ly", "ly config directory not found"); try deleteTree(allocator, config_directory, "/ly", "ly config directory not found");
} }
const exe_path = try std.Io.Dir.path.join(allocator, &[_][]const u8{ prefix_directory, "/bin/", executable_name }); const exe_path = try std.fs.path.join(allocator, &[_][]const u8{ prefix_directory, "/bin/", executable_name });
var success = true; var success = true;
std.Io.Dir.cwd().deleteFile(io, exe_path) catch { std.fs.cwd().deleteFile(exe_path) catch {
std.debug.print("warn: ly executable not found\n", .{}); std.debug.print("warn: ly executable not found\n", .{});
success = false; success = false;
}; };
if (success) std.debug.print("info: deleted {s}\n", .{exe_path}); if (success) std.debug.print("info: deleted {s}\n", .{exe_path});
try deleteFile(allocator, io, config_directory, "/pam.d/ly", "ly pam file not found"); try deleteFile(allocator, config_directory, "/pam.d/ly", "ly pam file not found");
switch (init_system) { switch (init_system) {
.systemd => try deleteFile(allocator, io, prefix_directory, "/lib/systemd/system/ly@.service", "systemd service not found"), .systemd => try deleteFile(allocator, prefix_directory, "/lib/systemd/system/ly@.service", "systemd service not found"),
.openrc => try deleteFile(allocator, io, config_directory, "/init.d/ly", "openrc service not found"), .openrc => try deleteFile(allocator, config_directory, "/init.d/ly", "openrc service not found"),
.runit => try deleteTree(allocator, io, config_directory, "/sv/ly", "runit service not found"), .runit => try deleteTree(allocator, config_directory, "/sv/ly", "runit service not found"),
.s6 => { .s6 => {
try deleteTree(allocator, io, config_directory, "/s6/sv/ly-srv", "s6 service not found"); try deleteTree(allocator, config_directory, "/s6/sv/ly-srv", "s6 service not found");
try deleteFile(allocator, io, config_directory, "/s6/adminsv/default/contents.d/ly-srv", "s6 admin service not found"); try deleteFile(allocator, config_directory, "/s6/adminsv/default/contents.d/ly-srv", "s6 admin service not found");
}, },
.dinit => try deleteFile(allocator, io, config_directory, "/dinit.d/ly", "dinit service not found"), .dinit => try deleteFile(allocator, config_directory, "/dinit.d/ly", "dinit service not found"),
.sysvinit => try deleteFile(allocator, io, config_directory, "/init.d/ly", "sysvinit service not found"), .sysvinit => try deleteFile(allocator, config_directory, "/init.d/ly", "sysvinit service not found"),
.freebsd => try deleteFile(allocator, io, prefix_directory, "/bin/ly_wrapper", "freebsd wrapper not found"), .freebsd => try deleteFile(allocator, prefix_directory, "/bin/ly_wrapper", "freebsd wrapper not found"),
} }
} }
}; };
@@ -387,11 +392,11 @@ fn getVersionStr(b: *std.Build, name: []const u8, version: std.SemanticVersion)
"--match", "--match",
"*.*.*", "*.*.*",
"--tags", "--tags",
}, &status, .ignore) catch { }, &status, .Ignore) catch {
return version_str; return version_str;
}; };
var git_describe = std.mem.trim(u8, git_describe_raw, " \n\r"); var git_describe = std.mem.trim(u8, git_describe_raw, " \n\r");
git_describe = std.mem.trimStart(u8, git_describe, "v"); git_describe = std.mem.trimLeft(u8, git_describe, "v");
switch (std.mem.count(u8, git_describe, "-")) { switch (std.mem.count(u8, git_describe, "-")) {
0 => { 0 => {
@@ -404,7 +409,7 @@ fn getVersionStr(b: *std.Build, name: []const u8, version: std.SemanticVersion)
2 => { 2 => {
// Untagged development build (e.g. 0.10.0-dev.2025+ecf0050a9). // Untagged development build (e.g. 0.10.0-dev.2025+ecf0050a9).
var it = std.mem.splitScalar(u8, git_describe, '-'); var it = std.mem.splitScalar(u8, git_describe, '-');
const tagged_ancestor = std.mem.trimStart(u8, it.first(), "v"); const tagged_ancestor = std.mem.trimLeft(u8, it.first(), "v");
const commit_height = it.next().?; const commit_height = it.next().?;
const commit_id = it.next().?; const commit_id = it.next().?;
@@ -431,26 +436,25 @@ fn getVersionStr(b: *std.Build, name: []const u8, version: std.SemanticVersion)
} }
fn installFile( fn installFile(
io: std.Io,
source_file: []const u8, source_file: []const u8,
destination_directory: std.Io.Dir, destination_directory: std.fs.Dir,
destination_directory_path: []const u8, destination_directory_path: []const u8,
destination_file: []const u8, destination_file: []const u8,
options: std.Io.Dir.CopyFileOptions, options: std.fs.Dir.CopyFileOptions,
) !void { ) !void {
try std.Io.Dir.cwd().copyFile(source_file, destination_directory, destination_file, io, options); try std.fs.cwd().copyFile(source_file, destination_directory, destination_file, options);
std.debug.print("info: installed {s}/{s}\n", .{ destination_directory_path, destination_file }); std.debug.print("info: installed {s}/{s}\n", .{ destination_directory_path, destination_file });
} }
fn patchFile(allocator: std.mem.Allocator, io: std.Io, source_file: []const u8, patch_map: PatchMap) ![]const u8 { fn patchFile(allocator: std.mem.Allocator, source_file: []const u8, patch_map: PatchMap) ![]const u8 {
var file = try std.Io.Dir.cwd().openFile(io, source_file, .{}); var file = try std.fs.cwd().openFile(source_file, .{});
defer file.close(io); defer file.close();
const stat = try file.stat(io); const stat = try file.stat();
var buffer: [4096]u8 = undefined; var buffer: [4096]u8 = undefined;
var reader = file.reader(io, &buffer); var reader = file.reader(&buffer);
var text = try reader.interface.readAlloc(allocator, @intCast(stat.size)); var text = try reader.interface.readAlloc(allocator, stat.size);
var iterator = patch_map.iterator(); var iterator = patch_map.iterator();
while (iterator.next()) |kv| { while (iterator.next()) |kv| {
@@ -463,18 +467,17 @@ fn patchFile(allocator: std.mem.Allocator, io: std.Io, source_file: []const u8,
} }
fn installText( fn installText(
io: std.Io,
text: []const u8, text: []const u8,
destination_directory: std.Io.Dir, destination_directory: std.fs.Dir,
destination_directory_path: []const u8, destination_directory_path: []const u8,
destination_file: []const u8, destination_file: []const u8,
options: std.Io.File.CreateFlags, options: std.fs.File.CreateFlags,
) !void { ) !void {
var file = try destination_directory.createFile(io, destination_file, options); var file = try destination_directory.createFile(destination_file, options);
defer file.close(io); defer file.close();
var buffer: [1024]u8 = undefined; var buffer: [1024]u8 = undefined;
var writer = file.writer(io, &buffer); var writer = file.writer(&buffer);
try writer.interface.writeAll(text); try writer.interface.writeAll(text);
try writer.interface.flush(); try writer.interface.flush();
@@ -483,14 +486,13 @@ fn installText(
fn deleteFile( fn deleteFile(
allocator: std.mem.Allocator, allocator: std.mem.Allocator,
io: std.Io,
prefix: []const u8, prefix: []const u8,
file: []const u8, file: []const u8,
warning: []const u8, warning: []const u8,
) !void { ) !void {
const path = try std.Io.Dir.path.join(allocator, &[_][]const u8{ dest_directory, prefix, file }); const path = try std.fs.path.join(allocator, &[_][]const u8{ dest_directory, prefix, file });
std.Io.Dir.cwd().deleteFile(io, path) catch |err| { std.fs.cwd().deleteFile(path) catch |err| {
if (err == error.FileNotFound) { if (err == error.FileNotFound) {
std.debug.print("warn: {s}\n", .{warning}); std.debug.print("warn: {s}\n", .{warning});
return; return;
@@ -504,14 +506,13 @@ fn deleteFile(
fn deleteTree( fn deleteTree(
allocator: std.mem.Allocator, allocator: std.mem.Allocator,
io: std.Io,
prefix: []const u8, prefix: []const u8,
directory: []const u8, directory: []const u8,
warning: []const u8, warning: []const u8,
) !void { ) !void {
const path = try std.Io.Dir.path.join(allocator, &[_][]const u8{ dest_directory, prefix, directory }); const path = try std.fs.path.join(allocator, &[_][]const u8{ dest_directory, prefix, directory });
var dir = std.Io.Dir.cwd().openDir(io, path, .{}) catch |err| { var dir = std.fs.cwd().openDir(path, .{}) catch |err| {
if (err == error.FileNotFound) { if (err == error.FileNotFound) {
std.debug.print("warn: {s}\n", .{warning}); std.debug.print("warn: {s}\n", .{warning});
return; return;
@@ -519,9 +520,9 @@ fn deleteTree(
return err; return err;
}; };
dir.close(io); dir.close();
try std.Io.Dir.cwd().deleteTree(io, path); try std.fs.cwd().deleteTree(path);
std.debug.print("info: deleted {s}\n", .{path}); std.debug.print("info: deleted {s}\n", .{path});
} }

View File

@@ -1,20 +1,21 @@
.{ .{
.name = .ly, .name = .ly,
.version = "1.4.0", .version = "1.3.1",
.fingerprint = 0xa148ffcc5dc2cb59, .fingerprint = 0xa148ffcc5dc2cb59,
.minimum_zig_version = "0.16.0", .minimum_zig_version = "0.15.0",
.dependencies = .{ .dependencies = .{
.ly_ui = .{
.path = "ly-ui",
},
.clap = .{ .clap = .{
.url = "git+https://github.com/Hejsil/zig-clap#fc1e5cc3f6d9d3001112385ee6256d694e959d2f", .url = "git+https://github.com/Hejsil/zig-clap#5289e0753cd274d65344bef1c114284c633536ea",
.hash = "clap-0.11.0-oBajB7foAQC3Iyn4IVCkUdYaOVVng5IZkSncySTjNig1", .hash = "clap-0.11.0-oBajB-HnAQDPCKYzwF7rO3qDFwRcD39Q0DALlTSz5H7e",
},
.zigini = .{
.url = "git+https://github.com/AnErrupTion/zigini?ref=zig-0.15.0#9281f47702b57779e831d7618e158abb8eb4d4a2",
.hash = "zigini-0.3.3-36M0FRJJAADZVq5HPm-hYKMpFFTr0OgjbEYcK2ijKZ5n",
},
.termbox2 = .{
.url = "git+https://github.com/AnErrupTion/termbox2?ref=master#290ac6b8225aacfd16851224682b851b65fcb918",
.hash = "N-V-__8AAGcUBQAa5vov1Yi_9AXEffFQ1e2KsXaK4dgygRKq",
}, },
}, },
.paths = .{ .paths = .{""},
"build.zig",
"build.zig.zon",
"src",
},
} }

View File

@@ -1,2 +0,0 @@
#!/bin/sh
tar --zstd -cvf vendor.tar.zst zig-pkg ly-ui/zig-pkg ly-core/zig-pkg

View File

@@ -1,71 +0,0 @@
const std = @import("std");
const Translator = @import("translate_c").Translator;
pub fn build(b: *std.Build) void {
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
const mod = b.addModule("ly-core", .{
.root_source_file = b.path("src/root.zig"),
.target = target,
.optimize = optimize,
});
const zigini = b.dependency("zigini", .{ .target = target, .optimize = optimize });
mod.addImport("zigini", zigini.module("zigini"));
const translate_c = b.dependency("translate_c", .{
.target = target,
.optimize = optimize,
});
addCImport(b, mod, translate_c, target, optimize, "pam", "#include <security/pam_appl.h>");
addCImport(b, mod, translate_c, target, optimize, "utmp", "#include <utmpx.h>");
addCImport(b, mod, translate_c, target, optimize, "xcb", "#include <xcb/xcb.h>");
if (target.result.os.tag == .freebsd) {
addCImport(b, mod, translate_c, target, optimize, "pwd",
\\#include <pwd.h>
\\#include <sys/types.h>
\\#include <login_cap.h>
);
} else {
addCImport(b, mod, translate_c, target, optimize, "pwd", "#include <pwd.h>");
}
addCImport(b, mod, translate_c, target, optimize, "stdlib", "#include <stdlib.h>");
addCImport(b, mod, translate_c, target, optimize, "unistd", "#include <unistd.h>");
addCImport(b, mod, translate_c, target, optimize, "grp", "#include <grp.h>");
addCImport(b, mod, translate_c, target, optimize, "system_time", "#include <sys/time.h>");
addCImport(b, mod, translate_c, target, optimize, "time", "#include <time.h>");
if (target.result.os.tag == .linux) {
addCImport(b, mod, translate_c, target, optimize, "kd", "#include <sys/kd.h>");
addCImport(b, mod, translate_c, target, optimize, "vt", "#include <sys/vt.h>");
} else if (target.result.os.tag == .freebsd) {
addCImport(b, mod, translate_c, target, optimize, "kbio", "#include <sys/kbio.h>");
addCImport(b, mod, translate_c, target, optimize, "consio", "#include <sys/consio.h>");
}
const mod_tests = b.addTest(.{
.root_module = mod,
});
const run_mod_tests = b.addRunArtifact(mod_tests);
const test_step = b.step("test", "Run tests");
test_step.dependOn(&run_mod_tests.step);
}
fn addCImport(
b: *std.Build,
mod: *std.Build.Module,
translate_c: *std.Build.Dependency,
target: std.Build.ResolvedTarget,
optimize: std.builtin.OptimizeMode,
comptime name: []const u8,
comptime bytes: []const u8,
) void {
const pam: Translator = .init(translate_c, .{
.c_source_file = b.addWriteFiles().add(name ++ ".h", bytes),
.target = target,
.optimize = optimize,
});
mod.addImport(name, pam.mod);
}

View File

@@ -1,21 +0,0 @@
.{
.name = .ly_core,
.version = "1.0.0",
.fingerprint = 0xddda7afda795472,
.minimum_zig_version = "0.16.0",
.dependencies = .{
.zigini = .{
.url = "git+https://github.com/AshAmetrine/zigini?ref=master#a665d081dda42664a96da2840ea09c5ccf9d0692",
.hash = "zigini-0.5.0-BSkB7e9WAACfyCBABNZiWL3gFMw18GKn3qBcPs8L1Ec1",
},
.translate_c = .{
.url = "git+https://codeberg.org/ziglang/translate-c#7a1a9fdc4ab00835748a6657ecbb835e3d5d45f7",
.hash = "translate_c-0.0.0-Q_BUWvP1BgCjAk6PWv5286tOlvzD9-X-NkuTzh0KxY0Q",
},
},
.paths = .{
"build.zig",
"build.zig.zon",
"src",
},
}

View File

@@ -1,71 +0,0 @@
const std = @import("std");
const interop = @import("interop.zig");
const LogFile = @This();
path: []const u8,
could_open_log_file: bool = undefined,
file: std.Io.File = undefined,
buffer: []u8,
file_writer: std.Io.File.Writer = undefined,
pub fn init(io: std.Io, path: []const u8, buffer: []u8) !LogFile {
var log_file = LogFile{ .path = path, .buffer = buffer };
log_file.could_open_log_file = try openLogFile(io, path, &log_file);
return log_file;
}
pub fn reinit(self: *LogFile, io: std.Io) !void {
self.could_open_log_file = try openLogFile(io, self.path, self);
}
pub fn deinit(self: *LogFile, io: std.Io) void {
self.file.close(io);
}
pub fn info(self: *LogFile, io: std.Io, category: []const u8, comptime message: []const u8, args: anytype) !void {
var buffer: [128:0]u8 = undefined;
const time = interop.timeAsString(io, &buffer, "%Y-%m-%d %H:%M:%S");
try self.file_writer.interface.print("{s} [info/{s}] ", .{ time, category });
try self.file_writer.interface.print(message, args);
try self.file_writer.interface.writeByte('\n');
try self.file_writer.interface.flush();
}
pub fn err(self: *LogFile, io: std.Io, category: []const u8, comptime message: []const u8, args: anytype) !void {
var buffer: [128:0]u8 = undefined;
const time = interop.timeAsString(io, &buffer, "%Y-%m-%d %H:%M:%S");
try self.file_writer.interface.print("{s} [err/{s}] ", .{ time, category });
try self.file_writer.interface.print(message, args);
try self.file_writer.interface.writeByte('\n');
try self.file_writer.interface.flush();
}
fn openLogFile(io: std.Io, path: []const u8, log_file: *LogFile) !bool {
var could_open_log_file = true;
open_log_file: {
log_file.file = std.Io.Dir.cwd().openFile(io, path, .{ .mode = .write_only }) catch std.Io.Dir.cwd().createFile(io, path, .{ .permissions = .fromMode(0o666) }) catch {
// If we could neither open an existing log file nor create a new
// one, abort.
could_open_log_file = false;
break :open_log_file;
};
}
if (!could_open_log_file) {
log_file.file = try std.Io.Dir.openFileAbsolute(io, "/dev/null", .{ .mode = .write_only });
}
var log_file_writer = log_file.file.writer(io, log_file.buffer);
// Seek to the end of the log file
if (could_open_log_file) {
const stat = try log_file.file.stat(io);
try log_file_writer.seekTo(stat.size);
}
log_file.file_writer = log_file_writer;
return could_open_log_file;
}

View File

@@ -1,52 +0,0 @@
const std = @import("std");
const ErrInt = std.meta.Int(.unsigned, @bitSizeOf(anyerror));
const PaddingInt = std.meta.Int(.unsigned, 8 - (@bitSizeOf(ErrInt) + @bitSizeOf(bool)) % 8);
const ErrorHandler = packed struct {
has_error: bool = false,
err_int: ErrInt = 0,
padding: PaddingInt = 0,
};
const SharedError = @This();
data: []align(std.heap.page_size_min) u8,
write_error_event_fn: ?*const fn (anyerror, *anyopaque) anyerror!void,
ctx: ?*anyopaque,
pub fn init(
write_error_event_fn: ?*const fn (anyerror, *anyopaque) anyerror!void,
ctx: ?*anyopaque,
) !SharedError {
const data = try std.posix.mmap(null, @sizeOf(ErrorHandler), .{ .READ = true, .WRITE = true }, .{ .TYPE = .SHARED, .ANONYMOUS = true }, -1, 0);
return .{
.data = data,
.write_error_event_fn = write_error_event_fn,
.ctx = ctx,
};
}
pub fn deinit(self: *SharedError) void {
std.posix.munmap(self.data);
}
pub fn writeError(self: SharedError, err: anyerror) void {
var writer: std.Io.Writer = .fixed(self.data);
writer.writeStruct(ErrorHandler{ .has_error = true, .err_int = @intFromError(err) }, .native) catch {};
if (self.write_error_event_fn) |write_error_event_fn| {
@call(.auto, write_error_event_fn, .{ err, self.ctx.? }) catch {};
}
}
pub fn readError(self: SharedError) ?anyerror {
var reader: std.Io.Reader = .fixed(self.data);
const err_handler = try reader.takeStruct(ErrorHandler, .native);
if (err_handler.has_error)
return @errorFromInt(err_handler.err_int);
return null;
}

View File

@@ -1,78 +0,0 @@
const std = @import("std");
pub const ini = @import("zigini");
pub const interop = @import("interop.zig");
pub const UidRange = @import("UidRange.zig");
pub const LogFile = @import("LogFile.zig");
pub const SharedError = @import("SharedError.zig");
pub fn IniParser(comptime Struct: type) type {
return struct {
const Self = @This();
const temporary_allocator = std.heap.page_allocator;
pub const Error = struct {
type_name: []const u8,
key: []const u8,
value: []const u8,
error_name: []const u8,
};
pub var global_errors: std.ArrayList(Error) = .empty;
ini_struct: ini.Ini(Struct),
structure: Struct,
maybe_load_error: ?anyerror,
errors: std.ArrayList(Error),
pub fn init(
allocator: std.mem.Allocator,
io: std.Io,
path: []const u8,
field_handler: ?fn (allocator: std.mem.Allocator, field: ini.IniField) ?ini.IniField,
) !Self {
var ini_struct = ini.Ini(Struct).init(allocator);
errdefer ini_struct.deinit();
var maybe_load_error: ?anyerror = null;
const structure = ini_struct.readFileToStruct(io, path, .{
.fieldHandler = field_handler,
.errorHandler = errorHandler,
.comment_characters = "#",
}) catch |err| load_error: {
maybe_load_error = err;
break :load_error Struct{};
};
return .{
.ini_struct = ini_struct,
.structure = structure,
.maybe_load_error = maybe_load_error,
.errors = global_errors,
};
}
pub fn deinit(self: *Self) void {
self.ini_struct.deinit();
for (0..global_errors.items.len) |i| {
const err = global_errors.items[i];
temporary_allocator.free(err.type_name);
temporary_allocator.free(err.key);
temporary_allocator.free(err.value);
}
global_errors.deinit(temporary_allocator);
}
fn errorHandler(type_name: []const u8, key: []const u8, value: []const u8, err: anyerror) void {
global_errors.append(temporary_allocator, .{
.type_name = temporary_allocator.dupe(u8, type_name) catch return,
.key = temporary_allocator.dupe(u8, key) catch return,
.value = temporary_allocator.dupe(u8, value) catch return,
.error_name = @errorName(err),
}) catch return;
}
};
}

View File

@@ -1,53 +0,0 @@
const std = @import("std");
const Translator = @import("translate_c").Translator;
pub fn build(b: *std.Build) void {
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
const mod = b.addModule("ly-ui", .{
.root_source_file = b.path("src/root.zig"),
.target = target,
.optimize = optimize,
});
const ly_core = b.dependency("ly_core", .{ .target = target, .optimize = optimize });
mod.addImport("ly-core", ly_core.module("ly-core"));
const termbox_dep = b.dependency("termbox2", .{
.target = target,
.optimize = optimize,
});
const translate_c_dep = b.dependency("translate_c", .{
.target = target,
.optimize = optimize,
});
const termbox2: Translator = .init(translate_c_dep, .{
.c_source_file = termbox_dep.path("termbox2.h"),
.target = target,
.optimize = optimize,
});
termbox2.defineCMacro("TB_IMPL", null);
// TODO 0.16.0: Workaround until Aro gets better...
// https://codeberg.org/ziglang/translate-c/issues/319
termbox2.defineCMacro("_XOPEN_SOURCE", "700");
termbox2.defineCMacro("TB_OPT_ATTR_W", "32"); // Enable 24-bit color support + styling (32-bit)
// TODO 0.16.0: Including <fcntl.h> with -OReleaseSafe causes
// __attribute__(__error__()) to be called. Below
// is the workaround.
termbox2.defineCMacro("_FORTIFY_SOURCE", "0");
// TODO 0.16.0: Needed for now
if (target.result.os.tag == .freebsd) {
termbox2.defineCMacro("__BSD_VISIBLE", "1");
}
mod.addImport("termbox2", termbox2.mod);
const mod_tests = b.addTest(.{
.root_module = mod,
});
const run_mod_tests = b.addRunArtifact(mod_tests);
const test_step = b.step("test", "Run tests");
test_step.dependOn(&run_mod_tests.step);
}

View File

@@ -1,24 +0,0 @@
.{
.name = .ly_ui,
.version = "1.0.0",
.fingerprint = 0x8d11bf85a74ec803,
.minimum_zig_version = "0.16.0",
.dependencies = .{
.ly_core = .{
.path = "../ly-core",
},
.termbox2 = .{
.url = "git+https://github.com/AnErrupTion/termbox2?ref=master#c7f241e8888ce243e1748b05c26a42fcfaaad936",
.hash = "N-V-__8AAAUXBQD6Fwpi9m0MBqWXFFaqW5l1lVrJC2Ynj7a-",
},
.translate_c = .{
.url = "git+https://codeberg.org/ziglang/translate-c#7a1a9fdc4ab00835748a6657ecbb835e3d5d45f7",
.hash = "translate_c-0.0.0-Q_BUWvP1BgCjAk6PWv5286tOlvzD9-X-NkuTzh0KxY0Q",
},
},
.paths = .{
"build.zig",
"build.zig.zon",
"src",
},
}

View File

@@ -1,221 +0,0 @@
const Position = @This();
x: usize,
y: usize,
pub fn init(x: usize, y: usize) Position {
return .{
.x = x,
.y = y,
};
}
pub fn add(self: Position, other: Position) Position {
return .{
.x = self.x + other.x,
.y = self.y + other.y,
};
}
pub fn addIf(self: Position, other: Position, condition: bool) Position {
return .{
.x = self.x + if (condition) other.x else 0,
.y = self.y + if (condition) other.y else 0,
};
}
pub fn addX(self: Position, x: usize) Position {
return .{
.x = self.x + x,
.y = self.y,
};
}
pub fn addY(self: Position, y: usize) Position {
return .{
.x = self.x,
.y = self.y + y,
};
}
pub fn addXIf(self: Position, x: usize, condition: bool) Position {
return .{
.x = self.x + if (condition) x else 0,
.y = self.y,
};
}
pub fn addYIf(self: Position, y: usize, condition: bool) Position {
return .{
.x = self.x,
.y = self.y + if (condition) y else 0,
};
}
pub fn addXFrom(self: Position, other: Position) Position {
return .{
.x = self.x + other.x,
.y = self.y,
};
}
pub fn addYFrom(self: Position, other: Position) Position {
return .{
.x = self.x,
.y = self.y + other.y,
};
}
pub fn addXFromIf(self: Position, other: Position, condition: bool) Position {
return .{
.x = self.x + if (condition) other.x else 0,
.y = self.y,
};
}
pub fn addYFromIf(self: Position, other: Position, condition: bool) Position {
return .{
.x = self.x,
.y = self.y + if (condition) other.y else 0,
};
}
pub fn remove(self: Position, other: Position) Position {
return .{
.x = self.x - other.x,
.y = self.y - other.y,
};
}
pub fn removeIf(self: Position, other: Position, condition: bool) Position {
return .{
.x = self.x - if (condition) other.x else 0,
.y = self.y - if (condition) other.y else 0,
};
}
pub fn removeX(self: Position, x: usize) Position {
return .{
.x = self.x - x,
.y = self.y,
};
}
pub fn removeY(self: Position, y: usize) Position {
return .{
.x = self.x,
.y = self.y - y,
};
}
pub fn removeXIf(self: Position, x: usize, condition: bool) Position {
return .{
.x = self.x - if (condition) x else 0,
.y = self.y,
};
}
pub fn removeYIf(self: Position, y: usize, condition: bool) Position {
return .{
.x = self.x,
.y = self.y - if (condition) y else 0,
};
}
pub fn removeXFrom(self: Position, other: Position) Position {
return .{
.x = self.x - other.x,
.y = self.y,
};
}
pub fn removeYFrom(self: Position, other: Position) Position {
return .{
.x = self.x,
.y = self.y - other.y,
};
}
pub fn removeXFromIf(self: Position, other: Position, condition: bool) Position {
return .{
.x = self.x - if (condition) other.x else 0,
.y = self.y,
};
}
pub fn removeYFromIf(self: Position, other: Position, condition: bool) Position {
return .{
.x = self.x,
.y = self.y - if (condition) other.y else 0,
};
}
pub fn invert(self: Position, other: Position) Position {
return .{
.x = other.x - self.x,
.y = other.y - self.y,
};
}
pub fn invertIf(self: Position, other: Position, condition: bool) Position {
return .{
.x = if (condition) other.x - self.x else self.x,
.y = if (condition) other.y - self.y else self.y,
};
}
pub fn invertX(self: Position, width: usize) Position {
return .{
.x = width - self.x,
.y = self.y,
};
}
pub fn invertY(self: Position, height: usize) Position {
return .{
.x = self.x,
.y = height - self.y,
};
}
pub fn invertXIf(self: Position, width: usize, condition: bool) Position {
return .{
.x = if (condition) width - self.x else self.x,
.y = self.y,
};
}
pub fn invertYIf(self: Position, height: usize, condition: bool) Position {
return .{
.x = self.x,
.y = if (condition) height - self.y else self.y,
};
}
pub fn resetXFrom(self: Position, other: Position) Position {
return .{
.x = other.x,
.y = self.y,
};
}
pub fn resetYFrom(self: Position, other: Position) Position {
return .{
.x = self.x,
.y = other.y,
};
}
pub fn resetXFromIf(self: Position, other: Position, condition: bool) Position {
return .{
.x = if (condition) other.x else self.x,
.y = self.y,
};
}
pub fn resetYFromIf(self: Position, other: Position, condition: bool) Position {
return .{
.x = self.x,
.y = if (condition) other.y else self.y,
};
}

View File

@@ -1,632 +0,0 @@
const std = @import("std");
const Allocator = std.mem.Allocator;
const Random = std.Random;
const ly_core = @import("ly-core");
const interop = ly_core.interop;
const LogFile = ly_core.LogFile;
const SharedError = ly_core.SharedError;
pub const termbox = @import("termbox2");
const Cell = @import("Cell.zig");
const keyboard = @import("keyboard.zig");
const Position = @import("Position.zig");
const Widget = @import("Widget.zig");
const TerminalBuffer = @This();
pub const KeybindCallbackFn = *const fn (*anyopaque) anyerror!bool;
pub const KeybindMap = std.AutoHashMap(keyboard.Key, struct {
callback: KeybindCallbackFn,
context: *anyopaque,
});
pub const InitOptions = struct {
fg: u32,
bg: u32,
border_fg: u32,
full_color: bool,
is_tty: bool,
};
pub const Styling = struct {
pub const BOLD = termbox.TB_BOLD;
pub const UNDERLINE = termbox.TB_UNDERLINE;
pub const REVERSE = termbox.TB_REVERSE;
pub const ITALIC = termbox.TB_ITALIC;
pub const BLINK = termbox.TB_BLINK;
pub const HI_BLACK = termbox.TB_HI_BLACK;
pub const BRIGHT = termbox.TB_BRIGHT;
pub const DIM = termbox.TB_DIM;
};
pub const Color = struct {
pub const DEFAULT = 0x00000000;
pub const TRUE_BLACK = Styling.HI_BLACK;
pub const TRUE_RED = 0x00FF0000;
pub const TRUE_GREEN = 0x0000FF00;
pub const TRUE_YELLOW = 0x00FFFF00;
pub const TRUE_BLUE = 0x000000FF;
pub const TRUE_MAGENTA = 0x00FF00FF;
pub const TRUE_CYAN = 0x0000FFFF;
pub const TRUE_WHITE = 0x00FFFFFF;
pub const TRUE_DIM_RED = 0x00800000;
pub const TRUE_DIM_GREEN = 0x00008000;
pub const TRUE_DIM_YELLOW = 0x00808000;
pub const TRUE_DIM_BLUE = 0x00000080;
pub const TRUE_DIM_MAGENTA = 0x00800080;
pub const TRUE_DIM_CYAN = 0x00008080;
pub const TRUE_DIM_WHITE = 0x00C0C0C0;
pub const ECOL_BLACK = 1;
pub const ECOL_RED = 2;
pub const ECOL_GREEN = 3;
pub const ECOL_YELLOW = 4;
pub const ECOL_BLUE = 5;
pub const ECOL_MAGENTA = 6;
pub const ECOL_CYAN = 7;
pub const ECOL_WHITE = 8;
};
pub const START_POSITION = Position.init(0, 0);
log_file: *LogFile,
random: Random,
width: usize,
height: usize,
fg: u32,
bg: u32,
border_fg: u32,
box_chars: struct {
left_up: u32,
left_down: u32,
right_up: u32,
right_down: u32,
top: u32,
bottom: u32,
left: u32,
right: u32,
},
blank_cell: Cell,
full_color: bool,
termios: ?std.posix.termios,
keybinds: KeybindMap,
handlable_widgets: std.ArrayList(*Widget),
run: bool,
update: bool,
active_widget_index: usize,
pub fn init(
allocator: Allocator,
io: std.Io,
options: InitOptions,
log_file: *LogFile,
random: Random,
) !TerminalBuffer {
// Initialize termbox
_ = termbox.tb_init();
if (options.full_color) {
_ = termbox.tb_set_output_mode(termbox.TB_OUTPUT_TRUECOLOR);
try log_file.info(io, "tui", "termbox2 set to 24-bit color output mode", .{});
} else {
try log_file.info(io, "tui", "termbox2 set to eight-color output mode", .{});
}
_ = termbox.tb_clear();
// Let's take some precautions here and clear the back buffer as well
try clearBackBuffer();
const width: usize = @intCast(termbox.tb_width());
const height: usize = @intCast(termbox.tb_height());
try log_file.info(io, "tui", "screen resolution is {d}x{d}", .{ width, height });
return .{
.log_file = log_file,
.random = random,
.width = width,
.height = height,
.fg = options.fg,
.bg = options.bg,
.border_fg = options.border_fg,
.box_chars = if (interop.supportsUnicode()) .{
.left_up = 0x250C,
.left_down = 0x2514,
.right_up = 0x2510,
.right_down = 0x2518,
.top = 0x2500,
.bottom = 0x2500,
.left = 0x2502,
.right = 0x2502,
} else .{
.left_up = '+',
.left_down = '+',
.right_up = '+',
.right_down = '+',
.top = '-',
.bottom = '-',
.left = '|',
.right = '|',
},
.blank_cell = Cell.init(' ', options.fg, options.bg),
.full_color = options.full_color,
// Needed to reclaim the TTY after giving up its control
.termios = try std.posix.tcgetattr(std.posix.STDIN_FILENO),
.keybinds = KeybindMap.init(allocator),
.handlable_widgets = .empty,
.run = true,
.update = true,
.active_widget_index = 0,
};
}
pub fn deinit(self: *TerminalBuffer) void {
self.keybinds.deinit();
TerminalBuffer.shutdown();
}
pub fn runEventLoop(
self: *TerminalBuffer,
allocator: Allocator,
io: std.Io,
shared_error: SharedError,
layers: [][]*Widget,
active_widget: *Widget,
inactivity_delay: u16,
position_widgets_fn: *const fn (*anyopaque) anyerror!void,
inactivity_event_fn: ?*const fn (*anyopaque) anyerror!void,
context: *anyopaque,
) !void {
try self.registerGlobalKeybind(io, "Ctrl+K", &moveCursorUp, self);
try self.registerGlobalKeybind(io, "Up", &moveCursorUp, self);
try self.registerGlobalKeybind(io, "Ctrl+J", &moveCursorDown, self);
try self.registerGlobalKeybind(io, "Down", &moveCursorDown, self);
try self.registerGlobalKeybind(io, "Tab", &wrapCursor, self);
try self.registerGlobalKeybind(io, "Shift+Tab", &wrapCursorReverse, self);
defer self.handlable_widgets.deinit(allocator);
var i: usize = 0;
for (layers) |layer| {
for (layer) |widget| {
try widget.update(context);
if (widget.vtable.handle_fn != null) {
try self.handlable_widgets.append(allocator, widget);
if (widget.id == active_widget.id) self.active_widget_index = i;
i += 1;
}
}
}
try @call(.auto, position_widgets_fn, .{context});
var event: termbox.tb_event = undefined;
var inactivity_cmd_ran = false;
var inactivity_time_start = try interop.getTimeOfDay();
while (self.run) {
var maybe_timeout: ?usize = null;
if (self.update) {
try TerminalBuffer.clearScreen(false);
// Reset cursor
const current_widget = self.getActiveWidget();
current_widget.handle(null) catch |err| {
shared_error.writeError(error.SetCursorFailed);
try self.log_file.err(
io,
"tui",
"failed to set cursor in active widget '{s}': {s}",
.{ current_widget.display_name, @errorName(err) },
);
};
for (layers) |layer| {
for (layer) |widget| {
try widget.update(context);
widget.draw();
if (try widget.calculateTimeout(context)) |widget_timeout| {
if (maybe_timeout == null or widget_timeout < maybe_timeout.?) maybe_timeout = widget_timeout;
}
}
}
TerminalBuffer.presentBuffer();
}
if (inactivity_event_fn) |inactivity_fn| {
const time = try interop.getTimeOfDay();
if (!inactivity_cmd_ran and time.seconds - inactivity_time_start.seconds > inactivity_delay) {
try @call(.auto, inactivity_fn, .{context});
inactivity_cmd_ran = true;
}
}
const event_error = if (maybe_timeout) |timeout| termbox.tb_peek_event(&event, @intCast(timeout)) else termbox.tb_poll_event(&event);
self.update = maybe_timeout != null or event_error >= 0;
if (event_error < 0) continue;
// Input of some kind was detected, so reset the inactivity timer
inactivity_time_start = try interop.getTimeOfDay();
if (event.type == termbox.TB_EVENT_RESIZE) {
self.width = TerminalBuffer.getWidth();
self.height = TerminalBuffer.getHeight();
try self.log_file.info(
io,
"tui",
"screen resolution updated to {d}x{d}",
.{ self.width, self.height },
);
for (layers) |layer| {
for (layer) |widget| {
widget.realloc() catch |err| {
shared_error.writeError(error.WidgetReallocationFailed);
try self.log_file.err(
io,
"tui",
"failed to reallocate widget '{s}': {s}",
.{ widget.display_name, @errorName(err) },
);
};
}
}
try @call(.auto, position_widgets_fn, .{context});
self.update = true;
continue;
}
var maybe_keys = try self.handleKeybind(allocator, event);
if (maybe_keys) |*keys| {
defer keys.deinit(allocator);
const current_widget = self.getActiveWidget();
for (keys.items) |key| {
current_widget.handle(key) catch |err| {
shared_error.writeError(error.CurrentWidgetHandlingFailed);
try self.log_file.err(
io,
"tui",
"failed to handle active widget '{s}': {s}",
.{ current_widget.display_name, @errorName(err) },
);
};
}
self.update = true;
}
}
}
pub fn stopEventLoop(self: *TerminalBuffer) void {
self.run = false;
}
pub fn drawNextFrame(self: *TerminalBuffer, value: bool) void {
self.update = value;
}
pub fn getActiveWidget(self: *TerminalBuffer) *Widget {
return self.handlable_widgets.items[self.active_widget_index];
}
pub fn setActiveWidget(self: *TerminalBuffer, widget: *Widget) void {
for (self.handlable_widgets.items, 0..) |widg, i| {
if (widg.id == widget.id) self.active_widget_index = i;
}
}
pub fn getWidth() usize {
return @intCast(termbox.tb_width());
}
pub fn getHeight() usize {
return @intCast(termbox.tb_height());
}
pub fn setCursor(x: usize, y: usize) void {
_ = termbox.tb_set_cursor(@intCast(x), @intCast(y));
}
pub fn clearScreen(clear_back_buffer: bool) !void {
_ = termbox.tb_clear();
if (clear_back_buffer) try clearBackBuffer();
}
pub fn shutdown() void {
_ = termbox.tb_shutdown();
}
pub fn presentBuffer() void {
_ = termbox.tb_present();
}
pub fn getCell(x: usize, y: usize) ?Cell {
var maybe_cell: ?*termbox.tb_cell = undefined;
_ = termbox.tb_get_cell(
@intCast(x),
@intCast(y),
1,
&maybe_cell,
);
if (maybe_cell) |cell| {
return Cell.init(cell.ch, cell.fg, cell.bg);
}
return null;
}
pub fn setCell(x: usize, y: usize, cell: Cell) void {
_ = termbox.tb_set_cell(
@intCast(x),
@intCast(y),
cell.ch,
cell.fg,
cell.bg,
);
}
pub fn reclaim(self: TerminalBuffer) !void {
if (self.termios) |termios| {
// Take back control of the TTY
_ = termbox.tb_init();
if (self.full_color) {
_ = termbox.tb_set_output_mode(termbox.TB_OUTPUT_TRUECOLOR);
}
try std.posix.tcsetattr(std.posix.STDIN_FILENO, .FLUSH, termios);
}
}
pub fn registerKeybind(
self: *TerminalBuffer,
io: std.Io,
keybinds: *KeybindMap,
keybind: []const u8,
callback: KeybindCallbackFn,
context: *anyopaque,
) !void {
const key = try self.parseKeybind(io, keybind);
keybinds.put(key, .{
.callback = callback,
.context = context,
}) catch |err| {
try self.log_file.err(
io,
"tui",
"failed to register keybind {s}: {s}",
.{ keybind, @errorName(err) },
);
};
}
pub fn registerGlobalKeybind(
self: *TerminalBuffer,
io: std.Io,
keybind: []const u8,
callback: KeybindCallbackFn,
context: *anyopaque,
) !void {
try self.registerKeybind(io, &self.keybinds, keybind, callback, context);
}
pub fn simulateKeybind(self: *TerminalBuffer, io: std.Io, keybind: []const u8) !bool {
const key = try self.parseKeybind(io, keybind);
if (self.keybinds.get(key)) |binding| {
return try @call(
.auto,
binding.callback,
.{binding.context},
);
}
const current_widget = self.getActiveWidget();
if (current_widget.keybinds) |keybinds| {
if (keybinds.get(key)) |binding| {
return try @call(
.auto,
binding.callback,
.{binding.context},
);
}
}
return true;
}
pub fn drawText(
text: []const u8,
x: usize,
y: usize,
fg: u32,
bg: u32,
) void {
const yc: c_int = @intCast(y);
const utf8view = std.unicode.Utf8View.init(text) catch return;
var utf8 = utf8view.iterator();
var i: c_int = @intCast(x);
while (utf8.nextCodepoint()) |codepoint| : (i += termbox.tb_wcwidth(codepoint)) {
_ = termbox.tb_set_cell(i, yc, codepoint, fg, bg);
}
}
pub fn drawConfinedText(
text: []const u8,
x: usize,
y: usize,
max_length: usize,
fg: u32,
bg: u32,
) void {
const yc: c_int = @intCast(y);
const utf8view = std.unicode.Utf8View.init(text) catch return;
var utf8 = utf8view.iterator();
var i: c_int = @intCast(x);
while (utf8.nextCodepoint()) |codepoint| : (i += termbox.tb_wcwidth(codepoint)) {
if (i - @as(c_int, @intCast(x)) >= max_length) break;
_ = termbox.tb_set_cell(i, yc, codepoint, fg, bg);
}
}
pub fn drawCharMultiple(
char: u32,
x: usize,
y: usize,
length: usize,
fg: u32,
bg: u32,
) void {
const cell = Cell.init(char, fg, bg);
for (0..length) |xx| cell.put(x + xx, y);
}
// Every codepoint is assumed to have a width of 1.
// Since Ly is normally running in a TTY, this should be fine.
pub fn strWidth(str: []const u8) usize {
const utf8view = std.unicode.Utf8View.init(str) catch return str.len;
var utf8 = utf8view.iterator();
var length: c_int = 0;
while (utf8.nextCodepoint()) |codepoint| {
length += termbox.tb_wcwidth(codepoint);
}
return @intCast(length);
}
fn clearBackBuffer() !void {
// Clear the TTY because termbox2 doesn't seem to do it properly
const capability = termbox.global.caps[termbox.TB_CAP_CLEAR_SCREEN];
const capability_slice = std.mem.span(capability);
const result = std.posix.system.write(termbox.global.ttyfd, capability_slice.ptr, capability_slice.len);
if (result != capability_slice.len) return error.PartialClearBackBuffer;
if (result < 0) return error.ClearBackBufferFailed;
}
fn parseKeybind(self: *TerminalBuffer, io: std.Io, keybind: []const u8) !keyboard.Key {
var key = std.mem.zeroes(keyboard.Key);
var iterator = std.mem.splitScalar(u8, keybind, '+');
while (iterator.next()) |item| {
var found = false;
inline for (std.meta.fields(keyboard.Key)) |field| {
if (std.ascii.eqlIgnoreCase(field.name, item)) {
@field(key, field.name) = true;
found = true;
break;
}
}
if (!found) {
try self.log_file.err(
io,
"tui",
"failed to parse key {s} of keybind {s}",
.{ item, keybind },
);
}
}
return key;
}
fn handleKeybind(
self: *TerminalBuffer,
allocator: Allocator,
tb_event: termbox.tb_event,
) !?std.ArrayList(keyboard.Key) {
var keys = try keyboard.getKeyList(allocator, tb_event);
for (keys.items) |key| {
if (self.keybinds.get(key)) |binding| {
const passthrough_event = try @call(
.auto,
binding.callback,
.{binding.context},
);
if (!passthrough_event) {
keys.deinit(allocator);
return null;
}
return keys;
}
const current_widget = self.getActiveWidget();
if (current_widget.keybinds) |keybinds| {
if (keybinds.get(key)) |binding| {
const passthrough_event = try @call(
.auto,
binding.callback,
.{binding.context},
);
if (!passthrough_event) {
keys.deinit(allocator);
return null;
}
return keys;
}
}
}
return keys;
}
fn moveCursorUp(ptr: *anyopaque) !bool {
var state: *TerminalBuffer = @ptrCast(@alignCast(ptr));
if (state.active_widget_index == 0) return false;
state.active_widget_index -= 1;
state.update = true;
return false;
}
fn moveCursorDown(ptr: *anyopaque) !bool {
var state: *TerminalBuffer = @ptrCast(@alignCast(ptr));
if (state.active_widget_index == state.handlable_widgets.items.len - 1) return false;
state.active_widget_index += 1;
state.update = true;
return false;
}
fn wrapCursor(ptr: *anyopaque) !bool {
var state: *TerminalBuffer = @ptrCast(@alignCast(ptr));
state.active_widget_index = (state.active_widget_index + 1) % state.handlable_widgets.items.len;
state.update = true;
return false;
}
fn wrapCursorReverse(ptr: *anyopaque) !bool {
var state: *TerminalBuffer = @ptrCast(@alignCast(ptr));
state.active_widget_index = if (state.active_widget_index == 0) state.handlable_widgets.items.len - 1 else state.active_widget_index - 1;
state.update = true;
return false;
}

View File

@@ -1,186 +0,0 @@
const Widget = @This();
const keyboard = @import("keyboard.zig");
const TerminalBuffer = @import("TerminalBuffer.zig");
const VTable = struct {
deinit_fn: ?*const fn (ptr: *anyopaque) void,
realloc_fn: ?*const fn (ptr: *anyopaque) anyerror!void,
draw_fn: *const fn (ptr: *anyopaque) void,
update_fn: ?*const fn (ptr: *anyopaque, ctx: *anyopaque) anyerror!void,
handle_fn: ?*const fn (ptr: *anyopaque, maybe_key: ?keyboard.Key) anyerror!void,
calculate_timeout_fn: ?*const fn (ptr: *anyopaque, ctx: *anyopaque) anyerror!?usize,
};
pub var idCounter: u64 = 0;
id: u64,
display_name: []const u8,
keybinds: ?TerminalBuffer.KeybindMap,
pointer: *anyopaque,
vtable: VTable,
pub fn init(
display_name: []const u8,
keybinds: ?TerminalBuffer.KeybindMap,
pointer: anytype,
comptime deinit_fn: ?fn (ptr: @TypeOf(pointer)) void,
comptime realloc_fn: ?fn (ptr: @TypeOf(pointer)) anyerror!void,
comptime draw_fn: fn (ptr: @TypeOf(pointer)) void,
comptime update_fn: ?fn (ptr: @TypeOf(pointer), ctx: *anyopaque) anyerror!void,
comptime handle_fn: ?fn (ptr: @TypeOf(pointer), maybe_key: ?keyboard.Key) anyerror!void,
comptime calculate_timeout_fn: ?fn (ptr: @TypeOf(pointer), ctx: *anyopaque) anyerror!?usize,
) Widget {
const Pointer = @TypeOf(pointer);
const Impl = struct {
pub fn deinitImpl(ptr: *anyopaque) void {
const impl: Pointer = @ptrCast(@alignCast(ptr));
return @call(
.always_inline,
deinit_fn.?,
.{impl},
);
}
pub fn reallocImpl(ptr: *anyopaque) !void {
const impl: Pointer = @ptrCast(@alignCast(ptr));
return @call(
.always_inline,
realloc_fn.?,
.{impl},
);
}
pub fn drawImpl(ptr: *anyopaque) void {
const impl: Pointer = @ptrCast(@alignCast(ptr));
return @call(
.always_inline,
draw_fn,
.{impl},
);
}
pub fn updateImpl(ptr: *anyopaque, ctx: *anyopaque) !void {
const impl: Pointer = @ptrCast(@alignCast(ptr));
return @call(
.always_inline,
update_fn.?,
.{ impl, ctx },
);
}
pub fn handleImpl(ptr: *anyopaque, maybe_key: ?keyboard.Key) !void {
const impl: Pointer = @ptrCast(@alignCast(ptr));
return @call(
.always_inline,
handle_fn.?,
.{ impl, maybe_key },
);
}
pub fn calculateTimeoutImpl(ptr: *anyopaque, ctx: *anyopaque) !?usize {
const impl: Pointer = @ptrCast(@alignCast(ptr));
return @call(
.always_inline,
calculate_timeout_fn.?,
.{ impl, ctx },
);
}
const vtable = VTable{
.deinit_fn = if (deinit_fn != null) deinitImpl else null,
.realloc_fn = if (realloc_fn != null) reallocImpl else null,
.draw_fn = drawImpl,
.update_fn = if (update_fn != null) updateImpl else null,
.handle_fn = if (handle_fn != null) handleImpl else null,
.calculate_timeout_fn = if (calculate_timeout_fn != null) calculateTimeoutImpl else null,
};
};
idCounter += 1;
return .{
.id = idCounter,
.display_name = display_name,
.keybinds = keybinds,
.pointer = pointer,
.vtable = Impl.vtable,
};
}
pub fn deinit(self: *Widget) void {
const impl: @TypeOf(self.pointer) = @ptrCast(@alignCast(self.pointer));
if (self.vtable.deinit_fn) |deinit_fn| {
return @call(
.auto,
deinit_fn,
.{impl},
);
}
}
pub fn realloc(self: *Widget) !void {
const impl: @TypeOf(self.pointer) = @ptrCast(@alignCast(self.pointer));
if (self.vtable.realloc_fn) |realloc_fn| {
return @call(
.auto,
realloc_fn,
.{impl},
);
}
}
pub fn draw(self: *Widget) void {
const impl: @TypeOf(self.pointer) = @ptrCast(@alignCast(self.pointer));
@call(
.auto,
self.vtable.draw_fn,
.{impl},
);
}
pub fn update(self: *Widget, ctx: *anyopaque) !void {
const impl: @TypeOf(self.pointer) = @ptrCast(@alignCast(self.pointer));
if (self.vtable.update_fn) |update_fn| {
return @call(
.auto,
update_fn,
.{ impl, ctx },
);
}
}
pub fn handle(self: *Widget, maybe_key: ?keyboard.Key) !void {
const impl: @TypeOf(self.pointer) = @ptrCast(@alignCast(self.pointer));
if (self.vtable.handle_fn) |handle_fn| {
return @call(
.auto,
handle_fn,
.{ impl, maybe_key },
);
}
}
pub fn calculateTimeout(self: *Widget, ctx: *anyopaque) !?usize {
const impl: @TypeOf(self.pointer) = @ptrCast(@alignCast(self.pointer));
if (self.vtable.calculate_timeout_fn) |calculate_timeout_fn| {
return @call(
.auto,
calculate_timeout_fn,
.{ impl, ctx },
);
}
return null;
}

View File

@@ -1,237 +0,0 @@
const BigLabel = @This();
const std = @import("std");
const Allocator = std.mem.Allocator;
const ly_core = @import("ly-core");
const interop = ly_core.interop;
const en = @import("bigLabelLocales/en.zig");
const fa = @import("bigLabelLocales/fa.zig");
const Cell = @import("../Cell.zig");
const Position = @import("../Position.zig");
const TerminalBuffer = @import("../TerminalBuffer.zig");
const Widget = @import("../Widget.zig");
pub const CHAR_WIDTH = 5;
pub const CHAR_HEIGHT = 5;
pub const CHAR_SIZE = CHAR_WIDTH * CHAR_HEIGHT;
pub const X: u32 = if (ly_core.interop.supportsUnicode()) 0x2593 else '#';
pub const O: u32 = 0;
// zig fmt: off
pub const LocaleChars = struct {
ZERO: [CHAR_SIZE]u21,
ONE: [CHAR_SIZE]u21,
TWO: [CHAR_SIZE]u21,
THREE: [CHAR_SIZE]u21,
FOUR: [CHAR_SIZE]u21,
FIVE: [CHAR_SIZE]u21,
SIX: [CHAR_SIZE]u21,
SEVEN: [CHAR_SIZE]u21,
EIGHT: [CHAR_SIZE]u21,
NINE: [CHAR_SIZE]u21,
S: [CHAR_SIZE]u21,
E: [CHAR_SIZE]u21,
P: [CHAR_SIZE]u21,
A: [CHAR_SIZE]u21,
M: [CHAR_SIZE]u21,
};
// zig fmt: on
pub const BigLabelLocale = enum {
en,
fa,
};
instance: ?Widget = null,
allocator: ?Allocator = null,
buffer: *TerminalBuffer,
text: []const u8,
max_width: ?usize,
fg: u32,
bg: u32,
locale: BigLabelLocale,
update_fn: ?*const fn (*BigLabel, *anyopaque) anyerror!void,
calculate_timeout_fn: ?*const fn (*BigLabel, *anyopaque) anyerror!?usize,
component_pos: Position,
children_pos: Position,
pub fn init(
buffer: *TerminalBuffer,
text: []const u8,
max_width: ?usize,
fg: u32,
bg: u32,
locale: BigLabelLocale,
update_fn: ?*const fn (*BigLabel, *anyopaque) anyerror!void,
calculate_timeout_fn: ?*const fn (*BigLabel, *anyopaque) anyerror!?usize,
) BigLabel {
return .{
.instance = null,
.allocator = null,
.buffer = buffer,
.text = text,
.max_width = max_width,
.fg = fg,
.bg = bg,
.locale = locale,
.update_fn = update_fn,
.calculate_timeout_fn = calculate_timeout_fn,
.component_pos = TerminalBuffer.START_POSITION,
.children_pos = TerminalBuffer.START_POSITION,
};
}
pub fn deinit(self: *BigLabel) void {
if (self.allocator) |allocator| allocator.free(self.text);
}
pub fn widget(self: *BigLabel) *Widget {
if (self.instance) |*instance| return instance;
self.instance = Widget.init(
"BigLabel",
null,
self,
deinit,
null,
draw,
update,
null,
calculateTimeout,
);
return &self.instance.?;
}
pub fn setTextAlloc(
self: *BigLabel,
allocator: Allocator,
comptime fmt: []const u8,
args: anytype,
) !void {
self.text = try std.fmt.allocPrint(allocator, fmt, args);
self.allocator = allocator;
}
pub fn setTextBuf(
self: *BigLabel,
buffer: []u8,
comptime fmt: []const u8,
args: anytype,
) !void {
self.text = try std.fmt.bufPrint(buffer, fmt, args);
self.allocator = null;
}
pub fn setText(self: *BigLabel, text: []const u8) void {
self.text = text;
self.allocator = null;
}
pub fn positionX(self: *BigLabel, original_pos: Position) void {
self.component_pos = original_pos;
self.children_pos = original_pos.addX(TerminalBuffer.strWidth(self.text) * CHAR_WIDTH);
}
pub fn positionY(self: *BigLabel, original_pos: Position) void {
self.component_pos = original_pos;
self.children_pos = original_pos.addY(CHAR_HEIGHT);
}
pub fn positionXY(self: *BigLabel, original_pos: Position) void {
self.component_pos = original_pos;
self.children_pos = Position.init(
TerminalBuffer.strWidth(self.text) * CHAR_WIDTH,
CHAR_HEIGHT,
).add(original_pos);
}
pub fn childrenPosition(self: BigLabel) Position {
return self.children_pos;
}
fn draw(self: *BigLabel) void {
for (self.text, 0..) |c, i| {
const clock_cell = clockCell(
c,
self.fg,
self.bg,
self.locale,
);
alphaBlit(
self.component_pos.x + i * (CHAR_WIDTH + 1),
self.component_pos.y,
self.buffer.width,
self.buffer.height,
clock_cell,
);
}
}
fn update(self: *BigLabel, context: *anyopaque) !void {
if (self.update_fn) |update_fn| {
return @call(
.auto,
update_fn,
.{ self, context },
);
}
}
fn calculateTimeout(self: *BigLabel, ctx: *anyopaque) !?usize {
if (self.calculate_timeout_fn) |calculate_timeout_fn| {
return @call(
.auto,
calculate_timeout_fn,
.{ self, ctx },
);
}
return null;
}
fn clockCell(char: u8, fg: u32, bg: u32, locale: BigLabelLocale) [CHAR_SIZE]Cell {
var cells: [CHAR_SIZE]Cell = undefined;
//@divTrunc(time.microseconds, 500000) != 0)
const clock_chars = toBigNumber(char, locale);
for (0..cells.len) |i| cells[i] = Cell.init(clock_chars[i], fg, bg);
return cells;
}
fn alphaBlit(x: usize, y: usize, tb_width: usize, tb_height: usize, cells: [CHAR_SIZE]Cell) void {
if (x + CHAR_WIDTH >= tb_width or y + CHAR_HEIGHT >= tb_height) return;
for (0..CHAR_HEIGHT) |yy| {
for (0..CHAR_WIDTH) |xx| {
const cell = cells[yy * CHAR_WIDTH + xx];
cell.put(x + xx, y + yy);
}
}
}
fn toBigNumber(char: u8, locale: BigLabelLocale) [CHAR_SIZE]u21 {
const locale_chars = switch (locale) {
.fa => fa.locale_chars,
.en => en.locale_chars,
};
return switch (char) {
'0' => locale_chars.ZERO,
'1' => locale_chars.ONE,
'2' => locale_chars.TWO,
'3' => locale_chars.THREE,
'4' => locale_chars.FOUR,
'5' => locale_chars.FIVE,
'6' => locale_chars.SIX,
'7' => locale_chars.SEVEN,
'8' => locale_chars.EIGHT,
'9' => locale_chars.NINE,
'p', 'P' => locale_chars.P,
'a', 'A' => locale_chars.A,
'm', 'M' => locale_chars.M,
':' => locale_chars.S,
else => locale_chars.E,
};
}

View File

@@ -1,190 +0,0 @@
const std = @import("std");
const Cell = @import("../Cell.zig");
const Position = @import("../Position.zig");
const TerminalBuffer = @import("../TerminalBuffer.zig");
const Widget = @import("../Widget.zig");
const Box = @This();
instance: ?Widget = null,
buffer: *TerminalBuffer,
horizontal_margin: usize,
vertical_margin: usize,
width: usize,
height: usize,
show_borders: bool,
blank_box: bool,
top_title: ?[]const u8,
bottom_title: ?[]const u8,
border_fg: u32,
title_fg: u32,
bg: u32,
update_fn: ?*const fn (*Box, *anyopaque) anyerror!void,
left_pos: Position,
right_pos: Position,
children_pos: Position,
pub fn init(
buffer: *TerminalBuffer,
horizontal_margin: usize,
vertical_margin: usize,
width: usize,
height: usize,
show_borders: bool,
blank_box: bool,
top_title: ?[]const u8,
bottom_title: ?[]const u8,
border_fg: u32,
title_fg: u32,
bg: u32,
update_fn: ?*const fn (*Box, *anyopaque) anyerror!void,
) Box {
return .{
.instance = null,
.buffer = buffer,
.horizontal_margin = horizontal_margin,
.vertical_margin = vertical_margin,
.width = width,
.height = height,
.show_borders = show_borders,
.blank_box = blank_box,
.top_title = top_title,
.bottom_title = bottom_title,
.border_fg = border_fg,
.title_fg = title_fg,
.bg = bg,
.update_fn = update_fn,
.left_pos = TerminalBuffer.START_POSITION,
.right_pos = TerminalBuffer.START_POSITION,
.children_pos = TerminalBuffer.START_POSITION,
};
}
pub fn widget(self: *Box) *Widget {
if (self.instance) |*instance| return instance;
self.instance = Widget.init(
"Box",
null,
self,
null,
null,
draw,
update,
null,
null,
);
return &self.instance.?;
}
pub fn positionXY(self: *Box, original_pos: Position) void {
if (self.buffer.width < 2 or self.buffer.height < 2) return;
self.left_pos = original_pos;
self.right_pos = Position.init(
@min(self.buffer.width, self.width),
@min(self.buffer.height, self.height),
).add(self.left_pos);
self.children_pos = Position.init(
self.horizontal_margin,
self.vertical_margin,
).add(self.left_pos);
}
pub fn childrenPosition(self: Box) Position {
return self.children_pos;
}
fn draw(self: *Box) void {
if (self.show_borders) {
var left_up = Cell.init(
self.buffer.box_chars.left_up,
self.border_fg,
self.bg,
);
var right_up = Cell.init(
self.buffer.box_chars.right_up,
self.border_fg,
self.bg,
);
var left_down = Cell.init(
self.buffer.box_chars.left_down,
self.border_fg,
self.bg,
);
var right_down = Cell.init(
self.buffer.box_chars.right_down,
self.border_fg,
self.bg,
);
var top = Cell.init(
self.buffer.box_chars.top,
self.border_fg,
self.bg,
);
var bottom = Cell.init(
self.buffer.box_chars.bottom,
self.border_fg,
self.bg,
);
left_up.put(self.left_pos.x - 1, self.left_pos.y - 1);
right_up.put(self.right_pos.x, self.left_pos.y - 1);
left_down.put(self.left_pos.x - 1, self.right_pos.y);
right_down.put(self.right_pos.x, self.right_pos.y);
for (0..self.width) |i| {
top.put(self.left_pos.x + i, self.left_pos.y - 1);
bottom.put(self.left_pos.x + i, self.right_pos.y);
}
top.ch = self.buffer.box_chars.left;
bottom.ch = self.buffer.box_chars.right;
for (0..self.height) |i| {
top.put(self.left_pos.x - 1, self.left_pos.y + i);
bottom.put(self.right_pos.x, self.left_pos.y + i);
}
}
if (self.blank_box) {
for (0..self.height) |y| {
for (0..self.width) |x| {
self.buffer.blank_cell.put(self.left_pos.x + x, self.left_pos.y + y);
}
}
}
if (self.top_title) |title| {
TerminalBuffer.drawConfinedText(
title,
self.left_pos.x,
self.left_pos.y - 1,
self.width,
self.title_fg,
self.bg,
);
}
if (self.bottom_title) |title| {
TerminalBuffer.drawConfinedText(
title,
self.left_pos.x,
self.left_pos.y + self.height,
self.width,
self.title_fg,
self.bg,
);
}
}
fn update(self: *Box, ctx: *anyopaque) !void {
if (self.update_fn) |update_fn| {
return @call(
.auto,
update_fn,
.{ self, ctx },
);
}
}

View File

@@ -1,153 +0,0 @@
const Label = @This();
const std = @import("std");
const Allocator = std.mem.Allocator;
const Cell = @import("../Cell.zig");
const Position = @import("../Position.zig");
const TerminalBuffer = @import("../TerminalBuffer.zig");
const Widget = @import("../Widget.zig");
instance: ?Widget,
allocator: ?Allocator,
text: []const u8,
max_width: ?usize,
fg: u32,
bg: u32,
update_fn: ?*const fn (*Label, *anyopaque) anyerror!void,
calculate_timeout_fn: ?*const fn (*Label, *anyopaque) anyerror!?usize,
component_pos: Position,
children_pos: Position,
pub fn init(
text: []const u8,
max_width: ?usize,
fg: u32,
bg: u32,
update_fn: ?*const fn (*Label, *anyopaque) anyerror!void,
calculate_timeout_fn: ?*const fn (*Label, *anyopaque) anyerror!?usize,
) Label {
return .{
.instance = null,
.allocator = null,
.text = text,
.max_width = max_width,
.fg = fg,
.bg = bg,
.update_fn = update_fn,
.calculate_timeout_fn = calculate_timeout_fn,
.component_pos = TerminalBuffer.START_POSITION,
.children_pos = TerminalBuffer.START_POSITION,
};
}
pub fn deinit(self: *Label) void {
if (self.allocator) |allocator| allocator.free(self.text);
}
pub fn widget(self: *Label) *Widget {
if (self.instance) |*instance| return instance;
self.instance = Widget.init(
"Label",
null,
self,
deinit,
null,
draw,
update,
null,
calculateTimeout,
);
return &self.instance.?;
}
pub fn setTextAlloc(
self: *Label,
allocator: Allocator,
comptime fmt: []const u8,
args: anytype,
) !void {
self.text = try std.fmt.allocPrint(allocator, fmt, args);
self.allocator = allocator;
}
pub fn setTextBuf(
self: *Label,
buffer: []u8,
comptime fmt: []const u8,
args: anytype,
) !void {
self.text = try std.fmt.bufPrint(buffer, fmt, args);
self.allocator = null;
}
pub fn setText(self: *Label, text: []const u8) void {
self.text = text;
self.allocator = null;
}
pub fn positionX(self: *Label, original_pos: Position) void {
self.component_pos = original_pos;
self.children_pos = original_pos.addX(TerminalBuffer.strWidth(self.text));
}
pub fn positionY(self: *Label, original_pos: Position) void {
self.component_pos = original_pos;
self.children_pos = original_pos.addY(1);
}
pub fn positionXY(self: *Label, original_pos: Position) void {
self.component_pos = original_pos;
self.children_pos = Position.init(
TerminalBuffer.strWidth(self.text),
1,
).add(original_pos);
}
pub fn childrenPosition(self: Label) Position {
return self.children_pos;
}
fn draw(self: *Label) void {
if (self.max_width) |width| {
TerminalBuffer.drawConfinedText(
self.text,
self.component_pos.x,
self.component_pos.y,
width,
self.fg,
self.bg,
);
return;
}
TerminalBuffer.drawText(
self.text,
self.component_pos.x,
self.component_pos.y,
self.fg,
self.bg,
);
}
fn update(self: *Label, ctx: *anyopaque) !void {
if (self.update_fn) |update_fn| {
return @call(
.auto,
update_fn,
.{ self, ctx },
);
}
}
fn calculateTimeout(self: *Label, ctx: *anyopaque) !?usize {
if (self.calculate_timeout_fn) |calculate_timeout_fn| {
return @call(
.auto,
calculate_timeout_fn,
.{ self, ctx },
);
}
return null;
}

View File

@@ -1,246 +0,0 @@
const std = @import("std");
const Allocator = std.mem.Allocator;
const keyboard = @import("../keyboard.zig");
const TerminalBuffer = @import("../TerminalBuffer.zig");
const Position = @import("../Position.zig");
const Widget = @import("../Widget.zig");
const DynamicString = std.ArrayListUnmanaged(u8);
const Text = @This();
instance: ?Widget,
allocator: Allocator,
buffer: *TerminalBuffer,
text: DynamicString,
end: usize,
cursor: usize,
visible_start: usize,
width: usize,
component_pos: Position,
children_pos: Position,
should_insert: bool,
masked: bool,
maybe_mask: ?u32,
fg: u32,
bg: u32,
keybinds: TerminalBuffer.KeybindMap,
pub fn init(
allocator: Allocator,
io: std.Io,
buffer: *TerminalBuffer,
should_insert: bool,
masked: bool,
maybe_mask: ?u32,
width: usize,
fg: u32,
bg: u32,
) !*Text {
var self = try allocator.create(Text);
self.* = Text{
.instance = null,
.allocator = allocator,
.buffer = buffer,
.text = .empty,
.end = 0,
.cursor = 0,
.visible_start = 0,
.width = width,
.component_pos = TerminalBuffer.START_POSITION,
.children_pos = TerminalBuffer.START_POSITION,
.should_insert = should_insert,
.masked = masked,
.maybe_mask = maybe_mask,
.fg = fg,
.bg = bg,
.keybinds = .init(allocator),
};
try buffer.registerKeybind(io, &self.keybinds, "Left", &goLeft, self);
try buffer.registerKeybind(io, &self.keybinds, "Right", &goRight, self);
try buffer.registerKeybind(io, &self.keybinds, "Delete", &delete, self);
try buffer.registerKeybind(io, &self.keybinds, "Backspace", &backspace, self);
try buffer.registerKeybind(io, &self.keybinds, "Ctrl+U", &clearTextEntry, self);
return self;
}
pub fn deinit(self: *Text) void {
self.text.deinit(self.allocator);
self.keybinds.deinit();
self.allocator.destroy(self);
}
pub fn widget(self: *Text) *Widget {
if (self.instance) |*instance| return instance;
self.instance = Widget.init(
"Text",
self.keybinds,
self,
deinit,
null,
draw,
null,
handle,
null,
);
return &self.instance.?;
}
pub fn positionX(self: *Text, original_pos: Position) void {
self.component_pos = original_pos;
self.children_pos = original_pos.addX(self.width);
}
pub fn positionY(self: *Text, original_pos: Position) void {
self.component_pos = original_pos;
self.children_pos = original_pos.addY(1);
}
pub fn positionXY(self: *Text, original_pos: Position) void {
self.component_pos = original_pos;
self.children_pos = Position.init(
self.width,
1,
).add(original_pos);
}
pub fn childrenPosition(self: Text) Position {
return self.children_pos;
}
pub fn clear(self: *Text) void {
self.text.clearRetainingCapacity();
self.end = 0;
self.cursor = 0;
self.visible_start = 0;
}
pub fn toggleMask(self: *Text) void {
self.masked = !self.masked;
}
pub fn handle(self: *Text, maybe_key: ?keyboard.Key) !void {
if (maybe_key) |key| {
if (self.should_insert) {
const maybe_character = key.getEnabledPrintableAscii();
if (maybe_character) |character| try self.write(character);
}
}
if (self.masked and self.maybe_mask == null) {
TerminalBuffer.setCursor(
self.component_pos.x,
self.component_pos.y,
);
return;
}
TerminalBuffer.setCursor(
self.component_pos.x + (self.cursor - self.visible_start),
self.component_pos.y,
);
}
fn draw(self: *Text) void {
if (self.masked) {
if (self.maybe_mask) |mask| {
if (self.width < 1) return;
const length = @min(TerminalBuffer.strWidth(self.text.items), self.width - 1);
if (length == 0) return;
TerminalBuffer.drawCharMultiple(
mask,
self.component_pos.x,
self.component_pos.y,
length,
self.fg,
self.bg,
);
}
return;
}
const str_length = TerminalBuffer.strWidth(self.text.items);
const length = @min(str_length, self.width);
if (length == 0) return;
const visible_slice = vs: {
if (str_length > self.width and self.cursor < str_length) {
break :vs self.text.items[self.visible_start..(self.width + self.visible_start)];
} else {
break :vs self.text.items[self.visible_start..];
}
};
TerminalBuffer.drawText(
visible_slice,
self.component_pos.x,
self.component_pos.y,
self.fg,
self.bg,
);
}
fn goLeft(ptr: *anyopaque) !bool {
var self: *Text = @ptrCast(@alignCast(ptr));
if (self.cursor == 0) return false;
if (self.visible_start > 0) self.visible_start -= 1;
self.cursor -= 1;
return false;
}
fn goRight(ptr: *anyopaque) !bool {
var self: *Text = @ptrCast(@alignCast(ptr));
if (self.cursor >= self.end) return false;
if (self.cursor - self.visible_start == self.width - 1) self.visible_start += 1;
self.cursor += 1;
return false;
}
fn delete(ptr: *anyopaque) !bool {
var self: *Text = @ptrCast(@alignCast(ptr));
if (self.cursor >= self.end or !self.should_insert) return false;
_ = self.text.orderedRemove(self.cursor);
self.end -= 1;
return false;
}
fn backspace(ptr: *anyopaque) !bool {
const self: *Text = @ptrCast(@alignCast(ptr));
if (self.cursor == 0 or !self.should_insert) return false;
_ = try goLeft(ptr);
_ = try delete(ptr);
return false;
}
fn write(self: *Text, char: u8) !void {
if (char == 0) return;
try self.text.insert(self.allocator, self.cursor, char);
self.end += 1;
_ = try goRight(self);
}
fn clearTextEntry(ptr: *anyopaque) !bool {
var self: *Text = @ptrCast(@alignCast(ptr));
if (!self.should_insert) return false;
self.clear();
self.buffer.drawNextFrame(true);
return false;
}

View File

@@ -1,172 +0,0 @@
const std = @import("std");
const Cell = @import("../Cell.zig");
const keyboard = @import("../keyboard.zig");
const TerminalBuffer = @import("../TerminalBuffer.zig");
const Position = @import("../Position.zig");
pub fn CyclableLabel(comptime ItemType: type, comptime ChangeItemType: type) type {
return struct {
const Allocator = std.mem.Allocator;
const ItemList = std.ArrayListUnmanaged(ItemType);
const DrawItemFn = *const fn (*Self, ItemType, usize, usize, usize) void;
const ChangeItemFn = *const fn (ItemType, ?ChangeItemType) void;
const Self = @This();
allocator: Allocator,
buffer: *TerminalBuffer,
list: ItemList,
current: usize,
width: usize,
component_pos: Position,
children_pos: Position,
text_in_center: bool,
fg: u32,
bg: u32,
cursor: usize,
draw_item_fn: DrawItemFn,
change_item_fn: ?ChangeItemFn,
change_item_arg: ?ChangeItemType,
keybinds: TerminalBuffer.KeybindMap,
pub fn init(
allocator: Allocator,
io: std.Io,
buffer: *TerminalBuffer,
draw_item_fn: DrawItemFn,
change_item_fn: ?ChangeItemFn,
change_item_arg: ?ChangeItemType,
width: usize,
text_in_center: bool,
fg: u32,
bg: u32,
) !*Self {
var self = try allocator.create(Self);
self.* = .{
.allocator = allocator,
.buffer = buffer,
.list = .empty,
.current = 0,
.width = width,
.component_pos = TerminalBuffer.START_POSITION,
.children_pos = TerminalBuffer.START_POSITION,
.text_in_center = text_in_center,
.fg = fg,
.bg = bg,
.cursor = 0,
.draw_item_fn = draw_item_fn,
.change_item_fn = change_item_fn,
.change_item_arg = change_item_arg,
.keybinds = .init(allocator),
};
try buffer.registerKeybind(io, &self.keybinds, "Left", &goLeft, self);
try buffer.registerKeybind(io, &self.keybinds, "Ctrl+H", &goLeft, self);
try buffer.registerKeybind(io, &self.keybinds, "Right", &goRight, self);
try buffer.registerKeybind(io, &self.keybinds, "Ctrl+L", &goRight, self);
return self;
}
pub fn deinit(self: *Self) void {
self.list.deinit(self.allocator);
self.keybinds.deinit();
self.allocator.destroy(self);
}
pub fn positionX(self: *Self, original_pos: Position) void {
self.component_pos = original_pos;
self.cursor = self.component_pos.x + 2;
self.children_pos = original_pos.addX(self.width);
}
pub fn positionY(self: *Self, original_pos: Position) void {
self.component_pos = original_pos;
self.cursor = self.component_pos.x + 2;
self.children_pos = original_pos.addY(1);
}
pub fn positionXY(self: *Self, original_pos: Position) void {
self.component_pos = original_pos;
self.cursor = self.component_pos.x + 2;
self.children_pos = Position.init(
self.width,
1,
).add(original_pos);
}
pub fn childrenPosition(self: Self) Position {
return self.children_pos;
}
pub fn addItem(self: *Self, item: ItemType) !void {
try self.list.append(self.allocator, item);
self.current = self.list.items.len - 1;
}
pub fn handle(self: *Self, _: ?keyboard.Key) void {
TerminalBuffer.setCursor(
self.component_pos.x + self.cursor + 2,
self.component_pos.y,
);
}
pub fn draw(self: *Self) void {
if (self.list.items.len == 0) return;
if (self.width < 2) return;
var left_arrow = Cell.init('<', self.fg, self.bg);
var right_arrow = Cell.init('>', self.fg, self.bg);
left_arrow.put(self.component_pos.x, self.component_pos.y);
right_arrow.put(
self.component_pos.x + self.width - 1,
self.component_pos.y,
);
const current_item = self.list.items[self.current];
const x = self.component_pos.x + 2;
const y = self.component_pos.y;
const width = self.width - 2;
@call(
.auto,
self.draw_item_fn,
.{ self, current_item, x, y, width },
);
}
fn goLeft(ptr: *anyopaque) !bool {
var self: *Self = @ptrCast(@alignCast(ptr));
self.current = if (self.current == 0) self.list.items.len - 1 else self.current - 1;
if (self.change_item_fn) |change_item_fn| {
@call(
.auto,
change_item_fn,
.{ self.list.items[self.current], self.change_item_arg },
);
}
return false;
}
fn goRight(ptr: *anyopaque) !bool {
var self: *Self = @ptrCast(@alignCast(ptr));
self.current = if (self.current == self.list.items.len - 1) 0 else self.current + 1;
if (self.change_item_fn) |change_item_fn| {
@call(
.auto,
change_item_fn,
.{ self.list.items[self.current], self.change_item_arg },
);
}
return false;
}
};
}

View File

@@ -1,708 +0,0 @@
const std = @import("std");
const Allocator = std.mem.Allocator;
const KeyList = std.ArrayList(Key);
const TerminalBuffer = @import("TerminalBuffer.zig");
const termbox = TerminalBuffer.termbox;
pub const Key = packed struct {
ctrl: bool,
shift: bool,
alt: bool,
f1: bool,
f2: bool,
f3: bool,
f4: bool,
f5: bool,
f6: bool,
f7: bool,
f8: bool,
f9: bool,
f10: bool,
f11: bool,
f12: bool,
insert: bool,
delete: bool,
home: bool,
end: bool,
pageup: bool,
pagedown: bool,
up: bool,
down: bool,
left: bool,
right: bool,
tab: bool,
backspace: bool,
enter: bool,
@" ": bool,
@"!": bool,
@"`": bool,
esc: bool,
@"[": bool,
@"\\": bool,
@"]": bool,
@"/": bool,
_: bool,
@"'": bool,
@"\"": bool,
@",": bool,
@"-": bool,
@".": bool,
@"#": bool,
@"$": bool,
@"%": bool,
@"&": bool,
@"*": bool,
@"(": bool,
@")": bool,
@"+": bool,
@"=": bool,
@":": bool,
@";": bool,
@"<": bool,
@">": bool,
@"?": bool,
@"@": bool,
@"^": bool,
@"~": bool,
@"{": bool,
@"}": bool,
@"|": bool,
@"0": bool,
@"1": bool,
@"2": bool,
@"3": bool,
@"4": bool,
@"5": bool,
@"6": bool,
@"7": bool,
@"8": bool,
@"9": bool,
a: bool,
b: bool,
c: bool,
d: bool,
e: bool,
f: bool,
g: bool,
h: bool,
i: bool,
j: bool,
k: bool,
l: bool,
m: bool,
n: bool,
o: bool,
p: bool,
q: bool,
r: bool,
s: bool,
t: bool,
u: bool,
v: bool,
w: bool,
x: bool,
y: bool,
z: bool,
pub fn getEnabledPrintableAscii(self: Key) ?u8 {
if (self.ctrl or self.alt) return null;
inline for (std.meta.fields(Key)) |field| {
if (field.name.len == 1 and std.ascii.isPrint(field.name[0]) and @field(self, field.name)) {
if (self.shift) {
if (!std.ascii.isAlphanumeric(field.name[0])) return null;
return std.ascii.toUpper(field.name[0]);
}
return field.name[0];
}
}
return null;
}
};
pub fn getKeyList(allocator: Allocator, tb_event: termbox.tb_event) !KeyList {
var keys: KeyList = .empty;
var key = std.mem.zeroes(Key);
if (tb_event.mod & termbox.TB_MOD_CTRL != 0) key.ctrl = true;
if (tb_event.mod & termbox.TB_MOD_SHIFT != 0) key.shift = true;
if (tb_event.mod & termbox.TB_MOD_ALT != 0) key.alt = true;
if (tb_event.key == termbox.TB_KEY_BACK_TAB) {
key.shift = true;
key.tab = true;
} else if (tb_event.key > termbox.TB_KEY_BACK_TAB) {
const code = 0xFFFF - tb_event.key;
switch (code) {
0 => key.f1 = true,
1 => key.f2 = true,
2 => key.f3 = true,
3 => key.f4 = true,
4 => key.f5 = true,
5 => key.f6 = true,
6 => key.f7 = true,
7 => key.f8 = true,
8 => key.f9 = true,
9 => key.f10 = true,
10 => key.f11 = true,
11 => key.f12 = true,
12 => key.insert = true,
13 => key.delete = true,
14 => key.home = true,
15 => key.end = true,
16 => key.pageup = true,
17 => key.pagedown = true,
18 => key.up = true,
19 => key.down = true,
20 => key.left = true,
21 => key.right = true,
else => {},
}
} else if (tb_event.ch < 128) {
const code = if (tb_event.ch == 0 and tb_event.key < 128) tb_event.key else tb_event.ch;
switch (code) {
0 => {
key.ctrl = true;
key.@"2" = true;
try keys.append(allocator, key);
key = std.mem.zeroes(Key);
key.@"`" = true;
},
1 => {
key.ctrl = true;
key.a = true;
},
2 => {
key.ctrl = true;
key.b = true;
},
3 => {
key.ctrl = true;
key.c = true;
},
4 => {
key.ctrl = true;
key.d = true;
},
5 => {
key.ctrl = true;
key.e = true;
},
6 => {
key.ctrl = true;
key.f = true;
},
7 => {
key.ctrl = true;
key.g = true;
},
8 => {
key.ctrl = true;
key.h = true;
try keys.append(allocator, key);
key = std.mem.zeroes(Key);
key.backspace = true;
},
9 => {
key.ctrl = true;
key.i = true;
try keys.append(allocator, key);
key = std.mem.zeroes(Key);
key.tab = true;
},
10 => {
key.ctrl = true;
key.j = true;
},
11 => {
key.ctrl = true;
key.k = true;
},
12 => {
key.ctrl = true;
key.l = true;
},
13 => {
key.ctrl = true;
key.m = true;
try keys.append(allocator, key);
key = std.mem.zeroes(Key);
key.enter = true;
},
14 => {
key.ctrl = true;
key.n = true;
},
15 => {
key.ctrl = true;
key.o = true;
},
16 => {
key.ctrl = true;
key.p = true;
},
17 => {
key.ctrl = true;
key.q = true;
},
18 => {
key.ctrl = true;
key.r = true;
},
19 => {
key.ctrl = true;
key.s = true;
},
20 => {
key.ctrl = true;
key.t = true;
},
21 => {
key.ctrl = true;
key.u = true;
},
22 => {
key.ctrl = true;
key.v = true;
},
23 => {
key.ctrl = true;
key.w = true;
},
24 => {
key.ctrl = true;
key.x = true;
},
25 => {
key.ctrl = true;
key.y = true;
},
26 => {
key.ctrl = true;
key.z = true;
},
27 => {
key.ctrl = true;
key.@"3" = true;
try keys.append(allocator, key);
key = std.mem.zeroes(Key);
key.esc = true;
try keys.append(allocator, key);
key = std.mem.zeroes(Key);
key.@"[" = true;
},
28 => {
key.ctrl = true;
key.@"4" = true;
try keys.append(allocator, key);
key = std.mem.zeroes(Key);
key.@"\\" = true;
},
29 => {
key.ctrl = true;
key.@"5" = true;
try keys.append(allocator, key);
key = std.mem.zeroes(Key);
key.@"]" = true;
},
30 => {
key.ctrl = true;
try keys.append(allocator, key);
key = std.mem.zeroes(Key);
key.@"6" = true;
},
31 => {
key.ctrl = true;
key.@"7" = true;
try keys.append(allocator, key);
key = std.mem.zeroes(Key);
key.@"/" = true;
try keys.append(allocator, key);
key = std.mem.zeroes(Key);
key._ = true;
},
32 => {
key.@" " = true;
},
33 => {
key = std.mem.zeroes(Key);
key.@"!" = true;
},
34 => {
key = std.mem.zeroes(Key);
key.@"\"" = true;
},
35 => {
key = std.mem.zeroes(Key);
key.@"#" = true;
},
36 => {
key = std.mem.zeroes(Key);
key.@"$" = true;
},
37 => {
key = std.mem.zeroes(Key);
key.@"%" = true;
},
38 => {
key = std.mem.zeroes(Key);
key.@"&" = true;
},
39 => {
key.@"'" = true;
},
40 => {
key = std.mem.zeroes(Key);
key.@"(" = true;
},
41 => {
key = std.mem.zeroes(Key);
key.@")" = true;
},
42 => {
key = std.mem.zeroes(Key);
key.@"*" = true;
},
43 => {
key = std.mem.zeroes(Key);
key.@"+" = true;
},
44 => {
key.@"," = true;
},
45 => {
key.@"-" = true;
},
46 => {
key.@"." = true;
},
47 => {
key.@"/" = true;
},
48 => {
key.@"0" = true;
},
49 => {
key.@"1" = true;
},
50 => {
key.@"2" = true;
},
51 => {
key.@"3" = true;
},
52 => {
key.@"4" = true;
},
53 => {
key.@"5" = true;
},
54 => {
key.@"6" = true;
},
55 => {
key.@"7" = true;
},
56 => {
key.@"8" = true;
},
57 => {
key.@"9" = true;
},
58 => {
key.shift = true;
key.@":" = true;
},
59 => {
key.@";" = true;
},
60 => {
key.shift = true;
key.@"<" = true;
},
61 => {
key.@"=" = true;
},
62 => {
key.shift = true;
key.@">" = true;
},
63 => {
key.shift = true;
key.@"?" = true;
},
64 => {
key.shift = true;
key.@"2" = true;
try keys.append(allocator, key);
key = std.mem.zeroes(Key);
key.@"@" = true;
},
65 => {
key.shift = true;
key.a = true;
},
66 => {
key.shift = true;
key.b = true;
},
67 => {
key.shift = true;
key.c = true;
},
68 => {
key.shift = true;
key.d = true;
},
69 => {
key.shift = true;
key.e = true;
},
70 => {
key.shift = true;
key.f = true;
},
71 => {
key.shift = true;
key.g = true;
},
72 => {
key.shift = true;
key.h = true;
},
73 => {
key.shift = true;
key.i = true;
},
74 => {
key.shift = true;
key.j = true;
},
75 => {
key.shift = true;
key.k = true;
},
76 => {
key.shift = true;
key.l = true;
},
77 => {
key.shift = true;
key.m = true;
},
78 => {
key.shift = true;
key.n = true;
},
79 => {
key.shift = true;
key.o = true;
},
80 => {
key.shift = true;
key.p = true;
},
81 => {
key.shift = true;
key.q = true;
},
82 => {
key.shift = true;
key.r = true;
},
83 => {
key.shift = true;
key.s = true;
},
84 => {
key.shift = true;
key.t = true;
},
85 => {
key.shift = true;
key.u = true;
},
86 => {
key.shift = true;
key.v = true;
},
87 => {
key.shift = true;
key.w = true;
},
88 => {
key.shift = true;
key.x = true;
},
89 => {
key.shift = true;
key.y = true;
},
90 => {
key.shift = true;
key.z = true;
},
91 => {
key.@"[" = true;
},
92 => {
key.@"\\" = true;
},
93 => {
key.@"]" = true;
},
94 => {
key = std.mem.zeroes(Key);
key.@"^" = true;
},
95 => {
key.shift = true;
key.@"-" = true;
try keys.append(allocator, key);
key = std.mem.zeroes(Key);
key._ = true;
},
96 => {
key.@"`" = true;
},
97 => {
key.a = true;
},
98 => {
key.b = true;
},
99 => {
key.c = true;
},
100 => {
key.d = true;
},
101 => {
key.e = true;
},
102 => {
key.f = true;
},
103 => {
key.g = true;
},
104 => {
key.h = true;
},
105 => {
key.i = true;
},
106 => {
key.j = true;
},
107 => {
key.k = true;
},
108 => {
key.l = true;
},
109 => {
key.m = true;
},
110 => {
key.n = true;
},
111 => {
key.o = true;
},
112 => {
key.p = true;
},
113 => {
key.q = true;
},
114 => {
key.r = true;
},
115 => {
key.s = true;
},
116 => {
key.t = true;
},
117 => {
key.u = true;
},
118 => {
key.v = true;
},
119 => {
key.w = true;
},
120 => {
key.x = true;
},
121 => {
key.y = true;
},
122 => {
key.z = true;
},
123 => {
key.shift = true;
key.@"{" = true;
},
124 => {
key.shift = true;
key.@"\\" = true;
try keys.append(allocator, key);
key = std.mem.zeroes(Key);
key.@"|" = true;
},
125 => {
key.shift = true;
key.@"}" = true;
},
126 => {
key.shift = true;
key.@"`" = true;
try keys.append(allocator, key);
key = std.mem.zeroes(Key);
key.@"~" = true;
},
127 => {
key.ctrl = true;
key.@"8" = true;
try keys.append(allocator, key);
key = std.mem.zeroes(Key);
key.backspace = true;
},
else => {},
}
}
try keys.append(allocator, key);
return keys;
}

View File

@@ -1,13 +0,0 @@
pub const ly_core = @import("ly-core");
pub const Cell = @import("Cell.zig");
pub const keyboard = @import("keyboard.zig");
pub const Position = @import("Position.zig");
pub const TerminalBuffer = @import("TerminalBuffer.zig");
pub const Widget = @import("Widget.zig");
pub const BigLabel = @import("components/BigLabel.zig");
pub const Box = @import("components/Box.zig");
pub const CyclableLabel = @import("components/generic.zig").CyclableLabel;
pub const Label = @import("components/Label.zig");
pub const Text = @import("components/Text.zig");

152
readme.md
View File

@@ -2,33 +2,25 @@
![Ly screenshot](.github/screenshot.png "Ly screenshot") ![Ly screenshot](.github/screenshot.png "Ly screenshot")
_Note: the above animation can be found [here](https://codeberg.org/attachments/f336d6ac-8331-4323-91fc-0e4619803401)!_ Ly is a lightweight TUI (ncurses-like) display manager for Linux and BSD,
designed with portability in mind (e.g. it does not require systemd to run).
Ly is a lightweight TUI (ncurses-like) display manager for Linux and BSD, designed with portability in mind and doesn't require systemd to run. Join us on Matrix over at [#ly:envs.net](https://matrix.to/#/#ly:envs.net)!
Join us on Matrix over at [#ly-dm:matrix.org](https://matrix.to/#/#ly-dm:matrix.org)! **Note**: Development happens on [Codeberg](https://codeberg.org/fairyglade/ly)
with a mirror on [GitHub](https://github.com/fairyglade/ly).
> [!NOTE]
> Development happens on [Codeberg](https://codeberg.org/fairyglade/ly) with a mirror on [GitHub](https://github.com/fairyglade/ly).
## Dependencies ## Dependencies
- Compile-time: - Compile-time:
- zig 0.16.x - zig 0.15.x
- libc - libc
- pam - pam
- xcb (optional, required by default; needed for X11 support) - xcb (optional, required by default; needed for X11 support)
- Runtime (with default config): - Runtime (with default config):
- xorg - xorg
- xorg-xauth - xorg-xauth
- shutdown - shutdown
- brightnessctl - brightnessctl
### Debian ### Debian
@@ -39,8 +31,8 @@ Join us on Matrix over at [#ly-dm:matrix.org](https://matrix.to/#/#ly-dm:matrix.
### Fedora ### Fedora
> [!WARNING] **Warning**: You may encounter issues with SELinux on Fedora.
> You may encounter issues with SELinux on Fedora. It is recommended to add a rule for Ly as it currently does not ship one. It is recommended to add a rule for Ly as it currently does not ship one.
``` ```
# dnf install kernel-devel pam-devel libxcb-devel zig xorg-x11-xauth xorg-x11-server brightnessctl # dnf install kernel-devel pam-devel libxcb-devel zig xorg-x11-xauth xorg-x11-server brightnessctl
@@ -58,22 +50,16 @@ Join us on Matrix over at [#ly-dm:matrix.org](https://matrix.to/#/#ly-dm:matrix.
## Support ## Support
Every environment that works on other login managers also should work on Ly. Ly has been tested with a wide variety of desktop environments and window
managers, all of which you can find in the sections below:
- Unlike most login managers Ly has an xinitrc and shell entry. [Wayland environments](#supported-wayland-environments)
- If you installed your favorite environment and you don't see it, that's because Ly doesn't automatically refresh itself. To fix this you should restart Ly service (depends on your init system) or the easy way is to reboot your system. [X11 environments](#supported-x11-environments)
- If your environment is still missing then check at `/usr/share/xsessions` or `/usr/share/wayland-sessions` to see if a .desktop file is present.
- If there isn't a .desktop file then create a new one at `/etc/ly/custom-sessions` that launches your favorite environment. These .desktop files can be only seen by Ly and if you want them system-wide you also can create at those directories instead.
- If Xorg sessions don't work then check if your distro compiles Ly with Xorg.
Logs are defined by `/etc/ly/config.ini`: Logs are defined by `/etc/ly/config.ini`:
- The session log is located at `~/.local/state/ly-session.log` by default. - The session log is located at `~/.local/state/ly-session.log` by default.
- The system log is located at `/var/log/ly.log` by default. - The system log is located at `/var/log/ly.log` by default.
## Manually building ## Manually building
@@ -86,19 +72,23 @@ $ cd ly
$ zig build $ zig build
``` ```
After building, you can (optionally) test Ly in a terminal emulator, although authentication will **not** work: After building, you can (optionally) test Ly in a terminal emulator, although
authentication will **not** work:
``` ```
$ zig build run $ zig build run
``` ```
> [!IMPORTANT] **Important**: While you can also run Ly in a terminal emulator as root, it is
> While you can run Ly in a terminal emulator as root, it is **not** recommended. If you want to test Ly, please enable its service (as described below) and reboot your machine. **not** recommended either. If you want to properly test Ly, please enable its
service (as described below) and reboot your machine.
The next sections will explain how to use Ly with a variety of init systems. Detailed explanation is only given for systemd, but should be applicable for all. The following sections show how to install Ly for a particular init system.
Because the procedure is very similar for all of them, the commands will only
be detailed for the first section (which is about systemd).
> [!NOTE] **Note**: All following sections will assume you are using LightDM for
> All following sections will assume you are using LightDM for convenience sake. convenience sake.
### systemd ### systemd
@@ -108,10 +98,11 @@ Now, you can install Ly on your system:
# zig build installexe -Dinit_system=systemd # zig build installexe -Dinit_system=systemd
``` ```
> [!NOTE] **Note**: The `init_system` parameter is optional and defaults to `systemd`.
> The `init_system` parameter is optional and defaults to `systemd`.
Note that you also need to disable your current display manager. For example, if you are using LightDM, you can execute the following command: Note that you also need to disable your current display manager. For example,
if LightDM is the current display manager, you can execute the following
command:
``` ```
# systemctl disable lightdm.service # systemctl disable lightdm.service
@@ -123,20 +114,22 @@ Then, similarly to the previous command, you need to enable the Ly service:
# systemctl enable ly@tty2.service # systemctl enable ly@tty2.service
``` ```
> [!IMPORTANT] **Important**: Because Ly runs in a TTY, you **must** disable the TTY service
> Because Ly runs in a TTY, you **must** disable the TTY service that Ly will run on, otherwise bad things will happen. For example, to disable `getty` spawning on TTY 2, you need to execute the following command: that Ly will run on, otherwise bad things will happen. For example, to disable `getty` spawning on TTY 2, you need to execute the following command:
``` ```
# systemctl disable getty@tty2.service # systemctl disable getty@tty2.service
``` ```
On platforms that use systemd-logind to dynamically start `autovt@.service` instances when the switch to a new tty occurs, any ly instances for ttys _except the default tty_ need to be enabled using a different mechanism: To autostart ly on switch to `tty2`, do not enable any `ly` unit directly, instead symlink `autovt@tty2.service` to `ly@tty2.service` within `/usr/lib/systemd/system/` (analogous for every other tty you want to enable ly on). On platforms that use systemd-logind to dynamically start `autovt@.service` instances when the switch to a new tty occurs, any ly instances for ttys *except the default tty* need to be enabled using a different mechanism: To autostart ly on switch to `tty2`, do not enable any `ly` unit directly, instead symlink `autovt@tty2.service` to `ly@tty2.service` within `/usr/lib/systemd/system/` (analogous for every other tty you want to enable ly on).
The target of the symlink, `ly@ttyN.service`, does not actually exist, but systemd nevertheless recognizes that the instanciation of `autovt@.service` with `%I` equal to `ttyN` now points to an instanciation of `ly@.service` with `%I` set to `ttyN`. The target of the symlink, `ly@ttyN.service`, does not actually exist, but systemd nevertheless recognizes that the instanciation of `autovt@.service` with `%I` equal to `ttyN` now points to an instanciation of `ly@.service` with `%I` set to `ttyN`.
Compare to `man 5 logind.conf`, especially regarding the `NAutoVTs=` and `ReserveVT=` parameters. Compare to `man 5 logind.conf`, especially regarding the `NAutoVTs=` and `ReserveVT=` parameters.
On non-systemd systems, you can change the TTY Ly will run on by editing the corresponding service file for your platform.
On non-systemd systems, you can change the TTY Ly will run on by editing the corresponding
service file for your platform.
### OpenRC ### OpenRC
@@ -147,8 +140,8 @@ On non-systemd systems, you can change the TTY Ly will run on by editing the cor
# rc-update del agetty.tty2 # rc-update del agetty.tty2
``` ```
> [!NOTE] **Note**: On Gentoo specifically, you also **must** comment out the appropriate
> On Gentoo specifically, you also **must** comment out the appropriate line for the TTY in /etc/inittab. line for the TTY in /etc/inittab.
### runit ### runit
@@ -179,7 +172,8 @@ To disable TTY 2, edit `/etc/s6/config/tty2.conf` and set `SPAWN="no"`.
# dinitctl enable ly # dinitctl enable ly
``` ```
To disable TTY 2, go to `/etc/dinit.d/config/console.conf` and modify `ACTIVE_CONSOLES`. To disable TTY 2, go to `/etc/dinit.d/config/console.conf` and modify
`ACTIVE_CONSOLES`.
### sysvinit ### sysvinit
@@ -206,7 +200,8 @@ Ly:\
:al=root: :al=root:
``` ```
Then, modify the command field of the `ttyv1` terminal entry in `/etc/ttys` (TTYs in FreeBSD start at 0): Then, modify the command field of the `ttyv1` terminal entry in `/etc/ttys`
(TTYs in FreeBSD start at 0):
``` ```
ttyv1 "/usr/libexec/getty Ly" xterm on secure ttyv1 "/usr/libexec/getty Ly" xterm on secure
@@ -214,27 +209,37 @@ ttyv1 "/usr/libexec/getty Ly" xterm on secure
### Updating ### Updating
You can also install Ly without overrding the current configuration file. This is called **updating**. To update, simply run: You can also install Ly without overrding the current configuration file. This
is called **updating**. To update, simply run:
``` ```
# zig build installnoconf # zig build installnoconf
``` ```
You can, of course, still select the init system of your choice when using this command. You can, of course, still select the init system of your choice when using this
command.
## Configuration ## Configuration
You can find all the configuration in `/etc/ly/config.ini`. The file is fully commented, and includes the default values. You can find all the configuration in `/etc/ly/config.ini`. The file is fully
commented, and includes the default values.
## Controls ## Controls
Use the Up/Down arrow keys to change the current field, and the Left/Right arrow keys to scroll through the different fields (whether it be the info line, the desktop environment, or the username). The info line is where messages and errors are displayed. Use the Up/Down arrow keys to change the current field, and the Left/Right
arrow keys to scroll through the different fields (whether it be the info line,
the desktop environment, or the username). The info line is where messages and
errors are displayed.
## A note on .xinitrc ## A note on .xinitrc
If your `.xinitrc` file doesn't work ,make sure it is executable and includes a shebang. This file is supposed to be a shell script! Quoting from `xinit`'s man page: If your `.xinitrc` file doesn't work ,make sure it is executable and includes a
shebang. This file is supposed to be a shell script! Quoting from `xinit`'s man
page:
> If no specific client program is given on the command line, xinit will look for a file in the user's home directory called .xinitrc to run as a shell script to start up client programs. > If no specific client program is given on the command line, xinit will look
> for a file in the user's home directory called .xinitrc to run as a shell
> script to start up client programs.
A typical shebang for a shell script looks like this: A typical shebang for a shell script looks like this:
@@ -245,18 +250,47 @@ A typical shebang for a shell script looks like this:
## Tips ## Tips
- The numlock and capslock state is printed in the top-right corner. - The numlock and capslock state is printed in the top-right corner.
- Use the F1 and F2 keys to respectively shutdown and reboot. - Use the F1 and F2 keys to respectively shutdown and reboot.
- Take a look at your `.xsession` file if X doesn't start, as it can interfere
(this file is launched with X to configure the display properly).
- Take a look at your `.xsession` file if X doesn't start, as it can interfere (this file is launched with X to configure the display properly). ## Supported Wayland environments
- budgie
- cosmic
- deepin
- enlightenment
- gnome
- hyprland
- kde
- labwc
- niri
- pantheon
- sway
- weston
## Supported X11 environments
- awesome
- bspwm
- budgie
- cinnamon
- dwm
- enlightenment
- gnome
- kde
- leftwm
- lxde
- mate
- maxx
- pantheon
- qwm
- spectrwm
- windowmaker
- xfce
- xmonad
## A final note ## A final note
The name "Ly" is a tribute to the fairy from the game Rayman. Ly was tested by oxodao, who is some seriously awesome dude. The name "Ly" is a tribute to the fairy from the game Rayman. Ly was tested by
oxodao, who is some seriously awesome dude.
Also, Ly wouldn't be there today without [ashametrine](https://github.com/ashametrine), who has done significant contributions to the project for the Zig rewrite, which lead to the release of Ly v1.0.0. Massive thanks, and sorry for not crediting you enough beforehand!
### Donate
If you like Ly and wish to support my work further, feel free to donate via my
[Liberapay link](https://liberapay.com/ShiningLea)!

View File

@@ -27,9 +27,6 @@ allow_empty_password = true
# dur_file -> .dur file format (https://github.com/cmang/durdraw/tree/master) # dur_file -> .dur file format (https://github.com/cmang/durdraw/tree/master)
animation = none animation = none
# Delay between each animation frame in milliseconds
animation_frame_delay = 5
# Stop the animation after some time # Stop the animation after some time
# 0 -> Run forever # 0 -> Run forever
# 1..2e12 -> Stop the animation after this many seconds # 1..2e12 -> Stop the animation after this many seconds
@@ -65,7 +62,7 @@ auto_login_service = ly-autologin
# To find available session names, check the .desktop files in: # To find available session names, check the .desktop files in:
# - /usr/share/xsessions/ (for X11 sessions) # - /usr/share/xsessions/ (for X11 sessions)
# - /usr/share/wayland-sessions/ (for Wayland sessions) # - /usr/share/wayland-sessions/ (for Wayland sessions)
# Use the filename without .desktop extension, the Name field inside the file or the value of the DesktopNames field # Use the filename without .desktop extension, or the value of DesktopNames field
# Examples: "i3", "sway", "gnome", "plasma", "xfce" # Examples: "i3", "sway", "gnome", "plasma", "xfce"
# If null, automatic login is disabled # If null, automatic login is disabled
auto_login_session = null auto_login_session = null
@@ -104,13 +101,13 @@ box_title = null
# Brightness decrease command # Brightness decrease command
brightness_down_cmd = $PREFIX_DIRECTORY/bin/brightnessctl -q -n s 10%- brightness_down_cmd = $PREFIX_DIRECTORY/bin/brightnessctl -q -n s 10%-
# Brightness decrease key combination, or null to disable # Brightness decrease key, or null to disable
brightness_down_key = F5 brightness_down_key = F5
# Brightness increase command # Brightness increase command
brightness_up_cmd = $PREFIX_DIRECTORY/bin/brightnessctl -q -n s +10% brightness_up_cmd = $PREFIX_DIRECTORY/bin/brightnessctl -q -n s +10%
# Brightness increase key combination, or null to disable # Brightness increase key, or null to disable
brightness_up_key = F6 brightness_up_key = F6
# Erase password input on failure # Erase password input on failure
@@ -143,11 +140,6 @@ colormix_col2 = 0x000000FF
# Color mixing animation third color id # Color mixing animation third color id
colormix_col3 = 0x20000000 colormix_col3 = 0x20000000
# For custom binds: the horizontal limit in characters for each
# line of custom binds before moving on to the next.
# If null, defaults to the width of the terminal instead.
custom_bind_width = null
# Custom sessions directory # Custom sessions directory
# You can specify multiple directories, # You can specify multiple directories,
# e.g. $CONFIG_DIRECTORY/ly/custom-sessions:$PREFIX_DIRECTORY/share/custom-sessions # e.g. $CONFIG_DIRECTORY/ly/custom-sessions:$PREFIX_DIRECTORY/share/custom-sessions
@@ -175,15 +167,10 @@ doom_bottom_color = 0x00FFFFFF
# Dur file path # Dur file path
dur_file_path = $CONFIG_DIRECTORY/ly/example.dur dur_file_path = $CONFIG_DIRECTORY/ly/example.dur
# Dur file alignment # Dur offset x direction
# The dur file can be aligned with a direction and centered easily with the flags below
# Available inputs: topleft, topcenter, topright, centerleft, center, centerright, bottomleft, bottomcenter, bottomright
dur_offset_alignment = center
# Dur offset x direction (value is added to the current position determined by alignment, negatives are supported)
dur_x_offset = 0 dur_x_offset = 0
# Dur offset y direction (value is added to the current position determined by alignment, negatives are supported) # Dur offset y direction
dur_y_offset = 0 dur_y_offset = 0
# Set margin to the edges of the DM (useful for curved monitors) # Set margin to the edges of the DM (useful for curved monitors)
@@ -241,7 +228,7 @@ gameoflife_initial_density = 0.4
# Command executed when pressing hibernate key (can be null) # Command executed when pressing hibernate key (can be null)
hibernate_cmd = null hibernate_cmd = null
# Specifies the key combination used for hibernate # Specifies the key used for hibernate (F1-F12)
hibernate_key = F4 hibernate_key = F4
# Remove main box borders # Remove main box borders
@@ -299,6 +286,9 @@ margin_box_h = 2
# Main box vertical margin # Main box vertical margin
margin_box_v = 1 margin_box_v = 1
# Event timeout in milliseconds
min_refresh_delta = 5
# Set numlock on/off at startup # Set numlock on/off at startup
numlock = false numlock = false
@@ -309,7 +299,7 @@ path = /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
# Command executed when pressing restart_key # Command executed when pressing restart_key
restart_cmd = /sbin/shutdown -r now restart_cmd = /sbin/shutdown -r now
# Specifies the key combination used for restart # Specifies the key used for restart (F1-F12)
restart_key = F2 restart_key = F2
# Save the current desktop and login as defaults, and load them on startup # Save the current desktop and login as defaults, and load them on startup
@@ -321,42 +311,29 @@ service_name = ly
# Session log file path # Session log file path
# This will contain stdout and stderr of Wayland sessions # This will contain stdout and stderr of Wayland sessions
# By default it's saved in the user's home directory # By default it's saved in the user's home directory
# Important: due to technical limitations, X11, shell sessions as well as # Important: due to technical limitations, X11 and shell sessions aren't supported, which
# launching session via KMSCON aren't supported, which means you won't get any # means you won't get any logs from those sessions.
# logs from those sessions.
# If null, no session log will be created # If null, no session log will be created
session_log = .local/state/ly-session.log session_log = .local/state/ly-session.log
# Setup command # Setup command
setup_cmd = $CONFIG_DIRECTORY/ly/setup.sh setup_cmd = $CONFIG_DIRECTORY/ly/setup.sh
# Show the shell session in the session list
# If false, the shell session will be hidden
shell = true
# Specifies the key combination used for showing the password
show_password_key = F7
# Display the active TTY number (e.g. tty3) to the right of the clock in the top right corner
# If the clock is disabled, the TTY label occupies the top right corner on its own
# If false, the TTY number will not be shown
show_tty = false
# Command executed when pressing shutdown_key # Command executed when pressing shutdown_key
shutdown_cmd = /sbin/shutdown $PLATFORM_SHUTDOWN_ARG now shutdown_cmd = /sbin/shutdown $PLATFORM_SHUTDOWN_ARG now
# Specifies the key combination used for shutdown # Specifies the key used for shutdown (F1-F12)
shutdown_key = F1 shutdown_key = F1
# Command executed when pressing sleep key (can be null) # Command executed when pressing sleep key (can be null)
sleep_cmd = null sleep_cmd = null
# Specifies the key combination used for sleep # Specifies the key used for sleep (F1-F12)
sleep_key = F3 sleep_key = F3
# Command executed when starting Ly (before the TTY is taken control of) # Command executed when starting Ly (before the TTY is taken control of)
# See file at path below for an example of changing the default TTY colors # If null, no command will be executed
start_cmd = $CONFIG_DIRECTORY/ly/startup.sh start_cmd = null
# Center the session name. # Center the session name.
text_in_center = false text_in_center = false
@@ -372,18 +349,11 @@ vi_mode = false
# Wayland desktop environments # Wayland desktop environments
# You can specify multiple directories, # You can specify multiple directories,
# e.g. $PREFIX_DIRECTORY/share/wayland-sessions:$PREFIX_DIRECTORY/local/share/wayland-sessions # e.g. $PREFIX_DIRECTORY/share/wayland-sessions:$PREFIX_DIRECTORY/local/share/wayland-sessions
# If null, Wayland sessions will not be shown
waylandsessions = $PREFIX_DIRECTORY/share/wayland-sessions waylandsessions = $PREFIX_DIRECTORY/share/wayland-sessions
# Xorg server command # Xorg server command
# Add the -quiet argument to hide startup logs from the server
x_cmd = $PREFIX_DIRECTORY/bin/X x_cmd = $PREFIX_DIRECTORY/bin/X
# Xorg virtual terminal number
# Mostly useful for FreeBSD where choosing the current TTY causes issues
# If null, the current TTY will be chosen
x_vt = null
# Xorg xauthority edition tool # Xorg xauthority edition tool
xauth_cmd = $PREFIX_DIRECTORY/bin/xauth xauth_cmd = $PREFIX_DIRECTORY/bin/xauth
@@ -394,29 +364,4 @@ xinitrc = ~/.xinitrc
# Xorg desktop environments # Xorg desktop environments
# You can specify multiple directories, # You can specify multiple directories,
# e.g. $PREFIX_DIRECTORY/share/xsessions:$PREFIX_DIRECTORY/local/share/xsessions # e.g. $PREFIX_DIRECTORY/share/xsessions:$PREFIX_DIRECTORY/local/share/xsessions
# If null, X11 sessions will not be shown
xsessions = $PREFIX_DIRECTORY/share/xsessions xsessions = $PREFIX_DIRECTORY/share/xsessions
# Custom Commands and Labels:
# The following examples below give an outline for setting up custom commands and labels.
# Unless specified as optional, an option is mandatory.
# Comments preceding with '##' are for documentation.
# Comments preceding with '#' comment out the example INI.
## Declare a command with the F8 binding.
#[cmd:F8]
## The name of the command to show up in Ly.
## Note: "$" in "$brightness_up" fetches the appropriate string from the specified locale file
## and is replaced with the value representing "brightness_up".
## You can see the list of keys in any locale file in $CONFIG_DIRECTORY/ly/lang.
#cmd = touch /tmp/ly.gaming
#name = custom command $brightness_up
## Declare a label with an ID. This ID should be unique across all labels.
#[lbl:kernel]
#cmd = uname -srn
## Optional, defaulting to 0.
## In frames, the time to re-run the command and update the label.
## If 0, only run once and do not refresh afterwards
#refresh = 0

View File

@@ -17,7 +17,7 @@ redirected to the session log file found in Ly's configuration file. If set to
true, Ly will consider the program is going to run in a TTY, and thus will not true, Ly will consider the program is going to run in a TTY, and thus will not
redirect standard output & error. It is optional and defaults to false. redirect standard output & error. It is optional and defaults to false.
Finally, do note that if the Terminal value is set to true, the Finally, do note that, if the Terminal value is set to true, the
XDG_SESSION_TYPE environment variable will be set to "tty". Otherwise, it will XDG_SESSION_TYPE environment variable will be set to "tty". Otherwise, it will
be set to "unspecified" (without quotes), which is behavior that at least be set to "unspecified" (without quotes), which is behavior that at least
systemd recognizes (see pam_systemd's man page). systemd recognizes (see pam_systemd's man page).

Binary file not shown.

View File

@@ -3,9 +3,6 @@ brightness_down = خفض السطوع
brightness_up = رفع السطوع brightness_up = رفع السطوع
capslock = capslock capslock = capslock
err_alloc = فشل في تخصيص الذاكرة err_alloc = فشل في تخصيص الذاكرة
@@ -76,7 +73,6 @@ restart = اعادة التشغيل
shell = shell shell = shell
shutdown = ايقاف التشغيل shutdown = ايقاف التشغيل
sleep = وضع السكون sleep = وضع السكون
wayland = wayland wayland = wayland
x11 = x11 x11 = x11
xinitrc = xinitrc xinitrc = xinitrc

View File

@@ -3,9 +3,6 @@ brightness_down = намаляване на яркостта
brightness_up = увеличаване на яркостта brightness_up = увеличаване на яркостта
capslock = caps lock capslock = caps lock
custom = персонализирано custom = персонализирано
custom_info_err_output_long = резултатът е твърде дълъг
custom_info_err_no_output = няма резултат
custom_info_err_no_output_error = , възможна грешка
err_alloc = неуспешно заделяне на памет err_alloc = неуспешно заделяне на памет
err_args = неуспешен анализ на аргументите от командния ред err_args = неуспешен анализ на аргументите от командния ред
err_autologin_session = сесията за автоматично влизане не е намерена err_autologin_session = сесията за автоматично влизане не е намерена
@@ -76,7 +73,6 @@ restart = рестартиране
shell = обвивка shell = обвивка
shutdown = изключване shutdown = изключване
sleep = заспиване sleep = заспиване
toggle_password = превключване на паролата
wayland = wayland wayland = wayland
x11 = x11 x11 = x11
xinitrc = xinitrc xinitrc = xinitrc

View File

@@ -3,9 +3,6 @@ brightness_down = abaixar brillantor
brightness_up = apujar brillantor brightness_up = apujar brillantor
capslock = Bloq Majús capslock = Bloq Majús
err_alloc = assignació de memòria fallida err_alloc = assignació de memòria fallida
@@ -76,7 +73,6 @@ restart = reiniciar
shell = shell shell = shell
shutdown = aturar shutdown = aturar
sleep = suspendre sleep = suspendre
wayland = wayland wayland = wayland
x11 = x11 x11 = x11
xinitrc = xinitrc xinitrc = xinitrc

View File

@@ -3,9 +3,6 @@
capslock = capslock capslock = capslock
err_alloc = alokace paměti selhala err_alloc = alokace paměti selhala
@@ -76,7 +73,6 @@ restart = restartovat
shell = příkazový řádek shell = příkazový řádek
shutdown = vypnout shutdown = vypnout
wayland = wayland wayland = wayland
xinitrc = xinitrc xinitrc = xinitrc

View File

@@ -3,9 +3,6 @@ brightness_down = Helligkeit-
brightness_up = Helligkeit+ brightness_up = Helligkeit+
capslock = Feststelltaste capslock = Feststelltaste
err_alloc = Speicherzuweisung fehlgeschlagen err_alloc = Speicherzuweisung fehlgeschlagen
@@ -76,7 +73,6 @@ restart = Neustarten
shell = Shell shell = Shell
shutdown = Herunterfahren shutdown = Herunterfahren
sleep = Sleep sleep = Sleep
wayland = wayland wayland = wayland
x11 = X11 x11 = X11
xinitrc = xinitrc xinitrc = xinitrc

View File

@@ -3,9 +3,6 @@ brightness_down = decrease brightness
brightness_up = increase brightness brightness_up = increase brightness
capslock = capslock capslock = capslock
custom = custom custom = custom
custom_info_err_output_long = output too long
custom_info_err_no_output = no output
custom_info_err_no_output_error = , possible error
err_alloc = failed memory allocation err_alloc = failed memory allocation
err_args = unable to parse command line arguments err_args = unable to parse command line arguments
err_autologin_session = autologin session not found err_autologin_session = autologin session not found
@@ -76,7 +73,6 @@ restart = reboot
shell = shell shell = shell
shutdown = shutdown shutdown = shutdown
sleep = sleep sleep = sleep
toggle_password = toggle password
wayland = wayland wayland = wayland
x11 = x11 x11 = x11
xinitrc = xinitrc xinitrc = xinitrc

View File

@@ -1,82 +0,0 @@
authenticating = aŭtentigado...
brightness_down = malpliigi helecon
brightness_up = pliigi helecon
capslock = majuskla baskulo
custom = propra
err_alloc = malsukcesis memorasignon
err_args = ne povas analizi argumentojn de komanda linio
err_autologin_session = aŭtomatan ensalutan seancon ne trovis
err_bounds = indico estas ekster-intervala
err_brightness_change = malsukcesis ŝanĝi la helecon
err_chdir = malsukcesis malfermi hejman dosierujon
err_clock_too_long = horloĝa ĉeno estas tro longa
err_config = ne povas analizi agordan dosieron
err_crawl = malsukcesis dum serĉado de seancaj dosierujoj
err_dgn_oob = protokola mesaĝo
err_domain = malvalida domajno
err_empty_password = ne akceptas malplenan pasvorton
err_envlist = malsukcesis preni la medivariablojn
err_get_active_tty = malsukcesis preni la aktivan TTY-on
err_hibernate = malsukcesis ruli la komandon por diskodormo
err_hostname = malsukcesis preni la sistemnomon
err_inactivity = malsukcesis ruli la agorditan komandon por malaktiveco
err_lock_state = malsukcesis preni la ŝlosan staton
err_log = malsukcesis malfermi la protokolan dosieron
err_mlock = malsukcesis ŝlosi pasvortan memoron
err_null = nula memorloko
err_numlock = malsukcesis agordi numeran baskulon
err_pam = PAM-a transakcio malsukcesis
err_pam_abort = PAM-a transakcio malsukcesis
err_pam_acct_expired = konto eksvalidiĝis
err_pam_auth = aŭtentiga eraro
err_pam_authinfo_unavail = malsukcesis preni uzantajn informojn
err_pam_authok_reqd = memorsigno eksvalidiĝis
err_pam_buf = bufra eraro
err_pam_cred_err = malsukcesis agordi akreditaĵon
err_pam_cred_expired = akreditaĵo eksvalidiĝis
err_pam_cred_insufficient = nesufiĉa akreditaĵo
err_pam_cred_unavail = malsukcesis preni akreditaĵon
err_pam_maxtries = atingis maksimuman kvanton da provoj
err_pam_perm_denied = permeso negis
err_pam_session = seancan eraron
err_pam_sys = sisteman eraron
err_pam_user_unknown = ne konas uzanton
err_path = malsukcesis agordi la median dosierindikon
err_perm_dir = malsukcesis ŝanĝi la nunan dosierujon
err_perm_group = malsukcesis redukti grupajn permesojn
err_perm_user = malsukcesis redukti uzantajn permesojn
err_pwnam = malsukcesis preni uzantajn informojn
err_sleep = malsukcesis ruli memordorman komandon
err_start = malsukcesis ruli startan komandon
err_battery = malsukcesis ŝargi baterian staton
err_switch_tty = malsukcesis ŝanĝi TTY-on
err_tty_ctrl = TTY-an stiran transigon malsukcesis
err_no_users = nul uzantojn trovas
err_uid_range = malsukcesis dinamike preni UID-an intervalon
err_user_gid = malsukcesis agordi uzantan GID-on
err_user_init = malsukcesis iniciĝi uzanto
err_user_uid = malsukcesis agordi uzantan UID-on
err_xauth = malsukcesis plenumi je xauth
err_xcb_conn = malsukcesis dum konectado al xcb
err_xsessions_dir = malsukcesis trovi seancan dosierujon
err_xsessions_open = malsukcesis malfermi seancan dosierujon
hibernate = diskodormi
insert = enmeti
login = uzanto
logout = elsalutis
no_x11_support = x11 estas foriĝita de kompil-tempo
normal = normala
numlock = numera baskulo
other = alia
password = pasvorto
restart = restartigi
shell = ŝelo
shutdown = malŝalti
sleep = memordormi
wayland = wayland
x11 = x11
xinitrc = xinitrc

View File

@@ -3,9 +3,6 @@ brightness_down = bajar brillo
brightness_up = subir brillo brightness_up = subir brillo
capslock = Bloq Mayús capslock = Bloq Mayús
err_alloc = asignación de memoria fallida err_alloc = asignación de memoria fallida
@@ -76,7 +73,6 @@ restart = reiniciar
shell = shell shell = shell
shutdown = apagar shutdown = apagar
sleep = suspender sleep = suspender
wayland = wayland wayland = wayland
xinitrc = xinitrc xinitrc = xinitrc

View File

@@ -3,9 +3,6 @@ brightness_down = diminuer la luminosité
brightness_up = augmenter la luminosité brightness_up = augmenter la luminosité
capslock = verr.maj capslock = verr.maj
custom = customisé custom = customisé
custom_info_err_output_long = sortie trop longue
custom_info_err_no_output = pas de sortie
custom_info_err_no_output_error = , erreur possible
err_alloc = échec d'allocation mémoire err_alloc = échec d'allocation mémoire
err_args = échec de l'analyse des arguments en lignes de commande err_args = échec de l'analyse des arguments en lignes de commande
err_autologin_session = session de connexion automatique introuvable err_autologin_session = session de connexion automatique introuvable
@@ -76,7 +73,6 @@ restart = redémarrer
shell = shell shell = shell
shutdown = éteindre shutdown = éteindre
sleep = veille sleep = veille
toggle_password = afficher le mot de passe
wayland = wayland wayland = wayland
x11 = x11 x11 = x11
xinitrc = xinitrc xinitrc = xinitrc

View File

@@ -3,9 +3,6 @@
capslock = capslock capslock = capslock
err_alloc = impossibile allocare memoria err_alloc = impossibile allocare memoria
@@ -76,7 +73,6 @@ restart = riavvio
shell = shell shell = shell
shutdown = arresto shutdown = arresto
wayland = wayland wayland = wayland
xinitrc = xinitrc xinitrc = xinitrc

View File

@@ -3,9 +3,6 @@ brightness_down = 明るさを下げる
brightness_up = 明るさを上げる brightness_up = 明るさを上げる
capslock = CapsLock capslock = CapsLock
err_alloc = メモリ割り当て失敗 err_alloc = メモリ割り当て失敗
@@ -76,7 +73,6 @@ restart = 再起動
shell = シェル shell = シェル
shutdown = シャットダウン shutdown = シャットダウン
sleep = スリープ sleep = スリープ
wayland = Wayland wayland = Wayland
x11 = X11 x11 = X11
xinitrc = xinitrc xinitrc = xinitrc

View File

@@ -1,82 +0,0 @@
authenticating = tê piştrastkirin...
brightness_down = ronahiyê kêm bike
brightness_up = ronahiyê bilind bike
capslock = tîpên girdek (capslock)
custom = kesane
err_alloc = veqetandina bîrê têk çû
err_args = argumanên rêzika fermanê nehatin analîzkirin
err_autologin_session = danişîna têketina xweber nehate dîtin
err_bounds = îndeksa derveyî sînor
err_brightness_change = guherandina ronahiyê têk çû
err_chdir = vekirina peldanka malê têk çû
err_clock_too_long = rêzika demjimêrê pir dirêj e
err_config = pela rêkxistinê nehat analîzkirin
err_crawl = gerandina pelrêçên danişînê têk çû
err_dgn_oob = peyama têketinê
err_domain = navpara nederbasdar
err_empty_password = borînpeyv nabe ku vala be
err_envlist = girtina lîsteya jîngehê (envlist) têk çû
err_get_active_tty = girtina tty ya çalak têk çû
err_hibernate = fermana cemidaninê nehat xebitandin
err_hostname = girtina navê mêvandar têk çû
err_inactivity = fermana neçalaktiyê nehat xebitandin
err_lock_state = girtina rewşa kilîtkirinê têk çû
err_log = vekirina pelê têkeinê têk çû
err_mlock = kilîtkirina bîra borînpeyvê têk çû
err_null = nîşandera null
err_numlock = sazkirina numlock têk çû
err_pam = danûstendina pam têk çû
err_pam_abort = danûstendina pam hate têkbirin
err_pam_acct_expired = dema jimarê derbas bûye
err_pam_auth = şaşetiya piştrastkirinê
err_pam_authinfo_unavail = zanyariyên bikarhêner nehatin girtin
err_pam_authok_reqd = dema nîşandanê derbas bûye
err_pam_buf = şaşetiya bîra demkî
err_pam_cred_err = sazkirina rastkitinê têk çû
err_pam_cred_expired = dema rastkitinê derbas bûye
err_pam_cred_insufficient = rastkitinê kêm
err_pam_cred_unavail = girtina rastkitinê têk çû
err_pam_maxtries = sînorê hewldanên herî bilind hat gihîştin
err_pam_perm_denied = mafdayîn hat paşguhkirin
err_pam_session = şaşetiya danişînê
err_pam_sys = şaşetiya pergalê
err_pam_user_unknown = bikarhênerê nenas
err_path = sazkirina rêgehê têk çû
err_perm_dir = guhertina pelrêçê heyî têk çû
err_perm_group = kêmkirina mafdayînên komê têk çû
err_perm_user = kêmkirina mafdayînên bikarhêner têk çû
err_pwnam = girtina zanyariyên bikarhêner têk çû
err_sleep = fermana cemidaninê nehat xebitandin
err_start = fermana destpêkirinê nehat xebitandin
err_battery = barkirina rewşa betariyê têk çû
err_switch_tty = guhertina tty têk çû
err_tty_ctrl = guhertina kontrola tty têk çû
err_no_users = tu bikarhêner nehatin dîtin
err_uid_range = girtina rêjeya dînamîk a sînorê uid têk çû
err_user_gid = sazkirina GID a bikarhêner têk çû
err_user_init = destpêkirina bikarhêner têk çû
err_user_uid = sazkirina UID a bikarhêner têk çû
err_xauth = fermana xauth têk çû
err_xcb_conn = girêdana xcb têk çû
err_xsessions_dir = dîtina peldanka danişînan têk çû
err_xsessions_open = vekirina peldanka danişînan têk çû
hibernate = bicemidîne
insert = têxîne
login = têketin
logout = derkeve
no_x11_support = piştgiriya x11 di dema berhevkirinê de hatiye girtin
normal = normal
numlock = numlock
other = ên din
password = borînpeyv
restart = ji nû ve bide destpêkirin
shell = shell
shutdown = vemirîne
sleep = têxîne xewê
wayland = wayland
x11 = x11
xinitrc = xinitrc

View File

@@ -3,9 +3,6 @@ brightness_down = samazināt spilgtumu
brightness_up = palielināt spilgtumu brightness_up = palielināt spilgtumu
capslock = caps lock capslock = caps lock
custom = pielāgots custom = pielāgots
err_alloc = neizdevās atmiņas piešķiršana err_alloc = neizdevās atmiņas piešķiršana
@@ -76,7 +73,6 @@ restart = restartēt
shell = terminālis shell = terminālis
shutdown = izslēgt shutdown = izslēgt
sleep = snauda sleep = snauda
wayland = wayland wayland = wayland
x11 = x11 x11 = x11
xinitrc = xinitrc xinitrc = xinitrc

View File

@@ -3,9 +3,6 @@ brightness_down = zmniejsz jasność
brightness_up = zwiększ jasność brightness_up = zwiększ jasność
capslock = capslock capslock = capslock
custom = własny custom = własny
err_alloc = nieudana alokacja pamięci err_alloc = nieudana alokacja pamięci
err_autologin_session = nie znaleziono sesji autologowania err_autologin_session = nie znaleziono sesji autologowania
@@ -76,7 +73,6 @@ restart = uruchom ponownie
shell = powłoka shell = powłoka
shutdown = wyłącz shutdown = wyłącz
sleep = uśpij sleep = uśpij
wayland = wayland wayland = wayland
x11 = x11 x11 = x11
xinitrc = xinitrc xinitrc = xinitrc

View File

@@ -3,9 +3,6 @@
capslock = capslock capslock = capslock
err_alloc = erro na atribuição de memória err_alloc = erro na atribuição de memória
@@ -76,7 +73,6 @@ restart = reiniciar
shell = shell shell = shell
shutdown = encerrar shutdown = encerrar
wayland = wayland wayland = wayland
xinitrc = xinitrc xinitrc = xinitrc

View File

@@ -3,9 +3,6 @@
capslock = caixa alta capslock = caixa alta
err_alloc = alocação de memória malsucedida err_alloc = alocação de memória malsucedida
@@ -76,7 +73,6 @@ restart = reiniciar
shell = shell shell = shell
shutdown = desligar shutdown = desligar
wayland = wayland wayland = wayland
xinitrc = xinitrc xinitrc = xinitrc

View File

@@ -22,9 +22,6 @@ capslock = capslock
@@ -76,7 +73,6 @@ restart = resetează
shell = shell shell = shell
shutdown = opreşte sistemul shutdown = opreşte sistemul
wayland = wayland wayland = wayland
xinitrc = xinitrc xinitrc = xinitrc

View File

@@ -3,9 +3,6 @@ brightness_down = уменьшить яркость
brightness_up = увеличить яркость brightness_up = увеличить яркость
capslock = capslock capslock = capslock
custom = пользовательский custom = пользовательский
err_alloc = не удалось выделить память err_alloc = не удалось выделить память
err_autologin_session = не найдена сессия с автологином err_autologin_session = не найдена сессия с автологином
@@ -76,7 +73,6 @@ restart = перезагрузить
shell = оболочка shell = оболочка
shutdown = выключить shutdown = выключить
sleep = сон sleep = сон
wayland = wayland wayland = wayland
x11 = x11 x11 = x11
xinitrc = xinitrc xinitrc = xinitrc

View File

@@ -3,9 +3,6 @@
capslock = capslock capslock = capslock
err_alloc = neuspijesna alokacija memorije err_alloc = neuspijesna alokacija memorije
@@ -76,7 +73,6 @@ restart = ponovo pokreni
shell = shell shell = shell
shutdown = ugasi shutdown = ugasi
wayland = wayland wayland = wayland
xinitrc = xinitrc xinitrc = xinitrc

View File

@@ -1,82 +1,78 @@
authenticating = autentiserar...
brightness_down = minska ljusstyrka
brightness_up = öka ljusstyrka
capslock = capslock capslock = capslock
custom = anpassad
err_alloc = misslyckad minnesallokering
err_bounds = utanför banan index
err_alloc = minnesallokering misslyckades
err_args = tolkning av kommandoargument misslyckades
err_autologin_session = autologin-session hittades inte
err_bounds = index-värde utanför intervallet
err_brightness_change = ändring av ljusstyrka misslyckades
err_chdir = misslyckades att öppna hemkatalog err_chdir = misslyckades att öppna hemkatalog
err_clock_too_long = klocksträng för lång
err_config = tolkning av konfigfil misslyckades
err_crawl = genomsökning av sessionskataloger misslyckades
err_dgn_oob = loggmeddelande err_dgn_oob = loggmeddelande
err_domain = ogitlig domän err_domain = okänd domän
err_empty_password = tomt lösenord godtas ej
err_envlist = hämtning av env-lista misslyckades
err_get_active_tty = hämtning av aktiv tty misslyckades
err_hibernate = vilolägets kommando misslyckades
err_hostname = hämtning av hostname misslyckades err_hostname = misslyckades att hämta värdnamn
err_inactivity = inaktivitetslägets kommando misslyckades
err_lock_state = hämtning av låsningsstatus misslyckades
err_log = öppning av loggfil misslyckades
err_mlock = låsning av lösenordsminne misslyckades err_mlock = misslyckades att låsa lösenordsminne
err_null = null pointer err_null = nullpekare
err_numlock = inställning av numlock misslyckades
err_pam = pam-transaktion misslyckades err_pam = pam-transaktion misslyckades
err_pam_abort = pam-transaktion avbröts err_pam_abort = pam-transaktion avbröts
err_pam_acct_expired = kontot har löpt ut err_pam_acct_expired = konto upphört
err_pam_auth = autentisering misslyckades err_pam_auth = autentiseringsfel
err_pam_authinfo_unavail = hämtning av användarinformation misslyckades err_pam_authinfo_unavail = misslyckades att hämta användarinfo
err_pam_authok_reqd = token har löpt ut err_pam_authok_reqd = token utgången
err_pam_buf = minnesbufferfel err_pam_buf = minnesbuffer fel
err_pam_cred_err = inställning av inloggningsuppgifter misslyckades err_pam_cred_err = misslyckades att ställa in inloggningsuppgifter
err_pam_cred_expired = inloggningsuppgifterna har löpt ut err_pam_cred_expired = inloggningsuppgifter upphörda
err_pam_cred_insufficient = otillräckliga inloggningsuppgifter err_pam_cred_insufficient = otillräckliga inloggningsuppgifter
err_pam_cred_unavail = hämtning av inloggningsuppgifter misslyckades err_pam_cred_unavail = misslyckades att hämta inloggningsuppgifter
err_pam_maxtries = gränsen för antal försök nådd err_pam_maxtries = nådde maximal försöksgräns
err_pam_perm_denied = tillstånd nekas err_pam_perm_denied = åtkomst nekad
err_pam_session = sessionsfel err_pam_session = sessionsfel
err_pam_sys = systemfel err_pam_sys = systemfel
err_pam_user_unknown = okänd användare err_pam_user_unknown = okänd användare
err_path = inställning av sökväg misslyckades err_path = misslyckades att ställa in sökväg
err_perm_dir = byte av nuvarande katalog misslyckades err_perm_dir = misslyckades att ändra aktuell katalog
err_perm_group = nedgradering av grupptillstånd misslyckades err_perm_group = misslyckades att nergradera gruppbehörigheter
err_perm_user = nedgradering av användartillstånd misslyckades err_perm_user = misslyckades att nergradera användarbehörigheter
err_pwnam = hämtning av användarinformation misslyckades err_pwnam = misslyckades att hämta användarinfo
err_sleep = strömsparlägets kommando misslyckades
err_start = startkommando misslyckades
err_battery = hämtning av batteristatus misslyckades
err_switch_tty = byte av tty misslyckades
err_tty_ctrl = överföring av tty-kontroll misslyckades
err_no_users = inga användare hittades
err_uid_range = dynamisk hämtning av uid-intervall misslyckades
err_user_gid = inställning av användarens GID misslyckades err_user_gid = misslyckades att ställa in användar-GID
err_user_init = initiering av användare misslyckades err_user_init = misslyckades att initialisera användaren
err_user_uid = inställning av användarens UID misslyckades err_user_uid = misslyckades att ställa in användar-UID
err_xauth = xauth-kommando misslyckades
err_xcb_conn = xcb-anslutning misslyckades
err_xsessions_dir = sessionskatalog hittades inte err_xsessions_dir = misslyckades att hitta sessionskatalog
err_xsessions_open = öppning av sessionskatalog misslyckades err_xsessions_open = misslyckades att öppna sessionskatalog
hibernate = viloläge
insert = infoga
login = inloggning login = inloggning
logout = utloggad logout = utloggad
no_x11_support = x11-stöd inaktiverat vid kompilering
normal = normal
numlock = numlock numlock = numlock
other = övrig
password = lösenord password = lösenord
restart = starta om restart = starta om
shell = shell shell = skal
shutdown = stäng av shutdown = stäng av
sleep = viloläge
wayland = wayland wayland = wayland
x11 = x11
xinitrc = xinitrc xinitrc = xinitrc

View File

@@ -1,11 +1,8 @@
brightness_down = parlakligi azalt
brightness_up = parlakligi arttir
capslock = capslock capslock = capslock
err_alloc = basarisiz bellek ayirma err_alloc = basarisiz bellek ayirma
@@ -63,19 +60,18 @@ err_user_uid = kullanici icin UID ayarlanamadi
err_xsessions_dir = oturumlar klasoru bulunamadi err_xsessions_dir = oturumlar klasoru bulunamadi
err_xsessions_open = oturumlar klasoru acilamadi err_xsessions_open = oturumlar klasoru acilamadi
hibernate = askiya al
login = kullanici login = kullanici
logout = oturumdan cikis yapildi logout = oturumdan cikis yapildi
numlock = numlock numlock = numlock
other = baska
password = sifre password = sifre
restart = yeniden baslat restart = yeniden baslat
shell = shell shell = shell
shutdown = makineyi kapat shutdown = makineyi kapat
sleep = uykuya al
wayland = wayland wayland = wayland

View File

@@ -3,9 +3,6 @@
capslock = capslock capslock = capslock
err_alloc = невдале виділення пам'яті err_alloc = невдале виділення пам'яті
@@ -76,7 +73,6 @@ restart = перезавантажити
shell = оболонка shell = оболонка
shutdown = вимкнути shutdown = вимкнути
wayland = wayland wayland = wayland
xinitrc = xinitrc xinitrc = xinitrc

View File

@@ -3,9 +3,6 @@
capslock = 大写锁定 capslock = 大写锁定
err_alloc = 内存分配失败 err_alloc = 内存分配失败
@@ -76,7 +73,6 @@ password = 密码
shell = shell shell = shell
wayland = wayland wayland = wayland
x11 = x11 x11 = x11
xinitrc = xinitrc xinitrc = xinitrc

View File

@@ -1,17 +0,0 @@
[Unit]
Description=TUI display manager using KMSCON
After=systemd-user-sessions.service plymouth-quit-wait.service
After=kmsconvt@%i.service
Conflicts=kmsconvt@%i.service
[Service]
ExecStart=$PREFIX_DIRECTORY/bin/kmscon --font-engine unifont --vt=%I --seats=seat0 --login -- $PREFIX_DIRECTORY/bin/ly --use-kmscon-vt
StandardInput=tty
UtmpIdentifier=%I
TTYPath=/dev/%I
TTYReset=yes
TTYVHangup=yes
TTYVTDisallocate=yes
[Install]
WantedBy=multi-user.target

View File

@@ -1,37 +0,0 @@
#!/bin/sh
# This file is executed when starting Ly (before the TTY is taken control of)
# Custom startup code can be placed in this file or the start_cmd var can be pointed to a different file
# Uncomment the example below for an example of changing the default TTY colors to an alternitive palette on linux
# Colors are in red/green/blue hex (the current colors are a brighter palette than default)
#
# if [ "$TERM" = "linux" ]; then
# BLACK="232323"
# DARK_RED="D75F5F"
# DARK_GREEN="87AF5F"
# DARK_YELLOW="D7AF87"
# DARK_BLUE="8787AF"
# DARK_MAGENTA="BD53A5"
# DARK_CYAN="5FAFAF"
# LIGHT_GRAY="E5E5E5"
# DARK_GRAY="2B2B2B"
# RED="E33636"
# GREEN="98E34D"
# YELLOW="FFD75F"
# BLUE="7373C9"
# MAGENTA="D633B2"
# CYAN="44C9C9"
# WHITE="FFFFFF"
# COLORS="${BLACK} ${DARK_RED} ${DARK_GREEN} ${DARK_YELLOW} ${DARK_BLUE} ${DARK_MAGENTA} ${DARK_CYAN} ${LIGHT_GRAY} ${DARK_GRAY} ${RED} ${GREEN} ${YELLOW} ${BLUE} ${MAGENTA} ${CYAN} ${WHITE}"
# i=0
# while [ $i -lt 16 ]; do
# printf "\033]P%x%s" ${i} "$(echo "$COLORS" | cut -d ' ' -f$(( i + 1)))"
# i=$(( i + 1 ))
# done
# clear # for fixing background artifacting after changing color
# fi

View File

@@ -1,8 +1,8 @@
const ini = @import("ly-ui").ly_core.ini;
const Ini = ini.Ini;
const enums = @import("enums.zig"); const enums = @import("enums.zig");
const ini = @import("zigini");
const DisplayServer = enums.DisplayServer; const DisplayServer = enums.DisplayServer;
const Ini = ini.Ini;
pub const DesktopEntry = struct { pub const DesktopEntry = struct {
Exec: []const u8 = "", Exec: []const u8 = "",

51
src/LogFile.zig Normal file
View File

@@ -0,0 +1,51 @@
const std = @import("std");
const LogFile = @This();
path: []const u8,
could_open_log_file: bool = undefined,
file: std.fs.File = undefined,
buffer: []u8,
file_writer: std.fs.File.Writer = undefined,
pub fn init(path: []const u8, buffer: []u8) !LogFile {
var log_file = LogFile{ .path = path, .buffer = buffer };
log_file.could_open_log_file = try openLogFile(path, &log_file);
return log_file;
}
pub fn reinit(self: *LogFile) !void {
self.could_open_log_file = try openLogFile(self.path, self);
}
pub fn deinit(self: *LogFile) void {
self.file_writer.interface.flush() catch {};
self.file.close();
}
fn openLogFile(path: []const u8, log_file: *LogFile) !bool {
var could_open_log_file = true;
open_log_file: {
log_file.file = std.fs.cwd().openFile(path, .{ .mode = .write_only }) catch std.fs.cwd().createFile(path, .{ .mode = 0o666 }) catch {
// If we could neither open an existing log file nor create a new
// one, abort.
could_open_log_file = false;
break :open_log_file;
};
}
if (!could_open_log_file) {
log_file.file = try std.fs.openFileAbsolute("/dev/null", .{ .mode = .write_only });
}
var log_file_writer = log_file.file.writer(log_file.buffer);
// Seek to the end of the log file
if (could_open_log_file) {
const stat = try log_file.file.stat();
try log_file_writer.seekTo(stat.size);
}
log_file.file_writer = log_file_writer;
return could_open_log_file;
}

39
src/SharedError.zig Normal file
View File

@@ -0,0 +1,39 @@
const std = @import("std");
const ErrInt = std.meta.Int(.unsigned, @bitSizeOf(anyerror));
const ErrorHandler = packed struct {
has_error: bool = false,
err_int: ErrInt = 0,
};
const SharedError = @This();
data: []align(std.heap.page_size_min) u8,
pub fn init() !SharedError {
const data = try std.posix.mmap(null, @sizeOf(ErrorHandler), std.posix.PROT.READ | std.posix.PROT.WRITE, .{ .TYPE = .SHARED, .ANONYMOUS = true }, -1, 0);
return .{ .data = data };
}
pub fn deinit(self: *SharedError) void {
std.posix.munmap(self.data);
}
pub fn writeError(self: SharedError, err: anyerror) void {
var buf_stream = std.io.fixedBufferStream(self.data);
const writer = buf_stream.writer();
writer.writeStruct(ErrorHandler{ .has_error = true, .err_int = @intFromError(err) }) catch {};
}
pub fn readError(self: SharedError) ?anyerror {
var buf_stream = std.io.fixedBufferStream(self.data);
const reader = buf_stream.reader();
const err_handler = try reader.readStruct(ErrorHandler);
if (err_handler.has_error)
return @errorFromInt(err_handler.err_int);
return null;
}

View File

@@ -1,92 +0,0 @@
const std = @import("std");
const math = std.math;
const ly_ui = @import("ly-ui");
const Cell = ly_ui.Cell;
const TerminalBuffer = ly_ui.TerminalBuffer;
const Widget = ly_ui.Widget;
const Cascade = @This();
io: std.Io,
instance: ?Widget = null,
buffer: *TerminalBuffer,
current_auth_fails: *u64,
max_auth_fails: u64,
pub fn init(
io: std.Io,
buffer: *TerminalBuffer,
current_auth_fails: *u64,
max_auth_fails: u64,
) Cascade {
return .{
.io = io,
.instance = null,
.buffer = buffer,
.current_auth_fails = current_auth_fails,
.max_auth_fails = max_auth_fails,
};
}
pub fn widget(self: *Cascade) *Widget {
if (self.instance) |*instance| return instance;
self.instance = Widget.init(
"Cascade",
null,
self,
null,
null,
draw,
null,
null,
null,
);
return &self.instance.?;
}
fn draw(self: *Cascade) void {
while (self.current_auth_fails.* >= self.max_auth_fails) {
self.io.sleep(.fromMilliseconds(10), .real) catch {};
var changed = false;
var y = self.buffer.height - 2;
while (y > 0) : (y -= 1) {
for (0..self.buffer.width) |x| {
const cell = TerminalBuffer.getCell(x, y - 1);
const cell_under = TerminalBuffer.getCell(x, y);
// This shouldn't happen under normal circumstances, but because
// this is a *secret* animation, there's no need to care that much
if (cell == null or cell_under == null) continue;
const char: u8 = @truncate(cell.?.ch);
if (std.ascii.isWhitespace(char)) continue;
const char_under: u8 = @truncate(cell_under.?.ch);
if (!std.ascii.isWhitespace(char_under)) continue;
changed = true;
if ((self.buffer.random.int(u16) % 10) > 7) continue;
cell.?.put(x, y);
var space = Cell.init(
' ',
cell_under.?.fg,
cell_under.?.bg,
);
space.put(x, y - 1);
}
}
if (!changed) {
self.io.sleep(.fromSeconds(7), .real) catch {};
self.current_auth_fails.* = 0;
}
TerminalBuffer.presentBuffer();
}
}

View File

@@ -1,17 +1,11 @@
const std = @import("std"); const std = @import("std");
const math = std.math; const Animation = @import("../tui/Animation.zig");
const Cell = @import("../tui/Cell.zig");
const ly_ui = @import("ly-ui"); const TerminalBuffer = @import("../tui/TerminalBuffer.zig");
const Cell = ly_ui.Cell;
const TerminalBuffer = ly_ui.TerminalBuffer;
const Widget = ly_ui.Widget;
const ly_core = ly_ui.ly_core;
const interop = ly_core.interop;
const TimeOfDay = interop.TimeOfDay;
const ColorMix = @This(); const ColorMix = @This();
const math = std.math;
const Vec2 = @Vector(2, f32); const Vec2 = @Vector(2, f32);
const time_scale: f32 = 0.01; const time_scale: f32 = 0.01;
@@ -21,33 +15,15 @@ fn length(vec: Vec2) f32 {
return math.sqrt(vec[0] * vec[0] + vec[1] * vec[1]); return math.sqrt(vec[0] * vec[0] + vec[1] * vec[1]);
} }
instance: ?Widget = null,
start_time: TimeOfDay,
terminal_buffer: *TerminalBuffer, terminal_buffer: *TerminalBuffer,
animate: *bool,
timeout_sec: u12,
frame_delay: u16,
frames: u64, frames: u64,
pattern_cos_mod: f32, pattern_cos_mod: f32,
pattern_sin_mod: f32, pattern_sin_mod: f32,
palette: [palette_len]Cell, palette: [palette_len]Cell,
pub fn init( pub fn init(terminal_buffer: *TerminalBuffer, col1: u32, col2: u32, col3: u32) ColorMix {
terminal_buffer: *TerminalBuffer,
col1: u32,
col2: u32,
col3: u32,
animate: *bool,
timeout_sec: u12,
frame_delay: u16,
) !ColorMix {
return .{ return .{
.instance = null,
.start_time = try interop.getTimeOfDay(),
.terminal_buffer = terminal_buffer, .terminal_buffer = terminal_buffer,
.animate = animate,
.timeout_sec = timeout_sec,
.frame_delay = frame_delay,
.frames = 0, .frames = 0,
.pattern_cos_mod = terminal_buffer.random.float(f32) * math.pi * 2.0, .pattern_cos_mod = terminal_buffer.random.float(f32) * math.pi * 2.0,
.pattern_sin_mod = terminal_buffer.random.float(f32) * math.pi * 2.0, .pattern_sin_mod = terminal_buffer.random.float(f32) * math.pi * 2.0,
@@ -68,25 +44,15 @@ pub fn init(
}; };
} }
pub fn widget(self: *ColorMix) *Widget { pub fn animation(self: *ColorMix) Animation {
if (self.instance) |*instance| return instance; return Animation.init(self, deinit, realloc, draw);
self.instance = Widget.init(
"ColorMix",
null,
self,
null,
null,
draw,
update,
null,
calculateTimeout,
);
return &self.instance.?;
} }
fn draw(self: *ColorMix) void { fn deinit(_: *ColorMix) void {}
if (!self.animate.*) return;
fn realloc(_: *ColorMix) anyerror!void {}
fn draw(self: *ColorMix) void {
self.frames +%= 1; self.frames +%= 1;
const time: f32 = @as(f32, @floatFromInt(self.frames)) * time_scale; const time: f32 = @as(f32, @floatFromInt(self.frames)) * time_scale;
@@ -113,20 +79,8 @@ fn draw(self: *ColorMix) void {
uv -= @splat(1.0 * math.cos(uv[0] + uv[1]) - math.sin(uv[0] * 0.7 - uv[1])); uv -= @splat(1.0 * math.cos(uv[0] + uv[1]) - math.sin(uv[0] * 0.7 - uv[1]));
} }
const cell = self.palette[@as(usize, @trunc(math.floor(length(uv) * 5.0))) % palette_len]; const cell = self.palette[@as(usize, @intFromFloat(math.floor(length(uv) * 5.0))) % palette_len];
cell.put(x, y); cell.put(x, y);
} }
} }
} }
fn update(self: *ColorMix, _: *anyopaque) !void {
const time = try interop.getTimeOfDay();
if (self.timeout_sec > 0 and time.seconds - self.start_time.seconds > self.timeout_sec) {
self.animate.* = false;
}
}
fn calculateTimeout(self: *ColorMix, _: *anyopaque) !?usize {
return self.frame_delay;
}

View File

@@ -1,14 +1,8 @@
const std = @import("std"); const std = @import("std");
const Allocator = std.mem.Allocator; const Allocator = std.mem.Allocator;
const Animation = @import("../tui/Animation.zig");
const ly_ui = @import("ly-ui"); const Cell = @import("../tui/Cell.zig");
const Cell = ly_ui.Cell; const TerminalBuffer = @import("../tui/TerminalBuffer.zig");
const TerminalBuffer = ly_ui.TerminalBuffer;
const Widget = ly_ui.Widget;
const ly_core = ly_ui.ly_core;
const interop = ly_core.interop;
const TimeOfDay = interop.TimeOfDay;
const Doom = @This(); const Doom = @This();
@@ -16,30 +10,14 @@ pub const STEPS = 12;
pub const HEIGHT_MAX = 9; pub const HEIGHT_MAX = 9;
pub const SPREAD_MAX = 4; pub const SPREAD_MAX = 4;
instance: ?Widget = null,
start_time: TimeOfDay,
allocator: Allocator, allocator: Allocator,
terminal_buffer: *TerminalBuffer, terminal_buffer: *TerminalBuffer,
animate: *bool,
timeout_sec: u12,
frame_delay: u16,
buffer: []u8, buffer: []u8,
height: u8, height: u8,
spread: u8, spread: u8,
fire: [STEPS + 1]Cell, fire: [STEPS + 1]Cell,
pub fn init( pub fn init(allocator: Allocator, terminal_buffer: *TerminalBuffer, top_color: u32, middle_color: u32, bottom_color: u32, fire_height: u8, fire_spread: u8) !Doom {
allocator: Allocator,
terminal_buffer: *TerminalBuffer,
top_color: u32,
middle_color: u32,
bottom_color: u32,
fire_height: u8,
fire_spread: u8,
animate: *bool,
timeout_sec: u12,
frame_delay: u16,
) !Doom {
const buffer = try allocator.alloc(u8, terminal_buffer.width * terminal_buffer.height); const buffer = try allocator.alloc(u8, terminal_buffer.width * terminal_buffer.height);
initBuffer(buffer, terminal_buffer.width); initBuffer(buffer, terminal_buffer.width);
@@ -61,13 +39,8 @@ pub fn init(
}; };
return .{ return .{
.instance = null,
.start_time = try interop.getTimeOfDay(),
.allocator = allocator, .allocator = allocator,
.terminal_buffer = terminal_buffer, .terminal_buffer = terminal_buffer,
.animate = animate,
.timeout_sec = timeout_sec,
.frame_delay = frame_delay,
.buffer = buffer, .buffer = buffer,
.height = @min(HEIGHT_MAX, fire_height), .height = @min(HEIGHT_MAX, fire_height),
.spread = @min(SPREAD_MAX, fire_spread), .spread = @min(SPREAD_MAX, fire_spread),
@@ -75,35 +48,21 @@ pub fn init(
}; };
} }
pub fn widget(self: *Doom) *Widget { pub fn animation(self: *Doom) Animation {
if (self.instance) |*instance| return instance; return Animation.init(self, deinit, realloc, draw);
self.instance = Widget.init(
"Doom",
null,
self,
deinit,
realloc,
draw,
update,
null,
calculateTimeout,
);
return &self.instance.?;
} }
fn deinit(self: *Doom) void { fn deinit(self: *Doom) void {
self.allocator.free(self.buffer); self.allocator.free(self.buffer);
} }
fn realloc(self: *Doom) !void { fn realloc(self: *Doom) anyerror!void {
const buffer = try self.allocator.realloc(self.buffer, self.terminal_buffer.width * self.terminal_buffer.height); const buffer = try self.allocator.realloc(self.buffer, self.terminal_buffer.width * self.terminal_buffer.height);
initBuffer(buffer, self.terminal_buffer.width); initBuffer(buffer, self.terminal_buffer.width);
self.buffer = buffer; self.buffer = buffer;
} }
fn draw(self: *Doom) void { fn draw(self: *Doom) void {
if (!self.animate.*) return;
for (0..self.terminal_buffer.width) |x| { for (0..self.terminal_buffer.width) |x| {
// We start from 1 so that we always have the topmost line when spreading fire // We start from 1 so that we always have the topmost line when spreading fire
for (1..self.terminal_buffer.height) |y| { for (1..self.terminal_buffer.height) |y| {
@@ -149,15 +108,3 @@ fn initBuffer(buffer: []u8, width: usize) void {
@memset(slice_start, 0); @memset(slice_start, 0);
@memset(slice_end, STEPS); @memset(slice_end, STEPS);
} }
fn update(self: *Doom, _: *anyopaque) !void {
const time = try interop.getTimeOfDay();
if (self.timeout_sec > 0 and time.seconds - self.start_time.seconds > self.timeout_sec) {
self.animate.* = false;
}
}
fn calculateTimeout(self: *Doom, _: *anyopaque) !?usize {
return self.frame_delay;
}

14
src/animations/Dummy.zig Normal file
View File

@@ -0,0 +1,14 @@
const std = @import("std");
const Animation = @import("../tui/Animation.zig");
const Dummy = @This();
pub fn animation(self: *Dummy) Animation {
return Animation.init(self, deinit, realloc, draw);
}
fn deinit(_: *Dummy) void {}
fn realloc(_: *Dummy) anyerror!void {}
fn draw(_: *Dummy) void {}

View File

@@ -1,34 +1,24 @@
const std = @import("std"); const std = @import("std");
const Animation = @import("../tui/Animation.zig");
const Cell = @import("../tui/Cell.zig");
const TerminalBuffer = @import("../tui/TerminalBuffer.zig");
const Color = TerminalBuffer.Color;
const Styling = TerminalBuffer.Styling;
const Allocator = std.mem.Allocator; const Allocator = std.mem.Allocator;
const Json = std.json; const Json = std.json;
const eql = std.mem.eql; const eql = std.mem.eql;
const flate = std.compress.flate; const flate = std.compress.flate;
const ly_ui = @import("ly-ui"); fn read_decompress_file(allocator: Allocator, file_path: []const u8) ![]u8 {
const Cell = ly_ui.Cell; const file_buffer = std.fs.cwd().openFile(file_path, .{}) catch {
const TerminalBuffer = ly_ui.TerminalBuffer;
const Color = TerminalBuffer.Color;
const Styling = TerminalBuffer.Styling;
const Widget = ly_ui.Widget;
const ly_core = ly_ui.ly_core;
const interop = ly_core.interop;
const TimeOfDay = interop.TimeOfDay;
const LogFile = ly_core.LogFile;
const enums = @import("../enums.zig");
const DurOffsetAlignment = enums.DurOffsetAlignment;
fn read_decompress_file(allocator: Allocator, io: std.Io, file_path: []const u8) ![]u8 {
const file_buffer = std.Io.Dir.cwd().openFile(io, file_path, .{}) catch {
return error.FileNotFound; return error.FileNotFound;
}; };
defer file_buffer.close(io); defer file_buffer.close();
var file_reader_buffer: [4096]u8 = undefined; var file_reader_buffer: [4096]u8 = undefined;
var decompress_buffer: [flate.max_window_len]u8 = undefined; var decompress_buffer: [flate.max_window_len]u8 = undefined;
var file_reader = file_buffer.reader(io, &file_reader_buffer); var file_reader = file_buffer.reader(&file_reader_buffer);
var decompress: flate.Decompress = .init(&file_reader.interface, .gzip, &decompress_buffer); var decompress: flate.Decompress = .init(&file_reader.interface, .gzip, &decompress_buffer);
const file_decompressed = decompress.reader.allocRemaining(allocator, .unlimited) catch { const file_decompressed = decompress.reader.allocRemaining(allocator, .unlimited) catch {
@@ -150,8 +140,8 @@ const DurFormat = struct {
} }
} }
pub fn create_from_file(self: *DurFormat, allocator: Allocator, io: std.Io, file_path: []const u8) !void { pub fn create_from_file(self: *DurFormat, allocator: Allocator, file_path: []const u8) !void {
const file_decompressed = try read_decompress_file(allocator, io, file_path); const file_decompressed = try read_decompress_file(allocator, file_path);
defer allocator.free(file_decompressed); defer allocator.free(file_decompressed);
const parsed = try Json.parseFromSlice(Json.Value, allocator, file_decompressed, .{}); const parsed = try Json.parseFromSlice(Json.Value, allocator, file_decompressed, .{});
@@ -296,100 +286,32 @@ fn convert_256_to_rgb(color_256: u32) u32 {
return rgb_color; return rgb_color;
} }
const UVec2 = @Vector(2, u32);
const IVec2 = @Vector(2, i64);
const VEC_X = 0;
const VEC_Y = 1;
const DurFile = @This(); const DurFile = @This();
instance: ?Widget = null,
start_time: TimeOfDay,
allocator: Allocator, allocator: Allocator,
io: std.Io,
terminal_buffer: *TerminalBuffer, terminal_buffer: *TerminalBuffer,
dur_movie: DurFormat, frames: u64,
frames: usize,
frame_size: UVec2,
start_pos: IVec2,
full_color: bool,
animate: *bool,
timeout_sec: u12,
frame_delay: u16,
frame_time: u32,
time_previous: i64, time_previous: i64,
x_offset: u32,
y_offset: u32,
full_color: bool,
dur_movie: DurFormat,
frame_width: u32,
frame_height: u32,
frame_time: u32,
is_color_format_16: bool, is_color_format_16: bool,
offset_alignment: DurOffsetAlignment,
offset: IVec2,
// if the user has an even number of columns or rows, we will default to the left or higher position (e.g. 4 columns center = .x..) pub fn init(allocator: Allocator, terminal_buffer: *TerminalBuffer, log_writer: *std.io.Writer, file_path: []const u8, x_offset: u32, y_offset: u32, full_color: bool) !DurFile {
fn center(v: u32) i64 {
return @intCast((v / 2) + (v % 2));
}
fn calc_start_position(terminal_buffer: *TerminalBuffer, dur_movie: *DurFormat, offset_alignment: DurOffsetAlignment, offset: IVec2) IVec2 {
const buf_width: u32 = @intCast(terminal_buffer.width);
const buf_height: u32 = @intCast(terminal_buffer.height);
var movie_width: u32 = @intCast(dur_movie.columns.?);
var movie_height: u32 = @intCast(dur_movie.lines.?);
if (movie_width > buf_width) movie_width = buf_width;
if (movie_height > buf_height) movie_height = buf_height;
const start_pos: IVec2 = switch (offset_alignment) {
DurOffsetAlignment.center => .{ center(buf_width) - center(movie_width), center(buf_height) - center(movie_height) },
DurOffsetAlignment.topleft => .{ 0, 0 },
DurOffsetAlignment.topcenter => .{ center(buf_width) - center(movie_width), 0 },
DurOffsetAlignment.topright => .{ buf_width - movie_width, 0 },
DurOffsetAlignment.centerleft => .{ 0, center(buf_height) - center(movie_height) },
DurOffsetAlignment.centerright => .{ buf_width - movie_width, center(buf_height) - center(movie_height) },
DurOffsetAlignment.bottomleft => .{ 0, buf_height - movie_height },
DurOffsetAlignment.bottomcenter => .{ center(buf_width) - center(movie_width), buf_height - movie_height },
DurOffsetAlignment.bottomright => .{ buf_width - movie_width, buf_height - movie_height },
};
return start_pos + offset;
}
fn calc_frame_size(terminal_buffer: *TerminalBuffer, dur_movie: *DurFormat) UVec2 {
const buf_width: u32 = @intCast(terminal_buffer.width);
const buf_height: u32 = @intCast(terminal_buffer.height);
const movie_width: u32 = @intCast(dur_movie.columns.?);
const movie_height: u32 = @intCast(dur_movie.lines.?);
// Draw only the needed amount if movie smaller than screen. If movie is bigger, we will just draw entire screen
const frame_width = if (movie_width < buf_width) movie_width else buf_width;
const frame_height = if (movie_height < buf_height) movie_height else buf_height;
return .{ frame_width, frame_height };
}
pub fn init(
allocator: Allocator,
io: std.Io,
terminal_buffer: *TerminalBuffer,
log_file: *LogFile,
file_path: []const u8,
offset_alignment: DurOffsetAlignment,
x_offset: i32,
y_offset: i32,
full_color: bool,
animate: *bool,
timeout_sec: u12,
frame_delay: u16,
) !DurFile {
var dur_movie: DurFormat = .init(allocator); var dur_movie: DurFormat = .init(allocator);
dur_movie.create_from_file(allocator, io, file_path) catch |err| switch (err) { // error state is recoverable when thrown to main and results in no background with Dummy in main
dur_movie.create_from_file(allocator, file_path) catch |err| switch (err) {
error.FileNotFound => { error.FileNotFound => {
try log_file.err(io, "tui", "dur_file was not found at: {s}", .{file_path}); try log_writer.print("error: dur_file was not found at: {s}\n", .{file_path});
return err; return err;
}, },
error.NotValidFile => { error.NotValidFile => {
try log_file.err(io, "tui", "dur_file loaded was invalid or not a dur file!", .{}); try log_writer.print("error: dur_file loaded was invalid or not a dur file!\n", .{});
return err; return err;
}, },
else => return err, else => return err,
@@ -397,91 +319,61 @@ pub fn init(
// 4 bit mode with 256 color is unsupported // 4 bit mode with 256 color is unsupported
if (!full_color and eql(u8, dur_movie.colorFormat.?, "256")) { if (!full_color and eql(u8, dur_movie.colorFormat.?, "256")) {
try log_file.err(io, "tui", "dur_file can not be 256 color encoded when not using full_color option!", .{}); try log_writer.print("error: dur_file can not be 256 color encoded when not using full_color option!\n", .{});
dur_movie.deinit(); dur_movie.deinit();
return error.InvalidColorFormat; return error.InvalidColorFormat;
} }
const offset: IVec2 = .{ x_offset, y_offset }; const buf_width: u32 = @intCast(terminal_buffer.width);
const buf_height: u32 = @intCast(terminal_buffer.height);
const start_pos = calc_start_position(terminal_buffer, &dur_movie, offset_alignment, offset); const movie_width: u32 = @intCast(dur_movie.columns.?);
const frame_size = calc_frame_size(terminal_buffer, &dur_movie); const movie_height: u32 = @intCast(dur_movie.lines.?);
// Clamp to prevent user from exceeding draw window
const x_offset_clamped = std.math.clamp(x_offset, 0, buf_width - 1);
const y_offset_clamped = std.math.clamp(y_offset, 0, buf_height - 1);
// Ensure if user offsets and frame goes offscreen, it will not overflow draw
const frame_width = if ((movie_width + x_offset_clamped) < buf_width) movie_width else buf_width - x_offset_clamped;
const frame_height = if ((movie_height + y_offset_clamped) < buf_height) movie_height else buf_height - y_offset_clamped;
// Convert dur fps to frames per ms // Convert dur fps to frames per ms
const frame_time: u32 = @trunc(1000 / dur_movie.framerate.?); const frame_time: u32 = @intFromFloat(1000 / dur_movie.framerate.?);
return .{ return .{
.instance = null,
.start_time = try interop.getTimeOfDay(),
.allocator = allocator, .allocator = allocator,
.io = io,
.terminal_buffer = terminal_buffer, .terminal_buffer = terminal_buffer,
.frames = 0, .frames = 0,
.time_previous = std.Io.Timestamp.now(io, .real).toMilliseconds(), .time_previous = std.time.milliTimestamp(),
.frame_size = frame_size, .x_offset = x_offset_clamped,
.start_pos = start_pos, .y_offset = y_offset_clamped,
.full_color = full_color, .full_color = full_color,
.animate = animate,
.timeout_sec = timeout_sec,
.frame_delay = frame_delay,
.dur_movie = dur_movie, .dur_movie = dur_movie,
.frame_width = frame_width,
.frame_height = frame_height,
.frame_time = frame_time, .frame_time = frame_time,
.is_color_format_16 = eql(u8, dur_movie.colorFormat.?, "16"), .is_color_format_16 = eql(u8, dur_movie.colorFormat.?, "16"),
.offset_alignment = offset_alignment,
.offset = offset,
}; };
} }
pub fn widget(self: *DurFile) *Widget { pub fn animation(self: *DurFile) Animation {
if (self.instance) |*instance| return instance; return Animation.init(self, deinit, realloc, draw);
self.instance = Widget.init(
"DurFile",
null,
self,
deinit,
realloc,
draw,
update,
null,
calculateTimeout,
);
return &self.instance.?;
} }
fn deinit(self: *DurFile) void { fn deinit(self: *DurFile) void {
self.dur_movie.deinit(); self.dur_movie.deinit();
} }
fn realloc(self: *DurFile) !void { fn realloc(_: *DurFile) anyerror!void {}
// when terminal size changes, we need to recalculate the start_pos and frame_size based on the new size
self.start_pos = calc_start_position(self.terminal_buffer, &self.dur_movie, self.offset_alignment, self.offset);
self.frame_size = calc_frame_size(self.terminal_buffer, &self.dur_movie);
}
fn draw(self: *DurFile) void { fn draw(self: *DurFile) void {
if (!self.animate.*) return;
const current_frame = self.dur_movie.frames.items[self.frames]; const current_frame = self.dur_movie.frames.items[self.frames];
const buf_width: u32 = @intCast(self.terminal_buffer.width); for (0..self.frame_height) |y| {
const buf_height: u32 = @intCast(self.terminal_buffer.height);
// y is used as an iterator in the durformat, while cell_y gives us the correct placement for the cell (same for x)
for (0..self.frame_size[VEC_Y]) |y| {
const y_offset_i = @as(i32, @intCast(y)) + self.start_pos[VEC_Y];
// we skip the pass if it falls outside of the draw window (ensure no int underflow)
const cell_y: u32 = if (y_offset_i >= 0 and y_offset_i < buf_height) @intCast(y_offset_i) else continue;
var iter = std.unicode.Utf8View.initUnchecked(current_frame.contents[y]).iterator(); var iter = std.unicode.Utf8View.initUnchecked(current_frame.contents[y]).iterator();
for (0..self.frame_size[VEC_X]) |x| { for (0..self.frame_width) |x| {
const x_offset_i = @as(i32, @intCast(x)) + self.start_pos[VEC_X];
// skip pass, same as y but also increment the codepoint iter to fetch correct values in later passes
const cell_x: u32 = if (x_offset_i >= 0 and x_offset_i < buf_width) @intCast(x_offset_i) else {
_ = iter.nextCodepoint().?;
continue;
};
const codepoint: u21 = iter.nextCodepoint().?; const codepoint: u21 = iter.nextCodepoint().?;
const color_map = current_frame.colorMap[x][y]; const color_map = current_frame.colorMap[x][y];
@@ -498,15 +390,15 @@ fn draw(self: *DurFile) void {
const cell = Cell{ .ch = @intCast(codepoint), .fg = fg_color, .bg = bg_color }; const cell = Cell{ .ch = @intCast(codepoint), .fg = fg_color, .bg = bg_color };
cell.put(cell_x, cell_y); cell.put(x + self.x_offset, y + self.y_offset);
} }
} }
const time_current = std.Io.Timestamp.now(self.io, .real).toMilliseconds(); const time_current = std.time.milliTimestamp();
const delta_time = time_current - self.time_previous; const delta_time = time_current - self.time_previous;
// Convert delay from sec to ms // Convert delay from sec to ms
const delay_time: u32 = @trunc(current_frame.delay * 1000); const delay_time: u32 = @intFromFloat(current_frame.delay * 1000);
if (delta_time > (self.frame_time + delay_time)) { if (delta_time > (self.frame_time + delay_time)) {
self.time_previous = time_current; self.time_previous = time_current;
@@ -514,15 +406,3 @@ fn draw(self: *DurFile) void {
self.frames = (self.frames + 1) % frame_count; self.frames = (self.frames + 1) % frame_count;
} }
} }
fn update(self: *DurFile, _: *anyopaque) !void {
const time = try interop.getTimeOfDay();
if (self.timeout_sec > 0 and time.seconds - self.start_time.seconds > self.timeout_sec) {
self.animate.* = false;
}
}
fn calculateTimeout(self: *DurFile, _: *anyopaque) !?usize {
return self.frame_delay;
}

View File

@@ -1,15 +1,10 @@
const std = @import("std"); const std = @import("std");
const Animation = @import("../tui/Animation.zig");
const Cell = @import("../tui/Cell.zig");
const TerminalBuffer = @import("../tui/TerminalBuffer.zig");
const Allocator = std.mem.Allocator; const Allocator = std.mem.Allocator;
const ly_ui = @import("ly-ui");
const Cell = ly_ui.Cell;
const TerminalBuffer = ly_ui.TerminalBuffer;
const Widget = ly_ui.Widget;
const ly_core = ly_ui.ly_core;
const interop = ly_core.interop;
const TimeOfDay = interop.TimeOfDay;
const GameOfLife = @This(); const GameOfLife = @This();
// Visual styles - using block characters like other animations // Visual styles - using block characters like other animations
@@ -21,8 +16,6 @@ const NEIGHBOR_DIRS = [_][2]i8{
.{ 1, 0 }, .{ 1, 1 }, .{ 1, 0 }, .{ 1, 1 },
}; };
instance: ?Widget = null,
start_time: TimeOfDay,
allocator: Allocator, allocator: Allocator,
terminal_buffer: *TerminalBuffer, terminal_buffer: *TerminalBuffer,
current_grid: []bool, current_grid: []bool,
@@ -33,24 +26,11 @@ fg_color: u32,
entropy_interval: usize, entropy_interval: usize,
frame_delay: usize, frame_delay: usize,
initial_density: f32, initial_density: f32,
animate: *bool,
timeout_sec: u12,
animation_frame_delay: u16,
dead_cell: Cell, dead_cell: Cell,
width: usize, width: usize,
height: usize, height: usize,
pub fn init( pub fn init(allocator: Allocator, terminal_buffer: *TerminalBuffer, fg_color: u32, entropy_interval: usize, frame_delay: usize, initial_density: f32) !GameOfLife {
allocator: Allocator,
terminal_buffer: *TerminalBuffer,
fg_color: u32,
entropy_interval: usize,
frame_delay: usize,
initial_density: f32,
animate: *bool,
timeout_sec: u12,
animation_frame_delay: u16,
) !GameOfLife {
const width = terminal_buffer.width; const width = terminal_buffer.width;
const height = terminal_buffer.height; const height = terminal_buffer.height;
const grid_size = width * height; const grid_size = width * height;
@@ -59,8 +39,6 @@ pub fn init(
const next_grid = try allocator.alloc(bool, grid_size); const next_grid = try allocator.alloc(bool, grid_size);
var game = GameOfLife{ var game = GameOfLife{
.instance = null,
.start_time = try interop.getTimeOfDay(),
.allocator = allocator, .allocator = allocator,
.terminal_buffer = terminal_buffer, .terminal_buffer = terminal_buffer,
.current_grid = current_grid, .current_grid = current_grid,
@@ -71,9 +49,6 @@ pub fn init(
.entropy_interval = entropy_interval, .entropy_interval = entropy_interval,
.frame_delay = frame_delay, .frame_delay = frame_delay,
.initial_density = initial_density, .initial_density = initial_density,
.animate = animate,
.timeout_sec = timeout_sec,
.animation_frame_delay = animation_frame_delay,
.dead_cell = .{ .ch = DEAD_CHAR, .fg = @intCast(TerminalBuffer.Color.DEFAULT), .bg = terminal_buffer.bg }, .dead_cell = .{ .ch = DEAD_CHAR, .fg = @intCast(TerminalBuffer.Color.DEFAULT), .bg = terminal_buffer.bg },
.width = width, .width = width,
.height = height, .height = height,
@@ -85,20 +60,8 @@ pub fn init(
return game; return game;
} }
pub fn widget(self: *GameOfLife) *Widget { pub fn animation(self: *GameOfLife) Animation {
if (self.instance) |*instance| return instance; return Animation.init(self, deinit, realloc, draw);
self.instance = Widget.init(
"GameOfLife",
null,
self,
deinit,
realloc,
draw,
update,
null,
calculateTimeout,
);
return &self.instance.?;
} }
fn deinit(self: *GameOfLife) void { fn deinit(self: *GameOfLife) void {
@@ -106,7 +69,7 @@ fn deinit(self: *GameOfLife) void {
self.allocator.free(self.next_grid); self.allocator.free(self.next_grid);
} }
fn realloc(self: *GameOfLife) !void { fn realloc(self: *GameOfLife) anyerror!void {
const new_width = self.terminal_buffer.width; const new_width = self.terminal_buffer.width;
const new_height = self.terminal_buffer.height; const new_height = self.terminal_buffer.height;
const new_size = new_width * new_height; const new_size = new_width * new_height;
@@ -124,8 +87,6 @@ fn realloc(self: *GameOfLife) !void {
} }
fn draw(self: *GameOfLife) void { fn draw(self: *GameOfLife) void {
if (!self.animate.*) return;
// Update game state at controlled frame rate // Update game state at controlled frame rate
self.frame_counter += 1; self.frame_counter += 1;
if (self.frame_counter >= self.frame_delay) { if (self.frame_counter >= self.frame_delay) {
@@ -151,18 +112,6 @@ fn draw(self: *GameOfLife) void {
} }
} }
fn update(self: *GameOfLife, _: *anyopaque) !void {
const time = try interop.getTimeOfDay();
if (self.timeout_sec > 0 and time.seconds - self.start_time.seconds > self.timeout_sec) {
self.animate.* = false;
}
}
fn calculateTimeout(self: *GameOfLife, _: *anyopaque) !?usize {
return self.animation_frame_delay;
}
fn updateGeneration(self: *GameOfLife) void { fn updateGeneration(self: *GameOfLife) void {
// Conway's Game of Life rules with optimized neighbor counting // Conway's Game of Life rules with optimized neighbor counting
for (0..self.height) |y| { for (0..self.height) |y| {

View File

@@ -1,16 +1,11 @@
const std = @import("std"); const std = @import("std");
const Animation = @import("../tui/Animation.zig");
const Cell = @import("../tui/Cell.zig");
const TerminalBuffer = @import("../tui/TerminalBuffer.zig");
const Allocator = std.mem.Allocator; const Allocator = std.mem.Allocator;
const Random = std.Random; const Random = std.Random;
const ly_ui = @import("ly-ui");
const Cell = ly_ui.Cell;
const TerminalBuffer = ly_ui.TerminalBuffer;
const Widget = ly_ui.Widget;
const ly_core = ly_ui.ly_core;
const interop = ly_core.interop;
const TimeOfDay = interop.TimeOfDay;
pub const FRAME_DELAY: usize = 8; pub const FRAME_DELAY: usize = 8;
// Characters change mid-scroll // Characters change mid-scroll
@@ -29,8 +24,6 @@ pub const Line = struct {
update: usize, update: usize,
}; };
instance: ?Widget = null,
start_time: TimeOfDay,
allocator: Allocator, allocator: Allocator,
terminal_buffer: *TerminalBuffer, terminal_buffer: *TerminalBuffer,
dots: []Dot, dots: []Dot,
@@ -41,30 +34,15 @@ fg: u32,
head_col: u32, head_col: u32,
min_codepoint: u16, min_codepoint: u16,
max_codepoint: u16, max_codepoint: u16,
animate: *bool,
timeout_sec: u12,
frame_delay: u16,
default_cell: Cell, default_cell: Cell,
pub fn init( pub fn init(allocator: Allocator, terminal_buffer: *TerminalBuffer, fg: u32, head_col: u32, min_codepoint: u16, max_codepoint: u16) !Matrix {
allocator: Allocator,
terminal_buffer: *TerminalBuffer,
fg: u32,
head_col: u32,
min_codepoint: u16,
max_codepoint: u16,
animate: *bool,
timeout_sec: u12,
frame_delay: u16,
) !Matrix {
const dots = try allocator.alloc(Dot, terminal_buffer.width * (terminal_buffer.height + 1)); const dots = try allocator.alloc(Dot, terminal_buffer.width * (terminal_buffer.height + 1));
const lines = try allocator.alloc(Line, terminal_buffer.width); const lines = try allocator.alloc(Line, terminal_buffer.width);
initBuffers(dots, lines, terminal_buffer.width, terminal_buffer.height, terminal_buffer.random); initBuffers(dots, lines, terminal_buffer.width, terminal_buffer.height, terminal_buffer.random);
return .{ return .{
.instance = null,
.start_time = try interop.getTimeOfDay(),
.allocator = allocator, .allocator = allocator,
.terminal_buffer = terminal_buffer, .terminal_buffer = terminal_buffer,
.dots = dots, .dots = dots,
@@ -75,27 +53,12 @@ pub fn init(
.head_col = head_col, .head_col = head_col,
.min_codepoint = min_codepoint, .min_codepoint = min_codepoint,
.max_codepoint = max_codepoint - min_codepoint, .max_codepoint = max_codepoint - min_codepoint,
.animate = animate,
.timeout_sec = timeout_sec,
.frame_delay = frame_delay,
.default_cell = .{ .ch = ' ', .fg = fg, .bg = terminal_buffer.bg }, .default_cell = .{ .ch = ' ', .fg = fg, .bg = terminal_buffer.bg },
}; };
} }
pub fn widget(self: *Matrix) *Widget { pub fn animation(self: *Matrix) Animation {
if (self.instance) |*instance| return instance; return Animation.init(self, deinit, realloc, draw);
self.instance = Widget.init(
"Matrix",
null,
self,
deinit,
realloc,
draw,
update,
null,
calculateTimeout,
);
return &self.instance.?;
} }
fn deinit(self: *Matrix) void { fn deinit(self: *Matrix) void {
@@ -103,7 +66,7 @@ fn deinit(self: *Matrix) void {
self.allocator.free(self.lines); self.allocator.free(self.lines);
} }
fn realloc(self: *Matrix) !void { fn realloc(self: *Matrix) anyerror!void {
const dots = try self.allocator.realloc(self.dots, self.terminal_buffer.width * (self.terminal_buffer.height + 1)); const dots = try self.allocator.realloc(self.dots, self.terminal_buffer.width * (self.terminal_buffer.height + 1));
const lines = try self.allocator.realloc(self.lines, self.terminal_buffer.width); const lines = try self.allocator.realloc(self.lines, self.terminal_buffer.width);
@@ -114,8 +77,6 @@ fn realloc(self: *Matrix) !void {
} }
fn draw(self: *Matrix) void { fn draw(self: *Matrix) void {
if (!self.animate.*) return;
const buf_height = self.terminal_buffer.height; const buf_height = self.terminal_buffer.height;
const buf_width = self.terminal_buffer.width; const buf_width = self.terminal_buffer.width;
self.count += 1; self.count += 1;
@@ -125,17 +86,17 @@ fn draw(self: *Matrix) void {
self.count = 0; self.count = 0;
var x: usize = 0; var x: usize = 0;
while (x < buf_width) : (x += 2) { while (x < self.terminal_buffer.width) : (x += 2) {
var tail: usize = 0; var tail: usize = 0;
var line = &self.lines[x]; var line = &self.lines[x];
if (self.frame <= line.update) continue; if (self.frame <= line.update) continue;
if (self.dots[x].value == null and self.dots[buf_width + x].value == ' ') { if (self.dots[x].value == null and self.dots[self.terminal_buffer.width + x].value == ' ') {
if (line.space > 0) { if (line.space > 0) {
line.space -= 1; line.space -= 1;
} else { } else {
const randint = self.terminal_buffer.random.int(u16); const randint = self.terminal_buffer.random.int(u16);
const h = buf_height; const h = self.terminal_buffer.height;
line.length = @mod(randint, h - 3) + 3; line.length = @mod(randint, h - 3) + 3;
self.dots[x].value = @mod(randint, self.max_codepoint) + self.min_codepoint; self.dots[x].value = @mod(randint, self.max_codepoint) + self.min_codepoint;
line.space = @mod(randint, h + 1); line.space = @mod(randint, h + 1);
@@ -192,7 +153,7 @@ fn draw(self: *Matrix) void {
var x: usize = 0; var x: usize = 0;
while (x < buf_width) : (x += 2) { while (x < buf_width) : (x += 2) {
var y: usize = 1; var y: usize = 1;
while (y <= buf_height) : (y += 1) { while (y <= self.terminal_buffer.height) : (y += 1) {
const dot = self.dots[buf_width * y + x]; const dot = self.dots[buf_width * y + x];
const cell = if (dot.value == null or dot.value == ' ') self.default_cell else Cell{ const cell = if (dot.value == null or dot.value == ' ') self.default_cell else Cell{
.ch = @intCast(dot.value.?), .ch = @intCast(dot.value.?),
@@ -207,18 +168,6 @@ fn draw(self: *Matrix) void {
} }
} }
fn update(self: *Matrix, _: *anyopaque) !void {
const time = try interop.getTimeOfDay();
if (self.timeout_sec > 0 and time.seconds - self.start_time.seconds > self.timeout_sec) {
self.animate.* = false;
}
}
fn calculateTimeout(self: *Matrix, _: *anyopaque) !?usize {
return self.frame_delay;
}
fn initBuffers(dots: []Dot, lines: []Line, width: usize, height: usize, random: Random) void { fn initBuffers(dots: []Dot, lines: []Line, width: usize, height: usize, random: Random) void {
var y: usize = 0; var y: usize = 0;
while (y <= height) : (y += 1) { while (y <= height) : (y += 1) {

View File

@@ -1,17 +1,16 @@
const std = @import("std"); const std = @import("std");
const Md5 = std.crypto.hash.Md5;
const builtin = @import("builtin");
const build_options = @import("build_options"); const build_options = @import("build_options");
const builtin = @import("builtin");
const enums = @import("enums.zig");
const Environment = @import("Environment.zig");
const interop = @import("interop.zig");
const SharedError = @import("SharedError.zig");
const LogFile = @import("LogFile.zig");
const ly_core = @import("ly-ui").ly_core; const Md5 = std.crypto.hash.Md5;
const interop = ly_core.interop;
const SharedError = ly_core.SharedError;
const LogFile = ly_core.LogFile;
const utmp = interop.utmp; const utmp = interop.utmp;
const Utmp = utmp.utmpx; const Utmp = utmp.utmpx;
const Environment = @import("Environment.zig");
pub const AuthOptions = struct { pub const AuthOptions = struct {
tty: u8, tty: u8,
service_name: [:0]const u8, service_name: [:0]const u8,
@@ -21,22 +20,20 @@ pub const AuthOptions = struct {
setup_cmd: []const u8, setup_cmd: []const u8,
login_cmd: ?[]const u8, login_cmd: ?[]const u8,
x_cmd: []const u8, x_cmd: []const u8,
x_vt: ?u8,
session_pid: std.posix.pid_t, session_pid: std.posix.pid_t,
use_kmscon_vt: bool,
}; };
var xorg_pid: std.posix.pid_t = 0; var xorg_pid: std.posix.pid_t = 0;
pub fn xorgSignalHandler(sig: std.posix.SIG) callconv(.c) void { pub fn xorgSignalHandler(i: c_int) callconv(.c) void {
if (xorg_pid > 0) _ = std.c.kill(xorg_pid, sig); if (xorg_pid > 0) _ = std.c.kill(xorg_pid, i);
} }
var child_pid: std.posix.pid_t = 0; var child_pid: std.posix.pid_t = 0;
pub fn sessionSignalHandler(sig: std.posix.SIG) callconv(.c) void { pub fn sessionSignalHandler(i: c_int) callconv(.c) void {
if (child_pid > 0) _ = std.c.kill(child_pid, sig); if (child_pid > 0) _ = std.c.kill(child_pid, i);
} }
pub fn authenticate(allocator: std.mem.Allocator, io: std.Io, log_file: *LogFile, options: AuthOptions, current_environment: Environment, login: []const u8, password: []const u8) !void { pub fn authenticate(allocator: std.mem.Allocator, log_file: *LogFile, options: AuthOptions, current_environment: Environment, login: []const u8, password: []const u8) !void {
var tty_buffer: [3]u8 = undefined; var tty_buffer: [3]u8 = undefined;
const tty_str = try std.fmt.bufPrint(&tty_buffer, "{d}", .{options.tty}); const tty_str = try std.fmt.bufPrint(&tty_buffer, "{d}", .{options.tty});
@@ -44,11 +41,9 @@ pub fn authenticate(allocator: std.mem.Allocator, io: std.Io, log_file: *LogFile
const pam_tty_str = try std.fmt.bufPrintZ(&pam_tty_buffer, "tty{d}", .{options.tty}); const pam_tty_str = try std.fmt.bufPrintZ(&pam_tty_buffer, "tty{d}", .{options.tty});
// Set the XDG environment variables // Set the XDG environment variables
try log_file.info(io, "auth/env", "setting xdg environment variables", .{});
try setXdgEnv(allocator, tty_str, current_environment); try setXdgEnv(allocator, tty_str, current_environment);
// Open the PAM session // Open the PAM session
try log_file.info(io, "auth/pam", "encoding credentials", .{});
const login_z = try allocator.dupeZ(u8, login); const login_z = try allocator.dupeZ(u8, login);
defer allocator.free(login_z); defer allocator.free(login_z);
@@ -63,36 +58,37 @@ pub fn authenticate(allocator: std.mem.Allocator, io: std.Io, log_file: *LogFile
}; };
var handle: ?*interop.pam.pam_handle = undefined; var handle: ?*interop.pam.pam_handle = undefined;
try log_file.info(io, "auth/pam", "starting session", .{}); var log_writer = &log_file.file_writer.interface;
try log_writer.writeAll("[pam] starting session\n");
var status = interop.pam.pam_start(options.service_name, null, &conv, &handle); var status = interop.pam.pam_start(options.service_name, null, &conv, &handle);
if (status != interop.pam.PAM_SUCCESS) return pamDiagnose(status); if (status != interop.pam.PAM_SUCCESS) return pamDiagnose(status);
defer _ = interop.pam.pam_end(handle, status); defer _ = interop.pam.pam_end(handle, status);
// Set PAM_TTY as the current TTY. This is required in case it isn't being set by another PAM module // Set PAM_TTY as the current TTY. This is required in case it isn't being set by another PAM module
try log_file.info(io, "auth/pam", "setting tty", .{}); try log_writer.writeAll("[pam] setting tty\n");
status = interop.pam.pam_set_item(handle, interop.pam.PAM_TTY, pam_tty_str.ptr); status = interop.pam.pam_set_item(handle, interop.pam.PAM_TTY, pam_tty_str.ptr);
if (status != interop.pam.PAM_SUCCESS) return pamDiagnose(status); if (status != interop.pam.PAM_SUCCESS) return pamDiagnose(status);
// Do the PAM routine // Do the PAM routine
try log_file.info(io, "auth/pam", "authenticating", .{}); try log_writer.writeAll("[pam] authenticating\n");
status = interop.pam.pam_authenticate(handle, 0); status = interop.pam.pam_authenticate(handle, 0);
if (status != interop.pam.PAM_SUCCESS) return pamDiagnose(status); if (status != interop.pam.PAM_SUCCESS) return pamDiagnose(status);
try log_file.info(io, "auth/pam", "validating account", .{}); try log_writer.writeAll("[pam] validating account\n");
status = interop.pam.pam_acct_mgmt(handle, 0); status = interop.pam.pam_acct_mgmt(handle, 0);
if (status != interop.pam.PAM_SUCCESS) return pamDiagnose(status); if (status != interop.pam.PAM_SUCCESS) return pamDiagnose(status);
try log_file.info(io, "auth/pam", "setting credentials", .{}); try log_writer.writeAll("[pam] setting credentials\n");
status = interop.pam.pam_setcred(handle, interop.pam.PAM_ESTABLISH_CRED); status = interop.pam.pam_setcred(handle, interop.pam.PAM_ESTABLISH_CRED);
if (status != interop.pam.PAM_SUCCESS) return pamDiagnose(status); if (status != interop.pam.PAM_SUCCESS) return pamDiagnose(status);
defer status = interop.pam.pam_setcred(handle, interop.pam.PAM_DELETE_CRED); defer status = interop.pam.pam_setcred(handle, interop.pam.PAM_DELETE_CRED);
try log_file.info(io, "auth/pam", "opening session", .{}); try log_writer.writeAll("[pam] opening session\n");
status = interop.pam.pam_open_session(handle, 0); status = interop.pam.pam_open_session(handle, 0);
if (status != interop.pam.PAM_SUCCESS) return pamDiagnose(status); if (status != interop.pam.PAM_SUCCESS) return pamDiagnose(status);
defer status = interop.pam.pam_close_session(handle, 0); defer status = interop.pam.pam_close_session(handle, 0);
try log_file.info(io, "auth/passwd", "getting struct", .{});
var user_entry: interop.UsernameEntry = undefined; var user_entry: interop.UsernameEntry = undefined;
{ {
defer interop.closePasswordDatabase(); defer interop.closePasswordDatabase();
@@ -102,27 +98,28 @@ pub fn authenticate(allocator: std.mem.Allocator, io: std.Io, log_file: *LogFile
} }
// Set user shell if it hasn't already been set // Set user shell if it hasn't already been set
try log_file.info(io, "auth/passwd", "setting user shell", .{});
if (user_entry.shell == null) interop.setUserShell(&user_entry); if (user_entry.shell == null) interop.setUserShell(&user_entry);
var shared_err = try SharedError.init(null, null); var shared_err = try SharedError.init();
defer shared_err.deinit(); defer shared_err.deinit();
log_file.deinit(io); log_file.deinit();
child_pid = std.posix.system.fork(); child_pid = try std.posix.fork();
if (child_pid == 0) { if (child_pid == 0) {
try log_file.reinit(io); try log_file.reinit();
try log_file.info(io, "auth/sys", "starting session", .{}); log_writer = &log_file.file_writer.interface;
startSession(log_file, allocator, io, options, tty_str, user_entry, handle, current_environment) catch |e| { try log_writer.writeAll("starting session\n");
startSession(log_file, allocator, options, tty_str, user_entry, handle, current_environment) catch |e| {
shared_err.writeError(e); shared_err.writeError(e);
log_file.deinit(io); log_file.deinit();
std.process.exit(1); std.process.exit(1);
}; };
log_file.deinit(io); log_file.deinit();
std.process.exit(0); std.process.exit(0);
} }
@@ -132,8 +129,7 @@ pub fn authenticate(allocator: std.mem.Allocator, io: std.Io, log_file: *LogFile
// If an error occurs here, we can send SIGTERM to the session // If an error occurs here, we can send SIGTERM to the session
errdefer cleanup: { errdefer cleanup: {
std.posix.kill(child_pid, std.posix.SIG.TERM) catch break :cleanup; std.posix.kill(child_pid, std.posix.SIG.TERM) catch break :cleanup;
var child_status: c_int = undefined; _ = std.posix.waitpid(child_pid, 0);
_ = std.posix.system.waitpid(child_pid, &child_status, 0);
} }
// If we receive SIGTERM, forward it to child_pid // If we receive SIGTERM, forward it to child_pid
@@ -144,15 +140,13 @@ pub fn authenticate(allocator: std.mem.Allocator, io: std.Io, log_file: *LogFile
}; };
std.posix.sigaction(std.posix.SIG.TERM, &act, null); std.posix.sigaction(std.posix.SIG.TERM, &act, null);
try addUtmpEntry(io, &entry, user_entry.username.?, child_pid); try addUtmpEntry(&entry, user_entry.username.?, child_pid);
} }
// Wait for the session to stop // Wait for the session to stop
var child_status: c_int = undefined; _ = std.posix.waitpid(child_pid, 0);
_ = std.posix.system.waitpid(child_pid, &child_status, 0);
try log_file.reinit(io); try log_file.reinit();
try log_file.info(io, "auth/utmp", "removing utmp entry", .{});
removeUtmpEntry(&entry); removeUtmpEntry(&entry);
if (shared_err.readError()) |err| return err; if (shared_err.readError()) |err| return err;
@@ -161,7 +155,6 @@ pub fn authenticate(allocator: std.mem.Allocator, io: std.Io, log_file: *LogFile
fn startSession( fn startSession(
log_file: *LogFile, log_file: *LogFile,
allocator: std.mem.Allocator, allocator: std.mem.Allocator,
io: std.Io,
options: AuthOptions, options: AuthOptions,
tty_str: []u8, tty_str: []u8,
user_entry: interop.UsernameEntry, user_entry: interop.UsernameEntry,
@@ -169,15 +162,12 @@ fn startSession(
current_environment: Environment, current_environment: Environment,
) !void { ) !void {
// Set the user's GID & PID // Set the user's GID & PID
try log_file.info(io, "auth/passwd", "setting user context", .{});
try interop.setUserContext(allocator, user_entry); try interop.setUserContext(allocator, user_entry);
// Set up the environment // Set up the environment
try log_file.info(io, "auth/env", "setting environment variables", .{});
try initEnv(allocator, user_entry, options.path); try initEnv(allocator, user_entry, options.path);
// Reset the XDG environment variables // Reset the XDG environment variables
try log_file.info(io, "auth/env", "resetting xdg environment variables", .{});
try setXdgEnv(allocator, tty_str, current_environment); try setXdgEnv(allocator, tty_str, current_environment);
try setXdgRuntimeDir(allocator); try setXdgRuntimeDir(allocator);
@@ -186,32 +176,21 @@ fn startSession(
if (pam_env_vars == null) return error.GetEnvListFailed; if (pam_env_vars == null) return error.GetEnvListFailed;
const env_list = std.mem.span(pam_env_vars.?); const env_list = std.mem.span(pam_env_vars.?);
for (env_list) |env_var| { for (env_list) |env_var| try interop.putEnvironmentVariable(env_var);
if (env_var == null) continue;
try log_file.info(io, "auth/env", "setting pam environment variable: {s}", .{std.mem.span(env_var.?)});
try interop.putEnvironmentVariable(env_var);
}
const home_z = try allocator.dupeZ(u8, user_entry.home.?);
defer allocator.free(home_z);
// Change to the user's home directory // Change to the user's home directory
try log_file.info(io, "auth/sys", "changing cwd to user home", .{}); std.posix.chdir(user_entry.home.?) catch return error.ChangeDirectoryFailed;
if (std.posix.system.chdir(home_z.ptr) < 0) return error.ChangeDirectoryFailed;
// Signal to the session process to give up control on the TTY // Signal to the session process to give up control on the TTY
try log_file.info(io, "auth/sys", "releasing tty", .{}); std.posix.kill(options.session_pid, std.posix.SIG.CHLD) catch return error.TtyControlTransferFailed;
std.posix.kill(options.session_pid, std.posix.SIG.INT) catch return error.TtyControlTransferFailed;
// Execute what the user requested // Execute what the user requested
switch (current_environment.display_server) { switch (current_environment.display_server) {
.wayland, .shell, .custom => try executeCmd(log_file, allocator, io, user_entry.shell.?, options, current_environment.is_terminal, current_environment.cmd), .wayland, .shell, .custom => try executeCmd(log_file, allocator, user_entry.shell.?, options, current_environment.is_terminal, current_environment.cmd),
.xinitrc, .x11 => if (build_options.enable_x11_support) { .xinitrc, .x11 => if (build_options.enable_x11_support) {
var vt_buf: [5]u8 = undefined; var vt_buf: [5]u8 = undefined;
const vt = try std.fmt.bufPrint(&vt_buf, "vt{d}", .{options.x_vt orelse options.tty}); const vt = try std.fmt.bufPrint(&vt_buf, "vt{d}", .{options.tty});
try executeX11Cmd(log_file, allocator, user_entry.shell.?, user_entry.home.?, options, current_environment.cmd orelse "", vt);
try log_file.info(io, "auth/x11", "setting vt to {s}", .{vt});
try executeX11Cmd(log_file, allocator, io, user_entry.shell.?, user_entry.home.?, options, current_environment.cmd orelse "", vt);
}, },
} }
} }
@@ -253,7 +232,7 @@ fn setXdgRuntimeDir(allocator: std.mem.Allocator) !void {
// XDG_RUNTIME_DIR to fall back to directories inside user's home // XDG_RUNTIME_DIR to fall back to directories inside user's home
// directory. // directory.
if (builtin.os.tag != .freebsd) { if (builtin.os.tag != .freebsd) {
const uid = std.posix.system.getuid(); const uid = std.posix.getuid();
var uid_buffer: [32]u8 = undefined; // No UID can be larger than this var uid_buffer: [32]u8 = undefined; // No UID can be larger than this
const uid_str = try std.fmt.bufPrint(&uid_buffer, "/run/user/{d}", .{uid}); const uid_str = try std.fmt.bufPrint(&uid_buffer, "/run/user/{d}", .{uid});
@@ -323,20 +302,20 @@ fn getFreeDisplay() !u8 {
var buf: [15]u8 = undefined; var buf: [15]u8 = undefined;
var i: u8 = 0; var i: u8 = 0;
while (i < 200) : (i += 1) { while (i < 200) : (i += 1) {
const xlock = try std.fmt.bufPrintZ(&buf, "/tmp/.X{d}-lock", .{i}); const xlock = try std.fmt.bufPrint(&buf, "/tmp/.X{d}-lock", .{i});
if (interop.isError(std.posix.system.access(xlock.ptr, std.posix.F_OK))) break; std.posix.access(xlock, std.posix.F_OK) catch break;
} }
return i; return i;
} }
fn getXPid(io: std.Io, display_num: u8) !i32 { fn getXPid(display_num: u8) !i32 {
var buf: [15]u8 = undefined; var buf: [15]u8 = undefined;
const file_name = try std.fmt.bufPrint(&buf, "/tmp/.X{d}-lock", .{display_num}); const file_name = try std.fmt.bufPrint(&buf, "/tmp/.X{d}-lock", .{display_num});
const file = try std.Io.Dir.openFileAbsolute(io, file_name, .{}); const file = try std.fs.openFileAbsolute(file_name, .{});
defer file.close(io); defer file.close();
var file_buffer: [32]u8 = undefined; var file_buffer: [32]u8 = undefined;
var file_reader = file.reader(io, &file_buffer); var file_reader = file.reader(&file_buffer);
var reader = &file_reader.interface; var reader = &file_reader.interface;
var buffer: [20]u8 = undefined; var buffer: [20]u8 = undefined;
@@ -346,41 +325,41 @@ fn getXPid(io: std.Io, display_num: u8) !i32 {
return std.fmt.parseInt(i32, std.mem.trim(u8, buffer[0..written], " "), 10); return std.fmt.parseInt(i32, std.mem.trim(u8, buffer[0..written], " "), 10);
} }
fn createXauthFile(log_file: *LogFile, io: std.Io, pwd: []const u8, buffer: []u8) ![]const u8 { fn createXauthFile(pwd: []const u8, buffer: []u8) ![]const u8 {
var xauth_buf: [100]u8 = undefined; var xauth_buf: [100]u8 = undefined;
var xauth_dir: []const u8 = undefined; var xauth_dir: []const u8 = undefined;
const xdg_rt_dir = std.posix.system.getenv("XDG_RUNTIME_DIR"); const xdg_rt_dir = std.posix.getenv("XDG_RUNTIME_DIR");
var xauth_file: []const u8 = "lyxauth"; var xauth_file: []const u8 = "lyxauth";
if (xdg_rt_dir == null) no_rt_dir: { if (xdg_rt_dir == null) no_rt_dir: {
const xdg_cfg_home = std.posix.system.getenv("XDG_CONFIG_HOME"); const xdg_cfg_home = std.posix.getenv("XDG_CONFIG_HOME");
if (xdg_cfg_home == null) no_cfg_home: { if (xdg_cfg_home == null) no_cfg_home: {
xauth_dir = try std.fmt.bufPrint(&xauth_buf, "{s}/.config", .{pwd}); xauth_dir = try std.fmt.bufPrint(&xauth_buf, "{s}/.config", .{pwd});
var dir = std.Io.Dir.cwd().openDir(io, xauth_dir, .{}) catch { var dir = std.fs.cwd().openDir(xauth_dir, .{}) catch {
// xauth_dir isn't a directory // xauth_dir isn't a directory
xauth_dir = pwd; xauth_dir = pwd;
xauth_file = ".lyxauth"; xauth_file = ".lyxauth";
break :no_cfg_home; break :no_cfg_home;
}; };
dir.close(io); dir.close();
// xauth_dir is a directory, use it to store Xauthority // xauth_dir is a directory, use it to store Xauthority
xauth_dir = try std.fmt.bufPrint(&xauth_buf, "{s}/.config/ly", .{pwd}); xauth_dir = try std.fmt.bufPrint(&xauth_buf, "{s}/.config/ly", .{pwd});
} else { } else {
xauth_dir = try std.fmt.bufPrint(&xauth_buf, "{s}/ly", .{std.mem.span(xdg_cfg_home.?)}); xauth_dir = try std.fmt.bufPrint(&xauth_buf, "{s}/ly", .{xdg_cfg_home.?});
} }
const file = std.Io.Dir.cwd().openFile(io, xauth_dir, .{}) catch break :no_rt_dir; const file = std.fs.cwd().openFile(xauth_dir, .{}) catch break :no_rt_dir;
file.close(io); file.close();
// xauth_dir is a file, create the parent directory // xauth_dir is a file, create the parent directory
std.Io.Dir.createDirAbsolute(io, xauth_dir, .fromMode(777)) catch { std.posix.mkdir(xauth_dir, 777) catch {
xauth_dir = pwd; xauth_dir = pwd;
xauth_file = ".lyxauth"; xauth_file = ".lyxauth";
}; };
} else { } else {
xauth_dir = std.mem.span(xdg_rt_dir.?); xauth_dir = xdg_rt_dir.?;
} }
// Trim trailing slashes // Trim trailing slashes
@@ -390,19 +369,17 @@ fn createXauthFile(log_file: *LogFile, io: std.Io, pwd: []const u8, buffer: []u8
const xauthority: []u8 = try std.fmt.bufPrint(buffer, "{s}/{s}", .{ trimmed_xauth_dir, xauth_file }); const xauthority: []u8 = try std.fmt.bufPrint(buffer, "{s}/{s}", .{ trimmed_xauth_dir, xauth_file });
std.Io.Dir.cwd().createDirPath(io, trimmed_xauth_dir) catch {}; std.fs.cwd().makePath(trimmed_xauth_dir) catch {};
try log_file.info(io, "auth/x11", "creating xauth file: {s}", .{xauthority}); const file = try std.fs.createFileAbsolute(xauthority, .{});
file.close();
const file = try std.Io.Dir.createFileAbsolute(io, xauthority, .{});
file.close(io);
return xauthority; return xauthority;
} }
fn mcookie(io: std.Io) [Md5.digest_length * 2]u8 { fn mcookie() [Md5.digest_length * 2]u8 {
var buf: [4096]u8 = undefined; var buf: [4096]u8 = undefined;
io.random(&buf); std.crypto.random.bytes(&buf);
var out: [Md5.digest_length]u8 = undefined; var out: [Md5.digest_length]u8 = undefined;
Md5.hash(&buf, &out, .{}); Md5.hash(&buf, &out, .{});
@@ -410,92 +387,79 @@ fn mcookie(io: std.Io) [Md5.digest_length * 2]u8 {
return std.fmt.bytesToHex(&out, .lower); return std.fmt.bytesToHex(&out, .lower);
} }
fn xauth(log_file: *LogFile, allocator: std.mem.Allocator, io: std.Io, display_name: []u8, shell: [*:0]const u8, home: []const u8, xauth_buffer: []u8, options: AuthOptions) ![]const u8 { fn xauth(log_file: *LogFile, allocator: std.mem.Allocator, display_name: []u8, shell: [*:0]const u8, home: []const u8, xauth_buffer: []u8, options: AuthOptions) !void {
const xauthority = try createXauthFile(log_file, io, home, xauth_buffer); const xauthority = try createXauthFile(home, xauth_buffer);
try interop.setEnvironmentVariable(allocator, "XAUTHORITY", xauthority, true); try interop.setEnvironmentVariable(allocator, "XAUTHORITY", xauthority, true);
try interop.setEnvironmentVariable(allocator, "DISPLAY", display_name, true); try interop.setEnvironmentVariable(allocator, "DISPLAY", display_name, true);
const magic_cookie = mcookie(io); const magic_cookie = mcookie();
const pid = std.posix.system.fork(); const pid = try std.posix.fork();
if (pid == 0) { if (pid == 0) {
var cmd_buffer: [1024]u8 = undefined; var cmd_buffer: [1024]u8 = undefined;
const cmd_str = std.fmt.bufPrintZ(&cmd_buffer, "{s} add {s} . {s}", .{ options.xauth_cmd, display_name, magic_cookie }) catch std.process.exit(1); const cmd_str = std.fmt.bufPrintZ(&cmd_buffer, "{s} add {s} . {s}", .{ options.xauth_cmd, display_name, magic_cookie }) catch std.process.exit(1);
try log_file.info(io, "auth/x11", "executing: {s} -c {s}", .{ shell, cmd_str });
const args = [_:null]?[*:0]const u8{ shell, "-c", cmd_str }; const args = [_:null]?[*:0]const u8{ shell, "-c", cmd_str };
_ = std.posix.system.execve(shell, &args, std.c.environ); std.posix.execveZ(shell, &args, std.c.environ) catch {};
std.process.exit(1); std.process.exit(1);
} }
var status: c_int = undefined; const status = std.posix.waitpid(pid, 0);
const result = std.posix.system.waitpid(pid, &status, 0); if (status.status != 0) {
if (interop.isError(result) or status != 0) { try log_file.file_writer.interface.print("xauth command failed with status {d}\n", .{status.status});
try log_file.err(
io,
"auth/x11",
"xauth command failed with status: {d}",
.{status},
);
return error.XauthFailed; return error.XauthFailed;
} }
return xauthority;
} }
fn executeX11Cmd(log_file: *LogFile, allocator: std.mem.Allocator, io: std.Io, shell: []const u8, home: []const u8, options: AuthOptions, desktop_cmd: []const u8, vt: []const u8) !void { fn executeX11Cmd(log_file: *LogFile, allocator: std.mem.Allocator, shell: []const u8, home: []const u8, options: AuthOptions, desktop_cmd: []const u8, vt: []const u8) !void {
var log_writer = &log_file.file_writer.interface;
var xauth_buffer: [256]u8 = undefined; var xauth_buffer: [256]u8 = undefined;
try log_file.info(io, "auth/x11", "getting free display", .{}); try log_writer.writeAll("[x11] getting free display\n");
const display_num = try getFreeDisplay(); const display_num = try getFreeDisplay();
var buf: [4]u8 = undefined; var buf: [4]u8 = undefined;
const display_name = try std.fmt.bufPrint(&buf, ":{d}", .{display_num}); const display_name = try std.fmt.bufPrint(&buf, ":{d}", .{display_num});
try log_file.info(io, "auth/x11", "got free display: {d}", .{display_num});
const shell_z = try allocator.dupeZ(u8, shell); const shell_z = try allocator.dupeZ(u8, shell);
defer allocator.free(shell_z); defer allocator.free(shell_z);
try log_file.info(io, "auth/x11", "creating xauth file", .{}); try log_writer.writeAll("[x11] creating xauth file\n");
const xauthority = try xauth(log_file, allocator, io, display_name, shell_z, home, &xauth_buffer, options); try xauth(log_file, allocator, display_name, shell_z, home, &xauth_buffer, options);
try log_file.info(io, "auth/x11", "starting x server", .{}); try log_writer.writeAll("[x11] starting x server\n");
const pid = std.posix.system.fork(); const pid = try std.posix.fork();
if (pid == 0) { if (pid == 0) {
var cmd_buffer: [1024]u8 = undefined; var cmd_buffer: [1024]u8 = undefined;
const cmd_str = std.fmt.bufPrintZ(&cmd_buffer, "{s} {s} {s} -auth {s}", .{ options.x_cmd, display_name, vt, xauthority }) catch std.process.exit(1); const cmd_str = std.fmt.bufPrintZ(&cmd_buffer, "{s} {s} {s}", .{ options.x_cmd, display_name, vt }) catch std.process.exit(1);
try log_file.info(io, "auth/x11", "executing: {s} -c {s} -auth {s}", .{ shell, cmd_str, xauthority });
const args = [_:null]?[*:0]const u8{ shell_z, "-c", cmd_str }; const args = [_:null]?[*:0]const u8{ shell_z, "-c", cmd_str };
_ = std.posix.system.execve(shell_z, &args, std.c.environ); std.posix.execveZ(shell_z, &args, std.c.environ) catch {};
std.process.exit(1); std.process.exit(1);
} }
try log_file.info(io, "auth/x11", "waiting for xcb connection", .{}); var ok: c_int = undefined;
var ok: c_int = -1;
var xcb: ?*interop.xcb.xcb_connection_t = null; var xcb: ?*interop.xcb.xcb_connection_t = null;
while (ok != 0) { while (ok != 0) {
xcb = interop.xcb.xcb_connect(null, null); xcb = interop.xcb.xcb_connect(null, null);
ok = interop.xcb.xcb_connection_has_error(xcb); ok = interop.xcb.xcb_connection_has_error(xcb);
std.posix.kill(pid, @enumFromInt(0)) catch |e| { std.posix.kill(pid, 0) catch |e| {
if (e == error.ProcessNotFound and ok != 0) return error.XcbConnectionFailed; if (e == error.ProcessNotFound and ok != 0) return error.XcbConnectionFailed;
}; };
} }
// X Server detaches from the process. // X Server detaches from the process.
// PID can be fetched from /tmp/X{d}.lock // PID can be fetched from /tmp/X{d}.lock
try log_file.info(io, "auth/x11", "getting x server pid", .{}); try log_writer.writeAll("[x11] getting x server pid\n");
const x_pid = try getXPid(io, display_num); const x_pid = try getXPid(display_num);
try log_file.info(io, "auth/x11", "got x server pid: {d}", .{x_pid});
try log_file.info(io, "auth/x11", "launching environment", .{}); try log_writer.writeAll("[x11] launching environment\n");
xorg_pid = std.posix.system.fork(); xorg_pid = try std.posix.fork();
if (xorg_pid == 0) { if (xorg_pid == 0) {
var cmd_buffer: [1024]u8 = undefined; var cmd_buffer: [1024]u8 = undefined;
const cmd_str = std.fmt.bufPrintZ(&cmd_buffer, "{s} {s} {s} {s}", .{ if (options.use_kmscon_vt) "kmscon-launch-gui" else "", options.setup_cmd, options.login_cmd orelse "", desktop_cmd }) catch std.process.exit(1); 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);
try log_file.info(io, "auth/x11", "executing: {s} -c {s}", .{ shell, cmd_str });
const args = [_:null]?[*:0]const u8{ shell_z, "-c", cmd_str }; const args = [_:null]?[*:0]const u8{ shell_z, "-c", cmd_str };
_ = std.posix.system.execve(shell_z, &args, std.c.environ); std.posix.execveZ(shell_z, &args, std.c.environ) catch {};
std.process.exit(1); std.process.exit(1);
} }
@@ -507,83 +471,68 @@ fn executeX11Cmd(log_file: *LogFile, allocator: std.mem.Allocator, io: std.Io, s
}; };
std.posix.sigaction(std.posix.SIG.TERM, &act, null); std.posix.sigaction(std.posix.SIG.TERM, &act, null);
var xorg_status: c_int = undefined; _ = std.posix.waitpid(xorg_pid, 0);
_ = std.posix.system.waitpid(xorg_pid, &xorg_status, 0);
try log_file.info(io, "auth/x11", "disconnecting xcb", .{});
interop.xcb.xcb_disconnect(xcb); interop.xcb.xcb_disconnect(xcb);
// TODO: Find a more robust way to ensure that X has been terminated (pidfds?) // TODO: Find a more robust way to ensure that X has been terminated (pidfds?)
std.posix.kill(x_pid, std.posix.SIG.TERM) catch {}; std.posix.kill(x_pid, std.posix.SIG.TERM) catch {};
io.sleep(.fromSeconds(1), .real) catch {}; // Wait 1 second before sending SIGKILL std.Thread.sleep(std.time.ns_per_s * 1); // Wait 1 second before sending SIGKILL
std.posix.kill(x_pid, std.posix.SIG.KILL) catch return; std.posix.kill(x_pid, std.posix.SIG.KILL) catch return;
var x_status: c_int = undefined; _ = std.posix.waitpid(x_pid, 0);
_ = std.posix.system.waitpid(x_pid, &x_status, 0);
} }
fn executeCmd(global_log_file: *LogFile, allocator: std.mem.Allocator, io: std.Io, shell: []const u8, options: AuthOptions, is_terminal: bool, exec_cmd: ?[]const u8) !void { fn executeCmd(global_log_file: *LogFile, allocator: std.mem.Allocator, shell: []const u8, options: AuthOptions, is_terminal: bool, exec_cmd: ?[]const u8) !void {
try global_log_file.info(io, "auth/sys", "launching wayland/shell/custom session", .{}); var maybe_log_file: ?std.fs.File = null;
if (!is_terminal) {
var maybe_log_file: ?std.Io.File = null;
if (!is_terminal) redirect_streams: {
if (options.use_kmscon_vt) {
try global_log_file.err(io, "auth/sys", "cannot redirect stdio & stderr with kmscon", .{});
break :redirect_streams;
}
// For custom desktop entries, the "Terminal" value here determines if // For custom desktop entries, the "Terminal" value here determines if
// we redirect standard output & error or not. That is, we redirect only // we redirect standard output & error or not. That is, we redirect only
// if it's equal to false (so if it's not running in a TTY). // if it's equal to false (so if it's not running in a TTY).
if (options.session_log) |log_path| { if (options.session_log) |log_path| {
try global_log_file.info(io, "auth/sys", "setting up stdio & stderr redirection", .{}); maybe_log_file = try redirectStandardStreams(global_log_file, log_path, true);
maybe_log_file = try redirectStandardStreams(global_log_file, io, log_path, true);
} }
} }
defer if (maybe_log_file) |log_file| log_file.close(io); defer if (maybe_log_file) |log_file| log_file.close();
const shell_z = try allocator.dupeZ(u8, shell); const shell_z = try allocator.dupeZ(u8, shell);
defer allocator.free(shell_z); defer allocator.free(shell_z);
var cmd_buffer: [1024]u8 = undefined; var cmd_buffer: [1024]u8 = undefined;
const cmd_str = try std.fmt.bufPrintZ(&cmd_buffer, "{s} {s} {s} {s}", .{ if (!is_terminal and options.use_kmscon_vt) "kmscon-launch-gui" else "", options.setup_cmd, options.login_cmd orelse "", exec_cmd orelse shell }); const cmd_str = try std.fmt.bufPrintZ(&cmd_buffer, "{s} {s} {s}", .{ options.setup_cmd, options.login_cmd orelse "", exec_cmd orelse shell });
try global_log_file.info(io, "auth/sys", "executing: {s} -c {s}", .{ shell, cmd_str });
const args = [_:null]?[*:0]const u8{ shell_z, "-c", cmd_str }; const args = [_:null]?[*:0]const u8{ shell_z, "-c", cmd_str };
_ = std.posix.system.execve(shell_z, &args, std.c.environ); return std.posix.execveZ(shell_z, &args, std.c.environ);
return error.CmdExecveFailed;
} }
fn redirectStandardStreams(global_log_file: *LogFile, io: std.Io, session_log: []const u8, create: bool) !std.Io.File { fn redirectStandardStreams(global_log_file: *LogFile, session_log: []const u8, create: bool) !std.fs.File {
create_session_log_dir: { create_session_log_dir: {
const session_log_dir = std.Io.Dir.path.dirname(session_log) orelse break :create_session_log_dir; const session_log_dir = std.fs.path.dirname(session_log) orelse break :create_session_log_dir;
std.Io.Dir.cwd().createDirPath(io, session_log_dir) catch |err| { std.fs.cwd().makePath(session_log_dir) catch |err| {
try global_log_file.err(io, "auth/sys", "failed to create session log file directory: {s}", .{@errorName(err)}); try global_log_file.file_writer.interface.print("failed to create session log file directory: {s}\n", .{@errorName(err)});
return err; return err;
}; };
} }
const log_file = if (create) (std.Io.Dir.cwd().createFile(io, session_log, .{ .permissions = .fromMode(0o666) }) catch |err| { const log_file = if (create) (std.fs.cwd().createFile(session_log, .{ .mode = 0o666 }) catch |err| {
try global_log_file.err(io, "auth/sys", "failed to create new session log file: {s}", .{@errorName(err)}); try global_log_file.file_writer.interface.print("failed to create new session log file: {s}\n", .{@errorName(err)});
return err; return err;
}) else (std.Io.Dir.cwd().openFile(io, session_log, .{ .mode = .read_write }) catch |err| { }) else (std.fs.cwd().openFile(session_log, .{ .mode = .read_write }) catch |err| {
try global_log_file.err(io, "auth/sys", "failed to open existing session log file: {s}", .{@errorName(err)}); try global_log_file.file_writer.interface.print("failed to open existing session log file: {s}\n", .{@errorName(err)});
return err; return err;
}); });
if (interop.isError(std.posix.system.dup2(std.posix.STDOUT_FILENO, std.posix.STDERR_FILENO))) return error.StdoutDup2Failed; try std.posix.dup2(std.posix.STDOUT_FILENO, std.posix.STDERR_FILENO);
if (interop.isError(std.posix.system.dup2(log_file.handle, std.posix.STDOUT_FILENO))) return error.LogFileDup2Failed; try std.posix.dup2(log_file.handle, std.posix.STDOUT_FILENO);
return log_file; return log_file;
} }
fn addUtmpEntry(io: std.Io, entry: *Utmp, username: []const u8, pid: c_int) !void { fn addUtmpEntry(entry: *Utmp, username: []const u8, pid: c_int) !void {
entry.ut_type = utmp.USER_PROCESS; entry.ut_type = utmp.USER_PROCESS;
entry.ut_pid = pid; entry.ut_pid = pid;
var buf: [std.Io.Dir.max_path_bytes]u8 = undefined; var buf: [std.fs.max_path_bytes]u8 = undefined;
const length = try std.Io.File.stdin().realPath(io, &buf); const tty_path = try std.os.getFdPath(std.posix.STDIN_FILENO, &buf);
const tty_path = buf[0..length];
// Get the TTY name (i.e. without the /dev/ prefix) // Get the TTY name (i.e. without the /dev/ prefix)
var ttyname_buf: [@sizeOf(@TypeOf(entry.ut_line))]u8 = undefined; var ttyname_buf: [@sizeOf(@TypeOf(entry.ut_line))]u8 = undefined;

58
src/bigclock.zig Normal file
View File

@@ -0,0 +1,58 @@
const std = @import("std");
const interop = @import("interop.zig");
const enums = @import("enums.zig");
const Lang = @import("bigclock/Lang.zig");
const en = @import("bigclock/en.zig");
const fa = @import("bigclock/fa.zig");
const Cell = @import("tui/Cell.zig");
const Bigclock = enums.Bigclock;
pub const WIDTH = Lang.WIDTH;
pub const HEIGHT = Lang.HEIGHT;
pub const SIZE = Lang.SIZE;
pub fn clockCell(animate: bool, char: u8, fg: u32, bg: u32, bigclock: Bigclock) ![SIZE]Cell {
var cells: [SIZE]Cell = undefined;
const time = try interop.getTimeOfDay();
const clock_chars = toBigNumber(if (animate and char == ':' and @divTrunc(time.microseconds, 500000) != 0) ' ' else char, bigclock);
for (0..cells.len) |i| cells[i] = Cell.init(clock_chars[i], fg, bg);
return cells;
}
pub fn alphaBlit(x: usize, y: usize, tb_width: usize, tb_height: usize, cells: [SIZE]Cell) void {
if (x + WIDTH >= tb_width or y + HEIGHT >= tb_height) return;
for (0..HEIGHT) |yy| {
for (0..WIDTH) |xx| {
const cell = cells[yy * WIDTH + xx];
cell.put(x + xx, y + yy);
}
}
}
fn toBigNumber(char: u8, bigclock: Bigclock) [SIZE]u21 {
const locale_chars = switch (bigclock) {
.fa => fa.locale_chars,
.en => en.locale_chars,
.none => unreachable,
};
return switch (char) {
'0' => locale_chars.ZERO,
'1' => locale_chars.ONE,
'2' => locale_chars.TWO,
'3' => locale_chars.THREE,
'4' => locale_chars.FOUR,
'5' => locale_chars.FIVE,
'6' => locale_chars.SIX,
'7' => locale_chars.SEVEN,
'8' => locale_chars.EIGHT,
'9' => locale_chars.NINE,
'p', 'P' => locale_chars.P,
'a', 'A' => locale_chars.A,
'm', 'M' => locale_chars.M,
':' => locale_chars.S,
else => locale_chars.E,
};
}

28
src/bigclock/Lang.zig Normal file
View File

@@ -0,0 +1,28 @@
const interop = @import("../interop.zig");
pub const WIDTH = 5;
pub const HEIGHT = 5;
pub const SIZE = WIDTH * HEIGHT;
pub const X: u32 = if (interop.supportsUnicode()) 0x2593 else '#';
pub const O: u32 = 0;
// zig fmt: off
pub const LocaleChars = struct {
ZERO: [SIZE]u21,
ONE: [SIZE]u21,
TWO: [SIZE]u21,
THREE: [SIZE]u21,
FOUR: [SIZE]u21,
FIVE: [SIZE]u21,
SIX: [SIZE]u21,
SEVEN: [SIZE]u21,
EIGHT: [SIZE]u21,
NINE: [SIZE]u21,
S: [SIZE]u21,
E: [SIZE]u21,
P: [SIZE]u21,
A: [SIZE]u21,
M: [SIZE]u21,
};
// zig fmt: on

View File

@@ -1,7 +1,8 @@
const BigLabel = @import("../BigLabel.zig"); const Lang = @import("Lang.zig");
const LocaleChars = BigLabel.LocaleChars;
const X = BigLabel.X; const LocaleChars = Lang.LocaleChars;
const O = BigLabel.O; const X = Lang.X;
const O = Lang.O;
// zig fmt: off // zig fmt: off
pub const locale_chars = LocaleChars{ pub const locale_chars = LocaleChars{

View File

@@ -1,7 +1,8 @@
const BigLabel = @import("../BigLabel.zig"); const Lang = @import("Lang.zig");
const LocaleChars = BigLabel.LocaleChars;
const X = BigLabel.X; const LocaleChars = Lang.LocaleChars;
const O = BigLabel.O; const X = Lang.X;
const O = Lang.O;
// zig fmt: off // zig fmt: off
pub const locale_chars = LocaleChars{ pub const locale_chars = LocaleChars{

View File

@@ -1,118 +0,0 @@
const std = @import("std");
const Allocator = std.mem.Allocator;
const ly_ui = @import("ly-ui");
const keyboard = ly_ui.keyboard;
const TerminalBuffer = ly_ui.TerminalBuffer;
const Widget = ly_ui.Widget;
const CyclableLabel = ly_ui.CyclableLabel;
const MessageLabel = CyclableLabel(Message, Message);
const InfoLine = @This();
const Message = struct {
width: usize,
text: []const u8,
bg: u32,
fg: u32,
};
instance: ?Widget = null,
label: *MessageLabel,
pub fn init(
allocator: Allocator,
io: std.Io,
buffer: *TerminalBuffer,
width: usize,
arrow_fg: u32,
arrow_bg: u32,
) !InfoLine {
return .{
.instance = null,
.label = try MessageLabel.init(
allocator,
io,
buffer,
drawItem,
null,
null,
width,
true,
arrow_fg,
arrow_bg,
),
};
}
pub fn deinit(self: *InfoLine) void {
self.label.deinit();
}
pub fn widget(self: *InfoLine) *Widget {
if (self.instance) |*instance| return instance;
self.instance = Widget.init(
"InfoLine",
self.label.keybinds,
self,
deinit,
null,
draw,
null,
handle,
null,
);
return &self.instance.?;
}
pub fn addMessage(self: *InfoLine, text: []const u8, bg: u32, fg: u32) !void {
if (text.len == 0) return;
try self.label.addItem(.{
.width = TerminalBuffer.strWidth(text),
.text = text,
.bg = bg,
.fg = fg,
});
}
pub fn clearRendered(self: InfoLine, allocator: Allocator) !void {
// Draw over the area
const spaces = try allocator.alloc(u8, self.label.width - 2);
defer allocator.free(spaces);
@memset(spaces, ' ');
TerminalBuffer.drawText(
spaces,
self.label.component_pos.x + 2,
self.label.component_pos.y,
TerminalBuffer.Color.DEFAULT,
TerminalBuffer.Color.DEFAULT,
);
}
fn draw(self: *InfoLine) void {
self.label.draw();
}
fn handle(self: *InfoLine, maybe_key: ?keyboard.Key) !void {
self.label.handle(maybe_key);
}
fn drawItem(label: *MessageLabel, message: Message, x: usize, y: usize, width: usize) void {
if (message.width == 0) return;
const x_offset = if (label.text_in_center and width >= message.width) (width - message.width) / 2 else 0;
label.cursor = message.width + x_offset;
TerminalBuffer.drawConfinedText(
message.text,
x + x_offset,
y,
width,
message.fg,
message.bg,
);
}

View File

@@ -1,123 +0,0 @@
const std = @import("std");
const Allocator = std.mem.Allocator;
const ly_ui = @import("ly-ui");
const keyboard = ly_ui.keyboard;
const TerminalBuffer = ly_ui.TerminalBuffer;
const Widget = ly_ui.Widget;
const CyclableLabel = ly_ui.CyclableLabel;
const UserList = @import("UserList.zig");
const Environment = @import("../Environment.zig");
const Env = struct {
environment: Environment,
index: usize,
};
const EnvironmentLabel = CyclableLabel(Env, *UserList);
const Session = @This();
instance: ?Widget = null,
label: *EnvironmentLabel,
user_list: *UserList,
pub fn init(
allocator: Allocator,
io: std.Io,
buffer: *TerminalBuffer,
user_list: *UserList,
width: usize,
text_in_center: bool,
fg: u32,
bg: u32,
) !Session {
return .{
.instance = null,
.label = try EnvironmentLabel.init(
allocator,
io,
buffer,
drawItem,
sessionChanged,
user_list,
width,
text_in_center,
fg,
bg,
),
.user_list = user_list,
};
}
pub fn deinit(self: *Session) void {
for (self.label.list.items) |*env| {
if (env.environment.entry_ini) |*entry_ini| entry_ini.deinit();
self.label.allocator.free(env.environment.file_name);
}
self.label.deinit();
}
pub fn widget(self: *Session) *Widget {
if (self.instance) |*instance| return instance;
self.instance = Widget.init(
"Session",
self.label.keybinds,
self,
deinit,
null,
draw,
null,
handle,
null,
);
return &self.instance.?;
}
pub fn addEnvironment(self: *Session, environment: Environment) !void {
const env = Env{ .environment = environment, .index = self.label.list.items.len };
try self.label.addItem(env);
addedSession(env, self.user_list);
}
fn draw(self: *Session) void {
self.label.draw();
}
fn handle(self: *Session, maybe_key: ?keyboard.Key) !void {
self.label.handle(maybe_key);
}
fn addedSession(env: Env, user_list: *UserList) void {
const user = user_list.label.list.items[user_list.label.current];
if (!user.first_run) return;
user.session_index.* = env.index;
}
fn sessionChanged(env: Env, maybe_user_list: ?*UserList) void {
if (maybe_user_list) |user_list| {
user_list.label.list.items[user_list.label.current].session_index.* = env.index;
}
}
fn drawItem(label: *EnvironmentLabel, env: Env, x: usize, y: usize, width: usize) void {
if (width < 3) return;
const length = @min(TerminalBuffer.strWidth(env.environment.name), width - 3);
if (length == 0) return;
const x_offset = if (label.text_in_center and width >= length) (width - length) / 2 else 0;
label.cursor = length + x_offset;
TerminalBuffer.drawConfinedText(
env.environment.name,
x + x_offset,
y,
width,
label.fg,
label.bg,
);
}

View File

@@ -1,147 +0,0 @@
const std = @import("std");
const Allocator = std.mem.Allocator;
const ly_ui = @import("ly-ui");
const keyboard = ly_ui.keyboard;
const TerminalBuffer = ly_ui.TerminalBuffer;
const Widget = ly_ui.Widget;
const CyclableLabel = ly_ui.CyclableLabel;
const Session = @import("Session.zig");
const SavedUsers = @import("../config/SavedUsers.zig");
const StringList = std.ArrayListUnmanaged([]const u8);
pub const User = struct {
name: []const u8,
session_index: *usize,
allocated_index: bool,
first_run: bool,
};
const UserLabel = CyclableLabel(User, *Session);
const UserList = @This();
instance: ?Widget = null,
label: *UserLabel,
pub fn init(
allocator: Allocator,
io: std.Io,
buffer: *TerminalBuffer,
usernames: StringList,
saved_users: *SavedUsers,
session: *Session,
width: usize,
text_in_center: bool,
fg: u32,
bg: u32,
) !UserList {
var user_list = UserList{
.instance = null,
.label = try UserLabel.init(
allocator,
io,
buffer,
drawItem,
usernameChanged,
session,
width,
text_in_center,
fg,
bg,
),
};
for (usernames.items) |username| {
if (username.len == 0) continue;
var maybe_session_index: ?*usize = null;
var first_run = true;
for (saved_users.user_list.items) |*saved_user| {
if (std.mem.eql(u8, username, saved_user.username)) {
maybe_session_index = &saved_user.session_index;
first_run = saved_user.first_run;
break;
}
}
var allocated_index = false;
if (maybe_session_index == null) {
maybe_session_index = try allocator.create(usize);
maybe_session_index.?.* = 0;
allocated_index = true;
}
try user_list.label.addItem(.{
.name = username,
.session_index = maybe_session_index.?,
.allocated_index = allocated_index,
.first_run = first_run,
});
}
return user_list;
}
pub fn deinit(self: *UserList) void {
for (self.label.list.items) |user| {
if (user.allocated_index) {
self.label.allocator.destroy(user.session_index);
}
}
self.label.deinit();
}
pub fn widget(self: *UserList) *Widget {
if (self.instance) |*instance| return instance;
self.instance = Widget.init(
"UserList",
self.label.keybinds,
self,
deinit,
null,
draw,
null,
handle,
null,
);
return &self.instance.?;
}
pub fn getCurrentUsername(self: UserList) []const u8 {
return self.label.list.items[self.label.current].name;
}
fn draw(self: *UserList) void {
self.label.draw();
}
fn handle(self: *UserList, maybe_key: ?keyboard.Key) !void {
self.label.handle(maybe_key);
}
fn usernameChanged(user: User, maybe_session: ?*Session) void {
if (maybe_session) |session| {
session.label.current = @min(user.session_index.*, session.label.list.items.len - 1);
}
}
fn drawItem(label: *UserLabel, user: User, x: usize, y: usize, width: usize) void {
if (width < 3) return;
const length = @min(TerminalBuffer.strWidth(user.name), width - 3);
if (length == 0) return;
const x_offset = if (label.text_in_center and width >= length) (width - length) / 2 else 0;
label.cursor = length + x_offset;
TerminalBuffer.drawConfinedText(
user.name,
x + x_offset,
y,
width,
label.fg,
label.bg,
);
}

View File

@@ -1,15 +1,13 @@
const build_options = @import("build_options"); const build_options = @import("build_options");
const enums = @import("../enums.zig"); const enums = @import("../enums.zig");
const Animation = enums.Animation; const Animation = enums.Animation;
const Input = enums.Input; const Input = enums.Input;
const ViMode = enums.ViMode; const ViMode = enums.ViMode;
const Bigclock = enums.Bigclock; const Bigclock = enums.Bigclock;
const DurOffsetAlignment = enums.DurOffsetAlignment;
allow_empty_password: bool = true, allow_empty_password: bool = true,
animation: Animation = .none, animation: Animation = .none,
animation_frame_delay: u16 = 5,
animation_timeout_sec: u12 = 0, animation_timeout_sec: u12 = 0,
asterisk: ?u32 = '*', asterisk: ?u32 = '*',
auth_fails: u64 = 10, auth_fails: u64 = 10,
@@ -37,7 +35,6 @@ cmatrix_max_codepoint: u16 = 0x7B,
colormix_col1: u32 = 0x00FF0000, colormix_col1: u32 = 0x00FF0000,
colormix_col2: u32 = 0x000000FF, colormix_col2: u32 = 0x000000FF,
colormix_col3: u32 = 0x20000000, colormix_col3: u32 = 0x20000000,
custom_bind_width: ?u32 = null,
custom_sessions: []const u8 = build_options.config_directory ++ "/ly/custom-sessions", custom_sessions: []const u8 = build_options.config_directory ++ "/ly/custom-sessions",
default_input: Input = .login, default_input: Input = .login,
doom_fire_height: u8 = 6, doom_fire_height: u8 = 6,
@@ -46,9 +43,8 @@ doom_top_color: u32 = 0x00FF0000,
doom_middle_color: u32 = 0x00FFFF00, doom_middle_color: u32 = 0x00FFFF00,
doom_bottom_color: u32 = 0x00FFFFFF, doom_bottom_color: u32 = 0x00FFFFFF,
dur_file_path: []const u8 = build_options.config_directory ++ "/ly/example.dur", dur_file_path: []const u8 = build_options.config_directory ++ "/ly/example.dur",
dur_offset_alignment: DurOffsetAlignment = .center, dur_x_offset: u32 = 0,
dur_x_offset: i32 = 0, dur_y_offset: u32 = 0,
dur_y_offset: i32 = 0,
edge_margin: u8 = 0, edge_margin: u8 = 0,
error_bg: u32 = 0x00000000, error_bg: u32 = 0x00000000,
error_fg: u32 = 0x01FF0000, error_fg: u32 = 0x01FF0000,
@@ -75,6 +71,7 @@ logout_cmd: ?[]const u8 = null,
ly_log: []const u8 = "/var/log/ly.log", ly_log: []const u8 = "/var/log/ly.log",
margin_box_h: u8 = 2, margin_box_h: u8 = 2,
margin_box_v: u8 = 1, margin_box_v: u8 = 1,
min_refresh_delta: u16 = 5,
numlock: bool = false, numlock: bool = false,
path: ?[]const u8 = "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", path: ?[]const u8 = "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
restart_cmd: []const u8 = "/sbin/shutdown -r now", restart_cmd: []const u8 = "/sbin/shutdown -r now",
@@ -83,9 +80,6 @@ save: bool = true,
service_name: [:0]const u8 = "ly", service_name: [:0]const u8 = "ly",
session_log: ?[]const u8 = "ly-session.log", session_log: ?[]const u8 = "ly-session.log",
setup_cmd: []const u8 = build_options.config_directory ++ "/ly/setup.sh", setup_cmd: []const u8 = build_options.config_directory ++ "/ly/setup.sh",
shell: bool = true,
show_password_key: []const u8 = "F7",
show_tty: bool = false,
shutdown_cmd: []const u8 = "/sbin/shutdown -a now", shutdown_cmd: []const u8 = "/sbin/shutdown -a now",
shutdown_key: []const u8 = "F1", shutdown_key: []const u8 = "F1",
sleep_cmd: ?[]const u8 = null, sleep_cmd: ?[]const u8 = null,
@@ -94,9 +88,8 @@ start_cmd: ?[]const u8 = null,
text_in_center: bool = false, text_in_center: bool = false,
vi_default_mode: ViMode = .normal, vi_default_mode: ViMode = .normal,
vi_mode: bool = false, vi_mode: bool = false,
waylandsessions: ?[]const u8 = build_options.prefix_directory ++ "/share/wayland-sessions", waylandsessions: []const u8 = build_options.prefix_directory ++ "/share/wayland-sessions",
x_cmd: []const u8 = build_options.prefix_directory ++ "/bin/X", x_cmd: []const u8 = build_options.prefix_directory ++ "/bin/X",
x_vt: ?u8 = null,
xauth_cmd: []const u8 = build_options.prefix_directory ++ "/bin/xauth", xauth_cmd: []const u8 = build_options.prefix_directory ++ "/bin/xauth",
xinitrc: ?[]const u8 = "~/.xinitrc", xinitrc: ?[]const u8 = "~/.xinitrc",
xsessions: ?[]const u8 = build_options.prefix_directory ++ "/share/xsessions", xsessions: []const u8 = build_options.prefix_directory ++ "/share/xsessions",

View File

@@ -8,9 +8,6 @@ brightness_down: []const u8 = "decrease brightness",
brightness_up: []const u8 = "increase brightness", brightness_up: []const u8 = "increase brightness",
capslock: []const u8 = "capslock", capslock: []const u8 = "capslock",
custom: []const u8 = "custom", custom: []const u8 = "custom",
custom_info_err_output_long: []const u8 = "output too long",
custom_info_err_no_output: []const u8 = "no output",
custom_info_err_no_output_error: []const u8 = ", possible error",
err_alloc: []const u8 = "failed memory allocation", err_alloc: []const u8 = "failed memory allocation",
err_args: []const u8 = "unable to parse command line arguments", err_args: []const u8 = "unable to parse command line arguments",
err_autologin_session: []const u8 = "autologin session not found", err_autologin_session: []const u8 = "autologin session not found",
@@ -81,7 +78,6 @@ restart: []const u8 = "reboot",
shell: [:0]const u8 = "shell", shell: [:0]const u8 = "shell",
shutdown: []const u8 = "shutdown", shutdown: []const u8 = "shutdown",
sleep: []const u8 = "sleep", sleep: []const u8 = "sleep",
toggle_password: []const u8 = "toggle password",
wayland: []const u8 = "wayland", wayland: []const u8 = "wayland",
x11: []const u8 = "x11", x11: []const u8 = "x11",
xinitrc: [:0]const u8 = "xinitrc", xinitrc: [:0]const u8 = "xinitrc",

View File

@@ -1,25 +0,0 @@
const std = @import("std");
const custom = @This();
pub const CustomCommandBind = struct {
name: []const u8 = "",
cmd: []const u8 = "",
};
pub const UNDEFINED_CMD: []const u8 = "echo \"You forgot to define 'cmd'!\"";
pub const CustomCommandInfo = struct {
name: []const u8 = "",
cmd: ?[]const u8 = null,
/// To be set to the label's widget ID
id: u64 = 0,
/// In frames, the refresh rate for the `cmd` to run again
/// If 0, only run once.
refresh: u32 = 0,
counter: u32 = 0,
};
pub var binds: std.array_hash_map.String(CustomCommandBind) = undefined;
pub var labels: std.array_hash_map.String(CustomCommandInfo) = undefined;

View File

@@ -3,20 +3,14 @@
// Color codes interpreted differently since 1.1.0 // Color codes interpreted differently since 1.1.0
const std = @import("std"); const std = @import("std");
var temporary_allocator = std.heap.page_allocator; const ini = @import("zigini");
const ly_ui = @import("ly-ui");
const TerminalBuffer = ly_ui.TerminalBuffer;
const Color = TerminalBuffer.Color;
const Styling = TerminalBuffer.Styling;
const ly_core = ly_ui.ly_core;
const IniParser = ly_core.IniParser;
const ini = ly_core.ini;
const Config = @import("Config.zig"); const Config = @import("Config.zig");
const OldSave = @import("OldSave.zig"); const OldSave = @import("OldSave.zig");
const SavedUsers = @import("SavedUsers.zig"); const SavedUsers = @import("SavedUsers.zig");
const custom = @import("custom.zig"); const TerminalBuffer = @import("../tui/TerminalBuffer.zig");
const Color = TerminalBuffer.Color;
const Styling = TerminalBuffer.Styling;
const color_properties = [_][]const u8{ const color_properties = [_][]const u8{
"bg", "bg",
@@ -47,6 +41,8 @@ const removed_properties = [_][]const u8{
"load", "load",
}; };
var temporary_allocator = std.heap.page_allocator;
pub var auto_eight_colors: bool = true; pub var auto_eight_colors: bool = true;
pub var maybe_animate: ?bool = null; pub var maybe_animate: ?bool = null;
@@ -155,69 +151,6 @@ pub fn configFieldHandler(_: std.mem.Allocator, field: ini.IniField) ?ini.IniFie
return field; return field;
} }
if (std.mem.eql(u8, field.key, "min_refresh_delta")) {
// The option has simply been renamed
var mapped_field = field;
mapped_field.key = "animation_frame_delay";
return mapped_field;
}
// TODO: Dearest Melpert,
// I pray this message finds you well, as daylight dwindles and the witching hour
// approaches, I find it more and more imperative as time continues that I place
// this reminder here in such a format that you cannot ignore.
// Do you know how long I have been waiting for this petition to be authorized
// in regards to this particular segment of computerized instructions?
// It has been many a moon since this particular audit has been
// posted regarding the position of handling configurable literature
// apparatuses and plans for a new feature to the configuration
// interface and as time continues onwards I grow more restless
// on the progress of said interface, only to find out afterwards
// that you have PROCRASTINATED on the efforts meant to enhance
// configuration. Thus the requirement for this reminder larger
// compared to the two reminders regarding better methods of
// X termination detection and new usernames with existing
// save files.
//
// Thus is my que to leave this TODO at thy request,
//
// Forever Sullied,
//
// Ly Contributor.
//
if (std.mem.startsWith(u8, field.header, "cmd:")) {
const key = field.header["cmd:".len..];
const keyZ = temporary_allocator.dupe(u8, key) catch "";
if (!custom.binds.contains(key)) {
custom.binds.put(temporary_allocator, keyZ, .{}) catch {};
}
if (custom.binds.getPtr(keyZ)) |command| {
if (std.mem.eql(u8, field.key, "name")) {
command.name = temporary_allocator.dupe(u8, field.value) catch "";
}
if (std.mem.eql(u8, field.key, "cmd")) {
command.cmd = temporary_allocator.dupe(u8, field.value) catch "";
}
}
}
if (std.mem.startsWith(u8, field.header, "lbl:")) {
const key = field.header["lbl:".len..];
const keyZ = temporary_allocator.dupe(u8, key) catch "";
if (!custom.labels.contains(keyZ)) {
custom.labels.put(temporary_allocator, keyZ, .{ .name = keyZ }) catch {};
}
if (custom.labels.getPtr(keyZ)) |label| {
if (std.mem.eql(u8, field.key, "cmd")) {
label.cmd = temporary_allocator.dupe(u8, field.value) catch "";
}
if (std.mem.eql(u8, field.key, "refresh")) {
label.refresh = std.fmt.parseInt(u32, field.value, 10) catch 0;
}
}
}
return field; return field;
} }
@@ -254,14 +187,51 @@ pub fn lateConfigFieldHandler(config: *Config) void {
} }
} }
pub fn tryMigrateIniSaveFile(allocator: std.mem.Allocator, io: std.Io, path: []const u8, saved_users: *SavedUsers, usernames: [][]const u8) !?IniParser(OldSave) { pub fn tryMigrateFirstSaveFile(user_buf: *[32]u8) OldSave {
var save_parser = try IniParser(OldSave).init(allocator, io, path, null); var save = OldSave{};
errdefer save_parser.deinit();
if (maybe_save_file) |path| {
defer temporary_allocator.free(path);
var file = std.fs.openFileAbsolute(path, .{}) catch return save;
defer file.close();
var file_buffer: [64]u8 = undefined;
var file_reader = file.reader(&file_buffer);
var reader = &file_reader.interface;
var user_writer = std.Io.Writer.fixed(user_buf);
var written = reader.streamDelimiter(&user_writer, '\n') catch return save;
if (written > 0) save.user = user_buf[0..written];
var session_buf: [20]u8 = undefined;
var session_writer = std.Io.Writer.fixed(&session_buf);
written = reader.streamDelimiter(&session_writer, '\n') catch return save;
var session_index: ?usize = null;
if (written > 0) {
session_index = std.fmt.parseUnsigned(usize, session_buf[0..written], 10) catch return save;
}
save.session_index = session_index;
}
return save;
}
pub fn tryMigrateIniSaveFile(allocator: std.mem.Allocator, save_ini: *ini.Ini(OldSave), path: []const u8, saved_users: *SavedUsers, usernames: [][]const u8) !bool {
var old_save_file_exists = true;
var user_buf: [32]u8 = undefined; var user_buf: [32]u8 = undefined;
const maybe_save = if (save_parser.maybe_load_error == null) save_parser.structure else tryMigrateFirstSaveFile(io, &user_buf); const save = save_ini.readFileToStruct(path, .{
.fieldHandler = null,
.comment_characters = "#",
}) catch no_save_file: {
old_save_file_exists = false;
break :no_save_file tryMigrateFirstSaveFile(&user_buf);
};
if (!old_save_file_exists) return false;
if (maybe_save) |save| {
// Add all other users to the list // Add all other users to the list
for (usernames, 0..) |username, i| { for (usernames, 0..) |username, i| {
if (save.user) |user| { if (save.user) |user| {
@@ -276,40 +246,5 @@ pub fn tryMigrateIniSaveFile(allocator: std.mem.Allocator, io: std.Io, path: []c
}); });
} }
return save_parser; return true;
}
return null;
}
fn tryMigrateFirstSaveFile(io: std.Io, user_buf: *[32]u8) ?OldSave {
if (maybe_save_file) |path| {
defer temporary_allocator.free(path);
var save = OldSave{};
var file = std.Io.Dir.openFileAbsolute(io, path, .{}) catch return null;
defer file.close(io);
var file_buffer: [64]u8 = undefined;
var file_reader = file.reader(io, &file_buffer);
var reader = &file_reader.interface;
var user_writer = std.Io.Writer.fixed(user_buf);
var written = reader.streamDelimiter(&user_writer, '\n') catch return null;
if (written > 0) save.user = user_buf[0..written];
var session_buf: [20]u8 = undefined;
var session_writer = std.Io.Writer.fixed(&session_buf);
written = reader.streamDelimiter(&session_writer, '\n') catch return null;
var session_index: ?usize = null;
if (written > 0) {
session_index = std.fmt.parseUnsigned(usize, session_buf[0..written], 10) catch return null;
}
save.session_index = session_index;
return save;
}
return null;
} }

View File

@@ -1,5 +1,4 @@
const std = @import("std"); const std = @import("std");
pub const Animation = enum { pub const Animation = enum {
none, none,
doom, doom,
@@ -54,15 +53,3 @@ pub const Bigclock = enum {
en, en,
fa, fa,
}; };
pub const DurOffsetAlignment = enum {
topleft,
topcenter,
topright,
centerleft,
center,
centerright,
bottomleft,
bottomcenter,
bottomright,
};

View File

@@ -1,17 +1,51 @@
const std = @import("std"); const std = @import("std");
const builtin = @import("builtin"); const builtin = @import("builtin");
const UidRange = @import("UidRange.zig"); const UidRange = @import("UidRange.zig");
const pwd = @import("pwd");
const stdlib = @import("stdlib");
const unistd = @import("unistd");
const grp = @import("grp");
const system_time = @import("system_time");
const time = @import("time");
pub const pam = @import("pam"); pub const termbox = @import("termbox2");
pub const utmp = @import("utmp");
pub const pam = @cImport({
@cInclude("security/pam_appl.h");
});
pub const utmp = @cImport({
@cInclude("utmpx.h");
});
// Exists for X11 support only // Exists for X11 support only
pub const xcb = @import("xcb"); pub const xcb = @cImport({
@cInclude("xcb/xcb.h");
});
const pwd = @cImport({
@cInclude("pwd.h");
// We include a FreeBSD-specific header here since login_cap.h references
// the passwd struct directly, so we can't import it separately
if (builtin.os.tag == .freebsd) {
@cInclude("sys/types.h");
@cInclude("login_cap.h");
}
});
const stdlib = @cImport({
@cInclude("stdlib.h");
});
const unistd = @cImport({
@cInclude("unistd.h");
});
const grp = @cImport({
@cInclude("grp.h");
});
const system_time = @cImport({
@cInclude("sys/time.h");
});
const time = @cImport({
@cInclude("time.h");
});
pub const TimeOfDay = struct { pub const TimeOfDay = struct {
seconds: i64, seconds: i64,
@@ -31,8 +65,13 @@ pub const UsernameEntry = struct {
fn PlatformStruct() type { fn PlatformStruct() type {
return switch (builtin.os.tag) { return switch (builtin.os.tag) {
.linux => struct { .linux => struct {
pub const kd = @import("kd"); pub const kd = @cImport({
pub const vt = @import("vt"); @cInclude("sys/kd.h");
});
pub const vt = @cImport({
@cInclude("sys/vt.h");
});
pub const LedState = c_char; pub const LedState = c_char;
pub const get_led_state = kd.KDGKBLED; pub const get_led_state = kd.KDGKBLED;
@@ -42,12 +81,15 @@ fn PlatformStruct() type {
pub const vt_activate = vt.VT_ACTIVATE; pub const vt_activate = vt.VT_ACTIVATE;
pub const vt_waitactive = vt.VT_WAITACTIVE; pub const vt_waitactive = vt.VT_WAITACTIVE;
const SYSTEMD_HOMED_UID_MIN = 60001;
const SYSTEMD_HOMED_UID_MAX = 60513;
pub fn setUserContextImpl(username: [*:0]const u8, entry: UsernameEntry) !void { pub fn setUserContextImpl(username: [*:0]const u8, entry: UsernameEntry) !void {
const status = grp.initgroups(username, @intCast(entry.gid)); const status = grp.initgroups(username, @intCast(entry.gid));
if (status != 0) return error.GroupInitializationFailed; if (status != 0) return error.GroupInitializationFailed;
if (isError(std.posix.system.setgid(@intCast(entry.gid)))) return error.SetUserGidFailed; std.posix.setgid(@intCast(entry.gid)) catch return error.SetUserGidFailed;
if (isError(std.posix.system.setuid(@intCast(entry.uid)))) return error.SetUserUidFailed; std.posix.setuid(@intCast(entry.uid)) catch return error.SetUserUidFailed;
} }
// Procedure: // Procedure:
@@ -59,29 +101,16 @@ fn PlatformStruct() type {
// 4. Finally, compare the major and minor device numbers with the // 4. Finally, compare the major and minor device numbers with the
// extracted values. If they correspond, parse [dir] to get the // extracted values. If they correspond, parse [dir] to get the
// TTY ID // TTY ID
pub fn getActiveTtyImpl(allocator: std.mem.Allocator, io: std.Io, use_kmscon_vt: bool) !u8 { pub fn getActiveTtyImpl(allocator: std.mem.Allocator) !u8 {
var file_buffer: [256]u8 = undefined; var file_buffer: [256]u8 = undefined;
if (use_kmscon_vt) {
var file = try std.Io.Dir.openFileAbsolute(io, "/sys/class/tty/tty0/active", .{});
defer file.close(io);
var reader = file.reader(io, &file_buffer);
var buffer: [16]u8 = undefined;
const read = try readBuffer(&reader.interface, &buffer);
const tty = buffer[0..(read - 1)];
return std.fmt.parseInt(u8, tty["tty".len..], 10);
}
var tty_major: u16 = undefined; var tty_major: u16 = undefined;
var tty_minor: u16 = undefined; var tty_minor: u16 = undefined;
{ {
var file = try std.Io.Dir.openFileAbsolute(io, "/proc/self/stat", .{}); var file = try std.fs.openFileAbsolute("/proc/self/stat", .{});
defer file.close(io); defer file.close();
var reader = file.reader(io, &file_buffer); var reader = file.reader(&file_buffer);
var buffer: [1024]u8 = undefined; var buffer: [1024]u8 = undefined;
const read = try readBuffer(&reader.interface, &buffer); const read = try readBuffer(&reader.interface, &buffer);
@@ -99,18 +128,18 @@ fn PlatformStruct() type {
tty_minor = tty_nr % 256; tty_minor = tty_nr % 256;
} }
var directory = try std.Io.Dir.openDirAbsolute(io, "/sys/class/tty", .{ .iterate = true }); var directory = try std.fs.openDirAbsolute("/sys/class/tty", .{ .iterate = true });
defer directory.close(io); defer directory.close();
var iterator = directory.iterate(); var iterator = directory.iterate();
while (try iterator.next(io)) |entry| { while (try iterator.next()) |entry| {
const path = try std.fmt.allocPrint(allocator, "/sys/class/tty/{s}/dev", .{entry.name}); const path = try std.fmt.allocPrint(allocator, "/sys/class/tty/{s}/dev", .{entry.name});
defer allocator.free(path); defer allocator.free(path);
var file = try std.Io.Dir.openFileAbsolute(io, path, .{}); var file = try std.fs.openFileAbsolute(path, .{});
defer file.close(io); defer file.close();
var reader = file.reader(io, &file_buffer); var reader = file.reader(&file_buffer);
var buffer: [16]u8 = undefined; var buffer: [16]u8 = undefined;
const read = try readBuffer(&reader.interface, &buffer); const read = try readBuffer(&reader.interface, &buffer);
@@ -133,14 +162,11 @@ fn PlatformStruct() type {
// This is very bad parsing, but we only need to get 2 values.. // This is very bad parsing, but we only need to get 2 values..
// and the format of the file seems to be standard? So this should // and the format of the file seems to be standard? So this should
// be fine... // be fine...
pub fn getUserIdRange(allocator: std.mem.Allocator, io: std.Io, file_path: []const u8) !UidRange { pub fn getUserIdRange(allocator: std.mem.Allocator, file_path: []const u8) !UidRange {
const login_defs_file = try std.Io.Dir.cwd().openFile(io, file_path, .{}); const login_defs_file = try std.fs.cwd().openFile(file_path, .{});
defer login_defs_file.close(io); defer login_defs_file.close();
var buffer: [4096]u8 = undefined; const login_defs_buffer = try login_defs_file.readToEndAlloc(allocator, std.math.maxInt(u16));
var reader = login_defs_file.reader(io, &buffer);
const login_defs_buffer = try reader.interface.allocRemaining(allocator, .unlimited);
defer allocator.free(login_defs_buffer); defer allocator.free(login_defs_buffer);
var iterator = std.mem.splitScalar(u8, login_defs_buffer, '\n'); var iterator = std.mem.splitScalar(u8, login_defs_buffer, '\n');
@@ -161,6 +187,19 @@ fn PlatformStruct() type {
if (!nameFound) return error.UidNameNotFound; if (!nameFound) return error.UidNameNotFound;
// This code assumes the OS has a login.defs file with UID_MIN
// and UID_MAX values defined in it, which should be the case
// for most systemd-based Linux distributions out there.
// This should be a good enough safeguard for now, as there's
// no reliable (and clean) way to check for systemd support
if (uid_range.uid_min > SYSTEMD_HOMED_UID_MIN) {
uid_range.uid_min = SYSTEMD_HOMED_UID_MIN;
}
if (uid_range.uid_max < SYSTEMD_HOMED_UID_MAX) {
uid_range.uid_max = SYSTEMD_HOMED_UID_MAX;
}
return uid_range; return uid_range;
} }
@@ -192,8 +231,13 @@ fn PlatformStruct() type {
} }
}, },
.freebsd => struct { .freebsd => struct {
pub const kbio = @import("kbio"); pub const kbio = @cImport({
pub const consio = @import("consio"); @cInclude("sys/kbio.h");
});
pub const consio = @cImport({
@cInclude("sys/consio.h");
});
pub const LedState = c_int; pub const LedState = c_int;
pub const get_led_state = kbio.KDGETLED; pub const get_led_state = kbio.KDGETLED;
@@ -216,11 +260,11 @@ fn PlatformStruct() type {
if (result != 0) return error.SetUserUidFailed; if (result != 0) return error.SetUserUidFailed;
} }
pub fn getActiveTtyImpl(_: std.mem.Allocator, _: std.Io, _: bool) !u8 { pub fn getActiveTtyImpl(_: std.mem.Allocator) !u8 {
return error.FeatureUnimplemented; return error.FeatureUnimplemented;
} }
pub fn getUserIdRange(_: std.mem.Allocator, _: std.Io, _: []const u8) !UidRange { pub fn getUserIdRange(_: std.mem.Allocator, _: []const u8) !UidRange {
return .{ return .{
// Hardcoded default values chosen from // Hardcoded default values chosen from
// /usr/src/usr.sbin/pw/pw_conf.c // /usr/src/usr.sbin/pw/pw_conf.c
@@ -235,28 +279,12 @@ fn PlatformStruct() type {
const platform_struct = PlatformStruct(); const platform_struct = PlatformStruct();
// TODO 0.16.0: Can we get away with this?
pub fn isError(result: anytype) bool {
if (@typeInfo(@TypeOf(result)).int.signedness == .signed) {
return result < 0;
}
if (@typeInfo(@TypeOf(result)).int.signedness == .unsigned) {
return switch (builtin.os.tag) {
.linux => std.os.linux.errno(result) != .SUCCESS,
else => @compileError("interop.isError() not implemented for current target!"),
};
}
unreachable;
}
pub fn supportsUnicode() bool { pub fn supportsUnicode() bool {
return builtin.os.tag == .linux or builtin.os.tag == .freebsd; return builtin.os.tag == .linux or builtin.os.tag == .freebsd;
} }
pub fn timeAsString(io: std.Io, buf: [:0]u8, format: [:0]const u8) []u8 { pub fn timeAsString(buf: [:0]u8, format: [:0]const u8) []u8 {
const timer: isize = @intCast(std.Io.Timestamp.now(io, .real).toSeconds()); const timer = std.time.timestamp();
const tm_info = time.localtime(&timer); const tm_info = time.localtime(&timer);
const len = time.strftime(buf, buf.len, format, tm_info); const len = time.strftime(buf, buf.len, format, tm_info);
@@ -275,8 +303,8 @@ pub fn getTimeOfDay() !TimeOfDay {
}; };
} }
pub fn getActiveTty(allocator: std.mem.Allocator, io: std.Io, use_kmscon_vt: bool) !u8 { pub fn getActiveTty(allocator: std.mem.Allocator) !u8 {
return platform_struct.getActiveTtyImpl(allocator, io, use_kmscon_vt); return platform_struct.getActiveTtyImpl(allocator);
} }
pub fn switchTty(tty: u8) !void { pub fn switchTty(tty: u8) !void {
@@ -379,6 +407,6 @@ pub fn closePasswordDatabase() void {
// This is very bad parsing, but we only need to get 2 values... and the format // This is very bad parsing, but we only need to get 2 values... and the format
// of the file doesn't seem to be standard? So this should be fine... // of the file doesn't seem to be standard? So this should be fine...
pub fn getUserIdRange(allocator: std.mem.Allocator, io: std.Io, file_path: []const u8) !UidRange { pub fn getUserIdRange(allocator: std.mem.Allocator, file_path: []const u8) !UidRange {
return platform_struct.getUserIdRange(allocator, io, file_path); return platform_struct.getUserIdRange(allocator, file_path);
} }

File diff suppressed because it is too large Load Diff

61
src/tui/Animation.zig Normal file
View File

@@ -0,0 +1,61 @@
const Animation = @This();
const VTable = struct {
deinit_fn: *const fn (ptr: *anyopaque) void,
realloc_fn: *const fn (ptr: *anyopaque) anyerror!void,
draw_fn: *const fn (ptr: *anyopaque) void,
};
pointer: *anyopaque,
vtable: VTable,
pub fn init(
pointer: anytype,
comptime deinit_fn: fn (ptr: @TypeOf(pointer)) void,
comptime realloc_fn: fn (ptr: @TypeOf(pointer)) anyerror!void,
comptime draw_fn: fn (ptr: @TypeOf(pointer)) void,
) Animation {
const Pointer = @TypeOf(pointer);
const Impl = struct {
pub fn deinitImpl(ptr: *anyopaque) void {
const impl: Pointer = @ptrCast(@alignCast(ptr));
return @call(.always_inline, deinit_fn, .{impl});
}
pub fn reallocImpl(ptr: *anyopaque) anyerror!void {
const impl: Pointer = @ptrCast(@alignCast(ptr));
return @call(.always_inline, realloc_fn, .{impl});
}
pub fn drawImpl(ptr: *anyopaque) void {
const impl: Pointer = @ptrCast(@alignCast(ptr));
return @call(.always_inline, draw_fn, .{impl});
}
const vtable = VTable{
.deinit_fn = deinitImpl,
.realloc_fn = reallocImpl,
.draw_fn = drawImpl,
};
};
return .{
.pointer = pointer,
.vtable = Impl.vtable,
};
}
pub fn deinit(self: *Animation) void {
const impl: @TypeOf(self.pointer) = @ptrCast(@alignCast(self.pointer));
return @call(.auto, self.vtable.deinit_fn, .{impl});
}
pub fn realloc(self: *Animation) anyerror!void {
const impl: @TypeOf(self.pointer) = @ptrCast(@alignCast(self.pointer));
return @call(.auto, self.vtable.realloc_fn, .{impl});
}
pub fn draw(self: *Animation) void {
const impl: @TypeOf(self.pointer) = @ptrCast(@alignCast(self.pointer));
return @call(.auto, self.vtable.draw_fn, .{impl});
}

View File

@@ -1,4 +1,6 @@
const TerminalBuffer = @import("TerminalBuffer.zig"); const interop = @import("../interop.zig");
const termbox = interop.termbox;
const Cell = @This(); const Cell = @This();
@@ -17,5 +19,5 @@ pub fn init(ch: u32, fg: u32, bg: u32) Cell {
pub fn put(self: Cell, x: usize, y: usize) void { pub fn put(self: Cell, x: usize, y: usize) void {
if (self.ch == 0) return; if (self.ch == 0) return;
TerminalBuffer.setCell(x, y, self); _ = termbox.tb_set_cell(@intCast(x), @intCast(y), self.ch, self.fg, self.bg);
} }

260
src/tui/TerminalBuffer.zig Normal file
View File

@@ -0,0 +1,260 @@
const std = @import("std");
const interop = @import("../interop.zig");
const Cell = @import("Cell.zig");
const Random = std.Random;
const termbox = interop.termbox;
const TerminalBuffer = @This();
pub const InitOptions = struct {
fg: u32,
bg: u32,
border_fg: u32,
margin_box_h: u8,
margin_box_v: u8,
input_len: u8,
};
pub const Styling = struct {
pub const BOLD = termbox.TB_BOLD;
pub const UNDERLINE = termbox.TB_UNDERLINE;
pub const REVERSE = termbox.TB_REVERSE;
pub const ITALIC = termbox.TB_ITALIC;
pub const BLINK = termbox.TB_BLINK;
pub const HI_BLACK = termbox.TB_HI_BLACK;
pub const BRIGHT = termbox.TB_BRIGHT;
pub const DIM = termbox.TB_DIM;
};
pub const Color = struct {
pub const DEFAULT = 0x00000000;
pub const TRUE_BLACK = Styling.HI_BLACK;
pub const TRUE_RED = 0x00FF0000;
pub const TRUE_GREEN = 0x0000FF00;
pub const TRUE_YELLOW = 0x00FFFF00;
pub const TRUE_BLUE = 0x000000FF;
pub const TRUE_MAGENTA = 0x00FF00FF;
pub const TRUE_CYAN = 0x0000FFFF;
pub const TRUE_WHITE = 0x00FFFFFF;
pub const TRUE_DIM_RED = 0x00800000;
pub const TRUE_DIM_GREEN = 0x00008000;
pub const TRUE_DIM_YELLOW = 0x00808000;
pub const TRUE_DIM_BLUE = 0x00000080;
pub const TRUE_DIM_MAGENTA = 0x00800080;
pub const TRUE_DIM_CYAN = 0x00008080;
pub const TRUE_DIM_WHITE = 0x00C0C0C0;
pub const ECOL_BLACK = 1;
pub const ECOL_RED = 2;
pub const ECOL_GREEN = 3;
pub const ECOL_YELLOW = 4;
pub const ECOL_BLUE = 5;
pub const ECOL_MAGENTA = 6;
pub const ECOL_CYAN = 7;
pub const ECOL_WHITE = 8;
};
random: Random,
width: usize,
height: usize,
fg: u32,
bg: u32,
border_fg: u32,
box_chars: struct {
left_up: u32,
left_down: u32,
right_up: u32,
right_down: u32,
top: u32,
bottom: u32,
left: u32,
right: u32,
},
labels_max_length: usize,
box_x: usize,
box_y: usize,
box_width: usize,
box_height: usize,
margin_box_v: u8,
margin_box_h: u8,
blank_cell: Cell,
pub fn init(options: InitOptions, labels_max_length: usize, random: Random) TerminalBuffer {
return .{
.random = random,
.width = @intCast(termbox.tb_width()),
.height = @intCast(termbox.tb_height()),
.fg = options.fg,
.bg = options.bg,
.border_fg = options.border_fg,
.box_chars = if (interop.supportsUnicode()) .{
.left_up = 0x250C,
.left_down = 0x2514,
.right_up = 0x2510,
.right_down = 0x2518,
.top = 0x2500,
.bottom = 0x2500,
.left = 0x2502,
.right = 0x2502,
} else .{
.left_up = '+',
.left_down = '+',
.right_up = '+',
.right_down = '+',
.top = '-',
.bottom = '-',
.left = '|',
.right = '|',
},
.labels_max_length = labels_max_length,
.box_x = 0,
.box_y = 0,
.box_width = (2 * options.margin_box_h) + options.input_len + 1 + labels_max_length,
.box_height = 7 + (2 * options.margin_box_v),
.margin_box_v = options.margin_box_v,
.margin_box_h = options.margin_box_h,
.blank_cell = Cell.init(' ', options.fg, options.bg),
};
}
pub fn cascade(self: TerminalBuffer) bool {
var changed = false;
var y = self.height - 2;
while (y > 0) : (y -= 1) {
for (0..self.width) |x| {
var cell: ?*termbox.tb_cell = undefined;
var cell_under: ?*termbox.tb_cell = undefined;
_ = termbox.tb_get_cell(@intCast(x), @intCast(y - 1), 1, &cell);
_ = termbox.tb_get_cell(@intCast(x), @intCast(y), 1, &cell_under);
// This shouldn't happen under normal circumstances, but because
// this is a *secret* animation, there's no need to care that much
if (cell == null or cell_under == null) continue;
const char: u8 = @truncate(cell.?.ch);
if (std.ascii.isWhitespace(char)) continue;
const char_under: u8 = @truncate(cell_under.?.ch);
if (!std.ascii.isWhitespace(char_under)) continue;
changed = true;
if ((self.random.int(u16) % 10) > 7) continue;
_ = termbox.tb_set_cell(@intCast(x), @intCast(y), cell.?.ch, cell.?.fg, cell.?.bg);
_ = termbox.tb_set_cell(@intCast(x), @intCast(y - 1), ' ', cell_under.?.fg, cell_under.?.bg);
}
}
return changed;
}
pub fn drawBoxCenter(self: *TerminalBuffer, show_borders: bool, blank_box: bool) void {
if (self.width < 2 or self.height < 2) return;
const x1 = (self.width - @min(self.width - 2, self.box_width)) / 2;
const y1 = (self.height - @min(self.height - 2, self.box_height)) / 2;
const x2 = (self.width + @min(self.width, self.box_width)) / 2;
const y2 = (self.height + @min(self.height, self.box_height)) / 2;
self.box_x = x1;
self.box_y = y1;
if (show_borders) {
_ = termbox.tb_set_cell(@intCast(x1 - 1), @intCast(y1 - 1), self.box_chars.left_up, self.border_fg, self.bg);
_ = termbox.tb_set_cell(@intCast(x2), @intCast(y1 - 1), self.box_chars.right_up, self.border_fg, self.bg);
_ = termbox.tb_set_cell(@intCast(x1 - 1), @intCast(y2), self.box_chars.left_down, self.border_fg, self.bg);
_ = termbox.tb_set_cell(@intCast(x2), @intCast(y2), self.box_chars.right_down, self.border_fg, self.bg);
var c1 = Cell.init(self.box_chars.top, self.border_fg, self.bg);
var c2 = Cell.init(self.box_chars.bottom, self.border_fg, self.bg);
for (0..self.box_width) |i| {
c1.put(x1 + i, y1 - 1);
c2.put(x1 + i, y2);
}
c1.ch = self.box_chars.left;
c2.ch = self.box_chars.right;
for (0..self.box_height) |i| {
c1.put(x1 - 1, y1 + i);
c2.put(x2, y1 + i);
}
}
if (blank_box) {
for (0..self.box_height) |y| {
for (0..self.box_width) |x| {
self.blank_cell.put(x1 + x, y1 + y);
}
}
}
}
pub fn calculateComponentCoordinates(self: TerminalBuffer) struct {
start_x: usize,
x: usize,
y: usize,
full_visible_length: usize,
visible_length: usize,
} {
const start_x = self.box_x + self.margin_box_h;
const x = start_x + self.labels_max_length + 1;
const y = self.box_y + self.margin_box_v;
const full_visible_length = self.box_x + self.box_width - self.margin_box_h - start_x;
const visible_length = self.box_x + self.box_width - self.margin_box_h - x;
return .{
.start_x = start_x,
.x = x,
.y = y,
.full_visible_length = full_visible_length,
.visible_length = visible_length,
};
}
pub fn drawLabel(self: TerminalBuffer, text: []const u8, x: usize, y: usize) void {
drawColorLabel(text, x, y, self.fg, self.bg);
}
pub fn drawColorLabel(text: []const u8, x: usize, y: usize, fg: u32, bg: u32) void {
const yc: c_int = @intCast(y);
const utf8view = std.unicode.Utf8View.init(text) catch return;
var utf8 = utf8view.iterator();
var i: c_int = @intCast(x);
while (utf8.nextCodepoint()) |codepoint| : (i += termbox.tb_wcwidth(codepoint)) {
_ = termbox.tb_set_cell(i, yc, codepoint, fg, bg);
}
}
pub fn drawConfinedLabel(self: TerminalBuffer, text: []const u8, x: usize, y: usize, max_length: usize) void {
const yc: c_int = @intCast(y);
const utf8view = std.unicode.Utf8View.init(text) catch return;
var utf8 = utf8view.iterator();
var i: c_int = @intCast(x);
while (utf8.nextCodepoint()) |codepoint| : (i += termbox.tb_wcwidth(codepoint)) {
if (i - @as(c_int, @intCast(x)) >= max_length) break;
_ = termbox.tb_set_cell(i, yc, codepoint, self.fg, self.bg);
}
}
pub fn drawCharMultiple(self: TerminalBuffer, char: u32, x: usize, y: usize, length: usize) void {
const cell = Cell.init(char, self.fg, self.bg);
for (0..length) |xx| cell.put(x + xx, y);
}
// Every codepoint is assumed to have a width of 1.
// Since Ly is normally running in a TTY, this should be fine.
pub fn strWidth(str: []const u8) !u8 {
const utf8view = try std.unicode.Utf8View.init(str);
var utf8 = utf8view.iterator();
var i: c_int = 0;
while (utf8.nextCodepoint()) |codepoint| i += termbox.tb_wcwidth(codepoint);
return @intCast(i);
}

View File

@@ -0,0 +1,60 @@
const std = @import("std");
const TerminalBuffer = @import("../TerminalBuffer.zig");
const generic = @import("generic.zig");
const Allocator = std.mem.Allocator;
const MessageLabel = generic.CyclableLabel(Message, Message);
const InfoLine = @This();
const Message = struct {
width: u8,
text: []const u8,
bg: u32,
fg: u32,
};
label: MessageLabel,
pub fn init(allocator: Allocator, buffer: *TerminalBuffer) InfoLine {
return .{
.label = MessageLabel.init(allocator, buffer, drawItem, null, null),
};
}
pub fn deinit(self: *InfoLine) void {
self.label.deinit();
}
pub fn addMessage(self: *InfoLine, text: []const u8, bg: u32, fg: u32) !void {
if (text.len == 0) return;
try self.label.addItem(.{
.width = try TerminalBuffer.strWidth(text),
.text = text,
.bg = bg,
.fg = fg,
});
}
pub fn clearRendered(allocator: Allocator, buffer: TerminalBuffer) !void {
// Draw over the area
const y = buffer.box_y + buffer.margin_box_v;
const spaces = try allocator.alloc(u8, buffer.box_width);
defer allocator.free(spaces);
@memset(spaces, ' ');
buffer.drawLabel(spaces, buffer.box_x, y);
}
fn drawItem(label: *MessageLabel, message: Message, _: usize, _: usize) bool {
if (message.width == 0 or label.buffer.box_width <= message.width) return false;
const x = label.buffer.box_x + ((label.buffer.box_width - message.width) / 2);
label.first_char_x = x + message.width;
TerminalBuffer.drawColorLabel(message.text, x, label.y, message.fg, message.bg);
return true;
}

View File

@@ -0,0 +1,68 @@
const std = @import("std");
const TerminalBuffer = @import("../TerminalBuffer.zig");
const enums = @import("../../enums.zig");
const Environment = @import("../../Environment.zig");
const generic = @import("generic.zig");
const UserList = @import("UserList.zig");
const Allocator = std.mem.Allocator;
const DisplayServer = enums.DisplayServer;
const Env = struct {
environment: Environment,
index: usize,
};
const EnvironmentLabel = generic.CyclableLabel(Env, *UserList);
const Session = @This();
label: EnvironmentLabel,
user_list: *UserList,
pub fn init(allocator: Allocator, buffer: *TerminalBuffer, user_list: *UserList) Session {
return .{
.label = EnvironmentLabel.init(allocator, buffer, drawItem, sessionChanged, user_list),
.user_list = user_list,
};
}
pub fn deinit(self: *Session) void {
for (self.label.list.items) |*env| {
if (env.environment.entry_ini) |*entry_ini| entry_ini.deinit();
self.label.allocator.free(env.environment.file_name);
}
self.label.deinit();
}
pub fn addEnvironment(self: *Session, environment: Environment) !void {
const env = Env{ .environment = environment, .index = self.label.list.items.len };
try self.label.addItem(env);
addedSession(env, self.user_list);
}
fn addedSession(env: Env, user_list: *UserList) void {
const user = user_list.label.list.items[user_list.label.current];
if (!user.first_run) return;
user.session_index.* = env.index;
}
fn sessionChanged(env: Env, maybe_user_list: ?*UserList) void {
if (maybe_user_list) |user_list| {
user_list.label.list.items[user_list.label.current].session_index.* = env.index;
}
}
fn drawItem(label: *EnvironmentLabel, env: Env, x: usize, y: usize) bool {
const length = @min(env.environment.name.len, label.visible_length - 3);
if (length == 0) return false;
const nx = if (label.text_in_center) (label.x + (label.visible_length - env.environment.name.len) / 2) else (label.x + 2);
label.first_char_x = nx + env.environment.name.len;
label.buffer.drawLabel(env.environment.specifier, x, y);
label.buffer.drawLabel(env.environment.name, nx, label.y);
return true;
}

160
src/tui/components/Text.zig Normal file
View File

@@ -0,0 +1,160 @@
const std = @import("std");
const interop = @import("../../interop.zig");
const TerminalBuffer = @import("../TerminalBuffer.zig");
const Allocator = std.mem.Allocator;
const DynamicString = std.ArrayListUnmanaged(u8);
const termbox = interop.termbox;
const Text = @This();
allocator: Allocator,
buffer: *TerminalBuffer,
text: DynamicString,
end: usize,
cursor: usize,
visible_start: usize,
visible_length: usize,
x: usize,
y: usize,
masked: bool,
maybe_mask: ?u32,
pub fn init(allocator: Allocator, buffer: *TerminalBuffer, masked: bool, maybe_mask: ?u32) Text {
const text: DynamicString = .empty;
return .{
.allocator = allocator,
.buffer = buffer,
.text = text,
.end = 0,
.cursor = 0,
.visible_start = 0,
.visible_length = 0,
.x = 0,
.y = 0,
.masked = masked,
.maybe_mask = maybe_mask,
};
}
pub fn deinit(self: *Text) void {
self.text.deinit(self.allocator);
}
pub fn position(self: *Text, x: usize, y: usize, visible_length: usize) void {
self.x = x;
self.y = y;
self.visible_length = visible_length;
}
pub fn handle(self: *Text, maybe_event: ?*termbox.tb_event, insert_mode: bool) !void {
if (maybe_event) |event| blk: {
if (event.type != termbox.TB_EVENT_KEY) break :blk;
switch (event.key) {
termbox.TB_KEY_ARROW_LEFT => self.goLeft(),
termbox.TB_KEY_ARROW_RIGHT => self.goRight(),
termbox.TB_KEY_DELETE => self.delete(),
termbox.TB_KEY_BACKSPACE, termbox.TB_KEY_BACKSPACE2 => {
if (insert_mode) {
self.backspace();
} else {
self.goLeft();
}
},
termbox.TB_KEY_SPACE => try self.write(' '),
else => {
if (event.ch > 31 and event.ch < 127) {
if (insert_mode) {
try self.write(@intCast(event.ch));
} else {
switch (event.ch) {
'h' => self.goLeft(),
'l' => self.goRight(),
else => {},
}
}
}
},
}
}
if (self.masked and self.maybe_mask == null) {
_ = termbox.tb_set_cursor(@intCast(self.x), @intCast(self.y));
return;
}
_ = termbox.tb_set_cursor(@intCast(self.x + (self.cursor - self.visible_start)), @intCast(self.y));
}
pub fn draw(self: Text) void {
if (self.masked) {
if (self.maybe_mask) |mask| {
const length = @min(self.text.items.len, self.visible_length - 1);
if (length == 0) return;
self.buffer.drawCharMultiple(mask, self.x, self.y, length);
}
return;
}
const length = @min(self.text.items.len, self.visible_length);
if (length == 0) return;
const visible_slice = vs: {
if (self.text.items.len > self.visible_length and self.cursor < self.text.items.len) {
break :vs self.text.items[self.visible_start..(self.visible_length + self.visible_start)];
} else {
break :vs self.text.items[self.visible_start..];
}
};
self.buffer.drawLabel(visible_slice, self.x, self.y);
}
pub fn clear(self: *Text) void {
self.text.clearRetainingCapacity();
self.end = 0;
self.cursor = 0;
self.visible_start = 0;
}
fn goLeft(self: *Text) void {
if (self.cursor == 0) return;
if (self.visible_start > 0) self.visible_start -= 1;
self.cursor -= 1;
}
fn goRight(self: *Text) void {
if (self.cursor >= self.end) return;
if (self.cursor - self.visible_start == self.visible_length - 1) self.visible_start += 1;
self.cursor += 1;
}
fn delete(self: *Text) void {
if (self.cursor >= self.end) return;
_ = self.text.orderedRemove(self.cursor);
self.end -= 1;
}
fn backspace(self: *Text) void {
if (self.cursor == 0) return;
self.goLeft();
self.delete();
}
fn write(self: *Text, char: u8) !void {
if (char == 0) return;
try self.text.insert(self.allocator, self.cursor, char);
self.end += 1;
self.goRight();
}

View File

@@ -0,0 +1,87 @@
const std = @import("std");
const TerminalBuffer = @import("../TerminalBuffer.zig");
const generic = @import("generic.zig");
const Session = @import("Session.zig");
const SavedUsers = @import("../../config/SavedUsers.zig");
const StringList = std.ArrayListUnmanaged([]const u8);
const Allocator = std.mem.Allocator;
pub const User = struct {
name: []const u8,
session_index: *usize,
allocated_index: bool,
first_run: bool,
};
const UserLabel = generic.CyclableLabel(User, *Session);
const UserList = @This();
label: UserLabel,
pub fn init(allocator: Allocator, buffer: *TerminalBuffer, usernames: StringList, saved_users: *SavedUsers, session: *Session) !UserList {
var userList = UserList{
.label = UserLabel.init(allocator, buffer, drawItem, usernameChanged, session),
};
for (usernames.items) |username| {
if (username.len == 0) continue;
var maybe_session_index: ?*usize = null;
var first_run = true;
for (saved_users.user_list.items) |*saved_user| {
if (std.mem.eql(u8, username, saved_user.username)) {
maybe_session_index = &saved_user.session_index;
first_run = saved_user.first_run;
break;
}
}
var allocated_index = false;
if (maybe_session_index == null) {
maybe_session_index = try allocator.create(usize);
maybe_session_index.?.* = 0;
allocated_index = true;
}
try userList.label.addItem(.{
.name = username,
.session_index = maybe_session_index.?,
.allocated_index = allocated_index,
.first_run = first_run,
});
}
return userList;
}
pub fn deinit(self: *UserList) void {
for (self.label.list.items) |user| {
if (user.allocated_index) {
self.label.allocator.destroy(user.session_index);
}
}
self.label.deinit();
}
pub fn getCurrentUsername(self: UserList) []const u8 {
return self.label.list.items[self.label.current].name;
}
fn usernameChanged(user: User, maybe_session: ?*Session) void {
if (maybe_session) |session| {
session.label.current = @min(user.session_index.*, session.label.list.items.len - 1);
}
}
fn drawItem(label: *UserLabel, user: User, _: usize, _: usize) bool {
const length = @min(user.name.len, label.visible_length - 3);
if (length == 0) return false;
const x = if (label.text_in_center) (label.x + (label.visible_length - user.name.len) / 2) else (label.x + 2);
label.first_char_x = x + user.name.len;
label.buffer.drawLabel(user.name, x, label.y);
return true;
}

View File

@@ -0,0 +1,117 @@
const std = @import("std");
const interop = @import("../../interop.zig");
const TerminalBuffer = @import("../TerminalBuffer.zig");
pub fn CyclableLabel(comptime ItemType: type, comptime ChangeItemType: type) type {
return struct {
const Allocator = std.mem.Allocator;
const ItemList = std.ArrayListUnmanaged(ItemType);
const DrawItemFn = *const fn (*Self, ItemType, usize, usize) bool;
const ChangeItemFn = *const fn (ItemType, ?ChangeItemType) void;
const termbox = interop.termbox;
const Self = @This();
allocator: Allocator,
buffer: *TerminalBuffer,
list: ItemList,
current: usize,
visible_length: usize,
x: usize,
y: usize,
first_char_x: usize,
text_in_center: bool,
draw_item_fn: DrawItemFn,
change_item_fn: ?ChangeItemFn,
change_item_arg: ?ChangeItemType,
pub fn init(allocator: Allocator, buffer: *TerminalBuffer, draw_item_fn: DrawItemFn, change_item_fn: ?ChangeItemFn, change_item_arg: ?ChangeItemType) Self {
return .{
.allocator = allocator,
.buffer = buffer,
.list = .empty,
.current = 0,
.visible_length = 0,
.x = 0,
.y = 0,
.first_char_x = 0,
.text_in_center = false,
.draw_item_fn = draw_item_fn,
.change_item_fn = change_item_fn,
.change_item_arg = change_item_arg,
};
}
pub fn deinit(self: *Self) void {
self.list.deinit(self.allocator);
}
pub fn position(self: *Self, x: usize, y: usize, visible_length: usize, text_in_center: ?bool) void {
self.x = x;
self.y = y;
self.visible_length = visible_length;
self.first_char_x = x + 2;
if (text_in_center) |value| {
self.text_in_center = value;
}
}
pub fn addItem(self: *Self, item: ItemType) !void {
try self.list.append(self.allocator, item);
self.current = self.list.items.len - 1;
}
pub fn handle(self: *Self, maybe_event: ?*termbox.tb_event, insert_mode: bool) void {
if (maybe_event) |event| blk: {
if (event.type != termbox.TB_EVENT_KEY) break :blk;
switch (event.key) {
termbox.TB_KEY_ARROW_LEFT, termbox.TB_KEY_CTRL_H => self.goLeft(),
termbox.TB_KEY_ARROW_RIGHT, termbox.TB_KEY_CTRL_L => self.goRight(),
else => {
if (!insert_mode) {
switch (event.ch) {
'h' => self.goLeft(),
'l' => self.goRight(),
else => {},
}
}
},
}
}
_ = termbox.tb_set_cursor(@intCast(self.first_char_x), @intCast(self.y));
}
pub fn draw(self: *Self) void {
if (self.list.items.len == 0) return;
const current_item = self.list.items[self.current];
const x = self.buffer.box_x + self.buffer.margin_box_h;
const y = self.buffer.box_y + self.buffer.margin_box_v + 2;
const continue_drawing = @call(.auto, self.draw_item_fn, .{ self, current_item, x, y });
if (!continue_drawing) return;
_ = termbox.tb_set_cell(@intCast(self.x), @intCast(self.y), '<', self.buffer.fg, self.buffer.bg);
_ = termbox.tb_set_cell(@intCast(self.x + self.visible_length - 1), @intCast(self.y), '>', self.buffer.fg, self.buffer.bg);
}
fn goLeft(self: *Self) void {
self.current = if (self.current == 0) self.list.items.len - 1 else self.current - 1;
if (self.change_item_fn) |change_item_fn| {
@call(.auto, change_item_fn, .{ self.list.items[self.current], self.change_item_arg });
}
}
fn goRight(self: *Self) void {
self.current = if (self.current == self.list.items.len - 1) 0 else self.current + 1;
if (self.change_item_fn) |change_item_fn| {
@call(.auto, change_item_fn, .{ self.list.items[self.current], self.change_item_arg });
}
}
};
}