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#### Sharing `uucode` with your application
92
93By default, libvaxis pulls in
94[`uucode`](https://github.com/jacobsandlund/uucode) with a fixed set of fields
95it needs (see [build.zig](./build.zig)). If your application also uses
96`uucode`, you can build libvaxis against your own `uucode` module so that
97everyone shares one table (or set of tables, based on your `uucode`
98configuration).
99
100Pass `.external_uucode = true` to the libvaxis dependency and wire your
101`uucode` module into the `vaxis` module yourself:
102
103```zig
104 const uucode = b.dependency("uucode", .{
105 .target = target,
106 .optimize = optimize,
107 .fields = @as([]const []const u8, &.{
108 // Add any fields your application needs, plus the fields libvaxis
109 // requires. The compiler will tell you which fields are missing
110 // (you only need the libvaxis fields for the parts of libvaxis you
111 // actually use). See `build.zig` in libvaxis for the full set it
112 // uses internally.
113 }),
114 });
115
116 const vaxis = b.dependency("vaxis", .{
117 .target = target,
118 .optimize = optimize,
119 .external_uucode = true,
120 });
121 vaxis.module("vaxis").addImport("uucode", uucode.module("uucode"));
122
123 exe.root_module.addImport("vaxis", vaxis.module("vaxis"));
124 exe.root_module.addImport("uucode", uucode.module("uucode"));
125```
126
127### vxfw (Vaxis framework)
128
129Let's build a simple button counter application. This example can be run using
130the command `zig build example -Dexample=counter`. The below application has
131full mouse support: the button *and mouse shape* will change style on hover, on
132click, and has enough logic to cancel a press if the release does not occur over
133the button. Try it! Click the button, move the mouse off the button and release.
134All of this logic is baked into the base `Button` widget.
135
136```zig
137const std = @import("std");
138const vaxis = @import("vaxis");
139const vxfw = vaxis.vxfw;
140
141/// Our main application state
142const Model = struct {
143 /// State of the counter
144 count: u32 = 0,
145 /// The button. This widget is stateful and must live between frames
146 button: vxfw.Button,
147
148 /// Helper function to return a vxfw.Widget struct
149 pub fn widget(self: *Model) vxfw.Widget {
150 return .{
151 .userdata = self,
152 .eventHandler = Model.typeErasedEventHandler,
153 .drawFn = Model.typeErasedDrawFn,
154 };
155 }
156
157 /// This function will be called from the vxfw runtime.
158 fn typeErasedEventHandler(ptr: *anyopaque, ctx: *vxfw.EventContext, event: vxfw.Event) anyerror!void {
159 const self: *Model = @ptrCast(@alignCast(ptr));
160 switch (event) {
161 // The root widget is always sent an init event as the first event. Users of the
162 // library can also send this event to other widgets they create if they need to do
163 // some initialization.
164 .init => return ctx.requestFocus(self.button.widget()),
165 .key_press => |key| {
166 if (key.matches('c', .{ .ctrl = true })) {
167 ctx.quit = true;
168 return;
169 }
170 },
171 // We can request a specific widget gets focus. In this case, we always want to focus
172 // our button. Having focus means that key events will be sent up the widget tree to
173 // the focused widget, and then bubble back down the tree to the root. Users can tell
174 // the runtime the event was handled and the capture or bubble phase will stop
175 .focus_in => return ctx.requestFocus(self.button.widget()),
176 else => {},
177 }
178 }
179
180 /// This function is called from the vxfw runtime. It will be called on a regular interval, and
181 /// only when any event handler has marked the redraw flag in EventContext as true. By
182 /// explicitly requiring setting the redraw flag, vxfw can prevent excessive redraws for events
183 /// which don't change state (ie mouse motion, unhandled key events, etc)
184 fn typeErasedDrawFn(ptr: *anyopaque, ctx: vxfw.DrawContext) std.mem.Allocator.Error!vxfw.Surface {
185 const self: *Model = @ptrCast(@alignCast(ptr));
186 // The DrawContext is inspired from Flutter. Each widget will receive a minimum and maximum
187 // constraint. The minimum constraint will always be set, even if it is set to 0x0. The
188 // maximum constraint can have null width and/or height - meaning there is no constraint in
189 // that direction and the widget should take up as much space as it needs. By calling size()
190 // on the max, we assert that it has some constrained size. This is *always* the case for
191 // the root widget - the maximum size will always be the size of the terminal screen.
192 const max_size = ctx.max.size();
193
194 // The DrawContext also contains an arena allocator that can be used for each frame. The
195 // lifetime of this allocation is until the next time we draw a frame. This is useful for
196 // temporary allocations such as the one below: we have an integer we want to print as text.
197 // We can safely allocate this with the ctx arena since we only need it for this frame.
198 const count_text = try std.fmt.allocPrint(ctx.arena, "{d}", .{self.count});
199 const text: vxfw.Text = .{ .text = count_text };
200
201 // Each widget returns a Surface from its draw function. A Surface contains the rectangular
202 // area of the widget, as well as some information about the surface or widget: can we focus
203 // it? does it handle the mouse?
204 //
205 // It DOES NOT contain the location it should be within its parent. Only the parent can set
206 // this via a SubSurface. Here, we will return a Surface for the root widget (Model), which
207 // has two SubSurfaces: one for the text and one for the button. A SubSurface is a Surface
208 // with an offset and a z-index - the offset can be negative. This lets a parent draw a
209 // child and place it within itself
210 const text_child: vxfw.SubSurface = .{
211 .origin = .{ .row = 0, .col = 0 },
212 .surface = try text.draw(ctx),
213 };
214
215 const button_child: vxfw.SubSurface = .{
216 .origin = .{ .row = 2, .col = 0 },
217 .surface = try self.button.draw(ctx.withConstraints(
218 ctx.min,
219 // Here we explicitly set a new maximum size constraint for the Button. A Button will
220 // expand to fill its area and must have some hard limit in the maximum constraint
221 .{ .width = 16, .height = 3 },
222 )),
223 };
224
225 // We also can use our arena to allocate the slice for our SubSurfaces. This slice only
226 // needs to live until the next frame, making this safe.
227 const children = try ctx.arena.alloc(vxfw.SubSurface, 2);
228 children[0] = text_child;
229 children[1] = button_child;
230
231 return .{
232 // A Surface must have a size. Our root widget is the size of the screen
233 .size = max_size,
234 .widget = self.widget(),
235 // We didn't actually need to draw anything for the root. In this case, we can set
236 // buffer to a zero length slice. If this slice is *not zero length*, the runtime will
237 // assert that its length is equal to the size.width * size.height.
238 .buffer = &.{},
239 .children = children,
240 };
241 }
242
243 /// The onClick callback for our button. This is also called if we press enter while the button
244 /// has focus
245 fn onClick(maybe_ptr: ?*anyopaque, ctx: *vxfw.EventContext) anyerror!void {
246 const ptr = maybe_ptr orelse return;
247 const self: *Model = @ptrCast(@alignCast(ptr));
248 self.count +|= 1;
249 return ctx.consumeAndRedraw();
250 }
251};
252
253pub fn main(init: std.process.Init) !void {
254 const io = init.io;
255 const alloc = init.gpa;
256
257 var buffer: [1024]u8 = undefined;
258 var app: vxfw.App = try .init(io, alloc, init.environ_map, &buffer);
259 defer app.deinit();
260
261 // We heap allocate our model because we will require a stable pointer to it in our Button
262 // widget
263 const model = try alloc.create(Model);
264 defer allocator.destroy(model);
265
266 // Set the initial state of our button
267 model.* = .{
268 .count = 0,
269 .button = .{
270 .label = "Click me!",
271 .onClick = Model.onClick,
272 .userdata = model,
273 },
274 };
275
276 try app.run(model.widget(), .{});
277}
278```
279
280### Low level API
281
282Vaxis requires three basic primitives to operate:
283
2841. A TTY instance
2852. An instance of Vaxis
2863. An event loop
287
288The library provides a general purpose posix TTY implementation, as well as a
289multi-threaded event loop implementation. Users of the library are encouraged to
290use the event loop of their choice. The event loop is responsible for reading
291the TTY, passing the read bytes to the vaxis parser, and handling events.
292
293A core feature of Vaxis is its ability to detect features via terminal queries
294instead of relying on a terminfo database. This requires that the event loop
295also handle these query responses and update the Vaxis.caps struct accordingly.
296See the `Loop` implementation to see how this is done if writing your own event
297loop.
298
299```zig
300const std = @import("std");
301const vaxis = @import("vaxis");
302const Cell = vaxis.Cell;
303const TextInput = vaxis.widgets.TextInput;
304const border = vaxis.widgets.border;
305
306// This can contain internal events as well as Vaxis events.
307// Internal events can be posted into the same queue as vaxis events to allow
308// for a single event loop with exhaustive switching. Booya
309const Event = union(enum) {
310 key_press: vaxis.Key,
311 winsize: vaxis.Winsize,
312 focus_in,
313 foo: u8,
314};
315
316pub fn main(init: std.process.Init) !void {
317 const io = init.io;
318 const alloc = init.gpa;
319
320 // Initialize a tty
321 var buffer: [1024]u8 = undefined;
322 var tty = try vaxis.Tty.init(io, &buffer);
323 defer tty.deinit();
324
325 // Initialize Vaxis
326 var vx = try vaxis.init(io, alloc, init.environ_map, .{});
327 // deinit takes an optional allocator. If your program is exiting, you can
328 // choose to pass a null allocator to save some exit time.
329 defer vx.deinit(alloc, tty.writer());
330
331
332 // The event loop requires an intrusive init. We create an instance with
333 // stable pointers to Vaxis and our TTY, then init the instance. Doing so
334 // installs a signal handler for SIGWINCH on posix TTYs
335 //
336 // This event loop is thread safe. It reads the tty in a separate thread
337 var loop: vaxis.Loop(Event) = .init(io, &tty, &vx);
338
339 // Start the read loop. This puts the terminal in raw mode and begins
340 // reading user input
341 try loop.start();
342 defer loop.stop();
343
344 // Optionally enter the alternate screen
345 try vx.enterAltScreen(tty.writer());
346
347 // We'll adjust the color index every keypress for the border
348 var color_idx: u8 = 0;
349
350 // init our text input widget. The text input widget needs an allocator to
351 // store the contents of the input
352 var text_input = TextInput.init(alloc);
353 defer text_input.deinit();
354
355 // Sends queries to terminal to detect certain features. This should always
356 // be called after entering the alt screen, if you are using the alt screen
357 try vx.queryTerminal(tty.writer(), .fromSeconds(1));
358
359 while (true) {
360 // nextEvent blocks until an event is in the queue
361 const event = try loop.nextEvent();
362 // exhaustive switching ftw. Vaxis will send events if your Event enum
363 // has the fields for those events (ie "key_press", "winsize")
364 switch (event) {
365 .key_press => |key| {
366 color_idx = switch (color_idx) {
367 255 => 0,
368 else => color_idx + 1,
369 };
370 if (key.matches('c', .{ .ctrl = true })) {
371 break;
372 } else if (key.matches('l', .{ .ctrl = true })) {
373 vx.queueRefresh();
374 } else {
375 try text_input.update(.{ .key_press = key });
376 }
377 },
378
379 // winsize events are sent to the application to ensure that all
380 // resizes occur in the main thread. This lets us avoid expensive
381 // locks on the screen. All applications must handle this event
382 // unless they aren't using a screen (IE only detecting features)
383 //
384 // The allocations are because we keep a copy of each cell to
385 // optimize renders. When resize is called, we allocated two slices:
386 // one for the screen, and one for our buffered screen. Each cell in
387 // the buffered screen contains an ArrayList(u8) to be able to store
388 // the grapheme for that cell. Each cell is initialized with a size
389 // of 1, which is sufficient for all of ASCII. Anything requiring
390 // more than one byte will incur an allocation on the first render
391 // after it is drawn. Thereafter, it will not allocate unless the
392 // screen is resized
393 .winsize => |ws| try vx.resize(alloc, tty.writer(), ws),
394 else => {},
395 }
396
397 // vx.window() returns the root window. This window is the size of the
398 // terminal and can spawn child windows as logical areas. Child windows
399 // cannot draw outside of their bounds
400 const win = vx.window();
401
402 // Clear the entire space because we are drawing in immediate mode.
403 // vaxis double buffers the screen. This new frame will be compared to
404 // the old and only updated cells will be drawn
405 win.clear();
406
407 // Create a style
408 const style: vaxis.Style = .{
409 .fg = .{ .index = color_idx },
410 };
411
412 // Create a bordered child window
413 const child = win.child(.{
414 .x_off = win.width / 2 - 20,
415 .y_off = win.height / 2 - 3,
416 .width = 40 ,
417 .height = 3 ,
418 .border = .{
419 .where = .all,
420 .style = style,
421 },
422 });
423
424 // Draw the text_input in the child window
425 text_input.draw(child);
426
427 // Render the screen. Using a buffered writer will offer much better
428 // performance, but is not required
429 try vx.render(tty.writer());
430 }
431}
432```
433
434## Contributing
435
436Contributions are welcome. Please submit a PR on Github,
437[tangled](https://tangled.sh/@rockorager.dev/libvaxis), or a patch on the
438[mailing list](mailto:~rockorager/libvaxis@lists.sr.ht)
439
440## Community
441
442We use [Github Discussions](https://github.com/rockorager/libvaxis/discussions)
443as the primary location for community support, showcasing what you are working
444on, and discussing library features and usage.
445
446We also have an IRC channel on libera.chat: join us in #vaxis.