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