Welcome back!
In the previous chapter, Command Categorization Strategy, we acted like a librarian sorting books. We took a pile of commands and organized them into "buckets" (Built-in, Custom, Internal).
Now, we need to actually display those books on the shelf.
Simply printing a list of names isn't enough. What if the description is too long? What if the same command appears twice? What if the list is longer than the screen?
In this chapter, we will build the Command Catalog Renderer (Commands.tsx). This component acts like a "Smart Directory" that cleans up, formats, and displays our lists.
We need a component that takes a raw list of commands and transforms it into a beautiful, interactive menu.
Imagine the user has a custom script named deploy.
deploy globally on their machine.deploy in their current project folder.
If we just listed everything, the user would see deploy twice. That is confusing!
Our Component Responsibilities:
We are working in Commands.tsx. Let's build this "Smart Directory" piece by piece.
First, we define what this component needs to function. It needs the data (commands), the screen limits (height/columns), and a title.
// Commands.tsx
type Props = {
commands: Command[]; // The raw list
maxHeight: number; // How tall can we be?
columns: number; // How wide is the terminal?
title: string; // "General" or "Custom"?
onCancel: () => void; // What to do on "Esc"
emptyMessage?: string; // What if the list is empty?
};
Explanation:
maxHeight and columns are crucial. They tell us exactly how much screen real estate we own, so we don't draw outside the lines.Before we process the text, we need to know our physical limits.
export function Commands({ commands, maxHeight, columns, ...props }: Props) {
// Reserve 10 columns for padding/margins
const maxWidth = Math.max(1, columns - 10);
// Reserve 10 rows for headers/footers, split remaining by 2
const visibleCount = Math.max(1, Math.floor((maxHeight - 10) / 2));
// Logic continues...
}
Explanation:
This is the "Brain" of the component. We use useMemo to process the data only when it changes. This prevents the computer from doing heavy work on every single frame.
We need to remove duplicates using a Set.
const options = useMemo(() => {
const seen = new Set<string>(); // Keeps track of names we've seen
return commands.filter(cmd => {
// If we've seen "deploy" before, skip this one!
if (seen.has(cmd.name)) return false;
seen.add(cmd.name); // Mark "deploy" as seen
return true;
});
// ... sorting continues in next block
}, [commands]);
Explanation:
Set as a club guest list. We check the list before letting a command in. If deploy is already on the list, the second deploy is turned away.Immediately after filtering, we sort the list alphabetically and format the text for the screen.
// ... continuining the chain from above
.sort((a, b) => a.name.localeCompare(b.name)) // Sort A-Z
.map(cmd => ({
label: `/${cmd.name}`, // Add a slash for style
value: cmd.name,
// Truncate description if it's too wide
description: truncate(cmd.description, maxWidth, true),
}));
Explanation:
localeCompare: The standard way to alphabetize text in JavaScript.truncate: A utility function. If maxWidth is 50 chars, and the description is 100 chars, it cuts it off and adds "..." so the line doesn't break.
Now that our data is clean, sorted, and formatted, we pass it to the <Select> component. This is a pre-built UI component that handles the arrow keys and highlighting.
return (
<Box flexDirection="column" paddingY={1}>
<Text>{title}</Text>
<Box marginTop={1}>
<Select
options={options}
visibleOptionCount={visibleCount}
onCancel={props.onCancel}
// ... other props
/>
</Box>
</Box>
);
Explanation:
Box (container).title (e.g., "Custom Commands").options to the Select component.How does the data flow from the parent component into the pixels on the screen?
Without this abstraction, every part of our app that lists commands would need to rewrite the logic for:
By creating the Command Catalog Renderer, we centralize all that messy logic.
If we ever want to change how we handle duplicates (e.g., "Show both but label them differently"), we only have to change code in one file, and it updates everywhere.
In this chapter, we built the visual engine for our help system.
We learned how to:
columns and maxHeight to keep our UI tidy.Set to remove duplicate entries.We have a container (Chapter 1), a welcome screen (Chapter 2), a sorting strategy (Chapter 3), and now a renderer (Chapter 4).
However, terminal screens come in all shapes and sizes. What happens if the user resizes their window while the help menu is open? How do we ensure everything stays responsive?
Next Chapter: Responsive Terminal Layout
Generated by Code IQ