this repo has no description
13
fork

Configure Feed

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

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