@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.
Files changed (67) hide show
  1. package/README.md +27 -58
  2. package/SECURITY.md +16 -45
  3. package/dist/app/app.js +53 -55
  4. package/dist/app/chrome.js +67 -50
  5. package/dist/app/fields/list-field.js +12 -25
  6. package/dist/app/fields/text-field.js +3 -8
  7. package/dist/app/frame.js +2 -21
  8. package/dist/app/keys.js +10 -2
  9. package/dist/app/views/docs-render.js +31 -24
  10. package/dist/app/views/docs.js +211 -60
  11. package/dist/app/views/events-render.js +19 -26
  12. package/dist/app/views/events.js +44 -31
  13. package/dist/app/views/home.js +28 -30
  14. package/dist/app/views/schedule-grid-cursor.js +9 -18
  15. package/dist/app/views/schedule-render.js +47 -71
  16. package/dist/app/views/schedule.js +158 -81
  17. package/dist/app/views/settings-render.js +8 -19
  18. package/dist/app/views/settings.js +92 -17
  19. package/dist/auth/cookie-transport.js +31 -32
  20. package/dist/auth/errors.js +3 -1
  21. package/dist/auth/nbt-auth.js +42 -25
  22. package/dist/auth/session-store.js +17 -9
  23. package/dist/config/data.js +9 -11
  24. package/dist/config/preferences.js +14 -7
  25. package/dist/core/calendar-day.js +37 -0
  26. package/dist/core/capabilities.js +6 -3
  27. package/dist/core/components/confirm.js +9 -8
  28. package/dist/core/components/menu.js +41 -16
  29. package/dist/core/components/messages.js +12 -4
  30. package/dist/core/components/painter.js +3 -1
  31. package/dist/core/components/spinner.js +17 -6
  32. package/dist/core/components/text-input.js +24 -18
  33. package/dist/core/icons.js +2 -2
  34. package/dist/core/logo.js +23 -5
  35. package/dist/core/motion.js +25 -19
  36. package/dist/core/text.js +182 -69
  37. package/dist/core/theme.js +0 -28
  38. package/dist/core/transitions.js +2 -2
  39. package/dist/core/ui.js +15 -13
  40. package/dist/core/vim-keys.js +9 -15
  41. package/dist/features/about.js +23 -0
  42. package/dist/features/calendar-heatmap.js +16 -40
  43. package/dist/features/calendar-query.js +1 -2
  44. package/dist/features/calendar.js +12 -185
  45. package/dist/features/docs.js +436 -275
  46. package/dist/features/schedule-render.js +65 -101
  47. package/dist/features/schedule-store.js +51 -9
  48. package/dist/features/schedule-view.js +46 -213
  49. package/dist/features/status.js +44 -56
  50. package/dist/features/student-timetable.js +73 -95
  51. package/dist/features/theme.js +6 -2
  52. package/dist/features/timetable-sanitize.js +40 -0
  53. package/dist/features/update.js +9 -27
  54. package/dist/i18n/index.js +83 -19
  55. package/dist/i18n/locales/en.json +1 -1
  56. package/dist/i18n/locales/zh.json +1 -1
  57. package/dist/index.js +83 -58
  58. package/dist/logo/ca-dotmatrix.txt +16 -18
  59. package/dist/main.js +7 -48
  60. package/package.json +27 -18
  61. package/bin/nbtca-welcome.js +0 -2
  62. package/dist/core/components/screen.js +0 -18
  63. package/dist/core/menu.js +0 -68
  64. package/dist/features/links.js +0 -36
  65. package/dist/features/schedule-query.js +0 -47
  66. package/dist/features/settings.js +0 -127
  67. 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 { enterScreen, breadcrumb } from '../core/transitions.js';
11
- import { URLS } from '../config/data.js';
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 = new Date(now.getTime() - 365 * 24 * 60 * 60 * 1000);
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
- && e.startDate.getMonth() === now.getMonth()
122
- && e.startDate.getDate() === now.getDate();
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);