Add interactive SSH portfolio features

This commit is contained in:
2026-07-27 11:08:12 +09:00
parent eac572dcaf
commit 604080749b
32 changed files with 1356 additions and 31 deletions
+12 -8
View File
@@ -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 };
}