Rewrite website as a Hyprland desktop

This commit is contained in:
SirBlob committed 2026-10-26 14:33:05 -04:00
1 parent 7c52779cf6
commit 3065ad6092
197 files changed
+5609 -17779

No files matched your search

+63
View File
@@ -0,0 +1,63 @@
import { profile } from '#lib/content';
import { allSitePages } from '#lib/site/pages';
import { blankLine, errorLine, line, span } from '../output';
import { infoCommands } from './info';
import { systemCommands } from './system';
import type { Command, CommandContext } from './types';
export type { Command, CommandContext } from './types';
const helpCommand: Command = {
name: 'help',
aliases: ['h', '?'],
description: 'List all available commands',
run: (_args, context) => {
commands
.filter((command) => !command.hidden)
.forEach((command) => {
const aliases = command.aliases?.length ? ` (${command.aliases.join(', ')})` : '';
context.print(
line(span(command.name.padEnd(12), 'accent', { bold: true }), command.description, span(aliases, 'muted'))
);
});
context.print(blankLine, line(span('Tip: try ', 'muted'), span('fastfetch', 'accent'), span(', ', 'muted'), span('cowsay hi', 'accent'), span(' or ', 'muted'), span('cd projects', 'accent')), blankLine);
}
};
const pageShortcuts: Command[] = allSitePages.map((page) => ({
name: page.name,
description: `Open ${page.title}`,
hidden: true,
run: (_args, context) => context.openPage(page.path)
}));
const socialShortcuts: Command[] = profile.socials.map((social) => ({
name: social.name.toLowerCase(),
description: `Open ${social.name}`,
hidden: true,
run: (_args, context) => context.openUrl(social.url)
}));
export const commands: Command[] = [helpCommand, ...infoCommands, ...systemCommands, ...pageShortcuts, ...socialShortcuts];
export function findCommand(name: string): Command | undefined {
const lowerName = name.toLowerCase();
return commands.find((command) => command.name === lowerName || command.aliases?.includes(lowerName));
}
export function visibleCommandNames(): string[] {
return commands.filter((command) => !command.hidden).map((command) => command.name);
}
export function runCommand(input: string, context: CommandContext) {
const trimmed = input.trim();
if (!trimmed) return;
const [name, ...args] = trimmed.split(/\s+/);
const command = findCommand(name);
if (!command) {
context.print(errorLine(`command not found: ${name}. Type help for a list`));
return;
}
command.run(args, context);
}
+82
View File
@@ -0,0 +1,82 @@
import { experience, profile, projectCategories, findProjectCategory } from '#lib/content';
import { blankLine, errorLine, line, link, span, type Tone } from '../output';
import type { Command } from './types';
import { whoamiCommand } from './whoami';
const skillTones: Tone[] = ['blue', 'magenta', 'green', 'yellow', 'orange', 'cyan', 'accent'];
export const infoCommands: Command[] = [
whoamiCommand,
{
name: 'fastfetch',
aliases: ['neofetch', 'banner'],
description: 'Display system info',
run: (_args, context) => context.print({ kind: 'fetch' })
},
{
name: 'skills',
description: 'List skills by category',
run: (_args, context) => {
Object.entries(profile.skills).forEach(([category, skills], index) => {
const tone = skillTones[index % skillTones.length];
context.print(line(span(category, tone, { bold: true })), line(' ', span(skills.join(' · '), 'foreground')));
});
context.print(blankLine);
}
},
{
name: 'contact',
aliases: ['socials'],
description: 'Show social links',
run: (_args, context) => {
context.print(line(span('Find me online:', 'accent', { bold: true })));
profile.socials.forEach((social) =>
context.print(line(span(` ${social.name.padEnd(10)}`, 'muted'), link(social.url.replace(/^mailto:/, ''), social.url)))
);
context.print(blankLine);
}
},
{
name: 'email',
description: 'Show contact email',
run: (_args, context) => context.print(line(link(profile.email, `mailto:${profile.email}`)))
},
{
name: 'experience',
aliases: ['work'],
description: 'Show work experience',
run: (_args, context) => {
experience.forEach((job) => {
context.print(
line(span(job.role, 'orange', { bold: true }), span(` @ ${job.organization}`, 'accent')),
line(span(` ${job.period}`, 'muted'))
);
job.awards?.forEach((award) => context.print(line(span(` * ${award}`, 'green'))));
});
context.print(blankLine);
}
},
{
name: 'projects',
description: 'List projects, optionally by category',
usage: 'projects [opensource|packages|hackathons|research]',
argumentOptions: () => projectCategories.map((category) => category.id),
run: (args, context) => {
const categories = args[0] ? [findProjectCategory(args[0])] : projectCategories;
if (!categories[0]) {
context.print(errorLine(`unknown category ${args[0]}`));
return;
}
categories.forEach((category) => {
if (!category) return;
context.print(line(span(`${category.title}/`, 'blue', { bold: true })));
category.projects.forEach((project, index) => {
const branch = index === category.projects.length - 1 ? '└──' : '├──';
const marker = project.awards?.length ? span(' [award]', 'yellow') : '';
context.print(line(span(` ${branch} `, 'muted'), project.name, marker));
});
});
context.print(line(span('Open one with ', 'muted'), span('cd projects/<category>', 'accent')), blankLine);
}
}
];
+134
View File
@@ -0,0 +1,134 @@
import { profile } from '#lib/content';
import { themes } from '#lib/theme/themes';
import { allSitePages, findSitePage, sitePages } from '#lib/site/pages';
import { blankLine, errorLine, line, span } from '../output';
import type { Command } from './types';
function cowsay(message: string): string[] {
const text = message || 'Moo?';
const border = '-'.repeat(text.length + 2);
return [
` ${border}`,
`< ${text} >`,
` ${border}`,
' \\ ^__^',
' \\ (oo)\\_______',
' (__)\\ )\\/\\',
' ||----w |',
' || ||'
];
}
function easternDate(): string {
return new Date().toLocaleString('en-US', {
timeZone: 'America/New_York',
weekday: 'long',
year: 'numeric',
month: 'long',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
timeZoneName: 'short'
});
}
export const systemCommands: Command[] = [
{
name: 'clear',
aliases: ['cls'],
description: 'Clear the terminal',
run: (_args, context) => context.clear()
},
{
name: 'ls',
aliases: ['dir', 'tree'],
description: 'List the pages you can open',
run: (_args, context) => {
context.print(line(span('~', 'blue', { bold: true })));
sitePages.forEach((page, pageIndex) => {
const isLastPage = pageIndex === sitePages.length - 1;
context.print(line(span(isLastPage ? '└── ' : '├── ', 'muted'), span(`${page.name}/`, 'blue', { bold: true })));
page.children?.forEach((child, childIndex) => {
const isLastChild = childIndex === page.children!.length - 1;
const indent = isLastPage ? ' ' : '│ ';
context.print(line(span(`${indent}${isLastChild ? '└── ' : '├── '}`, 'muted'), span(`${child.name}/`, 'cyan')));
});
});
context.print(blankLine);
}
},
{
name: 'cd',
aliases: ['open', 'goto'],
description: 'Open a page in a window',
usage: 'cd <page>, for example cd projects/hackathons',
argumentOptions: () => ['~', ...allSitePages.map((page) => page.path.slice(1))],
run: (args, context) => {
const target = args[0] ?? '~';
if (target === '~' || target === '/' || target === 'home') {
context.openPage('/');
return;
}
const page = findSitePage(target);
if (!page) {
context.print(errorLine(`no such page: ${target}. Try ls`));
return;
}
context.print(line(span('opening ', 'muted'), span(page.path, 'accent')));
context.openPage(page.path);
}
},
{
name: 'theme',
description: 'List or set the color theme',
usage: 'theme [name]',
argumentOptions: () => themes.map((theme) => theme.id),
run: (args, context) => {
if (!args[0]) {
themes.forEach((theme) => {
const isCurrent = theme.id === context.currentThemeId();
context.print(line(span(isCurrent ? '* ' : ' ', 'accent'), span(theme.id, isCurrent ? 'accent' : 'foreground'), span(` ${theme.mode}`, 'muted')));
});
return;
}
if (context.setTheme(args[0].toLowerCase())) context.print(line(span('theme set to ', 'muted'), span(args[0], 'accent')));
else context.print(errorLine(`unknown theme ${args[0]}. Run theme to list them`));
}
},
{
name: 'echo',
description: 'Print text back',
usage: 'echo <text>',
run: (args, context) => context.print(line(args.join(' ')))
},
{
name: 'cowsay',
description: 'Make a cow say something',
usage: 'cowsay <text>',
run: (args, context) => context.print(...cowsay(args.join(' ')).map((text) => line(span(text, 'accent'))))
},
{
name: 'date',
description: 'Show the current date and time (Eastern)',
run: (_args, context) => context.print(line(easternDate()))
},
{
name: 'exit',
description: 'Close this terminal',
run: (_args, context) => context.exit()
},
{
name: 'sudo',
hidden: true,
description: 'Elevated privileges',
run: (_args, context) =>
context.print(errorLine(`${profile.username} is not in the sudoers file. This incident will be reported.`))
},
{
name: 'pwd',
hidden: true,
description: 'Print working directory',
run: (_args, context) => context.print(line(`/home/${profile.username}`))
}
];
+21
View File
@@ -0,0 +1,21 @@
import type { OutputBlock } from '../output';
export interface CommandContext {
print: (...blocks: OutputBlock[]) => void;
clear: () => void;
exit: () => void;
openPage: (path: string) => void;
openUrl: (url: string) => void;
setTheme: (themeId: string) => boolean;
currentThemeId: () => string;
}
export interface Command {
name: string;
description: string;
usage?: string;
aliases?: string[];
hidden?: boolean;
argumentOptions?: () => string[];
run: (args: string[], context: CommandContext) => void;
}
+52
View File
@@ -0,0 +1,52 @@
import { experience, profile } from '#lib/content';
import { blankLine, line, link, span, type OutputBlock, type Span } from '../output';
import type { Command } from './types';
const labelWidth = 11;
function field(label: string, ...value: (string | Span)[]): OutputBlock {
return line(span(` ${label.padEnd(labelWidth)}`, 'blue', { bold: true }), ...value);
}
function joinWithDots(spans: Span[]): Span[] {
return spans.flatMap((item, index) => (index === 0 ? [item] : [span(' · ', 'muted'), item]));
}
function whoamiOutput(): OutputBlock[] {
const currentRole = experience[0];
const header = `${profile.username}@${profile.hostname}`;
const interests = profile.interests;
const socialLinks = profile.socials.map((social) => link(social.name.toLowerCase(), social.url));
return [
line(span(profile.username, 'accent', { bold: true }), span('@', 'muted'), span(profile.hostname, 'accent', { bold: true })),
line(span('─'.repeat(header.length), 'muted')),
field('alias', profile.displayName),
field('role', profile.title),
field('school', `${profile.education.school}, ${profile.education.degree}`),
...(currentRole ? [field('now', span(currentRole.role, 'orange'), span(' @ ', 'muted'), currentRole.organization)] : []),
...(interests.length ? [field('interests', ...joinWithDots(interests.map((interest) => span(interest, 'magenta'))))] : []),
blankLine,
line(span(' ', 'muted'), span(profile.bio, 'foreground')),
blankLine,
field('links', ...joinWithDots(socialLinks)),
blankLine,
line(
span(' try ', 'muted'),
span('skills', 'green'),
span(', ', 'muted'),
span('experience', 'green'),
span(', ', 'muted'),
span('projects', 'green'),
span(' or ', 'muted'),
span('cd portfolio', 'green')
),
blankLine
];
}
export const whoamiCommand: Command = {
name: 'whoami',
description: 'Show who I am',
run: (_args, context) => context.print(...whoamiOutput())
};
+37
View File
@@ -0,0 +1,37 @@
import { findCommand, visibleCommandNames } from './commands';
export interface CompletionResult {
input: string;
options: string[];
}
function sharedPrefix(words: string[]): string {
return words.reduce((prefix, word) => {
let length = 0;
while (length < prefix.length && prefix[length] === word[length]) length++;
return prefix.slice(0, length);
});
}
function candidatesFor(words: string[]): string[] {
if (words.length <= 1) return visibleCommandNames();
if (words.length > 2) return [];
return findCommand(words[0])?.argumentOptions?.() ?? [];
}
export function complete(input: string): CompletionResult {
const words = input.trimStart().split(/\s+/);
const partial = words.at(-1) ?? '';
const matches = [...new Set(candidatesFor(words))].filter((candidate) =>
candidate.toLowerCase().startsWith(partial.toLowerCase())
);
if (matches.length === 0) return { input, options: [] };
const before = input.slice(0, input.length - partial.length);
if (matches.length === 1) return { input: `${before}${matches[0]} `, options: [] };
const prefix = sharedPrefix(matches);
const madeProgress = prefix.length > partial.length;
return { input: `${before}${madeProgress ? prefix : partial}`, options: madeProgress ? [] : matches.sort() };
}
+41
View File
@@ -0,0 +1,41 @@
export type Tone =
| 'foreground'
| 'muted'
| 'accent'
| 'red'
| 'green'
| 'yellow'
| 'orange'
| 'blue'
| 'cyan'
| 'magenta';
export interface Span {
text: string;
tone?: Tone;
bold?: boolean;
href?: string;
}
export type OutputBlock =
| { kind: 'prompt'; command: string }
| { kind: 'line'; spans: Span[] }
| { kind: 'fetch' };
export function span(text: string, tone?: Tone, options: Omit<Span, 'text' | 'tone'> = {}): Span {
return { text, tone, ...options };
}
export function line(...parts: (string | Span)[]): OutputBlock {
return { kind: 'line', spans: parts.map((part) => (typeof part === 'string' ? { text: part } : part)) };
}
export function link(text: string, href: string, tone: Tone = 'accent'): Span {
return { text, tone, href };
}
export function errorLine(message: string): OutputBlock {
return line(span('error: ', 'red', { bold: true }), message);
}
export const blankLine: OutputBlock = line('');