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