ttyd-mux 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +105 -1
- package/dist/caddy/client.d.ts +3 -55
- package/dist/caddy/client.d.ts.map +1 -1
- package/dist/caddy/client.js +0 -73
- package/dist/caddy/client.js.map +1 -1
- package/dist/caddy/route-builder.d.ts +49 -0
- package/dist/caddy/route-builder.d.ts.map +1 -0
- package/dist/caddy/route-builder.js +175 -0
- package/dist/caddy/route-builder.js.map +1 -0
- package/dist/caddy/types.d.ts +27 -0
- package/dist/caddy/types.d.ts.map +1 -0
- package/dist/caddy/types.js +3 -0
- package/dist/caddy/types.js.map +1 -0
- package/dist/client/api-client.d.ts +26 -0
- package/dist/client/api-client.d.ts.map +1 -0
- package/dist/client/api-client.js +62 -0
- package/dist/client/api-client.js.map +1 -0
- package/dist/client/daemon-client.d.ts +48 -0
- package/dist/client/daemon-client.d.ts.map +1 -0
- package/dist/client/daemon-client.js +205 -0
- package/dist/client/daemon-client.js.map +1 -0
- package/dist/client/index.d.ts +2 -10
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +4 -136
- package/dist/client/index.js.map +1 -1
- package/dist/commands/attach.js +3 -4
- package/dist/commands/attach.js.map +1 -1
- package/dist/commands/caddy.d.ts +2 -1
- package/dist/commands/caddy.d.ts.map +1 -1
- package/dist/commands/caddy.js +227 -75
- package/dist/commands/caddy.js.map +1 -1
- package/dist/commands/daemon.js.map +1 -1
- package/dist/commands/deploy.d.ts +7 -0
- package/dist/commands/deploy.d.ts.map +1 -0
- package/dist/commands/deploy.js +100 -0
- package/dist/commands/deploy.js.map +1 -0
- package/dist/commands/doctor.d.ts +8 -0
- package/dist/commands/doctor.d.ts.map +1 -0
- package/dist/commands/doctor.js +180 -0
- package/dist/commands/doctor.js.map +1 -0
- package/dist/commands/down.d.ts.map +1 -1
- package/dist/commands/down.js +11 -0
- package/dist/commands/down.js.map +1 -1
- package/dist/commands/reload.d.ts +14 -0
- package/dist/commands/reload.d.ts.map +1 -0
- package/dist/commands/reload.js +50 -0
- package/dist/commands/reload.js.map +1 -0
- package/dist/commands/shutdown.d.ts +2 -1
- package/dist/commands/shutdown.d.ts.map +1 -1
- package/dist/commands/shutdown.js +8 -2
- package/dist/commands/shutdown.js.map +1 -1
- package/dist/commands/start.d.ts.map +1 -1
- package/dist/commands/start.js +16 -3
- package/dist/commands/start.js.map +1 -1
- package/dist/commands/status.js.map +1 -1
- package/dist/commands/stop.js.map +1 -1
- package/dist/commands/up.js.map +1 -1
- package/dist/config/config.d.ts.map +1 -1
- package/dist/config/config.js +9 -2
- package/dist/config/config.js.map +1 -1
- package/dist/config/index.d.ts +3 -3
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/index.js +6 -3
- package/dist/config/index.js.map +1 -1
- package/dist/config/state-store.d.ts +27 -0
- package/dist/config/state-store.d.ts.map +1 -0
- package/dist/config/state-store.js +55 -0
- package/dist/config/state-store.js.map +1 -0
- package/dist/config/state.d.ts +6 -0
- package/dist/config/state.d.ts.map +1 -1
- package/dist/config/state.js +49 -14
- package/dist/config/state.js.map +1 -1
- package/dist/config/types.d.ts +35 -0
- package/dist/config/types.d.ts.map +1 -1
- package/dist/config/types.js +23 -1
- package/dist/config/types.js.map +1 -1
- package/dist/daemon/api-handler.d.ts +5 -0
- package/dist/daemon/api-handler.d.ts.map +1 -0
- package/dist/daemon/api-handler.js +97 -0
- package/dist/daemon/api-handler.js.map +1 -0
- package/dist/daemon/config-manager.d.ts +43 -0
- package/dist/daemon/config-manager.d.ts.map +1 -0
- package/dist/daemon/config-manager.js +154 -0
- package/dist/daemon/config-manager.js.map +1 -0
- package/dist/daemon/http-proxy.d.ts +27 -0
- package/dist/daemon/http-proxy.d.ts.map +1 -0
- package/dist/daemon/http-proxy.js +110 -0
- package/dist/daemon/http-proxy.js.map +1 -0
- package/dist/daemon/ime-helper.d.ts +1 -1
- package/dist/daemon/ime-helper.d.ts.map +1 -1
- package/dist/daemon/ime-helper.js +284 -10
- package/dist/daemon/ime-helper.js.map +1 -1
- package/dist/daemon/index.d.ts.map +1 -1
- package/dist/daemon/index.js +134 -29
- package/dist/daemon/index.js.map +1 -1
- package/dist/daemon/portal-utils.d.ts +20 -0
- package/dist/daemon/portal-utils.d.ts.map +1 -0
- package/dist/daemon/portal-utils.js +109 -0
- package/dist/daemon/portal-utils.js.map +1 -0
- package/dist/daemon/portal.d.ts.map +1 -1
- package/dist/daemon/portal.js +20 -77
- package/dist/daemon/portal.js.map +1 -1
- package/dist/daemon/pwa.d.ts +52 -0
- package/dist/daemon/pwa.d.ts.map +1 -0
- package/dist/daemon/pwa.js +229 -0
- package/dist/daemon/pwa.js.map +1 -0
- package/dist/daemon/router.d.ts +15 -0
- package/dist/daemon/router.d.ts.map +1 -0
- package/dist/daemon/router.js +164 -0
- package/dist/daemon/router.js.map +1 -0
- package/dist/daemon/server.d.ts +15 -3
- package/dist/daemon/server.d.ts.map +1 -1
- package/dist/daemon/server.js +23 -271
- package/dist/daemon/server.js.map +1 -1
- package/dist/daemon/session-manager.d.ts +44 -10
- package/dist/daemon/session-manager.d.ts.map +1 -1
- package/dist/daemon/session-manager.js +125 -49
- package/dist/daemon/session-manager.js.map +1 -1
- package/dist/daemon/session-resolver.d.ts +1 -1
- package/dist/daemon/session-resolver.d.ts.map +1 -1
- package/dist/daemon/session-resolver.js.map +1 -1
- package/dist/daemon/toolbar/config.d.ts +13 -0
- package/dist/daemon/toolbar/config.d.ts.map +1 -0
- package/dist/daemon/toolbar/config.js +13 -0
- package/dist/daemon/toolbar/config.js.map +1 -0
- package/dist/daemon/toolbar/index.d.ts +43 -0
- package/dist/daemon/toolbar/index.d.ts.map +1 -0
- package/dist/daemon/toolbar/index.js +835 -0
- package/dist/daemon/toolbar/index.js.map +1 -0
- package/dist/daemon/toolbar/styles.d.ts +5 -0
- package/dist/daemon/toolbar/styles.d.ts.map +1 -0
- package/dist/daemon/toolbar/styles.js +278 -0
- package/dist/daemon/toolbar/styles.js.map +1 -0
- package/dist/daemon/toolbar/template.d.ts +6 -0
- package/dist/daemon/toolbar/template.d.ts.map +1 -0
- package/dist/daemon/toolbar/template.js +45 -0
- package/dist/daemon/toolbar/template.js.map +1 -0
- package/dist/daemon/ws-proxy.d.ts +17 -0
- package/dist/daemon/ws-proxy.d.ts.map +1 -0
- package/dist/daemon/ws-proxy.js +95 -0
- package/dist/daemon/ws-proxy.js.map +1 -0
- package/dist/deploy/caddyfile.d.ts +8 -0
- package/dist/deploy/caddyfile.d.ts.map +1 -0
- package/dist/deploy/caddyfile.js +62 -0
- package/dist/deploy/caddyfile.js.map +1 -0
- package/dist/deploy/deploy-script.d.ts +8 -0
- package/dist/deploy/deploy-script.d.ts.map +1 -0
- package/dist/deploy/deploy-script.js +72 -0
- package/dist/deploy/deploy-script.js.map +1 -0
- package/dist/deploy/static-portal.d.ts +3 -0
- package/dist/deploy/static-portal.d.ts.map +1 -0
- package/dist/deploy/static-portal.js +59 -0
- package/dist/deploy/static-portal.js.map +1 -0
- package/dist/index.js +38 -9
- package/dist/index.js.map +1 -1
- package/dist/test-setup.d.ts +19 -0
- package/dist/test-setup.d.ts.map +1 -0
- package/dist/test-setup.js +33 -0
- package/dist/test-setup.js.map +1 -0
- package/dist/tmux.d.ts +28 -1
- package/dist/tmux.d.ts.map +1 -1
- package/dist/tmux.js +37 -32
- package/dist/tmux.js.map +1 -1
- package/dist/ui.d.ts +2 -1
- package/dist/ui.d.ts.map +1 -1
- package/dist/ui.js +16 -9
- package/dist/ui.js.map +1 -1
- package/dist/utils/errors.d.ts +4 -0
- package/dist/utils/errors.d.ts.map +1 -1
- package/dist/utils/errors.js +9 -1
- package/dist/utils/errors.js.map +1 -1
- package/dist/utils/logger.d.ts +14 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +53 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/process-runner.d.ts +50 -0
- package/dist/utils/process-runner.d.ts.map +1 -0
- package/dist/utils/process-runner.js +73 -0
- package/dist/utils/process-runner.js.map +1 -0
- package/dist/utils/socket-client.d.ts +24 -0
- package/dist/utils/socket-client.d.ts.map +1 -0
- package/dist/utils/socket-client.js +30 -0
- package/dist/utils/socket-client.js.map +1 -0
- package/dist/utils/tmux-client.d.ts +57 -0
- package/dist/utils/tmux-client.d.ts.map +1 -0
- package/dist/utils/tmux-client.js +117 -0
- package/dist/utils/tmux-client.js.map +1 -0
- package/dist/version.d.ts +3 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +4 -0
- package/dist/version.js.map +1 -0
- package/package.json +6 -2
- package/dist/daemon/proxy.d.ts +0 -7
- package/dist/daemon/proxy.d.ts.map +0 -1
- package/dist/daemon/proxy.js +0 -17
- package/dist/daemon/proxy.js.map +0 -1
|
@@ -0,0 +1,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,
|
|
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
|
-
|
|
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
|
|
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
|
|
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":"
|
|
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"}
|