Welcome back! In the previous chapter, Structural Containers, we learned how to build the "walls" and "frames" of our application using Panes and Dividers.
However, a CLI tool where you can only look at things isn't very useful. You need to be able to make choices. Whether it's selecting a file to delete, picking a deployment region, or choosing a pizza topping, you need a menu.
In this chapter, we will build the Interactive List Picker.
Building a menu in a terminal is deceptively difficult.
> arrow).
The Solution: We use the FuzzyPicker. This is a high-level component that handles the math, the keyboard events, and the filtering for you. You just give it a list of data.
Before we code, let's understand the two main parts of this system:
>) pointer.Let's imagine our CLI tool initializes new projects. We want to ask the user: "Which framework do you want to use?"
First, we need a list of options.
const frameworks = [
{ id: 'react', label: 'React' },
{ id: 'vue', label: 'Vue' },
{ id: 'angular', label: 'Angular' },
{ id: 'svelte', label: 'Svelte' },
];
The Picker needs to know how to draw each row. We provide a small function that returns a component.
import { Text } from 'ink';
// This function runs for every visible item
const renderFramework = (item, isFocused) => (
<Text color={isFocused ? 'green' : 'white'}>
{item.label}
</Text>
);
Now we drop in the FuzzyPicker.
import { FuzzyPicker } from './design-system';
<FuzzyPicker
title="Choose a Framework"
items={frameworks}
renderItem={renderFramework}
getKey={(item) => item.id}
onSelect={(item) => console.log('You picked:', item.label)}
/>
What happens here?
Enter triggers onSelect.
In the example above, we manually changed the text color. However, our design system provides a pre-made ListItem component that handles standard styling, pointers (>), and checkmarks (โ).
Let's upgrade our render function to use it.
import { ListItem } from './design-system';
const renderFramework = (item, isFocused) => (
<ListItem isFocused={isFocused}>
{item.label}
</ListItem>
);
Why use this?
ListItem automatically talks to the Theming Context. If you switch to "Light Mode," the ListItem knows exactly what color the focused text should be, without you manually setting it to "green".
The magic of the List Picker is how it handles Scrolling. It uses a technique called a "Sliding Window."
Imagine you have a long strip of paper (your list) behind a small window (your terminal screen).
focusedIndex (e.g., item #10).windowStart. If we can only see 5 items, and we are at item #10, our window might start at item #6.items.slice(6, 11) to get only the items we need to render right now.
Let's look at the actual code in design-system to see how this sliding window is built.
ListItem.tsx)This component decides what "Icon" to show on the left side of the text.
// ListItem.tsx (Simplified)
function renderIndicator() {
if (isFocused) {
// The "Pointer"
return <Text color="suggestion">โฏ</Text>;
}
if (isSelected) {
// The "Checkmark"
return <Text color="success">โ</Text>;
}
return <Text> </Text>; // Empty space for alignment
}
Beginner Note: Notice the use of semantic colors like suggestion and success. This relies on the Theme-Aware Primitives concept.
FuzzyPicker.tsx)Inside the main component, we determine which items are actually visible.
// FuzzyPicker.tsx
const windowStart = clamp(
focusedIndex - visibleCount + 1, // Try to keep cursor at bottom
0, // Don't go below 0
items.length - visibleCount // Don't scroll past the end
);
// Get ONLY the items that fit in the window
const visibleItems = items.slice(windowStart, windowStart + visibleCount);
Explanation:
clamp ensures we don't try to render item number -5 or item number 1000 in a list of 10. We essentially "slide" the viewing rectangle based on where your focusedIndex is.
We use a custom hook to listen for arrow keys.
// FuzzyPicker.tsx (Simplified Input Handler)
const handleKeyDown = (e) => {
if (e.key === 'up') {
// Move focus up, but stop at 0
setFocusedIndex(i => Math.max(0, i - 1));
}
if (e.key === 'down') {
// Move focus down, stop at last item
setFocusedIndex(i => Math.min(items.length - 1, i + 1));
}
if (e.key === 'return') {
// Select the currently focused item
onSelect(items[focusedIndex]);
}
};
Beginner Note: We stop the event propagation (e.preventDefault) so that pressing "Enter" doesn't accidentally trigger other things in your app.
The Interactive List Picker is one of the most powerful tools in your CLI arsenal.
By combining FuzzyPicker with ListItem, you can build professional-grade menus in seconds.
But what if you have too many options to put in a single list? What if you need to categorize settings into "General," "Network," and "Display"? You need tabs.
In the next chapter, we will organize our UI using the Tabbed Interface.
Next Chapter: Tabbed Interface
Generated by Code IQ