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
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
//! crossterm event reader → AppMsg pump.
//!
//! Owns the keyboard. The only place in the codebase allowed to read
//! from crossterm's event source.
//!
//! ## Why not `crossterm::event::EventStream`?
//!
//! The obvious async-ergonomic choice would be `EventStream::next().await`
//! from the `event-stream` feature. We *did* use that originally and it
//! worked until we started aborting and respawning the input actor
//! across `tmux attach` cycles (to stop crossterm from racing tmux for
//! stdin bytes during an attach). At that point every few attaches
//! bosun would deadlock with the whole tokio runtime idle and one
//! unnamed `std::thread` parked in
//! `std::sync::mpmc::array::Channel<T>::recv` ->
//! `_dispatch_semaphore_wait_slow`.
//!
//! That's crossterm's internal `EventStream` background thread
//! (see `event/stream.rs` in crossterm 0.29). It uses a bounded
//! `std::sync::mpsc::sync_channel::<Task>(1)` to receive poll requests
//! from `Stream::poll_next`. On macOS the bounded-channel implementation
//! sits on top of libdispatch semaphores, and dropping the last
//! `SyncSender` (which is what `EventStream::drop` eventually does)
//! doesn't always wake a blocked `recv()` — there's a race between the
//! semaphore signal and the sender drop. The result is that our
//! abort/respawn pattern leaves crossterm's reader thread stranded,
//! tokio task wakeups stop flowing, and the TUI freezes.
//!
//! So we bypass `EventStream` entirely. A `spawn_blocking` worker runs
//! a simple loop of `event::poll(100ms)` + `event::read()` and forwards
//! events to the app via a tokio mpsc.
//!
//! ## Why `UnboundedSender` and not `blocking_send`?
//!
//! Earlier revisions of this file used `tokio::sync::mpsc::Sender`
//! (bounded) with `blocking_send`, and then a hand-rolled try_send +
//! backoff + timeout loop. Both were variants of the same bug: if the
//! event channel is full for any reason (e.g. main is wedged in
//! `perform_attach` and the poller is spamming ticks), the blocking
//! side parks the spawn_blocking thread on a condvar that the
//! shutdown `AtomicBool` can't interrupt. We've burned three debug
//! sessions on this exact shape of issue.
//!
//! The real fix, landed together with the tmux -C control-mode
//! rewrite, is to make `evt_tx` an `UnboundedSender`. Unbounded
//! `send` is synchronous and returns immediately — no parking, no
//! capacity wait, no way for this reader to get stuck trying to
//! deliver. Memory growth in practice is negligible because there's
//! no longer a 1Hz poller filling the channel during long attaches
//! (tmux control-mode notifications drive refreshes instead), and
//! `AppMsg` is small.
use ;
use Arc;
use Duration;
use ;
use mpsc;
use JoinHandle;
use crateAppMsg;
/// Handle returned by [`spawn`]. Drop it (or call [`Handle::shutdown`]
/// explicitly) to stop the input reader. The shutdown flag is checked
/// between every poll iteration (max ~100ms latency).
/// How long `event::poll` waits per iteration for stdin activity.
/// Shorter = more responsive to shutdown, longer = less wake-up
/// overhead. 100ms is comfortably under human perception.
const POLL_TIMEOUT: Duration = from_millis;
/// Spawn the input reader. Events are translated to `AppMsg` and
/// forwarded to `tx` via the sync unbounded `send`. The returned
/// [`Handle`] owns the shutdown flag and the blocking-task join
/// handle; shut it down before handing the tty to another process
/// (e.g. `tmux attach`) so crossterm stops polling stdin.