a modern tui library written in zig
fork

Configure Feed

Select the types of activity you want to include in your feed.

vaxis: use vt caps to measure grapheme widths

Implement our own grapheme measuring function which switches on whether
the terminal supports mode 2027

Signed-off-by: Tim Culverhouse <tim@timculverhouse.com>

+30 -9
+2
src/InternalScreen.zig
··· 10 10 pub const InternalCell = struct { 11 11 char: std.ArrayList(u8) = undefined, 12 12 style: Style = .{}, 13 + // if we got skipped because of a wide character 14 + skipped: bool = false, 13 15 14 16 pub fn eql(self: InternalCell, cell: Cell) bool { 15 17 return std.mem.eql(u8, self.char.items, cell.char.grapheme) and std.meta.eql(self.style, cell.style);
+2
src/Screen.zig
··· 16 16 cursor_col: usize = 0, 17 17 cursor_vis: bool = false, 18 18 19 + unicode: bool = false, 20 + 19 21 pub fn init(alloc: std.mem.Allocator, w: usize, h: usize) !Screen { 20 22 var self = Screen{ 21 23 .buf = try alloc.alloc(Cell, w * h),
+4
src/Tty.zig
··· 178 178 } 179 179 }, 180 180 .cap_kitty_keyboard => { 181 + log.info("kitty capability detected", .{}); 181 182 vx.caps.kitty_keyboard = true; 182 183 }, 183 184 .cap_rgb => { 185 + log.info("rgb capability detected", .{}); 184 186 vx.caps.rgb = true; 185 187 }, 186 188 .cap_unicode => { 189 + log.info("unicode capability detected", .{}); 187 190 vx.caps.unicode = true; 191 + vx.screen.unicode = true; 188 192 }, 189 193 .cap_da1 => { 190 194 std.Thread.Futex.wake(&vx.query_futex, 10);
+8
src/Window.zig
··· 2 2 3 3 const Screen = @import("Screen.zig"); 4 4 const Cell = @import("cell.zig").Cell; 5 + const gw = @import("gwidth.zig"); 5 6 6 7 const log = std.log.scoped(.window); 7 8 ··· 70 71 /// fills the window with the default cell 71 72 pub fn clear(self: Window) void { 72 73 self.fill(.{}); 74 + } 75 + 76 + /// returns the width of the grapheme. This depends on the terminal capabilities 77 + pub fn gwidth(self: Window, str: []const u8) usize { 78 + const m: gw.Method = if (self.screen.unicode) .unicode else .wcwidth; 79 + log.info("using method {any}", .{m}); 80 + return gw.gwidth(str, m) catch 1; 73 81 } 74 82 75 83 /// fills the window with the provided cell
+13 -7
src/vaxis.zig
··· 12 12 const Options = @import("Options.zig"); 13 13 const Style = @import("cell.zig").Style; 14 14 const strWidth = @import("ziglyph").display_width.strWidth; 15 + const gwidth = @import("gwidth.zig"); 15 16 16 17 /// Vaxis is the entrypoint for a Vaxis application. The provided type T should 17 18 /// be a tagged union which contains all of the events the application will ··· 141 142 log.debug("resizing screen: width={d} height={d}", .{ winsize.cols, winsize.rows }); 142 143 self.screen.deinit(alloc); 143 144 self.screen = try Screen.init(alloc, winsize.cols, winsize.rows); 145 + self.screen.unicode = self.caps.unicode; 144 146 // try self.screen.int(alloc, winsize.cols, winsize.rows); 145 147 // we only init our current screen. This has the effect of redrawing 146 148 // every cell ··· 270 272 const cell = self.screen.buf[i]; 271 273 defer { 272 274 // advance by the width of this char mod 1 273 - const width = blk: { 274 - if (cell.char.width > 0) break :blk cell.char.width; 275 - break :blk strWidth(cell.char.grapheme, .half) catch 1; 276 - }; 277 - col += width; 278 - i += width; 275 + const method: gwidth.Method = if (self.caps.unicode) .unicode else .wcwidth; 276 + const w = gwidth.gwidth(cell.char.grapheme, method) catch 1; 277 + var j = i + 1; 278 + while (j < i + w) : (j += 1) { 279 + self.screen_last.buf[j].skipped = true; 280 + } 281 + col += w; 282 + i += w; 279 283 } 280 284 if (col >= self.screen.width) { 281 285 row += 1; ··· 283 287 } 284 288 // If cell is the same as our last frame, we don't need to do 285 289 // anything 286 - if (!self.refresh and self.screen_last.buf[i].eql(cell)) { 290 + const last = self.screen_last.buf[i]; 291 + if (!self.refresh and last.eql(cell) and !last.skipped) { 287 292 reposition = true; 288 293 // Close any osc8 sequence we might be in before 289 294 // repositioning ··· 292 297 } 293 298 continue; 294 299 } 300 + self.screen_last.buf[i].skipped = false; 295 301 defer cursor = cell.style; 296 302 // Set this cell in the last frame 297 303 self.screen_last.writeCell(col, row, cell.char.grapheme, cell.style);
+1 -2
src/widgets/TextInput.zig
··· 3 3 const Key = @import("../Key.zig"); 4 4 const Window = @import("../Window.zig"); 5 5 const GraphemeIterator = @import("ziglyph").GraphemeIterator; 6 - const strWidth = @import("ziglyph").display_width.strWidth; 7 6 8 7 const log = std.log.scoped(.text_input); 9 8 ··· 67 66 var cursor_idx: usize = 0; 68 67 while (iter.next()) |grapheme| { 69 68 const g = grapheme.slice(self.buf.items); 70 - const w = strWidth(g, .half) catch 1; 69 + const w = win.gwidth(g); 71 70 win.writeCell(col, 0, .{ 72 71 .char = .{ 73 72 .grapheme = g,