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: 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}