@nbtca/prompt 1.4.2 → 1.5.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 +27 -58
- package/SECURITY.md +16 -45
- package/dist/app/app.js +53 -55
- 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 +2 -21
- package/dist/app/keys.js +10 -2
- package/dist/app/views/docs-render.js +31 -24
- package/dist/app/views/docs.js +211 -60
- package/dist/app/views/events-render.js +19 -26
- package/dist/app/views/events.js +44 -31
- package/dist/app/views/home.js +28 -30
- package/dist/app/views/schedule-grid-cursor.js +9 -18
- package/dist/app/views/schedule-render.js +47 -71
- package/dist/app/views/schedule.js +158 -81
- 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/config/data.js +9 -11
- package/dist/config/preferences.js +14 -7
- package/dist/core/calendar-day.js +37 -0
- package/dist/core/capabilities.js +6 -3
- package/dist/core/components/confirm.js +9 -8
- package/dist/core/components/menu.js +41 -16
- package/dist/core/components/messages.js +12 -4
- package/dist/core/components/painter.js +3 -1
- package/dist/core/components/spinner.js +17 -6
- 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 +182 -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 +9 -15
- 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.js +12 -185
- package/dist/features/docs.js +436 -275
- 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 +44 -56
- package/dist/features/student-timetable.js +73 -95
- package/dist/features/theme.js +6 -2
- package/dist/features/timetable-sanitize.js +40 -0
- package/dist/features/update.js +9 -27
- package/dist/i18n/index.js +83 -19
- package/dist/i18n/locales/en.json +1 -1
- package/dist/i18n/locales/zh.json +1 -1
- package/dist/index.js +83 -58
- package/dist/logo/ca-dotmatrix.txt +16 -18
- package/dist/main.js +7 -48
- package/package.json +27 -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/links.js +0 -36
- 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/frame.js
CHANGED
|
@@ -1,27 +1,8 @@
|
|
|
1
|
-
import { visualWidth } from '../core/text.js';
|
|
1
|
+
import { clipAnsiToVisualWidth, visualWidth } from '../core/text.js';
|
|
2
2
|
export function clipToWidth(line, cols) {
|
|
3
3
|
if (visualWidth(line) <= cols)
|
|
4
4
|
return line;
|
|
5
|
-
|
|
6
|
-
let w = 0;
|
|
7
|
-
let i = 0;
|
|
8
|
-
while (i < line.length) {
|
|
9
|
-
const esc = line.slice(i).match(/^\x1b\[[0-9;]*m/);
|
|
10
|
-
if (esc) {
|
|
11
|
-
out += esc[0];
|
|
12
|
-
i += esc[0].length;
|
|
13
|
-
continue;
|
|
14
|
-
}
|
|
15
|
-
const cp = line.codePointAt(i);
|
|
16
|
-
const ch = String.fromCodePoint(cp);
|
|
17
|
-
const cw = visualWidth(ch);
|
|
18
|
-
if (w + cw > cols)
|
|
19
|
-
break;
|
|
20
|
-
out += ch;
|
|
21
|
-
w += cw;
|
|
22
|
-
i += ch.length;
|
|
23
|
-
}
|
|
24
|
-
return out + '\x1b[0m';
|
|
5
|
+
return clipAnsiToVisualWidth(line, cols) + '\x1b[0m';
|
|
25
6
|
}
|
|
26
7
|
export function fitLine(line, cols) {
|
|
27
8
|
const clipped = visualWidth(line) > cols ? clipToWidth(line, cols) : line;
|
package/dist/app/keys.js
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
function switchResult(target) {
|
|
2
|
+
return target === undefined ? { handled: true } : { switchTo: target, handled: true };
|
|
3
|
+
}
|
|
1
4
|
export function routeGlobalKey(key, viewIds, current) {
|
|
2
5
|
if (key === 'q' || key === '\x03')
|
|
3
6
|
return { quit: true, handled: true };
|
|
@@ -5,7 +8,12 @@ export function routeGlobalKey(key, viewIds, current) {
|
|
|
5
8
|
return current === 'home' ? { quit: true, handled: true } : { back: true, handled: true };
|
|
6
9
|
if (key === '\t') {
|
|
7
10
|
const i = viewIds.indexOf(current);
|
|
8
|
-
return
|
|
11
|
+
return switchResult(viewIds[(i + 1) % viewIds.length]);
|
|
12
|
+
}
|
|
13
|
+
if (key === '\x1b[Z') {
|
|
14
|
+
const i = viewIds.indexOf(current);
|
|
15
|
+
const previous = i < 0 ? viewIds.length - 1 : (i - 1 + viewIds.length) % viewIds.length;
|
|
16
|
+
return switchResult(viewIds[previous]);
|
|
9
17
|
}
|
|
10
18
|
if (key === '\x1b[5~')
|
|
11
19
|
return { scrollBy: -1, handled: true };
|
|
@@ -14,7 +22,7 @@ export function routeGlobalKey(key, viewIds, current) {
|
|
|
14
22
|
if (/^[1-9]$/.test(key)) {
|
|
15
23
|
const idx = Number(key) - 1;
|
|
16
24
|
if (idx < viewIds.length)
|
|
17
|
-
return
|
|
25
|
+
return switchResult(viewIds[idx]);
|
|
18
26
|
}
|
|
19
27
|
return { handled: false };
|
|
20
28
|
}
|
|
@@ -1,32 +1,34 @@
|
|
|
1
1
|
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
|
-
import { visualWidth, wrapAnsiToVisualWidth } from '../../core/text.js';
|
|
4
|
+
import { visualWidth, wrapAnsiToVisualWidth, wrapAnsiWithIndent } from '../../core/text.js';
|
|
5
5
|
function hintLines(label, cols) {
|
|
6
|
-
|
|
7
|
-
const styled = type.hint(label);
|
|
8
|
-
const preferredIndent = visualWidth(space.indent) < width ? space.indent : '';
|
|
9
|
-
const indent = preferredIndent
|
|
10
|
-
&& visualWidth(styled) > width - visualWidth(preferredIndent)
|
|
11
|
-
&& visualWidth(styled) <= width
|
|
12
|
-
? ''
|
|
13
|
-
: preferredIndent;
|
|
14
|
-
const contentWidth = Math.max(1, width - visualWidth(indent));
|
|
15
|
-
return wrapAnsiToVisualWidth(styled, contentWidth).map((line) => `${indent}${line}`);
|
|
6
|
+
return wrapAnsiWithIndent(type.hint(label), cols, space.indent);
|
|
16
7
|
}
|
|
17
8
|
function renderReader(lines, cols) {
|
|
18
9
|
const contentWidth = Math.max(1, Math.min(80, cols - visualWidth(space.indent)));
|
|
19
|
-
return lines.flatMap((line) =>
|
|
10
|
+
return lines.flatMap((line) => wrapAnsiToVisualWidth(line, contentWidth).map((part) => `${space.indent}${part}`));
|
|
20
11
|
}
|
|
21
12
|
function listFieldForState(state) {
|
|
22
13
|
switch (state.mode) {
|
|
23
|
-
case 'sections':
|
|
24
|
-
|
|
25
|
-
case '
|
|
26
|
-
|
|
27
|
-
case '
|
|
28
|
-
|
|
29
|
-
|
|
14
|
+
case 'sections':
|
|
15
|
+
return state.sectionsField;
|
|
16
|
+
case 'files':
|
|
17
|
+
return state.filesField;
|
|
18
|
+
case 'archivedGroups':
|
|
19
|
+
return state.archivedGroupsField;
|
|
20
|
+
case 'archivedFiles':
|
|
21
|
+
return state.archivedFilesField;
|
|
22
|
+
case 'searchResults':
|
|
23
|
+
return state.searchResultsField;
|
|
24
|
+
case 'reader':
|
|
25
|
+
return state.readerLinksField;
|
|
26
|
+
case 'error':
|
|
27
|
+
case 'loading':
|
|
28
|
+
case 'readerLoading':
|
|
29
|
+
case 'search':
|
|
30
|
+
case 'searchLoading':
|
|
31
|
+
return undefined;
|
|
30
32
|
}
|
|
31
33
|
}
|
|
32
34
|
export function renderDocs(state, cols = 80, bodyRows = Number.POSITIVE_INFINITY) {
|
|
@@ -51,10 +53,17 @@ export function renderDocs(state, cols = 80, bodyRows = Number.POSITIVE_INFINITY
|
|
|
51
53
|
case 'search':
|
|
52
54
|
lines = state.searchField?.render(cols) ?? [];
|
|
53
55
|
break;
|
|
56
|
+
case 'searchLoading':
|
|
57
|
+
lines = hintLines(trans.docs.searching, cols);
|
|
58
|
+
break;
|
|
54
59
|
case 'searchResults':
|
|
55
|
-
lines = state.searchResultsField
|
|
56
|
-
|
|
57
|
-
|
|
60
|
+
lines = state.searchResultsField
|
|
61
|
+
? renderListFieldWithContext([
|
|
62
|
+
...(state.searchResultsEmpty
|
|
63
|
+
? [...hintLines(trans.docs.searchNoResults, cols), '']
|
|
64
|
+
: []),
|
|
65
|
+
], state.searchResultsField, bodyRows, cols)
|
|
66
|
+
: [];
|
|
58
67
|
break;
|
|
59
68
|
case 'readerLoading':
|
|
60
69
|
lines = hintLines(trans.docs.loadingFile, cols);
|
|
@@ -66,8 +75,6 @@ export function renderDocs(state, cols = 80, bodyRows = Number.POSITIVE_INFINITY
|
|
|
66
75
|
break;
|
|
67
76
|
case 'error':
|
|
68
77
|
return hintLines(state.errorMessage ?? trans.docs.loadError, cols);
|
|
69
|
-
default:
|
|
70
|
-
lines = [];
|
|
71
78
|
}
|
|
72
79
|
if (!state.errorMessage)
|
|
73
80
|
return lines;
|
package/dist/app/views/docs.js
CHANGED
|
@@ -5,7 +5,8 @@ import { renderDocs } from './docs-render.js';
|
|
|
5
5
|
import { setVimKeysActive } from '../../core/vim-keys.js';
|
|
6
6
|
import { pickIcon } from '../../core/icons.js';
|
|
7
7
|
import { getCurrentLanguage, t } from '../../i18n/index.js';
|
|
8
|
-
import {
|
|
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
10
|
let state = { mode: 'loading' };
|
|
10
11
|
let sections = [];
|
|
11
12
|
let archivedGroups = new Map();
|
|
@@ -14,19 +15,34 @@ 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
|
+
const DOC_HINT_WIDTH = 44;
|
|
27
27
|
function backLabel() {
|
|
28
28
|
return t().common.back;
|
|
29
29
|
}
|
|
30
|
+
function optionalHint(hint) {
|
|
31
|
+
return hint === undefined ? {} : { hint };
|
|
32
|
+
}
|
|
33
|
+
function docHint(value) {
|
|
34
|
+
return value ? truncate(value, DOC_HINT_WIDTH) : undefined;
|
|
35
|
+
}
|
|
36
|
+
function withoutReaderLinksField(value) {
|
|
37
|
+
const next = { ...value };
|
|
38
|
+
delete next.readerLinksField;
|
|
39
|
+
return next;
|
|
40
|
+
}
|
|
41
|
+
function withoutErrorMessage(value) {
|
|
42
|
+
const next = { ...value };
|
|
43
|
+
delete next.errorMessage;
|
|
44
|
+
return next;
|
|
45
|
+
}
|
|
30
46
|
function buildSectionsField() {
|
|
31
47
|
const trans = t();
|
|
32
48
|
const options = [
|
|
@@ -39,12 +55,16 @@ function buildSectionsField() {
|
|
|
39
55
|
}
|
|
40
56
|
function buildFilesField(section, maxVisible, initialIndex = 0) {
|
|
41
57
|
const trans = t();
|
|
42
|
-
const isIndex = (
|
|
58
|
+
const isIndex = (file) => file.name === 'index.md' || file.name.startsWith('index.');
|
|
43
59
|
const index = section.files.find(isIndex);
|
|
44
60
|
const files = section.files.filter((f) => !isIndex(f));
|
|
45
61
|
const options = [
|
|
46
62
|
...(index ? [{ value: index.path, label: trans.docs.overviewLabel }] : []),
|
|
47
|
-
...files.map((
|
|
63
|
+
...files.map((file) => ({
|
|
64
|
+
value: file.path,
|
|
65
|
+
label: displayDocTitle(file.name, file.title),
|
|
66
|
+
...optionalHint(docHint(file.summary)),
|
|
67
|
+
})),
|
|
48
68
|
{ value: '__back__', label: backLabel() },
|
|
49
69
|
];
|
|
50
70
|
return new ListField({ title: section.label, options, maxVisible, initialIndex });
|
|
@@ -63,7 +83,7 @@ function buildArchivedGroupsField(groups, maxVisible, initialIndex = 0) {
|
|
|
63
83
|
return a.localeCompare(b);
|
|
64
84
|
});
|
|
65
85
|
const options = [
|
|
66
|
-
...sortedKeys.map((k) => ({ value: k, label: k, hint: String(groups.get(k)
|
|
86
|
+
...sortedKeys.map((k) => ({ value: k, label: k, hint: String(groups.get(k)?.length ?? 0) })),
|
|
67
87
|
{ value: '__back__', label: backLabel() },
|
|
68
88
|
];
|
|
69
89
|
return new ListField({ title: trans.docs.categoryArchived, options, maxVisible, initialIndex });
|
|
@@ -74,11 +94,22 @@ function buildArchivedFilesField(groupKey, groupFiles, maxVisible, initialIndex
|
|
|
74
94
|
const options = [
|
|
75
95
|
...groupFiles.map((f) => {
|
|
76
96
|
const sub = f.path.split('/').slice(2, -1).join('/');
|
|
77
|
-
return {
|
|
97
|
+
return {
|
|
98
|
+
value: f.path,
|
|
99
|
+
label: displayDocTitle(f.name, f.title),
|
|
100
|
+
...optionalHint(docHint(subDirs.size > 1
|
|
101
|
+
? [sanitizeTerminalLine(sub), f.summary].filter(Boolean).join(' · ')
|
|
102
|
+
: f.summary)),
|
|
103
|
+
};
|
|
78
104
|
}),
|
|
79
105
|
{ value: '__back__', label: backLabel() },
|
|
80
106
|
];
|
|
81
|
-
return new ListField({
|
|
107
|
+
return new ListField({
|
|
108
|
+
title: `${trans.docs.categoryArchived} · ${groupKey}`,
|
|
109
|
+
options,
|
|
110
|
+
maxVisible,
|
|
111
|
+
initialIndex,
|
|
112
|
+
});
|
|
82
113
|
}
|
|
83
114
|
function buildReaderLinksField(links, maxVisible, initialIndex = 0) {
|
|
84
115
|
const trans = t();
|
|
@@ -93,8 +124,12 @@ function buildSearchResultsField(matches, maxVisible, initialIndex = 0) {
|
|
|
93
124
|
const options = [
|
|
94
125
|
...matches.map((result) => ({
|
|
95
126
|
value: result.path,
|
|
96
|
-
label: displayDocTitle(result.
|
|
97
|
-
|
|
127
|
+
label: displayDocTitle(result.name, result.title),
|
|
128
|
+
...optionalHint(docHint(result.excerpt ||
|
|
129
|
+
result.summary ||
|
|
130
|
+
(result.path.includes('/')
|
|
131
|
+
? sanitizeTerminalLine(result.path.split('/').slice(0, -1).join('/'))
|
|
132
|
+
: undefined))),
|
|
98
133
|
})),
|
|
99
134
|
{ value: '__back__', label: backLabel() },
|
|
100
135
|
];
|
|
@@ -107,7 +142,10 @@ function relocalizeStateFields(value, maxVisible) {
|
|
|
107
142
|
if (value.mode === 'files' && currentSectionKey) {
|
|
108
143
|
const section = sections.find((candidate) => candidate.key === currentSectionKey);
|
|
109
144
|
return section
|
|
110
|
-
? {
|
|
145
|
+
? {
|
|
146
|
+
...value,
|
|
147
|
+
filesField: buildFilesField(section, maxVisible, value.filesField?.selectedIndex),
|
|
148
|
+
}
|
|
111
149
|
: value;
|
|
112
150
|
}
|
|
113
151
|
if (value.mode === 'archivedGroups') {
|
|
@@ -142,10 +180,95 @@ function goToSections() {
|
|
|
142
180
|
currentSearchResults = [];
|
|
143
181
|
state = { mode: 'sections', sectionsField: buildSectionsField() };
|
|
144
182
|
}
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
183
|
+
function replaceSection(section) {
|
|
184
|
+
sections = sections.map((current) => (current.key === section.key ? section : current));
|
|
185
|
+
}
|
|
186
|
+
async function openSectionFiles(ctx, section) {
|
|
187
|
+
const requestId = ++metadataRequestId;
|
|
188
|
+
currentSectionKey = section.key;
|
|
189
|
+
state = {
|
|
190
|
+
mode: 'files',
|
|
191
|
+
filesField: buildFilesField(section, computeMaxVisible(ctx.bodyRows)),
|
|
192
|
+
};
|
|
193
|
+
ctx.rerender();
|
|
194
|
+
try {
|
|
195
|
+
const hydrated = await fetchSectionMetadata(section);
|
|
196
|
+
if (requestId !== metadataRequestId ||
|
|
197
|
+
state.mode !== 'files' ||
|
|
198
|
+
currentSectionKey !== section.key)
|
|
199
|
+
return;
|
|
200
|
+
const localized = localizeDocSections([hydrated], t())[0] ?? hydrated;
|
|
201
|
+
replaceSection(localized);
|
|
202
|
+
state = {
|
|
203
|
+
mode: 'files',
|
|
204
|
+
filesField: buildFilesField(localized, computeMaxVisible(ctx.bodyRows), state.filesField?.selectedIndex),
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
catch {
|
|
208
|
+
if (requestId !== metadataRequestId ||
|
|
209
|
+
state.mode !== 'files' ||
|
|
210
|
+
currentSectionKey !== section.key)
|
|
211
|
+
return;
|
|
212
|
+
state = { ...state, errorMessage: t().docs.loadError };
|
|
213
|
+
}
|
|
214
|
+
ctx.rerender();
|
|
215
|
+
}
|
|
216
|
+
async function openArchivedFiles(ctx, groupKey, groupFiles) {
|
|
217
|
+
const requestId = ++metadataRequestId;
|
|
218
|
+
currentArchivedGroupKey = groupKey;
|
|
219
|
+
state = {
|
|
220
|
+
mode: 'archivedFiles',
|
|
221
|
+
archivedFilesField: buildArchivedFilesField(groupKey, groupFiles, computeMaxVisible(ctx.bodyRows)),
|
|
222
|
+
};
|
|
223
|
+
ctx.rerender();
|
|
224
|
+
try {
|
|
225
|
+
const hydrated = await fetchDocMetadata(groupFiles);
|
|
226
|
+
if (requestId !== metadataRequestId ||
|
|
227
|
+
state.mode !== 'archivedFiles' ||
|
|
228
|
+
currentArchivedGroupKey !== groupKey)
|
|
229
|
+
return;
|
|
230
|
+
archivedGroups.set(groupKey, hydrated);
|
|
231
|
+
state = {
|
|
232
|
+
mode: 'archivedFiles',
|
|
233
|
+
archivedFilesField: buildArchivedFilesField(groupKey, hydrated, computeMaxVisible(ctx.bodyRows), state.archivedFilesField?.selectedIndex),
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
catch {
|
|
237
|
+
if (requestId !== metadataRequestId ||
|
|
238
|
+
state.mode !== 'archivedFiles' ||
|
|
239
|
+
currentArchivedGroupKey !== groupKey)
|
|
240
|
+
return;
|
|
241
|
+
state = {
|
|
242
|
+
...state,
|
|
243
|
+
errorMessage: t().docs.loadError,
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
ctx.rerender();
|
|
247
|
+
}
|
|
248
|
+
async function runSearch(ctx, query) {
|
|
249
|
+
const requestId = ++searchRequestId;
|
|
250
|
+
state = { mode: 'searchLoading' };
|
|
251
|
+
ctx.rerender();
|
|
252
|
+
try {
|
|
253
|
+
const matches = await searchDocuments(query);
|
|
254
|
+
if (requestId !== searchRequestId)
|
|
255
|
+
return;
|
|
256
|
+
currentSearchResults = matches;
|
|
257
|
+
state = {
|
|
258
|
+
mode: 'searchResults',
|
|
259
|
+
searchResultsEmpty: matches.length === 0,
|
|
260
|
+
searchResultsField: buildSearchResultsField(matches, computeMaxVisible(ctx.bodyRows)),
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
catch {
|
|
264
|
+
if (requestId !== searchRequestId)
|
|
265
|
+
return;
|
|
266
|
+
currentSearchResults = [];
|
|
267
|
+
goToSections();
|
|
268
|
+
state = { ...state, errorMessage: t().docs.loadError };
|
|
269
|
+
}
|
|
270
|
+
ctx.rerender();
|
|
271
|
+
}
|
|
149
272
|
async function openInReader(ctx, path, pushCurrent) {
|
|
150
273
|
const requestId = ++readerRequestId;
|
|
151
274
|
const previousState = state;
|
|
@@ -161,7 +284,12 @@ async function openInReader(ctx, path, pushCurrent) {
|
|
|
161
284
|
readerNavStack.push(previousPath);
|
|
162
285
|
readerCurrentPath = path;
|
|
163
286
|
readerLoadingPrevState = null;
|
|
164
|
-
state = {
|
|
287
|
+
state = {
|
|
288
|
+
mode: 'reader',
|
|
289
|
+
readerTitle: doc.title,
|
|
290
|
+
readerLines: doc.lines,
|
|
291
|
+
readerLinks: doc.links,
|
|
292
|
+
};
|
|
165
293
|
ctx.resetScroll();
|
|
166
294
|
}
|
|
167
295
|
catch {
|
|
@@ -169,15 +297,12 @@ async function openInReader(ctx, path, pushCurrent) {
|
|
|
169
297
|
return;
|
|
170
298
|
readerLoadingPrevState = null;
|
|
171
299
|
const fallbackState = pushCurrent && previousState.mode === 'reader'
|
|
172
|
-
?
|
|
300
|
+
? withoutReaderLinksField(previousState)
|
|
173
301
|
: previousState;
|
|
174
302
|
state = { ...fallbackState, errorMessage: t().docs.loadError };
|
|
175
303
|
}
|
|
176
304
|
ctx.rerender();
|
|
177
305
|
}
|
|
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
306
|
function enterReaderFrom(ctx, path) {
|
|
182
307
|
readerPrevState = state;
|
|
183
308
|
readerNavStack = [];
|
|
@@ -201,23 +326,26 @@ export const docsView = {
|
|
|
201
326
|
}
|
|
202
327
|
return;
|
|
203
328
|
}
|
|
329
|
+
const requestId = ++sectionsRequestId;
|
|
204
330
|
state = { mode: 'loading' };
|
|
205
331
|
ctx.rerender();
|
|
206
332
|
try {
|
|
207
|
-
|
|
333
|
+
const nextSections = await fetchSections();
|
|
334
|
+
if (requestId !== sectionsRequestId)
|
|
335
|
+
return;
|
|
336
|
+
sections = nextSections;
|
|
208
337
|
loaded = true;
|
|
209
338
|
loadedLanguage = getCurrentLanguage();
|
|
210
339
|
goToSections();
|
|
211
340
|
}
|
|
212
341
|
catch {
|
|
342
|
+
if (requestId !== sectionsRequestId)
|
|
343
|
+
return;
|
|
213
344
|
state = { mode: 'error', errorMessage: t().docs.loadError };
|
|
214
345
|
}
|
|
215
346
|
ctx.rerender();
|
|
216
347
|
},
|
|
217
348
|
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
349
|
const maxVisible = computeMaxVisible(ctx.bodyRows);
|
|
222
350
|
state.filesField?.setMaxVisible(maxVisible);
|
|
223
351
|
state.archivedGroupsField?.setMaxVisible(maxVisible);
|
|
@@ -229,10 +357,18 @@ export const docsView = {
|
|
|
229
357
|
capturesInput() {
|
|
230
358
|
return state.mode === 'search';
|
|
231
359
|
},
|
|
360
|
+
capturesPageKeys() {
|
|
361
|
+
return (state.mode === 'sections' ||
|
|
362
|
+
state.mode === 'files' ||
|
|
363
|
+
state.mode === 'archivedGroups' ||
|
|
364
|
+
state.mode === 'archivedFiles' ||
|
|
365
|
+
state.mode === 'searchResults' ||
|
|
366
|
+
(state.mode === 'reader' && state.readerLinksField !== undefined));
|
|
367
|
+
},
|
|
232
368
|
footerHint(tabCount, cols = Number.POSITIVE_INFINITY) {
|
|
233
369
|
if (state.mode === 'search')
|
|
234
370
|
return captureFooterHint(cols);
|
|
235
|
-
if (state.mode === 'loading' || state.mode === 'error')
|
|
371
|
+
if (state.mode === 'loading' || state.mode === 'searchLoading' || state.mode === 'error')
|
|
236
372
|
return passiveFooterHint(tabCount, cols);
|
|
237
373
|
if (state.mode === 'readerLoading') {
|
|
238
374
|
return fitFooterHint(cols, `${digitTabHint(tabCount)}q ${t().menu.hintQuit}`, `${digitTabHint(tabCount)}q`, 'q');
|
|
@@ -250,6 +386,11 @@ export const docsView = {
|
|
|
250
386
|
return undefined;
|
|
251
387
|
},
|
|
252
388
|
handleBack(ctx) {
|
|
389
|
+
if (state.mode === 'searchLoading') {
|
|
390
|
+
searchRequestId++;
|
|
391
|
+
goToSections();
|
|
392
|
+
return true;
|
|
393
|
+
}
|
|
253
394
|
if (state.mode === 'reader' || state.mode === 'readerLoading') {
|
|
254
395
|
if (state.mode === 'readerLoading') {
|
|
255
396
|
const previousState = readerLoadingPrevState;
|
|
@@ -261,7 +402,7 @@ export const docsView = {
|
|
|
261
402
|
return true;
|
|
262
403
|
}
|
|
263
404
|
if (state.readerLinksField) {
|
|
264
|
-
state =
|
|
405
|
+
state = withoutReaderLinksField(state);
|
|
265
406
|
return true;
|
|
266
407
|
}
|
|
267
408
|
const prevPath = readerNavStack.pop();
|
|
@@ -278,7 +419,10 @@ export const docsView = {
|
|
|
278
419
|
return false;
|
|
279
420
|
}
|
|
280
421
|
if (state.mode === 'archivedFiles') {
|
|
281
|
-
state = {
|
|
422
|
+
state = {
|
|
423
|
+
mode: 'archivedGroups',
|
|
424
|
+
archivedGroupsField: buildArchivedGroupsField(archivedGroups, computeMaxVisible(ctx.bodyRows)),
|
|
425
|
+
};
|
|
282
426
|
return true;
|
|
283
427
|
}
|
|
284
428
|
if (state.mode === 'search') {
|
|
@@ -286,7 +430,9 @@ export const docsView = {
|
|
|
286
430
|
goToSections();
|
|
287
431
|
return true;
|
|
288
432
|
}
|
|
289
|
-
if (state.mode === 'files' ||
|
|
433
|
+
if (state.mode === 'files' ||
|
|
434
|
+
state.mode === 'archivedGroups' ||
|
|
435
|
+
state.mode === 'searchResults') {
|
|
290
436
|
goToSections();
|
|
291
437
|
return true;
|
|
292
438
|
}
|
|
@@ -294,7 +440,7 @@ export const docsView = {
|
|
|
294
440
|
},
|
|
295
441
|
handleKey(key, ctx) {
|
|
296
442
|
if (state.mode !== 'error' && state.errorMessage)
|
|
297
|
-
state =
|
|
443
|
+
state = withoutErrorMessage(state);
|
|
298
444
|
switch (state.mode) {
|
|
299
445
|
case 'sections': {
|
|
300
446
|
const result = state.sectionsField?.handleKey(key);
|
|
@@ -302,11 +448,21 @@ export const docsView = {
|
|
|
302
448
|
return;
|
|
303
449
|
if (result.selected === '__search__') {
|
|
304
450
|
setVimKeysActive(false);
|
|
305
|
-
state = {
|
|
451
|
+
state = {
|
|
452
|
+
mode: 'search',
|
|
453
|
+
searchField: new TextField({
|
|
454
|
+
message: t().docs.searchPrompt,
|
|
455
|
+
placeholder: t().docs.searchPlaceholder,
|
|
456
|
+
allowEmpty: true,
|
|
457
|
+
}),
|
|
458
|
+
};
|
|
306
459
|
return;
|
|
307
460
|
}
|
|
308
461
|
if (result.selected === '__refresh__') {
|
|
309
462
|
clearDocsCache();
|
|
463
|
+
sectionsRequestId++;
|
|
464
|
+
metadataRequestId++;
|
|
465
|
+
searchRequestId++;
|
|
310
466
|
loaded = false;
|
|
311
467
|
loadedLanguage = null;
|
|
312
468
|
sections = [];
|
|
@@ -319,7 +475,7 @@ export const docsView = {
|
|
|
319
475
|
readerPrevState = null;
|
|
320
476
|
readerLoadingPrevState = null;
|
|
321
477
|
readerRequestId++;
|
|
322
|
-
void docsView.load
|
|
478
|
+
void docsView.load(ctx);
|
|
323
479
|
return;
|
|
324
480
|
}
|
|
325
481
|
if (result.selected === '__browser__') {
|
|
@@ -330,14 +486,17 @@ export const docsView = {
|
|
|
330
486
|
if (!section)
|
|
331
487
|
return;
|
|
332
488
|
if (section.key === 'archived') {
|
|
489
|
+
metadataRequestId++;
|
|
333
490
|
currentSectionKey = null;
|
|
334
491
|
currentArchivedGroupKey = null;
|
|
335
492
|
archivedGroups = getArchivedGroups(section.files);
|
|
336
|
-
state = {
|
|
493
|
+
state = {
|
|
494
|
+
mode: 'archivedGroups',
|
|
495
|
+
archivedGroupsField: buildArchivedGroupsField(archivedGroups, computeMaxVisible(ctx.bodyRows)),
|
|
496
|
+
};
|
|
337
497
|
}
|
|
338
498
|
else {
|
|
339
|
-
|
|
340
|
-
state = { mode: 'files', filesField: buildFilesField(section, computeMaxVisible(ctx.bodyRows)) };
|
|
499
|
+
void openSectionFiles(ctx, section);
|
|
341
500
|
}
|
|
342
501
|
return;
|
|
343
502
|
}
|
|
@@ -360,9 +519,8 @@ export const docsView = {
|
|
|
360
519
|
goToSections();
|
|
361
520
|
return;
|
|
362
521
|
}
|
|
363
|
-
currentArchivedGroupKey = result.selected;
|
|
364
522
|
const groupFiles = archivedGroups.get(result.selected) ?? [];
|
|
365
|
-
|
|
523
|
+
void openArchivedFiles(ctx, result.selected, groupFiles);
|
|
366
524
|
return;
|
|
367
525
|
}
|
|
368
526
|
case 'archivedFiles': {
|
|
@@ -371,7 +529,10 @@ export const docsView = {
|
|
|
371
529
|
return;
|
|
372
530
|
if (result.selected === '__back__') {
|
|
373
531
|
currentArchivedGroupKey = null;
|
|
374
|
-
state = {
|
|
532
|
+
state = {
|
|
533
|
+
mode: 'archivedGroups',
|
|
534
|
+
archivedGroupsField: buildArchivedGroupsField(archivedGroups, computeMaxVisible(ctx.bodyRows)),
|
|
535
|
+
};
|
|
375
536
|
return;
|
|
376
537
|
}
|
|
377
538
|
enterReaderFrom(ctx, result.selected);
|
|
@@ -391,19 +552,7 @@ export const docsView = {
|
|
|
391
552
|
goToSections();
|
|
392
553
|
return;
|
|
393
554
|
}
|
|
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
|
-
});
|
|
555
|
+
void runSearch(ctx, query);
|
|
407
556
|
}
|
|
408
557
|
return;
|
|
409
558
|
}
|
|
@@ -422,19 +571,18 @@ export const docsView = {
|
|
|
422
571
|
if (state.readerLinksField) {
|
|
423
572
|
const result = state.readerLinksField.handleKey(key);
|
|
424
573
|
if (result.cancelled || result.selected === '__back__') {
|
|
425
|
-
state =
|
|
574
|
+
state = withoutReaderLinksField(state);
|
|
426
575
|
return;
|
|
427
576
|
}
|
|
428
577
|
if (result.selected)
|
|
429
578
|
void openInReader(ctx, result.selected, true);
|
|
430
579
|
return;
|
|
431
580
|
}
|
|
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
581
|
if (key === 'f' && (state.readerLinks?.length ?? 0) > 0) {
|
|
437
|
-
state = {
|
|
582
|
+
state = {
|
|
583
|
+
...state,
|
|
584
|
+
readerLinksField: buildReaderLinksField(state.readerLinks ?? [], computeMaxVisible(ctx.bodyRows)),
|
|
585
|
+
};
|
|
438
586
|
return;
|
|
439
587
|
}
|
|
440
588
|
if (key === 'b') {
|
|
@@ -443,7 +591,10 @@ export const docsView = {
|
|
|
443
591
|
}
|
|
444
592
|
return;
|
|
445
593
|
}
|
|
446
|
-
|
|
594
|
+
case 'error':
|
|
595
|
+
case 'loading':
|
|
596
|
+
case 'readerLoading':
|
|
597
|
+
case 'searchLoading':
|
|
447
598
|
return;
|
|
448
599
|
}
|
|
449
600
|
},
|