@nbtca/prompt 1.5.0 → 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/dist/app/app.js +115 -10
- package/dist/app/frame.js +14 -2
- package/dist/app/keys.js +100 -0
- package/dist/app/views/docs-render.js +3 -2
- package/dist/app/views/docs.js +85 -24
- package/dist/app/views/events-render.js +2 -1
- package/dist/app/views/events.js +13 -2
- package/dist/app/views/home.js +46 -24
- package/dist/app/views/schedule-render.js +4 -3
- package/dist/app/views/schedule.js +101 -33
- package/dist/cli.js +570 -0
- package/dist/config/preferences.js +7 -0
- package/dist/core/canvas.js +1 -0
- package/dist/core/components/menu.js +23 -22
- package/dist/core/components/spinner.js +17 -1
- package/dist/core/text.js +7 -3
- package/dist/core/vim-keys.js +149 -6
- package/dist/features/calendar-store.js +27 -0
- package/dist/features/calendar.js +55 -6
- package/dist/features/docs-client.js +225 -0
- package/dist/features/docs.js +224 -68
- package/dist/features/links.js +51 -0
- package/dist/features/status.js +83 -14
- package/dist/features/student-timetable.js +1 -2
- package/dist/features/theme.js +3 -3
- package/dist/features/update.js +3 -2
- package/dist/i18n/locales/en.json +7 -2
- package/dist/i18n/locales/zh.json +7 -2
- package/dist/index.js +5 -498
- package/package.json +2 -1
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
import { glyph, type, space } from '../theme.js';
|
|
2
2
|
import { visualWidth, padEndV, wrapAnsiToVisualWidth } from '../text.js';
|
|
3
|
-
import { ansi, ensureCursorRestored } from '../canvas.js';
|
|
4
3
|
import { createPainter } from './painter.js';
|
|
4
|
+
import { startRawInput } from './input-session.js';
|
|
5
5
|
import { t } from '../../i18n/index.js';
|
|
6
6
|
export function parseKey(data) {
|
|
7
7
|
const s = data.toString();
|
|
8
8
|
switch (s) {
|
|
9
9
|
case '\x1b[A':
|
|
10
|
+
case 'k':
|
|
10
11
|
return 'up';
|
|
11
12
|
case '\x1b[B':
|
|
13
|
+
case 'j':
|
|
12
14
|
return 'down';
|
|
13
15
|
case '\x1b[5~':
|
|
14
16
|
return 'pageUp';
|
|
@@ -17,16 +19,20 @@ export function parseKey(data) {
|
|
|
17
19
|
case '\x1b[H':
|
|
18
20
|
case '\x1b[1~':
|
|
19
21
|
case '\x1bOH':
|
|
22
|
+
case 'g':
|
|
20
23
|
return 'home';
|
|
21
24
|
case '\x1b[F':
|
|
22
25
|
case '\x1b[4~':
|
|
23
26
|
case '\x1bOF':
|
|
27
|
+
case 'G':
|
|
24
28
|
return 'end';
|
|
25
29
|
case '\r':
|
|
26
30
|
case '\n':
|
|
31
|
+
case 'l':
|
|
27
32
|
return 'enter';
|
|
28
33
|
case '\x03':
|
|
29
34
|
case '\x1b':
|
|
35
|
+
case 'q':
|
|
30
36
|
return 'cancel';
|
|
31
37
|
default:
|
|
32
38
|
return 'none';
|
|
@@ -97,38 +103,32 @@ export function menuFooter() {
|
|
|
97
103
|
const m = t().menu;
|
|
98
104
|
return `${glyph.updown()} ${m.hintMove} ${glyph.enter()} ${m.hintOpen} q ${m.hintQuit}`;
|
|
99
105
|
}
|
|
100
|
-
// Note: runMenu relies on ambient vim-key translation (j/k/l/g/G/q) being ACTIVE.
|
|
101
|
-
// Callers must not invoke it with setVimKeysActive(false) still in effect.
|
|
102
106
|
export function runMenu(config) {
|
|
103
107
|
return new Promise((resolve) => {
|
|
104
|
-
const stdin = process.stdin;
|
|
105
|
-
if (!stdin.isTTY || !process.stdout.isTTY) {
|
|
106
|
-
resolve(null);
|
|
107
|
-
return;
|
|
108
|
-
}
|
|
109
108
|
let index = config.initialIndex ?? 0;
|
|
109
|
+
let finished = false;
|
|
110
110
|
const paint = createPainter(() => renderMenu({
|
|
111
111
|
title: config.title,
|
|
112
112
|
options: config.options,
|
|
113
113
|
selectedIndex: index,
|
|
114
114
|
...(config.footer === undefined ? {} : { footer: config.footer }),
|
|
115
115
|
}));
|
|
116
|
-
const
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
116
|
+
const finish = (result) => {
|
|
117
|
+
if (finished)
|
|
118
|
+
return;
|
|
119
|
+
finished = true;
|
|
120
|
+
handle?.stop();
|
|
121
|
+
process.stdout.write('\n');
|
|
122
|
+
resolve(result);
|
|
121
123
|
};
|
|
122
124
|
const onData = (data) => {
|
|
123
125
|
const key = parseKey(data);
|
|
124
126
|
if (key === 'cancel') {
|
|
125
|
-
|
|
126
|
-
resolve(null);
|
|
127
|
+
finish(null);
|
|
127
128
|
return;
|
|
128
129
|
}
|
|
129
130
|
if (key === 'enter') {
|
|
130
|
-
|
|
131
|
-
resolve(config.options[index]?.value ?? null);
|
|
131
|
+
finish(config.options[index]?.value ?? null);
|
|
132
132
|
return;
|
|
133
133
|
}
|
|
134
134
|
const next = nextIndex(index, key, config.options.length);
|
|
@@ -137,11 +137,12 @@ export function runMenu(config) {
|
|
|
137
137
|
paint();
|
|
138
138
|
}
|
|
139
139
|
};
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
140
|
+
const handle = startRawInput(onData);
|
|
141
|
+
if (!handle) {
|
|
142
|
+
finished = true;
|
|
143
|
+
resolve(null);
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
144
146
|
paint();
|
|
145
|
-
stdin.on('data', onData);
|
|
146
147
|
});
|
|
147
148
|
}
|
|
@@ -2,12 +2,28 @@ import { getCapabilities } from '../capabilities.js';
|
|
|
2
2
|
import { ansi, ensureCursorRestored } from '../canvas.js';
|
|
3
3
|
import { renderMessage } from './messages.js';
|
|
4
4
|
import { pickIcon } from '../icons.js';
|
|
5
|
-
import { c, space } from '../theme.js';
|
|
5
|
+
import { c, space, type } from '../theme.js';
|
|
6
|
+
import { visualWidth, wrapAnsiWithIndent } from '../text.js';
|
|
6
7
|
const FRAMES_UNICODE = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
7
8
|
const FRAMES_ASCII = ['|', '/', '-', '\\'];
|
|
9
|
+
export const SPINNER_FRAME_MS = 80;
|
|
8
10
|
export function renderSpinnerFrame(frame, msg) {
|
|
9
11
|
return `${space.indent}${c.accent(frame)} ${msg}`;
|
|
10
12
|
}
|
|
13
|
+
export function spinnerFrame(at = Date.now()) {
|
|
14
|
+
const frames = pickIcon('u', 'a') === 'u' ? FRAMES_UNICODE : FRAMES_ASCII;
|
|
15
|
+
const index = getCapabilities().reducedMotion
|
|
16
|
+
? 0
|
|
17
|
+
: Math.floor(at / SPINNER_FRAME_MS) % frames.length;
|
|
18
|
+
return frames[index] ?? '|';
|
|
19
|
+
}
|
|
20
|
+
export function loadingLines(label, cols = Number.POSITIVE_INFINITY, at = Date.now()) {
|
|
21
|
+
const message = type.hint(label);
|
|
22
|
+
const spun = `${space.indent}${c.accent(spinnerFrame(at))} ${message}`;
|
|
23
|
+
if (visualWidth(spun) <= cols)
|
|
24
|
+
return [spun];
|
|
25
|
+
return wrapAnsiWithIndent(message, cols, space.indent);
|
|
26
|
+
}
|
|
11
27
|
export function startSpinner(msg = '', opts = {}) {
|
|
12
28
|
const write = opts.write ??
|
|
13
29
|
((s) => {
|
package/dist/core/text.js
CHANGED
|
@@ -273,9 +273,13 @@ export function wrapAnsiWithIndent(str, maxWidth, preferredIndent = '') {
|
|
|
273
273
|
: Number.POSITIVE_INFINITY;
|
|
274
274
|
const indentWidth = visualWidth(preferredIndent);
|
|
275
275
|
const contentWidth = visualWidth(str);
|
|
276
|
-
|
|
276
|
+
let indent = indentWidth >= width || (contentWidth > width - indentWidth && contentWidth <= width)
|
|
277
277
|
? ''
|
|
278
278
|
: preferredIndent;
|
|
279
|
-
|
|
280
|
-
|
|
279
|
+
let lines = wrapAnsiToVisualWidth(str, Math.max(1, width - visualWidth(indent)));
|
|
280
|
+
if (indent && lines.some((line) => visualWidth(indent + line) > width)) {
|
|
281
|
+
indent = '';
|
|
282
|
+
lines = wrapAnsiToVisualWidth(str, width);
|
|
283
|
+
}
|
|
284
|
+
return lines.map((line) => `${indent}${line}`);
|
|
281
285
|
}
|
package/dist/core/vim-keys.js
CHANGED
|
@@ -4,24 +4,167 @@ const VIM_TO_SEQ = {
|
|
|
4
4
|
l: Buffer.from('\r'),
|
|
5
5
|
g: Buffer.from('\u001b[H'),
|
|
6
6
|
G: Buffer.from('\u001b[F'),
|
|
7
|
-
q: Buffer.from('\u0003'),
|
|
8
7
|
};
|
|
9
8
|
let vimActive = true;
|
|
10
9
|
export function setVimKeysActive(active) {
|
|
11
10
|
vimActive = active;
|
|
12
11
|
}
|
|
12
|
+
function vimKeysAreActive() {
|
|
13
|
+
return vimActive;
|
|
14
|
+
}
|
|
15
|
+
function utf8Length(chunk, start) {
|
|
16
|
+
const first = chunk[start] ?? 0;
|
|
17
|
+
const length = first <= 0x7f
|
|
18
|
+
? 1
|
|
19
|
+
: first >= 0xc2 && first <= 0xdf
|
|
20
|
+
? 2
|
|
21
|
+
: first >= 0xe0 && first <= 0xef
|
|
22
|
+
? 3
|
|
23
|
+
: first >= 0xf0 && first <= 0xf4
|
|
24
|
+
? 4
|
|
25
|
+
: 1;
|
|
26
|
+
if (start + length > chunk.length)
|
|
27
|
+
return null;
|
|
28
|
+
for (let index = start + 1; index < start + length; index += 1) {
|
|
29
|
+
const byte = chunk[index] ?? 0;
|
|
30
|
+
if (byte < 0x80 || byte > 0xbf)
|
|
31
|
+
return 1;
|
|
32
|
+
}
|
|
33
|
+
return length;
|
|
34
|
+
}
|
|
35
|
+
function escapeLength(chunk, start) {
|
|
36
|
+
const introducer = chunk[start + 1];
|
|
37
|
+
if (introducer === undefined)
|
|
38
|
+
return null;
|
|
39
|
+
if (introducer === 0x1b || introducer <= 0x1f || introducer === 0x7f)
|
|
40
|
+
return 1;
|
|
41
|
+
if (introducer === 0x5b || introducer === 0x4f) {
|
|
42
|
+
for (let index = start + 2; index < chunk.length; index += 1) {
|
|
43
|
+
const byte = chunk[index] ?? 0;
|
|
44
|
+
if (byte >= 0x40 && byte <= 0x7e)
|
|
45
|
+
return index - start + 1;
|
|
46
|
+
if (byte < 0x20 || byte > 0x3f)
|
|
47
|
+
return 1;
|
|
48
|
+
}
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
if ([0x5d, 0x50, 0x5e, 0x5f].includes(introducer)) {
|
|
52
|
+
for (let index = start + 2; index < chunk.length; index += 1) {
|
|
53
|
+
if (chunk[index] === 0x07)
|
|
54
|
+
return index - start + 1;
|
|
55
|
+
if (chunk[index] === 0x1b && chunk[index + 1] === 0x5c)
|
|
56
|
+
return index - start + 2;
|
|
57
|
+
}
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
const length = utf8Length(chunk, start + 1);
|
|
61
|
+
return length === null ? null : length + 1;
|
|
62
|
+
}
|
|
63
|
+
function keyLength(chunk, start) {
|
|
64
|
+
return chunk[start] === 0x1b ? escapeLength(chunk, start) : utf8Length(chunk, start);
|
|
65
|
+
}
|
|
66
|
+
function concatBuffers(chunks) {
|
|
67
|
+
const size = chunks.reduce((total, chunk) => total + chunk.length, 0);
|
|
68
|
+
const result = Buffer.allocUnsafe(size);
|
|
69
|
+
let offset = 0;
|
|
70
|
+
for (const chunk of chunks) {
|
|
71
|
+
for (const byte of chunk) {
|
|
72
|
+
result[offset] = byte;
|
|
73
|
+
offset += 1;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return result;
|
|
77
|
+
}
|
|
78
|
+
class KeyByteFramer {
|
|
79
|
+
pending = Buffer.alloc(0);
|
|
80
|
+
get hasPending() {
|
|
81
|
+
return this.pending.length > 0;
|
|
82
|
+
}
|
|
83
|
+
write(chunk) {
|
|
84
|
+
this.pending = this.pending.length === 0 ? chunk : concatBuffers([this.pending, chunk]);
|
|
85
|
+
return this.drain(false);
|
|
86
|
+
}
|
|
87
|
+
flush() {
|
|
88
|
+
return this.drain(true);
|
|
89
|
+
}
|
|
90
|
+
takePending() {
|
|
91
|
+
const pending = this.pending;
|
|
92
|
+
this.pending = Buffer.alloc(0);
|
|
93
|
+
return pending;
|
|
94
|
+
}
|
|
95
|
+
drain(flush) {
|
|
96
|
+
const keys = [];
|
|
97
|
+
let offset = 0;
|
|
98
|
+
while (offset < this.pending.length) {
|
|
99
|
+
const length = keyLength(this.pending, offset);
|
|
100
|
+
if (length === null) {
|
|
101
|
+
if (flush) {
|
|
102
|
+
keys.push(this.pending.subarray(offset));
|
|
103
|
+
offset = this.pending.length;
|
|
104
|
+
}
|
|
105
|
+
break;
|
|
106
|
+
}
|
|
107
|
+
keys.push(this.pending.subarray(offset, offset + length));
|
|
108
|
+
offset += length;
|
|
109
|
+
}
|
|
110
|
+
this.pending = this.pending.subarray(offset);
|
|
111
|
+
return keys;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
13
114
|
export function enableVimKeys() {
|
|
14
115
|
const stdin = process.stdin;
|
|
15
116
|
if (!stdin.isTTY)
|
|
16
117
|
return;
|
|
17
118
|
const originalEmit = stdin.emit.bind(stdin);
|
|
119
|
+
const framer = new KeyByteFramer();
|
|
120
|
+
let flushTimer;
|
|
121
|
+
const clearFlush = () => {
|
|
122
|
+
if (flushTimer === undefined)
|
|
123
|
+
return;
|
|
124
|
+
clearTimeout(flushTimer);
|
|
125
|
+
flushTimer = undefined;
|
|
126
|
+
};
|
|
127
|
+
const emitKeys = (keys) => {
|
|
128
|
+
let emitted = false;
|
|
129
|
+
for (let index = 0; index < keys.length; index += 1) {
|
|
130
|
+
if (!vimKeysAreActive()) {
|
|
131
|
+
emitted = originalEmit('data', concatBuffers(keys.slice(index))) || emitted;
|
|
132
|
+
return emitted;
|
|
133
|
+
}
|
|
134
|
+
const key = keys[index];
|
|
135
|
+
if (key === undefined)
|
|
136
|
+
continue;
|
|
137
|
+
const sequence = key.length === 1 ? VIM_TO_SEQ[String.fromCharCode(key[0] ?? 0)] : undefined;
|
|
138
|
+
emitted = originalEmit('data', sequence ?? key) || emitted;
|
|
139
|
+
}
|
|
140
|
+
return emitted;
|
|
141
|
+
};
|
|
142
|
+
const scheduleFlush = () => {
|
|
143
|
+
if (!framer.hasPending)
|
|
144
|
+
return;
|
|
145
|
+
flushTimer = setTimeout(() => {
|
|
146
|
+
flushTimer = undefined;
|
|
147
|
+
if (!vimKeysAreActive()) {
|
|
148
|
+
originalEmit('data', framer.takePending());
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
emitKeys(framer.flush());
|
|
152
|
+
}, 20);
|
|
153
|
+
};
|
|
18
154
|
const translatedEmit = (event, ...args) => {
|
|
19
|
-
if (event === 'data'
|
|
155
|
+
if (event === 'data') {
|
|
20
156
|
const chunk = args[0];
|
|
21
|
-
if (Buffer.isBuffer(chunk)
|
|
22
|
-
|
|
23
|
-
if (
|
|
24
|
-
|
|
157
|
+
if (Buffer.isBuffer(chunk)) {
|
|
158
|
+
clearFlush();
|
|
159
|
+
if (!vimKeysAreActive()) {
|
|
160
|
+
const pending = framer.takePending();
|
|
161
|
+
return pending.length === 0
|
|
162
|
+
? originalEmit(event, ...args)
|
|
163
|
+
: originalEmit('data', concatBuffers([pending, chunk]));
|
|
164
|
+
}
|
|
165
|
+
const emitted = emitKeys(framer.write(chunk));
|
|
166
|
+
scheduleFlush();
|
|
167
|
+
return emitted;
|
|
25
168
|
}
|
|
26
169
|
}
|
|
27
170
|
return originalEmit(event, ...args);
|
|
@@ -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,11 +1,12 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { fetchFeed, parseCalendar, createCalendar, FeedFetchError, FeedParseError, eventToICS, } from '@nbtca/nbtcal';
|
|
2
2
|
import chalk from 'chalk';
|
|
3
3
|
import { c, type, space, glyph } from '../core/theme.js';
|
|
4
4
|
import { pickIcon } from '../core/icons.js';
|
|
5
|
-
import { padEndV, sanitizeTerminalLine, sanitizeTerminalText, truncate, visualWidth, wrapAnsiToVisualWidth, } from '../core/text.js';
|
|
5
|
+
import { padEndV, sanitizeTerminalLine, sanitizeTerminalText, truncate, visualWidth, wrapAnsiWithIndent, wrapAnsiToVisualWidth, } from '../core/text.js';
|
|
6
6
|
import { t } from '../i18n/index.js';
|
|
7
7
|
import { addLocalDays } from '../core/calendar-day.js';
|
|
8
8
|
import { countdownParts, isCountdownUrgent, buildExportFilename } from './calendar-query.js';
|
|
9
|
+
import { loadFeedCache, saveFeedCache } from './calendar-store.js';
|
|
9
10
|
import { writeFileSync, existsSync } from 'fs';
|
|
10
11
|
import { join } from 'path';
|
|
11
12
|
function formatDate(date) {
|
|
@@ -22,15 +23,46 @@ function formatTime(date) {
|
|
|
22
23
|
const minutes = String(date.getMinutes()).padStart(2, '0');
|
|
23
24
|
return `${hours}:${minutes}`;
|
|
24
25
|
}
|
|
25
|
-
|
|
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;
|
|
26
35
|
try {
|
|
27
|
-
|
|
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;
|
|
28
52
|
}
|
|
29
53
|
catch (err) {
|
|
30
54
|
const detail = sanitizeTerminalLine(err instanceof FeedFetchError || err instanceof FeedParseError ? err.message : String(err));
|
|
31
55
|
throw new Error(`${t().calendar.error}: ${detail}`);
|
|
32
56
|
}
|
|
33
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
|
+
}
|
|
34
66
|
export function toDisplayEvent(e) {
|
|
35
67
|
const trans = t();
|
|
36
68
|
return {
|
|
@@ -70,13 +102,30 @@ export function serializeEvents(events) {
|
|
|
70
102
|
export function renderEventsTable(events, options) {
|
|
71
103
|
const trans = t();
|
|
72
104
|
const useColor = options?.color !== false;
|
|
73
|
-
|
|
74
|
-
|
|
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
|
+
}
|
|
75
111
|
const id = (s) => s;
|
|
76
112
|
const applyDim = useColor ? chalk.dim : id;
|
|
77
113
|
const applyCyan = useColor ? chalk.cyan : id;
|
|
78
114
|
const applyBold = useColor ? chalk.bold : id;
|
|
79
115
|
const applyGray = useColor ? chalk.gray : id;
|
|
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
|
+
}
|
|
80
129
|
const dateWidth = 16;
|
|
81
130
|
const titleWidth = 32;
|
|
82
131
|
const locWidth = 14;
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
2
|
+
import { createDocsClient } from '@nbtca/docs';
|
|
3
|
+
const contextKey = Symbol.for('@nbtca/prompt/docs-fetch-context/v2');
|
|
4
|
+
const globalWithContext = globalThis;
|
|
5
|
+
function abortError(signal) {
|
|
6
|
+
return signal.reason instanceof Error ? signal.reason : new DOMException('Aborted', 'AbortError');
|
|
7
|
+
}
|
|
8
|
+
function raceWithSignal(request, signal) {
|
|
9
|
+
if (!signal)
|
|
10
|
+
return request;
|
|
11
|
+
if (signal.aborted)
|
|
12
|
+
return Promise.reject(abortError(signal));
|
|
13
|
+
return new Promise((resolve, reject) => {
|
|
14
|
+
const finish = (result) => {
|
|
15
|
+
signal.removeEventListener('abort', onAbort);
|
|
16
|
+
result();
|
|
17
|
+
};
|
|
18
|
+
const onAbort = () => {
|
|
19
|
+
finish(() => {
|
|
20
|
+
reject(abortError(signal));
|
|
21
|
+
});
|
|
22
|
+
};
|
|
23
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
24
|
+
void request.then((value) => {
|
|
25
|
+
finish(() => {
|
|
26
|
+
resolve(value);
|
|
27
|
+
});
|
|
28
|
+
}, (error) => {
|
|
29
|
+
finish(() => {
|
|
30
|
+
reject(error instanceof Error ? error : new Error(String(error)));
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
function createDocsFetchContext() {
|
|
36
|
+
const installed = globalWithContext[contextKey];
|
|
37
|
+
if (installed)
|
|
38
|
+
return installed;
|
|
39
|
+
const nativeFetch = globalThis.fetch;
|
|
40
|
+
const storage = new AsyncLocalStorage();
|
|
41
|
+
let delegate = nativeFetch;
|
|
42
|
+
let fallbackDispatching = false;
|
|
43
|
+
const contextualFetch = (input, init) => {
|
|
44
|
+
const forward = (delegate, options) => options ? delegate(input, options) : delegate(input);
|
|
45
|
+
const store = storage.getStore();
|
|
46
|
+
if (!store) {
|
|
47
|
+
if (fallbackDispatching)
|
|
48
|
+
return forward(nativeFetch, init);
|
|
49
|
+
fallbackDispatching = true;
|
|
50
|
+
try {
|
|
51
|
+
return forward(delegate, init);
|
|
52
|
+
}
|
|
53
|
+
finally {
|
|
54
|
+
fallbackDispatching = false;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
if (store.dispatching)
|
|
58
|
+
return forward(nativeFetch, init);
|
|
59
|
+
if (store.signal?.aborted)
|
|
60
|
+
return Promise.reject(abortError(store.signal));
|
|
61
|
+
const requestSignal = init?.signal;
|
|
62
|
+
const signal = store.signal
|
|
63
|
+
? requestSignal
|
|
64
|
+
? AbortSignal.any([store.signal, requestSignal])
|
|
65
|
+
: store.signal
|
|
66
|
+
: requestSignal;
|
|
67
|
+
store.dispatching = true;
|
|
68
|
+
try {
|
|
69
|
+
return raceWithSignal(forward(store.delegate, signal ? { ...init, signal } : init), signal);
|
|
70
|
+
}
|
|
71
|
+
finally {
|
|
72
|
+
store.dispatching = false;
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
const getFetch = () => (storage.getStore() ? contextualFetch : delegate);
|
|
76
|
+
const setFetch = (value) => {
|
|
77
|
+
delegate = value;
|
|
78
|
+
};
|
|
79
|
+
const context = {
|
|
80
|
+
active: 0,
|
|
81
|
+
contextualFetch,
|
|
82
|
+
get delegate() {
|
|
83
|
+
return delegate;
|
|
84
|
+
},
|
|
85
|
+
set delegate(value) {
|
|
86
|
+
delegate = value;
|
|
87
|
+
},
|
|
88
|
+
getFetch,
|
|
89
|
+
nativeFetch,
|
|
90
|
+
restoreDescriptor: undefined,
|
|
91
|
+
setFetch,
|
|
92
|
+
storage,
|
|
93
|
+
};
|
|
94
|
+
globalWithContext[contextKey] = context;
|
|
95
|
+
return context;
|
|
96
|
+
}
|
|
97
|
+
function beginDocsFetch() {
|
|
98
|
+
const context = createDocsFetchContext();
|
|
99
|
+
const descriptor = Object.getOwnPropertyDescriptor(globalThis, 'fetch');
|
|
100
|
+
const installedAccessor = descriptor?.get === context.getFetch && descriptor.set === context.setFetch;
|
|
101
|
+
const installedValue = descriptor?.value === context.contextualFetch;
|
|
102
|
+
if (context.active === 0)
|
|
103
|
+
context.restoreDescriptor = descriptor;
|
|
104
|
+
if (!installedAccessor && !installedValue) {
|
|
105
|
+
const observed = globalThis.fetch;
|
|
106
|
+
if (observed !== context.contextualFetch)
|
|
107
|
+
context.delegate = observed;
|
|
108
|
+
if (descriptor?.configurable === false) {
|
|
109
|
+
if (descriptor.writable !== true) {
|
|
110
|
+
return {
|
|
111
|
+
context,
|
|
112
|
+
finish() {
|
|
113
|
+
return undefined;
|
|
114
|
+
},
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
globalThis.fetch = context.contextualFetch;
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
Object.defineProperty(globalThis, 'fetch', {
|
|
121
|
+
configurable: true,
|
|
122
|
+
enumerable: descriptor?.enumerable ?? true,
|
|
123
|
+
get: context.getFetch,
|
|
124
|
+
set: context.setFetch,
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
context.active += 1;
|
|
129
|
+
let finished = false;
|
|
130
|
+
return {
|
|
131
|
+
context,
|
|
132
|
+
finish() {
|
|
133
|
+
if (finished)
|
|
134
|
+
return;
|
|
135
|
+
finished = true;
|
|
136
|
+
context.active = Math.max(0, context.active - 1);
|
|
137
|
+
if (context.active > 0)
|
|
138
|
+
return;
|
|
139
|
+
const current = Object.getOwnPropertyDescriptor(globalThis, 'fetch');
|
|
140
|
+
if (current?.value === context.contextualFetch && current.writable === true) {
|
|
141
|
+
globalThis.fetch = context.delegate;
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
if (current?.get !== context.getFetch || current.set !== context.setFetch)
|
|
145
|
+
return;
|
|
146
|
+
const restore = context.restoreDescriptor;
|
|
147
|
+
if (restore && ('get' in restore || 'set' in restore)) {
|
|
148
|
+
Object.defineProperty(globalThis, 'fetch', restore);
|
|
149
|
+
}
|
|
150
|
+
else {
|
|
151
|
+
Object.defineProperty(globalThis, 'fetch', {
|
|
152
|
+
configurable: restore?.configurable ?? true,
|
|
153
|
+
enumerable: restore?.enumerable ?? true,
|
|
154
|
+
value: context.delegate,
|
|
155
|
+
writable: restore?.writable ?? true,
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
},
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
const defaultClient = createDocsClient();
|
|
162
|
+
const clientsBySignal = new WeakMap();
|
|
163
|
+
const clients = new Set([defaultClient]);
|
|
164
|
+
function clientFor(signal) {
|
|
165
|
+
if (!signal)
|
|
166
|
+
return defaultClient;
|
|
167
|
+
const existing = clientsBySignal.get(signal);
|
|
168
|
+
if (existing)
|
|
169
|
+
return existing;
|
|
170
|
+
const client = createDocsClient();
|
|
171
|
+
clientsBySignal.set(signal, client);
|
|
172
|
+
clients.add(client);
|
|
173
|
+
signal.addEventListener('abort', () => {
|
|
174
|
+
client.clear();
|
|
175
|
+
clients.delete(client);
|
|
176
|
+
clientsBySignal.delete(signal);
|
|
177
|
+
}, { once: true });
|
|
178
|
+
return client;
|
|
179
|
+
}
|
|
180
|
+
export function clearDocsClients() {
|
|
181
|
+
for (const client of clients)
|
|
182
|
+
client.clear();
|
|
183
|
+
}
|
|
184
|
+
export function runDocsClientOperation(signal, operation) {
|
|
185
|
+
if (signal?.aborted)
|
|
186
|
+
return Promise.reject(abortError(signal));
|
|
187
|
+
const scope = beginDocsFetch();
|
|
188
|
+
let request;
|
|
189
|
+
try {
|
|
190
|
+
request = scope.context.storage.run({ delegate: scope.context.delegate, dispatching: false, signal }, () => operation(clientFor(signal)));
|
|
191
|
+
}
|
|
192
|
+
catch (error) {
|
|
193
|
+
scope.finish();
|
|
194
|
+
return Promise.reject(error instanceof Error ? error : new Error(String(error)));
|
|
195
|
+
}
|
|
196
|
+
return new Promise((resolve, reject) => {
|
|
197
|
+
let settled = false;
|
|
198
|
+
const settle = (result) => {
|
|
199
|
+
if (settled)
|
|
200
|
+
return;
|
|
201
|
+
settled = true;
|
|
202
|
+
signal?.removeEventListener('abort', onAbort);
|
|
203
|
+
result();
|
|
204
|
+
};
|
|
205
|
+
const onAbort = () => {
|
|
206
|
+
settle(() => {
|
|
207
|
+
reject(signal ? abortError(signal) : new DOMException('Aborted', 'AbortError'));
|
|
208
|
+
});
|
|
209
|
+
};
|
|
210
|
+
signal?.addEventListener('abort', onAbort, { once: true });
|
|
211
|
+
if (signal?.aborted)
|
|
212
|
+
onAbort();
|
|
213
|
+
void request.then((value) => {
|
|
214
|
+
scope.finish();
|
|
215
|
+
settle(() => {
|
|
216
|
+
resolve(value);
|
|
217
|
+
});
|
|
218
|
+
}, (error) => {
|
|
219
|
+
scope.finish();
|
|
220
|
+
settle(() => {
|
|
221
|
+
reject(error instanceof Error ? error : new Error(String(error)));
|
|
222
|
+
});
|
|
223
|
+
});
|
|
224
|
+
});
|
|
225
|
+
}
|