@nbtca/prompt 1.5.0 → 1.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/app/app.js +142 -13
- package/dist/app/chrome.js +13 -2
- package/dist/app/frame.js +14 -2
- package/dist/app/keys.js +109 -1
- package/dist/app/views/docs-render.js +3 -2
- package/dist/app/views/docs.js +90 -25
- package/dist/app/views/events-render.js +2 -1
- package/dist/app/views/events.js +13 -2
- package/dist/app/views/home.js +46 -24
- package/dist/app/views/schedule-render.js +4 -3
- package/dist/app/views/schedule.js +101 -33
- package/dist/cli.js +570 -0
- package/dist/config/preferences.js +7 -0
- package/dist/core/canvas.js +1 -0
- package/dist/core/components/menu.js +23 -22
- package/dist/core/components/spinner.js +17 -1
- package/dist/core/text.js +7 -3
- package/dist/core/vim-keys.js +149 -6
- package/dist/features/calendar-store.js +27 -0
- package/dist/features/calendar.js +55 -6
- package/dist/features/docs-client.js +225 -0
- package/dist/features/docs.js +224 -68
- package/dist/features/links.js +51 -0
- package/dist/features/status.js +83 -14
- package/dist/features/student-timetable.js +1 -2
- package/dist/features/theme.js +3 -3
- package/dist/features/update.js +3 -2
- package/dist/i18n/locales/en.json +7 -2
- package/dist/i18n/locales/zh.json +7 -2
- package/dist/index.js +5 -498
- package/package.json +2 -1
package/dist/app/views/docs.js
CHANGED
|
@@ -4,9 +4,10 @@ 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 {
|
|
7
|
+
import { glyph } from '../../core/theme.js';
|
|
8
|
+
import { fmt, getCurrentLanguage, t } from '../../i18n/index.js';
|
|
8
9
|
import { sanitizeTerminalLine, truncate } from '../../core/text.js';
|
|
9
|
-
import { localizeDocSections, fetchSections, fetchDocMetadata, fetchSectionMetadata, searchDocuments, getArchivedGroups, displayDocTitle, loadDocForReader, openDocsInBrowser, clearDocsCache, } from '../../features/docs.js';
|
|
10
|
+
import { localizeDocSections, fetchSections, fetchDocMetadata, fetchSectionMetadata, searchDocuments, getArchivedGroups, displayDocTitle, loadDocForReader, openDocsInBrowser, docsUrlFromPath, clearDocsCache, } from '../../features/docs.js';
|
|
10
11
|
let state = { mode: 'loading' };
|
|
11
12
|
let sections = [];
|
|
12
13
|
let archivedGroups = new Map();
|
|
@@ -23,7 +24,11 @@ let readerNavStack = [];
|
|
|
23
24
|
let readerPrevState = null;
|
|
24
25
|
let readerLoadingPrevState = null;
|
|
25
26
|
let readerRequestId = 0;
|
|
27
|
+
let lifecycleGeneration = 0;
|
|
26
28
|
const DOC_HINT_WIDTH = 44;
|
|
29
|
+
function isLifecycleActive(ctx, generation) {
|
|
30
|
+
return generation === lifecycleGeneration && ctx.signal?.aborted !== true;
|
|
31
|
+
}
|
|
27
32
|
function backLabel() {
|
|
28
33
|
return t().common.back;
|
|
29
34
|
}
|
|
@@ -43,6 +48,27 @@ function withoutErrorMessage(value) {
|
|
|
43
48
|
delete next.errorMessage;
|
|
44
49
|
return next;
|
|
45
50
|
}
|
|
51
|
+
async function runBrowserOpen(ctx, path) {
|
|
52
|
+
let opened;
|
|
53
|
+
await ctx.runClassic(async () => {
|
|
54
|
+
opened =
|
|
55
|
+
path === undefined
|
|
56
|
+
? await openDocsInBrowser(undefined, ctx.signal)
|
|
57
|
+
: await openDocsInBrowser(path, ctx.signal);
|
|
58
|
+
});
|
|
59
|
+
return opened;
|
|
60
|
+
}
|
|
61
|
+
async function openBrowserFromView(ctx, path, generation = lifecycleGeneration) {
|
|
62
|
+
const opened = await runBrowserOpen(ctx, path);
|
|
63
|
+
if (!isLifecycleActive(ctx, generation) || opened === true)
|
|
64
|
+
return;
|
|
65
|
+
const url = docsUrlFromPath(path);
|
|
66
|
+
state = {
|
|
67
|
+
...state,
|
|
68
|
+
errorMessage: sanitizeTerminalLine(`${t().docs.browserError}. ${fmt(t().links.openManually, { url })}`),
|
|
69
|
+
};
|
|
70
|
+
ctx.rerender();
|
|
71
|
+
}
|
|
46
72
|
function buildSectionsField() {
|
|
47
73
|
const trans = t();
|
|
48
74
|
const options = [
|
|
@@ -184,6 +210,9 @@ function replaceSection(section) {
|
|
|
184
210
|
sections = sections.map((current) => (current.key === section.key ? section : current));
|
|
185
211
|
}
|
|
186
212
|
async function openSectionFiles(ctx, section) {
|
|
213
|
+
const generation = lifecycleGeneration;
|
|
214
|
+
if (!isLifecycleActive(ctx, generation))
|
|
215
|
+
return;
|
|
187
216
|
const requestId = ++metadataRequestId;
|
|
188
217
|
currentSectionKey = section.key;
|
|
189
218
|
state = {
|
|
@@ -192,8 +221,9 @@ async function openSectionFiles(ctx, section) {
|
|
|
192
221
|
};
|
|
193
222
|
ctx.rerender();
|
|
194
223
|
try {
|
|
195
|
-
const hydrated = await fetchSectionMetadata(section);
|
|
196
|
-
if (
|
|
224
|
+
const hydrated = await fetchSectionMetadata(section, ctx.signal);
|
|
225
|
+
if (!isLifecycleActive(ctx, generation) ||
|
|
226
|
+
requestId !== metadataRequestId ||
|
|
197
227
|
state.mode !== 'files' ||
|
|
198
228
|
currentSectionKey !== section.key)
|
|
199
229
|
return;
|
|
@@ -205,15 +235,20 @@ async function openSectionFiles(ctx, section) {
|
|
|
205
235
|
};
|
|
206
236
|
}
|
|
207
237
|
catch {
|
|
208
|
-
if (
|
|
238
|
+
if (!isLifecycleActive(ctx, generation) ||
|
|
239
|
+
requestId !== metadataRequestId ||
|
|
209
240
|
state.mode !== 'files' ||
|
|
210
241
|
currentSectionKey !== section.key)
|
|
211
242
|
return;
|
|
212
243
|
state = { ...state, errorMessage: t().docs.loadError };
|
|
213
244
|
}
|
|
214
|
-
ctx
|
|
245
|
+
if (isLifecycleActive(ctx, generation))
|
|
246
|
+
ctx.rerender();
|
|
215
247
|
}
|
|
216
248
|
async function openArchivedFiles(ctx, groupKey, groupFiles) {
|
|
249
|
+
const generation = lifecycleGeneration;
|
|
250
|
+
if (!isLifecycleActive(ctx, generation))
|
|
251
|
+
return;
|
|
217
252
|
const requestId = ++metadataRequestId;
|
|
218
253
|
currentArchivedGroupKey = groupKey;
|
|
219
254
|
state = {
|
|
@@ -222,8 +257,9 @@ async function openArchivedFiles(ctx, groupKey, groupFiles) {
|
|
|
222
257
|
};
|
|
223
258
|
ctx.rerender();
|
|
224
259
|
try {
|
|
225
|
-
const hydrated = await fetchDocMetadata(groupFiles);
|
|
226
|
-
if (
|
|
260
|
+
const hydrated = await fetchDocMetadata(groupFiles, ctx.signal);
|
|
261
|
+
if (!isLifecycleActive(ctx, generation) ||
|
|
262
|
+
requestId !== metadataRequestId ||
|
|
227
263
|
state.mode !== 'archivedFiles' ||
|
|
228
264
|
currentArchivedGroupKey !== groupKey)
|
|
229
265
|
return;
|
|
@@ -234,7 +270,8 @@ async function openArchivedFiles(ctx, groupKey, groupFiles) {
|
|
|
234
270
|
};
|
|
235
271
|
}
|
|
236
272
|
catch {
|
|
237
|
-
if (
|
|
273
|
+
if (!isLifecycleActive(ctx, generation) ||
|
|
274
|
+
requestId !== metadataRequestId ||
|
|
238
275
|
state.mode !== 'archivedFiles' ||
|
|
239
276
|
currentArchivedGroupKey !== groupKey)
|
|
240
277
|
return;
|
|
@@ -243,15 +280,19 @@ async function openArchivedFiles(ctx, groupKey, groupFiles) {
|
|
|
243
280
|
errorMessage: t().docs.loadError,
|
|
244
281
|
};
|
|
245
282
|
}
|
|
246
|
-
ctx
|
|
283
|
+
if (isLifecycleActive(ctx, generation))
|
|
284
|
+
ctx.rerender();
|
|
247
285
|
}
|
|
248
286
|
async function runSearch(ctx, query) {
|
|
287
|
+
const generation = lifecycleGeneration;
|
|
288
|
+
if (!isLifecycleActive(ctx, generation))
|
|
289
|
+
return;
|
|
249
290
|
const requestId = ++searchRequestId;
|
|
250
291
|
state = { mode: 'searchLoading' };
|
|
251
292
|
ctx.rerender();
|
|
252
293
|
try {
|
|
253
|
-
const matches = await searchDocuments(query);
|
|
254
|
-
if (requestId !== searchRequestId)
|
|
294
|
+
const matches = await searchDocuments(query, ctx.signal);
|
|
295
|
+
if (!isLifecycleActive(ctx, generation) || requestId !== searchRequestId)
|
|
255
296
|
return;
|
|
256
297
|
currentSearchResults = matches;
|
|
257
298
|
state = {
|
|
@@ -261,15 +302,19 @@ async function runSearch(ctx, query) {
|
|
|
261
302
|
};
|
|
262
303
|
}
|
|
263
304
|
catch {
|
|
264
|
-
if (requestId !== searchRequestId)
|
|
305
|
+
if (!isLifecycleActive(ctx, generation) || requestId !== searchRequestId)
|
|
265
306
|
return;
|
|
266
307
|
currentSearchResults = [];
|
|
267
308
|
goToSections();
|
|
268
309
|
state = { ...state, errorMessage: t().docs.loadError };
|
|
269
310
|
}
|
|
270
|
-
ctx
|
|
311
|
+
if (isLifecycleActive(ctx, generation))
|
|
312
|
+
ctx.rerender();
|
|
271
313
|
}
|
|
272
314
|
async function openInReader(ctx, path, pushCurrent) {
|
|
315
|
+
const generation = lifecycleGeneration;
|
|
316
|
+
if (!isLifecycleActive(ctx, generation))
|
|
317
|
+
return;
|
|
273
318
|
const requestId = ++readerRequestId;
|
|
274
319
|
const previousState = state;
|
|
275
320
|
const previousPath = readerCurrentPath;
|
|
@@ -277,8 +322,8 @@ async function openInReader(ctx, path, pushCurrent) {
|
|
|
277
322
|
state = { mode: 'readerLoading' };
|
|
278
323
|
ctx.rerender();
|
|
279
324
|
try {
|
|
280
|
-
const doc = await loadDocForReader(path);
|
|
281
|
-
if (requestId !== readerRequestId)
|
|
325
|
+
const doc = await loadDocForReader(path, ctx.signal);
|
|
326
|
+
if (!isLifecycleActive(ctx, generation) || requestId !== readerRequestId)
|
|
282
327
|
return;
|
|
283
328
|
if (pushCurrent && previousPath)
|
|
284
329
|
readerNavStack.push(previousPath);
|
|
@@ -293,7 +338,7 @@ async function openInReader(ctx, path, pushCurrent) {
|
|
|
293
338
|
ctx.resetScroll();
|
|
294
339
|
}
|
|
295
340
|
catch {
|
|
296
|
-
if (requestId !== readerRequestId)
|
|
341
|
+
if (!isLifecycleActive(ctx, generation) || requestId !== readerRequestId)
|
|
297
342
|
return;
|
|
298
343
|
readerLoadingPrevState = null;
|
|
299
344
|
const fallbackState = pushCurrent && previousState.mode === 'reader'
|
|
@@ -301,7 +346,8 @@ async function openInReader(ctx, path, pushCurrent) {
|
|
|
301
346
|
: previousState;
|
|
302
347
|
state = { ...fallbackState, errorMessage: t().docs.loadError };
|
|
303
348
|
}
|
|
304
|
-
ctx
|
|
349
|
+
if (isLifecycleActive(ctx, generation))
|
|
350
|
+
ctx.rerender();
|
|
305
351
|
}
|
|
306
352
|
function enterReaderFrom(ctx, path) {
|
|
307
353
|
readerPrevState = state;
|
|
@@ -313,6 +359,9 @@ export const docsView = {
|
|
|
313
359
|
id: 'docs',
|
|
314
360
|
title: t().menu.docs,
|
|
315
361
|
async load(ctx) {
|
|
362
|
+
const generation = loaded ? lifecycleGeneration : ++lifecycleGeneration;
|
|
363
|
+
if (!isLifecycleActive(ctx, generation))
|
|
364
|
+
return;
|
|
316
365
|
if (loaded) {
|
|
317
366
|
const language = getCurrentLanguage();
|
|
318
367
|
if (loadedLanguage !== language) {
|
|
@@ -330,8 +379,8 @@ export const docsView = {
|
|
|
330
379
|
state = { mode: 'loading' };
|
|
331
380
|
ctx.rerender();
|
|
332
381
|
try {
|
|
333
|
-
const nextSections = await fetchSections();
|
|
334
|
-
if (requestId !== sectionsRequestId)
|
|
382
|
+
const nextSections = await fetchSections(ctx.signal);
|
|
383
|
+
if (!isLifecycleActive(ctx, generation) || requestId !== sectionsRequestId)
|
|
335
384
|
return;
|
|
336
385
|
sections = nextSections;
|
|
337
386
|
loaded = true;
|
|
@@ -339,11 +388,21 @@ export const docsView = {
|
|
|
339
388
|
goToSections();
|
|
340
389
|
}
|
|
341
390
|
catch {
|
|
342
|
-
if (requestId !== sectionsRequestId)
|
|
391
|
+
if (!isLifecycleActive(ctx, generation) || requestId !== sectionsRequestId)
|
|
343
392
|
return;
|
|
344
393
|
state = { mode: 'error', errorMessage: t().docs.loadError };
|
|
345
394
|
}
|
|
346
|
-
ctx
|
|
395
|
+
if (isLifecycleActive(ctx, generation))
|
|
396
|
+
ctx.rerender();
|
|
397
|
+
},
|
|
398
|
+
dispose() {
|
|
399
|
+
lifecycleGeneration += 1;
|
|
400
|
+
sectionsRequestId += 1;
|
|
401
|
+
metadataRequestId += 1;
|
|
402
|
+
searchRequestId += 1;
|
|
403
|
+
readerRequestId += 1;
|
|
404
|
+
clearDocsCache();
|
|
405
|
+
setVimKeysActive(true);
|
|
347
406
|
},
|
|
348
407
|
render(ctx) {
|
|
349
408
|
const maxVisible = computeMaxVisible(ctx.bodyRows);
|
|
@@ -354,9 +413,15 @@ export const docsView = {
|
|
|
354
413
|
state.readerLinksField?.setMaxVisible(maxVisible);
|
|
355
414
|
return renderDocs(state, ctx.size.cols, ctx.bodyRows);
|
|
356
415
|
},
|
|
416
|
+
isBusy() {
|
|
417
|
+
return (state.mode === 'loading' || state.mode === 'searchLoading' || state.mode === 'readerLoading');
|
|
418
|
+
},
|
|
357
419
|
capturesInput() {
|
|
358
420
|
return state.mode === 'search';
|
|
359
421
|
},
|
|
422
|
+
scrollsBody() {
|
|
423
|
+
return state.mode === 'reader' && state.readerLinksField === undefined;
|
|
424
|
+
},
|
|
360
425
|
capturesPageKeys() {
|
|
361
426
|
return (state.mode === 'sections' ||
|
|
362
427
|
state.mode === 'files' ||
|
|
@@ -378,7 +443,7 @@ export const docsView = {
|
|
|
378
443
|
const dot = pickIcon('·', '-');
|
|
379
444
|
const hasLinks = (state.readerLinks?.length ?? 0) > 0;
|
|
380
445
|
const linkHint = hasLinks ? `f ${trans.docs.readerLinksHint} ${dot} ` : '';
|
|
381
|
-
const pageHint =
|
|
446
|
+
const pageHint = `${glyph.updown()} PgUp/PgDn ${dot} `;
|
|
382
447
|
const localFull = `${pageHint}${linkHint}b ${trans.docs.openBrowser} ${dot} Esc ${dot} q ${trans.menu.hintQuit}`;
|
|
383
448
|
const localCompact = `${pageHint}${hasLinks ? `f ${dot} ` : ''}b ${dot} Esc ${dot} q`;
|
|
384
449
|
return fitFooterHint(cols, `${digitTabHint(tabCount)}${localFull}`, localFull, localCompact, `${hasLinks ? 'f ' : ''}b Esc q`, 'Esc q', 'q');
|
|
@@ -479,7 +544,7 @@ export const docsView = {
|
|
|
479
544
|
return;
|
|
480
545
|
}
|
|
481
546
|
if (result.selected === '__browser__') {
|
|
482
|
-
void ctx
|
|
547
|
+
void openBrowserFromView(ctx);
|
|
483
548
|
return;
|
|
484
549
|
}
|
|
485
550
|
const section = sections.find((s) => s.key === result.selected);
|
|
@@ -586,7 +651,7 @@ export const docsView = {
|
|
|
586
651
|
return;
|
|
587
652
|
}
|
|
588
653
|
if (key === 'b') {
|
|
589
|
-
void ctx
|
|
654
|
+
void openBrowserFromView(ctx, readerCurrentPath ?? undefined);
|
|
590
655
|
return;
|
|
591
656
|
}
|
|
592
657
|
return;
|
|
@@ -4,6 +4,7 @@ import { renderListFieldWithContext } from '../fields/list-field.js';
|
|
|
4
4
|
import { renderCountdownBanner, renderEventBrief } from '../../features/calendar.js';
|
|
5
5
|
import { renderHeatmap } from '../../features/calendar-heatmap.js';
|
|
6
6
|
import { wrapAnsiWithIndent } from '../../core/text.js';
|
|
7
|
+
import { loadingLines } from '../../core/components/spinner.js';
|
|
7
8
|
function wrappedIndentedLines(label, cols, style) {
|
|
8
9
|
return wrapAnsiWithIndent(style(label), cols ?? Number.POSITIVE_INFINITY, space.indent);
|
|
9
10
|
}
|
|
@@ -61,7 +62,7 @@ export function renderEvents(state, now, bodyRows = 100, cols) {
|
|
|
61
62
|
const trans = t();
|
|
62
63
|
switch (state.mode) {
|
|
63
64
|
case 'loading':
|
|
64
|
-
return
|
|
65
|
+
return loadingLines(trans.calendar.loading, cols);
|
|
65
66
|
case 'hub':
|
|
66
67
|
return renderHubBody(state, now, bodyRows, cols);
|
|
67
68
|
case 'heatmap':
|
package/dist/app/views/events.js
CHANGED
|
@@ -93,21 +93,32 @@ export const eventsView = {
|
|
|
93
93
|
id: 'events',
|
|
94
94
|
title: t().menu.events,
|
|
95
95
|
async load(ctx) {
|
|
96
|
+
if (ctx.signal?.aborted)
|
|
97
|
+
return;
|
|
96
98
|
state = { mode: 'loading' };
|
|
97
99
|
ctx.rerender();
|
|
98
100
|
try {
|
|
99
|
-
|
|
101
|
+
const loadedCalendar = await loadCalendarOrThrow(ctx.signal);
|
|
102
|
+
if (ctx.signal?.aborted)
|
|
103
|
+
return;
|
|
104
|
+
calendar = loadedCalendar;
|
|
100
105
|
goToHub();
|
|
101
106
|
}
|
|
102
107
|
catch {
|
|
108
|
+
if (ctx.signal?.aborted)
|
|
109
|
+
return;
|
|
103
110
|
state = { mode: 'error', errorMessage: t().calendar.error };
|
|
104
111
|
}
|
|
105
|
-
ctx.
|
|
112
|
+
if (!ctx.signal?.aborted)
|
|
113
|
+
ctx.rerender();
|
|
106
114
|
},
|
|
107
115
|
render(ctx) {
|
|
108
116
|
state.listField?.setMaxVisible(computeMaxVisible(ctx.bodyRows));
|
|
109
117
|
return renderEvents(state, new Date(), ctx.bodyRows, ctx.size.cols);
|
|
110
118
|
},
|
|
119
|
+
isBusy() {
|
|
120
|
+
return state.mode === 'loading';
|
|
121
|
+
},
|
|
111
122
|
capturesInput() {
|
|
112
123
|
return state.mode === 'search';
|
|
113
124
|
},
|
package/dist/app/views/home.js
CHANGED
|
@@ -3,11 +3,12 @@ import { t } from '../../i18n/index.js';
|
|
|
3
3
|
import { pickIcon } from '../../core/icons.js';
|
|
4
4
|
import { padEndV, visualWidth, wrapAnsiWithIndent } from '../../core/text.js';
|
|
5
5
|
import { peekNextClassLine, peekTodayLines, peekWeekAheadInfo, peekUnresolvedCount, } from '../../features/schedule-view.js';
|
|
6
|
-
import { loadCalendarOrThrow, toDisplayEvent, renderEventBrief } from '../../features/calendar.js';
|
|
6
|
+
import { loadCalendarOrThrow, peekCalendar, toDisplayEvent, renderEventBrief, } from '../../features/calendar.js';
|
|
7
7
|
import { weekdayShortLabel } from '../../features/schedule-render.js';
|
|
8
8
|
import { addLocalDays } from '../../core/calendar-day.js';
|
|
9
9
|
import { passiveFooterHint } from '../chrome.js';
|
|
10
10
|
import { campusWeekday } from '@nbtca/nbtcal/timetable';
|
|
11
|
+
import { loadingLines } from '../../core/components/spinner.js';
|
|
11
12
|
const WEEKDAYS = [1, 2, 3, 4, 5, 6, 7];
|
|
12
13
|
function wrappedIndentedLines(label, cols, style) {
|
|
13
14
|
return wrapAnsiWithIndent(style(label), cols, space.indent);
|
|
@@ -15,9 +16,6 @@ function wrappedIndentedLines(label, cols, style) {
|
|
|
15
16
|
function panelHeading(label, cols) {
|
|
16
17
|
return wrappedIndentedLines(label, cols, type.heading);
|
|
17
18
|
}
|
|
18
|
-
function loadingLines(cols) {
|
|
19
|
-
return wrappedIndentedLines(t().common.loading, cols, type.hint);
|
|
20
|
-
}
|
|
21
19
|
function wrappedRenderedLines(line, cols) {
|
|
22
20
|
const content = line.startsWith(space.indent) ? line.slice(space.indent.length) : line;
|
|
23
21
|
return wrappedIndentedLines(content, cols, (value) => value);
|
|
@@ -111,7 +109,10 @@ export function renderHome(data, now, bodyRows = 100, cols = 80) {
|
|
|
111
109
|
lines.push(...wrappedIndentedLines(`${pickIcon('⚠', '!')} ${trans.timetable.hubUnresolved} · ${data.unresolvedCount}`, cols, c.warn));
|
|
112
110
|
lines.push('');
|
|
113
111
|
}
|
|
114
|
-
|
|
112
|
+
const eventsStale = data.eventsLoadFailed === true && (data.eventLines?.length ?? 0) > 0;
|
|
113
|
+
lines.push(...(eventsStale
|
|
114
|
+
? wrappedIndentedLines(`${type.heading(trans.menu.events)} ${type.hint(`${pickIcon('·', '-')} ${trans.calendar.stale}`)}`, cols, (value) => value)
|
|
115
|
+
: panelHeading(trans.menu.events, cols)));
|
|
115
116
|
if (data.eventLines && data.eventLines.length > 0) {
|
|
116
117
|
const remaining = Number.isFinite(bodyRows)
|
|
117
118
|
? Math.max(0, Math.floor(bodyRows) - lines.length)
|
|
@@ -129,7 +130,7 @@ export function renderHome(data, now, bodyRows = 100, cols = 80) {
|
|
|
129
130
|
}
|
|
130
131
|
}
|
|
131
132
|
else if (data.loading) {
|
|
132
|
-
lines.push(...loadingLines(cols));
|
|
133
|
+
lines.push(...loadingLines(trans.common.loading, cols));
|
|
133
134
|
}
|
|
134
135
|
else if (data.eventsLoadFailed) {
|
|
135
136
|
lines.push(...wrappedIndentedLines(trans.calendar.error, cols, type.hint));
|
|
@@ -139,6 +140,25 @@ export function renderHome(data, now, bodyRows = 100, cols = 80) {
|
|
|
139
140
|
}
|
|
140
141
|
return lines;
|
|
141
142
|
}
|
|
143
|
+
const HOME_EVENT_FETCH_CAP = 15;
|
|
144
|
+
function calendarSnapshot(cal, weekAheadInfo) {
|
|
145
|
+
const now = new Date();
|
|
146
|
+
const eventLines = cal
|
|
147
|
+
.upcoming({ days: 30 })
|
|
148
|
+
.slice(0, HOME_EVENT_FETCH_CAP)
|
|
149
|
+
.map((event) => renderEventBrief(toDisplayEvent(event), now));
|
|
150
|
+
if (!weekAheadInfo)
|
|
151
|
+
return { eventLines };
|
|
152
|
+
const weekEnd = addLocalDays(weekAheadInfo.weekStartDate, 7);
|
|
153
|
+
const daySet = new Set(cal.inRange(weekAheadInfo.weekStartDate, weekEnd).map((event) => campusWeekday(event.start)));
|
|
154
|
+
return {
|
|
155
|
+
eventLines,
|
|
156
|
+
weekAhead: {
|
|
157
|
+
classDays: weekAheadInfo.classDays,
|
|
158
|
+
eventDays: WEEKDAYS.map((weekday) => daySet.has(weekday)),
|
|
159
|
+
},
|
|
160
|
+
};
|
|
161
|
+
}
|
|
142
162
|
let data = { loading: true };
|
|
143
163
|
export const homeView = {
|
|
144
164
|
id: 'home',
|
|
@@ -147,6 +167,8 @@ export const homeView = {
|
|
|
147
167
|
return passiveFooterHint(tabCount, cols);
|
|
148
168
|
},
|
|
149
169
|
async load(ctx) {
|
|
170
|
+
if (ctx.signal?.aborted)
|
|
171
|
+
return;
|
|
150
172
|
const weekAheadInfo = peekWeekAheadInfo();
|
|
151
173
|
try {
|
|
152
174
|
data = {
|
|
@@ -160,33 +182,33 @@ export const homeView = {
|
|
|
160
182
|
catch {
|
|
161
183
|
data = { loading: true };
|
|
162
184
|
}
|
|
185
|
+
const cached = peekCalendar();
|
|
186
|
+
if (cached)
|
|
187
|
+
data = { ...data, ...calendarSnapshot(cached, weekAheadInfo) };
|
|
188
|
+
if (ctx.signal?.aborted)
|
|
189
|
+
return;
|
|
163
190
|
ctx.rerender();
|
|
164
|
-
const HOME_EVENT_FETCH_CAP = 15;
|
|
165
191
|
try {
|
|
166
|
-
const cal = await loadCalendarOrThrow();
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
let weekAhead = data.weekAhead;
|
|
171
|
-
if (weekAheadInfo) {
|
|
172
|
-
const weekEnd = addLocalDays(weekAheadInfo.weekStartDate, 7);
|
|
173
|
-
const weekEvents = cal.inRange(weekAheadInfo.weekStartDate, weekEnd);
|
|
174
|
-
const daySet = new Set(weekEvents.map((event) => campusWeekday(event.start)));
|
|
175
|
-
weekAhead = {
|
|
176
|
-
classDays: weekAheadInfo.classDays,
|
|
177
|
-
eventDays: WEEKDAYS.map((weekday) => daySet.has(weekday)),
|
|
178
|
-
};
|
|
179
|
-
}
|
|
180
|
-
data = weekAhead ? { ...data, eventLines, weekAhead } : { ...data, eventLines };
|
|
192
|
+
const cal = await loadCalendarOrThrow(ctx.signal);
|
|
193
|
+
if (ctx.signal?.aborted)
|
|
194
|
+
return;
|
|
195
|
+
data = { ...data, ...calendarSnapshot(cal, weekAheadInfo) };
|
|
181
196
|
}
|
|
182
197
|
catch {
|
|
198
|
+
if (ctx.signal?.aborted)
|
|
199
|
+
return;
|
|
183
200
|
data = { ...data, eventsLoadFailed: true };
|
|
184
201
|
}
|
|
185
202
|
finally {
|
|
186
|
-
|
|
187
|
-
|
|
203
|
+
if (!ctx.signal?.aborted) {
|
|
204
|
+
data = { ...data, loading: false };
|
|
205
|
+
ctx.rerender();
|
|
206
|
+
}
|
|
188
207
|
}
|
|
189
208
|
},
|
|
209
|
+
isBusy() {
|
|
210
|
+
return data.loading === true;
|
|
211
|
+
},
|
|
190
212
|
render(ctx) {
|
|
191
213
|
return renderHome(data, new Date(), ctx.bodyRows, ctx.size.cols);
|
|
192
214
|
},
|
|
@@ -7,6 +7,7 @@ import { renderNextClassBanner, renderWeekGrid, renderUnresolvedItems, renderTod
|
|
|
7
7
|
import { renderEventBrief } from '../../features/calendar.js';
|
|
8
8
|
import { sanitizeTerminalLine, visualWidth, wrapAnsiWithIndent } from '../../core/text.js';
|
|
9
9
|
import { localDayDifference, parseLocalDate, parseLocalMonday } from '../../core/calendar-day.js';
|
|
10
|
+
import { loadingLines } from '../../core/components/spinner.js';
|
|
10
11
|
function heading(label) {
|
|
11
12
|
return `${space.indent}${type.heading(label)}`;
|
|
12
13
|
}
|
|
@@ -189,7 +190,7 @@ function renderPublicBody(state, now, bodyRows, cols) {
|
|
|
189
190
|
const lines = [];
|
|
190
191
|
const w = state.publicWindow;
|
|
191
192
|
if (w === undefined) {
|
|
192
|
-
lines.push(...
|
|
193
|
+
lines.push(...loadingLines(trans.common.loading, cols));
|
|
193
194
|
}
|
|
194
195
|
else if (w === null) {
|
|
195
196
|
lines.push(...hintLines(trans.timetable.publicUnavailable, cols));
|
|
@@ -240,7 +241,7 @@ export function renderSchedule(state, now, bodyRows = 100, cols = 80) {
|
|
|
240
241
|
const trans = t();
|
|
241
242
|
switch (state.mode) {
|
|
242
243
|
case 'loading':
|
|
243
|
-
return
|
|
244
|
+
return loadingLines(trans.common.loading, cols);
|
|
244
245
|
case 'public':
|
|
245
246
|
return renderPublicBody(state, now, bodyRows, cols);
|
|
246
247
|
case 'needsLoginId':
|
|
@@ -251,7 +252,7 @@ export function renderSchedule(state, now, bodyRows = 100, cols = 80) {
|
|
|
251
252
|
case 'needsLoginPassword':
|
|
252
253
|
return state.passwordField?.render(cols) ?? [];
|
|
253
254
|
case 'authenticating':
|
|
254
|
-
return
|
|
255
|
+
return loadingLines(state.statusMessage ?? trans.common.loading, cols);
|
|
255
256
|
case 'needsWeekOne':
|
|
256
257
|
return [
|
|
257
258
|
...(state.errorMessage ? [...hintLines(state.errorMessage, cols), ''] : []),
|