Skip to content

Commit f95360d

Browse files
committed
feat: implement command history navigation in terminal with arrow keys
1 parent 969ff99 commit f95360d

1 file changed

Lines changed: 58 additions & 0 deletions

File tree

src/components/Terminal.tsx

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,11 @@ const Terminal = forwardRef<TerminalHandle, TerminalProps>(
5252
const requestConfirmRef = useRef(requestConfirm);
5353
useEffect(() => { requestConfirmRef.current = requestConfirm; }, [requestConfirm]);
5454

55+
// Command history
56+
const commandHistory = useRef<string[]>([]);
57+
const historyIndex = useRef(-1);
58+
const savedInput = useRef('');
59+
5560
// Exec mode refs — active while a Java process is running
5661
const execStdinCallback = useRef<((data: string) => void) | null>(null);
5762
const execKillCallback = useRef<(() => void) | null>(null);
@@ -175,11 +180,62 @@ const Terminal = forwardRef<TerminalHandle, TerminalProps>(
175180
}
176181

177182
// ── Normal command mode ──
183+
184+
// Arrow keys arrive as escape sequences
185+
if (data === '\x1b[A' || data === '\x1b[B') {
186+
const history = commandHistory.current;
187+
if (history.length === 0) return;
188+
189+
if (data === '\x1b[A') {
190+
// Up — go back in history
191+
if (historyIndex.current === -1) {
192+
// Save whatever the user was typing
193+
savedInput.current = inputBuffer.current;
194+
historyIndex.current = history.length - 1;
195+
} else if (historyIndex.current > 0) {
196+
historyIndex.current--;
197+
} else {
198+
return; // Already at oldest
199+
}
200+
} else {
201+
// Down — go forward in history
202+
if (historyIndex.current === -1) return; // Nothing to navigate
203+
if (historyIndex.current < history.length - 1) {
204+
historyIndex.current++;
205+
} else {
206+
// Past newest — restore saved input
207+
historyIndex.current = -1;
208+
}
209+
}
210+
211+
// Erase the current input on screen
212+
const eraseLen = inputBuffer.current.length;
213+
if (eraseLen > 0) term.write('\b \b'.repeat(eraseLen));
214+
215+
// Replace with history entry or saved input
216+
const replacement = historyIndex.current === -1
217+
? savedInput.current
218+
: history[historyIndex.current];
219+
inputBuffer.current = replacement;
220+
term.write(replacement);
221+
return;
222+
}
223+
178224
if (code === 13) {
179225
// Enter
180226
term.write('\r\n');
181227
const raw = inputBuffer.current.trim();
182228
inputBuffer.current = '';
229+
230+
// Push to history (skip duplicates of the last entry)
231+
if (raw && raw !== commandHistory.current[commandHistory.current.length - 1]) {
232+
commandHistory.current.push(raw);
233+
// Cap at 100 entries
234+
if (commandHistory.current.length > 100) commandHistory.current.shift();
235+
}
236+
historyIndex.current = -1;
237+
savedInput.current = '';
238+
183239
const parts = raw.split(/\s+/);
184240
const cmd = parts[0]?.toLowerCase() ?? '';
185241
const arg = parts.slice(1).join(' ');
@@ -374,6 +430,8 @@ const Terminal = forwardRef<TerminalHandle, TerminalProps>(
374430
inputBuffer.current = inputBuffer.current.slice(0, -1);
375431
term.write('\b \b');
376432
}
433+
} else if (code === 27) {
434+
// Escape sequence — ignore (Left/Right/etc already handled above)
377435
} else if (code >= 32) {
378436
// Printable chars
379437
inputBuffer.current += data;

0 commit comments

Comments
 (0)