···99Libvaxis _does not use terminfo_. Support for vt features is detected through
1010terminal queries.
11111212-Contributions are welcome.
1313-1412Vaxis uses zig `0.13.0`.
15131614## Features
···37353836[Documentation](https://rockorager.github.io/libvaxis/#vaxis.Vaxis)
39374040-[Starter repo](https://github.com/rockorager/libvaxis-starter)
3838+The library provides both a low level API suitable for making applications of
3939+any sort as well as a higher level framework. The low level API is suitable for
4040+making applications of any type, providing your own event loop, and gives you
4141+full control over each cell on the screen.
4242+4343+The high level API, called `vxfw` (Vaxis framework), provides a Flutter-like
4444+style of API. The framework provides an application runtime which handles the
4545+event loop, focus management, mouse handling, and more. Several widgets are
4646+provided, and custom widgets are easy to build. This API is most likely what you
4747+want to use for typical TUI applications.
4848+4949+### vxfw (Vaxis framework)
5050+5151+Let's build a simple button counter application. This example can be run using
5252+the command `zig build example -Dexample=counter`. The below application has
5353+full mouse support: the button *and mouse shape* will change style on hover, on
5454+click, and has enough logic to cancel a press if the release does not occur over
5555+the button. Try it! Click the button, move the mouse off the button and release.
5656+All of this logic is baked into the base `Button` widget.
5757+5858+```zig
5959+const std = @import("std");
6060+const vaxis = @import("vaxis");
6161+const vxfw = vaxis.vxfw;
6262+6363+/// Our main application state
6464+const Model = struct {
6565+ /// State of the counter
6666+ count: u32 = 0,
6767+ /// The button. This widget is stateful and must live between frames
6868+ button: vxfw.Button,
6969+7070+ /// Helper function to return a vxfw.Widget struct
7171+ pub fn widget(self: *Model) vxfw.Widget {
7272+ return .{
7373+ .userdata = self,
7474+ .eventHandler = Model.typeErasedEventHandler,
7575+ .drawFn = Model.typeErasedDrawFn,
7676+ };
7777+ }
7878+7979+ /// This function will be called from the vxfw runtime.
8080+ fn typeErasedEventHandler(ptr: *anyopaque, ctx: *vxfw.EventContext, event: vxfw.Event) anyerror!void {
8181+ const self: *Model = @ptrCast(@alignCast(ptr));
8282+ switch (event) {
8383+ // The root widget is always sent an init event as the first event. Users of the
8484+ // library can also send this event to other widgets they create if they need to do
8585+ // some initialization.
8686+ .init => return ctx.requestFocus(self.button.widget()),
8787+ .key_press => |key| {
8888+ if (key.matches('c', .{ .ctrl = true })) {
8989+ ctx.quit = true;
9090+ return;
9191+ }
9292+ },
9393+ // We can request a specific widget gets focus. In this case, we always want to focus
9494+ // our button. Having focus means that key events will be sent up the widget tree to
9595+ // the focused widget, and then bubble back down the tree to the root. Users can tell
9696+ // the runtime the event was handled and the capture or bubble phase will stop
9797+ .focus_in => return ctx.requestFocus(self.button.widget()),
9898+ else => {},
9999+ }
100100+ }
101101+102102+ /// This function is called from the vxfw runtime. It will be called on a regular interval, and
103103+ /// only when any event handler has marked the redraw flag in EventContext as true. By
104104+ /// explicitly requiring setting the redraw flag, vxfw can prevent excessive redraws for events
105105+ /// which don't change state (ie mouse motion, unhandled key events, etc)
106106+ fn typeErasedDrawFn(ptr: *anyopaque, ctx: vxfw.DrawContext) std.mem.Allocator.Error!vxfw.Surface {
107107+ const self: *Model = @ptrCast(@alignCast(ptr));
108108+ // The DrawContext is inspired from Flutter. Each widget will receive a minimum and maximum
109109+ // constraint. The minimum constraint will always be set, even if it is set to 0x0. The
110110+ // maximum constraint can have null width and/or height - meaning there is no constraint in
111111+ // that direction and the widget should take up as much space as it needs. By calling size()
112112+ // on the max, we assert that it has some constrained size. This is *always* the case for
113113+ // the root widget - the maximum size will always be the size of the terminal screen.
114114+ const max_size = ctx.max.size();
115115+116116+ // The DrawContext also contains an arena allocator that can be used for each frame. The
117117+ // lifetime of this allocation is until the next time we draw a frame. This is useful for
118118+ // temporary allocations such as the one below: we have an integer we want to print as text.
119119+ // We can safely allocate this with the ctx arena since we only need it for this frame.
120120+ const count_text = try std.fmt.allocPrint(ctx.arena, "{d}", .{self.count});
121121+ const text: vxfw.Text = .{ .text = count_text };
122122+123123+ // Each widget returns a Surface from it's draw function. A Surface contains the rectangular
124124+ // area of the widget, as well as some information about the surface or widget: can we focus
125125+ // it? does it handle the mouse?
126126+ //
127127+ // It DOES NOT contain the location it should be within it's parent. Only the parent can set
128128+ // this via a SubSurface. Here, we will return a Surface for the root widget (Model), which
129129+ // has two SubSurfaces: one for the text and one for the button. A SubSurface is a Surface
130130+ // with an offset and a z-index - the offset can be negative. This lets a parent draw a
131131+ // child and place it within itself
132132+ const text_child: vxfw.SubSurface = .{
133133+ .origin = .{ .row = 0, .col = 0 },
134134+ .surface = try text.draw(ctx),
135135+ };
136136+137137+ const button_child: vxfw.SubSurface = .{
138138+ .origin = .{ .row = 2, .col = 0 },
139139+ .surface = try self.button.draw(ctx.withConstraints(
140140+ ctx.min,
141141+ // Here we explicitly set a new maximum size constraint for the Button. A Button will
142142+ // expand to fill it's area and must have some hard limit in the maximum constraint
143143+ .{ .width = 16, .height = 3 },
144144+ )),
145145+ };
146146+147147+ // We also can use our arena to allocate the slice for our SubSurfaces. This slice only
148148+ // needs to live until the next frame, making this safe.
149149+ const children = try ctx.arena.alloc(vxfw.SubSurface, 2);
150150+ children[0] = text_child;
151151+ children[1] = button_child;
152152+153153+ return .{
154154+ // A Surface must have a size. Our root widget is the size of the screen
155155+ .size = max_size,
156156+ .widget = self.widget(),
157157+ .focusable = false,
158158+ // We didn't actually need to draw anything for the root. In this case, we can set
159159+ // buffer to a zero length slice. If this slice is *not zero length*, the runtime will
160160+ // assert that it's length is equal to the size.width * size.height.
161161+ .buffer = &.{},
162162+ .children = children,
163163+ };
164164+ }
165165+166166+ /// The onClick callback for our button. This is also called if we press enter while the button
167167+ /// has focus
168168+ fn onClick(maybe_ptr: ?*anyopaque, ctx: *vxfw.EventContext) anyerror!void {
169169+ const ptr = maybe_ptr orelse return;
170170+ const self: *Model = @ptrCast(@alignCast(ptr));
171171+ self.count +|= 1;
172172+ return ctx.consumeAndRedraw();
173173+ }
174174+};
175175+176176+pub fn main() !void {
177177+ var gpa = std.heap.GeneralPurposeAllocator(.{}){};
178178+ defer _ = gpa.deinit();
179179+180180+ const allocator = gpa.allocator();
181181+182182+ var app = try vxfw.App.init(allocator);
183183+ defer app.deinit();
184184+185185+ // We heap allocate our model because we will require a stable pointer to it in our Button
186186+ // widget
187187+ const model = try allocator.create(Model);
188188+ defer allocator.destroy(model);
189189+190190+ // Set the initial state of our button
191191+ model.* = .{
192192+ .count = 0,
193193+ .button = .{
194194+ .label = "Click me!",
195195+ .onClick = Model.onClick,
196196+ .userdata = model,
197197+ },
198198+ };
199199+200200+ try app.run(model.widget(), .{});
201201+}
202202+```
203203+204204+### Low level API
4120542206Vaxis requires three basic primitives to operate:
43207···55219also handle these query responses and update the Vaxis.caps struct accordingly.
56220See the `Loop` implementation to see how this is done if writing your own event
57221loop.
5858-5959-## Example
6022261223```zig
62224const std = @import("std");
···11+const std = @import("std");
22+const vaxis = @import("vaxis");
33+const vxfw = vaxis.vxfw;
44+55+/// Our main application state
66+const Model = struct {
77+ /// State of the counter
88+ count: u32 = 0,
99+ /// The button. This widget is stateful and must live between frames
1010+ button: vxfw.Button,
1111+1212+ /// Helper function to return a vxfw.Widget struct
1313+ pub fn widget(self: *Model) vxfw.Widget {
1414+ return .{
1515+ .userdata = self,
1616+ .eventHandler = Model.typeErasedEventHandler,
1717+ .drawFn = Model.typeErasedDrawFn,
1818+ };
1919+ }
2020+2121+ /// This function will be called from the vxfw runtime.
2222+ fn typeErasedEventHandler(ptr: *anyopaque, ctx: *vxfw.EventContext, event: vxfw.Event) anyerror!void {
2323+ const self: *Model = @ptrCast(@alignCast(ptr));
2424+ switch (event) {
2525+ // The root widget is always sent an init event as the first event. Users of the
2626+ // library can also send this event to other widgets they create if they need to do
2727+ // some initialization.
2828+ .init => return ctx.requestFocus(self.button.widget()),
2929+ .key_press => |key| {
3030+ if (key.matches('c', .{ .ctrl = true })) {
3131+ ctx.quit = true;
3232+ return;
3333+ }
3434+ },
3535+ // We can request a specific widget gets focus. In this case, we always want to focus
3636+ // our button. Having focus means that key events will be sent up the widget tree to
3737+ // the focused widget, and then bubble back down the tree to the root. Users can tell
3838+ // the runtime the event was handled and the capture or bubble phase will stop
3939+ .focus_in => return ctx.requestFocus(self.button.widget()),
4040+ else => {},
4141+ }
4242+ }
4343+4444+ /// This function is called from the vxfw runtime. It will be called on a regular interval, and
4545+ /// only when any event handler has marked the redraw flag in EventContext as true. By
4646+ /// explicitly requiring setting the redraw flag, vxfw can prevent excessive redraws for events
4747+ /// which don't change state (ie mouse motion, unhandled key events, etc)
4848+ fn typeErasedDrawFn(ptr: *anyopaque, ctx: vxfw.DrawContext) std.mem.Allocator.Error!vxfw.Surface {
4949+ const self: *Model = @ptrCast(@alignCast(ptr));
5050+ // The DrawContext is inspired from Flutter. Each widget will receive a minimum and maximum
5151+ // constraint. The minimum constraint will always be set, even if it is set to 0x0. The
5252+ // maximum constraint can have null width and/or height - meaning there is no constraint in
5353+ // that direction and the widget should take up as much space as it needs. By calling size()
5454+ // on the max, we assert that it has some constrained size. This is *always* the case for
5555+ // the root widget - the maximum size will always be the size of the terminal screen.
5656+ const max_size = ctx.max.size();
5757+5858+ // The DrawContext also contains an arena allocator that can be used for each frame. The
5959+ // lifetime of this allocation is until the next time we draw a frame. This is useful for
6060+ // temporary allocations such as the one below: we have an integer we want to print as text.
6161+ // We can safely allocate this with the ctx arena since we only need it for this frame.
6262+ if (self.count > 0) {
6363+ self.button.label = try std.fmt.allocPrint(ctx.arena, "Clicks: {d}", .{self.count});
6464+ } else {
6565+ self.button.label = "Click me!";
6666+ }
6767+6868+ // Each widget returns a Surface from it's draw function. A Surface contains the rectangular
6969+ // area of the widget, as well as some information about the surface or widget: can we focus
7070+ // it? does it handle the mouse?
7171+ //
7272+ // It DOES NOT contain the location it should be within it's parent. Only the parent can set
7373+ // this via a SubSurface. Here, we will return a Surface for the root widget (Model), which
7474+ // has two SubSurfaces: one for the text and one for the button. A SubSurface is a Surface
7575+ // with an offset and a z-index - the offset can be negative. This lets a parent draw a
7676+ // child and place it within itself
7777+ const button_child: vxfw.SubSurface = .{
7878+ .origin = .{ .row = 0, .col = 0 },
7979+ .surface = try self.button.draw(ctx.withConstraints(
8080+ ctx.min,
8181+ // Here we explicitly set a new maximum size constraint for the Button. A Button will
8282+ // expand to fill it's area and must have some hard limit in the maximum constraint
8383+ .{ .width = 16, .height = 3 },
8484+ )),
8585+ };
8686+8787+ // We also can use our arena to allocate the slice for our SubSurfaces. This slice only
8888+ // needs to live until the next frame, making this safe.
8989+ const children = try ctx.arena.alloc(vxfw.SubSurface, 1);
9090+ children[0] = button_child;
9191+9292+ return .{
9393+ // A Surface must have a size. Our root widget is the size of the screen
9494+ .size = max_size,
9595+ .widget = self.widget(),
9696+ .focusable = false,
9797+ // We didn't actually need to draw anything for the root. In this case, we can set
9898+ // buffer to a zero length slice. If this slice is *not zero length*, the runtime will
9999+ // assert that it's length is equal to the size.width * size.height.
100100+ .buffer = &.{},
101101+ .children = children,
102102+ };
103103+ }
104104+105105+ /// The onClick callback for our button. This is also called if we press enter while the button
106106+ /// has focus
107107+ fn onClick(maybe_ptr: ?*anyopaque, ctx: *vxfw.EventContext) anyerror!void {
108108+ const ptr = maybe_ptr orelse return;
109109+ const self: *Model = @ptrCast(@alignCast(ptr));
110110+ self.count +|= 1;
111111+ return ctx.consumeAndRedraw();
112112+ }
113113+};
114114+115115+pub fn main() !void {
116116+ var gpa = std.heap.GeneralPurposeAllocator(.{}){};
117117+ defer _ = gpa.deinit();
118118+119119+ const allocator = gpa.allocator();
120120+121121+ var app = try vxfw.App.init(allocator);
122122+ defer app.deinit();
123123+124124+ // We heap allocate our model because we will require a stable pointer to it in our Button
125125+ // widget
126126+ const model = try allocator.create(Model);
127127+ defer allocator.destroy(model);
128128+129129+ // Set the initial state of our button
130130+ model.* = .{
131131+ .count = 0,
132132+ .button = .{
133133+ .label = "Click me!",
134134+ .onClick = Model.onClick,
135135+ .userdata = model,
136136+ },
137137+ };
138138+139139+ try app.run(model.widget(), .{});
140140+}