@lamemind/loom-deck 0.55.0 → 0.56.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/actions.js +291 -0
- package/dist/cli.js +130 -1584
- package/dist/deck-model.js +348 -0
- package/dist/frame.js +178 -0
- package/dist/inbox.js +135 -0
- package/dist/input.js +469 -0
- package/dist/overlays/modals.js +231 -0
- package/dist/overlays/purge.js +44 -0
- package/dist/plugin-cache.js +106 -0
- package/dist/task-ops.js +235 -0
- package/dist/ui/screens.js +201 -0
- package/dist/wrap-scan.js +122 -0
- package/package.json +1 -1
package/dist/input.js
ADDED
|
@@ -0,0 +1,469 @@
|
|
|
1
|
+
// Il DISPATCH dell'input: tastiera e mouse, un ramo per tasto, nessun corpo di
|
|
2
|
+
// azione.
|
|
3
|
+
//
|
|
4
|
+
// Esce per ultimo dallo split (T131) e la sua firma è la prova del perché:
|
|
5
|
+
// riceve sei oggetti. Estratto per primo ne avrebbe richiesti venticinque —
|
|
6
|
+
// `onKey` tocca il focus, la selezione, la vista, le voci launch, l'identità,
|
|
7
|
+
// le conversazioni, i pin, i binding, i quattro overlay, la geometria e una
|
|
8
|
+
// dozzina di funzioni. Dopo che modello, attuatore e overlay hanno un nome,
|
|
9
|
+
// ognuna di quelle cose vive dentro uno di sei oggetti. È la lezione di T104
|
|
10
|
+
// ripetuta: l'input non si estrae per primo, si estrae quando ciò che dispaccia
|
|
11
|
+
// ha smesso di essere anonimo.
|
|
12
|
+
//
|
|
13
|
+
// `frame` arriva come VALORE già calcolato, non come closure. Prima di T131 la
|
|
14
|
+
// geometria delle liste nasceva seicento righe più in basso di `onListClick` e
|
|
15
|
+
// funzionava solo perché la closure di `useInput` legge il binding quando
|
|
16
|
+
// l'evento arriva, cioè dopo la fine del render. Con il dispatch in un hook
|
|
17
|
+
// chiamato in testa al componente quel binding non esisterebbe ancora, quindi
|
|
18
|
+
// il calcolo della geometria è stato hoistato sopra la chiamata. Il
|
|
19
|
+
// comportamento non cambia: il mouse esce comunque su `mode !== 'normal'`.
|
|
20
|
+
import { useEffect, useRef } from 'react';
|
|
21
|
+
import { useApp, useInput } from 'ink';
|
|
22
|
+
import { hitRegion, isWheel, listHit, takeMouse, wheelDir, WHEEL_LINES, } from './mouse.js';
|
|
23
|
+
import { LAUNCH_ROW } from './frame.js';
|
|
24
|
+
import { moveSelection } from './session-list.js';
|
|
25
|
+
import { captures, scrolls } from './input-modes.js';
|
|
26
|
+
import { META_ROWS, QUIT_WINDOW_MS } from './model.js';
|
|
27
|
+
import { TASK_VIEWS, taskView } from './pane-views.js';
|
|
28
|
+
import { loadTaskFileText } from './tasks.js';
|
|
29
|
+
// T116 — l'avviso della prima pressione di `^C`. La durata è INTERPOLATA dalla
|
|
30
|
+
// finestra, non ricopiata: un numero scritto a mano in un testo che promette un
|
|
31
|
+
// comportamento diventa falso il giorno che la costante cambia, e nessuno
|
|
32
|
+
// strumento lo segnala. Vive a livello di modulo perché ha due lettori — il ramo
|
|
33
|
+
// che la scrive e il timer che la ritira, e quest'ultimo deve riconoscerla.
|
|
34
|
+
export const QUIT_NOTE = `⚠ ^C di nuovo entro ${QUIT_WINDOW_MS / 1000}s per chiudere il deck`;
|
|
35
|
+
// T21 — il `Key` che accompagna un tasto sintetizzato da un click: nessun
|
|
36
|
+
// modificatore, nessun tasto speciale. Deve elencare ogni campo, o i rami che
|
|
37
|
+
// leggono `key.ctrl` o `key.tab` riceverebbero `undefined` invece di `false` —
|
|
38
|
+
// equivalente nel test di verità, ma non nel tipo.
|
|
39
|
+
export const NO_MODIFIERS = {
|
|
40
|
+
upArrow: false,
|
|
41
|
+
downArrow: false,
|
|
42
|
+
leftArrow: false,
|
|
43
|
+
rightArrow: false,
|
|
44
|
+
pageDown: false,
|
|
45
|
+
pageUp: false,
|
|
46
|
+
return: false,
|
|
47
|
+
escape: false,
|
|
48
|
+
ctrl: false,
|
|
49
|
+
shift: false,
|
|
50
|
+
tab: false,
|
|
51
|
+
backspace: false,
|
|
52
|
+
delete: false,
|
|
53
|
+
meta: false,
|
|
54
|
+
};
|
|
55
|
+
export function useDeckInput({ tasksDir, mode, setMode, setNote, model, actions, overlays, frame, launchRegions, }) {
|
|
56
|
+
// T116 — uscita a doppio `^C`. Il timer È lo stato dell'armamento: finché il
|
|
57
|
+
// handle esiste la finestra è aperta, e non serve un secondo stato da tenere
|
|
58
|
+
// in fase con lui. In un `useRef` e non in `useState` perché il ramo di uscita
|
|
59
|
+
// lo legge NELLO STESSO tasto in cui potrebbe averlo scritto — un valore di
|
|
60
|
+
// stato React arriverebbe al render dopo, cioè troppo tardi per decidere.
|
|
61
|
+
const { exit } = useApp();
|
|
62
|
+
const quitTimer = useRef(null);
|
|
63
|
+
// Il timer pendente è un handle vivo del loop di Node: lasciarlo appeso allo
|
|
64
|
+
// smontaggio terrebbe il processo in piedi fino allo scadere della finestra,
|
|
65
|
+
// con lo schermo già restituito al terminale.
|
|
66
|
+
useEffect(() => () => {
|
|
67
|
+
if (quitTimer.current)
|
|
68
|
+
clearTimeout(quitTimer.current);
|
|
69
|
+
}, []);
|
|
70
|
+
// Il dispatch consulta il CATALOGO (`input-modes.ts`), non l'ordine di una
|
|
71
|
+
// catena di `if`. `MODE_KEYS` è il custode di compilazione: essendo un
|
|
72
|
+
// `Record` su `CapturingMode`, un modo nuovo senza handler non compila.
|
|
73
|
+
// T131 — e ogni voce viene da un overlay, mai da una funzione dichiarata qui:
|
|
74
|
+
// è ciò che `test/input-wiring.test.ts` verifica, dopo che T112 aveva
|
|
75
|
+
// dimostrato che il solo `Record` non lo impedisce.
|
|
76
|
+
const MODE_KEYS = {
|
|
77
|
+
detail: overlays.sheet.onKey,
|
|
78
|
+
status: overlays.status.onKey,
|
|
79
|
+
reader: overlays.search.onReaderKey,
|
|
80
|
+
search: overlays.search.onSearchKey,
|
|
81
|
+
assign: overlays.assign.onKey,
|
|
82
|
+
create: overlays.text.onCreateKey,
|
|
83
|
+
note: overlays.text.onNoteKey,
|
|
84
|
+
sort: overlays.view.onSortKey,
|
|
85
|
+
filter: overlays.view.onFilterKey,
|
|
86
|
+
edit: overlays.text.onEditKey,
|
|
87
|
+
purge: overlays.purge.onKey,
|
|
88
|
+
};
|
|
89
|
+
// T21 (mandata 2) — la ROTELLA, per i soli modi che scorrono un contenuto
|
|
90
|
+
// lungo (`SCROLLING_MODES`). Stessa forma di `MODE_KEYS`: un `Record` sul
|
|
91
|
+
// catalogo, quindi un modo dichiarato scorrevole senza uno scroll da chiamare
|
|
92
|
+
// non compila. Il delta è in righe, col segno del verso.
|
|
93
|
+
const MODE_WHEEL = {
|
|
94
|
+
detail: overlays.sheet.scroll,
|
|
95
|
+
status: overlays.status.scroll,
|
|
96
|
+
reader: overlays.search.scrollReader,
|
|
97
|
+
};
|
|
98
|
+
/**
|
|
99
|
+
* T116 — `^C`: la prima pressione ARMA, la seconda entro la finestra chiude.
|
|
100
|
+
*
|
|
101
|
+
* Prima di questa task `^C` usciva subito, e a chiuderlo era Ink stesso
|
|
102
|
+
* (`exitOnCtrlC`, di default vero) PRIMA che il tasto raggiungesse
|
|
103
|
+
* `useInput`. Spegnere quell'opzione è la condizione perché il ramo esista —
|
|
104
|
+
* ma spegnerla e basta lascerebbe il deck senza NESSUNA uscita da tastiera:
|
|
105
|
+
* il ramo qui sotto è ciò che restituisce quella via, e i due cambi non si
|
|
106
|
+
* possono separare.
|
|
107
|
+
*
|
|
108
|
+
* La prima pressione RIPORTA IN `normal` quando si è dentro un modo
|
|
109
|
+
* capturing. L'avviso vive nella riga di stato, e le tre schermate
|
|
110
|
+
* sostitutive (detail, ricerca, reader) prendono l'intero frame senza
|
|
111
|
+
* renderizzarla: armare restando lì dentro darebbe un deck che sembra
|
|
112
|
+
* ignorare il tasto e che alla pressione successiva sparisce senza aver mai
|
|
113
|
+
* avvertito. Chiudere lo strato è anche coerente con `esc` — `^C` dice
|
|
114
|
+
* «voglio uscire», e il primo passo fuori è la lista.
|
|
115
|
+
*/
|
|
116
|
+
function onQuitKey() {
|
|
117
|
+
if (quitTimer.current) {
|
|
118
|
+
clearTimeout(quitTimer.current);
|
|
119
|
+
quitTimer.current = null;
|
|
120
|
+
exit();
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
if (captures(mode))
|
|
124
|
+
setMode('normal');
|
|
125
|
+
setNote(QUIT_NOTE);
|
|
126
|
+
quitTimer.current = setTimeout(() => {
|
|
127
|
+
quitTimer.current = null;
|
|
128
|
+
// Solo se l'avviso è ANCORA il proprio: nei 5 secondi una qualunque altra
|
|
129
|
+
// azione può aver scritto in riga di stato, e cancellare quella nota
|
|
130
|
+
// significherebbe far sparire il feedback di un'azione che non c'entra.
|
|
131
|
+
setNote((n) => (n === QUIT_NOTE ? '' : n));
|
|
132
|
+
}, QUIT_WINDOW_MS);
|
|
133
|
+
}
|
|
134
|
+
function onMouse(ev) {
|
|
135
|
+
// Un click produce due eventi (pressione e rilascio): agire su entrambi
|
|
136
|
+
// spawnerebbe due volte. La rotella arriva come sola pressione, quindi il
|
|
137
|
+
// filtro su `press` la lascia passare e non la dedoppia.
|
|
138
|
+
if (!ev.press)
|
|
139
|
+
return;
|
|
140
|
+
if (isWheel(ev.button)) {
|
|
141
|
+
// La rotella NON rientra dalla porta della tastiera come il click: nel
|
|
142
|
+
// detail `↑↓` muovono il fuoco fra i campi (T117), non il testo, e il
|
|
143
|
+
// tasto che scorre — `PgUp`/`PgDn` — ha la granularità sbagliata per una
|
|
144
|
+
// tacca. Va quindi allo scroll del modo, in righe. Fuori dai modi
|
|
145
|
+
// scorrevoli è inerte: nelle liste la rotella non muove mai la
|
|
146
|
+
// selezione (D5), e una tacca mentre il deck è sulla lista non deve
|
|
147
|
+
// fare niente.
|
|
148
|
+
if (scrolls(mode))
|
|
149
|
+
MODE_WHEEL[mode](wheelDir(ev.button) * WHEEL_LINES);
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
// Il bottone si legge sui due bit bassi, perché gli alti portano i
|
|
153
|
+
// modificatori (shift/meta/ctrl).
|
|
154
|
+
if ((ev.button & 3) !== 0)
|
|
155
|
+
return;
|
|
156
|
+
// Le superfici esistono solo nella lista: un modo capturing prende il
|
|
157
|
+
// frame intero e a quella riga c'è dell'altro.
|
|
158
|
+
if (mode !== 'normal')
|
|
159
|
+
return;
|
|
160
|
+
if (ev.row === LAUNCH_ROW) {
|
|
161
|
+
const hit = hitRegion(launchRegions, ev.col);
|
|
162
|
+
// Il click NON chiama l'azione: rientra dalla porta della tastiera col
|
|
163
|
+
// tasto che la superficie annuncia. Un ramo nuovo su `t`/`c`/cifre nasce
|
|
164
|
+
// così già cliccabile, e non esiste un secondo posto in cui il click possa
|
|
165
|
+
// dire una cosa diversa dal tasto che gli sta scritto sopra.
|
|
166
|
+
if (hit)
|
|
167
|
+
onKey(hit, NO_MODIFIERS);
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
onListClick(ev);
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* T21 (mandata 3) — click su un elemento di lista = SOLO FUOCO (D2): il pane
|
|
174
|
+
* prende il focus e la selezione va sull'elemento, nessuna azione parte. Qui
|
|
175
|
+
* il click non sintetizza un tasto, perché nessun tasto nomina una riga o una
|
|
176
|
+
* vista — `↑↓` e `tab` sono relativi — e chiama i setter che la tastiera
|
|
177
|
+
* usa a sua volta. Un click sulla vista già attiva si limita al focus.
|
|
178
|
+
*
|
|
179
|
+
* ECCEZIONE: la riga già selezionata col pane già a fuoco. Lì un tasto che
|
|
180
|
+
* la nomina ESISTE — `⏎`, che agisce proprio su «la riga selezionata» — e il
|
|
181
|
+
* click lo sintetizza, come le superfici della riga launch: due click sulla
|
|
182
|
+
* stessa riga aprono il detail della task o fanno il resume della
|
|
183
|
+
* conversazione, e ogni caso limite (pin stale, nessuna sessione) resta per
|
|
184
|
+
* costruzione quello del tasto. Le righe meta ne sono fuori: `⏎` su di loro
|
|
185
|
+
* non fa nulla, e un click che lo sintetizzasse produrrebbe solo la nota di
|
|
186
|
+
* scarto di `selectedTaskOr`.
|
|
187
|
+
*/
|
|
188
|
+
function onListClick(ev) {
|
|
189
|
+
if (!frame.listGeometry)
|
|
190
|
+
return;
|
|
191
|
+
const hit = listHit(ev, frame.listGeometry);
|
|
192
|
+
if (!hit)
|
|
193
|
+
return;
|
|
194
|
+
if (hit.target === 'view') {
|
|
195
|
+
model.setFocus(hit.pane);
|
|
196
|
+
if (hit.pane === 'tasks')
|
|
197
|
+
model.selectTaskView(hit.key);
|
|
198
|
+
else
|
|
199
|
+
model.selectSessionView(hit.key);
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
if (hit.pane === 'tasks') {
|
|
203
|
+
// Le due righe meta hanno indice fisso; le task riportano l'indice di
|
|
204
|
+
// finestra a quello della lista completa, su cui è keyata la selezione.
|
|
205
|
+
const index = hit.index < META_ROWS ? hit.index : frame.taskWin.start + hit.index;
|
|
206
|
+
if (model.focus === 'tasks' && index === model.selIndex && index >= META_ROWS) {
|
|
207
|
+
onKey('', { ...NO_MODIFIERS, return: true });
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
model.setFocus('tasks');
|
|
211
|
+
model.selectTaskRow(index);
|
|
212
|
+
}
|
|
213
|
+
else {
|
|
214
|
+
const row = frame.windowRows[hit.index];
|
|
215
|
+
if (!row)
|
|
216
|
+
return;
|
|
217
|
+
if (model.focus === 'sessions' && row.sessionId === model.selSessionId) {
|
|
218
|
+
onKey('', { ...NO_MODIFIERS, return: true });
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
model.setFocus('sessions');
|
|
222
|
+
model.setSelSessionId(row.sessionId);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
function onKey(input, key) {
|
|
226
|
+
// T116 — `^C` sta SOPRA il dispatch dei modi, unico tasto a scavalcarlo.
|
|
227
|
+
// Ogni altra combo `ctrl` è un acceleratore, e un acceleratore dentro un
|
|
228
|
+
// modo capturing dev'essere inerte (`input-modes.ts`); questo non è un
|
|
229
|
+
// acceleratore ma la via d'uscita dal processo, e una via d'uscita che
|
|
230
|
+
// funziona solo in una schermata su undici non è una via d'uscita. Non è
|
|
231
|
+
// quindi una deroga di `CTRL_DEROGATIONS` — quelle vivono DENTRO un modo e
|
|
232
|
+
// le gestisce il modo, questa li precede tutti.
|
|
233
|
+
if (key.ctrl && input === 'c') {
|
|
234
|
+
onQuitKey();
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
// Un modo capturing consuma TUTTO — acceleratori globali compresi — e le
|
|
238
|
+
// deroghe se le gestisce da sé (`CTRL_DEROGATIONS`). Da qui in giù si è
|
|
239
|
+
// quindi in `normal`, l'unico modo che cede ai `key.ctrl`.
|
|
240
|
+
if (captures(mode)) {
|
|
241
|
+
MODE_KEYS[mode](input, key);
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
// T52/D1 — il ramo CTRL sta PRIMA di quelli su lettera nuda e li chiude
|
|
245
|
+
// tutti. `CTRL+F` e `f` nudo arrivano con lo STESSO `input` ('f'),
|
|
246
|
+
// distinguibili solo da `key.ctrl`: senza questa precedenza `CTRL+F`
|
|
247
|
+
// cadrebbe nel ramo fork e spawnerebbe una sessione invece di aprire la
|
|
248
|
+
// ricerca. Non è un caso isolato della `f` — `^Q` finirebbe nel quit, `^T`
|
|
249
|
+
// aprirebbe un terminale, `^C` … ogni lettera legata a un'azione ha la sua
|
|
250
|
+
// combo omonima. Chiudere qui l'intera classe è più solido che ricordarsi
|
|
251
|
+
// un `!key.ctrl` su ognuno dei rami, oggi e a ogni tasto aggiunto domani.
|
|
252
|
+
if (key.ctrl) {
|
|
253
|
+
if (input === 'f') {
|
|
254
|
+
setNote('');
|
|
255
|
+
setMode('search');
|
|
256
|
+
}
|
|
257
|
+
else if (input === 'k') {
|
|
258
|
+
actions.spawnTaskSession('recap', '^K');
|
|
259
|
+
}
|
|
260
|
+
else if (input === 'p') {
|
|
261
|
+
actions.spawnTaskSession('preflight', '^P');
|
|
262
|
+
}
|
|
263
|
+
else if (input === 'r') {
|
|
264
|
+
actions.spawnTaskSession('run', '^R');
|
|
265
|
+
}
|
|
266
|
+
else if (input === 'g') {
|
|
267
|
+
// T121 — GENERA e APRI su due tasti distinti (D2): aprire un recap
|
|
268
|
+
// vecchio deve costare zero, e generare non deve essere un effetto
|
|
269
|
+
// collaterale del guardare. Nessuno dei due è preso né bruciato da Ink,
|
|
270
|
+
// e `^S`/`^Q` sono evitati perché il terminale li intercetta come flow
|
|
271
|
+
// control.
|
|
272
|
+
overlays.status.generate();
|
|
273
|
+
}
|
|
274
|
+
else if (input === 'o') {
|
|
275
|
+
overlays.status.open();
|
|
276
|
+
}
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
if (key.tab) {
|
|
280
|
+
// `tab` cicla la VISTA del pane a fuoco, `←→` spostano il focus fra i due
|
|
281
|
+
// pane: il criterio dell'assegnazione è spaziale. Una freccia orizzontale
|
|
282
|
+
// porta con sé una direzione e i pane sono affiancati (task a sinistra,
|
|
283
|
+
// sessioni a destra), quindi il tasto NOMINA il pane invece di limitarsi
|
|
284
|
+
// a scambiarlo; un catalogo ciclico non ha un verso da rispettare, e un
|
|
285
|
+
// tasto solo gli basta. `shift+tab` (backtab, `[Z`) scorre a rovescio — il
|
|
286
|
+
// verso che un tasto singolo non esprime sta nel modificatore, e su cinque
|
|
287
|
+
// voci risparmia quattro pressioni.
|
|
288
|
+
model.cycleView(key.shift ? -1 : 1);
|
|
289
|
+
}
|
|
290
|
+
else if (key.leftArrow || key.rightArrow) {
|
|
291
|
+
// Binding ASSOLUTO, non toggle: `←` porta sempre sui task e `→` sempre
|
|
292
|
+
// sulle sessioni, quindi ripremere lo stesso tasto non riporta indietro.
|
|
293
|
+
// È ciò che lo rende spaziale — la direzione indica una destinazione, e
|
|
294
|
+
// con due soli pane un toggle sarebbe indistinguibile solo per caso.
|
|
295
|
+
model.setFocus(key.leftArrow ? 'tasks' : 'sessions');
|
|
296
|
+
}
|
|
297
|
+
else if (key.upArrow) {
|
|
298
|
+
if (model.focus === 'tasks')
|
|
299
|
+
model.moveTaskSel(-1);
|
|
300
|
+
else
|
|
301
|
+
model.setSelSessionId((id) => moveSelection(model.sessionRows, id, -1));
|
|
302
|
+
}
|
|
303
|
+
else if (key.downArrow) {
|
|
304
|
+
if (model.focus === 'tasks')
|
|
305
|
+
model.moveTaskSel(1);
|
|
306
|
+
else
|
|
307
|
+
model.setSelSessionId((id) => moveSelection(model.sessionRows, id, 1));
|
|
308
|
+
}
|
|
309
|
+
else if (key.return) {
|
|
310
|
+
if (model.focus === 'tasks') {
|
|
311
|
+
// T66 — ⏎ apre il DETAIL, non più una sessione. Secondo rimappaggio in
|
|
312
|
+
// due task (T56 lo spostò da recap a sessione a mani nude), e la
|
|
313
|
+
// direzione è una sola: da azione singola a punto d'ingresso. Il tasto
|
|
314
|
+
// più battuto non è il posto dove inchiodare una scelta di prompt — le
|
|
315
|
+
// shortcut CTRL restano per chi sa già cosa vuole, `⏎` apre il ventaglio.
|
|
316
|
+
// Lo spawn a mani nude di prima è `open`, cioè `⏎ ⏎` (è il focus iniziale).
|
|
317
|
+
const task = actions.selectedTaskOr('⏎', 'aprire');
|
|
318
|
+
if (task) {
|
|
319
|
+
overlays.sheet.open({
|
|
320
|
+
id: task.id,
|
|
321
|
+
// Il titolo del task file quando c'è (è l'H1, cioè la forma
|
|
322
|
+
// lunga), la riga di tasks.md altrimenti: il detail non deve
|
|
323
|
+
// restare senza intestazione solo perché il file manca.
|
|
324
|
+
title: model.detail?.title || task.desc,
|
|
325
|
+
text: loadTaskFileText(tasksDir, task.id),
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
else {
|
|
330
|
+
// T49 — ⏎ su una sessione = resume in nuova tab. Il binding si rilegge
|
|
331
|
+
// dal sidecar (non dal padre selezionato): vale anche per le spot.
|
|
332
|
+
const s = model.selSessionObj;
|
|
333
|
+
if (!s) {
|
|
334
|
+
setNote(model.selSessionId
|
|
335
|
+
? 'pin stale: transcript non più presente'
|
|
336
|
+
: 'nessuna sessione da riprendere');
|
|
337
|
+
}
|
|
338
|
+
else {
|
|
339
|
+
actions.resumeSession(s.sessionId);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
else if (key.delete) {
|
|
344
|
+
// T112/D1 — `CANC` non esiste come tasto distinto in Ink: `\x7f`
|
|
345
|
+
// (Backspace) e `\x1b[3~` (Canc) collassano entrambi su `key.delete` con
|
|
346
|
+
// `input` azzerato, quindi a valle non resta niente da cui distinguerli.
|
|
347
|
+
// Il binding vale per entrambi e si accetta come tale: in `normal` non
|
|
348
|
+
// c'è nessun campo di testo da cui Backspace possa rubare un significato,
|
|
349
|
+
// e ogni modo che ne ha uno è capturing e non vede questo ramo. Il modale
|
|
350
|
+
// di conferma è la rete contro chi usa Backspace come «indietro».
|
|
351
|
+
overlays.purge.open();
|
|
352
|
+
}
|
|
353
|
+
else if (input === 'C') {
|
|
354
|
+
overlays.text.openCreate();
|
|
355
|
+
}
|
|
356
|
+
else if (input === 'E') {
|
|
357
|
+
// L'edit ha senso solo su una task reale: le righe meta non ne sono.
|
|
358
|
+
if (!model.selTask)
|
|
359
|
+
setNote('E → nessuna task selezionata');
|
|
360
|
+
else
|
|
361
|
+
overlays.text.openEdit(model.selTask);
|
|
362
|
+
}
|
|
363
|
+
else if (input === 'S') {
|
|
364
|
+
overlays.view.openSort();
|
|
365
|
+
}
|
|
366
|
+
else if (input === 'F') {
|
|
367
|
+
// T100/D3 — i filtri valgono SOLO sulla vista principale: su `nascoste`
|
|
368
|
+
// riapplicarli non ha senso (quella vista È il loro complemento), su
|
|
369
|
+
// `archiviabili` non li si vuole (è cieca ai filtri per decisione). Stessa
|
|
370
|
+
// forma dell'inerzia di ^K/^P/^R dentro il detail, ma keyed sulla vista
|
|
371
|
+
// invece che sul modo — e come là, l'inerzia lo DICE invece di non fare
|
|
372
|
+
// niente in silenzio.
|
|
373
|
+
if (model.taskViewId !== 'tasks') {
|
|
374
|
+
setNote(`F → filtri: solo sulla vista ${TASK_VIEWS[0].label(model.taskCounts)} (ora: ${taskView(model.taskViewId).label(model.taskCounts)})`);
|
|
375
|
+
}
|
|
376
|
+
else {
|
|
377
|
+
overlays.view.openFilter();
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
else if (input === 'f') {
|
|
381
|
+
// T28 — fork della sessione selezionata. Minuscola come `t`/`c` (T39):
|
|
382
|
+
// azione immediata, nessun modale — la `F` maiuscola resta ai filtri.
|
|
383
|
+
actions.forkSession();
|
|
384
|
+
}
|
|
385
|
+
else if (input === 'p') {
|
|
386
|
+
// T50 — pin/unpin, gemella di `f`: azione immediata sulla riga
|
|
387
|
+
// selezionata del pane sessioni.
|
|
388
|
+
actions.togglePin();
|
|
389
|
+
}
|
|
390
|
+
else if (input === 'N') {
|
|
391
|
+
// T53 — nota sulla conversazione selezionata. MAIUSCOLA perché apre un
|
|
392
|
+
// modale: nel deck le minuscole sono azioni immediate (`f` fork, `p` pin,
|
|
393
|
+
// `t` term, `c` claude) e le maiuscole aprono un box (`C` create, `E`
|
|
394
|
+
// edit, `S` sort, `F` filtri). Vincolo di focus identico a `p`: vale anche
|
|
395
|
+
// su una pinnata STALE, perché annotare «questa non c'è più, era X» è
|
|
396
|
+
// proprio il caso in cui una nota serve.
|
|
397
|
+
if (model.focus !== 'sessions') {
|
|
398
|
+
setNote('N → titolo: seleziona una sessione (→ per il pane)');
|
|
399
|
+
}
|
|
400
|
+
else if (!model.selSessionId) {
|
|
401
|
+
setNote('N → nessuna sessione da annotare');
|
|
402
|
+
}
|
|
403
|
+
else {
|
|
404
|
+
overlays.text.openNote(model.selSessionId);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
else if (input === 'A') {
|
|
408
|
+
// T57 — assegna la conversazione selezionata a una task. MAIUSCOLA perché
|
|
409
|
+
// apre un modale (convenzione T39), e il modale è obbligatorio: il pane
|
|
410
|
+
// task non può fare da picker, perché spostare la selezione lì cambia il
|
|
411
|
+
// parent e la sessione da assegnare sparisce dalla lista sotto le mani.
|
|
412
|
+
// Vincolo di focus identico a `p`/`N`, e vale anche su una pinnata STALE:
|
|
413
|
+
// il binding è nostro, il transcript è di CC — riassegnare una
|
|
414
|
+
// conversazione il cui transcript non c'è più resta legittimo.
|
|
415
|
+
if (model.focus !== 'sessions') {
|
|
416
|
+
setNote('A → assegna: seleziona una sessione (→ per il pane)');
|
|
417
|
+
}
|
|
418
|
+
else if (!model.selSessionId) {
|
|
419
|
+
setNote('A → nessuna sessione da assegnare');
|
|
420
|
+
}
|
|
421
|
+
else {
|
|
422
|
+
overlays.assign.open(model.selSessionId, model.bindings.get(model.selSessionId));
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
else if (input === 't') {
|
|
426
|
+
actions.openTerminal();
|
|
427
|
+
}
|
|
428
|
+
else if (input === 'c') {
|
|
429
|
+
// Minuscola = azione immediata (convenzione T39), gemella di `t`: entrambe
|
|
430
|
+
// aprono una surface del cappello senza passare da un modale. `C` (create
|
|
431
|
+
// task) resta distinta — stessa lettera, ma la maiuscola è per i modali.
|
|
432
|
+
actions.openClaude();
|
|
433
|
+
}
|
|
434
|
+
else if (input === 'w') {
|
|
435
|
+
actions.saveCurrentView();
|
|
436
|
+
}
|
|
437
|
+
else if (input && /^[1-9]$/.test(input)) {
|
|
438
|
+
actions.runLaunchAt(input);
|
|
439
|
+
}
|
|
440
|
+
// Nessun tasto NUDO di uscita: `q` resta libera per un binding futuro, e
|
|
441
|
+
// `esc` in modalità normale è inerte — dentro un overlay continua a
|
|
442
|
+
// chiuderlo, perché quel ramo esce prima di qui. L'unica uscita da tastiera
|
|
443
|
+
// è `^C` battuto due volte (T116, in testa a questo handler); resta poi la
|
|
444
|
+
// chiusura della tab Ptyxis che ospita il processo.
|
|
445
|
+
}
|
|
446
|
+
/**
|
|
447
|
+
* T21 — il MOUSE precede tutto, in ogni modo del deck.
|
|
448
|
+
*
|
|
449
|
+
* Il filtro è incondizionato — anche dentro un campo di testo, anche in un
|
|
450
|
+
* modo che non ha nessuna superficie cliccabile — e non è una precauzione:
|
|
451
|
+
* `useInput` riceve le sequenze SGR come testo (`mouse.ts`, fatto ①), quindi
|
|
452
|
+
* senza filtro un click battuto mentre è aperto il campo del titolo ci
|
|
453
|
+
* scriverebbe dentro `[<0;5;3M`. Un listener raw su stdin non risolverebbe:
|
|
454
|
+
* riceve gli stessi chunk in broadcast e non li sottrae a `useInput`.
|
|
455
|
+
*
|
|
456
|
+
* Il chunk MISTO — un tasto e un click nella stessa scrittura, che arriva
|
|
457
|
+
* come una sola chiamata — si separa a mano: gli eventi vanno al mouse, il
|
|
458
|
+
* testo residuo prosegue verso la tastiera. Solo un chunk di solo mouse
|
|
459
|
+
* chiude qui.
|
|
460
|
+
*/
|
|
461
|
+
useInput((raw, key) => {
|
|
462
|
+
const { text, events } = takeMouse(raw);
|
|
463
|
+
for (const ev of events)
|
|
464
|
+
onMouse(ev);
|
|
465
|
+
if (events.length > 0 && !text)
|
|
466
|
+
return;
|
|
467
|
+
onKey(text, key);
|
|
468
|
+
});
|
|
469
|
+
}
|