a modern tui library written in zig
at v0.2.0 156 lines 6.0 kB view raw
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}