Files
atridotdad/src/components/Terminal.tsx
Atridad Lahiji 41614a49a8
All checks were successful
Docker Deploy / build-and-push (push) Successful in 3m26s
Fixed spotify
2025-06-19 23:34:44 -06:00

235 lines
8.0 KiB
TypeScript

import { useState, useEffect, useRef } from "preact/hooks";
import type { JSX } from "preact";
import type { Command } from "../utils/terminal/types";
import { buildFileSystem } from "../utils/terminal/fs";
import {
executeCommand,
type CommandContext,
} from "../utils/terminal/commands";
import {
getCompletions,
formatOutput,
saveCommandToHistory,
loadCommandHistory,
} from "../utils/terminal/utils";
const Terminal = () => {
const [currentPath, setCurrentPath] = useState("/");
const [commandHistory, setCommandHistory] = useState<Command[]>([
{
input: "",
output:
'Welcome to Atridad\'s Shell!\nType "help" to see available commands.\n',
timestamp: new Date(),
path: "/",
},
]);
const [currentInput, setCurrentInput] = useState("");
const [historyIndex, setHistoryIndex] = useState(-1);
const [fileSystem, setFileSystem] = useState<{ [key: string]: any }>({});
const [isTrainRunning, setIsTrainRunning] = useState(false);
const [trainPosition, setTrainPosition] = useState(100);
const [persistentHistory, setPersistentHistory] = useState<string[]>([]);
const inputRef = useRef<HTMLInputElement>(null);
const terminalRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (terminalRef.current && !isTrainRunning) {
terminalRef.current.scrollTop = terminalRef.current.scrollHeight;
}
}, [commandHistory, isTrainRunning]);
// Load command history from localStorage
useEffect(() => {
const history = loadCommandHistory();
setPersistentHistory(history);
}, []);
// Initialize file system
useEffect(() => {
buildFileSystem().then(setFileSystem);
}, []);
const handleSubmit = (e: JSX.TargetedEvent<HTMLFormElement>) => {
e.preventDefault();
const commandContext: CommandContext = {
currentPath,
fileSystem,
setCurrentPath,
setIsTrainRunning,
setTrainPosition,
};
const output = executeCommand(currentInput, commandContext);
const newCommand: Command = {
input: currentInput,
output,
timestamp: new Date(),
path: currentPath,
};
// Save command to persistent history
const updatedHistory = saveCommandToHistory(
currentInput,
persistentHistory,
);
setPersistentHistory(updatedHistory);
if (currentInput.trim().toLowerCase() === "clear") {
setCommandHistory([]);
} else {
setCommandHistory((prev: Command[]) => [...prev, newCommand]);
}
setCurrentInput("");
setHistoryIndex(-1);
};
const handleKeyDown = (e: JSX.TargetedKeyboardEvent<HTMLInputElement>) => {
if (e.key === "Tab") {
e.preventDefault();
const { completion, replaceFrom } = getCompletions(
currentInput,
currentPath,
fileSystem,
);
if (completion) {
const beforeReplacement = currentInput.substring(0, replaceFrom);
const newInput = beforeReplacement + completion;
setCurrentInput(newInput + (completion.endsWith("/") ? "" : " "));
}
} else if (e.key === "ArrowUp") {
e.preventDefault();
if (persistentHistory.length > 0) {
const newIndex =
historyIndex === -1
? persistentHistory.length - 1
: Math.max(0, historyIndex - 1);
setHistoryIndex(newIndex);
setCurrentInput(persistentHistory[newIndex]);
}
} else if (e.key === "ArrowDown") {
e.preventDefault();
if (historyIndex !== -1) {
const newIndex = Math.min(
persistentHistory.length - 1,
historyIndex + 1,
);
if (
newIndex === persistentHistory.length - 1 &&
historyIndex === newIndex
) {
setHistoryIndex(-1);
setCurrentInput("");
} else {
setHistoryIndex(newIndex);
setCurrentInput(persistentHistory[newIndex]);
}
}
}
};
return (
<div className="bg-base-100 text-base-content font-mono text-sm h-full flex flex-col rounded-lg border-2 border-primary shadow-2xl relative">
<div className="bg-base-200 px-4 py-2 rounded-t-lg border-b border-base-300">
<div className="flex items-center space-x-2">
<div className="w-3 h-3 bg-error rounded-full"></div>
<div className="w-3 h-3 bg-warning rounded-full"></div>
<div className="w-3 h-3 bg-success rounded-full"></div>
<span className="ml-4 text-base-content/70 text-xs">
guest@atri.dad: {currentPath}
</span>
</div>
</div>
<div
ref={terminalRef}
className={`flex-1 p-4 overflow-y-auto scrollbar-thin scrollbar-thumb-base-300 scrollbar-track-base-100 relative ${
isTrainRunning ? "opacity-0" : "opacity-100"
}`}
onClick={() => !isTrainRunning && inputRef.current?.focus()}
>
<div className="min-h-full">
{commandHistory.map((command: Command, index: number) => (
<div key={index} className="mb-2">
{command.input && (
<div className="flex items-center">
<span className="text-primary font-semibold">
guest@atri.dad
</span>
<span className="text-base-content">:</span>
<span className="text-secondary font-semibold">
{command.path}
</span>
<span className="text-base-content">$ </span>
<span className="text-accent">{command.input}</span>
</div>
)}
{command.output && (
<div
className="whitespace-pre-wrap text-base-content/80 mt-1"
dangerouslySetInnerHTML={{
__html: formatOutput(command.output),
}}
/>
)}
</div>
))}
{!isTrainRunning && (
<form onSubmit={handleSubmit} className="flex items-center">
<span className="text-primary font-semibold">guest@atri.dad</span>
<span className="text-base-content">:</span>
<span className="text-secondary font-semibold">
{currentPath}
</span>
<span className="text-base-content">$ </span>
<input
ref={inputRef}
type="text"
value={currentInput}
onInput={(e) =>
setCurrentInput((e.target as HTMLInputElement).value)
}
onKeyDown={handleKeyDown}
className="flex-1 bg-transparent border-none outline-none text-accent ml-1"
spellcheck={false}
/>
</form>
)}
</div>
</div>
{/* Train animation overlay - positioned over the content area but outside the opacity div */}
{isTrainRunning && (
<div className="absolute inset-x-0 top-16 bottom-0 flex items-center justify-center overflow-hidden pointer-events-none">
<div
className="text-white font-mono text-xs whitespace-nowrap"
style={{
transform: `translateX(${trainPosition}%)`,
transition: "none",
}}
>
<pre className="leading-none">{`
==== ________ ___________
_D _| |_______/ \\__I_I_____===__|_________|
|(_)--- | H\\________/ | | =|___ ___| _________________
/ | | H | | | | ||_| |_|| _| \\_____A
| | | H |__--------------------| [___] | =| |
| ________|___H__/__|_____/[][]~\\_______| | -| |
|/ | |-----------I_____I [][] [] D |=======|____|________________________|_
__/ =| o |=-O=====O=====O=====O \\ ____Y___________|__|__________________________|_
|/-=|___|= || || || |_____/~\\___/ |_D__D__D_| |_D__D__D_|
\\_/ \\__/ \\__/ \\__/ \\__/ \\_/ \\_/ \\_/ \\_/ \\_/`}</pre>
</div>
</div>
)}
</div>
);
};
export default Terminal;