58 lines
4.2 KiB
JavaScript
58 lines
4.2 KiB
JavaScript
import { color, crlf } from './terminal.js';
|
|
|
|
export function configureTerminal(stream, title) {
|
|
stream.write('\x1b[?2004h\x1b[?7h\x1b[?25h\x1b[?1h\x1b=');
|
|
stream.write(`\x1b]2;${String(title).replace(/[\x00-\x1f\x7f]/g, '')}\x07`);
|
|
}
|
|
|
|
export function createTuiSession({ channel, ctx, router, renderHome, renderPage, prompt, onClose, onCommand, maxInputLength = 1024 }) {
|
|
const state = { input: '', cursor: 0, history: [], historyIndex: 0, closed: false, inputHandler: null };
|
|
const redrawInput = () => {
|
|
if (state.closed) return;
|
|
channel.write(`\r\x1b[2K${prompt(ctx)}${state.input}`);
|
|
if (state.cursor < state.input.length) channel.write(`\x1b[${state.input.length - state.cursor}D`);
|
|
};
|
|
const insert = text => {
|
|
let clean = text.replace(/\x1b\[20[01]~/g, '').replace(/[\r\n]+/g, ' ').replace(/[\x00-\x1f\x7f-\x9f]/g, '');
|
|
if (!clean) return; const room = Math.max(0, maxInputLength - state.input.length); clean = clean.slice(0, room); if (!clean) return; state.input = state.input.slice(0, state.cursor) + clean + state.input.slice(state.cursor); state.cursor += clean.length; redrawInput();
|
|
};
|
|
const showHome = () => { if (!state.closed) channel.write(renderHome(ctx) + prompt(ctx) + state.input); };
|
|
const notify = text => { if (!state.closed && !state.inputHandler) { channel.write(`\r\x1b[2K${crlf(text)}\r\n`); redrawInput(); } };
|
|
const updateHeader = text => { if (!state.closed && !state.inputHandler) channel.write(`\x1b7\x1b[1;1H\x1b[2K${text}\x1b8`); };
|
|
const setInputHandler = handler => { state.inputHandler = handler; };
|
|
const finishLive = text => { state.inputHandler = null; if (!state.closed) { channel.write(`\x1b[2J\x1b[H${crlf(text)}\r\n`); redrawInput(); } };
|
|
const submit = () => {
|
|
const input = state.input.trim(); channel.write('\r\n');
|
|
if (input) { if (state.history.at(-1) !== input) state.history.push(input); if (state.history.length > 100) state.history.shift(); onCommand?.(input); }
|
|
state.historyIndex = state.history.length; state.input = ''; state.cursor = 0;
|
|
const name = input.split(/\s+/)[0]?.toLowerCase() || 'home';
|
|
if (name === 'home' || name === 'clear') { router.execute(ctx, input); showHome(); return; }
|
|
const output = router.execute(ctx, input); if (state.closed || channel.destroyed) return;
|
|
if (output === '__LIVE__') return;
|
|
channel.write(renderPage(ctx, input, output) + prompt(ctx));
|
|
};
|
|
const history = direction => {
|
|
if (!state.history.length) return;
|
|
state.historyIndex = Math.max(0, Math.min(state.history.length, state.historyIndex + direction));
|
|
state.input = state.historyIndex === state.history.length ? '' : state.history[state.historyIndex]; state.cursor = state.input.length; redrawInput();
|
|
};
|
|
const move = delta => { const next = Math.max(0, Math.min(state.input.length, state.cursor + delta)); if (next !== state.cursor) { channel.write(delta < 0 ? `\x1b[${state.cursor - next}D` : `\x1b[${next - state.cursor}C`); state.cursor = next; } };
|
|
channel.on('data', data => {
|
|
if (state.inputHandler?.(data.toString()) === true) return;
|
|
let key = data.toString();
|
|
if (key.includes('\x1b[200~')) { insert(key); return; }
|
|
while (key.length) {
|
|
const sequence = ['\x1b[A', '\x1bOA', '\x1b[B', '\x1bOB', '\x1b[C', '\x1bOC', '\x1b[D', '\x1bOD'].find(item => key.startsWith(item));
|
|
if (sequence) { if (sequence.endsWith('A')) history(-1); else if (sequence.endsWith('B')) history(1); else if (sequence.endsWith('C')) move(1); else move(-1); key = key.slice(sequence.length); continue; }
|
|
const char = key[0]; key = key.slice(1);
|
|
if (char === '\r' || char === '\n') { if (char === '\r' && key[0] === '\n') key = key.slice(1); submit(); }
|
|
else if (char === '\x03' || (char === '\x04' && !state.input)) channel.end();
|
|
else if (char === '\x7f' || char === '\b') { if (state.cursor > 0) { state.input = state.input.slice(0, state.cursor - 1) + state.input.slice(state.cursor); state.cursor--; redrawInput(); } }
|
|
else if (char === '\x01') move(-state.cursor); else if (char === '\x05') move(state.input.length - state.cursor);
|
|
else if (char >= ' ') insert(char);
|
|
}
|
|
});
|
|
channel.on('close', () => { state.closed = true; onClose?.(); });
|
|
return { showHome, notify, redrawInput, updateHeader, setInputHandler, finishLive, state };
|
|
}
|