this repo has no description
1const std = @import("std");
2const builtin = @import("builtin");
3
4const grapheme = @import("grapheme");
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.g_data,
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 grapheme.GraphemeData,
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 explicity 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 if (@hasField(Event, "key_press")) {
193 // HACK: yuck. there has to be a better way
194 var mut_key = key;
195 if (key.text) |text| {
196 mut_key.text = cache.put(text);
197 }
198 return self.postEvent(.{ .key_press = mut_key });
199 }
200 },
201 .key_release => |key| {
202 if (@hasField(Event, "key_release")) {
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_release = mut_key });
209 }
210 },
211 .cap_da1 => {
212 std.Thread.Futex.wake(&vx.query_futex, 10);
213 vx.queries_done.store(true, .unordered);
214 },
215 .mouse => |mouse| {
216 if (@hasField(Event, "mouse")) {
217 return self.postEvent(.{ .mouse = vx.translateMouse(mouse) });
218 }
219 },
220 .focus_in => {
221 if (@hasField(Event, "focus_in")) {
222 return self.postEvent(.focus_in);
223 }
224 },
225 .focus_out => {
226 if (@hasField(Event, "focus_out")) {
227 return self.postEvent(.focus_out);
228 }
229 }, // Unsupported currently
230 else => {},
231 }
232 },
233 else => {
234 switch (event) {
235 .key_press => |key| {
236 // Check for a cursor position response for our explicity width query. This will
237 // always be an F3 key with shift = true, and we must be looking for queries
238 if (key.codepoint == vaxis.Key.f3 and
239 key.mods.shift and
240 !vx.queries_done.load(.unordered))
241 {
242 log.info("explicit width capability detected", .{});
243 vx.caps.explicit_width = true;
244 vx.caps.unicode = .unicode;
245 vx.screen.width_method = .unicode;
246 return;
247 }
248 if (@hasField(Event, "key_press")) {
249 // HACK: yuck. there has to be a better way
250 var mut_key = key;
251 if (key.text) |text| {
252 mut_key.text = cache.put(text);
253 }
254 return self.postEvent(.{ .key_press = mut_key });
255 }
256 },
257 .key_release => |key| {
258 if (@hasField(Event, "key_release")) {
259 // HACK: yuck. there has to be a better way
260 var mut_key = key;
261 if (key.text) |text| {
262 mut_key.text = cache.put(text);
263 }
264 return self.postEvent(.{ .key_release = mut_key });
265 }
266 },
267 .mouse => |mouse| {
268 if (@hasField(Event, "mouse")) {
269 return self.postEvent(.{ .mouse = vx.translateMouse(mouse) });
270 }
271 },
272 .focus_in => {
273 if (@hasField(Event, "focus_in")) {
274 return self.postEvent(.focus_in);
275 }
276 },
277 .focus_out => {
278 if (@hasField(Event, "focus_out")) {
279 return self.postEvent(.focus_out);
280 }
281 },
282 .paste_start => {
283 if (@hasField(Event, "paste_start")) {
284 return self.postEvent(.paste_start);
285 }
286 },
287 .paste_end => {
288 if (@hasField(Event, "paste_end")) {
289 return self.postEvent(.paste_end);
290 }
291 },
292 .paste => |text| {
293 if (@hasField(Event, "paste")) {
294 return self.postEvent(.{ .paste = text });
295 } else {
296 if (paste_allocator) |_|
297 paste_allocator.?.free(text);
298 }
299 },
300 .color_report => |report| {
301 if (@hasField(Event, "color_report")) {
302 return self.postEvent(.{ .color_report = report });
303 }
304 },
305 .color_scheme => |scheme| {
306 if (@hasField(Event, "color_scheme")) {
307 return self.postEvent(.{ .color_scheme = scheme });
308 }
309 },
310 .cap_kitty_keyboard => {
311 log.info("kitty keyboard capability detected", .{});
312 vx.caps.kitty_keyboard = true;
313 },
314 .cap_kitty_graphics => {
315 if (!vx.caps.kitty_graphics) {
316 log.info("kitty graphics capability detected", .{});
317 vx.caps.kitty_graphics = true;
318 }
319 },
320 .cap_rgb => {
321 log.info("rgb capability detected", .{});
322 vx.caps.rgb = true;
323 },
324 .cap_unicode => {
325 log.info("unicode capability detected", .{});
326 vx.caps.unicode = .unicode;
327 vx.screen.width_method = .unicode;
328 },
329 .cap_sgr_pixels => {
330 log.info("pixel mouse capability detected", .{});
331 vx.caps.sgr_pixels = true;
332 },
333 .cap_color_scheme_updates => {
334 log.info("color_scheme_updates capability detected", .{});
335 vx.caps.color_scheme_updates = true;
336 },
337 .cap_da1 => {
338 std.Thread.Futex.wake(&vx.query_futex, 10);
339 vx.queries_done.store(true, .unordered);
340 },
341 .winsize => |winsize| {
342 vx.state.in_band_resize = true;
343 switch (builtin.os.tag) {
344 .windows => {},
345 // Reset the signal handler if we are receiving in_band_resize
346 else => Tty.resetSignalHandler(),
347 }
348 if (@hasField(Event, "winsize")) {
349 return self.postEvent(.{ .winsize = winsize });
350 }
351 },
352 }
353 },
354 }
355}
356
357test Loop {
358 const Event = union(enum) {
359 key_press: vaxis.Key,
360 winsize: vaxis.Winsize,
361 focus_in,
362 foo: u8,
363 };
364
365 var tty = try vaxis.Tty.init();
366 defer tty.deinit();
367
368 var vx = try vaxis.init(std.testing.allocator, .{});
369 defer vx.deinit(std.testing.allocator, tty.anyWriter());
370
371 var loop: vaxis.Loop(Event) = .{ .tty = &tty, .vaxis = &vx };
372 try loop.init();
373
374 try loop.start();
375 defer loop.stop();
376
377 // Optionally enter the alternate screen
378 try vx.enterAltScreen(tty.anyWriter());
379 try vx.queryTerminal(tty.anyWriter(), 1 * std.time.ns_per_ms);
380}