py-sparcli 0.3.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 (93) hide show
  1. py_sparcli-0.3.0/.editorconfig +25 -0
  2. py_sparcli-0.3.0/.gitattributes +29 -0
  3. py_sparcli-0.3.0/.gitignore +222 -0
  4. py_sparcli-0.3.0/.pre-commit-config.yaml +32 -0
  5. py_sparcli-0.3.0/CHANGELOG.md +57 -0
  6. py_sparcli-0.3.0/CLAUDE.md +107 -0
  7. py_sparcli-0.3.0/CONTRIBUTING.md +52 -0
  8. py_sparcli-0.3.0/LICENSE +21 -0
  9. py_sparcli-0.3.0/PKG-INFO +257 -0
  10. py_sparcli-0.3.0/README.md +231 -0
  11. py_sparcli-0.3.0/TODO.md +7 -0
  12. py_sparcli-0.3.0/docs/DEVELOPMENT.md +121 -0
  13. py_sparcli-0.3.0/examples/output_dynamic.py +101 -0
  14. py_sparcli-0.3.0/examples/output_gallery.py +264 -0
  15. py_sparcli-0.3.0/examples/output_readme.py +223 -0
  16. py_sparcli-0.3.0/examples/prompt_readme.py +138 -0
  17. py_sparcli-0.3.0/examples/prompts.py +161 -0
  18. py_sparcli-0.3.0/images/screenshot-1.png +0 -0
  19. py_sparcli-0.3.0/images/screenshot-2.png +0 -0
  20. py_sparcli-0.3.0/pyproject.toml +76 -0
  21. py_sparcli-0.3.0/sparcli/__init__.py +184 -0
  22. py_sparcli-0.3.0/sparcli/core/__init__.py +92 -0
  23. py_sparcli-0.3.0/sparcli/core/border.py +160 -0
  24. py_sparcli-0.3.0/sparcli/core/color.py +306 -0
  25. py_sparcli-0.3.0/sparcli/core/cursor.py +59 -0
  26. py_sparcli-0.3.0/sparcli/core/geometry.py +141 -0
  27. py_sparcli-0.3.0/sparcli/core/markup.py +215 -0
  28. py_sparcli-0.3.0/sparcli/core/render.py +245 -0
  29. py_sparcli-0.3.0/sparcli/core/style.py +128 -0
  30. py_sparcli-0.3.0/sparcli/core/terminal.py +127 -0
  31. py_sparcli-0.3.0/sparcli/core/text.py +143 -0
  32. py_sparcli-0.3.0/sparcli/core/theme.py +117 -0
  33. py_sparcli-0.3.0/sparcli/core/width.py +244 -0
  34. py_sparcli-0.3.0/sparcli/errors.py +38 -0
  35. py_sparcli-0.3.0/sparcli/input/__init__.py +42 -0
  36. py_sparcli-0.3.0/sparcli/input/calc.py +214 -0
  37. py_sparcli-0.3.0/sparcli/input/confirm.py +188 -0
  38. py_sparcli-0.3.0/sparcli/input/datepicker.py +298 -0
  39. py_sparcli-0.3.0/sparcli/input/editor.py +189 -0
  40. py_sparcli-0.3.0/sparcli/input/event.py +551 -0
  41. py_sparcli-0.3.0/sparcli/input/field.py +90 -0
  42. py_sparcli-0.3.0/sparcli/input/fuzzy.py +401 -0
  43. py_sparcli-0.3.0/sparcli/input/guard.py +103 -0
  44. py_sparcli-0.3.0/sparcli/input/history.py +160 -0
  45. py_sparcli-0.3.0/sparcli/input/line_edit.py +229 -0
  46. py_sparcli-0.3.0/sparcli/input/number.py +232 -0
  47. py_sparcli-0.3.0/sparcli/input/outcome.py +107 -0
  48. py_sparcli-0.3.0/sparcli/input/password.py +233 -0
  49. py_sparcli-0.3.0/sparcli/input/prompt.py +134 -0
  50. py_sparcli-0.3.0/sparcli/input/select.py +336 -0
  51. py_sparcli-0.3.0/sparcli/input/shortcut.py +123 -0
  52. py_sparcli-0.3.0/sparcli/input/text.py +564 -0
  53. py_sparcli-0.3.0/sparcli/input/textarea.py +242 -0
  54. py_sparcli-0.3.0/sparcli/input/validate.py +67 -0
  55. py_sparcli-0.3.0/sparcli/output/__init__.py +56 -0
  56. py_sparcli-0.3.0/sparcli/output/alert.py +107 -0
  57. py_sparcli-0.3.0/sparcli/output/badge.py +82 -0
  58. py_sparcli-0.3.0/sparcli/output/box.py +196 -0
  59. py_sparcli-0.3.0/sparcli/output/columns.py +157 -0
  60. py_sparcli-0.3.0/sparcli/output/compose.py +111 -0
  61. py_sparcli-0.3.0/sparcli/output/diff.py +229 -0
  62. py_sparcli-0.3.0/sparcli/output/kv.py +175 -0
  63. py_sparcli-0.3.0/sparcli/output/layout.py +77 -0
  64. py_sparcli-0.3.0/sparcli/output/list.py +274 -0
  65. py_sparcli-0.3.0/sparcli/output/live.py +129 -0
  66. py_sparcli-0.3.0/sparcli/output/multiprogress.py +73 -0
  67. py_sparcli-0.3.0/sparcli/output/pager.py +83 -0
  68. py_sparcli-0.3.0/sparcli/output/panel.py +125 -0
  69. py_sparcli-0.3.0/sparcli/output/progress.py +233 -0
  70. py_sparcli-0.3.0/sparcli/output/rule.py +134 -0
  71. py_sparcli-0.3.0/sparcli/output/spinner.py +138 -0
  72. py_sparcli-0.3.0/sparcli/output/table/__init__.py +17 -0
  73. py_sparcli-0.3.0/sparcli/output/table/plan.py +198 -0
  74. py_sparcli-0.3.0/sparcli/output/table/render.py +302 -0
  75. py_sparcli-0.3.0/sparcli/output/table/table.py +351 -0
  76. py_sparcli-0.3.0/sparcli/output/tree.py +157 -0
  77. py_sparcli-0.3.0/sparcli/py.typed +0 -0
  78. py_sparcli-0.3.0/tests/test_confirm.py +94 -0
  79. py_sparcli-0.3.0/tests/test_core.py +248 -0
  80. py_sparcli-0.3.0/tests/test_cursor.py +107 -0
  81. py_sparcli-0.3.0/tests/test_datepicker.py +178 -0
  82. py_sparcli-0.3.0/tests/test_fuzzy.py +152 -0
  83. py_sparcli-0.3.0/tests/test_input_infra.py +372 -0
  84. py_sparcli-0.3.0/tests/test_number.py +186 -0
  85. py_sparcli-0.3.0/tests/test_output_basic.py +222 -0
  86. py_sparcli-0.3.0/tests/test_output_dynamic.py +269 -0
  87. py_sparcli-0.3.0/tests/test_output_structured.py +202 -0
  88. py_sparcli-0.3.0/tests/test_password.py +109 -0
  89. py_sparcli-0.3.0/tests/test_public_api.py +23 -0
  90. py_sparcli-0.3.0/tests/test_select.py +154 -0
  91. py_sparcli-0.3.0/tests/test_table.py +295 -0
  92. py_sparcli-0.3.0/tests/test_text_input.py +181 -0
  93. py_sparcli-0.3.0/tests/test_textarea.py +110 -0
