this repo has no description
13
fork

Configure Feed

Select the types of activity you want to include in your feed.

at 4320ec29d03415eba80a14b2eaaff8cefa6822e8 793 lines 33 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_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}