Add interactive terminal commands

This commit is contained in:
2026-06-30 20:58:44 +00:00
parent 0e6a764aab
commit c87e510ca9
4 changed files with 504 additions and 3 deletions
+284
View File
@@ -0,0 +1,284 @@
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<string, string[]>;
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<string, string> = {
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 <page>',
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 <text>',
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);
}