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.
Files changed (196) hide show
  1. package/README.md +105 -1
  2. package/dist/caddy/client.d.ts +3 -55
  3. package/dist/caddy/client.d.ts.map +1 -1
  4. package/dist/caddy/client.js +0 -73
  5. package/dist/caddy/client.js.map +1 -1
  6. package/dist/caddy/route-builder.d.ts +49 -0
  7. package/dist/caddy/route-builder.d.ts.map +1 -0
  8. package/dist/caddy/route-builder.js +175 -0
  9. package/dist/caddy/route-builder.js.map +1 -0
  10. package/dist/caddy/types.d.ts +27 -0
  11. package/dist/caddy/types.d.ts.map +1 -0
  12. package/dist/caddy/types.js +3 -0
  13. package/dist/caddy/types.js.map +1 -0
  14. package/dist/client/api-client.d.ts +26 -0
  15. package/dist/client/api-client.d.ts.map +1 -0
  16. package/dist/client/api-client.js +62 -0
  17. package/dist/client/api-client.js.map +1 -0
  18. package/dist/client/daemon-client.d.ts +48 -0
  19. package/dist/client/daemon-client.d.ts.map +1 -0
  20. package/dist/client/daemon-client.js +205 -0
  21. package/dist/client/daemon-client.js.map +1 -0
  22. package/dist/client/index.d.ts +2 -10
  23. package/dist/client/index.d.ts.map +1 -1
  24. package/dist/client/index.js +4 -136
  25. package/dist/client/index.js.map +1 -1
  26. package/dist/commands/attach.js +3 -4
  27. package/dist/commands/attach.js.map +1 -1
  28. package/dist/commands/caddy.d.ts +2 -1
  29. package/dist/commands/caddy.d.ts.map +1 -1
  30. package/dist/commands/caddy.js +227 -75
  31. package/dist/commands/caddy.js.map +1 -1
  32. package/dist/commands/daemon.js.map +1 -1
  33. package/dist/commands/deploy.d.ts +7 -0
  34. package/dist/commands/deploy.d.ts.map +1 -0
  35. package/dist/commands/deploy.js +100 -0
  36. package/dist/commands/deploy.js.map +1 -0
  37. package/dist/commands/doctor.d.ts +8 -0
  38. package/dist/commands/doctor.d.ts.map +1 -0
  39. package/dist/commands/doctor.js +180 -0
  40. package/dist/commands/doctor.js.map +1 -0
  41. package/dist/commands/down.d.ts.map +1 -1
  42. package/dist/commands/down.js +11 -0
  43. package/dist/commands/down.js.map +1 -1
  44. package/dist/commands/reload.d.ts +14 -0
  45. package/dist/commands/reload.d.ts.map +1 -0
  46. package/dist/commands/reload.js +50 -0
  47. package/dist/commands/reload.js.map +1 -0
  48. package/dist/commands/shutdown.d.ts +2 -1
  49. package/dist/commands/shutdown.d.ts.map +1 -1
  50. package/dist/commands/shutdown.js +8 -2
  51. package/dist/commands/shutdown.js.map +1 -1
  52. package/dist/commands/start.d.ts.map +1 -1
  53. package/dist/commands/start.js +16 -3
  54. package/dist/commands/start.js.map +1 -1
  55. package/dist/commands/status.js.map +1 -1
  56. package/dist/commands/stop.js.map +1 -1
  57. package/dist/commands/up.js.map +1 -1
  58. package/dist/config/config.d.ts.map +1 -1
  59. package/dist/config/config.js +9 -2
  60. package/dist/config/config.js.map +1 -1
  61. package/dist/config/index.d.ts +3 -3
  62. package/dist/config/index.d.ts.map +1 -1
  63. package/dist/config/index.js +6 -3
  64. package/dist/config/index.js.map +1 -1
  65. package/dist/config/state-store.d.ts +27 -0
  66. package/dist/config/state-store.d.ts.map +1 -0
  67. package/dist/config/state-store.js +55 -0
  68. package/dist/config/state-store.js.map +1 -0
  69. package/dist/config/state.d.ts +6 -0
  70. package/dist/config/state.d.ts.map +1 -1
  71. package/dist/config/state.js +49 -14
  72. package/dist/config/state.js.map +1 -1
  73. package/dist/config/types.d.ts +35 -0
  74. package/dist/config/types.d.ts.map +1 -1
  75. package/dist/config/types.js +23 -1
  76. package/dist/config/types.js.map +1 -1
  77. package/dist/daemon/api-handler.d.ts +5 -0
  78. package/dist/daemon/api-handler.d.ts.map +1 -0
  79. package/dist/daemon/api-handler.js +97 -0
  80. package/dist/daemon/api-handler.js.map +1 -0
  81. package/dist/daemon/config-manager.d.ts +43 -0
  82. package/dist/daemon/config-manager.d.ts.map +1 -0
  83. package/dist/daemon/config-manager.js +154 -0
  84. package/dist/daemon/config-manager.js.map +1 -0
  85. package/dist/daemon/http-proxy.d.ts +27 -0
  86. package/dist/daemon/http-proxy.d.ts.map +1 -0
  87. package/dist/daemon/http-proxy.js +110 -0
  88. package/dist/daemon/http-proxy.js.map +1 -0
  89. package/dist/daemon/ime-helper.d.ts +1 -1
  90. package/dist/daemon/ime-helper.d.ts.map +1 -1
  91. package/dist/daemon/ime-helper.js +284 -10
  92. package/dist/daemon/ime-helper.js.map +1 -1
  93. package/dist/daemon/index.d.ts.map +1 -1
  94. package/dist/daemon/index.js +134 -29
  95. package/dist/daemon/index.js.map +1 -1
  96. package/dist/daemon/portal-utils.d.ts +20 -0
  97. package/dist/daemon/portal-utils.d.ts.map +1 -0
  98. package/dist/daemon/portal-utils.js +109 -0
  99. package/dist/daemon/portal-utils.js.map +1 -0
  100. package/dist/daemon/portal.d.ts.map +1 -1
  101. package/dist/daemon/portal.js +20 -77
  102. package/dist/daemon/portal.js.map +1 -1
  103. package/dist/daemon/pwa.d.ts +52 -0
  104. package/dist/daemon/pwa.d.ts.map +1 -0
  105. package/dist/daemon/pwa.js +229 -0
  106. package/dist/daemon/pwa.js.map +1 -0
  107. package/dist/daemon/router.d.ts +15 -0
  108. package/dist/daemon/router.d.ts.map +1 -0
  109. package/dist/daemon/router.js +164 -0
  110. package/dist/daemon/router.js.map +1 -0
  111. package/dist/daemon/server.d.ts +15 -3
  112. package/dist/daemon/server.d.ts.map +1 -1
  113. package/dist/daemon/server.js +23 -271
  114. package/dist/daemon/server.js.map +1 -1
  115. package/dist/daemon/session-manager.d.ts +44 -10
  116. package/dist/daemon/session-manager.d.ts.map +1 -1
  117. package/dist/daemon/session-manager.js +125 -49
  118. package/dist/daemon/session-manager.js.map +1 -1
  119. package/dist/daemon/session-resolver.d.ts +1 -1
  120. package/dist/daemon/session-resolver.d.ts.map +1 -1
  121. package/dist/daemon/session-resolver.js.map +1 -1
  122. package/dist/daemon/toolbar/config.d.ts +13 -0
  123. package/dist/daemon/toolbar/config.d.ts.map +1 -0
  124. package/dist/daemon/toolbar/config.js +13 -0
  125. package/dist/daemon/toolbar/config.js.map +1 -0
  126. package/dist/daemon/toolbar/index.d.ts +43 -0
  127. package/dist/daemon/toolbar/index.d.ts.map +1 -0
  128. package/dist/daemon/toolbar/index.js +835 -0
  129. package/dist/daemon/toolbar/index.js.map +1 -0
  130. package/dist/daemon/toolbar/styles.d.ts +5 -0
  131. package/dist/daemon/toolbar/styles.d.ts.map +1 -0
  132. package/dist/daemon/toolbar/styles.js +278 -0
  133. package/dist/daemon/toolbar/styles.js.map +1 -0
  134. package/dist/daemon/toolbar/template.d.ts +6 -0
  135. package/dist/daemon/toolbar/template.d.ts.map +1 -0
  136. package/dist/daemon/toolbar/template.js +45 -0
  137. package/dist/daemon/toolbar/template.js.map +1 -0
  138. package/dist/daemon/ws-proxy.d.ts +17 -0
  139. package/dist/daemon/ws-proxy.d.ts.map +1 -0
  140. package/dist/daemon/ws-proxy.js +95 -0
  141. package/dist/daemon/ws-proxy.js.map +1 -0
  142. package/dist/deploy/caddyfile.d.ts +8 -0
  143. package/dist/deploy/caddyfile.d.ts.map +1 -0
  144. package/dist/deploy/caddyfile.js +62 -0
  145. package/dist/deploy/caddyfile.js.map +1 -0
  146. package/dist/deploy/deploy-script.d.ts +8 -0
  147. package/dist/deploy/deploy-script.d.ts.map +1 -0
  148. package/dist/deploy/deploy-script.js +72 -0
  149. package/dist/deploy/deploy-script.js.map +1 -0
  150. package/dist/deploy/static-portal.d.ts +3 -0
  151. package/dist/deploy/static-portal.d.ts.map +1 -0
  152. package/dist/deploy/static-portal.js +59 -0
  153. package/dist/deploy/static-portal.js.map +1 -0
  154. package/dist/index.js +38 -9
  155. package/dist/index.js.map +1 -1
  156. package/dist/test-setup.d.ts +19 -0
  157. package/dist/test-setup.d.ts.map +1 -0
  158. package/dist/test-setup.js +33 -0
  159. package/dist/test-setup.js.map +1 -0
  160. package/dist/tmux.d.ts +28 -1
  161. package/dist/tmux.d.ts.map +1 -1
  162. package/dist/tmux.js +37 -32
  163. package/dist/tmux.js.map +1 -1
  164. package/dist/ui.d.ts +2 -1
  165. package/dist/ui.d.ts.map +1 -1
  166. package/dist/ui.js +16 -9
  167. package/dist/ui.js.map +1 -1
  168. package/dist/utils/errors.d.ts +4 -0
  169. package/dist/utils/errors.d.ts.map +1 -1
  170. package/dist/utils/errors.js +9 -1
  171. package/dist/utils/errors.js.map +1 -1
  172. package/dist/utils/logger.d.ts +14 -0
  173. package/dist/utils/logger.d.ts.map +1 -0
  174. package/dist/utils/logger.js +53 -0
  175. package/dist/utils/logger.js.map +1 -0
  176. package/dist/utils/process-runner.d.ts +50 -0
  177. package/dist/utils/process-runner.d.ts.map +1 -0
  178. package/dist/utils/process-runner.js +73 -0
  179. package/dist/utils/process-runner.js.map +1 -0
  180. package/dist/utils/socket-client.d.ts +24 -0
  181. package/dist/utils/socket-client.d.ts.map +1 -0
  182. package/dist/utils/socket-client.js +30 -0
  183. package/dist/utils/socket-client.js.map +1 -0
  184. package/dist/utils/tmux-client.d.ts +57 -0
  185. package/dist/utils/tmux-client.d.ts.map +1 -0
  186. package/dist/utils/tmux-client.js +117 -0
  187. package/dist/utils/tmux-client.js.map +1 -0
  188. package/dist/version.d.ts +3 -0
  189. package/dist/version.d.ts.map +1 -0
  190. package/dist/version.js +4 -0
  191. package/dist/version.js.map +1 -0
  192. package/package.json +6 -2
  193. package/dist/daemon/proxy.d.ts +0 -7
  194. package/dist/daemon/proxy.d.ts.map +0 -1
  195. package/dist/daemon/proxy.js +0 -17
  196. 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