this repo has no description
13
fork

Configure Feed

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

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