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