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