Merge branch 'master' of codeberg.org:fairyglade/ly

Signed-off-by: AnErrupTion <anerruption@disroot.org>
This commit is contained in:
AnErrupTion
2026-03-27 17:17:18 +01:00
30 changed files with 376 additions and 1 deletions

View File

@@ -37,6 +37,7 @@ cmatrix_max_codepoint: u16 = 0x7B,
colormix_col1: u32 = 0x00FF0000,
colormix_col2: u32 = 0x000000FF,
colormix_col3: u32 = 0x20000000,
custom_bind_width: ?u32 = null,
custom_sessions: []const u8 = build_options.config_directory ++ "/ly/custom-sessions",
default_input: Input = .login,
doom_fire_height: u8 = 6,

View File

@@ -8,6 +8,9 @@ brightness_down: []const u8 = "decrease brightness",
brightness_up: []const u8 = "increase brightness",
capslock: []const u8 = "capslock",
custom: []const u8 = "custom",
custom_info_err_output_long: []const u8 = "output too long",
custom_info_err_no_output: []const u8 = "no output",
custom_info_err_no_output_error: []const u8 = ", possible error",
err_alloc: []const u8 = "failed memory allocation",
err_args: []const u8 = "unable to parse command line arguments",
err_autologin_session: []const u8 = "autologin session not found",

25
src/config/custom.zig Normal file
View File

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

View File

