this repo has no description
13
fork

Configure Feed

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

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