diff --git a/src/terminal/new/Screen.zig b/src/terminal/new/Screen.zig index c80db969a..98210aed4 100644 --- a/src/terminal/new/Screen.zig +++ b/src/terminal/new/Screen.zig @@ -3,6 +3,7 @@ const Screen = @This(); const std = @import("std"); const Allocator = std.mem.Allocator; const assert = std.debug.assert; +const sgr = @import("../sgr.zig"); const unicode = @import("../../unicode/main.zig"); const PageList = @import("PageList.zig"); const pagepkg = @import("page.zig"); @@ -31,7 +32,12 @@ const Cursor = struct { /// next character print will force a soft-wrap. pending_wrap: bool = false, - /// The currently active style. The style is page-specific so when + /// The currently active style. This is the concrete style value + /// that should be kept up to date. The style ID to use for cell writing + /// is below. + style: style.Style = .{}, + + /// The currently active style ID. The style is page-specific so when /// we change pages we need to ensure that we update that page with /// our style when used. style_id: style.Id = style.default_id, @@ -208,6 +214,7 @@ pub fn cursorDownScroll(self: *Screen) !void { } // No space, we need to allocate a new page and move the cursor to it. + // TODO: copy style over const new_page = try self.pages.grow(); assert(new_page.data.size.rows == 0); @@ -241,6 +248,171 @@ pub fn scroll(self: *Screen, behavior: Scroll) void { } } +/// Set a style attribute for the current cursor. +/// +/// This can cause a page split if the current page cannot fit this style. +/// This is the only scenario an error return is possible. +pub fn setAttribute(self: *Screen, attr: sgr.Attribute) !void { + switch (attr) { + .unset => { + self.cursor.style = .{}; + }, + + .bold => { + self.cursor.style.flags.bold = true; + }, + + .reset_bold => { + // Bold and faint share the same SGR code for this + self.cursor.style.flags.bold = false; + self.cursor.style.flags.faint = false; + }, + + .italic => { + self.cursor.style.flags.italic = true; + }, + + .reset_italic => { + self.cursor.style.flags.italic = false; + }, + + .faint => { + self.cursor.style.flags.faint = true; + }, + + .underline => |v| { + self.cursor.style.flags.underline = v; + }, + + .reset_underline => { + self.cursor.style.flags.underline = .none; + }, + + .underline_color => |rgb| { + self.cursor.style.underline_color = .{ .rgb = .{ + .r = rgb.r, + .g = rgb.g, + .b = rgb.b, + } }; + }, + + .@"256_underline_color" => |idx| { + self.cursor.style.underline_color = .{ .palette = idx }; + }, + + .reset_underline_color => { + self.cursor.style.underline_color = .none; + }, + + .blink => { + self.cursor.style.flags.blink = true; + }, + + .reset_blink => { + self.cursor.style.flags.blink = false; + }, + + .inverse => { + self.cursor.style.flags.inverse = true; + }, + + .reset_inverse => { + self.cursor.style.flags.inverse = false; + }, + + .invisible => { + self.cursor.style.flags.invisible = true; + }, + + .reset_invisible => { + self.cursor.style.flags.invisible = false; + }, + + .strikethrough => { + self.cursor.style.flags.strikethrough = true; + }, + + .reset_strikethrough => { + self.cursor.style.flags.strikethrough = false; + }, + + .direct_color_fg => |rgb| { + self.cursor.style.fg_color = .{ + .rgb = .{ + .r = rgb.r, + .g = rgb.g, + .b = rgb.b, + }, + }; + }, + + .direct_color_bg => |rgb| { + self.cursor.style.bg_color = .{ + .rgb = .{ + .r = rgb.r, + .g = rgb.g, + .b = rgb.b, + }, + }; + }, + + .@"8_fg" => |n| { + self.cursor.style.fg_color = .{ .palette = @intFromEnum(n) }; + }, + + .@"8_bg" => |n| { + self.cursor.style.bg_color = .{ .palette = @intFromEnum(n) }; + }, + + .reset_fg => self.cursor.style.fg_color = .none, + + .reset_bg => self.cursor.style.bg_color = .none, + + .@"8_bright_fg" => |n| { + self.cursor.style.fg_color = .{ .palette = @intFromEnum(n) }; + }, + + .@"8_bright_bg" => |n| { + self.cursor.style.bg_color = .{ .palette = @intFromEnum(n) }; + }, + + .@"256_fg" => |idx| { + self.cursor.style.fg_color = .{ .palette = idx }; + }, + + .@"256_bg" => |idx| { + self.cursor.style.bg_color = .{ .palette = idx }; + }, + + .unknown => return, + } + + var page = self.cursor.page_offset.page.data; + + // Remove our previous style if is unused. + if (self.cursor.style_ref) |ref| { + if (ref.* == 0) { + page.styles.remove(page.memory, self.cursor.style_id); + } + } + + // If our new style is the default, just reset to that + if (self.cursor.style.default()) { + self.cursor.style_id = 0; + self.cursor.style_ref = null; + return; + } + + // After setting the style, we need to update our style map. + // Note that we COULD lazily do this in print. We should look into + // if that makes a meaningful difference. Our priority is to keep print + // fast because setting a ton of styles that do nothing is uncommon + // and weird. + const md = try page.styles.upsert(page.memory, self.cursor.style); + self.cursor.style_id = md.id; + self.cursor.style_ref = &md.ref; +} + /// Dump the screen to a string. The writer given should be buffered; /// this function does not attempt to efficiently write and generally writes /// one byte at a time. @@ -360,9 +532,72 @@ test "Screen read and write" { var s = try Screen.init(alloc, 80, 24, 1000); defer s.deinit(); + try testing.expectEqual(@as(style.Id, 0), s.cursor.style_id); try s.testWriteString("hello, world"); const str = try s.dumpStringAlloc(alloc, .{ .screen = .{} }); defer alloc.free(str); try testing.expectEqualStrings("hello, world", str); } + +test "Screen style basics" { + const testing = std.testing; + const alloc = testing.allocator; + + var s = try Screen.init(alloc, 80, 24, 1000); + defer s.deinit(); + const page = s.cursor.page_offset.page.data; + try testing.expectEqual(@as(usize, 0), page.styles.count(page.memory)); + + // Set a new style + try s.setAttribute(.{ .bold = {} }); + try testing.expect(s.cursor.style_id != 0); + try testing.expectEqual(@as(usize, 1), page.styles.count(page.memory)); + try testing.expect(s.cursor.style.flags.bold); + + // Set another style, we should still only have one since it was unused + try s.setAttribute(.{ .italic = {} }); + try testing.expect(s.cursor.style_id != 0); + try testing.expectEqual(@as(usize, 1), page.styles.count(page.memory)); + try testing.expect(s.cursor.style.flags.italic); +} + +test "Screen style reset to default" { + const testing = std.testing; + const alloc = testing.allocator; + + var s = try Screen.init(alloc, 80, 24, 1000); + defer s.deinit(); + const page = s.cursor.page_offset.page.data; + try testing.expectEqual(@as(usize, 0), page.styles.count(page.memory)); + + // Set a new style + try s.setAttribute(.{ .bold = {} }); + try testing.expect(s.cursor.style_id != 0); + try testing.expectEqual(@as(usize, 1), page.styles.count(page.memory)); + + // Reset to default + try s.setAttribute(.{ .reset_bold = {} }); + try testing.expect(s.cursor.style_id == 0); + try testing.expectEqual(@as(usize, 0), page.styles.count(page.memory)); +} + +test "Screen style reset with unset" { + const testing = std.testing; + const alloc = testing.allocator; + + var s = try Screen.init(alloc, 80, 24, 1000); + defer s.deinit(); + const page = s.cursor.page_offset.page.data; + try testing.expectEqual(@as(usize, 0), page.styles.count(page.memory)); + + // Set a new style + try s.setAttribute(.{ .bold = {} }); + try testing.expect(s.cursor.style_id != 0); + try testing.expectEqual(@as(usize, 1), page.styles.count(page.memory)); + + // Reset to default + try s.setAttribute(.{ .unset = {} }); + try testing.expect(s.cursor.style_id == 0); + try testing.expectEqual(@as(usize, 0), page.styles.count(page.memory)); +} diff --git a/src/terminal/new/Terminal.zig b/src/terminal/new/Terminal.zig index ae127e544..dd30c2c01 100644 --- a/src/terminal/new/Terminal.zig +++ b/src/terminal/new/Terminal.zig @@ -1239,6 +1239,11 @@ pub fn eraseChars(self: *Terminal, count_req: usize) void { // } } +/// Set a style attribute. +pub fn setAttribute(self: *Terminal, attr: sgr.Attribute) !void { + try self.screen.setAttribute(attr); +} + /// Return the current string value of the terminal. Newlines are /// encoded as "\n". This omits any formatting such as fg/bg. /// @@ -3920,3 +3925,34 @@ test "Terminal: deleteLines resets wrap" { try testing.expectEqualStrings("B", str); } } + +test "Terminal: default style is empty" { + const alloc = testing.allocator; + var t = try init(alloc, 5, 5); + defer t.deinit(alloc); + + try t.print('A'); + + { + const list_cell = t.screen.pages.getCell(.{ .screen = .{ .x = 0, .y = 0 } }).?; + const cell = list_cell.cell; + try testing.expectEqual(@as(u21, 'A'), cell.content.codepoint); + try testing.expectEqual(@as(style.Id, 0), cell.style_id); + } +} + +test "Terminal: bold style" { + const alloc = testing.allocator; + var t = try init(alloc, 5, 5); + defer t.deinit(alloc); + + try t.setAttribute(.{ .bold = {} }); + try t.print('A'); + + { + const list_cell = t.screen.pages.getCell(.{ .screen = .{ .x = 0, .y = 0 } }).?; + const cell = list_cell.cell; + try testing.expectEqual(@as(u21, 'A'), cell.content.codepoint); + try testing.expect(cell.style_id != 0); + } +} diff --git a/src/terminal/new/style.zig b/src/terminal/new/style.zig index 3de581676..9d867940d 100644 --- a/src/terminal/new/style.zig +++ b/src/terminal/new/style.zig @@ -45,6 +45,12 @@ pub const Style = struct { rgb: color.RGB, }; + /// True if the style is the default style. + pub fn default(self: Style) bool { + const def: []const u8 = comptime std.mem.asBytes(&Style{}); + return std.mem.eql(u8, std.mem.asBytes(&self), def); + } + test { // The size of the struct so we can be aware of changes. const testing = std.testing; @@ -203,6 +209,11 @@ pub const Set = struct { id_map.removeByPtr(id_entry.key_ptr); style_map.removeByPtr(style_ptr); } + + /// Return the number of styles currently in the set. + pub fn count(self: *const Set, base: anytype) usize { + return self.id_map.map(base).count(); + } }; /// Metadata about a style. This is used to track the reference count