@nbtca/prompt 1.3.2 → 1.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +44 -0
- package/SECURITY.md +47 -0
- package/dist/app/app.js +202 -0
- package/dist/app/chrome.js +104 -0
- package/dist/app/fields/list-field.js +174 -0
- package/dist/app/fields/text-field.js +38 -0
- package/dist/app/frame.js +48 -0
- package/dist/app/keys.js +20 -0
- package/dist/app/tabs.js +11 -0
- package/dist/app/view.js +1 -0
- package/dist/app/views/docs-render.js +82 -0
- package/dist/app/views/docs.js +457 -0
- package/dist/app/views/events-render.js +111 -0
- package/dist/app/views/events.js +228 -0
- package/dist/app/views/home.js +236 -0
- package/dist/app/views/schedule-grid-cursor.js +52 -0
- package/dist/app/views/schedule-render.js +317 -0
- package/dist/app/views/schedule.js +472 -0
- package/dist/app/views/settings-render.js +53 -0
- package/dist/app/views/settings.js +153 -0
- package/dist/auth/cookie-transport.js +222 -0
- package/dist/auth/errors.js +18 -0
- package/dist/auth/nbt-auth.js +239 -0
- package/dist/auth/session-store.js +118 -0
- package/dist/config/paths.js +22 -2
- package/dist/core/canvas.js +23 -0
- package/dist/core/capabilities.js +42 -0
- package/dist/core/components/confirm.js +75 -0
- package/dist/core/components/input-session.js +24 -0
- package/dist/core/components/menu.js +122 -0
- package/dist/core/components/messages.js +16 -0
- package/dist/core/components/note.js +18 -0
- package/dist/core/components/painter.js +26 -0
- package/dist/core/components/screen.js +18 -0
- package/dist/core/components/spinner.js +47 -0
- package/dist/core/components/text-input.js +98 -0
- package/dist/core/logo.js +40 -15
- package/dist/core/menu.js +24 -6
- package/dist/core/motion.js +86 -0
- package/dist/core/text.js +127 -5
- package/dist/core/theme.js +61 -0
- package/dist/core/transitions.js +19 -0
- package/dist/core/ui.js +5 -29
- package/dist/features/calendar-heatmap.js +29 -27
- package/dist/features/calendar-query.js +50 -0
- package/dist/features/calendar.js +192 -98
- package/dist/features/docs.js +258 -55
- package/dist/features/links.js +7 -4
- package/dist/features/schedule-query.js +47 -0
- package/dist/features/schedule-render.js +574 -0
- package/dist/features/schedule-store.js +73 -0
- package/dist/features/schedule-view.js +260 -0
- package/dist/features/settings.js +41 -30
- package/dist/features/status.js +37 -13
- package/dist/features/student-timetable.js +346 -0
- package/dist/features/update.js +16 -8
- package/dist/i18n/locales/en.json +149 -6
- package/dist/i18n/locales/zh.json +149 -6
- package/dist/index.js +59 -5
- package/dist/logo/ca-dotmatrix-large.txt +26 -0
- package/dist/logo/ca-dotmatrix-small.txt +12 -0
- package/dist/logo/ca-dotmatrix.txt +18 -16
- package/dist/logo/ca-logo.png +0 -0
- package/dist/main.js +33 -13
- package/package.json +10 -7
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { createNbtTimetableClient, timetableToIcs, } from '@nbtca/nbtcal/timetable';
|
|
3
|
+
import { runMenu, menuFooter } from '../core/components/menu.js';
|
|
4
|
+
import { runTextInput } from '../core/components/text-input.js';
|
|
5
|
+
import { enterScreen, breadcrumb } from '../core/transitions.js';
|
|
6
|
+
import { createSpinner, success, error } from '../core/ui.js';
|
|
7
|
+
import { c, type, space } from '../core/theme.js';
|
|
8
|
+
import { t, fmt } from '../i18n/index.js';
|
|
9
|
+
import { createSessionStore } from '../auth/session-store.js';
|
|
10
|
+
import { withAuthenticatedSession, resolveTerm, relevantTerms, writePrivateIcs, isSessionExpired, safeMessage, JWXT_ORIGIN, } from './student-timetable.js';
|
|
11
|
+
import { currentWeekNumber, campusWeekday, meetingsOnDay, nextMeeting } from './schedule-query.js';
|
|
12
|
+
import { renderNextClassBanner, renderTodayClasses, renderTodayTimeline, renderWeekGrid } from './schedule-render.js';
|
|
13
|
+
import { termKey, loadWeekOne, saveWeekOne, saveTimetableCache, saveCurrentPointer, loadCurrentPointer, loadTimetableCache, clearScheduleCache, } from './schedule-store.js';
|
|
14
|
+
/** Loads a saved week-one Monday for `key`, or prompts for and persists a new one.
|
|
15
|
+
* Returns null when the user cancels or enters an unparsable date (caller aborts). */
|
|
16
|
+
async function ensureWeekOne(key) {
|
|
17
|
+
const saved = loadWeekOne(key);
|
|
18
|
+
if (saved)
|
|
19
|
+
return saved;
|
|
20
|
+
const value = await runTextInput({
|
|
21
|
+
message: t().timetable.promptWeekOne,
|
|
22
|
+
placeholder: 'YYYY-MM-DD',
|
|
23
|
+
});
|
|
24
|
+
if (value === null)
|
|
25
|
+
return null;
|
|
26
|
+
const trimmed = value.trim();
|
|
27
|
+
if (!/^\d{4}-\d{2}-\d{2}$/.test(trimmed))
|
|
28
|
+
return null;
|
|
29
|
+
if (Number.isNaN(new Date(`${trimmed}T00:00:00`).getTime()))
|
|
30
|
+
return null;
|
|
31
|
+
saveWeekOne(key, trimmed);
|
|
32
|
+
return trimmed;
|
|
33
|
+
}
|
|
34
|
+
/** Fetches a term's timetable behind a spinner, caching it on success. Returns null
|
|
35
|
+
* (after reporting the error) on failure so callers can keep the previous state. */
|
|
36
|
+
async function fetchTimetableWithSpinner(client, term, key, weekOne) {
|
|
37
|
+
const trans = t();
|
|
38
|
+
const spinner = createSpinner(trans.calendar.loading);
|
|
39
|
+
try {
|
|
40
|
+
const tt = await client.fetchTerm(term);
|
|
41
|
+
spinner.stop();
|
|
42
|
+
saveTimetableCache(key, tt);
|
|
43
|
+
saveCurrentPointer(key, weekOne);
|
|
44
|
+
return tt;
|
|
45
|
+
}
|
|
46
|
+
catch (err) {
|
|
47
|
+
if (isSessionExpired(err)) {
|
|
48
|
+
spinner.stop();
|
|
49
|
+
throw err;
|
|
50
|
+
}
|
|
51
|
+
spinner.error(trans.timetable.genericError);
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
function renderHub(tt, weekOne, now) {
|
|
56
|
+
const week = currentWeekNumber(weekOne, now);
|
|
57
|
+
const today = meetingsOnDay(tt.meetings, campusWeekday(now), week);
|
|
58
|
+
const trans = t();
|
|
59
|
+
console.log();
|
|
60
|
+
const banner = renderNextClassBanner(nextMeeting(tt.meetings, tt.periods, weekOne, now), now);
|
|
61
|
+
console.log(banner || `${space.indent}${type.hint(trans.timetable.noNextClass)}`);
|
|
62
|
+
console.log();
|
|
63
|
+
console.log(renderTodayClasses(today, tt.periods, now));
|
|
64
|
+
console.log();
|
|
65
|
+
return { week, today };
|
|
66
|
+
}
|
|
67
|
+
async function switchTerm(client, catalog) {
|
|
68
|
+
const trans = t();
|
|
69
|
+
const picked = await runMenu({
|
|
70
|
+
title: trans.timetable.hubSwitchTerm,
|
|
71
|
+
options: relevantTerms(catalog).map((tm) => ({
|
|
72
|
+
value: `${tm.academicYear}:${tm.semester}`,
|
|
73
|
+
label: tm.academicYearLabel,
|
|
74
|
+
hint: tm.current ? trans.common.current : undefined,
|
|
75
|
+
})),
|
|
76
|
+
footer: menuFooter(),
|
|
77
|
+
});
|
|
78
|
+
if (picked === null)
|
|
79
|
+
return null;
|
|
80
|
+
const term = resolveTerm(catalog, picked);
|
|
81
|
+
const key = termKey(term);
|
|
82
|
+
const weekOne = await ensureWeekOne(key);
|
|
83
|
+
if (!weekOne)
|
|
84
|
+
return null;
|
|
85
|
+
const tt = await fetchTimetableWithSpinner(client, term, key, weekOne);
|
|
86
|
+
if (!tt)
|
|
87
|
+
return null;
|
|
88
|
+
return { term, key, weekOne, tt };
|
|
89
|
+
}
|
|
90
|
+
function exportTimetable(tt, term, key, weekOne) {
|
|
91
|
+
const trans = t();
|
|
92
|
+
const ics = timetableToIcs(tt, { weekOneMonday: weekOne, calendarName: `NBT ${term.academicYearLabel}` });
|
|
93
|
+
const out = `timetable-${key}.ics`;
|
|
94
|
+
try {
|
|
95
|
+
writePrivateIcs(out, ics);
|
|
96
|
+
success(fmt(trans.timetable.exported, { count: tt.meetings.length, file: path.resolve(out) }));
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
error(trans.timetable.genericError);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
/** Interactive schedule hub: login/restore, resolve the current term, load or prompt
|
|
103
|
+
* for the week-one Monday, fetch the timetable, then loop a menu of today / week-grid /
|
|
104
|
+
* switch-term / export actions until the user cancels. */
|
|
105
|
+
export async function showSchedule() {
|
|
106
|
+
const trans = t();
|
|
107
|
+
await enterScreen(breadcrumb(trans.timetable.menuEntry));
|
|
108
|
+
try {
|
|
109
|
+
await withAuthenticatedSession(async (session) => {
|
|
110
|
+
const client = createNbtTimetableClient(session.timetableTransport, { baseUrl: JWXT_ORIGIN });
|
|
111
|
+
const catalog = await client.listTerms();
|
|
112
|
+
let term = resolveTerm(catalog);
|
|
113
|
+
let key = termKey(term);
|
|
114
|
+
let weekOne = await ensureWeekOne(key);
|
|
115
|
+
if (!weekOne)
|
|
116
|
+
return 0;
|
|
117
|
+
const initial = await fetchTimetableWithSpinner(client, term, key, weekOne);
|
|
118
|
+
if (!initial)
|
|
119
|
+
return 1;
|
|
120
|
+
let tt = initial;
|
|
121
|
+
while (true) {
|
|
122
|
+
const now = new Date();
|
|
123
|
+
const { week, today } = renderHub(tt, weekOne, now);
|
|
124
|
+
const action = await runMenu({
|
|
125
|
+
title: `${trans.timetable.menuEntry} ${c.muted(term.academicYearLabel)} ${c.muted(trans.timetable.weekLabel + String(week))}`,
|
|
126
|
+
options: [
|
|
127
|
+
{ value: 'today', label: trans.timetable.hubToday, hint: String(today.length) },
|
|
128
|
+
{ value: 'week', label: trans.timetable.hubWeek },
|
|
129
|
+
{ value: 'term', label: trans.timetable.hubSwitchTerm, hint: term.academicYearLabel },
|
|
130
|
+
{ value: 'export', label: trans.timetable.hubExport },
|
|
131
|
+
{ value: 'logout', label: trans.timetable.hubLogout },
|
|
132
|
+
],
|
|
133
|
+
footer: menuFooter(),
|
|
134
|
+
});
|
|
135
|
+
if (action === null)
|
|
136
|
+
return 0;
|
|
137
|
+
if (action === 'today') {
|
|
138
|
+
continue; // The next loop iteration repaints today's classes.
|
|
139
|
+
}
|
|
140
|
+
if (action === 'week') {
|
|
141
|
+
console.log();
|
|
142
|
+
console.log(renderWeekGrid(tt.meetings, tt.periods, week, now));
|
|
143
|
+
console.log();
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
if (action === 'term') {
|
|
147
|
+
const switched = await switchTerm(client, catalog);
|
|
148
|
+
if (!switched)
|
|
149
|
+
continue;
|
|
150
|
+
term = switched.term;
|
|
151
|
+
key = switched.key;
|
|
152
|
+
weekOne = switched.weekOne;
|
|
153
|
+
tt = switched.tt;
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
if (action === 'export') {
|
|
157
|
+
exportTimetable(tt, term, key, weekOne);
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
if (action === 'logout') {
|
|
161
|
+
createSessionStore().clear();
|
|
162
|
+
clearScheduleCache();
|
|
163
|
+
return 0;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}, {
|
|
167
|
+
oneShot: false,
|
|
168
|
+
isInteractive: true,
|
|
169
|
+
store: createSessionStore(),
|
|
170
|
+
stderr: process.stderr,
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
catch (err) {
|
|
174
|
+
error(safeMessage(err));
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
/** Best-effort, cache-only startup line: reads the last-used term pointer and its
|
|
178
|
+
* cached timetable (no network) and renders the next-class banner, or '' if the
|
|
179
|
+
* student isn't "set up" yet or anything about the cache is missing/corrupt. */
|
|
180
|
+
export function peekNextClassLine(now = new Date()) {
|
|
181
|
+
try {
|
|
182
|
+
const ptr = loadCurrentPointer();
|
|
183
|
+
if (!ptr)
|
|
184
|
+
return '';
|
|
185
|
+
const cached = loadTimetableCache(ptr.termKey);
|
|
186
|
+
if (!cached || !Array.isArray(cached.meetings) || !Array.isArray(cached.periods))
|
|
187
|
+
return '';
|
|
188
|
+
const next = nextMeeting(cached.meetings, cached.periods, ptr.weekOneMonday, now);
|
|
189
|
+
return renderNextClassBanner(next, now);
|
|
190
|
+
}
|
|
191
|
+
catch {
|
|
192
|
+
return '';
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
/** Cache-only (no network) render of today's classes for the current term, or
|
|
196
|
+
* [] if not set up. Uses the same renderTodayTimeline Schedule's own hub
|
|
197
|
+
* renders, so Home's preview and Schedule's detail view show the exact same
|
|
198
|
+
* visual language for the exact same data instead of drifting apart. */
|
|
199
|
+
export function peekTodayLines(now = new Date()) {
|
|
200
|
+
try {
|
|
201
|
+
const ptr = loadCurrentPointer();
|
|
202
|
+
if (!ptr)
|
|
203
|
+
return [];
|
|
204
|
+
const cached = loadTimetableCache(ptr.termKey);
|
|
205
|
+
if (!cached || !Array.isArray(cached.meetings) || !Array.isArray(cached.periods))
|
|
206
|
+
return [];
|
|
207
|
+
const week = currentWeekNumber(ptr.weekOneMonday, now);
|
|
208
|
+
const today = meetingsOnDay(cached.meetings, campusWeekday(now), week);
|
|
209
|
+
return renderTodayTimeline(today, cached.periods, now).split('\n');
|
|
210
|
+
}
|
|
211
|
+
catch {
|
|
212
|
+
return [];
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
/** Best-effort, cache-only (no network) computation of this week's per-day
|
|
216
|
+
* class signal, for Home's combined week-overview grid. Returns null when
|
|
217
|
+
* there's no set-up personal timetable, the cache is unusable, or the term
|
|
218
|
+
* hasn't started yet (current week < 1) — the same "before term start"
|
|
219
|
+
* guard already fixed once for Schedule's own hub (a future-dated weekOne,
|
|
220
|
+
* auto-inferred while on break, must not render a nonsensical negative
|
|
221
|
+
* week's worth of content). */
|
|
222
|
+
export function peekWeekAheadInfo(now = new Date()) {
|
|
223
|
+
try {
|
|
224
|
+
const ptr = loadCurrentPointer();
|
|
225
|
+
if (!ptr)
|
|
226
|
+
return null;
|
|
227
|
+
const cached = loadTimetableCache(ptr.termKey);
|
|
228
|
+
if (!cached || !Array.isArray(cached.meetings))
|
|
229
|
+
return null;
|
|
230
|
+
const week = currentWeekNumber(ptr.weekOneMonday, now);
|
|
231
|
+
if (week < 1)
|
|
232
|
+
return null;
|
|
233
|
+
const meetings = cached.meetings;
|
|
234
|
+
const classDays = [1, 2, 3, 4, 5, 6, 7].map((wd) => meetingsOnDay(meetings, wd, week).length > 0);
|
|
235
|
+
const base = new Date(`${ptr.weekOneMonday}T00:00:00`);
|
|
236
|
+
const weekStartDate = new Date(base.getTime() + (week - 1) * 7 * 86400000);
|
|
237
|
+
return { weekStartDate, classDays };
|
|
238
|
+
}
|
|
239
|
+
catch {
|
|
240
|
+
return null;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
/** Best-effort, cache-only (no network) read of how many timetable items
|
|
244
|
+
* still need the student's attention — the same data buildHubField()
|
|
245
|
+
* (schedule.ts) already surfaces inside Schedule's own hub menu, exposed
|
|
246
|
+
* here so Home can show it too without a second source of truth. */
|
|
247
|
+
export function peekUnresolvedCount() {
|
|
248
|
+
try {
|
|
249
|
+
const ptr = loadCurrentPointer();
|
|
250
|
+
if (!ptr)
|
|
251
|
+
return 0;
|
|
252
|
+
const cached = loadTimetableCache(ptr.termKey);
|
|
253
|
+
if (!cached || !Array.isArray(cached.unresolvedItems))
|
|
254
|
+
return 0;
|
|
255
|
+
return cached.unresolvedItems.length;
|
|
256
|
+
}
|
|
257
|
+
catch {
|
|
258
|
+
return 0;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Unified settings — language, theme, about
|
|
3
3
|
*/
|
|
4
|
-
import { select, isCancel, note } from '@clack/prompts';
|
|
5
4
|
import chalk from 'chalk';
|
|
6
5
|
import { applyColorModePreference, loadPreferences, resetPreferences, setColorMode, setIconMode, } from '../config/preferences.js';
|
|
7
6
|
import { pickIcon } from '../core/icons.js';
|
|
@@ -10,6 +9,9 @@ import { padEndV } from '../core/text.js';
|
|
|
10
9
|
import { success, warning } from '../core/ui.js';
|
|
11
10
|
import { APP_INFO, URLS } from '../config/data.js';
|
|
12
11
|
import { t, getCurrentLanguage, setLanguage, clearTranslationCache } from '../i18n/index.js';
|
|
12
|
+
import { runMenu, menuFooter } from '../core/components/menu.js';
|
|
13
|
+
import { note } from '../core/components/note.js';
|
|
14
|
+
import { enterScreen, breadcrumb } from '../core/transitions.js';
|
|
13
15
|
function notifyResult(saved, successMsg, warningMsg) {
|
|
14
16
|
if (saved) {
|
|
15
17
|
success(successMsg);
|
|
@@ -37,12 +39,14 @@ function showAbout() {
|
|
|
37
39
|
note(content, trans.about.title);
|
|
38
40
|
}
|
|
39
41
|
export async function showSettingsMenu() {
|
|
42
|
+
await enterScreen(breadcrumb(t().menu.settings));
|
|
40
43
|
while (true) {
|
|
41
44
|
const trans = t();
|
|
42
45
|
const prefs = loadPreferences();
|
|
43
46
|
const currentLang = getCurrentLanguage();
|
|
44
|
-
const
|
|
45
|
-
|
|
47
|
+
const footer = menuFooter();
|
|
48
|
+
const action = await runMenu({
|
|
49
|
+
title: trans.theme.chooseAction,
|
|
46
50
|
options: [
|
|
47
51
|
{ value: 'language', label: trans.language.selectLanguage, hint: currentLang === 'zh' ? trans.language.zh : trans.language.en },
|
|
48
52
|
{ value: 'icon', label: trans.theme.iconMode, hint: prefs.iconMode },
|
|
@@ -50,23 +54,26 @@ export async function showSettingsMenu() {
|
|
|
50
54
|
{ value: 'reset', label: trans.theme.resetLabel },
|
|
51
55
|
{ value: 'about', label: trans.about.title },
|
|
52
56
|
],
|
|
57
|
+
footer,
|
|
53
58
|
});
|
|
54
|
-
if (
|
|
59
|
+
if (action === null)
|
|
55
60
|
return;
|
|
56
61
|
if (action === 'about') {
|
|
57
62
|
showAbout();
|
|
58
63
|
continue;
|
|
59
64
|
}
|
|
60
65
|
if (action === 'language') {
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
66
|
+
const langOptions = [
|
|
67
|
+
{ value: 'zh', label: trans.language.zh, hint: currentLang === 'zh' ? trans.common.current : undefined },
|
|
68
|
+
{ value: 'en', label: trans.language.en, hint: currentLang === 'en' ? trans.common.current : undefined },
|
|
69
|
+
];
|
|
70
|
+
const language = await runMenu({
|
|
71
|
+
title: trans.language.selectLanguage,
|
|
72
|
+
options: langOptions,
|
|
73
|
+
footer,
|
|
74
|
+
initialIndex: Math.max(0, langOptions.findIndex(o => o.value === currentLang)),
|
|
68
75
|
});
|
|
69
|
-
if (
|
|
76
|
+
if (language === null)
|
|
70
77
|
continue;
|
|
71
78
|
if (language !== currentLang) {
|
|
72
79
|
const saved = setLanguage(language);
|
|
@@ -76,16 +83,18 @@ export async function showSettingsMenu() {
|
|
|
76
83
|
continue;
|
|
77
84
|
}
|
|
78
85
|
if (action === 'icon') {
|
|
79
|
-
const
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
86
|
+
const iconOptions = [
|
|
87
|
+
{ value: 'auto', label: trans.theme.modeAuto, hint: prefs.iconMode === 'auto' ? trans.common.current : undefined },
|
|
88
|
+
{ value: 'ascii', label: trans.theme.modeAscii, hint: prefs.iconMode === 'ascii' ? trans.common.current : undefined },
|
|
89
|
+
{ value: 'unicode', label: trans.theme.modeUnicode, hint: prefs.iconMode === 'unicode' ? trans.common.current : undefined },
|
|
90
|
+
];
|
|
91
|
+
const mode = await runMenu({
|
|
92
|
+
title: trans.theme.chooseIconMode,
|
|
93
|
+
options: iconOptions,
|
|
94
|
+
footer,
|
|
95
|
+
initialIndex: Math.max(0, iconOptions.findIndex(o => o.value === prefs.iconMode)),
|
|
87
96
|
});
|
|
88
|
-
if (
|
|
97
|
+
if (mode === null)
|
|
89
98
|
continue;
|
|
90
99
|
const saved = setIconMode(mode);
|
|
91
100
|
resetIconCache();
|
|
@@ -93,16 +102,18 @@ export async function showSettingsMenu() {
|
|
|
93
102
|
continue;
|
|
94
103
|
}
|
|
95
104
|
if (action === 'color') {
|
|
96
|
-
const
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
105
|
+
const colorOptions = [
|
|
106
|
+
{ value: 'auto', label: trans.theme.modeAuto, hint: prefs.colorMode === 'auto' ? trans.common.current : undefined },
|
|
107
|
+
{ value: 'on', label: trans.theme.modeOn, hint: prefs.colorMode === 'on' ? trans.common.current : undefined },
|
|
108
|
+
{ value: 'off', label: trans.theme.modeOff, hint: prefs.colorMode === 'off' ? trans.common.current : undefined },
|
|
109
|
+
];
|
|
110
|
+
const mode = await runMenu({
|
|
111
|
+
title: trans.theme.chooseColorMode,
|
|
112
|
+
options: colorOptions,
|
|
113
|
+
footer,
|
|
114
|
+
initialIndex: Math.max(0, colorOptions.findIndex(o => o.value === prefs.colorMode)),
|
|
104
115
|
});
|
|
105
|
-
if (
|
|
116
|
+
if (mode === null)
|
|
106
117
|
continue;
|
|
107
118
|
const saved = setColorMode(mode);
|
|
108
119
|
applyColorModePreference(false);
|
package/dist/features/status.js
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
2
|
import { APP_INFO, URLS } from '../config/data.js';
|
|
3
|
+
import { getCapabilities } from '../core/capabilities.js';
|
|
4
|
+
import { createPainter } from '../core/components/painter.js';
|
|
3
5
|
import { pickIcon } from '../core/icons.js';
|
|
4
6
|
import { padEndV, visualWidth } from '../core/text.js';
|
|
5
7
|
import { c } from '../core/theme.js';
|
|
6
8
|
import { createSpinner } from '../core/ui.js';
|
|
9
|
+
import { enterScreen, breadcrumb } from '../core/transitions.js';
|
|
7
10
|
import { t } from '../i18n/index.js';
|
|
8
11
|
function getServiceTargets() {
|
|
9
12
|
const trans = t();
|
|
@@ -44,7 +47,7 @@ async function checkService(name, url, timeoutMs) {
|
|
|
44
47
|
return { name, url, ok: false, latencyMs, error };
|
|
45
48
|
}
|
|
46
49
|
}
|
|
47
|
-
async function checkServiceWithRetry(target, timeoutMs, retries) {
|
|
50
|
+
export async function checkServiceWithRetry(target, timeoutMs, retries) {
|
|
48
51
|
let lastResult = await checkService(target.name, target.url, timeoutMs);
|
|
49
52
|
if (!lastResult.ok) {
|
|
50
53
|
for (let attempt = 0; attempt < retries; attempt++) {
|
|
@@ -116,9 +119,12 @@ export function renderServiceStatusTable(items, options) {
|
|
|
116
119
|
lines.push(` ${applyDim(sep.repeat(nameWidth + statusWidth + 12))}`);
|
|
117
120
|
currentGroup = item.group;
|
|
118
121
|
}
|
|
119
|
-
const nameCol = padEndV(item.intranet ? applyDim(item.name) : applyCyan(item.name), nameWidth);
|
|
122
|
+
const nameCol = padEndV(item.pending ? applyDim(item.name) : (item.intranet ? applyDim(item.name) : applyCyan(item.name)), nameWidth);
|
|
120
123
|
let statusLabel;
|
|
121
|
-
if (item.
|
|
124
|
+
if (item.pending) {
|
|
125
|
+
statusLabel = applyDim(pickIcon('…', '.'));
|
|
126
|
+
}
|
|
127
|
+
else if (item.ok) {
|
|
122
128
|
statusLabel = applyGreen(`${onIcon} ${trans.status.up}`);
|
|
123
129
|
}
|
|
124
130
|
else if (item.intranet) {
|
|
@@ -128,7 +134,7 @@ export function renderServiceStatusTable(items, options) {
|
|
|
128
134
|
statusLabel = applyRed(`${offIcon} ${trans.status.down}`);
|
|
129
135
|
}
|
|
130
136
|
const statusCol = padEndV(statusLabel, statusWidth);
|
|
131
|
-
const latencyCol = item.ok && item.latencyMs != null
|
|
137
|
+
const latencyCol = !item.pending && item.ok && item.latencyMs != null
|
|
132
138
|
? applyLatency(item.latencyMs)
|
|
133
139
|
: applyDim('—');
|
|
134
140
|
lines.push(` ${nameCol} ${statusCol} ${latencyCol}`);
|
|
@@ -137,17 +143,35 @@ export function renderServiceStatusTable(items, options) {
|
|
|
137
143
|
}
|
|
138
144
|
export async function showServiceStatus() {
|
|
139
145
|
const trans = t();
|
|
140
|
-
|
|
141
|
-
const
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
146
|
+
await enterScreen(breadcrumb(trans.menu.status));
|
|
147
|
+
const targets = getServiceTargets();
|
|
148
|
+
if (getCapabilities().reducedMotion) {
|
|
149
|
+
const spinner = createSpinner(trans.status.checking);
|
|
150
|
+
const items = await checkServices();
|
|
151
|
+
const hasFailures = hasServiceFailures(items);
|
|
152
|
+
if (hasFailures)
|
|
153
|
+
spinner.error(trans.status.summaryFail);
|
|
154
|
+
else
|
|
155
|
+
spinner.stop(trans.status.summaryOk);
|
|
156
|
+
console.log();
|
|
157
|
+
console.log(renderServiceStatusTable(items, { color: !!process.stdout.isTTY }));
|
|
158
|
+
console.log();
|
|
159
|
+
return items;
|
|
148
160
|
}
|
|
161
|
+
const items = targets.map((tg) => ({
|
|
162
|
+
name: tg.name, url: tg.url, ok: false, group: tg.group, intranet: tg.intranet, pending: true,
|
|
163
|
+
}));
|
|
164
|
+
const paint = createPainter(() => renderServiceStatusTable(items, { color: getCapabilities().color }));
|
|
149
165
|
console.log();
|
|
150
|
-
|
|
166
|
+
paint();
|
|
167
|
+
await Promise.all(targets.map(async (target, i) => {
|
|
168
|
+
const status = await checkServiceWithRetry(target, 6000, 1);
|
|
169
|
+
items[i] = { ...status, pending: false };
|
|
170
|
+
paint();
|
|
171
|
+
}));
|
|
172
|
+
console.log('\n');
|
|
173
|
+
const hasFailures = hasServiceFailures(items);
|
|
174
|
+
console.log(hasFailures ? c.warn(trans.status.summaryFail) : c.success(trans.status.summaryOk));
|
|
151
175
|
console.log();
|
|
152
176
|
return items;
|
|
153
177
|
}
|