|
|
|
@ -9,12 +9,16 @@ type FileSystemStructure = {
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
interface TerminalProps {
|
|
|
|
|
solution1: number;
|
|
|
|
|
solution2: number;
|
|
|
|
|
solution3: number;
|
|
|
|
|
puzzle1Content: string;
|
|
|
|
|
puzzle2Content: string;
|
|
|
|
|
puzzle3Content: string;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const TerminalComponent: React.FC<TerminalProps> = ({ solution1 = 42, solution2 = 87, solution3 = 5 }) => {
|
|
|
|
|
const TerminalComponent: React.FC<TerminalProps> = ({
|
|
|
|
|
puzzle1Content,
|
|
|
|
|
puzzle2Content,
|
|
|
|
|
puzzle3Content
|
|
|
|
|
}) => {
|
|
|
|
|
const terminalRef = useRef<HTMLDivElement>(null);
|
|
|
|
|
const xtermRef = useRef<Terminal | null>(null);
|
|
|
|
|
const fitAddonRef = useRef<FitAddon | null>(null);
|
|
|
|
@ -57,35 +61,13 @@ const TerminalComponent: React.FC<TerminalProps> = ({ solution1 = 42, solution2
|
|
|
|
|
"- /usr: System-wide shared resources and executables (read-only for you!).\n",
|
|
|
|
|
|
|
|
|
|
// Puzzle 1: Number Base
|
|
|
|
|
'/etc/sys_data/core_access.bin': `System memory address reference:\n` +
|
|
|
|
|
`Register B1: ${solution1.toString(2).padStart(8, '0')}\n` +
|
|
|
|
|
`Note: Always decode binary values when transferring between subsystems.`,
|
|
|
|
|
'/etc/sys_data/core_access.bin': puzzle1Content,
|
|
|
|
|
|
|
|
|
|
// Puzzle 2: Hex Code (Color)
|
|
|
|
|
'/etc/config/color_palette.config': `# Display Color Settings (RGB hex format)
|
|
|
|
|
PRIMARY_COLOR=#FF${solution2.toString(16).padStart(2, '0')}33
|
|
|
|
|
SECONDARY_COLOR=#33FF57
|
|
|
|
|
ALERT_COLOR=#FF0000
|
|
|
|
|
|
|
|
|
|
# System Note:
|
|
|
|
|
# Color channels must be isolated when importing to legacy devices.
|
|
|
|
|
# Remember middle values carry essential security data.`,
|
|
|
|
|
'/etc/config/color_palette.config': puzzle2Content,
|
|
|
|
|
|
|
|
|
|
// Puzzle 3: Log File Analysis
|
|
|
|
|
'/var/log/critical_errors.log': "[ERROR 001] System boot failure sequence A.\n" +
|
|
|
|
|
"[INFO 002] Retrying boot sequence B.\n" +
|
|
|
|
|
"[ERROR 003] Disk read error on /dev/sda1.\n" +
|
|
|
|
|
"[WARN 004] Network interface eth0 flapping.\n" +
|
|
|
|
|
"[INFO 005] System recovered.\n" +
|
|
|
|
|
"[DEBUG 006] User 'admin' login attempt.\n" +
|
|
|
|
|
"[ERROR 007] Authentication failure for user 'admin'.\n" +
|
|
|
|
|
"[INFO 008] User 'hacker' login successful.\n" +
|
|
|
|
|
"[WARN 009] Unusual activity detected from IP 192.168.1.101.\n" +
|
|
|
|
|
"[ERROR 010] Critical process 'security_daemon' terminated unexpectedly.\n" +
|
|
|
|
|
"[INFO 011] Attempting to restart 'security_daemon'.\n" +
|
|
|
|
|
`[ERROR 012] Failed to restart 'security_daemon'. Max retries (${solution3}) reached.\n` +
|
|
|
|
|
"[ERROR 014] System shutting down due to critical errors.\n" +
|
|
|
|
|
"[RECOVER] Backup facility requires retry count for authentication sequence.",
|
|
|
|
|
'/var/log/critical_errors.log': puzzle3Content,
|
|
|
|
|
|
|
|
|
|
// Home directory files with hints
|
|
|
|
|
'/home/user/puzzle_progress.txt': "My notes on the combination lock:\n- First number: __ (Check core system data)\n- Second number: __ (UI color settings have relevant info)\n- Third number: __ (Error logs contain critical values)\n\nReminder: Always convert between number formats as needed!",
|
|
|
|
@ -103,7 +85,7 @@ ALERT_COLOR=#FF0000
|
|
|
|
|
'/usr/bin/common_commands.txt': "Available commands: ls, cd, cat, clear, help (for README)\n\nImportant system paths:\n- Main config: /etc/config/\n- System data: /etc/sys_data/\n- Error logs: /var/log/",
|
|
|
|
|
|
|
|
|
|
'/usr/share/misc/old_data.bak': "ARCHIVE FILE: Contains outdated research notes from Project Chimera. Access restricted.\n\nPossible combination values tested:\n- 12-45-78 (Failed)\n- 24-68-99 (Failed)\n- 11-22-33 (Failed)\n\nNOTE: New secure generation algorithm implemented. Old combinations no longer valid.\n\nCurrent security relies on: core_access biometrics, color_palette UI safeguards, and error logging thresholds."
|
|
|
|
|
}), [solution1, solution2, solution3]);
|
|
|
|
|
}), [puzzle1Content, puzzle2Content, puzzle3Content]);
|
|
|
|
|
const currentPath = useRef<string>('/');
|
|
|
|
|
|
|
|
|
|
const prompt = () => {
|
|
|
|
@ -237,7 +219,10 @@ ALERT_COLOR=#FF0000
|
|
|
|
|
background: '#1e1e1e',
|
|
|
|
|
foreground: '#d4d4d4',
|
|
|
|
|
cursor: '#d4d4d4',
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
allowTransparency: true,
|
|
|
|
|
scrollback: 1000,
|
|
|
|
|
rightClickSelectsWord: true
|
|
|
|
|
});
|
|
|
|
|
const fit = new FitAddon();
|
|
|
|
|
const webLinks = new WebLinksAddon();
|
|
|
|
@ -250,11 +235,164 @@ ALERT_COLOR=#FF0000
|
|
|
|
|
term.open(terminalRef.current);
|
|
|
|
|
fit.fit();
|
|
|
|
|
|
|
|
|
|
// Enhanced paste handler for Ctrl+V and Cmd+V
|
|
|
|
|
term.attachCustomKeyEventHandler((e) => {
|
|
|
|
|
// Handle paste with Ctrl+V (Windows/Linux) or Cmd+V (Mac)
|
|
|
|
|
if ((e.ctrlKey || e.metaKey) && e.key === 'v') {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
navigator.clipboard.readText().then(text => {
|
|
|
|
|
// Process the pasted text character by character to update currentLine
|
|
|
|
|
const cleanText = text.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
|
|
|
|
|
for (const char of cleanText) {
|
|
|
|
|
if (char === '\n') {
|
|
|
|
|
// Handle newline as Enter key
|
|
|
|
|
if (currentLine.current.trim()) {
|
|
|
|
|
commandHistory.current.push(currentLine.current);
|
|
|
|
|
}
|
|
|
|
|
historyIndex.current = commandHistory.current.length;
|
|
|
|
|
processCommand(currentLine.current);
|
|
|
|
|
currentLine.current = '';
|
|
|
|
|
} else {
|
|
|
|
|
// Add printable characters to current line and display
|
|
|
|
|
term.write(char);
|
|
|
|
|
currentLine.current += char;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}).catch(err => {
|
|
|
|
|
console.warn('Failed to read clipboard:', err);
|
|
|
|
|
});
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Handle copy with Ctrl+C or Cmd+C (when text is selected)
|
|
|
|
|
if ((e.ctrlKey || e.metaKey) && e.key === 'c') {
|
|
|
|
|
const selection = term.getSelection();
|
|
|
|
|
if (selection) {
|
|
|
|
|
navigator.clipboard.writeText(selection).catch(err => {
|
|
|
|
|
console.warn('Failed to write to clipboard:', err);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
return true; // Don't prevent default for copy
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return true;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Add right-click context menu support
|
|
|
|
|
term.element?.addEventListener('contextmenu', (e) => {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
|
|
|
|
|
// Create context menu
|
|
|
|
|
const existingMenu = document.getElementById('terminal-context-menu');
|
|
|
|
|
if (existingMenu) {
|
|
|
|
|
existingMenu.remove();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const menu = document.createElement('div');
|
|
|
|
|
menu.id = 'terminal-context-menu';
|
|
|
|
|
menu.style.cssText = `
|
|
|
|
|
position: fixed;
|
|
|
|
|
top: ${e.clientY}px;
|
|
|
|
|
left: ${e.clientX}px;
|
|
|
|
|
background: #2d2d2d;
|
|
|
|
|
border: 1px solid #555;
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
padding: 4px 0;
|
|
|
|
|
box-shadow: 0 2px 8px rgba(0,0,0,0.3);
|
|
|
|
|
z-index: 1000;
|
|
|
|
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
min-width: 120px;
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
const selection = term.getSelection();
|
|
|
|
|
|
|
|
|
|
// Copy option (only if text is selected)
|
|
|
|
|
if (selection) {
|
|
|
|
|
const copyItem = document.createElement('div');
|
|
|
|
|
copyItem.textContent = 'Copy';
|
|
|
|
|
copyItem.style.cssText = `
|
|
|
|
|
padding: 8px 16px;
|
|
|
|
|
color: #fff;
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
transition: background-color 0.1s;
|
|
|
|
|
`;
|
|
|
|
|
copyItem.addEventListener('mouseenter', () => {
|
|
|
|
|
copyItem.style.backgroundColor = '#404040';
|
|
|
|
|
});
|
|
|
|
|
copyItem.addEventListener('mouseleave', () => {
|
|
|
|
|
copyItem.style.backgroundColor = 'transparent';
|
|
|
|
|
});
|
|
|
|
|
copyItem.addEventListener('click', () => {
|
|
|
|
|
navigator.clipboard.writeText(selection).catch(err => {
|
|
|
|
|
console.warn('Failed to write to clipboard:', err);
|
|
|
|
|
});
|
|
|
|
|
menu.remove();
|
|
|
|
|
});
|
|
|
|
|
menu.appendChild(copyItem);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Paste option
|
|
|
|
|
const pasteItem = document.createElement('div');
|
|
|
|
|
pasteItem.textContent = 'Paste';
|
|
|
|
|
pasteItem.style.cssText = `
|
|
|
|
|
padding: 8px 16px;
|
|
|
|
|
color: #fff;
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
transition: background-color 0.1s;
|
|
|
|
|
`;
|
|
|
|
|
pasteItem.addEventListener('mouseenter', () => {
|
|
|
|
|
pasteItem.style.backgroundColor = '#404040';
|
|
|
|
|
});
|
|
|
|
|
pasteItem.addEventListener('mouseleave', () => {
|
|
|
|
|
pasteItem.style.backgroundColor = 'transparent';
|
|
|
|
|
});
|
|
|
|
|
pasteItem.addEventListener('click', () => {
|
|
|
|
|
navigator.clipboard.readText().then(text => {
|
|
|
|
|
// Process the pasted text character by character to update currentLine
|
|
|
|
|
const cleanText = text.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
|
|
|
|
|
for (const char of cleanText) {
|
|
|
|
|
if (char === '\n') {
|
|
|
|
|
// Handle newline as Enter key
|
|
|
|
|
if (currentLine.current.trim()) {
|
|
|
|
|
commandHistory.current.push(currentLine.current);
|
|
|
|
|
}
|
|
|
|
|
historyIndex.current = commandHistory.current.length;
|
|
|
|
|
processCommand(currentLine.current);
|
|
|
|
|
currentLine.current = '';
|
|
|
|
|
} else {
|
|
|
|
|
// Add printable characters to current line and display
|
|
|
|
|
term.write(char);
|
|
|
|
|
currentLine.current += char;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}).catch(err => {
|
|
|
|
|
console.warn('Failed to read clipboard:', err);
|
|
|
|
|
});
|
|
|
|
|
menu.remove();
|
|
|
|
|
});
|
|
|
|
|
menu.appendChild(pasteItem);
|
|
|
|
|
|
|
|
|
|
document.body.appendChild(menu);
|
|
|
|
|
|
|
|
|
|
// Remove menu when clicking elsewhere
|
|
|
|
|
const removeMenu = (event: MouseEvent) => {
|
|
|
|
|
if (!menu.contains(event.target as Node)) {
|
|
|
|
|
menu.remove();
|
|
|
|
|
document.removeEventListener('click', removeMenu);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Use setTimeout to avoid immediate removal
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
document.addEventListener('click', removeMenu);
|
|
|
|
|
}, 0);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
term.write('Welcome to Ixabata-shell!\r\n');
|
|
|
|
|
term.write('Type a command to begin. (Try ls, cd, cat)\r\n');
|
|
|
|
|
prompt();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
term.onKey(({ key, domEvent }) => {
|
|
|
|
|
if (!xtermRef.current) return;
|
|
|
|
|
|
|
|
|
@ -305,40 +443,142 @@ ALERT_COLOR=#FF0000
|
|
|
|
|
let suggestions: string[] = [];
|
|
|
|
|
const availableCommands = ['ls', 'cd', 'cat', 'clear', 'help'];
|
|
|
|
|
|
|
|
|
|
// If we're completing a command (first word)
|
|
|
|
|
if (inputParts.length === 0 && !currentLine.current.includes(' ')) {
|
|
|
|
|
suggestions = availableCommands.filter(c => c.startsWith(currentArgument));
|
|
|
|
|
} else {
|
|
|
|
|
const currentDirItems = Array.isArray(fileSystem[currentPath.current]) ? fileSystem[currentPath.current] as string[] : [];
|
|
|
|
|
suggestions = currentDirItems.filter(item => item.startsWith(currentArgument));
|
|
|
|
|
} else {
|
|
|
|
|
// For path completion (after a command)
|
|
|
|
|
const command = inputParts[0];
|
|
|
|
|
|
|
|
|
|
// Only complete paths for commands that accept file paths
|
|
|
|
|
if (['ls', 'cd', 'cat'].includes(command)) {
|
|
|
|
|
// Handle absolute paths
|
|
|
|
|
if (currentArgument.startsWith('/')) {
|
|
|
|
|
const pathParts = currentArgument.split('/');
|
|
|
|
|
const lastPart = pathParts.pop() || '';
|
|
|
|
|
const basePath = pathParts.join('/') || '/';
|
|
|
|
|
|
|
|
|
|
if (fileSystem[basePath] && Array.isArray(fileSystem[basePath])) {
|
|
|
|
|
suggestions = (fileSystem[basePath] as string[])
|
|
|
|
|
.filter(item => item.startsWith(lastPart))
|
|
|
|
|
.map(item => {
|
|
|
|
|
// Return the full path for absolute completions
|
|
|
|
|
return basePath === '/' ? '/' + item : basePath + '/' + item;
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
// Handle relative paths
|
|
|
|
|
const currentDirItems = Array.isArray(fileSystem[currentPath.current])
|
|
|
|
|
? fileSystem[currentPath.current] as string[]
|
|
|
|
|
: [];
|
|
|
|
|
suggestions = currentDirItems.filter(item => item.startsWith(currentArgument));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (suggestions.length === 1) {
|
|
|
|
|
const suggestion = suggestions[0];
|
|
|
|
|
let resolvedSuggestionPath = suggestion;
|
|
|
|
|
if (!suggestion.startsWith('/')) {
|
|
|
|
|
resolvedSuggestionPath = (currentPath.current === '/' ? '' : currentPath.current) + '/' + suggestion;
|
|
|
|
|
|
|
|
|
|
// Handle path resolution for the suggestion
|
|
|
|
|
if (currentArgument.startsWith('/')) {
|
|
|
|
|
// For absolute paths, the suggestion is already the full path
|
|
|
|
|
resolvedSuggestionPath = suggestion;
|
|
|
|
|
} else {
|
|
|
|
|
// For relative paths, use the current directory
|
|
|
|
|
resolvedSuggestionPath = (currentPath.current === '/' ? '' : currentPath.current) + '/' + suggestion;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Normalize path
|
|
|
|
|
if (resolvedSuggestionPath !== '/' && resolvedSuggestionPath.endsWith('/')) {
|
|
|
|
|
resolvedSuggestionPath = resolvedSuggestionPath.slice(0, -1);
|
|
|
|
|
resolvedSuggestionPath = resolvedSuggestionPath.slice(0, -1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const isDirectory = Array.isArray(fileSystem[resolvedSuggestionPath]);
|
|
|
|
|
const completion = suggestion + (isDirectory ? '/' : ' ');
|
|
|
|
|
|
|
|
|
|
xtermRef.current.write(completion.substring(currentArgument.length));
|
|
|
|
|
currentLine.current = (commandPart ? commandPart + ' ' : '') + completion;
|
|
|
|
|
|
|
|
|
|
let completion: string;
|
|
|
|
|
if (currentArgument.startsWith('/')) {
|
|
|
|
|
// For absolute paths, replace the entire argument
|
|
|
|
|
completion = suggestion + (isDirectory ? '/' : ' ');
|
|
|
|
|
xtermRef.current.write('\x1b[2K\r'); // Clear the line
|
|
|
|
|
xtermRef.current.write(`root@ixabatasha.life:${currentPath.current}# `);
|
|
|
|
|
xtermRef.current.write((commandPart ? commandPart + ' ' : '') + completion);
|
|
|
|
|
currentLine.current = (commandPart ? commandPart + ' ' : '') + completion;
|
|
|
|
|
} else {
|
|
|
|
|
// For relative paths, append to the existing argument
|
|
|
|
|
completion = suggestion + (isDirectory ? '/' : ' ');
|
|
|
|
|
xtermRef.current.write(completion.substring(currentArgument.length));
|
|
|
|
|
currentLine.current = (commandPart ? commandPart + ' ' : '') + completion;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!isDirectory || !completion.endsWith('/')) {
|
|
|
|
|
currentLine.current = currentLine.current.trimEnd();
|
|
|
|
|
currentLine.current = currentLine.current.trimEnd();
|
|
|
|
|
}
|
|
|
|
|
} else if (suggestions.length > 1) {
|
|
|
|
|
// Show all possible completions in a bash-like format
|
|
|
|
|
xtermRef.current.write('\r\n');
|
|
|
|
|
|
|
|
|
|
// For display purposes, show just the item names, not full paths
|
|
|
|
|
const displaySuggestions = suggestions.map(s => {
|
|
|
|
|
if (currentArgument.startsWith('/')) {
|
|
|
|
|
return s.split('/').pop() || s;
|
|
|
|
|
}
|
|
|
|
|
return s;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Calculate the longest common prefix
|
|
|
|
|
let commonPrefix = '';
|
|
|
|
|
if (displaySuggestions.length > 0) {
|
|
|
|
|
const first = displaySuggestions[0];
|
|
|
|
|
for (let i = 0; i < first.length; i++) {
|
|
|
|
|
if (displaySuggestions.every(s => s[i] === first[i])) {
|
|
|
|
|
commonPrefix += first[i];
|
|
|
|
|
} else {
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
} else if (suggestions.length > 1) {
|
|
|
|
|
xtermRef.current.write('\r\n' + suggestions.join('\t') + '\r\n');
|
|
|
|
|
prompt();
|
|
|
|
|
xtermRef.current.write(currentLine.current);
|
|
|
|
|
// If there's a common prefix, complete up to that point
|
|
|
|
|
if (commonPrefix.length > (currentArgument.startsWith('/') ? currentArgument.split('/').pop()?.length || 0 : currentArgument.length)) {
|
|
|
|
|
if (currentArgument.startsWith('/')) {
|
|
|
|
|
const pathParts = currentArgument.split('/');
|
|
|
|
|
pathParts.pop();
|
|
|
|
|
const newPath = pathParts.join('/') + '/' + commonPrefix;
|
|
|
|
|
xtermRef.current.write('\x1b[2K\r'); // Clear the line
|
|
|
|
|
xtermRef.current.write(`root@ixabatasha.life:${currentPath.current}# `);
|
|
|
|
|
xtermRef.current.write((commandPart ? commandPart + ' ' : '') + newPath);
|
|
|
|
|
currentLine.current = (commandPart ? commandPart + ' ' : '') + newPath;
|
|
|
|
|
} else {
|
|
|
|
|
xtermRef.current.write(commonPrefix.substring(currentArgument.length));
|
|
|
|
|
currentLine.current = (commandPart ? commandPart + ' ' : '') + commonPrefix;
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
// Display all options in columns
|
|
|
|
|
const maxLength = Math.max(...displaySuggestions.map(s => s.length)) + 2;
|
|
|
|
|
const columns = Math.floor(term.cols / maxLength);
|
|
|
|
|
const rows = Math.ceil(displaySuggestions.length / columns);
|
|
|
|
|
|
|
|
|
|
for (let row = 0; row < rows; row++) {
|
|
|
|
|
let line = '';
|
|
|
|
|
for (let col = 0; col < columns; col++) {
|
|
|
|
|
const index = row + (col * rows);
|
|
|
|
|
if (index < displaySuggestions.length) {
|
|
|
|
|
const item = displaySuggestions[index];
|
|
|
|
|
const fullPath = suggestions[index];
|
|
|
|
|
const isDir = Array.isArray(fileSystem[fullPath.startsWith('/') ? fullPath : (currentPath.current === '/' ? '' : currentPath.current) + '/' + fullPath]);
|
|
|
|
|
line += item + (isDir ? '/' : '') + ' '.repeat(maxLength - item.length - (isDir ? 1 : 0));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
xtermRef.current.write(line + '\r\n');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Reprint the prompt and current input
|
|
|
|
|
prompt();
|
|
|
|
|
xtermRef.current.write(currentLine.current);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
// If no suggestions, do nothing (or beep: xtermRef.current.write('\x07');)
|
|
|
|
|
|
|
|
|
|
// If no suggestions, do nothing
|
|
|
|
|
} else if (printable && key.length === 1) { // Ensure it's a single character, not a special key like 'Shift'
|
|
|
|
|
xtermRef.current.write(key);
|
|
|
|
|
currentLine.current += key;
|
|
|
|
|