@@ -0,0 +1,25 @@
1
+ # EditorConfig: https://editorconfig.org
2
+ root = true
3
+
4
+ [*]
5
+ charset = utf-8
6
+ end_of_line = lf
7
+ insert_final_newline = true
8
+ trim_trailing_whitespace = true
9
+ indent_style = space
10
+ indent_size = 4
11
+ max_line_length = 80
12
+
13
+ [*.py]
14
+ indent_size = 4
15
+
16
+ [*.{md,markdown}]
17
+ indent_size = 2
18
+ # Keep trailing spaces off; hard line breaks use a backslash, not two spaces.
19
+ trim_trailing_whitespace = true
20
+
21
+ [*.{toml,yaml,yml,json}]
22
+ indent_size = 2
23
+
24
+ [Makefile]
25
+ indent_style = tab
@@ -0,0 +1,29 @@
1
+ # Normalize line endings: LF in the repository, auto on checkout.
2
+ * text=auto eol=lf
3
+
4
+ # Explicitly text.
5
+ *.py text
6
+ *.pyi text
7
+ *.md text
8
+ *.toml text
9
+ *.cfg text
10
+ *.txt text
11
+ *.yml text
12
+ *.yaml text
13
+
14
+ # Binary assets (add screenshots/GIFs here as they are introduced).
15
+ *.png binary
16
+ *.gif binary
17
+ *.jpg binary
18
+ *.svg text
19
+
20
+ # Keep source-distribution / `git archive` tarballs lean: development-only
21
+ # files are excluded from exports.
22
+ .editorconfig export-ignore
23
+ .gitattributes export-ignore
24
+ .gitignore export-ignore
25
+ .github/ export-ignore
26
+ CLAUDE.md export-ignore
27
+ TODO.md export-ignore
28
+ tests/ export-ignore
29
+ examples/ export-ignore
@@ -0,0 +1,222 @@
1
+ # ============================================================
2
+ # Python
3
+ # ============================================================
4
+
5
+ # macOS
6
+ .DS_Store
7
+ **/.DS_Store
8
+ ._*
9
+ .AppleDouble
10
+ .LSOverride
11
+ .Spotlight-V100
12
+ .Trashes
13
+ .fseventsd
14
+ .DocumentRevisions-V100
15
+ Icon
16
+
17
+ # Windows
18
+ Thumbs.db
19
+ ehthumbs.db
20
+ Desktop.ini
21
+ $RECYCLE.BIN/
22
+
23
+ # Linux / editor backups
24
+ *~
25
+ .netrwhist
26
+
27
+ # Editors / IDEs
28
+ .vscode
29
+ .idea
30
+ *.code-workspace
31
+ *.sw?
32
+ .history
33
+
34
+ # Environment files
35
+ .env
36
+ # ...but keep committable example/template files:
37
+ !.env.example
38
+ !.env.template
39
+
40
+ # Logs
41
+ *.log
42
+
43
+ # Claude Code
44
+ .claude
45
+
46
+ # Byte-compiled / optimized / DLL files
47
+ __pycache__/
48
+ *.py[codz]
49
+ *$py.class
50
+
51
+ # C extensions
52
+ *.so
53
+
54
+ # Distribution / packaging
55
+ .Python
56
+ build/
57
+ develop-eggs/
58
+ dist/
59
+ downloads/
60
+ eggs/
61
+ .eggs/
62
+ lib/
63
+ lib64/
64
+ parts/
65
+ sdist/
66
+ var/
67
+ wheels/
68
+ share/python-wheels/
69
+ *.egg-info/
70
+ src/*.egg-info/
71
+ .installed.cfg
72
+ *.egg
73
+ MANIFEST
74
+
75
+ # PyInstaller
76
+ *.manifest
77
+ *.spec
78
+
79
+ # Installer logs
80
+ pip-log.txt
81
+ pip-delete-this-directory.txt
82
+
83
+ # Unit test / coverage reports
84
+ htmlcov/
85
+ .tox/
86
+ .nox/
87
+ .coverage
88
+ .coverage.*
89
+ .cache
90
+ .benchmarks/
91
+ nosetests.xml
92
+ coverage.xml
93
+ *.cover
94
+ *.lcov
95
+ .hypothesis/
96
+ .pytest_cache/
97
+ cover/
98
+
99
+ # Translations
100
+ *.mo
101
+ *.pot
102
+
103
+ # Django / Flask / Scrapy
104
+ local_settings.py
105
+ db.sqlite3
106
+ db.sqlite3-journal
107
+ instance/
108
+ .webassets-cache
109
+ .scrapy
110
+
111
+ # Sphinx / mkdocs documentation
112
+ docs/_build/
113
+ docs/build/
114
+ /site
115
+
116
+ # PyBuilder
117
+ .pybuilder/
118
+ target/
119
+
120
+ # Jupyter Notebook
121
+ .ipynb_checkpoints
122
+
123
+ # IPython
124
+ profile_default/
125
+ ipython_config.py
126
+
127
+ # pyenv
128
+ .python-version
129
+
130
+ # pdm
131
+ .pdm.toml
132
+ .pdm-python
133
+ .pdm-build/
134
+
135
+ # PEP 582
136
+ __pypackages__/
137
+
138
+ # Celery / message brokers
139
+ celerybeat-schedule*
140
+ celerybeat.pid
141
+ *.rdb
142
+ *.aof
143
+ *.pid
144
+ mnesia/
145
+ rabbitmq/
146
+ rabbitmq-data/
147
+ activemq-data/
148
+
149
+ # SageMath
150
+ *.sage.py
151
+
152
+ # pixi
153
+ .pixi/*
154
+ !.pixi/config.toml
155
+
156
+ # Environments
157
+ .envrc
158
+ .venv
159
+ .venv/
160
+ # .venv may be a symlink -> .venv.nosync (iCloud-excluded real dir)
161
+ .venv.nosync/
162
+ env/
163
+ venv/
164
+ ENV/
165
+ env.bak/
166
+ venv.bak/
167
+
168
+ # Spyder / Rope project settings
169
+ .spyderproject
170
+ .spyproject
171
+ .ropeproject
172
+
173
+ # mypy
174
+ .mypy_cache*
175
+ .dmypy.json
176
+ dmypy.json
177
+
178
+ # Pyre / pytype
179
+ .pyre/
180
+ .pytype/
181
+
182
+ # Cython debug symbols
183
+ cython_debug/
184
+
185
+ # Ruff
186
+ .ruff_cache/
187
+
188
+ # isort
189
+ *.isorted
190
+
191
+ # PyPI configuration file
192
+ .pypirc
193
+
194
+ # Cursor
195
+ .cursorignore
196
+ .cursorindexingignore
197
+
198
+ # Marimo
199
+ marimo/_static/
200
+ marimo/_lsp/
201
+ __marimo__/
202
+
203
+ # Streamlit
204
+ .streamlit/secrets.toml
205
+
206
+ # Abstra
207
+ .abstra/
208
+
209
+ # VS Code "Code Runner" extension
210
+ tempCodeRunnerFile.py
211
+
212
+ # Temp dirs
213
+ temp/
214
+ _temp/
215
+ tmp/
216
+ temp-*/
217
+ tmp-*/
218
+
219
+ # ------------------------------------------------------------
220
+ # Project-specific
221
+ # ------------------------------------------------------------
222
+ .pixi
@@ -0,0 +1,32 @@
1
+ # Pre-commit hooks: https://pre-commit.com
2
+ # Install once with `pre-commit install`; run on demand with
3
+ # `pre-commit run --all-files`. Optional but recommended for contributors,
4
+ # since the project has no CI.
5
+ repos:
6
+ - repo: https://github.com/pre-commit/pre-commit-hooks
7
+ rev: v5.0.0
8
+ hooks:
9
+ - id: check-toml
10
+ - id: check-yaml
11
+ - id: check-merge-conflict
12
+ - id: end-of-file-fixer
13
+ - id: trailing-whitespace
14
+ args: [--markdown-linebreak-ext=md]
15
+ - id: mixed-line-ending
16
+ args: [--fix=lf]
17
+
18
+ - repo: https://github.com/astral-sh/ruff-pre-commit
19
+ rev: v0.15.21
20
+ hooks:
21
+ - id: ruff-check
22
+ args: [--fix]
23
+ - id: ruff-format
24
+
25
+ - repo: local
26
+ hooks:
27
+ - id: basedpyright
28
+ name: basedpyright (strict)
29
+ entry: basedpyright sparcli examples
30
+ language: system
31
+ types: [python]
32
+ pass_filenames: false
@@ -0,0 +1,57 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
+
7
+ ## [Unreleased]
8
+
9
+ ## [0.3.0] – 2026-07-11
10
+
11
+ ### Added
12
+
13
+ - `Style.remove_modifier` clears one or more attributes from a layered style (the counterpart to `add_modifier`).
14
+ - `TextInput` and `PasswordInput` gained keyword-only constructor options mirroring their fluent setters, so they configure like every other widget.
15
+ - `OutcomeKind` is re-exported from the flat `sparcli` namespace.
16
+
17
+ ### Changed
18
+
19
+ - Hide the terminal hardware cursor during in-place redraws (spinner, progress, multi-progress, live) and interactive prompts, and restore it reliably on finish, on prompt exit and at interpreter exit.
20
+
21
+ ### Fixed
22
+
23
+ - `DatePicker` navigation past the representable date range (before year 1 or after year 9999) no longer raises; it clamps to `date.min` / `date.max`.
24
+ - Inline markup no longer swallows a closed bracket that names no known style or attribute (such as `array[0]`); such text is emitted literally.
25
+ - `char_width` no longer raises on an empty or multi-character string.
26
+ - A negative `NumberInput.decimals` setting is clamped to zero instead of raising when the value is formatted.
27
+ - The raw-mode paths in `TerminalGuard` and the external-editor suspension now also catch `termios.error`, so a non-terminal stdin falls back to the documented no-op instead of raising (`termios.error` is not an `OSError`).
28
+ - `TextInput`'s Ctrl-G external editor now runs with raw mode suspended (cooked), matching `Textarea` and the Rust original; previously the editor launched while the terminal was still in raw mode.
29
+ - `KeyValue` rows are no longer padded with trailing spaces when there is no right margin; shorter rows stay ragged, matching the Rust original.
30
+ - Word wrapping splits on runs of whitespace (tabs, repeated spaces), matching Rust's `split_whitespace`, instead of on a single space only.
31
+ - History files are split into entries on newlines only (like Rust's `str::lines`), so an entry containing an exotic line separator is no longer split in two.
32
+
33
+ ### Security
34
+
35
+ - Control characters (C0, DEL and C1, except tab) are stripped from span content and OSC-8 link URLs before they reach the terminal, so untrusted text can no longer inject escape sequences or terminate a hyperlink early.
36
+ - History files are written atomically (temp file plus rename), so a crash or a concurrent writer can no longer truncate the file.
37
+
38
+ ### Documentation
39
+
40
+ - Align the minor version with the Rust `sparcli` port (both at 0.3.x); the two ports now track the same minor number.
41
+ - Add `CONTRIBUTING.md`, `SECURITY.md`, `CODE_OF_CONDUCT.md`, `TODO.md`, README badges and editor/VCS tooling configs (`.editorconfig`, `.gitattributes`); migrate packaging metadata to an SPDX license expression and expand the project URLs for PyPI.
42
+
43
+ ### Notes
44
+
45
+ - `DatePicker`'s initial "today" uses the local date here, while the Rust original uses UTC (it stays dependency-free). This one-point divergence is intentional; near midnight the default day can differ by one between the two ports.
46
+
47
+ ## [0.1.0] – 2026-07-10
48
+
49
+ ### Added
50
+
51
+ - Initial release: idiomatic Python port of the Rust `sparcli` library.
52
+ - Styled output widgets: `Panel`, `Table`, `List`, `Rule`, `Alert`, `Badge`, `Columns`, `Tree`, `KeyValue`, `Diff`, `Spinner`, `ProgressBar`, `MultiProgress`, `Live`, `Pager`.
53
+ - Interactive input widgets: `TextInput`, `PasswordInput`, `NumberInput`, `Confirm`, `Select`, `FuzzySelect`, `DatePicker`, `Textarea`.
54
+ - Unified `Theme`, inline `markup`, width utilities, terminal capability detection, validators, shortcuts and command history.
55
+ - Dependency-free (standard library only).
56
+
57
+ [Unreleased]: https://github.com/cgroening/py-sparcli/compare/v0.3.0...HEAD [0.3.0]: https://github.com/cgroening/py-sparcli/compare/v0.1.0...v0.3.0 [0.1.0]: https://github.com/cgroening/py-sparcli/releases/tag/v0.1.0
@@ -0,0 +1,107 @@
1
+ # CLAUDE.md – sparcli (Python)
2
+
3
+ Requirements for all future sessions in this project. On conflict, this file takes precedence over general conventions, but not over explicit user instructions. The global style guide (`~/.claude/CLAUDE.md` → §12 Python) applies additionally; this file refines and overrides it wherever it is more specific.
4
+
5
+ ## What is this?
6
+
7
+ `sparcli` is a **lightweight, cross-platform** toolkit (macOS, Windows, Linux) for **styled CLI output** and **interactive single-input widgets** – an **idiomatic, dependency-free Python reimplementation** of the Rust library `sparcli`. Guiding idea: small, for little CLI tools. No async, no rich/textual/prompt_toolkit, minimal footprint.
8
+
9
+ - **Foundation:** a custom ANSI renderer built directly on the standard library (no framework).
10
+ - **API feel:** ratatui-familiar vocabulary (`Style`, `Color`, `Span`, `Line`, `Text`, `Modifier`); **both** a keyword constructor **and** fluent builder methods per widget.
11
+ - **Scope:** output is complete; input covers single widgets only (no form/app/args). Fuzzy is only an inline select.
12
+ - **Rust original is the SSOT for behavior:** `/Users/cgroening/Developer/Rust/ libs/sparcli`. For behavior questions, check there and preserve parity.
13
+
14
+ ## Rust parity (mandatory)
15
+
16
+ - The Rust version lives at `/Users/cgroening/Developer/Rust/libs/sparcli`.
17
+ - **On every change, check whether the same change also has to be made in the Rust version** (behavior, API, docs, examples) to keep both in parity.
18
+ - When such a change might be needed, **ask the user** whether it should be carried over to the Rust version instead of deciding silently.
19
+ - **Version sync:** the Python and Rust ports keep their **minor version in sync** (at minimum the minor number matches). On a release, bump this port to the Rust port's minor. This is why the Python port jumped 0.1.0 -> 0.3.0 to meet Rust `sparcli` v0.3.
20
+ - **Known intentional divergence:** `DatePicker`'s initial "today" uses the **local** date here (`datetime.date.today`), while the Rust version uses **UTC** (it has no local-time API without a dependency, and both stay dependency-free). Near midnight the default day can differ by one between the two ports. Do not "fix" this into parity without user sign-off.
21
+
22
+ ## Distribution & import
23
+
24
+ - PyPI distribution name: **`py-sparcli`** (the name `sparcli` is taken).
25
+ - Import name: **`import sparcli`**.
26
+ - Build backend: hatchling, static version in `pyproject.toml`, `[tool.hatch.build.targets.wheel] packages = ["sparcli"]`, ships `py.typed`.
27
+
28
+ ## Architecture (keep the layers strictly separated)
29
+
30
+ - `core/` – foundation: style, color, text, render, geometry, border, theme, width, terminal, markup. No widget logic.
31
+ - `output/` – printable widgets, subclass `Renderable` (`render(max_width) -> Rendered`). Shared helpers: `layout`, `compose`, `box`, `live`.
32
+ - `input/` – interactive prompts over `EventSource` (DI) + the `prompt.run_prompt` loop + `line_edit.LineEditor` (SSOT for text editing) + `field` (rendering).
33
+ - **Dependency direction:** `output`/`input` → `core`. Never cyclic, never `core` → widget layer.
34
+ - **A single unified theme** in `core/theme.py` drives both input and output.
35
+ - The public API is re-exported flat (`from sparcli import ...`, 80 symbols) plus the namespaces `sparcli.width/terminal/markup/event/validate/shortcut`. Every `__init__.py` carries an explicit `__all__` list.
36
+
37
+ ## Dependencies
38
+
39
+ - **Zero runtime dependencies – standard library only.** Do not add a new runtime dependency without first checking with the user.
40
+ - Deliberate stdlib decisions (do not soften them):
41
+ - Display width via `unicodedata` (no `wcwidth`).
42
+ - DatePicker via `datetime.date`/`calendar` (no hand-rolled date class).
43
+ - Fuzzy scorer written by hand (no `nucleo`/`rapidfuzz`).
44
+ - Terminal raw mode / key parsing via `termios`/`tty` + `msvcrt`.
45
+ - `markup`, `fuzzy` and `pager` are always included (no feature flags/extras).
46
+
47
+ ## Style & tooling
48
+
49
+ - Target Python: **3.12+** (`requires-python = ">=3.12"`).
50
+ - **Strings: double quotes `"..."`** – a deliberate, user-mandated deviation from the style-guide default (§12.5). Enforced via `[tool.ruff.format] quote-style = "double"`. Exceptions that stay single-quoted: string literals nested inside a double-quoted f-string (`';'`, `' '`) and `repr` outputs in doctests.
51
+ - **Formatting/linting:** `ruff` (format + lint). 80-character lines. Imports isort-sorted (stdlib → third-party → local). `from __future__ import annotations` as the first line of every module.
52
+ - **Type checking:** `basedpyright` in **strict** mode must report 0 errors. Complete, modern type hints (`X | None`, builtin generics, PEP 695).
53
+ - No em dash; straight quotes/apostrophes; named constants instead of magic numbers/strings.
54
+
55
+ Commands (from the project root):
56
+
57
+ ```bash
58
+ ruff check --fix . && ruff format .
59
+ basedpyright sparcli examples
60
+ python -m pytest -q
61
+ ```
62
+
63
+ ## API conventions (idiomatic)
64
+
65
+ - Widgets: regular classes with `__slots__`, subclass `Renderable`, a keyword constructor (`*` keyword-only options) **and** fluent builder methods that mutate `self` and return it. `Rendered` is itself a `Renderable`.
66
+ - Value types (`Style`, `Color`, `Edges`, `Title`, `BorderChars`, `Theme`) as `@dataclass(frozen=True, slots=True)` or `enum.Enum`; enums instead of magic strings.
67
+ - Interfaces as `ABC` + `@abstractmethod` (`Renderable`, `EventSource`); DI via constructor/parameters.
68
+ - Docstrings in NumPy style with an RST-title module docstring; public items get `Parameters`/`Returns`/`Examples`, third-person present ("Returns…").
69
+ - Keep functions small (SRP), ≤ 3 parameters (otherwise bundle into an options/state object), guard clauses, at most 2 levels of nesting.
70
+
71
+ ## Error handling
72
+
73
+ - Exception hierarchy in `sparcli/errors.py`: `SparcliError` (base) with `TerminalError`, `NoTerminalError`, `ConfigError`. Wrap foreign errors sensibly.
74
+ - **No `assert` for validation** (stripped under `-O`). Do not return `None` for "no result"; use an empty collection, a special-case object, or an exception. (Parser optionals such as `Color.from_hex -> Color | None` are the documented exception.)
75
+ - Prompts return `Outcome[T]` (`submitted`/`cancelled`/`shortcut`), not a cancel exception.
76
+ - `TerminalGuard` (context manager, RAII) restores raw mode / bracketed paste on exit or error.
77
+ - **Logging:** only `logging.getLogger(__name__)` and only `warning`/`debug` where a result would otherwise be silently swallowed (terminal restore, history load/save, temp cleanup). Do not ship a logger/backend; nothing in render/event loops; real errors come back via `SparcliError`.
78
+ - `subprocess` only with an argument list (never `shell=True`/`os.system`); use `tempfile` safely; honor `NO_COLOR`/non-TTY.
79
+
80
+ ## Look
81
+
82
+ - Muted look, a single accent tone, `dim` for secondary text. Default border `Rounded`; truncate overflow with `…`.
83
+ - Glyphs in two tiers (Unicode + ASCII fallback), selected via the theme.
84
+ - Selection lists navigate cyclically; honor `NO_COLOR` and non-TTY (then plain, no ANSI codes, no OSC-8).
85
+
86
+ ## Tests – mandatory
87
+
88
+ - `pytest`; tests in `tests/`, grouped in `class TestXxx`; test names describe the expected behavior; prefer fakes over mocks.
89
+ - **Output:** render to `Rendered` and assert on `plain()` or ANSI-stripped content and styles (no TTY).
90
+ - **Input:** drive headlessly via `event.ScriptedSource` (scripted keys) + `run_with(source)` and assert on the `Outcome`.
91
+ - Doctests in `# Examples` count as tests (`--doctest-modules`) and must pass. `repr` expected-outputs keep single quotes.
92
+ - Test override: `SPARCLI_NO_TTY=1` forces non-TTY.
93
+ - **Run all tests after every change** and keep ruff + basedpyright clean.
94
+
95
+ ## Environment variables
96
+
97
+ `NO_COLOR` (disables color), `CLICOLOR_FORCE` (forces color), `COLORTERM` (truecolor/24bit → TrueColor), `SPARCLI_NO_TTY` (test override, non-TTY).
98
+
99
+ ## Docs / maintenance
100
+
101
+ - On changes, update README.md, `docs/DEVELOPMENT.md`, CHANGELOG.md, docstrings and tests together. The CHANGELOG follows Keep a Changelog + SemVer.
102
+ - Keep the `examples/` (five counterparts to the Rust version, including the two README generators `output_readme.py`/`prompt_readme.py` via `frame()`) current on API changes.
103
+ - Remove dead/commented-out code; fix the cause, not the symptom.
104
+
105
+ ## Git
106
+
107
+ - **No commits on your own.** At the end of a change, suggest a commit title (English, imperative, Conventional Commits).
@@ -0,0 +1,52 @@
1
+ # Contributing to sparcli
2
+
3
+ Thanks for your interest in improving `sparcli`. This is a small, dependency-free library, so the workflow is deliberately lightweight.
4
+
5
+ ## Getting started
6
+
7
+ The full build, test and lint workflow lives in [`docs/DEVELOPMENT.md`](docs/DEVELOPMENT.md). In short:
8
+
9
+ ```bash
10
+ pip install -e .
11
+ pip install ruff basedpyright pytest # or: uv sync --group dev
12
+
13
+ python -m pytest -q # tests + doctests
14
+ ruff check . # lint
15
+ ruff format --check . # formatting (drop --check to apply)
16
+ basedpyright sparcli examples # strict type checking
17
+ ```
18
+
19
+ All three checks must be clean before a change is ready. Optionally, install the pre-commit hooks to run linting, formatting and type checks automatically:
20
+
21
+ ```bash
22
+ pre-commit install # once
23
+ pre-commit run --all-files # on demand
24
+ ```
25
+
26
+ ## Ground rules
27
+
28
+ - **Target Python 3.12+.** The code uses PEP 695 generics and `type` statements.
29
+ - **Zero runtime dependencies.** Do not add a runtime dependency; the standard library only. Open an issue first if you think one is unavoidable.
30
+ - **Style gates** (enforced by `ruff` and `basedpyright`): 80-column lines, double-quoted strings, isort-ordered imports, complete modern type hints, and `basedpyright` strict with zero errors.
31
+ - **Layering.** `output/` and `input/` depend on `core/`, never the reverse, and `core/` depends on nothing internal.
32
+ - **Tests are mandatory.** Add headless tests under `tests/` following the patterns in `docs/DEVELOPMENT.md`; doctests in `# Examples` count as tests.
33
+ - **Docs travel with code.** Update `README.md`, `CHANGELOG.md`, docstrings and, where relevant, the `examples/` gallery in the same change.
34
+
35
+ The "Adding a widget" checklist in `docs/DEVELOPMENT.md` walks through a typical contribution end to end.
36
+
37
+ ## Rust parity
38
+
39
+ `sparcli` is an idiomatic port of the Rust library at a matching version (the two ports keep their **minor version in sync**). The Rust original is the source of truth for behavior. When a change affects behavior, API or docs, note in your PR whether the same change should be mirrored to the Rust port.
40
+
41
+ ## Commits and pull requests
42
+
43
+ - Use [Conventional Commits](https://www.conventionalcommits.org/) for commit titles (`feat:`, `fix:`, `docs:`, `refactor:`, `test:`, `chore:`), imperative mood, English.
44
+ - Add a `CHANGELOG.md` entry under `## [Unreleased]` (Keep a Changelog format).
45
+ - Keep pull requests focused; describe the behavior change and how you verified it.
46
+
47
+ ## Reporting bugs and security issues
48
+
49
+ - Regular bugs and feature requests: open a [GitHub issue](https://github.com/cgroening/py-sparcli/issues).
50
+ - Security vulnerabilities: **do not** open a public issue; follow [`SECURITY.md`](SECURITY.md).
51
+
52
+ By contributing you agree that your contributions are licensed under the project's [MIT license](LICENSE).
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Corvin Gröning
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 all
13
+ 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 THE
21
+ SOFTWARE.