utilium 3.5.1 → 3.6.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/dom.js +1 -0
- package/dist/shell.d.ts +35 -25
- package/dist/shell.js +85 -26
- package/package.json +4 -13
package/dist/dom.js
CHANGED
package/dist/shell.d.ts
CHANGED
|
@@ -1,46 +1,56 @@
|
|
|
1
|
-
import type { Terminal } from '@xterm/xterm';
|
|
2
1
|
/**
|
|
3
2
|
* Parse a line into arguments.
|
|
4
3
|
* Supports single and double quoted strings as well as backslash escaping.
|
|
5
4
|
*/
|
|
6
5
|
export declare function splitIntoArgs(input: string): string[];
|
|
6
|
+
/** Where a shell reads keystrokes from, i.e. the parts of `NodeJS.ReadStream` it uses. */
|
|
7
|
+
export interface ShellInput {
|
|
8
|
+
on(event: 'data', listener: (chunk: string | Uint8Array) => void): unknown;
|
|
9
|
+
off?(event: 'data', listener: (chunk: string | Uint8Array) => void): unknown;
|
|
10
|
+
/** Hand over keystrokes as they are typed, instead of a line at a time with editing and echo */
|
|
11
|
+
setRawMode?(raw: boolean): unknown;
|
|
12
|
+
resume?(): unknown;
|
|
13
|
+
pause?(): unknown;
|
|
14
|
+
readonly isTTY?: boolean;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Where a shell draws, i.e. the parts of `NodeJS.WriteStream` it uses.
|
|
18
|
+
*/
|
|
19
|
+
export interface ShellOutput {
|
|
20
|
+
write(data: string): unknown;
|
|
21
|
+
readonly columns?: number;
|
|
22
|
+
readonly rows?: number;
|
|
23
|
+
}
|
|
7
24
|
export interface ShellOptions {
|
|
8
25
|
/**
|
|
9
|
-
* The
|
|
10
|
-
|
|
11
|
-
terminal: Terminal;
|
|
12
|
-
/**
|
|
13
|
-
* The prompt to use, can be a getter.
|
|
26
|
+
* The stream keystrokes are read from, e.g. `process.stdin`.
|
|
27
|
+
* The shell does its own line editing, so it is put into raw mode.
|
|
14
28
|
*/
|
|
29
|
+
stdin: ShellInput;
|
|
30
|
+
/** The stream the prompt and the line being edited are written to, e.g. `process.stdout`. */
|
|
31
|
+
stdout: ShellOutput;
|
|
32
|
+
/** The prompt to use, can be a getter. */
|
|
15
33
|
readonly prompt?: string;
|
|
16
|
-
/**
|
|
17
|
-
* The length to use for the prompt. Useful if escape sequences are used in the prompt.
|
|
18
|
-
*/
|
|
34
|
+
/** The length to use for the prompt. Useful if escape sequences are used in the prompt. */
|
|
19
35
|
readonly promptLength?: number;
|
|
20
|
-
/**
|
|
21
|
-
* The handler for when a line is parsed
|
|
22
|
-
*/
|
|
36
|
+
/** The handler for when a line is parsed */
|
|
23
37
|
onLine?(this: void, line: string): unknown;
|
|
24
38
|
}
|
|
25
39
|
export interface ShellContext extends Required<ShellOptions> {
|
|
26
|
-
/**
|
|
27
|
-
* The input currently being shown
|
|
28
|
-
*/
|
|
40
|
+
/** The input currently being shown */
|
|
29
41
|
input: string;
|
|
30
|
-
/**
|
|
31
|
-
|
|
32
|
-
|
|
42
|
+
/** Where the cursor is in `input`. */
|
|
43
|
+
cursor: number;
|
|
44
|
+
/** The index for which input is being shown */
|
|
33
45
|
index: number;
|
|
34
|
-
/**
|
|
35
|
-
* The current, uncached input
|
|
36
|
-
*/
|
|
46
|
+
/** The current, uncached input */
|
|
37
47
|
currentInput: string;
|
|
38
|
-
/**
|
|
39
|
-
* array of previous inputs
|
|
40
|
-
*/
|
|
48
|
+
/** array of previous inputs */
|
|
41
49
|
inputs: string[];
|
|
50
|
+
/** Stop reading from `stdin` and put it back the way it was found */
|
|
51
|
+
close(): void;
|
|
42
52
|
}
|
|
43
53
|
/**
|
|
44
|
-
* A simple wrapper for
|
|
54
|
+
* A simple wrapper for a pair of streams that makes implementing shells easier.
|
|
45
55
|
*/
|
|
46
56
|
export declare function createShell(options: ShellOptions): ShellContext;
|
package/dist/shell.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
// SPDX-License-Identifier: LGPL-3.0-or-later
|
|
2
|
+
// Copyright (c) 2026 James Prevett
|
|
1
3
|
/**
|
|
2
4
|
* Parse a line into arguments.
|
|
3
5
|
* Supports single and double quoted strings as well as backslash escaping.
|
|
@@ -35,75 +37,116 @@ export function splitIntoArgs(input) {
|
|
|
35
37
|
args.push(current);
|
|
36
38
|
return args;
|
|
37
39
|
}
|
|
38
|
-
|
|
40
|
+
/**
|
|
41
|
+
* One key press: a CSI or SS3 escape sequence, or a single character.
|
|
42
|
+
* A read can come back with more than one key in it, e.g. when text is pasted,
|
|
43
|
+
* and an escape sequence must not be taken for the characters it is made of.
|
|
44
|
+
*/
|
|
45
|
+
// eslint-disable-next-line no-control-regex
|
|
46
|
+
const keyPattern = /\x1b\[[0-?]*[ -\/]*[@-~]|\x1bO[@-~]|[\s\S]/gu;
|
|
47
|
+
async function handleKey($, key) {
|
|
39
48
|
if ($.index == -1) {
|
|
40
49
|
$.currentInput = $.input;
|
|
41
50
|
}
|
|
42
|
-
|
|
43
|
-
|
|
51
|
+
/** Redraw the whole line, for when the input is replaced rather than edited */
|
|
52
|
+
function redraw() {
|
|
53
|
+
$.stdout.write('\x1b[2K\r' + $.prompt + $.input);
|
|
54
|
+
$.cursor = $.input.length;
|
|
44
55
|
}
|
|
45
|
-
|
|
46
|
-
switch (data) {
|
|
56
|
+
switch (key) {
|
|
47
57
|
case 'ArrowUp':
|
|
48
58
|
case '\x1b[A':
|
|
49
|
-
clear();
|
|
50
59
|
if ($.index < $.inputs.length - 1) {
|
|
51
60
|
$.input = $.inputs[++$.index];
|
|
52
61
|
}
|
|
53
|
-
|
|
62
|
+
redraw();
|
|
54
63
|
break;
|
|
55
64
|
case 'ArrowDown':
|
|
56
65
|
case '\x1b[B':
|
|
57
|
-
clear();
|
|
58
66
|
if ($.index >= 0) {
|
|
59
67
|
$.input = $.index-- == 0 ? $.currentInput : $.inputs[$.index];
|
|
60
68
|
}
|
|
61
|
-
|
|
69
|
+
redraw();
|
|
62
70
|
break;
|
|
63
71
|
case '\x1b[D':
|
|
64
|
-
if (
|
|
65
|
-
$.
|
|
72
|
+
if ($.cursor > 0) {
|
|
73
|
+
$.cursor--;
|
|
74
|
+
$.stdout.write(key);
|
|
66
75
|
}
|
|
67
76
|
break;
|
|
68
77
|
case '\x1b[C':
|
|
69
|
-
if (
|
|
70
|
-
$.
|
|
78
|
+
if ($.cursor < $.input.length) {
|
|
79
|
+
$.cursor++;
|
|
80
|
+
$.stdout.write(key);
|
|
71
81
|
}
|
|
72
82
|
break;
|
|
73
83
|
case '\x1b[F':
|
|
74
|
-
$.
|
|
84
|
+
$.cursor = $.input.length;
|
|
85
|
+
$.stdout.write(`\x1b[${$.promptLength + $.cursor + 1}G`);
|
|
75
86
|
break;
|
|
76
87
|
case '\x1b[H':
|
|
77
|
-
$.
|
|
88
|
+
$.cursor = 0;
|
|
89
|
+
$.stdout.write(`\x1b[${$.promptLength + 1}G`);
|
|
78
90
|
break;
|
|
79
91
|
case '\x7f':
|
|
80
|
-
if (
|
|
92
|
+
if ($.cursor <= 0) {
|
|
81
93
|
return;
|
|
82
94
|
}
|
|
83
|
-
$.
|
|
84
|
-
$.
|
|
95
|
+
$.input = $.input.slice(0, $.cursor - 1) + $.input.slice($.cursor);
|
|
96
|
+
$.cursor--;
|
|
97
|
+
$.stdout.write('\b\x1b[P');
|
|
98
|
+
break;
|
|
99
|
+
case '\x1b[3~':
|
|
100
|
+
if ($.cursor >= $.input.length) {
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
$.input = $.input.slice(0, $.cursor) + $.input.slice($.cursor + 1);
|
|
104
|
+
$.stdout.write('\x1b[P');
|
|
105
|
+
break;
|
|
106
|
+
case '\x0c': {
|
|
107
|
+
$.stdout.write('\x1b[2J\x1b[H' + $.prompt + $.input);
|
|
108
|
+
const back = $.input.length - $.cursor;
|
|
109
|
+
if (back)
|
|
110
|
+
$.stdout.write(`\x1b[${back}D`);
|
|
111
|
+
break;
|
|
112
|
+
}
|
|
113
|
+
case '\x03':
|
|
114
|
+
$.stdout.write('^C\r\n');
|
|
115
|
+
$.index = -1;
|
|
116
|
+
$.input = '';
|
|
117
|
+
$.currentInput = '';
|
|
118
|
+
$.cursor = 0;
|
|
119
|
+
$.stdout.write($.prompt);
|
|
85
120
|
break;
|
|
86
121
|
case '\r':
|
|
87
122
|
if ($.input != $.inputs[0]) {
|
|
88
123
|
$.inputs.unshift($.input);
|
|
89
124
|
}
|
|
90
|
-
$.
|
|
125
|
+
$.stdout.write('\r\n');
|
|
91
126
|
await $.onLine($.input);
|
|
92
127
|
$.index = -1;
|
|
93
128
|
$.input = '';
|
|
94
|
-
$.
|
|
129
|
+
$.cursor = 0;
|
|
130
|
+
$.stdout.write($.prompt);
|
|
95
131
|
break;
|
|
96
|
-
default:
|
|
97
|
-
|
|
98
|
-
|
|
132
|
+
default: {
|
|
133
|
+
if (key.startsWith('\x1b') || key < ' ')
|
|
134
|
+
return;
|
|
135
|
+
$.input = $.input.slice(0, $.cursor) + key + $.input.slice($.cursor);
|
|
136
|
+
$.cursor += key.length;
|
|
137
|
+
const rest = $.input.slice($.cursor);
|
|
138
|
+
$.stdout.write(key + rest + (rest ? `\x1b[${rest.length}D` : ''));
|
|
139
|
+
}
|
|
99
140
|
}
|
|
100
141
|
}
|
|
101
142
|
/**
|
|
102
|
-
* A simple wrapper for
|
|
143
|
+
* A simple wrapper for a pair of streams that makes implementing shells easier.
|
|
103
144
|
*/
|
|
104
145
|
export function createShell(options) {
|
|
146
|
+
const decoder = new TextDecoder();
|
|
105
147
|
const context = {
|
|
106
|
-
|
|
148
|
+
stdin: options.stdin,
|
|
149
|
+
stdout: options.stdout,
|
|
107
150
|
get prompt() {
|
|
108
151
|
return options.prompt ?? '';
|
|
109
152
|
},
|
|
@@ -112,10 +155,26 @@ export function createShell(options) {
|
|
|
112
155
|
},
|
|
113
156
|
onLine: options.onLine ?? (() => { }),
|
|
114
157
|
input: '',
|
|
158
|
+
cursor: 0,
|
|
115
159
|
index: -1,
|
|
116
160
|
currentInput: '',
|
|
117
161
|
inputs: [],
|
|
162
|
+
close() {
|
|
163
|
+
options.stdin.off?.('data', listener);
|
|
164
|
+
options.stdin.setRawMode?.(false);
|
|
165
|
+
options.stdin.pause?.();
|
|
166
|
+
},
|
|
167
|
+
};
|
|
168
|
+
let pending = Promise.resolve();
|
|
169
|
+
const listener = (chunk) => {
|
|
170
|
+
const data = typeof chunk == 'string' ? chunk : decoder.decode(chunk, { stream: true });
|
|
171
|
+
pending = pending.then(async () => {
|
|
172
|
+
for (const [key] of data.matchAll(keyPattern))
|
|
173
|
+
await handleKey(context, key);
|
|
174
|
+
});
|
|
118
175
|
};
|
|
119
|
-
options.
|
|
176
|
+
options.stdin.setRawMode?.(true);
|
|
177
|
+
options.stdin.on('data', listener);
|
|
178
|
+
options.stdin.resume?.();
|
|
120
179
|
return context;
|
|
121
180
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "utilium",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.6.1",
|
|
4
4
|
"description": "Typescript utilities",
|
|
5
5
|
"funding": {
|
|
6
6
|
"type": "individual",
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"./eslint": "./eslint.shared.js"
|
|
16
16
|
},
|
|
17
17
|
"bin": {
|
|
18
|
-
"lice": "
|
|
18
|
+
"lice": "scripts/lice.js"
|
|
19
19
|
},
|
|
20
20
|
"files": [
|
|
21
21
|
"dist",
|
|
@@ -42,27 +42,18 @@
|
|
|
42
42
|
"homepage": "https://github.com/james-pre/utilium#readme",
|
|
43
43
|
"devDependencies": {
|
|
44
44
|
"@eslint/js": "^10.0.1",
|
|
45
|
-
"@types/node": "^
|
|
45
|
+
"@types/node": "^26.0.0",
|
|
46
46
|
"eslint": "^10.1.0",
|
|
47
47
|
"globals": "^17.8.0",
|
|
48
48
|
"prettier": "^3.2.5",
|
|
49
49
|
"tsx": "^4.19.1",
|
|
50
50
|
"typedoc": "^0.28.18",
|
|
51
51
|
"typescript": "^6.0.0",
|
|
52
|
-
"typescript-eslint": "^8.58.0"
|
|
53
|
-
"@xterm/xterm": "^6.0.0"
|
|
52
|
+
"typescript-eslint": "^8.58.0"
|
|
54
53
|
},
|
|
55
54
|
"dependencies": {
|
|
56
55
|
"eventemitter3": "^5.0.1"
|
|
57
56
|
},
|
|
58
|
-
"peerDependencies": {
|
|
59
|
-
"@xterm/xterm": "^6.0.0"
|
|
60
|
-
},
|
|
61
|
-
"peerDependenciesMeta": {
|
|
62
|
-
"@xterm/xterm": {
|
|
63
|
-
"optional": true
|
|
64
|
-
}
|
|
65
|
-
},
|
|
66
57
|
"engines": {
|
|
67
58
|
"node": ">=22.0.0"
|
|
68
59
|
},
|