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