+8
-7
README.md
+8
-7
README.md
···
290
290
const alloc = gpa.allocator();
291
291
292
292
// Initialize a tty
293
-
var tty = try vaxis.Tty.init();
293
+
var buffer: [1024]u8 = undefined;
294
+
var tty = try vaxis.Tty.init(&buffer);
294
295
defer tty.deinit();
295
296
296
297
// Initialize Vaxis
297
298
var vx = try vaxis.init(alloc, .{});
298
299
// deinit takes an optional allocator. If your program is exiting, you can
299
300
// choose to pass a null allocator to save some exit time.
300
-
defer vx.deinit(alloc, tty.anyWriter());
301
+
defer vx.deinit(alloc, tty.writer());
301
302
302
303
303
304
// The event loop requires an intrusive init. We create an instance with
···
317
318
defer loop.stop();
318
319
319
320
// Optionally enter the alternate screen
320
-
try vx.enterAltScreen(tty.anyWriter());
321
+
try vx.enterAltScreen(tty.writer());
321
322
322
323
// We'll adjust the color index every keypress for the border
323
324
var color_idx: u8 = 0;
324
325
325
326
// init our text input widget. The text input widget needs an allocator to
326
327
// store the contents of the input
327
-
var text_input = TextInput.init(alloc, &vx.unicode);
328
+
var text_input = TextInput.init(alloc);
328
329
defer text_input.deinit();
329
330
330
331
// Sends queries to terminal to detect certain features. This should always
331
332
// be called after entering the alt screen, if you are using the alt screen
332
-
try vx.queryTerminal(tty.anyWriter(), 1 * std.time.ns_per_s);
333
+
try vx.queryTerminal(tty.writer(), 1 * std.time.ns_per_s);
333
334
334
335
while (true) {
335
336
// nextEvent blocks until an event is in the queue
···
365
366
// more than one byte will incur an allocation on the first render
366
367
// after it is drawn. Thereafter, it will not allocate unless the
367
368
// screen is resized
368
-
.winsize => |ws| try vx.resize(alloc, tty.anyWriter(), ws),
369
+
.winsize => |ws| try vx.resize(alloc, tty.writer(), ws),
369
370
else => {},
370
371
}
371
372
···
401
402
402
403
// Render the screen. Using a buffered writer will offer much better
403
404
// performance, but is not required
404
-
try vx.render(tty.anyWriter());
405
+
try vx.render(tty.writer());
405
406
}
406
407
}
407
408
```
+2
-2
USAGE.md
+2
-2
USAGE.md
···
247
247
self.vx.caps.color_scheme_updates = true;
248
248
},
249
249
.cap_da1 => {
250
-
self.vx.enableDetectedFeatures(self.tty.anyWriter()) catch |err| {
250
+
self.vx.enableDetectedFeatures(self.tty.writer()) catch |err| {
251
251
log.err("couldn't enable features: {}", .{err});
252
252
};
253
253
},
···
328
328
}
329
329
330
330
pub fn deinit(self: *@This(), vx: *vaxis.Vaxis, tty: *vaxis.Tty) void {
331
-
vx.deviceStatusReport(tty.anyWriter()) catch {};
331
+
vx.deviceStatusReport(tty.writer()) catch {};
332
332
if (self.winsize_task) |task| task.cancel();
333
333
if (self.reader_task) |task| task.cancel();
334
334
self.source.deinit();
+62
bench/bench.zig
+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 = .{
+5
-5
examples/cli.zig
+5
-5
examples/cli.zig
···
19
19
defer tty.deinit();
20
20
21
21
var vx = try vaxis.init(alloc, .{});
22
-
defer vx.deinit(alloc, tty.anyWriter());
22
+
defer vx.deinit(alloc, tty.writer());
23
23
24
24
var loop: vaxis.Loop(Event) = .{ .tty = &tty, .vaxis = &vx };
25
25
try loop.init();
···
27
27
try loop.start();
28
28
defer loop.stop();
29
29
30
-
try vx.queryTerminal(tty.anyWriter(), 1 * std.time.ns_per_s);
30
+
try vx.queryTerminal(tty.writer(), 1 * std.time.ns_per_s);
31
31
32
-
var text_input = TextInput.init(alloc, &vx.unicode);
32
+
var text_input = TextInput.init(alloc);
33
33
defer text_input.deinit();
34
34
35
35
var selected_option: ?usize = null;
···
75
75
}
76
76
},
77
77
.winsize => |ws| {
78
-
try vx.resize(alloc, tty.anyWriter(), ws);
78
+
try vx.resize(alloc, tty.writer(), ws);
79
79
},
80
80
else => {},
81
81
}
···
96
96
_ = win.print(&seg, .{ .row_offset = @intCast(j + 1) });
97
97
}
98
98
}
99
-
try vx.render(tty.anyWriter());
99
+
try vx.render(tty.writer());
100
100
}
101
101
}
102
102
+70
-60
examples/fuzzy.zig
+70
-60
examples/fuzzy.zig
···
4
4
5
5
const Model = struct {
6
6
list: std.ArrayList(vxfw.Text),
7
+
/// Memory owned by .arena
7
8
filtered: std.ArrayList(vxfw.RichText),
8
9
list_view: vxfw.ListView,
9
10
text_field: vxfw.TextField,
11
+
12
+
/// Used for filtered RichText Spans and result
13
+
arena: std.heap.ArenaAllocator,
14
+
filtered: std.ArrayList(vxfw.RichText),
10
15
result: []const u8,
11
-
unicode_data: *const vaxis.Unicode,
16
+
17
+
pub fn init(gpa: std.mem.Allocator) !*Model {
18
+
const model = try gpa.create(Model);
19
+
errdefer gpa.destroy(model);
20
+
21
+
model.* = .{
22
+
.list = .empty,
23
+
.filtered = .empty,
24
+
.list_view = .{
25
+
.children = .{
26
+
.builder = .{
27
+
.userdata = model,
28
+
.buildFn = Model.widgetBuilder,
29
+
},
30
+
},
31
+
},
32
+
.text_field = .{
33
+
.buf = vxfw.TextField.Buffer.init(gpa),
34
+
.userdata = model,
35
+
.onChange = Model.onChange,
36
+
.onSubmit = Model.onSubmit,
37
+
},
38
+
.result = "",
39
+
.arena = std.heap.ArenaAllocator.init(gpa),
40
+
};
41
+
42
+
return model;
43
+
}
12
44
13
-
/// Used for filtered RichText Spans
14
-
arena: std.heap.ArenaAllocator,
45
+
pub fn deinit(self: *Model, gpa: std.mem.Allocator) void {
46
+
self.arena.deinit();
47
+
self.text_field.deinit();
48
+
self.list.deinit(gpa);
49
+
gpa.destroy(self);
50
+
}
15
51
16
52
pub fn widget(self: *Model) vxfw.Widget {
17
53
return .{
···
26
62
switch (event) {
27
63
.init => {
28
64
// Initialize the filtered list
29
-
const allocator = self.arena.allocator();
65
+
const arena = self.arena.allocator();
30
66
for (self.list.items) |line| {
31
-
var spans = std.ArrayList(vxfw.RichText.TextSpan){};
67
+
var spans = std.ArrayList(vxfw.RichText.TextSpan).empty;
32
68
const span: vxfw.RichText.TextSpan = .{ .text = line.text };
33
-
try spans.append(allocator, span);
34
-
try self.filtered.append(allocator, .{ .text = spans.items });
69
+
try spans.append(arena, span);
70
+
try self.filtered.append(arena, .{ .text = spans.items });
35
71
}
36
72
37
73
return ctx.requestFocus(self.text_field.widget());
···
100
136
fn onChange(maybe_ptr: ?*anyopaque, _: *vxfw.EventContext, str: []const u8) anyerror!void {
101
137
const ptr = maybe_ptr orelse return;
102
138
const self: *Model = @ptrCast(@alignCast(ptr));
103
-
const allocator = self.arena.allocator();
104
-
self.filtered.clearAndFree(allocator);
139
+
const arena = self.arena.allocator();
140
+
self.filtered.clearAndFree(arena);
105
141
_ = self.arena.reset(.free_all);
106
142
107
143
const hasUpper = for (str) |b| {
···
115
151
const tgt = if (hasUpper)
116
152
item.text
117
153
else
118
-
try toLower(allocator, item.text);
154
+
try toLower(arena, item.text);
119
155
120
-
var spans = std.ArrayList(vxfw.RichText.TextSpan){};
156
+
var spans = std.ArrayList(vxfw.RichText.TextSpan).empty;
121
157
var i: usize = 0;
122
-
var iter = self.unicode_data.graphemeIterator(str);
158
+
var iter = vaxis.unicode.graphemeIterator(str);
123
159
while (iter.next()) |g| {
124
160
if (std.mem.indexOfPos(u8, tgt, i, g.bytes(str))) |idx| {
125
161
const up_to_here: vxfw.RichText.TextSpan = .{ .text = item.text[i..idx] };
···
127
163
.text = item.text[idx .. idx + g.len],
128
164
.style = .{ .fg = .{ .index = 4 }, .reverse = true },
129
165
};
130
-
try spans.append(allocator, up_to_here);
131
-
try spans.append(allocator, match);
166
+
try spans.append(arena, up_to_here);
167
+
try spans.append(arena, match);
132
168
i = idx + g.len;
133
169
} else continue :outer;
134
170
}
135
171
const up_to_here: vxfw.RichText.TextSpan = .{ .text = item.text[i..] };
136
-
try spans.append(allocator, up_to_here);
137
-
try self.filtered.append(allocator, .{ .text = spans.items });
172
+
try spans.append(arena, up_to_here);
173
+
try self.filtered.append(arena, .{ .text = spans.items });
138
174
}
139
175
self.list_view.scroll.top = 0;
140
176
self.list_view.scroll.offset = 0;
···
146
182
const self: *Model = @ptrCast(@alignCast(ptr));
147
183
if (self.list_view.cursor < self.filtered.items.len) {
148
184
const selected = self.filtered.items[self.list_view.cursor];
149
-
const allocator = self.arena.allocator();
150
-
var result = std.ArrayList(u8){};
185
+
const arena = self.arena.allocator();
186
+
var result = std.ArrayList(u8).empty;
151
187
for (selected.text) |span| {
152
-
try result.appendSlice(allocator, span.text);
188
+
try result.appendSlice(arena, span.text);
153
189
}
154
190
self.result = result.items;
155
191
}
···
157
193
}
158
194
};
159
195
160
-
fn toLower(allocator: std.mem.Allocator, src: []const u8) std.mem.Allocator.Error![]const u8 {
161
-
const lower = try allocator.alloc(u8, src.len);
196
+
fn toLower(arena: std.mem.Allocator, src: []const u8) std.mem.Allocator.Error![]const u8 {
197
+
const lower = try arena.alloc(u8, src.len);
162
198
for (src, 0..) |b, i| {
163
199
lower[i] = std.ascii.toLower(b);
164
200
}
···
166
202
}
167
203
168
204
pub fn main() !void {
169
-
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
170
-
defer _ = gpa.deinit();
205
+
var debug_allocator = std.heap.GeneralPurposeAllocator(.{}){};
206
+
defer _ = debug_allocator.deinit();
171
207
172
-
const allocator = gpa.allocator();
208
+
const gpa = debug_allocator.allocator();
173
209
174
-
var app = try vxfw.App.init(allocator);
210
+
var app = try vxfw.App.init(gpa);
175
211
errdefer app.deinit();
176
212
177
-
const model = try allocator.create(Model);
178
-
defer allocator.destroy(model);
179
-
model.* = .{
180
-
.list = std.ArrayList(vxfw.Text){},
181
-
.filtered = std.ArrayList(vxfw.RichText){},
182
-
.list_view = .{
183
-
.children = .{
184
-
.builder = .{
185
-
.userdata = model,
186
-
.buildFn = Model.widgetBuilder,
187
-
},
188
-
},
189
-
},
190
-
.text_field = .{
191
-
.buf = vxfw.TextField.Buffer.init(allocator),
192
-
.unicode = &app.vx.unicode,
193
-
.userdata = model,
194
-
.onChange = Model.onChange,
195
-
.onSubmit = Model.onSubmit,
196
-
},
197
-
.result = "",
198
-
.arena = std.heap.ArenaAllocator.init(allocator),
199
-
.unicode_data = &app.vx.unicode,
200
-
};
201
-
defer model.text_field.deinit();
202
-
defer model.list.deinit(allocator);
203
-
defer model.filtered.deinit(allocator);
204
-
defer model.arena.deinit();
213
+
const model = try Model.init(gpa);
214
+
defer model.deinit(gpa);
205
215
206
216
// Run the command
207
-
var fd = std.process.Child.init(&.{"fd"}, allocator);
217
+
var fd = std.process.Child.init(&.{"fd"}, gpa);
208
218
fd.stdout_behavior = .Pipe;
209
219
fd.stderr_behavior = .Pipe;
210
-
var stdout = std.ArrayList(u8){};
211
-
var stderr = std.ArrayList(u8){};
212
-
defer stdout.deinit(allocator);
213
-
defer stderr.deinit(allocator);
220
+
var stdout = std.ArrayList(u8).empty;
221
+
var stderr = std.ArrayList(u8).empty;
222
+
defer stdout.deinit(gpa);
223
+
defer stderr.deinit(gpa);
214
224
try fd.spawn();
215
-
try fd.collectOutput(allocator, &stdout, &stderr, 10_000_000);
225
+
try fd.collectOutput(gpa, &stdout, &stderr, 10_000_000);
216
226
_ = try fd.wait();
217
227
218
228
var iter = std.mem.splitScalar(u8, stdout.items, '\n');
219
229
while (iter.next()) |line| {
220
230
if (line.len == 0) continue;
221
-
try model.list.append(allocator, .{ .text = line });
231
+
try model.list.append(gpa, .{ .text = line });
222
232
}
223
233
224
234
try app.run(model.widget(), .{});
+11
-11
examples/image.zig
+11
-11
examples/image.zig
···
23
23
defer tty.deinit();
24
24
25
25
var vx = try vaxis.init(alloc, .{});
26
-
defer vx.deinit(alloc, tty.anyWriter());
26
+
defer vx.deinit(alloc, tty.writer());
27
27
28
28
var loop: vaxis.Loop(Event) = .{ .tty = &tty, .vaxis = &vx };
29
29
try loop.init();
···
31
31
try loop.start();
32
32
defer loop.stop();
33
33
34
-
try vx.enterAltScreen(tty.anyWriter());
35
-
try vx.queryTerminal(tty.anyWriter(), 1 * std.time.ns_per_s);
34
+
try vx.enterAltScreen(tty.writer());
35
+
try vx.queryTerminal(tty.writer(), 1 * std.time.ns_per_s);
36
36
37
37
var read_buffer: [1024 * 1024]u8 = undefined; // 1MB buffer
38
38
var img1 = try vaxis.zigimg.Image.fromFilePath(alloc, "examples/zig.png", &read_buffer);
39
-
defer img1.deinit();
39
+
defer img1.deinit(alloc);
40
40
41
41
const imgs = [_]vaxis.Image{
42
-
try vx.transmitImage(alloc, tty.anyWriter(), &img1, .rgba),
42
+
try vx.transmitImage(alloc, tty.writer(), &img1, .rgba),
43
43
// var img1 = try vaxis.zigimg.Image.fromFilePath(alloc, "examples/zig.png");
44
-
// try vx.loadImage(alloc, tty.anyWriter(), .{ .path = "examples/zig.png" }),
45
-
try vx.loadImage(alloc, tty.anyWriter(), .{ .path = "examples/vaxis.png" }),
44
+
// try vx.loadImage(alloc, tty.writer(), .{ .path = "examples/zig.png" }),
45
+
try vx.loadImage(alloc, tty.writer(), .{ .path = "examples/vaxis.png" }),
46
46
};
47
-
defer vx.freeImage(tty.anyWriter(), imgs[0].id);
48
-
defer vx.freeImage(tty.anyWriter(), imgs[1].id);
47
+
defer vx.freeImage(tty.writer(), imgs[0].id);
48
+
defer vx.freeImage(tty.writer(), imgs[1].id);
49
49
50
50
var n: usize = 0;
51
51
···
64
64
else if (key.matches('k', .{}))
65
65
clip_y -|= 1;
66
66
},
67
-
.winsize => |ws| try vx.resize(alloc, tty.anyWriter(), ws),
67
+
.winsize => |ws| try vx.resize(alloc, tty.writer(), ws),
68
68
}
69
69
70
70
n = (n + 1) % imgs.len;
···
78
78
.y = clip_y,
79
79
} });
80
80
81
-
try vx.render(tty.anyWriter());
81
+
try vx.render(tty.writer());
82
82
}
83
83
}
+99
examples/list_view.zig
+99
examples/list_view.zig
···
1
+
const std = @import("std");
2
+
const vaxis = @import("vaxis");
3
+
const vxfw = vaxis.vxfw;
4
+
5
+
const Text = vxfw.Text;
6
+
const ListView = vxfw.ListView;
7
+
const Widget = vxfw.Widget;
8
+
9
+
const Model = struct {
10
+
list_view: ListView,
11
+
12
+
pub fn widget(self: *Model) Widget {
13
+
return .{
14
+
.userdata = self,
15
+
.eventHandler = Model.typeErasedEventHandler,
16
+
.drawFn = Model.typeErasedDrawFn,
17
+
};
18
+
}
19
+
20
+
pub fn typeErasedEventHandler(ptr: *anyopaque, ctx: *vxfw.EventContext, event: vxfw.Event) anyerror!void {
21
+
const self: *Model = @ptrCast(@alignCast(ptr));
22
+
try ctx.requestFocus(self.list_view.widget());
23
+
switch (event) {
24
+
.key_press => |key| {
25
+
if (key.matches('q', .{}) or key.matchExact('c', .{ .ctrl = true })) {
26
+
ctx.quit = true;
27
+
return;
28
+
}
29
+
},
30
+
else => {},
31
+
}
32
+
}
33
+
34
+
fn typeErasedDrawFn(ptr: *anyopaque, ctx: vxfw.DrawContext) std.mem.Allocator.Error!vxfw.Surface {
35
+
const self: *Model = @ptrCast(@alignCast(ptr));
36
+
const max = ctx.max.size();
37
+
38
+
const list_view: vxfw.SubSurface = .{
39
+
.origin = .{ .row = 1, .col = 1 },
40
+
.surface = try self.list_view.draw(ctx),
41
+
};
42
+
43
+
const children = try ctx.arena.alloc(vxfw.SubSurface, 1);
44
+
children[0] = list_view;
45
+
46
+
return .{
47
+
.size = max,
48
+
.widget = self.widget(),
49
+
.buffer = &.{},
50
+
.children = children,
51
+
};
52
+
}
53
+
};
54
+
55
+
pub fn main() !void {
56
+
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
57
+
defer _ = gpa.deinit();
58
+
59
+
const allocator = gpa.allocator();
60
+
61
+
var app = try vxfw.App.init(allocator);
62
+
defer app.deinit();
63
+
64
+
const model = try allocator.create(Model);
65
+
defer allocator.destroy(model);
66
+
67
+
const n = 80;
68
+
var texts = try std.ArrayList(Widget).initCapacity(allocator, n);
69
+
70
+
var allocs = try std.ArrayList(*Text).initCapacity(allocator, n);
71
+
defer {
72
+
for (allocs.items) |tw| {
73
+
allocator.free(tw.text);
74
+
allocator.destroy(tw);
75
+
}
76
+
allocs.deinit(allocator);
77
+
texts.deinit(allocator);
78
+
}
79
+
80
+
for (0..n) |i| {
81
+
const t = std.fmt.allocPrint(allocator, "List Item {d}", .{i}) catch "placeholder";
82
+
const tw = try allocator.create(Text);
83
+
tw.* = .{ .text = t };
84
+
_ = try allocs.append(allocator, tw);
85
+
_ = try texts.append(allocator, tw.widget());
86
+
}
87
+
88
+
model.* = .{
89
+
.list_view = .{
90
+
.wheel_scroll = 3,
91
+
.scroll = .{
92
+
.wants_cursor = true,
93
+
},
94
+
.children = .{ .slice = texts.items },
95
+
},
96
+
};
97
+
98
+
try app.run(model.widget(), .{});
99
+
}
+5
-5
examples/main.zig
+5
-5
examples/main.zig
···
19
19
defer tty.deinit();
20
20
21
21
var vx = try vaxis.init(alloc, .{});
22
-
defer vx.deinit(alloc, tty.anyWriter());
22
+
defer vx.deinit(alloc, tty.writer());
23
23
24
24
var loop: vaxis.Loop(Event) = .{ .tty = &tty, .vaxis = &vx };
25
25
try loop.init();
···
28
28
defer loop.stop();
29
29
30
30
// Optionally enter the alternate screen
31
-
try vx.enterAltScreen(tty.anyWriter());
32
-
try vx.queryTerminal(tty.anyWriter(), 1 * std.time.ns_per_s);
31
+
try vx.enterAltScreen(tty.writer());
32
+
try vx.queryTerminal(tty.writer(), 1 * std.time.ns_per_s);
33
33
34
34
// We'll adjust the color index every keypress
35
35
var color_idx: u8 = 0;
···
66
66
}
67
67
},
68
68
.winsize => |ws| {
69
-
try vx.resize(alloc, tty.anyWriter(), ws);
69
+
try vx.resize(alloc, tty.writer(), ws);
70
70
},
71
71
else => {},
72
72
}
···
114
114
child.writeCell(@intCast(i), scale, second_cell);
115
115
}
116
116
// Render the screen
117
-
try vx.render(tty.anyWriter());
117
+
try vx.render(tty.writer());
118
118
}
119
119
}
120
120
+11
-15
examples/table.zig
+11
-15
examples/table.zig
···
21
21
22
22
// Users set up below the main function
23
23
const users_buf = try alloc.dupe(User, users[0..]);
24
-
var user_list = std.ArrayList(User).fromOwnedSlice(users_buf);
25
-
defer user_list.deinit(alloc);
26
-
var user_mal = std.MultiArrayList(User){};
27
-
for (users_buf[0..]) |user| try user_mal.append(alloc, user);
28
-
defer user_mal.deinit(alloc);
29
24
30
25
var buffer: [1024]u8 = undefined;
31
26
var tty = try vaxis.Tty.init(&buffer);
32
27
defer tty.deinit();
33
-
const tty_writer = tty.anyWriter();
28
+
const tty_writer = tty.writer();
34
29
var vx = try vaxis.init(alloc, .{
35
30
.kitty_keyboard_flags = .{ .report_events = true },
36
31
});
37
-
defer vx.deinit(alloc, tty.anyWriter());
32
+
defer vx.deinit(alloc, tty.writer());
38
33
39
34
var loop: vaxis.Loop(union(enum) {
40
35
key_press: vaxis.Key,
···
44
39
try loop.init();
45
40
try loop.start();
46
41
defer loop.stop();
47
-
try vx.enterAltScreen(tty.anyWriter());
48
-
try vx.queryTerminal(tty.anyWriter(), 250 * std.time.ns_per_ms);
42
+
try vx.enterAltScreen(tty.writer());
43
+
try vx.queryTerminal(tty.writer(), 250 * std.time.ns_per_ms);
49
44
50
45
const logo =
51
46
\\โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
···
66
61
};
67
62
var title_segs = [_]vaxis.Cell.Segment{ title_logo, title_info, title_disclaimer };
68
63
69
-
var cmd_input = vaxis.widgets.TextInput.init(alloc, &vx.unicode);
64
+
var cmd_input = vaxis.widgets.TextInput.init(alloc);
70
65
defer cmd_input.deinit();
71
66
72
67
// Colors
···
178
173
mem.eql(u8, ":quit", cmd) or
179
174
mem.eql(u8, ":exit", cmd)) return;
180
175
if (mem.eql(u8, "G", cmd)) {
181
-
demo_tbl.row = @intCast(user_list.items.len - 1);
176
+
demo_tbl.row = @intCast(users_buf.len - 1);
182
177
active = .mid;
183
178
}
184
179
if (cmd.len >= 2 and mem.eql(u8, "gg", cmd[0..2])) {
···
191
186
}
192
187
moving = false;
193
188
},
194
-
.winsize => |ws| try vx.resize(alloc, tty.anyWriter(), ws),
189
+
.winsize => |ws| try vx.resize(alloc, tty.writer(), ws),
195
190
else => {},
196
191
}
197
192
···
277
272
.width = win.width,
278
273
.height = win.height - (top_bar.height + 1),
279
274
});
280
-
if (user_list.items.len > 0) {
275
+
if (users_buf.len > 0) {
281
276
demo_tbl.active = active == .mid;
282
277
try vaxis.widgets.Table.drawTable(
283
-
event_alloc,
278
+
null,
279
+
// event_alloc,
284
280
middle_bar,
285
281
//users_buf[0..],
286
282
//user_list,
287
-
user_mal,
283
+
users_buf,
288
284
&demo_tbl,
289
285
);
290
286
}
+7
-7
examples/text_input.zig
+7
-7
examples/text_input.zig
···
36
36
37
37
// Use a buffered writer for better performance. There are a lot of writes
38
38
// in the render loop and this can have a significant savings
39
-
const writer = tty.anyWriter();
39
+
const writer = tty.writer();
40
40
41
41
// Initialize Vaxis
42
42
var vx = try vaxis.init(alloc, .{
43
43
.kitty_keyboard_flags = .{ .report_events = true },
44
44
});
45
-
defer vx.deinit(alloc, tty.anyWriter());
45
+
defer vx.deinit(alloc, tty.writer());
46
46
47
47
var loop: vaxis.Loop(Event) = .{
48
48
.vaxis = &vx,
···
63
63
64
64
// init our text input widget. The text input widget needs an allocator to
65
65
// store the contents of the input
66
-
var text_input = TextInput.init(alloc, &vx.unicode);
66
+
var text_input = TextInput.init(alloc);
67
67
defer text_input.deinit();
68
68
69
69
try vx.setMouseMode(writer, true);
···
71
71
try writer.flush();
72
72
// Sends queries to terminal to detect certain features. This should
73
73
// _always_ be called, but is left to the application to decide when
74
-
try vx.queryTerminal(tty.anyWriter(), 1 * std.time.ns_per_s);
74
+
try vx.queryTerminal(tty.writer(), 1 * std.time.ns_per_s);
75
75
76
76
// The main event loop. Vaxis provides a thread safe, blocking, buffered
77
77
// queue which can serve as the primary event queue for an application
···
92
92
} else if (key.matches('l', .{ .ctrl = true })) {
93
93
vx.queueRefresh();
94
94
} else if (key.matches('n', .{ .ctrl = true })) {
95
-
try vx.notify(tty.anyWriter(), "vaxis", "hello from vaxis");
95
+
try vx.notify(tty.writer(), "vaxis", "hello from vaxis");
96
96
loop.stop();
97
97
var child = std.process.Child.init(&.{"nvim"}, alloc);
98
98
_ = try child.spawnAndWait();
99
99
try loop.start();
100
-
try vx.enterAltScreen(tty.anyWriter());
100
+
try vx.enterAltScreen(tty.writer());
101
101
vx.queueRefresh();
102
102
} else if (key.matches(vaxis.Key.enter, .{}) or key.matches('j', .{ .ctrl = true })) {
103
103
text_input.clearAndFree();
···
121
121
// more than one byte will incur an allocation on the first render
122
122
// after it is drawn. Thereafter, it will not allocate unless the
123
123
// screen is resized
124
-
.winsize => |ws| try vx.resize(alloc, tty.anyWriter(), ws),
124
+
.winsize => |ws| try vx.resize(alloc, tty.writer(), ws),
125
125
else => {},
126
126
}
127
127
+66
examples/text_view.zig
+66
examples/text_view.zig
···
1
+
const std = @import("std");
2
+
const log = std.log.scoped(.main);
3
+
const vaxis = @import("vaxis");
4
+
5
+
const TextView = vaxis.widgets.TextView;
6
+
7
+
const Event = union(enum) {
8
+
key_press: vaxis.Key,
9
+
winsize: vaxis.Winsize,
10
+
};
11
+
12
+
pub fn main() !void {
13
+
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
14
+
15
+
defer {
16
+
const deinit_status = gpa.deinit();
17
+
if (deinit_status == .leak) {
18
+
log.err("memory leak", .{});
19
+
}
20
+
}
21
+
22
+
const alloc = gpa.allocator();
23
+
var buffer: [1024]u8 = undefined;
24
+
var tty = try vaxis.Tty.init(&buffer);
25
+
defer tty.deinit();
26
+
var vx = try vaxis.init(alloc, .{});
27
+
defer vx.deinit(alloc, tty.writer());
28
+
var loop: vaxis.Loop(Event) = .{
29
+
.vaxis = &vx,
30
+
.tty = &tty,
31
+
};
32
+
try loop.init();
33
+
try loop.start();
34
+
defer loop.stop();
35
+
try vx.enterAltScreen(tty.writer());
36
+
try vx.queryTerminal(tty.writer(), 20 * std.time.ns_per_s);
37
+
var text_view = TextView{};
38
+
var text_view_buffer = TextView.Buffer{};
39
+
defer text_view_buffer.deinit(alloc);
40
+
try text_view_buffer.append(alloc, .{ .bytes = "Press Enter to add a line, Up/Down to scroll, 'c' to close." });
41
+
42
+
var counter: i32 = 0;
43
+
var lineBuf: [128]u8 = undefined;
44
+
45
+
while (true) {
46
+
const event = loop.nextEvent();
47
+
switch (event) {
48
+
.key_press => |key| {
49
+
// Close demo
50
+
if (key.matches('c', .{})) break;
51
+
if (key.matches(vaxis.Key.enter, .{})) {
52
+
counter += 1;
53
+
const new_content = try std.fmt.bufPrint(&lineBuf, "\nLine {d}", .{counter});
54
+
try text_view_buffer.append(alloc, .{ .bytes = new_content });
55
+
}
56
+
text_view.input(key);
57
+
},
58
+
.winsize => |ws| try vx.resize(alloc, tty.writer(), ws),
59
+
}
60
+
const win = vx.window();
61
+
win.clear();
62
+
text_view.draw(win, text_view_buffer);
63
+
try vx.render(tty.writer());
64
+
try tty.writer.flush();
65
+
}
66
+
}
+8
-8
examples/vaxis.zig
+8
-8
examples/vaxis.zig
···
25
25
defer tty.deinit();
26
26
27
27
var vx = try vaxis.init(alloc, .{});
28
-
defer vx.deinit(alloc, tty.anyWriter());
28
+
defer vx.deinit(alloc, tty.writer());
29
29
30
30
var loop: vaxis.Loop(Event) = .{ .tty = &tty, .vaxis = &vx };
31
31
try loop.init();
···
33
33
try loop.start();
34
34
defer loop.stop();
35
35
36
-
try vx.enterAltScreen(tty.anyWriter());
37
-
try vx.queryTerminal(tty.anyWriter(), 1 * std.time.ns_per_s);
36
+
try vx.enterAltScreen(tty.writer());
37
+
try vx.queryTerminal(tty.writer(), 1 * std.time.ns_per_s);
38
38
39
-
try vx.queryColor(tty.anyWriter(), .fg);
40
-
try vx.queryColor(tty.anyWriter(), .bg);
39
+
try vx.queryColor(tty.writer(), .fg);
40
+
try vx.queryColor(tty.writer(), .bg);
41
41
var pct: u8 = 0;
42
42
var dir: enum {
43
43
up,
···
53
53
switch (event) {
54
54
.key_press => |key| if (key.matches('c', .{ .ctrl = true })) return,
55
55
.winsize => |ws| {
56
-
try vx.resize(alloc, tty.anyWriter(), ws);
56
+
try vx.resize(alloc, tty.writer(), ws);
57
57
break;
58
58
},
59
59
}
···
63
63
while (loop.tryEvent()) |event| {
64
64
switch (event) {
65
65
.key_press => |key| if (key.matches('c', .{ .ctrl = true })) return,
66
-
.winsize => |ws| try vx.resize(alloc, tty.anyWriter(), ws),
66
+
.winsize => |ws| try vx.resize(alloc, tty.writer(), ws),
67
67
}
68
68
}
69
69
···
83
83
// var bw = tty.bufferedWriter();
84
84
// try vx.render(bw.writer().any());
85
85
// try bw.flush();
86
-
try vx.render(tty.anyWriter());
86
+
try vx.render(tty.writer());
87
87
std.Thread.sleep(16 * std.time.ns_per_ms);
88
88
switch (dir) {
89
89
.up => {
+6
-6
examples/view.zig
+6
-6
examples/view.zig
···
48
48
var tty = try vaxis.Tty.init(&buffer);
49
49
defer tty.deinit();
50
50
51
-
const writer = tty.anyWriter();
51
+
const writer = tty.writer();
52
52
53
53
// Initialize Vaxis
54
54
var vx = try vaxis.init(alloc, .{
55
55
.kitty_keyboard_flags = .{ .report_events = true },
56
56
});
57
-
defer vx.deinit(alloc, tty.anyWriter());
57
+
defer vx.deinit(alloc, tty.writer());
58
58
var loop: vaxis.Loop(Event) = .{
59
59
.vaxis = &vx,
60
60
.tty = &tty,
···
64
64
defer loop.stop();
65
65
try vx.enterAltScreen(writer);
66
66
try writer.flush();
67
-
try vx.queryTerminal(tty.anyWriter(), 20 * std.time.ns_per_s);
67
+
try vx.queryTerminal(tty.writer(), 20 * std.time.ns_per_s);
68
68
69
69
// Initialize Views
70
70
// - Large Map
71
-
var lg_map_view = try View.init(alloc, &vx.unicode, .{ .width = lg_map_width, .height = lg_map_height });
71
+
var lg_map_view = try View.init(alloc, .{ .width = lg_map_width, .height = lg_map_height });
72
72
defer lg_map_view.deinit();
73
73
//w = lg_map_view.screen.width;
74
74
//h = lg_map_view.screen.height;
···
76
76
_ = mem.replace(u8, lg_world_map, "\n", "", lg_map_buf[0..]);
77
77
_ = lg_map_view.printSegment(.{ .text = lg_map_buf[0..] }, .{ .wrap = .grapheme });
78
78
// - Small Map
79
-
var sm_map_view = try View.init(alloc, &vx.unicode, .{ .width = sm_map_width, .height = sm_map_height });
79
+
var sm_map_view = try View.init(alloc, .{ .width = sm_map_width, .height = sm_map_height });
80
80
defer sm_map_view.deinit();
81
81
w = sm_map_view.screen.width;
82
82
h = sm_map_view.screen.height;
···
128
128
// Mini View (Forced Width & Height Limits)
129
129
if (key.matches('m', .{})) use_mini_view = !use_mini_view;
130
130
},
131
-
.winsize => |ws| try vx.resize(alloc, tty.anyWriter(), ws),
131
+
.winsize => |ws| try vx.resize(alloc, tty.writer(), ws),
132
132
}
133
133
134
134
const win = vx.window();
+11
-14
examples/vt.zig
+11
-14
examples/vt.zig
···
22
22
23
23
var buffer: [1024]u8 = undefined;
24
24
var tty = try vaxis.Tty.init(&buffer);
25
+
const writer = tty.writer();
25
26
var vx = try vaxis.init(alloc, .{});
26
-
defer vx.deinit(alloc, tty.anyWriter());
27
+
defer vx.deinit(alloc, writer);
27
28
28
29
var loop: vaxis.Loop(Event) = .{ .tty = &tty, .vaxis = &vx };
29
30
try loop.init();
···
31
32
try loop.start();
32
33
defer loop.stop();
33
34
34
-
var buffered = tty.bufferedWriter();
35
-
36
-
try vx.enterAltScreen(tty.anyWriter());
37
-
try vx.queryTerminal(tty.anyWriter(), 1 * std.time.ns_per_s);
35
+
try vx.enterAltScreen(writer);
36
+
try vx.queryTerminal(writer, 1 * std.time.ns_per_s);
38
37
var env = try std.process.getEnvMap(alloc);
39
38
defer env.deinit();
40
39
···
50
49
};
51
50
const shell = env.get("SHELL") orelse "bash";
52
51
const argv = [_][]const u8{shell};
52
+
var write_buf: [4096]u8 = undefined;
53
53
var vt = try vaxis.widgets.Terminal.init(
54
54
alloc,
55
55
&argv,
56
56
&env,
57
-
&vx.unicode,
58
57
vt_opts,
58
+
&write_buf,
59
59
);
60
60
defer vt.deinit();
61
61
try vt.spawn();
···
81
81
if (key.matches('c', .{ .ctrl = true })) return;
82
82
try vt.update(.{ .key_press = key });
83
83
},
84
-
.winsize => |ws| {
85
-
try vx.resize(alloc, tty.anyWriter(), ws);
86
-
},
84
+
.winsize => |ws| try vx.resize(alloc, writer, ws),
87
85
}
88
86
}
89
87
if (!redraw) continue;
···
95
93
const child = win.child(.{
96
94
.x_off = 4,
97
95
.y_off = 2,
98
-
.width = 8,
99
-
.height = 6,
96
+
.width = 120,
97
+
.height = 40,
100
98
.border = .{
101
99
.where = .all,
102
100
},
···
108
106
.x_pixel = 0,
109
107
.y_pixel = 0,
110
108
});
111
-
try vt.draw(child);
109
+
try vt.draw(alloc, child);
112
110
113
-
try vx.render(buffered.writer().any());
114
-
try buffered.flush();
111
+
try vx.render(writer);
115
112
}
116
113
}
+18
-4
src/InternalScreen.zig
+18
-4
src/InternalScreen.zig
···
83
83
row: u16,
84
84
cell: Cell,
85
85
) void {
86
-
if (self.width < col) {
86
+
if (self.width <= col) {
87
87
// column out of bounds
88
88
return;
89
89
}
90
-
if (self.height < row) {
90
+
if (self.height <= row) {
91
91
// height out of bounds
92
92
return;
93
93
}
···
110
110
}
111
111
112
112
pub fn readCell(self: *InternalScreen, col: u16, row: u16) ?Cell {
113
-
if (self.width < col) {
113
+
if (self.width <= col) {
114
114
// column out of bounds
115
115
return null;
116
116
}
117
-
if (self.height < row) {
117
+
if (self.height <= row) {
118
118
// height out of bounds
119
119
return null;
120
120
}
···
131
131
.default = cell.default,
132
132
};
133
133
}
134
+
135
+
test "InternalScreen: out-of-bounds read/write are ignored" {
136
+
var screen = try InternalScreen.init(std.testing.allocator, 2, 2);
137
+
defer screen.deinit(std.testing.allocator);
138
+
139
+
const sentinel: Cell = .{ .char = .{ .grapheme = "A", .width = 1 } };
140
+
screen.writeCell(0, 1, sentinel);
141
+
142
+
const oob_cell: Cell = .{ .char = .{ .grapheme = "X", .width = 1 } };
143
+
screen.writeCell(2, 0, oob_cell);
144
+
const read_back = screen.readCell(0, 1) orelse return error.TestUnexpectedResult;
145
+
try std.testing.expect(std.mem.eql(u8, read_back.char.grapheme, "A"));
146
+
try std.testing.expect(screen.readCell(2, 0) == null);
147
+
}
+10
-14
src/Loop.zig
+10
-14
src/Loop.zig
···
1
1
const std = @import("std");
2
2
const builtin = @import("builtin");
3
3
4
-
const Graphemes = @import("Graphemes");
5
-
6
4
const GraphemeCache = @import("GraphemeCache.zig");
7
5
const Parser = @import("Parser.zig");
8
6
const Queue = @import("queue.zig").Queue;
···
47
45
if (self.thread) |_| return;
48
46
self.thread = try std.Thread.spawn(.{}, Self.ttyRun, .{
49
47
self,
50
-
&self.vaxis.unicode.width_data.graphemes,
51
48
self.vaxis.opts.system_clipboard_allocator,
52
49
});
53
50
}
···
58
55
if (self.thread == null) return;
59
56
self.should_quit = true;
60
57
// trigger a read
61
-
self.vaxis.deviceStatusReport(self.tty.anyWriter()) catch {};
58
+
self.vaxis.deviceStatusReport(self.tty.writer()) catch {};
62
59
63
60
if (self.thread) |thread| {
64
61
thread.join();
···
107
104
/// read input from the tty. This is run in a separate thread
108
105
fn ttyRun(
109
106
self: *Self,
110
-
grapheme_data: *const Graphemes,
111
107
paste_allocator: ?std.mem.Allocator,
112
108
) !void {
113
109
// Return early if we're in test mode to avoid infinite loops
···
118
114
119
115
switch (builtin.os.tag) {
120
116
.windows => {
121
-
var parser: Parser = .{
122
-
.grapheme_data = grapheme_data,
123
-
};
117
+
var parser: Parser = .{};
124
118
while (!self.should_quit) {
125
119
const event = try self.tty.nextEvent(&parser, paste_allocator);
126
120
try handleEventGeneric(self, self.vaxis, &cache, Event, event, null);
···
133
127
self.postEvent(.{ .winsize = winsize });
134
128
}
135
129
136
-
var parser: Parser = .{
137
-
.grapheme_data = grapheme_data,
138
-
};
130
+
var parser: Parser = .{};
139
131
140
132
// initialize the read buffer
141
133
var buf: [1024]u8 = undefined;
···
362
354
log.info("color_scheme_updates capability detected", .{});
363
355
vx.caps.color_scheme_updates = true;
364
356
},
357
+
.cap_multi_cursor => {
358
+
log.info("multi cursor capability detected", .{});
359
+
vx.caps.multi_cursor = true;
360
+
},
365
361
.cap_da1 => {
366
362
std.Thread.Futex.wake(&vx.query_futex, 10);
367
363
vx.queries_done.store(true, .unordered);
···
394
390
defer tty.deinit();
395
391
396
392
var vx = try vaxis.init(std.testing.allocator, .{});
397
-
defer vx.deinit(std.testing.allocator, tty.anyWriter());
393
+
defer vx.deinit(std.testing.allocator, tty.writer());
398
394
399
395
var loop: vaxis.Loop(Event) = .{ .tty = &tty, .vaxis = &vx };
400
396
try loop.init();
···
403
399
defer loop.stop();
404
400
405
401
// Optionally enter the alternate screen
406
-
try vx.enterAltScreen(tty.anyWriter());
407
-
try vx.queryTerminal(tty.anyWriter(), 1 * std.time.ns_per_ms);
402
+
try vx.enterAltScreen(tty.writer());
403
+
try vx.queryTerminal(tty.writer(), 1 * std.time.ns_per_ms);
408
404
}
+2
-2
src/Mouse.zig
+2
-2
src/Mouse.zig
+185
-92
src/Parser.zig
+185
-92
src/Parser.zig
···
4
4
const Event = @import("event.zig").Event;
5
5
const Key = @import("Key.zig");
6
6
const Mouse = @import("Mouse.zig");
7
-
const code_point = @import("code_point");
8
-
const Graphemes = @import("Graphemes");
7
+
const uucode = @import("uucode");
9
8
const Winsize = @import("main.zig").Winsize;
10
9
11
10
const log = std.log.scoped(.vaxis_parser);
···
45
44
// a buffer to temporarily store text in. We need this to encode
46
45
// text-as-codepoints
47
46
buf: [128]u8 = undefined,
48
-
49
-
grapheme_data: *const Graphemes,
50
47
51
48
/// Parse the first event from the input buffer. If a completion event is not
52
49
/// present, Result.event will be null and Result.n will be 0
···
78
75
};
79
76
},
80
77
}
81
-
} else return parseGround(input, self.grapheme_data);
78
+
} else return parseGround(input);
82
79
}
83
80
84
81
/// Parse ground state
85
-
inline fn parseGround(input: []const u8, data: *const Graphemes) !Result {
82
+
inline fn parseGround(input: []const u8) !Result {
86
83
std.debug.assert(input.len > 0);
87
84
88
85
const b = input[0];
···
109
106
},
110
107
0x7F => .{ .codepoint = Key.backspace },
111
108
else => blk: {
112
-
var iter: code_point.Iterator = .{ .bytes = input };
109
+
var iter = uucode.utf8.Iterator.init(input);
113
110
// return null if we don't have a valid codepoint
114
-
const cp = iter.next() orelse return error.InvalidUTF8;
111
+
const first_cp = iter.next() orelse return error.InvalidUTF8;
115
112
116
-
n = cp.len;
113
+
n = std.unicode.utf8CodepointSequenceLength(first_cp) catch return error.InvalidUTF8;
117
114
118
115
// Check if we have a multi-codepoint grapheme
119
-
var code = cp.code;
120
-
var g_state: Graphemes.IterState = .{};
121
-
var prev_cp = code;
122
-
while (iter.next()) |next_cp| {
123
-
if (Graphemes.graphemeBreak(prev_cp, next_cp.code, data, &g_state)) {
116
+
var code = first_cp;
117
+
var grapheme_iter = uucode.grapheme.Iterator(uucode.utf8.Iterator).init(.init(input));
118
+
var grapheme_len: usize = 0;
119
+
var cp_count: usize = 0;
120
+
121
+
while (grapheme_iter.next()) |result| {
122
+
cp_count += 1;
123
+
if (result.is_break) {
124
+
// Found the first grapheme boundary
125
+
grapheme_len = grapheme_iter.i;
124
126
break;
125
127
}
126
-
prev_cp = next_cp.code;
127
-
code = Key.multicodepoint;
128
-
n += next_cp.len;
128
+
}
129
+
130
+
if (grapheme_len > 0) {
131
+
n = grapheme_len;
132
+
if (cp_count > 1) {
133
+
code = Key.multicodepoint;
134
+
}
129
135
}
130
136
131
137
break :blk .{ .codepoint = code, .text = input[0..n] };
···
646
652
else => return null_event,
647
653
}
648
654
},
655
+
'q' => {
656
+
// kitty multi cursor cap (CSI > 1;2;3;29;30;40;100;101 TRAILER) (TRAILER is " q")
657
+
const second_final = sequence[sequence.len - 2];
658
+
if (second_final != ' ') return null_event;
659
+
// check for any digits. we're not too picky about checking the supported cursor types here
660
+
for (sequence[0 .. sequence.len - 2]) |c| switch (c) {
661
+
'0'...'9' => return .{ .event = .cap_multi_cursor, .n = sequence.len },
662
+
else => continue,
663
+
};
664
+
return null_event;
665
+
},
649
666
else => return null_event,
650
667
}
651
668
}
···
653
670
/// Parse a param buffer, returning a default value if the param was empty
654
671
inline fn parseParam(comptime T: type, buf: []const u8, default: ?T) ?T {
655
672
if (buf.len == 0) return default;
656
-
return std.fmt.parseUnsigned(T, buf, 10) catch return null;
673
+
return std.fmt.parseInt(T, buf, 10) catch return null;
657
674
}
658
675
659
676
/// Parse a mouse event
···
661
678
const null_event: Result = .{ .event = null, .n = input.len };
662
679
663
680
var button_mask: u16 = undefined;
664
-
var px: u16 = undefined;
665
-
var py: u16 = undefined;
681
+
var px: i16 = undefined;
682
+
var py: i16 = undefined;
666
683
var xterm: bool = undefined;
667
684
if (input.len == 3 and (input[2] == 'M') and full_input.len >= 6) {
668
685
xterm = true;
···
674
691
const delim1 = std.mem.indexOfScalarPos(u8, input, 3, ';') orelse return null_event;
675
692
button_mask = parseParam(u16, input[3..delim1], null) orelse return null_event;
676
693
const delim2 = std.mem.indexOfScalarPos(u8, input, delim1 + 1, ';') orelse return null_event;
677
-
px = parseParam(u16, input[delim1 + 1 .. delim2], 1) orelse return null_event;
678
-
py = parseParam(u16, input[delim2 + 1 .. input.len - 1], 1) orelse return null_event;
694
+
px = parseParam(i16, input[delim1 + 1 .. delim2], 1) orelse return null_event;
695
+
py = parseParam(i16, input[delim2 + 1 .. input.len - 1], 1) orelse return null_event;
679
696
} else {
680
697
return null_event;
681
698
}
···
720
737
721
738
test "parse: single xterm keypress" {
722
739
const alloc = testing.allocator_instance.allocator();
723
-
const grapheme_data = try Graphemes.init(alloc);
724
-
defer grapheme_data.deinit(alloc);
725
740
const input = "a";
726
-
var parser: Parser = .{ .grapheme_data = &grapheme_data };
741
+
var parser: Parser = .{};
727
742
const result = try parser.parse(input, alloc);
728
743
const expected_key: Key = .{
729
744
.codepoint = 'a',
···
737
752
738
753
test "parse: single xterm keypress backspace" {
739
754
const alloc = testing.allocator_instance.allocator();
740
-
const grapheme_data = try Graphemes.init(alloc);
741
-
defer grapheme_data.deinit(alloc);
742
755
const input = "\x08";
743
-
var parser: Parser = .{ .grapheme_data = &grapheme_data };
756
+
var parser: Parser = .{};
744
757
const result = try parser.parse(input, alloc);
745
758
const expected_key: Key = .{
746
759
.codepoint = Key.backspace,
···
753
766
754
767
test "parse: single xterm keypress with more buffer" {
755
768
const alloc = testing.allocator_instance.allocator();
756
-
const grapheme_data = try Graphemes.init(alloc);
757
-
defer grapheme_data.deinit(alloc);
758
769
const input = "ab";
759
-
var parser: Parser = .{ .grapheme_data = &grapheme_data };
770
+
var parser: Parser = .{};
760
771
const result = try parser.parse(input, alloc);
761
772
const expected_key: Key = .{
762
773
.codepoint = 'a',
···
771
782
772
783
test "parse: xterm escape keypress" {
773
784
const alloc = testing.allocator_instance.allocator();
774
-
const grapheme_data = try Graphemes.init(alloc);
775
-
defer grapheme_data.deinit(alloc);
776
785
const input = "\x1b";
777
-
var parser: Parser = .{ .grapheme_data = &grapheme_data };
786
+
var parser: Parser = .{};
778
787
const result = try parser.parse(input, alloc);
779
788
const expected_key: Key = .{ .codepoint = Key.escape };
780
789
const expected_event: Event = .{ .key_press = expected_key };
···
785
794
786
795
test "parse: xterm ctrl+a" {
787
796
const alloc = testing.allocator_instance.allocator();
788
-
const grapheme_data = try Graphemes.init(alloc);
789
-
defer grapheme_data.deinit(alloc);
790
797
const input = "\x01";
791
-
var parser: Parser = .{ .grapheme_data = &grapheme_data };
798
+
var parser: Parser = .{};
792
799
const result = try parser.parse(input, alloc);
793
800
const expected_key: Key = .{ .codepoint = 'a', .mods = .{ .ctrl = true } };
794
801
const expected_event: Event = .{ .key_press = expected_key };
···
799
806
800
807
test "parse: xterm alt+a" {
801
808
const alloc = testing.allocator_instance.allocator();
802
-
const grapheme_data = try Graphemes.init(alloc);
803
-
defer grapheme_data.deinit(alloc);
804
809
const input = "\x1ba";
805
-
var parser: Parser = .{ .grapheme_data = &grapheme_data };
810
+
var parser: Parser = .{};
806
811
const result = try parser.parse(input, alloc);
807
812
const expected_key: Key = .{ .codepoint = 'a', .mods = .{ .alt = true } };
808
813
const expected_event: Event = .{ .key_press = expected_key };
···
813
818
814
819
test "parse: xterm key up" {
815
820
const alloc = testing.allocator_instance.allocator();
816
-
const grapheme_data = try Graphemes.init(alloc);
817
-
defer grapheme_data.deinit(alloc);
818
821
{
819
822
// normal version
820
823
const input = "\x1b[A";
821
-
var parser: Parser = .{ .grapheme_data = &grapheme_data };
824
+
var parser: Parser = .{};
822
825
const result = try parser.parse(input, alloc);
823
826
const expected_key: Key = .{ .codepoint = Key.up };
824
827
const expected_event: Event = .{ .key_press = expected_key };
···
830
833
{
831
834
// application keys version
832
835
const input = "\x1bOA";
833
-
var parser: Parser = .{ .grapheme_data = &grapheme_data };
836
+
var parser: Parser = .{};
834
837
const result = try parser.parse(input, alloc);
835
838
const expected_key: Key = .{ .codepoint = Key.up };
836
839
const expected_event: Event = .{ .key_press = expected_key };
···
842
845
843
846
test "parse: xterm shift+up" {
844
847
const alloc = testing.allocator_instance.allocator();
845
-
const grapheme_data = try Graphemes.init(alloc);
846
-
defer grapheme_data.deinit(alloc);
847
848
const input = "\x1b[1;2A";
848
-
var parser: Parser = .{ .grapheme_data = &grapheme_data };
849
+
var parser: Parser = .{};
849
850
const result = try parser.parse(input, alloc);
850
851
const expected_key: Key = .{ .codepoint = Key.up, .mods = .{ .shift = true } };
851
852
const expected_event: Event = .{ .key_press = expected_key };
···
856
857
857
858
test "parse: xterm insert" {
858
859
const alloc = testing.allocator_instance.allocator();
859
-
const grapheme_data = try Graphemes.init(alloc);
860
-
defer grapheme_data.deinit(alloc);
861
860
const input = "\x1b[2~";
862
-
var parser: Parser = .{ .grapheme_data = &grapheme_data };
861
+
var parser: Parser = .{};
863
862
const result = try parser.parse(input, alloc);
864
863
const expected_key: Key = .{ .codepoint = Key.insert, .mods = .{} };
865
864
const expected_event: Event = .{ .key_press = expected_key };
···
870
869
871
870
test "parse: paste_start" {
872
871
const alloc = testing.allocator_instance.allocator();
873
-
const grapheme_data = try Graphemes.init(alloc);
874
-
defer grapheme_data.deinit(alloc);
875
872
const input = "\x1b[200~";
876
-
var parser: Parser = .{ .grapheme_data = &grapheme_data };
873
+
var parser: Parser = .{};
877
874
const result = try parser.parse(input, alloc);
878
875
const expected_event: Event = .paste_start;
879
876
···
883
880
884
881
test "parse: paste_end" {
885
882
const alloc = testing.allocator_instance.allocator();
886
-
const grapheme_data = try Graphemes.init(alloc);
887
-
defer grapheme_data.deinit(alloc);
888
883
const input = "\x1b[201~";
889
-
var parser: Parser = .{ .grapheme_data = &grapheme_data };
884
+
var parser: Parser = .{};
890
885
const result = try parser.parse(input, alloc);
891
886
const expected_event: Event = .paste_end;
892
887
···
896
891
897
892
test "parse: osc52 paste" {
898
893
const alloc = testing.allocator_instance.allocator();
899
-
const grapheme_data = try Graphemes.init(alloc);
900
-
defer grapheme_data.deinit(alloc);
901
894
const input = "\x1b]52;c;b3NjNTIgcGFzdGU=\x1b\\";
902
895
const expected_text = "osc52 paste";
903
-
var parser: Parser = .{ .grapheme_data = &grapheme_data };
896
+
var parser: Parser = .{};
904
897
const result = try parser.parse(input, alloc);
905
898
906
899
try testing.expectEqual(25, result.n);
···
915
908
916
909
test "parse: focus_in" {
917
910
const alloc = testing.allocator_instance.allocator();
918
-
const grapheme_data = try Graphemes.init(alloc);
919
-
defer grapheme_data.deinit(alloc);
920
911
const input = "\x1b[I";
921
-
var parser: Parser = .{ .grapheme_data = &grapheme_data };
912
+
var parser: Parser = .{};
922
913
const result = try parser.parse(input, alloc);
923
914
const expected_event: Event = .focus_in;
924
915
···
928
919
929
920
test "parse: focus_out" {
930
921
const alloc = testing.allocator_instance.allocator();
931
-
const grapheme_data = try Graphemes.init(alloc);
932
-
defer grapheme_data.deinit(alloc);
933
922
const input = "\x1b[O";
934
-
var parser: Parser = .{ .grapheme_data = &grapheme_data };
923
+
var parser: Parser = .{};
935
924
const result = try parser.parse(input, alloc);
936
925
const expected_event: Event = .focus_out;
937
926
···
941
930
942
931
test "parse: kitty: shift+a without text reporting" {
943
932
const alloc = testing.allocator_instance.allocator();
944
-
const grapheme_data = try Graphemes.init(alloc);
945
-
defer grapheme_data.deinit(alloc);
946
933
const input = "\x1b[97:65;2u";
947
-
var parser: Parser = .{ .grapheme_data = &grapheme_data };
934
+
var parser: Parser = .{};
948
935
const result = try parser.parse(input, alloc);
949
936
const expected_key: Key = .{
950
937
.codepoint = 'a',
···
960
947
961
948
test "parse: kitty: alt+shift+a without text reporting" {
962
949
const alloc = testing.allocator_instance.allocator();
963
-
const grapheme_data = try Graphemes.init(alloc);
964
-
defer grapheme_data.deinit(alloc);
965
950
const input = "\x1b[97:65;4u";
966
-
var parser: Parser = .{ .grapheme_data = &grapheme_data };
951
+
var parser: Parser = .{};
967
952
const result = try parser.parse(input, alloc);
968
953
const expected_key: Key = .{
969
954
.codepoint = 'a',
···
978
963
979
964
test "parse: kitty: a without text reporting" {
980
965
const alloc = testing.allocator_instance.allocator();
981
-
const grapheme_data = try Graphemes.init(alloc);
982
-
defer grapheme_data.deinit(alloc);
983
966
const input = "\x1b[97u";
984
-
var parser: Parser = .{ .grapheme_data = &grapheme_data };
967
+
var parser: Parser = .{};
985
968
const result = try parser.parse(input, alloc);
986
969
const expected_key: Key = .{
987
970
.codepoint = 'a',
···
994
977
995
978
test "parse: kitty: release event" {
996
979
const alloc = testing.allocator_instance.allocator();
997
-
const grapheme_data = try Graphemes.init(alloc);
998
-
defer grapheme_data.deinit(alloc);
999
980
const input = "\x1b[97;1:3u";
1000
-
var parser: Parser = .{ .grapheme_data = &grapheme_data };
981
+
var parser: Parser = .{};
1001
982
const result = try parser.parse(input, alloc);
1002
983
const expected_key: Key = .{
1003
984
.codepoint = 'a',
···
1010
991
1011
992
test "parse: single codepoint" {
1012
993
const alloc = testing.allocator_instance.allocator();
1013
-
const grapheme_data = try Graphemes.init(alloc);
1014
-
defer grapheme_data.deinit(alloc);
1015
994
const input = "๐";
1016
-
var parser: Parser = .{ .grapheme_data = &grapheme_data };
995
+
var parser: Parser = .{};
1017
996
const result = try parser.parse(input, alloc);
1018
997
const expected_key: Key = .{
1019
998
.codepoint = 0x1F642,
···
1027
1006
1028
1007
test "parse: single codepoint with more in buffer" {
1029
1008
const alloc = testing.allocator_instance.allocator();
1030
-
const grapheme_data = try Graphemes.init(alloc);
1031
-
defer grapheme_data.deinit(alloc);
1032
1009
const input = "๐a";
1033
-
var parser: Parser = .{ .grapheme_data = &grapheme_data };
1010
+
var parser: Parser = .{};
1034
1011
const result = try parser.parse(input, alloc);
1035
1012
const expected_key: Key = .{
1036
1013
.codepoint = 0x1F642,
···
1044
1021
1045
1022
test "parse: multiple codepoint grapheme" {
1046
1023
const alloc = testing.allocator_instance.allocator();
1047
-
const grapheme_data = try Graphemes.init(alloc);
1048
-
defer grapheme_data.deinit(alloc);
1049
1024
const input = "๐ฉโ๐";
1050
-
var parser: Parser = .{ .grapheme_data = &grapheme_data };
1025
+
var parser: Parser = .{};
1051
1026
const result = try parser.parse(input, alloc);
1052
1027
const expected_key: Key = .{
1053
1028
.codepoint = Key.multicodepoint,
···
1061
1036
1062
1037
test "parse: multiple codepoint grapheme with more after" {
1063
1038
const alloc = testing.allocator_instance.allocator();
1064
-
const grapheme_data = try Graphemes.init(alloc);
1065
-
defer grapheme_data.deinit(alloc);
1066
1039
const input = "๐ฉโ๐abc";
1067
-
var parser: Parser = .{ .grapheme_data = &grapheme_data };
1040
+
var parser: Parser = .{};
1068
1041
const result = try parser.parse(input, alloc);
1069
1042
const expected_key: Key = .{
1070
1043
.codepoint = Key.multicodepoint,
···
1077
1050
try testing.expectEqual(expected_key.codepoint, actual.codepoint);
1078
1051
}
1079
1052
1053
+
test "parse: flag emoji" {
1054
+
const alloc = testing.allocator_instance.allocator();
1055
+
const input = "๐บ๐ธ";
1056
+
var parser: Parser = .{};
1057
+
const result = try parser.parse(input, alloc);
1058
+
const expected_key: Key = .{
1059
+
.codepoint = Key.multicodepoint,
1060
+
.text = input,
1061
+
};
1062
+
const expected_event: Event = .{ .key_press = expected_key };
1063
+
1064
+
try testing.expectEqual(input.len, result.n);
1065
+
try testing.expectEqual(expected_event, result.event);
1066
+
}
1067
+
1068
+
test "parse: combining mark" {
1069
+
const alloc = testing.allocator_instance.allocator();
1070
+
// a with combining acute accent (NFD form)
1071
+
const input = "a\u{0301}";
1072
+
var parser: Parser = .{};
1073
+
const result = try parser.parse(input, alloc);
1074
+
const expected_key: Key = .{
1075
+
.codepoint = Key.multicodepoint,
1076
+
.text = input,
1077
+
};
1078
+
const expected_event: Event = .{ .key_press = expected_key };
1079
+
1080
+
try testing.expectEqual(input.len, result.n);
1081
+
try testing.expectEqual(expected_event, result.event);
1082
+
}
1083
+
1084
+
test "parse: skin tone emoji" {
1085
+
const alloc = testing.allocator_instance.allocator();
1086
+
const input = "๐๐ฟ";
1087
+
var parser: Parser = .{};
1088
+
const result = try parser.parse(input, alloc);
1089
+
const expected_key: Key = .{
1090
+
.codepoint = Key.multicodepoint,
1091
+
.text = input,
1092
+
};
1093
+
const expected_event: Event = .{ .key_press = expected_key };
1094
+
1095
+
try testing.expectEqual(input.len, result.n);
1096
+
try testing.expectEqual(expected_event, result.event);
1097
+
}
1098
+
1099
+
test "parse: text variation selector" {
1100
+
const alloc = testing.allocator_instance.allocator();
1101
+
// Heavy black heart with text variation selector
1102
+
const input = "โค๏ธ";
1103
+
var parser: Parser = .{};
1104
+
const result = try parser.parse(input, alloc);
1105
+
const expected_key: Key = .{
1106
+
.codepoint = Key.multicodepoint,
1107
+
.text = input,
1108
+
};
1109
+
const expected_event: Event = .{ .key_press = expected_key };
1110
+
1111
+
try testing.expectEqual(input.len, result.n);
1112
+
try testing.expectEqual(expected_event, result.event);
1113
+
}
1114
+
1115
+
test "parse: keycap sequence" {
1116
+
const alloc = testing.allocator_instance.allocator();
1117
+
const input = "1๏ธโฃ";
1118
+
var parser: Parser = .{};
1119
+
const result = try parser.parse(input, alloc);
1120
+
const expected_key: Key = .{
1121
+
.codepoint = Key.multicodepoint,
1122
+
.text = input,
1123
+
};
1124
+
const expected_event: Event = .{ .key_press = expected_key };
1125
+
1126
+
try testing.expectEqual(input.len, result.n);
1127
+
try testing.expectEqual(expected_event, result.event);
1128
+
}
1129
+
1130
+
test "parse(csi): kitty multi cursor" {
1131
+
var buf: [1]u8 = undefined;
1132
+
{
1133
+
const input = "\x1b[>1;2;3;29;30;40;100;101 q";
1134
+
const result = parseCsi(input, &buf);
1135
+
const expected: Result = .{
1136
+
.event = .cap_multi_cursor,
1137
+
.n = input.len,
1138
+
};
1139
+
1140
+
try testing.expectEqual(expected.n, result.n);
1141
+
try testing.expectEqual(expected.event, result.event);
1142
+
}
1143
+
{
1144
+
const input = "\x1b[> q";
1145
+
const result = parseCsi(input, &buf);
1146
+
const expected: Result = .{
1147
+
.event = null,
1148
+
.n = input.len,
1149
+
};
1150
+
1151
+
try testing.expectEqual(expected.n, result.n);
1152
+
try testing.expectEqual(expected.event, result.event);
1153
+
}
1154
+
}
1155
+
1080
1156
test "parse(csi): decrpm" {
1081
1157
var buf: [1]u8 = undefined;
1082
1158
{
···
1172
1248
try testing.expectEqual(expected.event, result.event);
1173
1249
}
1174
1250
1251
+
test "parse(csi): mouse (negative)" {
1252
+
var buf: [1]u8 = undefined;
1253
+
const input = "\x1b[<35;-50;-100m";
1254
+
const result = parseCsi(input, &buf);
1255
+
const expected: Result = .{
1256
+
.event = .{ .mouse = .{
1257
+
.col = -51,
1258
+
.row = -101,
1259
+
.button = .none,
1260
+
.type = .motion,
1261
+
.mods = .{},
1262
+
} },
1263
+
.n = input.len,
1264
+
};
1265
+
1266
+
try testing.expectEqual(expected.n, result.n);
1267
+
try testing.expectEqual(expected.event, result.event);
1268
+
}
1269
+
1175
1270
test "parse(csi): xterm mouse" {
1176
1271
var buf: [1]u8 = undefined;
1177
1272
const input = "\x1b[M\x20\x21\x21";
···
1193
1288
1194
1289
test "parse: disambiguate shift + space" {
1195
1290
const alloc = testing.allocator_instance.allocator();
1196
-
const grapheme_data = try Graphemes.init(alloc);
1197
-
defer grapheme_data.deinit(alloc);
1198
1291
const input = "\x1b[32;2u";
1199
-
var parser: Parser = .{ .grapheme_data = &grapheme_data };
1292
+
var parser: Parser = .{};
1200
1293
const result = try parser.parse(input, alloc);
1201
1294
const expected_key: Key = .{
1202
1295
.codepoint = ' ',
+4
src/Screen.zig
+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
-
}
+84
-44
src/Vaxis.zig
+84
-44
src/Vaxis.zig
···
11
11
const Key = @import("Key.zig");
12
12
const Mouse = @import("Mouse.zig");
13
13
const Screen = @import("Screen.zig");
14
-
const Unicode = @import("Unicode.zig");
14
+
const unicode = @import("unicode.zig");
15
15
const Window = @import("Window.zig");
16
16
17
17
const Hyperlink = Cell.Hyperlink;
···
38
38
color_scheme_updates: bool = false,
39
39
explicit_width: bool = false,
40
40
scaled_text: bool = false,
41
+
multi_cursor: bool = false,
41
42
};
42
43
43
44
pub const Options = struct {
···
73
74
// images
74
75
next_img_id: u32 = 1,
75
76
76
-
unicode: Unicode,
77
-
78
77
sgr: enum {
79
78
standard,
80
79
legacy,
···
109
108
.opts = opts,
110
109
.screen = .{},
111
110
.screen_last = try .init(alloc, 0, 0),
112
-
.unicode = try Unicode.init(alloc),
113
111
};
114
112
}
115
113
···
123
121
if (alloc) |a| {
124
122
self.screen.deinit(a);
125
123
self.screen_last.deinit(a);
126
-
self.unicode.deinit(a);
127
124
}
128
125
}
129
126
···
226
223
.width = self.screen.width,
227
224
.height = self.screen.height,
228
225
.screen = &self.screen,
229
-
.unicode = &self.unicode,
230
226
};
231
227
}
232
228
···
300
296
// why we see a Shift modifier
301
297
ctlseqs.home ++
302
298
ctlseqs.scaled_text_query ++
299
+
ctlseqs.multi_cursor_query ++
303
300
ctlseqs.cursor_position_request ++
304
301
ctlseqs.xtversion ++
305
302
ctlseqs.csi_u_query ++
···
363
360
assert(self.screen.buf.len == @as(usize, @intCast(self.screen.width)) * self.screen.height); // correct size
364
361
assert(self.screen.buf.len == self.screen_last.buf.len); // same size
365
362
366
-
// Set up sync before we write anything
367
-
// TODO: optimize sync so we only sync _when we have changes_. This
368
-
// requires a smarter buffered writer, we'll probably have to write
369
-
// our own
370
-
try tty.writeAll(ctlseqs.sync_set);
371
-
errdefer tty.writeAll(ctlseqs.sync_reset) catch {};
363
+
var started: bool = false;
364
+
var sync_active: bool = false;
365
+
errdefer if (sync_active) tty.writeAll(ctlseqs.sync_reset) catch {};
372
366
373
-
// Send the cursor to 0,0
374
-
// TODO: this needs to move after we optimize writes. We only do
375
-
// this if we have an update to make. We also need to hide cursor
376
-
// and then reshow it if needed
377
-
try tty.writeAll(ctlseqs.hide_cursor);
378
-
if (self.state.alt_screen)
379
-
try tty.writeAll(ctlseqs.home)
380
-
else {
381
-
try tty.writeByte('\r');
382
-
for (0..self.state.cursor.row) |_| {
383
-
try tty.writeAll(ctlseqs.ri);
384
-
}
385
-
}
386
-
try tty.writeAll(ctlseqs.sgr_reset);
367
+
const cursor_vis_changed = self.screen.cursor_vis != self.screen_last.cursor_vis;
368
+
const cursor_shape_changed = self.screen.cursor_shape != self.screen_last.cursor_shape;
369
+
const mouse_shape_changed = self.screen.mouse_shape != self.screen_last.mouse_shape;
370
+
const cursor_pos_changed = self.screen.cursor_vis and
371
+
(self.screen.cursor_row != self.state.cursor.row or
372
+
self.screen.cursor_col != self.state.cursor.col);
373
+
const needs_render = self.refresh or cursor_vis_changed or cursor_shape_changed or mouse_shape_changed or cursor_pos_changed;
387
374
388
375
// initialize some variables
389
376
var reposition: bool = false;
···
391
378
var col: u16 = 0;
392
379
var cursor: Style = .{};
393
380
var link: Hyperlink = .{};
394
-
var cursor_pos: struct {
381
+
const CursorPos = struct {
395
382
row: u16 = 0,
396
383
col: u16 = 0,
397
-
} = .{};
384
+
};
385
+
var cursor_pos: CursorPos = .{};
398
386
399
-
// Clear all images
400
-
if (self.caps.kitty_graphics)
401
-
try tty.writeAll(ctlseqs.kitty_graphics_clear);
387
+
const startRender = struct {
388
+
fn run(
389
+
vx: *Vaxis,
390
+
io: *IoWriter,
391
+
cursor_pos_ptr: *CursorPos,
392
+
reposition_ptr: *bool,
393
+
started_ptr: *bool,
394
+
sync_active_ptr: *bool,
395
+
) !void {
396
+
if (started_ptr.*) return;
397
+
started_ptr.* = true;
398
+
sync_active_ptr.* = true;
399
+
// Set up sync before we write anything
400
+
try io.writeAll(ctlseqs.sync_set);
401
+
// Send the cursor to 0,0
402
+
try io.writeAll(ctlseqs.hide_cursor);
403
+
if (vx.state.alt_screen)
404
+
try io.writeAll(ctlseqs.home)
405
+
else {
406
+
try io.writeByte('\r');
407
+
for (0..vx.state.cursor.row) |_| {
408
+
try io.writeAll(ctlseqs.ri);
409
+
}
410
+
}
411
+
try io.writeAll(ctlseqs.sgr_reset);
412
+
cursor_pos_ptr.* = .{};
413
+
reposition_ptr.* = true;
414
+
// Clear all images
415
+
if (vx.caps.kitty_graphics)
416
+
try io.writeAll(ctlseqs.kitty_graphics_clear);
417
+
}
418
+
};
402
419
403
420
// Reset skip flag on all last_screen cells
404
421
for (self.screen_last.buf) |*last_cell| {
405
422
last_cell.skip = false;
406
423
}
407
424
425
+
if (needs_render) {
426
+
try startRender.run(self, tty, &cursor_pos, &reposition, &started, &sync_active);
427
+
}
428
+
408
429
var i: usize = 0;
409
430
while (i < self.screen.buf.len) {
410
431
const cell = self.screen.buf[i];
···
412
433
if (cell.char.width != 0) break :blk cell.char.width;
413
434
414
435
const method: gwidth.Method = self.caps.unicode;
415
-
const width: u16 = @intCast(gwidth.gwidth(cell.char.grapheme, method, &self.unicode.width_data));
436
+
const width: u16 = @intCast(gwidth.gwidth(cell.char.grapheme, method));
416
437
break :blk @max(1, width);
417
438
};
418
439
defer {
···
449
470
try tty.writeAll(ctlseqs.osc8_clear);
450
471
}
451
472
continue;
473
+
}
474
+
if (!started) {
475
+
try startRender.run(self, tty, &cursor_pos, &reposition, &started, &sync_active);
452
476
}
453
477
self.screen_last.buf[i].skipped = false;
454
478
defer {
···
733
757
cursor_pos.col = col + w;
734
758
cursor_pos.row = row;
735
759
}
760
+
if (!started) return;
736
761
if (self.screen.cursor_vis) {
737
762
if (self.state.alt_screen) {
738
763
try tty.print(
···
764
789
self.state.cursor.row = cursor_pos.row;
765
790
self.state.cursor.col = cursor_pos.col;
766
791
}
792
+
self.screen_last.cursor_vis = self.screen.cursor_vis;
767
793
if (self.screen.mouse_shape != self.screen_last.mouse_shape) {
768
794
try tty.print(
769
795
ctlseqs.osc22_mouse_shape,
···
854
880
const ypos = mouse.row;
855
881
const xextra = self.screen.width_pix % self.screen.width;
856
882
const yextra = self.screen.height_pix % self.screen.height;
857
-
const xcell = (self.screen.width_pix - xextra) / self.screen.width;
858
-
const ycell = (self.screen.height_pix - yextra) / self.screen.height;
883
+
const xcell: i16 = @intCast((self.screen.width_pix - xextra) / self.screen.width);
884
+
const ycell: i16 = @intCast((self.screen.height_pix - yextra) / self.screen.height);
859
885
if (xcell == 0 or ycell == 0) return mouse;
860
-
result.col = xpos / xcell;
861
-
result.row = ypos / ycell;
862
-
result.xoffset = xpos % xcell;
863
-
result.yoffset = ypos % ycell;
886
+
result.col = @divFloor(xpos, xcell);
887
+
result.row = @divFloor(ypos, ycell);
888
+
result.xoffset = @intCast(@mod(xpos, xcell));
889
+
result.yoffset = @intCast(@mod(ypos, ycell));
864
890
}
865
891
return result;
866
892
}
···
998
1024
const buf = switch (format) {
999
1025
.png => png: {
1000
1026
const png_buf = try arena.allocator().alloc(u8, img.imageByteSize());
1001
-
const png = try img.writeToMemory(png_buf, .{ .png = .{} });
1027
+
const png = try img.writeToMemory(arena.allocator(), png_buf, .{ .png = .{} });
1002
1028
break :png png;
1003
1029
},
1004
1030
.rgb => rgb: {
1005
-
try img.convert(.rgb24);
1031
+
try img.convert(arena.allocator(), .rgb24);
1006
1032
break :rgb img.rawBytes();
1007
1033
},
1008
1034
.rgba => rgba: {
1009
-
try img.convert(.rgba32);
1035
+
try img.convert(arena.allocator(), .rgba32);
1010
1036
break :rgba img.rawBytes();
1011
1037
},
1012
1038
};
···
1030
1056
.path => |path| try zigimg.Image.fromFilePath(alloc, path, &read_buffer),
1031
1057
.mem => |bytes| try zigimg.Image.fromMemory(alloc, bytes),
1032
1058
};
1033
-
defer img.deinit();
1059
+
defer img.deinit(alloc);
1034
1060
return self.transmitImage(alloc, tty, &img, .png);
1035
1061
}
1036
1062
···
1147
1173
if (cell.char.width != 0) break :blk cell.char.width;
1148
1174
1149
1175
const method: gwidth.Method = self.caps.unicode;
1150
-
const width = gwidth.gwidth(cell.char.grapheme, method, &self.unicode.width_data);
1176
+
const width = gwidth.gwidth(cell.char.grapheme, method);
1151
1177
break :blk @max(1, width);
1152
1178
};
1153
1179
defer {
···
1409
1435
.host = .{ .raw = hostname },
1410
1436
.path = .{ .raw = path },
1411
1437
};
1412
-
try tty.print(ctlseqs.osc7, .{uri});
1438
+
try tty.print(ctlseqs.osc7, .{uri.fmt(.{ .scheme = true, .authority = true, .path = true })});
1413
1439
try tty.flush();
1414
1440
}
1441
+
1442
+
test "render: no output when no changes" {
1443
+
var vx = try Vaxis.init(std.testing.allocator, .{});
1444
+
var deinit_writer = std.io.Writer.Allocating.init(std.testing.allocator);
1445
+
defer deinit_writer.deinit();
1446
+
defer vx.deinit(std.testing.allocator, &deinit_writer.writer);
1447
+
1448
+
var render_writer = std.io.Writer.Allocating.init(std.testing.allocator);
1449
+
defer render_writer.deinit();
1450
+
try vx.render(&render_writer.writer);
1451
+
const output = try render_writer.toOwnedSlice();
1452
+
defer std.testing.allocator.free(output);
1453
+
try std.testing.expectEqual(@as(usize, 0), output.len);
1454
+
}
+5
-20
src/Window.zig
+5
-20
src/Window.zig
···
4
4
const Cell = @import("Cell.zig");
5
5
const Mouse = @import("Mouse.zig");
6
6
const Segment = @import("Cell.zig").Segment;
7
-
const Unicode = @import("Unicode.zig");
7
+
const unicode = @import("unicode.zig");
8
8
const gw = @import("gwidth.zig");
9
9
10
10
const Window = @This();
···
25
25
height: u16,
26
26
27
27
screen: *Screen,
28
-
unicode: *const Unicode,
29
28
30
29
/// Creates a new window with offset relative to parent and size clamped to the
31
30
/// parent's size. Windows do not retain a reference to their parent and are
···
50
49
.width = @min(width, max_width),
51
50
.height = @min(height, max_height),
52
51
.screen = self.screen,
53
-
.unicode = self.unicode,
54
52
};
55
53
}
56
54
···
207
205
208
206
/// returns the width of the grapheme. This depends on the terminal capabilities
209
207
pub fn gwidth(self: Window, str: []const u8) u16 {
210
-
return gw.gwidth(str, self.screen.width_method, &self.unicode.width_data);
208
+
return gw.gwidth(str, self.screen.width_method);
211
209
}
212
210
213
211
/// fills the window with the provided cell
···
295
293
.grapheme => {
296
294
var col: u16 = opts.col_offset;
297
295
const overflow: bool = blk: for (segments) |segment| {
298
-
var iter = self.unicode.graphemeIterator(segment.text);
296
+
var iter = unicode.graphemeIterator(segment.text);
299
297
while (iter.next()) |grapheme| {
300
298
if (col >= self.width) {
301
299
row += 1;
···
378
376
col = 0;
379
377
}
380
378
381
-
var grapheme_iterator = self.unicode.graphemeIterator(word);
379
+
var grapheme_iterator = unicode.graphemeIterator(word);
382
380
while (grapheme_iterator.next()) |grapheme| {
383
381
soft_wrapped = false;
384
382
if (row >= self.height) {
···
417
415
.none => {
418
416
var col: u16 = opts.col_offset;
419
417
const overflow: bool = blk: for (segments) |segment| {
420
-
var iter = self.unicode.graphemeIterator(segment.text);
418
+
var iter = unicode.graphemeIterator(segment.text);
421
419
while (iter.next()) |grapheme| {
422
420
if (col >= self.width) break :blk true;
423
421
const s = grapheme.bytes(segment.text);
···
489
487
.width = 20,
490
488
.height = 20,
491
489
.screen = undefined,
492
-
.unicode = undefined,
493
490
};
494
491
495
492
const ch = parent.initChild(1, 1, null, null);
···
506
503
.width = 20,
507
504
.height = 20,
508
505
.screen = undefined,
509
-
.unicode = undefined,
510
506
};
511
507
512
508
const ch = parent.initChild(0, 0, 21, 21);
···
523
519
.width = 20,
524
520
.height = 20,
525
521
.screen = undefined,
526
-
.unicode = undefined,
527
522
};
528
523
529
524
const ch = parent.initChild(10, 10, 21, 21);
···
540
535
.width = 20,
541
536
.height = 20,
542
537
.screen = undefined,
543
-
.unicode = undefined,
544
538
};
545
539
546
540
const ch = parent.initChild(10, 10, 21, 21);
···
557
551
.width = 20,
558
552
.height = 20,
559
553
.screen = undefined,
560
-
.unicode = undefined,
561
554
};
562
555
563
556
const ch = parent.initChild(10, 10, 21, 21);
···
569
562
}
570
563
571
564
test "print: grapheme" {
572
-
const alloc = std.testing.allocator_instance.allocator();
573
-
const unicode = try Unicode.init(alloc);
574
-
defer unicode.deinit(alloc);
575
565
var screen: Screen = .{ .width_method = .unicode };
576
566
const win: Window = .{
577
567
.x_off = 0,
···
581
571
.width = 4,
582
572
.height = 2,
583
573
.screen = &screen,
584
-
.unicode = &unicode,
585
574
};
586
575
const opts: PrintOptions = .{
587
576
.commit = false,
···
636
625
}
637
626
638
627
test "print: word" {
639
-
const alloc = std.testing.allocator_instance.allocator();
640
-
const unicode = try Unicode.init(alloc);
641
-
defer unicode.deinit(alloc);
642
628
var screen: Screen = .{
643
629
.width_method = .unicode,
644
630
};
···
650
636
.width = 4,
651
637
.height = 2,
652
638
.screen = &screen,
653
-
.unicode = &unicode,
654
639
};
655
640
const opts: PrintOptions = .{
656
641
.commit = false,
+2
-1
src/ctlseqs.zig
+2
-1
src/ctlseqs.zig
···
14
14
pub const cursor_position_request = "\x1b[6n";
15
15
pub const explicit_width_query = "\x1b]66;w=1; \x1b\\";
16
16
pub const scaled_text_query = "\x1b]66;s=2; \x1b\\";
17
+
pub const multi_cursor_query = "\x1b[> q";
17
18
18
19
// mouse. We try for button motion and any motion. terminals will enable the
19
20
// last one we tried (any motion). This was added because zellij doesn't
···
122
123
123
124
// OSC sequences
124
125
pub const osc2_set_title = "\x1b]2;{s}\x1b\\";
125
-
pub const osc7 = "\x1b]7;{;+/}\x1b\\";
126
+
pub const osc7 = "\x1b]7;{f}\x1b\\";
126
127
pub const osc8 = "\x1b]8;{s};{s}\x1b\\";
127
128
pub const osc8_clear = "\x1b]8;;\x1b\\";
128
129
pub const osc9_notify = "\x1b]9;{s}\x1b\\";
+1
src/event.zig
+1
src/event.zig
+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
}
+3
-5
src/main.zig
+3
-5
src/main.zig
···
26
26
pub const widgets = @import("widgets.zig");
27
27
pub const gwidth = @import("gwidth.zig");
28
28
pub const ctlseqs = @import("ctlseqs.zig");
29
-
pub const DisplayWidth = @import("DisplayWidth");
30
29
pub const GraphemeCache = @import("GraphemeCache.zig");
31
-
pub const Graphemes = @import("Graphemes");
32
30
pub const Event = @import("event.zig").Event;
33
-
pub const Unicode = @import("Unicode.zig");
31
+
pub const unicode = @import("unicode.zig");
34
32
35
33
pub const vxfw = @import("vxfw/vxfw.zig");
36
34
···
73
71
ctlseqs.bp_reset ++
74
72
ctlseqs.rmcup;
75
73
76
-
gty.anyWriter().writeAll(reset) catch {};
77
-
74
+
gty.writer().writeAll(reset) catch {};
75
+
gty.writer().flush() catch {};
78
76
gty.deinit();
79
77
}
80
78
}
+48
-46
src/queue.zig
+48
-46
src/queue.zig
···
30
30
self.not_empty.wait(&self.mutex);
31
31
}
32
32
std.debug.assert(!self.isEmptyLH());
33
-
if (self.isFullLH()) {
34
-
// If we are full, wake up a push that might be
35
-
// waiting here.
36
-
self.not_full.signal();
37
-
}
38
-
39
-
return self.popLH();
33
+
return self.popAndSignalLH();
40
34
}
41
35
42
36
/// Push an item into the queue. Blocks until an item has been
···
48
42
self.not_full.wait(&self.mutex);
49
43
}
50
44
std.debug.assert(!self.isFullLH());
51
-
const was_empty = self.isEmptyLH();
52
-
53
-
self.buf[self.mask(self.write_index)] = item;
54
-
self.write_index = self.mask2(self.write_index + 1);
55
-
56
-
// If we were empty, wake up a pop if it was waiting.
57
-
if (was_empty) {
58
-
self.not_empty.signal();
59
-
}
45
+
self.pushAndSignalLH(item);
60
46
}
61
47
62
48
/// Push an item into the queue. Returns true when the item
···
64
50
/// was full.
65
51
pub fn tryPush(self: *Self, item: T) bool {
66
52
self.mutex.lock();
67
-
if (self.isFullLH()) {
68
-
self.mutex.unlock();
69
-
return false;
70
-
}
71
-
self.mutex.unlock();
72
-
self.push(item);
53
+
defer self.mutex.unlock();
54
+
if (self.isFullLH()) return false;
55
+
self.pushAndSignalLH(item);
73
56
return true;
74
57
}
75
58
···
77
60
/// available.
78
61
pub fn tryPop(self: *Self) ?T {
79
62
self.mutex.lock();
80
-
if (self.isEmptyLH()) {
81
-
self.mutex.unlock();
82
-
return null;
83
-
}
84
-
self.mutex.unlock();
85
-
return self.pop();
63
+
defer self.mutex.unlock();
64
+
if (self.isEmptyLH()) return null;
65
+
return self.popAndSignalLH();
86
66
}
87
67
88
68
/// Poll the queue. This call blocks until events are in the queue
···
150
130
return index % (2 * self.buf.len);
151
131
}
152
132
133
+
fn pushAndSignalLH(self: *Self, item: T) void {
134
+
const was_empty = self.isEmptyLH();
135
+
self.buf[self.mask(self.write_index)] = item;
136
+
self.write_index = self.mask2(self.write_index + 1);
137
+
if (was_empty) {
138
+
self.not_empty.signal();
139
+
}
140
+
}
141
+
142
+
fn popAndSignalLH(self: *Self) T {
143
+
const was_full = self.isFullLH();
144
+
const result = self.popLH();
145
+
if (was_full) {
146
+
self.not_full.signal();
147
+
}
148
+
return result;
149
+
}
150
+
153
151
fn popLH(self: *Self) T {
154
152
const result = self.buf[self.mask(self.read_index)];
155
153
self.read_index = self.mask2(self.read_index + 1);
···
204
202
thread.join();
205
203
}
206
204
207
-
fn sleepyPop(q: *Queue(u8, 2)) !void {
205
+
fn sleepyPop(q: *Queue(u8, 2), state: *atomic.Value(u8)) !void {
208
206
// First we wait for the queue to be full.
209
-
while (!q.isFull())
207
+
while (state.load(.acquire) < 1)
210
208
try Thread.yield();
211
209
212
210
// Then we spuriously wake it up, because that's a thing that can
···
220
218
// still full and the push in the other thread is still blocked
221
219
// waiting for space.
222
220
try Thread.yield();
223
-
std.Thread.sleep(std.time.ns_per_s);
221
+
std.Thread.sleep(10 * std.time.ns_per_ms);
224
222
// Finally, let that other thread go.
225
223
try std.testing.expectEqual(1, q.pop());
226
224
227
-
// This won't continue until the other thread has had a chance to
228
-
// put at least one item in the queue.
229
-
while (!q.isFull())
225
+
// Wait for the other thread to signal it's ready for second push
226
+
while (state.load(.acquire) < 2)
230
227
try Thread.yield();
231
228
// But we want to ensure that there's a second push waiting, so
232
229
// here's another sleep.
233
-
std.Thread.sleep(std.time.ns_per_s / 2);
230
+
std.Thread.sleep(10 * std.time.ns_per_ms);
234
231
235
232
// Another spurious wake...
236
233
q.not_full.signal();
···
238
235
// And another chance for the other thread to see that it's
239
236
// spurious and go back to sleep.
240
237
try Thread.yield();
241
-
std.Thread.sleep(std.time.ns_per_s / 2);
238
+
std.Thread.sleep(10 * std.time.ns_per_ms);
242
239
243
240
// Pop that thing and we're done.
244
241
try std.testing.expectEqual(2, q.pop());
···
252
249
// fails if the while loop in `push` is turned into an `if`.
253
250
254
251
var queue: Queue(u8, 2) = .{};
255
-
const thread = try Thread.spawn(cfg, sleepyPop, .{&queue});
252
+
var state = atomic.Value(u8).init(0);
253
+
const thread = try Thread.spawn(cfg, sleepyPop, .{ &queue, &state });
256
254
queue.push(1);
257
255
queue.push(2);
256
+
state.store(1, .release);
258
257
const now = std.time.milliTimestamp();
259
258
queue.push(3); // This one should block.
260
259
const then = std.time.milliTimestamp();
261
260
262
261
// Just to make sure the sleeps are yielding to this thread, make
263
-
// sure it took at least 900ms to do the push.
264
-
try std.testing.expect(then - now > 900);
262
+
// sure it took at least 5ms to do the push.
263
+
try std.testing.expect(then - now > 5);
265
264
265
+
state.store(2, .release);
266
266
// This should block again, waiting for the other thread.
267
267
queue.push(4);
268
268
···
272
272
try std.testing.expectEqual(4, queue.pop());
273
273
}
274
274
275
-
fn sleepyPush(q: *Queue(u8, 1)) !void {
275
+
fn sleepyPush(q: *Queue(u8, 1), state: *atomic.Value(u8)) !void {
276
276
// Try to ensure the other thread has already started trying to pop.
277
277
try Thread.yield();
278
-
std.Thread.sleep(std.time.ns_per_s / 2);
278
+
std.Thread.sleep(10 * std.time.ns_per_ms);
279
279
280
280
// Spurious wake
281
281
q.not_full.signal();
282
282
q.not_empty.signal();
283
283
284
284
try Thread.yield();
285
-
std.Thread.sleep(std.time.ns_per_s / 2);
285
+
std.Thread.sleep(10 * std.time.ns_per_ms);
286
286
287
287
// Stick something in the queue so it can be popped.
288
288
q.push(1);
289
289
// Ensure it's been popped.
290
-
while (!q.isEmpty())
290
+
while (state.load(.acquire) < 1)
291
291
try Thread.yield();
292
292
// Give the other thread time to block again.
293
293
try Thread.yield();
294
-
std.Thread.sleep(std.time.ns_per_s / 2);
294
+
std.Thread.sleep(10 * std.time.ns_per_ms);
295
295
296
296
// Spurious wake
297
297
q.not_full.signal();
···
306
306
// `if`.
307
307
308
308
var queue: Queue(u8, 1) = .{};
309
-
const thread = try Thread.spawn(cfg, sleepyPush, .{&queue});
309
+
var state = atomic.Value(u8).init(0);
310
+
const thread = try Thread.spawn(cfg, sleepyPush, .{ &queue, &state });
310
311
try std.testing.expectEqual(1, queue.pop());
312
+
state.store(1, .release);
311
313
try std.testing.expectEqual(2, queue.pop());
312
314
thread.join();
313
315
}
···
322
324
const t1 = try Thread.spawn(cfg, readerThread, .{&queue});
323
325
const t2 = try Thread.spawn(cfg, readerThread, .{&queue});
324
326
try Thread.yield();
325
-
std.Thread.sleep(std.time.ns_per_s / 2);
327
+
std.Thread.sleep(10 * std.time.ns_per_ms);
326
328
queue.push(1);
327
329
queue.push(1);
328
330
t1.join();
+19
-22
src/tty.zig
+19
-22
src/tty.zig
···
33
33
/// The file descriptor of the tty
34
34
fd: posix.fd_t,
35
35
36
-
reader: std.fs.File.Reader,
37
-
38
36
/// File.Writer for efficient buffered writing
39
-
writer: std.fs.File.Writer,
37
+
tty_writer: std.fs.File.Writer,
40
38
41
39
pub const SignalHandler = struct {
42
40
context: *anyopaque,
···
76
74
const self: PosixTty = .{
77
75
.fd = fd,
78
76
.termios = termios,
79
-
.reader = file.reader(),
80
-
.writer = .initStreaming(file, buffer),
77
+
.tty_writer = .initStreaming(file, buffer),
81
78
};
82
79
83
80
global_tty = self;
···
109
106
posix.sigaction(posix.SIG.WINCH, &act, null);
110
107
}
111
108
112
-
pub fn anyWriter(self: *PosixTty) *std.Io.Writer {
113
-
return &self.writer.interface;
109
+
pub fn writer(self: *PosixTty) *std.Io.Writer {
110
+
return &self.tty_writer.interface;
114
111
}
115
112
116
113
pub fn read(self: *const PosixTty, buf: []u8) !usize {
···
201
198
buf: [4]u8 = undefined,
202
199
203
200
/// File.Writer for efficient buffered writing
204
-
reader: std.fs.File.Writer,
205
-
writer: std.fs.File.Writer,
201
+
tty_writer: std.fs.File.Writer,
206
202
207
203
/// The last mouse button that was pressed. We store the previous state of button presses on each
208
204
/// mouse event so we can detect which button was released
···
226
222
};
227
223
228
224
pub fn init(buffer: []u8) !Tty {
229
-
const stdin: std.fs.File = .stdout();
225
+
const stdin: std.fs.File = .stdin();
230
226
const stdout: std.fs.File = .stdout();
231
227
232
228
// get initial modes
···
246
242
.initial_codepage = initial_output_codepage,
247
243
.initial_input_mode = initial_input_mode,
248
244
.initial_output_mode = initial_output_mode,
249
-
.writer = .initStreaming(stdout, buffer),
245
+
.tty_writer = .initStreaming(stdout, buffer),
250
246
};
251
247
252
248
// save a copy of this tty as the global_tty for panic handling
···
301
297
};
302
298
}
303
299
304
-
pub fn anyWriter(self: *Tty) *std.Io.Writer {
305
-
return &self.writer.interface;
300
+
pub fn writer(self: *Tty) *std.Io.Writer {
301
+
return &self.tty_writer.interface;
306
302
}
307
303
308
304
pub fn read(self: *const Tty, buf: []u8) !usize {
···
457
453
0xc0 => '`',
458
454
0xdb => '[',
459
455
0xdc => '\\',
456
+
0xdf => '\\',
460
457
0xe2 => '\\',
461
458
0xdd => ']',
462
459
0xde => '\'',
···
579
576
};
580
577
581
578
const mouse: Mouse = .{
582
-
.col = @as(u16, @bitCast(event.dwMousePosition.X)), // Windows reports with 0 index
583
-
.row = @as(u16, @bitCast(event.dwMousePosition.Y)), // Windows reports with 0 index
579
+
.col = @as(i16, @bitCast(event.dwMousePosition.X)), // Windows reports with 0 index
580
+
.row = @as(i16, @bitCast(event.dwMousePosition.Y)), // Windows reports with 0 index
584
581
.mods = mods,
585
582
.type = event_type,
586
583
.button = btn,
···
691
688
};
692
689
pub const PINPUT_RECORD = *INPUT_RECORD;
693
690
694
-
pub extern "kernel32" fn ReadConsoleInputW(hConsoleInput: windows.HANDLE, lpBuffer: PINPUT_RECORD, nLength: windows.DWORD, lpNumberOfEventsRead: *windows.DWORD) callconv(windows.WINAPI) windows.BOOL;
691
+
pub extern "kernel32" fn ReadConsoleInputW(hConsoleInput: windows.HANDLE, lpBuffer: PINPUT_RECORD, nLength: windows.DWORD, lpNumberOfEventsRead: *windows.DWORD) callconv(.winapi) windows.BOOL;
695
692
};
696
693
697
694
pub const TestTty = struct {
···
699
696
fd: posix.fd_t,
700
697
pipe_read: posix.fd_t,
701
698
pipe_write: posix.fd_t,
702
-
writer: *std.Io.Writer.Allocating,
699
+
tty_writer: *std.Io.Writer.Allocating,
703
700
704
701
/// Initializes a TestTty.
705
702
pub fn init(buffer: []u8) !TestTty {
···
713
710
.fd = r,
714
711
.pipe_read = r,
715
712
.pipe_write = w,
716
-
.writer = list,
713
+
.tty_writer = list,
717
714
};
718
715
}
719
716
720
717
pub fn deinit(self: TestTty) void {
721
718
std.posix.close(self.pipe_read);
722
719
std.posix.close(self.pipe_write);
723
-
self.writer.deinit();
724
-
std.testing.allocator.destroy(self.writer);
720
+
self.tty_writer.deinit();
721
+
std.testing.allocator.destroy(self.tty_writer);
725
722
}
726
723
727
-
pub fn anyWriter(self: *TestTty) *std.Io.Writer {
728
-
return &self.writer.writer;
724
+
pub fn writer(self: *TestTty) *std.Io.Writer {
725
+
return &self.tty_writer.writer;
729
726
}
730
727
731
728
pub fn read(self: *const TestTty, buf: []u8) !usize {
+64
src/unicode.zig
+64
src/unicode.zig
···
1
+
const std = @import("std");
2
+
const uucode = @import("uucode");
3
+
4
+
// Old API-compatible Grapheme value
5
+
pub const Grapheme = struct {
6
+
start: usize,
7
+
len: usize,
8
+
9
+
pub fn bytes(self: Grapheme, str: []const u8) []const u8 {
10
+
return str[self.start .. self.start + self.len];
11
+
}
12
+
};
13
+
14
+
// Old API-compatible iterator that yields Grapheme with .len and .bytes()
15
+
pub const GraphemeIterator = struct {
16
+
str: []const u8,
17
+
inner: uucode.grapheme.Iterator(uucode.utf8.Iterator),
18
+
start: usize = 0,
19
+
prev_break: bool = true,
20
+
21
+
pub fn init(str: []const u8) GraphemeIterator {
22
+
return .{
23
+
.str = str,
24
+
.inner = uucode.grapheme.Iterator(uucode.utf8.Iterator).init(.init(str)),
25
+
};
26
+
}
27
+
28
+
pub fn next(self: *GraphemeIterator) ?Grapheme {
29
+
while (self.inner.next()) |res| {
30
+
// When leaving a break and entering a non-break, set the start of a cluster
31
+
if (self.prev_break and !res.is_break) {
32
+
const cp_len: usize = std.unicode.utf8CodepointSequenceLength(res.cp) catch 1;
33
+
self.start = self.inner.i - cp_len;
34
+
}
35
+
36
+
// A break marks the end of the current grapheme
37
+
if (res.is_break) {
38
+
const end = self.inner.i;
39
+
const s = self.start;
40
+
self.start = end;
41
+
self.prev_break = true;
42
+
return .{ .start = s, .len = end - s };
43
+
}
44
+
45
+
self.prev_break = false;
46
+
}
47
+
48
+
// Flush the last grapheme if we ended mid-cluster
49
+
if (!self.prev_break and self.start < self.str.len) {
50
+
const s = self.start;
51
+
const len = self.str.len - s;
52
+
self.start = self.str.len;
53
+
self.prev_break = true;
54
+
return .{ .start = s, .len = len };
55
+
}
56
+
57
+
return null;
58
+
}
59
+
};
60
+
61
+
/// creates a grapheme iterator based on str
62
+
pub fn graphemeIterator(str: []const u8) GraphemeIterator {
63
+
return GraphemeIterator.init(str);
64
+
}
+16
-18
src/vxfw/App.zig
+16
-18
src/vxfw/App.zig
···
47
47
48
48
pub fn deinit(self: *App) void {
49
49
self.timers.deinit(self.allocator);
50
-
self.vx.deinit(self.allocator, self.tty.anyWriter());
50
+
self.vx.deinit(self.allocator, self.tty.writer());
51
51
self.tty.deinit();
52
52
}
53
53
···
64
64
// Also always initialize the app with a focus event
65
65
loop.postEvent(.focus_in);
66
66
67
-
try vx.enterAltScreen(tty.anyWriter());
68
-
try vx.queryTerminal(tty.anyWriter(), 1 * std.time.ns_per_s);
69
-
try vx.setBracketedPaste(tty.anyWriter(), true);
70
-
try vx.subscribeToColorSchemeUpdates(tty.anyWriter());
67
+
try vx.enterAltScreen(tty.writer());
68
+
try vx.queryTerminal(tty.writer(), 1 * std.time.ns_per_s);
69
+
try vx.setBracketedPaste(tty.writer(), true);
70
+
try vx.subscribeToColorSchemeUpdates(tty.writer());
71
71
72
72
{
73
73
// This part deserves a comment. loop.init installs a signal handler for the tty. We wait to
···
78
78
79
79
// NOTE: We don't use pixel mouse anywhere
80
80
vx.caps.sgr_pixels = false;
81
-
try vx.setMouseMode(tty.anyWriter(), true);
81
+
try vx.setMouseMode(tty.writer(), true);
82
82
83
-
// Give DrawContext the unicode data
84
-
vxfw.DrawContext.init(&vx.unicode, vx.screen.width_method);
83
+
vxfw.DrawContext.init(vx.screen.width_method);
85
84
86
85
const framerate: u64 = if (opts.framerate > 0) opts.framerate else 60;
87
86
// Calculate tick rate
···
149
148
},
150
149
.mouse => |mouse| try mouse_handler.handleMouse(self, &ctx, mouse),
151
150
.winsize => |ws| {
152
-
try vx.resize(self.allocator, tty.anyWriter(), ws);
151
+
try vx.resize(self.allocator, tty.writer(), ws);
153
152
ctx.redraw = true;
154
153
},
155
154
else => {
···
246
245
});
247
246
surface.render(root_win, focused_widget);
248
247
249
-
try vx.render(tty.anyWriter());
248
+
try vx.render(tty.writer());
250
249
}
251
250
252
251
fn addTick(self: *App, tick: vxfw.Tick) Allocator.Error!void {
···
262
261
.set_mouse_shape => |shape| self.vx.setMouseShape(shape),
263
262
.request_focus => |widget| self.wants_focus = widget,
264
263
.copy_to_clipboard => |content| {
265
-
self.vx.copyToSystemClipboard(self.tty.anyWriter(), content, self.allocator) catch |err| {
264
+
defer self.allocator.free(content);
265
+
self.vx.copyToSystemClipboard(self.tty.writer(), content, self.allocator) catch |err| {
266
266
switch (err) {
267
267
error.OutOfMemory => return Allocator.Error.OutOfMemory,
268
268
else => std.log.err("copy error: {}", .{err}),
···
270
270
};
271
271
},
272
272
.set_title => |title| {
273
-
self.vx.setTitle(self.tty.anyWriter(), title) catch |err| {
273
+
defer self.allocator.free(title);
274
+
self.vx.setTitle(self.tty.writer(), title) catch |err| {
274
275
std.log.err("set_title error: {}", .{err});
275
276
};
276
277
},
277
278
.queue_refresh => self.vx.queueRefresh(),
278
279
.notify => |notification| {
279
-
self.vx.notify(self.tty.anyWriter(), notification.title, notification.body) catch |err| {
280
+
self.vx.notify(self.tty.writer(), notification.title, notification.body) catch |err| {
280
281
std.log.err("notify error: {}", .{err});
281
282
};
282
283
const alloc = self.allocator;
···
286
287
alloc.free(notification.body);
287
288
},
288
289
.query_color => |kind| {
289
-
self.vx.queryColor(self.tty.anyWriter(), kind) catch |err| {
290
+
self.vx.queryColor(self.tty.writer(), kind) catch |err| {
290
291
std.log.err("queryColor error: {}", .{err});
291
292
};
292
293
},
···
531
532
// Find the path to the focused widget. This builds a list that has the first element as the
532
533
// focused widget, and walks backward to the root. It's possible our focused widget is *not*
533
534
// in this tree. If this is the case, we refocus to the root widget
534
-
const has_focus = try self.childHasFocus(allocator, surface);
535
+
_ = try self.childHasFocus(allocator, surface);
535
536
536
-
// We assert that the focused widget *must* be in the widget tree. There is certianly a
537
-
// logic bug in the code somewhere if this is not the case
538
-
assert(has_focus); // Focused widget not found in Surface tree
539
537
if (!self.root.eql(surface.widget)) {
540
538
// If the root of surface is not the initial widget, we append the initial widget
541
539
try self.path_to_focused.append(allocator, self.root);
+1
-2
src/vxfw/Border.zig
+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;
+13
-12
src/vxfw/vxfw.zig
+13
-12
src/vxfw/vxfw.zig
···
1
1
const std = @import("std");
2
2
const vaxis = @import("../main.zig");
3
+
const uucode = @import("uucode");
3
4
4
-
const Graphemes = vaxis.Graphemes;
5
5
const testing = std.testing;
6
6
7
7
const assert = std.debug.assert;
···
141
141
try self.addCmd(.{ .request_focus = widget });
142
142
}
143
143
144
+
/// Copy content to clipboard.
145
+
/// content is duplicated using self.alloc.
146
+
/// Caller retains ownership of their copy of content.
144
147
pub fn copyToClipboard(self: *EventContext, content: []const u8) Allocator.Error!void {
145
-
try self.addCmd(.{ .copy_to_clipboard = content });
148
+
try self.addCmd(.{ .copy_to_clipboard = try self.alloc.dupe(u8, content) });
146
149
}
147
150
151
+
/// Set window title.
152
+
/// title is duplicated using self.alloc.
153
+
/// Caller retains ownership of their copy of title.
148
154
pub fn setTitle(self: *EventContext, title: []const u8) Allocator.Error!void {
149
-
try self.addCmd(.{ .set_title = title });
155
+
try self.addCmd(.{ .set_title = try self.alloc.dupe(u8, title) });
150
156
}
151
157
152
158
pub fn queueRefresh(self: *EventContext) Allocator.Error!void {
···
161
167
maybe_title: ?[]const u8,
162
168
body: []const u8,
163
169
) Allocator.Error!void {
164
-
const alloc = self.cmds.allocator;
170
+
const alloc = self.alloc;
165
171
if (maybe_title) |title| {
166
172
return self.addCmd(.{ .notify = .{
167
173
.title = try alloc.dupe(u8, title),
···
191
197
cell_size: Size,
192
198
193
199
// Unicode stuff
194
-
var unicode: ?*const vaxis.Unicode = null;
195
200
var width_method: vaxis.gwidth.Method = .unicode;
196
201
197
-
pub fn init(ucd: *const vaxis.Unicode, method: vaxis.gwidth.Method) void {
198
-
DrawContext.unicode = ucd;
202
+
pub fn init(method: vaxis.gwidth.Method) void {
199
203
DrawContext.width_method = method;
200
204
}
201
205
202
206
pub fn stringWidth(_: DrawContext, str: []const u8) usize {
203
-
assert(DrawContext.unicode != null); // DrawContext not initialized
204
207
return vaxis.gwidth.gwidth(
205
208
str,
206
209
DrawContext.width_method,
207
-
&DrawContext.unicode.?.width_data,
208
210
);
209
211
}
210
212
211
-
pub fn graphemeIterator(_: DrawContext, str: []const u8) Graphemes.Iterator {
212
-
assert(DrawContext.unicode != null); // DrawContext not initialized
213
-
return DrawContext.unicode.?.graphemeIterator(str);
213
+
pub fn graphemeIterator(_: DrawContext, str: []const u8) vaxis.unicode.GraphemeIterator {
214
+
return vaxis.unicode.graphemeIterator(str);
214
215
}
215
216
216
217
pub fn withConstraints(self: DrawContext, min: Size, max: MaxSize) DrawContext {
+1
-1
src/widgets/Table.zig
+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
+8
-8
src/widgets/terminal/Command.zig
+8
-8
src/widgets/terminal/Command.zig
···
36
36
37
37
// set the controlling terminal
38
38
var u: c_uint = std.posix.STDIN_FILENO;
39
-
if (posix.system.ioctl(self.pty.tty, posix.T.IOCSCTTY, @intFromPtr(&u)) != 0) return error.IoctlError;
39
+
if (posix.system.ioctl(self.pty.tty.handle, posix.T.IOCSCTTY, @intFromPtr(&u)) != 0) return error.IoctlError;
40
40
41
41
// set up io
42
-
try posix.dup2(self.pty.tty, std.posix.STDIN_FILENO);
43
-
try posix.dup2(self.pty.tty, std.posix.STDOUT_FILENO);
44
-
try posix.dup2(self.pty.tty, std.posix.STDERR_FILENO);
42
+
try posix.dup2(self.pty.tty.handle, std.posix.STDIN_FILENO);
43
+
try posix.dup2(self.pty.tty.handle, std.posix.STDOUT_FILENO);
44
+
try posix.dup2(self.pty.tty.handle, std.posix.STDERR_FILENO);
45
45
46
-
posix.close(self.pty.tty);
47
-
if (self.pty.pty > 2) posix.close(self.pty.pty);
46
+
self.pty.tty.close();
47
+
if (self.pty.pty.handle > 2) self.pty.pty.close();
48
48
49
49
if (self.working_directory) |wd| {
50
50
try std.posix.chdir(wd);
···
75
75
return;
76
76
}
77
77
78
-
fn handleSigChild(_: c_int) callconv(.C) void {
78
+
fn handleSigChild(_: c_int) callconv(.c) void {
79
79
const result = std.posix.waitpid(-1, 0);
80
80
81
81
Terminal.global_vt_mutex.lock();
···
107
107
{
108
108
var it = map.iterator();
109
109
while (it.next()) |pair| {
110
-
envp_buf[i] = try std.fmt.allocPrintZ(arena, "{s}={s}", .{ pair.key_ptr.*, pair.value_ptr.* });
110
+
envp_buf[i] = try std.fmt.allocPrintSentinel(arena, "{s}={s}", .{ pair.key_ptr.*, pair.value_ptr.* }, 0);
111
111
i += 1;
112
112
}
113
113
}
+26
-28
src/widgets/terminal/Parser.zig
+26
-28
src/widgets/terminal/Parser.zig
···
2
2
const Parser = @This();
3
3
4
4
const std = @import("std");
5
-
const Reader = std.io.AnyReader;
5
+
const Reader = std.Io.Reader;
6
6
const ansi = @import("ansi.zig");
7
-
const BufferedReader = std.io.BufferedReader(4096, std.io.AnyReader);
8
7
9
8
/// A terminal event
10
9
const Event = union(enum) {
···
18
17
apc: []const u8,
19
18
};
20
19
21
-
buf: std.ArrayList(u8),
20
+
buf: std.array_list.Managed(u8),
22
21
/// a leftover byte from a ground event
23
22
pending_byte: ?u8 = null,
24
23
25
-
pub fn parseReader(self: *Parser, buffered: *BufferedReader) !Event {
26
-
const reader = buffered.reader().any();
24
+
pub fn parseReader(self: *Parser, reader: *Reader) !Event {
27
25
self.buf.clearRetainingCapacity();
28
26
while (true) {
29
-
const b = if (self.pending_byte) |p| p else try reader.readByte();
27
+
const b = if (self.pending_byte) |p| p else try reader.takeByte();
30
28
self.pending_byte = null;
31
29
switch (b) {
32
30
// Escape sequence
33
31
0x1b => {
34
-
const next = try reader.readByte();
32
+
const next = try reader.takeByte();
35
33
switch (next) {
36
-
0x4E => return .{ .ss2 = try reader.readByte() },
37
-
0x4F => return .{ .ss3 = try reader.readByte() },
34
+
0x4E => return .{ .ss2 = try reader.takeByte() },
35
+
0x4F => return .{ .ss3 = try reader.takeByte() },
38
36
0x50 => try skipUntilST(reader), // DCS
39
37
0x58 => try skipUntilST(reader), // SOS
40
38
0x5B => return self.parseCsi(reader), // CSI
···
58
56
=> return .{ .c0 = @enumFromInt(b) },
59
57
else => {
60
58
try self.buf.append(b);
61
-
return self.parseGround(buffered);
59
+
return self.parseGround(reader);
62
60
},
63
61
}
64
62
}
65
63
}
66
64
67
-
inline fn parseGround(self: *Parser, reader: *BufferedReader) !Event {
65
+
inline fn parseGround(self: *Parser, reader: *Reader) !Event {
68
66
var buf: [1]u8 = undefined;
69
67
{
70
68
std.debug.assert(self.buf.items.len > 0);
···
72
70
const len = try std.unicode.utf8ByteSequenceLength(self.buf.items[0]);
73
71
var i: usize = 1;
74
72
while (i < len) : (i += 1) {
75
-
const read = try reader.read(&buf);
73
+
const read = try reader.readSliceShort(&buf);
76
74
if (read == 0) return error.EOF;
77
75
try self.buf.append(buf[0]);
78
76
}
79
77
}
80
78
while (true) {
81
-
if (reader.start == reader.end) return .{ .print = self.buf.items };
82
-
const n = try reader.read(&buf);
79
+
if (reader.bufferedLen() == 0) return .{ .print = self.buf.items };
80
+
const n = try reader.readSliceShort(&buf);
83
81
if (n == 0) return error.EOF;
84
82
const b = buf[0];
85
83
switch (b) {
···
92
90
const len = try std.unicode.utf8ByteSequenceLength(b);
93
91
var i: usize = 1;
94
92
while (i < len) : (i += 1) {
95
-
const read = try reader.read(&buf);
93
+
const read = try reader.readSliceShort(&buf);
96
94
if (read == 0) return error.EOF;
97
95
98
96
try self.buf.append(buf[0]);
···
103
101
}
104
102
105
103
/// parse until b >= 0x30
106
-
inline fn parseEscape(self: *Parser, reader: Reader) !Event {
104
+
inline fn parseEscape(self: *Parser, reader: *Reader) !Event {
107
105
while (true) {
108
-
const b = try reader.readByte();
106
+
const b = try reader.takeByte();
109
107
switch (b) {
110
108
0x20...0x2F => continue,
111
109
else => {
···
116
114
}
117
115
}
118
116
119
-
inline fn parseApc(self: *Parser, reader: Reader) !Event {
117
+
inline fn parseApc(self: *Parser, reader: *Reader) !Event {
120
118
while (true) {
121
-
const b = try reader.readByte();
119
+
const b = try reader.takeByte();
122
120
switch (b) {
123
121
0x00...0x17,
124
122
0x19,
125
123
0x1c...0x1f,
126
124
=> continue,
127
125
0x1b => {
128
-
try reader.skipBytes(1, .{ .buf_size = 1 });
126
+
_ = try reader.discard(std.Io.Limit.limited(1));
129
127
return .{ .apc = self.buf.items };
130
128
},
131
129
else => try self.buf.append(b),
···
134
132
}
135
133
136
134
/// Skips sequences until we see an ST (String Terminator, ESC \)
137
-
inline fn skipUntilST(reader: Reader) !void {
138
-
try reader.skipUntilDelimiterOrEof('\x1b');
139
-
try reader.skipBytes(1, .{ .buf_size = 1 });
135
+
inline fn skipUntilST(reader: *Reader) !void {
136
+
_ = try reader.discardDelimiterExclusive('\x1b');
137
+
_ = try reader.discard(std.Io.Limit.limited(1));
140
138
}
141
139
142
140
/// Parses an OSC sequence
143
-
inline fn parseOsc(self: *Parser, reader: Reader) !Event {
141
+
inline fn parseOsc(self: *Parser, reader: *Reader) !Event {
144
142
while (true) {
145
-
const b = try reader.readByte();
143
+
const b = try reader.takeByte();
146
144
switch (b) {
147
145
0x00...0x06,
148
146
0x08...0x17,
···
150
148
0x1c...0x1f,
151
149
=> continue,
152
150
0x1b => {
153
-
try reader.skipBytes(1, .{ .buf_size = 1 });
151
+
_ = try reader.discard(std.Io.Limit.limited(1));
154
152
return .{ .osc = self.buf.items };
155
153
},
156
154
0x07 => return .{ .osc = self.buf.items },
···
159
157
}
160
158
}
161
159
162
-
inline fn parseCsi(self: *Parser, reader: Reader) !Event {
160
+
inline fn parseCsi(self: *Parser, reader: *Reader) !Event {
163
161
var intermediate: ?u8 = null;
164
162
var pm: ?u8 = null;
165
163
166
164
while (true) {
167
-
const b = try reader.readByte();
165
+
const b = try reader.takeByte();
168
166
switch (b) {
169
167
0x20...0x2F => intermediate = b,
170
168
0x30...0x3B => try self.buf.append(b),
+7
-7
src/widgets/terminal/Pty.zig
+7
-7
src/widgets/terminal/Pty.zig
···
7
7
8
8
const posix = std.posix;
9
9
10
-
pty: posix.fd_t,
11
-
tty: posix.fd_t,
10
+
pty: std.fs.File,
11
+
tty: std.fs.File,
12
12
13
13
/// opens a new tty/pty pair
14
14
pub fn init() !Pty {
···
20
20
21
21
/// closes the tty and pty
22
22
pub fn deinit(self: Pty) void {
23
-
posix.close(self.pty);
24
-
posix.close(self.tty);
23
+
self.pty.close();
24
+
self.tty.close();
25
25
}
26
26
27
27
/// sets the size of the pty
···
32
32
.xpixel = @truncate(ws.x_pixel),
33
33
.ypixel = @truncate(ws.y_pixel),
34
34
};
35
-
if (posix.system.ioctl(self.pty, posix.T.IOCSWINSZ, @intFromPtr(&_ws)) != 0)
35
+
if (posix.system.ioctl(self.pty.handle, posix.T.IOCSWINSZ, @intFromPtr(&_ws)) != 0)
36
36
return error.SetWinsizeError;
37
37
}
38
38
···
53
53
const t = try posix.open(sname, .{ .ACCMODE = .RDWR, .NOCTTY = true }, 0);
54
54
55
55
return .{
56
-
.pty = p,
57
-
.tty = t,
56
+
.pty = .{ .handle = p },
57
+
.tty = .{ .handle = t },
58
58
};
59
59
}
+34
-33
src/widgets/terminal/Screen.zig
+34
-33
src/widgets/terminal/Screen.zig
···
9
9
const Screen = @This();
10
10
11
11
pub const Cell = struct {
12
-
char: std.ArrayList(u8) = undefined,
12
+
char: std.ArrayList(u8) = .empty,
13
13
style: vaxis.Style = .{},
14
-
uri: std.ArrayList(u8) = undefined,
15
-
uri_id: std.ArrayList(u8) = undefined,
14
+
uri: std.ArrayList(u8) = .empty,
15
+
uri_id: std.ArrayList(u8) = .empty,
16
16
width: u8 = 1,
17
17
18
18
wrapped: bool = false,
19
19
dirty: bool = true,
20
20
21
-
pub fn erase(self: *Cell, bg: vaxis.Color) void {
21
+
pub fn erase(self: *Cell, allocator: std.mem.Allocator, bg: vaxis.Color) void {
22
22
self.char.clearRetainingCapacity();
23
-
self.char.append(' ') catch unreachable; // we never completely free this list
23
+
self.char.append(allocator, ' ') catch unreachable; // we never completely free this list
24
24
self.style = .{};
25
25
self.style.bg = bg;
26
26
self.uri.clearRetainingCapacity();
···
30
30
self.dirty = true;
31
31
}
32
32
33
-
pub fn copyFrom(self: *Cell, src: Cell) !void {
33
+
pub fn copyFrom(self: *Cell, allocator: std.mem.Allocator, src: Cell) !void {
34
34
self.char.clearRetainingCapacity();
35
-
try self.char.appendSlice(src.char.items);
35
+
try self.char.appendSlice(allocator, src.char.items);
36
36
self.style = src.style;
37
37
self.uri.clearRetainingCapacity();
38
-
try self.uri.appendSlice(src.uri.items);
38
+
try self.uri.appendSlice(allocator, src.uri.items);
39
39
self.uri_id.clearRetainingCapacity();
40
-
try self.uri_id.appendSlice(src.uri_id.items);
40
+
try self.uri_id.appendSlice(allocator, src.uri_id.items);
41
41
self.width = src.width;
42
42
self.wrapped = src.wrapped;
43
43
···
81
81
}
82
82
};
83
83
84
+
allocator: std.mem.Allocator,
85
+
84
86
width: u16 = 0,
85
87
height: u16 = 0,
86
88
···
95
97
/// sets each cell to the default cell
96
98
pub fn init(alloc: std.mem.Allocator, w: u16, h: u16) !Screen {
97
99
var screen = Screen{
100
+
.allocator = alloc,
98
101
.buf = try alloc.alloc(Cell, @as(usize, @intCast(w)) * h),
99
102
.scrolling_region = .{
100
103
.top = 0,
···
107
110
};
108
111
for (screen.buf, 0..) |_, i| {
109
112
screen.buf[i] = .{
110
-
.char = try std.ArrayList(u8).initCapacity(alloc, 1),
111
-
.uri = std.ArrayList(u8).init(alloc),
112
-
.uri_id = std.ArrayList(u8).init(alloc),
113
+
.char = try .initCapacity(alloc, 1),
113
114
};
114
-
try screen.buf[i].char.append(' ');
115
+
try screen.buf[i].char.append(alloc, ' ');
115
116
}
116
117
return screen;
117
118
}
118
119
119
120
pub fn deinit(self: *Screen, alloc: std.mem.Allocator) void {
120
121
for (self.buf, 0..) |_, i| {
121
-
self.buf[i].char.deinit();
122
-
self.buf[i].uri.deinit();
123
-
self.buf[i].uri_id.deinit();
122
+
self.buf[i].char.deinit(alloc);
123
+
self.buf[i].uri.deinit(alloc);
124
+
self.buf[i].uri_id.deinit(alloc);
124
125
}
125
126
126
127
alloc.free(self.buf);
127
128
}
128
129
129
130
/// copies the visible area to the destination screen
130
-
pub fn copyTo(self: *Screen, dst: *Screen) !void {
131
+
pub fn copyTo(self: *Screen, allocator: std.mem.Allocator, dst: *Screen) !void {
131
132
dst.cursor = self.cursor;
132
133
for (self.buf, 0..) |cell, i| {
133
134
if (!cell.dirty) continue;
134
135
self.buf[i].dirty = false;
135
136
const grapheme = cell.char.items;
136
137
dst.buf[i].char.clearRetainingCapacity();
137
-
try dst.buf[i].char.appendSlice(grapheme);
138
+
try dst.buf[i].char.appendSlice(allocator, grapheme);
138
139
dst.buf[i].width = cell.width;
139
140
dst.buf[i].style = cell.style;
140
141
}
···
182
183
const i = (row * self.width) + col;
183
184
assert(i < self.buf.len);
184
185
self.buf[i].char.clearRetainingCapacity();
185
-
self.buf[i].char.appendSlice(grapheme) catch {
186
+
self.buf[i].char.appendSlice(self.allocator, grapheme) catch {
186
187
log.warn("couldn't write grapheme", .{});
187
188
};
188
189
self.buf[i].uri.clearRetainingCapacity();
189
-
self.buf[i].uri.appendSlice(self.cursor.uri.items) catch {
190
+
self.buf[i].uri.appendSlice(self.allocator, self.cursor.uri.items) catch {
190
191
log.warn("couldn't write uri", .{});
191
192
};
192
193
self.buf[i].uri_id.clearRetainingCapacity();
193
-
self.buf[i].uri_id.appendSlice(self.cursor.uri_id.items) catch {
194
+
self.buf[i].uri_id.appendSlice(self.allocator, self.cursor.uri_id.items) catch {
194
195
log.warn("couldn't write uri_id", .{});
195
196
};
196
197
self.buf[i].style = self.cursor.style;
···
368
369
const end = (self.cursor.row * self.width) + (self.width);
369
370
var i = (self.cursor.row * self.width) + self.cursor.col;
370
371
while (i < end) : (i += 1) {
371
-
self.buf[i].erase(self.cursor.style.bg);
372
+
self.buf[i].erase(self.allocator, self.cursor.style.bg);
372
373
}
373
374
}
374
375
···
378
379
const end = start + self.cursor.col + 1;
379
380
var i = start;
380
381
while (i < end) : (i += 1) {
381
-
self.buf[i].erase(self.cursor.style.bg);
382
+
self.buf[i].erase(self.allocator, self.cursor.style.bg);
382
383
}
383
384
}
384
385
···
388
389
const end = start + self.width;
389
390
var i = start;
390
391
while (i < end) : (i += 1) {
391
-
self.buf[i].erase(self.cursor.style.bg);
392
+
self.buf[i].erase(self.allocator, self.cursor.style.bg);
392
393
}
393
394
}
394
395
···
411
412
while (col <= self.scrolling_region.right) : (col += 1) {
412
413
const i = (row * self.width) + col;
413
414
if (row + cnt > self.scrolling_region.bottom)
414
-
self.buf[i].erase(self.cursor.style.bg)
415
+
self.buf[i].erase(self.allocator, self.cursor.style.bg)
415
416
else
416
-
try self.buf[i].copyFrom(self.buf[i + stride]);
417
+
try self.buf[i].copyFrom(self.allocator, self.buf[i + stride]);
417
418
}
418
419
}
419
420
}
···
434
435
var col: usize = self.scrolling_region.left;
435
436
while (col <= self.scrolling_region.right) : (col += 1) {
436
437
const i = (row * self.width) + col;
437
-
try self.buf[i].copyFrom(self.buf[i - stride]);
438
+
try self.buf[i].copyFrom(self.allocator, self.buf[i - stride]);
438
439
}
439
440
}
440
441
···
443
444
var col: usize = self.scrolling_region.left;
444
445
while (col <= self.scrolling_region.right) : (col += 1) {
445
446
const i = (row * self.width) + col;
446
-
self.buf[i].erase(self.cursor.style.bg);
447
+
self.buf[i].erase(self.allocator, self.cursor.style.bg);
447
448
}
448
449
}
449
450
}
···
454
455
const start = (self.cursor.row * self.width) + (self.width);
455
456
var i = start;
456
457
while (i < self.buf.len) : (i += 1) {
457
-
self.buf[i].erase(self.cursor.style.bg);
458
+
self.buf[i].erase(self.allocator, self.cursor.style.bg);
458
459
}
459
460
}
460
461
···
465
466
const end = self.cursor.row * self.width;
466
467
var i = start;
467
468
while (i < end) : (i += 1) {
468
-
self.buf[i].erase(self.cursor.style.bg);
469
+
self.buf[i].erase(self.allocator, self.cursor.style.bg);
469
470
}
470
471
}
471
472
472
473
pub fn eraseAll(self: *Screen) void {
473
474
var i: usize = 0;
474
475
while (i < self.buf.len) : (i += 1) {
475
-
self.buf[i].erase(self.cursor.style.bg);
476
+
self.buf[i].erase(self.allocator, self.cursor.style.bg);
476
477
}
477
478
}
478
479
···
483
484
var col = self.cursor.col;
484
485
while (col <= self.scrolling_region.right) : (col += 1) {
485
486
if (col + n <= self.scrolling_region.right)
486
-
try self.buf[col].copyFrom(self.buf[col + n])
487
+
try self.buf[col].copyFrom(self.allocator, self.buf[col + n])
487
488
else
488
-
self.buf[col].erase(self.cursor.style.bg);
489
+
self.buf[col].erase(self.allocator, self.cursor.style.bg);
489
490
}
490
491
}
491
492
+64
-73
src/widgets/terminal/Terminal.zig
+64
-73
src/widgets/terminal/Terminal.zig
···
10
10
const vaxis = @import("../../main.zig");
11
11
const Winsize = vaxis.Winsize;
12
12
const Screen = @import("Screen.zig");
13
-
const DisplayWidth = @import("DisplayWidth");
14
13
const Key = vaxis.Key;
15
14
const Queue = vaxis.Queue(Event, 16);
16
-
const code_point = @import("code_point");
17
15
const key = @import("key.zig");
18
16
19
17
pub const Event = union(enum) {
···
53
51
scrollback_size: u16,
54
52
55
53
pty: Pty,
54
+
pty_writer: std.fs.File.Writer,
56
55
cmd: Command,
57
56
thread: ?std.Thread = null,
58
57
···
70
69
// dirty is protected by back_mutex. Only access this field when you hold that mutex
71
70
dirty: bool = false,
72
71
73
-
unicode: *const vaxis.Unicode,
74
72
should_quit: bool = false,
75
73
76
74
mode: Mode = .{},
77
75
78
76
tab_stops: std.ArrayList(u16),
79
-
title: std.ArrayList(u8),
80
-
working_directory: std.ArrayList(u8),
77
+
title: std.ArrayList(u8) = .empty,
78
+
working_directory: std.ArrayList(u8) = .empty,
81
79
82
80
last_printed: []const u8 = "",
83
81
···
89
87
allocator: std.mem.Allocator,
90
88
argv: []const []const u8,
91
89
env: *const std.process.EnvMap,
92
-
unicode: *const vaxis.Unicode,
93
90
opts: Options,
91
+
write_buf: []u8,
94
92
) !Terminal {
95
93
// Verify we have an absolute path
96
94
if (opts.initial_working_directory) |pwd| {
···
104
102
.pty = pty,
105
103
.working_directory = opts.initial_working_directory,
106
104
};
107
-
var tabs = try std.ArrayList(u16).initCapacity(allocator, opts.winsize.cols / 8);
105
+
var tabs: std.ArrayList(u16) = try .initCapacity(allocator, opts.winsize.cols / 8);
108
106
var col: u16 = 0;
109
107
while (col < opts.winsize.cols) : (col += 8) {
110
-
try tabs.append(col);
108
+
try tabs.append(allocator, col);
111
109
}
112
110
return .{
113
111
.allocator = allocator,
114
112
.pty = pty,
113
+
.pty_writer = pty.pty.writerStreaming(write_buf),
115
114
.cmd = cmd,
116
115
.scrollback_size = opts.scrollback_size,
117
116
.front_screen = try Screen.init(allocator, opts.winsize.cols, opts.winsize.rows),
118
117
.back_screen_pri = try Screen.init(allocator, opts.winsize.cols, opts.winsize.rows + opts.scrollback_size),
119
118
.back_screen_alt = try Screen.init(allocator, opts.winsize.cols, opts.winsize.rows),
120
-
.unicode = unicode,
121
119
.tab_stops = tabs,
122
-
.title = std.ArrayList(u8).init(allocator),
123
-
.working_directory = std.ArrayList(u8).init(allocator),
124
120
};
125
121
}
126
122
···
143
139
if (self.thread) |thread| {
144
140
// write an EOT into the tty to trigger a read on our thread
145
141
const EOT = "\x04";
146
-
_ = std.posix.write(self.pty.tty, EOT) catch {};
142
+
_ = self.pty.tty.write(EOT) catch {};
147
143
thread.join();
148
144
self.thread = null;
149
145
}
···
151
147
self.front_screen.deinit(self.allocator);
152
148
self.back_screen_pri.deinit(self.allocator);
153
149
self.back_screen_alt.deinit(self.allocator);
154
-
self.tab_stops.deinit();
155
-
self.title.deinit();
156
-
self.working_directory.deinit();
150
+
self.tab_stops.deinit(self.allocator);
151
+
self.title.deinit(self.allocator);
152
+
self.working_directory.deinit(self.allocator);
157
153
}
158
154
159
155
pub fn spawn(self: *Terminal) !void {
···
164
160
165
161
self.working_directory.clearRetainingCapacity();
166
162
if (self.cmd.working_directory) |pwd| {
167
-
try self.working_directory.appendSlice(pwd);
163
+
try self.working_directory.appendSlice(self.allocator, pwd);
168
164
} else {
169
165
const pwd = std.fs.cwd();
170
166
var buffer: [std.fs.max_path_bytes]u8 = undefined;
171
167
const out_path = try std.os.getFdPath(pwd.fd, &buffer);
172
-
try self.working_directory.appendSlice(out_path);
168
+
try self.working_directory.appendSlice(self.allocator, out_path);
173
169
}
174
170
175
171
{
···
208
204
try self.pty.setSize(ws);
209
205
}
210
206
211
-
pub fn draw(self: *Terminal, win: vaxis.Window) !void {
207
+
pub fn draw(self: *Terminal, allocator: std.mem.Allocator, win: vaxis.Window) !void {
212
208
if (self.back_mutex.tryLock()) {
213
209
defer self.back_mutex.unlock();
214
210
// We keep this as a separate condition so we don't deadlock by obtaining the lock but not
215
211
// having sync
216
212
if (!self.mode.sync) {
217
-
try self.back_screen.copyTo(&self.front_screen);
213
+
try self.back_screen.copyTo(allocator, &self.front_screen);
218
214
self.dirty = false;
219
215
}
220
216
}
···
241
237
242
238
pub fn update(self: *Terminal, event: InputEvent) !void {
243
239
switch (event) {
244
-
.key_press => |k| try key.encode(self.anyWriter(), k, true, self.back_screen.csi_u_flags),
240
+
.key_press => |k| {
241
+
const pty_writer = self.get_pty_writer();
242
+
defer pty_writer.flush() catch {};
243
+
try key.encode(pty_writer, k, true, self.back_screen.csi_u_flags);
244
+
},
245
245
}
246
246
}
247
247
248
-
fn opaqueWrite(ptr: *const anyopaque, buf: []const u8) !usize {
249
-
const self: *const Terminal = @ptrCast(@alignCast(ptr));
250
-
return posix.write(self.pty.pty, buf);
251
-
}
252
-
253
-
pub fn anyWriter(self: *const Terminal) *std.io.Writer {
254
-
const writer: std.io.Writer = .{
255
-
.context = self,
256
-
.writeFn = Terminal.opaqueWrite,
257
-
};
258
-
return @constCast(&writer);
259
-
}
260
-
261
-
fn opaqueRead(ptr: *const anyopaque, buf: []u8) !usize {
262
-
const self: *const Terminal = @ptrCast(@alignCast(ptr));
263
-
return posix.read(self.pty.pty, buf);
248
+
pub fn get_pty_writer(self: *Terminal) *std.Io.Writer {
249
+
return &self.pty_writer.interface;
264
250
}
265
251
266
-
fn anyReader(self: *const Terminal) std.io.AnyReader {
267
-
return .{
268
-
.context = self,
269
-
.readFn = Terminal.opaqueRead,
270
-
};
252
+
fn reader(self: *const Terminal, buf: []u8) std.fs.File.Reader {
253
+
return self.pty.pty.readerStreaming(buf);
271
254
}
272
255
273
256
/// process the output from the command on the pty
274
257
fn run(self: *Terminal) !void {
275
258
var parser: Parser = .{
276
-
.buf = try std.ArrayList(u8).initCapacity(self.allocator, 128),
259
+
.buf = try .initCapacity(self.allocator, 128),
277
260
};
278
261
defer parser.buf.deinit();
279
262
280
-
// Use our anyReader to make a buffered reader, then get *that* any reader
281
-
var reader = std.io.bufferedReader(self.anyReader());
263
+
var reader_buf: [4096]u8 = undefined;
264
+
var reader_ = self.reader(&reader_buf);
282
265
283
266
while (!self.should_quit) {
284
-
const event = try parser.parseReader(&reader);
267
+
const event = try parser.parseReader(&reader_.interface);
285
268
self.back_mutex.lock();
286
269
defer self.back_mutex.unlock();
287
270
···
290
273
291
274
switch (event) {
292
275
.print => |str| {
293
-
var iter = self.unicode.graphemeIterator(str);
294
-
while (iter.next()) |g| {
295
-
const gr = g.bytes(str);
276
+
var iter = vaxis.unicode.graphemeIterator(str);
277
+
while (iter.next()) |grapheme| {
278
+
const gr = grapheme.bytes(str);
296
279
// TODO: use actual instead of .unicode
297
-
const w = vaxis.gwidth.gwidth(gr, .unicode, &self.unicode.width_data);
280
+
const w = vaxis.gwidth.gwidth(gr, .unicode);
298
281
try self.back_screen.print(gr, @truncate(w), self.mode.autowrap);
299
282
}
300
283
},
···
316
299
if (ts == self.back_screen.cursor.col) break true;
317
300
} else false;
318
301
if (already_set) continue;
319
-
try self.tab_stops.append(@truncate(self.back_screen.cursor.col));
302
+
try self.tab_stops.append(self.allocator, @truncate(self.back_screen.cursor.col));
320
303
std.mem.sort(u16, self.tab_stops.items, {}, std.sort.asc(u16));
321
304
},
322
305
// Reverse Index
···
467
450
self.tab_stops.clearRetainingCapacity();
468
451
var col: u16 = 0;
469
452
while (col < self.back_screen.width) : (col += 8) {
470
-
try self.tab_stops.append(col);
453
+
try self.tab_stops.append(self.allocator, col);
471
454
}
472
455
}
473
456
},
···
483
466
);
484
467
var i: usize = start;
485
468
while (i < end) : (i += 1) {
486
-
self.back_screen.buf[i].erase(self.back_screen.cursor.style.bg);
469
+
self.back_screen.buf[i].erase(self.allocator, self.back_screen.cursor.style.bg);
487
470
}
488
471
},
489
472
'Z' => {
···
510
493
var iter = seq.iterator(u16);
511
494
const n = iter.next() orelse 1;
512
495
// TODO: maybe not .unicode
513
-
const w = vaxis.gwidth.gwidth(self.last_printed, .unicode, &self.unicode.width_data);
496
+
const w = vaxis.gwidth.gwidth(self.last_printed, .unicode);
514
497
var i: usize = 0;
515
498
while (i < n) : (i += 1) {
516
499
try self.back_screen.print(self.last_printed, @truncate(w), self.mode.autowrap);
···
518
501
},
519
502
// Device Attributes
520
503
'c' => {
504
+
const pty_writer = self.get_pty_writer();
505
+
defer pty_writer.flush() catch {};
521
506
if (seq.private_marker) |pm| {
522
507
switch (pm) {
523
508
// Secondary
524
-
'>' => try self.anyWriter().writeAll("\x1B[>1;69;0c"),
525
-
'=' => try self.anyWriter().writeAll("\x1B[=0000c"),
526
-
else => log.info("unhandled CSI: {}", .{seq}),
509
+
'>' => try pty_writer.writeAll("\x1B[>1;69;0c"),
510
+
'=' => try pty_writer.writeAll("\x1B[=0000c"),
511
+
else => log.info("unhandled CSI: {f}", .{seq}),
527
512
}
528
513
} else {
529
514
// Primary
530
-
try self.anyWriter().writeAll("\x1B[?62;22c");
515
+
try pty_writer.writeAll("\x1B[?62;22c");
531
516
}
532
517
},
533
518
// Cursor Vertical Position Absolute
···
561
546
const n = iter.next() orelse 0;
562
547
switch (n) {
563
548
0 => {
564
-
const current = try self.tab_stops.toOwnedSlice();
565
-
defer self.tab_stops.allocator.free(current);
549
+
const current = try self.tab_stops.toOwnedSlice(self.allocator);
550
+
defer self.allocator.free(current);
566
551
self.tab_stops.clearRetainingCapacity();
567
552
for (current) |stop| {
568
553
if (stop == self.back_screen.cursor.col) continue;
569
-
try self.tab_stops.append(stop);
554
+
try self.tab_stops.append(self.allocator, stop);
570
555
}
571
556
},
572
-
3 => self.tab_stops.clearAndFree(),
573
-
else => log.info("unhandled CSI: {}", .{seq}),
557
+
3 => self.tab_stops.clearAndFree(self.allocator),
558
+
else => log.info("unhandled CSI: {f}", .{seq}),
574
559
}
575
560
},
576
561
'h', 'l' => {
···
591
576
var iter = seq.iterator(u16);
592
577
const ps = iter.next() orelse 0;
593
578
if (seq.intermediate == null and seq.private_marker == null) {
579
+
const pty_writer = self.get_pty_writer();
580
+
defer pty_writer.flush() catch {};
594
581
switch (ps) {
595
-
5 => try self.anyWriter().writeAll("\x1b[0n"),
596
-
6 => try self.anyWriter().print("\x1b[{d};{d}R", .{
582
+
5 => try pty_writer.writeAll("\x1b[0n"),
583
+
6 => try pty_writer.print("\x1b[{d};{d}R", .{
597
584
self.back_screen.cursor.row + 1,
598
585
self.back_screen.cursor.col + 1,
599
586
}),
600
-
else => log.info("unhandled CSI: {}", .{seq}),
587
+
else => log.info("unhandled CSI: {f}", .{seq}),
601
588
}
602
589
}
603
590
},
···
608
595
switch (int) {
609
596
// report mode
610
597
'$' => {
598
+
const pty_writer = self.get_pty_writer();
599
+
defer pty_writer.flush() catch {};
611
600
switch (ps) {
612
-
2026 => try self.anyWriter().writeAll("\x1b[?2026;2$p"),
601
+
2026 => try pty_writer.writeAll("\x1b[?2026;2$p"),
613
602
else => {
614
603
std.log.warn("unhandled mode: {}", .{ps});
615
-
try self.anyWriter().print("\x1b[?{d};0$p", .{ps});
604
+
try pty_writer.print("\x1b[?{d};0$p", .{ps});
616
605
},
617
606
}
618
607
},
619
-
else => log.info("unhandled CSI: {}", .{seq}),
608
+
else => log.info("unhandled CSI: {f}", .{seq}),
620
609
}
621
610
}
622
611
},
···
632
621
}
633
622
}
634
623
if (seq.private_marker) |pm| {
624
+
const pty_writer = self.get_pty_writer();
625
+
defer pty_writer.flush() catch {};
635
626
switch (pm) {
636
627
// XTVERSION
637
-
'>' => try self.anyWriter().print(
628
+
'>' => try pty_writer.print(
638
629
"\x1bP>|libvaxis {s}\x1B\\",
639
630
.{"dev"},
640
631
),
641
-
else => log.info("unhandled CSI: {}", .{seq}),
632
+
else => log.info("unhandled CSI: {f}", .{seq}),
642
633
}
643
634
}
644
635
},
···
666
657
self.back_screen.cursor.row = 0;
667
658
}
668
659
},
669
-
else => log.info("unhandled CSI: {}", .{seq}),
660
+
else => log.info("unhandled CSI: {f}", .{seq}),
670
661
}
671
662
},
672
663
.osc => |osc| {
···
681
672
switch (ps) {
682
673
0 => {
683
674
self.title.clearRetainingCapacity();
684
-
try self.title.appendSlice(osc[semicolon + 1 ..]);
675
+
try self.title.appendSlice(self.allocator, osc[semicolon + 1 ..]);
685
676
self.event_queue.push(.{ .title_change = self.title.items });
686
677
},
687
678
7 => {
···
700
691
defer i += 2;
701
692
break :blk try std.fmt.parseUnsigned(u8, enc[i + 1 .. i + 3], 16);
702
693
} else enc[i];
703
-
try self.working_directory.append(b);
694
+
try self.working_directory.append(self.allocator, b);
704
695
}
705
696
self.event_queue.push(.{ .pwd_change = self.working_directory.items });
706
697
},
+5
-12
src/widgets/terminal/ansi.zig
+5
-12
src/widgets/terminal/ansi.zig
···
55
55
return .{ .bytes = self.params };
56
56
}
57
57
58
-
pub fn format(
59
-
self: CSI,
60
-
comptime layout: []const u8,
61
-
opts: std.fmt.FormatOptions,
62
-
writer: anytype,
63
-
) !void {
64
-
_ = layout;
65
-
_ = opts;
58
+
pub fn format(self: CSI, writer: anytype) !void {
66
59
if (self.private_marker == null and self.intermediate == null)
67
-
try std.fmt.format(writer, "CSI {s} {c}", .{
60
+
try writer.print("CSI {s} {c}", .{
68
61
self.params,
69
62
self.final,
70
63
})
71
64
else if (self.private_marker != null and self.intermediate == null)
72
-
try std.fmt.format(writer, "CSI {c} {s} {c}", .{
65
+
try writer.print("CSI {c} {s} {c}", .{
73
66
self.private_marker.?,
74
67
self.params,
75
68
self.final,
76
69
})
77
70
else if (self.private_marker == null and self.intermediate != null)
78
-
try std.fmt.format(writer, "CSI {s} {c} {c}", .{
71
+
try writer.print("CSI {s} {c} {c}", .{
79
72
self.params,
80
73
self.intermediate.?,
81
74
self.final,
82
75
})
83
76
else
84
-
try std.fmt.format(writer, "CSI {c} {s} {c} {c}", .{
77
+
try writer.print("CSI {c} {s} {c} {c}", .{
85
78
self.private_marker.?,
86
79
self.params,
87
80
self.intermediate.?,
+2
-2
src/widgets/terminal/key.zig
+2
-2
src/widgets/terminal/key.zig
···
2
2
const vaxis = @import("../../main.zig");
3
3
4
4
pub fn encode(
5
-
writer: std.io.AnyWriter,
5
+
writer: *std.Io.Writer,
6
6
key: vaxis.Key,
7
7
press: bool,
8
8
kitty_flags: vaxis.Key.KittyFlags,
···
19
19
}
20
20
}
21
21
22
-
fn legacy(writer: std.io.AnyWriter, key: vaxis.Key) !void {
22
+
fn legacy(writer: *std.Io.Writer, key: vaxis.Key) !void {
23
23
// If we have text, we always write it directly
24
24
if (key.text) |text| {
25
25
try writer.writeAll(text);