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