diff --git a/src/commands.js b/src/commands.js index fa5d0a3..11c08d0 100644 --- a/src/commands.js +++ b/src/commands.js @@ -6,7 +6,7 @@ export function createCommandRouter(api) { const page = name => { const item = api.config.pages?.[name]; if (!item) return `${color('red', `No page named “${name}”.`)}\nTry: pages`; - return `${color(api.config.theme?.accent || 'yellow', item.title || name)}\n${wrap((item.lines || []).join('\n')).join('\n')}`; + return wrap((item.lines || []).join('\n')).join('\n'); }; add({ name: 'home', description: 'Return home', run: () => { @@ -14,7 +14,11 @@ export function createCommandRouter(api) { const status = api.ownerOnline() ? color('green', 'Owner is online') : color(api.config.theme?.muted || 'brightBlack', 'Owner is offline'); return `${color(api.config.theme?.accent || 'yellow', `Welcome to ${p.name || 'my portfolio'}`)}\n\n${wrap(p.bio || 'You are exploring a portfolio over SSH. Type “help” to get started.').join('\n')}\n\n${status}\n\nCommands: home · pages · open · chat · contact · clear · quit`; }}); - add({ name: 'help', description: 'Show this help', run: ctx => [...commands.values()].filter(c => !c.admin || api.isAdmin(ctx)).map(c => `${c.name.padEnd(18)} ${c.description}`).join('\n') }); + add({ name: 'help', description: 'Show this help', run: ctx => { + const actions = [...commands.values()].filter(c => !c.admin || api.isAdmin(ctx)).map(c => `${c.name.padEnd(18)} ${c.description}`); + const pages = Object.entries(api.config.pages || {}).filter(([name]) => !commands.has(name)).map(([name, item]) => `${name.padEnd(18)} Open ${item.title || name}`); + return [...actions, ...pages].join('\n'); + }}); add({ name: 'pages', description: 'List available pages', run: () => Object.entries(api.config.pages || {}).map(([name, item]) => `${color(api.config.theme?.accent || 'yellow', name.padEnd(14))} ${item.title || ''}`).join('\n') || 'No pages configured.' }); add({ name: 'open', description: 'Open a page', run: (ctx, arg) => arg ? page(arg) : 'Usage: open ' }); add({ name: 'contact', description: 'Show contact details', run: () => `Email: ${api.config.persona?.email || 'not configured'}` }); @@ -32,7 +36,7 @@ export function createCommandRouter(api) { return { home: ctx => commands.get('home').run(ctx, ''), execute(ctx, input) { - const [name, ...rest] = input.trim().split(/\s+/); const command = commands.get(name || 'home'); + const [name, ...rest] = input.trim().split(/\s+/); const command = commands.get(name || 'home') || (api.config.pages?.[name] ? { run: () => page(name) } : null); if (!command || (command.admin && !api.isAdmin(ctx))) return `Unknown command: ${name}. Try “help”.`; return command.run(ctx, rest.join(' ')); } diff --git a/src/server.js b/src/server.js index 96cde12..bc3cf69 100644 --- a/src/server.js +++ b/src/server.js @@ -26,12 +26,17 @@ function renderHome(ctx) { const content = router.home(ctx).split('\n'); return `${clear}${header(title, `${count()} connected`, width, config.theme?.primary, config.theme?.muted)}\r\n${color(config.theme?.muted || 'brightBlack', p.location || '')}\r\n\r\n${box('Portfolio', content, width, config.theme)}\r\n\r\n`; } +function renderPage(ctx, input, content = '') { + const width = Math.max(40, ctx.cols || 80); const [command, argument] = input.split(/\s+/); const pageName = command === 'open' ? argument : command; + const title = config.pages?.[pageName]?.title || (command ? command[0].toUpperCase() + command.slice(1) : 'Portfolio'); + return `${clear}${header(`● ${config.persona?.name || 'Your Name'} — ${config.persona?.tagline || ''}`, `${count()} connected`, width, config.theme?.primary, config.theme?.muted)}\r\n${color(config.theme?.muted || 'brightBlack', config.persona?.location || '')}\r\n\r\n${box(title, String(content || '').split(/\r?\n/), width, config.theme)}\r\n\r\n`; +} function refreshHeaders() { for (const s of sessions) s.tui?.updateHeader(header(`● ${config.persona?.name || 'Your Name'} — ${config.persona?.tagline || ''}`, `${count()} connected`, s.ctx.cols, config.theme?.primary, config.theme?.muted)); } const router = createCommandRouter({ config, isAdmin, ownerOnline: () => [...sessions].some(s => s.ctx.admin), sendNotification, reload: () => Object.assign(config, loadConfig()), broadcast: line => { for (const s of sessions) s.tui?.notify(color(config.theme?.accent || 'yellow', line)); } }); function handleSession(channel, ctx) { ctx.channel = channel; const active = { channel, ctx, tui: null }; sessions.add(active); refreshHeaders(); configureTerminal(channel, config.persona?.name || 'SSH Portfolio'); - active.tui = createTuiSession({ channel, ctx, router, prompt, renderHome, onClose: () => { sessions.delete(active); refreshHeaders(); } }); + active.tui = createTuiSession({ channel, ctx, router, prompt, renderHome, renderPage, onClose: () => { sessions.delete(active); refreshHeaders(); } }); active.tui.showHome(); } const server = new Server({ hostKeys: [fs.readFileSync(hostKey)], ident: 'ssh-portfolio' }, client => { diff --git a/src/tui.js b/src/tui.js index dbf7d1d..5ee32c6 100644 --- a/src/tui.js +++ b/src/tui.js @@ -5,7 +5,7 @@ export function configureTerminal(stream, title) { stream.write(`\x1b]2;${String(title).replace(/[\x00-\x1f\x7f]/g, '')}\x07`); } -export function createTuiSession({ channel, ctx, router, renderHome, prompt, onClose }) { +export function createTuiSession({ channel, ctx, router, renderHome, renderPage, prompt, onClose }) { const state = { input: '', cursor: 0, history: [], historyIndex: 0, closed: false }; const redrawInput = () => { if (state.closed) return; @@ -26,7 +26,7 @@ export function createTuiSession({ channel, ctx, router, renderHome, prompt, onC 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) channel.write(`${crlf(output)}\r\n`); channel.write(prompt(ctx)); + channel.write(renderPage(ctx, input, output) + prompt(ctx)); }; const history = direction => { if (!state.history.length) return;