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