pyclack-lib 1.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pyclack_lib-1.0.0/PKG-INFO +2212 -0
- pyclack_lib-1.0.0/README.md +2199 -0
- pyclack_lib-1.0.0/pyproject.toml +39 -0
- pyclack_lib-1.0.0/pyproject.toml.orig +32 -0
- pyclack_lib-1.0.0/src/pyclack/__init__.py +7 -0
- pyclack_lib-1.0.0/src/pyclack/config/__init__.py +3 -0
- pyclack_lib-1.0.0/src/pyclack/config/theme_conf.py +32 -0
- pyclack_lib-1.0.0/src/pyclack/prompts/__init__.py +19 -0
- pyclack_lib-1.0.0/src/pyclack/prompts/ask.py +258 -0
- pyclack_lib-1.0.0/src/pyclack/prompts/autocomplete.py +466 -0
- pyclack_lib-1.0.0/src/pyclack/prompts/autocomplete_multiselect.py +564 -0
- pyclack_lib-1.0.0/src/pyclack/prompts/confirm.py +203 -0
- pyclack_lib-1.0.0/src/pyclack/prompts/multiline.py +279 -0
- pyclack_lib-1.0.0/src/pyclack/prompts/multiselect.py +472 -0
- pyclack_lib-1.0.0/src/pyclack/prompts/password.py +251 -0
- pyclack_lib-1.0.0/src/pyclack/prompts/pick_date.py +444 -0
- pyclack_lib-1.0.0/src/pyclack/prompts/prompt_base.py +210 -0
- pyclack_lib-1.0.0/src/pyclack/prompts/select.py +346 -0
- pyclack_lib-1.0.0/src/pyclack/prompts/select_key.py +318 -0
- pyclack_lib-1.0.0/src/pyclack/prompts/select_path.py +521 -0
- pyclack_lib-1.0.0/src/pyclack/prompts/util.py +785 -0
- pyclack_lib-1.0.0/src/pyclack/prompts_async/__init__.py +11 -0
- pyclack_lib-1.0.0/src/pyclack/prompts_async/async_prompts.py +458 -0
- pyclack_lib-1.0.0/src/pyclack/py.typed +0 -0
- pyclack_lib-1.0.0/src/pyclack/renderer/__init__.py +9 -0
- pyclack_lib-1.0.0/src/pyclack/renderer/render_frame.py +122 -0
- pyclack_lib-1.0.0/src/pyclack/renderer/symbols.py +233 -0
- pyclack_lib-1.0.0/src/pyclack/renderer/text.py +146 -0
- pyclack_lib-1.0.0/src/pyclack/renderer/themes.py +1479 -0
- pyclack_lib-1.0.0/src/pyclack/terminal/__init__.py +4 -0
- pyclack_lib-1.0.0/src/pyclack/terminal/base_key.py +37 -0
- pyclack_lib-1.0.0/src/pyclack/terminal/cursor_controller.py +139 -0
- pyclack_lib-1.0.0/src/pyclack/terminal/echo_controller.py +60 -0
- pyclack_lib-1.0.0/src/pyclack/terminal/key_reader.py +60 -0
- pyclack_lib-1.0.0/src/pyclack/terminal/os_utils.py +19 -0
- pyclack_lib-1.0.0/src/pyclack/terminal/posix_keys.py +40 -0
- pyclack_lib-1.0.0/src/pyclack/terminal/stdout.py +33 -0
- pyclack_lib-1.0.0/src/pyclack/terminal/win_keys.py +34 -0
- pyclack_lib-1.0.0/src/pyclack/widgets/__init__.py +13 -0
- pyclack_lib-1.0.0/src/pyclack/widgets/activity.py +317 -0
- pyclack_lib-1.0.0/src/pyclack/widgets/box.py +81 -0
- pyclack_lib-1.0.0/src/pyclack/widgets/cancel.py +37 -0
- pyclack_lib-1.0.0/src/pyclack/widgets/intro.py +33 -0
- pyclack_lib-1.0.0/src/pyclack/widgets/log/__init__.py +1 -0
- pyclack_lib-1.0.0/src/pyclack/widgets/log/log.py +214 -0
- pyclack_lib-1.0.0/src/pyclack/widgets/note.py +51 -0
- pyclack_lib-1.0.0/src/pyclack/widgets/outro.py +34 -0
- pyclack_lib-1.0.0/src/pyclack/widgets/progress.py +329 -0
- pyclack_lib-1.0.0/src/pyclack/widgets/spinner.py +283 -0
- pyclack_lib-1.0.0/src/pyclack/widgets/stream/__init__.py +3 -0
- pyclack_lib-1.0.0/src/pyclack/widgets/stream/stream.py +40 -0
- pyclack_lib-1.0.0/src/pyclack/widgets/stream/util.py +81 -0
- pyclack_lib-1.0.0/src/pyclack/widgets/task_log.py +218 -0
- pyclack_lib-1.0.0/src/pyclack/widgets_async/__init__.py +11 -0
- pyclack_lib-1.0.0/src/pyclack/widgets_async/async_log/__init__.py +1 -0
- pyclack_lib-1.0.0/src/pyclack/widgets_async/async_log/async_log.py +85 -0
- pyclack_lib-1.0.0/src/pyclack/widgets_async/async_stream/__init__.py +3 -0
- pyclack_lib-1.0.0/src/pyclack/widgets_async/async_stream/async_stream.py +33 -0
- pyclack_lib-1.0.0/src/pyclack/widgets_async/async_widgets.py +502 -0
|
@@ -0,0 +1,2212 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: pyclack-lib
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: A Python library for building beautiful interactive CLI prompts with clack-style UX.
|
|
5
|
+
Author: Maddox-RVS
|
|
6
|
+
Author-email: Maddox-RVS <>
|
|
7
|
+
Requires-Dist: colorama>=0.4.6
|
|
8
|
+
Requires-Dist: readchar>=4.2.2
|
|
9
|
+
Requires-Dist: rich>=15.0.0
|
|
10
|
+
Requires-Dist: strsimpy>=0.2.1
|
|
11
|
+
Requires-Python: >=3.10
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
|
|
14
|
+

|
|
15
|
+

|
|
16
|
+
[](LICENSE)
|
|
17
|
+
|
|
18
|
+

|
|
19
|
+

