Command History

- Up/Down Arrow Navigation: Users can now press ↑/↓ to navigate through previously executed commands
- History Storage: Commands are stored in state and persist during the session
- Duplicate Prevention: Avoids adding the same command multiple times consecutively
- Reset on Typing: Manual typing resets the history navigation position
 Tab Completion
- Auto-complete: Pressing Tab completes partial commands that match available commands
- Single Match: If only one command matches, it completes the full command
- Multiple Matches: If multiple commands match, displays all possible completions in the terminal output
- Case-insensitive: Works regardless of input case
This commit is contained in:
JorySeverijnse 2025-12-21 13:57:47 +01:00
parent 1495a9a5c5
commit cde5f34858

View File

@ -65,6 +65,8 @@ const TerminalCommand = () => {
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
const [input, setInput] = useState(''); const [input, setInput] = useState('');
const [output, setOutput] = useState<string[]>(['Type /help for available commands']); const [output, setOutput] = useState<string[]>(['Type /help for available commands']);
const [commandHistory, setCommandHistory] = useState<string[]>([]);
const [historyIndex, setHistoryIndex] = useState(-1);
const inputRef = useRef<HTMLInputElement>(null); const inputRef = useRef<HTMLInputElement>(null);
const outputRef = useRef<HTMLDivElement>(null); const outputRef = useRef<HTMLDivElement>(null);
const navigate = useNavigate(); const navigate = useNavigate();
@ -116,19 +118,28 @@ const TerminalCommand = () => {
const handleSubmit = (e: React.FormEvent) => { const handleSubmit = (e: React.FormEvent) => {
e.preventDefault(); e.preventDefault();
const trimmedInput = input.trim().toLowerCase(); const trimmedInput = input.trim();
if (!trimmedInput) return; if (!trimmedInput) return;
playSound('click'); playSound('click');
setOutput(prev => [...prev, `> ${input}`]); setOutput(prev => [...prev, `> ${input}`]);
// Add to command history (avoid duplicates of the last command)
setCommandHistory(prev => {
const newHistory = prev.filter(cmd => cmd !== trimmedInput);
return [...newHistory, trimmedInput];
});
setHistoryIndex(-1);
// Unlock terminal user achievement // Unlock terminal user achievement
unlockAchievement('terminal_user'); unlockAchievement('terminal_user');
if (trimmedInput === '/help' || trimmedInput === '/h') { const lowerInput = trimmedInput.toLowerCase();
if (lowerInput === '/help' || lowerInput === '/h') {
setOutput(prev => [...prev, helpText, '---HINT---' + helpHint]); setOutput(prev => [...prev, helpText, '---HINT---' + helpHint]);
} else if (trimmedInput === '/hint') { } else if (lowerInput === '/hint') {
unlockAchievement('hint_seeker'); unlockAchievement('hint_seeker');
setOutput(prev => [...prev, setOutput(prev => [...prev,
'Hidden feature detected in system...', 'Hidden feature detected in system...',
@ -136,17 +147,17 @@ const TerminalCommand = () => {
'Think NES, 1986, Contra... 30 lives anyone?', 'Think NES, 1986, Contra... 30 lives anyone?',
'The sequence uses arrow keys and two letters.' 'The sequence uses arrow keys and two letters.'
]); ]);
} else if (trimmedInput === '/clear' || trimmedInput === '/c') { } else if (lowerInput === '/clear' || lowerInput === '/c') {
setOutput(['Terminal cleared. Type /help for commands.']); setOutput(['Terminal cleared. Type /help for commands.']);
} else if (commands[trimmedInput]) { } else if (commands[lowerInput]) {
setOutput(prev => [...prev, `Navigating to ${trimmedInput.slice(1)}...`]); setOutput(prev => [...prev, `Navigating to ${lowerInput.slice(1)}...`]);
playSound('beep'); playSound('beep');
setTimeout(() => { setTimeout(() => {
navigate(commands[trimmedInput]); navigate(commands[lowerInput]);
setIsOpen(false); setIsOpen(false);
}, 300); }, 300);
} else { } else {
setOutput(prev => [...prev, `Command not found: ${trimmedInput}`, 'Type /help for available commands']); setOutput(prev => [...prev, `Command not found: ${lowerInput}`, 'Type /help for available commands']);
} }
setInput(''); setInput('');
@ -209,16 +220,53 @@ const TerminalCommand = () => {
<form onSubmit={handleSubmit} className="border-t border-primary/50"> <form onSubmit={handleSubmit} className="border-t border-primary/50">
<div className="flex items-center px-4 py-3"> <div className="flex items-center px-4 py-3">
<span className="text-primary font-mono mr-2">{'>'}</span> <span className="text-primary font-mono mr-2">{'>'}</span>
<input <input
ref={inputRef} ref={inputRef}
type="text" type="text"
value={input} value={input}
onChange={(e) => setInput(e.target.value)} onChange={(e) => {
className="flex-1 bg-transparent border-none outline-none font-mono text-primary placeholder-primary/40" setInput(e.target.value);
placeholder="Enter command..." setHistoryIndex(-1); // Reset history navigation when typing
autoComplete="off" }}
spellCheck={false} onKeyDown={(e) => {
/> if (e.key === 'ArrowUp') {
e.preventDefault();
if (commandHistory.length > 0) {
const newIndex = historyIndex === -1 ? commandHistory.length - 1 : Math.max(0, historyIndex - 1);
setHistoryIndex(newIndex);
setInput(commandHistory[newIndex]);
}
} else if (e.key === 'ArrowDown') {
e.preventDefault();
if (historyIndex >= 0) {
const newIndex = historyIndex + 1;
if (newIndex >= commandHistory.length) {
setHistoryIndex(-1);
setInput('');
} else {
setHistoryIndex(newIndex);
setInput(commandHistory[newIndex]);
}
}
} else if (e.key === 'Tab') {
e.preventDefault();
const currentInput = input.trim().toLowerCase();
if (currentInput) {
// Find commands that start with current input
const matches = Object.keys(commands).filter(cmd => cmd.startsWith(currentInput));
if (matches.length === 1) {
setInput(matches[0]);
} else if (matches.length > 1) {
setOutput(prev => [...prev, `Possible completions: ${matches.join(', ')}`]);
}
}
}
}}
className="flex-1 bg-transparent border-none outline-none font-mono text-primary placeholder-primary/40"
placeholder="Enter command..."
autoComplete="off"
spellCheck={false}
/>
<span className="text-primary animate-pulse">_</span> <span className="text-primary animate-pulse">_</span>
</div> </div>
</form> </form>