@nbtca/prompt 1.4.2 → 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/README.md +27 -58
- package/SECURITY.md +16 -45
- package/dist/app/app.js +167 -64
- package/dist/app/chrome.js +67 -50
- package/dist/app/fields/list-field.js +12 -25
- package/dist/app/fields/text-field.js +3 -8
- package/dist/app/frame.js +16 -23
- package/dist/app/keys.js +110 -2
- package/dist/app/views/docs-render.js +34 -26
- package/dist/app/views/docs.js +280 -68
- package/dist/app/views/events-render.js +21 -27
- package/dist/app/views/events.js +57 -33
- package/dist/app/views/home.js +67 -47
- package/dist/app/views/schedule-grid-cursor.js +9 -18
- package/dist/app/views/schedule-render.js +51 -74
- package/dist/app/views/schedule.js +246 -101
- package/dist/app/views/settings-render.js +8 -19
- package/dist/app/views/settings.js +92 -17
- package/dist/auth/cookie-transport.js +31 -32
- package/dist/auth/errors.js +3 -1
- package/dist/auth/nbt-auth.js +42 -25
- package/dist/auth/session-store.js +17 -9
- package/dist/cli.js +570 -0
- package/dist/config/data.js +9 -11
- package/dist/config/preferences.js +21 -7
- package/dist/core/calendar-day.js +37 -0
- package/dist/core/canvas.js +1 -0
- package/dist/core/capabilities.js +6 -3
- package/dist/core/components/confirm.js +9 -8
- package/dist/core/components/menu.js +64 -38
- package/dist/core/components/messages.js +12 -4
- package/dist/core/components/painter.js +3 -1
- package/dist/core/components/spinner.js +34 -7
- package/dist/core/components/text-input.js +24 -18
- package/dist/core/icons.js +2 -2
- package/dist/core/logo.js +23 -5
- package/dist/core/motion.js +25 -19
- package/dist/core/text.js +186 -69
- package/dist/core/theme.js +0 -28
- package/dist/core/transitions.js +2 -2
- package/dist/core/ui.js +15 -13
- package/dist/core/vim-keys.js +156 -19
- package/dist/features/about.js +23 -0
- package/dist/features/calendar-heatmap.js +16 -40
- package/dist/features/calendar-query.js +1 -2
- package/dist/features/calendar-store.js +27 -0
- package/dist/features/calendar.js +66 -190
- package/dist/features/docs-client.js +225 -0
- package/dist/features/docs.js +615 -298
- package/dist/features/links.js +44 -29
- package/dist/features/schedule-render.js +65 -101
- package/dist/features/schedule-store.js +51 -9
- package/dist/features/schedule-view.js +46 -213
- package/dist/features/status.js +117 -60
- package/dist/features/student-timetable.js +74 -97
- package/dist/features/theme.js +9 -5
- package/dist/features/timetable-sanitize.js +40 -0
- package/dist/features/update.js +12 -29
- package/dist/i18n/index.js +83 -19
- package/dist/i18n/locales/en.json +8 -3
- package/dist/i18n/locales/zh.json +8 -3
- package/dist/index.js +6 -474
- package/dist/logo/ca-dotmatrix.txt +16 -18
- package/dist/main.js +7 -48
- package/package.json +28 -18
- package/bin/nbtca-welcome.js +0 -2
- package/dist/core/components/screen.js +0 -18
- package/dist/core/menu.js +0 -68
- package/dist/features/schedule-query.js +0 -47
- package/dist/features/settings.js +0 -127
- package/dist/logo/ca-logo.png +0 -0
package/dist/app/views/docs.js
CHANGED
|
@@ -4,8 +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';
|
|
8
|
-
import {
|
|
7
|
+
import { fmt, getCurrentLanguage, t } from '../../i18n/index.js';
|
|
8
|
+
import { sanitizeTerminalLine, truncate } from '../../core/text.js';
|
|
9
|
+
import { localizeDocSections, fetchSections, fetchDocMetadata, fetchSectionMetadata, searchDocuments, getArchivedGroups, displayDocTitle, loadDocForReader, openDocsInBrowser, docsUrlFromPath, clearDocsCache, } from '../../features/docs.js';
|
|
9
10
|
let state = { mode: 'loading' };
|
|
10
11
|
let sections = [];
|
|
11
12
|
let archivedGroups = new Map();
|
|
@@ -14,19 +15,59 @@ let loadedLanguage = null;
|
|
|
14
15
|
let currentSectionKey = null;
|
|
15
16
|
let currentArchivedGroupKey = null;
|
|
16
17
|
let currentSearchResults = [];
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
// whichever file-listing state (files/archivedFiles/searchResults) the
|
|
21
|
-
// reader was entered from, restored once the nav stack empties.
|
|
18
|
+
let sectionsRequestId = 0;
|
|
19
|
+
let metadataRequestId = 0;
|
|
20
|
+
let searchRequestId = 0;
|
|
22
21
|
let readerCurrentPath = null;
|
|
23
22
|
let readerNavStack = [];
|
|
24
23
|
let readerPrevState = null;
|
|
25
24
|
let readerLoadingPrevState = null;
|
|
26
25
|
let readerRequestId = 0;
|
|
26
|
+
let lifecycleGeneration = 0;
|
|
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
|
}
|
|
34
|
+
function optionalHint(hint) {
|
|
35
|
+
return hint === undefined ? {} : { hint };
|
|
36
|
+
}
|
|
37
|
+
function docHint(value) {
|
|
38
|
+
return value ? truncate(value, DOC_HINT_WIDTH) : undefined;
|
|
39
|
+
}
|
|
40
|
+
function withoutReaderLinksField(value) {
|
|
41
|
+
const next = { ...value };
|
|
42
|
+
delete next.readerLinksField;
|
|
43
|
+
return next;
|
|
44
|
+
}
|
|
45
|
+
function withoutErrorMessage(value) {
|
|
46
|
+
const next = { ...value };
|
|
47
|
+
delete next.errorMessage;
|
|
48
|
+
return next;
|
|
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
|
+
}
|
|
30
71
|
function buildSectionsField() {
|
|
31
72
|
const trans = t();
|
|
32
73
|
const options = [
|
|
@@ -39,12 +80,16 @@ function buildSectionsField() {
|
|
|
39
80
|
}
|
|
40
81
|
function buildFilesField(section, maxVisible, initialIndex = 0) {
|
|
41
82
|
const trans = t();
|
|
42
|
-
const isIndex = (
|
|
83
|
+
const isIndex = (file) => file.name === 'index.md' || file.name.startsWith('index.');
|
|
43
84
|
const index = section.files.find(isIndex);
|
|
44
85
|
const files = section.files.filter((f) => !isIndex(f));
|
|
45
86
|
const options = [
|
|
46
87
|
...(index ? [{ value: index.path, label: trans.docs.overviewLabel }] : []),
|
|
47
|
-
...files.map((
|
|
88
|
+
...files.map((file) => ({
|
|
89
|
+
value: file.path,
|
|
90
|
+
label: displayDocTitle(file.name, file.title),
|
|
91
|
+
...optionalHint(docHint(file.summary)),
|
|
92
|
+
})),
|
|
48
93
|
{ value: '__back__', label: backLabel() },
|
|
49
94
|
];
|
|
50
95
|
return new ListField({ title: section.label, options, maxVisible, initialIndex });
|
|
@@ -63,7 +108,7 @@ function buildArchivedGroupsField(groups, maxVisible, initialIndex = 0) {
|
|
|
63
108
|
return a.localeCompare(b);
|
|
64
109
|
});
|
|
65
110
|
const options = [
|
|
66
|
-
...sortedKeys.map((k) => ({ value: k, label: k, hint: String(groups.get(k)
|
|
111
|
+
...sortedKeys.map((k) => ({ value: k, label: k, hint: String(groups.get(k)?.length ?? 0) })),
|
|
67
112
|
{ value: '__back__', label: backLabel() },
|
|
68
113
|
];
|
|
69
114
|
return new ListField({ title: trans.docs.categoryArchived, options, maxVisible, initialIndex });
|
|
@@ -74,11 +119,22 @@ function buildArchivedFilesField(groupKey, groupFiles, maxVisible, initialIndex
|
|
|
74
119
|
const options = [
|
|
75
120
|
...groupFiles.map((f) => {
|
|
76
121
|
const sub = f.path.split('/').slice(2, -1).join('/');
|
|
77
|
-
return {
|
|
122
|
+
return {
|
|
123
|
+
value: f.path,
|
|
124
|
+
label: displayDocTitle(f.name, f.title),
|
|
125
|
+
...optionalHint(docHint(subDirs.size > 1
|
|
126
|
+
? [sanitizeTerminalLine(sub), f.summary].filter(Boolean).join(' · ')
|
|
127
|
+
: f.summary)),
|
|
128
|
+
};
|
|
78
129
|
}),
|
|
79
130
|
{ value: '__back__', label: backLabel() },
|
|
80
131
|
];
|
|
81
|
-
return new ListField({
|
|
132
|
+
return new ListField({
|
|
133
|
+
title: `${trans.docs.categoryArchived} · ${groupKey}`,
|
|
134
|
+
options,
|
|
135
|
+
maxVisible,
|
|
136
|
+
initialIndex,
|
|
137
|
+
});
|
|
82
138
|
}
|
|
83
139
|
function buildReaderLinksField(links, maxVisible, initialIndex = 0) {
|
|
84
140
|
const trans = t();
|
|
@@ -93,8 +149,12 @@ function buildSearchResultsField(matches, maxVisible, initialIndex = 0) {
|
|
|
93
149
|
const options = [
|
|
94
150
|
...matches.map((result) => ({
|
|
95
151
|
value: result.path,
|
|
96
|
-
label: displayDocTitle(result.
|
|
97
|
-
|
|
152
|
+
label: displayDocTitle(result.name, result.title),
|
|
153
|
+
...optionalHint(docHint(result.excerpt ||
|
|
154
|
+
result.summary ||
|
|
155
|
+
(result.path.includes('/')
|
|
156
|
+
? sanitizeTerminalLine(result.path.split('/').slice(0, -1).join('/'))
|
|
157
|
+
: undefined))),
|
|
98
158
|
})),
|
|
99
159
|
{ value: '__back__', label: backLabel() },
|
|
100
160
|
];
|
|
@@ -107,7 +167,10 @@ function relocalizeStateFields(value, maxVisible) {
|
|
|
107
167
|
if (value.mode === 'files' && currentSectionKey) {
|
|
108
168
|
const section = sections.find((candidate) => candidate.key === currentSectionKey);
|
|
109
169
|
return section
|
|
110
|
-
? {
|
|
170
|
+
? {
|
|
171
|
+
...value,
|
|
172
|
+
filesField: buildFilesField(section, maxVisible, value.filesField?.selectedIndex),
|
|
173
|
+
}
|
|
111
174
|
: value;
|
|
112
175
|
}
|
|
113
176
|
if (value.mode === 'archivedGroups') {
|
|
@@ -142,11 +205,115 @@ function goToSections() {
|
|
|
142
205
|
currentSearchResults = [];
|
|
143
206
|
state = { mode: 'sections', sectionsField: buildSectionsField() };
|
|
144
207
|
}
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
208
|
+
function replaceSection(section) {
|
|
209
|
+
sections = sections.map((current) => (current.key === section.key ? section : current));
|
|
210
|
+
}
|
|
211
|
+
async function openSectionFiles(ctx, section) {
|
|
212
|
+
const generation = lifecycleGeneration;
|
|
213
|
+
if (!isLifecycleActive(ctx, generation))
|
|
214
|
+
return;
|
|
215
|
+
const requestId = ++metadataRequestId;
|
|
216
|
+
currentSectionKey = section.key;
|
|
217
|
+
state = {
|
|
218
|
+
mode: 'files',
|
|
219
|
+
filesField: buildFilesField(section, computeMaxVisible(ctx.bodyRows)),
|
|
220
|
+
};
|
|
221
|
+
ctx.rerender();
|
|
222
|
+
try {
|
|
223
|
+
const hydrated = await fetchSectionMetadata(section, ctx.signal);
|
|
224
|
+
if (!isLifecycleActive(ctx, generation) ||
|
|
225
|
+
requestId !== metadataRequestId ||
|
|
226
|
+
state.mode !== 'files' ||
|
|
227
|
+
currentSectionKey !== section.key)
|
|
228
|
+
return;
|
|
229
|
+
const localized = localizeDocSections([hydrated], t())[0] ?? hydrated;
|
|
230
|
+
replaceSection(localized);
|
|
231
|
+
state = {
|
|
232
|
+
mode: 'files',
|
|
233
|
+
filesField: buildFilesField(localized, computeMaxVisible(ctx.bodyRows), state.filesField?.selectedIndex),
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
catch {
|
|
237
|
+
if (!isLifecycleActive(ctx, generation) ||
|
|
238
|
+
requestId !== metadataRequestId ||
|
|
239
|
+
state.mode !== 'files' ||
|
|
240
|
+
currentSectionKey !== section.key)
|
|
241
|
+
return;
|
|
242
|
+
state = { ...state, errorMessage: t().docs.loadError };
|
|
243
|
+
}
|
|
244
|
+
if (isLifecycleActive(ctx, generation))
|
|
245
|
+
ctx.rerender();
|
|
246
|
+
}
|
|
247
|
+
async function openArchivedFiles(ctx, groupKey, groupFiles) {
|
|
248
|
+
const generation = lifecycleGeneration;
|
|
249
|
+
if (!isLifecycleActive(ctx, generation))
|
|
250
|
+
return;
|
|
251
|
+
const requestId = ++metadataRequestId;
|
|
252
|
+
currentArchivedGroupKey = groupKey;
|
|
253
|
+
state = {
|
|
254
|
+
mode: 'archivedFiles',
|
|
255
|
+
archivedFilesField: buildArchivedFilesField(groupKey, groupFiles, computeMaxVisible(ctx.bodyRows)),
|
|
256
|
+
};
|
|
257
|
+
ctx.rerender();
|
|
258
|
+
try {
|
|
259
|
+
const hydrated = await fetchDocMetadata(groupFiles, ctx.signal);
|
|
260
|
+
if (!isLifecycleActive(ctx, generation) ||
|
|
261
|
+
requestId !== metadataRequestId ||
|
|
262
|
+
state.mode !== 'archivedFiles' ||
|
|
263
|
+
currentArchivedGroupKey !== groupKey)
|
|
264
|
+
return;
|
|
265
|
+
archivedGroups.set(groupKey, hydrated);
|
|
266
|
+
state = {
|
|
267
|
+
mode: 'archivedFiles',
|
|
268
|
+
archivedFilesField: buildArchivedFilesField(groupKey, hydrated, computeMaxVisible(ctx.bodyRows), state.archivedFilesField?.selectedIndex),
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
catch {
|
|
272
|
+
if (!isLifecycleActive(ctx, generation) ||
|
|
273
|
+
requestId !== metadataRequestId ||
|
|
274
|
+
state.mode !== 'archivedFiles' ||
|
|
275
|
+
currentArchivedGroupKey !== groupKey)
|
|
276
|
+
return;
|
|
277
|
+
state = {
|
|
278
|
+
...state,
|
|
279
|
+
errorMessage: t().docs.loadError,
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
if (isLifecycleActive(ctx, generation))
|
|
283
|
+
ctx.rerender();
|
|
284
|
+
}
|
|
285
|
+
async function runSearch(ctx, query) {
|
|
286
|
+
const generation = lifecycleGeneration;
|
|
287
|
+
if (!isLifecycleActive(ctx, generation))
|
|
288
|
+
return;
|
|
289
|
+
const requestId = ++searchRequestId;
|
|
290
|
+
state = { mode: 'searchLoading' };
|
|
291
|
+
ctx.rerender();
|
|
292
|
+
try {
|
|
293
|
+
const matches = await searchDocuments(query, ctx.signal);
|
|
294
|
+
if (!isLifecycleActive(ctx, generation) || requestId !== searchRequestId)
|
|
295
|
+
return;
|
|
296
|
+
currentSearchResults = matches;
|
|
297
|
+
state = {
|
|
298
|
+
mode: 'searchResults',
|
|
299
|
+
searchResultsEmpty: matches.length === 0,
|
|
300
|
+
searchResultsField: buildSearchResultsField(matches, computeMaxVisible(ctx.bodyRows)),
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
catch {
|
|
304
|
+
if (!isLifecycleActive(ctx, generation) || requestId !== searchRequestId)
|
|
305
|
+
return;
|
|
306
|
+
currentSearchResults = [];
|
|
307
|
+
goToSections();
|
|
308
|
+
state = { ...state, errorMessage: t().docs.loadError };
|
|
309
|
+
}
|
|
310
|
+
if (isLifecycleActive(ctx, generation))
|
|
311
|
+
ctx.rerender();
|
|
312
|
+
}
|
|
149
313
|
async function openInReader(ctx, path, pushCurrent) {
|
|
314
|
+
const generation = lifecycleGeneration;
|
|
315
|
+
if (!isLifecycleActive(ctx, generation))
|
|
316
|
+
return;
|
|
150
317
|
const requestId = ++readerRequestId;
|
|
151
318
|
const previousState = state;
|
|
152
319
|
const previousPath = readerCurrentPath;
|
|
@@ -154,30 +321,33 @@ async function openInReader(ctx, path, pushCurrent) {
|
|
|
154
321
|
state = { mode: 'readerLoading' };
|
|
155
322
|
ctx.rerender();
|
|
156
323
|
try {
|
|
157
|
-
const doc = await loadDocForReader(path);
|
|
158
|
-
if (requestId !== readerRequestId)
|
|
324
|
+
const doc = await loadDocForReader(path, ctx.signal);
|
|
325
|
+
if (!isLifecycleActive(ctx, generation) || requestId !== readerRequestId)
|
|
159
326
|
return;
|
|
160
327
|
if (pushCurrent && previousPath)
|
|
161
328
|
readerNavStack.push(previousPath);
|
|
162
329
|
readerCurrentPath = path;
|
|
163
330
|
readerLoadingPrevState = null;
|
|
164
|
-
state = {
|
|
331
|
+
state = {
|
|
332
|
+
mode: 'reader',
|
|
333
|
+
readerTitle: doc.title,
|
|
334
|
+
readerLines: doc.lines,
|
|
335
|
+
readerLinks: doc.links,
|
|
336
|
+
};
|
|
165
337
|
ctx.resetScroll();
|
|
166
338
|
}
|
|
167
339
|
catch {
|
|
168
|
-
if (requestId !== readerRequestId)
|
|
340
|
+
if (!isLifecycleActive(ctx, generation) || requestId !== readerRequestId)
|
|
169
341
|
return;
|
|
170
342
|
readerLoadingPrevState = null;
|
|
171
343
|
const fallbackState = pushCurrent && previousState.mode === 'reader'
|
|
172
|
-
?
|
|
344
|
+
? withoutReaderLinksField(previousState)
|
|
173
345
|
: previousState;
|
|
174
346
|
state = { ...fallbackState, errorMessage: t().docs.loadError };
|
|
175
347
|
}
|
|
176
|
-
ctx
|
|
348
|
+
if (isLifecycleActive(ctx, generation))
|
|
349
|
+
ctx.rerender();
|
|
177
350
|
}
|
|
178
|
-
/** Enters the reader from a file-listing mode (files/archivedFiles/
|
|
179
|
-
* searchResults) -- saves that listing so Esc can restore it once the nav
|
|
180
|
-
* stack (built by following links from here) empties back out. */
|
|
181
351
|
function enterReaderFrom(ctx, path) {
|
|
182
352
|
readerPrevState = state;
|
|
183
353
|
readerNavStack = [];
|
|
@@ -188,6 +358,9 @@ export const docsView = {
|
|
|
188
358
|
id: 'docs',
|
|
189
359
|
title: t().menu.docs,
|
|
190
360
|
async load(ctx) {
|
|
361
|
+
const generation = loaded ? lifecycleGeneration : ++lifecycleGeneration;
|
|
362
|
+
if (!isLifecycleActive(ctx, generation))
|
|
363
|
+
return;
|
|
191
364
|
if (loaded) {
|
|
192
365
|
const language = getCurrentLanguage();
|
|
193
366
|
if (loadedLanguage !== language) {
|
|
@@ -201,23 +374,36 @@ export const docsView = {
|
|
|
201
374
|
}
|
|
202
375
|
return;
|
|
203
376
|
}
|
|
377
|
+
const requestId = ++sectionsRequestId;
|
|
204
378
|
state = { mode: 'loading' };
|
|
205
379
|
ctx.rerender();
|
|
206
380
|
try {
|
|
207
|
-
|
|
381
|
+
const nextSections = await fetchSections(ctx.signal);
|
|
382
|
+
if (!isLifecycleActive(ctx, generation) || requestId !== sectionsRequestId)
|
|
383
|
+
return;
|
|
384
|
+
sections = nextSections;
|
|
208
385
|
loaded = true;
|
|
209
386
|
loadedLanguage = getCurrentLanguage();
|
|
210
387
|
goToSections();
|
|
211
388
|
}
|
|
212
389
|
catch {
|
|
390
|
+
if (!isLifecycleActive(ctx, generation) || requestId !== sectionsRequestId)
|
|
391
|
+
return;
|
|
213
392
|
state = { mode: 'error', errorMessage: t().docs.loadError };
|
|
214
393
|
}
|
|
215
|
-
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);
|
|
216
405
|
},
|
|
217
406
|
render(ctx) {
|
|
218
|
-
// Sync every visible field's scroll window to the *current* terminal
|
|
219
|
-
// size on every frame (not just construction time) — this is what
|
|
220
|
-
// keeps a long list correctly windowed across a live resize.
|
|
221
407
|
const maxVisible = computeMaxVisible(ctx.bodyRows);
|
|
222
408
|
state.filesField?.setMaxVisible(maxVisible);
|
|
223
409
|
state.archivedGroupsField?.setMaxVisible(maxVisible);
|
|
@@ -226,13 +412,24 @@ export const docsView = {
|
|
|
226
412
|
state.readerLinksField?.setMaxVisible(maxVisible);
|
|
227
413
|
return renderDocs(state, ctx.size.cols, ctx.bodyRows);
|
|
228
414
|
},
|
|
415
|
+
isBusy() {
|
|
416
|
+
return (state.mode === 'loading' || state.mode === 'searchLoading' || state.mode === 'readerLoading');
|
|
417
|
+
},
|
|
229
418
|
capturesInput() {
|
|
230
419
|
return state.mode === 'search';
|
|
231
420
|
},
|
|
421
|
+
capturesPageKeys() {
|
|
422
|
+
return (state.mode === 'sections' ||
|
|
423
|
+
state.mode === 'files' ||
|
|
424
|
+
state.mode === 'archivedGroups' ||
|
|
425
|
+
state.mode === 'archivedFiles' ||
|
|
426
|
+
state.mode === 'searchResults' ||
|
|
427
|
+
(state.mode === 'reader' && state.readerLinksField !== undefined));
|
|
428
|
+
},
|
|
232
429
|
footerHint(tabCount, cols = Number.POSITIVE_INFINITY) {
|
|
233
430
|
if (state.mode === 'search')
|
|
234
431
|
return captureFooterHint(cols);
|
|
235
|
-
if (state.mode === 'loading' || state.mode === 'error')
|
|
432
|
+
if (state.mode === 'loading' || state.mode === 'searchLoading' || state.mode === 'error')
|
|
236
433
|
return passiveFooterHint(tabCount, cols);
|
|
237
434
|
if (state.mode === 'readerLoading') {
|
|
238
435
|
return fitFooterHint(cols, `${digitTabHint(tabCount)}q ${t().menu.hintQuit}`, `${digitTabHint(tabCount)}q`, 'q');
|
|
@@ -250,6 +447,11 @@ export const docsView = {
|
|
|
250
447
|
return undefined;
|
|
251
448
|
},
|
|
252
449
|
handleBack(ctx) {
|
|
450
|
+
if (state.mode === 'searchLoading') {
|
|
451
|
+
searchRequestId++;
|
|
452
|
+
goToSections();
|
|
453
|
+
return true;
|
|
454
|
+
}
|
|
253
455
|
if (state.mode === 'reader' || state.mode === 'readerLoading') {
|
|
254
456
|
if (state.mode === 'readerLoading') {
|
|
255
457
|
const previousState = readerLoadingPrevState;
|
|
@@ -261,7 +463,7 @@ export const docsView = {
|
|
|
261
463
|
return true;
|
|
262
464
|
}
|
|
263
465
|
if (state.readerLinksField) {
|
|
264
|
-
state =
|
|
466
|
+
state = withoutReaderLinksField(state);
|
|
265
467
|
return true;
|
|
266
468
|
}
|
|
267
469
|
const prevPath = readerNavStack.pop();
|
|
@@ -278,7 +480,10 @@ export const docsView = {
|
|
|
278
480
|
return false;
|
|
279
481
|
}
|
|
280
482
|
if (state.mode === 'archivedFiles') {
|
|
281
|
-
state = {
|
|
483
|
+
state = {
|
|
484
|
+
mode: 'archivedGroups',
|
|
485
|
+
archivedGroupsField: buildArchivedGroupsField(archivedGroups, computeMaxVisible(ctx.bodyRows)),
|
|
486
|
+
};
|
|
282
487
|
return true;
|
|
283
488
|
}
|
|
284
489
|
if (state.mode === 'search') {
|
|
@@ -286,7 +491,9 @@ export const docsView = {
|
|
|
286
491
|
goToSections();
|
|
287
492
|
return true;
|
|
288
493
|
}
|
|
289
|
-
if (state.mode === 'files' ||
|
|
494
|
+
if (state.mode === 'files' ||
|
|
495
|
+
state.mode === 'archivedGroups' ||
|
|
496
|
+
state.mode === 'searchResults') {
|
|
290
497
|
goToSections();
|
|
291
498
|
return true;
|
|
292
499
|
}
|
|
@@ -294,7 +501,7 @@ export const docsView = {
|
|
|
294
501
|
},
|
|
295
502
|
handleKey(key, ctx) {
|
|
296
503
|
if (state.mode !== 'error' && state.errorMessage)
|
|
297
|
-
state =
|
|
504
|
+
state = withoutErrorMessage(state);
|
|
298
505
|
switch (state.mode) {
|
|
299
506
|
case 'sections': {
|
|
300
507
|
const result = state.sectionsField?.handleKey(key);
|
|
@@ -302,11 +509,21 @@ export const docsView = {
|
|
|
302
509
|
return;
|
|
303
510
|
if (result.selected === '__search__') {
|
|
304
511
|
setVimKeysActive(false);
|
|
305
|
-
state = {
|
|
512
|
+
state = {
|
|
513
|
+
mode: 'search',
|
|
514
|
+
searchField: new TextField({
|
|
515
|
+
message: t().docs.searchPrompt,
|
|
516
|
+
placeholder: t().docs.searchPlaceholder,
|
|
517
|
+
allowEmpty: true,
|
|
518
|
+
}),
|
|
519
|
+
};
|
|
306
520
|
return;
|
|
307
521
|
}
|
|
308
522
|
if (result.selected === '__refresh__') {
|
|
309
523
|
clearDocsCache();
|
|
524
|
+
sectionsRequestId++;
|
|
525
|
+
metadataRequestId++;
|
|
526
|
+
searchRequestId++;
|
|
310
527
|
loaded = false;
|
|
311
528
|
loadedLanguage = null;
|
|
312
529
|
sections = [];
|
|
@@ -319,25 +536,28 @@ export const docsView = {
|
|
|
319
536
|
readerPrevState = null;
|
|
320
537
|
readerLoadingPrevState = null;
|
|
321
538
|
readerRequestId++;
|
|
322
|
-
void docsView.load
|
|
539
|
+
void docsView.load(ctx);
|
|
323
540
|
return;
|
|
324
541
|
}
|
|
325
542
|
if (result.selected === '__browser__') {
|
|
326
|
-
void ctx
|
|
543
|
+
void openBrowserFromView(ctx);
|
|
327
544
|
return;
|
|
328
545
|
}
|
|
329
546
|
const section = sections.find((s) => s.key === result.selected);
|
|
330
547
|
if (!section)
|
|
331
548
|
return;
|
|
332
549
|
if (section.key === 'archived') {
|
|
550
|
+
metadataRequestId++;
|
|
333
551
|
currentSectionKey = null;
|
|
334
552
|
currentArchivedGroupKey = null;
|
|
335
553
|
archivedGroups = getArchivedGroups(section.files);
|
|
336
|
-
state = {
|
|
554
|
+
state = {
|
|
555
|
+
mode: 'archivedGroups',
|
|
556
|
+
archivedGroupsField: buildArchivedGroupsField(archivedGroups, computeMaxVisible(ctx.bodyRows)),
|
|
557
|
+
};
|
|
337
558
|
}
|
|
338
559
|
else {
|
|
339
|
-
|
|
340
|
-
state = { mode: 'files', filesField: buildFilesField(section, computeMaxVisible(ctx.bodyRows)) };
|
|
560
|
+
void openSectionFiles(ctx, section);
|
|
341
561
|
}
|
|
342
562
|
return;
|
|
343
563
|
}
|
|
@@ -360,9 +580,8 @@ export const docsView = {
|
|
|
360
580
|
goToSections();
|
|
361
581
|
return;
|
|
362
582
|
}
|
|
363
|
-
currentArchivedGroupKey = result.selected;
|
|
364
583
|
const groupFiles = archivedGroups.get(result.selected) ?? [];
|
|
365
|
-
|
|
584
|
+
void openArchivedFiles(ctx, result.selected, groupFiles);
|
|
366
585
|
return;
|
|
367
586
|
}
|
|
368
587
|
case 'archivedFiles': {
|
|
@@ -371,7 +590,10 @@ export const docsView = {
|
|
|
371
590
|
return;
|
|
372
591
|
if (result.selected === '__back__') {
|
|
373
592
|
currentArchivedGroupKey = null;
|
|
374
|
-
state = {
|
|
593
|
+
state = {
|
|
594
|
+
mode: 'archivedGroups',
|
|
595
|
+
archivedGroupsField: buildArchivedGroupsField(archivedGroups, computeMaxVisible(ctx.bodyRows)),
|
|
596
|
+
};
|
|
375
597
|
return;
|
|
376
598
|
}
|
|
377
599
|
enterReaderFrom(ctx, result.selected);
|
|
@@ -391,19 +613,7 @@ export const docsView = {
|
|
|
391
613
|
goToSections();
|
|
392
614
|
return;
|
|
393
615
|
}
|
|
394
|
-
void
|
|
395
|
-
const matches = all.filter((item) => item.path.toLowerCase().includes(query));
|
|
396
|
-
currentSearchResults = matches;
|
|
397
|
-
state = {
|
|
398
|
-
mode: 'searchResults',
|
|
399
|
-
searchResultsEmpty: matches.length === 0,
|
|
400
|
-
searchResultsField: buildSearchResultsField(matches, computeMaxVisible(ctx.bodyRows)),
|
|
401
|
-
};
|
|
402
|
-
ctx.rerender();
|
|
403
|
-
}).catch(() => {
|
|
404
|
-
state = { mode: 'error', errorMessage: t().docs.loadError };
|
|
405
|
-
ctx.rerender();
|
|
406
|
-
});
|
|
616
|
+
void runSearch(ctx, query);
|
|
407
617
|
}
|
|
408
618
|
return;
|
|
409
619
|
}
|
|
@@ -422,28 +632,30 @@ export const docsView = {
|
|
|
422
632
|
if (state.readerLinksField) {
|
|
423
633
|
const result = state.readerLinksField.handleKey(key);
|
|
424
634
|
if (result.cancelled || result.selected === '__back__') {
|
|
425
|
-
state =
|
|
635
|
+
state = withoutReaderLinksField(state);
|
|
426
636
|
return;
|
|
427
637
|
}
|
|
428
638
|
if (result.selected)
|
|
429
639
|
void openInReader(ctx, result.selected, true);
|
|
430
640
|
return;
|
|
431
641
|
}
|
|
432
|
-
// 'f' (Vimium/vim-browser-extension convention: "follow a link"),
|
|
433
|
-
// not 'l' -- core/vim-keys.ts already reserves 'l' globally,
|
|
434
|
-
// ranger-style, as an alias for Enter/confirm (vimActive defaults
|
|
435
|
-
// to true), so a literal 'l' keypress never even reaches here.
|
|
436
642
|
if (key === 'f' && (state.readerLinks?.length ?? 0) > 0) {
|
|
437
|
-
state = {
|
|
643
|
+
state = {
|
|
644
|
+
...state,
|
|
645
|
+
readerLinksField: buildReaderLinksField(state.readerLinks ?? [], computeMaxVisible(ctx.bodyRows)),
|
|
646
|
+
};
|
|
438
647
|
return;
|
|
439
648
|
}
|
|
440
649
|
if (key === 'b') {
|
|
441
|
-
void ctx
|
|
650
|
+
void openBrowserFromView(ctx, readerCurrentPath ?? undefined);
|
|
442
651
|
return;
|
|
443
652
|
}
|
|
444
653
|
return;
|
|
445
654
|
}
|
|
446
|
-
|
|
655
|
+
case 'error':
|
|
656
|
+
case 'loading':
|
|
657
|
+
case 'readerLoading':
|
|
658
|
+
case 'searchLoading':
|
|
447
659
|
return;
|
|
448
660
|
}
|
|
449
661
|
},
|
|
@@ -3,29 +3,15 @@ import { t } from '../../i18n/index.js';
|
|
|
3
3
|
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
|
-
import {
|
|
6
|
+
import { wrapAnsiWithIndent } from '../../core/text.js';
|
|
7
|
+
import { loadingLines } from '../../core/components/spinner.js';
|
|
7
8
|
function wrappedIndentedLines(label, cols, style) {
|
|
8
|
-
|
|
9
|
-
const styled = style(label);
|
|
10
|
-
const styledWidth = visualWidth(styled);
|
|
11
|
-
const preferredIndent = visualWidth(space.indent) < width ? space.indent : '';
|
|
12
|
-
const indent = preferredIndent
|
|
13
|
-
&& styledWidth > width - visualWidth(preferredIndent)
|
|
14
|
-
&& styledWidth <= width
|
|
15
|
-
? ''
|
|
16
|
-
: preferredIndent;
|
|
17
|
-
const contentWidth = Math.max(1, width - visualWidth(indent));
|
|
18
|
-
return wrapAnsiToVisualWidth(styled, contentWidth).map((line) => `${indent}${line}`);
|
|
9
|
+
return wrapAnsiWithIndent(style(label), cols ?? Number.POSITIVE_INFINITY, space.indent);
|
|
19
10
|
}
|
|
20
11
|
function wrappedRenderedLine(line, cols) {
|
|
21
12
|
const content = line.startsWith(space.indent) ? line.slice(space.indent.length) : line;
|
|
22
13
|
return wrappedIndentedLines(content, cols, (value) => value);
|
|
23
14
|
}
|
|
24
|
-
// Lines a fully-expanded hub needs: banner+blank (2) + heatmap+blank (12) +
|
|
25
|
-
// recent-activity heading+up to 5 events+blank (7) + hubField
|
|
26
|
-
// (title+blank+6 options, 8) = 29. Below this, a terminal can't fit the
|
|
27
|
-
// heatmap without pushing the menu into scroll territory — better to keep
|
|
28
|
-
// it as the existing drill-down destination than show a truncated grid.
|
|
29
15
|
const EXPANDED_HUB_MIN_BODY_ROWS = 29;
|
|
30
16
|
function renderHubBody(state, now, bodyRows, cols) {
|
|
31
17
|
const trans = t();
|
|
@@ -38,7 +24,10 @@ function renderHubBody(state, now, bodyRows, cols) {
|
|
|
38
24
|
lines.push(...banner.split('\n'), '');
|
|
39
25
|
const buckets = state.heatmapBuckets;
|
|
40
26
|
if (bodyRows >= EXPANDED_HUB_MIN_BODY_ROWS && buckets && buckets.length > 0) {
|
|
41
|
-
lines.push(...renderHeatmap(buckets, now, {
|
|
27
|
+
lines.push(...renderHeatmap(buckets, now, {
|
|
28
|
+
color: true,
|
|
29
|
+
...(cols === undefined ? {} : { cols }),
|
|
30
|
+
}).split('\n'));
|
|
42
31
|
lines.push('');
|
|
43
32
|
}
|
|
44
33
|
if (state.recentEvents && state.recentEvents.length > 0) {
|
|
@@ -73,16 +62,15 @@ export function renderEvents(state, now, bodyRows = 100, cols) {
|
|
|
73
62
|
const trans = t();
|
|
74
63
|
switch (state.mode) {
|
|
75
64
|
case 'loading':
|
|
76
|
-
return
|
|
65
|
+
return loadingLines(trans.calendar.loading, cols);
|
|
77
66
|
case 'hub':
|
|
78
67
|
return renderHubBody(state, now, bodyRows, cols);
|
|
79
68
|
case 'heatmap':
|
|
80
|
-
// renderHeatmap() already prints its own title (space.indent +
|
|
81
|
-
// type.heading), so this mode doesn't add a second heading on top —
|
|
82
|
-
// unlike Schedule's 'week'/'unresolved' modes, which wrap a
|
|
83
|
-
// title-less renderer.
|
|
84
69
|
return state.heatmapBuckets && state.heatmapBuckets.length > 0
|
|
85
|
-
? renderHeatmap(state.heatmapBuckets, now, {
|
|
70
|
+
? renderHeatmap(state.heatmapBuckets, now, {
|
|
71
|
+
color: true,
|
|
72
|
+
...(cols === undefined ? {} : { cols }),
|
|
73
|
+
}).split('\n')
|
|
86
74
|
: wrappedIndentedLines(trans.calendar.noEvents, cols, type.hint);
|
|
87
75
|
case 'list':
|
|
88
76
|
return state.listField?.render(bodyRows, cols) ?? [];
|
|
@@ -92,14 +80,20 @@ export function renderEvents(state, now, bodyRows = 100, cols) {
|
|
|
92
80
|
...wrappedIndentedLines(state.detailMeta ?? '', cols, type.hint),
|
|
93
81
|
'',
|
|
94
82
|
...(state.detailDescription
|
|
95
|
-
? state.detailDescription
|
|
83
|
+
? state.detailDescription
|
|
84
|
+
.split('\n')
|
|
85
|
+
.flatMap((line) => wrappedIndentedLines(line, cols, type.body))
|
|
96
86
|
: wrappedIndentedLines(trans.calendar.noDescription, cols, type.hint)),
|
|
97
87
|
'',
|
|
98
|
-
...(state.statusMessage
|
|
88
|
+
...(state.statusMessage
|
|
89
|
+
? [...wrappedIndentedLines(state.statusMessage, cols, type.hint), '']
|
|
90
|
+
: []),
|
|
99
91
|
];
|
|
100
92
|
return state.detailField
|
|
101
93
|
? renderListFieldWithContext(context, state.detailField, bodyRows, cols)
|
|
102
|
-
: Number.isFinite(bodyRows)
|
|
94
|
+
: Number.isFinite(bodyRows)
|
|
95
|
+
? context.slice(0, Math.max(0, Math.floor(bodyRows)))
|
|
96
|
+
: context;
|
|
103
97
|
}
|
|
104
98
|
case 'search':
|
|
105
99
|
return state.searchField?.render(cols) ?? [];
|