From 7626f194e99a2fb6ded51460252872ae53e4b83f Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 27 Aug 2022 09:42:05 -0700 Subject: [PATCH] basic charset mapping, support configuration, print tests --- src/Window.zig | 8 +++++ src/terminal/Terminal.zig | 63 ++++++++++++++++++++++++++++++++++++++- src/terminal/charsets.zig | 24 +++++++++++++++ src/terminal/main.zig | 1 + src/terminal/stream.zig | 43 ++++++++++++++++++++++---- 5 files changed, 133 insertions(+), 6 deletions(-) diff --git a/src/Window.zig b/src/Window.zig index e7a06619c..7b533a0ee 100644 --- a/src/Window.zig +++ b/src/Window.zig @@ -1632,3 +1632,11 @@ pub fn setActiveStatusDisplay( ) !void { self.terminal.status_display = req; } + +pub fn configureCharset( + self: *Window, + slot: terminal.CharsetSlot, + set: terminal.Charset, +) !void { + self.terminal.configureCharset(slot, set); +} diff --git a/src/terminal/Terminal.zig b/src/terminal/Terminal.zig index 0433ed553..ba40fb19a 100644 --- a/src/terminal/Terminal.zig +++ b/src/terminal/Terminal.zig @@ -12,6 +12,7 @@ const assert = std.debug.assert; const Allocator = std.mem.Allocator; const ansi = @import("ansi.zig"); +const charsets = @import("charsets.zig"); const csi = @import("csi.zig"); const sgr = @import("sgr.zig"); const Selection = @import("Selection.zig"); @@ -56,6 +57,9 @@ cols: usize, /// The current scrolling region. scrolling_region: ScrollingRegion, +/// The charset state +charset: CharsetState = .{}, + /// Modes - This isn't exhaustive, since some modes (i.e. cursor origin) /// are applied to the cursor and others aren't boolean yes/no. modes: packed struct { @@ -79,6 +83,20 @@ modes: packed struct { } } = .{}, +/// State required for all charset operations. +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, + + /// An array to map a charset slot to a lookup table. + const CharsetArray = std.EnumArray(charsets.Slots, charsets.Charset); +}; + /// The event types that can be reported for mouse-related activities. /// These are all mutually exclusive (hence in a single enum). pub const MouseEvents = enum(u3) { @@ -376,6 +394,11 @@ pub fn setAttribute(self: *Terminal, attr: sgr.Attribute) !void { } } +/// Set the charset into the given slot. +pub fn configureCharset(self: *Terminal, slot: charsets.Slots, set: charsets.Charset) void { + self.charset.charsets.set(slot, set); +} + pub fn print(self: *Terminal, c: u21) !void { const tracy = trace(@src()); defer tracy.end(); @@ -438,7 +461,21 @@ pub fn print(self: *Terminal, c: u21) !void { } } -fn printCell(self: *Terminal, c: u21) *Screen.Cell { +fn printCell(self: *Terminal, unmapped_c: u21) *Screen.Cell { + const c = c: { + // TODO: non-utf8 handling, gr + + const key = self.charset.gl; + const set = self.charset.charsets.get(key); + + // UTF-8 or ASCII is used as-is + if (set == .utf8 or set == .ascii) break :c unmapped_c; + + // Get our lookup table and map it + const table = set.table(); + break :c @intCast(u21, table[@intCast(u8, unmapped_c)]); + }; + const cell = self.screen.getCell( self.screen.cursor.y, self.screen.cursor.x, @@ -1245,6 +1282,30 @@ 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('`'); + { + var str = try t.plainString(testing.allocator); + defer testing.allocator.free(str); + try testing.expectEqualStrings("```◆", str); + } +} + test "Terminal: linefeed and carriage return" { var t = try init(testing.allocator, 80, 80); defer t.deinit(testing.allocator); diff --git a/src/terminal/charsets.zig b/src/terminal/charsets.zig index a61b3e886..798adb326 100644 --- a/src/terminal/charsets.zig +++ b/src/terminal/charsets.zig @@ -1,8 +1,18 @@ const std = @import("std"); const assert = std.debug.assert; +/// The available charset slots for a terminal. +pub const Slots = enum(u3) { + G0 = 0, + G1 = 1, + G2 = 2, + G3 = 3, +}; + /// The list of supported character sets and their associated tables. pub const Charset = enum { + utf8, + ascii, british, dec_special, @@ -13,10 +23,21 @@ pub const Charset = enum { return switch (set) { .british => &british, .dec_special => &dec_special, + + // utf8 is not a table, callers should double-check if the + // charset is utf8 and NOT use tables. + .utf8 => unreachable, + + // recommended that callers just map ascii directly but we can + // support a table + .ascii => &ascii, }; } }; +/// Just a basic c => c ascii table +const ascii = initTable(); + /// https://vt100.net/docs/vt220-rm/chapter2.html const british = british: { var table = initTable(); @@ -76,6 +97,9 @@ test { const testing = std.testing; const info = @typeInfo(Charset).Enum; inline for (info.fields) |field| { + // utf8 has no table + if (@field(Charset, field.name) == .utf8) continue; + const table = @field(Charset, field.name).table(); // Yes, I could use `max_u8` here, but I want to explicitly use a diff --git a/src/terminal/main.zig b/src/terminal/main.zig index 459c70ec2..ae10f8e7c 100644 --- a/src/terminal/main.zig +++ b/src/terminal/main.zig @@ -9,6 +9,7 @@ pub const point = @import("point.zig"); pub const color = @import("color.zig"); pub const Charset = charsets.Charset; +pub const CharsetSlot = charsets.Slots; pub const Terminal = @import("Terminal.zig"); pub const Parser = @import("Parser.zig"); pub const Selection = @import("Selection.zig"); diff --git a/src/terminal/stream.zig b/src/terminal/stream.zig index b695b4e96..4219f9551 100644 --- a/src/terminal/stream.zig +++ b/src/terminal/stream.zig @@ -2,6 +2,7 @@ const std = @import("std"); const testing = std.testing; const Parser = @import("Parser.zig"); const ansi = @import("ansi.zig"); +const charsets = @import("charsets.zig"); const csi = @import("csi.zig"); const sgr = @import("sgr.zig"); const trace = @import("tracy").trace; @@ -408,17 +409,49 @@ pub fn Stream(comptime Handler: type) type { } } + fn configureCharset( + self: Self, + intermediates: []const u8, + set: charsets.Charset, + ) !void { + if (intermediates.len != 1) { + log.warn("invalid charset intermediate: {any}", .{intermediates}); + return; + } + + const slot: charsets.Slots = switch (intermediates[0]) { + // TODO: support slots '-', '.', '/' + + '(' => .G0, + ')' => .G1, + '*' => .G2, + '+' => .G3, + else => { + log.warn("invalid charset intermediate: {any}", .{intermediates}); + return; + }, + }; + + if (@hasDecl(T, "configureCharset")) { + try self.handler.configureCharset(slot, set); + return; + } + + log.warn("unimplemented configureCharset callback slot={} set={}", .{ + slot, + set, + }); + } + fn escDispatch( self: *Self, action: Parser.Action.ESC, ) !void { switch (action.final) { // Charsets - 'B' => { - // TODO: Charset support. Just ignore this for now because - // every application sets this and it makes our logs SO - // noisy. - }, + 'B' => try self.configureCharset(action.intermediates, .ascii), + 'A' => try self.configureCharset(action.intermediates, .british), + '0' => try self.configureCharset(action.intermediates, .dec_special), // DECSC - Save Cursor '7' => if (@hasDecl(T, "saveCursor")) switch (action.intermediates.len) {