@vibesdotdev/terminal-client 0.0.0-stage → 1.0.0-rc.2
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/LICENSE +18 -0
- package/README.md +102 -2
- package/dist/ansi/index.d.ts +11 -0
- package/dist/ansi/index.d.ts.map +1 -0
- package/dist/ansi/index.js +11 -0
- package/dist/ansi/index.js.map +1 -0
- package/dist/ansi/keymap.d.ts +77 -0
- package/dist/ansi/keymap.d.ts.map +1 -0
- package/dist/ansi/keymap.js +257 -0
- package/dist/ansi/keymap.js.map +1 -0
- package/dist/api-client-plugin.d.ts +4 -0
- package/dist/api-client-plugin.d.ts.map +1 -0
- package/dist/api-client-plugin.js +16 -0
- package/dist/api-client-plugin.js.map +1 -0
- package/dist/client/http-session.d.ts +44 -0
- package/dist/client/http-session.d.ts.map +1 -0
- package/dist/client/http-session.js +457 -0
- package/dist/client/http-session.js.map +1 -0
- package/dist/client/index.d.ts +8 -0
- package/dist/client/index.d.ts.map +1 -0
- package/dist/client/index.js +8 -0
- package/dist/client/index.js.map +1 -0
- package/dist/client.d.ts +56 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +169 -0
- package/dist/client.js.map +1 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +14 -0
- package/dist/index.js.map +1 -0
- package/dist/pty-bridge/index.d.ts +15 -0
- package/dist/pty-bridge/index.d.ts.map +1 -0
- package/dist/pty-bridge/index.js +15 -0
- package/dist/pty-bridge/index.js.map +1 -0
- package/dist/pty-bridge/protocol.d.ts +6 -0
- package/dist/pty-bridge/protocol.d.ts.map +1 -0
- package/dist/pty-bridge/protocol.js +6 -0
- package/dist/pty-bridge/protocol.js.map +1 -0
- package/dist/pty-bridge/server.d.ts +7 -0
- package/dist/pty-bridge/server.d.ts.map +1 -0
- package/dist/pty-bridge/server.js +9 -0
- package/dist/pty-bridge/server.js.map +1 -0
- package/dist/remote-session.d.ts +51 -0
- package/dist/remote-session.d.ts.map +1 -0
- package/dist/remote-session.js +326 -0
- package/dist/remote-session.js.map +1 -0
- package/dist/schemas/http.d.ts +61 -0
- package/dist/schemas/http.d.ts.map +1 -0
- package/dist/schemas/http.js +45 -0
- package/dist/schemas/http.js.map +1 -0
- package/dist/schemas/remote.d.ts +25 -0
- package/dist/schemas/remote.d.ts.map +1 -0
- package/dist/schemas/remote.js +23 -0
- package/dist/schemas/remote.js.map +1 -0
- package/dist/schemas/session.d.ts +26 -0
- package/dist/schemas/session.d.ts.map +1 -0
- package/dist/schemas/session.js +17 -0
- package/dist/schemas/session.js.map +1 -0
- package/dist/schemas/store.d.ts +29 -0
- package/dist/schemas/store.d.ts.map +1 -0
- package/dist/schemas/store.js +22 -0
- package/dist/schemas/store.js.map +1 -0
- package/dist/schemas/terminal.d.ts +296 -0
- package/dist/schemas/terminal.d.ts.map +1 -0
- package/dist/schemas/terminal.js +243 -0
- package/dist/schemas/terminal.js.map +1 -0
- package/dist/server/index.d.ts +9 -0
- package/dist/server/index.d.ts.map +1 -0
- package/dist/server/index.js +9 -0
- package/dist/server/index.js.map +1 -0
- package/dist/server/session-store.d.ts +176 -0
- package/dist/server/session-store.d.ts.map +1 -0
- package/dist/server/session-store.js +462 -0
- package/dist/server/session-store.js.map +1 -0
- package/dist/server/sse-stream.d.ts +32 -0
- package/dist/server/sse-stream.d.ts.map +1 -0
- package/dist/server/sse-stream.js +87 -0
- package/dist/server/sse-stream.js.map +1 -0
- package/dist/svelte/HostPicker.svelte +256 -0
- package/dist/svelte/HostPicker.svelte.d.ts +30 -0
- package/dist/svelte/HostPicker.svelte.d.ts.map +1 -0
- package/dist/svelte/README.md +52 -0
- package/dist/svelte/ReconnectBanner.svelte +88 -0
- package/dist/svelte/ReconnectBanner.svelte.d.ts +16 -0
- package/dist/svelte/ReconnectBanner.svelte.d.ts.map +1 -0
- package/dist/svelte/SessionTabs.svelte +192 -0
- package/dist/svelte/SessionTabs.svelte.d.ts +17 -0
- package/dist/svelte/SessionTabs.svelte.d.ts.map +1 -0
- package/dist/svelte/Terminal.svelte +567 -0
- package/dist/svelte/Terminal.svelte.d.ts +25 -0
- package/dist/svelte/Terminal.svelte.d.ts.map +1 -0
- package/dist/svelte/TerminalMobileToolbar.svelte +56 -0
- package/dist/svelte/TerminalMobileToolbar.svelte.d.ts +14 -0
- package/dist/svelte/TerminalMobileToolbar.svelte.d.ts.map +1 -0
- package/dist/svelte/TerminalWorkspace.svelte +562 -0
- package/dist/svelte/TerminalWorkspace.svelte.d.ts +25 -0
- package/dist/svelte/TerminalWorkspace.svelte.d.ts.map +1 -0
- package/dist/svelte/ambient.d.ts +9 -0
- package/dist/svelte/docs/terminal-svelte.docs.descriptor.d.ts +4 -0
- package/dist/svelte/docs/terminal-svelte.docs.descriptor.d.ts.map +1 -0
- package/dist/svelte/docs/terminal-svelte.docs.descriptor.js +74 -0
- package/dist/svelte/index.d.ts +27 -0
- package/dist/svelte/index.d.ts.map +1 -0
- package/dist/svelte/index.js +26 -0
- package/dist/svelte/styles.css +8 -0
- package/dist/svelte/terminal-session-authority.d.ts +17 -0
- package/dist/svelte/terminal-session-authority.d.ts.map +1 -0
- package/dist/svelte/terminal-session-authority.js +25 -0
- package/dist/svelte/terminal-svelte.plugin.d.ts +19 -0
- package/dist/svelte/terminal-svelte.plugin.d.ts.map +1 -0
- package/dist/svelte/terminal-svelte.plugin.js +42 -0
- package/dist/svelte/terminal-theme.d.ts +11 -0
- package/dist/svelte/terminal-theme.d.ts.map +1 -0
- package/dist/svelte/terminal-theme.js +116 -0
- package/dist/svelte/tool-widgets/TerminalToolCard.svelte +65 -0
- package/dist/svelte/tool-widgets/TerminalToolCard.svelte.d.ts +5 -0
- package/dist/svelte/tool-widgets/TerminalToolCard.svelte.d.ts.map +1 -0
- package/dist/svelte/tool-widgets/terminal-tool-card.d.ts +25 -0
- package/dist/svelte/tool-widgets/terminal-tool-card.d.ts.map +1 -0
- package/dist/svelte/tool-widgets/terminal-tool-card.js +116 -0
- package/dist/svelte/types.d.ts +160 -0
- package/dist/svelte/types.d.ts.map +1 -0
- package/dist/svelte/types.js +51 -0
- package/dist/svelte/workspace.d.ts +3 -0
- package/dist/svelte/workspace.d.ts.map +1 -0
- package/dist/svelte/workspace.js +2 -0
- package/dist/terminal-client.plugin.d.ts +3 -0
- package/dist/terminal-client.plugin.d.ts.map +1 -0
- package/dist/terminal-client.plugin.js +3 -0
- package/dist/terminal-client.plugin.js.map +1 -0
- package/dist/terminal-runtime.d.ts +30 -0
- package/dist/terminal-runtime.d.ts.map +1 -0
- package/dist/terminal-runtime.js +673 -0
- package/dist/terminal-runtime.js.map +1 -0
- package/dist/terminal.consumer.plugin.d.ts +14 -0
- package/dist/terminal.consumer.plugin.d.ts.map +1 -0
- package/dist/terminal.consumer.plugin.js +31 -0
- package/dist/terminal.consumer.plugin.js.map +1 -0
- package/dist/tools/terminal.impl.consumer.d.ts +11 -0
- package/dist/tools/terminal.impl.consumer.d.ts.map +1 -0
- package/dist/tools/terminal.impl.consumer.js +20 -0
- package/dist/tools/terminal.impl.consumer.js.map +1 -0
- package/dist/tools/terminal.schemas.d.ts +3 -0
- package/dist/tools/terminal.schemas.d.ts.map +1 -0
- package/dist/tools/terminal.schemas.js +3 -0
- package/dist/tools/terminal.schemas.js.map +1 -0
- package/dist/tools/terminal.unified.descriptor.d.ts +218 -0
- package/dist/tools/terminal.unified.descriptor.d.ts.map +1 -0
- package/dist/tools/terminal.unified.descriptor.js +36 -0
- package/dist/tools/terminal.unified.descriptor.js.map +1 -0
- package/dist/tui/index.d.ts +9 -0
- package/dist/tui/index.d.ts.map +1 -0
- package/dist/tui/index.js +8 -0
- package/dist/tui/index.js.map +1 -0
- package/dist/tui/terminal-open.descriptor.d.ts +9 -0
- package/dist/tui/terminal-open.descriptor.d.ts.map +1 -0
- package/dist/tui/terminal-open.descriptor.js +18 -0
- package/dist/tui/terminal-open.descriptor.js.map +1 -0
- package/dist/tui/terminal-open.impl.consumer.d.ts +9 -0
- package/dist/tui/terminal-open.impl.consumer.d.ts.map +1 -0
- package/dist/tui/terminal-open.impl.consumer.js +13 -0
- package/dist/tui/terminal-open.impl.consumer.js.map +1 -0
- package/dist/tui/terminal-registry.descriptor.d.ts +11 -0
- package/dist/tui/terminal-registry.descriptor.d.ts.map +1 -0
- package/dist/tui/terminal-registry.descriptor.js +17 -0
- package/dist/tui/terminal-registry.descriptor.js.map +1 -0
- package/dist/tui/terminal-registry.impl.consumer.d.ts +71 -0
- package/dist/tui/terminal-registry.impl.consumer.d.ts.map +1 -0
- package/dist/tui/terminal-registry.impl.consumer.js +190 -0
- package/dist/tui/terminal-registry.impl.consumer.js.map +1 -0
- package/dist/tui/terminal-tui.plugin.d.ts +10 -0
- package/dist/tui/terminal-tui.plugin.d.ts.map +1 -0
- package/dist/tui/terminal-tui.plugin.js +32 -0
- package/dist/tui/terminal-tui.plugin.js.map +1 -0
- package/dist/tui/terminal.descriptor.d.ts +10 -0
- package/dist/tui/terminal.descriptor.d.ts.map +1 -0
- package/dist/tui/terminal.descriptor.js +25 -0
- package/dist/tui/terminal.descriptor.js.map +1 -0
- package/dist/tui/terminal.impl.consumer.d.ts +14 -0
- package/dist/tui/terminal.impl.consumer.d.ts.map +1 -0
- package/dist/tui/terminal.impl.consumer.js +189 -0
- package/dist/tui/terminal.impl.consumer.js.map +1 -0
- package/dist/tui/terminal.state-key.d.ts +30 -0
- package/dist/tui/terminal.state-key.d.ts.map +1 -0
- package/dist/tui/terminal.state-key.js +9 -0
- package/dist/tui/terminal.state-key.js.map +1 -0
- package/dist/tui/terminal.state.d.ts +152 -0
- package/dist/tui/terminal.state.d.ts.map +1 -0
- package/dist/tui/terminal.state.js +149 -0
- package/dist/tui/terminal.state.js.map +1 -0
- package/dist/workspace.d.ts +155 -0
- package/dist/workspace.d.ts.map +1 -0
- package/dist/workspace.js +292 -0
- package/dist/workspace.js.map +1 -0
- package/package.json +228 -4
|
@@ -0,0 +1,567 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* <Terminal session={…} /> — the xterm.js rendering surface.
|
|
4
|
+
*
|
|
5
|
+
* Renders an L3 `ExecutionSession`'s byte stream. Transport-agnostic: the
|
|
6
|
+
* session may be a real local PTY (L9a) or an RPC proxy (L9b) — the component
|
|
7
|
+
* only ever sees the `ExecutionSession` interface.
|
|
8
|
+
*
|
|
9
|
+
* SSR safety: `@xterm/xterm` + addons are browser-only (they touch `document`
|
|
10
|
+
* and WebGL). They are imported INSIDE `onMount` via dynamic `import()`, never
|
|
11
|
+
* at module top-level, so this component is import-safe in a SvelteKit server
|
|
12
|
+
* render. This also sidesteps the `*-svelte` barrel-index `$state` trap — no
|
|
13
|
+
* eager xterm/DOM import leaks into a non-browser context.
|
|
14
|
+
*
|
|
15
|
+
* connectionState precedence (P6):
|
|
16
|
+
* The component internally derives 'live' / 'exited' from session events.
|
|
17
|
+
* When the `connectionState` prop is supplied by the parent (e.g. 'reconnecting',
|
|
18
|
+
* 'reconnected', 'failed') it takes precedence over the internal derivation, so
|
|
19
|
+
* the parent can drive the full state machine (transport reconnect flows).
|
|
20
|
+
*/
|
|
21
|
+
import { onMount } from 'svelte';
|
|
22
|
+
import { peekRuntimeSvelteContext } from '@vibesdotdev/surface-sveltekit/svelte';
|
|
23
|
+
import { cn } from '@vibesdotdev/kit/utils';
|
|
24
|
+
import { resolveSequence, chordToSequence } from '@vibesdotdev/terminal-client/ansi';
|
|
25
|
+
import { CapabilityError, CaughtFailureSchema } from '@vibesdotdev/kernel';
|
|
26
|
+
import type { ExecutionSession } from '@vibesdotdev/kernel/execution';
|
|
27
|
+
import { DEFAULT_FONT, DEFAULT_TERMINAL_THEME, type TerminalProps } from './types.js';
|
|
28
|
+
import { TerminalSessionAuthority } from './terminal-session-authority.js';
|
|
29
|
+
import { observeTerminalAppearance, readTerminalAppearance } from './terminal-theme.js';
|
|
30
|
+
import ReconnectBanner from './ReconnectBanner.svelte';
|
|
31
|
+
import TerminalMobileToolbar from './TerminalMobileToolbar.svelte';
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Connection state values driven externally or derived internally.
|
|
35
|
+
* connecting — xterm not yet mounted (boot async in progress)
|
|
36
|
+
* live — session active, no banner
|
|
37
|
+
* reconnecting — transport dropped, retry in progress
|
|
38
|
+
* reconnected — transport restored (auto-dismissed by caller)
|
|
39
|
+
* failed — unrecoverable transport drop
|
|
40
|
+
* exited — shell process exited cleanly
|
|
41
|
+
*/
|
|
42
|
+
export type ConnectionState =
|
|
43
|
+
'connecting' | 'live' | 'reconnecting' | 'reconnected' | 'failed' | 'exited';
|
|
44
|
+
|
|
45
|
+
interface ExtendedProps extends TerminalProps {
|
|
46
|
+
/**
|
|
47
|
+
* External connection state override. When provided it takes precedence
|
|
48
|
+
* over the component's internally-derived state (live / exited).
|
|
49
|
+
* Defaults to 'connecting' on first render.
|
|
50
|
+
*/
|
|
51
|
+
connectionState?: ConnectionState;
|
|
52
|
+
/** Called when the user clicks "Start new session" in failed/exited state. */
|
|
53
|
+
onNewSession?: () => void;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
let {
|
|
57
|
+
session = null,
|
|
58
|
+
onReady,
|
|
59
|
+
onInputReady,
|
|
60
|
+
theme,
|
|
61
|
+
font,
|
|
62
|
+
class: className = '',
|
|
63
|
+
onExit,
|
|
64
|
+
onDisconnect,
|
|
65
|
+
readonly = false,
|
|
66
|
+
connectionState: externalConnectionState,
|
|
67
|
+
onNewSession,
|
|
68
|
+
cols: lockedCols,
|
|
69
|
+
rows: lockedRows
|
|
70
|
+
}: ExtendedProps = $props();
|
|
71
|
+
|
|
72
|
+
function lockedGrid(): { cols: number; rows: number } | null {
|
|
73
|
+
const cols = Number(lockedCols);
|
|
74
|
+
const rows = Number(lockedRows);
|
|
75
|
+
if (Number.isInteger(cols) && cols > 0 && Number.isInteger(rows) && rows > 0) {
|
|
76
|
+
return { cols, rows };
|
|
77
|
+
}
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
let host = $state<HTMLDivElement | null>(null);
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Internal derived state — updated from session lifecycle events.
|
|
85
|
+
* 'connecting' until xterm boots, then 'live', then 'exited'/'disconnected'.
|
|
86
|
+
*/
|
|
87
|
+
let internalState = $state<'connecting' | 'live' | 'exited' | 'disconnected'>('connecting');
|
|
88
|
+
let internalDetail = $state<string>('');
|
|
89
|
+
let rendererFallback = $state<string>('');
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Effective connection state — external prop wins when present.
|
|
93
|
+
* See file-level docblock for precedence rules.
|
|
94
|
+
*/
|
|
95
|
+
const effectiveState = $derived<ConnectionState>(
|
|
96
|
+
externalConnectionState !== undefined
|
|
97
|
+
? externalConnectionState
|
|
98
|
+
: internalState === 'disconnected'
|
|
99
|
+
? 'failed'
|
|
100
|
+
: internalState
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Map effectiveState to the ReconnectBanner status slot.
|
|
105
|
+
* null → banner hidden (live / connecting).
|
|
106
|
+
*/
|
|
107
|
+
const bannerStatus = $derived.by<'reconnecting' | 'reconnected' | 'failed' | null>(() => {
|
|
108
|
+
switch (effectiveState) {
|
|
109
|
+
case 'reconnecting':
|
|
110
|
+
return 'reconnecting';
|
|
111
|
+
case 'reconnected':
|
|
112
|
+
return 'reconnected';
|
|
113
|
+
case 'failed':
|
|
114
|
+
return 'failed';
|
|
115
|
+
case 'exited':
|
|
116
|
+
return 'failed'; // exited reuses the failed slot with a distinct message
|
|
117
|
+
default:
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Human-readable failure/exit reason forwarded to the banner.
|
|
124
|
+
*/
|
|
125
|
+
const bannerReason = $derived.by<string>(() => {
|
|
126
|
+
if (effectiveState === 'exited') {
|
|
127
|
+
return 'Session ended — the shell stopped running.';
|
|
128
|
+
}
|
|
129
|
+
if (effectiveState === 'failed') {
|
|
130
|
+
return internalDetail
|
|
131
|
+
? `Connection lost — ${internalDetail}`
|
|
132
|
+
: 'Connection lost and could not be restored.';
|
|
133
|
+
}
|
|
134
|
+
return '';
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
/** Whether the state calls for a "Start new session" button. */
|
|
138
|
+
const bannerShowAction = $derived<boolean>(
|
|
139
|
+
effectiveState === 'failed' || effectiveState === 'exited'
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
let appearance = $state({
|
|
143
|
+
theme: { ...DEFAULT_TERMINAL_THEME },
|
|
144
|
+
fontFamily: DEFAULT_FONT.family
|
|
145
|
+
});
|
|
146
|
+
const mergedTheme = $derived({ ...appearance.theme, ...theme });
|
|
147
|
+
const fontFamily = $derived(font?.family ?? appearance.fontFamily);
|
|
148
|
+
const fontSize = $derived(font?.size ?? DEFAULT_FONT.size);
|
|
149
|
+
let refit: (() => void) | null = null;
|
|
150
|
+
let mobileCtrl = $state(false);
|
|
151
|
+
let mobileAlt = $state(false);
|
|
152
|
+
|
|
153
|
+
const encoder = new TextEncoder();
|
|
154
|
+
const runtimeStore = peekRuntimeSvelteContext();
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* The xterm instance once booted, and the session currently wired to it.
|
|
158
|
+
* Input paths (onData / key chords / onResize) are registered ONCE at boot
|
|
159
|
+
* and route through `sessionAuthority` so a session can attach late or be
|
|
160
|
+
* swapped without re-registering xterm handlers.
|
|
161
|
+
*/
|
|
162
|
+
let xterm = $state<import('@xterm/xterm').Terminal | null>(null);
|
|
163
|
+
const sessionAuthority = new TerminalSessionAuthority<ExecutionSession>();
|
|
164
|
+
let detachActiveSession: (() => void) | null = null;
|
|
165
|
+
|
|
166
|
+
onMount(() => {
|
|
167
|
+
// All xterm imports are deferred to the browser. Cleanup is registered as
|
|
168
|
+
// teardown handles inside the async boot; onMount's returned disposer drains
|
|
169
|
+
// them whether or not boot finished.
|
|
170
|
+
let disposed = false;
|
|
171
|
+
const teardown: Array<() => void> = [];
|
|
172
|
+
const observeUiTask = (task: Promise<void>): void => {
|
|
173
|
+
void task
|
|
174
|
+
.then(
|
|
175
|
+
() => ({ kind: 'complete' as const }),
|
|
176
|
+
(cause) => ({
|
|
177
|
+
kind: 'failure' as const,
|
|
178
|
+
failure: CapabilityError.from(CaughtFailureSchema.parse(cause))
|
|
179
|
+
})
|
|
180
|
+
)
|
|
181
|
+
.then((outcome) => {
|
|
182
|
+
if (outcome.kind === 'complete' || disposed) return;
|
|
183
|
+
internalState = 'disconnected';
|
|
184
|
+
internalDetail = outcome.failure.message;
|
|
185
|
+
onDisconnect?.(outcome.failure.message);
|
|
186
|
+
});
|
|
187
|
+
};
|
|
188
|
+
const runTeardown = (dispose: () => void) => {
|
|
189
|
+
try {
|
|
190
|
+
dispose();
|
|
191
|
+
return { kind: 'complete' as const };
|
|
192
|
+
} catch (cause) {
|
|
193
|
+
return {
|
|
194
|
+
kind: 'failure' as const,
|
|
195
|
+
failure: CapabilityError.from(CaughtFailureSchema.parse(cause))
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
const boot = async () => {
|
|
201
|
+
const { Terminal } = await import('@xterm/xterm');
|
|
202
|
+
const { FitAddon } = await import('@xterm/addon-fit');
|
|
203
|
+
await import('@xterm/xterm/css/xterm.css');
|
|
204
|
+
|
|
205
|
+
if (disposed || !host) return;
|
|
206
|
+
appearance = readTerminalAppearance(host);
|
|
207
|
+
teardown.push(
|
|
208
|
+
observeTerminalAppearance(host, () => {
|
|
209
|
+
if (host) appearance = readTerminalAppearance(host);
|
|
210
|
+
})
|
|
211
|
+
);
|
|
212
|
+
|
|
213
|
+
const term = new Terminal({
|
|
214
|
+
cursorBlink: true,
|
|
215
|
+
minimumContrastRatio: 4.5,
|
|
216
|
+
allowProposedApi: true,
|
|
217
|
+
// Start immediately with native metrics while an edge font is downloading.
|
|
218
|
+
// The font effect switches families after loading, invalidating xterm's metrics.
|
|
219
|
+
fontFamily: document.fonts.check(`${fontSize}px ${fontFamily}`) ? fontFamily : 'monospace',
|
|
220
|
+
fontSize,
|
|
221
|
+
theme: mergedTheme,
|
|
222
|
+
scrollback: 5000
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
const fit = new FitAddon();
|
|
226
|
+
term.loadAddon(fit);
|
|
227
|
+
term.open(host);
|
|
228
|
+
|
|
229
|
+
// P1 AUTOFOCUS — focus the terminal after mount, but only when the
|
|
230
|
+
// current active element is not an input/textarea/contenteditable outside
|
|
231
|
+
// the terminal. This eliminates the "click first to type" dead zone while
|
|
232
|
+
// respecting pre-existing focus (e.g. a form field the user is editing).
|
|
233
|
+
if (!disposed && host) {
|
|
234
|
+
const active = document.activeElement;
|
|
235
|
+
const isExternalInput =
|
|
236
|
+
active instanceof HTMLElement &&
|
|
237
|
+
!host.contains(active) &&
|
|
238
|
+
(active.tagName === 'INPUT' || active.tagName === 'TEXTAREA' || active.isContentEditable);
|
|
239
|
+
if (!isExternalInput) {
|
|
240
|
+
term.focus();
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// P7 FIT/PADDING — run initial fit AFTER flex layout settles via rAF.
|
|
245
|
+
// The surface has no padding (moved to a CSS wrapper) so FitAddon
|
|
246
|
+
// measures the full content box. A follow-up rAF ensures the browser
|
|
247
|
+
// has completed its layout pass before we ask for cols/rows.
|
|
248
|
+
const applyFit = () => {
|
|
249
|
+
if (disposed) return;
|
|
250
|
+
const lock = lockedGrid();
|
|
251
|
+
if (lock) term.resize(lock.cols, lock.rows);
|
|
252
|
+
else if (host && host.clientWidth > 0 && host.clientHeight > 0) fit.fit();
|
|
253
|
+
};
|
|
254
|
+
refit = applyFit;
|
|
255
|
+
teardown.push(() => {
|
|
256
|
+
refit = null;
|
|
257
|
+
});
|
|
258
|
+
const fontsLoaded = () => {
|
|
259
|
+
// xterm caches metrics even when a face arrives later under the same name.
|
|
260
|
+
// A public font option change remeasures cells and rebuilds the glyph atlas.
|
|
261
|
+
const family = term.options.fontFamily;
|
|
262
|
+
term.options.fontFamily = 'monospace';
|
|
263
|
+
term.options.fontFamily = family;
|
|
264
|
+
applyFit();
|
|
265
|
+
};
|
|
266
|
+
document.fonts?.addEventListener('loadingdone', fontsLoaded);
|
|
267
|
+
teardown.push(() => document.fonts?.removeEventListener('loadingdone', fontsLoaded));
|
|
268
|
+
|
|
269
|
+
// Initial fit: wait one animation frame for flex to settle, then report
|
|
270
|
+
// the fitted grid via onReady so the parent opens its session at the
|
|
271
|
+
// RIGHT size from the start. No boot-time PTY resize happens this way —
|
|
272
|
+
// crucial because Bun TUI children cache process.stdout.columns and
|
|
273
|
+
// never observe a post-boot resize.
|
|
274
|
+
//
|
|
275
|
+
// P8 RENDERER-BEFORE-FIT — the WebGL renderer must attach BEFORE the
|
|
276
|
+
// initial fit so fit-time cell metrics equal render-time metrics. The
|
|
277
|
+
// DOM renderer measures a wider cell (~7.95px at the default font)
|
|
278
|
+
// than WebGL renders (~7.0px); a fit taken against the DOM renderer
|
|
279
|
+
// under-reports cols, and because the PTY grid is fixed at the onReady
|
|
280
|
+
// size (no post-boot resize, see above) every route then shows a
|
|
281
|
+
// permanent dead band on the right. WebGL stays an optional
|
|
282
|
+
// enhancement, never a readiness dependency: the import is raced
|
|
283
|
+
// against a short timeout and any failure (no GPU, context refused,
|
|
284
|
+
// chunk stall) falls back to the default renderer, whose metrics the
|
|
285
|
+
// fit below then measures correctly. A late-arriving WebGL module is
|
|
286
|
+
// deliberately NOT loaded after the fit — that would reintroduce the
|
|
287
|
+
// exact metric mismatch this ordering exists to prevent.
|
|
288
|
+
const prepareRenderer = async () => {
|
|
289
|
+
if (disposed) return;
|
|
290
|
+
const loadRenderer = async () => {
|
|
291
|
+
// Standalone renderers need only a session. An enclosing runtime owns
|
|
292
|
+
// the optional GPU enhancement's deadline and cancellation when present.
|
|
293
|
+
if (!runtimeStore) return null;
|
|
294
|
+
const runtime = await runtimeStore.load();
|
|
295
|
+
return runtime.lifecycle.wait(() => import('@xterm/addon-webgl'), {
|
|
296
|
+
timeoutMs: 1000, reason: 'optional terminal renderer'
|
|
297
|
+
});
|
|
298
|
+
};
|
|
299
|
+
const renderer = await loadRenderer()
|
|
300
|
+
.then(
|
|
301
|
+
(module) => ({ kind: 'available' as const, module }),
|
|
302
|
+
(cause) => ({
|
|
303
|
+
kind: 'unavailable' as const,
|
|
304
|
+
failure: CapabilityError.from(CaughtFailureSchema.parse(cause))
|
|
305
|
+
})
|
|
306
|
+
);
|
|
307
|
+
if (renderer.kind === 'unavailable') {
|
|
308
|
+
rendererFallback = renderer.failure.message;
|
|
309
|
+
} else if (!disposed && renderer.module) {
|
|
310
|
+
const webgl = new renderer.module.WebglAddon();
|
|
311
|
+
webgl.onContextLoss(() => {
|
|
312
|
+
webgl.dispose();
|
|
313
|
+
});
|
|
314
|
+
term.loadAddon(webgl);
|
|
315
|
+
teardown.push(() => webgl.dispose());
|
|
316
|
+
}
|
|
317
|
+
if (disposed) return;
|
|
318
|
+
applyFit();
|
|
319
|
+
onReady?.({ cols: term.cols, rows: term.rows });
|
|
320
|
+
};
|
|
321
|
+
requestAnimationFrame(() => {
|
|
322
|
+
observeUiTask(prepareRenderer());
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
// --- Data path: xterm input → PTY (routes via session authority so the
|
|
326
|
+
// session may attach late or be swapped without re-registering) -------
|
|
327
|
+
// READ-ONLY GATE: when `readonly` is set, keystrokes are NOT forwarded to
|
|
328
|
+
// the PTY — the terminal renders output only (observe mode). The prop is
|
|
329
|
+
// read live inside the handler so toggling it takes effect without
|
|
330
|
+
// re-registering xterm handlers.
|
|
331
|
+
const onDataDisposable = term.onData((data: string) => {
|
|
332
|
+
if (readonly) return;
|
|
333
|
+
const input = data.length === 1 && (mobileCtrl || mobileAlt)
|
|
334
|
+
? resolveSequence({ key: data, ctrl: mobileCtrl, alt: mobileAlt }) : data;
|
|
335
|
+
mobileCtrl = false;
|
|
336
|
+
mobileAlt = false;
|
|
337
|
+
sessionAuthority.current?.write(encoder.encode(input));
|
|
338
|
+
});
|
|
339
|
+
teardown.push(() => onDataDisposable.dispose());
|
|
340
|
+
|
|
341
|
+
// --- Non-printing keys via the extracted key-map --------------------
|
|
342
|
+
// P4 NULL-SAFETY: chordToSequence may return null to mean "let the browser
|
|
343
|
+
// handle this key natively" (e.g. Ctrl/Cmd+V for paste). When it returns
|
|
344
|
+
// null we must NOT call session.write and must NOT preventDefault — return
|
|
345
|
+
// true so xterm passes the event to the browser.
|
|
346
|
+
term.attachCustomKeyEventHandler((ev: KeyboardEvent) => {
|
|
347
|
+
if (ev.type !== 'keydown') return true;
|
|
348
|
+
// READ-ONLY GATE: don't consume or forward any chord — let the browser
|
|
349
|
+
// handle it natively (selection, copy, etc.) and never write to the PTY.
|
|
350
|
+
if (readonly) return true;
|
|
351
|
+
const seq = chordToSequence({
|
|
352
|
+
key: ev.key,
|
|
353
|
+
ctrl: ev.ctrlKey,
|
|
354
|
+
alt: ev.altKey,
|
|
355
|
+
shift: ev.shiftKey
|
|
356
|
+
});
|
|
357
|
+
// null → browser-native behavior (clipboard paste, etc.) — do not consume.
|
|
358
|
+
if (seq === null) return true;
|
|
359
|
+
sessionAuthority.current?.write(encoder.encode(seq));
|
|
360
|
+
return false; // we owned this key
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
// --- Resize: FitAddon → PTY -----------------------------------------
|
|
364
|
+
const onResizeDisposable = term.onResize(({ cols, rows }) => {
|
|
365
|
+
// Propagate fitted cols/rows to the session resize callback.
|
|
366
|
+
sessionAuthority.current?.resize(cols, rows);
|
|
367
|
+
});
|
|
368
|
+
teardown.push(() => onResizeDisposable.dispose());
|
|
369
|
+
|
|
370
|
+
// P7 ResizeObserver-driven refit — reuses the existing observer pattern.
|
|
371
|
+
// The observer fires on every layout change so cols/rows always track
|
|
372
|
+
// the container; the onResize handler above forwards them to the PTY.
|
|
373
|
+
const ro = new ResizeObserver(() => applyFit());
|
|
374
|
+
if (host) {
|
|
375
|
+
ro.observe(host);
|
|
376
|
+
teardown.push(() => ro.disconnect());
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
teardown.push(() => term.dispose());
|
|
380
|
+
|
|
381
|
+
// Publish the booted instance — the $effect below attaches the session
|
|
382
|
+
// (output stream, exit watcher, input-ready flag) when both exist.
|
|
383
|
+
xterm = term;
|
|
384
|
+
teardown.push(() => {
|
|
385
|
+
detachActiveSession?.();
|
|
386
|
+
detachActiveSession = null;
|
|
387
|
+
sessionAuthority.detach();
|
|
388
|
+
xterm = null;
|
|
389
|
+
});
|
|
390
|
+
};
|
|
391
|
+
observeUiTask(boot());
|
|
392
|
+
|
|
393
|
+
return () => {
|
|
394
|
+
disposed = true;
|
|
395
|
+
for (const fn of teardown.splice(0)) {
|
|
396
|
+
const outcome = runTeardown(fn);
|
|
397
|
+
if (outcome.kind === 'failure') rendererFallback = outcome.failure.message;
|
|
398
|
+
}
|
|
399
|
+
};
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
$effect(() => {
|
|
403
|
+
if (!xterm) return;
|
|
404
|
+
xterm.options.theme = mergedTheme;
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
$effect(() => {
|
|
408
|
+
const term = xterm;
|
|
409
|
+
const family = fontFamily;
|
|
410
|
+
const size = fontSize;
|
|
411
|
+
if (!term || (term.options.fontFamily === family && term.options.fontSize === size)) return;
|
|
412
|
+
let cancelled = false;
|
|
413
|
+
const apply = () => {
|
|
414
|
+
if (cancelled) return;
|
|
415
|
+
term.options.fontFamily = family;
|
|
416
|
+
term.options.fontSize = size;
|
|
417
|
+
refit?.();
|
|
418
|
+
};
|
|
419
|
+
// Measure after the face loads; failed downloads still use the browser fallback.
|
|
420
|
+
void document.fonts
|
|
421
|
+
.load(`${size}px ${family}`)
|
|
422
|
+
.then(
|
|
423
|
+
() => ({ kind: 'loaded' as const }),
|
|
424
|
+
(cause) => ({
|
|
425
|
+
kind: 'fallback' as const,
|
|
426
|
+
failure: CapabilityError.from(CaughtFailureSchema.parse(cause))
|
|
427
|
+
})
|
|
428
|
+
)
|
|
429
|
+
.then((outcome) => {
|
|
430
|
+
if (outcome.kind === 'fallback') rendererFallback = outcome.failure.message;
|
|
431
|
+
apply();
|
|
432
|
+
});
|
|
433
|
+
return () => {
|
|
434
|
+
cancelled = true;
|
|
435
|
+
};
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
// Attach/detach the session to the booted xterm. Runs when either the
|
|
439
|
+
// session prop or the xterm instance becomes available; supports a session
|
|
440
|
+
// arriving after boot (the onReady → open-at-fitted-size flow).
|
|
441
|
+
$effect(() => {
|
|
442
|
+
const term = xterm;
|
|
443
|
+
const s = session;
|
|
444
|
+
if (!term) return;
|
|
445
|
+
if (!s) {
|
|
446
|
+
detachActiveSession?.();
|
|
447
|
+
detachActiveSession = null;
|
|
448
|
+
sessionAuthority.detach();
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
451
|
+
if (s === sessionAuthority.current) return;
|
|
452
|
+
|
|
453
|
+
detachActiveSession?.();
|
|
454
|
+
detachActiveSession = null;
|
|
455
|
+
const attachment = sessionAuthority.attach(s);
|
|
456
|
+
|
|
457
|
+
// --- Data path: PTY output → xterm ----------------------------------
|
|
458
|
+
const decoder = new TextDecoder();
|
|
459
|
+
const unsubscribeOutput = s.onOutput((bytes: Uint8Array) => {
|
|
460
|
+
// xterm.write accepts string or Uint8Array; we decode so multibyte
|
|
461
|
+
// and escape sequences survive intact (PTY stream is raw bytes).
|
|
462
|
+
term.write(decoder.decode(bytes, { stream: true }));
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
// Align the PTY with the current grid. When the parent opened the session
|
|
466
|
+
// at the onReady dimensions this is a no-op server-side; it only matters
|
|
467
|
+
// if the layout changed between open and attach.
|
|
468
|
+
s.resize(term.cols, term.rows);
|
|
469
|
+
|
|
470
|
+
internalState = 'live';
|
|
471
|
+
|
|
472
|
+
// Signal to test harnesses that the input path is fully wired END-TO-END
|
|
473
|
+
// (xterm onData registered at boot AND a session attached) so keystrokes
|
|
474
|
+
// typed on this flag are guaranteed to reach the PTY. The buffer reader
|
|
475
|
+
// is for tui:shot / Playwright waits: WebGL paints to a canvas, so
|
|
476
|
+
// `.xterm-screen` innerText is empty.
|
|
477
|
+
Reflect.set(window, '__vibesTerminalInputReady', true);
|
|
478
|
+
Reflect.set(window, '__vibesReadTerminalBuffer', () => {
|
|
479
|
+
const buf = term.buffer.active;
|
|
480
|
+
const lines: string[] = [];
|
|
481
|
+
for (let i = 0; i < buf.length; i++) {
|
|
482
|
+
lines.push(buf.getLine(i)?.translateToString(true) ?? '');
|
|
483
|
+
}
|
|
484
|
+
return lines.join('\n');
|
|
485
|
+
});
|
|
486
|
+
onInputReady?.();
|
|
487
|
+
|
|
488
|
+
// --- Lifecycle: exit + disconnect -----------------------------------
|
|
489
|
+
void s.exited()
|
|
490
|
+
.then(
|
|
491
|
+
(result) => ({ kind: 'exit' as const, result }),
|
|
492
|
+
(cause) => ({
|
|
493
|
+
kind: 'failure' as const,
|
|
494
|
+
failure: CapabilityError.from(CaughtFailureSchema.parse(cause))
|
|
495
|
+
})
|
|
496
|
+
)
|
|
497
|
+
.then((outcome) => {
|
|
498
|
+
if (!sessionAuthority.isCurrent(attachment)) return;
|
|
499
|
+
if (outcome.kind === 'failure') {
|
|
500
|
+
internalState = 'disconnected';
|
|
501
|
+
internalDetail = outcome.failure.message;
|
|
502
|
+
onDisconnect?.(outcome.failure.message);
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
505
|
+
const { result } = outcome;
|
|
506
|
+
internalState = 'exited';
|
|
507
|
+
internalDetail =
|
|
508
|
+
result.exitCode === null
|
|
509
|
+
? 'process terminated'
|
|
510
|
+
: `process exited (code ${result.exitCode})`;
|
|
511
|
+
onExit?.(result);
|
|
512
|
+
});
|
|
513
|
+
|
|
514
|
+
detachActiveSession = () => {
|
|
515
|
+
unsubscribeOutput();
|
|
516
|
+
Reflect.deleteProperty(window, '__vibesTerminalInputReady');
|
|
517
|
+
Reflect.deleteProperty(window, '__vibesReadTerminalBuffer');
|
|
518
|
+
sessionAuthority.detach(s);
|
|
519
|
+
};
|
|
520
|
+
});
|
|
521
|
+
</script>
|
|
522
|
+
|
|
523
|
+
<div
|
|
524
|
+
class={cn('relative flex h-full min-h-0 w-full flex-col overflow-hidden rounded-md', className)}
|
|
525
|
+
style:background={mergedTheme.background}
|
|
526
|
+
>
|
|
527
|
+
<!-- P6 F3 loading skeleton — shown while xterm is booting (connectionState='connecting') -->
|
|
528
|
+
{#if effectiveState === 'connecting'}
|
|
529
|
+
<div
|
|
530
|
+
class="absolute inset-0 z-10 flex items-center justify-start gap-2 px-3.5 py-3 font-mono text-xs text-muted-foreground"
|
|
531
|
+
role="status"
|
|
532
|
+
aria-label="Connecting…"
|
|
533
|
+
>
|
|
534
|
+
<span
|
|
535
|
+
class="size-2 shrink-0 animate-pulse rounded-full bg-muted-foreground"
|
|
536
|
+
aria-hidden="true"
|
|
537
|
+
></span>
|
|
538
|
+
<span class="animate-pulse">connecting…</span>
|
|
539
|
+
</div>
|
|
540
|
+
{/if}
|
|
541
|
+
|
|
542
|
+
<!-- xterm host — always in DOM so xterm can attach; invisible during connecting -->
|
|
543
|
+
<div
|
|
544
|
+
class={cn('min-h-0 w-full flex-1', effectiveState === 'connecting' ? 'invisible' : '')}
|
|
545
|
+
bind:this={host}
|
|
546
|
+
></div>
|
|
547
|
+
|
|
548
|
+
{#if !readonly}
|
|
549
|
+
<TerminalMobileToolbar
|
|
550
|
+
class="sm:hidden"
|
|
551
|
+
bind:ctrlActive={mobileCtrl}
|
|
552
|
+
bind:altActive={mobileAlt}
|
|
553
|
+
disabled={effectiveState !== 'live' || !session}
|
|
554
|
+
onInput={sequence => sessionAuthority.current?.write(encoder.encode(sequence))}
|
|
555
|
+
/>
|
|
556
|
+
{/if}
|
|
557
|
+
|
|
558
|
+
<!-- P6 ReconnectBanner replaces the old inline banner -->
|
|
559
|
+
<ReconnectBanner
|
|
560
|
+
status={bannerStatus}
|
|
561
|
+
reason={bannerReason}
|
|
562
|
+
onNewSession={bannerShowAction ? onNewSession : undefined}
|
|
563
|
+
/>
|
|
564
|
+
{#if rendererFallback}
|
|
565
|
+
<span class="sr-only" aria-live="polite">Optional terminal renderer unavailable.</span>
|
|
566
|
+
{/if}
|
|
567
|
+
</div>
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { type TerminalProps } from './types.ts';
|
|
2
|
+
/**
|
|
3
|
+
* Connection state values driven externally or derived internally.
|
|
4
|
+
* connecting — xterm not yet mounted (boot async in progress)
|
|
5
|
+
* live — session active, no banner
|
|
6
|
+
* reconnecting — transport dropped, retry in progress
|
|
7
|
+
* reconnected — transport restored (auto-dismissed by caller)
|
|
8
|
+
* failed — unrecoverable transport drop
|
|
9
|
+
* exited — shell process exited cleanly
|
|
10
|
+
*/
|
|
11
|
+
export type ConnectionState = 'connecting' | 'live' | 'reconnecting' | 'reconnected' | 'failed' | 'exited';
|
|
12
|
+
interface ExtendedProps extends TerminalProps {
|
|
13
|
+
/**
|
|
14
|
+
* External connection state override. When provided it takes precedence
|
|
15
|
+
* over the component's internally-derived state (live / exited).
|
|
16
|
+
* Defaults to 'connecting' on first render.
|
|
17
|
+
*/
|
|
18
|
+
connectionState?: ConnectionState;
|
|
19
|
+
/** Called when the user clicks "Start new session" in failed/exited state. */
|
|
20
|
+
onNewSession?: () => void;
|
|
21
|
+
}
|
|
22
|
+
declare const Terminal: import("svelte").Component<ExtendedProps, {}, "">;
|
|
23
|
+
type Terminal = ReturnType<typeof Terminal>;
|
|
24
|
+
export default Terminal;
|
|
25
|
+
//# sourceMappingURL=Terminal.svelte.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Terminal.svelte.d.ts","sourceRoot":"","sources":["../../src/svelte/Terminal.svelte.ts"],"names":[],"mappings":"AA4BA,OAAO,EAAwC,KAAK,aAAa,EAAE,MAAM,YAAY,CAAC;AAOrF;;;;;;;;GAQG;AACH,MAAM,MAAM,eAAe,GAC1B,YAAY,GAAG,MAAM,GAAG,cAAc,GAAG,aAAa,GAAG,QAAQ,GAAG,QAAQ,CAAC;AAE9E,UAAU,aAAc,SAAQ,aAAa;IAC5C;;;;OAIG;IACH,eAAe,CAAC,EAAE,eAAe,CAAC;IAClC,8EAA8E;IAC9E,YAAY,CAAC,EAAE,MAAM,IAAI,CAAC;CAC1B;AA6fF,QAAA,MAAM,QAAQ,mDAAwC,CAAC;AACvD,KAAK,QAAQ,GAAG,UAAU,CAAC,OAAO,QAAQ,CAAC,CAAC;AAC5C,eAAe,QAAQ,CAAC"}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { Button } from '@vibesdotdev/kit/components/ui/button';
|
|
3
|
+
import { resolveSequence, type KeyChord } from '../ansi/keymap.js';
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
onInput?: (sequence: string) => void;
|
|
7
|
+
session?: { write: (data: string | Uint8Array) => Promise<void> | void } | null;
|
|
8
|
+
disabled?: boolean;
|
|
9
|
+
ctrlActive?: boolean;
|
|
10
|
+
altActive?: boolean;
|
|
11
|
+
class?: string;
|
|
12
|
+
}
|
|
13
|
+
let { onInput, session = null, disabled = false, class: className = '',
|
|
14
|
+
ctrlActive = $bindable(false), altActive = $bindable(false)
|
|
15
|
+
}: Props = $props();
|
|
16
|
+
const keys = [
|
|
17
|
+
{ key: 'Escape', label: 'Escape', text: 'Esc' },
|
|
18
|
+
{ key: 'Tab', label: 'Tab', text: 'Tab' },
|
|
19
|
+
{ key: 'c', ctrl: true, label: 'Interrupt command', text: '^C' },
|
|
20
|
+
{ key: 'd', ctrl: true, label: 'End input', text: '^D' },
|
|
21
|
+
{ key: '|', label: 'Pipe', text: '|' },
|
|
22
|
+
{ key: '\\', label: 'Backslash', text: '\\' },
|
|
23
|
+
{ key: '~', label: 'Tilde', text: '~' },
|
|
24
|
+
{ key: 'ArrowUp', label: 'Up Arrow', text: '↑' },
|
|
25
|
+
{ key: 'ArrowDown', label: 'Down Arrow', text: '↓' },
|
|
26
|
+
{ key: 'ArrowLeft', label: 'Left Arrow', text: '←' },
|
|
27
|
+
{ key: 'ArrowRight', label: 'Right Arrow', text: '→' },
|
|
28
|
+
{ key: 'PageUp', label: 'Page Up', text: 'PgUp' },
|
|
29
|
+
{ key: 'PageDown', label: 'Page Down', text: 'PgDn' }
|
|
30
|
+
];
|
|
31
|
+
function emit(chord: KeyChord) {
|
|
32
|
+
if (disabled) return;
|
|
33
|
+
const sequence = resolveSequence({ ...chord, ctrl: chord.ctrl ?? ctrlActive, alt: chord.alt ?? altActive });
|
|
34
|
+
ctrlActive = false;
|
|
35
|
+
altActive = false;
|
|
36
|
+
if (onInput) onInput(sequence);
|
|
37
|
+
else if (session) void session.write(sequence);
|
|
38
|
+
}
|
|
39
|
+
</script>
|
|
40
|
+
|
|
41
|
+
<div
|
|
42
|
+
class="flex shrink-0 items-center gap-1 overflow-x-auto border-t bg-background p-1.5 font-mono text-xs text-foreground touch-manipulation {className}"
|
|
43
|
+
role="toolbar"
|
|
44
|
+
aria-label="Terminal touch accessory keyboard"
|
|
45
|
+
>
|
|
46
|
+
<Button type="button" variant={ctrlActive ? 'secondary' : 'ghost'} size="sm"
|
|
47
|
+
class="min-h-11 shrink-0" {disabled} aria-pressed={ctrlActive} aria-label="Control key"
|
|
48
|
+
onpointerdown={event => event.preventDefault()} onclick={() => ctrlActive = !ctrlActive}>Ctrl</Button>
|
|
49
|
+
<Button type="button" variant={altActive ? 'secondary' : 'ghost'} size="sm"
|
|
50
|
+
class="min-h-11 shrink-0" {disabled} aria-pressed={altActive} aria-label="Alt key"
|
|
51
|
+
onpointerdown={event => event.preventDefault()} onclick={() => altActive = !altActive}>Alt</Button>
|
|
52
|
+
{#each keys as key (key.label)}
|
|
53
|
+
<Button type="button" variant="ghost" size="sm" class="min-h-11 shrink-0" {disabled}
|
|
54
|
+
aria-label={key.label} onpointerdown={event => event.preventDefault()} onclick={() => emit(key)}>{key.text}</Button>
|
|
55
|
+
{/each}
|
|
56
|
+
</div>
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
interface Props {
|
|
2
|
+
onInput?: (sequence: string) => void;
|
|
3
|
+
session?: {
|
|
4
|
+
write: (data: string | Uint8Array) => Promise<void> | void;
|
|
5
|
+
} | null;
|
|
6
|
+
disabled?: boolean;
|
|
7
|
+
ctrlActive?: boolean;
|
|
8
|
+
altActive?: boolean;
|
|
9
|
+
class?: string;
|
|
10
|
+
}
|
|
11
|
+
declare const TerminalMobileToolbar: import("svelte").Component<Props, {}, "ctrlActive" | "altActive">;
|
|
12
|
+
type TerminalMobileToolbar = ReturnType<typeof TerminalMobileToolbar>;
|
|
13
|
+
export default TerminalMobileToolbar;
|
|
14
|
+
//# sourceMappingURL=TerminalMobileToolbar.svelte.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"TerminalMobileToolbar.svelte.d.ts","sourceRoot":"","sources":["../../src/svelte/TerminalMobileToolbar.svelte.ts"],"names":[],"mappings":"AAOC,UAAU,KAAK;IACd,OAAO,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAC;IACrC,OAAO,CAAC,EAAE;QAAE,KAAK,EAAE,CAAC,IAAI,EAAE,MAAM,GAAG,UAAU,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAA;KAAE,GAAG,IAAI,CAAC;IAChF,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,KAAK,CAAC,EAAE,MAAM,CAAC;CACf;AA2CF,QAAA,MAAM,qBAAqB,mEAAwC,CAAC;AACpE,KAAK,qBAAqB,GAAG,UAAU,CAAC,OAAO,qBAAqB,CAAC,CAAC;AACtE,eAAe,qBAAqB,CAAC"}
|