ttyd-mux 0.3.0 → 0.4.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 +105 -1
- package/dist/caddy/client.d.ts +3 -55
- package/dist/caddy/client.d.ts.map +1 -1
- package/dist/caddy/client.js +0 -73
- package/dist/caddy/client.js.map +1 -1
- package/dist/caddy/route-builder.d.ts +49 -0
- package/dist/caddy/route-builder.d.ts.map +1 -0
- package/dist/caddy/route-builder.js +175 -0
- package/dist/caddy/route-builder.js.map +1 -0
- package/dist/caddy/types.d.ts +27 -0
- package/dist/caddy/types.d.ts.map +1 -0
- package/dist/caddy/types.js +3 -0
- package/dist/caddy/types.js.map +1 -0
- package/dist/client/api-client.d.ts +26 -0
- package/dist/client/api-client.d.ts.map +1 -0
- package/dist/client/api-client.js +62 -0
- package/dist/client/api-client.js.map +1 -0
- package/dist/client/daemon-client.d.ts +48 -0
- package/dist/client/daemon-client.d.ts.map +1 -0
- package/dist/client/daemon-client.js +205 -0
- package/dist/client/daemon-client.js.map +1 -0
- package/dist/client/index.d.ts +2 -10
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +4 -136
- package/dist/client/index.js.map +1 -1
- package/dist/commands/attach.js +3 -4
- package/dist/commands/attach.js.map +1 -1
- package/dist/commands/caddy.d.ts +2 -1
- package/dist/commands/caddy.d.ts.map +1 -1
- package/dist/commands/caddy.js +227 -75
- package/dist/commands/caddy.js.map +1 -1
- package/dist/commands/daemon.js.map +1 -1
- package/dist/commands/deploy.d.ts +7 -0
- package/dist/commands/deploy.d.ts.map +1 -0
- package/dist/commands/deploy.js +100 -0
- package/dist/commands/deploy.js.map +1 -0
- package/dist/commands/doctor.d.ts +8 -0
- package/dist/commands/doctor.d.ts.map +1 -0
- package/dist/commands/doctor.js +180 -0
- package/dist/commands/doctor.js.map +1 -0
- package/dist/commands/down.d.ts.map +1 -1
- package/dist/commands/down.js +11 -0
- package/dist/commands/down.js.map +1 -1
- package/dist/commands/reload.d.ts +14 -0
- package/dist/commands/reload.d.ts.map +1 -0
- package/dist/commands/reload.js +50 -0
- package/dist/commands/reload.js.map +1 -0
- package/dist/commands/shutdown.d.ts +2 -1
- package/dist/commands/shutdown.d.ts.map +1 -1
- package/dist/commands/shutdown.js +8 -2
- package/dist/commands/shutdown.js.map +1 -1
- package/dist/commands/start.d.ts.map +1 -1
- package/dist/commands/start.js +16 -3
- package/dist/commands/start.js.map +1 -1
- package/dist/commands/status.js.map +1 -1
- package/dist/commands/stop.js.map +1 -1
- package/dist/commands/up.js.map +1 -1
- package/dist/config/config.d.ts.map +1 -1
- package/dist/config/config.js +9 -2
- package/dist/config/config.js.map +1 -1
- package/dist/config/index.d.ts +3 -3
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/index.js +6 -3
- package/dist/config/index.js.map +1 -1
- package/dist/config/state-store.d.ts +27 -0
- package/dist/config/state-store.d.ts.map +1 -0
- package/dist/config/state-store.js +55 -0
- package/dist/config/state-store.js.map +1 -0
- package/dist/config/state.d.ts +6 -0
- package/dist/config/state.d.ts.map +1 -1
- package/dist/config/state.js +49 -14
- package/dist/config/state.js.map +1 -1
- package/dist/config/types.d.ts +35 -0
- package/dist/config/types.d.ts.map +1 -1
- package/dist/config/types.js +23 -1
- package/dist/config/types.js.map +1 -1
- package/dist/daemon/api-handler.d.ts +5 -0
- package/dist/daemon/api-handler.d.ts.map +1 -0
- package/dist/daemon/api-handler.js +97 -0
- package/dist/daemon/api-handler.js.map +1 -0
- package/dist/daemon/config-manager.d.ts +43 -0
- package/dist/daemon/config-manager.d.ts.map +1 -0
- package/dist/daemon/config-manager.js +154 -0
- package/dist/daemon/config-manager.js.map +1 -0
- package/dist/daemon/http-proxy.d.ts +27 -0
- package/dist/daemon/http-proxy.d.ts.map +1 -0
- package/dist/daemon/http-proxy.js +110 -0
- package/dist/daemon/http-proxy.js.map +1 -0
- package/dist/daemon/ime-helper.d.ts +1 -1
- package/dist/daemon/ime-helper.d.ts.map +1 -1
- package/dist/daemon/ime-helper.js +284 -10
- package/dist/daemon/ime-helper.js.map +1 -1
- package/dist/daemon/index.d.ts.map +1 -1
- package/dist/daemon/index.js +134 -29
- package/dist/daemon/index.js.map +1 -1
- package/dist/daemon/portal-utils.d.ts +20 -0
- package/dist/daemon/portal-utils.d.ts.map +1 -0
- package/dist/daemon/portal-utils.js +109 -0
- package/dist/daemon/portal-utils.js.map +1 -0
- package/dist/daemon/portal.d.ts.map +1 -1
- package/dist/daemon/portal.js +20 -77
- package/dist/daemon/portal.js.map +1 -1
- package/dist/daemon/pwa.d.ts +52 -0
- package/dist/daemon/pwa.d.ts.map +1 -0
- package/dist/daemon/pwa.js +229 -0
- package/dist/daemon/pwa.js.map +1 -0
- package/dist/daemon/router.d.ts +15 -0
- package/dist/daemon/router.d.ts.map +1 -0
- package/dist/daemon/router.js +164 -0
- package/dist/daemon/router.js.map +1 -0
- package/dist/daemon/server.d.ts +15 -3
- package/dist/daemon/server.d.ts.map +1 -1
- package/dist/daemon/server.js +23 -271
- package/dist/daemon/server.js.map +1 -1
- package/dist/daemon/session-manager.d.ts +44 -10
- package/dist/daemon/session-manager.d.ts.map +1 -1
- package/dist/daemon/session-manager.js +125 -49
- package/dist/daemon/session-manager.js.map +1 -1
- package/dist/daemon/session-resolver.d.ts +1 -1
- package/dist/daemon/session-resolver.d.ts.map +1 -1
- package/dist/daemon/session-resolver.js.map +1 -1
- package/dist/daemon/toolbar/config.d.ts +13 -0
- package/dist/daemon/toolbar/config.d.ts.map +1 -0
- package/dist/daemon/toolbar/config.js +13 -0
- package/dist/daemon/toolbar/config.js.map +1 -0
- package/dist/daemon/toolbar/index.d.ts +43 -0
- package/dist/daemon/toolbar/index.d.ts.map +1 -0
- package/dist/daemon/toolbar/index.js +835 -0
- package/dist/daemon/toolbar/index.js.map +1 -0
- package/dist/daemon/toolbar/styles.d.ts +5 -0
- package/dist/daemon/toolbar/styles.d.ts.map +1 -0
- package/dist/daemon/toolbar/styles.js +278 -0
- package/dist/daemon/toolbar/styles.js.map +1 -0
- package/dist/daemon/toolbar/template.d.ts +6 -0
- package/dist/daemon/toolbar/template.d.ts.map +1 -0
- package/dist/daemon/toolbar/template.js +45 -0
- package/dist/daemon/toolbar/template.js.map +1 -0
- package/dist/daemon/ws-proxy.d.ts +17 -0
- package/dist/daemon/ws-proxy.d.ts.map +1 -0
- package/dist/daemon/ws-proxy.js +95 -0
- package/dist/daemon/ws-proxy.js.map +1 -0
- package/dist/deploy/caddyfile.d.ts +8 -0
- package/dist/deploy/caddyfile.d.ts.map +1 -0
- package/dist/deploy/caddyfile.js +62 -0
- package/dist/deploy/caddyfile.js.map +1 -0
- package/dist/deploy/deploy-script.d.ts +8 -0
- package/dist/deploy/deploy-script.d.ts.map +1 -0
- package/dist/deploy/deploy-script.js +72 -0
- package/dist/deploy/deploy-script.js.map +1 -0
- package/dist/deploy/static-portal.d.ts +3 -0
- package/dist/deploy/static-portal.d.ts.map +1 -0
- package/dist/deploy/static-portal.js +59 -0
- package/dist/deploy/static-portal.js.map +1 -0
- package/dist/index.js +38 -9
- package/dist/index.js.map +1 -1
- package/dist/test-setup.d.ts +19 -0
- package/dist/test-setup.d.ts.map +1 -0
- package/dist/test-setup.js +33 -0
- package/dist/test-setup.js.map +1 -0
- package/dist/tmux.d.ts +28 -1
- package/dist/tmux.d.ts.map +1 -1
- package/dist/tmux.js +37 -32
- package/dist/tmux.js.map +1 -1
- package/dist/ui.d.ts +2 -1
- package/dist/ui.d.ts.map +1 -1
- package/dist/ui.js +16 -9
- package/dist/ui.js.map +1 -1
- package/dist/utils/errors.d.ts +4 -0
- package/dist/utils/errors.d.ts.map +1 -1
- package/dist/utils/errors.js +9 -1
- package/dist/utils/errors.js.map +1 -1
- package/dist/utils/logger.d.ts +14 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +53 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/process-runner.d.ts +50 -0
- package/dist/utils/process-runner.d.ts.map +1 -0
- package/dist/utils/process-runner.js +73 -0
- package/dist/utils/process-runner.js.map +1 -0
- package/dist/utils/socket-client.d.ts +24 -0
- package/dist/utils/socket-client.d.ts.map +1 -0
- package/dist/utils/socket-client.js +30 -0
- package/dist/utils/socket-client.js.map +1 -0
- package/dist/utils/tmux-client.d.ts +57 -0
- package/dist/utils/tmux-client.d.ts.map +1 -0
- package/dist/utils/tmux-client.js +117 -0
- package/dist/utils/tmux-client.js.map +1 -0
- package/dist/version.d.ts +3 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +4 -0
- package/dist/version.js.map +1 -0
- package/package.json +6 -2
- package/dist/daemon/proxy.d.ts +0 -7
- package/dist/daemon/proxy.d.ts.map +0 -1
- package/dist/daemon/proxy.js +0 -17
- package/dist/daemon/proxy.js.map +0 -1
|
@@ -0,0 +1,835 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Terminal Toolbar Module
|
|
3
|
+
*
|
|
4
|
+
* Provides a toolbar for ttyd sessions with:
|
|
5
|
+
* - IME input support for Japanese
|
|
6
|
+
* - Font size zoom controls
|
|
7
|
+
* - Copy/paste functionality
|
|
8
|
+
* - Touch gesture support
|
|
9
|
+
* - Modifier key buttons (Ctrl, Alt, Shift)
|
|
10
|
+
*/
|
|
11
|
+
import { DEFAULT_TOOLBAR_CONFIG } from '../../config/types.js';
|
|
12
|
+
import { AUTO_RUN_KEY, ONBOARDING_SHOWN_KEY, STORAGE_KEY } from './config.js';
|
|
13
|
+
import { toolbarStyles } from './styles.js';
|
|
14
|
+
import { onboardingHtml, toolbarHtml } from './template.js';
|
|
15
|
+
// Re-export config constants (localStorage keys only)
|
|
16
|
+
export { AUTO_RUN_KEY, ONBOARDING_SHOWN_KEY, STORAGE_KEY };
|
|
17
|
+
// Re-export for direct access
|
|
18
|
+
export { onboardingHtml, toolbarHtml, toolbarStyles };
|
|
19
|
+
// Re-export type and default config
|
|
20
|
+
export { DEFAULT_TOOLBAR_CONFIG };
|
|
21
|
+
/**
|
|
22
|
+
* Generate the toolbar JavaScript code
|
|
23
|
+
* @param config - Toolbar configuration from config.yaml
|
|
24
|
+
*/
|
|
25
|
+
export function getToolbarScript(config = DEFAULT_TOOLBAR_CONFIG) {
|
|
26
|
+
const { font_size_min, font_size_max, font_size_default_mobile, font_size_default_pc, double_tap_delay } = config;
|
|
27
|
+
return `(function() {
|
|
28
|
+
const container = document.getElementById('ttyd-toolbar');
|
|
29
|
+
const input = document.getElementById('ttyd-toolbar-input');
|
|
30
|
+
const sendBtn = document.getElementById('ttyd-toolbar-send');
|
|
31
|
+
const enterBtn = document.getElementById('ttyd-toolbar-enter');
|
|
32
|
+
const zoomInBtn = document.getElementById('ttyd-toolbar-zoomin');
|
|
33
|
+
const zoomOutBtn = document.getElementById('ttyd-toolbar-zoomout');
|
|
34
|
+
const runBtn = document.getElementById('ttyd-toolbar-run');
|
|
35
|
+
const toggleBtn = document.getElementById('ttyd-toolbar-toggle');
|
|
36
|
+
const ctrlBtn = document.getElementById('ttyd-toolbar-ctrl');
|
|
37
|
+
const altBtn = document.getElementById('ttyd-toolbar-alt');
|
|
38
|
+
const shiftBtn = document.getElementById('ttyd-toolbar-shift');
|
|
39
|
+
const escBtn = document.getElementById('ttyd-toolbar-esc');
|
|
40
|
+
const tabBtn = document.getElementById('ttyd-toolbar-tab');
|
|
41
|
+
const upBtn = document.getElementById('ttyd-toolbar-up');
|
|
42
|
+
const downBtn = document.getElementById('ttyd-toolbar-down');
|
|
43
|
+
const copyBtn = document.getElementById('ttyd-toolbar-copy');
|
|
44
|
+
const copyAllBtn = document.getElementById('ttyd-toolbar-copyall');
|
|
45
|
+
const autoBtn = document.getElementById('ttyd-toolbar-auto');
|
|
46
|
+
const minimizeBtn = document.getElementById('ttyd-toolbar-minimize');
|
|
47
|
+
const scrollBtn = document.getElementById('ttyd-toolbar-scroll');
|
|
48
|
+
const pageUpBtn = document.getElementById('ttyd-toolbar-pageup');
|
|
49
|
+
const pageDownBtn = document.getElementById('ttyd-toolbar-pagedown');
|
|
50
|
+
|
|
51
|
+
let ws = null;
|
|
52
|
+
let ctrlActive = false;
|
|
53
|
+
let altActive = false;
|
|
54
|
+
let shiftActive = false;
|
|
55
|
+
let autoRunActive = false;
|
|
56
|
+
let scrollActive = false;
|
|
57
|
+
|
|
58
|
+
// Detect mobile device
|
|
59
|
+
const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
|
|
60
|
+
|
|
61
|
+
// Font size configuration (from config.yaml)
|
|
62
|
+
const FONT_SIZE_MIN = ${font_size_min};
|
|
63
|
+
const FONT_SIZE_MAX = ${font_size_max};
|
|
64
|
+
const FONT_SIZE_DEFAULT = isMobile ? ${font_size_default_mobile} : ${font_size_default_pc};
|
|
65
|
+
const FONT_SIZE_STORAGE_KEY = '${STORAGE_KEY}';
|
|
66
|
+
const ONBOARDING_KEY = '${ONBOARDING_SHOWN_KEY}';
|
|
67
|
+
const AUTO_RUN_STORAGE_KEY = '${AUTO_RUN_KEY}';
|
|
68
|
+
|
|
69
|
+
function saveFontSize(size) {
|
|
70
|
+
try {
|
|
71
|
+
localStorage.setItem(FONT_SIZE_STORAGE_KEY, String(size));
|
|
72
|
+
} catch (e) {
|
|
73
|
+
console.warn('[Toolbar] Failed to save font size:', e);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function loadFontSize() {
|
|
78
|
+
try {
|
|
79
|
+
const saved = localStorage.getItem(FONT_SIZE_STORAGE_KEY);
|
|
80
|
+
if (saved) {
|
|
81
|
+
const size = parseInt(saved, 10);
|
|
82
|
+
if (!isNaN(size) && size >= FONT_SIZE_MIN && size <= FONT_SIZE_MAX) {
|
|
83
|
+
return size;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
} catch (e) {
|
|
87
|
+
console.warn('[Toolbar] Failed to load font size:', e);
|
|
88
|
+
}
|
|
89
|
+
return FONT_SIZE_DEFAULT;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function saveAutoRun(enabled) {
|
|
93
|
+
try {
|
|
94
|
+
localStorage.setItem(AUTO_RUN_STORAGE_KEY, enabled ? '1' : '0');
|
|
95
|
+
} catch (e) {
|
|
96
|
+
console.warn('[Toolbar] Failed to save auto-run state:', e);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function loadAutoRun() {
|
|
101
|
+
try {
|
|
102
|
+
const saved = localStorage.getItem(AUTO_RUN_STORAGE_KEY);
|
|
103
|
+
return saved === '1';
|
|
104
|
+
} catch (e) {
|
|
105
|
+
console.warn('[Toolbar] Failed to load auto-run state:', e);
|
|
106
|
+
}
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Find the WebSocket connection
|
|
111
|
+
function findWebSocket() {
|
|
112
|
+
if (ws && ws.readyState === WebSocket.OPEN) return ws;
|
|
113
|
+
|
|
114
|
+
if (window.socket && window.socket.readyState === WebSocket.OPEN) {
|
|
115
|
+
ws = window.socket;
|
|
116
|
+
return ws;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Intercept WebSocket creation to capture the connection
|
|
123
|
+
const OriginalWebSocket = window.WebSocket;
|
|
124
|
+
window.WebSocket = function(url, protocols) {
|
|
125
|
+
const socket = new OriginalWebSocket(url, protocols);
|
|
126
|
+
if (url.includes('/ws')) {
|
|
127
|
+
ws = socket;
|
|
128
|
+
}
|
|
129
|
+
return socket;
|
|
130
|
+
};
|
|
131
|
+
window.WebSocket.prototype = OriginalWebSocket.prototype;
|
|
132
|
+
window.WebSocket.CONNECTING = OriginalWebSocket.CONNECTING;
|
|
133
|
+
window.WebSocket.OPEN = OriginalWebSocket.OPEN;
|
|
134
|
+
window.WebSocket.CLOSING = OriginalWebSocket.CLOSING;
|
|
135
|
+
window.WebSocket.CLOSED = OriginalWebSocket.CLOSED;
|
|
136
|
+
|
|
137
|
+
function sendText(text) {
|
|
138
|
+
const socket = findWebSocket();
|
|
139
|
+
if (!socket) {
|
|
140
|
+
console.error('[Toolbar] WebSocket not found');
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// ttyd protocol: binary data with '0' (input command) as first byte
|
|
145
|
+
const encoder = new TextEncoder();
|
|
146
|
+
const textBytes = encoder.encode(text);
|
|
147
|
+
const data = new Uint8Array(textBytes.length + 1);
|
|
148
|
+
data[0] = '0'.charCodeAt(0); // Input command
|
|
149
|
+
data.set(textBytes, 1);
|
|
150
|
+
socket.send(data);
|
|
151
|
+
return true;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function sendKey(key) {
|
|
155
|
+
// Apply modifiers
|
|
156
|
+
if (ctrlActive && key.length === 1) {
|
|
157
|
+
// Ctrl+key: send as control character (A=1, B=2, ..., Z=26)
|
|
158
|
+
const code = key.toUpperCase().charCodeAt(0) - 64;
|
|
159
|
+
if (code > 0 && code < 32) {
|
|
160
|
+
sendBytes([code]);
|
|
161
|
+
}
|
|
162
|
+
resetModifiers();
|
|
163
|
+
} else if (altActive && key.length === 1) {
|
|
164
|
+
// Alt+key: send ESC + key
|
|
165
|
+
const keyCode = key.charCodeAt(0);
|
|
166
|
+
sendBytes([0x1B, keyCode]);
|
|
167
|
+
resetModifiers();
|
|
168
|
+
} else {
|
|
169
|
+
sendText(key);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function resetModifiers() {
|
|
174
|
+
ctrlActive = false;
|
|
175
|
+
altActive = false;
|
|
176
|
+
ctrlBtn.classList.remove('active');
|
|
177
|
+
altBtn.classList.remove('active');
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Send raw bytes for special keys
|
|
181
|
+
function sendBytes(bytes) {
|
|
182
|
+
const socket = findWebSocket();
|
|
183
|
+
if (!socket) {
|
|
184
|
+
console.error('[Toolbar] WebSocket not found');
|
|
185
|
+
return false;
|
|
186
|
+
}
|
|
187
|
+
const data = new Uint8Array(bytes.length + 1);
|
|
188
|
+
data[0] = 0x30; // '0' = input command
|
|
189
|
+
data.set(bytes, 1);
|
|
190
|
+
socket.send(data);
|
|
191
|
+
return true;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function sendEnter() {
|
|
195
|
+
sendBytes([0x0D]); // CR
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function sendEsc() {
|
|
199
|
+
sendBytes([0x1B]); // ESC
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function sendTab() {
|
|
203
|
+
sendBytes([0x09]); // TAB
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function sendUp() {
|
|
207
|
+
sendBytes([0x1B, 0x5B, 0x41]); // ESC [ A
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function sendDown() {
|
|
211
|
+
sendBytes([0x1B, 0x5B, 0x42]); // ESC [ B
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function sendPageUp() {
|
|
215
|
+
sendBytes([0x1B, 0x5B, 0x35, 0x7E]); // ESC [ 5 ~
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function sendPageDown() {
|
|
219
|
+
sendBytes([0x1B, 0x5B, 0x36, 0x7E]); // ESC [ 6 ~
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function fitTerminal() {
|
|
223
|
+
if (window.fitAddon && typeof window.fitAddon.fit === 'function') {
|
|
224
|
+
window.fitAddon.fit();
|
|
225
|
+
console.log('[Toolbar] Terminal fitted via fitAddon');
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (window.term && window.term.fitAddon && typeof window.term.fitAddon.fit === 'function') {
|
|
230
|
+
window.term.fitAddon.fit();
|
|
231
|
+
console.log('[Toolbar] Terminal fitted via term.fitAddon');
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
window.dispatchEvent(new Event('resize'));
|
|
236
|
+
console.log('[Toolbar] Dispatched resize event');
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function findTerminal() {
|
|
240
|
+
if (window.term) return window.term;
|
|
241
|
+
const termEl = document.querySelector('.xterm');
|
|
242
|
+
if (termEl && termEl._core) return termEl._core;
|
|
243
|
+
return null;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function zoomTerminal(delta) {
|
|
247
|
+
const term = findTerminal();
|
|
248
|
+
|
|
249
|
+
if (term && term.options) {
|
|
250
|
+
const currentSize = term.options.fontSize || FONT_SIZE_DEFAULT;
|
|
251
|
+
const newSize = Math.max(FONT_SIZE_MIN, Math.min(FONT_SIZE_MAX, currentSize + delta));
|
|
252
|
+
term.options.fontSize = newSize;
|
|
253
|
+
saveFontSize(newSize);
|
|
254
|
+
console.log('[Toolbar] Font size changed to ' + newSize);
|
|
255
|
+
fitTerminal();
|
|
256
|
+
} else {
|
|
257
|
+
console.log('[Toolbar] Terminal not found for zoom');
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function copySelection() {
|
|
262
|
+
const term = findTerminal();
|
|
263
|
+
if (!term) {
|
|
264
|
+
console.log('[Toolbar] Terminal not found for copy');
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
const selection = term.getSelection();
|
|
268
|
+
if (selection) {
|
|
269
|
+
navigator.clipboard.writeText(selection).then(function() {
|
|
270
|
+
console.log('[Toolbar] Copied selection to clipboard');
|
|
271
|
+
}).catch(function(err) {
|
|
272
|
+
console.error('[Toolbar] Failed to copy:', err);
|
|
273
|
+
});
|
|
274
|
+
} else {
|
|
275
|
+
console.log('[Toolbar] No text selected');
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function copyAll() {
|
|
280
|
+
const term = findTerminal();
|
|
281
|
+
if (!term || !term.buffer || !term.buffer.active) {
|
|
282
|
+
console.log('[Toolbar] Terminal buffer not found');
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
const buffer = term.buffer.active;
|
|
286
|
+
const lines = [];
|
|
287
|
+
for (let i = 0; i < buffer.length; i++) {
|
|
288
|
+
const line = buffer.getLine(i);
|
|
289
|
+
if (line) {
|
|
290
|
+
lines.push(line.translateToString(true));
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
const text = lines.join('\\n').trimEnd();
|
|
294
|
+
navigator.clipboard.writeText(text).then(function() {
|
|
295
|
+
console.log('[Toolbar] Copied all text to clipboard');
|
|
296
|
+
}).catch(function(err) {
|
|
297
|
+
console.error('[Toolbar] Failed to copy:', err);
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function submitInput() {
|
|
302
|
+
const text = input.value;
|
|
303
|
+
if (!text) return;
|
|
304
|
+
|
|
305
|
+
if (sendText(text)) {
|
|
306
|
+
input.value = '';
|
|
307
|
+
adjustTextareaHeight();
|
|
308
|
+
// Auto mode: send Enter after 1 second
|
|
309
|
+
if (autoRunActive) {
|
|
310
|
+
setTimeout(function() {
|
|
311
|
+
sendEnter();
|
|
312
|
+
}, 1000);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function runInput() {
|
|
318
|
+
const text = input.value;
|
|
319
|
+
if (!text) return;
|
|
320
|
+
|
|
321
|
+
if (sendText(text)) {
|
|
322
|
+
input.value = '';
|
|
323
|
+
adjustTextareaHeight();
|
|
324
|
+
// Wait 1 second then send Enter
|
|
325
|
+
setTimeout(function() {
|
|
326
|
+
sendEnter();
|
|
327
|
+
}, 1000);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function toggleToolbar(show) {
|
|
332
|
+
if (typeof show === 'boolean') {
|
|
333
|
+
container.classList.toggle('hidden', !show);
|
|
334
|
+
} else {
|
|
335
|
+
container.classList.toggle('hidden');
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
if (!container.classList.contains('hidden')) {
|
|
339
|
+
input.focus();
|
|
340
|
+
// Fit terminal after showing toolbar
|
|
341
|
+
setTimeout(fitTerminal, 100);
|
|
342
|
+
} else {
|
|
343
|
+
const terminal = document.querySelector('.xterm-helper-textarea');
|
|
344
|
+
if (terminal) terminal.focus();
|
|
345
|
+
setTimeout(fitTerminal, 100);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function adjustTextareaHeight() {
|
|
350
|
+
input.style.height = 'auto';
|
|
351
|
+
input.style.height = Math.min(input.scrollHeight, 120) + 'px';
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// Event listeners
|
|
355
|
+
sendBtn.addEventListener('click', function(e) {
|
|
356
|
+
e.preventDefault();
|
|
357
|
+
submitInput();
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
enterBtn.addEventListener('click', function(e) {
|
|
361
|
+
e.preventDefault();
|
|
362
|
+
sendEnter();
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
runBtn.addEventListener('click', function(e) {
|
|
366
|
+
e.preventDefault();
|
|
367
|
+
runInput();
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
zoomInBtn.addEventListener('click', function(e) {
|
|
371
|
+
e.preventDefault();
|
|
372
|
+
zoomTerminal(2);
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
zoomOutBtn.addEventListener('click', function(e) {
|
|
376
|
+
e.preventDefault();
|
|
377
|
+
zoomTerminal(-2);
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
ctrlBtn.addEventListener('click', function(e) {
|
|
381
|
+
e.preventDefault();
|
|
382
|
+
ctrlActive = !ctrlActive;
|
|
383
|
+
ctrlBtn.classList.toggle('active', ctrlActive);
|
|
384
|
+
if (ctrlActive) {
|
|
385
|
+
altActive = false;
|
|
386
|
+
altBtn.classList.remove('active');
|
|
387
|
+
}
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
altBtn.addEventListener('click', function(e) {
|
|
391
|
+
e.preventDefault();
|
|
392
|
+
altActive = !altActive;
|
|
393
|
+
altBtn.classList.toggle('active', altActive);
|
|
394
|
+
if (altActive) {
|
|
395
|
+
ctrlActive = false;
|
|
396
|
+
ctrlBtn.classList.remove('active');
|
|
397
|
+
}
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
shiftBtn.addEventListener('click', function(e) {
|
|
401
|
+
e.preventDefault();
|
|
402
|
+
shiftActive = !shiftActive;
|
|
403
|
+
shiftBtn.classList.toggle('active', shiftActive);
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
autoBtn.addEventListener('click', function(e) {
|
|
407
|
+
e.preventDefault();
|
|
408
|
+
autoRunActive = !autoRunActive;
|
|
409
|
+
autoBtn.classList.toggle('active', autoRunActive);
|
|
410
|
+
saveAutoRun(autoRunActive);
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
escBtn.addEventListener('click', function(e) {
|
|
414
|
+
e.preventDefault();
|
|
415
|
+
sendEsc();
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
tabBtn.addEventListener('click', function(e) {
|
|
419
|
+
e.preventDefault();
|
|
420
|
+
sendTab();
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
upBtn.addEventListener('click', function(e) {
|
|
424
|
+
e.preventDefault();
|
|
425
|
+
sendUp();
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
downBtn.addEventListener('click', function(e) {
|
|
429
|
+
e.preventDefault();
|
|
430
|
+
sendDown();
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
pageUpBtn.addEventListener('click', function(e) {
|
|
434
|
+
e.preventDefault();
|
|
435
|
+
sendPageUp();
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
pageDownBtn.addEventListener('click', function(e) {
|
|
439
|
+
e.preventDefault();
|
|
440
|
+
sendPageDown();
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
scrollBtn.addEventListener('click', function(e) {
|
|
444
|
+
e.preventDefault();
|
|
445
|
+
scrollActive = !scrollActive;
|
|
446
|
+
scrollBtn.classList.toggle('active', scrollActive);
|
|
447
|
+
if (scrollActive) {
|
|
448
|
+
console.log('[Toolbar] Scroll mode enabled - drag to scroll');
|
|
449
|
+
} else {
|
|
450
|
+
console.log('[Toolbar] Scroll mode disabled');
|
|
451
|
+
}
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
copyBtn.addEventListener('click', function(e) {
|
|
455
|
+
e.preventDefault();
|
|
456
|
+
copySelection();
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
copyAllBtn.addEventListener('click', function(e) {
|
|
460
|
+
e.preventDefault();
|
|
461
|
+
copyAll();
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
minimizeBtn.addEventListener('click', function(e) {
|
|
465
|
+
e.preventDefault();
|
|
466
|
+
container.classList.toggle('minimized');
|
|
467
|
+
// Update button text based on state
|
|
468
|
+
minimizeBtn.textContent = container.classList.contains('minimized') ? '▲' : '▼';
|
|
469
|
+
setTimeout(fitTerminal, 100);
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
input.addEventListener('input', adjustTextareaHeight);
|
|
473
|
+
|
|
474
|
+
input.addEventListener('keydown', function(e) {
|
|
475
|
+
if (e.key === 'Enter' && !e.shiftKey && !e.isComposing) {
|
|
476
|
+
e.preventDefault();
|
|
477
|
+
submitInput();
|
|
478
|
+
} else if (e.key === 'Escape') {
|
|
479
|
+
e.preventDefault();
|
|
480
|
+
toggleToolbar(false);
|
|
481
|
+
}
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
toggleBtn.addEventListener('click', function(e) {
|
|
485
|
+
e.preventDefault();
|
|
486
|
+
toggleToolbar();
|
|
487
|
+
});
|
|
488
|
+
|
|
489
|
+
// Keyboard shortcut: Ctrl+J to toggle toolbar
|
|
490
|
+
document.addEventListener('keydown', function(e) {
|
|
491
|
+
if (e.ctrlKey && e.key === 'j') {
|
|
492
|
+
e.preventDefault();
|
|
493
|
+
toggleToolbar();
|
|
494
|
+
}
|
|
495
|
+
});
|
|
496
|
+
|
|
497
|
+
// Inject shiftKey into mouse events when Shift button is active
|
|
498
|
+
// This allows text selection to bypass tmux mouse mode
|
|
499
|
+
['mousedown', 'mousemove', 'mouseup'].forEach(function(eventType) {
|
|
500
|
+
document.addEventListener(eventType, function(e) {
|
|
501
|
+
// Don't interfere with toolbar buttons
|
|
502
|
+
if (e.target.closest('#ttyd-toolbar') || e.target.closest('#ttyd-toolbar-toggle')) {
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
505
|
+
if (shiftActive && !e.shiftKey) {
|
|
506
|
+
const newEvent = new MouseEvent(e.type, {
|
|
507
|
+
bubbles: e.bubbles,
|
|
508
|
+
cancelable: e.cancelable,
|
|
509
|
+
view: e.view,
|
|
510
|
+
detail: e.detail,
|
|
511
|
+
screenX: e.screenX,
|
|
512
|
+
screenY: e.screenY,
|
|
513
|
+
clientX: e.clientX,
|
|
514
|
+
clientY: e.clientY,
|
|
515
|
+
ctrlKey: e.ctrlKey,
|
|
516
|
+
altKey: e.altKey,
|
|
517
|
+
shiftKey: true,
|
|
518
|
+
metaKey: e.metaKey,
|
|
519
|
+
button: e.button,
|
|
520
|
+
buttons: e.buttons,
|
|
521
|
+
relatedTarget: e.relatedTarget
|
|
522
|
+
});
|
|
523
|
+
e.stopImmediatePropagation();
|
|
524
|
+
e.preventDefault();
|
|
525
|
+
e.target.dispatchEvent(newEvent);
|
|
526
|
+
}
|
|
527
|
+
}, true);
|
|
528
|
+
});
|
|
529
|
+
|
|
530
|
+
// Convert touch events to mouse events with shiftKey when Shift is active
|
|
531
|
+
// This enables text selection on mobile devices
|
|
532
|
+
let touchStartPos = null;
|
|
533
|
+
|
|
534
|
+
function dispatchMouseEvent(type, touch, shiftKey) {
|
|
535
|
+
const mouseEvent = new MouseEvent(type, {
|
|
536
|
+
bubbles: true,
|
|
537
|
+
cancelable: true,
|
|
538
|
+
view: window,
|
|
539
|
+
detail: 1,
|
|
540
|
+
screenX: touch.screenX,
|
|
541
|
+
screenY: touch.screenY,
|
|
542
|
+
clientX: touch.clientX,
|
|
543
|
+
clientY: touch.clientY,
|
|
544
|
+
ctrlKey: false,
|
|
545
|
+
altKey: false,
|
|
546
|
+
shiftKey: shiftKey,
|
|
547
|
+
metaKey: false,
|
|
548
|
+
button: 0,
|
|
549
|
+
buttons: type === 'mouseup' ? 0 : 1,
|
|
550
|
+
relatedTarget: null
|
|
551
|
+
});
|
|
552
|
+
touch.target.dispatchEvent(mouseEvent);
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
let shiftTouchActive = false; // Track if we're in Shift+touch selection mode
|
|
556
|
+
let scrollTouchActive = false; // Track if we're in scroll drag mode
|
|
557
|
+
let scrollLastY = 0; // Track last Y position for scroll delta
|
|
558
|
+
const SCROLL_THRESHOLD = 30; // Pixels to drag before triggering scroll
|
|
559
|
+
|
|
560
|
+
document.addEventListener('touchstart', function(e) {
|
|
561
|
+
// Don't interfere with toolbar buttons
|
|
562
|
+
if (e.target.closest('#ttyd-toolbar') || e.target.closest('#ttyd-toolbar-toggle')) {
|
|
563
|
+
return;
|
|
564
|
+
}
|
|
565
|
+
// Single finger touch with Scroll active -> enable scroll drag mode
|
|
566
|
+
if (e.touches.length === 1 && scrollActive) {
|
|
567
|
+
const touch = e.touches[0];
|
|
568
|
+
touchStartPos = { x: touch.clientX, y: touch.clientY };
|
|
569
|
+
scrollLastY = touch.clientY;
|
|
570
|
+
scrollTouchActive = true;
|
|
571
|
+
e.preventDefault();
|
|
572
|
+
}
|
|
573
|
+
// Single finger touch with Shift active -> convert to mouse event for selection
|
|
574
|
+
else if (e.touches.length === 1 && shiftActive) {
|
|
575
|
+
const touch = e.touches[0];
|
|
576
|
+
touchStartPos = { x: touch.clientX, y: touch.clientY };
|
|
577
|
+
shiftTouchActive = true;
|
|
578
|
+
e.preventDefault();
|
|
579
|
+
dispatchMouseEvent('mousedown', touch, true);
|
|
580
|
+
}
|
|
581
|
+
// 2nd finger added -> cancel Shift/Scroll mode, allow pinch
|
|
582
|
+
else if (e.touches.length === 2 && (shiftTouchActive || scrollTouchActive)) {
|
|
583
|
+
if (shiftTouchActive) {
|
|
584
|
+
dispatchMouseEvent('mouseup', e.touches[0], true);
|
|
585
|
+
}
|
|
586
|
+
shiftTouchActive = false;
|
|
587
|
+
scrollTouchActive = false;
|
|
588
|
+
touchStartPos = null;
|
|
589
|
+
// Don't preventDefault - let pinch handlers take over
|
|
590
|
+
}
|
|
591
|
+
// Track non-Shift/Scroll single touch for hint
|
|
592
|
+
else if (e.touches.length === 1 && !shiftActive && !scrollActive) {
|
|
593
|
+
const touch = e.touches[0];
|
|
594
|
+
touchStartPos = { x: touch.clientX, y: touch.clientY };
|
|
595
|
+
}
|
|
596
|
+
}, { passive: false, capture: true });
|
|
597
|
+
|
|
598
|
+
document.addEventListener('touchmove', function(e) {
|
|
599
|
+
// Handle scroll drag mode
|
|
600
|
+
if (e.touches.length === 1 && scrollTouchActive) {
|
|
601
|
+
e.preventDefault();
|
|
602
|
+
const touch = e.touches[0];
|
|
603
|
+
const deltaY = scrollLastY - touch.clientY; // Positive = dragging up (scroll down/page down)
|
|
604
|
+
|
|
605
|
+
// Trigger scroll when threshold is reached
|
|
606
|
+
if (Math.abs(deltaY) >= SCROLL_THRESHOLD) {
|
|
607
|
+
if (deltaY > 0) {
|
|
608
|
+
sendPageDown();
|
|
609
|
+
} else {
|
|
610
|
+
sendPageUp();
|
|
611
|
+
}
|
|
612
|
+
scrollLastY = touch.clientY; // Reset for next scroll step
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
// Handle Shift selection mode
|
|
616
|
+
else if (e.touches.length === 1 && shiftTouchActive) {
|
|
617
|
+
e.preventDefault();
|
|
618
|
+
dispatchMouseEvent('mousemove', e.touches[0], true);
|
|
619
|
+
}
|
|
620
|
+
// Don't interfere with 2-finger gestures (pinch)
|
|
621
|
+
}, { passive: false, capture: true });
|
|
622
|
+
|
|
623
|
+
document.addEventListener('touchend', function(e) {
|
|
624
|
+
// Scroll mode ending
|
|
625
|
+
if (scrollTouchActive && e.touches.length === 0) {
|
|
626
|
+
scrollTouchActive = false;
|
|
627
|
+
touchStartPos = null;
|
|
628
|
+
}
|
|
629
|
+
// Shift selection mode ending
|
|
630
|
+
else if (shiftTouchActive && e.touches.length === 0) {
|
|
631
|
+
const touch = e.changedTouches[0];
|
|
632
|
+
dispatchMouseEvent('mouseup', touch, true);
|
|
633
|
+
shiftTouchActive = false;
|
|
634
|
+
touchStartPos = null;
|
|
635
|
+
}
|
|
636
|
+
}, { passive: true, capture: true });
|
|
637
|
+
|
|
638
|
+
// Pinch-to-zoom for font size (when Ctrl or Shift is active)
|
|
639
|
+
let pinchStartDistance = 0;
|
|
640
|
+
let pinchStartFontSize = FONT_SIZE_DEFAULT;
|
|
641
|
+
|
|
642
|
+
function getTouchDistance(touches) {
|
|
643
|
+
const dx = touches[0].clientX - touches[1].clientX;
|
|
644
|
+
const dy = touches[0].clientY - touches[1].clientY;
|
|
645
|
+
return Math.sqrt(dx * dx + dy * dy);
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
document.addEventListener('touchstart', function(e) {
|
|
649
|
+
if (e.touches.length === 2 && (ctrlActive || shiftActive)) {
|
|
650
|
+
pinchStartDistance = getTouchDistance(e.touches);
|
|
651
|
+
const term = findTerminal();
|
|
652
|
+
pinchStartFontSize = (term && term.options) ? (term.options.fontSize || FONT_SIZE_DEFAULT) : FONT_SIZE_DEFAULT;
|
|
653
|
+
}
|
|
654
|
+
}, { passive: true });
|
|
655
|
+
|
|
656
|
+
document.addEventListener('touchmove', function(e) {
|
|
657
|
+
if (e.touches.length === 2 && (ctrlActive || shiftActive) && pinchStartDistance > 0) {
|
|
658
|
+
e.preventDefault(); // Suppress browser zoom
|
|
659
|
+
const currentDistance = getTouchDistance(e.touches);
|
|
660
|
+
const scale = currentDistance / pinchStartDistance;
|
|
661
|
+
const newSize = Math.round(pinchStartFontSize * scale);
|
|
662
|
+
const clampedSize = Math.max(FONT_SIZE_MIN, Math.min(FONT_SIZE_MAX, newSize));
|
|
663
|
+
|
|
664
|
+
const term = findTerminal();
|
|
665
|
+
if (term && term.options && term.options.fontSize !== clampedSize) {
|
|
666
|
+
term.options.fontSize = clampedSize;
|
|
667
|
+
saveFontSize(clampedSize);
|
|
668
|
+
fitTerminal();
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
}, { passive: false });
|
|
672
|
+
|
|
673
|
+
document.addEventListener('touchend', function(e) {
|
|
674
|
+
if (e.touches.length < 2) {
|
|
675
|
+
pinchStartDistance = 0;
|
|
676
|
+
}
|
|
677
|
+
}, { passive: true });
|
|
678
|
+
|
|
679
|
+
// ========== PC: Ctrl+Wheel / Trackpad Pinch ==========
|
|
680
|
+
document.addEventListener('wheel', function(e) {
|
|
681
|
+
// ctrlKey = trackpad pinch (Mac) or Ctrl+scroll (PC)
|
|
682
|
+
if (e.ctrlKey) {
|
|
683
|
+
e.preventDefault(); // Suppress browser zoom
|
|
684
|
+
|
|
685
|
+
// deltaY > 0: zoom out, deltaY < 0: zoom in
|
|
686
|
+
const delta = e.deltaY > 0 ? -2 : 2;
|
|
687
|
+
zoomTerminal(delta);
|
|
688
|
+
}
|
|
689
|
+
}, { passive: false });
|
|
690
|
+
|
|
691
|
+
// Double-tap to send Enter (for reconnecting)
|
|
692
|
+
let lastTapTime = 0;
|
|
693
|
+
const DOUBLE_TAP_DELAY = ${double_tap_delay};
|
|
694
|
+
|
|
695
|
+
document.addEventListener('touchend', function(e) {
|
|
696
|
+
// Exclude toolbar elements
|
|
697
|
+
if (e.target.closest('#ttyd-toolbar') || e.target.closest('#ttyd-toolbar-toggle')) {
|
|
698
|
+
return;
|
|
699
|
+
}
|
|
700
|
+
// Single touch only
|
|
701
|
+
if (e.changedTouches.length !== 1) return;
|
|
702
|
+
|
|
703
|
+
const now = Date.now();
|
|
704
|
+
if (now - lastTapTime < DOUBLE_TAP_DELAY) {
|
|
705
|
+
// Double tap detected -> send Enter
|
|
706
|
+
sendEnter();
|
|
707
|
+
lastTapTime = 0; // Reset
|
|
708
|
+
} else {
|
|
709
|
+
lastTapTime = now;
|
|
710
|
+
}
|
|
711
|
+
}, { passive: true });
|
|
712
|
+
|
|
713
|
+
// Auto-show on mobile devices
|
|
714
|
+
if (isMobile) {
|
|
715
|
+
setTimeout(function() {
|
|
716
|
+
toggleToolbar(true);
|
|
717
|
+
}, 1000);
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
// Onboarding: show tips on first access (mobile only)
|
|
721
|
+
function showOnboarding() {
|
|
722
|
+
const onboarding = document.getElementById('ttyd-toolbar-onboarding');
|
|
723
|
+
if (!onboarding) return;
|
|
724
|
+
|
|
725
|
+
try {
|
|
726
|
+
if (localStorage.getItem(ONBOARDING_KEY)) {
|
|
727
|
+
onboarding.remove();
|
|
728
|
+
return;
|
|
729
|
+
}
|
|
730
|
+
} catch (e) {
|
|
731
|
+
// localStorage not available
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
// Show onboarding tooltip
|
|
735
|
+
onboarding.style.display = 'block';
|
|
736
|
+
|
|
737
|
+
const closeBtn = document.getElementById('ttyd-toolbar-onboarding-close');
|
|
738
|
+
if (closeBtn) {
|
|
739
|
+
closeBtn.addEventListener('click', function() {
|
|
740
|
+
onboarding.remove();
|
|
741
|
+
try {
|
|
742
|
+
localStorage.setItem(ONBOARDING_KEY, '1');
|
|
743
|
+
} catch (e) {
|
|
744
|
+
// Ignore
|
|
745
|
+
}
|
|
746
|
+
});
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
// Auto-dismiss after 15 seconds
|
|
750
|
+
setTimeout(function() {
|
|
751
|
+
if (onboarding.parentNode) {
|
|
752
|
+
onboarding.remove();
|
|
753
|
+
try {
|
|
754
|
+
localStorage.setItem(ONBOARDING_KEY, '1');
|
|
755
|
+
} catch (e) {
|
|
756
|
+
// Ignore
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
}, 15000);
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
if (isMobile) {
|
|
763
|
+
setTimeout(showOnboarding, 1500);
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
// Restore font size from localStorage
|
|
767
|
+
function applyStoredFontSize() {
|
|
768
|
+
const term = findTerminal();
|
|
769
|
+
if (term && term.options) {
|
|
770
|
+
const storedSize = loadFontSize();
|
|
771
|
+
term.options.fontSize = storedSize;
|
|
772
|
+
fitTerminal();
|
|
773
|
+
console.log('[Toolbar] Restored font size: ' + storedSize);
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
// Try to apply stored font size after terminal is ready
|
|
778
|
+
setTimeout(applyStoredFontSize, 500);
|
|
779
|
+
setTimeout(applyStoredFontSize, 1500);
|
|
780
|
+
|
|
781
|
+
// Restore auto-run state from localStorage
|
|
782
|
+
function applyStoredAutoRun() {
|
|
783
|
+
const storedAutoRun = loadAutoRun();
|
|
784
|
+
if (storedAutoRun) {
|
|
785
|
+
autoRunActive = true;
|
|
786
|
+
autoBtn.classList.add('active');
|
|
787
|
+
console.log('[Toolbar] Restored auto-run mode: enabled');
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
applyStoredAutoRun();
|
|
791
|
+
|
|
792
|
+
// Auto-reload when tab becomes visible if WebSocket is disconnected
|
|
793
|
+
document.addEventListener('visibilitychange', function() {
|
|
794
|
+
if (!document.hidden) {
|
|
795
|
+
const socket = findWebSocket();
|
|
796
|
+
if (!socket || socket.readyState !== WebSocket.OPEN) {
|
|
797
|
+
console.log('[Toolbar] Connection lost, reloading...');
|
|
798
|
+
location.reload();
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
});
|
|
802
|
+
|
|
803
|
+
console.log('[Toolbar] Loaded. ' + (isMobile ? 'Mobile mode.' : 'Press Ctrl+J or click keyboard button to toggle.'));
|
|
804
|
+
})();`;
|
|
805
|
+
}
|
|
806
|
+
/**
|
|
807
|
+
* Get the complete toolbar JavaScript for serving as external file
|
|
808
|
+
* @param config - Toolbar configuration from config.yaml
|
|
809
|
+
*/
|
|
810
|
+
export function getToolbarJs(config = DEFAULT_TOOLBAR_CONFIG) {
|
|
811
|
+
return getToolbarScript(config);
|
|
812
|
+
}
|
|
813
|
+
/**
|
|
814
|
+
* Inject toolbar into HTML response
|
|
815
|
+
*
|
|
816
|
+
* Injects:
|
|
817
|
+
* - CSS styles (inline for FOUC avoidance)
|
|
818
|
+
* - HTML structure
|
|
819
|
+
* - Onboarding tooltip (hidden by default)
|
|
820
|
+
* - Script tag referencing external toolbar.js
|
|
821
|
+
*
|
|
822
|
+
* @param html - Original HTML content
|
|
823
|
+
* @param basePath - Base path for the ttyd-mux routes (e.g., "/ttyd-mux")
|
|
824
|
+
* @returns Modified HTML with toolbar injected
|
|
825
|
+
*/
|
|
826
|
+
export function injectToolbar(html, basePath) {
|
|
827
|
+
const injection = `
|
|
828
|
+
<style>${toolbarStyles}</style>
|
|
829
|
+
${toolbarHtml}
|
|
830
|
+
${onboardingHtml.replace('id="ttyd-toolbar-onboarding"', 'id="ttyd-toolbar-onboarding" style="display:none"')}
|
|
831
|
+
<script src="${basePath}/toolbar.js"></script>
|
|
832
|
+
`;
|
|
833
|
+
return html.replace('</body>', `${injection}</body>`);
|
|
834
|
+
}
|
|
835
|
+
//# sourceMappingURL=index.js.map
|