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,110 @@
1
+ import { gzipSync } from 'node:zlib';
2
+ import { createLogger } from '../utils/logger.js';
3
+ import httpProxy from 'http-proxy';
4
+ import { injectToolbar } from './toolbar/index.js';
5
+ const log = createLogger('proxy');
6
+ /**
7
+ * Build clean headers object (filter out undefined values and encoding headers)
8
+ */
9
+ export function buildCleanHeaders(headers) {
10
+ const cleanHeaders = {};
11
+ for (const [key, value] of Object.entries(headers)) {
12
+ if (value !== undefined && key !== 'content-encoding' && key !== 'transfer-encoding') {
13
+ cleanHeaders[key] = value;
14
+ }
15
+ }
16
+ return cleanHeaders;
17
+ }
18
+ /**
19
+ * Transform HTML response with toolbar injection and optional gzip compression
20
+ */
21
+ export function transformHtmlResponse(originalHtml, supportsGzip, basePath) {
22
+ const modifiedHtml = injectToolbar(originalHtml, basePath);
23
+ const headers = {};
24
+ if (supportsGzip) {
25
+ const compressed = gzipSync(modifiedHtml);
26
+ headers['content-encoding'] = 'gzip';
27
+ headers['content-length'] = String(compressed.length);
28
+ return { body: compressed, headers };
29
+ }
30
+ headers['content-length'] = String(Buffer.byteLength(modifiedHtml));
31
+ return { body: Buffer.from(modifiedHtml), headers };
32
+ }
33
+ /**
34
+ * Check if content type is HTML
35
+ */
36
+ export function isHtmlContentType(contentType) {
37
+ return contentType.includes('text/html');
38
+ }
39
+ /**
40
+ * Check if client supports gzip encoding
41
+ */
42
+ export function supportsGzipEncoding(acceptEncoding) {
43
+ return acceptEncoding.includes('gzip');
44
+ }
45
+ // Create proxy server for HTTP only
46
+ export const proxy = httpProxy.createProxyServer({
47
+ changeOrigin: true,
48
+ xfwd: true
49
+ });
50
+ // Handle proxy errors
51
+ proxy.on('error', (err, req, res) => {
52
+ const url = req.url ?? 'unknown';
53
+ log.error(`Proxy error for ${url}: ${err.message}`);
54
+ if (res && 'writeHead' in res && typeof res.writeHead === 'function') {
55
+ const httpRes = res;
56
+ if (!httpRes.headersSent) {
57
+ httpRes.writeHead(502, { 'Content-Type': 'text/html; charset=utf-8' });
58
+ httpRes.end(`<!DOCTYPE html>
59
+ <html><head><title>502 Bad Gateway</title></head>
60
+ <body style="font-family:sans-serif;padding:2em;">
61
+ <h1>502 Bad Gateway</h1>
62
+ <p>The ttyd session may have been stopped or is not responding.</p>
63
+ <p>Try <a href="javascript:location.reload()">reloading</a> or check session status with: <code>ttyd-mux status</code></p>
64
+ </body></html>`);
65
+ }
66
+ }
67
+ });
68
+ // Handle selfHandleResponse for HTML injection
69
+ proxy.on('proxyRes', (proxyRes, req, res) => {
70
+ const httpRes = res;
71
+ // Check if this is a self-handled HTML response
72
+ const contentType = proxyRes.headers['content-type'] ?? '';
73
+ if (!isHtmlContentType(contentType)) {
74
+ // Not HTML, just pipe through
75
+ httpRes.writeHead(proxyRes.statusCode ?? 200, proxyRes.headers);
76
+ proxyRes.pipe(httpRes);
77
+ return;
78
+ }
79
+ const extReq = req;
80
+ const acceptEncoding = extReq.originalAcceptEncoding ?? '';
81
+ const supportsGzip = supportsGzipEncoding(acceptEncoding);
82
+ const basePath = extReq.toolbarBasePath ?? '/ttyd-mux';
83
+ // Collect HTML body and inject toolbar
84
+ const chunks = [];
85
+ proxyRes.on('data', (chunk) => chunks.push(chunk));
86
+ proxyRes.on('end', () => {
87
+ const originalHtml = Buffer.concat(chunks).toString('utf-8');
88
+ const { body, headers: transformHeaders } = transformHtmlResponse(originalHtml, supportsGzip, basePath);
89
+ // Build clean headers object
90
+ const headers = buildCleanHeaders(proxyRes.headers);
91
+ Object.assign(headers, transformHeaders);
92
+ httpRes.writeHead(proxyRes.statusCode ?? 200, headers);
93
+ httpRes.end(body);
94
+ });
95
+ });
96
+ /**
97
+ * Proxy HTTP request to session backend
98
+ */
99
+ export function proxyToSession(req, res, port, basePath) {
100
+ const target = `http://localhost:${port}`;
101
+ log.debug(`Proxying ${req.url} to ${target}`);
102
+ const extReq = req;
103
+ extReq.originalAcceptEncoding = req.headers['accept-encoding'];
104
+ extReq.toolbarBasePath = basePath;
105
+ // Request uncompressed response for HTML injection (identity = no encoding)
106
+ req.headers['accept-encoding'] = 'identity';
107
+ // Always use selfHandleResponse to avoid conflicts with proxyRes handler
108
+ proxy.web(req, res, { target, selfHandleResponse: true });
109
+ }
110
+ //# sourceMappingURL=http-proxy.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"http-proxy.js","sourceRoot":"","sources":["../../src/daemon/http-proxy.ts"],"names":[],"mappings":"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"}
@@ -3,6 +3,6 @@
3
3
  * Provides a pseudo copy-paste input field for Japanese IME support
4
4
  * Optimized for mobile devices
5
5
  */
