From f7cba73f5705d9060a1c87a3b2473b2f1f41554d Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 27 Feb 2024 21:35:50 -0800 Subject: [PATCH] terminal/new: charsets --- src/terminal/Terminal.zig | 4 + src/terminal/new/Screen.zig | 24 +++++- src/terminal/new/Terminal.zig | 144 +++++++++++++++++++++++++++++++--- 3 files changed, 160 insertions(+), 12 deletions(-) diff --git a/src/terminal/Terminal.zig b/src/terminal/Terminal.zig index d68983ad6..561963568 100644 --- a/src/terminal/Terminal.zig +++ b/src/terminal/Terminal.zig @@ -2785,6 +2785,7 @@ test "Terminal: print writes to bottom if scrolled" { } } +// X test "Terminal: print charset" { var t = try init(testing.allocator, 80, 80); defer t.deinit(testing.allocator); @@ -2809,6 +2810,7 @@ test "Terminal: print charset" { } } +// X test "Terminal: print charset outside of ASCII" { var t = try init(testing.allocator, 80, 80); defer t.deinit(testing.allocator); @@ -2829,6 +2831,7 @@ test "Terminal: print charset outside of ASCII" { } } +// X test "Terminal: print invoke charset" { var t = try init(testing.allocator, 80, 80); defer t.deinit(testing.allocator); @@ -2849,6 +2852,7 @@ test "Terminal: print invoke charset" { } } +// X test "Terminal: print invoke charset single" { var t = try init(testing.allocator, 80, 80); defer t.deinit(testing.allocator); diff --git a/src/terminal/new/Screen.zig b/src/terminal/new/Screen.zig index 41035ef45..9c9cf76c3 100644 --- a/src/terminal/new/Screen.zig +++ b/src/terminal/new/Screen.zig @@ -4,6 +4,7 @@ const std = @import("std"); const Allocator = std.mem.Allocator; const assert = std.debug.assert; const ansi = @import("../ansi.zig"); +const charsets = @import("../charsets.zig"); const kitty = @import("../kitty.zig"); const sgr = @import("../sgr.zig"); const unicode = @import("../../unicode/main.zig"); @@ -29,6 +30,9 @@ cursor: Cursor, /// The saved cursor saved_cursor: ?SavedCursor = null, +/// The charset state +charset: CharsetState = .{}, + /// The current or most recent protected mode. Once a protection mode is /// set, this will never become "off" again until the screen is reset. /// The current state of whether protection attributes should be set is @@ -79,8 +83,24 @@ pub const SavedCursor = struct { protected: bool, pending_wrap: bool, origin: bool, - // TODO - //charset: CharsetState, + charset: CharsetState, +}; + +/// State required for all charset operations. +pub const CharsetState = struct { + /// The list of graphical charsets by slot + charsets: CharsetArray = CharsetArray.initFill(charsets.Charset.utf8), + + /// GL is the slot to use when using a 7-bit printable char (up to 127) + /// GR used for 8-bit printable chars. + gl: charsets.Slots = .G0, + gr: charsets.Slots = .G2, + + /// Single shift where a slot is used for exactly one char. + single_shift: ?charsets.Slots = null, + + /// An array to map a charset slot to a lookup table. + const CharsetArray = std.EnumArray(charsets.Slots, charsets.Charset); }; /// Initialize a new screen. diff --git a/src/terminal/new/Terminal.zig b/src/terminal/new/Terminal.zig index bc071208a..6db0d1ac1 100644 --- a/src/terminal/new/Terminal.zig +++ b/src/terminal/new/Terminal.zig @@ -483,8 +483,27 @@ fn printCell( ) void { // TODO: spacers should use a bgcolor only cell - // TODO: charsets - const c: u21 = unmapped_c; + const c: u21 = c: { + // TODO: non-utf8 handling, gr + + // If we're single shifting, then we use the key exactly once. + const key = if (self.screen.charset.single_shift) |key_once| blk: { + self.screen.charset.single_shift = null; + break :blk key_once; + } else self.screen.charset.gl; + const set = self.screen.charset.charsets.get(key); + + // UTF-8 or ASCII is used as-is + if (set == .utf8 or set == .ascii) break :c unmapped_c; + + // If we're outside of ASCII range this is an invalid value in + // this table so we just return space. + if (unmapped_c > std.math.maxInt(u8)) break :c ' '; + + // Get our lookup table and map it + const table = set.table(); + break :c @intCast(table[@intCast(unmapped_c)]); + }; const cell = self.screen.cursor.page_cell; @@ -598,6 +617,31 @@ fn printWrap(self: *Terminal) !void { self.screen.cursor.page_row.wrap_continuation = true; } +/// Set the charset into the given slot. +pub fn configureCharset(self: *Terminal, slot: charsets.Slots, set: charsets.Charset) void { + self.screen.charset.charsets.set(slot, set); +} + +/// Invoke the charset in slot into the active slot. If single is true, +/// then this will only be invoked for a single character. +pub fn invokeCharset( + self: *Terminal, + active: charsets.ActiveSlot, + slot: charsets.Slots, + single: bool, +) void { + if (single) { + assert(active == .GL); + self.screen.charset.single_shift = slot; + return; + } + + switch (active) { + .GL => self.screen.charset.gl = slot, + .GR => self.screen.charset.gr = slot, + } +} + /// Carriage return moves the cursor to the first column. pub fn carriageReturn(self: *Terminal) void { // Always reset pending wrap state @@ -787,8 +831,7 @@ pub fn saveCursor(self: *Terminal) void { .protected = self.screen.cursor.protected, .pending_wrap = self.screen.cursor.pending_wrap, .origin = self.modes.get(.origin), - //TODO - //.charset = self.screen.charset, + .charset = self.screen.charset, }; } @@ -804,8 +847,7 @@ pub fn restoreCursor(self: *Terminal) !void { .protected = false, .pending_wrap = false, .origin = false, - // TODO - //.charset = .{}, + .charset = .{}, }; // Set the style first because it can fail @@ -814,7 +856,7 @@ pub fn restoreCursor(self: *Terminal) !void { errdefer self.screen.cursor.style = old_style; try self.screen.manualStyleUpdate(); - //self.screen.charset = saved.charset; + self.screen.charset = saved.charset; self.modes.set(.origin, saved.origin); self.screen.cursor.pending_wrap = saved.pending_wrap; self.screen.cursor.protected = saved.protected; @@ -2429,6 +2471,88 @@ test "Terminal: print writes to bottom if scrolled" { } } +test "Terminal: print charset" { + var t = try init(testing.allocator, 80, 80); + defer t.deinit(testing.allocator); + + // G1 should have no effect + t.configureCharset(.G1, .dec_special); + t.configureCharset(.G2, .dec_special); + t.configureCharset(.G3, .dec_special); + + // Basic grid writing + try t.print('`'); + t.configureCharset(.G0, .utf8); + try t.print('`'); + t.configureCharset(.G0, .ascii); + try t.print('`'); + t.configureCharset(.G0, .dec_special); + try t.print('`'); + { + const str = try t.plainString(testing.allocator); + defer testing.allocator.free(str); + try testing.expectEqualStrings("```◆", str); + } +} + +test "Terminal: print charset outside of ASCII" { + var t = try init(testing.allocator, 80, 80); + defer t.deinit(testing.allocator); + + // G1 should have no effect + t.configureCharset(.G1, .dec_special); + t.configureCharset(.G2, .dec_special); + t.configureCharset(.G3, .dec_special); + + // Basic grid writing + t.configureCharset(.G0, .dec_special); + try t.print('`'); + try t.print(0x1F600); + { + const str = try t.plainString(testing.allocator); + defer testing.allocator.free(str); + try testing.expectEqualStrings("◆ ", str); + } +} + +test "Terminal: print invoke charset" { + var t = try init(testing.allocator, 80, 80); + defer t.deinit(testing.allocator); + + t.configureCharset(.G1, .dec_special); + + // Basic grid writing + try t.print('`'); + t.invokeCharset(.GL, .G1, false); + try t.print('`'); + try t.print('`'); + t.invokeCharset(.GL, .G0, false); + try t.print('`'); + { + const str = try t.plainString(testing.allocator); + defer testing.allocator.free(str); + try testing.expectEqualStrings("`◆◆`", str); + } +} + +test "Terminal: print invoke charset single" { + var t = try init(testing.allocator, 80, 80); + defer t.deinit(testing.allocator); + + t.configureCharset(.G1, .dec_special); + + // Basic grid writing + try t.print('`'); + t.invokeCharset(.GL, .G1, true); + try t.print('`'); + try t.print('`'); + { + const str = try t.plainString(testing.allocator); + defer testing.allocator.free(str); + try testing.expectEqualStrings("`◆`", str); + } +} + test "Terminal: soft wrap" { var t = try init(testing.allocator, 3, 80); defer t.deinit(testing.allocator); @@ -5946,15 +6070,15 @@ test "Terminal: saveCursor" { defer t.deinit(alloc); try t.setAttribute(.{ .bold = {} }); - //t.screen.charset.gr = .G3; + t.screen.charset.gr = .G3; t.modes.set(.origin, true); t.saveCursor(); - //t.screen.charset.gr = .G0; + t.screen.charset.gr = .G0; try t.setAttribute(.{ .unset = {} }); t.modes.set(.origin, false); try t.restoreCursor(); try testing.expect(t.screen.cursor.style.flags.bold); - //try testing.expect(t.screen.charset.gr == .G3); + try testing.expect(t.screen.charset.gr == .G3); try testing.expect(t.modes.get(.origin)); }