mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-14 15:56:13 +03:00
terminal: print wide characters
This commit is contained in:
@ -71,7 +71,11 @@ pub fn build(b: *std.build.Builder) !void {
|
|||||||
wasm.setTarget(.{ .cpu_arch = .wasm32, .os_tag = .freestanding });
|
wasm.setTarget(.{ .cpu_arch = .wasm32, .os_tag = .freestanding });
|
||||||
wasm.setBuildMode(mode);
|
wasm.setBuildMode(mode);
|
||||||
wasm.setOutputDir("zig-out");
|
wasm.setOutputDir("zig-out");
|
||||||
|
|
||||||
|
// Wasm-specific deps
|
||||||
wasm.addPackage(tracylib.pkg);
|
wasm.addPackage(tracylib.pkg);
|
||||||
|
wasm.addPackage(utf8proc.pkg);
|
||||||
|
_ = try utf8proc.link(b, wasm);
|
||||||
|
|
||||||
const step = b.step("term-wasm", "Build the terminal.wasm library");
|
const step = b.step("term-wasm", "Build the terminal.wasm library");
|
||||||
step.dependOn(&wasm.step);
|
step.dependOn(&wasm.step);
|
||||||
|
@ -71,6 +71,15 @@ pub const Cell = struct {
|
|||||||
/// should have this set. The first cell of the next row is actually
|
/// should have this set. The first cell of the next row is actually
|
||||||
/// part of this row in raw input.
|
/// part of this row in raw input.
|
||||||
wrap: u1 = 0,
|
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
|
/// True if the cell should be skipped for drawing
|
||||||
|
@ -6,9 +6,11 @@ const Terminal = @This();
|
|||||||
|
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
const builtin = @import("builtin");
|
const builtin = @import("builtin");
|
||||||
|
const utf8proc = @import("utf8proc");
|
||||||
const testing = std.testing;
|
const testing = std.testing;
|
||||||
const assert = std.debug.assert;
|
const assert = std.debug.assert;
|
||||||
const Allocator = std.mem.Allocator;
|
const Allocator = std.mem.Allocator;
|
||||||
|
|
||||||
const ansi = @import("ansi.zig");
|
const ansi = @import("ansi.zig");
|
||||||
const csi = @import("csi.zig");
|
const csi = @import("csi.zig");
|
||||||
const sgr = @import("sgr.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 we're not on the main display, do nothing for now
|
||||||
if (self.status_display != .main) return;
|
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 we're soft-wrapping, then handle that first.
|
||||||
if (self.screen.cursor.pending_wrap and self.modes.autowrap == 1) {
|
if (self.screen.cursor.pending_wrap and self.modes.autowrap == 1)
|
||||||
// Mark that the cell is wrapped, which guarantees that there is
|
_ = self.printWrap();
|
||||||
// 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
|
switch (width) {
|
||||||
self.index();
|
// Single cell is very easy: just write in the cell
|
||||||
self.screen.cursor.x = 0;
|
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
|
// Move the cursor
|
||||||
self.screen.cursor.x += 1;
|
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'
|
/// Resets all margins and fills the whole screen with the character 'E'
|
||||||
///
|
///
|
||||||
/// Sets the cursor to the top left corner.
|
/// Sets the cursor to the top left corner.
|
||||||
|
Reference in New Issue
Block a user