+1
-1
README.md
+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
+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
+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
-7
build.zig.zon
···
5
5
.minimum_zig_version = "0.15.1",
6
6
.dependencies = .{
7
7
.zigimg = .{
8
-
// TODO .url = "git+https://github.com/zigimg/zigimg",
9
-
.url = "https://github.com/ivanstepanovftw/zigimg/archive/d7b7ab0ba0899643831ef042bd73289510b39906.tar.gz",
10
-
.hash = "zigimg-0.1.0-8_eo2vHnEwCIVW34Q14Ec-xUlzIoVg86-7FU2ypPtxms",
8
+
.url = "git+https://github.com/zigimg/zigimg#eab2522c023b9259db8b13f2f90d609b7437e5f6",
9
+
.hash = "zigimg-0.1.0-8_eo2vUZFgAAtN1c6dAO5DdqL0d4cEWHtn6iR5ucZJti",
11
10
},
12
-
.zg = .{
13
-
// Upstream PR: https://codeberg.org/atman/zg/pulls/90/
14
-
.url = "https://codeberg.org/chaten/zg/archive/749197a3f9d25e211615960c02380a3d659b20f9.tar.gz",
15
-
.hash = "zg-0.15.1-oGqU3M0-tALZCy7boQS86znlBloyKx6--JriGlY0Paa9",
11
+
.uucode = .{
12
+
.url = "git+https://github.com/jacobsandlund/uucode#5f05f8f83a75caea201f12cc8ea32a2d82ea9732",
13
+
.hash = "uucode-0.1.0-ZZjBPj96QADXyt5sqwBJUnhaDYs_qBeeKijZvlRa0eqM",
16
14
},
17
15
},
18
16
.paths = .{
+1
-1
examples/cli.zig
+1
-1
examples/cli.zig
+70
-60
examples/fuzzy.zig
+70
-60
examples/fuzzy.zig
···
4
4
5
5
const Model = struct {
6
6
list: std.ArrayList(vxfw.Text),
7
+
/// Memory owned by .arena
7
8
filtered: std.ArrayList(vxfw.RichText),
8
9
list_view: vxfw.ListView,
9
10
text_field: vxfw.TextField,
11
+
12
+
/// Used for filtered RichText Spans and result
13
+
arena: std.heap.ArenaAllocator,
14
+
filtered: std.ArrayList(vxfw.RichText),
10
15
result: []const u8,
11
-
unicode_data: *const vaxis.Unicode,
16
+
17
+
pub fn init(gpa: std.mem.Allocator) !*Model {
18
+
const model = try gpa.create(Model);
19
+
errdefer gpa.destroy(model);
20
+
21
+
model.* = .{
22
+
.list = .empty,
23
+
.filtered = .empty,
24
+
.list_view = .{
25
+
.children = .{
26
+
.builder = .{
27
+
.userdata = model,
28
+
.buildFn = Model.widgetBuilder,
29
+
},
30
+
},
31
+
},
32
+
.text_field = .{
33
+
.buf = vxfw.TextField.Buffer.init(gpa),
34
+
.userdata = model,
35
+
.onChange = Model.onChange,
36
+
.onSubmit = Model.onSubmit,
37
+
},
38
+
.result = "",
39
+
.arena = std.heap.ArenaAllocator.init(gpa),
40
+
};
41
+
42
+
return model;
43
+
}
12
44
13
-
/// Used for filtered RichText Spans
14
-
arena: std.heap.ArenaAllocator,
45
+
pub fn deinit(self: *Model, gpa: std.mem.Allocator) void {
46
+
self.arena.deinit();
47
+
self.text_field.deinit();
48
+
self.list.deinit(gpa);
49
+
gpa.destroy(self);
50
+
}
15
51
16
52
pub fn widget(self: *Model) vxfw.Widget {
17
53
return .{
···
26
62
switch (event) {
27
63
.init => {
28
64
// Initialize the filtered list
29
-
const allocator = self.arena.allocator();
65
+
const arena = self.arena.allocator();
30
66
for (self.list.items) |line| {
31
-
var spans = std.ArrayList(vxfw.RichText.TextSpan){};
67
+
var spans = std.ArrayList(vxfw.RichText.TextSpan).empty;
32
68
const span: vxfw.RichText.TextSpan = .{ .text = line.text };
33
-
try spans.append(allocator, span);
34
-
try self.filtered.append(allocator, .{ .text = spans.items });
69
+
try spans.append(arena, span);
70
+
try self.filtered.append(arena, .{ .text = spans.items });
35
71
}
36
72
37
73
return ctx.requestFocus(self.text_field.widget());
···
100
136
fn onChange(maybe_ptr: ?*anyopaque, _: *vxfw.EventContext, str: []const u8) anyerror!void {
101
137
const ptr = maybe_ptr orelse return;
102
138
const self: *Model = @ptrCast(@alignCast(ptr));
103
-
const allocator = self.arena.allocator();
104
-
self.filtered.clearAndFree(allocator);
139
+
const arena = self.arena.allocator();
140
+
self.filtered.clearAndFree(arena);
105
141
_ = self.arena.reset(.free_all);
106
142
107
143
const hasUpper = for (str) |b| {
···
115
151
const tgt = if (hasUpper)
116
152
item.text
117
153
else
118
-
try toLower(allocator, item.text);
154
+
try toLower(arena, item.text);
119
155
120
-
var spans = std.ArrayList(vxfw.RichText.TextSpan){};
156
+
var spans = std.ArrayList(vxfw.RichText.TextSpan).empty;
121
157
var i: usize = 0;
122
-
var iter = self.unicode_data.graphemeIterator(str);
158
+
var iter = vaxis.unicode.graphemeIterator(str);
123
159
while (iter.next()) |g| {
124
160
if (std.mem.indexOfPos(u8, tgt, i, g.bytes(str))) |idx| {
125
161
const up_to_here: vxfw.RichText.TextSpan = .{ .text = item.text[i..idx] };
···
127
163
.text = item.text[idx .. idx + g.len],
128
164
.style = .{ .fg = .{ .index = 4 }, .reverse = true },
129
165
};
130
-
try spans.append(allocator, up_to_here);
131
-
try spans.append(allocator, match);
166
+
try spans.append(arena, up_to_here);
167
+
try spans.append(arena, match);
132
168
i = idx + g.len;
133
169
} else continue :outer;
134
170
}
135
171
const up_to_here: vxfw.RichText.TextSpan = .{ .text = item.text[i..] };
136
-
try spans.append(allocator, up_to_here);
137
-
try self.filtered.append(allocator, .{ .text = spans.items });
172
+
try spans.append(arena, up_to_here);
173
+
try self.filtered.append(arena, .{ .text = spans.items });
138
174
}
139
175
self.list_view.scroll.top = 0;
140
176
self.list_view.scroll.offset = 0;
···
146
182
const self: *Model = @ptrCast(@alignCast(ptr));
147
183
if (self.list_view.cursor < self.filtered.items.len) {
148
184
const selected = self.filtered.items[self.list_view.cursor];
149
-
const allocator = self.arena.allocator();
150
-
var result = std.ArrayList(u8){};
185
+
const arena = self.arena.allocator();
186
+
var result = std.ArrayList(u8).empty;
151
187
for (selected.text) |span| {
152
-
try result.appendSlice(allocator, span.text);
188
+
try result.appendSlice(arena, span.text);
153
189
}
154
190
self.result = result.items;
155
191
}
···
157
193
}
158
194
};
159
195
160
-
fn toLower(allocator: std.mem.Allocator, src: []const u8) std.mem.Allocator.Error![]const u8 {
161
-
const lower = try allocator.alloc(u8, src.len);
196
+
fn toLower(arena: std.mem.Allocator, src: []const u8) std.mem.Allocator.Error![]const u8 {
197
+
const lower = try arena.alloc(u8, src.len);
162
198
for (src, 0..) |b, i| {
163
199
lower[i] = std.ascii.toLower(b);
164
200
}
···
166
202
}
167
203
168
204
pub fn main() !void {
169
-
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
170
-
defer _ = gpa.deinit();
205
+
var debug_allocator = std.heap.GeneralPurposeAllocator(.{}){};
206
+
defer _ = debug_allocator.deinit();
171
207
172
-
const allocator = gpa.allocator();
208
+
const gpa = debug_allocator.allocator();
173
209
174
-
var app = try vxfw.App.init(allocator);
210
+
var app = try vxfw.App.init(gpa);
175
211
errdefer app.deinit();
176
212
177
-
const model = try allocator.create(Model);
178
-
defer allocator.destroy(model);
179
-
model.* = .{
180
-
.list = std.ArrayList(vxfw.Text){},
181
-
.filtered = std.ArrayList(vxfw.RichText){},
182
-
.list_view = .{
183
-
.children = .{
184
-
.builder = .{
185
-
.userdata = model,
186
-
.buildFn = Model.widgetBuilder,
187
-
},
188
-
},
189
-
},
190
-
.text_field = .{
191
-
.buf = vxfw.TextField.Buffer.init(allocator),
192
-
.unicode = &app.vx.unicode,
193
-
.userdata = model,
194
-
.onChange = Model.onChange,
195
-
.onSubmit = Model.onSubmit,
196
-
},
197
-
.result = "",
198
-
.arena = std.heap.ArenaAllocator.init(allocator),
199
-
.unicode_data = &app.vx.unicode,
200
-
};
201
-
defer model.text_field.deinit();
202
-
defer model.list.deinit(allocator);
203
-
defer model.filtered.deinit(allocator);
204
-
defer model.arena.deinit();
213
+
const model = try Model.init(gpa);
214
+
defer model.deinit(gpa);
205
215
206
216
// Run the command
207
-
var fd = std.process.Child.init(&.{"fd"}, allocator);
217
+
var fd = std.process.Child.init(&.{"fd"}, gpa);
208
218
fd.stdout_behavior = .Pipe;
209
219
fd.stderr_behavior = .Pipe;
210
-
var stdout = std.ArrayList(u8){};
211
-
var stderr = std.ArrayList(u8){};
212
-
defer stdout.deinit(allocator);
213
-
defer stderr.deinit(allocator);
220
+
var stdout = std.ArrayList(u8).empty;
221
+
var stderr = std.ArrayList(u8).empty;
222
+
defer stdout.deinit(gpa);
223
+
defer stderr.deinit(gpa);
214
224
try fd.spawn();
215
-
try fd.collectOutput(allocator, &stdout, &stderr, 10_000_000);
225
+
try fd.collectOutput(gpa, &stdout, &stderr, 10_000_000);
216
226
_ = try fd.wait();
217
227
218
228
var iter = std.mem.splitScalar(u8, stdout.items, '\n');
219
229
while (iter.next()) |line| {
220
230
if (line.len == 0) continue;
221
-
try model.list.append(allocator, .{ .text = line });
231
+
try model.list.append(gpa, .{ .text = line });
222
232
}
223
233
224
234
try app.run(model.widget(), .{});
+1
-1
examples/image.zig
+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
+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
+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
+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
+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
+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
-1
examples/vt.zig
+18
-4
src/InternalScreen.zig
+18
-4
src/InternalScreen.zig
···
83
83
row: u16,
84
84
cell: Cell,
85
85
) void {
86
-
if (self.width < col) {
86
+
if (self.width <= col) {
87
87
// column out of bounds
88
88
return;
89
89
}
90
-
if (self.height < row) {
90
+
if (self.height <= row) {
91
91
// height out of bounds
92
92
return;
93
93
}
···
110
110
}
111
111
112
112
pub fn readCell(self: *InternalScreen, col: u16, row: u16) ?Cell {
113
-
if (self.width < col) {
113
+
if (self.width <= col) {
114
114
// column out of bounds
115
115
return null;
116
116
}
117
-
if (self.height < row) {
117
+
if (self.height <= row) {
118
118
// height out of bounds
119
119
return null;
120
120
}
···
131
131
.default = cell.default,
132
132
};
133
133
}
134
+
135
+
test "InternalScreen: out-of-bounds read/write are ignored" {
136
+
var screen = try InternalScreen.init(std.testing.allocator, 2, 2);
137
+
defer screen.deinit(std.testing.allocator);
138
+
139
+
const sentinel: Cell = .{ .char = .{ .grapheme = "A", .width = 1 } };
140
+
screen.writeCell(0, 1, sentinel);
141
+
142
+
const oob_cell: Cell = .{ .char = .{ .grapheme = "X", .width = 1 } };
143
+
screen.writeCell(2, 0, oob_cell);
144
+
const read_back = screen.readCell(0, 1) orelse return error.TestUnexpectedResult;
145
+
try std.testing.expect(std.mem.eql(u8, read_back.char.grapheme, "A"));
146
+
try std.testing.expect(screen.readCell(2, 0) == null);
147
+
}
+2
-10
src/Loop.zig
+2
-10
src/Loop.zig
···
1
1
const std = @import("std");
2
2
const builtin = @import("builtin");
3
3
4
-
const Graphemes = @import("Graphemes");
5
-
6
4
const GraphemeCache = @import("GraphemeCache.zig");
7
5
const Parser = @import("Parser.zig");
8
6
const Queue = @import("queue.zig").Queue;
···
47
45
if (self.thread) |_| return;
48
46
self.thread = try std.Thread.spawn(.{}, Self.ttyRun, .{
49
47
self,
50
-
&self.vaxis.unicode.width_data.graphemes,
51
48
self.vaxis.opts.system_clipboard_allocator,
52
49
});
53
50
}
···
107
104
/// read input from the tty. This is run in a separate thread
108
105
fn ttyRun(
109
106
self: *Self,
110
-
grapheme_data: *const Graphemes,
111
107
paste_allocator: ?std.mem.Allocator,
112
108
) !void {
113
109
// Return early if we're in test mode to avoid infinite loops
···
118
114
119
115
switch (builtin.os.tag) {
120
116
.windows => {
121
-
var parser: Parser = .{
122
-
.grapheme_data = grapheme_data,
123
-
};
117
+
var parser: Parser = .{};
124
118
while (!self.should_quit) {
125
119
const event = try self.tty.nextEvent(&parser, paste_allocator);
126
120
try handleEventGeneric(self, self.vaxis, &cache, Event, event, null);
···
133
127
self.postEvent(.{ .winsize = winsize });
134
128
}
135
129
136
-
var parser: Parser = .{
137
-
.grapheme_data = grapheme_data,
138
-
};
130
+
var parser: Parser = .{};
139
131
140
132
// initialize the read buffer
141
133
var buf: [1024]u8 = undefined;
+2
-2
src/Mouse.zig
+2
-2
src/Mouse.zig
+148
-92
src/Parser.zig
+148
-92
src/Parser.zig
···
4
4
const Event = @import("event.zig").Event;
5
5
const Key = @import("Key.zig");
6
6
const Mouse = @import("Mouse.zig");
7
-
const code_point = @import("code_point");
8
-
const Graphemes = @import("Graphemes");
7
+
const uucode = @import("uucode");
9
8
const Winsize = @import("main.zig").Winsize;
10
9
11
10
const log = std.log.scoped(.vaxis_parser);
···
45
44
// a buffer to temporarily store text in. We need this to encode
46
45
// text-as-codepoints
47
46
buf: [128]u8 = undefined,
48
-
49
-
grapheme_data: *const Graphemes,
50
47
51
48
/// Parse the first event from the input buffer. If a completion event is not
52
49
/// present, Result.event will be null and Result.n will be 0
···
78
75
};
79
76
},
80
77
}
81
-
} else return parseGround(input, self.grapheme_data);
78
+
} else return parseGround(input);
82
79
}
83
80
84
81
/// Parse ground state
85
-
inline fn parseGround(input: []const u8, data: *const Graphemes) !Result {
82
+
inline fn parseGround(input: []const u8) !Result {
86
83
std.debug.assert(input.len > 0);
87
84
88
85
const b = input[0];
···
109
106
},
110
107
0x7F => .{ .codepoint = Key.backspace },
111
108
else => blk: {
112
-
var iter: code_point.Iterator = .{ .bytes = input };
109
+
var iter = uucode.utf8.Iterator.init(input);
113
110
// return null if we don't have a valid codepoint
114
-
const cp = iter.next() orelse return error.InvalidUTF8;
111
+
const first_cp = iter.next() orelse return error.InvalidUTF8;
115
112
116
-
n = cp.len;
113
+
n = std.unicode.utf8CodepointSequenceLength(first_cp) catch return error.InvalidUTF8;
117
114
118
115
// Check if we have a multi-codepoint grapheme
119
-
var code = cp.code;
120
-
var g_state: Graphemes.IterState = .{};
121
-
var prev_cp = code;
122
-
while (iter.next()) |next_cp| {
123
-
if (Graphemes.graphemeBreak(prev_cp, next_cp.code, data, &g_state)) {
116
+
var code = first_cp;
117
+
var grapheme_iter = uucode.grapheme.Iterator(uucode.utf8.Iterator).init(.init(input));
118
+
var grapheme_len: usize = 0;
119
+
var cp_count: usize = 0;
120
+
121
+
while (grapheme_iter.next()) |result| {
122
+
cp_count += 1;
123
+
if (result.is_break) {
124
+
// Found the first grapheme boundary
125
+
grapheme_len = grapheme_iter.i;
124
126
break;
125
127
}
126
-
prev_cp = next_cp.code;
127
-
code = Key.multicodepoint;
128
-
n += next_cp.len;
128
+
}
129
+
130
+
if (grapheme_len > 0) {
131
+
n = grapheme_len;
132
+
if (cp_count > 1) {
133
+
code = Key.multicodepoint;
134
+
}
129
135
}
130
136
131
137
break :blk .{ .codepoint = code, .text = input[0..n] };
···
664
670
/// Parse a param buffer, returning a default value if the param was empty
665
671
inline fn parseParam(comptime T: type, buf: []const u8, default: ?T) ?T {
666
672
if (buf.len == 0) return default;
667
-
return std.fmt.parseUnsigned(T, buf, 10) catch return null;
673
+
return std.fmt.parseInt(T, buf, 10) catch return null;
668
674
}
669
675
670
676
/// Parse a mouse event
···
672
678
const null_event: Result = .{ .event = null, .n = input.len };
673
679
674
680
var button_mask: u16 = undefined;
675
-
var px: u16 = undefined;
676
-
var py: u16 = undefined;
681
+
var px: i16 = undefined;
682
+
var py: i16 = undefined;
677
683
var xterm: bool = undefined;
678
684
if (input.len == 3 and (input[2] == 'M') and full_input.len >= 6) {
679
685
xterm = true;
···
685
691
const delim1 = std.mem.indexOfScalarPos(u8, input, 3, ';') orelse return null_event;
686
692
button_mask = parseParam(u16, input[3..delim1], null) orelse return null_event;
687
693
const delim2 = std.mem.indexOfScalarPos(u8, input, delim1 + 1, ';') orelse return null_event;
688
-
px = parseParam(u16, input[delim1 + 1 .. delim2], 1) orelse return null_event;
689
-
py = parseParam(u16, input[delim2 + 1 .. input.len - 1], 1) orelse return null_event;
694
+
px = parseParam(i16, input[delim1 + 1 .. delim2], 1) orelse return null_event;
695
+
py = parseParam(i16, input[delim2 + 1 .. input.len - 1], 1) orelse return null_event;
690
696
} else {
691
697
return null_event;
692
698
}
···
731
737
732
738
test "parse: single xterm keypress" {
733
739
const alloc = testing.allocator_instance.allocator();
734
-
const grapheme_data = try Graphemes.init(alloc);
735
-
defer grapheme_data.deinit(alloc);
736
740
const input = "a";
737
-
var parser: Parser = .{ .grapheme_data = &grapheme_data };
741
+
var parser: Parser = .{};
738
742
const result = try parser.parse(input, alloc);
739
743
const expected_key: Key = .{
740
744
.codepoint = 'a',
···
748
752
749
753
test "parse: single xterm keypress backspace" {
750
754
const alloc = testing.allocator_instance.allocator();
751
-
const grapheme_data = try Graphemes.init(alloc);
752
-
defer grapheme_data.deinit(alloc);
753
755
const input = "\x08";
754
-
var parser: Parser = .{ .grapheme_data = &grapheme_data };
756
+
var parser: Parser = .{};
755
757
const result = try parser.parse(input, alloc);
756
758
const expected_key: Key = .{
757
759
.codepoint = Key.backspace,
···
764
766
765
767
test "parse: single xterm keypress with more buffer" {
766
768
const alloc = testing.allocator_instance.allocator();
767
-
const grapheme_data = try Graphemes.init(alloc);
768
-
defer grapheme_data.deinit(alloc);
769
769
const input = "ab";
770
-
var parser: Parser = .{ .grapheme_data = &grapheme_data };
770
+
var parser: Parser = .{};
771
771
const result = try parser.parse(input, alloc);
772
772
const expected_key: Key = .{
773
773
.codepoint = 'a',
···
782
782
783
783
test "parse: xterm escape keypress" {
784
784
const alloc = testing.allocator_instance.allocator();
785
-
const grapheme_data = try Graphemes.init(alloc);
786
-
defer grapheme_data.deinit(alloc);
787
785
const input = "\x1b";
788
-
var parser: Parser = .{ .grapheme_data = &grapheme_data };
786
+
var parser: Parser = .{};
789
787
const result = try parser.parse(input, alloc);
790
788
const expected_key: Key = .{ .codepoint = Key.escape };
791
789
const expected_event: Event = .{ .key_press = expected_key };
···
796
794
797
795
test "parse: xterm ctrl+a" {
798
796
const alloc = testing.allocator_instance.allocator();
799
-
const grapheme_data = try Graphemes.init(alloc);
800
-
defer grapheme_data.deinit(alloc);
801
797
const input = "\x01";
802
-
var parser: Parser = .{ .grapheme_data = &grapheme_data };
798
+
var parser: Parser = .{};
803
799
const result = try parser.parse(input, alloc);
804
800
const expected_key: Key = .{ .codepoint = 'a', .mods = .{ .ctrl = true } };
805
801
const expected_event: Event = .{ .key_press = expected_key };
···
810
806
811
807
test "parse: xterm alt+a" {
812
808
const alloc = testing.allocator_instance.allocator();
813
-
const grapheme_data = try Graphemes.init(alloc);
814
-
defer grapheme_data.deinit(alloc);
815
809
const input = "\x1ba";
816
-
var parser: Parser = .{ .grapheme_data = &grapheme_data };
810
+
var parser: Parser = .{};
817
811
const result = try parser.parse(input, alloc);
818
812
const expected_key: Key = .{ .codepoint = 'a', .mods = .{ .alt = true } };
819
813
const expected_event: Event = .{ .key_press = expected_key };
···
824
818
825
819
test "parse: xterm key up" {
826
820
const alloc = testing.allocator_instance.allocator();
827
-
const grapheme_data = try Graphemes.init(alloc);
828
-
defer grapheme_data.deinit(alloc);
829
821
{
830
822
// normal version
831
823
const input = "\x1b[A";
832
-
var parser: Parser = .{ .grapheme_data = &grapheme_data };
824
+
var parser: Parser = .{};
833
825
const result = try parser.parse(input, alloc);
834
826
const expected_key: Key = .{ .codepoint = Key.up };
835
827
const expected_event: Event = .{ .key_press = expected_key };
···
841
833
{
842
834
// application keys version
843
835
const input = "\x1bOA";
844
-
var parser: Parser = .{ .grapheme_data = &grapheme_data };
836
+
var parser: Parser = .{};
845
837
const result = try parser.parse(input, alloc);
846
838
const expected_key: Key = .{ .codepoint = Key.up };
847
839
const expected_event: Event = .{ .key_press = expected_key };
···
853
845
854
846
test "parse: xterm shift+up" {
855
847
const alloc = testing.allocator_instance.allocator();
856
-
const grapheme_data = try Graphemes.init(alloc);
857
-
defer grapheme_data.deinit(alloc);
858
848
const input = "\x1b[1;2A";
859
-
var parser: Parser = .{ .grapheme_data = &grapheme_data };
849
+
var parser: Parser = .{};
860
850
const result = try parser.parse(input, alloc);
861
851
const expected_key: Key = .{ .codepoint = Key.up, .mods = .{ .shift = true } };
862
852
const expected_event: Event = .{ .key_press = expected_key };
···
867
857
868
858
test "parse: xterm insert" {
869
859
const alloc = testing.allocator_instance.allocator();
870
-
const grapheme_data = try Graphemes.init(alloc);
871
-
defer grapheme_data.deinit(alloc);
872
860
const input = "\x1b[2~";
873
-
var parser: Parser = .{ .grapheme_data = &grapheme_data };
861
+
var parser: Parser = .{};
874
862
const result = try parser.parse(input, alloc);
875
863
const expected_key: Key = .{ .codepoint = Key.insert, .mods = .{} };
876
864
const expected_event: Event = .{ .key_press = expected_key };
···
881
869
882
870
test "parse: paste_start" {
883
871
const alloc = testing.allocator_instance.allocator();
884
-
const grapheme_data = try Graphemes.init(alloc);
885
-
defer grapheme_data.deinit(alloc);
886
872
const input = "\x1b[200~";
887
-
var parser: Parser = .{ .grapheme_data = &grapheme_data };
873
+
var parser: Parser = .{};
888
874
const result = try parser.parse(input, alloc);
889
875
const expected_event: Event = .paste_start;
890
876
···
894
880
895
881
test "parse: paste_end" {
896
882
const alloc = testing.allocator_instance.allocator();
897
-
const grapheme_data = try Graphemes.init(alloc);
898
-
defer grapheme_data.deinit(alloc);
899
883
const input = "\x1b[201~";
900
-
var parser: Parser = .{ .grapheme_data = &grapheme_data };
884
+
var parser: Parser = .{};
901
885
const result = try parser.parse(input, alloc);
902
886
const expected_event: Event = .paste_end;
903
887
···
907
891
908
892
test "parse: osc52 paste" {
909
893
const alloc = testing.allocator_instance.allocator();
910
-
const grapheme_data = try Graphemes.init(alloc);
911
-
defer grapheme_data.deinit(alloc);
912
894
const input = "\x1b]52;c;b3NjNTIgcGFzdGU=\x1b\\";
913
895
const expected_text = "osc52 paste";
914
-
var parser: Parser = .{ .grapheme_data = &grapheme_data };
896
+
var parser: Parser = .{};
915
897
const result = try parser.parse(input, alloc);
916
898
917
899
try testing.expectEqual(25, result.n);
···
926
908
927
909
test "parse: focus_in" {
928
910
const alloc = testing.allocator_instance.allocator();
929
-
const grapheme_data = try Graphemes.init(alloc);
930
-
defer grapheme_data.deinit(alloc);
931
911
const input = "\x1b[I";
932
-
var parser: Parser = .{ .grapheme_data = &grapheme_data };
912
+
var parser: Parser = .{};
933
913
const result = try parser.parse(input, alloc);
934
914
const expected_event: Event = .focus_in;
935
915
···
939
919
940
920
test "parse: focus_out" {
941
921
const alloc = testing.allocator_instance.allocator();
942
-
const grapheme_data = try Graphemes.init(alloc);
943
-
defer grapheme_data.deinit(alloc);
944
922
const input = "\x1b[O";
945
-
var parser: Parser = .{ .grapheme_data = &grapheme_data };
923
+
var parser: Parser = .{};
946
924
const result = try parser.parse(input, alloc);
947
925
const expected_event: Event = .focus_out;
948
926
···
952
930
953
931
test "parse: kitty: shift+a without text reporting" {
954
932
const alloc = testing.allocator_instance.allocator();
955
-
const grapheme_data = try Graphemes.init(alloc);
956
-
defer grapheme_data.deinit(alloc);
957
933
const input = "\x1b[97:65;2u";
958
-
var parser: Parser = .{ .grapheme_data = &grapheme_data };
934
+
var parser: Parser = .{};
959
935
const result = try parser.parse(input, alloc);
960
936
const expected_key: Key = .{
961
937
.codepoint = 'a',
···
971
947
972
948
test "parse: kitty: alt+shift+a without text reporting" {
973
949
const alloc = testing.allocator_instance.allocator();
974
-
const grapheme_data = try Graphemes.init(alloc);
975
-
defer grapheme_data.deinit(alloc);
976
950
const input = "\x1b[97:65;4u";
977
-
var parser: Parser = .{ .grapheme_data = &grapheme_data };
951
+
var parser: Parser = .{};
978
952
const result = try parser.parse(input, alloc);
979
953
const expected_key: Key = .{
980
954
.codepoint = 'a',
···
989
963
990
964
test "parse: kitty: a without text reporting" {
991
965
const alloc = testing.allocator_instance.allocator();
992
-
const grapheme_data = try Graphemes.init(alloc);
993
-
defer grapheme_data.deinit(alloc);
994
966
const input = "\x1b[97u";
995
-
var parser: Parser = .{ .grapheme_data = &grapheme_data };
967
+
var parser: Parser = .{};
996
968
const result = try parser.parse(input, alloc);
997
969
const expected_key: Key = .{
998
970
.codepoint = 'a',
···
1005
977
1006
978
test "parse: kitty: release event" {
1007
979
const alloc = testing.allocator_instance.allocator();
1008
-
const grapheme_data = try Graphemes.init(alloc);
1009
-
defer grapheme_data.deinit(alloc);
1010
980
const input = "\x1b[97;1:3u";
1011
-
var parser: Parser = .{ .grapheme_data = &grapheme_data };
981
+
var parser: Parser = .{};
1012
982
const result = try parser.parse(input, alloc);
1013
983
const expected_key: Key = .{
1014
984
.codepoint = 'a',
···
1021
991
1022
992
test "parse: single codepoint" {
1023
993
const alloc = testing.allocator_instance.allocator();
1024
-
const grapheme_data = try Graphemes.init(alloc);
1025
-
defer grapheme_data.deinit(alloc);
1026
994
const input = "๐";
1027
-
var parser: Parser = .{ .grapheme_data = &grapheme_data };
995
+
var parser: Parser = .{};
1028
996
const result = try parser.parse(input, alloc);
1029
997
const expected_key: Key = .{
1030
998
.codepoint = 0x1F642,
···
1038
1006
1039
1007
test "parse: single codepoint with more in buffer" {
1040
1008
const alloc = testing.allocator_instance.allocator();
1041
-
const grapheme_data = try Graphemes.init(alloc);
1042
-
defer grapheme_data.deinit(alloc);
1043
1009
const input = "๐a";
1044
-
var parser: Parser = .{ .grapheme_data = &grapheme_data };
1010
+
var parser: Parser = .{};
1045
1011
const result = try parser.parse(input, alloc);
1046
1012
const expected_key: Key = .{
1047
1013
.codepoint = 0x1F642,
···
1055
1021
1056
1022
test "parse: multiple codepoint grapheme" {
1057
1023
const alloc = testing.allocator_instance.allocator();
1058
-
const grapheme_data = try Graphemes.init(alloc);
1059
-
defer grapheme_data.deinit(alloc);
1060
1024
const input = "๐ฉโ๐";
1061
-
var parser: Parser = .{ .grapheme_data = &grapheme_data };
1025
+
var parser: Parser = .{};
1062
1026
const result = try parser.parse(input, alloc);
1063
1027
const expected_key: Key = .{
1064
1028
.codepoint = Key.multicodepoint,
···
1072
1036
1073
1037
test "parse: multiple codepoint grapheme with more after" {
1074
1038
const alloc = testing.allocator_instance.allocator();
1075
-
const grapheme_data = try Graphemes.init(alloc);
1076
-
defer grapheme_data.deinit(alloc);
1077
1039
const input = "๐ฉโ๐abc";
1078
-
var parser: Parser = .{ .grapheme_data = &grapheme_data };
1040
+
var parser: Parser = .{};
1079
1041
const result = try parser.parse(input, alloc);
1080
1042
const expected_key: Key = .{
1081
1043
.codepoint = Key.multicodepoint,
···
1088
1050
try testing.expectEqual(expected_key.codepoint, actual.codepoint);
1089
1051
}
1090
1052
1053
+
test "parse: flag emoji" {
1054
+
const alloc = testing.allocator_instance.allocator();
1055
+
const input = "๐บ๐ธ";
1056
+
var parser: Parser = .{};
1057
+
const result = try parser.parse(input, alloc);
1058
+
const expected_key: Key = .{
1059
+
.codepoint = Key.multicodepoint,
1060
+
.text = input,
1061
+
};
1062
+
const expected_event: Event = .{ .key_press = expected_key };
1063
+
1064
+
try testing.expectEqual(input.len, result.n);
1065
+
try testing.expectEqual(expected_event, result.event);
1066
+
}
1067
+
1068
+
test "parse: combining mark" {
1069
+
const alloc = testing.allocator_instance.allocator();
1070
+
// a with combining acute accent (NFD form)
1071
+
const input = "a\u{0301}";
1072
+
var parser: Parser = .{};
1073
+
const result = try parser.parse(input, alloc);
1074
+
const expected_key: Key = .{
1075
+
.codepoint = Key.multicodepoint,
1076
+
.text = input,
1077
+
};
1078
+
const expected_event: Event = .{ .key_press = expected_key };
1079
+
1080
+
try testing.expectEqual(input.len, result.n);
1081
+
try testing.expectEqual(expected_event, result.event);
1082
+
}
1083
+
1084
+
test "parse: skin tone emoji" {
1085
+
const alloc = testing.allocator_instance.allocator();
1086
+
const input = "๐๐ฟ";
1087
+
var parser: Parser = .{};
1088
+
const result = try parser.parse(input, alloc);
1089
+
const expected_key: Key = .{
1090
+
.codepoint = Key.multicodepoint,
1091
+
.text = input,
1092
+
};
1093
+
const expected_event: Event = .{ .key_press = expected_key };
1094
+
1095
+
try testing.expectEqual(input.len, result.n);
1096
+
try testing.expectEqual(expected_event, result.event);
1097
+
}
1098
+
1099
+
test "parse: text variation selector" {
1100
+
const alloc = testing.allocator_instance.allocator();
1101
+
// Heavy black heart with text variation selector
1102
+
const input = "โค๏ธ";
1103
+
var parser: Parser = .{};
1104
+
const result = try parser.parse(input, alloc);
1105
+
const expected_key: Key = .{
1106
+
.codepoint = Key.multicodepoint,
1107
+
.text = input,
1108
+
};
1109
+
const expected_event: Event = .{ .key_press = expected_key };
1110
+
1111
+
try testing.expectEqual(input.len, result.n);
1112
+
try testing.expectEqual(expected_event, result.event);
1113
+
}
1114
+
1115
+
test "parse: keycap sequence" {
1116
+
const alloc = testing.allocator_instance.allocator();
1117
+
const input = "1๏ธโฃ";
1118
+
var parser: Parser = .{};
1119
+
const result = try parser.parse(input, alloc);
1120
+
const expected_key: Key = .{
1121
+
.codepoint = Key.multicodepoint,
1122
+
.text = input,
1123
+
};
1124
+
const expected_event: Event = .{ .key_press = expected_key };
1125
+
1126
+
try testing.expectEqual(input.len, result.n);
1127
+
try testing.expectEqual(expected_event, result.event);
1128
+
}
1129
+
1091
1130
test "parse(csi): kitty multi cursor" {
1092
1131
var buf: [1]u8 = undefined;
1093
1132
{
···
1209
1248
try testing.expectEqual(expected.event, result.event);
1210
1249
}
1211
1250
1251
+
test "parse(csi): mouse (negative)" {
1252
+
var buf: [1]u8 = undefined;
1253
+
const input = "\x1b[<35;-50;-100m";
1254
+
const result = parseCsi(input, &buf);
1255
+
const expected: Result = .{
1256
+
.event = .{ .mouse = .{
1257
+
.col = -51,
1258
+
.row = -101,
1259
+
.button = .none,
1260
+
.type = .motion,
1261
+
.mods = .{},
1262
+
} },
1263
+
.n = input.len,
1264
+
};
1265
+
1266
+
try testing.expectEqual(expected.n, result.n);
1267
+
try testing.expectEqual(expected.event, result.event);
1268
+
}
1269
+
1212
1270
test "parse(csi): xterm mouse" {
1213
1271
var buf: [1]u8 = undefined;
1214
1272
const input = "\x1b[M\x20\x21\x21";
···
1230
1288
1231
1289
test "parse: disambiguate shift + space" {
1232
1290
const alloc = testing.allocator_instance.allocator();
1233
-
const grapheme_data = try Graphemes.init(alloc);
1234
-
defer grapheme_data.deinit(alloc);
1235
1291
const input = "\x1b[32;2u";
1236
-
var parser: Parser = .{ .grapheme_data = &grapheme_data };
1292
+
var parser: Parser = .{};
1237
1293
const result = try parser.parse(input, alloc);
1238
1294
const expected_key: Key = .{
1239
1295
.codepoint = ' ',
+4
src/Screen.zig
+4
src/Screen.zig
-25
src/Unicode.zig
-25
src/Unicode.zig
···
1
-
const std = @import("std");
2
-
const Graphemes = @import("Graphemes");
3
-
const DisplayWidth = @import("DisplayWidth");
4
-
5
-
/// A thin wrapper around zg data
6
-
const Unicode = @This();
7
-
8
-
width_data: DisplayWidth,
9
-
10
-
/// initialize all unicode data vaxis may possibly need
11
-
pub fn init(alloc: std.mem.Allocator) !Unicode {
12
-
return .{
13
-
.width_data = try DisplayWidth.init(alloc),
14
-
};
15
-
}
16
-
17
-
/// free all data
18
-
pub fn deinit(self: *const Unicode, alloc: std.mem.Allocator) void {
19
-
self.width_data.deinit(alloc);
20
-
}
21
-
22
-
/// creates a grapheme iterator based on str
23
-
pub fn graphemeIterator(self: *const Unicode, str: []const u8) Graphemes.Iterator {
24
-
return self.width_data.graphemes.iterator(str);
25
-
}
+81
-43
src/Vaxis.zig
+81
-43
src/Vaxis.zig
···
11
11
const Key = @import("Key.zig");
12
12
const Mouse = @import("Mouse.zig");
13
13
const Screen = @import("Screen.zig");
14
-
const Unicode = @import("Unicode.zig");
14
+
const unicode = @import("unicode.zig");
15
15
const Window = @import("Window.zig");
16
16
17
17
const Hyperlink = Cell.Hyperlink;
···
73
73
74
74
// images
75
75
next_img_id: u32 = 1,
76
-
77
-
unicode: Unicode,
78
76
79
77
sgr: enum {
80
78
standard,
···
110
108
.opts = opts,
111
109
.screen = .{},
112
110
.screen_last = try .init(alloc, 0, 0),
113
-
.unicode = try Unicode.init(alloc),
114
111
};
115
112
}
116
113
···
124
121
if (alloc) |a| {
125
122
self.screen.deinit(a);
126
123
self.screen_last.deinit(a);
127
-
self.unicode.deinit(a);
128
124
}
129
125
}
130
126
···
227
223
.width = self.screen.width,
228
224
.height = self.screen.height,
229
225
.screen = &self.screen,
230
-
.unicode = &self.unicode,
231
226
};
232
227
}
233
228
···
365
360
assert(self.screen.buf.len == @as(usize, @intCast(self.screen.width)) * self.screen.height); // correct size
366
361
assert(self.screen.buf.len == self.screen_last.buf.len); // same size
367
362
368
-
// Set up sync before we write anything
369
-
// TODO: optimize sync so we only sync _when we have changes_. This
370
-
// requires a smarter buffered writer, we'll probably have to write
371
-
// our own
372
-
try tty.writeAll(ctlseqs.sync_set);
373
-
errdefer tty.writeAll(ctlseqs.sync_reset) catch {};
363
+
var started: bool = false;
364
+
var sync_active: bool = false;
365
+
errdefer if (sync_active) tty.writeAll(ctlseqs.sync_reset) catch {};
374
366
375
-
// Send the cursor to 0,0
376
-
// TODO: this needs to move after we optimize writes. We only do
377
-
// this if we have an update to make. We also need to hide cursor
378
-
// and then reshow it if needed
379
-
try tty.writeAll(ctlseqs.hide_cursor);
380
-
if (self.state.alt_screen)
381
-
try tty.writeAll(ctlseqs.home)
382
-
else {
383
-
try tty.writeByte('\r');
384
-
for (0..self.state.cursor.row) |_| {
385
-
try tty.writeAll(ctlseqs.ri);
386
-
}
387
-
}
388
-
try tty.writeAll(ctlseqs.sgr_reset);
367
+
const cursor_vis_changed = self.screen.cursor_vis != self.screen_last.cursor_vis;
368
+
const cursor_shape_changed = self.screen.cursor_shape != self.screen_last.cursor_shape;
369
+
const mouse_shape_changed = self.screen.mouse_shape != self.screen_last.mouse_shape;
370
+
const cursor_pos_changed = self.screen.cursor_vis and
371
+
(self.screen.cursor_row != self.state.cursor.row or
372
+
self.screen.cursor_col != self.state.cursor.col);
373
+
const needs_render = self.refresh or cursor_vis_changed or cursor_shape_changed or mouse_shape_changed or cursor_pos_changed;
389
374
390
375
// initialize some variables
391
376
var reposition: bool = false;
···
393
378
var col: u16 = 0;
394
379
var cursor: Style = .{};
395
380
var link: Hyperlink = .{};
396
-
var cursor_pos: struct {
381
+
const CursorPos = struct {
397
382
row: u16 = 0,
398
383
col: u16 = 0,
399
-
} = .{};
384
+
};
385
+
var cursor_pos: CursorPos = .{};
400
386
401
-
// Clear all images
402
-
if (self.caps.kitty_graphics)
403
-
try tty.writeAll(ctlseqs.kitty_graphics_clear);
387
+
const startRender = struct {
388
+
fn run(
389
+
vx: *Vaxis,
390
+
io: *IoWriter,
391
+
cursor_pos_ptr: *CursorPos,
392
+
reposition_ptr: *bool,
393
+
started_ptr: *bool,
394
+
sync_active_ptr: *bool,
395
+
) !void {
396
+
if (started_ptr.*) return;
397
+
started_ptr.* = true;
398
+
sync_active_ptr.* = true;
399
+
// Set up sync before we write anything
400
+
try io.writeAll(ctlseqs.sync_set);
401
+
// Send the cursor to 0,0
402
+
try io.writeAll(ctlseqs.hide_cursor);
403
+
if (vx.state.alt_screen)
404
+
try io.writeAll(ctlseqs.home)
405
+
else {
406
+
try io.writeByte('\r');
407
+
for (0..vx.state.cursor.row) |_| {
408
+
try io.writeAll(ctlseqs.ri);
409
+
}
410
+
}
411
+
try io.writeAll(ctlseqs.sgr_reset);
412
+
cursor_pos_ptr.* = .{};
413
+
reposition_ptr.* = true;
414
+
// Clear all images
415
+
if (vx.caps.kitty_graphics)
416
+
try io.writeAll(ctlseqs.kitty_graphics_clear);
417
+
}
418
+
};
404
419
405
420
// Reset skip flag on all last_screen cells
406
421
for (self.screen_last.buf) |*last_cell| {
407
422
last_cell.skip = false;
408
423
}
409
424
425
+
if (needs_render) {
426
+
try startRender.run(self, tty, &cursor_pos, &reposition, &started, &sync_active);
427
+
}
428
+
410
429
var i: usize = 0;
411
430
while (i < self.screen.buf.len) {
412
431
const cell = self.screen.buf[i];
···
414
433
if (cell.char.width != 0) break :blk cell.char.width;
415
434
416
435
const method: gwidth.Method = self.caps.unicode;
417
-
const width: u16 = @intCast(gwidth.gwidth(cell.char.grapheme, method, &self.unicode.width_data));
436
+
const width: u16 = @intCast(gwidth.gwidth(cell.char.grapheme, method));
418
437
break :blk @max(1, width);
419
438
};
420
439
defer {
···
451
470
try tty.writeAll(ctlseqs.osc8_clear);
452
471
}
453
472
continue;
473
+
}
474
+
if (!started) {
475
+
try startRender.run(self, tty, &cursor_pos, &reposition, &started, &sync_active);
454
476
}
455
477
self.screen_last.buf[i].skipped = false;
456
478
defer {
···
735
757
cursor_pos.col = col + w;
736
758
cursor_pos.row = row;
737
759
}
760
+
if (!started) return;
738
761
if (self.screen.cursor_vis) {
739
762
if (self.state.alt_screen) {
740
763
try tty.print(
···
766
789
self.state.cursor.row = cursor_pos.row;
767
790
self.state.cursor.col = cursor_pos.col;
768
791
}
792
+
self.screen_last.cursor_vis = self.screen.cursor_vis;
769
793
if (self.screen.mouse_shape != self.screen_last.mouse_shape) {
770
794
try tty.print(
771
795
ctlseqs.osc22_mouse_shape,
···
856
880
const ypos = mouse.row;
857
881
const xextra = self.screen.width_pix % self.screen.width;
858
882
const yextra = self.screen.height_pix % self.screen.height;
859
-
const xcell = (self.screen.width_pix - xextra) / self.screen.width;
860
-
const ycell = (self.screen.height_pix - yextra) / self.screen.height;
883
+
const xcell: i16 = @intCast((self.screen.width_pix - xextra) / self.screen.width);
884
+
const ycell: i16 = @intCast((self.screen.height_pix - yextra) / self.screen.height);
861
885
if (xcell == 0 or ycell == 0) return mouse;
862
-
result.col = xpos / xcell;
863
-
result.row = ypos / ycell;
864
-
result.xoffset = xpos % xcell;
865
-
result.yoffset = ypos % ycell;
886
+
result.col = @divFloor(xpos, xcell);
887
+
result.row = @divFloor(ypos, ycell);
888
+
result.xoffset = @intCast(@mod(xpos, xcell));
889
+
result.yoffset = @intCast(@mod(ypos, ycell));
866
890
}
867
891
return result;
868
892
}
···
1000
1024
const buf = switch (format) {
1001
1025
.png => png: {
1002
1026
const png_buf = try arena.allocator().alloc(u8, img.imageByteSize());
1003
-
const png = try img.writeToMemory(png_buf, .{ .png = .{} });
1027
+
const png = try img.writeToMemory(arena.allocator(), png_buf, .{ .png = .{} });
1004
1028
break :png png;
1005
1029
},
1006
1030
.rgb => rgb: {
1007
-
try img.convert(.rgb24);
1031
+
try img.convert(arena.allocator(), .rgb24);
1008
1032
break :rgb img.rawBytes();
1009
1033
},
1010
1034
.rgba => rgba: {
1011
-
try img.convert(.rgba32);
1035
+
try img.convert(arena.allocator(), .rgba32);
1012
1036
break :rgba img.rawBytes();
1013
1037
},
1014
1038
};
···
1032
1056
.path => |path| try zigimg.Image.fromFilePath(alloc, path, &read_buffer),
1033
1057
.mem => |bytes| try zigimg.Image.fromMemory(alloc, bytes),
1034
1058
};
1035
-
defer img.deinit();
1059
+
defer img.deinit(alloc);
1036
1060
return self.transmitImage(alloc, tty, &img, .png);
1037
1061
}
1038
1062
···
1149
1173
if (cell.char.width != 0) break :blk cell.char.width;
1150
1174
1151
1175
const method: gwidth.Method = self.caps.unicode;
1152
-
const width = gwidth.gwidth(cell.char.grapheme, method, &self.unicode.width_data);
1176
+
const width = gwidth.gwidth(cell.char.grapheme, method);
1153
1177
break :blk @max(1, width);
1154
1178
};
1155
1179
defer {
···
1414
1438
try tty.print(ctlseqs.osc7, .{uri.fmt(.{ .scheme = true, .authority = true, .path = true })});
1415
1439
try tty.flush();
1416
1440
}
1441
+
1442
+
test "render: no output when no changes" {
1443
+
var vx = try Vaxis.init(std.testing.allocator, .{});
1444
+
var deinit_writer = std.io.Writer.Allocating.init(std.testing.allocator);
1445
+
defer deinit_writer.deinit();
1446
+
defer vx.deinit(std.testing.allocator, &deinit_writer.writer);
1447
+
1448
+
var render_writer = std.io.Writer.Allocating.init(std.testing.allocator);
1449
+
defer render_writer.deinit();
1450
+
try vx.render(&render_writer.writer);
1451
+
const output = try render_writer.toOwnedSlice();
1452
+
defer std.testing.allocator.free(output);
1453
+
try std.testing.expectEqual(@as(usize, 0), output.len);
1454
+
}
+5
-20
src/Window.zig
+5
-20
src/Window.zig
···
4
4
const Cell = @import("Cell.zig");
5
5
const Mouse = @import("Mouse.zig");
6
6
const Segment = @import("Cell.zig").Segment;
7
-
const Unicode = @import("Unicode.zig");
7
+
const unicode = @import("unicode.zig");
8
8
const gw = @import("gwidth.zig");
9
9
10
10
const Window = @This();
···
25
25
height: u16,
26
26
27
27
screen: *Screen,
28
-
unicode: *const Unicode,
29
28
30
29
/// Creates a new window with offset relative to parent and size clamped to the
31
30
/// parent's size. Windows do not retain a reference to their parent and are
···
50
49
.width = @min(width, max_width),
51
50
.height = @min(height, max_height),
52
51
.screen = self.screen,
53
-
.unicode = self.unicode,
54
52
};
55
53
}
56
54
···
207
205
208
206
/// returns the width of the grapheme. This depends on the terminal capabilities
209
207
pub fn gwidth(self: Window, str: []const u8) u16 {
210
-
return gw.gwidth(str, self.screen.width_method, &self.unicode.width_data);
208
+
return gw.gwidth(str, self.screen.width_method);
211
209
}
212
210
213
211
/// fills the window with the provided cell
···
295
293
.grapheme => {
296
294
var col: u16 = opts.col_offset;
297
295
const overflow: bool = blk: for (segments) |segment| {
298
-
var iter = self.unicode.graphemeIterator(segment.text);
296
+
var iter = unicode.graphemeIterator(segment.text);
299
297
while (iter.next()) |grapheme| {
300
298
if (col >= self.width) {
301
299
row += 1;
···
378
376
col = 0;
379
377
}
380
378
381
-
var grapheme_iterator = self.unicode.graphemeIterator(word);
379
+
var grapheme_iterator = unicode.graphemeIterator(word);
382
380
while (grapheme_iterator.next()) |grapheme| {
383
381
soft_wrapped = false;
384
382
if (row >= self.height) {
···
417
415
.none => {
418
416
var col: u16 = opts.col_offset;
419
417
const overflow: bool = blk: for (segments) |segment| {
420
-
var iter = self.unicode.graphemeIterator(segment.text);
418
+
var iter = unicode.graphemeIterator(segment.text);
421
419
while (iter.next()) |grapheme| {
422
420
if (col >= self.width) break :blk true;
423
421
const s = grapheme.bytes(segment.text);
···
489
487
.width = 20,
490
488
.height = 20,
491
489
.screen = undefined,
492
-
.unicode = undefined,
493
490
};
494
491
495
492
const ch = parent.initChild(1, 1, null, null);
···
506
503
.width = 20,
507
504
.height = 20,
508
505
.screen = undefined,
509
-
.unicode = undefined,
510
506
};
511
507
512
508
const ch = parent.initChild(0, 0, 21, 21);
···
523
519
.width = 20,
524
520
.height = 20,
525
521
.screen = undefined,
526
-
.unicode = undefined,
527
522
};
528
523
529
524
const ch = parent.initChild(10, 10, 21, 21);
···
540
535
.width = 20,
541
536
.height = 20,
542
537
.screen = undefined,
543
-
.unicode = undefined,
544
538
};
545
539
546
540
const ch = parent.initChild(10, 10, 21, 21);
···
557
551
.width = 20,
558
552
.height = 20,
559
553
.screen = undefined,
560
-
.unicode = undefined,
561
554
};
562
555
563
556
const ch = parent.initChild(10, 10, 21, 21);
···
569
562
}
570
563
571
564
test "print: grapheme" {
572
-
const alloc = std.testing.allocator_instance.allocator();
573
-
const unicode = try Unicode.init(alloc);
574
-
defer unicode.deinit(alloc);
575
565
var screen: Screen = .{ .width_method = .unicode };
576
566
const win: Window = .{
577
567
.x_off = 0,
···
581
571
.width = 4,
582
572
.height = 2,
583
573
.screen = &screen,
584
-
.unicode = &unicode,
585
574
};
586
575
const opts: PrintOptions = .{
587
576
.commit = false,
···
636
625
}
637
626
638
627
test "print: word" {
639
-
const alloc = std.testing.allocator_instance.allocator();
640
-
const unicode = try Unicode.init(alloc);
641
-
defer unicode.deinit(alloc);
642
628
var screen: Screen = .{
643
629
.width_method = .unicode,
644
630
};
···
650
636
.width = 4,
651
637
.height = 2,
652
638
.screen = &screen,
653
-
.unicode = &unicode,
654
639
};
655
640
const opts: PrintOptions = .{
656
641
.commit = false,
+172
-35
src/gwidth.zig
+172
-35
src/gwidth.zig
···
1
1
const std = @import("std");
2
2
const unicode = std.unicode;
3
3
const testing = std.testing;
4
-
const DisplayWidth = @import("DisplayWidth");
5
-
const code_point = @import("code_point");
4
+
const uucode = @import("uucode");
6
5
7
6
/// the method to use when calculating the width of a grapheme
8
7
pub const Method = enum {
···
11
10
no_zwj,
12
11
};
13
12
13
+
/// Calculate width from east asian width property and Unicode properties
14
+
fn eawToWidth(cp: u21, eaw: uucode.types.EastAsianWidth) i16 {
15
+
// Based on wcwidth implementation
16
+
// Control characters
17
+
if (cp == 0) return 0;
18
+
if (cp < 32 or (cp >= 0x7f and cp < 0xa0)) return -1;
19
+
20
+
// Use general category for comprehensive zero-width detection
21
+
const gc = uucode.get(.general_category, cp);
22
+
switch (gc) {
23
+
.mark_nonspacing, .mark_enclosing => return 0,
24
+
else => {},
25
+
}
26
+
27
+
// Additional zero-width characters not covered by general category
28
+
if (cp == 0x00ad) return 0; // soft hyphen
29
+
if (cp == 0x200b) return 0; // zero-width space
30
+
if (cp == 0x200c) return 0; // zero-width non-joiner
31
+
if (cp == 0x200d) return 0; // zero-width joiner
32
+
if (cp == 0x2060) return 0; // word joiner
33
+
if (cp == 0x034f) return 0; // combining grapheme joiner
34
+
if (cp == 0xfeff) return 0; // zero-width no-break space (BOM)
35
+
if (cp >= 0x180b and cp <= 0x180d) return 0; // Mongolian variation selectors
36
+
if (cp >= 0xfe00 and cp <= 0xfe0f) return 0; // variation selectors
37
+
if (cp >= 0xe0100 and cp <= 0xe01ef) return 0; // Plane-14 variation selectors
38
+
39
+
// East Asian Width: fullwidth or wide = 2
40
+
// ambiguous in East Asian context = 2, otherwise 1
41
+
// halfwidth, narrow, or neutral = 1
42
+
return switch (eaw) {
43
+
.fullwidth, .wide => 2,
44
+
else => 1,
45
+
};
46
+
}
47
+
14
48
/// returns the width of the provided string, as measured by the method chosen
15
-
pub fn gwidth(str: []const u8, method: Method, data: *const DisplayWidth) u16 {
49
+
pub fn gwidth(str: []const u8, method: Method) u16 {
16
50
switch (method) {
17
51
.unicode => {
18
-
return @intCast(data.strWidth(str));
52
+
var total: u16 = 0;
53
+
var grapheme_iter = uucode.grapheme.Iterator(uucode.utf8.Iterator).init(.init(str));
54
+
55
+
var grapheme_start: usize = 0;
56
+
var prev_break: bool = true;
57
+
58
+
while (grapheme_iter.next()) |result| {
59
+
if (prev_break and !result.is_break) {
60
+
// Start of a new grapheme
61
+
const cp_len: usize = std.unicode.utf8CodepointSequenceLength(result.cp) catch 1;
62
+
grapheme_start = grapheme_iter.i - cp_len;
63
+
}
64
+
65
+
if (result.is_break) {
66
+
// End of a grapheme - calculate its width
67
+
const grapheme_end = grapheme_iter.i;
68
+
const grapheme_bytes = str[grapheme_start..grapheme_end];
69
+
70
+
// Calculate grapheme width
71
+
var g_iter = uucode.utf8.Iterator.init(grapheme_bytes);
72
+
var width: i16 = 0;
73
+
var has_emoji_vs: bool = false;
74
+
var has_text_vs: bool = false;
75
+
var has_emoji_presentation: bool = false;
76
+
var ri_count: u8 = 0;
77
+
78
+
while (g_iter.next()) |cp| {
79
+
// Check for emoji variation selector (U+FE0F)
80
+
if (cp == 0xfe0f) {
81
+
has_emoji_vs = true;
82
+
continue;
83
+
}
84
+
85
+
// Check for text variation selector (U+FE0E)
86
+
if (cp == 0xfe0e) {
87
+
has_text_vs = true;
88
+
continue;
89
+
}
90
+
91
+
// Check if this codepoint has emoji presentation
92
+
if (uucode.get(.is_emoji_presentation, cp)) {
93
+
has_emoji_presentation = true;
94
+
}
95
+
96
+
// Count regional indicators (for flag emojis)
97
+
if (cp >= 0x1F1E6 and cp <= 0x1F1FF) {
98
+
ri_count += 1;
99
+
}
100
+
101
+
const eaw = uucode.get(.east_asian_width, cp);
102
+
const w = eawToWidth(cp, eaw);
103
+
// Take max of non-zero widths
104
+
if (w > 0 and w > width) width = w;
105
+
}
106
+
107
+
// Handle variation selectors and emoji presentation
108
+
if (has_text_vs) {
109
+
// Text presentation explicit - keep width as-is (usually 1)
110
+
width = @max(1, width);
111
+
} else if (has_emoji_vs or has_emoji_presentation or ri_count == 2) {
112
+
// Emoji presentation or flag pair - force width 2
113
+
width = @max(2, width);
114
+
}
115
+
116
+
total += @max(0, width);
117
+
118
+
grapheme_start = grapheme_end;
119
+
}
120
+
prev_break = result.is_break;
121
+
}
122
+
123
+
return total;
19
124
},
20
125
.wcwidth => {
21
126
var total: u16 = 0;
22
-
var iter: code_point.Iterator = .{ .bytes = str };
127
+
var iter = uucode.utf8.Iterator.init(str);
23
128
while (iter.next()) |cp| {
24
-
const w: u16 = switch (cp.code) {
129
+
const w: i16 = switch (cp) {
25
130
// undo an override in zg for emoji skintone selectors
26
-
0x1f3fb...0x1f3ff,
27
-
=> 2,
28
-
else => @max(0, data.codePointWidth(cp.code)),
131
+
0x1f3fb...0x1f3ff => 2,
132
+
else => blk: {
133
+
const eaw = uucode.get(.east_asian_width, cp);
134
+
break :blk eawToWidth(cp, eaw);
135
+
},
29
136
};
30
-
total += w;
137
+
total += @intCast(@max(0, w));
31
138
}
32
139
return total;
33
140
},
···
35
142
var iter = std.mem.splitSequence(u8, str, "\u{200D}");
36
143
var result: u16 = 0;
37
144
while (iter.next()) |s| {
38
-
result += gwidth(s, .unicode, data);
145
+
result += gwidth(s, .unicode);
39
146
}
40
147
return result;
41
148
},
···
43
150
}
44
151
45
152
test "gwidth: a" {
46
-
const alloc = testing.allocator_instance.allocator();
47
-
const data = try DisplayWidth.init(alloc);
48
-
defer data.deinit(alloc);
49
-
try testing.expectEqual(1, gwidth("a", .unicode, &data));
50
-
try testing.expectEqual(1, gwidth("a", .wcwidth, &data));
51
-
try testing.expectEqual(1, gwidth("a", .no_zwj, &data));
153
+
try testing.expectEqual(1, gwidth("a", .unicode));
154
+
try testing.expectEqual(1, gwidth("a", .wcwidth));
155
+
try testing.expectEqual(1, gwidth("a", .no_zwj));
52
156
}
53
157
54
158
test "gwidth: emoji with ZWJ" {
55
-
const alloc = testing.allocator_instance.allocator();
56
-
const data = try DisplayWidth.init(alloc);
57
-
defer data.deinit(alloc);
58
-
try testing.expectEqual(2, gwidth("๐ฉโ๐", .unicode, &data));
59
-
try testing.expectEqual(4, gwidth("๐ฉโ๐", .wcwidth, &data));
60
-
try testing.expectEqual(4, gwidth("๐ฉโ๐", .no_zwj, &data));
159
+
try testing.expectEqual(2, gwidth("๐ฉโ๐", .unicode));
160
+
try testing.expectEqual(4, gwidth("๐ฉโ๐", .wcwidth));
161
+
try testing.expectEqual(4, gwidth("๐ฉโ๐", .no_zwj));
61
162
}
62
163
63
164
test "gwidth: emoji with VS16 selector" {
64
-
const alloc = testing.allocator_instance.allocator();
65
-
const data = try DisplayWidth.init(alloc);
66
-
defer data.deinit(alloc);
67
-
try testing.expectEqual(2, gwidth("\xE2\x9D\xA4\xEF\xB8\x8F", .unicode, &data));
68
-
try testing.expectEqual(1, gwidth("\xE2\x9D\xA4\xEF\xB8\x8F", .wcwidth, &data));
69
-
try testing.expectEqual(2, gwidth("\xE2\x9D\xA4\xEF\xB8\x8F", .no_zwj, &data));
165
+
try testing.expectEqual(2, gwidth("\xE2\x9D\xA4\xEF\xB8\x8F", .unicode));
166
+
try testing.expectEqual(1, gwidth("\xE2\x9D\xA4\xEF\xB8\x8F", .wcwidth));
167
+
try testing.expectEqual(2, gwidth("\xE2\x9D\xA4\xEF\xB8\x8F", .no_zwj));
70
168
}
71
169
72
170
test "gwidth: emoji with skin tone selector" {
73
-
const alloc = testing.allocator_instance.allocator();
74
-
const data = try DisplayWidth.init(alloc);
75
-
defer data.deinit(alloc);
76
-
try testing.expectEqual(2, gwidth("๐๐ฟ", .unicode, &data));
77
-
try testing.expectEqual(4, gwidth("๐๐ฟ", .wcwidth, &data));
78
-
try testing.expectEqual(2, gwidth("๐๐ฟ", .no_zwj, &data));
171
+
try testing.expectEqual(2, gwidth("๐๐ฟ", .unicode));
172
+
try testing.expectEqual(4, gwidth("๐๐ฟ", .wcwidth));
173
+
try testing.expectEqual(2, gwidth("๐๐ฟ", .no_zwj));
174
+
}
175
+
176
+
test "gwidth: zero-width space" {
177
+
try testing.expectEqual(0, gwidth("\u{200B}", .unicode));
178
+
try testing.expectEqual(0, gwidth("\u{200B}", .wcwidth));
179
+
}
180
+
181
+
test "gwidth: zero-width non-joiner" {
182
+
try testing.expectEqual(0, gwidth("\u{200C}", .unicode));
183
+
try testing.expectEqual(0, gwidth("\u{200C}", .wcwidth));
184
+
}
185
+
186
+
test "gwidth: combining marks" {
187
+
// Hebrew combining mark
188
+
try testing.expectEqual(0, gwidth("\u{05B0}", .unicode));
189
+
// Devanagari combining mark
190
+
try testing.expectEqual(0, gwidth("\u{093C}", .unicode));
191
+
}
192
+
193
+
test "gwidth: flag emoji (regional indicators)" {
194
+
// US flag ๐บ๐ธ
195
+
try testing.expectEqual(2, gwidth("๐บ๐ธ", .unicode));
196
+
// UK flag ๐ฌ๐ง
197
+
try testing.expectEqual(2, gwidth("๐ฌ๐ง", .unicode));
198
+
}
199
+
200
+
test "gwidth: text variation selector" {
201
+
// U+2764 (heavy black heart) + U+FE0E (text variation selector)
202
+
// Should be width 1 with text presentation
203
+
try testing.expectEqual(1, gwidth("โค๏ธ", .unicode));
204
+
}
205
+
206
+
test "gwidth: keycap sequence" {
207
+
// Digit 1 + U+FE0F + U+20E3 (combining enclosing keycap)
208
+
// Should be width 2
209
+
try testing.expectEqual(2, gwidth("1๏ธโฃ", .unicode));
210
+
}
211
+
212
+
test "gwidth: base letter with combining mark" {
213
+
// 'a' + combining acute accent (NFD form)
214
+
// Should be width 1 (combining mark is zero-width)
215
+
try testing.expectEqual(1, gwidth("รก", .unicode));
79
216
}
+2
-4
src/main.zig
+2
-4
src/main.zig
···
26
26
pub const widgets = @import("widgets.zig");
27
27
pub const gwidth = @import("gwidth.zig");
28
28
pub const ctlseqs = @import("ctlseqs.zig");
29
-
pub const DisplayWidth = @import("DisplayWidth");
30
29
pub const GraphemeCache = @import("GraphemeCache.zig");
31
-
pub const Graphemes = @import("Graphemes");
32
30
pub const Event = @import("event.zig").Event;
33
-
pub const Unicode = @import("Unicode.zig");
31
+
pub const unicode = @import("unicode.zig");
34
32
35
33
pub const vxfw = @import("vxfw/vxfw.zig");
36
34
···
74
72
ctlseqs.rmcup;
75
73
76
74
gty.writer().writeAll(reset) catch {};
77
-
75
+
gty.writer().flush() catch {};
78
76
gty.deinit();
79
77
}
80
78
}
+48
-46
src/queue.zig
+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
+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
+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
+4
-6
src/vxfw/App.zig
···
80
80
vx.caps.sgr_pixels = false;
81
81
try vx.setMouseMode(tty.writer(), true);
82
82
83
-
// Give DrawContext the unicode data
84
-
vxfw.DrawContext.init(&vx.unicode, vx.screen.width_method);
83
+
vxfw.DrawContext.init(vx.screen.width_method);
85
84
86
85
const framerate: u64 = if (opts.framerate > 0) opts.framerate else 60;
87
86
// Calculate tick rate
···
262
261
.set_mouse_shape => |shape| self.vx.setMouseShape(shape),
263
262
.request_focus => |widget| self.wants_focus = widget,
264
263
.copy_to_clipboard => |content| {
264
+
defer self.allocator.free(content);
265
265
self.vx.copyToSystemClipboard(self.tty.writer(), content, self.allocator) catch |err| {
266
266
switch (err) {
267
267
error.OutOfMemory => return Allocator.Error.OutOfMemory,
···
270
270
};
271
271
},
272
272
.set_title => |title| {
273
+
defer self.allocator.free(title);
273
274
self.vx.setTitle(self.tty.writer(), title) catch |err| {
274
275
std.log.err("set_title error: {}", .{err});
275
276
};
···
531
532
// Find the path to the focused widget. This builds a list that has the first element as the
532
533
// focused widget, and walks backward to the root. It's possible our focused widget is *not*
533
534
// in this tree. If this is the case, we refocus to the root widget
534
-
const has_focus = try self.childHasFocus(allocator, surface);
535
+
_ = try self.childHasFocus(allocator, surface);
535
536
536
-
// We assert that the focused widget *must* be in the widget tree. There is certianly a
537
-
// logic bug in the code somewhere if this is not the case
538
-
assert(has_focus); // Focused widget not found in Surface tree
539
537
if (!self.root.eql(surface.widget)) {
540
538
// If the root of surface is not the initial widget, we append the initial widget
541
539
try self.path_to_focused.append(allocator, self.root);
+1
-2
src/vxfw/Border.zig
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+14
-21
src/vxfw/TextField.zig
···
9
9
const Key = vaxis.Key;
10
10
const Cell = vaxis.Cell;
11
11
const Window = vaxis.Window;
12
-
const Unicode = vaxis.Unicode;
12
+
const unicode = vaxis.unicode;
13
13
14
14
const TextField = @This();
15
15
···
32
32
/// Previous width we drew at
33
33
prev_width: u16 = 0,
34
34
35
-
unicode: *const Unicode,
36
-
37
35
previous_val: []const u8 = "",
38
36
39
37
userdata: ?*anyopaque = null,
40
38
onChange: ?*const fn (?*anyopaque, *vxfw.EventContext, []const u8) anyerror!void = null,
41
39
onSubmit: ?*const fn (?*anyopaque, *vxfw.EventContext, []const u8) anyerror!void = null,
42
40
43
-
pub fn init(alloc: std.mem.Allocator, unicode: *const Unicode) TextField {
41
+
pub fn init(alloc: std.mem.Allocator) TextField {
44
42
return TextField{
45
43
.buf = Buffer.init(alloc),
46
-
.unicode = unicode,
47
44
};
48
45
}
49
46
···
137
134
138
135
/// insert text at the cursor position
139
136
pub fn insertSliceAtCursor(self: *TextField, data: []const u8) std.mem.Allocator.Error!void {
140
-
var iter = self.unicode.graphemeIterator(data);
137
+
var iter = unicode.graphemeIterator(data);
141
138
while (iter.next()) |text| {
142
139
try self.buf.insertSliceAtCursor(text.bytes(data));
143
140
}
···
153
150
pub fn widthToCursor(self: *TextField, ctx: vxfw.DrawContext) u16 {
154
151
var width: u16 = 0;
155
152
const first_half = self.buf.firstHalf();
156
-
var first_iter = self.unicode.graphemeIterator(first_half);
153
+
var first_iter = unicode.graphemeIterator(first_half);
157
154
var i: usize = 0;
158
155
while (first_iter.next()) |grapheme| {
159
156
defer i += 1;
···
168
165
169
166
pub fn cursorLeft(self: *TextField) void {
170
167
// We need to find the size of the last grapheme in the first half
171
-
var iter = self.unicode.graphemeIterator(self.buf.firstHalf());
168
+
var iter = unicode.graphemeIterator(self.buf.firstHalf());
172
169
var len: usize = 0;
173
170
while (iter.next()) |grapheme| {
174
171
len = grapheme.len;
···
177
174
}
178
175
179
176
pub fn cursorRight(self: *TextField) void {
180
-
var iter = self.unicode.graphemeIterator(self.buf.secondHalf());
177
+
var iter = unicode.graphemeIterator(self.buf.secondHalf());
181
178
const grapheme = iter.next() orelse return;
182
179
self.buf.moveGapRight(grapheme.len);
183
180
}
184
181
185
182
pub fn graphemesBeforeCursor(self: *const TextField) u16 {
186
183
const first_half = self.buf.firstHalf();
187
-
var first_iter = self.unicode.graphemeIterator(first_half);
184
+
var first_iter = unicode.graphemeIterator(first_half);
188
185
var i: u16 = 0;
189
186
while (first_iter.next()) |_| {
190
187
i += 1;
···
230
227
self.prev_cursor_col = 0;
231
228
232
229
const first_half = self.buf.firstHalf();
233
-
var first_iter = self.unicode.graphemeIterator(first_half);
230
+
var first_iter = unicode.graphemeIterator(first_half);
234
231
var col: u16 = 0;
235
232
var i: u16 = 0;
236
233
while (first_iter.next()) |grapheme| {
···
259
256
if (i == cursor_idx) self.prev_cursor_col = col;
260
257
}
261
258
const second_half = self.buf.secondHalf();
262
-
var second_iter = self.unicode.graphemeIterator(second_half);
259
+
var second_iter = unicode.graphemeIterator(second_half);
263
260
while (second_iter.next()) |grapheme| {
264
261
if (i < self.draw_offset) {
265
262
i += 1;
···
332
329
333
330
pub fn deleteBeforeCursor(self: *TextField) void {
334
331
// We need to find the size of the last grapheme in the first half
335
-
var iter = self.unicode.graphemeIterator(self.buf.firstHalf());
332
+
var iter = unicode.graphemeIterator(self.buf.firstHalf());
336
333
var len: usize = 0;
337
334
while (iter.next()) |grapheme| {
338
335
len = grapheme.len;
···
341
338
}
342
339
343
340
pub fn deleteAfterCursor(self: *TextField) void {
344
-
var iter = self.unicode.graphemeIterator(self.buf.secondHalf());
341
+
var iter = unicode.graphemeIterator(self.buf.secondHalf());
345
342
const grapheme = iter.next() orelse return;
346
343
self.buf.growGapRight(grapheme.len);
347
344
}
···
384
381
}
385
382
386
383
test "sliceToCursor" {
387
-
const alloc = std.testing.allocator_instance.allocator();
388
-
const unicode = try Unicode.init(alloc);
389
-
defer unicode.deinit(alloc);
390
-
var input = init(alloc, &unicode);
384
+
var input = init(std.testing.allocator);
391
385
defer input.deinit();
392
386
try input.insertSliceAtCursor("hello, world");
393
387
input.cursorLeft();
···
541
535
// Boiler plate draw context init
542
536
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
543
537
defer arena.deinit();
544
-
const ucd = try vaxis.Unicode.init(arena.allocator());
545
-
vxfw.DrawContext.init(&ucd, .unicode);
538
+
vxfw.DrawContext.init(.unicode);
546
539
547
540
// Create some object which reacts to text field changes
548
541
const Foo = struct {
···
572
565
};
573
566
574
567
// Enough boiler plate...Create the text field
575
-
var text_field = TextField.init(std.testing.allocator, &ucd);
568
+
var text_field = TextField.init(std.testing.allocator);
576
569
defer text_field.deinit();
577
570
text_field.onChange = Foo.onChange;
578
571
text_field.onSubmit = Foo.onChange;
+12
-11
src/vxfw/vxfw.zig
+12
-11
src/vxfw/vxfw.zig
···
1
1
const std = @import("std");
2
2
const vaxis = @import("../main.zig");
3
+
const uucode = @import("uucode");
3
4
4
-
const Graphemes = vaxis.Graphemes;
5
5
const testing = std.testing;
6
6
7
7
const assert = std.debug.assert;
···
141
141
try self.addCmd(.{ .request_focus = widget });
142
142
}
143
143
144
+
/// Copy content to clipboard.
145
+
/// content is duplicated using self.alloc.
146
+
/// Caller retains ownership of their copy of content.
144
147
pub fn copyToClipboard(self: *EventContext, content: []const u8) Allocator.Error!void {
145
-
try self.addCmd(.{ .copy_to_clipboard = content });
148
+
try self.addCmd(.{ .copy_to_clipboard = try self.alloc.dupe(u8, content) });
146
149
}
147
150
151
+
/// Set window title.
152
+
/// title is duplicated using self.alloc.
153
+
/// Caller retains ownership of their copy of title.
148
154
pub fn setTitle(self: *EventContext, title: []const u8) Allocator.Error!void {
149
-
try self.addCmd(.{ .set_title = title });
155
+
try self.addCmd(.{ .set_title = try self.alloc.dupe(u8, title) });
150
156
}
151
157
152
158
pub fn queueRefresh(self: *EventContext) Allocator.Error!void {
···
191
197
cell_size: Size,
192
198
193
199
// Unicode stuff
194
-
var unicode: ?*const vaxis.Unicode = null;
195
200
var width_method: vaxis.gwidth.Method = .unicode;
196
201
197
-
pub fn init(ucd: *const vaxis.Unicode, method: vaxis.gwidth.Method) void {
198
-
DrawContext.unicode = ucd;
202
+
pub fn init(method: vaxis.gwidth.Method) void {
199
203
DrawContext.width_method = method;
200
204
}
201
205
202
206
pub fn stringWidth(_: DrawContext, str: []const u8) usize {
203
-
assert(DrawContext.unicode != null); // DrawContext not initialized
204
207
return vaxis.gwidth.gwidth(
205
208
str,
206
209
DrawContext.width_method,
207
-
&DrawContext.unicode.?.width_data,
208
210
);
209
211
}
210
212
211
-
pub fn graphemeIterator(_: DrawContext, str: []const u8) Graphemes.Iterator {
212
-
assert(DrawContext.unicode != null); // DrawContext not initialized
213
-
return DrawContext.unicode.?.graphemeIterator(str);
213
+
pub fn graphemeIterator(_: DrawContext, str: []const u8) vaxis.unicode.GraphemeIterator {
214
+
return vaxis.unicode.graphemeIterator(str);
214
215
}
215
216
216
217
pub fn withConstraints(self: DrawContext, min: Size, max: MaxSize) DrawContext {
+1
-1
src/widgets/Table.zig
+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
+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
+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
+3
-7
src/widgets/View.zig
···
9
9
10
10
const Screen = @import("../Screen.zig");
11
11
const Window = @import("../Window.zig");
12
-
const Unicode = @import("../Unicode.zig");
12
+
const unicode = @import("../unicode.zig");
13
13
const Cell = @import("../Cell.zig");
14
14
15
15
/// View Allocator
···
17
17
18
18
/// Underlying Screen
19
19
screen: Screen,
20
-
21
-
unicode: *const Unicode,
22
20
23
21
/// View Initialization Config
24
22
pub const Config = struct {
···
27
25
};
28
26
29
27
/// Initialize a new View
30
-
pub fn init(alloc: mem.Allocator, unicode: *const Unicode, config: Config) mem.Allocator.Error!View {
28
+
pub fn init(alloc: mem.Allocator, config: Config) mem.Allocator.Error!View {
31
29
return .{
32
30
.alloc = alloc,
33
31
.screen = try Screen.init(alloc, .{
···
36
34
.x_pixel = 0,
37
35
.y_pixel = 0,
38
36
}),
39
-
.unicode = unicode,
40
37
};
41
38
}
42
39
···
49
46
.width = self.screen.width,
50
47
.height = self.screen.height,
51
48
.screen = &self.screen,
52
-
.unicode = self.unicode,
53
49
};
54
50
}
55
51
···
141
137
142
138
/// Returns the width of the grapheme. This depends on the terminal capabilities
143
139
pub fn gwidth(self: View, str: []const u8) u16 {
144
-
return gw.gwidth(str, self.screen.width_method, &self.unicode.width_data);
140
+
return gw.gwidth(str, self.screen.width_method);
145
141
}
146
142
147
143
/// Fills the View with the provided cell
+5
-10
src/widgets/terminal/Terminal.zig
+5
-10
src/widgets/terminal/Terminal.zig
···
10
10
const vaxis = @import("../../main.zig");
11
11
const Winsize = vaxis.Winsize;
12
12
const Screen = @import("Screen.zig");
13
-
const DisplayWidth = @import("DisplayWidth");
14
13
const Key = vaxis.Key;
15
14
const Queue = vaxis.Queue(Event, 16);
16
-
const code_point = @import("code_point");
17
15
const key = @import("key.zig");
18
16
19
17
pub const Event = union(enum) {
···
71
69
// dirty is protected by back_mutex. Only access this field when you hold that mutex
72
70
dirty: bool = false,
73
71
74
-
unicode: *const vaxis.Unicode,
75
72
should_quit: bool = false,
76
73
77
74
mode: Mode = .{},
···
90
87
allocator: std.mem.Allocator,
91
88
argv: []const []const u8,
92
89
env: *const std.process.EnvMap,
93
-
unicode: *const vaxis.Unicode,
94
90
opts: Options,
95
91
write_buf: []u8,
96
92
) !Terminal {
···
120
116
.front_screen = try Screen.init(allocator, opts.winsize.cols, opts.winsize.rows),
121
117
.back_screen_pri = try Screen.init(allocator, opts.winsize.cols, opts.winsize.rows + opts.scrollback_size),
122
118
.back_screen_alt = try Screen.init(allocator, opts.winsize.cols, opts.winsize.rows),
123
-
.unicode = unicode,
124
119
.tab_stops = tabs,
125
120
};
126
121
}
···
278
273
279
274
switch (event) {
280
275
.print => |str| {
281
-
var iter = self.unicode.graphemeIterator(str);
282
-
while (iter.next()) |g| {
283
-
const gr = g.bytes(str);
276
+
var iter = vaxis.unicode.graphemeIterator(str);
277
+
while (iter.next()) |grapheme| {
278
+
const gr = grapheme.bytes(str);
284
279
// TODO: use actual instead of .unicode
285
-
const w = vaxis.gwidth.gwidth(gr, .unicode, &self.unicode.width_data);
280
+
const w = vaxis.gwidth.gwidth(gr, .unicode);
286
281
try self.back_screen.print(gr, @truncate(w), self.mode.autowrap);
287
282
}
288
283
},
···
498
493
var iter = seq.iterator(u16);
499
494
const n = iter.next() orelse 1;
500
495
// TODO: maybe not .unicode
501
-
const w = vaxis.gwidth.gwidth(self.last_printed, .unicode, &self.unicode.width_data);
496
+
const w = vaxis.gwidth.gwidth(self.last_printed, .unicode);
502
497
var i: usize = 0;
503
498
while (i < n) : (i += 1) {
504
499
try self.back_screen.print(self.last_printed, @truncate(w), self.mode.autowrap);