this repo has no description
13
fork

Configure Feed

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

at main 446 lines 18 kB view raw view rendered
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#### 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.