Files
website/src/lib/components/Terminal.svelte
T
2025-11-28 19:44:11 +00:00

168 lines
4.2 KiB
Svelte
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script lang="ts">
import { onMount } from 'svelte';
import { themeColors } from '$lib/stores/theme';
import '$lib/assets/css/terminal.css';
interface Props {
lines?: TerminalLine[];
typeSpeed?: number;
startDelay?: number;
showCursor?: boolean;
autoType?: boolean;
class?: string;
}
interface TerminalLine {
type: 'command' | 'output' | 'prompt' | 'error' | 'success' | 'info';
content: string;
delay?: number;
}
let {
lines = [],
typeSpeed = 30,
startDelay = 500,
showCursor = true,
autoType = true,
class: className = ''
}: Props = $props();
let displayedLines = $state<{ line: TerminalLine; text: string; complete: boolean }[]>([]);
let currentLineIndex = $state(0);
let currentCharIndex = $state(0);
let isTyping = $state(false);
let terminalElement: HTMLDivElement;
function sleep(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
async function typeText() {
if (!autoType || lines.length === 0) {
displayedLines = lines.map(line => ({ line, text: line.content, complete: true }));
return;
}
isTyping = true;
await sleep(startDelay);
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
displayedLines = [...displayedLines, { line, text: '', complete: false }];
currentLineIndex = i;
if (line.delay) {
await sleep(line.delay);
}
// Type out the line character by character
for (let j = 0; j <= line.content.length; j++) {
currentCharIndex = j;
displayedLines[i] = {
line,
text: line.content.slice(0, j),
complete: j === line.content.length
};
// Scroll to bottom
if (terminalElement) {
terminalElement.scrollTop = terminalElement.scrollHeight;
}
if (j < line.content.length) {
await sleep(line.type === 'output' ? typeSpeed / 3 : typeSpeed);
}
}
displayedLines[i].complete = true;
// Pause between lines
if (i < lines.length - 1) {
await sleep(150);
}
}
isTyping = false;
}
onMount(() => {
typeText();
});
function getLinePrefix(type: TerminalLine['type']): string {
switch (type) {
case 'command':
case 'prompt':
return '';
case 'error':
return '✗ ';
case 'success':
return '✓ ';
case 'info':
return ' ';
default:
return '';
}
}
</script>
<div
class="terminal {className}"
style="
--terminal-bg: {$themeColors.terminal};
--terminal-text: {$themeColors.text};
--terminal-muted: {$themeColors.textMuted};
--terminal-border: {$themeColors.border};
--terminal-prompt: {$themeColors.terminalPrompt};
--terminal-user: {$themeColors.terminalUser};
--terminal-path: {$themeColors.terminalPath};
--terminal-primary: {$themeColors.primary};
--terminal-secondary: {$themeColors.secondary};
--terminal-accent: {$themeColors.accent};
--terminal-bg-light: {$themeColors.backgroundLight};
"
bind:this={terminalElement}
>
<!-- Terminal Header -->
<div class="terminal-header">
<div class="terminal-buttons">
<span class="btn close"></span>
<span class="btn minimize"></span>
<span class="btn maximize"></span>
</div>
<div class="terminal-title">
<span class="terminal-icon">🐧</span>
<span>arch@terminal: ~</span>
</div>
<div class="terminal-spacer"></div>
</div>
<!-- Terminal Body -->
<div class="terminal-body">
{#each displayedLines as { line, text, complete }, i}
<div class="terminal-line {line.type}">
{#if line.type === 'command' || line.type === 'prompt'}
<span class="prompt">
<span class="user">user@arch</span><span class="separator">:</span><span class="path">~</span><span class="symbol">$</span>
</span>
{/if}
<span class="content">
{getLinePrefix(line.type)}{text}
</span>
{#if showCursor && i === currentLineIndex && !complete && isTyping}
<span class="cursor"></span>
{/if}
</div>
{/each}
{#if showCursor && !isTyping && displayedLines.length > 0}
<div class="terminal-line prompt">
<span class="prompt">
<span class="user">user@arch</span><span class="separator">:</span><span class="path">~</span><span class="symbol">$</span>
</span>
<span class="cursor"></span>
</div>
{/if}
</div>
</div>