dsh-single-terminal 0.1.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 +144 -0
- package/README.zh.md +129 -0
- package/cordis.patch.yml +11 -0
- package/lib/client.js +11438 -0
- package/lib/client.js.map +1 -0
- package/lib/index.js +588 -0
- package/lib/types/client/controller.d.ts +86 -0
- package/lib/types/client/controller.d.ts.map +1 -0
- package/lib/types/client/controller.js +235 -0
- package/lib/types/client/drawer.d.ts +10 -0
- package/lib/types/client/drawer.d.ts.map +1 -0
- package/lib/types/client/drawer.js +84 -0
- package/lib/types/client/i18n.d.ts +8 -0
- package/lib/types/client/i18n.d.ts.map +1 -0
- package/lib/types/client/i18n.js +51 -0
- package/lib/types/client/index.d.ts +13 -0
- package/lib/types/client/index.d.ts.map +1 -0
- package/lib/types/client/index.js +13 -0
- package/lib/types/client/plugin.d.ts +23 -0
- package/lib/types/client/plugin.d.ts.map +1 -0
- package/lib/types/client/plugin.js +60 -0
- package/lib/types/client/protocol.d.ts +81 -0
- package/lib/types/client/protocol.d.ts.map +1 -0
- package/lib/types/client/protocol.js +5 -0
- package/lib/types/client/storage.d.ts +6 -0
- package/lib/types/client/storage.d.ts.map +1 -0
- package/lib/types/client/storage.js +21 -0
- package/lib/types/client/styles.d.ts +5 -0
- package/lib/types/client/styles.d.ts.map +1 -0
- package/lib/types/client/styles.js +103 -0
- package/lib/types/client/term.d.ts +15 -0
- package/lib/types/client/term.d.ts.map +1 -0
- package/lib/types/client/term.js +86 -0
- package/lib/types/client/toggle.d.ts +9 -0
- package/lib/types/client/toggle.d.ts.map +1 -0
- package/lib/types/client/toggle.js +19 -0
- package/lib/types/client/ws.d.ts +25 -0
- package/lib/types/client/ws.d.ts.map +1 -0
- package/lib/types/client/ws.js +79 -0
- package/lib/types/client/xterm-css.d.ts +7 -0
- package/lib/types/client/xterm-css.d.ts.map +1 -0
- package/lib/types/client/xterm-css.js +224 -0
- package/lib/types/host/hub.d.ts +39 -0
- package/lib/types/host/hub.d.ts.map +1 -0
- package/lib/types/host/hub.js +291 -0
- package/lib/types/host/index.d.ts +17 -0
- package/lib/types/host/index.d.ts.map +1 -0
- package/lib/types/host/index.js +66 -0
- package/lib/types/host/shells.d.ts +30 -0
- package/lib/types/host/shells.d.ts.map +1 -0
- package/lib/types/host/shells.js +202 -0
- package/lib/types/host/types.d.ts +109 -0
- package/lib/types/host/types.d.ts.map +1 -0
- package/lib/types/host/types.js +4 -0
- package/package.json +102 -0
- package/src/client/controller.ts +293 -0
- package/src/client/drawer.tsx +182 -0
- package/src/client/i18n.ts +58 -0
- package/src/client/index.ts +17 -0
- package/src/client/plugin.tsx +92 -0
- package/src/client/protocol.ts +40 -0
- package/src/client/storage.ts +21 -0
- package/src/client/styles.ts +106 -0
- package/src/client/term.tsx +95 -0
- package/src/client/toggle.tsx +43 -0
- package/src/client/ws.ts +81 -0
- package/src/client/xterm-css.ts +224 -0
- package/src/host/cordis-augment.d.ts +22 -0
- package/src/host/hub.ts +302 -0
- package/src/host/index.ts +78 -0
- package/src/host/shells.ts +216 -0
- package/src/host/types.ts +79 -0
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-single-terminal —— 客户端控制器(模块级单例):
|
|
3
|
+
* WebSocket 生命周期、帧路由、sink 注册(xterm 实例)、UI 状态 store。
|
|
4
|
+
*
|
|
5
|
+
* 会话输出在 sink 注册前先积压到 pending(React 挂载/重连时序下不丢字节);
|
|
6
|
+
* 抽屉关闭不卸载组件(display:none),xterm 缓冲与会话保持。
|
|
7
|
+
*/
|
|
8
|
+
import { useSyncExternalStore } from 'react';
|
|
9
|
+
import { loadJson, saveJson } from "./storage.js";
|
|
10
|
+
import { TerminalSocket } from "./ws.js";
|
|
11
|
+
class DrawerStore {
|
|
12
|
+
state = {
|
|
13
|
+
open: false,
|
|
14
|
+
mode: loadJson('mode', 'overlay'),
|
|
15
|
+
height: loadJson('height', 360),
|
|
16
|
+
conn: 'connecting',
|
|
17
|
+
tabs: [],
|
|
18
|
+
activeId: null,
|
|
19
|
+
shells: [],
|
|
20
|
+
defaultShell: 'powershell',
|
|
21
|
+
fontSize: 13,
|
|
22
|
+
fontFamily: 'Consolas, "Cascadia Mono", "Courier New", monospace',
|
|
23
|
+
notice: null,
|
|
24
|
+
};
|
|
25
|
+
listeners = new Set();
|
|
26
|
+
subscribe = (listener) => {
|
|
27
|
+
this.listeners.add(listener);
|
|
28
|
+
return () => { this.listeners.delete(listener); };
|
|
29
|
+
};
|
|
30
|
+
getSnapshot = () => this.state;
|
|
31
|
+
set(patch) {
|
|
32
|
+
this.state = { ...this.state, ...patch };
|
|
33
|
+
for (const listener of this.listeners)
|
|
34
|
+
listener();
|
|
35
|
+
}
|
|
36
|
+
setOpen(open) {
|
|
37
|
+
this.set({ open });
|
|
38
|
+
}
|
|
39
|
+
setMode(mode) {
|
|
40
|
+
saveJson('mode', mode);
|
|
41
|
+
this.set({ mode });
|
|
42
|
+
}
|
|
43
|
+
setHeight(height) {
|
|
44
|
+
saveJson('height', height);
|
|
45
|
+
this.set({ height });
|
|
46
|
+
}
|
|
47
|
+
setConn(conn) {
|
|
48
|
+
this.set({ conn });
|
|
49
|
+
}
|
|
50
|
+
setNotice(notice) {
|
|
51
|
+
this.set({ notice });
|
|
52
|
+
}
|
|
53
|
+
applyHello(frame) {
|
|
54
|
+
this.set({ defaultShell: frame.defaultShell, fontSize: frame.fontSize, fontFamily: frame.fontFamily });
|
|
55
|
+
}
|
|
56
|
+
applyInventory(shells, sessions) {
|
|
57
|
+
const known = new Set(this.state.tabs.map((tab) => tab.id));
|
|
58
|
+
const adopted = sessions.filter((session) => !known.has(session.id));
|
|
59
|
+
if (adopted.length > 0) {
|
|
60
|
+
const tabs = [...this.state.tabs, ...adopted.map(toTab)];
|
|
61
|
+
const activeId = this.state.activeId ?? tabs[tabs.length - 1]?.id ?? null;
|
|
62
|
+
this.set({ shells, tabs, activeId });
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
this.set({ shells });
|
|
66
|
+
}
|
|
67
|
+
return { adopted };
|
|
68
|
+
}
|
|
69
|
+
addSession(session) {
|
|
70
|
+
if (this.state.tabs.some((tab) => tab.id === session.id))
|
|
71
|
+
return;
|
|
72
|
+
this.set({ tabs: [...this.state.tabs, toTab(session)], activeId: session.id });
|
|
73
|
+
}
|
|
74
|
+
setActive(id) {
|
|
75
|
+
this.set({ activeId: id });
|
|
76
|
+
}
|
|
77
|
+
markDead(id, exitCode) {
|
|
78
|
+
this.set({
|
|
79
|
+
tabs: this.state.tabs.map((tab) => (tab.id === id ? { ...tab, alive: false, exitCode } : tab)),
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
removeTab(id) {
|
|
83
|
+
const tabs = this.state.tabs.filter((tab) => tab.id !== id);
|
|
84
|
+
const activeId = this.state.activeId === id ? tabs[tabs.length - 1]?.id ?? null : this.state.activeId;
|
|
85
|
+
this.set({ tabs, activeId });
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
function toTab(session) {
|
|
89
|
+
return { id: session.id, shellId: session.shellId, label: session.label, cwd: session.cwd, alive: session.alive, exitCode: session.exitCode };
|
|
90
|
+
}
|
|
91
|
+
class TerminalController {
|
|
92
|
+
store = new DrawerStore();
|
|
93
|
+
socket = null;
|
|
94
|
+
sinks = new Map();
|
|
95
|
+
pending = new Map();
|
|
96
|
+
noticeTimer = null;
|
|
97
|
+
toggle() {
|
|
98
|
+
this.setOpen(!this.store.getSnapshot().open);
|
|
99
|
+
}
|
|
100
|
+
setOpen(open) {
|
|
101
|
+
this.store.setOpen(open);
|
|
102
|
+
if (open) {
|
|
103
|
+
this.ensureStarted();
|
|
104
|
+
this.maybeAutoOpen();
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
this.autoOpening = false;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
ensureStarted() {
|
|
111
|
+
if (this.socket !== null)
|
|
112
|
+
return;
|
|
113
|
+
this.socket = new TerminalSocket({
|
|
114
|
+
onFrame: (frame) => { this.onFrame(frame); },
|
|
115
|
+
onState: (state) => {
|
|
116
|
+
this.store.setConn(state);
|
|
117
|
+
// 每次连上(含重连)都拉取会话清单:adopt 存活会话 + attach 回放,
|
|
118
|
+
// 完成页面刷新后的标签恢复。
|
|
119
|
+
if (state === 'connected')
|
|
120
|
+
this.socket?.send({ type: 'list' });
|
|
121
|
+
},
|
|
122
|
+
});
|
|
123
|
+
this.socket.connect();
|
|
124
|
+
}
|
|
125
|
+
// 打开抽屉且没有任何终端时自动新建默认 shell;清单未就绪时由 shells 帧兜底。
|
|
126
|
+
autoOpening = false;
|
|
127
|
+
maybeAutoOpen() {
|
|
128
|
+
const state = this.store.getSnapshot();
|
|
129
|
+
if (!state.open || state.conn !== 'connected' || state.shells.length === 0)
|
|
130
|
+
return;
|
|
131
|
+
if (state.tabs.length > 0 || this.autoOpening)
|
|
132
|
+
return;
|
|
133
|
+
this.autoOpening = true;
|
|
134
|
+
this.newTerminal(state.defaultShell);
|
|
135
|
+
}
|
|
136
|
+
newTerminal(shellId) {
|
|
137
|
+
this.ensureStarted();
|
|
138
|
+
this.socket?.send({ type: 'open', shellId, cols: 80, rows: 24 });
|
|
139
|
+
}
|
|
140
|
+
closeTab(id) {
|
|
141
|
+
this.sinks.delete(id);
|
|
142
|
+
this.pending.delete(id);
|
|
143
|
+
this.store.removeTab(id);
|
|
144
|
+
this.socket?.send({ type: 'close', id });
|
|
145
|
+
}
|
|
146
|
+
setActive(id) {
|
|
147
|
+
this.store.setActive(id);
|
|
148
|
+
}
|
|
149
|
+
setMode(mode) {
|
|
150
|
+
this.store.setMode(mode);
|
|
151
|
+
}
|
|
152
|
+
setHeight(height) {
|
|
153
|
+
this.store.setHeight(height);
|
|
154
|
+
}
|
|
155
|
+
showNotice(message) {
|
|
156
|
+
this.store.setNotice(message);
|
|
157
|
+
if (this.noticeTimer !== null)
|
|
158
|
+
clearTimeout(this.noticeTimer);
|
|
159
|
+
this.noticeTimer = setTimeout(() => {
|
|
160
|
+
this.noticeTimer = null;
|
|
161
|
+
this.store.setNotice(null);
|
|
162
|
+
}, 6000);
|
|
163
|
+
}
|
|
164
|
+
sendInput(id, data) {
|
|
165
|
+
this.socket?.send({ type: 'input', id, data });
|
|
166
|
+
}
|
|
167
|
+
sendResize(id, cols, rows) {
|
|
168
|
+
this.socket?.send({ type: 'resize', id, cols, rows });
|
|
169
|
+
}
|
|
170
|
+
registerSink(id, sink) {
|
|
171
|
+
this.sinks.set(id, sink);
|
|
172
|
+
const buffered = this.pending.get(id);
|
|
173
|
+
if (buffered !== undefined) {
|
|
174
|
+
this.pending.delete(id);
|
|
175
|
+
sink.write(buffered);
|
|
176
|
+
}
|
|
177
|
+
return () => {
|
|
178
|
+
if (this.sinks.get(id) === sink)
|
|
179
|
+
this.sinks.delete(id);
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
dispose() {
|
|
183
|
+
this.socket?.dispose();
|
|
184
|
+
this.socket = null;
|
|
185
|
+
}
|
|
186
|
+
writeData(id, data) {
|
|
187
|
+
const sink = this.sinks.get(id);
|
|
188
|
+
if (sink !== undefined) {
|
|
189
|
+
sink.write(data);
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
this.pending.set(id, (this.pending.get(id) ?? '') + data);
|
|
193
|
+
}
|
|
194
|
+
onFrame(frame) {
|
|
195
|
+
switch (frame.type) {
|
|
196
|
+
case 'hello':
|
|
197
|
+
this.store.applyHello(frame);
|
|
198
|
+
break;
|
|
199
|
+
case 'shells': {
|
|
200
|
+
const { adopted } = this.store.applyInventory(frame.shells, frame.sessions);
|
|
201
|
+
for (const session of adopted) {
|
|
202
|
+
this.socket?.send({ type: 'attach', id: session.id });
|
|
203
|
+
}
|
|
204
|
+
this.maybeAutoOpen();
|
|
205
|
+
break;
|
|
206
|
+
}
|
|
207
|
+
case 'opened':
|
|
208
|
+
this.autoOpening = false;
|
|
209
|
+
this.store.addSession(frame.session);
|
|
210
|
+
break;
|
|
211
|
+
case 'data':
|
|
212
|
+
case 'replay':
|
|
213
|
+
this.writeData(frame.id, frame.data);
|
|
214
|
+
break;
|
|
215
|
+
case 'exit':
|
|
216
|
+
if (frame.closed === true) {
|
|
217
|
+
this.sinks.delete(frame.id);
|
|
218
|
+
this.pending.delete(frame.id);
|
|
219
|
+
this.store.removeTab(frame.id);
|
|
220
|
+
}
|
|
221
|
+
else {
|
|
222
|
+
this.store.markDead(frame.id, frame.exitCode);
|
|
223
|
+
}
|
|
224
|
+
break;
|
|
225
|
+
case 'error':
|
|
226
|
+
this.autoOpening = false;
|
|
227
|
+
this.showNotice(frame.message);
|
|
228
|
+
break;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
export const terminal = new TerminalController();
|
|
233
|
+
export function useDrawerState() {
|
|
234
|
+
return useSyncExternalStore(terminal.store.subscribe, terminal.store.getSnapshot);
|
|
235
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-single-terminal —— 底部抽屉(shell.overlay 插槽)。
|
|
3
|
+
*
|
|
4
|
+
* - 两种模式:docked(占高度:对 AppFrame 根元素设 inline padding-bottom,把
|
|
5
|
+
* 主内容顶起;找不到 [data-shell-overlay] 锚点时静默降级为浮层)/ overlay(浮层);
|
|
6
|
+
* - 顶部拖拽条调高度(pointer capture + 帧节流),localStorage 持久化 mode/height;
|
|
7
|
+
* - 组件常挂载(关闭时 display:none),保证 xterm 缓冲与会话不因抽屉开合丢失。
|
|
8
|
+
*/
|
|
9
|
+
export declare function TerminalDrawer(): import("react").JSX.Element;
|
|
10
|
+
//# sourceMappingURL=drawer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"drawer.d.ts","sourceRoot":"","sources":["../../../src/client/drawer.tsx"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAeH,wBAAgB,cAAc,gCA+J7B"}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* dsh-single-terminal —— 底部抽屉(shell.overlay 插槽)。
|
|
4
|
+
*
|
|
5
|
+
* - 两种模式:docked(占高度:对 AppFrame 根元素设 inline padding-bottom,把
|
|
6
|
+
* 主内容顶起;找不到 [data-shell-overlay] 锚点时静默降级为浮层)/ overlay(浮层);
|
|
7
|
+
* - 顶部拖拽条调高度(pointer capture + 帧节流),localStorage 持久化 mode/height;
|
|
8
|
+
* - 组件常挂载(关闭时 display:none),保证 xterm 缓冲与会话不因抽屉开合丢失。
|
|
9
|
+
*/
|
|
10
|
+
import { useEffect, useRef, useState } from 'react';
|
|
11
|
+
import { terminal, useDrawerState } from "./controller.js";
|
|
12
|
+
import { t } from "./i18n.js";
|
|
13
|
+
import { TerminalView } from "./term.js";
|
|
14
|
+
const HEIGHT_MIN = 140;
|
|
15
|
+
function findFrameElement() {
|
|
16
|
+
const overlay = document.querySelector('[data-shell-overlay]');
|
|
17
|
+
const frame = overlay?.parentElement;
|
|
18
|
+
return frame instanceof HTMLElement ? frame : null;
|
|
19
|
+
}
|
|
20
|
+
export function TerminalDrawer() {
|
|
21
|
+
const state = useDrawerState();
|
|
22
|
+
const [menuOpen, setMenuOpen] = useState(false);
|
|
23
|
+
const menuRef = useRef(null);
|
|
24
|
+
// 首次挂载即建立 WS 连接(空闲连接开销极小;页面刷新后由 list 帧恢复标签)。
|
|
25
|
+
useEffect(() => { terminal.ensureStarted(); }, []);
|
|
26
|
+
// 占高度模式:把 AppFrame 根元素顶起(border-box + padding-bottom)。
|
|
27
|
+
useEffect(() => {
|
|
28
|
+
if (!state.open || state.mode !== 'docked')
|
|
29
|
+
return;
|
|
30
|
+
const frame = findFrameElement();
|
|
31
|
+
if (frame === null)
|
|
32
|
+
return;
|
|
33
|
+
frame.style.boxSizing = 'border-box';
|
|
34
|
+
frame.style.paddingBottom = `${state.height}px`;
|
|
35
|
+
return () => {
|
|
36
|
+
frame.style.paddingBottom = '';
|
|
37
|
+
frame.style.boxSizing = '';
|
|
38
|
+
};
|
|
39
|
+
}, [state.open, state.mode, state.height]);
|
|
40
|
+
// 菜单外点关闭。
|
|
41
|
+
useEffect(() => {
|
|
42
|
+
if (!menuOpen)
|
|
43
|
+
return;
|
|
44
|
+
const onPointerDown = (event) => {
|
|
45
|
+
if (menuRef.current !== null && !menuRef.current.contains(event.target)) {
|
|
46
|
+
setMenuOpen(false);
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
document.addEventListener('pointerdown', onPointerDown);
|
|
50
|
+
return () => { document.removeEventListener('pointerdown', onPointerDown); };
|
|
51
|
+
}, [menuOpen]);
|
|
52
|
+
const availableShells = state.shells.filter((shell) => shell.available);
|
|
53
|
+
const startDrag = (event) => {
|
|
54
|
+
event.preventDefault();
|
|
55
|
+
const handle = event.currentTarget;
|
|
56
|
+
const startY = event.clientY;
|
|
57
|
+
const startHeight = state.height;
|
|
58
|
+
try {
|
|
59
|
+
handle.setPointerCapture(event.pointerId);
|
|
60
|
+
}
|
|
61
|
+
catch { /* 指针已失效(如合成事件):仅失去拖出把手的跟随能力 */ }
|
|
62
|
+
let lastAt = 0;
|
|
63
|
+
const onMove = (move) => {
|
|
64
|
+
if (move.timeStamp - lastAt < 16)
|
|
65
|
+
return;
|
|
66
|
+
lastAt = move.timeStamp;
|
|
67
|
+
const next = Math.min(window.innerHeight - 100, Math.max(HEIGHT_MIN, startHeight + (startY - move.clientY)));
|
|
68
|
+
terminal.setHeight(next);
|
|
69
|
+
};
|
|
70
|
+
const onUp = () => {
|
|
71
|
+
handle.removeEventListener('pointermove', onMove);
|
|
72
|
+
};
|
|
73
|
+
handle.addEventListener('pointermove', onMove);
|
|
74
|
+
handle.addEventListener('pointerup', onUp, { once: true });
|
|
75
|
+
handle.addEventListener('pointercancel', onUp, { once: true });
|
|
76
|
+
};
|
|
77
|
+
return (_jsxs("div", { className: "dst-drawer", style: { display: state.open ? 'flex' : 'none', '--dst-height': `${state.height}px` }, children: [_jsx("div", { className: "dst-drag", onPointerDown: startDrag, title: t('terminal.mode.toggle') }), _jsxs("div", { className: "dst-head", children: [_jsx("div", { className: "dst-tabs", children: state.tabs.map((tab) => (_jsxs("button", { className: `dst-tab${tab.id === state.activeId ? ' active' : ''}`, onClick: () => { terminal.setActive(tab.id); }, title: tab.cwd, children: [_jsx("span", { className: `dst-dot${tab.alive ? '' : ' dead'}` }), _jsx("span", { children: tab.label }), !tab.alive && _jsx("span", { children: t('terminal.exited') }), _jsx("span", { className: "dst-tabclose", role: "button", title: t('terminal.closeTab'), onClick: (event) => {
|
|
78
|
+
event.stopPropagation();
|
|
79
|
+
terminal.closeTab(tab.id);
|
|
80
|
+
}, children: "\u2715" })] }, tab.id))) }), _jsx("span", { className: `dst-conn ${state.conn}`, title: t(`terminal.status.${state.conn}`) }), _jsxs("div", { className: "dst-actions", children: [_jsx("button", { className: "dst-modebtn", onClick: () => { terminal.setMode(state.mode === 'docked' ? 'overlay' : 'docked'); }, title: t('terminal.mode.toggle'), children: state.mode === 'docked' ? `⬓ ${t('terminal.mode.dock')}` : `◫ ${t('terminal.mode.overlay')}` }), _jsxs("div", { style: { position: 'relative' }, ref: menuRef, children: [_jsx("button", { className: "dst-iconbtn", onClick: () => { terminal.newTerminal(state.defaultShell); }, title: `${t('terminal.new')}(${availableShells.find((shell) => shell.id === state.defaultShell)?.name ?? state.defaultShell})`, children: "\uFF0B" }), _jsx("button", { className: "dst-iconbtn", style: { width: 18 }, onClick: () => { setMenuOpen((open) => !open); }, title: t('terminal.new'), children: "\u25B8" }), menuOpen && (_jsxs("div", { className: "dst-menu", children: [availableShells.length === 0 && (_jsx("div", { className: "dst-menuitem", style: { cursor: 'default', opacity: 0.6 }, children: "\u2014" })), availableShells.map((shell) => (_jsx("button", { className: "dst-menuitem", onClick: () => {
|
|
81
|
+
setMenuOpen(false);
|
|
82
|
+
terminal.newTerminal(shell.id);
|
|
83
|
+
}, children: shell.name }, shell.id)))] }))] }), _jsx("button", { className: "dst-iconbtn", onClick: () => { terminal.setOpen(false); }, title: t('terminal.closeDrawer'), children: "\u25BE" })] })] }), _jsxs("div", { className: "dst-body", children: [state.tabs.map((tab) => (_jsx(TerminalView, { tab: tab, active: tab.id === state.activeId, fontSize: state.fontSize, fontFamily: state.fontFamily }, tab.id))), state.tabs.length === 0 && _jsx("div", { className: "dst-empty", children: t('terminal.empty') }), state.notice !== null && _jsx("div", { className: "dst-notice", children: state.notice })] })] }));
|
|
84
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-single-terminal —— 双语字典(zh/en),语言由宿主 locale 服务驱动。
|
|
3
|
+
*/
|
|
4
|
+
export type Lang = 'zh' | 'en';
|
|
5
|
+
export declare function setLang(next: Lang): void;
|
|
6
|
+
export declare function getLang(): Lang;
|
|
7
|
+
export declare function t(key: string): string;
|
|
8
|
+
//# sourceMappingURL=i18n.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"i18n.d.ts","sourceRoot":"","sources":["../../../src/client/i18n.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,MAAM,MAAM,IAAI,GAAG,IAAI,GAAG,IAAI,CAAA;AAI9B,wBAAgB,OAAO,CAAC,IAAI,EAAE,IAAI,GAAG,IAAI,CAExC;AAED,wBAAgB,OAAO,IAAI,IAAI,CAE9B;AAyCD,wBAAgB,CAAC,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAErC"}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-single-terminal —— 双语字典(zh/en),语言由宿主 locale 服务驱动。
|
|
3
|
+
*/
|
|
4
|
+
let lang = 'en';
|
|
5
|
+
export function setLang(next) {
|
|
6
|
+
lang = next;
|
|
7
|
+
}
|
|
8
|
+
export function getLang() {
|
|
9
|
+
return lang;
|
|
10
|
+
}
|
|
11
|
+
const DICT = {
|
|
12
|
+
zh: {
|
|
13
|
+
'terminal.title': '终端',
|
|
14
|
+
'terminal.toggleTip': '打开终端面板 Alt+C',
|
|
15
|
+
'terminal.new': '新建终端',
|
|
16
|
+
'terminal.closeTab': '关闭终端',
|
|
17
|
+
'terminal.closeDrawer': '收起抽屉',
|
|
18
|
+
'terminal.mode.dock': '占高度',
|
|
19
|
+
'terminal.mode.overlay': '浮层',
|
|
20
|
+
'terminal.mode.toggle': '切换抽屉模式',
|
|
21
|
+
'terminal.status.connected': '已连接',
|
|
22
|
+
'terminal.status.connecting': '连接中…',
|
|
23
|
+
'terminal.status.reconnecting': '重连中…',
|
|
24
|
+
'terminal.status.disconnected': '已断开',
|
|
25
|
+
'terminal.exited': '已退出',
|
|
26
|
+
'terminal.empty': '暂无终端会话,点击右上角 + 新建',
|
|
27
|
+
'terminal.error.limit': '终端数量已达上限',
|
|
28
|
+
'terminal.error.spawn': '终端启动失败',
|
|
29
|
+
},
|
|
30
|
+
en: {
|
|
31
|
+
'terminal.title': 'Terminal',
|
|
32
|
+
'terminal.toggleTip': 'Open terminal panel Alt+C',
|
|
33
|
+
'terminal.new': 'New terminal',
|
|
34
|
+
'terminal.closeTab': 'Close terminal',
|
|
35
|
+
'terminal.closeDrawer': 'Collapse drawer',
|
|
36
|
+
'terminal.mode.dock': 'Docked',
|
|
37
|
+
'terminal.mode.overlay': 'Overlay',
|
|
38
|
+
'terminal.mode.toggle': 'Toggle drawer mode',
|
|
39
|
+
'terminal.status.connected': 'Connected',
|
|
40
|
+
'terminal.status.connecting': 'Connecting…',
|
|
41
|
+
'terminal.status.reconnecting': 'Reconnecting…',
|
|
42
|
+
'terminal.status.disconnected': 'Disconnected',
|
|
43
|
+
'terminal.exited': 'Exited',
|
|
44
|
+
'terminal.empty': 'No terminal sessions yet — click + to create one',
|
|
45
|
+
'terminal.error.limit': 'Terminal session limit reached',
|
|
46
|
+
'terminal.error.spawn': 'Failed to spawn terminal',
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
export function t(key) {
|
|
50
|
+
return DICT[lang][key] ?? DICT.en[key] ?? key;
|
|
51
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-single-terminal —— 浏览器半边入口(tsdown 打包为 __ModuleLoader__ 工厂)。
|
|
3
|
+
*
|
|
4
|
+
* 本文件为纯 ESM 模块,直接导出插件形状 { name, inject, apply };
|
|
5
|
+
* window.__ModuleLoader__.load 工厂包装由 tsdown 的 banner/footer 在构建时生成
|
|
6
|
+
* (见 tsdown.config.ts)。外部依赖(react / react/jsx-runtime)构建时保持
|
|
7
|
+
* external,运行时经 factory 的 require 解析宿主模块表(seed)。
|
|
8
|
+
*/
|
|
9
|
+
export declare const name: string;
|
|
10
|
+
export declare const inject: string[];
|
|
11
|
+
export declare const apply: (ctx: import("./plugin.tsx").ClientCtx) => void;
|
|
12
|
+
export type { ClientCtx, ClientPluginModule } from './plugin.tsx';
|
|
13
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/client/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAMH,eAAO,MAAM,IAAI,QAAc,CAAA;AAC/B,eAAO,MAAM,MAAM,UAAgB,CAAA;AACnC,eAAO,MAAM,KAAK,iDAAe,CAAA;AACjC,YAAY,EAAE,SAAS,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAA"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-single-terminal —— 浏览器半边入口(tsdown 打包为 __ModuleLoader__ 工厂)。
|
|
3
|
+
*
|
|
4
|
+
* 本文件为纯 ESM 模块,直接导出插件形状 { name, inject, apply };
|
|
5
|
+
* window.__ModuleLoader__.load 工厂包装由 tsdown 的 banner/footer 在构建时生成
|
|
6
|
+
* (见 tsdown.config.ts)。外部依赖(react / react/jsx-runtime)构建时保持
|
|
7
|
+
* external,运行时经 factory 的 require 解析宿主模块表(seed)。
|
|
8
|
+
*/
|
|
9
|
+
import { createPlugin } from "./plugin.js";
|
|
10
|
+
const plugin = createPlugin();
|
|
11
|
+
export const name = plugin.name;
|
|
12
|
+
export const inject = plugin.inject;
|
|
13
|
+
export const apply = plugin.apply;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-single-terminal —— 浏览器半边插件主体(slots 注册)。
|
|
3
|
+
*
|
|
4
|
+
* - shell.overlay:底部终端抽屉;
|
|
5
|
+
* - conversation.session.header.utilities:会话头部开关按钮(宿主 Tooltip 气泡);
|
|
6
|
+
* - Alt+C 快捷键开关抽屉(ctx.effect 挂窗级监听,卸载自动清理;焦点在终端
|
|
7
|
+
* 输入区时放行,让 shell 收到 ESC c 而不误触);
|
|
8
|
+
* - 语言跟随宿主 locale 服务(软依赖,缺失时回退 en)。
|
|
9
|
+
*/
|
|
10
|
+
/** 浏览器侧插件上下文(宿主注入)。 */
|
|
11
|
+
export interface ClientCtx {
|
|
12
|
+
get<T = unknown>(name: string): T | undefined;
|
|
13
|
+
/** 注册随插件卸载自动清理的副作用(回调返回 disposer)。 */
|
|
14
|
+
effect(fn: () => (() => void) | void): unknown;
|
|
15
|
+
on?(event: string, listener: (payload: unknown) => void): unknown;
|
|
16
|
+
}
|
|
17
|
+
export interface ClientPluginModule {
|
|
18
|
+
name: string;
|
|
19
|
+
inject: string[];
|
|
20
|
+
apply(ctx: ClientCtx): void;
|
|
21
|
+
}
|
|
22
|
+
export declare function createPlugin(): ClientPluginModule;
|
|
23
|
+
//# sourceMappingURL=plugin.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../../../src/client/plugin.tsx"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAmBH,uBAAuB;AACvB,MAAM,WAAW,SAAS;IACxB,GAAG,CAAC,CAAC,GAAG,OAAO,EAAE,IAAI,EAAE,MAAM,GAAG,CAAC,GAAG,SAAS,CAAA;IAC7C,sCAAsC;IACtC,MAAM,CAAC,EAAE,EAAE,MAAM,CAAC,MAAM,IAAI,CAAC,GAAG,IAAI,GAAG,OAAO,CAAA;IAC9C,EAAE,CAAC,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,IAAI,GAAG,OAAO,CAAA;CAClE;AAED,MAAM,WAAW,kBAAkB;IACjC,IAAI,EAAE,MAAM,CAAA;IACZ,MAAM,EAAE,MAAM,EAAE,CAAA;IAChB,KAAK,CAAC,GAAG,EAAE,SAAS,GAAG,IAAI,CAAA;CAC5B;AAED,wBAAgB,YAAY,IAAI,kBAAkB,CAkDjD"}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-single-terminal —— 浏览器半边插件主体(slots 注册)。
|
|
3
|
+
*
|
|
4
|
+
* - shell.overlay:底部终端抽屉;
|
|
5
|
+
* - conversation.session.header.utilities:会话头部开关按钮(宿主 Tooltip 气泡);
|
|
6
|
+
* - Alt+C 快捷键开关抽屉(ctx.effect 挂窗级监听,卸载自动清理;焦点在终端
|
|
7
|
+
* 输入区时放行,让 shell 收到 ESC c 而不误触);
|
|
8
|
+
* - 语言跟随宿主 locale 服务(软依赖,缺失时回退 en)。
|
|
9
|
+
*/
|
|
10
|
+
import { terminal } from "./controller.js";
|
|
11
|
+
import { TerminalDrawer } from "./drawer.js";
|
|
12
|
+
import { setLang } from "./i18n.js";
|
|
13
|
+
import { injectStyles } from "./styles.js";
|
|
14
|
+
import { TerminalToggle } from "./toggle.js";
|
|
15
|
+
export function createPlugin() {
|
|
16
|
+
return {
|
|
17
|
+
name: 'dsh-single-terminal',
|
|
18
|
+
inject: ['slots', 'locale'],
|
|
19
|
+
apply(ctx) {
|
|
20
|
+
const toLang = (active) => (/^zh/i.test(active) ? 'zh' : 'en');
|
|
21
|
+
const locale = ctx.get('locale');
|
|
22
|
+
if (locale !== undefined) {
|
|
23
|
+
const syncLang = () => { setLang(toLang(locale.getSnapshot().active)); };
|
|
24
|
+
syncLang();
|
|
25
|
+
locale.subscribe(syncLang);
|
|
26
|
+
}
|
|
27
|
+
else if (typeof ctx.on === 'function') {
|
|
28
|
+
ctx.on('locale/change', (snapshot) => {
|
|
29
|
+
const active = snapshot?.active;
|
|
30
|
+
if (typeof active === 'string')
|
|
31
|
+
setLang(toLang(active));
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
const slots = ctx.get('slots');
|
|
35
|
+
if (slots === undefined)
|
|
36
|
+
return;
|
|
37
|
+
injectStyles();
|
|
38
|
+
// 底部终端抽屉(frame 级浮层,additive list 插槽)。
|
|
39
|
+
slots.inject('shell.overlay', () => slots.register({ name: 'shell.overlay', id: 'dsh-single-terminal', order: 50 }, TerminalDrawer));
|
|
40
|
+
// 会话头部开关按钮(utilities 区,会话标题行右侧)。
|
|
41
|
+
slots.inject('conversation.session.header.utilities', () => slots.register({ name: 'conversation.session.header.utilities', id: 'dsh-single-terminal-toggle', order: 100 }, TerminalToggle));
|
|
42
|
+
// Alt+C 开关抽屉;焦点在抽屉内(终端输入)时放行,Alt+C 作为 ESC c 发给 shell。
|
|
43
|
+
ctx.effect(() => {
|
|
44
|
+
const onKeyDown = (event) => {
|
|
45
|
+
if (!event.altKey || event.ctrlKey || event.metaKey || event.code !== 'KeyC')
|
|
46
|
+
return;
|
|
47
|
+
if (event.repeat)
|
|
48
|
+
return;
|
|
49
|
+
const target = event.target;
|
|
50
|
+
if (target instanceof Element && target.closest('.dst-drawer') !== null)
|
|
51
|
+
return;
|
|
52
|
+
event.preventDefault();
|
|
53
|
+
terminal.toggle();
|
|
54
|
+
};
|
|
55
|
+
window.addEventListener('keydown', onKeyDown);
|
|
56
|
+
return () => { window.removeEventListener('keydown', onKeyDown); };
|
|
57
|
+
});
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-single-terminal —— 客户端侧 WS 帧协议视图(与 src/host/types.ts 保持一致;
|
|
3
|
+
* 双面插件不共享运行时代码,这里独立声明)。
|
|
4
|
+
*/
|
|
5
|
+
export interface ShellInfo {
|
|
6
|
+
id: string;
|
|
7
|
+
name: string;
|
|
8
|
+
available: boolean;
|
|
9
|
+
}
|
|
10
|
+
export interface SessionInfo {
|
|
11
|
+
id: string;
|
|
12
|
+
shellId: string;
|
|
13
|
+
label: string;
|
|
14
|
+
cwd: string;
|
|
15
|
+
alive: boolean;
|
|
16
|
+
pid: number;
|
|
17
|
+
exitCode: number | null;
|
|
18
|
+
signal: number | null;
|
|
19
|
+
}
|
|
20
|
+
export type ClientFrame = {
|
|
21
|
+
type: 'ping';
|
|
22
|
+
} | {
|
|
23
|
+
type: 'list';
|
|
24
|
+
} | {
|
|
25
|
+
type: 'open';
|
|
26
|
+
shellId: string;
|
|
27
|
+
cols: number;
|
|
28
|
+
rows: number;
|
|
29
|
+
cwd?: string;
|
|
30
|
+
} | {
|
|
31
|
+
type: 'input';
|
|
32
|
+
id: string;
|
|
33
|
+
data: string;
|
|
34
|
+
} | {
|
|
35
|
+
type: 'resize';
|
|
36
|
+
id: string;
|
|
37
|
+
cols: number;
|
|
38
|
+
rows: number;
|
|
39
|
+
} | {
|
|
40
|
+
type: 'attach';
|
|
41
|
+
id: string;
|
|
42
|
+
} | {
|
|
43
|
+
type: 'close';
|
|
44
|
+
id: string;
|
|
45
|
+
};
|
|
46
|
+
export type HostFrame = {
|
|
47
|
+
type: 'pong';
|
|
48
|
+
} | {
|
|
49
|
+
type: 'hello';
|
|
50
|
+
platform: string;
|
|
51
|
+
defaultShell: string;
|
|
52
|
+
fontSize: number;
|
|
53
|
+
fontFamily: string;
|
|
54
|
+
} | {
|
|
55
|
+
type: 'shells';
|
|
56
|
+
defaultShell: string;
|
|
57
|
+
shells: ShellInfo[];
|
|
58
|
+
sessions: SessionInfo[];
|
|
59
|
+
} | {
|
|
60
|
+
type: 'opened';
|
|
61
|
+
session: SessionInfo;
|
|
62
|
+
} | {
|
|
63
|
+
type: 'data';
|
|
64
|
+
id: string;
|
|
65
|
+
data: string;
|
|
66
|
+
} | {
|
|
67
|
+
type: 'replay';
|
|
68
|
+
id: string;
|
|
69
|
+
data: string;
|
|
70
|
+
} | {
|
|
71
|
+
type: 'exit';
|
|
72
|
+
id: string;
|
|
73
|
+
exitCode: number | null;
|
|
74
|
+
signal: number | null;
|
|
75
|
+
closed?: boolean;
|
|
76
|
+
} | {
|
|
77
|
+
type: 'error';
|
|
78
|
+
message: string;
|
|
79
|
+
id?: string;
|
|
80
|
+
};
|
|
81
|
+
//# sourceMappingURL=protocol.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"protocol.d.ts","sourceRoot":"","sources":["../../../src/client/protocol.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,MAAM,WAAW,SAAS;IACxB,EAAE,EAAE,MAAM,CAAA;IACV,IAAI,EAAE,MAAM,CAAA;IACZ,SAAS,EAAE,OAAO,CAAA;CACnB;AAED,MAAM,WAAW,WAAW;IAC1B,EAAE,EAAE,MAAM,CAAA;IACV,OAAO,EAAE,MAAM,CAAA;IACf,KAAK,EAAE,MAAM,CAAA;IACb,GAAG,EAAE,MAAM,CAAA;IACX,KAAK,EAAE,OAAO,CAAA;IACd,GAAG,EAAE,MAAM,CAAA;IACX,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;IACvB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAA;CACtB;AAED,MAAM,MAAM,WAAW,GACnB;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,GAChB;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,GAChB;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,GAAG,CAAC,EAAE,MAAM,CAAA;CAAE,GAC3E;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,EAAE,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAC3C;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,EAAE,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAC1D;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,EAAE,EAAE,MAAM,CAAA;CAAE,GAC9B;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,EAAE,EAAE,MAAM,CAAA;CAAE,CAAA;AAEjC,MAAM,MAAM,SAAS,GACjB;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,GAChB;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,YAAY,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,MAAM,CAAA;CAAE,GAC/F;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,YAAY,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,SAAS,EAAE,CAAC;IAAC,QAAQ,EAAE,WAAW,EAAE,CAAA;CAAE,GACtF;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,OAAO,EAAE,WAAW,CAAA;CAAE,GACxC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAC1C;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,EAAE,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAC5C;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,MAAM,CAAC,EAAE,OAAO,CAAA;CAAE,GAC9F;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,EAAE,CAAC,EAAE,MAAM,CAAA;CAAE,CAAA"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"storage.d.ts","sourceRoot":"","sources":["../../../src/client/storage.ts"],"names":[],"mappings":"AAAA;;GAEG;AAIH,wBAAgB,QAAQ,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,GAAG,CAAC,CAQvD;AAED,wBAAgB,QAAQ,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,IAAI,CAI1D"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-single-terminal —— 浏览器侧 UI 状态持久化(localStorage,JSON 容错)。
|
|
3
|
+
*/
|
|
4
|
+
const PREFIX = 'dsh-single-terminal.';
|
|
5
|
+
export function loadJson(key, fallback) {
|
|
6
|
+
try {
|
|
7
|
+
const raw = localStorage.getItem(PREFIX + key);
|
|
8
|
+
if (raw === null)
|
|
9
|
+
return fallback;
|
|
10
|
+
return JSON.parse(raw);
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
return fallback;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
export function saveJson(key, value) {
|
|
17
|
+
try {
|
|
18
|
+
localStorage.setItem(PREFIX + key, JSON.stringify(value));
|
|
19
|
+
}
|
|
20
|
+
catch { /* storage unavailable */ }
|
|
21
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"styles.d.ts","sourceRoot":"","sources":["../../../src/client/styles.ts"],"names":[],"mappings":"AAAA;;GAEG;AAgGH,wBAAgB,YAAY,IAAI,IAAI,CAOnC"}
|