this repo has no description
13
fork

Configure Feed

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

at b58ae3a2fa16b3b7f11d2a7297c73a1e839d035b 411 lines 18 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("../main.zig"); 8 9/// Table Context for maintaining state and drawing Tables with `drawTable()`. 10pub const TableContext = struct { 11 /// Current active Row of the Table. 12 row: u16 = 0, 13 /// Current active Column of the Table. 14 col: u16 = 0, 15 /// Starting point within the Data List. 16 start: u16 = 0, 17 /// Selected Rows. 18 sel_rows: ?[]u16 = null, 19 20 /// Active status of the Table. 21 active: bool = false, 22 /// Active Content Callback Function. 23 /// If available, this will be called to vertically expand the active row with additional info. 24 active_content_fn: ?*const fn (*vaxis.Window, *const anyopaque) anyerror!u16 = null, 25 /// Active Content Context 26 /// This will be provided to the `active_content` callback when called. 27 active_ctx: *const anyopaque = &{}, 28 /// Y Offset for rows beyond the Active Content. 29 /// (This will be calculated automatically) 30 active_y_off: u16 = 0, 31 32 /// The Background Color for Selected Rows. 33 selected_bg: vaxis.Cell.Color, 34 /// The Foreground Color for Selected Rows. 35 selected_fg: vaxis.Cell.Color = .default, 36 /// The Background Color for the Active Row and Column Header. 37 active_bg: vaxis.Cell.Color, 38 /// The Foreground Color for the Active Row and Column Header. 39 active_fg: vaxis.Cell.Color = .default, 40 /// First Column Header Background Color 41 hdr_bg_1: vaxis.Cell.Color = .{ .rgb = [_]u8{ 64, 64, 64 } }, 42 /// Second Column Header Background Color 43 hdr_bg_2: vaxis.Cell.Color = .{ .rgb = [_]u8{ 8, 8, 24 } }, 44 /// First Row Background Color 45 row_bg_1: vaxis.Cell.Color = .{ .rgb = [_]u8{ 32, 32, 32 } }, 46 /// Second Row Background Color 47 row_bg_2: vaxis.Cell.Color = .{ .rgb = [_]u8{ 8, 8, 8 } }, 48 49 /// Y Offset for drawing to the parent Window. 50 y_off: u16 = 0, 51 /// X Offset for printing each Cell/Item. 52 cell_x_off: u16 = 1, 53 54 /// Column Width 55 /// Note, if this is left `null` the Column Width will be dynamically calculated during `drawTable()`. 56 //col_width: ?usize = null, 57 col_width: WidthStyle = .dynamic_fill, 58 59 // Header Names 60 header_names: HeaderNames = .field_names, 61 // Column Indexes 62 col_indexes: ColumnIndexes = .all, 63 // Header Alignment 64 header_align: HorizontalAlignment = .center, 65 // Column Alignment 66 col_align: ColumnAlignment = .{ .all = .left }, 67 68 // Header Borders 69 header_borders: bool = false, 70 // Row Borders 71 //row_borders: bool = false, 72 // Col Borders 73 col_borders: bool = false, 74}; 75 76/// Width Styles for `col_width`. 77pub const WidthStyle = union(enum) { 78 /// Dynamically calculate Column Widths such that the entire (or most) of the screen is filled horizontally. 79 dynamic_fill, 80 /// Dynamically calculate the Column Width for each Column based on its Header Length and the provided Padding length. 81 dynamic_header_len: u16, 82 /// Statically set all Column Widths to the same value. 83 static_all: u16, 84 /// Statically set individual Column Widths to specific values. 85 static_individual: []const u16, 86}; 87 88/// Column Indexes 89pub const ColumnIndexes = union(enum) { 90 /// Use all of the Columns. 91 all, 92 /// Use Columns from the specified indexes. 93 by_idx: []const usize, 94}; 95 96/// Header Names 97pub const HeaderNames = union(enum) { 98 /// Use Field Names as Headers 99 field_names, 100 /// Custom 101 custom: []const []const u8, 102}; 103 104/// Horizontal Alignment 105pub const HorizontalAlignment = enum { 106 left, 107 center, 108}; 109/// Column Alignment 110pub const ColumnAlignment = union(enum) { 111 all: HorizontalAlignment, 112 by_idx: []const HorizontalAlignment, 113}; 114 115/// Draw a Table for the TUI. 116pub fn drawTable( 117 /// This should be an ArenaAllocator that can be deinitialized after each event call. 118 /// The Allocator is only used in three cases: 119 /// 1. If a cell is a non-String. (If the Allocator is not provided, those cells will show "[unsupported (TypeName)]".) 120 /// 2. To show that a value is too large to fit into a cell using '...'. (If the Allocator is not provided, they'll just be cutoff.) 121 /// 3. To copy a MultiArrayList into a normal slice. (Note, this is an expensive operation. Prefer to pass a Slice or ArrayList if possible.) 122 alloc: ?mem.Allocator, 123 /// The parent Window to draw to. 124 win: vaxis.Window, 125 /// This must be a Slice, ArrayList, or MultiArrayList. 126 /// Note, MultiArrayList support currently requires allocation. 127 data_list: anytype, 128 // The Table Context for this Table. 129 table_ctx: *TableContext, 130) !void { 131 var di_is_mal = false; 132 const data_items = getData: { 133 const DataListT = @TypeOf(data_list); 134 const data_ti = @typeInfo(DataListT); 135 switch (data_ti) { 136 .pointer => |ptr| { 137 if (ptr.size != .slice) return error.UnsupportedTableDataType; 138 break :getData data_list; 139 }, 140 .@"struct" => { 141 const di_fields = meta.fields(DataListT); 142 const al_fields = meta.fields(std.ArrayList([]const u8)); 143 const mal_fields = meta.fields(std.MultiArrayList(struct { a: u8 = 0, b: u32 = 0 })); 144 // Probably an ArrayList 145 const is_al = comptime if (mem.indexOf(u8, @typeName(DataListT), "MultiArrayList") == null and 146 mem.indexOf(u8, @typeName(DataListT), "ArrayList") != null and 147 al_fields.len == di_fields.len) 148 isAL: { 149 var is = true; 150 for (al_fields, di_fields) |al_field, di_field| 151 is = is and mem.eql(u8, al_field.name, di_field.name); 152 break :isAL is; 153 } else false; 154 if (is_al) break :getData data_list.items; 155 156 // Probably a MultiArrayList 157 const is_mal = if (mem.indexOf(u8, @typeName(DataListT), "MultiArrayList") != null and 158 mal_fields.len == di_fields.len) 159 isMAL: { 160 var is = true; 161 inline for (mal_fields, di_fields) |mal_field, di_field| 162 is = is and mem.eql(u8, mal_field.name, di_field.name); 163 break :isMAL is; 164 } else false; 165 if (!is_mal) return error.UnsupportedTableDataType; 166 if (alloc) |_alloc| { 167 di_is_mal = true; 168 const mal_slice = data_list.slice(); 169 const DataT = dataType: { 170 const fn_info = @typeInfo(@TypeOf(@field(@TypeOf(mal_slice), "get"))); 171 break :dataType fn_info.@"fn".return_type orelse @panic("No Child Type"); 172 }; 173 var data_out_list = std.ArrayList(DataT){}; 174 for (0..mal_slice.len) |idx| try data_out_list.append(_alloc, mal_slice.get(idx)); 175 break :getData try data_out_list.toOwnedSlice(_alloc); 176 } 177 return error.UnsupportedTableDataType; 178 }, 179 else => return error.UnsupportedTableDataType, 180 } 181 }; 182 defer if (di_is_mal) alloc.?.free(data_items); 183 const DataT = @TypeOf(data_items[0]); 184 const fields = meta.fields(DataT); 185 const field_indexes = switch (table_ctx.col_indexes) { 186 .all => comptime allIdx: { 187 var indexes_buf: [fields.len]usize = undefined; 188 for (0..fields.len) |idx| indexes_buf[idx] = idx; 189 const indexes = indexes_buf; 190 break :allIdx indexes[0..]; 191 }, 192 .by_idx => |by_idx| by_idx, 193 }; 194 195 // Headers for the Table 196 var hdrs_buf: [fields.len][]const u8 = undefined; 197 const headers = hdrs: { 198 switch (table_ctx.header_names) { 199 .field_names => { 200 for (field_indexes) |f_idx| { 201 inline for (fields, 0..) |field, idx| { 202 if (f_idx == idx) 203 hdrs_buf[idx] = field.name; 204 } 205 } 206 break :hdrs hdrs_buf[0..]; 207 }, 208 .custom => |hdrs| break :hdrs hdrs, 209 } 210 }; 211 212 const table_win = win.child(.{ 213 .y_off = table_ctx.y_off, 214 .width = win.width, 215 .height = win.height, 216 }); 217 218 // Headers 219 if (table_ctx.col > headers.len - 1) table_ctx.col = @intCast(headers.len - 1); 220 var col_start: u16 = 0; 221 for (headers[0..], 0..) |hdr_txt, idx| { 222 const col_width = try calcColWidth( 223 @intCast(idx), 224 headers, 225 table_ctx.col_width, 226 table_win, 227 ); 228 defer col_start += col_width; 229 const hdr_fg, const hdr_bg = hdrColors: { 230 if (table_ctx.active and idx == table_ctx.col) 231 break :hdrColors .{ table_ctx.active_fg, table_ctx.active_bg } 232 else if (idx % 2 == 0) 233 break :hdrColors .{ .default, table_ctx.hdr_bg_1 } 234 else 235 break :hdrColors .{ .default, table_ctx.hdr_bg_2 }; 236 }; 237 const hdr_win = table_win.child(.{ 238 .x_off = col_start, 239 .y_off = 0, 240 .width = col_width, 241 .height = 1, 242 .border = .{ .where = if (table_ctx.header_borders and idx > 0) .left else .none }, 243 }); 244 var hdr = switch (table_ctx.header_align) { 245 .left => hdr_win, 246 .center => vaxis.widgets.alignment.center(hdr_win, @min(col_width -| 1, hdr_txt.len +| 1), 1), 247 }; 248 hdr_win.fill(.{ .style = .{ .bg = hdr_bg } }); 249 var seg = [_]vaxis.Cell.Segment{.{ 250 .text = if (hdr_txt.len > col_width and alloc != null) try fmt.allocPrint(alloc.?, "{s}...", .{hdr_txt[0..(col_width -| 4)]}) else hdr_txt, 251 .style = .{ 252 .fg = hdr_fg, 253 .bg = hdr_bg, 254 .bold = true, 255 .ul_style = if (idx == table_ctx.col) .single else .dotted, 256 }, 257 }}; 258 _ = hdr.print(seg[0..], .{ .wrap = .word }); 259 } 260 261 // Rows 262 if (table_ctx.active_content_fn == null) table_ctx.active_y_off = 0; 263 const max_items: u16 = 264 if (data_items.len > table_win.height -| 1) table_win.height -| 1 else @intCast(data_items.len); 265 var end = table_ctx.start + max_items; 266 if (table_ctx.row + table_ctx.active_y_off >= win.height -| 2) 267 end -|= table_ctx.active_y_off; 268 if (end > data_items.len) end = @intCast(data_items.len); 269 table_ctx.start = tableStart: { 270 if (table_ctx.row == 0) 271 break :tableStart 0; 272 if (table_ctx.row < table_ctx.start) 273 break :tableStart table_ctx.start - (table_ctx.start - table_ctx.row); 274 if (table_ctx.row >= data_items.len - 1) 275 table_ctx.row = @intCast(data_items.len - 1); 276 if (table_ctx.row >= end) 277 break :tableStart table_ctx.start + (table_ctx.row - end + 1); 278 break :tableStart table_ctx.start; 279 }; 280 end = table_ctx.start + max_items; 281 if (table_ctx.row + table_ctx.active_y_off >= win.height -| 2) 282 end -|= table_ctx.active_y_off; 283 if (end > data_items.len) end = @intCast(data_items.len); 284 table_ctx.start = @min(table_ctx.start, end); 285 table_ctx.active_y_off = 0; 286 for (data_items[table_ctx.start..end], 0..) |data, row| { 287 const row_fg, const row_bg = rowColors: { 288 if (table_ctx.active and table_ctx.start + row == table_ctx.row) 289 break :rowColors .{ table_ctx.active_fg, table_ctx.active_bg }; 290 if (table_ctx.sel_rows) |rows| { 291 if (mem.indexOfScalar(u16, rows, @intCast(table_ctx.start + row)) != null) 292 break :rowColors .{ table_ctx.selected_fg, table_ctx.selected_bg }; 293 } 294 if (row % 2 == 0) break :rowColors .{ .default, table_ctx.row_bg_1 }; 295 break :rowColors .{ .default, table_ctx.row_bg_2 }; 296 }; 297 var row_win = table_win.child(.{ 298 .x_off = 0, 299 .y_off = @intCast(1 + row + table_ctx.active_y_off), 300 .width = table_win.width, 301 .height = 1, 302 //.border = .{ .where = if (table_ctx.row_borders) .top else .none }, 303 }); 304 if (table_ctx.start + row == table_ctx.row) { 305 table_ctx.active_y_off = if (table_ctx.active_content_fn) |content| try content(&row_win, table_ctx.active_ctx) else 0; 306 } 307 col_start = 0; 308 const item_fields = meta.fields(DataT); 309 var col_idx: usize = 0; 310 for (field_indexes) |f_idx| { 311 inline for (item_fields[0..], 0..) |item_field, item_idx| contFields: { 312 switch (table_ctx.col_indexes) { 313 .all => {}, 314 .by_idx => { 315 if (item_idx != f_idx) break :contFields; 316 }, 317 } 318 defer col_idx += 1; 319 const col_width = try calcColWidth( 320 item_idx, 321 headers, 322 table_ctx.col_width, 323 table_win, 324 ); 325 defer col_start += col_width; 326 const item = @field(data, item_field.name); 327 const ItemT = @TypeOf(item); 328 const item_win = row_win.child(.{ 329 .x_off = col_start, 330 .y_off = 0, 331 .width = col_width, 332 .height = 1, 333 .border = .{ .where = if (table_ctx.col_borders and col_idx > 0) .left else .none }, 334 }); 335 const item_txt = switch (ItemT) { 336 []const u8 => item, 337 [][]const u8, []const []const u8 => strSlice: { 338 if (alloc) |_alloc| break :strSlice try fmt.allocPrint(_alloc, "{s}", .{item}); 339 break :strSlice item; 340 }, 341 else => nonStr: { 342 switch (@typeInfo(ItemT)) { 343 .@"enum" => break :nonStr @tagName(item), 344 .optional => { 345 const opt_item = item orelse break :nonStr "-"; 346 switch (@typeInfo(ItemT).optional.child) { 347 []const u8 => break :nonStr opt_item, 348 [][]const u8, []const []const u8 => { 349 break :nonStr if (alloc) |_alloc| try fmt.allocPrint(_alloc, "{s}", .{opt_item}) else fmt.comptimePrint("[unsupported ({s})]", .{@typeName(DataT)}); 350 }, 351 else => { 352 break :nonStr if (alloc) |_alloc| try fmt.allocPrint(_alloc, "{any}", .{opt_item}) else fmt.comptimePrint("[unsupported ({s})]", .{@typeName(DataT)}); 353 }, 354 } 355 }, 356 else => { 357 break :nonStr if (alloc) |_alloc| try fmt.allocPrint(_alloc, "{any}", .{item}) else fmt.comptimePrint("[unsupported ({s})]", .{@typeName(DataT)}); 358 }, 359 } 360 }, 361 }; 362 item_win.fill(.{ .style = .{ .bg = row_bg } }); 363 const item_align_win = itemAlignWin: { 364 const col_align = switch (table_ctx.col_align) { 365 .all => |all| all, 366 .by_idx => |aligns| aligns[col_idx], 367 }; 368 break :itemAlignWin switch (col_align) { 369 .left => item_win, 370 .center => center: { 371 const center = vaxis.widgets.alignment.center(item_win, @min(col_width -| 1, item_txt.len +| 1), 1); 372 center.fill(.{ .style = .{ .bg = row_bg } }); 373 break :center center; 374 }, 375 }; 376 }; 377 var seg = [_]vaxis.Cell.Segment{.{ 378 .text = if (item_txt.len > col_width and alloc != null) try fmt.allocPrint(alloc.?, "{s}...", .{item_txt[0..(col_width -| 4)]}) else item_txt, 379 .style = .{ .fg = row_fg, .bg = row_bg }, 380 }}; 381 _ = item_align_win.print(seg[0..], .{ .wrap = .word, .col_offset = table_ctx.cell_x_off }); 382 } 383 } 384 } 385} 386 387/// Calculate the Column Width of `col` using the provided Number of Headers (`num_hdrs`), Width Style (`style`), and Table Window (`table_win`). 388pub fn calcColWidth( 389 col: u16, 390 headers: []const []const u8, 391 style: WidthStyle, 392 table_win: vaxis.Window, 393) !u16 { 394 return switch (style) { 395 .dynamic_fill => dynFill: { 396 var cw: u16 = table_win.width / @as(u16, @intCast(headers.len)); 397 if (cw % 2 != 0) cw +|= 1; 398 while (cw * headers.len < table_win.width - 1) cw +|= 1; 399 break :dynFill cw; 400 }, 401 .dynamic_header_len => dynHdrs: { 402 if (col >= headers.len) break :dynHdrs error.NotEnoughStaticWidthsProvided; 403 break :dynHdrs @as(u16, @intCast(headers[col].len)) + (style.dynamic_header_len * 2); 404 }, 405 .static_all => style.static_all, 406 .static_individual => statInd: { 407 if (col >= headers.len) break :statInd error.NotEnoughStaticWidthsProvided; 408 break :statInd style.static_individual[col]; 409 }, 410 }; 411}