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