@jhebe/web-terminal 0.2.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 +86 -0
- package/bin/web-terminal.mjs +309 -0
- package/dist/index.d.ts +3 -0
- package/dist/protocol.d.ts +98 -0
- package/dist/terminal-client.d.ts +62 -0
- package/dist/terminal-view.d.ts +28 -0
- package/dist/web-terminal.css +2 -0
- package/dist/web-terminal.js +352 -0
- package/dist-server/server/history-buffer.d.ts +8 -0
- package/dist-server/server/history-buffer.js +59 -0
- package/dist-server/server/index.d.ts +2 -0
- package/dist-server/server/index.js +2 -0
- package/dist-server/server/session-manager.d.ts +51 -0
- package/dist-server/server/session-manager.js +274 -0
- package/dist-server/server/terminal-server.d.ts +16 -0
- package/dist-server/server/terminal-server.js +250 -0
- package/dist-server/src/protocol.d.ts +98 -0
- package/dist-server/src/protocol.js +199 -0
- package/package.json +59 -0
- package/scripts/prepare-node-pty.mjs +28 -0
package/README.md
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# web-terminal
|
|
2
|
+
|
|
3
|
+
`web-terminal` is a framework-agnostic terminal-session library. A hosting
|
|
4
|
+
backend instantiates a private in-memory session manager; browser applications
|
|
5
|
+
interact through the limited verbs `open`, `close`, `attach`, `detach`, and
|
|
6
|
+
`display`.
|
|
7
|
+
|
|
8
|
+
## Backend
|
|
9
|
+
|
|
10
|
+
```ts
|
|
11
|
+
import { createServer } from 'node:http';
|
|
12
|
+
import {
|
|
13
|
+
attachTerminalServer,
|
|
14
|
+
createTerminalSessionManager,
|
|
15
|
+
} from '@jhebe/web-terminal/server';
|
|
16
|
+
|
|
17
|
+
const server = createServer(app);
|
|
18
|
+
const manager = createTerminalSessionManager({
|
|
19
|
+
cwd: '/workspace',
|
|
20
|
+
idleTimeoutMs: 10 * 60 * 1000,
|
|
21
|
+
maxSessions: 100,
|
|
22
|
+
maxObserversPerSession: 32,
|
|
23
|
+
maxInputBytes: 64 * 1024,
|
|
24
|
+
maxWriteBytes: 64 * 1024,
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
manager.observe(sessionId, 'host-monitor', {
|
|
28
|
+
output: (data) => process.stdout.write(data),
|
|
29
|
+
exit: (code) => console.log('Exited:', code),
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
manager.write(sessionId, 'echo controlled by host\r');
|
|
33
|
+
manager.unobserve(sessionId, 'host-monitor');
|
|
34
|
+
|
|
35
|
+
attachTerminalServer({
|
|
36
|
+
server,
|
|
37
|
+
manager,
|
|
38
|
+
maxPayloadBytes: 64 * 1024,
|
|
39
|
+
authorize: (request, action, sessionId) => {
|
|
40
|
+
return authorizeUser(request, action, sessionId);
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
The host owns authentication, permissions, names, business context, and page
|
|
46
|
+
layout. The manager owns PTY lifetime, session identity, attachments, bounded
|
|
47
|
+
history, and idle cleanup.
|
|
48
|
+
|
|
49
|
+
## Browser
|
|
50
|
+
|
|
51
|
+
```ts
|
|
52
|
+
import { createTerminalClient } from '@jhebe/web-terminal';
|
|
53
|
+
import '@jhebe/web-terminal/style.css';
|
|
54
|
+
|
|
55
|
+
const client = createTerminalClient({
|
|
56
|
+
socketUrl: 'wss://example.com/terminal',
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
const session = await client.open({ name: '小鍵' });
|
|
60
|
+
const view = await client.display(session.id, element);
|
|
61
|
+
|
|
62
|
+
await client.write(session.id, 'echo inserted by the host UI\r');
|
|
63
|
+
await client.detach(); // Session and process continue.
|
|
64
|
+
await client.display(session.id, anotherElement);
|
|
65
|
+
await client.close(session.id); // Session and process end permanently.
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
This version is intentionally in-memory and single-process. Sessions do not
|
|
69
|
+
survive backend restart and are not coordinated across multiple backend
|
|
70
|
+
instances.
|
|
71
|
+
|
|
72
|
+
## CLI
|
|
73
|
+
|
|
74
|
+
The package installs a `web-terminal` command that uses the same authenticated
|
|
75
|
+
WebSocket endpoint:
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
web-terminal --url ws://127.0.0.1:5173/terminal sessions
|
|
79
|
+
web-terminal --url ws://127.0.0.1:5173/terminal open "小鍵"
|
|
80
|
+
web-terminal --url ws://127.0.0.1:5173/terminal send SESSION_ID "echo hello"
|
|
81
|
+
web-terminal --url ws://127.0.0.1:5173/terminal follow SESSION_ID
|
|
82
|
+
web-terminal --url ws://127.0.0.1:5173/terminal close SESSION_ID
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Use `--token` or `WEB_TERMINAL_TOKEN` when the host authorization callback
|
|
86
|
+
expects a bearer token.
|
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { randomUUID } from 'node:crypto';
|
|
4
|
+
import process from 'node:process';
|
|
5
|
+
import { WebSocket } from 'ws';
|
|
6
|
+
|
|
7
|
+
async function main() {
|
|
8
|
+
const { command, arguments: commandArguments, url, token } = parseArguments(
|
|
9
|
+
process.argv.slice(2),
|
|
10
|
+
);
|
|
11
|
+
|
|
12
|
+
if (!command || command === 'help' || command === '--help') {
|
|
13
|
+
printHelp();
|
|
14
|
+
process.exit(0);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const client = new SessionWireClient(url, token);
|
|
18
|
+
|
|
19
|
+
try {
|
|
20
|
+
await client.connect();
|
|
21
|
+
|
|
22
|
+
switch (command) {
|
|
23
|
+
case 'sessions': {
|
|
24
|
+
const sessions = await client.waitForSessions();
|
|
25
|
+
if (sessions.length === 0) {
|
|
26
|
+
console.log('No terminal sessions.');
|
|
27
|
+
break;
|
|
28
|
+
}
|
|
29
|
+
for (const session of sessions) {
|
|
30
|
+
console.log(
|
|
31
|
+
[
|
|
32
|
+
session.id,
|
|
33
|
+
session.state,
|
|
34
|
+
session.attached ? 'displayed' : 'detached',
|
|
35
|
+
`${session.observers} observer(s)`,
|
|
36
|
+
session.name,
|
|
37
|
+
].join('\t'),
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
break;
|
|
41
|
+
}
|
|
42
|
+
case 'open': {
|
|
43
|
+
const response = await client.request({
|
|
44
|
+
type: 'open',
|
|
45
|
+
requestId: randomUUID(),
|
|
46
|
+
...(commandArguments.length === 0
|
|
47
|
+
? {}
|
|
48
|
+
: { name: commandArguments.join(' ') }),
|
|
49
|
+
});
|
|
50
|
+
requireResponse(response, 'opened');
|
|
51
|
+
console.log(response.session.id);
|
|
52
|
+
break;
|
|
53
|
+
}
|
|
54
|
+
case 'send':
|
|
55
|
+
case 'write': {
|
|
56
|
+
const [sessionId, ...inputParts] = commandArguments;
|
|
57
|
+
if (!sessionId || inputParts.length === 0) {
|
|
58
|
+
throw new Error(`${command} requires a session ID and input.`);
|
|
59
|
+
}
|
|
60
|
+
const data =
|
|
61
|
+
command === 'send'
|
|
62
|
+
? `${inputParts.join(' ')}\r`
|
|
63
|
+
: inputParts.join(' ');
|
|
64
|
+
const response = await client.request({
|
|
65
|
+
type: 'write',
|
|
66
|
+
requestId: randomUUID(),
|
|
67
|
+
sessionId,
|
|
68
|
+
data,
|
|
69
|
+
});
|
|
70
|
+
requireResponse(response, 'written');
|
|
71
|
+
break;
|
|
72
|
+
}
|
|
73
|
+
case 'follow': {
|
|
74
|
+
const [sessionId] = commandArguments;
|
|
75
|
+
if (!sessionId) {
|
|
76
|
+
throw new Error('follow requires a session ID.');
|
|
77
|
+
}
|
|
78
|
+
const response = await client.request({
|
|
79
|
+
type: 'observe',
|
|
80
|
+
requestId: randomUUID(),
|
|
81
|
+
sessionId,
|
|
82
|
+
});
|
|
83
|
+
requireResponse(response, 'observing');
|
|
84
|
+
process.stdout.write(response.history);
|
|
85
|
+
|
|
86
|
+
if (response.session.state === 'exited') {
|
|
87
|
+
console.error(
|
|
88
|
+
`\nSession has already exited with code ${
|
|
89
|
+
response.session.exitCode ?? 'unknown'
|
|
90
|
+
}.`,
|
|
91
|
+
);
|
|
92
|
+
} else {
|
|
93
|
+
await new Promise((resolve) => {
|
|
94
|
+
const finish = () => {
|
|
95
|
+
unsubscribe();
|
|
96
|
+
resolve();
|
|
97
|
+
};
|
|
98
|
+
const unsubscribe = client.onMessage((message) => {
|
|
99
|
+
if (message.type === 'output' && message.sessionId === sessionId) {
|
|
100
|
+
process.stdout.write(message.data);
|
|
101
|
+
} else if (
|
|
102
|
+
message.type === 'exit' &&
|
|
103
|
+
message.sessionId === sessionId
|
|
104
|
+
) {
|
|
105
|
+
console.error(`\nSession exited with code ${message.exitCode}.`);
|
|
106
|
+
finish();
|
|
107
|
+
} else if (
|
|
108
|
+
message.type === 'sessions' &&
|
|
109
|
+
!message.sessions.some((session) => session.id === sessionId)
|
|
110
|
+
) {
|
|
111
|
+
console.error('\nSession was closed.');
|
|
112
|
+
finish();
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
process.once('SIGINT', finish);
|
|
116
|
+
client.onceClosed(finish);
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
if (client.connected) {
|
|
120
|
+
await client.request({
|
|
121
|
+
type: 'unobserve',
|
|
122
|
+
requestId: randomUUID(),
|
|
123
|
+
sessionId,
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
break;
|
|
127
|
+
}
|
|
128
|
+
case 'close': {
|
|
129
|
+
const [sessionId] = commandArguments;
|
|
130
|
+
if (!sessionId) {
|
|
131
|
+
throw new Error('close requires a session ID.');
|
|
132
|
+
}
|
|
133
|
+
const response = await client.request({
|
|
134
|
+
type: 'close',
|
|
135
|
+
requestId: randomUUID(),
|
|
136
|
+
sessionId,
|
|
137
|
+
});
|
|
138
|
+
requireResponse(response, 'closed');
|
|
139
|
+
break;
|
|
140
|
+
}
|
|
141
|
+
default:
|
|
142
|
+
throw new Error(`Unknown command: ${command}`);
|
|
143
|
+
}
|
|
144
|
+
} catch (error) {
|
|
145
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
146
|
+
process.exitCode = 1;
|
|
147
|
+
} finally {
|
|
148
|
+
client.close();
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
class SessionWireClient {
|
|
153
|
+
#socket;
|
|
154
|
+
#pending = new Map();
|
|
155
|
+
#listeners = new Set();
|
|
156
|
+
#closeListeners = new Set();
|
|
157
|
+
#sessions;
|
|
158
|
+
#sessionWaiters = [];
|
|
159
|
+
|
|
160
|
+
constructor(url, token) {
|
|
161
|
+
this.url = url;
|
|
162
|
+
this.token = token;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
get connected() {
|
|
166
|
+
return this.#socket?.readyState === WebSocket.OPEN;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
connect() {
|
|
170
|
+
return new Promise((resolve, reject) => {
|
|
171
|
+
const headers = this.token
|
|
172
|
+
? { Authorization: `Bearer ${this.token}` }
|
|
173
|
+
: undefined;
|
|
174
|
+
const socket = new WebSocket(this.url, { headers });
|
|
175
|
+
this.#socket = socket;
|
|
176
|
+
socket.once('open', resolve);
|
|
177
|
+
socket.once('error', reject);
|
|
178
|
+
socket.on('message', (raw) => this.#handle(JSON.parse(raw.toString())));
|
|
179
|
+
socket.on('close', () => {
|
|
180
|
+
for (const pending of this.#pending.values()) {
|
|
181
|
+
pending.reject(new Error('Terminal connection closed.'));
|
|
182
|
+
}
|
|
183
|
+
this.#pending.clear();
|
|
184
|
+
for (const listener of this.#closeListeners) {
|
|
185
|
+
listener();
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
request(message) {
|
|
192
|
+
if (!this.connected) {
|
|
193
|
+
return Promise.reject(new Error('Terminal CLI is not connected.'));
|
|
194
|
+
}
|
|
195
|
+
return new Promise((resolve, reject) => {
|
|
196
|
+
this.#pending.set(message.requestId, { resolve, reject });
|
|
197
|
+
this.#socket.send(JSON.stringify(message));
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
waitForSessions() {
|
|
202
|
+
if (this.#sessions) {
|
|
203
|
+
return Promise.resolve(this.#sessions);
|
|
204
|
+
}
|
|
205
|
+
return new Promise((resolve) => this.#sessionWaiters.push(resolve));
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
onMessage(listener) {
|
|
209
|
+
this.#listeners.add(listener);
|
|
210
|
+
return () => this.#listeners.delete(listener);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
onceClosed(listener) {
|
|
214
|
+
this.#closeListeners.add(listener);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
close() {
|
|
218
|
+
this.#socket?.close(1000, 'CLI completed');
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
#handle(message) {
|
|
222
|
+
if (message.type === 'sessions') {
|
|
223
|
+
this.#sessions = message.sessions;
|
|
224
|
+
for (const resolve of this.#sessionWaiters.splice(0)) {
|
|
225
|
+
resolve(message.sessions);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (message.type === 'error') {
|
|
230
|
+
if (message.requestId) {
|
|
231
|
+
const pending = this.#pending.get(message.requestId);
|
|
232
|
+
this.#pending.delete(message.requestId);
|
|
233
|
+
pending?.reject(new Error(message.message));
|
|
234
|
+
} else {
|
|
235
|
+
console.error(message.message);
|
|
236
|
+
}
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
if (message.requestId) {
|
|
241
|
+
const pending = this.#pending.get(message.requestId);
|
|
242
|
+
this.#pending.delete(message.requestId);
|
|
243
|
+
pending?.resolve(message);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
for (const listener of this.#listeners) {
|
|
247
|
+
listener(message);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function parseArguments(args) {
|
|
253
|
+
const remaining = [];
|
|
254
|
+
let url =
|
|
255
|
+
process.env.WEB_TERMINAL_URL ?? 'ws://127.0.0.1:5173/terminal';
|
|
256
|
+
let token = process.env.WEB_TERMINAL_TOKEN;
|
|
257
|
+
|
|
258
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
259
|
+
const argument = args[index];
|
|
260
|
+
if (argument === '--url') {
|
|
261
|
+
url = requireOptionValue(args, ++index, '--url');
|
|
262
|
+
} else if (argument === '--token') {
|
|
263
|
+
token = requireOptionValue(args, ++index, '--token');
|
|
264
|
+
} else {
|
|
265
|
+
remaining.push(argument);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
return {
|
|
270
|
+
command: remaining[0],
|
|
271
|
+
arguments: remaining.slice(1),
|
|
272
|
+
url,
|
|
273
|
+
token,
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function requireOptionValue(args, index, option) {
|
|
278
|
+
const value = args[index];
|
|
279
|
+
if (!value) {
|
|
280
|
+
throw new Error(`${option} requires a value.`);
|
|
281
|
+
}
|
|
282
|
+
return value;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function requireResponse(response, expectedType) {
|
|
286
|
+
if (response.type !== expectedType) {
|
|
287
|
+
throw new Error(
|
|
288
|
+
`Expected ${expectedType}, received ${response.type ?? 'unknown'}.`,
|
|
289
|
+
);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function printHelp() {
|
|
294
|
+
console.log(`Usage: web-terminal [--url URL] [--token TOKEN] <command>
|
|
295
|
+
|
|
296
|
+
Commands:
|
|
297
|
+
sessions List visible sessions
|
|
298
|
+
open [name] Open a named session and print its ID
|
|
299
|
+
send <session-id> <command> Write a command followed by Enter
|
|
300
|
+
write <session-id> <data> Write exact input without Enter
|
|
301
|
+
follow <session-id> Print history and follow live output
|
|
302
|
+
close <session-id> Permanently close a session
|
|
303
|
+
|
|
304
|
+
Environment:
|
|
305
|
+
WEB_TERMINAL_URL
|
|
306
|
+
WEB_TERMINAL_TOKEN`);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
await main();
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export { createTerminalClient, TerminalClient, type DisplayOptions, type OpenOptions, type TerminalClientEvent, type TerminalClientOptions, } from './terminal-client.js';
|
|
2
|
+
export { TerminalView } from './terminal-view.js';
|
|
3
|
+
export type { ClientMessage, ServerMessage, SessionSummary, } from './protocol.js';
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
export interface SessionSummary {
|
|
2
|
+
readonly id: string;
|
|
3
|
+
readonly name: string;
|
|
4
|
+
readonly state: 'running' | 'exited';
|
|
5
|
+
readonly attached: boolean;
|
|
6
|
+
readonly observers: number;
|
|
7
|
+
readonly createdAt: string;
|
|
8
|
+
readonly lastActiveAt: string;
|
|
9
|
+
readonly exitCode?: number;
|
|
10
|
+
}
|
|
11
|
+
export type ClientMessage = {
|
|
12
|
+
type: 'open';
|
|
13
|
+
requestId: string;
|
|
14
|
+
name?: string;
|
|
15
|
+
} | {
|
|
16
|
+
type: 'close';
|
|
17
|
+
requestId: string;
|
|
18
|
+
sessionId: string;
|
|
19
|
+
} | {
|
|
20
|
+
type: 'attach';
|
|
21
|
+
requestId: string;
|
|
22
|
+
sessionId: string;
|
|
23
|
+
} | {
|
|
24
|
+
type: 'detach';
|
|
25
|
+
requestId: string;
|
|
26
|
+
sessionId: string;
|
|
27
|
+
} | {
|
|
28
|
+
type: 'write';
|
|
29
|
+
requestId: string;
|
|
30
|
+
sessionId: string;
|
|
31
|
+
data: string;
|
|
32
|
+
} | {
|
|
33
|
+
type: 'observe';
|
|
34
|
+
requestId: string;
|
|
35
|
+
sessionId: string;
|
|
36
|
+
} | {
|
|
37
|
+
type: 'unobserve';
|
|
38
|
+
requestId: string;
|
|
39
|
+
sessionId: string;
|
|
40
|
+
} | {
|
|
41
|
+
type: 'input';
|
|
42
|
+
sessionId: string;
|
|
43
|
+
data: string;
|
|
44
|
+
} | {
|
|
45
|
+
type: 'resize';
|
|
46
|
+
sessionId: string;
|
|
47
|
+
cols: number;
|
|
48
|
+
rows: number;
|
|
49
|
+
};
|
|
50
|
+
export type ServerMessage = {
|
|
51
|
+
type: 'sessions';
|
|
52
|
+
sessions: SessionSummary[];
|
|
53
|
+
} | {
|
|
54
|
+
type: 'opened';
|
|
55
|
+
requestId: string;
|
|
56
|
+
session: SessionSummary;
|
|
57
|
+
} | {
|
|
58
|
+
type: 'closed';
|
|
59
|
+
requestId: string;
|
|
60
|
+
sessionId: string;
|
|
61
|
+
} | {
|
|
62
|
+
type: 'attached';
|
|
63
|
+
requestId: string;
|
|
64
|
+
session: SessionSummary;
|
|
65
|
+
history: string;
|
|
66
|
+
} | {
|
|
67
|
+
type: 'detached';
|
|
68
|
+
requestId: string;
|
|
69
|
+
sessionId: string;
|
|
70
|
+
} | {
|
|
71
|
+
type: 'written';
|
|
72
|
+
requestId: string;
|
|
73
|
+
sessionId: string;
|
|
74
|
+
} | {
|
|
75
|
+
type: 'observing';
|
|
76
|
+
requestId: string;
|
|
77
|
+
session: SessionSummary;
|
|
78
|
+
history: string;
|
|
79
|
+
} | {
|
|
80
|
+
type: 'unobserved';
|
|
81
|
+
requestId: string;
|
|
82
|
+
sessionId: string;
|
|
83
|
+
} | {
|
|
84
|
+
type: 'output';
|
|
85
|
+
sessionId: string;
|
|
86
|
+
data: string;
|
|
87
|
+
} | {
|
|
88
|
+
type: 'exit';
|
|
89
|
+
sessionId: string;
|
|
90
|
+
exitCode: number;
|
|
91
|
+
signal?: number;
|
|
92
|
+
} | {
|
|
93
|
+
type: 'error';
|
|
94
|
+
message: string;
|
|
95
|
+
requestId?: string;
|
|
96
|
+
};
|
|
97
|
+
export declare function parseClientMessage(value: unknown): ClientMessage | undefined;
|
|
98
|
+
export declare function parseServerMessage(value: unknown): ServerMessage | undefined;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type { ITerminalOptions } from '@xterm/xterm';
|
|
2
|
+
import { type SessionSummary } from './protocol.js';
|
|
3
|
+
import { TerminalView } from './terminal-view.js';
|
|
4
|
+
export interface TerminalClientOptions {
|
|
5
|
+
socketUrl: string | (() => string);
|
|
6
|
+
autoConnect?: boolean;
|
|
7
|
+
onError?: (error: Error) => void;
|
|
8
|
+
}
|
|
9
|
+
export interface OpenOptions {
|
|
10
|
+
name?: string;
|
|
11
|
+
}
|
|
12
|
+
export interface DisplayOptions {
|
|
13
|
+
terminalOptions?: ITerminalOptions;
|
|
14
|
+
}
|
|
15
|
+
export interface TerminalClientEvent {
|
|
16
|
+
readonly type: 'connected' | 'disconnected' | 'opened' | 'attached' | 'detached' | 'closed' | 'exit';
|
|
17
|
+
readonly sessionId?: string;
|
|
18
|
+
readonly exitCode?: number;
|
|
19
|
+
}
|
|
20
|
+
export declare class TerminalClient {
|
|
21
|
+
private readonly options;
|
|
22
|
+
private readonly sessionListeners;
|
|
23
|
+
private readonly eventListeners;
|
|
24
|
+
private readonly pending;
|
|
25
|
+
private socket;
|
|
26
|
+
private connectPromise;
|
|
27
|
+
private sessionSnapshot;
|
|
28
|
+
private attachedSessionId;
|
|
29
|
+
private view;
|
|
30
|
+
private viewSessionId;
|
|
31
|
+
private bufferedOutput;
|
|
32
|
+
constructor(options: TerminalClientOptions);
|
|
33
|
+
get sessions(): readonly SessionSummary[];
|
|
34
|
+
get connected(): boolean;
|
|
35
|
+
get attachedSession(): string | undefined;
|
|
36
|
+
connect(): Promise<void>;
|
|
37
|
+
disconnect(): void;
|
|
38
|
+
open(options?: OpenOptions): Promise<SessionSummary>;
|
|
39
|
+
close(sessionId: string): Promise<void>;
|
|
40
|
+
write(sessionId: string, data: string): Promise<void>;
|
|
41
|
+
attach(sessionId: string): Promise<{
|
|
42
|
+
session: SessionSummary;
|
|
43
|
+
history: string;
|
|
44
|
+
}>;
|
|
45
|
+
detach(): Promise<void>;
|
|
46
|
+
private detachSession;
|
|
47
|
+
display(sessionId: string, target: HTMLElement, options?: DisplayOptions): Promise<TerminalView>;
|
|
48
|
+
subscribeSessions(listener: (sessions: readonly SessionSummary[]) => void): () => void;
|
|
49
|
+
subscribeEvents(listener: (event: TerminalClientEvent) => void): () => void;
|
|
50
|
+
dispose(): void;
|
|
51
|
+
private input;
|
|
52
|
+
private resize;
|
|
53
|
+
private releaseView;
|
|
54
|
+
private disposeView;
|
|
55
|
+
private request;
|
|
56
|
+
private send;
|
|
57
|
+
private handleMessage;
|
|
58
|
+
private rejectPending;
|
|
59
|
+
private emitEvent;
|
|
60
|
+
private reportError;
|
|
61
|
+
}
|
|
62
|
+
export declare function createTerminalClient(options: TerminalClientOptions): TerminalClient;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { Terminal, type ITerminalOptions } from '@xterm/xterm';
|
|
2
|
+
import '@xterm/xterm/css/xterm.css';
|
|
3
|
+
import './web-terminal.css';
|
|
4
|
+
export interface TerminalViewController {
|
|
5
|
+
input(data: string): void;
|
|
6
|
+
resize(cols: number, rows: number): void;
|
|
7
|
+
release(view: TerminalView): void;
|
|
8
|
+
}
|
|
9
|
+
export interface TerminalViewOptions {
|
|
10
|
+
target: HTMLElement;
|
|
11
|
+
controller: TerminalViewController;
|
|
12
|
+
terminalOptions?: ITerminalOptions;
|
|
13
|
+
}
|
|
14
|
+
export declare class TerminalView {
|
|
15
|
+
readonly terminal: Terminal;
|
|
16
|
+
private readonly fitAddon;
|
|
17
|
+
private readonly target;
|
|
18
|
+
private readonly controller;
|
|
19
|
+
private readonly resizeObserver;
|
|
20
|
+
private disposed;
|
|
21
|
+
constructor(options: TerminalViewOptions);
|
|
22
|
+
write(data: string): void;
|
|
23
|
+
focus(): void;
|
|
24
|
+
fit(): void;
|
|
25
|
+
dispose(): void;
|
|
26
|
+
disposeVisuals(): void;
|
|
27
|
+
private assertActive;
|
|
28
|
+
}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
.xterm{cursor:text;-webkit-user-select:none;user-select:none;position:relative}.xterm.focus,.xterm:focus{outline:none}.xterm .xterm-helpers{z-index:5;position:absolute;top:0}.xterm .xterm-helper-textarea{opacity:0;z-index:-5;white-space:nowrap;resize:none;border:0;width:0;height:0;margin:0;padding:0;position:absolute;top:0;left:-9999em;overflow:hidden}.xterm .composition-view{color:#fff;white-space:nowrap;z-index:1;background:#000;display:none;position:absolute}.xterm .composition-view.active{display:block}.xterm .xterm-viewport{cursor:default;background-color:#000;position:absolute;inset:0;overflow-y:scroll}.xterm .xterm-screen{position:relative}.xterm .xterm-screen canvas{position:absolute;top:0;left:0}.xterm-char-measure-element{visibility:hidden;line-height:normal;display:inline-block;position:absolute;top:0;left:-9999em}.xterm.enable-mouse-events{cursor:default}.xterm.xterm-cursor-pointer,.xterm .xterm-cursor-pointer{cursor:pointer}.xterm.column-select.focus{cursor:crosshair}.xterm .xterm-accessibility:not(.debug),.xterm .xterm-message{z-index:10;color:#0000;pointer-events:none;position:absolute;inset:0}.xterm .xterm-accessibility-tree:not(.debug) ::selection{color:#0000}.xterm .xterm-accessibility-tree{-webkit-user-select:text;user-select:text;white-space:pre;font-family:monospace}.xterm .xterm-accessibility-tree>div{transform-origin:0;width:fit-content}.xterm .live-region{width:1px;height:1px;position:absolute;left:-9999px;overflow:hidden}.xterm-dim{opacity:1!important}.xterm-underline-1{text-decoration:underline}.xterm-underline-2{-webkit-text-decoration:underline double;text-decoration:underline double}.xterm-underline-3{-webkit-text-decoration:underline wavy;text-decoration:underline wavy}.xterm-underline-4{-webkit-text-decoration:underline dotted;text-decoration:underline dotted}.xterm-underline-5{-webkit-text-decoration:underline dashed;text-decoration:underline dashed}.xterm-overline{text-decoration:overline}.xterm-overline.xterm-underline-1{text-decoration:underline overline}.xterm-overline.xterm-underline-2{-webkit-text-decoration:overline double underline;text-decoration:overline double underline}.xterm-overline.xterm-underline-3{-webkit-text-decoration:overline wavy underline;text-decoration:overline wavy underline}.xterm-overline.xterm-underline-4{-webkit-text-decoration:overline dotted underline;text-decoration:overline dotted underline}.xterm-overline.xterm-underline-5{-webkit-text-decoration:overline dashed underline;text-decoration:overline dashed underline}.xterm-strikethrough{text-decoration:line-through}.xterm-screen .xterm-decoration-container .xterm-decoration{z-index:6;position:absolute}.xterm-screen .xterm-decoration-container .xterm-decoration.xterm-decoration-top-layer{z-index:7}.xterm-decoration-overview-ruler{z-index:8;pointer-events:none;position:absolute;top:0;right:0}.xterm-decoration-top{z-index:2;position:relative}.xterm .xterm-scrollable-element>.scrollbar{cursor:default}.xterm .xterm-scrollable-element>.scrollbar>.scra{cursor:pointer;font-size:11px!important}.xterm .xterm-scrollable-element>.visible{opacity:1;z-index:11;background:0 0;transition:opacity .1s linear}.xterm .xterm-scrollable-element>.invisible{opacity:0;pointer-events:none}.xterm .xterm-scrollable-element>.invisible.fade{transition:opacity .8s linear}.xterm .xterm-scrollable-element>.shadow{display:none;position:absolute}.xterm .xterm-scrollable-element>.shadow.top{width:100%;height:3px;box-shadow:var(--vscode-scrollbar-shadow,#000) 0 6px 6px -6px inset;display:block;top:0;left:3px}.xterm .xterm-scrollable-element>.shadow.left{width:3px;height:100%;box-shadow:var(--vscode-scrollbar-shadow,#000) 6px 0 6px -6px inset;display:block;top:3px;left:0}.xterm .xterm-scrollable-element>.shadow.top-left-corner{width:3px;height:3px;display:block;top:0;left:0}.xterm .xterm-scrollable-element>.shadow.top.left{box-shadow:var(--vscode-scrollbar-shadow,#000) 6px 0 6px -6px inset}.web-terminal{box-sizing:border-box;background:#111318;min-width:0;min-height:0;overflow:hidden}.web-terminal .xterm{height:100%;padding:12px}.web-terminal .xterm-viewport{scrollbar-color:#434957 transparent;scrollbar-width:thin}
|
|
2
|
+
/*$vite$:1*/
|