CORVO-AI commited on
Commit
333874f
·
verified ·
1 Parent(s): 2608bcb

Update file-manager/app.py

Browse files
Files changed (1) hide show
  1. file-manager/app.py +66 -13
file-manager/app.py CHANGED
@@ -146,6 +146,7 @@ def term_page():
146
  <div class="bar">
147
  <b>Terminals</b> — sessions live on the box and survive closing the browser.
148
  <span style="color:#777">Reopen this page anytime to reattach.</span>
 
149
  </div>
150
  <div id="tabs"></div>
151
  <div id="term" style="height:68vh;background:#000;border-radius:0 6px 6px 6px"></div>
@@ -156,6 +157,36 @@ def term_page():
156
  <script>
157
  let term, fit, current = null, cursor = 0, polling = false;
158
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
159
  function initTerm(){
160
  document.getElementById('term').innerHTML = '';
161
  term = new Terminal({convertEol:false, cursorBlink:true, fontFamily:'monospace', fontSize:13});
@@ -163,15 +194,41 @@ def term_page():
163
  term.loadAddon(fit);
164
  term.open(document.getElementById('term'));
165
  fit.fit();
 
166
  // FIX 1: only SEND the keystroke; never echo locally.
167
  // The PTY (bash) echoes it back through the poll, so echoing here = duplicates.
168
- term.onData(d => {
169
- if(!current) return;
170
- fetch('/fm/term_in', {method:'POST',
171
- headers:{'Content-Type':'application/json'},
172
- body: JSON.stringify({name: current, data: d})});
 
 
 
 
 
 
 
 
 
 
 
 
 
 
173
  });
 
 
 
 
 
 
 
 
 
 
174
  }
 
175
  window.addEventListener('resize', () => { if(fit) fit.fit(); });
176
 
177
  async function refreshTabs(){
@@ -189,7 +246,7 @@ def term_page():
189
 
190
  // FIX 3: explicit close button, separate from tab-switch click
191
  const x = document.createElement('span');
192
- x.textContent = '';
193
  x.style.marginLeft = '8px';
194
  x.style.color = '#F43256';
195
  x.onclick = (e) => { e.stopPropagation(); closeSession(n); };
@@ -199,21 +256,18 @@ def term_page():
199
  wrap.appendChild(t);
200
  });
201
  const add = document.createElement('span');
202
- add.className = 'tab'; add.textContent = ' New';
203
  add.onclick = newSession;
204
  wrap.appendChild(add);
205
 
206
- // if the current session vanished, drop it (poll will stop touching it)
207
  if(current && !names.includes(current)){ current = null; }
208
- // auto-attach to first session only if nothing is currently attached
209
  if(!current && names.length){ attach(names[0]); }
210
  }
211
 
212
  async function attach(name){
213
- if(current === name){ return; } // already attached, don't re-stack
214
  current = name; cursor = 0;
215
  initTerm();
216
- // replay full scrollback so you see what happened while away
217
  const r = await fetch('/fm/term_snapshot?name=' + encodeURIComponent(name));
218
  const j = await r.json();
219
  if(j.gone){ current = null; refreshTabs(); return; }
@@ -222,7 +276,7 @@ def term_page():
222
  startPoll();
223
  }
224
 
225
- // FIX 2: single guarded poll loop — never stack multiple loops (that duplicated output)
226
  function startPoll(){
227
  if(polling) return;
228
  polling = true;
@@ -255,7 +309,6 @@ def term_page():
255
  await refreshTabs();
256
  }
257
 
