From 7a8d913531d2f3638268bc894766c51845bc411a Mon Sep 17 00:00:00 2001 From: RadsammyT Date: Fri, 27 Mar 2026 17:15:49 +0100 Subject: [PATCH] Feature: Add custom command & label support (#945) ## What are the changes about? Adds customizable commands and labels to ly. Solves https://codeberg.org/fairyglade/ly/issues/905. Since Ly doesn't use INI headers. I use them exclusively for declarations of custom commands and labels. ### Commands Bind a keybind to a command, and add a hint to the HUD. Useful for use cases like display brightness, switching between GPUs, etc. Supports localization in the `name` field only. ex: where `lang = es`: `$brightness_up` => `bajar brillo` Declared in config.ini with the following: ```ini [cmd:F8] name = custom command 2 cmd = touch /tmp/ly.gaming ``` ### Labels Add a label to the HUD. As specified in #905. The text of the label corresponds to the output of the command specified in `[lbl:NAME]`. Only shows the first line of the output. Declared in config.ini with the following: ```ini [lbl:kernel] cmd = uname -srn refresh = 0 ``` Example to add to the config.ini: ```ini # Declare a command with the F8 binding. [cmd:F8] #The name of the command to show up in Ly. name = custom command cmd = touch /tmp/ly.gaming # Declare a label with an ID. This ID should be unique across all labels. [lbl:kernel] cmd = uname -srn # In frames, the time to re-run the command and update the label. If 0, only run once- do not refresh. refresh = 0 # Once you're done setting up labels and commands, add an empty header # below to continue configurating the rest of Ly. # Put other settings not belonging to custom commands/labels below here. [] ``` ## Pre-requisites - [x] I have tested & confirmed the changes work locally ![image](/attachments/f9373ac9-567e-4f47-987c-1df6f4ee0d84) Reviewed-on: https://codeberg.org/fairyglade/ly/pulls/945 Reviewed-by: AnErrupTion Co-authored-by: RadsammyT Co-committed-by: RadsammyT --- ly-ui/src/Widget.zig | 5 +- res/config.ini | 37 +++++++++ res/lang/ar.ini | 3 + res/lang/bg.ini | 3 + res/lang/cat.ini | 3 + res/lang/cs.ini | 3 + res/lang/de.ini | 3 + res/lang/en.ini | 3 + res/lang/eo.ini | 4 + res/lang/es.ini | 3 + res/lang/fr.ini | 3 + res/lang/it.ini | 3 + res/lang/ja_JP.ini | 3 + res/lang/ku.ini | 3 + res/lang/lv.ini | 3 + res/lang/pl.ini | 3 + res/lang/pt.ini | 3 + res/lang/pt_BR.ini | 3 + res/lang/ro.ini | 3 + res/lang/ru.ini | 3 + res/lang/sr.ini | 3 + res/lang/sv.ini | 3 + res/lang/tr.ini | 3 + res/lang/uk.ini | 3 + res/lang/zh_CN.ini | 3 + src/config/Config.zig | 1 + src/config/Lang.zig | 3 + src/config/custom.zig | 25 ++++++ src/config/migrator.zig | 56 +++++++++++++ src/main.zig | 180 ++++++++++++++++++++++++++++++++++++++++ 30 files changed, 376 insertions(+), 1 deletion(-) create mode 100644 src/config/custom.zig diff --git a/ly-ui/src/Widget.zig b/ly-ui/src/Widget.zig index d66fce8..07f76c3 100644 --- a/ly-ui/src/Widget.zig +++ b/ly-ui/src/Widget.zig @@ -12,6 +12,8 @@ const VTable = struct { 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, @@ -101,8 +103,9 @@ pub fn init( }; }; + idCounter += 1; return .{ - .id = @intFromPtr(Impl.vtable.draw_fn), + .id = idCounter, .display_name = display_name, .keybinds = keybinds, .pointer = pointer, diff --git a/res/config.ini b/res/config.ini index 0814165..8bcbd29 100644 --- a/res/config.ini +++ b/res/config.ini @@ -143,6 +143,11 @@ colormix_col2 = 0x000000FF # Color mixing animation third color id 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 # You can specify multiple directories, # e.g. $CONFIG_DIRECTORY/ly/custom-sessions:$PREFIX_DIRECTORY/share/custom-sessions @@ -380,3 +385,35 @@ xinitrc = ~/.xinitrc # You can specify multiple directories, # e.g. $PREFIX_DIRECTORY/share/xsessions:$PREFIX_DIRECTORY/local/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. +#name = custom command $brightness_up +#cmd = touch /tmp/ly.gaming +# +## 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 +# +## Once you're done setting up labels and commands, add an empty header +## below to continue configurating the rest of Ly. +## Put other settings not belonging to custom commands/labels below here. +#[] +# +##--- diff --git a/res/lang/ar.ini b/res/lang/ar.ini index 14aaee6..d8cbecf 100644 --- a/res/lang/ar.ini +++ b/res/lang/ar.ini @@ -3,6 +3,9 @@ brightness_down = خفض السطوع brightness_up = رفع السطوع capslock = capslock + + + err_alloc = فشل في تخصيص الذاكرة diff --git a/res/lang/bg.ini b/res/lang/bg.ini index ee38f60..6795a4d 100644 --- a/res/lang/bg.ini +++ b/res/lang/bg.ini @@ -3,6 +3,9 @@ brightness_down = намаляване на яркостта brightness_up = увеличаване на яркостта capslock = caps lock custom = персонализирано + + + err_alloc = неуспешно заделяне на памет err_args = неуспешен анализ на аргументите от командния ред err_autologin_session = сесията за автоматично влизане не е намерена diff --git a/res/lang/cat.ini b/res/lang/cat.ini index 967c728..152cb96 100644 --- a/res/lang/cat.ini +++ b/res/lang/cat.ini @@ -3,6 +3,9 @@ brightness_down = abaixar brillantor brightness_up = apujar brillantor capslock = Bloq Majús + + + err_alloc = assignació de memòria fallida diff --git a/res/lang/cs.ini b/res/lang/cs.ini index 9e879e1..da0bce2 100644 --- a/res/lang/cs.ini +++ b/res/lang/cs.ini @@ -3,6 +3,9 @@ capslock = capslock + + + err_alloc = alokace paměti selhala diff --git a/res/lang/de.ini b/res/lang/de.ini index d4a5f30..f869dcb 100644 --- a/res/lang/de.ini +++ b/res/lang/de.ini @@ -3,6 +3,9 @@ brightness_down = Helligkeit- brightness_up = Helligkeit+ capslock = Feststelltaste + + + err_alloc = Speicherzuweisung fehlgeschlagen diff --git a/res/lang/en.ini b/res/lang/en.ini index 082b02f..b3d40f2 100644 --- a/res/lang/en.ini +++ b/res/lang/en.ini @@ -3,6 +3,9 @@ brightness_down = decrease brightness brightness_up = increase brightness capslock = capslock custom = custom +custom_info_err_output_long = output too long +custom_info_err_no_output = no output +custom_info_err_no_output_error = , possible error err_alloc = failed memory allocation err_args = unable to parse command line arguments err_autologin_session = autologin session not found diff --git a/res/lang/eo.ini b/res/lang/eo.ini index 9c75dc3..8463432 100644 --- a/res/lang/eo.ini +++ b/res/lang/eo.ini @@ -3,6 +3,9 @@ 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 @@ -73,6 +76,7 @@ restart = restartigi shell = ŝelo shutdown = malŝalti sleep = memordormi + wayland = wayland x11 = x11 xinitrc = xinitrc diff --git a/res/lang/es.ini b/res/lang/es.ini index 9f04ecb..38c2b9d 100644 --- a/res/lang/es.ini +++ b/res/lang/es.ini @@ -3,6 +3,9 @@ brightness_down = bajar brillo brightness_up = subir brillo capslock = Bloq Mayús + + + err_alloc = asignación de memoria fallida diff --git a/res/lang/fr.ini b/res/lang/fr.ini index 886149b..df619d2 100644 --- a/res/lang/fr.ini +++ b/res/lang/fr.ini @@ -3,6 +3,9 @@ brightness_down = diminuer la luminosité brightness_up = augmenter la luminosité capslock = verr.maj custom = customisé + + + err_alloc = échec d'allocation mémoire err_args = échec de l'analyse des arguments en lignes de commande err_autologin_session = session de connexion automatique introuvable diff --git a/res/lang/it.ini b/res/lang/it.ini index 245c84e..e8af2a6 100644 --- a/res/lang/it.ini +++ b/res/lang/it.ini @@ -3,6 +3,9 @@ capslock = capslock + + + err_alloc = impossibile allocare memoria diff --git a/res/lang/ja_JP.ini b/res/lang/ja_JP.ini index 9c98b93..309ff39 100644 --- a/res/lang/ja_JP.ini +++ b/res/lang/ja_JP.ini @@ -3,6 +3,9 @@ brightness_down = 明るさを下げる brightness_up = 明るさを上げる capslock = CapsLock + + + err_alloc = メモリ割り当て失敗 diff --git a/res/lang/ku.ini b/res/lang/ku.ini index a47ef65..5775274 100644 --- a/res/lang/ku.ini +++ b/res/lang/ku.ini @@ -3,6 +3,9 @@ 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 diff --git a/res/lang/lv.ini b/res/lang/lv.ini index 6def7f8..a1a3fa9 100644 --- a/res/lang/lv.ini +++ b/res/lang/lv.ini @@ -3,6 +3,9 @@ brightness_down = samazināt spilgtumu brightness_up = palielināt spilgtumu capslock = caps lock custom = pielāgots + + + err_alloc = neizdevās atmiņas piešķiršana diff --git a/res/lang/pl.ini b/res/lang/pl.ini index 3adef78..4c521e4 100644 --- a/res/lang/pl.ini +++ b/res/lang/pl.ini @@ -3,6 +3,9 @@ brightness_down = zmniejsz jasność brightness_up = zwiększ jasność capslock = capslock custom = własny + + + err_alloc = nieudana alokacja pamięci err_autologin_session = nie znaleziono sesji autologowania diff --git a/res/lang/pt.ini b/res/lang/pt.ini index 608a122..0b13276 100644 --- a/res/lang/pt.ini +++ b/res/lang/pt.ini @@ -3,6 +3,9 @@ capslock = capslock + + + err_alloc = erro na atribuição de memória diff --git a/res/lang/pt_BR.ini b/res/lang/pt_BR.ini index fb5d58e..ca96a3e 100644 --- a/res/lang/pt_BR.ini +++ b/res/lang/pt_BR.ini @@ -3,6 +3,9 @@ capslock = caixa alta + + + err_alloc = alocação de memória malsucedida diff --git a/res/lang/ro.ini b/res/lang/ro.ini index 33c6e5d..0bf92f2 100644 --- a/res/lang/ro.ini +++ b/res/lang/ro.ini @@ -22,6 +22,9 @@ capslock = capslock + + + diff --git a/res/lang/ru.ini b/res/lang/ru.ini index baad2f2..23cec27 100644 --- a/res/lang/ru.ini +++ b/res/lang/ru.ini @@ -3,6 +3,9 @@ brightness_down = уменьшить яркость brightness_up = увеличить яркость capslock = capslock custom = пользовательский + + + err_alloc = не удалось выделить память err_autologin_session = не найдена сессия с автологином diff --git a/res/lang/sr.ini b/res/lang/sr.ini index e5dcd4b..d0ad85a 100644 --- a/res/lang/sr.ini +++ b/res/lang/sr.ini @@ -3,6 +3,9 @@ capslock = capslock + + + err_alloc = neuspijesna alokacija memorije diff --git a/res/lang/sv.ini b/res/lang/sv.ini index 2cb113c..adec801 100644 --- a/res/lang/sv.ini +++ b/res/lang/sv.ini @@ -3,6 +3,9 @@ brightness_down = minska ljusstyrka brightness_up = öka ljusstyrka capslock = capslock custom = anpassad + + + err_alloc = minnesallokering misslyckades err_args = tolkning av kommandoargument misslyckades err_autologin_session = autologin-session hittades inte diff --git a/res/lang/tr.ini b/res/lang/tr.ini index 807cf30..4ee5960 100644 --- a/res/lang/tr.ini +++ b/res/lang/tr.ini @@ -3,6 +3,9 @@ brightness_down = parlakligi azalt brightness_up = parlakligi arttir capslock = capslock + + + err_alloc = basarisiz bellek ayirma diff --git a/res/lang/uk.ini b/res/lang/uk.ini index fe37435..7b47f8a 100644 --- a/res/lang/uk.ini +++ b/res/lang/uk.ini @@ -3,6 +3,9 @@ capslock = capslock + + + err_alloc = невдале виділення пам'яті diff --git a/res/lang/zh_CN.ini b/res/lang/zh_CN.ini index ce1b23c..d2af6c3 100644 --- a/res/lang/zh_CN.ini +++ b/res/lang/zh_CN.ini @@ -3,6 +3,9 @@ capslock = 大写锁定 + + + err_alloc = 内存分配失败 diff --git a/src/config/Config.zig b/src/config/Config.zig index b3f1e53..ec6d60a 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -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, diff --git a/src/config/Lang.zig b/src/config/Lang.zig index e21700a..c01cff0 100644 --- a/src/config/Lang.zig +++ b/src/config/Lang.zig @@ -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", diff --git a/src/config/custom.zig b/src/config/custom.zig new file mode 100644 index 0000000..057e063 --- /dev/null +++ b/src/config/custom.zig @@ -0,0 +1,25 @@ +const std = @import("std"); + +const custom = @This(); + +pub const CustomCommandBind = struct { + name: []const u8 = "", + cmd: []const u8 = "", +}; + +pub const UNDEFINED_CMD: []const u8 = "echo \"You forgot to define 'cmd'!\""; + +pub const CustomCommandInfo = struct { + name: []const u8 = "", + cmd: ?[]const u8 = null, + /// To be set to the label's widget ID + id: u64 = 0, + + /// In frames, the refresh rate for the `cmd` to run again + /// If 0, only run once. + refresh: u32 = 0, + counter: u32 = 0, +}; + +pub var binds: std.StringHashMap(CustomCommandBind) = undefined; +pub var labels: std.StringHashMap(CustomCommandInfo) = undefined; diff --git a/src/config/migrator.zig b/src/config/migrator.zig index cc32cf4..1c95d51 100644 --- a/src/config/migrator.zig +++ b/src/config/migrator.zig @@ -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; } diff --git a/src/main.zig b/src/main.zig index a3e767f..844a832 100644 --- a/src/main.zig +++ b/src/main.zig @@ -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 '�' (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