In the previous chapter, Command Registration, we set up the "menu entry" for our tag command. Now, it is time to cook the meal!
In this chapter, we will explore React-based Terminal UI.
Traditionally, Command Line Interfaces (CLIs) work like a typewriter:
But what if you need interactivity? What if you want to show a confirmation dialog, use arrow keys to select options, or update a progress bar without spamming the screen?
We use a library called Ink. It allows us to use Reactβthe same technology used for modern websitesβto build interactive interfaces inside the terminal.
<div> and <span> tags to draw pixels in a browser.<Box> and <Text> components to draw characters in a command prompt.call()
In our tag.tsx file, the main entry point is a function named call. When the user types tag, the CLI Core invokes this function.
Instead of just running a script, this function returns a React Component.
// tag.tsx
export async function call(
onDone: LocalJSXCommandOnDone,
_context: unknown,
args?: string
): Promise<React.ReactNode> {
// 1. Clean up the user input
const tagName = args?.trim() || '';
// 2. Return a React Component to render
return <ToggleTagAndClose tagName={tagName} onDone={onDone} />;
}
Explanation:
bugfix in tag bugfix).
Let's look at ToggleTagAndClose. This component doesn't necessarily "draw" anything immediately; it decides what to do based on the application state.
It acts as a traffic controller.
function ToggleTagAndClose({ tagName, onDone }) {
// State to track if we need to show a confirmation dialog
const [showConfirm, setShowConfirm] = React.useState(false);
const [sessionId, setSessionId] = React.useState(null);
// ... logic continues below
Explanation:
true, we will render a dialog asking "Are you sure?".
We use useEffect to check if the tag already exists.
React.useEffect(() => {
const id = getSessionId();
const currentTag = getCurrentSessionTag(id);
if (currentTag === tagName) {
// The tag already exists! Ask user to remove it.
setShowConfirm(true);
} else {
// Tag is new. Save it and exit immediately.
saveTag(id, tagName);
onDone(`Tagged session with #${tagName}`);
}
}, [tagName, onDone]);
Note: The code above is simplified for clarity.
Explanation:
onDone to close the app.setShowConfirm(true)). This triggers a re-render.
If showConfirm becomes true, the component re-renders and returns a visual dialog. This is where the Terminal UI shines.
if (showConfirm) {
return (
<ConfirmRemoveTag
tagName={tagName}
onConfirm={() => { /* Remove tag logic */ }}
onCancel={() => { /* Cancel logic */ }}
/>
);
}
return null; // Render nothing if we are just processing logic
ConfirmRemoveTag
This component uses UI elements provided by our design system (Dialog, Select) which are built on top of ink primitives (Box, Text).
function ConfirmRemoveTag({ tagName, onConfirm, onCancel }) {
return (
<Dialog title="Remove tag?" color="warning">
<Box flexDirection="column">
<Text>This will remove the tag.</Text>
{/* Interactive Selection Menu */}
<Select
onChange={(val) => val === 'yes' ? onConfirm() : onCancel()}
options={[
{ label: 'Yes, remove tag', value: 'yes' },
{ label: 'No, keep tag', value: 'no' }
]}
/>
</Box>
</Dialog>
)
}
Explanation:
<Dialog>: Draws a border box around the content.<Box>: Acts like a <div> (flexbox) to layout items vertically.<Select>: This is a custom component that listens to Arrow Up/Down and Enter keys.How does React run in a terminal? It doesn't have a DOM (Document Object Model).
stdout (Standard Output). It uses special hidden codes (ANSI escape codes) to move the cursor, change colors, and clear lines to simulate a "refresh."Here is what happens when a user tries to remove a tag:
The CLI framework needs to know how to handle this specific type of command. In our previous chapter, we set type: 'local-jsx'.
The framework sees this type and sets up an Ink instance.
// Framework Pseudo-code (Simplified)
import { render } from 'ink';
async function runLocalJSX(commandModule, args) {
return new Promise((resolve) => {
// 1. Define the 'onDone' function
const onDone = (message) => {
// Stop React and print final message
app.unmount();
console.log(message);
resolve();
};
// 2. Call the module to get the React Node
const ui = await commandModule.call(onDone, context, args);
// 3. Start Ink Rendering
const app = render(ui);
});
}
Explanation:
onDone.render() function from ink, connecting React to your terminal window.In this chapter, we learned:
call function is our entry point, returning JSX.useState and useEffect.<Box> and <Text> instead of <div> and <span>.onDone.
Now that we know how to render the interface, we need to understand how we actually save and retrieve the tag data (like the getSessionId function we saw earlier).
Next Chapter: Session State Management
Generated by Code IQ