Spaces:
Paused
Paused
Update file-manager/app.py
Browse files- file-manager/app.py +36 -17
file-manager/app.py
CHANGED
|
@@ -154,7 +154,7 @@ def term_page():
|
|
| 154 |
<script src="https://cdn.jsdelivr.net/npm/xterm@5.3.0/lib/xterm.js"></script>
|
| 155 |
<script src="https://cdn.jsdelivr.net/npm/xterm-addon-fit@0.8.0/lib/xterm-addon-fit.js"></script>
|
| 156 |
<script>
|
| 157 |
-
let term, fit, current = null, cursor = 0,
|
| 158 |
|
| 159 |
function initTerm(){
|
| 160 |
document.getElementById('term').innerHTML = '';
|
|
@@ -163,9 +163,12 @@ def term_page():
|
|
| 163 |
term.loadAddon(fit);
|
| 164 |
term.open(document.getElementById('term'));
|
| 165 |
fit.fit();
|
|
|
|
|
|
|
| 166 |
term.onData(d => {
|
| 167 |
if(!current) return;
|
| 168 |
-
fetch('/fm/term_in', {method:'POST',
|
|
|
|
| 169 |
body: JSON.stringify({name: current, data: d})});
|
| 170 |
});
|
| 171 |
}
|
|
@@ -179,45 +182,60 @@ def term_page():
|
|
| 179 |
names.forEach(n => {
|
| 180 |
const t = document.createElement('span');
|
| 181 |
t.className = 'tab' + (n===current ? ' active':'');
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 186 |
wrap.appendChild(t);
|
| 187 |
});
|
| 188 |
const add = document.createElement('span');
|
| 189 |
add.className = 'tab'; add.textContent = '+ New';
|
| 190 |
add.onclick = newSession;
|
| 191 |
wrap.appendChild(add);
|
| 192 |
-
|
|
|
|
| 193 |
if(current && !names.includes(current)){ current = null; }
|
|
|
|
| 194 |
if(!current && names.length){ attach(names[0]); }
|
| 195 |
}
|
| 196 |
|
| 197 |
async function attach(name){
|
|
|
|
| 198 |
current = name; cursor = 0;
|
| 199 |
initTerm();
|
| 200 |
// replay full scrollback so you see what happened while away
|
| 201 |
const r = await fetch('/fm/term_snapshot?name=' + encodeURIComponent(name));
|
| 202 |
const j = await r.json();
|
|
|
|
| 203 |
term.write(j.data); cursor = j.cursor;
|
| 204 |
refreshTabs();
|
| 205 |
startPoll();
|
| 206 |
}
|
| 207 |
|
|
|
|
| 208 |
function startPoll(){
|
| 209 |
-
if(
|
|
|
|
| 210 |
async function poll(){
|
| 211 |
if(current){
|
| 212 |
try{
|
| 213 |
const r = await fetch('/fm/term_out?name=' + encodeURIComponent(current) + '&cursor=' + cursor);
|
| 214 |
const j = await r.json();
|
| 215 |
-
if(j.gone){ current = null; refreshTabs();
|
| 216 |
-
if(j.data) term.write(j.data);
|
| 217 |
-
cursor = j.cursor;
|
| 218 |
}catch(e){}
|
| 219 |
}
|
| 220 |
-
|
| 221 |
}
|
| 222 |
poll();
|
| 223 |
}
|
|
@@ -230,10 +248,11 @@ def term_page():
|
|
| 230 |
}
|
| 231 |
|
| 232 |
async function closeSession(name){
|
| 233 |
-
await fetch('/fm/term_close', {method:'POST',
|
|
|
|
| 234 |
body: JSON.stringify({name})});
|
| 235 |
-
if(current===name) current=null;
|
| 236 |
-
refreshTabs();
|
| 237 |
}
|
| 238 |
|
| 239 |
// boot
|
|
@@ -259,8 +278,8 @@ def term_close():
|
|
| 259 |
name = request.get_json(force=True).get("name")
|
| 260 |
sh = _shells.get(name)
|
| 261 |
if sh:
|
| 262 |
-
sh.kill()
|
| 263 |
-
|
| 264 |
return Response(json.dumps({"ok": True}), mimetype="application/json")
|
| 265 |
|
| 266 |
@app.route("/term_snapshot")
|
|
|
|
| 154 |
<script src="https://cdn.jsdelivr.net/npm/xterm@5.3.0/lib/xterm.js"></script>
|
| 155 |
<script src="https://cdn.jsdelivr.net/npm/xterm-addon-fit@0.8.0/lib/xterm-addon-fit.js"></script>
|
| 156 |
<script>
|
| 157 |
+
let term, fit, current = null, cursor = 0, polling = false;
|
| 158 |
|
| 159 |
function initTerm(){
|
| 160 |
document.getElementById('term').innerHTML = '';
|
|
|
|
| 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 |
}
|
|
|
|
| 182 |
names.forEach(n => {
|
| 183 |
const t = document.createElement('span');
|
| 184 |
t.className = 'tab' + (n===current ? ' active':'');
|
| 185 |
+
|
| 186 |
+
const label = document.createElement('span');
|
| 187 |
+
label.textContent = n + ' ';
|
| 188 |
+
label.onclick = () => attach(n);
|
| 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); };
|
| 196 |
+
|
| 197 |
+
t.appendChild(label);
|
| 198 |
+
t.appendChild(x);
|
| 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; }
|
| 220 |
term.write(j.data); cursor = j.cursor;
|
| 221 |
refreshTabs();
|
| 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;
|
| 229 |
async function poll(){
|
| 230 |
if(current){
|
| 231 |
try{
|
| 232 |
const r = await fetch('/fm/term_out?name=' + encodeURIComponent(current) + '&cursor=' + cursor);
|
| 233 |
const j = await r.json();
|
| 234 |
+
if(j.gone){ current = null; refreshTabs(); }
|
| 235 |
+
else { if(j.data) term.write(j.data); cursor = j.cursor; }
|
|
|
|
| 236 |
}catch(e){}
|
| 237 |
}
|
| 238 |
+
setTimeout(poll, 200);
|
| 239 |
}
|
| 240 |
poll();
|
| 241 |
}
|
|
|
|
| 248 |
}
|
| 249 |
|
| 250 |
async function closeSession(name){
|
| 251 |
+
await fetch('/fm/term_close', {method:'POST',
|
| 252 |
+
headers:{'Content-Type':'application/json'},
|
| 253 |
body: JSON.stringify({name})});
|
| 254 |
+
if(current === name){ current = null; }
|
| 255 |
+
await refreshTabs();
|
| 256 |
}
|
| 257 |
|
| 258 |
// boot
|
|
|
|
| 278 |
name = request.get_json(force=True).get("name")
|
| 279 |
sh = _shells.get(name)
|
| 280 |
if sh:
|
| 281 |
+
sh.kill() # SIGKILL the bash process
|
| 282 |
+
_shells.pop(name, None) # remove from the session list
|
| 283 |
return Response(json.dumps({"ok": True}), mimetype="application/json")
|
| 284 |
|
| 285 |
@app.route("/term_snapshot")
|