termcore 0.2.0__py3-none-any.whl
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.
- termcore/__init__.py +6 -0
- termcore/cli/__init__.py +3 -0
- termcore/cli/output.py +70 -0
- termcore/io/__init__.py +7 -0
- termcore/io/app_state_storage.py +185 -0
- termcore/io/database.py +655 -0
- termcore/io/errors.py +71 -0
- termcore/io/file.py +263 -0
- termcore/io/textfile.py +92 -0
- termcore/py.typed +0 -0
- termcore/tui/__init__.py +9 -0
- termcore/tui/custom_bindings.py +551 -0
- termcore/tui/custom_widgets/__init__.py +1 -0
- termcore/tui/custom_widgets/custom_checkbox.py +41 -0
- termcore/tui/custom_widgets/custom_data_table.py +247 -0
- termcore/tui/custom_widgets/custom_selection.py +80 -0
- termcore/tui/custom_widgets/multiline_footer.py +240 -0
- termcore/tui/question_screen.py +107 -0
- termcore/tui/theme_loader.py +495 -0
- termcore/tui/themes/__init__.py +2 -0
- termcore/tui/themes/classic-black-saturated/style.css +57 -0
- termcore/tui/themes/classic-black-saturated/theme.py +22 -0
- termcore/tui/themes/classic-black-v1/style.css +57 -0
- termcore/tui/themes/classic-black-v1/theme.py +22 -0
- termcore/tui/themes/classic-black-v2/style.css +57 -0
- termcore/tui/themes/classic-black-v2/theme.py +24 -0
- termcore/tui/themes/classic-blue/theme.py +21 -0
- termcore/tui/themes/compact-gray/style.tcss +175 -0
- termcore/tui/themes/compact-gray/theme.py +22 -0
- termcore/tui/themes/mnml-black/style.css +57 -0
- termcore/tui/themes/mnml-black/theme.py +21 -0
- termcore/tui/themes/mnml-deepblack/style.css +57 -0
- termcore/tui/themes/mnml-deepblack/theme.py +21 -0
- termcore/tui/themes/pure-amber/theme.py +18 -0
- termcore/tui/themes/pure-black/theme.py +18 -0
- termcore/tui/themes/pure-blue/theme.py +18 -0
- termcore/tui/themes/pure-green/theme.py +18 -0
- termcore/tui/themes/pure-sweet16/theme.py +20 -0
- termcore/tui/themes/xplore-black/theme.py +18 -0
- termcore/tui/themes/xplore-blue/theme.py +18 -0
- termcore/tui/themes/xplore-blue-muted/theme.py +18 -0
- termcore/tui/themes/xplore-teal/theme.py +18 -0
- termcore/util/__init__.py +10 -0
- termcore/util/datetime.py +173 -0
- termcore/util/debug.py +196 -0
- termcore/util/index.py +112 -0
- termcore/util/logger.py +36 -0
- termcore/util/singleton.py +46 -0
- termcore/util/string.py +263 -0
- termcore/util/validation.py +31 -0
- termcore/util/version.py +56 -0
- termcore-0.2.0.dist-info/METADATA +504 -0
- termcore-0.2.0.dist-info/RECORD +55 -0
- termcore-0.2.0.dist-info/WHEEL +4 -0
- termcore-0.2.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,504 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: termcore
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Terminal utilities for CLI, TUI, IO and general use
|
|
5
|
+
Project-URL: Homepage, https://github.com/cgroening/py-termcore
|
|
6
|
+
Project-URL: Repository, https://github.com/cgroening/py-termcore
|
|
7
|
+
License: MIT
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Keywords: cli,io,terminal,tui,utils
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Environment :: Console
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
18
|
+
Classifier: Topic :: Terminals
|
|
19
|
+
Requires-Python: >=3.12
|
|
20
|
+
Requires-Dist: pyyaml
|
|
21
|
+
Requires-Dist: rich
|
|
22
|
+
Requires-Dist: textual
|
|
23
|
+
Provides-Extra: dev
|
|
24
|
+
Requires-Dist: basedpyright>=1.31; extra == 'dev'
|
|
25
|
+
Requires-Dist: coverage>=7.13.4; extra == 'dev'
|
|
26
|
+
Requires-Dist: pytest-asyncio>=1.0; extra == 'dev'
|
|
27
|
+
Requires-Dist: pytest-cov>=7.0; extra == 'dev'
|
|
28
|
+
Requires-Dist: pytest>=9.0; extra == 'dev'
|
|
29
|
+
Requires-Dist: ruff>=0.15; extra == 'dev'
|
|
30
|
+
Description-Content-Type: text/markdown
|
|
31
|
+
|
|
32
|
+
# termcore
|
|
33
|
+
|
|
34
|
+
Terminal utilities for CLI, TUI, IO and general use.
|
|
35
|
+
|
|
36
|
+
## Overview
|
|
37
|
+
|
|
38
|
+
**termcore** is a Python library that bundles reusable building blocks for terminal applications. It is organized into four sub-packages:
|
|
39
|
+
|
|
40
|
+
| Package | Contents |
|
|
41
|
+
|---------|----------|
|
|
42
|
+
| `termcore.cli` | Styled console output via [Rich](https://github.com/Textualize/rich) |
|
|
43
|
+
| `termcore.tui` | [Textual](https://github.com/Textualize/textual) TUI helpers – theme loading, custom widgets, modal screens |
|
|
44
|
+
| `termcore.io` | SQLite database abstraction, JSON app-state storage, file utilities |
|
|
45
|
+
| `termcore.util` | Datetime helpers, string utilities, singleton metaclass, debug decorators, logging setup |
|
|
46
|
+
|
|
47
|
+
## Requirements
|
|
48
|
+
|
|
49
|
+
- Python >= 3.12
|
|
50
|
+
- [`rich`](https://pypi.org/project/rich/)
|
|
51
|
+
- [`textual`](https://pypi.org/project/textual/) (required for `termcore.tui`)
|
|
52
|
+
|
|
53
|
+
## Installation
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
pip install termcore
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## termcore.cli – Styled CLI Output
|
|
60
|
+
|
|
61
|
+
Provides helpers for printing color-coded panels using Rich and for clearing terminal output.
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
from termcore import print_error, print_warning, print_success, print_info, clear_lines
|
|
65
|
+
|
|
66
|
+
print_success("File saved.")
|
|
67
|
+
print_warning("Disk space is low.")
|
|
68
|
+
print_error("Connection refused.")
|
|
69
|
+
print_info("Starting process...")
|
|
70
|
+
|
|
71
|
+
# Remove the last 4 rendered lines from the terminal
|
|
72
|
+
clear_lines(4)
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### Functions
|
|
76
|
+
|
|
77
|
+
| Function | Description |
|
|
78
|
+
|----------|-------------|
|
|
79
|
+
| `print_error(message)` | Red panel with ✗ prefix |
|
|
80
|
+
| `print_warning(message)` | Yellow panel with ⚠ prefix |
|
|
81
|
+
| `print_success(message)` | Green panel with ✓ prefix |
|
|
82
|
+
| `print_info(message)` | Cyan panel with ℹ prefix |
|
|
83
|
+
| `print_panel(message, color)` | Panel with bold colored text |
|
|
84
|
+
| `print_custom_panel(formatted_message, panel_color)` | Panel with pre-formatted Rich markup |
|
|
85
|
+
| `clear_lines(count)` | Move cursor up `count` lines and clear everything below |
|
|
86
|
+
| `get_console()` | Returns the shared Rich `Console` instance |
|
|
87
|
+
|
|
88
|
+
## termcore.tui – Textual TUI Helpers
|
|
89
|
+
|
|
90
|
+
### ThemeLoader
|
|
91
|
+
|
|
92
|
+
Dynamically loads and registers [Textual](https://github.com/Textualize/textual) themes from a folder. Each theme lives in its own sub-directory and must expose a `TEXTUAL_THEME` variable of type `textual.theme.Theme`. Optional `.css` / `.tcss` files in the same folder are loaded automatically when the theme is activated.
|
|
93
|
+
|
|
94
|
+
A theme is identified by the `name` of its `TEXTUAL_THEME` plus the prefix it is registered under – the name of its directory carries no meaning and is free to differ. A theme without a stylesheet is equally valid; ten of the built-in themes below ship none. Each theme folder is read on its own, so an app can register several loaders and its own themes never collide with the ones the toolkit brings.
|
|
95
|
+
|
|
96
|
+
termcore ships 16 built-in themes:
|
|
97
|
+
|
|
98
|
+
`classic-black-saturated`, `classic-black-v1`, `classic-black-v2`, `classic-blue`, `compact-gray`, `mnml-black`, `mnml-deepblack`, `pure-amber`, `pure-black`, `pure-blue`, `pure-green`, `pure-sweet16`, `xplore-black`, `xplore-blue`, `xplore-blue-muted`, `xplore-teal`
|
|
99
|
+
|
|
100
|
+
```python
|
|
101
|
+
from pathlib import Path
|
|
102
|
+
from termcore import ThemeLoader
|
|
103
|
+
|
|
104
|
+
loader = ThemeLoader("themes") # bundled themes plus that folder
|
|
105
|
+
loader = ThemeLoader.custom_only("themes") # only that folder
|
|
106
|
+
|
|
107
|
+
# In your Textual App.on_mount():
|
|
108
|
+
loader.register_themes_in_textual_app(app)
|
|
109
|
+
loader.set_previous_theme_in_textual_app(
|
|
110
|
+
app,
|
|
111
|
+
default_theme_name="TERMCORE_xplore-blue",
|
|
112
|
+
theme_config_file=Path("~/.config/myapp/theme.json").expanduser(),
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
# When the user changes theme:
|
|
116
|
+
loader.save_theme_to_config(app.theme, Path("~/.config/myapp/theme.json").expanduser())
|
|
117
|
+
loader.load_theme_css(app.theme, app)
|
|
118
|
+
|
|
119
|
+
# Cycle through themes with arrow keys:
|
|
120
|
+
loader.change_to_next_or_previous_theme(direction=1, app=app)
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Theme name prefixes:
|
|
124
|
+
|
|
125
|
+
- Built-in termcore themes: `TERMCORE_` (e.g. `TERMCORE_xplore-blue`)
|
|
126
|
+
- Custom themes: `CUSTOM_` (e.g. `CUSTOM_mytheme`)
|
|
127
|
+
|
|
128
|
+
Both prefixes can be customized via the `ThemeLoader` constructor.
|
|
129
|
+
|
|
130
|
+
### QuestionScreen
|
|
131
|
+
|
|
132
|
+
A Textual `ModalScreen` that presents a yes/no dialog and returns a `bool`.
|
|
133
|
+
|
|
134
|
+
```python
|
|
135
|
+
from termcore import QuestionScreen, ButtonColor
|
|
136
|
+
|
|
137
|
+
async def confirm_delete(self):
|
|
138
|
+
answer = await self.app.push_screen_wait(
|
|
139
|
+
QuestionScreen(
|
|
140
|
+
question="Delete this entry?",
|
|
141
|
+
yes_button_color=ButtonColor.ERROR,
|
|
142
|
+
no_button_color=ButtonColor.PRIMARY,
|
|
143
|
+
)
|
|
144
|
+
)
|
|
145
|
+
if answer:
|
|
146
|
+
self.do_delete()
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
`ButtonColor` values: `DEFAULT`, `PRIMARY`, `ERROR`, `SUCCESS`, `WARNING`.
|
|
150
|
+
|
|
151
|
+
### CustomDataTable
|
|
152
|
+
|
|
153
|
+
A subclass of Textual's `DataTable` that supports *flexible columns* – columns that automatically fill the remaining width when the terminal is resized.
|
|
154
|
+
|
|
155
|
+
```python
|
|
156
|
+
from termcore.tui.custom_widgets.custom_data_table import CustomDataTable
|
|
157
|
+
|
|
158
|
+
table = CustomDataTable()
|
|
159
|
+
col_name = table.add_column("Name", width=20)
|
|
160
|
+
col_desc = table.add_column("Description")
|
|
161
|
+
table.flexible_columns = [col_desc] # This column will stretch to fill space
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
### CustomCheckbox
|
|
165
|
+
|
|
166
|
+
A subclass of Textual's `Checkbox` that shows a `✔` when checked and an empty box when unchecked, instead of the default `X`.
|
|
167
|
+
|
|
168
|
+
```python
|
|
169
|
+
from termcore.tui.custom_widgets.custom_checkbox import CustomCheckbox
|
|
170
|
+
|
|
171
|
+
yield CustomCheckbox("Enable feature", value=True)
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
### CustomSelectionList
|
|
175
|
+
|
|
176
|
+
A subclass of Textual's `SelectionList` whose items show a `✔` when selected and an empty box when unselected, instead of the default `X`.
|
|
177
|
+
|
|
178
|
+
```python
|
|
179
|
+
from termcore.tui.custom_widgets.custom_selection import CustomSelectionList
|
|
180
|
+
|
|
181
|
+
yield CustomSelectionList(
|
|
182
|
+
("Option A", "a"),
|
|
183
|
+
("Option B", "b"),
|
|
184
|
+
)
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
### MultiLineFooter
|
|
188
|
+
|
|
189
|
+
A drop-in replacement for Textual's built-in `Footer` that supports multiple rows of key bindings. Two modes are available:
|
|
190
|
+
|
|
191
|
+
- **`auto_wrap=True`** (default) – bindings wrap automatically when the row is full.
|
|
192
|
+
- **`auto_wrap=False`** – bindings are assigned to rows explicitly via `row_map`.
|
|
193
|
+
|
|
194
|
+
```python
|
|
195
|
+
from termcore.tui.custom_widgets.multiline_footer import MultiLineFooter
|
|
196
|
+
|
|
197
|
+
# Auto-wrap (default)
|
|
198
|
+
yield MultiLineFooter()
|
|
199
|
+
|
|
200
|
+
# Manual row assignment
|
|
201
|
+
yield MultiLineFooter(
|
|
202
|
+
auto_wrap=False,
|
|
203
|
+
row_map={
|
|
204
|
+
'quit': 0,
|
|
205
|
+
'save': 0,
|
|
206
|
+
'toggle_dark': 1,
|
|
207
|
+
'help': 1,
|
|
208
|
+
},
|
|
209
|
+
)
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
### CustomBindings
|
|
213
|
+
|
|
214
|
+
Loads keyboard bindings from a YAML file and exposes them as Textual `Binding` objects. Supports global bindings, tab-specific bindings, and screen-specific bindings.
|
|
215
|
+
|
|
216
|
+
**Group naming conventions:**
|
|
217
|
+
|
|
218
|
+
| Group name | Scope | Action prefix |
|
|
219
|
+
|------------|-------|---------------|
|
|
220
|
+
| `_global` | Always visible | none (used as-is) |
|
|
221
|
+
| `<name>_tab` | Shown when that tab is active | `<name>_tab_` |
|
|
222
|
+
| `<name>_screen` | Shown on that screen | none (used as-is) |
|
|
223
|
+
|
|
224
|
+
**YAML example (`bindings.yaml`):**
|
|
225
|
+
|
|
226
|
+
```yaml
|
|
227
|
+
_global:
|
|
228
|
+
- key: q
|
|
229
|
+
action: quit
|
|
230
|
+
description: Quit
|
|
231
|
+
priority: true
|
|
232
|
+
row: 1
|
|
233
|
+
|
|
234
|
+
tasks_tab:
|
|
235
|
+
- key: a
|
|
236
|
+
action: add_task
|
|
237
|
+
description: Add
|
|
238
|
+
row: 0
|
|
239
|
+
|
|
240
|
+
add_screen:
|
|
241
|
+
- key: escape
|
|
242
|
+
action: cancel
|
|
243
|
+
description: Cancel
|
|
244
|
+
row: 0
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
**Usage:**
|
|
248
|
+
|
|
249
|
+
```python
|
|
250
|
+
from termcore.tui.custom_bindings import CustomBindings
|
|
251
|
+
|
|
252
|
+
bindings = CustomBindings("bindings.yaml") # file order
|
|
253
|
+
bindings = CustomBindings.sorted_by_key("bindings.yaml") # sorted by key
|
|
254
|
+
|
|
255
|
+
# In your App or Screen:
|
|
256
|
+
# On the App: globals dispatch as declared
|
|
257
|
+
BINDINGS = bindings.get_bindings() # every tab + global
|
|
258
|
+
BINDINGS = bindings.get_bindings(tab_name="tasks_tab") # one tab + global
|
|
259
|
+
|
|
260
|
+
# On a Screen: globals are prefixed with `app.` so they dispatch on the App
|
|
261
|
+
BINDINGS = bindings.get_screen_bindings() # every tab + global
|
|
262
|
+
BINDINGS = bindings.get_screen_bindings("add") # that screen + global
|
|
263
|
+
|
|
264
|
+
# Row map for MultiLineFooter(auto_wrap=False):
|
|
265
|
+
row_map = bindings.get_row_map() # pair with get_bindings
|
|
266
|
+
row_map = bindings.get_screen_row_map() # pair with get_screen_bindings
|
|
267
|
+
|
|
268
|
+
# In check_action to hide tab bindings that don't belong to the active tab:
|
|
269
|
+
def check_action(self, action, parameters):
|
|
270
|
+
return bindings.handle_check_action(action, parameters, active_group=self.active_tab)
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
## termcore.io – IO Utilities
|
|
274
|
+
|
|
275
|
+
### AppStateStorage
|
|
276
|
+
|
|
277
|
+
A JSON-backed singleton for persisting small application states (scroll position, last selection, command history, etc.).
|
|
278
|
+
|
|
279
|
+
```python
|
|
280
|
+
from termcore import AppStateStorage
|
|
281
|
+
|
|
282
|
+
# Initialize once (e.g. at startup)
|
|
283
|
+
storage = AppStateStorage(package_name="myapp")
|
|
284
|
+
# State file is created at ~/.local/state/myapp/state.json
|
|
285
|
+
|
|
286
|
+
# Read / write simple values
|
|
287
|
+
storage.set("last_tab", "settings")
|
|
288
|
+
tab = storage.get("last_tab", default_value="home")
|
|
289
|
+
|
|
290
|
+
# List operations
|
|
291
|
+
storage.list_insert("history", 0, "command_1")
|
|
292
|
+
storage.edit_list_item("history", 0, "label", "renamed")
|
|
293
|
+
storage.move_list_item("history", 0, 2)
|
|
294
|
+
storage.delete_list_item("history", 0)
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
Because `AppStateStorage` is a singleton, subsequent calls to `AppStateStorage()` anywhere in the application return the same instance. Supply an explicit `state_file_path` instead of `package_name` for a custom path.
|
|
298
|
+
|
|
299
|
+
### Database
|
|
300
|
+
|
|
301
|
+
A lightweight SQLite abstraction.
|
|
302
|
+
|
|
303
|
+
Values are always bound as query parameters, never formatted into the statement, so a value can never be read as SQL. Identifiers cannot be parameters, so a table or column name is checked against the schema of the open database and quoted before it is used; a name that is not part of the schema raises `UnknownIdentifierError` instead of reaching the database. `update` and `remove` refuse an empty condition list with `EmptyConditionsError` rather than rewriting or emptying the whole table.
|
|
304
|
+
|
|
305
|
+
```python
|
|
306
|
+
from termcore import Database, Condition, SQLComparisonOperator, SQLOrderByDirection, ColumnOrder
|
|
307
|
+
|
|
308
|
+
with Database("data.db") as db:
|
|
309
|
+
# Fetch with conditions and ordering
|
|
310
|
+
rows = db.fetch(
|
|
311
|
+
table="tasks",
|
|
312
|
+
columns=["id", "title", "due_date"],
|
|
313
|
+
conditions=[
|
|
314
|
+
Condition("done", SQLComparisonOperator.EQ, 0),
|
|
315
|
+
],
|
|
316
|
+
orderby=[ColumnOrder("due_date", SQLOrderByDirection.ASC)],
|
|
317
|
+
limit=50,
|
|
318
|
+
)
|
|
319
|
+
|
|
320
|
+
# Insert
|
|
321
|
+
inserted = db.insert("tasks", [{"title": "Buy milk", "done": 0}])
|
|
322
|
+
|
|
323
|
+
# Update - returns the number of changed rows
|
|
324
|
+
changed = db.update(
|
|
325
|
+
"tasks",
|
|
326
|
+
{"done": 1},
|
|
327
|
+
[Condition("id", SQLComparisonOperator.EQ, inserted[0]["id"])],
|
|
328
|
+
)
|
|
329
|
+
|
|
330
|
+
# Delete
|
|
331
|
+
db.remove("tasks", [Condition("id", SQLComparisonOperator.EQ, 42)])
|
|
332
|
+
|
|
333
|
+
# Raw SQL - pass values through params, never format them into the string
|
|
334
|
+
db.query("CREATE TABLE IF NOT EXISTS tasks (id INTEGER PRIMARY KEY, title TEXT, done INTEGER)")
|
|
335
|
+
db.query("INSERT INTO tasks (title) VALUES (?)", ["Buy bread"])
|
|
336
|
+
db.save()
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
Leaving the `with` block commits, unless it is left through an exception, in which case the transaction is rolled back. Available comparison operators are `LT`, `LE`, `EQ`, `NE`, `GE`, `GT`, `LIKE`, `IS_NULL` and `IS_NOT_NULL`; the last two take no value.
|
|
340
|
+
|
|
341
|
+
### File
|
|
342
|
+
|
|
343
|
+
Static helpers for file and folder operations.
|
|
344
|
+
|
|
345
|
+
```python
|
|
346
|
+
from termcore import File
|
|
347
|
+
|
|
348
|
+
# List folder contents (optionally filtered by extension and recursive)
|
|
349
|
+
items = File.folder_content("./data", extfilter="csv", withsubfolders=True)
|
|
350
|
+
|
|
351
|
+
# Copy a folder
|
|
352
|
+
File.copy_folder("./src_folder", "./dst_folder")
|
|
353
|
+
|
|
354
|
+
# File extension helpers
|
|
355
|
+
ext = File.extension("report.csv") # "csv"
|
|
356
|
+
new_name = File.change_extension("report.csv", "txt") # "report.txt"
|
|
357
|
+
folder = File.path("/home/user/docs/file.txt") # "/home/user/docs"
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
### Textfile
|
|
361
|
+
|
|
362
|
+
Simple UTF-8 text file read/write.
|
|
363
|
+
|
|
364
|
+
```python
|
|
365
|
+
from termcore import Textfile # or: from termcore.io.textfile import Textfile
|
|
366
|
+
|
|
367
|
+
content = Textfile.read("notes.txt")
|
|
368
|
+
lines = Textfile.readlines("notes.txt")
|
|
369
|
+
Textfile.write("notes.txt", "New content")
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
## termcore.util – General Utilities
|
|
373
|
+
|
|
374
|
+
### Logging
|
|
375
|
+
|
|
376
|
+
```python
|
|
377
|
+
from termcore import setup_logging
|
|
378
|
+
import logging
|
|
379
|
+
|
|
380
|
+
setup_logging("myapp", level=logging.INFO)
|
|
381
|
+
# Writes to ~/.local/state/myapp/app.log
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
### Singleton
|
|
385
|
+
|
|
386
|
+
A metaclass that enforces the singleton pattern.
|
|
387
|
+
|
|
388
|
+
```python
|
|
389
|
+
from termcore import Singleton
|
|
390
|
+
|
|
391
|
+
class Config(metaclass=Singleton):
|
|
392
|
+
def __init__(self):
|
|
393
|
+
self.debug = False
|
|
394
|
+
|
|
395
|
+
a = Config()
|
|
396
|
+
b = Config()
|
|
397
|
+
assert a is b # True
|
|
398
|
+
```
|
|
399
|
+
|
|
400
|
+
### Datetime
|
|
401
|
+
|
|
402
|
+
```python
|
|
403
|
+
from termcore import DateFormat, timestamp_to_date, date_to_timestamp, date_diff, today_timestamp, today_date
|
|
404
|
+
|
|
405
|
+
ts = date_to_timestamp("01.04.2025") # German format (default)
|
|
406
|
+
ts = date_to_timestamp("2025-04-01", DateFormat.ISO)
|
|
407
|
+
|
|
408
|
+
s = timestamp_to_date(ts) # "01.04.2025"
|
|
409
|
+
s = timestamp_to_date(ts, DateFormat.ISO) # "2025-04-01"
|
|
410
|
+
|
|
411
|
+
days = date_diff(ts1, ts2) # difference in days
|
|
412
|
+
|
|
413
|
+
ts_today = today_timestamp() # midnight UNIX timestamp
|
|
414
|
+
s_today = today_date() # "02.04.2026"
|
|
415
|
+
```
|
|
416
|
+
|
|
417
|
+
### String
|
|
418
|
+
|
|
419
|
+
```python
|
|
420
|
+
from termcore import linewrap, charpos, cell_width, str_with_fixed_width
|
|
421
|
+
|
|
422
|
+
wrapped = linewrap("A long piece of text that should be wrapped.", linewidth=20)
|
|
423
|
+
positions = charpos("hello world", "l") # [2, 3, 9]
|
|
424
|
+
|
|
425
|
+
cell_width("abc") # 3
|
|
426
|
+
cell_width("日本語") # 6 - one CJK glyph occupies two terminal cells
|
|
427
|
+
|
|
428
|
+
# Exactly `width` terminal cells, truncated with an ellipsis or padded
|
|
429
|
+
str_with_fixed_width("a long value", 8) # "a long …"
|
|
430
|
+
str_with_fixed_width("a long value", 8, "right") # "…g value"
|
|
431
|
+
str_with_fixed_width("ok", 8, "center") # " ok "
|
|
432
|
+
str_with_fixed_width("日本語テキスト", 8) # "日本語… "
|
|
433
|
+
```
|
|
434
|
+
|
|
435
|
+
Widths are counted in terminal cells rather than in code points, so a column stays aligned whatever the data contains. A double-width glyph cannot be split, so where one would straddle the boundary the result is padded with a space to reach the requested width exactly.
|
|
436
|
+
|
|
437
|
+
### Index Navigation
|
|
438
|
+
|
|
439
|
+
```python
|
|
440
|
+
from termcore import next_index, clamped_index
|
|
441
|
+
|
|
442
|
+
# Navigate a list of 5 items, wrapping around at edges
|
|
443
|
+
idx = next_index(current_index=4, length=5, direction=1) # 0 (wraps)
|
|
444
|
+
idx = next_index(current_index=0, length=5, direction=-1) # 4 (wraps)
|
|
445
|
+
|
|
446
|
+
# Clamp at the boundaries instead of wrapping
|
|
447
|
+
idx = clamped_index(current_index=4, length=5, direction=1) # 4
|
|
448
|
+
|
|
449
|
+
# An empty list has no valid index
|
|
450
|
+
idx = next_index(current_index=0, length=0) # 0
|
|
451
|
+
```
|
|
452
|
+
|
|
453
|
+
### Validation
|
|
454
|
+
|
|
455
|
+
```python
|
|
456
|
+
from termcore import is_number
|
|
457
|
+
|
|
458
|
+
is_number("3.14") # True
|
|
459
|
+
is_number("abc") # False
|
|
460
|
+
is_number(None) # False
|
|
461
|
+
```
|
|
462
|
+
|
|
463
|
+
### Debug Decorators
|
|
464
|
+
|
|
465
|
+
```python
|
|
466
|
+
from termcore import print_arguments, timing, timing_ns
|
|
467
|
+
|
|
468
|
+
@print_arguments
|
|
469
|
+
def add(a: int, b: int) -> int:
|
|
470
|
+
return a + b
|
|
471
|
+
|
|
472
|
+
add(3, 5)
|
|
473
|
+
# Logged at debug level on the `termcore.util.debug` logger:
|
|
474
|
+
# Function add called
|
|
475
|
+
# Args: (3, 5)
|
|
476
|
+
# Kwargs: {}
|
|
477
|
+
# Function add returns: 8
|
|
478
|
+
|
|
479
|
+
|
|
480
|
+
@timing() # seconds
|
|
481
|
+
@timing_ns() # nanoseconds
|
|
482
|
+
def heavy_computation():
|
|
483
|
+
...
|
|
484
|
+
```
|
|
485
|
+
|
|
486
|
+
The decorators log at debug level rather than printing, so enable that logger to see them.
|
|
487
|
+
|
|
488
|
+
## Development
|
|
489
|
+
|
|
490
|
+
```zsh
|
|
491
|
+
uv venv
|
|
492
|
+
source .venv/bin/activate
|
|
493
|
+
uv pip install -e ".[dev]"
|
|
494
|
+
|
|
495
|
+
ruff check . # linting; the rule set is the style guide's baseline
|
|
496
|
+
basedpyright # strict type checking
|
|
497
|
+
pytest # tests, with coverage
|
|
498
|
+
```
|
|
499
|
+
|
|
500
|
+
The ruff formatter is deliberately not configured: line breaking is hand-made and the 80-column limit is enforced by `E501`.
|
|
501
|
+
|
|
502
|
+
## License
|
|
503
|
+
|
|
504
|
+
MIT
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
termcore/__init__.py,sha256=-sTFmQMycWEjQ-otXyl7qZIBC7aqIO8cAsVwsoWZy5w,136
|
|
2
|
+
termcore/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
3
|
+
termcore/cli/__init__.py,sha256=dDze7NMCAvbq5np9_JxU1SgpJ0FGjb07CakKHG6CtXo,82
|
|
4
|
+
termcore/cli/output.py,sha256=Fiy_WKSPNj_pMQaoRzn5IFukDe-OhSAqQpmA22kROX0,1918
|
|
5
|
+
termcore/io/__init__.py,sha256=qs-Qpc6Ftwrlo4hty_0sUy4IbVXHtQ-jWQKcf5AChr8,187
|
|
6
|
+
termcore/io/app_state_storage.py,sha256=BG7Ff-H8XyCO-DVyOVJ-u0gQGFDxDlRcyUsavql5OO8,6667
|
|
7
|
+
termcore/io/database.py,sha256=lpWeLa7YQlcaG6ryMK3Dd-albG-YcDxD9E8amWkHeLE,21209
|
|
8
|
+
termcore/io/errors.py,sha256=VyUBqum8JyignWz-39VyVgon9fDKc-mRbmq9qWCI41w,2262
|
|
9
|
+
termcore/io/file.py,sha256=LStUYFEO_CkkObvDJTlYyS0OdVikQ1_4yBskBLOSJ88,8057
|
|
10
|
+
termcore/io/textfile.py,sha256=bDGFz1vDEiVFSZqkXNtVoKNqWbAfQ9Uowk-oWkwgqR8,2777
|
|
11
|
+
termcore/tui/__init__.py,sha256=1zQioSa_GTD9CDI0oe1AXJBuWWbcfFSCqnU0pgbn2Dw,343
|
|
12
|
+
termcore/tui/custom_bindings.py,sha256=ANLGSh1cPRgIb1ryxn_jHTe8iAOEMRuzChtyS1bJ2t4,19589
|
|
13
|
+
termcore/tui/question_screen.py,sha256=Y4r5PokHgK0o_s_3kZub0LbK5wrvbzXzxf0f7NJbbQk,3135
|
|
14
|
+
termcore/tui/theme_loader.py,sha256=LiseGo5X215lqDsYmRRqJuqhhQPOEcsXwgI8yv6VdfQ,17472
|
|
15
|
+
termcore/tui/custom_widgets/__init__.py,sha256=yCe0Ve2_vY8Mk-JXXKYEyfRsGHkJz8SBSnhKzmhBLqQ,75
|
|
16
|
+
termcore/tui/custom_widgets/custom_checkbox.py,sha256=C3ZHCdYWUCHx9m0yFY3GRBAI8BB98PGV8l8DgzOgoZM,1214
|
|
17
|
+
termcore/tui/custom_widgets/custom_data_table.py,sha256=ZRSB8fiwNr7yYFleeztCqqw3FntF1Y5l7hTROTs_jzI,8767
|
|
18
|
+
termcore/tui/custom_widgets/custom_selection.py,sha256=cIxYdGuqQ8b2EfJe7wEtXScGh_w6FCRpYs3MMuwJBA0,2648
|
|
19
|
+
termcore/tui/custom_widgets/multiline_footer.py,sha256=RE3CUNh1Q_JBjaCuZBHwE5LytUect5N3xmjM9k_29Nc,8221
|
|
20
|
+
termcore/tui/themes/__init__.py,sha256=LApCQM6Ho8FULZB2-1wVq4i5AgkIzFLRr4qqQdV-xJo,63
|
|
21
|
+
termcore/tui/themes/classic-black-saturated/style.css,sha256=Z01O6mC_f2oKjs1Fi_NsujtjEqqvdOzWnHJMut67wO8,1419
|
|
22
|
+
termcore/tui/themes/classic-black-saturated/theme.py,sha256=9LQgbhCo25Rh7J601OEvYU_nXf_ieoJpnPh9aJPfhbg,520
|
|
23
|
+
termcore/tui/themes/classic-black-v1/style.css,sha256=Z01O6mC_f2oKjs1Fi_NsujtjEqqvdOzWnHJMut67wO8,1419
|
|
24
|
+
termcore/tui/themes/classic-black-v1/theme.py,sha256=9DbXpgAI_wpRgiXSxvvyi7UTz7BsoAsLZY7_UW47PuQ,513
|
|
25
|
+
termcore/tui/themes/classic-black-v2/style.css,sha256=Z01O6mC_f2oKjs1Fi_NsujtjEqqvdOzWnHJMut67wO8,1419
|
|
26
|
+
termcore/tui/themes/classic-black-v2/theme.py,sha256=83YdFDM-TvpDcumtzHcc4Z9tzQPmszRy-Vr0gAGF_y8,605
|
|
27
|
+
termcore/tui/themes/classic-blue/theme.py,sha256=bdSptsI2gyKfd1HGl2liwJVI9vhPpsnPr6wXwp9NDhs,494
|
|
28
|
+
termcore/tui/themes/compact-gray/style.tcss,sha256=ENdB4UGVRzC8hkGR0RPppBctV7vQoHTc223RL_A9ZbA,3050
|
|
29
|
+
termcore/tui/themes/compact-gray/theme.py,sha256=gnj9voHh_NiRllwA4kwg37zX6-_NfHtUSgv8sSXtp6o,509
|
|
30
|
+
termcore/tui/themes/mnml-black/style.css,sha256=Gl3kwzTWmJL6XEIlomJS4rq7g7L1gzdK_P9u2teVowA,1398
|
|
31
|
+
termcore/tui/themes/mnml-black/theme.py,sha256=CD-VvI6_nKLNO2aetPKLXehwZfZB64Kj2xxUTNrxcQ0,462
|
|
32
|
+
termcore/tui/themes/mnml-deepblack/style.css,sha256=Gl3kwzTWmJL6XEIlomJS4rq7g7L1gzdK_P9u2teVowA,1398
|
|
33
|
+
termcore/tui/themes/mnml-deepblack/theme.py,sha256=dqg6qR0I-tn9nt3Pm9ulTiaf7dZXBUomvGyp0P9GcqA,466
|
|
34
|
+
termcore/tui/themes/pure-amber/theme.py,sha256=XxF7d0ckdUC__MHSvn5tXRdXFQzVVdLOTczKiSwFPXI,351
|
|
35
|
+
termcore/tui/themes/pure-black/theme.py,sha256=rYC4xMW6OvUsDzBOiR4v8wAfah3VXmcDZc9-pGaJ4PE,351
|
|
36
|
+
termcore/tui/themes/pure-blue/theme.py,sha256=pFvYV71YBFcSUW4uj5uSMsOxGhgKbopOoU95D2fArYw,350
|
|
37
|
+
termcore/tui/themes/pure-green/theme.py,sha256=irtXe99nsAjvkjiNyO-06x5Lfh_PyHPsdjlPzzEl7J4,351
|
|
38
|
+
termcore/tui/themes/pure-sweet16/theme.py,sha256=ZZ1-k5rrDdwwNRuYZHlpUzI4esHHTdZPJBIKkCfUQQQ,435
|
|
39
|
+
termcore/tui/themes/xplore-black/theme.py,sha256=JBN2UHZ1ftiniSxX4uhP2gtj1JV2dZhNCytxwteHHeU,353
|
|
40
|
+
termcore/tui/themes/xplore-blue/theme.py,sha256=Qahy0y371Z1md2lh8DkZQv_a6yQ7ma9lU5-Ko0AxAVM,352
|
|
41
|
+
termcore/tui/themes/xplore-blue-muted/theme.py,sha256=muzAP6q1eMCrkDlqi_YWRbX9rtCrcsPV_dm7EJM5FVI,358
|
|
42
|
+
termcore/tui/themes/xplore-teal/theme.py,sha256=_A3ecWsc6uScbEUAPVOixh0O2afo3jUkNa7HRwQ6HaQ,352
|
|
43
|
+
termcore/util/__init__.py,sha256=wnUtySdSvNp_nE763n2qnRiqZi8scXKYgU65MIHwikA,247
|
|
44
|
+
termcore/util/datetime.py,sha256=Ul-6GL6U2ECqsQdXrMI26ZCLoiK1IAY69qR0bAC1jzc,5047
|
|
45
|
+
termcore/util/debug.py,sha256=a1GUzvrv7IcCxtJKOw4GPiuEVl2J8JBL2wKHS-21ZHA,4891
|
|
46
|
+
termcore/util/index.py,sha256=14fobhL1sak4gtf-MfIL6_j__x2CmVUBjOtpvHlwn6c,2970
|
|
47
|
+
termcore/util/logger.py,sha256=JPV9FPa-sMOF_0ZAsR7d0RWEOwUa5c0wW9b0PouzyRw,973
|
|
48
|
+
termcore/util/singleton.py,sha256=UrrsmPKnVdup0H4ht_FXcpGIS-YpYuwy-YOC5WOcfjc,1384
|
|
49
|
+
termcore/util/string.py,sha256=dcKhc05EnvNlRkFSsRm0HAVgtfDnDQjzbM9sEQCwOzs,7506
|
|
50
|
+
termcore/util/validation.py,sha256=eTMCExnyh4loFuLaIWQSWWzOz3r0CZrVNEavrwYmhYc,570
|
|
51
|
+
termcore/util/version.py,sha256=GVWpw_m947CS-7FZG6suSPSWM_Nz8YTCO0yI8_4xIns,1688
|
|
52
|
+
termcore-0.2.0.dist-info/METADATA,sha256=lLyNV07DAYjd6Ljg6qqBr3oZsitTOtFRxEGIGpNmHZY,16336
|
|
53
|
+
termcore-0.2.0.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
|
|
54
|
+
termcore-0.2.0.dist-info/licenses/LICENSE,sha256=KGU4zmBB5SJOxn_dfYTxKVB-tRV94wl3vVLe8HiQ_X8,1072
|
|
55
|
+
termcore-0.2.0.dist-info/RECORD,,
|
|
@@ -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.
|