From fb135afe1634e91e456deba7d19ef76b71884c0d Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Fri, 10 Apr 2026 12:05:34 +0200 Subject: [PATCH 01/12] feat: add retro82 theme --- build.zig.zon | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.zig.zon b/build.zig.zon index 10ce37ec..24cdbfd1 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -22,8 +22,8 @@ .hash = "thespian-0.0.1-owFOjlgiBgC8w4XqkCOegxz5vMy6kNErcssWQWf2QHeE", }, .themes = .{ - .url = "https://github.com/neurocyte/flow-themes/releases/download/master-80ac47f701dc4fab73dafb5c0da10b0c9e395052/flow-themes.tar.gz", - .hash = "N-V-__8AAIjCKwDJwoOzcI3233UAVqIaOTOOLMgQK1FnFDfx", + .url = "https://github.com/neurocyte/flow-themes/releases/download/master-9114e2d9ff4e01064ba1da4f780b0c5448b0f0ef/flow-themes.tar.gz", + .hash = "N-V-__8AAJZfLAAP6zSuW13nEIrBBHZM9pbeMRwhvQ-8qqx1", }, .fuzzig = .{ .url = "https://github.com/fjebaker/fuzzig/archive/4251fe4230d38e721514394a485db62ee1667ff3.tar.gz", From 76ed87f87ba43fe2da32ab33293b01321b7720f7 Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Fri, 10 Apr 2026 12:16:24 +0200 Subject: [PATCH 02/12] refactor(gui): set background color to match theme --- src/gui/wio/app.zig | 22 ++++++++++++++++++++++ src/renderer/gui/renderer.zig | 2 +- 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/src/gui/wio/app.zig b/src/gui/wio/app.zig index 02cf70b5..cc08edff 100644 --- a/src/gui/wio/app.zig +++ b/src/gui/wio/app.zig @@ -55,6 +55,11 @@ var font_backend: gpu.RasterizerBackend = .freetype; var font_dirty: std.atomic.Value(bool) = .init(true); var stop_requested: std.atomic.Value(bool) = .init(false); +// Background color (written from TUI thread, applied by wio thread on each paint). +// Stored as packed RGBA u32 to allow atomic reads/writes. +var background_color: std.atomic.Value(u32) = .init(0x131313ff); // matches gpu.zig default +var background_dirty: std.atomic.Value(bool) = .init(false); + var config_arena_instance: std.heap.ArenaAllocator = std.heap.ArenaAllocator.init(std.heap.page_allocator); const config_arena = config_arena_instance.allocator(); @@ -311,6 +316,13 @@ fn saveConfig() void { log.err("failed to write gui config file", .{}); } +pub fn setBackground(color: gpu.Color) void { + const color_u32: u32 = (@as(u32, color.r) << 24) | (@as(u32, color.g) << 16) | (@as(u32, color.b) << 8) | color.a; + background_color.store(color_u32, .release); + background_dirty.store(true, .release); + wio.cancelWait(); +} + pub fn requestAttention() void { attention_pending.store(true, .release); wio.cancelWait(); @@ -635,6 +647,16 @@ fn wioLoop() void { state.size = .{ .x = win_size.width, .y = win_size.height }; const font = wio_font; + if (background_dirty.swap(false, .acq_rel)) { + const color_u32 = background_color.load(.acquire); + gpu.setBackground(.{ + .r = @truncate(color_u32 >> 24), + .g = @truncate(color_u32 >> 16), + .b = @truncate(color_u32 >> 8), + .a = @truncate(color_u32), + }); + } + // Regenerate glyph indices using the GPU state. // For double-wide characters vaxis emits width=2 for the left // cell and width=0 (continuation) for the right cell. The diff --git a/src/renderer/gui/renderer.zig b/src/renderer/gui/renderer.zig index 5a645703..fffce5db 100644 --- a/src/renderer/gui/renderer.zig +++ b/src/renderer/gui/renderer.zig @@ -409,7 +409,7 @@ pub fn set_terminal_title(self: *Self, text: []const u8) void { pub fn set_terminal_style(self: *Self, style_: Style) void { _ = self; - _ = style_; + if (style_.bg) |bg| app.setBackground(themeColorToGpu(bg)); } pub fn adjust_fontsize(self: *Self, amount: f32) void { From 347ce61f5dad33f472666dea9500f47066d792dc Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Fri, 10 Apr 2026 12:17:47 +0200 Subject: [PATCH 03/12] refactor(gui): move RGBA to color module --- build.zig | 21 ++++++++++++++---- src/color.zig | 41 +++++++++++++++++++++++++++++++++++ src/gui/Cell.zig | 20 ----------------- src/gui/cell.zig | 9 ++++++++ src/gui/gpu/gpu.zig | 39 ++++++++++++--------------------- src/gui/wio/app.zig | 22 ++++++------------- src/renderer/gui/renderer.zig | 7 +++--- test/tests_color.zig | 17 +++++++++++++++ 8 files changed, 109 insertions(+), 67 deletions(-) delete mode 100644 src/gui/Cell.zig create mode 100644 src/gui/cell.zig diff --git a/build.zig b/build.zig index b2c46749..44cc96ef 100644 --- a/build.zig +++ b/build.zig @@ -492,7 +492,12 @@ pub fn build_exe( const win32_dep = b.lazyDependency("win32", .{}) orelse break :blk tui_renderer_mod; const win32_mod = win32_dep.module("win32"); const gui_xy_mod = b.createModule(.{ .root_source_file = b.path("src/gui/xy.zig") }); - const gui_cell_mod = b.createModule(.{ .root_source_file = b.path("src/gui/Cell.zig") }); + const gui_cell_mod = b.createModule(.{ + .root_source_file = b.path("src/gui/cell.zig"), + .imports = &.{ + .{ .name = "color", .module = color_mod }, + }, + }); const gui_glyph_cache_mod = b.createModule(.{ .root_source_file = b.path("src/gui/GlyphIndexCache.zig") }); const gui_xterm_mod = b.createModule(.{ .root_source_file = b.path("src/gui/xterm.zig") }); const gui_mod = b.createModule(.{ @@ -509,7 +514,7 @@ pub fn build_exe( .{ .name = "gui_config", .module = gui_config_mod }, .{ .name = "tracy", .module = tracy_mod }, .{ .name = "xy", .module = gui_xy_mod }, - .{ .name = "Cell", .module = gui_cell_mod }, + .{ .name = "cell", .module = gui_cell_mod }, .{ .name = "GlyphIndexCache", .module = gui_glyph_cache_mod }, .{ .name = "xterm", .module = gui_xterm_mod }, }, @@ -550,7 +555,12 @@ pub fn build_exe( const sokol_mod = sokol_dep.module("sokol"); const gui_xy_mod = b.createModule(.{ .root_source_file = b.path("src/gui/xy.zig") }); - const gui_cell_mod = b.createModule(.{ .root_source_file = b.path("src/gui/Cell.zig") }); + const gui_cell_mod = b.createModule(.{ + .root_source_file = b.path("src/gui/cell.zig"), + .imports = &.{ + .{ .name = "color", .module = color_mod }, + }, + }); const gui_glyph_cache_mod = b.createModule(.{ .root_source_file = b.path("src/gui/GlyphIndexCache.zig") }); const gui_xterm_mod = b.createModule(.{ .root_source_file = b.path("src/gui/xterm.zig") }); @@ -609,10 +619,11 @@ pub fn build_exe( const gpu_mod = b.createModule(.{ .root_source_file = b.path("src/gui/gpu/gpu.zig"), .imports = &.{ + .{ .name = "color", .module = color_mod }, .{ .name = "sokol", .module = sokol_mod }, .{ .name = "rasterizer", .module = combined_rasterizer_mod }, .{ .name = "xy", .module = gui_xy_mod }, - .{ .name = "Cell", .module = gui_cell_mod }, + .{ .name = "cell", .module = gui_cell_mod }, .{ .name = "GlyphIndexCache", .module = gui_glyph_cache_mod }, }, }); @@ -620,6 +631,7 @@ pub fn build_exe( const app_mod = b.createModule(.{ .root_source_file = b.path("src/gui/wio/app.zig"), .imports = &.{ + .{ .name = "color", .module = color_mod }, .{ .name = "wio", .module = wio_mod }, .{ .name = "sokol", .module = sokol_mod }, .{ .name = "gpu", .module = gpu_mod }, @@ -635,6 +647,7 @@ pub fn build_exe( const mod = b.createModule(.{ .root_source_file = b.path("src/renderer/gui/renderer.zig"), .imports = &.{ + .{ .name = "color", .module = color_mod }, .{ .name = "theme", .module = themes_dep.module("theme") }, .{ .name = "cbor", .module = cbor_mod }, .{ .name = "thespian", .module = thespian_mod }, diff --git a/src/color.zig b/src/color.zig index c97c300e..e978fe96 100644 --- a/src/color.zig +++ b/src/color.zig @@ -106,6 +106,47 @@ pub const RGBf = struct { const GAMMA = 2.4; }; +// Packed RGBA color type used by GPUs +pub const RGBA = packed struct(u32) { + a: u8, + b: u8, + g: u8, + r: u8, + + pub fn init(r: u8, g: u8, b: u8, a: u8) RGBA { + return .{ .r = r, .g = g, .b = b, .a = a }; + } + + pub fn from_RGB(v: RGB) RGBA { + return .{ .r = v.r, .g = v.g, .b = v.b, .a = 255 }; + } + + pub inline fn from_u32(v: u32) RGBA { + return @bitCast(v); + } + + pub inline fn from_u24(v: u24) RGBA { + return from_RGB(RGB.from_u24(v)); + } + + pub inline fn from_u8s(v: [3]u8) RGBA { + return .{ .r = v[0], .g = v[1], .b = v[2], .a = 255 }; + } + + pub inline fn to_u32(v: RGBA) u32 { + return @bitCast(v); + } + + pub fn to_vec4(c: RGBA) [4]f32 { + return .{ + @as(f32, @floatFromInt(c.r)) / 255.0, + @as(f32, @floatFromInt(c.g)) / 255.0, + @as(f32, @floatFromInt(c.b)) / 255.0, + @as(f32, @floatFromInt(c.a)) / 255.0, + }; + } +}; + pub fn u24_to_u8s(v: u24) [3]u8 { return .{ @truncate(v >> 16), @truncate(v >> 8), @truncate(v) }; } diff --git a/src/gui/Cell.zig b/src/gui/Cell.zig deleted file mode 100644 index 242d534a..00000000 --- a/src/gui/Cell.zig +++ /dev/null @@ -1,20 +0,0 @@ -// Shared GPU cell types used by all GPU renderer backends. - -pub const Rgba8 = packed struct(u32) { - a: u8, - b: u8, - g: u8, - r: u8, - pub fn initRgb(r: u8, g: u8, b: u8) Rgba8 { - return .{ .r = r, .g = g, .b = b, .a = 255 }; - } - pub fn initRgba(r: u8, g: u8, b: u8, a: u8) Rgba8 { - return .{ .r = r, .g = g, .b = b, .a = a }; - } -}; - -pub const Cell = extern struct { - glyph_index: u32, - background: Rgba8, - foreground: Rgba8, -}; diff --git a/src/gui/cell.zig b/src/gui/cell.zig new file mode 100644 index 00000000..39e8b502 --- /dev/null +++ b/src/gui/cell.zig @@ -0,0 +1,9 @@ +// Shared GPU cell types used by all GPU renderer backends. + +const RGBA = @import("color").RGBA; + +pub const Cell = extern struct { + glyph_index: u32, + background: RGBA, + foreground: RGBA, +}; diff --git a/src/gui/gpu/gpu.zig b/src/gui/gpu/gpu.zig index 7c75b181..fb23273e 100644 --- a/src/gui/gpu/gpu.zig +++ b/src/gui/gpu/gpu.zig @@ -7,16 +7,14 @@ const std = @import("std"); const sg = @import("sokol").gfx; const Rasterizer = @import("rasterizer"); const GlyphIndexCache = @import("GlyphIndexCache"); -const gui_cell = @import("Cell"); const XY = @import("xy").XY; const builtin_shader = @import("builtin.glsl.zig"); pub const Font = Rasterizer.Font; pub const GlyphKind = Rasterizer.GlyphKind; pub const RasterizerBackend = Rasterizer.Backend; -pub const Cell = gui_cell.Cell; -pub const Color = gui_cell.Rgba8; -const Rgba8 = gui_cell.Rgba8; +pub const Cell = @import("cell").Cell; +pub const RGBA = @import("color").RGBA; pub const CursorShape = enum(i32) { block = 0, beam = 1, underline = 2 }; @@ -25,7 +23,7 @@ pub const CursorInfo = struct { row: u16 = 0, col: u16 = 0, shape: CursorShape = .block, - color: Color = Color.initRgb(255, 255, 255), + color: RGBA = .init(255, 255, 255, 255), }; const log = std.log.scoped(.gpu); @@ -44,7 +42,7 @@ fn getAtlasCellCount(cell_size: XY(u16)) XY(u16) { // Shader cell layout for the RGBA32UI cell texture. // Each texel encodes one terminal cell: // .r = glyph_index (u32) -// .g = bg packed (Rgba8 bit-cast to u32: r<<24|g<<16|b<<8|a) +// .g = bg packed (RGBA bit-cast to u32: r<<24|g<<16|b<<8|a) // .b = fg packed (same) // .a = 0 (reserved) const ShaderCell = extern struct { @@ -61,7 +59,7 @@ const global = struct { var glyph_sampler: sg.Sampler = .{}; var cell_sampler: sg.Sampler = .{}; var glyph_cache_arena: std.heap.ArenaAllocator = undefined; - var background: Rgba8 = .{ .r = 19, .g = 19, .b = 19, .a = 255 }; + var background: RGBA = .init(0, 255, 255, 255); // default is warning yellow }; pub fn init(allocator: std.mem.Allocator) !void { @@ -118,7 +116,7 @@ pub fn setFontWeight(font: *Font, w: u8) void { Rasterizer.setFontWeight(font, w); } -pub fn setBackground(color: Rgba8) void { +pub fn setBackground(color: RGBA) void { global.background = color; } @@ -387,15 +385,15 @@ pub fn paint( const src = cells[src_row_offset + ci]; shader_cells[dst_row_offset + ci] = .{ .glyph_index = src.glyph_index, - .bg = @bitCast(src.background), - .fg = @bitCast(src.foreground), + .bg = src.background.to_u32(), + .fg = src.foreground.to_u32(), }; } for (copy_len..shader_col_count) |ci| { shader_cells[dst_row_offset + ci] = .{ .glyph_index = blank_glyph_index, - .bg = @bitCast(global.background), - .fg = @bitCast(global.background), + .bg = global.background.to_u32(), + .fg = global.background.to_u32(), }; } } @@ -448,10 +446,10 @@ pub fn paint( bindings.samplers[1] = global.cell_sampler; sg.applyBindings(bindings); - const sec_color: Color = if (secondary_cursors.len > 0) + const sec_color: RGBA = if (secondary_cursors.len > 0) secondary_cursors[0].color else - Color.initRgb(255, 255, 255); + .init(255, 255, 255, 255); const fs_params = builtin_shader.FsParams{ .cell_size_x = font.cell_size.x, @@ -463,8 +461,8 @@ pub fn paint( .cursor_row = cursor.row, .cursor_shape = @intFromEnum(cursor.shape), .cursor_vis = if (cursor.vis) 1 else 0, - .cursor_color = colorToVec4(cursor.color), - .sec_cursor_color = colorToVec4(sec_color), + .cursor_color = cursor.color.to_vec4(), + .sec_cursor_color = sec_color.to_vec4(), }; sg.applyUniforms(0, .{ .ptr = &fs_params, @@ -476,15 +474,6 @@ pub fn paint( // Note: caller (app.zig) calls sg.commit() and window.swapBuffers() } -fn colorToVec4(c: Color) [4]f32 { - return .{ - @as(f32, @floatFromInt(c.r)) / 255.0, - @as(f32, @floatFromInt(c.g)) / 255.0, - @as(f32, @floatFromInt(c.b)) / 255.0, - @as(f32, @floatFromInt(c.a)) / 255.0, - }; -} - fn oom(e: error{OutOfMemory}) noreturn { @panic(@errorName(e)); } diff --git a/src/gui/wio/app.zig b/src/gui/wio/app.zig index cc08edff..0cdd97c8 100644 --- a/src/gui/wio/app.zig +++ b/src/gui/wio/app.zig @@ -15,6 +15,7 @@ const gpu = @import("gpu"); const thespian = @import("thespian"); const cbor = @import("cbor"); const vaxis = @import("vaxis"); +const RGBA = @import("color").RGBA; const input_translate = @import("input.zig"); const root = @import("soft_root").root; @@ -26,7 +27,6 @@ const log = std.log.scoped(.wio_app); // can use them without a direct dependency on the gpu module. pub const CursorInfo = gpu.CursorInfo; pub const CursorShape = gpu.CursorShape; -pub const GpuColor = gpu.Color; // ── Shared state (protected by screen_mutex) ────────────────────────────── @@ -57,7 +57,7 @@ var stop_requested: std.atomic.Value(bool) = .init(false); // Background color (written from TUI thread, applied by wio thread on each paint). // Stored as packed RGBA u32 to allow atomic reads/writes. -var background_color: std.atomic.Value(u32) = .init(0x131313ff); // matches gpu.zig default +var background_color: std.atomic.Value(u32) = .init(RGBA.init(0, 255, 255, 255).to_u32()); // warning yellow, we should never see the default var background_dirty: std.atomic.Value(bool) = .init(false); var config_arena_instance: std.heap.ArenaAllocator = std.heap.ArenaAllocator.init(std.heap.page_allocator); @@ -316,7 +316,7 @@ fn saveConfig() void { log.err("failed to write gui config file", .{}); } -pub fn setBackground(color: gpu.Color) void { +pub fn setBackground(color: RGBA) void { const color_u32: u32 = (@as(u32, color.r) << 24) | (@as(u32, color.g) << 16) | (@as(u32, color.b) << 8) | color.a; background_color.store(color_u32, .release); background_dirty.store(true, .release); @@ -371,19 +371,11 @@ fn maybeReloadFont(win_size: wio.Size, state: *gpu.WindowState, cell_width: *u16 } } -fn colorFromVaxis(color: vaxis.Cell.Color) gpu.Color { +fn colorFromVaxis(color: vaxis.Cell.Color) RGBA { return switch (color) { - .default => gpu.Color.initRgb(0, 0, 0), - .index => |idx| blk: { - const xterm = @import("xterm"); - const rgb24 = xterm.colors[idx]; - break :blk gpu.Color.initRgb( - @truncate(rgb24 >> 16), - @truncate(rgb24 >> 8), - @truncate(rgb24), - ); - }, - .rgb => |rgb| gpu.Color.initRgb(rgb[0], rgb[1], rgb[2]), + .default => .init(0, 0, 0, 255), + .index => |idx| .from_u24(@import("xterm").colors[idx]), + .rgb => |rgb| .from_u8s(rgb), }; } diff --git a/src/renderer/gui/renderer.zig b/src/renderer/gui/renderer.zig index fffce5db..4bbd1aff 100644 --- a/src/renderer/gui/renderer.zig +++ b/src/renderer/gui/renderer.zig @@ -3,6 +3,7 @@ pub const log_name = "renderer"; const std = @import("std"); const cbor = @import("cbor"); +const RGBA = @import("color").RGBA; pub const vaxis = @import("vaxis"); const Style = @import("theme").Style; const Color = @import("theme").Color; @@ -57,9 +58,9 @@ thread: ?std.Thread = null, window_ready: bool = false, cursor_info: app.CursorInfo = .{}, -cursor_color: app.GpuColor = app.GpuColor.initRgb(255, 255, 255), +cursor_color: RGBA = .init(255, 255, 255, 255), secondary_cursors: std.ArrayListUnmanaged(app.CursorInfo) = .{}, -secondary_color: app.GpuColor = app.GpuColor.initRgb(255, 255, 255), +secondary_color: RGBA = .init(255, 255, 255, 255), cursor_blink: bool = false, blink_on: bool = true, @@ -545,7 +546,7 @@ pub fn show_multi_cursor_yx(self: *Self, y: i32, x: i32) !void { }); } -fn themeColorToGpu(color: Color) app.GpuColor { +fn themeColorToGpu(color: Color) RGBA { return .{ .r = @truncate(color.color >> 16), .g = @truncate(color.color >> 8), diff --git a/test/tests_color.zig b/test/tests_color.zig index e9843773..cb99179e 100644 --- a/test/tests_color.zig +++ b/test/tests_color.zig @@ -2,6 +2,7 @@ const std = @import("std"); const color = @import("color"); const RGB = color.RGB; +const RGBA = color.RGBA; const rgb = RGB.from_u24; test "contrast white/yellow" { @@ -41,3 +42,19 @@ test "best contrast black/white to blue" { const best = color.max_contrast(0x0000FF, 0xFFFFFF, 0x000000); try std.testing.expectEqual(best, 0xFFFFFF); } + +test "verify RGBA byte order" { + const v1: RGBA = .init(0xA, 0xB, 0xC, 0xD); + const v1_u32: u32 = @bitCast(v1); + const v2: RGBA = .{ + .r = @truncate(v1_u32 >> 24), + .g = @truncate(v1_u32 >> 16), + .b = @truncate(v1_u32 >> 8), + .a = @truncate(v1_u32), + }; + const v3: RGBA = @bitCast(v1_u32); + + const testing = @import("std").testing; + try testing.expectEqual(v1, v2); + try testing.expectEqual(v1, v3); +} From 8190380e79271ce3aadecfab5930907b70fee7fb Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Fri, 10 Apr 2026 13:48:45 +0200 Subject: [PATCH 04/12] fix(gui): send capability_detection_complete events from gui --- src/renderer/gui/renderer.zig | 1 + 1 file changed, 1 insertion(+) diff --git a/src/renderer/gui/renderer.zig b/src/renderer/gui/renderer.zig index 4bbd1aff..efbb1c05 100644 --- a/src/renderer/gui/renderer.zig +++ b/src/renderer/gui/renderer.zig @@ -379,6 +379,7 @@ pub fn process_renderer_event(self: *Self, msg: []const u8) Error!void { })) { self.window_ready = true; self.dispatch_initialized(self.handler_ctx); + if (self.dispatch_event) |f| f(self.handler_ctx, try self.fmtmsg(.{"capability_detection_complete"})); return; } } From be4f939b268b7569633dc3968a2cfe8898978781 Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Fri, 10 Apr 2026 13:50:19 +0200 Subject: [PATCH 05/12] refactor(gui): fill background outside of grid --- src/gui/gpu/builtin.glsl.zig | 6 +++++- src/gui/gpu/gpu.zig | 1 + 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/gui/gpu/builtin.glsl.zig b/src/gui/gpu/builtin.glsl.zig index eb1b3873..5a89dcf4 100644 --- a/src/gui/gpu/builtin.glsl.zig +++ b/src/gui/gpu/builtin.glsl.zig @@ -23,6 +23,8 @@ pub const FsParams = extern struct { cursor_color: [4]f32, // RGBA normalized [0,1] // Secondary cursor colour (positions encoded in ShaderCell._pad) sec_cursor_color: [4]f32, // RGBA normalized [0,1] + // Background fill for border area outside the cell grid + bg_color: [4]f32, // RGBA normalized [0,1] }; const vs_src = @@ -48,6 +50,7 @@ const fs_src = \\uniform int cursor_vis; \\uniform vec4 cursor_color; \\uniform vec4 sec_cursor_color; + \\uniform vec4 bg_color; \\uniform sampler2D glyph_tex_glyph_smp; \\uniform usampler2D cell_tex_cell_smp; \\out vec4 frag_color; @@ -69,7 +72,7 @@ const fs_src = \\ int row = py / cell_size_y; \\ \\ if (col >= col_count || row >= row_count || row < 0 || col < 0) { - \\ frag_color = vec4(0.0, 0.0, 0.0, 1.0); + \\ frag_color = vec4(bg_color.rgb, 1.0); \\ return; \\ } \\ @@ -142,6 +145,7 @@ pub fn shaderDesc(backend: sg.Backend) sg.ShaderDesc { desc.uniform_blocks[0].glsl_uniforms[8] = .{ .type = .INT, .glsl_name = "cursor_vis" }; desc.uniform_blocks[0].glsl_uniforms[9] = .{ .type = .FLOAT4, .glsl_name = "cursor_color" }; desc.uniform_blocks[0].glsl_uniforms[10] = .{ .type = .FLOAT4, .glsl_name = "sec_cursor_color" }; + desc.uniform_blocks[0].glsl_uniforms[11] = .{ .type = .FLOAT4, .glsl_name = "bg_color" }; // Glyph atlas texture: R8 → sample_type = FLOAT desc.views[0].texture = .{ diff --git a/src/gui/gpu/gpu.zig b/src/gui/gpu/gpu.zig index fb23273e..66860182 100644 --- a/src/gui/gpu/gpu.zig +++ b/src/gui/gpu/gpu.zig @@ -463,6 +463,7 @@ pub fn paint( .cursor_vis = if (cursor.vis) 1 else 0, .cursor_color = cursor.color.to_vec4(), .sec_cursor_color = sec_color.to_vec4(), + .bg_color = global.background.to_vec4(), }; sg.applyUniforms(0, .{ .ptr = &fs_params, From 36ac0024818fd52a051bd507cbd1ba45a3d14f36 Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Fri, 10 Apr 2026 13:50:50 +0200 Subject: [PATCH 06/12] feat: bind f11 to show_logview And remove hidden alt+l binding. --- src/keybind/builtin/flow.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/keybind/builtin/flow.json b/src/keybind/builtin/flow.json index 4469e89c..6c5ab7de 100644 --- a/src/keybind/builtin/flow.json +++ b/src/keybind/builtin/flow.json @@ -46,7 +46,6 @@ ["alt+shift+p", "open_command_palette"], ["alt+n", "goto_next_file_or_diagnostic"], ["alt+p", "goto_prev_file_or_diagnostic"], - ["alt+l", "toggle_panel"], ["alt+i", "toggle_inputview"], ["alt+k", "toggle_keybindview"], ["alt+x", "open_command_palette"], @@ -56,7 +55,7 @@ ["ctrl+k ctrl+s", "insert_command_name"], ["f9", "theme_prev"], ["f10", "theme_next"], - ["f11", "toggle_panel"], + ["f11", "show_logview"], ["alt+f11", "toggle_color_scheme"], ["alt+f9", "panel_next_widget_style"], ["shift+alt+f9", "hint_window_next_widget_style"], From 80988915d6ac1794f51d0993d7628af03ddcdb3c Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Fri, 10 Apr 2026 13:51:49 +0200 Subject: [PATCH 07/12] fix(gui): map vaxis .default color to background color --- src/gui/gpu/gpu.zig | 4 ++++ src/gui/wio/app.zig | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/gui/gpu/gpu.zig b/src/gui/gpu/gpu.zig index 66860182..fefd8a29 100644 --- a/src/gui/gpu/gpu.zig +++ b/src/gui/gpu/gpu.zig @@ -120,6 +120,10 @@ pub fn setBackground(color: RGBA) void { global.background = color; } +pub fn getBackground() RGBA { + return global.background; +} + // ── WindowState ──────────────────────────────────────────────────────────── pub const WindowState = struct { diff --git a/src/gui/wio/app.zig b/src/gui/wio/app.zig index 0cdd97c8..3fbead22 100644 --- a/src/gui/wio/app.zig +++ b/src/gui/wio/app.zig @@ -373,7 +373,7 @@ fn maybeReloadFont(win_size: wio.Size, state: *gpu.WindowState, cell_width: *u16 fn colorFromVaxis(color: vaxis.Cell.Color) RGBA { return switch (color) { - .default => .init(0, 0, 0, 255), + .default => gpu.getBackground(), .index => |idx| .from_u24(@import("xterm").colors[idx]), .rgb => |rgb| .from_u8s(rgb), }; From cde7e4c6f50feedfccf8364db08b8fe46011afcc Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Fri, 10 Apr 2026 15:46:39 +0200 Subject: [PATCH 08/12] refactor: add tui.active_color_scheme() method --- src/tui/tui.zig | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/tui/tui.zig b/src/tui/tui.zig index a4637eb2..40beb7e7 100644 --- a/src/tui/tui.zig +++ b/src/tui/tui.zig @@ -85,7 +85,7 @@ query_cache_: *syntax.QueryCache, frames_rendered_: usize = 0, clipboard: ?std.ArrayList(ClipboardEntry) = null, clipboard_current_group_number: usize = 0, -color_scheme: enum { dark, light } = .dark, +color_scheme: ColorScheme = .dark, color_scheme_locked: bool = false, hint_mode: HintMode = .prefix, last_palette: ?LastPalette = null, @@ -97,6 +97,8 @@ jump_mode_: bool = false, auto_run_timer: ?tp.Cancellable = null, +pub const ColorScheme = enum { dark, light }; + const HintMode = enum { none, prefix, all }; const LastPalette = struct { @@ -1068,14 +1070,14 @@ fn set_theme_by_name(self: *Self, name: []const u8, action: enum { none, store } } } -fn force_color_scheme(self: *Self, color_scheme: @TypeOf(self.color_scheme)) void { +fn force_color_scheme(self: *Self, color_scheme: ColorScheme) void { self.color_scheme = color_scheme; self.color_scheme_locked = true; self.set_terminal_style(self.current_theme()); self.logger.print("color scheme: {t} ({s})", .{ self.color_scheme, self.current_theme().name }); } -fn set_color_scheme(self: *Self, color_scheme: @TypeOf(self.color_scheme)) void { +fn set_color_scheme(self: *Self, color_scheme: ColorScheme) void { if (self.color_scheme_locked) return; self.color_scheme = color_scheme; self.set_terminal_style(self.current_theme()); @@ -1931,6 +1933,10 @@ pub fn config() *const @import("config") { return ¤t().config_; } +pub fn active_color_scheme() ColorScheme { + return current().color_scheme; +} + pub fn get_tab_width() usize { const self = current(); return self.session_tab_width orelse self.config_.tab_width; From a6b3da2d169d9446e16c6a806f2d1c1fc20eb4f7 Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Fri, 10 Apr 2026 15:47:05 +0200 Subject: [PATCH 09/12] feat(terminal): export COLORTERM and COLORFGBG evn vars --- src/tui/terminal_view.zig | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/tui/terminal_view.zig b/src/tui/terminal_view.zig index eadb52ab..1570f899 100644 --- a/src/tui/terminal_view.zig +++ b/src/tui/terminal_view.zig @@ -76,6 +76,12 @@ pub fn run_cmd(self: *Self, ctx: command.Context) !void { errdefer env.deinit(); if (env.get("TERM") == null) try env.put("TERM", "xterm-256color"); + try env.put("COLORTERM", "truecolor"); + // COLORFGBG tells apps whether the terminal background is dark or light + try env.put("COLORFGBG", switch (tui.active_color_scheme()) { + .dark => "15;0", + .light => "0;15", + }); var cmd_arg: []const u8 = ""; var on_exit: TerminalOnExit = tui.config().terminal_on_exit; From e01d9c13b776b4339aeed0daabc0be0da134a369 Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Fri, 10 Apr 2026 21:34:44 +0200 Subject: [PATCH 10/12] refactor: add tui.active_theme() function --- src/tui/tui.zig | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/tui/tui.zig b/src/tui/tui.zig index 40beb7e7..ef919976 100644 --- a/src/tui/tui.zig +++ b/src/tui/tui.zig @@ -1933,6 +1933,10 @@ pub fn config() *const @import("config") { return ¤t().config_; } +pub fn active_theme() *const Widget.Theme { + return current().current_theme(); +} + pub fn active_color_scheme() ColorScheme { return current().color_scheme; } From b1cc9193fe9a7f29da91a1df36ecc0301ce9dfdf Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Fri, 10 Apr 2026 21:35:33 +0200 Subject: [PATCH 11/12] fix(terminal): set vt fg & bg colors from theme --- src/tui/terminal_view.zig | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/tui/terminal_view.zig b/src/tui/terminal_view.zig index 1570f899..ee5476f7 100644 --- a/src/tui/terminal_view.zig +++ b/src/tui/terminal_view.zig @@ -537,6 +537,10 @@ const Vt = struct { &self.write_buf, ); + const theme = tui.active_theme(); + if (theme.editor.fg) |fg| self.vt.fg_color = color.u24_to_u8s(fg.color); + if (theme.editor.bg) |bg| self.vt.bg_color = color.u24_to_u8s(bg.color); + try self.vt.spawn(); self.pty_pid = try pty.spawn(allocator, &self.vt); } From 4391d6d0f0b92288d8747de452d6638720ac66c8 Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Fri, 10 Apr 2026 21:35:57 +0200 Subject: [PATCH 12/12] build: update libvaxis to close fds on spawn --- build.zig.zon | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.zig.zon b/build.zig.zon index b7f0ab08..3495f0fb 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -30,8 +30,8 @@ .hash = "fuzzig-0.1.1-Ji0xivxIAQBD0g8O_NV_0foqoPf3elsg9Sc3pNfdVH4D", }, .vaxis = .{ - .url = "git+https://github.com/neurocyte/libvaxis?ref=main#cecc97d9ff8da9df13499da0d0b19c5cd18742c3", - .hash = "vaxis-0.5.1-BWNV_BcgCgDG3wpSPxCHxaRAZukEfnnKrBa-52zjnjex", + .url = "git+https://github.com/neurocyte/libvaxis?ref=main#9fc042aa0fc8b08d15fce1c897bc65ed4a4c65ed", + .hash = "vaxis-0.5.1-BWNV_GgiCgCimbrqWwG7XRgYXNFWFBcH9dTmxx-8Gs-2", }, .zeit = .{ .url = "git+https://github.com/rockorager/zeit?ref=zig-0.15#ed2ca60db118414bda2b12df2039e33bad3b0b88",