this repo has no description
13
fork

Configure Feed

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

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