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