a modern tui library written in zig
at main 784 lines 32 kB view raw
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}