@lamemind/loom-deck 0.43.0 → 0.44.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 +16 -3
- package/dist/glyphs.js +22 -0
- package/dist/ui/panes.js +23 -9
- package/dist/view.js +31 -0
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -11,12 +11,12 @@ import { cellWidth, launchLegend, loadArchivableDays, loadIdentity, loadLaunch,
|
|
|
11
11
|
import { cycleSessionView, cycleTaskView, selectSessionRows, selectTasks, sessionView, taskView, TASK_VIEWS, } from './pane-views.js';
|
|
12
12
|
import { isCompact, layoutBudget, searchPreviewCapacity, windowRange, } from './viewport.js';
|
|
13
13
|
import { cut, cutMiddle, sanitize, termWidth } from './width.js';
|
|
14
|
-
import { applyView, cycleSort, describeSort, priName, progName, toggleHidden, PRI_ENTRIES, PROG_ENTRIES, } from './view.js';
|
|
14
|
+
import { applyView, cycleSort, describeSort, idColumnWidth, priName, progName, toggleHidden, PRI_ENTRIES, PROG_ENTRIES, } from './view.js';
|
|
15
15
|
import { initialDetail, writeTaskEdit, PRI_GLYPH, PRI_LABEL } from './task-edit.js';
|
|
16
16
|
import { ALL, EDIT_PRI, EDIT_PROG, MAX_SESSIONS, MAX_SESSIONS_ALL, META_ROWS, QUIT_WINDOW_MS, ROW_ALL, ROW_SPOT, SORT_TASTI, SPOT, } from './model.js';
|
|
17
17
|
import { purgeTargets, splitTargets } from './purge.js';
|
|
18
18
|
import { conversationLabel, cpLen, editField, insertAt, isDone, isTextRow, removeAt, EDIT_ROWS, } from './layout.js';
|
|
19
|
-
import { TASK_EMPTY, relTime, sanitizeTyped } from './glyphs.js';
|
|
19
|
+
import { TASK_EMPTY, relTime, sanitizeTyped, taskTail } from './glyphs.js';
|
|
20
20
|
import { commitTaskEdit, runLaunch, spawnClaudeEmpty, spawnCleanTasks, spawnCreateTask, spawnDeck, spawnDeckFork, spawnDeckResume, spawnTerminal, onInTabCommand, CLAUDE_CMD, DECK_RUN, MODEL_DEFAULT, } from './spawn.js';
|
|
21
21
|
import { EditModal, FilterModal, PurgeModal, SortModal, idList } from './ui/modals.js';
|
|
22
22
|
import { ReaderScreen, SearchScreen } from './ui/search-screen.js';
|
|
@@ -169,6 +169,19 @@ function Deck({ cwd, tasksPath, tasksDir }) {
|
|
|
169
169
|
else
|
|
170
170
|
spotCount++;
|
|
171
171
|
}
|
|
172
|
+
// T118 — colonne fisse della lista task, misurate su `paneTasks` (la vista
|
|
173
|
+
// attiva INTERA) e non sulla finestra visibile: gemelle di `sessionCols` e
|
|
174
|
+
// per la stessa ragione, che una larghezza derivata dallo schermo si muove a
|
|
175
|
+
// ogni scroll. La coda porta dentro il proprio gutter, così l'allineamento a
|
|
176
|
+
// destra lo produce `pad` da sé; nessuna riga con qualcosa da scrivere →
|
|
177
|
+
// colonna spenta a `0`, e le descrizioni si riprendono lo spazio.
|
|
178
|
+
const taskCols = (() => {
|
|
179
|
+
let tail = 0;
|
|
180
|
+
for (const t of paneTasks) {
|
|
181
|
+
tail = Math.max(tail, termWidth(taskTail(childCount.get(t.id) ?? 0, dirtyFolders.has(t.id))));
|
|
182
|
+
}
|
|
183
|
+
return { id: idColumnWidth(paneTasks), tail: tail > 0 ? tail + 1 : 0 };
|
|
184
|
+
})();
|
|
172
185
|
// Figli della selezione: tutte le conversazioni del progetto (`≡ tutte`), le
|
|
173
186
|
// sessioni bound alla task selezionata, oppure (spot) quelle senza binding.
|
|
174
187
|
// sessions è già ts desc → l'ordine si eredita in tutti e tre i rami.
|
|
@@ -1412,7 +1425,7 @@ function Deck({ cwd, tasksPath, tasksDir }) {
|
|
|
1412
1425
|
if (budget.compact) {
|
|
1413
1426
|
return (_jsxs(Text, { wrap: "truncate-end", children: [_jsx(Text, { bold: true, color: "cyan", children: "loom-deck" }), _jsxs(Text, { dimColor: true, children: [' ', "v", VERSION, " \u00B7 ", viewTasks.length, " task \u00B7 sel ", selectedTaskId ?? parentLabel, " \u00B7 terminale ", rows, "\u00D7", columns, ": troppo basso, allarga"] })] }));
|
|
1414
1427
|
}
|
|
1415
|
-
return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1, children: [_jsxs(Box, { flexDirection: "row", justifyContent: "space-between", children: [_jsx(Text, { bold: true, color: "cyan", children: "loom-deck" }), _jsxs(Text, { dimColor: true, children: ["v", VERSION] })] }), mode === 'create' ? (_jsxs(Text, { dimColor: true, wrap: "truncate-end", children: ["nuova task \u00B7 ", _jsx(Text, { color: "yellow", children: "\u23CE" }), " crea \u00B7 ", _jsx(Text, { color: "yellow", children: "esc" }), " annulla"] })) : mode === 'sort' ? (_jsxs(Text, { dimColor: true, wrap: "truncate-end", children: ["sort \u00B7 ", _jsx(Text, { color: "yellow", children: "p" }), " pri ", _jsx(Text, { color: "yellow", children: "s" }), " stato", ' ', _jsx(Text, { color: "yellow", children: "i" }), " id (asc\u2192desc\u2192off) \u00B7 ", _jsx(Text, { color: "yellow", children: "\u23CE" }), " ok \u00B7", ' ', _jsx(Text, { color: "yellow", children: "esc" }), " annulla"] })) : mode === 'filter' ? (_jsxs(Text, { dimColor: true, wrap: "truncate-end", children: ["filtri \u00B7 ", _jsx(Text, { color: "yellow", children: "\u2191\u2193\u2190\u2192" }), " naviga \u00B7 ", _jsx(Text, { color: "yellow", children: "spazio" }), ' ', "mostra/nascondi \u00B7 ", _jsx(Text, { color: "yellow", children: "\u23CE" }), " ok \u00B7 ", _jsx(Text, { color: "yellow", children: "esc" }), " annulla"] })) : mode === 'note' ? (_jsxs(Text, { dimColor: true, wrap: "truncate-end", children: ["nota conversazione \u00B7 ", _jsx(Text, { color: "yellow", children: "^U" }), " svuota \u00B7", ' ', _jsx(Text, { color: "yellow", children: "\u23CE" }), " salva (vuoto = rimuove) \u00B7", ' ', _jsx(Text, { color: "yellow", children: "esc" }), " annulla"] })) : mode === 'purge' ? (_jsxs(Text, { dimColor: true, wrap: "truncate-end", children: ["elimina task \u00B7", ' ', purge?.ignored ? (_jsxs(_Fragment, { children: [_jsx(Text, { color: "yellow", children: "\u2190\u2192" }), " keep/purge dei file non tracciati \u00B7", ' '] })) : null, _jsx(Text, { color: "yellow", children: "\u23CE" }), " conferma \u00B7 ", _jsx(Text, { color: "yellow", children: "esc" }), " annulla"] })) : mode === 'edit' ? (_jsxs(Text, { dimColor: true, wrap: "truncate-end", children: ["edit \u00B7 ", _jsx(Text, { color: "yellow", children: "\u2191\u2193" }), " campo \u00B7 ", _jsx(Text, { color: "yellow", children: "\u2190\u2192" }), " valore, o cursore sul testo \u00B7 ", _jsx(Text, { color: "yellow", children: "^A/^E" }), " inizio/fine \u00B7", ' ', _jsx(Text, { color: "yellow", children: "^D" }), " canc \u00B7 ", _jsx(Text, { color: "yellow", children: "\u23CE" }), " salva+commit \u00B7", ' ', _jsx(Text, { color: "yellow", children: "esc" }), " annulla"] })) : (_jsx(Text, { dimColor: true, wrap: "truncate-end", children: keyLegend })), mode === 'normal' ? (_jsxs(Text, { dimColor: true, wrap: "truncate-end", children: [surfaceLegend, legend.shown ? ` · ${legend.shown}` : '', legend.overflow > 0 ? (_jsxs(Text, { color: "yellow", children: [" \u00B7 +", legend.overflow, " fuori riga"] })) : null, legend.unreachable > 0 ? (_jsxs(Text, { color: "yellow", children: [" \u00B7 ", legend.unreachable, " oltre la 9\u00AA (non raggiungibili)"] })) : null] })) : null, mode === 'create' ? (_jsxs(Box, { borderStyle: "round", borderColor: "yellow", paddingX: 1, marginTop: 1, children: [_jsx(Text, { color: "yellow", children: "C \u203A " }), _jsx(Text, { children: draft }), _jsx(Text, { inverse: true, children: " " })] })) : null, mode === 'note' ? (_jsxs(Box, { borderStyle: "round", borderColor: "yellow", paddingX: 1, marginTop: 1, children: [_jsx(Text, { color: "yellow", children: "\u270E \u203A " }), _jsx(Text, { children: noteDraft }), _jsx(Text, { inverse: true, children: " " })] })) : null, mode === 'sort' ? _jsx(SortModal, { sort: view.sort }) : null, mode === 'filter' ? _jsx(FilterModal, { view: view, cursor: filterCursor }) : null, mode === 'edit' && edit && selTask ? (_jsx(EditModal, { id: selTask.id, draft: edit, row: editRow, columns: columns })) : null, mode === 'purge' && purge ? _jsx(PurgeModal, { draft: purge, columns: columns }) : null, _jsxs(Box, { flexDirection: "row", marginTop: 1, children: [_jsx(TasksPane, { tasks: windowTasks, counts: taskCounts, activeView: taskViewId, paneCount: paneTasks.length, view: view, selected: selIndex, spotCount: spotCount, allCount: sessions.length, childCount: childCount, focused: focus === 'tasks', loadError: loadError, windowStart: taskWin.start, above: taskWin.start, below: paneTasks.length - taskWin.end, columns: columns, dirty: dirtyFolders }), _jsx(SessionsPane, { parentLabel: parentLabel, isSpot: isSpot, isAll: isAll, bindings: bindings, taskW: sessionCols.task, ageW: sessionCols.age, rows: windowRows, counts: sessionCounts, activeView: sessionViewId, paneCount: sessionRows.length, selectedId: selSessionId ?? undefined, focused: focus === 'sessions', above: sessionWin.start, below: sessionRows.length - sessionWin.end, columns: columns, forkOf: forkOf, sessionNotes: sessionNotes, projectCore: projectCore, live: live })] }), budget.preview && previewKind === 'task' && detail ? (_jsx(PreviewPane, { kind: "task", detail: detail, maxLines: budget.detailLines, columns: columns })) : budget.preview && previewKind === 'session' && selSessionObj ? (_jsx(PreviewPane, { kind: "session", s: selSessionObj, firstLines: budget.sessionFirstLines, lastLines: budget.sessionLastLines, columns: columns, origin: forkOf.get(selSessionObj.sessionId) ?? null, note: sessionNotes.get(selSessionObj.sessionId) ?? '', live: live.get(selSessionObj.sessionId) ?? null })) : null, note ? _jsx(Text, { color: "green", wrap: "truncate-end", children: sanitize(note) }) : null] }));
|
|
1428
|
+
return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1, children: [_jsxs(Box, { flexDirection: "row", justifyContent: "space-between", children: [_jsx(Text, { bold: true, color: "cyan", children: "loom-deck" }), _jsxs(Text, { dimColor: true, children: ["v", VERSION] })] }), mode === 'create' ? (_jsxs(Text, { dimColor: true, wrap: "truncate-end", children: ["nuova task \u00B7 ", _jsx(Text, { color: "yellow", children: "\u23CE" }), " crea \u00B7 ", _jsx(Text, { color: "yellow", children: "esc" }), " annulla"] })) : mode === 'sort' ? (_jsxs(Text, { dimColor: true, wrap: "truncate-end", children: ["sort \u00B7 ", _jsx(Text, { color: "yellow", children: "p" }), " pri ", _jsx(Text, { color: "yellow", children: "s" }), " stato", ' ', _jsx(Text, { color: "yellow", children: "i" }), " id (asc\u2192desc\u2192off) \u00B7 ", _jsx(Text, { color: "yellow", children: "\u23CE" }), " ok \u00B7", ' ', _jsx(Text, { color: "yellow", children: "esc" }), " annulla"] })) : mode === 'filter' ? (_jsxs(Text, { dimColor: true, wrap: "truncate-end", children: ["filtri \u00B7 ", _jsx(Text, { color: "yellow", children: "\u2191\u2193\u2190\u2192" }), " naviga \u00B7 ", _jsx(Text, { color: "yellow", children: "spazio" }), ' ', "mostra/nascondi \u00B7 ", _jsx(Text, { color: "yellow", children: "\u23CE" }), " ok \u00B7 ", _jsx(Text, { color: "yellow", children: "esc" }), " annulla"] })) : mode === 'note' ? (_jsxs(Text, { dimColor: true, wrap: "truncate-end", children: ["nota conversazione \u00B7 ", _jsx(Text, { color: "yellow", children: "^U" }), " svuota \u00B7", ' ', _jsx(Text, { color: "yellow", children: "\u23CE" }), " salva (vuoto = rimuove) \u00B7", ' ', _jsx(Text, { color: "yellow", children: "esc" }), " annulla"] })) : mode === 'purge' ? (_jsxs(Text, { dimColor: true, wrap: "truncate-end", children: ["elimina task \u00B7", ' ', purge?.ignored ? (_jsxs(_Fragment, { children: [_jsx(Text, { color: "yellow", children: "\u2190\u2192" }), " keep/purge dei file non tracciati \u00B7", ' '] })) : null, _jsx(Text, { color: "yellow", children: "\u23CE" }), " conferma \u00B7 ", _jsx(Text, { color: "yellow", children: "esc" }), " annulla"] })) : mode === 'edit' ? (_jsxs(Text, { dimColor: true, wrap: "truncate-end", children: ["edit \u00B7 ", _jsx(Text, { color: "yellow", children: "\u2191\u2193" }), " campo \u00B7 ", _jsx(Text, { color: "yellow", children: "\u2190\u2192" }), " valore, o cursore sul testo \u00B7 ", _jsx(Text, { color: "yellow", children: "^A/^E" }), " inizio/fine \u00B7", ' ', _jsx(Text, { color: "yellow", children: "^D" }), " canc \u00B7 ", _jsx(Text, { color: "yellow", children: "\u23CE" }), " salva+commit \u00B7", ' ', _jsx(Text, { color: "yellow", children: "esc" }), " annulla"] })) : (_jsx(Text, { dimColor: true, wrap: "truncate-end", children: keyLegend })), mode === 'normal' ? (_jsxs(Text, { dimColor: true, wrap: "truncate-end", children: [surfaceLegend, legend.shown ? ` · ${legend.shown}` : '', legend.overflow > 0 ? (_jsxs(Text, { color: "yellow", children: [" \u00B7 +", legend.overflow, " fuori riga"] })) : null, legend.unreachable > 0 ? (_jsxs(Text, { color: "yellow", children: [" \u00B7 ", legend.unreachable, " oltre la 9\u00AA (non raggiungibili)"] })) : null] })) : null, mode === 'create' ? (_jsxs(Box, { borderStyle: "round", borderColor: "yellow", paddingX: 1, marginTop: 1, children: [_jsx(Text, { color: "yellow", children: "C \u203A " }), _jsx(Text, { children: draft }), _jsx(Text, { inverse: true, children: " " })] })) : null, mode === 'note' ? (_jsxs(Box, { borderStyle: "round", borderColor: "yellow", paddingX: 1, marginTop: 1, children: [_jsx(Text, { color: "yellow", children: "\u270E \u203A " }), _jsx(Text, { children: noteDraft }), _jsx(Text, { inverse: true, children: " " })] })) : null, mode === 'sort' ? _jsx(SortModal, { sort: view.sort }) : null, mode === 'filter' ? _jsx(FilterModal, { view: view, cursor: filterCursor }) : null, mode === 'edit' && edit && selTask ? (_jsx(EditModal, { id: selTask.id, draft: edit, row: editRow, columns: columns })) : null, mode === 'purge' && purge ? _jsx(PurgeModal, { draft: purge, columns: columns }) : null, _jsxs(Box, { flexDirection: "row", marginTop: 1, children: [_jsx(TasksPane, { tasks: windowTasks, counts: taskCounts, activeView: taskViewId, paneCount: paneTasks.length, view: view, selected: selIndex, spotCount: spotCount, allCount: sessions.length, childCount: childCount, idW: taskCols.id, tailW: taskCols.tail, focused: focus === 'tasks', loadError: loadError, windowStart: taskWin.start, above: taskWin.start, below: paneTasks.length - taskWin.end, columns: columns, dirty: dirtyFolders }), _jsx(SessionsPane, { parentLabel: parentLabel, isSpot: isSpot, isAll: isAll, bindings: bindings, taskW: sessionCols.task, ageW: sessionCols.age, rows: windowRows, counts: sessionCounts, activeView: sessionViewId, paneCount: sessionRows.length, selectedId: selSessionId ?? undefined, focused: focus === 'sessions', above: sessionWin.start, below: sessionRows.length - sessionWin.end, columns: columns, forkOf: forkOf, sessionNotes: sessionNotes, projectCore: projectCore, live: live })] }), budget.preview && previewKind === 'task' && detail ? (_jsx(PreviewPane, { kind: "task", detail: detail, maxLines: budget.detailLines, columns: columns })) : budget.preview && previewKind === 'session' && selSessionObj ? (_jsx(PreviewPane, { kind: "session", s: selSessionObj, firstLines: budget.sessionFirstLines, lastLines: budget.sessionLastLines, columns: columns, origin: forkOf.get(selSessionObj.sessionId) ?? null, note: sessionNotes.get(selSessionObj.sessionId) ?? '', live: live.get(selSessionObj.sessionId) ?? null })) : null, note ? _jsx(Text, { color: "green", wrap: "truncate-end", children: sanitize(note) }) : null] }));
|
|
1416
1429
|
}
|
|
1417
1430
|
const cwd = process.cwd();
|
|
1418
1431
|
// T116 — `exitOnCtrlC: false` toglie a Ink l'uscita immediata su `^C`: senza,
|
package/dist/glyphs.js
CHANGED
|
@@ -74,6 +74,28 @@ export const LIVE_NONE = ' ';
|
|
|
74
74
|
export function displayProg(prog) {
|
|
75
75
|
return sanitize(prog);
|
|
76
76
|
}
|
|
77
|
+
/**
|
|
78
|
+
* T118 — coda della riga task: marker dirty e contatore di conversazioni, nella
|
|
79
|
+
* colonna ancorata al bordo destro del pane.
|
|
80
|
+
*
|
|
81
|
+
* Il contatore va per ULTIMO perché è lui ad allinearsi al bordo — la colonna è
|
|
82
|
+
* resa `pad(…, 'right')`, quindi ciò che sta in fondo alla stringa cade sempre
|
|
83
|
+
* nella stessa colonna. Il marker gli sta a sinistra, dentro la stessa cella
|
|
84
|
+
* invece che in una colonna propria: è raro, e una colonna dedicata costerebbe
|
|
85
|
+
* 3 spazi vuoti su ogni riga di un pane largo la metà del terminale.
|
|
86
|
+
*
|
|
87
|
+
* Vive qui e non nel sito di render perché la STESSA stringa serve due volte —
|
|
88
|
+
* a misurare la larghezza della colonna (sulla vista completa, in `cli.tsx`) e
|
|
89
|
+
* a disegnarla (sulla finestra, in `panes.tsx`). Due formattazioni gemelle
|
|
90
|
+
* divergerebbero alla prima modifica, e la colonna risulterebbe larga quanto
|
|
91
|
+
* una stringa che nessuno scrive.
|
|
92
|
+
*/
|
|
93
|
+
export function taskTail(childCount, dirty) {
|
|
94
|
+
const count = childCount > 0 ? `(${childCount})` : '';
|
|
95
|
+
if (!dirty)
|
|
96
|
+
return count;
|
|
97
|
+
return count ? `${WARN} ${count}` : WARN;
|
|
98
|
+
}
|
|
77
99
|
// T49 — size umana compatta per il detail pane sessione.
|
|
78
100
|
export function fmtSize(bytes) {
|
|
79
101
|
if (bytes < 1024)
|
package/dist/ui/panes.js
CHANGED
|
@@ -5,11 +5,11 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
5
5
|
import { Box, Text } from 'ink';
|
|
6
6
|
import { cut, cutParts, pad, sanitize, termWidth } from '../width.js';
|
|
7
7
|
import { isDone, paneTextWidth } from '../layout.js';
|
|
8
|
-
import { CARET, CARET_OFF, LIVE_BUSY, LIVE_IDLE, LIVE_NONE, MODEL_W, SESSION_SEP, SID_CHARS, TASK_EMPTY, WARN, displayProg, modelShort, relTime, } from '../glyphs.js';
|
|
8
|
+
import { CARET, CARET_OFF, LIVE_BUSY, LIVE_IDLE, LIVE_NONE, MODEL_W, SESSION_SEP, SID_CHARS, TASK_EMPTY, WARN, displayProg, modelShort, relTime, taskTail, } from '../glyphs.js';
|
|
9
9
|
import { META_ROWS, ROW_ALL, ROW_SPOT } from '../model.js';
|
|
10
10
|
import { rowLabel, sessionTitle } from '../session-list.js';
|
|
11
11
|
import { sessionView, taskView, SESSION_VIEWS, TASK_VIEWS, } from '../pane-views.js';
|
|
12
|
-
import { describeSort, PRI_ENTRIES, PROG_ENTRIES } from '../view.js';
|
|
12
|
+
import { describeSort, padId, PRI_ENTRIES, PROG_ENTRIES } from '../view.js';
|
|
13
13
|
/**
|
|
14
14
|
* Header del pane task, tagliato QUI e non da Ink — stesso motivo del gemello
|
|
15
15
|
* `SessionsHeader`, con una differenza di rischio: qui i segmenti sono tutti
|
|
@@ -49,7 +49,7 @@ export function TasksHeader({ counts, active, above, below, focused, columns, })
|
|
|
49
49
|
const shown = cutParts(segments.map((s) => s.text), paneTextWidth(columns), segments.findIndex((s) => s.active));
|
|
50
50
|
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) }));
|
|
51
51
|
}
|
|
52
|
-
export function TasksPane({ tasks, counts, activeView, paneCount, view, selected, spotCount, allCount, childCount, focused, loadError, windowStart, above, below, columns, dirty, }) {
|
|
52
|
+
export function TasksPane({ tasks, counts, activeView, paneCount, view, selected, spotCount, allCount, childCount, idW, tailW, focused, loadError, windowStart, above, below, columns, dirty, }) {
|
|
53
53
|
const allSelected = selected === ROW_ALL;
|
|
54
54
|
const spotSelected = selected === ROW_SPOT;
|
|
55
55
|
return (_jsxs(Box, { flexDirection: "column", width: "50%", marginRight: 1, borderStyle: "single", borderColor: focused ? 'cyan' : 'gray', paddingX: 1, children: [_jsx(TasksHeader, { counts: counts, active: activeView, above: above, below: below, focused: focused, columns: columns }), _jsx(Text, { dimColor: true, wrap: "truncate-end", children: cut(sanitize(`sort: ${describeSort(view.sort)}` +
|
|
@@ -71,17 +71,31 @@ export function TasksPane({ tasks, counts, activeView, paneCount, view, selected
|
|
|
71
71
|
// completa, su cui è keyata la selezione. +META_ROWS: le prime due
|
|
72
72
|
// righe sono le meta.
|
|
73
73
|
const sel = windowStart + i + META_ROWS === selected;
|
|
74
|
-
const
|
|
74
|
+
const tail = taskTail(childCount.get(task.id) ?? 0, dirty.has(task.id));
|
|
75
|
+
// T118 — l'id è paddato a `idW`, quindi `head` ha la stessa larghezza
|
|
76
|
+
// su ogni riga e Pri/Prog cadono sempre nella stessa colonna.
|
|
77
|
+
const id = padId(task.id, idW);
|
|
75
78
|
// Invariante ③: la descrizione è l'unico pezzo a lunghezza libera, e
|
|
76
79
|
// si taglia QUI sul budget che resta dopo le colonne fisse. Lasciarlo
|
|
77
80
|
// fare a `truncate-end` significa passare da `cli-truncate`, che
|
|
78
81
|
// restituisce una riga più larga del pane (una colonna per emoji) e
|
|
79
82
|
// quindi scrive sopra il bordo. Le parti fisse si misurano con
|
|
80
|
-
// `termWidth`:
|
|
81
|
-
const head = `${CARET_OFF}${
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
83
|
+
// `termWidth`: i due glifi Pri/Prog valgono 2 ciascuno.
|
|
84
|
+
const head = `${CARET_OFF}${id} ${sanitize(task.pri)} ${displayProg(task.prog)} `;
|
|
85
|
+
// T118 — la colonna della coda si riserva PRIMA di tagliare la
|
|
86
|
+
// descrizione, e per la stessa larghezza su ogni riga: appesa dopo,
|
|
87
|
+
// entrava nel budget solo dove c'era qualcosa da scrivere, e la
|
|
88
|
+
// descrizione si tagliava a una colonna diversa riga per riga.
|
|
89
|
+
//
|
|
90
|
+
// Pavimento `0` su tutte e tre le misure e non un minimo di cortesia:
|
|
91
|
+
// il budget è un TETTO. Un pavimento sopra lo spazio reale fa uscire
|
|
92
|
+
// la riga dal pane e le mangia il bordo. `reserve` si clampa su ciò
|
|
93
|
+
// che avanza, così `head + desc + coda` sta sempre dentro il pane.
|
|
94
|
+
const avail = Math.max(0, paneTextWidth(columns) - termWidth(head));
|
|
95
|
+
const reserve = Math.min(tailW, avail);
|
|
96
|
+
const descW = avail - reserve;
|
|
97
|
+
const desc = cut(task.desc, descW);
|
|
98
|
+
return (_jsxs(Text, { inverse: sel && focused, bold: sel && !focused, dimColor: !sel && isDone(task.prog), wrap: "truncate-end", children: [sel ? CARET : CARET_OFF, id, " ", sanitize(task.pri), " ", displayProg(task.prog), " ", desc, ' '.repeat(Math.max(0, descW - termWidth(desc))), pad(tail, reserve, 'right')] }, task.id));
|
|
85
99
|
}))] }));
|
|
86
100
|
}
|
|
87
101
|
/**
|
package/dist/view.js
CHANGED
|
@@ -59,6 +59,37 @@ export function idNum(id) {
|
|
|
59
59
|
const m = /^T(\d+)$/.exec(id);
|
|
60
60
|
return m ? Number(m[1]) : Number.MAX_SAFE_INTEGER;
|
|
61
61
|
}
|
|
62
|
+
/**
|
|
63
|
+
* T118 — larghezza della colonna id, DERIVATA dalla popolazione.
|
|
64
|
+
*
|
|
65
|
+
* La popolazione è la vista attiva COMPLETA, non la finestra visibile: una
|
|
66
|
+
* larghezza misurata sulle sole righe renderizzate sposta le colonne mentre si
|
|
67
|
+
* scorre, cioè una tabella corretta a ogni frame e illeggibile nel movimento.
|
|
68
|
+
* Il chiamante deve quindi passare `paneTasks`, non l'array che finisce nel
|
|
69
|
+
* pane (`tasks` in `TaskPane` è già la finestra).
|
|
70
|
+
*
|
|
71
|
+
* Gli id sono `T` + cifre (`TASK_ID_RE`), cioè ASCII: `.length` conta colonne.
|
|
72
|
+
*/
|
|
73
|
+
export function idColumnWidth(tasks) {
|
|
74
|
+
let w = 0;
|
|
75
|
+
for (const t of tasks)
|
|
76
|
+
w = Math.max(w, t.id.length);
|
|
77
|
+
return w;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* T118 — id allineato a destra DENTRO il prefisso: `T102` in lista rende `T90`
|
|
81
|
+
* come `T 90`, non come ` T90`.
|
|
82
|
+
*
|
|
83
|
+
* Il prefisso di tipo è l'ancora con cui l'occhio riconosce la colonna:
|
|
84
|
+
* spostarlo riga per riga toglie proprio ciò che l'allineamento dava. Sono le
|
|
85
|
+
* cifre ad allinearsi fra loro, la `T` resta ferma.
|
|
86
|
+
*/
|
|
87
|
+
export function padId(id, cols) {
|
|
88
|
+
const m = /^([A-Za-z]+)(\d+)$/.exec(id);
|
|
89
|
+
if (!m)
|
|
90
|
+
return id.padEnd(cols);
|
|
91
|
+
return m[1] + ' '.repeat(Math.max(0, cols - id.length)) + m[2];
|
|
92
|
+
}
|
|
62
93
|
function rankOf(task, key) {
|
|
63
94
|
if (key === 'pri')
|
|
64
95
|
return priRank(task.pri);
|
package/package.json
CHANGED