@lamemind/loom-deck 0.70.0 → 0.71.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 +55 -0
- package/dist/cli.js +39 -13
- package/dist/deck-model.js +113 -31
- package/dist/doc-tree.js +468 -0
- package/dist/doc-views.js +117 -0
- package/dist/frame.js +51 -23
- package/dist/hooks.js +45 -0
- package/dist/inbox.js +10 -1
- package/dist/input-modes.js +38 -1
- package/dist/input.js +90 -21
- package/dist/model.js +11 -0
- package/dist/mouse.js +18 -8
- package/dist/overlays/file-sheet.js +101 -0
- package/dist/pane-header.js +42 -0
- package/dist/sane-note.js +4 -4
- package/dist/spawn-catalog.js +40 -4
- package/dist/ui/doc-pane.js +85 -0
- package/dist/ui/file-sheet-screen.js +58 -0
- package/dist/ui/header-line.js +19 -0
- package/dist/ui/inbox-pane.js +83 -0
- package/dist/ui/panes.js +13 -74
- package/dist/ui/preview.js +25 -1
- package/dist/ui/screens.js +16 -4
- package/dist/viewport.js +41 -18
- package/package.json +1 -1
- package/scripts/deck-run +6 -5
package/dist/frame.js
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
// leggeva dichiarata seicento righe più in basso. Il costo è calcolare il
|
|
16
16
|
// budget anche quando il render esce presto su una schermata sostitutiva:
|
|
17
17
|
// aritmetica su array già in memoria, e `layoutBudget` accetta ogni `Mode`.
|
|
18
|
-
import { headerItems, inboxHeaderParts, sessionHeaderParts, taskHeaderParts } from './pane-header.js';
|
|
18
|
+
import { docHeaderParts, headerItems, inboxHeaderParts, sessionHeaderParts, taskHeaderParts, } from './pane-header.js';
|
|
19
19
|
import { FRAME_TEXT_COL, HINT_ROW, inlineRegions, LAUNCH_ROW, PANE_TEXT_PAD, paneSpans, rowRegions, } from './mouse.js';
|
|
20
20
|
import { cellWidth, launchLegend, LAUNCH_SEP } from './config.js';
|
|
21
21
|
import { layoutBudget, windowRange } from './viewport.js';
|
|
@@ -23,6 +23,7 @@ import { sanitize, termWidth } from './width.js';
|
|
|
23
23
|
import { MODEL_SHORT_LIST, WARN } from './glyphs.js';
|
|
24
24
|
import { STATUS_MISSING } from './project-status.js';
|
|
25
25
|
import { META_ROWS } from './model.js';
|
|
26
|
+
import { TASK_MODE_LEGEND } from './input-modes.js';
|
|
26
27
|
import { rowIndexOf } from './session-list.js';
|
|
27
28
|
import { VERSION } from './version.js';
|
|
28
29
|
// La surface built-in del cappello, in testa alla riga launch. Non sta fra i
|
|
@@ -79,6 +80,7 @@ export function deckLegend(state) {
|
|
|
79
80
|
const canSpawn = state.focus === 'tasks' && state.hasTask;
|
|
80
81
|
const canResume = state.focus === 'sessions' && state.hasSession;
|
|
81
82
|
const canOpenInbox = state.focus === 'inbox' && state.hasInbox;
|
|
83
|
+
const canOpenDoc = state.focus === 'doctree' && state.hasDoc;
|
|
82
84
|
// T50 — il pin agisce su qualunque riga selezionata (anche stale, per
|
|
83
85
|
// spinnarla); basta il focus sul pane e una selezione.
|
|
84
86
|
const canPin = state.focus === 'sessions' && state.hasSessionId;
|
|
@@ -87,7 +89,7 @@ export function deckLegend(state) {
|
|
|
87
89
|
? ['⏎ detail', '^K/^P/^R spawn']
|
|
88
90
|
: canResume
|
|
89
91
|
? ['⏎ resume']
|
|
90
|
-
: canOpenInbox
|
|
92
|
+
: canOpenInbox || canOpenDoc
|
|
91
93
|
? ['⏎ apri']
|
|
92
94
|
: []),
|
|
93
95
|
// T112 — la voce nomina il BERSAGLIO, che cambia di taglia senza che
|
|
@@ -112,21 +114,27 @@ export function deckLegend(state) {
|
|
|
112
114
|
// T155 — la voce nomina il GESTO, come le due sopra: «gitlink» da solo
|
|
113
115
|
// direbbe l'oggetto e non che il tasto lo allinea con un commit.
|
|
114
116
|
...(state.gitlink ? ['^U bumpa gitlink'] : []),
|
|
115
|
-
//
|
|
116
|
-
// `^B` scambia
|
|
117
|
-
//
|
|
118
|
-
//
|
|
119
|
-
state.
|
|
117
|
+
// T160 — la voce nomina il MODO che il tasto monta, non quello montato:
|
|
118
|
+
// `^B` li scambia, e annunciare quello che si sta già guardando direbbe il
|
|
119
|
+
// contrario di ciò che il tasto fa. Stessa regola di `CANC elimina tutte`,
|
|
120
|
+
// che nomina il bersaglio e non il tasto.
|
|
121
|
+
state.docMode ? '^B task' : '^B doc',
|
|
120
122
|
// T161 — la voce nomina l'OGGETTO e non il gesto, al contrario di `^G`/
|
|
121
123
|
// `^O`/`^U`: lì il tasto fa qualcosa (genera, bumpa) e il nome dell'oggetto
|
|
122
124
|
// da solo sarebbe ambiguo, qui apre una pagina e basta.
|
|
123
125
|
'^S azioni',
|
|
124
126
|
'^F cerca',
|
|
125
|
-
'
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
127
|
+
// T160 — le voci dei tasti del mondo task vengono dall'ELENCO che governa
|
|
128
|
+
// anche il dispatch (`input-modes.ts` §TASK_MODE_KEYS), non da cinque
|
|
129
|
+
// stringhe scritte qui: due elenchi divergono al primo tasto aggiunto, e
|
|
130
|
+
// il tasto nuovo resterebbe annunciato mentre è inerte — cioè una legenda
|
|
131
|
+
// che descrive un deck che non esiste.
|
|
132
|
+
//
|
|
133
|
+
// In modo doc spariscono tutte. Le altre voci del mondo task (`⏎ detail`,
|
|
134
|
+
// `^K/^P/^R spawn`, `CANC elimina`) non sono qui perché sono già
|
|
135
|
+
// condizionate dal focus, che in modo doc non è mai `tasks`: spariscono da
|
|
136
|
+
// sé, senza una seconda guardia.
|
|
137
|
+
...(state.docMode ? [] : TASK_MODE_LEGEND),
|
|
130
138
|
].join(' · '));
|
|
131
139
|
}
|
|
132
140
|
/**
|
|
@@ -191,17 +199,28 @@ export function launchRow(launch, columns) {
|
|
|
191
199
|
*/
|
|
192
200
|
export const INDICATOR_SEP = ' ';
|
|
193
201
|
/**
|
|
194
|
-
* Il bottone che monta il
|
|
202
|
+
* Il bottone che monta il modo doc: `[ Doc 📄 3/1/2 🚨2 ]`.
|
|
195
203
|
*
|
|
196
|
-
*
|
|
197
|
-
*
|
|
198
|
-
*
|
|
204
|
+
* T160/P4 — nomina il MODO, non più il solo pane inbox di T134, perché è quello
|
|
205
|
+
* che `^B` monta adesso. I contatori restano quelli dell'inbox e non ne
|
|
206
|
+
* guadagnano di doc: un contatore dei flag doc obbligherebbe a tenere lo scan da
|
|
207
|
+
* quattro secondi acceso anche in modo task, cioè a pagarlo quando nessuno
|
|
208
|
+
* guarda l'albero — l'opposto dell'interruttore di `useDocScan`.
|
|
199
209
|
*
|
|
200
|
-
*
|
|
201
|
-
*
|
|
202
|
-
*
|
|
203
|
-
*
|
|
204
|
-
*
|
|
210
|
+
* SEMPRE ACCESO, mai grigio (D7 di T134): è un modo, non un sottoinsieme che può
|
|
211
|
+
* essere vuoto. I tre numeri sono le tre nature nell'ordine del catalogo; la
|
|
212
|
+
* sirena e il suo contatore compaiono solo sopra la soglia.
|
|
213
|
+
*
|
|
214
|
+
* Il nome non si scambia col modo montato, a differenza della voce di legenda
|
|
215
|
+
* qui sopra: il bottone porta i contatori della coda inbox, che vive in modo
|
|
216
|
+
* doc, quindi è l'etichetta di quel mondo — e un `[ Task 📄 3/1/2 ]` leggerebbe
|
|
217
|
+
* quei tre numeri come se fossero delle task.
|
|
218
|
+
*
|
|
219
|
+
* Prima del primo scan i numeri non esistono ancora, e stampare `0/0/0` direbbe
|
|
220
|
+
* «ho misurato e non c'è niente» — cioè la cosa sbagliata (D2 preflight:
|
|
221
|
+
* `missing` e guasto sono stati distinti). Il glifo di allerta si AGGIUNGE ai
|
|
222
|
+
* numeri invece di sostituirli: un tentativo fallito lascia in piedi l'esito
|
|
223
|
+
* dell'ultimo riuscito, che resta vero e ancora apribile.
|
|
205
224
|
*/
|
|
206
225
|
export function inboxButton(state) {
|
|
207
226
|
const numbers = state.scanned
|
|
@@ -209,7 +228,7 @@ export function inboxButton(state) {
|
|
|
209
228
|
: STATUS_MISSING;
|
|
210
229
|
const siren = state.stale > 0 ? ` 🚨${state.stale}` : '';
|
|
211
230
|
const warn = state.ok ? '' : ` ${WARN}`;
|
|
212
|
-
return sanitize(`[
|
|
231
|
+
return sanitize(`[ Doc 📄 ${numbers}${siren}${warn} ]`);
|
|
213
232
|
}
|
|
214
233
|
/**
|
|
215
234
|
* L'indicatore hard-wrap: `wrap 77`.
|
|
@@ -333,6 +352,9 @@ export function frameGeometry(input) {
|
|
|
333
352
|
const selInboxIndex = input.inboxFiles.findIndex((f) => f.path === input.selInboxPath);
|
|
334
353
|
const inboxWin = windowRange(input.inboxFiles.length, selInboxIndex, budget.sessionRows);
|
|
335
354
|
const windowInbox = input.inboxFiles.slice(inboxWin.start, inboxWin.end);
|
|
355
|
+
const selDocIndex = input.docRows.findIndex((r) => r.path === input.selDocPath);
|
|
356
|
+
const docWin = windowRange(input.docRows.length, selDocIndex, budget.sessionRows);
|
|
357
|
+
const windowDoc = input.docRows.slice(docWin.start, docWin.end);
|
|
336
358
|
// T21 — geometria delle liste per l'hit-test del click, dalla STESSA
|
|
337
359
|
// aritmetica che disegna i pane: le parti degli header escono dal modulo che
|
|
338
360
|
// `ui/panes.tsx` consuma per renderle, le righe cliccabili sono le finestre
|
|
@@ -345,12 +367,16 @@ export function frameGeometry(input) {
|
|
|
345
367
|
taskHeader: inlineRegions(headerItems(taskHeaderParts(input.taskCounts, input.taskViewId, taskWin.start, input.paneTasks.length - taskWin.end, input.columns)), spans.tasks.start + PANE_TEXT_PAD),
|
|
346
368
|
sessionHeader: inlineRegions(headerItems(sessionHeaderParts(input.parentLabel, input.sessionCounts, input.sessionViewId, sessionWin.start, input.sessionRows.length - sessionWin.end, input.columns)), spans.sessions.start + PANE_TEXT_PAD),
|
|
347
369
|
inboxHeader: inlineRegions(headerItems(inboxHeaderParts(input.inboxCounts, input.inboxViewId, inboxWin.start, input.inboxFiles.length - inboxWin.end, input.columns)), spans.sessions.start + PANE_TEXT_PAD),
|
|
370
|
+
// Il pane doc occupa lo slot SINISTRO, quindi le sue regioni partono
|
|
371
|
+
// dallo span del pane task — la posizione è del lato, non del contenuto.
|
|
372
|
+
docHeader: inlineRegions(headerItems(docHeaderParts(input.docCounts, input.docViewId, docWin.start, input.docRows.length - docWin.end, input.columns, input.docHasDirs)), spans.tasks.start + PANE_TEXT_PAD),
|
|
348
373
|
// Con un errore di caricamento al posto delle task c'è la riga rossa:
|
|
349
374
|
// restano cliccabili le sole righe meta.
|
|
350
375
|
taskRows: META_ROWS + (input.hasLoadError ? 0 : windowTasks.length),
|
|
351
376
|
sessionRows: windowRows.length,
|
|
352
377
|
inboxRows: windowInbox.length,
|
|
353
|
-
|
|
378
|
+
docRows: windowDoc.length,
|
|
379
|
+
mode: input.deckMode,
|
|
354
380
|
};
|
|
355
381
|
return {
|
|
356
382
|
budget,
|
|
@@ -360,6 +386,8 @@ export function frameGeometry(input) {
|
|
|
360
386
|
windowRows,
|
|
361
387
|
inboxWin,
|
|
362
388
|
windowInbox,
|
|
389
|
+
docWin,
|
|
390
|
+
windowDoc,
|
|
363
391
|
listGeometry,
|
|
364
392
|
// Dimensione del terminale in CELLE (colonne×righe, mai pixel — un processo
|
|
365
393
|
// dentro un terminale vede solo la griglia di caratteri) e versione. La
|
package/dist/hooks.js
CHANGED
|
@@ -14,6 +14,7 @@ import { archivableIds, SCAN_INTERVAL_MS } from './archivable.js';
|
|
|
14
14
|
import { commitTimes } from './commit-times.js';
|
|
15
15
|
import { scanEpicHierarchy, EMPTY_EPIC_HIERARCHY } from './epic-hierarchy.js';
|
|
16
16
|
import { scanInbox, INBOX_SCAN_INTERVAL_MS } from './inbox.js';
|
|
17
|
+
import { scanDocTree, DOC_SCAN_INTERVAL_MS } from './doc-tree.js';
|
|
17
18
|
import { mixedCount, readWrapCache, runWrapScan, wrapCacheFile, wrapCount, } from './wrap-scan.js';
|
|
18
19
|
import { purgeTargets } from './purge.js';
|
|
19
20
|
import { loadSpawnOverrides, saveSpawnOverrides, } from './spawn-config.js';
|
|
@@ -381,6 +382,50 @@ export function useInboxScan(projectRoot, docsRoot) {
|
|
|
381
382
|
}, []);
|
|
382
383
|
return { ...state, scan };
|
|
383
384
|
}
|
|
385
|
+
export function useDocScan(projectRoot, docsRoot, enabled) {
|
|
386
|
+
const [state, setState] = useState({
|
|
387
|
+
files: [],
|
|
388
|
+
dirs: [],
|
|
389
|
+
hasDirs: false,
|
|
390
|
+
ok: true,
|
|
391
|
+
scanned: false,
|
|
392
|
+
scanning: false,
|
|
393
|
+
});
|
|
394
|
+
const [epoch, setEpoch] = useState(0);
|
|
395
|
+
const busy = useRef(false);
|
|
396
|
+
const alive = useRef(true);
|
|
397
|
+
useEffect(() => {
|
|
398
|
+
alive.current = true;
|
|
399
|
+
return () => {
|
|
400
|
+
alive.current = false;
|
|
401
|
+
};
|
|
402
|
+
}, []);
|
|
403
|
+
const run = useCallback(() => {
|
|
404
|
+
if (busy.current)
|
|
405
|
+
return;
|
|
406
|
+
busy.current = true;
|
|
407
|
+
setState((prev) => ({ ...prev, scanning: true }));
|
|
408
|
+
scanDocTree(projectRoot, docsRoot).then((res) => {
|
|
409
|
+
busy.current = false;
|
|
410
|
+
if (!alive.current)
|
|
411
|
+
return;
|
|
412
|
+
setState((prev) => res.ok
|
|
413
|
+
? { files: res.files, dirs: res.dirs, hasDirs: res.hasDirs, ok: true, scanned: true, scanning: false }
|
|
414
|
+
: { ...prev, ok: false, scanned: true, scanning: false });
|
|
415
|
+
});
|
|
416
|
+
}, [projectRoot, docsRoot]);
|
|
417
|
+
useEffect(() => {
|
|
418
|
+
if (!enabled)
|
|
419
|
+
return;
|
|
420
|
+
run();
|
|
421
|
+
const id = setInterval(run, DOC_SCAN_INTERVAL_MS);
|
|
422
|
+
return () => clearInterval(id);
|
|
423
|
+
}, [run, epoch, enabled]);
|
|
424
|
+
const scan = useCallback(() => {
|
|
425
|
+
setEpoch((e) => e + 1);
|
|
426
|
+
}, []);
|
|
427
|
+
return { ...state, scan };
|
|
428
|
+
}
|
|
384
429
|
export function useWrapScan(projectRoot) {
|
|
385
430
|
const path = useMemo(() => wrapCacheFile(projectRoot), [projectRoot]);
|
|
386
431
|
const [cache, setCache] = useState(() => readWrapCache(path));
|
package/dist/inbox.js
CHANGED
|
@@ -14,6 +14,7 @@ import { readFileSync } from 'node:fs';
|
|
|
14
14
|
import { promisify } from 'node:util';
|
|
15
15
|
import { basename, join } from 'node:path';
|
|
16
16
|
import { pluginScript } from './plugin-cache.js';
|
|
17
|
+
import { sanitize } from './width.js';
|
|
17
18
|
const execFileAsync = promisify(execFile);
|
|
18
19
|
export const INBOX_METRICS_SCRIPT = 'scripts/docs/doc-metrics.sh';
|
|
19
20
|
/**
|
|
@@ -216,10 +217,18 @@ export function inboxWords(f) {
|
|
|
216
217
|
* il detail lo dice e tiene l'azione attiva, come fa quello della task con un
|
|
217
218
|
* task file mancante: la skill risolve il file per nome, non per il testo che
|
|
218
219
|
* il deck è riuscito a leggere.
|
|
220
|
+
*
|
|
221
|
+
* `sanitize` AL CONFINE DI CARICAMENTO, come `loadTaskFileText` e il gemello
|
|
222
|
+
* `loadDocText`: la catena dichiarata in `markdown.ts` è `sanitize →
|
|
223
|
+
* parseMarkdown → wrapWithOffsets`, e la prima non conserva la lunghezza —
|
|
224
|
+
* applicarla più a valle sposterebbe gli offset degli span appena calcolati. Un
|
|
225
|
+
* file di nozioni è testo libero e porta glifi su cui Ink e il terminale
|
|
226
|
+
* divergono: senza la sanificazione il wrap conta una colonna in meno di quante
|
|
227
|
+
* Ink ne disegna, e la riga esce dal box mangiandone il bordo.
|
|
219
228
|
*/
|
|
220
229
|
export function loadInboxText(projectRoot, relPath) {
|
|
221
230
|
try {
|
|
222
|
-
return readFileSync(join(projectRoot, relPath), 'utf8');
|
|
231
|
+
return sanitize(readFileSync(join(projectRoot, relPath), 'utf8'));
|
|
223
232
|
}
|
|
224
233
|
catch {
|
|
225
234
|
return null;
|
package/dist/input-modes.js
CHANGED
|
@@ -25,6 +25,7 @@ export const CAPTURING_MODES = [
|
|
|
25
25
|
'detail',
|
|
26
26
|
'status',
|
|
27
27
|
'inbox',
|
|
28
|
+
'doc',
|
|
28
29
|
'wrap',
|
|
29
30
|
'spawn',
|
|
30
31
|
'reader',
|
|
@@ -60,6 +61,42 @@ export function captures(mode) {
|
|
|
60
61
|
export const CTRL_DEROGATIONS = {
|
|
61
62
|
detail: ['f'],
|
|
62
63
|
};
|
|
64
|
+
export const TASK_MODE_KEYS = [
|
|
65
|
+
{ chord: { kind: 'ctrl', input: 'k' }, label: '^K', legend: null },
|
|
66
|
+
{ chord: { kind: 'ctrl', input: 'p' }, label: '^P', legend: null },
|
|
67
|
+
{ chord: { kind: 'ctrl', input: 'r' }, label: '^R', legend: null },
|
|
68
|
+
{ chord: { kind: 'delete' }, label: 'CANC', legend: null },
|
|
69
|
+
{ chord: { kind: 'plain', input: 'C' }, label: 'C', legend: 'C nuova' },
|
|
70
|
+
{ chord: { kind: 'plain', input: 'E' }, label: 'E', legend: 'E edit' },
|
|
71
|
+
{ chord: { kind: 'plain', input: 'S' }, label: 'S', legend: 'S sort' },
|
|
72
|
+
{ chord: { kind: 'plain', input: 'F' }, label: 'F', legend: 'F filtri' },
|
|
73
|
+
{ chord: { kind: 'plain', input: 'w' }, label: 'w', legend: 'w salva' },
|
|
74
|
+
];
|
|
75
|
+
/** Le voci di legenda dei tasti del mondo task, nell'ordine dell'elenco: è ciò
|
|
76
|
+
* che `deckLegend` emette in modo task e omette in modo doc. */
|
|
77
|
+
export const TASK_MODE_LEGEND = TASK_MODE_KEYS.map((k) => k.legend).filter((v) => v !== null);
|
|
78
|
+
/**
|
|
79
|
+
* Il nome del tasto del mondo task appena premuto, o `null` se non è uno di
|
|
80
|
+
* loro.
|
|
81
|
+
*
|
|
82
|
+
* I tre booleani arrivano dal chiamante invece del `Key` di Ink: questo modulo
|
|
83
|
+
* è un catalogo di dati e resta provabile senza montare nulla — la stessa
|
|
84
|
+
* ragione per cui `CAPTURING_MODES` è un array e non una proprietà di un hook.
|
|
85
|
+
*/
|
|
86
|
+
export function taskModeKey(input, ctrl, del) {
|
|
87
|
+
for (const k of TASK_MODE_KEYS) {
|
|
88
|
+
const c = k.chord;
|
|
89
|
+
if (c.kind === 'delete' ? del : c.kind === 'ctrl' ? ctrl && c.input === input : !ctrl && !del && c.input === input) {
|
|
90
|
+
return k.label;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
/** La riga di stato di un tasto inerte: nomina il tasto, il modo in cui vive e
|
|
96
|
+
* come tornarci. */
|
|
97
|
+
export function inertNote(label) {
|
|
98
|
+
return `${label} → solo in modo task (^B)`;
|
|
99
|
+
}
|
|
63
100
|
/**
|
|
64
101
|
* T21 (mandata 2) — i modi che SCORRONO un contenuto lungo, cioè gli unici in
|
|
65
102
|
* cui la rotella del mouse fa qualcosa.
|
|
@@ -74,7 +111,7 @@ export const CTRL_DEROGATIONS = {
|
|
|
74
111
|
* Il custode è `MODE_WHEEL` in `input.ts`, un `Record<ScrollingMode, …>` che non
|
|
75
112
|
* compila se un modo entra qui senza uno scroll da chiamare.
|
|
76
113
|
*/
|
|
77
|
-
export const SCROLLING_MODES = ['detail', 'status', 'inbox', 'wrap', 'reader'];
|
|
114
|
+
export const SCROLLING_MODES = ['detail', 'status', 'inbox', 'doc', 'wrap', 'reader'];
|
|
78
115
|
const SCROLLING = new Set(SCROLLING_MODES);
|
|
79
116
|
/** `true` se la rotella scorre il contenuto del modo. */
|
|
80
117
|
export function scrolls(mode) {
|
package/dist/input.js
CHANGED
|
@@ -22,11 +22,12 @@ import { useApp, useInput } from 'ink';
|
|
|
22
22
|
import { hitRegion, isWheel, listHit, takeMouse, wheelDir, WHEEL_LINES, } from './mouse.js';
|
|
23
23
|
import { HINT_ROW, LAUNCH_ROW } from './frame.js';
|
|
24
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';
|
|
25
|
+
import { captures, inertNote, scrolls, taskModeKey, } from './input-modes.js';
|
|
26
|
+
import { LEFT_PANE, META_ROWS, QUIT_WINDOW_MS, RIGHT_PANE } from './model.js';
|
|
27
27
|
import { TASK_VIEWS, taskView } from './pane-views.js';
|
|
28
28
|
import { loadTaskFileText } from './tasks.js';
|
|
29
|
-
import { loadInboxText } from './inbox.js';
|
|
29
|
+
import { inboxPrompt, loadInboxText } from './inbox.js';
|
|
30
|
+
import { dirListing, loadDocText } from './doc-tree.js';
|
|
30
31
|
// T116 — l'avviso della prima pressione di `^C`. La durata è INTERPOLATA dalla
|
|
31
32
|
// finestra, non ricopiata: un numero scritto a mano in un testo che promette un
|
|
32
33
|
// comportamento diventa falso il giorno che la costante cambia, e nessuno
|
|
@@ -78,6 +79,7 @@ export function useDeckInput({ cwd, tasksDir, mode, setMode, setNote, model, act
|
|
|
78
79
|
detail: overlays.sheet.onKey,
|
|
79
80
|
status: overlays.status.onKey,
|
|
80
81
|
inbox: overlays.inbox.onKey,
|
|
82
|
+
doc: overlays.doc.onKey,
|
|
81
83
|
wrap: overlays.wrap.onKey,
|
|
82
84
|
spawn: overlays.spawn.onKey,
|
|
83
85
|
reader: overlays.search.onReaderKey,
|
|
@@ -98,6 +100,7 @@ export function useDeckInput({ cwd, tasksDir, mode, setMode, setNote, model, act
|
|
|
98
100
|
detail: overlays.sheet.scroll,
|
|
99
101
|
status: overlays.status.scroll,
|
|
100
102
|
inbox: overlays.inbox.scroll,
|
|
103
|
+
doc: overlays.doc.scroll,
|
|
101
104
|
wrap: overlays.wrap.scroll,
|
|
102
105
|
reader: overlays.search.scrollReader,
|
|
103
106
|
};
|
|
@@ -212,10 +215,24 @@ export function useDeckInput({ cwd, tasksDir, mode, setMode, setNote, model, act
|
|
|
212
215
|
model.selectTaskView(hit.key);
|
|
213
216
|
else if (hit.pane === 'inbox')
|
|
214
217
|
model.selectInboxView(hit.key);
|
|
218
|
+
else if (hit.pane === 'doctree')
|
|
219
|
+
model.selectDocView(hit.key);
|
|
215
220
|
else
|
|
216
221
|
model.selectSessionView(hit.key);
|
|
217
222
|
return;
|
|
218
223
|
}
|
|
224
|
+
if (hit.pane === 'doctree') {
|
|
225
|
+
const r = frame.windowDoc[hit.index];
|
|
226
|
+
if (!r)
|
|
227
|
+
return;
|
|
228
|
+
if (model.focus === 'doctree' && r.path === model.selDocPath) {
|
|
229
|
+
onKey('', { ...NO_MODIFIERS, return: true });
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
model.setFocus('doctree');
|
|
233
|
+
model.selectDocRow(frame.docWin.start + hit.index);
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
219
236
|
if (hit.pane === 'inbox') {
|
|
220
237
|
const f = frame.windowInbox[hit.index];
|
|
221
238
|
if (!f)
|
|
@@ -270,6 +287,23 @@ export function useDeckInput({ cwd, tasksDir, mode, setMode, setNote, model, act
|
|
|
270
287
|
MODE_KEYS[mode](input, key);
|
|
271
288
|
return;
|
|
272
289
|
}
|
|
290
|
+
// T160 — i tasti del mondo task sono INERTI in modo doc, e lo dicono
|
|
291
|
+
// (`input-modes.ts` §TASK_MODE_KEYS). Il gate sta QUI, sopra tutti i rami e
|
|
292
|
+
// sotto il dispatch dei modi: un tasto gated non deve raggiungere né il ramo
|
|
293
|
+
// `ctrl` né quello delle lettere nude, e dentro un modo capturing la
|
|
294
|
+
// domanda non si pone perché il modo ha già consumato l'input.
|
|
295
|
+
//
|
|
296
|
+
// Un ramo per tasto con la sua guardia avrebbe funzionato uguale e sarebbe
|
|
297
|
+
// stato il modo per farne dimenticare uno: il gate legge l'elenco, e un
|
|
298
|
+
// tasto aggiunto lì è gated e smette di essere annunciato senza toccare
|
|
299
|
+
// questo file.
|
|
300
|
+
if (model.deckMode === 'doc') {
|
|
301
|
+
const gated = taskModeKey(input, key.ctrl, key.delete);
|
|
302
|
+
if (gated) {
|
|
303
|
+
setNote(inertNote(gated));
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
273
307
|
// T52/D1 — il ramo CTRL sta PRIMA di quelli su lettera nuda e li chiude
|
|
274
308
|
// tutti. `CTRL+F` e `f` nudo arrivano con lo STESSO `input` ('f'),
|
|
275
309
|
// distinguibili solo da `key.ctrl`: senza questa precedenza `CTRL+F`
|
|
@@ -352,11 +386,13 @@ export function useDeckInput({ cwd, tasksDir, mode, setMode, setNote, model, act
|
|
|
352
386
|
overlays.spawn.openPage();
|
|
353
387
|
}
|
|
354
388
|
else if (input === 'b') {
|
|
355
|
-
//
|
|
356
|
-
//
|
|
357
|
-
//
|
|
358
|
-
//
|
|
359
|
-
|
|
389
|
+
// T160 — `^B` scambia il MODO del deck, cioè entrambi gli slot insieme.
|
|
390
|
+
// Erede del tasto di T134, che scambiava il solo pane destro: la
|
|
391
|
+
// mnemonica (box/bacheca) resta, il bersaglio è salito di un livello.
|
|
392
|
+
// Non è un modale né una schermata — i due riquadri della vista normale
|
|
393
|
+
// restano al loro posto, quindi `tab` continua a ciclare le viste DENTRO
|
|
394
|
+
// il pane a fuoco e `←→` a spostare il fuoco fra le colonne.
|
|
395
|
+
model.toggleDeckMode();
|
|
360
396
|
}
|
|
361
397
|
return;
|
|
362
398
|
}
|
|
@@ -372,25 +408,28 @@ export function useDeckInput({ cwd, tasksDir, mode, setMode, setNote, model, act
|
|
|
372
408
|
model.cycleView(key.shift ? -1 : 1);
|
|
373
409
|
}
|
|
374
410
|
else if (key.escape) {
|
|
375
|
-
//
|
|
376
|
-
//
|
|
377
|
-
//
|
|
378
|
-
model.
|
|
411
|
+
// T160 — `esc` in vista normale torna al modo task. Resta inerte quando il
|
|
412
|
+
// modo task è già montato: `esc` dice «esci da dove sei», e da lì non c'è
|
|
413
|
+
// niente da cui uscire.
|
|
414
|
+
model.closeDocMode();
|
|
379
415
|
}
|
|
380
416
|
else if (key.leftArrow || key.rightArrow) {
|
|
381
|
-
// Binding ASSOLUTO, non toggle: `←` porta sempre
|
|
382
|
-
// sul
|
|
383
|
-
// È ciò che lo rende spaziale — la direzione indica una
|
|
384
|
-
// con due colonne un toggle sarebbe indistinguibile solo
|
|
385
|
-
//
|
|
386
|
-
//
|
|
387
|
-
|
|
417
|
+
// Binding ASSOLUTO, non toggle: `←` porta sempre sul pane SINISTRO e `→`
|
|
418
|
+
// sempre sul destro, quindi ripremere lo stesso tasto non riporta
|
|
419
|
+
// indietro. È ciò che lo rende spaziale — la direzione indica una
|
|
420
|
+
// destinazione, e con due colonne un toggle sarebbe indistinguibile solo
|
|
421
|
+
// per caso.
|
|
422
|
+
// T160 — «sinistro» e «destro» sono posizioni, non contenuti: quale pane
|
|
423
|
+
// le occupi lo decide il modo, che è un'altra decisione (`^B`).
|
|
424
|
+
model.setFocus((key.leftArrow ? LEFT_PANE : RIGHT_PANE)[model.deckMode]);
|
|
388
425
|
}
|
|
389
426
|
else if (key.upArrow) {
|
|
390
427
|
if (model.focus === 'tasks')
|
|
391
428
|
model.moveTaskSel(-1);
|
|
392
429
|
else if (model.focus === 'inbox')
|
|
393
430
|
model.moveInboxSel(-1);
|
|
431
|
+
else if (model.focus === 'doctree')
|
|
432
|
+
model.moveDocSel(-1);
|
|
394
433
|
else
|
|
395
434
|
model.setSelSessionId((id) => moveSelection(model.sessionRows, id, -1));
|
|
396
435
|
}
|
|
@@ -399,11 +438,33 @@ export function useDeckInput({ cwd, tasksDir, mode, setMode, setNote, model, act
|
|
|
399
438
|
model.moveTaskSel(1);
|
|
400
439
|
else if (model.focus === 'inbox')
|
|
401
440
|
model.moveInboxSel(1);
|
|
441
|
+
else if (model.focus === 'doctree')
|
|
442
|
+
model.moveDocSel(1);
|
|
402
443
|
else
|
|
403
444
|
model.setSelSessionId((id) => moveSelection(model.sessionRows, id, 1));
|
|
404
445
|
}
|
|
405
446
|
else if (key.return) {
|
|
406
|
-
if (model.focus === '
|
|
447
|
+
if (model.focus === 'doctree') {
|
|
448
|
+
// T160 — `⏎` apre il DETAIL del bersaglio, non la sessione: la sessione
|
|
449
|
+
// la apre il `⏎` di dentro, come sul pane inbox. Il gesto è lo stesso su
|
|
450
|
+
// un file e su una cartella (P10) — cambia cosa si legge nel corpo, non
|
|
451
|
+
// cosa si preme: il testo del file, oppure l'elenco che dice perché la
|
|
452
|
+
// cartella è flaggata.
|
|
453
|
+
const r = model.selDoc;
|
|
454
|
+
if (!r) {
|
|
455
|
+
setNote('nessuna riga doc selezionata');
|
|
456
|
+
}
|
|
457
|
+
else {
|
|
458
|
+
const { prompt, hint } = actions.rebalanceSpawn(r);
|
|
459
|
+
overlays.doc.open({
|
|
460
|
+
item: r,
|
|
461
|
+
text: r.kind === 'dir' ? dirListing(model.docData, r.path) : loadDocText(cwd, r.path),
|
|
462
|
+
prompt,
|
|
463
|
+
hint,
|
|
464
|
+
});
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
else if (model.focus === 'inbox') {
|
|
407
468
|
// T134 — `⏎` apre il DETAIL del file, non la sessione: la sessione la
|
|
408
469
|
// apre il `⏎` di dentro. Stessa scala del pane task, dove `⏎` apre il
|
|
409
470
|
// detail e le combo restano per chi sa già cosa vuole — con la
|
|
@@ -413,7 +474,15 @@ export function useDeckInput({ cwd, tasksDir, mode, setMode, setNote, model, act
|
|
|
413
474
|
if (!f)
|
|
414
475
|
setNote('nessun file inbox selezionato');
|
|
415
476
|
else
|
|
416
|
-
overlays.inbox.open({
|
|
477
|
+
overlays.inbox.open({
|
|
478
|
+
item: f,
|
|
479
|
+
text: loadInboxText(cwd, f.path),
|
|
480
|
+
// T134 — la skill la decide la NATURA del file, e quella mappa vive
|
|
481
|
+
// in un posto solo (`inboxPrompt`, `src/inbox.ts`): il prompt si
|
|
482
|
+
// compone qui, dove si apre lo sheet, e non dentro l'hook — che da
|
|
483
|
+
// T160 è condiviso con un bersaglio di tutt'altra natura.
|
|
484
|
+
prompt: inboxPrompt(f),
|
|
485
|
+
});
|
|
417
486
|
}
|
|
418
487
|
else if (model.focus === 'tasks') {
|
|
419
488
|
// T66 — ⏎ apre il DETAIL, non più una sessione. Secondo rimappaggio in
|
package/dist/model.js
CHANGED
|
@@ -31,6 +31,17 @@ export const ALL = Symbol('all');
|
|
|
31
31
|
export const ROW_ALL = 0;
|
|
32
32
|
export const ROW_SPOT = 1;
|
|
33
33
|
export const META_ROWS = 2;
|
|
34
|
+
/** Il pane SINISTRO di ciascun modo, e il destro. Tabelle e non due ternari
|
|
35
|
+
* sparsi: `←→`, il toggle del modo e l'hit-test del mouse fanno tutti e tre la
|
|
36
|
+
* stessa domanda, e tre risposte scritte a mano divergono al primo modo
|
|
37
|
+
* aggiunto. */
|
|
38
|
+
export const LEFT_PANE = { task: 'tasks', doc: 'doctree' };
|
|
39
|
+
export const RIGHT_PANE = { task: 'sessions', doc: 'inbox' };
|
|
40
|
+
/** Il fuoco è sul pane di sinistra? Serve al toggle del modo, che deve portare
|
|
41
|
+
* chi guardava a sinistra sul pane sinistro dell'altro modo. */
|
|
42
|
+
export function isLeftPane(focus) {
|
|
43
|
+
return focus === LEFT_PANE.task || focus === LEFT_PANE.doc;
|
|
44
|
+
}
|
|
34
45
|
// Modale sort a grammatica libera: un tasto per chiave, pressioni successive
|
|
35
46
|
// ciclano asc → desc → fuori dalla chain.
|
|
36
47
|
export const SORT_TASTI = { p: 'pri', s: 'prog', i: 'id', c: 'commit' };
|
package/dist/mouse.js
CHANGED
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
// eventi che sotto render pesante si frammentano su stdin e leakano dentro
|
|
20
20
|
// i campi di testo. Con soli 1000+1006 gli eventi sono due per click.
|
|
21
21
|
import { termWidth } from './width.js';
|
|
22
|
+
import { LEFT_PANE, RIGHT_PANE } from './model.js';
|
|
22
23
|
/**
|
|
23
24
|
* Sequenza SGR: `ESC [ < b ; x ; y M|m`, con l'ESC opzionale (fatto ①).
|
|
24
25
|
*
|
|
@@ -277,19 +278,28 @@ export function inlineRegions(parts, startCol) {
|
|
|
277
278
|
* riga sort, spazio vuoto sotto la lista o fuori dai pane. */
|
|
278
279
|
export function listHit(ev, g) {
|
|
279
280
|
const spans = paneSpans(g.columns);
|
|
280
|
-
const
|
|
281
|
+
const inLeft = ev.col >= spans.tasks.start && ev.col <= spans.tasks.end;
|
|
281
282
|
const inRight = ev.col >= spans.sessions.start && ev.col <= spans.sessions.end;
|
|
282
|
-
if (!
|
|
283
|
+
if (!inLeft && !inRight)
|
|
283
284
|
return null;
|
|
284
|
-
const
|
|
285
|
-
const pane =
|
|
286
|
-
const
|
|
285
|
+
const isDoc = g.mode === 'doc';
|
|
286
|
+
const pane = inLeft ? LEFT_PANE[g.mode] : RIGHT_PANE[g.mode];
|
|
287
|
+
const leftHeader = isDoc ? g.docHeader : g.taskHeader;
|
|
288
|
+
const rightHeader = isDoc ? g.inboxHeader : g.sessionHeader;
|
|
287
289
|
if (ev.row === PANE_HEADER_ROW) {
|
|
288
|
-
const key = hitRegion(
|
|
290
|
+
const key = hitRegion(inLeft ? leftHeader : rightHeader, ev.col);
|
|
289
291
|
return key ? { pane, target: 'view', key } : null;
|
|
290
292
|
}
|
|
291
|
-
|
|
292
|
-
|
|
293
|
+
// Fra header e lista il pane task porta la riga sort/filtri, che non si
|
|
294
|
+
// seleziona; il pane doc non ce l'ha e la sua lista comincia una riga prima.
|
|
295
|
+
const first = inLeft && !isDoc ? TASK_LIST_ROW : PANE_BODY_ROW;
|
|
296
|
+
const count = inLeft
|
|
297
|
+
? isDoc
|
|
298
|
+
? g.docRows
|
|
299
|
+
: g.taskRows
|
|
300
|
+
: isDoc
|
|
301
|
+
? g.inboxRows
|
|
302
|
+
: g.sessionRows;
|
|
293
303
|
const index = ev.row - first;
|
|
294
304
|
if (index < 0 || index >= count)
|
|
295
305
|
return null;
|