Fix tab complete
This commit is contained in:
@ -42,11 +42,9 @@ const Terminal = () => {
|
|||||||
const [currentInput, setCurrentInput] = useState('');
|
const [currentInput, setCurrentInput] = useState('');
|
||||||
const [historyIndex, setHistoryIndex] = useState(-1);
|
const [historyIndex, setHistoryIndex] = useState(-1);
|
||||||
const [fileSystem, setFileSystem] = useState<{ [key: string]: FileSystemNode }>({});
|
const [fileSystem, setFileSystem] = useState<{ [key: string]: FileSystemNode }>({});
|
||||||
const [lastTabInput, setLastTabInput] = useState('');
|
|
||||||
const [showCompletions, setShowCompletions] = useState(false);
|
|
||||||
const [completionsList, setCompletionsList] = useState<string[]>([]);
|
|
||||||
const [isTrainRunning, setIsTrainRunning] = useState(false);
|
const [isTrainRunning, setIsTrainRunning] = useState(false);
|
||||||
const [trainPosition, setTrainPosition] = useState(100);
|
const [trainPosition, setTrainPosition] = useState(100);
|
||||||
|
const [persistentHistory, setPersistentHistory] = useState<string[]>([]);
|
||||||
const inputRef = useRef<HTMLInputElement>(null);
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
const terminalRef = useRef<HTMLDivElement>(null);
|
const terminalRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
@ -62,6 +60,28 @@ const Terminal = () => {
|
|||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// Load persistent command history from localStorage
|
||||||
|
useEffect(() => {
|
||||||
|
const savedHistory = localStorage.getItem('terminal-history');
|
||||||
|
if (savedHistory) {
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(savedHistory);
|
||||||
|
setPersistentHistory(parsed);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading command history:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Save command history to localStorage
|
||||||
|
const saveCommandToHistory = (command: string) => {
|
||||||
|
if (command.trim()) {
|
||||||
|
const updatedHistory = [...persistentHistory, command].slice(-100); // Keep last 100 commands
|
||||||
|
setPersistentHistory(updatedHistory);
|
||||||
|
localStorage.setItem('terminal-history', JSON.stringify(updatedHistory));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const initializeFileSystem = async () => {
|
const initializeFileSystem = async () => {
|
||||||
try {
|
try {
|
||||||
@ -269,35 +289,13 @@ ${post.content}`;
|
|||||||
return '/' + currentParts.join('/');
|
return '/' + currentParts.join('/');
|
||||||
};
|
};
|
||||||
|
|
||||||
const getCommonPrefix = (strings: string[]): string => {
|
const getCompletions = (input: string): { completion: string | null, replaceFrom: number } => {
|
||||||
if (strings.length === 0) return '';
|
|
||||||
if (strings.length === 1) return strings[0];
|
|
||||||
|
|
||||||
let commonPrefix = strings[0];
|
|
||||||
for (let i = 1; i < strings.length; i++) {
|
|
||||||
let j = 0;
|
|
||||||
while (j < commonPrefix.length && j < strings[i].length && commonPrefix[j] === strings[i][j]) {
|
|
||||||
j++;
|
|
||||||
}
|
|
||||||
commonPrefix = commonPrefix.substring(0, j);
|
|
||||||
if (commonPrefix === '') break;
|
|
||||||
}
|
|
||||||
return commonPrefix;
|
|
||||||
};
|
|
||||||
|
|
||||||
const getCompletions = (input: string): { completions: string[], prefix: string, replaceFrom: number } => {
|
|
||||||
const parts = input.trim().split(' ');
|
const parts = input.trim().split(' ');
|
||||||
const command = parts[0];
|
const command = parts[0];
|
||||||
const partialPath = parts[parts.length - 1] || '';
|
const partialPath = parts[parts.length - 1] || '';
|
||||||
|
|
||||||
if (parts.length === 1) {
|
// Only complete paths for these commands, not the commands themselves
|
||||||
// Command completion
|
if (parts.length > 1 && ['ls', 'cd', 'cat', 'open'].includes(command)) {
|
||||||
const commands = ['ls', 'cd', 'cat', 'pwd', 'clear', 'tree', 'whoami', 'open', 'help'];
|
|
||||||
const matches = commands.filter(cmd => cmd.startsWith(command));
|
|
||||||
return { completions: matches, prefix: command, replaceFrom: 0 };
|
|
||||||
}
|
|
||||||
|
|
||||||
if (['ls', 'cd', 'cat', 'open'].includes(command)) {
|
|
||||||
// Path completion
|
// Path completion
|
||||||
const isAbsolute = partialPath.startsWith('/');
|
const isAbsolute = partialPath.startsWith('/');
|
||||||
const pathToComplete = isAbsolute ? partialPath : resolvePath(partialPath);
|
const pathToComplete = isAbsolute ? partialPath : resolvePath(partialPath);
|
||||||
@ -334,26 +332,26 @@ ${post.content}`;
|
|||||||
if (current?.children && current.children[part] && current.children[part].type === 'directory') {
|
if (current?.children && current.children[part] && current.children[part].type === 'directory') {
|
||||||
current = current.children[part];
|
current = current.children[part];
|
||||||
} else {
|
} else {
|
||||||
return { completions: [], prefix: searchPrefix, replaceFrom };
|
return { completion: null, replaceFrom };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!current?.children) {
|
if (!current?.children) {
|
||||||
return { completions: [], prefix: searchPrefix, replaceFrom };
|
return { completion: null, replaceFrom };
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get matching items
|
// Get first matching item
|
||||||
const matches = Object.keys(current.children)
|
const match = Object.keys(current.children)
|
||||||
.filter(name => name.startsWith(searchPrefix))
|
.find(name => name.startsWith(searchPrefix));
|
||||||
.map(name => {
|
|
||||||
const item = current.children![name];
|
|
||||||
return item.type === 'directory' ? `${name}/` : name;
|
|
||||||
});
|
|
||||||
|
|
||||||
return { completions: matches, prefix: searchPrefix, replaceFrom };
|
if (match) {
|
||||||
|
const item = current.children[match];
|
||||||
|
const completion = item.type === 'directory' ? `${match}/` : match;
|
||||||
|
return { completion, replaceFrom };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return { completions: [], prefix: '', replaceFrom: input.length };
|
return { completion: null, replaceFrom: input.length };
|
||||||
};
|
};
|
||||||
|
|
||||||
const executeCommand = (input: string): string => {
|
const executeCommand = (input: string): string => {
|
||||||
@ -556,6 +554,9 @@ ${post.content}`;
|
|||||||
timestamp: new Date()
|
timestamp: new Date()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Save command to persistent history
|
||||||
|
saveCommandToHistory(currentInput);
|
||||||
|
|
||||||
if (currentInput.trim().toLowerCase() === 'clear') {
|
if (currentInput.trim().toLowerCase() === 'clear') {
|
||||||
setCommandHistory([]);
|
setCommandHistory([]);
|
||||||
} else {
|
} else {
|
||||||
@ -564,91 +565,39 @@ ${post.content}`;
|
|||||||
|
|
||||||
setCurrentInput('');
|
setCurrentInput('');
|
||||||
setHistoryIndex(-1);
|
setHistoryIndex(-1);
|
||||||
setShowCompletions(false);
|
|
||||||
setLastTabInput('');
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleKeyDown = (e: JSX.TargetedKeyboardEvent<HTMLInputElement>) => {
|
const handleKeyDown = (e: JSX.TargetedKeyboardEvent<HTMLInputElement>) => {
|
||||||
if (e.key === 'Tab') {
|
if (e.key === 'Tab') {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
const { completions, prefix, replaceFrom } = getCompletions(currentInput);
|
const { completion, replaceFrom } = getCompletions(currentInput);
|
||||||
|
|
||||||
if (completions.length === 0) {
|
if (completion) {
|
||||||
return;
|
// Replace from the correct position with the completion
|
||||||
}
|
|
||||||
|
|
||||||
if (completions.length === 1) {
|
|
||||||
// Single completion - replace from the correct position
|
|
||||||
const beforeReplacement = currentInput.substring(0, replaceFrom);
|
const beforeReplacement = currentInput.substring(0, replaceFrom);
|
||||||
const completion = completions[0];
|
|
||||||
const newInput = beforeReplacement + completion;
|
const newInput = beforeReplacement + completion;
|
||||||
|
|
||||||
setCurrentInput(newInput + (completion.endsWith('/') ? '' : ' '));
|
setCurrentInput(newInput + (completion.endsWith('/') ? '' : ' '));
|
||||||
setShowCompletions(false);
|
|
||||||
setLastTabInput('');
|
|
||||||
} else {
|
|
||||||
// Multiple completions - find common prefix
|
|
||||||
const commonPrefix = getCommonPrefix(completions);
|
|
||||||
|
|
||||||
if (commonPrefix.length > prefix.length) {
|
|
||||||
// Complete to common prefix
|
|
||||||
const beforeReplacement = currentInput.substring(0, replaceFrom);
|
|
||||||
setCurrentInput(beforeReplacement + commonPrefix);
|
|
||||||
setShowCompletions(false);
|
|
||||||
setLastTabInput(currentInput);
|
|
||||||
} else {
|
|
||||||
// Show completions if we hit tab twice on the same input
|
|
||||||
if (currentInput === lastTabInput && !showCompletions) {
|
|
||||||
setCompletionsList(completions);
|
|
||||||
setShowCompletions(true);
|
|
||||||
|
|
||||||
// Add completions to command history for display
|
|
||||||
const output = completions.length > 8
|
|
||||||
? completions.slice(0, 8).join(' ') + ` ... (${completions.length - 8} more)`
|
|
||||||
: completions.join(' ');
|
|
||||||
|
|
||||||
const newCommand: Command = {
|
|
||||||
input: '',
|
|
||||||
output,
|
|
||||||
timestamp: new Date()
|
|
||||||
};
|
|
||||||
setCommandHistory((prev: Command[]) => [...prev, newCommand]);
|
|
||||||
} else {
|
|
||||||
setLastTabInput(currentInput);
|
|
||||||
setShowCompletions(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else if (e.key === 'ArrowUp') {
|
} else if (e.key === 'ArrowUp') {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const inputCommands = commandHistory.filter((cmd: Command) => cmd.input.trim() !== '');
|
if (persistentHistory.length > 0) {
|
||||||
if (inputCommands.length > 0) {
|
const newIndex = historyIndex === -1 ? persistentHistory.length - 1 : Math.max(0, historyIndex - 1);
|
||||||
const newIndex = historyIndex === -1 ? inputCommands.length - 1 : Math.max(0, historyIndex - 1);
|
|
||||||
setHistoryIndex(newIndex);
|
setHistoryIndex(newIndex);
|
||||||
setCurrentInput(inputCommands[newIndex].input);
|
setCurrentInput(persistentHistory[newIndex]);
|
||||||
}
|
}
|
||||||
setShowCompletions(false);
|
|
||||||
setLastTabInput('');
|
|
||||||
} else if (e.key === 'ArrowDown') {
|
} else if (e.key === 'ArrowDown') {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const inputCommands = commandHistory.filter((cmd: Command) => cmd.input.trim() !== '');
|
|
||||||
if (historyIndex !== -1) {
|
if (historyIndex !== -1) {
|
||||||
const newIndex = Math.min(inputCommands.length - 1, historyIndex + 1);
|
const newIndex = Math.min(persistentHistory.length - 1, historyIndex + 1);
|
||||||
if (newIndex === inputCommands.length - 1 && historyIndex === newIndex) {
|
if (newIndex === persistentHistory.length - 1 && historyIndex === newIndex) {
|
||||||
setHistoryIndex(-1);
|
setHistoryIndex(-1);
|
||||||
setCurrentInput('');
|
setCurrentInput('');
|
||||||
} else {
|
} else {
|
||||||
setHistoryIndex(newIndex);
|
setHistoryIndex(newIndex);
|
||||||
setCurrentInput(inputCommands[newIndex].input);
|
setCurrentInput(persistentHistory[newIndex]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
setShowCompletions(false);
|
|
||||||
setLastTabInput('');
|
|
||||||
} else {
|
|
||||||
// Reset completion state on any other key
|
|
||||||
setShowCompletions(false);
|
|
||||||
setLastTabInput('');
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -718,12 +667,6 @@ __/ =| o |=-O=====O=====O=====O \\ ____Y___________|__|_________________________
|
|||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
{showCompletions && (
|
|
||||||
<div className="text-secondary text-sm mb-2">
|
|
||||||
Tab completions: {completionsList.join(' ')}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<form onSubmit={handleSubmit} className="flex items-center">
|
<form onSubmit={handleSubmit} className="flex items-center">
|
||||||
<span className="text-primary font-semibold">guest@atri.dad</span>
|
<span className="text-primary font-semibold">guest@atri.dad</span>
|
||||||
<span className="text-base-content">:</span>
|
<span className="text-base-content">:</span>
|
||||||
|
Reference in New Issue
Block a user