@nbtca/prompt 1.5.9 → 1.5.11
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 +0 -2
- package/dist/app/app.js +22 -3
- package/dist/app/chrome.js +2 -1
- package/dist/app/help.js +44 -0
- package/dist/app/views/docs.js +11 -0
- package/dist/app/views/home.js +13 -6
- package/dist/app/views/schedule-render.js +7 -7
- package/dist/app/views/schedule.js +5 -0
- package/dist/auth/cookie-transport.js +5 -0
- package/dist/auth/nbt-auth.js +15 -7
- package/dist/core/calendar-day.js +3 -0
- package/dist/core/logo.js +4 -2
- package/dist/core/motion.js +2 -1
- package/dist/features/schedule-render.js +6 -26
- package/dist/features/schedule-view.js +4 -4
- package/dist/features/student-timetable.js +2 -0
- package/dist/i18n/locales/en.json +16 -0
- package/dist/i18n/locales/zh.json +16 -0
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -51,8 +51,6 @@ npm run check
|
|
|
51
51
|
`check` runs formatting, lint, full TypeScript validation, tests, build, package
|
|
52
52
|
consumer checks and dependency audit.
|
|
53
53
|
|
|
54
|
-
Project guides and release notes live in the [Wiki](https://github.com/nbtca/Prompt/wiki).
|
|
55
|
-
|
|
56
54
|
## License
|
|
57
55
|
|
|
58
56
|
MIT
|
package/dist/app/app.js
CHANGED
|
@@ -8,6 +8,8 @@ import { docsView } from './views/docs.js';
|
|
|
8
8
|
import { eventsView } from './views/events.js';
|
|
9
9
|
import { settingsView } from './views/settings.js';
|
|
10
10
|
import { getAppTabs } from './tabs.js';
|
|
11
|
+
import { renderHelp } from './help.js';
|
|
12
|
+
import { t } from '../i18n/index.js';
|
|
11
13
|
import { SPINNER_FRAME_MS } from '../core/components/spinner.js';
|
|
12
14
|
export async function runApp() {
|
|
13
15
|
if (!process.stdin.isTTY || !process.stdout.isTTY)
|
|
@@ -22,6 +24,7 @@ export async function runApp() {
|
|
|
22
24
|
let keyFlushTimer;
|
|
23
25
|
let clockTimer;
|
|
24
26
|
let busyTimer;
|
|
27
|
+
let helpOpen = false;
|
|
25
28
|
let painted;
|
|
26
29
|
let lastBody = { length: 0, height: 0 };
|
|
27
30
|
const viewIds = getAppTabs().map((tab) => tab.id);
|
|
@@ -77,11 +80,14 @@ export async function runApp() {
|
|
|
77
80
|
const tabs = getAppTabs();
|
|
78
81
|
const chrome = resolveChromeLayout(rows);
|
|
79
82
|
const header = renderHeader(tabs, view, cols, chrome.headerLines, active?.contextPath?.());
|
|
80
|
-
const
|
|
81
|
-
const
|
|
83
|
+
const activeTab = tabs.find((tab) => tab.id === view);
|
|
84
|
+
const body = helpOpen
|
|
85
|
+
? renderHelp(activeTab?.title ?? '', active?.shortcuts?.() ?? [], tabs.length, cols)
|
|
86
|
+
: (active?.render(ctx) ?? []);
|
|
87
|
+
const bodyScroll = !helpOpen && active?.capturesInput?.() ? Number.MAX_SAFE_INTEGER : scroll;
|
|
82
88
|
const height = computeBodyRows(rows, chrome.headerLines, chrome.footerLines);
|
|
83
89
|
lastBody = { length: body.length, height };
|
|
84
|
-
const footer = renderFooter(view, cols, tabs.length, active?.footerHint?.(tabs.length, cols), chrome.footerLines, active?.scrollsBody?.() === true ? scrollPercent() : undefined);
|
|
90
|
+
const footer = renderFooter(view, cols, tabs.length, helpOpen ? t().help.close : active?.footerHint?.(tabs.length, cols), chrome.footerLines, !helpOpen && active?.scrollsBody?.() === true ? scrollPercent() : undefined);
|
|
85
91
|
const lines = composeFrameLines(header, body, footer, rows, cols, bodyScroll);
|
|
86
92
|
const patch = diffFrame(painted?.cols === cols ? painted.lines : undefined, lines);
|
|
87
93
|
painted = { cols, lines };
|
|
@@ -127,6 +133,19 @@ export async function runApp() {
|
|
|
127
133
|
render();
|
|
128
134
|
return;
|
|
129
135
|
}
|
|
136
|
+
if (helpOpen) {
|
|
137
|
+
if (key === '?' || key === '\x1b') {
|
|
138
|
+
helpOpen = false;
|
|
139
|
+
render();
|
|
140
|
+
}
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
if (key === '?') {
|
|
144
|
+
helpOpen = true;
|
|
145
|
+
scroll = 0;
|
|
146
|
+
render();
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
130
149
|
const g = routeGlobalKey(key, viewIds, view);
|
|
131
150
|
if (g.quit) {
|
|
132
151
|
quit();
|
package/dist/app/chrome.js
CHANGED
|
@@ -112,7 +112,7 @@ export function passiveFooterHint(tabCount, cols = Number.POSITIVE_INFINITY) {
|
|
|
112
112
|
const trans = t();
|
|
113
113
|
const dot = pickIcon('·', '-');
|
|
114
114
|
const compactTabs = tabCount > 1 ? `1-${tabCount}/Tab ${dot} ` : '';
|
|
115
|
-
return fitFooterHint(cols, `${digitTabHint(tabCount)}Esc ${dot} q ${trans.menu.hintQuit}`, `${
|
|
115
|
+
return fitFooterHint(cols, `${digitTabHint(tabCount)}Esc ${dot} q ${trans.menu.hintQuit} ${dot} ${trans.help.hint}`, `${digitTabHint(tabCount)}Esc ${dot} q ${trans.menu.hintQuit}`, `${compactTabs}Esc ${dot} q ${dot} ?`, `Esc ${dot} q ${dot} ?`, `Esc ${dot} q`, 'q');
|
|
116
116
|
}
|
|
117
117
|
function interactiveFooterHint(tabCount, cols) {
|
|
118
118
|
const trans = t();
|
|
@@ -122,6 +122,7 @@ function interactiveFooterHint(tabCount, cols) {
|
|
|
122
122
|
const localFull = `${trans.menu.hintMove} ${dot} ${trans.menu.hintOpen} ${dot} Esc ${dot} q ${trans.menu.hintQuit}`;
|
|
123
123
|
const localCompact = `${trans.menu.hintMove} ${trans.menu.hintOpen} Esc q`;
|
|
124
124
|
const candidates = [
|
|
125
|
+
`${fullTabs}${localFull} ${dot} ${trans.help.hint}`,
|
|
125
126
|
`${fullTabs}${localFull}`,
|
|
126
127
|
`${compactTabs}${localFull}`,
|
|
127
128
|
localFull,
|
package/dist/app/help.js
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { type, space, glyph } from '../core/theme.js';
|
|
2
|
+
import { pickIcon } from '../core/icons.js';
|
|
3
|
+
import { t } from '../i18n/index.js';
|
|
4
|
+
import { padEndV, visualWidth, wrapAnsiWithIndent } from '../core/text.js';
|
|
5
|
+
function row(shortcut, keyWidth, cols) {
|
|
6
|
+
const key = padEndV(type.label(shortcut.key), keyWidth);
|
|
7
|
+
const line = `${space.indent}${space.indent}${key} ${type.hint(shortcut.label)}`;
|
|
8
|
+
if (visualWidth(line) <= cols)
|
|
9
|
+
return [line];
|
|
10
|
+
return wrapAnsiWithIndent(`${type.label(shortcut.key)} ${type.hint(shortcut.label)}`, cols, space.indent + space.indent);
|
|
11
|
+
}
|
|
12
|
+
function group(title, shortcuts, cols) {
|
|
13
|
+
if (shortcuts.length === 0)
|
|
14
|
+
return [];
|
|
15
|
+
const keyWidth = shortcuts.reduce((width, item) => Math.max(width, visualWidth(item.key)), 0);
|
|
16
|
+
return [
|
|
17
|
+
...wrapAnsiWithIndent(type.heading(title), cols, space.indent),
|
|
18
|
+
...shortcuts.flatMap((shortcut) => row(shortcut, keyWidth, cols)),
|
|
19
|
+
'',
|
|
20
|
+
];
|
|
21
|
+
}
|
|
22
|
+
export function globalShortcuts(tabCount) {
|
|
23
|
+
const trans = t();
|
|
24
|
+
const updown = glyph.updown();
|
|
25
|
+
return [
|
|
26
|
+
...(tabCount > 1 ? [{ key: `1-${String(tabCount)}`, label: trans.help.tabs }] : []),
|
|
27
|
+
{ key: 'Tab', label: trans.help.nextTab },
|
|
28
|
+
{ key: `${updown} / j k`, label: trans.help.scroll },
|
|
29
|
+
{ key: `PgUp/PgDn / ${pickIcon('␣', 'Space')}`, label: trans.help.page },
|
|
30
|
+
{ key: 'Home/End / g G', label: trans.help.ends },
|
|
31
|
+
{ key: pickIcon('⏎', 'Enter'), label: trans.help.open },
|
|
32
|
+
{ key: 'Esc', label: trans.help.back },
|
|
33
|
+
{ key: 'q', label: trans.help.quit },
|
|
34
|
+
];
|
|
35
|
+
}
|
|
36
|
+
export function renderHelp(viewTitle, viewShortcuts, tabCount, cols) {
|
|
37
|
+
const trans = t();
|
|
38
|
+
return [
|
|
39
|
+
...wrapAnsiWithIndent(type.heading(trans.help.title), cols, space.indent),
|
|
40
|
+
'',
|
|
41
|
+
...group(trans.help.sectionGlobal, globalShortcuts(tabCount), cols),
|
|
42
|
+
...group(viewTitle, viewShortcuts, cols),
|
|
43
|
+
];
|
|
44
|
+
}
|
package/dist/app/views/docs.js
CHANGED
|
@@ -465,6 +465,17 @@ export const docsView = {
|
|
|
465
465
|
return undefined;
|
|
466
466
|
}
|
|
467
467
|
},
|
|
468
|
+
shortcuts() {
|
|
469
|
+
const trans = t();
|
|
470
|
+
if (state.mode !== 'reader')
|
|
471
|
+
return [];
|
|
472
|
+
return [
|
|
473
|
+
...((state.readerLinks?.length ?? 0) > 0
|
|
474
|
+
? [{ key: 'f', label: trans.docs.readerLinksHint }]
|
|
475
|
+
: []),
|
|
476
|
+
{ key: 'b', label: trans.docs.openBrowser },
|
|
477
|
+
];
|
|
478
|
+
},
|
|
468
479
|
scrollsBody() {
|
|
469
480
|
return state.mode === 'reader' && state.readerLinksField === undefined;
|
|
470
481
|
},
|
package/dist/app/views/home.js
CHANGED
|
@@ -5,9 +5,9 @@ import { padEndV, visualWidth, wrapAnsiWithIndent } from '../../core/text.js';
|
|
|
5
5
|
import { peekNextClassLine, peekTodayLines, peekWeekAheadInfo, peekUnresolvedCount, } from '../../features/schedule-view.js';
|
|
6
6
|
import { loadCalendarOrThrow, peekCalendar, toDisplayEvent, renderEventBrief, } from '../../features/calendar.js';
|
|
7
7
|
import { weekdayShortLabel } from '../../features/schedule-render.js';
|
|
8
|
-
import { addLocalDays } from '../../core/calendar-day.js';
|
|
9
8
|
import { passiveFooterHint } from '../chrome.js';
|
|
10
|
-
import {
|
|
9
|
+
import { campusDateTime, campusIsoDate } from '@nbtca/nbtcal/timetable';
|
|
10
|
+
import { isoDayDifference, localDayDifference, parseLocalDate } from '../../core/calendar-day.js';
|
|
11
11
|
import { loadingLines } from '../../core/components/spinner.js';
|
|
12
12
|
const WEEKDAYS = [1, 2, 3, 4, 5, 6, 7];
|
|
13
13
|
function wrappedIndentedLines(label, cols, style) {
|
|
@@ -20,10 +20,11 @@ function wrappedRenderedLines(line, cols) {
|
|
|
20
20
|
const content = line.startsWith(space.indent) ? line.slice(space.indent.length) : line;
|
|
21
21
|
return wrappedIndentedLines(content, cols, (value) => value);
|
|
22
22
|
}
|
|
23
|
+
const DAY_MS = 86_400_000;
|
|
23
24
|
const DAY_PROGRESS_WIDTH = 20;
|
|
24
25
|
function renderDayProgress(now, cols) {
|
|
25
|
-
const
|
|
26
|
-
const fraction = Math.min(1, Math.max(0,
|
|
26
|
+
const midnight = campusDateTime(campusIsoDate(now), '00:00');
|
|
27
|
+
const fraction = Math.min(1, Math.max(0, (now.getTime() - midnight.getTime()) / DAY_MS));
|
|
27
28
|
const pct = Math.round(fraction * 100);
|
|
28
29
|
const percentage = `${pct}%`;
|
|
29
30
|
const width = Number.isFinite(cols) ? Math.max(1, Math.floor(cols)) : Number.POSITIVE_INFINITY;
|
|
@@ -149,8 +150,14 @@ function calendarSnapshot(cal, weekAheadInfo) {
|
|
|
149
150
|
.map((event) => renderEventBrief(toDisplayEvent(event), now));
|
|
150
151
|
if (!weekAheadInfo)
|
|
151
152
|
return { eventLines };
|
|
152
|
-
const
|
|
153
|
-
const
|
|
153
|
+
const { weekStart } = weekAheadInfo;
|
|
154
|
+
const monday = campusDateTime(weekStart, '00:00');
|
|
155
|
+
const daySet = new Set(cal
|
|
156
|
+
.inRange(new Date(monday.getTime() - DAY_MS), new Date(monday.getTime() + 8 * DAY_MS))
|
|
157
|
+
.map((event) => 1 +
|
|
158
|
+
(event.isAllDay
|
|
159
|
+
? localDayDifference(parseLocalDate(weekStart), event.start)
|
|
160
|
+
: isoDayDifference(weekStart, campusIsoDate(event.start)))));
|
|
154
161
|
return {
|
|
155
162
|
eventLines,
|
|
156
163
|
weekAhead: {
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import { createTimetableSchedule, } from '@nbtca/nbtcal/timetable';
|
|
1
|
+
import { campusIsoDate, createTimetableSchedule, } from '@nbtca/nbtcal/timetable';
|
|
2
2
|
import { c, type, space, glyph } from '../../core/theme.js';
|
|
3
3
|
import { pickIcon } from '../../core/icons.js';
|
|
4
4
|
import { t, fmt } from '../../i18n/index.js';
|
|
5
5
|
import { renderListFieldWithContext } from '../fields/list-field.js';
|
|
6
6
|
import { renderNextClassBanner, renderWeekGrid, renderUnresolvedItems, renderTodayTimeline, weekdayShortLabel, renderTermDensity, renderMeetingDetail, renderDayTimeline, renderDaySwitcher, } from '../../features/schedule-render.js';
|
|
7
7
|
import { sanitizeTerminalLine, visualWidth, wrapAnsiWithIndent } from '../../core/text.js';
|
|
8
|
-
import {
|
|
8
|
+
import { isoDayDifference } from '../../core/calendar-day.js';
|
|
9
9
|
import { loadingLines } from '../../core/components/spinner.js';
|
|
10
10
|
function heading(label) {
|
|
11
11
|
return `${space.indent}${type.heading(label)}`;
|
|
@@ -90,7 +90,7 @@ function hubPreGridLines(state, now, cols) {
|
|
|
90
90
|
lines.push(heading(trans.timetable.termNotStarted));
|
|
91
91
|
lines.push(hint(fmt(trans.timetable.termStartsIn, {
|
|
92
92
|
date: state.weekOne,
|
|
93
|
-
days: String(
|
|
93
|
+
days: String(daysUntil(now, state.weekOne)),
|
|
94
94
|
})));
|
|
95
95
|
lines.push('');
|
|
96
96
|
lines.push(heading(trans.timetable.termPreviewWeek));
|
|
@@ -160,7 +160,7 @@ const TERM_PROGRESS_WIDTH = 20;
|
|
|
160
160
|
function renderTermProgressBar(w, cols) {
|
|
161
161
|
if (!w.nextBreakStart)
|
|
162
162
|
return null;
|
|
163
|
-
const totalWeeks = Math.max(1, Math.round(
|
|
163
|
+
const totalWeeks = Math.max(1, Math.round(isoDayDifference(w.weekOneMonday, w.nextBreakStart) / 7));
|
|
164
164
|
const currentWeek = w.currentWeek;
|
|
165
165
|
const labelText = fmt(t().timetable.weekLabel2, { week: `${currentWeek}/${totalWeeks}` });
|
|
166
166
|
const label = type.hint(labelText);
|
|
@@ -177,8 +177,8 @@ function renderTermProgressBar(w, cols) {
|
|
|
177
177
|
? [`${indent}${bar} ${label}`]
|
|
178
178
|
: [`${indent}${bar}`, ...hintLines(labelText, cols)];
|
|
179
179
|
}
|
|
180
|
-
function
|
|
181
|
-
return Math.max(0,
|
|
180
|
+
function daysUntil(now, date) {
|
|
181
|
+
return Math.max(0, isoDayDifference(campusIsoDate(now), date));
|
|
182
182
|
}
|
|
183
183
|
function renderPublicBody(state, now, bodyRows, cols) {
|
|
184
184
|
const trans = t();
|
|
@@ -202,7 +202,7 @@ function renderPublicBody(state, now, bodyRows, cols) {
|
|
|
202
202
|
if (w.nextBreakStart && w.nextBreakTitle) {
|
|
203
203
|
lines.push(...hintLines(fmt(trans.timetable.daysUntilBreak, {
|
|
204
204
|
title: sanitizeTerminalLine(w.nextBreakTitle),
|
|
205
|
-
days: String(
|
|
205
|
+
days: String(daysUntil(now, w.nextBreakStart)),
|
|
206
206
|
}), cols));
|
|
207
207
|
}
|
|
208
208
|
}
|
|
@@ -298,6 +298,11 @@ export const scheduleView = {
|
|
|
298
298
|
isBusy() {
|
|
299
299
|
return state.mode === 'loading';
|
|
300
300
|
},
|
|
301
|
+
shortcuts() {
|
|
302
|
+
return state.mode === 'hub' && state.timetable
|
|
303
|
+
? hubShortcuts(state.timetable).map(({ key, label }) => ({ key, label }))
|
|
304
|
+
: [];
|
|
305
|
+
},
|
|
301
306
|
capturesInput() {
|
|
302
307
|
return (state.mode === 'needsLoginId' ||
|
|
303
308
|
state.mode === 'needsLoginPassword' ||
|
|
@@ -12,6 +12,8 @@ const WEBVPN_PATHS = new Set([
|
|
|
12
12
|
'/vpn_key/update',
|
|
13
13
|
]);
|
|
14
14
|
const AUTH_PATHS = new Set(['/authserver/login', '/authserver/checkNeedCaptcha.htl']);
|
|
15
|
+
// CAS parks accounts owing a profile here instead of issuing a ticket.
|
|
16
|
+
const PROFILE_GATE_PATH = '/authserver/improveInfo/improveUserInfo.do';
|
|
15
17
|
const JWXT_EXACT_PATHS = new Set([
|
|
16
18
|
'/sso/jziotlogin',
|
|
17
19
|
'/jwglxt/ticketlogin',
|
|
@@ -92,6 +94,9 @@ export function assertAllowedCampusUrl(url) {
|
|
|
92
94
|
}
|
|
93
95
|
if (hostname === JWXT_HOST && isAllowedJwxtPath(url.pathname))
|
|
94
96
|
return;
|
|
97
|
+
if (hostname === AUTH_HOST && url.pathname === PROFILE_GATE_PATH) {
|
|
98
|
+
throw new AuthError('PROFILE_INCOMPLETE', 'credentials', 'The campus requires this account to complete its profile.');
|
|
99
|
+
}
|
|
95
100
|
throw new AuthError('UNTRUSTED_URL', 'session', 'The campus service URL is not allowed.');
|
|
96
101
|
}
|
|
97
102
|
function safeHeaders(headers) {
|
package/dist/auth/nbt-auth.js
CHANGED
|
@@ -88,19 +88,21 @@ function classifyRejectedLogin(html) {
|
|
|
88
88
|
if (/锁定|冻结|次数过多|稍后再试/.test(visibleError)) {
|
|
89
89
|
return new AuthError('ACCOUNT_LOCKED', 'credentials', 'The campus account is temporarily locked.');
|
|
90
90
|
}
|
|
91
|
-
if (/激活|未启用/.test(visibleError)) {
|
|
92
|
-
return new AuthError('ACCOUNT_INACTIVE', 'credentials', 'The campus account must be activated first.');
|
|
93
|
-
}
|
|
94
91
|
if (/验证码|滑块|captcha/i.test(visibleError)) {
|
|
95
92
|
return new AuthError('INTERACTIVE_CHALLENGE', 'credentials', 'The campus login requires an interactive browser challenge.');
|
|
96
93
|
}
|
|
97
|
-
|
|
94
|
+
// Key `accountLogin_account_pwd_error`; localized it also tells first-time users
|
|
95
|
+
// to activate, so it must be classified before the activation branch below.
|
|
96
|
+
if (/用户名|密码|credential|password|pwd/i.test(visibleError)) {
|
|
98
97
|
return new AuthError('INVALID_CREDENTIALS', 'credentials', 'The student id or password was rejected.');
|
|
99
98
|
}
|
|
99
|
+
if (/激活|未启用/.test(visibleError)) {
|
|
100
|
+
return new AuthError('ACCOUNT_INACTIVE', 'credentials', 'The campus account must be activated first.');
|
|
101
|
+
}
|
|
100
102
|
return new AuthError('UNEXPECTED_RESPONSE', 'credentials', 'Campus login could not be confirmed.');
|
|
101
103
|
}
|
|
102
|
-
async function readText(response, stage) {
|
|
103
|
-
if (response.status < 200 || response.status >= 300) {
|
|
104
|
+
async function readText(response, stage, toleratedStatus) {
|
|
105
|
+
if ((response.status < 200 || response.status >= 300) && response.status !== toleratedStatus) {
|
|
104
106
|
throw new AuthError('HTTP_ERROR', stage, 'The campus service returned an error.', {
|
|
105
107
|
retryable: response.status >= 500,
|
|
106
108
|
});
|
|
@@ -220,15 +222,21 @@ export async function loginWithStudentPassword(username, password, options = {})
|
|
|
220
222
|
method: 'POST',
|
|
221
223
|
headers: {
|
|
222
224
|
Accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
|
|
225
|
+
// No Accept-Language on purpose: unresolved, the campus renders the bare
|
|
226
|
+
// message key, which is the only unambiguous error id this login offers.
|
|
223
227
|
'Content-Type': 'application/x-www-form-urlencoded',
|
|
224
228
|
Referer: loginResponse.url,
|
|
225
229
|
},
|
|
226
230
|
body: body.toString(),
|
|
227
231
|
...signalOption(options.signal),
|
|
228
232
|
}, 'credentials');
|
|
229
|
-
|
|
233
|
+
// A rejected login is a 401 carrying the login page; classify it, don't report HTTP_ERROR.
|
|
234
|
+
const credentialHtml = await readText(credentialResponse, 'credentials', 401);
|
|
230
235
|
if (hasLoginFingerprint(credentialHtml))
|
|
231
236
|
throw classifyRejectedLogin(credentialHtml);
|
|
237
|
+
if (credentialResponse.status === 401) {
|
|
238
|
+
throw new AuthError('HTTP_ERROR', 'credentials', 'The campus service returned an error.');
|
|
239
|
+
}
|
|
232
240
|
await verifyJwxtSession(cookies, options.signal);
|
|
233
241
|
const authenticatedAt = (options.now ?? (() => new Date()))().toISOString();
|
|
234
242
|
return createAuthenticatedSession(cookies, {
|
package/dist/core/logo.js
CHANGED
|
@@ -5,7 +5,7 @@ import chalk from 'chalk';
|
|
|
5
5
|
import { useUnicodeIcons } from './icons.js';
|
|
6
6
|
import { APP_INFO } from '../config/data.js';
|
|
7
7
|
import { typeReveal, materializeBraille } from './motion.js';
|
|
8
|
-
import { brandGradient as brand } from './theme.js';
|
|
8
|
+
import { brandGradient as brand, c } from './theme.js';
|
|
9
9
|
import { visualWidth } from './text.js';
|
|
10
10
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
11
11
|
const TAGLINE = 'To be at the intersection of technology and liberal arts.';
|
|
@@ -69,7 +69,9 @@ export async function runStartup() {
|
|
|
69
69
|
return;
|
|
70
70
|
const color = !process.env['NO_COLOR'];
|
|
71
71
|
process.stdout.write('\n');
|
|
72
|
-
await materializeBraille(art, (s) => paint(s, color)
|
|
72
|
+
await materializeBraille(art, (s) => paint(s, color), {
|
|
73
|
+
paintProgress: (s) => (color ? c.brand(s) : s),
|
|
74
|
+
});
|
|
73
75
|
await typeReveal([
|
|
74
76
|
'',
|
|
75
77
|
color ? brand(TAGLINE) : TAGLINE,
|
package/dist/core/motion.js
CHANGED
|
@@ -83,7 +83,8 @@ export async function materializeBraille(art, paint, opts = {}) {
|
|
|
83
83
|
}
|
|
84
84
|
shown++;
|
|
85
85
|
}
|
|
86
|
-
|
|
86
|
+
const painter = f === frameCount ? paint : (opts.paintProgress ?? paint);
|
|
87
|
+
write(painter(renderFrame()) + '\n');
|
|
87
88
|
if (f < frameCount) {
|
|
88
89
|
await sleep(frameMs);
|
|
89
90
|
write(ansi.cursorUp(lines.length) + ansi.cursorToCol0 + ansi.eraseDown);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createTimetableSchedule } from '@nbtca/nbtcal/timetable';
|
|
1
|
+
import { campusDateTime, campusIsoDate, createTimetableSchedule } from '@nbtca/nbtcal/timetable';
|
|
2
2
|
import { countdownParts, isCountdownUrgent } from './calendar-query.js';
|
|
3
3
|
import { c, type, space, glyph } from '../core/theme.js';
|
|
4
4
|
import { pickIcon } from '../core/icons.js';
|
|
@@ -64,25 +64,6 @@ export function renderNextClassBanner(next, now, cols = Number.POSITIVE_INFINITY
|
|
|
64
64
|
}
|
|
65
65
|
return styleWhen(compactWhen);
|
|
66
66
|
}
|
|
67
|
-
export function renderTodayClasses(meetings, periods, now) {
|
|
68
|
-
const trans = t();
|
|
69
|
-
const sorted = [...meetings].sort((a, b) => a.startPeriod - b.startPeriod);
|
|
70
|
-
if (sorted.length === 0)
|
|
71
|
-
return `${space.indent}${type.hint(trans.timetable.noClassToday)}`;
|
|
72
|
-
const dot = pickIcon('·', '-');
|
|
73
|
-
const marker = pickIcon('▸', '>');
|
|
74
|
-
const lines = sorted.map((m) => {
|
|
75
|
-
const time = span(m, periods);
|
|
76
|
-
const startStr = periods.find((p) => p.period === m.startPeriod)?.start ?? '00:00';
|
|
77
|
-
const endStr = periods.find((p) => p.period === m.endPeriod)?.end ?? '23:59';
|
|
78
|
-
const nowStr = `${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}`;
|
|
79
|
-
const live = nowStr >= startStr && nowStr <= endStr;
|
|
80
|
-
const head = live ? `${type.active(marker)} ` : ' ';
|
|
81
|
-
const loc = m.location ? ` ${dot} ${type.hint(m.location)}` : '';
|
|
82
|
-
return `${space.indent}${head}${type.hint(time)} ${live ? type.active(m.courseName) : type.body(m.courseName)}${loc}`;
|
|
83
|
-
});
|
|
84
|
-
return lines.join('\n');
|
|
85
|
-
}
|
|
86
67
|
export function weekdayShortLabel(wd) {
|
|
87
68
|
const trans = t();
|
|
88
69
|
const labels = [
|
|
@@ -101,7 +82,8 @@ function renderTimeline(meetings, periods, now, isToday, alwaysShowLocation, cur
|
|
|
101
82
|
const sorted = [...meetings].sort((a, b) => a.startPeriod - b.startPeriod);
|
|
102
83
|
if (sorted.length === 0)
|
|
103
84
|
return `${space.indent}${type.hint(trans.timetable.noClassToday)}`;
|
|
104
|
-
const
|
|
85
|
+
const today = campusIsoDate(now);
|
|
86
|
+
const minute = Math.floor(now.getTime() / 60_000) * 60_000;
|
|
105
87
|
const dot = pickIcon('·', '-');
|
|
106
88
|
const rule = pickIcon('─', '-');
|
|
107
89
|
const midConnector = pickIcon('┼', '+');
|
|
@@ -110,8 +92,9 @@ function renderTimeline(meetings, periods, now, isToday, alwaysShowLocation, cur
|
|
|
110
92
|
const lines = sorted.map((m, i) => {
|
|
111
93
|
const startStr = periods.find((p) => p.period === m.startPeriod)?.start ?? '00:00';
|
|
112
94
|
const endStr = periods.find((p) => p.period === m.endPeriod)?.end ?? '23:59';
|
|
113
|
-
const
|
|
114
|
-
const
|
|
95
|
+
const end = campusDateTime(today, endStr);
|
|
96
|
+
const isLive = isToday && campusDateTime(today, startStr).getTime() <= minute && minute <= end.getTime();
|
|
97
|
+
const isDone = isToday && minute > end.getTime();
|
|
115
98
|
const isCursor = cursorPeriod !== undefined && m.startPeriod <= cursorPeriod && cursorPeriod <= m.endPeriod;
|
|
116
99
|
const connector = i === 0 ? topConnector : midConnector;
|
|
117
100
|
const marker = isLive ? type.active(pickIcon('▶', '>')) : ' ';
|
|
@@ -124,9 +107,6 @@ function renderTimeline(meetings, periods, now, isToday, alwaysShowLocation, cur
|
|
|
124
107
|
compactStatusText = statusText;
|
|
125
108
|
}
|
|
126
109
|
else if (isLive) {
|
|
127
|
-
const end = new Date(now);
|
|
128
|
-
const [eh, em] = endStr.split(':').map((x) => Number.parseInt(x, 10));
|
|
129
|
-
end.setHours(eh !== undefined && Number.isFinite(eh) ? eh : 0, em !== undefined && Number.isFinite(em) ? em : 0, 0, 0);
|
|
130
110
|
const remaining = countdownParts(end, now);
|
|
131
111
|
const mins = remaining.days * 1440 + remaining.hours * 60 + remaining.minutes;
|
|
132
112
|
statusText = `${trans.timetable.classLive} ${dot} ${fmt(trans.timetable.minutesRemaining, { minutes: String(mins) })}`;
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { createTimetableSchedule } from '@nbtca/nbtcal/timetable';
|
|
2
|
-
import { addLocalDays, parseLocalMonday } from '../core/calendar-day.js';
|
|
1
|
+
import { campusDateTime, campusIsoDate, createTimetableSchedule, } from '@nbtca/nbtcal/timetable';
|
|
3
2
|
import { renderNextClassBanner, renderTodayTimeline } from './schedule-render.js';
|
|
4
3
|
import { loadCurrentPointer, loadTimetableCache } from './schedule-store.js';
|
|
5
4
|
import { sanitizeTimetable } from './timetable-sanitize.js';
|
|
6
5
|
const WEEKDAYS = [1, 2, 3, 4, 5, 6, 7];
|
|
6
|
+
const WEEK_MS = 7 * 86_400_000;
|
|
7
7
|
function loadCachedTimetable() {
|
|
8
8
|
const pointer = loadCurrentPointer();
|
|
9
9
|
if (!pointer)
|
|
@@ -69,8 +69,8 @@ export function peekWeekAheadInfo(now = new Date()) {
|
|
|
69
69
|
if (week < 1)
|
|
70
70
|
return null;
|
|
71
71
|
const classDays = WEEKDAYS.map((weekday) => schedule.meetingsOnDay(week, weekday).length > 0);
|
|
72
|
-
const
|
|
73
|
-
return {
|
|
72
|
+
const weekStart = campusDateTime(cached.weekOneMonday, '00:00').getTime() + (week - 1) * WEEK_MS;
|
|
73
|
+
return { weekStart: campusIsoDate(new Date(weekStart)), classDays };
|
|
74
74
|
}
|
|
75
75
|
catch {
|
|
76
76
|
return null;
|
|
@@ -59,6 +59,8 @@ export function safeMessage(error) {
|
|
|
59
59
|
return trans.accountInactive;
|
|
60
60
|
case 'INTERACTIVE_CHALLENGE':
|
|
61
61
|
return trans.challenge;
|
|
62
|
+
case 'PROFILE_INCOMPLETE':
|
|
63
|
+
return trans.profileIncomplete;
|
|
62
64
|
case 'SESSION_EXPIRED':
|
|
63
65
|
return trans.sessionExpired;
|
|
64
66
|
case 'TIMEOUT':
|
|
@@ -12,6 +12,21 @@
|
|
|
12
12
|
"moreAbove": "{count} more above",
|
|
13
13
|
"moreBelow": "{count} more below"
|
|
14
14
|
},
|
|
15
|
+
"help": {
|
|
16
|
+
"title": "Keys",
|
|
17
|
+
"sectionGlobal": "Anywhere",
|
|
18
|
+
"sectionView": "Here",
|
|
19
|
+
"tabs": "Switch tab",
|
|
20
|
+
"nextTab": "Next tab",
|
|
21
|
+
"scroll": "Scroll a line",
|
|
22
|
+
"page": "Scroll a page",
|
|
23
|
+
"ends": "Jump to top or bottom",
|
|
24
|
+
"open": "Open",
|
|
25
|
+
"back": "Back",
|
|
26
|
+
"quit": "Quit",
|
|
27
|
+
"close": "Esc or ? to close",
|
|
28
|
+
"hint": "? keys"
|
|
29
|
+
},
|
|
15
30
|
"menu": {
|
|
16
31
|
"events": "Events",
|
|
17
32
|
"eventsDesc": "",
|
|
@@ -218,6 +233,7 @@
|
|
|
218
233
|
"sessionExpired": "The login session expired. Please sign in again.",
|
|
219
234
|
"timeout": "The school service timed out.",
|
|
220
235
|
"network": "Could not connect to the school service.",
|
|
236
|
+
"profileIncomplete": "The school wants this account's profile completed first; finish it in a browser, then try again.",
|
|
221
237
|
"untrustedUrl": "The school returned an unapproved redirect; the request stopped safely.",
|
|
222
238
|
"httpError": "The school login service returned an error status.",
|
|
223
239
|
"loginChanged": "The school login page changed; credentials were not submitted.",
|
|
@@ -12,6 +12,21 @@
|
|
|
12
12
|
"moreAbove": "上方还有 {count} 项",
|
|
13
13
|
"moreBelow": "下方还有 {count} 项"
|
|
14
14
|
},
|
|
15
|
+
"help": {
|
|
16
|
+
"title": "快捷键",
|
|
17
|
+
"sectionGlobal": "全局",
|
|
18
|
+
"sectionView": "当前页",
|
|
19
|
+
"tabs": "切换标签页",
|
|
20
|
+
"nextTab": "下一个标签页",
|
|
21
|
+
"scroll": "滚动一行",
|
|
22
|
+
"page": "翻页",
|
|
23
|
+
"ends": "跳到开头或结尾",
|
|
24
|
+
"open": "打开",
|
|
25
|
+
"back": "返回",
|
|
26
|
+
"quit": "退出",
|
|
27
|
+
"close": "Esc 或 ? 关闭",
|
|
28
|
+
"hint": "? 快捷键"
|
|
29
|
+
},
|
|
15
30
|
"menu": {
|
|
16
31
|
"events": "活动",
|
|
17
32
|
"eventsDesc": "",
|
|
@@ -218,6 +233,7 @@
|
|
|
218
233
|
"sessionExpired": "登录状态已过期,请重新登录。",
|
|
219
234
|
"timeout": "学校服务响应超时。",
|
|
220
235
|
"network": "无法连接学校服务。",
|
|
236
|
+
"profileIncomplete": "学校要求先补全个人资料;请在浏览器登录统一身份认证完成后再试。",
|
|
221
237
|
"untrustedUrl": "学校返回了未授权的登录跳转,已安全中止。",
|
|
222
238
|
"httpError": "学校登录服务返回了错误状态。",
|
|
223
239
|
"loginChanged": "学校登录页结构已经变化,当前版本未提交凭据。",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nbtca/prompt",
|
|
3
|
-
"version": "1.5.
|
|
3
|
+
"version": "1.5.11",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"exports": {
|
|
@@ -46,8 +46,8 @@
|
|
|
46
46
|
"interactive"
|
|
47
47
|
],
|
|
48
48
|
"dependencies": {
|
|
49
|
-
"@nbtca/docs": "^0.3.
|
|
50
|
-
"@nbtca/nbtcal": "^0.4.
|
|
49
|
+
"@nbtca/docs": "^0.3.1",
|
|
50
|
+
"@nbtca/nbtcal": "^0.4.1",
|
|
51
51
|
"chalk": "^5.6.2",
|
|
52
52
|
"cheerio": "1.0.0",
|
|
53
53
|
"fetch-cookie": "^3.2.0",
|
|
@@ -68,7 +68,7 @@
|
|
|
68
68
|
"typescript": "^5.9.3",
|
|
69
69
|
"typescript-eslint": "^8.67.0",
|
|
70
70
|
"vite": "^6.4.3",
|
|
71
|
-
"vitest": "^
|
|
71
|
+
"vitest": "^4.1.11"
|
|
72
72
|
},
|
|
73
73
|
"engines": {
|
|
74
74
|
"node": ">=20.12.0"
|