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