Add interactive SSH portfolio features
This commit is contained in:
+12
-8
@@ -5,27 +5,30 @@ export function configureTerminal(stream, title) {
|
||||
stream.write(`\x1b]2;${String(title).replace(/[\x00-\x1f\x7f]/g, '')}\x07`);
|
||||
}
|
||||
|
||||
export function createTuiSession({ channel, ctx, router, renderHome, renderPage, prompt, onClose }) {
|
||||
const state = { input: '', cursor: 0, history: [], historyIndex: 0, closed: false };
|
||||
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 => {
|
||||
const clean = text.replace(/\x1b\[20[01]~/g, '').replace(/[\r\n]+/g, ' ').replace(/[\x00-\x1f\x7f-\x9f]/g, '');
|
||||
if (!clean) return; state.input = state.input.slice(0, state.cursor) + clean + state.input.slice(state.cursor); state.cursor += clean.length; redrawInput();
|
||||
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) { channel.write(`\r\x1b[2K${crlf(text)}\r\n`); redrawInput(); } };
|
||||
const updateHeader = text => { if (!state.closed) channel.write(`\x1b7\x1b[1;1H\x1b[2K${text}\x1b8`); };
|
||||
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(); }
|
||||
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 => {
|
||||
@@ -35,6 +38,7 @@ export function createTuiSession({ channel, ctx, router, renderHome, renderPage,
|
||||
};
|
||||
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) {
|
||||
@@ -49,5 +53,5 @@ export function createTuiSession({ channel, ctx, router, renderHome, renderPage,
|
||||
}
|
||||
});
|
||||
channel.on('close', () => { state.closed = true; onClose?.(); });
|
||||
return { showHome, notify, redrawInput, updateHeader, state };
|
||||
return { showHome, notify, redrawInput, updateHeader, setInputHandler, finishLive, state };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user