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