hatui-kit 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- hatui_kit-0.1.0/.gitignore +8 -0
- hatui_kit-0.1.0/LICENSE +21 -0
- hatui_kit-0.1.0/PKG-INFO +154 -0
- hatui_kit-0.1.0/README.md +108 -0
- hatui_kit-0.1.0/pyproject.toml +59 -0
- hatui_kit-0.1.0/src/hatui/__init__.py +11 -0
- hatui_kit-0.1.0/src/hatui/__main__.py +5 -0
- hatui_kit-0.1.0/src/hatui/app.py +200 -0
- hatui_kit-0.1.0/src/hatui/core/__init__.py +1 -0
- hatui_kit-0.1.0/src/hatui/core/actions.py +46 -0
- hatui_kit-0.1.0/src/hatui/core/context.py +33 -0
- hatui_kit-0.1.0/src/hatui/core/input_manager.py +167 -0
- hatui_kit-0.1.0/src/hatui/core/screen_buffer.py +150 -0
- hatui_kit-0.1.0/src/hatui/core/style.py +381 -0
- hatui_kit-0.1.0/src/hatui/core/terminal_env.py +67 -0
- hatui_kit-0.1.0/src/hatui/core/util.py +0 -0
- hatui_kit-0.1.0/src/hatui/core/widget.py +203 -0
- hatui_kit-0.1.0/src/hatui/demo/__init__.py +1 -0
- hatui_kit-0.1.0/src/hatui/demo/screens/dashboard/modals/debug.yaml +72 -0
- hatui_kit-0.1.0/src/hatui/demo/screens/dashboard/modals/help.yaml +43 -0
- hatui_kit-0.1.0/src/hatui/demo/screens/dashboard/providers/phase8.yaml +429 -0
- hatui_kit-0.1.0/src/hatui/demo/screens/dashboard/providers.yaml +487 -0
- hatui_kit-0.1.0/src/hatui/demo/screens/dashboard/screen.yaml +64 -0
- hatui_kit-0.1.0/src/hatui/demo/screens/dashboard/tabs/analysis.yaml +65 -0
- hatui_kit-0.1.0/src/hatui/demo/screens/dashboard/tabs/forensics.yaml +71 -0
- hatui_kit-0.1.0/src/hatui/demo/screens/dashboard/tabs/hacker.yaml +80 -0
- hatui_kit-0.1.0/src/hatui/demo/screens/dashboard/tabs/logs.yaml +39 -0
- hatui_kit-0.1.0/src/hatui/demo/screens/dashboard/tabs/overview.yaml +180 -0
- hatui_kit-0.1.0/src/hatui/demo/screens/dashboard/tabs/visuals.yaml +84 -0
- hatui_kit-0.1.0/src/hatui/demo/screens/dashboard.yaml +166 -0
- hatui_kit-0.1.0/src/hatui/demo_assets.py +16 -0
- hatui_kit-0.1.0/src/hatui/providers/__init__.py +54 -0
- hatui_kit-0.1.0/src/hatui/providers/base.py +61 -0
- hatui_kit-0.1.0/src/hatui/providers/builtins.py +546 -0
- hatui_kit-0.1.0/src/hatui/providers/helpers.py +169 -0
- hatui_kit-0.1.0/src/hatui/runtime/__init__.py +24 -0
- hatui_kit-0.1.0/src/hatui/runtime/action_registry.py +102 -0
- hatui_kit-0.1.0/src/hatui/runtime/bindings.py +40 -0
- hatui_kit-0.1.0/src/hatui/runtime/bootstrap.py +94 -0
- hatui_kit-0.1.0/src/hatui/runtime/cli.py +66 -0
- hatui_kit-0.1.0/src/hatui/runtime/defaults.py +219 -0
- hatui_kit-0.1.0/src/hatui/runtime/engine.py +135 -0
- hatui_kit-0.1.0/src/hatui/runtime/formatters.py +65 -0
- hatui_kit-0.1.0/src/hatui/runtime/loader.py +140 -0
- hatui_kit-0.1.0/src/hatui/runtime/provider_manager.py +138 -0
- hatui_kit-0.1.0/src/hatui/runtime/registries.py +65 -0
- hatui_kit-0.1.0/src/hatui/runtime/render_policy.py +144 -0
- hatui_kit-0.1.0/src/hatui/runtime/router.py +74 -0
- hatui_kit-0.1.0/src/hatui/runtime/store.py +29 -0
- hatui_kit-0.1.0/src/hatui/runtime/validation.py +433 -0
- hatui_kit-0.1.0/src/hatui/runtime/watcher.py +167 -0
- hatui_kit-0.1.0/src/hatui/widgets/__init__.py +89 -0
- hatui_kit-0.1.0/src/hatui/widgets/alert_stack_widget.py +64 -0
- hatui_kit-0.1.0/src/hatui/widgets/alert_widget.py +83 -0
- hatui_kit-0.1.0/src/hatui/widgets/banner_widget.py +76 -0
- hatui_kit-0.1.0/src/hatui/widgets/border_widget.py +93 -0
- hatui_kit-0.1.0/src/hatui/widgets/box_widget.py +81 -0
- hatui_kit-0.1.0/src/hatui/widgets/center_widget.py +29 -0
- hatui_kit-0.1.0/src/hatui/widgets/chart_widget.py +222 -0
- hatui_kit-0.1.0/src/hatui/widgets/code_block_widget.py +91 -0
- hatui_kit-0.1.0/src/hatui/widgets/column_widget.py +48 -0
- hatui_kit-0.1.0/src/hatui/widgets/diff_viewer_widget.py +88 -0
- hatui_kit-0.1.0/src/hatui/widgets/divider_widget.py +66 -0
- hatui_kit-0.1.0/src/hatui/widgets/event_feed_widget.py +72 -0
- hatui_kit-0.1.0/src/hatui/widgets/flow_widget.py +77 -0
- hatui_kit-0.1.0/src/hatui/widgets/gauge_widget.py +109 -0
- hatui_kit-0.1.0/src/hatui/widgets/heatmap_widget.py +153 -0
- hatui_kit-0.1.0/src/hatui/widgets/hex_dump_widget.py +91 -0
- hatui_kit-0.1.0/src/hatui/widgets/histogram_widget.py +96 -0
- hatui_kit-0.1.0/src/hatui/widgets/inspector_widget.py +85 -0
- hatui_kit-0.1.0/src/hatui/widgets/kv_inspector_widget.py +134 -0
- hatui_kit-0.1.0/src/hatui/widgets/label_widget.py +99 -0
- hatui_kit-0.1.0/src/hatui/widgets/list_widget.py +185 -0
- hatui_kit-0.1.0/src/hatui/widgets/log_widget.py +106 -0
- hatui_kit-0.1.0/src/hatui/widgets/menu_widget.py +31 -0
- hatui_kit-0.1.0/src/hatui/widgets/metric_grid_widget.py +103 -0
- hatui_kit-0.1.0/src/hatui/widgets/mini_chart_widget.py +79 -0
- hatui_kit-0.1.0/src/hatui/widgets/modal_host_widget.py +85 -0
- hatui_kit-0.1.0/src/hatui/widgets/modal_widget.py +112 -0
- hatui_kit-0.1.0/src/hatui/widgets/paragraph_widget.py +94 -0
- hatui_kit-0.1.0/src/hatui/widgets/progress_bar_widget.py +124 -0
- hatui_kit-0.1.0/src/hatui/widgets/root_services.py +267 -0
- hatui_kit-0.1.0/src/hatui/widgets/root_widget.py +130 -0
- hatui_kit-0.1.0/src/hatui/widgets/row_widget.py +48 -0
- hatui_kit-0.1.0/src/hatui/widgets/scroll_widget.py +224 -0
- hatui_kit-0.1.0/src/hatui/widgets/selection.py +52 -0
- hatui_kit-0.1.0/src/hatui/widgets/signal_strip_widget.py +79 -0
- hatui_kit-0.1.0/src/hatui/widgets/sparkline_widget.py +84 -0
- hatui_kit-0.1.0/src/hatui/widgets/stat_widget.py +107 -0
- hatui_kit-0.1.0/src/hatui/widgets/status_matrix_widget.py +234 -0
- hatui_kit-0.1.0/src/hatui/widgets/status_strip_widget.py +85 -0
- hatui_kit-0.1.0/src/hatui/widgets/table_widget.py +239 -0
- hatui_kit-0.1.0/src/hatui/widgets/tabs_widget.py +188 -0
- hatui_kit-0.1.0/src/hatui/widgets/text_widget.py +64 -0
- hatui_kit-0.1.0/src/hatui/widgets/timeline_widget.py +81 -0
- hatui_kit-0.1.0/src/hatui/widgets/tree_widget.py +341 -0
- hatui_kit-0.1.0/src/hatui/widgets/visualization.py +110 -0
hatui_kit-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Hatui contributors
|
|
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.
|
hatui_kit-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: hatui-kit
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A configurable hacker-style terminal UI toolkit driven by YAML screens, widgets, and providers.
|
|
5
|
+
Project-URL: Homepage, https://github.com/almajd3713/hacker_tui_sim
|
|
6
|
+
Project-URL: Repository, https://github.com/almajd3713/hacker_tui_sim
|
|
7
|
+
Project-URL: Issues, https://github.com/almajd3713/hacker_tui_sim/issues
|
|
8
|
+
Author: almajd3713
|
|
9
|
+
License: MIT License
|
|
10
|
+
|
|
11
|
+
Copyright (c) 2026 Hatui contributors
|
|
12
|
+
|
|
13
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
14
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
15
|
+
in the Software without restriction, including without limitation the rights
|
|
16
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
17
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
18
|
+
furnished to do so, subject to the following conditions:
|
|
19
|
+
|
|
20
|
+
The above copyright notice and this permission notice shall be included in all
|
|
21
|
+
copies or substantial portions of the Software.
|
|
22
|
+
|
|
23
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
24
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
25
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
26
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
27
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
28
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
29
|
+
SOFTWARE.
|
|
30
|
+
License-File: LICENSE
|
|
31
|
+
Keywords: cli,dashboard,terminal,tui,yaml
|
|
32
|
+
Classifier: Development Status :: 3 - Alpha
|
|
33
|
+
Classifier: Environment :: Console
|
|
34
|
+
Classifier: Intended Audience :: Developers
|
|
35
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
36
|
+
Classifier: Operating System :: OS Independent
|
|
37
|
+
Classifier: Programming Language :: Python :: 3
|
|
38
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
39
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
40
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
41
|
+
Classifier: Topic :: Software Development :: User Interfaces
|
|
42
|
+
Classifier: Topic :: Terminals
|
|
43
|
+
Requires-Python: >=3.10
|
|
44
|
+
Requires-Dist: pyyaml>=6.0
|
|
45
|
+
Description-Content-Type: text/markdown
|
|
46
|
+
|
|
47
|
+
# Hatui
|
|
48
|
+
|
|
49
|
+
Hatui is a configurable hacker-style terminal UI toolkit for Python. It lets you compose full-screen TUIs from YAML screens, reusable widgets, themes, routing, and data providers.
|
|
50
|
+
|
|
51
|
+
## Install
|
|
52
|
+
|
|
53
|
+
Recommended for end users:
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
pipx install hatui-kit
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Python package install:
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
pip install hatui-kit
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Installed CLI command:
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
hatui
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Windows convenience build:
|
|
72
|
+
|
|
73
|
+
- Download `hatui-windows-amd64.zip` from GitHub Releases
|
|
74
|
+
- Unzip it
|
|
75
|
+
- Run `hatui.exe`
|
|
76
|
+
|
|
77
|
+
## Quick start
|
|
78
|
+
|
|
79
|
+
Run the bundled demo:
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
hatui
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Validate the bundled demo spec:
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
hatui validate
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Render a non-interactive preview:
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
hatui preview --width 100 --height 28 --frames 1
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Run a custom spec:
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
hatui run path/to/screen.yaml
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## What Hatui provides
|
|
104
|
+
|
|
105
|
+
- declarative YAML screen loading with includes
|
|
106
|
+
- widget and provider registries
|
|
107
|
+
- routing, modals, focus traversal, and debug overlays
|
|
108
|
+
- theme slots, color tokens, and font tokens
|
|
109
|
+
- reusable hacker-style widgets for dashboards and console simulations
|
|
110
|
+
|
|
111
|
+
## Example custom spec
|
|
112
|
+
|
|
113
|
+
```yaml
|
|
114
|
+
theme:
|
|
115
|
+
preset: clean_ops
|
|
116
|
+
|
|
117
|
+
providers:
|
|
118
|
+
- type: constant
|
|
119
|
+
name: hello_text
|
|
120
|
+
target: ui.hello
|
|
121
|
+
value: Hello from Hatui
|
|
122
|
+
|
|
123
|
+
screen:
|
|
124
|
+
type: box
|
|
125
|
+
name: root_box
|
|
126
|
+
title: demo
|
|
127
|
+
padding: 1
|
|
128
|
+
child:
|
|
129
|
+
type: text
|
|
130
|
+
name: hello_text
|
|
131
|
+
text_key: ui.hello
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Run it with:
|
|
135
|
+
|
|
136
|
+
```bash
|
|
137
|
+
hatui run ./demo.yaml
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
## Development
|
|
141
|
+
|
|
142
|
+
Editable install:
|
|
143
|
+
|
|
144
|
+
```bash
|
|
145
|
+
python -m pip install -e .
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Smoke checks:
|
|
149
|
+
|
|
150
|
+
```bash
|
|
151
|
+
python -m compileall src
|
|
152
|
+
PYTHONPATH=src python -m hatui validate
|
|
153
|
+
PYTHONPATH=src python -m hatui preview --width 100 --height 28 --frames 1
|
|
154
|
+
```
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# Hatui
|
|
2
|
+
|
|
3
|
+
Hatui is a configurable hacker-style terminal UI toolkit for Python. It lets you compose full-screen TUIs from YAML screens, reusable widgets, themes, routing, and data providers.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
Recommended for end users:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pipx install hatui-kit
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Python package install:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
pip install hatui-kit
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Installed CLI command:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
hatui
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Windows convenience build:
|
|
26
|
+
|
|
27
|
+
- Download `hatui-windows-amd64.zip` from GitHub Releases
|
|
28
|
+
- Unzip it
|
|
29
|
+
- Run `hatui.exe`
|
|
30
|
+
|
|
31
|
+
## Quick start
|
|
32
|
+
|
|
33
|
+
Run the bundled demo:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
hatui
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Validate the bundled demo spec:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
hatui validate
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Render a non-interactive preview:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
hatui preview --width 100 --height 28 --frames 1
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Run a custom spec:
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
hatui run path/to/screen.yaml
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## What Hatui provides
|
|
58
|
+
|
|
59
|
+
- declarative YAML screen loading with includes
|
|
60
|
+
- widget and provider registries
|
|
61
|
+
- routing, modals, focus traversal, and debug overlays
|
|
62
|
+
- theme slots, color tokens, and font tokens
|
|
63
|
+
- reusable hacker-style widgets for dashboards and console simulations
|
|
64
|
+
|
|
65
|
+
## Example custom spec
|
|
66
|
+
|
|
67
|
+
```yaml
|
|
68
|
+
theme:
|
|
69
|
+
preset: clean_ops
|
|
70
|
+
|
|
71
|
+
providers:
|
|
72
|
+
- type: constant
|
|
73
|
+
name: hello_text
|
|
74
|
+
target: ui.hello
|
|
75
|
+
value: Hello from Hatui
|
|
76
|
+
|
|
77
|
+
screen:
|
|
78
|
+
type: box
|
|
79
|
+
name: root_box
|
|
80
|
+
title: demo
|
|
81
|
+
padding: 1
|
|
82
|
+
child:
|
|
83
|
+
type: text
|
|
84
|
+
name: hello_text
|
|
85
|
+
text_key: ui.hello
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Run it with:
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
hatui run ./demo.yaml
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## Development
|
|
95
|
+
|
|
96
|
+
Editable install:
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
python -m pip install -e .
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Smoke checks:
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
python -m compileall src
|
|
106
|
+
PYTHONPATH=src python -m hatui validate
|
|
107
|
+
PYTHONPATH=src python -m hatui preview --width 100 --height 28 --frames 1
|
|
108
|
+
```
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling>=1.27.0"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "hatui-kit"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "A configurable hacker-style terminal UI toolkit driven by YAML screens, widgets, and providers."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = { file = "LICENSE" }
|
|
11
|
+
requires-python = ">=3.10"
|
|
12
|
+
dependencies = ["PyYAML>=6.0"]
|
|
13
|
+
authors = [
|
|
14
|
+
{ name = "almajd3713" },
|
|
15
|
+
]
|
|
16
|
+
keywords = ["tui", "terminal", "dashboard", "yaml", "cli"]
|
|
17
|
+
classifiers = [
|
|
18
|
+
"Development Status :: 3 - Alpha",
|
|
19
|
+
"Environment :: Console",
|
|
20
|
+
"Intended Audience :: Developers",
|
|
21
|
+
"License :: OSI Approved :: MIT License",
|
|
22
|
+
"Operating System :: OS Independent",
|
|
23
|
+
"Programming Language :: Python :: 3",
|
|
24
|
+
"Programming Language :: Python :: 3.10",
|
|
25
|
+
"Programming Language :: Python :: 3.11",
|
|
26
|
+
"Programming Language :: Python :: 3.12",
|
|
27
|
+
"Topic :: Software Development :: User Interfaces",
|
|
28
|
+
"Topic :: Terminals",
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
[project.urls]
|
|
32
|
+
Homepage = "https://github.com/almajd3713/hacker_tui_sim"
|
|
33
|
+
Repository = "https://github.com/almajd3713/hacker_tui_sim"
|
|
34
|
+
Issues = "https://github.com/almajd3713/hacker_tui_sim/issues"
|
|
35
|
+
|
|
36
|
+
[project.scripts]
|
|
37
|
+
hatui = "hatui.app:main"
|
|
38
|
+
|
|
39
|
+
[tool.hatch.build]
|
|
40
|
+
include = [
|
|
41
|
+
"src/hatui/**/*.py",
|
|
42
|
+
"src/hatui/demo/screens/**/*.yaml",
|
|
43
|
+
"README.md",
|
|
44
|
+
"LICENSE",
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
[tool.hatch.build.targets.wheel]
|
|
48
|
+
packages = ["src/hatui"]
|
|
49
|
+
|
|
50
|
+
[tool.hatch.build.targets.sdist]
|
|
51
|
+
include = [
|
|
52
|
+
"src/hatui",
|
|
53
|
+
"README.md",
|
|
54
|
+
"LICENSE",
|
|
55
|
+
"pyproject.toml",
|
|
56
|
+
]
|
|
57
|
+
|
|
58
|
+
[tool.uv]
|
|
59
|
+
package = true
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
from copy import deepcopy
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from hatui.runtime.bootstrap import build_runtime
|
|
8
|
+
from hatui.runtime.cli import build_parser, normalize_argv, preferred_glyph_mode, resolve_spec_context, write_cli_text
|
|
9
|
+
from hatui.runtime.defaults import create_provider_registry, create_widget_registry
|
|
10
|
+
from hatui.runtime.engine import AppEngine
|
|
11
|
+
from hatui.runtime.loader import ScreenSpecLoader
|
|
12
|
+
from hatui.runtime.watcher import SpecWatcher
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class HatuiApp:
|
|
16
|
+
def __init__(
|
|
17
|
+
self,
|
|
18
|
+
spec_path: str | Path,
|
|
19
|
+
*,
|
|
20
|
+
widget_registry=None,
|
|
21
|
+
provider_registry=None,
|
|
22
|
+
watch: bool = False,
|
|
23
|
+
bundled_demo: bool = False,
|
|
24
|
+
glyph_mode: str = "unicode",
|
|
25
|
+
):
|
|
26
|
+
self.spec_path = Path(spec_path)
|
|
27
|
+
self.is_bundled_demo = bundled_demo
|
|
28
|
+
self.glyph_mode = glyph_mode
|
|
29
|
+
self.widget_registry = widget_registry or create_widget_registry()
|
|
30
|
+
self.provider_registry = provider_registry or create_provider_registry()
|
|
31
|
+
self.loader = ScreenSpecLoader(self.widget_registry, self.provider_registry)
|
|
32
|
+
self.engine = AppEngine()
|
|
33
|
+
self.watcher = SpecWatcher(cli_watch=watch, allow_watch=not bundled_demo)
|
|
34
|
+
self.loaded_files: list[Path] = []
|
|
35
|
+
self._bootstrap_runtime()
|
|
36
|
+
|
|
37
|
+
@property
|
|
38
|
+
def screen_buffer(self):
|
|
39
|
+
return self.engine.screen_buffer
|
|
40
|
+
|
|
41
|
+
@property
|
|
42
|
+
def input_manager(self):
|
|
43
|
+
return self.engine.input_manager
|
|
44
|
+
|
|
45
|
+
@property
|
|
46
|
+
def environment(self):
|
|
47
|
+
return self.engine.environment
|
|
48
|
+
|
|
49
|
+
def _bootstrap_runtime(
|
|
50
|
+
self,
|
|
51
|
+
*,
|
|
52
|
+
preserved_state: dict | None = None,
|
|
53
|
+
preserved_stack: list[str] | None = None,
|
|
54
|
+
preserved_focus: str | None = None,
|
|
55
|
+
preserved_focus_map: dict[str, str] | None = None,
|
|
56
|
+
) -> None:
|
|
57
|
+
runtime = build_runtime(
|
|
58
|
+
spec_path=self.spec_path,
|
|
59
|
+
loader=self.loader,
|
|
60
|
+
preserved_state=preserved_state,
|
|
61
|
+
preserved_stack=preserved_stack,
|
|
62
|
+
preserved_focus=preserved_focus,
|
|
63
|
+
preserved_focus_map=preserved_focus_map,
|
|
64
|
+
glyph_mode=self.glyph_mode,
|
|
65
|
+
)
|
|
66
|
+
self.store = runtime.store
|
|
67
|
+
self.router = runtime.router
|
|
68
|
+
self.root_widget = runtime.root_widget
|
|
69
|
+
self.provider_manager = runtime.provider_manager
|
|
70
|
+
self.loaded_files = runtime.loaded_files
|
|
71
|
+
self.watcher.configure(
|
|
72
|
+
spec_path=self.spec_path,
|
|
73
|
+
loaded_files=self.loaded_files,
|
|
74
|
+
dev_spec=runtime.dev_spec,
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
def debug_snapshot(self) -> dict[str, object]:
|
|
78
|
+
watch = self.watcher.snapshot()
|
|
79
|
+
return {
|
|
80
|
+
"watch_enabled": watch.enabled,
|
|
81
|
+
"watch_interval_s": watch.interval_s,
|
|
82
|
+
"watch_debounce_s": watch.debounce_s,
|
|
83
|
+
"watched_files": watch.watched_files,
|
|
84
|
+
"watched_file_count": len(watch.watched_files),
|
|
85
|
+
"pending_reload": watch.pending_reload,
|
|
86
|
+
"pending_changed_files": watch.pending_changed_files,
|
|
87
|
+
"reload_count": watch.reload_count,
|
|
88
|
+
"last_reload_at": watch.last_reload_at,
|
|
89
|
+
"last_reload_error": watch.last_reload_error,
|
|
90
|
+
**self.engine.stats.as_dict(),
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
def render_frame(self, *, width: int | None = None, height: int | None = None, flush: bool = False):
|
|
94
|
+
return self.engine.render_frame(self, width=width, height=height, flush=flush)
|
|
95
|
+
|
|
96
|
+
def preview(self, *, width: int = 100, height: int = 32, frames: int = 1, route: str | None = None) -> str:
|
|
97
|
+
if route:
|
|
98
|
+
self.root_widget.route_set(route, self.root_widget.context)
|
|
99
|
+
self.engine.activate_providers(self)
|
|
100
|
+
try:
|
|
101
|
+
for _ in range(max(frames, 1)):
|
|
102
|
+
self.render_frame(width=width, height=height, flush=False)
|
|
103
|
+
return self.screen_buffer.to_plain_text()
|
|
104
|
+
finally:
|
|
105
|
+
self.engine.deactivate_providers(self)
|
|
106
|
+
|
|
107
|
+
def reload(self) -> None:
|
|
108
|
+
previous_state = deepcopy(self.store.state)
|
|
109
|
+
previous_stack = list(self.router.stack)
|
|
110
|
+
previous_focus = self.root_widget.context.focused_widget
|
|
111
|
+
previous_focus_map = dict(self.root_widget.state.get("last_focused_by_route", {}))
|
|
112
|
+
old_store = self.store
|
|
113
|
+
old_router = self.router
|
|
114
|
+
old_root_widget = self.root_widget
|
|
115
|
+
old_provider_manager = self.provider_manager
|
|
116
|
+
old_loaded_files = list(self.loaded_files)
|
|
117
|
+
watcher_state = self.watcher.capture_state()
|
|
118
|
+
providers_active = self.engine.providers_active
|
|
119
|
+
self.engine.deactivate_providers(self)
|
|
120
|
+
try:
|
|
121
|
+
self._bootstrap_runtime(
|
|
122
|
+
preserved_state=previous_state,
|
|
123
|
+
preserved_stack=previous_stack,
|
|
124
|
+
preserved_focus=previous_focus,
|
|
125
|
+
preserved_focus_map=previous_focus_map,
|
|
126
|
+
)
|
|
127
|
+
if providers_active:
|
|
128
|
+
self.engine.activate_providers(self)
|
|
129
|
+
self.watcher.mark_reload_success(round(self.engine.elapsed_time, 3))
|
|
130
|
+
except Exception:
|
|
131
|
+
self.store = old_store
|
|
132
|
+
self.router = old_router
|
|
133
|
+
self.root_widget = old_root_widget
|
|
134
|
+
self.provider_manager = old_provider_manager
|
|
135
|
+
self.loaded_files = old_loaded_files
|
|
136
|
+
self.watcher.restore_state(watcher_state)
|
|
137
|
+
if providers_active:
|
|
138
|
+
self.engine.activate_providers(self)
|
|
139
|
+
raise
|
|
140
|
+
|
|
141
|
+
def run(self):
|
|
142
|
+
with self.environment.manage():
|
|
143
|
+
self.engine.activate_providers(self)
|
|
144
|
+
try:
|
|
145
|
+
while True:
|
|
146
|
+
self.engine.dispatch_input(self)
|
|
147
|
+
self.engine.resize_to_terminal()
|
|
148
|
+
if self.watcher.should_reload():
|
|
149
|
+
try:
|
|
150
|
+
self.reload()
|
|
151
|
+
except Exception as exc:
|
|
152
|
+
self.watcher.mark_reload_error(str(exc))
|
|
153
|
+
self.render_frame(flush=True)
|
|
154
|
+
except KeyboardInterrupt:
|
|
155
|
+
pass
|
|
156
|
+
finally:
|
|
157
|
+
self.engine.deactivate_providers(self)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def main():
|
|
161
|
+
argv = normalize_argv()
|
|
162
|
+
parser = build_parser()
|
|
163
|
+
args = parser.parse_args(argv)
|
|
164
|
+
|
|
165
|
+
command = args.command or "run"
|
|
166
|
+
spec_arg = getattr(args, "spec", None)
|
|
167
|
+
spec_context, is_bundled_demo = resolve_spec_context(spec_arg)
|
|
168
|
+
|
|
169
|
+
with spec_context as spec_path:
|
|
170
|
+
if command == "validate":
|
|
171
|
+
loader = ScreenSpecLoader(create_widget_registry(), create_provider_registry())
|
|
172
|
+
messages = loader.validate_spec(spec_path)
|
|
173
|
+
errors = [message for message in messages if message.level == "error"]
|
|
174
|
+
if errors:
|
|
175
|
+
for message in errors:
|
|
176
|
+
print(message.format(), file=sys.stderr)
|
|
177
|
+
raise SystemExit(1)
|
|
178
|
+
print(f"Valid Hatui spec: {spec_path}")
|
|
179
|
+
return
|
|
180
|
+
|
|
181
|
+
app = HatuiApp(
|
|
182
|
+
spec_path=spec_path,
|
|
183
|
+
watch=bool(getattr(args, "watch", False)),
|
|
184
|
+
bundled_demo=is_bundled_demo,
|
|
185
|
+
glyph_mode=preferred_glyph_mode(),
|
|
186
|
+
)
|
|
187
|
+
if command == "preview":
|
|
188
|
+
preview_text = app.preview(
|
|
189
|
+
width=max(args.width, 1),
|
|
190
|
+
height=max(args.height, 1),
|
|
191
|
+
frames=max(args.frames, 1),
|
|
192
|
+
route=args.route,
|
|
193
|
+
)
|
|
194
|
+
write_cli_text(preview_text)
|
|
195
|
+
return
|
|
196
|
+
app.run()
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
if __name__ == "__main__":
|
|
200
|
+
main()
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Core module, containing base classes and utilities for the application."""
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dataclass(slots=True)
|
|
8
|
+
class KeyBinding:
|
|
9
|
+
key: str
|
|
10
|
+
modifiers: tuple[str, ...] = ()
|
|
11
|
+
action: str = ""
|
|
12
|
+
payload: dict[str, Any] = field(default_factory=dict)
|
|
13
|
+
|
|
14
|
+
@property
|
|
15
|
+
def chord(self) -> str:
|
|
16
|
+
if not self.modifiers:
|
|
17
|
+
return self.key
|
|
18
|
+
return "+".join([*self.modifiers, self.key])
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def normalize_chord(key: str, modifiers: list[str] | tuple[str, ...] | None = None) -> str:
|
|
22
|
+
parts = [
|
|
23
|
+
part.strip().lower()
|
|
24
|
+
for part in (modifiers or [])
|
|
25
|
+
if part and part.strip().lower() != "arrow"
|
|
26
|
+
]
|
|
27
|
+
key_name = key.strip().lower()
|
|
28
|
+
if key_name:
|
|
29
|
+
parts.append(key_name)
|
|
30
|
+
return "+".join(parts)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def parse_keybinding(spec: str | dict[str, Any]) -> KeyBinding:
|
|
34
|
+
if isinstance(spec, str):
|
|
35
|
+
return KeyBinding(key=spec.strip().lower())
|
|
36
|
+
|
|
37
|
+
chord = str(spec.get("key", "")).strip().lower()
|
|
38
|
+
if not chord:
|
|
39
|
+
raise ValueError("Keybinding spec missing 'key'")
|
|
40
|
+
|
|
41
|
+
parts = [part for part in chord.split("+") if part]
|
|
42
|
+
key = parts[-1]
|
|
43
|
+
modifiers = tuple(parts[:-1])
|
|
44
|
+
action = str(spec.get("action", "")).strip()
|
|
45
|
+
payload = dict(spec.get("payload", {}) or {})
|
|
46
|
+
return KeyBinding(key=key, modifiers=modifiers, action=action, payload=payload)
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
from dataclasses import dataclass, field
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
@dataclass
|
|
5
|
+
class Context:
|
|
6
|
+
"""
|
|
7
|
+
A class to represent the context of the application.
|
|
8
|
+
"""
|
|
9
|
+
name: str
|
|
10
|
+
version: str
|
|
11
|
+
data: dict[str, Any] = field(default_factory=dict)
|
|
12
|
+
|
|
13
|
+
# Terminal properties
|
|
14
|
+
terminal_width: int = None
|
|
15
|
+
terminal_height:int = None
|
|
16
|
+
delta_time: float = 0.0
|
|
17
|
+
elapsed_time: float = 0.0
|
|
18
|
+
frame: int = 0
|
|
19
|
+
focused_widget: str | None = None
|
|
20
|
+
last_key: str | None = None
|
|
21
|
+
last_modifiers: list[str] = field(default_factory=list)
|
|
22
|
+
render_policy: Any = None
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass
|
|
26
|
+
class Constraints:
|
|
27
|
+
"""
|
|
28
|
+
A class to represent constraints for the application.
|
|
29
|
+
"""
|
|
30
|
+
max_width: int = None
|
|
31
|
+
max_height: int = None
|
|
32
|
+
min_width: int = None
|
|
33
|
+
min_height: int = None
|