this repo has no description
1const std = @import("std");
2const builtin = @import("builtin");
3
4const Graphemes = @import("Graphemes");
5
6const GraphemeCache = @import("GraphemeCache.zig");
7const Parser = @import("Parser.zig");
8const Queue = @import("queue.zig").Queue;
9const vaxis = @import("main.zig");
10const Tty = vaxis.Tty;
11const Vaxis = @import("Vaxis.zig");
12
13const log = std.log.scoped(.vaxis);
14
15pub fn Loop(comptime T: type) type {
16 return struct {
17 const Self = @This();
18
19 const Event = T;
20
21 tty: *Tty,
22 vaxis: *Vaxis,
23
24 queue: Queue(T, 512) = .{},
25 thread: ?std.Thread = null,
26 should_quit: bool = false,
27
28 /// Initialize the event loop. This is an intrusive init so that we have
29 /// a stable pointer to register signal callbacks with posix TTYs
30 pub fn init(self: *Self) !void {
31 switch (builtin.os.tag) {
32 .windows => {},
33 else => {
34 if (!builtin.is_test) {
35 const handler: Tty.SignalHandler = .{
36 .context = self,
37 .callback = Self.winsizeCallback,
38 };
39 try Tty.notifyWinsize(handler);
40 }
41 },
42 }
43 }
44
45 /// spawns the input thread to read input from the tty
46 pub fn start(self: *Self) !void {
47 if (self.thread) |_| return;
48 self.thread = try std.Thread.spawn(.{}, Self.ttyRun, .{
49 self,
50 &self.vaxis.unicode.width_data.graphemes,
51 self.vaxis.opts.system_clipboard_allocator,
52 });
53 }
54
55 /// stops reading from the tty.
56 pub fn stop(self: *Self) void {
57 // If we don't have a thread, we have nothing to stop
58 if (self.thread == null) return;
59 self.should_quit = true;
60 // trigger a read
61 self.vaxis.deviceStatusReport(self.tty.anyWriter()) catch {};
62
63 if (self.thread) |thread| {
64 thread.join();
65 self.thread = null;
66 self.should_quit = false;
67 }
68 }
69
70 /// returns the next available event, blocking until one is available
71 pub fn nextEvent(self: *Self) T {
72 return self.queue.pop();
73 }
74
75 /// blocks until an event is available. Useful when your application is
76 /// operating on a poll + drain architecture (see tryEvent)
77 pub fn pollEvent(self: *Self) void {
78 self.queue.poll();
79 }
80
81 /// returns an event if one is available, otherwise null. Non-blocking.
82 pub fn tryEvent(self: *Self) ?T {
83 return self.queue.tryPop();
84 }
85
86 /// posts an event into the event queue. Will block if there is not
87 /// capacity for the event
88 pub fn postEvent(self: *Self, event: T) void {
89 self.queue.push(event);
90 }
91
92 pub fn tryPostEvent(self: *Self, event: T) bool {
93 return self.queue.tryPush(event);
94 }
95
96 pub fn winsizeCallback(ptr: *anyopaque) void {
97 const self: *Self = @ptrCast(@alignCast(ptr));
98 // We will be receiving winsize updates in-band
99 if (self.vaxis.state.in_band_resize) return;
100
101 const winsize = Tty.getWinsize(self.tty.fd) catch return;
102 if (@hasField(Event, "winsize")) {
103 self.postEvent(.{ .winsize = winsize });
104 }
105 }
106
107 /// read input from the tty. This is run in a separate thread
108 fn ttyRun(
109 self: *Self,
110 grapheme_data: *const Graphemes,
111 paste_allocator: ?std.mem.Allocator,
112 ) !void {
113 // Return early if we're in test mode to avoid infinite loops
114 if (builtin.is_test) return;
115
116 // initialize a grapheme cache
117 var cache: GraphemeCache = .{};
118
119 switch (builtin.os.tag) {
120 .windows => {
121 var parser: Parser = .{
122 .grapheme_data = grapheme_data,
123 };
124 while (!self.should_quit) {
125 const event = try self.tty.nextEvent(&parser, paste_allocator);
126 try handleEventGeneric(self, self.vaxis, &cache, Event, event, null);
127 }
128 },
129 else => {
130 // get our initial winsize
131 const winsize = try Tty.getWinsize(self.tty.fd);
132 if (@hasField(Event, "winsize")) {
133 self.postEvent(.{ .winsize = winsize });
134 }
135
136 var parser: Parser = .{
137 .grapheme_data = grapheme_data,
138 };
139
140 // initialize the read buffer
141 var buf: [1024]u8 = undefined;
142 var read_start: usize = 0;
143 // read loop
144 read_loop: while (!self.should_quit) {
145 const n = try self.tty.read(buf[read_start..]);
146 var seq_start: usize = 0;
147 while (seq_start < n) {
148 const result = try parser.parse(buf[seq_start..n], paste_allocator);
149 if (result.n == 0) {
150 // copy the read to the beginning. We don't use memcpy because
151 // this could be overlapping, and it's also rare
152 const initial_start = seq_start;
153 while (seq_start < n) : (seq_start += 1) {
154 buf[seq_start - initial_start] = buf[seq_start];
155 }
156 read_start = seq_start - initial_start + 1;
157 continue :read_loop;
158 }
159 read_start = 0;
160 seq_start += result.n;
161
162 const event = result.event orelse continue;
163 try handleEventGeneric(self, self.vaxis, &cache, Event, event, paste_allocator);
164 }
165 }
166 },
167 }
168 }
169 };
170}
171
172// Use return on the self.postEvent's so it can either return error union or void
173pub fn handleEventGeneric(self: anytype, vx: *Vaxis, cache: *GraphemeCache, Event: type, event: anytype, paste_allocator: ?std.mem.Allocator) !void {
174 switch (builtin.os.tag) {
175 .windows => {
176 switch (event) {
177 .winsize => |ws| {
178 if (@hasField(Event, "winsize")) {
179 return self.postEvent(.{ .winsize = ws });
180 }
181 },
182 .key_press => |key| {
183 // Check for a cursor position response for our explicit width query. This will
184 // always be an F3 key with shift = true, and we must be looking for queries
185 if (key.codepoint == vaxis.Key.f3 and
186 key.mods.shift and
187 !vx.queries_done.load(.unordered))
188 {
189 log.info("explicit width capability detected", .{});
190 vx.caps.explicit_width = true;
191 vx.caps.unicode = .unicode;
192 vx.screen.width_method = .unicode;
193 return;
194 }
195 // Check for a cursor position response for our scaled text query. This will
196 // always be an F3 key with alt = true, and we must be looking for queries
197 if (key.codepoint == vaxis.Key.f3 and
198 key.mods.alt and
199 !vx.queries_done.load(.unordered))
200 {
201 log.info("scaled text capability detected", .{});
202 vx.caps.scaled_text = true;
203 return;
204 }
205 if (@hasField(Event, "key_press")) {
206 // HACK: yuck. there has to be a better way
207 var mut_key = key;
208 if (key.text) |text| {
209 mut_key.text = cache.put(text);
210 }
211 return self.postEvent(.{ .key_press = mut_key });
212 }
213 },
214 .key_release => |key| {
215 if (@hasField(Event, "key_release")) {
216 // HACK: yuck. there has to be a better way
217 var mut_key = key;
218 if (key.text) |text| {
219 mut_key.text = cache.put(text);
220 }
221 return self.postEvent(.{ .key_release = mut_key });
222 }
223 },
224 .cap_da1 => {
225 std.Thread.Futex.wake(&vx.query_futex, 10);
226 vx.queries_done.store(true, .unordered);
227 },
228 .mouse => |mouse| {
229 if (@hasField(Event, "mouse")) {
230 return self.postEvent(.{ .mouse = vx.translateMouse(mouse) });
231 }
232 },
233 .focus_in => {
234 if (@hasField(Event, "focus_in")) {
235 return self.postEvent(.focus_in);
236 }
237 },
238 .focus_out => {
239 if (@hasField(Event, "focus_out")) {
240 return self.postEvent(.focus_out);
241 }
242 }, // Unsupported currently
243 else => {},
244 }
245 },
246 else => {
247 switch (event) {
248 .key_press => |key| {
249 // Check for a cursor position response for our explicity width query. This will
250 // always be an F3 key with shift = true, and we must be looking for queries
251 if (key.codepoint == vaxis.Key.f3 and
252 key.mods.shift and
253 !vx.queries_done.load(.unordered))
254 {
255 log.info("explicit width capability detected", .{});
256 vx.caps.explicit_width = true;
257 vx.caps.unicode = .unicode;
258 vx.screen.width_method = .unicode;
259 return;
260 }
261 // Check for a cursor position response for our scaled text query. This will
262 // always be an F3 key with alt = true, and we must be looking for queries
263 if (key.codepoint == vaxis.Key.f3 and
264 key.mods.alt and
265 !vx.queries_done.load(.unordered))
266 {
267 log.info("scaled text capability detected", .{});
268 vx.caps.scaled_text = true;
269 return;
270 }
271 if (@hasField(Event, "key_press")) {
272 // HACK: yuck. there has to be a better way
273 var mut_key = key;
274 if (key.text) |text| {
275 mut_key.text = cache.put(text);
276 }
277 return self.postEvent(.{ .key_press = mut_key });
278 }
279 },
280 .key_release => |key| {
281 if (@hasField(Event, "key_release")) {
282 // HACK: yuck. there has to be a better way
283 var mut_key = key;
284 if (key.text) |text| {
285 mut_key.text = cache.put(text);
286 }
287 return self.postEvent(.{ .key_release = mut_key });
288 }
289 },
290 .mouse => |mouse| {
291 if (@hasField(Event, "mouse")) {
292 return self.postEvent(.{ .mouse = vx.translateMouse(mouse) });
293 }
294 },
295 .mouse_leave => {
296 if (@hasField(Event, "mouse_leave")) {
297 return self.postEvent(.mouse_leave);
298 }
299 },
300 .focus_in => {
301 if (@hasField(Event, "focus_in")) {
302 return self.postEvent(.focus_in);
303 }
304 },
305 .focus_out => {
306 if (@hasField(Event, "focus_out")) {
307 return self.postEvent(.focus_out);
308 }
309 },
310 .paste_start => {
311 if (@hasField(Event, "paste_start")) {
312 return self.postEvent(.paste_start);
313 }
314 },
315 .paste_end => {
316 if (@hasField(Event, "paste_end")) {
317 return self.postEvent(.paste_end);
318 }
319 },
320 .paste => |text| {
321 if (@hasField(Event, "paste")) {
322 return self.postEvent(.{ .paste = text });
323 } else {
324 if (paste_allocator) |_|
325 paste_allocator.?.free(text);
326 }
327 },
328 .color_report => |report| {
329 if (@hasField(Event, "color_report")) {
330 return self.postEvent(.{ .color_report = report });
331 }
332 },
333 .color_scheme => |scheme| {
334 if (@hasField(Event, "color_scheme")) {
335 return self.postEvent(.{ .color_scheme = scheme });
336 }
337 },
338 .cap_kitty_keyboard => {
339 log.info("kitty keyboard capability detected", .{});
340 vx.caps.kitty_keyboard = true;
341 },
342 .cap_kitty_graphics => {
343 if (!vx.caps.kitty_graphics) {
344 log.info("kitty graphics capability detected", .{});
345 vx.caps.kitty_graphics = true;
346 }
347 },
348 .cap_rgb => {
349 log.info("rgb capability detected", .{});
350 vx.caps.rgb = true;
351 },
352 .cap_unicode => {
353 log.info("unicode capability detected", .{});
354 vx.caps.unicode = .unicode;
355 vx.screen.width_method = .unicode;
356 },
357 .cap_sgr_pixels => {
358 log.info("pixel mouse capability detected", .{});
359 vx.caps.sgr_pixels = true;
360 },
361 .cap_color_scheme_updates => {
362 log.info("color_scheme_updates capability detected", .{});
363 vx.caps.color_scheme_updates = true;
364 },
365 .cap_da1 => {
366 std.Thread.Futex.wake(&vx.query_futex, 10);
367 vx.queries_done.store(true, .unordered);
368 },
369 .winsize => |winsize| {
370 vx.state.in_band_resize = true;
371 switch (builtin.os.tag) {
372 .windows => {},
373 // Reset the signal handler if we are receiving in_band_resize
374 else => Tty.resetSignalHandler(),
375 }
376 if (@hasField(Event, "winsize")) {
377 return self.postEvent(.{ .winsize = winsize });
378 }
379 },
380 }
381 },
382 }
383}
384
385test Loop {
386 const Event = union(enum) {
387 key_press: vaxis.Key,
388 winsize: vaxis.Winsize,
389 focus_in,
390 foo: u8,
391 };
392
393 var tty = try vaxis.Tty.init(&.{});
394 defer tty.deinit();
395
396 var vx = try vaxis.init(std.testing.allocator, .{});
397 defer vx.deinit(std.testing.allocator, tty.anyWriter());
398
399 var loop: vaxis.Loop(Event) = .{ .tty = &tty, .vaxis = &vx };
400 try loop.init();
401
402 try loop.start();
403 defer loop.stop();
404
405 // Optionally enter the alternate screen
406 try vx.enterAltScreen(tty.anyWriter());
407 try vx.queryTerminal(tty.anyWriter(), 1 * std.time.ns_per_ms);
408}