Pick an option in the terminal with a simple GUI
npx @tessl/cli install tessl/pypi-pick@2.4.0A lightweight Python library for creating interactive terminal-based selection menus using the curses library. It enables developers to build command-line interfaces with intuitive selection capabilities, supporting both single-choice and multi-choice selection modes with keyboard navigation.
pip install pickfrom pick import pickFor advanced usage:
from pick import pick, Picker, Optionfrom pick import pick
# Simple selection
title = 'Please choose your favorite programming language: '
options = ['Java', 'JavaScript', 'Python', 'PHP', 'C++', 'Erlang', 'Haskell']
option, index = pick(options, title)
print(f"You chose {option} at index {index}")
# Multi-selection
title = 'Choose your favorite programming languages (press SPACE to mark, ENTER to continue): '
selected = pick(options, title, multiselect=True, min_selection_count=1)
print(selected) # [('Java', 0), ('C++', 4)]Pick is built on Python's curses library, providing a terminal-based user interface with keyboard navigation. The architecture consists of:
The design prioritizes simplicity while maintaining flexibility for integration into larger terminal applications.
The main pick function provides a simple interface for creating interactive selection menus with support for both single and multi-selection modes.
def pick(
options: Sequence[OPTION_T],
title: Optional[str] = None,
indicator: str = "*",
default_index: int = 0,
multiselect: bool = False,
min_selection_count: int = 0,
screen: Optional["curses._CursesWindow"] = None,
position: Position = Position(0, 0),
clear_screen: bool = True,
quit_keys: Optional[Union[Container[int], Iterable[int]]] = None,
) -> Union[PICK_RETURN_T, List[PICK_RETURN_T]]:
"""
Create and run an interactive picker menu.
Parameters:
- options: Sequence of options to choose from (strings or Option objects)
- title: Optional title displayed above the options
- indicator: Selection indicator string (default: "*")
- default_index: Index of default selected option (default: 0)
- multiselect: Enable multi-selection mode (default: False)
- min_selection_count: Minimum selections required in multiselect mode (default: 0)
- screen: Existing curses window object for integration (default: None)
- position: Starting position as Position(y, x) namedtuple (default: Position(0, 0))
- clear_screen: Whether to clear screen before drawing (default: True)
- quit_keys: Key codes that quit the menu early (default: None)
Returns:
- Single selection: (option, index) tuple
- Multi-selection: List of (option, index) tuples
- Early quit: (None, -1) for single mode, [] for multi mode
"""# Custom indicator and default selection
option, index = pick(
options,
title,
indicator="=>",
default_index=2
)
# Multi-selection with minimum requirement
selected = pick(
options,
title,
multiselect=True,
min_selection_count=2
)
# With quit keys (Ctrl+C, Escape, 'q')
KEY_CTRL_C = 3
KEY_ESCAPE = 27
QUIT_KEYS = (KEY_CTRL_C, KEY_ESCAPE, ord("q"))
option, index = pick(
options,
title,
quit_keys=QUIT_KEYS
)The Picker class provides more control over the selection interface and allows for custom configuration and integration with existing curses applications.
class Picker(Generic[OPTION_T]):
"""
Interactive picker class for terminal-based selection menus.
"""
def __init__(
self,
options: Sequence[OPTION_T],
title: Optional[str] = None,
indicator: str = "*",
default_index: int = 0,
multiselect: bool = False,
min_selection_count: int = 0,
screen: Optional["curses._CursesWindow"] = None,
position: Position = Position(0, 0),
clear_screen: bool = True,
quit_keys: Optional[Union[Container[int], Iterable[int]]] = None,
):
"""
Initialize picker with configuration options.
Parameters: Same as pick() function
Raises:
- ValueError: If options is empty
- ValueError: If default_index >= len(options)
- ValueError: If min_selection_count > len(options) in multiselect mode
- ValueError: If all options are disabled
"""
def move_up(self) -> None:
"""Move selection cursor up, wrapping to bottom and skipping disabled options."""
def move_down(self) -> None:
"""Move selection cursor down, wrapping to top and skipping disabled options."""
def mark_index(self) -> None:
"""Toggle selection of current option in multiselect mode."""
def get_selected(self) -> Union[PICK_RETURN_T, List[PICK_RETURN_T]]:
"""
Get current selection(s).
Returns:
- Single selection: (option, index) tuple
- Multi-selection: List of (option, index) tuples
"""
def get_title_lines(self, *, max_width: int = 80) -> List[str]:
"""Get formatted title lines with word wrapping."""
def get_option_lines(self) -> List[str]:
"""Get formatted option lines with indicators and multi-select symbols."""
def get_lines(self, *, max_width: int = 80) -> Tuple[List[str], int]:
"""
Get all display lines (title + options) and current line position.
Returns:
- Tuple of (lines, current_line_position)
"""
def draw(self, screen: "curses._CursesWindow") -> None:
"""Draw the picker UI on the screen with scroll handling."""
def config_curses(self) -> None:
"""Configure curses settings (colors, cursor visibility)."""
def run_loop(
self,
screen: "curses._CursesWindow",
position: Position
) -> Union[PICK_RETURN_T, List[PICK_RETURN_T]]:
"""
Run the main interaction loop handling keyboard input.
Parameters:
- screen: Curses window object
- position: Starting position for drawing
Returns: Same format as get_selected()
"""
def start(self) -> Union[PICK_RETURN_T, List[PICK_RETURN_T]]:
"""
Start the picker interface and return user selection.
Returns: Same format as get_selected()
"""from pick import Picker, Option
# Create picker instance
picker = Picker(
options=['Option 1', 'Option 2', 'Option 3'],
title='Choose an option:',
multiselect=True
)
# Programmatic navigation
picker.move_down()
picker.mark_index() # Select current option
# Get current state
current_selection = picker.get_selected()
# Start interactive mode
final_selection = picker.start()The Option class allows for rich option definitions with labels, values, descriptions, and enable/disable states.
@dataclass
class Option:
"""
Represents a selectable option with metadata.
"""
def __init__(
self,
label: str,
value: Any = None,
description: Optional[str] = None,
enabled: bool = True
):
"""
Create an option object.
Parameters:
- label: Display text for the option
- value: Associated value (defaults to None)
- description: Optional description shown on selection
- enabled: Whether option can be selected (default: True)
"""
label: str
value: Any
description: Optional[str]
enabled: boolfrom pick import pick, Option
# Options with values and descriptions
options = [
Option("Python", ".py", "High-level, general-purpose programming language"),
Option("Java", ".java", "Class-based, object-oriented programming language"),
Option("JavaScript", ".js"),
Option("Disabled Option", enabled=False) # This option cannot be selected
]
option, index = pick(options, "Choose a language:")
print(f"Selected: {option.label} with value: {option.value}")
# Options with complex values
database_options = [
Option("PostgreSQL", {"driver": "psycopg2", "port": 5432}),
Option("MySQL", {"driver": "mysql", "port": 3306}),
Option("SQLite", {"driver": "sqlite3", "file": "db.sqlite"})
]
selected_db, _ = pick(database_options, "Choose database:")
config = selected_db.value
print(f"Using {selected_db.label} with config: {config}")from collections import namedtuple
from typing import Any, Container, Generic, Iterable, List, Optional, Sequence, Tuple, TypeVar, Union
Position = namedtuple('Position', ['y', 'x'])
"""Screen position coordinates as (y, x) tuple."""
OPTION_T = TypeVar("OPTION_T", str, Option)
"""Generic type variable for option types (string or Option objects)."""
PICK_RETURN_T = Tuple[OPTION_T, int]
"""Return type for pick results: (option, index) tuple."""# Keyboard navigation key codes
KEYS_ENTER = (curses.KEY_ENTER, ord("\n"), ord("\r")) # Enter, Return, Keypad Enter
KEYS_UP = (curses.KEY_UP, ord("k")) # Up arrow, 'k'
KEYS_DOWN = (curses.KEY_DOWN, ord("j")) # Down arrow, 'j'
KEYS_SELECT = (curses.KEY_RIGHT, ord(" ")) # Right arrow, Space
# Multi-select symbols
SYMBOL_CIRCLE_FILLED = "(x)" # Selected indicator
SYMBOL_CIRCLE_EMPTY = "( )" # Unselected indicatorThe package raises ValueError exceptions in the following cases:
options parameter is emptydefault_index >= len(options)min_selection_count > len(options) in multiselect modeOption objects have enabled=Falsefrom pick import pick, Option
try:
# This will raise ValueError
pick([], "Choose from nothing:")
except ValueError as e:
print(f"Error: {e}")
try:
# This will also raise ValueError
disabled_options = [
Option("Option 1", enabled=False),
Option("Option 2", enabled=False)
]
pick(disabled_options, "All disabled:")
except ValueError as e:
print(f"Error: {e}")The pick library can be integrated into existing curses applications by passing a screen object:
import curses
from pick import pick
def main(screen):
# Your existing curses setup
curses.curs_set(0)
# Use pick within your curses app
options = ['Option 1', 'Option 2', 'Option 3']
option, index = pick(
options,
"Choose an option:",
screen=screen,
position=(5, 10), # Position within your screen
clear_screen=False # Don't clear your existing content
)
# Continue with your curses application
screen.addstr(0, 0, f"You selected: {option}")
screen.refresh()
curses.wrapper(main)