@nbtca/prompt 1.4.2 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +27 -58
- package/SECURITY.md +16 -45
- package/dist/app/app.js +53 -55
- 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 +2 -21
- package/dist/app/keys.js +10 -2
- package/dist/app/views/docs-render.js +31 -24
- package/dist/app/views/docs.js +211 -60
- package/dist/app/views/events-render.js +19 -26
- package/dist/app/views/events.js +44 -31
- package/dist/app/views/home.js +28 -30
- package/dist/app/views/schedule-grid-cursor.js +9 -18
- package/dist/app/views/schedule-render.js +47 -71
- package/dist/app/views/schedule.js +158 -81
- 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/config/data.js +9 -11
- package/dist/config/preferences.js +14 -7
- package/dist/core/calendar-day.js +37 -0
- package/dist/core/capabilities.js +6 -3
- package/dist/core/components/confirm.js +9 -8
- package/dist/core/components/menu.js +41 -16
- package/dist/core/components/messages.js +12 -4
- package/dist/core/components/painter.js +3 -1
- package/dist/core/components/spinner.js +17 -6
- 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 +182 -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 +9 -15
- 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.js +12 -185
- package/dist/features/docs.js +436 -275
- 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 +44 -56
- package/dist/features/student-timetable.js +73 -95
- package/dist/features/theme.js +6 -2
- package/dist/features/timetable-sanitize.js +40 -0
- package/dist/features/update.js +9 -27
- package/dist/i18n/index.js +83 -19
- package/dist/i18n/locales/en.json +1 -1
- package/dist/i18n/locales/zh.json +1 -1
- package/dist/index.js +83 -58
- package/dist/logo/ca-dotmatrix.txt +16 -18
- package/dist/main.js +7 -48
- package/package.json +27 -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/links.js +0 -36
- package/dist/features/schedule-query.js +0 -47
- package/dist/features/settings.js +0 -127
- package/dist/logo/ca-logo.png +0 -0
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
2
|
import path from 'path';
|
|
3
|
+
import chalk from 'chalk';
|
|
3
4
|
import { getConfigDir, getWritableConfigDir } from './paths.js';
|
|
4
5
|
const DEFAULT_PREFERENCES = {
|
|
5
6
|
iconMode: 'auto',
|
|
6
7
|
colorMode: 'auto',
|
|
7
8
|
};
|
|
9
|
+
const detectedColorLevel = chalk.level;
|
|
10
|
+
const inheritedNoColor = process.env['NO_COLOR'];
|
|
8
11
|
function getPreferencesPath() {
|
|
9
12
|
return path.join(getConfigDir(), 'preferences.json');
|
|
10
13
|
}
|
|
@@ -50,30 +53,34 @@ export function resetPreferences() {
|
|
|
50
53
|
return savePreferences({ ...DEFAULT_PREFERENCES });
|
|
51
54
|
}
|
|
52
55
|
export function resolveIconMode() {
|
|
53
|
-
const env = (process.env['NBTCA_ICON_MODE']
|
|
56
|
+
const env = (process.env['NBTCA_ICON_MODE'] ?? '').toLowerCase();
|
|
54
57
|
if (env === 'ascii' || env === 'unicode' || env === 'auto') {
|
|
55
58
|
return env;
|
|
56
59
|
}
|
|
57
60
|
return loadPreferences().iconMode;
|
|
58
61
|
}
|
|
59
62
|
export function resolveColorMode() {
|
|
60
|
-
const env = (process.env['NBTCA_COLOR_MODE']
|
|
63
|
+
const env = (process.env['NBTCA_COLOR_MODE'] ?? '').toLowerCase();
|
|
61
64
|
if (env === 'on' || env === 'off' || env === 'auto') {
|
|
62
65
|
return env;
|
|
63
66
|
}
|
|
64
67
|
return loadPreferences().colorMode;
|
|
65
68
|
}
|
|
66
69
|
export function applyColorModePreference(forcePlain) {
|
|
67
|
-
|
|
68
|
-
process.env['NO_COLOR'] = '1';
|
|
69
|
-
return;
|
|
70
|
-
}
|
|
71
|
-
const mode = resolveColorMode();
|
|
70
|
+
const mode = forcePlain ? 'off' : resolveColorMode();
|
|
72
71
|
if (mode === 'off') {
|
|
73
72
|
process.env['NO_COLOR'] = '1';
|
|
73
|
+
chalk.level = 0;
|
|
74
74
|
return;
|
|
75
75
|
}
|
|
76
76
|
if (mode === 'on') {
|
|
77
77
|
delete process.env['NO_COLOR'];
|
|
78
|
+
chalk.level = 3;
|
|
79
|
+
return;
|
|
78
80
|
}
|
|
81
|
+
if (inheritedNoColor === undefined)
|
|
82
|
+
delete process.env['NO_COLOR'];
|
|
83
|
+
else
|
|
84
|
+
process.env['NO_COLOR'] = inheritedNoColor;
|
|
85
|
+
chalk.level = detectedColorLevel;
|
|
79
86
|
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
const DAY_MS = 86_400_000;
|
|
2
|
+
const LOCAL_DATE_PATTERN = /^(\d{4})-(\d{2})-(\d{2})$/;
|
|
3
|
+
export function parseLocalDate(value) {
|
|
4
|
+
const match = LOCAL_DATE_PATTERN.exec(value);
|
|
5
|
+
if (!match)
|
|
6
|
+
throw new RangeError(`Invalid local date: ${value}`);
|
|
7
|
+
const year = Number(match[1]);
|
|
8
|
+
const month = Number(match[2]);
|
|
9
|
+
const day = Number(match[3]);
|
|
10
|
+
const date = new Date(0);
|
|
11
|
+
date.setHours(0, 0, 0, 0);
|
|
12
|
+
date.setFullYear(year, month - 1, day);
|
|
13
|
+
if (date.getFullYear() !== year || date.getMonth() !== month - 1 || date.getDate() !== day) {
|
|
14
|
+
throw new RangeError(`Invalid local date: ${value}`);
|
|
15
|
+
}
|
|
16
|
+
return date;
|
|
17
|
+
}
|
|
18
|
+
export function parseLocalMonday(value) {
|
|
19
|
+
const date = parseLocalDate(value);
|
|
20
|
+
if (date.getDay() !== 1)
|
|
21
|
+
throw new RangeError(`Local date is not a Monday: ${value}`);
|
|
22
|
+
return date;
|
|
23
|
+
}
|
|
24
|
+
export function addLocalDays(date, days) {
|
|
25
|
+
const result = new Date(date.getTime());
|
|
26
|
+
result.setDate(result.getDate() + days);
|
|
27
|
+
return result;
|
|
28
|
+
}
|
|
29
|
+
function localDayIndex(date) {
|
|
30
|
+
const index = new Date(0);
|
|
31
|
+
index.setUTCHours(0, 0, 0, 0);
|
|
32
|
+
index.setUTCFullYear(date.getFullYear(), date.getMonth(), date.getDate());
|
|
33
|
+
return index.getTime() / DAY_MS;
|
|
34
|
+
}
|
|
35
|
+
export function localDayDifference(start, end) {
|
|
36
|
+
return localDayIndex(end) - localDayIndex(start);
|
|
37
|
+
}
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { useUnicodeIcons } from './icons.js';
|
|
2
2
|
import { resolveColorMode } from '../config/preferences.js';
|
|
3
|
+
function isTty(value) {
|
|
4
|
+
return value === true;
|
|
5
|
+
}
|
|
3
6
|
export function deriveReducedMotion(o) {
|
|
4
7
|
const env = o.env ?? process.env;
|
|
5
8
|
if (!o.isTTY)
|
|
@@ -8,7 +11,7 @@ export function deriveReducedMotion(o) {
|
|
|
8
11
|
return true;
|
|
9
12
|
if (env['CI'])
|
|
10
13
|
return true;
|
|
11
|
-
if ((env['TERM']
|
|
14
|
+
if ((env['TERM'] ?? '').toLowerCase() === 'dumb')
|
|
12
15
|
return true;
|
|
13
16
|
if (!o.color)
|
|
14
17
|
return true;
|
|
@@ -24,13 +27,13 @@ function detectColor() {
|
|
|
24
27
|
return false;
|
|
25
28
|
if (mode === 'on')
|
|
26
29
|
return true;
|
|
27
|
-
return
|
|
30
|
+
return isTty(process.stdout.isTTY);
|
|
28
31
|
}
|
|
29
32
|
let cached = null;
|
|
30
33
|
export function getCapabilities() {
|
|
31
34
|
if (cached)
|
|
32
35
|
return cached;
|
|
33
|
-
const isTTY =
|
|
36
|
+
const isTTY = isTty(process.stdout.isTTY) && isTty(process.stdin.isTTY);
|
|
34
37
|
const unicode = useUnicodeIcons();
|
|
35
38
|
const color = detectColor();
|
|
36
39
|
const reducedMotion = deriveReducedMotion({ isTTY, color, unicode });
|
|
@@ -19,12 +19,13 @@ export function parseConfirmData(data) {
|
|
|
19
19
|
export function renderConfirm(opts) {
|
|
20
20
|
const cursor = glyph.cursor();
|
|
21
21
|
const gap = ' '.repeat(cursor.length);
|
|
22
|
-
const yes = opts.value
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
`${
|
|
27
|
-
|
|
22
|
+
const yes = opts.value
|
|
23
|
+
? `${type.active(cursor)} ${type.active('Yes')}`
|
|
24
|
+
: `${gap} ${type.body('Yes')}`;
|
|
25
|
+
const no = opts.value
|
|
26
|
+
? `${gap} ${type.body('No')}`
|
|
27
|
+
: `${type.active(cursor)} ${type.active('No')}`;
|
|
28
|
+
return [space.indent + type.label(opts.message), `${space.indent}${yes} ${no}`].join('\n');
|
|
28
29
|
}
|
|
29
30
|
export function runConfirm(config) {
|
|
30
31
|
return new Promise((resolve) => {
|
|
@@ -47,12 +48,12 @@ export function runConfirm(config) {
|
|
|
47
48
|
finish(value);
|
|
48
49
|
return;
|
|
49
50
|
}
|
|
50
|
-
if (ev === 'yes' && value
|
|
51
|
+
if (ev === 'yes' && !value) {
|
|
51
52
|
value = true;
|
|
52
53
|
paint();
|
|
53
54
|
return;
|
|
54
55
|
}
|
|
55
|
-
if (ev === 'no' && value
|
|
56
|
+
if (ev === 'no' && value) {
|
|
56
57
|
value = false;
|
|
57
58
|
paint();
|
|
58
59
|
return;
|
|
@@ -6,26 +6,52 @@ import { t } from '../../i18n/index.js';
|
|
|
6
6
|
export function parseKey(data) {
|
|
7
7
|
const s = data.toString();
|
|
8
8
|
switch (s) {
|
|
9
|
-
case '\x1b[A':
|
|
10
|
-
|
|
11
|
-
case '\x1b[
|
|
12
|
-
|
|
9
|
+
case '\x1b[A':
|
|
10
|
+
return 'up';
|
|
11
|
+
case '\x1b[B':
|
|
12
|
+
return 'down';
|
|
13
|
+
case '\x1b[5~':
|
|
14
|
+
return 'pageUp';
|
|
15
|
+
case '\x1b[6~':
|
|
16
|
+
return 'pageDown';
|
|
17
|
+
case '\x1b[H':
|
|
18
|
+
case '\x1b[1~':
|
|
19
|
+
case '\x1bOH':
|
|
20
|
+
return 'home';
|
|
21
|
+
case '\x1b[F':
|
|
22
|
+
case '\x1b[4~':
|
|
23
|
+
case '\x1bOF':
|
|
24
|
+
return 'end';
|
|
13
25
|
case '\r':
|
|
14
|
-
case '\n':
|
|
26
|
+
case '\n':
|
|
27
|
+
return 'enter';
|
|
15
28
|
case '\x03':
|
|
16
|
-
case '\x1b':
|
|
17
|
-
|
|
29
|
+
case '\x1b':
|
|
30
|
+
return 'cancel';
|
|
31
|
+
default:
|
|
32
|
+
return 'none';
|
|
18
33
|
}
|
|
19
34
|
}
|
|
20
|
-
export function nextIndex(current, key, len) {
|
|
35
|
+
export function nextIndex(current, key, len, pageSize = 5) {
|
|
21
36
|
if (len <= 0)
|
|
22
37
|
return 0;
|
|
23
38
|
switch (key) {
|
|
24
|
-
case 'up':
|
|
25
|
-
|
|
26
|
-
case '
|
|
27
|
-
|
|
28
|
-
|
|
39
|
+
case 'up':
|
|
40
|
+
return (current - 1 + len) % len;
|
|
41
|
+
case 'down':
|
|
42
|
+
return (current + 1) % len;
|
|
43
|
+
case 'pageUp':
|
|
44
|
+
return Math.max(0, current - Math.max(1, pageSize));
|
|
45
|
+
case 'pageDown':
|
|
46
|
+
return Math.min(len - 1, current + Math.max(1, pageSize));
|
|
47
|
+
case 'home':
|
|
48
|
+
return 0;
|
|
49
|
+
case 'end':
|
|
50
|
+
return len - 1;
|
|
51
|
+
case 'enter':
|
|
52
|
+
case 'cancel':
|
|
53
|
+
case 'none':
|
|
54
|
+
return current;
|
|
29
55
|
}
|
|
30
56
|
}
|
|
31
57
|
function normalizedWidth(cols) {
|
|
@@ -51,8 +77,7 @@ export function renderMenuOption(option, selected, labelWidth = visualWidth(opti
|
|
|
51
77
|
const padded = padEndV(option.label, paddedWidth);
|
|
52
78
|
const label = selected ? type.active(padded) : type.body(padded);
|
|
53
79
|
const hint = option.hint ? ` ${type.hint(option.hint)}` : '';
|
|
54
|
-
return wrapAnsiToVisualWidth(`${label}${hint}`, contentWidth)
|
|
55
|
-
.map((line, index) => `${index === 0 ? prefix : continuation}${line}`);
|
|
80
|
+
return wrapAnsiToVisualWidth(`${label}${hint}`, contentWidth).map((line, index) => `${index === 0 ? prefix : continuation}${line}`);
|
|
56
81
|
}
|
|
57
82
|
export function renderMenu(state, cols = Number.POSITIVE_INFINITY) {
|
|
58
83
|
const labelWidth = state.options.reduce((width, option) => Math.max(width, visualWidth(option.label)), 0);
|
|
@@ -86,7 +111,7 @@ export function runMenu(config) {
|
|
|
86
111
|
title: config.title,
|
|
87
112
|
options: config.options,
|
|
88
113
|
selectedIndex: index,
|
|
89
|
-
footer: config.footer,
|
|
114
|
+
...(config.footer === undefined ? {} : { footer: config.footer }),
|
|
90
115
|
}));
|
|
91
116
|
const cleanup = () => {
|
|
92
117
|
stdin.removeListener('data', onData);
|
|
@@ -10,7 +10,15 @@ export function renderMessage(kind, msg) {
|
|
|
10
10
|
const m = MARKERS[kind];
|
|
11
11
|
return `${space.indent}${m.color(m.icon())} ${msg}`;
|
|
12
12
|
}
|
|
13
|
-
export function success(msg) {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
export function
|
|
13
|
+
export function success(msg) {
|
|
14
|
+
console.log(renderMessage('success', msg));
|
|
15
|
+
}
|
|
16
|
+
export function error(msg) {
|
|
17
|
+
console.log(renderMessage('error', msg));
|
|
18
|
+
}
|
|
19
|
+
export function warning(msg) {
|
|
20
|
+
console.log(renderMessage('warn', msg));
|
|
21
|
+
}
|
|
22
|
+
export function info(msg) {
|
|
23
|
+
console.log(renderMessage('info', msg));
|
|
24
|
+
}
|
|
@@ -11,7 +11,9 @@ export function frameRows(frame, cols) {
|
|
|
11
11
|
* Returns a paint() closure that redraws a fixed-line-count frame in place,
|
|
12
12
|
* erasing the previous frame first. `write` is injectable for tests.
|
|
13
13
|
*/
|
|
14
|
-
export function createPainter(frame, write = (s) => {
|
|
14
|
+
export function createPainter(frame, write = (s) => {
|
|
15
|
+
process.stdout.write(s);
|
|
16
|
+
}) {
|
|
15
17
|
let painted = 0;
|
|
16
18
|
return () => {
|
|
17
19
|
const f = frame();
|
|
@@ -9,16 +9,21 @@ export function renderSpinnerFrame(frame, msg) {
|
|
|
9
9
|
return `${space.indent}${c.accent(frame)} ${msg}`;
|
|
10
10
|
}
|
|
11
11
|
export function startSpinner(msg = '', opts = {}) {
|
|
12
|
-
const write = opts.write ??
|
|
12
|
+
const write = opts.write ??
|
|
13
|
+
((s) => {
|
|
14
|
+
process.stdout.write(s);
|
|
15
|
+
});
|
|
13
16
|
const reduced = opts.reducedMotion ?? getCapabilities().reducedMotion;
|
|
14
17
|
const frames = pickIcon('u', 'a') === 'u' ? FRAMES_UNICODE : FRAMES_ASCII;
|
|
15
18
|
let current = msg;
|
|
16
19
|
let timer = null;
|
|
17
20
|
let i = 0;
|
|
18
|
-
const clearLine = () =>
|
|
21
|
+
const clearLine = () => {
|
|
22
|
+
write(ansi.cursorToCol0 + ansi.eraseDown);
|
|
23
|
+
};
|
|
19
24
|
const paint = () => {
|
|
20
25
|
clearLine();
|
|
21
|
-
write(renderSpinnerFrame(frames
|
|
26
|
+
write(renderSpinnerFrame(frames.at(i % frames.length) ?? '|', current));
|
|
22
27
|
i++;
|
|
23
28
|
};
|
|
24
29
|
if (!reduced) {
|
|
@@ -40,8 +45,14 @@ export function startSpinner(msg = '', opts = {}) {
|
|
|
40
45
|
write(line + '\n');
|
|
41
46
|
};
|
|
42
47
|
return {
|
|
43
|
-
message: (m) => {
|
|
44
|
-
|
|
45
|
-
|
|
48
|
+
message: (m) => {
|
|
49
|
+
current = m;
|
|
50
|
+
},
|
|
51
|
+
stop: (m) => {
|
|
52
|
+
finish(m ? renderMessage('success', m) : null);
|
|
53
|
+
},
|
|
54
|
+
error: (m) => {
|
|
55
|
+
finish(m ? renderMessage('error', m) : null);
|
|
56
|
+
},
|
|
46
57
|
};
|
|
47
58
|
}
|
|
@@ -2,28 +2,34 @@ import { glyph, type, space } from '../theme.js';
|
|
|
2
2
|
import { startRawInput } from './input-session.js';
|
|
3
3
|
import { setVimKeysActive } from '../vim-keys.js';
|
|
4
4
|
import { createPainter } from './painter.js';
|
|
5
|
-
import { visualWidth, wrapAnsiToVisualWidth } from '../text.js';
|
|
5
|
+
import { visualWidth, truncateStart, wrapAnsiToVisualWidth } from '../text.js';
|
|
6
|
+
import { pickIcon } from '../icons.js';
|
|
7
|
+
const GRAPHEME_SEGMENTER = new Intl.Segmenter(undefined, { granularity: 'grapheme' });
|
|
8
|
+
function graphemes(value) {
|
|
9
|
+
return Array.from(GRAPHEME_SEGMENTER.segment(value), ({ segment }) => segment);
|
|
10
|
+
}
|
|
6
11
|
export function parseInputData(data) {
|
|
7
12
|
const s = data.toString();
|
|
8
13
|
if (s === '\r' || s === '\n')
|
|
9
14
|
return { type: 'enter' };
|
|
10
15
|
if (s === '\x03')
|
|
11
|
-
return { type: 'cancel' };
|
|
16
|
+
return { type: 'cancel' };
|
|
12
17
|
if (s === '\x1b')
|
|
13
|
-
return { type: 'cancel' };
|
|
18
|
+
return { type: 'cancel' };
|
|
14
19
|
if (s === '\x7f' || s === '\b')
|
|
15
20
|
return { type: 'backspace' };
|
|
16
21
|
if (s.startsWith('\x1b'))
|
|
17
|
-
return { type: 'none' };
|
|
18
|
-
|
|
19
|
-
|
|
22
|
+
return { type: 'none' };
|
|
23
|
+
const text = graphemes(s)
|
|
24
|
+
.filter((ch) => ch >= ' ' && ch !== '\x7f')
|
|
25
|
+
.join('');
|
|
20
26
|
return text.length > 0 ? { type: 'char', ch: text } : { type: 'none' };
|
|
21
27
|
}
|
|
22
28
|
export function applyInputEvent(value, ev) {
|
|
23
29
|
if (ev.type === 'char')
|
|
24
30
|
return value + ev.ch;
|
|
25
31
|
if (ev.type === 'backspace')
|
|
26
|
-
return
|
|
32
|
+
return graphemes(value).slice(0, -1).join('');
|
|
27
33
|
return value;
|
|
28
34
|
}
|
|
29
35
|
export function renderInput(opts) {
|
|
@@ -32,21 +38,21 @@ export function renderInput(opts) {
|
|
|
32
38
|
: Number.POSITIVE_INFINITY;
|
|
33
39
|
const indent = visualWidth(space.indent) < width ? space.indent : '';
|
|
34
40
|
const messageWidth = Math.max(1, width - visualWidth(indent));
|
|
35
|
-
const messageLines = wrapAnsiToVisualWidth(type.label(opts.message), messageWidth)
|
|
36
|
-
.map((line) => `${indent}${line}`);
|
|
41
|
+
const messageLines = wrapAnsiToVisualWidth(type.label(opts.message), messageWidth).map((line) => `${indent}${line}`);
|
|
37
42
|
const visibleValue = opts.secret
|
|
38
|
-
? (opts.mask ?? '*').repeat(
|
|
43
|
+
? (opts.mask ?? '*').repeat(graphemes(opts.value).length)
|
|
39
44
|
: opts.value;
|
|
40
|
-
const shown = opts.value.length > 0
|
|
41
|
-
? type.body(visibleValue)
|
|
42
|
-
: type.hint(opts.placeholder ?? '');
|
|
43
45
|
const cursor = type.active(glyph.cursor());
|
|
44
46
|
const prefixes = [`${space.indent}${cursor} `, `${cursor} `, cursor, ''];
|
|
45
47
|
const prefix = prefixes.find((candidate) => visualWidth(candidate) < width) ?? '';
|
|
46
48
|
const continuation = ' '.repeat(visualWidth(prefix));
|
|
47
49
|
const inputWidth = Math.max(1, width - visualWidth(prefix));
|
|
48
|
-
const
|
|
49
|
-
.
|
|
50
|
+
const shown = opts.value.length > 0
|
|
51
|
+
? type.body(truncateStart(visibleValue, inputWidth, pickIcon('…', '<')))
|
|
52
|
+
: type.hint(opts.placeholder ?? '');
|
|
53
|
+
const inputLines = opts.value.length > 0
|
|
54
|
+
? [`${prefix}${shown}`]
|
|
55
|
+
: wrapAnsiToVisualWidth(shown, inputWidth).map((line, index) => `${index === 0 ? prefix : continuation}${line}`);
|
|
50
56
|
return [...messageLines, ...inputLines].join('\n');
|
|
51
57
|
}
|
|
52
58
|
export function runTextInput(config) {
|
|
@@ -55,9 +61,9 @@ export function runTextInput(config) {
|
|
|
55
61
|
const frame = () => renderInput({
|
|
56
62
|
message: config.message,
|
|
57
63
|
value,
|
|
58
|
-
placeholder: config.placeholder,
|
|
59
|
-
secret: config.secret,
|
|
60
|
-
mask: config.mask,
|
|
64
|
+
...(config.placeholder === undefined ? {} : { placeholder: config.placeholder }),
|
|
65
|
+
...(config.secret === undefined ? {} : { secret: config.secret }),
|
|
66
|
+
...(config.mask === undefined ? {} : { mask: config.mask }),
|
|
61
67
|
});
|
|
62
68
|
const paint = createPainter(frame);
|
|
63
69
|
const onData = (data) => {
|
package/dist/core/icons.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { resolveIconMode } from '../config/preferences.js';
|
|
2
2
|
function localeSupportsUnicode() {
|
|
3
|
-
const locale = `${process.env['LC_ALL']
|
|
3
|
+
const locale = `${process.env['LC_ALL'] ?? ''} ${process.env['LANG'] ?? ''}`.toLowerCase();
|
|
4
4
|
return locale.includes('utf-8') || locale.includes('utf8');
|
|
5
5
|
}
|
|
6
6
|
let cachedUseUnicode = null;
|
|
@@ -16,7 +16,7 @@ export function useUnicodeIcons() {
|
|
|
16
16
|
cachedUseUnicode = true;
|
|
17
17
|
return true;
|
|
18
18
|
}
|
|
19
|
-
const term = (process.env['TERM']
|
|
19
|
+
const term = (process.env['TERM'] ?? '').toLowerCase();
|
|
20
20
|
if (!process.stdout.isTTY || term === 'dumb') {
|
|
21
21
|
cachedUseUnicode = false;
|
|
22
22
|
return false;
|
package/dist/core/logo.js
CHANGED
|
@@ -6,6 +6,7 @@ import { useUnicodeIcons } from './icons.js';
|
|
|
6
6
|
import { APP_INFO } from '../config/data.js';
|
|
7
7
|
import { typeReveal, materializeBraille } from './motion.js';
|
|
8
8
|
import { brandGradient as brand } from './theme.js';
|
|
9
|
+
import { visualWidth } from './text.js';
|
|
9
10
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
10
11
|
const TAGLINE = 'To be at the intersection of technology and liberal arts.';
|
|
11
12
|
function readArt(file) {
|
|
@@ -22,8 +23,8 @@ const LOGO_TIERS = [
|
|
|
22
23
|
{ file: 'ca-dotmatrix-small.txt', minCols: 0, minRows: 0 },
|
|
23
24
|
];
|
|
24
25
|
function dotmatrixFile() {
|
|
25
|
-
const cols = process.stdout.columns
|
|
26
|
-
const rows = process.stdout.rows
|
|
26
|
+
const cols = process.stdout.columns;
|
|
27
|
+
const rows = process.stdout.rows;
|
|
27
28
|
const tier = LOGO_TIERS.find((t) => cols >= t.minCols && rows >= t.minRows);
|
|
28
29
|
return tier?.file ?? 'ca-dotmatrix-small.txt';
|
|
29
30
|
}
|
|
@@ -33,12 +34,21 @@ function paint(text, color) {
|
|
|
33
34
|
const fn = brand;
|
|
34
35
|
return typeof fn.multiline === 'function'
|
|
35
36
|
? fn.multiline(text)
|
|
36
|
-
: text
|
|
37
|
+
: text
|
|
38
|
+
.split('\n')
|
|
39
|
+
.map((line) => brand(line))
|
|
40
|
+
.join('\n');
|
|
37
41
|
}
|
|
38
42
|
function loadArt() {
|
|
39
43
|
const art = useUnicodeIcons() ? readArt(dotmatrixFile()) : readArt('ascii-logo.txt');
|
|
40
44
|
return art ?? 'NBTCA';
|
|
41
45
|
}
|
|
46
|
+
export function startupFitsTerminal(rows, cols, art) {
|
|
47
|
+
const lines = art.split('\n');
|
|
48
|
+
const fitsRows = rows === undefined || rows >= lines.length + 5;
|
|
49
|
+
const fitsCols = cols === undefined || lines.every((line) => visualWidth(line) <= cols);
|
|
50
|
+
return fitsRows && fitsCols;
|
|
51
|
+
}
|
|
42
52
|
export function buildLogoLines() {
|
|
43
53
|
const color = !process.env['NO_COLOR'];
|
|
44
54
|
const paintedArt = paint(loadArt(), color).split('\n');
|
|
@@ -54,8 +64,16 @@ export function buildLogoLines() {
|
|
|
54
64
|
export async function runStartup() {
|
|
55
65
|
if (!process.stdout.isTTY)
|
|
56
66
|
return;
|
|
67
|
+
const art = loadArt();
|
|
68
|
+
if (!startupFitsTerminal(process.stdout.rows, process.stdout.columns, art))
|
|
69
|
+
return;
|
|
57
70
|
const color = !process.env['NO_COLOR'];
|
|
58
71
|
process.stdout.write('\n');
|
|
59
|
-
await materializeBraille(
|
|
60
|
-
await typeReveal([
|
|
72
|
+
await materializeBraille(art, (s) => paint(s, color));
|
|
73
|
+
await typeReveal([
|
|
74
|
+
'',
|
|
75
|
+
color ? brand(TAGLINE) : TAGLINE,
|
|
76
|
+
chalk.dim(`@nbtca/prompt v${APP_INFO.version}`),
|
|
77
|
+
'',
|
|
78
|
+
]);
|
|
61
79
|
}
|
package/dist/core/motion.js
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
import { getCapabilities } from './capabilities.js';
|
|
2
2
|
import { ansi } from './canvas.js';
|
|
3
|
+
const GRAPHEME_SEGMENTER = new Intl.Segmenter(undefined, { granularity: 'grapheme' });
|
|
3
4
|
export function sleep(ms) {
|
|
4
5
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
5
6
|
}
|
|
6
7
|
export async function typeReveal(lines, opts = {}) {
|
|
7
|
-
const write = opts.write ??
|
|
8
|
+
const write = opts.write ??
|
|
9
|
+
((s) => {
|
|
10
|
+
process.stdout.write(s);
|
|
11
|
+
});
|
|
8
12
|
const reduced = opts.reducedMotion ?? getCapabilities().reducedMotion;
|
|
9
13
|
if (reduced) {
|
|
10
14
|
write(lines.join('\n') + '\n');
|
|
@@ -21,31 +25,29 @@ function brailleMask(ch) {
|
|
|
21
25
|
const code = ch.codePointAt(0) ?? 0;
|
|
22
26
|
return code >= BRAILLE_BASE && code <= BRAILLE_BASE + 0xff ? code - BRAILLE_BASE : -1;
|
|
23
27
|
}
|
|
24
|
-
/**
|
|
25
|
-
* Reveals a block of braille dot-matrix art by assembling it dot-by-dot in
|
|
26
|
-
* scattered order, rather than popping it in line by line -- a reveal that
|
|
27
|
-
* matches what the art actually is (an addressable grid of dots) instead of
|
|
28
|
-
* reusing the plain-text typewriter effect built for prose. Non-braille
|
|
29
|
-
* characters (spaces, stray ASCII) pass through unchanged on every frame.
|
|
30
|
-
*/
|
|
31
28
|
export async function materializeBraille(art, paint, opts = {}) {
|
|
32
|
-
const write = opts.write ??
|
|
29
|
+
const write = opts.write ??
|
|
30
|
+
((s) => {
|
|
31
|
+
process.stdout.write(s);
|
|
32
|
+
});
|
|
33
33
|
const reduced = opts.reducedMotion ?? getCapabilities().reducedMotion;
|
|
34
34
|
const lines = art.split('\n');
|
|
35
|
-
const charGrid = lines.map((line) =>
|
|
35
|
+
const charGrid = lines.map((line) => Array.from(GRAPHEME_SEGMENTER.segment(line), ({ segment }) => segment));
|
|
36
36
|
const maskGrid = charGrid.map((row) => row.map(brailleMask));
|
|
37
37
|
if (reduced || !maskGrid.some((row) => row.some((m) => m > 0))) {
|
|
38
38
|
write(paint(art) + '\n');
|
|
39
39
|
return;
|
|
40
40
|
}
|
|
41
41
|
const dots = [];
|
|
42
|
-
maskGrid.forEach((row, r) =>
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
42
|
+
maskGrid.forEach((row, r) => {
|
|
43
|
+
row.forEach((mask, c) => {
|
|
44
|
+
if (mask <= 0)
|
|
45
|
+
return;
|
|
46
|
+
for (let bit = 0; bit < 8; bit++)
|
|
47
|
+
if (mask & (1 << bit))
|
|
48
|
+
dots.push([r, c, bit]);
|
|
49
|
+
});
|
|
50
|
+
});
|
|
49
51
|
const rand = opts.random ?? Math.random;
|
|
50
52
|
for (let i = dots.length - 1; i > 0; i--) {
|
|
51
53
|
const j = Math.floor(rand() * (i + 1));
|
|
@@ -56,12 +58,16 @@ export async function materializeBraille(art, paint, opts = {}) {
|
|
|
56
58
|
}
|
|
57
59
|
}
|
|
58
60
|
const acc = maskGrid.map((row) => row.map(() => 0));
|
|
59
|
-
const renderFrame = () => charGrid
|
|
61
|
+
const renderFrame = () => charGrid
|
|
62
|
+
.map((row, r) => row
|
|
63
|
+
.map((original, c) => {
|
|
60
64
|
const mask = maskGrid[r]?.[c] ?? -1;
|
|
61
65
|
if (mask <= 0)
|
|
62
66
|
return original;
|
|
63
67
|
return String.fromCodePoint(BRAILLE_BASE + (acc[r]?.[c] ?? 0));
|
|
64
|
-
})
|
|
68
|
+
})
|
|
69
|
+
.join(''))
|
|
70
|
+
.join('\n');
|
|
65
71
|
const frameCount = Math.max(1, opts.frames ?? 12);
|
|
66
72
|
const frameMs = opts.frameMs ?? 35;
|
|
67
73
|
let shown = 0;
|