Spaces:
Paused
Paused
Update file-manager/app.py
Browse files- 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 |
-
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 = '
|
| 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; }
|
| 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
|
| 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');
|