this repo has no description
1# libvaxis
2
3```
4It begins with them, but ends with me. Their son, Vaxis
5```
6
7
8
9Libvaxis _does not use terminfo_. Support for vt features is detected through
10terminal queries.
11
12Vaxis uses zig `0.16.0`.
13
14## Features
15
16libvaxis supports all major platforms: macOS, Windows, Linux/BSD/and other
17Unix-likes.
18
19- RGB
20- [Hyperlinks](https://gist.github.com/egmontkob/eb114294efbcd5adb1944c9f3cb5feda) (OSC 8)
21- Bracketed Paste
22- [Kitty Keyboard Protocol](https://sw.kovidgoyal.net/kitty/keyboard-protocol/)
23- [Fancy underlines](https://sw.kovidgoyal.net/kitty/underlines/) (undercurl, etc)
24- Mouse Shapes (OSC 22)
25- System Clipboard (OSC 52)
26- System Notifications (OSC 9)
27- System Notifications (OSC 777)
28- Synchronized Output (Mode 2026)
29- [Unicode Core](https://github.com/contour-terminal/terminal-unicode-core) (Mode 2027)
30- Color Mode Updates (Mode 2031)
31- [In-Band Resize Reports](https://gist.github.com/rockorager/e695fb2924d36b2bcf1fff4a3704bd83) (Mode 2048)
32- Images ([kitty graphics protocol](https://sw.kovidgoyal.net/kitty/graphics-protocol/))
33- [Explicit Width](https://github.com/kovidgoyal/kitty/blob/master/docs/text-sizing-protocol.rst) (width modifiers only)
34
35## Usage
36
37[Documentation](https://rockorager.github.io/libvaxis/#vaxis.Vaxis)
38
39The library provides both a low level API suitable for making applications of
40any sort as well as a higher level framework. The low level API is suitable for
41making applications of any type, providing your own event loop, and gives you
42full control over each cell on the screen.
43
44The high level API, called `vxfw` (Vaxis framework), provides a Flutter-like
45style of API. The framework provides an application runtime which handles the
46event loop, focus management, mouse handling, and more. Several widgets are
47provided, and custom widgets are easy to build. This API is most likely what you
48want to use for typical TUI applications.
49
50### Add libvaxis to your project
51
52```console
53zig fetch --save git+https://github.com/rockorager/libvaxis.git
54```
55Add this to your build.zig
56
57```zig
58 const vaxis = b.dependency("vaxis", .{
59 .target = target,
60 .optimize = optimize,
61 });
62
63 exe.root_module.addImport("vaxis", vaxis.module("vaxis"));
64```
65
66or for ZLS support
67
68```zig
69 // create module
70 const exe_mod = b.createModule(.{
71 .root_source_file = b.path("src/main.zig"),
72 .target = target,
73 .optimize = optimize,
74 });
75
76 // add vaxis dependency to module
77 const vaxis = b.dependency("vaxis", .{
78 .target = target,
79 .optimize = optimize,
80 });
81 exe_mod.addImport("vaxis", vaxis.module("vaxis"));
82
83 //create executable
84 const exe = b.addExecutable(.{
85 .name = "project_foo",
86 .root_module = exe_mod,
87 });
88 // install exe below
89```
90
91### vxfw (Vaxis framework)
92
93Let's build a simple button counter application. This example can be run using
94the command `zig build example -Dexample=counter`. The below application has
95full mouse support: the button *and mouse shape* will change style on hover, on
96click, and has enough logic to cancel a press if the release does not occur over
97the button. Try it! Click the button, move the mouse off the button and release.
98All of this logic is baked into the base `Button` widget.
99
100```zig
101const std = @import("std");
102const vaxis = @import("vaxis");
103const vxfw = vaxis.vxfw;
104
105/// Our main application state
106const Model = struct {
107 /// State of the counter
108 count: u32 = 0,
109 /// The button. This widget is stateful and must live between frames
110 button: vxfw.Button,
111
112 /// Helper function to return a vxfw.Widget struct
113 pub fn widget(self: *Model) vxfw.Widget {
114 return .{
115 .userdata = self,
116 .eventHandler = Model.typeErasedEventHandler,
117 .drawFn = Model.typeErasedDrawFn,
118 };
119 }
120
121 /// This function will be called from the vxfw runtime.
122 fn typeErasedEventHandler(ptr: *anyopaque, ctx: *vxfw.EventContext, event: vxfw.Event) anyerror!void {
123 const self: *Model = @ptrCast(@alignCast(ptr));
124 switch (event) {
125 // The root widget is always sent an init event as the first event. Users of the
126 // library can also send this event to other widgets they create if they need to do
127 // some initialization.
128 .init => return ctx.requestFocus(self.button.widget()),
129 .key_press => |key| {
130 if (key.matches('c', .{ .ctrl = true })) {
131 ctx.quit = true;
132 return;
133 }
134 },
135 // We can request a specific widget gets focus. In this case, we always want to focus
136 // our button. Having focus means that key events will be sent up the widget tree to
137 // the focused widget, and then bubble back down the tree to the root. Users can tell
138 // the runtime the event was handled and the capture or bubble phase will stop
139 .focus_in => return ctx.requestFocus(self.button.widget()),
140 else => {},
141 }
142 }
143
144 /// This function is called from the vxfw runtime. It will be called on a regular interval, and
145 /// only when any event handler has marked the redraw flag in EventContext as true. By
146 /// explicitly requiring setting the redraw flag, vxfw can prevent excessive redraws for events
147 /// which don't change state (ie mouse motion, unhandled key events, etc)
148 fn typeErasedDrawFn(ptr: *anyopaque, ctx: vxfw.DrawContext) std.mem.Allocator.Error!vxfw.Surface {
149 const self: *Model = @ptrCast(@alignCast(ptr));
150 // The DrawContext is inspired from Flutter. Each widget will receive a minimum and maximum
151 // constraint. The minimum constraint will always be set, even if it is set to 0x0. The
152 // maximum constraint can have null width and/or height - meaning there is no constraint in
153 // that direction and the widget should take up as much space as it needs. By calling size()
154 // on the max, we assert that it has some constrained size. This is *always* the case for
155 // the root widget - the maximum size will always be the size of the terminal screen.
156 const max_size = ctx.max.size();
157
158 // The DrawContext also contains an arena allocator that can be used for each frame. The
159 // lifetime of this allocation is until the next time we draw a frame. This is useful for
160 // temporary allocations such as the one below: we have an integer we want to print as text.
161 // We can safely allocate this with the ctx arena since we only need it for this frame.
162 const count_text = try std.fmt.allocPrint(ctx.arena, "{d}", .{self.count});
163 const text: vxfw.Text = .{ .text = count_text };
164
165 // Each widget returns a Surface from its draw function. A Surface contains the rectangular
166 // area of the widget, as well as some information about the surface or widget: can we focus
167 // it? does it handle the mouse?
168 //
169 // It DOES NOT contain the location it should be within its parent. Only the parent can set
170 // this via a SubSurface. Here, we will return a Surface for the root widget (Model), which
171 // has two SubSurfaces: one for the text and one for the button. A SubSurface is a Surface
172 // with an offset and a z-index - the offset can be negative. This lets a parent draw a
173 // child and place it within itself
174 const text_child: vxfw.SubSurface = .{
175 .origin = .{ .row = 0, .col = 0 },
176 .surface = try text.draw(ctx),
177 };
178
179 const button_child: vxfw.SubSurface = .{
180 .origin = .{ .row = 2, .col = 0 },
181 .surface = try self.button.draw(ctx.withConstraints(
182 ctx.min,
183 // Here we explicitly set a new maximum size constraint for the Button. A Button will
184 // expand to fill its area and must have some hard limit in the maximum constraint
185 .{ .width = 16, .height = 3 },
186 )),
187 };
188
189 // We also can use our arena to allocate the slice for our SubSurfaces. This slice only
190 // needs to live until the next frame, making this safe.
191 const children = try ctx.arena.alloc(vxfw.SubSurface, 2);
192 children[0] = text_child;
193 children[1] = button_child;
194
195 return .{
196 // A Surface must have a size. Our root widget is the size of the screen
197 .size = max_size,
198 .widget = self.widget(),
199 // We didn't actually need to draw anything for the root. In this case, we can set
200 // buffer to a zero length slice. If this slice is *not zero length*, the runtime will
201 // assert that its length is equal to the size.width * size.height.
202 .buffer = &.{},
203 .children = children,
204 };
205 }
206
207 /// The onClick callback for our button. This is also called if we press enter while the button
208 /// has focus
209 fn onClick(maybe_ptr: ?*anyopaque, ctx: *vxfw.EventContext) anyerror!void {
210 const ptr = maybe_ptr orelse return;
211 const self: *Model = @ptrCast(@alignCast(ptr));
212 self.count +|= 1;
213 return ctx.consumeAndRedraw();
214 }
215};
216
217pub fn main(init: std.process.Init) !void {
218 const io = init.io;
219 const alloc = init.gpa;
220
221 var buffer: [1024]u8 = undefined;
222 var app: vxfw.App = try .init(io, alloc, init.environ_map, &buffer);
223 defer app.deinit();
224
225 // We heap allocate our model because we will require a stable pointer to it in our Button
226 // widget
227 const model = try alloc.create(Model);
228 defer allocator.destroy(model);
229
230 // Set the initial state of our button
231 model.* = .{
232 .count = 0,
233 .button = .{
234 .label = "Click me!",
235 .onClick = Model.onClick,
236 .userdata = model,
237 },
238 };
239
240 try app.run(model.widget(), .{});
241}
242```
243
244### Low level API
245
246Vaxis requires three basic primitives to operate:
247
2481. A TTY instance
2492. An instance of Vaxis
2503. An event loop
251
252The library provides a general purpose posix TTY implementation, as well as a
253multi-threaded event loop implementation. Users of the library are encouraged to
254use the event loop of their choice. The event loop is responsible for reading
255the TTY, passing the read bytes to the vaxis parser, and handling events.
256
257A core feature of Vaxis is its ability to detect features via terminal queries
258instead of relying on a terminfo database. This requires that the event loop
259also handle these query responses and update the Vaxis.caps struct accordingly.
260See the `Loop` implementation to see how this is done if writing your own event
261loop.
262
263```zig
264const std = @import("std");
265const vaxis = @import("vaxis");
266const Cell = vaxis.Cell;
267const TextInput = vaxis.widgets.TextInput;
268const border = vaxis.widgets.border;
269
270// This can contain internal events as well as Vaxis events.
271// Internal events can be posted into the same queue as vaxis events to allow
272// for a single event loop with exhaustive switching. Booya
273const Event = union(enum) {
274 key_press: vaxis.Key,
275 winsize: vaxis.Winsize,
276 focus_in,
277 foo: u8,
278};
279
280pub fn main(init: std.process.Init) !void {
281 const io = init.io;
282 const alloc = init.gpa;
283
284 // Initialize a tty
285 var buffer: [1024]u8 = undefined;
286 var tty = try vaxis.Tty.init(io, &buffer);
287 defer tty.deinit();
288
289 // Initialize Vaxis
290 var vx = try vaxis.init(io, alloc, init.environ_map, .{});
291 // deinit takes an optional allocator. If your program is exiting, you can
292 // choose to pass a null allocator to save some exit time.
293 defer vx.deinit(alloc, tty.writer());
294
295
296 // The event loop requires an intrusive init. We create an instance with
297 // stable pointers to Vaxis and our TTY, then init the instance. Doing so
298 // installs a signal handler for SIGWINCH on posix TTYs
299 //
300 // This event loop is thread safe. It reads the tty in a separate thread
301 var loop: vaxis.Loop(Event) = .init(io, &tty, &vx);
302
303 // Start the read loop. This puts the terminal in raw mode and begins
304 // reading user input
305 try loop.start();
306 defer loop.stop();
307
308 // Optionally enter the alternate screen
309 try vx.enterAltScreen(tty.writer());
310
311 // We'll adjust the color index every keypress for the border
312 var color_idx: u8 = 0;
313
314 // init our text input widget. The text input widget needs an allocator to
315 // store the contents of the input
316 var text_input = TextInput.init(alloc);
317 defer text_input.deinit();
318
319 // Sends queries to terminal to detect certain features. This should always
320 // be called after entering the alt screen, if you are using the alt screen
321 try vx.queryTerminal(tty.writer(), .fromSeconds(1));
322
323 while (true) {
324 // nextEvent blocks until an event is in the queue
325 const event = loop.nextEvent();
326 // exhaustive switching ftw. Vaxis will send events if your Event enum
327 // has the fields for those events (ie "key_press", "winsize")
328 switch (event) {
329 .key_press => |key| {
330 color_idx = switch (color_idx) {
331 255 => 0,
332 else => color_idx + 1,
333 };
334 if (key.matches('c', .{ .ctrl = true })) {
335 break;
336 } else if (key.matches('l', .{ .ctrl = true })) {
337 vx.queueRefresh();
338 } else {
339 try text_input.update(.{ .key_press = key });
340 }
341 },
342
343 // winsize events are sent to the application to ensure that all
344 // resizes occur in the main thread. This lets us avoid expensive
345 // locks on the screen. All applications must handle this event
346 // unless they aren't using a screen (IE only detecting features)
347 //
348 // The allocations are because we keep a copy of each cell to
349 // optimize renders. When resize is called, we allocated two slices:
350 // one for the screen, and one for our buffered screen. Each cell in
351 // the buffered screen contains an ArrayList(u8) to be able to store
352 // the grapheme for that cell. Each cell is initialized with a size
353 // of 1, which is sufficient for all of ASCII. Anything requiring
354 // more than one byte will incur an allocation on the first render
355 // after it is drawn. Thereafter, it will not allocate unless the
356 // screen is resized
357 .winsize => |ws| try vx.resize(alloc, tty.writer(), ws),
358 else => {},
359 }
360
361 // vx.window() returns the root window. This window is the size of the
362 // terminal and can spawn child windows as logical areas. Child windows
363 // cannot draw outside of their bounds
364 const win = vx.window();
365
366 // Clear the entire space because we are drawing in immediate mode.
367 // vaxis double buffers the screen. This new frame will be compared to
368 // the old and only updated cells will be drawn
369 win.clear();
370
371 // Create a style
372 const style: vaxis.Style = .{
373 .fg = .{ .index = color_idx },
374 };
375
376 // Create a bordered child window
377 const child = win.child(.{
378 .x_off = win.width / 2 - 20,
379 .y_off = win.height / 2 - 3,
380 .width = 40 ,
381 .height = 3 ,
382 .border = .{
383 .where = .all,
384 .style = style,
385 },
386 });
387
388 // Draw the text_input in the child window
389 text_input.draw(child);
390
391 // Render the screen. Using a buffered writer will offer much better
392 // performance, but is not required
393 try vx.render(tty.writer());
394 }
395}
396```
397
398## Contributing
399
400Contributions are welcome. Please submit a PR on Github,
401[tangled](https://tangled.sh/@rockorager.dev/libvaxis), or a patch on the
402[mailing list](mailto:~rockorager/libvaxis@lists.sr.ht)
403
404## Community
405
406We use [Github Discussions](https://github.com/rockorager/libvaxis/discussions)
407as the primary location for community support, showcasing what you are working
408on, and discussing library features and usage.
409
410We also have an IRC channel on libera.chat: join us in #vaxis.