Element Bug Fixes
This commit is contained in:
@@ -1,18 +1,34 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { themeColors } from '$lib/stores/theme';
|
||||
import { terminalSettings, type SpeedPreset, speedPresets } from '$lib/config';
|
||||
import TuiHeader from './tui/TuiHeader.svelte';
|
||||
import TuiBody from './tui/TuiBody.svelte';
|
||||
import TuiFooter from './tui/TuiFooter.svelte';
|
||||
import '$lib/assets/css/terminal-tui.css';
|
||||
import { onMount } from "svelte";
|
||||
import { themeColors } from "$lib/stores/theme";
|
||||
import {
|
||||
terminalSettings,
|
||||
type SpeedPreset,
|
||||
speedPresets,
|
||||
} from "$lib/config";
|
||||
import TuiHeader from "./tui/TuiHeader.svelte";
|
||||
import TuiBody from "./tui/TuiBody.svelte";
|
||||
import TuiFooter from "./tui/TuiFooter.svelte";
|
||||
import "$lib/assets/css/terminal-tui.css";
|
||||
|
||||
// Import extracted modules
|
||||
import { parseLine, createTerminalAPI } from './tui/terminal-api';
|
||||
import { runTypingAnimation, skipTypingAnimation } from './tui/terminal-typing';
|
||||
import { createKeyboardHandler, handleNavigation, scrollToHash } from './tui/terminal-keyboard';
|
||||
import { parseLine, createTerminalAPI } from "./tui/terminal-api";
|
||||
import {
|
||||
runTypingAnimation,
|
||||
skipTypingAnimation,
|
||||
} from "./tui/terminal-typing";
|
||||
import {
|
||||
createKeyboardHandler,
|
||||
handleNavigation,
|
||||
scrollToHash,
|
||||
} from "./tui/terminal-keyboard";
|
||||
|
||||
import type { TerminalLine, ParsedLine, DisplayedLine, TerminalAPI } from './tui/types';
|
||||
import type {
|
||||
TerminalLine,
|
||||
ParsedLine,
|
||||
DisplayedLine,
|
||||
TerminalAPI,
|
||||
} from "./tui/types";
|
||||
|
||||
interface Props {
|
||||
lines?: TerminalLine[];
|
||||
@@ -25,20 +41,20 @@
|
||||
terminal?: TerminalAPI;
|
||||
}
|
||||
|
||||
let {
|
||||
lines = $bindable([]),
|
||||
title = 'terminal',
|
||||
class: className = '',
|
||||
let {
|
||||
lines = $bindable([]),
|
||||
title = "terminal",
|
||||
class: className = "",
|
||||
onComplete,
|
||||
interactive = true,
|
||||
speed = 'normal',
|
||||
speed = "normal",
|
||||
autoscroll = true,
|
||||
terminal = $bindable()
|
||||
terminal = $bindable(),
|
||||
}: Props = $props();
|
||||
|
||||
// Calculate speed multiplier from preset or number
|
||||
const speedMultiplier = $derived(
|
||||
typeof speed === 'number' ? speed : (speedPresets[speed] ?? 1)
|
||||
typeof speed === "number" ? speed : (speedPresets[speed] ?? 1),
|
||||
);
|
||||
|
||||
// Get colorMap from current theme
|
||||
@@ -46,7 +62,7 @@
|
||||
|
||||
// Pre-parse all lines upfront (segments + plain text)
|
||||
const parsedLines = $derived<ParsedLine[]>(
|
||||
lines.map(line => parseLine(line, colorMap))
|
||||
lines.map((line) => parseLine(line, colorMap)),
|
||||
);
|
||||
|
||||
let displayedLines = $state<DisplayedLine[]>([]);
|
||||
@@ -59,35 +75,39 @@
|
||||
let bodyElement = $state<HTMLDivElement>();
|
||||
|
||||
// Track colorMap identity to detect theme/mode changes
|
||||
let lastColorMapId = $state('');
|
||||
|
||||
let lastColorMapId = $state("");
|
||||
|
||||
// When colorMap changes (theme/mode toggle), update displayedLines with new parsed segments
|
||||
$effect(() => {
|
||||
// Create a simple identity string from a few colorMap values
|
||||
const colorMapId = `${colorMap.red}-${colorMap.text}-${colorMap.primary}`;
|
||||
|
||||
|
||||
// Only update if colorMap actually changed and animation is complete
|
||||
if (colorMapId !== lastColorMapId && isComplete && displayedLines.length > 0) {
|
||||
if (
|
||||
colorMapId !== lastColorMapId &&
|
||||
isComplete &&
|
||||
displayedLines.length > 0
|
||||
) {
|
||||
// Store current showImage states before updating
|
||||
const showImageStates = displayedLines.map(d => d.showImage);
|
||||
|
||||
const showImageStates = displayedLines.map((d) => d.showImage);
|
||||
|
||||
// Update with new parsed content
|
||||
displayedLines = parsedLines.map((parsed, i) => ({
|
||||
parsed,
|
||||
charIndex: parsed.plainText.length,
|
||||
complete: true,
|
||||
showImage: showImageStates[i] ?? (parsed.line.type === 'image')
|
||||
showImage: showImageStates[i] ?? parsed.line.type === "image",
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
lastColorMapId = colorMapId;
|
||||
});
|
||||
|
||||
// Helper to check if a line or its children contain buttons
|
||||
function hasButtons(line: TerminalLine): boolean {
|
||||
if (line.type === 'button') return true;
|
||||
if (line.type === 'group' && line.children) {
|
||||
return line.children.some(child => hasButtons(child));
|
||||
if (line.type === "button") return true;
|
||||
if (line.type === "group" && line.children) {
|
||||
return line.children.some((child) => hasButtons(child));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -95,8 +115,8 @@
|
||||
// Get all interactive button indices (including buttons nested in groups)
|
||||
const buttonIndices = $derived(
|
||||
displayedLines
|
||||
.map((item, i) => hasButtons(item.parsed.line) ? i : -1)
|
||||
.filter(i => i !== -1)
|
||||
.map((item, i) => (hasButtons(item.parsed.line) ? i : -1))
|
||||
.filter((i) => i !== -1),
|
||||
);
|
||||
|
||||
// ========================================================================
|
||||
@@ -112,17 +132,29 @@
|
||||
interactive,
|
||||
autoscroll,
|
||||
getBodyElement: () => bodyElement,
|
||||
getState: () => ({ displayedLines, currentLineIndex, isTyping, isComplete, skipRequested, selectedIndex }),
|
||||
getState: () => ({
|
||||
displayedLines,
|
||||
currentLineIndex,
|
||||
isTyping,
|
||||
isComplete,
|
||||
skipRequested,
|
||||
selectedIndex,
|
||||
}),
|
||||
setState: (updates) => {
|
||||
if (updates.displayedLines !== undefined) displayedLines = updates.displayedLines;
|
||||
if (updates.currentLineIndex !== undefined) currentLineIndex = updates.currentLineIndex;
|
||||
if (updates.displayedLines !== undefined)
|
||||
displayedLines = updates.displayedLines;
|
||||
if (updates.currentLineIndex !== undefined)
|
||||
currentLineIndex = updates.currentLineIndex;
|
||||
if (updates.isTyping !== undefined) isTyping = updates.isTyping;
|
||||
if (updates.isComplete !== undefined) isComplete = updates.isComplete;
|
||||
if (updates.skipRequested !== undefined) skipRequested = updates.skipRequested;
|
||||
if (updates.selectedIndex !== undefined) selectedIndex = updates.selectedIndex;
|
||||
if (updates.isComplete !== undefined)
|
||||
isComplete = updates.isComplete;
|
||||
if (updates.skipRequested !== undefined)
|
||||
skipRequested = updates.skipRequested;
|
||||
if (updates.selectedIndex !== undefined)
|
||||
selectedIndex = updates.selectedIndex;
|
||||
},
|
||||
onComplete,
|
||||
scrollToHash: () => scrollToHash(bodyElement)
|
||||
scrollToHash: () => scrollToHash(bodyElement),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -131,18 +163,30 @@
|
||||
parsedLines,
|
||||
buttonIndices,
|
||||
interactive,
|
||||
() => ({ displayedLines, currentLineIndex, isTyping, isComplete, skipRequested, selectedIndex }),
|
||||
() => ({
|
||||
displayedLines,
|
||||
currentLineIndex,
|
||||
isTyping,
|
||||
isComplete,
|
||||
skipRequested,
|
||||
selectedIndex,
|
||||
}),
|
||||
(updates) => {
|
||||
if (updates.displayedLines !== undefined) displayedLines = updates.displayedLines;
|
||||
if (updates.currentLineIndex !== undefined) currentLineIndex = updates.currentLineIndex;
|
||||
if (updates.displayedLines !== undefined)
|
||||
displayedLines = updates.displayedLines;
|
||||
if (updates.currentLineIndex !== undefined)
|
||||
currentLineIndex = updates.currentLineIndex;
|
||||
if (updates.isTyping !== undefined) isTyping = updates.isTyping;
|
||||
if (updates.isComplete !== undefined) isComplete = updates.isComplete;
|
||||
if (updates.skipRequested !== undefined) skipRequested = updates.skipRequested;
|
||||
if (updates.selectedIndex !== undefined) selectedIndex = updates.selectedIndex;
|
||||
if (updates.isComplete !== undefined)
|
||||
isComplete = updates.isComplete;
|
||||
if (updates.skipRequested !== undefined)
|
||||
skipRequested = updates.skipRequested;
|
||||
if (updates.selectedIndex !== undefined)
|
||||
selectedIndex = updates.selectedIndex;
|
||||
},
|
||||
() => bodyElement,
|
||||
autoscroll,
|
||||
onComplete
|
||||
onComplete,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -158,20 +202,25 @@
|
||||
isComplete,
|
||||
currentLineIndex,
|
||||
selectedIndex,
|
||||
skipRequested
|
||||
skipRequested,
|
||||
}),
|
||||
setState: (updates) => {
|
||||
if (updates.lines !== undefined) lines = updates.lines;
|
||||
if (updates.displayedLines !== undefined) displayedLines = updates.displayedLines;
|
||||
if (updates.displayedLines !== undefined)
|
||||
displayedLines = updates.displayedLines;
|
||||
if (updates.isTyping !== undefined) isTyping = updates.isTyping;
|
||||
if (updates.isComplete !== undefined) isComplete = updates.isComplete;
|
||||
if (updates.currentLineIndex !== undefined) currentLineIndex = updates.currentLineIndex;
|
||||
if (updates.selectedIndex !== undefined) selectedIndex = updates.selectedIndex;
|
||||
if (updates.skipRequested !== undefined) skipRequested = updates.skipRequested;
|
||||
if (updates.isComplete !== undefined)
|
||||
isComplete = updates.isComplete;
|
||||
if (updates.currentLineIndex !== undefined)
|
||||
currentLineIndex = updates.currentLineIndex;
|
||||
if (updates.selectedIndex !== undefined)
|
||||
selectedIndex = updates.selectedIndex;
|
||||
if (updates.skipRequested !== undefined)
|
||||
skipRequested = updates.skipRequested;
|
||||
},
|
||||
getColorMap: () => colorMap,
|
||||
getBodyElement: () => bodyElement,
|
||||
typeText
|
||||
typeText,
|
||||
});
|
||||
|
||||
// ========================================================================
|
||||
@@ -184,20 +233,29 @@
|
||||
getInteractive: () => interactive,
|
||||
getButtonIndices: () => buttonIndices,
|
||||
getSelectedIndex: () => selectedIndex,
|
||||
setSelectedIndex: (index) => { selectedIndex = index; },
|
||||
setSelectedIndex: (index) => {
|
||||
selectedIndex = index;
|
||||
},
|
||||
getDisplayedLines: () => displayedLines,
|
||||
getBodyElement: () => bodyElement,
|
||||
skipAnimation,
|
||||
scrollMargin: terminalSettings.scrollMargin
|
||||
scrollMargin: terminalSettings.scrollMargin,
|
||||
});
|
||||
|
||||
function handleButtonClick(index: number) {
|
||||
selectedIndex = index;
|
||||
handleNavigation(displayedLines[index]?.parsed.line);
|
||||
function handleButtonClick(index: number, line?: TerminalLine) {
|
||||
if (line) {
|
||||
// If line is provided (e.g. from nested group), use it directly
|
||||
// We don't update selectedIndex because it might not map to a top-level line
|
||||
handleNavigation(line as any);
|
||||
} else {
|
||||
// Fallback to top-level index lookup
|
||||
selectedIndex = index;
|
||||
handleNavigation(displayedLines[index]?.parsed.line as any);
|
||||
}
|
||||
}
|
||||
|
||||
function handleLinkClick(index: number) {
|
||||
handleNavigation(displayedLines[index]?.parsed.line);
|
||||
handleNavigation(displayedLines[index]?.parsed.line as any);
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
@@ -207,7 +265,7 @@
|
||||
|
||||
<svelte:window on:keydown={handleKeydown} />
|
||||
|
||||
<div
|
||||
<div
|
||||
class="tui-terminal {className}"
|
||||
style="
|
||||
--terminal-bg: {$themeColors.terminal};
|
||||
@@ -228,23 +286,32 @@
|
||||
>
|
||||
<!-- Hyprland-style border glow -->
|
||||
<div class="tui-border-glow"></div>
|
||||
|
||||
|
||||
<!-- TUI Content -->
|
||||
<div class="tui-content">
|
||||
<TuiHeader {title} {interactive} hasButtons={buttonIndices.length > 0} />
|
||||
<TuiHeader
|
||||
{title}
|
||||
{interactive}
|
||||
hasButtons={buttonIndices.length > 0}
|
||||
/>
|
||||
|
||||
<!-- Main terminal area -->
|
||||
<TuiBody bind:ref={bodyElement}
|
||||
<TuiBody
|
||||
bind:ref={bodyElement}
|
||||
{displayedLines}
|
||||
{currentLineIndex}
|
||||
{isTyping}
|
||||
{selectedIndex}
|
||||
onButtonClick={handleButtonClick}
|
||||
onHoverButton={(i) => selectedIndex = i}
|
||||
onHoverButton={(i) => (selectedIndex = i)}
|
||||
onLinkClick={handleLinkClick}
|
||||
terminalSettings={terminalSettings}
|
||||
{terminalSettings}
|
||||
/>
|
||||
|
||||
<TuiFooter isTyping={isTyping} linesCount={displayedLines.length} skipAnimation={skipAnimation} />
|
||||
<TuiFooter
|
||||
{isTyping}
|
||||
linesCount={displayedLines.length}
|
||||
{skipAnimation}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user