termcore 0.2.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 (75) hide show
  1. termcore-0.2.0/.gitignore +222 -0
  2. termcore-0.2.0/CHANGELOG.md +95 -0
  3. termcore-0.2.0/CLAUDE.md +55 -0
  4. termcore-0.2.0/LICENSE +21 -0
  5. termcore-0.2.0/PKG-INFO +504 -0
  6. termcore-0.2.0/README.md +473 -0
  7. termcore-0.2.0/TODO.md +107 -0
  8. termcore-0.2.0/docs/CLEAN-UP.md +118 -0
  9. termcore-0.2.0/docs/DEVELOPMENT.md +155 -0
  10. termcore-0.2.0/pyproject.toml +199 -0
  11. termcore-0.2.0/termcore/__init__.py +6 -0
  12. termcore-0.2.0/termcore/cli/__init__.py +3 -0
  13. termcore-0.2.0/termcore/cli/output.py +70 -0
  14. termcore-0.2.0/termcore/io/__init__.py +7 -0
  15. termcore-0.2.0/termcore/io/app_state_storage.py +185 -0
  16. termcore-0.2.0/termcore/io/database.py +655 -0
  17. termcore-0.2.0/termcore/io/errors.py +71 -0
  18. termcore-0.2.0/termcore/io/file.py +263 -0
  19. termcore-0.2.0/termcore/io/textfile.py +92 -0
  20. termcore-0.2.0/termcore/py.typed +0 -0
  21. termcore-0.2.0/termcore/tui/__init__.py +9 -0
  22. termcore-0.2.0/termcore/tui/custom_bindings.py +551 -0
  23. termcore-0.2.0/termcore/tui/custom_widgets/__init__.py +1 -0
  24. termcore-0.2.0/termcore/tui/custom_widgets/custom_checkbox.py +41 -0
  25. termcore-0.2.0/termcore/tui/custom_widgets/custom_data_table.py +247 -0
  26. termcore-0.2.0/termcore/tui/custom_widgets/custom_selection.py +80 -0
  27. termcore-0.2.0/termcore/tui/custom_widgets/multiline_footer.py +240 -0
  28. termcore-0.2.0/termcore/tui/question_screen.py +107 -0
  29. termcore-0.2.0/termcore/tui/theme_loader.py +495 -0
  30. termcore-0.2.0/termcore/tui/themes/__init__.py +2 -0
  31. termcore-0.2.0/termcore/tui/themes/classic-black-saturated/style.css +57 -0
  32. termcore-0.2.0/termcore/tui/themes/classic-black-saturated/theme.py +22 -0
  33. termcore-0.2.0/termcore/tui/themes/classic-black-v1/style.css +57 -0
  34. termcore-0.2.0/termcore/tui/themes/classic-black-v1/theme.py +22 -0
  35. termcore-0.2.0/termcore/tui/themes/classic-black-v2/style.css +57 -0
  36. termcore-0.2.0/termcore/tui/themes/classic-black-v2/theme.py +24 -0
  37. termcore-0.2.0/termcore/tui/themes/classic-blue/theme.py +21 -0
  38. termcore-0.2.0/termcore/tui/themes/compact-gray/style.tcss +175 -0
  39. termcore-0.2.0/termcore/tui/themes/compact-gray/theme.py +22 -0
  40. termcore-0.2.0/termcore/tui/themes/mnml-black/style.css +57 -0
  41. termcore-0.2.0/termcore/tui/themes/mnml-black/theme.py +21 -0
  42. termcore-0.2.0/termcore/tui/themes/mnml-deepblack/style.css +57 -0
  43. termcore-0.2.0/termcore/tui/themes/mnml-deepblack/theme.py +21 -0
  44. termcore-0.2.0/termcore/tui/themes/pure-amber/theme.py +18 -0
  45. termcore-0.2.0/termcore/tui/themes/pure-black/theme.py +18 -0
  46. termcore-0.2.0/termcore/tui/themes/pure-blue/theme.py +18 -0
  47. termcore-0.2.0/termcore/tui/themes/pure-green/theme.py +18 -0
  48. termcore-0.2.0/termcore/tui/themes/pure-sweet16/theme.py +20 -0
  49. termcore-0.2.0/termcore/tui/themes/xplore-black/theme.py +18 -0
  50. termcore-0.2.0/termcore/tui/themes/xplore-blue/theme.py +18 -0
  51. termcore-0.2.0/termcore/tui/themes/xplore-blue-muted/theme.py +18 -0
  52. termcore-0.2.0/termcore/tui/themes/xplore-teal/theme.py +18 -0
  53. termcore-0.2.0/termcore/util/__init__.py +10 -0
  54. termcore-0.2.0/termcore/util/datetime.py +173 -0
  55. termcore-0.2.0/termcore/util/debug.py +196 -0
  56. termcore-0.2.0/termcore/util/index.py +112 -0
  57. termcore-0.2.0/termcore/util/logger.py +36 -0
  58. termcore-0.2.0/termcore/util/singleton.py +46 -0
  59. termcore-0.2.0/termcore/util/string.py +263 -0
  60. termcore-0.2.0/termcore/util/validation.py +31 -0
  61. termcore-0.2.0/termcore/util/version.py +56 -0
  62. termcore-0.2.0/tests/__init__.py +0 -0
  63. termcore-0.2.0/tests/io/__init__.py +0 -0
  64. termcore-0.2.0/tests/io/conftest.py +34 -0
  65. termcore-0.2.0/tests/io/test_database.py +549 -0
  66. termcore-0.2.0/tests/test_docs.py +192 -0
  67. termcore-0.2.0/tests/test_public_api.py +66 -0
  68. termcore-0.2.0/tests/tui/__init__.py +0 -0
  69. termcore-0.2.0/tests/tui/conftest.py +67 -0
  70. termcore-0.2.0/tests/tui/test_custom_bindings.py +575 -0
  71. termcore-0.2.0/tests/tui/test_theme_loader.py +646 -0
  72. termcore-0.2.0/tests/util/__init__.py +0 -0
  73. termcore-0.2.0/tests/util/test_datetime.py +152 -0
  74. termcore-0.2.0/tests/util/test_index.py +65 -0
  75. termcore-0.2.0/tests/util/test_string.py +186 -0
