import type { TerminalAPI, TerminalLine } from './types'; import { user } from '$lib/config'; export interface CommandContext { terminal: TerminalAPI; navigate: (path: string, external?: boolean) => void; toggleMode: () => void; setColorTheme: (name: string) => void; getMode: () => string; getColorTheme: () => string; themeNames: string[]; user: typeof user; skills: Record; navigation: Array<{ name: string; path: string; external?: boolean }>; } export interface Command { name: string; description: string; usage?: string; aliases?: string[]; hidden?: boolean; run: (args: string[], ctx: CommandContext) => void; } const out = (content: string): TerminalLine => ({ type: 'output', content }); const ok = (content: string): TerminalLine => ({ type: 'success', content }); const err = (content: string): TerminalLine => ({ type: 'error', content }); const info = (content: string): TerminalLine => ({ type: 'info', content }); const blank = (): TerminalLine => ({ type: 'blank', content: '' }); const pageRoutes: Record = { home: '/', about: '/about', portfolio: '/portfolio', projects: '/projects', models: '/models', components: '/components', links: '/links' }; const baseCommands: Command[] = [ { name: 'help', aliases: ['h', '?'], description: 'List all available commands', run: (_args, ctx) => { const rows = commands .filter((command) => !command.hidden) .map((command) => [ `(&primary,bold)${command.name}(&)` + (command.aliases?.length ? ` (&muted)(${command.aliases.join(', ')})(&)` : ''), command.description ]); ctx.terminal.write({ type: 'table', content: 'Available Commands', tableHeaders: ['Command', 'Description'], tableRows: rows, style: 'accent' } as TerminalLine); ctx.terminal.write(info('Tip: try (&accent)neofetch(&), (&accent)cd projects(&), or (&accent)theme(&)')); ctx.terminal.write(blank()); } }, { name: 'clear', aliases: ['cls'], description: 'Clear the terminal', run: (_args, ctx) => ctx.terminal.clear() }, { name: 'whoami', description: 'Show a short bio', run: (_args, ctx) => { ctx.terminal.writeLines([ out(`(&primary,bold)${ctx.user.name}(&) (&muted)— ${ctx.user.title}(&)`), out(`(&muted)${ctx.user.bio}(&)`), blank() ]); } }, { name: 'neofetch', aliases: ['banner'], description: 'Display system info', run: (_args, ctx) => { ctx.terminal.writeLines([ out(`(&accent,bold)${ctx.user.username}(&)(&muted)@(&)(&primary,bold)${ctx.user.hostname}(&)`), out('(&muted)------------------------------(&)'), out(`(&blue,bold)Name(&): ${ctx.user.name}`), out(`(&blue,bold)Title(&): ${ctx.user.title}`), out(`(&blue,bold)Location(&): ${ctx.user.location}`), out(`(&blue,bold)Shell(&): tui-terminal`), out(`(&blue,bold)Languages(&): ${ctx.skills.languages.slice(0, 6).join(', ')}`), blank() ]); } }, { name: 'skills', description: 'List skills by category', run: (_args, ctx) => { const order = ['languages', 'frameworks', 'tools', 'platforms', 'applications', 'databases', 'interests']; const lines: TerminalLine[] = []; for (const key of order) { const values = ctx.skills[key]; if (!values?.length) continue; lines.push(info(`(&blue,bold)${key}(&)`)); lines.push(out(' ' + values.map((value) => `(&cyan)${value}(&)`).join(' (&muted)•(&) '))); } lines.push(blank()); ctx.terminal.writeLines(lines); } }, { name: 'contact', aliases: ['socials'], description: 'Show social links', run: (_args, ctx) => { ctx.terminal.write(info('(&accent,bold)Find me online:(&)')); ctx.terminal.writeLines( ctx.user.socials.map((social) => ({ type: 'button', content: social.name, icon: social.icon, href: social.link, style: 'primary', inline: true })) as TerminalLine[] ); ctx.terminal.write(blank()); } }, { name: 'email', description: 'Open a mail draft', run: (_args, ctx) => { ctx.terminal.write(ok(`Opening mail to (&accent)${ctx.user.email}(&)`)); ctx.navigate(`mailto:${ctx.user.email}`, true); } }, { name: 'cd', aliases: ['goto', 'open'], description: 'Navigate to a page', usage: 'cd ', run: (args, ctx) => { const target = (args[0] || 'home').toLowerCase().replace(/^\/+|\/+$/g, ''); const navItem = ctx.navigation.find((item) => item.name.toLowerCase() === target); if (navItem) { ctx.terminal.write(info(`Navigating to (&accent)${target}(&)...`)); ctx.navigate(navItem.path, navItem.external); return; } if (pageRoutes[target] !== undefined) { ctx.terminal.write(info(`Navigating to (&accent)${target}(&)...`)); ctx.navigate(pageRoutes[target]); return; } ctx.terminal.write(err(`No such page: ${target}. Try (&bold)ls(&).`)); } }, { name: 'ls', description: 'List the pages you can visit', run: (_args, ctx) => { const names = Object.keys(pageRoutes); ctx.navigation.forEach((item) => { if (item.external && !names.includes(item.name)) names.push(item.name); }); ctx.terminal.write(out(names.map((name) => `(&blue)${name}(&)`).join(' '))); } }, { name: 'theme', description: 'List or set the color theme', usage: 'theme [name]', run: (args, ctx) => { if (!args.length) { ctx.terminal.write(out('Themes: ' + ctx.themeNames.map((name) => `(&blue)${name}(&)`).join(' '))); ctx.terminal.write(info(`Current: (&accent)${ctx.getColorTheme()}(&)`)); return; } const name = args[0].toLowerCase(); if (ctx.themeNames.includes(name)) { ctx.setColorTheme(name); ctx.terminal.write(ok(`Theme set to (&accent)${name}(&)`)); } else { ctx.terminal.write(err(`Unknown theme: ${name}. Available: ${ctx.themeNames.join(', ')}`)); } } }, { name: 'mode', description: 'Toggle or set dark/light mode', usage: 'mode [dark|light]', run: (args, ctx) => { const current = ctx.getMode(); if (!args.length) { ctx.toggleMode(); ctx.terminal.write(ok(`Switched to ${current === 'dark' ? 'light' : 'dark'} mode`)); return; } const requested = args[0].toLowerCase(); if (requested === 'dark' || requested === 'light') { if (requested !== current) ctx.toggleMode(); ctx.terminal.write(ok(`${requested} mode active`)); } else { ctx.terminal.write(err('Usage: mode [dark|light]')); } } }, { name: 'echo', description: 'Print text back', usage: 'echo ', run: (args, ctx) => ctx.terminal.write(out(args.join(' '))) }, { name: 'date', description: 'Show the current date and time', run: (_args, ctx) => ctx.terminal.write(out(new Date().toString())) }, { name: 'sudo', hidden: true, description: 'Elevated privileges', run: (_args, ctx) => ctx.terminal.write( err(`(&error)Nice try.(&) ${ctx.user.username} is not in the sudoers file. This incident will be reported.`) ) }, { name: 'blog', hidden: true, description: 'Open the blog', run: (_args, ctx) => { const blogItem = ctx.navigation.find((item) => item.name.toLowerCase() === 'blog'); if (blogItem) ctx.navigate(blogItem.path, blogItem.external); } } ]; const pageCommands: Command[] = Object.entries(pageRoutes).map(([name, path]) => ({ name, description: `Go to the ${name} page`, hidden: true, run: (_args, ctx) => ctx.navigate(path) })); const socialCommands: Command[] = user.socials.map((social) => ({ name: social.name.toLowerCase(), description: `Open ${social.name}`, hidden: true, run: (_args, ctx) => ctx.navigate(social.link, true) })); export const commands: Command[] = [...baseCommands, ...pageCommands, ...socialCommands]; function findCommand(name: string): Command | undefined { const lower = name.toLowerCase(); return commands.find( (command) => command.name === lower || command.aliases?.some((alias) => alias.toLowerCase() === lower) ); } export function runCommand(input: string, ctx: CommandContext): void { const raw = input.trim(); ctx.terminal.write({ type: 'command', content: raw }); if (!raw) return; const parts = raw.split(/\s+/); const name = parts[0]; const args = parts.slice(1); const command = findCommand(name); if (!command) { ctx.terminal.write(err(`command not found: (&bold)${name}(&) — type (&accent)help(&) for a list.`)); return; } command.run(args, ctx); }