a modern tui library written in zig
at main 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}