@nbtca/prompt 1.4.1 → 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/LICENSE +1 -1
- 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 -67
- package/dist/app/views/events-render.js +19 -26
- package/dist/app/views/events.js +44 -31
- package/dist/app/views/home.js +33 -76
- 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 -90
- package/dist/app/views/settings-render.js +8 -19
- package/dist/app/views/settings.js +93 -18
- 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 +10 -13
- 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 +25 -21
- package/dist/core/motion.js +25 -19
- package/dist/core/text.js +182 -75
- package/dist/core/theme.js +0 -28
- package/dist/core/transitions.js +2 -2
- package/dist/core/ui.js +15 -30
- 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 +439 -320
- package/dist/features/schedule-render.js +65 -102
- package/dist/features/schedule-store.js +51 -9
- package/dist/features/schedule-view.js +46 -220
- package/dist/features/status.js +44 -59
- package/dist/features/student-timetable.js +73 -95
- package/dist/features/theme.js +6 -5
- package/dist/features/timetable-sanitize.js +40 -0
- package/dist/features/update.js +9 -37
- package/dist/i18n/index.js +87 -65
- package/dist/i18n/locales/en.json +1 -1
- package/dist/i18n/locales/zh.json +1 -1
- package/dist/index.js +85 -64
- package/dist/logo/ca-dotmatrix.txt +16 -18
- package/dist/main.js +7 -48
- package/package.json +30 -18
- package/bin/nbtca-welcome.js +0 -2
- package/dist/core/components/screen.js +0 -18
- package/dist/core/menu.js +0 -71
- package/dist/features/links.js +0 -39
- package/dist/features/schedule-query.js +0 -47
- package/dist/features/settings.js +0 -130
- package/dist/logo/ca-logo.png +0 -0
|
@@ -1,9 +1,4 @@
|
|
|
1
1
|
import { renderInput, applyInputEvent, parseInputData } from '../../core/components/text-input.js';
|
|
2
|
-
/** Non-blocking equivalent of `runTextInput`/`runSecretInput`: a view holds
|
|
3
|
-
* one of these and drives it via `handleKey` from the app loop's single
|
|
4
|
-
* stdin listener. Does not touch vim-key activation — the owning view is
|
|
5
|
-
* responsible for `setVimKeysActive(false)` while a TextField is focused
|
|
6
|
-
* (mirrors what `runTextInput` already does for the blocking widget). */
|
|
7
2
|
export class TextField {
|
|
8
3
|
config;
|
|
9
4
|
value = '';
|
|
@@ -17,9 +12,9 @@ export class TextField {
|
|
|
17
12
|
return renderInput({
|
|
18
13
|
message: this.config.message,
|
|
19
14
|
value: this.value,
|
|
20
|
-
placeholder: this.config.placeholder,
|
|
21
|
-
secret: this.config.secret,
|
|
22
|
-
mask: this.config.mask,
|
|
15
|
+
...(this.config.placeholder === undefined ? {} : { placeholder: this.config.placeholder }),
|
|
16
|
+
...(this.config.secret === undefined ? {} : { secret: this.config.secret }),
|
|
17
|
+
...(this.config.mask === undefined ? {} : { mask: this.config.mask }),
|
|
23
18
|
cols,
|
|
24
19
|
}).split('\n');
|
|
25
20
|
}
|
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,19 +55,16 @@ function buildSectionsField() {
|
|
|
39
55
|
}
|
|
40
56
|
function buildFilesField(section, maxVisible, initialIndex = 0) {
|
|
41
57
|
const trans = t();
|
|
42
|
-
const isIndex = (
|
|
43
|
-
// nbtca/documents' repair/ and concepts/ sections are explicitly built
|
|
44
|
-
// "hub + inline-link + search, no full sidebar" (.vitepress/config.mts) --
|
|
45
|
-
// each has a hand-curated index.md landing page (concepts/index.md groups
|
|
46
|
-
// all 21 entries by topic with one-line definitions; nothing like that
|
|
47
|
-
// exists in a flat alphabetical list). It used to be filtered out
|
|
48
|
-
// entirely here, making it unreachable from the Docs tab -- now it's
|
|
49
|
-
// pinned to the top as a distinctly-labeled entry instead.
|
|
58
|
+
const isIndex = (file) => file.name === 'index.md' || file.name.startsWith('index.');
|
|
50
59
|
const index = section.files.find(isIndex);
|
|
51
60
|
const files = section.files.filter((f) => !isIndex(f));
|
|
52
61
|
const options = [
|
|
53
62
|
...(index ? [{ value: index.path, label: trans.docs.overviewLabel }] : []),
|
|
54
|
-
...files.map((
|
|
63
|
+
...files.map((file) => ({
|
|
64
|
+
value: file.path,
|
|
65
|
+
label: displayDocTitle(file.name, file.title),
|
|
66
|
+
...optionalHint(docHint(file.summary)),
|
|
67
|
+
})),
|
|
55
68
|
{ value: '__back__', label: backLabel() },
|
|
56
69
|
];
|
|
57
70
|
return new ListField({ title: section.label, options, maxVisible, initialIndex });
|
|
@@ -70,7 +83,7 @@ function buildArchivedGroupsField(groups, maxVisible, initialIndex = 0) {
|
|
|
70
83
|
return a.localeCompare(b);
|
|
71
84
|
});
|
|
72
85
|
const options = [
|
|
73
|
-
...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) })),
|
|
74
87
|
{ value: '__back__', label: backLabel() },
|
|
75
88
|
];
|
|
76
89
|
return new ListField({ title: trans.docs.categoryArchived, options, maxVisible, initialIndex });
|
|
@@ -81,11 +94,22 @@ function buildArchivedFilesField(groupKey, groupFiles, maxVisible, initialIndex
|
|
|
81
94
|
const options = [
|
|
82
95
|
...groupFiles.map((f) => {
|
|
83
96
|
const sub = f.path.split('/').slice(2, -1).join('/');
|
|
84
|
-
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
|
+
};
|
|
85
104
|
}),
|
|
86
105
|
{ value: '__back__', label: backLabel() },
|
|
87
106
|
];
|
|
88
|
-
return new ListField({
|
|
107
|
+
return new ListField({
|
|
108
|
+
title: `${trans.docs.categoryArchived} · ${groupKey}`,
|
|
109
|
+
options,
|
|
110
|
+
maxVisible,
|
|
111
|
+
initialIndex,
|
|
112
|
+
});
|
|
89
113
|
}
|
|
90
114
|
function buildReaderLinksField(links, maxVisible, initialIndex = 0) {
|
|
91
115
|
const trans = t();
|
|
@@ -100,8 +124,12 @@ function buildSearchResultsField(matches, maxVisible, initialIndex = 0) {
|
|
|
100
124
|
const options = [
|
|
101
125
|
...matches.map((result) => ({
|
|
102
126
|
value: result.path,
|
|
103
|
-
label: displayDocTitle(result.
|
|
104
|
-
|
|
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))),
|
|
105
133
|
})),
|
|
106
134
|
{ value: '__back__', label: backLabel() },
|
|
107
135
|
];
|
|
@@ -114,7 +142,10 @@ function relocalizeStateFields(value, maxVisible) {
|
|
|
114
142
|
if (value.mode === 'files' && currentSectionKey) {
|
|
115
143
|
const section = sections.find((candidate) => candidate.key === currentSectionKey);
|
|
116
144
|
return section
|
|
117
|
-
? {
|
|
145
|
+
? {
|
|
146
|
+
...value,
|
|
147
|
+
filesField: buildFilesField(section, maxVisible, value.filesField?.selectedIndex),
|
|
148
|
+
}
|
|
118
149
|
: value;
|
|
119
150
|
}
|
|
120
151
|
if (value.mode === 'archivedGroups') {
|
|
@@ -149,10 +180,95 @@ function goToSections() {
|
|
|
149
180
|
currentSearchResults = [];
|
|
150
181
|
state = { mode: 'sections', sectionsField: buildSectionsField() };
|
|
151
182
|
}
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
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
|
+
}
|
|
156
272
|
async function openInReader(ctx, path, pushCurrent) {
|
|
157
273
|
const requestId = ++readerRequestId;
|
|
158
274
|
const previousState = state;
|
|
@@ -168,7 +284,12 @@ async function openInReader(ctx, path, pushCurrent) {
|
|
|
168
284
|
readerNavStack.push(previousPath);
|
|
169
285
|
readerCurrentPath = path;
|
|
170
286
|
readerLoadingPrevState = null;
|
|
171
|
-
state = {
|
|
287
|
+
state = {
|
|
288
|
+
mode: 'reader',
|
|
289
|
+
readerTitle: doc.title,
|
|
290
|
+
readerLines: doc.lines,
|
|
291
|
+
readerLinks: doc.links,
|
|
292
|
+
};
|
|
172
293
|
ctx.resetScroll();
|
|
173
294
|
}
|
|
174
295
|
catch {
|
|
@@ -176,15 +297,12 @@ async function openInReader(ctx, path, pushCurrent) {
|
|
|
176
297
|
return;
|
|
177
298
|
readerLoadingPrevState = null;
|
|
178
299
|
const fallbackState = pushCurrent && previousState.mode === 'reader'
|
|
179
|
-
?
|
|
300
|
+
? withoutReaderLinksField(previousState)
|
|
180
301
|
: previousState;
|
|
181
302
|
state = { ...fallbackState, errorMessage: t().docs.loadError };
|
|
182
303
|
}
|
|
183
304
|
ctx.rerender();
|
|
184
305
|
}
|
|
185
|
-
/** Enters the reader from a file-listing mode (files/archivedFiles/
|
|
186
|
-
* searchResults) -- saves that listing so Esc can restore it once the nav
|
|
187
|
-
* stack (built by following links from here) empties back out. */
|
|
188
306
|
function enterReaderFrom(ctx, path) {
|
|
189
307
|
readerPrevState = state;
|
|
190
308
|
readerNavStack = [];
|
|
@@ -208,23 +326,26 @@ export const docsView = {
|
|
|
208
326
|
}
|
|
209
327
|
return;
|
|
210
328
|
}
|
|
329
|
+
const requestId = ++sectionsRequestId;
|
|
211
330
|
state = { mode: 'loading' };
|
|
212
331
|
ctx.rerender();
|
|
213
332
|
try {
|
|
214
|
-
|
|
333
|
+
const nextSections = await fetchSections();
|
|
334
|
+
if (requestId !== sectionsRequestId)
|
|
335
|
+
return;
|
|
336
|
+
sections = nextSections;
|
|
215
337
|
loaded = true;
|
|
216
338
|
loadedLanguage = getCurrentLanguage();
|
|
217
339
|
goToSections();
|
|
218
340
|
}
|
|
219
341
|
catch {
|
|
342
|
+
if (requestId !== sectionsRequestId)
|
|
343
|
+
return;
|
|
220
344
|
state = { mode: 'error', errorMessage: t().docs.loadError };
|
|
221
345
|
}
|
|
222
346
|
ctx.rerender();
|
|
223
347
|
},
|
|
224
348
|
render(ctx) {
|
|
225
|
-
// Sync every visible field's scroll window to the *current* terminal
|
|
226
|
-
// size on every frame (not just construction time) — this is what
|
|
227
|
-
// keeps a long list correctly windowed across a live resize.
|
|
228
349
|
const maxVisible = computeMaxVisible(ctx.bodyRows);
|
|
229
350
|
state.filesField?.setMaxVisible(maxVisible);
|
|
230
351
|
state.archivedGroupsField?.setMaxVisible(maxVisible);
|
|
@@ -236,10 +357,18 @@ export const docsView = {
|
|
|
236
357
|
capturesInput() {
|
|
237
358
|
return state.mode === 'search';
|
|
238
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
|
+
},
|
|
239
368
|
footerHint(tabCount, cols = Number.POSITIVE_INFINITY) {
|
|
240
369
|
if (state.mode === 'search')
|
|
241
370
|
return captureFooterHint(cols);
|
|
242
|
-
if (state.mode === 'loading' || state.mode === 'error')
|
|
371
|
+
if (state.mode === 'loading' || state.mode === 'searchLoading' || state.mode === 'error')
|
|
243
372
|
return passiveFooterHint(tabCount, cols);
|
|
244
373
|
if (state.mode === 'readerLoading') {
|
|
245
374
|
return fitFooterHint(cols, `${digitTabHint(tabCount)}q ${t().menu.hintQuit}`, `${digitTabHint(tabCount)}q`, 'q');
|
|
@@ -257,6 +386,11 @@ export const docsView = {
|
|
|
257
386
|
return undefined;
|
|
258
387
|
},
|
|
259
388
|
handleBack(ctx) {
|
|
389
|
+
if (state.mode === 'searchLoading') {
|
|
390
|
+
searchRequestId++;
|
|
391
|
+
goToSections();
|
|
392
|
+
return true;
|
|
393
|
+
}
|
|
260
394
|
if (state.mode === 'reader' || state.mode === 'readerLoading') {
|
|
261
395
|
if (state.mode === 'readerLoading') {
|
|
262
396
|
const previousState = readerLoadingPrevState;
|
|
@@ -268,7 +402,7 @@ export const docsView = {
|
|
|
268
402
|
return true;
|
|
269
403
|
}
|
|
270
404
|
if (state.readerLinksField) {
|
|
271
|
-
state =
|
|
405
|
+
state = withoutReaderLinksField(state);
|
|
272
406
|
return true;
|
|
273
407
|
}
|
|
274
408
|
const prevPath = readerNavStack.pop();
|
|
@@ -285,7 +419,10 @@ export const docsView = {
|
|
|
285
419
|
return false;
|
|
286
420
|
}
|
|
287
421
|
if (state.mode === 'archivedFiles') {
|
|
288
|
-
state = {
|
|
422
|
+
state = {
|
|
423
|
+
mode: 'archivedGroups',
|
|
424
|
+
archivedGroupsField: buildArchivedGroupsField(archivedGroups, computeMaxVisible(ctx.bodyRows)),
|
|
425
|
+
};
|
|
289
426
|
return true;
|
|
290
427
|
}
|
|
291
428
|
if (state.mode === 'search') {
|
|
@@ -293,7 +430,9 @@ export const docsView = {
|
|
|
293
430
|
goToSections();
|
|
294
431
|
return true;
|
|
295
432
|
}
|
|
296
|
-
if (state.mode === 'files' ||
|
|
433
|
+
if (state.mode === 'files' ||
|
|
434
|
+
state.mode === 'archivedGroups' ||
|
|
435
|
+
state.mode === 'searchResults') {
|
|
297
436
|
goToSections();
|
|
298
437
|
return true;
|
|
299
438
|
}
|
|
@@ -301,7 +440,7 @@ export const docsView = {
|
|
|
301
440
|
},
|
|
302
441
|
handleKey(key, ctx) {
|
|
303
442
|
if (state.mode !== 'error' && state.errorMessage)
|
|
304
|
-
state =
|
|
443
|
+
state = withoutErrorMessage(state);
|
|
305
444
|
switch (state.mode) {
|
|
306
445
|
case 'sections': {
|
|
307
446
|
const result = state.sectionsField?.handleKey(key);
|
|
@@ -309,11 +448,21 @@ export const docsView = {
|
|
|
309
448
|
return;
|
|
310
449
|
if (result.selected === '__search__') {
|
|
311
450
|
setVimKeysActive(false);
|
|
312
|
-
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
|
+
};
|
|
313
459
|
return;
|
|
314
460
|
}
|
|
315
461
|
if (result.selected === '__refresh__') {
|
|
316
462
|
clearDocsCache();
|
|
463
|
+
sectionsRequestId++;
|
|
464
|
+
metadataRequestId++;
|
|
465
|
+
searchRequestId++;
|
|
317
466
|
loaded = false;
|
|
318
467
|
loadedLanguage = null;
|
|
319
468
|
sections = [];
|
|
@@ -326,7 +475,7 @@ export const docsView = {
|
|
|
326
475
|
readerPrevState = null;
|
|
327
476
|
readerLoadingPrevState = null;
|
|
328
477
|
readerRequestId++;
|
|
329
|
-
void docsView.load
|
|
478
|
+
void docsView.load(ctx);
|
|
330
479
|
return;
|
|
331
480
|
}
|
|
332
481
|
if (result.selected === '__browser__') {
|
|
@@ -337,14 +486,17 @@ export const docsView = {
|
|
|
337
486
|
if (!section)
|
|
338
487
|
return;
|
|
339
488
|
if (section.key === 'archived') {
|
|
489
|
+
metadataRequestId++;
|
|
340
490
|
currentSectionKey = null;
|
|
341
491
|
currentArchivedGroupKey = null;
|
|
342
492
|
archivedGroups = getArchivedGroups(section.files);
|
|
343
|
-
state = {
|
|
493
|
+
state = {
|
|
494
|
+
mode: 'archivedGroups',
|
|
495
|
+
archivedGroupsField: buildArchivedGroupsField(archivedGroups, computeMaxVisible(ctx.bodyRows)),
|
|
496
|
+
};
|
|
344
497
|
}
|
|
345
498
|
else {
|
|
346
|
-
|
|
347
|
-
state = { mode: 'files', filesField: buildFilesField(section, computeMaxVisible(ctx.bodyRows)) };
|
|
499
|
+
void openSectionFiles(ctx, section);
|
|
348
500
|
}
|
|
349
501
|
return;
|
|
350
502
|
}
|
|
@@ -367,9 +519,8 @@ export const docsView = {
|
|
|
367
519
|
goToSections();
|
|
368
520
|
return;
|
|
369
521
|
}
|
|
370
|
-
currentArchivedGroupKey = result.selected;
|
|
371
522
|
const groupFiles = archivedGroups.get(result.selected) ?? [];
|
|
372
|
-
|
|
523
|
+
void openArchivedFiles(ctx, result.selected, groupFiles);
|
|
373
524
|
return;
|
|
374
525
|
}
|
|
375
526
|
case 'archivedFiles': {
|
|
@@ -378,7 +529,10 @@ export const docsView = {
|
|
|
378
529
|
return;
|
|
379
530
|
if (result.selected === '__back__') {
|
|
380
531
|
currentArchivedGroupKey = null;
|
|
381
|
-
state = {
|
|
532
|
+
state = {
|
|
533
|
+
mode: 'archivedGroups',
|
|
534
|
+
archivedGroupsField: buildArchivedGroupsField(archivedGroups, computeMaxVisible(ctx.bodyRows)),
|
|
535
|
+
};
|
|
382
536
|
return;
|
|
383
537
|
}
|
|
384
538
|
enterReaderFrom(ctx, result.selected);
|
|
@@ -398,19 +552,7 @@ export const docsView = {
|
|
|
398
552
|
goToSections();
|
|
399
553
|
return;
|
|
400
554
|
}
|
|
401
|
-
void
|
|
402
|
-
const matches = all.filter((item) => item.path.toLowerCase().includes(query));
|
|
403
|
-
currentSearchResults = matches;
|
|
404
|
-
state = {
|
|
405
|
-
mode: 'searchResults',
|
|
406
|
-
searchResultsEmpty: matches.length === 0,
|
|
407
|
-
searchResultsField: buildSearchResultsField(matches, computeMaxVisible(ctx.bodyRows)),
|
|
408
|
-
};
|
|
409
|
-
ctx.rerender();
|
|
410
|
-
}).catch(() => {
|
|
411
|
-
state = { mode: 'error', errorMessage: t().docs.loadError };
|
|
412
|
-
ctx.rerender();
|
|
413
|
-
});
|
|
555
|
+
void runSearch(ctx, query);
|
|
414
556
|
}
|
|
415
557
|
return;
|
|
416
558
|
}
|
|
@@ -429,19 +571,18 @@ export const docsView = {
|
|
|
429
571
|
if (state.readerLinksField) {
|
|
430
572
|
const result = state.readerLinksField.handleKey(key);
|
|
431
573
|
if (result.cancelled || result.selected === '__back__') {
|
|
432
|
-
state =
|
|
574
|
+
state = withoutReaderLinksField(state);
|
|
433
575
|
return;
|
|
434
576
|
}
|
|
435
577
|
if (result.selected)
|
|
436
578
|
void openInReader(ctx, result.selected, true);
|
|
437
579
|
return;
|
|
438
580
|
}
|
|
439
|
-
// 'f' (Vimium/vim-browser-extension convention: "follow a link"),
|
|
440
|
-
// not 'l' -- core/vim-keys.ts already reserves 'l' globally,
|
|
441
|
-
// ranger-style, as an alias for Enter/confirm (vimActive defaults
|
|
442
|
-
// to true), so a literal 'l' keypress never even reaches here.
|
|
443
581
|
if (key === 'f' && (state.readerLinks?.length ?? 0) > 0) {
|
|
444
|
-
state = {
|
|
582
|
+
state = {
|
|
583
|
+
...state,
|
|
584
|
+
readerLinksField: buildReaderLinksField(state.readerLinks ?? [], computeMaxVisible(ctx.bodyRows)),
|
|
585
|
+
};
|
|
445
586
|
return;
|
|
446
587
|
}
|
|
447
588
|
if (key === 'b') {
|
|
@@ -450,7 +591,10 @@ export const docsView = {
|
|
|
450
591
|
}
|
|
451
592
|
return;
|
|
452
593
|
}
|
|
453
|
-
|
|
594
|
+
case 'error':
|
|
595
|
+
case 'loading':
|
|
596
|
+
case 'readerLoading':
|
|
597
|
+
case 'searchLoading':
|
|
454
598
|
return;
|
|
455
599
|
}
|
|
456
600
|
},
|