1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
//! The app half of `bitrouter chat` — everything the renderer cannot own.
//!
//! `bitrouter-tui` draws; it does not read. Keys, raw mode, and the session's
//! lifetime belong here, because they are properties of *this* process rather
//! than of anything ACP carries.
//!
//! # The three exits
//!
//! The session holds raw mode from its first prompt (see [`input::Stdin`]), so
//! every way out has to give it back. There are three, and each reaches
//! [`bitrouter_tui::lifecycle::restore`] by a different route:
//!
//! | exit | route |
//! |---|---|
//! | normal — Ctrl-C or Ctrl-D at an idle prompt, or stdin ending | `session::run` drops `Stdin` after teardown; `Stdin`'s `Drop` also covers every `?` out of `chat` |
//! | panic | `bitrouter_tui::lifecycle::install_panic_restore`, chained in front of the existing hook |
//! | INT / TERM / HUP | [`signals::Shutdown`] as the one `select!` arm in `session::run`'s single loop |
//!
//! Giving the terminal back is only half of an exit. The other half is the
//! harness child and this session's route leases, and those are
//! `ControlledSession::shutdown`'s — which is why the loop lives in a function
//! whose errors `session::run` *carries* rather than returns. A `?` that
//! escaped would restore the terminal (`Drop` sees to that) and leave the child
//! unreaped.
//!
//! ## Checking it by hand
//!
//! The unit tests cover the arm and the hook, not the terminal: raw mode needs
//! a real tty, and a test that took one would take the *developer's*. So the
//! terminal itself is checked by hand, once per exit. In each case `stty -a`
//! afterwards must report `echo` and `icanon` (a restored terminal), not
//! `-echo` and `-icanon`, and the shell must still echo what you type:
//!
//! ```text
//! bitrouter chat <agent> # then Ctrl-D at the prompt → stty -a | grep -o '\-\?echo'
//! bitrouter chat <agent> # then Ctrl-C at the prompt → stty -a | grep -o '\-\?echo'
//! bitrouter chat <agent> & ; kill -TERM %1 → stty -a | grep -o '\-\?echo'
//! bitrouter chat <agent> & ; kill -HUP %1 → stty -a | grep -o '\-\?echo'
//! ```
//!
//! The panic exit has no keystroke to trigger it; it is covered by
//! `lifecycle`'s `the_panic_hook_restores_and_still_reports`, which pins that
//! `restore` runs *and* that the panic is still reported afterwards.