@profullstack/readm3 0.2.0 → 0.4.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 +222 -13
- package/dist/cli.d.ts +5 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +52 -6
- package/dist/cli.js.map +1 -1
- package/dist/cloud-cli.d.ts +4 -0
- package/dist/cloud-cli.d.ts.map +1 -0
- package/dist/cloud-cli.js +192 -0
- package/dist/cloud-cli.js.map +1 -0
- package/dist/cloud-client.d.ts +14 -0
- package/dist/cloud-client.d.ts.map +1 -0
- package/dist/cloud-client.js +62 -0
- package/dist/cloud-client.js.map +1 -0
- package/dist/editor.d.ts +77 -0
- package/dist/editor.d.ts.map +1 -0
- package/dist/editor.js +308 -0
- package/dist/editor.js.map +1 -0
- package/dist/emoji.d.ts +12 -0
- package/dist/emoji.d.ts.map +1 -0
- package/dist/emoji.js +73 -0
- package/dist/emoji.js.map +1 -0
- package/dist/flavors.d.ts +68 -0
- package/dist/flavors.d.ts.map +1 -0
- package/dist/flavors.js +223 -0
- package/dist/flavors.js.map +1 -0
- package/dist/index.d.ts +10 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -4
- package/dist/index.js.map +1 -1
- package/dist/markdown.d.ts +12 -3
- package/dist/markdown.d.ts.map +1 -1
- package/dist/markdown.js +424 -379
- package/dist/markdown.js.map +1 -1
- package/dist/mcp.d.ts +2 -0
- package/dist/mcp.d.ts.map +1 -0
- package/dist/mcp.js +301 -0
- package/dist/mcp.js.map +1 -0
- package/dist/print.d.ts +3 -3
- package/dist/print.d.ts.map +1 -1
- package/dist/print.js +8 -3
- package/dist/print.js.map +1 -1
- package/dist/roles.d.ts.map +1 -1
- package/dist/roles.js +17 -0
- package/dist/roles.js.map +1 -1
- package/dist/viewer.d.ts +5 -0
- package/dist/viewer.d.ts.map +1 -1
- package/dist/viewer.js +375 -60
- package/dist/viewer.js.map +1 -1
- package/package.json +18 -6
package/dist/viewer.js
CHANGED
|
@@ -1,22 +1,36 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The reader: a file browser on the left, the
|
|
2
|
+
* The reader: a file browser on the left, the document on the right.
|
|
3
|
+
*
|
|
4
|
+
* The right pane has two modes. Reading renders the buffer through
|
|
5
|
+
* `markdown.ts`; editing shows the same buffer raw, with a caret. Both read the
|
|
6
|
+
* same text, so leaving edit mode is all it takes to see the change rendered —
|
|
7
|
+
* there is no separate preview to keep in step.
|
|
3
8
|
*/
|
|
4
|
-
import { readFileSync, statSync } from "node:fs";
|
|
9
|
+
import { readFileSync, statSync, writeFileSync } from "node:fs";
|
|
5
10
|
import { basename, dirname, resolve } from "node:path";
|
|
6
11
|
import { createApp, stringWidth } from "@profullstack/hqtui";
|
|
12
|
+
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";
|
|
13
|
+
import { DEFAULT_FLAVOR } from "./flavors.js";
|
|
7
14
|
import { renderMarkdown } from "./markdown.js";
|
|
8
15
|
import { colorOf } from "./roles.js";
|
|
9
16
|
import { filterTree, flatten, indexOfPath, label as pathLabel, reveal, scan, } from "./tree.js";
|
|
10
17
|
/** Files above this size are refused rather than rendered. */
|
|
11
18
|
const MAX_BYTES = 4 * 1024 * 1024;
|
|
19
|
+
/** A tab is two columns, in the buffer and on screen alike. */
|
|
20
|
+
const TAB = " ";
|
|
12
21
|
const clamp = (n, lo, hi) => Math.max(lo, Math.min(hi, n));
|
|
22
|
+
const expand = (line) => line.replace(/\t/g, TAB);
|
|
23
|
+
/** Where a caret at `col` lands once tabs are expanded. */
|
|
24
|
+
const displayCol = (line, col) => expand(line.slice(0, col)).length;
|
|
13
25
|
export async function run(options) {
|
|
14
26
|
const root = resolve(options.root);
|
|
27
|
+
const flavor = options.flavor ?? DEFAULT_FLAVOR;
|
|
28
|
+
const readOnly = options.readOnly ?? false;
|
|
15
29
|
const app = await createApp({
|
|
16
30
|
theme: options.theme,
|
|
17
31
|
mouse: options.mouse !== false,
|
|
18
|
-
// `q` is handled below so it can be typed into the filter.
|
|
19
|
-
quitKeys: [
|
|
32
|
+
// `q` is handled below so it can be typed into the filter, and into the text.
|
|
33
|
+
quitKeys: [],
|
|
20
34
|
});
|
|
21
35
|
let entries = scan(root, { all: options.all ?? false });
|
|
22
36
|
let filter = "";
|
|
@@ -28,9 +42,16 @@ export async function run(options) {
|
|
|
28
42
|
let help = false;
|
|
29
43
|
let message = "";
|
|
30
44
|
let openPath = null;
|
|
31
|
-
|
|
45
|
+
/** The open file's text, and the caret into it. Null until a file opens. */
|
|
46
|
+
let buffer = null;
|
|
47
|
+
let mode = "read";
|
|
48
|
+
let spoilers = false;
|
|
32
49
|
let doc = null;
|
|
33
50
|
let scroll = 0;
|
|
51
|
+
/** Bumped on every edit, so the rendered document is rebuilt exactly then. */
|
|
52
|
+
let stamp = 0;
|
|
53
|
+
/** A yes/no the next keypress answers. */
|
|
54
|
+
let pending = null;
|
|
34
55
|
let sidebar = options.sidebar ?? 0;
|
|
35
56
|
// Last measured pane interiors; the panel header needs them a frame early.
|
|
36
57
|
let viewport = { w: 80, h: 20 };
|
|
@@ -38,9 +59,14 @@ export async function run(options) {
|
|
|
38
59
|
// Owned here rather than left to the widget, so a click can be turned back
|
|
39
60
|
// into an index and the view stays put when the selection moves inside it.
|
|
40
61
|
let treeOffset = 0;
|
|
62
|
+
// The edit pane scrolls on both axes: long lines are not wrapped, because a
|
|
63
|
+
// caret in a wrapped line is a lie about where the text is.
|
|
64
|
+
let editTop = 0;
|
|
65
|
+
let editLeft = 0;
|
|
41
66
|
// The panel header quotes numbers only the draw can measure, so a change in
|
|
42
67
|
// geometry or document length schedules exactly one more frame.
|
|
43
68
|
let measured = { total: -1, h: -1 };
|
|
69
|
+
const dirty = () => buffer?.dirty ?? false;
|
|
44
70
|
const rebuild = (keepPath) => {
|
|
45
71
|
view = filter ? filterTree(entries, filter, root) : entries;
|
|
46
72
|
flat = flatten(view);
|
|
@@ -54,16 +80,50 @@ export async function run(options) {
|
|
|
54
80
|
message = `${basename(path)} is ${(info.size / 1024 / 1024).toFixed(1)} MB — too large to render`;
|
|
55
81
|
return;
|
|
56
82
|
}
|
|
57
|
-
|
|
83
|
+
buffer = createEditor(readFileSync(path, "utf8"));
|
|
58
84
|
openPath = path;
|
|
59
85
|
doc = null;
|
|
60
86
|
scroll = 0;
|
|
87
|
+
editTop = 0;
|
|
88
|
+
editLeft = 0;
|
|
89
|
+
mode = "read";
|
|
61
90
|
message = "";
|
|
62
91
|
}
|
|
63
92
|
catch (error) {
|
|
64
93
|
message = `Cannot read ${basename(path)}: ${error instanceof Error ? error.message : String(error)}`;
|
|
65
94
|
}
|
|
66
95
|
};
|
|
96
|
+
/** Do something that would discard unsaved work, asking first if it would. */
|
|
97
|
+
const guard = (what, run) => {
|
|
98
|
+
if (!dirty()) {
|
|
99
|
+
run();
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
pending = { message: `${basename(openPath ?? "")} has unsaved changes. ${what} anyway?`, run };
|
|
103
|
+
};
|
|
104
|
+
const save = () => {
|
|
105
|
+
if (!openPath || !buffer)
|
|
106
|
+
return;
|
|
107
|
+
try {
|
|
108
|
+
writeFileSync(openPath, bufferText(buffer), "utf8");
|
|
109
|
+
markSaved(buffer);
|
|
110
|
+
message = "";
|
|
111
|
+
}
|
|
112
|
+
catch (error) {
|
|
113
|
+
message = `Cannot save: ${error instanceof Error ? error.message : String(error)}`;
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
const enterEdit = () => {
|
|
117
|
+
if (!buffer)
|
|
118
|
+
return;
|
|
119
|
+
if (readOnly) {
|
|
120
|
+
message = "Read-only: started with --read-only.";
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
mode = "edit";
|
|
124
|
+
pane = "view";
|
|
125
|
+
breakRun(buffer);
|
|
126
|
+
};
|
|
67
127
|
const maxScroll = () => Math.max(0, (doc?.lines.length ?? 0) - viewport.h);
|
|
68
128
|
const moveSelection = (delta) => {
|
|
69
129
|
if (flat.length === 0)
|
|
@@ -79,8 +139,11 @@ export async function run(options) {
|
|
|
79
139
|
rebuild(current.entry.path);
|
|
80
140
|
}
|
|
81
141
|
else {
|
|
82
|
-
|
|
83
|
-
|
|
142
|
+
const path = current.entry.path;
|
|
143
|
+
guard("Open another file", () => {
|
|
144
|
+
openFile(path);
|
|
145
|
+
pane = "view";
|
|
146
|
+
});
|
|
84
147
|
}
|
|
85
148
|
};
|
|
86
149
|
// Open whatever was asked for, or fall back to the first file in the tree.
|
|
@@ -91,13 +154,139 @@ export async function run(options) {
|
|
|
91
154
|
rebuild(openPath);
|
|
92
155
|
}
|
|
93
156
|
}
|
|
157
|
+
/** Keys while the caret is in the text. Returns false when unhandled. */
|
|
158
|
+
const editKey = (event) => {
|
|
159
|
+
const ed = buffer;
|
|
160
|
+
const { key, name, char, ctrl, alt } = event;
|
|
161
|
+
const page = Math.max(1, viewport.h - 2);
|
|
162
|
+
switch (key) {
|
|
163
|
+
case "escape":
|
|
164
|
+
case "ctrl+c":
|
|
165
|
+
// Never a dead key: from the text it steps out, and the next one quits.
|
|
166
|
+
mode = "read";
|
|
167
|
+
breakRun(ed);
|
|
168
|
+
return;
|
|
169
|
+
case "ctrl+s":
|
|
170
|
+
save();
|
|
171
|
+
return;
|
|
172
|
+
case "ctrl+g":
|
|
173
|
+
case "f1":
|
|
174
|
+
// "?" is a character in here, so help moves to a key that is not.
|
|
175
|
+
help = true;
|
|
176
|
+
return;
|
|
177
|
+
case "ctrl+z":
|
|
178
|
+
undo(ed);
|
|
179
|
+
stamp++;
|
|
180
|
+
return;
|
|
181
|
+
case "ctrl+y":
|
|
182
|
+
case "ctrl+shift+z":
|
|
183
|
+
redo(ed);
|
|
184
|
+
stamp++;
|
|
185
|
+
return;
|
|
186
|
+
case "ctrl+k":
|
|
187
|
+
killToLineEnd(ed);
|
|
188
|
+
stamp++;
|
|
189
|
+
return;
|
|
190
|
+
case "ctrl+u":
|
|
191
|
+
killToLineStart(ed);
|
|
192
|
+
stamp++;
|
|
193
|
+
return;
|
|
194
|
+
case "ctrl+a":
|
|
195
|
+
moveHome(ed);
|
|
196
|
+
return;
|
|
197
|
+
case "ctrl+e":
|
|
198
|
+
moveEnd(ed);
|
|
199
|
+
return;
|
|
200
|
+
case "ctrl+home":
|
|
201
|
+
moveDocStart(ed);
|
|
202
|
+
return;
|
|
203
|
+
case "ctrl+end":
|
|
204
|
+
moveDocEnd(ed);
|
|
205
|
+
return;
|
|
206
|
+
case "ctrl+left":
|
|
207
|
+
case "alt+left":
|
|
208
|
+
moveWordLeft(ed);
|
|
209
|
+
return;
|
|
210
|
+
case "ctrl+right":
|
|
211
|
+
case "alt+right":
|
|
212
|
+
moveWordRight(ed);
|
|
213
|
+
return;
|
|
214
|
+
default:
|
|
215
|
+
break;
|
|
216
|
+
}
|
|
217
|
+
switch (name) {
|
|
218
|
+
case "left":
|
|
219
|
+
moveLeft(ed);
|
|
220
|
+
return;
|
|
221
|
+
case "right":
|
|
222
|
+
moveRight(ed);
|
|
223
|
+
return;
|
|
224
|
+
case "up":
|
|
225
|
+
moveUp(ed);
|
|
226
|
+
return;
|
|
227
|
+
case "down":
|
|
228
|
+
moveDown(ed);
|
|
229
|
+
return;
|
|
230
|
+
case "home":
|
|
231
|
+
moveHome(ed);
|
|
232
|
+
return;
|
|
233
|
+
case "end":
|
|
234
|
+
moveEnd(ed);
|
|
235
|
+
return;
|
|
236
|
+
case "pageup":
|
|
237
|
+
moveVertical(ed, -page);
|
|
238
|
+
return;
|
|
239
|
+
case "pagedown":
|
|
240
|
+
moveVertical(ed, page);
|
|
241
|
+
return;
|
|
242
|
+
case "enter":
|
|
243
|
+
newline(ed);
|
|
244
|
+
stamp++;
|
|
245
|
+
return;
|
|
246
|
+
case "backspace":
|
|
247
|
+
backspace(ed);
|
|
248
|
+
stamp++;
|
|
249
|
+
return;
|
|
250
|
+
case "delete":
|
|
251
|
+
del(ed);
|
|
252
|
+
stamp++;
|
|
253
|
+
return;
|
|
254
|
+
case "tab":
|
|
255
|
+
indent(ed);
|
|
256
|
+
stamp++;
|
|
257
|
+
return;
|
|
258
|
+
case "space":
|
|
259
|
+
insert(ed, " ");
|
|
260
|
+
stamp++;
|
|
261
|
+
return;
|
|
262
|
+
default:
|
|
263
|
+
break;
|
|
264
|
+
}
|
|
265
|
+
if (char && !ctrl && !alt) {
|
|
266
|
+
insert(ed, char);
|
|
267
|
+
stamp++;
|
|
268
|
+
}
|
|
269
|
+
};
|
|
94
270
|
app.on("key", (event) => {
|
|
95
271
|
const { key, name, char } = event;
|
|
272
|
+
if (pending) {
|
|
273
|
+
const answer = pending;
|
|
274
|
+
pending = null;
|
|
275
|
+
if (key === "y" || key === "Y")
|
|
276
|
+
answer.run();
|
|
277
|
+
app.invalidate();
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
96
280
|
if (help) {
|
|
97
281
|
help = false;
|
|
98
282
|
app.invalidate();
|
|
99
283
|
return;
|
|
100
284
|
}
|
|
285
|
+
if (mode === "edit" && buffer) {
|
|
286
|
+
editKey(event);
|
|
287
|
+
app.invalidate();
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
101
290
|
if (filtering) {
|
|
102
291
|
if (key === "escape") {
|
|
103
292
|
filtering = false;
|
|
@@ -128,8 +317,24 @@ export async function run(options) {
|
|
|
128
317
|
return;
|
|
129
318
|
}
|
|
130
319
|
switch (key) {
|
|
320
|
+
case "ctrl+c":
|
|
131
321
|
case "q":
|
|
132
|
-
app.stop();
|
|
322
|
+
guard("Quit", () => app.stop());
|
|
323
|
+
app.invalidate();
|
|
324
|
+
return;
|
|
325
|
+
case "ctrl+s":
|
|
326
|
+
save();
|
|
327
|
+
app.invalidate();
|
|
328
|
+
return;
|
|
329
|
+
case "e":
|
|
330
|
+
case "i":
|
|
331
|
+
enterEdit();
|
|
332
|
+
app.invalidate();
|
|
333
|
+
return;
|
|
334
|
+
case "s":
|
|
335
|
+
spoilers = !spoilers;
|
|
336
|
+
doc = null;
|
|
337
|
+
app.invalidate();
|
|
133
338
|
return;
|
|
134
339
|
case "?":
|
|
135
340
|
help = true;
|
|
@@ -141,12 +346,14 @@ export async function run(options) {
|
|
|
141
346
|
return;
|
|
142
347
|
case "r": {
|
|
143
348
|
const keep = openPath;
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
349
|
+
guard("Reload from disk", () => {
|
|
350
|
+
entries = scan(root, { all: options.all ?? false });
|
|
351
|
+
if (keep)
|
|
352
|
+
reveal(entries, keep);
|
|
353
|
+
rebuild(keep);
|
|
354
|
+
if (keep)
|
|
355
|
+
openFile(keep);
|
|
356
|
+
});
|
|
150
357
|
app.invalidate();
|
|
151
358
|
return;
|
|
152
359
|
}
|
|
@@ -297,14 +504,15 @@ export async function run(options) {
|
|
|
297
504
|
}
|
|
298
505
|
p.list({
|
|
299
506
|
items: flat.map(({ entry, depth }) => {
|
|
300
|
-
const
|
|
507
|
+
const indentation = " ".repeat(depth);
|
|
301
508
|
const glyph = entry.dir ? (entry.expanded ? "▾ " : "▸ ") : " ";
|
|
509
|
+
const mark = entry.path === openPath && dirty() ? "● " : "";
|
|
302
510
|
const color = entry.dir
|
|
303
511
|
? theme.primary
|
|
304
512
|
: entry.path === openPath
|
|
305
513
|
? theme.accent
|
|
306
514
|
: theme.foreground;
|
|
307
|
-
return { label: `${
|
|
515
|
+
return { label: `${indentation}${glyph}${mark}${entry.name}`, color };
|
|
308
516
|
}),
|
|
309
517
|
selected,
|
|
310
518
|
offset: treeOffset,
|
|
@@ -328,11 +536,17 @@ export async function run(options) {
|
|
|
328
536
|
});
|
|
329
537
|
});
|
|
330
538
|
// -------------------------------------------------------- reader
|
|
539
|
+
const editing = mode === "edit" && buffer !== null;
|
|
331
540
|
panes.panel({
|
|
332
|
-
title: openRel ? ` ${openRel} ` : " readm3 ",
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
541
|
+
title: openRel ? ` ${dirty() ? "● " : ""}${openRel} ` : " readm3 ",
|
|
542
|
+
titleColor: editing ? theme.warning : theme.title,
|
|
543
|
+
subtitle: openPath ? (editing ? "EDIT" : `${percent}%`) : undefined,
|
|
544
|
+
footer: openPath && editing && buffer
|
|
545
|
+
? `${buffer.row + 1}:${buffer.col + 1}`
|
|
546
|
+
: openPath && total > 0
|
|
547
|
+
? `${Math.min(scroll + viewport.h, total)}/${total}`
|
|
548
|
+
: undefined,
|
|
549
|
+
borderColor: editing ? theme.warning : pane === "view" ? theme.borderFocused : theme.border,
|
|
336
550
|
padding: [0, 1],
|
|
337
551
|
}, (p) => {
|
|
338
552
|
const w = p.width;
|
|
@@ -341,7 +555,10 @@ export async function run(options) {
|
|
|
341
555
|
p.ctx.hit({
|
|
342
556
|
rect: p.surface.hitRect(),
|
|
343
557
|
onScroll: (delta) => {
|
|
344
|
-
|
|
558
|
+
if (editing && buffer)
|
|
559
|
+
moveVertical(buffer, delta * 3);
|
|
560
|
+
else
|
|
561
|
+
scroll = clamp(scroll + delta * 3, 0, maxScroll());
|
|
345
562
|
app.invalidate();
|
|
346
563
|
},
|
|
347
564
|
onClick: () => {
|
|
@@ -353,15 +570,25 @@ export async function run(options) {
|
|
|
353
570
|
p.text(message, { fg: theme.danger, wrap: true });
|
|
354
571
|
return;
|
|
355
572
|
}
|
|
356
|
-
if (!openPath ||
|
|
573
|
+
if (!openPath || !buffer) {
|
|
357
574
|
p.spacer("fill");
|
|
358
575
|
p.text("readm3", { fg: theme.title, bold: true, align: "center" });
|
|
359
576
|
p.text("Pick a file on the left. ? for keys.", { fg: theme.muted, align: "center" });
|
|
360
577
|
p.spacer("fill");
|
|
361
578
|
return;
|
|
362
579
|
}
|
|
363
|
-
if (
|
|
364
|
-
|
|
580
|
+
if (editing) {
|
|
581
|
+
drawEditor(p, buffer, theme, w, h);
|
|
582
|
+
return;
|
|
583
|
+
}
|
|
584
|
+
if (!doc || doc.path !== openPath || doc.width !== w || doc.stamp !== stamp || doc.spoilers !== spoilers) {
|
|
585
|
+
doc = {
|
|
586
|
+
path: openPath,
|
|
587
|
+
width: w,
|
|
588
|
+
stamp,
|
|
589
|
+
spoilers,
|
|
590
|
+
lines: renderMarkdown(bufferText(buffer), w, { flavor, spoilers }),
|
|
591
|
+
};
|
|
365
592
|
}
|
|
366
593
|
scroll = clamp(scroll, 0, Math.max(0, doc.lines.length - h));
|
|
367
594
|
if (measured.total !== doc.lines.length || measured.h !== h) {
|
|
@@ -399,57 +626,145 @@ export async function run(options) {
|
|
|
399
626
|
});
|
|
400
627
|
});
|
|
401
628
|
screen.statusBar({
|
|
402
|
-
items:
|
|
629
|
+
items: mode === "edit"
|
|
403
630
|
? [
|
|
404
|
-
{ key: "
|
|
405
|
-
{ key: "
|
|
406
|
-
{ key: "
|
|
631
|
+
{ key: "esc", label: "preview" },
|
|
632
|
+
{ key: "^s", label: "save" },
|
|
633
|
+
{ key: "^z", label: "undo" },
|
|
634
|
+
{ key: "^k", label: "kill" },
|
|
635
|
+
{ key: "^g", label: "keys" },
|
|
407
636
|
]
|
|
408
|
-
:
|
|
637
|
+
: filtering
|
|
409
638
|
? [
|
|
410
|
-
{ key: "
|
|
639
|
+
{ key: "type", label: "filter" },
|
|
411
640
|
{ key: "↵", label: "open" },
|
|
412
|
-
{ key: "
|
|
413
|
-
{ key: "/", label: "find" },
|
|
414
|
-
{ key: "tab", label: "reader" },
|
|
415
|
-
{ key: "?", label: "keys" },
|
|
416
|
-
{ key: "q", label: "quit" },
|
|
641
|
+
{ key: "esc", label: "clear" },
|
|
417
642
|
]
|
|
418
|
-
:
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
643
|
+
: pane === "tree"
|
|
644
|
+
? [
|
|
645
|
+
{ key: "↑↓", label: "move" },
|
|
646
|
+
{ key: "↵", label: "open" },
|
|
647
|
+
{ key: "←→", label: "fold" },
|
|
648
|
+
{ key: "/", label: "find" },
|
|
649
|
+
{ key: "tab", label: "reader" },
|
|
650
|
+
{ key: "?", label: "keys" },
|
|
651
|
+
{ key: "q", label: "quit" },
|
|
652
|
+
]
|
|
653
|
+
: [
|
|
654
|
+
{ key: "↑↓", label: "scroll" },
|
|
655
|
+
{ key: "spc", label: "page" },
|
|
656
|
+
{ key: "e", label: "edit" },
|
|
657
|
+
{ key: "g/G", label: "ends" },
|
|
658
|
+
{ key: "tab", label: "files" },
|
|
659
|
+
{ key: "?", label: "keys" },
|
|
660
|
+
{ key: "q", label: "quit" },
|
|
661
|
+
],
|
|
426
662
|
right: [
|
|
427
663
|
{ label: openPath ? (dirname(openRel) === "." ? basename(root) : dirname(openRel)) : basename(root) },
|
|
664
|
+
{ label: flavor, color: theme.muted },
|
|
428
665
|
{ label: theme.name, color: theme.muted },
|
|
429
666
|
],
|
|
430
667
|
});
|
|
431
|
-
if (
|
|
432
|
-
screen.modal({ title: "
|
|
433
|
-
m.
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
668
|
+
if (pending) {
|
|
669
|
+
screen.modal({ title: " unsaved changes ", width: 56, height: 9 }, (m) => {
|
|
670
|
+
m.spacer(1);
|
|
671
|
+
m.text(pending?.message ?? "", { fg: theme.foreground, align: "center", wrap: true });
|
|
672
|
+
m.spacer(1);
|
|
673
|
+
m.text("y to continue, any other key to stay", { fg: theme.muted, align: "center" });
|
|
674
|
+
m.spacer("fill");
|
|
675
|
+
});
|
|
676
|
+
}
|
|
677
|
+
else if (help) {
|
|
678
|
+
screen.modal({ title: " readm3 — keys ", width: 56, height: 24 }, (m) => {
|
|
679
|
+
m.keyValues(mode === "edit"
|
|
680
|
+
? [
|
|
681
|
+
{ label: "esc", value: "back to the preview" },
|
|
682
|
+
{ label: "ctrl+g", value: "this help" },
|
|
683
|
+
{ label: "ctrl+s", value: "save" },
|
|
684
|
+
{ label: "ctrl+z / ctrl+y", value: "undo / redo" },
|
|
685
|
+
{ label: "ctrl+k / ctrl+u", value: "kill to end / start" },
|
|
686
|
+
{ label: "ctrl+a / ctrl+e", value: "line start / end" },
|
|
687
|
+
{ label: "ctrl+← →", value: "by word" },
|
|
688
|
+
{ label: "ctrl+home/end", value: "top / bottom" },
|
|
689
|
+
{ label: "enter", value: "split, continuing a list" },
|
|
690
|
+
{ label: "tab", value: "indent two spaces" },
|
|
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" },
|
|
698
|
+
{ 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
|
+
{ label: "q / ctrl+c", value: "quit" },
|
|
708
|
+
], { labelColor: theme.accent, labelWidth: 16 });
|
|
447
709
|
m.spacer(1);
|
|
448
710
|
m.text("Any key closes.", { fg: theme.muted, align: "center" });
|
|
449
711
|
});
|
|
450
712
|
}
|
|
451
713
|
});
|
|
452
714
|
});
|
|
715
|
+
/**
|
|
716
|
+
* The raw buffer with a caret.
|
|
717
|
+
*
|
|
718
|
+
* Long lines scroll sideways rather than wrap: a caret inside a wrapped line
|
|
719
|
+
* cannot say honestly which column of the file it is in.
|
|
720
|
+
*/
|
|
721
|
+
function drawEditor(p, ed, theme, w, h) {
|
|
722
|
+
const gutter = Math.max(3, String(ed.lines.length).length + 1);
|
|
723
|
+
const textWidth = Math.max(8, w - gutter - 1);
|
|
724
|
+
// Keep the caret on screen on both axes.
|
|
725
|
+
if (ed.row < editTop)
|
|
726
|
+
editTop = ed.row;
|
|
727
|
+
else if (ed.row >= editTop + h)
|
|
728
|
+
editTop = ed.row - h + 1;
|
|
729
|
+
editTop = clamp(editTop, 0, Math.max(0, ed.lines.length - 1));
|
|
730
|
+
const caretCol = displayCol(ed.lines[ed.row] ?? "", ed.col);
|
|
731
|
+
if (caretCol < editLeft)
|
|
732
|
+
editLeft = caretCol;
|
|
733
|
+
else if (caretCol >= editLeft + textWidth)
|
|
734
|
+
editLeft = caretCol - textWidth + 1;
|
|
735
|
+
editLeft = Math.max(0, editLeft);
|
|
736
|
+
for (let i = 0; i < h; i++) {
|
|
737
|
+
const row = editTop + i;
|
|
738
|
+
if (row >= ed.lines.length) {
|
|
739
|
+
p.text("", { height: 1 });
|
|
740
|
+
continue;
|
|
741
|
+
}
|
|
742
|
+
const line = expand(ed.lines[row]);
|
|
743
|
+
const shown = line.slice(editLeft, editLeft + textWidth);
|
|
744
|
+
const onCaretRow = row === ed.row;
|
|
745
|
+
const caretAt = caretCol - editLeft;
|
|
746
|
+
p.row({ height: 1 }, (r) => {
|
|
747
|
+
r.text(`${String(row + 1).padStart(gutter - 1)} `, {
|
|
748
|
+
width: gutter,
|
|
749
|
+
fg: onCaretRow ? theme.accent : theme.border,
|
|
750
|
+
});
|
|
751
|
+
if (!onCaretRow) {
|
|
752
|
+
r.text(shown, { width: textWidth, fg: theme.foreground });
|
|
753
|
+
return;
|
|
754
|
+
}
|
|
755
|
+
// Split the line so the caret cell can be painted on its own.
|
|
756
|
+
const before = shown.slice(0, caretAt);
|
|
757
|
+
const under = shown.slice(caretAt, caretAt + 1) || " ";
|
|
758
|
+
const after = shown.slice(caretAt + 1);
|
|
759
|
+
if (before)
|
|
760
|
+
r.text(before, { width: stringWidth(before), fg: theme.foreground });
|
|
761
|
+
r.text(under, { width: Math.max(1, stringWidth(under)), fg: theme.background, bg: theme.cursor });
|
|
762
|
+
if (after)
|
|
763
|
+
r.text(after, { width: stringWidth(after), fg: theme.foreground });
|
|
764
|
+
r.spacer("fill");
|
|
765
|
+
});
|
|
766
|
+
}
|
|
767
|
+
}
|
|
453
768
|
await app.start();
|
|
454
769
|
}
|
|
455
770
|
//# sourceMappingURL=viewer.js.map
|