···2929 }
3030 const alloc = gpa.allocator();
31313232+ // Initalize a tty
3333+ var tty = try vaxis.Tty.init();
3434+ defer tty.deinit();
3535+3636+ // Use a buffered writer for better performance. There are a lot of writes
3737+ // in the render loop and this can have a significant savings
3838+ var buffered_writer = tty.bufferedWriter();
3939+ const writer = buffered_writer.writer().any();
4040+3241 // Initialize Vaxis
3342 var vx = try vaxis.init(alloc, .{});
3434- // deinit takes an optional allocator. If your program is exiting, you can
3535- // choose to pass a null allocator to save some exit time.
3636- defer vx.deinit(alloc);
4343+ defer vx.deinit(tty.anyWriter(), alloc);
37443838- // create our event loop
3945 var loop: vaxis.Loop(Event) = .{
4046 .vaxis = &vx,
4747+ .tty = &tty,
4148 };
4949+ try loop.init();
42504351 // Start the read loop. This puts the terminal in raw mode and begins
4452 // reading user input
4545- try loop.run();
5353+ try loop.start();
4654 defer loop.stop();
47554856 // Optionally enter the alternate screen
4949- try vx.enterAltScreen();
5757+ try vx.enterAltScreen(writer);
50585159 // We'll adjust the color index every keypress for the border
5260 var color_idx: u8 = 0;
···58665967 // Sends queries to terminal to detect certain features. This should
6068 // _always_ be called, but is left to the application to decide when
6161- try vx.queryTerminal();
6969+ // try vx.queryTerminal();
62706363- try vx.setMouseMode(true);
7171+ try vx.setMouseMode(writer, true);
7272+7373+ try buffered_writer.flush();
64746575 // The main event loop. Vaxis provides a thread safe, blocking, buffered
6676 // queue which can serve as the primary event queue for an application
···8191 } else if (key.matches('l', .{ .ctrl = true })) {
8292 vx.queueRefresh();
8393 } else if (key.matches('n', .{ .ctrl = true })) {
8484- try vx.notify("vaxis", "hello from vaxis");
9494+ try vx.notify(tty.anyWriter(), "vaxis", "hello from vaxis");
8595 loop.stop();
8696 var child = std.process.Child.init(&.{"nvim"}, alloc);
8797 _ = try child.spawnAndWait();
8888- try loop.run();
8989- try vx.enterAltScreen();
9898+ try loop.start();
9999+ try vx.enterAltScreen(tty.anyWriter());
90100 vx.queueRefresh();
91101 } else if (key.matches(vaxis.Key.enter, .{})) {
92102 text_input.clearAndFree();
···110120 // more than one byte will incur an allocation on the first render
111121 // after it is drawn. Thereafter, it will not allocate unless the
112122 // screen is resized
113113- .winsize => |ws| try vx.resize(alloc, ws),
123123+ .winsize => |ws| try vx.resize(alloc, ws, tty.anyWriter()),
114124 else => {},
115125 }
116126···140150 text_input.draw(child);
141151142152 // Render the screen
143143- try vx.render();
153153+ try vx.render(writer);
154154+ try buffered_writer.flush();
144155 }
145156}
+186-18
src/Loop.zig
···11const std = @import("std");
22+const builtin = @import("builtin");
2344+const grapheme = @import("grapheme");
55+66+const GraphemeCache = @import("GraphemeCache.zig");
77+const Parser = @import("Parser.zig");
38const Queue = @import("queue.zig").Queue;
44-const Tty = @import("Tty.zig");
99+const tty = @import("tty.zig");
1010+const Tty = tty.Tty;
511const Vaxis = @import("Vaxis.zig");
612713pub fn Loop(comptime T: type) type {
814 return struct {
915 const Self = @This();
10161717+ const Event = T;
1818+1119 const log = std.log.scoped(.loop);
12201313- queue: Queue(T, 512) = .{},
2121+ tty: *Tty,
2222+ vaxis: *Vaxis,
14232424+ queue: Queue(T, 512) = .{},
1525 thread: ?std.Thread = null,
2626+ should_quit: bool = false,
16271717- vaxis: *Vaxis,
2828+ /// Initialize the event loop. This is an intrusive init so that we have
2929+ /// a stable pointer to register signal callbacks with posix TTYs
3030+ pub fn init(self: *Self) !void {
3131+ switch (builtin.os.tag) {
3232+ .windows => @compileError("windows not supported"),
3333+ else => {
3434+ const handler: Tty.SignalHandler = .{
3535+ .context = self,
3636+ .callback = Self.winsizeCallback,
3737+ };
3838+ try Tty.notifyWinsize(handler);
3939+ },
4040+ }
4141+ }
18421943 /// spawns the input thread to read input from the tty
2020- pub fn run(self: *Self) !void {
4444+ pub fn start(self: *Self) !void {
2145 if (self.thread) |_| return;
2222- if (self.vaxis.tty == null) self.vaxis.tty = try Tty.init();
2323- self.thread = try std.Thread.spawn(.{}, Tty.run, .{
2424- &self.vaxis.tty.?,
2525- T,
4646+ self.thread = try std.Thread.spawn(.{}, Self.ttyRun, .{
2647 self,
2748 &self.vaxis.unicode.grapheme_data,
2849 self.vaxis.opts.system_clipboard_allocator,
···31523253 /// stops reading from the tty and returns it to it's initial state
3354 pub fn stop(self: *Self) void {
3434- if (self.vaxis.tty) |*tty| {
3535- // stop the read loop, then join the thread
3636- tty.stop();
3737- if (self.thread) |thread| {
3838- thread.join();
3939- self.thread = null;
4040- }
4141- // once thread is closed we can deinit the tty
4242- tty.deinit();
4343- self.vaxis.tty = null;
5555+ self.should_quit = true;
5656+ // trigger a read
5757+ self.vaxis.deviceStatusReport(self.tty.anyWriter()) catch {};
5858+5959+ if (self.thread) |thread| {
6060+ thread.join();
6161+ self.thread = null;
4462 }
4563 }
4664···68866987 pub fn tryPostEvent(self: *Self, event: T) bool {
7088 return self.queue.tryPush(event);
8989+ }
9090+9191+ pub fn winsizeCallback(ptr: *anyopaque) void {
9292+ const self: *Self = @ptrCast(@alignCast(ptr));
9393+9494+ const winsize = Tty.getWinsize(self.tty.fd) catch return;
9595+ if (@hasField(Event, "winsize")) {
9696+ self.postEvent(.{ .winsize = winsize });
9797+ }
9898+ }
9999+100100+ /// read input from the tty. This is run in a separate thread
101101+ fn ttyRun(
102102+ self: *Self,
103103+ grapheme_data: *const grapheme.GraphemeData,
104104+ paste_allocator: ?std.mem.Allocator,
105105+ ) !void {
106106+ // get our initial winsize
107107+ const winsize = try Tty.getWinsize(self.tty.fd);
108108+ if (@hasField(Event, "winsize")) {
109109+ self.postEvent(.{ .winsize = winsize });
110110+ }
111111+112112+ // initialize a grapheme cache
113113+ var cache: GraphemeCache = .{};
114114+115115+ var parser: Parser = .{
116116+ .grapheme_data = grapheme_data,
117117+ };
118118+119119+ // initialize the read buffer
120120+ var buf: [1024]u8 = undefined;
121121+ var read_start: usize = 0;
122122+ // read loop
123123+ while (!self.should_quit) {
124124+ const n = try self.tty.read(buf[read_start..]);
125125+ var seq_start: usize = 0;
126126+ while (seq_start < n) {
127127+ const result = try parser.parse(buf[seq_start..n], paste_allocator);
128128+ if (result.n == 0) {
129129+ // copy the read to the beginning. We don't use memcpy because
130130+ // this could be overlapping, and it's also rare
131131+ const initial_start = seq_start;
132132+ while (seq_start < n) : (seq_start += 1) {
133133+ buf[seq_start - initial_start] = buf[seq_start];
134134+ }
135135+ read_start = seq_start - initial_start + 1;
136136+ continue;
137137+ }
138138+ read_start = 0;
139139+ seq_start += result.n;
140140+141141+ const event = result.event orelse continue;
142142+ switch (event) {
143143+ .key_press => |key| {
144144+ if (@hasField(Event, "key_press")) {
145145+ // HACK: yuck. there has to be a better way
146146+ var mut_key = key;
147147+ if (key.text) |text| {
148148+ mut_key.text = cache.put(text);
149149+ }
150150+ self.postEvent(.{ .key_press = mut_key });
151151+ }
152152+ },
153153+ .key_release => |*key| {
154154+ if (@hasField(Event, "key_release")) {
155155+ // HACK: yuck. there has to be a better way
156156+ var mut_key = key;
157157+ if (key.text) |text| {
158158+ mut_key.text = cache.put(text);
159159+ }
160160+ self.postEvent(.{ .key_release = mut_key });
161161+ }
162162+ },
163163+ .mouse => |mouse| {
164164+ if (@hasField(Event, "mouse")) {
165165+ self.postEvent(.{ .mouse = self.vaxis.translateMouse(mouse) });
166166+ }
167167+ },
168168+ .focus_in => {
169169+ if (@hasField(Event, "focus_in")) {
170170+ self.postEvent(.focus_in);
171171+ }
172172+ },
173173+ .focus_out => {
174174+ if (@hasField(Event, "focus_out")) {
175175+ self.postEvent(.focus_out);
176176+ }
177177+ },
178178+ .paste_start => {
179179+ if (@hasField(Event, "paste_start")) {
180180+ self.postEvent(.paste_start);
181181+ }
182182+ },
183183+ .paste_end => {
184184+ if (@hasField(Event, "paste_end")) {
185185+ self.postEvent(.paste_end);
186186+ }
187187+ },
188188+ .paste => |text| {
189189+ if (@hasField(Event, "paste")) {
190190+ self.postEvent(.{ .paste = text });
191191+ } else {
192192+ if (paste_allocator) |_|
193193+ paste_allocator.?.free(text);
194194+ }
195195+ },
196196+ .color_report => |report| {
197197+ if (@hasField(Event, "color_report")) {
198198+ self.postEvent(.{ .color_report = report });
199199+ }
200200+ },
201201+ .color_scheme => |scheme| {
202202+ if (@hasField(Event, "color_scheme")) {
203203+ self.postEvent(.{ .color_scheme = scheme });
204204+ }
205205+ },
206206+ .cap_kitty_keyboard => {
207207+ log.info("kitty keyboard capability detected", .{});
208208+ self.vaxis.caps.kitty_keyboard = true;
209209+ },
210210+ .cap_kitty_graphics => {
211211+ if (!self.vaxis.caps.kitty_graphics) {
212212+ log.info("kitty graphics capability detected", .{});
213213+ self.vaxis.caps.kitty_graphics = true;
214214+ }
215215+ },
216216+ .cap_rgb => {
217217+ log.info("rgb capability detected", .{});
218218+ self.vaxis.caps.rgb = true;
219219+ },
220220+ .cap_unicode => {
221221+ log.info("unicode capability detected", .{});
222222+ self.vaxis.caps.unicode = .unicode;
223223+ self.vaxis.screen.width_method = .unicode;
224224+ },
225225+ .cap_sgr_pixels => {
226226+ log.info("pixel mouse capability detected", .{});
227227+ self.vaxis.caps.sgr_pixels = true;
228228+ },
229229+ .cap_color_scheme_updates => {
230230+ log.info("color_scheme_updates capability detected", .{});
231231+ self.vaxis.caps.color_scheme_updates = true;
232232+ },
233233+ .cap_da1 => {
234234+ std.Thread.Futex.wake(&self.vaxis.query_futex, 10);
235235+ },
236236+ }
237237+ }
238238+ }
71239 }
72240 };
73241}