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