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 changed_default_fg: bool = false,
90 changed_default_bg: bool = false,
91 cursor: struct {
92 row: usize = 0,
93 col: usize = 0,
94 } = .{},
95} = .{},
96
97/// Initialize Vaxis with runtime options
98pub fn init(alloc: std.mem.Allocator, opts: Options) !Vaxis {
99 return .{
100 .opts = opts,
101 .screen = .{},
102 .screen_last = .{},
103 .render_timer = try std.time.Timer.start(),
104 .unicode = try Unicode.init(alloc),
105 };
106}
107
108/// Resets the terminal to it's original state. If an allocator is
109/// passed, this will free resources associated with Vaxis. This is left as an
110/// optional so applications can choose to not free resources when the
111/// application will be exiting anyways
112pub fn deinit(self: *Vaxis, alloc: ?std.mem.Allocator, tty: AnyWriter) void {
113 self.resetState(tty) catch {};
114
115 // always show the cursor on exit
116 tty.writeAll(ctlseqs.show_cursor) catch {};
117 tty.writeAll(ctlseqs.sgr_reset) catch {};
118 if (self.screen.cursor_shape != .default) {
119 // In many terminals, `.default` will set to the configured cursor shape. Others, it will
120 // change to a blinking block.
121 tty.print(ctlseqs.cursor_shape, .{@intFromEnum(Cell.CursorShape.default)}) catch {};
122 }
123 if (alloc) |a| {
124 self.screen.deinit(a);
125 self.screen_last.deinit(a);
126 }
127 if (self.renders > 0) {
128 const tpr = @divTrunc(self.render_dur, self.renders);
129 log.debug("total renders = {d}\r", .{self.renders});
130 log.debug("microseconds per render = {d}\r", .{tpr});
131 }
132 self.unicode.deinit();
133}
134
135/// resets enabled features, sends cursor to home and clears below cursor
136pub fn resetState(self: *Vaxis, tty: AnyWriter) !void {
137 if (self.state.kitty_keyboard) {
138 try tty.writeAll(ctlseqs.csi_u_pop);
139 self.state.kitty_keyboard = false;
140 }
141 if (self.state.mouse) {
142 try self.setMouseMode(tty, false);
143 }
144 if (self.state.bracketed_paste) {
145 try self.setBracketedPaste(tty, false);
146 }
147 if (self.state.alt_screen) {
148 try tty.writeAll(ctlseqs.home);
149 try tty.writeAll(ctlseqs.erase_below_cursor);
150 try self.exitAltScreen(tty);
151 } else {
152 try tty.writeByte('\r');
153 var i: usize = 0;
154 while (i < self.state.cursor.row) : (i += 1) {
155 try tty.writeAll(ctlseqs.ri);
156 }
157 try tty.writeAll(ctlseqs.erase_below_cursor);
158 }
159 if (self.state.color_scheme_updates) {
160 try tty.writeAll(ctlseqs.color_scheme_reset);
161 self.state.color_scheme_updates = false;
162 }
163 if (self.state.in_band_resize) {
164 try tty.writeAll(ctlseqs.in_band_resize_reset);
165 self.state.in_band_resize = false;
166 }
167 if (self.state.changed_default_fg) {
168 try tty.writeAll(ctlseqs.osc10_reset);
169 self.state.changed_default_fg = false;
170 }
171 if (self.state.changed_default_bg) {
172 try tty.writeAll(ctlseqs.osc11_reset);
173 self.state.changed_default_bg = false;
174 }
175}
176
177/// resize allocates a slice of cells equal to the number of cells
178/// required to display the screen (ie width x height). Any previous screen is
179/// freed when resizing. The cursor will be sent to it's home position and a
180/// hardware clear-below-cursor will be sent
181pub fn resize(
182 self: *Vaxis,
183 alloc: std.mem.Allocator,
184 tty: AnyWriter,
185 winsize: Winsize,
186) !void {
187 log.debug("resizing screen: width={d} height={d}", .{ winsize.cols, winsize.rows });
188 self.screen.deinit(alloc);
189 self.screen = try Screen.init(alloc, winsize, &self.unicode);
190 self.screen.width_method = self.caps.unicode;
191 // try self.screen.int(alloc, winsize.cols, winsize.rows);
192 // we only init our current screen. This has the effect of redrawing
193 // every cell
194 self.screen_last.deinit(alloc);
195 self.screen_last = try InternalScreen.init(alloc, winsize.cols, winsize.rows);
196 if (self.state.alt_screen)
197 try tty.writeAll(ctlseqs.home)
198 else {
199 try tty.writeBytesNTimes(ctlseqs.ri, self.state.cursor.row);
200 try tty.writeByte('\r');
201 }
202 self.state.cursor.row = 0;
203 self.state.cursor.col = 0;
204 try tty.writeAll(ctlseqs.sgr_reset ++ ctlseqs.erase_below_cursor);
205}
206
207/// returns a Window comprising of the entire terminal screen
208pub fn window(self: *Vaxis) Window {
209 return .{
210 .x_off = 0,
211 .y_off = 0,
212 .width = self.screen.width,
213 .height = self.screen.height,
214 .screen = &self.screen,
215 };
216}
217
218/// enter the alternate screen. The alternate screen will automatically
219/// be exited if calling deinit while in the alt screen
220pub fn enterAltScreen(self: *Vaxis, tty: AnyWriter) !void {
221 try tty.writeAll(ctlseqs.smcup);
222 self.state.alt_screen = true;
223}
224
225/// exit the alternate screen
226pub fn exitAltScreen(self: *Vaxis, tty: AnyWriter) !void {
227 try tty.writeAll(ctlseqs.rmcup);
228 self.state.alt_screen = false;
229}
230
231/// write queries to the terminal to determine capabilities. Individual
232/// capabilities will be delivered to the client and possibly intercepted by
233/// Vaxis to enable features.
234///
235/// This call will block until Vaxis.query_futex is woken up, or the timeout.
236/// Event loops can wake up this futex when cap_da1 is received
237pub fn queryTerminal(self: *Vaxis, tty: AnyWriter, timeout_ns: u64) !void {
238 try self.queryTerminalSend(tty);
239 // 1 second timeout
240 std.Thread.Futex.timedWait(&self.query_futex, 0, timeout_ns) catch {};
241 try self.enableDetectedFeatures(tty);
242}
243
244/// write queries to the terminal to determine capabilities. This function
245/// is only for use with a custom main loop. Call Vaxis.queryTerminal() if
246/// you are using Loop.run()
247pub fn queryTerminalSend(_: Vaxis, tty: AnyWriter) !void {
248
249 // TODO: re-enable this
250 // const colorterm = std.posix.getenv("COLORTERM") orelse "";
251 // if (std.mem.eql(u8, colorterm, "truecolor") or
252 // std.mem.eql(u8, colorterm, "24bit"))
253 // {
254 // if (@hasField(Event, "cap_rgb")) {
255 // self.postEvent(.cap_rgb);
256 // }
257 // }
258
259 // TODO: XTGETTCAP queries ("RGB", "Smulx")
260 // TODO: decide if we actually want to query for focus and sync. It
261 // doesn't hurt to blindly use them
262 // _ = try tty.write(ctlseqs.decrqm_focus);
263 // _ = try tty.write(ctlseqs.decrqm_sync);
264 try tty.writeAll(ctlseqs.decrqm_sgr_pixels ++
265 ctlseqs.decrqm_unicode ++
266 ctlseqs.decrqm_color_scheme ++
267 ctlseqs.in_band_resize_set ++
268 ctlseqs.xtversion ++
269 ctlseqs.csi_u_query ++
270 ctlseqs.kitty_graphics_query ++
271 ctlseqs.primary_device_attrs);
272}
273
274/// Enable features detected by responses to queryTerminal. This function
275/// is only for use with a custom main loop. Call Vaxis.queryTerminal() if
276/// you are using Loop.run()
277pub fn enableDetectedFeatures(self: *Vaxis, tty: AnyWriter) !void {
278 switch (builtin.os.tag) {
279 .windows => {
280 // No feature detection on windows. We just hard enable some knowns for ConPTY
281 self.sgr = .legacy;
282 },
283 else => {
284 // Apply any environment variables
285 if (std.posix.getenv("TERMUX_VERSION")) |_|
286 self.sgr = .legacy;
287 if (std.posix.getenv("VHS_RECORD")) |_| {
288 self.caps.unicode = .wcwidth;
289 self.caps.kitty_keyboard = false;
290 self.sgr = .legacy;
291 }
292 if (std.posix.getenv("TERM_PROGRAM")) |prg| {
293 if (std.mem.eql(u8, prg, "vscode"))
294 self.sgr = .legacy;
295 }
296 if (std.posix.getenv("VAXIS_FORCE_LEGACY_SGR")) |_|
297 self.sgr = .legacy;
298 if (std.posix.getenv("VAXIS_FORCE_WCWIDTH")) |_|
299 self.caps.unicode = .wcwidth;
300 if (std.posix.getenv("VAXIS_FORCE_UNICODE")) |_|
301 self.caps.unicode = .unicode;
302
303 // enable detected features
304 if (self.caps.kitty_keyboard) {
305 try self.enableKittyKeyboard(tty, self.opts.kitty_keyboard_flags);
306 }
307 if (self.caps.unicode == .unicode) {
308 try tty.writeAll(ctlseqs.unicode_set);
309 }
310 },
311 }
312}
313
314// the next render call will refresh the entire screen
315pub fn queueRefresh(self: *Vaxis) void {
316 self.refresh = true;
317}
318
319/// draws the screen to the terminal
320pub fn render(self: *Vaxis, tty: AnyWriter) !void {
321 self.renders += 1;
322 self.render_timer.reset();
323 defer {
324 self.render_dur += self.render_timer.read() / std.time.ns_per_us;
325 }
326
327 defer self.refresh = false;
328
329 // Set up sync before we write anything
330 // TODO: optimize sync so we only sync _when we have changes_. This
331 // requires a smarter buffered writer, we'll probably have to write
332 // our own
333 try tty.writeAll(ctlseqs.sync_set);
334 defer tty.writeAll(ctlseqs.sync_reset) catch {};
335
336 // Send the cursor to 0,0
337 // TODO: this needs to move after we optimize writes. We only do
338 // this if we have an update to make. We also need to hide cursor
339 // and then reshow it if needed
340 try tty.writeAll(ctlseqs.hide_cursor);
341 if (self.state.alt_screen)
342 try tty.writeAll(ctlseqs.home)
343 else {
344 try tty.writeByte('\r');
345 try tty.writeBytesNTimes(ctlseqs.ri, self.state.cursor.row);
346 }
347 try tty.writeAll(ctlseqs.sgr_reset);
348
349 // initialize some variables
350 var reposition: bool = false;
351 var row: usize = 0;
352 var col: usize = 0;
353 var cursor: Style = .{};
354 var link: Hyperlink = .{};
355 var cursor_pos: struct {
356 row: usize = 0,
357 col: usize = 0,
358 } = .{};
359
360 // Clear all images
361 if (self.caps.kitty_graphics)
362 try tty.writeAll(ctlseqs.kitty_graphics_clear);
363
364 var i: usize = 0;
365 while (i < self.screen.buf.len) {
366 const cell = self.screen.buf[i];
367 const w = blk: {
368 if (cell.char.width != 0) break :blk cell.char.width;
369
370 const method: gwidth.Method = self.caps.unicode;
371 const width = gwidth.gwidth(cell.char.grapheme, method, &self.unicode.width_data);
372 break :blk @max(1, width);
373 };
374 defer {
375 // advance by the width of this char mod 1
376 std.debug.assert(w > 0);
377 var j = i + 1;
378 while (j < i + w) : (j += 1) {
379 if (j >= self.screen_last.buf.len) break;
380 self.screen_last.buf[j].skipped = true;
381 }
382 col += w;
383 i += w;
384 }
385 if (col >= self.screen.width) {
386 row += 1;
387 col = 0;
388 // Rely on terminal wrapping to reposition into next row instead of forcing it
389 if (!cell.wrapped)
390 reposition = true;
391 }
392 // If cell is the same as our last frame, we don't need to do
393 // anything
394 const last = self.screen_last.buf[i];
395 if (!self.refresh and last.eql(cell) and !last.skipped and cell.image == null) {
396 reposition = true;
397 // Close any osc8 sequence we might be in before
398 // repositioning
399 if (link.uri.len > 0) {
400 try tty.writeAll(ctlseqs.osc8_clear);
401 }
402 continue;
403 }
404 self.screen_last.buf[i].skipped = false;
405 defer {
406 cursor = cell.style;
407 link = cell.link;
408 }
409 // Set this cell in the last frame
410 self.screen_last.writeCell(col, row, cell);
411
412 // reposition the cursor, if needed
413 if (reposition) {
414 reposition = false;
415 link = .{};
416 if (self.state.alt_screen)
417 try tty.print(ctlseqs.cup, .{ row + 1, col + 1 })
418 else {
419 if (cursor_pos.row == row) {
420 const n = col - cursor_pos.col;
421 if (n > 0)
422 try tty.print(ctlseqs.cuf, .{n});
423 } else {
424 const n = row - cursor_pos.row;
425 try tty.writeByteNTimes('\n', n);
426 try tty.writeByte('\r');
427 if (col > 0)
428 try tty.print(ctlseqs.cuf, .{col});
429 }
430 }
431 }
432
433 if (cell.image) |img| {
434 try tty.print(
435 ctlseqs.kitty_graphics_preamble,
436 .{img.img_id},
437 );
438 if (img.options.pixel_offset) |offset| {
439 try tty.print(
440 ",X={d},Y={d}",
441 .{ offset.x, offset.y },
442 );
443 }
444 if (img.options.clip_region) |clip| {
445 if (clip.x) |x|
446 try tty.print(",x={d}", .{x});
447 if (clip.y) |y|
448 try tty.print(",y={d}", .{y});
449 if (clip.width) |width|
450 try tty.print(",w={d}", .{width});
451 if (clip.height) |height|
452 try tty.print(",h={d}", .{height});
453 }
454 if (img.options.size) |size| {
455 if (size.rows) |rows|
456 try tty.print(",r={d}", .{rows});
457 if (size.cols) |cols|
458 try tty.print(",c={d}", .{cols});
459 }
460 if (img.options.z_index) |z| {
461 try tty.print(",z={d}", .{z});
462 }
463 try tty.writeAll(ctlseqs.kitty_graphics_closing);
464 }
465
466 // something is different, so let's loop through everything and
467 // find out what
468
469 // foreground
470 if (!Cell.Color.eql(cursor.fg, cell.style.fg)) {
471 switch (cell.style.fg) {
472 .default => try tty.writeAll(ctlseqs.fg_reset),
473 .index => |idx| {
474 switch (idx) {
475 0...7 => try tty.print(ctlseqs.fg_base, .{idx}),
476 8...15 => try tty.print(ctlseqs.fg_bright, .{idx - 8}),
477 else => {
478 switch (self.sgr) {
479 .standard => try tty.print(ctlseqs.fg_indexed, .{idx}),
480 .legacy => try tty.print(ctlseqs.fg_indexed_legacy, .{idx}),
481 }
482 },
483 }
484 },
485 .rgb => |rgb| {
486 switch (self.sgr) {
487 .standard => try tty.print(ctlseqs.fg_rgb, .{ rgb[0], rgb[1], rgb[2] }),
488 .legacy => try tty.print(ctlseqs.fg_rgb_legacy, .{ rgb[0], rgb[1], rgb[2] }),
489 }
490 },
491 }
492 }
493 // background
494 if (!Cell.Color.eql(cursor.bg, cell.style.bg)) {
495 switch (cell.style.bg) {
496 .default => try tty.writeAll(ctlseqs.bg_reset),
497 .index => |idx| {
498 switch (idx) {
499 0...7 => try tty.print(ctlseqs.bg_base, .{idx}),
500 8...15 => try tty.print(ctlseqs.bg_bright, .{idx - 8}),
501 else => {
502 switch (self.sgr) {
503 .standard => try tty.print(ctlseqs.bg_indexed, .{idx}),
504 .legacy => try tty.print(ctlseqs.bg_indexed_legacy, .{idx}),
505 }
506 },
507 }
508 },
509 .rgb => |rgb| {
510 switch (self.sgr) {
511 .standard => try tty.print(ctlseqs.bg_rgb, .{ rgb[0], rgb[1], rgb[2] }),
512 .legacy => try tty.print(ctlseqs.bg_rgb_legacy, .{ rgb[0], rgb[1], rgb[2] }),
513 }
514 },
515 }
516 }
517 // underline color
518 if (!Cell.Color.eql(cursor.ul, cell.style.ul)) {
519 switch (cell.style.ul) {
520 .default => try tty.writeAll(ctlseqs.ul_reset),
521 .index => |idx| {
522 switch (self.sgr) {
523 .standard => try tty.print(ctlseqs.ul_indexed, .{idx}),
524 .legacy => try tty.print(ctlseqs.ul_indexed_legacy, .{idx}),
525 }
526 },
527 .rgb => |rgb| {
528 switch (self.sgr) {
529 .standard => try tty.print(ctlseqs.ul_rgb, .{ rgb[0], rgb[1], rgb[2] }),
530 .legacy => try tty.print(ctlseqs.ul_rgb_legacy, .{ rgb[0], rgb[1], rgb[2] }),
531 }
532 },
533 }
534 }
535 // underline style
536 if (cursor.ul_style != cell.style.ul_style) {
537 const seq = switch (cell.style.ul_style) {
538 .off => ctlseqs.ul_off,
539 .single => ctlseqs.ul_single,
540 .double => ctlseqs.ul_double,
541 .curly => ctlseqs.ul_curly,
542 .dotted => ctlseqs.ul_dotted,
543 .dashed => ctlseqs.ul_dashed,
544 };
545 try tty.writeAll(seq);
546 }
547 // bold
548 if (cursor.bold != cell.style.bold) {
549 const seq = switch (cell.style.bold) {
550 true => ctlseqs.bold_set,
551 false => ctlseqs.bold_dim_reset,
552 };
553 try tty.writeAll(seq);
554 if (cell.style.dim) {
555 try tty.writeAll(ctlseqs.dim_set);
556 }
557 }
558 // dim
559 if (cursor.dim != cell.style.dim) {
560 const seq = switch (cell.style.dim) {
561 true => ctlseqs.dim_set,
562 false => ctlseqs.bold_dim_reset,
563 };
564 try tty.writeAll(seq);
565 if (cell.style.bold) {
566 try tty.writeAll(ctlseqs.bold_set);
567 }
568 }
569 // dim
570 if (cursor.italic != cell.style.italic) {
571 const seq = switch (cell.style.italic) {
572 true => ctlseqs.italic_set,
573 false => ctlseqs.italic_reset,
574 };
575 try tty.writeAll(seq);
576 }
577 // dim
578 if (cursor.blink != cell.style.blink) {
579 const seq = switch (cell.style.blink) {
580 true => ctlseqs.blink_set,
581 false => ctlseqs.blink_reset,
582 };
583 try tty.writeAll(seq);
584 }
585 // reverse
586 if (cursor.reverse != cell.style.reverse) {
587 const seq = switch (cell.style.reverse) {
588 true => ctlseqs.reverse_set,
589 false => ctlseqs.reverse_reset,
590 };
591 try tty.writeAll(seq);
592 }
593 // invisible
594 if (cursor.invisible != cell.style.invisible) {
595 const seq = switch (cell.style.invisible) {
596 true => ctlseqs.invisible_set,
597 false => ctlseqs.invisible_reset,
598 };
599 try tty.writeAll(seq);
600 }
601 // strikethrough
602 if (cursor.strikethrough != cell.style.strikethrough) {
603 const seq = switch (cell.style.strikethrough) {
604 true => ctlseqs.strikethrough_set,
605 false => ctlseqs.strikethrough_reset,
606 };
607 try tty.writeAll(seq);
608 }
609
610 // url
611 if (!std.mem.eql(u8, link.uri, cell.link.uri)) {
612 var ps = cell.link.params;
613 if (cell.link.uri.len == 0) {
614 // Empty out the params no matter what if we don't have
615 // a url
616 ps = "";
617 }
618 try tty.print(ctlseqs.osc8, .{ ps, cell.link.uri });
619 }
620 try tty.writeAll(cell.char.grapheme);
621 cursor_pos.col = col + w;
622 cursor_pos.row = row;
623 }
624 if (self.screen.cursor_vis) {
625 if (self.state.alt_screen) {
626 try tty.print(
627 ctlseqs.cup,
628 .{
629 self.screen.cursor_row + 1,
630 self.screen.cursor_col + 1,
631 },
632 );
633 } else {
634 // TODO: position cursor relative to current location
635 try tty.writeByte('\r');
636 if (self.screen.cursor_row >= cursor_pos.row)
637 try tty.writeByteNTimes('\n', self.screen.cursor_row - cursor_pos.row)
638 else
639 try tty.writeBytesNTimes(ctlseqs.ri, cursor_pos.row - self.screen.cursor_row);
640 if (self.screen.cursor_col > 0)
641 try tty.print(ctlseqs.cuf, .{self.screen.cursor_col});
642 }
643 self.state.cursor.row = self.screen.cursor_row;
644 self.state.cursor.col = self.screen.cursor_col;
645 try tty.writeAll(ctlseqs.show_cursor);
646 } else {
647 self.state.cursor.row = cursor_pos.row;
648 self.state.cursor.col = cursor_pos.col;
649 }
650 if (self.screen.mouse_shape != self.screen_last.mouse_shape) {
651 try tty.print(
652 ctlseqs.osc22_mouse_shape,
653 .{@tagName(self.screen.mouse_shape)},
654 );
655 self.screen_last.mouse_shape = self.screen.mouse_shape;
656 }
657 if (self.screen.cursor_shape != self.screen_last.cursor_shape) {
658 try tty.print(
659 ctlseqs.cursor_shape,
660 .{@intFromEnum(self.screen.cursor_shape)},
661 );
662 self.screen_last.cursor_shape = self.screen.cursor_shape;
663 }
664}
665
666fn enableKittyKeyboard(self: *Vaxis, tty: AnyWriter, flags: Key.KittyFlags) !void {
667 const flag_int: u5 = @bitCast(flags);
668 try tty.print(ctlseqs.csi_u_push, .{flag_int});
669 self.state.kitty_keyboard = true;
670}
671
672/// send a system notification
673pub fn notify(_: *Vaxis, tty: AnyWriter, title: ?[]const u8, body: []const u8) !void {
674 if (title) |t|
675 try tty.print(ctlseqs.osc777_notify, .{ t, body })
676 else
677 try tty.print(ctlseqs.osc9_notify, .{body});
678}
679
680/// sets the window title
681pub fn setTitle(_: *Vaxis, tty: AnyWriter, title: []const u8) !void {
682 try tty.print(ctlseqs.osc2_set_title, .{title});
683}
684
685// turn bracketed paste on or off. An event will be sent at the
686// beginning and end of a detected paste. All keystrokes between these
687// events were pasted
688pub fn setBracketedPaste(self: *Vaxis, tty: AnyWriter, enable: bool) !void {
689 const seq = if (enable)
690 ctlseqs.bp_set
691 else
692 ctlseqs.bp_reset;
693 try tty.writeAll(seq);
694 self.state.bracketed_paste = enable;
695}
696
697/// set the mouse shape
698pub fn setMouseShape(self: *Vaxis, shape: Shape) void {
699 self.screen.mouse_shape = shape;
700}
701
702/// Change the mouse reporting mode
703pub fn setMouseMode(self: *Vaxis, tty: AnyWriter, enable: bool) !void {
704 if (enable) {
705 self.state.mouse = true;
706 if (self.caps.sgr_pixels) {
707 log.debug("enabling mouse mode: pixel coordinates", .{});
708 self.state.pixel_mouse = true;
709 try tty.writeAll(ctlseqs.mouse_set_pixels);
710 } else {
711 log.debug("enabling mouse mode: cell coordinates", .{});
712 try tty.writeAll(ctlseqs.mouse_set);
713 }
714 } else {
715 try tty.writeAll(ctlseqs.mouse_reset);
716 }
717}
718
719/// Translate pixel mouse coordinates to cell + offset
720pub fn translateMouse(self: Vaxis, mouse: Mouse) Mouse {
721 if (self.screen.width == 0 or self.screen.height == 0) return mouse;
722 var result = mouse;
723 if (self.state.pixel_mouse) {
724 std.debug.assert(mouse.xoffset == 0);
725 std.debug.assert(mouse.yoffset == 0);
726 const xpos = mouse.col;
727 const ypos = mouse.row;
728 const xextra = self.screen.width_pix % self.screen.width;
729 const yextra = self.screen.height_pix % self.screen.height;
730 const xcell = (self.screen.width_pix - xextra) / self.screen.width;
731 const ycell = (self.screen.height_pix - yextra) / self.screen.height;
732 result.col = xpos / xcell;
733 result.row = ypos / ycell;
734 result.xoffset = xpos % xcell;
735 result.yoffset = ypos % ycell;
736 }
737 return result;
738}
739
740/// Transmit an image using the local filesystem. Allocates only for base64 encoding
741pub fn transmitLocalImagePath(
742 self: *Vaxis,
743 allocator: std.mem.Allocator,
744 tty: AnyWriter,
745 payload: []const u8,
746 width: usize,
747 height: usize,
748 medium: Image.TransmitMedium,
749 format: Image.TransmitFormat,
750) !Image {
751 defer self.next_img_id += 1;
752
753 const id = self.next_img_id;
754
755 const size = base64Encoder.calcSize(payload.len);
756 if (size >= 4096) return error.PathTooLong;
757
758 const buf = try allocator.alloc(u8, size);
759 const encoded = base64Encoder.encode(buf, payload);
760 defer allocator.free(buf);
761
762 const medium_char: u8 = switch (medium) {
763 .file => 'f',
764 .temp_file => 't',
765 .shared_mem => 's',
766 };
767
768 switch (format) {
769 .rgb => {
770 try tty.print(
771 "\x1b_Gf=24,s={d},v={d},i={d},t={c};{s}\x1b\\",
772 .{ width, height, id, medium_char, encoded },
773 );
774 },
775 .rgba => {
776 try tty.print(
777 "\x1b_Gf=32,s={d},v={d},i={d},t={c};{s}\x1b\\",
778 .{ width, height, id, medium_char, encoded },
779 );
780 },
781 .png => {
782 try tty.print(
783 "\x1b_Gf=100,i={d},t={c};{s}\x1b\\",
784 .{ id, medium_char, encoded },
785 );
786 },
787 }
788 return .{
789 .id = id,
790 .width = width,
791 .height = height,
792 };
793}
794
795/// Transmit an image which has been pre-base64 encoded
796pub fn transmitPreEncodedImage(
797 self: *Vaxis,
798 tty: AnyWriter,
799 bytes: []const u8,
800 width: usize,
801 height: usize,
802 format: Image.TransmitFormat,
803) !Image {
804 defer self.next_img_id += 1;
805 const id = self.next_img_id;
806
807 const fmt: u8 = switch (format) {
808 .rgb => 24,
809 .rgba => 32,
810 .png => 100,
811 };
812
813 if (bytes.len < 4096) {
814 try tty.print(
815 "\x1b_Gf={d},s={d},v={d},i={d};{s}\x1b\\",
816 .{
817 fmt,
818 width,
819 height,
820 id,
821 bytes,
822 },
823 );
824 } else {
825 var n: usize = 4096;
826
827 try tty.print(
828 "\x1b_Gf={d},s={d},v={d},i={d},m=1;{s}\x1b\\",
829 .{ fmt, width, height, id, bytes[0..n] },
830 );
831 while (n < bytes.len) : (n += 4096) {
832 const end: usize = @min(n + 4096, bytes.len);
833 const m: u2 = if (end == bytes.len) 0 else 1;
834 try tty.print(
835 "\x1b_Gm={d};{s}\x1b\\",
836 .{
837 m,
838 bytes[n..end],
839 },
840 );
841 }
842 }
843 return .{
844 .id = id,
845 .width = width,
846 .height = height,
847 };
848}
849
850pub fn transmitImage(
851 self: *Vaxis,
852 alloc: std.mem.Allocator,
853 tty: AnyWriter,
854 img: *zigimg.Image,
855 format: Image.TransmitFormat,
856) !Image {
857 if (!self.caps.kitty_graphics) return error.NoGraphicsCapability;
858
859 var arena = std.heap.ArenaAllocator.init(alloc);
860 defer arena.deinit();
861
862 const buf = switch (format) {
863 .png => png: {
864 const png_buf = try arena.allocator().alloc(u8, img.imageByteSize());
865 const png = try img.writeToMemory(png_buf, .{ .png = .{} });
866 break :png png;
867 },
868 .rgb => rgb: {
869 try img.convert(.rgb24);
870 break :rgb img.rawBytes();
871 },
872 .rgba => rgba: {
873 try img.convert(.rgba32);
874 break :rgba img.rawBytes();
875 },
876 };
877
878 const b64_buf = try arena.allocator().alloc(u8, base64Encoder.calcSize(buf.len));
879 const encoded = base64Encoder.encode(b64_buf, buf);
880
881 return self.transmitPreEncodedImage(tty, encoded, img.width, img.height, format);
882}
883
884pub fn loadImage(
885 self: *Vaxis,
886 alloc: std.mem.Allocator,
887 tty: AnyWriter,
888 src: Image.Source,
889) !Image {
890 if (!self.caps.kitty_graphics) return error.NoGraphicsCapability;
891
892 var img = switch (src) {
893 .path => |path| try zigimg.Image.fromFilePath(alloc, path),
894 .mem => |bytes| try zigimg.Image.fromMemory(alloc, bytes),
895 };
896 defer img.deinit();
897 return self.transmitImage(alloc, tty, &img, .png);
898}
899
900/// deletes an image from the terminal's memory
901pub fn freeImage(_: Vaxis, tty: AnyWriter, id: u32) void {
902 tty.print("\x1b_Ga=d,d=I,i={d};\x1b\\", .{id}) catch |err| {
903 log.err("couldn't delete image {d}: {}", .{ id, err });
904 return;
905 };
906}
907
908pub fn copyToSystemClipboard(_: Vaxis, tty: AnyWriter, text: []const u8, encode_allocator: std.mem.Allocator) !void {
909 const encoder = std.base64.standard.Encoder;
910 const size = encoder.calcSize(text.len);
911 const buf = try encode_allocator.alloc(u8, size);
912 const b64 = encoder.encode(buf, text);
913 defer encode_allocator.free(buf);
914 try tty.print(
915 ctlseqs.osc52_clipboard_copy,
916 .{b64},
917 );
918}
919
920pub fn requestSystemClipboard(self: Vaxis, tty: AnyWriter) !void {
921 if (self.opts.system_clipboard_allocator == null) return error.NoClipboardAllocator;
922 try tty.print(
923 ctlseqs.osc52_clipboard_request,
924 .{},
925 );
926}
927
928/// Set the default terminal foreground color
929pub fn setTerminalForegroundColor(self: *Vaxis, tty: AnyWriter, rgb: [3]u8) !void {
930 try tty.print(ctlseqs.osc10_set, .{ rgb[0], rgb[0], rgb[1], rgb[1], rgb[2], rgb[2] });
931 self.state.changed_default_fg = true;
932}
933
934/// Set the default terminal background color
935pub fn setTerminalBackgroundColor(self: *Vaxis, tty: AnyWriter, rgb: [3]u8) !void {
936 try tty.print(ctlseqs.osc11_set, .{ rgb[0], rgb[0], rgb[1], rgb[1], rgb[2], rgb[2] });
937 self.state.changed_default_bg = true;
938}
939
940/// Request a color report from the terminal. Note: not all terminals support
941/// reporting colors. It is always safe to try, but you may not receive a
942/// response.
943pub fn queryColor(_: Vaxis, tty: AnyWriter, kind: Cell.Color.Kind) !void {
944 switch (kind) {
945 .fg => try tty.writeAll(ctlseqs.osc10_query),
946 .bg => try tty.writeAll(ctlseqs.osc11_query),
947 .cursor => try tty.writeAll(ctlseqs.osc12_query),
948 .index => |idx| try tty.print(ctlseqs.osc4_query, .{idx}),
949 }
950}
951
952/// Subscribe to color theme updates. A `color_scheme: Color.Scheme` tag must
953/// exist on your Event type to receive the response. This is a queried
954/// capability. Support can be detected by checking the value of
955/// vaxis.caps.color_scheme_updates. The initial scheme will be reported when
956/// subscribing.
957pub fn subscribeToColorSchemeUpdates(self: *Vaxis, tty: AnyWriter) !void {
958 try tty.writeAll(ctlseqs.color_scheme_request);
959 try tty.writeAll(ctlseqs.color_scheme_set);
960 self.state.color_scheme_updates = true;
961}
962
963pub fn deviceStatusReport(_: Vaxis, tty: AnyWriter) !void {
964 try tty.writeAll(ctlseqs.device_status_report);
965}
966
967/// prettyPrint is used to print the contents of the Screen to the tty. The state is not stored, and
968/// the cursor will be put on the next line after the last line is printed. This is useful to
969/// sequentially print data in a styled format to eg. stdout. This function returns an error if you
970/// are not in the alt screen. The cursor is always hidden, and mouse shapes are not available
971pub fn prettyPrint(self: *Vaxis, tty: AnyWriter) !void {
972 if (self.state.alt_screen) return error.NotInPrimaryScreen;
973
974 try tty.writeAll(ctlseqs.hide_cursor);
975 try tty.writeAll(ctlseqs.sync_set);
976 defer tty.writeAll(ctlseqs.sync_reset) catch {};
977 try tty.writeAll(ctlseqs.sgr_reset);
978 defer tty.writeAll(ctlseqs.sgr_reset) catch {};
979
980 var reposition: bool = false;
981 var row: usize = 0;
982 var col: usize = 0;
983 var cursor: Style = .{};
984 var link: Hyperlink = .{};
985 var cursor_pos: struct {
986 row: usize = 0,
987 col: usize = 0,
988 } = .{};
989
990 var i: usize = 0;
991 while (i < self.screen.buf.len) {
992 const cell = self.screen.buf[i];
993 const w = blk: {
994 if (cell.char.width != 0) break :blk cell.char.width;
995
996 const method: gwidth.Method = self.caps.unicode;
997 const width = gwidth.gwidth(cell.char.grapheme, method, &self.unicode.width_data);
998 break :blk @max(1, width);
999 };
1000 defer {
1001 // advance by the width of this char mod 1
1002 std.debug.assert(w > 0);
1003 var j = i + 1;
1004 while (j < i + w) : (j += 1) {
1005 if (j >= self.screen_last.buf.len) break;
1006 self.screen_last.buf[j].skipped = true;
1007 }
1008 col += w;
1009 i += w;
1010 }
1011 if (col >= self.screen.width) {
1012 row += 1;
1013 col = 0;
1014 // Rely on terminal wrapping to reposition into next row instead of forcing it
1015 if (!cell.wrapped)
1016 reposition = true;
1017 }
1018 if (cell.default) {
1019 reposition = true;
1020 continue;
1021 }
1022 defer {
1023 cursor = cell.style;
1024 link = cell.link;
1025 }
1026
1027 // reposition the cursor, if needed
1028 if (reposition) {
1029 reposition = false;
1030 link = .{};
1031 if (cursor_pos.row == row) {
1032 const n = col - cursor_pos.col;
1033 if (n > 0)
1034 try tty.print(ctlseqs.cuf, .{n});
1035 } else {
1036 const n = row - cursor_pos.row;
1037 try tty.writeByteNTimes('\n', n);
1038 try tty.writeByte('\r');
1039 if (col > 0)
1040 try tty.print(ctlseqs.cuf, .{col});
1041 }
1042 }
1043
1044 if (cell.image) |img| {
1045 try tty.print(
1046 ctlseqs.kitty_graphics_preamble,
1047 .{img.img_id},
1048 );
1049 if (img.options.pixel_offset) |offset| {
1050 try tty.print(
1051 ",X={d},Y={d}",
1052 .{ offset.x, offset.y },
1053 );
1054 }
1055 if (img.options.clip_region) |clip| {
1056 if (clip.x) |x|
1057 try tty.print(",x={d}", .{x});
1058 if (clip.y) |y|
1059 try tty.print(",y={d}", .{y});
1060 if (clip.width) |width|
1061 try tty.print(",w={d}", .{width});
1062 if (clip.height) |height|
1063 try tty.print(",h={d}", .{height});
1064 }
1065 if (img.options.size) |size| {
1066 if (size.rows) |rows|
1067 try tty.print(",r={d}", .{rows});
1068 if (size.cols) |cols|
1069 try tty.print(",c={d}", .{cols});
1070 }
1071 if (img.options.z_index) |z| {
1072 try tty.print(",z={d}", .{z});
1073 }
1074 try tty.writeAll(ctlseqs.kitty_graphics_closing);
1075 }
1076
1077 // something is different, so let's loop through everything and
1078 // find out what
1079
1080 // foreground
1081 if (!Cell.Color.eql(cursor.fg, cell.style.fg)) {
1082 switch (cell.style.fg) {
1083 .default => try tty.writeAll(ctlseqs.fg_reset),
1084 .index => |idx| {
1085 switch (idx) {
1086 0...7 => try tty.print(ctlseqs.fg_base, .{idx}),
1087 8...15 => try tty.print(ctlseqs.fg_bright, .{idx - 8}),
1088 else => {
1089 switch (self.sgr) {
1090 .standard => try tty.print(ctlseqs.fg_indexed, .{idx}),
1091 .legacy => try tty.print(ctlseqs.fg_indexed_legacy, .{idx}),
1092 }
1093 },
1094 }
1095 },
1096 .rgb => |rgb| {
1097 switch (self.sgr) {
1098 .standard => try tty.print(ctlseqs.fg_rgb, .{ rgb[0], rgb[1], rgb[2] }),
1099 .legacy => try tty.print(ctlseqs.fg_rgb_legacy, .{ rgb[0], rgb[1], rgb[2] }),
1100 }
1101 },
1102 }
1103 }
1104 // background
1105 if (!Cell.Color.eql(cursor.bg, cell.style.bg)) {
1106 switch (cell.style.bg) {
1107 .default => try tty.writeAll(ctlseqs.bg_reset),
1108 .index => |idx| {
1109 switch (idx) {
1110 0...7 => try tty.print(ctlseqs.bg_base, .{idx}),
1111 8...15 => try tty.print(ctlseqs.bg_bright, .{idx - 8}),
1112 else => {
1113 switch (self.sgr) {
1114 .standard => try tty.print(ctlseqs.bg_indexed, .{idx}),
1115 .legacy => try tty.print(ctlseqs.bg_indexed_legacy, .{idx}),
1116 }
1117 },
1118 }
1119 },
1120 .rgb => |rgb| {
1121 switch (self.sgr) {
1122 .standard => try tty.print(ctlseqs.bg_rgb, .{ rgb[0], rgb[1], rgb[2] }),
1123 .legacy => try tty.print(ctlseqs.bg_rgb_legacy, .{ rgb[0], rgb[1], rgb[2] }),
1124 }
1125 },
1126 }
1127 }
1128 // underline color
1129 if (!Cell.Color.eql(cursor.ul, cell.style.ul)) {
1130 switch (cell.style.ul) {
1131 .default => try tty.writeAll(ctlseqs.ul_reset),
1132 .index => |idx| {
1133 switch (self.sgr) {
1134 .standard => try tty.print(ctlseqs.ul_indexed, .{idx}),
1135 .legacy => try tty.print(ctlseqs.ul_indexed_legacy, .{idx}),
1136 }
1137 },
1138 .rgb => |rgb| {
1139 switch (self.sgr) {
1140 .standard => try tty.print(ctlseqs.ul_rgb, .{ rgb[0], rgb[1], rgb[2] }),
1141 .legacy => try tty.print(ctlseqs.ul_rgb_legacy, .{ rgb[0], rgb[1], rgb[2] }),
1142 }
1143 },
1144 }
1145 }
1146 // underline style
1147 if (cursor.ul_style != cell.style.ul_style) {
1148 const seq = switch (cell.style.ul_style) {
1149 .off => ctlseqs.ul_off,
1150 .single => ctlseqs.ul_single,
1151 .double => ctlseqs.ul_double,
1152 .curly => ctlseqs.ul_curly,
1153 .dotted => ctlseqs.ul_dotted,
1154 .dashed => ctlseqs.ul_dashed,
1155 };
1156 try tty.writeAll(seq);
1157 }
1158 // bold
1159 if (cursor.bold != cell.style.bold) {
1160 const seq = switch (cell.style.bold) {
1161 true => ctlseqs.bold_set,
1162 false => ctlseqs.bold_dim_reset,
1163 };
1164 try tty.writeAll(seq);
1165 if (cell.style.dim) {
1166 try tty.writeAll(ctlseqs.dim_set);
1167 }
1168 }
1169 // dim
1170 if (cursor.dim != cell.style.dim) {
1171 const seq = switch (cell.style.dim) {
1172 true => ctlseqs.dim_set,
1173 false => ctlseqs.bold_dim_reset,
1174 };
1175 try tty.writeAll(seq);
1176 if (cell.style.bold) {
1177 try tty.writeAll(ctlseqs.bold_set);
1178 }
1179 }
1180 // dim
1181 if (cursor.italic != cell.style.italic) {
1182 const seq = switch (cell.style.italic) {
1183 true => ctlseqs.italic_set,
1184 false => ctlseqs.italic_reset,
1185 };
1186 try tty.writeAll(seq);
1187 }
1188 // dim
1189 if (cursor.blink != cell.style.blink) {
1190 const seq = switch (cell.style.blink) {
1191 true => ctlseqs.blink_set,
1192 false => ctlseqs.blink_reset,
1193 };
1194 try tty.writeAll(seq);
1195 }
1196 // reverse
1197 if (cursor.reverse != cell.style.reverse) {
1198 const seq = switch (cell.style.reverse) {
1199 true => ctlseqs.reverse_set,
1200 false => ctlseqs.reverse_reset,
1201 };
1202 try tty.writeAll(seq);
1203 }
1204 // invisible
1205 if (cursor.invisible != cell.style.invisible) {
1206 const seq = switch (cell.style.invisible) {
1207 true => ctlseqs.invisible_set,
1208 false => ctlseqs.invisible_reset,
1209 };
1210 try tty.writeAll(seq);
1211 }
1212 // strikethrough
1213 if (cursor.strikethrough != cell.style.strikethrough) {
1214 const seq = switch (cell.style.strikethrough) {
1215 true => ctlseqs.strikethrough_set,
1216 false => ctlseqs.strikethrough_reset,
1217 };
1218 try tty.writeAll(seq);
1219 }
1220
1221 // url
1222 if (!std.mem.eql(u8, link.uri, cell.link.uri)) {
1223 var ps = cell.link.params;
1224 if (cell.link.uri.len == 0) {
1225 // Empty out the params no matter what if we don't have
1226 // a url
1227 ps = "";
1228 }
1229 try tty.print(ctlseqs.osc8, .{ ps, cell.link.uri });
1230 }
1231 try tty.writeAll(cell.char.grapheme);
1232 cursor_pos.col = col + w;
1233 cursor_pos.row = row;
1234 }
1235 try tty.writeAll("\r\n");
1236}
1237
1238/// Set the terminal's current working directory
1239pub fn setTerminalWorkingDirectory(_: *Vaxis, tty: AnyWriter, path: []const u8) !void {
1240 if (path.len == 0 or path[0] != '/')
1241 return error.InvalidAbsolutePath;
1242 const hostname = switch (builtin.os.tag) {
1243 .windows => null,
1244 else => std.posix.getenv("HOSTNAME"),
1245 } orelse "localhost";
1246
1247 const uri: std.Uri = .{
1248 .scheme = "file",
1249 .host = .{ .raw = hostname },
1250 .path = .{ .raw = path },
1251 };
1252 try tty.print(ctlseqs.osc7, .{uri});
1253}