@nbtca/prompt 1.5.0 → 1.5.1
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/app/app.js +115 -10
- package/dist/app/frame.js +14 -2
- package/dist/app/keys.js +100 -0
- package/dist/app/views/docs-render.js +3 -2
- package/dist/app/views/docs.js +85 -24
- package/dist/app/views/events-render.js +2 -1
- package/dist/app/views/events.js +13 -2
- package/dist/app/views/home.js +46 -24
- package/dist/app/views/schedule-render.js +4 -3
- package/dist/app/views/schedule.js +101 -33
- package/dist/cli.js +570 -0
- package/dist/config/preferences.js +7 -0
- package/dist/core/canvas.js +1 -0
- package/dist/core/components/menu.js +23 -22
- package/dist/core/components/spinner.js +17 -1
- package/dist/core/text.js +7 -3
- package/dist/core/vim-keys.js +149 -6
- package/dist/features/calendar-store.js +27 -0
- package/dist/features/calendar.js +55 -6
- package/dist/features/docs-client.js +225 -0
- package/dist/features/docs.js +224 -68
- package/dist/features/links.js +51 -0
- package/dist/features/status.js +83 -14
- package/dist/features/student-timetable.js +1 -2
- package/dist/features/theme.js +3 -3
- package/dist/features/update.js +3 -2
- package/dist/i18n/locales/en.json +7 -2
- package/dist/i18n/locales/zh.json +7 -2
- package/dist/index.js +5 -498
- package/package.json +2 -1
package/dist/app/app.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { ansi, ensureCursorRestored } from '../core/canvas.js';
|
|
2
|
-
import {
|
|
3
|
-
import { routeGlobalKey } from './keys.js';
|
|
2
|
+
import { composeFrameLines, computeBodyRows, diffFrame } from './frame.js';
|
|
3
|
+
import { isPrintableKey, KeyStreamDecoder, routeGlobalKey } from './keys.js';
|
|
4
4
|
import { renderHeader, renderFooter, resolveChromeLayout } from './chrome.js';
|
|
5
5
|
import { homeView } from './views/home.js';
|
|
6
6
|
import { scheduleView } from './views/schedule.js';
|
|
@@ -8,6 +8,7 @@ import { docsView } from './views/docs.js';
|
|
|
8
8
|
import { eventsView } from './views/events.js';
|
|
9
9
|
import { settingsView } from './views/settings.js';
|
|
10
10
|
import { getAppTabs } from './tabs.js';
|
|
11
|
+
import { SPINNER_FRAME_MS } from '../core/components/spinner.js';
|
|
11
12
|
export async function runApp() {
|
|
12
13
|
if (!process.stdin.isTTY || !process.stdout.isTTY)
|
|
13
14
|
return;
|
|
@@ -15,6 +16,13 @@ export async function runApp() {
|
|
|
15
16
|
let scroll = 0;
|
|
16
17
|
let running = true;
|
|
17
18
|
let suspended = false;
|
|
19
|
+
let entered = false;
|
|
20
|
+
const lifecycle = new AbortController();
|
|
21
|
+
const keyDecoder = new KeyStreamDecoder();
|
|
22
|
+
let keyFlushTimer;
|
|
23
|
+
let clockTimer;
|
|
24
|
+
let busyTimer;
|
|
25
|
+
let painted;
|
|
18
26
|
const viewIds = getAppTabs().map((tab) => tab.id);
|
|
19
27
|
const nativeViews = {
|
|
20
28
|
home: homeView,
|
|
@@ -38,6 +46,7 @@ export async function runApp() {
|
|
|
38
46
|
return { rows: process.stdout.rows || 24, cols: process.stdout.columns || 80 };
|
|
39
47
|
}
|
|
40
48
|
const ctx = {
|
|
49
|
+
signal: lifecycle.signal,
|
|
41
50
|
get size() {
|
|
42
51
|
return size();
|
|
43
52
|
},
|
|
@@ -70,11 +79,29 @@ export async function runApp() {
|
|
|
70
79
|
const footer = renderFooter(view, cols, tabs.length, active?.footerHint?.(tabs.length, cols), chrome.footerLines);
|
|
71
80
|
const body = active?.render(ctx) ?? [];
|
|
72
81
|
const bodyScroll = active?.capturesInput?.() ? Number.MAX_SAFE_INTEGER : scroll;
|
|
73
|
-
|
|
82
|
+
const lines = composeFrameLines(header, body, footer, rows, cols, bodyScroll);
|
|
83
|
+
const patch = diffFrame(painted?.cols === cols ? painted.lines : undefined, lines);
|
|
84
|
+
painted = { cols, lines };
|
|
85
|
+
if (patch)
|
|
86
|
+
process.stdout.write(patch);
|
|
87
|
+
scheduleBusyTick(active?.isBusy?.() === true);
|
|
74
88
|
}
|
|
75
|
-
function
|
|
76
|
-
|
|
89
|
+
function scheduleBusyTick(busy) {
|
|
90
|
+
if (busy && running && busyTimer === undefined) {
|
|
91
|
+
busyTimer = setTimeout(() => {
|
|
92
|
+
busyTimer = undefined;
|
|
93
|
+
render();
|
|
94
|
+
}, SPINNER_FRAME_MS);
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
if (busy || busyTimer === undefined)
|
|
98
|
+
return;
|
|
99
|
+
clearTimeout(busyTimer);
|
|
100
|
+
busyTimer = undefined;
|
|
101
|
+
}
|
|
102
|
+
function dispatchKey(key) {
|
|
77
103
|
if (key === '\x03') {
|
|
104
|
+
process.exitCode = 130;
|
|
78
105
|
quit();
|
|
79
106
|
return;
|
|
80
107
|
} // Ctrl-C always quits, even mid-capture.
|
|
@@ -119,15 +146,73 @@ export async function runApp() {
|
|
|
119
146
|
active?.handleKey?.(key, ctx);
|
|
120
147
|
render();
|
|
121
148
|
}
|
|
149
|
+
function dispatchKeys(keys) {
|
|
150
|
+
for (let index = 0; index < keys.length && running && !suspended; index += 1) {
|
|
151
|
+
let key = keys[index];
|
|
152
|
+
if (key === undefined)
|
|
153
|
+
continue;
|
|
154
|
+
if (nativeViews[view]?.capturesInput?.() && isPrintableKey(key)) {
|
|
155
|
+
while (index + 1 < keys.length) {
|
|
156
|
+
const next = keys[index + 1];
|
|
157
|
+
if (next === undefined || !isPrintableKey(next))
|
|
158
|
+
break;
|
|
159
|
+
key += next;
|
|
160
|
+
index += 1;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
dispatchKey(key);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
function scheduleClock() {
|
|
167
|
+
if (!running)
|
|
168
|
+
return;
|
|
169
|
+
clockTimer = setTimeout(() => {
|
|
170
|
+
clockTimer = undefined;
|
|
171
|
+
render();
|
|
172
|
+
scheduleClock();
|
|
173
|
+
}, 60_000 - (Date.now() % 60_000));
|
|
174
|
+
}
|
|
175
|
+
function clearKeyFlush() {
|
|
176
|
+
if (keyFlushTimer === undefined)
|
|
177
|
+
return;
|
|
178
|
+
clearTimeout(keyFlushTimer);
|
|
179
|
+
keyFlushTimer = undefined;
|
|
180
|
+
}
|
|
181
|
+
function onKey(data) {
|
|
182
|
+
clearKeyFlush();
|
|
183
|
+
dispatchKeys(keyDecoder.write(data));
|
|
184
|
+
if (!running || suspended || !keyDecoder.hasPending)
|
|
185
|
+
return;
|
|
186
|
+
keyFlushTimer = setTimeout(() => {
|
|
187
|
+
keyFlushTimer = undefined;
|
|
188
|
+
dispatchKeys(keyDecoder.flush());
|
|
189
|
+
}, 20);
|
|
190
|
+
}
|
|
122
191
|
function enter() {
|
|
192
|
+
if (entered || lifecycle.signal.aborted)
|
|
193
|
+
return;
|
|
194
|
+
entered = true;
|
|
195
|
+
painted = undefined;
|
|
196
|
+
keyDecoder.reset();
|
|
123
197
|
ensureCursorRestored();
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
process.stdin.
|
|
127
|
-
|
|
128
|
-
|
|
198
|
+
try {
|
|
199
|
+
process.stdout.write(ansi.enterAlt + ansi.hideCursor);
|
|
200
|
+
if (process.stdin.isTTY)
|
|
201
|
+
process.stdin.setRawMode(true);
|
|
202
|
+
process.stdin.resume();
|
|
203
|
+
process.stdin.on('data', onKey);
|
|
204
|
+
}
|
|
205
|
+
catch (error) {
|
|
206
|
+
leave();
|
|
207
|
+
throw error;
|
|
208
|
+
}
|
|
129
209
|
}
|
|
130
210
|
function leave() {
|
|
211
|
+
if (!entered)
|
|
212
|
+
return;
|
|
213
|
+
entered = false;
|
|
214
|
+
clearKeyFlush();
|
|
215
|
+
keyDecoder.reset();
|
|
131
216
|
process.stdin.removeListener('data', onKey);
|
|
132
217
|
if (process.stdin.isTTY)
|
|
133
218
|
process.stdin.setRawMode(false);
|
|
@@ -162,6 +247,15 @@ export async function runApp() {
|
|
|
162
247
|
leave();
|
|
163
248
|
}
|
|
164
249
|
function onSigint() {
|
|
250
|
+
process.exitCode = 130;
|
|
251
|
+
quit();
|
|
252
|
+
}
|
|
253
|
+
function onSigterm() {
|
|
254
|
+
process.exitCode = 143;
|
|
255
|
+
quit();
|
|
256
|
+
}
|
|
257
|
+
function onSighup() {
|
|
258
|
+
process.exitCode = 129;
|
|
165
259
|
quit();
|
|
166
260
|
}
|
|
167
261
|
let resolveRun;
|
|
@@ -172,6 +266,12 @@ export async function runApp() {
|
|
|
172
266
|
if (!running)
|
|
173
267
|
return;
|
|
174
268
|
running = false;
|
|
269
|
+
lifecycle.abort();
|
|
270
|
+
if (clockTimer !== undefined) {
|
|
271
|
+
clearTimeout(clockTimer);
|
|
272
|
+
clockTimer = undefined;
|
|
273
|
+
}
|
|
274
|
+
scheduleBusyTick(false);
|
|
175
275
|
try {
|
|
176
276
|
leave();
|
|
177
277
|
}
|
|
@@ -179,16 +279,21 @@ export async function runApp() {
|
|
|
179
279
|
process.stdout.removeListener('resize', onResize);
|
|
180
280
|
process.removeListener('exit', onExit);
|
|
181
281
|
process.removeListener('SIGINT', onSigint);
|
|
282
|
+
process.removeListener('SIGTERM', onSigterm);
|
|
283
|
+
process.removeListener('SIGHUP', onSighup);
|
|
182
284
|
resolveRun();
|
|
183
285
|
}
|
|
184
286
|
}
|
|
185
287
|
process.on('exit', onExit);
|
|
186
288
|
process.stdout.on('resize', onResize);
|
|
187
289
|
process.once('SIGINT', onSigint);
|
|
290
|
+
process.once('SIGTERM', onSigterm);
|
|
291
|
+
process.once('SIGHUP', onSighup);
|
|
188
292
|
try {
|
|
189
293
|
enter();
|
|
190
294
|
loadView('home');
|
|
191
295
|
render();
|
|
296
|
+
scheduleClock();
|
|
192
297
|
await done;
|
|
193
298
|
}
|
|
194
299
|
finally {
|
package/dist/app/frame.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ansi } from '../core/canvas.js';
|
|
1
2
|
import { clipAnsiToVisualWidth, visualWidth } from '../core/text.js';
|
|
2
3
|
export function clipToWidth(line, cols) {
|
|
3
4
|
if (visualWidth(line) <= cols)
|
|
@@ -17,12 +18,23 @@ export function fitBody(lines, height, scroll, cols) {
|
|
|
17
18
|
out.push(' '.repeat(cols));
|
|
18
19
|
return out;
|
|
19
20
|
}
|
|
20
|
-
export function
|
|
21
|
+
export function composeFrameLines(header, body, footer, rows, cols, scroll) {
|
|
21
22
|
const h = header.map((l) => fitLine(l, cols));
|
|
22
23
|
const f = footer.map((l) => fitLine(l, cols));
|
|
23
24
|
const bodyH = Math.max(0, rows - h.length - f.length);
|
|
24
25
|
const b = fitBody(body, bodyH, scroll, cols);
|
|
25
|
-
return [...h, ...b, ...f].slice(0, rows)
|
|
26
|
+
return [...h, ...b, ...f].slice(0, rows);
|
|
27
|
+
}
|
|
28
|
+
export function diffFrame(prev, next) {
|
|
29
|
+
if (prev?.length !== next.length)
|
|
30
|
+
return ansi.home + next.join('\n') + ansi.eraseDown;
|
|
31
|
+
let out = '';
|
|
32
|
+
for (let row = 0; row < next.length; row += 1) {
|
|
33
|
+
const line = next[row];
|
|
34
|
+
if (line !== undefined && prev[row] !== line)
|
|
35
|
+
out += ansi.cursorToRow(row + 1) + line;
|
|
36
|
+
}
|
|
37
|
+
return out;
|
|
26
38
|
}
|
|
27
39
|
export function computeBodyRows(rows, headerLines, footerLines) {
|
|
28
40
|
return Math.max(0, rows - headerLines - footerLines);
|
package/dist/app/keys.js
CHANGED
|
@@ -1,3 +1,103 @@
|
|
|
1
|
+
const ESC = '\x1b';
|
|
2
|
+
const GRAPHEME_SEGMENTER = new Intl.Segmenter(undefined, { granularity: 'grapheme' });
|
|
3
|
+
function isControl(value) {
|
|
4
|
+
const code = value.charCodeAt(0);
|
|
5
|
+
return code <= 0x1f || (code >= 0x7f && code <= 0x9f);
|
|
6
|
+
}
|
|
7
|
+
export function isPrintableKey(value) {
|
|
8
|
+
if (value.length === 0 || value.startsWith(ESC))
|
|
9
|
+
return false;
|
|
10
|
+
return Array.from(value).every((char) => !isControl(char));
|
|
11
|
+
}
|
|
12
|
+
function escapeSequenceLength(value) {
|
|
13
|
+
if (value.length === 1)
|
|
14
|
+
return null;
|
|
15
|
+
const introducer = value[1];
|
|
16
|
+
if (introducer === ESC || (introducer !== undefined && isControl(introducer)))
|
|
17
|
+
return 1;
|
|
18
|
+
if (introducer === '[' || introducer === 'O') {
|
|
19
|
+
for (let index = 2; index < value.length; index += 1) {
|
|
20
|
+
const code = value.charCodeAt(index);
|
|
21
|
+
if (code >= 0x40 && code <= 0x7e)
|
|
22
|
+
return index + 1;
|
|
23
|
+
if (code < 0x20 || code > 0x3f)
|
|
24
|
+
return 1;
|
|
25
|
+
}
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
if (introducer === ']' || introducer === 'P' || introducer === '^' || introducer === '_') {
|
|
29
|
+
for (let index = 2; index < value.length; index += 1) {
|
|
30
|
+
if (value[index] === '\x07')
|
|
31
|
+
return index + 1;
|
|
32
|
+
if (value[index] === ESC && value[index + 1] === '\\')
|
|
33
|
+
return index + 2;
|
|
34
|
+
}
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
const segment = GRAPHEME_SEGMENTER.segment(value.slice(1))[Symbol.iterator]().next();
|
|
38
|
+
return segment.done ? null : 1 + segment.value.segment.length;
|
|
39
|
+
}
|
|
40
|
+
export class KeyStreamDecoder {
|
|
41
|
+
decoder = new TextDecoder();
|
|
42
|
+
pending = '';
|
|
43
|
+
get hasPending() {
|
|
44
|
+
return this.pending.length > 0;
|
|
45
|
+
}
|
|
46
|
+
write(data) {
|
|
47
|
+
this.pending += typeof data === 'string' ? data : this.decoder.decode(data, { stream: true });
|
|
48
|
+
return this.drain(false);
|
|
49
|
+
}
|
|
50
|
+
flush() {
|
|
51
|
+
return this.drain(true);
|
|
52
|
+
}
|
|
53
|
+
reset() {
|
|
54
|
+
this.pending = '';
|
|
55
|
+
this.decoder = new TextDecoder();
|
|
56
|
+
}
|
|
57
|
+
drain(flush) {
|
|
58
|
+
const keys = [];
|
|
59
|
+
let offset = 0;
|
|
60
|
+
while (offset < this.pending.length) {
|
|
61
|
+
const value = this.pending.slice(offset);
|
|
62
|
+
const first = value[0];
|
|
63
|
+
if (first === undefined)
|
|
64
|
+
break;
|
|
65
|
+
if (first === ESC) {
|
|
66
|
+
const length = escapeSequenceLength(value);
|
|
67
|
+
if (length === null) {
|
|
68
|
+
if (flush) {
|
|
69
|
+
keys.push(value);
|
|
70
|
+
offset = this.pending.length;
|
|
71
|
+
}
|
|
72
|
+
break;
|
|
73
|
+
}
|
|
74
|
+
keys.push(value.slice(0, length));
|
|
75
|
+
offset += length;
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
if (isControl(first)) {
|
|
79
|
+
keys.push(first);
|
|
80
|
+
offset += 1;
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
let end = offset;
|
|
84
|
+
while (end < this.pending.length) {
|
|
85
|
+
const char = this.pending[end];
|
|
86
|
+
if (char === undefined || char === ESC || isControl(char))
|
|
87
|
+
break;
|
|
88
|
+
end += char.length;
|
|
89
|
+
}
|
|
90
|
+
const run = this.pending.slice(offset, end);
|
|
91
|
+
const segments = Array.from(GRAPHEME_SEGMENTER.segment(run), ({ segment }) => segment);
|
|
92
|
+
for (const segment of segments) {
|
|
93
|
+
keys.push(segment);
|
|
94
|
+
offset += segment.length;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
this.pending = this.pending.slice(offset);
|
|
98
|
+
return keys;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
1
101
|
function switchResult(target) {
|
|
2
102
|
return target === undefined ? { handled: true } : { switchTo: target, handled: true };
|
|
3
103
|
}
|
|
@@ -2,6 +2,7 @@ import { type, space } from '../../core/theme.js';
|
|
|
2
2
|
import { t } from '../../i18n/index.js';
|
|
3
3
|
import { renderListFieldWithContext } from '../fields/list-field.js';
|
|
4
4
|
import { visualWidth, wrapAnsiToVisualWidth, wrapAnsiWithIndent } from '../../core/text.js';
|
|
5
|
+
import { loadingLines } from '../../core/components/spinner.js';
|
|
5
6
|
function hintLines(label, cols) {
|
|
6
7
|
return wrapAnsiWithIndent(type.hint(label), cols, space.indent);
|
|
7
8
|
}
|
|
@@ -36,7 +37,7 @@ export function renderDocs(state, cols = 80, bodyRows = Number.POSITIVE_INFINITY
|
|
|
36
37
|
let lines;
|
|
37
38
|
switch (state.mode) {
|
|
38
39
|
case 'loading':
|
|
39
|
-
lines =
|
|
40
|
+
lines = loadingLines(trans.common.loading, cols);
|
|
40
41
|
break;
|
|
41
42
|
case 'sections':
|
|
42
43
|
lines = state.sectionsField?.render(bodyRows, cols) ?? [];
|
|
@@ -66,7 +67,7 @@ export function renderDocs(state, cols = 80, bodyRows = Number.POSITIVE_INFINITY
|
|
|
66
67
|
: [];
|
|
67
68
|
break;
|
|
68
69
|
case 'readerLoading':
|
|
69
|
-
lines =
|
|
70
|
+
lines = loadingLines(trans.docs.loadingFile, cols);
|
|
70
71
|
break;
|
|
71
72
|
case 'reader':
|
|
72
73
|
lines = state.readerLinksField
|
package/dist/app/views/docs.js
CHANGED
|
@@ -4,9 +4,9 @@ import { TextField } from '../fields/text-field.js';
|
|
|
4
4
|
import { renderDocs } from './docs-render.js';
|
|
5
5
|
import { setVimKeysActive } from '../../core/vim-keys.js';
|
|
6
6
|
import { pickIcon } from '../../core/icons.js';
|
|
7
|
-
import { getCurrentLanguage, t } from '../../i18n/index.js';
|
|
7
|
+
import { fmt, getCurrentLanguage, t } from '../../i18n/index.js';
|
|
8
8
|
import { sanitizeTerminalLine, truncate } from '../../core/text.js';
|
|
9
|
-
import { localizeDocSections, fetchSections, fetchDocMetadata, fetchSectionMetadata, searchDocuments, getArchivedGroups, displayDocTitle, loadDocForReader, openDocsInBrowser, clearDocsCache, } from '../../features/docs.js';
|
|
9
|
+
import { localizeDocSections, fetchSections, fetchDocMetadata, fetchSectionMetadata, searchDocuments, getArchivedGroups, displayDocTitle, loadDocForReader, openDocsInBrowser, docsUrlFromPath, clearDocsCache, } from '../../features/docs.js';
|
|
10
10
|
let state = { mode: 'loading' };
|
|
11
11
|
let sections = [];
|
|
12
12
|
let archivedGroups = new Map();
|
|
@@ -23,7 +23,11 @@ let readerNavStack = [];
|
|
|
23
23
|
let readerPrevState = null;
|
|
24
24
|
let readerLoadingPrevState = null;
|
|
25
25
|
let readerRequestId = 0;
|
|
26
|
+
let lifecycleGeneration = 0;
|
|
26
27
|
const DOC_HINT_WIDTH = 44;
|
|
28
|
+
function isLifecycleActive(ctx, generation) {
|
|
29
|
+
return generation === lifecycleGeneration && ctx.signal?.aborted !== true;
|
|
30
|
+
}
|
|
27
31
|
function backLabel() {
|
|
28
32
|
return t().common.back;
|
|
29
33
|
}
|
|
@@ -43,6 +47,27 @@ function withoutErrorMessage(value) {
|
|
|
43
47
|
delete next.errorMessage;
|
|
44
48
|
return next;
|
|
45
49
|
}
|
|
50
|
+
async function runBrowserOpen(ctx, path) {
|
|
51
|
+
let opened;
|
|
52
|
+
await ctx.runClassic(async () => {
|
|
53
|
+
opened =
|
|
54
|
+
path === undefined
|
|
55
|
+
? await openDocsInBrowser(undefined, ctx.signal)
|
|
56
|
+
: await openDocsInBrowser(path, ctx.signal);
|
|
57
|
+
});
|
|
58
|
+
return opened;
|
|
59
|
+
}
|
|
60
|
+
async function openBrowserFromView(ctx, path, generation = lifecycleGeneration) {
|
|
61
|
+
const opened = await runBrowserOpen(ctx, path);
|
|
62
|
+
if (!isLifecycleActive(ctx, generation) || opened === true)
|
|
63
|
+
return;
|
|
64
|
+
const url = docsUrlFromPath(path);
|
|
65
|
+
state = {
|
|
66
|
+
...state,
|
|
67
|
+
errorMessage: sanitizeTerminalLine(`${t().docs.browserError}. ${fmt(t().links.openManually, { url })}`),
|
|
68
|
+
};
|
|
69
|
+
ctx.rerender();
|
|
70
|
+
}
|
|
46
71
|
function buildSectionsField() {
|
|
47
72
|
const trans = t();
|
|
48
73
|
const options = [
|
|
@@ -184,6 +209,9 @@ function replaceSection(section) {
|
|
|
184
209
|
sections = sections.map((current) => (current.key === section.key ? section : current));
|
|
185
210
|
}
|
|
186
211
|
async function openSectionFiles(ctx, section) {
|
|
212
|
+
const generation = lifecycleGeneration;
|
|
213
|
+
if (!isLifecycleActive(ctx, generation))
|
|
214
|
+
return;
|
|
187
215
|
const requestId = ++metadataRequestId;
|
|
188
216
|
currentSectionKey = section.key;
|
|
189
217
|
state = {
|
|
@@ -192,8 +220,9 @@ async function openSectionFiles(ctx, section) {
|
|
|
192
220
|
};
|
|
193
221
|
ctx.rerender();
|
|
194
222
|
try {
|
|
195
|
-
const hydrated = await fetchSectionMetadata(section);
|
|
196
|
-
if (
|
|
223
|
+
const hydrated = await fetchSectionMetadata(section, ctx.signal);
|
|
224
|
+
if (!isLifecycleActive(ctx, generation) ||
|
|
225
|
+
requestId !== metadataRequestId ||
|
|
197
226
|
state.mode !== 'files' ||
|
|
198
227
|
currentSectionKey !== section.key)
|
|
199
228
|
return;
|
|
@@ -205,15 +234,20 @@ async function openSectionFiles(ctx, section) {
|
|
|
205
234
|
};
|
|
206
235
|
}
|
|
207
236
|
catch {
|
|
208
|
-
if (
|
|
237
|
+
if (!isLifecycleActive(ctx, generation) ||
|
|
238
|
+
requestId !== metadataRequestId ||
|
|
209
239
|
state.mode !== 'files' ||
|
|
210
240
|
currentSectionKey !== section.key)
|
|
211
241
|
return;
|
|
212
242
|
state = { ...state, errorMessage: t().docs.loadError };
|
|
213
243
|
}
|
|
214
|
-
ctx
|
|
244
|
+
if (isLifecycleActive(ctx, generation))
|
|
245
|
+
ctx.rerender();
|
|
215
246
|
}
|
|
216
247
|
async function openArchivedFiles(ctx, groupKey, groupFiles) {
|
|
248
|
+
const generation = lifecycleGeneration;
|
|
249
|
+
if (!isLifecycleActive(ctx, generation))
|
|
250
|
+
return;
|
|
217
251
|
const requestId = ++metadataRequestId;
|
|
218
252
|
currentArchivedGroupKey = groupKey;
|
|
219
253
|
state = {
|
|
@@ -222,8 +256,9 @@ async function openArchivedFiles(ctx, groupKey, groupFiles) {
|
|
|
222
256
|
};
|
|
223
257
|
ctx.rerender();
|
|
224
258
|
try {
|
|
225
|
-
const hydrated = await fetchDocMetadata(groupFiles);
|
|
226
|
-
if (
|
|
259
|
+
const hydrated = await fetchDocMetadata(groupFiles, ctx.signal);
|
|
260
|
+
if (!isLifecycleActive(ctx, generation) ||
|
|
261
|
+
requestId !== metadataRequestId ||
|
|
227
262
|
state.mode !== 'archivedFiles' ||
|
|
228
263
|
currentArchivedGroupKey !== groupKey)
|
|
229
264
|
return;
|
|
@@ -234,7 +269,8 @@ async function openArchivedFiles(ctx, groupKey, groupFiles) {
|
|
|
234
269
|
};
|
|
235
270
|
}
|
|
236
271
|
catch {
|
|
237
|
-
if (
|
|
272
|
+
if (!isLifecycleActive(ctx, generation) ||
|
|
273
|
+
requestId !== metadataRequestId ||
|
|
238
274
|
state.mode !== 'archivedFiles' ||
|
|
239
275
|
currentArchivedGroupKey !== groupKey)
|
|
240
276
|
return;
|
|
@@ -243,15 +279,19 @@ async function openArchivedFiles(ctx, groupKey, groupFiles) {
|
|
|
243
279
|
errorMessage: t().docs.loadError,
|
|
244
280
|
};
|
|
245
281
|
}
|
|
246
|
-
ctx
|
|
282
|
+
if (isLifecycleActive(ctx, generation))
|
|
283
|
+
ctx.rerender();
|
|
247
284
|
}
|
|
248
285
|
async function runSearch(ctx, query) {
|
|
286
|
+
const generation = lifecycleGeneration;
|
|
287
|
+
if (!isLifecycleActive(ctx, generation))
|
|
288
|
+
return;
|
|
249
289
|
const requestId = ++searchRequestId;
|
|
250
290
|
state = { mode: 'searchLoading' };
|
|
251
291
|
ctx.rerender();
|
|
252
292
|
try {
|
|
253
|
-
const matches = await searchDocuments(query);
|
|
254
|
-
if (requestId !== searchRequestId)
|
|
293
|
+
const matches = await searchDocuments(query, ctx.signal);
|
|
294
|
+
if (!isLifecycleActive(ctx, generation) || requestId !== searchRequestId)
|
|
255
295
|
return;
|
|
256
296
|
currentSearchResults = matches;
|
|
257
297
|
state = {
|
|
@@ -261,15 +301,19 @@ async function runSearch(ctx, query) {
|
|
|
261
301
|
};
|
|
262
302
|
}
|
|
263
303
|
catch {
|
|
264
|
-
if (requestId !== searchRequestId)
|
|
304
|
+
if (!isLifecycleActive(ctx, generation) || requestId !== searchRequestId)
|
|
265
305
|
return;
|
|
266
306
|
currentSearchResults = [];
|
|
267
307
|
goToSections();
|
|
268
308
|
state = { ...state, errorMessage: t().docs.loadError };
|
|
269
309
|
}
|
|
270
|
-
ctx
|
|
310
|
+
if (isLifecycleActive(ctx, generation))
|
|
311
|
+
ctx.rerender();
|
|
271
312
|
}
|
|
272
313
|
async function openInReader(ctx, path, pushCurrent) {
|
|
314
|
+
const generation = lifecycleGeneration;
|
|
315
|
+
if (!isLifecycleActive(ctx, generation))
|
|
316
|
+
return;
|
|
273
317
|
const requestId = ++readerRequestId;
|
|
274
318
|
const previousState = state;
|
|
275
319
|
const previousPath = readerCurrentPath;
|
|
@@ -277,8 +321,8 @@ async function openInReader(ctx, path, pushCurrent) {
|
|
|
277
321
|
state = { mode: 'readerLoading' };
|
|
278
322
|
ctx.rerender();
|
|
279
323
|
try {
|
|
280
|
-
const doc = await loadDocForReader(path);
|
|
281
|
-
if (requestId !== readerRequestId)
|
|
324
|
+
const doc = await loadDocForReader(path, ctx.signal);
|
|
325
|
+
if (!isLifecycleActive(ctx, generation) || requestId !== readerRequestId)
|
|
282
326
|
return;
|
|
283
327
|
if (pushCurrent && previousPath)
|
|
284
328
|
readerNavStack.push(previousPath);
|
|
@@ -293,7 +337,7 @@ async function openInReader(ctx, path, pushCurrent) {
|
|
|
293
337
|
ctx.resetScroll();
|
|
294
338
|
}
|
|
295
339
|
catch {
|
|
296
|
-
if (requestId !== readerRequestId)
|
|
340
|
+
if (!isLifecycleActive(ctx, generation) || requestId !== readerRequestId)
|
|
297
341
|
return;
|
|
298
342
|
readerLoadingPrevState = null;
|
|
299
343
|
const fallbackState = pushCurrent && previousState.mode === 'reader'
|
|
@@ -301,7 +345,8 @@ async function openInReader(ctx, path, pushCurrent) {
|
|
|
301
345
|
: previousState;
|
|
302
346
|
state = { ...fallbackState, errorMessage: t().docs.loadError };
|
|
303
347
|
}
|
|
304
|
-
ctx
|
|
348
|
+
if (isLifecycleActive(ctx, generation))
|
|
349
|
+
ctx.rerender();
|
|
305
350
|
}
|
|
306
351
|
function enterReaderFrom(ctx, path) {
|
|
307
352
|
readerPrevState = state;
|
|
@@ -313,6 +358,9 @@ export const docsView = {
|
|
|
313
358
|
id: 'docs',
|
|
314
359
|
title: t().menu.docs,
|
|
315
360
|
async load(ctx) {
|
|
361
|
+
const generation = loaded ? lifecycleGeneration : ++lifecycleGeneration;
|
|
362
|
+
if (!isLifecycleActive(ctx, generation))
|
|
363
|
+
return;
|
|
316
364
|
if (loaded) {
|
|
317
365
|
const language = getCurrentLanguage();
|
|
318
366
|
if (loadedLanguage !== language) {
|
|
@@ -330,8 +378,8 @@ export const docsView = {
|
|
|
330
378
|
state = { mode: 'loading' };
|
|
331
379
|
ctx.rerender();
|
|
332
380
|
try {
|
|
333
|
-
const nextSections = await fetchSections();
|
|
334
|
-
if (requestId !== sectionsRequestId)
|
|
381
|
+
const nextSections = await fetchSections(ctx.signal);
|
|
382
|
+
if (!isLifecycleActive(ctx, generation) || requestId !== sectionsRequestId)
|
|
335
383
|
return;
|
|
336
384
|
sections = nextSections;
|
|
337
385
|
loaded = true;
|
|
@@ -339,11 +387,21 @@ export const docsView = {
|
|
|
339
387
|
goToSections();
|
|
340
388
|
}
|
|
341
389
|
catch {
|
|
342
|
-
if (requestId !== sectionsRequestId)
|
|
390
|
+
if (!isLifecycleActive(ctx, generation) || requestId !== sectionsRequestId)
|
|
343
391
|
return;
|
|
344
392
|
state = { mode: 'error', errorMessage: t().docs.loadError };
|
|
345
393
|
}
|
|
346
|
-
ctx
|
|
394
|
+
if (isLifecycleActive(ctx, generation))
|
|
395
|
+
ctx.rerender();
|
|
396
|
+
},
|
|
397
|
+
dispose() {
|
|
398
|
+
lifecycleGeneration += 1;
|
|
399
|
+
sectionsRequestId += 1;
|
|
400
|
+
metadataRequestId += 1;
|
|
401
|
+
searchRequestId += 1;
|
|
402
|
+
readerRequestId += 1;
|
|
403
|
+
clearDocsCache();
|
|
404
|
+
setVimKeysActive(true);
|
|
347
405
|
},
|
|
348
406
|
render(ctx) {
|
|
349
407
|
const maxVisible = computeMaxVisible(ctx.bodyRows);
|
|
@@ -354,6 +412,9 @@ export const docsView = {
|
|
|
354
412
|
state.readerLinksField?.setMaxVisible(maxVisible);
|
|
355
413
|
return renderDocs(state, ctx.size.cols, ctx.bodyRows);
|
|
356
414
|
},
|
|
415
|
+
isBusy() {
|
|
416
|
+
return (state.mode === 'loading' || state.mode === 'searchLoading' || state.mode === 'readerLoading');
|
|
417
|
+
},
|
|
357
418
|
capturesInput() {
|
|
358
419
|
return state.mode === 'search';
|
|
359
420
|
},
|
|
@@ -479,7 +540,7 @@ export const docsView = {
|
|
|
479
540
|
return;
|
|
480
541
|
}
|
|
481
542
|
if (result.selected === '__browser__') {
|
|
482
|
-
void ctx
|
|
543
|
+
void openBrowserFromView(ctx);
|
|
483
544
|
return;
|
|
484
545
|
}
|
|
485
546
|
const section = sections.find((s) => s.key === result.selected);
|
|
@@ -586,7 +647,7 @@ export const docsView = {
|
|
|
586
647
|
return;
|
|
587
648
|
}
|
|
588
649
|
if (key === 'b') {
|
|
589
|
-
void ctx
|
|
650
|
+
void openBrowserFromView(ctx, readerCurrentPath ?? undefined);
|
|
590
651
|
return;
|
|
591
652
|
}
|
|
592
653
|
return;
|
|
@@ -4,6 +4,7 @@ import { renderListFieldWithContext } from '../fields/list-field.js';
|
|
|
4
4
|
import { renderCountdownBanner, renderEventBrief } from '../../features/calendar.js';
|
|
5
5
|
import { renderHeatmap } from '../../features/calendar-heatmap.js';
|
|
6
6
|
import { wrapAnsiWithIndent } from '../../core/text.js';
|
|
7
|
+
import { loadingLines } from '../../core/components/spinner.js';
|
|
7
8
|
function wrappedIndentedLines(label, cols, style) {
|
|
8
9
|
return wrapAnsiWithIndent(style(label), cols ?? Number.POSITIVE_INFINITY, space.indent);
|
|
9
10
|
}
|
|
@@ -61,7 +62,7 @@ export function renderEvents(state, now, bodyRows = 100, cols) {
|
|
|
61
62
|
const trans = t();
|
|
62
63
|
switch (state.mode) {
|
|
63
64
|
case 'loading':
|
|
64
|
-
return
|
|
65
|
+
return loadingLines(trans.calendar.loading, cols);
|
|
65
66
|
case 'hub':
|
|
66
67
|
return renderHubBody(state, now, bodyRows, cols);
|
|
67
68
|
case 'heatmap':
|
package/dist/app/views/events.js
CHANGED
|
@@ -93,21 +93,32 @@ export const eventsView = {
|
|
|
93
93
|
id: 'events',
|
|
94
94
|
title: t().menu.events,
|
|
95
95
|
async load(ctx) {
|
|
96
|
+
if (ctx.signal?.aborted)
|
|
97
|
+
return;
|
|
96
98
|
state = { mode: 'loading' };
|
|
97
99
|
ctx.rerender();
|
|
98
100
|
try {
|
|
99
|
-
|
|
101
|
+
const loadedCalendar = await loadCalendarOrThrow(ctx.signal);
|
|
102
|
+
if (ctx.signal?.aborted)
|
|
103
|
+
return;
|
|
104
|
+
calendar = loadedCalendar;
|
|
100
105
|
goToHub();
|
|
101
106
|
}
|
|
102
107
|
catch {
|
|
108
|
+
if (ctx.signal?.aborted)
|
|
109
|
+
return;
|
|
103
110
|
state = { mode: 'error', errorMessage: t().calendar.error };
|
|
104
111
|
}
|
|
105
|
-
ctx.
|
|
112
|
+
if (!ctx.signal?.aborted)
|
|
113
|
+
ctx.rerender();
|
|
106
114
|
},
|
|
107
115
|
render(ctx) {
|
|
108
116
|
state.listField?.setMaxVisible(computeMaxVisible(ctx.bodyRows));
|
|
109
117
|
return renderEvents(state, new Date(), ctx.bodyRows, ctx.size.cols);
|
|
110
118
|
},
|
|
119
|
+
isBusy() {
|
|
120
|
+
return state.mode === 'loading';
|
|
121
|
+
},
|
|
111
122
|
capturesInput() {
|
|
112
123
|
return state.mode === 'search';
|
|
113
124
|
},
|