this repo has no description
13
fork

Configure Feed

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

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