this repo has no description
13
fork

Configure Feed

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

at b58ae3a2fa16b3b7f11d2a7297c73a1e839d035b 514 lines 17 kB view raw
1const std = @import("std"); 2const assert = std.debug.assert; 3const vaxis = @import("../../main.zig"); 4 5const ansi = @import("ansi.zig"); 6 7const log = std.log.scoped(.vaxis_terminal); 8 9const Screen = @This(); 10 11pub const Cell = struct { 12 char: std.ArrayList(u8) = .empty, 13 style: vaxis.Style = .{}, 14 uri: std.ArrayList(u8) = .empty, 15 uri_id: std.ArrayList(u8) = .empty, 16 width: u8 = 1, 17 18 wrapped: bool = false, 19 dirty: bool = true, 20 21 pub fn erase(self: *Cell, allocator: std.mem.Allocator, bg: vaxis.Color) void { 22 self.char.clearRetainingCapacity(); 23 self.char.append(allocator, ' ') catch unreachable; // we never completely free this list 24 self.style = .{}; 25 self.style.bg = bg; 26 self.uri.clearRetainingCapacity(); 27 self.uri_id.clearRetainingCapacity(); 28 self.width = 1; 29 self.wrapped = false; 30 self.dirty = true; 31 } 32 33 pub fn copyFrom(self: *Cell, allocator: std.mem.Allocator, src: Cell) !void { 34 self.char.clearRetainingCapacity(); 35 try self.char.appendSlice(allocator, src.char.items); 36 self.style = src.style; 37 self.uri.clearRetainingCapacity(); 38 try self.uri.appendSlice(allocator, src.uri.items); 39 self.uri_id.clearRetainingCapacity(); 40 try self.uri_id.appendSlice(allocator, src.uri_id.items); 41 self.width = src.width; 42 self.wrapped = src.wrapped; 43 44 self.dirty = true; 45 } 46}; 47 48pub const Cursor = struct { 49 style: vaxis.Style = .{}, 50 uri: std.ArrayList(u8) = undefined, 51 uri_id: std.ArrayList(u8) = undefined, 52 col: u16 = 0, 53 row: u16 = 0, 54 pending_wrap: bool = false, 55 shape: vaxis.Cell.CursorShape = .default, 56 visible: bool = true, 57 58 pub fn isOutsideScrollingRegion(self: Cursor, sr: ScrollingRegion) bool { 59 return self.row < sr.top or 60 self.row > sr.bottom or 61 self.col < sr.left or 62 self.col > sr.right; 63 } 64 65 pub fn isInsideScrollingRegion(self: Cursor, sr: ScrollingRegion) bool { 66 return !self.isOutsideScrollingRegion(sr); 67 } 68}; 69 70pub const ScrollingRegion = struct { 71 top: u16, 72 bottom: u16, 73 left: u16, 74 right: u16, 75 76 pub fn contains(self: ScrollingRegion, col: usize, row: usize) bool { 77 return col >= self.left and 78 col <= self.right and 79 row >= self.top and 80 row <= self.bottom; 81 } 82}; 83 84allocator: std.mem.Allocator, 85 86width: u16 = 0, 87height: u16 = 0, 88 89scrolling_region: ScrollingRegion, 90 91buf: []Cell = undefined, 92 93cursor: Cursor = .{}, 94 95csi_u_flags: vaxis.Key.KittyFlags = @bitCast(@as(u5, 0)), 96 97/// sets each cell to the default cell 98pub fn init(alloc: std.mem.Allocator, w: u16, h: u16) !Screen { 99 var screen = Screen{ 100 .allocator = alloc, 101 .buf = try alloc.alloc(Cell, @as(usize, @intCast(w)) * h), 102 .scrolling_region = .{ 103 .top = 0, 104 .bottom = h - 1, 105 .left = 0, 106 .right = w - 1, 107 }, 108 .width = w, 109 .height = h, 110 }; 111 for (screen.buf, 0..) |_, i| { 112 screen.buf[i] = .{ 113 .char = try .initCapacity(alloc, 1), 114 }; 115 try screen.buf[i].char.append(alloc, ' '); 116 } 117 return screen; 118} 119 120pub fn deinit(self: *Screen, alloc: std.mem.Allocator) void { 121 for (self.buf, 0..) |_, i| { 122 self.buf[i].char.deinit(alloc); 123 self.buf[i].uri.deinit(alloc); 124 self.buf[i].uri_id.deinit(alloc); 125 } 126 127 alloc.free(self.buf); 128} 129 130/// copies the visible area to the destination screen 131pub fn copyTo(self: *Screen, allocator: std.mem.Allocator, dst: *Screen) !void { 132 dst.cursor = self.cursor; 133 for (self.buf, 0..) |cell, i| { 134 if (!cell.dirty) continue; 135 self.buf[i].dirty = false; 136 const grapheme = cell.char.items; 137 dst.buf[i].char.clearRetainingCapacity(); 138 try dst.buf[i].char.appendSlice(allocator, grapheme); 139 dst.buf[i].width = cell.width; 140 dst.buf[i].style = cell.style; 141 } 142} 143 144pub fn readCell(self: *Screen, col: usize, row: usize) ?vaxis.Cell { 145 if (self.width < col) { 146 // column out of bounds 147 return null; 148 } 149 if (self.height < row) { 150 // height out of bounds 151 return null; 152 } 153 const i = (row * self.width) + col; 154 assert(i < self.buf.len); 155 const cell = self.buf[i]; 156 return .{ 157 .char = .{ .grapheme = cell.char.items, .width = cell.width }, 158 .style = cell.style, 159 }; 160} 161 162/// returns true if the current cursor position is within the scrolling region 163pub fn withinScrollingRegion(self: Screen) bool { 164 return self.scrolling_region.contains(self.cursor.col, self.cursor.row); 165} 166 167/// writes a cell to a location. 0 indexed 168pub fn print( 169 self: *Screen, 170 grapheme: []const u8, 171 width: u8, 172 wrap: bool, 173) !void { 174 if (self.cursor.pending_wrap) { 175 try self.index(); 176 self.cursor.col = self.scrolling_region.left; 177 } 178 if (self.cursor.col >= self.width) return; 179 if (self.cursor.row >= self.height) return; 180 const col = self.cursor.col; 181 const row = self.cursor.row; 182 183 const i = (row * self.width) + col; 184 assert(i < self.buf.len); 185 self.buf[i].char.clearRetainingCapacity(); 186 self.buf[i].char.appendSlice(self.allocator, grapheme) catch { 187 log.warn("couldn't write grapheme", .{}); 188 }; 189 self.buf[i].uri.clearRetainingCapacity(); 190 self.buf[i].uri.appendSlice(self.allocator, self.cursor.uri.items) catch { 191 log.warn("couldn't write uri", .{}); 192 }; 193 self.buf[i].uri_id.clearRetainingCapacity(); 194 self.buf[i].uri_id.appendSlice(self.allocator, self.cursor.uri_id.items) catch { 195 log.warn("couldn't write uri_id", .{}); 196 }; 197 self.buf[i].style = self.cursor.style; 198 self.buf[i].width = width; 199 self.buf[i].dirty = true; 200 201 if (wrap and self.cursor.col >= self.width - 1) self.cursor.pending_wrap = true; 202 self.cursor.col += width; 203} 204 205/// IND 206pub fn index(self: *Screen) !void { 207 self.cursor.pending_wrap = false; 208 209 if (self.cursor.isOutsideScrollingRegion(self.scrolling_region)) { 210 // Outside, we just move cursor down one 211 self.cursor.row = @min(self.height - 1, self.cursor.row + 1); 212 return; 213 } 214 // We are inside the scrolling region 215 if (self.cursor.row == self.scrolling_region.bottom) { 216 // Inside scrolling region *and* at bottom of screen, we scroll contents up and insert a 217 // blank line 218 // TODO: scrollback if scrolling region is entire visible screen 219 try self.deleteLine(1); 220 return; 221 } 222 self.cursor.row += 1; 223} 224 225pub fn sgr(self: *Screen, seq: ansi.CSI) void { 226 if (seq.params.len == 0) { 227 self.cursor.style = .{}; 228 return; 229 } 230 231 var iter = seq.iterator(u8); 232 while (iter.next()) |ps| { 233 switch (ps) { 234 0 => self.cursor.style = .{}, 235 1 => self.cursor.style.bold = true, 236 2 => self.cursor.style.dim = true, 237 3 => self.cursor.style.italic = true, 238 4 => { 239 const kind: vaxis.Style.Underline = if (iter.next_is_sub) 240 @enumFromInt(iter.next() orelse 1) 241 else 242 .single; 243 self.cursor.style.ul_style = kind; 244 }, 245 5 => self.cursor.style.blink = true, 246 7 => self.cursor.style.reverse = true, 247 8 => self.cursor.style.invisible = true, 248 9 => self.cursor.style.strikethrough = true, 249 21 => self.cursor.style.ul_style = .double, 250 22 => { 251 self.cursor.style.bold = false; 252 self.cursor.style.dim = false; 253 }, 254 23 => self.cursor.style.italic = false, 255 24 => self.cursor.style.ul_style = .off, 256 25 => self.cursor.style.blink = false, 257 27 => self.cursor.style.reverse = false, 258 28 => self.cursor.style.invisible = false, 259 29 => self.cursor.style.strikethrough = false, 260 30...37 => self.cursor.style.fg = .{ .index = ps - 30 }, 261 38 => { 262 // must have another parameter 263 const kind = iter.next() orelse return; 264 switch (kind) { 265 2 => { // rgb 266 const r = r: { 267 // First param can be empty 268 var ps_r = iter.next() orelse return; 269 if (iter.is_empty) 270 ps_r = iter.next() orelse return; 271 break :r ps_r; 272 }; 273 const g = iter.next() orelse return; 274 const b = iter.next() orelse return; 275 self.cursor.style.fg = .{ .rgb = .{ r, g, b } }; 276 }, 277 5 => { 278 const idx = iter.next() orelse return; 279 self.cursor.style.fg = .{ .index = idx }; 280 }, // index 281 else => return, 282 } 283 }, 284 39 => self.cursor.style.fg = .default, 285 40...47 => self.cursor.style.bg = .{ .index = ps - 40 }, 286 48 => { 287 // must have another parameter 288 const kind = iter.next() orelse return; 289 switch (kind) { 290 2 => { // rgb 291 const r = r: { 292 // First param can be empty 293 var ps_r = iter.next() orelse return; 294 if (iter.is_empty) 295 ps_r = iter.next() orelse return; 296 break :r ps_r; 297 }; 298 const g = iter.next() orelse return; 299 const b = iter.next() orelse return; 300 self.cursor.style.bg = .{ .rgb = .{ r, g, b } }; 301 }, 302 5 => { 303 const idx = iter.next() orelse return; 304 self.cursor.style.bg = .{ .index = idx }; 305 }, // index 306 else => return, 307 } 308 }, 309 49 => self.cursor.style.bg = .default, 310 90...97 => self.cursor.style.fg = .{ .index = ps - 90 + 8 }, 311 100...107 => self.cursor.style.bg = .{ .index = ps - 100 + 8 }, 312 else => continue, 313 } 314 } 315} 316 317pub fn cursorUp(self: *Screen, n: u16) void { 318 self.cursor.pending_wrap = false; 319 if (self.withinScrollingRegion()) 320 self.cursor.row = @max( 321 self.cursor.row -| n, 322 self.scrolling_region.top, 323 ) 324 else 325 self.cursor.row -|= n; 326} 327 328pub fn cursorLeft(self: *Screen, n: u16) void { 329 self.cursor.pending_wrap = false; 330 if (self.withinScrollingRegion()) 331 self.cursor.col = @max( 332 self.cursor.col -| n, 333 self.scrolling_region.left, 334 ) 335 else 336 self.cursor.col = self.cursor.col -| n; 337} 338 339pub fn cursorRight(self: *Screen, n: u16) void { 340 self.cursor.pending_wrap = false; 341 if (self.withinScrollingRegion()) 342 self.cursor.col = @min( 343 self.cursor.col + n, 344 self.scrolling_region.right, 345 ) 346 else 347 self.cursor.col = @min( 348 self.cursor.col + n, 349 self.width - 1, 350 ); 351} 352 353pub fn cursorDown(self: *Screen, n: usize) void { 354 self.cursor.pending_wrap = false; 355 if (self.withinScrollingRegion()) 356 self.cursor.row = @min( 357 self.scrolling_region.bottom, 358 self.cursor.row + n, 359 ) 360 else 361 self.cursor.row = @min( 362 self.height -| 1, 363 self.cursor.row + n, 364 ); 365} 366 367pub fn eraseRight(self: *Screen) void { 368 self.cursor.pending_wrap = false; 369 const end = (self.cursor.row * self.width) + (self.width); 370 var i = (self.cursor.row * self.width) + self.cursor.col; 371 while (i < end) : (i += 1) { 372 self.buf[i].erase(self.allocator, self.cursor.style.bg); 373 } 374} 375 376pub fn eraseLeft(self: *Screen) void { 377 self.cursor.pending_wrap = false; 378 const start = self.cursor.row * self.width; 379 const end = start + self.cursor.col + 1; 380 var i = start; 381 while (i < end) : (i += 1) { 382 self.buf[i].erase(self.allocator, self.cursor.style.bg); 383 } 384} 385 386pub fn eraseLine(self: *Screen) void { 387 self.cursor.pending_wrap = false; 388 const start = self.cursor.row * self.width; 389 const end = start + self.width; 390 var i = start; 391 while (i < end) : (i += 1) { 392 self.buf[i].erase(self.allocator, self.cursor.style.bg); 393 } 394} 395 396/// delete n lines from the bottom of the scrolling region 397pub fn deleteLine(self: *Screen, n: usize) !void { 398 if (n == 0) return; 399 400 // Don't delete if outside scroll region 401 if (!self.withinScrollingRegion()) return; 402 403 self.cursor.pending_wrap = false; 404 405 // Number of rows from here to bottom of scroll region or n 406 const cnt = @min(self.scrolling_region.bottom - self.cursor.row + 1, n); 407 const stride = (self.width) * cnt; 408 409 var row: usize = self.scrolling_region.top; 410 while (row <= self.scrolling_region.bottom) : (row += 1) { 411 var col: usize = self.scrolling_region.left; 412 while (col <= self.scrolling_region.right) : (col += 1) { 413 const i = (row * self.width) + col; 414 if (row + cnt > self.scrolling_region.bottom) 415 self.buf[i].erase(self.allocator, self.cursor.style.bg) 416 else 417 try self.buf[i].copyFrom(self.allocator, self.buf[i + stride]); 418 } 419 } 420} 421 422/// insert n lines at the top of the scrolling region 423pub fn insertLine(self: *Screen, n: usize) !void { 424 if (n == 0) return; 425 426 self.cursor.pending_wrap = false; 427 // Don't insert if outside scroll region 428 if (!self.withinScrollingRegion()) return; 429 430 const adjusted_n = @min(self.scrolling_region.bottom - self.cursor.row, n); 431 const stride = (self.width) * adjusted_n; 432 433 var row: usize = self.scrolling_region.bottom; 434 while (row >= self.scrolling_region.top + adjusted_n) : (row -|= 1) { 435 var col: usize = self.scrolling_region.left; 436 while (col <= self.scrolling_region.right) : (col += 1) { 437 const i = (row * self.width) + col; 438 try self.buf[i].copyFrom(self.allocator, self.buf[i - stride]); 439 } 440 } 441 442 row = self.scrolling_region.top; 443 while (row < self.scrolling_region.top + adjusted_n) : (row += 1) { 444 var col: usize = self.scrolling_region.left; 445 while (col <= self.scrolling_region.right) : (col += 1) { 446 const i = (row * self.width) + col; 447 self.buf[i].erase(self.allocator, self.cursor.style.bg); 448 } 449 } 450} 451 452pub fn eraseBelow(self: *Screen) void { 453 self.eraseRight(); 454 // start is the first column of the row below us 455 const start = (self.cursor.row * self.width) + (self.width); 456 var i = start; 457 while (i < self.buf.len) : (i += 1) { 458 self.buf[i].erase(self.allocator, self.cursor.style.bg); 459 } 460} 461 462pub fn eraseAbove(self: *Screen) void { 463 self.eraseLeft(); 464 // start is the first column of the row below us 465 const start: usize = 0; 466 const end = self.cursor.row * self.width; 467 var i = start; 468 while (i < end) : (i += 1) { 469 self.buf[i].erase(self.allocator, self.cursor.style.bg); 470 } 471} 472 473pub fn eraseAll(self: *Screen) void { 474 var i: usize = 0; 475 while (i < self.buf.len) : (i += 1) { 476 self.buf[i].erase(self.allocator, self.cursor.style.bg); 477 } 478} 479 480pub fn deleteCharacters(self: *Screen, n: usize) !void { 481 if (!self.withinScrollingRegion()) return; 482 483 self.cursor.pending_wrap = false; 484 var col = self.cursor.col; 485 while (col <= self.scrolling_region.right) : (col += 1) { 486 if (col + n <= self.scrolling_region.right) 487 try self.buf[col].copyFrom(self.allocator, self.buf[col + n]) 488 else 489 self.buf[col].erase(self.allocator, self.cursor.style.bg); 490 } 491} 492 493pub fn reverseIndex(self: *Screen) !void { 494 if (self.cursor.row != self.scrolling_region.top or 495 self.cursor.col < self.scrolling_region.left or 496 self.cursor.col > self.scrolling_region.right) 497 self.cursorUp(1) 498 else 499 try self.scrollDown(1); 500} 501 502pub fn scrollDown(self: *Screen, n: usize) !void { 503 const cur_row = self.cursor.row; 504 const cur_col = self.cursor.col; 505 const wrap = self.cursor.pending_wrap; 506 defer { 507 self.cursor.row = cur_row; 508 self.cursor.col = cur_col; 509 self.cursor.pending_wrap = wrap; 510 } 511 self.cursor.col = self.scrolling_region.left; 512 self.cursor.row = self.scrolling_region.top; 513 try self.insertLine(n); 514}