6
- export declare const imeHelperScript = "\n<style>\n#ttyd-ime-container {\n position: fixed;\n bottom: 0;\n left: 0;\n right: 0;\n background: #1e1e1e;\n border-top: 2px solid #007acc;\n padding: 8px;\n z-index: 10000;\n font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, sans-serif;\n box-shadow: 0 -2px 10px rgba(0,0,0,0.3);\n}\n\n#ttyd-ime-container.hidden {\n display: none;\n}\n\n#ttyd-ime-buttons {\n display: flex;\n gap: 6px;\n margin-bottom: 8px;\n flex-wrap: wrap;\n}\n\n#ttyd-ime-buttons button {\n background: #3a3a3a;\n border: 1px solid #555;\n border-radius: 6px;\n color: #fff;\n cursor: pointer;\n font-size: 13px;\n padding: 8px 12px;\n min-height: 40px;\n min-width: 44px;\n touch-action: manipulation;\n flex-shrink: 0;\n}\n\n#ttyd-ime-buttons button:hover, #ttyd-ime-buttons button:active {\n background: #4a4a4a;\n}\n\n#ttyd-ime-buttons button.active {\n background: #007acc;\n border-color: #005a9e;\n}\n\n#ttyd-ime-buttons button.modifier {\n background: #2d2d2d;\n font-weight: bold;\n}\n\n#ttyd-ime-buttons button.modifier.active {\n background: #d9534f;\n border-color: #c9302c;\n}\n\n#ttyd-ime-send {\n background: #007acc !important;\n border-color: #005a9e !important;\n font-weight: bold;\n}\n\n#ttyd-ime-send:hover, #ttyd-ime-send:active {\n background: #005a9e !important;\n}\n\n#ttyd-ime-run {\n background: #28a745 !important;\n border-color: #1e7e34 !important;\n font-weight: bold;\n}\n\n#ttyd-ime-run:hover, #ttyd-ime-run:active {\n background: #1e7e34 !important;\n}\n\n#ttyd-ime-input-row {\n display: flex;\n gap: 8px;\n align-items: flex-end;\n}\n\n#ttyd-ime-input {\n flex: 1;\n background: #2d2d2d;\n border: 1px solid #555;\n border-radius: 8px;\n color: #fff;\n font-family: monospace;\n font-size: 16px;\n padding: 12px;\n outline: none;\n resize: none;\n min-height: 44px;\n max-height: 120px;\n line-height: 1.4;\n}\n\n#ttyd-ime-input:focus {\n border-color: #007acc;\n}\n\n#ttyd-ime-input::placeholder {\n color: #888;\n}\n\n#ttyd-ime-toggle {\n position: fixed;\n bottom: 16px;\n right: 16px;\n background: #007acc;\n border: 2px solid #005a9e;\n border-radius: 50%;\n color: #fff;\n cursor: pointer;\n font-size: 20px;\n width: 56px;\n height: 56px;\n z-index: 10001;\n touch-action: manipulation;\n box-shadow: 0 2px 8px rgba(0,0,0,0.3);\n display: flex;\n align-items: center;\n justify-content: center;\n}\n\n#ttyd-ime-toggle:hover, #ttyd-ime-toggle:active {\n background: #005a9e;\n transform: scale(1.05);\n}\n\n#ttyd-ime-container.hidden ~ #ttyd-ime-toggle {\n bottom: 16px;\n}\n\n/* Adjust terminal height when IME bar is visible */\nbody:has(#ttyd-ime-container:not(.hidden)) .xterm {\n height: calc(100vh - 140px) !important;\n}\n\n/* Mobile optimizations */\n@media (max-width: 768px) {\n #ttyd-ime-container {\n padding: 6px;\n }\n\n #ttyd-ime-buttons {\n gap: 4px;\n margin-bottom: 6px;\n }\n\n #ttyd-ime-buttons button {\n font-size: 12px;\n padding: 6px 10px;\n min-height: 36px;\n min-width: 40px;\n }\n\n #ttyd-ime-input {\n font-size: 16px;\n padding: 10px;\n }\n\n #ttyd-ime-toggle {\n width: 64px;\n height: 64px;\n font-size: 24px;\n }\n\n body:has(#ttyd-ime-container:not(.hidden)) .xterm {\n height: calc(100vh - 130px) !important;\n }\n}\n</style>\n\n<div id=\"ttyd-ime-container\" class=\"hidden\">\n <div id=\"ttyd-ime-buttons\">\n <button id=\"ttyd-ime-ctrl\" class=\"modifier\">Ctrl</button>\n <button id=\"ttyd-ime-alt\" class=\"modifier\">Alt</button>\n <button id=\"ttyd-ime-esc\">Esc</button>\n <button id=\"ttyd-ime-tab\">Tab</button>\n <button id=\"ttyd-ime-up\">\u2191</button>\n <button id=\"ttyd-ime-down\">\u2193</button>\n <button id=\"ttyd-ime-enter\">Enter</button>\n <button id=\"ttyd-ime-zoomout\">A-</button>\n <button id=\"ttyd-ime-zoomin\">A+</button>\n <button id=\"ttyd-ime-send\">Send</button>\n <button id=\"ttyd-ime-run\">Run</button>\n </div>\n <div id=\"ttyd-ime-input-row\">\n <textarea id=\"ttyd-ime-input\" rows=\"1\" placeholder=\"\u65E5\u672C\u8A9E\u5165\u529B (Enter: \u9001\u4FE1)\"></textarea>\n </div>\n</div>\n<button id=\"ttyd-ime-toggle\">\u2328</button>\n\n<script>\n(function() {\n const container = document.getElementById('ttyd-ime-container');\n const input = document.getElementById('ttyd-ime-input');\n const sendBtn = document.getElementById('ttyd-ime-send');\n const enterBtn = document.getElementById('ttyd-ime-enter');\n const zoomInBtn = document.getElementById('ttyd-ime-zoomin');\n const zoomOutBtn = document.getElementById('ttyd-ime-zoomout');\n const runBtn = document.getElementById('ttyd-ime-run');\n const toggleBtn = document.getElementById('ttyd-ime-toggle');\n const ctrlBtn = document.getElementById('ttyd-ime-ctrl');\n const altBtn = document.getElementById('ttyd-ime-alt');\n const escBtn = document.getElementById('ttyd-ime-esc');\n const tabBtn = document.getElementById('ttyd-ime-tab');\n const upBtn = document.getElementById('ttyd-ime-up');\n const downBtn = document.getElementById('ttyd-ime-down');\n\n let ws = null;\n let ctrlActive = false;\n let altActive = false;\n\n // Detect mobile device\n const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);\n\n // Find the WebSocket connection\n function findWebSocket() {\n if (ws && ws.readyState === WebSocket.OPEN) return ws;\n\n if (window.socket && window.socket.readyState === WebSocket.OPEN) {\n ws = window.socket;\n return ws;\n }\n\n return null;\n }\n\n // Intercept WebSocket creation to capture the connection\n const OriginalWebSocket = window.WebSocket;\n window.WebSocket = function(url, protocols) {\n const socket = new OriginalWebSocket(url, protocols);\n if (url.includes('/ws')) {\n ws = socket;\n }\n return socket;\n };\n window.WebSocket.prototype = OriginalWebSocket.prototype;\n window.WebSocket.CONNECTING = OriginalWebSocket.CONNECTING;\n window.WebSocket.OPEN = OriginalWebSocket.OPEN;\n window.WebSocket.CLOSING = OriginalWebSocket.CLOSING;\n window.WebSocket.CLOSED = OriginalWebSocket.CLOSED;\n\n function sendText(text) {\n const socket = findWebSocket();\n if (!socket) {\n console.error('[IME Helper] WebSocket not found');\n return false;\n }\n\n // ttyd protocol: binary data with '0' (input command) as first byte\n const encoder = new TextEncoder();\n const textBytes = encoder.encode(text);\n const data = new Uint8Array(textBytes.length + 1);\n data[0] = '0'.charCodeAt(0); // Input command\n data.set(textBytes, 1);\n socket.send(data);\n return true;\n }\n\n function sendKey(key) {\n // Apply modifiers\n if (ctrlActive && key.length === 1) {\n // Ctrl+key: send as control character (A=1, B=2, ..., Z=26)\n const code = key.toUpperCase().charCodeAt(0) - 64;\n if (code > 0 && code < 32) {\n sendBytes([code]);\n }\n resetModifiers();\n } else if (altActive && key.length === 1) {\n // Alt+key: send ESC + key\n const keyCode = key.charCodeAt(0);\n sendBytes([0x1B, keyCode]);\n resetModifiers();\n } else {\n sendText(key);\n }\n }\n\n function resetModifiers() {\n ctrlActive = false;\n altActive = false;\n ctrlBtn.classList.remove('active');\n altBtn.classList.remove('active');\n }\n\n // Send raw bytes for special keys\n function sendBytes(bytes) {\n const socket = findWebSocket();\n if (!socket) {\n console.error('[IME Helper] WebSocket not found');\n return false;\n }\n const data = new Uint8Array(bytes.length + 1);\n data[0] = 0x30; // '0' = input command\n data.set(bytes, 1);\n socket.send(data);\n return true;\n }\n\n function sendEnter() {\n sendBytes([0x0D]); // CR\n }\n\n function sendEsc() {\n sendBytes([0x1B]); // ESC\n }\n\n function sendTab() {\n sendBytes([0x09]); // TAB\n }\n\n function sendUp() {\n sendBytes([0x1B, 0x5B, 0x41]); // ESC [ A\n }\n\n function sendDown() {\n sendBytes([0x1B, 0x5B, 0x42]); // ESC [ B\n }\n\n function fitTerminal() {\n if (window.fitAddon && typeof window.fitAddon.fit === 'function') {\n window.fitAddon.fit();\n console.log('[IME Helper] Terminal fitted via fitAddon');\n return;\n }\n\n if (window.term && window.term.fitAddon && typeof window.term.fitAddon.fit === 'function') {\n window.term.fitAddon.fit();\n console.log('[IME Helper] Terminal fitted via term.fitAddon');\n return;\n }\n\n window.dispatchEvent(new Event('resize'));\n console.log('[IME Helper] Dispatched resize event');\n }\n\n function zoomTerminal(delta) {\n // Try to find the terminal instance\n let term = window.term;\n if (!term) {\n // ttyd might store it differently\n const termEl = document.querySelector('.xterm');\n if (termEl && termEl._core) {\n term = termEl._core;\n }\n }\n\n if (term && term.options) {\n const currentSize = term.options.fontSize || 14;\n const newSize = Math.max(8, Math.min(32, currentSize + delta));\n term.options.fontSize = newSize;\n console.log('[IME Helper] Font size changed to ' + newSize);\n fitTerminal();\n } else {\n console.log('[IME Helper] Terminal not found for zoom');\n }\n }\n\n function submitInput() {\n const text = input.value;\n if (!text) return;\n\n if (sendText(text)) {\n input.value = '';\n adjustTextareaHeight();\n }\n }\n\n function runInput() {\n const text = input.value;\n if (!text) return;\n\n if (sendText(text)) {\n input.value = '';\n adjustTextareaHeight();\n // Wait 1 second then send Enter\n setTimeout(function() {\n sendEnter();\n }, 1000);\n }\n }\n\n function toggleIME(show) {\n if (typeof show === 'boolean') {\n container.classList.toggle('hidden', !show);\n } else {\n container.classList.toggle('hidden');\n }\n\n if (!container.classList.contains('hidden')) {\n input.focus();\n // Fit terminal after showing IME bar\n setTimeout(fitTerminal, 100);\n } else {\n const terminal = document.querySelector('.xterm-helper-textarea');\n if (terminal) terminal.focus();\n setTimeout(fitTerminal, 100);\n }\n }\n\n function adjustTextareaHeight() {\n input.style.height = 'auto';\n input.style.height = Math.min(input.scrollHeight, 120) + 'px';\n }\n\n // Event listeners\n sendBtn.addEventListener('click', function(e) {\n e.preventDefault();\n submitInput();\n });\n\n enterBtn.addEventListener('click', function(e) {\n e.preventDefault();\n sendEnter();\n });\n\n runBtn.addEventListener('click', function(e) {\n e.preventDefault();\n runInput();\n });\n\n zoomInBtn.addEventListener('click', function(e) {\n e.preventDefault();\n zoomTerminal(2);\n });\n\n zoomOutBtn.addEventListener('click', function(e) {\n e.preventDefault();\n zoomTerminal(-2);\n });\n\n ctrlBtn.addEventListener('click', function(e) {\n e.preventDefault();\n ctrlActive = !ctrlActive;\n ctrlBtn.classList.toggle('active', ctrlActive);\n if (ctrlActive) {\n altActive = false;\n altBtn.classList.remove('active');\n }\n });\n\n altBtn.addEventListener('click', function(e) {\n e.preventDefault();\n altActive = !altActive;\n altBtn.classList.toggle('active', altActive);\n if (altActive) {\n ctrlActive = false;\n ctrlBtn.classList.remove('active');\n }\n });\n\n escBtn.addEventListener('click', function(e) {\n e.preventDefault();\n sendEsc();\n });\n\n tabBtn.addEventListener('click', function(e) {\n e.preventDefault();\n sendTab();\n });\n\n upBtn.addEventListener('click', function(e) {\n e.preventDefault();\n sendUp();\n });\n\n downBtn.addEventListener('click', function(e) {\n e.preventDefault();\n sendDown();\n });\n\n input.addEventListener('input', adjustTextareaHeight);\n\n input.addEventListener('keydown', function(e) {\n if (e.key === 'Enter' && !e.shiftKey && !e.isComposing) {\n e.preventDefault();\n submitInput();\n } else if (e.key === 'Escape') {\n e.preventDefault();\n toggleIME(false);\n }\n });\n\n toggleBtn.addEventListener('click', function(e) {\n e.preventDefault();\n toggleIME();\n });\n\n // Keyboard shortcut: Ctrl+J to toggle IME\n document.addEventListener('keydown', function(e) {\n if (e.ctrlKey && e.key === 'j') {\n e.preventDefault();\n toggleIME();\n }\n });\n\n // Auto-show on mobile devices\n if (isMobile) {\n setTimeout(function() {\n toggleIME(true);\n }, 1000);\n }\n\n console.log('[IME Helper] Loaded. ' + (isMobile ? 'Mobile mode.' : 'Press Ctrl+J or click keyboard button to toggle.'));\n})();\n</script>\n";
6
+ export declare const imeHelperScript = "\n<style>\n#ttyd-ime-container {\n position: fixed;\n bottom: 0;\n left: 0;\n right: 0;\n background: #1e1e1e;\n border-top: 2px solid #007acc;\n padding: 8px;\n z-index: 10000;\n font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, sans-serif;\n box-shadow: 0 -2px 10px rgba(0,0,0,0.3);\n}\n\n#ttyd-ime-container.hidden {\n display: none;\n}\n\n#ttyd-ime-buttons {\n display: flex;\n gap: 6px;\n margin-bottom: 8px;\n flex-wrap: wrap;\n}\n\n#ttyd-ime-buttons button {\n background: #3a3a3a;\n border: 1px solid #555;\n border-radius: 6px;\n color: #fff;\n cursor: pointer;\n font-size: 13px;\n padding: 8px 12px;\n min-height: 40px;\n min-width: 44px;\n touch-action: manipulation;\n flex-shrink: 0;\n}\n\n#ttyd-ime-buttons button:hover, #ttyd-ime-buttons button:active {\n background: #4a4a4a;\n}\n\n#ttyd-ime-buttons button.active {\n background: #007acc;\n border-color: #005a9e;\n}\n\n#ttyd-ime-buttons button.modifier {\n background: #2d2d2d;\n font-weight: bold;\n}\n\n#ttyd-ime-buttons button.modifier.active {\n background: #d9534f;\n border-color: #c9302c;\n}\n\n#ttyd-ime-send {\n background: #007acc !important;\n border-color: #005a9e !important;\n font-weight: bold;\n}\n\n#ttyd-ime-send:hover, #ttyd-ime-send:active {\n background: #005a9e !important;\n}\n\n#ttyd-ime-run {\n background: #28a745 !important;\n border-color: #1e7e34 !important;\n font-weight: bold;\n}\n\n#ttyd-ime-run:hover, #ttyd-ime-run:active {\n background: #1e7e34 !important;\n}\n\n#ttyd-ime-auto.active {\n background: #f0ad4e !important;\n border-color: #eea236 !important;\n color: #000;\n}\n\n#ttyd-ime-input-row {\n display: flex;\n gap: 8px;\n align-items: flex-end;\n}\n\n#ttyd-ime-input {\n flex: 1;\n background: #2d2d2d;\n border: 1px solid #555;\n border-radius: 8px;\n color: #fff;\n font-family: monospace;\n font-size: 16px;\n padding: 12px;\n outline: none;\n resize: none;\n min-height: 44px;\n max-height: 120px;\n line-height: 1.4;\n}\n\n#ttyd-ime-input:focus {\n border-color: #007acc;\n}\n\n#ttyd-ime-input::placeholder {\n color: #888;\n}\n\n#ttyd-ime-toggle {\n position: fixed;\n bottom: 16px;\n right: 16px;\n background: #007acc;\n border: 2px solid #005a9e;\n border-radius: 50%;\n color: #fff;\n cursor: pointer;\n font-size: 20px;\n width: 56px;\n height: 56px;\n z-index: 10001;\n touch-action: manipulation;\n box-shadow: 0 2px 8px rgba(0,0,0,0.3);\n display: flex;\n align-items: center;\n justify-content: center;\n}\n\n#ttyd-ime-toggle:hover, #ttyd-ime-toggle:active {\n background: #005a9e;\n transform: scale(1.05);\n}\n\n#ttyd-ime-container.hidden ~ #ttyd-ime-toggle {\n bottom: 16px;\n}\n\n/* Adjust terminal height when IME bar is visible */\nbody:has(#ttyd-ime-container:not(.hidden)) .xterm {\n height: calc(100vh - 140px) !important;\n}\n\n/* Mobile optimizations */\n@media (max-width: 768px) {\n #ttyd-ime-container {\n padding: 6px;\n }\n\n #ttyd-ime-buttons {\n gap: 4px;\n margin-bottom: 6px;\n }\n\n #ttyd-ime-buttons button {\n font-size: 12px;\n padding: 6px 10px;\n min-height: 36px;\n min-width: 40px;\n }\n\n #ttyd-ime-input {\n font-size: 16px;\n padding: 10px;\n }\n\n #ttyd-ime-toggle {\n width: 64px;\n height: 64px;\n font-size: 24px;\n }\n\n body:has(#ttyd-ime-container:not(.hidden)) .xterm {\n height: calc(100vh - 130px) !important;\n }\n}\n</style>\n\n<div id=\"ttyd-ime-container\" class=\"hidden\">\n <div id=\"ttyd-ime-buttons\">\n <button id=\"ttyd-ime-ctrl\" class=\"modifier\">Ctrl</button>\n <button id=\"ttyd-ime-alt\" class=\"modifier\">Alt</button>\n <button id=\"ttyd-ime-shift\" class=\"modifier\">Shift</button>\n <button id=\"ttyd-ime-esc\">Esc</button>\n <button id=\"ttyd-ime-tab\">Tab</button>\n <button id=\"ttyd-ime-up\">\u2191</button>\n <button id=\"ttyd-ime-down\">\u2193</button>\n <button id=\"ttyd-ime-enter\">Enter</button>\n <button id=\"ttyd-ime-zoomout\">A-</button>\n <button id=\"ttyd-ime-zoomin\">A+</button>\n <button id=\"ttyd-ime-copy\">Copy</button>\n <button id=\"ttyd-ime-copyall\">All</button>\n <button id=\"ttyd-ime-send\">Send</button>\n <button id=\"ttyd-ime-run\">Run</button>\n <button id=\"ttyd-ime-auto\" class=\"modifier\">Auto</button>\n </div>\n <div id=\"ttyd-ime-input-row\">\n <textarea id=\"ttyd-ime-input\" rows=\"1\" placeholder=\"\u65E5\u672C\u8A9E\u5165\u529B (Enter: \u9001\u4FE1)\"></textarea>\n </div>\n</div>\n<button id=\"ttyd-ime-toggle\">\u2328</button>\n\n<script>\n(function() {\n const container = document.getElementById('ttyd-ime-container');\n const input = document.getElementById('ttyd-ime-input');\n const sendBtn = document.getElementById('ttyd-ime-send');\n const enterBtn = document.getElementById('ttyd-ime-enter');\n const zoomInBtn = document.getElementById('ttyd-ime-zoomin');\n const zoomOutBtn = document.getElementById('ttyd-ime-zoomout');\n const runBtn = document.getElementById('ttyd-ime-run');\n const toggleBtn = document.getElementById('ttyd-ime-toggle');\n const ctrlBtn = document.getElementById('ttyd-ime-ctrl');\n const altBtn = document.getElementById('ttyd-ime-alt');\n const shiftBtn = document.getElementById('ttyd-ime-shift');\n const escBtn = document.getElementById('ttyd-ime-esc');\n const tabBtn = document.getElementById('ttyd-ime-tab');\n const upBtn = document.getElementById('ttyd-ime-up');\n const downBtn = document.getElementById('ttyd-ime-down');\n const copyBtn = document.getElementById('ttyd-ime-copy');\n const copyAllBtn = document.getElementById('ttyd-ime-copyall');\n const autoBtn = document.getElementById('ttyd-ime-auto');\n\n let ws = null;\n let ctrlActive = false;\n let altActive = false;\n let shiftActive = false;\n let autoRunActive = false;\n\n // Detect mobile device\n const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);\n\n // Find the WebSocket connection\n function findWebSocket() {\n if (ws && ws.readyState === WebSocket.OPEN) return ws;\n\n if (window.socket && window.socket.readyState === WebSocket.OPEN) {\n ws = window.socket;\n return ws;\n }\n\n return null;\n }\n\n // Intercept WebSocket creation to capture the connection\n const OriginalWebSocket = window.WebSocket;\n window.WebSocket = function(url, protocols) {\n const socket = new OriginalWebSocket(url, protocols);\n if (url.includes('/ws')) {\n ws = socket;\n }\n return socket;\n };\n window.WebSocket.prototype = OriginalWebSocket.prototype;\n window.WebSocket.CONNECTING = OriginalWebSocket.CONNECTING;\n window.WebSocket.OPEN = OriginalWebSocket.OPEN;\n window.WebSocket.CLOSING = OriginalWebSocket.CLOSING;\n window.WebSocket.CLOSED = OriginalWebSocket.CLOSED;\n\n function sendText(text) {\n const socket = findWebSocket();\n if (!socket) {\n console.error('[IME Helper] WebSocket not found');\n return false;\n }\n\n // ttyd protocol: binary data with '0' (input command) as first byte\n const encoder = new TextEncoder();\n const textBytes = encoder.encode(text);\n const data = new Uint8Array(textBytes.length + 1);\n data[0] = '0'.charCodeAt(0); // Input command\n data.set(textBytes, 1);\n socket.send(data);\n return true;\n }\n\n function sendKey(key) {\n // Apply modifiers\n if (ctrlActive && key.length === 1) {\n // Ctrl+key: send as control character (A=1, B=2, ..., Z=26)\n const code = key.toUpperCase().charCodeAt(0) - 64;\n if (code > 0 && code < 32) {\n sendBytes([code]);\n }\n resetModifiers();\n } else if (altActive && key.length === 1) {\n // Alt+key: send ESC + key\n const keyCode = key.charCodeAt(0);\n sendBytes([0x1B, keyCode]);\n resetModifiers();\n } else {\n sendText(key);\n }\n }\n\n function resetModifiers() {\n ctrlActive = false;\n altActive = false;\n ctrlBtn.classList.remove('active');\n altBtn.classList.remove('active');\n }\n\n // Send raw bytes for special keys\n function sendBytes(bytes) {\n const socket = findWebSocket();\n if (!socket) {\n console.error('[IME Helper] WebSocket not found');\n return false;\n }\n const data = new Uint8Array(bytes.length + 1);\n data[0] = 0x30; // '0' = input command\n data.set(bytes, 1);\n socket.send(data);\n return true;\n }\n\n function sendEnter() {\n sendBytes([0x0D]); // CR\n }\n\n function sendEsc() {\n sendBytes([0x1B]); // ESC\n }\n\n function sendTab() {\n sendBytes([0x09]); // TAB\n }\n\n function sendUp() {\n sendBytes([0x1B, 0x5B, 0x41]); // ESC [ A\n }\n\n function sendDown() {\n sendBytes([0x1B, 0x5B, 0x42]); // ESC [ B\n }\n\n function fitTerminal() {\n if (window.fitAddon && typeof window.fitAddon.fit === 'function') {\n window.fitAddon.fit();\n console.log('[IME Helper] Terminal fitted via fitAddon');\n return;\n }\n\n if (window.term && window.term.fitAddon && typeof window.term.fitAddon.fit === 'function') {\n window.term.fitAddon.fit();\n console.log('[IME Helper] Terminal fitted via term.fitAddon');\n return;\n }\n\n window.dispatchEvent(new Event('resize'));\n console.log('[IME Helper] Dispatched resize event');\n }\n\n function findTerminal() {\n if (window.term) return window.term;\n const termEl = document.querySelector('.xterm');\n if (termEl && termEl._core) return termEl._core;\n return null;\n }\n\n function zoomTerminal(delta) {\n const term = findTerminal();\n\n if (term && term.options) {\n const currentSize = term.options.fontSize || 14;\n const newSize = Math.max(8, Math.min(32, currentSize + delta));\n term.options.fontSize = newSize;\n console.log('[IME Helper] Font size changed to ' + newSize);\n fitTerminal();\n } else {\n console.log('[IME Helper] Terminal not found for zoom');\n }\n }\n\n function copySelection() {\n const term = findTerminal();\n if (!term) {\n console.log('[IME Helper] Terminal not found for copy');\n return;\n }\n const selection = term.getSelection();\n if (selection) {\n navigator.clipboard.writeText(selection).then(function() {\n console.log('[IME Helper] Copied selection to clipboard');\n }).catch(function(err) {\n console.error('[IME Helper] Failed to copy:', err);\n });\n } else {\n console.log('[IME Helper] No text selected');\n }\n }\n\n function copyAll() {\n const term = findTerminal();\n if (!term || !term.buffer || !term.buffer.active) {\n console.log('[IME Helper] Terminal buffer not found');\n return;\n }\n const buffer = term.buffer.active;\n const lines = [];\n for (let i = 0; i < buffer.length; i++) {\n const line = buffer.getLine(i);\n if (line) {\n lines.push(line.translateToString(true));\n }\n }\n const text = lines.join('\\n').trimEnd();\n navigator.clipboard.writeText(text).then(function() {\n console.log('[IME Helper] Copied all text to clipboard');\n }).catch(function(err) {\n console.error('[IME Helper] Failed to copy:', err);\n });\n }\n\n function submitInput() {\n const text = input.value;\n if (!text) return;\n\n if (sendText(text)) {\n input.value = '';\n adjustTextareaHeight();\n // Auto mode: send Enter after 1 second\n if (autoRunActive) {\n setTimeout(function() {\n sendEnter();\n }, 1000);\n }\n }\n }\n\n function runInput() {\n const text = input.value;\n if (!text) return;\n\n if (sendText(text)) {\n input.value = '';\n adjustTextareaHeight();\n // Wait 1 second then send Enter\n setTimeout(function() {\n sendEnter();\n }, 1000);\n }\n }\n\n function toggleIME(show) {\n if (typeof show === 'boolean') {\n container.classList.toggle('hidden', !show);\n } else {\n container.classList.toggle('hidden');\n }\n\n if (!container.classList.contains('hidden')) {\n input.focus();\n // Fit terminal after showing IME bar\n setTimeout(fitTerminal, 100);\n } else {\n const terminal = document.querySelector('.xterm-helper-textarea');\n if (terminal) terminal.focus();\n setTimeout(fitTerminal, 100);\n }\n }\n\n function adjustTextareaHeight() {\n input.style.height = 'auto';\n input.style.height = Math.min(input.scrollHeight, 120) + 'px';\n }\n\n // Event listeners\n sendBtn.addEventListener('click', function(e) {\n e.preventDefault();\n submitInput();\n });\n\n enterBtn.addEventListener('click', function(e) {\n e.preventDefault();\n sendEnter();\n });\n\n runBtn.addEventListener('click', function(e) {\n e.preventDefault();\n runInput();\n });\n\n zoomInBtn.addEventListener('click', function(e) {\n e.preventDefault();\n zoomTerminal(2);\n });\n\n zoomOutBtn.addEventListener('click', function(e) {\n e.preventDefault();\n zoomTerminal(-2);\n });\n\n ctrlBtn.addEventListener('click', function(e) {\n e.preventDefault();\n ctrlActive = !ctrlActive;\n ctrlBtn.classList.toggle('active', ctrlActive);\n if (ctrlActive) {\n altActive = false;\n altBtn.classList.remove('active');\n }\n });\n\n altBtn.addEventListener('click', function(e) {\n e.preventDefault();\n altActive = !altActive;\n altBtn.classList.toggle('active', altActive);\n if (altActive) {\n ctrlActive = false;\n ctrlBtn.classList.remove('active');\n }\n });\n\n shiftBtn.addEventListener('click', function(e) {\n e.preventDefault();\n shiftActive = !shiftActive;\n shiftBtn.classList.toggle('active', shiftActive);\n });\n\n autoBtn.addEventListener('click', function(e) {\n e.preventDefault();\n autoRunActive = !autoRunActive;\n autoBtn.classList.toggle('active', autoRunActive);\n });\n\n escBtn.addEventListener('click', function(e) {\n e.preventDefault();\n sendEsc();\n });\n\n tabBtn.addEventListener('click', function(e) {\n e.preventDefault();\n sendTab();\n });\n\n upBtn.addEventListener('click', function(e) {\n e.preventDefault();\n sendUp();\n });\n\n downBtn.addEventListener('click', function(e) {\n e.preventDefault();\n sendDown();\n });\n\n copyBtn.addEventListener('click', function(e) {\n e.preventDefault();\n copySelection();\n });\n\n copyAllBtn.addEventListener('click', function(e) {\n e.preventDefault();\n copyAll();\n });\n\n input.addEventListener('input', adjustTextareaHeight);\n\n input.addEventListener('keydown', function(e) {\n if (e.key === 'Enter' && !e.shiftKey && !e.isComposing) {\n e.preventDefault();\n submitInput();\n } else if (e.key === 'Escape') {\n e.preventDefault();\n toggleIME(false);\n }\n });\n\n toggleBtn.addEventListener('click', function(e) {\n e.preventDefault();\n toggleIME();\n });\n\n // Keyboard shortcut: Ctrl+J to toggle IME\n document.addEventListener('keydown', function(e) {\n if (e.ctrlKey && e.key === 'j') {\n e.preventDefault();\n toggleIME();\n }\n });\n\n // Inject shiftKey into mouse events when Shift button is active\n // This allows text selection to bypass tmux mouse mode\n ['mousedown', 'mousemove', 'mouseup'].forEach(function(eventType) {\n document.addEventListener(eventType, function(e) {\n // Don't interfere with IME helper buttons\n if (e.target.closest('#ttyd-ime-container') || e.target.closest('#ttyd-ime-toggle')) {\n return;\n }\n if (shiftActive && !e.shiftKey) {\n const newEvent = new MouseEvent(e.type, {\n bubbles: e.bubbles,\n cancelable: e.cancelable,\n view: e.view,\n detail: e.detail,\n screenX: e.screenX,\n screenY: e.screenY,\n clientX: e.clientX,\n clientY: e.clientY,\n ctrlKey: e.ctrlKey,\n altKey: e.altKey,\n shiftKey: true,\n metaKey: e.metaKey,\n button: e.button,\n buttons: e.buttons,\n relatedTarget: e.relatedTarget\n });\n e.stopImmediatePropagation();\n e.preventDefault();\n e.target.dispatchEvent(newEvent);\n }\n }, true);\n });\n\n // Convert touch events to mouse events with shiftKey when Shift is active\n // This enables text selection on mobile devices\n let touchStartPos = null;\n\n function dispatchMouseEvent(type, touch, shiftKey) {\n const mouseEvent = new MouseEvent(type, {\n bubbles: true,\n cancelable: true,\n view: window,\n detail: 1,\n screenX: touch.screenX,\n screenY: touch.screenY,\n clientX: touch.clientX,\n clientY: touch.clientY,\n ctrlKey: false,\n altKey: false,\n shiftKey: shiftKey,\n metaKey: false,\n button: 0,\n buttons: type === 'mouseup' ? 0 : 1,\n relatedTarget: null\n });\n touch.target.dispatchEvent(mouseEvent);\n }\n\n let shiftTouchActive = false; // Track if we're in Shift+touch selection mode\n\n document.addEventListener('touchstart', function(e) {\n // Don't interfere with IME helper buttons\n if (e.target.closest('#ttyd-ime-container') || e.target.closest('#ttyd-ime-toggle')) {\n return;\n }\n // Single finger touch with Shift active -> convert to mouse event for selection\n if (e.touches.length === 1 && shiftActive) {\n const touch = e.touches[0];\n touchStartPos = { x: touch.clientX, y: touch.clientY };\n shiftTouchActive = true;\n e.preventDefault();\n dispatchMouseEvent('mousedown', touch, true);\n }\n // 2nd finger added -> cancel Shift selection mode, allow pinch\n else if (e.touches.length === 2 && shiftTouchActive) {\n dispatchMouseEvent('mouseup', e.touches[0], true);\n shiftTouchActive = false;\n touchStartPos = null;\n // Don't preventDefault - let pinch handlers take over\n }\n // Track non-Shift single touch for hint\n else if (e.touches.length === 1 && !shiftActive) {\n const touch = e.touches[0];\n touchStartPos = { x: touch.clientX, y: touch.clientY };\n }\n }, { passive: false, capture: true });\n\n document.addEventListener('touchmove', function(e) {\n // Only handle single-finger moves when in Shift selection mode\n if (e.touches.length === 1 && shiftTouchActive) {\n e.preventDefault();\n dispatchMouseEvent('mousemove', e.touches[0], true);\n }\n // Don't interfere with 2-finger gestures (pinch)\n }, { passive: false, capture: true });\n\n document.addEventListener('touchend', function(e) {\n // Shift selection mode ending\n if (shiftTouchActive && e.touches.length === 0) {\n const touch = e.changedTouches[0];\n dispatchMouseEvent('mouseup', touch, true);\n shiftTouchActive = false;\n touchStartPos = null;\n }\n }, { passive: true, capture: true });\n\n // Pinch-to-zoom for font size (when Ctrl or Shift is active)\n let pinchStartDistance = 0;\n let pinchStartFontSize = 14;\n\n function getTouchDistance(touches) {\n const dx = touches[0].clientX - touches[1].clientX;\n const dy = touches[0].clientY - touches[1].clientY;\n return Math.sqrt(dx * dx + dy * dy);\n }\n\n document.addEventListener('touchstart', function(e) {\n if (e.touches.length === 2 && (ctrlActive || shiftActive)) {\n pinchStartDistance = getTouchDistance(e.touches);\n const term = findTerminal();\n pinchStartFontSize = (term && term.options) ? (term.options.fontSize || 14) : 14;\n }\n }, { passive: true });\n\n document.addEventListener('touchmove', function(e) {\n if (e.touches.length === 2 && (ctrlActive || shiftActive) && pinchStartDistance > 0) {\n e.preventDefault(); // Suppress browser zoom\n const currentDistance = getTouchDistance(e.touches);\n const scale = currentDistance / pinchStartDistance;\n const newSize = Math.round(pinchStartFontSize * scale);\n const clampedSize = Math.max(8, Math.min(32, newSize));\n\n const term = findTerminal();\n if (term && term.options && term.options.fontSize !== clampedSize) {\n term.options.fontSize = clampedSize;\n fitTerminal();\n }\n }\n }, { passive: false });\n\n document.addEventListener('touchend', function(e) {\n if (e.touches.length < 2) {\n pinchStartDistance = 0;\n }\n }, { passive: true });\n\n // ========== PC: Ctrl+Wheel / Trackpad Pinch ==========\n document.addEventListener('wheel', function(e) {\n // ctrlKey = trackpad pinch (Mac) or Ctrl+scroll (PC)\n if (e.ctrlKey) {\n e.preventDefault(); // Suppress browser zoom\n\n // deltaY > 0: zoom out, deltaY < 0: zoom in\n const delta = e.deltaY > 0 ? -2 : 2;\n zoomTerminal(delta);\n }\n }, { passive: false });\n\n // Double-tap to send Enter (for reconnecting)\n let lastTapTime = 0;\n const DOUBLE_TAP_DELAY = 300; // 300ms \u4EE5\u5185\u306E2\u56DE\u30BF\u30C3\u30D7\n\n document.addEventListener('touchend', function(e) {\n // IME \u30D8\u30EB\u30D1\u30FC\u8981\u7D20\u306F\u9664\u5916\n if (e.target.closest('#ttyd-ime-container') || e.target.closest('#ttyd-ime-toggle')) {\n return;\n }\n // \u30B7\u30F3\u30B0\u30EB\u30BF\u30C3\u30C1\u306E\u307F\n if (e.changedTouches.length !== 1) return;\n\n const now = Date.now();\n if (now - lastTapTime < DOUBLE_TAP_DELAY) {\n // \u30C0\u30D6\u30EB\u30BF\u30C3\u30D7\u691C\u51FA \u2192 Enter \u9001\u4FE1\n sendEnter();\n lastTapTime = 0; // \u30EA\u30BB\u30C3\u30C8\n } else {\n lastTapTime = now;\n }\n }, { passive: true });\n\n // Auto-show on mobile devices\n if (isMobile) {\n setTimeout(function() {\n toggleIME(true);\n }, 1000);\n }\n\n // Auto-reload when tab becomes visible if WebSocket is disconnected\n document.addEventListener('visibilitychange', function() {\n if (!document.hidden) {\n const socket = findWebSocket();\n if (!socket || socket.readyState !== WebSocket.OPEN) {\n console.log('[IME Helper] Connection lost, reloading...');\n location.reload();\n }\n }\n });\n\n console.log('[IME Helper] Loaded. ' + (isMobile ? 'Mobile mode.' : 'Press Ctrl+J or click keyboard button to toggle.'));\n})();\n</script>\n";
7
7
  export declare function injectImeHelper(html: string): string;
