a modern tui library written in zig
at v0.4.1 877 lines 30 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 cursor: struct { 90 row: usize = 0, 91 col: usize = 0, 92 } = .{}, 93} = .{}, 94 95/// Initialize Vaxis with runtime options 96pub fn init(alloc: std.mem.Allocator, opts: Options) !Vaxis { 97 return .{ 98 .opts = opts, 99 .screen = .{}, 100 .screen_last = .{}, 101 .render_timer = try std.time.Timer.start(), 102 .unicode = try Unicode.init(alloc), 103 }; 104} 105 106/// Resets the terminal to it's original state. If an allocator is 107/// passed, this will free resources associated with Vaxis. This is left as an 108/// optional so applications can choose to not free resources when the 109/// application will be exiting anyways 110pub fn deinit(self: *Vaxis, alloc: ?std.mem.Allocator, tty: AnyWriter) void { 111 self.resetState(tty) catch {}; 112 113 // always show the cursor on exit 114 tty.writeAll(ctlseqs.show_cursor) catch {}; 115 if (alloc) |a| { 116 self.screen.deinit(a); 117 self.screen_last.deinit(a); 118 } 119 if (self.renders > 0) { 120 const tpr = @divTrunc(self.render_dur, self.renders); 121 log.debug("total renders = {d}\r", .{self.renders}); 122 log.debug("microseconds per render = {d}\r", .{tpr}); 123 } 124 self.unicode.deinit(); 125} 126 127/// resets enabled features, sends cursor to home and clears below cursor 128pub fn resetState(self: *Vaxis, tty: AnyWriter) !void { 129 if (self.state.kitty_keyboard) { 130 try tty.writeAll(ctlseqs.csi_u_pop); 131 self.state.kitty_keyboard = false; 132 } 133 if (self.state.mouse) { 134 try self.setMouseMode(tty, false); 135 } 136 if (self.state.bracketed_paste) { 137 try self.setBracketedPaste(tty, false); 138 } 139 if (self.state.alt_screen) { 140 try tty.writeAll(ctlseqs.home); 141 try tty.writeAll(ctlseqs.erase_below_cursor); 142 try self.exitAltScreen(tty); 143 } else { 144 try tty.writeByte('\r'); 145 var i: usize = 0; 146 while (i < self.state.cursor.row) : (i += 1) { 147 try tty.writeAll(ctlseqs.ri); 148 } 149 try tty.writeAll(ctlseqs.erase_below_cursor); 150 } 151 if (self.state.color_scheme_updates) { 152 try tty.writeAll(ctlseqs.color_scheme_reset); 153 self.state.color_scheme_updates = false; 154 } 155 if (self.state.in_band_resize) { 156 try tty.writeAll(ctlseqs.in_band_resize_reset); 157 self.state.in_band_resize = false; 158 } 159} 160 161/// resize allocates a slice of cells equal to the number of cells 162/// required to display the screen (ie width x height). Any previous screen is 163/// freed when resizing. The cursor will be sent to it's home position and a 164/// hardware clear-below-cursor will be sent 165pub fn resize( 166 self: *Vaxis, 167 alloc: std.mem.Allocator, 168 tty: AnyWriter, 169 winsize: Winsize, 170) !void { 171 log.debug("resizing screen: width={d} height={d}", .{ winsize.cols, winsize.rows }); 172 self.screen.deinit(alloc); 173 self.screen = try Screen.init(alloc, winsize, &self.unicode); 174 self.screen.width_method = self.caps.unicode; 175 // try self.screen.int(alloc, winsize.cols, winsize.rows); 176 // we only init our current screen. This has the effect of redrawing 177 // every cell 178 self.screen_last.deinit(alloc); 179 self.screen_last = try InternalScreen.init(alloc, winsize.cols, winsize.rows); 180 if (self.state.alt_screen) 181 try tty.writeAll(ctlseqs.home) 182 else { 183 try tty.writeBytesNTimes(ctlseqs.ri, self.state.cursor.row); 184 try tty.writeByte('\r'); 185 } 186 self.state.cursor.row = 0; 187 self.state.cursor.col = 0; 188 try tty.writeAll(ctlseqs.sgr_reset ++ ctlseqs.erase_below_cursor); 189} 190 191/// returns a Window comprising of the entire terminal screen 192pub fn window(self: *Vaxis) Window { 193 return .{ 194 .x_off = 0, 195 .y_off = 0, 196 .width = self.screen.width, 197 .height = self.screen.height, 198 .screen = &self.screen, 199 }; 200} 201 202/// enter the alternate screen. The alternate screen will automatically 203/// be exited if calling deinit while in the alt screen 204pub fn enterAltScreen(self: *Vaxis, tty: AnyWriter) !void { 205 try tty.writeAll(ctlseqs.smcup); 206 self.state.alt_screen = true; 207} 208 209/// exit the alternate screen 210pub fn exitAltScreen(self: *Vaxis, tty: AnyWriter) !void { 211 try tty.writeAll(ctlseqs.rmcup); 212 self.state.alt_screen = false; 213} 214 215/// write queries to the terminal to determine capabilities. Individual 216/// capabilities will be delivered to the client and possibly intercepted by 217/// Vaxis to enable features. 218/// 219/// This call will block until Vaxis.query_futex is woken up, or the timeout. 220/// Event loops can wake up this futex when cap_da1 is received 221pub fn queryTerminal(self: *Vaxis, tty: AnyWriter, timeout_ns: u64) !void { 222 try self.queryTerminalSend(tty); 223 // 1 second timeout 224 std.Thread.Futex.timedWait(&self.query_futex, 0, timeout_ns) catch {}; 225 try self.enableDetectedFeatures(tty); 226} 227 228/// write queries to the terminal to determine capabilities. This function 229/// is only for use with a custom main loop. Call Vaxis.queryTerminal() if 230/// you are using Loop.run() 231pub fn queryTerminalSend(_: Vaxis, tty: AnyWriter) !void { 232 233 // TODO: re-enable this 234 // const colorterm = std.posix.getenv("COLORTERM") orelse ""; 235 // if (std.mem.eql(u8, colorterm, "truecolor") or 236 // std.mem.eql(u8, colorterm, "24bit")) 237 // { 238 // if (@hasField(Event, "cap_rgb")) { 239 // self.postEvent(.cap_rgb); 240 // } 241 // } 242 243 // TODO: XTGETTCAP queries ("RGB", "Smulx") 244 // TODO: decide if we actually want to query for focus and sync. It 245 // doesn't hurt to blindly use them 246 // _ = try tty.write(ctlseqs.decrqm_focus); 247 // _ = try tty.write(ctlseqs.decrqm_sync); 248 try tty.writeAll(ctlseqs.decrqm_sgr_pixels ++ 249 ctlseqs.decrqm_unicode ++ 250 ctlseqs.decrqm_color_scheme ++ 251 ctlseqs.in_band_resize_set ++ 252 ctlseqs.xtversion ++ 253 ctlseqs.csi_u_query ++ 254 ctlseqs.kitty_graphics_query ++ 255 ctlseqs.primary_device_attrs); 256} 257 258/// Enable features detected by responses to queryTerminal. This function 259/// is only for use with a custom main loop. Call Vaxis.queryTerminal() if 260/// you are using Loop.run() 261pub fn enableDetectedFeatures(self: *Vaxis, tty: AnyWriter) !void { 262 switch (builtin.os.tag) { 263 .windows => { 264 // No feature detection on windows. We just hard enable some knowns for ConPTY 265 self.sgr = .legacy; 266 }, 267 else => { 268 // Apply any environment variables 269 if (std.posix.getenv("TERMUX_VERSION")) |_| 270 self.sgr = .legacy; 271 if (std.posix.getenv("VHS_RECORD")) |_| { 272 self.caps.unicode = .wcwidth; 273 self.caps.kitty_keyboard = false; 274 self.sgr = .legacy; 275 } 276 if (std.posix.getenv("VAXIS_FORCE_LEGACY_SGR")) |_| 277 self.sgr = .legacy; 278 if (std.posix.getenv("VAXIS_FORCE_WCWIDTH")) |_| 279 self.caps.unicode = .wcwidth; 280 if (std.posix.getenv("VAXIS_FORCE_UNICODE")) |_| 281 self.caps.unicode = .unicode; 282 283 // enable detected features 284 if (self.caps.kitty_keyboard) { 285 try self.enableKittyKeyboard(tty, self.opts.kitty_keyboard_flags); 286 } 287 if (self.caps.unicode == .unicode) { 288 try tty.writeAll(ctlseqs.unicode_set); 289 } 290 }, 291 } 292} 293 294// the next render call will refresh the entire screen 295pub fn queueRefresh(self: *Vaxis) void { 296 self.refresh = true; 297} 298 299/// draws the screen to the terminal 300pub fn render(self: *Vaxis, tty: AnyWriter) !void { 301 self.renders += 1; 302 self.render_timer.reset(); 303 defer { 304 self.render_dur += self.render_timer.read() / std.time.ns_per_us; 305 } 306 307 defer self.refresh = false; 308 309 // Set up sync before we write anything 310 // TODO: optimize sync so we only sync _when we have changes_. This 311 // requires a smarter buffered writer, we'll probably have to write 312 // our own 313 try tty.writeAll(ctlseqs.sync_set); 314 defer tty.writeAll(ctlseqs.sync_reset) catch {}; 315 316 // Send the cursor to 0,0 317 // TODO: this needs to move after we optimize writes. We only do 318 // this if we have an update to make. We also need to hide cursor 319 // and then reshow it if needed 320 try tty.writeAll(ctlseqs.hide_cursor); 321 if (self.state.alt_screen) 322 try tty.writeAll(ctlseqs.home) 323 else { 324 try tty.writeByte('\r'); 325 try tty.writeBytesNTimes(ctlseqs.ri, self.state.cursor.row); 326 } 327 try tty.writeAll(ctlseqs.sgr_reset); 328 329 // initialize some variables 330 var reposition: bool = false; 331 var row: usize = 0; 332 var col: usize = 0; 333 var cursor: Style = .{}; 334 var link: Hyperlink = .{}; 335 var cursor_pos: struct { 336 row: usize = 0, 337 col: usize = 0, 338 } = .{}; 339 340 // Clear all images 341 if (self.caps.kitty_graphics) 342 try tty.writeAll(ctlseqs.kitty_graphics_clear); 343 344 var i: usize = 0; 345 while (i < self.screen.buf.len) { 346 const cell = self.screen.buf[i]; 347 const w = blk: { 348 if (cell.char.width != 0) break :blk cell.char.width; 349 350 const method: gwidth.Method = self.caps.unicode; 351 const width = gwidth.gwidth(cell.char.grapheme, method, &self.unicode.width_data) catch 1; 352 break :blk @max(1, width); 353 }; 354 defer { 355 // advance by the width of this char mod 1 356 std.debug.assert(w > 0); 357 var j = i + 1; 358 while (j < i + w) : (j += 1) { 359 if (j >= self.screen_last.buf.len) break; 360 self.screen_last.buf[j].skipped = true; 361 } 362 col += w; 363 i += w; 364 } 365 if (col >= self.screen.width) { 366 row += 1; 367 col = 0; 368 // Rely on terminal wrapping to reposition into next row instead of forcing it 369 if (!cell.wrapped) 370 reposition = true; 371 } 372 // If cell is the same as our last frame, we don't need to do 373 // anything 374 const last = self.screen_last.buf[i]; 375 if (!self.refresh and last.eql(cell) and !last.skipped and cell.image == null) { 376 reposition = true; 377 // Close any osc8 sequence we might be in before 378 // repositioning 379 if (link.uri.len > 0) { 380 try tty.writeAll(ctlseqs.osc8_clear); 381 } 382 continue; 383 } 384 self.screen_last.buf[i].skipped = false; 385 defer { 386 cursor = cell.style; 387 link = cell.link; 388 } 389 // Set this cell in the last frame 390 self.screen_last.writeCell(col, row, cell); 391 392 // reposition the cursor, if needed 393 if (reposition) { 394 reposition = false; 395 if (self.state.alt_screen) 396 try tty.print(ctlseqs.cup, .{ row + 1, col + 1 }) 397 else { 398 if (cursor_pos.row == row) { 399 const n = col - cursor_pos.col; 400 if (n > 0) 401 try tty.print(ctlseqs.cuf, .{n}); 402 } else { 403 const n = row - cursor_pos.row; 404 try tty.writeByteNTimes('\n', n); 405 try tty.writeByte('\r'); 406 if (col > 0) 407 try tty.print(ctlseqs.cuf, .{col}); 408 } 409 } 410 } 411 412 if (cell.image) |img| { 413 try tty.print( 414 ctlseqs.kitty_graphics_preamble, 415 .{img.img_id}, 416 ); 417 if (img.options.pixel_offset) |offset| { 418 try tty.print( 419 ",X={d},Y={d}", 420 .{ offset.x, offset.y }, 421 ); 422 } 423 if (img.options.clip_region) |clip| { 424 if (clip.x) |x| 425 try tty.print(",x={d}", .{x}); 426 if (clip.y) |y| 427 try tty.print(",y={d}", .{y}); 428 if (clip.width) |width| 429 try tty.print(",w={d}", .{width}); 430 if (clip.height) |height| 431 try tty.print(",h={d}", .{height}); 432 } 433 if (img.options.size) |size| { 434 if (size.rows) |rows| 435 try tty.print(",r={d}", .{rows}); 436 if (size.cols) |cols| 437 try tty.print(",c={d}", .{cols}); 438 } 439 if (img.options.z_index) |z| { 440 try tty.print(",z={d}", .{z}); 441 } 442 try tty.writeAll(ctlseqs.kitty_graphics_closing); 443 } 444 445 // something is different, so let's loop through everything and 446 // find out what 447 448 // foreground 449 if (!Cell.Color.eql(cursor.fg, cell.style.fg)) { 450 switch (cell.style.fg) { 451 .default => try tty.writeAll(ctlseqs.fg_reset), 452 .index => |idx| { 453 switch (idx) { 454 0...7 => try tty.print(ctlseqs.fg_base, .{idx}), 455 8...15 => try tty.print(ctlseqs.fg_bright, .{idx - 8}), 456 else => { 457 switch (self.sgr) { 458 .standard => try tty.print(ctlseqs.fg_indexed, .{idx}), 459 .legacy => try tty.print(ctlseqs.fg_indexed_legacy, .{idx}), 460 } 461 }, 462 } 463 }, 464 .rgb => |rgb| { 465 switch (self.sgr) { 466 .standard => try tty.print(ctlseqs.fg_rgb, .{ rgb[0], rgb[1], rgb[2] }), 467 .legacy => try tty.print(ctlseqs.fg_rgb_legacy, .{ rgb[0], rgb[1], rgb[2] }), 468 } 469 }, 470 } 471 } 472 // background 473 if (!Cell.Color.eql(cursor.bg, cell.style.bg)) { 474 switch (cell.style.bg) { 475 .default => try tty.writeAll(ctlseqs.bg_reset), 476 .index => |idx| { 477 switch (idx) { 478 0...7 => try tty.print(ctlseqs.bg_base, .{idx}), 479 8...15 => try tty.print(ctlseqs.bg_bright, .{idx - 8}), 480 else => { 481 switch (self.sgr) { 482 .standard => try tty.print(ctlseqs.bg_indexed, .{idx}), 483 .legacy => try tty.print(ctlseqs.bg_indexed_legacy, .{idx}), 484 } 485 }, 486 } 487 }, 488 .rgb => |rgb| { 489 switch (self.sgr) { 490 .standard => try tty.print(ctlseqs.bg_rgb, .{ rgb[0], rgb[1], rgb[2] }), 491 .legacy => try tty.print(ctlseqs.bg_rgb_legacy, .{ rgb[0], rgb[1], rgb[2] }), 492 } 493 }, 494 } 495 } 496 // underline color 497 if (!Cell.Color.eql(cursor.ul, cell.style.ul)) { 498 switch (cell.style.ul) { 499 .default => try tty.writeAll(ctlseqs.ul_reset), 500 .index => |idx| { 501 switch (self.sgr) { 502 .standard => try tty.print(ctlseqs.ul_indexed, .{idx}), 503 .legacy => try tty.print(ctlseqs.ul_indexed_legacy, .{idx}), 504 } 505 }, 506 .rgb => |rgb| { 507 switch (self.sgr) { 508 .standard => try tty.print(ctlseqs.ul_rgb, .{ rgb[0], rgb[1], rgb[2] }), 509 .legacy => try tty.print(ctlseqs.ul_rgb_legacy, .{ rgb[0], rgb[1], rgb[2] }), 510 } 511 }, 512 } 513 } 514 // underline style 515 if (cursor.ul_style != cell.style.ul_style) { 516 const seq = switch (cell.style.ul_style) { 517 .off => ctlseqs.ul_off, 518 .single => ctlseqs.ul_single, 519 .double => ctlseqs.ul_double, 520 .curly => ctlseqs.ul_curly, 521 .dotted => ctlseqs.ul_dotted, 522 .dashed => ctlseqs.ul_dashed, 523 }; 524 try tty.writeAll(seq); 525 } 526 // bold 527 if (cursor.bold != cell.style.bold) { 528 const seq = switch (cell.style.bold) { 529 true => ctlseqs.bold_set, 530 false => ctlseqs.bold_dim_reset, 531 }; 532 try tty.writeAll(seq); 533 if (cell.style.dim) { 534 try tty.writeAll(ctlseqs.dim_set); 535 } 536 } 537 // dim 538 if (cursor.dim != cell.style.dim) { 539 const seq = switch (cell.style.dim) { 540 true => ctlseqs.dim_set, 541 false => ctlseqs.bold_dim_reset, 542 }; 543 try tty.writeAll(seq); 544 if (cell.style.bold) { 545 try tty.writeAll(ctlseqs.bold_set); 546 } 547 } 548 // dim 549 if (cursor.italic != cell.style.italic) { 550 const seq = switch (cell.style.italic) { 551 true => ctlseqs.italic_set, 552 false => ctlseqs.italic_reset, 553 }; 554 try tty.writeAll(seq); 555 } 556 // dim 557 if (cursor.blink != cell.style.blink) { 558 const seq = switch (cell.style.blink) { 559 true => ctlseqs.blink_set, 560 false => ctlseqs.blink_reset, 561 }; 562 try tty.writeAll(seq); 563 } 564 // reverse 565 if (cursor.reverse != cell.style.reverse) { 566 const seq = switch (cell.style.reverse) { 567 true => ctlseqs.reverse_set, 568 false => ctlseqs.reverse_reset, 569 }; 570 try tty.writeAll(seq); 571 } 572 // invisible 573 if (cursor.invisible != cell.style.invisible) { 574 const seq = switch (cell.style.invisible) { 575 true => ctlseqs.invisible_set, 576 false => ctlseqs.invisible_reset, 577 }; 578 try tty.writeAll(seq); 579 } 580 // strikethrough 581 if (cursor.strikethrough != cell.style.strikethrough) { 582 const seq = switch (cell.style.strikethrough) { 583 true => ctlseqs.strikethrough_set, 584 false => ctlseqs.strikethrough_reset, 585 }; 586 try tty.writeAll(seq); 587 } 588 589 // url 590 if (!std.mem.eql(u8, link.uri, cell.link.uri)) { 591 var ps = cell.link.params; 592 if (cell.link.uri.len == 0) { 593 // Empty out the params no matter what if we don't have 594 // a url 595 ps = ""; 596 } 597 try tty.print(ctlseqs.osc8, .{ ps, cell.link.uri }); 598 } 599 try tty.writeAll(cell.char.grapheme); 600 cursor_pos.col = col + w; 601 cursor_pos.row = row; 602 } 603 if (self.screen.cursor_vis) { 604 if (self.state.alt_screen) { 605 try tty.print( 606 ctlseqs.cup, 607 .{ 608 self.screen.cursor_row + 1, 609 self.screen.cursor_col + 1, 610 }, 611 ); 612 } else { 613 // TODO: position cursor relative to current location 614 try tty.writeByte('\r'); 615 if (self.screen.cursor_row >= cursor_pos.row) 616 try tty.writeByteNTimes('\n', self.screen.cursor_row - cursor_pos.row) 617 else 618 try tty.writeBytesNTimes(ctlseqs.ri, cursor_pos.row - self.screen.cursor_row); 619 if (self.screen.cursor_col > 0) 620 try tty.print(ctlseqs.cuf, .{self.screen.cursor_col}); 621 } 622 self.state.cursor.row = self.screen.cursor_row; 623 self.state.cursor.col = self.screen.cursor_col; 624 try tty.writeAll(ctlseqs.show_cursor); 625 } else { 626 self.state.cursor.row = cursor_pos.row; 627 self.state.cursor.col = cursor_pos.col; 628 } 629 if (self.screen.mouse_shape != self.screen_last.mouse_shape) { 630 try tty.print( 631 ctlseqs.osc22_mouse_shape, 632 .{@tagName(self.screen.mouse_shape)}, 633 ); 634 self.screen_last.mouse_shape = self.screen.mouse_shape; 635 } 636 if (self.screen.cursor_shape != self.screen_last.cursor_shape) { 637 try tty.print( 638 ctlseqs.cursor_shape, 639 .{@intFromEnum(self.screen.cursor_shape)}, 640 ); 641 self.screen_last.cursor_shape = self.screen.cursor_shape; 642 } 643} 644 645fn enableKittyKeyboard(self: *Vaxis, tty: AnyWriter, flags: Key.KittyFlags) !void { 646 const flag_int: u5 = @bitCast(flags); 647 try tty.print(ctlseqs.csi_u_push, .{flag_int}); 648 self.state.kitty_keyboard = true; 649} 650 651/// send a system notification 652pub fn notify(_: *Vaxis, tty: AnyWriter, title: ?[]const u8, body: []const u8) !void { 653 if (title) |t| 654 try tty.print(ctlseqs.osc777_notify, .{ t, body }) 655 else 656 try tty.print(ctlseqs.osc9_notify, .{body}); 657} 658 659/// sets the window title 660pub fn setTitle(_: *Vaxis, tty: AnyWriter, title: []const u8) !void { 661 try tty.print(ctlseqs.osc2_set_title, .{title}); 662} 663 664// turn bracketed paste on or off. An event will be sent at the 665// beginning and end of a detected paste. All keystrokes between these 666// events were pasted 667pub fn setBracketedPaste(self: *Vaxis, tty: AnyWriter, enable: bool) !void { 668 const seq = if (enable) 669 ctlseqs.bp_set 670 else 671 ctlseqs.bp_reset; 672 try tty.writeAll(seq); 673 self.state.bracketed_paste = enable; 674} 675 676/// set the mouse shape 677pub fn setMouseShape(self: *Vaxis, shape: Shape) void { 678 self.screen.mouse_shape = shape; 679} 680 681/// Change the mouse reporting mode 682pub fn setMouseMode(self: *Vaxis, tty: AnyWriter, enable: bool) !void { 683 if (enable) { 684 self.state.mouse = true; 685 if (self.caps.sgr_pixels) { 686 log.debug("enabling mouse mode: pixel coordinates", .{}); 687 self.state.pixel_mouse = true; 688 try tty.writeAll(ctlseqs.mouse_set_pixels); 689 } else { 690 log.debug("enabling mouse mode: cell coordinates", .{}); 691 try tty.writeAll(ctlseqs.mouse_set); 692 } 693 } else { 694 try tty.writeAll(ctlseqs.mouse_reset); 695 } 696} 697 698/// Translate pixel mouse coordinates to cell + offset 699pub fn translateMouse(self: Vaxis, mouse: Mouse) Mouse { 700 if (self.screen.width == 0 or self.screen.height == 0) return mouse; 701 var result = mouse; 702 if (self.state.pixel_mouse) { 703 std.debug.assert(mouse.xoffset == 0); 704 std.debug.assert(mouse.yoffset == 0); 705 const xpos = mouse.col; 706 const ypos = mouse.row; 707 const xextra = self.screen.width_pix % self.screen.width; 708 const yextra = self.screen.height_pix % self.screen.height; 709 const xcell = (self.screen.width_pix - xextra) / self.screen.width; 710 const ycell = (self.screen.height_pix - yextra) / self.screen.height; 711 result.col = xpos / xcell; 712 result.row = ypos / ycell; 713 result.xoffset = xpos % xcell; 714 result.yoffset = ypos % ycell; 715 } 716 return result; 717} 718 719/// Transmit an image which has been pre-base64 encoded 720pub fn transmitPreEncodedImage( 721 self: *Vaxis, 722 tty: AnyWriter, 723 bytes: []const u8, 724 width: usize, 725 height: usize, 726 format: Image.TransmitFormat, 727) !Image { 728 defer self.next_img_id += 1; 729 const id = self.next_img_id; 730 731 const fmt: u8 = switch (format) { 732 .rgb => 24, 733 .rgba => 32, 734 .png => 100, 735 }; 736 737 if (bytes.len < 4096) { 738 try tty.print( 739 "\x1b_Gf={d},s={d},v={d},i={d};{s}\x1b\\", 740 .{ 741 fmt, 742 width, 743 height, 744 id, 745 bytes, 746 }, 747 ); 748 } else { 749 var n: usize = 4096; 750 751 try tty.print( 752 "\x1b_Gf={d},s={d},v={d},i={d},m=1;{s}\x1b\\", 753 .{ fmt, width, height, id, bytes[0..n] }, 754 ); 755 while (n < bytes.len) : (n += 4096) { 756 const end: usize = @min(n + 4096, bytes.len); 757 const m: u2 = if (end == bytes.len) 0 else 1; 758 try tty.print( 759 "\x1b_Gm={d};{s}\x1b\\", 760 .{ 761 m, 762 bytes[n..end], 763 }, 764 ); 765 } 766 } 767 return .{ 768 .id = id, 769 .width = width, 770 .height = height, 771 }; 772} 773 774pub fn transmitImage( 775 self: *Vaxis, 776 alloc: std.mem.Allocator, 777 tty: AnyWriter, 778 img: *zigimg.Image, 779 format: Image.TransmitFormat, 780) !Image { 781 if (!self.caps.kitty_graphics) return error.NoGraphicsCapability; 782 783 var arena = std.heap.ArenaAllocator.init(alloc); 784 defer arena.deinit(); 785 786 const buf = switch (format) { 787 .png => png: { 788 const png_buf = try arena.allocator().alloc(u8, img.imageByteSize()); 789 const png = try img.writeToMemory(png_buf, .{ .png = .{} }); 790 break :png png; 791 }, 792 .rgb => rgb: { 793 try img.convert(.rgb24); 794 break :rgb img.rawBytes(); 795 }, 796 .rgba => rgba: { 797 try img.convert(.rgba32); 798 break :rgba img.rawBytes(); 799 }, 800 }; 801 802 const b64_buf = try arena.allocator().alloc(u8, base64Encoder.calcSize(buf.len)); 803 const encoded = base64Encoder.encode(b64_buf, buf); 804 805 return self.transmitPreEncodedImage(tty, encoded, img.width, img.height, format); 806} 807 808pub fn loadImage( 809 self: *Vaxis, 810 alloc: std.mem.Allocator, 811 tty: AnyWriter, 812 src: Image.Source, 813) !Image { 814 if (!self.caps.kitty_graphics) return error.NoGraphicsCapability; 815 816 var img = switch (src) { 817 .path => |path| try zigimg.Image.fromFilePath(alloc, path), 818 .mem => |bytes| try zigimg.Image.fromMemory(alloc, bytes), 819 }; 820 defer img.deinit(); 821 return self.transmitImage(alloc, tty, &img, .png); 822} 823 824/// deletes an image from the terminal's memory 825pub fn freeImage(_: Vaxis, tty: AnyWriter, id: u32) void { 826 tty.print("\x1b_Ga=d,d=I,i={d};\x1b\\", .{id}) catch |err| { 827 log.err("couldn't delete image {d}: {}", .{ id, err }); 828 return; 829 }; 830} 831 832pub fn copyToSystemClipboard(_: Vaxis, tty: AnyWriter, text: []const u8, encode_allocator: std.mem.Allocator) !void { 833 const encoder = std.base64.standard.Encoder; 834 const size = encoder.calcSize(text.len); 835 const buf = try encode_allocator.alloc(u8, size); 836 const b64 = encoder.encode(buf, text); 837 defer encode_allocator.free(buf); 838 try tty.print( 839 ctlseqs.osc52_clipboard_copy, 840 .{b64}, 841 ); 842} 843 844pub fn requestSystemClipboard(self: Vaxis, tty: AnyWriter) !void { 845 if (self.opts.system_clipboard_allocator == null) return error.NoClipboardAllocator; 846 try tty.print( 847 ctlseqs.osc52_clipboard_request, 848 .{}, 849 ); 850} 851 852/// Request a color report from the terminal. Note: not all terminals support 853/// reporting colors. It is always safe to try, but you may not receive a 854/// response. 855pub fn queryColor(_: Vaxis, tty: AnyWriter, kind: Cell.Color.Kind) !void { 856 switch (kind) { 857 .fg => try tty.writeAll(ctlseqs.osc10_query), 858 .bg => try tty.writeAll(ctlseqs.osc11_query), 859 .cursor => try tty.writeAll(ctlseqs.osc12_query), 860 .index => |idx| try tty.print(ctlseqs.osc4_query, .{idx}), 861 } 862} 863 864/// Subscribe to color theme updates. A `color_scheme: Color.Scheme` tag must 865/// exist on your Event type to receive the response. This is a queried 866/// capability. Support can be detected by checking the value of 867/// vaxis.caps.color_scheme_updates. The initial scheme will be reported when 868/// subscribing. 869pub fn subscribeToColorSchemeUpdates(self: Vaxis, tty: AnyWriter) !void { 870 try tty.writeAll(ctlseqs.color_scheme_request); 871 try tty.writeAll(ctlseqs.color_scheme_set); 872 self.state.color_scheme_updates = true; 873} 874 875pub fn deviceStatusReport(_: Vaxis, tty: AnyWriter) !void { 876 try tty.writeAll(ctlseqs.device_status_report); 877}