simplibs-cli 0.1.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.
Files changed (47) hide show
  1. simplibs_cli-0.1.0/LICENSE +21 -0
  2. simplibs_cli-0.1.0/PKG-INFO +436 -0
  3. simplibs_cli-0.1.0/README.md +403 -0
  4. simplibs_cli-0.1.0/pyproject.toml +78 -0
  5. simplibs_cli-0.1.0/setup.cfg +4 -0
  6. simplibs_cli-0.1.0/src/simplibs/cli/__init__.py +87 -0
  7. simplibs_cli-0.1.0/src/simplibs/cli/interaction/__init__.py +21 -0
  8. simplibs_cli-0.1.0/src/simplibs/cli/interaction/_helpers/__init__.py +15 -0
  9. simplibs_cli-0.1.0/src/simplibs/cli/interaction/_helpers/is_terminal.py +34 -0
  10. simplibs_cli-0.1.0/src/simplibs/cli/interaction/_validations/__init__.py +15 -0
  11. simplibs_cli-0.1.0/src/simplibs/cli/interaction/_validations/raise_not_interactive_error.py +42 -0
  12. simplibs_cli-0.1.0/src/simplibs/cli/interaction/is_interactive.py +41 -0
  13. simplibs_cli-0.1.0/src/simplibs/cli/interaction/require_interactive.py +41 -0
  14. simplibs_cli-0.1.0/src/simplibs/cli/prompts/__init__.py +43 -0
  15. simplibs_cli-0.1.0/src/simplibs/cli/prompts/_helpers/__init__.py +27 -0
  16. simplibs_cli-0.1.0/src/simplibs/cli/prompts/_helpers/build_rejection_message.py +43 -0
  17. simplibs_cli-0.1.0/src/simplibs/cli/prompts/_helpers/describe_range.py +36 -0
  18. simplibs_cli-0.1.0/src/simplibs/cli/prompts/_helpers/format_default_hint.py +33 -0
  19. simplibs_cli-0.1.0/src/simplibs/cli/prompts/_helpers/parse_keys.py +30 -0
  20. simplibs_cli-0.1.0/src/simplibs/cli/prompts/_helpers/print_problem.py +23 -0
  21. simplibs_cli-0.1.0/src/simplibs/cli/prompts/_helpers/read_answer.py +44 -0
  22. simplibs_cli-0.1.0/src/simplibs/cli/prompts/_helpers/unquote_answer.py +32 -0
  23. simplibs_cli-0.1.0/src/simplibs/cli/prompts/_validations/__init__.py +23 -0
  24. simplibs_cli-0.1.0/src/simplibs/cli/prompts/_validations/raise_create_needs_kind_error.py +31 -0
  25. simplibs_cli-0.1.0/src/simplibs/cli/prompts/_validations/raise_default_not_in_options_error.py +38 -0
  26. simplibs_cli-0.1.0/src/simplibs/cli/prompts/_validations/raise_no_answer_available_error.py +41 -0
  27. simplibs_cli-0.1.0/src/simplibs/cli/prompts/_validations/raise_options_empty_error.py +28 -0
  28. simplibs_cli-0.1.0/src/simplibs/cli/prompts/_validations/raise_range_empty_error.py +41 -0
  29. simplibs_cli-0.1.0/src/simplibs/cli/prompts/prompt_choice.py +87 -0
  30. simplibs_cli-0.1.0/src/simplibs/cli/prompts/prompt_confirm.py +73 -0
  31. simplibs_cli-0.1.0/src/simplibs/cli/prompts/prompt_int.py +99 -0
  32. simplibs_cli-0.1.0/src/simplibs/cli/prompts/prompt_multi_choice.py +107 -0
  33. simplibs_cli-0.1.0/src/simplibs/cli/prompts/prompt_path.py +153 -0
  34. simplibs_cli-0.1.0/src/simplibs/cli/prompts/prompt_secret.py +71 -0
  35. simplibs_cli-0.1.0/src/simplibs/cli/prompts/prompt_text.py +90 -0
  36. simplibs_cli-0.1.0/src/simplibs/cli/prompts/resolve_or_prompt.py +74 -0
  37. simplibs_cli-0.1.0/src/simplibs/cli/py.typed +1 -0
  38. simplibs_cli-0.1.0/src/simplibs/cli/reporting/__init__.py +18 -0
  39. simplibs_cli-0.1.0/src/simplibs/cli/reporting/print_choices.py +58 -0
  40. simplibs_cli-0.1.0/src/simplibs/cli/reporting/report_failures.py +64 -0
  41. simplibs_cli-0.1.0/src/simplibs/cli/verbosity/__init__.py +16 -0
  42. simplibs_cli-0.1.0/src/simplibs/cli/verbosity/resolve_verbosity.py +76 -0
  43. simplibs_cli-0.1.0/src/simplibs_cli.egg-info/PKG-INFO +436 -0
  44. simplibs_cli-0.1.0/src/simplibs_cli.egg-info/SOURCES.txt +45 -0
  45. simplibs_cli-0.1.0/src/simplibs_cli.egg-info/dependency_links.txt +1 -0
  46. simplibs_cli-0.1.0/src/simplibs_cli.egg-info/requires.txt +9 -0
  47. simplibs_cli-0.1.0/src/simplibs_cli.egg-info/top_level.txt +1 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Dalibor Sova (Sudip2708)
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
@@ -0,0 +1,436 @@
1
+ Metadata-Version: 2.4
2
+ Name: simplibs-cli
3
+ Version: 0.1.0
4
+ Summary: The small, repeatable parts of a command-line program that talks to a person: prompts that ask again until the answer is usable, a check whether anybody can answer, option lists, failure summaries and verbosity flags resolved to a logging level.
5
+ Author-email: "Dalibor Sova (Sudip2708)" <daliborsova@seznam.cz>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/simplibs/simplibs-cli
8
+ Project-URL: Repository, https://github.com/simplibs/simplibs-cli
9
+ Project-URL: Issues, https://github.com/simplibs/simplibs-cli/issues
10
+ Project-URL: Changelog, https://github.com/simplibs/simplibs-cli/blob/main/CHANGELOG.md
11
+ Keywords: cli,command-line,prompt,prompts,input,interactive,terminal,verbosity,argparse,simplibs,simplibs-cli,developer-tools
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
19
+ Classifier: Topic :: Software Development :: User Interfaces
20
+ Classifier: Typing :: Typed
21
+ Requires-Python: >=3.11
22
+ Description-Content-Type: text/markdown
23
+ License-File: LICENSE
24
+ Requires-Dist: simplibs-exception>=1.1.4
25
+ Requires-Dist: simplibs-validate>=0.3.2
26
+ Requires-Dist: simplibs-rules>=0.4.0
27
+ Requires-Dist: simplibs-path>=0.1.1
28
+ Requires-Dist: simplibs-style>=0.1.0
29
+ Provides-Extra: dev
30
+ Requires-Dist: pytest>=9.0; extra == "dev"
31
+ Requires-Dist: pytest-cov>=5.0; extra == "dev"
32
+ Dynamic: license-file
33
+
34
+ # 💬 `simplibs-cli`
35
+
36
+ [![PyPI](https://img.shields.io/pypi/v/simplibs-cli)](https://pypi.org/project/simplibs-cli/)
37
+ [![Python](https://img.shields.io/badge/python-3.11%2B-blue)](https://www.python.org/downloads/)
38
+ [![Licence](https://img.shields.io/badge/licence-MIT-green)](https://github.com/simplibs/simplibs-cli/blob/main/LICENSE)
39
+ [![tests](https://github.com/simplibs/simplibs-cli/actions/workflows/tests.yml/badge.svg)](https://github.com/simplibs/simplibs-cli/actions)
40
+
41
+ **The conversation with the person at the terminal — asked once, asked right.**
42
+
43
+ A small Python library that replaces the `while True: input()` loop every command-line program
44
+ writes by hand. A prompt **asks again until the answer is acceptable**, so what it returns needs no
45
+ second check; a typo is answered with one warning line, a wrong call with a card, and a program with
46
+ nobody to ask with a clear message instead of a hang. It starts where `argparse`, `click` or `typer`
47
+ hand over the values, and it does nothing but the conversation.
48
+
49
+ ```python
50
+ from simplibs.cli import prompt_int, prompt_confirm
51
+
52
+ workers = prompt_int("Workers", default=4, minimum=1, maximum=64)
53
+ # Workers [4]: 100
54
+ # Please enter a whole number from 1 to 64. <- asked again, nothing raised
55
+ # Workers [4]: <- Enter: the default
56
+
57
+ if prompt_confirm("Start?", default=False): # a hurried Enter means "no"
58
+ print(f"Starting {workers} workers")
59
+ ```
60
+
61
+ > Colors come from [`simplibs-style`](https://pypi.org/project/simplibs-style/) and disappear on their
62
+ > own when the output is a pipe or a file.
63
+
64
+ ---
65
+
66
+ ## 🧭 The Core Philosophy
67
+
68
+ Every command-line program that asks a question ends up with the same loop: read a line, strip it,
69
+ parse it, check the range, print a complaint, repeat. Each copy forgets something — the empty Enter,
70
+ the default that is not an option, the input that has ended and turns into a bare `EOFError` in CI.
71
+ `simplibs-cli` writes the loop once, and writes it carefully.
72
+
73
+ * **Ask until it is usable.** A prompt returns only an acceptable answer — the right type, within the
74
+ limits, one of the options, satisfying the rule. What comes back needs no second check.
75
+ * **A typo is not an exception.** A refused answer is explained in one warning line and the question
76
+ is repeated. A *wrong call* — an empty option list, a default that is not an option, limits that
77
+ exclude everything — is the programmer's mistake and raises a card before the first question.
78
+ * **A default is a real answer.** Pressing Enter returns it, and it goes through the same checks as
79
+ anything typed, so the result always satisfies what the caller asked for.
80
+ * **The command line first.** `resolve_or_prompt` uses a value from the arguments when it is good and
81
+ asks only when it is missing or refused — the program reads the same either way.
82
+ * **Nobody there is an answer too.** `is_interactive()` and `require_interactive()` turn "the program
83
+ hangs in CI" into a card that names the option to pass.
84
+ * **It starts where the parser stops.** No argument parsing, no logging setup — `resolve_verbosity`
85
+ only returns the level. There are no classes, rules or actions: a prompt is an interaction, not a
86
+ step of a data pipeline.
87
+ * **Layers that only look left.** `interaction`, `reporting` and `verbosity` stand alone ← `prompts`
88
+ — every layer imports only from those before it.
89
+
90
+ ```python
91
+ prompt_choice("Format", {}) # -> raises: nothing to choose from
92
+ prompt_int("Workers", minimum=10, maximum=1) # -> raises: the limits exclude everything
93
+ prompt_choice("Format", {"json": "…"}, default="xml") # -> raises: the default is not an option
94
+ ```
95
+
96
+ ---
97
+
98
+ ## 📦 Installation
99
+
100
+ ```bash
101
+ pip install simplibs-cli
102
+ ```
103
+
104
+ Requires Python 3.11+. The runtime dependencies (installed automatically) are other simplibs
105
+ libraries: [`simplibs-exception`](https://pypi.org/project/simplibs-exception/) for the structured
106
+ error cards, [`simplibs-validate`](https://pypi.org/project/simplibs-validate/) for `@validate_call` on
107
+ the public functions, [`simplibs-rules`](https://pypi.org/project/simplibs-rules/) for the message of
108
+ a refusing rule, [`simplibs-path`](https://pypi.org/project/simplibs-path/) for `prompt_path` and
109
+ [`simplibs-style`](https://pypi.org/project/simplibs-style/) for colors that disappear outside a
110
+ terminal.
111
+
112
+ ---
113
+
114
+ ## 🚀 Quick Start in 60 Seconds
115
+
116
+ ### Level 1: Ask and get a usable answer
117
+
118
+ ```python
119
+ from simplibs.cli import prompt_text, prompt_int, prompt_confirm
120
+
121
+ name = prompt_text("Project name", default="demo")
122
+ workers = prompt_int("Workers", default=4, minimum=1, maximum=64)
123
+
124
+ if prompt_confirm("Create the project?", default=True):
125
+ print(f"Creating {name} with {workers} workers")
126
+ ```
127
+
128
+ Every question shows its default in brackets, and Enter takes it. An answer that is not accepted is
129
+ explained in one line and the question comes again.
130
+
131
+ ### Level 2: Choices and paths
132
+
133
+ ```python
134
+ from simplibs.cli import prompt_choice, prompt_multi_choice, prompt_path
135
+
136
+ fmt = prompt_choice("Format", {"json": "Machine-readable", "text": "For people"}, default="text")
137
+ extras = prompt_multi_choice("Extras", {"tests": "Test folder", "docs": "Docs folder"}, allow_empty=True)
138
+ target = prompt_path("Output folder", kind="dir", create_if_missing=True)
139
+ ```
140
+
141
+ ```text
142
+ json - Machine-readable
143
+ text - For people
144
+
145
+ Format [text]: xml
146
+ 'xml' is not a valid option. Please try again.
147
+ Format [text]:
148
+ ```
149
+
150
+ `fmt` is always one of the keys, `extras` is a tuple (possibly empty), and `target` is a normalized
151
+ `Path` of a folder that exists — created on the spot when it did not.
152
+
153
+ ### Level 3: The command line first, the question second
154
+
155
+ ```python
156
+ import argparse
157
+ from pathlib import Path
158
+ from simplibs.cli import prompt_path, require_interactive, resolve_or_prompt
159
+
160
+ parser = argparse.ArgumentParser()
161
+ parser.add_argument("--folder")
162
+ args = parser.parse_args()
163
+
164
+ if args.folder is None:
165
+ require_interactive("--folder is missing") # in CI: a card that names the option, not a hang
166
+
167
+ folder = resolve_or_prompt(
168
+ args.folder,
169
+ is_valid=lambda value: Path(value).is_dir(),
170
+ prompt=lambda: prompt_path("Folder", kind="dir", must_exist=True),
171
+ invalid_message=f"'{args.folder}' is not a folder.",
172
+ )
173
+ ```
174
+
175
+ A good `--folder` is used as it is. A bad one is mentioned and replaced by a question. A missing one
176
+ is asked for — or, without a terminal, refused with a card.
177
+
178
+ ### Level 4: A whole program — verbosity, a batch and an exit code
179
+
180
+ ```python
181
+ import argparse
182
+ import logging
183
+ from simplibs.cli import report_failures, resolve_verbosity
184
+
185
+ def process(name: str) -> bool:
186
+ return name.endswith(".txt") # stands in for the real work
187
+
188
+ parser = argparse.ArgumentParser()
189
+ parser.add_argument("-q", "--quiet", action="store_true")
190
+ parser.add_argument("-v", "--verbose", action="count", default=0)
191
+ parser.add_argument("files", nargs="*")
192
+ args = parser.parse_args()
193
+
194
+ logging.basicConfig(level=resolve_verbosity(quiet=args.quiet, verbose=args.verbose))
195
+
196
+ results = [(name, process(name)) for name in args.files] # (item, succeeded) pairs
197
+ ok = report_failures(results, describe=lambda name: f"{name} could not be processed")
198
+ raise SystemExit(0 if ok else 1)
199
+ ```
200
+
201
+ `-q` shows only errors, `-v` adds progress, `-vv` shows everything. The batch runs to the end, the
202
+ failures are listed once, and the exit code follows.
203
+
204
+ ---
205
+
206
+ ## 🛠️ Architecture & Package Structure
207
+
208
+ ```
209
+ src/simplibs/cli/
210
+ ├── interaction/ ◄── Is there somebody to ask? (is_interactive, require_interactive)
211
+ ├── reporting/ ◄── Output that is not a question (print_choices, report_failures)
212
+ ├── verbosity/ ◄── Flags to a logging level (resolve_verbosity)
213
+ └── prompts/ ◄── The questions (seven prompts and resolve_or_prompt, with the helpers
214
+ and the diagnostic raisers behind them)
215
+ ```
216
+
217
+ The root of the package exports every function, so one import line is all a program needs. The
218
+ folders are plain packages for the code's own organization; nothing has to be imported from them.
219
+
220
+ ```python
221
+ from simplibs.cli import prompt_text, prompt_path, resolve_or_prompt # the questions
222
+ from simplibs.cli import is_interactive, report_failures, resolve_verbosity
223
+ ```
224
+
225
+ ---
226
+
227
+ ## 🙋 Prompts — Ask a Person
228
+
229
+ Seven prompts that ask at a terminal and ask again until the answer is acceptable, plus
230
+ `resolve_or_prompt`, which puts one of them behind a value from the command line. They share four
231
+ rules: an empty answer takes the default (or is refused), a refused answer is explained in one line
232
+ and asked again, a wrong call raises a `ParamError` before the first question, and an input that has
233
+ ended raises a `StateError`.
234
+
235
+ ```python
236
+ name = prompt_text("Project name", default="demo")
237
+ port = prompt_int("Port", minimum=1024, maximum=65535)
238
+ ok = prompt_confirm("Overwrite?", default=False)
239
+ ```
240
+
241
+ ➡️ [README_CLI_PROMPTS](https://github.com/simplibs/simplibs-cli/blob/main/docs/README_CLI_PROMPTS.md)
242
+
243
+ ---
244
+
245
+ ## 🚦 Interaction — Is Anybody There?
246
+
247
+ Two functions that answer one question before anything is asked: can this program talk to a person?
248
+ A person needs a way to answer and a way to read the question, so both standard input and standard
249
+ output must be terminals. `is_interactive()` returns the answer; `require_interactive()` turns it
250
+ into a card that says why an answer was needed.
251
+
252
+ ```python
253
+ if not is_interactive():
254
+ print("Pass --name, there is nobody to ask.")
255
+ ```
256
+
257
+ ➡️ [README_CLI_INTERACTION](https://github.com/simplibs/simplibs-cli/blob/main/docs/README_CLI_INTERACTION.md)
258
+
259
+ ---
260
+
261
+ ## 📋 Reporting — Say Something That Is Not a Question
262
+
263
+ Two functions for output that is not a question: `print_choices` prints an aligned
264
+ `key - description` list (the prompts use it too, so a list looks the same everywhere), and
265
+ `report_failures` prints the failed items of a batch and returns whether everything succeeded — ready
266
+ for `SystemExit`.
267
+
268
+ ```python
269
+ print_choices({"json": "Machine-readable", "text": "For people"}, title="Formats:")
270
+ # Formats:
271
+ # json - Machine-readable
272
+ # text - For people
273
+ ```
274
+
275
+ ➡️ [README_CLI_REPORTING](https://github.com/simplibs/simplibs-cli/blob/main/docs/README_CLI_REPORTING.md)
276
+
277
+ ---
278
+
279
+ ## 🔊 Verbosity — Flags to a Level
280
+
281
+ One function that turns `--quiet` and a counted `-v` / `-vv` into one `logging` level. `quiet` wins
282
+ over `verbose`, and a single `-v` never makes the output quieter than the default. It only returns
283
+ the level; the program's own logging applies it.
284
+
285
+ | Flags | Level |
286
+ |----------------------|-----------------------------|
287
+ | none | `WARNING` (the default) |
288
+ | `-v` | `INFO` |
289
+ | `-vv` | `DEBUG` |
290
+ | `-q` (with or without `-v`) | `ERROR` |
291
+
292
+ ➡️ [README_CLI_VERBOSITY](https://github.com/simplibs/simplibs-cli/blob/main/docs/README_CLI_VERBOSITY.md)
293
+
294
+ ---
295
+
296
+ ## 📖 Quick Reference
297
+
298
+ ### 🙋 `prompts/` — Ask a Person
299
+
300
+ | Function | Description / Parameters |
301
+ |-----------------------|-------------------------------------------------------------------------------------------------------|
302
+ | `prompt_text` | A line of text, optionally judged by a rule (`message`, `default`, `allow_empty=False`, `rule`, `invalid_message`). |
303
+ | `prompt_int` | A whole number, optionally within limits (`message`, `default`, `minimum`, `maximum`). |
304
+ | `prompt_secret` | A line typed without being shown (`message`, `allow_empty=False`). |
305
+ | `prompt_confirm` | A yes/no question; always returns a `bool` (`message`, `default`). |
306
+ | `prompt_choice` | One key from a described set (`message`, `options`, `default`). |
307
+ | `prompt_multi_choice` | One or more comma-separated keys, in the order typed, as a tuple (`message`, `options`, `default=()`, `allow_empty=False`). |
308
+ | `prompt_path` | A normalized `Path` of a given kind, existing or created (`message`, `kind="any"`, `must_exist=False`, `create_if_missing=False`, `default`, `rule`, `invalid_message`). |
309
+ | `resolve_or_prompt` | Uses a value from the command line, asks only when it is missing or refused (`value`, `is_valid`, `prompt`, `invalid_message`). |
310
+
311
+ ➡️ [README_CLI_PROMPTS](https://github.com/simplibs/simplibs-cli/blob/main/docs/README_CLI_PROMPTS.md)
312
+
313
+ ---
314
+
315
+ ### 🚦 `interaction/` — Is Anybody There?
316
+
317
+ | Function | Description / Parameters |
318
+ |-----------------------|--------------------------------------------------------------------------------|
319
+ | `is_interactive` | `True` when both standard input and standard output are terminals. |
320
+ | `require_interactive` | Raises `StateError` when there is nobody to ask (`reason=None`). |
321
+
322
+ ➡️ [README_CLI_INTERACTION](https://github.com/simplibs/simplibs-cli/blob/main/docs/README_CLI_INTERACTION.md)
323
+
324
+ ---
325
+
326
+ ### 📋 `reporting/` — Say Something That Is Not a Question
327
+
328
+ | Function | Description / Parameters |
329
+ |-------------------|------------------------------------------------------------------------------------------|
330
+ | `print_choices` | Prints an aligned `key - description` list (`options`, `title=None`). |
331
+ | `report_failures` | Prints the failed items of a batch, returns whether all succeeded (`results`, `describe`, `title`). |
332
+
333
+ ➡️ [README_CLI_REPORTING](https://github.com/simplibs/simplibs-cli/blob/main/docs/README_CLI_REPORTING.md)
334
+
335
+ ---
336
+
337
+ ### 🔊 `verbosity/` — Flags to a Level
338
+
339
+ | Function | Description / Parameters |
340
+ |---------------------|-------------------------------------------------------------------------------------------|
341
+ | `resolve_verbosity` | Resolves `--quiet` and `-v` / `-vv` into a logging level (`quiet=False`, `verbose=0`, `default_level=logging.WARNING`). |
342
+
343
+ ➡️ [README_CLI_VERBOSITY](https://github.com/simplibs/simplibs-cli/blob/main/docs/README_CLI_VERBOSITY.md)
344
+
345
+ ---
346
+
347
+ ## 🔌 Pipes, CI and Ctrl+C
348
+
349
+ How the library behaves where there is no person at the keyboard — or where the person gives up:
350
+
351
+ * **Piped answers work.** `echo y | tool` drives the prompts, and so do tests. The prompts do not check
352
+ for a terminal themselves; whether a *missing* answer is an error is the program's decision, made
353
+ with `require_interactive()`.
354
+ * **A dead input is a card.** When the input has ended (`tool < /dev/null`, a CI job), the prompt
355
+ raises `CLI_NO_ANSWER_AVAILABLE` — what was asked, and what to do — instead of a bare `EOFError`.
356
+ * **Secrets are not echoed, but they can be piped.** Without a terminal `prompt_secret` falls back to
357
+ standard input with a warning, unhidden. A program that must never read a secret from a pipe checks
358
+ `is_interactive()` first.
359
+ * **`Ctrl+C` is never swallowed.** `KeyboardInterrupt` goes up unchanged — it is the user saying
360
+ "stop", and a prompt that caught it would be a trap.
361
+ * **Everything goes to standard output.** Questions, warnings, lists and reports share one stream, so
362
+ colors are decided once and never leak into a redirected file.
363
+
364
+ ---
365
+
366
+ ## ⚠️ Exceptions
367
+
368
+ Every error is a structured [`simplibs.exception`](https://pypi.org/project/simplibs-exception/) card,
369
+ and every error name starts with `CLI_`. A wrong call is a `ParamError`, raised before the first
370
+ question; a missing terminal or a dead input is a `StateError`. Each error is documented next to the
371
+ function that raises it.
372
+
373
+ | Error name | Card | Wraps | Raised when |
374
+ |------------------------------|--------------|----------------|--------------------------------------------------------------------|
375
+ | `CLI_OPTIONS_EMPTY` | `ParamError` | `ValueError` | `prompt_choice` / `prompt_multi_choice` got no options. |
376
+ | `CLI_DEFAULT_NOT_IN_OPTIONS` | `ParamError` | `ValueError` | A default (or a key of it) is not one of the options. |
377
+ | `CLI_RANGE_EMPTY` | `ParamError` | `ValueError` | `prompt_int` got a `minimum` greater than the `maximum`. |
378
+ | `CLI_CREATE_NEEDS_KIND` | `ParamError` | `ValueError` | `prompt_path` got `create_if_missing=True` with `kind="any"`. |
379
+ | `CLI_NOT_INTERACTIVE` | `StateError` | `RuntimeError` | `require_interactive` found input or output that is not a terminal. |
380
+ | `CLI_NO_ANSWER_AVAILABLE` | `StateError` | `EOFError` | The input ended before a prompt got its answer. |
381
+
382
+ A mistake in the *type* of an argument is refused by `@validate_call` (from
383
+ [`simplibs-validate`](https://pypi.org/project/simplibs-validate/)) before the function runs.
384
+
385
+ ---
386
+
387
+ ## 🔗 Related libraries
388
+
389
+ * **[`simplibs-exception`](https://pypi.org/project/simplibs-exception/)** — the structured cards behind every error here.
390
+ * **[`simplibs-validate`](https://pypi.org/project/simplibs-validate/)** — `@validate_call`, which checks the arguments of every public function.
391
+ * **[`simplibs-rules`](https://pypi.org/project/simplibs-rules/)** — the `Rule` objects that `prompt_text` and `prompt_path` accept, and whose `describe()` becomes the refusal message.
392
+ * **[`simplibs-path`](https://pypi.org/project/simplibs-path/)** — normalizing and creating the path in `prompt_path`, and the path rules to hand it.
393
+ * **[`simplibs-style`](https://pypi.org/project/simplibs-style/)** — the colors of warnings, headings and keys, plain outside a terminal.
394
+
395
+ ---
396
+
397
+ ## ☯️ About simplibs
398
+
399
+ All libraries in the **simplibs** (Simple Libraries) ecosystem share a common
400
+ engineering philosophy:
401
+
402
+ * **Dyslexia-friendly:**
403
+ We actively minimize cognitive load. Code is atomized into small, self-contained units,
404
+ files are named directly after the logical task they perform, and explanations describe
405
+ *why* something is designed, not just *what* it is.
406
+ * **Programmer's Zen:**
407
+ Nothing should be missing, and nothing should be superfluous. We value clean execution
408
+ paths and robust, understandable code architectures over rushed, messy feature sets.
409
+ * **Defensive Style:**
410
+ We actively anticipate edge cases and failure modes so that only safe operational paths
411
+ remain. Our code is built to degrade gracefully rather than crash unexpectedly.
412
+ * **Minimalism:**
413
+ Find the most direct path to the goal in as few operational steps as possible without
414
+ taking shortcuts on safety, readability, or completeness.
415
+ * **Code as Craft:**
416
+ Code should be pleasant to look at, readable at a glance, and evoke structural harmony.
417
+ We treat software engineering as a precision trade.
418
+
419
+ ---
420
+
421
+ ### 🤝 Contributing & Community
422
+
423
+ This is an **open-source project** built with love and care. We strongly believe in
424
+ community collaboration and welcome any feedback, bug reports, or feature ideas!
425
+
426
+ * **Want to contribute?** Feel free to open an Issue or submit a Pull Request.
427
+ * **Want to get in touch?** If you'd like to discuss the project further, collaborate,
428
+ or just say hello, feel free to open a GitHub Issue or start a Discussion.
429
+
430
+ ---
431
+
432
+ ### 📝 License
433
+
434
+ This library is released under the **MIT License**. Build great things!
435
+
436
+ ---