a modern tui library written in zig
at main 16 kB view raw
1const std = @import("std"); 2const fmt = std.fmt; 3const heap = std.heap; 4const mem = std.mem; 5const meta = std.meta; 6 7const vaxis = @import("vaxis"); 8 9const log = std.log.scoped(.main); 10 11const ActiveSection = enum { 12 top, 13 mid, 14 btm, 15}; 16 17pub fn main() !void { 18 var gpa = heap.GeneralPurposeAllocator(.{}){}; 19 defer if (gpa.detectLeaks()) log.err("Memory leak detected!", .{}); 20 const alloc = gpa.allocator(); 21 22 // Users set up below the main function 23 const users_buf = try alloc.dupe(User, users[0..]); 24 25 var buffer: [1024]u8 = undefined; 26 var tty = try vaxis.Tty.init(&buffer); 27 defer tty.deinit(); 28 const tty_writer = tty.writer(); 29 var vx = try vaxis.init(alloc, .{ 30 .kitty_keyboard_flags = .{ .report_events = true }, 31 }); 32 defer vx.deinit(alloc, tty.writer()); 33 34 var loop: vaxis.Loop(union(enum) { 35 key_press: vaxis.Key, 36 winsize: vaxis.Winsize, 37 table_upd, 38 }) = .{ .tty = &tty, .vaxis = &vx }; 39 try loop.init(); 40 try loop.start(); 41 defer loop.stop(); 42 try vx.enterAltScreen(tty.writer()); 43 try vx.queryTerminal(tty.writer(), 250 * std.time.ns_per_ms); 44 45 const logo = 46 \\░█░█░█▀█░█░█░▀█▀░█▀▀░░░▀█▀░█▀█░█▀▄░█░░░█▀▀░ 47 \\░▀▄▀░█▀█░▄▀▄░░█░░▀▀█░░░░█░░█▀█░█▀▄░█░░░█▀▀░ 48 \\░░▀░░▀░▀░▀░▀░▀▀▀░▀▀▀░░░░▀░░▀░▀░▀▀░░▀▀▀░▀▀▀░ 49 ; 50 const title_logo = vaxis.Cell.Segment{ 51 .text = logo, 52 .style = .{}, 53 }; 54 const title_info = vaxis.Cell.Segment{ 55 .text = "===A Demo of the the Vaxis Table Widget!===", 56 .style = .{}, 57 }; 58 const title_disclaimer = vaxis.Cell.Segment{ 59 .text = "(All data is non-sensical & LLM generated.)", 60 .style = .{}, 61 }; 62 var title_segs = [_]vaxis.Cell.Segment{ title_logo, title_info, title_disclaimer }; 63 64 var cmd_input = vaxis.widgets.TextInput.init(alloc); 65 defer cmd_input.deinit(); 66 67 // Colors 68 const active_bg: vaxis.Cell.Color = .{ .rgb = .{ 64, 128, 255 } }; 69 const selected_bg: vaxis.Cell.Color = .{ .rgb = .{ 32, 64, 255 } }; 70 const other_bg: vaxis.Cell.Color = .{ .rgb = .{ 32, 32, 48 } }; 71 72 // Table Context 73 var demo_tbl: vaxis.widgets.Table.TableContext = .{ 74 .active_bg = active_bg, 75 .active_fg = .{ .rgb = .{ 0, 0, 0 } }, 76 .row_bg_1 = .{ .rgb = .{ 8, 8, 8 } }, 77 .selected_bg = selected_bg, 78 .header_names = .{ .custom = &.{ "First", "Last", "Username", "Phone#", "Email" } }, 79 //.header_align = .left, 80 .col_indexes = .{ .by_idx = &.{ 0, 1, 2, 4, 3 } }, 81 //.col_align = .{ .by_idx = &.{ .left, .left, .center, .center, .left } }, 82 //.col_align = .{ .all = .center }, 83 //.header_borders = true, 84 //.col_borders = true, 85 //.col_width = .{ .static_all = 15 }, 86 //.col_width = .{ .dynamic_header_len = 3 }, 87 //.col_width = .{ .static_individual = &.{ 10, 20, 15, 25, 15 } }, 88 //.col_width = .dynamic_fill, 89 //.y_off = 10, 90 }; 91 defer if (demo_tbl.sel_rows) |rows| alloc.free(rows); 92 93 // TUI State 94 var active: ActiveSection = .mid; 95 var moving = false; 96 var see_content = false; 97 98 // Create an Arena Allocator for easy allocations on each Event. 99 var event_arena = heap.ArenaAllocator.init(alloc); 100 defer event_arena.deinit(); 101 while (true) { 102 defer _ = event_arena.reset(.retain_capacity); 103 defer tty_writer.flush() catch {}; 104 const event_alloc = event_arena.allocator(); 105 const event = loop.nextEvent(); 106 107 switch (event) { 108 .key_press => |key| keyEvt: { 109 // Close the Program 110 if (key.matches('c', .{ .ctrl = true })) { 111 break; 112 } 113 // Refresh the Screen 114 if (key.matches('l', .{ .ctrl = true })) { 115 vx.queueRefresh(); 116 break :keyEvt; 117 } 118 // Enter Moving State 119 if (key.matches('w', .{ .ctrl = true })) { 120 moving = !moving; 121 break :keyEvt; 122 } 123 // Command State 124 if (active != .btm and 125 key.matchesAny(&.{ ':', '/', 'g', 'G' }, .{})) 126 { 127 active = .btm; 128 cmd_input.clearAndFree(); 129 try cmd_input.update(.{ .key_press = key }); 130 break :keyEvt; 131 } 132 133 switch (active) { 134 .top => { 135 if (key.matchesAny(&.{ vaxis.Key.down, 'j' }, .{}) and moving) active = .mid; 136 }, 137 .mid => midEvt: { 138 if (moving) { 139 if (key.matchesAny(&.{ vaxis.Key.up, 'k' }, .{})) active = .top; 140 if (key.matchesAny(&.{ vaxis.Key.down, 'j' }, .{})) active = .btm; 141 break :midEvt; 142 } 143 // Change Row 144 if (key.matchesAny(&.{ vaxis.Key.up, 'k' }, .{})) demo_tbl.row -|= 1; 145 if (key.matchesAny(&.{ vaxis.Key.down, 'j' }, .{})) demo_tbl.row +|= 1; 146 // Change Column 147 if (key.matchesAny(&.{ vaxis.Key.left, 'h' }, .{})) demo_tbl.col -|= 1; 148 if (key.matchesAny(&.{ vaxis.Key.right, 'l' }, .{})) demo_tbl.col +|= 1; 149 // Select/Unselect Row 150 if (key.matches(vaxis.Key.space, .{})) { 151 const rows = demo_tbl.sel_rows orelse createRows: { 152 demo_tbl.sel_rows = try alloc.alloc(u16, 1); 153 break :createRows demo_tbl.sel_rows.?; 154 }; 155 var rows_list = std.ArrayList(u16).fromOwnedSlice(rows); 156 for (rows_list.items, 0..) |row, idx| { 157 if (row != demo_tbl.row) continue; 158 _ = rows_list.orderedRemove(idx); 159 break; 160 } else try rows_list.append(alloc, demo_tbl.row); 161 demo_tbl.sel_rows = try rows_list.toOwnedSlice(alloc); 162 } 163 // See Row Content 164 if (key.matches(vaxis.Key.enter, .{}) or key.matches('j', .{ .ctrl = true })) see_content = !see_content; 165 }, 166 .btm => { 167 if (key.matchesAny(&.{ vaxis.Key.up, 'k' }, .{}) and moving) active = .mid 168 // Run Command and Clear Command Bar 169 else if (key.matchExact(vaxis.Key.enter, .{}) or key.matchExact('j', .{ .ctrl = true })) { 170 const cmd = try cmd_input.toOwnedSlice(); 171 defer alloc.free(cmd); 172 if (mem.eql(u8, ":q", cmd) or 173 mem.eql(u8, ":quit", cmd) or 174 mem.eql(u8, ":exit", cmd)) return; 175 if (mem.eql(u8, "G", cmd)) { 176 demo_tbl.row = @intCast(users_buf.len - 1); 177 active = .mid; 178 } 179 if (cmd.len >= 2 and mem.eql(u8, "gg", cmd[0..2])) { 180 const goto_row = fmt.parseInt(u16, cmd[2..], 0) catch 0; 181 demo_tbl.row = goto_row; 182 active = .mid; 183 } 184 } else try cmd_input.update(.{ .key_press = key }); 185 }, 186 } 187 moving = false; 188 }, 189 .winsize => |ws| try vx.resize(alloc, tty.writer(), ws), 190 else => {}, 191 } 192 193 // Content 194 seeRow: { 195 if (!see_content) { 196 demo_tbl.active_content_fn = null; 197 demo_tbl.active_ctx = &{}; 198 break :seeRow; 199 } 200 const RowContext = struct { 201 row: []const u8, 202 bg: vaxis.Color, 203 }; 204 const row_ctx = RowContext{ 205 .row = try fmt.allocPrint(event_alloc, "Row #: {d}", .{demo_tbl.row}), 206 .bg = demo_tbl.active_bg, 207 }; 208 demo_tbl.active_ctx = &row_ctx; 209 demo_tbl.active_content_fn = struct { 210 fn see(win: *vaxis.Window, ctx_raw: *const anyopaque) !u16 { 211 const ctx: *const RowContext = @ptrCast(@alignCast(ctx_raw)); 212 win.height = 5; 213 const see_win = win.child(.{ 214 .x_off = 0, 215 .y_off = 1, 216 .width = win.width, 217 .height = 4, 218 }); 219 see_win.fill(.{ .style = .{ .bg = ctx.bg } }); 220 const content_logo = 221 \\ 222 \\░█▀▄░█▀█░█░█░░░█▀▀░█▀█░█▀█░▀█▀░█▀▀░█▀█░▀█▀ 223 \\░█▀▄░█░█░█▄█░░░█░░░█░█░█░█░░█░░█▀▀░█░█░░█░ 224 \\░▀░▀░▀▀▀░▀░▀░░░▀▀▀░▀▀▀░▀░▀░░▀░░▀▀▀░▀░▀░░▀░ 225 ; 226 const content_segs: []const vaxis.Cell.Segment = &.{ 227 .{ 228 .text = ctx.row, 229 .style = .{ .bg = ctx.bg }, 230 }, 231 .{ 232 .text = content_logo, 233 .style = .{ .bg = ctx.bg }, 234 }, 235 }; 236 _ = see_win.print(content_segs, .{}); 237 return see_win.height; 238 } 239 }.see; 240 loop.postEvent(.table_upd); 241 } 242 243 // Sections 244 // - Window 245 const win = vx.window(); 246 win.clear(); 247 248 // - Top 249 const top_div = 6; 250 const top_bar = win.child(.{ 251 .x_off = 0, 252 .y_off = 0, 253 .width = win.width, 254 .height = win.height / top_div, 255 }); 256 for (title_segs[0..]) |*title_seg| 257 title_seg.style.bg = if (active == .top) selected_bg else other_bg; 258 top_bar.fill(.{ .style = .{ 259 .bg = if (active == .top) selected_bg else other_bg, 260 } }); 261 const logo_bar = vaxis.widgets.alignment.center( 262 top_bar, 263 44, 264 top_bar.height - (top_bar.height / 3), 265 ); 266 _ = logo_bar.print(title_segs[0..], .{ .wrap = .word }); 267 268 // - Middle 269 const middle_bar = win.child(.{ 270 .x_off = 0, 271 .y_off = win.height / top_div, 272 .width = win.width, 273 .height = win.height - (top_bar.height + 1), 274 }); 275 if (users_buf.len > 0) { 276 demo_tbl.active = active == .mid; 277 try vaxis.widgets.Table.drawTable( 278 null, 279 // event_alloc, 280 middle_bar, 281 //users_buf[0..], 282 //user_list, 283 users_buf, 284 &demo_tbl, 285 ); 286 } 287 288 // - Bottom 289 const bottom_bar = win.child(.{ 290 .x_off = 0, 291 .y_off = win.height - 1, 292 .width = win.width, 293 .height = 1, 294 }); 295 if (active == .btm) bottom_bar.fill(.{ .style = .{ .bg = active_bg } }); 296 cmd_input.draw(bottom_bar); 297 298 // Render the screen 299 try vx.render(tty_writer); 300 } 301} 302 303/// User Struct 304pub const User = struct { 305 first: []const u8, 306 last: []const u8, 307 user: []const u8, 308 email: ?[]const u8 = null, 309 phone: ?[]const u8 = null, 310}; 311 312// Users Array 313const users = [_]User{ 314 .{ .first = "Nancy", .last = "Dudley", .user = "angela73", .email = "brian47@rodriguez.biz", .phone = null }, 315 .{ .first = "Emily", .last = "Thornton", .user = "mrogers", .email = null, .phone = "(558)888-8604x094" }, 316 .{ .first = "Kyle", .last = "Huff", .user = "xsmith", .email = null, .phone = "301.127.0801x12398" }, 317 .{ .first = "Christine", .last = "Dodson", .user = "amandabradley", .email = "cheryl21@sullivan.com", .phone = null }, 318 .{ .first = "Nathaniel", .last = "Kennedy", .user = "nrobinson", .email = null, .phone = null }, 319 .{ .first = "Laura", .last = "Leon", .user = "dawnjones", .email = "fjenkins@patel.com", .phone = "1833013180" }, 320 .{ .first = "Patrick", .last = "Landry", .user = "michaelhutchinson", .email = "daniel17@medina-wallace.net", .phone = "+1-634-486-6444x964" }, 321 .{ .first = "Tammy", .last = "Hall", .user = "jamessmith", .email = null, .phone = "(926)810-3385x22059" }, 322 .{ .first = "Stephanie", .last = "Anderson", .user = "wgillespie", .email = "campbelljaime@yahoo.com", .phone = null }, 323 .{ .first = "Jennifer", .last = "Williams", .user = "shawn60", .email = null, .phone = "611-385-4771x97523" }, 324 .{ .first = "Elizabeth", .last = "Ortiz", .user = "jennifer76", .email = "johnbradley@delgado.info", .phone = null }, 325 .{ .first = "Stacy", .last = "Mays", .user = "scottgonzalez", .email = "kramermatthew@gmail.com", .phone = null }, 326 .{ .first = "Jennifer", .last = "Smith", .user = "joseph75", .email = "masseyalexander@hill-moore.net", .phone = null }, 327 .{ .first = "Gary", .last = "Hammond", .user = "brittany26", .email = null, .phone = null }, 328 .{ .first = "Lisa", .last = "Johnson", .user = "tina28", .email = null, .phone = "850-606-2978x1081" }, 329 .{ .first = "Zachary", .last = "Hopkins", .user = "vargasmichael", .email = null, .phone = null }, 330 .{ .first = "Joshua", .last = "Kidd", .user = "ghanna", .email = "jbrown@yahoo.com", .phone = null }, 331 .{ .first = "Dawn", .last = "Jones", .user = "alisonlindsey", .email = null, .phone = null }, 332 .{ .first = "Monica", .last = "Berry", .user = "barbara40", .email = "michael00@hotmail.com", .phone = "(295)346-6453x343" }, 333 .{ .first = "Shannon", .last = "Roberts", .user = "krystal37", .email = null, .phone = "980-920-9386x454" }, 334 .{ .first = "Thomas", .last = "Mitchell", .user = "williamscorey", .email = "richardduncan@roberts.com", .phone = null }, 335 .{ .first = "Nicole", .last = "Shaffer", .user = "rogerstroy", .email = null, .phone = "(570)128-5662" }, 336 .{ .first = "Edward", .last = "Bennett", .user = "andersonchristina", .email = null, .phone = null }, 337 .{ .first = "Duane", .last = "Howard", .user = "pcarpenter", .email = "griffithwayne@parker.net", .phone = null }, 338 .{ .first = "Mary", .last = "Brown", .user = "kimberlyfrost", .email = "perezsara@anderson-andrews.net", .phone = null }, 339 .{ .first = "Pamela", .last = "Sloan", .user = "kvelez", .email = "huynhlacey@moore-bell.biz", .phone = "001-359-125-1393x8716" }, 340 .{ .first = "Timothy", .last = "Charles", .user = "anthony04", .email = "morrissara@hawkins.info", .phone = "+1-619-369-9572" }, 341 .{ .first = "Sydney", .last = "Torres", .user = "scott42", .email = "asnyder@mitchell.net", .phone = null }, 342 .{ .first = "John", .last = "Jones", .user = "anthonymoore", .email = null, .phone = "701.236.0571x99622" }, 343 .{ .first = "Erik", .last = "Johnson", .user = "allisonsanders", .email = null, .phone = null }, 344 .{ .first = "Donna", .last = "Kirk", .user = "laurie81", .email = null, .phone = null }, 345 .{ .first = "Karina", .last = "White", .user = "uperez", .email = null, .phone = null }, 346 .{ .first = "Jesse", .last = "Schwartz", .user = "ryan60", .email = "latoyawilliams@gmail.com", .phone = null }, 347 .{ .first = "Cindy", .last = "Romero", .user = "christopher78", .email = "faulknerchristina@gmail.com", .phone = "780.288.2319x583" }, 348 .{ .first = "Tyler", .last = "Sanders", .user = "bennettjessica", .email = null, .phone = "1966269423" }, 349 .{ .first = "Pamela", .last = "Carter", .user = "zsnyder", .email = null, .phone = "125-062-9130x58413" }, 350};