@profullstack/readm3 0.1.1 → 0.3.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 +127 -16
- package/dist/cli.d.ts +9 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +77 -5
- package/dist/cli.js.map +1 -1
- 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 +14 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +10 -5
- package/dist/index.js.map +1 -1
- package/dist/markdown.d.ts +20 -3
- package/dist/markdown.d.ts.map +1 -1
- package/dist/markdown.js +455 -379
- package/dist/markdown.js.map +1 -1
- package/dist/print.d.ts +21 -0
- package/dist/print.d.ts.map +1 -0
- package/dist/print.js +95 -0
- package/dist/print.js.map +1 -0
- package/dist/roles.d.ts +10 -0
- package/dist/roles.d.ts.map +1 -0
- package/dist/roles.js +48 -0
- package/dist/roles.js.map +1 -0
- package/dist/viewer.d.ts +5 -0
- package/dist/viewer.d.ts.map +1 -1
- package/dist/viewer.js +378 -94
- package/dist/viewer.js.map +1 -1
- package/package.json +10 -4
package/dist/viewer.js
CHANGED
|
@@ -1,52 +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";
|
|
5
|
-
import { basename, dirname,
|
|
9
|
+
import { readFileSync, statSync, writeFileSync } from "node:fs";
|
|
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";
|
|
15
|
+
import { colorOf } from "./roles.js";
|
|
8
16
|
import { filterTree, flatten, indexOfPath, label as pathLabel, reveal, scan, } from "./tree.js";
|
|
9
17
|
/** Files above this size are refused rather than rendered. */
|
|
10
18
|
const MAX_BYTES = 4 * 1024 * 1024;
|
|
19
|
+
/** A tab is two columns, in the buffer and on screen alike. */
|
|
20
|
+
const TAB = " ";
|
|
11
21
|
const clamp = (n, lo, hi) => Math.max(lo, Math.min(hi, n));
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
case "h1":
|
|
16
|
-
return theme.title;
|
|
17
|
-
case "h2":
|
|
18
|
-
return theme.primary;
|
|
19
|
-
case "h3":
|
|
20
|
-
return theme.secondary;
|
|
21
|
-
case "code":
|
|
22
|
-
case "lang":
|
|
23
|
-
case "bullet":
|
|
24
|
-
return theme.accent;
|
|
25
|
-
case "fence":
|
|
26
|
-
return theme.foreground;
|
|
27
|
-
case "gutter":
|
|
28
|
-
case "rule":
|
|
29
|
-
return theme.border;
|
|
30
|
-
case "link":
|
|
31
|
-
case "url":
|
|
32
|
-
return theme.info;
|
|
33
|
-
case "quote":
|
|
34
|
-
return theme.secondary;
|
|
35
|
-
case "meta":
|
|
36
|
-
return theme.muted;
|
|
37
|
-
case "th":
|
|
38
|
-
return theme.title;
|
|
39
|
-
default:
|
|
40
|
-
return theme.foreground;
|
|
41
|
-
}
|
|
42
|
-
}
|
|
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;
|
|
43
25
|
export async function run(options) {
|
|
44
26
|
const root = resolve(options.root);
|
|
27
|
+
const flavor = options.flavor ?? DEFAULT_FLAVOR;
|
|
28
|
+
const readOnly = options.readOnly ?? false;
|
|
45
29
|
const app = await createApp({
|
|
46
30
|
theme: options.theme,
|
|
47
31
|
mouse: options.mouse !== false,
|
|
48
|
-
// `q` is handled below so it can be typed into the filter.
|
|
49
|
-
quitKeys: [
|
|
32
|
+
// `q` is handled below so it can be typed into the filter, and into the text.
|
|
33
|
+
quitKeys: [],
|
|
50
34
|
});
|
|
51
35
|
let entries = scan(root, { all: options.all ?? false });
|
|
52
36
|
let filter = "";
|
|
@@ -58,9 +42,16 @@ export async function run(options) {
|
|
|
58
42
|
let help = false;
|
|
59
43
|
let message = "";
|
|
60
44
|
let openPath = null;
|
|
61
|
-
|
|
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;
|
|
62
49
|
let doc = null;
|
|
63
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;
|
|
64
55
|
let sidebar = options.sidebar ?? 0;
|
|
65
56
|
// Last measured pane interiors; the panel header needs them a frame early.
|
|
66
57
|
let viewport = { w: 80, h: 20 };
|
|
@@ -68,9 +59,14 @@ export async function run(options) {
|
|
|
68
59
|
// Owned here rather than left to the widget, so a click can be turned back
|
|
69
60
|
// into an index and the view stays put when the selection moves inside it.
|
|
70
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;
|
|
71
66
|
// The panel header quotes numbers only the draw can measure, so a change in
|
|
72
67
|
// geometry or document length schedules exactly one more frame.
|
|
73
68
|
let measured = { total: -1, h: -1 };
|
|
69
|
+
const dirty = () => buffer?.dirty ?? false;
|
|
74
70
|
const rebuild = (keepPath) => {
|
|
75
71
|
view = filter ? filterTree(entries, filter, root) : entries;
|
|
76
72
|
flat = flatten(view);
|
|
@@ -84,16 +80,50 @@ export async function run(options) {
|
|
|
84
80
|
message = `${basename(path)} is ${(info.size / 1024 / 1024).toFixed(1)} MB — too large to render`;
|
|
85
81
|
return;
|
|
86
82
|
}
|
|
87
|
-
|
|
83
|
+
buffer = createEditor(readFileSync(path, "utf8"));
|
|
88
84
|
openPath = path;
|
|
89
85
|
doc = null;
|
|
90
86
|
scroll = 0;
|
|
87
|
+
editTop = 0;
|
|
88
|
+
editLeft = 0;
|
|
89
|
+
mode = "read";
|
|
91
90
|
message = "";
|
|
92
91
|
}
|
|
93
92
|
catch (error) {
|
|
94
93
|
message = `Cannot read ${basename(path)}: ${error instanceof Error ? error.message : String(error)}`;
|
|
95
94
|
}
|
|
96
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
|
+
};
|
|
97
127
|
const maxScroll = () => Math.max(0, (doc?.lines.length ?? 0) - viewport.h);
|
|
98
128
|
const moveSelection = (delta) => {
|
|
99
129
|
if (flat.length === 0)
|
|
@@ -109,8 +139,11 @@ export async function run(options) {
|
|
|
109
139
|
rebuild(current.entry.path);
|
|
110
140
|
}
|
|
111
141
|
else {
|
|
112
|
-
|
|
113
|
-
|
|
142
|
+
const path = current.entry.path;
|
|
143
|
+
guard("Open another file", () => {
|
|
144
|
+
openFile(path);
|
|
145
|
+
pane = "view";
|
|
146
|
+
});
|
|
114
147
|
}
|
|
115
148
|
};
|
|
116
149
|
// Open whatever was asked for, or fall back to the first file in the tree.
|
|
@@ -121,13 +154,139 @@ export async function run(options) {
|
|
|
121
154
|
rebuild(openPath);
|
|
122
155
|
}
|
|
123
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
|
+
};
|
|
124
270
|
app.on("key", (event) => {
|
|
125
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
|
+
}
|
|
126
280
|
if (help) {
|
|
127
281
|
help = false;
|
|
128
282
|
app.invalidate();
|
|
129
283
|
return;
|
|
130
284
|
}
|
|
285
|
+
if (mode === "edit" && buffer) {
|
|
286
|
+
editKey(event);
|
|
287
|
+
app.invalidate();
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
131
290
|
if (filtering) {
|
|
132
291
|
if (key === "escape") {
|
|
133
292
|
filtering = false;
|
|
@@ -158,8 +317,24 @@ export async function run(options) {
|
|
|
158
317
|
return;
|
|
159
318
|
}
|
|
160
319
|
switch (key) {
|
|
320
|
+
case "ctrl+c":
|
|
161
321
|
case "q":
|
|
162
|
-
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();
|
|
163
338
|
return;
|
|
164
339
|
case "?":
|
|
165
340
|
help = true;
|
|
@@ -171,12 +346,14 @@ export async function run(options) {
|
|
|
171
346
|
return;
|
|
172
347
|
case "r": {
|
|
173
348
|
const keep = openPath;
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
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
|
+
});
|
|
180
357
|
app.invalidate();
|
|
181
358
|
return;
|
|
182
359
|
}
|
|
@@ -296,7 +473,7 @@ export async function run(options) {
|
|
|
296
473
|
}
|
|
297
474
|
app.invalidate();
|
|
298
475
|
});
|
|
299
|
-
app.render(({ ui, theme, width
|
|
476
|
+
app.render(({ ui, theme, width }) => {
|
|
300
477
|
if (sidebar === 0)
|
|
301
478
|
sidebar = clamp(Math.round(width * 0.28), 22, 42);
|
|
302
479
|
const side = clamp(sidebar, 16, Math.max(16, width - 24));
|
|
@@ -327,14 +504,15 @@ export async function run(options) {
|
|
|
327
504
|
}
|
|
328
505
|
p.list({
|
|
329
506
|
items: flat.map(({ entry, depth }) => {
|
|
330
|
-
const
|
|
507
|
+
const indentation = " ".repeat(depth);
|
|
331
508
|
const glyph = entry.dir ? (entry.expanded ? "▾ " : "▸ ") : " ";
|
|
509
|
+
const mark = entry.path === openPath && dirty() ? "● " : "";
|
|
332
510
|
const color = entry.dir
|
|
333
511
|
? theme.primary
|
|
334
512
|
: entry.path === openPath
|
|
335
513
|
? theme.accent
|
|
336
514
|
: theme.foreground;
|
|
337
|
-
return { label: `${
|
|
515
|
+
return { label: `${indentation}${glyph}${mark}${entry.name}`, color };
|
|
338
516
|
}),
|
|
339
517
|
selected,
|
|
340
518
|
offset: treeOffset,
|
|
@@ -358,11 +536,17 @@ export async function run(options) {
|
|
|
358
536
|
});
|
|
359
537
|
});
|
|
360
538
|
// -------------------------------------------------------- reader
|
|
539
|
+
const editing = mode === "edit" && buffer !== null;
|
|
361
540
|
panes.panel({
|
|
362
|
-
title: openRel ? ` ${openRel} ` : " readm3 ",
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
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,
|
|
366
550
|
padding: [0, 1],
|
|
367
551
|
}, (p) => {
|
|
368
552
|
const w = p.width;
|
|
@@ -371,7 +555,10 @@ export async function run(options) {
|
|
|
371
555
|
p.ctx.hit({
|
|
372
556
|
rect: p.surface.hitRect(),
|
|
373
557
|
onScroll: (delta) => {
|
|
374
|
-
|
|
558
|
+
if (editing && buffer)
|
|
559
|
+
moveVertical(buffer, delta * 3);
|
|
560
|
+
else
|
|
561
|
+
scroll = clamp(scroll + delta * 3, 0, maxScroll());
|
|
375
562
|
app.invalidate();
|
|
376
563
|
},
|
|
377
564
|
onClick: () => {
|
|
@@ -383,15 +570,25 @@ export async function run(options) {
|
|
|
383
570
|
p.text(message, { fg: theme.danger, wrap: true });
|
|
384
571
|
return;
|
|
385
572
|
}
|
|
386
|
-
if (!openPath ||
|
|
573
|
+
if (!openPath || !buffer) {
|
|
387
574
|
p.spacer("fill");
|
|
388
575
|
p.text("readm3", { fg: theme.title, bold: true, align: "center" });
|
|
389
576
|
p.text("Pick a file on the left. ? for keys.", { fg: theme.muted, align: "center" });
|
|
390
577
|
p.spacer("fill");
|
|
391
578
|
return;
|
|
392
579
|
}
|
|
393
|
-
if (
|
|
394
|
-
|
|
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
|
+
};
|
|
395
592
|
}
|
|
396
593
|
scroll = clamp(scroll, 0, Math.max(0, doc.lines.length - h));
|
|
397
594
|
if (measured.total !== doc.lines.length || measured.h !== h) {
|
|
@@ -429,58 +626,145 @@ export async function run(options) {
|
|
|
429
626
|
});
|
|
430
627
|
});
|
|
431
628
|
screen.statusBar({
|
|
432
|
-
items:
|
|
629
|
+
items: mode === "edit"
|
|
433
630
|
? [
|
|
434
|
-
{ key: "
|
|
435
|
-
{ key: "
|
|
436
|
-
{ 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" },
|
|
437
636
|
]
|
|
438
|
-
:
|
|
637
|
+
: filtering
|
|
439
638
|
? [
|
|
440
|
-
{ key: "
|
|
639
|
+
{ key: "type", label: "filter" },
|
|
441
640
|
{ key: "↵", label: "open" },
|
|
442
|
-
{ key: "
|
|
443
|
-
{ key: "/", label: "find" },
|
|
444
|
-
{ key: "tab", label: "reader" },
|
|
445
|
-
{ key: "?", label: "keys" },
|
|
446
|
-
{ key: "q", label: "quit" },
|
|
641
|
+
{ key: "esc", label: "clear" },
|
|
447
642
|
]
|
|
448
|
-
:
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
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
|
+
],
|
|
456
662
|
right: [
|
|
457
663
|
{ label: openPath ? (dirname(openRel) === "." ? basename(root) : dirname(openRel)) : basename(root) },
|
|
664
|
+
{ label: flavor, color: theme.muted },
|
|
458
665
|
{ label: theme.name, color: theme.muted },
|
|
459
666
|
],
|
|
460
667
|
});
|
|
461
|
-
if (
|
|
462
|
-
screen.modal({ title: "
|
|
463
|
-
m.
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
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 });
|
|
477
709
|
m.spacer(1);
|
|
478
710
|
m.text("Any key closes.", { fg: theme.muted, align: "center" });
|
|
479
711
|
});
|
|
480
712
|
}
|
|
481
713
|
});
|
|
482
|
-
void height;
|
|
483
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
|
+
}
|
|
484
768
|
await app.start();
|
|
485
769
|
}
|
|
486
770
|
//# sourceMappingURL=viewer.js.map
|