@nbtca/prompt 1.4.2 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +27 -58
- package/SECURITY.md +16 -45
- package/dist/app/app.js +53 -55
- package/dist/app/chrome.js +67 -50
- package/dist/app/fields/list-field.js +12 -25
- package/dist/app/fields/text-field.js +3 -8
- package/dist/app/frame.js +2 -21
- package/dist/app/keys.js +10 -2
- package/dist/app/views/docs-render.js +31 -24
- package/dist/app/views/docs.js +211 -60
- package/dist/app/views/events-render.js +19 -26
- package/dist/app/views/events.js +44 -31
- package/dist/app/views/home.js +28 -30
- package/dist/app/views/schedule-grid-cursor.js +9 -18
- package/dist/app/views/schedule-render.js +47 -71
- package/dist/app/views/schedule.js +158 -81
- package/dist/app/views/settings-render.js +8 -19
- package/dist/app/views/settings.js +92 -17
- package/dist/auth/cookie-transport.js +31 -32
- package/dist/auth/errors.js +3 -1
- package/dist/auth/nbt-auth.js +42 -25
- package/dist/auth/session-store.js +17 -9
- package/dist/config/data.js +9 -11
- package/dist/config/preferences.js +14 -7
- package/dist/core/calendar-day.js +37 -0
- package/dist/core/capabilities.js +6 -3
- package/dist/core/components/confirm.js +9 -8
- package/dist/core/components/menu.js +41 -16
- package/dist/core/components/messages.js +12 -4
- package/dist/core/components/painter.js +3 -1
- package/dist/core/components/spinner.js +17 -6
- package/dist/core/components/text-input.js +24 -18
- package/dist/core/icons.js +2 -2
- package/dist/core/logo.js +23 -5
- package/dist/core/motion.js +25 -19
- package/dist/core/text.js +182 -69
- package/dist/core/theme.js +0 -28
- package/dist/core/transitions.js +2 -2
- package/dist/core/ui.js +15 -13
- package/dist/core/vim-keys.js +9 -15
- package/dist/features/about.js +23 -0
- package/dist/features/calendar-heatmap.js +16 -40
- package/dist/features/calendar-query.js +1 -2
- package/dist/features/calendar.js +12 -185
- package/dist/features/docs.js +436 -275
- package/dist/features/schedule-render.js +65 -101
- package/dist/features/schedule-store.js +51 -9
- package/dist/features/schedule-view.js +46 -213
- package/dist/features/status.js +44 -56
- package/dist/features/student-timetable.js +73 -95
- package/dist/features/theme.js +6 -2
- package/dist/features/timetable-sanitize.js +40 -0
- package/dist/features/update.js +9 -27
- package/dist/i18n/index.js +83 -19
- package/dist/i18n/locales/en.json +1 -1
- package/dist/i18n/locales/zh.json +1 -1
- package/dist/index.js +83 -58
- package/dist/logo/ca-dotmatrix.txt +16 -18
- package/dist/main.js +7 -48
- package/package.json +27 -18
- package/bin/nbtca-welcome.js +0 -2
- package/dist/core/components/screen.js +0 -18
- package/dist/core/menu.js +0 -68
- package/dist/features/links.js +0 -36
- package/dist/features/schedule-query.js +0 -47
- package/dist/features/settings.js +0 -127
- package/dist/logo/ca-logo.png +0 -0
|
@@ -1,16 +1,11 @@
|
|
|
1
1
|
import { loadCalendar, FeedFetchError, FeedParseError, eventToICS } from '@nbtca/nbtcal';
|
|
2
2
|
import chalk from 'chalk';
|
|
3
|
-
import { createSpinner, success, error } from '../core/ui.js';
|
|
4
3
|
import { c, type, space, glyph } from '../core/theme.js';
|
|
5
|
-
import { runMenu, menuFooter } from '../core/components/menu.js';
|
|
6
|
-
import { runTextInput } from '../core/components/text-input.js';
|
|
7
4
|
import { pickIcon } from '../core/icons.js';
|
|
8
|
-
import { padEndV, truncate, visualWidth, wrapAnsiToVisualWidth } from '../core/text.js';
|
|
5
|
+
import { padEndV, sanitizeTerminalLine, sanitizeTerminalText, truncate, visualWidth, wrapAnsiToVisualWidth, } from '../core/text.js';
|
|
9
6
|
import { t } from '../i18n/index.js';
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import { renderHeatmap } from './calendar-heatmap.js';
|
|
13
|
-
import { countdownParts, isCountdownUrgent, buildExportFilename, weekRange, monthRange, filterEvents } from './calendar-query.js';
|
|
7
|
+
import { addLocalDays } from '../core/calendar-day.js';
|
|
8
|
+
import { countdownParts, isCountdownUrgent, buildExportFilename } from './calendar-query.js';
|
|
14
9
|
import { writeFileSync, existsSync } from 'fs';
|
|
15
10
|
import { join } from 'path';
|
|
16
11
|
function formatDate(date) {
|
|
@@ -32,9 +27,7 @@ export async function loadCalendarOrThrow() {
|
|
|
32
27
|
return await loadCalendar({ timeoutMs: 15000 });
|
|
33
28
|
}
|
|
34
29
|
catch (err) {
|
|
35
|
-
const detail = err instanceof FeedFetchError || err instanceof FeedParseError
|
|
36
|
-
? err.message
|
|
37
|
-
: String(err);
|
|
30
|
+
const detail = sanitizeTerminalLine(err instanceof FeedFetchError || err instanceof FeedParseError ? err.message : String(err));
|
|
38
31
|
throw new Error(`${t().calendar.error}: ${detail}`);
|
|
39
32
|
}
|
|
40
33
|
}
|
|
@@ -43,9 +36,9 @@ export function toDisplayEvent(e) {
|
|
|
43
36
|
return {
|
|
44
37
|
date: formatDate(e.start),
|
|
45
38
|
time: e.isAllDay ? '' : formatTime(e.start),
|
|
46
|
-
title: e.title ?? trans.calendar.untitledEvent,
|
|
47
|
-
location: e.location ?? trans.calendar.tbdLocation,
|
|
48
|
-
description: e.description ?? '',
|
|
39
|
+
title: sanitizeTerminalLine(e.title ?? trans.calendar.untitledEvent),
|
|
40
|
+
location: sanitizeTerminalLine(e.location ?? trans.calendar.tbdLocation),
|
|
41
|
+
description: sanitizeTerminalText(e.description ?? ''),
|
|
49
42
|
startDate: e.start,
|
|
50
43
|
recurring: e.recurring,
|
|
51
44
|
uid: e.uid,
|
|
@@ -59,7 +52,7 @@ export async function fetchInRange(start, end) {
|
|
|
59
52
|
}
|
|
60
53
|
export async function fetchHeatmapBuckets() {
|
|
61
54
|
const now = new Date();
|
|
62
|
-
const start =
|
|
55
|
+
const start = addLocalDays(now, -365);
|
|
63
56
|
return (await loadCalendarOrThrow()).heatmap({ start, end: now, bucket: 'day' });
|
|
64
57
|
}
|
|
65
58
|
export function serializeEvents(events) {
|
|
@@ -84,7 +77,6 @@ export function renderEventsTable(events, options) {
|
|
|
84
77
|
const applyCyan = useColor ? chalk.cyan : id;
|
|
85
78
|
const applyBold = useColor ? chalk.bold : id;
|
|
86
79
|
const applyGray = useColor ? chalk.gray : id;
|
|
87
|
-
// dateWidth must fit YYYY-MM-DD HH:MM (16 chars) for cross-year events
|
|
88
80
|
const dateWidth = 16;
|
|
89
81
|
const titleWidth = 32;
|
|
90
82
|
const locWidth = 14;
|
|
@@ -92,12 +84,8 @@ export function renderEventsTable(events, options) {
|
|
|
92
84
|
const headerDate = padEndV(applyDim(trans.calendar.dateTime), dateWidth);
|
|
93
85
|
const headerTitle = padEndV(applyDim(trans.calendar.eventName), titleWidth);
|
|
94
86
|
const headerLoc = applyDim(trans.calendar.location);
|
|
95
|
-
// divider covers exactly: dateWidth + 2-char sep + titleWidth + 2-char sep + locWidth
|
|
96
87
|
const divider = applyDim(sep.repeat(dateWidth + 2 + titleWidth + 2 + locWidth));
|
|
97
|
-
const lines = [
|
|
98
|
-
` ${headerDate} ${headerTitle} ${headerLoc}`,
|
|
99
|
-
` ${divider}`,
|
|
100
|
-
];
|
|
88
|
+
const lines = [` ${headerDate} ${headerTitle} ${headerLoc}`, ` ${divider}`];
|
|
101
89
|
for (const event of events) {
|
|
102
90
|
const dateTime = event.time ? `${event.date} ${event.time}` : event.date;
|
|
103
91
|
const dateCol = padEndV(applyCyan(dateTime), dateWidth);
|
|
@@ -109,22 +97,12 @@ export function renderEventsTable(events, options) {
|
|
|
109
97
|
}
|
|
110
98
|
return lines.join('\n');
|
|
111
99
|
}
|
|
112
|
-
/** One compact line for an "at a glance" activity briefing: date/time · title,
|
|
113
|
-
* with today's events picked out (bold + accent) so the one thing worth
|
|
114
|
-
* noticing doesn't read the same as everything else in the list — and a
|
|
115
|
-
* recurring marker, matching renderEventsTable's convention. Shared by Home's
|
|
116
|
-
* dashboard panel and the Events hub so the two surfaces read consistently
|
|
117
|
-
* instead of drifting into slightly different formats. */
|
|
118
100
|
export function renderEventBrief(e, now) {
|
|
119
101
|
const dot = pickIcon('·', '-');
|
|
120
|
-
const isToday = e.startDate.getFullYear() === now.getFullYear()
|
|
121
|
-
|
|
122
|
-
|
|
102
|
+
const isToday = e.startDate.getFullYear() === now.getFullYear() &&
|
|
103
|
+
e.startDate.getMonth() === now.getMonth() &&
|
|
104
|
+
e.startDate.getDate() === now.getDate();
|
|
123
105
|
const dateTime = `${e.date}${e.time ? ' ' + e.time : ''}`;
|
|
124
|
-
// "Today" gets one consistent brand-colored signal across marker/date/
|
|
125
|
-
// title — c.warn (yellow) stays reserved for genuine time-pressure
|
|
126
|
-
// urgency (isCountdownUrgent below), not "happens today" alone, so the
|
|
127
|
-
// two alert levels never compete for the same color on one line.
|
|
128
106
|
const marker = isToday ? type.active(pickIcon('●', '*')) : type.hint(pickIcon('·', '-'));
|
|
129
107
|
const dateStyled = isToday ? type.active(dateTime) : type.hint(dateTime);
|
|
130
108
|
const titleStyled = isToday ? type.active(e.title) : type.body(e.title);
|
|
@@ -157,157 +135,6 @@ export function renderCountdownBanner(event, now, cols = Number.POSITIVE_INFINIT
|
|
|
157
135
|
.map((line, index) => `${index === 0 ? prefix : continuation}${line}`)
|
|
158
136
|
.join('\n');
|
|
159
137
|
}
|
|
160
|
-
function renderSubscribeHint() {
|
|
161
|
-
const icon = pickIcon('◆', '*');
|
|
162
|
-
console.log(c.muted(` ${icon} ${t().calendar.subscribeHint}: ${URLS.calendar}`));
|
|
163
|
-
}
|
|
164
|
-
/** Startup preview: auto-loads and displays upcoming events, then returns. */
|
|
165
|
-
export async function showEventsPreview() {
|
|
166
|
-
const trans = t();
|
|
167
|
-
const s = createSpinner(trans.calendar.loading);
|
|
168
|
-
try {
|
|
169
|
-
const cal = await loadCalendarOrThrow();
|
|
170
|
-
const events = cal.upcoming({ days: 30 }).map(toDisplayEvent);
|
|
171
|
-
if (events.length === 0) {
|
|
172
|
-
s.stop(trans.calendar.noEvents);
|
|
173
|
-
console.log();
|
|
174
|
-
return;
|
|
175
|
-
}
|
|
176
|
-
s.stop(`${events.length} ${trans.calendar.eventsFound}`);
|
|
177
|
-
console.log();
|
|
178
|
-
console.log(renderEventsTable(events.slice(0, 5), { color: !!process.stdout.isTTY }));
|
|
179
|
-
console.log();
|
|
180
|
-
renderSubscribeHint();
|
|
181
|
-
console.log();
|
|
182
|
-
}
|
|
183
|
-
catch {
|
|
184
|
-
s.error(trans.calendar.error);
|
|
185
|
-
console.log();
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
/** Full interactive calendar hub: countdown + heatmap + a menu of range/search/past views. */
|
|
189
|
-
export async function showCalendar() {
|
|
190
|
-
const trans = t();
|
|
191
|
-
await enterScreen(breadcrumb(trans.menu.events));
|
|
192
|
-
const spinner = createSpinner(trans.calendar.loading);
|
|
193
|
-
let cal;
|
|
194
|
-
try {
|
|
195
|
-
cal = await loadCalendarOrThrow();
|
|
196
|
-
spinner.stop();
|
|
197
|
-
}
|
|
198
|
-
catch {
|
|
199
|
-
spinner.error(trans.calendar.error);
|
|
200
|
-
console.log(c.muted(' ' + trans.calendar.errorHint));
|
|
201
|
-
console.log();
|
|
202
|
-
return;
|
|
203
|
-
}
|
|
204
|
-
const now = new Date();
|
|
205
|
-
const upcoming = cal.upcoming({ days: 30 });
|
|
206
|
-
console.log();
|
|
207
|
-
console.log(renderCountdownBanner(upcoming[0] ? toDisplayEvent(upcoming[0]) : undefined, now));
|
|
208
|
-
console.log();
|
|
209
|
-
console.log(renderHeatmap(cal.heatmap({ start: new Date(now.getTime() - 365 * 24 * 60 * 60 * 1000), end: now, bucket: 'day' }), now, { color: true }));
|
|
210
|
-
console.log();
|
|
211
|
-
while (true) {
|
|
212
|
-
const action = await runMenu({
|
|
213
|
-
title: trans.calendar.viewDetail,
|
|
214
|
-
options: [
|
|
215
|
-
{ value: 'upcoming', label: trans.menu.events, hint: String(upcoming.length) },
|
|
216
|
-
{ value: 'week', label: trans.calendar.thisWeek },
|
|
217
|
-
{ value: 'month', label: trans.calendar.thisMonth },
|
|
218
|
-
{ value: 'search', label: trans.calendar.search },
|
|
219
|
-
{ value: 'past', label: trans.calendar.pastEvents },
|
|
220
|
-
],
|
|
221
|
-
footer: menuFooter(),
|
|
222
|
-
});
|
|
223
|
-
if (action === null)
|
|
224
|
-
return;
|
|
225
|
-
if (action === 'upcoming')
|
|
226
|
-
await showEventList(upcoming, trans.menu.events);
|
|
227
|
-
else if (action === 'week') {
|
|
228
|
-
const r = weekRange(now);
|
|
229
|
-
await showEventList(cal.inRange(r.start, r.end), trans.calendar.thisWeek);
|
|
230
|
-
}
|
|
231
|
-
else if (action === 'month') {
|
|
232
|
-
const r = monthRange(now);
|
|
233
|
-
await showEventList(cal.inRange(r.start, r.end), trans.calendar.thisMonth);
|
|
234
|
-
}
|
|
235
|
-
else if (action === 'search')
|
|
236
|
-
await showSearch(cal);
|
|
237
|
-
else if (action === 'past')
|
|
238
|
-
await showEventList(cal.past({ days: 30 }).reverse(), trans.calendar.pastEvents);
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
async function showEventList(events, title) {
|
|
242
|
-
const trans = t();
|
|
243
|
-
if (events.length === 0) {
|
|
244
|
-
console.log(`${space.indent}${type.hint(trans.calendar.noEvents)}`);
|
|
245
|
-
console.log();
|
|
246
|
-
return;
|
|
247
|
-
}
|
|
248
|
-
const display = events.map(toDisplayEvent);
|
|
249
|
-
console.log();
|
|
250
|
-
console.log(renderEventsTable(display, { color: true }));
|
|
251
|
-
console.log();
|
|
252
|
-
const selected = await runMenu({
|
|
253
|
-
title,
|
|
254
|
-
options: events.map((_e, i) => ({
|
|
255
|
-
value: String(i),
|
|
256
|
-
label: `${display[i].date}${display[i].time ? ' ' + display[i].time : ''} ${display[i].title}`,
|
|
257
|
-
hint: display[i].location,
|
|
258
|
-
})),
|
|
259
|
-
footer: menuFooter(),
|
|
260
|
-
});
|
|
261
|
-
if (selected === null)
|
|
262
|
-
return;
|
|
263
|
-
const raw = events[Number.parseInt(selected, 10)];
|
|
264
|
-
if (raw)
|
|
265
|
-
await showEventDetailRaw(raw);
|
|
266
|
-
}
|
|
267
|
-
async function showEventDetailRaw(raw) {
|
|
268
|
-
const trans = t();
|
|
269
|
-
const e = toDisplayEvent(raw);
|
|
270
|
-
console.log();
|
|
271
|
-
console.log(chalk.bold.cyan(` ${e.title}`));
|
|
272
|
-
console.log(c.muted(` ${e.date}${e.time ? ' ' + e.time : ''} ${pickIcon('·', '|')} ${e.location}`));
|
|
273
|
-
if (raw.recurring)
|
|
274
|
-
console.log(c.muted(` ${pickIcon('↻', '~')} ${trans.calendar.recurringLabel}`));
|
|
275
|
-
if (e.description) {
|
|
276
|
-
console.log();
|
|
277
|
-
for (const line of e.description.trim().split('\n'))
|
|
278
|
-
console.log(` ${line}`);
|
|
279
|
-
}
|
|
280
|
-
else
|
|
281
|
-
console.log(c.muted(` ${trans.calendar.noDescription}`));
|
|
282
|
-
console.log();
|
|
283
|
-
const action = await runMenu({
|
|
284
|
-
title: e.title,
|
|
285
|
-
options: [{ value: 'export', label: trans.calendar.exportIcs }],
|
|
286
|
-
footer: menuFooter(),
|
|
287
|
-
});
|
|
288
|
-
if (action === 'export') {
|
|
289
|
-
const res = exportEventIcs(raw);
|
|
290
|
-
if (res.ok)
|
|
291
|
-
success(`${trans.calendar.exportSuccess}: ${res.path}`);
|
|
292
|
-
else
|
|
293
|
-
error(`${trans.calendar.exportError}: ${res.error ?? ''}`);
|
|
294
|
-
}
|
|
295
|
-
}
|
|
296
|
-
async function showSearch(cal) {
|
|
297
|
-
const trans = t();
|
|
298
|
-
const query = await runTextInput({ message: trans.calendar.searchPrompt, placeholder: trans.calendar.searchPlaceholder });
|
|
299
|
-
if (query === null || !query.trim())
|
|
300
|
-
return;
|
|
301
|
-
const now = new Date();
|
|
302
|
-
const pool = cal.inRange(now, new Date(now.getTime() + 365 * 24 * 60 * 60 * 1000));
|
|
303
|
-
const results = filterEvents(pool, query);
|
|
304
|
-
if (results.length === 0) {
|
|
305
|
-
console.log(`${space.indent}${type.hint(trans.calendar.searchNoResults)}`);
|
|
306
|
-
console.log();
|
|
307
|
-
return;
|
|
308
|
-
}
|
|
309
|
-
await showEventList(results, `${trans.calendar.search}: ${query.trim()}`);
|
|
310
|
-
}
|
|
311
138
|
export function exportEventIcs(event, dir = process.cwd()) {
|
|
312
139
|
const base = buildExportFilename(event);
|
|
313
140
|
let path = join(dir, base);
|