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