168 lines
4.2 KiB
Svelte
168 lines
4.2 KiB
Svelte
<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>
|