Rewrite website as a Hyprland desktop
This commit is contained in:
1 parent
7c52779cf6
commit
3065ad6092
197 files changed
+5609
-17779
No files matched your search
@@ -0,0 +1,188 @@
|
||||
<script lang="ts">
|
||||
import { onMount, tick } from 'svelte';
|
||||
import { profile } from '#lib/content';
|
||||
import { openPage, openUrl } from '#lib/desktop/actions';
|
||||
import { themeState } from '#lib/theme/theme.svelte';
|
||||
import { runCommand, type CommandContext } from '#lib/terminal/commands';
|
||||
import { complete } from '#lib/terminal/completion';
|
||||
import { line, span, type OutputBlock } from '#lib/terminal/output';
|
||||
import OutputLine from './OutputLine.svelte';
|
||||
import Fetch from './Fetch.svelte';
|
||||
|
||||
let { startupCommand = '', focused, onexit }: { startupCommand?: string; focused: boolean; onexit: () => void } =
|
||||
$props();
|
||||
|
||||
const typingDelay = 35;
|
||||
|
||||
let blocks = $state<OutputBlock[]>([]);
|
||||
let input = $state('');
|
||||
let isTyping = $state(false);
|
||||
let history: string[] = [];
|
||||
let historyIndex = 0;
|
||||
let inputElement = $state<HTMLInputElement>();
|
||||
let screenElement = $state<HTMLDivElement>();
|
||||
|
||||
const context: CommandContext = {
|
||||
print: (...newBlocks) => blocks.push(...newBlocks),
|
||||
clear: () => (blocks = []),
|
||||
exit: onexit,
|
||||
openPage,
|
||||
openUrl,
|
||||
setTheme: (themeId) => themeState.set(themeId),
|
||||
currentThemeId: () => themeState.currentId
|
||||
};
|
||||
|
||||
async function execute(command: string) {
|
||||
blocks.push({ kind: 'prompt', command });
|
||||
if (command.trim()) history.push(command);
|
||||
historyIndex = history.length;
|
||||
runCommand(command, context);
|
||||
await tick();
|
||||
screenElement?.scrollTo({ top: screenElement.scrollHeight });
|
||||
}
|
||||
|
||||
async function typeOut(command: string) {
|
||||
isTyping = true;
|
||||
for (const character of command) {
|
||||
input += character;
|
||||
await new Promise((resolve) => setTimeout(resolve, typingDelay));
|
||||
}
|
||||
input = '';
|
||||
isTyping = false;
|
||||
await execute(command);
|
||||
}
|
||||
|
||||
async function showCompletion() {
|
||||
const result = complete(input);
|
||||
input = result.input;
|
||||
if (result.options.length === 0) return;
|
||||
blocks.push({ kind: 'prompt', command: input }, line(span(result.options.join(' '), 'cyan')));
|
||||
await tick();
|
||||
screenElement?.scrollTo({ top: screenElement.scrollHeight });
|
||||
}
|
||||
|
||||
function recallHistory(step: number) {
|
||||
if (history.length === 0) return;
|
||||
historyIndex = Math.min(history.length, Math.max(0, historyIndex + step));
|
||||
input = history[historyIndex] ?? '';
|
||||
}
|
||||
|
||||
function handleKeydown(event: KeyboardEvent) {
|
||||
if (isTyping) {
|
||||
event.preventDefault();
|
||||
return;
|
||||
}
|
||||
if (event.key === 'Enter') {
|
||||
const command = input;
|
||||
input = '';
|
||||
execute(command);
|
||||
} else if (event.key === 'ArrowUp') {
|
||||
event.preventDefault();
|
||||
recallHistory(-1);
|
||||
} else if (event.key === 'ArrowDown') {
|
||||
event.preventDefault();
|
||||
recallHistory(1);
|
||||
} else if (event.key === 'Tab') {
|
||||
event.preventDefault();
|
||||
showCompletion();
|
||||
} else if (event.key === 'l' && event.ctrlKey) {
|
||||
event.preventDefault();
|
||||
context.clear();
|
||||
}
|
||||
}
|
||||
|
||||
function focusInput() {
|
||||
if (!window.getSelection()?.toString()) inputElement?.focus({ preventScroll: true });
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
if (focused) inputElement?.focus({ preventScroll: true });
|
||||
});
|
||||
|
||||
onMount(() => {
|
||||
if (startupCommand) typeOut(startupCommand);
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="terminal" bind:this={screenElement} onclick={focusInput} role="presentation">
|
||||
{#each blocks as block, index (index)}
|
||||
{#if block.kind === 'prompt'}
|
||||
<div class="prompt-line">
|
||||
<span class="prompt">{profile.username}@{profile.hostname}</span>
|
||||
<span class="path">~</span>
|
||||
<span class="symbol">$</span>
|
||||
<span>{block.command}</span>
|
||||
</div>
|
||||
{:else if block.kind === 'fetch'}
|
||||
<Fetch />
|
||||
{:else}
|
||||
<OutputLine spans={block.spans} />
|
||||
{/if}
|
||||
{/each}
|
||||
|
||||
<label class="prompt-line input-line">
|
||||
<span class="prompt">{profile.username}@{profile.hostname}</span>
|
||||
<span class="path">~</span>
|
||||
<span class="symbol">$</span>
|
||||
<span class="input-wrapper">
|
||||
<input
|
||||
bind:this={inputElement}
|
||||
bind:value={input}
|
||||
onkeydown={handleKeydown}
|
||||
spellcheck="false"
|
||||
autocomplete="off"
|
||||
autocapitalize="off"
|
||||
aria-label="Terminal input"
|
||||
/>
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.terminal {
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
padding: 0.75rem 1.1rem 1.5rem;
|
||||
line-height: 1.5;
|
||||
cursor: text;
|
||||
}
|
||||
|
||||
.prompt-line {
|
||||
display: flex;
|
||||
gap: 0.5ch;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.prompt {
|
||||
color: var(--green);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.path {
|
||||
color: var(--blue);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.symbol {
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.input-wrapper {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
input {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
padding: 0;
|
||||
background: none;
|
||||
border: none;
|
||||
outline: none;
|
||||
box-shadow: none;
|
||||
color: var(--foreground);
|
||||
font: inherit;
|
||||
caret-color: var(--foreground);
|
||||
}
|
||||
</style>
|
||||
Reference in new issue
Block a user