utilium 3.5.1 → 3.6.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/dist/dom.js CHANGED
@@ -1,6 +1,7 @@
1
1
  // SPDX-License-Identifier: LGPL-3.0-or-later
2
2
  // Copyright (c) 2025 James Prevett
3
3
  /* eslint-disable @typescript-eslint/no-unused-expressions */
4
+ /// <reference lib="dom" />
4
5
  /**
5
6
  * Upload a file
6
7
  * @todo use Promise.withResolvers
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 terminal associated with the context
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
- * The index for which input is being shown
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 xterm.js that makes implementing shells easier.
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,94 @@ export function splitIntoArgs(input) {
35
37
  args.push(current);
36
38
  return args;
37
39
  }
38
- async function handleData($, data) {
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
- function clear() {
43
- $.terminal.write('\x1b[2K\r' + $.prompt);
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
- const x = $.terminal.buffer.active.cursorX - $.promptLength;
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
- $.terminal.write($.input);
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
- $.terminal.write($.input);
69
+ redraw();
62
70
  break;
63
71
  case '\x1b[D':
64
- if (x > 0) {
65
- $.terminal.write(data);
72
+ if ($.cursor > 0) {
73
+ $.cursor--;
74
+ $.stdout.write(key);
66
75
  }
67
76
  break;
68
77
  case '\x1b[C':
69
- if (x < $.input.length) {
70
- $.terminal.write(data);
78
+ if ($.cursor < $.input.length) {
79
+ $.cursor++;
80
+ $.stdout.write(key);
71
81
  }
72
82
  break;
73
83
  case '\x1b[F':
74
- $.terminal.write(`\x1b[${$.promptLength + $.input.length + 1}G`);
84
+ $.cursor = $.input.length;
85
+ $.stdout.write(`\x1b[${$.promptLength + $.cursor + 1}G`);
75
86
  break;
76
87
  case '\x1b[H':
77
- $.terminal.write(`\x1b[${$.promptLength + 1}G`);
88
+ $.cursor = 0;
89
+ $.stdout.write(`\x1b[${$.promptLength + 1}G`);
78
90
  break;
79
91
  case '\x7f':
80
- if (x <= 0) {
92
+ if ($.cursor <= 0) {
81
93
  return;
82
94
  }
83
- $.terminal.write('\b\x1b[P');
84
- $.input = $.input.slice(0, x - 1) + $.input.slice(x);
95
+ $.input = $.input.slice(0, $.cursor - 1) + $.input.slice($.cursor);
96
+ $.cursor--;
97
+ $.stdout.write('\b\x1b[P');
85
98
  break;
86
99
  case '\r':
87
100
  if ($.input != $.inputs[0]) {
88
101
  $.inputs.unshift($.input);
89
102
  }
90
- $.terminal.write('\r\n');
103
+ $.stdout.write('\r\n');
91
104
  await $.onLine($.input);
92
105
  $.index = -1;
93
106
  $.input = '';
94
- $.terminal.write($.prompt);
107
+ $.cursor = 0;
108
+ $.stdout.write($.prompt);
95
109
  break;
96
- default:
97
- $.terminal.write(data);
98
- $.input = $.input.slice(0, x) + data + $.input.slice(x);
110
+ default: {
111
+ if (key.startsWith('\x1b') || key < ' ')
112
+ return;
113
+ $.input = $.input.slice(0, $.cursor) + key + $.input.slice($.cursor);
114
+ $.cursor += key.length;
115
+ const rest = $.input.slice($.cursor);
116
+ $.stdout.write(key + rest + (rest ? `\x1b[${rest.length}D` : ''));
117
+ }
99
118
  }
100
119
  }
101
120
  /**
102
- * A simple wrapper for xterm.js that makes implementing shells easier.
121
+ * A simple wrapper for a pair of streams that makes implementing shells easier.
103
122
  */
104
123
  export function createShell(options) {
124
+ const decoder = new TextDecoder();
105
125
  const context = {
106
- terminal: options.terminal,
126
+ stdin: options.stdin,
127
+ stdout: options.stdout,
107
128
  get prompt() {
108
129
  return options.prompt ?? '';
109
130
  },
@@ -112,10 +133,26 @@ export function createShell(options) {
112
133
  },
113
134
  onLine: options.onLine ?? (() => { }),
114
135
  input: '',
136
+ cursor: 0,
115
137
  index: -1,
116
138
  currentInput: '',
117
139
  inputs: [],
140
+ close() {
141
+ options.stdin.off?.('data', listener);
142
+ options.stdin.setRawMode?.(false);
143
+ options.stdin.pause?.();
144
+ },
145
+ };
146
+ let pending = Promise.resolve();
147
+ const listener = (chunk) => {
148
+ const data = typeof chunk == 'string' ? chunk : decoder.decode(chunk, { stream: true });
149
+ pending = pending.then(async () => {
150
+ for (const [key] of data.matchAll(keyPattern))
151
+ await handleKey(context, key);
152
+ });
118
153
  };
119
- options.terminal.onData(data => handleData(context, data));
154
+ options.stdin.setRawMode?.(true);
155
+ options.stdin.on('data', listener);
156
+ options.stdin.resume?.();
120
157
  return context;
121
158
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "utilium",
3
- "version": "3.5.1",
3
+ "version": "3.6.0",
4
4
  "description": "Typescript utilities",
5
5
  "funding": {
6
6
  "type": "individual",
@@ -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": "^24.7.0",
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
  },