a modern tui library written in zig

Compare changes

Choose any two refs to compare.

+1 -1
README.md
··· 325 325 326 326 // init our text input widget. The text input widget needs an allocator to 327 327 // store the contents of the input 328 - var text_input = TextInput.init(alloc, &vx.unicode); 328 + var text_input = TextInput.init(alloc); 329 329 defer text_input.deinit(); 330 330 331 331 // Sends queries to terminal to detect certain features. This should always
+62
bench/bench.zig
··· 1 + const std = @import("std"); 2 + const vaxis = @import("vaxis"); 3 + 4 + fn parseIterations(allocator: std.mem.Allocator) !usize { 5 + var args = try std.process.argsWithAllocator(allocator); 6 + defer args.deinit(); 7 + _ = args.next(); 8 + if (args.next()) |val| { 9 + return std.fmt.parseUnsigned(usize, val, 10); 10 + } 11 + return 200; 12 + } 13 + 14 + fn printResults(writer: anytype, label: []const u8, iterations: usize, elapsed_ns: u64, total_bytes: usize) !void { 15 + const ns_per_frame = elapsed_ns / @as(u64, @intCast(iterations)); 16 + const bytes_per_frame = total_bytes / iterations; 17 + try writer.print( 18 + "{s}: frames={d} total_ns={d} ns/frame={d} bytes={d} bytes/frame={d}\n", 19 + .{ label, iterations, elapsed_ns, ns_per_frame, total_bytes, bytes_per_frame }, 20 + ); 21 + } 22 + 23 + pub fn main() !void { 24 + var gpa = std.heap.GeneralPurposeAllocator(.{}){}; 25 + defer _ = gpa.deinit(); 26 + const allocator = gpa.allocator(); 27 + 28 + const iterations = try parseIterations(allocator); 29 + 30 + var vx = try vaxis.init(allocator, .{}); 31 + var init_writer = std.io.Writer.Allocating.init(allocator); 32 + defer init_writer.deinit(); 33 + defer vx.deinit(allocator, &init_writer.writer); 34 + 35 + const winsize = vaxis.Winsize{ .rows = 24, .cols = 80, .x_pixel = 0, .y_pixel = 0 }; 36 + try vx.resize(allocator, &init_writer.writer, winsize); 37 + 38 + const stdout = std.fs.File.stdout().deprecatedWriter(); 39 + 40 + var idle_writer = std.io.Writer.Allocating.init(allocator); 41 + defer idle_writer.deinit(); 42 + var timer = try std.time.Timer.start(); 43 + var i: usize = 0; 44 + while (i < iterations) : (i += 1) { 45 + try vx.render(&idle_writer.writer); 46 + } 47 + const idle_ns = timer.read(); 48 + const idle_bytes: usize = idle_writer.writer.end; 49 + try printResults(stdout, "idle", iterations, idle_ns, idle_bytes); 50 + 51 + var dirty_writer = std.io.Writer.Allocating.init(allocator); 52 + defer dirty_writer.deinit(); 53 + timer.reset(); 54 + i = 0; 55 + while (i < iterations) : (i += 1) { 56 + vx.queueRefresh(); 57 + try vx.render(&dirty_writer.writer); 58 + } 59 + const dirty_ns = timer.read(); 60 + const dirty_bytes: usize = dirty_writer.writer.end; 61 + try printResults(stdout, "dirty", iterations, dirty_ns, dirty_bytes); 62 + }
+21
build.zig
··· 41 41 split_view, 42 42 table, 43 43 text_input, 44 + text_view, 45 + list_view, 44 46 vaxis, 45 47 view, 46 48 vt, ··· 63 65 64 66 const example_run = b.addRunArtifact(example); 65 67 example_step.dependOn(&example_run.step); 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); 66 87 67 88 // Tests 68 89 const tests_step = b.step("test", "Run tests");
+2 -3
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 11 .uucode = .{ 13 12 .url = "git+https://github.com/jacobsandlund/uucode#5f05f8f83a75caea201f12cc8ea32a2d82ea9732",
+1 -1
examples/cli.zig
··· 29 29 30 30 try vx.queryTerminal(tty.writer(), 1 * std.time.ns_per_s); 31 31 32 - var text_input = TextInput.init(alloc, &vx.unicode); 32 + var text_input = TextInput.init(alloc); 33 33 defer text_input.deinit(); 34 34 35 35 var selected_option: ?usize = null;
+69 -56
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 16 12 - /// Used for filtered RichText Spans 13 - arena: std.heap.ArenaAllocator, 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 + } 44 + 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 + } 14 51 15 52 pub fn widget(self: *Model) vxfw.Widget { 16 53 return .{ ··· 25 62 switch (event) { 26 63 .init => { 27 64 // Initialize the filtered list 28 - const allocator = self.arena.allocator(); 65 + const arena = self.arena.allocator(); 29 66 for (self.list.items) |line| { 30 - var spans = std.ArrayList(vxfw.RichText.TextSpan){}; 67 + var spans = std.ArrayList(vxfw.RichText.TextSpan).empty; 31 68 const span: vxfw.RichText.TextSpan = .{ .text = line.text }; 32 - try spans.append(allocator, span); 33 - try self.filtered.append(allocator, .{ .text = spans.items }); 69 + try spans.append(arena, span); 70 + try self.filtered.append(arena, .{ .text = spans.items }); 34 71 } 35 72 36 73 return ctx.requestFocus(self.text_field.widget()); ··· 99 136 fn onChange(maybe_ptr: ?*anyopaque, _: *vxfw.EventContext, str: []const u8) anyerror!void { 100 137 const ptr = maybe_ptr orelse return; 101 138 const self: *Model = @ptrCast(@alignCast(ptr)); 102 - const allocator = self.arena.allocator(); 103 - self.filtered.clearAndFree(allocator); 139 + const arena = self.arena.allocator(); 140 + self.filtered.clearAndFree(arena); 104 141 _ = self.arena.reset(.free_all); 105 142 106 143 const hasUpper = for (str) |b| { ··· 114 151 const tgt = if (hasUpper) 115 152 item.text 116 153 else 117 - try toLower(allocator, item.text); 154 + try toLower(arena, item.text); 118 155 119 - var spans = std.ArrayList(vxfw.RichText.TextSpan){}; 156 + var spans = std.ArrayList(vxfw.RichText.TextSpan).empty; 120 157 var i: usize = 0; 121 158 var iter = vaxis.unicode.graphemeIterator(str); 122 159 while (iter.next()) |g| { ··· 126 163 .text = item.text[idx .. idx + g.len], 127 164 .style = .{ .fg = .{ .index = 4 }, .reverse = true }, 128 165 }; 129 - try spans.append(allocator, up_to_here); 130 - try spans.append(allocator, match); 166 + try spans.append(arena, up_to_here); 167 + try spans.append(arena, match); 131 168 i = idx + g.len; 132 169 } else continue :outer; 133 170 } 134 171 const up_to_here: vxfw.RichText.TextSpan = .{ .text = item.text[i..] }; 135 - try spans.append(allocator, up_to_here); 136 - 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 }); 137 174 } 138 175 self.list_view.scroll.top = 0; 139 176 self.list_view.scroll.offset = 0; ··· 145 182 const self: *Model = @ptrCast(@alignCast(ptr)); 146 183 if (self.list_view.cursor < self.filtered.items.len) { 147 184 const selected = self.filtered.items[self.list_view.cursor]; 148 - const allocator = self.arena.allocator(); 149 - var result = std.ArrayList(u8){}; 185 + const arena = self.arena.allocator(); 186 + var result = std.ArrayList(u8).empty; 150 187 for (selected.text) |span| { 151 - try result.appendSlice(allocator, span.text); 188 + try result.appendSlice(arena, span.text); 152 189 } 153 190 self.result = result.items; 154 191 } ··· 156 193 } 157 194 }; 158 195 159 - fn toLower(allocator: std.mem.Allocator, src: []const u8) std.mem.Allocator.Error![]const u8 { 160 - 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); 161 198 for (src, 0..) |b, i| { 162 199 lower[i] = std.ascii.toLower(b); 163 200 } ··· 165 202 } 166 203 167 204 pub fn main() !void { 168 - var gpa = std.heap.GeneralPurposeAllocator(.{}){}; 169 - defer _ = gpa.deinit(); 205 + var debug_allocator = std.heap.GeneralPurposeAllocator(.{}){}; 206 + defer _ = debug_allocator.deinit(); 170 207 171 - const allocator = gpa.allocator(); 208 + const gpa = debug_allocator.allocator(); 172 209 173 - var app = try vxfw.App.init(allocator); 210 + var app = try vxfw.App.init(gpa); 174 211 errdefer app.deinit(); 175 212 176 - const model = try allocator.create(Model); 177 - defer allocator.destroy(model); 178 - model.* = .{ 179 - .list = std.ArrayList(vxfw.Text){}, 180 - .filtered = std.ArrayList(vxfw.RichText){}, 181 - .list_view = .{ 182 - .children = .{ 183 - .builder = .{ 184 - .userdata = model, 185 - .buildFn = Model.widgetBuilder, 186 - }, 187 - }, 188 - }, 189 - .text_field = .{ 190 - .buf = vxfw.TextField.Buffer.init(allocator), 191 - .userdata = model, 192 - .onChange = Model.onChange, 193 - .onSubmit = Model.onSubmit, 194 - }, 195 - .result = "", 196 - .arena = std.heap.ArenaAllocator.init(allocator), 197 - }; 198 - defer model.text_field.deinit(); 199 - defer model.list.deinit(allocator); 200 - defer model.filtered.deinit(allocator); 201 - defer model.arena.deinit(); 213 + const model = try Model.init(gpa); 214 + defer model.deinit(gpa); 202 215 203 216 // Run the command 204 - var fd = std.process.Child.init(&.{"fd"}, allocator); 217 + var fd = std.process.Child.init(&.{"fd"}, gpa); 205 218 fd.stdout_behavior = .Pipe; 206 219 fd.stderr_behavior = .Pipe; 207 - var stdout = std.ArrayList(u8){}; 208 - var stderr = std.ArrayList(u8){}; 209 - defer stdout.deinit(allocator); 210 - 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); 211 224 try fd.spawn(); 212 - try fd.collectOutput(allocator, &stdout, &stderr, 10_000_000); 225 + try fd.collectOutput(gpa, &stdout, &stderr, 10_000_000); 213 226 _ = try fd.wait(); 214 227 215 228 var iter = std.mem.splitScalar(u8, stdout.items, '\n'); 216 229 while (iter.next()) |line| { 217 230 if (line.len == 0) continue; 218 - try model.list.append(allocator, .{ .text = line }); 231 + try model.list.append(gpa, .{ .text = line }); 219 232 } 220 233 221 234 try app.run(model.widget(), .{});
+1 -1
examples/image.zig
··· 36 36 37 37 var read_buffer: [1024 * 1024]u8 = undefined; // 1MB buffer 38 38 var img1 = try vaxis.zigimg.Image.fromFilePath(alloc, "examples/zig.png", &read_buffer); 39 - defer img1.deinit(); 39 + defer img1.deinit(alloc); 40 40 41 41 const imgs = [_]vaxis.Image{ 42 42 try vx.transmitImage(alloc, tty.writer(), &img1, .rgba),
+99
examples/list_view.zig
··· 1 + const std = @import("std"); 2 + const vaxis = @import("vaxis"); 3 + const vxfw = vaxis.vxfw; 4 + 5 + const Text = vxfw.Text; 6 + const ListView = vxfw.ListView; 7 + const Widget = vxfw.Widget; 8 + 9 + const Model = struct { 10 + list_view: ListView, 11 + 12 + pub fn widget(self: *Model) Widget { 13 + return .{ 14 + .userdata = self, 15 + .eventHandler = Model.typeErasedEventHandler, 16 + .drawFn = Model.typeErasedDrawFn, 17 + }; 18 + } 19 + 20 + pub fn typeErasedEventHandler(ptr: *anyopaque, ctx: *vxfw.EventContext, event: vxfw.Event) anyerror!void { 21 + const self: *Model = @ptrCast(@alignCast(ptr)); 22 + try ctx.requestFocus(self.list_view.widget()); 23 + switch (event) { 24 + .key_press => |key| { 25 + if (key.matches('q', .{}) or key.matchExact('c', .{ .ctrl = true })) { 26 + ctx.quit = true; 27 + return; 28 + } 29 + }, 30 + else => {}, 31 + } 32 + } 33 + 34 + fn typeErasedDrawFn(ptr: *anyopaque, ctx: vxfw.DrawContext) std.mem.Allocator.Error!vxfw.Surface { 35 + const self: *Model = @ptrCast(@alignCast(ptr)); 36 + const max = ctx.max.size(); 37 + 38 + const list_view: vxfw.SubSurface = .{ 39 + .origin = .{ .row = 1, .col = 1 }, 40 + .surface = try self.list_view.draw(ctx), 41 + }; 42 + 43 + const children = try ctx.arena.alloc(vxfw.SubSurface, 1); 44 + children[0] = list_view; 45 + 46 + return .{ 47 + .size = max, 48 + .widget = self.widget(), 49 + .buffer = &.{}, 50 + .children = children, 51 + }; 52 + } 53 + }; 54 + 55 + pub fn main() !void { 56 + var gpa = std.heap.GeneralPurposeAllocator(.{}){}; 57 + defer _ = gpa.deinit(); 58 + 59 + const allocator = gpa.allocator(); 60 + 61 + var app = try vxfw.App.init(allocator); 62 + defer app.deinit(); 63 + 64 + const model = try allocator.create(Model); 65 + defer allocator.destroy(model); 66 + 67 + const n = 80; 68 + var texts = try std.ArrayList(Widget).initCapacity(allocator, n); 69 + 70 + var allocs = try std.ArrayList(*Text).initCapacity(allocator, n); 71 + defer { 72 + for (allocs.items) |tw| { 73 + allocator.free(tw.text); 74 + allocator.destroy(tw); 75 + } 76 + allocs.deinit(allocator); 77 + texts.deinit(allocator); 78 + } 79 + 80 + for (0..n) |i| { 81 + const t = std.fmt.allocPrint(allocator, "List Item {d}", .{i}) catch "placeholder"; 82 + const tw = try allocator.create(Text); 83 + tw.* = .{ .text = t }; 84 + _ = try allocs.append(allocator, tw); 85 + _ = try texts.append(allocator, tw.widget()); 86 + } 87 + 88 + model.* = .{ 89 + .list_view = .{ 90 + .wheel_scroll = 3, 91 + .scroll = .{ 92 + .wants_cursor = true, 93 + }, 94 + .children = .{ .slice = texts.items }, 95 + }, 96 + }; 97 + 98 + try app.run(model.widget(), .{}); 99 + }
+6 -10
examples/table.zig
··· 21 21 22 22 // Users set up below the main function 23 23 const users_buf = try alloc.dupe(User, users[0..]); 24 - var user_list = std.ArrayList(User).fromOwnedSlice(users_buf); 25 - defer user_list.deinit(alloc); 26 - var user_mal = std.MultiArrayList(User){}; 27 - for (users_buf[0..]) |user| try user_mal.append(alloc, user); 28 - defer user_mal.deinit(alloc); 29 24 30 25 var buffer: [1024]u8 = undefined; 31 26 var tty = try vaxis.Tty.init(&buffer); ··· 66 61 }; 67 62 var title_segs = [_]vaxis.Cell.Segment{ title_logo, title_info, title_disclaimer }; 68 63 69 - var cmd_input = vaxis.widgets.TextInput.init(alloc, &vx.unicode); 64 + var cmd_input = vaxis.widgets.TextInput.init(alloc); 70 65 defer cmd_input.deinit(); 71 66 72 67 // Colors ··· 178 173 mem.eql(u8, ":quit", cmd) or 179 174 mem.eql(u8, ":exit", cmd)) return; 180 175 if (mem.eql(u8, "G", cmd)) { 181 - demo_tbl.row = @intCast(user_list.items.len - 1); 176 + demo_tbl.row = @intCast(users_buf.len - 1); 182 177 active = .mid; 183 178 } 184 179 if (cmd.len >= 2 and mem.eql(u8, "gg", cmd[0..2])) { ··· 277 272 .width = win.width, 278 273 .height = win.height - (top_bar.height + 1), 279 274 }); 280 - if (user_list.items.len > 0) { 275 + if (users_buf.len > 0) { 281 276 demo_tbl.active = active == .mid; 282 277 try vaxis.widgets.Table.drawTable( 283 - event_alloc, 278 + null, 279 + // event_alloc, 284 280 middle_bar, 285 281 //users_buf[0..], 286 282 //user_list, 287 - user_mal, 283 + users_buf, 288 284 &demo_tbl, 289 285 ); 290 286 }
+1 -1
examples/text_input.zig
··· 63 63 64 64 // init our text input widget. The text input widget needs an allocator to 65 65 // store the contents of the input 66 - var text_input = TextInput.init(alloc, &vx.unicode); 66 + var text_input = TextInput.init(alloc); 67 67 defer text_input.deinit(); 68 68 69 69 try vx.setMouseMode(writer, true);
+66
examples/text_view.zig
··· 1 + const std = @import("std"); 2 + const log = std.log.scoped(.main); 3 + const vaxis = @import("vaxis"); 4 + 5 + const TextView = vaxis.widgets.TextView; 6 + 7 + const Event = union(enum) { 8 + key_press: vaxis.Key, 9 + winsize: vaxis.Winsize, 10 + }; 11 + 12 + pub fn main() !void { 13 + var gpa = std.heap.GeneralPurposeAllocator(.{}){}; 14 + 15 + defer { 16 + const deinit_status = gpa.deinit(); 17 + if (deinit_status == .leak) { 18 + log.err("memory leak", .{}); 19 + } 20 + } 21 + 22 + const alloc = gpa.allocator(); 23 + var buffer: [1024]u8 = undefined; 24 + var tty = try vaxis.Tty.init(&buffer); 25 + defer tty.deinit(); 26 + var vx = try vaxis.init(alloc, .{}); 27 + defer vx.deinit(alloc, tty.writer()); 28 + var loop: vaxis.Loop(Event) = .{ 29 + .vaxis = &vx, 30 + .tty = &tty, 31 + }; 32 + try loop.init(); 33 + try loop.start(); 34 + defer loop.stop(); 35 + try vx.enterAltScreen(tty.writer()); 36 + try vx.queryTerminal(tty.writer(), 20 * std.time.ns_per_s); 37 + var text_view = TextView{}; 38 + var text_view_buffer = TextView.Buffer{}; 39 + defer text_view_buffer.deinit(alloc); 40 + try text_view_buffer.append(alloc, .{ .bytes = "Press Enter to add a line, Up/Down to scroll, 'c' to close." }); 41 + 42 + var counter: i32 = 0; 43 + var lineBuf: [128]u8 = undefined; 44 + 45 + while (true) { 46 + const event = loop.nextEvent(); 47 + switch (event) { 48 + .key_press => |key| { 49 + // Close demo 50 + if (key.matches('c', .{})) break; 51 + if (key.matches(vaxis.Key.enter, .{})) { 52 + counter += 1; 53 + const new_content = try std.fmt.bufPrint(&lineBuf, "\nLine {d}", .{counter}); 54 + try text_view_buffer.append(alloc, .{ .bytes = new_content }); 55 + } 56 + text_view.input(key); 57 + }, 58 + .winsize => |ws| try vx.resize(alloc, tty.writer(), ws), 59 + } 60 + const win = vx.window(); 61 + win.clear(); 62 + text_view.draw(win, text_view_buffer); 63 + try vx.render(tty.writer()); 64 + try tty.writer.flush(); 65 + } 66 + }
+2 -2
examples/view.zig
··· 68 68 69 69 // Initialize Views 70 70 // - Large Map 71 - var lg_map_view = try View.init(alloc, &vx.unicode, .{ .width = lg_map_width, .height = lg_map_height }); 71 + var lg_map_view = try View.init(alloc, .{ .width = lg_map_width, .height = lg_map_height }); 72 72 defer lg_map_view.deinit(); 73 73 //w = lg_map_view.screen.width; 74 74 //h = lg_map_view.screen.height; ··· 76 76 _ = mem.replace(u8, lg_world_map, "\n", "", lg_map_buf[0..]); 77 77 _ = lg_map_view.printSegment(.{ .text = lg_map_buf[0..] }, .{ .wrap = .grapheme }); 78 78 // - Small Map 79 - var sm_map_view = try View.init(alloc, &vx.unicode, .{ .width = sm_map_width, .height = sm_map_height }); 79 + var sm_map_view = try View.init(alloc, .{ .width = sm_map_width, .height = sm_map_height }); 80 80 defer sm_map_view.deinit(); 81 81 w = sm_map_view.screen.width; 82 82 h = sm_map_view.screen.height;
-1
examples/vt.zig
··· 54 54 alloc, 55 55 &argv, 56 56 &env, 57 - &vx.unicode, 58 57 vt_opts, 59 58 &write_buf, 60 59 );
+18 -4
src/InternalScreen.zig
··· 83 83 row: u16, 84 84 cell: Cell, 85 85 ) void { 86 - if (self.width < col) { 86 + if (self.width <= col) { 87 87 // column out of bounds 88 88 return; 89 89 } 90 - if (self.height < row) { 90 + if (self.height <= row) { 91 91 // height out of bounds 92 92 return; 93 93 } ··· 110 110 } 111 111 112 112 pub fn readCell(self: *InternalScreen, col: u16, row: u16) ?Cell { 113 - if (self.width < col) { 113 + if (self.width <= col) { 114 114 // column out of bounds 115 115 return null; 116 116 } 117 - if (self.height < row) { 117 + if (self.height <= row) { 118 118 // height out of bounds 119 119 return null; 120 120 } ··· 131 131 .default = cell.default, 132 132 }; 133 133 } 134 + 135 + test "InternalScreen: out-of-bounds read/write are ignored" { 136 + var screen = try InternalScreen.init(std.testing.allocator, 2, 2); 137 + defer screen.deinit(std.testing.allocator); 138 + 139 + const sentinel: Cell = .{ .char = .{ .grapheme = "A", .width = 1 } }; 140 + screen.writeCell(0, 1, sentinel); 141 + 142 + const oob_cell: Cell = .{ .char = .{ .grapheme = "X", .width = 1 } }; 143 + screen.writeCell(2, 0, oob_cell); 144 + const read_back = screen.readCell(0, 1) orelse return error.TestUnexpectedResult; 145 + try std.testing.expect(std.mem.eql(u8, read_back.char.grapheme, "A")); 146 + try std.testing.expect(screen.readCell(2, 0) == null); 147 + }
+2 -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,
+24 -5
src/Parser.zig
··· 670 670 /// Parse a param buffer, returning a default value if the param was empty 671 671 inline fn parseParam(comptime T: type, buf: []const u8, default: ?T) ?T { 672 672 if (buf.len == 0) return default; 673 - return std.fmt.parseUnsigned(T, buf, 10) catch return null; 673 + return std.fmt.parseInt(T, buf, 10) catch return null; 674 674 } 675 675 676 676 /// Parse a mouse event ··· 678 678 const null_event: Result = .{ .event = null, .n = input.len }; 679 679 680 680 var button_mask: u16 = undefined; 681 - var px: u16 = undefined; 682 - var py: u16 = undefined; 681 + var px: i16 = undefined; 682 + var py: i16 = undefined; 683 683 var xterm: bool = undefined; 684 684 if (input.len == 3 and (input[2] == 'M') and full_input.len >= 6) { 685 685 xterm = true; ··· 691 691 const delim1 = std.mem.indexOfScalarPos(u8, input, 3, ';') orelse return null_event; 692 692 button_mask = parseParam(u16, input[3..delim1], null) orelse return null_event; 693 693 const delim2 = std.mem.indexOfScalarPos(u8, input, delim1 + 1, ';') orelse return null_event; 694 - px = parseParam(u16, input[delim1 + 1 .. delim2], 1) orelse return null_event; 695 - 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; 696 696 } else { 697 697 return null_event; 698 698 } ··· 1237 1237 .event = .{ .mouse = .{ 1238 1238 .col = 0, 1239 1239 .row = 0, 1240 + .button = .none, 1241 + .type = .motion, 1242 + .mods = .{}, 1243 + } }, 1244 + .n = input.len, 1245 + }; 1246 + 1247 + try testing.expectEqual(expected.n, result.n); 1248 + try testing.expectEqual(expected.event, result.event); 1249 + } 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, 1240 1259 .button = .none, 1241 1260 .type = .motion, 1242 1261 .mods = .{},
+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 }
-81
src/Unicode.zig
··· 1 - const std = @import("std"); 2 - const uucode = @import("uucode"); 3 - 4 - /// A thin wrapper around Unicode data - no longer needs allocation with uucode 5 - const Unicode = @This(); 6 - 7 - /// initialize all unicode data vaxis may possibly need 8 - /// With uucode, no initialization is needed but we keep this for API compatibility 9 - pub fn init(alloc: std.mem.Allocator) !Unicode { 10 - _ = alloc; 11 - return .{}; 12 - } 13 - 14 - /// free all data 15 - /// With uucode, no deinitialization is needed but we keep this for API compatibility 16 - pub fn deinit(self: *const Unicode, alloc: std.mem.Allocator) void { 17 - _ = self; 18 - _ = alloc; 19 - } 20 - 21 - // Old API-compatible Grapheme value 22 - pub const Grapheme = struct { 23 - start: usize, 24 - len: usize, 25 - 26 - pub fn bytes(self: Grapheme, str: []const u8) []const u8 { 27 - return str[self.start .. self.start + self.len]; 28 - } 29 - }; 30 - 31 - // Old API-compatible iterator that yields Grapheme with .len and .bytes() 32 - pub const GraphemeIterator = struct { 33 - str: []const u8, 34 - inner: uucode.grapheme.Iterator(uucode.utf8.Iterator), 35 - start: usize = 0, 36 - prev_break: bool = true, 37 - 38 - pub fn init(str: []const u8) GraphemeIterator { 39 - return .{ 40 - .str = str, 41 - .inner = uucode.grapheme.Iterator(uucode.utf8.Iterator).init(.init(str)), 42 - }; 43 - } 44 - 45 - pub fn next(self: *GraphemeIterator) ?Grapheme { 46 - while (self.inner.next()) |res| { 47 - // When leaving a break and entering a non-break, set the start of a cluster 48 - if (self.prev_break and !res.is_break) { 49 - const cp_len: usize = std.unicode.utf8CodepointSequenceLength(res.cp) catch 1; 50 - self.start = self.inner.i - cp_len; 51 - } 52 - 53 - // A break marks the end of the current grapheme 54 - if (res.is_break) { 55 - const end = self.inner.i; 56 - const s = self.start; 57 - self.start = end; 58 - self.prev_break = true; 59 - return .{ .start = s, .len = end - s }; 60 - } 61 - 62 - self.prev_break = false; 63 - } 64 - 65 - // Flush the last grapheme if we ended mid-cluster 66 - if (!self.prev_break and self.start < self.str.len) { 67 - const s = self.start; 68 - const len = self.str.len - s; 69 - self.start = self.str.len; 70 - self.prev_break = true; 71 - return .{ .start = s, .len = len }; 72 - } 73 - 74 - return null; 75 - } 76 - }; 77 - 78 - /// creates a grapheme iterator based on str 79 - pub fn graphemeIterator(str: []const u8) GraphemeIterator { 80 - return GraphemeIterator.init(str); 81 - }
+78 -35
src/Vaxis.zig
··· 360 360 assert(self.screen.buf.len == @as(usize, @intCast(self.screen.width)) * self.screen.height); // correct size 361 361 assert(self.screen.buf.len == self.screen_last.buf.len); // same size 362 362 363 - // Set up sync before we write anything 364 - // TODO: optimize sync so we only sync _when we have changes_. This 365 - // requires a smarter buffered writer, we'll probably have to write 366 - // our own 367 - try tty.writeAll(ctlseqs.sync_set); 368 - 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 {}; 369 366 370 - // Send the cursor to 0,0 371 - // TODO: this needs to move after we optimize writes. We only do 372 - // this if we have an update to make. We also need to hide cursor 373 - // and then reshow it if needed 374 - try tty.writeAll(ctlseqs.hide_cursor); 375 - if (self.state.alt_screen) 376 - try tty.writeAll(ctlseqs.home) 377 - else { 378 - try tty.writeByte('\r'); 379 - for (0..self.state.cursor.row) |_| { 380 - try tty.writeAll(ctlseqs.ri); 381 - } 382 - } 383 - 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; 384 374 385 375 // initialize some variables 386 376 var reposition: bool = false; ··· 388 378 var col: u16 = 0; 389 379 var cursor: Style = .{}; 390 380 var link: Hyperlink = .{}; 391 - var cursor_pos: struct { 381 + const CursorPos = struct { 392 382 row: u16 = 0, 393 383 col: u16 = 0, 394 - } = .{}; 384 + }; 385 + var cursor_pos: CursorPos = .{}; 395 386 396 - // Clear all images 397 - if (self.caps.kitty_graphics) 398 - 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 + }; 399 419 400 420 // Reset skip flag on all last_screen cells 401 421 for (self.screen_last.buf) |*last_cell| { 402 422 last_cell.skip = false; 423 + } 424 + 425 + if (needs_render) { 426 + try startRender.run(self, tty, &cursor_pos, &reposition, &started, &sync_active); 403 427 } 404 428 405 429 var i: usize = 0; ··· 446 470 try tty.writeAll(ctlseqs.osc8_clear); 447 471 } 448 472 continue; 473 + } 474 + if (!started) { 475 + try startRender.run(self, tty, &cursor_pos, &reposition, &started, &sync_active); 449 476 } 450 477 self.screen_last.buf[i].skipped = false; 451 478 defer { ··· 730 757 cursor_pos.col = col + w; 731 758 cursor_pos.row = row; 732 759 } 760 + if (!started) return; 733 761 if (self.screen.cursor_vis) { 734 762 if (self.state.alt_screen) { 735 763 try tty.print( ··· 761 789 self.state.cursor.row = cursor_pos.row; 762 790 self.state.cursor.col = cursor_pos.col; 763 791 } 792 + self.screen_last.cursor_vis = self.screen.cursor_vis; 764 793 if (self.screen.mouse_shape != self.screen_last.mouse_shape) { 765 794 try tty.print( 766 795 ctlseqs.osc22_mouse_shape, ··· 851 880 const ypos = mouse.row; 852 881 const xextra = self.screen.width_pix % self.screen.width; 853 882 const yextra = self.screen.height_pix % self.screen.height; 854 - const xcell = (self.screen.width_pix - xextra) / self.screen.width; 855 - 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); 856 885 if (xcell == 0 or ycell == 0) return mouse; 857 - result.col = xpos / xcell; 858 - result.row = ypos / ycell; 859 - result.xoffset = xpos % xcell; 860 - 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)); 861 890 } 862 891 return result; 863 892 } ··· 995 1024 const buf = switch (format) { 996 1025 .png => png: { 997 1026 const png_buf = try arena.allocator().alloc(u8, img.imageByteSize()); 998 - const png = try img.writeToMemory(png_buf, .{ .png = .{} }); 1027 + const png = try img.writeToMemory(arena.allocator(), png_buf, .{ .png = .{} }); 999 1028 break :png png; 1000 1029 }, 1001 1030 .rgb => rgb: { 1002 - try img.convert(.rgb24); 1031 + try img.convert(arena.allocator(), .rgb24); 1003 1032 break :rgb img.rawBytes(); 1004 1033 }, 1005 1034 .rgba => rgba: { 1006 - try img.convert(.rgba32); 1035 + try img.convert(arena.allocator(), .rgba32); 1007 1036 break :rgba img.rawBytes(); 1008 1037 }, 1009 1038 }; ··· 1027 1056 .path => |path| try zigimg.Image.fromFilePath(alloc, path, &read_buffer), 1028 1057 .mem => |bytes| try zigimg.Image.fromMemory(alloc, bytes), 1029 1058 }; 1030 - defer img.deinit(); 1059 + defer img.deinit(alloc); 1031 1060 return self.transmitImage(alloc, tty, &img, .png); 1032 1061 } 1033 1062 ··· 1409 1438 try tty.print(ctlseqs.osc7, .{uri.fmt(.{ .scheme = true, .authority = true, .path = true })}); 1410 1439 try tty.flush(); 1411 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 + }
+1 -1
src/main.zig
··· 72 72 ctlseqs.rmcup; 73 73 74 74 gty.writer().writeAll(reset) catch {}; 75 - 75 + gty.writer().flush() catch {}; 76 76 gty.deinit(); 77 77 } 78 78 }
+48 -46
src/queue.zig
··· 30 30 self.not_empty.wait(&self.mutex); 31 31 } 32 32 std.debug.assert(!self.isEmptyLH()); 33 - if (self.isFullLH()) { 34 - // If we are full, wake up a push that might be 35 - // waiting here. 36 - self.not_full.signal(); 37 - } 38 - 39 - return self.popLH(); 33 + return self.popAndSignalLH(); 40 34 } 41 35 42 36 /// Push an item into the queue. Blocks until an item has been ··· 48 42 self.not_full.wait(&self.mutex); 49 43 } 50 44 std.debug.assert(!self.isFullLH()); 51 - const was_empty = self.isEmptyLH(); 52 - 53 - self.buf[self.mask(self.write_index)] = item; 54 - self.write_index = self.mask2(self.write_index + 1); 55 - 56 - // If we were empty, wake up a pop if it was waiting. 57 - if (was_empty) { 58 - self.not_empty.signal(); 59 - } 45 + self.pushAndSignalLH(item); 60 46 } 61 47 62 48 /// Push an item into the queue. Returns true when the item ··· 64 50 /// was full. 65 51 pub fn tryPush(self: *Self, item: T) bool { 66 52 self.mutex.lock(); 67 - if (self.isFullLH()) { 68 - self.mutex.unlock(); 69 - return false; 70 - } 71 - self.mutex.unlock(); 72 - self.push(item); 53 + defer self.mutex.unlock(); 54 + if (self.isFullLH()) return false; 55 + self.pushAndSignalLH(item); 73 56 return true; 74 57 } 75 58 ··· 77 60 /// available. 78 61 pub fn tryPop(self: *Self) ?T { 79 62 self.mutex.lock(); 80 - if (self.isEmptyLH()) { 81 - self.mutex.unlock(); 82 - return null; 83 - } 84 - self.mutex.unlock(); 85 - return self.pop(); 63 + defer self.mutex.unlock(); 64 + if (self.isEmptyLH()) return null; 65 + return self.popAndSignalLH(); 86 66 } 87 67 88 68 /// Poll the queue. This call blocks until events are in the queue ··· 150 130 return index % (2 * self.buf.len); 151 131 } 152 132 133 + fn pushAndSignalLH(self: *Self, item: T) void { 134 + const was_empty = self.isEmptyLH(); 135 + self.buf[self.mask(self.write_index)] = item; 136 + self.write_index = self.mask2(self.write_index + 1); 137 + if (was_empty) { 138 + self.not_empty.signal(); 139 + } 140 + } 141 + 142 + fn popAndSignalLH(self: *Self) T { 143 + const was_full = self.isFullLH(); 144 + const result = self.popLH(); 145 + if (was_full) { 146 + self.not_full.signal(); 147 + } 148 + return result; 149 + } 150 + 153 151 fn popLH(self: *Self) T { 154 152 const result = self.buf[self.mask(self.read_index)]; 155 153 self.read_index = self.mask2(self.read_index + 1); ··· 204 202 thread.join(); 205 203 } 206 204 207 - fn sleepyPop(q: *Queue(u8, 2)) !void { 205 + fn sleepyPop(q: *Queue(u8, 2), state: *atomic.Value(u8)) !void { 208 206 // First we wait for the queue to be full. 209 - while (!q.isFull()) 207 + while (state.load(.acquire) < 1) 210 208 try Thread.yield(); 211 209 212 210 // Then we spuriously wake it up, because that's a thing that can ··· 220 218 // still full and the push in the other thread is still blocked 221 219 // waiting for space. 222 220 try Thread.yield(); 223 - std.Thread.sleep(std.time.ns_per_s); 221 + std.Thread.sleep(10 * std.time.ns_per_ms); 224 222 // Finally, let that other thread go. 225 223 try std.testing.expectEqual(1, q.pop()); 226 224 227 - // This won't continue until the other thread has had a chance to 228 - // put at least one item in the queue. 229 - while (!q.isFull()) 225 + // Wait for the other thread to signal it's ready for second push 226 + while (state.load(.acquire) < 2) 230 227 try Thread.yield(); 231 228 // But we want to ensure that there's a second push waiting, so 232 229 // here's another sleep. 233 - std.Thread.sleep(std.time.ns_per_s / 2); 230 + std.Thread.sleep(10 * std.time.ns_per_ms); 234 231 235 232 // Another spurious wake... 236 233 q.not_full.signal(); ··· 238 235 // And another chance for the other thread to see that it's 239 236 // spurious and go back to sleep. 240 237 try Thread.yield(); 241 - std.Thread.sleep(std.time.ns_per_s / 2); 238 + std.Thread.sleep(10 * std.time.ns_per_ms); 242 239 243 240 // Pop that thing and we're done. 244 241 try std.testing.expectEqual(2, q.pop()); ··· 252 249 // fails if the while loop in `push` is turned into an `if`. 253 250 254 251 var queue: Queue(u8, 2) = .{}; 255 - const thread = try Thread.spawn(cfg, sleepyPop, .{&queue}); 252 + var state = atomic.Value(u8).init(0); 253 + const thread = try Thread.spawn(cfg, sleepyPop, .{ &queue, &state }); 256 254 queue.push(1); 257 255 queue.push(2); 256 + state.store(1, .release); 258 257 const now = std.time.milliTimestamp(); 259 258 queue.push(3); // This one should block. 260 259 const then = std.time.milliTimestamp(); 261 260 262 261 // Just to make sure the sleeps are yielding to this thread, make 263 - // sure it took at least 900ms to do the push. 264 - try std.testing.expect(then - now > 900); 262 + // sure it took at least 5ms to do the push. 263 + try std.testing.expect(then - now > 5); 265 264 265 + state.store(2, .release); 266 266 // This should block again, waiting for the other thread. 267 267 queue.push(4); 268 268 ··· 272 272 try std.testing.expectEqual(4, queue.pop()); 273 273 } 274 274 275 - fn sleepyPush(q: *Queue(u8, 1)) !void { 275 + fn sleepyPush(q: *Queue(u8, 1), state: *atomic.Value(u8)) !void { 276 276 // Try to ensure the other thread has already started trying to pop. 277 277 try Thread.yield(); 278 - std.Thread.sleep(std.time.ns_per_s / 2); 278 + std.Thread.sleep(10 * std.time.ns_per_ms); 279 279 280 280 // Spurious wake 281 281 q.not_full.signal(); 282 282 q.not_empty.signal(); 283 283 284 284 try Thread.yield(); 285 - std.Thread.sleep(std.time.ns_per_s / 2); 285 + std.Thread.sleep(10 * std.time.ns_per_ms); 286 286 287 287 // Stick something in the queue so it can be popped. 288 288 q.push(1); 289 289 // Ensure it's been popped. 290 - while (!q.isEmpty()) 290 + while (state.load(.acquire) < 1) 291 291 try Thread.yield(); 292 292 // Give the other thread time to block again. 293 293 try Thread.yield(); 294 - std.Thread.sleep(std.time.ns_per_s / 2); 294 + std.Thread.sleep(10 * std.time.ns_per_ms); 295 295 296 296 // Spurious wake 297 297 q.not_full.signal(); ··· 306 306 // `if`. 307 307 308 308 var queue: Queue(u8, 1) = .{}; 309 - const thread = try Thread.spawn(cfg, sleepyPush, .{&queue}); 309 + var state = atomic.Value(u8).init(0); 310 + const thread = try Thread.spawn(cfg, sleepyPush, .{ &queue, &state }); 310 311 try std.testing.expectEqual(1, queue.pop()); 312 + state.store(1, .release); 311 313 try std.testing.expectEqual(2, queue.pop()); 312 314 thread.join(); 313 315 } ··· 322 324 const t1 = try Thread.spawn(cfg, readerThread, .{&queue}); 323 325 const t2 = try Thread.spawn(cfg, readerThread, .{&queue}); 324 326 try Thread.yield(); 325 - std.Thread.sleep(std.time.ns_per_s / 2); 327 + std.Thread.sleep(10 * std.time.ns_per_ms); 326 328 queue.push(1); 327 329 queue.push(1); 328 330 t1.join();
+3 -2
src/tty.zig
··· 453 453 0xc0 => '`', 454 454 0xdb => '[', 455 455 0xdc => '\\', 456 + 0xdf => '\\', 456 457 0xe2 => '\\', 457 458 0xdd => ']', 458 459 0xde => '\'', ··· 575 576 }; 576 577 577 578 const mouse: Mouse = .{ 578 - .col = @as(u16, @bitCast(event.dwMousePosition.X)), // Windows reports with 0 index 579 - .row = @as(u16, @bitCast(event.dwMousePosition.Y)), // Windows reports with 0 index 579 + .col = @as(i16, @bitCast(event.dwMousePosition.X)), // Windows reports with 0 index 580 + .row = @as(i16, @bitCast(event.dwMousePosition.Y)), // Windows reports with 0 index 580 581 .mods = mods, 581 582 .type = event_type, 582 583 .button = btn,
+64
src/unicode.zig
··· 1 + const std = @import("std"); 2 + const uucode = @import("uucode"); 3 + 4 + // Old API-compatible Grapheme value 5 + pub const Grapheme = struct { 6 + start: usize, 7 + len: usize, 8 + 9 + pub fn bytes(self: Grapheme, str: []const u8) []const u8 { 10 + return str[self.start .. self.start + self.len]; 11 + } 12 + }; 13 + 14 + // Old API-compatible iterator that yields Grapheme with .len and .bytes() 15 + pub const GraphemeIterator = struct { 16 + str: []const u8, 17 + inner: uucode.grapheme.Iterator(uucode.utf8.Iterator), 18 + start: usize = 0, 19 + prev_break: bool = true, 20 + 21 + pub fn init(str: []const u8) GraphemeIterator { 22 + return .{ 23 + .str = str, 24 + .inner = uucode.grapheme.Iterator(uucode.utf8.Iterator).init(.init(str)), 25 + }; 26 + } 27 + 28 + pub fn next(self: *GraphemeIterator) ?Grapheme { 29 + while (self.inner.next()) |res| { 30 + // When leaving a break and entering a non-break, set the start of a cluster 31 + if (self.prev_break and !res.is_break) { 32 + const cp_len: usize = std.unicode.utf8CodepointSequenceLength(res.cp) catch 1; 33 + self.start = self.inner.i - cp_len; 34 + } 35 + 36 + // A break marks the end of the current grapheme 37 + if (res.is_break) { 38 + const end = self.inner.i; 39 + const s = self.start; 40 + self.start = end; 41 + self.prev_break = true; 42 + return .{ .start = s, .len = end - s }; 43 + } 44 + 45 + self.prev_break = false; 46 + } 47 + 48 + // Flush the last grapheme if we ended mid-cluster 49 + if (!self.prev_break and self.start < self.str.len) { 50 + const s = self.start; 51 + const len = self.str.len - s; 52 + self.start = self.str.len; 53 + self.prev_break = true; 54 + return .{ .start = s, .len = len }; 55 + } 56 + 57 + return null; 58 + } 59 + }; 60 + 61 + /// creates a grapheme iterator based on str 62 + pub fn graphemeIterator(str: []const u8) GraphemeIterator { 63 + return GraphemeIterator.init(str); 64 + }
+4 -6
src/vxfw/App.zig
··· 80 80 vx.caps.sgr_pixels = false; 81 81 try vx.setMouseMode(tty.writer(), true); 82 82 83 - // Give DrawContext the unicode data 84 - vxfw.DrawContext.init(&vx.unicode, vx.screen.width_method); 83 + vxfw.DrawContext.init(vx.screen.width_method); 85 84 86 85 const framerate: u64 = if (opts.framerate > 0) opts.framerate else 60; 87 86 // Calculate tick rate ··· 262 261 .set_mouse_shape => |shape| self.vx.setMouseShape(shape), 263 262 .request_focus => |widget| self.wants_focus = widget, 264 263 .copy_to_clipboard => |content| { 264 + defer self.allocator.free(content); 265 265 self.vx.copyToSystemClipboard(self.tty.writer(), content, self.allocator) catch |err| { 266 266 switch (err) { 267 267 error.OutOfMemory => return Allocator.Error.OutOfMemory, ··· 270 270 }; 271 271 }, 272 272 .set_title => |title| { 273 + defer self.allocator.free(title); 273 274 self.vx.setTitle(self.tty.writer(), title) catch |err| { 274 275 std.log.err("set_title error: {}", .{err}); 275 276 }; ··· 531 532 // Find the path to the focused widget. This builds a list that has the first element as the 532 533 // focused widget, and walks backward to the root. It's possible our focused widget is *not* 533 534 // in this tree. If this is the case, we refocus to the root widget 534 - const has_focus = try self.childHasFocus(allocator, surface); 535 + _ = try self.childHasFocus(allocator, surface); 535 536 536 - // We assert that the focused widget *must* be in the widget tree. There is certianly a 537 - // logic bug in the code somewhere if this is not the case 538 - assert(has_focus); // Focused widget not found in Surface tree 539 537 if (!self.root.eql(surface.widget)) { 540 538 // If the root of surface is not the initial widget, we append the initial widget 541 539 try self.path_to_focused.append(allocator, self.root);
+10 -9
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.
+5 -3
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 } ··· 218 219 // Send the widget a mouse press on the separator 219 220 var mouse: vaxis.Mouse = .{ 220 221 // The separator is at width 221 - .col = split_view.width, 222 + .col = @intCast(split_view.width), 222 223 .row = 0, 223 224 .type = .press, 224 225 .button = .left, ··· 241 242 try split_widget.handleEvent(&ctx, .{ .mouse = mouse }); 242 243 try std.testing.expect(ctx.redraw); 243 244 try std.testing.expect(split_view.pressed); 244 - 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); 245 247 } 246 248 247 249 test "refAllDecls" {
+8 -2
src/vxfw/vxfw.zig
··· 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 {
+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" => {
+2 -1
src/widgets/TextView.zig
··· 85 85 while (iter.next()) |result| { 86 86 if (prev_break and !result.is_break) { 87 87 // Start of a new grapheme 88 - grapheme_start = iter.i - std.unicode.utf8CodepointSequenceLength(result.cp) catch 1; 88 + const cp_len: usize = std.unicode.utf8CodepointSequenceLength(result.cp) catch 1; 89 + grapheme_start = iter.i - cp_len; 89 90 } 90 91 91 92 if (result.is_break) {