@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.
- package/README.md +27 -58
- package/SECURITY.md +16 -45
- package/dist/app/app.js +167 -64
- 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 +16 -23
- package/dist/app/keys.js +110 -2
- package/dist/app/views/docs-render.js +34 -26
- package/dist/app/views/docs.js +280 -68
- package/dist/app/views/events-render.js +21 -27
- package/dist/app/views/events.js +57 -33
- package/dist/app/views/home.js +67 -47
- package/dist/app/views/schedule-grid-cursor.js +9 -18
- package/dist/app/views/schedule-render.js +51 -74
- package/dist/app/views/schedule.js +246 -101
- 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/cli.js +570 -0
- package/dist/config/data.js +9 -11
- package/dist/config/preferences.js +21 -7
- package/dist/core/calendar-day.js +37 -0
- package/dist/core/canvas.js +1 -0
- package/dist/core/capabilities.js +6 -3
- package/dist/core/components/confirm.js +9 -8
- package/dist/core/components/menu.js +64 -38
- package/dist/core/components/messages.js +12 -4
- package/dist/core/components/painter.js +3 -1
- package/dist/core/components/spinner.js +34 -7
- 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 +186 -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 +156 -19
- 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-store.js +27 -0
- package/dist/features/calendar.js +66 -190
- package/dist/features/docs-client.js +225 -0
- package/dist/features/docs.js +615 -298
- package/dist/features/links.js +44 -29
- 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 +117 -60
- package/dist/features/student-timetable.js +74 -97
- package/dist/features/theme.js +9 -5
- package/dist/features/timetable-sanitize.js +40 -0
- package/dist/features/update.js +12 -29
- package/dist/i18n/index.js +83 -19
- package/dist/i18n/locales/en.json +8 -3
- package/dist/i18n/locales/zh.json +8 -3
- package/dist/index.js +6 -474
- package/dist/logo/ca-dotmatrix.txt +16 -18
- package/dist/main.js +7 -48
- package/package.json +28 -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/schedule-query.js +0 -47
- package/dist/features/settings.js +0 -127
- package/dist/logo/ca-logo.png +0 -0
|
@@ -1,12 +1,7 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Heatmap renderer for calendar activity.
|
|
3
|
-
* GitHub-contributions-style grid: 7 weekday rows (Mon..Sun), week columns.
|
|
4
|
-
*/
|
|
5
1
|
import chalk from 'chalk';
|
|
6
2
|
import { pickIcon } from '../core/icons.js';
|
|
7
3
|
import { space, type } from '../core/theme.js';
|
|
8
4
|
import { t, getCurrentLanguage } from '../i18n/index.js';
|
|
9
|
-
/** Parse a 'YYYY-MM-DD' date string into a UTC proxy Date (host-timezone-independent). */
|
|
10
5
|
function parseBucketDate(date) {
|
|
11
6
|
const parts = date.split('-').map(Number);
|
|
12
7
|
const y = parts[0] ?? 0;
|
|
@@ -14,11 +9,9 @@ function parseBucketDate(date) {
|
|
|
14
9
|
const d = parts[2] ?? 1;
|
|
15
10
|
return new Date(Date.UTC(y, m - 1, d));
|
|
16
11
|
}
|
|
17
|
-
/** 0=Sun,1=Mon,...,6=Sat -> Mon-indexed 0..6 */
|
|
18
12
|
function utcDayToMonIndex(utcDay) {
|
|
19
13
|
return (utcDay + 6) % 7;
|
|
20
14
|
}
|
|
21
|
-
/** Map a count to a unicode intensity glyph (or ASCII fallback). */
|
|
22
15
|
function countToGlyph(count) {
|
|
23
16
|
if (count <= 0)
|
|
24
17
|
return pickIcon('·', ' ');
|
|
@@ -30,46 +23,28 @@ function countToGlyph(count) {
|
|
|
30
23
|
return pickIcon('▓', '-');
|
|
31
24
|
return pickIcon('█', '=');
|
|
32
25
|
}
|
|
33
|
-
// A fixed 4-stop truecolor green ramp, not chalk's named green/greenBright --
|
|
34
|
-
// those resolve to whatever the user's terminal theme defines for "green",
|
|
35
|
-
// same problem the rest of the app's palette solved by specifying hex
|
|
36
|
-
// directly (theme.ts's brand blue, brandGradient) instead of leaning on
|
|
37
|
-
// ANSI color names. Kept muted/desaturated rather than a punchy grass
|
|
38
|
-
// green so it sits quietly next to the app's own cool blue palette instead
|
|
39
|
-
// of reading as a louder, disconnected accent.
|
|
40
26
|
const HEATMAP_RAMP = ['#1b4332', '#2d6a4f', '#40916c', '#52b788'];
|
|
41
27
|
const MAX_WEEK_COLUMNS = 53;
|
|
42
28
|
const GRID_PREFIX_WIDTH = 6;
|
|
43
|
-
/** Apply a green color ramp based on count. Identity when count is 0. */
|
|
44
29
|
function applyColor(glyph, count, useColor) {
|
|
45
30
|
if (!useColor || count <= 0)
|
|
46
31
|
return glyph;
|
|
47
32
|
const level = Math.min(count, HEATMAP_RAMP.length) - 1;
|
|
48
33
|
return chalk.hex(HEATMAP_RAMP[level])(glyph);
|
|
49
34
|
}
|
|
50
|
-
/**
|
|
51
|
-
* Render a GitHub-contributions-style heatmap grid.
|
|
52
|
-
*
|
|
53
|
-
* @param buckets Dense daily buckets from nbtcal's .heatmap() call.
|
|
54
|
-
* @param today The "today" date (used to determine the end column).
|
|
55
|
-
* @param options Optional rendering options.
|
|
56
|
-
*/
|
|
57
35
|
export function renderHeatmap(buckets, today, options) {
|
|
58
36
|
const useColor = options?.color === true;
|
|
59
37
|
const trans = t();
|
|
60
|
-
const cellWidth = options?.cols !== undefined
|
|
61
|
-
&& options.cols < GRID_PREFIX_WIDTH + MAX_WEEK_COLUMNS * 2 ? 1 : 2;
|
|
38
|
+
const cellWidth = options?.cols !== undefined && options.cols < GRID_PREFIX_WIDTH + MAX_WEEK_COLUMNS * 2 ? 1 : 2;
|
|
62
39
|
const availableColumns = options?.cols === undefined
|
|
63
40
|
? MAX_WEEK_COLUMNS
|
|
64
41
|
: Math.floor((options.cols - GRID_PREFIX_WIDTH) / cellWidth);
|
|
65
42
|
const numCols = Math.max(1, Math.min(MAX_WEEK_COLUMNS, availableColumns));
|
|
66
|
-
// Build a lookup map: 'YYYY-MM-DD' -> count
|
|
67
43
|
const countByDate = new Map();
|
|
68
44
|
for (const b of buckets) {
|
|
69
45
|
countByDate.set(b.date, b.count);
|
|
70
46
|
}
|
|
71
47
|
const todayProxy = new Date(Date.UTC(today.getFullYear(), today.getMonth(), today.getDate()));
|
|
72
|
-
// Find the Monday that starts the week containing today
|
|
73
48
|
const todayMonIndex = utcDayToMonIndex(todayProxy.getUTCDay());
|
|
74
49
|
const gridEndMs = todayProxy.getTime() + (6 - todayMonIndex) * 86400000; // Sunday of today's week
|
|
75
50
|
const gridStartMs = gridEndMs - (numCols * 7 - 1) * 86400000;
|
|
@@ -96,7 +71,11 @@ export function renderHeatmap(buckets, today, options) {
|
|
|
96
71
|
columns.push(column);
|
|
97
72
|
}
|
|
98
73
|
const weekdayLabel = space.indent; // matches the grid rows' "Mo " prefix width
|
|
99
|
-
const
|
|
74
|
+
const language = getCurrentLanguage();
|
|
75
|
+
const monthFmt = new Intl.DateTimeFormat(language === 'zh' ? 'zh-CN' : 'en-US', {
|
|
76
|
+
month: 'short',
|
|
77
|
+
timeZone: 'UTC',
|
|
78
|
+
});
|
|
100
79
|
const cellsWidth = numCols * cellWidth;
|
|
101
80
|
const monthChars = new Array(cellsWidth).fill(' ');
|
|
102
81
|
let prevMonth = -1;
|
|
@@ -122,24 +101,22 @@ export function renderHeatmap(buckets, today, options) {
|
|
|
122
101
|
}
|
|
123
102
|
}
|
|
124
103
|
const monthLabelLine = space.indent + weekdayLabel + monthChars.join('');
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
104
|
+
const weekdayNames = [
|
|
105
|
+
trans.timetable.weekdayMon.slice(0, 2),
|
|
106
|
+
' ',
|
|
107
|
+
trans.timetable.weekdayWed.slice(0, 2),
|
|
108
|
+
' ',
|
|
109
|
+
trans.timetable.weekdayFri.slice(0, 2),
|
|
110
|
+
' ',
|
|
111
|
+
' ',
|
|
112
|
+
];
|
|
131
113
|
const lines = [];
|
|
132
|
-
// Title line — same space.indent + type.heading treatment every other
|
|
133
|
-
// section heading in the app uses (this one was bare, so it sat flush
|
|
134
|
-
// against the terminal edge instead of matching the app's 3-space margin).
|
|
135
114
|
lines.push(space.indent + type.heading(trans.calendar.heatmap.title));
|
|
136
115
|
lines.push('');
|
|
137
|
-
// Month labels line
|
|
138
116
|
lines.push(monthLabelLine);
|
|
139
|
-
// Grid rows (7 rows: Mon..Sun)
|
|
140
117
|
for (let row = 0; row < 7; row++) {
|
|
141
118
|
const wdLabel = weekdayNames[row] ?? ' ';
|
|
142
|
-
const cells = columns.map(col => {
|
|
119
|
+
const cells = columns.map((col) => {
|
|
143
120
|
const cell = col[row];
|
|
144
121
|
if (cell === null || cell === undefined)
|
|
145
122
|
return ' ';
|
|
@@ -148,7 +125,6 @@ export function renderHeatmap(buckets, today, options) {
|
|
|
148
125
|
});
|
|
149
126
|
lines.push(`${space.indent}${wdLabel} ${cells.join(cellWidth === 2 ? ' ' : '')}`);
|
|
150
127
|
}
|
|
151
|
-
// Legend line
|
|
152
128
|
const legendGlyphs = [
|
|
153
129
|
pickIcon('·', ' '),
|
|
154
130
|
pickIcon('░', '.'),
|
|
@@ -16,8 +16,7 @@ export function filterEvents(events, query) {
|
|
|
16
16
|
const q = query.trim().toLowerCase();
|
|
17
17
|
if (!q)
|
|
18
18
|
return events;
|
|
19
|
-
return events.filter((e) => (e.title ?? '').toLowerCase().includes(q) ||
|
|
20
|
-
(e.location ?? '').toLowerCase().includes(q));
|
|
19
|
+
return events.filter((e) => (e.title ?? '').toLowerCase().includes(q) || (e.location ?? '').toLowerCase().includes(q));
|
|
21
20
|
}
|
|
22
21
|
export function countdownParts(target, now) {
|
|
23
22
|
const ms = target.getTime() - now.getTime();
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { getStateDir, getWritableStateDir } from '../config/paths.js';
|
|
4
|
+
const FEED_FILE = 'calendar-feed.ics';
|
|
5
|
+
const MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000;
|
|
6
|
+
export function saveFeedCache(text, dir) {
|
|
7
|
+
try {
|
|
8
|
+
fs.writeFileSync(path.join(dir ?? getWritableStateDir(), FEED_FILE), text, {
|
|
9
|
+
encoding: 'utf8',
|
|
10
|
+
mode: 0o600,
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
/* best effort */
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
export function loadFeedCache(dir, maxAgeMs = MAX_AGE_MS) {
|
|
18
|
+
try {
|
|
19
|
+
const file = path.join(dir ?? getStateDir(), FEED_FILE);
|
|
20
|
+
if (Date.now() - fs.statSync(file).mtimeMs > maxAgeMs)
|
|
21
|
+
return null;
|
|
22
|
+
return fs.readFileSync(file, 'utf8');
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -1,16 +1,12 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { fetchFeed, parseCalendar, createCalendar, 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, wrapAnsiWithIndent, wrapAnsiToVisualWidth, } from '../core/text.js';
|
|
9
6
|
import { t } from '../i18n/index.js';
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
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';
|
|
9
|
+
import { loadFeedCache, saveFeedCache } from './calendar-store.js';
|
|
14
10
|
import { writeFileSync, existsSync } from 'fs';
|
|
15
11
|
import { join } from 'path';
|
|
16
12
|
function formatDate(date) {
|
|
@@ -27,25 +23,54 @@ function formatTime(date) {
|
|
|
27
23
|
const minutes = String(date.getMinutes()).padStart(2, '0');
|
|
28
24
|
return `${hours}:${minutes}`;
|
|
29
25
|
}
|
|
30
|
-
|
|
26
|
+
const MEMO_TTL_MS = 5 * 60 * 1000;
|
|
27
|
+
let memo;
|
|
28
|
+
let inFlight;
|
|
29
|
+
export function peekCalendar() {
|
|
30
|
+
if (memo)
|
|
31
|
+
return memo.calendar;
|
|
32
|
+
const text = loadFeedCache();
|
|
33
|
+
if (text === null)
|
|
34
|
+
return undefined;
|
|
31
35
|
try {
|
|
32
|
-
|
|
36
|
+
memo = { calendar: createCalendar(parseCalendar(text)), fetchedAt: 0 };
|
|
37
|
+
return memo.calendar;
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
async function refetchCalendar(signal) {
|
|
44
|
+
try {
|
|
45
|
+
const text = await fetchFeed(undefined, {
|
|
46
|
+
timeoutMs: 15000,
|
|
47
|
+
...(signal === undefined ? {} : { signal }),
|
|
48
|
+
});
|
|
49
|
+
memo = { calendar: createCalendar(parseCalendar(text)), fetchedAt: Date.now() };
|
|
50
|
+
saveFeedCache(text);
|
|
51
|
+
return memo.calendar;
|
|
33
52
|
}
|
|
34
53
|
catch (err) {
|
|
35
|
-
const detail = err instanceof FeedFetchError || err instanceof FeedParseError
|
|
36
|
-
? err.message
|
|
37
|
-
: String(err);
|
|
54
|
+
const detail = sanitizeTerminalLine(err instanceof FeedFetchError || err instanceof FeedParseError ? err.message : String(err));
|
|
38
55
|
throw new Error(`${t().calendar.error}: ${detail}`);
|
|
39
56
|
}
|
|
40
57
|
}
|
|
58
|
+
export async function loadCalendarOrThrow(signal) {
|
|
59
|
+
if (memo && Date.now() - memo.fetchedAt < MEMO_TTL_MS)
|
|
60
|
+
return memo.calendar;
|
|
61
|
+
inFlight ??= refetchCalendar(signal).finally(() => {
|
|
62
|
+
inFlight = undefined;
|
|
63
|
+
});
|
|
64
|
+
return inFlight;
|
|
65
|
+
}
|
|
41
66
|
export function toDisplayEvent(e) {
|
|
42
67
|
const trans = t();
|
|
43
68
|
return {
|
|
44
69
|
date: formatDate(e.start),
|
|
45
70
|
time: e.isAllDay ? '' : formatTime(e.start),
|
|
46
|
-
title: e.title ?? trans.calendar.untitledEvent,
|
|
47
|
-
location: e.location ?? trans.calendar.tbdLocation,
|
|
48
|
-
description: e.description ?? '',
|
|
71
|
+
title: sanitizeTerminalLine(e.title ?? trans.calendar.untitledEvent),
|
|
72
|
+
location: sanitizeTerminalLine(e.location ?? trans.calendar.tbdLocation),
|
|
73
|
+
description: sanitizeTerminalText(e.description ?? ''),
|
|
49
74
|
startDate: e.start,
|
|
50
75
|
recurring: e.recurring,
|
|
51
76
|
uid: e.uid,
|
|
@@ -59,7 +84,7 @@ export async function fetchInRange(start, end) {
|
|
|
59
84
|
}
|
|
60
85
|
export async function fetchHeatmapBuckets() {
|
|
61
86
|
const now = new Date();
|
|
62
|
-
const start =
|
|
87
|
+
const start = addLocalDays(now, -365);
|
|
63
88
|
return (await loadCalendarOrThrow()).heatmap({ start, end: now, bucket: 'day' });
|
|
64
89
|
}
|
|
65
90
|
export function serializeEvents(events) {
|
|
@@ -77,14 +102,30 @@ export function serializeEvents(events) {
|
|
|
77
102
|
export function renderEventsTable(events, options) {
|
|
78
103
|
const trans = t();
|
|
79
104
|
const useColor = options?.color !== false;
|
|
80
|
-
|
|
81
|
-
|
|
105
|
+
const width = options?.width === undefined || !Number.isFinite(options.width)
|
|
106
|
+
? Number.POSITIVE_INFINITY
|
|
107
|
+
: Math.max(1, Math.floor(options.width));
|
|
108
|
+
if (events.length === 0) {
|
|
109
|
+
return wrapAnsiWithIndent(type.hint(trans.calendar.noEvents), width, space.indent).join('\n');
|
|
110
|
+
}
|
|
82
111
|
const id = (s) => s;
|
|
83
112
|
const applyDim = useColor ? chalk.dim : id;
|
|
84
113
|
const applyCyan = useColor ? chalk.cyan : id;
|
|
85
114
|
const applyBold = useColor ? chalk.bold : id;
|
|
86
115
|
const applyGray = useColor ? chalk.gray : id;
|
|
87
|
-
|
|
116
|
+
if (width < 68) {
|
|
117
|
+
const lines = [];
|
|
118
|
+
for (const event of events) {
|
|
119
|
+
if (lines.length > 0)
|
|
120
|
+
lines.push('');
|
|
121
|
+
const dateTime = event.time ? `${event.date} ${event.time}` : event.date;
|
|
122
|
+
const marker = event.recurring ? `${pickIcon('↻', '~')} ` : '';
|
|
123
|
+
lines.push(...wrapAnsiWithIndent(applyCyan(dateTime), width, space.indent));
|
|
124
|
+
lines.push(...wrapAnsiWithIndent(applyBold(`${marker}${event.title}`), width, space.indent));
|
|
125
|
+
lines.push(...wrapAnsiWithIndent(applyGray(`${pickIcon('⌖', '@')} ${event.location}`), width, space.indent));
|
|
126
|
+
}
|
|
127
|
+
return lines.join('\n');
|
|
128
|
+
}
|
|
88
129
|
const dateWidth = 16;
|
|
89
130
|
const titleWidth = 32;
|
|
90
131
|
const locWidth = 14;
|
|
@@ -92,12 +133,8 @@ export function renderEventsTable(events, options) {
|
|
|
92
133
|
const headerDate = padEndV(applyDim(trans.calendar.dateTime), dateWidth);
|
|
93
134
|
const headerTitle = padEndV(applyDim(trans.calendar.eventName), titleWidth);
|
|
94
135
|
const headerLoc = applyDim(trans.calendar.location);
|
|
95
|
-
// divider covers exactly: dateWidth + 2-char sep + titleWidth + 2-char sep + locWidth
|
|
96
136
|
const divider = applyDim(sep.repeat(dateWidth + 2 + titleWidth + 2 + locWidth));
|
|
97
|
-
const lines = [
|
|
98
|
-
` ${headerDate} ${headerTitle} ${headerLoc}`,
|
|
99
|
-
` ${divider}`,
|
|
100
|
-
];
|
|
137
|
+
const lines = [` ${headerDate} ${headerTitle} ${headerLoc}`, ` ${divider}`];
|
|
101
138
|
for (const event of events) {
|
|
102
139
|
const dateTime = event.time ? `${event.date} ${event.time}` : event.date;
|
|
103
140
|
const dateCol = padEndV(applyCyan(dateTime), dateWidth);
|
|
@@ -109,22 +146,12 @@ export function renderEventsTable(events, options) {
|
|
|
109
146
|
}
|
|
110
147
|
return lines.join('\n');
|
|
111
148
|
}
|
|
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
149
|
export function renderEventBrief(e, now) {
|
|
119
150
|
const dot = pickIcon('·', '-');
|
|
120
|
-
const isToday = e.startDate.getFullYear() === now.getFullYear()
|
|
121
|
-
|
|
122
|
-
|
|
151
|
+
const isToday = e.startDate.getFullYear() === now.getFullYear() &&
|
|
152
|
+
e.startDate.getMonth() === now.getMonth() &&
|
|
153
|
+
e.startDate.getDate() === now.getDate();
|
|
123
154
|
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
155
|
const marker = isToday ? type.active(pickIcon('●', '*')) : type.hint(pickIcon('·', '-'));
|
|
129
156
|
const dateStyled = isToday ? type.active(dateTime) : type.hint(dateTime);
|
|
130
157
|
const titleStyled = isToday ? type.active(e.title) : type.body(e.title);
|
|
@@ -157,157 +184,6 @@ export function renderCountdownBanner(event, now, cols = Number.POSITIVE_INFINIT
|
|
|
157
184
|
.map((line, index) => `${index === 0 ? prefix : continuation}${line}`)
|
|
158
185
|
.join('\n');
|
|
159
186
|
}
|
|
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
187
|
export function exportEventIcs(event, dir = process.cwd()) {
|
|
312
188
|
const base = buildExportFilename(event);
|
|
313
189
|
let path = join(dir, base);
|