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
//! Verbless, color-coded row rendering for the `tm ls`/bare-`tm` session
//! picker (issue #3723).
//!
//! Why: Bob's feedback on the pre-existing `[N] restart <name> (state)` /
//! `[N] resume <name> (state)` rows — "remove 'restart/resume', that's
//! confusing. Use colors to indicate their current state." The verb (restart
//! vs. resume vs. attach) is an implementation detail chosen at SELECTION
//! time (see `guided_resume::plan_resume`); it tells the operator nothing
//! about the session's CURRENT state and duplicates the state word already
//! printed in parentheses. Extracted out of `session_picker.rs` (which sits
//! near the 500-SLOC production cap) so this new logic doesn't push it over.
//!
//! What: [`picker_use_color`] is the TTY/`NO_COLOR` gate, resolved ONCE per
//! picker invocation at the I/O boundary (`run_tty_picker`) rather than
//! probed per-row — matching this crate's established pattern of an explicit
//! `use_color: bool` parameter threaded into pure formatting functions
//! (`formatters::banner::shade_image`) instead of relying on the `colored`
//! crate's global `colored::control` override, whose process-wide mutable
//! state is documented (`two_panel/tests.rs`) to race across parallel tests.
//! [`format_session_row`] is the pure, verbless row formatter: `[N] <name>
//! (<colored state>)`, with the deleted/unresumable notices preserved
//! unchanged apart from the verb drop. [`colorize`] wraps text in a raw ANSI
//! SGR escape when `use_color` is true, else returns it unchanged — no
//! external color crate dependency, fully deterministic and side-effect-free.
//!
//! #3741: rows lead with the `[N]` index and carry no `tm:` prefix — the
//! prefix is reserved for genuine log/prompt lines (`session_picker.rs`'s
//! `eprintln!("tm: ...")` warnings/prompts), never for menu rows a human
//! scans by leading index.
//!
//! Test: `session_picker_tests.rs` — `format_session_row_*` (verbless output,
//! color on/off), `state_color_*` (mapping), `colorize_*` (escape/plain).
use ManagedSessionSummary;
/// Color category for a picker row's state word.
///
/// Why: a small closed set — rather than raw ANSI codes scattered through the
/// formatter — keeps [`state_color`]'s mapping table the single place state
/// strings turn into a color, and keeps [`colorize`] trivially testable
/// against each variant.
/// What: `Green` (active, not attached), `AttachedCyan` (a client is
/// attached — bold cyan, deliberately distinct from plain `Green`), `Dim`
/// (stopped — gray/faint), `Red` (errored, dead, deleted), `Yellow`
/// (provisioning), `Plain` (unrecognised/future state — never fails closed
/// into a misleading color).
///
/// `Magenta` and `Cyan` are COLUMN hues rather than state hues — the `tm ls`
/// table colors its `NUM` and `NAME` columns through the same [`colorize`]
/// seam, so they share this enum instead of growing a second, parallel one
/// with its own `NO_COLOR` handling. [`state_color`] never returns either.
/// Test: `state_color_*` in `session_picker_tests.rs`.
pub
/// Map a [`StateColor`] to its raw ANSI SGR parameter, or `None` for `Plain`.
///
/// The two column hues are deliberately the plain (non-bold, non-bright) 35/36
/// pair: both render as mid-tone colors that stay legible against a white and a
/// black background, unlike `33` (yellow), which most light themes wash out.
/// Wrap `text` in `color`'s ANSI escape when `use_color` is true; otherwise
/// return `text` unchanged.
///
/// Why: a single seam every colored fragment in this module goes through, so
/// the NO_COLOR/non-TTY fallback (Bob: "respect NO_COLOR / non-TTY by
/// falling back to plain text") is enforced in exactly one place.
/// What: `use_color && ansi_param(color).is_some()` → `"\x1b[{param}m{text}\x1b[0m"`;
/// otherwise `text.to_string()`.
/// Test: `colorize_wraps_when_color_enabled`, `colorize_plain_when_color_disabled`,
/// `colorize_plain_variant_never_wraps_even_with_color_enabled`.
pub
/// Resolve the color for a session's displayed state word.
///
/// Why: centralizes Bob's suggested mapping (active=green, attached=cyan/
/// bold-green, stopped=dim/gray, dead/errored=red, provisioning=yellow) so
/// [`format_session_row`]'s three call shapes (deleted/unresumable/normal)
/// never diverge on what a given state renders as.
/// What: `attached` wins over every `state` value (a client is connected
/// RIGHT NOW, the strongest signal); otherwise `"active"` → `Green`,
/// `"stopped"` → `Dim`, `"errored"` → `Red`, `"provisioning"` → `Yellow`,
/// anything else → `Plain` (never guesses a color for a state this mapping
/// doesn't recognise).
/// Test: `state_color_attached_wins_over_active`, `state_color_maps_known_states`,
/// `state_color_unknown_state_is_plain`.
pub
/// Decide whether the picker should emit color, resolved ONCE per invocation.
///
/// Why: the picker writes its menu via `eprintln!` (stderr), not `println!`
/// — probing `stdout`'s TTY-ness (as `colored`'s own env-based default does)
/// would be the WRONG stream. `session_ls_connector::should_show_picker`
/// already requires stdin
/// AND stdout to be TTYs before the interactive picker ever runs, so a
/// non-TTY stderr here is the narrow remaining case (e.g. `2>file`) this
/// still needs to catch on its own.
/// What: `stderr_tty && NO_COLOR is unset` (any value, including empty,
/// per the NO_COLOR spec — https://no-color.org — disables color).
/// Test: `picker_use_color_requires_tty`,
/// `picker_use_color_false_when_no_color_set_even_on_tty` in
/// `session_picker_tests.rs`.
pub
/// Decide whether the `tm ls` TABLE should emit color, resolved ONCE per
/// invocation.
///
/// Why: separate from [`picker_use_color`] because it gates on a DIFFERENT
/// stream. The picker writes its menu with `eprintln!`; the table writes with
/// `println!`. Reusing the picker's stderr gate would leak ANSI escapes into
/// `tm ls | grep` on any terminal where stderr is still a TTY while stdout is a
/// pipe — which is the normal shape of a piped invocation.
/// What: `stdout_tty && NO_COLOR is unset`, sharing [`color_enabled`]'s rule so
/// the two gates can never disagree about `NO_COLOR`.
/// Test: `table_use_color_requires_stdout_tty`,
/// `table_use_color_false_when_no_color_set_even_on_tty` in
/// `session_picker_tests.rs`.
pub
/// The shared TTY + `NO_COLOR` rule behind both gates.
///
/// Any set `NO_COLOR` value — including the empty string — disables color, per
/// the spec (https://no-color.org).
/// Format one verbless, color-coded picker menu row (issue #3723).
///
/// Why: replaces the old `[N] restart|resume <name> (state)` line — the verb
/// is dropped entirely (an implementation detail of *how* the session will be
/// reached, decided at selection time, not a property of its current state);
/// the state word itself now carries the signal via color, in addition to
/// its plain text (so a NO_COLOR/non-TTY reader loses nothing).
/// What: `sessions[idx].deleted` → the tombstone notice (`-- deleted --`,
/// colored red); `unresumable` → the DEAD notice, its state word colored red;
/// otherwise the normal row, `[N] <name> (<state>)` — `<state>` is
/// `"attached"` when a client is connected (else the raw `state` string),
/// colored via [`state_color`]. Never prints a verb in any branch.
/// Test: `format_session_row_normal_is_verbless`,
/// `format_session_row_attached_uses_attached_word`,
/// `format_session_row_deleted_notice`, `format_session_row_unresumable_notice`,
/// `format_session_row_color_disabled_is_plain_text`.
pub
/// Column width the command legend pads each `[key]` to (#4965).
///
/// Why: the descriptions must line up in BOTH menus — the empty-session
/// variant was aligned while the populated one (the common case) was left
/// ragged, so the two looked like different tools. Sized to the widest key,
/// `[r<N> <new-name>]`, plus one separating space.
const MENU_KEY_WIDTH: usize = 18;
/// Build the picker's command legend — the `[key] description` lines printed
/// under the session rows.
///
/// Why: kept out of `run_tty_picker` for two reasons. This module exists
/// because `session_picker.rs` sits at the 500-SLOC production cap, and the
/// legend is exactly the kind of pure formatting it was split off to hold;
/// and a pure function makes the wording and the column alignment assertable
/// without driving a real TTY loop.
/// What: `launch_slot` is `None` for the no-sessions menu (bare Enter is the
/// launch key, and the delete/rename rows have nothing to target) and
/// `Some(slot)` for the populated one. The `n` row spells out `tm-<name>-NN`
/// because the adjacent `r<N> <new-name>` row takes a VERBATIM full session
/// name — without it the two `<name>` arguments read as interchangeable and
/// are not.
/// Test: `command_legend_empty_menu_shape`, `command_legend_populated_menu_shape`,
/// `command_legend_columns_are_aligned`.
pub
/// Build the restart-confirmation hint shared by both #2148 restart prompts.
///
/// Why: those two prompts read as a yes/no question ("\[Enter\] does NOT restart
/// — type \[1\] to confirm the restart") while naming no refusal key. `n` is the
/// universal "no", and since #4965 it is also the launch-new alias — so an
/// operator declining a restart with `n` got a real cloned, spawned, attached
/// session instead of a decline. The grammar is correct and stays; the prompts
/// were the defect, inviting a token they never mentioned whose meaning is the
/// opposite of the intent. Naming the confirm key, the actual refusal key, and
/// what `n` really does leaves no reading that invites `n` as "no".
/// What: returns one line, no `tm: ` prefix and no trailing period, so each
/// call site can prepend its own context clause.
/// Test: `restart_confirm_hint_names_the_refusal_key`,
/// `restart_confirm_hint_disowns_n_as_no`.
pub