Spaces:
Running
Running
| // 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); | |
| } | |