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