this repo has no description
13
fork

Configure Feed

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

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