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