diff --git a/src/components/history/hook.ts b/src/components/history/hook.ts index d453033..d6a5d5f 100644 --- a/src/components/history/hook.ts +++ b/src/components/history/hook.ts @@ -11,15 +11,19 @@ export const useHistory = (defaultValue: Array) => { command, lastCommandIndex, setHistory: (value: string) => - setHistory([ - ...history, + setHistory((prev) => [ + ...prev, { - id: history.length, + id: prev.length, date: new Date(), command, output: value, }, ]), + updateHistory: (index: number, value: string) => + setHistory((prev) => + prev.map((entry, i) => (i === index ? { ...entry, output: value } : entry)), + ), setCommand, setLastCommandIndex, clearHistory: () => setHistory([]), diff --git a/src/components/input.tsx b/src/components/input.tsx index bf9de68..e254b4d 100644 --- a/src/components/input.tsx +++ b/src/components/input.tsx @@ -12,6 +12,7 @@ export const Input = ({ lastCommandIndex, setCommand, setHistory, + updateHistory, setLastCommandIndex, clearHistory, }) => { @@ -40,7 +41,14 @@ export const Input = ({ if (event.key === 'Enter' || event.code === '13') { event.preventDefault(); setLastCommandIndex(0); - await shell(command, setHistory, clearHistory, setCommand); + await shell( + command, + setHistory, + updateHistory, + clearHistory, + setCommand, + history.length, + ); containerRef.current.scrollTo(0, containerRef.current.scrollHeight); } diff --git a/src/pages/index.tsx b/src/pages/index.tsx index 806febf..f5b1089 100644 --- a/src/pages/index.tsx +++ b/src/pages/index.tsx @@ -7,7 +7,7 @@ import { History } from '../components/history/History'; import { banner } from '../utils/bin/commands'; interface IndexPageProps { - inputRef: React.MutableRefObject; + inputRef: React.RefObject; } const IndexPage: React.FC = ({ inputRef }) => { @@ -18,6 +18,7 @@ const IndexPage: React.FC = ({ inputRef }) => { lastCommandIndex, setCommand, setHistory, + updateHistory, clearHistory, setLastCommandIndex, } = useHistory([]); @@ -53,6 +54,7 @@ const IndexPage: React.FC = ({ inputRef }) => { lastCommandIndex={lastCommandIndex} setCommand={setCommand} setHistory={setHistory} + updateHistory={updateHistory} setLastCommandIndex={setLastCommandIndex} clearHistory={clearHistory} /> diff --git a/src/utils/shell.ts b/src/utils/shell.ts index 53751a5..dac3fb6 100644 --- a/src/utils/shell.ts +++ b/src/utils/shell.ts @@ -1,28 +1,56 @@ import React from 'react'; import * as bin from './bin'; +// Braille frames that read as a single-cell grid of dots appearing to spin. +const spinnerFrames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏']; + +// Commands that fetch from a remote API and may take a moment to respond. +const asyncCommands = ['quote', 'readme']; + export const shell = async ( command: string, setHistory: (value: string) => void, + updateHistory: (index: number, value: string) => void, clearHistory: () => void, setCommand: React.Dispatch>, + historyIndex: number, ) => { const args = command.split(' '); args[0] = args[0].toLowerCase(); if (args[0] === 'clear') { clearHistory(); + setCommand(''); } else if (command === '') { setHistory(''); + setCommand(''); } else if (Object.keys(bin).indexOf(args[0]) === -1) { setHistory( ` shell: command not found: ${args[0]}. Try 'help' to get started.`, ); + setCommand(''); + } else if (asyncCommands.includes(args[0])) { + // Show a spinner and clear the prompt so the user can keep working while + // the request is in flight. The output lands in its own history entry, so + // running other commands meanwhile won't clobber it. + let frame = 0; + setHistory(spinnerFrames[frame]); + setCommand(''); + const spinner = setInterval(() => { + frame = (frame + 1) % spinnerFrames.length; + updateHistory(historyIndex, spinnerFrames[frame]); + }, 80); + + try { + const output = await bin[args[0]](args.slice(1)); + updateHistory(historyIndex, output); + } finally { + clearInterval(spinner); + } } else { const output = await bin[args[0]](args.slice(1)); setHistory(output); + setCommand(''); } - - setCommand(''); };