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:
2025-12-21 13:57:47 +01:00
parent 1495a9a5c5
commit cde5f34858
+69 -21
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,37 +118,46 @@ 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...',
'Hint: Old-school gamers know a certain cheat code.', 'Hint: Old-school gamers know a certain cheat code.',
'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>