|
|
20
|
+
|
|
21
|
+
# pyclack
|
|
22
|
+
|
|
23
|
+
**pyclack** is a Python port of [Clack](https://github.com/bombshell-dev/clack), the beautiful and minimal command-line prompt library for JavaScript, originally created by [Nate Moore (@natemoo-re)](https://github.com/natemoo-re).
|
|
24
|
+
|
|
25
|
+
pyclack brings Clack's interactive prompts, terminal UI components, and styling to Python while maintaining the same philosophy of providing a simple API for building beautiful command-line applications.
|
|
26
|
+
|
|
27
|
+
> [!NOTE]
|
|
28
|
+
> pyclack is an independent Python implementation inspired by Clack. It is not affiliated with or maintained by the Clack/Bombshell project.
|
|
29
|
+
|
|
30
|
+

|
|
31
|
+
|
|
32
|
+
I miss-spelled "writing" 😔, but I'm too lazy to re-record and edit this GIF again so deal with it...
|
|
33
|
+
|
|
34
|
+
# Features
|
|
35
|
+
|
|
36
|
+
- Interactive prompts for text input, multiline input, passwords, confirmations, selections, autocomplete, dates, and filesystem paths
|
|
37
|
+
- Synchronous and asynchronous APIs
|
|
38
|
+
- Animated spinners and progress bars
|
|
39
|
+
- Streaming output
|
|
40
|
+
- Task logging and semantic logging
|
|
41
|
+
- Intro, outro, cancellation, notes, and boxed messages
|
|
42
|
+
- Cross-platform terminal support for Windows, Linux, and macOS
|
|
43
|
+
- Terminal cursor, echo, and keyboard input control
|
|
44
|
+
- Customizable themes and symbols
|
|
45
|
+
- Reusable terminal rendering system
|
|
46
|
+
- Type-safe Python API with bundled type information
|
|
47
|
+
|
|
48
|
+
# Table of Contents
|
|
49
|
+
|
|
50
|
+
- [Installation & Setup](#installation--setup)
|
|
51
|
+
- [Quick Start (Recommended)](#quick-start-recommended)
|
|
52
|
+
- [Developer Installation & Local Development Build](#developer-installation--local-development-build)
|
|
53
|
+
- [Documentation](#documentation)
|
|
54
|
+
- [`ClackOption`](#clackoption)
|
|
55
|
+
- [Generic typing convention](#generic-typing-convention)
|
|
56
|
+
- [Prompts](#prompts)
|
|
57
|
+
- [Prompt overview](#prompt-overview)
|
|
58
|
+
- [`ask()`](#ask)
|
|
59
|
+
- [`password()`](#password)
|
|
60
|
+
- [`confirm()`](#confirm)
|
|
61
|
+
- [`pick_date()`](#pick_date)
|
|
62
|
+
- [`multiline()`](#multiline)
|
|
63
|
+
- [`select()`](#select)
|
|
64
|
+
- [`multiselect()`](#multiselect)
|
|
65
|
+
- [`autocomplete()`](#autocomplete)
|
|
66
|
+
- [`autocomplete_multiselect()`](#autocomplete_multiselect)
|
|
67
|
+
- [`select_key()`](#select_key)
|
|
68
|
+
- [`select_path()`](#select_path)
|
|
69
|
+
- [Cancellation](#cancellation)
|
|
70
|
+
- [Cancellation values](#cancellation-values)
|
|
71
|
+
- [Widgets](#widgets)
|
|
72
|
+
- [`intro()`](#intro)
|
|
73
|
+
- [`outro()`](#outro)
|
|
74
|
+
- [`cancel()`](#cancel)
|
|
75
|
+
- [`note()`](#note)
|
|
76
|
+
- [`box()`](#box)
|
|
77
|
+
- [`log`](#log)
|
|
78
|
+
- [`TaskLog`](#tasklog)
|
|
79
|
+
- [Adding messages](#adding-messages)
|
|
80
|
+
- [Reading the log](#reading-the-log)
|
|
81
|
+
- [Cancellation](#cancellation-1)
|
|
82
|
+
- [`Spinner`](#spinner)
|
|
83
|
+
- [Lifecycle](#lifecycle)
|
|
84
|
+
- [Updating the message](#updating-the-message)
|
|
85
|
+
- [Cancellation and errors](#cancellation-and-errors)
|
|
86
|
+
- [Clearing](#clearing)
|
|
87
|
+
- [Checking cancellation](#checking-cancellation)
|
|
88
|
+
- [`Progress`](#progress)
|
|
89
|
+
- [Basic lifecycle](#basic-lifecycle)
|
|
90
|
+
- [`Activity`](#activity)
|
|
91
|
+
- [Lifecycle](#lifecycle-1)
|
|
92
|
+
- [`stream`](#stream)
|
|
93
|
+
- [A normal iterable](#a-normal-iterable)
|
|
94
|
+
- [An info stream](#an-info-stream)
|
|
95
|
+
- [A step stream](#a-step-stream)
|
|
96
|
+
- [An async iterable](#an-async-iterable)
|
|
97
|
+
- [Asynchronous APIs](#asynchronous-apis)
|
|
98
|
+
- [Themes](#themes)
|
|
99
|
+
- [Custom themes](#custom-themes)
|
|
100
|
+
- [Unicode and ASCII symbols](#unicode-and-ascii-symbols)
|
|
101
|
+
- [Rendering](#rendering)
|
|
102
|
+
- [`Text`](#text)
|
|
103
|
+
- [`FrameBuilder`](#framebuilder)
|
|
104
|
+
- [`RenderFrame`](#renderframe)
|
|
105
|
+
- [Terminal](#terminal)
|
|
106
|
+
- [`KeyReader`](#keyreader)
|
|
107
|
+
- [`CursorController`](#cursorcontroller)
|
|
108
|
+
- [`Stdout`](#stdout)
|
|
109
|
+
- [`EchoController`](#echocontroller)
|
|
110
|
+
- [Building a custom prompt](#building-a-custom-prompt)
|
|
111
|
+
- [Custom prompt structure](#custom-prompt-structure)
|
|
112
|
+
- [Validation](#validation)
|
|
113
|
+
- [Propagating the key after an error](#propagating-the-key-after-an-error)
|
|
114
|
+
- [Building a custom widget](#building-a-custom-widget)
|
|
115
|
+
- [Custom component conventions](#custom-component-conventions)
|
|
116
|
+
- [Prompts](#prompts-1)
|
|
117
|
+
- [Widgets](#widgets-1)
|
|
118
|
+
- [Themes](#themes-1)
|
|
119
|
+
- [Example](#example)
|
|
120
|
+
|
|
121
|
+
# Installation & Setup
|
|
122
|
+
|
|
123
|
+
## Quick Start (Recommended)
|
|
124
|
+
|
|
125
|
+
The easiest way to get started with pyclack is via pip or uv. These methods will install the latest stable version directly into your active environment.
|
|
126
|
+
|
|
127
|
+
**Using pip:**
|
|
128
|
+
```bash
|
|
129
|
+
pip install pyclack-lib
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
**Using uv** (recommended for fast virtual environment management):
|
|
133
|
+
```bash
|
|
134
|
+
uv add pyclack-lib
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
### Developer Installation & Local Development Build
|
|
138
|
+
|
|
139
|
+
> **Prerequisites:**
|
|
140
|
+
> - uv (needed to manage project enviornment and to run the project)
|
|
141
|
+
|
|
142
|
+
If you are developing with the repository and need to test changes locally, follow these steps. This process uses uv sync to ensure your virtual environment is perfectly synchronized with the project's dependencies.
|
|
143
|
+
|
|
144
|
+
1). Clone the Repository:
|
|
145
|
+
```bash
|
|
146
|
+
git clone https://github.com/Maddox-RVS/pyclack.git
|
|
147
|
+
cd pyclack
|
|
148
|
+
```
|
|
149
|
+
2). Create and Sync Virtual Environment:
|
|
150
|
+
This command reads the project's dependency definitions in `pyproject.toml` and sets up a
|
|
151
|
+
pristine, isolated virtual environment based on those requirements.
|
|
152
|
+
```bash
|
|
153
|
+
uv sync --frozen
|
|
154
|
+
```
|
|
155
|
+
3). Run the Project:
|
|
156
|
+
You can now run the application using the local package installation:
|
|
157
|
+
```bash
|
|
158
|
+
uv run examples/my_pyclack_text_script.py
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
---
|
|
162
|
+
|
|
163
|
+
# Documentation
|
|
164
|
+
|
|
165
|
+
pyclack has two kinds of building blocks:
|
|
166
|
+
|
|
167
|
+
- **Prompts** collect input from the user and return a value.
|
|
168
|
+
- **Widgets** show output in the terminal and usually return `None`.
|
|
169
|
+
The synchronous API lives under `pyclack.prompts` and `pyclack.widgets`.
|
|
170
|
+
|
|
171
|
+
The asynchronous API lives under `pyclack.prompts_async` and `pyclack.widgets_async`.
|
|
172
|
+
|
|
173
|
+
---
|
|
174
|
+
|
|
175
|
+
# `ClackOption`
|
|
176
|
+
|
|
177
|
+
Every selection prompt uses `ClackOption`, a generic dataclass.
|
|
178
|
+
|
|
179
|
+
```python
|
|
180
|
+
from pyclack import ClackOption
|
|
181
|
+
|
|
182
|
+
option: ClackOption[str] = ClackOption[str](
|
|
183
|
+
value='python',
|
|
184
|
+
label='Python')
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
Its fields are:
|
|
188
|
+
|
|
189
|
+
```python
|
|
190
|
+
from pyclack import ClackOption
|
|
191
|
+
|
|
192
|
+
option: ClackOption[str] = ClackOption[str](
|
|
193
|
+
value='python',
|
|
194
|
+
label='Python',
|
|
195
|
+
hint='Recommended',
|
|
196
|
+
disabled=False)
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
- `value: V` - the value the option carries
|
|
200
|
+
- `label: str` - the text shown to the user
|
|
201
|
+
- `hint: str | None` - optional extra text next to the label
|
|
202
|
+
- `disabled: bool` - whether the user can select this option
|
|
203
|
+
## Generic typing convention
|
|
204
|
+
|
|
205
|
+
When you know the type of `value`, parameterize both the annotation and the constructor.
|
|
206
|
+
|
|
207
|
+
```python
|
|
208
|
+
from pyclack import ClackOption
|
|
209
|
+
|
|
210
|
+
integer_option: ClackOption[int] = ClackOption[int](
|
|
211
|
+
value=3,
|
|
212
|
+
label='Three')
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
For a `str` value:
|
|
216
|
+
|
|
217
|
+
```python
|
|
218
|
+
from pyclack import ClackOption
|
|
219
|
+
|
|
220
|
+
string_option: ClackOption[str] = ClackOption[str](
|
|
221
|
+
value='python',
|
|
222
|
+
label='Python')
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
For a custom type:
|
|
226
|
+
|
|
227
|
+
```python
|
|
228
|
+
from dataclasses import dataclass
|
|
229
|
+
|
|
230
|
+
from pyclack import ClackOption
|
|
231
|
+
|
|
232
|
+
@dataclass
|
|
233
|
+
class Language:
|
|
234
|
+
name: str
|
|
235
|
+
version: str
|
|
236
|
+
|
|
237
|
+
language: Language = Language(
|
|
238
|
+
name='Python',
|
|
239
|
+
version='3.13')
|
|
240
|
+
|
|
241
|
+
option: ClackOption[Language] = ClackOption[Language](
|
|
242
|
+
value=language,
|
|
243
|
+
label='Python 3.13')
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
---
|
|
247
|
+
|
|
248
|
+
# Prompts
|
|
249
|
+
|
|
250
|
+
A prompt blocks until the user submits, cancels, or its `abort_time` runs out.
|
|
251
|
+
|
|
252
|
+
A successful prompt returns the value described in its own section below.
|
|
253
|
+
|
|
254
|
+
A cancelled prompt raises `CancelException`. It does not return a special value for cancellation.
|
|
255
|
+
|
|
256
|
+
## Prompt overview
|
|
257
|
+
|
|
258
|
+
| Prompt | Purpose | Successful return |
|
|
259
|
+
| --- | --- | --- |
|
|
260
|
+
| `ask()` | Single-line text input | `str` |
|
|
261
|
+
| `password()` | Hidden text input | `str` |
|
|
262
|
+
| `confirm()` | Yes/no confirmation | `bool` |
|
|
263
|
+
| `pick_date()` | Date selection within a range | `date` |
|
|
264
|
+
| `multiline()` | Multi-line text input | `str` |
|
|
265
|
+
| `select()` | Single selection from options | `ClackOption[T]` |
|
|
266
|
+
| `multiselect()` | Multiple selection from options | `list[ClackOption[T]]` |
|
|
267
|
+
| `autocomplete()` | Filtered single selection | `ClackOption[T]` |
|
|
268
|
+
| `autocomplete_multiselect()` | Filtered multiple selection | `list[ClackOption[T]]` |
|
|
269
|
+
| `select_key()` | Selection by pressing a key | `ClackOption[str]` |
|
|
270
|
+
| `select_path()` | Filesystem path selection | `Path` |
|
|
271
|
+
|
|
272
|
+
Import any prompt from `pyclack.prompts`:
|
|
273
|
+
|
|
274
|
+
```python
|
|
275
|
+
from pyclack.prompts import ask, autocomplete, autocomplete_multiselect, confirm, multiline, multiselect, password, pick_date, select, select_key, select_path
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
## Common prompt options
|
|
279
|
+
|
|
280
|
+
Most prompts accept an `abort_time`, in seconds. When it runs out, the prompt cancels itself.
|
|
281
|
+
|
|
282
|
+
```python
|
|
283
|
+
from pyclack.prompts import ask
|
|
284
|
+
|
|
285
|
+
name: str = ask(
|
|
286
|
+
message='What is your name?',
|
|
287
|
+
abort_time=10.0)
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
Most prompts also accept a `validate` function.
|
|
291
|
+
|
|
292
|
+
```python
|
|
293
|
+
from pyclack.prompts import ask
|
|
294
|
+
|
|
295
|
+
def validate_name(value: str) -> str | None:
|
|
296
|
+
if not value: return 'Name cannot be empty'
|
|
297
|
+
|
|
298
|
+
name: str = ask(
|
|
299
|
+
message='What is your name?',
|
|
300
|
+
validate=validate_name)
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
`validate` returns `None` when the value passes. It returns a `str` error message when the value fails.
|
|
304
|
+
|
|
305
|
+
---
|
|
306
|
+
|
|
307
|
+
# `ask()`
|
|
308
|
+
|
|
309
|
+
`ask()` collects one line of text.
|
|
310
|
+
|
|
311
|
+
### Input
|
|
312
|
+
|
|
313
|
+
```python
|
|
314
|
+
from pyclack.prompts import ask
|
|
315
|
+
|
|
316
|
+
name: str = ask(
|
|
317
|
+
message='What is your name?',
|
|
318
|
+
placeholder='(e.g. Bobby)')
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
Parameters:
|
|
322
|
+
|
|
323
|
+
- `message: str` - the prompt message
|
|
324
|
+
- `placeholder: str | None` - text shown when the input is empty
|
|
325
|
+
- `initial_value: str | None` - the starting text in the input
|
|
326
|
+
- `validate: Callable[[str], str | None] | None` - an optional validator
|
|
327
|
+
- `abort_time: float | None` - an optional timeout, in seconds
|
|
328
|
+
### Output
|
|
329
|
+
|
|
330
|
+
```python
|
|
331
|
+
from pyclack.prompts import ask
|
|
332
|
+
|
|
333
|
+
name: str = ask('What is your name?')
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
The return value is the entered `str`.
|
|
337
|
+
|
|
338
|
+
### Cancellation
|
|
339
|
+
|
|
340
|
+
`e.value` holds the text the user had typed at the time of cancellation.
|
|
341
|
+
|
|
342
|
+
```python
|
|
343
|
+
from pyclack.prompts import CancelException, ask
|
|
344
|
+
|
|
345
|
+
try:
|
|
346
|
+
name: str = ask('What is your name?')
|
|
347
|
+
except CancelException as e:
|
|
348
|
+
current_name: str | None = e.value
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
---
|
|
352
|
+
|
|
353
|
+
# `password()`
|
|
354
|
+
|
|
355
|
+
`password()` collects text and masks each character as the user types it.
|
|
356
|
+
|
|
357
|
+
### Input
|
|
358
|
+
|
|
359
|
+
```python
|
|
360
|
+
from pyclack.prompts import password
|
|
361
|
+
|
|
362
|
+
secret: str = password(
|
|
363
|
+
message='Create a password',
|
|
364
|
+
show_nothing=False)
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
Parameters:
|
|
368
|
+
|
|
369
|
+
- `message: str` - the prompt message
|
|
370
|
+
- `mask: Symbol | None` - a custom mask symbol
|
|
371
|
+
- `show_nothing: bool` - hide the entered characters completely, with no mask at all
|
|
372
|
+
- `clear_on_error: bool` - clear the input after a failed validation
|
|
373
|
+
- `validate: Callable[[str], str | None] | None` - an optional validator
|
|
374
|
+
- `abort_time: float | None` - an optional timeout, in seconds
|
|
375
|
+
The active theme sets the default mask symbol.
|
|
376
|
+
|
|
377
|
+
### Output
|
|
378
|
+
|
|
379
|
+
```python
|
|
380
|
+
from pyclack.prompts import password
|
|
381
|
+
|
|
382
|
+
secret: str = password('Create a password')
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
The return value is the entered `str`, in plain text. It is never the masked text.
|
|
386
|
+
|
|
387
|
+
### Cancellation
|
|
388
|
+
|
|
389
|
+
`e.value` holds the password text entered so far.
|
|
390
|
+
|
|
391
|
+
```python
|
|
392
|
+
from pyclack.prompts import CancelException, password
|
|
393
|
+
|
|
394
|
+
try:
|
|
395
|
+
secret: str = password('Create a password')
|
|
396
|
+
except CancelException as e:
|
|
397
|
+
current_secret: str | None = e.value
|
|
398
|
+
```
|
|
399
|
+
|
|
400
|
+
---
|
|
401
|
+
|
|
402
|
+
# `confirm()`
|
|
403
|
+
|
|
404
|
+
`confirm()` asks a yes/no question and returns a `bool`.
|
|
405
|
+
|
|
406
|
+
### Input
|
|
407
|
+
|
|
408
|
+
```python
|
|
409
|
+
from pyclack.prompts import confirm
|
|
410
|
+
|
|
411
|
+
confirmed: bool = confirm('Continue?')
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
Parameters:
|
|
415
|
+
|
|
416
|
+
- `message: str` - the prompt message
|
|
417
|
+
- `active: str` - the label for `True`
|
|
418
|
+
- `inactive: str` - the label for `False`
|
|
419
|
+
- `vertical: bool` - stack the two choices vertically instead of side by side
|
|
420
|
+
- `default_option: bool` - the option selected when the prompt opens
|
|
421
|
+
- `abort_time: float | None` - an optional timeout, in seconds
|
|
422
|
+
### Output
|
|
423
|
+
|
|
424
|
+
```python
|
|
425
|
+
from pyclack.prompts import confirm
|
|
426
|
+
|
|
427
|
+
confirmed: bool = confirm('Continue?')
|
|
428
|
+
```
|
|
429
|
+
|
|
430
|
+
The return value is `True` or `False`.
|
|
431
|
+
|
|
432
|
+
### Cancellation
|
|
433
|
+
|
|
434
|
+
`e.value` holds the option selected at the time of cancellation.
|
|
435
|
+
|
|
436
|
+
```python
|
|
437
|
+
from pyclack.prompts import CancelException, confirm
|
|
438
|
+
|
|
439
|
+
try:
|
|
440
|
+
confirmed: bool = confirm('Continue?')
|
|
441
|
+
except CancelException as e:
|
|
442
|
+
current_choice: bool | None = e.value
|
|
443
|
+
```
|
|
444
|
+
|
|
445
|
+
---
|
|
446
|
+
|
|
447
|
+
# `pick_date()`
|
|
448
|
+
|
|
449
|
+
`pick_date()` collects a date within a minimum and maximum bound.
|
|
450
|
+
|
|
451
|
+
### Input
|
|
452
|
+
|
|
453
|
+
```python
|
|
454
|
+
from datetime import date
|
|
455
|
+
|
|
456
|
+
from pyclack.prompts import pick_date
|
|
457
|
+
|
|
458
|
+
release_date: date = pick_date(
|
|
459
|
+
message='Release date',
|
|
460
|
+
initial_date=date.today(),
|
|
461
|
+
min_date=date(2026, 1, 1),
|
|
462
|
+
max_date=date(2030, 12, 31))
|
|
463
|
+
```
|
|
464
|
+
|
|
465
|
+
Parameters:
|
|
466
|
+
|
|
467
|
+
- `message: str` - the prompt message
|
|
468
|
+
- `initial_date: date` - the date the prompt starts on
|
|
469
|
+
- `min_date: date` - the earliest date the prompt accepts
|
|
470
|
+
- `max_date: date` - the latest date the prompt accepts
|
|
471
|
+
- `validate: Callable[[date], str | None] | None` - an optional validator
|
|
472
|
+
- `abort_time: float | None` - an optional timeout, in seconds
|
|
473
|
+
The user enters the date as `mm/dd/yyyy`.
|
|
474
|
+
|
|
475
|
+
### Output
|
|
476
|
+
|
|
477
|
+
```python
|
|
478
|
+
from datetime import date
|
|
479
|
+
|
|
480
|
+
from pyclack.prompts import pick_date
|
|
481
|
+
|
|
482
|
+
release_date: date = pick_date(
|
|
483
|
+
'Release date',
|
|
484
|
+
initial_date=date.today(),
|
|
485
|
+
min_date=date(2026, 1, 1),
|
|
486
|
+
max_date=date(2030, 12, 31))
|
|
487
|
+
```
|
|
488
|
+
|
|
489
|
+
The return value is a `datetime.date`.
|
|
490
|
+
|
|
491
|
+
### Cancellation
|
|
492
|
+
|
|
493
|
+
At cancellation, pyclack converts the current date fields to a `YYYY-MM-DD` string and stores that string in `e.value`.
|
|
494
|
+
|
|
495
|
+
```python
|
|
496
|
+
from datetime import date
|
|
497
|
+
|
|
498
|
+
from pyclack.prompts import CancelException, pick_date
|
|
499
|
+
|
|
500
|
+
try:
|
|
501
|
+
release_date: date = pick_date(
|
|
502
|
+
'Release date',
|
|
503
|
+
initial_date=date.today(),
|
|
504
|
+
min_date=date(2026, 1, 1),
|
|
505
|
+
max_date=date(2030, 12, 31))
|
|
506
|
+
except CancelException as e:
|
|
507
|
+
current_date: str | None = e.value
|
|
508
|
+
```
|
|
509
|
+
|
|
510
|
+
---
|
|
511
|
+
|
|
512
|
+
# `multiline()`
|
|
513
|
+
|
|
514
|
+
`multiline()` collects text that can span several lines.
|
|
515
|
+
|
|
516
|
+
### Input
|
|
517
|
+
|
|
518
|
+
```python
|
|
519
|
+
from pyclack.prompts import multiline
|
|
520
|
+
|
|
521
|
+
description: str = multiline(
|
|
522
|
+
message='Description',
|
|
523
|
+
placeholder='Enter a description...',
|
|
524
|
+
show_submit=True)
|
|
525
|
+
```
|
|
526
|
+
|
|
527
|
+
Parameters:
|
|
528
|
+
|
|
529
|
+
- `message: str` - the prompt message
|
|
530
|
+
- `placeholder: str | None` - text shown when the input is empty
|
|
531
|
+
- `initial_value: str | None` - the starting text in the input
|
|
532
|
+
- `validate: Callable[[str], str | None] | None` - an optional validator
|
|
533
|
+
- `show_submit: bool` - show a submit button the user can move focus to
|
|
534
|
+
- `abort_time: float | None` - an optional timeout, in seconds
|
|
535
|
+
When `show_submit=False`, pressing Enter twice in a row submits the input.
|
|
536
|
+
|
|
537
|
+
When `show_submit=True`, Tab moves focus to the submit button, then Enter submits.
|
|
538
|
+
|
|
539
|
+
### Output
|
|
540
|
+
|
|
541
|
+
```python
|
|
542
|
+
from pyclack.prompts import multiline
|
|
543
|
+
|
|
544
|
+
description: str = multiline('Description')
|
|
545
|
+
```
|
|
546
|
+
|
|
547
|
+
The return value is a `str`. It keeps the newline characters the user typed.
|
|
548
|
+
|
|
549
|
+
### Cancellation
|
|
550
|
+
|
|
551
|
+
`e.value` holds the text entered so far.
|
|
552
|
+
|
|
553
|
+
```python
|
|
554
|
+
from pyclack.prompts import CancelException, multiline
|
|
555
|
+
|
|
556
|
+
try:
|
|
557
|
+
description: str = multiline('Description')
|
|
558
|
+
except CancelException as e:
|
|
559
|
+
current_description: str | None = e.value
|
|
560
|
+
```
|
|
561
|
+
|
|
562
|
+
---
|
|
563
|
+
|
|
564
|
+
# `select()`
|
|
565
|
+
|
|
566
|
+
`select()` lets the user choose exactly one option.
|
|
567
|
+
|
|
568
|
+
### Input
|
|
569
|
+
|
|
570
|
+
```python
|
|
571
|
+
from pyclack import ClackOption
|
|
572
|
+
from pyclack.prompts import select
|
|
573
|
+
|
|
574
|
+
options: list[ClackOption[str]] = [
|
|
575
|
+
ClackOption[str](value='python', label='Python'),
|
|
576
|
+
ClackOption[str](value='rust', label='Rust'),
|
|
577
|
+
ClackOption[str](value='go', label='Go')]
|
|
578
|
+
|
|
579
|
+
selected: ClackOption[str] = select(
|
|
580
|
+
message='Choose a language',
|
|
581
|
+
options=options)
|
|
582
|
+
```
|
|
583
|
+
|
|
584
|
+
Parameters:
|
|
585
|
+
|
|
586
|
+
- `message: str` - the prompt message
|
|
587
|
+
- `options: list[ClackOption[T]]` - the list of options to choose from
|
|
588
|
+
- `show_instructions: bool` - show navigation instructions above the list
|
|
589
|
+
- `max_items: int` - the maximum number of option lines shown at once (the list scrolls past this)
|
|
590
|
+
- `abort_time: float | None` - an optional timeout, in seconds
|
|
591
|
+
`max_items` sets the size of the visible window into the option list. pyclack keeps this window at least 5 lines tall, even if you pass a smaller value.
|
|
592
|
+
|
|
593
|
+
### Output
|
|
594
|
+
|
|
595
|
+
The return value is the selected `ClackOption`, not its `.value`.
|
|
596
|
+
|
|
597
|
+
```python
|
|
598
|
+
from pyclack import ClackOption
|
|
599
|
+
from pyclack.prompts import select
|
|
600
|
+
|
|
601
|
+
options: list[ClackOption[int]] = [
|
|
602
|
+
ClackOption[int](value=1, label='One'),
|
|
603
|
+
ClackOption[int](value=2, label='Two'),
|
|
604
|
+
ClackOption[int](value=3, label='Three')]
|
|
605
|
+
|
|
606
|
+
selected: ClackOption[int] = select(
|
|
607
|
+
message='Choose a number',
|
|
608
|
+
options=options)
|
|
609
|
+
|
|
610
|
+
number: int = selected.value
|
|
611
|
+
```
|
|
612
|
+
|
|
613
|
+
### Cancellation
|
|
614
|
+
|
|
615
|
+
`e.value` holds the option that was highlighted at the time of cancellation.
|
|
616
|
+
|
|
617
|
+
```python
|
|
618
|
+
from pyclack import CancelException, ClackOption
|
|
619
|
+
from pyclack.prompts import select
|
|
620
|
+
|
|
621
|
+
options: list[ClackOption[str]] = [
|
|
622
|
+
ClackOption[str](value='python', label='Python'),
|
|
623
|
+
ClackOption[str](value='rust', label='Rust')]
|
|
624
|
+
|
|
625
|
+
try:
|
|
626
|
+
selected: ClackOption[str] = select(
|
|
627
|
+
message='Language',
|
|
628
|
+
options=options)
|
|
629
|
+
except CancelException as e:
|
|
630
|
+
selected_before_cancel: ClackOption[str] | None = e.value
|
|
631
|
+
```
|
|
632
|
+
|
|
633
|
+
An empty option list, or an option list where every option is disabled, raises `RuntimeError`.
|
|
634
|
+
|
|
635
|
+
---
|
|
636
|
+
|
|
637
|
+
# `multiselect()`
|
|
638
|
+
|
|
639
|
+
`multiselect()` lets the user select more than one option.
|
|
640
|
+
|
|
641
|
+
### Input
|
|
642
|
+
|
|
643
|
+
```python
|
|
644
|
+
from pyclack import ClackOption
|
|
645
|
+
from pyclack.prompts import multiselect
|
|
646
|
+
|
|
647
|
+
options: list[ClackOption[str]] = [
|
|
648
|
+
ClackOption[str](value='git', label='Git'),
|
|
649
|
+
ClackOption[str](value='docker', label='Docker'),
|
|
650
|
+
ClackOption[str](value='pytest', label='Pytest')]
|
|
651
|
+
|
|
652
|
+
selected: list[ClackOption[str]] = multiselect(
|
|
653
|
+
message='Select tools',
|
|
654
|
+
options=options)
|
|
655
|
+
```
|
|
656
|
+
|
|
657
|
+
Parameters:
|
|
658
|
+
|
|
659
|
+
- `message: str` - the prompt message
|
|
660
|
+
- `options: list[ClackOption[T]]` - the list of options to choose from
|
|
661
|
+
- `show_instructions: bool` - show navigation instructions above the list
|
|
662
|
+
- `max_items: int` - the maximum number of option lines shown at once (the list scrolls past this)
|
|
663
|
+
- `abort_time: float | None` - an optional timeout, in seconds
|
|
664
|
+
Space selects or deselects the focused option.
|
|
665
|
+
|
|
666
|
+
### Output
|
|
667
|
+
|
|
668
|
+
```python
|
|
669
|
+
from pyclack import ClackOption
|
|
670
|
+
from pyclack.prompts import multiselect
|
|
671
|
+
|
|
672
|
+
options: list[ClackOption[str]] = [
|
|
673
|
+
ClackOption[str](value='git', label='Git'),
|
|
674
|
+
ClackOption[str](value='docker', label='Docker')]
|
|
675
|
+
|
|
676
|
+
selected: list[ClackOption[str]] = multiselect(
|
|
677
|
+
message='Select tools',
|
|
678
|
+
options=options)
|
|
679
|
+
|
|
680
|
+
tools: list[str] = [option.value for option in selected]
|
|
681
|
+
```
|
|
682
|
+
|
|
683
|
+
### Cancellation
|
|
684
|
+
|
|
685
|
+
`e.value` holds the list of options selected so far. It is an empty list if nothing was selected yet.
|
|
686
|
+
|
|
687
|
+
```python
|
|
688
|
+
from pyclack import CancelException, ClackOption
|
|
689
|
+
from pyclack.prompts import multiselect
|
|
690
|
+
|
|
691
|
+
options: list[ClackOption[str]] = [
|
|
692
|
+
ClackOption[str](value='git', label='Git'),
|
|
693
|
+
ClackOption[str](value='docker', label='Docker')]
|
|
694
|
+
|
|
695
|
+
try:
|
|
696
|
+
selected: list[ClackOption[str]] = multiselect(
|
|
697
|
+
message='Select tools',
|
|
698
|
+
options=options)
|
|
699
|
+
except CancelException as e:
|
|
700
|
+
selected_before_cancel: list[ClackOption[str]] | None = e.value
|
|
701
|
+
```
|
|
702
|
+
|
|
703
|
+
An empty option list, or an option list where every option is disabled, raises `RuntimeError`.
|
|
704
|
+
|
|
705
|
+
---
|
|
706
|
+
|
|
707
|
+
# `autocomplete()`
|
|
708
|
+
|
|
709
|
+
`autocomplete()` adds text search on top of single-option selection.
|
|
710
|
+
|
|
711
|
+
### Input
|
|
712
|
+
|
|
713
|
+
```python
|
|
714
|
+
from pyclack import ClackOption
|
|
715
|
+
from pyclack.prompts import autocomplete
|
|
716
|
+
|
|
717
|
+
options: list[ClackOption[str]] = [
|
|
718
|
+
ClackOption[str](value='python', label='Python'),
|
|
719
|
+
ClackOption[str](value='rust', label='Rust'),
|
|
720
|
+
ClackOption[str](value='javascript', label='JavaScript')]
|
|
721
|
+
|
|
722
|
+
selected: ClackOption[str] = autocomplete(
|
|
723
|
+
message='Language',
|
|
724
|
+
options=options,
|
|
725
|
+
placeholder='Type to search...')
|
|
726
|
+
```
|
|
727
|
+
|
|
728
|
+
Parameters:
|
|
729
|
+
|
|
730
|
+
- `message: str` - the prompt message
|
|
731
|
+
- `options: list[ClackOption[T]]` - the list of options to choose from
|
|
732
|
+
- `placeholder: str` - text shown when the search input is empty
|
|
733
|
+
- `show_instructions: bool` - show navigation instructions above the list
|
|
734
|
+
- `max_items: int` - the maximum number of option lines shown at once (the list scrolls past this)
|
|
735
|
+
- `filter: Callable[[str, list[ClackOption[T]]], list[ClackOption[T]]] | None` - an optional custom search filter
|
|
736
|
+
- `abort_time: float | None` - an optional timeout, in seconds
|
|
737
|
+
When `filter=None`, pyclack uses its default filter.
|
|
738
|
+
|
|
739
|
+
A custom filter receives the current search text and the full option list, and returns the options to show.
|
|
740
|
+
|
|
741
|
+
```python
|
|
742
|
+
from pyclack import ClackOption
|
|
743
|
+
from pyclack.prompts import autocomplete
|
|
744
|
+
|
|
745
|
+
def filter_options(search: str, options: list[ClackOption[str]]) -> list[ClackOption[str]]:
|
|
746
|
+
search_lower: str = search.lower()
|
|
747
|
+
return [option for option in options if search_lower in option.label.lower()]
|
|
748
|
+
|
|
749
|
+
options: list[ClackOption[str]] = [
|
|
750
|
+
ClackOption[str](value='python', label='Python'),
|
|
751
|
+
ClackOption[str](value='rust', label='Rust')]
|
|
752
|
+
|
|
753
|
+
selected: ClackOption[str] = autocomplete(
|
|
754
|
+
message='Language',
|
|
755
|
+
options=options,
|
|
756
|
+
filter=filter_options)
|
|
757
|
+
```
|
|
758
|
+
|
|
759
|
+
### Output
|
|
760
|
+
|
|
761
|
+
The return value is the selected `ClackOption`.
|
|
762
|
+
|
|
763
|
+
```python
|
|
764
|
+
from pyclack import ClackOption
|
|
765
|
+
from pyclack.prompts import autocomplete
|
|
766
|
+
|
|
767
|
+
options: list[ClackOption[int]] = [
|
|
768
|
+
ClackOption[int](value=1, label='One'),
|
|
769
|
+
ClackOption[int](value=2, label='Two')]
|
|
770
|
+
|
|
771
|
+
selected: ClackOption[int] = autocomplete(
|
|
772
|
+
message='Number',
|
|
773
|
+
options=options)
|
|
774
|
+
|
|
775
|
+
number: int = selected.value
|
|
776
|
+
```
|
|
777
|
+
|
|
778
|
+
### Cancellation
|
|
779
|
+
|
|
780
|
+
`e.value` holds the currently highlighted, enabled option. It is `None` when no option is usable.
|
|
781
|
+
|
|
782
|
+
```python
|
|
783
|
+
from pyclack import CancelException, ClackOption
|
|
784
|
+
from pyclack.prompts import autocomplete
|
|
785
|
+
|
|
786
|
+
options: list[ClackOption[str]] = [
|
|
787
|
+
ClackOption[str](value='python', label='Python'),
|
|
788
|
+
ClackOption[str](value='rust', label='Rust')]
|
|
789
|
+
|
|
790
|
+
try:
|
|
791
|
+
selected: ClackOption[str] = autocomplete(
|
|
792
|
+
message='Language',
|
|
793
|
+
options=options)
|
|
794
|
+
except CancelException as e:
|
|
795
|
+
selected_before_cancel: ClackOption[str] | None = e.value
|
|
796
|
+
```
|
|
797
|
+
|
|
798
|
+
An empty option list, or an option list where every option is disabled, raises `RuntimeError`.
|
|
799
|
+
|
|
800
|
+
---
|
|
801
|
+
|
|
802
|
+
# `autocomplete_multiselect()`
|
|
803
|
+
|
|
804
|
+
`autocomplete_multiselect()` adds text search on top of multiple selection.
|
|
805
|
+
|
|
806
|
+
### Input
|
|
807
|
+
|
|
808
|
+
```python
|
|
809
|
+
from pyclack import ClackOption
|
|
810
|
+
from pyclack.prompts import autocomplete_multiselect
|
|
811
|
+
|
|
812
|
+
options: list[ClackOption[str]] = [
|
|
813
|
+
ClackOption[str](value='git', label='Git'),
|
|
814
|
+
ClackOption[str](value='docker', label='Docker'),
|
|
815
|
+
ClackOption[str](value='pytest', label='Pytest')]
|
|
816
|
+
|
|
817
|
+
selected: list[ClackOption[str]] = autocomplete_multiselect(
|
|
818
|
+
message='Select tools',
|
|
819
|
+
options=options)
|
|
820
|
+
```
|
|
821
|
+
|
|
822
|
+
Parameters:
|
|
823
|
+
|
|
824
|
+
- `message: str` - the prompt message
|
|
825
|
+
- `options: list[ClackOption[T]]` - the list of options to choose from
|
|
826
|
+
- `placeholder: str` - text shown when the search input is empty
|
|
827
|
+
- `show_instructions: bool` - show navigation instructions above the list
|
|
828
|
+
- `max_items: int` - the maximum number of option lines shown at once (the list scrolls past this)
|
|
829
|
+
- `filter: Callable[[str, list[ClackOption[T]]], list[ClackOption[T]]] | None` - an optional custom search filter
|
|
830
|
+
- `abort_time: float | None` - an optional timeout, in seconds
|
|
831
|
+
Space selects or deselects the highlighted option.
|
|
832
|
+
|
|
833
|
+
### Output
|
|
834
|
+
|
|
835
|
+
```python
|
|
836
|
+
from pyclack import ClackOption
|
|
837
|
+
from pyclack.prompts import autocomplete_multiselect
|
|
838
|
+
|
|
839
|
+
options: list[ClackOption[int]] = [
|
|
840
|
+
ClackOption[int](value=1, label='One'),
|
|
841
|
+
ClackOption[int](value=2, label='Two')]
|
|
842
|
+
|
|
843
|
+
selected: list[ClackOption[int]] = autocomplete_multiselect(
|
|
844
|
+
message='Select numbers',
|
|
845
|
+
options=options)
|
|
846
|
+
|
|
847
|
+
numbers: list[int] = [option.value for option in selected]
|
|
848
|
+
```
|
|
849
|
+
|
|
850
|
+
### Cancellation
|
|
851
|
+
|
|
852
|
+
`e.value` holds the list of options selected so far. It is an empty list if nothing was selected yet.
|
|
853
|
+
|
|
854
|
+
```python
|
|
855
|
+
from pyclack import CancelException, ClackOption
|
|
856
|
+
from pyclack.prompts import autocomplete_multiselect
|
|
857
|
+
|
|
858
|
+
options: list[ClackOption[str]] = [
|
|
859
|
+
ClackOption[str](value='git', label='Git'),
|
|
860
|
+
ClackOption[str](value='docker', label='Docker')]
|
|
861
|
+
|
|
862
|
+
try:
|
|
863
|
+
selected: list[ClackOption[str]] = autocomplete_multiselect(
|
|
864
|
+
message='Select tools',
|
|
865
|
+
options=options)
|
|
866
|
+
except CancelException as e:
|
|
867
|
+
selected_before_cancel: list[ClackOption[str]] | None = e.value
|
|
868
|
+
```
|
|
869
|
+
|
|
870
|
+
An empty option list, or an option list where every option is disabled, raises `RuntimeError`.
|
|
871
|
+
|
|
872
|
+
---
|
|
873
|
+
|
|
874
|
+
# `select_key()`
|
|
875
|
+
|
|
876
|
+
`select_key()` selects an option when the user presses its key.
|
|
877
|
+
|
|
878
|
+
Each option's `value` must be the key string that selects it.
|
|
879
|
+
|
|
880
|
+
### Input
|
|
881
|
+
|
|
882
|
+
```python
|
|
883
|
+
from pyclack import ClackOption
|
|
884
|
+
from pyclack.prompts import select_key
|
|
885
|
+
|
|
886
|
+
options: list[ClackOption[str]] = [
|
|
887
|
+
ClackOption[str](value='y', label='Yes'),
|
|
888
|
+
ClackOption[str](value='n', label='No'),
|
|
889
|
+
ClackOption[str](value='s', label='Skip')]
|
|
890
|
+
|
|
891
|
+
selected: ClackOption[str] = select_key(
|
|
892
|
+
'Choose an action',
|
|
893
|
+
options=options,
|
|
894
|
+
case_sensitive=True)
|
|
895
|
+
```
|
|
896
|
+
|
|
897
|
+
Parameters:
|
|
898
|
+
|
|
899
|
+
- `message: str` - the prompt message
|
|
900
|
+
- `options: list[ClackOption[str]]` - the list of options to choose from
|
|
901
|
+
- `case_sensitive: bool` - treat upper and lower case key presses as different keys
|
|
902
|
+
- `abort_time: float | None` - an optional timeout, in seconds
|
|
903
|
+
Pressing Enter selects the first option in the list.
|
|
904
|
+
|
|
905
|
+
### Output
|
|
906
|
+
|
|
907
|
+
```python
|
|
908
|
+
from pyclack import ClackOption
|
|
909
|
+
from pyclack.prompts import select_key
|
|
910
|
+
|
|
911
|
+
options: list[ClackOption[str]] = [
|
|
912
|
+
ClackOption[str](value='y', label='Yes'),
|
|
913
|
+
ClackOption[str](value='n', label='No')]
|
|
914
|
+
|
|
915
|
+
selected: ClackOption[str] = select_key(
|
|
916
|
+
'Continue?',
|
|
917
|
+
options=options)
|
|
918
|
+
|
|
919
|
+
key: str = selected.value
|
|
920
|
+
```
|
|
921
|
+
|
|
922
|
+
### Cancellation
|
|
923
|
+
|
|
924
|
+
`e.value` holds the first option in the list.
|
|
925
|
+
|
|
926
|
+
```python
|
|
927
|
+
from pyclack import CancelException, ClackOption
|
|
928
|
+
from pyclack.prompts import select_key
|
|
929
|
+
|
|
930
|
+
options: list[ClackOption[str]] = [
|
|
931
|
+
ClackOption[str](value='y', label='Yes'),
|
|
932
|
+
ClackOption[str](value='n', label='No')]
|
|
933
|
+
|
|
934
|
+
try:
|
|
935
|
+
selected: ClackOption[str] = select_key(
|
|
936
|
+
'Continue?',
|
|
937
|
+
options=options)
|
|
938
|
+
except CancelException as e:
|
|
939
|
+
default_option: ClackOption[str] | None = e.value
|
|
940
|
+
```
|
|
941
|
+
|
|
942
|
+
`select_key()` raises `RuntimeError` for an invalid or duplicate key value, an empty option list, or an option list where every option is disabled.
|
|
943
|
+
|
|
944
|
+
---
|
|
945
|
+
|
|
946
|
+
# `select_path()`
|
|
947
|
+
|
|
948
|
+
`select_path()` is an autocomplete-style filesystem picker.
|
|
949
|
+
|
|
950
|
+
### Input
|
|
951
|
+
|
|
952
|
+
```python
|
|
953
|
+
from pathlib import Path
|
|
954
|
+
|
|
955
|
+
from pyclack.prompts import select_path
|
|
956
|
+
|
|
957
|
+
selected_path: Path = select_path(
|
|
958
|
+
message='Select a path',
|
|
959
|
+
root=Path.cwd(),
|
|
960
|
+
directory=False)
|
|
961
|
+
```
|
|
962
|
+
|
|
963
|
+
Parameters:
|
|
964
|
+
|
|
965
|
+
- `message: str` - the prompt message
|
|
966
|
+
- `placeholder: str` - text shown when the search input is empty
|
|
967
|
+
- `show_instructions: bool` - show navigation instructions above the list
|
|
968
|
+
- `max_items: int` - the maximum number of option lines shown at once (the list scrolls past this)
|
|
969
|
+
- `root: Path` - the directory the prompt starts in
|
|
970
|
+
- `directory: bool` - show only directories, not files
|
|
971
|
+
- `abort_time: float | None` - an optional timeout, in seconds
|
|
972
|
+
When `directory=False`, the prompt shows both files and directories.
|
|
973
|
+
|
|
974
|
+
When `directory=True`, the prompt shows only directories.
|
|
975
|
+
|
|
976
|
+
`root` defaults to the current working directory.
|
|
977
|
+
|
|
978
|
+
### Output
|
|
979
|
+
|
|
980
|
+
```python
|
|
981
|
+
from pathlib import Path
|
|
982
|
+
|
|
983
|
+
from pyclack.prompts import select_path
|
|
984
|
+
|
|
985
|
+
selected_path: Path = select_path('Select a file')
|
|
986
|
+
```
|
|
987
|
+
|
|
988
|
+
The return value is a `Path`.
|
|
989
|
+
|
|
990
|
+
If `root` does not exist, `select_path()` raises `FileNotFoundError`.
|
|
991
|
+
|
|
992
|
+
### Cancellation
|
|
993
|
+
|
|
994
|
+
`e.value` holds the currently selected `Path`. It is `None` when nothing is selected.
|
|
995
|
+
|
|
996
|
+
```python
|
|
997
|
+
from pathlib import Path
|
|
998
|
+
|
|
999
|
+
from pyclack import CancelException
|
|
1000
|
+
from pyclack.prompts import select_path
|
|
1001
|
+
|
|
1002
|
+
try:
|
|
1003
|
+
selected_path: Path = select_path('Select a file')
|
|
1004
|
+
except CancelException as e:
|
|
1005
|
+
selected_before_cancel: Path | None = e.value
|
|
1006
|
+
```
|
|
1007
|
+
|
|
1008
|
+
---
|
|
1009
|
+
|
|
1010
|
+
# Cancellation
|
|
1011
|
+
|
|
1012
|
+
Cancellation works the same way across every prompt.
|
|
1013
|
+
|
|
1014
|
+
Escape or Ctrl+C cancels the active prompt. A prompt also cancels itself when its `abort_time` runs out.
|
|
1015
|
+
|
|
1016
|
+
The prompt raises `CancelException`:
|
|
1017
|
+
|
|
1018
|
+
```python
|
|
1019
|
+
from pyclack import CancelException
|
|
1020
|
+
from pyclack.prompts import ask
|
|
1021
|
+
|
|
1022
|
+
try:
|
|
1023
|
+
value: str = ask('Value')
|
|
1024
|
+
except CancelException as e:
|
|
1025
|
+
current_value: str | None = e.value
|
|
1026
|
+
```
|
|
1027
|
+
|
|
1028
|
+
`CancelException` is generic over the type of value it carries:
|
|
1029
|
+
|
|
1030
|
+
```python
|
|
1031
|
+
from pyclack import CancelException
|
|
1032
|
+
|
|
1033
|
+
exception: CancelException[str] = CancelException('partial input')
|
|
1034
|
+
```
|
|
1035
|
+
|
|
1036
|
+
Its `value` attribute holds whatever state the prompt chose to keep at the time of cancellation:
|
|
1037
|
+
|
|
1038
|
+
```python
|
|
1039
|
+
from pyclack import CancelException
|
|
1040
|
+
|
|
1041
|
+
exception: CancelException[str] = CancelException('partial input')
|
|
1042
|
+
|
|
1043
|
+
value: str | None = exception.value
|
|
1044
|
+
```
|
|
1045
|
+
|
|
1046
|
+
## Cancellation values
|
|
1047
|
+
|
|
1048
|
+
| Prompt | Successful return | `e.value` |
|
|
1049
|
+
| --- | --- | --- |
|
|
1050
|
+
| `ask()` | `str` | current `str`, or `None` |
|
|
1051
|
+
| `password()` | `str` | current `str`, or `None` |
|
|
1052
|
+
| `confirm()` | `bool` | current `bool`, or `None` |
|
|
1053
|
+
| `pick_date()` | `date` | current date as a `YYYY-MM-DD` `str`, or `None` |
|
|
1054
|
+
| `multiline()` | `str` | current `str`, or `None` |
|
|
1055
|
+
| `select()` | `ClackOption[T]` | highlighted `ClackOption[T]`, or `None` |
|
|
1056
|
+
| `multiselect()` | `list[ClackOption[T]]` | selected `list[ClackOption[T]]`, or `None` |
|
|
1057
|
+
| `autocomplete()` | `ClackOption[T]` | highlighted `ClackOption[T]`, or `None` |
|
|
1058
|
+
| `autocomplete_multiselect()` | `list[ClackOption[T]]` | selected `list[ClackOption[T]]`, or `None` |
|
|
1059
|
+
| `select_key()` | `ClackOption[str]` | first option `ClackOption[str]`, or `None` |
|
|
1060
|
+
| `select_path()` | `Path` | highlighted `Path`, or `None` |
|
|
1061
|
+
|
|
1062
|
+
The exception always signals cancellation. `e.value` always carries the useful partial state. This split stays the same across every prompt.
|
|
1063
|
+
|
|
1064
|
+
---
|
|
1065
|
+
|
|
1066
|
+
# Widgets
|
|
1067
|
+
|
|
1068
|
+
A widget shows terminal output. It does not collect a value from the user.
|
|
1069
|
+
|
|
1070
|
+
Import the synchronous widget API from `pyclack.widgets`:
|
|
1071
|
+
|
|
1072
|
+
```python
|
|
1073
|
+
from pyclack.widgets import Activity, Progress, ProgressStyle, Spinner, TaskLog, box, cancel, intro, log, note, outro, stream
|
|
1074
|
+
```
|
|
1075
|
+
|
|
1076
|
+
A simple widget, such as `intro()` or `note()`, returns `None` and has no state.
|
|
1077
|
+
|
|
1078
|
+
A stateful widget, such as `Spinner`, `Progress`, `Activity`, and `TaskLog`, is an object. Its methods control what it shows and when.
|
|
1079
|
+
|
|
1080
|
+
---
|
|
1081
|
+
|
|
1082
|
+
# `intro()`
|
|
1083
|
+
|
|
1084
|
+
`intro()` shows an introductory message.
|
|
1085
|
+
|
|
1086
|
+
```python
|
|
1087
|
+
from pyclack.widgets import intro
|
|
1088
|
+
|
|
1089
|
+
intro('My Application')
|
|
1090
|
+
```
|
|
1091
|
+
|
|
1092
|
+
Parameters:
|
|
1093
|
+
|
|
1094
|
+
- `title: str` - the title to show
|
|
1095
|
+
- `custom_style: Style | None` - an optional style override for the title
|
|
1096
|
+
---
|
|
1097
|
+
|
|
1098
|
+
# `outro()`
|
|
1099
|
+
|
|
1100
|
+
`outro()` shows a closing message.
|
|
1101
|
+
|
|
1102
|
+
```python
|
|
1103
|
+
from pyclack.widgets import outro
|
|
1104
|
+
|
|
1105
|
+
outro('Done!')
|
|
1106
|
+
```
|
|
1107
|
+
|
|
1108
|
+
Parameters:
|
|
1109
|
+
|
|
1110
|
+
- `message: str` - the message to show
|
|
1111
|
+
- `custom_style: Style | None` - an optional style override for the message
|
|
1112
|
+
---
|
|
1113
|
+
|
|
1114
|
+
# `cancel()`
|
|
1115
|
+
|
|
1116
|
+
`cancel()` shows a cancellation message.
|
|
1117
|
+
|
|
1118
|
+
```python
|
|
1119
|
+
from pyclack.widgets import cancel
|
|
1120
|
+
|
|
1121
|
+
cancel('Operation cancelled.')
|
|
1122
|
+
```
|
|
1123
|
+
|
|
1124
|
+
Parameters:
|
|
1125
|
+
|
|
1126
|
+
- `message: str` - the cancellation message to show
|
|
1127
|
+
---
|
|
1128
|
+
|
|
1129
|
+
# `note()`
|
|
1130
|
+
|
|
1131
|
+
`note()` shows a titled note.
|
|
1132
|
+
|
|
1133
|
+
```python
|
|
1134
|
+
from pyclack.widgets import note
|
|
1135
|
+
|
|
1136
|
+
note('Configuration', 'Using configuration from pyproject.toml.')
|
|
1137
|
+
```
|
|
1138
|
+
|
|
1139
|
+
Parameters:
|
|
1140
|
+
|
|
1141
|
+
- `title: str` - the note's title
|
|
1142
|
+
- `message: str` - the text inside the note
|
|
1143
|
+
---
|
|
1144
|
+
|
|
1145
|
+
# `box()`
|
|
1146
|
+
|
|
1147
|
+
`box()` shows text inside a bordered box.
|
|
1148
|
+
|
|
1149
|
+
```python
|
|
1150
|
+
from pyclack import Alignment
|
|
1151
|
+
from pyclack.widgets import box
|
|
1152
|
+
|
|
1153
|
+
box(
|
|
1154
|
+
'Build complete.',
|
|
1155
|
+
'Status',
|
|
1156
|
+
content_align=Alignment.CENTER,
|
|
1157
|
+
title_align=Alignment.LEFT,
|
|
1158
|
+
rounded=True)
|
|
1159
|
+
```
|
|
1160
|
+
|
|
1161
|
+
Parameters:
|
|
1162
|
+
|
|
1163
|
+
- `content: str` - the text inside the box
|
|
1164
|
+
- `title: str` - the box's title
|
|
1165
|
+
- `content_align: Alignment` - the alignment of the content
|
|
1166
|
+
- `title_align: Alignment` - the alignment of the title
|
|
1167
|
+
- `width: int | None` - the box's maximum width in the terminal, or `None` for no maximum
|
|
1168
|
+
- `rounded: bool` - use rounded corners
|
|
1169
|
+
- `title_padding: int` - the spacing on each side of the title
|
|
1170
|
+
- `content_padding: int` - the spacing on each side of the content
|
|
1171
|
+
---
|
|
1172
|
+
|
|
1173
|
+
# `log`
|
|
1174
|
+
|
|
1175
|
+
`log` shows one-off messages at different severity levels.
|
|
1176
|
+
|
|
1177
|
+
```python
|
|
1178
|
+
from pyclack.widgets import log
|
|
1179
|
+
|
|
1180
|
+
log.message('Starting build')
|
|
1181
|
+
log.info('Using Python 3.13')
|
|
1182
|
+
log.warning('Configuration file not found')
|
|
1183
|
+
log.error('Compilation failed')
|
|
1184
|
+
log.success('Build complete')
|
|
1185
|
+
log.step('Installing dependencies')
|
|
1186
|
+
```
|
|
1187
|
+
|
|
1188
|
+
Each function takes one argument:
|
|
1189
|
+
|
|
1190
|
+
- `msg: str` - the message to show
|
|
1191
|
+
|
|
1192
|
+
The available levels are:
|
|
1193
|
+
|
|
1194
|
+
- `message()`
|
|
1195
|
+
- `info()`
|
|
1196
|
+
- `warning()`
|
|
1197
|
+
- `warn()`
|
|
1198
|
+
- `error()`
|
|
1199
|
+
- `success()`
|
|
1200
|
+
- `step()`
|
|
1201
|
+
|
|
1202
|
+
`warn()` is an alias for `warning()`.
|
|
1203
|
+
|
|
1204
|
+
---
|
|
1205
|
+
|
|
1206
|
+
# `TaskLog`
|
|
1207
|
+
|
|
1208
|
+
`TaskLog` shows a running task and the messages it produces while it works.
|
|
1209
|
+
|
|
1210
|
+
### Input
|
|
1211
|
+
|
|
1212
|
+
```python
|
|
1213
|
+
from pyclack.widgets import TaskLog
|
|
1214
|
+
|
|
1215
|
+
task: TaskLog = TaskLog(
|
|
1216
|
+
title='Building project',
|
|
1217
|
+
limit=5,
|
|
1218
|
+
retain_log=False)
|
|
1219
|
+
```
|
|
1220
|
+
|
|
1221
|
+
Parameters:
|
|
1222
|
+
|
|
1223
|
+
- `title: str` - the title shown above the log
|
|
1224
|
+
- `limit: int | None` - the maximum number of messages kept and shown at once
|
|
1225
|
+
- `retain_log: bool` - keep the full log instead of trimming it to `limit`
|
|
1226
|
+
### Adding messages
|
|
1227
|
+
|
|
1228
|
+
```python
|
|
1229
|
+
from pyclack.widgets import TaskLog
|
|
1230
|
+
|
|
1231
|
+
task: TaskLog = TaskLog(title='Building')
|
|
1232
|
+
|
|
1233
|
+
task.message('Compiling main.py')
|
|
1234
|
+
task.message('Compiling utils.py')
|
|
1235
|
+
task.success('Build complete')
|
|
1236
|
+
```
|
|
1237
|
+
|
|
1238
|
+
Once `success()` runs, the task is marked done. Later calls to `message()` have no effect.
|
|
1239
|
+
|
|
1240
|
+
### Reading the log
|
|
1241
|
+
|
|
1242
|
+
```python
|
|
1243
|
+
from pyclack.widgets import TaskLog
|
|
1244
|
+
|
|
1245
|
+
task: TaskLog = TaskLog(title='Building')
|
|
1246
|
+
|
|
1247
|
+
task.message('Compiling')
|
|
1248
|
+
task.message('Linking')
|
|
1249
|
+
|
|
1250
|
+
messages: list[str] = task.get_log()
|
|
1251
|
+
```
|
|
1252
|
+
|
|
1253
|
+
The stored log includes the initial title as its first entry.
|
|
1254
|
+
|
|
1255
|
+
### Cancellation
|
|
1256
|
+
|
|
1257
|
+
Ctrl+C while a `TaskLog` is active raises `CancelException`, with no value attached.
|
|
1258
|
+
|
|
1259
|
+
```python
|
|
1260
|
+
from pyclack import CancelException
|
|
1261
|
+
from pyclack.widgets import TaskLog, cancel
|
|
1262
|
+
|
|
1263
|
+
task: TaskLog = TaskLog(title='Building')
|
|
1264
|
+
|
|
1265
|
+
try:
|
|
1266
|
+
task.message('Compiling')
|
|
1267
|
+
# ... do some work ...
|
|
1268
|
+
except CancelException:
|
|
1269
|
+
cancel('Compiling cancelled!')
|
|
1270
|
+
exit(0)
|
|
1271
|
+
```
|
|
1272
|
+
|
|
1273
|
+
---
|
|
1274
|
+
|
|
1275
|
+
# `Spinner`
|
|
1276
|
+
|
|
1277
|
+
`Spinner` shows an animated spinner while work runs.
|
|
1278
|
+
|
|
1279
|
+
### Input
|
|
1280
|
+
|
|
1281
|
+
```python
|
|
1282
|
+
from pyclack.widgets import Spinner
|
|
1283
|
+
|
|
1284
|
+
spinner: Spinner = Spinner(
|
|
1285
|
+
show_timer=False,
|
|
1286
|
+
show_elipse=True,
|
|
1287
|
+
spinner_delay=80,
|
|
1288
|
+
elipse_delay=500)
|
|
1289
|
+
```
|
|
1290
|
+
|
|
1291
|
+
Parameters:
|
|
1292
|
+
|
|
1293
|
+
- `show_timer: bool` - show elapsed time since the spinner started
|
|
1294
|
+
- `show_elipse: bool` - show an animated ellipsis after the message
|
|
1295
|
+
- `spinner_delay: float` - milliseconds between spinner frames
|
|
1296
|
+
- `elipse_delay: float` - milliseconds between ellipsis frames
|
|
1297
|
+
- `spinner_frames: SpinnerSymbols | None` - a custom set of spinner frames, or `None` to use the active theme's set
|
|
1298
|
+
### Lifecycle
|
|
1299
|
+
|
|
1300
|
+
```python
|
|
1301
|
+
from pyclack.widgets import Spinner
|
|
1302
|
+
|
|
1303
|
+
spinner: Spinner = Spinner()
|
|
1304
|
+
|
|
1305
|
+
spinner.start('Installing dependencies')
|
|
1306
|
+
# ... do some work ...
|
|
1307
|
+
spinner.stop('Dependencies installed')
|
|
1308
|
+
```
|
|
1309
|
+
|
|
1310
|
+
### Updating the message
|
|
1311
|
+
|
|
1312
|
+
```python
|
|
1313
|
+
from pyclack.widgets import Spinner
|
|
1314
|
+
|
|
1315
|
+
spinner: Spinner = Spinner()
|
|
1316
|
+
|
|
1317
|
+
spinner.start('Installing')
|
|
1318
|
+
# ... do some work ...
|
|
1319
|
+
spinner.set_message('Installing package 2/5')
|
|
1320
|
+
```
|
|
1321
|
+
|
|
1322
|
+
### Cancellation and errors
|
|
1323
|
+
|
|
1324
|
+
```python
|
|
1325
|
+
from pyclack.widgets import Spinner
|
|
1326
|
+
|
|
1327
|
+
spinner: Spinner = Spinner()
|
|
1328
|
+
|
|
1329
|
+
spinner.start('Installing')
|
|
1330
|
+
# ... do some interrupted work ...
|
|
1331
|
+
spinner.cancel('Installation cancelled')
|
|
1332
|
+
```
|
|
1333
|
+
|
|
1334
|
+
```python
|
|
1335
|
+
from pyclack.widgets import Spinner
|
|
1336
|
+
|
|
1337
|
+
spinner: Spinner = Spinner()
|
|
1338
|
+
|
|
1339
|
+
spinner.start('Installing')
|
|
1340
|
+
# ... do some failed work ...
|
|
1341
|
+
spinner.error('Installation failed')
|
|
1342
|
+
```
|
|
1343
|
+
|
|
1344
|
+
### Clearing
|
|
1345
|
+
|
|
1346
|
+
```python
|
|
1347
|
+
from pyclack.widgets import Spinner
|
|
1348
|
+
|
|
1349
|
+
spinner: Spinner = Spinner()
|
|
1350
|
+
|
|
1351
|
+
spinner.start('Installing')
|
|
1352
|
+
# ... do some work ...
|
|
1353
|
+
spinner.clear()
|
|
1354
|
+
```
|
|
1355
|
+
|
|
1356
|
+
### Checking cancellation
|
|
1357
|
+
|
|
1358
|
+
```python
|
|
1359
|
+
from pyclack.widgets import Spinner
|
|
1360
|
+
|
|
1361
|
+
spinner: Spinner = Spinner()
|
|
1362
|
+
|
|
1363
|
+
cancelled: bool = spinner.is_cancelled()
|
|
1364
|
+
```
|
|
1365
|
+
|
|
1366
|
+
Ctrl+C raises `CancelException` while the spinner runs. The spinner restores the terminal state during cleanup either way.
|
|
1367
|
+
|
|
1368
|
+
```python
|
|
1369
|
+
from pyclack import CancelException
|
|
1370
|
+
from pyclack.widgets import Spinner, cancel
|
|
1371
|
+
|
|
1372
|
+
spinner: Spinner = Spinner()
|
|
1373
|
+
|
|
1374
|
+
spinner.start('Installing')
|
|
1375
|
+
try:
|
|
1376
|
+
pass # ... do some work ...
|
|
1377
|
+
except CancelException:
|
|
1378
|
+
spinner.cancel('Installation cancelled')
|
|
1379
|
+
cancel('Operation cancelled')
|
|
1380
|
+
exit(0)
|
|
1381
|
+
spinner.stop('Dependencies installed')
|
|
1382
|
+
```
|
|
1383
|
+
|
|
1384
|
+
---
|
|
1385
|
+
|
|
1386
|
+
# `Progress`
|
|
1387
|
+
|
|
1388
|
+
`Progress` shows a progress bar, with an optional spinner, ellipsis, and timer.
|
|
1389
|
+
|
|
1390
|
+
### Input
|
|
1391
|
+
|
|
1392
|
+
```python
|
|
1393
|
+
from pyclack.widgets import Progress, ProgressStyle
|
|
1394
|
+
|
|
1395
|
+
progress: Progress = Progress(
|
|
1396
|
+
max=100,
|
|
1397
|
+
size=30,
|
|
1398
|
+
style=ProgressStyle.HEAVY,
|
|
1399
|
+
show_timer=False,
|
|
1400
|
+
show_elipse=True)
|
|
1401
|
+
```
|
|
1402
|
+
|
|
1403
|
+
Parameters:
|
|
1404
|
+
|
|
1405
|
+
- `max: int` - the progress value that means "done"
|
|
1406
|
+
- `size: int` - the bar's width, in characters
|
|
1407
|
+
- `style: ProgressStyle` - `LIGHT`, `HEAVY`, or `BLOCK`
|
|
1408
|
+
- `show_timer: bool` - show elapsed time since the bar started
|
|
1409
|
+
- `show_elipse: bool` - show an animated ellipsis after the message
|
|
1410
|
+
- `spinner_delay: float` - milliseconds between spinner frames
|
|
1411
|
+
- `elipse_delay: float` - milliseconds between ellipsis frames
|
|
1412
|
+
- `spinner_frames: SpinnerSymbols | None` - a custom set of spinner frames, or `None` to use the active theme's set
|
|
1413
|
+
### Basic lifecycle
|
|
1414
|
+
|
|
1415
|
+
```python
|
|
1416
|
+
from pyclack.widgets import Progress
|
|
1417
|
+
|
|
1418
|
+
progress: Progress = Progress(max=100, size=30)
|
|
1419
|
+
|
|
1420
|
+
progress.start('Downloading')
|
|
1421
|
+
|
|
1422
|
+
progress.advance(25)
|
|
1423
|
+
progress.advance(25)
|
|
1424
|
+
|
|
1425
|
+
progress.stop('Download complete')
|
|
1426
|
+
```
|
|
1427
|
+
|
|
1428
|
+
`Progress` also supports:
|
|
1429
|
+
|
|
1430
|
+
```python
|
|
1431
|
+
progress.error('Download failed')
|
|
1432
|
+
progress.clear()
|
|
1433
|
+
cancelled: bool = progress.is_cancelled()
|
|
1434
|
+
```
|
|
1435
|
+
|
|
1436
|
+
`Progress` is stateful. It keeps its current value and redraws its own frame as that value changes.
|
|
1437
|
+
|
|
1438
|
+
Ctrl+C raises `CancelException` while the bar runs. The bar restores the terminal state during cleanup either way.
|
|
1439
|
+
|
|
1440
|
+
```python
|
|
1441
|
+
from pyclack import CancelException, cancel
|
|
1442
|
+
from pyclack.widgets import Progress
|
|
1443
|
+
|
|
1444
|
+
progress: Progress = Progress(max=100, size=30)
|
|
1445
|
+
|
|
1446
|
+
progress.start('Downloading')
|
|
1447
|
+
try:
|
|
1448
|
+
for i in range(100):
|
|
1449
|
+
progress.advance()
|
|
1450
|
+
except CancelException:
|
|
1451
|
+
progress.cancel('Download cancelled')
|
|
1452
|
+
cancel('Operation cancelled')
|
|
1453
|
+
exit(0)
|
|
1454
|
+
progress.stop('Download complete')
|
|
1455
|
+
```
|
|
1456
|
+
|
|
1457
|
+
---
|
|
1458
|
+
|
|
1459
|
+
# `Activity`
|
|
1460
|
+
|
|
1461
|
+
`Activity` pairs a spinner with a running log of activity messages.
|
|
1462
|
+
|
|
1463
|
+
### Input
|
|
1464
|
+
|
|
1465
|
+
```python
|
|
1466
|
+
from pyclack.widgets import Activity
|
|
1467
|
+
|
|
1468
|
+
activity: Activity = Activity(
|
|
1469
|
+
limit=5,
|
|
1470
|
+
show_timer=False,
|
|
1471
|
+
show_elipse=True)
|
|
1472
|
+
```
|
|
1473
|
+
|
|
1474
|
+
Parameters:
|
|
1475
|
+
|
|
1476
|
+
- `limit: int | None` - the maximum number of messages kept and shown at once
|
|
1477
|
+
- `show_timer: bool` - show elapsed time since the activity started
|
|
1478
|
+
- `show_elipse: bool` - show an animated ellipsis after the spinner message
|
|
1479
|
+
- `spinner_delay: float` - milliseconds between spinner frames
|
|
1480
|
+
- `elipse_delay: float` - milliseconds between ellipsis frames
|
|
1481
|
+
- `spinner_frames: SpinnerSymbols | None` - a custom set of spinner frames, or `None` to use the active theme's set
|
|
1482
|
+
### Lifecycle
|
|
1483
|
+
|
|
1484
|
+
```python
|
|
1485
|
+
from pyclack.widgets import Activity
|
|
1486
|
+
|
|
1487
|
+
activity: Activity = Activity()
|
|
1488
|
+
|
|
1489
|
+
activity.start('Building')
|
|
1490
|
+
activity.set_activity_message('Compiling main.py')
|
|
1491
|
+
activity.set_activity_message('Compiling utils.py')
|
|
1492
|
+
activity.stop('Build complete')
|
|
1493
|
+
```
|
|
1494
|
+
|
|
1495
|
+
The activity message and the spinner message are separate pieces of state.
|
|
1496
|
+
|
|
1497
|
+
```python
|
|
1498
|
+
from pyclack.widgets import Activity
|
|
1499
|
+
|
|
1500
|
+
activity: Activity = Activity()
|
|
1501
|
+
|
|
1502
|
+
activity.start('Building')
|
|
1503
|
+
activity.set_spinner_message('Still building')
|
|
1504
|
+
activity.set_activity_message('Compiling main.py')
|
|
1505
|
+
|
|
1506
|
+
current_activity: str = activity.get_activity_message()
|
|
1507
|
+
```
|
|
1508
|
+
|
|
1509
|
+
`Activity` also supports:
|
|
1510
|
+
|
|
1511
|
+
```python
|
|
1512
|
+
activity.cancel('Build cancelled')
|
|
1513
|
+
activity.error('Build failed')
|
|
1514
|
+
activity.clear()
|
|
1515
|
+
cancelled: bool = activity.is_cancelled()
|
|
1516
|
+
```
|
|
1517
|
+
|
|
1518
|
+
---
|
|
1519
|
+
|
|
1520
|
+
# `stream`
|
|
1521
|
+
|
|
1522
|
+
`stream` is for output where the number of messages is not known ahead of time.
|
|
1523
|
+
|
|
1524
|
+
Unlike the other widgets, `stream` accepts an `Iterable[str]` or an `AsyncIterable[str]` directly, and shows each item as it arrives.
|
|
1525
|
+
|
|
1526
|
+
```python
|
|
1527
|
+
from pyclack.widgets import stream
|
|
1528
|
+
|
|
1529
|
+
messages: list[str] = ['Downloading...', 'Extracting...', 'Installing...', 'Complete']
|
|
1530
|
+
|
|
1531
|
+
stream.message(messages)
|
|
1532
|
+
```
|
|
1533
|
+
|
|
1534
|
+
The three stream levels are:
|
|
1535
|
+
|
|
1536
|
+
- `stream.message()`
|
|
1537
|
+
- `stream.info()`
|
|
1538
|
+
- `stream.step()`
|
|
1539
|
+
### A normal iterable
|
|
1540
|
+
|
|
1541
|
+
```python
|
|
1542
|
+
from collections.abc import Iterator
|
|
1543
|
+
|
|
1544
|
+
from pyclack.widgets import stream
|
|
1545
|
+
|
|
1546
|
+
def messages() -> Iterator[str]:
|
|
1547
|
+
yield 'Downloading...'
|
|
1548
|
+
yield 'Extracting...'
|
|
1549
|
+
yield 'Installing...'
|
|
1550
|
+
yield 'Complete'
|
|
1551
|
+
|
|
1552
|
+
stream.message(messages())
|
|
1553
|
+
```
|
|
1554
|
+
|
|
1555
|
+
### An info stream
|
|
1556
|
+
|
|
1557
|
+
```python
|
|
1558
|
+
from collections.abc import Iterator
|
|
1559
|
+
|
|
1560
|
+
from pyclack.widgets import stream
|
|
1561
|
+
|
|
1562
|
+
def messages() -> Iterator[str]:
|
|
1563
|
+
yield 'Connected'
|
|
1564
|
+
yield 'Downloading'
|
|
1565
|
+
yield 'Complete'
|
|
1566
|
+
|
|
1567
|
+
stream.info(messages())
|
|
1568
|
+
```
|
|
1569
|
+
|
|
1570
|
+
### A step stream
|
|
1571
|
+
|
|
1572
|
+
```python
|
|
1573
|
+
from collections.abc import Iterator
|
|
1574
|
+
|
|
1575
|
+
from pyclack.widgets import stream
|
|
1576
|
+
|
|
1577
|
+
def steps() -> Iterator[str]:
|
|
1578
|
+
yield 'Installing dependencies'
|
|
1579
|
+
yield 'Building project'
|
|
1580
|
+
yield 'Running tests'
|
|
1581
|
+
|
|
1582
|
+
stream.step(steps())
|
|
1583
|
+
```
|
|
1584
|
+
|
|
1585
|
+
### An async iterable
|
|
1586
|
+
|
|
1587
|
+
```python
|
|
1588
|
+
from collections.abc import AsyncIterator
|
|
1589
|
+
|
|
1590
|
+
from pyclack.widgets import stream
|
|
1591
|
+
|
|
1592
|
+
async def messages() -> AsyncIterator[str]:
|
|
1593
|
+
yield 'Connecting...'
|
|
1594
|
+
yield 'Downloading...'
|
|
1595
|
+
yield 'Complete'
|
|
1596
|
+
|
|
1597
|
+
stream.message(messages())
|
|
1598
|
+
```
|
|
1599
|
+
|
|
1600
|
+
`stream.message()`, `stream.info()`, and `stream.step()` all accept the same type:
|
|
1601
|
+
|
|
1602
|
+
```python
|
|
1603
|
+
from collections.abc import AsyncIterable, Iterable
|
|
1604
|
+
|
|
1605
|
+
values: Iterable[str] | AsyncIterable[str]
|
|
1606
|
+
```
|
|
1607
|
+
|
|
1608
|
+
Every stream function blocks until its iterable runs out of items.
|
|
1609
|
+
|
|
1610
|
+
---
|
|
1611
|
+
|
|
1612
|
+
# Asynchronous APIs
|
|
1613
|
+
|
|
1614
|
+
pyclack provides asynchronous wrappers for every prompt, under `pyclack.prompts_async`.
|
|
1615
|
+
|
|
1616
|
+
```python
|
|
1617
|
+
from pyclack.prompts_async import ask
|
|
1618
|
+
|
|
1619
|
+
async def get_name() -> str:
|
|
1620
|
+
name: str = await ask('Name')
|
|
1621
|
+
return name
|
|
1622
|
+
```
|
|
1623
|
+
|
|
1624
|
+
Each wrapper keeps the same behavior and return type as its synchronous counterpart.
|
|
1625
|
+
|
|
1626
|
+
A wrapper runs the synchronous prompt in a worker thread, through `asyncio.to_thread()`. This lets you `await` the prompt without blocking the event loop.
|
|
1627
|
+
|
|
1628
|
+
Every prompt has a wrapper:
|
|
1629
|
+
|
|
1630
|
+
```python
|
|
1631
|
+
from pyclack.prompts_async import ask, autocomplete, autocomplete_multiselect, confirm, multiline, multiselect, password, pick_date, select, select_key, select_path
|
|
1632
|
+
```
|
|
1633
|
+
|
|
1634
|
+
The widget wrappers live under `pyclack.widgets_async`:
|
|
1635
|
+
|
|
1636
|
+
```python
|
|
1637
|
+
from pyclack.widgets_async import Activity, Progress, Spinner, TaskLog, box, cancel, intro, note, outro, stream, log
|
|
1638
|
+
```
|
|
1639
|
+
|
|
1640
|
+
The async widget wrappers also delegate through `asyncio.to_thread()`. For example:
|
|
1641
|
+
|
|
1642
|
+
```python
|
|
1643
|
+
from pyclack.widgets_async import Spinner
|
|
1644
|
+
|
|
1645
|
+
async def build() -> None:
|
|
1646
|
+
spinner: Spinner = Spinner()
|
|
1647
|
+
|
|
1648
|
+
await spinner.start('Building')
|
|
1649
|
+
await do_async_build()
|
|
1650
|
+
await spinner.stop('Build complete')
|
|
1651
|
+
```
|
|
1652
|
+
|
|
1653
|
+
---
|
|
1654
|
+
|
|
1655
|
+
# Themes
|
|
1656
|
+
|
|
1657
|
+
A theme sets the colors and symbols every prompt and widget uses.
|
|
1658
|
+
|
|
1659
|
+
A `Theme` holds:
|
|
1660
|
+
|
|
1661
|
+
- an `active` style
|
|
1662
|
+
- a `submit` style
|
|
1663
|
+
- a `cancel` style
|
|
1664
|
+
- an `error` style
|
|
1665
|
+
- an `info` style
|
|
1666
|
+
- a `muted` style
|
|
1667
|
+
- a `text` style
|
|
1668
|
+
- a `cursor` style
|
|
1669
|
+
- a `Symbols` object
|
|
1670
|
+
`Theme` defines one theme. `Themes` collects the built-in themes.
|
|
1671
|
+
|
|
1672
|
+
Change the active theme with `set_active_theme()`:
|
|
1673
|
+
|
|
1674
|
+
```python
|
|
1675
|
+
from pyclack import Themes, set_active_theme
|
|
1676
|
+
|
|
1677
|
+
set_active_theme(Themes.DEFAULT)
|
|
1678
|
+
```
|
|
1679
|
+
|
|
1680
|
+
Read the active theme with `get_active_theme()`:
|
|
1681
|
+
|
|
1682
|
+
```python
|
|
1683
|
+
from pyclack import get_active_theme
|
|
1684
|
+
|
|
1685
|
+
theme = get_active_theme()
|
|
1686
|
+
```
|
|
1687
|
+
|
|
1688
|
+
The theme system stays separate from the prompt and widget code. A prompt or widget asks the active theme for its colors and symbols each time it renders.
|
|
1689
|
+
|
|
1690
|
+
This means switching the active theme changes how every prompt and widget looks, without changing a single line of their rendering code.
|
|
1691
|
+
|
|
1692
|
+
## Custom themes
|
|
1693
|
+
|
|
1694
|
+
Build a `Theme` from `Style`, `Symbols`, `Symbol`, and `SpinnerSymbols`.
|
|
1695
|
+
|
|
1696
|
+
```python
|
|
1697
|
+
from pyclack import set_active_theme
|
|
1698
|
+
from pyclack.renderer import SpinnerSymbols, Style, Symbol, Symbols, Theme
|
|
1699
|
+
|
|
1700
|
+
custom_theme: Theme = Theme(
|
|
1701
|
+
active=Style(fg_color='cyan'),
|
|
1702
|
+
submit=Style(fg_color='green'),
|
|
1703
|
+
cancel=Style(fg_color='red'),
|
|
1704
|
+
error=Style(fg_color='yellow'),
|
|
1705
|
+
info=Style(fg_color='blue'),
|
|
1706
|
+
muted=Style(fg_color='bright_black'),
|
|
1707
|
+
text=Style(fg_color='white'),
|
|
1708
|
+
cursor=Style(fg_color='bright_black', bg_color='white'),
|
|
1709
|
+
symbols=Symbols(
|
|
1710
|
+
step_marker_active=Symbol('◆', '*'),
|
|
1711
|
+
step_marker_cancel=Symbol('■', 'x'),
|
|
1712
|
+
step_marker_error=Symbol('▲', 'x'),
|
|
1713
|
+
step_marker_submit=Symbol('◇', 'o'),
|
|
1714
|
+
connector_bar_start=Symbol('┌', 'T'),
|
|
1715
|
+
connector_bar_vertical=Symbol('│', '|'),
|
|
1716
|
+
connector_bar_end=Symbol('└', '-'),
|
|
1717
|
+
selection_widget_radio_active=Symbol('●', '>'),
|
|
1718
|
+
selection_widget_radio_inactive=Symbol('○', ' '),
|
|
1719
|
+
selection_widget_checkbox_active=Symbol('◻', '[•]'),
|
|
1720
|
+
selection_widget_checkbox_selected=Symbol('◼', '[+]'),
|
|
1721
|
+
selection_widget_checkbox_inactive=Symbol('◻', '[ ]'),
|
|
1722
|
+
selection_widget_password_mask=Symbol('▪', '*'),
|
|
1723
|
+
box_drawing_horizontal_bar=Symbol('─', '-'),
|
|
1724
|
+
box_drawing_vertical_bar=Symbol('│', '|'),
|
|
1725
|
+
box_drawing_top_right_corner_rounded=Symbol('╮', '+'),
|
|
1726
|
+
box_drawing_left_connector=Symbol('├', '+'),
|
|
1727
|
+
box_drawing_bottom_right_corner_rounded=Symbol('╯', '+'),
|
|
1728
|
+
box_drawing_top_left_corner_rounded=Symbol('╭', '+'),
|
|
1729
|
+
box_drawing_bottom_left_corner_rounded=Symbol('╰', '+'),
|
|
1730
|
+
box_drawing_top_right_corner=Symbol('┐', '+'),
|
|
1731
|
+
box_drawing_bottom_right_corner=Symbol('┘', '+'),
|
|
1732
|
+
box_drawing_top_left_corner=Symbol('┌', '+'),
|
|
1733
|
+
box_drawing_bottom_left_corner=Symbol('└', '+'),
|
|
1734
|
+
log_level_info=Symbol('●', 'i'),
|
|
1735
|
+
log_level_success=Symbol('◆', '*'),
|
|
1736
|
+
log_level_warn=Symbol('▲', '!'),
|
|
1737
|
+
log_level_error=Symbol('■', 'x'),
|
|
1738
|
+
spinner=SpinnerSymbols(
|
|
1739
|
+
unicode_symbols=('◒', '◐', '◓', '◑'),
|
|
1740
|
+
ascii_symbols=('|', '/', '-', '\\')),
|
|
1741
|
+
progress_light=Symbol('─', '-'),
|
|
1742
|
+
progress_heavy=Symbol('━', '='),
|
|
1743
|
+
progress_block=Symbol('█', '#')))
|
|
1744
|
+
|
|
1745
|
+
set_active_theme(custom_theme)
|
|
1746
|
+
```
|
|
1747
|
+
|
|
1748
|
+
The repository ships more than 30 built-in themes, each with its own colors and symbols. See `demos/demo.py` for the full list of names under `Themes`.
|
|
1749
|
+
|
|
1750
|
+
## Unicode and ASCII symbols
|
|
1751
|
+
|
|
1752
|
+
Every `Symbol` holds a Unicode form and an ASCII fallback.
|
|
1753
|
+
|
|
1754
|
+
```python
|
|
1755
|
+
from pyclack.renderer import Symbol
|
|
1756
|
+
|
|
1757
|
+
marker: Symbol = Symbol('◆', '*')
|
|
1758
|
+
```
|
|
1759
|
+
|
|
1760
|
+
The renderer picks whichever form fits the current terminal.
|
|
1761
|
+
|
|
1762
|
+
Force ASCII-only output with:
|
|
1763
|
+
|
|
1764
|
+
```python
|
|
1765
|
+
from pyclack import set_print_mode_ascii
|
|
1766
|
+
|
|
1767
|
+
set_print_mode_ascii()
|
|
1768
|
+
```
|
|
1769
|
+
|
|
1770
|
+
---
|
|
1771
|
+
|
|
1772
|
+
# Rendering
|
|
1773
|
+
|
|
1774
|
+
The rendering system lives under `pyclack.renderer`.
|
|
1775
|
+
|
|
1776
|
+
```python
|
|
1777
|
+
from pyclack.renderer import FrameBuilder, RenderFrame, SpinnerSymbols, Style, Symbol, Symbols, Text, Theme, Themes
|
|
1778
|
+
```
|
|
1779
|
+
|
|
1780
|
+
The rendering model has four steps:
|
|
1781
|
+
|
|
1782
|
+
1. Build `Text` objects.
|
|
1783
|
+
2. Add them to a `FrameBuilder`.
|
|
1784
|
+
3. Build the frame.
|
|
1785
|
+
4. Draw it with `RenderFrame`.
|
|
1786
|
+
## `Text`
|
|
1787
|
+
|
|
1788
|
+
`Text` pairs terminal text with an optional style.
|
|
1789
|
+
|
|
1790
|
+
```python
|
|
1791
|
+
from pyclack.renderer import Style, Text
|
|
1792
|
+
|
|
1793
|
+
style: Style = Style(fg_color='cyan', bold=True)
|
|
1794
|
+
|
|
1795
|
+
text: Text = Text('Hello', style)
|
|
1796
|
+
```
|
|
1797
|
+
|
|
1798
|
+
You can combine `Text` objects to build more complex output.
|
|
1799
|
+
|
|
1800
|
+
## `FrameBuilder`
|
|
1801
|
+
|
|
1802
|
+
`FrameBuilder` collects the lines that make up one frame.
|
|
1803
|
+
|
|
1804
|
+
```python
|
|
1805
|
+
from pyclack.renderer import FrameBuilder, Text
|
|
1806
|
+
|
|
1807
|
+
builder: FrameBuilder = FrameBuilder()
|
|
1808
|
+
|
|
1809
|
+
builder.add_line(Text('First line'))
|
|
1810
|
+
builder.add_line(Text('Second line'))
|
|
1811
|
+
|
|
1812
|
+
frame: tuple[Text, ...] = builder.build()
|
|
1813
|
+
```
|
|
1814
|
+
|
|
1815
|
+
Use `add_lines()` to add more than one line at once.
|
|
1816
|
+
|
|
1817
|
+
```python
|
|
1818
|
+
from pyclack.renderer import FrameBuilder, Text
|
|
1819
|
+
|
|
1820
|
+
builder: FrameBuilder = FrameBuilder()
|
|
1821
|
+
|
|
1822
|
+
builder.add_lines(
|
|
1823
|
+
Text('First'),
|
|
1824
|
+
Text('Second'),
|
|
1825
|
+
Text('Third'))
|
|
1826
|
+
|
|
1827
|
+
frame: tuple[Text, ...] = builder.build()
|
|
1828
|
+
```
|
|
1829
|
+
|
|
1830
|
+
## `RenderFrame`
|
|
1831
|
+
|
|
1832
|
+
`RenderFrame` owns the frame currently on screen.
|
|
1833
|
+
|
|
1834
|
+
When you draw a new frame, `RenderFrame` clears the old one first.
|
|
1835
|
+
|
|
1836
|
+
```python
|
|
1837
|
+
from pyclack.renderer import RenderFrame, Text
|
|
1838
|
+
|
|
1839
|
+
render_frame: RenderFrame = RenderFrame()
|
|
1840
|
+
|
|
1841
|
+
render_frame.draw_frame(Text('Loading...'))
|
|
1842
|
+
```
|
|
1843
|
+
|
|
1844
|
+
Clear a frame directly with:
|
|
1845
|
+
|
|
1846
|
+
```python
|
|
1847
|
+
from pyclack.renderer import RenderFrame, Text
|
|
1848
|
+
|
|
1849
|
+
render_frame: RenderFrame = RenderFrame()
|
|
1850
|
+
|
|
1851
|
+
render_frame.draw_frame(Text('Loading...'))
|
|
1852
|
+
render_frame.clear_frame()
|
|
1853
|
+
```
|
|
1854
|
+
|
|
1855
|
+
This frame model lets a spinner, a prompt, or a progress bar redraw itself in place, instead of printing a new line every time it updates.
|
|
1856
|
+
|
|
1857
|
+
---
|
|
1858
|
+
|
|
1859
|
+
# Terminal
|
|
1860
|
+
|
|
1861
|
+
The terminal subsystem lives under `pyclack.terminal`.
|
|
1862
|
+
|
|
1863
|
+
```python
|
|
1864
|
+
from pyclack.terminal import CursorController, EchoController, KeyReader, Stdout
|
|
1865
|
+
```
|
|
1866
|
+
|
|
1867
|
+
It gives prompts and widgets the low-level terminal operations they need.
|
|
1868
|
+
|
|
1869
|
+
## `KeyReader`
|
|
1870
|
+
|
|
1871
|
+
`KeyReader` reads one key at a time, instead of waiting for a full line of input.
|
|
1872
|
+
|
|
1873
|
+
A custom component should use `KeyReader` instead of reading `stdin` directly.
|
|
1874
|
+
|
|
1875
|
+
## `CursorController`
|
|
1876
|
+
|
|
1877
|
+
`CursorController` builds the escape sequences that hide the cursor, show the cursor, and move or clear rendered lines.
|
|
1878
|
+
|
|
1879
|
+
```python
|
|
1880
|
+
from pyclack.terminal import CursorController
|
|
1881
|
+
|
|
1882
|
+
hide_sequence: str = CursorController.hide_cursor()
|
|
1883
|
+
show_sequence: str = CursorController.show_cursor()
|
|
1884
|
+
```
|
|
1885
|
+
|
|
1886
|
+
## `Stdout`
|
|
1887
|
+
|
|
1888
|
+
`Stdout` is the output abstraction every pyclack component writes through.
|
|
1889
|
+
|
|
1890
|
+
A custom widget should use `Stdout` instead of calling `print()` directly inside its rendering code.
|
|
1891
|
+
|
|
1892
|
+
## `EchoController`
|
|
1893
|
+
|
|
1894
|
+
`EchoController` turns terminal echo on and off for interactive components.
|
|
1895
|
+
|
|
1896
|
+
This matters most for a prompt that needs to control how a keypress, such as Ctrl+C, appears on screen.
|
|
1897
|
+
|
|
1898
|
+
A custom interactive component should use the existing terminal controllers, instead of writing its own platform-specific terminal code.
|
|
1899
|
+
|
|
1900
|
+
---
|
|
1901
|
+
|
|
1902
|
+
# Building a custom prompt
|
|
1903
|
+
|
|
1904
|
+
If none of the built-in prompts fit your use case, subclass `PromptBase`. It gives your prompt the same state machine and rendering conventions the built-in prompts use.
|
|
1905
|
+
|
|
1906
|
+
A prompt moves through five states:
|
|
1907
|
+
|
|
1908
|
+
```python
|
|
1909
|
+
from pyclack.prompts import PromptState
|
|
1910
|
+
|
|
1911
|
+
initial: PromptState = PromptState.INITIAL
|
|
1912
|
+
active: PromptState = PromptState.ACTIVE
|
|
1913
|
+
submit: PromptState = PromptState.SUBMIT
|
|
1914
|
+
cancel: PromptState = PromptState.CANCEL
|
|
1915
|
+
error: PromptState = PromptState.ERROR
|
|
1916
|
+
```
|
|
1917
|
+
|
|
1918
|
+
The normal flow is:
|
|
1919
|
+
|
|
1920
|
+
```text
|
|
1921
|
+
INITIAL
|
|
1922
|
+
|
|
|
1923
|
+
v
|
|
1924
|
+
ACTIVE <----+
|
|
1925
|
+
| |
|
|
1926
|
+
v |
|
|
1927
|
+
SUBMIT |
|
|
1928
|
+
| |
|
|
1929
|
+
+-- error
|
|
1930
|
+
|
|
|
1931
|
+
v
|
|
1932
|
+
EXIT
|
|
1933
|
+
|
|
1934
|
+
ACTIVE/ERROR
|
|
1935
|
+
|
|
|
1936
|
+
+----> CANCEL
|
|
1937
|
+
```
|
|
1938
|
+
|
|
1939
|
+
`PromptBase` runs the state machine. Your subclass supplies the behavior for each state.
|
|
1940
|
+
|
|
1941
|
+
## Custom prompt structure
|
|
1942
|
+
|
|
1943
|
+
A custom prompt should:
|
|
1944
|
+
|
|
1945
|
+
1. Subclass `PromptBase`.
|
|
1946
|
+
2. Store its input state on the prompt instance.
|
|
1947
|
+
3. Create a `RenderFrame`.
|
|
1948
|
+
4. Implement `handle_active()`.
|
|
1949
|
+
5. Implement `handle_submit()`.
|
|
1950
|
+
6. Implement `handle_error()`, if the prompt needs validation.
|
|
1951
|
+
7. Implement `handle_cancel()`.
|
|
1952
|
+
8. Raise `CancelException` from `handle_cancel()`.
|
|
1953
|
+
9. Render each state through `FrameBuilder`, `Text`, the active `Theme`, and `RenderFrame`.
|
|
1954
|
+
10. Expose a small public function that builds the prompt and returns its final value.
|
|
1955
|
+
A minimal prompt looks like this:
|
|
1956
|
+
|
|
1957
|
+
```python
|
|
1958
|
+
from typing import override
|
|
1959
|
+
|
|
1960
|
+
from pyclack.prompts import CancelException, PromptBase
|
|
1961
|
+
from pyclack.renderer import FrameBuilder, RenderFrame, Text
|
|
1962
|
+
|
|
1963
|
+
class CustomPrompt(PromptBase):
|
|
1964
|
+
def __init__(self, message: str) -> None:
|
|
1965
|
+
super().__init__()
|
|
1966
|
+
|
|
1967
|
+
self.message: str = message
|
|
1968
|
+
self.value: str = ''
|
|
1969
|
+
self.render_frame: RenderFrame = RenderFrame()
|
|
1970
|
+
|
|
1971
|
+
self.activate()
|
|
1972
|
+
|
|
1973
|
+
@override
|
|
1974
|
+
def handle_active(self, key: str | None) -> bool:
|
|
1975
|
+
frame_builder: FrameBuilder = FrameBuilder()
|
|
1976
|
+
frame_builder.add_line(Text(self.message))
|
|
1977
|
+
frame_builder.add_line(Text(self.value))
|
|
1978
|
+
|
|
1979
|
+
frame: tuple[Text, ...] = frame_builder.build()
|
|
1980
|
+
self.render_frame.draw_frame(*frame)
|
|
1981
|
+
|
|
1982
|
+
if key == 'ENTER': return True
|
|
1983
|
+
if key: self.value += key
|
|
1984
|
+
return False
|
|
1985
|
+
|
|
1986
|
+
@override
|
|
1987
|
+
def handle_submit(self) -> bool:
|
|
1988
|
+
return True
|
|
1989
|
+
|
|
1990
|
+
@override
|
|
1991
|
+
def handle_cancel(self) -> None:
|
|
1992
|
+
raise CancelException[str](self.value)
|
|
1993
|
+
```
|
|
1994
|
+
|
|
1995
|
+
Then expose it through a small function:
|
|
1996
|
+
|
|
1997
|
+
```python
|
|
1998
|
+
from pyclack.prompts import CancelException
|
|
1999
|
+
|
|
2000
|
+
def custom_prompt(message: str) -> str:
|
|
2001
|
+
prompt: CustomPrompt = CustomPrompt(message)
|
|
2002
|
+
return prompt.value
|
|
2003
|
+
```
|
|
2004
|
+
|
|
2005
|
+
A real prompt's rendering is usually more involved than this. The convention that matters is: the prompt owns its state, and `PromptBase` owns the state machine.
|
|
2006
|
+
|
|
2007
|
+
## Validation
|
|
2008
|
+
|
|
2009
|
+
When the prompt can enter an invalid state, `handle_submit()` should return `False`.
|
|
2010
|
+
|
|
2011
|
+
Returning `False` sends the prompt into `handle_error()`.
|
|
2012
|
+
|
|
2013
|
+
```python
|
|
2014
|
+
from typing import override
|
|
2015
|
+
|
|
2016
|
+
from pyclack.prompts import PromptBase
|
|
2017
|
+
|
|
2018
|
+
class ValidatedPrompt(PromptBase):
|
|
2019
|
+
@override
|
|
2020
|
+
def handle_submit(self) -> bool:
|
|
2021
|
+
return self._is_valid()
|
|
2022
|
+
```
|
|
2023
|
+
|
|
2024
|
+
`handle_error()` renders the error state, then returns one of:
|
|
2025
|
+
|
|
2026
|
+
- `False`, to stay in the error state
|
|
2027
|
+
- `True`, to return to the active state
|
|
2028
|
+
`PromptBase` handles the state change either way.
|
|
2029
|
+
|
|
2030
|
+
## Propagating the key after an error
|
|
2031
|
+
|
|
2032
|
+
A prompt that wants the key that clears an error to also act as the next active-state key can set:
|
|
2033
|
+
|
|
2034
|
+
```python
|
|
2035
|
+
from pyclack.prompts import PromptBase
|
|
2036
|
+
|
|
2037
|
+
class CustomPrompt(PromptBase):
|
|
2038
|
+
def __init__(self) -> None:
|
|
2039
|
+
super().__init__()
|
|
2040
|
+
|
|
2041
|
+
self.propagate_key_after_error: bool = True
|
|
2042
|
+
```
|
|
2043
|
+
|
|
2044
|
+
---
|
|
2045
|
+
|
|
2046
|
+
# Building a custom widget
|
|
2047
|
+
|
|
2048
|
+
A widget does not use the prompt state machine.
|
|
2049
|
+
|
|
2050
|
+
A custom widget owns its own rendering state directly, and uses `RenderFrame` to redraw it.
|
|
2051
|
+
|
|
2052
|
+
A minimal stateful widget looks like this:
|
|
2053
|
+
|
|
2054
|
+
```python
|
|
2055
|
+
from pyclack.renderer import FrameBuilder, RenderFrame, Text
|
|
2056
|
+
|
|
2057
|
+
class CustomWidget:
|
|
2058
|
+
def __init__(self) -> None:
|
|
2059
|
+
self.render_frame: RenderFrame = RenderFrame()
|
|
2060
|
+
self.message: str = ''
|
|
2061
|
+
|
|
2062
|
+
def start(self, message: str) -> None:
|
|
2063
|
+
self.message = message
|
|
2064
|
+
self._render()
|
|
2065
|
+
|
|
2066
|
+
def set_message(self, message: str) -> None:
|
|
2067
|
+
self.message = message
|
|
2068
|
+
self._render()
|
|
2069
|
+
|
|
2070
|
+
def clear(self) -> None:
|
|
2071
|
+
self.render_frame.clear_frame()
|
|
2072
|
+
|
|
2073
|
+
def _render(self) -> None:
|
|
2074
|
+
frame_builder: FrameBuilder = FrameBuilder()
|
|
2075
|
+
frame_builder.add_line(Text(self.message))
|
|
2076
|
+
|
|
2077
|
+
frame: tuple[Text, ...] = frame_builder.build()
|
|
2078
|
+
self.render_frame.draw_frame(*frame)
|
|
2079
|
+
```
|
|
2080
|
+
|
|
2081
|
+
A real pyclack widget pulls its style from the active theme, instead of hard-coding it:
|
|
2082
|
+
|
|
2083
|
+
```python
|
|
2084
|
+
from pyclack import get_active_theme
|
|
2085
|
+
from pyclack.renderer import FrameBuilder, RenderFrame, Text
|
|
2086
|
+
|
|
2087
|
+
class ThemedWidget:
|
|
2088
|
+
def __init__(self) -> None:
|
|
2089
|
+
self.render_frame: RenderFrame = RenderFrame()
|
|
2090
|
+
|
|
2091
|
+
def render(self, message: str) -> None:
|
|
2092
|
+
theme = get_active_theme()
|
|
2093
|
+
|
|
2094
|
+
frame_builder: FrameBuilder = FrameBuilder()
|
|
2095
|
+
frame_builder.add_line(Text(message, theme.text))
|
|
2096
|
+
|
|
2097
|
+
frame: tuple[Text, ...] = frame_builder.build()
|
|
2098
|
+
self.render_frame.draw_frame(*frame)
|
|
2099
|
+
```
|
|
2100
|
+
|
|
2101
|
+
This keeps the widget correct under every active theme.
|
|
2102
|
+
|
|
2103
|
+
Some widgets take time to render, what if the user presses Ctrl+C during that? `CancelException` should be raised. pyclack does this be swapping the hander that runs when a `SIGINT` is raised by a Ctrl+C press.
|
|
2104
|
+
|
|
2105
|
+
This is usually run at the start of the pyclack widget:
|
|
2106
|
+
```python
|
|
2107
|
+
import signal
|
|
2108
|
+
from pyclack import CancelException
|
|
2109
|
+
|
|
2110
|
+
old_sigint_handler = signal.getsignal(signal.SIGINT)
|
|
2111
|
+
def handle_interrupt(signum, frame) -> None:
|
|
2112
|
+
signal.signal(signal.SIGINT, old_sigint_handler) # resets back to old handler if cancelled
|
|
2113
|
+
raise CancelException
|
|
2114
|
+
signal.signal(signal.SIGINT, handle_interrupt)
|
|
2115
|
+
```
|
|
2116
|
+
|
|
2117
|
+
At the end of the pyclack widget the handler is reset:
|
|
2118
|
+
```python
|
|
2119
|
+
signal.signal(signal.SIGINT, old_sigint_handler)
|
|
2120
|
+
```
|
|
2121
|
+
|
|
2122
|
+
---
|
|
2123
|
+
|
|
2124
|
+
# Custom component conventions
|
|
2125
|
+
|
|
2126
|
+
When you extend pyclack, follow the same pattern as the built-in components.
|
|
2127
|
+
|
|
2128
|
+
### Prompts
|
|
2129
|
+
|
|
2130
|
+
- Subclass `PromptBase`.
|
|
2131
|
+
- Keep interactive state on the prompt instance.
|
|
2132
|
+
- Drive the prompt through `PromptState`, using the base state machine, instead of writing a separate input loop.
|
|
2133
|
+
- Render every state through `RenderFrame`.
|
|
2134
|
+
- Build output with `Text` and `FrameBuilder`.
|
|
2135
|
+
- Pull colors and symbols from `get_active_theme()`.
|
|
2136
|
+
- Signal cancellation through `CancelException`.
|
|
2137
|
+
- Put useful partial state in `CancelException.value`.
|
|
2138
|
+
- Support `abort_time` when the prompt should be able to cancel itself.
|
|
2139
|
+
- Expose one small public function that returns the prompt's final value.
|
|
2140
|
+
### Widgets
|
|
2141
|
+
|
|
2142
|
+
- Do not subclass `PromptBase`.
|
|
2143
|
+
- Own the widget's state directly.
|
|
2144
|
+
- Set own custom Ctrl+C handler.
|
|
2145
|
+
- Use `RenderFrame` for output that redraws in place.
|
|
2146
|
+
- Build frames with `FrameBuilder` and `Text`.
|
|
2147
|
+
- Pull visual properties from the active theme.
|
|
2148
|
+
- Use `Stdout` and the terminal controllers for terminal changes.
|
|
2149
|
+
- Restore old handler, cursor, and echo state when the widget finishes or is cancelled.
|
|
2150
|
+
### Themes
|
|
2151
|
+
|
|
2152
|
+
- Never hard-code a color or symbol that belongs in the theme.
|
|
2153
|
+
- Use `Style` for styles.
|
|
2154
|
+
- Use `Symbol` for individual symbols.
|
|
2155
|
+
- Use `SpinnerSymbols` for animated spinner frames.
|
|
2156
|
+
- Provide both a Unicode form and an ASCII fallback where one applies.
|
|
2157
|
+
This separation lets pyclack change its whole appearance without touching a single prompt or widget's code.
|
|
2158
|
+
|
|
2159
|
+
---
|
|
2160
|
+
|
|
2161
|
+
# Example
|
|
2162
|
+
|
|
2163
|
+
This small script combines a few prompts and widgets. It follows the same style as `demos/demo.py`, which covers every prompt and widget in the package.
|
|
2164
|
+
|
|
2165
|
+
```python
|
|
2166
|
+
from pyclack import CancelException, ClackOption
|
|
2167
|
+
from pyclack.prompts import ask, select
|
|
2168
|
+
from pyclack.widgets import intro, outro, cancel
|
|
2169
|
+
|
|
2170
|
+
def main() -> None:
|
|
2171
|
+
intro('Example')
|
|
2172
|
+
|
|
2173
|
+
try:
|
|
2174
|
+
name: str = ask('What is your name?')
|
|
2175
|
+
|
|
2176
|
+
languages: list[ClackOption[str]] = [
|
|
2177
|
+
ClackOption[str](value='python', label='Python'),
|
|
2178
|
+
ClackOption[str](value='rust', label='Rust')]
|
|
2179
|
+
|
|
2180
|
+
language: ClackOption[str] = select(
|
|
2181
|
+
'Favorite language',
|
|
2182
|
+
options=languages)
|
|
2183
|
+
except CancelException:
|
|
2184
|
+
cancel('Operation cancelled.')
|
|
2185
|
+
exit(0)
|
|
2186
|
+
|
|
2187
|
+
outro(f'Hello {name}! You chose {language.value}.')
|
|
2188
|
+
|
|
2189
|
+
if __name__ == '__main__':
|
|
2190
|
+
main()
|
|
2191
|
+
```
|
|
2192
|
+
|
|
2193
|
+
Run the full demo with:
|
|
2194
|
+
|
|
2195
|
+
```bash
|
|
2196
|
+
uv run demos/demo.py
|
|
2197
|
+
```
|
|
2198
|
+
|
|
2199
|
+
The pattern behind every piece of pyclack stays the same:
|
|
2200
|
+
|
|
2201
|
+
```text
|
|
2202
|
+
prompt -> value
|
|
2203
|
+
widget -> terminal output
|
|
2204
|
+
cancel -> CancelException
|
|
2205
|
+
partial state -> e.value
|
|
2206
|
+
selection -> ClackOption[T]
|
|
2207
|
+
theme -> active Theme
|
|
2208
|
+
custom prompt -> PromptBase
|
|
2209
|
+
custom widget -> RenderFrame
|
|
2210
|
+
```
|
|
2211
|
+
|
|
2212
|
+
That is the core of pyclack.
|