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
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
//! `tm ls` — the top-level managed-session connector.
//!
//! Why: split out of `session_picker.rs`, which sits at the 500-SLOC
//! production cap. The picker module owns the interactive loop and its pure
//! input→decision seam; deciding whether `tm ls` should even OPEN that loop —
//! scope resolution, the TTY/`--json`/`--all` gate, and the static-renderer
//! fallbacks — is a separate responsibility with one call site (`main.rs`),
//! so it moves rather than making the picker file grow.
//!
//! What: [`should_show_picker`] is the pure gate; [`run_ls_connector`] is the
//! orchestrator that applies it. Everything either touches — fetching,
//! filtering, sorting — still lives in [`super::session_picker`] and is called
//! from here, so there is one implementation of each.
//!
//! #7224: the interactive branch now opens the full-screen
//! [`super::session_tui`] rather than the line-based numbered picker.
//! `--plain` is the opt-out back to the static table.
//!
//! #7224: bare `tm` reaches that same TUI, through this module rather than a
//! second copy of the gate. [`bare_tm_opens_session_tui`] is the decision and
//! [`run_bare_tm_surface`] the dispatch; `guided::try_show_picker` calls the
//! latter where it used to call `run_tty_picker` directly. The numbered picker
//! is still bare `tm`'s fallback — it is the surface that can launch a NEW
//! session, which the TUI cannot, so an empty fleet or a dumb terminal keeps
//! it.
//!
//! Test: the gate is unit-tested by `ls_connector_should_show_picker_*` in
//! `tests_behavior_d_tests.rs`; the orchestrator's argument parsing is covered
//! by `cli_parses_ls_*` in the same file, and its I/O path by the e2e suite.
use IsTerminal as _;
use ;
use ;
/// Decide whether `tm ls` should open the interactive picker or print statically.
///
/// Why: a testable seam that folds every gate into one pure decision so the
/// non-TTY / `--json` / `--all` / `--attached` / empty-list branches are
/// unit-testable without a live terminal or daemon. Requiring BOTH stdin and
/// stdout to be TTYs is the anti-hang guarantee: a piped input (would EOF) or a
/// piped output (must stay a clean pipeable table) both fall through to static
/// output.
/// What: returns `true` only when stdin AND stdout are TTYs, none of `--json`,
/// `--all`, or `--attached` was requested, and at least one session exists.
/// `--all` forces static output because its purpose is the forensic full list
/// (including decommissioned tombstones), not connecting; `--attached` forces it
/// because the sessions it keeps are exactly the ones a client is already on, so
/// the picker's connect action has nothing left to do.
///
/// #7224: `plain` is the operator's explicit "give me the table, not the
/// interactive surface" — the escape hatch for a TTY invocation whose output is
/// meant to be READ rather than acted on. It sits with `json`/`all`/`attached`
/// rather than being a separate gate because they all answer the same question:
/// is this invocation a listing, or a connect?
///
/// #7224: `term` is the `TERM` value, injected rather than read here so the gate
/// stays pure. A TTY is not enough — `TERM=dumb` under a real pty (`script`, an
/// Emacs shell buffer, a CI pty) is two TTYs with no cursor addressing, and the
/// TUI would smear its redraw down the screen after raw mode had already taken
/// the operator's echo away. The predicate is
/// [`trusty_mpm::tui::terminal::term_supports_raw_mode`], shared with `tm f`'s
/// `interactive_filter_allowed` so the two surfaces cannot drift.
/// Test: `ls_connector_should_show_picker_*` and
/// `ls_connector_dumb_term_reaches_the_static_renderer` in
/// `tests_behavior_d_ls_connector_tests.rs`.
// #7224: the eighth input is `TERM`. The alternative to the arity is a flags
// struct built at the one call site and destructured here, which hides the same
// operands behind a type without removing one.
pub
/// Does this invocation print the static table and return before any TUI?
///
/// Why (#7224): raw mode is the one thing `tm ls` must never enter when its
/// output is not a terminal — a piped `tm ls | head` that switched the tty to
/// raw mode would leave the operator's shell without echo. Naming that branch
/// makes "a piped or `--plain` run never reaches the TUI" a unit test rather
/// than something to establish by reading the function. It is also the sole
/// path to [`super::managed::session_ls`] from here, so every static invocation
/// produces one renderer's bytes.
/// What: true for `--json`, `--all`, `--attached`, `--plain`, either stream not
/// being a terminal, or (#7224) a `TERM` that cannot address the cursor. The
/// complement is NOT [`should_show_picker`] — that one additionally requires a
/// non-empty fleet, because zero sessions on a TTY prints the static "no managed
/// sessions" line from a later branch instead.
/// Test: `plain_and_non_tty_reach_the_same_static_renderer`,
/// `ls_connector_should_show_picker_plain_static`,
/// `ls_connector_dumb_term_reaches_the_static_renderer`.
pub
/// `tm ls` — the interactive managed-session connector (top-level).
///
/// Why: bare `tm ls` should do the most useful thing for connecting to the
/// managed fleet: on a real terminal it opens the session TUI; piped,
/// scripted, or `--plain` it degrades to the same static, pipeable list as
/// `tm session ls`.
/// What: resolves the scope (`--current` derives `owner/repo` from the cwd git
/// remote, mirroring `tm session ls`); routes `--json`, `--all`, `--attached`, a
/// `TERM` that cannot address the cursor (#7224), or
/// any non-TTY invocation straight to the static [`super::managed::session_ls`]
/// renderer (preserving its raw `--json` passthrough byte-for-byte); otherwise
/// fetches the live sessions once and either renders the static table (0
/// sessions) or opens the session TUI
/// ([`super::session_tui::run_session_tui`], #7224) for one or more.
/// `attached` is a pure listing filter and never reaches the TUI — see
/// [`should_show_picker`] — so the static renderer is the single place it is
/// applied.
/// Test: parse tests `cli_parses_ls_*` and the gate tests
/// `ls_connector_should_show_picker_*` in `tests_behavior_d_tests.rs`.
pub async
/// Does bare `tm` open the session TUI on this invocation?
///
/// Why (#7224): the owner ruling is that bare `tm` reaches the same surface
/// `tm ls` reaches. Re-spelling the gate here is the defect #7224 round 2 just
/// removed — the `TERM` check lived in `tm f` and not in `tm ls`, so one
/// surface entered raw mode where the other refused. The body is therefore one
/// delegation to [`should_show_picker`], and a gate added there reaches bare
/// `tm` without a second edit.
/// What: `json`, `all`, `attached`, and `plain` are pinned `false` because bare
/// `tm` has no grammar that could set them — it takes no flags and no
/// positionals (a leading token parses as `cli::Command::External`), so
/// `tm recent` and `tm alpha <filter>` are not forms that exist. The one
/// operand bare `tm` alone carries is `managed_pane`: inside a tm-managed pane,
/// bare `tm` is that pane's relaunch verb, so a full-screen surface must never
/// take the pane over there. `tm ls` in the same pane still opens the TUI and
/// reads the same variable for its self-delete guard — two different questions
/// about one fact, not a divergent gate. The operand is a `bool` for the same
/// reason [`super::guided_outside_git::route_bare_tm`]'s is: what counts as a
/// managed pane is
/// [`guided_inplace::resolve_env_managed_session_id`](super::guided_inplace::resolve_env_managed_session_id),
/// the one implementation of that lookup, and re-deciding it here would be a
/// second one.
/// Test: `bare_tm_opens_the_session_tui_on_two_ttys_with_a_capable_term`,
/// `bare_tm_never_opens_the_tui_inside_a_managed_pane`,
/// `bare_tm_and_ls_refuse_the_tui_on_the_same_inputs`.
pub
/// Open bare `tm`'s managed-session surface: the TUI, or the numbered picker.
///
/// Why (#7224): `guided::try_show_picker` used to call
/// [`super::session_picker::run_tty_picker`] unconditionally, so the surface
/// the owner asked for was reachable from `tm ls` and from nothing else.
/// Putting the choice HERE rather than in `guided.rs` is what keeps the gate
/// single: this module already owns "which surface does a managed-session
/// listing open", and `guided.rs` sits one line under the 500-SLOC cap.
/// What: reads `TERM` and the managed-session id at this I/O boundary and
/// injects them into [`bare_tm_opens_session_tui`], mirroring
/// [`run_ls_connector`] — the gate stays pure and `src/bin/tm/**` keeps its
/// no-process-global-env posture (#5544), which these reads never violate. The
/// id comes from
/// [`guided_inplace::resolve_env_managed_session_id`](super::guided_inplace::resolve_env_managed_session_id),
/// the process-env-then-tmux-env chain `try_inplace_relaunch` and
/// `try_outside_git` already ask, rather than a third reading of the variable.
/// The picker is the fallback, not a lesser one: it is the only surface with a
/// launch-new action, which is exactly what an empty fleet needs, and it reads
/// whole lines instead of raw-mode keys, which is exactly what a `TERM` with no
/// cursor addressing needs.
/// Test: the choice is `bare_tm_opens_the_session_tui_*` and
/// `bare_tm_never_opens_the_tui_inside_a_managed_pane`; the two surfaces it
/// dispatches to carry their own.
pub async