Skip to content

Commit b8eaf45

Browse files
committed
fix(git_chain): restore captured terminal state on exit
1 parent 9ab7bed commit b8eaf45

3 files changed

Lines changed: 34 additions & 8 deletions

File tree

git_chain/CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
**Bug Fixes**
44

5-
- fix mangled / "stairstepped" shell output after a sync that opened the merge tool. nocterm and `git mergetool` can leave the terminal with output post-processing (`ONLCR`) disabled, which Dart's stdin API can't restore; git_chain now runs `stty sane` on exit to put the terminal back.
5+
- restore the terminal to its exact pre-launch state on exit by snapshotting `stty -g` before the TUI and replaying it afterwards. Fixes Ctrl+C not working, no echo, and "stairstepped" output (`ONLCR` off) left behind by nocterm / `git mergetool` — none of which Dart's stdin API can restore on its own.
66

77
## 0.5.5
88

git_chain/lib/src/commands/tui_command.dart

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,10 @@ class TuiCommand extends Command<void> {
2828
// If launched inside a git repo, register it and jump straight to it.
2929
final discovered = await service.registerRepo(Directory.current.path);
3030

31+
// Snapshot the terminal before nocterm touches it, so we can restore the
32+
// exact original modes (echo, Ctrl+C/ISIG, ONLCR) on exit.
33+
captureTerminalState();
34+
3135
final intent = AppIntent();
3236
await runApp(GitChainApp(
3337
db: db,

git_chain/lib/src/util/terminal_input.dart

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -42,17 +42,39 @@ void drainTerminalInput() {
4242
}
4343
}
4444

45-
/// Restores sane terminal modes (echo, canonical input, and crucially output
46-
/// post-processing / `ONLCR`).
45+
/// The terminal settings captured before the TUI started, used to restore the
46+
/// exact original state on exit.
47+
String? _savedTtyState;
48+
49+
/// Snapshots the controlling terminal's settings (`stty -g`) so they can be
50+
/// restored verbatim later. Call once, before `runApp`.
51+
void captureTerminalState() {
52+
if (!(Platform.isMacOS || Platform.isLinux)) return;
53+
try {
54+
final result = Process.runSync('sh', ['-c', 'stty -g < /dev/tty']);
55+
if (result.exitCode == 0) {
56+
final state = (result.stdout as String).trim();
57+
if (state.isNotEmpty) _savedTtyState = state;
58+
}
59+
} catch (_) {
60+
// Best-effort.
61+
}
62+
}
63+
64+
/// Restores the terminal to the exact state captured by [captureTerminalState]
65+
/// (echo, canonical input, signal handling / Ctrl+C, output post-processing /
66+
/// `ONLCR`). Falls back to `stty sane` if no snapshot was taken.
4767
///
48-
/// nocterm and `git mergetool` can leave the terminal with output processing
49-
/// disabled, which makes subsequent shell output "stairstep" (line feeds with
50-
/// no carriage return). Dart's stdin API only restores input flags, so we shell
51-
/// out to `stty sane` against the controlling terminal.
68+
/// Dart's stdin API only restores input flags, and nocterm / `git mergetool`
69+
/// can leave the terminal with signals or output processing disabled — which
70+
/// breaks Ctrl+C and makes shell output "stairstep". Restoring the full
71+
/// snapshot fixes all of it at once.
5272
Future<void> restoreTerminalModes() async {
5373
if (!(Platform.isMacOS || Platform.isLinux)) return;
74+
final saved = _savedTtyState;
75+
final cmd = saved != null ? 'stty $saved < /dev/tty' : 'stty sane < /dev/tty';
5476
try {
55-
await Process.run('sh', ['-c', 'stty sane < /dev/tty']);
77+
await Process.run('sh', ['-c', cmd]);
5678
} catch (_) {
5779
// Best-effort.
5880
}

0 commit comments

Comments
 (0)