@lamemind/loom-deck 0.50.0 → 0.52.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/cli.js +120 -30
- package/dist/input-modes.js +20 -0
- package/dist/mouse.js +106 -0
- package/dist/overlays/search.js +1 -0
- package/dist/overlays/sheet.js +1 -0
- package/dist/overlays/status.js +1 -0
- package/dist/pane-header.js +62 -0
- package/dist/ui/panes.js +15 -39
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -8,7 +8,8 @@ import { rowIndexOfKey, selectedRow, } from './search.js';
|
|
|
8
8
|
import { appendNote, appendPin, appendSessionRecord, appendTaskBinding, } from './task-index.js';
|
|
9
9
|
import { assembleSessionList, firstSelectableId, moveSelection, neighborId, rowIndexOf, selectedSession, unpinLandingId, } from './session-list.js';
|
|
10
10
|
import { cellWidth, launchLegend, LAUNCH_SEP, loadArchivableDays, loadIdentity, loadLaunch, } from './config.js';
|
|
11
|
-
import { anchorFrame, enableMouse, FRAME_TEXT_COL, hitRegion, isWheel, LAUNCH_ROW, rowRegions, takeMouse, } from './mouse.js';
|
|
11
|
+
import { anchorFrame, enableMouse, FRAME_TEXT_COL, hitRegion, inlineRegions, isWheel, LAUNCH_ROW, listHit, PANE_TEXT_PAD, paneSpans, rowRegions, takeMouse, WHEEL_LINES, wheelDir, } from './mouse.js';
|
|
12
|
+
import { headerItems, sessionHeaderParts, taskHeaderParts } from './pane-header.js';
|
|
12
13
|
import { cycleSessionView, cycleTaskView, selectSessionRows, selectTasks, sessionView, taskView, TASK_VIEWS, } from './pane-views.js';
|
|
13
14
|
import { isCompact, layoutBudget, searchPreviewCapacity, windowRange, } from './viewport.js';
|
|
14
15
|
import { cut, cutMiddle, sanitize, termWidth } from './width.js';
|
|
@@ -33,7 +34,7 @@ import { useSearchOverlay } from './overlays/search.js';
|
|
|
33
34
|
import { useSheetOverlay } from './overlays/sheet.js';
|
|
34
35
|
import { useAssignOverlay } from './overlays/assign.js';
|
|
35
36
|
import { useProjectStatus } from './overlays/status.js';
|
|
36
|
-
import { captures } from './input-modes.js';
|
|
37
|
+
import { captures, scrolls } from './input-modes.js';
|
|
37
38
|
import { VERSION } from './version.js';
|
|
38
39
|
// T116 — l'avviso della prima pressione di `^C`. La durata è INTERPOLATA dalla
|
|
39
40
|
// finestra, non ricopiata: un numero scritto a mano in un testo che promette un
|
|
@@ -713,13 +714,17 @@ function Deck({ cwd, tasksPath, tasksDir }) {
|
|
|
713
714
|
// 2..N+1 = task visibili) e la riconverte subito in sentinella o id: l'indice
|
|
714
715
|
// non sopravvive a un cambio di filtro, l'id sì.
|
|
715
716
|
function moveTaskSel(delta) {
|
|
716
|
-
|
|
717
|
-
|
|
717
|
+
selectTaskRow(Math.max(0, Math.min(paneTasks.length + META_ROWS - 1, selIndex + delta)));
|
|
718
|
+
}
|
|
719
|
+
/** Selezione per INDICE nella vista, riconvertita subito in sentinella o id.
|
|
720
|
+
* T21 — la chiama anche il click su una riga del pane task. */
|
|
721
|
+
function selectTaskRow(index) {
|
|
722
|
+
if (index === ROW_ALL)
|
|
718
723
|
setSel(ALL);
|
|
719
|
-
else if (
|
|
724
|
+
else if (index === ROW_SPOT)
|
|
720
725
|
setSel(SPOT);
|
|
721
726
|
else
|
|
722
|
-
setSel(paneTasks[
|
|
727
|
+
setSel(paneTasks[index - META_ROWS]?.id ?? SPOT);
|
|
723
728
|
}
|
|
724
729
|
// T100 — `tab` naviga il catalogo viste del pane in focus. Il reset della
|
|
725
730
|
// selezione è la regola letterale «prima riga in alto», senza eccezioni: sul
|
|
@@ -728,18 +733,28 @@ function Deck({ cwd, tasksPath, tasksDir }) {
|
|
|
728
733
|
// sessioni basta invalidare l'id, e l'effect di validità atterra sulla prima
|
|
729
734
|
// riga selezionabile della vista nuova.
|
|
730
735
|
function cycleView(delta) {
|
|
731
|
-
if (focus === 'tasks')
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
736
|
+
if (focus === 'tasks')
|
|
737
|
+
selectTaskView(cycleTaskView(taskViewId, delta));
|
|
738
|
+
else
|
|
739
|
+
selectSessionView(cycleSessionView(sessionViewId, delta));
|
|
740
|
+
}
|
|
741
|
+
// T21 — per VISTA e non per passo: il click sull'header nomina una voce, e
|
|
742
|
+
// `tab` la raggiunge ciclando. Sulla vista già attiva non si fa niente — un
|
|
743
|
+
// click che azzerasse la selezione alla riga in alto sarebbe un effetto che
|
|
744
|
+
// la tastiera non ha mai avuto, perché `tab` non può restare ferma.
|
|
745
|
+
function selectTaskView(next) {
|
|
746
|
+
if (next === taskViewId)
|
|
747
|
+
return;
|
|
748
|
+
setTaskViewId(next);
|
|
749
|
+
setSel(ALL);
|
|
750
|
+
setNote(`vista task: ${taskView(next).label(taskCounts)}`);
|
|
751
|
+
}
|
|
752
|
+
function selectSessionView(next) {
|
|
753
|
+
if (next === sessionViewId)
|
|
754
|
+
return;
|
|
755
|
+
setSessionViewId(next);
|
|
756
|
+
setSelSessionId(null);
|
|
757
|
+
setNote(`vista sessioni: ${sessionView(next).label(sessionCounts, parentLabel)}`);
|
|
743
758
|
}
|
|
744
759
|
// T49 — resume di una conversazione in una nuova tab. Unico punto: lo chiamano
|
|
745
760
|
// il `⏎` della lista sessioni e quello sulla riga-sessione della ricerca, che
|
|
@@ -994,6 +1009,15 @@ function Deck({ cwd, tasksPath, tasksDir }) {
|
|
|
994
1009
|
edit: onEditKey,
|
|
995
1010
|
purge: onPurgeKey,
|
|
996
1011
|
};
|
|
1012
|
+
// T21 (mandata 2) — la ROTELLA, per i soli modi che scorrono un contenuto
|
|
1013
|
+
// lungo (`SCROLLING_MODES`). Stessa forma di `MODE_KEYS`: un `Record` sul
|
|
1014
|
+
// catalogo, quindi un modo dichiarato scorrevole senza uno scroll da chiamare
|
|
1015
|
+
// non compila. Il delta è in righe, col segno del verso.
|
|
1016
|
+
const MODE_WHEEL = {
|
|
1017
|
+
detail: sheet.scroll,
|
|
1018
|
+
status: status.scroll,
|
|
1019
|
+
reader: search.scrollReader,
|
|
1020
|
+
};
|
|
997
1021
|
/**
|
|
998
1022
|
* T21 — il MOUSE precede tutto, in ogni modo del deck.
|
|
999
1023
|
*
|
|
@@ -1019,22 +1043,72 @@ function Deck({ cwd, tasksPath, tasksDir }) {
|
|
|
1019
1043
|
});
|
|
1020
1044
|
function onMouse(ev) {
|
|
1021
1045
|
// Un click produce due eventi (pressione e rilascio): agire su entrambi
|
|
1022
|
-
// spawnerebbe due volte. La rotella
|
|
1023
|
-
//
|
|
1024
|
-
|
|
1025
|
-
|
|
1046
|
+
// spawnerebbe due volte. La rotella arriva come sola pressione, quindi il
|
|
1047
|
+
// filtro su `press` la lascia passare e non la dedoppia.
|
|
1048
|
+
if (!ev.press)
|
|
1049
|
+
return;
|
|
1050
|
+
if (isWheel(ev.button)) {
|
|
1051
|
+
// La rotella NON rientra dalla porta della tastiera come il click: nel
|
|
1052
|
+
// detail `↑↓` muovono il fuoco fra i campi (T117), non il testo, e il
|
|
1053
|
+
// tasto che scorre — `PgUp`/`PgDn` — ha la granularità sbagliata per una
|
|
1054
|
+
// tacca. Va quindi allo scroll del modo, in righe. Fuori dai modi
|
|
1055
|
+
// scorrevoli è inerte: nelle liste la rotella non muove mai la
|
|
1056
|
+
// selezione (D5), e una tacca mentre il deck è sulla lista non deve
|
|
1057
|
+
// fare niente.
|
|
1058
|
+
if (scrolls(mode))
|
|
1059
|
+
MODE_WHEEL[mode](wheelDir(ev.button) * WHEEL_LINES);
|
|
1060
|
+
return;
|
|
1061
|
+
}
|
|
1062
|
+
// Il bottone si legge sui due bit bassi, perché gli alti portano i
|
|
1063
|
+
// modificatori (shift/meta/ctrl).
|
|
1064
|
+
if ((ev.button & 3) !== 0)
|
|
1026
1065
|
return;
|
|
1027
1066
|
// Le superfici esistono solo nella lista: un modo capturing prende il
|
|
1028
1067
|
// frame intero e a quella riga c'è dell'altro.
|
|
1029
|
-
if (mode !== 'normal'
|
|
1068
|
+
if (mode !== 'normal')
|
|
1069
|
+
return;
|
|
1070
|
+
if (ev.row === LAUNCH_ROW) {
|
|
1071
|
+
const hit = hitRegion(launchRegions, ev.col);
|
|
1072
|
+
// Il click NON chiama l'azione: rientra dalla porta della tastiera col
|
|
1073
|
+
// tasto che la superficie annuncia. Un ramo nuovo su `t`/`c`/cifre nasce
|
|
1074
|
+
// così già cliccabile, e non esiste un secondo posto in cui il click possa
|
|
1075
|
+
// dire una cosa diversa dal tasto che gli sta scritto sopra.
|
|
1076
|
+
if (hit)
|
|
1077
|
+
onKey(hit, NO_MODIFIERS);
|
|
1078
|
+
return;
|
|
1079
|
+
}
|
|
1080
|
+
onListClick(ev);
|
|
1081
|
+
}
|
|
1082
|
+
/**
|
|
1083
|
+
* T21 (mandata 3) — click su un elemento di lista = SOLO FUOCO (D2): il pane
|
|
1084
|
+
* prende il focus e la selezione va sull'elemento, nessuna azione parte. Qui
|
|
1085
|
+
* il click non sintetizza un tasto, perché nessun tasto nomina una riga o una
|
|
1086
|
+
* vista — `↑↓` e `tab` sono relativi — e chiama i setter che la tastiera
|
|
1087
|
+
* usa a sua volta. Un click sulla vista già attiva si limita al focus.
|
|
1088
|
+
*/
|
|
1089
|
+
function onListClick(ev) {
|
|
1090
|
+
if (!listGeometry)
|
|
1030
1091
|
return;
|
|
1031
|
-
const hit =
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1092
|
+
const hit = listHit(ev, listGeometry);
|
|
1093
|
+
if (!hit)
|
|
1094
|
+
return;
|
|
1095
|
+
setFocus(hit.pane);
|
|
1096
|
+
if (hit.pane === 'tasks') {
|
|
1097
|
+
if (hit.target === 'view')
|
|
1098
|
+
selectTaskView(hit.key);
|
|
1099
|
+
// Le due righe meta hanno indice fisso; le task riportano l'indice di
|
|
1100
|
+
// finestra a quello della lista completa, su cui è keyata la selezione.
|
|
1101
|
+
else
|
|
1102
|
+
selectTaskRow(hit.index < META_ROWS ? hit.index : taskWin.start + hit.index);
|
|
1103
|
+
}
|
|
1104
|
+
else if (hit.target === 'view') {
|
|
1105
|
+
selectSessionView(hit.key);
|
|
1106
|
+
}
|
|
1107
|
+
else {
|
|
1108
|
+
const row = windowRows[hit.index];
|
|
1109
|
+
if (row && row.kind !== 'separator')
|
|
1110
|
+
setSelSessionId(row.sessionId);
|
|
1111
|
+
}
|
|
1038
1112
|
}
|
|
1039
1113
|
function onKey(input, key) {
|
|
1040
1114
|
// T116 — `^C` sta SOPRA il dispatch dei modi, unico tasto a scavalcarlo.
|
|
@@ -1520,6 +1594,22 @@ function Deck({ cwd, tasksPath, tasksDir }) {
|
|
|
1520
1594
|
const selRowIndex = rowIndexOf(sessionRows, selSessionId);
|
|
1521
1595
|
const sessionWin = windowRange(sessionRows.length, selRowIndex, budget.sessionRows);
|
|
1522
1596
|
const windowRows = sessionRows.slice(sessionWin.start, sessionWin.end);
|
|
1597
|
+
// T21 — geometria delle liste per l'hit-test del click, dalla STESSA
|
|
1598
|
+
// aritmetica che disegna i pane: le parti degli header escono dal modulo che
|
|
1599
|
+
// `ui/panes.tsx` consuma per renderle, le righe cliccabili sono le finestre
|
|
1600
|
+
// appena calcolate. `null` in compatto: lì i pane non esistono.
|
|
1601
|
+
const spans = paneSpans(columns);
|
|
1602
|
+
const listGeometry = budget.compact
|
|
1603
|
+
? null
|
|
1604
|
+
: {
|
|
1605
|
+
columns,
|
|
1606
|
+
taskHeader: inlineRegions(headerItems(taskHeaderParts(taskCounts, taskViewId, taskWin.start, paneTasks.length - taskWin.end, columns)), spans.tasks.start + PANE_TEXT_PAD),
|
|
1607
|
+
sessionHeader: inlineRegions(headerItems(sessionHeaderParts(parentLabel, sessionCounts, sessionViewId, sessionWin.start, sessionRows.length - sessionWin.end, columns)), spans.sessions.start + PANE_TEXT_PAD),
|
|
1608
|
+
// Con un errore di caricamento al posto delle task c'è la riga rossa:
|
|
1609
|
+
// restano cliccabili le sole righe meta.
|
|
1610
|
+
taskRows: META_ROWS + (loadError ? 0 : windowTasks.length),
|
|
1611
|
+
sessionRows: windowRows.length,
|
|
1612
|
+
};
|
|
1523
1613
|
// Sotto la soglia minima il layout a box non entra a nessun costo: si scende
|
|
1524
1614
|
// a una riga sola. Perdere il deck per un terminale basso è meglio che
|
|
1525
1615
|
// sporcare la cronologia del terminale a ogni poll.
|
package/dist/input-modes.js
CHANGED
|
@@ -55,3 +55,23 @@ export function captures(mode) {
|
|
|
55
55
|
export const CTRL_DEROGATIONS = {
|
|
56
56
|
detail: ['f'],
|
|
57
57
|
};
|
|
58
|
+
/**
|
|
59
|
+
* T21 (mandata 2) — i modi che SCORRONO un contenuto lungo, cioè gli unici in
|
|
60
|
+
* cui la rotella del mouse fa qualcosa.
|
|
61
|
+
*
|
|
62
|
+
* La rotella scorre il TESTO, mai la selezione di una lista (D5): nelle liste
|
|
63
|
+
* la selezione è un'intenzione — la riga su cui si preme `⏎` — e una rotella
|
|
64
|
+
* che la muovesse trasformerebbe ogni sfioramento in una scelta. Nei tre modi
|
|
65
|
+
* qui sotto lo scroll non sceglie niente, è solo posizione di lettura. Non
|
|
66
|
+
* coincide con «ha un documento a schermo»: `search` mostra un'anteprima, ma
|
|
67
|
+
* il fuoco lì è sulla lista dei risultati, che è una selezione.
|
|
68
|
+
*
|
|
69
|
+
* Il custode è `MODE_WHEEL` in `cli.tsx`, un `Record<ScrollingMode, …>` che non
|
|
70
|
+
* compila se un modo entra qui senza uno scroll da chiamare.
|
|
71
|
+
*/
|
|
72
|
+
export const SCROLLING_MODES = ['detail', 'status', 'reader'];
|
|
73
|
+
const SCROLLING = new Set(SCROLLING_MODES);
|
|
74
|
+
/** `true` se la rotella scorre il contenuto del modo. */
|
|
75
|
+
export function scrolls(mode) {
|
|
76
|
+
return SCROLLING.has(mode);
|
|
77
|
+
}
|
package/dist/mouse.js
CHANGED
|
@@ -32,6 +32,28 @@ const SGR_MOUSE = /\x1b?\[<(\d+);(\d+);(\d+)([Mm])/g;
|
|
|
32
32
|
export function isWheel(button) {
|
|
33
33
|
return (button & 64) !== 0;
|
|
34
34
|
}
|
|
35
|
+
/**
|
|
36
|
+
* Verso della rotella: `-1` su (codice 64), `+1` giù (65), `0` se non è una
|
|
37
|
+
* rotella. Il verso sta nel bit 0, i modificatori nei bit 2-4 (shift/meta/
|
|
38
|
+
* ctrl): `66`/`67` sono le rotelle orizzontali, che qui valgono `0` perché il
|
|
39
|
+
* deck non ha nulla da scorrere in orizzontale.
|
|
40
|
+
*
|
|
41
|
+
* Un terminale manda la rotella come SOLA pressione (`M`), mai seguita da un
|
|
42
|
+
* rilascio: un tacca = un evento, e il chiamante non deve dedoppiare come fa
|
|
43
|
+
* per il click.
|
|
44
|
+
*/
|
|
45
|
+
export function wheelDir(button) {
|
|
46
|
+
if (!isWheel(button) || (button & 2) !== 0)
|
|
47
|
+
return 0;
|
|
48
|
+
return (button & 1) === 0 ? -1 : 1;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Righe scorse per tacca di rotella nei contenuti lunghi (detail, project
|
|
52
|
+
* status, reader). Tre è la convenzione dei terminali e dei browser: una tacca
|
|
53
|
+
* a riga singola costringe a girare la rotella quanto basta a stancare la mano,
|
|
54
|
+
* una a pagina salta il testo che si stava leggendo.
|
|
55
|
+
*/
|
|
56
|
+
export const WHEEL_LINES = 3;
|
|
35
57
|
/**
|
|
36
58
|
* Separa le sequenze mouse dal testo di un chunk di `useInput`.
|
|
37
59
|
*
|
|
@@ -177,3 +199,87 @@ export const LAUNCH_ROW = 4;
|
|
|
177
199
|
/** Colonna (1-based) del primo carattere di testo dentro il box esterno:
|
|
178
200
|
* bordo + `paddingX={1}`. */
|
|
179
201
|
export const FRAME_TEXT_COL = 3;
|
|
202
|
+
// ── Hit-test delle liste (mandata 3) ────────────────────────────────────────
|
|
203
|
+
//
|
|
204
|
+
// Click su un elemento di lista = SOLO FUOCO (D2): la selezione si sposta, non
|
|
205
|
+
// parte nessuna azione. A differenza della riga launch il click qui non può
|
|
206
|
+
// rientrare dalla porta della tastiera — non esiste un tasto che dica «vai alla
|
|
207
|
+
// riga 7» o «vista `nascoste`»: `↑↓` e `tab` sono relativi alla posizione
|
|
208
|
+
// corrente. Il chiamante chiama quindi il setter della selezione, come la
|
|
209
|
+
// rotella chiama lo `scroll` del modo.
|
|
210
|
+
/**
|
|
211
|
+
* Righe fisse dei due pane in modalità normale, contate dall'alto come
|
|
212
|
+
* `LAUNCH_ROW` e per la stessa ragione: sopra di loro non c'è niente di
|
|
213
|
+
* condizionale. Sotto la riga launch stanno il `marginTop` del blocco pane
|
|
214
|
+
* (riga 5), il bordo superiore (6), l'header con le viste (7) e il corpo (8+).
|
|
215
|
+
*/
|
|
216
|
+
export const PANE_HEADER_ROW = LAUNCH_ROW + 3;
|
|
217
|
+
export const PANE_BODY_ROW = PANE_HEADER_ROW + 1;
|
|
218
|
+
/** Nel pane task fra header e lista sta la riga sort/filtri, che non si
|
|
219
|
+
* seleziona: la prima riga di lista (`≡ tutte`) è quella dopo. */
|
|
220
|
+
export const TASK_LIST_ROW = PANE_BODY_ROW + 1;
|
|
221
|
+
/** Colonne fra il bordo di un pane e il suo testo: bordo + `paddingX={1}`. */
|
|
222
|
+
export const PANE_TEXT_PAD = 2;
|
|
223
|
+
/**
|
|
224
|
+
* Colonne (1-based, estremi inclusi, bordi compresi) dei due pane affiancati.
|
|
225
|
+
*
|
|
226
|
+
* Entrambi dichiarano `width="50%"` dell'interno del box esterno (`columns - 4`)
|
|
227
|
+
* e il primo porta `marginRight={1}`: la somma sfora di una colonna e Yoga la
|
|
228
|
+
* toglie per shrink, metà a testa, poi arrotonda le posizioni. L'esito
|
|
229
|
+
* misurato sotto pty è che il pane task è largo `floor((columns - 4) / 2)` —
|
|
230
|
+
* lo stesso conto di `paneTextWidth` più i suoi 4 di cornice — e il pane
|
|
231
|
+
* sessioni comincia dopo la colonna di margine e arriva al padding del box
|
|
232
|
+
* esterno. La colonna di margine non appartiene a nessuno dei due.
|
|
233
|
+
*/
|
|
234
|
+
export function paneSpans(columns) {
|
|
235
|
+
const inner = (columns || 80) - 4;
|
|
236
|
+
const taskW = Math.floor(inner / 2);
|
|
237
|
+
const tasksEnd = FRAME_TEXT_COL + taskW - 1;
|
|
238
|
+
return {
|
|
239
|
+
tasks: { start: FRAME_TEXT_COL, end: tasksEnd },
|
|
240
|
+
sessions: { start: tasksEnd + 2, end: FRAME_TEXT_COL + inner - 1 },
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* Colonne delle parti di una riga che portano il proprio separatore IN TESTA
|
|
245
|
+
* (gli header dei pane: ` · 68 nascoste`). La regione di una parte keyed
|
|
246
|
+
* esclude il separatore: un click sul ` · ` fra due viste non deve sceglierne
|
|
247
|
+
* una. Le parti con `key` null (nome del pane, `↑N`/`↓N`) consumano colonne ma
|
|
248
|
+
* non producono regione; quelle vuote (cadute al taglio) non consumano niente.
|
|
249
|
+
*/
|
|
250
|
+
export function inlineRegions(parts, startCol) {
|
|
251
|
+
const out = [];
|
|
252
|
+
let col = startCol;
|
|
253
|
+
for (const p of parts) {
|
|
254
|
+
const w = termWidth(p.text);
|
|
255
|
+
if (w === 0)
|
|
256
|
+
continue;
|
|
257
|
+
if (p.key !== null) {
|
|
258
|
+
const lead = termWidth(p.text.match(/^[\s·]+/)?.[0] ?? '');
|
|
259
|
+
if (w > lead)
|
|
260
|
+
out.push({ key: p.key, start: col + lead, end: col + w - 1 });
|
|
261
|
+
}
|
|
262
|
+
col += w;
|
|
263
|
+
}
|
|
264
|
+
return out;
|
|
265
|
+
}
|
|
266
|
+
/** L'elemento di lista sotto il click, o `null` se il click cade su cornice,
|
|
267
|
+
* riga sort, spazio vuoto sotto la lista o fuori dai pane. */
|
|
268
|
+
export function listHit(ev, g) {
|
|
269
|
+
const spans = paneSpans(g.columns);
|
|
270
|
+
const inTasks = ev.col >= spans.tasks.start && ev.col <= spans.tasks.end;
|
|
271
|
+
const inSessions = ev.col >= spans.sessions.start && ev.col <= spans.sessions.end;
|
|
272
|
+
if (!inTasks && !inSessions)
|
|
273
|
+
return null;
|
|
274
|
+
const pane = inTasks ? 'tasks' : 'sessions';
|
|
275
|
+
if (ev.row === PANE_HEADER_ROW) {
|
|
276
|
+
const key = hitRegion(inTasks ? g.taskHeader : g.sessionHeader, ev.col);
|
|
277
|
+
return key ? { pane, target: 'view', key } : null;
|
|
278
|
+
}
|
|
279
|
+
const first = inTasks ? TASK_LIST_ROW : PANE_BODY_ROW;
|
|
280
|
+
const count = inTasks ? g.taskRows : g.sessionRows;
|
|
281
|
+
const index = ev.row - first;
|
|
282
|
+
if (index < 0 || index >= count)
|
|
283
|
+
return null;
|
|
284
|
+
return { pane, target: 'row', index };
|
|
285
|
+
}
|
package/dist/overlays/search.js
CHANGED
package/dist/overlays/sheet.js
CHANGED
package/dist/overlays/status.js
CHANGED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// T21 (mandata 3) — le PARTI degli header dei due pane come DATO, non come JSX.
|
|
2
|
+
//
|
|
3
|
+
// Stesso principio della riga launch (`launchSegments` in `cli.tsx`): gli
|
|
4
|
+
// stessi oggetti compongono la riga disegnata (`ui/panes.tsx`) e le colonne
|
|
5
|
+
// dell'hit-test (`cli.tsx`). Chi li deriva dall'altro — ri-splittando la
|
|
6
|
+
// stringa renderizzata, o ricontando le voci del catalogo — tiene due conti
|
|
7
|
+
// paralleli che divergono alla prima voce che cambia testo o ordine.
|
|
8
|
+
//
|
|
9
|
+
// Modulo PURO: nessun import da ink/react. Il taglio al budget (`cutParts`) sta
|
|
10
|
+
// qui e non nel componente perché è il taglio a decidere quali parti esistono a
|
|
11
|
+
// schermo, e una parte caduta non deve rispondere a un click.
|
|
12
|
+
import { cutParts } from './width.js';
|
|
13
|
+
import { paneTextWidth } from './layout.js';
|
|
14
|
+
import { SESSION_VIEWS, TASK_VIEWS, } from './pane-views.js';
|
|
15
|
+
function finish(parts, columns) {
|
|
16
|
+
const shown = cutParts(parts.map((p) => p.text), paneTextWidth(columns), parts.findIndex((p) => p.active));
|
|
17
|
+
return { parts, shown };
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Header del pane task: le tre viste del catalogo, poi `↑N`/`↓N`.
|
|
21
|
+
* Le voci ci sono tutte anche a 0 (T100/D1) e l'attiva ha la precedenza sul
|
|
22
|
+
* budget (D6); i contatori di scroll stanno in coda perché sono i primi a
|
|
23
|
+
* cedere il posto su un terminale stretto.
|
|
24
|
+
*/
|
|
25
|
+
export function taskHeaderParts(counts, active, above, below, columns) {
|
|
26
|
+
const views = TASK_VIEWS.map((v, i) => ({
|
|
27
|
+
key: v.id,
|
|
28
|
+
text: `${i > 0 ? ' · ' : ''}${v.label(counts)}`,
|
|
29
|
+
color: v.color,
|
|
30
|
+
dim: Boolean(v.dim) || v.count(counts) === 0,
|
|
31
|
+
active: v.id === active,
|
|
32
|
+
}));
|
|
33
|
+
return finish([
|
|
34
|
+
...views,
|
|
35
|
+
{ key: null, text: above > 0 ? ` · ↑${above}` : '', dim: true, active: false },
|
|
36
|
+
{ key: null, text: below > 0 ? ` · ↓${below}` : '', dim: true, active: false },
|
|
37
|
+
], columns);
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Header del pane sessioni: `Sessions` nomina il pane e non è una voce del
|
|
41
|
+
* catalogo (non si seleziona), poi le quattro viste, poi `↑N`/`↓N`.
|
|
42
|
+
*/
|
|
43
|
+
export function sessionHeaderParts(parentLabel, counts, active, above, below, columns) {
|
|
44
|
+
const views = SESSION_VIEWS.map((v) => ({
|
|
45
|
+
key: v.id,
|
|
46
|
+
text: ` · ${v.label(counts, parentLabel)}`,
|
|
47
|
+
color: v.color,
|
|
48
|
+
dim: Boolean(v.dim) || v.count(counts) === 0,
|
|
49
|
+
active: v.id === active,
|
|
50
|
+
}));
|
|
51
|
+
return finish([
|
|
52
|
+
{ key: null, text: 'Sessions', dim: false, active: false },
|
|
53
|
+
...views,
|
|
54
|
+
{ key: null, text: above > 0 ? ` · ↑${above}` : '', dim: true, active: false },
|
|
55
|
+
{ key: null, text: below > 0 ? ` · ↓${below}` : '', dim: true, active: false },
|
|
56
|
+
], columns);
|
|
57
|
+
}
|
|
58
|
+
/** Le parti COME SONO A SCHERMO — testo tagliato, chiave intatta — nella forma
|
|
59
|
+
* che `inlineRegions` (`mouse.ts`) misura per l'hit-test. */
|
|
60
|
+
export function headerItems(h) {
|
|
61
|
+
return h.parts.map((p, i) => ({ key: p.key, text: h.shown[i] ?? '' }));
|
|
62
|
+
}
|
package/dist/ui/panes.js
CHANGED
|
@@ -3,13 +3,14 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
3
3
|
// header. Sono presentazionali puri: ricevono la vista già selezionata e le
|
|
4
4
|
// larghezze già calcolate, non li derivano.
|
|
5
5
|
import { Box, Text } from 'ink';
|
|
6
|
-
import { cut,
|
|
6
|
+
import { cut, pad, sanitize, termWidth } from '../width.js';
|
|
7
7
|
import { paneTextWidth } from '../layout.js';
|
|
8
8
|
import { CARET, CARET_OFF, LIVE_BUSY, LIVE_IDLE, LIVE_NONE, MODEL_W, SESSION_SEP, SID_CHARS, TASK_EMPTY, WARN, metaCount, modelShort, relTime, } from '../glyphs.js';
|
|
9
9
|
import { TaskRow } from './task-row.js';
|
|
10
10
|
import { META_ROWS, ROW_ALL, ROW_SPOT } from '../model.js';
|
|
11
11
|
import { rowLabel, sessionTitle } from '../session-list.js';
|
|
12
|
-
import { sessionView, taskView,
|
|
12
|
+
import { sessionView, taskView, } from '../pane-views.js';
|
|
13
|
+
import { sessionHeaderParts, taskHeaderParts } from '../pane-header.js';
|
|
13
14
|
import { describeSort, PRI_ENTRIES, PROG_ENTRIES, } from '../view.js';
|
|
14
15
|
/**
|
|
15
16
|
* Header del pane task, tagliato QUI e non da Ink — stesso motivo del gemello
|
|
@@ -31,24 +32,16 @@ import { describeSort, PRI_ENTRIES, PROG_ENTRIES, } from '../view.js';
|
|
|
31
32
|
* su un terminale stretto — e la voce attiva ha la precedenza sul budget (D6).
|
|
32
33
|
*/
|
|
33
34
|
export function TasksHeader({ counts, active, above, below, focused, columns, }) {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
});
|
|
45
|
-
const segments = [
|
|
46
|
-
...views,
|
|
47
|
-
{ text: above > 0 ? ` · ↑${above}` : '', dim: true, active: false, color: undefined },
|
|
48
|
-
{ text: below > 0 ? ` · ↓${below}` : '', dim: true, active: false, color: undefined },
|
|
49
|
-
];
|
|
50
|
-
const shown = cutParts(segments.map((s) => s.text), paneTextWidth(columns), segments.findIndex((s) => s.active));
|
|
51
|
-
return (_jsx(Text, { bold: true, color: focused ? 'cyan' : undefined, wrap: "truncate-end", children: segments.map((seg, i) => shown[i] ? (_jsx(Text, { color: seg.color, dimColor: seg.dim, inverse: seg.active, children: shown[i] }, i)) : null) }));
|
|
35
|
+
// T21 — le parti e il loro taglio vengono da `pane-header.ts`, lo stesso
|
|
36
|
+
// modulo da cui `cli.tsx` ricava le colonne cliccabili: una fonte sola.
|
|
37
|
+
const { parts, shown } = taskHeaderParts(counts, active, above, below, columns);
|
|
38
|
+
return _jsx(HeaderLine, { parts: parts, shown: shown, focused: focused });
|
|
39
|
+
}
|
|
40
|
+
/** Resa di un header già tagliato: ogni parte col suo stile, l'attiva in
|
|
41
|
+
* video inverso (T100/D5 — costa 0 colonne e non entra in gara con la
|
|
42
|
+
* semantica di colore già occupata). */
|
|
43
|
+
function HeaderLine({ parts, shown, focused, }) {
|
|
44
|
+
return (_jsx(Text, { bold: true, color: focused ? 'cyan' : undefined, wrap: "truncate-end", children: parts.map((seg, i) => shown[i] ? (_jsx(Text, { color: seg.color, dimColor: seg.dim, inverse: seg.active, children: shown[i] }, i)) : null) }));
|
|
52
45
|
}
|
|
53
46
|
export function TasksPane({ tasks, counts, activeView, paneCount, view, selected, spotCount, allCount, idW, tailW, focused, loadError, windowStart, above, below, columns, data, }) {
|
|
54
47
|
const allSelected = selected === ROW_ALL;
|
|
@@ -87,25 +80,8 @@ export function TasksPane({ tasks, counts, activeView, paneCount, view, selected
|
|
|
87
80
|
* pinnate dal resto dell'header e non è decorazione.
|
|
88
81
|
*/
|
|
89
82
|
export function SessionsHeader({ parentLabel, counts, active, above, below, focused, columns, }) {
|
|
90
|
-
const
|
|
91
|
-
|
|
92
|
-
return {
|
|
93
|
-
text: ` · ${v.label(counts, parentLabel)}`,
|
|
94
|
-
color: v.color,
|
|
95
|
-
dim: v.dim || n === 0,
|
|
96
|
-
active: v.id === active,
|
|
97
|
-
};
|
|
98
|
-
});
|
|
99
|
-
const segments = [
|
|
100
|
-
// `Sessions` non è una voce del catalogo: nomina il pane, non un
|
|
101
|
-
// sottoinsieme, quindi non è raggiungibile con le frecce.
|
|
102
|
-
{ text: 'Sessions', color: undefined, dim: false, active: false },
|
|
103
|
-
...views,
|
|
104
|
-
{ text: above > 0 ? ` · ↑${above}` : '', dim: true, active: false, color: undefined },
|
|
105
|
-
{ text: below > 0 ? ` · ↓${below}` : '', dim: true, active: false, color: undefined },
|
|
106
|
-
];
|
|
107
|
-
const shown = cutParts(segments.map((s) => s.text), paneTextWidth(columns), segments.findIndex((s) => s.active));
|
|
108
|
-
return (_jsx(Text, { bold: true, color: focused ? 'cyan' : undefined, wrap: "truncate-end", children: segments.map((seg, i) => shown[i] ? (_jsx(Text, { color: seg.color, dimColor: seg.dim, inverse: seg.active, children: shown[i] }, i)) : null) }));
|
|
83
|
+
const { parts, shown } = sessionHeaderParts(parentLabel, counts, active, above, below, columns);
|
|
84
|
+
return _jsx(HeaderLine, { parts: parts, shown: shown, focused: focused });
|
|
109
85
|
}
|
|
110
86
|
export function SessionsPane({ parentLabel, isSpot, isAll, bindings, taskW, ageW, rows, counts, activeView, paneCount, selectedId, focused, above, below, columns, forkOf, sessionNotes, projectCore, live, }) {
|
|
111
87
|
return (_jsxs(Box, { flexDirection: "column", width: "50%", borderStyle: "single", borderColor: focused ? 'cyan' : 'gray', paddingX: 1, children: [_jsx(SessionsHeader, { parentLabel: parentLabel, counts: counts, active: activeView, above: above, below: below, focused: focused, columns: columns }), paneCount === 0 ? (
|
package/package.json
CHANGED