@wasm-idle/terminal 1.0.0-next.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 +24 -0
- package/dist/Terminal.svelte +520 -0
- package/dist/Terminal.svelte.d.ts +22 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +5 -0
- package/dist/plugin/index.d.ts +9 -0
- package/dist/plugin/index.js +20 -0
- package/dist/theme.d.ts +49 -0
- package/dist/theme.js +49 -0
- package/dist/types.d.ts +54 -0
- package/dist/types.js +1 -0
- package/package.json +56 -0
package/README.md
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# @wasm-idle/terminal
|
|
2
|
+
|
|
3
|
+
Optional Svelte/xterm terminal UI for a wasm-idle playground binding. Install it only in browser
|
|
4
|
+
applications that render the terminal:
|
|
5
|
+
|
|
6
|
+
```bash
|
|
7
|
+
pnpm add wasm-idle @wasm-idle/terminal svelte
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
```svelte
|
|
11
|
+
<script lang="ts">
|
|
12
|
+
import Terminal from '@wasm-idle/terminal';
|
|
13
|
+
import { createPlaygroundBinding } from 'wasm-idle';
|
|
14
|
+
|
|
15
|
+
const playground = createPlaygroundBinding({
|
|
16
|
+
rootUrl: 'https://cdn.example.com/wasm-idle'
|
|
17
|
+
});
|
|
18
|
+
</script>
|
|
19
|
+
|
|
20
|
+
<Terminal {playground} />
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Compiler and language-runtime payloads are not included in this package. Configure their external
|
|
24
|
+
URLs through the injected playground binding.
|
|
@@ -0,0 +1,520 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import {
|
|
3
|
+
createRuntimeAssetsKey,
|
|
4
|
+
phaseProgress,
|
|
5
|
+
progressBandsForLanguage,
|
|
6
|
+
type DebugCommand,
|
|
7
|
+
type DebugSessionEvent
|
|
8
|
+
} from '@wasm-idle/core';
|
|
9
|
+
import { onMount } from 'svelte';
|
|
10
|
+
import '@xterm/xterm/css/xterm.css';
|
|
11
|
+
import type { Terminal as TerminalType } from '@xterm/xterm';
|
|
12
|
+
import registerAllPlugins from './plugin/index.js';
|
|
13
|
+
import Theme from './theme.js';
|
|
14
|
+
import type {
|
|
15
|
+
BoundSandbox,
|
|
16
|
+
CompilerDiagnostic,
|
|
17
|
+
PlaygroundBinding,
|
|
18
|
+
TerminalControl,
|
|
19
|
+
TerminalExecutionOptions
|
|
20
|
+
} from './types.js';
|
|
21
|
+
|
|
22
|
+
interface Props {
|
|
23
|
+
dark?: boolean;
|
|
24
|
+
playground: PlaygroundBinding;
|
|
25
|
+
font?: string;
|
|
26
|
+
|
|
27
|
+
onload?: () => void;
|
|
28
|
+
onfinish?: () => void;
|
|
29
|
+
onkey?: (e: KeyboardEvent) => void;
|
|
30
|
+
ondebug?: (event: DebugSessionEvent) => void;
|
|
31
|
+
oncompilediagnostic?: (diagnostic: CompilerDiagnostic) => void;
|
|
32
|
+
onimage?: (payload: { mime: string; b64: string; ts?: number }) => void;
|
|
33
|
+
terminal?: TerminalControl;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
let {
|
|
37
|
+
dark = false,
|
|
38
|
+
playground,
|
|
39
|
+
font = "'D2 coding', monospace",
|
|
40
|
+
onload,
|
|
41
|
+
onfinish,
|
|
42
|
+
onkey,
|
|
43
|
+
ondebug,
|
|
44
|
+
oncompilediagnostic,
|
|
45
|
+
onimage,
|
|
46
|
+
terminal = $bindable()
|
|
47
|
+
}: Props = $props();
|
|
48
|
+
|
|
49
|
+
let ref = $state<HTMLElement>(),
|
|
50
|
+
clientWidth = $state(0),
|
|
51
|
+
clientHeight = $state(0),
|
|
52
|
+
term = $state<TerminalType>(),
|
|
53
|
+
debugOutput = $state(''),
|
|
54
|
+
finish = true,
|
|
55
|
+
input = '',
|
|
56
|
+
inputCursor = 0,
|
|
57
|
+
pendingSandboxInput: string[] = [],
|
|
58
|
+
pendingSandboxEof = false,
|
|
59
|
+
sandbox: BoundSandbox,
|
|
60
|
+
sandboxAcceptingInput = false,
|
|
61
|
+
first = true,
|
|
62
|
+
tc = 0,
|
|
63
|
+
plugin = $state(),
|
|
64
|
+
ll: string | null = null,
|
|
65
|
+
loadedRuntimeAssetsKey: string | undefined = undefined,
|
|
66
|
+
stopRequested = false;
|
|
67
|
+
|
|
68
|
+
function writeTerminalOutput(text: string) {
|
|
69
|
+
if (!text) return;
|
|
70
|
+
debugOutput += text;
|
|
71
|
+
term?.write(text);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function wait() {
|
|
75
|
+
return new Promise<void>((r) => {
|
|
76
|
+
const i = setInterval(() => {
|
|
77
|
+
if (term) {
|
|
78
|
+
clearInterval(i);
|
|
79
|
+
r();
|
|
80
|
+
}
|
|
81
|
+
}, 100);
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async function initSandbox(language: string) {
|
|
86
|
+
const currentPlayground = playground;
|
|
87
|
+
const currentRuntimeAssets = currentPlayground.runtimeAssets;
|
|
88
|
+
const currentRuntimeAssetsKey = createRuntimeAssetsKey(currentRuntimeAssets);
|
|
89
|
+
const requiresSandboxReset =
|
|
90
|
+
ll !== language || loadedRuntimeAssetsKey !== currentRuntimeAssetsKey;
|
|
91
|
+
let _tc = ++tc;
|
|
92
|
+
await wait();
|
|
93
|
+
sandboxAcceptingInput = false;
|
|
94
|
+
if (sandbox && requiresSandboxReset) await sandbox.clear();
|
|
95
|
+
input = '';
|
|
96
|
+
inputCursor = 0;
|
|
97
|
+
finish = false;
|
|
98
|
+
if (!sandbox || requiresSandboxReset) {
|
|
99
|
+
sandbox = await currentPlayground.load(language);
|
|
100
|
+
await sandbox.clear();
|
|
101
|
+
ll = language;
|
|
102
|
+
loadedRuntimeAssetsKey = currentRuntimeAssetsKey;
|
|
103
|
+
}
|
|
104
|
+
sandbox.image = onimage;
|
|
105
|
+
sandbox.ondebug = ondebug;
|
|
106
|
+
sandbox.oncompilerdiagnostic = oncompilediagnostic;
|
|
107
|
+
sandbox.output = (output: string) =>
|
|
108
|
+
_tc === tc && writeTerminalOutput(output.replaceAll('\n', '\r\n'));
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function flushPendingSandboxInput() {
|
|
112
|
+
if (pendingSandboxInput.length > 0) {
|
|
113
|
+
for (const pendingInput of pendingSandboxInput) {
|
|
114
|
+
sandbox.write?.(pendingInput);
|
|
115
|
+
}
|
|
116
|
+
pendingSandboxInput = [];
|
|
117
|
+
}
|
|
118
|
+
if (pendingSandboxEof) {
|
|
119
|
+
sandbox.eof?.();
|
|
120
|
+
pendingSandboxEof = false;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function submitSandboxEof() {
|
|
125
|
+
if (sandbox && sandboxAcceptingInput) sandbox.eof?.();
|
|
126
|
+
else pendingSandboxEof = true;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function runSandbox<T>(pr: Promise<T>, reportFinish = true) {
|
|
130
|
+
return pr
|
|
131
|
+
.then((x) => {
|
|
132
|
+
if (reportFinish) {
|
|
133
|
+
writeTerminalOutput(
|
|
134
|
+
`\r\nProcess finished after ${sandbox.elapse}ms\u001B[?25l`
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
return x;
|
|
138
|
+
})
|
|
139
|
+
.catch((msg) => {
|
|
140
|
+
if (stopRequested) return false;
|
|
141
|
+
writeTerminalOutput(`\r\n\x1B[1;3;31m${msg}\u001B[?25l`);
|
|
142
|
+
return false;
|
|
143
|
+
})
|
|
144
|
+
.finally(() => {
|
|
145
|
+
sandboxAcceptingInput = false;
|
|
146
|
+
stopRequested = false;
|
|
147
|
+
onfinish?.();
|
|
148
|
+
finish = true;
|
|
149
|
+
if (term) term.options.cursorBlink = false;
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async function initTerm(blink = true) {
|
|
154
|
+
await wait();
|
|
155
|
+
if (!term) return;
|
|
156
|
+
term.options.cursorBlink = blink;
|
|
157
|
+
term.write('\u001B[?25h');
|
|
158
|
+
term.focus();
|
|
159
|
+
|
|
160
|
+
if (!first) term?.write(`\r\n\x1b[0m`);
|
|
161
|
+
first = false;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function appendInputText(text: string) {
|
|
165
|
+
if (!text) return;
|
|
166
|
+
const inputCharacters = getInputCharacters(input);
|
|
167
|
+
const insertedCharacters = getInputCharacters(text);
|
|
168
|
+
const inputTail = inputCharacters.slice(inputCursor).join('');
|
|
169
|
+
inputCharacters.splice(inputCursor, 0, ...insertedCharacters);
|
|
170
|
+
input = inputCharacters.join('');
|
|
171
|
+
inputCursor += insertedCharacters.length;
|
|
172
|
+
let inputEcho = text;
|
|
173
|
+
if (inputTail && term) {
|
|
174
|
+
inputEcho += inputTail;
|
|
175
|
+
const inputTailCellWidth = getInputCellWidth(inputTail);
|
|
176
|
+
if (inputTailCellWidth > 0) inputEcho += `\x1b[${inputTailCellWidth}D`;
|
|
177
|
+
}
|
|
178
|
+
term?.write(inputEcho);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function getInputCharacters(text: string) {
|
|
182
|
+
const intlWithSegmenter = Intl as typeof Intl & {
|
|
183
|
+
Segmenter?: new (
|
|
184
|
+
locales?: string | string[],
|
|
185
|
+
options?: { granularity?: 'grapheme' | 'word' | 'sentence' }
|
|
186
|
+
) => { segment(input: string): Iterable<{ segment: string }> };
|
|
187
|
+
};
|
|
188
|
+
return intlWithSegmenter.Segmenter
|
|
189
|
+
? Array.from(
|
|
190
|
+
new intlWithSegmenter.Segmenter(undefined, {
|
|
191
|
+
granularity: 'grapheme'
|
|
192
|
+
}).segment(text),
|
|
193
|
+
({ segment }) => segment
|
|
194
|
+
)
|
|
195
|
+
: Array.from(text);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function getInputCellWidth(text: string) {
|
|
199
|
+
if (!text) return 0;
|
|
200
|
+
// The public Unicode API selects a provider but does not expose its width functions.
|
|
201
|
+
const unicode = (
|
|
202
|
+
term as
|
|
203
|
+
| (TerminalType & {
|
|
204
|
+
_core?: {
|
|
205
|
+
unicodeService?: {
|
|
206
|
+
getStringCellWidth?: (text: string) => number;
|
|
207
|
+
wcwidth?: (codepoint: number) => 0 | 1 | 2;
|
|
208
|
+
};
|
|
209
|
+
};
|
|
210
|
+
})
|
|
211
|
+
| undefined
|
|
212
|
+
)?._core?.unicodeService;
|
|
213
|
+
return (
|
|
214
|
+
unicode?.getStringCellWidth?.(text) ??
|
|
215
|
+
Array.from(text).reduce(
|
|
216
|
+
(width, character) =>
|
|
217
|
+
width + (unicode?.wcwidth?.(character.codePointAt(0) || 0) ?? 1),
|
|
218
|
+
0
|
|
219
|
+
)
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function submitCurrentInput() {
|
|
224
|
+
term?.write('\r\n');
|
|
225
|
+
const submittedInput = input + '\n';
|
|
226
|
+
if (sandbox && sandboxAcceptingInput) sandbox.write?.(submittedInput);
|
|
227
|
+
else pendingSandboxInput.push(submittedInput);
|
|
228
|
+
input = '';
|
|
229
|
+
inputCursor = 0;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function applyPastedText(text: string) {
|
|
233
|
+
const lines = text.replaceAll('\r\n', '\n').replaceAll('\r', '\n').split('\n');
|
|
234
|
+
for (let i = 0; i < lines.length; i++) {
|
|
235
|
+
appendInputText(lines[i]);
|
|
236
|
+
if (i < lines.length - 1) submitCurrentInput();
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
async function waitForInput() {
|
|
241
|
+
await wait();
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const terminalControl: TerminalControl = {
|
|
245
|
+
async clear() {
|
|
246
|
+
await wait();
|
|
247
|
+
term?.reset();
|
|
248
|
+
term?.write(`\u001B[?25l\x1b[0m\x1b[?25h`);
|
|
249
|
+
if (term) term.options.cursorBlink = false;
|
|
250
|
+
debugOutput = '';
|
|
251
|
+
input = '';
|
|
252
|
+
inputCursor = 0;
|
|
253
|
+
sandboxAcceptingInput = false;
|
|
254
|
+
pendingSandboxEof = false;
|
|
255
|
+
first = true;
|
|
256
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
257
|
+
},
|
|
258
|
+
async prepare(
|
|
259
|
+
language: string,
|
|
260
|
+
code: string,
|
|
261
|
+
log = true,
|
|
262
|
+
prog?: { set?: (value: number, stage?: string) => void },
|
|
263
|
+
args: string[] = [],
|
|
264
|
+
options: TerminalExecutionOptions = {}
|
|
265
|
+
) {
|
|
266
|
+
prog?.set?.(0, `Loading ${language} runtime`);
|
|
267
|
+
const progressBands = progressBandsForLanguage(language);
|
|
268
|
+
const loadProgress = phaseProgress(
|
|
269
|
+
prog,
|
|
270
|
+
progressBands.load[0],
|
|
271
|
+
progressBands.load[1],
|
|
272
|
+
`Loading ${language} runtime`
|
|
273
|
+
);
|
|
274
|
+
const prepareProgress = phaseProgress(
|
|
275
|
+
prog,
|
|
276
|
+
progressBands.prepare[0],
|
|
277
|
+
progressBands.prepare[1],
|
|
278
|
+
`Preparing ${language} program`
|
|
279
|
+
);
|
|
280
|
+
await Promise.all([
|
|
281
|
+
initSandbox(language).then(() =>
|
|
282
|
+
sandbox.load(code, log, args, options, loadProgress)
|
|
283
|
+
),
|
|
284
|
+
initTerm(false)
|
|
285
|
+
]);
|
|
286
|
+
prepareProgress?.set?.(0, `Preparing ${language} program`);
|
|
287
|
+
const prepared = !!(await runSandbox(
|
|
288
|
+
sandbox.run(code, true, log, prepareProgress, args, options),
|
|
289
|
+
false
|
|
290
|
+
));
|
|
291
|
+
if (prepared) prepareProgress?.set?.(1, `${language} runtime ready`);
|
|
292
|
+
return prepared;
|
|
293
|
+
},
|
|
294
|
+
async run(
|
|
295
|
+
language: string,
|
|
296
|
+
code: string,
|
|
297
|
+
log = true,
|
|
298
|
+
prog?: { set?: (value: number, stage?: string) => void },
|
|
299
|
+
args: string[] = [],
|
|
300
|
+
options: TerminalExecutionOptions = {}
|
|
301
|
+
) {
|
|
302
|
+
await Promise.all([
|
|
303
|
+
initSandbox(language).then(() => sandbox.load(code, log, args, options, prog)),
|
|
304
|
+
initTerm()
|
|
305
|
+
]);
|
|
306
|
+
sandboxAcceptingInput = true;
|
|
307
|
+
flushPendingSandboxInput();
|
|
308
|
+
return await runSandbox(sandbox.run(code, false, log, prog, args, options));
|
|
309
|
+
},
|
|
310
|
+
async destroy() {
|
|
311
|
+
await wait();
|
|
312
|
+
sandboxAcceptingInput = false;
|
|
313
|
+
pendingSandboxEof = false;
|
|
314
|
+
term?.dispose();
|
|
315
|
+
if (sandbox) await sandbox.clear();
|
|
316
|
+
},
|
|
317
|
+
async stop() {
|
|
318
|
+
await wait();
|
|
319
|
+
stopRequested = true;
|
|
320
|
+
finish = true;
|
|
321
|
+
sandboxAcceptingInput = false;
|
|
322
|
+
pendingSandboxEof = false;
|
|
323
|
+
if (sandbox?.kill) sandbox.kill();
|
|
324
|
+
else sandbox?.terminate?.();
|
|
325
|
+
},
|
|
326
|
+
async debugCommand(command: DebugCommand) {
|
|
327
|
+
await wait();
|
|
328
|
+
sandbox.debugCommand?.(command);
|
|
329
|
+
},
|
|
330
|
+
async setBreakpoints(lines: number[]) {
|
|
331
|
+
await wait();
|
|
332
|
+
sandbox?.setBreakpoints?.(lines);
|
|
333
|
+
},
|
|
334
|
+
async debugEvaluate(expression: string) {
|
|
335
|
+
await wait();
|
|
336
|
+
return (await sandbox.debugEvaluate?.(expression)) || '?';
|
|
337
|
+
},
|
|
338
|
+
async waitForInput() {
|
|
339
|
+
await waitForInput();
|
|
340
|
+
},
|
|
341
|
+
async write(input: string) {
|
|
342
|
+
await waitForInput();
|
|
343
|
+
if (!input) return;
|
|
344
|
+
applyPastedText(input);
|
|
345
|
+
if (!input.endsWith('\n') && !input.endsWith('\r')) submitCurrentInput();
|
|
346
|
+
},
|
|
347
|
+
async eof() {
|
|
348
|
+
await waitForInput();
|
|
349
|
+
submitSandboxEof();
|
|
350
|
+
}
|
|
351
|
+
};
|
|
352
|
+
|
|
353
|
+
$effect(() => {
|
|
354
|
+
if (term) {
|
|
355
|
+
if (dark) term.options.theme = Theme.Tango_Dark;
|
|
356
|
+
else term.options.theme = Theme.Tango_Light;
|
|
357
|
+
}
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
$effect(() => {
|
|
361
|
+
let _ = clientWidth + clientHeight;
|
|
362
|
+
(plugin as any)?.fit?.fit?.();
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
$effect(() => {
|
|
366
|
+
terminal = terminalControl;
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
onMount(() => {
|
|
370
|
+
import('@xterm/xterm').then(async ({ Terminal }) => {
|
|
371
|
+
if (!ref) return;
|
|
372
|
+
term = new Terminal({
|
|
373
|
+
theme: dark ? Theme.Tango_Dark : Theme.Tango_Light,
|
|
374
|
+
cursorBlink: false,
|
|
375
|
+
allowTransparency: true,
|
|
376
|
+
fontFamily: font,
|
|
377
|
+
allowProposedApi: true
|
|
378
|
+
});
|
|
379
|
+
term.open(ref);
|
|
380
|
+
term.onData((data: string) => {
|
|
381
|
+
if (!term || finish) return;
|
|
382
|
+
let pendingText = '';
|
|
383
|
+
for (let i = 0; i < data.length; ) {
|
|
384
|
+
const escapeSequence = data.slice(i).match(/^\x1b(?:\[[0-9;?]*[ABCD]|O[ABCD])/);
|
|
385
|
+
if (escapeSequence) {
|
|
386
|
+
if (pendingText) {
|
|
387
|
+
appendInputText(pendingText);
|
|
388
|
+
pendingText = '';
|
|
389
|
+
}
|
|
390
|
+
const direction = escapeSequence[0].at(-1);
|
|
391
|
+
const inputCharacters = getInputCharacters(input);
|
|
392
|
+
if (direction === 'D' && inputCursor > 0) {
|
|
393
|
+
const inputCharacter = inputCharacters[inputCursor - 1];
|
|
394
|
+
const inputCharacterCellWidth = getInputCellWidth(inputCharacter);
|
|
395
|
+
if (inputCharacterCellWidth > 0)
|
|
396
|
+
term.write(`\x1b[${inputCharacterCellWidth}D`);
|
|
397
|
+
inputCursor--;
|
|
398
|
+
} else if (direction === 'C' && inputCursor < inputCharacters.length) {
|
|
399
|
+
const inputCharacter = inputCharacters[inputCursor];
|
|
400
|
+
const inputCharacterCellWidth = getInputCellWidth(inputCharacter);
|
|
401
|
+
if (inputCharacterCellWidth > 0)
|
|
402
|
+
term.write(`\x1b[${inputCharacterCellWidth}C`);
|
|
403
|
+
inputCursor++;
|
|
404
|
+
}
|
|
405
|
+
i += escapeSequence[0].length;
|
|
406
|
+
continue;
|
|
407
|
+
}
|
|
408
|
+
const codePoint = data.codePointAt(i);
|
|
409
|
+
if (codePoint === undefined) break;
|
|
410
|
+
const chunk = String.fromCodePoint(codePoint);
|
|
411
|
+
i += chunk.length;
|
|
412
|
+
if (chunk === '\r' || chunk === '\n') {
|
|
413
|
+
if (pendingText) {
|
|
414
|
+
appendInputText(pendingText);
|
|
415
|
+
pendingText = '';
|
|
416
|
+
}
|
|
417
|
+
submitCurrentInput();
|
|
418
|
+
continue;
|
|
419
|
+
}
|
|
420
|
+
if (chunk === '\u007f') {
|
|
421
|
+
if (pendingText) {
|
|
422
|
+
appendInputText(pendingText);
|
|
423
|
+
pendingText = '';
|
|
424
|
+
}
|
|
425
|
+
if (inputCursor > 0) {
|
|
426
|
+
const inputCharacters = getInputCharacters(input);
|
|
427
|
+
const removedInput = inputCharacters.splice(inputCursor - 1, 1)[0];
|
|
428
|
+
if (removedInput) {
|
|
429
|
+
inputCursor--;
|
|
430
|
+
const inputTail = inputCharacters.slice(inputCursor).join('');
|
|
431
|
+
const removedInputCellWidth = getInputCellWidth(removedInput);
|
|
432
|
+
const inputTailCellWidth = getInputCellWidth(inputTail);
|
|
433
|
+
let backspaceEcho = '';
|
|
434
|
+
if (removedInputCellWidth > 0)
|
|
435
|
+
backspaceEcho += `\x1b[${removedInputCellWidth}D`;
|
|
436
|
+
if (inputTail) backspaceEcho += inputTail;
|
|
437
|
+
if (removedInputCellWidth > 0)
|
|
438
|
+
backspaceEcho += ' '.repeat(removedInputCellWidth);
|
|
439
|
+
const cursorReturnCellWidth =
|
|
440
|
+
inputTailCellWidth + removedInputCellWidth;
|
|
441
|
+
if (cursorReturnCellWidth > 0)
|
|
442
|
+
backspaceEcho += `\x1b[${cursorReturnCellWidth}D`;
|
|
443
|
+
if (backspaceEcho) term.write(backspaceEcho);
|
|
444
|
+
input = inputCharacters.join('');
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
continue;
|
|
448
|
+
}
|
|
449
|
+
if ((chunk.codePointAt(0) || 0) >= 0x20) {
|
|
450
|
+
pendingText += chunk;
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
if (pendingText) {
|
|
454
|
+
appendInputText(pendingText);
|
|
455
|
+
}
|
|
456
|
+
});
|
|
457
|
+
term.onKey((e: { key: string; domEvent: KeyboardEvent }) => {
|
|
458
|
+
if (!term) return;
|
|
459
|
+
const ev = e.domEvent;
|
|
460
|
+
const isCopyShortcut = (ev.ctrlKey || ev.metaKey) && ev.key.toLowerCase() === 'c';
|
|
461
|
+
if (isCopyShortcut && term.hasSelection()) {
|
|
462
|
+
const selectedText = term.getSelection();
|
|
463
|
+
if (selectedText) {
|
|
464
|
+
ev.preventDefault();
|
|
465
|
+
navigator.clipboard.writeText(selectedText).catch(() => {});
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
if (finish) return;
|
|
470
|
+
onkey?.(ev);
|
|
471
|
+
if (isCopyShortcut) {
|
|
472
|
+
ev.preventDefault();
|
|
473
|
+
sandbox.kill?.();
|
|
474
|
+
} else if ((ev.ctrlKey || ev.metaKey) && ev.key.toLowerCase() === 'd') {
|
|
475
|
+
ev.preventDefault();
|
|
476
|
+
if (input.length > 0) submitCurrentInput();
|
|
477
|
+
submitSandboxEof();
|
|
478
|
+
} else if ((ev.ctrlKey || ev.metaKey) && ev.key.toLowerCase() === 'v') {
|
|
479
|
+
ev.preventDefault();
|
|
480
|
+
navigator.clipboard.readText().then((text) => {
|
|
481
|
+
applyPastedText(text);
|
|
482
|
+
});
|
|
483
|
+
}
|
|
484
|
+
});
|
|
485
|
+
plugin = await registerAllPlugins(term);
|
|
486
|
+
|
|
487
|
+
onload?.();
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
return async () => {
|
|
491
|
+
term?.dispose();
|
|
492
|
+
if (sandbox) await sandbox.clear();
|
|
493
|
+
};
|
|
494
|
+
});
|
|
495
|
+
</script>
|
|
496
|
+
|
|
497
|
+
<main>
|
|
498
|
+
<div bind:this={ref} bind:clientWidth bind:clientHeight></div>
|
|
499
|
+
<pre data-testid="terminal-debug-output" style="display: none;">{debugOutput}</pre>
|
|
500
|
+
</main>
|
|
501
|
+
|
|
502
|
+
<style>
|
|
503
|
+
main {
|
|
504
|
+
padding: 10px;
|
|
505
|
+
width: calc(100% - 20px);
|
|
506
|
+
height: calc(100% - 20px);
|
|
507
|
+
overflow: hidden;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
div {
|
|
511
|
+
width: 100%;
|
|
512
|
+
height: 100%;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
:global(.xterm),
|
|
516
|
+
:global(.xterm .xterm-viewport),
|
|
517
|
+
:global(.xterm .composition-view) {
|
|
518
|
+
background-color: transparent;
|
|
519
|
+
}
|
|
520
|
+
</style>
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { type DebugSessionEvent } from '@wasm-idle/core';
|
|
2
|
+
import '@xterm/xterm/css/xterm.css';
|
|
3
|
+
import type { CompilerDiagnostic, PlaygroundBinding, TerminalControl } from './types.js';
|
|
4
|
+
interface Props {
|
|
5
|
+
dark?: boolean;
|
|
6
|
+
playground: PlaygroundBinding;
|
|
7
|
+
font?: string;
|
|
8
|
+
onload?: () => void;
|
|
9
|
+
onfinish?: () => void;
|
|
10
|
+
onkey?: (e: KeyboardEvent) => void;
|
|
11
|
+
ondebug?: (event: DebugSessionEvent) => void;
|
|
12
|
+
oncompilediagnostic?: (diagnostic: CompilerDiagnostic) => void;
|
|
13
|
+
onimage?: (payload: {
|
|
14
|
+
mime: string;
|
|
15
|
+
b64: string;
|
|
16
|
+
ts?: number;
|
|
17
|
+
}) => void;
|
|
18
|
+
terminal?: TerminalControl;
|
|
19
|
+
}
|
|
20
|
+
declare const Terminal: import("svelte").Component<Props, {}, "terminal">;
|
|
21
|
+
type Terminal = ReturnType<typeof Terminal>;
|
|
22
|
+
export default Terminal;
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import Terminal from './Terminal.svelte';
|
|
2
|
+
import registerAllPlugins from './plugin/index.js';
|
|
3
|
+
import Theme from './theme.js';
|
|
4
|
+
import type { BoundSandbox, CompilerDiagnostic, CompilerDiagnosticSeverity, DebugCommand, DebugFrame, DebugSessionEvent, DebugVariable, PlaygroundBinding, SandboxExecutionOptions, TerminalControl, TerminalExecutionOptions } from './types.js';
|
|
5
|
+
export { Terminal, Theme, registerAllPlugins };
|
|
6
|
+
export type { BoundSandbox, CompilerDiagnostic, CompilerDiagnosticSeverity, DebugCommand, DebugFrame, DebugSessionEvent, DebugVariable, PlaygroundBinding, SandboxExecutionOptions, TerminalControl, TerminalExecutionOptions };
|
|
7
|
+
export default Terminal;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Terminal } from '@xterm/xterm';
|
|
2
|
+
export default function registerAllPlugins(term: Terminal): Promise<{
|
|
3
|
+
fit: import("@xterm/addon-fit").FitAddon;
|
|
4
|
+
search: import("@xterm/addon-search").SearchAddon;
|
|
5
|
+
serialize: import("@xterm/addon-serialize").SerializeAddon;
|
|
6
|
+
weblink: import("@xterm/addon-web-links").WebLinksAddon;
|
|
7
|
+
webgl: import("@xterm/addon-webgl").WebglAddon;
|
|
8
|
+
unicode: import("@xterm/addon-unicode11").Unicode11Addon;
|
|
9
|
+
}>;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export default async function registerAllPlugins(term) {
|
|
2
|
+
const { FitAddon } = await import('@xterm/addon-fit');
|
|
3
|
+
const { SearchAddon } = await import('@xterm/addon-search');
|
|
4
|
+
const { SerializeAddon } = await import('@xterm/addon-serialize');
|
|
5
|
+
const { WebLinksAddon } = await import('@xterm/addon-web-links');
|
|
6
|
+
const { WebglAddon } = await import('@xterm/addon-webgl');
|
|
7
|
+
const { Unicode11Addon } = await import('@xterm/addon-unicode11');
|
|
8
|
+
const plugins = {
|
|
9
|
+
fit: new FitAddon(),
|
|
10
|
+
search: new SearchAddon(),
|
|
11
|
+
serialize: new SerializeAddon(),
|
|
12
|
+
weblink: new WebLinksAddon(),
|
|
13
|
+
webgl: new WebglAddon(),
|
|
14
|
+
unicode: new Unicode11Addon()
|
|
15
|
+
};
|
|
16
|
+
for (const plugin in plugins)
|
|
17
|
+
term.loadAddon(plugins[plugin]);
|
|
18
|
+
term.unicode.activeVersion = '11';
|
|
19
|
+
return plugins;
|
|
20
|
+
}
|
package/dist/theme.d.ts
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
declare const theme: {
|
|
2
|
+
Tango_Dark: {
|
|
3
|
+
foreground: string;
|
|
4
|
+
background: string;
|
|
5
|
+
cursor: string;
|
|
6
|
+
cursorAccent: string;
|
|
7
|
+
selection: string;
|
|
8
|
+
black: string;
|
|
9
|
+
red: string;
|
|
10
|
+
green: string;
|
|
11
|
+
yellow: string;
|
|
12
|
+
blue: string;
|
|
13
|
+
magenta: string;
|
|
14
|
+
cyan: string;
|
|
15
|
+
white: string;
|
|
16
|
+
brightBlack: string;
|
|
17
|
+
brightRed: string;
|
|
18
|
+
brightGreen: string;
|
|
19
|
+
brightYellow: string;
|
|
20
|
+
brightBlue: string;
|
|
21
|
+
brightMagenta: string;
|
|
22
|
+
brightCyan: string;
|
|
23
|
+
brightWhite: string;
|
|
24
|
+
};
|
|
25
|
+
Tango_Light: {
|
|
26
|
+
foreground: string;
|
|
27
|
+
background: string;
|
|
28
|
+
cursor: string;
|
|
29
|
+
cursorAccent: string;
|
|
30
|
+
selection: string;
|
|
31
|
+
black: string;
|
|
32
|
+
red: string;
|
|
33
|
+
green: string;
|
|
34
|
+
yellow: string;
|
|
35
|
+
blue: string;
|
|
36
|
+
magenta: string;
|
|
37
|
+
cyan: string;
|
|
38
|
+
white: string;
|
|
39
|
+
brightBlack: string;
|
|
40
|
+
brightRed: string;
|
|
41
|
+
brightGreen: string;
|
|
42
|
+
brightYellow: string;
|
|
43
|
+
brightBlue: string;
|
|
44
|
+
brightMagenta: string;
|
|
45
|
+
brightCyan: string;
|
|
46
|
+
brightWhite: string;
|
|
47
|
+
};
|
|
48
|
+
};
|
|
49
|
+
export default theme;
|
package/dist/theme.js
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
const theme = {
|
|
2
|
+
Tango_Dark: {
|
|
3
|
+
foreground: '#eee',
|
|
4
|
+
background: 'rgba(0, 0, 0, 0)',
|
|
5
|
+
cursor: '#FFFFFF',
|
|
6
|
+
cursorAccent: '#ffffff',
|
|
7
|
+
selection: '#ffffff44',
|
|
8
|
+
black: '#000000',
|
|
9
|
+
red: '#CC0000',
|
|
10
|
+
green: '#4E9A06',
|
|
11
|
+
yellow: '#C4A000',
|
|
12
|
+
blue: '#3465A4',
|
|
13
|
+
magenta: '#75507B',
|
|
14
|
+
cyan: '#06989A',
|
|
15
|
+
white: '#D3D7CF',
|
|
16
|
+
brightBlack: '#555753',
|
|
17
|
+
brightRed: '#EF2929',
|
|
18
|
+
brightGreen: '#8AE234',
|
|
19
|
+
brightYellow: '#FCE94F',
|
|
20
|
+
brightBlue: '#729FCF',
|
|
21
|
+
brightMagenta: '#AD7FA8',
|
|
22
|
+
brightCyan: '#34E2E2',
|
|
23
|
+
brightWhite: '#EEEEEC'
|
|
24
|
+
},
|
|
25
|
+
Tango_Light: {
|
|
26
|
+
foreground: '#111',
|
|
27
|
+
background: 'rgba(255, 255, 255, 0)',
|
|
28
|
+
cursor: '#000000',
|
|
29
|
+
cursorAccent: '#000000',
|
|
30
|
+
selection: '#00000044',
|
|
31
|
+
black: '#000000',
|
|
32
|
+
red: '#CC0000',
|
|
33
|
+
green: '#4E9A06',
|
|
34
|
+
yellow: '#C4A000',
|
|
35
|
+
blue: '#3465A4',
|
|
36
|
+
magenta: '#75507B',
|
|
37
|
+
cyan: '#06989A',
|
|
38
|
+
white: '#D3D7CF',
|
|
39
|
+
brightBlack: '#555753',
|
|
40
|
+
brightRed: '#EF2929',
|
|
41
|
+
brightGreen: '#8AE234',
|
|
42
|
+
brightYellow: '#FCE94F',
|
|
43
|
+
brightBlue: '#729FCF',
|
|
44
|
+
brightMagenta: '#AD7FA8',
|
|
45
|
+
brightCyan: '#34E2E2',
|
|
46
|
+
brightWhite: '#EEEEEC'
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
export default theme;
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { BoundSandbox as CoreBoundSandbox, DebugCommand, PlaygroundBinding as CorePlaygroundBinding, ProgressLike, SandboxExecutionOptions as CoreSandboxExecutionOptions, TerminalControl as CoreTerminalControl } from '@wasm-idle/core';
|
|
2
|
+
export type CompilerDiagnosticSeverity = 'error' | 'warning' | 'other';
|
|
3
|
+
export interface CompilerDiagnostic {
|
|
4
|
+
fileName?: string | null;
|
|
5
|
+
lineNumber: number;
|
|
6
|
+
columnNumber?: number;
|
|
7
|
+
endColumnNumber?: number;
|
|
8
|
+
severity: CompilerDiagnosticSeverity;
|
|
9
|
+
message: string;
|
|
10
|
+
}
|
|
11
|
+
export type RustTargetTriple = 'wasm32-wasip1' | 'wasm32-wasip2' | 'wasm32-wasip3';
|
|
12
|
+
export type GoTarget = 'wasip1/wasm' | 'wasip2/wasm' | 'wasip3/wasm' | 'js/wasm';
|
|
13
|
+
export type TinyGoTarget = 'wasm' | 'wasip1' | 'wasip2' | 'wasip3';
|
|
14
|
+
export type OcamlBackend = 'js' | 'wasm';
|
|
15
|
+
export type OcamlWasmBinaryenMode = 'fast' | 'full';
|
|
16
|
+
export type ZigTargetTriple = 'wasm64-wasi';
|
|
17
|
+
export interface SandboxWorkspaceFile {
|
|
18
|
+
path: string;
|
|
19
|
+
content: string;
|
|
20
|
+
}
|
|
21
|
+
export interface SandboxExecutionOptions {
|
|
22
|
+
debug?: boolean;
|
|
23
|
+
breakpoints?: number[];
|
|
24
|
+
pauseOnEntry?: boolean;
|
|
25
|
+
stdin?: string;
|
|
26
|
+
activePath?: string;
|
|
27
|
+
debugPath?: string;
|
|
28
|
+
workspaceFiles?: SandboxWorkspaceFile[];
|
|
29
|
+
compileArgs?: string[];
|
|
30
|
+
programArgs?: string[];
|
|
31
|
+
cppVersion?: string;
|
|
32
|
+
cVersion?: string;
|
|
33
|
+
rustTargetTriple?: RustTargetTriple;
|
|
34
|
+
goTarget?: GoTarget;
|
|
35
|
+
tinygoTarget?: TinyGoTarget;
|
|
36
|
+
ocamlBackend?: OcamlBackend;
|
|
37
|
+
ocamlWasmBinaryenMode?: OcamlWasmBinaryenMode;
|
|
38
|
+
zigTargetTriple?: ZigTargetTriple;
|
|
39
|
+
}
|
|
40
|
+
export type TerminalExecutionOptions = SandboxExecutionOptions | CoreSandboxExecutionOptions;
|
|
41
|
+
export interface BoundSandbox extends Omit<CoreBoundSandbox, 'load' | 'run' | 'oncompilerdiagnostic'> {
|
|
42
|
+
load(code?: string, log?: boolean, args?: string[], options?: TerminalExecutionOptions, progress?: ProgressLike): Promise<void>;
|
|
43
|
+
run(code: string, prepare: boolean, log?: boolean, progress?: ProgressLike, args?: string[], options?: TerminalExecutionOptions): Promise<boolean | string>;
|
|
44
|
+
oncompilerdiagnostic?: (diagnostic: CompilerDiagnostic) => void;
|
|
45
|
+
}
|
|
46
|
+
export interface PlaygroundBinding extends Pick<CorePlaygroundBinding, 'runtimeAssets'> {
|
|
47
|
+
load: (language: string) => Promise<BoundSandbox>;
|
|
48
|
+
}
|
|
49
|
+
export interface TerminalControl extends Omit<CoreTerminalControl, 'prepare' | 'run' | 'debugCommand'> {
|
|
50
|
+
prepare: (language: string, code: string, log?: boolean, progress?: ProgressLike, args?: string[], options?: TerminalExecutionOptions) => Promise<boolean>;
|
|
51
|
+
run: (language: string, code: string, log?: boolean, progress?: ProgressLike, args?: string[], options?: TerminalExecutionOptions) => Promise<boolean | string>;
|
|
52
|
+
debugCommand: (command: DebugCommand) => Promise<void>;
|
|
53
|
+
}
|
|
54
|
+
export type { DebugCommand, DebugFrame, DebugSessionEvent, DebugVariable, ProgressLike } from '@wasm-idle/core';
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@wasm-idle/terminal",
|
|
3
|
+
"version": "1.0.0-next.0",
|
|
4
|
+
"description": "Svelte terminal component for wasm-idle playground bindings",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"svelte": "./dist/index.js",
|
|
11
|
+
"import": "./dist/index.js",
|
|
12
|
+
"default": "./dist/index.js"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"svelte": "./dist/index.js",
|
|
16
|
+
"types": "./dist/index.d.ts",
|
|
17
|
+
"files": [
|
|
18
|
+
"dist"
|
|
19
|
+
],
|
|
20
|
+
"publishConfig": {
|
|
21
|
+
"access": "public",
|
|
22
|
+
"tag": "next"
|
|
23
|
+
},
|
|
24
|
+
"peerDependencies": {
|
|
25
|
+
"@wasm-idle/core": "1.0.0-next.0",
|
|
26
|
+
"svelte": "^5.0.0"
|
|
27
|
+
},
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"@xterm/addon-fit": "^0.11.0",
|
|
30
|
+
"@xterm/addon-search": "^0.16.0",
|
|
31
|
+
"@xterm/addon-serialize": "^0.14.0",
|
|
32
|
+
"@xterm/addon-unicode11": "^0.9.0",
|
|
33
|
+
"@xterm/addon-web-links": "^0.12.0",
|
|
34
|
+
"@xterm/addon-webgl": "^0.19.0",
|
|
35
|
+
"@xterm/xterm": "^6.0.0",
|
|
36
|
+
"@wasm-idle/core": "1.0.0-next.0"
|
|
37
|
+
},
|
|
38
|
+
"devDependencies": {
|
|
39
|
+
"@sveltejs/package": "^2.5.7",
|
|
40
|
+
"@sveltejs/vite-plugin-svelte": "^7.0.0",
|
|
41
|
+
"@types/node": "^25.6.0",
|
|
42
|
+
"playwright-core": "^1.59.1",
|
|
43
|
+
"publint": "^0.3.18",
|
|
44
|
+
"svelte": "^5.55.4",
|
|
45
|
+
"svelte-check": "^4.4.6",
|
|
46
|
+
"typescript": "^5.9.3",
|
|
47
|
+
"vite": "^8.0.8",
|
|
48
|
+
"vitest": "^4.1.4"
|
|
49
|
+
},
|
|
50
|
+
"scripts": {
|
|
51
|
+
"build": "svelte-package --input src --output dist --tsconfig ./tsconfig.json",
|
|
52
|
+
"check": "svelte-check --tsconfig ./tsconfig.json",
|
|
53
|
+
"test": "vitest run --config vitest.config.ts",
|
|
54
|
+
"lint:package": "pnpm run build && publint"
|
|
55
|
+
}
|
|
56
|
+
}
|