In the previous chapter, Command Architecture, we learned how to create commands that output simple text.
But what if you need to ask the user a question? Or show a loading spinner while downloading a file? Or display a menu where the user can select an option using their arrow keys?
Simple text output (console.log) isn't enough. We need an Interactive TUI (Text User Interface).
If you have built websites before, you likely know React. Itβs a tool for building user interfaces using "Components." Usually, React renders to a web browser (HTML).
In this project, we use React to render text directly to your terminal window.
Loading -> Success -> Error).
Instead of HTML tags like <div> or <span>, we use special components designed for the terminal:
<Box>: Think of this as a <div>. It's a container for layout (rows and columns).<Text>: Think of this as a <span> or <p>. It holds the actual text to display.<Dialog>: A pre-made window for asking questions.
Let's look at the theme command. When a user types /theme, we don't want to just list colors; we want a menu where they can pick one.
Here is how the command file (theme.tsx) uses a UI component.
// File: theme/theme.tsx
import * as React from 'react';
import { ThemePicker } from '../../components/ThemePicker.js';
// The command returns a Component, not just text!
export const call = async (onDone, context) => {
return <ThemePickerCommand onDone={onDone} />;
};
Explanation:
The call function here is special. Instead of doing the work immediately, it returns a React Component (<ThemePickerCommand />). The application detects this and "mounts" the component in the terminal.
Now let's look at the component itself (simplified):
function ThemePickerCommand({ onDone }) {
// We use a pre-built picker component
return (
<Pane color="permission">
<ThemePicker
onThemeSelect={(setting) => {
// When user presses Enter on a theme:
onDone(`Theme set to ${setting}`);
}}
/>
</Pane>
);
}
What happens here?
<Pane>: Creates a colored box around our content.<ThemePicker>: This component listens for arrow keys to highlight options.onDone: When the user selects a theme, we call onDone. This tells the main app, "We are finished, you can close this UI now."
The /install command is more complex. It needs to show progress: "Checking...", then "Installing...", then "Success!".
We use React Hooks (useState, useEffect) to manage this.
We define the different "modes" our UI can be in.
// File: install.tsx (Simplified)
function Install({ onDone }) {
// Start in the 'checking' state
const [state, setState] = useState({ type: 'checking' });
// ... logic continues ...
We decide what to show based on the current state.
return (
<Box flexDirection="column">
{/* If checking, show this text */}
{state.type === 'checking' && (
<Text color="blue">Checking installation status...</Text>
)}
{/* If installing, show this text with the version */}
{state.type === 'installing' && (
<Text color="yellow">Installing version {state.version}...</Text>
)}
{/* If success, show the green icon */}
{state.type === 'success' && (
<Text color="green">Installation Complete!</Text>
)}
</Box>
);
}
We use useEffect to trigger the actual installation logic when the component loads.
useEffect(() => {
async function run() {
// 1. Switch state to installing
setState({ type: 'installing', version: '1.0.0' });
// 2. Do the heavy lifting (downloading files)
await downloadFiles();
// 3. Switch state to success
setState({ type: 'success' });
// 4. Tell the app we are done after a short delay
setTimeout(() => onDone('Success'), 2000);
}
run();
}, []);
When you return a React element from a command, the system hands it over to a renderer (specifically, a library called ink).
Sometimes your UI needs to know global information, like "Is the user logged in?" or "What is their API Key?".
In the login.tsx command, we see this passed in via Context.
// File: login/login.tsx
export async function call(onDone, context) {
return <Login onDone={async (success) => {
if (success) {
// We can update global app state here
context.setAppState(prev => ({ ...prev, authVersion: 2 }));
}
onDone('Logged in');
}} />;
}
Components are powerful, but they are temporary. When the component unmounts, its internal state is lost. To store data permanently (like a session token), we must save it to the App State.
We will explore how this global memory works in the next chapter.
<Box>, <Text>).call returns JSX: Interactive commands return a component instead of a string.useState to switch between Loading, Success, and Error views.onDone: The component must manually tell the app when interaction is finished.Now that we can build interfaces, we need to handle the most critical part of a user-facing application: knowing who the user is.
Next Chapter: Authentication & Session State
Generated by Code IQ