@nbtca/prompt 1.4.2 → 1.5.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.
Files changed (71) hide show
  1. package/README.md +27 -58
  2. package/SECURITY.md +16 -45
  3. package/dist/app/app.js +167 -64
  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 +16 -23
  8. package/dist/app/keys.js +110 -2
  9. package/dist/app/views/docs-render.js +34 -26
  10. package/dist/app/views/docs.js +280 -68
  11. package/dist/app/views/events-render.js +21 -27
  12. package/dist/app/views/events.js +57 -33
  13. package/dist/app/views/home.js +67 -47
  14. package/dist/app/views/schedule-grid-cursor.js +9 -18
  15. package/dist/app/views/schedule-render.js +51 -74
  16. package/dist/app/views/schedule.js +246 -101
  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/cli.js +570 -0
  24. package/dist/config/data.js +9 -11
  25. package/dist/config/preferences.js +21 -7
  26. package/dist/core/calendar-day.js +37 -0
  27. package/dist/core/canvas.js +1 -0
  28. package/dist/core/capabilities.js +6 -3
  29. package/dist/core/components/confirm.js +9 -8
  30. package/dist/core/components/menu.js +64 -38
  31. package/dist/core/components/messages.js +12 -4
  32. package/dist/core/components/painter.js +3 -1
  33. package/dist/core/components/spinner.js +34 -7
  34. package/dist/core/components/text-input.js +24 -18
  35. package/dist/core/icons.js +2 -2
  36. package/dist/core/logo.js +23 -5
  37. package/dist/core/motion.js +25 -19
  38. package/dist/core/text.js +186 -69
  39. package/dist/core/theme.js +0 -28
  40. package/dist/core/transitions.js +2 -2
  41. package/dist/core/ui.js +15 -13
  42. package/dist/core/vim-keys.js +156 -19
  43. package/dist/features/about.js +23 -0
  44. package/dist/features/calendar-heatmap.js +16 -40
  45. package/dist/features/calendar-query.js +1 -2
  46. package/dist/features/calendar-store.js +27 -0
  47. package/dist/features/calendar.js +66 -190
  48. package/dist/features/docs-client.js +225 -0
  49. package/dist/features/docs.js +615 -298
  50. package/dist/features/links.js +44 -29
  51. package/dist/features/schedule-render.js +65 -101
  52. package/dist/features/schedule-store.js +51 -9
  53. package/dist/features/schedule-view.js +46 -213
  54. package/dist/features/status.js +117 -60
  55. package/dist/features/student-timetable.js +74 -97
  56. package/dist/features/theme.js +9 -5
  57. package/dist/features/timetable-sanitize.js +40 -0
  58. package/dist/features/update.js +12 -29
  59. package/dist/i18n/index.js +83 -19
  60. package/dist/i18n/locales/en.json +8 -3
  61. package/dist/i18n/locales/zh.json +8 -3
  62. package/dist/index.js +6 -474
  63. package/dist/logo/ca-dotmatrix.txt +16 -18
  64. package/dist/main.js +7 -48
  65. package/package.json +28 -18
  66. package/bin/nbtca-welcome.js +0 -2
  67. package/dist/core/components/screen.js +0 -18
  68. package/dist/core/menu.js +0 -68
  69. package/dist/features/schedule-query.js +0 -47
  70. package/dist/features/settings.js +0 -127
  71. package/dist/logo/ca-logo.png +0 -0
@@ -1,11 +1,13 @@
1
+ import { createTimetableSchedule, } from '@nbtca/nbtcal/timetable';
1
2
  import { c, type, space, glyph } from '../../core/theme.js';
2
3
  import { pickIcon } from '../../core/icons.js';
3
4
  import { t, fmt } from '../../i18n/index.js';
4
5
  import { renderListFieldWithContext } from '../fields/list-field.js';
5
- import { currentWeekNumber, campusWeekday, meetingsOnDay, nextMeeting } from '../../features/schedule-query.js';
6
6
  import { renderNextClassBanner, renderWeekGrid, renderUnresolvedItems, renderTodayTimeline, weekdayShortLabel, renderTermDensity, renderMeetingDetail, renderDayTimeline, renderDaySwitcher, } from '../../features/schedule-render.js';
