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