Start the child and block until it exits. Returns its exit code.
(self)
| 314 | self._last_activity_ts = now |
| 315 | |
| 316 | def run(self) -> int: |
| 317 | """Start the child and block until it exits. Returns its exit code.""" |
| 318 | if not os.isatty(sys.stdin.fileno()): |
| 319 | raise RuntimeError("ShellTUI requires a TTY on stdin") |
| 320 | |
| 321 | rows, cols = _get_winsize(sys.stdout.fileno()) |
| 322 | shell_rows = max(1, rows - self.footer_height) |
| 323 | |
| 324 | pid, master_fd = pty.fork() |
| 325 | if pid == 0: |
| 326 | child_env = os.environ.copy() |
| 327 | child_env.setdefault("TERM", "xterm-256color") |
| 328 | if self._env: |
| 329 | child_env.update(self._env) |
| 330 | try: |
| 331 | os.execvpe(self.command[0], self.command, child_env) |
| 332 | except OSError as e: |
| 333 | sys.stderr.write(f"exec failed: {e}\n") |
| 334 | os._exit(127) |
| 335 | |
| 336 | self._master_fd = master_fd |
| 337 | _set_winsize(master_fd, shell_rows, cols) |
| 338 | |
| 339 | # Non-blocking master so ``_drain_and_paint`` can read until |
| 340 | # ``BlockingIOError`` and emit the whole batch + footer atomically. |
| 341 | fcntl.fcntl( |
| 342 | master_fd, |
| 343 | fcntl.F_SETFL, |
| 344 | fcntl.fcntl(master_fd, fcntl.F_GETFL) | os.O_NONBLOCK, |
| 345 | ) |
| 346 | |
| 347 | # Disable echo + canonical mode on the PTY. Without this the line |
| 348 | # discipline echoes anything we forward into the child's stdin |
| 349 | # (``os.write(master_fd, ...)``) back through ``master_fd``'s read |
| 350 | # side, where we then paint it to the user's stdout. The user sees |
| 351 | # CSI-fragment artifacts (``[3;153R``, ``[?62;...c`` etc.) whenever |
| 352 | # their real terminal answers a DSR/DA query the child issued. |
| 353 | # Cbreak is the minimal-surgery option: clears ``ECHO`` and |
| 354 | # ``ICANON`` but leaves ``ISIG`` on so Ctrl-C still signals the |
| 355 | # child. Most TUIs (Claude Code's Ink included) reset their tty |
| 356 | # to raw mode shortly after exec, but during the startup window |
| 357 | # — and any time the terminal answers asynchronously — echo |
| 358 | # would otherwise leak. |
| 359 | try: |
| 360 | tty.setcbreak(master_fd) |
| 361 | except termios.error: |
| 362 | pass # best-effort — never fail launch on a quirky platform |
| 363 | |
| 364 | stdin_fd = sys.stdin.fileno() |
| 365 | old_termios = termios.tcgetattr(stdin_fd) |
| 366 | tty.setraw(stdin_fd) |
| 367 | old_winch = signal.signal(signal.SIGWINCH, self._on_winch) |
| 368 | |
| 369 | self._write(CSI + b"2J") |
| 370 | self._apply_scroll_region(shell_rows) |
| 371 | self._draw_footer(rows, cols) |
| 372 | |
| 373 | footer_thread = threading.Thread(target=self._footer_loop, daemon=True) |
no test coverage detected