a modern tui library written in zig

Compare changes

Choose any two refs to compare.

+8 -7
README.md
··· 290 290 const alloc = gpa.allocator(); 291 291 292 292 // Initialize a tty 293 - var tty = try vaxis.Tty.init(); 293 + var buffer: [1024]u8 = undefined; 294 + var tty = try vaxis.Tty.init(&buffer); 294 295 defer tty.deinit(); 295 296 296 297 // Initialize Vaxis 297 298 var vx = try vaxis.init(alloc, .{}); 298 299 // deinit takes an optional allocator. If your program is exiting, you can 299 300 // choose to pass a null allocator to save some exit time. 300 - defer vx.deinit(alloc, tty.anyWriter()); 301 + defer vx.deinit(alloc, tty.writer()); 301 302 302 303 303 304 // The event loop requires an intrusive init. We create an instance with ··· 317 318 defer loop.stop(); 318 319 319 320 // Optionally enter the alternate screen 320 - try vx.enterAltScreen(tty.anyWriter()); 321 + try vx.enterAltScreen(tty.writer()); 321 322 322 323 // We'll adjust the color index every keypress for the border 323 324 var color_idx: u8 = 0; 324 325 325 326 // init our text input widget. The text input widget needs an allocator to 326 327 // store the contents of the input 327 - var text_input = TextInput.init(alloc, &vx.unicode); 328 + var text_input = TextInput.init(alloc); 328 329 defer text_input.deinit(); 329 330 330 331 // Sends queries to terminal to detect certain features. This should always 331 332 // be called after entering the alt screen, if you are using the alt screen 332 - try vx.queryTerminal(tty.anyWriter(), 1 * std.time.ns_per_s); 333 + try vx.queryTerminal(tty.writer(), 1 * std.time.ns_per_s); 333 334 334 335 while (true) { 335 336 // nextEvent blocks until an event is in the queue ··· 365 366 // more than one byte will incur an allocation on the first render 366 367 // after it is drawn. Thereafter, it will not allocate unless the 367 368 // screen is resized 368 - .winsize => |ws| try vx.resize(alloc, tty.anyWriter(), ws), 369 + .winsize => |ws| try vx.resize(alloc, tty.writer(), ws), 369 370 else => {}, 370 371 } 371 372 ··· 401 402 402 403 // Render the screen. Using a buffered writer will offer much better 403 404 // performance, but is not required 404 - try vx.render(tty.anyWriter()); 405 + try vx.render(tty.writer()); 405 406 } 406 407 } 407 408 ```
+2 -2
USAGE.md
··· 247 247 self.vx.caps.color_scheme_updates = true; 248 248 }, 249 249 .cap_da1 => { 250 - self.vx.enableDetectedFeatures(self.tty.anyWriter()) catch |err| { 250 + self.vx.enableDetectedFeatures(self.tty.writer()) catch |err| { 251 251 log.err("couldn't enable features: {}", .{err}); 252 252 }; 253 253 }, ··· 328 328 } 329 329 330 330 pub fn deinit(self: *@This(), vx: *vaxis.Vaxis, tty: *vaxis.Tty) void { 331 - vx.deviceStatusReport(tty.anyWriter()) catch {}; 331 + vx.deviceStatusReport(tty.writer()) catch {}; 332 332 if (self.winsize_task) |task| task.cancel(); 333 333 if (self.reader_task) |task| task.cancel(); 334 334 self.source.deinit();
+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 = .{
+5 -5
examples/cli.zig
··· 19 19 defer tty.deinit(); 20 20 21 21 var vx = try vaxis.init(alloc, .{}); 22 - defer vx.deinit(alloc, tty.anyWriter()); 22 + defer vx.deinit(alloc, tty.writer()); 23 23 24 24 var loop: vaxis.Loop(Event) = .{ .tty = &tty, .vaxis = &vx }; 25 25 try loop.init(); ··· 27 27 try loop.start(); 28 28 defer loop.stop(); 29 29 30 - try vx.queryTerminal(tty.anyWriter(), 1 * std.time.ns_per_s); 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; ··· 75 75 } 76 76 }, 77 77 .winsize => |ws| { 78 - try vx.resize(alloc, tty.anyWriter(), ws); 78 + try vx.resize(alloc, tty.writer(), ws); 79 79 }, 80 80 else => {}, 81 81 } ··· 96 96 _ = win.print(&seg, .{ .row_offset = @intCast(j + 1) }); 97 97 } 98 98 } 99 - try vx.render(tty.anyWriter()); 99 + try vx.render(tty.writer()); 100 100 } 101 101 } 102 102
+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(), .{});
+11 -11
examples/image.zig
··· 23 23 defer tty.deinit(); 24 24 25 25 var vx = try vaxis.init(alloc, .{}); 26 - defer vx.deinit(alloc, tty.anyWriter()); 26 + defer vx.deinit(alloc, tty.writer()); 27 27 28 28 var loop: vaxis.Loop(Event) = .{ .tty = &tty, .vaxis = &vx }; 29 29 try loop.init(); ··· 31 31 try loop.start(); 32 32 defer loop.stop(); 33 33 34 - try vx.enterAltScreen(tty.anyWriter()); 35 - try vx.queryTerminal(tty.anyWriter(), 1 * std.time.ns_per_s); 34 + try vx.enterAltScreen(tty.writer()); 35 + try vx.queryTerminal(tty.writer(), 1 * std.time.ns_per_s); 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 - try vx.transmitImage(alloc, tty.anyWriter(), &img1, .rgba), 42 + try vx.transmitImage(alloc, tty.writer(), &img1, .rgba), 43 43 // var img1 = try vaxis.zigimg.Image.fromFilePath(alloc, "examples/zig.png"); 44 - // try vx.loadImage(alloc, tty.anyWriter(), .{ .path = "examples/zig.png" }), 45 - try vx.loadImage(alloc, tty.anyWriter(), .{ .path = "examples/vaxis.png" }), 44 + // try vx.loadImage(alloc, tty.writer(), .{ .path = "examples/zig.png" }), 45 + try vx.loadImage(alloc, tty.writer(), .{ .path = "examples/vaxis.png" }), 46 46 }; 47 - defer vx.freeImage(tty.anyWriter(), imgs[0].id); 48 - defer vx.freeImage(tty.anyWriter(), imgs[1].id); 47 + defer vx.freeImage(tty.writer(), imgs[0].id); 48 + defer vx.freeImage(tty.writer(), imgs[1].id); 49 49 50 50 var n: usize = 0; 51 51 ··· 64 64 else if (key.matches('k', .{})) 65 65 clip_y -|= 1; 66 66 }, 67 - .winsize => |ws| try vx.resize(alloc, tty.anyWriter(), ws), 67 + .winsize => |ws| try vx.resize(alloc, tty.writer(), ws), 68 68 } 69 69 70 70 n = (n + 1) % imgs.len; ··· 78 78 .y = clip_y, 79 79 } }); 80 80 81 - try vx.render(tty.anyWriter()); 81 + try vx.render(tty.writer()); 82 82 } 83 83 }
+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 + }
+5 -5
examples/main.zig
··· 19 19 defer tty.deinit(); 20 20 21 21 var vx = try vaxis.init(alloc, .{}); 22 - defer vx.deinit(alloc, tty.anyWriter()); 22 + defer vx.deinit(alloc, tty.writer()); 23 23 24 24 var loop: vaxis.Loop(Event) = .{ .tty = &tty, .vaxis = &vx }; 25 25 try loop.init(); ··· 28 28 defer loop.stop(); 29 29 30 30 // Optionally enter the alternate screen 31 - try vx.enterAltScreen(tty.anyWriter()); 32 - try vx.queryTerminal(tty.anyWriter(), 1 * std.time.ns_per_s); 31 + try vx.enterAltScreen(tty.writer()); 32 + try vx.queryTerminal(tty.writer(), 1 * std.time.ns_per_s); 33 33 34 34 // We'll adjust the color index every keypress 35 35 var color_idx: u8 = 0; ··· 66 66 } 67 67 }, 68 68 .winsize => |ws| { 69 - try vx.resize(alloc, tty.anyWriter(), ws); 69 + try vx.resize(alloc, tty.writer(), ws); 70 70 }, 71 71 else => {}, 72 72 } ··· 114 114 child.writeCell(@intCast(i), scale, second_cell); 115 115 } 116 116 // Render the screen 117 - try vx.render(tty.anyWriter()); 117 + try vx.render(tty.writer()); 118 118 } 119 119 } 120 120
+11 -15
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); 32 27 defer tty.deinit(); 33 - const tty_writer = tty.anyWriter(); 28 + const tty_writer = tty.writer(); 34 29 var vx = try vaxis.init(alloc, .{ 35 30 .kitty_keyboard_flags = .{ .report_events = true }, 36 31 }); 37 - defer vx.deinit(alloc, tty.anyWriter()); 32 + defer vx.deinit(alloc, tty.writer()); 38 33 39 34 var loop: vaxis.Loop(union(enum) { 40 35 key_press: vaxis.Key, ··· 44 39 try loop.init(); 45 40 try loop.start(); 46 41 defer loop.stop(); 47 - try vx.enterAltScreen(tty.anyWriter()); 48 - try vx.queryTerminal(tty.anyWriter(), 250 * std.time.ns_per_ms); 42 + try vx.enterAltScreen(tty.writer()); 43 + try vx.queryTerminal(tty.writer(), 250 * std.time.ns_per_ms); 49 44 50 45 const logo = 51 46 \\โ–‘โ–ˆโ–‘โ–ˆโ–‘โ–ˆโ–€โ–ˆโ–‘โ–ˆโ–‘โ–ˆโ–‘โ–€โ–ˆโ–€โ–‘โ–ˆโ–€โ–€โ–‘โ–‘โ–‘โ–€โ–ˆโ–€โ–‘โ–ˆโ–€โ–ˆโ–‘โ–ˆโ–€โ–„โ–‘โ–ˆโ–‘โ–‘โ–‘โ–ˆโ–€โ–€โ–‘ ··· 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])) { ··· 191 186 } 192 187 moving = false; 193 188 }, 194 - .winsize => |ws| try vx.resize(alloc, tty.anyWriter(), ws), 189 + .winsize => |ws| try vx.resize(alloc, tty.writer(), ws), 195 190 else => {}, 196 191 } 197 192 ··· 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 }
+7 -7
examples/text_input.zig
··· 36 36 37 37 // Use a buffered writer for better performance. There are a lot of writes 38 38 // in the render loop and this can have a significant savings 39 - const writer = tty.anyWriter(); 39 + const writer = tty.writer(); 40 40 41 41 // Initialize Vaxis 42 42 var vx = try vaxis.init(alloc, .{ 43 43 .kitty_keyboard_flags = .{ .report_events = true }, 44 44 }); 45 - defer vx.deinit(alloc, tty.anyWriter()); 45 + defer vx.deinit(alloc, tty.writer()); 46 46 47 47 var loop: vaxis.Loop(Event) = .{ 48 48 .vaxis = &vx, ··· 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); ··· 71 71 try writer.flush(); 72 72 // Sends queries to terminal to detect certain features. This should 73 73 // _always_ be called, but is left to the application to decide when 74 - try vx.queryTerminal(tty.anyWriter(), 1 * std.time.ns_per_s); 74 + try vx.queryTerminal(tty.writer(), 1 * std.time.ns_per_s); 75 75 76 76 // The main event loop. Vaxis provides a thread safe, blocking, buffered 77 77 // queue which can serve as the primary event queue for an application ··· 92 92 } else if (key.matches('l', .{ .ctrl = true })) { 93 93 vx.queueRefresh(); 94 94 } else if (key.matches('n', .{ .ctrl = true })) { 95 - try vx.notify(tty.anyWriter(), "vaxis", "hello from vaxis"); 95 + try vx.notify(tty.writer(), "vaxis", "hello from vaxis"); 96 96 loop.stop(); 97 97 var child = std.process.Child.init(&.{"nvim"}, alloc); 98 98 _ = try child.spawnAndWait(); 99 99 try loop.start(); 100 - try vx.enterAltScreen(tty.anyWriter()); 100 + try vx.enterAltScreen(tty.writer()); 101 101 vx.queueRefresh(); 102 102 } else if (key.matches(vaxis.Key.enter, .{}) or key.matches('j', .{ .ctrl = true })) { 103 103 text_input.clearAndFree(); ··· 121 121 // more than one byte will incur an allocation on the first render 122 122 // after it is drawn. Thereafter, it will not allocate unless the 123 123 // screen is resized 124 - .winsize => |ws| try vx.resize(alloc, tty.anyWriter(), ws), 124 + .winsize => |ws| try vx.resize(alloc, tty.writer(), ws), 125 125 else => {}, 126 126 } 127 127
+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 + }
+8 -8
examples/vaxis.zig
··· 25 25 defer tty.deinit(); 26 26 27 27 var vx = try vaxis.init(alloc, .{}); 28 - defer vx.deinit(alloc, tty.anyWriter()); 28 + defer vx.deinit(alloc, tty.writer()); 29 29 30 30 var loop: vaxis.Loop(Event) = .{ .tty = &tty, .vaxis = &vx }; 31 31 try loop.init(); ··· 33 33 try loop.start(); 34 34 defer loop.stop(); 35 35 36 - try vx.enterAltScreen(tty.anyWriter()); 37 - try vx.queryTerminal(tty.anyWriter(), 1 * std.time.ns_per_s); 36 + try vx.enterAltScreen(tty.writer()); 37 + try vx.queryTerminal(tty.writer(), 1 * std.time.ns_per_s); 38 38 39 - try vx.queryColor(tty.anyWriter(), .fg); 40 - try vx.queryColor(tty.anyWriter(), .bg); 39 + try vx.queryColor(tty.writer(), .fg); 40 + try vx.queryColor(tty.writer(), .bg); 41 41 var pct: u8 = 0; 42 42 var dir: enum { 43 43 up, ··· 53 53 switch (event) { 54 54 .key_press => |key| if (key.matches('c', .{ .ctrl = true })) return, 55 55 .winsize => |ws| { 56 - try vx.resize(alloc, tty.anyWriter(), ws); 56 + try vx.resize(alloc, tty.writer(), ws); 57 57 break; 58 58 }, 59 59 } ··· 63 63 while (loop.tryEvent()) |event| { 64 64 switch (event) { 65 65 .key_press => |key| if (key.matches('c', .{ .ctrl = true })) return, 66 - .winsize => |ws| try vx.resize(alloc, tty.anyWriter(), ws), 66 + .winsize => |ws| try vx.resize(alloc, tty.writer(), ws), 67 67 } 68 68 } 69 69 ··· 83 83 // var bw = tty.bufferedWriter(); 84 84 // try vx.render(bw.writer().any()); 85 85 // try bw.flush(); 86 - try vx.render(tty.anyWriter()); 86 + try vx.render(tty.writer()); 87 87 std.Thread.sleep(16 * std.time.ns_per_ms); 88 88 switch (dir) { 89 89 .up => {
+6 -6
examples/view.zig
··· 48 48 var tty = try vaxis.Tty.init(&buffer); 49 49 defer tty.deinit(); 50 50 51 - const writer = tty.anyWriter(); 51 + const writer = tty.writer(); 52 52 53 53 // Initialize Vaxis 54 54 var vx = try vaxis.init(alloc, .{ 55 55 .kitty_keyboard_flags = .{ .report_events = true }, 56 56 }); 57 - defer vx.deinit(alloc, tty.anyWriter()); 57 + defer vx.deinit(alloc, tty.writer()); 58 58 var loop: vaxis.Loop(Event) = .{ 59 59 .vaxis = &vx, 60 60 .tty = &tty, ··· 64 64 defer loop.stop(); 65 65 try vx.enterAltScreen(writer); 66 66 try writer.flush(); 67 - try vx.queryTerminal(tty.anyWriter(), 20 * std.time.ns_per_s); 67 + try vx.queryTerminal(tty.writer(), 20 * std.time.ns_per_s); 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; ··· 128 128 // Mini View (Forced Width & Height Limits) 129 129 if (key.matches('m', .{})) use_mini_view = !use_mini_view; 130 130 }, 131 - .winsize => |ws| try vx.resize(alloc, tty.anyWriter(), ws), 131 + .winsize => |ws| try vx.resize(alloc, tty.writer(), ws), 132 132 } 133 133 134 134 const win = vx.window();
+11 -14
examples/vt.zig
··· 22 22 23 23 var buffer: [1024]u8 = undefined; 24 24 var tty = try vaxis.Tty.init(&buffer); 25 + const writer = tty.writer(); 25 26 var vx = try vaxis.init(alloc, .{}); 26 - defer vx.deinit(alloc, tty.anyWriter()); 27 + defer vx.deinit(alloc, writer); 27 28 28 29 var loop: vaxis.Loop(Event) = .{ .tty = &tty, .vaxis = &vx }; 29 30 try loop.init(); ··· 31 32 try loop.start(); 32 33 defer loop.stop(); 33 34 34 - var buffered = tty.bufferedWriter(); 35 - 36 - try vx.enterAltScreen(tty.anyWriter()); 37 - try vx.queryTerminal(tty.anyWriter(), 1 * std.time.ns_per_s); 35 + try vx.enterAltScreen(writer); 36 + try vx.queryTerminal(writer, 1 * std.time.ns_per_s); 38 37 var env = try std.process.getEnvMap(alloc); 39 38 defer env.deinit(); 40 39 ··· 50 49 }; 51 50 const shell = env.get("SHELL") orelse "bash"; 52 51 const argv = [_][]const u8{shell}; 52 + var write_buf: [4096]u8 = undefined; 53 53 var vt = try vaxis.widgets.Terminal.init( 54 54 alloc, 55 55 &argv, 56 56 &env, 57 - &vx.unicode, 58 57 vt_opts, 58 + &write_buf, 59 59 ); 60 60 defer vt.deinit(); 61 61 try vt.spawn(); ··· 81 81 if (key.matches('c', .{ .ctrl = true })) return; 82 82 try vt.update(.{ .key_press = key }); 83 83 }, 84 - .winsize => |ws| { 85 - try vx.resize(alloc, tty.anyWriter(), ws); 86 - }, 84 + .winsize => |ws| try vx.resize(alloc, writer, ws), 87 85 } 88 86 } 89 87 if (!redraw) continue; ··· 95 93 const child = win.child(.{ 96 94 .x_off = 4, 97 95 .y_off = 2, 98 - .width = 8, 99 - .height = 6, 96 + .width = 120, 97 + .height = 40, 100 98 .border = .{ 101 99 .where = .all, 102 100 }, ··· 108 106 .x_pixel = 0, 109 107 .y_pixel = 0, 110 108 }); 111 - try vt.draw(child); 109 + try vt.draw(alloc, child); 112 110 113 - try vx.render(buffered.writer().any()); 114 - try buffered.flush(); 111 + try vx.render(writer); 115 112 } 116 113 }
+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 + }
+10 -14
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 } ··· 58 55 if (self.thread == null) return; 59 56 self.should_quit = true; 60 57 // trigger a read 61 - self.vaxis.deviceStatusReport(self.tty.anyWriter()) catch {}; 58 + self.vaxis.deviceStatusReport(self.tty.writer()) catch {}; 62 59 63 60 if (self.thread) |thread| { 64 61 thread.join(); ··· 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; ··· 362 354 log.info("color_scheme_updates capability detected", .{}); 363 355 vx.caps.color_scheme_updates = true; 364 356 }, 357 + .cap_multi_cursor => { 358 + log.info("multi cursor capability detected", .{}); 359 + vx.caps.multi_cursor = true; 360 + }, 365 361 .cap_da1 => { 366 362 std.Thread.Futex.wake(&vx.query_futex, 10); 367 363 vx.queries_done.store(true, .unordered); ··· 394 390 defer tty.deinit(); 395 391 396 392 var vx = try vaxis.init(std.testing.allocator, .{}); 397 - defer vx.deinit(std.testing.allocator, tty.anyWriter()); 393 + defer vx.deinit(std.testing.allocator, tty.writer()); 398 394 399 395 var loop: vaxis.Loop(Event) = .{ .tty = &tty, .vaxis = &vx }; 400 396 try loop.init(); ··· 403 399 defer loop.stop(); 404 400 405 401 // Optionally enter the alternate screen 406 - try vx.enterAltScreen(tty.anyWriter()); 407 - try vx.queryTerminal(tty.anyWriter(), 1 * std.time.ns_per_ms); 402 + try vx.enterAltScreen(tty.writer()); 403 + try vx.queryTerminal(tty.writer(), 1 * std.time.ns_per_ms); 408 404 }
+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,
+185 -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] }; ··· 646 652 else => return null_event, 647 653 } 648 654 }, 655 + 'q' => { 656 + // kitty multi cursor cap (CSI > 1;2;3;29;30;40;100;101 TRAILER) (TRAILER is " q") 657 + const second_final = sequence[sequence.len - 2]; 658 + if (second_final != ' ') return null_event; 659 + // check for any digits. we're not too picky about checking the supported cursor types here 660 + for (sequence[0 .. sequence.len - 2]) |c| switch (c) { 661 + '0'...'9' => return .{ .event = .cap_multi_cursor, .n = sequence.len }, 662 + else => continue, 663 + }; 664 + return null_event; 665 + }, 649 666 else => return null_event, 650 667 } 651 668 } ··· 653 670 /// Parse a param buffer, returning a default value if the param was empty 654 671 inline fn parseParam(comptime T: type, buf: []const u8, default: ?T) ?T { 655 672 if (buf.len == 0) return default; 656 - return std.fmt.parseUnsigned(T, buf, 10) catch return null; 673 + return std.fmt.parseInt(T, buf, 10) catch return null; 657 674 } 658 675 659 676 /// Parse a mouse event ··· 661 678 const null_event: Result = .{ .event = null, .n = input.len }; 662 679 663 680 var button_mask: u16 = undefined; 664 - var px: u16 = undefined; 665 - var py: u16 = undefined; 681 + var px: i16 = undefined; 682 + var py: i16 = undefined; 666 683 var xterm: bool = undefined; 667 684 if (input.len == 3 and (input[2] == 'M') and full_input.len >= 6) { 668 685 xterm = true; ··· 674 691 const delim1 = std.mem.indexOfScalarPos(u8, input, 3, ';') orelse return null_event; 675 692 button_mask = parseParam(u16, input[3..delim1], null) orelse return null_event; 676 693 const delim2 = std.mem.indexOfScalarPos(u8, input, delim1 + 1, ';') orelse return null_event; 677 - px = parseParam(u16, input[delim1 + 1 .. delim2], 1) orelse return null_event; 678 - 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; 679 696 } else { 680 697 return null_event; 681 698 } ··· 720 737 721 738 test "parse: single xterm keypress" { 722 739 const alloc = testing.allocator_instance.allocator(); 723 - const grapheme_data = try Graphemes.init(alloc); 724 - defer grapheme_data.deinit(alloc); 725 740 const input = "a"; 726 - var parser: Parser = .{ .grapheme_data = &grapheme_data }; 741 + var parser: Parser = .{}; 727 742 const result = try parser.parse(input, alloc); 728 743 const expected_key: Key = .{ 729 744 .codepoint = 'a', ··· 737 752 738 753 test "parse: single xterm keypress backspace" { 739 754 const alloc = testing.allocator_instance.allocator(); 740 - const grapheme_data = try Graphemes.init(alloc); 741 - defer grapheme_data.deinit(alloc); 742 755 const input = "\x08"; 743 - var parser: Parser = .{ .grapheme_data = &grapheme_data }; 756 + var parser: Parser = .{}; 744 757 const result = try parser.parse(input, alloc); 745 758 const expected_key: Key = .{ 746 759 .codepoint = Key.backspace, ··· 753 766 754 767 test "parse: single xterm keypress with more buffer" { 755 768 const alloc = testing.allocator_instance.allocator(); 756 - const grapheme_data = try Graphemes.init(alloc); 757 - defer grapheme_data.deinit(alloc); 758 769 const input = "ab"; 759 - var parser: Parser = .{ .grapheme_data = &grapheme_data }; 770 + var parser: Parser = .{}; 760 771 const result = try parser.parse(input, alloc); 761 772 const expected_key: Key = .{ 762 773 .codepoint = 'a', ··· 771 782 772 783 test "parse: xterm escape keypress" { 773 784 const alloc = testing.allocator_instance.allocator(); 774 - const grapheme_data = try Graphemes.init(alloc); 775 - defer grapheme_data.deinit(alloc); 776 785 const input = "\x1b"; 777 - var parser: Parser = .{ .grapheme_data = &grapheme_data }; 786 + var parser: Parser = .{}; 778 787 const result = try parser.parse(input, alloc); 779 788 const expected_key: Key = .{ .codepoint = Key.escape }; 780 789 const expected_event: Event = .{ .key_press = expected_key }; ··· 785 794 786 795 test "parse: xterm ctrl+a" { 787 796 const alloc = testing.allocator_instance.allocator(); 788 - const grapheme_data = try Graphemes.init(alloc); 789 - defer grapheme_data.deinit(alloc); 790 797 const input = "\x01"; 791 - var parser: Parser = .{ .grapheme_data = &grapheme_data }; 798 + var parser: Parser = .{}; 792 799 const result = try parser.parse(input, alloc); 793 800 const expected_key: Key = .{ .codepoint = 'a', .mods = .{ .ctrl = true } }; 794 801 const expected_event: Event = .{ .key_press = expected_key }; ··· 799 806 800 807 test "parse: xterm alt+a" { 801 808 const alloc = testing.allocator_instance.allocator(); 802 - const grapheme_data = try Graphemes.init(alloc); 803 - defer grapheme_data.deinit(alloc); 804 809 const input = "\x1ba"; 805 - var parser: Parser = .{ .grapheme_data = &grapheme_data }; 810 + var parser: Parser = .{}; 806 811 const result = try parser.parse(input, alloc); 807 812 const expected_key: Key = .{ .codepoint = 'a', .mods = .{ .alt = true } }; 808 813 const expected_event: Event = .{ .key_press = expected_key }; ··· 813 818 814 819 test "parse: xterm key up" { 815 820 const alloc = testing.allocator_instance.allocator(); 816 - const grapheme_data = try Graphemes.init(alloc); 817 - defer grapheme_data.deinit(alloc); 818 821 { 819 822 // normal version 820 823 const input = "\x1b[A"; 821 - var parser: Parser = .{ .grapheme_data = &grapheme_data }; 824 + var parser: Parser = .{}; 822 825 const result = try parser.parse(input, alloc); 823 826 const expected_key: Key = .{ .codepoint = Key.up }; 824 827 const expected_event: Event = .{ .key_press = expected_key }; ··· 830 833 { 831 834 // application keys version 832 835 const input = "\x1bOA"; 833 - var parser: Parser = .{ .grapheme_data = &grapheme_data }; 836 + var parser: Parser = .{}; 834 837 const result = try parser.parse(input, alloc); 835 838 const expected_key: Key = .{ .codepoint = Key.up }; 836 839 const expected_event: Event = .{ .key_press = expected_key }; ··· 842 845 843 846 test "parse: xterm shift+up" { 844 847 const alloc = testing.allocator_instance.allocator(); 845 - const grapheme_data = try Graphemes.init(alloc); 846 - defer grapheme_data.deinit(alloc); 847 848 const input = "\x1b[1;2A"; 848 - var parser: Parser = .{ .grapheme_data = &grapheme_data }; 849 + var parser: Parser = .{}; 849 850 const result = try parser.parse(input, alloc); 850 851 const expected_key: Key = .{ .codepoint = Key.up, .mods = .{ .shift = true } }; 851 852 const expected_event: Event = .{ .key_press = expected_key }; ··· 856 857 857 858 test "parse: xterm insert" { 858 859 const alloc = testing.allocator_instance.allocator(); 859 - const grapheme_data = try Graphemes.init(alloc); 860 - defer grapheme_data.deinit(alloc); 861 860 const input = "\x1b[2~"; 862 - var parser: Parser = .{ .grapheme_data = &grapheme_data }; 861 + var parser: Parser = .{}; 863 862 const result = try parser.parse(input, alloc); 864 863 const expected_key: Key = .{ .codepoint = Key.insert, .mods = .{} }; 865 864 const expected_event: Event = .{ .key_press = expected_key }; ··· 870 869 871 870 test "parse: paste_start" { 872 871 const alloc = testing.allocator_instance.allocator(); 873 - const grapheme_data = try Graphemes.init(alloc); 874 - defer grapheme_data.deinit(alloc); 875 872 const input = "\x1b[200~"; 876 - var parser: Parser = .{ .grapheme_data = &grapheme_data }; 873 + var parser: Parser = .{}; 877 874 const result = try parser.parse(input, alloc); 878 875 const expected_event: Event = .paste_start; 879 876 ··· 883 880 884 881 test "parse: paste_end" { 885 882 const alloc = testing.allocator_instance.allocator(); 886 - const grapheme_data = try Graphemes.init(alloc); 887 - defer grapheme_data.deinit(alloc); 888 883 const input = "\x1b[201~"; 889 - var parser: Parser = .{ .grapheme_data = &grapheme_data }; 884 + var parser: Parser = .{}; 890 885 const result = try parser.parse(input, alloc); 891 886 const expected_event: Event = .paste_end; 892 887 ··· 896 891 897 892 test "parse: osc52 paste" { 898 893 const alloc = testing.allocator_instance.allocator(); 899 - const grapheme_data = try Graphemes.init(alloc); 900 - defer grapheme_data.deinit(alloc); 901 894 const input = "\x1b]52;c;b3NjNTIgcGFzdGU=\x1b\\"; 902 895 const expected_text = "osc52 paste"; 903 - var parser: Parser = .{ .grapheme_data = &grapheme_data }; 896 + var parser: Parser = .{}; 904 897 const result = try parser.parse(input, alloc); 905 898 906 899 try testing.expectEqual(25, result.n); ··· 915 908 916 909 test "parse: focus_in" { 917 910 const alloc = testing.allocator_instance.allocator(); 918 - const grapheme_data = try Graphemes.init(alloc); 919 - defer grapheme_data.deinit(alloc); 920 911 const input = "\x1b[I"; 921 - var parser: Parser = .{ .grapheme_data = &grapheme_data }; 912 + var parser: Parser = .{}; 922 913 const result = try parser.parse(input, alloc); 923 914 const expected_event: Event = .focus_in; 924 915 ··· 928 919 929 920 test "parse: focus_out" { 930 921 const alloc = testing.allocator_instance.allocator(); 931 - const grapheme_data = try Graphemes.init(alloc); 932 - defer grapheme_data.deinit(alloc); 933 922 const input = "\x1b[O"; 934 - var parser: Parser = .{ .grapheme_data = &grapheme_data }; 923 + var parser: Parser = .{}; 935 924 const result = try parser.parse(input, alloc); 936 925 const expected_event: Event = .focus_out; 937 926 ··· 941 930 942 931 test "parse: kitty: shift+a without text reporting" { 943 932 const alloc = testing.allocator_instance.allocator(); 944 - const grapheme_data = try Graphemes.init(alloc); 945 - defer grapheme_data.deinit(alloc); 946 933 const input = "\x1b[97:65;2u"; 947 - var parser: Parser = .{ .grapheme_data = &grapheme_data }; 934 + var parser: Parser = .{}; 948 935 const result = try parser.parse(input, alloc); 949 936 const expected_key: Key = .{ 950 937 .codepoint = 'a', ··· 960 947 961 948 test "parse: kitty: alt+shift+a without text reporting" { 962 949 const alloc = testing.allocator_instance.allocator(); 963 - const grapheme_data = try Graphemes.init(alloc); 964 - defer grapheme_data.deinit(alloc); 965 950 const input = "\x1b[97:65;4u"; 966 - var parser: Parser = .{ .grapheme_data = &grapheme_data }; 951 + var parser: Parser = .{}; 967 952 const result = try parser.parse(input, alloc); 968 953 const expected_key: Key = .{ 969 954 .codepoint = 'a', ··· 978 963 979 964 test "parse: kitty: a without text reporting" { 980 965 const alloc = testing.allocator_instance.allocator(); 981 - const grapheme_data = try Graphemes.init(alloc); 982 - defer grapheme_data.deinit(alloc); 983 966 const input = "\x1b[97u"; 984 - var parser: Parser = .{ .grapheme_data = &grapheme_data }; 967 + var parser: Parser = .{}; 985 968 const result = try parser.parse(input, alloc); 986 969 const expected_key: Key = .{ 987 970 .codepoint = 'a', ··· 994 977 995 978 test "parse: kitty: release event" { 996 979 const alloc = testing.allocator_instance.allocator(); 997 - const grapheme_data = try Graphemes.init(alloc); 998 - defer grapheme_data.deinit(alloc); 999 980 const input = "\x1b[97;1:3u"; 1000 - var parser: Parser = .{ .grapheme_data = &grapheme_data }; 981 + var parser: Parser = .{}; 1001 982 const result = try parser.parse(input, alloc); 1002 983 const expected_key: Key = .{ 1003 984 .codepoint = 'a', ··· 1010 991 1011 992 test "parse: single codepoint" { 1012 993 const alloc = testing.allocator_instance.allocator(); 1013 - const grapheme_data = try Graphemes.init(alloc); 1014 - defer grapheme_data.deinit(alloc); 1015 994 const input = "๐Ÿ™‚"; 1016 - var parser: Parser = .{ .grapheme_data = &grapheme_data }; 995 + var parser: Parser = .{}; 1017 996 const result = try parser.parse(input, alloc); 1018 997 const expected_key: Key = .{ 1019 998 .codepoint = 0x1F642, ··· 1027 1006 1028 1007 test "parse: single codepoint with more in buffer" { 1029 1008 const alloc = testing.allocator_instance.allocator(); 1030 - const grapheme_data = try Graphemes.init(alloc); 1031 - defer grapheme_data.deinit(alloc); 1032 1009 const input = "๐Ÿ™‚a"; 1033 - var parser: Parser = .{ .grapheme_data = &grapheme_data }; 1010 + var parser: Parser = .{}; 1034 1011 const result = try parser.parse(input, alloc); 1035 1012 const expected_key: Key = .{ 1036 1013 .codepoint = 0x1F642, ··· 1044 1021 1045 1022 test "parse: multiple codepoint grapheme" { 1046 1023 const alloc = testing.allocator_instance.allocator(); 1047 - const grapheme_data = try Graphemes.init(alloc); 1048 - defer grapheme_data.deinit(alloc); 1049 1024 const input = "๐Ÿ‘ฉโ€๐Ÿš€"; 1050 - var parser: Parser = .{ .grapheme_data = &grapheme_data }; 1025 + var parser: Parser = .{}; 1051 1026 const result = try parser.parse(input, alloc); 1052 1027 const expected_key: Key = .{ 1053 1028 .codepoint = Key.multicodepoint, ··· 1061 1036 1062 1037 test "parse: multiple codepoint grapheme with more after" { 1063 1038 const alloc = testing.allocator_instance.allocator(); 1064 - const grapheme_data = try Graphemes.init(alloc); 1065 - defer grapheme_data.deinit(alloc); 1066 1039 const input = "๐Ÿ‘ฉโ€๐Ÿš€abc"; 1067 - var parser: Parser = .{ .grapheme_data = &grapheme_data }; 1040 + var parser: Parser = .{}; 1068 1041 const result = try parser.parse(input, alloc); 1069 1042 const expected_key: Key = .{ 1070 1043 .codepoint = Key.multicodepoint, ··· 1077 1050 try testing.expectEqual(expected_key.codepoint, actual.codepoint); 1078 1051 } 1079 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 + 1130 + test "parse(csi): kitty multi cursor" { 1131 + var buf: [1]u8 = undefined; 1132 + { 1133 + const input = "\x1b[>1;2;3;29;30;40;100;101 q"; 1134 + const result = parseCsi(input, &buf); 1135 + const expected: Result = .{ 1136 + .event = .cap_multi_cursor, 1137 + .n = input.len, 1138 + }; 1139 + 1140 + try testing.expectEqual(expected.n, result.n); 1141 + try testing.expectEqual(expected.event, result.event); 1142 + } 1143 + { 1144 + const input = "\x1b[> q"; 1145 + const result = parseCsi(input, &buf); 1146 + const expected: Result = .{ 1147 + .event = null, 1148 + .n = input.len, 1149 + }; 1150 + 1151 + try testing.expectEqual(expected.n, result.n); 1152 + try testing.expectEqual(expected.event, result.event); 1153 + } 1154 + } 1155 + 1080 1156 test "parse(csi): decrpm" { 1081 1157 var buf: [1]u8 = undefined; 1082 1158 { ··· 1172 1248 try testing.expectEqual(expected.event, result.event); 1173 1249 } 1174 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 + 1175 1270 test "parse(csi): xterm mouse" { 1176 1271 var buf: [1]u8 = undefined; 1177 1272 const input = "\x1b[M\x20\x21\x21"; ··· 1193 1288 1194 1289 test "parse: disambiguate shift + space" { 1195 1290 const alloc = testing.allocator_instance.allocator(); 1196 - const grapheme_data = try Graphemes.init(alloc); 1197 - defer grapheme_data.deinit(alloc); 1198 1291 const input = "\x1b[32;2u"; 1199 - var parser: Parser = .{ .grapheme_data = &grapheme_data }; 1292 + var parser: Parser = .{}; 1200 1293 const result = try parser.parse(input, alloc); 1201 1294 const expected_key: Key = .{ 1202 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 - }
+84 -44
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; ··· 38 38 color_scheme_updates: bool = false, 39 39 explicit_width: bool = false, 40 40 scaled_text: bool = false, 41 + multi_cursor: bool = false, 41 42 }; 42 43 43 44 pub const Options = struct { ··· 73 74 // images 74 75 next_img_id: u32 = 1, 75 76 76 - unicode: Unicode, 77 - 78 77 sgr: enum { 79 78 standard, 80 79 legacy, ··· 109 108 .opts = opts, 110 109 .screen = .{}, 111 110 .screen_last = try .init(alloc, 0, 0), 112 - .unicode = try Unicode.init(alloc), 113 111 }; 114 112 } 115 113 ··· 123 121 if (alloc) |a| { 124 122 self.screen.deinit(a); 125 123 self.screen_last.deinit(a); 126 - self.unicode.deinit(a); 127 124 } 128 125 } 129 126 ··· 226 223 .width = self.screen.width, 227 224 .height = self.screen.height, 228 225 .screen = &self.screen, 229 - .unicode = &self.unicode, 230 226 }; 231 227 } 232 228 ··· 300 296 // why we see a Shift modifier 301 297 ctlseqs.home ++ 302 298 ctlseqs.scaled_text_query ++ 299 + ctlseqs.multi_cursor_query ++ 303 300 ctlseqs.cursor_position_request ++ 304 301 ctlseqs.xtversion ++ 305 302 ctlseqs.csi_u_query ++ ··· 363 360 assert(self.screen.buf.len == @as(usize, @intCast(self.screen.width)) * self.screen.height); // correct size 364 361 assert(self.screen.buf.len == self.screen_last.buf.len); // same size 365 362 366 - // Set up sync before we write anything 367 - // TODO: optimize sync so we only sync _when we have changes_. This 368 - // requires a smarter buffered writer, we'll probably have to write 369 - // our own 370 - try tty.writeAll(ctlseqs.sync_set); 371 - 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 {}; 372 366 373 - // Send the cursor to 0,0 374 - // TODO: this needs to move after we optimize writes. We only do 375 - // this if we have an update to make. We also need to hide cursor 376 - // and then reshow it if needed 377 - try tty.writeAll(ctlseqs.hide_cursor); 378 - if (self.state.alt_screen) 379 - try tty.writeAll(ctlseqs.home) 380 - else { 381 - try tty.writeByte('\r'); 382 - for (0..self.state.cursor.row) |_| { 383 - try tty.writeAll(ctlseqs.ri); 384 - } 385 - } 386 - 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; 387 374 388 375 // initialize some variables 389 376 var reposition: bool = false; ··· 391 378 var col: u16 = 0; 392 379 var cursor: Style = .{}; 393 380 var link: Hyperlink = .{}; 394 - var cursor_pos: struct { 381 + const CursorPos = struct { 395 382 row: u16 = 0, 396 383 col: u16 = 0, 397 - } = .{}; 384 + }; 385 + var cursor_pos: CursorPos = .{}; 398 386 399 - // Clear all images 400 - if (self.caps.kitty_graphics) 401 - 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 + }; 402 419 403 420 // Reset skip flag on all last_screen cells 404 421 for (self.screen_last.buf) |*last_cell| { 405 422 last_cell.skip = false; 406 423 } 407 424 425 + if (needs_render) { 426 + try startRender.run(self, tty, &cursor_pos, &reposition, &started, &sync_active); 427 + } 428 + 408 429 var i: usize = 0; 409 430 while (i < self.screen.buf.len) { 410 431 const cell = self.screen.buf[i]; ··· 412 433 if (cell.char.width != 0) break :blk cell.char.width; 413 434 414 435 const method: gwidth.Method = self.caps.unicode; 415 - 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)); 416 437 break :blk @max(1, width); 417 438 }; 418 439 defer { ··· 449 470 try tty.writeAll(ctlseqs.osc8_clear); 450 471 } 451 472 continue; 473 + } 474 + if (!started) { 475 + try startRender.run(self, tty, &cursor_pos, &reposition, &started, &sync_active); 452 476 } 453 477 self.screen_last.buf[i].skipped = false; 454 478 defer { ··· 733 757 cursor_pos.col = col + w; 734 758 cursor_pos.row = row; 735 759 } 760 + if (!started) return; 736 761 if (self.screen.cursor_vis) { 737 762 if (self.state.alt_screen) { 738 763 try tty.print( ··· 764 789 self.state.cursor.row = cursor_pos.row; 765 790 self.state.cursor.col = cursor_pos.col; 766 791 } 792 + self.screen_last.cursor_vis = self.screen.cursor_vis; 767 793 if (self.screen.mouse_shape != self.screen_last.mouse_shape) { 768 794 try tty.print( 769 795 ctlseqs.osc22_mouse_shape, ··· 854 880 const ypos = mouse.row; 855 881 const xextra = self.screen.width_pix % self.screen.width; 856 882 const yextra = self.screen.height_pix % self.screen.height; 857 - const xcell = (self.screen.width_pix - xextra) / self.screen.width; 858 - 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); 859 885 if (xcell == 0 or ycell == 0) return mouse; 860 - result.col = xpos / xcell; 861 - result.row = ypos / ycell; 862 - result.xoffset = xpos % xcell; 863 - 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)); 864 890 } 865 891 return result; 866 892 } ··· 998 1024 const buf = switch (format) { 999 1025 .png => png: { 1000 1026 const png_buf = try arena.allocator().alloc(u8, img.imageByteSize()); 1001 - const png = try img.writeToMemory(png_buf, .{ .png = .{} }); 1027 + const png = try img.writeToMemory(arena.allocator(), png_buf, .{ .png = .{} }); 1002 1028 break :png png; 1003 1029 }, 1004 1030 .rgb => rgb: { 1005 - try img.convert(.rgb24); 1031 + try img.convert(arena.allocator(), .rgb24); 1006 1032 break :rgb img.rawBytes(); 1007 1033 }, 1008 1034 .rgba => rgba: { 1009 - try img.convert(.rgba32); 1035 + try img.convert(arena.allocator(), .rgba32); 1010 1036 break :rgba img.rawBytes(); 1011 1037 }, 1012 1038 }; ··· 1030 1056 .path => |path| try zigimg.Image.fromFilePath(alloc, path, &read_buffer), 1031 1057 .mem => |bytes| try zigimg.Image.fromMemory(alloc, bytes), 1032 1058 }; 1033 - defer img.deinit(); 1059 + defer img.deinit(alloc); 1034 1060 return self.transmitImage(alloc, tty, &img, .png); 1035 1061 } 1036 1062 ··· 1147 1173 if (cell.char.width != 0) break :blk cell.char.width; 1148 1174 1149 1175 const method: gwidth.Method = self.caps.unicode; 1150 - const width = gwidth.gwidth(cell.char.grapheme, method, &self.unicode.width_data); 1176 + const width = gwidth.gwidth(cell.char.grapheme, method); 1151 1177 break :blk @max(1, width); 1152 1178 }; 1153 1179 defer { ··· 1409 1435 .host = .{ .raw = hostname }, 1410 1436 .path = .{ .raw = path }, 1411 1437 }; 1412 - try tty.print(ctlseqs.osc7, .{uri}); 1438 + try tty.print(ctlseqs.osc7, .{uri.fmt(.{ .scheme = true, .authority = true, .path = true })}); 1413 1439 try tty.flush(); 1414 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,
+2 -1
src/ctlseqs.zig
··· 14 14 pub const cursor_position_request = "\x1b[6n"; 15 15 pub const explicit_width_query = "\x1b]66;w=1; \x1b\\"; 16 16 pub const scaled_text_query = "\x1b]66;s=2; \x1b\\"; 17 + pub const multi_cursor_query = "\x1b[> q"; 17 18 18 19 // mouse. We try for button motion and any motion. terminals will enable the 19 20 // last one we tried (any motion). This was added because zellij doesn't ··· 122 123 123 124 // OSC sequences 124 125 pub const osc2_set_title = "\x1b]2;{s}\x1b\\"; 125 - pub const osc7 = "\x1b]7;{;+/}\x1b\\"; 126 + pub const osc7 = "\x1b]7;{f}\x1b\\"; 126 127 pub const osc8 = "\x1b]8;{s};{s}\x1b\\"; 127 128 pub const osc8_clear = "\x1b]8;;\x1b\\"; 128 129 pub const osc9_notify = "\x1b]9;{s}\x1b\\";
+1
src/event.zig
··· 26 26 cap_unicode, 27 27 cap_da1, 28 28 cap_color_scheme_updates, 29 + cap_multi_cursor, 29 30 };
+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 }
+3 -5
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 ··· 73 71 ctlseqs.bp_reset ++ 74 72 ctlseqs.rmcup; 75 73 76 - gty.anyWriter().writeAll(reset) catch {}; 77 - 74 + gty.writer().writeAll(reset) catch {}; 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();
+19 -22
src/tty.zig
··· 33 33 /// The file descriptor of the tty 34 34 fd: posix.fd_t, 35 35 36 - reader: std.fs.File.Reader, 37 - 38 36 /// File.Writer for efficient buffered writing 39 - writer: std.fs.File.Writer, 37 + tty_writer: std.fs.File.Writer, 40 38 41 39 pub const SignalHandler = struct { 42 40 context: *anyopaque, ··· 76 74 const self: PosixTty = .{ 77 75 .fd = fd, 78 76 .termios = termios, 79 - .reader = file.reader(), 80 - .writer = .initStreaming(file, buffer), 77 + .tty_writer = .initStreaming(file, buffer), 81 78 }; 82 79 83 80 global_tty = self; ··· 109 106 posix.sigaction(posix.SIG.WINCH, &act, null); 110 107 } 111 108 112 - pub fn anyWriter(self: *PosixTty) *std.Io.Writer { 113 - return &self.writer.interface; 109 + pub fn writer(self: *PosixTty) *std.Io.Writer { 110 + return &self.tty_writer.interface; 114 111 } 115 112 116 113 pub fn read(self: *const PosixTty, buf: []u8) !usize { ··· 201 198 buf: [4]u8 = undefined, 202 199 203 200 /// File.Writer for efficient buffered writing 204 - reader: std.fs.File.Writer, 205 - writer: std.fs.File.Writer, 201 + tty_writer: std.fs.File.Writer, 206 202 207 203 /// The last mouse button that was pressed. We store the previous state of button presses on each 208 204 /// mouse event so we can detect which button was released ··· 226 222 }; 227 223 228 224 pub fn init(buffer: []u8) !Tty { 229 - const stdin: std.fs.File = .stdout(); 225 + const stdin: std.fs.File = .stdin(); 230 226 const stdout: std.fs.File = .stdout(); 231 227 232 228 // get initial modes ··· 246 242 .initial_codepage = initial_output_codepage, 247 243 .initial_input_mode = initial_input_mode, 248 244 .initial_output_mode = initial_output_mode, 249 - .writer = .initStreaming(stdout, buffer), 245 + .tty_writer = .initStreaming(stdout, buffer), 250 246 }; 251 247 252 248 // save a copy of this tty as the global_tty for panic handling ··· 301 297 }; 302 298 } 303 299 304 - pub fn anyWriter(self: *Tty) *std.Io.Writer { 305 - return &self.writer.interface; 300 + pub fn writer(self: *Tty) *std.Io.Writer { 301 + return &self.tty_writer.interface; 306 302 } 307 303 308 304 pub fn read(self: *const Tty, buf: []u8) !usize { ··· 457 453 0xc0 => '`', 458 454 0xdb => '[', 459 455 0xdc => '\\', 456 + 0xdf => '\\', 460 457 0xe2 => '\\', 461 458 0xdd => ']', 462 459 0xde => '\'', ··· 579 576 }; 580 577 581 578 const mouse: Mouse = .{ 582 - .col = @as(u16, @bitCast(event.dwMousePosition.X)), // Windows reports with 0 index 583 - .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 584 581 .mods = mods, 585 582 .type = event_type, 586 583 .button = btn, ··· 691 688 }; 692 689 pub const PINPUT_RECORD = *INPUT_RECORD; 693 690 694 - pub extern "kernel32" fn ReadConsoleInputW(hConsoleInput: windows.HANDLE, lpBuffer: PINPUT_RECORD, nLength: windows.DWORD, lpNumberOfEventsRead: *windows.DWORD) callconv(windows.WINAPI) windows.BOOL; 691 + pub extern "kernel32" fn ReadConsoleInputW(hConsoleInput: windows.HANDLE, lpBuffer: PINPUT_RECORD, nLength: windows.DWORD, lpNumberOfEventsRead: *windows.DWORD) callconv(.winapi) windows.BOOL; 695 692 }; 696 693 697 694 pub const TestTty = struct { ··· 699 696 fd: posix.fd_t, 700 697 pipe_read: posix.fd_t, 701 698 pipe_write: posix.fd_t, 702 - writer: *std.Io.Writer.Allocating, 699 + tty_writer: *std.Io.Writer.Allocating, 703 700 704 701 /// Initializes a TestTty. 705 702 pub fn init(buffer: []u8) !TestTty { ··· 713 710 .fd = r, 714 711 .pipe_read = r, 715 712 .pipe_write = w, 716 - .writer = list, 713 + .tty_writer = list, 717 714 }; 718 715 } 719 716 720 717 pub fn deinit(self: TestTty) void { 721 718 std.posix.close(self.pipe_read); 722 719 std.posix.close(self.pipe_write); 723 - self.writer.deinit(); 724 - std.testing.allocator.destroy(self.writer); 720 + self.tty_writer.deinit(); 721 + std.testing.allocator.destroy(self.tty_writer); 725 722 } 726 723 727 - pub fn anyWriter(self: *TestTty) *std.Io.Writer { 728 - return &self.writer.writer; 724 + pub fn writer(self: *TestTty) *std.Io.Writer { 725 + return &self.tty_writer.writer; 729 726 } 730 727 731 728 pub fn read(self: *const TestTty, buf: []u8) !usize {
+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 + }
+16 -18
src/vxfw/App.zig
··· 47 47 48 48 pub fn deinit(self: *App) void { 49 49 self.timers.deinit(self.allocator); 50 - self.vx.deinit(self.allocator, self.tty.anyWriter()); 50 + self.vx.deinit(self.allocator, self.tty.writer()); 51 51 self.tty.deinit(); 52 52 } 53 53 ··· 64 64 // Also always initialize the app with a focus event 65 65 loop.postEvent(.focus_in); 66 66 67 - try vx.enterAltScreen(tty.anyWriter()); 68 - try vx.queryTerminal(tty.anyWriter(), 1 * std.time.ns_per_s); 69 - try vx.setBracketedPaste(tty.anyWriter(), true); 70 - try vx.subscribeToColorSchemeUpdates(tty.anyWriter()); 67 + try vx.enterAltScreen(tty.writer()); 68 + try vx.queryTerminal(tty.writer(), 1 * std.time.ns_per_s); 69 + try vx.setBracketedPaste(tty.writer(), true); 70 + try vx.subscribeToColorSchemeUpdates(tty.writer()); 71 71 72 72 { 73 73 // This part deserves a comment. loop.init installs a signal handler for the tty. We wait to ··· 78 78 79 79 // NOTE: We don't use pixel mouse anywhere 80 80 vx.caps.sgr_pixels = false; 81 - try vx.setMouseMode(tty.anyWriter(), true); 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 ··· 149 148 }, 150 149 .mouse => |mouse| try mouse_handler.handleMouse(self, &ctx, mouse), 151 150 .winsize => |ws| { 152 - try vx.resize(self.allocator, tty.anyWriter(), ws); 151 + try vx.resize(self.allocator, tty.writer(), ws); 153 152 ctx.redraw = true; 154 153 }, 155 154 else => { ··· 246 245 }); 247 246 surface.render(root_win, focused_widget); 248 247 249 - try vx.render(tty.anyWriter()); 248 + try vx.render(tty.writer()); 250 249 } 251 250 252 251 fn addTick(self: *App, tick: vxfw.Tick) Allocator.Error!void { ··· 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| { 265 - self.vx.copyToSystemClipboard(self.tty.anyWriter(), content, self.allocator) catch |err| { 264 + defer self.allocator.free(content); 265 + self.vx.copyToSystemClipboard(self.tty.writer(), content, self.allocator) catch |err| { 266 266 switch (err) { 267 267 error.OutOfMemory => return Allocator.Error.OutOfMemory, 268 268 else => std.log.err("copy error: {}", .{err}), ··· 270 270 }; 271 271 }, 272 272 .set_title => |title| { 273 - self.vx.setTitle(self.tty.anyWriter(), title) catch |err| { 273 + defer self.allocator.free(title); 274 + self.vx.setTitle(self.tty.writer(), title) catch |err| { 274 275 std.log.err("set_title error: {}", .{err}); 275 276 }; 276 277 }, 277 278 .queue_refresh => self.vx.queueRefresh(), 278 279 .notify => |notification| { 279 - self.vx.notify(self.tty.anyWriter(), notification.title, notification.body) catch |err| { 280 + self.vx.notify(self.tty.writer(), notification.title, notification.body) catch |err| { 280 281 std.log.err("notify error: {}", .{err}); 281 282 }; 282 283 const alloc = self.allocator; ··· 286 287 alloc.free(notification.body); 287 288 }, 288 289 .query_color => |kind| { 289 - self.vx.queryColor(self.tty.anyWriter(), kind) catch |err| { 290 + self.vx.queryColor(self.tty.writer(), kind) catch |err| { 290 291 std.log.err("queryColor error: {}", .{err}); 291 292 }; 292 293 }, ··· 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;
+13 -12
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 { ··· 161 167 maybe_title: ?[]const u8, 162 168 body: []const u8, 163 169 ) Allocator.Error!void { 164 - const alloc = self.cmds.allocator; 170 + const alloc = self.alloc; 165 171 if (maybe_title) |title| { 166 172 return self.addCmd(.{ .notify = .{ 167 173 .title = try alloc.dupe(u8, title), ··· 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
+8 -8
src/widgets/terminal/Command.zig
··· 36 36 37 37 // set the controlling terminal 38 38 var u: c_uint = std.posix.STDIN_FILENO; 39 - if (posix.system.ioctl(self.pty.tty, posix.T.IOCSCTTY, @intFromPtr(&u)) != 0) return error.IoctlError; 39 + if (posix.system.ioctl(self.pty.tty.handle, posix.T.IOCSCTTY, @intFromPtr(&u)) != 0) return error.IoctlError; 40 40 41 41 // set up io 42 - try posix.dup2(self.pty.tty, std.posix.STDIN_FILENO); 43 - try posix.dup2(self.pty.tty, std.posix.STDOUT_FILENO); 44 - try posix.dup2(self.pty.tty, std.posix.STDERR_FILENO); 42 + try posix.dup2(self.pty.tty.handle, std.posix.STDIN_FILENO); 43 + try posix.dup2(self.pty.tty.handle, std.posix.STDOUT_FILENO); 44 + try posix.dup2(self.pty.tty.handle, std.posix.STDERR_FILENO); 45 45 46 - posix.close(self.pty.tty); 47 - if (self.pty.pty > 2) posix.close(self.pty.pty); 46 + self.pty.tty.close(); 47 + if (self.pty.pty.handle > 2) self.pty.pty.close(); 48 48 49 49 if (self.working_directory) |wd| { 50 50 try std.posix.chdir(wd); ··· 75 75 return; 76 76 } 77 77 78 - fn handleSigChild(_: c_int) callconv(.C) void { 78 + fn handleSigChild(_: c_int) callconv(.c) void { 79 79 const result = std.posix.waitpid(-1, 0); 80 80 81 81 Terminal.global_vt_mutex.lock(); ··· 107 107 { 108 108 var it = map.iterator(); 109 109 while (it.next()) |pair| { 110 - envp_buf[i] = try std.fmt.allocPrintZ(arena, "{s}={s}", .{ pair.key_ptr.*, pair.value_ptr.* }); 110 + envp_buf[i] = try std.fmt.allocPrintSentinel(arena, "{s}={s}", .{ pair.key_ptr.*, pair.value_ptr.* }, 0); 111 111 i += 1; 112 112 } 113 113 }
+26 -28
src/widgets/terminal/Parser.zig
··· 2 2 const Parser = @This(); 3 3 4 4 const std = @import("std"); 5 - const Reader = std.io.AnyReader; 5 + const Reader = std.Io.Reader; 6 6 const ansi = @import("ansi.zig"); 7 - const BufferedReader = std.io.BufferedReader(4096, std.io.AnyReader); 8 7 9 8 /// A terminal event 10 9 const Event = union(enum) { ··· 18 17 apc: []const u8, 19 18 }; 20 19 21 - buf: std.ArrayList(u8), 20 + buf: std.array_list.Managed(u8), 22 21 /// a leftover byte from a ground event 23 22 pending_byte: ?u8 = null, 24 23 25 - pub fn parseReader(self: *Parser, buffered: *BufferedReader) !Event { 26 - const reader = buffered.reader().any(); 24 + pub fn parseReader(self: *Parser, reader: *Reader) !Event { 27 25 self.buf.clearRetainingCapacity(); 28 26 while (true) { 29 - const b = if (self.pending_byte) |p| p else try reader.readByte(); 27 + const b = if (self.pending_byte) |p| p else try reader.takeByte(); 30 28 self.pending_byte = null; 31 29 switch (b) { 32 30 // Escape sequence 33 31 0x1b => { 34 - const next = try reader.readByte(); 32 + const next = try reader.takeByte(); 35 33 switch (next) { 36 - 0x4E => return .{ .ss2 = try reader.readByte() }, 37 - 0x4F => return .{ .ss3 = try reader.readByte() }, 34 + 0x4E => return .{ .ss2 = try reader.takeByte() }, 35 + 0x4F => return .{ .ss3 = try reader.takeByte() }, 38 36 0x50 => try skipUntilST(reader), // DCS 39 37 0x58 => try skipUntilST(reader), // SOS 40 38 0x5B => return self.parseCsi(reader), // CSI ··· 58 56 => return .{ .c0 = @enumFromInt(b) }, 59 57 else => { 60 58 try self.buf.append(b); 61 - return self.parseGround(buffered); 59 + return self.parseGround(reader); 62 60 }, 63 61 } 64 62 } 65 63 } 66 64 67 - inline fn parseGround(self: *Parser, reader: *BufferedReader) !Event { 65 + inline fn parseGround(self: *Parser, reader: *Reader) !Event { 68 66 var buf: [1]u8 = undefined; 69 67 { 70 68 std.debug.assert(self.buf.items.len > 0); ··· 72 70 const len = try std.unicode.utf8ByteSequenceLength(self.buf.items[0]); 73 71 var i: usize = 1; 74 72 while (i < len) : (i += 1) { 75 - const read = try reader.read(&buf); 73 + const read = try reader.readSliceShort(&buf); 76 74 if (read == 0) return error.EOF; 77 75 try self.buf.append(buf[0]); 78 76 } 79 77 } 80 78 while (true) { 81 - if (reader.start == reader.end) return .{ .print = self.buf.items }; 82 - const n = try reader.read(&buf); 79 + if (reader.bufferedLen() == 0) return .{ .print = self.buf.items }; 80 + const n = try reader.readSliceShort(&buf); 83 81 if (n == 0) return error.EOF; 84 82 const b = buf[0]; 85 83 switch (b) { ··· 92 90 const len = try std.unicode.utf8ByteSequenceLength(b); 93 91 var i: usize = 1; 94 92 while (i < len) : (i += 1) { 95 - const read = try reader.read(&buf); 93 + const read = try reader.readSliceShort(&buf); 96 94 if (read == 0) return error.EOF; 97 95 98 96 try self.buf.append(buf[0]); ··· 103 101 } 104 102 105 103 /// parse until b >= 0x30 106 - inline fn parseEscape(self: *Parser, reader: Reader) !Event { 104 + inline fn parseEscape(self: *Parser, reader: *Reader) !Event { 107 105 while (true) { 108 - const b = try reader.readByte(); 106 + const b = try reader.takeByte(); 109 107 switch (b) { 110 108 0x20...0x2F => continue, 111 109 else => { ··· 116 114 } 117 115 } 118 116 119 - inline fn parseApc(self: *Parser, reader: Reader) !Event { 117 + inline fn parseApc(self: *Parser, reader: *Reader) !Event { 120 118 while (true) { 121 - const b = try reader.readByte(); 119 + const b = try reader.takeByte(); 122 120 switch (b) { 123 121 0x00...0x17, 124 122 0x19, 125 123 0x1c...0x1f, 126 124 => continue, 127 125 0x1b => { 128 - try reader.skipBytes(1, .{ .buf_size = 1 }); 126 + _ = try reader.discard(std.Io.Limit.limited(1)); 129 127 return .{ .apc = self.buf.items }; 130 128 }, 131 129 else => try self.buf.append(b), ··· 134 132 } 135 133 136 134 /// Skips sequences until we see an ST (String Terminator, ESC \) 137 - inline fn skipUntilST(reader: Reader) !void { 138 - try reader.skipUntilDelimiterOrEof('\x1b'); 139 - try reader.skipBytes(1, .{ .buf_size = 1 }); 135 + inline fn skipUntilST(reader: *Reader) !void { 136 + _ = try reader.discardDelimiterExclusive('\x1b'); 137 + _ = try reader.discard(std.Io.Limit.limited(1)); 140 138 } 141 139 142 140 /// Parses an OSC sequence 143 - inline fn parseOsc(self: *Parser, reader: Reader) !Event { 141 + inline fn parseOsc(self: *Parser, reader: *Reader) !Event { 144 142 while (true) { 145 - const b = try reader.readByte(); 143 + const b = try reader.takeByte(); 146 144 switch (b) { 147 145 0x00...0x06, 148 146 0x08...0x17, ··· 150 148 0x1c...0x1f, 151 149 => continue, 152 150 0x1b => { 153 - try reader.skipBytes(1, .{ .buf_size = 1 }); 151 + _ = try reader.discard(std.Io.Limit.limited(1)); 154 152 return .{ .osc = self.buf.items }; 155 153 }, 156 154 0x07 => return .{ .osc = self.buf.items }, ··· 159 157 } 160 158 } 161 159 162 - inline fn parseCsi(self: *Parser, reader: Reader) !Event { 160 + inline fn parseCsi(self: *Parser, reader: *Reader) !Event { 163 161 var intermediate: ?u8 = null; 164 162 var pm: ?u8 = null; 165 163 166 164 while (true) { 167 - const b = try reader.readByte(); 165 + const b = try reader.takeByte(); 168 166 switch (b) { 169 167 0x20...0x2F => intermediate = b, 170 168 0x30...0x3B => try self.buf.append(b),
+7 -7
src/widgets/terminal/Pty.zig
··· 7 7 8 8 const posix = std.posix; 9 9 10 - pty: posix.fd_t, 11 - tty: posix.fd_t, 10 + pty: std.fs.File, 11 + tty: std.fs.File, 12 12 13 13 /// opens a new tty/pty pair 14 14 pub fn init() !Pty { ··· 20 20 21 21 /// closes the tty and pty 22 22 pub fn deinit(self: Pty) void { 23 - posix.close(self.pty); 24 - posix.close(self.tty); 23 + self.pty.close(); 24 + self.tty.close(); 25 25 } 26 26 27 27 /// sets the size of the pty ··· 32 32 .xpixel = @truncate(ws.x_pixel), 33 33 .ypixel = @truncate(ws.y_pixel), 34 34 }; 35 - if (posix.system.ioctl(self.pty, posix.T.IOCSWINSZ, @intFromPtr(&_ws)) != 0) 35 + if (posix.system.ioctl(self.pty.handle, posix.T.IOCSWINSZ, @intFromPtr(&_ws)) != 0) 36 36 return error.SetWinsizeError; 37 37 } 38 38 ··· 53 53 const t = try posix.open(sname, .{ .ACCMODE = .RDWR, .NOCTTY = true }, 0); 54 54 55 55 return .{ 56 - .pty = p, 57 - .tty = t, 56 + .pty = .{ .handle = p }, 57 + .tty = .{ .handle = t }, 58 58 }; 59 59 }
+34 -33
src/widgets/terminal/Screen.zig
··· 9 9 const Screen = @This(); 10 10 11 11 pub const Cell = struct { 12 - char: std.ArrayList(u8) = undefined, 12 + char: std.ArrayList(u8) = .empty, 13 13 style: vaxis.Style = .{}, 14 - uri: std.ArrayList(u8) = undefined, 15 - uri_id: std.ArrayList(u8) = undefined, 14 + uri: std.ArrayList(u8) = .empty, 15 + uri_id: std.ArrayList(u8) = .empty, 16 16 width: u8 = 1, 17 17 18 18 wrapped: bool = false, 19 19 dirty: bool = true, 20 20 21 - pub fn erase(self: *Cell, bg: vaxis.Color) void { 21 + pub fn erase(self: *Cell, allocator: std.mem.Allocator, bg: vaxis.Color) void { 22 22 self.char.clearRetainingCapacity(); 23 - self.char.append(' ') catch unreachable; // we never completely free this list 23 + self.char.append(allocator, ' ') catch unreachable; // we never completely free this list 24 24 self.style = .{}; 25 25 self.style.bg = bg; 26 26 self.uri.clearRetainingCapacity(); ··· 30 30 self.dirty = true; 31 31 } 32 32 33 - pub fn copyFrom(self: *Cell, src: Cell) !void { 33 + pub fn copyFrom(self: *Cell, allocator: std.mem.Allocator, src: Cell) !void { 34 34 self.char.clearRetainingCapacity(); 35 - try self.char.appendSlice(src.char.items); 35 + try self.char.appendSlice(allocator, src.char.items); 36 36 self.style = src.style; 37 37 self.uri.clearRetainingCapacity(); 38 - try self.uri.appendSlice(src.uri.items); 38 + try self.uri.appendSlice(allocator, src.uri.items); 39 39 self.uri_id.clearRetainingCapacity(); 40 - try self.uri_id.appendSlice(src.uri_id.items); 40 + try self.uri_id.appendSlice(allocator, src.uri_id.items); 41 41 self.width = src.width; 42 42 self.wrapped = src.wrapped; 43 43 ··· 81 81 } 82 82 }; 83 83 84 + allocator: std.mem.Allocator, 85 + 84 86 width: u16 = 0, 85 87 height: u16 = 0, 86 88 ··· 95 97 /// sets each cell to the default cell 96 98 pub fn init(alloc: std.mem.Allocator, w: u16, h: u16) !Screen { 97 99 var screen = Screen{ 100 + .allocator = alloc, 98 101 .buf = try alloc.alloc(Cell, @as(usize, @intCast(w)) * h), 99 102 .scrolling_region = .{ 100 103 .top = 0, ··· 107 110 }; 108 111 for (screen.buf, 0..) |_, i| { 109 112 screen.buf[i] = .{ 110 - .char = try std.ArrayList(u8).initCapacity(alloc, 1), 111 - .uri = std.ArrayList(u8).init(alloc), 112 - .uri_id = std.ArrayList(u8).init(alloc), 113 + .char = try .initCapacity(alloc, 1), 113 114 }; 114 - try screen.buf[i].char.append(' '); 115 + try screen.buf[i].char.append(alloc, ' '); 115 116 } 116 117 return screen; 117 118 } 118 119 119 120 pub fn deinit(self: *Screen, alloc: std.mem.Allocator) void { 120 121 for (self.buf, 0..) |_, i| { 121 - self.buf[i].char.deinit(); 122 - self.buf[i].uri.deinit(); 123 - self.buf[i].uri_id.deinit(); 122 + self.buf[i].char.deinit(alloc); 123 + self.buf[i].uri.deinit(alloc); 124 + self.buf[i].uri_id.deinit(alloc); 124 125 } 125 126 126 127 alloc.free(self.buf); 127 128 } 128 129 129 130 /// copies the visible area to the destination screen 130 - pub fn copyTo(self: *Screen, dst: *Screen) !void { 131 + pub fn copyTo(self: *Screen, allocator: std.mem.Allocator, dst: *Screen) !void { 131 132 dst.cursor = self.cursor; 132 133 for (self.buf, 0..) |cell, i| { 133 134 if (!cell.dirty) continue; 134 135 self.buf[i].dirty = false; 135 136 const grapheme = cell.char.items; 136 137 dst.buf[i].char.clearRetainingCapacity(); 137 - try dst.buf[i].char.appendSlice(grapheme); 138 + try dst.buf[i].char.appendSlice(allocator, grapheme); 138 139 dst.buf[i].width = cell.width; 139 140 dst.buf[i].style = cell.style; 140 141 } ··· 182 183 const i = (row * self.width) + col; 183 184 assert(i < self.buf.len); 184 185 self.buf[i].char.clearRetainingCapacity(); 185 - self.buf[i].char.appendSlice(grapheme) catch { 186 + self.buf[i].char.appendSlice(self.allocator, grapheme) catch { 186 187 log.warn("couldn't write grapheme", .{}); 187 188 }; 188 189 self.buf[i].uri.clearRetainingCapacity(); 189 - self.buf[i].uri.appendSlice(self.cursor.uri.items) catch { 190 + self.buf[i].uri.appendSlice(self.allocator, self.cursor.uri.items) catch { 190 191 log.warn("couldn't write uri", .{}); 191 192 }; 192 193 self.buf[i].uri_id.clearRetainingCapacity(); 193 - self.buf[i].uri_id.appendSlice(self.cursor.uri_id.items) catch { 194 + self.buf[i].uri_id.appendSlice(self.allocator, self.cursor.uri_id.items) catch { 194 195 log.warn("couldn't write uri_id", .{}); 195 196 }; 196 197 self.buf[i].style = self.cursor.style; ··· 368 369 const end = (self.cursor.row * self.width) + (self.width); 369 370 var i = (self.cursor.row * self.width) + self.cursor.col; 370 371 while (i < end) : (i += 1) { 371 - self.buf[i].erase(self.cursor.style.bg); 372 + self.buf[i].erase(self.allocator, self.cursor.style.bg); 372 373 } 373 374 } 374 375 ··· 378 379 const end = start + self.cursor.col + 1; 379 380 var i = start; 380 381 while (i < end) : (i += 1) { 381 - self.buf[i].erase(self.cursor.style.bg); 382 + self.buf[i].erase(self.allocator, self.cursor.style.bg); 382 383 } 383 384 } 384 385 ··· 388 389 const end = start + self.width; 389 390 var i = start; 390 391 while (i < end) : (i += 1) { 391 - self.buf[i].erase(self.cursor.style.bg); 392 + self.buf[i].erase(self.allocator, self.cursor.style.bg); 392 393 } 393 394 } 394 395 ··· 411 412 while (col <= self.scrolling_region.right) : (col += 1) { 412 413 const i = (row * self.width) + col; 413 414 if (row + cnt > self.scrolling_region.bottom) 414 - self.buf[i].erase(self.cursor.style.bg) 415 + self.buf[i].erase(self.allocator, self.cursor.style.bg) 415 416 else 416 - try self.buf[i].copyFrom(self.buf[i + stride]); 417 + try self.buf[i].copyFrom(self.allocator, self.buf[i + stride]); 417 418 } 418 419 } 419 420 } ··· 434 435 var col: usize = self.scrolling_region.left; 435 436 while (col <= self.scrolling_region.right) : (col += 1) { 436 437 const i = (row * self.width) + col; 437 - try self.buf[i].copyFrom(self.buf[i - stride]); 438 + try self.buf[i].copyFrom(self.allocator, self.buf[i - stride]); 438 439 } 439 440 } 440 441 ··· 443 444 var col: usize = self.scrolling_region.left; 444 445 while (col <= self.scrolling_region.right) : (col += 1) { 445 446 const i = (row * self.width) + col; 446 - self.buf[i].erase(self.cursor.style.bg); 447 + self.buf[i].erase(self.allocator, self.cursor.style.bg); 447 448 } 448 449 } 449 450 } ··· 454 455 const start = (self.cursor.row * self.width) + (self.width); 455 456 var i = start; 456 457 while (i < self.buf.len) : (i += 1) { 457 - self.buf[i].erase(self.cursor.style.bg); 458 + self.buf[i].erase(self.allocator, self.cursor.style.bg); 458 459 } 459 460 } 460 461 ··· 465 466 const end = self.cursor.row * self.width; 466 467 var i = start; 467 468 while (i < end) : (i += 1) { 468 - self.buf[i].erase(self.cursor.style.bg); 469 + self.buf[i].erase(self.allocator, self.cursor.style.bg); 469 470 } 470 471 } 471 472 472 473 pub fn eraseAll(self: *Screen) void { 473 474 var i: usize = 0; 474 475 while (i < self.buf.len) : (i += 1) { 475 - self.buf[i].erase(self.cursor.style.bg); 476 + self.buf[i].erase(self.allocator, self.cursor.style.bg); 476 477 } 477 478 } 478 479 ··· 483 484 var col = self.cursor.col; 484 485 while (col <= self.scrolling_region.right) : (col += 1) { 485 486 if (col + n <= self.scrolling_region.right) 486 - try self.buf[col].copyFrom(self.buf[col + n]) 487 + try self.buf[col].copyFrom(self.allocator, self.buf[col + n]) 487 488 else 488 - self.buf[col].erase(self.cursor.style.bg); 489 + self.buf[col].erase(self.allocator, self.cursor.style.bg); 489 490 } 490 491 } 491 492
+64 -73
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) { ··· 53 51 scrollback_size: u16, 54 52 55 53 pty: Pty, 54 + pty_writer: std.fs.File.Writer, 56 55 cmd: Command, 57 56 thread: ?std.Thread = null, 58 57 ··· 70 69 // dirty is protected by back_mutex. Only access this field when you hold that mutex 71 70 dirty: bool = false, 72 71 73 - unicode: *const vaxis.Unicode, 74 72 should_quit: bool = false, 75 73 76 74 mode: Mode = .{}, 77 75 78 76 tab_stops: std.ArrayList(u16), 79 - title: std.ArrayList(u8), 80 - working_directory: std.ArrayList(u8), 77 + title: std.ArrayList(u8) = .empty, 78 + working_directory: std.ArrayList(u8) = .empty, 81 79 82 80 last_printed: []const u8 = "", 83 81 ··· 89 87 allocator: std.mem.Allocator, 90 88 argv: []const []const u8, 91 89 env: *const std.process.EnvMap, 92 - unicode: *const vaxis.Unicode, 93 90 opts: Options, 91 + write_buf: []u8, 94 92 ) !Terminal { 95 93 // Verify we have an absolute path 96 94 if (opts.initial_working_directory) |pwd| { ··· 104 102 .pty = pty, 105 103 .working_directory = opts.initial_working_directory, 106 104 }; 107 - var tabs = try std.ArrayList(u16).initCapacity(allocator, opts.winsize.cols / 8); 105 + var tabs: std.ArrayList(u16) = try .initCapacity(allocator, opts.winsize.cols / 8); 108 106 var col: u16 = 0; 109 107 while (col < opts.winsize.cols) : (col += 8) { 110 - try tabs.append(col); 108 + try tabs.append(allocator, col); 111 109 } 112 110 return .{ 113 111 .allocator = allocator, 114 112 .pty = pty, 113 + .pty_writer = pty.pty.writerStreaming(write_buf), 115 114 .cmd = cmd, 116 115 .scrollback_size = opts.scrollback_size, 117 116 .front_screen = try Screen.init(allocator, opts.winsize.cols, opts.winsize.rows), 118 117 .back_screen_pri = try Screen.init(allocator, opts.winsize.cols, opts.winsize.rows + opts.scrollback_size), 119 118 .back_screen_alt = try Screen.init(allocator, opts.winsize.cols, opts.winsize.rows), 120 - .unicode = unicode, 121 119 .tab_stops = tabs, 122 - .title = std.ArrayList(u8).init(allocator), 123 - .working_directory = std.ArrayList(u8).init(allocator), 124 120 }; 125 121 } 126 122 ··· 143 139 if (self.thread) |thread| { 144 140 // write an EOT into the tty to trigger a read on our thread 145 141 const EOT = "\x04"; 146 - _ = std.posix.write(self.pty.tty, EOT) catch {}; 142 + _ = self.pty.tty.write(EOT) catch {}; 147 143 thread.join(); 148 144 self.thread = null; 149 145 } ··· 151 147 self.front_screen.deinit(self.allocator); 152 148 self.back_screen_pri.deinit(self.allocator); 153 149 self.back_screen_alt.deinit(self.allocator); 154 - self.tab_stops.deinit(); 155 - self.title.deinit(); 156 - self.working_directory.deinit(); 150 + self.tab_stops.deinit(self.allocator); 151 + self.title.deinit(self.allocator); 152 + self.working_directory.deinit(self.allocator); 157 153 } 158 154 159 155 pub fn spawn(self: *Terminal) !void { ··· 164 160 165 161 self.working_directory.clearRetainingCapacity(); 166 162 if (self.cmd.working_directory) |pwd| { 167 - try self.working_directory.appendSlice(pwd); 163 + try self.working_directory.appendSlice(self.allocator, pwd); 168 164 } else { 169 165 const pwd = std.fs.cwd(); 170 166 var buffer: [std.fs.max_path_bytes]u8 = undefined; 171 167 const out_path = try std.os.getFdPath(pwd.fd, &buffer); 172 - try self.working_directory.appendSlice(out_path); 168 + try self.working_directory.appendSlice(self.allocator, out_path); 173 169 } 174 170 175 171 { ··· 208 204 try self.pty.setSize(ws); 209 205 } 210 206 211 - pub fn draw(self: *Terminal, win: vaxis.Window) !void { 207 + pub fn draw(self: *Terminal, allocator: std.mem.Allocator, win: vaxis.Window) !void { 212 208 if (self.back_mutex.tryLock()) { 213 209 defer self.back_mutex.unlock(); 214 210 // We keep this as a separate condition so we don't deadlock by obtaining the lock but not 215 211 // having sync 216 212 if (!self.mode.sync) { 217 - try self.back_screen.copyTo(&self.front_screen); 213 + try self.back_screen.copyTo(allocator, &self.front_screen); 218 214 self.dirty = false; 219 215 } 220 216 } ··· 241 237 242 238 pub fn update(self: *Terminal, event: InputEvent) !void { 243 239 switch (event) { 244 - .key_press => |k| try key.encode(self.anyWriter(), k, true, self.back_screen.csi_u_flags), 240 + .key_press => |k| { 241 + const pty_writer = self.get_pty_writer(); 242 + defer pty_writer.flush() catch {}; 243 + try key.encode(pty_writer, k, true, self.back_screen.csi_u_flags); 244 + }, 245 245 } 246 246 } 247 247 248 - fn opaqueWrite(ptr: *const anyopaque, buf: []const u8) !usize { 249 - const self: *const Terminal = @ptrCast(@alignCast(ptr)); 250 - return posix.write(self.pty.pty, buf); 251 - } 252 - 253 - pub fn anyWriter(self: *const Terminal) *std.io.Writer { 254 - const writer: std.io.Writer = .{ 255 - .context = self, 256 - .writeFn = Terminal.opaqueWrite, 257 - }; 258 - return @constCast(&writer); 259 - } 260 - 261 - fn opaqueRead(ptr: *const anyopaque, buf: []u8) !usize { 262 - const self: *const Terminal = @ptrCast(@alignCast(ptr)); 263 - return posix.read(self.pty.pty, buf); 248 + pub fn get_pty_writer(self: *Terminal) *std.Io.Writer { 249 + return &self.pty_writer.interface; 264 250 } 265 251 266 - fn anyReader(self: *const Terminal) std.io.AnyReader { 267 - return .{ 268 - .context = self, 269 - .readFn = Terminal.opaqueRead, 270 - }; 252 + fn reader(self: *const Terminal, buf: []u8) std.fs.File.Reader { 253 + return self.pty.pty.readerStreaming(buf); 271 254 } 272 255 273 256 /// process the output from the command on the pty 274 257 fn run(self: *Terminal) !void { 275 258 var parser: Parser = .{ 276 - .buf = try std.ArrayList(u8).initCapacity(self.allocator, 128), 259 + .buf = try .initCapacity(self.allocator, 128), 277 260 }; 278 261 defer parser.buf.deinit(); 279 262 280 - // Use our anyReader to make a buffered reader, then get *that* any reader 281 - var reader = std.io.bufferedReader(self.anyReader()); 263 + var reader_buf: [4096]u8 = undefined; 264 + var reader_ = self.reader(&reader_buf); 282 265 283 266 while (!self.should_quit) { 284 - const event = try parser.parseReader(&reader); 267 + const event = try parser.parseReader(&reader_.interface); 285 268 self.back_mutex.lock(); 286 269 defer self.back_mutex.unlock(); 287 270 ··· 290 273 291 274 switch (event) { 292 275 .print => |str| { 293 - var iter = self.unicode.graphemeIterator(str); 294 - while (iter.next()) |g| { 295 - const gr = g.bytes(str); 276 + var iter = vaxis.unicode.graphemeIterator(str); 277 + while (iter.next()) |grapheme| { 278 + const gr = grapheme.bytes(str); 296 279 // TODO: use actual instead of .unicode 297 - const w = vaxis.gwidth.gwidth(gr, .unicode, &self.unicode.width_data); 280 + const w = vaxis.gwidth.gwidth(gr, .unicode); 298 281 try self.back_screen.print(gr, @truncate(w), self.mode.autowrap); 299 282 } 300 283 }, ··· 316 299 if (ts == self.back_screen.cursor.col) break true; 317 300 } else false; 318 301 if (already_set) continue; 319 - try self.tab_stops.append(@truncate(self.back_screen.cursor.col)); 302 + try self.tab_stops.append(self.allocator, @truncate(self.back_screen.cursor.col)); 320 303 std.mem.sort(u16, self.tab_stops.items, {}, std.sort.asc(u16)); 321 304 }, 322 305 // Reverse Index ··· 467 450 self.tab_stops.clearRetainingCapacity(); 468 451 var col: u16 = 0; 469 452 while (col < self.back_screen.width) : (col += 8) { 470 - try self.tab_stops.append(col); 453 + try self.tab_stops.append(self.allocator, col); 471 454 } 472 455 } 473 456 }, ··· 483 466 ); 484 467 var i: usize = start; 485 468 while (i < end) : (i += 1) { 486 - self.back_screen.buf[i].erase(self.back_screen.cursor.style.bg); 469 + self.back_screen.buf[i].erase(self.allocator, self.back_screen.cursor.style.bg); 487 470 } 488 471 }, 489 472 'Z' => { ··· 510 493 var iter = seq.iterator(u16); 511 494 const n = iter.next() orelse 1; 512 495 // TODO: maybe not .unicode 513 - const w = vaxis.gwidth.gwidth(self.last_printed, .unicode, &self.unicode.width_data); 496 + const w = vaxis.gwidth.gwidth(self.last_printed, .unicode); 514 497 var i: usize = 0; 515 498 while (i < n) : (i += 1) { 516 499 try self.back_screen.print(self.last_printed, @truncate(w), self.mode.autowrap); ··· 518 501 }, 519 502 // Device Attributes 520 503 'c' => { 504 + const pty_writer = self.get_pty_writer(); 505 + defer pty_writer.flush() catch {}; 521 506 if (seq.private_marker) |pm| { 522 507 switch (pm) { 523 508 // Secondary 524 - '>' => try self.anyWriter().writeAll("\x1B[>1;69;0c"), 525 - '=' => try self.anyWriter().writeAll("\x1B[=0000c"), 526 - else => log.info("unhandled CSI: {}", .{seq}), 509 + '>' => try pty_writer.writeAll("\x1B[>1;69;0c"), 510 + '=' => try pty_writer.writeAll("\x1B[=0000c"), 511 + else => log.info("unhandled CSI: {f}", .{seq}), 527 512 } 528 513 } else { 529 514 // Primary 530 - try self.anyWriter().writeAll("\x1B[?62;22c"); 515 + try pty_writer.writeAll("\x1B[?62;22c"); 531 516 } 532 517 }, 533 518 // Cursor Vertical Position Absolute ··· 561 546 const n = iter.next() orelse 0; 562 547 switch (n) { 563 548 0 => { 564 - const current = try self.tab_stops.toOwnedSlice(); 565 - defer self.tab_stops.allocator.free(current); 549 + const current = try self.tab_stops.toOwnedSlice(self.allocator); 550 + defer self.allocator.free(current); 566 551 self.tab_stops.clearRetainingCapacity(); 567 552 for (current) |stop| { 568 553 if (stop == self.back_screen.cursor.col) continue; 569 - try self.tab_stops.append(stop); 554 + try self.tab_stops.append(self.allocator, stop); 570 555 } 571 556 }, 572 - 3 => self.tab_stops.clearAndFree(), 573 - else => log.info("unhandled CSI: {}", .{seq}), 557 + 3 => self.tab_stops.clearAndFree(self.allocator), 558 + else => log.info("unhandled CSI: {f}", .{seq}), 574 559 } 575 560 }, 576 561 'h', 'l' => { ··· 591 576 var iter = seq.iterator(u16); 592 577 const ps = iter.next() orelse 0; 593 578 if (seq.intermediate == null and seq.private_marker == null) { 579 + const pty_writer = self.get_pty_writer(); 580 + defer pty_writer.flush() catch {}; 594 581 switch (ps) { 595 - 5 => try self.anyWriter().writeAll("\x1b[0n"), 596 - 6 => try self.anyWriter().print("\x1b[{d};{d}R", .{ 582 + 5 => try pty_writer.writeAll("\x1b[0n"), 583 + 6 => try pty_writer.print("\x1b[{d};{d}R", .{ 597 584 self.back_screen.cursor.row + 1, 598 585 self.back_screen.cursor.col + 1, 599 586 }), 600 - else => log.info("unhandled CSI: {}", .{seq}), 587 + else => log.info("unhandled CSI: {f}", .{seq}), 601 588 } 602 589 } 603 590 }, ··· 608 595 switch (int) { 609 596 // report mode 610 597 '$' => { 598 + const pty_writer = self.get_pty_writer(); 599 + defer pty_writer.flush() catch {}; 611 600 switch (ps) { 612 - 2026 => try self.anyWriter().writeAll("\x1b[?2026;2$p"), 601 + 2026 => try pty_writer.writeAll("\x1b[?2026;2$p"), 613 602 else => { 614 603 std.log.warn("unhandled mode: {}", .{ps}); 615 - try self.anyWriter().print("\x1b[?{d};0$p", .{ps}); 604 + try pty_writer.print("\x1b[?{d};0$p", .{ps}); 616 605 }, 617 606 } 618 607 }, 619 - else => log.info("unhandled CSI: {}", .{seq}), 608 + else => log.info("unhandled CSI: {f}", .{seq}), 620 609 } 621 610 } 622 611 }, ··· 632 621 } 633 622 } 634 623 if (seq.private_marker) |pm| { 624 + const pty_writer = self.get_pty_writer(); 625 + defer pty_writer.flush() catch {}; 635 626 switch (pm) { 636 627 // XTVERSION 637 - '>' => try self.anyWriter().print( 628 + '>' => try pty_writer.print( 638 629 "\x1bP>|libvaxis {s}\x1B\\", 639 630 .{"dev"}, 640 631 ), 641 - else => log.info("unhandled CSI: {}", .{seq}), 632 + else => log.info("unhandled CSI: {f}", .{seq}), 642 633 } 643 634 } 644 635 }, ··· 666 657 self.back_screen.cursor.row = 0; 667 658 } 668 659 }, 669 - else => log.info("unhandled CSI: {}", .{seq}), 660 + else => log.info("unhandled CSI: {f}", .{seq}), 670 661 } 671 662 }, 672 663 .osc => |osc| { ··· 681 672 switch (ps) { 682 673 0 => { 683 674 self.title.clearRetainingCapacity(); 684 - try self.title.appendSlice(osc[semicolon + 1 ..]); 675 + try self.title.appendSlice(self.allocator, osc[semicolon + 1 ..]); 685 676 self.event_queue.push(.{ .title_change = self.title.items }); 686 677 }, 687 678 7 => { ··· 700 691 defer i += 2; 701 692 break :blk try std.fmt.parseUnsigned(u8, enc[i + 1 .. i + 3], 16); 702 693 } else enc[i]; 703 - try self.working_directory.append(b); 694 + try self.working_directory.append(self.allocator, b); 704 695 } 705 696 self.event_queue.push(.{ .pwd_change = self.working_directory.items }); 706 697 },
+5 -12
src/widgets/terminal/ansi.zig
··· 55 55 return .{ .bytes = self.params }; 56 56 } 57 57 58 - pub fn format( 59 - self: CSI, 60 - comptime layout: []const u8, 61 - opts: std.fmt.FormatOptions, 62 - writer: anytype, 63 - ) !void { 64 - _ = layout; 65 - _ = opts; 58 + pub fn format(self: CSI, writer: anytype) !void { 66 59 if (self.private_marker == null and self.intermediate == null) 67 - try std.fmt.format(writer, "CSI {s} {c}", .{ 60 + try writer.print("CSI {s} {c}", .{ 68 61 self.params, 69 62 self.final, 70 63 }) 71 64 else if (self.private_marker != null and self.intermediate == null) 72 - try std.fmt.format(writer, "CSI {c} {s} {c}", .{ 65 + try writer.print("CSI {c} {s} {c}", .{ 73 66 self.private_marker.?, 74 67 self.params, 75 68 self.final, 76 69 }) 77 70 else if (self.private_marker == null and self.intermediate != null) 78 - try std.fmt.format(writer, "CSI {s} {c} {c}", .{ 71 + try writer.print("CSI {s} {c} {c}", .{ 79 72 self.params, 80 73 self.intermediate.?, 81 74 self.final, 82 75 }) 83 76 else 84 - try std.fmt.format(writer, "CSI {c} {s} {c} {c}", .{ 77 + try writer.print("CSI {c} {s} {c} {c}", .{ 85 78 self.private_marker.?, 86 79 self.params, 87 80 self.intermediate.?,
+2 -2
src/widgets/terminal/key.zig
··· 2 2 const vaxis = @import("../../main.zig"); 3 3 4 4 pub fn encode( 5 - writer: std.io.AnyWriter, 5 + writer: *std.Io.Writer, 6 6 key: vaxis.Key, 7 7 press: bool, 8 8 kitty_flags: vaxis.Key.KittyFlags, ··· 19 19 } 20 20 } 21 21 22 - fn legacy(writer: std.io.AnyWriter, key: vaxis.Key) !void { 22 + fn legacy(writer: *std.Io.Writer, key: vaxis.Key) !void { 23 23 // If we have text, we always write it directly 24 24 if (key.text) |text| { 25 25 try writer.writeAll(text);