a modern tui library written in zig
1const std = @import("std");
2const vaxis = @import("vaxis");
3const Cell = vaxis.Cell;
4const TextInput = vaxis.widgets.TextInput;
5const border = vaxis.widgets.border;
6
7const log = std.log.scoped(.main);
8
9// Our Event. This can contain internal events as well as Vaxis events.
10// Internal events can be posted into the same queue as vaxis events to allow
11// for a single event loop with exhaustive switching. Booya
12const Event = union(enum) {
13 key_press: vaxis.Key,
14 mouse: vaxis.Mouse,
15 winsize: vaxis.Winsize,
16 focus_in,
17 focus_out,
18 foo: u8,
19};
20
21pub fn main() !void {
22 var gpa = std.heap.GeneralPurposeAllocator(.{}){};
23 defer {
24 const deinit_status = gpa.deinit();
25 //fail test; can't try in defer as defer is executed after we return
26 if (deinit_status == .leak) {
27 log.err("memory leak", .{});
28 }
29 }
30 const alloc = gpa.allocator();
31
32 // Initalize a tty
33 var buffer: [1024]u8 = undefined;
34 var tty = try vaxis.Tty.init(&buffer);
35 defer tty.deinit();
36
37 // Use a buffered writer for better performance. There are a lot of writes
38 // in the render loop and this can have a significant savings
39 const writer = tty.writer();
40
41 // Initialize Vaxis
42 var vx = try vaxis.init(alloc, .{
43 .kitty_keyboard_flags = .{ .report_events = true },
44 });
45 defer vx.deinit(alloc, tty.writer());
46
47 var loop: vaxis.Loop(Event) = .{
48 .vaxis = &vx,
49 .tty = &tty,
50 };
51 try loop.init();
52
53 // Start the read loop. This puts the terminal in raw mode and begins
54 // reading user input
55 try loop.start();
56 defer loop.stop();
57
58 // Optionally enter the alternate screen
59 try vx.enterAltScreen(writer);
60
61 // We'll adjust the color index every keypress for the border
62 var color_idx: u8 = 0;
63
64 // init our text input widget. The text input widget needs an allocator to
65 // store the contents of the input
66 var text_input = TextInput.init(alloc);
67 defer text_input.deinit();
68
69 try vx.setMouseMode(writer, true);
70
71 try writer.flush();
72 // Sends queries to terminal to detect certain features. This should
73 // _always_ be called, but is left to the application to decide when
74 try vx.queryTerminal(tty.writer(), 1 * std.time.ns_per_s);
75
76 // The main event loop. Vaxis provides a thread safe, blocking, buffered
77 // queue which can serve as the primary event queue for an application
78 while (true) {
79 // nextEvent blocks until an event is in the queue
80 const event = loop.nextEvent();
81 log.debug("event: {}", .{event});
82 // exhaustive switching ftw. Vaxis will send events if your Event
83 // enum has the fields for those events (ie "key_press", "winsize")
84 switch (event) {
85 .key_press => |key| {
86 color_idx = switch (color_idx) {
87 255 => 0,
88 else => color_idx + 1,
89 };
90 if (key.matches('c', .{ .ctrl = true })) {
91 break;
92 } else if (key.matches('l', .{ .ctrl = true })) {
93 vx.queueRefresh();
94 } else if (key.matches('n', .{ .ctrl = true })) {
95 try vx.notify(tty.writer(), "vaxis", "hello from vaxis");
96 loop.stop();
97 var child = std.process.Child.init(&.{"nvim"}, alloc);
98 _ = try child.spawnAndWait();
99 try loop.start();
100 try vx.enterAltScreen(tty.writer());
101 vx.queueRefresh();
102 } else if (key.matches(vaxis.Key.enter, .{}) or key.matches('j', .{ .ctrl = true })) {
103 text_input.clearAndFree();
104 } else {
105 try text_input.update(.{ .key_press = key });
106 }
107 },
108
109 // winsize events are sent to the application to ensure that all
110 // resizes occur in the main thread. This lets us avoid expensive
111 // locks on the screen. All applications must handle this event
112 // unless they aren't using a screen (IE only detecting features)
113 //
114 // This is the only call that the core of Vaxis needs an allocator
115 // for. The allocations are because we keep a copy of each cell to
116 // optimize renders. When resize is called, we allocated two slices:
117 // one for the screen, and one for our buffered screen. Each cell in
118 // the buffered screen contains an ArrayList(u8) to be able to store
119 // the grapheme for that cell Each cell is initialized with a size
120 // of 1, which is sufficient for all of ASCII. Anything requiring
121 // more than one byte will incur an allocation on the first render
122 // after it is drawn. Thereafter, it will not allocate unless the
123 // screen is resized
124 .winsize => |ws| try vx.resize(alloc, tty.writer(), ws),
125 else => {},
126 }
127
128 // vx.window() returns the root window. This window is the size of the
129 // terminal and can spawn child windows as logical areas. Child windows
130 // cannot draw outside of their bounds
131 const win = vx.window();
132
133 // Clear the entire space because we are drawing in immediate mode.
134 // vaxis double buffers the screen. This new frame will be compared to
135 // the old and only updated cells will be drawn
136 win.clear();
137 // draw the text_input using a bordered window
138 const style: vaxis.Style = .{
139 .fg = .{ .index = color_idx },
140 };
141 const child = win.child(.{
142 .x_off = win.width / 2 - 20,
143 .y_off = win.height / 2 - 3,
144 .width = 40,
145 .height = 3,
146 .border = .{
147 .where = .all,
148 .style = style,
149 },
150 });
151 text_input.draw(child);
152
153 // Render the screen
154 try vx.render(writer);
155 try writer.flush();
156 }
157}