Files
ly/ly-ui/src/Widget.zig
RadsammyT 7a8d913531 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 <anerruption+codeberg@disroot.org>
Co-authored-by: RadsammyT <radsammyt@gmail.com>
Co-committed-by: RadsammyT <radsammyt@gmail.com>
2026-03-27 17:15:49 +01:00

187 lines
5.1 KiB
Zig

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;
}