Spaces:
Paused
Paused
從 HelloSun/testzerogpu 同步:ESC、Ctrl-C×2、方向鍵,並新增 TAB 鍵(ESC 左側)
Browse files
app.py
CHANGED
|
@@ -79,6 +79,10 @@ html,body{height:100%;overflow:hidden;background:#1e1e1e;color:#ccc;font-family:
|
|
| 79 |
.action-btn:disabled{opacity:.45;cursor:not-allowed}
|
| 80 |
.action-btn{background:#2d2d2d;color:#969696;border:1px solid #3c3c3c;border-radius:4px;padding:4px 10px;cursor:pointer;font-size:13px;margin-bottom:4px;margin-left:4px;transition:all 0.2s;white-space:nowrap}
|
| 81 |
.action-btn:hover{color:#fff;border-color:#aaa;background:#3c3c3c}
|
|
|
|
|
|
|
|
|
|
|
|
|
| 82 |
#app-status{flex:0 1 320px;min-width:150px;max-width:360px;height:28px;margin:0 0 4px 2cm;padding:0 10px;display:flex;align-items:center;overflow:hidden;border:1px solid #3c3c3c;border-radius:4px;background:#202020;color:#aaa;font-size:12px;white-space:nowrap;text-overflow:ellipsis}#app-status[data-kind="success"]{color:#7fd18b;border-color:#35613d}#app-status[data-kind="error"]{color:#ff8f8f;border-color:#703c3c}#app-status[data-kind="warning"]{color:#f0cb75;border-color:#705f32}#app-status[data-kind="info"]{color:#9cc9ff;border-color:#365577}
|
| 83 |
#term-box{flex:1;overflow:hidden;padding:2px}
|
| 84 |
.tw{width:100%;height:100%;display:none}
|
|
@@ -126,6 +130,13 @@ body{display:flex;flex-direction:column}
|
|
| 126 |
<button id="add-btn" title="New Terminal (Ctrl+Shift+T)">+</button>
|
| 127 |
<button id="upload-btn" class="action-btn" title="Upload to current directory">📤 上傳</button>
|
| 128 |
<button id="download-btn" class="action-btn" title="Browse and download server files">📥 下載</button>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 129 |
<div id="app-status" role="status" aria-live="polite" title="應用程式訊息">就緒</div>
|
| 130 |
</div>
|
| 131 |
<dialog id="file-picker">
|
|
@@ -188,6 +199,35 @@ let tabs = [], activeId = null, cnt = 0;
|
|
| 188 |
|
| 189 |
function activeTab() { return tabs.find(t => t.id === activeId); }
|
| 190 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 191 |
function setAppStatus(message, kind = 'info') {
|
| 192 |
const clean = String(message).replace(/\x1b\[[0-?]*[ -/]*[@-~]/g, '').replace(/[\r\n]+/g, ' ').trim();
|
| 193 |
appStatus.textContent = clean || '就緒';
|
|
@@ -1534,6 +1574,38 @@ document.addEventListener('visibilitychange', () => {
|
|
| 1534 |
if (!document.hidden) flushPendingClipboard();
|
| 1535 |
});
|
| 1536 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1537 |
retry.onclick = () => { delay = 800; connect(); };
|
| 1538 |
screenEl.addEventListener('click', () => { if (rfb) rfb.focus(); });
|
| 1539 |
window.addEventListener('beforeunload', () => {
|
|
|
|
| 79 |
.action-btn:disabled{opacity:.45;cursor:not-allowed}
|
| 80 |
.action-btn{background:#2d2d2d;color:#969696;border:1px solid #3c3c3c;border-radius:4px;padding:4px 10px;cursor:pointer;font-size:13px;margin-bottom:4px;margin-left:4px;transition:all 0.2s;white-space:nowrap}
|
| 81 |
.action-btn:hover{color:#fff;border-color:#aaa;background:#3c3c3c}
|
| 82 |
+
.key-btn#tab-btn{font-family:monospace;letter-spacing:.5px}
|
| 83 |
+
.key-btn#esc-btn{font-family:monospace;letter-spacing:.5px}
|
| 84 |
+
.key-btn#ctrlc2-btn{font-family:monospace;font-size:12px}
|
| 85 |
+
.key-btn#key-up-btn,.key-btn#key-down-btn,.key-btn#key-left-btn,.key-btn#key-right-btn{min-width:34px;padding:4px 6px;font-size:15px;line-height:1}
|
| 86 |
#app-status{flex:0 1 320px;min-width:150px;max-width:360px;height:28px;margin:0 0 4px 2cm;padding:0 10px;display:flex;align-items:center;overflow:hidden;border:1px solid #3c3c3c;border-radius:4px;background:#202020;color:#aaa;font-size:12px;white-space:nowrap;text-overflow:ellipsis}#app-status[data-kind="success"]{color:#7fd18b;border-color:#35613d}#app-status[data-kind="error"]{color:#ff8f8f;border-color:#703c3c}#app-status[data-kind="warning"]{color:#f0cb75;border-color:#705f32}#app-status[data-kind="info"]{color:#9cc9ff;border-color:#365577}
|
| 87 |
#term-box{flex:1;overflow:hidden;padding:2px}
|
| 88 |
.tw{width:100%;height:100%;display:none}
|
|
|
|
| 130 |
<button id="add-btn" title="New Terminal (Ctrl+Shift+T)">+</button>
|
| 131 |
<button id="upload-btn" class="action-btn" title="Upload to current directory">📤 上傳</button>
|
| 132 |
<button id="download-btn" class="action-btn" title="Browse and download server files">📥 下載</button>
|
| 133 |
+
<button id="tab-btn" class="action-btn key-btn" title="送出 Tab(TAB)">TAB</button>
|
| 134 |
+
<button id="esc-btn" class="action-btn key-btn" title="送出 Escape(ESC)">ESC</button>
|
| 135 |
+
<button id="ctrlc2-btn" class="action-btn key-btn" title="送出 Ctrl-C 兩次(Ctrl-C×2)">Ctrl-C×2</button>
|
| 136 |
+
<button id="key-up-btn" class="action-btn key-btn" title="方向鍵:上">↑</button>
|
| 137 |
+
<button id="key-down-btn" class="action-btn key-btn" title="方向鍵:下">↓</button>
|
| 138 |
+
<button id="key-left-btn" class="action-btn key-btn" title="方向鍵:左">←</button>
|
| 139 |
+
<button id="key-right-btn" class="action-btn key-btn" title="方向鍵:右">→</button>
|
| 140 |
<div id="app-status" role="status" aria-live="polite" title="應用程式訊息">就緒</div>
|
| 141 |
</div>
|
| 142 |
<dialog id="file-picker">
|
|
|
|
| 199 |
|
| 200 |
function activeTab() { return tabs.find(t => t.id === activeId); }
|
| 201 |
|
| 202 |
+
// 上方工具列的 TAB、ESC、Ctrl-C×2 與方向鍵:桌面分頁送進 VNC,終端機分頁送 ANSI 序列。
|
| 203 |
+
function sendKeyToActive(name) {
|
| 204 |
+
const t = activeTab();
|
| 205 |
+
if (!t) { setAppStatus('請先選擇一個分頁', 'warning'); return; }
|
| 206 |
+
if (t.type === 'desktop') {
|
| 207 |
+
let fn = null;
|
| 208 |
+
try { fn = t.frame && t.frame.contentWindow && t.frame.contentWindow.webtermSendKey; } catch (e) { fn = null; }
|
| 209 |
+
if (typeof fn !== 'function') { setAppStatus('桌面尚未連線,無法送出按鍵', 'warning'); return; }
|
| 210 |
+
try {
|
| 211 |
+
fn(name);
|
| 212 |
+
const label = name === 'Tab' ? 'TAB' : name === 'Escape' ? 'ESC' : name === 'CtrlC2' ? 'Ctrl-C×2' : '方向鍵';
|
| 213 |
+
setAppStatus(`已送出 ${label} 到桌面`, 'info');
|
| 214 |
+
} catch (e) { setAppStatus('送出按鍵失敗', 'error'); }
|
| 215 |
+
return;
|
| 216 |
+
}
|
| 217 |
+
const seqMap = {Tab: '\t', Escape: '\x1b', CtrlC2: '\x03\x03', ArrowUp: '\x1b[A', ArrowDown: '\x1b[B', ArrowRight: '\x1b[C', ArrowLeft: '\x1b[D'};
|
| 218 |
+
const seq = seqMap[name];
|
| 219 |
+
if (!seq) return;
|
| 220 |
+
if (!t.ws || t.ws.readyState !== WebSocket.OPEN) { setAppStatus('終端機尚未連線', 'warning'); return; }
|
| 221 |
+
t.ws.send(JSON.stringify({type: 'input', data: seq}));
|
| 222 |
+
if (t.term) t.term.focus();
|
| 223 |
+
}
|
| 224 |
+
|
| 225 |
+
document.getElementById('tab-btn').onclick = () => sendKeyToActive('Tab');
|
| 226 |
+
document.getElementById('esc-btn').onclick = () => sendKeyToActive('Escape');
|
| 227 |
+
document.getElementById('ctrlc2-btn').onclick = () => sendKeyToActive('CtrlC2');
|
| 228 |
+
[['key-up-btn', 'ArrowUp'], ['key-down-btn', 'ArrowDown'], ['key-left-btn', 'ArrowLeft'], ['key-right-btn', 'ArrowRight']]
|
| 229 |
+
.forEach(([id, name]) => { document.getElementById(id).onclick = () => sendKeyToActive(name); });
|
| 230 |
+
|
| 231 |
function setAppStatus(message, kind = 'info') {
|
| 232 |
const clean = String(message).replace(/\x1b\[[0-?]*[ -/]*[@-~]/g, '').replace(/[\r\n]+/g, ' ').trim();
|
| 233 |
appStatus.textContent = clean || '就緒';
|
|
|
|
| 1574 |
if (!document.hidden) flushPendingClipboard();
|
| 1575 |
});
|
| 1576 |
|
| 1577 |
+
// 供外層工具列(TAB、ESC、Ctrl-C×2、方向鍵)呼叫,把按鍵送進同一個 VNC 連線。
|
| 1578 |
+
window.webtermSendKey = name => {
|
| 1579 |
+
if (!rfb) return false;
|
| 1580 |
+
const keys = {
|
| 1581 |
+
Tab: [0xff09, 'Tab'],
|
| 1582 |
+
Escape: [0xff1b, 'Escape'],
|
| 1583 |
+
ArrowUp: [0xff52, 'ArrowUp'],
|
| 1584 |
+
ArrowDown: [0xff54, 'ArrowDown'],
|
| 1585 |
+
ArrowLeft: [0xff51, 'ArrowLeft'],
|
| 1586 |
+
ArrowRight: [0xff53, 'ArrowRight'],
|
| 1587 |
+
};
|
| 1588 |
+
try {
|
| 1589 |
+
rfb.focus();
|
| 1590 |
+
if (name === 'CtrlC2') {
|
| 1591 |
+
const sendCtrlC = () => {
|
| 1592 |
+
rfb.sendKey(0xffe3, 'ControlLeft', true);
|
| 1593 |
+
rfb.sendKey(0x63, 'KeyC', true);
|
| 1594 |
+
rfb.sendKey(0x63, 'KeyC', false);
|
| 1595 |
+
rfb.sendKey(0xffe3, 'ControlLeft', false);
|
| 1596 |
+
};
|
| 1597 |
+
sendCtrlC();
|
| 1598 |
+
setTimeout(sendCtrlC, 90);
|
| 1599 |
+
return true;
|
| 1600 |
+
}
|
| 1601 |
+
const k = keys[name];
|
| 1602 |
+
if (!k) return false;
|
| 1603 |
+
rfb.sendKey(k[0], k[1], true);
|
| 1604 |
+
rfb.sendKey(k[0], k[1], false);
|
| 1605 |
+
return true;
|
| 1606 |
+
} catch (e) { return false; }
|
| 1607 |
+
};
|
| 1608 |
+
|
| 1609 |
retry.onclick = () => { delay = 800; connect(); };
|
| 1610 |
screenEl.addEventListener('click', () => { if (rfb) rfb.focus(); });
|
| 1611 |
window.addEventListener('beforeunload', () => {
|