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.
Files changed (97) hide show
  1. hatui_kit-0.1.0/.gitignore +8 -0
  2. hatui_kit-0.1.0/LICENSE +21 -0
  3. hatui_kit-0.1.0/PKG-INFO +154 -0
  4. hatui_kit-0.1.0/README.md +108 -0
  5. hatui_kit-0.1.0/pyproject.toml +59 -0
  6. hatui_kit-0.1.0/src/hatui/__init__.py +11 -0
  7. hatui_kit-0.1.0/src/hatui/__main__.py +5 -0
  8. hatui_kit-0.1.0/src/hatui/app.py +200 -0
  9. hatui_kit-0.1.0/src/hatui/core/__init__.py +1 -0
  10. hatui_kit-0.1.0/src/hatui/core/actions.py +46 -0
  11. hatui_kit-0.1.0/src/hatui/core/context.py +33 -0
  12. hatui_kit-0.1.0/src/hatui/core/input_manager.py +167 -0
  13. hatui_kit-0.1.0/src/hatui/core/screen_buffer.py +150 -0
  14. hatui_kit-0.1.0/src/hatui/core/style.py +381 -0
  15. hatui_kit-0.1.0/src/hatui/core/terminal_env.py +67 -0
  16. hatui_kit-0.1.0/src/hatui/core/util.py +0 -0
  17. hatui_kit-0.1.0/src/hatui/core/widget.py +203 -0
  18. hatui_kit-0.1.0/src/hatui/demo/__init__.py +1 -0
  19. hatui_kit-0.1.0/src/hatui/demo/screens/dashboard/modals/debug.yaml +72 -0
  20. hatui_kit-0.1.0/src/hatui/demo/screens/dashboard/modals/help.yaml +43 -0
  21. hatui_kit-0.1.0/src/hatui/demo/screens/dashboard/providers/phase8.yaml +429 -0
  22. hatui_kit-0.1.0/src/hatui/demo/screens/dashboard/providers.yaml +487 -0
  23. hatui_kit-0.1.0/src/hatui/demo/screens/dashboard/screen.yaml +64 -0
  24. hatui_kit-0.1.0/src/hatui/demo/screens/dashboard/tabs/analysis.yaml +65 -0
  25. hatui_kit-0.1.0/src/hatui/demo/screens/dashboard/tabs/forensics.yaml +71 -0
  26. hatui_kit-0.1.0/src/hatui/demo/screens/dashboard/tabs/hacker.yaml +80 -0
  27. hatui_kit-0.1.0/src/hatui/demo/screens/dashboard/tabs/logs.yaml +39 -0
  28. hatui_kit-0.1.0/src/hatui/demo/screens/dashboard/tabs/overview.yaml +180 -0
  29. hatui_kit-0.1.0/src/hatui/demo/screens/dashboard/tabs/visuals.yaml +84 -0
  30. hatui_kit-0.1.0/src/hatui/demo/screens/dashboard.yaml +166 -0
  31. hatui_kit-0.1.0/src/hatui/demo_assets.py +16 -0
  32. hatui_kit-0.1.0/src/hatui/providers/__init__.py +54 -0
  33. hatui_kit-0.1.0/src/hatui/providers/base.py +61 -0
  34. hatui_kit-0.1.0/src/hatui/providers/builtins.py +546 -0
  35. hatui_kit-0.1.0/src/hatui/providers/helpers.py +169 -0
  36. hatui_kit-0.1.0/src/hatui/runtime/__init__.py +24 -0
  37. hatui_kit-0.1.0/src/hatui/runtime/action_registry.py +102 -0
  38. hatui_kit-0.1.0/src/hatui/runtime/bindings.py +40 -0
  39. hatui_kit-0.1.0/src/hatui/runtime/bootstrap.py +94 -0
  40. hatui_kit-0.1.0/src/hatui/runtime/cli.py +66 -0
  41. hatui_kit-0.1.0/src/hatui/runtime/defaults.py +219 -0
  42. hatui_kit-0.1.0/src/hatui/runtime/engine.py +135 -0
  43. hatui_kit-0.1.0/src/hatui/runtime/formatters.py +65 -0
  44. hatui_kit-0.1.0/src/hatui/runtime/loader.py +140 -0
  45. hatui_kit-0.1.0/src/hatui/runtime/provider_manager.py +138 -0
  46. hatui_kit-0.1.0/src/hatui/runtime/registries.py +65 -0
  47. hatui_kit-0.1.0/src/hatui/runtime/render_policy.py +144 -0
  48. hatui_kit-0.1.0/src/hatui/runtime/router.py +74 -0
  49. hatui_kit-0.1.0/src/hatui/runtime/store.py +29 -0
  50. hatui_kit-0.1.0/src/hatui/runtime/validation.py +433 -0
  51. hatui_kit-0.1.0/src/hatui/runtime/watcher.py +167 -0
  52. hatui_kit-0.1.0/src/hatui/widgets/__init__.py +89 -0
  53. hatui_kit-0.1.0/src/hatui/widgets/alert_stack_widget.py +64 -0
  54. hatui_kit-0.1.0/src/hatui/widgets/alert_widget.py +83 -0
  55. hatui_kit-0.1.0/src/hatui/widgets/banner_widget.py +76 -0
  56. hatui_kit-0.1.0/src/hatui/widgets/border_widget.py +93 -0
  57. hatui_kit-0.1.0/src/hatui/widgets/box_widget.py +81 -0
  58. hatui_kit-0.1.0/src/hatui/widgets/center_widget.py +29 -0
  59. hatui_kit-0.1.0/src/hatui/widgets/chart_widget.py +222 -0
  60. hatui_kit-0.1.0/src/hatui/widgets/code_block_widget.py +91 -0
  61. hatui_kit-0.1.0/src/hatui/widgets/column_widget.py +48 -0
  62. hatui_kit-0.1.0/src/hatui/widgets/diff_viewer_widget.py +88 -0
  63. hatui_kit-0.1.0/src/hatui/widgets/divider_widget.py +66 -0
  64. hatui_kit-0.1.0/src/hatui/widgets/event_feed_widget.py +72 -0
  65. hatui_kit-0.1.0/src/hatui/widgets/flow_widget.py +77 -0
  66. hatui_kit-0.1.0/src/hatui/widgets/gauge_widget.py +109 -0
  67. hatui_kit-0.1.0/src/hatui/widgets/heatmap_widget.py +153 -0
  68. hatui_kit-0.1.0/src/hatui/widgets/hex_dump_widget.py +91 -0
  69. hatui_kit-0.1.0/src/hatui/widgets/histogram_widget.py +96 -0
  70. hatui_kit-0.1.0/src/hatui/widgets/inspector_widget.py +85 -0
  71. hatui_kit-0.1.0/src/hatui/widgets/kv_inspector_widget.py +134 -0
  72. hatui_kit-0.1.0/src/hatui/widgets/label_widget.py +99 -0
  73. hatui_kit-0.1.0/src/hatui/widgets/list_widget.py +185 -0
  74. hatui_kit-0.1.0/src/hatui/widgets/log_widget.py +106 -0
  75. hatui_kit-0.1.0/src/hatui/widgets/menu_widget.py +31 -0
  76. hatui_kit-0.1.0/src/hatui/widgets/metric_grid_widget.py +103 -0
  77. hatui_kit-0.1.0/src/hatui/widgets/mini_chart_widget.py +79 -0
  78. hatui_kit-0.1.0/src/hatui/widgets/modal_host_widget.py +85 -0
  79. hatui_kit-0.1.0/src/hatui/widgets/modal_widget.py +112 -0
  80. hatui_kit-0.1.0/src/hatui/widgets/paragraph_widget.py +94 -0
  81. hatui_kit-0.1.0/src/hatui/widgets/progress_bar_widget.py +124 -0
  82. hatui_kit-0.1.0/src/hatui/widgets/root_services.py +267 -0
  83. hatui_kit-0.1.0/src/hatui/widgets/root_widget.py +130 -0
  84. hatui_kit-0.1.0/src/hatui/widgets/row_widget.py +48 -0
  85. hatui_kit-0.1.0/src/hatui/widgets/scroll_widget.py +224 -0
  86. hatui_kit-0.1.0/src/hatui/widgets/selection.py +52 -0
  87. hatui_kit-0.1.0/src/hatui/widgets/signal_strip_widget.py +79 -0
  88. hatui_kit-0.1.0/src/hatui/widgets/sparkline_widget.py +84 -0
  89. hatui_kit-0.1.0/src/hatui/widgets/stat_widget.py +107 -0
  90. hatui_kit-0.1.0/src/hatui/widgets/status_matrix_widget.py +234 -0
  91. hatui_kit-0.1.0/src/hatui/widgets/status_strip_widget.py +85 -0
  92. hatui_kit-0.1.0/src/hatui/widgets/table_widget.py +239 -0
  93. hatui_kit-0.1.0/src/hatui/widgets/tabs_widget.py +188 -0
  94. hatui_kit-0.1.0/src/hatui/widgets/text_widget.py +64 -0
  95. hatui_kit-0.1.0/src/hatui/widgets/timeline_widget.py +81 -0
  96. hatui_kit-0.1.0/src/hatui/widgets/tree_widget.py +341 -0
  97. hatui_kit-0.1.0/src/hatui/widgets/visualization.py +110 -0
@@ -0,0 +1,8 @@
1
+ # Python-based stuff
2
+ *.pyc
3
+ .venv
4
+ build/
5
+ dist/
6
+ *.egg-info
7
+
8
+ .codex/
@@ -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.
@@ -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,11 @@
1
+ """Hatui package."""
2
+
3
+ __all__ = ["HatuiApp"]
4
+
5
+
6
+ def __getattr__(name: str):
7
+ if name == "HatuiApp":
8
+ from hatui.app import HatuiApp
9
+
10
+ return HatuiApp
11
+ raise AttributeError(name)
@@ -0,0 +1,5 @@
1
+ from hatui.app import main
2
+
3
+
4
+ if __name__ == "__main__":
5
+ main()
@@ -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