@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
package/dist/core/text.js
CHANGED
|
@@ -1,95 +1,182 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
const GRAPHEME_SEGMENTER = new Intl.Segmenter(undefined, { granularity: 'grapheme' });
|
|
2
|
+
const MARK_RE = /^\p{Mark}$/u;
|
|
3
|
+
const EMOJI_PRESENTATION_RE = /\p{Emoji_Presentation}/u;
|
|
4
|
+
const REGIONAL_INDICATOR_RE = /\p{Regional_Indicator}/u;
|
|
5
|
+
function graphemes(value) {
|
|
6
|
+
return Array.from(GRAPHEME_SEGMENTER.segment(value), ({ segment }) => segment);
|
|
7
|
+
}
|
|
6
8
|
function isZeroWidth(cp) {
|
|
7
|
-
return cp ===
|
|
8
|
-
|
|
9
|
-
|
|
9
|
+
return (cp === 0x200d || // zero-width joiner
|
|
10
|
+
cp === 0xfe0e || // variation selector-15 (text presentation)
|
|
11
|
+
cp === 0xfe0f); // variation selector-16 (emoji presentation)
|
|
10
12
|
}
|
|
11
|
-
|
|
12
|
-
function charWidth(ch) {
|
|
13
|
+
function codePointWidth(ch) {
|
|
13
14
|
const cp = ch.codePointAt(0) ?? 0;
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
(cp >=
|
|
18
|
-
(cp >=
|
|
19
|
-
(cp >=
|
|
20
|
-
(cp >=
|
|
21
|
-
(cp >=
|
|
22
|
-
(cp >=
|
|
23
|
-
(cp >=
|
|
24
|
-
(cp >=
|
|
25
|
-
(cp >=
|
|
26
|
-
(cp >=
|
|
27
|
-
(cp >=
|
|
28
|
-
|
|
29
|
-
(cp >=
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
15
|
+
if (isZeroWidth(cp) || MARK_RE.test(ch) || cp < 0x20 || (cp >= 0x7f && cp <= 0x9f))
|
|
16
|
+
return 0;
|
|
17
|
+
return (cp >= 0x1100 && cp <= 0x115f) ||
|
|
18
|
+
(cp >= 0x2e80 && cp <= 0x303f) ||
|
|
19
|
+
(cp >= 0x3040 && cp <= 0x33ff) ||
|
|
20
|
+
(cp >= 0x3400 && cp <= 0x4dbf) ||
|
|
21
|
+
(cp >= 0x4e00 && cp <= 0x9fff) ||
|
|
22
|
+
(cp >= 0xac00 && cp <= 0xd7af) ||
|
|
23
|
+
(cp >= 0xf900 && cp <= 0xfaff) ||
|
|
24
|
+
(cp >= 0xfe30 && cp <= 0xfe4f) ||
|
|
25
|
+
(cp >= 0xff00 && cp <= 0xff60) ||
|
|
26
|
+
(cp >= 0xffe0 && cp <= 0xffe6) ||
|
|
27
|
+
(cp >= 0x20000 && cp <= 0x2a6df) ||
|
|
28
|
+
(cp >= 0x2a700 && cp <= 0x2ceaf) ||
|
|
29
|
+
(cp >= 0x2ceb0 && cp <= 0x2ebef) ||
|
|
30
|
+
(cp >= 0x30000 && cp <= 0x323af) ||
|
|
31
|
+
(cp >= 0x1f300 && cp <= 0x1faff)
|
|
32
|
+
? 2
|
|
33
|
+
: 1;
|
|
34
|
+
}
|
|
35
|
+
function graphemeWidth(grapheme) {
|
|
36
|
+
if (grapheme.includes('\ufe0f') ||
|
|
37
|
+
grapheme.includes('\u20e3') ||
|
|
38
|
+
EMOJI_PRESENTATION_RE.test(grapheme) ||
|
|
39
|
+
REGIONAL_INDICATOR_RE.test(grapheme)) {
|
|
40
|
+
return 2;
|
|
41
|
+
}
|
|
42
|
+
let width = 0;
|
|
43
|
+
for (const ch of grapheme)
|
|
44
|
+
width = Math.max(width, codePointWidth(ch));
|
|
45
|
+
return width;
|
|
46
|
+
}
|
|
47
|
+
const TERMINAL_ESCAPE_RE = /(?:\u001B\]|\u009D)[\s\S]*?(?:\u0007|\u001B\\|\u009C|$)|(?:\u001B[P_X^]|[\u0090\u0098\u009E\u009F])[\s\S]*?(?:\u001B\\|\u009C|$)|(?:\u001B\[|\u009B)[0-?]*[ -/]*[@-~]|\u001B[ -/]*[@-~]/g;
|
|
48
|
+
const SGR_RE = /^(?:\u001B\[|\u009B)[0-9;]*m$/;
|
|
49
|
+
const OSC8_RE = /^(?:\u001B\]|\u009D)8;[^;]*;([\s\S]*?)(?:\u0007|\u001B\\|\u009C)$/;
|
|
50
|
+
const OSC8_CLOSE = '\u001B]8;;\u0007';
|
|
34
51
|
export function stripAnsi(str) {
|
|
35
|
-
return str.replace(
|
|
52
|
+
return str.replace(TERMINAL_ESCAPE_RE, '');
|
|
53
|
+
}
|
|
54
|
+
const CONTROL_RE = /[\u0000-\u0008\u000B-\u001F\u007F-\u009F]/g;
|
|
55
|
+
export function sanitizeTerminalText(str) {
|
|
56
|
+
return str.replace(/\r\n?/g, '\n').replace(TERMINAL_ESCAPE_RE, '').replace(CONTROL_RE, '');
|
|
57
|
+
}
|
|
58
|
+
export function sanitizeTerminalLine(str) {
|
|
59
|
+
return sanitizeTerminalText(str).replace(/\s+/gu, ' ').trim();
|
|
36
60
|
}
|
|
37
|
-
/** Total visual width of a string (CJK/emoji count as 2, zero-width
|
|
38
|
-
* modifiers count as 0, ANSI codes ignored). */
|
|
39
61
|
export function visualWidth(str) {
|
|
40
62
|
const plain = stripAnsi(str);
|
|
41
63
|
let w = 0;
|
|
42
|
-
for (const
|
|
43
|
-
|
|
44
|
-
if (isZeroWidth(cp))
|
|
45
|
-
continue;
|
|
46
|
-
w += charWidth(ch);
|
|
47
|
-
}
|
|
64
|
+
for (const grapheme of graphemes(plain))
|
|
65
|
+
w += graphemeWidth(grapheme);
|
|
48
66
|
return w;
|
|
49
67
|
}
|
|
50
|
-
/** Pad string to target visual width with trailing spaces. */
|
|
51
68
|
export function padEndV(str, width) {
|
|
52
69
|
const pad = width - visualWidth(str);
|
|
53
70
|
return pad > 0 ? str + ' '.repeat(pad) : str;
|
|
54
71
|
}
|
|
55
|
-
/** Truncate to visual width limit, appending '...' if cut. */
|
|
56
72
|
export function truncate(str, maxWidth) {
|
|
57
73
|
if (visualWidth(str) <= maxWidth)
|
|
58
74
|
return str;
|
|
75
|
+
if (maxWidth <= 0)
|
|
76
|
+
return '';
|
|
77
|
+
const marker = maxWidth >= 3 ? '...' : '.'.repeat(maxWidth);
|
|
78
|
+
const available = maxWidth - visualWidth(marker);
|
|
59
79
|
let w = 0;
|
|
60
|
-
let
|
|
61
|
-
for (const
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
if (w + cw > maxWidth - 3)
|
|
80
|
+
let value = '';
|
|
81
|
+
for (const grapheme of graphemes(str)) {
|
|
82
|
+
const cw = graphemeWidth(grapheme);
|
|
83
|
+
if (w + cw > available)
|
|
65
84
|
break;
|
|
66
85
|
w += cw;
|
|
67
|
-
|
|
86
|
+
value += grapheme;
|
|
87
|
+
}
|
|
88
|
+
return value + marker;
|
|
89
|
+
}
|
|
90
|
+
export function truncateStart(str, maxWidth, marker = '...') {
|
|
91
|
+
if (visualWidth(str) <= maxWidth)
|
|
92
|
+
return str;
|
|
93
|
+
if (maxWidth <= 0)
|
|
94
|
+
return '';
|
|
95
|
+
let fittedMarker = '';
|
|
96
|
+
let markerWidth = 0;
|
|
97
|
+
for (const segment of graphemes(marker)) {
|
|
98
|
+
const segmentWidth = graphemeWidth(segment);
|
|
99
|
+
if (markerWidth + segmentWidth > maxWidth)
|
|
100
|
+
break;
|
|
101
|
+
fittedMarker += segment;
|
|
102
|
+
markerWidth += segmentWidth;
|
|
68
103
|
}
|
|
69
|
-
|
|
104
|
+
const available = Math.max(0, maxWidth - visualWidth(fittedMarker));
|
|
105
|
+
const segments = graphemes(str);
|
|
106
|
+
let value = '';
|
|
107
|
+
let width = 0;
|
|
108
|
+
for (let index = segments.length - 1; index >= 0; index -= 1) {
|
|
109
|
+
const segment = segments[index];
|
|
110
|
+
if (segment === undefined)
|
|
111
|
+
continue;
|
|
112
|
+
const segmentWidth = graphemeWidth(segment);
|
|
113
|
+
if (width + segmentWidth > available)
|
|
114
|
+
break;
|
|
115
|
+
value = segment + value;
|
|
116
|
+
width += segmentWidth;
|
|
117
|
+
}
|
|
118
|
+
return fittedMarker + value;
|
|
119
|
+
}
|
|
120
|
+
function osc8State(sequence) {
|
|
121
|
+
const match = OSC8_RE.exec(sequence);
|
|
122
|
+
if (!match)
|
|
123
|
+
return undefined;
|
|
124
|
+
return match[1] ? sequence : null;
|
|
70
125
|
}
|
|
71
126
|
function tokenizeForWrapping(str) {
|
|
72
127
|
const tokens = [];
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
128
|
+
const pushText = (value) => {
|
|
129
|
+
for (const grapheme of graphemes(value)) {
|
|
130
|
+
tokens.push({
|
|
131
|
+
raw: grapheme,
|
|
132
|
+
width: graphemeWidth(grapheme),
|
|
133
|
+
whitespace: /\s/u.test(grapheme),
|
|
134
|
+
sgr: false,
|
|
135
|
+
});
|
|
80
136
|
}
|
|
81
|
-
|
|
82
|
-
|
|
137
|
+
};
|
|
138
|
+
let index = 0;
|
|
139
|
+
for (const match of str.matchAll(TERMINAL_ESCAPE_RE)) {
|
|
140
|
+
const start = match.index;
|
|
141
|
+
pushText(str.slice(index, start));
|
|
142
|
+
const raw = match[0];
|
|
143
|
+
const osc8 = osc8State(raw);
|
|
83
144
|
tokens.push({
|
|
84
145
|
raw,
|
|
85
|
-
width:
|
|
86
|
-
whitespace:
|
|
87
|
-
sgr:
|
|
146
|
+
width: 0,
|
|
147
|
+
whitespace: false,
|
|
148
|
+
sgr: SGR_RE.test(raw),
|
|
149
|
+
...(osc8 === undefined ? {} : { osc8 }),
|
|
88
150
|
});
|
|
89
|
-
index
|
|
151
|
+
index = start + match[0].length;
|
|
90
152
|
}
|
|
153
|
+
pushText(str.slice(index));
|
|
91
154
|
return tokens;
|
|
92
155
|
}
|
|
156
|
+
export function clipAnsiToVisualWidth(str, maxWidth) {
|
|
157
|
+
const limit = Math.max(0, Math.floor(maxWidth));
|
|
158
|
+
if (visualWidth(str) <= limit)
|
|
159
|
+
return str;
|
|
160
|
+
const tokens = tokenizeForWrapping(str);
|
|
161
|
+
const output = [];
|
|
162
|
+
let width = 0;
|
|
163
|
+
let activeOsc8 = null;
|
|
164
|
+
for (const token of tokens) {
|
|
165
|
+
if (token.sgr || token.width === 0) {
|
|
166
|
+
output.push(token.raw);
|
|
167
|
+
if (token.osc8 !== undefined)
|
|
168
|
+
activeOsc8 = token.osc8;
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
if (width + token.width > limit)
|
|
172
|
+
break;
|
|
173
|
+
output.push(token.raw);
|
|
174
|
+
width += token.width;
|
|
175
|
+
}
|
|
176
|
+
if (activeOsc8)
|
|
177
|
+
output.push(OSC8_CLOSE);
|
|
178
|
+
return output.join('');
|
|
179
|
+
}
|
|
93
180
|
function advanceSgr(active, tokens, start, end) {
|
|
94
181
|
let next = active;
|
|
95
182
|
for (let index = start; index < end; index += 1) {
|
|
@@ -106,12 +193,24 @@ function advanceSgr(active, tokens, start, end) {
|
|
|
106
193
|
}
|
|
107
194
|
return next;
|
|
108
195
|
}
|
|
109
|
-
function
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
196
|
+
function advanceOsc8(active, tokens, start, end) {
|
|
197
|
+
let next = active;
|
|
198
|
+
for (let index = start; index < end; index += 1) {
|
|
199
|
+
const token = tokens[index];
|
|
200
|
+
if (token?.osc8 !== undefined)
|
|
201
|
+
next = token.osc8;
|
|
202
|
+
}
|
|
203
|
+
return next;
|
|
204
|
+
}
|
|
205
|
+
function renderWrappedSegment(tokens, start, end, activeSgr, activeOsc8) {
|
|
206
|
+
const body = tokens
|
|
207
|
+
.slice(start, end)
|
|
208
|
+
.map((token) => token.raw)
|
|
209
|
+
.join('');
|
|
210
|
+
const styled = `${activeOsc8 ?? ''}${activeSgr}${body}`;
|
|
211
|
+
const withReset = activeSgr || tokens.slice(start, end).some((token) => token.sgr) ? `${styled}\x1b[0m` : styled;
|
|
212
|
+
const osc8AtEnd = advanceOsc8(activeOsc8, tokens, start, end);
|
|
213
|
+
return osc8AtEnd ? `${withReset}${OSC8_CLOSE}` : withReset;
|
|
115
214
|
}
|
|
116
215
|
export function wrapAnsiToVisualWidth(str, maxWidth) {
|
|
117
216
|
const widthLimit = Math.max(1, Math.floor(maxWidth));
|
|
@@ -120,6 +219,7 @@ export function wrapAnsiToVisualWidth(str, maxWidth) {
|
|
|
120
219
|
const tokens = tokenizeForWrapping(str);
|
|
121
220
|
const lines = [];
|
|
122
221
|
let activeSgr = '';
|
|
222
|
+
let activeOsc8 = null;
|
|
123
223
|
let start = 0;
|
|
124
224
|
while (start < tokens.length) {
|
|
125
225
|
let width = 0;
|
|
@@ -143,7 +243,7 @@ export function wrapAnsiToVisualWidth(str, maxWidth) {
|
|
|
143
243
|
index += 1;
|
|
144
244
|
}
|
|
145
245
|
if (index >= tokens.length) {
|
|
146
|
-
lines.push(renderWrappedSegment(tokens, start, tokens.length, activeSgr));
|
|
246
|
+
lines.push(renderWrappedSegment(tokens, start, tokens.length, activeSgr, activeOsc8));
|
|
147
247
|
break;
|
|
148
248
|
}
|
|
149
249
|
const overflow = tokens[index];
|
|
@@ -160,9 +260,26 @@ export function wrapAnsiToVisualWidth(str, maxWidth) {
|
|
|
160
260
|
end = Math.max(index, start + 1);
|
|
161
261
|
next = end;
|
|
162
262
|
}
|
|
163
|
-
lines.push(renderWrappedSegment(tokens, start, end, activeSgr));
|
|
263
|
+
lines.push(renderWrappedSegment(tokens, start, end, activeSgr, activeOsc8));
|
|
164
264
|
activeSgr = advanceSgr(activeSgr, tokens, start, next);
|
|
265
|
+
activeOsc8 = advanceOsc8(activeOsc8, tokens, start, next);
|
|
165
266
|
start = next;
|
|
166
267
|
}
|
|
167
268
|
return lines.length > 0 ? lines : [''];
|
|
168
269
|
}
|
|
270
|
+
export function wrapAnsiWithIndent(str, maxWidth, preferredIndent = '') {
|
|
271
|
+
const width = Number.isFinite(maxWidth)
|
|
272
|
+
? Math.max(1, Math.floor(maxWidth))
|
|
273
|
+
: Number.POSITIVE_INFINITY;
|
|
274
|
+
const indentWidth = visualWidth(preferredIndent);
|
|
275
|
+
const contentWidth = visualWidth(str);
|
|
276
|
+
let indent = indentWidth >= width || (contentWidth > width - indentWidth && contentWidth <= width)
|
|
277
|
+
? ''
|
|
278
|
+
: preferredIndent;
|
|
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}`);
|
|
285
|
+
}
|
package/dist/core/theme.js
CHANGED
|
@@ -1,21 +1,11 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
2
|
import gradient from 'gradient-string';
|
|
3
3
|
import { pickIcon } from './icons.js';
|
|
4
|
-
// The one gradient the brand uses anywhere it appears -- the startup logo,
|
|
5
|
-
// and (in text form) the persistent header wordmark. Defined once here so
|
|
6
|
-
// both stay the same three stops instead of drifting apart.
|
|
7
4
|
export const brandGradient = gradient([
|
|
8
5
|
{ color: '#124689', pos: 0 },
|
|
9
6
|
{ color: '#0ea5e9', pos: 0.55 },
|
|
10
7
|
{ color: '#06b6d4', pos: 1 },
|
|
11
8
|
]);
|
|
12
|
-
/** The brand wordmark treatment: bold text painted in `brandGradient`,
|
|
13
|
-
* falling back to plain text under NO_COLOR (gradient-string doesn't
|
|
14
|
-
* auto-respect it the way chalk's own colors do). Currently used by the
|
|
15
|
-
* header's persistent "nbtca" mark (`app/chrome.ts`) -- named here, not
|
|
16
|
-
* left as a one-off local helper, so any future chrome element that wants
|
|
17
|
-
* "the brand gradient, as a wordmark" has a single place to reuse instead
|
|
18
|
-
* of re-deriving the NO_COLOR/bold/gradient combination again. */
|
|
19
9
|
export function brandMark(s) {
|
|
20
10
|
if (process.env['NO_COLOR'])
|
|
21
11
|
return s;
|
|
@@ -49,11 +39,6 @@ export const glyph = {
|
|
|
49
39
|
dot: () => pickIcon('●', '*'),
|
|
50
40
|
updown: () => pickIcon('↑↓', 'up/down'),
|
|
51
41
|
enter: () => pickIcon('⏎', 'enter'),
|
|
52
|
-
// Two-level "how full" bar cell (Home's day-progress, Schedule's
|
|
53
|
-
// term-progress) — a single source of truth so every such bar in the
|
|
54
|
-
// app reads as the same visual language. Not the same vocabulary as
|
|
55
|
-
// calendar-heatmap's 5-level intensity scale, which is a deliberately
|
|
56
|
-
// finer-grained density visualization, not a binary fill/empty bar.
|
|
57
42
|
barFilled: () => pickIcon('█', '#'),
|
|
58
43
|
barEmpty: () => pickIcon('░', '-'),
|
|
59
44
|
};
|
|
@@ -65,19 +50,6 @@ export const type = {
|
|
|
65
50
|
label: (s) => chalk.white(s),
|
|
66
51
|
body: (s) => s,
|
|
67
52
|
hint: (s) => chalk.dim(s),
|
|
68
|
-
/** The one thing on this screen your eye should land on: the app's own
|
|
69
|
-
* name, the tab you're on, the row a menu's cursor sits on, the class
|
|
70
|
-
* that's happening right now. `heading` marks a section as structure;
|
|
71
|
-
* `active` marks a single point as attention — never both on the same
|
|
72
|
-
* element, and never more than one or two `active` uses per screen, or
|
|
73
|
-
* the signal stops meaning anything. Brand color (#0ea5e9) precisely
|
|
74
|
-
* because there is exactly one brand-worthy thing to say on each of
|
|
75
|
-
* these screens, and this is where it belongs. */
|
|
76
53
|
active: (s) => chalk.bold(c.brand(s)),
|
|
77
|
-
/** The grid cursor's own visual signal: a solid brand-colored background
|
|
78
|
-
* block, deliberately distinct from `active` (bold text on the default
|
|
79
|
-
* background) so "this is today" and "this is where your cursor is" never
|
|
80
|
-
* share one visual language, even when the cursor lands on today's own
|
|
81
|
-
* column. */
|
|
82
54
|
cursor: (s) => chalk.bgHex('#0ea5e9').black(s),
|
|
83
55
|
};
|
package/dist/core/transitions.js
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
import { clearScreen } from './ui.js';
|
|
2
2
|
import { typeReveal } from './motion.js';
|
|
3
3
|
import { glyph, type, space } from './theme.js';
|
|
4
|
-
import { screenWidth } from './components/screen.js';
|
|
5
4
|
import { pickIcon } from './icons.js';
|
|
6
5
|
export function breadcrumb(label) {
|
|
7
6
|
return `nbtca ${pickIcon('›', '>')} ${label}`;
|
|
8
7
|
}
|
|
9
8
|
export function buildScreenHeaderLines(crumb) {
|
|
9
|
+
const width = Math.min(process.stdout.columns || 80, 64);
|
|
10
10
|
return [
|
|
11
11
|
space.indent + type.heading(crumb),
|
|
12
|
-
space.indent + type.hint(glyph.rule().repeat(
|
|
12
|
+
space.indent + type.hint(glyph.rule().repeat(width)),
|
|
13
13
|
'',
|
|
14
14
|
];
|
|
15
15
|
}
|
package/dist/core/ui.js
CHANGED
|
@@ -1,29 +1,31 @@
|
|
|
1
1
|
import { success, error, warning, info } from './components/messages.js';
|
|
2
2
|
import { startSpinner } from './components/spinner.js';
|
|
3
3
|
import chalk from 'chalk';
|
|
4
|
-
import { pickIcon } from './icons.js';
|
|
5
4
|
import { t } from '../i18n/index.js';
|
|
5
|
+
import { sanitizeTerminalLine } from './text.js';
|
|
6
6
|
export { success, error, warning, info };
|
|
7
|
-
export function printDivider() {
|
|
8
|
-
const terminalWidth = process.stdout.columns || 80;
|
|
9
|
-
const dividerChar = pickIcon('─', '-');
|
|
10
|
-
console.log(chalk.dim(dividerChar.repeat(Math.min(terminalWidth, 80))));
|
|
11
|
-
}
|
|
12
7
|
export function clearScreen() {
|
|
13
8
|
if (process.stdout.isTTY) {
|
|
14
9
|
console.clear();
|
|
15
10
|
}
|
|
16
11
|
}
|
|
17
|
-
export function printNewLine(count = 1) {
|
|
18
|
-
for (let i = 0; i < count; i++) {
|
|
19
|
-
console.log();
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
12
|
export function createSpinner(msg) {
|
|
23
13
|
return startSpinner(msg);
|
|
24
14
|
}
|
|
15
|
+
function errorMessage(errorValue) {
|
|
16
|
+
if (errorValue instanceof Error)
|
|
17
|
+
return errorValue.message;
|
|
18
|
+
if (typeof errorValue === 'string')
|
|
19
|
+
return errorValue;
|
|
20
|
+
if (typeof errorValue === 'number' ||
|
|
21
|
+
typeof errorValue === 'boolean' ||
|
|
22
|
+
typeof errorValue === 'bigint') {
|
|
23
|
+
return String(errorValue);
|
|
24
|
+
}
|
|
25
|
+
return '';
|
|
26
|
+
}
|
|
25
27
|
export function handleGracefulExit(err) {
|
|
26
|
-
const message =
|
|
28
|
+
const message = sanitizeTerminalLine(errorMessage(err));
|
|
27
29
|
if (message.includes('SIGINT') || message.includes('User force closed')) {
|
|
28
30
|
console.log();
|
|
29
31
|
console.log(chalk.dim(t().common.goodbye));
|
|
@@ -33,7 +35,7 @@ export function handleGracefulExit(err) {
|
|
|
33
35
|
console.error(message);
|
|
34
36
|
}
|
|
35
37
|
else {
|
|
36
|
-
console.error('
|
|
38
|
+
console.error('An unexpected error occurred.');
|
|
37
39
|
}
|
|
38
40
|
process.exit(1);
|
|
39
41
|
}
|
package/dist/core/vim-keys.js
CHANGED
|
@@ -1,36 +1,173 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Vim keybindings support
|
|
3
|
-
* Intercepts raw data events and replaces vim keys with terminal escape sequences
|
|
4
|
-
* before readline processes them. This avoids patching the keypress layer which
|
|
5
|
-
* breaks in Node.js v25+ (emitKeys generator crash).
|
|
6
|
-
*/
|
|
7
|
-
// Maps single-byte vim keys to terminal escape sequences (ranger-style hjkl)
|
|
8
1
|
const VIM_TO_SEQ = {
|
|
9
|
-
j: Buffer.from('\u001b[B'),
|
|
10
|
-
k: Buffer.from('\u001b[A'),
|
|
11
|
-
l: Buffer.from('\r'),
|
|
12
|
-
g: Buffer.from('\u001b[H'),
|
|
13
|
-
G: Buffer.from('\u001b[F'),
|
|
14
|
-
q: Buffer.from('\u0003'), // quit
|
|
2
|
+
j: Buffer.from('\u001b[B'),
|
|
3
|
+
k: Buffer.from('\u001b[A'),
|
|
4
|
+
l: Buffer.from('\r'),
|
|
5
|
+
g: Buffer.from('\u001b[H'),
|
|
6
|
+
G: Buffer.from('\u001b[F'),
|
|
15
7
|
};
|
|
16
8
|
let vimActive = true;
|
|
17
9
|
export function setVimKeysActive(active) {
|
|
18
10
|
vimActive = active;
|
|
19
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
|
+
}
|
|
20
114
|
export function enableVimKeys() {
|
|
21
115
|
const stdin = process.stdin;
|
|
22
116
|
if (!stdin.isTTY)
|
|
23
117
|
return;
|
|
24
118
|
const originalEmit = stdin.emit.bind(stdin);
|
|
25
|
-
|
|
26
|
-
|
|
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
|
+
};
|
|
154
|
+
const translatedEmit = (event, ...args) => {
|
|
155
|
+
if (event === 'data') {
|
|
27
156
|
const chunk = args[0];
|
|
28
|
-
if (Buffer.isBuffer(chunk)
|
|
29
|
-
|
|
30
|
-
if (
|
|
31
|
-
|
|
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;
|
|
32
168
|
}
|
|
33
169
|
}
|
|
34
170
|
return originalEmit(event, ...args);
|
|
35
171
|
};
|
|
172
|
+
stdin.emit = translatedEmit;
|
|
36
173
|
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { APP_INFO, URLS } from '../config/data.js';
|
|
3
|
+
import { note } from '../core/components/note.js';
|
|
4
|
+
import { pickIcon } from '../core/icons.js';
|
|
5
|
+
import { padEndV } from '../core/text.js';
|
|
6
|
+
import { t } from '../i18n/index.js';
|
|
7
|
+
export function showAbout() {
|
|
8
|
+
const trans = t();
|
|
9
|
+
const row = (label, value) => `${chalk.dim(padEndV(label, 12))}${value}`;
|
|
10
|
+
const link = (label, url) => row(label, chalk.cyan(url));
|
|
11
|
+
const content = [
|
|
12
|
+
row(trans.about.project, APP_INFO.name),
|
|
13
|
+
row(trans.about.version, `v${APP_INFO.version}`),
|
|
14
|
+
row(trans.about.description, trans.about.descriptionText),
|
|
15
|
+
'',
|
|
16
|
+
link(trans.about.github, APP_INFO.repository),
|
|
17
|
+
link(trans.about.website, URLS.homepage),
|
|
18
|
+
link(trans.about.email, URLS.email),
|
|
19
|
+
'',
|
|
20
|
+
row(trans.about.license, `MIT ${pickIcon('·', '|')} ${trans.about.author}: m1ngsama`),
|
|
21
|
+
].join('\n');
|
|
22
|
+
note(content, trans.about.title);
|
|
23
|
+
}
|