@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
|
@@ -1,13 +1,18 @@
|
|
|
1
|
-
import { loadCalendar, FeedFetchError, FeedParseError } from '@nbtca/nbtcal';
|
|
1
|
+
import { loadCalendar, FeedFetchError, FeedParseError, eventToICS } from '@nbtca/nbtcal';
|
|
2
2
|
import chalk from 'chalk';
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
3
|
+
import { createSpinner, success, error } from '../core/ui.js';
|
|
4
|
+
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';
|
|
6
7
|
import { pickIcon } from '../core/icons.js';
|
|
7
|
-
import { padEndV, truncate } from '../core/text.js';
|
|
8
|
+
import { padEndV, truncate, visualWidth, wrapAnsiToVisualWidth } from '../core/text.js';
|
|
8
9
|
import { t } from '../i18n/index.js';
|
|
10
|
+
import { enterScreen, breadcrumb } from '../core/transitions.js';
|
|
9
11
|
import { URLS } from '../config/data.js';
|
|
10
12
|
import { renderHeatmap } from './calendar-heatmap.js';
|
|
13
|
+
import { countdownParts, isCountdownUrgent, buildExportFilename, weekRange, monthRange, filterEvents } from './calendar-query.js';
|
|
14
|
+
import { writeFileSync, existsSync } from 'fs';
|
|
15
|
+
import { join } from 'path';
|
|
11
16
|
function formatDate(date) {
|
|
12
17
|
const now = new Date();
|
|
13
18
|
const month = String(date.getMonth() + 1).padStart(2, '0');
|
|
@@ -22,7 +27,7 @@ function formatTime(date) {
|
|
|
22
27
|
const minutes = String(date.getMinutes()).padStart(2, '0');
|
|
23
28
|
return `${hours}:${minutes}`;
|
|
24
29
|
}
|
|
25
|
-
async function loadCalendarOrThrow() {
|
|
30
|
+
export async function loadCalendarOrThrow() {
|
|
26
31
|
try {
|
|
27
32
|
return await loadCalendar({ timeoutMs: 15000 });
|
|
28
33
|
}
|
|
@@ -42,11 +47,16 @@ export function toDisplayEvent(e) {
|
|
|
42
47
|
location: e.location ?? trans.calendar.tbdLocation,
|
|
43
48
|
description: e.description ?? '',
|
|
44
49
|
startDate: e.start,
|
|
50
|
+
recurring: e.recurring,
|
|
51
|
+
uid: e.uid,
|
|
45
52
|
};
|
|
46
53
|
}
|
|
47
54
|
export async function fetchEvents() {
|
|
48
55
|
return (await loadCalendarOrThrow()).upcoming({ days: 30 }).map(toDisplayEvent);
|
|
49
56
|
}
|
|
57
|
+
export async function fetchInRange(start, end) {
|
|
58
|
+
return (await loadCalendarOrThrow()).inRange(start, end).map(toDisplayEvent);
|
|
59
|
+
}
|
|
50
60
|
export async function fetchHeatmapBuckets() {
|
|
51
61
|
const now = new Date();
|
|
52
62
|
const start = new Date(now.getTime() - 365 * 24 * 60 * 60 * 1000);
|
|
@@ -60,13 +70,15 @@ export function serializeEvents(events) {
|
|
|
60
70
|
location: event.location,
|
|
61
71
|
description: event.description,
|
|
62
72
|
startDateISO: event.startDate.toISOString(),
|
|
73
|
+
recurring: event.recurring,
|
|
74
|
+
uid: event.uid,
|
|
63
75
|
}));
|
|
64
76
|
}
|
|
65
77
|
export function renderEventsTable(events, options) {
|
|
66
78
|
const trans = t();
|
|
67
79
|
const useColor = options?.color !== false;
|
|
68
80
|
if (events.length === 0)
|
|
69
|
-
return
|
|
81
|
+
return `${space.indent}${type.hint(trans.calendar.noEvents)}`;
|
|
70
82
|
const id = (s) => s;
|
|
71
83
|
const applyDim = useColor ? chalk.dim : id;
|
|
72
84
|
const applyCyan = useColor ? chalk.cyan : id;
|
|
@@ -89,32 +101,66 @@ export function renderEventsTable(events, options) {
|
|
|
89
101
|
for (const event of events) {
|
|
90
102
|
const dateTime = event.time ? `${event.date} ${event.time}` : event.date;
|
|
91
103
|
const dateCol = padEndV(applyCyan(dateTime), dateWidth);
|
|
92
|
-
const
|
|
104
|
+
const marker = event.recurring ? `${pickIcon('↻', '~')} ` : '';
|
|
105
|
+
const titleText = truncate(`${marker}${event.title}`, titleWidth);
|
|
106
|
+
const titleCol = padEndV(applyBold(titleText), titleWidth);
|
|
93
107
|
const locCol = applyGray(truncate(event.location, locWidth));
|
|
94
108
|
lines.push(` ${dateCol} ${titleCol} ${locCol}`);
|
|
95
109
|
}
|
|
96
110
|
return lines.join('\n');
|
|
97
111
|
}
|
|
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
|
+
export function renderEventBrief(e, now) {
|
|
119
|
+
const dot = pickIcon('·', '-');
|
|
120
|
+
const isToday = e.startDate.getFullYear() === now.getFullYear()
|
|
121
|
+
&& e.startDate.getMonth() === now.getMonth()
|
|
122
|
+
&& e.startDate.getDate() === now.getDate();
|
|
123
|
+
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
|
+
const marker = isToday ? type.active(pickIcon('●', '*')) : type.hint(pickIcon('·', '-'));
|
|
129
|
+
const dateStyled = isToday ? type.active(dateTime) : type.hint(dateTime);
|
|
130
|
+
const titleStyled = isToday ? type.active(e.title) : type.body(e.title);
|
|
131
|
+
const recurringMark = e.recurring ? ` ${pickIcon('↻', '~')}` : '';
|
|
132
|
+
return `${space.indent}${marker} ${dateStyled} ${dot} ${titleStyled}${recurringMark}`;
|
|
133
|
+
}
|
|
134
|
+
export function renderCountdownBanner(event, now, cols = Number.POSITIVE_INFINITY) {
|
|
135
|
+
if (!event)
|
|
136
|
+
return '';
|
|
137
|
+
const trans = t();
|
|
138
|
+
const p = countdownParts(event.startDate, now);
|
|
139
|
+
const inp = trans.calendar.inPrefix;
|
|
140
|
+
const when = p.past
|
|
141
|
+
? trans.calendar.startingNow
|
|
142
|
+
: p.days > 0
|
|
143
|
+
? `${inp} ${p.days}d ${p.hours}h`
|
|
144
|
+
: p.hours > 0
|
|
145
|
+
? `${inp} ${p.hours}h ${p.minutes}m`
|
|
146
|
+
: `${inp} ${p.minutes}m`;
|
|
147
|
+
const whenStyled = isCountdownUrgent(p) ? c.warn(when) : type.hint(when);
|
|
148
|
+
const dot = pickIcon('·', '-');
|
|
149
|
+
const width = Number.isFinite(cols) ? Math.max(1, Math.floor(cols)) : Number.POSITIVE_INFINITY;
|
|
150
|
+
const cursor = type.active(glyph.cursor());
|
|
151
|
+
const prefixes = [`${space.indent}${cursor} `, `${cursor} `, cursor, ''];
|
|
152
|
+
const prefix = prefixes.find((candidate) => visualWidth(candidate) < width) ?? '';
|
|
153
|
+
const continuation = ' '.repeat(visualWidth(prefix));
|
|
154
|
+
const contentWidth = Math.max(1, width - visualWidth(prefix));
|
|
155
|
+
const content = `${type.label(trans.calendar.next)} ${dot} ${type.body(event.title)} ${dot} ${whenStyled}`;
|
|
156
|
+
return wrapAnsiToVisualWidth(content, contentWidth)
|
|
157
|
+
.map((line, index) => `${index === 0 ? prefix : continuation}${line}`)
|
|
158
|
+
.join('\n');
|
|
159
|
+
}
|
|
98
160
|
function renderSubscribeHint() {
|
|
99
161
|
const icon = pickIcon('◆', '*');
|
|
100
162
|
console.log(c.muted(` ${icon} ${t().calendar.subscribeHint}: ${URLS.calendar}`));
|
|
101
163
|
}
|
|
102
|
-
async function showEventDetail(event) {
|
|
103
|
-
const trans = t();
|
|
104
|
-
console.log();
|
|
105
|
-
console.log(chalk.bold.cyan(` ${event.title}`));
|
|
106
|
-
console.log(c.muted(` ${event.date}${event.time ? ' ' + event.time : ''} ${pickIcon('·', '|')} ${event.location}`));
|
|
107
|
-
if (event.description) {
|
|
108
|
-
console.log();
|
|
109
|
-
for (const line of event.description.trim().split('\n')) {
|
|
110
|
-
console.log(` ${line}`);
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
else {
|
|
114
|
-
console.log(c.muted(` ${trans.calendar.noDescription}`));
|
|
115
|
-
}
|
|
116
|
-
console.log();
|
|
117
|
-
}
|
|
118
164
|
/** Startup preview: auto-loads and displays upcoming events, then returns. */
|
|
119
165
|
export async function showEventsPreview() {
|
|
120
166
|
const trans = t();
|
|
@@ -139,94 +185,142 @@ export async function showEventsPreview() {
|
|
|
139
185
|
console.log();
|
|
140
186
|
}
|
|
141
187
|
}
|
|
142
|
-
/**
|
|
143
|
-
async function
|
|
188
|
+
/** Full interactive calendar hub: countdown + heatmap + a menu of range/search/past views. */
|
|
189
|
+
export async function showCalendar() {
|
|
144
190
|
const trans = t();
|
|
145
|
-
|
|
191
|
+
await enterScreen(breadcrumb(trans.menu.events));
|
|
192
|
+
const spinner = createSpinner(trans.calendar.loading);
|
|
193
|
+
let cal;
|
|
146
194
|
try {
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
if (events.length === 0) {
|
|
150
|
-
s.stop(trans.calendar.noPastEvents);
|
|
151
|
-
console.log();
|
|
152
|
-
return;
|
|
153
|
-
}
|
|
154
|
-
s.stop(`${events.length} ${trans.calendar.eventsFound}`);
|
|
155
|
-
console.log();
|
|
156
|
-
console.log(renderEventsTable(events, { color: true }));
|
|
157
|
-
console.log();
|
|
158
|
-
const options = [
|
|
159
|
-
...events.map((e, i) => ({
|
|
160
|
-
value: String(i),
|
|
161
|
-
label: `${e.date}${e.time ? ' ' + e.time : ''} ${e.title}`,
|
|
162
|
-
hint: e.location,
|
|
163
|
-
})),
|
|
164
|
-
{ value: '__back__', label: c.muted(trans.common.back) },
|
|
165
|
-
];
|
|
166
|
-
const selected = await select({ message: trans.calendar.viewPastDetail, options });
|
|
167
|
-
if (!isCancel(selected) && selected !== '__back__') {
|
|
168
|
-
const event = events[Number.parseInt(selected, 10)];
|
|
169
|
-
if (event)
|
|
170
|
-
await showEventDetail(event);
|
|
171
|
-
}
|
|
195
|
+
cal = await loadCalendarOrThrow();
|
|
196
|
+
spinner.stop();
|
|
172
197
|
}
|
|
173
198
|
catch {
|
|
174
|
-
|
|
199
|
+
spinner.error(trans.calendar.error);
|
|
175
200
|
console.log(c.muted(' ' + trans.calendar.errorHint));
|
|
176
201
|
console.log();
|
|
202
|
+
return;
|
|
177
203
|
}
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
const
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
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(),
|
|
191
222
|
});
|
|
192
|
-
if (
|
|
193
|
-
s.stop(trans.calendar.noEvents);
|
|
194
|
-
console.log();
|
|
195
|
-
console.log(renderHeatmap(heatmapBuckets, now, { color: true }));
|
|
196
|
-
console.log();
|
|
223
|
+
if (action === null)
|
|
197
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);
|
|
198
230
|
}
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
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)}`);
|
|
202
245
|
console.log();
|
|
203
|
-
|
|
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) {
|
|
204
276
|
console.log();
|
|
205
|
-
|
|
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)}`);
|
|
206
306
|
console.log();
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
if (selected === '__past__') {
|
|
220
|
-
await showPastEvents();
|
|
221
|
-
return;
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
await showEventList(results, `${trans.calendar.search}: ${query.trim()}`);
|
|
310
|
+
}
|
|
311
|
+
export function exportEventIcs(event, dir = process.cwd()) {
|
|
312
|
+
const base = buildExportFilename(event);
|
|
313
|
+
let path = join(dir, base);
|
|
314
|
+
try {
|
|
315
|
+
let n = 1;
|
|
316
|
+
while (existsSync(path)) {
|
|
317
|
+
path = join(dir, base.replace(/\.ics$/, `-${n}.ics`));
|
|
318
|
+
n++;
|
|
222
319
|
}
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
await showEventDetail(event);
|
|
320
|
+
writeFileSync(path, eventToICS(event), 'utf-8');
|
|
321
|
+
return { ok: true, path };
|
|
226
322
|
}
|
|
227
|
-
catch {
|
|
228
|
-
|
|
229
|
-
console.log(c.muted(' ' + trans.calendar.errorHint));
|
|
230
|
-
console.log();
|
|
323
|
+
catch (err) {
|
|
324
|
+
return { ok: false, path, error: err instanceof Error ? err.message : String(err) };
|
|
231
325
|
}
|
|
232
326
|
}
|