HelloSun commited on
Commit
8516fcf
·
1 Parent(s): fd7e22f

從 HelloSun/testzerogpu 同步:ESC、Ctrl-C×2、方向鍵,並新增 TAB 鍵(ESC 左側)

Browse files
Files changed (1) hide show
  1. app.py +72 -0
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', () => {