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