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 ConnectionResetByPeer,
118 EndOfStream,
119 InputOutput,
120 InvalidCharacter,
121 InvalidColorSpec,
122 InvalidPadding,
123 InvalidUTF8,
124 IoctlError,
125 IsDir,
126 LockViolation,
127 NoSpaceLeft,
128 NotOpenForReading,
129 OutOfMemory,
130 Overflow,
131 SocketUnconnected,
132 SystemResources,
133 Unexpected,
134 WouldBlock,
135 };
136
137 /// read input from the tty. This is run in a separate thread
138 fn ttyRun(self: *Self, paste_allocator: ?std.mem.Allocator) void {
139 self._ttyRun(paste_allocator) catch {};
140 }
141
142 fn _ttyRun(
143 self: *Self,
144 paste_allocator: ?std.mem.Allocator,
145 ) TtyRunError!void {
146 // Return early if we're in test mode to avoid infinite loops
147 if (builtin.is_test) return;
148
149 // initialize a grapheme cache
150 var cache: GraphemeCache = .{};
151
152 switch (builtin.os.tag) {
153 .windows => {
154 var parser: Parser = .{};
155 while (!self.should_quit) {
156 const event = try self.tty.nextEvent(&parser, paste_allocator);
157 try handleEventGeneric(self, self.vaxis, &cache, Event, event, null);
158 }
159 },
160 else => {
161 // get our initial winsize
162 const winsize = try self.tty.getWinsize();
163 if (@hasField(Event, "winsize")) {
164 try self.postEvent(.{ .winsize = winsize });
165 }
166
167 var parser: Parser = .{};
168
169 // initialize the read buffer
170 var buf: [1024]u8 = undefined;
171 var read_start: usize = 0;
172 // read loop
173 read_loop: while (!self.should_quit) {
174 const n = try self.tty.read(buf[read_start..]);
175 var seq_start: usize = 0;
176 while (seq_start < n) {
177 const result = try parser.parse(buf[seq_start..n], paste_allocator);
178 if (result.n == 0) {
179 // copy the read to the beginning. We don't use memcpy because
180 // this could be overlapping, and it's also rare
181 const initial_start = seq_start;
182 while (seq_start < n) : (seq_start += 1) {
183 buf[seq_start - initial_start] = buf[seq_start];
184 }
185 read_start = seq_start - initial_start + 1;
186 continue :read_loop;
187 }
188 read_start = 0;
189 seq_start += result.n;
190
191 const event = result.event orelse continue;
192 try handleEventGeneric(self, self.vaxis, &cache, Event, event, paste_allocator);
193 }
194 }
195 },
196 }
197 }
198 };
199}
200
201// Use return on the self.postEvent's so it can either return error union or void
202pub fn handleEventGeneric(self: anytype, vx: *Vaxis, cache: *GraphemeCache, Event: type, event: anytype, paste_allocator: ?std.mem.Allocator) !void {
203 switch (builtin.os.tag) {
204 .windows => {
205 switch (event) {
206 .winsize => |ws| {
207 if (@hasField(Event, "winsize")) {
208 return self.postEvent(.{ .winsize = ws });
209 }
210 },
211 .key_press => |key| {
212 // Check for a cursor position response for our explicit width query. This will
213 // always be an F3 key with shift = true, and we must be looking for queries
214 if (key.codepoint == vaxis.Key.f3 and
215 key.mods.shift and
216 !vx.queries_done.load(.unordered))
217 {
218 log.info("explicit width capability detected", .{});
219 vx.caps.explicit_width = true;
220 vx.caps.unicode = .unicode;
221 vx.screen.width_method = .unicode;
222 return;
223 }
224 // Check for a cursor position response for our scaled text query. This will
225 // always be an F3 key with alt = true, and we must be looking for queries
226 if (key.codepoint == vaxis.Key.f3 and
227 key.mods.alt and
228 !vx.queries_done.load(.unordered))
229 {
230 log.info("scaled text capability detected", .{});
231 vx.caps.scaled_text = true;
232 return;
233 }
234 if (@hasField(Event, "key_press")) {
235 // HACK: yuck. there has to be a better way
236 var mut_key = key;
237 if (key.text) |text| {
238 mut_key.text = cache.put(text);
239 }
240 return self.postEvent(.{ .key_press = mut_key });
241 }
242 },
243 .key_release => |key| {
244 if (@hasField(Event, "key_release")) {
245 // HACK: yuck. there has to be a better way
246 var mut_key = key;
247 if (key.text) |text| {
248 mut_key.text = cache.put(text);
249 }
250 return self.postEvent(.{ .key_release = mut_key });
251 }
252 },
253 .cap_da1 => {
254 std.Thread.Futex.wake(&vx.query_futex, 10);
255 vx.queries_done.store(true, .unordered);
256 },
257 .mouse => |mouse| {
258 if (@hasField(Event, "mouse")) {
259 return self.postEvent(.{ .mouse = vx.translateMouse(mouse) });
260 }
261 },
262 .focus_in => {
263 if (@hasField(Event, "focus_in")) {
264 return self.postEvent(.focus_in);
265 }
266 },
267 .focus_out => {
268 if (@hasField(Event, "focus_out")) {
269 return self.postEvent(.focus_out);
270 }
271 }, // Unsupported currently
272 else => {},
273 }
274 },
275 else => {
276 switch (event) {
277 .key_press => |key| {
278 // Check for a cursor position response for our explicitly width query. This will
279 // always be an F3 key with shift = true, and we must be looking for queries
280 if (key.codepoint == vaxis.Key.f3 and
281 key.mods.shift and
282 !vx.queries_done.load(.unordered))
283 {
284 log.info("explicit width capability detected", .{});
285 vx.caps.explicit_width = true;
286 vx.caps.unicode = .unicode;
287 vx.screen.width_method = .unicode;
288 return;
289 }
290 // Check for a cursor position response for our scaled text query. This will
291 // always be an F3 key with alt = true, and we must be looking for queries
292 if (key.codepoint == vaxis.Key.f3 and
293 key.mods.alt and
294 !vx.queries_done.load(.unordered))
295 {
296 log.info("scaled text capability detected", .{});
297 vx.caps.scaled_text = true;
298 return;
299 }
300 if (@hasField(Event, "key_press")) {
301 // HACK: yuck. there has to be a better way
302 var mut_key = key;
303 if (key.text) |text| {
304 mut_key.text = cache.put(text);
305 }
306 return self.postEvent(.{ .key_press = mut_key });
307 }
308 },
309 .key_release => |key| {
310 if (@hasField(Event, "key_release")) {
311 // HACK: yuck. there has to be a better way
312 var mut_key = key;
313 if (key.text) |text| {
314 mut_key.text = cache.put(text);
315 }
316 return self.postEvent(.{ .key_release = mut_key });
317 }
318 },
319 .mouse => |mouse| {
320 if (@hasField(Event, "mouse")) {
321 return self.postEvent(.{ .mouse = vx.translateMouse(mouse) });
322 }
323 },
324 .mouse_leave => {
325 if (@hasField(Event, "mouse_leave")) {
326 return self.postEvent(.mouse_leave);
327 }
328 },
329 .focus_in => {
330 if (@hasField(Event, "focus_in")) {
331 return self.postEvent(.focus_in);
332 }
333 },
334 .focus_out => {
335 if (@hasField(Event, "focus_out")) {
336 return self.postEvent(.focus_out);
337 }
338 },
339 .paste_start => {
340 if (@hasField(Event, "paste_start")) {
341 return self.postEvent(.paste_start);
342 }
343 },
344 .paste_end => {
345 if (@hasField(Event, "paste_end")) {
346 return self.postEvent(.paste_end);
347 }
348 },
349 .paste => |text| {
350 if (@hasField(Event, "paste")) {
351 return self.postEvent(.{ .paste = text });
352 } else {
353 if (paste_allocator) |_|
354 paste_allocator.?.free(text);
355 }
356 },
357 .color_report => |report| {
358 if (@hasField(Event, "color_report")) {
359 return self.postEvent(.{ .color_report = report });
360 }
361 },
362 .color_scheme => |scheme| {
363 if (@hasField(Event, "color_scheme")) {
364 return self.postEvent(.{ .color_scheme = scheme });
365 }
366 },
367 .cap_kitty_keyboard => {
368 log.info("kitty keyboard capability detected", .{});
369 vx.caps.kitty_keyboard = true;
370 },
371 .cap_kitty_graphics => {
372 if (!vx.caps.kitty_graphics) {
373 log.info("kitty graphics capability detected", .{});
374 vx.caps.kitty_graphics = true;
375 }
376 },
377 .cap_rgb => {
378 log.info("rgb capability detected", .{});
379 vx.caps.rgb = true;
380 },
381 .cap_unicode => {
382 log.info("unicode capability detected", .{});
383 vx.caps.unicode = .unicode;
384 vx.screen.width_method = .unicode;
385 },
386 .cap_sgr_pixels => {
387 log.info("pixel mouse capability detected", .{});
388 vx.caps.sgr_pixels = true;
389 },
390 .cap_color_scheme_updates => {
391 log.info("color_scheme_updates capability detected", .{});
392 vx.caps.color_scheme_updates = true;
393 },
394 .cap_multi_cursor => {
395 log.info("multi cursor capability detected", .{});
396 vx.caps.multi_cursor = true;
397 },
398 .cap_da1 => {
399 std.Io.futexWake(vx.io, std.atomic.Value(u32), &vx.query_futex, 10);
400 vx.queries_done.store(true, .unordered);
401 },
402 .winsize => |winsize| {
403 vx.state.in_band_resize = true;
404 switch (builtin.os.tag) {
405 .windows => {},
406 // Reset the signal handler if we are receiving in_band_resize
407 else => Tty.resetSignalHandler(),
408 }
409 if (@hasField(Event, "winsize")) {
410 return self.postEvent(.{ .winsize = winsize });
411 }
412 },
413 }
414 },
415 }
416}
417
418test Loop {
419 const io = std.testing.io;
420 var env_map = try std.testing.environ.createMap(std.testing.allocator);
421 defer env_map.deinit();
422
423 const Event = union(enum) {
424 key_press: vaxis.Key,
425 winsize: vaxis.Winsize,
426 focus_in,
427 foo: u8,
428 };
429
430 var tty = try vaxis.Tty.init(io, &.{});
431 defer tty.deinit();
432
433 var vx = try vaxis.init(io, std.testing.allocator, &env_map, .{});
434 defer vx.deinit(std.testing.allocator, tty.writer());
435
436 var loop: vaxis.Loop(Event) = .init(io, &tty, &vx);
437
438 try loop.start();
439 defer loop.stop();
440
441 // Optionally enter the alternate screen
442 try vx.enterAltScreen(tty.writer());
443 try vx.queryTerminal(tty.writer(), .fromSeconds(1));
444}
445
446test {
447 std.testing.refAllDecls(@This());
448}