a modern tui library written in zig

Compare changes

Choose any two refs to compare.

+1 -1
README.md
··· 325 325 326 326 // init our text input widget. The text input widget needs an allocator to 327 327 // store the contents of the input 328 - var text_input = TextInput.init(alloc, &vx.unicode); 328 + var text_input = TextInput.init(alloc); 329 329 defer text_input.deinit(); 330 330 331 331 // Sends queries to terminal to detect certain features. This should always
+62
bench/bench.zig
··· 1 + const std = @import("std"); 2 + const vaxis = @import("vaxis"); 3 + 4 + fn parseIterations(allocator: std.mem.Allocator) !usize { 5 + var args = try std.process.argsWithAllocator(allocator); 6 + defer args.deinit(); 7 + _ = args.next(); 8 + if (args.next()) |val| { 9 + return std.fmt.parseUnsigned(usize, val, 10); 10 + } 11 + return 200; 12 + } 13 + 14 + fn printResults(writer: anytype, label: []const u8, iterations: usize, elapsed_ns: u64, total_bytes: usize) !void { 15 + const ns_per_frame = elapsed_ns / @as(u64, @intCast(iterations)); 16 + const bytes_per_frame = total_bytes / iterations; 17 + try writer.print( 18 + "{s}: frames={d} total_ns={d} ns/frame={d} bytes={d} bytes/frame={d}\n", 19 + .{ label, iterations, elapsed_ns, ns_per_frame, total_bytes, bytes_per_frame }, 20 + ); 21 + } 22 + 23 + pub fn main() !void { 24 + var gpa = std.heap.GeneralPurposeAllocator(.{}){}; 25 + defer _ = gpa.deinit(); 26 + const allocator = gpa.allocator(); 27 + 28 + const iterations = try parseIterations(allocator); 29 + 30 + var vx = try vaxis.init(allocator, .{}); 31 + var init_writer = std.io.Writer.Allocating.init(allocator); 32 + defer init_writer.deinit(); 33 + defer vx.deinit(allocator, &init_writer.writer); 34 + 35 + const winsize = vaxis.Winsize{ .rows = 24, .cols = 80, .x_pixel = 0, .y_pixel = 0 }; 36 + try vx.resize(allocator, &init_writer.writer, winsize); 37 + 38 + const stdout = std.fs.File.stdout().deprecatedWriter(); 39 + 40 + var idle_writer = std.io.Writer.Allocating.init(allocator); 41 + defer idle_writer.deinit(); 42 + var timer = try std.time.Timer.start(); 43 + var i: usize = 0; 44 + while (i < iterations) : (i += 1) { 45 + try vx.render(&idle_writer.writer); 46 + } 47 + const idle_ns = timer.read(); 48 + const idle_bytes: usize = idle_writer.writer.end; 49 + try printResults(stdout, "idle", iterations, idle_ns, idle_bytes); 50 + 51 + var dirty_writer = std.io.Writer.Allocating.init(allocator); 52 + defer dirty_writer.deinit(); 53 + timer.reset(); 54 + i = 0; 55 + while (i < iterations) : (i += 1) { 56 + vx.queueRefresh(); 57 + try vx.render(&dirty_writer.writer); 58 + } 59 + const dirty_ns = timer.read(); 60 + const dirty_bytes: usize = dirty_writer.writer.end; 61 + try printResults(stdout, "dirty", iterations, dirty_ns, dirty_bytes); 62 + }
+32 -9
build.zig
··· 6 6 const root_source_file = b.path("src/main.zig"); 7 7 8 8 // Dependencies 9 - const zg_dep = b.dependency("zg", .{ 9 + const zigimg_dep = b.dependency("zigimg", .{ 10 10 .optimize = optimize, 11 11 .target = target, 12 12 }); 13 - const zigimg_dep = b.dependency("zigimg", .{ 13 + const uucode_dep = b.dependency("uucode", .{ 14 + .target = target, 14 15 .optimize = optimize, 15 - .target = target, 16 + .fields = @as([]const []const u8, &.{ 17 + "east_asian_width", 18 + "grapheme_break", 19 + "general_category", 20 + "is_emoji_presentation", 21 + }), 16 22 }); 17 23 18 24 // Module ··· 21 27 .target = target, 22 28 .optimize = optimize, 23 29 }); 24 - vaxis_mod.addImport("code_point", zg_dep.module("code_point")); 25 - vaxis_mod.addImport("Graphemes", zg_dep.module("Graphemes")); 26 - vaxis_mod.addImport("DisplayWidth", zg_dep.module("DisplayWidth")); 27 30 vaxis_mod.addImport("zigimg", zigimg_dep.module("zigimg")); 31 + vaxis_mod.addImport("uucode", uucode_dep.module("uucode")); 28 32 29 33 // Examples 30 34 const Example = enum { ··· 37 41 split_view, 38 42 table, 39 43 text_input, 44 + text_view, 45 + list_view, 40 46 vaxis, 41 47 view, 42 48 vt, ··· 60 66 const example_run = b.addRunArtifact(example); 61 67 example_step.dependOn(&example_run.step); 62 68 69 + // Benchmarks 70 + const bench_step = b.step("bench", "Run benchmarks"); 71 + const bench = b.addExecutable(.{ 72 + .name = "bench", 73 + .root_module = b.createModule(.{ 74 + .root_source_file = b.path("bench/bench.zig"), 75 + .target = target, 76 + .optimize = optimize, 77 + .imports = &.{ 78 + .{ .name = "vaxis", .module = vaxis_mod }, 79 + }, 80 + }), 81 + }); 82 + const bench_run = b.addRunArtifact(bench); 83 + if (b.args) |args| { 84 + bench_run.addArgs(args); 85 + } 86 + bench_step.dependOn(&bench_run.step); 87 + 63 88 // Tests 64 89 const tests_step = b.step("test", "Run tests"); 65 90 ··· 69 94 .target = target, 70 95 .optimize = optimize, 71 96 .imports = &.{ 72 - .{ .name = "code_point", .module = zg_dep.module("code_point") }, 73 - .{ .name = "Graphemes", .module = zg_dep.module("Graphemes") }, 74 - .{ .name = "DisplayWidth", .module = zg_dep.module("DisplayWidth") }, 75 97 .{ .name = "zigimg", .module = zigimg_dep.module("zigimg") }, 98 + .{ .name = "uucode", .module = uucode_dep.module("uucode") }, 76 99 }, 77 100 }), 78 101 });
+5 -7
build.zig.zon
··· 5 5 .minimum_zig_version = "0.15.1", 6 6 .dependencies = .{ 7 7 .zigimg = .{ 8 - // TODO .url = "git+https://github.com/zigimg/zigimg", 9 - .url = "https://github.com/ivanstepanovftw/zigimg/archive/d7b7ab0ba0899643831ef042bd73289510b39906.tar.gz", 10 - .hash = "zigimg-0.1.0-8_eo2vHnEwCIVW34Q14Ec-xUlzIoVg86-7FU2ypPtxms", 8 + .url = "git+https://github.com/zigimg/zigimg#eab2522c023b9259db8b13f2f90d609b7437e5f6", 9 + .hash = "zigimg-0.1.0-8_eo2vUZFgAAtN1c6dAO5DdqL0d4cEWHtn6iR5ucZJti", 11 10 }, 12 - .zg = .{ 13 - // Upstream PR: https://codeberg.org/atman/zg/pulls/90/ 14 - .url = "https://codeberg.org/chaten/zg/archive/749197a3f9d25e211615960c02380a3d659b20f9.tar.gz", 15 - .hash = "zg-0.15.1-oGqU3M0-tALZCy7boQS86znlBloyKx6--JriGlY0Paa9", 11 + .uucode = .{ 12 + .url = "git+https://github.com/jacobsandlund/uucode#5f05f8f83a75caea201f12cc8ea32a2d82ea9732", 13 + .hash = "uucode-0.1.0-ZZjBPj96QADXyt5sqwBJUnhaDYs_qBeeKijZvlRa0eqM", 16 14 }, 17 15 }, 18 16 .paths = .{
+1 -1
examples/cli.zig
··· 29 29 30 30 try vx.queryTerminal(tty.writer(), 1 * std.time.ns_per_s); 31 31 32 - var text_input = TextInput.init(alloc, &vx.unicode); 32 + var text_input = TextInput.init(alloc); 33 33 defer text_input.deinit(); 34 34 35 35 var selected_option: ?usize = null;
+70 -60
examples/fuzzy.zig
··· 4 4 5 5 const Model = struct { 6 6 list: std.ArrayList(vxfw.Text), 7 + /// Memory owned by .arena 7 8 filtered: std.ArrayList(vxfw.RichText), 8 9 list_view: vxfw.ListView, 9 10 text_field: vxfw.TextField, 11 + 12 + /// Used for filtered RichText Spans and result 13 + arena: std.heap.ArenaAllocator, 14 + filtered: std.ArrayList(vxfw.RichText), 10 15 result: []const u8, 11 - unicode_data: *const vaxis.Unicode, 16 + 17 + pub fn init(gpa: std.mem.Allocator) !*Model { 18 + const model = try gpa.create(Model); 19 + errdefer gpa.destroy(model); 20 + 21 + model.* = .{ 22 + .list = .empty, 23 + .filtered = .empty, 24 + .list_view = .{ 25 + .children = .{ 26 + .builder = .{ 27 + .userdata = model, 28 + .buildFn = Model.widgetBuilder, 29 + }, 30 + }, 31 + }, 32 + .text_field = .{ 33 + .buf = vxfw.TextField.Buffer.init(gpa), 34 + .userdata = model, 35 + .onChange = Model.onChange, 36 + .onSubmit = Model.onSubmit, 37 + }, 38 + .result = "", 39 + .arena = std.heap.ArenaAllocator.init(gpa), 40 + }; 41 + 42 + return model; 43 + } 12 44 13 - /// Used for filtered RichText Spans 14 - arena: std.heap.ArenaAllocator, 45 + pub fn deinit(self: *Model, gpa: std.mem.Allocator) void { 46 + self.arena.deinit(); 47 + self.text_field.deinit(); 48 + self.list.deinit(gpa); 49 + gpa.destroy(self); 50 + } 15 51 16 52 pub fn widget(self: *Model) vxfw.Widget { 17 53 return .{ ··· 26 62 switch (event) { 27 63 .init => { 28 64 // Initialize the filtered list 29 - const allocator = self.arena.allocator(); 65 + const arena = self.arena.allocator(); 30 66 for (self.list.items) |line| { 31 - var spans = std.ArrayList(vxfw.RichText.TextSpan){}; 67 + var spans = std.ArrayList(vxfw.RichText.TextSpan).empty; 32 68 const span: vxfw.RichText.TextSpan = .{ .text = line.text }; 33 - try spans.append(allocator, span); 34 - try self.filtered.append(allocator, .{ .text = spans.items }); 69 + try spans.append(arena, span); 70 + try self.filtered.append(arena, .{ .text = spans.items }); 35 71 } 36 72 37 73 return ctx.requestFocus(self.text_field.widget()); ··· 100 136 fn onChange(maybe_ptr: ?*anyopaque, _: *vxfw.EventContext, str: []const u8) anyerror!void { 101 137 const ptr = maybe_ptr orelse return; 102 138 const self: *Model = @ptrCast(@alignCast(ptr)); 103 - const allocator = self.arena.allocator(); 104 - self.filtered.clearAndFree(allocator); 139 + const arena = self.arena.allocator(); 140 + self.filtered.clearAndFree(arena); 105 141 _ = self.arena.reset(.free_all); 106 142 107 143 const hasUpper = for (str) |b| { ··· 115 151 const tgt = if (hasUpper) 116 152 item.text 117 153 else 118 - try toLower(allocator, item.text); 154 + try toLower(arena, item.text); 119 155 120 - var spans = std.ArrayList(vxfw.RichText.TextSpan){}; 156 + var spans = std.ArrayList(vxfw.RichText.TextSpan).empty; 121 157 var i: usize = 0; 122 - var iter = self.unicode_data.graphemeIterator(str); 158 + var iter = vaxis.unicode.graphemeIterator(str); 123 159 while (iter.next()) |g| { 124 160 if (std.mem.indexOfPos(u8, tgt, i, g.bytes(str))) |idx| { 125 161 const up_to_here: vxfw.RichText.TextSpan = .{ .text = item.text[i..idx] }; ··· 127 163 .text = item.text[idx .. idx + g.len], 128 164 .style = .{ .fg = .{ .index = 4 }, .reverse = true }, 129 165 }; 130 - try spans.append(allocator, up_to_here); 131 - try spans.append(allocator, match); 166 + try spans.append(arena, up_to_here); 167 + try spans.append(arena, match); 132 168 i = idx + g.len; 133 169 } else continue :outer; 134 170 } 135 171 const up_to_here: vxfw.RichText.TextSpan = .{ .text = item.text[i..] }; 136 - try spans.append(allocator, up_to_here); 137 - try self.filtered.append(allocator, .{ .text = spans.items }); 172 + try spans.append(arena, up_to_here); 173 + try self.filtered.append(arena, .{ .text = spans.items }); 138 174 } 139 175 self.list_view.scroll.top = 0; 140 176 self.list_view.scroll.offset = 0; ··· 146 182 const self: *Model = @ptrCast(@alignCast(ptr)); 147 183 if (self.list_view.cursor < self.filtered.items.len) { 148 184 const selected = self.filtered.items[self.list_view.cursor]; 149 - const allocator = self.arena.allocator(); 150 - var result = std.ArrayList(u8){}; 185 + const arena = self.arena.allocator(); 186 + var result = std.ArrayList(u8).empty; 151 187 for (selected.text) |span| { 152 - try result.appendSlice(allocator, span.text); 188 + try result.appendSlice(arena, span.text); 153 189 } 154 190 self.result = result.items; 155 191 } ··· 157 193 } 158 194 }; 159 195 160 - fn toLower(allocator: std.mem.Allocator, src: []const u8) std.mem.Allocator.Error![]const u8 { 161 - const lower = try allocator.alloc(u8, src.len); 196 + fn toLower(arena: std.mem.Allocator, src: []const u8) std.mem.Allocator.Error![]const u8 { 197 + const lower = try arena.alloc(u8, src.len); 162 198 for (src, 0..) |b, i| { 163 199 lower[i] = std.ascii.toLower(b); 164 200 } ··· 166 202 } 167 203 168 204 pub fn main() !void { 169 - var gpa = std.heap.GeneralPurposeAllocator(.{}){}; 170 - defer _ = gpa.deinit(); 205 + var debug_allocator = std.heap.GeneralPurposeAllocator(.{}){}; 206 + defer _ = debug_allocator.deinit(); 171 207 172 - const allocator = gpa.allocator(); 208 + const gpa = debug_allocator.allocator(); 173 209 174 - var app = try vxfw.App.init(allocator); 210 + var app = try vxfw.App.init(gpa); 175 211 errdefer app.deinit(); 176 212 177 - const model = try allocator.create(Model); 178 - defer allocator.destroy(model); 179 - model.* = .{ 180 - .list = std.ArrayList(vxfw.Text){}, 181 - .filtered = std.ArrayList(vxfw.RichText){}, 182 - .list_view = .{ 183 - .children = .{ 184 - .builder = .{ 185 - .userdata = model, 186 - .buildFn = Model.widgetBuilder, 187 - }, 188 - }, 189 - }, 190 - .text_field = .{ 191 - .buf = vxfw.TextField.Buffer.init(allocator), 192 - .unicode = &app.vx.unicode, 193 - .userdata = model, 194 - .onChange = Model.onChange, 195 - .onSubmit = Model.onSubmit, 196 - }, 197 - .result = "", 198 - .arena = std.heap.ArenaAllocator.init(allocator), 199 - .unicode_data = &app.vx.unicode, 200 - }; 201 - defer model.text_field.deinit(); 202 - defer model.list.deinit(allocator); 203 - defer model.filtered.deinit(allocator); 204 - defer model.arena.deinit(); 213 + const model = try Model.init(gpa); 214 + defer model.deinit(gpa); 205 215 206 216 // Run the command 207 - var fd = std.process.Child.init(&.{"fd"}, allocator); 217 + var fd = std.process.Child.init(&.{"fd"}, gpa); 208 218 fd.stdout_behavior = .Pipe; 209 219 fd.stderr_behavior = .Pipe; 210 - var stdout = std.ArrayList(u8){}; 211 - var stderr = std.ArrayList(u8){}; 212 - defer stdout.deinit(allocator); 213 - defer stderr.deinit(allocator); 220 + var stdout = std.ArrayList(u8).empty; 221 + var stderr = std.ArrayList(u8).empty; 222 + defer stdout.deinit(gpa); 223 + defer stderr.deinit(gpa); 214 224 try fd.spawn(); 215 - try fd.collectOutput(allocator, &stdout, &stderr, 10_000_000); 225 + try fd.collectOutput(gpa, &stdout, &stderr, 10_000_000); 216 226 _ = try fd.wait(); 217 227 218 228 var iter = std.mem.splitScalar(u8, stdout.items, '\n'); 219 229 while (iter.next()) |line| { 220 230 if (line.len == 0) continue; 221 - try model.list.append(allocator, .{ .text = line }); 231 + try model.list.append(gpa, .{ .text = line }); 222 232 } 223 233 224 234 try app.run(model.widget(), .{});
+1 -1
examples/image.zig
··· 36 36 37 37 var read_buffer: [1024 * 1024]u8 = undefined; // 1MB buffer 38 38 var img1 = try vaxis.zigimg.Image.fromFilePath(alloc, "examples/zig.png", &read_buffer); 39 - defer img1.deinit(); 39 + defer img1.deinit(alloc); 40 40 41 41 const imgs = [_]vaxis.Image{ 42 42 try vx.transmitImage(alloc, tty.writer(), &img1, .rgba),
+99
examples/list_view.zig
··· 1 + const std = @import("std"); 2 + const vaxis = @import("vaxis"); 3 + const vxfw = vaxis.vxfw; 4 + 5 + const Text = vxfw.Text; 6 + const ListView = vxfw.ListView; 7 + const Widget = vxfw.Widget; 8 + 9 + const Model = struct { 10 + list_view: ListView, 11 + 12 + pub fn widget(self: *Model) Widget { 13 + return .{ 14 + .userdata = self, 15 + .eventHandler = Model.typeErasedEventHandler, 16 + .drawFn = Model.typeErasedDrawFn, 17 + }; 18 + } 19 + 20 + pub fn typeErasedEventHandler(ptr: *anyopaque, ctx: *vxfw.EventContext, event: vxfw.Event) anyerror!void { 21 + const self: *Model = @ptrCast(@alignCast(ptr)); 22 + try ctx.requestFocus(self.list_view.widget()); 23 + switch (event) { 24 + .key_press => |key| { 25 + if (key.matches('q', .{}) or key.matchExact('c', .{ .ctrl = true })) { 26 + ctx.quit = true; 27 + return; 28 + } 29 + }, 30 + else => {}, 31 + } 32 + } 33 + 34 + fn typeErasedDrawFn(ptr: *anyopaque, ctx: vxfw.DrawContext) std.mem.Allocator.Error!vxfw.Surface { 35 + const self: *Model = @ptrCast(@alignCast(ptr)); 36 + const max = ctx.max.size(); 37 + 38 + const list_view: vxfw.SubSurface = .{ 39 + .origin = .{ .row = 1, .col = 1 }, 40 + .surface = try self.list_view.draw(ctx), 41 + }; 42 + 43 + const children = try ctx.arena.alloc(vxfw.SubSurface, 1); 44 + children[0] = list_view; 45 + 46 + return .{ 47 + .size = max, 48 + .widget = self.widget(), 49 + .buffer = &.{}, 50 + .children = children, 51 + }; 52 + } 53 + }; 54 + 55 + pub fn main() !void { 56 + var gpa = std.heap.GeneralPurposeAllocator(.{}){}; 57 + defer _ = gpa.deinit(); 58 + 59 + const allocator = gpa.allocator(); 60 + 61 + var app = try vxfw.App.init(allocator); 62 + defer app.deinit(); 63 + 64 + const model = try allocator.create(Model); 65 + defer allocator.destroy(model); 66 + 67 + const n = 80; 68 + var texts = try std.ArrayList(Widget).initCapacity(allocator, n); 69 + 70 + var allocs = try std.ArrayList(*Text).initCapacity(allocator, n); 71 + defer { 72 + for (allocs.items) |tw| { 73 + allocator.free(tw.text); 74 + allocator.destroy(tw); 75 + } 76 + allocs.deinit(allocator); 77 + texts.deinit(allocator); 78 + } 79 + 80 + for (0..n) |i| { 81 + const t = std.fmt.allocPrint(allocator, "List Item {d}", .{i}) catch "placeholder"; 82 + const tw = try allocator.create(Text); 83 + tw.* = .{ .text = t }; 84 + _ = try allocs.append(allocator, tw); 85 + _ = try texts.append(allocator, tw.widget()); 86 + } 87 + 88 + model.* = .{ 89 + .list_view = .{ 90 + .wheel_scroll = 3, 91 + .scroll = .{ 92 + .wants_cursor = true, 93 + }, 94 + .children = .{ .slice = texts.items }, 95 + }, 96 + }; 97 + 98 + try app.run(model.widget(), .{}); 99 + }
+6 -10
examples/table.zig
··· 21 21 22 22 // Users set up below the main function 23 23 const users_buf = try alloc.dupe(User, users[0..]); 24 - var user_list = std.ArrayList(User).fromOwnedSlice(users_buf); 25 - defer user_list.deinit(alloc); 26 - var user_mal = std.MultiArrayList(User){}; 27 - for (users_buf[0..]) |user| try user_mal.append(alloc, user); 28 - defer user_mal.deinit(alloc); 29 24 30 25 var buffer: [1024]u8 = undefined; 31 26 var tty = try vaxis.Tty.init(&buffer); ··· 66 61 }; 67 62 var title_segs = [_]vaxis.Cell.Segment{ title_logo, title_info, title_disclaimer }; 68 63 69 - var cmd_input = vaxis.widgets.TextInput.init(alloc, &vx.unicode); 64 + var cmd_input = vaxis.widgets.TextInput.init(alloc); 70 65 defer cmd_input.deinit(); 71 66 72 67 // Colors ··· 178 173 mem.eql(u8, ":quit", cmd) or 179 174 mem.eql(u8, ":exit", cmd)) return; 180 175 if (mem.eql(u8, "G", cmd)) { 181 - demo_tbl.row = @intCast(user_list.items.len - 1); 176 + demo_tbl.row = @intCast(users_buf.len - 1); 182 177 active = .mid; 183 178 } 184 179 if (cmd.len >= 2 and mem.eql(u8, "gg", cmd[0..2])) { ··· 277 272 .width = win.width, 278 273 .height = win.height - (top_bar.height + 1), 279 274 }); 280 - if (user_list.items.len > 0) { 275 + if (users_buf.len > 0) { 281 276 demo_tbl.active = active == .mid; 282 277 try vaxis.widgets.Table.drawTable( 283 - event_alloc, 278 + null, 279 + // event_alloc, 284 280 middle_bar, 285 281 //users_buf[0..], 286 282 //user_list, 287 - user_mal, 283 + users_buf, 288 284 &demo_tbl, 289 285 ); 290 286 }
+1 -1
examples/text_input.zig
··· 63 63 64 64 // init our text input widget. The text input widget needs an allocator to 65 65 // store the contents of the input 66 - var text_input = TextInput.init(alloc, &vx.unicode); 66 + var text_input = TextInput.init(alloc); 67 67 defer text_input.deinit(); 68 68 69 69 try vx.setMouseMode(writer, true);
+66
examples/text_view.zig
··· 1 + const std = @import("std"); 2 + const log = std.log.scoped(.main); 3 + const vaxis = @import("vaxis"); 4 + 5 + const TextView = vaxis.widgets.TextView; 6 + 7 + const Event = union(enum) { 8 + key_press: vaxis.Key, 9 + winsize: vaxis.Winsize, 10 + }; 11 + 12 + pub fn main() !void { 13 + var gpa = std.heap.GeneralPurposeAllocator(.{}){}; 14 + 15 + defer { 16 + const deinit_status = gpa.deinit(); 17 + if (deinit_status == .leak) { 18 + log.err("memory leak", .{}); 19 + } 20 + } 21 + 22 + const alloc = gpa.allocator(); 23 + var buffer: [1024]u8 = undefined; 24 + var tty = try vaxis.Tty.init(&buffer); 25 + defer tty.deinit(); 26 + var vx = try vaxis.init(alloc, .{}); 27 + defer vx.deinit(alloc, tty.writer()); 28 + var loop: vaxis.Loop(Event) = .{ 29 + .vaxis = &vx, 30 + .tty = &tty, 31 + }; 32 + try loop.init(); 33 + try loop.start(); 34 + defer loop.stop(); 35 + try vx.enterAltScreen(tty.writer()); 36 + try vx.queryTerminal(tty.writer(), 20 * std.time.ns_per_s); 37 + var text_view = TextView{}; 38 + var text_view_buffer = TextView.Buffer{}; 39 + defer text_view_buffer.deinit(alloc); 40 + try text_view_buffer.append(alloc, .{ .bytes = "Press Enter to add a line, Up/Down to scroll, 'c' to close." }); 41 + 42 + var counter: i32 = 0; 43 + var lineBuf: [128]u8 = undefined; 44 + 45 + while (true) { 46 + const event = loop.nextEvent(); 47 + switch (event) { 48 + .key_press => |key| { 49 + // Close demo 50 + if (key.matches('c', .{})) break; 51 + if (key.matches(vaxis.Key.enter, .{})) { 52 + counter += 1; 53 + const new_content = try std.fmt.bufPrint(&lineBuf, "\nLine {d}", .{counter}); 54 + try text_view_buffer.append(alloc, .{ .bytes = new_content }); 55 + } 56 + text_view.input(key); 57 + }, 58 + .winsize => |ws| try vx.resize(alloc, tty.writer(), ws), 59 + } 60 + const win = vx.window(); 61 + win.clear(); 62 + text_view.draw(win, text_view_buffer); 63 + try vx.render(tty.writer()); 64 + try tty.writer.flush(); 65 + } 66 + }
+2 -2
examples/view.zig
··· 68 68 69 69 // Initialize Views 70 70 // - Large Map 71 - var lg_map_view = try View.init(alloc, &vx.unicode, .{ .width = lg_map_width, .height = lg_map_height }); 71 + var lg_map_view = try View.init(alloc, .{ .width = lg_map_width, .height = lg_map_height }); 72 72 defer lg_map_view.deinit(); 73 73 //w = lg_map_view.screen.width; 74 74 //h = lg_map_view.screen.height; ··· 76 76 _ = mem.replace(u8, lg_world_map, "\n", "", lg_map_buf[0..]); 77 77 _ = lg_map_view.printSegment(.{ .text = lg_map_buf[0..] }, .{ .wrap = .grapheme }); 78 78 // - Small Map 79 - var sm_map_view = try View.init(alloc, &vx.unicode, .{ .width = sm_map_width, .height = sm_map_height }); 79 + var sm_map_view = try View.init(alloc, .{ .width = sm_map_width, .height = sm_map_height }); 80 80 defer sm_map_view.deinit(); 81 81 w = sm_map_view.screen.width; 82 82 h = sm_map_view.screen.height;
-1
examples/vt.zig
··· 54 54 alloc, 55 55 &argv, 56 56 &env, 57 - &vx.unicode, 58 57 vt_opts, 59 58 &write_buf, 60 59 );
+18 -4
src/InternalScreen.zig
··· 83 83 row: u16, 84 84 cell: Cell, 85 85 ) void { 86 - if (self.width < col) { 86 + if (self.width <= col) { 87 87 // column out of bounds 88 88 return; 89 89 } 90 - if (self.height < row) { 90 + if (self.height <= row) { 91 91 // height out of bounds 92 92 return; 93 93 } ··· 110 110 } 111 111 112 112 pub fn readCell(self: *InternalScreen, col: u16, row: u16) ?Cell { 113 - if (self.width < col) { 113 + if (self.width <= col) { 114 114 // column out of bounds 115 115 return null; 116 116 } 117 - if (self.height < row) { 117 + if (self.height <= row) { 118 118 // height out of bounds 119 119 return null; 120 120 } ··· 131 131 .default = cell.default, 132 132 }; 133 133 } 134 + 135 + test "InternalScreen: out-of-bounds read/write are ignored" { 136 + var screen = try InternalScreen.init(std.testing.allocator, 2, 2); 137 + defer screen.deinit(std.testing.allocator); 138 + 139 + const sentinel: Cell = .{ .char = .{ .grapheme = "A", .width = 1 } }; 140 + screen.writeCell(0, 1, sentinel); 141 + 142 + const oob_cell: Cell = .{ .char = .{ .grapheme = "X", .width = 1 } }; 143 + screen.writeCell(2, 0, oob_cell); 144 + const read_back = screen.readCell(0, 1) orelse return error.TestUnexpectedResult; 145 + try std.testing.expect(std.mem.eql(u8, read_back.char.grapheme, "A")); 146 + try std.testing.expect(screen.readCell(2, 0) == null); 147 + }
+2 -10
src/Loop.zig
··· 1 1 const std = @import("std"); 2 2 const builtin = @import("builtin"); 3 3 4 - const Graphemes = @import("Graphemes"); 5 - 6 4 const GraphemeCache = @import("GraphemeCache.zig"); 7 5 const Parser = @import("Parser.zig"); 8 6 const Queue = @import("queue.zig").Queue; ··· 47 45 if (self.thread) |_| return; 48 46 self.thread = try std.Thread.spawn(.{}, Self.ttyRun, .{ 49 47 self, 50 - &self.vaxis.unicode.width_data.graphemes, 51 48 self.vaxis.opts.system_clipboard_allocator, 52 49 }); 53 50 } ··· 107 104 /// read input from the tty. This is run in a separate thread 108 105 fn ttyRun( 109 106 self: *Self, 110 - grapheme_data: *const Graphemes, 111 107 paste_allocator: ?std.mem.Allocator, 112 108 ) !void { 113 109 // Return early if we're in test mode to avoid infinite loops ··· 118 114 119 115 switch (builtin.os.tag) { 120 116 .windows => { 121 - var parser: Parser = .{ 122 - .grapheme_data = grapheme_data, 123 - }; 117 + var parser: Parser = .{}; 124 118 while (!self.should_quit) { 125 119 const event = try self.tty.nextEvent(&parser, paste_allocator); 126 120 try handleEventGeneric(self, self.vaxis, &cache, Event, event, null); ··· 133 127 self.postEvent(.{ .winsize = winsize }); 134 128 } 135 129 136 - var parser: Parser = .{ 137 - .grapheme_data = grapheme_data, 138 - }; 130 + var parser: Parser = .{}; 139 131 140 132 // initialize the read buffer 141 133 var buf: [1024]u8 = undefined;
+2 -2
src/Mouse.zig
··· 41 41 drag, 42 42 }; 43 43 44 - col: u16, 45 - row: u16, 44 + col: i16, 45 + row: i16, 46 46 xoffset: u16 = 0, 47 47 yoffset: u16 = 0, 48 48 button: Button,
+148 -92
src/Parser.zig
··· 4 4 const Event = @import("event.zig").Event; 5 5 const Key = @import("Key.zig"); 6 6 const Mouse = @import("Mouse.zig"); 7 - const code_point = @import("code_point"); 8 - const Graphemes = @import("Graphemes"); 7 + const uucode = @import("uucode"); 9 8 const Winsize = @import("main.zig").Winsize; 10 9 11 10 const log = std.log.scoped(.vaxis_parser); ··· 45 44 // a buffer to temporarily store text in. We need this to encode 46 45 // text-as-codepoints 47 46 buf: [128]u8 = undefined, 48 - 49 - grapheme_data: *const Graphemes, 50 47 51 48 /// Parse the first event from the input buffer. If a completion event is not 52 49 /// present, Result.event will be null and Result.n will be 0 ··· 78 75 }; 79 76 }, 80 77 } 81 - } else return parseGround(input, self.grapheme_data); 78 + } else return parseGround(input); 82 79 } 83 80 84 81 /// Parse ground state 85 - inline fn parseGround(input: []const u8, data: *const Graphemes) !Result { 82 + inline fn parseGround(input: []const u8) !Result { 86 83 std.debug.assert(input.len > 0); 87 84 88 85 const b = input[0]; ··· 109 106 }, 110 107 0x7F => .{ .codepoint = Key.backspace }, 111 108 else => blk: { 112 - var iter: code_point.Iterator = .{ .bytes = input }; 109 + var iter = uucode.utf8.Iterator.init(input); 113 110 // return null if we don't have a valid codepoint 114 - const cp = iter.next() orelse return error.InvalidUTF8; 111 + const first_cp = iter.next() orelse return error.InvalidUTF8; 115 112 116 - n = cp.len; 113 + n = std.unicode.utf8CodepointSequenceLength(first_cp) catch return error.InvalidUTF8; 117 114 118 115 // Check if we have a multi-codepoint grapheme 119 - var code = cp.code; 120 - var g_state: Graphemes.IterState = .{}; 121 - var prev_cp = code; 122 - while (iter.next()) |next_cp| { 123 - if (Graphemes.graphemeBreak(prev_cp, next_cp.code, data, &g_state)) { 116 + var code = first_cp; 117 + var grapheme_iter = uucode.grapheme.Iterator(uucode.utf8.Iterator).init(.init(input)); 118 + var grapheme_len: usize = 0; 119 + var cp_count: usize = 0; 120 + 121 + while (grapheme_iter.next()) |result| { 122 + cp_count += 1; 123 + if (result.is_break) { 124 + // Found the first grapheme boundary 125 + grapheme_len = grapheme_iter.i; 124 126 break; 125 127 } 126 - prev_cp = next_cp.code; 127 - code = Key.multicodepoint; 128 - n += next_cp.len; 128 + } 129 + 130 + if (grapheme_len > 0) { 131 + n = grapheme_len; 132 + if (cp_count > 1) { 133 + code = Key.multicodepoint; 134 + } 129 135 } 130 136 131 137 break :blk .{ .codepoint = code, .text = input[0..n] }; ··· 664 670 /// Parse a param buffer, returning a default value if the param was empty 665 671 inline fn parseParam(comptime T: type, buf: []const u8, default: ?T) ?T { 666 672 if (buf.len == 0) return default; 667 - return std.fmt.parseUnsigned(T, buf, 10) catch return null; 673 + return std.fmt.parseInt(T, buf, 10) catch return null; 668 674 } 669 675 670 676 /// Parse a mouse event ··· 672 678 const null_event: Result = .{ .event = null, .n = input.len }; 673 679 674 680 var button_mask: u16 = undefined; 675 - var px: u16 = undefined; 676 - var py: u16 = undefined; 681 + var px: i16 = undefined; 682 + var py: i16 = undefined; 677 683 var xterm: bool = undefined; 678 684 if (input.len == 3 and (input[2] == 'M') and full_input.len >= 6) { 679 685 xterm = true; ··· 685 691 const delim1 = std.mem.indexOfScalarPos(u8, input, 3, ';') orelse return null_event; 686 692 button_mask = parseParam(u16, input[3..delim1], null) orelse return null_event; 687 693 const delim2 = std.mem.indexOfScalarPos(u8, input, delim1 + 1, ';') orelse return null_event; 688 - px = parseParam(u16, input[delim1 + 1 .. delim2], 1) orelse return null_event; 689 - py = parseParam(u16, input[delim2 + 1 .. input.len - 1], 1) orelse return null_event; 694 + px = parseParam(i16, input[delim1 + 1 .. delim2], 1) orelse return null_event; 695 + py = parseParam(i16, input[delim2 + 1 .. input.len - 1], 1) orelse return null_event; 690 696 } else { 691 697 return null_event; 692 698 } ··· 731 737 732 738 test "parse: single xterm keypress" { 733 739 const alloc = testing.allocator_instance.allocator(); 734 - const grapheme_data = try Graphemes.init(alloc); 735 - defer grapheme_data.deinit(alloc); 736 740 const input = "a"; 737 - var parser: Parser = .{ .grapheme_data = &grapheme_data }; 741 + var parser: Parser = .{}; 738 742 const result = try parser.parse(input, alloc); 739 743 const expected_key: Key = .{ 740 744 .codepoint = 'a', ··· 748 752 749 753 test "parse: single xterm keypress backspace" { 750 754 const alloc = testing.allocator_instance.allocator(); 751 - const grapheme_data = try Graphemes.init(alloc); 752 - defer grapheme_data.deinit(alloc); 753 755 const input = "\x08"; 754 - var parser: Parser = .{ .grapheme_data = &grapheme_data }; 756 + var parser: Parser = .{}; 755 757 const result = try parser.parse(input, alloc); 756 758 const expected_key: Key = .{ 757 759 .codepoint = Key.backspace, ··· 764 766 765 767 test "parse: single xterm keypress with more buffer" { 766 768 const alloc = testing.allocator_instance.allocator(); 767 - const grapheme_data = try Graphemes.init(alloc); 768 - defer grapheme_data.deinit(alloc); 769 769 const input = "ab"; 770 - var parser: Parser = .{ .grapheme_data = &grapheme_data }; 770 + var parser: Parser = .{}; 771 771 const result = try parser.parse(input, alloc); 772 772 const expected_key: Key = .{ 773 773 .codepoint = 'a', ··· 782 782 783 783 test "parse: xterm escape keypress" { 784 784 const alloc = testing.allocator_instance.allocator(); 785 - const grapheme_data = try Graphemes.init(alloc); 786 - defer grapheme_data.deinit(alloc); 787 785 const input = "\x1b"; 788 - var parser: Parser = .{ .grapheme_data = &grapheme_data }; 786 + var parser: Parser = .{}; 789 787 const result = try parser.parse(input, alloc); 790 788 const expected_key: Key = .{ .codepoint = Key.escape }; 791 789 const expected_event: Event = .{ .key_press = expected_key }; ··· 796 794 797 795 test "parse: xterm ctrl+a" { 798 796 const alloc = testing.allocator_instance.allocator(); 799 - const grapheme_data = try Graphemes.init(alloc); 800 - defer grapheme_data.deinit(alloc); 801 797 const input = "\x01"; 802 - var parser: Parser = .{ .grapheme_data = &grapheme_data }; 798 + var parser: Parser = .{}; 803 799 const result = try parser.parse(input, alloc); 804 800 const expected_key: Key = .{ .codepoint = 'a', .mods = .{ .ctrl = true } }; 805 801 const expected_event: Event = .{ .key_press = expected_key }; ··· 810 806 811 807 test "parse: xterm alt+a" { 812 808 const alloc = testing.allocator_instance.allocator(); 813 - const grapheme_data = try Graphemes.init(alloc); 814 - defer grapheme_data.deinit(alloc); 815 809 const input = "\x1ba"; 816 - var parser: Parser = .{ .grapheme_data = &grapheme_data }; 810 + var parser: Parser = .{}; 817 811 const result = try parser.parse(input, alloc); 818 812 const expected_key: Key = .{ .codepoint = 'a', .mods = .{ .alt = true } }; 819 813 const expected_event: Event = .{ .key_press = expected_key }; ··· 824 818 825 819 test "parse: xterm key up" { 826 820 const alloc = testing.allocator_instance.allocator(); 827 - const grapheme_data = try Graphemes.init(alloc); 828 - defer grapheme_data.deinit(alloc); 829 821 { 830 822 // normal version 831 823 const input = "\x1b[A"; 832 - var parser: Parser = .{ .grapheme_data = &grapheme_data }; 824 + var parser: Parser = .{}; 833 825 const result = try parser.parse(input, alloc); 834 826 const expected_key: Key = .{ .codepoint = Key.up }; 835 827 const expected_event: Event = .{ .key_press = expected_key }; ··· 841 833 { 842 834 // application keys version 843 835 const input = "\x1bOA"; 844 - var parser: Parser = .{ .grapheme_data = &grapheme_data }; 836 + var parser: Parser = .{}; 845 837 const result = try parser.parse(input, alloc); 846 838 const expected_key: Key = .{ .codepoint = Key.up }; 847 839 const expected_event: Event = .{ .key_press = expected_key }; ··· 853 845 854 846 test "parse: xterm shift+up" { 855 847 const alloc = testing.allocator_instance.allocator(); 856 - const grapheme_data = try Graphemes.init(alloc); 857 - defer grapheme_data.deinit(alloc); 858 848 const input = "\x1b[1;2A"; 859 - var parser: Parser = .{ .grapheme_data = &grapheme_data }; 849 + var parser: Parser = .{}; 860 850 const result = try parser.parse(input, alloc); 861 851 const expected_key: Key = .{ .codepoint = Key.up, .mods = .{ .shift = true } }; 862 852 const expected_event: Event = .{ .key_press = expected_key }; ··· 867 857 868 858 test "parse: xterm insert" { 869 859 const alloc = testing.allocator_instance.allocator(); 870 - const grapheme_data = try Graphemes.init(alloc); 871 - defer grapheme_data.deinit(alloc); 872 860 const input = "\x1b[2~"; 873 - var parser: Parser = .{ .grapheme_data = &grapheme_data }; 861 + var parser: Parser = .{}; 874 862 const result = try parser.parse(input, alloc); 875 863 const expected_key: Key = .{ .codepoint = Key.insert, .mods = .{} }; 876 864 const expected_event: Event = .{ .key_press = expected_key }; ··· 881 869 882 870 test "parse: paste_start" { 883 871 const alloc = testing.allocator_instance.allocator(); 884 - const grapheme_data = try Graphemes.init(alloc); 885 - defer grapheme_data.deinit(alloc); 886 872 const input = "\x1b[200~"; 887 - var parser: Parser = .{ .grapheme_data = &grapheme_data }; 873 + var parser: Parser = .{}; 888 874 const result = try parser.parse(input, alloc); 889 875 const expected_event: Event = .paste_start; 890 876 ··· 894 880 895 881 test "parse: paste_end" { 896 882 const alloc = testing.allocator_instance.allocator(); 897 - const grapheme_data = try Graphemes.init(alloc); 898 - defer grapheme_data.deinit(alloc); 899 883 const input = "\x1b[201~"; 900 - var parser: Parser = .{ .grapheme_data = &grapheme_data }; 884 + var parser: Parser = .{}; 901 885 const result = try parser.parse(input, alloc); 902 886 const expected_event: Event = .paste_end; 903 887 ··· 907 891 908 892 test "parse: osc52 paste" { 909 893 const alloc = testing.allocator_instance.allocator(); 910 - const grapheme_data = try Graphemes.init(alloc); 911 - defer grapheme_data.deinit(alloc); 912 894 const input = "\x1b]52;c;b3NjNTIgcGFzdGU=\x1b\\"; 913 895 const expected_text = "osc52 paste"; 914 - var parser: Parser = .{ .grapheme_data = &grapheme_data }; 896 + var parser: Parser = .{}; 915 897 const result = try parser.parse(input, alloc); 916 898 917 899 try testing.expectEqual(25, result.n); ··· 926 908 927 909 test "parse: focus_in" { 928 910 const alloc = testing.allocator_instance.allocator(); 929 - const grapheme_data = try Graphemes.init(alloc); 930 - defer grapheme_data.deinit(alloc); 931 911 const input = "\x1b[I"; 932 - var parser: Parser = .{ .grapheme_data = &grapheme_data }; 912 + var parser: Parser = .{}; 933 913 const result = try parser.parse(input, alloc); 934 914 const expected_event: Event = .focus_in; 935 915 ··· 939 919 940 920 test "parse: focus_out" { 941 921 const alloc = testing.allocator_instance.allocator(); 942 - const grapheme_data = try Graphemes.init(alloc); 943 - defer grapheme_data.deinit(alloc); 944 922 const input = "\x1b[O"; 945 - var parser: Parser = .{ .grapheme_data = &grapheme_data }; 923 + var parser: Parser = .{}; 946 924 const result = try parser.parse(input, alloc); 947 925 const expected_event: Event = .focus_out; 948 926 ··· 952 930 953 931 test "parse: kitty: shift+a without text reporting" { 954 932 const alloc = testing.allocator_instance.allocator(); 955 - const grapheme_data = try Graphemes.init(alloc); 956 - defer grapheme_data.deinit(alloc); 957 933 const input = "\x1b[97:65;2u"; 958 - var parser: Parser = .{ .grapheme_data = &grapheme_data }; 934 + var parser: Parser = .{}; 959 935 const result = try parser.parse(input, alloc); 960 936 const expected_key: Key = .{ 961 937 .codepoint = 'a', ··· 971 947 972 948 test "parse: kitty: alt+shift+a without text reporting" { 973 949 const alloc = testing.allocator_instance.allocator(); 974 - const grapheme_data = try Graphemes.init(alloc); 975 - defer grapheme_data.deinit(alloc); 976 950 const input = "\x1b[97:65;4u"; 977 - var parser: Parser = .{ .grapheme_data = &grapheme_data }; 951 + var parser: Parser = .{}; 978 952 const result = try parser.parse(input, alloc); 979 953 const expected_key: Key = .{ 980 954 .codepoint = 'a', ··· 989 963 990 964 test "parse: kitty: a without text reporting" { 991 965 const alloc = testing.allocator_instance.allocator(); 992 - const grapheme_data = try Graphemes.init(alloc); 993 - defer grapheme_data.deinit(alloc); 994 966 const input = "\x1b[97u"; 995 - var parser: Parser = .{ .grapheme_data = &grapheme_data }; 967 + var parser: Parser = .{}; 996 968 const result = try parser.parse(input, alloc); 997 969 const expected_key: Key = .{ 998 970 .codepoint = 'a', ··· 1005 977 1006 978 test "parse: kitty: release event" { 1007 979 const alloc = testing.allocator_instance.allocator(); 1008 - const grapheme_data = try Graphemes.init(alloc); 1009 - defer grapheme_data.deinit(alloc); 1010 980 const input = "\x1b[97;1:3u"; 1011 - var parser: Parser = .{ .grapheme_data = &grapheme_data }; 981 + var parser: Parser = .{}; 1012 982 const result = try parser.parse(input, alloc); 1013 983 const expected_key: Key = .{ 1014 984 .codepoint = 'a', ··· 1021 991 1022 992 test "parse: single codepoint" { 1023 993 const alloc = testing.allocator_instance.allocator(); 1024 - const grapheme_data = try Graphemes.init(alloc); 1025 - defer grapheme_data.deinit(alloc); 1026 994 const input = "๐Ÿ™‚"; 1027 - var parser: Parser = .{ .grapheme_data = &grapheme_data }; 995 + var parser: Parser = .{}; 1028 996 const result = try parser.parse(input, alloc); 1029 997 const expected_key: Key = .{ 1030 998 .codepoint = 0x1F642, ··· 1038 1006 1039 1007 test "parse: single codepoint with more in buffer" { 1040 1008 const alloc = testing.allocator_instance.allocator(); 1041 - const grapheme_data = try Graphemes.init(alloc); 1042 - defer grapheme_data.deinit(alloc); 1043 1009 const input = "๐Ÿ™‚a"; 1044 - var parser: Parser = .{ .grapheme_data = &grapheme_data }; 1010 + var parser: Parser = .{}; 1045 1011 const result = try parser.parse(input, alloc); 1046 1012 const expected_key: Key = .{ 1047 1013 .codepoint = 0x1F642, ··· 1055 1021 1056 1022 test "parse: multiple codepoint grapheme" { 1057 1023 const alloc = testing.allocator_instance.allocator(); 1058 - const grapheme_data = try Graphemes.init(alloc); 1059 - defer grapheme_data.deinit(alloc); 1060 1024 const input = "๐Ÿ‘ฉโ€๐Ÿš€"; 1061 - var parser: Parser = .{ .grapheme_data = &grapheme_data }; 1025 + var parser: Parser = .{}; 1062 1026 const result = try parser.parse(input, alloc); 1063 1027 const expected_key: Key = .{ 1064 1028 .codepoint = Key.multicodepoint, ··· 1072 1036 1073 1037 test "parse: multiple codepoint grapheme with more after" { 1074 1038 const alloc = testing.allocator_instance.allocator(); 1075 - const grapheme_data = try Graphemes.init(alloc); 1076 - defer grapheme_data.deinit(alloc); 1077 1039 const input = "๐Ÿ‘ฉโ€๐Ÿš€abc"; 1078 - var parser: Parser = .{ .grapheme_data = &grapheme_data }; 1040 + var parser: Parser = .{}; 1079 1041 const result = try parser.parse(input, alloc); 1080 1042 const expected_key: Key = .{ 1081 1043 .codepoint = Key.multicodepoint, ··· 1088 1050 try testing.expectEqual(expected_key.codepoint, actual.codepoint); 1089 1051 } 1090 1052 1053 + test "parse: flag emoji" { 1054 + const alloc = testing.allocator_instance.allocator(); 1055 + const input = "๐Ÿ‡บ๐Ÿ‡ธ"; 1056 + var parser: Parser = .{}; 1057 + const result = try parser.parse(input, alloc); 1058 + const expected_key: Key = .{ 1059 + .codepoint = Key.multicodepoint, 1060 + .text = input, 1061 + }; 1062 + const expected_event: Event = .{ .key_press = expected_key }; 1063 + 1064 + try testing.expectEqual(input.len, result.n); 1065 + try testing.expectEqual(expected_event, result.event); 1066 + } 1067 + 1068 + test "parse: combining mark" { 1069 + const alloc = testing.allocator_instance.allocator(); 1070 + // a with combining acute accent (NFD form) 1071 + const input = "a\u{0301}"; 1072 + var parser: Parser = .{}; 1073 + const result = try parser.parse(input, alloc); 1074 + const expected_key: Key = .{ 1075 + .codepoint = Key.multicodepoint, 1076 + .text = input, 1077 + }; 1078 + const expected_event: Event = .{ .key_press = expected_key }; 1079 + 1080 + try testing.expectEqual(input.len, result.n); 1081 + try testing.expectEqual(expected_event, result.event); 1082 + } 1083 + 1084 + test "parse: skin tone emoji" { 1085 + const alloc = testing.allocator_instance.allocator(); 1086 + const input = "๐Ÿ‘‹๐Ÿฟ"; 1087 + var parser: Parser = .{}; 1088 + const result = try parser.parse(input, alloc); 1089 + const expected_key: Key = .{ 1090 + .codepoint = Key.multicodepoint, 1091 + .text = input, 1092 + }; 1093 + const expected_event: Event = .{ .key_press = expected_key }; 1094 + 1095 + try testing.expectEqual(input.len, result.n); 1096 + try testing.expectEqual(expected_event, result.event); 1097 + } 1098 + 1099 + test "parse: text variation selector" { 1100 + const alloc = testing.allocator_instance.allocator(); 1101 + // Heavy black heart with text variation selector 1102 + const input = "โค๏ธŽ"; 1103 + var parser: Parser = .{}; 1104 + const result = try parser.parse(input, alloc); 1105 + const expected_key: Key = .{ 1106 + .codepoint = Key.multicodepoint, 1107 + .text = input, 1108 + }; 1109 + const expected_event: Event = .{ .key_press = expected_key }; 1110 + 1111 + try testing.expectEqual(input.len, result.n); 1112 + try testing.expectEqual(expected_event, result.event); 1113 + } 1114 + 1115 + test "parse: keycap sequence" { 1116 + const alloc = testing.allocator_instance.allocator(); 1117 + const input = "1๏ธโƒฃ"; 1118 + var parser: Parser = .{}; 1119 + const result = try parser.parse(input, alloc); 1120 + const expected_key: Key = .{ 1121 + .codepoint = Key.multicodepoint, 1122 + .text = input, 1123 + }; 1124 + const expected_event: Event = .{ .key_press = expected_key }; 1125 + 1126 + try testing.expectEqual(input.len, result.n); 1127 + try testing.expectEqual(expected_event, result.event); 1128 + } 1129 + 1091 1130 test "parse(csi): kitty multi cursor" { 1092 1131 var buf: [1]u8 = undefined; 1093 1132 { ··· 1209 1248 try testing.expectEqual(expected.event, result.event); 1210 1249 } 1211 1250 1251 + test "parse(csi): mouse (negative)" { 1252 + var buf: [1]u8 = undefined; 1253 + const input = "\x1b[<35;-50;-100m"; 1254 + const result = parseCsi(input, &buf); 1255 + const expected: Result = .{ 1256 + .event = .{ .mouse = .{ 1257 + .col = -51, 1258 + .row = -101, 1259 + .button = .none, 1260 + .type = .motion, 1261 + .mods = .{}, 1262 + } }, 1263 + .n = input.len, 1264 + }; 1265 + 1266 + try testing.expectEqual(expected.n, result.n); 1267 + try testing.expectEqual(expected.event, result.event); 1268 + } 1269 + 1212 1270 test "parse(csi): xterm mouse" { 1213 1271 var buf: [1]u8 = undefined; 1214 1272 const input = "\x1b[M\x20\x21\x21"; ··· 1230 1288 1231 1289 test "parse: disambiguate shift + space" { 1232 1290 const alloc = testing.allocator_instance.allocator(); 1233 - const grapheme_data = try Graphemes.init(alloc); 1234 - defer grapheme_data.deinit(alloc); 1235 1291 const input = "\x1b[32;2u"; 1236 - var parser: Parser = .{ .grapheme_data = &grapheme_data }; 1292 + var parser: Parser = .{}; 1237 1293 const result = try parser.parse(input, alloc); 1238 1294 const expected_key: Key = .{ 1239 1295 .codepoint = ' ',
+4
src/Screen.zig
··· 64 64 return self.buf[i]; 65 65 } 66 66 67 + pub fn clear(self: *Screen) void { 68 + @memset(self.buf, .{}); 69 + } 70 + 67 71 test "refAllDecls" { 68 72 std.testing.refAllDecls(@This()); 69 73 }
-25
src/Unicode.zig
··· 1 - const std = @import("std"); 2 - const Graphemes = @import("Graphemes"); 3 - const DisplayWidth = @import("DisplayWidth"); 4 - 5 - /// A thin wrapper around zg data 6 - const Unicode = @This(); 7 - 8 - width_data: DisplayWidth, 9 - 10 - /// initialize all unicode data vaxis may possibly need 11 - pub fn init(alloc: std.mem.Allocator) !Unicode { 12 - return .{ 13 - .width_data = try DisplayWidth.init(alloc), 14 - }; 15 - } 16 - 17 - /// free all data 18 - pub fn deinit(self: *const Unicode, alloc: std.mem.Allocator) void { 19 - self.width_data.deinit(alloc); 20 - } 21 - 22 - /// creates a grapheme iterator based on str 23 - pub fn graphemeIterator(self: *const Unicode, str: []const u8) Graphemes.Iterator { 24 - return self.width_data.graphemes.iterator(str); 25 - }
+81 -43
src/Vaxis.zig
··· 11 11 const Key = @import("Key.zig"); 12 12 const Mouse = @import("Mouse.zig"); 13 13 const Screen = @import("Screen.zig"); 14 - const Unicode = @import("Unicode.zig"); 14 + const unicode = @import("unicode.zig"); 15 15 const Window = @import("Window.zig"); 16 16 17 17 const Hyperlink = Cell.Hyperlink; ··· 73 73 74 74 // images 75 75 next_img_id: u32 = 1, 76 - 77 - unicode: Unicode, 78 76 79 77 sgr: enum { 80 78 standard, ··· 110 108 .opts = opts, 111 109 .screen = .{}, 112 110 .screen_last = try .init(alloc, 0, 0), 113 - .unicode = try Unicode.init(alloc), 114 111 }; 115 112 } 116 113 ··· 124 121 if (alloc) |a| { 125 122 self.screen.deinit(a); 126 123 self.screen_last.deinit(a); 127 - self.unicode.deinit(a); 128 124 } 129 125 } 130 126 ··· 227 223 .width = self.screen.width, 228 224 .height = self.screen.height, 229 225 .screen = &self.screen, 230 - .unicode = &self.unicode, 231 226 }; 232 227 } 233 228 ··· 365 360 assert(self.screen.buf.len == @as(usize, @intCast(self.screen.width)) * self.screen.height); // correct size 366 361 assert(self.screen.buf.len == self.screen_last.buf.len); // same size 367 362 368 - // Set up sync before we write anything 369 - // TODO: optimize sync so we only sync _when we have changes_. This 370 - // requires a smarter buffered writer, we'll probably have to write 371 - // our own 372 - try tty.writeAll(ctlseqs.sync_set); 373 - errdefer tty.writeAll(ctlseqs.sync_reset) catch {}; 363 + var started: bool = false; 364 + var sync_active: bool = false; 365 + errdefer if (sync_active) tty.writeAll(ctlseqs.sync_reset) catch {}; 374 366 375 - // Send the cursor to 0,0 376 - // TODO: this needs to move after we optimize writes. We only do 377 - // this if we have an update to make. We also need to hide cursor 378 - // and then reshow it if needed 379 - try tty.writeAll(ctlseqs.hide_cursor); 380 - if (self.state.alt_screen) 381 - try tty.writeAll(ctlseqs.home) 382 - else { 383 - try tty.writeByte('\r'); 384 - for (0..self.state.cursor.row) |_| { 385 - try tty.writeAll(ctlseqs.ri); 386 - } 387 - } 388 - try tty.writeAll(ctlseqs.sgr_reset); 367 + const cursor_vis_changed = self.screen.cursor_vis != self.screen_last.cursor_vis; 368 + const cursor_shape_changed = self.screen.cursor_shape != self.screen_last.cursor_shape; 369 + const mouse_shape_changed = self.screen.mouse_shape != self.screen_last.mouse_shape; 370 + const cursor_pos_changed = self.screen.cursor_vis and 371 + (self.screen.cursor_row != self.state.cursor.row or 372 + self.screen.cursor_col != self.state.cursor.col); 373 + const needs_render = self.refresh or cursor_vis_changed or cursor_shape_changed or mouse_shape_changed or cursor_pos_changed; 389 374 390 375 // initialize some variables 391 376 var reposition: bool = false; ··· 393 378 var col: u16 = 0; 394 379 var cursor: Style = .{}; 395 380 var link: Hyperlink = .{}; 396 - var cursor_pos: struct { 381 + const CursorPos = struct { 397 382 row: u16 = 0, 398 383 col: u16 = 0, 399 - } = .{}; 384 + }; 385 + var cursor_pos: CursorPos = .{}; 400 386 401 - // Clear all images 402 - if (self.caps.kitty_graphics) 403 - try tty.writeAll(ctlseqs.kitty_graphics_clear); 387 + const startRender = struct { 388 + fn run( 389 + vx: *Vaxis, 390 + io: *IoWriter, 391 + cursor_pos_ptr: *CursorPos, 392 + reposition_ptr: *bool, 393 + started_ptr: *bool, 394 + sync_active_ptr: *bool, 395 + ) !void { 396 + if (started_ptr.*) return; 397 + started_ptr.* = true; 398 + sync_active_ptr.* = true; 399 + // Set up sync before we write anything 400 + try io.writeAll(ctlseqs.sync_set); 401 + // Send the cursor to 0,0 402 + try io.writeAll(ctlseqs.hide_cursor); 403 + if (vx.state.alt_screen) 404 + try io.writeAll(ctlseqs.home) 405 + else { 406 + try io.writeByte('\r'); 407 + for (0..vx.state.cursor.row) |_| { 408 + try io.writeAll(ctlseqs.ri); 409 + } 410 + } 411 + try io.writeAll(ctlseqs.sgr_reset); 412 + cursor_pos_ptr.* = .{}; 413 + reposition_ptr.* = true; 414 + // Clear all images 415 + if (vx.caps.kitty_graphics) 416 + try io.writeAll(ctlseqs.kitty_graphics_clear); 417 + } 418 + }; 404 419 405 420 // Reset skip flag on all last_screen cells 406 421 for (self.screen_last.buf) |*last_cell| { 407 422 last_cell.skip = false; 408 423 } 409 424 425 + if (needs_render) { 426 + try startRender.run(self, tty, &cursor_pos, &reposition, &started, &sync_active); 427 + } 428 + 410 429 var i: usize = 0; 411 430 while (i < self.screen.buf.len) { 412 431 const cell = self.screen.buf[i]; ··· 414 433 if (cell.char.width != 0) break :blk cell.char.width; 415 434 416 435 const method: gwidth.Method = self.caps.unicode; 417 - const width: u16 = @intCast(gwidth.gwidth(cell.char.grapheme, method, &self.unicode.width_data)); 436 + const width: u16 = @intCast(gwidth.gwidth(cell.char.grapheme, method)); 418 437 break :blk @max(1, width); 419 438 }; 420 439 defer { ··· 451 470 try tty.writeAll(ctlseqs.osc8_clear); 452 471 } 453 472 continue; 473 + } 474 + if (!started) { 475 + try startRender.run(self, tty, &cursor_pos, &reposition, &started, &sync_active); 454 476 } 455 477 self.screen_last.buf[i].skipped = false; 456 478 defer { ··· 735 757 cursor_pos.col = col + w; 736 758 cursor_pos.row = row; 737 759 } 760 + if (!started) return; 738 761 if (self.screen.cursor_vis) { 739 762 if (self.state.alt_screen) { 740 763 try tty.print( ··· 766 789 self.state.cursor.row = cursor_pos.row; 767 790 self.state.cursor.col = cursor_pos.col; 768 791 } 792 + self.screen_last.cursor_vis = self.screen.cursor_vis; 769 793 if (self.screen.mouse_shape != self.screen_last.mouse_shape) { 770 794 try tty.print( 771 795 ctlseqs.osc22_mouse_shape, ··· 856 880 const ypos = mouse.row; 857 881 const xextra = self.screen.width_pix % self.screen.width; 858 882 const yextra = self.screen.height_pix % self.screen.height; 859 - const xcell = (self.screen.width_pix - xextra) / self.screen.width; 860 - const ycell = (self.screen.height_pix - yextra) / self.screen.height; 883 + const xcell: i16 = @intCast((self.screen.width_pix - xextra) / self.screen.width); 884 + const ycell: i16 = @intCast((self.screen.height_pix - yextra) / self.screen.height); 861 885 if (xcell == 0 or ycell == 0) return mouse; 862 - result.col = xpos / xcell; 863 - result.row = ypos / ycell; 864 - result.xoffset = xpos % xcell; 865 - result.yoffset = ypos % ycell; 886 + result.col = @divFloor(xpos, xcell); 887 + result.row = @divFloor(ypos, ycell); 888 + result.xoffset = @intCast(@mod(xpos, xcell)); 889 + result.yoffset = @intCast(@mod(ypos, ycell)); 866 890 } 867 891 return result; 868 892 } ··· 1000 1024 const buf = switch (format) { 1001 1025 .png => png: { 1002 1026 const png_buf = try arena.allocator().alloc(u8, img.imageByteSize()); 1003 - const png = try img.writeToMemory(png_buf, .{ .png = .{} }); 1027 + const png = try img.writeToMemory(arena.allocator(), png_buf, .{ .png = .{} }); 1004 1028 break :png png; 1005 1029 }, 1006 1030 .rgb => rgb: { 1007 - try img.convert(.rgb24); 1031 + try img.convert(arena.allocator(), .rgb24); 1008 1032 break :rgb img.rawBytes(); 1009 1033 }, 1010 1034 .rgba => rgba: { 1011 - try img.convert(.rgba32); 1035 + try img.convert(arena.allocator(), .rgba32); 1012 1036 break :rgba img.rawBytes(); 1013 1037 }, 1014 1038 }; ··· 1032 1056 .path => |path| try zigimg.Image.fromFilePath(alloc, path, &read_buffer), 1033 1057 .mem => |bytes| try zigimg.Image.fromMemory(alloc, bytes), 1034 1058 }; 1035 - defer img.deinit(); 1059 + defer img.deinit(alloc); 1036 1060 return self.transmitImage(alloc, tty, &img, .png); 1037 1061 } 1038 1062 ··· 1149 1173 if (cell.char.width != 0) break :blk cell.char.width; 1150 1174 1151 1175 const method: gwidth.Method = self.caps.unicode; 1152 - const width = gwidth.gwidth(cell.char.grapheme, method, &self.unicode.width_data); 1176 + const width = gwidth.gwidth(cell.char.grapheme, method); 1153 1177 break :blk @max(1, width); 1154 1178 }; 1155 1179 defer { ··· 1414 1438 try tty.print(ctlseqs.osc7, .{uri.fmt(.{ .scheme = true, .authority = true, .path = true })}); 1415 1439 try tty.flush(); 1416 1440 } 1441 + 1442 + test "render: no output when no changes" { 1443 + var vx = try Vaxis.init(std.testing.allocator, .{}); 1444 + var deinit_writer = std.io.Writer.Allocating.init(std.testing.allocator); 1445 + defer deinit_writer.deinit(); 1446 + defer vx.deinit(std.testing.allocator, &deinit_writer.writer); 1447 + 1448 + var render_writer = std.io.Writer.Allocating.init(std.testing.allocator); 1449 + defer render_writer.deinit(); 1450 + try vx.render(&render_writer.writer); 1451 + const output = try render_writer.toOwnedSlice(); 1452 + defer std.testing.allocator.free(output); 1453 + try std.testing.expectEqual(@as(usize, 0), output.len); 1454 + }
+5 -20
src/Window.zig
··· 4 4 const Cell = @import("Cell.zig"); 5 5 const Mouse = @import("Mouse.zig"); 6 6 const Segment = @import("Cell.zig").Segment; 7 - const Unicode = @import("Unicode.zig"); 7 + const unicode = @import("unicode.zig"); 8 8 const gw = @import("gwidth.zig"); 9 9 10 10 const Window = @This(); ··· 25 25 height: u16, 26 26 27 27 screen: *Screen, 28 - unicode: *const Unicode, 29 28 30 29 /// Creates a new window with offset relative to parent and size clamped to the 31 30 /// parent's size. Windows do not retain a reference to their parent and are ··· 50 49 .width = @min(width, max_width), 51 50 .height = @min(height, max_height), 52 51 .screen = self.screen, 53 - .unicode = self.unicode, 54 52 }; 55 53 } 56 54 ··· 207 205 208 206 /// returns the width of the grapheme. This depends on the terminal capabilities 209 207 pub fn gwidth(self: Window, str: []const u8) u16 { 210 - return gw.gwidth(str, self.screen.width_method, &self.unicode.width_data); 208 + return gw.gwidth(str, self.screen.width_method); 211 209 } 212 210 213 211 /// fills the window with the provided cell ··· 295 293 .grapheme => { 296 294 var col: u16 = opts.col_offset; 297 295 const overflow: bool = blk: for (segments) |segment| { 298 - var iter = self.unicode.graphemeIterator(segment.text); 296 + var iter = unicode.graphemeIterator(segment.text); 299 297 while (iter.next()) |grapheme| { 300 298 if (col >= self.width) { 301 299 row += 1; ··· 378 376 col = 0; 379 377 } 380 378 381 - var grapheme_iterator = self.unicode.graphemeIterator(word); 379 + var grapheme_iterator = unicode.graphemeIterator(word); 382 380 while (grapheme_iterator.next()) |grapheme| { 383 381 soft_wrapped = false; 384 382 if (row >= self.height) { ··· 417 415 .none => { 418 416 var col: u16 = opts.col_offset; 419 417 const overflow: bool = blk: for (segments) |segment| { 420 - var iter = self.unicode.graphemeIterator(segment.text); 418 + var iter = unicode.graphemeIterator(segment.text); 421 419 while (iter.next()) |grapheme| { 422 420 if (col >= self.width) break :blk true; 423 421 const s = grapheme.bytes(segment.text); ··· 489 487 .width = 20, 490 488 .height = 20, 491 489 .screen = undefined, 492 - .unicode = undefined, 493 490 }; 494 491 495 492 const ch = parent.initChild(1, 1, null, null); ··· 506 503 .width = 20, 507 504 .height = 20, 508 505 .screen = undefined, 509 - .unicode = undefined, 510 506 }; 511 507 512 508 const ch = parent.initChild(0, 0, 21, 21); ··· 523 519 .width = 20, 524 520 .height = 20, 525 521 .screen = undefined, 526 - .unicode = undefined, 527 522 }; 528 523 529 524 const ch = parent.initChild(10, 10, 21, 21); ··· 540 535 .width = 20, 541 536 .height = 20, 542 537 .screen = undefined, 543 - .unicode = undefined, 544 538 }; 545 539 546 540 const ch = parent.initChild(10, 10, 21, 21); ··· 557 551 .width = 20, 558 552 .height = 20, 559 553 .screen = undefined, 560 - .unicode = undefined, 561 554 }; 562 555 563 556 const ch = parent.initChild(10, 10, 21, 21); ··· 569 562 } 570 563 571 564 test "print: grapheme" { 572 - const alloc = std.testing.allocator_instance.allocator(); 573 - const unicode = try Unicode.init(alloc); 574 - defer unicode.deinit(alloc); 575 565 var screen: Screen = .{ .width_method = .unicode }; 576 566 const win: Window = .{ 577 567 .x_off = 0, ··· 581 571 .width = 4, 582 572 .height = 2, 583 573 .screen = &screen, 584 - .unicode = &unicode, 585 574 }; 586 575 const opts: PrintOptions = .{ 587 576 .commit = false, ··· 636 625 } 637 626 638 627 test "print: word" { 639 - const alloc = std.testing.allocator_instance.allocator(); 640 - const unicode = try Unicode.init(alloc); 641 - defer unicode.deinit(alloc); 642 628 var screen: Screen = .{ 643 629 .width_method = .unicode, 644 630 }; ··· 650 636 .width = 4, 651 637 .height = 2, 652 638 .screen = &screen, 653 - .unicode = &unicode, 654 639 }; 655 640 const opts: PrintOptions = .{ 656 641 .commit = false,
+172 -35
src/gwidth.zig
··· 1 1 const std = @import("std"); 2 2 const unicode = std.unicode; 3 3 const testing = std.testing; 4 - const DisplayWidth = @import("DisplayWidth"); 5 - const code_point = @import("code_point"); 4 + const uucode = @import("uucode"); 6 5 7 6 /// the method to use when calculating the width of a grapheme 8 7 pub const Method = enum { ··· 11 10 no_zwj, 12 11 }; 13 12 13 + /// Calculate width from east asian width property and Unicode properties 14 + fn eawToWidth(cp: u21, eaw: uucode.types.EastAsianWidth) i16 { 15 + // Based on wcwidth implementation 16 + // Control characters 17 + if (cp == 0) return 0; 18 + if (cp < 32 or (cp >= 0x7f and cp < 0xa0)) return -1; 19 + 20 + // Use general category for comprehensive zero-width detection 21 + const gc = uucode.get(.general_category, cp); 22 + switch (gc) { 23 + .mark_nonspacing, .mark_enclosing => return 0, 24 + else => {}, 25 + } 26 + 27 + // Additional zero-width characters not covered by general category 28 + if (cp == 0x00ad) return 0; // soft hyphen 29 + if (cp == 0x200b) return 0; // zero-width space 30 + if (cp == 0x200c) return 0; // zero-width non-joiner 31 + if (cp == 0x200d) return 0; // zero-width joiner 32 + if (cp == 0x2060) return 0; // word joiner 33 + if (cp == 0x034f) return 0; // combining grapheme joiner 34 + if (cp == 0xfeff) return 0; // zero-width no-break space (BOM) 35 + if (cp >= 0x180b and cp <= 0x180d) return 0; // Mongolian variation selectors 36 + if (cp >= 0xfe00 and cp <= 0xfe0f) return 0; // variation selectors 37 + if (cp >= 0xe0100 and cp <= 0xe01ef) return 0; // Plane-14 variation selectors 38 + 39 + // East Asian Width: fullwidth or wide = 2 40 + // ambiguous in East Asian context = 2, otherwise 1 41 + // halfwidth, narrow, or neutral = 1 42 + return switch (eaw) { 43 + .fullwidth, .wide => 2, 44 + else => 1, 45 + }; 46 + } 47 + 14 48 /// returns the width of the provided string, as measured by the method chosen 15 - pub fn gwidth(str: []const u8, method: Method, data: *const DisplayWidth) u16 { 49 + pub fn gwidth(str: []const u8, method: Method) u16 { 16 50 switch (method) { 17 51 .unicode => { 18 - return @intCast(data.strWidth(str)); 52 + var total: u16 = 0; 53 + var grapheme_iter = uucode.grapheme.Iterator(uucode.utf8.Iterator).init(.init(str)); 54 + 55 + var grapheme_start: usize = 0; 56 + var prev_break: bool = true; 57 + 58 + while (grapheme_iter.next()) |result| { 59 + if (prev_break and !result.is_break) { 60 + // Start of a new grapheme 61 + const cp_len: usize = std.unicode.utf8CodepointSequenceLength(result.cp) catch 1; 62 + grapheme_start = grapheme_iter.i - cp_len; 63 + } 64 + 65 + if (result.is_break) { 66 + // End of a grapheme - calculate its width 67 + const grapheme_end = grapheme_iter.i; 68 + const grapheme_bytes = str[grapheme_start..grapheme_end]; 69 + 70 + // Calculate grapheme width 71 + var g_iter = uucode.utf8.Iterator.init(grapheme_bytes); 72 + var width: i16 = 0; 73 + var has_emoji_vs: bool = false; 74 + var has_text_vs: bool = false; 75 + var has_emoji_presentation: bool = false; 76 + var ri_count: u8 = 0; 77 + 78 + while (g_iter.next()) |cp| { 79 + // Check for emoji variation selector (U+FE0F) 80 + if (cp == 0xfe0f) { 81 + has_emoji_vs = true; 82 + continue; 83 + } 84 + 85 + // Check for text variation selector (U+FE0E) 86 + if (cp == 0xfe0e) { 87 + has_text_vs = true; 88 + continue; 89 + } 90 + 91 + // Check if this codepoint has emoji presentation 92 + if (uucode.get(.is_emoji_presentation, cp)) { 93 + has_emoji_presentation = true; 94 + } 95 + 96 + // Count regional indicators (for flag emojis) 97 + if (cp >= 0x1F1E6 and cp <= 0x1F1FF) { 98 + ri_count += 1; 99 + } 100 + 101 + const eaw = uucode.get(.east_asian_width, cp); 102 + const w = eawToWidth(cp, eaw); 103 + // Take max of non-zero widths 104 + if (w > 0 and w > width) width = w; 105 + } 106 + 107 + // Handle variation selectors and emoji presentation 108 + if (has_text_vs) { 109 + // Text presentation explicit - keep width as-is (usually 1) 110 + width = @max(1, width); 111 + } else if (has_emoji_vs or has_emoji_presentation or ri_count == 2) { 112 + // Emoji presentation or flag pair - force width 2 113 + width = @max(2, width); 114 + } 115 + 116 + total += @max(0, width); 117 + 118 + grapheme_start = grapheme_end; 119 + } 120 + prev_break = result.is_break; 121 + } 122 + 123 + return total; 19 124 }, 20 125 .wcwidth => { 21 126 var total: u16 = 0; 22 - var iter: code_point.Iterator = .{ .bytes = str }; 127 + var iter = uucode.utf8.Iterator.init(str); 23 128 while (iter.next()) |cp| { 24 - const w: u16 = switch (cp.code) { 129 + const w: i16 = switch (cp) { 25 130 // undo an override in zg for emoji skintone selectors 26 - 0x1f3fb...0x1f3ff, 27 - => 2, 28 - else => @max(0, data.codePointWidth(cp.code)), 131 + 0x1f3fb...0x1f3ff => 2, 132 + else => blk: { 133 + const eaw = uucode.get(.east_asian_width, cp); 134 + break :blk eawToWidth(cp, eaw); 135 + }, 29 136 }; 30 - total += w; 137 + total += @intCast(@max(0, w)); 31 138 } 32 139 return total; 33 140 }, ··· 35 142 var iter = std.mem.splitSequence(u8, str, "\u{200D}"); 36 143 var result: u16 = 0; 37 144 while (iter.next()) |s| { 38 - result += gwidth(s, .unicode, data); 145 + result += gwidth(s, .unicode); 39 146 } 40 147 return result; 41 148 }, ··· 43 150 } 44 151 45 152 test "gwidth: a" { 46 - const alloc = testing.allocator_instance.allocator(); 47 - const data = try DisplayWidth.init(alloc); 48 - defer data.deinit(alloc); 49 - try testing.expectEqual(1, gwidth("a", .unicode, &data)); 50 - try testing.expectEqual(1, gwidth("a", .wcwidth, &data)); 51 - try testing.expectEqual(1, gwidth("a", .no_zwj, &data)); 153 + try testing.expectEqual(1, gwidth("a", .unicode)); 154 + try testing.expectEqual(1, gwidth("a", .wcwidth)); 155 + try testing.expectEqual(1, gwidth("a", .no_zwj)); 52 156 } 53 157 54 158 test "gwidth: emoji with ZWJ" { 55 - const alloc = testing.allocator_instance.allocator(); 56 - const data = try DisplayWidth.init(alloc); 57 - defer data.deinit(alloc); 58 - try testing.expectEqual(2, gwidth("๐Ÿ‘ฉโ€๐Ÿš€", .unicode, &data)); 59 - try testing.expectEqual(4, gwidth("๐Ÿ‘ฉโ€๐Ÿš€", .wcwidth, &data)); 60 - try testing.expectEqual(4, gwidth("๐Ÿ‘ฉโ€๐Ÿš€", .no_zwj, &data)); 159 + try testing.expectEqual(2, gwidth("๐Ÿ‘ฉโ€๐Ÿš€", .unicode)); 160 + try testing.expectEqual(4, gwidth("๐Ÿ‘ฉโ€๐Ÿš€", .wcwidth)); 161 + try testing.expectEqual(4, gwidth("๐Ÿ‘ฉโ€๐Ÿš€", .no_zwj)); 61 162 } 62 163 63 164 test "gwidth: emoji with VS16 selector" { 64 - const alloc = testing.allocator_instance.allocator(); 65 - const data = try DisplayWidth.init(alloc); 66 - defer data.deinit(alloc); 67 - try testing.expectEqual(2, gwidth("\xE2\x9D\xA4\xEF\xB8\x8F", .unicode, &data)); 68 - try testing.expectEqual(1, gwidth("\xE2\x9D\xA4\xEF\xB8\x8F", .wcwidth, &data)); 69 - try testing.expectEqual(2, gwidth("\xE2\x9D\xA4\xEF\xB8\x8F", .no_zwj, &data)); 165 + try testing.expectEqual(2, gwidth("\xE2\x9D\xA4\xEF\xB8\x8F", .unicode)); 166 + try testing.expectEqual(1, gwidth("\xE2\x9D\xA4\xEF\xB8\x8F", .wcwidth)); 167 + try testing.expectEqual(2, gwidth("\xE2\x9D\xA4\xEF\xB8\x8F", .no_zwj)); 70 168 } 71 169 72 170 test "gwidth: emoji with skin tone selector" { 73 - const alloc = testing.allocator_instance.allocator(); 74 - const data = try DisplayWidth.init(alloc); 75 - defer data.deinit(alloc); 76 - try testing.expectEqual(2, gwidth("๐Ÿ‘‹๐Ÿฟ", .unicode, &data)); 77 - try testing.expectEqual(4, gwidth("๐Ÿ‘‹๐Ÿฟ", .wcwidth, &data)); 78 - try testing.expectEqual(2, gwidth("๐Ÿ‘‹๐Ÿฟ", .no_zwj, &data)); 171 + try testing.expectEqual(2, gwidth("๐Ÿ‘‹๐Ÿฟ", .unicode)); 172 + try testing.expectEqual(4, gwidth("๐Ÿ‘‹๐Ÿฟ", .wcwidth)); 173 + try testing.expectEqual(2, gwidth("๐Ÿ‘‹๐Ÿฟ", .no_zwj)); 174 + } 175 + 176 + test "gwidth: zero-width space" { 177 + try testing.expectEqual(0, gwidth("\u{200B}", .unicode)); 178 + try testing.expectEqual(0, gwidth("\u{200B}", .wcwidth)); 179 + } 180 + 181 + test "gwidth: zero-width non-joiner" { 182 + try testing.expectEqual(0, gwidth("\u{200C}", .unicode)); 183 + try testing.expectEqual(0, gwidth("\u{200C}", .wcwidth)); 184 + } 185 + 186 + test "gwidth: combining marks" { 187 + // Hebrew combining mark 188 + try testing.expectEqual(0, gwidth("\u{05B0}", .unicode)); 189 + // Devanagari combining mark 190 + try testing.expectEqual(0, gwidth("\u{093C}", .unicode)); 191 + } 192 + 193 + test "gwidth: flag emoji (regional indicators)" { 194 + // US flag ๐Ÿ‡บ๐Ÿ‡ธ 195 + try testing.expectEqual(2, gwidth("๐Ÿ‡บ๐Ÿ‡ธ", .unicode)); 196 + // UK flag ๐Ÿ‡ฌ๐Ÿ‡ง 197 + try testing.expectEqual(2, gwidth("๐Ÿ‡ฌ๐Ÿ‡ง", .unicode)); 198 + } 199 + 200 + test "gwidth: text variation selector" { 201 + // U+2764 (heavy black heart) + U+FE0E (text variation selector) 202 + // Should be width 1 with text presentation 203 + try testing.expectEqual(1, gwidth("โค๏ธŽ", .unicode)); 204 + } 205 + 206 + test "gwidth: keycap sequence" { 207 + // Digit 1 + U+FE0F + U+20E3 (combining enclosing keycap) 208 + // Should be width 2 209 + try testing.expectEqual(2, gwidth("1๏ธโƒฃ", .unicode)); 210 + } 211 + 212 + test "gwidth: base letter with combining mark" { 213 + // 'a' + combining acute accent (NFD form) 214 + // Should be width 1 (combining mark is zero-width) 215 + try testing.expectEqual(1, gwidth("รก", .unicode)); 79 216 }
+2 -4
src/main.zig
··· 26 26 pub const widgets = @import("widgets.zig"); 27 27 pub const gwidth = @import("gwidth.zig"); 28 28 pub const ctlseqs = @import("ctlseqs.zig"); 29 - pub const DisplayWidth = @import("DisplayWidth"); 30 29 pub const GraphemeCache = @import("GraphemeCache.zig"); 31 - pub const Graphemes = @import("Graphemes"); 32 30 pub const Event = @import("event.zig").Event; 33 - pub const Unicode = @import("Unicode.zig"); 31 + pub const unicode = @import("unicode.zig"); 34 32 35 33 pub const vxfw = @import("vxfw/vxfw.zig"); 36 34 ··· 74 72 ctlseqs.rmcup; 75 73 76 74 gty.writer().writeAll(reset) catch {}; 77 - 75 + gty.writer().flush() catch {}; 78 76 gty.deinit(); 79 77 } 80 78 }
+48 -46
src/queue.zig
··· 30 30 self.not_empty.wait(&self.mutex); 31 31 } 32 32 std.debug.assert(!self.isEmptyLH()); 33 - if (self.isFullLH()) { 34 - // If we are full, wake up a push that might be 35 - // waiting here. 36 - self.not_full.signal(); 37 - } 38 - 39 - return self.popLH(); 33 + return self.popAndSignalLH(); 40 34 } 41 35 42 36 /// Push an item into the queue. Blocks until an item has been ··· 48 42 self.not_full.wait(&self.mutex); 49 43 } 50 44 std.debug.assert(!self.isFullLH()); 51 - const was_empty = self.isEmptyLH(); 52 - 53 - self.buf[self.mask(self.write_index)] = item; 54 - self.write_index = self.mask2(self.write_index + 1); 55 - 56 - // If we were empty, wake up a pop if it was waiting. 57 - if (was_empty) { 58 - self.not_empty.signal(); 59 - } 45 + self.pushAndSignalLH(item); 60 46 } 61 47 62 48 /// Push an item into the queue. Returns true when the item ··· 64 50 /// was full. 65 51 pub fn tryPush(self: *Self, item: T) bool { 66 52 self.mutex.lock(); 67 - if (self.isFullLH()) { 68 - self.mutex.unlock(); 69 - return false; 70 - } 71 - self.mutex.unlock(); 72 - self.push(item); 53 + defer self.mutex.unlock(); 54 + if (self.isFullLH()) return false; 55 + self.pushAndSignalLH(item); 73 56 return true; 74 57 } 75 58 ··· 77 60 /// available. 78 61 pub fn tryPop(self: *Self) ?T { 79 62 self.mutex.lock(); 80 - if (self.isEmptyLH()) { 81 - self.mutex.unlock(); 82 - return null; 83 - } 84 - self.mutex.unlock(); 85 - return self.pop(); 63 + defer self.mutex.unlock(); 64 + if (self.isEmptyLH()) return null; 65 + return self.popAndSignalLH(); 86 66 } 87 67 88 68 /// Poll the queue. This call blocks until events are in the queue ··· 150 130 return index % (2 * self.buf.len); 151 131 } 152 132 133 + fn pushAndSignalLH(self: *Self, item: T) void { 134 + const was_empty = self.isEmptyLH(); 135 + self.buf[self.mask(self.write_index)] = item; 136 + self.write_index = self.mask2(self.write_index + 1); 137 + if (was_empty) { 138 + self.not_empty.signal(); 139 + } 140 + } 141 + 142 + fn popAndSignalLH(self: *Self) T { 143 + const was_full = self.isFullLH(); 144 + const result = self.popLH(); 145 + if (was_full) { 146 + self.not_full.signal(); 147 + } 148 + return result; 149 + } 150 + 153 151 fn popLH(self: *Self) T { 154 152 const result = self.buf[self.mask(self.read_index)]; 155 153 self.read_index = self.mask2(self.read_index + 1); ··· 204 202 thread.join(); 205 203 } 206 204 207 - fn sleepyPop(q: *Queue(u8, 2)) !void { 205 + fn sleepyPop(q: *Queue(u8, 2), state: *atomic.Value(u8)) !void { 208 206 // First we wait for the queue to be full. 209 - while (!q.isFull()) 207 + while (state.load(.acquire) < 1) 210 208 try Thread.yield(); 211 209 212 210 // Then we spuriously wake it up, because that's a thing that can ··· 220 218 // still full and the push in the other thread is still blocked 221 219 // waiting for space. 222 220 try Thread.yield(); 223 - std.Thread.sleep(std.time.ns_per_s); 221 + std.Thread.sleep(10 * std.time.ns_per_ms); 224 222 // Finally, let that other thread go. 225 223 try std.testing.expectEqual(1, q.pop()); 226 224 227 - // This won't continue until the other thread has had a chance to 228 - // put at least one item in the queue. 229 - while (!q.isFull()) 225 + // Wait for the other thread to signal it's ready for second push 226 + while (state.load(.acquire) < 2) 230 227 try Thread.yield(); 231 228 // But we want to ensure that there's a second push waiting, so 232 229 // here's another sleep. 233 - std.Thread.sleep(std.time.ns_per_s / 2); 230 + std.Thread.sleep(10 * std.time.ns_per_ms); 234 231 235 232 // Another spurious wake... 236 233 q.not_full.signal(); ··· 238 235 // And another chance for the other thread to see that it's 239 236 // spurious and go back to sleep. 240 237 try Thread.yield(); 241 - std.Thread.sleep(std.time.ns_per_s / 2); 238 + std.Thread.sleep(10 * std.time.ns_per_ms); 242 239 243 240 // Pop that thing and we're done. 244 241 try std.testing.expectEqual(2, q.pop()); ··· 252 249 // fails if the while loop in `push` is turned into an `if`. 253 250 254 251 var queue: Queue(u8, 2) = .{}; 255 - const thread = try Thread.spawn(cfg, sleepyPop, .{&queue}); 252 + var state = atomic.Value(u8).init(0); 253 + const thread = try Thread.spawn(cfg, sleepyPop, .{ &queue, &state }); 256 254 queue.push(1); 257 255 queue.push(2); 256 + state.store(1, .release); 258 257 const now = std.time.milliTimestamp(); 259 258 queue.push(3); // This one should block. 260 259 const then = std.time.milliTimestamp(); 261 260 262 261 // Just to make sure the sleeps are yielding to this thread, make 263 - // sure it took at least 900ms to do the push. 264 - try std.testing.expect(then - now > 900); 262 + // sure it took at least 5ms to do the push. 263 + try std.testing.expect(then - now > 5); 265 264 265 + state.store(2, .release); 266 266 // This should block again, waiting for the other thread. 267 267 queue.push(4); 268 268 ··· 272 272 try std.testing.expectEqual(4, queue.pop()); 273 273 } 274 274 275 - fn sleepyPush(q: *Queue(u8, 1)) !void { 275 + fn sleepyPush(q: *Queue(u8, 1), state: *atomic.Value(u8)) !void { 276 276 // Try to ensure the other thread has already started trying to pop. 277 277 try Thread.yield(); 278 - std.Thread.sleep(std.time.ns_per_s / 2); 278 + std.Thread.sleep(10 * std.time.ns_per_ms); 279 279 280 280 // Spurious wake 281 281 q.not_full.signal(); 282 282 q.not_empty.signal(); 283 283 284 284 try Thread.yield(); 285 - std.Thread.sleep(std.time.ns_per_s / 2); 285 + std.Thread.sleep(10 * std.time.ns_per_ms); 286 286 287 287 // Stick something in the queue so it can be popped. 288 288 q.push(1); 289 289 // Ensure it's been popped. 290 - while (!q.isEmpty()) 290 + while (state.load(.acquire) < 1) 291 291 try Thread.yield(); 292 292 // Give the other thread time to block again. 293 293 try Thread.yield(); 294 - std.Thread.sleep(std.time.ns_per_s / 2); 294 + std.Thread.sleep(10 * std.time.ns_per_ms); 295 295 296 296 // Spurious wake 297 297 q.not_full.signal(); ··· 306 306 // `if`. 307 307 308 308 var queue: Queue(u8, 1) = .{}; 309 - const thread = try Thread.spawn(cfg, sleepyPush, .{&queue}); 309 + var state = atomic.Value(u8).init(0); 310 + const thread = try Thread.spawn(cfg, sleepyPush, .{ &queue, &state }); 310 311 try std.testing.expectEqual(1, queue.pop()); 312 + state.store(1, .release); 311 313 try std.testing.expectEqual(2, queue.pop()); 312 314 thread.join(); 313 315 } ··· 322 324 const t1 = try Thread.spawn(cfg, readerThread, .{&queue}); 323 325 const t2 = try Thread.spawn(cfg, readerThread, .{&queue}); 324 326 try Thread.yield(); 325 - std.Thread.sleep(std.time.ns_per_s / 2); 327 + std.Thread.sleep(10 * std.time.ns_per_ms); 326 328 queue.push(1); 327 329 queue.push(1); 328 330 t1.join();
+3 -2
src/tty.zig
··· 453 453 0xc0 => '`', 454 454 0xdb => '[', 455 455 0xdc => '\\', 456 + 0xdf => '\\', 456 457 0xe2 => '\\', 457 458 0xdd => ']', 458 459 0xde => '\'', ··· 575 576 }; 576 577 577 578 const mouse: Mouse = .{ 578 - .col = @as(u16, @bitCast(event.dwMousePosition.X)), // Windows reports with 0 index 579 - .row = @as(u16, @bitCast(event.dwMousePosition.Y)), // Windows reports with 0 index 579 + .col = @as(i16, @bitCast(event.dwMousePosition.X)), // Windows reports with 0 index 580 + .row = @as(i16, @bitCast(event.dwMousePosition.Y)), // Windows reports with 0 index 580 581 .mods = mods, 581 582 .type = event_type, 582 583 .button = btn,
+64
src/unicode.zig
··· 1 + const std = @import("std"); 2 + const uucode = @import("uucode"); 3 + 4 + // Old API-compatible Grapheme value 5 + pub const Grapheme = struct { 6 + start: usize, 7 + len: usize, 8 + 9 + pub fn bytes(self: Grapheme, str: []const u8) []const u8 { 10 + return str[self.start .. self.start + self.len]; 11 + } 12 + }; 13 + 14 + // Old API-compatible iterator that yields Grapheme with .len and .bytes() 15 + pub const GraphemeIterator = struct { 16 + str: []const u8, 17 + inner: uucode.grapheme.Iterator(uucode.utf8.Iterator), 18 + start: usize = 0, 19 + prev_break: bool = true, 20 + 21 + pub fn init(str: []const u8) GraphemeIterator { 22 + return .{ 23 + .str = str, 24 + .inner = uucode.grapheme.Iterator(uucode.utf8.Iterator).init(.init(str)), 25 + }; 26 + } 27 + 28 + pub fn next(self: *GraphemeIterator) ?Grapheme { 29 + while (self.inner.next()) |res| { 30 + // When leaving a break and entering a non-break, set the start of a cluster 31 + if (self.prev_break and !res.is_break) { 32 + const cp_len: usize = std.unicode.utf8CodepointSequenceLength(res.cp) catch 1; 33 + self.start = self.inner.i - cp_len; 34 + } 35 + 36 + // A break marks the end of the current grapheme 37 + if (res.is_break) { 38 + const end = self.inner.i; 39 + const s = self.start; 40 + self.start = end; 41 + self.prev_break = true; 42 + return .{ .start = s, .len = end - s }; 43 + } 44 + 45 + self.prev_break = false; 46 + } 47 + 48 + // Flush the last grapheme if we ended mid-cluster 49 + if (!self.prev_break and self.start < self.str.len) { 50 + const s = self.start; 51 + const len = self.str.len - s; 52 + self.start = self.str.len; 53 + self.prev_break = true; 54 + return .{ .start = s, .len = len }; 55 + } 56 + 57 + return null; 58 + } 59 + }; 60 + 61 + /// creates a grapheme iterator based on str 62 + pub fn graphemeIterator(str: []const u8) GraphemeIterator { 63 + return GraphemeIterator.init(str); 64 + }
+4 -6
src/vxfw/App.zig
··· 80 80 vx.caps.sgr_pixels = false; 81 81 try vx.setMouseMode(tty.writer(), true); 82 82 83 - // Give DrawContext the unicode data 84 - vxfw.DrawContext.init(&vx.unicode, vx.screen.width_method); 83 + vxfw.DrawContext.init(vx.screen.width_method); 85 84 86 85 const framerate: u64 = if (opts.framerate > 0) opts.framerate else 60; 87 86 // Calculate tick rate ··· 262 261 .set_mouse_shape => |shape| self.vx.setMouseShape(shape), 263 262 .request_focus => |widget| self.wants_focus = widget, 264 263 .copy_to_clipboard => |content| { 264 + defer self.allocator.free(content); 265 265 self.vx.copyToSystemClipboard(self.tty.writer(), content, self.allocator) catch |err| { 266 266 switch (err) { 267 267 error.OutOfMemory => return Allocator.Error.OutOfMemory, ··· 270 270 }; 271 271 }, 272 272 .set_title => |title| { 273 + defer self.allocator.free(title); 273 274 self.vx.setTitle(self.tty.writer(), title) catch |err| { 274 275 std.log.err("set_title error: {}", .{err}); 275 276 }; ··· 531 532 // Find the path to the focused widget. This builds a list that has the first element as the 532 533 // focused widget, and walks backward to the root. It's possible our focused widget is *not* 533 534 // in this tree. If this is the case, we refocus to the root widget 534 - const has_focus = try self.childHasFocus(allocator, surface); 535 + _ = try self.childHasFocus(allocator, surface); 535 536 536 - // We assert that the focused widget *must* be in the widget tree. There is certianly a 537 - // logic bug in the code somewhere if this is not the case 538 - assert(has_focus); // Focused widget not found in Surface tree 539 537 if (!self.root.eql(surface.widget)) { 540 538 // If the root of surface is not the initial widget, we append the initial widget 541 539 try self.path_to_focused.append(allocator, self.root);
+1 -2
src/vxfw/Border.zig
··· 119 119 120 120 var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 121 121 defer arena.deinit(); 122 - const ucd = try vaxis.Unicode.init(arena.allocator()); 123 - vxfw.DrawContext.init(&ucd, .unicode); 122 + vxfw.DrawContext.init(.unicode); 124 123 125 124 // Border will draw itself tightly around the child 126 125 const ctx: vxfw.DrawContext = .{
+1 -2
src/vxfw/Button.zig
··· 187 187 // Now we draw the button. Set up our context with some unicode data 188 188 var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 189 189 defer arena.deinit(); 190 - const ucd = try vaxis.Unicode.init(arena.allocator()); 191 - vxfw.DrawContext.init(&ucd, .unicode); 190 + vxfw.DrawContext.init(.unicode); 192 191 193 192 const draw_ctx: vxfw.DrawContext = .{ 194 193 .arena = arena.allocator(),
+1 -2
src/vxfw/Center.zig
··· 54 54 55 55 var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 56 56 defer arena.deinit(); 57 - const ucd = try vaxis.Unicode.init(arena.allocator()); 58 - vxfw.DrawContext.init(&ucd, .unicode); 57 + vxfw.DrawContext.init(.unicode); 59 58 60 59 { 61 60 // Center expands to the max size. It must therefore have non-null max width and max height.
+1 -2
src/vxfw/FlexColumn.zig
··· 115 115 // Boiler plate draw context 116 116 var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 117 117 defer arena.deinit(); 118 - const ucd = try vaxis.Unicode.init(arena.allocator()); 119 - vxfw.DrawContext.init(&ucd, .unicode); 118 + vxfw.DrawContext.init(.unicode); 120 119 121 120 const flex_widget = flex_column.widget(); 122 121 const ctx: vxfw.DrawContext = .{
+1 -2
src/vxfw/FlexRow.zig
··· 114 114 // Boiler plate draw context 115 115 var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 116 116 defer arena.deinit(); 117 - const ucd = try vaxis.Unicode.init(arena.allocator()); 118 - vxfw.DrawContext.init(&ucd, .unicode); 117 + vxfw.DrawContext.init(.unicode); 119 118 120 119 const flex_widget = flex_row.widget(); 121 120 const ctx: vxfw.DrawContext = .{
+2 -4
src/vxfw/ListView.zig
··· 536 536 // Boiler plate draw context 537 537 var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 538 538 defer arena.deinit(); 539 - const ucd = try vaxis.Unicode.init(arena.allocator()); 540 - vxfw.DrawContext.init(&ucd, .unicode); 539 + vxfw.DrawContext.init(.unicode); 541 540 542 541 const list_widget = list_view.widget(); 543 542 const draw_ctx: vxfw.DrawContext = .{ ··· 709 708 // Boiler plate draw context 710 709 var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 711 710 defer arena.deinit(); 712 - const ucd = try vaxis.Unicode.init(arena.allocator()); 713 - vxfw.DrawContext.init(&ucd, .unicode); 711 + vxfw.DrawContext.init(.unicode); 714 712 715 713 const list_widget = list_view.widget(); 716 714 const draw_ctx: vxfw.DrawContext = .{
+1 -2
src/vxfw/Padding.zig
··· 112 112 113 113 var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 114 114 defer arena.deinit(); 115 - const ucd = try vaxis.Unicode.init(arena.allocator()); 116 - vxfw.DrawContext.init(&ucd, .unicode); 115 + vxfw.DrawContext.init(.unicode); 117 116 118 117 // Center expands to the max size. It must therefore have non-null max width and max height. 119 118 // These values are asserted in draw
+4 -4
src/vxfw/RichText.zig
··· 363 363 364 364 var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 365 365 defer arena.deinit(); 366 - const ucd = try vaxis.Unicode.init(arena.allocator()); 367 - vxfw.DrawContext.init(&ucd, .unicode); 366 + 367 + vxfw.DrawContext.init(.unicode); 368 368 369 369 // Center expands to the max size. It must therefore have non-null max width and max height. 370 370 // These values are asserted in draw ··· 402 402 403 403 var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 404 404 defer arena.deinit(); 405 - const ucd = try vaxis.Unicode.init(arena.allocator()); 406 - vxfw.DrawContext.init(&ucd, .unicode); 405 + 406 + vxfw.DrawContext.init(.unicode); 407 407 408 408 const len = rich_text.text[0].text.len; 409 409 const width: u16 = 8;
+11 -11
src/vxfw/ScrollBars.zig
··· 268 268 switch (event) { 269 269 .mouse => |mouse| { 270 270 // 1. Process vertical scroll thumb hover. 271 - 271 + const mouse_col: u16 = if (mouse.col < 0) 0 else @intCast(mouse.col); 272 + const mouse_row: u16 = if (mouse.row < 0) 0 else @intCast(mouse.row); 272 273 const is_mouse_over_vertical_thumb = 273 - mouse.col == self.last_frame_size.width -| 1 and 274 - mouse.row >= self.vertical_thumb_top_row and 275 - mouse.row < self.vertical_thumb_bottom_row; 274 + mouse_col == self.last_frame_size.width -| 1 and 275 + mouse_row >= self.vertical_thumb_top_row and 276 + mouse_row < self.vertical_thumb_bottom_row; 276 277 277 278 // Make sure we only update the state and redraw when it's necessary. 278 279 if (!self.is_hovering_vertical_thumb and is_mouse_over_vertical_thumb) { ··· 288 289 289 290 if (did_start_dragging_vertical_thumb) { 290 291 self.is_dragging_vertical_thumb = true; 291 - self.mouse_offset_into_thumb = @intCast(mouse.row -| self.vertical_thumb_top_row); 292 + self.mouse_offset_into_thumb = @intCast(mouse_row -| self.vertical_thumb_top_row); 292 293 293 294 // No need to redraw yet, but we must consume the event. 294 295 return ctx.consumeEvent(); ··· 297 298 // 2. Process horizontal scroll thumb hover. 298 299 299 300 const is_mouse_over_horizontal_thumb = 300 - mouse.row == self.last_frame_size.height -| 1 and 301 - mouse.col >= self.horizontal_thumb_start_col and 302 - mouse.col < self.horizontal_thumb_end_col; 301 + mouse_row == self.last_frame_size.height -| 1 and 302 + mouse_col >= self.horizontal_thumb_start_col and 303 + mouse_col < self.horizontal_thumb_end_col; 303 304 304 305 // Make sure we only update the state and redraw when it's necessary. 305 306 if (!self.is_hovering_horizontal_thumb and is_mouse_over_horizontal_thumb) { ··· 316 317 if (did_start_dragging_horizontal_thumb) { 317 318 self.is_dragging_horizontal_thumb = true; 318 319 self.mouse_offset_into_thumb = @intCast( 319 - mouse.col -| self.horizontal_thumb_start_col, 320 + mouse_col -| self.horizontal_thumb_start_col, 320 321 ); 321 322 322 323 // No need to redraw yet, but we must consume the event. ··· 572 573 // Boiler plate draw context 573 574 var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 574 575 defer arena.deinit(); 575 - const ucd = try vaxis.Unicode.init(arena.allocator()); 576 - vxfw.DrawContext.init(&ucd, .unicode); 576 + vxfw.DrawContext.init(.unicode); 577 577 578 578 const scroll_widget = scroll_bars.widget(); 579 579 const draw_ctx: vxfw.DrawContext = .{
+2 -4
src/vxfw/ScrollView.zig
··· 609 609 // Boiler plate draw context 610 610 var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 611 611 defer arena.deinit(); 612 - const ucd = try vaxis.Unicode.init(arena.allocator()); 613 - vxfw.DrawContext.init(&ucd, .unicode); 612 + vxfw.DrawContext.init(.unicode); 614 613 615 614 const scroll_widget = scroll_view.widget(); 616 615 const draw_ctx: vxfw.DrawContext = .{ ··· 1022 1021 // Boiler plate draw context 1023 1022 var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 1024 1023 defer arena.deinit(); 1025 - const ucd = try vaxis.Unicode.init(arena.allocator()); 1026 - vxfw.DrawContext.init(&ucd, .unicode); 1024 + vxfw.DrawContext.init(.unicode); 1027 1025 1028 1026 const scroll_widget = scroll_view.widget(); 1029 1027 const draw_ctx: vxfw.DrawContext = .{
+1 -2
src/vxfw/SizedBox.zig
··· 59 59 // Boiler plate draw context 60 60 var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 61 61 defer arena.deinit(); 62 - const ucd = try vaxis.Unicode.init(arena.allocator()); 63 - vxfw.DrawContext.init(&ucd, .unicode); 62 + vxfw.DrawContext.init(.unicode); 64 63 65 64 var draw_ctx: vxfw.DrawContext = .{ 66 65 .arena = arena.allocator(),
+6 -5
src/vxfw/SplitView.zig
··· 88 88 }, 89 89 .rhs => { 90 90 const last_max = self.last_max_width orelse return; 91 - self.width = @min(last_max -| self.min_width, last_max -| mouse.col -| 1); 91 + const mouse_col: u16 = if (mouse.col < 0) 0 else @intCast(mouse.col); 92 + self.width = @min(last_max -| self.min_width, last_max -| mouse_col -| 1); 92 93 if (self.max_width) |max| { 93 94 self.width = @max(self.width, max); 94 95 } ··· 185 186 // Boiler plate draw context 186 187 var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 187 188 defer arena.deinit(); 188 - const ucd = try vaxis.Unicode.init(arena.allocator()); 189 - vxfw.DrawContext.init(&ucd, .unicode); 189 + vxfw.DrawContext.init(.unicode); 190 190 191 191 const draw_ctx: vxfw.DrawContext = .{ 192 192 .arena = arena.allocator(), ··· 219 219 // Send the widget a mouse press on the separator 220 220 var mouse: vaxis.Mouse = .{ 221 221 // The separator is at width 222 - .col = split_view.width, 222 + .col = @intCast(split_view.width), 223 223 .row = 0, 224 224 .type = .press, 225 225 .button = .left, ··· 242 242 try split_widget.handleEvent(&ctx, .{ .mouse = mouse }); 243 243 try std.testing.expect(ctx.redraw); 244 244 try std.testing.expect(split_view.pressed); 245 - try std.testing.expectEqual(mouse.col, split_view.width); 245 + const mouse_col: u16 = if (mouse.col < 0) 0 else @intCast(mouse.col); 246 + try std.testing.expectEqual(mouse_col, split_view.width); 246 247 } 247 248 248 249 test "refAllDecls" {
+5 -14
src/vxfw/Text.zig
··· 293 293 }; 294 294 295 295 test "SoftwrapIterator: LF breaks" { 296 - const unicode = try vaxis.Unicode.init(std.testing.allocator); 297 - defer unicode.deinit(std.testing.allocator); 298 - vxfw.DrawContext.init(&unicode, .unicode); 296 + vxfw.DrawContext.init(.unicode); 299 297 var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 300 298 defer arena.deinit(); 301 299 ··· 321 319 } 322 320 323 321 test "SoftwrapIterator: soft breaks that fit" { 324 - const unicode = try vaxis.Unicode.init(std.testing.allocator); 325 - defer unicode.deinit(std.testing.allocator); 326 - vxfw.DrawContext.init(&unicode, .unicode); 322 + vxfw.DrawContext.init(.unicode); 327 323 var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 328 324 defer arena.deinit(); 329 325 ··· 349 345 } 350 346 351 347 test "SoftwrapIterator: soft breaks that are longer than width" { 352 - const unicode = try vaxis.Unicode.init(std.testing.allocator); 353 - defer unicode.deinit(std.testing.allocator); 354 - vxfw.DrawContext.init(&unicode, .unicode); 348 + vxfw.DrawContext.init(.unicode); 355 349 var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 356 350 defer arena.deinit(); 357 351 ··· 387 381 } 388 382 389 383 test "SoftwrapIterator: soft breaks with leading spaces" { 390 - const unicode = try vaxis.Unicode.init(std.testing.allocator); 391 - defer unicode.deinit(std.testing.allocator); 392 - vxfw.DrawContext.init(&unicode, .unicode); 384 + vxfw.DrawContext.init(.unicode); 393 385 var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 394 386 defer arena.deinit(); 395 387 ··· 484 476 485 477 var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 486 478 defer arena.deinit(); 487 - const ucd = try vaxis.Unicode.init(arena.allocator()); 488 - vxfw.DrawContext.init(&ucd, .unicode); 479 + vxfw.DrawContext.init(.unicode); 489 480 490 481 // Center expands to the max size. It must therefore have non-null max width and max height. 491 482 // These values are asserted in draw
+14 -21
src/vxfw/TextField.zig
··· 9 9 const Key = vaxis.Key; 10 10 const Cell = vaxis.Cell; 11 11 const Window = vaxis.Window; 12 - const Unicode = vaxis.Unicode; 12 + const unicode = vaxis.unicode; 13 13 14 14 const TextField = @This(); 15 15 ··· 32 32 /// Previous width we drew at 33 33 prev_width: u16 = 0, 34 34 35 - unicode: *const Unicode, 36 - 37 35 previous_val: []const u8 = "", 38 36 39 37 userdata: ?*anyopaque = null, 40 38 onChange: ?*const fn (?*anyopaque, *vxfw.EventContext, []const u8) anyerror!void = null, 41 39 onSubmit: ?*const fn (?*anyopaque, *vxfw.EventContext, []const u8) anyerror!void = null, 42 40 43 - pub fn init(alloc: std.mem.Allocator, unicode: *const Unicode) TextField { 41 + pub fn init(alloc: std.mem.Allocator) TextField { 44 42 return TextField{ 45 43 .buf = Buffer.init(alloc), 46 - .unicode = unicode, 47 44 }; 48 45 } 49 46 ··· 137 134 138 135 /// insert text at the cursor position 139 136 pub fn insertSliceAtCursor(self: *TextField, data: []const u8) std.mem.Allocator.Error!void { 140 - var iter = self.unicode.graphemeIterator(data); 137 + var iter = unicode.graphemeIterator(data); 141 138 while (iter.next()) |text| { 142 139 try self.buf.insertSliceAtCursor(text.bytes(data)); 143 140 } ··· 153 150 pub fn widthToCursor(self: *TextField, ctx: vxfw.DrawContext) u16 { 154 151 var width: u16 = 0; 155 152 const first_half = self.buf.firstHalf(); 156 - var first_iter = self.unicode.graphemeIterator(first_half); 153 + var first_iter = unicode.graphemeIterator(first_half); 157 154 var i: usize = 0; 158 155 while (first_iter.next()) |grapheme| { 159 156 defer i += 1; ··· 168 165 169 166 pub fn cursorLeft(self: *TextField) void { 170 167 // We need to find the size of the last grapheme in the first half 171 - var iter = self.unicode.graphemeIterator(self.buf.firstHalf()); 168 + var iter = unicode.graphemeIterator(self.buf.firstHalf()); 172 169 var len: usize = 0; 173 170 while (iter.next()) |grapheme| { 174 171 len = grapheme.len; ··· 177 174 } 178 175 179 176 pub fn cursorRight(self: *TextField) void { 180 - var iter = self.unicode.graphemeIterator(self.buf.secondHalf()); 177 + var iter = unicode.graphemeIterator(self.buf.secondHalf()); 181 178 const grapheme = iter.next() orelse return; 182 179 self.buf.moveGapRight(grapheme.len); 183 180 } 184 181 185 182 pub fn graphemesBeforeCursor(self: *const TextField) u16 { 186 183 const first_half = self.buf.firstHalf(); 187 - var first_iter = self.unicode.graphemeIterator(first_half); 184 + var first_iter = unicode.graphemeIterator(first_half); 188 185 var i: u16 = 0; 189 186 while (first_iter.next()) |_| { 190 187 i += 1; ··· 230 227 self.prev_cursor_col = 0; 231 228 232 229 const first_half = self.buf.firstHalf(); 233 - var first_iter = self.unicode.graphemeIterator(first_half); 230 + var first_iter = unicode.graphemeIterator(first_half); 234 231 var col: u16 = 0; 235 232 var i: u16 = 0; 236 233 while (first_iter.next()) |grapheme| { ··· 259 256 if (i == cursor_idx) self.prev_cursor_col = col; 260 257 } 261 258 const second_half = self.buf.secondHalf(); 262 - var second_iter = self.unicode.graphemeIterator(second_half); 259 + var second_iter = unicode.graphemeIterator(second_half); 263 260 while (second_iter.next()) |grapheme| { 264 261 if (i < self.draw_offset) { 265 262 i += 1; ··· 332 329 333 330 pub fn deleteBeforeCursor(self: *TextField) void { 334 331 // We need to find the size of the last grapheme in the first half 335 - var iter = self.unicode.graphemeIterator(self.buf.firstHalf()); 332 + var iter = unicode.graphemeIterator(self.buf.firstHalf()); 336 333 var len: usize = 0; 337 334 while (iter.next()) |grapheme| { 338 335 len = grapheme.len; ··· 341 338 } 342 339 343 340 pub fn deleteAfterCursor(self: *TextField) void { 344 - var iter = self.unicode.graphemeIterator(self.buf.secondHalf()); 341 + var iter = unicode.graphemeIterator(self.buf.secondHalf()); 345 342 const grapheme = iter.next() orelse return; 346 343 self.buf.growGapRight(grapheme.len); 347 344 } ··· 384 381 } 385 382 386 383 test "sliceToCursor" { 387 - const alloc = std.testing.allocator_instance.allocator(); 388 - const unicode = try Unicode.init(alloc); 389 - defer unicode.deinit(alloc); 390 - var input = init(alloc, &unicode); 384 + var input = init(std.testing.allocator); 391 385 defer input.deinit(); 392 386 try input.insertSliceAtCursor("hello, world"); 393 387 input.cursorLeft(); ··· 541 535 // Boiler plate draw context init 542 536 var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 543 537 defer arena.deinit(); 544 - const ucd = try vaxis.Unicode.init(arena.allocator()); 545 - vxfw.DrawContext.init(&ucd, .unicode); 538 + vxfw.DrawContext.init(.unicode); 546 539 547 540 // Create some object which reacts to text field changes 548 541 const Foo = struct { ··· 572 565 }; 573 566 574 567 // Enough boiler plate...Create the text field 575 - var text_field = TextField.init(std.testing.allocator, &ucd); 568 + var text_field = TextField.init(std.testing.allocator); 576 569 defer text_field.deinit(); 577 570 text_field.onChange = Foo.onChange; 578 571 text_field.onSubmit = Foo.onChange;
+12 -11
src/vxfw/vxfw.zig
··· 1 1 const std = @import("std"); 2 2 const vaxis = @import("../main.zig"); 3 + const uucode = @import("uucode"); 3 4 4 - const Graphemes = vaxis.Graphemes; 5 5 const testing = std.testing; 6 6 7 7 const assert = std.debug.assert; ··· 141 141 try self.addCmd(.{ .request_focus = widget }); 142 142 } 143 143 144 + /// Copy content to clipboard. 145 + /// content is duplicated using self.alloc. 146 + /// Caller retains ownership of their copy of content. 144 147 pub fn copyToClipboard(self: *EventContext, content: []const u8) Allocator.Error!void { 145 - try self.addCmd(.{ .copy_to_clipboard = content }); 148 + try self.addCmd(.{ .copy_to_clipboard = try self.alloc.dupe(u8, content) }); 146 149 } 147 150 151 + /// Set window title. 152 + /// title is duplicated using self.alloc. 153 + /// Caller retains ownership of their copy of title. 148 154 pub fn setTitle(self: *EventContext, title: []const u8) Allocator.Error!void { 149 - try self.addCmd(.{ .set_title = title }); 155 + try self.addCmd(.{ .set_title = try self.alloc.dupe(u8, title) }); 150 156 } 151 157 152 158 pub fn queueRefresh(self: *EventContext) Allocator.Error!void { ··· 191 197 cell_size: Size, 192 198 193 199 // Unicode stuff 194 - var unicode: ?*const vaxis.Unicode = null; 195 200 var width_method: vaxis.gwidth.Method = .unicode; 196 201 197 - pub fn init(ucd: *const vaxis.Unicode, method: vaxis.gwidth.Method) void { 198 - DrawContext.unicode = ucd; 202 + pub fn init(method: vaxis.gwidth.Method) void { 199 203 DrawContext.width_method = method; 200 204 } 201 205 202 206 pub fn stringWidth(_: DrawContext, str: []const u8) usize { 203 - assert(DrawContext.unicode != null); // DrawContext not initialized 204 207 return vaxis.gwidth.gwidth( 205 208 str, 206 209 DrawContext.width_method, 207 - &DrawContext.unicode.?.width_data, 208 210 ); 209 211 } 210 212 211 - pub fn graphemeIterator(_: DrawContext, str: []const u8) Graphemes.Iterator { 212 - assert(DrawContext.unicode != null); // DrawContext not initialized 213 - return DrawContext.unicode.?.graphemeIterator(str); 213 + pub fn graphemeIterator(_: DrawContext, str: []const u8) vaxis.unicode.GraphemeIterator { 214 + return vaxis.unicode.graphemeIterator(str); 214 215 } 215 216 216 217 pub fn withConstraints(self: DrawContext, min: Size, max: MaxSize) DrawContext {
+1 -1
src/widgets/Table.zig
··· 134 134 const data_ti = @typeInfo(DataListT); 135 135 switch (data_ti) { 136 136 .pointer => |ptr| { 137 - if (ptr.size != .Slice) return error.UnsupportedTableDataType; 137 + if (ptr.size != .slice) return error.UnsupportedTableDataType; 138 138 break :getData data_list; 139 139 }, 140 140 .@"struct" => {
+13 -22
src/widgets/TextInput.zig
··· 3 3 const Key = @import("../Key.zig"); 4 4 const Cell = @import("../Cell.zig"); 5 5 const Window = @import("../Window.zig"); 6 - const Unicode = @import("../Unicode.zig"); 6 + const unicode = @import("../unicode.zig"); 7 7 8 8 const TextInput = @This(); 9 9 ··· 26 26 /// approximate distance from an edge before we scroll 27 27 scroll_offset: u16 = 4, 28 28 29 - unicode: *const Unicode, 30 - 31 - pub fn init(alloc: std.mem.Allocator, unicode: *const Unicode) TextInput { 29 + pub fn init(alloc: std.mem.Allocator) TextInput { 32 30 return TextInput{ 33 31 .buf = Buffer.init(alloc), 34 - .unicode = unicode, 35 32 }; 36 33 } 37 34 ··· 75 72 76 73 /// insert text at the cursor position 77 74 pub fn insertSliceAtCursor(self: *TextInput, data: []const u8) std.mem.Allocator.Error!void { 78 - var iter = self.unicode.graphemeIterator(data); 75 + var iter = unicode.graphemeIterator(data); 79 76 while (iter.next()) |text| { 80 77 try self.buf.insertSliceAtCursor(text.bytes(data)); 81 78 } ··· 91 88 pub fn widthToCursor(self: *TextInput, win: Window) u16 { 92 89 var width: u16 = 0; 93 90 const first_half = self.buf.firstHalf(); 94 - var first_iter = self.unicode.graphemeIterator(first_half); 91 + var first_iter = unicode.graphemeIterator(first_half); 95 92 var i: usize = 0; 96 93 while (first_iter.next()) |grapheme| { 97 94 defer i += 1; ··· 106 103 107 104 pub fn cursorLeft(self: *TextInput) void { 108 105 // We need to find the size of the last grapheme in the first half 109 - var iter = self.unicode.graphemeIterator(self.buf.firstHalf()); 106 + var iter = unicode.graphemeIterator(self.buf.firstHalf()); 110 107 var len: usize = 0; 111 108 while (iter.next()) |grapheme| { 112 109 len = grapheme.len; ··· 115 112 } 116 113 117 114 pub fn cursorRight(self: *TextInput) void { 118 - var iter = self.unicode.graphemeIterator(self.buf.secondHalf()); 115 + var iter = unicode.graphemeIterator(self.buf.secondHalf()); 119 116 const grapheme = iter.next() orelse return; 120 117 self.buf.moveGapRight(grapheme.len); 121 118 } 122 119 123 120 pub fn graphemesBeforeCursor(self: *const TextInput) u16 { 124 121 const first_half = self.buf.firstHalf(); 125 - var first_iter = self.unicode.graphemeIterator(first_half); 122 + var first_iter = unicode.graphemeIterator(first_half); 126 123 var i: u16 = 0; 127 124 while (first_iter.next()) |_| { 128 125 i += 1; ··· 152 149 // assumption!! the gap is never within a grapheme 153 150 // one way to _ensure_ this is to move the gap... but that's a cost we probably don't want to pay. 154 151 const first_half = self.buf.firstHalf(); 155 - var first_iter = self.unicode.graphemeIterator(first_half); 152 + var first_iter = unicode.graphemeIterator(first_half); 156 153 var col: u16 = 0; 157 154 var i: u16 = 0; 158 155 while (first_iter.next()) |grapheme| { ··· 181 178 if (i == cursor_idx) self.prev_cursor_col = col; 182 179 } 183 180 const second_half = self.buf.secondHalf(); 184 - var second_iter = self.unicode.graphemeIterator(second_half); 181 + var second_iter = unicode.graphemeIterator(second_half); 185 182 while (second_iter.next()) |grapheme| { 186 183 if (i < self.draw_offset) { 187 184 i += 1; ··· 252 249 253 250 pub fn deleteBeforeCursor(self: *TextInput) void { 254 251 // We need to find the size of the last grapheme in the first half 255 - var iter = self.unicode.graphemeIterator(self.buf.firstHalf()); 252 + var iter = unicode.graphemeIterator(self.buf.firstHalf()); 256 253 var len: usize = 0; 257 254 while (iter.next()) |grapheme| { 258 255 len = grapheme.len; ··· 261 258 } 262 259 263 260 pub fn deleteAfterCursor(self: *TextInput) void { 264 - var iter = self.unicode.graphemeIterator(self.buf.secondHalf()); 261 + var iter = unicode.graphemeIterator(self.buf.secondHalf()); 265 262 const grapheme = iter.next() orelse return; 266 263 self.buf.growGapRight(grapheme.len); 267 264 } ··· 304 301 } 305 302 306 303 test "assertion" { 307 - const alloc = std.testing.allocator_instance.allocator(); 308 - const unicode = try Unicode.init(alloc); 309 - defer unicode.deinit(); 310 304 const astronaut = "๐Ÿ‘ฉโ€๐Ÿš€"; 311 305 const astronaut_emoji: Key = .{ 312 306 .text = astronaut, 313 307 .codepoint = try std.unicode.utf8Decode(astronaut[0..4]), 314 308 }; 315 - var input = TextInput.init(std.testing.allocator, &unicode); 309 + var input = TextInput.init(std.testing.allocator); 316 310 defer input.deinit(); 317 311 for (0..6) |_| { 318 312 try input.update(.{ .key_press = astronaut_emoji }); ··· 320 314 } 321 315 322 316 test "sliceToCursor" { 323 - const alloc = std.testing.allocator_instance.allocator(); 324 - const unicode = try Unicode.init(alloc); 325 - defer unicode.deinit(); 326 - var input = init(alloc, &unicode); 317 + var input = init(std.testing.allocator); 327 318 defer input.deinit(); 328 319 try input.insertSliceAtCursor("hello, world"); 329 320 input.cursorLeft();
+57 -23
src/widgets/TextView.zig
··· 1 1 const std = @import("std"); 2 2 const vaxis = @import("../main.zig"); 3 - const Graphemes = @import("Graphemes"); 4 - const DisplayWidth = @import("DisplayWidth"); 3 + const uucode = @import("uucode"); 5 4 const ScrollView = vaxis.widgets.ScrollView; 6 5 6 + /// Simple grapheme representation to replace Graphemes.Grapheme 7 + const Grapheme = struct { 8 + len: u16, 9 + offset: u32, 10 + }; 11 + 7 12 pub const BufferWriter = struct { 8 13 pub const Error = error{OutOfMemory}; 9 14 pub const Writer = std.io.GenericWriter(@This(), Error, write); 10 15 11 16 allocator: std.mem.Allocator, 12 17 buffer: *Buffer, 13 - gd: *const Graphemes, 14 - wd: *const DisplayWidth, 15 18 16 19 pub fn write(self: @This(), bytes: []const u8) Error!usize { 17 20 try self.buffer.append(self.allocator, .{ 18 21 .bytes = bytes, 19 - .gd = self.gd, 20 - .wd = self.wd, 21 22 }); 22 23 return bytes.len; 23 24 } ··· 33 34 34 35 pub const Content = struct { 35 36 bytes: []const u8, 36 - gd: *const Graphemes, 37 - wd: *const DisplayWidth, 38 37 }; 39 38 40 39 pub const Style = struct { ··· 45 44 46 45 pub const Error = error{OutOfMemory}; 47 46 48 - grapheme: std.MultiArrayList(Graphemes.Grapheme) = .{}, 47 + grapheme: std.MultiArrayList(Grapheme) = .{}, 49 48 content: std.ArrayListUnmanaged(u8) = .{}, 50 49 style_list: StyleList = .{}, 51 50 style_map: StyleMap = .{}, ··· 78 77 /// Appends content to the buffer. 79 78 pub fn append(self: *@This(), allocator: std.mem.Allocator, content: Content) Error!void { 80 79 var cols: usize = self.last_cols; 81 - var iter = Graphemes.Iterator.init(content.bytes, content.gd); 82 - while (iter.next()) |g| { 80 + var iter = uucode.grapheme.Iterator(uucode.utf8.Iterator).init(.init(content.bytes)); 81 + 82 + var grapheme_start: usize = 0; 83 + var prev_break: bool = true; 84 + 85 + while (iter.next()) |result| { 86 + if (prev_break and !result.is_break) { 87 + // Start of a new grapheme 88 + const cp_len: usize = std.unicode.utf8CodepointSequenceLength(result.cp) catch 1; 89 + grapheme_start = iter.i - cp_len; 90 + } 91 + 92 + if (result.is_break) { 93 + // End of a grapheme 94 + const grapheme_end = iter.i; 95 + const grapheme_len = grapheme_end - grapheme_start; 96 + 97 + try self.grapheme.append(allocator, .{ 98 + .len = @intCast(grapheme_len), 99 + .offset = @intCast(self.content.items.len + grapheme_start), 100 + }); 101 + 102 + const cluster = content.bytes[grapheme_start..grapheme_end]; 103 + if (std.mem.eql(u8, cluster, "\n")) { 104 + self.cols = @max(self.cols, cols); 105 + cols = 0; 106 + } else { 107 + // Calculate width using gwidth 108 + const w = vaxis.gwidth.gwidth(cluster, .unicode); 109 + cols +|= w; 110 + } 111 + 112 + grapheme_start = grapheme_end; 113 + } 114 + prev_break = result.is_break; 115 + } 116 + 117 + // Flush the last grapheme if we ended mid-cluster 118 + if (!prev_break and grapheme_start < content.bytes.len) { 119 + const grapheme_len = content.bytes.len - grapheme_start; 120 + 83 121 try self.grapheme.append(allocator, .{ 84 - .len = g.len, 85 - .offset = @as(u32, @intCast(self.content.items.len)) + g.offset, 122 + .len = @intCast(grapheme_len), 123 + .offset = @intCast(self.content.items.len + grapheme_start), 86 124 }); 87 - const cluster = g.bytes(content.bytes); 88 - if (std.mem.eql(u8, cluster, "\n")) { 89 - self.cols = @max(self.cols, cols); 90 - cols = 0; 91 - continue; 125 + 126 + const cluster = content.bytes[grapheme_start..]; 127 + if (!std.mem.eql(u8, cluster, "\n")) { 128 + const w = vaxis.gwidth.gwidth(cluster, .unicode); 129 + cols +|= w; 92 130 } 93 - cols +|= content.wd.strWidth(cluster); 94 131 } 132 + 95 133 try self.content.appendSlice(allocator, content.bytes); 96 134 self.last_cols = cols; 97 135 self.cols = @max(self.cols, cols); ··· 123 161 pub fn writer( 124 162 self: *@This(), 125 163 allocator: std.mem.Allocator, 126 - gd: *const Graphemes, 127 - wd: *const DisplayWidth, 128 164 ) BufferWriter.Writer { 129 165 return .{ 130 166 .context = .{ 131 167 .allocator = allocator, 132 168 .buffer = self, 133 - .gd = gd, 134 - .wd = wd, 135 169 }, 136 170 }; 137 171 }
+3 -7
src/widgets/View.zig
··· 9 9 10 10 const Screen = @import("../Screen.zig"); 11 11 const Window = @import("../Window.zig"); 12 - const Unicode = @import("../Unicode.zig"); 12 + const unicode = @import("../unicode.zig"); 13 13 const Cell = @import("../Cell.zig"); 14 14 15 15 /// View Allocator ··· 17 17 18 18 /// Underlying Screen 19 19 screen: Screen, 20 - 21 - unicode: *const Unicode, 22 20 23 21 /// View Initialization Config 24 22 pub const Config = struct { ··· 27 25 }; 28 26 29 27 /// Initialize a new View 30 - pub fn init(alloc: mem.Allocator, unicode: *const Unicode, config: Config) mem.Allocator.Error!View { 28 + pub fn init(alloc: mem.Allocator, config: Config) mem.Allocator.Error!View { 31 29 return .{ 32 30 .alloc = alloc, 33 31 .screen = try Screen.init(alloc, .{ ··· 36 34 .x_pixel = 0, 37 35 .y_pixel = 0, 38 36 }), 39 - .unicode = unicode, 40 37 }; 41 38 } 42 39 ··· 49 46 .width = self.screen.width, 50 47 .height = self.screen.height, 51 48 .screen = &self.screen, 52 - .unicode = self.unicode, 53 49 }; 54 50 } 55 51 ··· 141 137 142 138 /// Returns the width of the grapheme. This depends on the terminal capabilities 143 139 pub fn gwidth(self: View, str: []const u8) u16 { 144 - return gw.gwidth(str, self.screen.width_method, &self.unicode.width_data); 140 + return gw.gwidth(str, self.screen.width_method); 145 141 } 146 142 147 143 /// Fills the View with the provided cell
+5 -10
src/widgets/terminal/Terminal.zig
··· 10 10 const vaxis = @import("../../main.zig"); 11 11 const Winsize = vaxis.Winsize; 12 12 const Screen = @import("Screen.zig"); 13 - const DisplayWidth = @import("DisplayWidth"); 14 13 const Key = vaxis.Key; 15 14 const Queue = vaxis.Queue(Event, 16); 16 - const code_point = @import("code_point"); 17 15 const key = @import("key.zig"); 18 16 19 17 pub const Event = union(enum) { ··· 71 69 // dirty is protected by back_mutex. Only access this field when you hold that mutex 72 70 dirty: bool = false, 73 71 74 - unicode: *const vaxis.Unicode, 75 72 should_quit: bool = false, 76 73 77 74 mode: Mode = .{}, ··· 90 87 allocator: std.mem.Allocator, 91 88 argv: []const []const u8, 92 89 env: *const std.process.EnvMap, 93 - unicode: *const vaxis.Unicode, 94 90 opts: Options, 95 91 write_buf: []u8, 96 92 ) !Terminal { ··· 120 116 .front_screen = try Screen.init(allocator, opts.winsize.cols, opts.winsize.rows), 121 117 .back_screen_pri = try Screen.init(allocator, opts.winsize.cols, opts.winsize.rows + opts.scrollback_size), 122 118 .back_screen_alt = try Screen.init(allocator, opts.winsize.cols, opts.winsize.rows), 123 - .unicode = unicode, 124 119 .tab_stops = tabs, 125 120 }; 126 121 } ··· 278 273 279 274 switch (event) { 280 275 .print => |str| { 281 - var iter = self.unicode.graphemeIterator(str); 282 - while (iter.next()) |g| { 283 - const gr = g.bytes(str); 276 + var iter = vaxis.unicode.graphemeIterator(str); 277 + while (iter.next()) |grapheme| { 278 + const gr = grapheme.bytes(str); 284 279 // TODO: use actual instead of .unicode 285 - const w = vaxis.gwidth.gwidth(gr, .unicode, &self.unicode.width_data); 280 + const w = vaxis.gwidth.gwidth(gr, .unicode); 286 281 try self.back_screen.print(gr, @truncate(w), self.mode.autowrap); 287 282 } 288 283 }, ··· 498 493 var iter = seq.iterator(u16); 499 494 const n = iter.next() orelse 1; 500 495 // TODO: maybe not .unicode 501 - const w = vaxis.gwidth.gwidth(self.last_printed, .unicode, &self.unicode.width_data); 496 + const w = vaxis.gwidth.gwidth(self.last_printed, .unicode); 502 497 var i: usize = 0; 503 498 while (i < n) : (i += 1) { 504 499 try self.back_screen.print(self.last_printed, @truncate(w), self.mode.autowrap);