@profullstack/readm3 0.19.0 → 0.24.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/README.md +59 -6
- package/dist/blob-store.d.ts +6 -1
- package/dist/blob-store.d.ts.map +1 -1
- package/dist/blob-store.js +6 -1
- package/dist/blob-store.js.map +1 -1
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +1 -1
- package/dist/code.d.ts.map +1 -1
- package/dist/code.js +6 -1
- package/dist/code.js.map +1 -1
- package/dist/csv.d.ts +174 -0
- package/dist/csv.d.ts.map +1 -0
- package/dist/csv.js +489 -0
- package/dist/csv.js.map +1 -0
- package/dist/mcp.d.ts.map +1 -1
- package/dist/mcp.js +48 -2
- package/dist/mcp.js.map +1 -1
- package/dist/print.d.ts.map +1 -1
- package/dist/print.js +4 -1
- package/dist/print.js.map +1 -1
- package/dist/sync-schema.d.ts.map +1 -1
- package/dist/sync-schema.js +3 -3
- package/dist/sync-schema.js.map +1 -1
- package/dist/tree.d.ts +3 -1
- package/dist/tree.d.ts.map +1 -1
- package/dist/tree.js +8 -5
- package/dist/tree.js.map +1 -1
- package/dist/viewer.d.ts.map +1 -1
- package/dist/viewer.js +346 -56
- package/dist/viewer.js.map +1 -1
- package/package.json +3 -2
package/dist/viewer.js
CHANGED
|
@@ -5,11 +5,16 @@
|
|
|
5
5
|
* `markdown.ts`; editing shows the same buffer raw, with a caret. Both read the
|
|
6
6
|
* same text, so leaving edit mode is all it takes to see the change rendered —
|
|
7
7
|
* there is no separate preview to keep in step.
|
|
8
|
+
*
|
|
9
|
+
* A CSV or TSV reads as a grid instead (src/csv.ts parses it, hqtui's data grid
|
|
10
|
+
* draws it): a cell cursor, sort by column, a row filter, a row sheet and column
|
|
11
|
+
* statistics, the way tabiew does it. Editing it is the same raw editor.
|
|
8
12
|
*/
|
|
9
13
|
import { readFileSync, statSync, writeFileSync } from "node:fs";
|
|
10
14
|
import { basename, dirname, resolve } from "node:path";
|
|
11
15
|
import { createApp, stringWidth } from "@profullstack/hqtui";
|
|
12
16
|
import { backspace, breakRun, createEditor, del, indent, insert, killToLineEnd, killToLineStart, markSaved, moveDocEnd, moveDocStart, moveDown, moveEnd, moveHome, moveLeft, moveRight, moveUp, moveVertical, moveWordLeft, moveWordRight, newline, redo, text as bufferText, undo, } from "./editor.js";
|
|
17
|
+
import { alignOf, columnStats, delimiterName, displayCell, isTableName, nextSort, parseTable, statsRows, viewRows, } from "./csv.js";
|
|
13
18
|
import { DEFAULT_FLAVOR } from "./flavors.js";
|
|
14
19
|
import { renderMarkdown } from "./markdown.js";
|
|
15
20
|
import { colorOf } from "./roles.js";
|
|
@@ -50,6 +55,24 @@ export async function run(options) {
|
|
|
50
55
|
let scroll = 0;
|
|
51
56
|
/** Bumped on every edit, so the rendered document is rebuilt exactly then. */
|
|
52
57
|
let stamp = 0;
|
|
58
|
+
// ---- the table view, for a CSV or TSV
|
|
59
|
+
/** The open file parsed, rebuilt when the text changes. */
|
|
60
|
+
let grid = null;
|
|
61
|
+
/** Row indices shown: filtered, then sorted. */
|
|
62
|
+
let gridRows = [];
|
|
63
|
+
let gridKey = "";
|
|
64
|
+
let rowFilter = "";
|
|
65
|
+
let rowFiltering = false;
|
|
66
|
+
let sort = null;
|
|
67
|
+
let cursor = { row: 0, col: 0 };
|
|
68
|
+
let gridOffset = { row: 0, col: 0 };
|
|
69
|
+
/** Body rows that fit, as the grid last measured them; page keys move by this. */
|
|
70
|
+
let gridPage = 10;
|
|
71
|
+
/** Show the table's text rather than the grid. */
|
|
72
|
+
let rawTable = false;
|
|
73
|
+
/** The cursor row as a sheet, or the cursor column's statistics. */
|
|
74
|
+
let sheet = false;
|
|
75
|
+
let stats = false;
|
|
53
76
|
/** A yes/no the next keypress answers. */
|
|
54
77
|
let pending = null;
|
|
55
78
|
let sidebar = options.sidebar ?? 0;
|
|
@@ -83,6 +106,14 @@ export async function run(options) {
|
|
|
83
106
|
buffer = createEditor(readFileSync(path, "utf8"));
|
|
84
107
|
openPath = path;
|
|
85
108
|
doc = null;
|
|
109
|
+
grid = null;
|
|
110
|
+
gridKey = "";
|
|
111
|
+
rowFilter = "";
|
|
112
|
+
rowFiltering = false;
|
|
113
|
+
sort = null;
|
|
114
|
+
cursor = { row: 0, col: 0 };
|
|
115
|
+
gridOffset = { row: 0, col: 0 };
|
|
116
|
+
rawTable = false;
|
|
86
117
|
scroll = 0;
|
|
87
118
|
editTop = 0;
|
|
88
119
|
editLeft = 0;
|
|
@@ -125,6 +156,105 @@ export async function run(options) {
|
|
|
125
156
|
breakRun(buffer);
|
|
126
157
|
};
|
|
127
158
|
const maxScroll = () => Math.max(0, (doc?.lines.length ?? 0) - viewport.h);
|
|
159
|
+
/** Whether the open file is drawn as a grid right now. */
|
|
160
|
+
const tableMode = () => !!openPath && isTableName(openPath) && !rawTable && mode === "read";
|
|
161
|
+
const currentTable = () => {
|
|
162
|
+
if (!openPath || !buffer || !isTableName(openPath))
|
|
163
|
+
return null;
|
|
164
|
+
if (!grid || grid.stamp !== stamp || grid.path !== openPath) {
|
|
165
|
+
grid = { stamp, path: openPath, table: parseTable(bufferText(buffer), { name: openPath }) };
|
|
166
|
+
gridKey = "";
|
|
167
|
+
}
|
|
168
|
+
return grid.table;
|
|
169
|
+
};
|
|
170
|
+
/** The rows shown, recomputed only when the text, the filter or the sort changed. */
|
|
171
|
+
const shownRows = () => {
|
|
172
|
+
const table = currentTable();
|
|
173
|
+
if (!table)
|
|
174
|
+
return [];
|
|
175
|
+
const key = JSON.stringify([grid?.stamp, openPath, rowFilter, sort]);
|
|
176
|
+
if (key !== gridKey) {
|
|
177
|
+
gridRows = viewRows(table, { filter: rowFilter, sort });
|
|
178
|
+
gridKey = key;
|
|
179
|
+
}
|
|
180
|
+
cursor.row = clamp(cursor.row, 0, Math.max(0, gridRows.length - 1));
|
|
181
|
+
cursor.col = clamp(cursor.col, 0, Math.max(0, table.columns.length - 1));
|
|
182
|
+
return gridRows;
|
|
183
|
+
};
|
|
184
|
+
/** The table's text inside a fence, so `v` shows it raw through the same renderer. */
|
|
185
|
+
const fenced = (text) => {
|
|
186
|
+
const longest = Math.max(2, ...(text.match(/`+/g) ?? []).map((run) => run.length));
|
|
187
|
+
const fence = "`".repeat(longest + 1);
|
|
188
|
+
return `${fence}csv\n${text}${text.endsWith("\n") ? "" : "\n"}${fence}\n`;
|
|
189
|
+
};
|
|
190
|
+
/** Keys while the grid has focus. */
|
|
191
|
+
const tableKey = (key) => {
|
|
192
|
+
const rows = shownRows();
|
|
193
|
+
const table = currentTable();
|
|
194
|
+
if (!table)
|
|
195
|
+
return;
|
|
196
|
+
const last = Math.max(0, rows.length - 1);
|
|
197
|
+
const page = Math.max(1, gridPage - 1);
|
|
198
|
+
switch (key) {
|
|
199
|
+
case "up":
|
|
200
|
+
case "k":
|
|
201
|
+
cursor.row = clamp(cursor.row - 1, 0, last);
|
|
202
|
+
return;
|
|
203
|
+
case "down":
|
|
204
|
+
case "j":
|
|
205
|
+
cursor.row = clamp(cursor.row + 1, 0, last);
|
|
206
|
+
return;
|
|
207
|
+
case "pageup":
|
|
208
|
+
case "b":
|
|
209
|
+
cursor.row = clamp(cursor.row - page, 0, last);
|
|
210
|
+
return;
|
|
211
|
+
case "pagedown":
|
|
212
|
+
case "space":
|
|
213
|
+
cursor.row = clamp(cursor.row + page, 0, last);
|
|
214
|
+
return;
|
|
215
|
+
case "home":
|
|
216
|
+
case "g":
|
|
217
|
+
cursor.row = 0;
|
|
218
|
+
return;
|
|
219
|
+
case "end":
|
|
220
|
+
case "G":
|
|
221
|
+
case "shift+g":
|
|
222
|
+
cursor.row = last;
|
|
223
|
+
return;
|
|
224
|
+
case "right":
|
|
225
|
+
case "l":
|
|
226
|
+
cursor.col = clamp(cursor.col + 1, 0, table.columns.length - 1);
|
|
227
|
+
return;
|
|
228
|
+
case "left":
|
|
229
|
+
case "h":
|
|
230
|
+
if (cursor.col === 0)
|
|
231
|
+
pane = "tree";
|
|
232
|
+
else
|
|
233
|
+
cursor.col--;
|
|
234
|
+
return;
|
|
235
|
+
case "0":
|
|
236
|
+
case "^":
|
|
237
|
+
cursor.col = 0;
|
|
238
|
+
return;
|
|
239
|
+
case "$":
|
|
240
|
+
cursor.col = table.columns.length - 1;
|
|
241
|
+
return;
|
|
242
|
+
case "o":
|
|
243
|
+
// A new order is read from its top, as in a spreadsheet.
|
|
244
|
+
sort = nextSort(sort, cursor.col);
|
|
245
|
+
cursor.row = 0;
|
|
246
|
+
return;
|
|
247
|
+
case "enter":
|
|
248
|
+
if (rows.length)
|
|
249
|
+
sheet = true;
|
|
250
|
+
return;
|
|
251
|
+
case "c":
|
|
252
|
+
stats = true;
|
|
253
|
+
return;
|
|
254
|
+
default:
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
};
|
|
128
258
|
const moveSelection = (delta) => {
|
|
129
259
|
if (flat.length === 0)
|
|
130
260
|
return;
|
|
@@ -277,8 +407,10 @@ export async function run(options) {
|
|
|
277
407
|
app.invalidate();
|
|
278
408
|
return;
|
|
279
409
|
}
|
|
280
|
-
if (help) {
|
|
410
|
+
if (help || sheet || stats) {
|
|
281
411
|
help = false;
|
|
412
|
+
sheet = false;
|
|
413
|
+
stats = false;
|
|
282
414
|
app.invalidate();
|
|
283
415
|
return;
|
|
284
416
|
}
|
|
@@ -287,6 +419,27 @@ export async function run(options) {
|
|
|
287
419
|
app.invalidate();
|
|
288
420
|
return;
|
|
289
421
|
}
|
|
422
|
+
if (rowFiltering) {
|
|
423
|
+
if (key === "escape") {
|
|
424
|
+
rowFiltering = false;
|
|
425
|
+
rowFilter = "";
|
|
426
|
+
}
|
|
427
|
+
else if (name === "enter") {
|
|
428
|
+
rowFiltering = false;
|
|
429
|
+
}
|
|
430
|
+
else if (name === "backspace") {
|
|
431
|
+
rowFilter = rowFilter.slice(0, -1);
|
|
432
|
+
}
|
|
433
|
+
else if (name === "up" || name === "down") {
|
|
434
|
+
tableKey(name);
|
|
435
|
+
}
|
|
436
|
+
else if (char && !event.ctrl && !event.alt) {
|
|
437
|
+
rowFilter += char;
|
|
438
|
+
cursor.row = 0;
|
|
439
|
+
}
|
|
440
|
+
app.invalidate();
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
290
443
|
if (filtering) {
|
|
291
444
|
if (key === "escape") {
|
|
292
445
|
filtering = false;
|
|
@@ -358,8 +511,21 @@ export async function run(options) {
|
|
|
358
511
|
return;
|
|
359
512
|
}
|
|
360
513
|
case "/":
|
|
361
|
-
|
|
362
|
-
pane
|
|
514
|
+
// Over a grid, `/` filters its rows; anywhere else, the file list.
|
|
515
|
+
if (pane === "view" && tableMode())
|
|
516
|
+
rowFiltering = true;
|
|
517
|
+
else {
|
|
518
|
+
filtering = true;
|
|
519
|
+
pane = "tree";
|
|
520
|
+
}
|
|
521
|
+
app.invalidate();
|
|
522
|
+
return;
|
|
523
|
+
case "v":
|
|
524
|
+
if (openPath && isTableName(openPath)) {
|
|
525
|
+
rawTable = !rawTable;
|
|
526
|
+
doc = null;
|
|
527
|
+
pane = "view";
|
|
528
|
+
}
|
|
363
529
|
app.invalidate();
|
|
364
530
|
return;
|
|
365
531
|
case "[":
|
|
@@ -371,7 +537,10 @@ export async function run(options) {
|
|
|
371
537
|
app.invalidate();
|
|
372
538
|
return;
|
|
373
539
|
case "escape":
|
|
374
|
-
if (
|
|
540
|
+
if (pane === "view" && tableMode() && rowFilter) {
|
|
541
|
+
rowFilter = "";
|
|
542
|
+
}
|
|
543
|
+
else if (filter) {
|
|
375
544
|
filter = "";
|
|
376
545
|
rebuild(openPath);
|
|
377
546
|
}
|
|
@@ -437,6 +606,11 @@ export async function run(options) {
|
|
|
437
606
|
app.invalidate();
|
|
438
607
|
return;
|
|
439
608
|
}
|
|
609
|
+
if (tableMode()) {
|
|
610
|
+
tableKey(key);
|
|
611
|
+
app.invalidate();
|
|
612
|
+
return;
|
|
613
|
+
}
|
|
440
614
|
const page = Math.max(1, viewport.h - 2);
|
|
441
615
|
switch (key) {
|
|
442
616
|
case "up":
|
|
@@ -480,6 +654,9 @@ export async function run(options) {
|
|
|
480
654
|
const openRel = openPath ? pathLabel(root, openPath) : "";
|
|
481
655
|
const total = doc?.lines.length ?? 0;
|
|
482
656
|
const percent = total <= viewport.h ? 100 : Math.round((Math.min(scroll + viewport.h, total) / total) * 100);
|
|
657
|
+
const gridding = tableMode();
|
|
658
|
+
const table = gridding ? currentTable() : null;
|
|
659
|
+
const rows = gridding ? shownRows() : [];
|
|
483
660
|
ui.column({}, (screen) => {
|
|
484
661
|
screen.row({ size: "fill" }, (panes) => {
|
|
485
662
|
// ------------------------------------------------------- browser
|
|
@@ -499,7 +676,7 @@ export async function run(options) {
|
|
|
499
676
|
treeOffset = selected - capacity + 1;
|
|
500
677
|
treeOffset = clamp(treeOffset, 0, Math.max(0, flat.length - capacity));
|
|
501
678
|
if (flat.length === 0) {
|
|
502
|
-
p.text(filter ? "No match." : "No markdown here.", { fg: theme.muted });
|
|
679
|
+
p.text(filter ? "No match." : "No markdown or CSV here.", { fg: theme.muted });
|
|
503
680
|
return;
|
|
504
681
|
}
|
|
505
682
|
p.list({
|
|
@@ -540,12 +717,22 @@ export async function run(options) {
|
|
|
540
717
|
panes.panel({
|
|
541
718
|
title: openRel ? ` ${dirty() ? "● " : ""}${openRel} ` : " readm3 ",
|
|
542
719
|
titleColor: editing ? theme.warning : theme.title,
|
|
543
|
-
subtitle: openPath
|
|
720
|
+
subtitle: openPath
|
|
721
|
+
? editing
|
|
722
|
+
? "EDIT"
|
|
723
|
+
: table
|
|
724
|
+
? `${rows.length === table.rows.length ? "" : `${rows.length.toLocaleString()} of `}${table.rows.length.toLocaleString()} rows · ${table.columns.length} cols · ${delimiterName(table.delimiter)}`
|
|
725
|
+
: `${percent}%`
|
|
726
|
+
: undefined,
|
|
544
727
|
footer: openPath && editing && buffer
|
|
545
728
|
? `${buffer.row + 1}:${buffer.col + 1}`
|
|
546
|
-
:
|
|
547
|
-
?
|
|
548
|
-
|
|
729
|
+
: table
|
|
730
|
+
? rows.length
|
|
731
|
+
? `${cursor.row + 1}/${rows.length} · ${table.columns[cursor.col]?.name ?? ""}`
|
|
732
|
+
: "no rows"
|
|
733
|
+
: openPath && total > 0
|
|
734
|
+
? `${Math.min(scroll + viewport.h, total)}/${total}`
|
|
735
|
+
: undefined,
|
|
549
736
|
borderColor: editing ? theme.warning : pane === "view" ? theme.borderFocused : theme.border,
|
|
550
737
|
padding: [0, 1],
|
|
551
738
|
}, (p) => {
|
|
@@ -581,13 +768,62 @@ export async function run(options) {
|
|
|
581
768
|
drawEditor(p, buffer, theme, w, h);
|
|
582
769
|
return;
|
|
583
770
|
}
|
|
771
|
+
if (table) {
|
|
772
|
+
if (rowFiltering || rowFilter) {
|
|
773
|
+
p.row({ height: 1 }, (r) => {
|
|
774
|
+
r.text(`/${rowFilter}${rowFiltering ? "▏" : ""}`, { width: stringWidth(rowFilter) + 2, fg: theme.warning });
|
|
775
|
+
r.text(rowFiltering ? " col:text col>n col=v !term" : " esc clears", { fg: theme.muted });
|
|
776
|
+
});
|
|
777
|
+
}
|
|
778
|
+
if (table.columns.length === 0) {
|
|
779
|
+
p.text("This file has no rows.", { fg: theme.muted });
|
|
780
|
+
return;
|
|
781
|
+
}
|
|
782
|
+
p.dataGrid({
|
|
783
|
+
columns: table.columns.map((column) => ({ title: column.name, align: alignOf(column.type) })),
|
|
784
|
+
rows: table.rows,
|
|
785
|
+
order: rows,
|
|
786
|
+
selectedRow: rows.length ? cursor.row : -1,
|
|
787
|
+
selectedColumn: cursor.col,
|
|
788
|
+
rowOffset: gridOffset.row,
|
|
789
|
+
columnOffset: gridOffset.col,
|
|
790
|
+
sort,
|
|
791
|
+
zebra: true,
|
|
792
|
+
scrollbar: true,
|
|
793
|
+
onLayout: (frame) => {
|
|
794
|
+
gridOffset = { row: frame.rowOffset, col: frame.columnOffset };
|
|
795
|
+
gridPage = Math.max(1, frame.capacity);
|
|
796
|
+
},
|
|
797
|
+
onScroll: (delta) => {
|
|
798
|
+
cursor.row = clamp(cursor.row + delta * 3, 0, Math.max(0, rows.length - 1));
|
|
799
|
+
pane = "view";
|
|
800
|
+
app.invalidate();
|
|
801
|
+
},
|
|
802
|
+
onSelectCell: (row, column) => {
|
|
803
|
+
cursor = { row, col: column >= 0 ? column : cursor.col };
|
|
804
|
+
pane = "view";
|
|
805
|
+
app.invalidate();
|
|
806
|
+
},
|
|
807
|
+
onActivateCell: () => {
|
|
808
|
+
sheet = true;
|
|
809
|
+
app.invalidate();
|
|
810
|
+
},
|
|
811
|
+
onHeaderClick: (column) => {
|
|
812
|
+
cursor.col = column;
|
|
813
|
+
tableKey("o");
|
|
814
|
+
pane = "view";
|
|
815
|
+
app.invalidate();
|
|
816
|
+
},
|
|
817
|
+
});
|
|
818
|
+
return;
|
|
819
|
+
}
|
|
584
820
|
if (!doc || doc.path !== openPath || doc.width !== w || doc.stamp !== stamp || doc.spoilers !== spoilers) {
|
|
585
821
|
doc = {
|
|
586
822
|
path: openPath,
|
|
587
823
|
width: w,
|
|
588
824
|
stamp,
|
|
589
825
|
spoilers,
|
|
590
|
-
lines: renderMarkdown(bufferText(buffer), w, { flavor, spoilers }),
|
|
826
|
+
lines: renderMarkdown(isTableName(openPath) ? fenced(bufferText(buffer)) : bufferText(buffer), w, { flavor, spoilers }),
|
|
591
827
|
};
|
|
592
828
|
}
|
|
593
829
|
scroll = clamp(scroll, 0, Math.max(0, doc.lines.length - h));
|
|
@@ -634,31 +870,49 @@ export async function run(options) {
|
|
|
634
870
|
{ key: "^k", label: "kill" },
|
|
635
871
|
{ key: "^g", label: "keys" },
|
|
636
872
|
]
|
|
637
|
-
:
|
|
873
|
+
: rowFiltering
|
|
638
874
|
? [
|
|
639
|
-
{ key: "type", label: "filter" },
|
|
640
|
-
{ key: "↵", label: "
|
|
875
|
+
{ key: "type", label: "filter rows" },
|
|
876
|
+
{ key: "↵", label: "keep" },
|
|
641
877
|
{ key: "esc", label: "clear" },
|
|
642
878
|
]
|
|
643
|
-
:
|
|
879
|
+
: filtering
|
|
644
880
|
? [
|
|
645
|
-
{ key: "
|
|
881
|
+
{ key: "type", label: "filter" },
|
|
646
882
|
{ key: "↵", label: "open" },
|
|
647
|
-
{ key: "
|
|
648
|
-
{ key: "/", label: "find" },
|
|
649
|
-
{ key: "tab", label: "reader" },
|
|
650
|
-
{ key: "?", label: "keys" },
|
|
651
|
-
{ key: "q", label: "quit" },
|
|
883
|
+
{ key: "esc", label: "clear" },
|
|
652
884
|
]
|
|
653
|
-
:
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
885
|
+
: pane === "view" && gridding
|
|
886
|
+
? [
|
|
887
|
+
{ key: "←↑↓→", label: "move" },
|
|
888
|
+
{ key: "o", label: "sort" },
|
|
889
|
+
{ key: "/", label: "filter" },
|
|
890
|
+
{ key: "↵", label: "row" },
|
|
891
|
+
{ key: "c", label: "stats" },
|
|
892
|
+
{ key: "v", label: "text" },
|
|
893
|
+
{ key: "e", label: "edit" },
|
|
894
|
+
{ key: "?", label: "keys" },
|
|
895
|
+
{ key: "q", label: "quit" },
|
|
896
|
+
]
|
|
897
|
+
: pane === "tree"
|
|
898
|
+
? [
|
|
899
|
+
{ key: "↑↓", label: "move" },
|
|
900
|
+
{ key: "↵", label: "open" },
|
|
901
|
+
{ key: "←→", label: "fold" },
|
|
902
|
+
{ key: "/", label: "find" },
|
|
903
|
+
{ key: "tab", label: "reader" },
|
|
904
|
+
{ key: "?", label: "keys" },
|
|
905
|
+
{ key: "q", label: "quit" },
|
|
906
|
+
]
|
|
907
|
+
: [
|
|
908
|
+
{ key: "↑↓", label: "scroll" },
|
|
909
|
+
{ key: "spc", label: "page" },
|
|
910
|
+
{ key: "e", label: "edit" },
|
|
911
|
+
{ key: "g/G", label: "ends" },
|
|
912
|
+
{ key: "tab", label: "files" },
|
|
913
|
+
{ key: "?", label: "keys" },
|
|
914
|
+
{ key: "q", label: "quit" },
|
|
915
|
+
],
|
|
662
916
|
right: [
|
|
663
917
|
{ label: openPath ? (dirname(openRel) === "." ? basename(root) : dirname(openRel)) : basename(root) },
|
|
664
918
|
{ label: flavor, color: theme.muted },
|
|
@@ -674,38 +928,74 @@ export async function run(options) {
|
|
|
674
928
|
m.spacer("fill");
|
|
675
929
|
});
|
|
676
930
|
}
|
|
931
|
+
else if (sheet && table && rows.length) {
|
|
932
|
+
const record = table.rows[rows[cursor.row]] ?? [];
|
|
933
|
+
const height = clamp(table.columns.length + 6, 8, Math.max(8, app.height - 2));
|
|
934
|
+
const labelWidth = clamp(Math.max(...table.columns.map((c) => stringWidth(c.name))) + 2, 8, 28);
|
|
935
|
+
const valueWidth = Math.max(...record.map((cell) => stringWidth(displayCell(cell))), 12);
|
|
936
|
+
// As wide as the row needs, so a short value sits beside its name, not across the screen.
|
|
937
|
+
const width = clamp(labelWidth + valueWidth + 6, 40, Math.max(40, app.width - 8));
|
|
938
|
+
screen.modal({ title: ` row ${(rows[cursor.row] ?? 0) + 1} `, width, height }, (m) => {
|
|
939
|
+
m.keyValues(table.columns.map((column, i) => ({ label: column.name, value: displayCell(record[i] ?? "") })), { labelColor: theme.accent, labelWidth });
|
|
940
|
+
m.spacer("fill");
|
|
941
|
+
m.text("Any key closes.", { fg: theme.muted, align: "center" });
|
|
942
|
+
});
|
|
943
|
+
}
|
|
944
|
+
else if (stats && table) {
|
|
945
|
+
const lines = statsRows(columnStats(table, cursor.col, rows));
|
|
946
|
+
screen.modal({ title: ` ${table.columns[cursor.col]?.name ?? ""} `, width: 56, height: clamp(lines.length + 6, 10, Math.max(10, app.height - 2)) }, (m) => {
|
|
947
|
+
m.keyValues(lines.map((line) => ({ label: line.label, value: displayCell(line.value) })), { labelColor: theme.accent, labelWidth: 12 });
|
|
948
|
+
m.spacer("fill");
|
|
949
|
+
m.text(rowFilter ? "Over the filtered rows. Any key closes." : "Any key closes.", { fg: theme.muted, align: "center" });
|
|
950
|
+
});
|
|
951
|
+
}
|
|
677
952
|
else if (help) {
|
|
678
953
|
screen.modal({ title: " readm3 — keys ", width: 56, height: 24 }, (m) => {
|
|
679
|
-
m.keyValues(mode
|
|
954
|
+
m.keyValues(mode !== "edit" && gridding
|
|
680
955
|
? [
|
|
681
|
-
{ label: "
|
|
682
|
-
{ label: "
|
|
683
|
-
{ label: "
|
|
684
|
-
{ label: "
|
|
685
|
-
{ label: "
|
|
686
|
-
{ label: "
|
|
687
|
-
{ label: "
|
|
688
|
-
{ label: "
|
|
689
|
-
{ label: "
|
|
690
|
-
{ label: "
|
|
691
|
-
]
|
|
692
|
-
: [
|
|
693
|
-
{ label: "↑ ↓ j k", value: "move / scroll" },
|
|
694
|
-
{ label: "→ ← l h", value: "expand / collapse" },
|
|
695
|
-
{ label: "enter", value: "open file" },
|
|
696
|
-
{ label: "e / i", value: "edit this file" },
|
|
697
|
-
{ label: "ctrl+s", value: "save" },
|
|
956
|
+
{ label: "← ↑ ↓ → h j k l", value: "move the cell cursor" },
|
|
957
|
+
{ label: "0 / $", value: "first / last column" },
|
|
958
|
+
{ label: "g / G space b", value: "top / bottom, page" },
|
|
959
|
+
{ label: "o", value: "sort by column ▲ ▼ off" },
|
|
960
|
+
{ label: "/", value: "filter: text col:x col>n !x" },
|
|
961
|
+
{ label: "enter", value: "the row as a sheet" },
|
|
962
|
+
{ label: "c", value: "column statistics" },
|
|
963
|
+
{ label: "v", value: "grid / raw text" },
|
|
964
|
+
{ label: "e / i", value: "edit the file" },
|
|
965
|
+
{ label: "esc", value: "clear the filter" },
|
|
698
966
|
{ label: "tab", value: "switch pane" },
|
|
699
|
-
{ label: "space / b", value: "page down / up" },
|
|
700
|
-
{ label: "g / G", value: "top / bottom" },
|
|
701
|
-
{ label: "/", value: "filter files" },
|
|
702
|
-
{ label: "s", value: "reveal spoilers" },
|
|
703
|
-
{ label: "esc", value: "clear filter" },
|
|
704
|
-
{ label: "r", value: "rescan and reload" },
|
|
705
|
-
{ label: "[ ]", value: "sidebar width" },
|
|
706
|
-
{ label: "?", value: "this help" },
|
|
707
967
|
{ label: "q / ctrl+c", value: "quit" },
|
|
708
|
-
]
|
|
968
|
+
]
|
|
969
|
+
: mode === "edit"
|
|
970
|
+
? [
|
|
971
|
+
{ label: "esc", value: "back to the preview" },
|
|
972
|
+
{ label: "ctrl+g", value: "this help" },
|
|
973
|
+
{ label: "ctrl+s", value: "save" },
|
|
974
|
+
{ label: "ctrl+z / ctrl+y", value: "undo / redo" },
|
|
975
|
+
{ label: "ctrl+k / ctrl+u", value: "kill to end / start" },
|
|
976
|
+
{ label: "ctrl+a / ctrl+e", value: "line start / end" },
|
|
977
|
+
{ label: "ctrl+← →", value: "by word" },
|
|
978
|
+
{ label: "ctrl+home/end", value: "top / bottom" },
|
|
979
|
+
{ label: "enter", value: "split, continuing a list" },
|
|
980
|
+
{ label: "tab", value: "indent two spaces" },
|
|
981
|
+
]
|
|
982
|
+
: [
|
|
983
|
+
{ label: "↑ ↓ j k", value: "move / scroll" },
|
|
984
|
+
{ label: "→ ← l h", value: "expand / collapse" },
|
|
985
|
+
{ label: "enter", value: "open file" },
|
|
986
|
+
{ label: "e / i", value: "edit this file" },
|
|
987
|
+
{ label: "ctrl+s", value: "save" },
|
|
988
|
+
{ label: "tab", value: "switch pane" },
|
|
989
|
+
{ label: "space / b", value: "page down / up" },
|
|
990
|
+
{ label: "g / G", value: "top / bottom" },
|
|
991
|
+
{ label: "/", value: "filter files" },
|
|
992
|
+
{ label: "s", value: "reveal spoilers" },
|
|
993
|
+
{ label: "esc", value: "clear filter" },
|
|
994
|
+
{ label: "r", value: "rescan and reload" },
|
|
995
|
+
{ label: "[ ]", value: "sidebar width" },
|
|
996
|
+
{ label: "?", value: "this help" },
|
|
997
|
+
{ label: "q / ctrl+c", value: "quit" },
|
|
998
|
+
], { labelColor: theme.accent, labelWidth: 16 });
|
|
709
999
|
m.spacer(1);
|
|
710
1000
|
m.text("Any key closes.", { fg: theme.muted, align: "center" });
|
|
711
1001
|
});
|