Welcome back! In the previous chapter, Global Application State, we built the "brain" and "memory" of our fast command. We know how to toggle the setting in memory and save it to a file.
But right now, if you ran the command, nothing would happen on the screen. The user would be staring at a blank terminal.
In this chapter, we will build the Terminal UI (TUI). This is the "face" of our feature.
In the old days of command-line tools, programs just printed text line-by-line using console.log.
> Task started.
> Task finished.
This is fine for simple scripts, but modern CLI tools need to be interactive. We want to:
To do this, we use a technology called Ink. It allows us to write React components (just like a website), but instead of rendering HTML to a browser, it renders text to your terminal window.
We are building the visual part of the Fast Mode Picker.
(Conceptual visualization: A box with text inside)
It needs to display:
If you have used React for the web, you know <div> and <span>. In the terminal, we don't have a browser engine, so we use equivalents provided by the Ink library:
<Box>: The replacement for <div>. It handles layout (Flexbox), margins, and padding.<Text>: The replacement for <span> or <p>. All text must be wrapped in this tag.Dialog ComponentThis is a custom component built for our project. It draws a nice border around our content and handles the title and helper text (like "Press Enter to confirm").
Just like a website, when our Global Application State changes, our component "re-renders" to update the terminal text instantly.
Let's look at fast.tsx and see how we construct the visual interface.
We start by importing our building blocks.
import * as React from 'react';
import { Box, Text } from '../../ink.js'; // Our TUI primitives
import { Dialog } from '../../components/design-system/Dialog.js';
import { FastIcon } from '../../components/FastIcon.js';
// The Component
export function FastModePicker({ onDone, unavailableReason }) {
// ... hooks from Chapter 3 go here ...
Explanation:
FastModePicker.unavailableReason so it knows if it should display an error message.Box and Text)We want to show the status. We use a Flexbox row layout.
// Inside the component return statement
<Box flexDirection="column" marginLeft={2}>
<Box flexDirection="row" gap={2}>
<Text bold>Fast mode</Text>
{/* Dynamic Color changing based on state */}
<Text color={enableFastMode ? "green" : "gray"}>
{enableFastMode ? "ON " : "OFF"}
</Text>
<Text dimColor>{pricing}</Text>
</Box>
</Box>
Explanation:
<Box flexDirection="row">: Places items side-by-side.<Text color={...}>: This is where the magic happens. If enableFastMode is true (from our State), the text turns green. If false, it turns gray.dimColor: A helper prop to make the pricing text less distracting.Dialog
Finally, we wrap that content in our Dialog component to give it a professional look.
return (
<Dialog
title={<Text><FastIcon /> Fast mode</Text>}
subtitle="High-speed mode. Billed as extra usage."
color="fastMode" // Theme color
inputGuide={<Text>Tab to toggle ยท Enter to confirm</Text>}
>
{/* The content from Step 2 goes here */}
{content}
</Dialog>
);
Explanation:
Dialog: Draws the border and standardizes the look.title: We can pass other components (like icons) into the title.inputGuide: Tells the user which keys to press.It might seem strange that we are writing HTML-like code for a black-and-white terminal. Here is how it works:
stdout.
The file fast.tsx handles a complex logic: what if Fast Mode is unavailable (e.g., server down)?
We use Conditional Rendering to swap the entire UI if there is an error.
// Inside FastModePicker
if (unavailableReason) {
return (
<Dialog title="...">
<Box marginLeft={2}>
{/* Render Error Message in Red */}
<Text color="error">{unavailableReason}</Text>
</Box>
</Dialog>
);
}
// Otherwise render the normal UI...
Explanation:
unavailableReason exists, we ignore the "ON/OFF" switch and just show the error in red.In this chapter, we learned about Terminal UI (TUI) Rendering:
<Box> (for layout) and <Text> (for content).Dialog to make it look consistent with the rest of the app.We have a beautiful interface that shows the state. But currently, if you press "Tab" or "Enter", nothing happens! The UI looks ready, but it isn't listening to your keyboard yet.
Next: Keyboard Input Abstraction
Generated by Code IQ