7
7
  import { renderEventBrief } from '../../features/calendar.js';
8
- import { visualWidth, wrapAnsiToVisualWidth } from '../../core/text.js';
8
+ import { sanitizeTerminalLine, visualWidth, wrapAnsiWithIndent } from '../../core/text.js';
9
+ import { localDayDifference, parseLocalDate, parseLocalMonday } from '../../core/calendar-day.js';
10
+ import { loadingLines } from '../../core/components/spinner.js';
9
11
  function heading(label) {
10
12
  return `${space.indent}${type.heading(label)}`;
11
13
  }
@@ -13,10 +15,7 @@ function hint(label) {
13
15
  return `${space.indent}${type.hint(label)}`;
14
16
  }
15
17
  function wrappedIndentedLines(label, cols, style) {
16
- const width = Number.isFinite(cols) ? Math.max(1, Math.floor(cols)) : Number.POSITIVE_INFINITY;
17
- const indent = visualWidth(space.indent) < width ? space.indent : '';
18
- const contentWidth = Math.max(1, width - visualWidth(indent));
19
- return wrapAnsiToVisualWidth(style(label), contentWidth).map((part) => `${indent}${part}`);
18
+ return wrapAnsiWithIndent(style(label), cols, space.indent);
20
19
  }
21
20
  function headingLines(label, cols) {
22
21
  return wrappedIndentedLines(label, cols, type.heading);
@@ -51,8 +50,12 @@ function renderShortcutLines(shortcuts, cols, compact = false) {
51
50
  const available = Math.max(1, cols - visualWidth(space.indent));
52
51
  const parts = shortcuts.map((shortcut) => {
53
52
  const text = compact
54
- ? shortcut.showKey === false ? `[${shortcut.key}] ${shortcut.label}` : `[${shortcut.key}]`
55
- : shortcut.showKey === false ? `[${shortcut.label}]` : `[${shortcut.key}] ${shortcut.label}`;
53
+ ? shortcut.showKey === false
54
+ ? `[${shortcut.key}] ${shortcut.label}`
55
+ : `[${shortcut.key}]`
56
+ : shortcut.showKey === false
57
+ ? `[${shortcut.label}]`
58
+ : `[${shortcut.key}] ${shortcut.label}`;
56
59
  return shortcut.warn ? c.warn(text) : type.hint(text);
57
60
  });
58
61
  const lines = [];
@@ -76,20 +79,19 @@ function hubPreGridLines(state, now, cols) {
76
79
  const tt = state.timetable;
77
80
  if (!tt || !state.weekOne)
78
81
  return null;
79
- const week = currentWeekNumber(state.weekOne, now);
82
+ const schedule = createTimetableSchedule(tt, { weekOneMonday: state.weekOne });
83
+ const week = schedule.weekAt(now);
80
84
  const lines = [];
81
- const banner = renderNextClassBanner(nextMeeting(tt.meetings, tt.periods, state.weekOne, now), now, cols);
82
- lines.push(banner || hint(trans.timetable.noNextClass));
85
+ let next = null;
86
+ try {
87
+ next = schedule.next(now);
88
+ }
89
+ catch { }
90
+ const banner = renderNextClassBanner(next, now, cols);
91
+ lines.push(...(banner ? [banner] : hintLines(trans.timetable.noNextClass, cols)));
83
92
  lines.push('');
84
- const todayWd = campusWeekday(now);
93
+ const todayWd = schedule.weekdayAt(now);
85
94
  if (week < 1) {
86
- // weekOne can be a *future* date -- auto-inferred while on break, it
87
- // deliberately points at the upcoming term (see academic-calendar.ts)
88
- // so it's ready the moment classes start. There is no "today" to show
89
- // yet, but the timetable's real week-1 data is already fetched -- show
90
- // it as an explicit preview rather than showing no grid at all
91
- // regardless of terminal height. The "Week 1 preview" heading keeps it
92
- // unambiguous that this isn't "happening right now".
93
95
  lines.push(heading(trans.timetable.termNotStarted));
94
96
  lines.push(hint(fmt(trans.timetable.termStartsIn, {
95
97
  date: state.weekOne,
@@ -99,49 +101,32 @@ function hubPreGridLines(state, now, cols) {
99
101
  lines.push(heading(trans.timetable.termPreviewWeek));
100
102
  return { inlineLines: lines, fallbackLines: [...lines], week: 1, tt };
101
103
  }
102
- const today = meetingsOnDay(tt.meetings, todayWd, week);
104
+ const today = schedule.meetingsOnDay(week, todayWd);
103
105
  const weekHeading = heading(trans.timetable.hubWeek);
104
106
  const inlineLines = [
105
107
  ...lines,
106
- heading(fmt(trans.timetable.todayHeading, { weekday: weekdayShortLabel(todayWd), week: String(week) })),
108
+ heading(fmt(trans.timetable.todayHeading, {
109
+ weekday: weekdayShortLabel(todayWd),
110
+ week: String(week),
111
+ })),
107
112
  ...renderTodayTimeline(today, tt.periods, now, cols).split('\n'),
108
113
  weekHeading,
109
114
  ];
110
115
  return { inlineLines, fallbackLines: [...lines, weekHeading], week, tt };
111
116
  }
112
- // Below this width, even an all-empty grid's own per-column floor (3, plus
113
- // row-head and separator overhead) leaves each of the 7 columns too cramped
114
- // to show real content -- a technically-fitting but practically unreadable
115
- // grid isn't better than the single-day view, so width gates the decision
116
- // just as much as height does.
117
117
  const MIN_GRID_COLS = 100;
118
- /** The one place that decides "does the real week grid fit inline (plus a
119
- * floor reserved for the shortcut bar), or does the hub fall back to the
120
- * single-day view." Both branches represent the exact same gridCursor, just
121
- * rendered differently, so unlike the old non-interactive strip fallback
122
- * this decision no longer needs to be exposed to key handling -- arrow
123
- * keys/Enter are always meaningful in hub mode regardless of which branch is
124
- * currently on screen. */
125
118
  function gridFitsInline(precedingLineCount, tt, week, now, bodyRows, cols, cursor, reservedRows) {
126
119
  if (cols < MIN_GRID_COLS)
127
120
  return false;
128
- const gridLines = renderWeekGrid(tt.meetings, tt.periods, week, now, cols, cursor).split('\n');
121
+ const gridLines = renderWeekGrid(tt, week, now, cols, cursor).split('\n');
129
122
  return precedingLineCount + gridLines.length <= bodyRows - reservedRows;
130
123
  }
131
- /** Renders the full weekday x period grid if it (plus a floor reserved for
132
- * the shortcut bar) fits within bodyRows and cols, otherwise a single day's
133
- * detailed timeline -- the day the cursor's own weekday points at (today, by
134
- * default). A large terminal genuinely has no excuse to not show the whole
135
- * week; a small one is better served by one day shown properly than seven
136
- * days crammed into unreadable slivers. Shared by the "this week" and "term
137
- * hasn't started yet, preview week 1" branches of renderHubBody -- the same
138
- * measure-and-fallback decision, just against a different week number. */
139
124
  function renderAdaptiveWeekGrid(inlineLines, fallbackLines, tt, week, todayWd, now, bodyRows, cols, cursor, reservedRows) {
140
125
  if (gridFitsInline(inlineLines.length, tt, week, now, bodyRows, cols, cursor, reservedRows)) {
141
- return [...inlineLines, ...renderWeekGrid(tt.meetings, tt.periods, week, now, cols, cursor).split('\n')];
126
+ return [...inlineLines, ...renderWeekGrid(tt, week, now, cols, cursor).split('\n')];
142
127
  }
143
128
  const selectedWd = cursor?.weekday ?? todayWd;
144
- const dayMeetings = meetingsOnDay(tt.meetings, selectedWd, week);
129
+ const dayMeetings = createTimetableSchedule(tt).meetingsOnDay(week, selectedWd);
145
130
  return [
146
131
  ...fallbackLines,
147
132
  renderDaySwitcher(selectedWd, todayWd, cols),
@@ -164,7 +149,7 @@ function renderHubBody(state, now, bodyRows, cols) {
164
149
  }
165
150
  tail.push(...shortcutLines);
166
151
  const content = pre
167
- ? renderAdaptiveWeekGrid(pre.inlineLines, pre.fallbackLines, pre.tt, pre.week, campusWeekday(now), now, rows, cols, state.gridCursor, tail.length)
152
+ ? renderAdaptiveWeekGrid(pre.inlineLines, pre.fallbackLines, pre.tt, pre.week, createTimetableSchedule(pre.tt).weekdayAt(now), now, rows, cols, state.gridCursor, tail.length)
168
153
  : [];
169
154
  return { content, tail };
170
155
  };
@@ -174,19 +159,14 @@ function renderHubBody(state, now, bodyRows, cols) {
174
159
  const compact = build(renderShortcutLines(shortcuts, cols, true));
175
160
  if (compact.tail.length >= rows)
176
161
  return rows > 0 ? compact.tail.slice(-rows) : [];
177
- return [
178
- ...compact.content.slice(0, rows - compact.tail.length),
179
- ...compact.tail,
180
- ];
162
+ return [...compact.content.slice(0, rows - compact.tail.length), ...compact.tail];
181
163
  }
182
164
  const TERM_PROGRESS_WIDTH = 20;
183
- function renderTermProgressBar(w, now, cols) {
165
+ function renderTermProgressBar(w, cols) {
184
166
  if (!w.nextBreakStart)
185
167
  return null;
186
- const weekOneMs = new Date(`${w.weekOneMonday}T00:00:00`).getTime();
187
- const nextBreakMs = new Date(`${w.nextBreakStart}T00:00:00`).getTime();
188
- const totalWeeks = Math.max(1, Math.round((nextBreakMs - weekOneMs) / (7 * 86400000)));
189
- const currentWeek = currentWeekNumber(w.weekOneMonday, now);
168
+ const totalWeeks = Math.max(1, Math.round(localDayDifference(parseLocalMonday(w.weekOneMonday), parseLocalDate(w.nextBreakStart)) / 7));
169
+ const currentWeek = w.currentWeek;
190
170
  const labelText = fmt(t().timetable.weekLabel2, { week: `${currentWeek}/${totalWeeks}` });
191
171
  const label = type.hint(labelText);
192
172
  const width = Number.isFinite(cols) ? Math.max(1, Math.floor(cols)) : Number.POSITIVE_INFINITY;
@@ -203,31 +183,31 @@ function renderTermProgressBar(w, now, cols) {
203
183
  : [`${indent}${bar}`, ...hintLines(labelText, cols)];
204
184
  }
205
185
  function daysBetween(a, b) {
206
- return Math.max(0, Math.ceil((b.getTime() - a.getTime()) / 86400000));
186
+ return Math.max(0, localDayDifference(a, b));
207
187
  }
208
188
  function renderPublicBody(state, now, bodyRows, cols) {
209
189
  const trans = t();
210
190
  const lines = [];
211
191
  const w = state.publicWindow;
212
192
  if (w === undefined) {
213
- lines.push(...hintLines(trans.common.loading, cols));
193
+ lines.push(...loadingLines(trans.common.loading, cols));
214
194
  }
215
195
  else if (w === null) {
216
196
  lines.push(...hintLines(trans.timetable.publicUnavailable, cols));
217
197
  }
218
198
  else if (w.status === 'onBreak') {
219
- lines.push(...headingLines(fmt(trans.timetable.onBreak, { title: w.breakTitle }), cols));
199
+ lines.push(...headingLines(fmt(trans.timetable.onBreak, { title: sanitizeTerminalLine(w.breakTitle) }), cols));
220
200
  }
221
201
  else {
222
202
  const semesterLabel = w.semester === '1' ? trans.timetable.semester1 : trans.timetable.semester2;
223
- lines.push(...headingLines(`${fmt(trans.timetable.academicYearSuffix, { year: w.academicYear })} · ${semesterLabel} · ${fmt(trans.timetable.weekLabel2, { week: String(w.currentWeek) })}`, cols));
224
- const bar = renderTermProgressBar(w, now, cols);
203
+ lines.push(...headingLines(`${fmt(trans.timetable.academicYearSuffix, { year: sanitizeTerminalLine(w.academicYear) })} · ${semesterLabel} · ${fmt(trans.timetable.weekLabel2, { week: String(w.currentWeek) })}`, cols));
204
+ const bar = renderTermProgressBar(w, cols);
225
205
  if (bar)
226
206
  lines.push(...bar);
227
207
  if (w.nextBreakStart && w.nextBreakTitle) {
228
208
  lines.push(...hintLines(fmt(trans.timetable.daysUntilBreak, {
229
- title: w.nextBreakTitle,
230
- days: String(daysBetween(now, new Date(`${w.nextBreakStart}T00:00:00`))),
209
+ title: sanitizeTerminalLine(w.nextBreakTitle),
210
+ days: String(daysBetween(now, parseLocalDate(w.nextBreakStart))),
231
211
  }), cols));
232
212
  }
233
213
  }
@@ -261,7 +241,7 @@ export function renderSchedule(state, now, bodyRows = 100, cols = 80) {
261
241
  const trans = t();
262
242
  switch (state.mode) {
263
243
  case 'loading':
264
- return hintLines(trans.common.loading, cols);
244
+ return loadingLines(trans.common.loading, cols);
265
245
  case 'public':
266
246
  return renderPublicBody(state, now, bodyRows, cols);
267
247
  case 'needsLoginId':
@@ -272,7 +252,7 @@ export function renderSchedule(state, now, bodyRows = 100, cols = 80) {
272
252
  case 'needsLoginPassword':
273
253
  return state.passwordField?.render(cols) ?? [];
274
254
  case 'authenticating':
275
- return hintLines(state.statusMessage ?? trans.common.loading, cols);
255
+ return loadingLines(state.statusMessage ?? trans.common.loading, cols);
276
256
  case 'needsWeekOne':
277
257
  return [
278
258
  ...(state.errorMessage ? [...hintLines(state.errorMessage, cols), ''] : []),
@@ -283,20 +263,17 @@ export function renderSchedule(state, now, bodyRows = 100, cols = 80) {
283
263
  case 'week': {
284
264
  if (!state.timetable || !state.weekOne)
285
265
  return [hint(trans.timetable.genericError)];
286
- const week = currentWeekNumber(state.weekOne, now);
266
+ const schedule = createTimetableSchedule(state.timetable, {
267
+ weekOneMonday: state.weekOne,
268
+ });
269
+ const week = Math.max(1, schedule.weekAt(now));
287
270
  const weekLines = [heading(trans.timetable.hubWeek), ''];
288
- // Standalone week mode gets the *whole* bodyRows to itself (no
289
- // banner/today-section eating into it first, unlike the hub's own
290
- // inline area) -- reached via the `w` shortcut specifically so a
291
- // marginal terminal that couldn't fit the grid inline the hub still
292
- // gets a real shot at it here, falling back to the single-day view
293
- // only if even the full screen isn't enough.
294
- return renderAdaptiveWeekGrid(weekLines, weekLines, state.timetable, week, campusWeekday(now), now, bodyRows, cols, state.gridCursor, 2);
271
+ return renderAdaptiveWeekGrid(weekLines, weekLines, state.timetable, week, schedule.weekdayAt(now), now, bodyRows, cols, state.gridCursor, 2);
295
272
  }
296
273
  case 'termDensity':
297
- return state.timetable && state.weekOne
298
- ? renderTermDensity(state.timetable.meetings, state.weekOne, currentWeekNumber(state.weekOne, now), cols).split('\n')
299
- : [hint(trans.timetable.genericError)];
274
+ if (!state.timetable || !state.weekOne)
275
+ return [hint(trans.timetable.genericError)];
276
+ return renderTermDensity(state.timetable.meetings, state.weekOne, createTimetableSchedule(state.timetable, { weekOneMonday: state.weekOne }).weekAt(now), cols).split('\n');
300
277
  case 'termPicker':
301
278
  return state.termField?.render(bodyRows, cols) ?? [];
302
279
  case 'unresolved':