@@ -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,95 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be 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
+ ## [0.2.0] – 2026-07-19
8
+
9
+ ### Added
10
+
11
+ - `util/version.py`: New utility module to retrieve the package version via `importlib.metadata` with a fallback to `pyproject.toml`
12
+ - First test suite, covering `tui/theme_loader.py`, together with the pytest, coverage and dev-dependency setup the project was missing
13
+ - Test suites for `tui/custom_bindings.py`, `io/database.py`, `util/datetime.py`, `util/index.py` and `util/string.py`, which completes the list of modules named in `TODO.md`
14
+ - `io/errors.py`: `DatabaseError` and its two subclasses `UnknownIdentifierError` and `EmptyConditionsError`, the project's first domain exceptions
15
+ - `SQLComparisonOperator`: `NE`, `LIKE`, `IS_NULL` and `IS_NOT_NULL`. There was previously no way to express "not equal" at all
16
+ - `py.typed`. Without the marker a consumer's type checker treated the whole package as untyped and degraded every import to `Any`
17
+ - A `[tool.ruff]` section with the rule set from section 3.2.1 of the style guide, and `typeCheckingMode = "strict"` for basedpyright. The library was previously linted on ruff's defaults, which select no rules and enforce no line length
18
+ - `io/errors.py`: `AppStateError` and `StateFileError`
19
+ - `termcore/tui/custom_widgets/__init__.py`. The directory was an implicit namespace package, which is one packaging tool away from being dropped from the wheel entirely
20
+ - `clamped_index`, `timing_ns`, `File.folder_content_recursive`, `ThemeLoader.custom_only`, `CustomBindings.sorted_by_key`, `CustomBindings.get_screen_bindings` and `CustomBindings.get_screen_row_map`, each replacing a boolean parameter (section 1.1.2)
21
+ - `DateFormat`, an enum replacing the `english_format` flag of the datetime helpers
22
+ - `cell_width`, which counts the terminal cells a string occupies rather than its code points
23
+ - An `__all__` in every module, so that `from termcore import *` carries the public API instead of every module's own imports. The star export shrank from 141 names to 85
24
+
25
+ ### Changed
26
+
27
+ - The project is now called `termcore`. This is a breaking change on three levels: the distribution on PyPI (`pip install termcore`), the import path (`from termcore.tui.theme_loader import ThemeLoader`), and the registration prefix of the bundled themes, which is `TERMCORE_` instead of `TERMZ_`. A theme name stored in a consumer's config file therefore no longer resolves; `ThemeLoader.set_previous_theme` reports this and falls back to the default rather than failing. The `termz` releases on PyPI are not maintained further
28
+ - `ThemeLoader`: The keyword argument `termz_theme_prefix` is now `termcore_theme_prefix`, and the constant `DEFAULT_TERMZ_THEME_PREFIX` is `DEFAULT_TERMCORE_THEME_PREFIX`
29
+ - `CustomBindings`: Global bindings are no longer prefixed with `*`
30
+ - Updated colors of Textual themes `compact-gray` and `mnml-deepblack`
31
+ - `ThemeLoader`: A theme is now identified by the `name` in its `theme.py` plus its registration prefix. The directory name is no longer used as a key, so it may differ freely, and a standard and a custom theme may share a name
32
+ - `ThemeLoader`: Theme modules are imported by file path instead of through `sys.path`, so loading no longer alters the interpreter's import state
33
+ - `ThemeData`: Now frozen, and its redundant `name` field was removed
34
+ - `Database`: Values are bound as query parameters instead of being formatted into the statement, and identifiers are checked against the schema of the open database and quoted. Neither a value nor a name can be read as SQL any more
35
+ - `Database.update`: Takes the table, the new values and the conditions as three arguments, replacing the list of dictionaries carrying the conditions under an `@`-prefixed key. It now returns the number of changed rows
36
+ - `Database.remove`: Returns the number of deleted rows
37
+ - `Database.query`: Takes an optional `params` sequence, so callers never have to format values into the statement themselves
38
+ - `Database`: Leaving a `with` block commits, or rolls back if the block is left through an exception. It previously closed the connection without committing, so writes made through `query()` were lost
39
+ - `Database`: `debug_mode` writes to the module logger at debug level instead of printing to stdout
40
+ - `next_index`: The `max_index` parameter is now called `length`. It was a length in both the code and the docstring, and the old name invited an off-by-one
41
+ - `util/datetime`: Every conversion states the local time zone instead of leaving it implied. Results are unchanged on a given machine
42
+ - `str_with_fixed_width`: The alignment is validated before the text length is looked at, so an invalid value is rejected either way
43
+ - Every boolean parameter that selected behaviour is gone, replaced by a second named function, a factory or an enum: `next_index(loop_behavior=)`, `timing(use_ns_timer=)`, `File.folder_content(withsubfolders=)`, `ThemeLoader(include_standard_themes=)`, `CustomBindings(sort_alphabetically=)`, `get_bindings(for_screen=)`, `get_row_map(for_screen=)` and `english_format=` on the five datetime helpers. All of these are breaking
44
+ - `AppStateStorage`: A failure to read, create or write the state file raises `StateFileError` instead of printing a message and calling `sys.exit()`. A library taking the whole process down left the caller no way to react
45
+ - `termcore.util.debug`: The decorators log at debug level through a module logger instead of printing to stdout
46
+ - File and directory handling moved from `os.path` to `pathlib` throughout
47
+ - `str_with_fixed_width` counts terminal cells instead of code points, so a column holding CJK text or emoji stays aligned. A double-width glyph cannot be split, so where one would straddle the boundary the result is padded to reach the width exactly. Breaking for anyone who relied on the returned string having exactly `width` characters
48
+ - `date_diff` compares local calendar dates instead of dividing by 86400. Two moments on the same day are 0 days apart whatever the clock says, and a span crossing a daylight saving switch is no longer off by one
49
+ - `CustomBindings` reads `show`, `priority` and `system` as genuine YAML booleans. The quoted string `"false"` used to be read as `True`, so a quoted boolean silently meant its opposite; it now falls back to the documented default and says so
50
+ - `CustomBindings`: an explicitly declared `key_display` is no longer overridden for function keys. The field exists to override how a key is rendered
51
+
52
+ ### Fixed
53
+
54
+ - `CustomBindings`: The binding registry was held in class attributes shared by every instance, the same defect as in `ThemeLoader`. A second `CustomBindings` in one process inherited the first one's groups and appended its own on top, so every binding appeared twice
55
+ - `next_index`: An empty list raised `ZeroDivisionError` instead of yielding index 0, and the clamped branch ignored the step size while the wrapping branch honoured it
56
+ - `str_with_fixed_width`: Did not return the promised number of characters at the edges. Width 1 with `align="right"` returned the entire string, because the offset became `-0`; width 0 returned the text minus its last character
57
+ - `linewrap`: Raised `ValueError` on a word longer than the line, and looped forever for a width below 1
58
+ - `termcore.util.datetime` resolved to the `datetime` class rather than the module, because the package's star-import re-exported the imported name over the submodule
59
+ - `Database.fetch`: An offset without a limit produced a syntax error, `limit=0` was silently dropped, and an empty column list produced `SELECT FROM`
60
+ - `Database`: `ORDER BY` and `WHERE` were joined using identity checks against the first and last element, so passing the same `Condition` or `ColumnOrder` object twice produced malformed SQL
61
+ - `Database.insert`: Read the new row back through a hard-coded `id` column, which failed after the write on any table without one, and committed each row separately, so a failure half way through left the earlier rows behind
62
+ - `Database.update`: Deleted the conditions out of the caller's own dictionary while parsing them, so a second call with the same data built a statement with an empty `WHERE` clause
63
+ - `Database`: `__del__` raised `AttributeError` as an unraisable when the connection had never been assigned
64
+ - `ThemeLoader`: The theme registry was held in class attributes shared by every instance, and registration re-prefixed the same `Theme` objects each time. A second application in one process saw `CUSTOM_CUSTOM_<name>`, a third `CUSTOM_CUSTOM_CUSTOM_<name>`. State is now per instance and the prefix is applied once, to a copy
65
+ - `ThemeLoader`: A theme folder whose name differed from the `name` in its `theme.py` registered but silently lost its stylesheet
66
+ - `ThemeLoader`: From the second loader in a process on, the bundled themes were not found at all, because a consumer's theme folder shadowed them under the shared package name `themes`. The bug was masked by the shared registry described above
67
+ - `ThemeLoader`: Switching themes never removed the stylesheet of a theme outside termcore's own theme folder, so a consumer's CSS stayed applied on top of every theme it switched to
68
+ - `ThemeLoader`: A theme without a stylesheet, and any theme not registered by termcore such as Textual's built-ins, logged a warning during entirely normal use. This is reported at debug level now
69
+ - `ThemeLoader`: Diagnostics went to the root logger and surfaced as `WARNING:root:` in every consumer; they now use a module logger and lazy formatting
70
+ - `ThemeLoader`: Two theme folders declaring the same name silently overwrote each other; the second is now refused with an error
71
+ - `Singleton`: The metaclass reported `object` as the type of every instance it built, so a consumer type-checking against a singleton got no type at all. It now reports the class it constructed
72
+ - `CustomDataTable.flexible_columns` and `AppStateStorage._json_dict` were class attributes with mutable defaults, so every instance in a process shared one object. Same defect as in `ThemeLoader` and `CustomBindings`
73
+ - `File.change_extension` printed the split file name to stdout, a debug statement left in a pure function
74
+ - A message in `AppStateStorage` was in German, against the rule that all visible text is English
75
+ - `ThemeLoader.set_previous_theme_in_textual_app` did nothing at all when the stored theme was no longer registered, so a theme removed between two runs left the application on Textual's default rather than on the `default_theme_name` the caller passed. It now falls back explicitly and reports both that and the case where the default is unknown too
76
+ - `ThemeLoader` called `Stylesheet.reparse()` unguarded, so a theme shipping invalid TCSS took the whole application down from inside the library. The failure is logged and the previous stylesheet is kept
77
+ - `CustomBindings` was silent in three places that now warn: a binding dropped for a missing `key`, `action` or `description`, a second group claiming an action name and overwriting the first one's footer row, and a tab or screen name matching no group at all
78
+
79
+ ### Removed
80
+
81
+ - `Database.tostr`: Values are parameters now, so the method had no purpose left; leaving it would have invited hand-built statements
82
+ - `Database.update` no longer accepts the list-of-dictionaries form with an `@`-prefixed conditions key
83
+ - `CustomBindings._action_belongs_to_group`: Never called
84
+ - Commented-out code in `CustomBindings` and `CustomDataTable`
85
+ - Two of the six blanket pyright suppressions, after measuring that they report nothing in either checking mode
86
+
87
+ ## [0.1.1] – 2026-04-07
88
+
89
+ - Initial Release
90
+
91
+ ## [0.1.0] – 2026-03-25
92
+
93
+ *PyPI name reservation – no functional code.*
94
+
95
+ [0.2.0]: https://github.com/cgroening/py-termcore/compare/v0.1.1...v0.2.0 [0.1.1]: https://github.com/cgroening/py-termcore/compare/v0.1.0...v0.1.1 [0.1.0]: https://github.com/cgroening/py-termcore/releases/tag/v0.1.0
@@ -0,0 +1,55 @@
1
+ # CLAUDE.md
2
+
3
+ Project-specific instructions for termcore. The global style guide referenced in `~/.claude/CLAUDE.md` remains authoritative; this file only records what is specific to this repository.
4
+
5
+ ## What this project is
6
+
7
+ termcore is a library, not an application. It is the foundation under termplate, which section 3.2.9 of the style guide names as the template every Textual-based TUI is checked against.
8
+
9
+ That ordering has a consequence worth stating plainly: a defect here reaches every application built on the toolkit, and it reaches them silently, because applications trust a library rather than testing it. Four of the theme bugs that held up termplate lived in this repository and none of them failed loudly – they just rendered the wrong colours. Prefer failing loudly over carrying on, and prefer a test over a comment.
10
+
11
+ ## Toolchain
12
+
13
+ All of these must be clean before a change is finished:
14
+
15
+ ```zsh
16
+ ruff check .
17
+ basedpyright
18
+ pytest
19
+ ```
20
+
21
+ `ruff format` is deliberately absent. The formatter is not configured, line breaking is hand-made, and `E501` enforces the column limit; the reasoning sits in a comment in `pyproject.toml`. Do not add `[tool.ruff.format]` without deciding that question again.
22
+
23
+ The binary is `basedpyright`, never `pyright`, even though the configuration section is called `[tool.pyright]`. Its default mode is stricter than the `strict` the configuration asks for, which is why the four remaining suppressions read as inert – they describe real properties of this code and keep a switch to `recommended` a one-line change.
24
+
25
+ Coverage runs through `--cov` in `addopts`. The gate in `[tool.coverage.report]` holds the level currently reached: raise `fail_under` when a module gains tests, never lower it to make a run pass.
26
+
27
+ ## Architecture
28
+
29
+ Four sub-packages, each a coherent purpose rather than a layer:
30
+
31
+ - `cli` – styled console output through Rich.
32
+ - `io` – SQLite, JSON application state, files and text files.
33
+ - `tui` – Textual helpers: theme loading, key bindings, custom widgets.
34
+ - `util` – dates, strings, index arithmetic, logging, debugging decorators.
35
+
36
+ The public API of each package is re-exported through `from .module import *` in its `__init__.py`, as section 3.2.2 requires. That has a consequence which has already caused a defect: a module without `__all__` re-exports its own imports too. `termcore/util/datetime.py` re-exported the `datetime` class over the submodule of the same name, so `import termcore.util.datetime` handed back the class. Every module declares `__all__`, and a new one has to as well – `tests/test_public_api.py` fails otherwise, and also checks that no borrowed name such as `json` or `Path` reaches the package namespace.
37
+
38
+ ## The defect this repository keeps producing
39
+
40
+ Mutable class attributes shared by every instance. It has been found and fixed four times: `ThemeLoader._theme_names` and `_theme_data`, the four registries in `CustomBindings`, `CustomDataTable.flexible_columns` and `AppStateStorage._json_dict`. Each time the symptom was different and none of them raised.
41
+
42
+ Declare instance attributes annotated at class level and assign them in `__init__`, as section 3.2.5 says. `RUF012` catches the annotated-with-a-value form; it does not catch every shape, so this stays worth looking for by hand.
43
+
44
+ ## Conventions particular to this repo
45
+
46
+ - No boolean parameter selects behaviour anywhere in the public API. Section 1.1.2 is enforced by `FBT`, and the established answers are two named functions (`next_index` and `clamped_index`, `timing` and `timing_ns`), a classmethod factory (`ThemeLoader.custom_only`, `CustomBindings.sorted_by_key`) or an enum (`DateFormat`). Pick whichever fits the parameter; a format selector is an enum, a behaviour switch is two functions.
47
+ - Domain exceptions live in `termcore/io/errors.py`. A library that calls `sys.exit()` or prints an error leaves its caller no way to react – `AppStateStorage` did both until recently.
48
+ - Diagnostics go to a module logger with lazy `%`-formatting, never to `print`. `termcore/cli/output.py` is the one module where printing is the product, and it carries a per-file ignore saying so.
49
+ - Every SQL statement binds its values as parameters, and every identifier passes `_quoted_table` or `_quoted_column`, which check it against the schema of the open database before quoting it. The `# noqa: S608` markers state exactly what the rule cannot see. There is no other way to build a statement here.
50
+ - A theme is identified by the `name` in its `theme.py` plus its registration prefix; the directory it lives in carries no meaning. Theme modules are imported by file path, never as a package – importing them by folder name made the first folder loaded in a process shadow every later one of the same name.
51
+ - Textual reads annotations at runtime, so type-only imports stay at module level under `termcore/tui/`. The per-file ignore for `TC002` and `TC003` in `pyproject.toml` is that rule, not an oversight.
52
+ - hatchling ships everything inside the package directory, so runtime data (`py.typed`, the theme `.css` and `theme.py` files) needs no `package-data` entry. Every package directory does need an `__init__.py`, or it becomes an implicit namespace package that a stricter packaging tool would drop.
53
+ - termplate is the only consumer. A breaking change is pulled through it in the same pass, and its suite has to be green before the change is finished.
54
+ - `TODO.md` tracks deferred work, including findings that were deliberately left alone and why. Read it before starting, so the same gap is not filled twice in different ways.
55
+ - `docs/DEVELOPMENT.md` holds the module tree, which shared helper already solves which problem, how to exercise a library that has no entry point, and the release procedure. `docs/CLEAN-UP.md` is the checklist for a structured review pass. Both are guarded by `tests/test_docs.py`, so a path that moves fails the suite.
termcore-0.2.0/LICENSE ADDED
@@ -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.