diff --git a/build.zig b/build.zig index 2ea696e7b..08f93e2a6 100644 --- a/build.zig +++ b/build.zig @@ -71,7 +71,11 @@ pub fn build(b: *std.build.Builder) !void { wasm.setTarget(.{ .cpu_arch = .wasm32, .os_tag = .freestanding }); wasm.setBuildMode(mode); wasm.setOutputDir("zig-out"); + + // Wasm-specific deps wasm.addPackage(tracylib.pkg); + wasm.addPackage(utf8proc.pkg); + _ = try utf8proc.link(b, wasm); const step = b.step("term-wasm", "Build the terminal.wasm library"); step.dependOn(&wasm.step); diff --git a/src/terminal/Screen.zig b/src/terminal/Screen.zig index 817c7d8fe..40520ccba 100644 --- a/src/terminal/Screen.zig +++ b/src/terminal/Screen.zig @@ -71,6 +71,15 @@ pub const Cell = struct { /// should have this set. The first cell of the next row is actually /// part of this row in raw input. wrap: u1 = 0, + + /// True if this is a wide character. This char takes up + /// two cells. The following cell ALWAYS is a space. + wide: u1 = 0, + + /// Notes that this only exists to be blank for a preceeding + /// wide character (tail) or following (head). + wide_spacer_tail: u1 = 0, + wide_spacer_head: u1 = 0, } = .{}, /// True if the cell should be skipped for drawing diff --git a/src/terminal/Terminal.zig b/src/terminal/Terminal.zig index d86e5c2da..dd9565505 100644 --- a/src/terminal/Terminal.zig +++ b/src/terminal/Terminal.zig @@ -6,9 +6,11 @@ const Terminal = @This(); const std = @import("std"); const builtin = @import("builtin"); +const utf8proc = @import("utf8proc"); const testing = std.testing; const assert = std.debug.assert; const Allocator = std.mem.Allocator; + const ansi = @import("ansi.zig"); const csi = @import("csi.zig"); const sgr = @import("sgr.zig"); @@ -341,23 +343,45 @@ pub fn print(self: *Terminal, c: u21) !void { // If we're not on the main display, do nothing for now if (self.status_display != .main) return; + // Determine the width of this character so we can handle + // non-single-width characters properly. + const width = utf8proc.charwidth(c); + assert(width == 1 or width == 2); + // If we're soft-wrapping, then handle that first. - if (self.screen.cursor.pending_wrap and self.modes.autowrap == 1) { - // Mark that the cell is wrapped, which guarantees that there is - // at least one cell after it in the next row. - const cell = self.screen.getCell(self.screen.cursor.y, self.screen.cursor.x); - cell.attrs.wrap = 1; + if (self.screen.cursor.pending_wrap and self.modes.autowrap == 1) + _ = self.printWrap(); - // Move to the next line - self.index(); - self.screen.cursor.x = 0; + switch (width) { + // Single cell is very easy: just write in the cell + 1 => _ = self.printCell(c), + + // Wide character requires a spacer. We print this by + // using two cells: the first is flagged "wide" and has the + // wide char. The second is guaranteed to be a spacer if + // we're not at the end of the line. + 2 => { + // If we don't have space for the wide char, we need + // to insert spacers and wrap. Then we just print the wide + // char as normal. + if (self.screen.cursor.x == self.cols - 1) { + const spacer_head = self.printCell(' '); + spacer_head.attrs.wide_spacer_head = 1; + _ = self.printWrap(); + } + + const wide_cell = self.printCell(c); + wide_cell.attrs.wide = 1; + + // Write our spacer + self.screen.cursor.x += 1; + const spacer = self.printCell(' '); + spacer.attrs.wide_spacer_tail = 1; + }, + + else => unreachable, } - // Build our cell - const cell = self.screen.getCell(self.screen.cursor.y, self.screen.cursor.x); - cell.* = self.screen.cursor.pen; - cell.char = @intCast(u32, c); - // Move the cursor self.screen.cursor.x += 1; @@ -370,6 +394,71 @@ pub fn print(self: *Terminal, c: u21) !void { } } +fn printCell(self: *Terminal, c: u21) *Screen.Cell { + const cell = self.screen.getCell( + self.screen.cursor.y, + self.screen.cursor.x, + ); + + // If this cell is wide char then we need to clear it. + // We ignore wide spacer HEADS because we can just write + // single-width characters into that. + if (cell.attrs.wide == 1) { + const x = self.screen.cursor.x + 1; + assert(x < self.cols); + + const spacer_cell = self.screen.getCell(self.screen.cursor.y, x); + assert(spacer_cell.attrs.wide_spacer_tail == 1); + spacer_cell.attrs.wide_spacer_tail = 0; + + if (self.screen.cursor.x <= 1) { + self.clearWideSpacerHead(); + } + } else if (cell.attrs.wide_spacer_tail == 1) { + assert(self.screen.cursor.x > 0); + const x = self.screen.cursor.x - 1; + + const wide_cell = self.screen.getCell(self.screen.cursor.y, x); + assert(wide_cell.attrs.wide == 1); + wide_cell.attrs.wide = 0; + + if (self.screen.cursor.x <= 1) { + self.clearWideSpacerHead(); + } + } + + // Write + cell.* = self.screen.cursor.pen; + cell.char = @intCast(u32, c); + return cell; +} + +fn printWrap(self: *Terminal) *Screen.Cell { + // Mark that the cell is wrapped, which guarantees that there is + // at least one cell after it in the next row. + const cell = self.screen.getCell( + self.screen.cursor.y, + self.screen.cursor.x, + ); + cell.attrs.wrap = 1; + + // Move to the next line + self.index(); + self.screen.cursor.x = 0; + + return cell; +} + +fn clearWideSpacerHead(self: *Terminal) void { + // TODO: handle deleting wide char on row 0 of active + assert(self.screen.cursor.y >= 1); + const cell = self.screen.getCell( + self.screen.cursor.y - 1, + self.cols - 1, + ); + cell.attrs.wide_spacer_head = 0; +} + /// Resets all margins and fills the whole screen with the character 'E' /// /// Sets the cursor to the top left corner.