mirror of
https://github.com/fairyglade/ly.git
synced 2026-03-24 17:26:04 +00:00
Split UI code into ly-ui library
Signed-off-by: AnErrupTion <anerruption@disroot.org>
This commit is contained in:
21
ly-ui/src/Cell.zig
Normal file
21
ly-ui/src/Cell.zig
Normal file
@@ -0,0 +1,21 @@
|
||||
const TerminalBuffer = @import("TerminalBuffer.zig");
|
||||
|
||||
const Cell = @This();
|
||||
|
||||
ch: u32,
|
||||
fg: u32,
|
||||
bg: u32,
|
||||
|
||||
pub fn init(ch: u32, fg: u32, bg: u32) Cell {
|
||||
return .{
|
||||
.ch = ch,
|
||||
.fg = fg,
|
||||
.bg = bg,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn put(self: Cell, x: usize, y: usize) void {
|
||||
if (self.ch == 0) return;
|
||||
|
||||
TerminalBuffer.setCell(x, y, self);
|
||||
}
|
||||
221
ly-ui/src/Position.zig
Normal file
221
ly-ui/src/Position.zig
Normal file
@@ -0,0 +1,221 @@
|
||||
const Position = @This();
|
||||
|
||||
x: usize,
|
||||
y: usize,
|
||||
|
||||
pub fn init(x: usize, y: usize) Position {
|
||||
return .{
|
||||
.x = x,
|
||||
.y = y,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn add(self: Position, other: Position) Position {
|
||||
return .{
|
||||
.x = self.x + other.x,
|
||||
.y = self.y + other.y,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn addIf(self: Position, other: Position, condition: bool) Position {
|
||||
return .{
|
||||
.x = self.x + if (condition) other.x else 0,
|
||||
.y = self.y + if (condition) other.y else 0,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn addX(self: Position, x: usize) Position {
|
||||
return .{
|
||||
.x = self.x + x,
|
||||
.y = self.y,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn addY(self: Position, y: usize) Position {
|
||||
return .{
|
||||
.x = self.x,
|
||||
.y = self.y + y,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn addXIf(self: Position, x: usize, condition: bool) Position {
|
||||
return .{
|
||||
.x = self.x + if (condition) x else 0,
|
||||
.y = self.y,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn addYIf(self: Position, y: usize, condition: bool) Position {
|
||||
return .{
|
||||
.x = self.x,
|
||||
.y = self.y + if (condition) y else 0,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn addXFrom(self: Position, other: Position) Position {
|
||||
return .{
|
||||
.x = self.x + other.x,
|
||||
.y = self.y,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn addYFrom(self: Position, other: Position) Position {
|
||||
return .{
|
||||
.x = self.x,
|
||||
.y = self.y + other.y,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn addXFromIf(self: Position, other: Position, condition: bool) Position {
|
||||
return .{
|
||||
.x = self.x + if (condition) other.x else 0,
|
||||
.y = self.y,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn addYFromIf(self: Position, other: Position, condition: bool) Position {
|
||||
return .{
|
||||
.x = self.x,
|
||||
.y = self.y + if (condition) other.y else 0,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn remove(self: Position, other: Position) Position {
|
||||
return .{
|
||||
.x = self.x - other.x,
|
||||
.y = self.y - other.y,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn removeIf(self: Position, other: Position, condition: bool) Position {
|
||||
return .{
|
||||
.x = self.x - if (condition) other.x else 0,
|
||||
.y = self.y - if (condition) other.y else 0,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn removeX(self: Position, x: usize) Position {
|
||||
return .{
|
||||
.x = self.x - x,
|
||||
.y = self.y,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn removeY(self: Position, y: usize) Position {
|
||||
return .{
|
||||
.x = self.x,
|
||||
.y = self.y - y,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn removeXIf(self: Position, x: usize, condition: bool) Position {
|
||||
return .{
|
||||
.x = self.x - if (condition) x else 0,
|
||||
.y = self.y,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn removeYIf(self: Position, y: usize, condition: bool) Position {
|
||||
return .{
|
||||
.x = self.x,
|
||||
.y = self.y - if (condition) y else 0,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn removeXFrom(self: Position, other: Position) Position {
|
||||
return .{
|
||||
.x = self.x - other.x,
|
||||
.y = self.y,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn removeYFrom(self: Position, other: Position) Position {
|
||||
return .{
|
||||
.x = self.x,
|
||||
.y = self.y - other.y,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn removeXFromIf(self: Position, other: Position, condition: bool) Position {
|
||||
return .{
|
||||
.x = self.x - if (condition) other.x else 0,
|
||||
.y = self.y,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn removeYFromIf(self: Position, other: Position, condition: bool) Position {
|
||||
return .{
|
||||
.x = self.x,
|
||||
.y = self.y - if (condition) other.y else 0,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn invert(self: Position, other: Position) Position {
|
||||
return .{
|
||||
.x = other.x - self.x,
|
||||
.y = other.y - self.y,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn invertIf(self: Position, other: Position, condition: bool) Position {
|
||||
return .{
|
||||
.x = if (condition) other.x - self.x else self.x,
|
||||
.y = if (condition) other.y - self.y else self.y,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn invertX(self: Position, width: usize) Position {
|
||||
return .{
|
||||
.x = width - self.x,
|
||||
.y = self.y,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn invertY(self: Position, height: usize) Position {
|
||||
return .{
|
||||
.x = self.x,
|
||||
.y = height - self.y,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn invertXIf(self: Position, width: usize, condition: bool) Position {
|
||||
return .{
|
||||
.x = if (condition) width - self.x else self.x,
|
||||
.y = self.y,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn invertYIf(self: Position, height: usize, condition: bool) Position {
|
||||
return .{
|
||||
.x = self.x,
|
||||
.y = if (condition) height - self.y else self.y,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn resetXFrom(self: Position, other: Position) Position {
|
||||
return .{
|
||||
.x = other.x,
|
||||
.y = self.y,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn resetYFrom(self: Position, other: Position) Position {
|
||||
return .{
|
||||
.x = self.x,
|
||||
.y = other.y,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn resetXFromIf(self: Position, other: Position, condition: bool) Position {
|
||||
return .{
|
||||
.x = if (condition) other.x else self.x,
|
||||
.y = self.y,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn resetYFromIf(self: Position, other: Position, condition: bool) Position {
|
||||
return .{
|
||||
.x = self.x,
|
||||
.y = if (condition) other.y else self.y,
|
||||
};
|
||||
}
|
||||
592
ly-ui/src/TerminalBuffer.zig
Normal file
592
ly-ui/src/TerminalBuffer.zig
Normal file
@@ -0,0 +1,592 @@
|
||||
const std = @import("std");
|
||||
const Allocator = std.mem.Allocator;
|
||||
const Random = std.Random;
|
||||
|
||||
const ly_core = @import("ly-core");
|
||||
const interop = ly_core.interop;
|
||||
const LogFile = ly_core.LogFile;
|
||||
const SharedError = ly_core.SharedError;
|
||||
pub const termbox = @import("termbox2");
|
||||
|
||||
const Cell = @import("Cell.zig");
|
||||
const keyboard = @import("keyboard.zig");
|
||||
const Position = @import("Position.zig");
|
||||
const Widget = @import("Widget.zig");
|
||||
|
||||
const TerminalBuffer = @This();
|
||||
|
||||
const KeybindCallbackFn = *const fn (*anyopaque) anyerror!bool;
|
||||
const KeybindMap = std.AutoHashMap(keyboard.Key, struct {
|
||||
callback: KeybindCallbackFn,
|
||||
context: *anyopaque,
|
||||
});
|
||||
|
||||
pub const InitOptions = struct {
|
||||
fg: u32,
|
||||
bg: u32,
|
||||
border_fg: u32,
|
||||
full_color: bool,
|
||||
is_tty: bool,
|
||||
};
|
||||
|
||||
pub const Styling = struct {
|
||||
pub const BOLD = termbox.TB_BOLD;
|
||||
pub const UNDERLINE = termbox.TB_UNDERLINE;
|
||||
pub const REVERSE = termbox.TB_REVERSE;
|
||||
pub const ITALIC = termbox.TB_ITALIC;
|
||||
pub const BLINK = termbox.TB_BLINK;
|
||||
pub const HI_BLACK = termbox.TB_HI_BLACK;
|
||||
pub const BRIGHT = termbox.TB_BRIGHT;
|
||||
pub const DIM = termbox.TB_DIM;
|
||||
};
|
||||
|
||||
pub const Color = struct {
|
||||
pub const DEFAULT = 0x00000000;
|
||||
pub const TRUE_BLACK = Styling.HI_BLACK;
|
||||
pub const TRUE_RED = 0x00FF0000;
|
||||
pub const TRUE_GREEN = 0x0000FF00;
|
||||
pub const TRUE_YELLOW = 0x00FFFF00;
|
||||
pub const TRUE_BLUE = 0x000000FF;
|
||||
pub const TRUE_MAGENTA = 0x00FF00FF;
|
||||
pub const TRUE_CYAN = 0x0000FFFF;
|
||||
pub const TRUE_WHITE = 0x00FFFFFF;
|
||||
pub const TRUE_DIM_RED = 0x00800000;
|
||||
pub const TRUE_DIM_GREEN = 0x00008000;
|
||||
pub const TRUE_DIM_YELLOW = 0x00808000;
|
||||
pub const TRUE_DIM_BLUE = 0x00000080;
|
||||
pub const TRUE_DIM_MAGENTA = 0x00800080;
|
||||
pub const TRUE_DIM_CYAN = 0x00008080;
|
||||
pub const TRUE_DIM_WHITE = 0x00C0C0C0;
|
||||
pub const ECOL_BLACK = 1;
|
||||
pub const ECOL_RED = 2;
|
||||
pub const ECOL_GREEN = 3;
|
||||
pub const ECOL_YELLOW = 4;
|
||||
pub const ECOL_BLUE = 5;
|
||||
pub const ECOL_MAGENTA = 6;
|
||||
pub const ECOL_CYAN = 7;
|
||||
pub const ECOL_WHITE = 8;
|
||||
};
|
||||
|
||||
pub const START_POSITION = Position.init(0, 0);
|
||||
|
||||
log_file: *LogFile,
|
||||
random: Random,
|
||||
width: usize,
|
||||
height: usize,
|
||||
fg: u32,
|
||||
bg: u32,
|
||||
border_fg: u32,
|
||||
box_chars: struct {
|
||||
left_up: u32,
|
||||
left_down: u32,
|
||||
right_up: u32,
|
||||
right_down: u32,
|
||||
top: u32,
|
||||
bottom: u32,
|
||||
left: u32,
|
||||
right: u32,
|
||||
},
|
||||
blank_cell: Cell,
|
||||
full_color: bool,
|
||||
termios: ?std.posix.termios,
|
||||
keybinds: KeybindMap,
|
||||
handlable_widgets: std.ArrayList(*Widget),
|
||||
run: bool,
|
||||
update: bool,
|
||||
active_widget_index: usize,
|
||||
|
||||
pub fn init(
|
||||
allocator: Allocator,
|
||||
options: InitOptions,
|
||||
log_file: *LogFile,
|
||||
random: Random,
|
||||
) !TerminalBuffer {
|
||||
// Initialize termbox
|
||||
_ = termbox.tb_init();
|
||||
|
||||
if (options.full_color) {
|
||||
_ = termbox.tb_set_output_mode(termbox.TB_OUTPUT_TRUECOLOR);
|
||||
try log_file.info("tui", "termbox2 set to 24-bit color output mode", .{});
|
||||
} else {
|
||||
try log_file.info("tui", "termbox2 set to eight-color output mode", .{});
|
||||
}
|
||||
|
||||
_ = termbox.tb_clear();
|
||||
|
||||
// Let's take some precautions here and clear the back buffer as well
|
||||
try clearBackBuffer();
|
||||
|
||||
const width: usize = @intCast(termbox.tb_width());
|
||||
const height: usize = @intCast(termbox.tb_height());
|
||||
|
||||
try log_file.info("tui", "screen resolution is {d}x{d}", .{ width, height });
|
||||
|
||||
return .{
|
||||
.log_file = log_file,
|
||||
.random = random,
|
||||
.width = width,
|
||||
.height = height,
|
||||
.fg = options.fg,
|
||||
.bg = options.bg,
|
||||
.border_fg = options.border_fg,
|
||||
.box_chars = if (interop.supportsUnicode()) .{
|
||||
.left_up = 0x250C,
|
||||
.left_down = 0x2514,
|
||||
.right_up = 0x2510,
|
||||
.right_down = 0x2518,
|
||||
.top = 0x2500,
|
||||
.bottom = 0x2500,
|
||||
.left = 0x2502,
|
||||
.right = 0x2502,
|
||||
} else .{
|
||||
.left_up = '+',
|
||||
.left_down = '+',
|
||||
.right_up = '+',
|
||||
.right_down = '+',
|
||||
.top = '-',
|
||||
.bottom = '-',
|
||||
.left = '|',
|
||||
.right = '|',
|
||||
},
|
||||
.blank_cell = Cell.init(' ', options.fg, options.bg),
|
||||
.full_color = options.full_color,
|
||||
// Needed to reclaim the TTY after giving up its control
|
||||
.termios = try std.posix.tcgetattr(std.posix.STDIN_FILENO),
|
||||
.keybinds = KeybindMap.init(allocator),
|
||||
.handlable_widgets = .empty,
|
||||
.run = true,
|
||||
.update = true,
|
||||
.active_widget_index = 0,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: *TerminalBuffer) void {
|
||||
self.keybinds.deinit();
|
||||
TerminalBuffer.shutdown();
|
||||
}
|
||||
|
||||
pub fn runEventLoop(
|
||||
self: *TerminalBuffer,
|
||||
allocator: Allocator,
|
||||
shared_error: SharedError,
|
||||
layers: [][]Widget,
|
||||
active_widget: Widget,
|
||||
inactivity_delay: u16,
|
||||
insert_mode: *bool,
|
||||
position_widgets_fn: *const fn (*anyopaque) anyerror!void,
|
||||
inactivity_event_fn: ?*const fn (*anyopaque) anyerror!void,
|
||||
context: *anyopaque,
|
||||
) !void {
|
||||
try self.registerKeybind("Ctrl+K", &moveCursorUp, self);
|
||||
try self.registerKeybind("Up", &moveCursorUp, self);
|
||||
|
||||
try self.registerKeybind("Ctrl+J", &moveCursorDown, self);
|
||||
try self.registerKeybind("Down", &moveCursorDown, self);
|
||||
|
||||
try self.registerKeybind("Tab", &wrapCursor, self);
|
||||
try self.registerKeybind("Shift+Tab", &wrapCursorReverse, self);
|
||||
|
||||
defer self.handlable_widgets.deinit(allocator);
|
||||
|
||||
var i: usize = 0;
|
||||
for (layers) |layer| {
|
||||
for (layer) |*widget| {
|
||||
if (widget.vtable.handle_fn != null) {
|
||||
try self.handlable_widgets.append(allocator, widget);
|
||||
|
||||
if (widget.id == active_widget.id) self.active_widget_index = i;
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (layers) |layer| {
|
||||
for (layer) |*widget| {
|
||||
try widget.update(context);
|
||||
}
|
||||
}
|
||||
try @call(.auto, position_widgets_fn, .{context});
|
||||
|
||||
var event: termbox.tb_event = undefined;
|
||||
var inactivity_cmd_ran = false;
|
||||
var inactivity_time_start = try interop.getTimeOfDay();
|
||||
|
||||
while (self.run) {
|
||||
if (self.update) {
|
||||
for (layers) |layer| {
|
||||
for (layer) |*widget| {
|
||||
try widget.update(context);
|
||||
}
|
||||
}
|
||||
|
||||
// Reset cursor
|
||||
const current_widget = self.getActiveWidget();
|
||||
current_widget.handle(null, insert_mode.*) catch |err| {
|
||||
shared_error.writeError(error.SetCursorFailed);
|
||||
try self.log_file.err(
|
||||
"tui",
|
||||
"failed to set cursor in active widget '{s}': {s}",
|
||||
.{ current_widget.display_name, @errorName(err) },
|
||||
);
|
||||
};
|
||||
|
||||
try TerminalBuffer.clearScreen(false);
|
||||
|
||||
for (layers) |layer| {
|
||||
for (layer) |*widget| {
|
||||
widget.draw();
|
||||
}
|
||||
}
|
||||
|
||||
TerminalBuffer.presentBuffer();
|
||||
}
|
||||
|
||||
var maybe_timeout: ?usize = null;
|
||||
for (layers) |layer| {
|
||||
for (layer) |*widget| {
|
||||
if (try widget.calculateTimeout(context)) |widget_timeout| {
|
||||
if (maybe_timeout == null or widget_timeout < maybe_timeout.?) maybe_timeout = widget_timeout;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (inactivity_event_fn) |inactivity_fn| {
|
||||
const time = try interop.getTimeOfDay();
|
||||
|
||||
if (!inactivity_cmd_ran and time.seconds - inactivity_time_start.seconds > inactivity_delay) {
|
||||
try @call(.auto, inactivity_fn, .{context});
|
||||
inactivity_cmd_ran = true;
|
||||
}
|
||||
}
|
||||
|
||||
const event_error = if (maybe_timeout) |timeout| termbox.tb_peek_event(&event, @intCast(timeout)) else termbox.tb_poll_event(&event);
|
||||
|
||||
self.update = maybe_timeout != null;
|
||||
|
||||
if (event_error < 0) continue;
|
||||
|
||||
// Input of some kind was detected, so reset the inactivity timer
|
||||
inactivity_time_start = try interop.getTimeOfDay();
|
||||
|
||||
if (event.type == termbox.TB_EVENT_RESIZE) {
|
||||
self.width = TerminalBuffer.getWidth();
|
||||
self.height = TerminalBuffer.getHeight();
|
||||
|
||||
try self.log_file.info(
|
||||
"tui",
|
||||
"screen resolution updated to {d}x{d}",
|
||||
.{ self.width, self.height },
|
||||
);
|
||||
|
||||
for (layers) |layer| {
|
||||
for (layer) |*widget| {
|
||||
widget.realloc() catch |err| {
|
||||
shared_error.writeError(error.WidgetReallocationFailed);
|
||||
try self.log_file.err(
|
||||
"tui",
|
||||
"failed to reallocate widget '{s}': {s}",
|
||||
.{ widget.display_name, @errorName(err) },
|
||||
);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
try @call(.auto, position_widgets_fn, .{context});
|
||||
|
||||
self.update = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
var maybe_keys = try self.handleKeybind(allocator, event);
|
||||
if (maybe_keys) |*keys| {
|
||||
defer keys.deinit(allocator);
|
||||
|
||||
const current_widget = self.getActiveWidget();
|
||||
for (keys.items) |key| {
|
||||
current_widget.handle(key, insert_mode.*) catch |err| {
|
||||
shared_error.writeError(error.CurrentWidgetHandlingFailed);
|
||||
try self.log_file.err(
|
||||
"tui",
|
||||
"failed to handle active widget '{s}': {s}",
|
||||
.{ current_widget.display_name, @errorName(err) },
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
self.update = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn stopEventLoop(self: *TerminalBuffer) void {
|
||||
self.run = false;
|
||||
}
|
||||
|
||||
pub fn drawNextFrame(self: *TerminalBuffer, value: bool) void {
|
||||
self.update = value;
|
||||
}
|
||||
|
||||
pub fn getActiveWidget(self: *TerminalBuffer) *Widget {
|
||||
return self.handlable_widgets.items[self.active_widget_index];
|
||||
}
|
||||
|
||||
pub fn setActiveWidget(self: *TerminalBuffer, widget: Widget) void {
|
||||
for (self.handlable_widgets.items, 0..) |widg, i| {
|
||||
if (widg.id == widget.id) self.active_widget_index = i;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn getWidth() usize {
|
||||
return @intCast(termbox.tb_width());
|
||||
}
|
||||
|
||||
pub fn getHeight() usize {
|
||||
return @intCast(termbox.tb_height());
|
||||
}
|
||||
|
||||
pub fn setCursor(x: usize, y: usize) void {
|
||||
_ = termbox.tb_set_cursor(@intCast(x), @intCast(y));
|
||||
}
|
||||
|
||||
pub fn clearScreen(clear_back_buffer: bool) !void {
|
||||
_ = termbox.tb_clear();
|
||||
if (clear_back_buffer) try clearBackBuffer();
|
||||
}
|
||||
|
||||
pub fn shutdown() void {
|
||||
_ = termbox.tb_shutdown();
|
||||
}
|
||||
|
||||
pub fn presentBuffer() void {
|
||||
_ = termbox.tb_present();
|
||||
}
|
||||
|
||||
pub fn getCell(x: usize, y: usize) ?Cell {
|
||||
var maybe_cell: ?*termbox.tb_cell = undefined;
|
||||
_ = termbox.tb_get_cell(
|
||||
@intCast(x),
|
||||
@intCast(y),
|
||||
1,
|
||||
&maybe_cell,
|
||||
);
|
||||
|
||||
if (maybe_cell) |cell| {
|
||||
return Cell.init(cell.ch, cell.fg, cell.bg);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
pub fn setCell(x: usize, y: usize, cell: Cell) void {
|
||||
_ = termbox.tb_set_cell(
|
||||
@intCast(x),
|
||||
@intCast(y),
|
||||
cell.ch,
|
||||
cell.fg,
|
||||
cell.bg,
|
||||
);
|
||||
}
|
||||
|
||||
pub fn reclaim(self: TerminalBuffer) !void {
|
||||
if (self.termios) |termios| {
|
||||
// Take back control of the TTY
|
||||
_ = termbox.tb_init();
|
||||
|
||||
if (self.full_color) {
|
||||
_ = termbox.tb_set_output_mode(termbox.TB_OUTPUT_TRUECOLOR);
|
||||
}
|
||||
|
||||
try std.posix.tcsetattr(std.posix.STDIN_FILENO, .FLUSH, termios);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn registerKeybind(
|
||||
self: *TerminalBuffer,
|
||||
keybind: []const u8,
|
||||
callback: KeybindCallbackFn,
|
||||
context: *anyopaque,
|
||||
) !void {
|
||||
const key = try self.parseKeybind(keybind);
|
||||
|
||||
self.keybinds.put(key, .{
|
||||
.callback = callback,
|
||||
.context = context,
|
||||
}) catch |err| {
|
||||
try self.log_file.err(
|
||||
"tui",
|
||||
"failed to register keybind {s}: {s}",
|
||||
.{ keybind, @errorName(err) },
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
pub fn simulateKeybind(self: *TerminalBuffer, keybind: []const u8) !bool {
|
||||
const key = try self.parseKeybind(keybind);
|
||||
|
||||
if (self.keybinds.get(key)) |binding| {
|
||||
return try @call(
|
||||
.auto,
|
||||
binding.callback,
|
||||
.{binding.context},
|
||||
);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
pub fn drawText(
|
||||
text: []const u8,
|
||||
x: usize,
|
||||
y: usize,
|
||||
fg: u32,
|
||||
bg: u32,
|
||||
) void {
|
||||
const yc: c_int = @intCast(y);
|
||||
const utf8view = std.unicode.Utf8View.init(text) catch return;
|
||||
var utf8 = utf8view.iterator();
|
||||
|
||||
var i: c_int = @intCast(x);
|
||||
while (utf8.nextCodepoint()) |codepoint| : (i += termbox.tb_wcwidth(codepoint)) {
|
||||
_ = termbox.tb_set_cell(i, yc, codepoint, fg, bg);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn drawConfinedText(
|
||||
text: []const u8,
|
||||
x: usize,
|
||||
y: usize,
|
||||
max_length: usize,
|
||||
fg: u32,
|
||||
bg: u32,
|
||||
) void {
|
||||
const yc: c_int = @intCast(y);
|
||||
const utf8view = std.unicode.Utf8View.init(text) catch return;
|
||||
var utf8 = utf8view.iterator();
|
||||
|
||||
var i: c_int = @intCast(x);
|
||||
while (utf8.nextCodepoint()) |codepoint| : (i += termbox.tb_wcwidth(codepoint)) {
|
||||
if (i - @as(c_int, @intCast(x)) >= max_length) break;
|
||||
_ = termbox.tb_set_cell(i, yc, codepoint, fg, bg);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn drawCharMultiple(
|
||||
char: u32,
|
||||
x: usize,
|
||||
y: usize,
|
||||
length: usize,
|
||||
fg: u32,
|
||||
bg: u32,
|
||||
) void {
|
||||
const cell = Cell.init(char, fg, bg);
|
||||
for (0..length) |xx| cell.put(x + xx, y);
|
||||
}
|
||||
|
||||
// Every codepoint is assumed to have a width of 1.
|
||||
// Since Ly is normally running in a TTY, this should be fine.
|
||||
pub fn strWidth(str: []const u8) usize {
|
||||
const utf8view = std.unicode.Utf8View.init(str) catch return str.len;
|
||||
var utf8 = utf8view.iterator();
|
||||
var length: c_int = 0;
|
||||
|
||||
while (utf8.nextCodepoint()) |codepoint| {
|
||||
length += termbox.tb_wcwidth(codepoint);
|
||||
}
|
||||
|
||||
return @intCast(length);
|
||||
}
|
||||
|
||||
fn clearBackBuffer() !void {
|
||||
// Clear the TTY because termbox2 doesn't seem to do it properly
|
||||
const capability = termbox.global.caps[termbox.TB_CAP_CLEAR_SCREEN];
|
||||
const capability_slice = std.mem.span(capability);
|
||||
_ = try std.posix.write(termbox.global.ttyfd, capability_slice);
|
||||
}
|
||||
|
||||
fn parseKeybind(self: *TerminalBuffer, keybind: []const u8) !keyboard.Key {
|
||||
var key = std.mem.zeroes(keyboard.Key);
|
||||
var iterator = std.mem.splitScalar(u8, keybind, '+');
|
||||
|
||||
while (iterator.next()) |item| {
|
||||
var found = false;
|
||||
|
||||
inline for (std.meta.fields(keyboard.Key)) |field| {
|
||||
if (std.ascii.eqlIgnoreCase(field.name, item)) {
|
||||
@field(key, field.name) = true;
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!found) {
|
||||
try self.log_file.err(
|
||||
"tui",
|
||||
"failed to parse key {s} of keybind {s}",
|
||||
.{ item, keybind },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return key;
|
||||
}
|
||||
|
||||
fn handleKeybind(
|
||||
self: *TerminalBuffer,
|
||||
allocator: Allocator,
|
||||
tb_event: termbox.tb_event,
|
||||
) !?std.ArrayList(keyboard.Key) {
|
||||
var keys = try keyboard.getKeyList(allocator, tb_event);
|
||||
|
||||
for (keys.items) |key| {
|
||||
if (self.keybinds.get(key)) |binding| {
|
||||
const passthrough_event = try @call(
|
||||
.auto,
|
||||
binding.callback,
|
||||
.{binding.context},
|
||||
);
|
||||
|
||||
if (!passthrough_event) {
|
||||
keys.deinit(allocator);
|
||||
return null;
|
||||
}
|
||||
|
||||
return keys;
|
||||
}
|
||||
}
|
||||
|
||||
return keys;
|
||||
}
|
||||
|
||||
fn moveCursorUp(ptr: *anyopaque) !bool {
|
||||
var state: *TerminalBuffer = @ptrCast(@alignCast(ptr));
|
||||
if (state.active_widget_index == 0) return false;
|
||||
|
||||
state.active_widget_index -= 1;
|
||||
state.update = true;
|
||||
return false;
|
||||
}
|
||||
|
||||
fn moveCursorDown(ptr: *anyopaque) !bool {
|
||||
var state: *TerminalBuffer = @ptrCast(@alignCast(ptr));
|
||||
if (state.active_widget_index == state.handlable_widgets.items.len - 1) return false;
|
||||
|
||||
state.active_widget_index += 1;
|
||||
state.update = true;
|
||||
return false;
|
||||
}
|
||||
|
||||
fn wrapCursor(ptr: *anyopaque) !bool {
|
||||
var state: *TerminalBuffer = @ptrCast(@alignCast(ptr));
|
||||
|
||||
state.active_widget_index = (state.active_widget_index + 1) % state.handlable_widgets.items.len;
|
||||
state.update = true;
|
||||
return false;
|
||||
}
|
||||
|
||||
fn wrapCursorReverse(ptr: *anyopaque) !bool {
|
||||
var state: *TerminalBuffer = @ptrCast(@alignCast(ptr));
|
||||
|
||||
state.active_widget_index = if (state.active_widget_index == 0) state.handlable_widgets.items.len - 1 else state.active_widget_index - 1;
|
||||
state.update = true;
|
||||
return false;
|
||||
}
|
||||
180
ly-ui/src/Widget.zig
Normal file
180
ly-ui/src/Widget.zig
Normal file
@@ -0,0 +1,180 @@
|
||||
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, insert_mode: bool) anyerror!void,
|
||||
calculate_timeout_fn: ?*const fn (ptr: *anyopaque, ctx: *anyopaque) anyerror!?usize,
|
||||
};
|
||||
|
||||
id: u64,
|
||||
display_name: []const u8,
|
||||
pointer: *anyopaque,
|
||||
vtable: VTable,
|
||||
|
||||
pub fn init(
|
||||
display_name: []const u8,
|
||||
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, insert_mode: bool) 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, insert_mode: bool) !void {
|
||||
const impl: Pointer = @ptrCast(@alignCast(ptr));
|
||||
|
||||
return @call(
|
||||
.always_inline,
|
||||
handle_fn.?,
|
||||
.{ impl, maybe_key, insert_mode },
|
||||
);
|
||||
}
|
||||
|
||||
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,
|
||||
};
|
||||
};
|
||||
|
||||
return .{
|
||||
.id = @intFromPtr(Impl.vtable.draw_fn),
|
||||
.display_name = display_name,
|
||||
.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, insert_mode: bool) !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, insert_mode },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
232
ly-ui/src/components/BigLabel.zig
Normal file
232
ly-ui/src/components/BigLabel.zig
Normal file
@@ -0,0 +1,232 @@
|
||||
const BigLabel = @This();
|
||||
|
||||
const std = @import("std");
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
const ly_core = @import("ly-core");
|
||||
const interop = ly_core.interop;
|
||||
|
||||
const en = @import("bigLabelLocales/en.zig");
|
||||
const fa = @import("bigLabelLocales/fa.zig");
|
||||
const Cell = @import("../Cell.zig");
|
||||
const Position = @import("../Position.zig");
|
||||
const TerminalBuffer = @import("../TerminalBuffer.zig");
|
||||
const Widget = @import("../Widget.zig");
|
||||
|
||||
pub const CHAR_WIDTH = 5;
|
||||
pub const CHAR_HEIGHT = 5;
|
||||
pub const CHAR_SIZE = CHAR_WIDTH * CHAR_HEIGHT;
|
||||
pub const X: u32 = if (ly_core.interop.supportsUnicode()) 0x2593 else '#';
|
||||
pub const O: u32 = 0;
|
||||
|
||||
// zig fmt: off
|
||||
pub const LocaleChars = struct {
|
||||
ZERO: [CHAR_SIZE]u21,
|
||||
ONE: [CHAR_SIZE]u21,
|
||||
TWO: [CHAR_SIZE]u21,
|
||||
THREE: [CHAR_SIZE]u21,
|
||||
FOUR: [CHAR_SIZE]u21,
|
||||
FIVE: [CHAR_SIZE]u21,
|
||||
SIX: [CHAR_SIZE]u21,
|
||||
SEVEN: [CHAR_SIZE]u21,
|
||||
EIGHT: [CHAR_SIZE]u21,
|
||||
NINE: [CHAR_SIZE]u21,
|
||||
S: [CHAR_SIZE]u21,
|
||||
E: [CHAR_SIZE]u21,
|
||||
P: [CHAR_SIZE]u21,
|
||||
A: [CHAR_SIZE]u21,
|
||||
M: [CHAR_SIZE]u21,
|
||||
};
|
||||
// zig fmt: on
|
||||
|
||||
pub const BigLabelLocale = enum {
|
||||
en,
|
||||
fa,
|
||||
};
|
||||
|
||||
allocator: ?Allocator = null,
|
||||
buffer: *TerminalBuffer,
|
||||
text: []const u8,
|
||||
max_width: ?usize,
|
||||
fg: u32,
|
||||
bg: u32,
|
||||
locale: BigLabelLocale,
|
||||
update_fn: ?*const fn (*BigLabel, *anyopaque) anyerror!void,
|
||||
calculate_timeout_fn: ?*const fn (*BigLabel, *anyopaque) anyerror!?usize,
|
||||
component_pos: Position,
|
||||
children_pos: Position,
|
||||
|
||||
pub fn init(
|
||||
buffer: *TerminalBuffer,
|
||||
text: []const u8,
|
||||
max_width: ?usize,
|
||||
fg: u32,
|
||||
bg: u32,
|
||||
locale: BigLabelLocale,
|
||||
update_fn: ?*const fn (*BigLabel, *anyopaque) anyerror!void,
|
||||
calculate_timeout_fn: ?*const fn (*BigLabel, *anyopaque) anyerror!?usize,
|
||||
) BigLabel {
|
||||
return .{
|
||||
.allocator = null,
|
||||
.buffer = buffer,
|
||||
.text = text,
|
||||
.max_width = max_width,
|
||||
.fg = fg,
|
||||
.bg = bg,
|
||||
.locale = locale,
|
||||
.update_fn = update_fn,
|
||||
.calculate_timeout_fn = calculate_timeout_fn,
|
||||
.component_pos = TerminalBuffer.START_POSITION,
|
||||
.children_pos = TerminalBuffer.START_POSITION,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: *BigLabel) void {
|
||||
if (self.allocator) |allocator| allocator.free(self.text);
|
||||
}
|
||||
|
||||
pub fn widget(self: *BigLabel) Widget {
|
||||
return Widget.init(
|
||||
"BigLabel",
|
||||
self,
|
||||
deinit,
|
||||
null,
|
||||
draw,
|
||||
update,
|
||||
null,
|
||||
calculateTimeout,
|
||||
);
|
||||
}
|
||||
|
||||
pub fn setTextAlloc(
|
||||
self: *BigLabel,
|
||||
allocator: Allocator,
|
||||
comptime fmt: []const u8,
|
||||
args: anytype,
|
||||
) !void {
|
||||
self.text = try std.fmt.allocPrint(allocator, fmt, args);
|
||||
self.allocator = allocator;
|
||||
}
|
||||
|
||||
pub fn setTextBuf(
|
||||
self: *BigLabel,
|
||||
buffer: []u8,
|
||||
comptime fmt: []const u8,
|
||||
args: anytype,
|
||||
) !void {
|
||||
self.text = try std.fmt.bufPrint(buffer, fmt, args);
|
||||
self.allocator = null;
|
||||
}
|
||||
|
||||
pub fn setText(self: *BigLabel, text: []const u8) void {
|
||||
self.text = text;
|
||||
self.allocator = null;
|
||||
}
|
||||
|
||||
pub fn positionX(self: *BigLabel, original_pos: Position) void {
|
||||
self.component_pos = original_pos;
|
||||
self.children_pos = original_pos.addX(TerminalBuffer.strWidth(self.text) * CHAR_WIDTH);
|
||||
}
|
||||
|
||||
pub fn positionY(self: *BigLabel, original_pos: Position) void {
|
||||
self.component_pos = original_pos;
|
||||
self.children_pos = original_pos.addY(CHAR_HEIGHT);
|
||||
}
|
||||
|
||||
pub fn positionXY(self: *BigLabel, original_pos: Position) void {
|
||||
self.component_pos = original_pos;
|
||||
self.children_pos = Position.init(
|
||||
TerminalBuffer.strWidth(self.text) * CHAR_WIDTH,
|
||||
CHAR_HEIGHT,
|
||||
).add(original_pos);
|
||||
}
|
||||
|
||||
pub fn childrenPosition(self: BigLabel) Position {
|
||||
return self.children_pos;
|
||||
}
|
||||
|
||||
fn draw(self: *BigLabel) void {
|
||||
for (self.text, 0..) |c, i| {
|
||||
const clock_cell = clockCell(
|
||||
c,
|
||||
self.fg,
|
||||
self.bg,
|
||||
self.locale,
|
||||
);
|
||||
|
||||
alphaBlit(
|
||||
self.component_pos.x + i * (CHAR_WIDTH + 1),
|
||||
self.component_pos.y,
|
||||
self.buffer.width,
|
||||
self.buffer.height,
|
||||
clock_cell,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn update(self: *BigLabel, context: *anyopaque) !void {
|
||||
if (self.update_fn) |update_fn| {
|
||||
return @call(
|
||||
.auto,
|
||||
update_fn,
|
||||
.{ self, context },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn calculateTimeout(self: *BigLabel, ctx: *anyopaque) !?usize {
|
||||
if (self.calculate_timeout_fn) |calculate_timeout_fn| {
|
||||
return @call(
|
||||
.auto,
|
||||
calculate_timeout_fn,
|
||||
.{ self, ctx },
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
fn clockCell(char: u8, fg: u32, bg: u32, locale: BigLabelLocale) [CHAR_SIZE]Cell {
|
||||
var cells: [CHAR_SIZE]Cell = undefined;
|
||||
|
||||
//@divTrunc(time.microseconds, 500000) != 0)
|
||||
const clock_chars = toBigNumber(char, locale);
|
||||
for (0..cells.len) |i| cells[i] = Cell.init(clock_chars[i], fg, bg);
|
||||
|
||||
return cells;
|
||||
}
|
||||
|
||||
fn alphaBlit(x: usize, y: usize, tb_width: usize, tb_height: usize, cells: [CHAR_SIZE]Cell) void {
|
||||
if (x + CHAR_WIDTH >= tb_width or y + CHAR_HEIGHT >= tb_height) return;
|
||||
|
||||
for (0..CHAR_HEIGHT) |yy| {
|
||||
for (0..CHAR_WIDTH) |xx| {
|
||||
const cell = cells[yy * CHAR_WIDTH + xx];
|
||||
cell.put(x + xx, y + yy);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn toBigNumber(char: u8, locale: BigLabelLocale) [CHAR_SIZE]u21 {
|
||||
const locale_chars = switch (locale) {
|
||||
.fa => fa.locale_chars,
|
||||
.en => en.locale_chars,
|
||||
};
|
||||
return switch (char) {
|
||||
'0' => locale_chars.ZERO,
|
||||
'1' => locale_chars.ONE,
|
||||
'2' => locale_chars.TWO,
|
||||
'3' => locale_chars.THREE,
|
||||
'4' => locale_chars.FOUR,
|
||||
'5' => locale_chars.FIVE,
|
||||
'6' => locale_chars.SIX,
|
||||
'7' => locale_chars.SEVEN,
|
||||
'8' => locale_chars.EIGHT,
|
||||
'9' => locale_chars.NINE,
|
||||
'p', 'P' => locale_chars.P,
|
||||
'a', 'A' => locale_chars.A,
|
||||
'm', 'M' => locale_chars.M,
|
||||
':' => locale_chars.S,
|
||||
else => locale_chars.E,
|
||||
};
|
||||
}
|
||||
189
ly-ui/src/components/CenteredBox.zig
Normal file
189
ly-ui/src/components/CenteredBox.zig
Normal file
@@ -0,0 +1,189 @@
|
||||
const std = @import("std");
|
||||
|
||||
const Cell = @import("../Cell.zig");
|
||||
const Position = @import("../Position.zig");
|
||||
const TerminalBuffer = @import("../TerminalBuffer.zig");
|
||||
const Widget = @import("../Widget.zig");
|
||||
|
||||
const CenteredBox = @This();
|
||||
|
||||
buffer: *TerminalBuffer,
|
||||
horizontal_margin: usize,
|
||||
vertical_margin: usize,
|
||||
width: usize,
|
||||
height: usize,
|
||||
show_borders: bool,
|
||||
blank_box: bool,
|
||||
top_title: ?[]const u8,
|
||||
bottom_title: ?[]const u8,
|
||||
border_fg: u32,
|
||||
title_fg: u32,
|
||||
bg: u32,
|
||||
update_fn: ?*const fn (*CenteredBox, *anyopaque) anyerror!void,
|
||||
left_pos: Position,
|
||||
right_pos: Position,
|
||||
children_pos: Position,
|
||||
|
||||
pub fn init(
|
||||
buffer: *TerminalBuffer,
|
||||
horizontal_margin: usize,
|
||||
vertical_margin: usize,
|
||||
width: usize,
|
||||
height: usize,
|
||||
show_borders: bool,
|
||||
blank_box: bool,
|
||||
top_title: ?[]const u8,
|
||||
bottom_title: ?[]const u8,
|
||||
border_fg: u32,
|
||||
title_fg: u32,
|
||||
bg: u32,
|
||||
update_fn: ?*const fn (*CenteredBox, *anyopaque) anyerror!void,
|
||||
) CenteredBox {
|
||||
return .{
|
||||
.buffer = buffer,
|
||||
.horizontal_margin = horizontal_margin,
|
||||
.vertical_margin = vertical_margin,
|
||||
.width = width,
|
||||
.height = height,
|
||||
.show_borders = show_borders,
|
||||
.blank_box = blank_box,
|
||||
.top_title = top_title,
|
||||
.bottom_title = bottom_title,
|
||||
.border_fg = border_fg,
|
||||
.title_fg = title_fg,
|
||||
.bg = bg,
|
||||
.update_fn = update_fn,
|
||||
.left_pos = TerminalBuffer.START_POSITION,
|
||||
.right_pos = TerminalBuffer.START_POSITION,
|
||||
.children_pos = TerminalBuffer.START_POSITION,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn widget(self: *CenteredBox) Widget {
|
||||
return Widget.init(
|
||||
"CenteredBox",
|
||||
self,
|
||||
null,
|
||||
null,
|
||||
draw,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
);
|
||||
}
|
||||
|
||||
pub fn positionXY(self: *CenteredBox, original_pos: Position) void {
|
||||
if (self.buffer.width < 2 or self.buffer.height < 2) return;
|
||||
|
||||
self.left_pos = Position.init(
|
||||
(self.buffer.width - @min(self.buffer.width - 2, self.width)) / 2,
|
||||
(self.buffer.height - @min(self.buffer.height - 2, self.height)) / 2,
|
||||
).add(original_pos);
|
||||
|
||||
self.right_pos = Position.init(
|
||||
(self.buffer.width + @min(self.buffer.width, self.width)) / 2,
|
||||
(self.buffer.height + @min(self.buffer.height, self.height)) / 2,
|
||||
).add(original_pos);
|
||||
|
||||
self.children_pos = Position.init(
|
||||
self.left_pos.x + self.horizontal_margin,
|
||||
self.left_pos.y + self.vertical_margin,
|
||||
).add(original_pos);
|
||||
}
|
||||
|
||||
pub fn childrenPosition(self: CenteredBox) Position {
|
||||
return self.children_pos;
|
||||
}
|
||||
|
||||
fn draw(self: *CenteredBox) void {
|
||||
if (self.show_borders) {
|
||||
var left_up = Cell.init(
|
||||
self.buffer.box_chars.left_up,
|
||||
self.border_fg,
|
||||
self.bg,
|
||||
);
|
||||
var right_up = Cell.init(
|
||||
self.buffer.box_chars.right_up,
|
||||
self.border_fg,
|
||||
self.bg,
|
||||
);
|
||||
var left_down = Cell.init(
|
||||
self.buffer.box_chars.left_down,
|
||||
self.border_fg,
|
||||
self.bg,
|
||||
);
|
||||
var right_down = Cell.init(
|
||||
self.buffer.box_chars.right_down,
|
||||
self.border_fg,
|
||||
self.bg,
|
||||
);
|
||||
var top = Cell.init(
|
||||
self.buffer.box_chars.top,
|
||||
self.border_fg,
|
||||
self.bg,
|
||||
);
|
||||
var bottom = Cell.init(
|
||||
self.buffer.box_chars.bottom,
|
||||
self.border_fg,
|
||||
self.bg,
|
||||
);
|
||||
|
||||
left_up.put(self.left_pos.x - 1, self.left_pos.y - 1);
|
||||
right_up.put(self.right_pos.x, self.left_pos.y - 1);
|
||||
left_down.put(self.left_pos.x - 1, self.right_pos.y);
|
||||
right_down.put(self.right_pos.x, self.right_pos.y);
|
||||
|
||||
for (0..self.width) |i| {
|
||||
top.put(self.left_pos.x + i, self.left_pos.y - 1);
|
||||
bottom.put(self.left_pos.x + i, self.right_pos.y);
|
||||
}
|
||||
|
||||
top.ch = self.buffer.box_chars.left;
|
||||
bottom.ch = self.buffer.box_chars.right;
|
||||
|
||||
for (0..self.height) |i| {
|
||||
top.put(self.left_pos.x - 1, self.left_pos.y + i);
|
||||
bottom.put(self.right_pos.x, self.left_pos.y + i);
|
||||
}
|
||||
}
|
||||
|
||||
if (self.blank_box) {
|
||||
for (0..self.height) |y| {
|
||||
for (0..self.width) |x| {
|
||||
self.buffer.blank_cell.put(self.left_pos.x + x, self.left_pos.y + y);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (self.top_title) |title| {
|
||||
TerminalBuffer.drawConfinedText(
|
||||
title,
|
||||
self.left_pos.x,
|
||||
self.left_pos.y - 1,
|
||||
self.width,
|
||||
self.title_fg,
|
||||
self.bg,
|
||||
);
|
||||
}
|
||||
|
||||
if (self.bottom_title) |title| {
|
||||
TerminalBuffer.drawConfinedText(
|
||||
title,
|
||||
self.left_pos.x,
|
||||
self.left_pos.y + self.height,
|
||||
self.width,
|
||||
self.title_fg,
|
||||
self.bg,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn update(self: *CenteredBox, ctx: *anyopaque) !void {
|
||||
if (self.update_fn) |update_fn| {
|
||||
return @call(
|
||||
.auto,
|
||||
update_fn,
|
||||
.{ self, ctx },
|
||||
);
|
||||
}
|
||||
}
|
||||
110
ly-ui/src/components/InfoLine.zig
Normal file
110
ly-ui/src/components/InfoLine.zig
Normal file
@@ -0,0 +1,110 @@
|
||||
const std = @import("std");
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
const keyboard = @import("../keyboard.zig");
|
||||
const TerminalBuffer = @import("../TerminalBuffer.zig");
|
||||
const Widget = @import("../Widget.zig");
|
||||
const generic = @import("generic.zig");
|
||||
|
||||
const MessageLabel = generic.CyclableLabel(Message, Message);
|
||||
|
||||
const InfoLine = @This();
|
||||
|
||||
const Message = struct {
|
||||
width: usize,
|
||||
text: []const u8,
|
||||
bg: u32,
|
||||
fg: u32,
|
||||
};
|
||||
|
||||
label: MessageLabel,
|
||||
|
||||
pub fn init(
|
||||
allocator: Allocator,
|
||||
buffer: *TerminalBuffer,
|
||||
width: usize,
|
||||
arrow_fg: u32,
|
||||
arrow_bg: u32,
|
||||
) InfoLine {
|
||||
return .{
|
||||
.label = MessageLabel.init(
|
||||
allocator,
|
||||
buffer,
|
||||
drawItem,
|
||||
null,
|
||||
null,
|
||||
width,
|
||||
true,
|
||||
arrow_fg,
|
||||
arrow_bg,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: *InfoLine) void {
|
||||
self.label.deinit();
|
||||
}
|
||||
|
||||
pub fn widget(self: *InfoLine) Widget {
|
||||
return Widget.init(
|
||||
"InfoLine",
|
||||
self,
|
||||
deinit,
|
||||
null,
|
||||
draw,
|
||||
null,
|
||||
handle,
|
||||
null,
|
||||
);
|
||||
}
|
||||
|
||||
pub fn addMessage(self: *InfoLine, text: []const u8, bg: u32, fg: u32) !void {
|
||||
if (text.len == 0) return;
|
||||
|
||||
try self.label.addItem(.{
|
||||
.width = TerminalBuffer.strWidth(text),
|
||||
.text = text,
|
||||
.bg = bg,
|
||||
.fg = fg,
|
||||
});
|
||||
}
|
||||
|
||||
pub fn clearRendered(self: InfoLine, allocator: Allocator) !void {
|
||||
// Draw over the area
|
||||
const spaces = try allocator.alloc(u8, self.label.width - 2);
|
||||
defer allocator.free(spaces);
|
||||
|
||||
@memset(spaces, ' ');
|
||||
|
||||
TerminalBuffer.drawText(
|
||||
spaces,
|
||||
self.label.component_pos.x + 2,
|
||||
self.label.component_pos.y,
|
||||
TerminalBuffer.Color.DEFAULT,
|
||||
TerminalBuffer.Color.DEFAULT,
|
||||
);
|
||||
}
|
||||
|
||||
fn draw(self: *InfoLine) void {
|
||||
self.label.draw();
|
||||
}
|
||||
|
||||
fn handle(self: *InfoLine, maybe_key: ?keyboard.Key, insert_mode: bool) !void {
|
||||
self.label.handle(maybe_key, insert_mode);
|
||||
}
|
||||
|
||||
fn drawItem(label: *MessageLabel, message: Message, x: usize, y: usize, width: usize) void {
|
||||
if (message.width == 0) return;
|
||||
|
||||
const x_offset = if (label.text_in_center and width >= message.width) (width - message.width) / 2 else 0;
|
||||
|
||||
label.cursor = message.width + x_offset;
|
||||
TerminalBuffer.drawConfinedText(
|
||||
message.text,
|
||||
x + x_offset,
|
||||
y,
|
||||
width,
|
||||
message.fg,
|
||||
message.bg,
|
||||
);
|
||||
}
|
||||
148
ly-ui/src/components/Label.zig
Normal file
148
ly-ui/src/components/Label.zig
Normal file
@@ -0,0 +1,148 @@
|
||||
const Label = @This();
|
||||
|
||||
const std = @import("std");
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
const Cell = @import("../Cell.zig");
|
||||
const Position = @import("../Position.zig");
|
||||
const TerminalBuffer = @import("../TerminalBuffer.zig");
|
||||
const Widget = @import("../Widget.zig");
|
||||
|
||||
allocator: ?Allocator,
|
||||
text: []const u8,
|
||||
max_width: ?usize,
|
||||
fg: u32,
|
||||
bg: u32,
|
||||
update_fn: ?*const fn (*Label, *anyopaque) anyerror!void,
|
||||
calculate_timeout_fn: ?*const fn (*Label, *anyopaque) anyerror!?usize,
|
||||
component_pos: Position,
|
||||
children_pos: Position,
|
||||
|
||||
pub fn init(
|
||||
text: []const u8,
|
||||
max_width: ?usize,
|
||||
fg: u32,
|
||||
bg: u32,
|
||||
update_fn: ?*const fn (*Label, *anyopaque) anyerror!void,
|
||||
calculate_timeout_fn: ?*const fn (*Label, *anyopaque) anyerror!?usize,
|
||||
) Label {
|
||||
return .{
|
||||
.allocator = null,
|
||||
.text = text,
|
||||
.max_width = max_width,
|
||||
.fg = fg,
|
||||
.bg = bg,
|
||||
.update_fn = update_fn,
|
||||
.calculate_timeout_fn = calculate_timeout_fn,
|
||||
.component_pos = TerminalBuffer.START_POSITION,
|
||||
.children_pos = TerminalBuffer.START_POSITION,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: *Label) void {
|
||||
if (self.allocator) |allocator| allocator.free(self.text);
|
||||
}
|
||||
|
||||
pub fn widget(self: *Label) Widget {
|
||||
return Widget.init(
|
||||
"Label",
|
||||
self,
|
||||
deinit,
|
||||
null,
|
||||
draw,
|
||||
update,
|
||||
null,
|
||||
calculateTimeout,
|
||||
);
|
||||
}
|
||||
|
||||
pub fn setTextAlloc(
|
||||
self: *Label,
|
||||
allocator: Allocator,
|
||||
comptime fmt: []const u8,
|
||||
args: anytype,
|
||||
) !void {
|
||||
self.text = try std.fmt.allocPrint(allocator, fmt, args);
|
||||
self.allocator = allocator;
|
||||
}
|
||||
|
||||
pub fn setTextBuf(
|
||||
self: *Label,
|
||||
buffer: []u8,
|
||||
comptime fmt: []const u8,
|
||||
args: anytype,
|
||||
) !void {
|
||||
self.text = try std.fmt.bufPrint(buffer, fmt, args);
|
||||
self.allocator = null;
|
||||
}
|
||||
|
||||
pub fn setText(self: *Label, text: []const u8) void {
|
||||
self.text = text;
|
||||
self.allocator = null;
|
||||
}
|
||||
|
||||
pub fn positionX(self: *Label, original_pos: Position) void {
|
||||
self.component_pos = original_pos;
|
||||
self.children_pos = original_pos.addX(TerminalBuffer.strWidth(self.text));
|
||||
}
|
||||
|
||||
pub fn positionY(self: *Label, original_pos: Position) void {
|
||||
self.component_pos = original_pos;
|
||||
self.children_pos = original_pos.addY(1);
|
||||
}
|
||||
|
||||
pub fn positionXY(self: *Label, original_pos: Position) void {
|
||||
self.component_pos = original_pos;
|
||||
self.children_pos = Position.init(
|
||||
TerminalBuffer.strWidth(self.text),
|
||||
1,
|
||||
).add(original_pos);
|
||||
}
|
||||
|
||||
pub fn childrenPosition(self: Label) Position {
|
||||
return self.children_pos;
|
||||
}
|
||||
|
||||
fn draw(self: *Label) void {
|
||||
if (self.max_width) |width| {
|
||||
TerminalBuffer.drawConfinedText(
|
||||
self.text,
|
||||
self.component_pos.x,
|
||||
self.component_pos.y,
|
||||
width,
|
||||
self.fg,
|
||||
self.bg,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
TerminalBuffer.drawText(
|
||||
self.text,
|
||||
self.component_pos.x,
|
||||
self.component_pos.y,
|
||||
self.fg,
|
||||
self.bg,
|
||||
);
|
||||
}
|
||||
|
||||
fn update(self: *Label, ctx: *anyopaque) !void {
|
||||
if (self.update_fn) |update_fn| {
|
||||
return @call(
|
||||
.auto,
|
||||
update_fn,
|
||||
.{ self, ctx },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn calculateTimeout(self: *Label, ctx: *anyopaque) !?usize {
|
||||
if (self.calculate_timeout_fn) |calculate_timeout_fn| {
|
||||
return @call(
|
||||
.auto,
|
||||
calculate_timeout_fn,
|
||||
.{ self, ctx },
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
117
ly-ui/src/components/Session.zig
Normal file
117
ly-ui/src/components/Session.zig
Normal file
@@ -0,0 +1,117 @@
|
||||
const std = @import("std");
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
const ly_core = @import("ly-core");
|
||||
const Environment = ly_core.Environment;
|
||||
|
||||
const keyboard = @import("../keyboard.zig");
|
||||
const TerminalBuffer = @import("../TerminalBuffer.zig");
|
||||
const Widget = @import("../Widget.zig");
|
||||
const generic = @import("generic.zig");
|
||||
const UserList = @import("UserList.zig");
|
||||
|
||||
const Env = struct {
|
||||
// TODO: Remove dependency on Environment
|
||||
environment: Environment,
|
||||
index: usize,
|
||||
};
|
||||
const EnvironmentLabel = generic.CyclableLabel(Env, *UserList);
|
||||
|
||||
const Session = @This();
|
||||
|
||||
label: EnvironmentLabel,
|
||||
user_list: *UserList,
|
||||
|
||||
pub fn init(
|
||||
allocator: Allocator,
|
||||
buffer: *TerminalBuffer,
|
||||
user_list: *UserList,
|
||||
width: usize,
|
||||
text_in_center: bool,
|
||||
fg: u32,
|
||||
bg: u32,
|
||||
) Session {
|
||||
return .{
|
||||
.label = EnvironmentLabel.init(
|
||||
allocator,
|
||||
buffer,
|
||||
drawItem,
|
||||
sessionChanged,
|
||||
user_list,
|
||||
width,
|
||||
text_in_center,
|
||||
fg,
|
||||
bg,
|
||||
),
|
||||
.user_list = user_list,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: *Session) void {
|
||||
for (self.label.list.items) |*env| {
|
||||
if (env.environment.entry_ini) |*entry_ini| entry_ini.deinit();
|
||||
self.label.allocator.free(env.environment.file_name);
|
||||
}
|
||||
|
||||
self.label.deinit();
|
||||
}
|
||||
|
||||
pub fn widget(self: *Session) Widget {
|
||||
return Widget.init(
|
||||
"Session",
|
||||
self,
|
||||
deinit,
|
||||
null,
|
||||
draw,
|
||||
null,
|
||||
handle,
|
||||
null,
|
||||
);
|
||||
}
|
||||
|
||||
pub fn addEnvironment(self: *Session, environment: Environment) !void {
|
||||
const env = Env{ .environment = environment, .index = self.label.list.items.len };
|
||||
|
||||
try self.label.addItem(env);
|
||||
addedSession(env, self.user_list);
|
||||
}
|
||||
|
||||
fn draw(self: *Session) void {
|
||||
self.label.draw();
|
||||
}
|
||||
|
||||
fn handle(self: *Session, maybe_key: ?keyboard.Key, insert_mode: bool) !void {
|
||||
self.label.handle(maybe_key, insert_mode);
|
||||
}
|
||||
|
||||
fn addedSession(env: Env, user_list: *UserList) void {
|
||||
const user = user_list.label.list.items[user_list.label.current];
|
||||
if (!user.first_run) return;
|
||||
|
||||
user.session_index.* = env.index;
|
||||
}
|
||||
|
||||
fn sessionChanged(env: Env, maybe_user_list: ?*UserList) void {
|
||||
if (maybe_user_list) |user_list| {
|
||||
user_list.label.list.items[user_list.label.current].session_index.* = env.index;
|
||||
}
|
||||
}
|
||||
|
||||
fn drawItem(label: *EnvironmentLabel, env: Env, x: usize, y: usize, width: usize) void {
|
||||
if (width < 3) return;
|
||||
|
||||
const length = @min(TerminalBuffer.strWidth(env.environment.name), width - 3);
|
||||
if (length == 0) return;
|
||||
|
||||
const x_offset = if (label.text_in_center and width >= length) (width - length) / 2 else 0;
|
||||
|
||||
label.cursor = length + x_offset;
|
||||
TerminalBuffer.drawConfinedText(
|
||||
env.environment.name,
|
||||
x + x_offset,
|
||||
y,
|
||||
width,
|
||||
label.fg,
|
||||
label.bg,
|
||||
);
|
||||
}
|
||||
210
ly-ui/src/components/Text.zig
Normal file
210
ly-ui/src/components/Text.zig
Normal file
@@ -0,0 +1,210 @@
|
||||
const std = @import("std");
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
const keyboard = @import("../keyboard.zig");
|
||||
const TerminalBuffer = @import("../TerminalBuffer.zig");
|
||||
const Position = @import("../Position.zig");
|
||||
const Widget = @import("../Widget.zig");
|
||||
|
||||
const DynamicString = std.ArrayListUnmanaged(u8);
|
||||
|
||||
const Text = @This();
|
||||
|
||||
allocator: Allocator,
|
||||
buffer: *TerminalBuffer,
|
||||
text: DynamicString,
|
||||
end: usize,
|
||||
cursor: usize,
|
||||
visible_start: usize,
|
||||
width: usize,
|
||||
component_pos: Position,
|
||||
children_pos: Position,
|
||||
masked: bool,
|
||||
maybe_mask: ?u32,
|
||||
fg: u32,
|
||||
bg: u32,
|
||||
|
||||
pub fn init(
|
||||
allocator: Allocator,
|
||||
buffer: *TerminalBuffer,
|
||||
masked: bool,
|
||||
maybe_mask: ?u32,
|
||||
width: usize,
|
||||
fg: u32,
|
||||
bg: u32,
|
||||
) Text {
|
||||
return .{
|
||||
.allocator = allocator,
|
||||
.buffer = buffer,
|
||||
.text = .empty,
|
||||
.end = 0,
|
||||
.cursor = 0,
|
||||
.visible_start = 0,
|
||||
.width = width,
|
||||
.component_pos = TerminalBuffer.START_POSITION,
|
||||
.children_pos = TerminalBuffer.START_POSITION,
|
||||
.masked = masked,
|
||||
.maybe_mask = maybe_mask,
|
||||
.fg = fg,
|
||||
.bg = bg,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: *Text) void {
|
||||
self.text.deinit(self.allocator);
|
||||
}
|
||||
|
||||
pub fn widget(self: *Text) Widget {
|
||||
return Widget.init(
|
||||
"Text",
|
||||
self,
|
||||
deinit,
|
||||
null,
|
||||
draw,
|
||||
null,
|
||||
handle,
|
||||
null,
|
||||
);
|
||||
}
|
||||
|
||||
pub fn positionX(self: *Text, original_pos: Position) void {
|
||||
self.component_pos = original_pos;
|
||||
self.children_pos = original_pos.addX(self.width);
|
||||
}
|
||||
|
||||
pub fn positionY(self: *Text, original_pos: Position) void {
|
||||
self.component_pos = original_pos;
|
||||
self.children_pos = original_pos.addY(1);
|
||||
}
|
||||
|
||||
pub fn positionXY(self: *Text, original_pos: Position) void {
|
||||
self.component_pos = original_pos;
|
||||
self.children_pos = Position.init(
|
||||
self.width,
|
||||
1,
|
||||
).add(original_pos);
|
||||
}
|
||||
|
||||
pub fn childrenPosition(self: Text) Position {
|
||||
return self.children_pos;
|
||||
}
|
||||
|
||||
pub fn clear(self: *Text) void {
|
||||
self.text.clearRetainingCapacity();
|
||||
self.end = 0;
|
||||
self.cursor = 0;
|
||||
self.visible_start = 0;
|
||||
}
|
||||
|
||||
pub fn toggleMask(self: *Text) void {
|
||||
self.masked = !self.masked;
|
||||
}
|
||||
|
||||
pub fn handle(self: *Text, maybe_key: ?keyboard.Key, insert_mode: bool) !void {
|
||||
if (maybe_key) |key| {
|
||||
if (key.left or (!insert_mode and (key.h or key.backspace))) {
|
||||
self.goLeft();
|
||||
} else if (key.right or (!insert_mode and key.l)) {
|
||||
self.goRight();
|
||||
} else if (key.delete) {
|
||||
self.delete();
|
||||
} else if (key.backspace) {
|
||||
self.backspace();
|
||||
} else if (insert_mode) {
|
||||
const maybe_character = key.getEnabledPrintableAscii();
|
||||
if (maybe_character) |character| try self.write(character);
|
||||
}
|
||||
}
|
||||
|
||||
if (self.masked and self.maybe_mask == null) {
|
||||
TerminalBuffer.setCursor(
|
||||
self.component_pos.x,
|
||||
self.component_pos.y,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
TerminalBuffer.setCursor(
|
||||
self.component_pos.x + (self.cursor - self.visible_start),
|
||||
self.component_pos.y,
|
||||
);
|
||||
}
|
||||
|
||||
fn draw(self: *Text) void {
|
||||
if (self.masked) {
|
||||
if (self.maybe_mask) |mask| {
|
||||
if (self.width < 1) return;
|
||||
|
||||
const length = @min(TerminalBuffer.strWidth(self.text.items), self.width - 1);
|
||||
if (length == 0) return;
|
||||
|
||||
TerminalBuffer.drawCharMultiple(
|
||||
mask,
|
||||
self.component_pos.x,
|
||||
self.component_pos.y,
|
||||
length,
|
||||
self.fg,
|
||||
self.bg,
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const str_length = TerminalBuffer.strWidth(self.text.items);
|
||||
const length = @min(str_length, self.width);
|
||||
if (length == 0) return;
|
||||
|
||||
const visible_slice = vs: {
|
||||
if (str_length > self.width and self.cursor < str_length) {
|
||||
break :vs self.text.items[self.visible_start..(self.width + self.visible_start)];
|
||||
} else {
|
||||
break :vs self.text.items[self.visible_start..];
|
||||
}
|
||||
};
|
||||
|
||||
TerminalBuffer.drawText(
|
||||
visible_slice,
|
||||
self.component_pos.x,
|
||||
self.component_pos.y,
|
||||
self.fg,
|
||||
self.bg,
|
||||
);
|
||||
}
|
||||
|
||||
fn goLeft(self: *Text) void {
|
||||
if (self.cursor == 0) return;
|
||||
if (self.visible_start > 0) self.visible_start -= 1;
|
||||
|
||||
self.cursor -= 1;
|
||||
}
|
||||
|
||||
fn goRight(self: *Text) void {
|
||||
if (self.cursor >= self.end) return;
|
||||
if (self.cursor - self.visible_start == self.width - 1) self.visible_start += 1;
|
||||
|
||||
self.cursor += 1;
|
||||
}
|
||||
|
||||
fn delete(self: *Text) void {
|
||||
if (self.cursor >= self.end) return;
|
||||
|
||||
_ = self.text.orderedRemove(self.cursor);
|
||||
|
||||
self.end -= 1;
|
||||
}
|
||||
|
||||
fn backspace(self: *Text) void {
|
||||
if (self.cursor == 0) return;
|
||||
|
||||
self.goLeft();
|
||||
self.delete();
|
||||
}
|
||||
|
||||
fn write(self: *Text, char: u8) !void {
|
||||
if (char == 0) return;
|
||||
|
||||
try self.text.insert(self.allocator, self.cursor, char);
|
||||
|
||||
self.end += 1;
|
||||
self.goRight();
|
||||
}
|
||||
141
ly-ui/src/components/UserList.zig
Normal file
141
ly-ui/src/components/UserList.zig
Normal file
@@ -0,0 +1,141 @@
|
||||
const std = @import("std");
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
const ly_core = @import("ly-core");
|
||||
const SavedUsers = ly_core.SavedUsers;
|
||||
|
||||
const keyboard = @import("../keyboard.zig");
|
||||
const TerminalBuffer = @import("../TerminalBuffer.zig");
|
||||
const Widget = @import("../Widget.zig");
|
||||
const generic = @import("generic.zig");
|
||||
const Session = @import("Session.zig");
|
||||
|
||||
const StringList = std.ArrayListUnmanaged([]const u8);
|
||||
pub const User = struct {
|
||||
name: []const u8,
|
||||
session_index: *usize,
|
||||
allocated_index: bool,
|
||||
first_run: bool,
|
||||
};
|
||||
const UserLabel = generic.CyclableLabel(User, *Session);
|
||||
|
||||
const UserList = @This();
|
||||
|
||||
label: UserLabel,
|
||||
|
||||
pub fn init(
|
||||
allocator: Allocator,
|
||||
buffer: *TerminalBuffer,
|
||||
usernames: StringList,
|
||||
// TODO: Remove dependency on SavedUsers
|
||||
saved_users: *SavedUsers,
|
||||
session: *Session,
|
||||
width: usize,
|
||||
text_in_center: bool,
|
||||
fg: u32,
|
||||
bg: u32,
|
||||
) !UserList {
|
||||
var user_list = UserList{
|
||||
.label = UserLabel.init(
|
||||
allocator,
|
||||
buffer,
|
||||
drawItem,
|
||||
usernameChanged,
|
||||
session,
|
||||
width,
|
||||
text_in_center,
|
||||
fg,
|
||||
bg,
|
||||
),
|
||||
};
|
||||
|
||||
for (usernames.items) |username| {
|
||||
if (username.len == 0) continue;
|
||||
|
||||
var maybe_session_index: ?*usize = null;
|
||||
var first_run = true;
|
||||
for (saved_users.user_list.items) |*saved_user| {
|
||||
if (std.mem.eql(u8, username, saved_user.username)) {
|
||||
maybe_session_index = &saved_user.session_index;
|
||||
first_run = saved_user.first_run;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
var allocated_index = false;
|
||||
if (maybe_session_index == null) {
|
||||
maybe_session_index = try allocator.create(usize);
|
||||
maybe_session_index.?.* = 0;
|
||||
allocated_index = true;
|
||||
}
|
||||
|
||||
try user_list.label.addItem(.{
|
||||
.name = username,
|
||||
.session_index = maybe_session_index.?,
|
||||
.allocated_index = allocated_index,
|
||||
.first_run = first_run,
|
||||
});
|
||||
}
|
||||
|
||||
return user_list;
|
||||
}
|
||||
|
||||
pub fn deinit(self: *UserList) void {
|
||||
for (self.label.list.items) |user| {
|
||||
if (user.allocated_index) {
|
||||
self.label.allocator.destroy(user.session_index);
|
||||
}
|
||||
}
|
||||
|
||||
self.label.deinit();
|
||||
}
|
||||
|
||||
pub fn widget(self: *UserList) Widget {
|
||||
return Widget.init(
|
||||
"UserList",
|
||||
self,
|
||||
deinit,
|
||||
null,
|
||||
draw,
|
||||
null,
|
||||
handle,
|
||||
null,
|
||||
);
|
||||
}
|
||||
|
||||
pub fn getCurrentUsername(self: UserList) []const u8 {
|
||||
return self.label.list.items[self.label.current].name;
|
||||
}
|
||||
|
||||
fn draw(self: *UserList) void {
|
||||
self.label.draw();
|
||||
}
|
||||
|
||||
fn handle(self: *UserList, maybe_key: ?keyboard.Key, insert_mode: bool) !void {
|
||||
self.label.handle(maybe_key, insert_mode);
|
||||
}
|
||||
|
||||
fn usernameChanged(user: User, maybe_session: ?*Session) void {
|
||||
if (maybe_session) |session| {
|
||||
session.label.current = @min(user.session_index.*, session.label.list.items.len - 1);
|
||||
}
|
||||
}
|
||||
|
||||
fn drawItem(label: *UserLabel, user: User, x: usize, y: usize, width: usize) void {
|
||||
if (width < 3) return;
|
||||
|
||||
const length = @min(TerminalBuffer.strWidth(user.name), width - 3);
|
||||
if (length == 0) return;
|
||||
|
||||
const x_offset = if (label.text_in_center and width >= length) (width - length) / 2 else 0;
|
||||
|
||||
label.cursor = length + x_offset;
|
||||
TerminalBuffer.drawConfinedText(
|
||||
user.name,
|
||||
x + x_offset,
|
||||
y,
|
||||
width,
|
||||
label.fg,
|
||||
label.bg,
|
||||
);
|
||||
}
|
||||
114
ly-ui/src/components/bigLabelLocales/en.zig
Normal file
114
ly-ui/src/components/bigLabelLocales/en.zig
Normal file
@@ -0,0 +1,114 @@
|
||||
const BigLabel = @import("../BigLabel.zig");
|
||||
const LocaleChars = BigLabel.LocaleChars;
|
||||
const X = BigLabel.X;
|
||||
const O = BigLabel.O;
|
||||
|
||||
// zig fmt: off
|
||||
pub const locale_chars = LocaleChars{
|
||||
.ZERO = [_]u21{
|
||||
X,X,X,X,X,
|
||||
X,X,O,X,X,
|
||||
X,X,O,X,X,
|
||||
X,X,O,X,X,
|
||||
X,X,X,X,X,
|
||||
},
|
||||
.ONE = [_]u21{
|
||||
O,O,O,X,X,
|
||||
O,O,O,X,X,
|
||||
O,O,O,X,X,
|
||||
O,O,O,X,X,
|
||||
O,O,O,X,X,
|
||||
},
|
||||
.TWO = [_]u21{
|
||||
X,X,X,X,X,
|
||||
O,O,O,X,X,
|
||||
X,X,X,X,X,
|
||||
X,X,O,O,O,
|
||||
X,X,X,X,X,
|
||||
},
|
||||
.THREE = [_]u21{
|
||||
X,X,X,X,X,
|
||||
O,O,O,X,X,
|
||||
X,X,X,X,X,
|
||||
O,O,O,X,X,
|
||||
X,X,X,X,X,
|
||||
},
|
||||
.FOUR = [_]u21{
|
||||
X,X,O,X,X,
|
||||
X,X,O,X,X,
|
||||
X,X,X,X,X,
|
||||
O,O,O,X,X,
|
||||
O,O,O,X,X,
|
||||
},
|
||||
.FIVE = [_]u21{
|
||||
X,X,X,X,X,
|
||||
X,X,O,O,O,
|
||||
X,X,X,X,X,
|
||||
O,O,O,X,X,
|
||||
X,X,X,X,X,
|
||||
},
|
||||
.SIX = [_]u21{
|
||||
X,X,X,X,X,
|
||||
X,X,O,O,O,
|
||||
X,X,X,X,X,
|
||||
X,X,O,X,X,
|
||||
X,X,X,X,X,
|
||||
},
|
||||
.SEVEN = [_]u21{
|
||||
X,X,X,X,X,
|
||||
O,O,O,X,X,
|
||||
O,O,O,X,X,
|
||||
O,O,O,X,X,
|
||||
O,O,O,X,X,
|
||||
},
|
||||
.EIGHT = [_]u21{
|
||||
X,X,X,X,X,
|
||||
X,X,O,X,X,
|
||||
X,X,X,X,X,
|
||||
X,X,O,X,X,
|
||||
X,X,X,X,X,
|
||||
},
|
||||
.NINE = [_]u21{
|
||||
X,X,X,X,X,
|
||||
X,X,O,X,X,
|
||||
X,X,X,X,X,
|
||||
O,O,O,X,X,
|
||||
X,X,X,X,X,
|
||||
},
|
||||
.S = [_]u21{
|
||||
O,O,O,O,O,
|
||||
O,O,X,O,O,
|
||||
O,O,O,O,O,
|
||||
O,O,X,O,O,
|
||||
O,O,O,O,O,
|
||||
},
|
||||
.E = [_]u21{
|
||||
O,O,O,O,O,
|
||||
O,O,O,O,O,
|
||||
O,O,O,O,O,
|
||||
O,O,O,O,O,
|
||||
O,O,O,O,O,
|
||||
},
|
||||
.P = [_]u21{
|
||||
X,X,X,X,X,
|
||||
X,X,O,X,X,
|
||||
X,X,X,X,X,
|
||||
X,X,O,O,O,
|
||||
X,X,O,O,O,
|
||||
},
|
||||
.A = [_]u21{
|
||||
X,X,X,X,X,
|
||||
X,X,O,X,X,
|
||||
X,X,X,X,X,
|
||||
X,X,O,X,X,
|
||||
X,X,O,X,X,
|
||||
},
|
||||
.M = [_]u21{
|
||||
X,X,X,X,X,
|
||||
X,O,X,O,X,
|
||||
X,O,X,O,X,
|
||||
X,O,O,O,X,
|
||||
X,O,O,O,X,
|
||||
},
|
||||
};
|
||||
// zig fmt: on
|
||||
114
ly-ui/src/components/bigLabelLocales/fa.zig
Normal file
114
ly-ui/src/components/bigLabelLocales/fa.zig
Normal file
@@ -0,0 +1,114 @@
|
||||
const BigLabel = @import("../BigLabel.zig");
|
||||
const LocaleChars = BigLabel.LocaleChars;
|
||||
const X = BigLabel.X;
|
||||
const O = BigLabel.O;
|
||||
|
||||
// zig fmt: off
|
||||
pub const locale_chars = LocaleChars{
|
||||
.ZERO = [_]u21{
|
||||
O,O,O,O,O,
|
||||
O,O,X,O,O,
|
||||
O,X,O,X,O,
|
||||
O,O,X,O,O,
|
||||
O,O,O,O,O,
|
||||
},
|
||||
.ONE = [_]u21{
|
||||
O,O,X,O,O,
|
||||
O,X,X,O,O,
|
||||
O,O,X,O,O,
|
||||
O,O,X,O,O,
|
||||
O,O,X,O,O,
|
||||
},
|
||||
.TWO = [_]u21{
|
||||
O,X,O,X,O,
|
||||
O,X,X,X,O,
|
||||
O,X,O,O,O,
|
||||
O,X,O,O,O,
|
||||
O,X,O,O,O,
|
||||
},
|
||||
.THREE = [_]u21{
|
||||
X,O,X,O,X,
|
||||
X,X,X,X,X,
|
||||
X,O,O,O,O,
|
||||
X,O,O,O,O,
|
||||
X,O,O,O,O,
|
||||
},
|
||||
.FOUR = [_]u21{
|
||||
O,X,O,X,X,
|
||||
O,X,X,O,O,
|
||||
O,X,X,X,X,
|
||||
O,X,O,O,O,
|
||||
O,X,O,O,O,
|
||||
},
|
||||
.FIVE = [_]u21{
|
||||
O,O,X,X,O,
|
||||
O,X,O,O,X,
|
||||
X,O,O,O,X,
|
||||
X,O,X,O,X,
|
||||
O,X,O,X,O,
|
||||
},
|
||||
.SIX = [_]u21{
|
||||
O,X,X,O,O,
|
||||
O,X,O,O,X,
|
||||
O,O,X,O,O,
|
||||
O,X,O,O,O,
|
||||
X,O,O,O,O,
|
||||
},
|
||||
.SEVEN = [_]u21{
|
||||
X,O,O,O,X,
|
||||
X,O,O,O,X,
|
||||
O,X,O,X,O,
|
||||
O,X,O,X,O,
|
||||
O,O,X,O,O,
|
||||
},
|
||||
.EIGHT = [_]u21{
|
||||
O,O,O,X,O,
|
||||
O,O,X,O,X,
|
||||
O,O,X,O,X,
|
||||
O,X,O,O,X,
|
||||
O,X,O,O,X,
|
||||
},
|
||||
.NINE = [_]u21{
|
||||
O,X,X,X,O,
|
||||
O,X,O,X,O,
|
||||
O,X,X,X,O,
|
||||
O,O,O,X,O,
|
||||
O,O,O,X,O,
|
||||
},
|
||||
.P = [_]u21{
|
||||
O,O,O,O,O,
|
||||
O,O,O,O,O,
|
||||
O,O,O,O,O,
|
||||
O,O,O,O,O,
|
||||
O,O,O,O,O,
|
||||
},
|
||||
.A = [_]u21{
|
||||
O,O,O,O,O,
|
||||
O,O,O,O,O,
|
||||
O,O,O,O,O,
|
||||
O,O,O,O,O,
|
||||
O,O,O,O,O,
|
||||
},
|
||||
.M = [_]u21{
|
||||
O,O,O,O,O,
|
||||
O,O,O,O,O,
|
||||
O,O,O,O,O,
|
||||
O,O,O,O,O,
|
||||
O,O,O,O,O,
|
||||
},
|
||||
.S = [_]u21{
|
||||
O,O,O,O,O,
|
||||
O,O,X,O,O,
|
||||
O,O,O,O,O,
|
||||
O,O,X,O,O,
|
||||
O,O,O,O,O,
|
||||
},
|
||||
.E = [_]u21{
|
||||
O,O,O,O,O,
|
||||
O,O,O,O,O,
|
||||
O,O,O,O,O,
|
||||
O,O,O,O,O,
|
||||
O,O,O,O,O,
|
||||
},
|
||||
};
|
||||
// zig fmt: on
|
||||
159
ly-ui/src/components/generic.zig
Normal file
159
ly-ui/src/components/generic.zig
Normal file
@@ -0,0 +1,159 @@
|
||||
const std = @import("std");
|
||||
|
||||
const Cell = @import("../Cell.zig");
|
||||
const keyboard = @import("../keyboard.zig");
|
||||
const TerminalBuffer = @import("../TerminalBuffer.zig");
|
||||
const Position = @import("../Position.zig");
|
||||
|
||||
pub fn CyclableLabel(comptime ItemType: type, comptime ChangeItemType: type) type {
|
||||
return struct {
|
||||
const Allocator = std.mem.Allocator;
|
||||
const ItemList = std.ArrayListUnmanaged(ItemType);
|
||||
const DrawItemFn = *const fn (*Self, ItemType, usize, usize, usize) void;
|
||||
const ChangeItemFn = *const fn (ItemType, ?ChangeItemType) void;
|
||||
|
||||
const Self = @This();
|
||||
|
||||
allocator: Allocator,
|
||||
buffer: *TerminalBuffer,
|
||||
list: ItemList,
|
||||
current: usize,
|
||||
width: usize,
|
||||
component_pos: Position,
|
||||
children_pos: Position,
|
||||
text_in_center: bool,
|
||||
fg: u32,
|
||||
bg: u32,
|
||||
cursor: usize,
|
||||
draw_item_fn: DrawItemFn,
|
||||
change_item_fn: ?ChangeItemFn,
|
||||
change_item_arg: ?ChangeItemType,
|
||||
|
||||
pub fn init(
|
||||
allocator: Allocator,
|
||||
buffer: *TerminalBuffer,
|
||||
draw_item_fn: DrawItemFn,
|
||||
change_item_fn: ?ChangeItemFn,
|
||||
change_item_arg: ?ChangeItemType,
|
||||
width: usize,
|
||||
text_in_center: bool,
|
||||
fg: u32,
|
||||
bg: u32,
|
||||
) Self {
|
||||
return .{
|
||||
.allocator = allocator,
|
||||
.buffer = buffer,
|
||||
.list = .empty,
|
||||
.current = 0,
|
||||
.width = width,
|
||||
.component_pos = TerminalBuffer.START_POSITION,
|
||||
.children_pos = TerminalBuffer.START_POSITION,
|
||||
.text_in_center = text_in_center,
|
||||
.fg = fg,
|
||||
.bg = bg,
|
||||
.cursor = 0,
|
||||
.draw_item_fn = draw_item_fn,
|
||||
.change_item_fn = change_item_fn,
|
||||
.change_item_arg = change_item_arg,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: *Self) void {
|
||||
self.list.deinit(self.allocator);
|
||||
}
|
||||
|
||||
pub fn positionX(self: *Self, original_pos: Position) void {
|
||||
self.component_pos = original_pos;
|
||||
self.cursor = self.component_pos.x + 2;
|
||||
self.children_pos = original_pos.addX(self.width);
|
||||
}
|
||||
|
||||
pub fn positionY(self: *Self, original_pos: Position) void {
|
||||
self.component_pos = original_pos;
|
||||
self.cursor = self.component_pos.x + 2;
|
||||
self.children_pos = original_pos.addY(1);
|
||||
}
|
||||
|
||||
pub fn positionXY(self: *Self, original_pos: Position) void {
|
||||
self.component_pos = original_pos;
|
||||
self.cursor = self.component_pos.x + 2;
|
||||
self.children_pos = Position.init(
|
||||
self.width,
|
||||
1,
|
||||
).add(original_pos);
|
||||
}
|
||||
|
||||
pub fn childrenPosition(self: Self) Position {
|
||||
return self.children_pos;
|
||||
}
|
||||
|
||||
pub fn addItem(self: *Self, item: ItemType) !void {
|
||||
try self.list.append(self.allocator, item);
|
||||
self.current = self.list.items.len - 1;
|
||||
}
|
||||
|
||||
pub fn handle(self: *Self, maybe_key: ?keyboard.Key, insert_mode: bool) void {
|
||||
if (maybe_key) |key| {
|
||||
if (key.left or (key.ctrl and key.h) or (!insert_mode and key.h)) {
|
||||
self.goLeft();
|
||||
} else if (key.right or (key.ctrl and key.l) or (!insert_mode and key.l)) {
|
||||
self.goRight();
|
||||
}
|
||||
}
|
||||
|
||||
TerminalBuffer.setCursor(
|
||||
self.component_pos.x + self.cursor + 2,
|
||||
self.component_pos.y,
|
||||
);
|
||||
}
|
||||
|
||||
pub fn draw(self: *Self) void {
|
||||
if (self.list.items.len == 0) return;
|
||||
if (self.width < 2) return;
|
||||
|
||||
var left_arrow = Cell.init('<', self.fg, self.bg);
|
||||
var right_arrow = Cell.init('>', self.fg, self.bg);
|
||||
|
||||
left_arrow.put(self.component_pos.x, self.component_pos.y);
|
||||
right_arrow.put(
|
||||
self.component_pos.x + self.width - 1,
|
||||
self.component_pos.y,
|
||||
);
|
||||
|
||||
const current_item = self.list.items[self.current];
|
||||
const x = self.component_pos.x + 2;
|
||||
const y = self.component_pos.y;
|
||||
const width = self.width - 2;
|
||||
|
||||
@call(
|
||||
.auto,
|
||||
self.draw_item_fn,
|
||||
.{ self, current_item, x, y, width },
|
||||
);
|
||||
}
|
||||
|
||||
fn goLeft(self: *Self) void {
|
||||
self.current = if (self.current == 0) self.list.items.len - 1 else self.current - 1;
|
||||
|
||||
if (self.change_item_fn) |change_item_fn| {
|
||||
@call(
|
||||
.auto,
|
||||
change_item_fn,
|
||||
.{ self.list.items[self.current], self.change_item_arg },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn goRight(self: *Self) void {
|
||||
self.current = if (self.current == self.list.items.len - 1) 0 else self.current + 1;
|
||||
|
||||
if (self.change_item_fn) |change_item_fn| {
|
||||
@call(
|
||||
.auto,
|
||||
change_item_fn,
|
||||
.{ self.list.items[self.current], self.change_item_arg },
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
708
ly-ui/src/keyboard.zig
Normal file
708
ly-ui/src/keyboard.zig
Normal file
@@ -0,0 +1,708 @@
|
||||
const std = @import("std");
|
||||
const Allocator = std.mem.Allocator;
|
||||
const KeyList = std.ArrayList(Key);
|
||||
|
||||
const TerminalBuffer = @import("TerminalBuffer.zig");
|
||||
const termbox = TerminalBuffer.termbox;
|
||||
|
||||
pub const Key = packed struct {
|
||||
ctrl: bool,
|
||||
shift: bool,
|
||||
alt: bool,
|
||||
|
||||
f1: bool,
|
||||
f2: bool,
|
||||
f3: bool,
|
||||
f4: bool,
|
||||
f5: bool,
|
||||
f6: bool,
|
||||
f7: bool,
|
||||
f8: bool,
|
||||
f9: bool,
|
||||
f10: bool,
|
||||
f11: bool,
|
||||
f12: bool,
|
||||
|
||||
insert: bool,
|
||||
delete: bool,
|
||||
home: bool,
|
||||
end: bool,
|
||||
pageup: bool,
|
||||
pagedown: bool,
|
||||
up: bool,
|
||||
down: bool,
|
||||
left: bool,
|
||||
right: bool,
|
||||
tab: bool,
|
||||
backspace: bool,
|
||||
enter: bool,
|
||||
|
||||
@" ": bool,
|
||||
@"!": bool,
|
||||
@"`": bool,
|
||||
esc: bool,
|
||||
@"[": bool,
|
||||
@"\\": bool,
|
||||
@"]": bool,
|
||||
@"/": bool,
|
||||
_: bool,
|
||||
@"'": bool,
|
||||
@"\"": bool,
|
||||
@",": bool,
|
||||
@"-": bool,
|
||||
@".": bool,
|
||||
@"#": bool,
|
||||
@"$": bool,
|
||||
@"%": bool,
|
||||
@"&": bool,
|
||||
@"*": bool,
|
||||
@"(": bool,
|
||||
@")": bool,
|
||||
@"+": bool,
|
||||
@"=": bool,
|
||||
@":": bool,
|
||||
@";": bool,
|
||||
@"<": bool,
|
||||
@">": bool,
|
||||
@"?": bool,
|
||||
@"@": bool,
|
||||
@"^": bool,
|
||||
@"~": bool,
|
||||
@"{": bool,
|
||||
@"}": bool,
|
||||
@"|": bool,
|
||||
|
||||
@"0": bool,
|
||||
@"1": bool,
|
||||
@"2": bool,
|
||||
@"3": bool,
|
||||
@"4": bool,
|
||||
@"5": bool,
|
||||
@"6": bool,
|
||||
@"7": bool,
|
||||
@"8": bool,
|
||||
@"9": bool,
|
||||
|
||||
a: bool,
|
||||
b: bool,
|
||||
c: bool,
|
||||
d: bool,
|
||||
e: bool,
|
||||
f: bool,
|
||||
g: bool,
|
||||
h: bool,
|
||||
i: bool,
|
||||
j: bool,
|
||||
k: bool,
|
||||
l: bool,
|
||||
m: bool,
|
||||
n: bool,
|
||||
o: bool,
|
||||
p: bool,
|
||||
q: bool,
|
||||
r: bool,
|
||||
s: bool,
|
||||
t: bool,
|
||||
u: bool,
|
||||
v: bool,
|
||||
w: bool,
|
||||
x: bool,
|
||||
y: bool,
|
||||
z: bool,
|
||||
|
||||
pub fn getEnabledPrintableAscii(self: Key) ?u8 {
|
||||
if (self.ctrl or self.alt) return null;
|
||||
|
||||
inline for (std.meta.fields(Key)) |field| {
|
||||
if (field.name.len == 1 and std.ascii.isPrint(field.name[0]) and @field(self, field.name)) {
|
||||
if (self.shift) {
|
||||
if (!std.ascii.isAlphanumeric(field.name[0])) return null;
|
||||
return std.ascii.toUpper(field.name[0]);
|
||||
}
|
||||
|
||||
return field.name[0];
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
pub fn getKeyList(allocator: Allocator, tb_event: termbox.tb_event) !KeyList {
|
||||
var keys: KeyList = .empty;
|
||||
var key = std.mem.zeroes(Key);
|
||||
|
||||
if (tb_event.mod & termbox.TB_MOD_CTRL != 0) key.ctrl = true;
|
||||
if (tb_event.mod & termbox.TB_MOD_SHIFT != 0) key.shift = true;
|
||||
if (tb_event.mod & termbox.TB_MOD_ALT != 0) key.alt = true;
|
||||
|
||||
if (tb_event.key == termbox.TB_KEY_BACK_TAB) {
|
||||
key.shift = true;
|
||||
key.tab = true;
|
||||
} else if (tb_event.key > termbox.TB_KEY_BACK_TAB) {
|
||||
const code = 0xFFFF - tb_event.key;
|
||||
|
||||
switch (code) {
|
||||
0 => key.f1 = true,
|
||||
1 => key.f2 = true,
|
||||
2 => key.f3 = true,
|
||||
3 => key.f4 = true,
|
||||
4 => key.f5 = true,
|
||||
5 => key.f6 = true,
|
||||
6 => key.f7 = true,
|
||||
7 => key.f8 = true,
|
||||
8 => key.f9 = true,
|
||||
9 => key.f10 = true,
|
||||
10 => key.f11 = true,
|
||||
11 => key.f12 = true,
|
||||
12 => key.insert = true,
|
||||
13 => key.delete = true,
|
||||
14 => key.home = true,
|
||||
15 => key.end = true,
|
||||
16 => key.pageup = true,
|
||||
17 => key.pagedown = true,
|
||||
18 => key.up = true,
|
||||
19 => key.down = true,
|
||||
20 => key.left = true,
|
||||
21 => key.right = true,
|
||||
else => {},
|
||||
}
|
||||
} else if (tb_event.ch < 128) {
|
||||
const code = if (tb_event.ch == 0 and tb_event.key < 128) tb_event.key else tb_event.ch;
|
||||
|
||||
switch (code) {
|
||||
0 => {
|
||||
key.ctrl = true;
|
||||
key.@"2" = true;
|
||||
try keys.append(allocator, key);
|
||||
|
||||
key = std.mem.zeroes(Key);
|
||||
key.@"`" = true;
|
||||
},
|
||||
1 => {
|
||||
key.ctrl = true;
|
||||
key.a = true;
|
||||
},
|
||||
2 => {
|
||||
key.ctrl = true;
|
||||
key.b = true;
|
||||
},
|
||||
3 => {
|
||||
key.ctrl = true;
|
||||
key.c = true;
|
||||
},
|
||||
4 => {
|
||||
key.ctrl = true;
|
||||
key.d = true;
|
||||
},
|
||||
5 => {
|
||||
key.ctrl = true;
|
||||
key.e = true;
|
||||
},
|
||||
6 => {
|
||||
key.ctrl = true;
|
||||
key.f = true;
|
||||
},
|
||||
7 => {
|
||||
key.ctrl = true;
|
||||
key.g = true;
|
||||
},
|
||||
8 => {
|
||||
key.ctrl = true;
|
||||
key.h = true;
|
||||
try keys.append(allocator, key);
|
||||
|
||||
key = std.mem.zeroes(Key);
|
||||
key.backspace = true;
|
||||
},
|
||||
9 => {
|
||||
key.ctrl = true;
|
||||
key.i = true;
|
||||
try keys.append(allocator, key);
|
||||
|
||||
key = std.mem.zeroes(Key);
|
||||
key.tab = true;
|
||||
},
|
||||
10 => {
|
||||
key.ctrl = true;
|
||||
key.j = true;
|
||||
},
|
||||
11 => {
|
||||
key.ctrl = true;
|
||||
key.k = true;
|
||||
},
|
||||
12 => {
|
||||
key.ctrl = true;
|
||||
key.l = true;
|
||||
},
|
||||
13 => {
|
||||
key.ctrl = true;
|
||||
key.m = true;
|
||||
try keys.append(allocator, key);
|
||||
|
||||
key = std.mem.zeroes(Key);
|
||||
key.enter = true;
|
||||
},
|
||||
14 => {
|
||||
key.ctrl = true;
|
||||
key.n = true;
|
||||
},
|
||||
15 => {
|
||||
key.ctrl = true;
|
||||
key.o = true;
|
||||
},
|
||||
16 => {
|
||||
key.ctrl = true;
|
||||
key.p = true;
|
||||
},
|
||||
17 => {
|
||||
key.ctrl = true;
|
||||
key.q = true;
|
||||
},
|
||||
18 => {
|
||||
key.ctrl = true;
|
||||
key.r = true;
|
||||
},
|
||||
19 => {
|
||||
key.ctrl = true;
|
||||
key.s = true;
|
||||
},
|
||||
20 => {
|
||||
key.ctrl = true;
|
||||
key.t = true;
|
||||
},
|
||||
21 => {
|
||||
key.ctrl = true;
|
||||
key.u = true;
|
||||
},
|
||||
22 => {
|
||||
key.ctrl = true;
|
||||
key.v = true;
|
||||
},
|
||||
23 => {
|
||||
key.ctrl = true;
|
||||
key.w = true;
|
||||
},
|
||||
24 => {
|
||||
key.ctrl = true;
|
||||
key.x = true;
|
||||
},
|
||||
25 => {
|
||||
key.ctrl = true;
|
||||
key.y = true;
|
||||
},
|
||||
26 => {
|
||||
key.ctrl = true;
|
||||
key.z = true;
|
||||
},
|
||||
27 => {
|
||||
key.ctrl = true;
|
||||
key.@"3" = true;
|
||||
try keys.append(allocator, key);
|
||||
|
||||
key = std.mem.zeroes(Key);
|
||||
key.esc = true;
|
||||
try keys.append(allocator, key);
|
||||
|
||||
key = std.mem.zeroes(Key);
|
||||
key.@"[" = true;
|
||||
},
|
||||
28 => {
|
||||
key.ctrl = true;
|
||||
key.@"4" = true;
|
||||
try keys.append(allocator, key);
|
||||
|
||||
key = std.mem.zeroes(Key);
|
||||
key.@"\\" = true;
|
||||
},
|
||||
29 => {
|
||||
key.ctrl = true;
|
||||
key.@"5" = true;
|
||||
try keys.append(allocator, key);
|
||||
|
||||
key = std.mem.zeroes(Key);
|
||||
key.@"]" = true;
|
||||
},
|
||||
30 => {
|
||||
key.ctrl = true;
|
||||
try keys.append(allocator, key);
|
||||
|
||||
key = std.mem.zeroes(Key);
|
||||
key.@"6" = true;
|
||||
},
|
||||
31 => {
|
||||
key.ctrl = true;
|
||||
key.@"7" = true;
|
||||
try keys.append(allocator, key);
|
||||
|
||||
key = std.mem.zeroes(Key);
|
||||
key.@"/" = true;
|
||||
try keys.append(allocator, key);
|
||||
|
||||
key = std.mem.zeroes(Key);
|
||||
key._ = true;
|
||||
},
|
||||
32 => {
|
||||
key.@" " = true;
|
||||
},
|
||||
33 => {
|
||||
key = std.mem.zeroes(Key);
|
||||
key.@"!" = true;
|
||||
},
|
||||
34 => {
|
||||
key = std.mem.zeroes(Key);
|
||||
key.@"\"" = true;
|
||||
},
|
||||
35 => {
|
||||
key = std.mem.zeroes(Key);
|
||||
key.@"#" = true;
|
||||
},
|
||||
36 => {
|
||||
key = std.mem.zeroes(Key);
|
||||
key.@"$" = true;
|
||||
},
|
||||
37 => {
|
||||
key = std.mem.zeroes(Key);
|
||||
key.@"%" = true;
|
||||
},
|
||||
38 => {
|
||||
key = std.mem.zeroes(Key);
|
||||
key.@"&" = true;
|
||||
},
|
||||
39 => {
|
||||
key.@"'" = true;
|
||||
},
|
||||
40 => {
|
||||
key = std.mem.zeroes(Key);
|
||||
key.@"(" = true;
|
||||
},
|
||||
41 => {
|
||||
key = std.mem.zeroes(Key);
|
||||
key.@")" = true;
|
||||
},
|
||||
42 => {
|
||||
key = std.mem.zeroes(Key);
|
||||
key.@"*" = true;
|
||||
},
|
||||
43 => {
|
||||
key = std.mem.zeroes(Key);
|
||||
key.@"+" = true;
|
||||
},
|
||||
44 => {
|
||||
key.@"," = true;
|
||||
},
|
||||
45 => {
|
||||
key.@"-" = true;
|
||||
},
|
||||
46 => {
|
||||
key.@"." = true;
|
||||
},
|
||||
47 => {
|
||||
key.@"/" = true;
|
||||
},
|
||||
48 => {
|
||||
key.@"0" = true;
|
||||
},
|
||||
49 => {
|
||||
key.@"1" = true;
|
||||
},
|
||||
50 => {
|
||||
key.@"2" = true;
|
||||
},
|
||||
51 => {
|
||||
key.@"3" = true;
|
||||
},
|
||||
52 => {
|
||||
key.@"4" = true;
|
||||
},
|
||||
53 => {
|
||||
key.@"5" = true;
|
||||
},
|
||||
54 => {
|
||||
key.@"6" = true;
|
||||
},
|
||||
55 => {
|
||||
key.@"7" = true;
|
||||
},
|
||||
56 => {
|
||||
key.@"8" = true;
|
||||
},
|
||||
57 => {
|
||||
key.@"9" = true;
|
||||
},
|
||||
58 => {
|
||||
key.shift = true;
|
||||
key.@":" = true;
|
||||
},
|
||||
59 => {
|
||||
key.@";" = true;
|
||||
},
|
||||
60 => {
|
||||
key.shift = true;
|
||||
key.@"<" = true;
|
||||
},
|
||||
61 => {
|
||||
key.@"=" = true;
|
||||
},
|
||||
62 => {
|
||||
key.shift = true;
|
||||
key.@">" = true;
|
||||
},
|
||||
63 => {
|
||||
key.shift = true;
|
||||
key.@"?" = true;
|
||||
},
|
||||
64 => {
|
||||
key.shift = true;
|
||||
key.@"2" = true;
|
||||
try keys.append(allocator, key);
|
||||
|
||||
key = std.mem.zeroes(Key);
|
||||
key.@"@" = true;
|
||||
},
|
||||
65 => {
|
||||
key.shift = true;
|
||||
key.a = true;
|
||||
},
|
||||
66 => {
|
||||
key.shift = true;
|
||||
key.b = true;
|
||||
},
|
||||
67 => {
|
||||
key.shift = true;
|
||||
key.c = true;
|
||||
},
|
||||
68 => {
|
||||
key.shift = true;
|
||||
key.d = true;
|
||||
},
|
||||
69 => {
|
||||
key.shift = true;
|
||||
key.e = true;
|
||||
},
|
||||
70 => {
|
||||
key.shift = true;
|
||||
key.f = true;
|
||||
},
|
||||
71 => {
|
||||
key.shift = true;
|
||||
key.g = true;
|
||||
},
|
||||
72 => {
|
||||
key.shift = true;
|
||||
key.h = true;
|
||||
},
|
||||
73 => {
|
||||
key.shift = true;
|
||||
key.i = true;
|
||||
},
|
||||
74 => {
|
||||
key.shift = true;
|
||||
key.j = true;
|
||||
},
|
||||
75 => {
|
||||
key.shift = true;
|
||||
key.k = true;
|
||||
},
|
||||
76 => {
|
||||
key.shift = true;
|
||||
key.l = true;
|
||||
},
|
||||
77 => {
|
||||
key.shift = true;
|
||||
key.m = true;
|
||||
},
|
||||
78 => {
|
||||
key.shift = true;
|
||||
key.n = true;
|
||||
},
|
||||
79 => {
|
||||
key.shift = true;
|
||||
key.o = true;
|
||||
},
|
||||
80 => {
|
||||
key.shift = true;
|
||||
key.p = true;
|
||||
},
|
||||
81 => {
|
||||
key.shift = true;
|
||||
key.q = true;
|
||||
},
|
||||
82 => {
|
||||
key.shift = true;
|
||||
key.r = true;
|
||||
},
|
||||
83 => {
|
||||
key.shift = true;
|
||||
key.s = true;
|
||||
},
|
||||
84 => {
|
||||
key.shift = true;
|
||||
key.t = true;
|
||||
},
|
||||
85 => {
|
||||
key.shift = true;
|
||||
key.u = true;
|
||||
},
|
||||
86 => {
|
||||
key.shift = true;
|
||||
key.v = true;
|
||||
},
|
||||
87 => {
|
||||
key.shift = true;
|
||||
key.w = true;
|
||||
},
|
||||
88 => {
|
||||
key.shift = true;
|
||||
key.x = true;
|
||||
},
|
||||
89 => {
|
||||
key.shift = true;
|
||||
key.y = true;
|
||||
},
|
||||
90 => {
|
||||
key.shift = true;
|
||||
key.z = true;
|
||||
},
|
||||
91 => {
|
||||
key.@"[" = true;
|
||||
},
|
||||
92 => {
|
||||
key.@"\\" = true;
|
||||
},
|
||||
93 => {
|
||||
key.@"]" = true;
|
||||
},
|
||||
94 => {
|
||||
key = std.mem.zeroes(Key);
|
||||
key.@"^" = true;
|
||||
},
|
||||
95 => {
|
||||
key.shift = true;
|
||||
key.@"-" = true;
|
||||
try keys.append(allocator, key);
|
||||
|
||||
key = std.mem.zeroes(Key);
|
||||
key._ = true;
|
||||
},
|
||||
96 => {
|
||||
key.@"`" = true;
|
||||
},
|
||||
97 => {
|
||||
key.a = true;
|
||||
},
|
||||
98 => {
|
||||
key.b = true;
|
||||
},
|
||||
99 => {
|
||||
key.c = true;
|
||||
},
|
||||
100 => {
|
||||
key.d = true;
|
||||
},
|
||||
101 => {
|
||||
key.e = true;
|
||||
},
|
||||
102 => {
|
||||
key.f = true;
|
||||
},
|
||||
103 => {
|
||||
key.g = true;
|
||||
},
|
||||
104 => {
|
||||
key.h = true;
|
||||
},
|
||||
105 => {
|
||||
key.i = true;
|
||||
},
|
||||
106 => {
|
||||
key.j = true;
|
||||
},
|
||||
107 => {
|
||||
key.k = true;
|
||||
},
|
||||
108 => {
|
||||
key.l = true;
|
||||
},
|
||||
109 => {
|
||||
key.m = true;
|
||||
},
|
||||
110 => {
|
||||
key.n = true;
|
||||
},
|
||||
111 => {
|
||||
key.o = true;
|
||||
},
|
||||
112 => {
|
||||
key.p = true;
|
||||
},
|
||||
113 => {
|
||||
key.q = true;
|
||||
},
|
||||
114 => {
|
||||
key.r = true;
|
||||
},
|
||||
115 => {
|
||||
key.s = true;
|
||||
},
|
||||
116 => {
|
||||
key.t = true;
|
||||
},
|
||||
117 => {
|
||||
key.u = true;
|
||||
},
|
||||
118 => {
|
||||
key.v = true;
|
||||
},
|
||||
119 => {
|
||||
key.w = true;
|
||||
},
|
||||
120 => {
|
||||
key.x = true;
|
||||
},
|
||||
121 => {
|
||||
key.y = true;
|
||||
},
|
||||
122 => {
|
||||
key.z = true;
|
||||
},
|
||||
123 => {
|
||||
key.shift = true;
|
||||
key.@"{" = true;
|
||||
},
|
||||
124 => {
|
||||
key.shift = true;
|
||||
key.@"\\" = true;
|
||||
try keys.append(allocator, key);
|
||||
|
||||
key = std.mem.zeroes(Key);
|
||||
key.@"|" = true;
|
||||
},
|
||||
125 => {
|
||||
key.shift = true;
|
||||
key.@"}" = true;
|
||||
},
|
||||
126 => {
|
||||
key.shift = true;
|
||||
key.@"`" = true;
|
||||
try keys.append(allocator, key);
|
||||
|
||||
key = std.mem.zeroes(Key);
|
||||
key.@"~" = true;
|
||||
},
|
||||
127 => {
|
||||
key.ctrl = true;
|
||||
key.@"8" = true;
|
||||
try keys.append(allocator, key);
|
||||
|
||||
key = std.mem.zeroes(Key);
|
||||
key.backspace = true;
|
||||
},
|
||||
else => {},
|
||||
}
|
||||
}
|
||||
|
||||
try keys.append(allocator, key);
|
||||
|
||||
return keys;
|
||||
}
|
||||
16
ly-ui/src/root.zig
Normal file
16
ly-ui/src/root.zig
Normal file
@@ -0,0 +1,16 @@
|
||||
pub const ly_core = @import("ly-core");
|
||||
|
||||
pub const Cell = @import("Cell.zig");
|
||||
pub const keyboard = @import("keyboard.zig");
|
||||
pub const Position = @import("Position.zig");
|
||||
pub const TerminalBuffer = @import("TerminalBuffer.zig");
|
||||
pub const Widget = @import("Widget.zig");
|
||||
|
||||
pub const BigLabel = @import("components/BigLabel.zig");
|
||||
pub const CenteredBox = @import("components/CenteredBox.zig");
|
||||
pub const CyclableLabel = @import("components/generic.zig").CyclableLabel;
|
||||
pub const InfoLine = @import("components/InfoLine.zig");
|
||||
pub const Label = @import("components/Label.zig");
|
||||
pub const Session = @import("components/Session.zig");
|
||||
pub const Text = @import("components/Text.zig");
|
||||
pub const UserList = @import("components/UserList.zig");
|
||||
Reference in New Issue
Block a user