@lamemind/loom-deck 0.19.0 → 0.20.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 +88 -40
- package/dist/width.js +96 -0
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
3
|
import { render, Box, Text, useApp, useInput, useStdout } from 'ink';
|
|
4
4
|
import { useState, useEffect, useMemo, useRef } from 'react';
|
|
5
5
|
import { spawn } from 'node:child_process';
|
|
@@ -15,7 +15,7 @@ import { appendNote, appendPin, appendSessionRecord, appendTaskBinding, loadSess
|
|
|
15
15
|
import { assembleSessionList, firstSelectableId, moveSelection, rowIndexOf, rowLabel, selectedSession, stripProjectCore, } from './session-list.js';
|
|
16
16
|
import { launchLegend, loadIdentity, loadLaunch } from './config.js';
|
|
17
17
|
import { isCompact, layoutBudget, readerCapacity, searchListCapacity, searchPreviewCapacity, windowRange, } from './viewport.js';
|
|
18
|
-
import { cut, sanitize, termWidth, wrapLines, wrapWithOffsets, } from './width.js';
|
|
18
|
+
import { caretWindow, cut, sanitize, termWidth, wrapLines, wrapWithOffsets, } from './width.js';
|
|
19
19
|
import { applyView, cycleSort, describeSort, priName, progName, toggleHidden, PRI_ENTRIES, PROG_ENTRIES, } from './view.js';
|
|
20
20
|
import { initialDetail, progressText, writeTaskEdit, PRI_GLYPH, PRI_LABEL, PROG_GLYPH, } from './task-edit.js';
|
|
21
21
|
import { loadView, saveView, viewFilePath } from './view-store.js';
|
|
@@ -35,6 +35,31 @@ const EDIT_ROWS = 4;
|
|
|
35
35
|
function isTextRow(r) {
|
|
36
36
|
return r === 2 || r === 3;
|
|
37
37
|
}
|
|
38
|
+
/** Chiave della bozza scritta dalla riga di testo `r`. */
|
|
39
|
+
function editField(r) {
|
|
40
|
+
return r === 2 ? 'detail' : 'title';
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Lunghezza in CODE POINT. Il caret ci indicizza sopra: `.length` conterebbe le
|
|
44
|
+
* code unit UTF-16 e un'emoji nel titolo varrebbe 2 posizioni, cioè un cursore
|
|
45
|
+
* che si ferma a metà glifo e un `slice` che lo spezza in due surrogati.
|
|
46
|
+
*/
|
|
47
|
+
function cpLen(s) {
|
|
48
|
+
return [...s].length;
|
|
49
|
+
}
|
|
50
|
+
/** Inserisce `ins` alla posizione `at` (code point). */
|
|
51
|
+
function insertAt(s, at, ins) {
|
|
52
|
+
const cp = [...s];
|
|
53
|
+
return cp.slice(0, at).join('') + ins + cp.slice(at).join('');
|
|
54
|
+
}
|
|
55
|
+
/** Toglie il code point in posizione `at`; fuori range = stringa invariata. */
|
|
56
|
+
function removeAt(s, at) {
|
|
57
|
+
const cp = [...s];
|
|
58
|
+
if (at < 0 || at >= cp.length)
|
|
59
|
+
return s;
|
|
60
|
+
cp.splice(at, 1);
|
|
61
|
+
return cp.join('');
|
|
62
|
+
}
|
|
38
63
|
// Modale sort a grammatica libera: un tasto per chiave, pressioni successive
|
|
39
64
|
// ciclano asc → desc → fuori dalla chain.
|
|
40
65
|
const SORT_TASTI = { p: 'pri', s: 'prog', i: 'id' };
|
|
@@ -669,6 +694,9 @@ function Deck({ cwd, tasksPath, tasksDir }) {
|
|
|
669
694
|
prog,
|
|
670
695
|
detail: initialDetail(detail?.fields['Progress'] ?? '', prog),
|
|
671
696
|
title: selTask.rawDesc,
|
|
697
|
+
// Si apre sulla riga 0 (priorità), che non è un campo di testo: il caret
|
|
698
|
+
// prende la sua posizione entrando in una riga di testo con ↑↓.
|
|
699
|
+
caret: 0,
|
|
672
700
|
});
|
|
673
701
|
setEditRow(0);
|
|
674
702
|
setNote('');
|
|
@@ -1026,11 +1054,13 @@ function Deck({ cwd, tasksPath, tasksDir }) {
|
|
|
1026
1054
|
else if (key.return) {
|
|
1027
1055
|
submitEdit();
|
|
1028
1056
|
}
|
|
1029
|
-
else if (key.upArrow) {
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1057
|
+
else if (key.upArrow || key.downArrow) {
|
|
1058
|
+
const next = ((editRow + EDIT_ROWS + (key.upArrow ? -1 : 1)) % EDIT_ROWS);
|
|
1059
|
+
setEditRow(next);
|
|
1060
|
+
// Il caret segue la riga attiva e atterra in CODA al nuovo campo: è la
|
|
1061
|
+
// posizione da cui si continua a scrivere, ed è anche l'unica che non
|
|
1062
|
+
// dipende da dove stava il cursore nel campo precedente.
|
|
1063
|
+
setEdit((e) => (e && isTextRow(next) ? { ...e, caret: cpLen(e[editField(next)]) } : e));
|
|
1034
1064
|
}
|
|
1035
1065
|
else if ((key.leftArrow || key.rightArrow) && !isTextRow(editRow)) {
|
|
1036
1066
|
const d = key.leftArrow ? -1 : 1;
|
|
@@ -1049,16 +1079,48 @@ function Deck({ cwd, tasksPath, tasksDir }) {
|
|
|
1049
1079
|
// Un solo ramo per i due campi di testo, la riga sceglie la chiave:
|
|
1050
1080
|
// duplicarlo significherebbe tenere allineate a mano due copie della
|
|
1051
1081
|
// stessa grammatica di input a ogni tasto aggiunto.
|
|
1052
|
-
const field = editRow
|
|
1053
|
-
if (key.
|
|
1054
|
-
|
|
1082
|
+
const field = editField(editRow);
|
|
1083
|
+
if (key.ctrl) {
|
|
1084
|
+
// T54 — ramo CTRL ANTEPOSTO a quelli su carattere, come in modalità
|
|
1085
|
+
// normale: `^A` e `a` arrivano con lo stesso `input`, quindi senza
|
|
1086
|
+
// questa precedenza il `^A` finirebbe dentro il testo.
|
|
1087
|
+
//
|
|
1088
|
+
// `^A`/`^E` (convenzione readline) perché `Home`/`End` NON sono
|
|
1089
|
+
// esposte da `useInput`: arrivano come input vuoto, indistinguibili
|
|
1090
|
+
// da qualunque altro tasto senza nome.
|
|
1091
|
+
//
|
|
1092
|
+
// `^D` è il delete-forward, e anche qui il motivo è un limite di Ink:
|
|
1093
|
+
// il tasto Backspace fisico manda `\x7f` e il tasto Canc manda
|
|
1094
|
+
// `\x1b[3~`, ma `parseKeypress` li battezza ENTRAMBI `delete` e
|
|
1095
|
+
// svuota `input` — a valle sono lo stesso evento. `key.delete` va
|
|
1096
|
+
// quindi al backspace (il tasto che si usa davvero) e la
|
|
1097
|
+
// cancellazione in avanti prende il suo tasto readline.
|
|
1098
|
+
if (input === 'a')
|
|
1099
|
+
setEdit((e) => (e ? { ...e, caret: 0 } : e));
|
|
1100
|
+
else if (input === 'e')
|
|
1101
|
+
setEdit((e) => (e ? { ...e, caret: cpLen(e[field]) } : e));
|
|
1102
|
+
else if (input === 'd')
|
|
1103
|
+
setEdit((e) => (e ? { ...e, [field]: removeAt(e[field], e.caret) } : e));
|
|
1104
|
+
}
|
|
1105
|
+
else if (key.leftArrow || key.rightArrow) {
|
|
1106
|
+
// CLAMP agli estremi, non wrap: a inizio campo `←` non deve saltare in
|
|
1107
|
+
// fondo. Le liste di valori (righe 0/1) ciclano perché sono 3-4 voci;
|
|
1108
|
+
// un testo no — il salto sarebbe indistinguibile da uno sfarfallio.
|
|
1109
|
+
const d = key.leftArrow ? -1 : 1;
|
|
1110
|
+
setEdit((e) => e ? { ...e, caret: Math.max(0, Math.min(cpLen(e[field]), e.caret + d)) } : e);
|
|
1055
1111
|
}
|
|
1056
|
-
else if (
|
|
1112
|
+
else if (key.backspace || key.delete) {
|
|
1113
|
+
setEdit((e) => e ? { ...e, [field]: removeAt(e[field], e.caret - 1), caret: Math.max(0, e.caret - 1) } : e);
|
|
1114
|
+
}
|
|
1115
|
+
else if (input && !key.meta) {
|
|
1057
1116
|
// `sanitizeTyped`: `useInput` consegna il CHUNK di stdin, quindi un
|
|
1058
1117
|
// incollaggio porta dentro newline e byte di controllo — invisibili
|
|
1059
1118
|
// nel campo ma contati da Ink nella larghezza della riga, e destinati
|
|
1060
|
-
// a finire tali e quali dentro tasks.md.
|
|
1061
|
-
|
|
1119
|
+
// a finire tali e quali dentro tasks.md. Ed è per lo stesso motivo che
|
|
1120
|
+
// il caret avanza della LUNGHEZZA del chunk, non di uno: un incollaggio
|
|
1121
|
+
// entra tutto insieme.
|
|
1122
|
+
const ins = sanitizeTyped(input);
|
|
1123
|
+
setEdit((e) => e ? { ...e, [field]: insertAt(e[field], e.caret, ins), caret: e.caret + cpLen(ins) } : e);
|
|
1062
1124
|
}
|
|
1063
1125
|
}
|
|
1064
1126
|
return;
|
|
@@ -1345,7 +1407,7 @@ function Deck({ cwd, tasksPath, tasksDir }) {
|
|
|
1345
1407
|
if (budget.compact) {
|
|
1346
1408
|
return (_jsxs(Text, { wrap: "truncate-end", children: [_jsx(Text, { bold: true, color: "cyan", children: "loom-deck" }), _jsxs(Text, { dimColor: true, children: [' ', "\u00B7 ", viewTasks.length, " task \u00B7 sel ", selectedTaskId ?? 'spot', " \u00B7 terminale ", rows, "\u00D7", columns, ": troppo basso, allarga"] })] }));
|
|
1347
1409
|
}
|
|
1348
|
-
return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1, children: [_jsx(Text, { bold: true, color: "cyan", children: "loom-deck" }), 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 === '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 \u00B7", ' ', _jsx(Text, { color: "yellow", children: "
|
|
1410
|
+
return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1, children: [_jsx(Text, { bold: true, color: "cyan", children: "loom-deck" }), 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 === '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"] })) : (_jsxs(Text, { dimColor: true, wrap: "truncate-end", children: ["\u2191\u2193 naviga \u00B7 \u2190\u2192 pane \u00B7 \u23CE ", canSpawn ? 'spawn' : canResume ? 'resume' : '—', canResume ? ' · f fork' : '', canPin ? ' · p pin · N nota' : '', " \u00B7 ^F cerca \u00B7 C nuova \u00B7 E edit \u00B7 S sort \u00B7 F filtri \u00B7 w salva \u00B7 t term \u00B7 c claude \u00B7 q esci \u00B7 focus:", ' ', _jsx(Text, { color: "cyan", children: focus })] })), mode === 'normal' && launch.length > 0 ? (_jsxs(Text, { dimColor: true, wrap: "truncate-end", children: ["launch ", 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, _jsxs(Box, { flexDirection: "row", marginTop: 1, children: [_jsx(TasksPane, { tasks: windowTasks, filtered: viewTasks.length, total: tasks.length, hidden: hiddenTasks, view: view, selected: selIndex, spotCount: spotCount, childCount: childCount, focused: focus === 'tasks', loadError: loadError, detail: detail, windowStart: taskWin.start, above: taskWin.start, below: viewTasks.length - taskWin.end, detailLines: budget.detailLines, columns: columns }), _jsx(SessionsPane, { parentLabel: parentLabel, isSpot: isSpot, rows: windowRows, total: assembled.pinnedCount + assembled.contextTotal, pinnedCount: assembled.pinnedCount, hidden: assembled.contextHidden, selectedId: selSessionId ?? undefined, focused: focus === 'sessions', above: sessionWin.start, below: sessionRows.length - sessionWin.end, detail: budget.sessionDetail ? selSessionObj : null, firstLines: budget.sessionFirstLines, lastLines: budget.sessionLastLines, columns: columns, forkOf: forkOf, sessionNotes: sessionNotes, projectCore: projectCore })] }), note ? _jsx(Text, { color: "green", wrap: "truncate-end", children: sanitize(note) }) : null] }));
|
|
1349
1411
|
}
|
|
1350
1412
|
const SORT_UI = { pri: 'pri', prog: 'stato', id: 'id' };
|
|
1351
1413
|
// Modali resi IN FLUSSO (come l'input box di create), non in overlay assoluto:
|
|
@@ -1368,31 +1430,17 @@ function FilterModal({ view, cursor }) {
|
|
|
1368
1430
|
})] }, row.label)))] }));
|
|
1369
1431
|
}
|
|
1370
1432
|
/**
|
|
1371
|
-
*
|
|
1433
|
+
* Campo di testo del modale edit: finestra ancorata al caret + cursore inverso
|
|
1434
|
+
* nella posizione REALE.
|
|
1372
1435
|
*
|
|
1373
|
-
*
|
|
1374
|
-
*
|
|
1375
|
-
*
|
|
1376
|
-
*
|
|
1377
|
-
* lo si sta scrivendo. Budget in COLONNE (`termWidth`), non in code unit: è
|
|
1378
|
-
* quello che decide se la riga sfonda il bordo del box.
|
|
1436
|
+
* Il cursore non è più uno spazio inverso appiccicato in coda ma la cella `at`
|
|
1437
|
+
* della finestra — cioè il carattere su cui il caret sta davvero. Fuori fuoco
|
|
1438
|
+
* (`focused` falso) il caret non si disegna e la finestra si ancora in fondo,
|
|
1439
|
+
* che è la vista utile per un campo che non si sta scrivendo.
|
|
1379
1440
|
*/
|
|
1380
|
-
function
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
if (termWidth(s) <= cols)
|
|
1384
|
-
return s;
|
|
1385
|
-
const chars = [...s];
|
|
1386
|
-
let out = '';
|
|
1387
|
-
let w = 1; // la colonna dell'ellissi di testa
|
|
1388
|
-
for (let i = chars.length - 1; i >= 0; i--) {
|
|
1389
|
-
const cw = termWidth(chars[i]);
|
|
1390
|
-
if (w + cw > cols)
|
|
1391
|
-
break;
|
|
1392
|
-
out = chars[i] + out;
|
|
1393
|
-
w += cw;
|
|
1394
|
-
}
|
|
1395
|
-
return `…${out}`;
|
|
1441
|
+
function EditTextField({ label, value, caret, focused, cols, }) {
|
|
1442
|
+
const win = caretWindow(value, focused ? caret : cpLen(value), cols);
|
|
1443
|
+
return (_jsxs(_Fragment, { children: [_jsx(Text, { dimColor: true, children: label }), _jsxs(Text, { children: [' ', sanitize(win.head)] }), focused ? _jsx(Text, { inverse: true, children: sanitize(win.at) }) : null, _jsx(Text, { children: sanitize(win.tail) })] }));
|
|
1396
1444
|
}
|
|
1397
1445
|
// T41 — modale edit, in flusso come gli altri (spinge giù i pane invece di
|
|
1398
1446
|
// coprirli: la riga che stai modificando resta visibile sopra la lista).
|
|
@@ -1400,14 +1448,14 @@ function tailCut(s, cols) {
|
|
|
1400
1448
|
// del task file — così il default (`✔️ Done at <oggi>`) non è una sorpresa.
|
|
1401
1449
|
function EditModal({ id, draft, row, columns, }) {
|
|
1402
1450
|
const mark = (r) => (row === r ? CARET : CARET_OFF);
|
|
1403
|
-
// Budget
|
|
1451
|
+
// Budget dei campi di testo, DERIVATO da `columns` (mai una costante): il box
|
|
1404
1452
|
// del modale è ANNIDATO nella cornice del deck, quindi le cornici da scalare
|
|
1405
1453
|
// sono due — root (bordo 2 + paddingX 2) e modale (bordo 2 + paddingX 2) — più
|
|
1406
1454
|
// caret 2, etichetta 6, gap 2 e cursore 1. Totale 19.
|
|
1407
1455
|
// Un titolo di tasks.md arriva a ~64 caratteri: senza taglio la riga va a capo
|
|
1408
1456
|
// dentro il box, che si alza di una riga e sfonda il budget verticale (invariante ③).
|
|
1409
|
-
const
|
|
1410
|
-
return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "yellow", paddingX: 1, marginTop: 1, children: [_jsxs(Text, { color: "yellow", children: ["E \u203A ", id, " \u00B7 titolo, priorit\u00E0 e stato"] }), _jsxs(Text, { children: [mark(0), _jsx(Text, { dimColor: true, children: "pri " }), EDIT_PRI.map((p) => (_jsxs(Text, { inverse: draft.pri === p, color: draft.pri === p ? 'green' : 'gray', children: [' ', sanitize(PRI_GLYPH[p]), " ", PRI_LABEL[p]] }, p)))] }), _jsxs(Text, { children: [mark(1), _jsx(Text, { dimColor: true, children: "stato " }), EDIT_PROG.map((p) => (_jsxs(Text, { inverse: draft.prog === p, color: draft.prog === p ? 'green' : 'gray', children: [' ', sanitize(PROG_GLYPH[p]), " ", p] }, p)))] }), _jsxs(Text, { children: [mark(2), _jsx(
|
|
1457
|
+
const fieldBudget = Math.max(8, columns - 19);
|
|
1458
|
+
return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "yellow", paddingX: 1, marginTop: 1, children: [_jsxs(Text, { color: "yellow", children: ["E \u203A ", id, " \u00B7 titolo, priorit\u00E0 e stato"] }), _jsxs(Text, { children: [mark(0), _jsx(Text, { dimColor: true, children: "pri " }), EDIT_PRI.map((p) => (_jsxs(Text, { inverse: draft.pri === p, color: draft.pri === p ? 'green' : 'gray', children: [' ', sanitize(PRI_GLYPH[p]), " ", PRI_LABEL[p]] }, p)))] }), _jsxs(Text, { children: [mark(1), _jsx(Text, { dimColor: true, children: "stato " }), EDIT_PROG.map((p) => (_jsxs(Text, { inverse: draft.prog === p, color: draft.prog === p ? 'green' : 'gray', children: [' ', sanitize(PROG_GLYPH[p]), " ", p] }, p)))] }), _jsxs(Text, { children: [mark(2), _jsx(EditTextField, { label: "prog ", value: draft.detail, caret: draft.caret, focused: row === 2, cols: fieldBudget }), !draft.detail && row !== 2 ? _jsx(Text, { dimColor: true, children: "(default)" }) : null] }), _jsxs(Text, { children: [mark(3), _jsx(EditTextField, { label: "titolo", value: draft.title, caret: draft.caret, focused: row === 3, cols: fieldBudget })] }), _jsxs(Text, { dimColor: true, wrap: "truncate-end", children: ["\u21B3 ", sanitize(progressText(draft.prog, draft.detail))] })] }));
|
|
1411
1459
|
}
|
|
1412
1460
|
// T52 — marcatore compatto del tipo di corpo sulla riga-occorrenza. Due
|
|
1413
1461
|
// caratteri ASCII e non un'emoji: con più toggle accesi la colonna deve
|
package/dist/width.js
CHANGED
|
@@ -202,6 +202,102 @@ export function cut(s, cols) {
|
|
|
202
202
|
// informazione, tolgono una colonna di testo.
|
|
203
203
|
return out.trimEnd().replace(/…$/, '') + '…';
|
|
204
204
|
}
|
|
205
|
+
/**
|
|
206
|
+
* Larghezza in colonne di UN carattere **dopo** `sanitize`.
|
|
207
|
+
*
|
|
208
|
+
* Misurare il carattere grezzo significa misurare qualcosa che nel frame non
|
|
209
|
+
* entra mai: la sanificazione può cambiarne la larghezza (`✔`, largo 1, diventa
|
|
210
|
+
* `✅`, largo 2 — invariante ①). Chi taglia su un budget deve contare le colonne
|
|
211
|
+
* che verranno DISEGNATE, altrimenti sbaglia di una colonna per ogni glifo
|
|
212
|
+
* sostituito. La cache di `fixChar` rende la doppia chiamata gratuita.
|
|
213
|
+
*/
|
|
214
|
+
export function cellWidth(ch) {
|
|
215
|
+
return termWidth(sanitize(ch));
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Porzione visibile di un campo di testo con un caret MOBILE, larga al più
|
|
219
|
+
* `cols` colonne.
|
|
220
|
+
*
|
|
221
|
+
* Perché non basta un taglio dalla coda: mostrare sempre le ultime `cols`
|
|
222
|
+
* colonne è corretto solo finché si scrive in fondo. Con un caret che si muove,
|
|
223
|
+
* appena rientra a sinistra del bordo visibile si finirebbe per digitare in un
|
|
224
|
+
* punto che non si vede — il testo deve scorrere DIETRO il cursore, non
|
|
225
|
+
* viceversa.
|
|
226
|
+
*
|
|
227
|
+
* Il caret è un indice per CODE POINT (`[...s]`), non per code unit: un'emoji
|
|
228
|
+
* nel titolo sono due code unit e muoversi per unità la spezzerebbe a metà.
|
|
229
|
+
*
|
|
230
|
+
* La finestra si espande ALTERNANDO i due lati, così il cursore resta al centro
|
|
231
|
+
* finché il testo lo consente: muoversi di un carattere fa scorrere il testo di
|
|
232
|
+
* uno, invece di far saltare la vista da un bordo all'altro. Le ellissi entrano
|
|
233
|
+
* nel budget e ne escono da sole — quando un lato tocca il bordo del testo la
|
|
234
|
+
* sua ellissi non serve più e la colonna liberata va all'altro lato.
|
|
235
|
+
*
|
|
236
|
+
* Restituisce i tre pezzi già pronti al render (non la sola stringa visibile):
|
|
237
|
+
* il cursore si disegna invertendo `at`, e ricavarlo tagliando `visible` a
|
|
238
|
+
* `cursorCol` vorrebbe dire rifare qui fuori lo stesso conteggio in colonne.
|
|
239
|
+
*/
|
|
240
|
+
export function caretWindow(text, caret, cols) {
|
|
241
|
+
const empty = { head: '', at: '', tail: '', cursorCol: 0 };
|
|
242
|
+
if (cols <= 0)
|
|
243
|
+
return empty;
|
|
244
|
+
const chars = [...text];
|
|
245
|
+
const c = Math.max(0, Math.min(caret, chars.length));
|
|
246
|
+
// Il caret a fine campo non ha un carattere sotto di sé: gliene si dà uno
|
|
247
|
+
// virtuale, così il cursore ha sempre una cella da invertire e la finestra ha
|
|
248
|
+
// sempre un centro. È lo spazio inverso che il modale disegnava in coda.
|
|
249
|
+
const cells = c === chars.length ? [...chars, ' '] : chars;
|
|
250
|
+
const len = cells.length;
|
|
251
|
+
const w = cells.map(cellWidth);
|
|
252
|
+
const cost = (s, e, tw) => tw + (s > 0 ? 1 : 0) + (e < len ? 1 : 0);
|
|
253
|
+
let start = c;
|
|
254
|
+
let end = c + 1;
|
|
255
|
+
let textW = w[c];
|
|
256
|
+
// Budget più stretto della sola cella del cursore: non c'è finestra da dare.
|
|
257
|
+
if (cost(start, end, textW) > cols)
|
|
258
|
+
return empty;
|
|
259
|
+
let goRight = true;
|
|
260
|
+
let stuckL = false;
|
|
261
|
+
let stuckR = false;
|
|
262
|
+
while (!stuckL || !stuckR) {
|
|
263
|
+
const canR = end < len && !stuckR;
|
|
264
|
+
const canL = start > 0 && !stuckL;
|
|
265
|
+
if (!canR && !canL)
|
|
266
|
+
break;
|
|
267
|
+
if (canR && (goRight || !canL)) {
|
|
268
|
+
const nw = textW + w[end];
|
|
269
|
+
if (cost(start, end + 1, nw) > cols)
|
|
270
|
+
stuckR = true;
|
|
271
|
+
else {
|
|
272
|
+
textW = nw;
|
|
273
|
+
end++;
|
|
274
|
+
// Arrivare in fondo toglie l'ellissi di coda: la colonna che si libera
|
|
275
|
+
// può rimettere in gioco il lato che si era fermato per un pelo.
|
|
276
|
+
if (end === len)
|
|
277
|
+
stuckL = false;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
else {
|
|
281
|
+
const nw = textW + w[start - 1];
|
|
282
|
+
if (cost(start - 1, end, nw) > cols)
|
|
283
|
+
stuckL = true;
|
|
284
|
+
else {
|
|
285
|
+
textW = nw;
|
|
286
|
+
start--;
|
|
287
|
+
if (start === 0)
|
|
288
|
+
stuckR = false;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
goRight = !goRight;
|
|
292
|
+
}
|
|
293
|
+
const head = (start > 0 ? '…' : '') + cells.slice(start, c).join('');
|
|
294
|
+
return {
|
|
295
|
+
head,
|
|
296
|
+
at: cells[c],
|
|
297
|
+
tail: cells.slice(c + 1, end).join('') + (end < len ? '…' : ''),
|
|
298
|
+
cursorCol: termWidth(sanitize(head)),
|
|
299
|
+
};
|
|
300
|
+
}
|
|
205
301
|
/**
|
|
206
302
|
* Indice sorgente raggiunto consumando al più `cols` colonne da `from`.
|
|
207
303
|
*
|
package/package.json
CHANGED