this repo has no description
1//! A virtual terminal widget
2const Terminal = @This();
3
4const std = @import("std");
5const builtin = @import("builtin");
6const ansi = @import("ansi.zig");
7pub const Command = @import("Command.zig");
8const Parser = @import("Parser.zig");
9const Pty = @import("Pty.zig");
10const vaxis = @import("../../main.zig");
11const Winsize = vaxis.Winsize;
12const Screen = @import("Screen.zig");
13const Key = vaxis.Key;
14const Queue = vaxis.Queue(Event, 16);
15const key = @import("key.zig");
16
17pub const Event = union(enum) {
18 exited,
19 redraw,
20 bell,
21 title_change: []const u8,
22 pwd_change: []const u8,
23};
24
25const posix = std.posix;
26
27const log = std.log.scoped(.terminal);
28
29pub const Options = struct {
30 scrollback_size: u16 = 500,
31 winsize: Winsize = .{ .rows = 24, .cols = 80, .x_pixel = 0, .y_pixel = 0 },
32 initial_working_directory: ?[]const u8 = null,
33};
34
35pub const Mode = struct {
36 origin: bool = false,
37 autowrap: bool = true,
38 cursor: bool = true,
39 sync: bool = false,
40};
41
42pub const InputEvent = union(enum) {
43 key_press: vaxis.Key,
44};
45
46pub var global_io: std.Io = undefined;
47pub var global_io_initialized: bool = false;
48pub var global_vt_mutex: std.Io.Mutex = .init;
49pub var global_vts: std.AutoHashMapUnmanaged(i32, *Terminal) = .empty;
50pub var global_sigchild_installed: bool = false;
51
52io: std.Io,
53allocator: std.mem.Allocator,
54scrollback_size: u16,
55
56pty: Pty,
57pty_writer: std.Io.File.Writer,
58cmd: Command,
59thread: ?std.Io.Future(void) = null,
60
61/// the screen we draw from
62front_screen: Screen,
63front_mutex: std.Io.Mutex = .init,
64
65/// the back screens
66back_screen: *Screen = undefined,
67back_screen_pri: Screen,
68back_screen_alt: Screen,
69// only applies to primary screen
70scroll_offset: usize = 0,
71back_mutex: std.Io.Mutex = .init,
72// dirty is protected by back_mutex. Only access this field when you hold that mutex
73dirty: bool = false,
74
75should_quit: bool = false,
76
77mode: Mode = .{},
78
79tab_stops: std.ArrayList(u16),
80title: std.ArrayList(u8) = .empty,
81working_directory: std.ArrayList(u8) = .empty,
82
83last_printed: []const u8 = "",
84
85event_queue: Queue,
86
87/// initialize a Terminal. This sets the size of the underlying pty and allocates the sizes of the
88/// screen
89pub fn init(
90 io: std.Io,
91 allocator: std.mem.Allocator,
92 argv: []const []const u8,
93 env: *const std.process.Environ.Map,
94 opts: Options,
95 write_buf: []u8,
96) !Terminal {
97 if (!global_io_initialized) {
98 global_io = io;
99 global_io_initialized = true;
100 }
101 // Verify we have an absolute path
102 if (opts.initial_working_directory) |pwd| {
103 if (!std.fs.path.isAbsolute(pwd)) return error.InvalidWorkingDirectory;
104 }
105 const pty = try Pty.init(io);
106 try pty.setSize(opts.winsize);
107 const cmd: Command = .{
108 .argv = argv,
109 .env_map = env,
110 .pty = pty,
111 .working_directory = opts.initial_working_directory,
112 };
113 var tabs: std.ArrayList(u16) = try .initCapacity(allocator, opts.winsize.cols / 8);
114 var col: u16 = 0;
115 while (col < opts.winsize.cols) : (col += 8) {
116 try tabs.append(allocator, col);
117 }
118 return .{
119 .io = io,
120 .allocator = allocator,
121 .pty = pty,
122 .pty_writer = pty.pty.writerStreaming(io, write_buf),
123 .cmd = cmd,
124 .scrollback_size = opts.scrollback_size,
125 .front_screen = try Screen.init(allocator, opts.winsize.cols, opts.winsize.rows),
126 .back_screen_pri = try Screen.init(allocator, opts.winsize.cols, opts.winsize.rows + opts.scrollback_size),
127 .back_screen_alt = try Screen.init(allocator, opts.winsize.cols, opts.winsize.rows),
128 .tab_stops = tabs,
129 .event_queue = .init(io),
130 };
131}
132
133/// release all resources of the Terminal
134pub fn deinit(self: *Terminal) void {
135 self.should_quit = true;
136
137 pid: {
138 global_vt_mutex.lock(self.io) catch break :pid;
139 defer global_vt_mutex.unlock(self.io);
140 if (self.cmd.pid) |pid|
141 _ = global_vts.remove(pid);
142 if (global_vts.count() == 0) {
143 global_vts.deinit(self.allocator);
144 }
145 }
146 self.cmd.kill();
147 if (self.thread) |*thread| {
148 // write an EOT into the tty to trigger a read on our thread
149 const EOT = "\x04";
150 self.pty.tty.writeStreamingAll(self.io, EOT) catch {};
151 thread.await(self.io);
152 self.thread = null;
153 }
154 self.pty.deinit(self.io);
155 self.front_screen.deinit(self.allocator);
156 self.back_screen_pri.deinit(self.allocator);
157 self.back_screen_alt.deinit(self.allocator);
158 self.tab_stops.deinit(self.allocator);
159 self.title.deinit(self.allocator);
160 self.working_directory.deinit(self.allocator);
161}
162
163pub fn spawn(self: *Terminal) !void {
164 if (self.thread != null) return;
165 self.back_screen = &self.back_screen_pri;
166
167 try self.cmd.spawn(self.io, self.allocator);
168
169 self.working_directory.clearRetainingCapacity();
170 if (self.cmd.working_directory) |pwd| {
171 try self.working_directory.appendSlice(self.allocator, pwd);
172 } else {
173 const pwd: std.Io.Dir = .cwd();
174 const out_path = try pwd.realPathFileAlloc(self.io, ".", self.allocator);
175 try self.working_directory.appendSlice(self.allocator, out_path);
176 }
177
178 {
179 // add to our global list
180 try global_vt_mutex.lock(self.io);
181 defer global_vt_mutex.unlock(self.io);
182 if (self.cmd.pid) |pid|
183 try global_vts.put(self.allocator, pid, self);
184 }
185
186 self.thread = try self.io.concurrent(Terminal.run, .{self});
187}
188
189/// resize the screen. Locks access to the back screen. Should only be called from the main thread.
190/// This is safe to call every render cycle: there is a guard to only perform a resize if the size
191/// of the window has changed.
192pub fn resize(self: *Terminal, ws: Winsize) !void {
193 // don't deinit with no size change
194 if (ws.cols == self.front_screen.width and
195 ws.rows == self.front_screen.height)
196 return;
197
198 try self.back_mutex.lock(self.io);
199 defer self.back_mutex.unlock(self.io);
200
201 self.front_screen.deinit(self.allocator);
202 self.front_screen = try Screen.init(self.allocator, ws.cols, ws.rows);
203
204 self.back_screen_pri.deinit(self.allocator);
205 self.back_screen_alt.deinit(self.allocator);
206 self.back_screen_pri = try Screen.init(self.allocator, ws.cols, ws.rows + self.scrollback_size);
207 self.back_screen_alt = try Screen.init(self.allocator, ws.cols, ws.rows);
208
209 try self.pty.setSize(ws);
210}
211
212pub fn draw(self: *Terminal, allocator: std.mem.Allocator, win: vaxis.Window) !void {
213 if (self.back_mutex.tryLock()) {
214 defer self.back_mutex.unlock(self.io);
215 // We keep this as a separate condition so we don't deadlock by obtaining the lock but not
216 // having sync
217 if (!self.mode.sync) {
218 try self.back_screen.copyTo(allocator, &self.front_screen);
219 self.dirty = false;
220 }
221 }
222
223 var row: u16 = 0;
224 while (row < self.front_screen.height) : (row += 1) {
225 var col: u16 = 0;
226 while (col < self.front_screen.width) {
227 const cell = self.front_screen.readCell(col, row) orelse continue;
228 win.writeCell(col, row, cell);
229 col += @max(cell.char.width, 1);
230 }
231 }
232
233 if (self.mode.cursor) {
234 win.setCursorShape(self.front_screen.cursor.shape);
235 win.showCursor(self.front_screen.cursor.col, self.front_screen.cursor.row);
236 }
237}
238
239pub fn tryEvent(self: *Terminal) !?Event {
240 return try self.event_queue.tryPop();
241}
242
243pub fn update(self: *Terminal, event: InputEvent) !void {
244 switch (event) {
245 .key_press => |k| {
246 const pty_writer = self.get_pty_writer();
247 defer pty_writer.flush() catch {};
248 try key.encode(pty_writer, k, true, self.back_screen.csi_u_flags);
249 },
250 }
251}
252
253pub fn get_pty_writer(self: *Terminal) *std.Io.Writer {
254 return &self.pty_writer.interface;
255}
256
257fn reader(self: *const Terminal, buf: []u8) std.Io.File.Reader {
258 return self.pty.pty.readerStreaming(self.io, buf);
259}
260
261/// process the output from the command on the pty
262fn run(self: *Terminal) void {
263 self._run() catch {};
264}
265
266fn _run(self: *Terminal) !void {
267 var parser: Parser = .{
268 .buf = try .initCapacity(self.allocator, 128),
269 };
270 defer parser.buf.deinit();
271
272 var reader_buf: [4096]u8 = undefined;
273 var reader_ = self.reader(&reader_buf);
274
275 while (!self.should_quit) {
276 const event = try parser.parseReader(&reader_.interface);
277 try self.back_mutex.lock(self.io);
278 defer self.back_mutex.unlock(self.io);
279
280 if (!self.dirty and try self.event_queue.tryPush(.redraw))
281 self.dirty = true;
282
283 switch (event) {
284 .print => |str| {
285 var iter = vaxis.unicode.graphemeIterator(str);
286 while (iter.next()) |grapheme| {
287 const gr = grapheme.bytes(str);
288 // TODO: use actual instead of .unicode
289 const w = vaxis.gwidth.gwidth(gr, .unicode);
290 try self.back_screen.print(gr, @truncate(w), self.mode.autowrap);
291 }
292 },
293 .c0 => |b| try self.handleC0(b),
294 .escape => |esc| {
295 const final = esc[esc.len - 1];
296 switch (final) {
297 'B' => {}, // TODO: handle charsets
298 // Index
299 'D' => try self.back_screen.index(),
300 // Next Line
301 'E' => {
302 try self.back_screen.index();
303 self.carriageReturn();
304 },
305 // Horizontal Tab Set
306 'H' => {
307 const already_set: bool = for (self.tab_stops.items) |ts| {
308 if (ts == self.back_screen.cursor.col) break true;
309 } else false;
310 if (already_set) continue;
311 try self.tab_stops.append(self.allocator, @truncate(self.back_screen.cursor.col));
312 std.mem.sort(u16, self.tab_stops.items, {}, std.sort.asc(u16));
313 },
314 // Reverse Index
315 'M' => try self.back_screen.reverseIndex(),
316 else => log.info("unhandled escape: {s}", .{esc}),
317 }
318 },
319 .ss2 => |ss2| log.info("unhandled ss2: {c}", .{ss2}),
320 .ss3 => |ss3| log.info("unhandled ss3: {c}", .{ss3}),
321 .csi => |seq| {
322 switch (seq.final) {
323 // Cursor up
324 'A', 'k' => {
325 var iter = seq.iterator(u16);
326 const delta = iter.next() orelse 1;
327 self.back_screen.cursorUp(delta);
328 },
329 // Cursor Down
330 'B' => {
331 var iter = seq.iterator(u16);
332 const delta = iter.next() orelse 1;
333 self.back_screen.cursorDown(delta);
334 },
335 // Cursor Right
336 'C' => {
337 var iter = seq.iterator(u16);
338 const delta = iter.next() orelse 1;
339 self.back_screen.cursorRight(delta);
340 },
341 // Cursor Left
342 'D', 'j' => {
343 var iter = seq.iterator(u16);
344 const delta = iter.next() orelse 1;
345 self.back_screen.cursorLeft(delta);
346 },
347 // Cursor Next Line
348 'E' => {
349 var iter = seq.iterator(u16);
350 const delta = iter.next() orelse 1;
351 self.back_screen.cursorDown(delta);
352 self.carriageReturn();
353 },
354 // Cursor Previous Line
355 'F' => {
356 var iter = seq.iterator(u16);
357 const delta = iter.next() orelse 1;
358 self.back_screen.cursorUp(delta);
359 self.carriageReturn();
360 },
361 // Horizontal Position Absolute
362 'G', '`' => {
363 var iter = seq.iterator(u16);
364 const col = iter.next() orelse 1;
365 self.back_screen.cursor.col = col -| 1;
366 if (self.back_screen.cursor.col < self.back_screen.scrolling_region.left)
367 self.back_screen.cursor.col = self.back_screen.scrolling_region.left;
368 if (self.back_screen.cursor.col > self.back_screen.scrolling_region.right)
369 self.back_screen.cursor.col = self.back_screen.scrolling_region.right;
370 self.back_screen.cursor.pending_wrap = false;
371 },
372 // Cursor Absolute Position
373 'H', 'f' => {
374 var iter = seq.iterator(u16);
375 const row = iter.next() orelse 1;
376 const col = iter.next() orelse 1;
377 self.back_screen.cursor.col = col -| 1;
378 self.back_screen.cursor.row = row -| 1;
379 self.back_screen.cursor.pending_wrap = false;
380 },
381 // Cursor Horizontal Tab
382 'I' => {
383 var iter = seq.iterator(u16);
384 const n = iter.next() orelse 1;
385 self.horizontalTab(n);
386 },
387 // Erase In Display
388 'J' => {
389 // TODO: selective erase (private_marker == '?')
390 var iter = seq.iterator(u16);
391 const kind = iter.next() orelse 0;
392 switch (kind) {
393 0 => self.back_screen.eraseBelow(),
394 1 => self.back_screen.eraseAbove(),
395 2 => self.back_screen.eraseAll(),
396 3 => {},
397 else => {},
398 }
399 },
400 // Erase in Line
401 'K' => {
402 // TODO: selective erase (private_marker == '?')
403 var iter = seq.iterator(u8);
404 const ps = iter.next() orelse 0;
405 switch (ps) {
406 0 => self.back_screen.eraseRight(),
407 1 => self.back_screen.eraseLeft(),
408 2 => self.back_screen.eraseLine(),
409 else => continue,
410 }
411 },
412 // Insert Lines
413 'L' => {
414 var iter = seq.iterator(u16);
415 const n = iter.next() orelse 1;
416 try self.back_screen.insertLine(n);
417 },
418 // Delete Lines
419 'M' => {
420 var iter = seq.iterator(u16);
421 const n = iter.next() orelse 1;
422 try self.back_screen.deleteLine(n);
423 },
424 // Delete Character
425 'P' => {
426 var iter = seq.iterator(u16);
427 const n = iter.next() orelse 1;
428 try self.back_screen.deleteCharacters(n);
429 },
430 // Scroll Up
431 'S' => {
432 var iter = seq.iterator(u16);
433 const n = iter.next() orelse 1;
434 const cur_row = self.back_screen.cursor.row;
435 const cur_col = self.back_screen.cursor.col;
436 const wrap = self.back_screen.cursor.pending_wrap;
437 defer {
438 self.back_screen.cursor.row = cur_row;
439 self.back_screen.cursor.col = cur_col;
440 self.back_screen.cursor.pending_wrap = wrap;
441 }
442 self.back_screen.cursor.col = self.back_screen.scrolling_region.left;
443 self.back_screen.cursor.row = self.back_screen.scrolling_region.top;
444 try self.back_screen.deleteLine(n);
445 },
446 // Scroll Down
447 'T' => {
448 var iter = seq.iterator(u16);
449 const n = iter.next() orelse 1;
450 try self.back_screen.scrollDown(n);
451 },
452 // Tab Control
453 'W' => {
454 if (seq.private_marker) |pm| {
455 if (pm != '?') continue;
456 var iter = seq.iterator(u16);
457 const n = iter.next() orelse continue;
458 if (n != 5) continue;
459 self.tab_stops.clearRetainingCapacity();
460 var col: u16 = 0;
461 while (col < self.back_screen.width) : (col += 8) {
462 try self.tab_stops.append(self.allocator, col);
463 }
464 }
465 },
466 'X' => {
467 self.back_screen.cursor.pending_wrap = false;
468 var iter = seq.iterator(u16);
469 const n = iter.next() orelse 1;
470 const start = self.back_screen.cursor.row * self.back_screen.width + self.back_screen.cursor.col;
471 const end = @max(
472 self.back_screen.cursor.row * self.back_screen.width + self.back_screen.width,
473 n,
474 1, // In case n == 0
475 );
476 var i: usize = start;
477 while (i < end) : (i += 1) {
478 self.back_screen.buf[i].erase(self.allocator, self.back_screen.cursor.style.bg);
479 }
480 },
481 'Z' => {
482 var iter = seq.iterator(u16);
483 const n = iter.next() orelse 1;
484 self.horizontalBackTab(n);
485 },
486 // Cursor Horizontal Position Relative
487 'a' => {
488 var iter = seq.iterator(u16);
489 const n = iter.next() orelse 1;
490 self.back_screen.cursor.pending_wrap = false;
491 const max_end = if (self.mode.origin)
492 self.back_screen.scrolling_region.right
493 else
494 self.back_screen.width - 1;
495 self.back_screen.cursor.col = @min(
496 self.back_screen.cursor.col + max_end,
497 self.back_screen.cursor.col + n,
498 );
499 },
500 // Repeat Previous Character
501 'b' => {
502 var iter = seq.iterator(u16);
503 const n = iter.next() orelse 1;
504 // TODO: maybe not .unicode
505 const w = vaxis.gwidth.gwidth(self.last_printed, .unicode);
506 var i: usize = 0;
507 while (i < n) : (i += 1) {
508 try self.back_screen.print(self.last_printed, @truncate(w), self.mode.autowrap);
509 }
510 },
511 // Device Attributes
512 'c' => {
513 const pty_writer = self.get_pty_writer();
514 defer pty_writer.flush() catch {};
515 if (seq.private_marker) |pm| {
516 switch (pm) {
517 // Secondary
518 '>' => try pty_writer.writeAll("\x1B[>1;69;0c"),
519 '=' => try pty_writer.writeAll("\x1B[=0000c"),
520 else => log.info("unhandled CSI: {f}", .{seq}),
521 }
522 } else {
523 // Primary
524 try pty_writer.writeAll("\x1B[?62;22c");
525 }
526 },
527 // Cursor Vertical Position Absolute
528 'd' => {
529 self.back_screen.cursor.pending_wrap = false;
530 var iter = seq.iterator(u16);
531 const n = iter.next() orelse 1;
532 const max = if (self.mode.origin)
533 self.back_screen.scrolling_region.bottom
534 else
535 self.back_screen.height -| 1;
536 self.back_screen.cursor.pending_wrap = false;
537 self.back_screen.cursor.row = @min(
538 max,
539 n -| 1,
540 );
541 },
542 // Cursor Vertical Position Absolute
543 'e' => {
544 var iter = seq.iterator(u16);
545 const n = iter.next() orelse 1;
546 self.back_screen.cursor.pending_wrap = false;
547 self.back_screen.cursor.row = @min(
548 self.back_screen.width -| 1,
549 n -| 1,
550 );
551 },
552 // Tab Clear
553 'g' => {
554 var iter = seq.iterator(u16);
555 const n = iter.next() orelse 0;
556 switch (n) {
557 0 => {
558 const current = try self.tab_stops.toOwnedSlice(self.allocator);
559 defer self.allocator.free(current);
560 self.tab_stops.clearRetainingCapacity();
561 for (current) |stop| {
562 if (stop == self.back_screen.cursor.col) continue;
563 try self.tab_stops.append(self.allocator, stop);
564 }
565 },
566 3 => self.tab_stops.clearAndFree(self.allocator),
567 else => log.info("unhandled CSI: {f}", .{seq}),
568 }
569 },
570 'h', 'l' => {
571 var iter = seq.iterator(u16);
572 const mode = iter.next() orelse continue;
573 // There is only one collision (mode = 4), and we don't support the private
574 // version of it
575 if (seq.private_marker != null and mode == 4) continue;
576 self.setMode(mode, seq.final == 'h');
577 },
578 'm' => {
579 if (seq.intermediate == null and seq.private_marker == null) {
580 self.back_screen.sgr(seq);
581 }
582 // TODO: private marker and intermediates
583 },
584 'n' => {
585 var iter = seq.iterator(u16);
586 const ps = iter.next() orelse 0;
587 if (seq.intermediate == null and seq.private_marker == null) {
588 const pty_writer = self.get_pty_writer();
589 defer pty_writer.flush() catch {};
590 switch (ps) {
591 5 => try pty_writer.writeAll("\x1b[0n"),
592 6 => try pty_writer.print("\x1b[{d};{d}R", .{
593 self.back_screen.cursor.row + 1,
594 self.back_screen.cursor.col + 1,
595 }),
596 else => log.info("unhandled CSI: {f}", .{seq}),
597 }
598 }
599 },
600 'p' => {
601 var iter = seq.iterator(u16);
602 const ps = iter.next() orelse 0;
603 if (seq.intermediate) |int| {
604 switch (int) {
605 // report mode
606 '$' => {
607 const pty_writer = self.get_pty_writer();
608 defer pty_writer.flush() catch {};
609 switch (ps) {
610 2026 => try pty_writer.writeAll("\x1b[?2026;2$p"),
611 else => {
612 std.log.warn("unhandled mode: {}", .{ps});
613 try pty_writer.print("\x1b[?{d};0$p", .{ps});
614 },
615 }
616 },
617 else => log.info("unhandled CSI: {f}", .{seq}),
618 }
619 }
620 },
621 'q' => {
622 if (seq.intermediate) |int| {
623 switch (int) {
624 ' ' => {
625 var iter = seq.iterator(u8);
626 const shape = iter.next() orelse 0;
627 self.back_screen.cursor.shape = @enumFromInt(shape);
628 },
629 else => {},
630 }
631 }
632 if (seq.private_marker) |pm| {
633 const pty_writer = self.get_pty_writer();
634 defer pty_writer.flush() catch {};
635 switch (pm) {
636 // XTVERSION
637 '>' => try pty_writer.print(
638 "\x1bP>|libvaxis {s}\x1B\\",
639 .{"dev"},
640 ),
641 else => log.info("unhandled CSI: {f}", .{seq}),
642 }
643 }
644 },
645 'r' => {
646 if (seq.intermediate) |_| {
647 // TODO: XTRESTORE
648 continue;
649 }
650 if (seq.private_marker) |_| {
651 // TODO: DECCARA
652 continue;
653 }
654 // DECSTBM
655 var iter = seq.iterator(u16);
656 const top = iter.next() orelse 1;
657 const bottom = iter.next() orelse self.back_screen.height;
658 self.back_screen.scrolling_region.top = top -| 1;
659 self.back_screen.scrolling_region.bottom = bottom -| 1;
660 self.back_screen.cursor.pending_wrap = false;
661 if (self.mode.origin) {
662 self.back_screen.cursor.col = self.back_screen.scrolling_region.left;
663 self.back_screen.cursor.row = self.back_screen.scrolling_region.top;
664 } else {
665 self.back_screen.cursor.col = 0;
666 self.back_screen.cursor.row = 0;
667 }
668 },
669 else => log.info("unhandled CSI: {f}", .{seq}),
670 }
671 },
672 .osc => |osc| {
673 const semicolon = std.mem.indexOfScalar(u8, osc, ';') orelse {
674 log.info("unhandled osc: {s}", .{osc});
675 continue;
676 };
677 const ps = std.fmt.parseUnsigned(u8, osc[0..semicolon], 10) catch {
678 log.info("unhandled osc: {s}", .{osc});
679 continue;
680 };
681 switch (ps) {
682 0 => {
683 self.title.clearRetainingCapacity();
684 try self.title.appendSlice(self.allocator, osc[semicolon + 1 ..]);
685 try self.event_queue.push(.{ .title_change = self.title.items });
686 },
687 7 => {
688 // OSC 7 ; file:// <hostname> <pwd>
689 log.err("osc: {s}", .{osc});
690 self.working_directory.clearRetainingCapacity();
691 const scheme = "file://";
692 const start = std.mem.indexOfScalarPos(u8, osc, semicolon + 2 + scheme.len + 1, '/') orelse {
693 log.info("unknown OSC 7 format: {s}", .{osc});
694 continue;
695 };
696 const enc = osc[start..];
697 var i: usize = 0;
698 while (i < enc.len) : (i += 1) {
699 const b = if (enc[i] == '%') blk: {
700 defer i += 2;
701 break :blk try std.fmt.parseUnsigned(u8, enc[i + 1 .. i + 3], 16);
702 } else enc[i];
703 try self.working_directory.append(self.allocator, b);
704 }
705 try self.event_queue.push(.{ .pwd_change = self.working_directory.items });
706 },
707 else => log.info("unhandled osc: {s}", .{osc}),
708 }
709 },
710 .apc => |apc| log.info("unhandled apc: {s}", .{apc}),
711 }
712 }
713}
714
715inline fn handleC0(self: *Terminal, b: ansi.C0) !void {
716 switch (b) {
717 .NUL, .SOH, .STX => {},
718 .EOT => {}, // we send EOT to quit the read thread
719 .ENQ => {},
720 .BEL => try self.event_queue.push(.bell),
721 .BS => self.back_screen.cursorLeft(1),
722 .HT => self.horizontalTab(1),
723 .LF, .VT, .FF => try self.back_screen.index(),
724 .CR => self.carriageReturn(),
725 .SO => {}, // TODO: Charset shift out
726 .SI => {}, // TODO: Charset shift in
727 else => log.warn("unhandled C0: 0x{x}", .{@intFromEnum(b)}),
728 }
729}
730
731pub fn setMode(self: *Terminal, mode: u16, val: bool) void {
732 switch (mode) {
733 7 => self.mode.autowrap = val,
734 25 => self.mode.cursor = val,
735 1049 => {
736 if (val)
737 self.back_screen = &self.back_screen_alt
738 else
739 self.back_screen = &self.back_screen_pri;
740 var i: usize = 0;
741 while (i < self.back_screen.buf.len) : (i += 1) {
742 self.back_screen.buf[i].dirty = true;
743 }
744 },
745 2026 => self.mode.sync = val,
746 else => return,
747 }
748}
749
750pub fn carriageReturn(self: *Terminal) void {
751 self.back_screen.cursor.pending_wrap = false;
752 self.back_screen.cursor.col = if (self.mode.origin)
753 self.back_screen.scrolling_region.left
754 else if (self.back_screen.cursor.col >= self.back_screen.scrolling_region.left)
755 self.back_screen.scrolling_region.left
756 else
757 0;
758}
759
760pub fn horizontalTab(self: *Terminal, n: usize) void {
761 // Get the current cursor position
762 const col = self.back_screen.cursor.col;
763
764 // Find desired final position
765 var i: usize = 0;
766 const final = for (self.tab_stops.items) |ts| {
767 if (ts <= col) continue;
768 i += 1;
769 if (i == n) break ts;
770 } else self.back_screen.width - 1;
771
772 // Move right the delta
773 self.back_screen.cursorRight(final -| col);
774}
775
776pub fn horizontalBackTab(self: *Terminal, n: usize) void {
777 // Get the current cursor position
778 const col = self.back_screen.cursor.col;
779
780 // Find the index of the next backtab
781 const idx = for (self.tab_stops.items, 0..) |ts, i| {
782 if (ts <= col) continue;
783 break i;
784 } else self.tab_stops.items.len - 1;
785
786 const final = if (self.mode.origin)
787 @max(self.tab_stops.items[idx -| (n -| 1)], self.back_screen.scrolling_region.left)
788 else
789 self.tab_stops.items[idx -| (n -| 1)];
790
791 // Move left the delta
792 self.back_screen.cursorLeft(final - col);
793}