From d0ccaa4d69044ddabbaf2202347cb26b6dd4b918 Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Thu, 6 Mar 2025 12:47:28 +0100 Subject: [PATCH] Enable true color output (closes #705) Signed-off-by: AnErrupTion --- build.zig | 1 + res/config.ini | 66 ++++++++++-------------- src/animations/ColorMix.zig | 2 +- src/animations/Matrix.zig | 4 +- src/bigclock.zig | 22 ++++---- src/config/Config.zig | 18 +++---- src/config/migrator.zig | 89 ++++++++++++++++++++++++++------- src/main.zig | 4 +- src/tui/TerminalBuffer.zig | 8 +-- src/tui/components/InfoLine.zig | 6 +-- src/tui/utils.zig | 6 +-- 11 files changed, 134 insertions(+), 92 deletions(-) diff --git a/build.zig b/build.zig index 5984a45..34e6adf 100644 --- a/build.zig +++ b/build.zig @@ -72,6 +72,7 @@ pub fn build(b: *std.Build) !void { .optimize = optimize, }); translate_c.defineCMacroRaw("TB_IMPL"); + translate_c.defineCMacro("TB_OPT_ATTR_W", "32"); // Enable 24-bit color support + styling (32-bit) const termbox2 = translate_c.addModule("termbox2"); exe.root_module.addImport("termbox2", termbox2); diff --git a/res/config.ini b/res/config.ini index debb6df..4490e92 100644 --- a/res/config.ini +++ b/res/config.ini @@ -1,33 +1,19 @@ -# The color settings in Ly take a digit 0-8 corresponding to: -#define TB_DEFAULT 0x00 -#define TB_BLACK 0x01 -#define TB_RED 0x02 -#define TB_GREEN 0x03 -#define TB_YELLOW 0x04 -#define TB_BLUE 0x05 -#define TB_MAGENTA 0x06 -#define TB_CYAN 0x07 -#define TB_WHITE 0x08 -# The default color varies, but usually it makes the background black and the foreground white. -# You can also combine these colors with the following style attributes using bitwise OR: -#define TB_BOLD 0x0100 -#define TB_UNDERLINE 0x0200 -#define TB_REVERSE 0x0400 -#define TB_ITALIC 0x0800 -#define TB_BLINK 0x1000 -#define TB_HI_BLACK 0x2000 -#define TB_BRIGHT 0x4000 -#define TB_DIM 0x8000 -# For example, to set the foreground color to red and bold, you would do 0x02 | 0x0100 = 0x0102. -# Note that you must pre-calculate the value because Ly doesn't parse bitwise OR operations in its config. -# -# Moreover, to set the VT color palette, you are encouraged to use another tool such as -# mkinitcpio-colors (https://github.com/evanpurkhiser/mkinitcpio-colors). Note that the color palette defined with -# mkinitcpio-colors takes 16 colors (0-15), only values 0-8 are valid with Ly and these values do not correspond -# exactly. For instance, in defining palettes with mkinitcpio-colors, the order is black, dark red, dark green, brown, dark -# blue, dark purple, dark cyan, light gray, dark gray, bright red, bright green, yellow, bright blue, bright purple, bright -# cyan, and white, indexed in that order 0 through 15. For example, the color defined for white (indexed at 15 in the mkinitcpio -# config) will be used by Ly for fg = 0x0008. +# Ly supports 24-bit true color with styling, which means each color is a 32-bit value. +# The format is 0xSSRRGGBB, where SS is the styling, RR is red, GG is green, and BB is blue. +# Here are the possible styling options: +#define TB_BOLD 0x01000000 +#define TB_UNDERLINE 0x02000000 +#define TB_REVERSE 0x04000000 +#define TB_ITALIC 0x08000000 +#define TB_BLINK 0x10000000 +#define TB_HI_BLACK 0x20000000 +#define TB_BRIGHT 0x40000000 +#define TB_DIM 0x80000000 +# Programmatically, you'd apply them using the bitwise OR operator (|), but because Ly's +# configuration doesn't support using it, you have to manually compute the color value. +# Note that, if you want to use the default color value of the terminal, you can use the +# special value 0x00000000. This means that, if you want to use black, you *must* use +# the styling option TB_HI_BLACK (the RGB values are ignored when using this option). # The active animation # none -> Nothing @@ -50,7 +36,7 @@ asterisk = * auth_fails = 10 # Background color id -bg = 0x0000 +bg = 0x00000000 # Change the state and language of the big clock # none -> Disabled (default) @@ -63,7 +49,7 @@ bigclock = none blank_box = true # Border foreground color id -border_fg = 0x0008 +border_fg = 0x00FFFFFF # Title to show at the top of the main box # If set to null, none will be shown @@ -89,16 +75,16 @@ clear_password = false clock = null # CMatrix animation foreground color id -cmatrix_fg = 0x0003 +cmatrix_fg = 0x0000FF00 # Color mixing animation first color id -colormix_col1 = 0x0002 +colormix_col1 = 0x00FF0000 # Color mixing animation second color id -colormix_col2 = 0x0005 +colormix_col2 = 0x000000FF # Color mixing animation third color id -colormix_col3 = 0x0001 +colormix_col3 = 0x20000000 # Console path console_dev = /dev/console @@ -108,14 +94,14 @@ console_dev = /dev/console default_input = login # Error background color id -error_bg = 0x0000 +error_bg = 0x00000000 # Error foreground color id -# Default is red and bold: TB_RED | TB_BOLD -error_fg = 0x0102 +# Default is red and bold +error_fg = 0x11FF0000 # Foreground color id -fg = 0x0008 +fg = 0x00FFFFFF # Remove main box borders hide_borders = false diff --git a/src/animations/ColorMix.zig b/src/animations/ColorMix.zig index fb22881..11f8f76 100644 --- a/src/animations/ColorMix.zig +++ b/src/animations/ColorMix.zig @@ -20,7 +20,7 @@ pattern_cos_mod: f32, pattern_sin_mod: f32, palette: [palette_len]utils.Cell, -pub fn init(terminal_buffer: *TerminalBuffer, col1: u16, col2: u16, col3: u16) ColorMix { +pub fn init(terminal_buffer: *TerminalBuffer, col1: u32, col2: u32, col3: u32) ColorMix { return .{ .terminal_buffer = terminal_buffer, .frames = 0, diff --git a/src/animations/Matrix.zig b/src/animations/Matrix.zig index 42bf89b..f4b2e27 100644 --- a/src/animations/Matrix.zig +++ b/src/animations/Matrix.zig @@ -34,9 +34,9 @@ dots: []Dot, lines: []Line, frame: u64, count: u64, -fg_ini: u16, +fg_ini: u32, -pub fn init(allocator: Allocator, terminal_buffer: *TerminalBuffer, fg_ini: u16) !Matrix { +pub fn init(allocator: Allocator, terminal_buffer: *TerminalBuffer, fg_ini: u32) !Matrix { const dots = try allocator.alloc(Dot, terminal_buffer.width * (terminal_buffer.height + 1)); const lines = try allocator.alloc(Line, terminal_buffer.width); diff --git a/src/bigclock.zig b/src/bigclock.zig index 6f17b72..fb4baa7 100644 --- a/src/bigclock.zig +++ b/src/bigclock.zig @@ -2,17 +2,17 @@ const std = @import("std"); const interop = @import("interop.zig"); const utils = @import("tui/utils.zig"); const enums = @import("enums.zig"); -const Lang = @import("bigclock/Lang.zig"); -const en = @import("bigclock/en.zig"); -const fa = @import("bigclock/fa.zig"); +const Lang = @import("bigclock/Lang.zig"); +const en = @import("bigclock/en.zig"); +const fa = @import("bigclock/fa.zig"); -const termbox = interop.termbox; -const Bigclock = enums.Bigclock; -pub const WIDTH = Lang.WIDTH; +const termbox = interop.termbox; +const Bigclock = enums.Bigclock; +pub const WIDTH = Lang.WIDTH; pub const HEIGHT = Lang.HEIGHT; -pub const SIZE = Lang.SIZE; +pub const SIZE = Lang.SIZE; -pub fn clockCell(animate: bool, char: u8, fg: u16, bg: u16, bigclock: Bigclock) [SIZE]utils.Cell { +pub fn clockCell(animate: bool, char: u8, fg: u32, bg: u32, bigclock: Bigclock) [SIZE]utils.Cell { var cells: [SIZE]utils.Cell = undefined; var tv: interop.system_time.timeval = undefined; @@ -37,9 +37,9 @@ pub fn alphaBlit(x: usize, y: usize, tb_width: usize, tb_height: usize, cells: [ fn toBigNumber(char: u8, bigclock: Bigclock) []const u21 { const locale_chars = switch (bigclock) { - .fa => fa.locale_chars, - .en => en.locale_chars, - .none => unreachable, + .fa => fa.locale_chars, + .en => en.locale_chars, + .none => unreachable, }; return switch (char) { '0' => &locale_chars.ZERO, diff --git a/src/config/Config.zig b/src/config/Config.zig index 1ab4232..e949ab1 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -10,10 +10,10 @@ animation: Animation = .none, animation_timeout_sec: u12 = 0, asterisk: ?u8 = '*', auth_fails: u64 = 10, -bg: u16 = 0, +bg: u32 = 0x00000000, bigclock: Bigclock = .none, blank_box: bool = true, -border_fg: u16 = 8, +border_fg: u32 = 0x00FFFFFF, box_title: ?[]const u8 = null, brightness_down_cmd: [:0]const u8 = build_options.prefix_directory ++ "/bin/brightnessctl -q s 10%-", brightness_down_key: []const u8 = "F5", @@ -21,15 +21,15 @@ brightness_up_cmd: [:0]const u8 = build_options.prefix_directory ++ "/bin/bright brightness_up_key: []const u8 = "F6", clear_password: bool = false, clock: ?[:0]const u8 = null, -cmatrix_fg: u16 = 3, -colormix_col1: u16 = 2, -colormix_col2: u16 = 5, -colormix_col3: u16 = 1, +cmatrix_fg: u32 = 0x0000FF00, +colormix_col1: u32 = 0x00FF0000, +colormix_col2: u32 = 0x000000FF, +colormix_col3: u32 = 0x20000000, console_dev: []const u8 = "/dev/console", default_input: Input = .login, -error_bg: u16 = 0, -error_fg: u16 = 258, -fg: u16 = 8, +error_bg: u32 = 0x00000000, +error_fg: u32 = 0x11FF0000, +fg: u32 = 0x00FFFFFF, hide_borders: bool = false, hide_key_hints: bool = false, initial_info_text: ?[]const u8 = null, diff --git a/src/config/migrator.zig b/src/config/migrator.zig index ad7c378..7155906 100644 --- a/src/config/migrator.zig +++ b/src/config/migrator.zig @@ -2,9 +2,34 @@ const std = @import("std"); const ini = @import("zigini"); +const interop = @import("../interop.zig"); const Save = @import("Save.zig"); const enums = @import("../enums.zig"); +const termbox = interop.termbox; +const color_properties = [_][]const u8{ + "bg", + "border_fg", + "cmatrix_fg", + "colormix_col1", + "colormix_col2", + "colormix_col3", + "error_bg", + "error_fg", + "fg", +}; +const removed_properties = [_][]const u8{ + "wayland_specifier", + "max_desktop_len", + "max_login_len", + "max_password_len", + "mcookie_cmd", + "term_reset_cmd", + "term_restore_cursor_cmd", + "x_cmd_setup", + "wayland_cmd", +}; + var temporary_allocator = std.heap.page_allocator; pub var maybe_animate: ?bool = null; @@ -12,7 +37,7 @@ pub var maybe_save_file: ?[]const u8 = null; pub var mapped_config_fields = false; -pub fn configFieldHandler(_: std.mem.Allocator, field: ini.IniField) ?ini.IniField { +pub fn configFieldHandler(allocator: std.mem.Allocator, field: ini.IniField) ?ini.IniField { if (std.mem.eql(u8, field.key, "animate")) { // The option doesn't exist anymore, but we save its value for "animation" maybe_animate = std.mem.eql(u8, field.value, "true"); @@ -37,6 +62,18 @@ pub fn configFieldHandler(_: std.mem.Allocator, field: ini.IniField) ?ini.IniFie return mapped_field; } + inline for (color_properties) |property| { + if (std.mem.eql(u8, field.key, property)) { + // These options now uses a 32-bit RGB value instead of an arbitrary 16-bit integer + const color = std.fmt.parseInt(u16, field.value, 0) catch return field; + var mapped_field = field; + + mapped_field.value = mapColor(allocator, color) catch return field; + mapped_config_fields = true; + return mapped_field; + } + } + if (std.mem.eql(u8, field.key, "blank_password")) { // The option has simply been renamed var mapped_field = field; @@ -70,19 +107,12 @@ pub fn configFieldHandler(_: std.mem.Allocator, field: ini.IniField) ?ini.IniFie return null; } - if (std.mem.eql(u8, field.key, "wayland_specifier") or - std.mem.eql(u8, field.key, "max_desktop_len") or - std.mem.eql(u8, field.key, "max_login_len") or - std.mem.eql(u8, field.key, "max_password_len") or - std.mem.eql(u8, field.key, "mcookie_cmd") or - std.mem.eql(u8, field.key, "term_reset_cmd") or - std.mem.eql(u8, field.key, "term_restore_cursor_cmd") or - std.mem.eql(u8, field.key, "x_cmd_setup") or - std.mem.eql(u8, field.key, "wayland_cmd")) - { - // The options don't exist anymore - mapped_config_fields = true; - return null; + inline for (removed_properties) |property| { + if (std.mem.eql(u8, field.key, property)) { + // The options don't exist anymore + mapped_config_fields = true; + return null; + } } if (std.mem.eql(u8, field.key, "bigclock")) { @@ -90,14 +120,14 @@ pub fn configFieldHandler(_: std.mem.Allocator, field: ini.IniField) ?ini.IniFie // It also includes the ability to change active bigclock's language var mapped_field = field; - if (std.mem.eql(u8, field.value, "true")){ + if (std.mem.eql(u8, field.value, "true")) { mapped_field.value = "en"; mapped_config_fields = true; - }else if (std.mem.eql(u8, field.value, "false")){ + } else if (std.mem.eql(u8, field.value, "false")) { mapped_field.value = "none"; mapped_config_fields = true; } - + return mapped_field; } @@ -142,3 +172,28 @@ pub fn tryMigrateSaveFile(user_buf: *[32]u8) Save { return save; } + +fn mapColor(allocator: std.mem.Allocator, color: u16) ![]const u8 { + const color_no_styling = color & 0x00FF; + const styling_only = color & 0xFF00; + + var new_color: u32 = switch (color_no_styling) { + termbox.TB_BLACK => termbox.TB_HI_BLACK, + termbox.TB_RED => 0x00FF0000, + termbox.TB_GREEN => 0x0000FF00, + termbox.TB_YELLOW => 0x00FFFF00, + termbox.TB_BLUE => 0x000000FF, + termbox.TB_MAGENTA => 0x00FF00FF, + termbox.TB_CYAN => 0x0000FFFF, + termbox.TB_WHITE => 0x00FFFFFF, + else => termbox.TB_DEFAULT, + }; + + // Only applying styling if color isn't black and styling isn't also black + if (!(new_color == termbox.TB_HI_BLACK and styling_only == termbox.TB_HI_BLACK)) { + // Shift styling by 16 to the left to apply it to the new 32-bit color + new_color |= @as(u32, @intCast(styling_only)) << 16; + } + + return try std.fmt.allocPrint(allocator, "0x{X}", .{new_color}); +} diff --git a/src/main.zig b/src/main.zig index 582db77..bb54c3e 100644 --- a/src/main.zig +++ b/src/main.zig @@ -212,7 +212,7 @@ pub fn main() !void { }; std.posix.sigaction(std.posix.SIG.TERM, &act, null); - _ = termbox.tb_set_output_mode(termbox.TB_OUTPUT_NORMAL); + _ = termbox.tb_set_output_mode(termbox.TB_OUTPUT_TRUECOLOR); _ = termbox.tb_clear(); // Needed to reset termbox after auth @@ -729,7 +729,7 @@ pub fn main() !void { // Take back control of the TTY _ = termbox.tb_init(); - _ = termbox.tb_set_output_mode(termbox.TB_OUTPUT_NORMAL); + _ = termbox.tb_set_output_mode(termbox.TB_OUTPUT_TRUECOLOR); const auth_err = shared_err.readError(); if (auth_err) |err| { diff --git a/src/tui/TerminalBuffer.zig b/src/tui/TerminalBuffer.zig index 494ab81..cc18100 100644 --- a/src/tui/TerminalBuffer.zig +++ b/src/tui/TerminalBuffer.zig @@ -14,9 +14,9 @@ random: Random, width: usize, height: usize, buffer: [*]termbox.tb_cell, -fg: u16, -bg: u16, -border_fg: u16, +fg: u32, +bg: u32, +border_fg: u32, box_chars: struct { left_up: u32, left_down: u32, @@ -170,7 +170,7 @@ pub fn drawLabel(self: TerminalBuffer, text: []const u8, x: usize, y: usize) voi drawColorLabel(text, x, y, self.fg, self.bg); } -pub fn drawColorLabel(text: []const u8, x: usize, y: usize, fg: u16, bg: u16) void { +pub fn drawColorLabel(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(); diff --git a/src/tui/components/InfoLine.zig b/src/tui/components/InfoLine.zig index a43b083..05d421c 100644 --- a/src/tui/components/InfoLine.zig +++ b/src/tui/components/InfoLine.zig @@ -12,8 +12,8 @@ const InfoLine = @This(); const Message = struct { width: u8, text: []const u8, - bg: u16, - fg: u16, + bg: u32, + fg: u32, }; label: MessageLabel, @@ -28,7 +28,7 @@ pub fn deinit(self: InfoLine) void { self.label.deinit(); } -pub fn addMessage(self: *InfoLine, text: []const u8, bg: u16, fg: u16) !void { +pub fn addMessage(self: *InfoLine, text: []const u8, bg: u32, fg: u32) !void { if (text.len == 0) return; try self.label.addItem(.{ diff --git a/src/tui/utils.zig b/src/tui/utils.zig index 43c3619..ace54a6 100644 --- a/src/tui/utils.zig +++ b/src/tui/utils.zig @@ -5,11 +5,11 @@ const termbox = interop.termbox; pub const Cell = struct { ch: u32, - fg: u16, - bg: u16, + fg: u32, + bg: u32, }; -pub fn initCell(ch: u32, fg: u16, bg: u16) Cell { +pub fn initCell(ch: u32, fg: u32, bg: u32) Cell { return .{ .ch = ch, .fg = fg,