8
8
  //# sourceMappingURL=ime-helper.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"ime-helper.d.ts","sourceRoot":"","sources":["../../src/daemon/ime-helper.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,eAAO,MAAM,eAAe,qpZAigB3B,CAAC;AAEF,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAGpD"}
1
+ {"version":3,"file":"ime-helper.d.ts","sourceRoot":"","sources":["../../src/daemon/ime-helper.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,eAAO,MAAM,eAAe,09rBAmxB3B,CAAC;AAEF,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAGpD"}
@@ -82,6 +82,12 @@ export const imeHelperScript = `
82
82
  background: #1e7e34 !important;
83
83
  }
84
84
 
85
+ #ttyd-ime-auto.active {
86
+ background: #f0ad4e !important;
87
+ border-color: #eea236 !important;
88
+ color: #000;
89
+ }
90
+
85
91
  #ttyd-ime-input-row {
86
92
  display: flex;
87
93
  gap: 8px;
@@ -185,6 +191,7 @@ body:has(#ttyd-ime-container:not(.hidden)) .xterm {
185
191
  <div id="ttyd-ime-buttons">
186
192
  <button id="ttyd-ime-ctrl" class="modifier">Ctrl</button>
187
193
  <button id="ttyd-ime-alt" class="modifier">Alt</button>
194
+ <button id="ttyd-ime-shift" class="modifier">Shift</button>
188
195
  <button id="ttyd-ime-esc">Esc</button>
189
196
  <button id="ttyd-ime-tab">Tab</button>
190
197
  <button id="ttyd-ime-up">↑</button>
@@ -192,8 +199,11 @@ body:has(#ttyd-ime-container:not(.hidden)) .xterm {
192
199
  <button id="ttyd-ime-enter">Enter</button>
193
200
  <button id="ttyd-ime-zoomout">A-</button>
194
201
  <button id="ttyd-ime-zoomin">A+</button>
202
+ <button id="ttyd-ime-copy">Copy</button>
203
+ <button id="ttyd-ime-copyall">All</button>
195
204
  <button id="ttyd-ime-send">Send</button>
196
205
  <button id="ttyd-ime-run">Run</button>
206
+ <button id="ttyd-ime-auto" class="modifier">Auto</button>
197
207
  </div>
198
208
  <div id="ttyd-ime-input-row">
199
209
  <textarea id="ttyd-ime-input" rows="1" placeholder="日本語入力 (Enter: 送信)"></textarea>
@@ -213,14 +223,20 @@ body:has(#ttyd-ime-container:not(.hidden)) .xterm {
213
223
  const toggleBtn = document.getElementById('ttyd-ime-toggle');
214
224
  const ctrlBtn = document.getElementById('ttyd-ime-ctrl');
215
225
  const altBtn = document.getElementById('ttyd-ime-alt');
226
+ const shiftBtn = document.getElementById('ttyd-ime-shift');
216
227
  const escBtn = document.getElementById('ttyd-ime-esc');
217
228
  const tabBtn = document.getElementById('ttyd-ime-tab');
218
229
  const upBtn = document.getElementById('ttyd-ime-up');
219
230
  const downBtn = document.getElementById('ttyd-ime-down');
231
+ const copyBtn = document.getElementById('ttyd-ime-copy');
232
+ const copyAllBtn = document.getElementById('ttyd-ime-copyall');
233
+ const autoBtn = document.getElementById('ttyd-ime-auto');
220
234
 
221
235
  let ws = null;
222
236
  let ctrlActive = false;
223
237
  let altActive = false;
238
+ let shiftActive = false;
239
+ let autoRunActive = false;
224
240
 
225
241
  // Detect mobile device
226
242
  const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
@@ -346,16 +362,15 @@ body:has(#ttyd-ime-container:not(.hidden)) .xterm {
346
362
  console.log('[IME Helper] Dispatched resize event');
347
363
  }
348
364
 
365
+ function findTerminal() {
366
+ if (window.term) return window.term;
367
+ const termEl = document.querySelector('.xterm');
368
+ if (termEl && termEl._core) return termEl._core;
369
+ return null;
370
+ }
371
+
349
372
  function zoomTerminal(delta) {
350
- // Try to find the terminal instance
351
- let term = window.term;
352
- if (!term) {
353
- // ttyd might store it differently
354
- const termEl = document.querySelector('.xterm');
355
- if (termEl && termEl._core) {
356
- term = termEl._core;
357
- }
358
- }
373
+ const term = findTerminal();
359
374
 
360
375
  if (term && term.options) {
361
376
  const currentSize = term.options.fontSize || 14;
@@ -368,6 +383,46 @@ body:has(#ttyd-ime-container:not(.hidden)) .xterm {
368
383
  }
369
384
  }
370
385
 
386
+ function copySelection() {
387
+ const term = findTerminal();
388
+ if (!term) {
389
+ console.log('[IME Helper] Terminal not found for copy');
390
+ return;
391
+ }
392
+ const selection = term.getSelection();
393
+ if (selection) {
394
+ navigator.clipboard.writeText(selection).then(function() {
395
+ console.log('[IME Helper] Copied selection to clipboard');
396
+ }).catch(function(err) {
397
+ console.error('[IME Helper] Failed to copy:', err);
398
+ });
399
+ } else {
400
+ console.log('[IME Helper] No text selected');
401
+ }
402
+ }
403
+
404
+ function copyAll() {
405
+ const term = findTerminal();
406
+ if (!term || !term.buffer || !term.buffer.active) {
407
+ console.log('[IME Helper] Terminal buffer not found');
408
+ return;
409
+ }
410
+ const buffer = term.buffer.active;
411
+ const lines = [];
412
+ for (let i = 0; i < buffer.length; i++) {
413
+ const line = buffer.getLine(i);
414
+ if (line) {
415
+ lines.push(line.translateToString(true));
416
+ }
417
+ }
418
+ const text = lines.join('\\n').trimEnd();
419
+ navigator.clipboard.writeText(text).then(function() {
420
+ console.log('[IME Helper] Copied all text to clipboard');
421
+ }).catch(function(err) {
422
+ console.error('[IME Helper] Failed to copy:', err);
423
+ });
424
+ }
425
+
371
426
  function submitInput() {
372
427
  const text = input.value;
373
428
  if (!text) return;
@@ -375,6 +430,12 @@ body:has(#ttyd-ime-container:not(.hidden)) .xterm {
375
430
  if (sendText(text)) {
376
431
  input.value = '';
377
432
  adjustTextareaHeight();
433
+ // Auto mode: send Enter after 1 second
434
+ if (autoRunActive) {
435
+ setTimeout(function() {
436
+ sendEnter();
437
+ }, 1000);
438
+ }
378
439
  }
379
440
  }
380
441
 
@@ -461,6 +522,18 @@ body:has(#ttyd-ime-container:not(.hidden)) .xterm {
461
522
  }
462
523
  });
463
524
 
525
+ shiftBtn.addEventListener('click', function(e) {
526
+ e.preventDefault();
527
+ shiftActive = !shiftActive;
528
+ shiftBtn.classList.toggle('active', shiftActive);
529
+ });
530
+
531
+ autoBtn.addEventListener('click', function(e) {
532
+ e.preventDefault();
533
+ autoRunActive = !autoRunActive;
534
+ autoBtn.classList.toggle('active', autoRunActive);
535
+ });
536
+
464
537
  escBtn.addEventListener('click', function(e) {
465
538
  e.preventDefault();
466
539
  sendEsc();
@@ -481,6 +554,16 @@ body:has(#ttyd-ime-container:not(.hidden)) .xterm {
481
554
  sendDown();
482
555
  });
483
556
 
557
+ copyBtn.addEventListener('click', function(e) {
558
+ e.preventDefault();
559
+ copySelection();
560
+ });
561
+
562
+ copyAllBtn.addEventListener('click', function(e) {
563
+ e.preventDefault();
564
+ copyAll();
565
+ });
566
+
484
567
  input.addEventListener('input', adjustTextareaHeight);
485
568
 
486
569
  input.addEventListener('keydown', function(e) {
@@ -506,6 +589,186 @@ body:has(#ttyd-ime-container:not(.hidden)) .xterm {
506
589
  }
507
590
  });
508
591
 
592
+ // Inject shiftKey into mouse events when Shift button is active
593
+ // This allows text selection to bypass tmux mouse mode
594
+ ['mousedown', 'mousemove', 'mouseup'].forEach(function(eventType) {
595
+ document.addEventListener(eventType, function(e) {
596
+ // Don't interfere with IME helper buttons
597
+ if (e.target.closest('#ttyd-ime-container') || e.target.closest('#ttyd-ime-toggle')) {
598
+ return;
599
+ }
600
+ if (shiftActive && !e.shiftKey) {
601
+ const newEvent = new MouseEvent(e.type, {
602
+ bubbles: e.bubbles,
603
+ cancelable: e.cancelable,
604
+ view: e.view,
605
+ detail: e.detail,
606
+ screenX: e.screenX,
607
+ screenY: e.screenY,
608
+ clientX: e.clientX,
609
+ clientY: e.clientY,
610
+ ctrlKey: e.ctrlKey,
611
+ altKey: e.altKey,
612
+ shiftKey: true,
613
+ metaKey: e.metaKey,
614
+ button: e.button,
615
+ buttons: e.buttons,
616
+ relatedTarget: e.relatedTarget
617
+ });
618
+ e.stopImmediatePropagation();
619
+ e.preventDefault();
620
+ e.target.dispatchEvent(newEvent);
621
+ }
622
+ }, true);
623
+ });
624
+
625
+ // Convert touch events to mouse events with shiftKey when Shift is active
626
+ // This enables text selection on mobile devices
627
+ let touchStartPos = null;
628
+
629
+ function dispatchMouseEvent(type, touch, shiftKey) {
630
+ const mouseEvent = new MouseEvent(type, {
631
+ bubbles: true,
632
+ cancelable: true,
633
+ view: window,
634
+ detail: 1,
635
+ screenX: touch.screenX,
636
+ screenY: touch.screenY,
637
+ clientX: touch.clientX,
638
+ clientY: touch.clientY,
639
+ ctrlKey: false,
640
+ altKey: false,
641
+ shiftKey: shiftKey,
642
+ metaKey: false,
643
+ button: 0,
644
+ buttons: type === 'mouseup' ? 0 : 1,
645
+ relatedTarget: null
646
+ });
647
+ touch.target.dispatchEvent(mouseEvent);
648
+ }
649
+
650
+ let shiftTouchActive = false; // Track if we're in Shift+touch selection mode
651
+
652
+ document.addEventListener('touchstart', function(e) {
653
+ // Don't interfere with IME helper buttons
654
+ if (e.target.closest('#ttyd-ime-container') || e.target.closest('#ttyd-ime-toggle')) {
655
+ return;
656
+ }
657
+ // Single finger touch with Shift active -> convert to mouse event for selection
658
+ if (e.touches.length === 1 && shiftActive) {
659
+ const touch = e.touches[0];
660
+ touchStartPos = { x: touch.clientX, y: touch.clientY };
661
+ shiftTouchActive = true;
662
+ e.preventDefault();
663
+ dispatchMouseEvent('mousedown', touch, true);
664
+ }
665
+ // 2nd finger added -> cancel Shift selection mode, allow pinch
666
+ else if (e.touches.length === 2 && shiftTouchActive) {
667
+ dispatchMouseEvent('mouseup', e.touches[0], true);
668
+ shiftTouchActive = false;
669
+ touchStartPos = null;
670
+ // Don't preventDefault - let pinch handlers take over
671
+ }
672
+ // Track non-Shift single touch for hint
673
+ else if (e.touches.length === 1 && !shiftActive) {
674
+ const touch = e.touches[0];
675
+ touchStartPos = { x: touch.clientX, y: touch.clientY };
676
+ }
677
+ }, { passive: false, capture: true });
678
+
679
+ document.addEventListener('touchmove', function(e) {
680
+ // Only handle single-finger moves when in Shift selection mode
681
+ if (e.touches.length === 1 && shiftTouchActive) {
682
+ e.preventDefault();
683
+ dispatchMouseEvent('mousemove', e.touches[0], true);
684
+ }
685
+ // Don't interfere with 2-finger gestures (pinch)
686
+ }, { passive: false, capture: true });
687
+
688
+ document.addEventListener('touchend', function(e) {
689
+ // Shift selection mode ending
690
+ if (shiftTouchActive && e.touches.length === 0) {
691
+ const touch = e.changedTouches[0];
692
+ dispatchMouseEvent('mouseup', touch, true);
693
+ shiftTouchActive = false;
694
+ touchStartPos = null;
695
+ }
696
+ }, { passive: true, capture: true });
697
+
698
+ // Pinch-to-zoom for font size (when Ctrl or Shift is active)
699
+ let pinchStartDistance = 0;
700
+ let pinchStartFontSize = 14;
701
+
702
+ function getTouchDistance(touches) {
703
+ const dx = touches[0].clientX - touches[1].clientX;
704
+ const dy = touches[0].clientY - touches[1].clientY;
705
+ return Math.sqrt(dx * dx + dy * dy);
706
+ }
707
+
708
+ document.addEventListener('touchstart', function(e) {
709
+ if (e.touches.length === 2 && (ctrlActive || shiftActive)) {
710
+ pinchStartDistance = getTouchDistance(e.touches);
711
+ const term = findTerminal();
712
+ pinchStartFontSize = (term && term.options) ? (term.options.fontSize || 14) : 14;
713
+ }
714
+ }, { passive: true });
715
+
716
+ document.addEventListener('touchmove', function(e) {
717
+ if (e.touches.length === 2 && (ctrlActive || shiftActive) && pinchStartDistance > 0) {
718
+ e.preventDefault(); // Suppress browser zoom
719
+ const currentDistance = getTouchDistance(e.touches);
720
+ const scale = currentDistance / pinchStartDistance;
721
+ const newSize = Math.round(pinchStartFontSize * scale);
722
+ const clampedSize = Math.max(8, Math.min(32, newSize));
723
+
724
+ const term = findTerminal();
725
+ if (term && term.options && term.options.fontSize !== clampedSize) {
726
+ term.options.fontSize = clampedSize;
727
+ fitTerminal();
728
+ }
729
+ }
730
+ }, { passive: false });
731
+
732
+ document.addEventListener('touchend', function(e) {
733
+ if (e.touches.length < 2) {
734
+ pinchStartDistance = 0;
735
+ }
736
+ }, { passive: true });
737
+
738
+ // ========== PC: Ctrl+Wheel / Trackpad Pinch ==========
739
+ document.addEventListener('wheel', function(e) {
740
+ // ctrlKey = trackpad pinch (Mac) or Ctrl+scroll (PC)
741
+ if (e.ctrlKey) {
742
+ e.preventDefault(); // Suppress browser zoom
743
+
744
+ // deltaY > 0: zoom out, deltaY < 0: zoom in
745
+ const delta = e.deltaY > 0 ? -2 : 2;
746
+ zoomTerminal(delta);
747
+ }
748
+ }, { passive: false });
749
+
750
+ // Double-tap to send Enter (for reconnecting)
751
+ let lastTapTime = 0;
752
+ const DOUBLE_TAP_DELAY = 300; // 300ms 以内の2回タップ
753
+
754
+ document.addEventListener('touchend', function(e) {
755
+ // IME ヘルパー要素は除外
756
+ if (e.target.closest('#ttyd-ime-container') || e.target.closest('#ttyd-ime-toggle')) {
757
+ return;
758
+ }
759
+ // シングルタッチのみ
760
+ if (e.changedTouches.length !== 1) return;
761
+
762
+ const now = Date.now();
763
+ if (now - lastTapTime < DOUBLE_TAP_DELAY) {
764
+ // ダブルタップ検出 → Enter 送信
765
+ sendEnter();
766
+ lastTapTime = 0; // リセット
767
+ } else {
768
+ lastTapTime = now;
769
+ }
770
+ }, { passive: true });
771
+
509
772
  // Auto-show on mobile devices
510
773
  if (isMobile) {
511
774
  setTimeout(function() {
@@ -513,12 +776,23 @@ body:has(#ttyd-ime-container:not(.hidden)) .xterm {
513
776
  }, 1000);
514
777
  }
515
778
 
779
+ // Auto-reload when tab becomes visible if WebSocket is disconnected
780
+ document.addEventListener('visibilitychange', function() {
781
+ if (!document.hidden) {
782
+ const socket = findWebSocket();
783
+ if (!socket || socket.readyState !== WebSocket.OPEN) {
784
+ console.log('[IME Helper] Connection lost, reloading...');
785
+ location.reload();
786
+ }
787
+ }
788
+ });
789
+
516
790
  console.log('[IME Helper] Loaded. ' + (isMobile ? 'Mobile mode.' : 'Press Ctrl+J or click keyboard button to toggle.'));
517
791
  })();
518
792
  </script>
519
793
  `;
520
794
  export function injectImeHelper(html) {
521
795
  // Inject before </body>
522
- return html.replace('</body>', imeHelperScript + '</body>');
796
+ return html.replace('</body>', `${imeHelperScript}</body>`);
523
797
  }
524
798
  //# sourceMappingURL=ime-helper.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"ime-helper.js","sourceRoot":"","sources":["../../src/daemon/ime-helper.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,MAAM,CAAC,MAAM,eAAe,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAigB9B,CAAC;AAEF,MAAM,UAAU,eAAe,CAAC,IAAY;IAC1C,wBAAwB;IACxB,OAAO,IAAI,CAAC,OAAO,CAAC,SAAS,EAAE,eAAe,GAAG,SAAS,CAAC,CAAC;AAC9D,CAAC"}
1
+ {"version":3,"file":"ime-helper.js","sourceRoot":"","sources":["../../src/daemon/ime-helper.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,MAAM,CAAC,MAAM,eAAe,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAmxB9B,CAAC;AAEF,MAAM,UAAU,eAAe,CAAC,IAAY;IAC1C,wBAAwB;IACxB,OAAO,IAAI,CAAC,OAAO,CAAC,SAAS,EAAE,GAAG,eAAe,SAAS,CAAC,CAAC;AAC9D,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/daemon/index.ts"],"names":[],"mappings":"AAOA,MAAM,WAAW,aAAa;IAC5B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB;AAED,wBAAsB,WAAW,CAAC,OAAO,GAAE,aAAkB,GAAG,OAAO,CAAC,IAAI,CAAC,CAiG5E"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/daemon/index.ts"],"names":[],"mappings":"AAUA,MAAM,WAAW,aAAa;IAC5B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB;AA0CD,wBAAsB,WAAW,CAAC,OAAO,GAAE,aAAkB,GAAG,OAAO,CAAC,IAAI,CAAC,CA+K5E"}