13 Commits

Author SHA1 Message Date
AnErrupTion
741e9e0345 Use correct naming convention for functions in DurFile.zig
Signed-off-by: AnErrupTion <anerruption@disroot.org>
2026-05-12 20:35:17 +02:00
AnErrupTion
de8579854c Use $EXECUTABLE_NAME in kmscon service
Signed-off-by: AnErrupTion <anerruption@disroot.org>
2026-05-11 21:03:01 +02:00
AnErrupTion
ee3196bab8 Fix labels_max_length calculation (closes #984)
Signed-off-by: AnErrupTion <anerruption@disroot.org>
2026-05-11 21:00:52 +02:00
AnErrupTion
9ff4ddd129 Improve keyboard handling (closes #982)
Signed-off-by: AnErrupTion <anerruption@disroot.org>
2026-05-10 13:55:19 +02:00
Titanium Brain
b3830d5bb6 fix(dur): apply correct offset for animations bigger than the terminal (#966)
Animations bigger than the rendering area would have an alignment to the
top left instead of center.

## What are the changes about?

Fixes incorrect offset calculations for dur movies bigger than the screen.

## What existing issue does this resolve?
N/A

## Pre-requisites

- [x] I have tested & confirmed the changes work locally
- [x] I have run `zig fmt` throughout my changes

Reviewed-on: https://codeberg.org/fairyglade/ly/pulls/966
Reviewed-by: AnErrupTion <anerruption+codeberg@disroot.org>
2026-05-09 21:06:45 +02:00
Titanium Brain
b8ae126623 Apply the typestate pattern to DurFormat (#972) 2026-05-04 12:34:32 +02:00
AnErrupTion
4db9295102 Fix building without X11
Signed-off-by: AnErrupTion <anerruption@disroot.org>
2026-05-03 20:05:44 +02:00
AnErrupTion
864f5f2892 Implement syslog functionality (closes #
Signed-off-by: AnErrupTion <anerruption@disroot.org>
2026-05-01 21:51:27 +02:00
AnErrupTion
c50af66407 Fix log file race condition
Signed-off-by: AnErrupTion <anerruption@disroot.org>
2026-05-01 20:54:54 +02:00
MartorSkull
fdf241bed5 Add option to move the box relative to the screen size (#964)
## What are the changes about?

Added a new option in the configuration file for moving the box relative to the screen size.

```
box_h_position = 0.5
box_v_position = 0.5
```

The big clock is centered relative to the box. In the cases where it would be outside of the screen, it moves the box to fit in the screen.

## What existing issue does this resolve?

Add more options for personalization

## Examples

Normal usage:
```
box_h_position = 0.15
box_v_position = 0.35
```
![image](/attachments/6595dfa9-aade-45f4-887c-e5db7f8d5a89)

Clock would be outside of the screen vertically:
```
box_h_position = 0.15
box_v_position = -1.0
```
![image](/attachments/0d6bdcc4-e9dd-4671-a65d-b8b9f063ffb6)

Clock would be outside of the screen horizontally and vertically:
```
box_h_position = -1.0
box_v_position = -1.0
input_len = 3
```
![image](/attachments/630b07fd-b400-4e71-8a67-1baf3a6700a0)

Clock would be outside of the screen horizontally and vertically on the bottom left of the screen:
```
box_h_position = 2.0
box_v_position = 2.0
input_len = 3
```
![image](/attachments/28902967-11a8-4c02-a4c9-1b92f9a728ee)

## What existing issue does this resolve?

_Replace this with a reference to an existing issue, or N/A if there is none_

## Pre-requisites

- [x] I have tested & confirmed the changes work locally
- [x] I have run `zig fmt` throughout my changes

Co-authored-by: AnErrupTion <anerruption+codeberg@disroot.org>
Reviewed-on: https://codeberg.org/fairyglade/ly/pulls/964
Reviewed-by: AnErrupTion <anerruption+codeberg@disroot.org>
2026-05-01 20:09:34 +02:00
AnErrupTion
79eebd8ee0 Prefer std.log instead of stderr directly
Signed-off-by: AnErrupTion <anerruption@disroot.org>
2026-05-01 17:43:28 +02:00
AnErrupTion
3869bfd2f9 Add config validation argument (closes #969)
Signed-off-by: AnErrupTion <anerruption@disroot.org>
2026-05-01 17:40:04 +02:00
AnErrupTion
5905e054c5 Start Ly v1.5.0 development cycle
Signed-off-by: AnErrupTion <anerruption@disroot.org>
2026-05-01 07:47:48 +02:00
16 changed files with 359 additions and 177 deletions

View File

@@ -23,7 +23,7 @@ comptime {
} }
} }
const ly_version = std.SemanticVersion{ .major = 1, .minor = 4, .patch = 0 }; const ly_version = std.SemanticVersion{ .major = 1, .minor = 5, .patch = 0 };
var dest_directory: []const u8 = undefined; var dest_directory: []const u8 = undefined;
var config_directory: []const u8 = undefined; var config_directory: []const u8 = undefined;
@@ -72,7 +72,11 @@ pub fn build(b: *std.Build) !void {
.use_llvm = true, .use_llvm = true,
}); });
const ly_ui = b.dependency("ly_ui", .{ .target = target, .optimize = optimize }); const ly_ui = b.dependency("ly_ui", .{
.target = target,
.optimize = optimize,
.enable_x11_support = enable_x11_support,
});
exe.root_module.addImport("ly-ui", ly_ui.module("ly-ui")); exe.root_module.addImport("ly-ui", ly_ui.module("ly-ui"));
exe.root_module.addOptions("build_options", build_options); exe.root_module.addOptions("build_options", build_options);

View File

@@ -1,6 +1,6 @@
.{ .{
.name = .ly, .name = .ly,
.version = "1.4.0", .version = "1.5.0",
.fingerprint = 0xa148ffcc5dc2cb59, .fingerprint = 0xa148ffcc5dc2cb59,
.minimum_zig_version = "0.16.0", .minimum_zig_version = "0.16.0",
.dependencies = .{ .dependencies = .{

View File

@@ -4,6 +4,7 @@ const Translator = @import("translate_c").Translator;
pub fn build(b: *std.Build) void { pub fn build(b: *std.Build) void {
const target = b.standardTargetOptions(.{}); const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{}); const optimize = b.standardOptimizeOption(.{});
const enable_x11_support = b.option(bool, "enable_x11_support", "Enable X11 support") orelse true;
const mod = b.addModule("ly-core", .{ const mod = b.addModule("ly-core", .{
.root_source_file = b.path("src/root.zig"), .root_source_file = b.path("src/root.zig"),
.target = target, .target = target,
@@ -20,7 +21,9 @@ pub fn build(b: *std.Build) void {
addCImport(b, mod, translate_c, target, optimize, "pam", "#include <security/pam_appl.h>"); 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, "utmp", "#include <utmpx.h>");
addCImport(b, mod, translate_c, target, optimize, "xcb", "#include <xcb/xcb.h>"); if (enable_x11_support) {
addCImport(b, mod, translate_c, target, optimize, "xcb", "#include <xcb/xcb.h>");
}
if (target.result.os.tag == .freebsd) { if (target.result.os.tag == .freebsd) {
addCImport(b, mod, translate_c, target, optimize, "pwd", addCImport(b, mod, translate_c, target, optimize, "pwd",
\\#include <pwd.h> \\#include <pwd.h>

View File

@@ -1,6 +1,6 @@
.{ .{
.name = .ly_core, .name = .ly_core,
.version = "1.0.0", .version = "1.1.0",
.fingerprint = 0xddda7afda795472, .fingerprint = 0xddda7afda795472,
.minimum_zig_version = "0.16.0", .minimum_zig_version = "0.16.0",
.dependencies = .{ .dependencies = .{

View File

@@ -3,50 +3,85 @@ const interop = @import("interop.zig");
const LogFile = @This(); const LogFile = @This();
path: []const u8, maybe_path: ?[]const u8,
could_open_log_file: bool = undefined, could_open_log_file: bool = undefined,
file: std.Io.File = undefined, maybe_file: ?std.Io.File = null,
buffer: []u8, buffer: []u8,
file_writer: std.Io.File.Writer = undefined, maybe_file_writer: ?std.Io.File.Writer = null,
pub fn init(io: std.Io, path: ?[]const u8, buffer: []u8) !LogFile {
var log_file = LogFile{
.maybe_path = path,
.buffer = buffer,
};
if (path) |p| {
log_file.could_open_log_file = try openLogFile(io, p, &log_file);
} else {
std.posix.system.openlog("ly", 0, 0);
log_file.could_open_log_file = true;
}
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; return log_file;
} }
pub fn reinit(self: *LogFile, io: std.Io) !void { pub fn reinit(self: *LogFile, io: std.Io) !void {
self.could_open_log_file = try openLogFile(io, self.path, self); if (self.maybe_path) |path| {
self.could_open_log_file = try openLogFile(io, path, self);
} else {
std.posix.system.openlog("ly", 0, 0);
self.could_open_log_file = true;
}
} }
pub fn deinit(self: *LogFile, io: std.Io) void { pub fn deinit(self: *LogFile, io: std.Io) void {
self.file.close(io); if (self.maybe_file) |file| {
file.close(io);
} else {
std.posix.system.closelog();
}
} }
pub fn info(self: *LogFile, io: std.Io, category: []const u8, comptime message: []const u8, args: anytype) !void { pub fn info(self: *LogFile, io: std.Io, category: []const u8, comptime message: []const u8, args: anytype) !void {
var buffer: [128:0]u8 = undefined; if (self.maybe_file_writer) |*writer| {
const time = interop.timeAsString(io, &buffer, "%Y-%m-%d %H:%M:%S"); 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 writer.interface.print("{s} [info/{s}] ", .{ time, category });
try self.file_writer.interface.print(message, args); try writer.interface.print(message, args);
try self.file_writer.interface.writeByte('\n'); try writer.interface.writeByte('\n');
try self.file_writer.interface.flush(); try writer.interface.flush();
} else {
var buffer: [1024]u8 = undefined;
const slice = try std.fmt.bufPrint(&buffer, message, args);
const msg = try std.fmt.bufPrintZ(buffer[slice.len..], "[info/{s}] {s}", .{ category, slice });
std.posix.system.syslog(std.posix.LOG.INFO, msg.ptr);
}
} }
pub fn err(self: *LogFile, io: std.Io, category: []const u8, comptime message: []const u8, args: anytype) !void { pub fn err(self: *LogFile, io: std.Io, category: []const u8, comptime message: []const u8, args: anytype) !void {
var buffer: [128:0]u8 = undefined; if (self.maybe_file_writer) |*writer| {
const time = interop.timeAsString(io, &buffer, "%Y-%m-%d %H:%M:%S"); 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 writer.interface.print("{s} [err/{s}] ", .{ time, category });
try self.file_writer.interface.print(message, args); try writer.interface.print(message, args);
try self.file_writer.interface.writeByte('\n'); try writer.interface.writeByte('\n');
try self.file_writer.interface.flush(); try writer.interface.flush();
} else {
var buffer: [1024]u8 = undefined;
const slice = try std.fmt.bufPrint(&buffer, message, args);
const msg = try std.fmt.bufPrintZ(buffer[slice.len..], "[info/{s}] {s}", .{ category, slice });
std.posix.system.syslog(std.posix.LOG.ERR, msg.ptr);
}
} }
fn openLogFile(io: std.Io, path: []const u8, log_file: *LogFile) !bool { fn openLogFile(io: std.Io, path: []const u8, log_file: *LogFile) !bool {
var could_open_log_file = true; var could_open_log_file = true;
open_log_file: { 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 { log_file.maybe_file = std.Io.Dir.cwd().openFile(io, path, .{ .mode = .write_only }) catch std.Io.Dir.cwd().createFile(io, path, .{ .permissions = .fromMode(0o666) }) catch {
// If we could neither open an existing log file nor create a new // If we could neither open an existing log file nor create a new
// one, abort. // one, abort.
could_open_log_file = false; could_open_log_file = false;
@@ -55,17 +90,17 @@ fn openLogFile(io: std.Io, path: []const u8, log_file: *LogFile) !bool {
} }
if (!could_open_log_file) { if (!could_open_log_file) {
log_file.file = try std.Io.Dir.openFileAbsolute(io, "/dev/null", .{ .mode = .write_only }); log_file.maybe_file = try std.Io.Dir.openFileAbsolute(io, "/dev/null", .{ .mode = .write_only });
} }
var log_file_writer = log_file.file.writer(io, log_file.buffer); var log_file_writer = log_file.maybe_file.?.writer(io, log_file.buffer);
// Seek to the end of the log file // Seek to the end of the log file
if (could_open_log_file) { if (could_open_log_file) {
const stat = try log_file.file.stat(io); const stat = try log_file.maybe_file.?.stat(io);
try log_file_writer.seekTo(stat.size); try log_file_writer.seekTo(stat.size);
} }
log_file.file_writer = log_file_writer; log_file.maybe_file_writer = log_file_writer;
return could_open_log_file; return could_open_log_file;
} }

View File

@@ -4,13 +4,18 @@ const Translator = @import("translate_c").Translator;
pub fn build(b: *std.Build) void { pub fn build(b: *std.Build) void {
const target = b.standardTargetOptions(.{}); const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{}); const optimize = b.standardOptimizeOption(.{});
const enable_x11_support = b.option(bool, "enable_x11_support", "Enable X11 support") orelse true;
const mod = b.addModule("ly-ui", .{ const mod = b.addModule("ly-ui", .{
.root_source_file = b.path("src/root.zig"), .root_source_file = b.path("src/root.zig"),
.target = target, .target = target,
.optimize = optimize, .optimize = optimize,
}); });
const ly_core = b.dependency("ly_core", .{ .target = target, .optimize = optimize }); const ly_core = b.dependency("ly_core", .{
.target = target,
.optimize = optimize,
.enable_x11_support = enable_x11_support,
});
mod.addImport("ly-core", ly_core.module("ly-core")); mod.addImport("ly-core", ly_core.module("ly-core"));
const termbox_dep = b.dependency("termbox2", .{ const termbox_dep = b.dependency("termbox2", .{

View File

@@ -1,6 +1,6 @@
.{ .{
.name = .ly_ui, .name = .ly_ui,
.version = "1.0.0", .version = "1.1.0",
.fingerprint = 0x8d11bf85a74ec803, .fingerprint = 0x8d11bf85a74ec803,
.minimum_zig_version = "0.16.0", .minimum_zig_version = "0.16.0",
.dependencies = .{ .dependencies = .{

View File

@@ -381,6 +381,12 @@ pub fn setCell(x: usize, y: usize, cell: Cell) void {
); );
} }
pub fn setCellBoundsChecked(self: *TerminalBuffer, x: isize, y: isize, cell: Cell) void {
if (0 <= x and x < self.width and 0 <= y and y < self.height) {
cell.put(@intCast(x), @intCast(y));
}
}
pub fn reclaim(self: TerminalBuffer) !void { pub fn reclaim(self: TerminalBuffer) !void {
if (self.termios) |termios| { if (self.termios) |termios| {
// Take back control of the TTY // Take back control of the TTY

View File

@@ -171,6 +171,7 @@ pub fn getKeyList(allocator: Allocator, tb_event: termbox.tb_event) !KeyList {
const code = if (tb_event.ch == 0 and tb_event.key < 128) tb_event.key else tb_event.ch; const code = if (tb_event.ch == 0 and tb_event.key < 128) tb_event.key else tb_event.ch;
switch (code) { switch (code) {
// Non-standard control codes
0 => { 0 => {
key.ctrl = true; key.ctrl = true;
key.@"2" = true; key.@"2" = true;
@@ -342,7 +343,9 @@ pub fn getKeyList(allocator: Allocator, tb_event: termbox.tb_event) !KeyList {
key = std.mem.zeroes(Key); key = std.mem.zeroes(Key);
key._ = true; key._ = true;
}, },
// Standard ASCII characters
32 => { 32 => {
key = std.mem.zeroes(Key);
key.@" " = true; key.@" " = true;
}, },
33 => { 33 => {
@@ -370,6 +373,7 @@ pub fn getKeyList(allocator: Allocator, tb_event: termbox.tb_event) !KeyList {
key.@"&" = true; key.@"&" = true;
}, },
39 => { 39 => {
key = std.mem.zeroes(Key);
key.@"'" = true; key.@"'" = true;
}, },
40 => { 40 => {
@@ -389,74 +393,86 @@ pub fn getKeyList(allocator: Allocator, tb_event: termbox.tb_event) !KeyList {
key.@"+" = true; key.@"+" = true;
}, },
44 => { 44 => {
key = std.mem.zeroes(Key);
key.@"," = true; key.@"," = true;
}, },
45 => { 45 => {
key = std.mem.zeroes(Key);
key.@"-" = true; key.@"-" = true;
}, },
46 => { 46 => {
key = std.mem.zeroes(Key);
key.@"." = true; key.@"." = true;
}, },
47 => { 47 => {
key = std.mem.zeroes(Key);
key.@"/" = true; key.@"/" = true;
}, },
48 => { 48 => {
key = std.mem.zeroes(Key);
key.@"0" = true; key.@"0" = true;
}, },
49 => { 49 => {
key = std.mem.zeroes(Key);
key.@"1" = true; key.@"1" = true;
}, },
50 => { 50 => {
key = std.mem.zeroes(Key);
key.@"2" = true; key.@"2" = true;
}, },
51 => { 51 => {
key = std.mem.zeroes(Key);
key.@"3" = true; key.@"3" = true;
}, },
52 => { 52 => {
key = std.mem.zeroes(Key);
key.@"4" = true; key.@"4" = true;
}, },
53 => { 53 => {
key = std.mem.zeroes(Key);
key.@"5" = true; key.@"5" = true;
}, },
54 => { 54 => {
key = std.mem.zeroes(Key);
key.@"6" = true; key.@"6" = true;
}, },
55 => { 55 => {
key = std.mem.zeroes(Key);
key.@"7" = true; key.@"7" = true;
}, },
56 => { 56 => {
key = std.mem.zeroes(Key);
key.@"8" = true; key.@"8" = true;
}, },
57 => { 57 => {
key = std.mem.zeroes(Key);
key.@"9" = true; key.@"9" = true;
}, },
58 => { 58 => {
key.shift = true; key = std.mem.zeroes(Key);
key.@":" = true; key.@":" = true;
}, },
59 => { 59 => {
key = std.mem.zeroes(Key);
key.@";" = true; key.@";" = true;
}, },
60 => { 60 => {
key.shift = true; key = std.mem.zeroes(Key);
key.@"<" = true; key.@"<" = true;
}, },
61 => { 61 => {
key = std.mem.zeroes(Key);
key.@"=" = true; key.@"=" = true;
}, },
62 => { 62 => {
key.shift = true; key = std.mem.zeroes(Key);
key.@">" = true; key.@">" = true;
}, },
63 => { 63 => {
key.shift = true; key = std.mem.zeroes(Key);
key.@"?" = true; key.@"?" = true;
}, },
64 => { 64 => {
key.shift = true;
key.@"2" = true;
try keys.append(allocator, key);
key = std.mem.zeroes(Key); key = std.mem.zeroes(Key);
key.@"@" = true; key.@"@" = true;
}, },
@@ -565,12 +581,15 @@ pub fn getKeyList(allocator: Allocator, tb_event: termbox.tb_event) !KeyList {
key.z = true; key.z = true;
}, },
91 => { 91 => {
key = std.mem.zeroes(Key);
key.@"[" = true; key.@"[" = true;
}, },
92 => { 92 => {
key = std.mem.zeroes(Key);
key.@"\\" = true; key.@"\\" = true;
}, },
93 => { 93 => {
key = std.mem.zeroes(Key);
key.@"]" = true; key.@"]" = true;
}, },
94 => { 94 => {
@@ -578,14 +597,11 @@ pub fn getKeyList(allocator: Allocator, tb_event: termbox.tb_event) !KeyList {
key.@"^" = true; key.@"^" = true;
}, },
95 => { 95 => {
key.shift = true;
key.@"-" = true;
try keys.append(allocator, key);
key = std.mem.zeroes(Key); key = std.mem.zeroes(Key);
key._ = true; key._ = true;
}, },
96 => { 96 => {
key = std.mem.zeroes(Key);
key.@"`" = true; key.@"`" = true;
}, },
97 => { 97 => {
@@ -667,34 +683,21 @@ pub fn getKeyList(allocator: Allocator, tb_event: termbox.tb_event) !KeyList {
key.z = true; key.z = true;
}, },
123 => { 123 => {
key.shift = true;
key.@"{" = true; key.@"{" = true;
}, },
124 => { 124 => {
key.shift = true;
key.@"\\" = true;
try keys.append(allocator, key);
key = std.mem.zeroes(Key); key = std.mem.zeroes(Key);
key.@"|" = true; key.@"|" = true;
}, },
125 => { 125 => {
key.shift = true; key = std.mem.zeroes(Key);
key.@"}" = true; key.@"}" = true;
}, },
126 => { 126 => {
key.shift = true;
key.@"`" = true;
try keys.append(allocator, key);
key = std.mem.zeroes(Key); key = std.mem.zeroes(Key);
key.@"~" = true; key.@"~" = true;
}, },
127 => { 127 => {
key.ctrl = true;
key.@"8" = true;
try keys.append(allocator, key);
key = std.mem.zeroes(Key); key = std.mem.zeroes(Key);
key.backspace = true; key.backspace = true;
}, },

View File

@@ -226,6 +226,12 @@ You can, of course, still select the init system of your choice when using this
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.
You may also check the validity of your configuration file (i.e. if there are any errors in it) with the following command:
```
$ ly --validate-config /etc/ly/config.ini
```
## Controls ## 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.

View File

@@ -97,6 +97,14 @@ blank_box = true
# Border foreground color id # Border foreground color id
border_fg = 0x00FFFFFF border_fg = 0x00FFFFFF
# Relative horizontal position from the end of the screen
# default: 0.5
box_position_h = 0.5
# Relative vertical position from the bottom of the screen
# default: 0.4
box_position_v = 0.4
# Title to show at the top of the main box # Title to show at the top of the main box
# If set to null, none will be shown # If set to null, none will be shown
box_title = null box_title = null
@@ -291,6 +299,7 @@ login_defs_path = /etc/login.defs
logout_cmd = null logout_cmd = null
# General log file path # General log file path
# If null, syslog will be used instead
ly_log = /var/log/ly.log ly_log = /var/log/ly.log
# Main box horizontal margin # Main box horizontal margin

View File

@@ -5,7 +5,7 @@ After=kmsconvt@%i.service
Conflicts=kmsconvt@%i.service Conflicts=kmsconvt@%i.service
[Service] [Service]
ExecStart=$PREFIX_DIRECTORY/bin/kmscon --font-engine unifont --vt=%I --seats=seat0 --login -- $PREFIX_DIRECTORY/bin/ly --use-kmscon-vt ExecStart=$PREFIX_DIRECTORY/bin/kmscon --font-engine unifont --vt=%I --seats=seat0 --login -- $PREFIX_DIRECTORY/bin/$EXECUTABLE_NAME --use-kmscon-vt
StandardInput=tty StandardInput=tty
UtmpIdentifier=%I UtmpIdentifier=%I
TTYPath=/dev/%I TTYPath=/dev/%I

View File

@@ -19,7 +19,7 @@ const LogFile = ly_core.LogFile;
const enums = @import("../enums.zig"); const enums = @import("../enums.zig");
const DurOffsetAlignment = enums.DurOffsetAlignment; const DurOffsetAlignment = enums.DurOffsetAlignment;
fn read_decompress_file(allocator: Allocator, io: std.Io, file_path: []const u8) ![]u8 { fn readDecompressFile(allocator: Allocator, io: std.Io, file_path: []const u8) ![]u8 {
const file_buffer = std.Io.Dir.cwd().openFile(io, file_path, .{}) catch { const file_buffer = std.Io.Dir.cwd().openFile(io, file_path, .{}) catch {
return error.FileNotFound; return error.FileNotFound;
}; };
@@ -62,7 +62,7 @@ const Frame = struct {
}; };
// https://github.com/cmang/durdraw/blob/0.29.0/durformat.md // https://github.com/cmang/durdraw/blob/0.29.0/durformat.md
const DurFormat = struct { const DurFormatRaw = struct {
allocator: Allocator, allocator: Allocator,
formatVersion: ?i64 = null, formatVersion: ?i64 = null,
colorFormat: ?[]const u8 = null, colorFormat: ?[]const u8 = null,
@@ -72,38 +72,48 @@ const DurFormat = struct {
lines: ?i64 = null, lines: ?i64 = null,
frames: std.ArrayList(Frame) = undefined, frames: std.ArrayList(Frame) = undefined,
pub fn valid(self: *DurFormat) bool { // Validate data and return a valid DurFormat
if (self.formatVersion != null and // Consumes `self`, making it unusable after
self.colorFormat != null and pub fn validate(self: *DurFormatRaw) !DurFormat {
self.encoding != null and // v8 may have breaking changes like changing the colormap xy direction
self.framerate != null and // (https://github.com/cmang/durdraw/issues/24)
self.columns != null and const format_version = self.formatVersion orelse return error.MissingFieldVersion;
self.lines != null and if (format_version != 7) return error.UnsupportedVersion;
self.frames.items.len >= 1)
{
// v8 may have breaking changes like changing the colormap xy direction
// (https://github.com/cmang/durdraw/issues/24)
if (self.formatVersion.? != 7) return false;
// Code currently only supports 16 and 256 color format only const color_format_str = self.colorFormat orelse return error.MissingFieldColorFormat;
if (!(eql(u8, "16", self.colorFormat.?) or eql(u8, "256", self.colorFormat.?))) // Code currently only supports 16 and 256 color format only
return false; const color_format: DurColorFormat =
if (eql(u8, color_format_str, "16")) .@"16" else if (eql(u8, color_format_str, "256")) .@"256" else return error.UnsupportedColorFormat;
// Code currently supports only utf-8 encoding const encoding_str = self.encoding orelse return error.MissingFieldEncoding;
if (!eql(u8, self.encoding.?, "utf-8")) return false; // Code currently supports only utf-8 encoding
const encoding: DurEncoding = if (eql(u8, encoding_str, "utf-8")) .utf_8 else return error.UnsupportedEncoding;
// Sanity check on file if (self.framerate == null) return error.MissingFieldFramerate;
if (self.columns.? <= 0) return false; if (self.framerate.? <= 0) return error.InvalidFramerate;
if (self.lines.? <= 0) return false; const framerate: f64 = self.framerate.?;
if (self.framerate.? < 0) return false;
return true; // Sanity check on file
} if (self.columns == null or self.lines == null) return error.MissingDimensions;
const columns = std.math.cast(u32, self.columns.?) orelse return error.InvalidColumnCount;
const lines = std.math.cast(u32, self.lines.?) orelse return error.InvalidLineCount;
return false; if (self.frames.items.len == 0) return error.NoFrames;
const frames = self.frames;
return .{
.allocator = self.allocator,
.formatVersion = format_version,
.colorFormat = color_format,
.encoding = encoding,
.framerate = framerate,
.columns = columns,
.lines = lines,
.frames = frames,
};
} }
fn parse_dur_from_json(self: *DurFormat, allocator: Allocator, dur_json_root: Json.Value) !void { fn parseFromJson(self: *DurFormatRaw, allocator: Allocator, dur_json_root: Json.Value) !void {
var dur_movie = if (dur_json_root.object.get("DurMovie")) |dm| dm.object else return error.NotValidFile; var dur_movie = if (dur_json_root.object.get("DurMovie")) |dm| dm.object else return error.NotValidFile;
// Depending on the version, a dur file can have different json object names (ie: columns vs sizeX) // Depending on the version, a dur file can have different json object names (ie: columns vs sizeX)
@@ -150,28 +160,44 @@ const DurFormat = struct {
} }
} }
pub fn create_from_file(self: *DurFormat, allocator: Allocator, io: std.Io, file_path: []const u8) !void { pub fn createFromFile(self: *DurFormatRaw, allocator: Allocator, io: std.Io, file_path: []const u8) !void {
const file_decompressed = try read_decompress_file(allocator, io, file_path); const file_decompressed = try readDecompressFile(allocator, io, 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, .{});
defer parsed.deinit(); defer parsed.deinit();
try parse_dur_from_json(self, allocator, parsed.value); try parseFromJson(self, allocator, parsed.value);
if (!self.valid()) {
return error.NotValidFile;
}
} }
pub fn init(allocator: Allocator) DurFormat { pub fn init(allocator: Allocator) DurFormatRaw {
return .{ .allocator = allocator }; return .{ .allocator = allocator };
} }
pub fn deinit(self: *DurFormat) void { pub fn deinit(self: *DurFormatRaw) void {
if (self.colorFormat) |str| self.allocator.free(str); if (self.colorFormat) |str| self.allocator.free(str);
if (self.encoding) |str| self.allocator.free(str); if (self.encoding) |str| self.allocator.free(str);
}
};
const DurColorFormat = enum {
@"16",
@"256",
};
const DurEncoding = enum { utf_8 };
const DurFormat = struct {
allocator: Allocator,
formatVersion: i64,
colorFormat: DurColorFormat,
encoding: DurEncoding,
framerate: f64,
columns: u32,
lines: u32,
frames: std.ArrayList(Frame),
pub fn deinit(self: *DurFormat) void {
for (self.frames.items) |frame| { for (self.frames.items) |frame| {
frame.deinit(self.allocator); frame.deinit(self.allocator);
} }
@@ -240,7 +266,7 @@ const durcolor_table_to_color16 = [17]u32{
15, // 16 bright white 15, // 16 bright white
}; };
fn sixcube_to_channel(sixcube: u32) u32 { fn sixCubeToChannel(sixcube: u32) u32 {
// Although the range top for the extended range is 0xFF, 6 is not divisible into 0xFF, // Although the range top for the extended range is 0xFF, 6 is not divisible into 0xFF,
// so we use 0xF0 instead with a scaler // so we use 0xF0 instead with a scaler
const equal_divisions = 0xF0 / 6; const equal_divisions = 0xF0 / 6;
@@ -251,7 +277,7 @@ fn sixcube_to_channel(sixcube: u32) u32 {
return if (sixcube > 0) (sixcube * equal_divisions) + scaler else 0; return if (sixcube > 0) (sixcube * equal_divisions) + scaler else 0;
} }
fn convert_256_to_rgb(color_256: u32) u32 { fn convert256ToRgb(color_256: u32) u32 {
var rgb_color: u32 = 0; var rgb_color: u32 = 0;
// 0 - 15 is the standard color range, map to array table // 0 - 15 is the standard color range, map to array table
@@ -267,9 +293,9 @@ fn convert_256_to_rgb(color_256: u32) u32 {
// divide by 1 gets the height of the cube (divide 1 for clarity for what we are doing) // divide by 1 gets the height of the cube (divide 1 for clarity for what we are doing)
// each channel can be 6 levels of brightness hence remander operation of 6 // each channel can be 6 levels of brightness hence remander operation of 6
// finally bitshift to correct rgb channel (16 for red, 8 for green, 0 for blue) // finally bitshift to correct rgb channel (16 for red, 8 for green, 0 for blue)
rgb_color |= sixcube_to_channel(((color_256 - 16) / 36) % 6) << 16; rgb_color |= sixCubeToChannel(((color_256 - 16) / 36) % 6) << 16;
rgb_color |= sixcube_to_channel(((color_256 - 16) / 6) % 6) << 8; rgb_color |= sixCubeToChannel(((color_256 - 16) / 6) % 6) << 8;
rgb_color |= sixcube_to_channel(((color_256 - 16) / 1) % 6); rgb_color |= sixCubeToChannel(((color_256 - 16) / 1) % 6);
} }
// 232 - 255 is the grayscale range // 232 - 255 is the grayscale range
else { else {
@@ -311,7 +337,6 @@ io: std.Io,
terminal_buffer: *TerminalBuffer, terminal_buffer: *TerminalBuffer,
dur_movie: DurFormat, dur_movie: DurFormat,
frames: usize, frames: usize,
frame_size: UVec2,
start_pos: IVec2, start_pos: IVec2,
full_color: bool, full_color: bool,
animate: *bool, animate: *bool,
@@ -324,19 +349,16 @@ offset_alignment: DurOffsetAlignment,
offset: IVec2, 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..) // if the user has an even number of columns or rows, we will default to the left or higher position (e.g. 4 columns center = .x..)
fn center(v: u32) i64 { fn center(v: i64) i64 {
return @intCast((v / 2) + (v % 2)); return @intCast(@divTrunc(v, 2) + @mod(v, 2));
} }
fn calc_start_position(terminal_buffer: *TerminalBuffer, dur_movie: *DurFormat, offset_alignment: DurOffsetAlignment, offset: IVec2) IVec2 { fn calculateStartPos(terminal_buffer: *TerminalBuffer, dur_movie: *DurFormat, offset_alignment: DurOffsetAlignment, offset: IVec2) IVec2 {
const buf_width: u32 = @intCast(terminal_buffer.width); const buf_width: i64 = @intCast(terminal_buffer.width);
const buf_height: u32 = @intCast(terminal_buffer.height); const buf_height: i64 = @intCast(terminal_buffer.height);
var movie_width: u32 = @intCast(dur_movie.columns.?); const movie_width: i64 = @intCast(dur_movie.columns);
var movie_height: u32 = @intCast(dur_movie.lines.?); const movie_height: i64 = @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) { const start_pos: IVec2 = switch (offset_alignment) {
DurOffsetAlignment.center => .{ center(buf_width) - center(movie_width), center(buf_height) - center(movie_height) }, DurOffsetAlignment.center => .{ center(buf_width) - center(movie_width), center(buf_height) - center(movie_height) },
@@ -353,20 +375,6 @@ fn calc_start_position(terminal_buffer: *TerminalBuffer, dur_movie: *DurFormat,
return start_pos + offset; 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( pub fn init(
allocator: Allocator, allocator: Allocator,
io: std.Io, io: std.Io,
@@ -381,9 +389,10 @@ pub fn init(
timeout_sec: u12, timeout_sec: u12,
frame_delay: u16, frame_delay: u16,
) !DurFile { ) !DurFile {
var dur_movie: DurFormat = .init(allocator); var dur_movie_raw: DurFormatRaw = .init(allocator);
defer dur_movie_raw.deinit();
dur_movie.create_from_file(allocator, io, file_path) catch |err| switch (err) { dur_movie_raw.createFromFile(allocator, io, 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_file.err(io, "tui", "dur_file was not found at: {s}", .{file_path});
return err; return err;
@@ -395,20 +404,70 @@ pub fn init(
else => return err, else => return err,
}; };
var dur_movie = dur_movie_raw.validate() catch |err| switch (err) {
error.MissingFieldVersion => {
try log_file.err(io, "tui", "dur_file loaded was invalid: missing field formatVersion!", .{});
return err;
},
error.UnsupportedVersion => {
try log_file.err(io, "tui", "dur_file loaded was invalid: unsupported version ({d})!", .{dur_movie_raw.formatVersion.?});
return err;
},
error.MissingFieldColorFormat => {
try log_file.err(io, "tui", "dur_file loaded was invalid: missing field colorFormat!", .{});
return err;
},
error.UnsupportedColorFormat => {
try log_file.err(io, "tui", "dur_file loaded was invalid: unsupported colorFormat ({s})!", .{dur_movie_raw.colorFormat.?});
return err;
},
error.MissingFieldEncoding => {
try log_file.err(io, "tui", "dur_file loaded was invalid: missing field encoding!", .{});
return err;
},
error.UnsupportedEncoding => {
try log_file.err(io, "tui", "dur_file loaded was invalid: unsupported encoding ({s})!", .{dur_movie_raw.encoding.?});
return err;
},
error.MissingFieldFramerate => {
try log_file.err(io, "tui", "dur_file loaded was invalid: missing field framerate!", .{});
return err;
},
error.InvalidFramerate => {
try log_file.err(io, "tui", "dur_file loaded was invalid: negative framerate value found!", .{});
return err;
},
error.MissingDimensions => {
try log_file.err(io, "tui", "dur_file loaded was invalid: missing field(s) lines and/or columns!", .{});
return err;
},
error.InvalidColumnCount => {
try log_file.err(io, "tui", "dur_file loaded was invalid: columns value falls outside of supported range ({d})!", .{dur_movie_raw.columns.?});
return err;
},
error.InvalidLineCount => {
try log_file.err(io, "tui", "dur_file loaded was invalid: lines value falls outside of supported range ({d})!", .{dur_movie_raw.lines.?});
return err;
},
error.NoFrames => {
try log_file.err(io, "tui", "dur_file loaded was invalid: animation has no frames!", .{});
return err;
},
};
// 4 bit mode with 256 color is unsupported // 4 bit mode with 256 color is unsupported
if (!full_color and eql(u8, dur_movie.colorFormat.?, "256")) { if (!full_color and dur_movie.colorFormat == .@"256") {
try log_file.err(io, "tui", "dur_file can not be 256 color encoded when not using full_color option!", .{}); try log_file.err(io, "tui", "dur_file can not be 256 color encoded when not using full_color option!", .{});
dur_movie.deinit(); dur_movie.deinit();
return error.InvalidColorFormat; return error.NotFullColor;
} }
const offset: IVec2 = .{ x_offset, y_offset }; const offset: IVec2 = .{ x_offset, y_offset };
const start_pos = calc_start_position(terminal_buffer, &dur_movie, offset_alignment, offset); const start_pos = calculateStartPos(terminal_buffer, &dur_movie, offset_alignment, offset);
const frame_size = calc_frame_size(terminal_buffer, &dur_movie);
// 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 = @trunc(1000 / dur_movie.framerate);
return .{ return .{
.instance = null, .instance = null,
@@ -418,7 +477,6 @@ pub fn init(
.terminal_buffer = terminal_buffer, .terminal_buffer = terminal_buffer,
.frames = 0, .frames = 0,
.time_previous = std.Io.Timestamp.now(io, .real).toMilliseconds(), .time_previous = std.Io.Timestamp.now(io, .real).toMilliseconds(),
.frame_size = frame_size,
.start_pos = start_pos, .start_pos = start_pos,
.full_color = full_color, .full_color = full_color,
.animate = animate, .animate = animate,
@@ -426,7 +484,7 @@ pub fn init(
.frame_delay = frame_delay, .frame_delay = frame_delay,
.dur_movie = dur_movie, .dur_movie = dur_movie,
.frame_time = frame_time, .frame_time = frame_time,
.is_color_format_16 = eql(u8, dur_movie.colorFormat.?, "16"), .is_color_format_16 = dur_movie.colorFormat == .@"16",
.offset_alignment = offset_alignment, .offset_alignment = offset_alignment,
.offset = offset, .offset = offset,
}; };
@@ -453,9 +511,8 @@ fn deinit(self: *DurFile) void {
} }
fn realloc(self: *DurFile) !void { fn realloc(self: *DurFile) !void {
// when terminal size changes, we need to recalculate the start_pos and frame_size based on the new size // when terminal size changes, we need to recalculate the start_pos based on the new size
self.start_pos = calc_start_position(self.terminal_buffer, &self.dur_movie, self.offset_alignment, self.offset); self.start_pos = calculateStartPos(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 {
@@ -463,24 +520,14 @@ fn draw(self: *DurFile) void {
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);
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) // 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| { for (0..@intCast(self.dur_movie.lines)) |y| {
const y_offset_i = @as(i32, @intCast(y)) + self.start_pos[VEC_Y]; const cell_y = @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..@intCast(self.dur_movie.columns)) |x| {
const x_offset_i = @as(i32, @intCast(x)) + self.start_pos[VEC_X]; const cell_x = @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];
@@ -493,12 +540,12 @@ fn draw(self: *DurFile) void {
color_map_1 = durcolor_table_to_color16[color_map_1 + 1]; // Add 1, dur source stores it like this for some reason color_map_1 = durcolor_table_to_color16[color_map_1 + 1]; // Add 1, dur source stores it like this for some reason
} }
const fg_color = if (self.full_color) convert_256_to_rgb(color_map_0) else tb_color_16[color_map_0]; const fg_color = if (self.full_color) convert256ToRgb(color_map_0) else tb_color_16[color_map_0];
const bg_color = if (self.full_color) convert_256_to_rgb(color_map_1) else tb_color_16[color_map_1]; const bg_color = if (self.full_color) convert256ToRgb(color_map_1) else tb_color_16[color_map_1];
const cell = Cell{ .ch = @intCast(codepoint), .fg = fg_color, .bg = bg_color }; const cell = Cell{ .ch = @intCast(codepoint), .fg = fg_color, .bg = bg_color };
cell.put(cell_x, cell_y); self.terminal_buffer.setCellBoundsChecked(cell_x, cell_y, cell);
} }
} }

View File

@@ -417,19 +417,27 @@ fn xauth(log_file: *LogFile, allocator: std.mem.Allocator, io: std.Io, display_n
const magic_cookie = mcookie(io); const magic_cookie = mcookie(io);
log_file.deinit(io);
const pid = std.posix.system.fork(); const pid = std.posix.system.fork();
if (pid == 0) { if (pid == 0) {
try log_file.reinit(io);
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 }); 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.system.execve(shell, &args, std.c.environ);
log_file.deinit(io);
std.process.exit(1); std.process.exit(1);
} }
var status: c_int = undefined; var status: c_int = undefined;
const result = std.posix.system.waitpid(pid, &status, 0); const result = std.posix.system.waitpid(pid, &status, 0);
try log_file.reinit(io);
if (interop.isError(result) or status != 0) { if (interop.isError(result) or status != 0) {
try log_file.err( try log_file.err(
io, io,

View File

@@ -23,6 +23,8 @@ bigclock_12hr: bool = false,
bigclock_seconds: bool = false, bigclock_seconds: bool = false,
blank_box: bool = true, blank_box: bool = true,
border_fg: u32 = 0x00FFFFFF, border_fg: u32 = 0x00FFFFFF,
box_position_h: f32 = 0.5,
box_position_v: f32 = 0.4,
box_title: ?[]const u8 = null, box_title: ?[]const u8 = null,
brightness_down_cmd: [:0]const u8 = build_options.prefix_directory ++ "/bin/brightnessctl -q -n s 10%-", brightness_down_cmd: [:0]const u8 = build_options.prefix_directory ++ "/bin/brightnessctl -q -n s 10%-",
brightness_down_key: ?[]const u8 = "F5", brightness_down_key: ?[]const u8 = "F5",
@@ -72,7 +74,7 @@ lang: []const u8 = "en",
login_cmd: ?[]const u8 = null, login_cmd: ?[]const u8 = null,
login_defs_path: []const u8 = "/etc/login.defs", login_defs_path: []const u8 = "/etc/login.defs",
logout_cmd: ?[]const u8 = null, 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,
numlock: bool = false, numlock: bool = false,

View File

@@ -146,12 +146,10 @@ pub fn main(init: std.process.Init) !void {
// If we can't shutdown or restart due to an error, we print it to standard error. If that fails, just bail out // If we can't shutdown or restart due to an error, we print it to standard error. If that fails, just bail out
if (shutdown) { if (shutdown) {
const shutdown_error = std.process.replace(state.io, .{ .argv = &[_][]const u8{ "/bin/sh", "-c", shutdown_cmd } }); const shutdown_error = std.process.replace(state.io, .{ .argv = &[_][]const u8{ "/bin/sh", "-c", shutdown_cmd } });
stderr.print("error: couldn't shutdown: {s}\n", .{@errorName(shutdown_error)}) catch std.process.exit(1); std.log.err("couldn't shutdown: {s}", .{@errorName(shutdown_error)});
stderr.flush() catch std.process.exit(1);
} else if (restart) { } else if (restart) {
const restart_error = std.process.replace(state.io, .{ .argv = &[_][]const u8{ "/bin/sh", "-c", restart_cmd } }); const restart_error = std.process.replace(state.io, .{ .argv = &[_][]const u8{ "/bin/sh", "-c", restart_cmd } });
stderr.print("error: couldn't restart: {s}\n", .{@errorName(restart_error)}) catch std.process.exit(1); std.log.err("couldn't restart: {s}", .{@errorName(restart_error)});
stderr.flush() catch std.process.exit(1);
} else { } else {
// The user has quit Ly using Ctrl+C // The user has quit Ly using Ctrl+C
if (commands_allocated) { if (commands_allocated) {
@@ -172,7 +170,8 @@ pub fn main(init: std.process.Init) !void {
\\-h, --help Shows all commands. \\-h, --help Shows all commands.
\\-v, --version Shows the version of Ly. \\-v, --version Shows the version of Ly.
\\-c, --config <str> Overrides the default configuration path. Example: --config /usr/share/ly \\-c, --config <str> Overrides the default configuration path. Example: --config /usr/share/ly
\\--use-kmscon-vt Use KMSCON instead of kernel VT \\--use-kmscon-vt Uses KMSCON instead of the kernel VT.
\\--validate-config <str> Validates the given configuration file.
); );
var diag = clap.Diagnostic{}; var diag = clap.Diagnostic{};
@@ -200,17 +199,39 @@ pub fn main(init: std.process.Init) !void {
if (res.args.help != 0) { if (res.args.help != 0) {
try clap.help(stderr, clap.Help, &params, .{}); try clap.help(stderr, clap.Help, &params, .{});
_ = try stderr.write("Note: if you want to configure Ly, please check the config file, which is located at " ++ build_options.config_directory ++ "/ly/config.ini.\n"); std.log.info("note: if you want to configure Ly, please check the config file, which is located at " ++ build_options.config_directory ++ "/ly/config.ini.", .{});
try stderr.flush();
std.process.exit(0); std.process.exit(0);
} }
if (res.args.version != 0) { if (res.args.version != 0) {
_ = try stderr.write("Ly version " ++ build_options.version ++ "\n"); std.log.info("ly version " ++ build_options.version, .{});
try stderr.flush();
std.process.exit(0); std.process.exit(0);
} }
if (res.args.config) |path| config_parent_path = path; if (res.args.config) |path| config_parent_path = path;
if (res.args.@"use-kmscon-vt" != 0) state.use_kmscon_vt = true; if (res.args.@"use-kmscon-vt" != 0) state.use_kmscon_vt = true;
if (res.args.@"validate-config") |path| {
var parser = try IniParser(Config).init(
state.allocator,
state.io,
path,
migrator.configFieldHandler,
);
defer parser.deinit();
for (parser.errors.items) |err| {
std.log.err(
"failed to convert value '{s}' of option '{s}' to type '{s}': {s}",
.{ err.value, err.key, err.type_name, err.error_name },
);
}
if (parser.maybe_load_error) |err| {
std.log.err("failed to load config file: {s}", .{@errorName(err)});
std.process.exit(1);
}
std.log.info("no errors detected!", .{});
std.process.exit(0);
}
} }
// Load configuration file // Load configuration file
@@ -359,7 +380,16 @@ pub fn main(init: std.process.Init) !void {
// Initialize terminal buffer // Initialize terminal buffer
try state.log_file.info(state.io, "tui", "initializing terminal buffer", .{}); try state.log_file.info(state.io, "tui", "initializing terminal buffer", .{});
state.labels_max_length = @max(TerminalBuffer.strWidth(state.lang.login), TerminalBuffer.strWidth(state.lang.password)); var labels = [_][]const u8{
state.lang.login,
state.lang.password,
state.lang.wayland,
state.lang.x11,
state.lang.shell,
state.lang.xinitrc,
state.lang.custom,
};
state.labels_max_length = maxWidths(&labels);
var seed: u64 = undefined; var seed: u64 = undefined;
state.io.random(std.mem.asBytes(&seed)); // Get a random seed for the PRNG (used by animations) state.io.random(std.mem.asBytes(&seed)); // Get a random seed for the PRNG (used by animations)
@@ -1312,6 +1342,16 @@ pub fn main(init: std.process.Init) !void {
); );
} }
fn maxWidths(labels: [][]const u8) usize {
var max_width: usize = 0;
for (labels) |label| {
max_width = @max(max_width, TerminalBuffer.strWidth(label));
}
return max_width;
}
fn uiErrorHandler(err: anyerror, ctx: *anyopaque) anyerror!void { fn uiErrorHandler(err: anyerror, ctx: *anyopaque) anyerror!void {
var state: *UiState = @ptrCast(@alignCast(ctx)); var state: *UiState = @ptrCast(@alignCast(ctx));
@@ -2025,22 +2065,36 @@ fn positionWidgets(ptr: *anyopaque) !void {
.childrenPosition() .childrenPosition()
.removeX(TerminalBuffer.strWidth(state.lang.numlock) + TerminalBuffer.strWidth(state.lang.capslock) + 1)); .removeX(TerminalBuffer.strWidth(state.lang.numlock) + TerminalBuffer.strWidth(state.lang.capslock) + 1));
state.box.positionXY(TerminalBuffer.START_POSITION var bb_height = state.box.height;
.addX((state.buffer.width - @min(state.buffer.width - 2, state.box.width)) / 2) var bb_width = state.box.width;
.addY((state.buffer.height - @min(state.buffer.height - 2, state.box.height)) / 2)); const clock_text_len = TerminalBuffer.strWidth(state.bigclock_label.text) * (BigLabel.CHAR_WIDTH + 1);
if (state.config.bigclock != .none) { if (state.config.bigclock != .none) {
const half_width = state.buffer.width / 2; bb_height += BigLabel.CHAR_HEIGHT + 2;
const half_label_width = (TerminalBuffer.strWidth(state.bigclock_label.text) * (BigLabel.CHAR_WIDTH + 1)) / 2; bb_width = @max(bb_width, clock_text_len);
const half_height = (if (state.buffer.height > state.box.height) state.buffer.height - state.box.height else state.buffer.height) / 2;
state.bigclock_label.positionXY(TerminalBuffer.START_POSITION
.addX(half_width)
.removeXIf(half_label_width, half_width > half_label_width)
.addY(half_height)
.removeYIf(BigLabel.CHAR_HEIGHT + 2, half_height > BigLabel.CHAR_HEIGHT + 2));
} }
const max_v_position: f32 = @floatFromInt(state.buffer.height - bb_height - 1);
const max_h_position: f32 = @floatFromInt(state.buffer.width - bb_width - 1);
bb_height = @min(bb_height, state.buffer.height - 2);
bb_width = @min(bb_width, state.buffer.width - 2);
const v_space: f32 = @floatFromInt(state.buffer.height - bb_height);
const v_position: usize = @intFromFloat(std.math.clamp(v_space * state.config.box_position_v, 1.0, max_v_position));
const h_space: f32 = @floatFromInt(state.buffer.width - bb_width);
const h_position: usize = @intFromFloat(std.math.clamp(h_space * state.config.box_position_h, 1.0, max_h_position));
if (state.config.bigclock != .none) {
state.bigclock_label.positionXY(TerminalBuffer.START_POSITION
.addX(h_position + (bb_width - clock_text_len) / 2)
.addY(v_position));
}
state.box.positionXY(TerminalBuffer.START_POSITION
.addX(h_position + (bb_width - state.box.width) / 2)
.addY(v_position + (bb_height - state.box.height)));
state.info_line.label.positionY(state.box state.info_line.label.positionY(state.box
.childrenPosition()); .childrenPosition());