258
- // boot
259
  (async () => {
260
  await refreshTabs();
261
  const r = await fetch('/fm/term_list');
 
146
  <div class="bar">
147
  <b>Terminals</b> — sessions live on the box and survive closing the browser.
148
  <span style="color:#777">Reopen this page anytime to reattach.</span>
149
+ <span style="color:#47E6C1;margin-left:12px">Paste: Ctrl/Cmd+V or right-click · Copy: select then Ctrl/Cmd+C</span>
150
  </div>
151
  <div id="tabs"></div>
152
  <div id="term" style="height:68vh;background:#000;border-radius:0 6px 6px 6px"></div>
 
157
  <script>
158
  let term, fit, current = null, cursor = 0, polling = false;
159
 
160
+ // send raw bytes to the PTY (used by both typing and pasting)
161
+ function sendData(d){
162
+ if(!current) return;
163
+ fetch('/fm/term_in', {method:'POST',
164
+ headers:{'Content-Type':'application/json'},
165
+ body: JSON.stringify({name: current, data: d})});
166
+ }
167
+
168
+ // FIX 4: robust paste. Reads the clipboard and streams it to the PTY.
169
+ async function doPaste(){
170
+ if(!current) return;
171
+ try {
172
+ const text = await navigator.clipboard.readText();
173
+ if(text) sendData(text);
174
+ } catch(e) {
175
+ // Clipboard API blocked (insecure context / permissions).
176
+ // Fall back to a prompt so the user can still paste manually.
177
+ const text = window.prompt('Paste here (clipboard access was blocked):', '');
178
+ if(text) sendData(text);
179
+ }
180
+ }
181
+
182
+ // FIX 5: copy current selection to the clipboard.
183
+ async function doCopy(){
184
+ const sel = term.getSelection();
185
+ if(sel){
186
+ try { await navigator.clipboard.writeText(sel); } catch(e) {}
187
+ }
188
+ }
189
+
190
  function initTerm(){
191
  document.getElementById('term').innerHTML = '';
192
  term = new Terminal({convertEol:false, cursorBlink:true, fontFamily:'monospace', fontSize:13});
 
194
  term.loadAddon(fit);
195
  term.open(document.getElementById('term'));
196
  fit.fit();
197
+
198
  // FIX 1: only SEND the keystroke; never echo locally.
199
  // The PTY (bash) echoes it back through the poll, so echoing here = duplicates.
200
+ term.onData(d => sendData(d));
201
+
202
+ // FIX 6: intercept clipboard shortcuts BEFORE xterm consumes them.
203
+ // Returning false stops xterm from also handling the key.
204
+ term.attachCustomKeyEventHandler((e) => {
205
+ const mod = e.ctrlKey || e.metaKey; // Ctrl on Win/Linux, Cmd on Mac
206
+ if(e.type === 'keydown' && mod && e.key.toLowerCase() === 'v'){
207
+ e.preventDefault();
208
+ doPaste();
209
+ return false;
210
+ }
211
+ // Only hijack Ctrl/Cmd+C for copy when there IS a selection,
212
+ // otherwise let it through so Ctrl+C still sends SIGINT to bash.
213
+ if(e.type === 'keydown' && mod && e.key.toLowerCase() === 'c' && term.hasSelection()){
214
+ e.preventDefault();
215
+ doCopy();
216
+ return false;
217
+ }
218
+ return true;
219
  });
220
+
221
+ // Right-click / middle-click / browser paste event fallback.
222
+ const el = document.getElementById('term');
223
+ el.addEventListener('paste', (e) => {
224
+ e.preventDefault();
225
+ const text = (e.clipboardData || window.clipboardData).getData('text');
226
+ if(text) sendData(text);
227
+ });
228
+ // Right-click = paste (common terminal convention)
229
+ el.addEventListener('contextmenu', (e) => { e.preventDefault(); doPaste(); });
230
  }
231
+
232
  window.addEventListener('resize', () => { if(fit) fit.fit(); });
233
 
234
  async function refreshTabs(){
 
246
 
247
  // FIX 3: explicit close button, separate from tab-switch click
248
  const x = document.createElement('span');
249
+ x.textContent = '\\u2715';
250
  x.style.marginLeft = '8px';
251
  x.style.color = '#F43256';
252
  x.onclick = (e) => { e.stopPropagation(); closeSession(n); };
 
256
  wrap.appendChild(t);
257
  });
258
  const add = document.createElement('span');
259
+ add.className = 'tab'; add.textContent = '\\uFF0B New';
260
  add.onclick = newSession;
261
  wrap.appendChild(add);
262
 
 
263
  if(current && !names.includes(current)){ current = null; }
 
264
  if(!current && names.length){ attach(names[0]); }
265
  }
266
 
267
  async function attach(name){
268
+ if(current === name){ return; }
269
  current = name; cursor = 0;
270
  initTerm();
 
271
  const r = await fetch('/fm/term_snapshot?name=' + encodeURIComponent(name));
272
  const j = await r.json();
273
  if(j.gone){ current = null; refreshTabs(); return; }
 
276
  startPoll();
277
  }
278
 
279
+ // FIX 2: single guarded poll loop — never stack multiple loops.
280
  function startPoll(){
281
  if(polling) return;
282
  polling = true;
 
309
  await refreshTabs();
310
  }
311
 
 
312
  (async () => {
313
  await refreshTabs();
314
  const r = await fetch('/fm/term_list');