this repo has no description
13
fork

Configure Feed

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

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