0
# Pick
1
2
A 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.
3
4
## Package Information
5
6
- **Package Name**: pick
7
- **Package Type**: pypi
8
- **Language**: Python
9
- **Installation**: `pip install pick`
10
- **Version**: 2.4.0
11
12
## Core Imports
13
14
```python
15
from pick import pick
16
```
17
18
For advanced usage:
19
20
```python
21
from pick import pick, Picker, Option
22
```
23
24
## Basic Usage
25
26
```python
27
from pick import pick
28
29
# Simple selection
30
title = 'Please choose your favorite programming language: '
31
options = ['Java', 'JavaScript', 'Python', 'PHP', 'C++', 'Erlang', 'Haskell']
32
option, index = pick(options, title)
33
print(f"You chose {option} at index {index}")
34
35
# Multi-selection
36
title = 'Choose your favorite programming languages (press SPACE to mark, ENTER to continue): '
37
selected = pick(options, title, multiselect=True, min_selection_count=1)
38
print(selected) # [('Java', 0), ('C++', 4)]
39
```
40
41
## Architecture
42
43
Pick is built on Python's `curses` library, providing a terminal-based user interface with keyboard navigation. The architecture consists of:
44
45
- **Curses Integration**: Uses the standard curses library for terminal control, with automatic fallback handling for systems with limited color support
46
- **Keyboard Navigation**: Vi-style navigation (j/k) combined with arrow keys for intuitive movement
47
- **Selection Modes**: Supports both single-selection and multi-selection with visual indicators
48
- **Screen Management**: Handles scrolling for long option lists and integrates with existing curses applications
49
- **Event Loop**: Non-blocking input handling with configurable quit keys for flexible integration
50
51
The design prioritizes simplicity while maintaining flexibility for integration into larger terminal applications.
52
53
## Capabilities
54
55
### Basic Selection Function
56
57
The main `pick` function provides a simple interface for creating interactive selection menus with support for both single and multi-selection modes.
58
59
```python { .api }
60
def pick(
61
options: Sequence[OPTION_T],
62
title: Optional[str] = None,
63
indicator: str = "*",
64
default_index: int = 0,
65
multiselect: bool = False,
66
min_selection_count: int = 0,
67
screen: Optional["curses._CursesWindow"] = None,
68
position: Position = Position(0, 0),
69
clear_screen: bool = True,
70
quit_keys: Optional[Union[Container[int], Iterable[int]]] = None,
71
) -> Union[PICK_RETURN_T, List[PICK_RETURN_T]]:
72
"""
73
Create and run an interactive picker menu.
74
75
Parameters:
76
- options: Sequence of options to choose from (strings or Option objects)
77
- title: Optional title displayed above the options
78
- indicator: Selection indicator string (default: "*")
79
- default_index: Index of default selected option (default: 0)
80
- multiselect: Enable multi-selection mode (default: False)
81
- min_selection_count: Minimum selections required in multiselect mode (default: 0)
82
- screen: Existing curses window object for integration (default: None)
83
- position: Starting position as Position(y, x) namedtuple (default: Position(0, 0))
84
- clear_screen: Whether to clear screen before drawing (default: True)
85
- quit_keys: Key codes that quit the menu early (default: None)
86
87
Returns:
88
- Single selection: (option, index) tuple
89
- Multi-selection: List of (option, index) tuples
90
- Early quit: (None, -1) for single mode, [] for multi mode
91
"""
92
```
93
94
#### Usage Examples
95
96
```python
97
# Custom indicator and default selection
98
option, index = pick(
99
options,
100
title,
101
indicator="=>",
102
default_index=2
103
)
104
105
# Multi-selection with minimum requirement
106
selected = pick(
107
options,
108
title,
109
multiselect=True,
110
min_selection_count=2
111
)
112
113
# With quit keys (Ctrl+C, Escape, 'q')
114
KEY_CTRL_C = 3
115
KEY_ESCAPE = 27
116
QUIT_KEYS = (KEY_CTRL_C, KEY_ESCAPE, ord("q"))
117
118
option, index = pick(
119
options,
120
title,
121
quit_keys=QUIT_KEYS
122
)
123
```
124
125
### Advanced Picker Class
126
127
The `Picker` class provides more control over the selection interface and allows for custom configuration and integration with existing curses applications.
128
129
```python { .api }
130
class Picker(Generic[OPTION_T]):
131
"""
132
Interactive picker class for terminal-based selection menus.
133
"""
134
135
def __init__(
136
self,
137
options: Sequence[OPTION_T],
138
title: Optional[str] = None,
139
indicator: str = "*",
140
default_index: int = 0,
141
multiselect: bool = False,
142
min_selection_count: int = 0,
143
screen: Optional["curses._CursesWindow"] = None,
144
position: Position = Position(0, 0),
145
clear_screen: bool = True,
146
quit_keys: Optional[Union[Container[int], Iterable[int]]] = None,
147
):
148
"""
149
Initialize picker with configuration options.
150
151
Parameters: Same as pick() function
152
153
Raises:
154
- ValueError: If options is empty
155
- ValueError: If default_index >= len(options)
156
- ValueError: If min_selection_count > len(options) in multiselect mode
157
- ValueError: If all options are disabled
158
"""
159
160
def move_up(self) -> None:
161
"""Move selection cursor up, wrapping to bottom and skipping disabled options."""
162
163
def move_down(self) -> None:
164
"""Move selection cursor down, wrapping to top and skipping disabled options."""
165
166
def mark_index(self) -> None:
167
"""Toggle selection of current option in multiselect mode."""
168
169
def get_selected(self) -> Union[PICK_RETURN_T, List[PICK_RETURN_T]]:
170
"""
171
Get current selection(s).
172
173
Returns:
174
- Single selection: (option, index) tuple
175
- Multi-selection: List of (option, index) tuples
176
"""
177
178
def get_title_lines(self, *, max_width: int = 80) -> List[str]:
179
"""Get formatted title lines with word wrapping."""
180
181
def get_option_lines(self) -> List[str]:
182
"""Get formatted option lines with indicators and multi-select symbols."""
183
184
def get_lines(self, *, max_width: int = 80) -> Tuple[List[str], int]:
185
"""
186
Get all display lines (title + options) and current line position.
187
188
Returns:
189
- Tuple of (lines, current_line_position)
190
"""
191
192
def draw(self, screen: "curses._CursesWindow") -> None:
193
"""Draw the picker UI on the screen with scroll handling."""
194
195
def config_curses(self) -> None:
196
"""Configure curses settings (colors, cursor visibility)."""
197
198
def run_loop(
199
self,
200
screen: "curses._CursesWindow",
201
position: Position
202
) -> Union[PICK_RETURN_T, List[PICK_RETURN_T]]:
203
"""
204
Run the main interaction loop handling keyboard input.
205
206
Parameters:
207
- screen: Curses window object
208
- position: Starting position for drawing
209
210
Returns: Same format as get_selected()
211
"""
212
213
def start(self) -> Union[PICK_RETURN_T, List[PICK_RETURN_T]]:
214
"""
215
Start the picker interface and return user selection.
216
217
Returns: Same format as get_selected()
218
"""
219
```
220
221
#### Advanced Usage Examples
222
223
```python
224
from pick import Picker, Option
225
226
# Create picker instance
227
picker = Picker(
228
options=['Option 1', 'Option 2', 'Option 3'],
229
title='Choose an option:',
230
multiselect=True
231
)
232
233
# Programmatic navigation
234
picker.move_down()
235
picker.mark_index() # Select current option
236
237
# Get current state
238
current_selection = picker.get_selected()
239
240
# Start interactive mode
241
final_selection = picker.start()
242
```
243
244
### Option Objects
245
246
The `Option` class allows for rich option definitions with labels, values, descriptions, and enable/disable states.
247
248
```python { .api }
249
@dataclass
250
class Option:
251
"""
252
Represents a selectable option with metadata.
253
"""
254
255
def __init__(
256
self,
257
label: str,
258
value: Any = None,
259
description: Optional[str] = None,
260
enabled: bool = True
261
):
262
"""
263
Create an option object.
264
265
Parameters:
266
- label: Display text for the option
267
- value: Associated value (defaults to None)
268
- description: Optional description shown on selection
269
- enabled: Whether option can be selected (default: True)
270
"""
271
272
label: str
273
value: Any
274
description: Optional[str]
275
enabled: bool
276
```
277
278
#### Option Usage Examples
279
280
```python
281
from pick import pick, Option
282
283
# Options with values and descriptions
284
options = [
285
Option("Python", ".py", "High-level, general-purpose programming language"),
286
Option("Java", ".java", "Class-based, object-oriented programming language"),
287
Option("JavaScript", ".js"),
288
Option("Disabled Option", enabled=False) # This option cannot be selected
289
]
290
291
option, index = pick(options, "Choose a language:")
292
print(f"Selected: {option.label} with value: {option.value}")
293
294
# Options with complex values
295
database_options = [
296
Option("PostgreSQL", {"driver": "psycopg2", "port": 5432}),
297
Option("MySQL", {"driver": "mysql", "port": 3306}),
298
Option("SQLite", {"driver": "sqlite3", "file": "db.sqlite"})
299
]
300
301
selected_db, _ = pick(database_options, "Choose database:")
302
config = selected_db.value
303
print(f"Using {selected_db.label} with config: {config}")
304
```
305
306
## Types
307
308
```python { .api }
309
from collections import namedtuple
310
from typing import Any, Container, Generic, Iterable, List, Optional, Sequence, Tuple, TypeVar, Union
311
312
Position = namedtuple('Position', ['y', 'x'])
313
"""Screen position coordinates as (y, x) tuple."""
314
315
OPTION_T = TypeVar("OPTION_T", str, Option)
316
"""Generic type variable for option types (string or Option objects)."""
317
318
PICK_RETURN_T = Tuple[OPTION_T, int]
319
"""Return type for pick results: (option, index) tuple."""
320
```
321
322
## Constants
323
324
```python { .api }
325
# Keyboard navigation key codes
326
KEYS_ENTER = (curses.KEY_ENTER, ord("\n"), ord("\r")) # Enter, Return, Keypad Enter
327
KEYS_UP = (curses.KEY_UP, ord("k")) # Up arrow, 'k'
328
KEYS_DOWN = (curses.KEY_DOWN, ord("j")) # Down arrow, 'j'
329
KEYS_SELECT = (curses.KEY_RIGHT, ord(" ")) # Right arrow, Space
330
331
# Multi-select symbols
332
SYMBOL_CIRCLE_FILLED = "(x)" # Selected indicator
333
SYMBOL_CIRCLE_EMPTY = "( )" # Unselected indicator
334
```
335
336
## Error Handling
337
338
The package raises `ValueError` exceptions in the following cases:
339
340
- **Empty options list**: When `options` parameter is empty
341
- **Invalid default_index**: When `default_index >= len(options)`
342
- **Invalid min_selection_count**: When `min_selection_count > len(options)` in multiselect mode
343
- **All options disabled**: When all `Option` objects have `enabled=False`
344
345
```python
346
from pick import pick, Option
347
348
try:
349
# This will raise ValueError
350
pick([], "Choose from nothing:")
351
except ValueError as e:
352
print(f"Error: {e}")
353
354
try:
355
# This will also raise ValueError
356
disabled_options = [
357
Option("Option 1", enabled=False),
358
Option("Option 2", enabled=False)
359
]
360
pick(disabled_options, "All disabled:")
361
except ValueError as e:
362
print(f"Error: {e}")
363
```
364
365
## Integration with Existing Curses Applications
366
367
The pick library can be integrated into existing curses applications by passing a screen object:
368
369
```python
370
import curses
371
from pick import pick
372
373
def main(screen):
374
# Your existing curses setup
375
curses.curs_set(0)
376
377
# Use pick within your curses app
378
options = ['Option 1', 'Option 2', 'Option 3']
379
option, index = pick(
380
options,
381
"Choose an option:",
382
screen=screen,
383
position=(5, 10), # Position within your screen
384
clear_screen=False # Don't clear your existing content
385
)
386
387
# Continue with your curses application
388
screen.addstr(0, 0, f"You selected: {option}")
389
screen.refresh()
390
391
curses.wrapper(main)
392
```