()
| 54 | */ |
| 55 | /* eslint-disable custom-rules/no-sync-fs -- must be sync to flush before process.exit */ |
| 56 | function cleanupTerminalModes(): void { |
| 57 | if (!process.stdout.isTTY) { |
| 58 | return |
| 59 | } |
| 60 | |
| 61 | try { |
| 62 | // Disable mouse tracking FIRST, before the React unmount tree-walk. |
| 63 | // The terminal needs a round-trip to process this and stop sending |
| 64 | // events; doing it now (not after unmount) gives that time while |
| 65 | // we're busy unmounting. Otherwise events arrive during cooked-mode |
| 66 | // cleanup and either echo to the screen or leak to the shell. |
| 67 | writeSync(1, DISABLE_MOUSE_TRACKING) |
| 68 | // Exit alt screen FIRST so printResumeHint() (and all sequences below) |
| 69 | // land on the main buffer. |
| 70 | // |
| 71 | // Unmount Ink directly rather than writing EXIT_ALT_SCREEN ourselves. |
| 72 | // Ink registered its unmount with signal-exit, so it will otherwise run |
| 73 | // AGAIN inside forceExit() → process.exit(). Two problems with letting |
| 74 | // that happen: |
| 75 | // 1. If we write 1049l here and unmount writes it again later, the |
| 76 | // second one triggers another DECRC — the cursor jumps back over |
| 77 | // the resume hint and the shell prompt lands on the wrong line. |
| 78 | // 2. unmount()'s onRender() must run with altScreenActive=true (alt- |
| 79 | // screen cursor math) AND on the alt buffer. Exiting alt-screen |
| 80 | // here first makes onRender() scribble a REPL frame onto main. |
| 81 | // Calling unmount() now does the final render on the alt buffer, |
| 82 | // unsubscribes from signal-exit, and writes 1049l exactly once. |
| 83 | const inst = instances.get(process.stdout) |
| 84 | if (inst?.isAltScreenActive) { |
| 85 | try { |
| 86 | inst.unmount() |
| 87 | } catch { |
| 88 | // Reconciler/render threw — fall back to manual alt-screen exit |
| 89 | // so printResumeHint still hits the main buffer. |
| 90 | writeSync(1, EXIT_ALT_SCREEN) |
| 91 | } |
| 92 | } |
| 93 | // Catches events that arrived during the unmount tree-walk. |
| 94 | // detachForShutdown() below also drains. |
| 95 | inst?.drainStdin() |
| 96 | // Mark the Ink instance unmounted so signal-exit's deferred ink.unmount() |
| 97 | // early-returns instead of sending redundant EXIT_ALT_SCREEN sequences |
| 98 | // (from its writeSync cleanup block + AlternateScreen's unmount cleanup). |
| 99 | // Those redundant sequences land AFTER printResumeHint() and clobber the |
| 100 | // resume hint on tmux (and possibly other terminals) by restoring the |
| 101 | // saved cursor position. Safe to skip full unmount: this function already |
| 102 | // sends all the terminal-reset sequences, and the process is exiting. |
| 103 | inst?.detachForShutdown() |
| 104 | // Disable extended key reporting — always send both since terminals |
| 105 | // silently ignore whichever they don't implement |
| 106 | writeSync(1, DISABLE_MODIFY_OTHER_KEYS) |
| 107 | writeSync(1, DISABLE_KITTY_KEYBOARD) |
| 108 | // Disable focus events (DECSET 1004) |
| 109 | writeSync(1, DFE) |
| 110 | // Disable bracketed paste mode |
| 111 | writeSync(1, DBP) |
| 112 | // Show cursor |
| 113 | writeSync(1, SHOW_CURSOR) |
no test coverage detected