24 KiB
Terminal Portfolio
An Arch Linux terminal-themed portfolio website with Hyprland-style TUI components, built with SvelteKit, Tailwind CSS, and Three.js.
Features
- Hyprland-style TUI - Terminal interface inspired by Textual Python TUI
- Interactive command line - Type commands directly into the terminal to navigate and run actions
- Theme Support - Arch, Catppuccin, Wintry, Rose, and Cerberus themes, each with dark/light variants
- Dark/Light Mode - Toggle between dark and light modes
- Keyboard Navigation - Navigate with arrow keys or vim-style j/k
- Native Blog - Markdown posts with tags, authors, syntax highlighting, search, pagination, and an RSS feed
- 3D Model Viewer - Interactive Three.js viewer for .glb models
- QR Links Page - Generate a scannable QR code for any of your links
- Configurable Speed - Per-page typing animation speed
- Responsive - Works on desktop and mobile
- Rich Text Formatting - Colors, backgrounds, text decorations, and sizeable inline icons
Pages
- Home (
/) - Neofetch-style intro with navigation - About (
/about) - Background and details - Portfolio (
/portfolio) - Profile, experience, skills, and contact info - Projects (
/projects) - Hub linking out to the three project categories below- Open Source (
/projects/opensource) - Projects built and maintained in the open - Packages (
/projects/packages) - Published packages and CLI tools - Hackathons (
/projects/hackathons) - Hackathon projects, awards, and stats
- Open Source (
- Blog (
/blog) - Native markdown blog with tags, search, pagination, and an RSS feed at/blog/rss.xml - Models (
/models) - 3D model gallery with interactive viewer - Components (
/components) - Showcase of all TUI components - Links (
/links) - All links with on-demand QR codes
The navbar exposes Home, About, Projects, and Blog. Portfolio, Models, Components, and Links are reachable directly by URL or with the terminal cd command (for example, cd links).
Terminal Commands
Every terminal page has an interactive command line at the bottom. Click the terminal (or just start typing once focused), enter a command, and press Enter. Output is printed back into the terminal.
Use the Up/Down arrows to cycle through command history.
| Command | Aliases | Description |
|---|---|---|
help |
h, ? |
List all available commands |
clear |
cls |
Clear the terminal |
whoami |
Show a short bio | |
neofetch |
banner |
Display system info with profile picture |
skills |
List skills by category | |
contact |
socials |
Show social links as buttons |
email |
Show contact email | |
cd <page> |
goto, open |
Navigate to a page, including subpages (e.g. cd projects/opensource) |
ls |
dir |
List the pages you can visit as a tree, with project subpages nested under projects/ |
theme [name] |
List or set the color theme | |
mode [dark|light] |
Toggle or set dark/light mode | |
echo <text> |
Print text back | |
cowsay <text> |
Make a cow say something | |
date |
Show the current date and time (Eastern) |
Command definitions live in src/lib/components/tui/terminal-commands.ts. Add a new entry to the commands list to extend the command set. Pages and their subpages are declared once in the pageTree structure in that file, which both ls (tree rendering) and cd (nested path resolution) read from.
Configuration
The site configuration is modular - split into focused files in src/lib/config/ for easier maintenance. You can still import everything from $lib/config for backward compatibility.
Config File Structure
src/lib/config/
├── index.ts # Barrel export (re-exports all modules)
├── user.ts # User profile, socials, links, skills
├── layout.ts # Layout dimensions, breakpoints, fonts, navbar, scrollbar
├── theme.ts # Colors, animations, effects, loading screen
├── content.ts # Projects, 3D models, experience, hackathon cards
├── terminal.ts # Terminal settings, TUI styling, speed presets, shortcuts
├── navigation.ts # Navigation links, site metadata, page meta
└── blog.ts # Blog name and tagline shown on /blog
Import Examples
// Barrel import (backward compatible)
import { user, colorPalette, projects } from '$lib/config';
// Direct imports (smaller bundles, faster builds)
import { user, skills, links } from '$lib/config/user';
import { colorPalette, animations } from '$lib/config/theme';
import { openSourceProjects, cards, experience } from '$lib/config/content';
import { terminalSettings, keyboardShortcuts } from '$lib/config/terminal';
import { navigation, site, pageMeta } from '$lib/config/navigation';
Config Modules
| File | Contents |
|---|---|
user.ts |
user, skills, links |
layout.ts |
layout, breakpoints, fonts, navbar, scrollbar |
theme.ts |
colorPalette, terminalButtons, loadingScreen, effects, animations |
content.ts |
openSourceProjects, packageProjects, models, experience, cards, sortedCards + types |
terminal.ts |
terminalSettings, tuiStyle, tuiText, pageSpeedSettings, pageAutoscrollSettings, speedPresets, modelViewer, particles, keyboardShortcuts |
navigation.ts |
navigation, site, pageMeta |
blog.ts |
blogName, blogTagline |
Example: Key config snippets
// Toggle theme keys and other shortcuts
export const keyboardShortcuts = {
skip: ['y', 'Y'], // Skip typing animation
toggleTheme: ['t', 'T'], // Toggle dark/light mode
navigateUp: ['ArrowUp', 'k'],
navigateDown: ['ArrowDown', 'j'],
select: ['Enter'],
};
// Terminal / typing
export const terminalSettings = {
baseTypeSpeed: 20,
minTypeSpeed: 5,
maxTypeSpeed: 50,
startDelay: 300,
lineDelay: 100,
showCursor: true,
promptStyle: 'full',
scrollMargin: 80,
};
// Color palette (Catppuccin Mocha by default)
export const colorPalette = {
red: '#f38ba8',
green: '#a6e3a1',
yellow: '#f9e2af',
blue: '#89b4fa',
magenta: '#cba6f7',
cyan: '#94e2d5',
white: '#cdd6f4',
gray: '#6c7086',
error: '#f38ba8',
success: '#a6e3a1',
};
How to Customize
- Edit
config/user.tsto update your profile, socials, links, and skills - Edit
config/content.tsto add projects, models, experience, or hackathon entries - Edit
config/theme.tsto change colors, animations, or loading screen - Edit
config/terminal.tsto adjust typing speed, TUI styling, or shortcuts - Edit
config/layout.tsto change dimensions, breakpoints, or navbar settings - Edit
config/navigation.tsto add/remove nav links or update page metadata
Changes take effect on next reload. Some values (fonts, CSS variables) may also require adjusting CSS or Tailwind config.
Where to Look for Types & Utilities
src/lib/config/- All configuration modulessrc/lib/components/tui/types.ts- TerminalLine typessrc/lib/components/tui/utils.ts- Parsing utilities and style helperssrc/lib/components/tui/terminal-commands.ts- Interactive command definitionssrc/lib/stores/theme.ts- Theme store &toggleMode()src/lib/index.ts- Helper functions (barrel export)
Links Page
The /links page lists everything in the links array (a dedicated list, separate from user.socials) and renders a scannable QR code for whichever link is selected, plus a clickable button to open it.
Add or remove entries in src/lib/config/user.ts:
export const links: LinkItem[] = [
{ name: 'GitHub', icon: 'mdi:github', link: 'https://github.com/SirBlobby' },
{ name: 'Email', icon: 'mdi:email', link: `mailto:${user.email}` },
// Add page-only links here without touching user.socials
];
QR codes are generated with the svelte-qrcode package, loaded on the client.
Blog
The /blog page is a native markdown blog — no external blogging platform required.
Writing a Post
Add a markdown file to the blogs/ directory at the project root:
---
title: My Post Title
author: Your Name
date: 2026-01-01
tags: meta, notes
excerpt: A one line summary shown on the blog list and in the RSS feed.
---
Post content goes here, written in markdown.
Posts are picked up automatically via import.meta.glob in src/lib/blog/posts.ts — no manual registration needed. author and date are optional; author falls back to user.displayname.
Rendering
src/lib/blog/markdown.ts is a small hand-written markdown-to-HTML renderer (headings, bold/italic, links, images, lists, blockquotes, and fenced code blocks) — no external markdown dependency. Fenced code blocks with a language tag (```js, ```python, etc.) get lightweight syntax highlighting via src/lib/blog/highlight.ts, and every code block gets a copy-to-clipboard button.
Dates are parsed and formatted in Eastern time via src/lib/blog/date.ts, anchored at noon UTC to avoid off-by-one day shifts from timezone conversion.
Features
- Tags with click-to-filter on the blog list
- Full-text search across title, excerpt, and tags
- Pagination (5 posts per page)
- Previous/next post navigation on each post page
- RSS feed at
/blog/rss.xml(src/routes/blog/rss.xml/+server.ts)
Speed Presets
| Preset | Effect |
|---|---|
instant |
No animation, appears immediately |
fast |
3x faster than normal |
normal |
Default typing speed |
slow |
2x slower than normal |
typewriter |
3x slower, classic feel |
Text Formatting
Use inline formatting with the (&specs)text(&) syntax:
Colors
// Basic colors
'(&red)Red text(&)'
'(&green)Green text(&)'
'(&blue)Blue text(&)'
'(&yellow)Yellow text(&)'
'(&magenta)Magenta text(&)'
'(&cyan)Cyan text(&)'
'(&orange)Orange text(&)'
'(&pink)Pink text(&)'
'(&gray)Gray text(&)'
'(&white)White text(&)'
// Semantic colors (theme-aware)
'(&primary)Primary color(&)'
'(&accent)Accent color(&)'
'(&muted)Muted text(&)'
'(&error)Error text(&)'
'(&success)Success text(&)'
'(&warning)Warning text(&)'
'(&info)Info text(&)'
// Custom hex colors
'(&#ff6b6b)Custom color(&)'
Background Colors
Add bg- prefix to any color:
'(&bg-red)Red background(&)'
'(&bg-blue,white)Blue bg with white text(&)'
'(&bg-surface)Surface background(&)'
'(&bg-#333333)Custom bg color(&)'
Text Styles
'(&bold)Bold text(&)'
'(&italic)Italic text(&)'
'(&dim)Dimmed text (60% opacity)(&)'
'(&underline)Underlined text(&)'
'(&strikethrough)Strikethrough text(&)'
'(&strike)Strikethrough shorthand(&)'
'(&overline)Overlined text(&)'
Inline Icons
'(&icon, mdi:github) GitHub' // Inline icon, default size
'(&icon, mdi:trophy, 32) Winner' // Inline icon with a custom size in pixels
Combining Styles
Combine multiple styles with commas:
'(&bold,red)Bold red text(&)'
'(&italic,cyan,underline)Italic cyan underlined(&)'
'(&bg-blue,white,bold)Bold white on blue(&)'
'(&dim,strikethrough,gray)Dim gray strikethrough(&)'
Line Types
Basic Lines
const lines: TerminalLine[] = [
{ type: 'command', content: 'ls -la' }, // With prompt prefix
{ type: 'output', content: 'File listing...' }, // Muted text
{ type: 'error', content: 'Error message' }, // Red with prefix
{ type: 'success', content: 'Success!' }, // Green with prefix
{ type: 'info', content: 'Information' }, // Primary with prefix
{ type: 'header', content: 'Section Title' }, // Bold heading
{ type: 'blank', content: '' }, // Empty line
{ type: 'divider', content: 'SECTION', id: 'section' }, // Horizontal divider with anchor ID
];
Line Properties
All line types support these optional properties:
{
type: 'output',
content: 'Hello world',
id: 'my-section', // Anchor ID for URL hash scrolling (e.g., /page#my-section)
inline: true, // Render inline with adjacent inline elements
delay: 500, // Delay before this line appears (ms)
}
Button (Full-width interactive)
{
type: 'button',
content: 'Click me',
icon: 'mdi:github', // Iconify icon
style: 'primary', // primary | accent | warning | error
href: 'https://github.com', // URL to navigate to
external: true, // Open in new tab (auto-detected for http/https)
inline: true, // Render as compact inline button
// OR
action: () => doSomething(), // Custom action
border: false, // Disable default border (default: true)
flex: true, // Grow to fill available space in a row group (default: false)
}
Inline Elements
Multiple elements can be rendered on the same line using inline: true:
// These will appear on the same line
{ type: 'output', content: 'Status:', inline: true },
{ type: 'success', content: 'Online', inline: true },
{ type: 'button', content: 'Refresh', icon: 'mdi:refresh', inline: true },
// Next line without inline breaks the group
{ type: 'blank', content: '' },
Supported inline types: button, link, tooltip, progress, output, info, success, error, warning
Link (Inline clickable text)
{
type: 'link',
content: 'Visit GitHub',
icon: 'mdi:github', // Optional icon
style: 'accent', // Styling
href: 'https://github.com',
external: true, // Open in new tab (auto-detected for http/https)
}
Image
{
type: 'image',
content: 'Caption text', // Optional caption
image: '/path/to/image.png',
imageAlt: 'Alt text',
imageWidth: 300, // Max width in pixels
}
Card
{
type: 'card',
content: 'Card body text with (&bold)formatting(&) support',
cardTitle: 'Card Title', // Optional header
cardFooter: 'Footer text', // Optional footer
icon: 'mdi:star', // Optional header icon
image: '/path/to/image.png', // Optional card image
style: 'primary', // Border accent color
display: 'flex', // flex | grid | block (controls children layout)
children: [ // Optional nested elements
{ type: 'button', content: 'Action', style: 'primary' }
],
cardWidth: '1/2', // Width (string fraction, decimal, or px)
cardHeight: 300, // Height (string fraction, decimal, or px)
cardFloat: 'center', // start | center | end
}
Progress Bar
{
type: 'progress',
content: 'Loading assets...', // Label above bar
progress: 75, // 0-100 percentage
progressLabel: '75%', // Custom label (defaults to percentage)
style: 'accent', // Bar color
}
Accordion
{
type: 'accordion',
content: '',
accordionItems: [
{ title: 'Section 1', content: 'Content for section 1 with (&cyan)colors(&)' },
{ title: 'Section 2', content: 'Content for section 2' },
],
accordionOpen: true, // First item open by default
style: 'primary', // Accent color
}
Table
{
type: 'table',
content: 'Table Title', // Optional title
tableHeaders: ['Name', 'Role', 'Status'],
tableRows: [
['Alice', 'Developer', '(&success)Active(&)'],
['Bob', 'Designer', '(&warning)Away(&)'],
],
style: 'accent', // Header color
}
Tooltip
{
type: 'tooltip',
content: 'Hover me', // Trigger text
tooltipText: 'This is helpful information!',
tooltipPosition: 'top', // top | bottom | left | right
style: 'info', // Tooltip border color
}
Group
Groups allow you to arrange multiple elements together with custom layout:
{
type: 'group',
content: '',
groupDirection: 'row', // row | column (default: row)
groupAlign: 'start', // start | center | end
groupGap: '1rem', // CSS gap value
groupExpand: true, // Expand children to fill width (default: false)
inline: true, // Render inline with other elements
children: [
{ type: 'output', content: 'Label:', inline: true },
{ type: 'button', content: 'Action', style: 'primary', inline: true },
{ type: 'link', content: 'More info', href: '/help', inline: true }
]
}
Groups support nested groups and all element types as children. Children are rendered using the same TuiLine component, ensuring consistent behavior.
TUI Components
TerminalTUI
Main terminal component:
<TerminalTUI
lines={lines}
title="~/directory"
interactive={true}
speed="normal"
autoscroll={true}
enableCommands={true}
onComplete={() => console.log('Done!')}
/>
Props:
lines- Array of TerminalLine objectstitle- Terminal window titleinteractive- Enable keyboard navigationspeed- Typing speed preset or multiplierautoscroll- Auto-scroll as content types (default: true)enableCommands- Show the interactive command input (default: true)onComplete- Callback when typing animation finishes
Anchor Scrolling
Add id to any line to create an anchor that can be linked to:
{ type: 'divider', content: 'SKILLS', id: 'skills' },
Then link to it with /portfolio#skills - the page will scroll to that section after typing completes.
Terminal API
The TerminalTUI component exposes a reactive API for programmatic control via the terminal bindable prop.
Setup
<script lang="ts">
import TerminalTUI from '$lib/components/TerminalTUI.svelte';
import type { TerminalAPI, TerminalLine } from '$lib';
let terminal = $state<TerminalAPI>();
let lines = $state<TerminalLine[]>([
{ type: 'output', content: 'Hello world!' }
]);
</script>
<TerminalTUI bind:terminal bind:lines title="~/demo" />
API Methods
Writing Lines
| Method | Description |
|---|---|
terminal.write(line) |
Append a single line |
terminal.writeLines(lines) |
Append multiple lines |
terminal.clear() |
Remove all lines |
terminal.setLines(lines) |
Replace all lines |
Updating Lines
| Method | Description |
|---|---|
terminal.update(index, updates) |
Update line by index with partial changes |
terminal.updateContent(index, content) |
Update just the content of a line |
terminal.updateById(id, updates) |
Update line by its id property |
Removing Lines
| Method | Description |
|---|---|
terminal.remove(index) |
Remove line at index |
terminal.removeRange(start, count) |
Remove a range of lines |
terminal.removeById(id) |
Remove line by its id property |
Inserting Lines
| Method | Description |
|---|---|
terminal.insert(index, line) |
Insert line at a specific index |
Reading State
| Method | Description |
|---|---|
terminal.getLine(index) |
Get line at index |
terminal.getLines() |
Get all lines (copy) |
terminal.getLineCount() |
Get number of lines |
terminal.findById(id) |
Find index of line by id |
terminal.isAnimating() |
Check if typing animation is active |
Navigation & Control
| Method | Description |
|---|---|
terminal.scrollToBottom() |
Scroll to bottom of terminal |
terminal.scrollToLine(index) |
Scroll to specific line |
terminal.skip() |
Skip current typing animation |
terminal.restart() |
Restart typing animation from beginning |
Example: Dynamic Updates
<script lang="ts">
import TerminalTUI from '$lib/components/TerminalTUI.svelte';
import type { TerminalAPI } from '$lib';
let terminal = $state<TerminalAPI>();
let lines = $state([
{ type: 'output', content: 'Initializing...', id: 'status' }
]);
async function runProcess() {
terminal?.updateById('status', {
type: 'info',
content: 'Processing...'
});
await delay(1000);
terminal?.write({
type: 'progress',
content: '',
progress: 50,
progressLabel: '50%'
});
await delay(1000);
terminal?.updateById('status', {
type: 'success',
content: 'Complete!'
});
}
</script>
<TerminalTUI bind:terminal bind:lines />
<button onclick={runProcess}>Start</button>
3D Model Viewer
The ModelViewer component provides an interactive Three.js viewer for .glb models.
Features
- Mouse Controls: Drag to rotate, scroll to zoom
- Arrow Key Controls: Use arrow keys to orbit the camera (click viewer to focus first)
- Auto-rotate: Toggle automatic rotation
- Wireframe Mode: View model wireframe
- Adjustable Lighting: Increase/decrease scene brightness
- Fullscreen Mode: Expand to full viewport (press Escape to exit)
- Ground Plane: Optional shadow-receiving ground
- Graceful Fallback: Shows an error message when WebGL is unavailable instead of crashing
Usage
<ModelViewer
modelPath="/models/my-model.glb"
modelName="My Model"
/>
Place .glb files in /static/models/ and they'll be accessible at /models/filename.glb.
Tech Stack
- Framework: SvelteKit 2.x with Svelte 5 runes
- Styling: Tailwind CSS 4.x
- 3D: Three.js with GLTFLoader
- QR codes: svelte-qrcode
- Icons: @iconify/svelte
- Font: JetBrains Mono
- Runtime: Bun
Development
# Install dependencies
bun install
# Start dev server
bun run dev
# Build for production
bun run build
# Type-check
bun run check
Keyboard Shortcuts
| Key | Action |
|---|---|
| Up / k | Navigate up |
| Down / j | Navigate down |
| Enter | Activate button |
| Y | Skip typing animation |
| T | Toggle dark/light mode |
When the command input is focused, these keys type normally instead of triggering navigation.
3D Model Viewer
| Key | Action |
|---|---|
| Left | Rotate camera left |
| Right | Rotate camera right |
| Up | Rotate camera up |
| Down | Rotate camera down |
| Escape | Exit fullscreen |
Theme System
Themes are defined as JSON files in src/lib/assets/themes/. Each theme contains colors for both dark and light modes. Shipped themes: Arch, Catppuccin, Wintry, Rose, and Cerberus (src/lib/stores/theme.ts).
Theme File Structure
{
"name": "Theme Name",
"icon": "arch",
"dark": {
"colors": {
"primary": "#89b4fa",
"secondary": "#313244",
"accent": "#a6e3a1",
"background": "#1e1e2e",
"backgroundLight": "#313244",
"text": "#cdd6f4",
"textMuted": "#a6adc8",
"border": "#45475a",
"terminal": "#1e1e2e",
"terminalPrompt": "#cba6f7",
"terminalUser": "#a6e3a1",
"terminalPath": "#89b4fa"
},
"colorMap": {
"red": "#f38ba8",
"green": "#a6e3a1",
"blue": "#89b4fa",
"primary": "var(--terminal-primary)",
"accent": "var(--terminal-accent)",
"muted": "var(--terminal-muted)"
}
},
"light": {
"colors": { },
"colorMap": { }
}
}
Adding a New Theme
- Create a new file:
src/lib/assets/themes/mytheme.theme.json - Import it in
src/lib/stores/theme.ts:import myTheme from '$lib/assets/themes/mytheme.theme.json'; - Add it to the themes object:
const themes: Record<ColorTheme, ThemeJson> = { arch: archTheme, catppuccin: catppuccinTheme, mytheme: myTheme as ThemeJson }; - Update the
ColorThemetype to include your theme name
Theme-Specific Colors
Beyond the basic colors, themes include:
teal,sky,sapphire,lavenderpeach,maroon,mauveflamingo,rosewater
// These colors adapt to the current theme
'(&teal)Teal text(&)'
'(&lavender)Lavender text(&)'
'(&peach)Peach text(&)'
Mobile Considerations
- Viewport height: Uses
100dvh(dynamic viewport height) to properly handle mobile browser chrome - Background color: A fallback dark background (
#1e1e2e) is set onhtmlandbodyto prevent white bars when the page content doesn't fill the viewport - Overflow handling: Hidden horizontal scrollbar to prevent accidental horizontal scroll on mobile