this repo has no description
13
fork

Configure Feed

Select the types of activity you want to include in your feed.

1# libvaxis 2 3``` 4It begins with them, but ends with me. Their son, Vaxis 5``` 6 7![vaxis demo gif](vaxis.gif) 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.