termcore 0.2.0__tar.gz → 0.4.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.
- {termcore-0.2.0 → termcore-0.4.0}/CHANGELOG.md +62 -1
- {termcore-0.2.0 → termcore-0.4.0}/CLAUDE.md +5 -4
- {termcore-0.2.0 → termcore-0.4.0}/PKG-INFO +175 -47
- {termcore-0.2.0 → termcore-0.4.0}/README.md +173 -45
- {termcore-0.2.0 → termcore-0.4.0}/TODO.md +13 -13
- {termcore-0.2.0 → termcore-0.4.0}/docs/CLEAN-UP.md +4 -4
- {termcore-0.2.0 → termcore-0.4.0}/docs/DEVELOPMENT.md +45 -6
- {termcore-0.2.0 → termcore-0.4.0}/pyproject.toml +15 -11
- {termcore-0.2.0 → termcore-0.4.0}/termcore/cli/__init__.py +1 -0
- termcore-0.4.0/termcore/cli/cli_theme.py +100 -0
- {termcore-0.2.0 → termcore-0.4.0}/termcore/cli/output.py +14 -10
- {termcore-0.2.0 → termcore-0.4.0}/termcore/io/app_state_storage.py +6 -2
- {termcore-0.2.0 → termcore-0.4.0}/termcore/io/textfile.py +67 -1
- termcore-0.4.0/termcore/tui/__init__.py +18 -0
- termcore-0.4.0/termcore/tui/binding_groups.py +129 -0
- termcore-0.4.0/termcore/tui/custom_widgets/app_header.py +197 -0
- termcore-0.4.0/termcore/tui/custom_widgets/footer_rows.py +288 -0
- termcore-0.4.0/termcore/tui/custom_widgets/multiline_footer.py +285 -0
- termcore-0.4.0/termcore/tui/custom_widgets/status_bar.py +105 -0
- termcore-0.4.0/termcore/tui/custom_widgets/tab_rows.py +166 -0
- termcore-0.4.0/termcore/tui/help_rows.py +278 -0
- termcore-0.4.0/termcore/tui/help_screen.py +218 -0
- termcore-0.4.0/termcore/tui/keymap.py +792 -0
- {termcore-0.2.0 → termcore-0.4.0}/termcore/tui/theme_loader.py +65 -4
- termcore-0.4.0/termcore/tui/theme_overrides.py +158 -0
- {termcore-0.2.0 → termcore-0.4.0}/termcore/tui/themes/classic-black-saturated/style.css +1 -1
- {termcore-0.2.0 → termcore-0.4.0}/termcore/tui/themes/classic-black-saturated/theme.py +1 -0
- {termcore-0.2.0/termcore/tui/themes/classic-black-v2 → termcore-0.4.0/termcore/tui/themes/classic-black-v1}/style.css +1 -1
- {termcore-0.2.0 → termcore-0.4.0}/termcore/tui/themes/classic-black-v1/theme.py +1 -0
- {termcore-0.2.0/termcore/tui/themes/classic-black-v1 → termcore-0.4.0/termcore/tui/themes/classic-black-v2}/style.css +1 -1
- {termcore-0.2.0 → termcore-0.4.0}/termcore/tui/themes/classic-black-v2/theme.py +1 -0
- {termcore-0.2.0 → termcore-0.4.0}/termcore/tui/themes/compact-gray/style.tcss +2 -2
- {termcore-0.2.0 → termcore-0.4.0}/termcore/tui/themes/compact-gray/theme.py +3 -1
- {termcore-0.2.0/termcore/tui/themes/mnml-deepblack → termcore-0.4.0/termcore/tui/themes/mnml-black}/style.css +1 -1
- {termcore-0.2.0 → termcore-0.4.0}/termcore/tui/themes/mnml-black/theme.py +1 -0
- {termcore-0.2.0/termcore/tui/themes/mnml-black → termcore-0.4.0/termcore/tui/themes/mnml-deepblack}/style.css +1 -1
- {termcore-0.2.0 → termcore-0.4.0}/termcore/tui/themes/mnml-deepblack/theme.py +1 -0
- termcore-0.4.0/termcore/util/logger.py +89 -0
- termcore-0.4.0/tests/cli/test_cli_theme.py +57 -0
- {termcore-0.2.0 → termcore-0.4.0}/tests/io/test_database.py +4 -2
- termcore-0.4.0/tests/io/test_textfile.py +196 -0
- {termcore-0.2.0 → termcore-0.4.0}/tests/test_docs.py +5 -3
- {termcore-0.2.0 → termcore-0.4.0}/tests/test_public_api.py +16 -4
- termcore-0.4.0/tests/test_theme_only_colours.py +154 -0
- {termcore-0.2.0 → termcore-0.4.0}/tests/tui/conftest.py +17 -0
- termcore-0.4.0/tests/tui/test_app_header.py +313 -0
- termcore-0.4.0/tests/tui/test_footer_rows.py +289 -0
- termcore-0.4.0/tests/tui/test_help_rows.py +284 -0
- termcore-0.4.0/tests/tui/test_help_screen.py +347 -0
- termcore-0.4.0/tests/tui/test_keymap.py +881 -0
- termcore-0.4.0/tests/tui/test_multiline_footer.py +288 -0
- termcore-0.4.0/tests/tui/test_status_bar.py +186 -0
- termcore-0.4.0/tests/tui/test_tab_rows.py +126 -0
- {termcore-0.2.0 → termcore-0.4.0}/tests/tui/test_theme_loader.py +8 -4
- termcore-0.4.0/tests/tui/test_theme_overrides.py +102 -0
- termcore-0.4.0/tests/util/__init__.py +0 -0
- {termcore-0.2.0 → termcore-0.4.0}/tests/util/test_datetime.py +4 -2
- {termcore-0.2.0 → termcore-0.4.0}/tests/util/test_index.py +2 -1
- {termcore-0.2.0 → termcore-0.4.0}/tests/util/test_string.py +2 -1
- termcore-0.4.0/uv.lock +430 -0
- termcore-0.2.0/termcore/tui/__init__.py +0 -9
- termcore-0.2.0/termcore/tui/custom_bindings.py +0 -551
- termcore-0.2.0/termcore/tui/custom_widgets/multiline_footer.py +0 -240
- termcore-0.2.0/termcore/util/logger.py +0 -36
- termcore-0.2.0/tests/tui/test_custom_bindings.py +0 -575
- {termcore-0.2.0 → termcore-0.4.0}/.gitignore +0 -0
- {termcore-0.2.0 → termcore-0.4.0}/LICENSE +0 -0
- {termcore-0.2.0 → termcore-0.4.0}/termcore/__init__.py +0 -0
- {termcore-0.2.0 → termcore-0.4.0}/termcore/io/__init__.py +0 -0
- {termcore-0.2.0 → termcore-0.4.0}/termcore/io/database.py +0 -0
- {termcore-0.2.0 → termcore-0.4.0}/termcore/io/errors.py +0 -0
- {termcore-0.2.0 → termcore-0.4.0}/termcore/io/file.py +0 -0
- {termcore-0.2.0 → termcore-0.4.0}/termcore/py.typed +0 -0
- {termcore-0.2.0 → termcore-0.4.0}/termcore/tui/custom_widgets/__init__.py +0 -0
- {termcore-0.2.0 → termcore-0.4.0}/termcore/tui/custom_widgets/custom_checkbox.py +0 -0
- {termcore-0.2.0 → termcore-0.4.0}/termcore/tui/custom_widgets/custom_data_table.py +0 -0
- {termcore-0.2.0 → termcore-0.4.0}/termcore/tui/custom_widgets/custom_selection.py +0 -0
- {termcore-0.2.0 → termcore-0.4.0}/termcore/tui/question_screen.py +0 -0
- {termcore-0.2.0 → termcore-0.4.0}/termcore/tui/themes/__init__.py +0 -0
- {termcore-0.2.0 → termcore-0.4.0}/termcore/tui/themes/classic-blue/theme.py +0 -0
- {termcore-0.2.0 → termcore-0.4.0}/termcore/tui/themes/pure-amber/theme.py +0 -0
- {termcore-0.2.0 → termcore-0.4.0}/termcore/tui/themes/pure-black/theme.py +0 -0
- {termcore-0.2.0 → termcore-0.4.0}/termcore/tui/themes/pure-blue/theme.py +0 -0
- {termcore-0.2.0 → termcore-0.4.0}/termcore/tui/themes/pure-green/theme.py +0 -0
- {termcore-0.2.0 → termcore-0.4.0}/termcore/tui/themes/pure-sweet16/theme.py +0 -0
- {termcore-0.2.0 → termcore-0.4.0}/termcore/tui/themes/xplore-black/theme.py +0 -0
- {termcore-0.2.0 → termcore-0.4.0}/termcore/tui/themes/xplore-blue/theme.py +0 -0
- {termcore-0.2.0 → termcore-0.4.0}/termcore/tui/themes/xplore-blue-muted/theme.py +0 -0
- {termcore-0.2.0 → termcore-0.4.0}/termcore/tui/themes/xplore-teal/theme.py +0 -0
- {termcore-0.2.0 → termcore-0.4.0}/termcore/util/__init__.py +0 -0
- {termcore-0.2.0 → termcore-0.4.0}/termcore/util/datetime.py +0 -0
- {termcore-0.2.0 → termcore-0.4.0}/termcore/util/debug.py +0 -0
- {termcore-0.2.0 → termcore-0.4.0}/termcore/util/index.py +0 -0
- {termcore-0.2.0 → termcore-0.4.0}/termcore/util/singleton.py +0 -0
- {termcore-0.2.0 → termcore-0.4.0}/termcore/util/string.py +0 -0
- {termcore-0.2.0 → termcore-0.4.0}/termcore/util/validation.py +0 -0
- {termcore-0.2.0 → termcore-0.4.0}/termcore/util/version.py +0 -0
- {termcore-0.2.0 → termcore-0.4.0}/tests/__init__.py +0 -0
- {termcore-0.2.0/tests/io → termcore-0.4.0/tests/cli}/__init__.py +0 -0
- {termcore-0.2.0/tests/tui → termcore-0.4.0/tests/io}/__init__.py +0 -0
- {termcore-0.2.0 → termcore-0.4.0}/tests/io/conftest.py +0 -0
- {termcore-0.2.0/tests/util → termcore-0.4.0/tests/tui}/__init__.py +0 -0
|
@@ -4,6 +4,63 @@ All notable changes to this project will be documented in this file.
|
|
|
4
4
|
|
|
5
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
6
|
|
|
7
|
+
## [Unreleased]
|
|
8
|
+
|
|
9
|
+
## [0.4.0] – 2026-11-06
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- `tui/theme_overrides.py`: `apply_color_overrides`, which layers single colours from an application's config over a theme. A base colour (`primary`, `surface`, `error`, ...) replaces the theme's field, any other name the CSS variable Textual generates under it (`footer-background`, `text-muted`, ...). An unknown name or an unparseable value is logged and skipped, so a typo does not stop the app; `color_override_problems` returns a message for each so the application can show it, and `known_color_names` and `unknown_color_names` list the valid and the unknown names
|
|
14
|
+
- `ThemeLoader.set_color_overrides`, which applies those overrides to every theme the loader registers, Textual's built-in ones included, so they survive a theme switch. Without overrides the built-in themes stay as Textual ships them
|
|
15
|
+
- `ThemeLoader.get_theme`, returning a registered or built-in theme by name with the overrides applied, without a running app
|
|
16
|
+
- `cli/cli_theme.py`: `CliTheme`, the colours of the four message panels, with `set_cli_theme` and `cli_theme_from_textual`. A CLI that hands it the TUI's theme shows errors, warnings, successes and notes in that theme's colours; the defaults keep the previous red, yellow, green and cyan
|
|
17
|
+
- `tests/test_theme_only_colours.py`, which fails on a colour value anywhere in the package outside the themes' `theme.py` files and `cli/cli_theme.py`: in Python strings, `DEFAULT_CSS` blocks and stylesheets alike. Textual's and Rich's own parsers decide what a colour is
|
|
18
|
+
|
|
19
|
+
### Changed
|
|
20
|
+
|
|
21
|
+
- **Breaking.** `CustomBindings` is now `Keymap`, and the module `tui/custom_bindings.py` is now `tui/keymap.py`. The old name described the class by its relation to Textual's own `BINDINGS` rather than by what it does: it is the map from a scope and key to a binding
|
|
22
|
+
- `print_error`, `print_warning`, `print_success` and `print_info` read their colour from the active `CliTheme` instead of a fixed one; the look is unchanged until an application sets a theme
|
|
23
|
+
- The six bundled themes with a stylesheet (`classic-black-v1`, `classic-black-v2`, `classic-black-saturated`, `compact-gray`, `mnml-black`, `mnml-deepblack`) no longer hold a colour value in it. The cursor colour is the new theme variable `input-cursor-foreground`, so a config override reaches it too
|
|
24
|
+
- `compact-gray` draws the text of an `Option` in the theme's `$error` instead of a fixed `red`, which is a slightly different red
|
|
25
|
+
|
|
26
|
+
## [0.3.0] – 2026-07-22
|
|
27
|
+
|
|
28
|
+
### Added
|
|
29
|
+
|
|
30
|
+
- `Textfile.write_atomic`, which writes through a synced temporary file and a rename. `AppStateStorage`, `ThemeLoader` and any consumer rewriting a file whole now keep their old content when a write is interrupted; a plain write truncates first, so a crash between truncating and writing loses everything rather than the last change
|
|
31
|
+
- `setup_logging` takes a `log_dir`, so an application that resolves its own paths does not have a second resolver behind its back. Its default now follows `XDG_STATE_HOME` instead of hard-coding `~/.local/state`
|
|
32
|
+
- `AppHeader` posts `TabSelected` when a tab label is clicked. The header does not switch anything itself - it does not own the content - and only inactive tabs carry the click, because Textual restyles anything clickable with its link colour and that would flatten the active tab to the same grey as the rest
|
|
33
|
+
|
|
34
|
+
- `tui/binding_groups.py`: `BindingGroup`, the labelled row of shortcuts that the footer and the help overlay both render from, plus `active_actions` to snapshot what is bound on a screen and `dispatch_name` to strip the `app.` prefix a Screen adds
|
|
35
|
+
- `CustomBindings.get_groups`, returning every declared group in the order the YAML file declares it. It replaces both row maps, and the defect they invited disappears with them: there is no longer a pair of calls that can be mismatched
|
|
36
|
+
- A `group` entry in the binding YAML. A scope may now hold groups, single bindings, or both; consecutive single bindings share one unlabelled row. Grouping is optional and adds no ceremony where it is not wanted
|
|
37
|
+
- `tui/help_screen.py`: `HelpScreen`, the overlay section 1.8 of the style guide asks for - every shortcut, grouped, with a fuzzy search over them and a `ctrl+t` toggle between all bindings and the ones currently active. Matching uses Textual's own `Matcher` rather than a hand-written one
|
|
38
|
+
- `tui/help_rows.py`: the filtering and row order behind the overlay, testable without an application
|
|
39
|
+
- A second file of scope display names, passed to `CustomBindings` as `scopes_file`. Scope names such as `tasks_tab` are identifiers; this maps them to the words people read. `scope_title` resolves one, `BindingGroup.scope_title` carries it, and a title naming no scope is reported - nothing else would ever notice it
|
|
40
|
+
- `display_scope`, which falls back to the raw scope name where no title was declared. The fallback is meant to look unfinished rather than invent a heading nobody chose
|
|
41
|
+
- `tui/custom_widgets/footer_rows.py`: the footer's arithmetic - label column width, key width and wrapping - split out so it can be tested without an application and so `multiline_footer.py` stays inside the file-size limit
|
|
42
|
+
- `tui/custom_widgets/app_header.py`: `AppHeader`, a header carrying the application name in its own column and the tabs after it, wrapping onto further rows whose brand cell is blank but as wide. Textual's `Header` is fixed at one line (three with `-tall`) with three hard-wired slots, and `Tabs` scrolls rather than wraps, so neither could serve
|
|
43
|
+
- `tui/custom_widgets/tab_rows.py`: the header's packing, testable without an application
|
|
44
|
+
- `tui/custom_widgets/status_bar.py`: `StatusBar`, one line with standing information on the left and a passing message on the right. Textual has no status widget; `App.notify` covers only the passing half, as a box that expires on a timer
|
|
45
|
+
- A `key` field in the scopes file, and `scope_key`, `get_tab_scopes` and `get_tab_bindings` to read it. Declaring a key is what makes a scope a tab: the key selects it, the header prints it, and the order of the file is the order of the bar
|
|
46
|
+
- The first tests for `MultiLineFooter`, which had none: row order, separator count, label alignment, wrapping at a narrow width, and that clicking a key still runs its action
|
|
47
|
+
|
|
48
|
+
### Changed
|
|
49
|
+
|
|
50
|
+
- **Breaking.** The `row` field is gone, and with it `CustomBindings.get_row_map`, `get_screen_row_map` and the `row_map` parameter of `MultiLineFooter`. A group is now one row, so a second way to assign rows could only contradict the first
|
|
51
|
+
- **Breaking.** `CustomBindings.sorted_by_key` is gone, along with the alphabetical sorting it enabled. The order of the YAML file is now the order of the footer, without exception
|
|
52
|
+
- **Breaking.** `MultiLineFooter` no longer takes `auto_wrap`. Passing `groups` selects the grouped layout; omitting it wraps on width, which is what the old default did
|
|
53
|
+
- **Breaking.** `handle_check_action`'s keyword argument `active_group` is now `active_scope`. The word "group" now means a footer row throughout, and what it used to mean - a top-level key of the YAML file - is called a scope
|
|
54
|
+
- The help overlay groups on two levels: the scope heads the section, the group is the level below it, and a scope holding a single group prints no group heading at all. The footer keeps its one-dimensional layout - the two surfaces answer different questions, which is why clibase gives them separate tables as well
|
|
55
|
+
- **Breaking.** The scopes file maps each scope to a mapping of `title` and optional `key`, where it previously mapped straight to the title. One shape covers both a plain heading and a tab
|
|
56
|
+
- **Breaking.** `HelpScope` is now `HelpCoverage` and `HelpRequest.scope` is `coverage`. With the overlay grouping by scope, the old name meant two things at once, which is exactly the ambiguity the group/scope split had just removed
|
|
57
|
+
- The footer prints group labels in a left column of shared width, keys separated by ` · `. A group too wide to fit wraps onto continuation rows whose label cell stays blank, so the keys align under the keys rather than under the label
|
|
58
|
+
- The command palette key's width is now reserved, so the last row can no longer wrap into the space it is docked in
|
|
59
|
+
- `setup_logging` installs a file handler only, and now says so. Its docstring claimed "file and console logging" while installing neither a console handler nor wanting one: a console handler writes into the terminal the application is drawing on
|
|
60
|
+
- The docstring layout rule of style guide 3.2.4 is enforced in the test suite as well. `tests/**` ignored the whole `D` group, so the one rule that is about layout rather than about requiring a docstring was off there too; the ignore is now `D1`
|
|
61
|
+
- Text fields are read strictly: a `key`, `action` or `description` that YAML resolved to a boolean is dropped with a warning naming the fix, instead of rendering `False` as a label
|
|
62
|
+
- Widths are measured in terminal cells rather than characters, so a CJK label no longer pushes the footer out of alignment
|
|
63
|
+
|
|
7
64
|
## [0.2.0] – 2026-07-19
|
|
8
65
|
|
|
9
66
|
### Added
|
|
@@ -92,4 +149,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
|
92
149
|
|
|
93
150
|
*PyPI name reservation – no functional code.*
|
|
94
151
|
|
|
95
|
-
[0.
|
|
152
|
+
[0.4.0]: https://github.com/cgroening/py-termcore/releases/tag/v0.4.0
|
|
153
|
+
[0.3.0]: https://github.com/cgroening/py-termcore/compare/v0.2.0...v0.3.0
|
|
154
|
+
[0.2.0]: https://github.com/cgroening/py-termcore/compare/v0.1.1...v0.2.0
|
|
155
|
+
[0.1.1]: https://github.com/cgroening/py-termcore/compare/v0.1.0...v0.1.1
|
|
156
|
+
[0.1.0]: https://github.com/cgroening/py-termcore/releases/tag/v0.1.0
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
# CLAUDE.md
|
|
2
2
|
|
|
3
|
-
Project-specific instructions for termcore.
|
|
3
|
+
Project-specific instructions for termcore. This file records what is specific to this repository.
|
|
4
4
|
|
|
5
5
|
## What this project is
|
|
6
6
|
|
|
7
|
-
termcore is a library, not an application. It is the foundation under termplate,
|
|
7
|
+
termcore is a library, not an application. It is the foundation under termplate, the template every Textual-based TUI is checked against.
|
|
8
8
|
|
|
9
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
10
|
|
|
@@ -37,17 +37,18 @@ The public API of each package is re-exported through `from .module import *` in
|
|
|
37
37
|
|
|
38
38
|
## The defect this repository keeps producing
|
|
39
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 `
|
|
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 `Keymap`, `CustomDataTable.flexible_columns` and `AppStateStorage._json_dict`. Each time the symptom was different and none of them raised.
|
|
41
41
|
|
|
42
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
43
|
|
|
44
44
|
## Conventions particular to this repo
|
|
45
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
|
|
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`) or an enum (`DateFormat`, `HelpScope`). Pick whichever fits the parameter; a format selector is an enum, a behaviour switch is two functions. A fourth answer appeared with `MultiLineFooter(groups=...)`: where the two behaviours differ only in whether data was supplied, the presence of that data selects the mode and no extra parameter is needed.
|
|
47
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
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
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
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
|
+
- Every colour comes from a theme. Colour values live only in the themes' `theme.py` files and in the defaults of `termcore/cli/cli_theme.py`; stylesheets and `DEFAULT_CSS` use `$variables`, and a colour Textual has no variable for becomes a theme variable. `tests/test_theme_only_colours.py` enforces it. Single colours a user changes come in through `ThemeLoader.set_color_overrides`, never through a new code path.
|
|
51
52
|
- 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
53
|
- 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
54
|
- 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.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: termcore
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: Terminal utilities for CLI, TUI, IO and general use
|
|
5
5
|
Project-URL: Homepage, https://github.com/cgroening/py-termcore
|
|
6
6
|
Project-URL: Repository, https://github.com/cgroening/py-termcore
|
|
@@ -76,15 +76,27 @@ clear_lines(4)
|
|
|
76
76
|
|
|
77
77
|
| Function | Description |
|
|
78
78
|
|----------|-------------|
|
|
79
|
-
| `print_error(message)` |
|
|
80
|
-
| `print_warning(message)` |
|
|
81
|
-
| `print_success(message)` |
|
|
82
|
-
| `print_info(message)` |
|
|
79
|
+
| `print_error(message)` | Error panel (theme colour, red by default) with ✗ prefix |
|
|
80
|
+
| `print_warning(message)` | Warning panel (yellow by default) with ⚠ prefix |
|
|
81
|
+
| `print_success(message)` | Success panel (green by default) with ✓ prefix |
|
|
82
|
+
| `print_info(message)` | Info panel (cyan by default) with ℹ prefix |
|
|
83
83
|
| `print_panel(message, color)` | Panel with bold colored text |
|
|
84
84
|
| `print_custom_panel(formatted_message, panel_color)` | Panel with pre-formatted Rich markup |
|
|
85
85
|
| `clear_lines(count)` | Move cursor up `count` lines and clear everything below |
|
|
86
86
|
| `get_console()` | Returns the shared Rich `Console` instance |
|
|
87
87
|
|
|
88
|
+
### Panel colours
|
|
89
|
+
|
|
90
|
+
The four message panels take their colours from a `CliTheme` (defaults: red, yellow, green, cyan). An application that also has a TUI hands the CLI the TUI's theme, so both show the same colours:
|
|
91
|
+
|
|
92
|
+
```python
|
|
93
|
+
from termcore import ThemeLoader, cli_theme_from_textual, set_cli_theme
|
|
94
|
+
|
|
95
|
+
theme = ThemeLoader("themes").get_theme("TERMCORE_xplore-blue")
|
|
96
|
+
if theme is not None:
|
|
97
|
+
set_cli_theme(cli_theme_from_textual(theme)) # error, warning, success, primary
|
|
98
|
+
```
|
|
99
|
+
|
|
88
100
|
## termcore.tui – Textual TUI Helpers
|
|
89
101
|
|
|
90
102
|
### ThemeLoader
|
|
@@ -127,6 +139,20 @@ Theme name prefixes:
|
|
|
127
139
|
|
|
128
140
|
Both prefixes can be customized via the `ThemeLoader` constructor.
|
|
129
141
|
|
|
142
|
+
### Single colours from the config
|
|
143
|
+
|
|
144
|
+
A user who likes a theme but wants one colour different sets that colour instead of writing a theme. `set_color_overrides` takes `name -> colour` and layers it over every theme the loader registers, Textual's built-in ones included, so the change survives a theme switch:
|
|
145
|
+
|
|
146
|
+
```python
|
|
147
|
+
loader.set_color_overrides({
|
|
148
|
+
"primary": "#ff8800", # a base colour of the theme
|
|
149
|
+
"footer-background": "#101010", # any variable Textual generates
|
|
150
|
+
})
|
|
151
|
+
loader.register_themes_in_textual_app(app)
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
The names are a theme's base colours (`primary`, `secondary`, `accent`, `foreground`, `background`, `surface`, `panel`, `boost`, `warning`, `error`, `success`) plus every CSS variable Textual derives from them (`known_color_names()` lists them for the installed Textual). An unknown name or a value Textual cannot parse is logged and skipped; `color_override_problems` returns a message for each, so an application can show them (`unknown_color_names` lists only the names). `get_theme(name)` returns a theme with the overrides applied, without a running app.
|
|
155
|
+
|
|
130
156
|
### QuestionScreen
|
|
131
157
|
|
|
132
158
|
A Textual `ModalScreen` that presents a yes/no dialog and returns a `bool`.
|
|
@@ -184,73 +210,174 @@ yield CustomSelectionList(
|
|
|
184
210
|
)
|
|
185
211
|
```
|
|
186
212
|
|
|
213
|
+
### AppHeader
|
|
214
|
+
|
|
215
|
+
The header every terminal interface needs: the application name in a column of its own, then the tabs with the key that selects each, wrapping onto further rows when they do not fit. Textual's `Header` cannot do it - it is fixed at `height: 1` (3 with `-tall`) with three hard-wired slots and a `nowrap` title - and neither can `Tabs`, which is `height: 2` with `overflow: hidden` and scrolls instead of wrapping.
|
|
216
|
+
|
|
217
|
+
```python
|
|
218
|
+
from termcore.tui.custom_widgets.app_header import AppHeader, HeaderTab
|
|
219
|
+
|
|
220
|
+
yield AppHeader(
|
|
221
|
+
"mdtask",
|
|
222
|
+
[HeaderTab(scope, bindings.scope_key(scope), bindings.scope_title(scope))
|
|
223
|
+
for scope in bindings.get_tab_scopes()],
|
|
224
|
+
active="tasks_tab",
|
|
225
|
+
)
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
```text
|
|
229
|
+
mdtask [1] Tasks │ [2] Software │ [3] Allgemein │ [4] Notes
|
|
230
|
+
[5] Quick Notes │ [6] Focus │ [7] Checklists
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
The continuation rows leave the brand cell blank but exactly as wide, so the tabs form a column. The active tab is bold at normal brightness and the others are dimmed; the separator is drawn only between tabs on the same row, never at either end of one. Set `active` to move the highlight and `set_tabs` to replace the bar.
|
|
234
|
+
|
|
235
|
+
`TabbedContent` keeps its panes - hide only its own bar with `TabbedContent > ContentTabs { display: none; }` and drive `TabbedContent.active` from the key bindings `Keymap.get_tab_bindings` builds.
|
|
236
|
+
|
|
237
|
+
### StatusBar
|
|
238
|
+
|
|
239
|
+
One line of status: standing information on the left, a passing message on the right. Textual has no status widget - `App.notify` covers only the passing half, and only as a box docked bottom right that expires on a timer.
|
|
240
|
+
|
|
241
|
+
```python
|
|
242
|
+
from termcore.tui.custom_widgets.status_bar import StatusBar
|
|
243
|
+
|
|
244
|
+
bar = self.query_one(StatusBar)
|
|
245
|
+
bar.info = "3 tasks · 1 done · 2 open"
|
|
246
|
+
bar.message = "theme: classic-black"
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
```text
|
|
250
|
+
3 tasks · 1 done · 2 open theme: classic-black
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
It does not dock itself, on purpose: Textual places everything docked to one edge in the same place, so a docked status bar would sit underneath the footer it belongs above. Put it last among the flowing widgets, above the docked footer, and give the content above it `height: 1fr`.
|
|
254
|
+
|
|
255
|
+
Clearing `message` is the caller's job. The convention is the next key press, which Textual dispatches before the key's own action, so an action is free to set a new message right afterwards:
|
|
256
|
+
|
|
257
|
+
```python
|
|
258
|
+
def on_key(self, _event: events.Key) -> None:
|
|
259
|
+
self.query_one(StatusBar).message = ""
|
|
260
|
+
```
|
|
261
|
+
|
|
187
262
|
### MultiLineFooter
|
|
188
263
|
|
|
189
|
-
A drop-in replacement for Textual's built-in `Footer` that supports multiple rows of key bindings.
|
|
264
|
+
A drop-in replacement for Textual's built-in `Footer` that supports multiple rows of key bindings. Which of its two modes applies follows from whether groups are passed:
|
|
190
265
|
|
|
191
|
-
-
|
|
192
|
-
-
|
|
266
|
+
- **without `groups`** – the keys wrap onto a new row whenever the current one is full.
|
|
267
|
+
- **with `groups`** – each declared group becomes one row, its label printed in a column on the left and its keys separated by ` · `. A group too wide for the terminal wraps onto continuation rows whose label cell stays blank, so the keys line up under the keys rather than under the label.
|
|
193
268
|
|
|
194
269
|
```python
|
|
270
|
+
from termcore.tui.keymap import Keymap
|
|
195
271
|
from termcore.tui.custom_widgets.multiline_footer import MultiLineFooter
|
|
196
272
|
|
|
197
|
-
|
|
273
|
+
bindings = Keymap("bindings.yaml")
|
|
274
|
+
|
|
275
|
+
# Wrap on width
|
|
198
276
|
yield MultiLineFooter()
|
|
199
277
|
|
|
200
|
-
#
|
|
201
|
-
yield MultiLineFooter(
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
},
|
|
209
|
-
)
|
|
278
|
+
# One row per group, in the order the YAML file declares them
|
|
279
|
+
yield MultiLineFooter(groups=bindings.get_groups())
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
```text
|
|
283
|
+
Tasks: a Add · d Done
|
|
284
|
+
Appearance: w Previous Theme · s Next Theme
|
|
285
|
+
App: ? Help · q Quit
|
|
210
286
|
```
|
|
211
287
|
|
|
212
|
-
|
|
288
|
+
`get_groups()` returns every declared group, not only the ones belonging to the current screen. A group whose bindings are not active right now renders no row at all, so the same call serves every screen.
|
|
289
|
+
|
|
290
|
+
### HelpScreen
|
|
291
|
+
|
|
292
|
+
The overlay every terminal interface needs: every shortcut, grouped, with a fuzzy search over them. It reads the same groups the footer does, so a binding cannot appear in one and be missing from the other.
|
|
293
|
+
|
|
294
|
+
Where the footer lays those groups out in one dimension, the overlay uses two. Its outer heading is the scope - the tab or screen a shortcut works on, which is what someone opening a help screen is asking about - and the group is the inner one. A scope holding a single group prints no group heading, since repeating "Tasks" under "Tasks" tells nobody anything.
|
|
295
|
+
|
|
296
|
+
```python
|
|
297
|
+
from termcore.tui.binding_groups import active_actions
|
|
298
|
+
from termcore.tui.help_screen import HelpScreen
|
|
299
|
+
|
|
300
|
+
def action_help(self) -> None:
|
|
301
|
+
# Snapshot before the push: afterwards `self.screen` is the modal.
|
|
302
|
+
active = active_actions(self.screen)
|
|
303
|
+
self.push_screen(HelpScreen(bindings.get_groups(), active))
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
```text
|
|
307
|
+
TASKS
|
|
308
|
+
a Add
|
|
309
|
+
d Done
|
|
310
|
+
GLOBAL
|
|
311
|
+
Appearance
|
|
312
|
+
w Previous Theme
|
|
313
|
+
App
|
|
314
|
+
q Quit
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
It opens listing every declared shortcut. `ctrl+t` narrows the list to the bindings that would actually fire where the overlay was opened, which is what the snapshot above feeds; `escape` and `?` close it. Matching is Textual's own `Matcher`, the fuzzy search behind its command palette, so `ntm` finds "next theme".
|
|
318
|
+
|
|
319
|
+
### Keymap
|
|
213
320
|
|
|
214
321
|
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
322
|
|
|
216
|
-
**
|
|
323
|
+
Two words are kept apart and are not interchangeable. A **scope** is a top-level key of the file: it decides when a binding is visible and how its action is named. A **group** is one labelled footer row inside a scope: it decides only how the footer and the help overlay lay the bindings out. Grouping is optional.
|
|
217
324
|
|
|
218
|
-
|
|
219
|
-
|
|
325
|
+
**Scope naming conventions:**
|
|
326
|
+
|
|
327
|
+
| Scope name | Visibility | Action prefix |
|
|
328
|
+
|------------|------------|---------------|
|
|
220
329
|
| `_global` | Always visible | none (used as-is) |
|
|
221
330
|
| `<name>_tab` | Shown when that tab is active | `<name>_tab_` |
|
|
222
331
|
| `<name>_screen` | Shown on that screen | none (used as-is) |
|
|
223
332
|
|
|
333
|
+
The order of the file is the order of the footer, without exception. Nothing is re-sorted on the way out, so a scope moved in the file moves its rows - which is why `_global` usually belongs at the bottom.
|
|
334
|
+
|
|
224
335
|
**YAML example (`bindings.yaml`):**
|
|
225
336
|
|
|
226
337
|
```yaml
|
|
338
|
+
tasks_tab:
|
|
339
|
+
- group: Tasks
|
|
340
|
+
bindings:
|
|
341
|
+
- key: a
|
|
342
|
+
action: add_task
|
|
343
|
+
description: Add
|
|
344
|
+
- key: d
|
|
345
|
+
action: mark_done
|
|
346
|
+
description: Done
|
|
347
|
+
- key: z # no group: its own row, no label
|
|
348
|
+
action: zoom
|
|
349
|
+
description: Zoom
|
|
350
|
+
|
|
227
351
|
_global:
|
|
228
|
-
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
352
|
+
- group: App
|
|
353
|
+
bindings:
|
|
354
|
+
- key: q
|
|
355
|
+
action: quit
|
|
356
|
+
description: Quit
|
|
357
|
+
priority: true
|
|
358
|
+
```
|
|
233
359
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
360
|
+
An entry carrying `bindings` is a group; an entry carrying `key` is a single binding. Both may be mixed inside one scope, and consecutive single bindings share one unlabelled row.
|
|
361
|
+
|
|
362
|
+
**Scope display names (`scopes.yaml`):**
|
|
363
|
+
|
|
364
|
+
Scope names are identifiers, not labels. A second file maps them to the names people see - the headings of the help overlay, and in an app that reads them, the labels of the tabs:
|
|
239
365
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
row: 0
|
|
366
|
+
```yaml
|
|
367
|
+
tasks_tab: "Tasks"
|
|
368
|
+
add_screen: "Add dialog"
|
|
369
|
+
_global: "Global"
|
|
245
370
|
```
|
|
246
371
|
|
|
372
|
+
A scope with no entry there is shown under its raw name, which is meant to look unfinished rather than be quietly invented. A title naming no scope is reported, because nothing else would ever notice it. `bindings.scope_title(scope)` resolves one, and every `BindingGroup` carries the title of its own scope.
|
|
373
|
+
|
|
247
374
|
**Usage:**
|
|
248
375
|
|
|
249
376
|
```python
|
|
250
|
-
from termcore.tui.
|
|
377
|
+
from termcore.tui.keymap import Keymap
|
|
251
378
|
|
|
252
|
-
bindings =
|
|
253
|
-
bindings =
|
|
379
|
+
bindings = Keymap("bindings.yaml") # file order, always
|
|
380
|
+
bindings = Keymap("bindings.yaml", "scopes.yaml") # with titles
|
|
254
381
|
|
|
255
382
|
# In your App or Screen:
|
|
256
383
|
# On the App: globals dispatch as declared
|
|
@@ -261,13 +388,14 @@ BINDINGS = bindings.get_bindings(tab_name="tasks_tab") # one tab + global
|
|
|
261
388
|
BINDINGS = bindings.get_screen_bindings() # every tab + global
|
|
262
389
|
BINDINGS = bindings.get_screen_bindings("add") # that screen + global
|
|
263
390
|
|
|
264
|
-
#
|
|
265
|
-
|
|
266
|
-
row_map = bindings.get_screen_row_map() # pair with get_screen_bindings
|
|
391
|
+
# Footer rows and help overlay, both from the same call:
|
|
392
|
+
groups = bindings.get_groups()
|
|
267
393
|
|
|
268
394
|
# In check_action to hide tab bindings that don't belong to the active tab:
|
|
269
395
|
def check_action(self, action, parameters):
|
|
270
|
-
return bindings.handle_check_action(
|
|
396
|
+
return bindings.handle_check_action(
|
|
397
|
+
dispatch_name(action), parameters, active_scope=self.active_tab
|
|
398
|
+
)
|
|
271
399
|
```
|
|
272
400
|
|
|
273
401
|
## termcore.io – IO Utilities
|
|
@@ -492,7 +620,7 @@ uv venv
|
|
|
492
620
|
source .venv/bin/activate
|
|
493
621
|
uv pip install -e ".[dev]"
|
|
494
622
|
|
|
495
|
-
ruff check . # linting;
|
|
623
|
+
ruff check . # linting; rule set in pyproject.toml
|
|
496
624
|
basedpyright # strict type checking
|
|
497
625
|
pytest # tests, with coverage
|
|
498
626
|
```
|