Spaces:
Running
Running
File size: 6,924 Bytes
b30b7c5 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 | // Terminal de Elffuss Code: xterm.js (VT100 real) + un readline propio que
// ejecuta contra shell.js (los ficheros REALES del proyecto). xterm no trae
// shell: nosotros gestionamos la línea (edición, historial, cursor) y delegamos
// la ejecución en shell.exec. La misma orden la puede lanzar la elfa vía el
// tool `terminal.run`, y su salida aparece aquí.
import * as shell from './shell.js';
let term = null, fit = null, ready = false;
let Terminal = null, FitAddon = null;
let line = '', pos = 0, histIdx = -1, busy = false;
const XT = '@xterm/xterm@5.5.0', FIT = '@xterm/addon-fit@0.10.0';
// IMPORTANTE: cargamos xterm como ESM, no UMD. Monaco trae un loader AMD
// (define/require) y la build UMD de xterm se registra como «define anónimo»
// en vez de exponer window.Terminal → «Can only have one anonymous define».
// El import ESM esquiva por completo el define de Monaco.
async function loadXterm() {
if (Terminal) return;
if (!document.getElementById('xterm-css')) {
const l = document.createElement('link');
l.id = 'xterm-css'; l.rel = 'stylesheet'; l.href = `https://cdn.jsdelivr.net/npm/${XT}/css/xterm.min.css`;
document.head.appendChild(l);
}
const [xt, ft] = await Promise.all([
import(`https://cdn.jsdelivr.net/npm/${XT}/+esm`),
import(`https://cdn.jsdelivr.net/npm/${FIT}/+esm`),
]);
Terminal = xt.Terminal || (xt.default && xt.default.Terminal);
FitAddon = ft.FitAddon || (ft.default && ft.default.FitAddon) || ft.default;
}
const prompt = () => `\x1b[36m${shell.cwdString()}\x1b[0m \x1b[35m❯\x1b[0m `;
const writePrompt = () => term.write('\r\n' + prompt());
// Redibuja la línea de entrada (prompt + buffer) y coloca el cursor en `pos`.
function redraw() {
term.write('\r\x1b[K' + prompt() + line);
const back = line.length - pos;
if (back > 0) term.write(`\x1b[${back}D`);
}
async function run(raw) {
const cmd = raw.trim();
if (!cmd) { writePrompt(); return; }
busy = true;
try {
const out = await shell.exec(cmd);
if (out === '\f') term.clear();
else if (out) term.write('\r\n' + out.replace(/\n/g, '\r\n'));
} catch (e) {
term.write('\r\n\x1b[31m' + String(e && e.message || e).replace(/\n/g, '\r\n') + '\x1b[0m');
}
busy = false;
writePrompt();
}
function onKey(data) {
if (busy) return;
switch (data) {
case '\r': // Enter
{ const cmd = line; line = ''; pos = 0; histIdx = -1; run(cmd); return; }
case '\x7f': // Retroceso
if (pos > 0) { line = line.slice(0, pos - 1) + line.slice(pos); pos--; redraw(); } return;
case '\x1b[3~': // Supr
if (pos < line.length) { line = line.slice(0, pos) + line.slice(pos + 1); redraw(); } return;
case '\x03': // Ctrl+C
term.write('^C'); line = ''; pos = 0; histIdx = -1; writePrompt(); return;
case '\x0c': // Ctrl+L
term.clear(); redraw(); return;
case '\x1b[A': case '\x1b[B': { // arriba / abajo: historial
const h = shell.getHistory(); if (!h.length) return;
if (histIdx === -1) histIdx = h.length;
histIdx = Math.max(0, Math.min(h.length, histIdx + (data === '\x1b[A' ? -1 : 1)));
line = h[histIdx] || ''; pos = line.length; redraw(); return;
}
case '\x1b[C': if (pos < line.length) { pos++; term.write('\x1b[C'); } return; // derecha
case '\x1b[D': if (pos > 0) { pos--; term.write('\x1b[D'); } return; // izquierda
case '\x1b[H': case '\x01': pos = 0; redraw(); return; // Inicio / Ctrl+A
case '\x1b[F': case '\x05': pos = line.length; redraw(); return; // Fin / Ctrl+E
case '\t': complete(); return; // Tab: completar rutas
}
if (data.length !== 1 || data.charCodeAt(0) < 32) return; // control/escape: ignorar
line = line.slice(0, pos) + data + line.slice(pos); pos += data.length; redraw();
}
// Completado de rutas simple: mira el último token contra el directorio actual.
async function complete() {
const frag = (line.slice(0, pos).split(/\s+/).pop()) || '';
const slash = frag.lastIndexOf('/');
const dirPart = slash >= 0 ? frag.slice(0, slash + 1) : '';
const base = slash >= 0 ? frag.slice(slash + 1) : frag;
let names;
try { names = await shell.listDir(dirPart); } catch { return; }
const matches = names.filter(n => n.name.startsWith(base));
if (matches.length === 1) {
const add = matches[0].name.slice(base.length) + (matches[0].kind === 'directory' ? '/' : ' ');
line = line.slice(0, pos) + add + line.slice(pos); pos += add.length; redraw();
} else if (matches.length > 1) {
term.write('\r\n' + matches.map(m => m.kind === 'directory' ? '\x1b[36m' + m.name + '/\x1b[0m' : m.name).join(' '));
writePrompt(); term.write(line); if (line.length - pos > 0) term.write(`\x1b[${line.length - pos}D`);
}
}
// Monta el terminal en `host` (una vez). Idempotente; refoca si ya existe.
export async function mount(host) {
if (ready) { setTimeout(() => { fit && fit.fit(); term && term.focus(); }, 0); return; }
await loadXterm();
term = new Terminal({
fontSize: 12.5, fontFamily: 'ui-monospace, SFMono-Regular, Menlo, monospace',
cursorBlink: true, convertEol: false, scrollback: 2000,
theme: { background: '#0b0d12', foreground: '#e8ebf4', cursor: '#ff4d8d', selectionBackground: '#7c5cff55',
brightBlack: '#6b7280', red: '#ff6b8b', green: '#7ee787', yellow: '#ffd479', blue: '#7c5cff', magenta: '#ff4d8d', cyan: '#56d4dd' },
});
fit = new FitAddon();
term.loadAddon(fit);
term.open(host);
fit.fit();
host.addEventListener('mousedown', () => setTimeout(() => term.focus(), 0));
term.onData(onKey);
const cap = shell.capabilities();
term.write('\x1b[1;35mElffuss shell\x1b[0m \x1b[90m— ficheros reales del proyecto, en tu navegador.\x1b[0m');
const rtHint = cap.bridge ? '🔌 Bridge local conectado: node/npm/python REALES en tu máquina.'
: 'Escribe \x1b[0m\x1b[32mhelp\x1b[0m\x1b[90m. node/npm/python reales → conecta el Bridge local (⚙ Ajustes).';
term.write('\r\n\x1b[90m' + rtHint + '\x1b[0m');
writePrompt();
ready = true;
setTimeout(() => term.focus(), 0);
}
export function refit() { if (ready) setTimeout(() => fit && fit.fit(), 0); }
export function focus() { if (ready) term.focus(); }
export function isReady() { return ready; }
export function clearScreen() { if (ready) { term.clear(); line = ''; pos = 0; term.write('\r\x1b[K' + prompt()); term.focus(); } }
// Eco de una orden lanzada por la elfa (para que el usuario la vea en el terminal).
export function echoAgent(cmd, out) {
if (!ready) return;
term.write('\r\x1b[K' + prompt() + '\x1b[90m' + cmd + ' (elfa)\x1b[0m');
if (out) term.write('\r\n' + shell.stripAnsi(out).replace(/\n/g, '\r\n'));
writePrompt(); term.write(line);
}
|