this repo has no description
13
fork

Configure Feed

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

at bf4e6ae3d396e62f1be2c1fde61dae1cbbd2aa57 1282 lines 45 kB view raw
1const std = @import("std"); 2const builtin = @import("builtin"); 3const atomic = std.atomic; 4const base64Encoder = std.base64.standard.Encoder; 5const zigimg = @import("zigimg"); 6 7const Cell = @import("Cell.zig"); 8const Image = @import("Image.zig"); 9const InternalScreen = @import("InternalScreen.zig"); 10const Key = @import("Key.zig"); 11const Mouse = @import("Mouse.zig"); 12const Screen = @import("Screen.zig"); 13const Unicode = @import("Unicode.zig"); 14const Window = @import("Window.zig"); 15 16const AnyWriter = std.io.AnyWriter; 17const Hyperlink = Cell.Hyperlink; 18const KittyFlags = Key.KittyFlags; 19const Shape = Mouse.Shape; 20const Style = Cell.Style; 21const Winsize = @import("main.zig").Winsize; 22 23const ctlseqs = @import("ctlseqs.zig"); 24const gwidth = @import("gwidth.zig"); 25 26const assert = std.debug.assert; 27 28const Vaxis = @This(); 29 30const log = std.log.scoped(.vaxis); 31 32pub const Capabilities = struct { 33 kitty_keyboard: bool = false, 34 kitty_graphics: bool = false, 35 rgb: bool = false, 36 unicode: gwidth.Method = .wcwidth, 37 sgr_pixels: bool = false, 38 color_scheme_updates: bool = false, 39 explicit_width: bool = false, 40}; 41 42pub const Options = struct { 43 kitty_keyboard_flags: KittyFlags = .{}, 44 /// When supplied, this allocator will be used for system clipboard 45 /// requests. If not supplied, it won't be possible to request the system 46 /// clipboard 47 system_clipboard_allocator: ?std.mem.Allocator = null, 48}; 49 50/// the screen we write to 51screen: Screen, 52/// The last screen we drew. We keep this so we can efficiently update on 53/// the next render 54screen_last: InternalScreen, 55 56caps: Capabilities = .{}, 57 58opts: Options = .{}, 59 60/// if we should redraw the entire screen on the next render 61refresh: bool = false, 62 63/// blocks the main thread until a DA1 query has been received, or the 64/// futex times out 65query_futex: atomic.Value(u32) = atomic.Value(u32).init(0), 66 67/// If Queries were sent, we set this to false. We reset to true when all queries are complete. This 68/// is used because we do explicit cursor position reports in the queries, which interfere with F3 69/// key encoding. This can be used as a flag to determine how we should evaluate this sequence 70queries_done: atomic.Value(bool) = atomic.Value(bool).init(true), 71 72// images 73next_img_id: u32 = 1, 74 75unicode: Unicode, 76 77sgr: enum { 78 standard, 79 legacy, 80} = .standard, 81 82state: struct { 83 /// if we are in the alt screen 84 alt_screen: bool = false, 85 /// if we have entered kitty keyboard 86 kitty_keyboard: bool = false, 87 bracketed_paste: bool = false, 88 mouse: bool = false, 89 pixel_mouse: bool = false, 90 color_scheme_updates: bool = false, 91 in_band_resize: bool = false, 92 changed_default_fg: bool = false, 93 changed_default_bg: bool = false, 94 changed_cursor_color: bool = false, 95 cursor: struct { 96 row: u16 = 0, 97 col: u16 = 0, 98 } = .{}, 99} = .{}, 100 101/// Initialize Vaxis with runtime options 102pub fn init(alloc: std.mem.Allocator, opts: Options) !Vaxis { 103 return .{ 104 .opts = opts, 105 .screen = .{}, 106 .screen_last = try .init(alloc, 80, 24), 107 .unicode = try Unicode.init(alloc), 108 }; 109} 110 111/// Resets the terminal to it's original state. If an allocator is 112/// passed, this will free resources associated with Vaxis. This is left as an 113/// optional so applications can choose to not free resources when the 114/// application will be exiting anyways 115pub fn deinit(self: *Vaxis, alloc: ?std.mem.Allocator, tty: AnyWriter) void { 116 self.resetState(tty) catch {}; 117 118 if (alloc) |a| { 119 self.screen.deinit(a); 120 self.screen_last.deinit(a); 121 } 122 self.unicode.deinit(); 123} 124 125/// resets enabled features, sends cursor to home and clears below cursor 126pub fn resetState(self: *Vaxis, tty: AnyWriter) !void { 127 // always show the cursor on state reset 128 tty.writeAll(ctlseqs.show_cursor) catch {}; 129 tty.writeAll(ctlseqs.sgr_reset) catch {}; 130 if (self.screen.cursor_shape != .default) { 131 // In many terminals, `.default` will set to the configured cursor shape. Others, it will 132 // change to a blinking block. 133 tty.print(ctlseqs.cursor_shape, .{@intFromEnum(Cell.CursorShape.default)}) catch {}; 134 } 135 if (self.state.kitty_keyboard) { 136 try tty.writeAll(ctlseqs.csi_u_pop); 137 self.state.kitty_keyboard = false; 138 } 139 if (self.state.mouse) { 140 try self.setMouseMode(tty, false); 141 } 142 if (self.state.bracketed_paste) { 143 try self.setBracketedPaste(tty, false); 144 } 145 if (self.state.alt_screen) { 146 try tty.writeAll(ctlseqs.home); 147 try tty.writeAll(ctlseqs.erase_below_cursor); 148 try self.exitAltScreen(tty); 149 } else { 150 try tty.writeByte('\r'); 151 var i: u16 = 0; 152 while (i < self.state.cursor.row) : (i += 1) { 153 try tty.writeAll(ctlseqs.ri); 154 } 155 try tty.writeAll(ctlseqs.erase_below_cursor); 156 } 157 if (self.state.color_scheme_updates) { 158 try tty.writeAll(ctlseqs.color_scheme_reset); 159 self.state.color_scheme_updates = false; 160 } 161 if (self.state.in_band_resize) { 162 try tty.writeAll(ctlseqs.in_band_resize_reset); 163 self.state.in_band_resize = false; 164 } 165 if (self.state.changed_default_fg) { 166 try tty.writeAll(ctlseqs.osc10_reset); 167 self.state.changed_default_fg = false; 168 } 169 if (self.state.changed_default_bg) { 170 try tty.writeAll(ctlseqs.osc11_reset); 171 self.state.changed_default_bg = false; 172 } 173 if (self.state.changed_cursor_color) { 174 try tty.writeAll(ctlseqs.osc12_reset); 175 self.state.changed_cursor_color = false; 176 } 177} 178 179/// resize allocates a slice of cells equal to the number of cells 180/// required to display the screen (ie width x height). Any previous screen is 181/// freed when resizing. The cursor will be sent to it's home position and a 182/// hardware clear-below-cursor will be sent 183pub fn resize( 184 self: *Vaxis, 185 alloc: std.mem.Allocator, 186 tty: AnyWriter, 187 winsize: Winsize, 188) !void { 189 log.debug("resizing screen: width={d} height={d}", .{ winsize.cols, winsize.rows }); 190 self.screen.deinit(alloc); 191 self.screen = try Screen.init(alloc, winsize, &self.unicode); 192 self.screen.width_method = self.caps.unicode; 193 // try self.screen.int(alloc, winsize.cols, winsize.rows); 194 // we only init our current screen. This has the effect of redrawing 195 // every cell 196 self.screen_last.deinit(alloc); 197 self.screen_last = try InternalScreen.init(alloc, winsize.cols, winsize.rows); 198 if (self.state.alt_screen) 199 try tty.writeAll(ctlseqs.home) 200 else { 201 try tty.writeBytesNTimes(ctlseqs.ri, self.state.cursor.row); 202 try tty.writeByte('\r'); 203 } 204 self.state.cursor.row = 0; 205 self.state.cursor.col = 0; 206 try tty.writeAll(ctlseqs.sgr_reset ++ ctlseqs.erase_below_cursor); 207} 208 209/// returns a Window comprising of the entire terminal screen 210pub fn window(self: *Vaxis) Window { 211 return .{ 212 .x_off = 0, 213 .y_off = 0, 214 .parent_x_off = 0, 215 .parent_y_off = 0, 216 .width = self.screen.width, 217 .height = self.screen.height, 218 .screen = &self.screen, 219 }; 220} 221 222/// enter the alternate screen. The alternate screen will automatically 223/// be exited if calling deinit while in the alt screen 224pub fn enterAltScreen(self: *Vaxis, tty: AnyWriter) !void { 225 try tty.writeAll(ctlseqs.smcup); 226 self.state.alt_screen = true; 227} 228 229/// exit the alternate screen 230pub fn exitAltScreen(self: *Vaxis, tty: AnyWriter) !void { 231 try tty.writeAll(ctlseqs.rmcup); 232 self.state.alt_screen = false; 233} 234 235/// write queries to the terminal to determine capabilities. Individual 236/// capabilities will be delivered to the client and possibly intercepted by 237/// Vaxis to enable features. 238/// 239/// This call will block until Vaxis.query_futex is woken up, or the timeout. 240/// Event loops can wake up this futex when cap_da1 is received 241pub fn queryTerminal(self: *Vaxis, tty: AnyWriter, timeout_ns: u64) !void { 242 try self.queryTerminalSend(tty); 243 // 1 second timeout 244 std.Thread.Futex.timedWait(&self.query_futex, 0, timeout_ns) catch {}; 245 self.queries_done.store(true, .unordered); 246 try self.enableDetectedFeatures(tty); 247} 248 249/// write queries to the terminal to determine capabilities. This function 250/// is only for use with a custom main loop. Call Vaxis.queryTerminal() if 251/// you are using Loop.run() 252pub fn queryTerminalSend(vx: *Vaxis, tty: AnyWriter) !void { 253 vx.queries_done.store(false, .unordered); 254 255 // TODO: re-enable this 256 // const colorterm = std.posix.getenv("COLORTERM") orelse ""; 257 // if (std.mem.eql(u8, colorterm, "truecolor") or 258 // std.mem.eql(u8, colorterm, "24bit")) 259 // { 260 // if (@hasField(Event, "cap_rgb")) { 261 // self.postEvent(.cap_rgb); 262 // } 263 // } 264 265 // TODO: XTGETTCAP queries ("RGB", "Smulx") 266 // TODO: decide if we actually want to query for focus and sync. It 267 // doesn't hurt to blindly use them 268 // _ = try tty.write(ctlseqs.decrqm_focus); 269 // _ = try tty.write(ctlseqs.decrqm_sync); 270 try tty.writeAll(ctlseqs.decrqm_sgr_pixels ++ 271 ctlseqs.decrqm_unicode ++ 272 ctlseqs.decrqm_color_scheme ++ 273 ctlseqs.in_band_resize_set ++ 274 275 // Explicit width query. We send the cursor home, then do an explicit width command, then 276 // query the position. If the parsed value is an F3 with shift, we support explicit width. 277 // The returned response will be something like \x1b[1;2R...which when parsed as a Key is a 278 // shift + F3 (the row is ignored). We only care if the column has moved from 1->2, which is 279 // why we see a Shift modifier 280 ctlseqs.home ++ 281 ctlseqs.explicit_width_query ++ 282 ctlseqs.cursor_position_request ++ 283 ctlseqs.xtversion ++ 284 ctlseqs.csi_u_query ++ 285 ctlseqs.kitty_graphics_query ++ 286 ctlseqs.primary_device_attrs); 287} 288 289/// Enable features detected by responses to queryTerminal. This function 290/// is only for use with a custom main loop. Call Vaxis.queryTerminal() if 291/// you are using Loop.run() 292pub fn enableDetectedFeatures(self: *Vaxis, tty: AnyWriter) !void { 293 switch (builtin.os.tag) { 294 .windows => { 295 // No feature detection on windows. We just hard enable some knowns for ConPTY 296 self.sgr = .legacy; 297 }, 298 else => { 299 // Apply any environment variables 300 if (std.posix.getenv("TERMUX_VERSION")) |_| 301 self.sgr = .legacy; 302 if (std.posix.getenv("VHS_RECORD")) |_| { 303 self.caps.unicode = .wcwidth; 304 self.caps.kitty_keyboard = false; 305 self.sgr = .legacy; 306 } 307 if (std.posix.getenv("TERM_PROGRAM")) |prg| { 308 if (std.mem.eql(u8, prg, "vscode")) 309 self.sgr = .legacy; 310 } 311 if (std.posix.getenv("VAXIS_FORCE_LEGACY_SGR")) |_| 312 self.sgr = .legacy; 313 if (std.posix.getenv("VAXIS_FORCE_WCWIDTH")) |_| 314 self.caps.unicode = .wcwidth; 315 if (std.posix.getenv("VAXIS_FORCE_UNICODE")) |_| 316 self.caps.unicode = .unicode; 317 318 // enable detected features 319 if (self.caps.kitty_keyboard) { 320 try self.enableKittyKeyboard(tty, self.opts.kitty_keyboard_flags); 321 } 322 // Only enable mode 2027 if we don't have explicit width 323 if (self.caps.unicode == .unicode and !self.caps.explicit_width) { 324 try tty.writeAll(ctlseqs.unicode_set); 325 } 326 }, 327 } 328} 329 330// the next render call will refresh the entire screen 331pub fn queueRefresh(self: *Vaxis) void { 332 self.refresh = true; 333} 334 335/// draws the screen to the terminal 336pub fn render(self: *Vaxis, tty: AnyWriter) !void { 337 defer self.refresh = false; 338 assert(self.screen.buf.len == @as(usize, @intCast(self.screen.width)) * self.screen.height); // correct size 339 assert(self.screen.buf.len == self.screen_last.buf.len); // same size 340 341 // Set up sync before we write anything 342 // TODO: optimize sync so we only sync _when we have changes_. This 343 // requires a smarter buffered writer, we'll probably have to write 344 // our own 345 try tty.writeAll(ctlseqs.sync_set); 346 defer tty.writeAll(ctlseqs.sync_reset) catch {}; 347 348 // Send the cursor to 0,0 349 // TODO: this needs to move after we optimize writes. We only do 350 // this if we have an update to make. We also need to hide cursor 351 // and then reshow it if needed 352 try tty.writeAll(ctlseqs.hide_cursor); 353 if (self.state.alt_screen) 354 try tty.writeAll(ctlseqs.home) 355 else { 356 try tty.writeByte('\r'); 357 try tty.writeBytesNTimes(ctlseqs.ri, self.state.cursor.row); 358 } 359 try tty.writeAll(ctlseqs.sgr_reset); 360 361 // initialize some variables 362 var reposition: bool = false; 363 var row: u16 = 0; 364 var col: u16 = 0; 365 var cursor: Style = .{}; 366 var link: Hyperlink = .{}; 367 var cursor_pos: struct { 368 row: u16 = 0, 369 col: u16 = 0, 370 } = .{}; 371 372 // Clear all images 373 if (self.caps.kitty_graphics) 374 try tty.writeAll(ctlseqs.kitty_graphics_clear); 375 376 var i: usize = 0; 377 while (i < self.screen.buf.len) { 378 const cell = self.screen.buf[i]; 379 const w: u16 = blk: { 380 if (cell.char.width != 0) break :blk cell.char.width; 381 382 const method: gwidth.Method = self.caps.unicode; 383 const width: u16 = @intCast(gwidth.gwidth(cell.char.grapheme, method, &self.unicode.width_data)); 384 break :blk @max(1, width); 385 }; 386 defer { 387 // advance by the width of this char mod 1 388 std.debug.assert(w > 0); 389 var j = i + 1; 390 while (j < i + w) : (j += 1) { 391 if (j >= self.screen_last.buf.len) break; 392 self.screen_last.buf[j].skipped = true; 393 } 394 col += w; 395 i += w; 396 } 397 if (col >= self.screen.width) { 398 row += 1; 399 col = 0; 400 // Rely on terminal wrapping to reposition into next row instead of forcing it 401 if (!cell.wrapped) 402 reposition = true; 403 } 404 // If cell is the same as our last frame, we don't need to do 405 // anything 406 const last = self.screen_last.buf[i]; 407 if (!self.refresh and last.eql(cell) and !last.skipped and cell.image == null) { 408 reposition = true; 409 // Close any osc8 sequence we might be in before 410 // repositioning 411 if (link.uri.len > 0) { 412 try tty.writeAll(ctlseqs.osc8_clear); 413 } 414 continue; 415 } 416 self.screen_last.buf[i].skipped = false; 417 defer { 418 cursor = cell.style; 419 link = cell.link; 420 } 421 // Set this cell in the last frame 422 self.screen_last.writeCell(col, row, cell); 423 424 // reposition the cursor, if needed 425 if (reposition) { 426 reposition = false; 427 link = .{}; 428 if (self.state.alt_screen) 429 try tty.print(ctlseqs.cup, .{ row + 1, col + 1 }) 430 else { 431 if (cursor_pos.row == row) { 432 const n = col - cursor_pos.col; 433 if (n > 0) 434 try tty.print(ctlseqs.cuf, .{n}); 435 } else { 436 const n = row - cursor_pos.row; 437 try tty.writeByteNTimes('\n', n); 438 try tty.writeByte('\r'); 439 if (col > 0) 440 try tty.print(ctlseqs.cuf, .{col}); 441 } 442 } 443 } 444 445 if (cell.image) |img| { 446 try tty.print( 447 ctlseqs.kitty_graphics_preamble, 448 .{img.img_id}, 449 ); 450 if (img.options.pixel_offset) |offset| { 451 try tty.print( 452 ",X={d},Y={d}", 453 .{ offset.x, offset.y }, 454 ); 455 } 456 if (img.options.clip_region) |clip| { 457 if (clip.x) |x| 458 try tty.print(",x={d}", .{x}); 459 if (clip.y) |y| 460 try tty.print(",y={d}", .{y}); 461 if (clip.width) |width| 462 try tty.print(",w={d}", .{width}); 463 if (clip.height) |height| 464 try tty.print(",h={d}", .{height}); 465 } 466 if (img.options.size) |size| { 467 if (size.rows) |rows| 468 try tty.print(",r={d}", .{rows}); 469 if (size.cols) |cols| 470 try tty.print(",c={d}", .{cols}); 471 } 472 if (img.options.z_index) |z| { 473 try tty.print(",z={d}", .{z}); 474 } 475 try tty.writeAll(ctlseqs.kitty_graphics_closing); 476 } 477 478 // something is different, so let's loop through everything and 479 // find out what 480 481 // foreground 482 if (!Cell.Color.eql(cursor.fg, cell.style.fg)) { 483 switch (cell.style.fg) { 484 .default => try tty.writeAll(ctlseqs.fg_reset), 485 .index => |idx| { 486 switch (idx) { 487 0...7 => try tty.print(ctlseqs.fg_base, .{idx}), 488 8...15 => try tty.print(ctlseqs.fg_bright, .{idx - 8}), 489 else => { 490 switch (self.sgr) { 491 .standard => try tty.print(ctlseqs.fg_indexed, .{idx}), 492 .legacy => try tty.print(ctlseqs.fg_indexed_legacy, .{idx}), 493 } 494 }, 495 } 496 }, 497 .rgb => |rgb| { 498 switch (self.sgr) { 499 .standard => try tty.print(ctlseqs.fg_rgb, .{ rgb[0], rgb[1], rgb[2] }), 500 .legacy => try tty.print(ctlseqs.fg_rgb_legacy, .{ rgb[0], rgb[1], rgb[2] }), 501 } 502 }, 503 } 504 } 505 // background 506 if (!Cell.Color.eql(cursor.bg, cell.style.bg)) { 507 switch (cell.style.bg) { 508 .default => try tty.writeAll(ctlseqs.bg_reset), 509 .index => |idx| { 510 switch (idx) { 511 0...7 => try tty.print(ctlseqs.bg_base, .{idx}), 512 8...15 => try tty.print(ctlseqs.bg_bright, .{idx - 8}), 513 else => { 514 switch (self.sgr) { 515 .standard => try tty.print(ctlseqs.bg_indexed, .{idx}), 516 .legacy => try tty.print(ctlseqs.bg_indexed_legacy, .{idx}), 517 } 518 }, 519 } 520 }, 521 .rgb => |rgb| { 522 switch (self.sgr) { 523 .standard => try tty.print(ctlseqs.bg_rgb, .{ rgb[0], rgb[1], rgb[2] }), 524 .legacy => try tty.print(ctlseqs.bg_rgb_legacy, .{ rgb[0], rgb[1], rgb[2] }), 525 } 526 }, 527 } 528 } 529 // underline color 530 if (!Cell.Color.eql(cursor.ul, cell.style.ul)) { 531 switch (cell.style.ul) { 532 .default => try tty.writeAll(ctlseqs.ul_reset), 533 .index => |idx| { 534 switch (self.sgr) { 535 .standard => try tty.print(ctlseqs.ul_indexed, .{idx}), 536 .legacy => try tty.print(ctlseqs.ul_indexed_legacy, .{idx}), 537 } 538 }, 539 .rgb => |rgb| { 540 switch (self.sgr) { 541 .standard => try tty.print(ctlseqs.ul_rgb, .{ rgb[0], rgb[1], rgb[2] }), 542 .legacy => try tty.print(ctlseqs.ul_rgb_legacy, .{ rgb[0], rgb[1], rgb[2] }), 543 } 544 }, 545 } 546 } 547 // underline style 548 if (cursor.ul_style != cell.style.ul_style) { 549 const seq = switch (cell.style.ul_style) { 550 .off => ctlseqs.ul_off, 551 .single => ctlseqs.ul_single, 552 .double => ctlseqs.ul_double, 553 .curly => ctlseqs.ul_curly, 554 .dotted => ctlseqs.ul_dotted, 555 .dashed => ctlseqs.ul_dashed, 556 }; 557 try tty.writeAll(seq); 558 } 559 // bold 560 if (cursor.bold != cell.style.bold) { 561 const seq = switch (cell.style.bold) { 562 true => ctlseqs.bold_set, 563 false => ctlseqs.bold_dim_reset, 564 }; 565 try tty.writeAll(seq); 566 if (cell.style.dim) { 567 try tty.writeAll(ctlseqs.dim_set); 568 } 569 } 570 // dim 571 if (cursor.dim != cell.style.dim) { 572 const seq = switch (cell.style.dim) { 573 true => ctlseqs.dim_set, 574 false => ctlseqs.bold_dim_reset, 575 }; 576 try tty.writeAll(seq); 577 if (cell.style.bold) { 578 try tty.writeAll(ctlseqs.bold_set); 579 } 580 } 581 // dim 582 if (cursor.italic != cell.style.italic) { 583 const seq = switch (cell.style.italic) { 584 true => ctlseqs.italic_set, 585 false => ctlseqs.italic_reset, 586 }; 587 try tty.writeAll(seq); 588 } 589 // dim 590 if (cursor.blink != cell.style.blink) { 591 const seq = switch (cell.style.blink) { 592 true => ctlseqs.blink_set, 593 false => ctlseqs.blink_reset, 594 }; 595 try tty.writeAll(seq); 596 } 597 // reverse 598 if (cursor.reverse != cell.style.reverse) { 599 const seq = switch (cell.style.reverse) { 600 true => ctlseqs.reverse_set, 601 false => ctlseqs.reverse_reset, 602 }; 603 try tty.writeAll(seq); 604 } 605 // invisible 606 if (cursor.invisible != cell.style.invisible) { 607 const seq = switch (cell.style.invisible) { 608 true => ctlseqs.invisible_set, 609 false => ctlseqs.invisible_reset, 610 }; 611 try tty.writeAll(seq); 612 } 613 // strikethrough 614 if (cursor.strikethrough != cell.style.strikethrough) { 615 const seq = switch (cell.style.strikethrough) { 616 true => ctlseqs.strikethrough_set, 617 false => ctlseqs.strikethrough_reset, 618 }; 619 try tty.writeAll(seq); 620 } 621 622 // url 623 if (!std.mem.eql(u8, link.uri, cell.link.uri)) { 624 var ps = cell.link.params; 625 if (cell.link.uri.len == 0) { 626 // Empty out the params no matter what if we don't have 627 // a url 628 ps = ""; 629 } 630 try tty.print(ctlseqs.osc8, .{ ps, cell.link.uri }); 631 } 632 633 // If we have explicit width and our width is greater than 1, let's use it 634 if (self.caps.explicit_width and w > 1) { 635 try tty.print(ctlseqs.explicit_width, .{ w, cell.char.grapheme }); 636 } else { 637 try tty.writeAll(cell.char.grapheme); 638 } 639 cursor_pos.col = col + w; 640 cursor_pos.row = row; 641 } 642 if (self.screen.cursor_vis) { 643 if (self.state.alt_screen) { 644 try tty.print( 645 ctlseqs.cup, 646 .{ 647 self.screen.cursor_row + 1, 648 self.screen.cursor_col + 1, 649 }, 650 ); 651 } else { 652 // TODO: position cursor relative to current location 653 try tty.writeByte('\r'); 654 if (self.screen.cursor_row >= cursor_pos.row) 655 try tty.writeByteNTimes('\n', self.screen.cursor_row - cursor_pos.row) 656 else 657 try tty.writeBytesNTimes(ctlseqs.ri, cursor_pos.row - self.screen.cursor_row); 658 if (self.screen.cursor_col > 0) 659 try tty.print(ctlseqs.cuf, .{self.screen.cursor_col}); 660 } 661 self.state.cursor.row = self.screen.cursor_row; 662 self.state.cursor.col = self.screen.cursor_col; 663 try tty.writeAll(ctlseqs.show_cursor); 664 } else { 665 self.state.cursor.row = cursor_pos.row; 666 self.state.cursor.col = cursor_pos.col; 667 } 668 if (self.screen.mouse_shape != self.screen_last.mouse_shape) { 669 try tty.print( 670 ctlseqs.osc22_mouse_shape, 671 .{@tagName(self.screen.mouse_shape)}, 672 ); 673 self.screen_last.mouse_shape = self.screen.mouse_shape; 674 } 675 if (self.screen.cursor_shape != self.screen_last.cursor_shape) { 676 try tty.print( 677 ctlseqs.cursor_shape, 678 .{@intFromEnum(self.screen.cursor_shape)}, 679 ); 680 self.screen_last.cursor_shape = self.screen.cursor_shape; 681 } 682} 683 684fn enableKittyKeyboard(self: *Vaxis, tty: AnyWriter, flags: Key.KittyFlags) !void { 685 const flag_int: u5 = @bitCast(flags); 686 try tty.print(ctlseqs.csi_u_push, .{flag_int}); 687 self.state.kitty_keyboard = true; 688} 689 690/// send a system notification 691pub fn notify(_: *Vaxis, tty: AnyWriter, title: ?[]const u8, body: []const u8) !void { 692 if (title) |t| 693 try tty.print(ctlseqs.osc777_notify, .{ t, body }) 694 else 695 try tty.print(ctlseqs.osc9_notify, .{body}); 696} 697 698/// sets the window title 699pub fn setTitle(_: *Vaxis, tty: AnyWriter, title: []const u8) !void { 700 try tty.print(ctlseqs.osc2_set_title, .{title}); 701} 702 703// turn bracketed paste on or off. An event will be sent at the 704// beginning and end of a detected paste. All keystrokes between these 705// events were pasted 706pub fn setBracketedPaste(self: *Vaxis, tty: AnyWriter, enable: bool) !void { 707 const seq = if (enable) 708 ctlseqs.bp_set 709 else 710 ctlseqs.bp_reset; 711 try tty.writeAll(seq); 712 self.state.bracketed_paste = enable; 713} 714 715/// set the mouse shape 716pub fn setMouseShape(self: *Vaxis, shape: Shape) void { 717 self.screen.mouse_shape = shape; 718} 719 720/// Change the mouse reporting mode 721pub fn setMouseMode(self: *Vaxis, tty: AnyWriter, enable: bool) !void { 722 if (enable) { 723 self.state.mouse = true; 724 if (self.caps.sgr_pixels) { 725 log.debug("enabling mouse mode: pixel coordinates", .{}); 726 self.state.pixel_mouse = true; 727 try tty.writeAll(ctlseqs.mouse_set_pixels); 728 } else { 729 log.debug("enabling mouse mode: cell coordinates", .{}); 730 try tty.writeAll(ctlseqs.mouse_set); 731 } 732 } else { 733 try tty.writeAll(ctlseqs.mouse_reset); 734 } 735} 736 737/// Translate pixel mouse coordinates to cell + offset 738pub fn translateMouse(self: Vaxis, mouse: Mouse) Mouse { 739 if (self.screen.width == 0 or self.screen.height == 0) return mouse; 740 var result = mouse; 741 if (self.state.pixel_mouse) { 742 std.debug.assert(mouse.xoffset == 0); 743 std.debug.assert(mouse.yoffset == 0); 744 const xpos = mouse.col; 745 const ypos = mouse.row; 746 const xextra = self.screen.width_pix % self.screen.width; 747 const yextra = self.screen.height_pix % self.screen.height; 748 const xcell = (self.screen.width_pix - xextra) / self.screen.width; 749 const ycell = (self.screen.height_pix - yextra) / self.screen.height; 750 if (xcell == 0 or ycell == 0) return mouse; 751 result.col = xpos / xcell; 752 result.row = ypos / ycell; 753 result.xoffset = xpos % xcell; 754 result.yoffset = ypos % ycell; 755 } 756 return result; 757} 758 759/// Transmit an image using the local filesystem. Allocates only for base64 encoding 760pub fn transmitLocalImagePath( 761 self: *Vaxis, 762 allocator: std.mem.Allocator, 763 tty: AnyWriter, 764 payload: []const u8, 765 width: u16, 766 height: u16, 767 medium: Image.TransmitMedium, 768 format: Image.TransmitFormat, 769) !Image { 770 if (!self.caps.kitty_graphics) return error.NoGraphicsCapability; 771 772 defer self.next_img_id += 1; 773 774 const id = self.next_img_id; 775 776 const size = base64Encoder.calcSize(payload.len); 777 if (size >= 4096) return error.PathTooLong; 778 779 const buf = try allocator.alloc(u8, size); 780 const encoded = base64Encoder.encode(buf, payload); 781 defer allocator.free(buf); 782 783 const medium_char: u8 = switch (medium) { 784 .file => 'f', 785 .temp_file => 't', 786 .shared_mem => 's', 787 }; 788 789 switch (format) { 790 .rgb => { 791 try tty.print( 792 "\x1b_Gf=24,s={d},v={d},i={d},t={c};{s}\x1b\\", 793 .{ width, height, id, medium_char, encoded }, 794 ); 795 }, 796 .rgba => { 797 try tty.print( 798 "\x1b_Gf=32,s={d},v={d},i={d},t={c};{s}\x1b\\", 799 .{ width, height, id, medium_char, encoded }, 800 ); 801 }, 802 .png => { 803 try tty.print( 804 "\x1b_Gf=100,i={d},t={c};{s}\x1b\\", 805 .{ id, medium_char, encoded }, 806 ); 807 }, 808 } 809 return .{ 810 .id = id, 811 .width = width, 812 .height = height, 813 }; 814} 815 816/// Transmit an image which has been pre-base64 encoded 817pub fn transmitPreEncodedImage( 818 self: *Vaxis, 819 tty: AnyWriter, 820 bytes: []const u8, 821 width: u16, 822 height: u16, 823 format: Image.TransmitFormat, 824) !Image { 825 if (!self.caps.kitty_graphics) return error.NoGraphicsCapability; 826 827 defer self.next_img_id += 1; 828 const id = self.next_img_id; 829 830 const fmt: u8 = switch (format) { 831 .rgb => 24, 832 .rgba => 32, 833 .png => 100, 834 }; 835 836 if (bytes.len < 4096) { 837 try tty.print( 838 "\x1b_Gf={d},s={d},v={d},i={d};{s}\x1b\\", 839 .{ 840 fmt, 841 width, 842 height, 843 id, 844 bytes, 845 }, 846 ); 847 } else { 848 var n: usize = 4096; 849 850 try tty.print( 851 "\x1b_Gf={d},s={d},v={d},i={d},m=1;{s}\x1b\\", 852 .{ fmt, width, height, id, bytes[0..n] }, 853 ); 854 while (n < bytes.len) : (n += 4096) { 855 const end: usize = @min(n + 4096, bytes.len); 856 const m: u2 = if (end == bytes.len) 0 else 1; 857 try tty.print( 858 "\x1b_Gm={d};{s}\x1b\\", 859 .{ 860 m, 861 bytes[n..end], 862 }, 863 ); 864 } 865 } 866 return .{ 867 .id = id, 868 .width = width, 869 .height = height, 870 }; 871} 872 873pub fn transmitImage( 874 self: *Vaxis, 875 alloc: std.mem.Allocator, 876 tty: AnyWriter, 877 img: *zigimg.Image, 878 format: Image.TransmitFormat, 879) !Image { 880 if (!self.caps.kitty_graphics) return error.NoGraphicsCapability; 881 882 var arena = std.heap.ArenaAllocator.init(alloc); 883 defer arena.deinit(); 884 885 const buf = switch (format) { 886 .png => png: { 887 const png_buf = try arena.allocator().alloc(u8, img.imageByteSize()); 888 const png = try img.writeToMemory(png_buf, .{ .png = .{} }); 889 break :png png; 890 }, 891 .rgb => rgb: { 892 try img.convert(.rgb24); 893 break :rgb img.rawBytes(); 894 }, 895 .rgba => rgba: { 896 try img.convert(.rgba32); 897 break :rgba img.rawBytes(); 898 }, 899 }; 900 901 const b64_buf = try arena.allocator().alloc(u8, base64Encoder.calcSize(buf.len)); 902 const encoded = base64Encoder.encode(b64_buf, buf); 903 904 return self.transmitPreEncodedImage(tty, encoded, @intCast(img.width), @intCast(img.height), format); 905} 906 907pub fn loadImage( 908 self: *Vaxis, 909 alloc: std.mem.Allocator, 910 tty: AnyWriter, 911 src: Image.Source, 912) !Image { 913 if (!self.caps.kitty_graphics) return error.NoGraphicsCapability; 914 915 var img = switch (src) { 916 .path => |path| try zigimg.Image.fromFilePath(alloc, path), 917 .mem => |bytes| try zigimg.Image.fromMemory(alloc, bytes), 918 }; 919 defer img.deinit(); 920 return self.transmitImage(alloc, tty, &img, .png); 921} 922 923/// deletes an image from the terminal's memory 924pub fn freeImage(_: Vaxis, tty: AnyWriter, id: u32) void { 925 tty.print("\x1b_Ga=d,d=I,i={d};\x1b\\", .{id}) catch |err| { 926 log.err("couldn't delete image {d}: {}", .{ id, err }); 927 return; 928 }; 929} 930 931pub fn copyToSystemClipboard(_: Vaxis, tty: AnyWriter, text: []const u8, encode_allocator: std.mem.Allocator) !void { 932 const encoder = std.base64.standard.Encoder; 933 const size = encoder.calcSize(text.len); 934 const buf = try encode_allocator.alloc(u8, size); 935 const b64 = encoder.encode(buf, text); 936 defer encode_allocator.free(buf); 937 try tty.print( 938 ctlseqs.osc52_clipboard_copy, 939 .{b64}, 940 ); 941} 942 943pub fn requestSystemClipboard(self: Vaxis, tty: AnyWriter) !void { 944 if (self.opts.system_clipboard_allocator == null) return error.NoClipboardAllocator; 945 try tty.print( 946 ctlseqs.osc52_clipboard_request, 947 .{}, 948 ); 949} 950 951/// Set the default terminal foreground color 952pub fn setTerminalForegroundColor(self: *Vaxis, tty: AnyWriter, rgb: [3]u8) !void { 953 try tty.print(ctlseqs.osc10_set, .{ rgb[0], rgb[0], rgb[1], rgb[1], rgb[2], rgb[2] }); 954 self.state.changed_default_fg = true; 955} 956 957/// Set the default terminal background color 958pub fn setTerminalBackgroundColor(self: *Vaxis, tty: AnyWriter, rgb: [3]u8) !void { 959 try tty.print(ctlseqs.osc11_set, .{ rgb[0], rgb[0], rgb[1], rgb[1], rgb[2], rgb[2] }); 960 self.state.changed_default_bg = true; 961} 962 963/// Set the terminal cursor color 964pub fn setTerminalCursorColor(self: *Vaxis, tty: AnyWriter, rgb: [3]u8) !void { 965 try tty.print(ctlseqs.osc12_set, .{ rgb[0], rgb[0], rgb[1], rgb[1], rgb[2], rgb[2] }); 966 self.state.changed_cursor_color = true; 967} 968 969/// Request a color report from the terminal. Note: not all terminals support 970/// reporting colors. It is always safe to try, but you may not receive a 971/// response. 972pub fn queryColor(_: Vaxis, tty: AnyWriter, kind: Cell.Color.Kind) !void { 973 switch (kind) { 974 .fg => try tty.writeAll(ctlseqs.osc10_query), 975 .bg => try tty.writeAll(ctlseqs.osc11_query), 976 .cursor => try tty.writeAll(ctlseqs.osc12_query), 977 .index => |idx| try tty.print(ctlseqs.osc4_query, .{idx}), 978 } 979} 980 981/// Subscribe to color theme updates. A `color_scheme: Color.Scheme` tag must 982/// exist on your Event type to receive the response. This is a queried 983/// capability. Support can be detected by checking the value of 984/// vaxis.caps.color_scheme_updates. The initial scheme will be reported when 985/// subscribing. 986pub fn subscribeToColorSchemeUpdates(self: *Vaxis, tty: AnyWriter) !void { 987 try tty.writeAll(ctlseqs.color_scheme_request); 988 try tty.writeAll(ctlseqs.color_scheme_set); 989 self.state.color_scheme_updates = true; 990} 991 992pub fn deviceStatusReport(_: Vaxis, tty: AnyWriter) !void { 993 try tty.writeAll(ctlseqs.device_status_report); 994} 995 996/// prettyPrint is used to print the contents of the Screen to the tty. The state is not stored, and 997/// the cursor will be put on the next line after the last line is printed. This is useful to 998/// sequentially print data in a styled format to eg. stdout. This function returns an error if you 999/// are not in the alt screen. The cursor is always hidden, and mouse shapes are not available 1000pub fn prettyPrint(self: *Vaxis, tty: AnyWriter) !void { 1001 if (self.state.alt_screen) return error.NotInPrimaryScreen; 1002 1003 try tty.writeAll(ctlseqs.hide_cursor); 1004 try tty.writeAll(ctlseqs.sync_set); 1005 defer tty.writeAll(ctlseqs.sync_reset) catch {}; 1006 try tty.writeAll(ctlseqs.sgr_reset); 1007 defer tty.writeAll(ctlseqs.sgr_reset) catch {}; 1008 1009 var reposition: bool = false; 1010 var row: u16 = 0; 1011 var col: u16 = 0; 1012 var cursor: Style = .{}; 1013 var link: Hyperlink = .{}; 1014 var cursor_pos: struct { 1015 row: u16 = 0, 1016 col: u16 = 0, 1017 } = .{}; 1018 1019 var i: u16 = 0; 1020 while (i < self.screen.buf.len) { 1021 const cell = self.screen.buf[i]; 1022 const w = blk: { 1023 if (cell.char.width != 0) break :blk cell.char.width; 1024 1025 const method: gwidth.Method = self.caps.unicode; 1026 const width = gwidth.gwidth(cell.char.grapheme, method, &self.unicode.width_data); 1027 break :blk @max(1, width); 1028 }; 1029 defer { 1030 // advance by the width of this char mod 1 1031 std.debug.assert(w > 0); 1032 var j = i + 1; 1033 while (j < i + w) : (j += 1) { 1034 if (j >= self.screen_last.buf.len) break; 1035 self.screen_last.buf[j].skipped = true; 1036 } 1037 col += w; 1038 i += w; 1039 } 1040 if (col >= self.screen.width) { 1041 row += 1; 1042 col = 0; 1043 // Rely on terminal wrapping to reposition into next row instead of forcing it 1044 if (!cell.wrapped) 1045 reposition = true; 1046 } 1047 if (cell.default) { 1048 reposition = true; 1049 continue; 1050 } 1051 defer { 1052 cursor = cell.style; 1053 link = cell.link; 1054 } 1055 1056 // reposition the cursor, if needed 1057 if (reposition) { 1058 reposition = false; 1059 link = .{}; 1060 if (cursor_pos.row == row) { 1061 const n = col - cursor_pos.col; 1062 if (n > 0) 1063 try tty.print(ctlseqs.cuf, .{n}); 1064 } else { 1065 const n = row - cursor_pos.row; 1066 try tty.writeByteNTimes('\n', n); 1067 try tty.writeByte('\r'); 1068 if (col > 0) 1069 try tty.print(ctlseqs.cuf, .{col}); 1070 } 1071 } 1072 1073 if (cell.image) |img| { 1074 try tty.print( 1075 ctlseqs.kitty_graphics_preamble, 1076 .{img.img_id}, 1077 ); 1078 if (img.options.pixel_offset) |offset| { 1079 try tty.print( 1080 ",X={d},Y={d}", 1081 .{ offset.x, offset.y }, 1082 ); 1083 } 1084 if (img.options.clip_region) |clip| { 1085 if (clip.x) |x| 1086 try tty.print(",x={d}", .{x}); 1087 if (clip.y) |y| 1088 try tty.print(",y={d}", .{y}); 1089 if (clip.width) |width| 1090 try tty.print(",w={d}", .{width}); 1091 if (clip.height) |height| 1092 try tty.print(",h={d}", .{height}); 1093 } 1094 if (img.options.size) |size| { 1095 if (size.rows) |rows| 1096 try tty.print(",r={d}", .{rows}); 1097 if (size.cols) |cols| 1098 try tty.print(",c={d}", .{cols}); 1099 } 1100 if (img.options.z_index) |z| { 1101 try tty.print(",z={d}", .{z}); 1102 } 1103 try tty.writeAll(ctlseqs.kitty_graphics_closing); 1104 } 1105 1106 // something is different, so let's loop through everything and 1107 // find out what 1108 1109 // foreground 1110 if (!Cell.Color.eql(cursor.fg, cell.style.fg)) { 1111 switch (cell.style.fg) { 1112 .default => try tty.writeAll(ctlseqs.fg_reset), 1113 .index => |idx| { 1114 switch (idx) { 1115 0...7 => try tty.print(ctlseqs.fg_base, .{idx}), 1116 8...15 => try tty.print(ctlseqs.fg_bright, .{idx - 8}), 1117 else => { 1118 switch (self.sgr) { 1119 .standard => try tty.print(ctlseqs.fg_indexed, .{idx}), 1120 .legacy => try tty.print(ctlseqs.fg_indexed_legacy, .{idx}), 1121 } 1122 }, 1123 } 1124 }, 1125 .rgb => |rgb| { 1126 switch (self.sgr) { 1127 .standard => try tty.print(ctlseqs.fg_rgb, .{ rgb[0], rgb[1], rgb[2] }), 1128 .legacy => try tty.print(ctlseqs.fg_rgb_legacy, .{ rgb[0], rgb[1], rgb[2] }), 1129 } 1130 }, 1131 } 1132 } 1133 // background 1134 if (!Cell.Color.eql(cursor.bg, cell.style.bg)) { 1135 switch (cell.style.bg) { 1136 .default => try tty.writeAll(ctlseqs.bg_reset), 1137 .index => |idx| { 1138 switch (idx) { 1139 0...7 => try tty.print(ctlseqs.bg_base, .{idx}), 1140 8...15 => try tty.print(ctlseqs.bg_bright, .{idx - 8}), 1141 else => { 1142 switch (self.sgr) { 1143 .standard => try tty.print(ctlseqs.bg_indexed, .{idx}), 1144 .legacy => try tty.print(ctlseqs.bg_indexed_legacy, .{idx}), 1145 } 1146 }, 1147 } 1148 }, 1149 .rgb => |rgb| { 1150 switch (self.sgr) { 1151 .standard => try tty.print(ctlseqs.bg_rgb, .{ rgb[0], rgb[1], rgb[2] }), 1152 .legacy => try tty.print(ctlseqs.bg_rgb_legacy, .{ rgb[0], rgb[1], rgb[2] }), 1153 } 1154 }, 1155 } 1156 } 1157 // underline color 1158 if (!Cell.Color.eql(cursor.ul, cell.style.ul)) { 1159 switch (cell.style.ul) { 1160 .default => try tty.writeAll(ctlseqs.ul_reset), 1161 .index => |idx| { 1162 switch (self.sgr) { 1163 .standard => try tty.print(ctlseqs.ul_indexed, .{idx}), 1164 .legacy => try tty.print(ctlseqs.ul_indexed_legacy, .{idx}), 1165 } 1166 }, 1167 .rgb => |rgb| { 1168 switch (self.sgr) { 1169 .standard => try tty.print(ctlseqs.ul_rgb, .{ rgb[0], rgb[1], rgb[2] }), 1170 .legacy => try tty.print(ctlseqs.ul_rgb_legacy, .{ rgb[0], rgb[1], rgb[2] }), 1171 } 1172 }, 1173 } 1174 } 1175 // underline style 1176 if (cursor.ul_style != cell.style.ul_style) { 1177 const seq = switch (cell.style.ul_style) { 1178 .off => ctlseqs.ul_off, 1179 .single => ctlseqs.ul_single, 1180 .double => ctlseqs.ul_double, 1181 .curly => ctlseqs.ul_curly, 1182 .dotted => ctlseqs.ul_dotted, 1183 .dashed => ctlseqs.ul_dashed, 1184 }; 1185 try tty.writeAll(seq); 1186 } 1187 // bold 1188 if (cursor.bold != cell.style.bold) { 1189 const seq = switch (cell.style.bold) { 1190 true => ctlseqs.bold_set, 1191 false => ctlseqs.bold_dim_reset, 1192 }; 1193 try tty.writeAll(seq); 1194 if (cell.style.dim) { 1195 try tty.writeAll(ctlseqs.dim_set); 1196 } 1197 } 1198 // dim 1199 if (cursor.dim != cell.style.dim) { 1200 const seq = switch (cell.style.dim) { 1201 true => ctlseqs.dim_set, 1202 false => ctlseqs.bold_dim_reset, 1203 }; 1204 try tty.writeAll(seq); 1205 if (cell.style.bold) { 1206 try tty.writeAll(ctlseqs.bold_set); 1207 } 1208 } 1209 // dim 1210 if (cursor.italic != cell.style.italic) { 1211 const seq = switch (cell.style.italic) { 1212 true => ctlseqs.italic_set, 1213 false => ctlseqs.italic_reset, 1214 }; 1215 try tty.writeAll(seq); 1216 } 1217 // dim 1218 if (cursor.blink != cell.style.blink) { 1219 const seq = switch (cell.style.blink) { 1220 true => ctlseqs.blink_set, 1221 false => ctlseqs.blink_reset, 1222 }; 1223 try tty.writeAll(seq); 1224 } 1225 // reverse 1226 if (cursor.reverse != cell.style.reverse) { 1227 const seq = switch (cell.style.reverse) { 1228 true => ctlseqs.reverse_set, 1229 false => ctlseqs.reverse_reset, 1230 }; 1231 try tty.writeAll(seq); 1232 } 1233 // invisible 1234 if (cursor.invisible != cell.style.invisible) { 1235 const seq = switch (cell.style.invisible) { 1236 true => ctlseqs.invisible_set, 1237 false => ctlseqs.invisible_reset, 1238 }; 1239 try tty.writeAll(seq); 1240 } 1241 // strikethrough 1242 if (cursor.strikethrough != cell.style.strikethrough) { 1243 const seq = switch (cell.style.strikethrough) { 1244 true => ctlseqs.strikethrough_set, 1245 false => ctlseqs.strikethrough_reset, 1246 }; 1247 try tty.writeAll(seq); 1248 } 1249 1250 // url 1251 if (!std.mem.eql(u8, link.uri, cell.link.uri)) { 1252 var ps = cell.link.params; 1253 if (cell.link.uri.len == 0) { 1254 // Empty out the params no matter what if we don't have 1255 // a url 1256 ps = ""; 1257 } 1258 try tty.print(ctlseqs.osc8, .{ ps, cell.link.uri }); 1259 } 1260 try tty.writeAll(cell.char.grapheme); 1261 cursor_pos.col = col + w; 1262 cursor_pos.row = row; 1263 } 1264 try tty.writeAll("\r\n"); 1265} 1266 1267/// Set the terminal's current working directory 1268pub fn setTerminalWorkingDirectory(_: *Vaxis, tty: AnyWriter, path: []const u8) !void { 1269 if (path.len == 0 or path[0] != '/') 1270 return error.InvalidAbsolutePath; 1271 const hostname = switch (builtin.os.tag) { 1272 .windows => null, 1273 else => std.posix.getenv("HOSTNAME"), 1274 } orelse "localhost"; 1275 1276 const uri: std.Uri = .{ 1277 .scheme = "file", 1278 .host = .{ .raw = hostname }, 1279 .path = .{ .raw = path }, 1280 }; 1281 try tty.print(ctlseqs.osc7, .{uri}); 1282}