diff --git a/src/font/shaper/harfbuzz.zig b/src/font/shaper/harfbuzz.zig index 4a3613276..cc36bcec4 100644 --- a/src/font/shaper/harfbuzz.zig +++ b/src/font/shaper/harfbuzz.zig @@ -260,8 +260,7 @@ test "run iterator: empty cells with background set" { // Make a screen with some data var screen = try terminal.Screen.init(alloc, 3, 5, 0); defer screen.deinit(); - screen.cursor.pen.bg = try terminal.color.Name.cyan.default(); - screen.cursor.pen.attrs.has_bg = true; + screen.cursor.pen.bg = .{ .rgb = try terminal.color.Name.cyan.default() }; try screen.testWriteString("A"); // Get our first row @@ -836,10 +835,9 @@ test "shape cell attribute change" { { var screen = try terminal.Screen.init(alloc, 3, 10, 0); defer screen.deinit(); - screen.cursor.pen.attrs.has_fg = true; - screen.cursor.pen.fg = .{ .r = 1, .g = 2, .b = 3 }; + screen.cursor.pen.fg = .{ .rgb = .{ .r = 1, .g = 2, .b = 3 } }; try screen.testWriteString(">"); - screen.cursor.pen.fg = .{ .r = 3, .g = 2, .b = 1 }; + screen.cursor.pen.fg = .{ .rgb = .{ .r = 3, .g = 2, .b = 1 } }; try screen.testWriteString("="); var shaper = &testdata.shaper; @@ -856,10 +854,9 @@ test "shape cell attribute change" { { var screen = try terminal.Screen.init(alloc, 3, 10, 0); defer screen.deinit(); - screen.cursor.pen.attrs.has_bg = true; - screen.cursor.pen.bg = .{ .r = 1, .g = 2, .b = 3 }; + screen.cursor.pen.bg = .{ .rgb = .{ .r = 1, .g = 2, .b = 3 } }; try screen.testWriteString(">"); - screen.cursor.pen.bg = .{ .r = 3, .g = 2, .b = 1 }; + screen.cursor.pen.bg = .{ .rgb = .{ .r = 3, .g = 2, .b = 1 } }; try screen.testWriteString("="); var shaper = &testdata.shaper; @@ -876,8 +873,7 @@ test "shape cell attribute change" { { var screen = try terminal.Screen.init(alloc, 3, 10, 0); defer screen.deinit(); - screen.cursor.pen.attrs.has_bg = true; - screen.cursor.pen.bg = .{ .r = 1, .g = 2, .b = 3 }; + screen.cursor.pen.bg = .{ .rgb = .{ .r = 1, .g = 2, .b = 3 } }; try screen.testWriteString(">"); try screen.testWriteString("="); diff --git a/src/font/shaper/run.zig b/src/font/shaper/run.zig index 093ffaebe..dd5b93be6 100644 --- a/src/font/shaper/run.zig +++ b/src/font/shaper/run.zig @@ -87,8 +87,8 @@ pub const RunIterator = struct { const prev_attrs: Int = @bitCast(prev_cell.attrs.styleAttrs()); const attrs: Int = @bitCast(cell.attrs.styleAttrs()); if (prev_attrs != attrs) break; - if (cell.attrs.has_bg and !cell.bg.eql(prev_cell.bg)) break; - if (cell.attrs.has_fg and !cell.fg.eql(prev_cell.fg)) break; + if (!cell.bg.eql(prev_cell.bg)) break; + if (!cell.fg.eql(prev_cell.fg)) break; } // Text runs break when font styles change so we need to get diff --git a/src/inspector/Inspector.zig b/src/inspector/Inspector.zig index 36d362e4c..6a4235a7e 100644 --- a/src/inspector/Inspector.zig +++ b/src/inspector/Inspector.zig @@ -299,7 +299,8 @@ fn renderScreenWindow(self: *Inspector) void { ); defer cimgui.c.igEndTable(); - inspector.cursor.renderInTable(&screen.cursor); + const palette = self.surface.io.terminal.color_palette.colors; + inspector.cursor.renderInTable(&screen.cursor, &palette); } // table cimgui.c.igTextDisabled("(Any styles not shown are not currently set)"); @@ -871,49 +872,66 @@ fn renderCellWindow(self: *Inspector) void { } // If we have a color then we show the color - color: { - cimgui.c.igTableNextRow(cimgui.c.ImGuiTableRowFlags_None, 0); - _ = cimgui.c.igTableSetColumnIndex(0); - cimgui.c.igText("Foreground Color"); - _ = cimgui.c.igTableSetColumnIndex(1); - if (!selected.cell.attrs.has_fg) { - cimgui.c.igText("default"); - break :color; - } + cimgui.c.igTableNextRow(cimgui.c.ImGuiTableRowFlags_None, 0); + _ = cimgui.c.igTableSetColumnIndex(0); + cimgui.c.igText("Foreground Color"); + _ = cimgui.c.igTableSetColumnIndex(1); + switch (selected.cell.fg) { + .none => cimgui.c.igText("default"), + else => { + const rgb = switch (selected.cell.fg) { + .none => unreachable, + .indexed => |idx| self.surface.io.terminal.color_palette.colors[idx], + .rgb => |rgb| rgb, + }; - var color: [3]f32 = .{ - @as(f32, @floatFromInt(selected.cell.fg.r)) / 255, - @as(f32, @floatFromInt(selected.cell.fg.g)) / 255, - @as(f32, @floatFromInt(selected.cell.fg.b)) / 255, - }; - _ = cimgui.c.igColorEdit3( - "color_fg", - &color, - cimgui.c.ImGuiColorEditFlags_NoPicker | - cimgui.c.ImGuiColorEditFlags_NoLabel, - ); + if (selected.cell.fg == .indexed) { + cimgui.c.igValue_Int("Palette", selected.cell.fg.indexed); + } + + var color: [3]f32 = .{ + @as(f32, @floatFromInt(rgb.r)) / 255, + @as(f32, @floatFromInt(rgb.g)) / 255, + @as(f32, @floatFromInt(rgb.b)) / 255, + }; + _ = cimgui.c.igColorEdit3( + "color_fg", + &color, + cimgui.c.ImGuiColorEditFlags_NoPicker | + cimgui.c.ImGuiColorEditFlags_NoLabel, + ); + }, } - color: { - cimgui.c.igTableNextRow(cimgui.c.ImGuiTableRowFlags_None, 0); - _ = cimgui.c.igTableSetColumnIndex(0); - cimgui.c.igText("Background Color"); - _ = cimgui.c.igTableSetColumnIndex(1); - if (!selected.cell.attrs.has_bg) { - cimgui.c.igText("default"); - break :color; - } - var color: [3]f32 = .{ - @as(f32, @floatFromInt(selected.cell.bg.r)) / 255, - @as(f32, @floatFromInt(selected.cell.bg.g)) / 255, - @as(f32, @floatFromInt(selected.cell.bg.b)) / 255, - }; - _ = cimgui.c.igColorEdit3( - "color_bg", - &color, - cimgui.c.ImGuiColorEditFlags_NoPicker | - cimgui.c.ImGuiColorEditFlags_NoLabel, - ); + cimgui.c.igTableNextRow(cimgui.c.ImGuiTableRowFlags_None, 0); + _ = cimgui.c.igTableSetColumnIndex(0); + cimgui.c.igText("Background Color"); + _ = cimgui.c.igTableSetColumnIndex(1); + switch (selected.cell.bg) { + .none => cimgui.c.igText("default"), + else => { + const rgb = switch (selected.cell.bg) { + .none => unreachable, + .indexed => |idx| self.surface.io.terminal.color_palette.colors[idx], + .rgb => |rgb| rgb, + }; + + if (selected.cell.bg == .indexed) { + cimgui.c.igValue_Int("Palette", selected.cell.bg.indexed); + } + + var color: [3]f32 = .{ + @as(f32, @floatFromInt(rgb.r)) / 255, + @as(f32, @floatFromInt(rgb.g)) / 255, + @as(f32, @floatFromInt(rgb.b)) / 255, + }; + _ = cimgui.c.igColorEdit3( + "color_bg", + &color, + cimgui.c.ImGuiColorEditFlags_NoPicker | + cimgui.c.ImGuiColorEditFlags_NoLabel, + ); + }, } // Boolean styles @@ -1109,7 +1127,8 @@ fn renderTermioWindow(self: *Inspector) void { 0, ); defer cimgui.c.igEndTable(); - inspector.cursor.renderInTable(&ev.cursor); + const palette = self.surface.io.terminal.color_palette.colors; + inspector.cursor.renderInTable(&ev.cursor, &palette); { cimgui.c.igTableNextRow(cimgui.c.ImGuiTableRowFlags_None, 0); diff --git a/src/inspector/cursor.zig b/src/inspector/cursor.zig index 9991e331a..c1098a7c7 100644 --- a/src/inspector/cursor.zig +++ b/src/inspector/cursor.zig @@ -3,7 +3,10 @@ const cimgui = @import("cimgui"); const terminal = @import("../terminal/main.zig"); /// Render cursor information with a table already open. -pub fn renderInTable(cursor: *const terminal.Screen.Cursor) void { +pub fn renderInTable( + cursor: *const terminal.Screen.Cursor, + palette: *const terminal.color.Palette, +) void { { cimgui.c.igTableNextRow(cimgui.c.ImGuiTableRowFlags_None, 0); { @@ -41,49 +44,66 @@ pub fn renderInTable(cursor: *const terminal.Screen.Cursor) void { } // If we have a color then we show the color - if (cursor.pen.attrs.has_fg) color: { - cimgui.c.igTableNextRow(cimgui.c.ImGuiTableRowFlags_None, 0); - _ = cimgui.c.igTableSetColumnIndex(0); - cimgui.c.igText("Foreground Color"); - _ = cimgui.c.igTableSetColumnIndex(1); - if (!cursor.pen.attrs.has_fg) { - cimgui.c.igText("default"); - break :color; - } + cimgui.c.igTableNextRow(cimgui.c.ImGuiTableRowFlags_None, 0); + _ = cimgui.c.igTableSetColumnIndex(0); + cimgui.c.igText("Foreground Color"); + _ = cimgui.c.igTableSetColumnIndex(1); + switch (cursor.pen.fg) { + .none => cimgui.c.igText("default"), + else => { + const rgb = switch (cursor.pen.fg) { + .none => unreachable, + .indexed => |idx| palette[idx], + .rgb => |rgb| rgb, + }; - var color: [3]f32 = .{ - @as(f32, @floatFromInt(cursor.pen.fg.r)) / 255, - @as(f32, @floatFromInt(cursor.pen.fg.g)) / 255, - @as(f32, @floatFromInt(cursor.pen.fg.b)) / 255, - }; - _ = cimgui.c.igColorEdit3( - "color_fg", - &color, - cimgui.c.ImGuiColorEditFlags_NoPicker | - cimgui.c.ImGuiColorEditFlags_NoLabel, - ); + if (cursor.pen.fg == .indexed) { + cimgui.c.igValue_Int("Palette", cursor.pen.fg.indexed); + } + + var color: [3]f32 = .{ + @as(f32, @floatFromInt(rgb.r)) / 255, + @as(f32, @floatFromInt(rgb.g)) / 255, + @as(f32, @floatFromInt(rgb.b)) / 255, + }; + _ = cimgui.c.igColorEdit3( + "color_fg", + &color, + cimgui.c.ImGuiColorEditFlags_NoPicker | + cimgui.c.ImGuiColorEditFlags_NoLabel, + ); + }, } - if (cursor.pen.attrs.has_bg) color: { - cimgui.c.igTableNextRow(cimgui.c.ImGuiTableRowFlags_None, 0); - _ = cimgui.c.igTableSetColumnIndex(0); - cimgui.c.igText("Background Color"); - _ = cimgui.c.igTableSetColumnIndex(1); - if (!cursor.pen.attrs.has_bg) { - cimgui.c.igText("default"); - break :color; - } - var color: [3]f32 = .{ - @as(f32, @floatFromInt(cursor.pen.bg.r)) / 255, - @as(f32, @floatFromInt(cursor.pen.bg.g)) / 255, - @as(f32, @floatFromInt(cursor.pen.bg.b)) / 255, - }; - _ = cimgui.c.igColorEdit3( - "color_bg", - &color, - cimgui.c.ImGuiColorEditFlags_NoPicker | - cimgui.c.ImGuiColorEditFlags_NoLabel, - ); + cimgui.c.igTableNextRow(cimgui.c.ImGuiTableRowFlags_None, 0); + _ = cimgui.c.igTableSetColumnIndex(0); + cimgui.c.igText("Background Color"); + _ = cimgui.c.igTableSetColumnIndex(1); + switch (cursor.pen.bg) { + .none => cimgui.c.igText("default"), + else => { + const rgb = switch (cursor.pen.bg) { + .none => unreachable, + .indexed => |idx| palette[idx], + .rgb => |rgb| rgb, + }; + + if (cursor.pen.bg == .indexed) { + cimgui.c.igValue_Int("Palette", cursor.pen.bg.indexed); + } + + var color: [3]f32 = .{ + @as(f32, @floatFromInt(rgb.r)) / 255, + @as(f32, @floatFromInt(rgb.g)) / 255, + @as(f32, @floatFromInt(rgb.b)) / 255, + }; + _ = cimgui.c.igColorEdit3( + "color_bg", + &color, + cimgui.c.ImGuiColorEditFlags_NoPicker | + cimgui.c.ImGuiColorEditFlags_NoLabel, + ); + }, } // Boolean styles diff --git a/src/renderer/Metal.zig b/src/renderer/Metal.zig index a606619ab..96b50dea4 100644 --- a/src/renderer/Metal.zig +++ b/src/renderer/Metal.zig @@ -579,6 +579,7 @@ pub fn updateFrame( mouse: renderer.State.Mouse, preedit: ?renderer.State.Preedit, cursor_style: ?renderer.CursorStyle, + color_palette: terminal.color.Palette, }; // Update all our data as tightly as possible within the mutex. @@ -655,6 +656,7 @@ pub fn updateFrame( .mouse = state.mouse, .preedit = preedit, .cursor_style = cursor_style, + .color_palette = state.terminal.color_palette.colors, }; }; defer { @@ -669,6 +671,7 @@ pub fn updateFrame( critical.mouse, critical.preedit, critical.cursor_style, + &critical.color_palette, ); // Update our background color @@ -1424,6 +1427,7 @@ fn rebuildCells( mouse: renderer.State.Mouse, preedit: ?renderer.State.Preedit, cursor_style_: ?renderer.CursorStyle, + color_palette: *const terminal.color.Palette, ) !void { // Bg cells at most will need space for the visible screen size self.cells_bg.clearRetainingCapacity(); @@ -1579,6 +1583,7 @@ fn rebuildCells( term_selection, screen, cell, + color_palette, shaper_cell, run, shaper_cell.x, @@ -1644,11 +1649,12 @@ fn rebuildCells( } } -pub fn updateCell( +fn updateCell( self: *Metal, selection: ?terminal.Selection, screen: *terminal.Screen, cell: terminal.Screen.Cell, + palette: *const terminal.color.Palette, shaper_cell: font.shape.Cell, shaper_run: font.shape.TextRun, x: usize, @@ -1684,14 +1690,30 @@ pub fn updateCell( const cell_res: BgFg = if (!cell.attrs.inverse) .{ // In normal mode, background and fg match the cell. We // un-optionalize the fg by defaulting to our fg color. - .bg = if (cell.attrs.has_bg) cell.bg else null, - .fg = if (cell.attrs.has_fg) cell.fg else self.foreground_color, + .bg = switch (cell.bg) { + .none => null, + .indexed => |i| palette[i], + .rgb => |rgb| rgb, + }, + .fg = switch (cell.fg) { + .none => self.foreground_color, + .indexed => |i| palette[i], + .rgb => |rgb| rgb, + }, } else .{ // In inverted mode, the background MUST be set to something // (is never null) so it is either the fg or default fg. The // fg is either the bg or default background. - .bg = if (cell.attrs.has_fg) cell.fg else self.foreground_color, - .fg = if (cell.attrs.has_bg) cell.bg else self.background_color, + .bg = switch (cell.fg) { + .none => self.foreground_color, + .indexed => |i| palette[i], + .rgb => |rgb| rgb, + }, + .fg = switch (cell.bg) { + .none => self.background_color, + .indexed => |i| palette[i], + .rgb => |rgb| rgb, + }, }; // If we are selected, we our colors are just inverted fg/bg @@ -1741,7 +1763,7 @@ pub fn updateCell( // If we have a background and its not the default background // then we apply background opacity - if (cell.attrs.has_bg and !std.meta.eql(rgb, self.background_color)) { + if (cell.bg != .none and !rgb.eql(self.background_color)) { break :bg_alpha default; } diff --git a/src/renderer/OpenGL.zig b/src/renderer/OpenGL.zig index fd9c1c9b3..699f51943 100644 --- a/src/renderer/OpenGL.zig +++ b/src/renderer/OpenGL.zig @@ -649,6 +649,7 @@ pub fn updateFrame( mouse: renderer.State.Mouse, preedit: ?renderer.State.Preedit, cursor_style: ?renderer.CursorStyle, + color_palette: terminal.color.Palette, }; // Update all our data as tightly as possible within the mutex. @@ -730,6 +731,7 @@ pub fn updateFrame( .mouse = state.mouse, .preedit = preedit, .cursor_style = cursor_style, + .color_palette = state.terminal.color_palette.colors, }; }; defer { @@ -752,6 +754,7 @@ pub fn updateFrame( critical.mouse, critical.preedit, critical.cursor_style, + &critical.color_palette, ); } } @@ -943,6 +946,7 @@ pub fn rebuildCells( mouse: renderer.State.Mouse, preedit: ?renderer.State.Preedit, cursor_style_: ?renderer.CursorStyle, + color_palette: *const terminal.color.Palette, ) !void { const t = trace(@src()); defer t.end(); @@ -1097,6 +1101,7 @@ pub fn rebuildCells( term_selection, screen, cell, + color_palette, shaper_cell, run, shaper_cell.x, @@ -1330,11 +1335,12 @@ fn addCursor( /// Update a single cell. The bool returns whether the cell was updated /// or not. If the cell wasn't updated, a full refreshCells call is /// needed. -pub fn updateCell( +fn updateCell( self: *OpenGL, selection: ?terminal.Selection, screen: *terminal.Screen, cell: terminal.Screen.Cell, + palette: *const terminal.color.Palette, shaper_cell: font.shape.Cell, shaper_run: font.shape.TextRun, x: usize, @@ -1373,14 +1379,30 @@ pub fn updateCell( const cell_res: BgFg = if (!cell.attrs.inverse) .{ // In normal mode, background and fg match the cell. We // un-optionalize the fg by defaulting to our fg color. - .bg = if (cell.attrs.has_bg) cell.bg else null, - .fg = if (cell.attrs.has_fg) cell.fg else self.foreground_color, + .bg = switch (cell.bg) { + .none => null, + .indexed => |i| palette[i], + .rgb => |rgb| rgb, + }, + .fg = switch (cell.fg) { + .none => self.foreground_color, + .indexed => |i| palette[i], + .rgb => |rgb| rgb, + }, } else .{ // In inverted mode, the background MUST be set to something // (is never null) so it is either the fg or default fg. The // fg is either the bg or default background. - .bg = if (cell.attrs.has_fg) cell.fg else self.foreground_color, - .fg = if (cell.attrs.has_bg) cell.bg else self.background_color, + .bg = switch (cell.fg) { + .none => self.foreground_color, + .indexed => |i| palette[i], + .rgb => |rgb| rgb, + }, + .fg = switch (cell.bg) { + .none => self.background_color, + .indexed => |i| palette[i], + .rgb => |rgb| rgb, + }, }; // If we are selected, we our colors are just inverted fg/bg @@ -1441,7 +1463,7 @@ pub fn updateCell( // If we have a background and its not the default background // then we apply background opacity - if (cell.attrs.has_bg and !std.meta.eql(rgb, self.background_color)) { + if (cell.bg != .none and !rgb.eql(self.background_color)) { break :bg_alpha default; } diff --git a/src/terminal/Screen.zig b/src/terminal/Screen.zig index fa0c04352..c7d94156f 100644 --- a/src/terminal/Screen.zig +++ b/src/terminal/Screen.zig @@ -211,6 +211,27 @@ pub const RowHeader = struct { }; }; +/// The color associated with a single cell's foreground or background. +const CellColor = union(enum) { + none, + indexed: u8, + rgb: color.RGB, + + pub fn eql(self: CellColor, other: CellColor) bool { + return switch (self) { + .none => other == .none, + .indexed => |i| switch (other) { + .indexed => other.indexed == i, + else => false, + }, + .rgb => |rgb| switch (other) { + .rgb => other.rgb.eql(rgb), + else => false, + }, + }; + } +}; + /// Cell is a single cell within the screen. pub const Cell = struct { /// The primary unicode codepoint for this cell. Most cells (almost all) @@ -224,10 +245,9 @@ pub const Cell = struct { /// waste memory for every cell, so we use a side lookup for it. char: u32 = 0, - /// Foreground and background color. attrs.has_{bg/fg} must be checked - /// to see if these are useful values. - fg: color.RGB = .{}, - bg: color.RGB = .{}, + /// Foreground and background color. + fg: CellColor = .none, + bg: CellColor = .none, /// Underline color. /// NOTE(mitchellh): This is very rarely set so ideally we wouldn't waste @@ -237,9 +257,6 @@ pub const Cell = struct { /// On/off attributes that can be set attrs: packed struct { - has_bg: bool = false, - has_fg: bool = false, - bold: bool = false, italic: bool = false, faint: bool = false, @@ -286,7 +303,10 @@ pub const Cell = struct { } }); // We're empty if we have no char AND we have no styling - return self.char == 0 and @as(AttrInt, @bitCast(self.attrs)) == 0; + return self.char == 0 and + self.fg == .none and + self.bg == .none and + @as(AttrInt, @bitCast(self.attrs)) == 0; } /// The width of the cell. @@ -1297,9 +1317,9 @@ pub fn scrollRegionUp(self: *Screen, top: RowIndex, bottom: RowIndex, count_req: // The pen we'll use for new cells (only the BG attribute is applied to new // cells) - const pen: Cell = if (!self.cursor.pen.attrs.has_bg) .{} else .{ - .bg = self.cursor.pen.bg, - .attrs = .{ .has_bg = true }, + const pen: Cell = switch (self.cursor.pen.bg) { + .none => .{}, + else => |bg| .{ .bg = bg }, }; // Fast-path is that we have a contiguous buffer in our circular buffer. @@ -2190,9 +2210,9 @@ fn scrollDelta(self: *Screen, delta: isize, viewport_only: bool) Allocator.Error const dst = slices[0]; // The pen we'll use for new cells (only the BG attribute is applied to new // cells) - const pen: Cell = if (!self.cursor.pen.attrs.has_bg) .{} else .{ - .bg = self.cursor.pen.bg, - .attrs = .{ .has_bg = true }, + const pen: Cell = switch (self.cursor.pen.bg) { + .none => .{}, + else => |bg| .{ .bg = bg }, }; @memset(dst, .{ .cell = pen }); @@ -3488,8 +3508,7 @@ test "Row: isEmpty with only styled cells" { const row = s.getRow(.{ .active = 0 }); for (0..s.cols) |x| { const cell = row.getCellPtr(x); - cell.*.bg = .{ .r = 0xAA, .g = 0xBB, .b = 0xCC }; - cell.*.attrs.has_bg = true; + cell.*.bg = .{ .rgb = .{ .r = 0xAA, .g = 0xBB, .b = 0xCC } }; } try testing.expect(row.isEmpty()); } @@ -3763,8 +3782,7 @@ test "Screen: scrolling" { var s = try init(alloc, 3, 5, 0); defer s.deinit(); - s.cursor.pen.bg = .{ .r = 155 }; - s.cursor.pen.attrs.has_bg = true; + s.cursor.pen.bg = .{ .rgb = .{ .r = 155 } }; try s.testWriteString("1ABCD\n2EFGH\n3IJKL"); try testing.expect(s.viewportIsBottom()); @@ -3781,7 +3799,7 @@ test "Screen: scrolling" { { // Test that our new row has the correct background const cell = s.getCell(.active, 2, 0); - try testing.expectEqual(@as(u8, 155), cell.bg.r); + try testing.expectEqual(@as(u8, 155), cell.bg.rgb.r); } // Scrolling to the bottom does nothing @@ -5095,8 +5113,7 @@ test "Screen: scrollRegionUp single with pen" { try s.testWriteString("1ABCD\n2EFGH\n3IJKL\n4ABCD"); s.cursor.pen = .{ .char = 'X' }; - s.cursor.pen.bg = .{ .r = 155 }; - s.cursor.pen.attrs.has_bg = true; + s.cursor.pen.bg = .{ .rgb = .{ .r = 155 } }; s.cursor.pen.attrs.bold = true; s.scrollRegionUp(.{ .active = 1 }, .{ .active = 2 }, 1); { @@ -5105,7 +5122,7 @@ test "Screen: scrollRegionUp single with pen" { defer alloc.free(contents); try testing.expectEqualStrings("1ABCD\n3IJKL\n\n4ABCD", contents); const cell = s.getCell(.active, 2, 0); - try testing.expectEqual(@as(u8, 155), cell.bg.r); + try testing.expectEqual(@as(u8, 155), cell.bg.rgb.r); try testing.expect(!cell.attrs.bold); try testing.expect(s.cursor.pen.attrs.bold); } @@ -5170,8 +5187,7 @@ test "Screen: scrollRegionUp fills with pen" { try s.testWriteString("A\nB\nC\nD"); s.cursor.pen = .{ .char = 'X' }; - s.cursor.pen.bg = .{ .r = 155 }; - s.cursor.pen.attrs.has_bg = true; + s.cursor.pen.bg = .{ .rgb = .{ .r = 155 } }; s.cursor.pen.attrs.bold = true; s.scrollRegionUp(.{ .active = 0 }, .{ .active = 2 }, 1); { @@ -5180,7 +5196,7 @@ test "Screen: scrollRegionUp fills with pen" { defer alloc.free(contents); try testing.expectEqualStrings("B\nC\n\nD", contents); const cell = s.getCell(.active, 2, 0); - try testing.expectEqual(@as(u8, 155), cell.bg.r); + try testing.expectEqual(@as(u8, 155), cell.bg.rgb.r); try testing.expect(!cell.attrs.bold); try testing.expect(s.cursor.pen.attrs.bold); } @@ -5202,8 +5218,7 @@ test "Screen: scrollRegionUp buffer wrap" { // Scroll s.cursor.pen = .{ .char = 'X' }; - s.cursor.pen.bg = .{ .r = 155 }; - s.cursor.pen.attrs.has_bg = true; + s.cursor.pen.bg = .{ .rgb = .{ .r = 155 } }; s.cursor.pen.attrs.bold = true; s.scrollRegionUp(.{ .screen = 0 }, .{ .screen = 2 }, 1); @@ -5213,7 +5228,7 @@ test "Screen: scrollRegionUp buffer wrap" { defer alloc.free(contents); try testing.expectEqualStrings("3IJKL\n4ABCD", contents); const cell = s.getCell(.active, 2, 0); - try testing.expectEqual(@as(u8, 155), cell.bg.r); + try testing.expectEqual(@as(u8, 155), cell.bg.rgb.r); try testing.expect(!cell.attrs.bold); try testing.expect(s.cursor.pen.attrs.bold); } @@ -5235,8 +5250,7 @@ test "Screen: scrollRegionUp buffer wrap alternate" { // Scroll s.cursor.pen = .{ .char = 'X' }; - s.cursor.pen.bg = .{ .r = 155 }; - s.cursor.pen.attrs.has_bg = true; + s.cursor.pen.bg = .{ .rgb = .{ .r = 155 } }; s.cursor.pen.attrs.bold = true; s.scrollRegionUp(.{ .screen = 0 }, .{ .screen = 2 }, 2); @@ -5246,7 +5260,7 @@ test "Screen: scrollRegionUp buffer wrap alternate" { defer alloc.free(contents); try testing.expectEqualStrings("4ABCD", contents); const cell = s.getCell(.active, 2, 0); - try testing.expectEqual(@as(u8, 155), cell.bg.r); + try testing.expectEqual(@as(u8, 155), cell.bg.rgb.r); try testing.expect(!cell.attrs.bold); try testing.expect(s.cursor.pen.attrs.bold); } @@ -5964,8 +5978,7 @@ test "Screen: resize (no reflow) less rows trims blank lines" { const row = s.getRow(.{ .active = y }); for (0..s.cols) |x| { const cell = row.getCellPtr(x); - cell.*.bg = .{ .r = 0xFF, .g = 0, .b = 0 }; - cell.*.attrs.has_bg = true; + cell.*.bg = .{ .rgb = .{ .r = 0xFF, .g = 0, .b = 0 } }; } } @@ -6000,8 +6013,7 @@ test "Screen: resize (no reflow) more rows trims blank lines" { const row = s.getRow(.{ .active = y }); for (0..s.cols) |x| { const cell = row.getCellPtr(x); - cell.*.bg = .{ .r = 0xFF, .g = 0, .b = 0 }; - cell.*.attrs.has_bg = true; + cell.*.bg = .{ .rgb = .{ .r = 0xFF, .g = 0, .b = 0 } }; } } @@ -6934,7 +6946,7 @@ test "Screen: resize less cols trailing background colors" { const cursor = s.cursor; // Color our cells red - const pen: Cell = .{ .bg = .{ .r = 0xFF }, .attrs = .{ .has_bg = true } }; + const pen: Cell = .{ .bg = .{ .rgb = .{ .r = 0xFF } } }; for (s.cursor.x..s.cols) |x| { const row = s.getRow(.{ .active = s.cursor.y }); const cell = row.getCellPtr(x); diff --git a/src/terminal/Terminal.zig b/src/terminal/Terminal.zig index af58f75da..a486144e3 100644 --- a/src/terminal/Terminal.zig +++ b/src/terminal/Terminal.zig @@ -474,8 +474,8 @@ pub fn setAttribute(self: *Terminal, attr: sgr.Attribute) !void { switch (attr) { .unset => { - self.screen.cursor.pen.attrs.has_fg = false; - self.screen.cursor.pen.attrs.has_bg = false; + self.screen.cursor.pen.fg = .none; + self.screen.cursor.pen.bg = .none; self.screen.cursor.pen.attrs = .{}; }, @@ -561,55 +561,51 @@ pub fn setAttribute(self: *Terminal, attr: sgr.Attribute) !void { }, .direct_color_fg => |rgb| { - self.screen.cursor.pen.attrs.has_fg = true; self.screen.cursor.pen.fg = .{ - .r = rgb.r, - .g = rgb.g, - .b = rgb.b, + .rgb = .{ + .r = rgb.r, + .g = rgb.g, + .b = rgb.b, + }, }; }, .direct_color_bg => |rgb| { - self.screen.cursor.pen.attrs.has_bg = true; self.screen.cursor.pen.bg = .{ - .r = rgb.r, - .g = rgb.g, - .b = rgb.b, + .rgb = .{ + .r = rgb.r, + .g = rgb.g, + .b = rgb.b, + }, }; }, .@"8_fg" => |n| { - self.screen.cursor.pen.attrs.has_fg = true; - self.screen.cursor.pen.fg = self.color_palette.colors[@intFromEnum(n)]; + self.screen.cursor.pen.fg = .{ .indexed = @intFromEnum(n) }; }, .@"8_bg" => |n| { - self.screen.cursor.pen.attrs.has_bg = true; - self.screen.cursor.pen.bg = self.color_palette.colors[@intFromEnum(n)]; + self.screen.cursor.pen.bg = .{ .indexed = @intFromEnum(n) }; }, - .reset_fg => self.screen.cursor.pen.attrs.has_fg = false, + .reset_fg => self.screen.cursor.pen.fg = .none, - .reset_bg => self.screen.cursor.pen.attrs.has_bg = false, + .reset_bg => self.screen.cursor.pen.bg = .none, .@"8_bright_fg" => |n| { - self.screen.cursor.pen.attrs.has_fg = true; - self.screen.cursor.pen.fg = self.color_palette.colors[@intFromEnum(n)]; + self.screen.cursor.pen.fg = .{ .indexed = @intFromEnum(n) }; }, .@"8_bright_bg" => |n| { - self.screen.cursor.pen.attrs.has_bg = true; - self.screen.cursor.pen.bg = self.color_palette.colors[@intFromEnum(n)]; + self.screen.cursor.pen.bg = .{ .indexed = @intFromEnum(n) }; }, .@"256_fg" => |idx| { - self.screen.cursor.pen.attrs.has_fg = true; - self.screen.cursor.pen.fg = self.color_palette.colors[idx]; + self.screen.cursor.pen.fg = .{ .indexed = idx }; }, .@"256_bg" => |idx| { - self.screen.cursor.pen.attrs.has_bg = true; - self.screen.cursor.pen.bg = self.color_palette.colors[idx]; + self.screen.cursor.pen.bg = .{ .indexed = idx }; }, .unknown => return error.InvalidAttribute, @@ -676,12 +672,26 @@ pub fn printAttributes(self: *Terminal, buf: []u8) ![]const u8 { try writer.print(";{c}", .{c}); } - if (pen.attrs.has_fg) { - try writer.print(";38:2::{[r]}:{[g]}:{[b]}", pen.fg); + switch (pen.fg) { + .none => {}, + .indexed => |idx| if (idx >= 16) + try writer.print(";38:5:{}", .{idx}) + else if (idx >= 8) + try writer.print(";9{}", .{idx - 8}) + else + try writer.print(";3{}", .{idx}), + .rgb => |rgb| try writer.print(";38:2::{[r]}:{[g]}:{[b]}", rgb), } - if (pen.attrs.has_bg) { - try writer.print(";48:2::{[r]}:{[g]}:{[b]}", pen.bg); + switch (pen.bg) { + .none => {}, + .indexed => |idx| if (idx >= 16) + try writer.print(";48:5:{}", .{idx}) + else if (idx >= 8) + try writer.print(";10{}", .{idx - 8}) + else + try writer.print(";4{}", .{idx}), + .rgb => |rgb| try writer.print(";48:2::{[r]}:{[g]}:{[b]}", rgb), } return stream.getWritten(); @@ -1080,8 +1090,6 @@ pub fn decaln(self: *Terminal) !void { .bg = self.screen.cursor.pen.bg, .fg = self.screen.cursor.pen.fg, .attrs = .{ - .has_bg = self.screen.cursor.pen.attrs.has_bg, - .has_fg = self.screen.cursor.pen.attrs.has_fg, .protected = self.screen.cursor.pen.attrs.protected, }, }; @@ -1229,9 +1237,9 @@ pub fn eraseDisplay( defer tracy.end(); // Erasing clears all attributes / colors _except_ the background - const pen: Screen.Cell = if (!self.screen.cursor.pen.attrs.has_bg) .{} else .{ - .bg = self.screen.cursor.pen.bg, - .attrs = .{ .has_bg = true }, + const pen: Screen.Cell = switch (self.screen.cursor.pen.bg) { + .none => .{}, + else => |bg| .{ .bg = bg }, }; // We respect protected attributes if explicitly requested (probably @@ -1380,9 +1388,9 @@ pub fn eraseLine( defer tracy.end(); // We always fill with the background - const pen: Screen.Cell = if (!self.screen.cursor.pen.attrs.has_bg) .{} else .{ - .bg = self.screen.cursor.pen.bg, - .attrs = .{ .has_bg = true }, + const pen: Screen.Cell = switch (self.screen.cursor.pen.bg) { + .none => .{}, + else => |bg| .{ .bg = bg }, }; // Get our start/end positions depending on mode. @@ -1470,7 +1478,6 @@ pub fn deleteChars(self: *Terminal, count: usize) !void { const pen: Screen.Cell = .{ .bg = self.screen.cursor.pen.bg, - .attrs = .{ .has_bg = self.screen.cursor.pen.attrs.has_bg }, }; // If our X is a wide spacer tail then we need to erase the @@ -1529,7 +1536,6 @@ pub fn eraseChars(self: *Terminal, count_req: usize) void { const pen: Screen.Cell = .{ .bg = self.screen.cursor.pen.bg, - .attrs = .{ .has_bg = self.screen.cursor.pen.attrs.has_bg }, }; // If we never had a protection mode, then we can assume no cells @@ -1873,7 +1879,6 @@ pub fn insertBlanks(self: *Terminal, count: usize) void { // Insert blanks. The blanks preserve the background color. row.fillSlice(.{ .bg = self.screen.cursor.pen.bg, - .attrs = .{ .has_bg = self.screen.cursor.pen.attrs.has_bg }, }, start, pivot); } @@ -1939,7 +1944,6 @@ pub fn insertLines(self: *Terminal, count: usize) !void { const row = self.screen.getRow(.{ .active = y }); row.fillSlice(.{ .bg = self.screen.cursor.pen.bg, - .attrs = .{ .has_bg = self.screen.cursor.pen.attrs.has_bg }, }, self.scrolling_region.left, self.scrolling_region.right + 1); } } @@ -2014,7 +2018,6 @@ pub fn deleteLines(self: *Terminal, count: usize) !void { row.setWrapped(false); row.fillSlice(.{ .bg = self.screen.cursor.pen.bg, - .attrs = .{ .has_bg = self.screen.cursor.pen.attrs.has_bg }, }, self.scrolling_region.left, self.scrolling_region.right + 1); } } @@ -2247,13 +2250,13 @@ test "Terminal: fullReset with a non-empty pen" { var t = try init(testing.allocator, 80, 80); defer t.deinit(testing.allocator); - t.screen.cursor.pen.bg = .{ .r = 0xFF, .g = 0x00, .b = 0x7F }; - t.screen.cursor.pen.fg = .{ .r = 0xFF, .g = 0x00, .b = 0x7F }; + t.screen.cursor.pen.bg = .{ .rgb = .{ .r = 0xFF, .g = 0x00, .b = 0x7F } }; + t.screen.cursor.pen.fg = .{ .rgb = .{ .r = 0xFF, .g = 0x00, .b = 0x7F } }; t.fullReset(testing.allocator); const cell = t.screen.getCell(.active, t.screen.cursor.y, t.screen.cursor.x); - try testing.expect(cell.bg.eql(.{})); - try testing.expect(cell.fg.eql(.{})); + try testing.expect(cell.bg == .none); + try testing.expect(cell.fg == .none); } test "Terminal: fullReset origin mode" { @@ -4165,8 +4168,7 @@ test "Terminal: index bottom of primary screen background sgr" { defer t.deinit(alloc); const pen: Screen.Cell = .{ - .bg = .{ .r = 0xFF, .g = 0x00, .b = 0x00 }, - .attrs = .{ .has_bg = true }, + .bg = .{ .rgb = .{ .r = 0xFF, .g = 0x00, .b = 0x00 } }, }; t.setCursorPos(5, 1); @@ -4338,8 +4340,7 @@ test "Terminal: decaln preserves color" { defer t.deinit(alloc); const pen: Screen.Cell = .{ - .bg = .{ .r = 0xFF, .g = 0x00, .b = 0x00 }, - .attrs = .{ .has_bg = true }, + .bg = .{ .rgb = .{ .r = 0xFF, .g = 0x00, .b = 0x00 } }, }; // Initial value @@ -4443,8 +4444,7 @@ test "Terminal: insertBlanks preserves background sgr" { defer t.deinit(alloc); const pen: Screen.Cell = .{ - .bg = .{ .r = 0xFF, .g = 0x00, .b = 0x00 }, - .attrs = .{ .has_bg = true }, + .bg = .{ .rgb = .{ .r = 0xFF, .g = 0x00, .b = 0x00 } }, }; for ("ABC") |c| try t.print(c); @@ -4836,8 +4836,7 @@ test "Terminal: deleteChars background sgr" { defer t.deinit(alloc); const pen: Screen.Cell = .{ - .bg = .{ .r = 0xFF, .g = 0x00, .b = 0x00 }, - .attrs = .{ .has_bg = true }, + .bg = .{ .rgb = .{ .r = 0xFF, .g = 0x00, .b = 0x00 } }, }; try t.printString("ABC123"); @@ -5001,8 +5000,7 @@ test "Terminal: eraseChars preserves background sgr" { defer t.deinit(alloc); const pen: Screen.Cell = .{ - .bg = .{ .r = 0xFF, .g = 0x00, .b = 0x00 }, - .attrs = .{ .has_bg = true }, + .bg = .{ .rgb = .{ .r = 0xFF, .g = 0x00, .b = 0x00 } }, }; for ("ABC") |c| try t.print(c); @@ -5312,8 +5310,7 @@ test "Terminal: eraseLine right preserves background sgr" { defer t.deinit(alloc); const pen: Screen.Cell = .{ - .bg = .{ .r = 0xFF, .g = 0x00, .b = 0x00 }, - .attrs = .{ .has_bg = true }, + .bg = .{ .rgb = .{ .r = 0xFF, .g = 0x00, .b = 0x00 } }, }; for ("ABCDE") |c| try t.print(c); @@ -5462,8 +5459,7 @@ test "Terminal: eraseLine left preserves background sgr" { defer t.deinit(alloc); const pen: Screen.Cell = .{ - .bg = .{ .r = 0xFF, .g = 0x00, .b = 0x00 }, - .attrs = .{ .has_bg = true }, + .bg = .{ .rgb = .{ .r = 0xFF, .g = 0x00, .b = 0x00 } }, }; for ("ABCDE") |c| try t.print(c); @@ -5578,8 +5574,7 @@ test "Terminal: eraseLine complete preserves background sgr" { defer t.deinit(alloc); const pen: Screen.Cell = .{ - .bg = .{ .r = 0xFF, .g = 0x00, .b = 0x00 }, - .attrs = .{ .has_bg = true }, + .bg = .{ .rgb = .{ .r = 0xFF, .g = 0x00, .b = 0x00 } }, }; for ("ABCDE") |c| try t.print(c); @@ -5707,8 +5702,7 @@ test "Terminal: eraseDisplay erase below preserves SGR bg" { t.setCursorPos(2, 2); const pen: Screen.Cell = .{ - .bg = .{ .r = 0xFF, .g = 0x00, .b = 0x00 }, - .attrs = .{ .has_bg = true }, + .bg = .{ .rgb = .{ .r = 0xFF, .g = 0x00, .b = 0x00 } }, }; t.screen.cursor.pen = pen; @@ -5878,8 +5872,7 @@ test "Terminal: eraseDisplay erase above preserves SGR bg" { t.setCursorPos(2, 2); const pen: Screen.Cell = .{ - .bg = .{ .r = 0xFF, .g = 0x00, .b = 0x00 }, - .attrs = .{ .has_bg = true }, + .bg = .{ .rgb = .{ .r = 0xFF, .g = 0x00, .b = 0x00 } }, }; t.screen.cursor.pen = pen; @@ -6018,16 +6011,16 @@ test "Terminal: eraseDisplay above" { const pink = color.RGB{ .r = 0xFF, .g = 0x00, .b = 0x7F }; t.screen.cursor.pen = Screen.Cell{ .char = 'a', - .bg = pink, - .fg = pink, - .attrs = .{ .bold = true, .has_bg = true }, + .bg = .{ .rgb = pink }, + .fg = .{ .rgb = pink }, + .attrs = .{ .bold = true }, }; const cell_ptr = t.screen.getCellPtr(.active, 0, 0); cell_ptr.* = t.screen.cursor.pen; // verify the cell was set var cell = t.screen.getCell(.active, 0, 0); - try testing.expect(cell.bg.eql(pink)); - try testing.expect(cell.fg.eql(pink)); + try testing.expect(cell.bg.rgb.eql(pink)); + try testing.expect(cell.fg.rgb.eql(pink)); try testing.expect(cell.char == 'a'); try testing.expect(cell.attrs.bold); // move the cursor below it @@ -6037,18 +6030,17 @@ test "Terminal: eraseDisplay above" { t.eraseDisplay(testing.allocator, .above, false); // check it was erased cell = t.screen.getCell(.active, 0, 0); - try testing.expect(cell.bg.eql(pink)); - try testing.expect(cell.fg.eql(.{})); + try testing.expect(cell.bg.rgb.eql(pink)); + try testing.expect(cell.fg == .none); try testing.expect(cell.char == 0); try testing.expect(!cell.attrs.bold); - try testing.expect(cell.attrs.has_bg); // Check that our pen hasn't changed try testing.expect(t.screen.cursor.pen.attrs.bold); // check that another cell got the correct bg cell = t.screen.getCell(.active, 0, 1); - try testing.expect(cell.bg.eql(pink)); + try testing.expect(cell.bg.rgb.eql(pink)); } test "Terminal: eraseDisplay below" { @@ -6058,31 +6050,30 @@ test "Terminal: eraseDisplay below" { const pink = color.RGB{ .r = 0xFF, .g = 0x00, .b = 0x7F }; t.screen.cursor.pen = Screen.Cell{ .char = 'a', - .bg = pink, - .fg = pink, - .attrs = .{ .bold = true, .has_bg = true }, + .bg = .{ .rgb = pink }, + .fg = .{ .rgb = pink }, + .attrs = .{ .bold = true }, }; const cell_ptr = t.screen.getCellPtr(.active, 60, 60); cell_ptr.* = t.screen.cursor.pen; // verify the cell was set var cell = t.screen.getCell(.active, 60, 60); - try testing.expect(cell.bg.eql(pink)); - try testing.expect(cell.fg.eql(pink)); + try testing.expect(cell.bg.rgb.eql(pink)); + try testing.expect(cell.fg.rgb.eql(pink)); try testing.expect(cell.char == 'a'); try testing.expect(cell.attrs.bold); // erase below the cursor t.eraseDisplay(testing.allocator, .below, false); // check it was erased cell = t.screen.getCell(.active, 60, 60); - try testing.expect(cell.bg.eql(pink)); - try testing.expect(cell.fg.eql(.{})); + try testing.expect(cell.bg.rgb.eql(pink)); + try testing.expect(cell.fg == .none); try testing.expect(cell.char == 0); try testing.expect(!cell.attrs.bold); - try testing.expect(cell.attrs.has_bg); // check that another cell got the correct bg cell = t.screen.getCell(.active, 0, 1); - try testing.expect(cell.bg.eql(pink)); + try testing.expect(cell.bg.rgb.eql(pink)); } test "Terminal: eraseDisplay complete" { @@ -6092,9 +6083,9 @@ test "Terminal: eraseDisplay complete" { const pink = color.RGB{ .r = 0xFF, .g = 0x00, .b = 0x7F }; t.screen.cursor.pen = Screen.Cell{ .char = 'a', - .bg = pink, - .fg = pink, - .attrs = .{ .bold = true, .has_bg = true }, + .bg = .{ .rgb = pink }, + .fg = .{ .rgb = pink }, + .attrs = .{ .bold = true }, }; var cell_ptr = t.screen.getCellPtr(.active, 60, 60); cell_ptr.* = t.screen.cursor.pen; @@ -6102,14 +6093,14 @@ test "Terminal: eraseDisplay complete" { cell_ptr.* = t.screen.cursor.pen; // verify the cell was set var cell = t.screen.getCell(.active, 60, 60); - try testing.expect(cell.bg.eql(pink)); - try testing.expect(cell.fg.eql(pink)); + try testing.expect(cell.bg.rgb.eql(pink)); + try testing.expect(cell.fg.rgb.eql(pink)); try testing.expect(cell.char == 'a'); try testing.expect(cell.attrs.bold); // verify the cell was set cell = t.screen.getCell(.active, 0, 0); - try testing.expect(cell.bg.eql(pink)); - try testing.expect(cell.fg.eql(pink)); + try testing.expect(cell.bg.rgb.eql(pink)); + try testing.expect(cell.fg.rgb.eql(pink)); try testing.expect(cell.char == 'a'); try testing.expect(cell.attrs.bold); // position our cursor between the cells @@ -6118,17 +6109,15 @@ test "Terminal: eraseDisplay complete" { t.eraseDisplay(testing.allocator, .complete, false); // check they were erased cell = t.screen.getCell(.active, 60, 60); - try testing.expect(cell.bg.eql(pink)); - try testing.expect(cell.fg.eql(.{})); + try testing.expect(cell.bg.rgb.eql(pink)); + try testing.expect(cell.fg == .none); try testing.expect(cell.char == 0); try testing.expect(!cell.attrs.bold); - try testing.expect(cell.attrs.has_bg); cell = t.screen.getCell(.active, 0, 0); - try testing.expect(cell.bg.eql(pink)); - try testing.expect(cell.fg.eql(.{})); + try testing.expect(cell.bg.rgb.eql(pink)); + try testing.expect(cell.fg == .none); try testing.expect(cell.char == 0); try testing.expect(!cell.attrs.bold); - try testing.expect(cell.attrs.has_bg); } test "Terminal: eraseDisplay protected complete" { @@ -7041,8 +7030,7 @@ test "Terminal: DECCOLM preserves SGR bg" { defer t.deinit(alloc); const pen: Screen.Cell = .{ - .bg = .{ .r = 0xFF, .g = 0x00, .b = 0x00 }, - .attrs = .{ .has_bg = true }, + .bg = .{ .rgb = .{ .r = 0xFF, .g = 0x00, .b = 0x00 } }, }; t.screen.cursor.pen = pen;