@@ -16,6 +16,7 @@ const ini = ly_core.ini;
const Config = @import("Config.zig");
const OldSave = @import("OldSave.zig");
const SavedUsers = @import("SavedUsers.zig");
const custom = @import("custom.zig");
const color_properties = [_][]const u8{
"bg",
@@ -162,6 +163,61 @@ pub fn configFieldHandler(_: std.mem.Allocator, field: ini.IniField) ?ini.IniFie
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(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(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;
}

View File

@@ -38,6 +38,7 @@ const Lang = @import("config/Lang.zig");
const migrator = @import("config/migrator.zig");
const OldSave = @import("config/OldSave.zig");
const SavedUsers = @import("config/SavedUsers.zig");
const custom = @import("config/custom.zig");
const DisplayServer = @import("enums.zig").DisplayServer;
const Environment = @import("Environment.zig");
const Entry = Environment.Entry;
@@ -63,6 +64,17 @@ fn ttyControlTransferSignalHandler(_: c_int) callconv(.c) void {
TerminalBuffer.shutdown();
}
const CustomBindLabel = struct {
cmd: custom.CustomCommandBind,
key: []const u8,
lbl: Label,
};
const CustomInfoLabel = struct {
info: custom.CustomCommandInfo,
lbl: Label,
};
const UiState = struct {
allocator: Allocator,
auth_fails: u64,
@@ -107,6 +119,8 @@ const UiState = struct {
bigclock_format_buf: [16:0]u8,
clock_buf: [64:0]u8,
bigclock_buf: [32:0]u8,
custom_binds: std.ArrayList(CustomBindLabel),
custom_info: std.ArrayList(CustomInfoLabel),
};
var shutdown = false;
@@ -206,8 +220,26 @@ pub fn main() !void {
const config_path = try std.fs.path.join(state.allocator, &[_][]const u8{ config_parent_path, "config.ini" });
defer state.allocator.free(config_path);
custom.binds = .init(state.allocator);
custom.labels = .init(state.allocator);
var config_parser = try IniParser(Config).init(state.allocator, config_path, migrator.configFieldHandler);
defer config_parser.deinit();
defer if (!shutdown or !restart) {
var iter = custom.binds.iterator();
while (iter.next()) |i| {
temporary_allocator.free(i.key_ptr.*);
temporary_allocator.free(i.value_ptr.*.cmd);
temporary_allocator.free(i.value_ptr.*.name);
}
custom.binds.deinit();
var labelIter = custom.labels.iterator();
while (labelIter.next()) |i| {
temporary_allocator.free(i.key_ptr.*);
if (i.value_ptr.cmd) |cmd|
temporary_allocator.free(cmd);
}
custom.labels.deinit();
};
state.config = config_parser.structure;
@@ -1043,6 +1075,56 @@ pub fn main() !void {
var layer2: std.ArrayList(*Widget) = .empty;
defer layer2.deinit(state.allocator);
state.custom_binds = .empty;
defer state.custom_binds.deinit(state.allocator);
state.custom_info = .empty;
defer state.custom_info.deinit(state.allocator);
var lblIter = custom.labels.iterator();
// NOTE: Because widgets have a pointer to the underlying Label, we have to ensure
// that the ArrayList doesn't allocate more memory than what we ensured. Otherwise
// the pointer to the Label becomes invalid.
try state.custom_info.ensureTotalCapacity(state.allocator, @intCast(custom.labels.count()));
while (lblIter.next()) |i| {
try state.custom_info.append(state.allocator, .{
.info = i.value_ptr.*,
.lbl = .init("", null, state.buffer.fg, state.buffer.bg, updateCustomInfo, null),
});
var latest = &state.custom_info.items[state.custom_info.items.len - 1];
latest.info.id = latest.lbl.widget().id;
latest.info.counter = 1;
}
defer for (state.custom_info.items) |*item| {
item.lbl.deinit();
};
var iter = custom.binds.iterator();
while (iter.next()) |i| {
var concat = try std.mem.concat(state.allocator, u8, &[_][]const u8{ i.key_ptr.*, " ", i.value_ptr.name });
inline for (@typeInfo(Lang).@"struct".fields) |lang_key| {
const new = try std.mem.replaceOwned(u8, state.allocator, concat, "$" ++ lang_key.name, @field(state.lang, lang_key.name));
state.allocator.free(concat);
concat = new;
}
try state.custom_binds.append(state.allocator, .{
.lbl = .init(
concat,
null,
state.buffer.fg,
state.buffer.bg,
null,
null,
),
.cmd = i.value_ptr.*,
.key = i.key_ptr.*,
});
state.custom_binds.items[state.custom_binds.items.len - 1].lbl.allocator = state.allocator;
}
defer for (state.custom_binds.items) |*i| {
i.lbl.deinit();
};
if (!state.config.hide_key_hints) {
try layer2.append(state.allocator, state.shutdown_label.widget());
try layer2.append(state.allocator, state.restart_label.widget());
@@ -1085,6 +1167,13 @@ pub fn main() !void {
try layer2.append(state.allocator, state.version_label.widget());
}
for (state.custom_binds.items) |*item| {
try layer2.append(state.allocator, item.lbl.widget());
}
for (state.custom_info.items) |*item| {
try layer2.append(state.allocator, item.lbl.widget());
}
try widgets.append(state.allocator, layer2.items);
// Layer 3
@@ -1093,6 +1182,10 @@ pub fn main() !void {
try widgets.append(state.allocator, &layer3);
}
for (state.custom_binds.items) |*item| {
try state.buffer.registerGlobalKeybind(item.key, &customCommand, item);
}
try state.buffer.registerGlobalKeybind("Esc", &disableInsertMode, &state);
try state.buffer.registerGlobalKeybind("I", &enableInsertMode, &state);
@@ -1253,6 +1346,16 @@ fn quit(ptr: *anyopaque) !bool {
return false;
}
fn customCommand(ptr: *anyopaque) !bool {
const lbl: *CustomBindLabel = @ptrCast(@alignCast(ptr));
var proc = std.process.Child.init(&[_][]const u8{ "/bin/sh", "-c", lbl.cmd.cmd }, lbl.lbl.allocator.?);
proc.stdout_behavior = .Ignore;
proc.stderr_behavior = .Ignore;
const res = proc.spawnAndWait() catch return false;
if (res.Exited != 0) return error.CommandFailed;
return false;
}
fn authenticate(ptr: *anyopaque) !bool {
var state: *UiState = @ptrCast(@alignCast(ptr));
@@ -1639,6 +1742,63 @@ fn updateClock(self: *Label, ptr: *anyopaque) !void {
}
}
fn updateCustomInfo(lbl: *Label, ptr: *anyopaque) !void {
const state: *UiState = @ptrCast(@alignCast(ptr));
const wid = lbl.widget().id;
var stdout = std.ArrayList(u8).empty;
defer stdout.deinit(state.allocator);
var stderr = std.ArrayList(u8).empty;
defer stderr.deinit(state.allocator);
for (state.custom_info.items) |*i| {
if (i.info.id != wid) continue;
// Here, a counter ticks down every time `updateCustomInfo` runs on that
// particular label. It will only run the command and update the label
// once it reaches to 1. If a refresh value is defined it's then reset to
// that refresh value.
if (i.info.counter == 1) {
var c = std.process.Child.init(&[_][]const u8{ "/bin/sh", "-c", i.info.cmd orelse custom.UNDEFINED_CMD }, state.allocator);
c.stderr_behavior = .Pipe;
c.stdout_behavior = .Pipe;
try c.spawn();
c.collectOutput(state.allocator, &stdout, &stderr, state.buffer.width) catch {
try stdout.print(state.allocator, "{s}: [{s}]", .{ i.info.name, state.lang.custom_info_err_output_long });
};
const newlineIdx = std.mem.indexOfAny(u8, stdout.items, "\n");
if (newlineIdx) |idx| {
stdout.shrinkAndFree(state.allocator, idx);
}
if (stdout.items.len > state.buffer.width) {
stdout.clearRetainingCapacity();
try stdout.print(state.allocator, "{s}: [{s}]", .{ i.info.name, state.lang.custom_info_err_output_long });
}
_ = try c.wait();
// Sometimes, the output of a command would have an unprintable character at
// the end of its output, causing '<27>' (U+FFFD) to appear in its place. Here, we check
// if this is the case and remove it.
if (stdout.items.len != 0 and !std.ascii.isPrint(stdout.items[stdout.items.len - 1])) {
_ = stdout.pop();
} else if (stdout.items.len == 0) {
try stdout.print(state.allocator, "{s}: [{s}{s}]", .{ i.info.name, state.lang.custom_info_err_no_output, if (stderr.items.len > 0) state.lang.custom_info_err_no_output_error else "" });
}
state.allocator.free(lbl.text);
try lbl.setTextAlloc(state.allocator, "{s}", .{stdout.items});
// Called to re-position the widgets after they receive their output.
try positionWidgets(state);
if (i.info.refresh != 0)
i.info.counter = i.info.refresh;
}
if (i.info.counter != 0)
i.info.counter -= 1;
}
}
fn calculateClockTimeout(_: *Label, _: *anyopaque) !?usize {
const time = try interop.getTimeOfDay();
@@ -1732,6 +1892,26 @@ fn positionWidgets(ptr: *anyopaque) !void {
state.brightness_up_label.positionXY(last_label
.childrenPosition()
.addX(1));
var x_offset: usize = 0;
var y_offset: usize = 1;
for (state.custom_binds.items) |*item| {
item.lbl.positionXY(state.edge_margin
.addY(y_offset)
.addX(x_offset));
x_offset += item.lbl.text.len + 1;
if (x_offset + item.lbl.text.len > state.config.custom_bind_width orelse state.buffer.width) {
x_offset = 0;
y_offset += 1;
}
}
}
for (state.custom_info.items, 0..) |*item, i| {
item.lbl.positionXY(state.edge_margin
.addY(@intCast(i))
.invertX(state.buffer.width)
.removeX(item.lbl.text.len)
.invertY(state.buffer.height)
.removeY(1));
}
state.battery_label.positionXY(state.edge_margin