From f22593f828bda35854545207fbc5a1f771ba8bb1 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Sun, 8 Feb 2026 17:40:50 +0100 Subject: [PATCH] Add Label component & make colors custom This commit also makes Ly more resilient to (impossible) screen resolutions. Signed-off-by: AnErrupTion --- src/main.zig | 409 +++++++++++++++++++++-------- src/tui/Position.zig | 189 +++++++++++++ src/tui/TerminalBuffer.zig | 34 ++- src/tui/components/CenteredBox.zig | 42 +-- src/tui/components/InfoLine.zig | 17 +- src/tui/components/Label.zig | 109 ++++++++ src/tui/components/Session.zig | 19 +- src/tui/components/Text.zig | 20 +- src/tui/components/UserList.zig | 25 +- src/tui/components/generic.zig | 27 +- 10 files changed, 737 insertions(+), 154 deletions(-) create mode 100644 src/tui/components/Label.zig diff --git a/src/main.zig b/src/main.zig index e36165c..3f8042a 100644 --- a/src/main.zig +++ b/src/main.zig @@ -30,8 +30,10 @@ const DisplayServer = enums.DisplayServer; const Environment = @import("Environment.zig"); const Entry = Environment.Entry; const Animation = @import("tui/Animation.zig"); +const Position = @import("tui/Position.zig"); const CenteredBox = @import("tui/components/CenteredBox.zig"); const InfoLine = @import("tui/components/InfoLine.zig"); +const Label = @import("tui/components/Label.zig"); const Session = @import("tui/components/Session.zig"); const Text = @import("tui/components/Text.zig"); const UserList = @import("tui/components/UserList.zig"); @@ -68,6 +70,20 @@ const UiState = struct { animation_timed_out: bool, animation: *?Animation, can_draw_battery: bool, + shutdown_label: *Label, + restart_label: *Label, + sleep_label: *Label, + hibernate_label: *Label, + brightness_down_label: *Label, + brightness_up_label: *Label, + numlock_label: *Label, + capslock_label: *Label, + battery_label: *Label, + clock_label: *Label, + session_specifier_label: *Label, + login_label: *Label, + password_label: *Label, + version_label: *Label, box: *CenteredBox, info_line: *InfoLine, animate: bool, @@ -85,6 +101,9 @@ const UiState = struct { brightness_down_len: u8, brightness_up_len: u8, can_get_lock_state: bool, + edge_margin: Position, + hide_key_hints: bool, + uses_clock: bool, }; pub fn main() !void { @@ -320,6 +339,127 @@ pub fn main() !void { std.posix.sigaction(std.posix.SIG.TERM, &act, null); // Initialize components + var shutdown_label = Label.init( + "", + null, + buffer.fg, + buffer.bg, + ); + defer shutdown_label.deinit(allocator); + + var restart_label = Label.init( + "", + null, + buffer.fg, + buffer.bg, + ); + defer restart_label.deinit(allocator); + + var sleep_label = Label.init( + "", + null, + buffer.fg, + buffer.bg, + ); + defer sleep_label.deinit(allocator); + + var hibernate_label = Label.init( + "", + null, + buffer.fg, + buffer.bg, + ); + defer hibernate_label.deinit(allocator); + + var brightness_down_label = Label.init( + "", + null, + buffer.fg, + buffer.bg, + ); + defer brightness_down_label.deinit(allocator); + + var brightness_up_label = Label.init( + "", + null, + buffer.fg, + buffer.bg, + ); + defer brightness_up_label.deinit(allocator); + + if (!config.hide_key_hints) { + try shutdown_label.setTextAlloc( + allocator, + "{s} {s}", + .{ config.shutdown_key, lang.shutdown }, + ); + try restart_label.setTextAlloc( + allocator, + "{s} {s}", + .{ config.restart_key, lang.restart }, + ); + if (config.sleep_cmd != null) { + try sleep_label.setTextAlloc( + allocator, + "{s} {s}", + .{ config.sleep_key, lang.sleep }, + ); + } + if (config.hibernate_cmd != null) { + try hibernate_label.setTextAlloc( + allocator, + "{s} {s}", + .{ config.hibernate_key, lang.hibernate }, + ); + } + if (config.brightness_down_key) |key| { + try brightness_down_label.setTextAlloc( + allocator, + "{s} {s}", + .{ key, lang.brightness_down }, + ); + } + if (config.brightness_up_key) |key| { + try brightness_up_label.setTextAlloc( + allocator, + "{s} {s}", + .{ key, lang.brightness_up }, + ); + } + } + + var numlock_label = Label.init( + lang.numlock, + null, + buffer.fg, + buffer.bg, + ); + defer numlock_label.deinit(null); + + var capslock_label = Label.init( + lang.capslock, + null, + buffer.fg, + buffer.bg, + ); + defer capslock_label.deinit(null); + + var battery_label = Label.init( + "", + null, + buffer.fg, + buffer.bg, + ); + defer battery_label.deinit(null); + + var clock_label = Label.init( + "", + null, + buffer.fg, + buffer.bg, + ); + defer clock_label.deinit(null); + var box = CenteredBox.init( &buffer, config.margin_box_h, @@ -330,12 +470,17 @@ pub fn main() !void { config.blank_box, config.box_title, null, + buffer.border_fg, + buffer.fg, + buffer.bg, ); var info_line = InfoLine.init( allocator, &buffer, box.width - 2 * box.horizontal_margin, + buffer.fg, + buffer.bg, ); defer info_line.deinit(); @@ -380,15 +525,33 @@ pub fn main() !void { var login: UserList = undefined; + var session_specifier_label = Label.init( + "", + null, + buffer.fg, + buffer.bg, + ); + defer session_specifier_label.deinit(null); + var session = Session.init( allocator, &buffer, &login, box.width - 2 * box.horizontal_margin - labels_max_length - 1, config.text_in_center, + buffer.fg, + buffer.bg, ); defer session.deinit(); + var login_label = Label.init( + lang.login, + null, + buffer.fg, + buffer.bg, + ); + defer login_label.deinit(null); + login = try UserList.init( allocator, &buffer, @@ -397,6 +560,8 @@ pub fn main() !void { &session, box.width - 2 * box.horizontal_margin - labels_max_length - 1, config.text_in_center, + buffer.fg, + buffer.bg, ); defer login.deinit(); @@ -459,15 +624,33 @@ pub fn main() !void { try log_file.err("sys", "no users found", .{}); } + var password_label = Label.init( + lang.password, + null, + buffer.fg, + buffer.bg, + ); + defer password_label.deinit(null); + var password = Text.init( allocator, &buffer, true, config.asterisk, box.width - 2 * box.horizontal_margin - labels_max_length - 1, + buffer.fg, + buffer.bg, ); defer password.deinit(); + var version_label = Label.init( + ly_version_str, + null, + buffer.fg, + buffer.bg, + ); + defer version_label.deinit(null); + var is_autologin = false; check_autologin: { @@ -506,6 +689,20 @@ pub fn main() !void { .animation_timed_out = false, .animation = &animation, .can_draw_battery = true, + .shutdown_label = &shutdown_label, + .restart_label = &restart_label, + .sleep_label = &sleep_label, + .hibernate_label = &hibernate_label, + .brightness_down_label = &brightness_down_label, + .brightness_up_label = &brightness_up_label, + .numlock_label = &numlock_label, + .capslock_label = &capslock_label, + .battery_label = &battery_label, + .clock_label = &clock_label, + .session_specifier_label = &session_specifier_label, + .login_label = &login_label, + .password_label = &password_label, + .version_label = &version_label, .box = &box, .info_line = &info_line, .animate = config.animation != .none, @@ -523,6 +720,12 @@ pub fn main() !void { .brightness_down_len = try TerminalBuffer.strWidth(lang.brightness_down), .brightness_up_len = try TerminalBuffer.strWidth(lang.brightness_up), .can_get_lock_state = true, + .edge_margin = Position.init( + config.edge_margin, + config.edge_margin, + ), + .hide_key_hints = config.hide_key_hints, + .uses_clock = config.clock != null, }; // Load last saved username and desktop selection, if any @@ -550,17 +753,13 @@ pub fn main() !void { } // Position components - state.box.position(TerminalBuffer.START_POSITION); - state.info_line.label.positionY(state.box.childrenPosition()); - state.session.label.positionY(state.info_line.label.childrenPosition().addY(1).addX(state.labels_max_length + 1)); - state.login.label.positionY(state.session.label.childrenPosition().addY(1)); - state.password.positionY(state.login.label.childrenPosition().addY(1)); + positionComponents(&state); switch (state.active_input) { - .info_line => info_line.label.handle(null, state.insert_mode), - .session => session.label.handle(null, state.insert_mode), - .login => login.label.handle(null, state.insert_mode), - .password => password.handle(null, state.insert_mode) catch |err| { + .info_line => state.info_line.label.handle(null, state.insert_mode), + .session => state.session.label.handle(null, state.insert_mode), + .login => state.login.label.handle(null, state.insert_mode), + .password => state.password.handle(null, state.insert_mode) catch |err| { try info_line.addMessage(lang.err_alloc, config.error_bg, config.error_fg); try log_file.err("tui", "failed to handle password input: {s}", .{@errorName(err)}); }, @@ -1006,13 +1205,9 @@ fn drawUi(config: Config, lang: Lang, log_file: *LogFile, state: *UiState) !bool try TerminalBuffer.clearScreenStatic(false); - var length: usize = config.edge_margin; - if (!state.animation_timed_out) if (state.animation.*) |*a| a.draw(); - if (!config.hide_version_string) { - state.buffer.drawLabel(ly_version_str, config.edge_margin, state.buffer.height - 1 - config.edge_margin); - } + if (!config.hide_version_string) state.version_label.draw(); if (config.battery_id) |id| draw_battery: { if (!state.can_draw_battery) break :draw_battery; @@ -1025,14 +1220,12 @@ fn drawUi(config: Config, lang: Lang, log_file: *LogFile, state: *UiState) !bool }; var battery_buf: [16:0]u8 = undefined; - const battery_str = std.fmt.bufPrintZ(&battery_buf, "BAT: {d}%", .{battery_percentage}) catch break :draw_battery; - - var battery_y: usize = config.edge_margin; - if (!config.hide_key_hints) { - battery_y += 1; - } - state.buffer.drawLabel(battery_str, config.edge_margin, battery_y); - state.can_draw_battery = true; + state.battery_label.setTextBuf( + &battery_buf, + "BAT: {d}%", + .{battery_percentage}, + ) catch break :draw_battery; + state.battery_label.draw(); } if (config.bigclock != .none and state.box.height + (bigclock.HEIGHT + 2) * 2 < state.buffer.height) { @@ -1060,12 +1253,7 @@ fn drawUi(config: Config, lang: Lang, log_file: *LogFile, state: *UiState) !bool state.box.draw(); if (state.resolution_changed) { - state.box.position(TerminalBuffer.START_POSITION); - state.info_line.label.positionY(state.box.childrenPosition()); - state.session.label.positionY(state.info_line.label.childrenPosition().addY(1).addX(state.labels_max_length + 1)); - state.login.label.positionY(state.session.label.childrenPosition().addY(1)); - state.password.positionY(state.login.label.childrenPosition().addY(1)); - + positionComponents(state); state.resolution_changed = false; } @@ -1092,78 +1280,25 @@ fn drawUi(config: Config, lang: Lang, log_file: *LogFile, state: *UiState) !bool break :draw_clock; } - state.buffer.drawLabel(clock_str, state.buffer.width - @min(state.buffer.width, clock_str.len) - config.edge_margin, config.edge_margin); + state.clock_label.setText(clock_str); + state.clock_label.draw(); } const env = state.session.label.list.items[state.session.label.current]; - state.buffer.drawLabel( - env.environment.specifier, - state.box.childrenPosition().x, - state.session.label.component_pos.y, - ); - state.buffer.drawLabel( - lang.login, - state.box.childrenPosition().x, - state.login.label.component_pos.y, - ); - state.buffer.drawLabel( - lang.password, - state.box.childrenPosition().x, - state.password.component_pos.y, - ); + state.session_specifier_label.setText(env.environment.specifier); + state.session_specifier_label.draw(); + state.login_label.draw(); + state.password_label.draw(); state.info_line.label.draw(); if (!config.hide_key_hints) { - state.buffer.drawLabel(config.shutdown_key, length, config.edge_margin); - length += config.shutdown_key.len + 1; - state.buffer.drawLabel(" ", length - 1, config.edge_margin); - - state.buffer.drawLabel(lang.shutdown, length, config.edge_margin); - length += state.shutdown_len + 1; - - state.buffer.drawLabel(config.restart_key, length, config.edge_margin); - length += config.restart_key.len + 1; - state.buffer.drawLabel(" ", length - 1, config.edge_margin); - - state.buffer.drawLabel(lang.restart, length, config.edge_margin); - length += state.restart_len + 1; - - if (config.sleep_cmd != null) { - state.buffer.drawLabel(config.sleep_key, length, config.edge_margin); - length += config.sleep_key.len + 1; - state.buffer.drawLabel(" ", length - 1, config.edge_margin); - - state.buffer.drawLabel(lang.sleep, length, config.edge_margin); - length += state.sleep_len + 1; - } - - if (config.hibernate_cmd != null) { - state.buffer.drawLabel(config.hibernate_key, length, config.edge_margin); - length += config.hibernate_key.len + 1; - state.buffer.drawLabel(" ", length - 1, config.edge_margin); - - state.buffer.drawLabel(lang.hibernate, length, config.edge_margin); - length += state.hibernate_len + 1; - } - - if (config.brightness_down_key) |key| { - state.buffer.drawLabel(key, length, config.edge_margin); - length += key.len + 1; - state.buffer.drawLabel(" ", length - 1, config.edge_margin); - - state.buffer.drawLabel(lang.brightness_down, length, config.edge_margin); - length += state.brightness_down_len + 1; - } - - if (config.brightness_up_key) |key| { - state.buffer.drawLabel(key, length, config.edge_margin); - length += key.len + 1; - state.buffer.drawLabel(" ", length - 1, config.edge_margin); - - state.buffer.drawLabel(lang.brightness_up, length, config.edge_margin); - length += state.brightness_up_len + 1; - } + state.shutdown_label.draw(); + state.restart_label.draw(); + state.sleep_label.draw(); + state.hibernate_label.draw(); + state.brightness_down_label.draw(); + state.brightness_up_label.draw(); } if (config.vi_mode) { @@ -1178,17 +1313,8 @@ fn drawUi(config: Config, lang: Lang, log_file: *LogFile, state: *UiState) !bool break :draw_lock_state; }; - var lock_state_x = state.buffer.width - @min(state.buffer.width, lang.numlock.len) - config.edge_margin; - var lock_state_y: usize = config.edge_margin; - - if (config.clock != null) lock_state_y += 1; - - if (lock_state.numlock) state.buffer.drawLabel(lang.numlock, lock_state_x, lock_state_y); - - if (lock_state_x >= lang.capslock.len + 1) { - lock_state_x -= lang.capslock.len + 1; - if (lock_state.capslock) state.buffer.drawLabel(lang.capslock, lock_state_x, lock_state_y); - } + if (lock_state.numlock) state.numlock_label.draw(); + if (lock_state.capslock) state.capslock_label.draw(); } state.session.label.draw(); @@ -1199,6 +1325,81 @@ fn drawUi(config: Config, lang: Lang, log_file: *LogFile, state: *UiState) !bool return true; } +fn positionComponents(state: *UiState) void { + if (!state.hide_key_hints) { + state.shutdown_label.positionX(state.edge_margin + .add(TerminalBuffer.START_POSITION)); + state.restart_label.positionX(state.shutdown_label + .childrenPosition() + .addX(1)); + state.sleep_label.positionX(state.restart_label + .childrenPosition() + .addX(1)); + state.hibernate_label.positionX(state.sleep_label + .childrenPosition() + .addX(1)); + state.brightness_down_label.positionX(state.hibernate_label + .childrenPosition() + .addX(1)); + state.brightness_up_label.positionX(state.brightness_down_label + .childrenPosition() + .addX(1)); + } + + state.battery_label.positionXY(state.edge_margin + .add(TerminalBuffer.START_POSITION) + .addYFromIf(state.shutdown_label.childrenPosition(), !state.hide_key_hints) + .removeYFromIf(state.edge_margin, !state.hide_key_hints)); + // TODO: Fix not showing on first try (with separate update function) + state.clock_label.positionXY(state.edge_margin + .add(TerminalBuffer.START_POSITION) + .invertX(state.buffer.width) + .removeXIf(state.clock_label.text.len, state.buffer.width > state.clock_label.text.len + state.edge_margin.x)); + + state.numlock_label.positionX(state.edge_margin + .add(TerminalBuffer.START_POSITION) + .addYFromIf(state.clock_label.childrenPosition(), state.uses_clock) + .removeYFromIf(state.edge_margin, state.uses_clock) + .invertX(state.buffer.width) + .removeXIf(state.numlock_label.text.len, state.buffer.width > state.numlock_label.text.len + state.edge_margin.x)); + state.capslock_label.positionX(state.numlock_label + .childrenPosition() + .removeX(state.numlock_label.text.len + state.capslock_label.text.len + 1)); + + state.box.positionXY(TerminalBuffer.START_POSITION); + + state.info_line.label.positionY(state.box + .childrenPosition()); + + // TODO: Same as above + state.session_specifier_label.positionX(state.info_line.label + .childrenPosition() + .addY(1)); + state.session.label.positionY(state.session_specifier_label + .childrenPosition() + .addX(state.labels_max_length - state.session_specifier_label.text.len + 1)); + + state.login_label.positionX(state.session.label + .childrenPosition() + .resetXFrom(state.info_line.label.childrenPosition()) + .addY(1)); + state.login.label.positionY(state.login_label + .childrenPosition() + .addX(state.labels_max_length - state.login_label.text.len + 1)); + + state.password_label.positionX(state.login.label + .childrenPosition() + .resetXFrom(state.info_line.label.childrenPosition()) + .addY(1)); + state.password.positionY(state.password_label + .childrenPosition() + .addX(state.labels_max_length - state.password_label.text.len + 1)); + + state.version_label.positionXY(state.edge_margin + .add(TerminalBuffer.START_POSITION) + .invertY(state.buffer.height - 1)); +} + fn addOtherEnvironment(session: *Session, lang: Lang, display_server: DisplayServer, exec: ?[]const u8) !void { const name = switch (display_server) { .shell => lang.shell, diff --git a/src/tui/Position.zig b/src/tui/Position.zig index 1a61f61..7073cba 100644 --- a/src/tui/Position.zig +++ b/src/tui/Position.zig @@ -17,6 +17,13 @@ pub fn add(self: Position, other: Position) Position { }; } +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, @@ -30,3 +37,185 @@ pub fn addY(self: Position, y: usize) Position { .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, + }; +} diff --git a/src/tui/TerminalBuffer.zig b/src/tui/TerminalBuffer.zig index d02a858..7b322b9 100644 --- a/src/tui/TerminalBuffer.zig +++ b/src/tui/TerminalBuffer.zig @@ -201,11 +201,13 @@ pub fn cascade(self: TerminalBuffer) bool { return changed; } -pub fn drawLabel(self: TerminalBuffer, text: []const u8, x: usize, y: usize) void { - drawColorLabel(text, x, y, self.fg, self.bg); -} - -pub fn drawColorLabel(text: []const u8, x: usize, y: usize, fg: u32, bg: u32) void { +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(); @@ -216,7 +218,14 @@ pub fn drawColorLabel(text: []const u8, x: usize, y: usize, fg: u32, bg: u32) vo } } -pub fn drawConfinedLabel(self: TerminalBuffer, text: []const u8, x: usize, y: usize, max_length: usize) void { +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(); @@ -224,12 +233,19 @@ pub fn drawConfinedLabel(self: TerminalBuffer, text: []const u8, x: usize, y: us 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, self.fg, self.bg); + _ = termbox.tb_set_cell(i, yc, codepoint, fg, bg); } } -pub fn drawCharMultiple(self: TerminalBuffer, char: u32, x: usize, y: usize, length: usize) void { - const cell = Cell.init(char, self.fg, self.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); } diff --git a/src/tui/components/CenteredBox.zig b/src/tui/components/CenteredBox.zig index da0028b..e0f1798 100644 --- a/src/tui/components/CenteredBox.zig +++ b/src/tui/components/CenteredBox.zig @@ -1,9 +1,8 @@ const std = @import("std"); -const Allocator = std.mem.Allocator; const Cell = @import("../Cell.zig"); -const TerminalBuffer = @import("../TerminalBuffer.zig"); const Position = @import("../Position.zig"); +const TerminalBuffer = @import("../TerminalBuffer.zig"); const termbox = TerminalBuffer.termbox; const CenteredBox = @This(); @@ -17,6 +16,9 @@ show_borders: bool, blank_box: bool, top_title: ?[]const u8, bottom_title: ?[]const u8, +border_fg: u32, +title_fg: u32, +bg: u32, left_pos: Position, right_pos: Position, children_pos: Position, @@ -31,6 +33,9 @@ pub fn init( blank_box: bool, top_title: ?[]const u8, bottom_title: ?[]const u8, + border_fg: u32, + title_fg: u32, + bg: u32, ) CenteredBox { return .{ .buffer = buffer, @@ -42,13 +47,16 @@ pub fn init( .blank_box = blank_box, .top_title = top_title, .bottom_title = bottom_title, + .border_fg = border_fg, + .title_fg = title_fg, + .bg = bg, .left_pos = TerminalBuffer.START_POSITION, .right_pos = TerminalBuffer.START_POSITION, .children_pos = TerminalBuffer.START_POSITION, }; } -pub fn position(self: *CenteredBox, original_pos: Position) void { +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( @@ -77,33 +85,33 @@ pub fn draw(self: CenteredBox) void { @intCast(self.left_pos.x - 1), @intCast(self.left_pos.y - 1), self.buffer.box_chars.left_up, - self.buffer.border_fg, - self.buffer.bg, + self.border_fg, + self.bg, ); _ = termbox.tb_set_cell( @intCast(self.right_pos.x), @intCast(self.left_pos.y - 1), self.buffer.box_chars.right_up, - self.buffer.border_fg, - self.buffer.bg, + self.border_fg, + self.bg, ); _ = termbox.tb_set_cell( @intCast(self.left_pos.x - 1), @intCast(self.right_pos.y), self.buffer.box_chars.left_down, - self.buffer.border_fg, - self.buffer.bg, + self.border_fg, + self.bg, ); _ = termbox.tb_set_cell( @intCast(self.right_pos.x), @intCast(self.right_pos.y), self.buffer.box_chars.right_down, - self.buffer.border_fg, - self.buffer.bg, + self.border_fg, + self.bg, ); - var c1 = Cell.init(self.buffer.box_chars.top, self.buffer.border_fg, self.buffer.bg); - var c2 = Cell.init(self.buffer.box_chars.bottom, self.buffer.border_fg, self.buffer.bg); + var c1 = Cell.init(self.buffer.box_chars.top, self.border_fg, self.bg); + var c2 = Cell.init(self.buffer.box_chars.bottom, self.border_fg, self.bg); for (0..self.width) |i| { c1.put(self.left_pos.x + i, self.left_pos.y - 1); @@ -128,20 +136,24 @@ pub fn draw(self: CenteredBox) void { } if (self.top_title) |title| { - self.buffer.drawConfinedLabel( + TerminalBuffer.drawConfinedText( title, self.left_pos.x, self.left_pos.y - 1, self.width, + self.title_fg, + self.bg, ); } if (self.bottom_title) |title| { - self.buffer.drawConfinedLabel( + TerminalBuffer.drawConfinedText( title, self.left_pos.x, self.left_pos.y + self.height, self.width, + self.title_fg, + self.bg, ); } } diff --git a/src/tui/components/InfoLine.zig b/src/tui/components/InfoLine.zig index 6248391..ecff573 100644 --- a/src/tui/components/InfoLine.zig +++ b/src/tui/components/InfoLine.zig @@ -21,6 +21,8 @@ pub fn init( allocator: Allocator, buffer: *TerminalBuffer, width: usize, + arrow_fg: u32, + arrow_bg: u32, ) InfoLine { return .{ .label = MessageLabel.init( @@ -31,6 +33,8 @@ pub fn init( null, width, true, + arrow_fg, + arrow_bg, ), }; } @@ -57,23 +61,26 @@ pub fn clearRendered(self: InfoLine, allocator: Allocator) !void { @memset(spaces, ' '); - self.label.buffer.drawLabel( + TerminalBuffer.drawText( spaces, self.label.component_pos.x + 2, self.label.component_pos.y, + TerminalBuffer.Color.DEFAULT, + TerminalBuffer.Color.DEFAULT, ); } fn drawItem(label: *MessageLabel, message: Message, x: usize, y: usize, width: usize) void { - if (message.width == 0 or width <= message.width) return; + if (message.width == 0) return; - const x_offset = if (label.text_in_center) (width - message.width - 1) / 2 else 0; + const x_offset = if (label.text_in_center and width >= message.width) (width - message.width) / 2 else 0; - label.item_width = message.width + x_offset; - TerminalBuffer.drawColorLabel( + label.cursor = message.width + x_offset; + TerminalBuffer.drawConfinedText( message.text, x + x_offset, y, + width, message.fg, message.bg, ); diff --git a/src/tui/components/Label.zig b/src/tui/components/Label.zig new file mode 100644 index 0000000..c12ee11 --- /dev/null +++ b/src/tui/components/Label.zig @@ -0,0 +1,109 @@ +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 termbox = TerminalBuffer.termbox; + +const Label = @This(); + +text: []const u8, +max_width: ?usize, +fg: u32, +bg: u32, +is_text_allocated: bool, +component_pos: Position, +children_pos: Position, + +pub fn init( + text: []const u8, + max_width: ?usize, + fg: u32, + bg: u32, +) Label { + return .{ + .text = text, + .max_width = max_width, + .fg = fg, + .bg = bg, + .is_text_allocated = false, + .component_pos = TerminalBuffer.START_POSITION, + .children_pos = TerminalBuffer.START_POSITION, + }; +} + +pub fn setTextAlloc( + self: *Label, + allocator: Allocator, + comptime fmt: []const u8, + args: anytype, +) !void { + self.text = try std.fmt.allocPrint(allocator, fmt, args); + self.is_text_allocated = true; +} + +pub fn setTextBuf( + self: *Label, + buffer: []u8, + comptime fmt: []const u8, + args: anytype, +) !void { + self.text = try std.fmt.bufPrint(buffer, fmt, args); + self.is_text_allocated = false; +} + +pub fn setText(self: *Label, text: []const u8) void { + self.text = text; + self.is_text_allocated = false; +} + +pub fn deinit(self: Label, allocator: ?Allocator) void { + if (self.is_text_allocated) { + if (allocator) |alloc| alloc.free(self.text); + } +} + +pub fn positionX(self: *Label, original_pos: Position) void { + self.component_pos = original_pos; + self.children_pos = original_pos.addX(self.text.len); +} + +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( + self.text.len, + 1, + ).add(original_pos); +} + +pub fn childrenPosition(self: Label) Position { + return self.children_pos; +} + +pub 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, + ); +} diff --git a/src/tui/components/Session.zig b/src/tui/components/Session.zig index 2c2ce62..d468eb0 100644 --- a/src/tui/components/Session.zig +++ b/src/tui/components/Session.zig @@ -25,6 +25,8 @@ pub fn init( user_list: *UserList, width: usize, text_in_center: bool, + fg: u32, + bg: u32, ) Session { return .{ .label = EnvironmentLabel.init( @@ -35,6 +37,8 @@ pub fn init( user_list, width, text_in_center, + fg, + bg, ), .user_list = user_list, }; @@ -70,11 +74,20 @@ fn sessionChanged(env: Env, maybe_user_list: ?*UserList) void { } fn drawItem(label: *EnvironmentLabel, env: Env, x: usize, y: usize, width: usize) void { + if (width < 3) return; + const length = @min(env.environment.name.len, width - 3); if (length == 0) return; - const x_offset = if (label.text_in_center) (width - length - 1) / 2 else 0; + const x_offset = if (label.text_in_center and width >= length) (width - length) / 2 else 0; - label.item_width = length + x_offset; - label.buffer.drawLabel(env.environment.name, x + x_offset, y); + label.cursor = length + x_offset; + TerminalBuffer.drawConfinedText( + env.environment.name, + x + x_offset, + y, + width, + label.fg, + label.bg, + ); } diff --git a/src/tui/components/Text.zig b/src/tui/components/Text.zig index c31bcb4..095253e 100644 --- a/src/tui/components/Text.zig +++ b/src/tui/components/Text.zig @@ -20,6 +20,8 @@ component_pos: Position, children_pos: Position, masked: bool, maybe_mask: ?u32, +fg: u32, +bg: u32, pub fn init( allocator: Allocator, @@ -27,6 +29,8 @@ pub fn init( masked: bool, maybe_mask: ?u32, width: usize, + fg: u32, + bg: u32, ) Text { return .{ .allocator = allocator, @@ -40,6 +44,8 @@ pub fn init( .children_pos = TerminalBuffer.START_POSITION, .masked = masked, .maybe_mask = maybe_mask, + .fg = fg, + .bg = bg, }; } @@ -115,14 +121,18 @@ pub fn handle(self: *Text, maybe_event: ?*termbox.tb_event, insert_mode: bool) ! pub fn draw(self: Text) void { if (self.masked) { if (self.maybe_mask) |mask| { + if (self.width < 1) return; + const length = @min(self.text.items.len, self.width - 1); if (length == 0) return; - self.buffer.drawCharMultiple( + TerminalBuffer.drawCharMultiple( mask, self.component_pos.x, self.component_pos.y, length, + self.fg, + self.bg, ); } return; @@ -139,7 +149,13 @@ pub fn draw(self: Text) void { } }; - self.buffer.drawLabel(visible_slice, self.component_pos.x, self.component_pos.y); + TerminalBuffer.drawText( + visible_slice, + self.component_pos.x, + self.component_pos.y, + self.fg, + self.bg, + ); } pub fn clear(self: *Text) void { diff --git a/src/tui/components/UserList.zig b/src/tui/components/UserList.zig index 26eee4c..3bb96b3 100644 --- a/src/tui/components/UserList.zig +++ b/src/tui/components/UserList.zig @@ -27,8 +27,10 @@ pub fn init( session: *Session, width: usize, text_in_center: bool, + fg: u32, + bg: u32, ) !UserList { - var userList = UserList{ + var user_list = UserList{ .label = UserLabel.init( allocator, buffer, @@ -37,6 +39,8 @@ pub fn init( session, width, text_in_center, + fg, + bg, ), }; @@ -60,7 +64,7 @@ pub fn init( allocated_index = true; } - try userList.label.addItem(.{ + try user_list.label.addItem(.{ .name = username, .session_index = maybe_session_index.?, .allocated_index = allocated_index, @@ -68,7 +72,7 @@ pub fn init( }); } - return userList; + return user_list; } pub fn deinit(self: *UserList) void { @@ -92,11 +96,20 @@ fn usernameChanged(user: User, maybe_session: ?*Session) void { } fn drawItem(label: *UserLabel, user: User, x: usize, y: usize, width: usize) void { + if (width < 3) return; + const length = @min(user.name.len, width - 3); if (length == 0) return; - const x_offset = if (label.text_in_center) (width - length - 1) / 2 else 0; + const x_offset = if (label.text_in_center and width >= length) (width - length) / 2 else 0; - label.item_width = length + x_offset; - label.buffer.drawLabel(user.name, x + x_offset, y); + label.cursor = length + x_offset; + TerminalBuffer.drawConfinedText( + user.name, + x + x_offset, + y, + width, + label.fg, + label.bg, + ); } diff --git a/src/tui/components/generic.zig b/src/tui/components/generic.zig index 479b14f..d2ddfd0 100644 --- a/src/tui/components/generic.zig +++ b/src/tui/components/generic.zig @@ -22,7 +22,9 @@ pub fn CyclableLabel(comptime ItemType: type, comptime ChangeItemType: type) typ component_pos: Position, children_pos: Position, text_in_center: bool, - item_width: usize, + fg: u32, + bg: u32, + cursor: usize, draw_item_fn: DrawItemFn, change_item_fn: ?ChangeItemFn, change_item_arg: ?ChangeItemType, @@ -35,6 +37,8 @@ pub fn CyclableLabel(comptime ItemType: type, comptime ChangeItemType: type) typ change_item_arg: ?ChangeItemType, width: usize, text_in_center: bool, + fg: u32, + bg: u32, ) Self { return .{ .allocator = allocator, @@ -45,7 +49,9 @@ pub fn CyclableLabel(comptime ItemType: type, comptime ChangeItemType: type) typ .component_pos = TerminalBuffer.START_POSITION, .children_pos = TerminalBuffer.START_POSITION, .text_in_center = text_in_center, - .item_width = 0, + .fg = fg, + .bg = bg, + .cursor = 0, .draw_item_fn = draw_item_fn, .change_item_fn = change_item_fn, .change_item_arg = change_item_arg, @@ -58,19 +64,19 @@ pub fn CyclableLabel(comptime ItemType: type, comptime ChangeItemType: type) typ pub fn positionX(self: *Self, original_pos: Position) void { self.component_pos = original_pos; - self.item_width = self.component_pos.x + 2; + 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.item_width = self.component_pos.x + 2; + 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.item_width = self.component_pos.x + 2; + self.cursor = self.component_pos.x + 2; self.children_pos = Position.init( self.width, 1, @@ -106,27 +112,28 @@ pub fn CyclableLabel(comptime ItemType: type, comptime ChangeItemType: type) typ } _ = termbox.tb_set_cursor( - @intCast(self.component_pos.x + self.item_width + 2), + @intCast(self.component_pos.x + self.cursor + 2), @intCast(self.component_pos.y), ); } pub fn draw(self: *Self) void { if (self.list.items.len == 0) return; + if (self.width < 2) return; _ = termbox.tb_set_cell( @intCast(self.component_pos.x), @intCast(self.component_pos.y), '<', - self.buffer.fg, - self.buffer.bg, + self.fg, + self.bg, ); _ = termbox.tb_set_cell( @intCast(self.component_pos.x + self.width - 1), @intCast(self.component_pos.y), '>', - self.buffer.fg, - self.buffer.bg, + self.fg, + self.bg, ); const current_item = self.list.items[self.current];