ricekit 0.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ricekit-0.2.0/LICENSE +21 -0
- ricekit-0.2.0/PKG-INFO +112 -0
- ricekit-0.2.0/README.md +90 -0
- ricekit-0.2.0/pyproject.toml +33 -0
- ricekit-0.2.0/ricekit/__init__.py +36 -0
- ricekit-0.2.0/ricekit/app.py +86 -0
- ricekit-0.2.0/ricekit/fx.py +53 -0
- ricekit-0.2.0/ricekit/gallery.py +203 -0
- ricekit-0.2.0/ricekit/icons.py +70 -0
- ricekit-0.2.0/ricekit/modals.py +184 -0
- ricekit-0.2.0/ricekit/palette.py +66 -0
- ricekit-0.2.0/ricekit/storage.py +79 -0
- ricekit-0.2.0/ricekit/themes.py +134 -0
- ricekit-0.2.0/ricekit/widgets.py +132 -0
- ricekit-0.2.0/ricekit.egg-info/PKG-INFO +112 -0
- ricekit-0.2.0/ricekit.egg-info/SOURCES.txt +19 -0
- ricekit-0.2.0/ricekit.egg-info/dependency_links.txt +1 -0
- ricekit-0.2.0/ricekit.egg-info/entry_points.txt +2 -0
- ricekit-0.2.0/ricekit.egg-info/requires.txt +1 -0
- ricekit-0.2.0/ricekit.egg-info/top_level.txt +1 -0
- ricekit-0.2.0/setup.cfg +4 -0
ricekit-0.2.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Gheat
|
|
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.
|
ricekit-0.2.0/PKG-INFO
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ricekit
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: A developer's TUI suite for Textual — themes, widgets, modals, icons, and the design system behind ltui, jtui, and sctui
|
|
5
|
+
Author: Gheat
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/Gheat1/ricekit
|
|
8
|
+
Project-URL: Repository, https://github.com/Gheat1/ricekit
|
|
9
|
+
Project-URL: Issues, https://github.com/Gheat1/ricekit/issues
|
|
10
|
+
Keywords: tui,textual,terminal,design-system,widgets,themes
|
|
11
|
+
Classifier: Environment :: Console
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Topic :: Terminals
|
|
16
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
17
|
+
Requires-Python: >=3.11
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
License-File: LICENSE
|
|
20
|
+
Requires-Dist: textual>=1.0
|
|
21
|
+
Dynamic: license-file
|
|
22
|
+
|
|
23
|
+
<div align="center">
|
|
24
|
+
|
|
25
|
+
# 🍚 ricekit
|
|
26
|
+
|
|
27
|
+
**A developer's TUI suite for [Textual](https://github.com/Textualize/textual).**
|
|
28
|
+
|
|
29
|
+
The themes, widgets, modals, icons, and design rules extracted from building
|
|
30
|
+
[ltui](https://github.com/runpantheon/ltui) — and now powering its whole family
|
|
31
|
+
(jtui, sctui, same repo) —
|
|
32
|
+
everything a fast, clean, rice-friendly terminal app needs, minus the app.
|
|
33
|
+
|
|
34
|
+
<img src="assets/gallery.png" alt="ricekit gallery" width="100%">
|
|
35
|
+
|
|
36
|
+
</div>
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## what's in the box
|
|
41
|
+
|
|
42
|
+
| module | contents |
|
|
43
|
+
| --- | --- |
|
|
44
|
+
| `ricekit.themes` | five themes — `mocha`, OLED-black `void`, monochrome `onyx`, **`clear`** (transparent: your terminal's blur shows through), **`system`** (your terminal's own ANSI palette) — all sharing one `$kit-*` CSS variable contract, with the scrollbar/selection fixes baked in |
|
|
45
|
+
| `ricekit.palette` | swappable chrome palette (`palette.text`, `.dim`, `.blue`, …) that flips to terminal ANSI colors under the `system` theme |
|
|
46
|
+
| `ricekit.app` | `KitApp` — registers the themes, flips `ansi_color` for transparent themes, injects CSS variables, replaces the palette's theme command with a live-preview picker |
|
|
47
|
+
| `ricekit.widgets` | `NavList` (vim keys, quiet cursor), `Splitter` (drag-to-resize with persistence hook, double-click reset), `KitScroll`, `pop_in` (the one sanctioned animation) |
|
|
48
|
+
| `ricekit.modals` | `PickerModal` (generic chooser), `ThemeModal` (restyles the app live as you scroll), `HelpModal` (keybinding cheatsheet from plain data) |
|
|
49
|
+
| `ricekit.icons` | curated nerd-font icons as `\uXXXX` escapes + unicode state glyphs (◌ ○ ◐ ◑ ● ⊘) + mini bar gauges |
|
|
50
|
+
| `ricekit.fx` | text effects — the letter wave (`Wave` + `wave_markup`: **G**heat → g**H**eat → …) and braille spinner frames, driven by one cheap shared ticker |
|
|
51
|
+
| `ricekit.storage` | `AppDirs` — XDG state/cache/config with merge-on-save state and chmod-600 secrets |
|
|
52
|
+
| [`DESIGN.md`](DESIGN.md) | the philosophy: cache-first speed, shape language, color roles, motion rules — and the appendix of Textual sharp edges that each cost a day to find |
|
|
53
|
+
|
|
54
|
+
## see it
|
|
55
|
+
|
|
56
|
+
```sh
|
|
57
|
+
uv tool install git+https://github.com/Gheat1/ricekit
|
|
58
|
+
ricekit-gallery
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
The gallery demos the palette, icons, glyphs, widgets, and modals — cycle
|
|
62
|
+
themes with `t`, live-preview every Textual theme with `ctrl+p`, drag the
|
|
63
|
+
divider, press `?`.
|
|
64
|
+
|
|
65
|
+
## use it
|
|
66
|
+
|
|
67
|
+
```sh
|
|
68
|
+
uv add git+https://github.com/Gheat1/ricekit # or pip install git+…
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
```python
|
|
72
|
+
from textual.binding import Binding
|
|
73
|
+
from textual.widgets import Footer
|
|
74
|
+
|
|
75
|
+
from ricekit import KitApp, icons, palette
|
|
76
|
+
from ricekit.storage import AppDirs
|
|
77
|
+
from ricekit.widgets import NavList
|
|
78
|
+
|
|
79
|
+
DIRS = AppDirs("myapp")
|
|
80
|
+
|
|
81
|
+
class MyApp(KitApp):
|
|
82
|
+
BINDINGS = [Binding("t", "cycle_kit_theme", "theme")]
|
|
83
|
+
CSS = """
|
|
84
|
+
NavList { border: round $kit-border; }
|
|
85
|
+
NavList:focus { border: round $kit-border-focus; }
|
|
86
|
+
"""
|
|
87
|
+
|
|
88
|
+
def compose(self):
|
|
89
|
+
yield NavList(id="items")
|
|
90
|
+
yield Footer()
|
|
91
|
+
|
|
92
|
+
def on_mount(self):
|
|
93
|
+
self.init_kit(theme=DIRS.load_state().get("theme"))
|
|
94
|
+
|
|
95
|
+
def on_kit_theme_changed(self):
|
|
96
|
+
if not self.kit_theme_previewing:
|
|
97
|
+
DIRS.save_state({"theme": self.theme})
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
That's a themed, transparent-capable, live-previewing app in ~25 lines.
|
|
101
|
+
Read [`DESIGN.md`](DESIGN.md) before building anything serious — especially
|
|
102
|
+
the sharp-edges table.
|
|
103
|
+
|
|
104
|
+
## the suite
|
|
105
|
+
|
|
106
|
+
- [**the tracker TUI suite**](https://github.com/runpantheon/ltui) — ltui (Linear), jtui (Jira), sctui (Shortcut); where all of this came from, now backed by Pantheon
|
|
107
|
+
- [**NaviTui**](https://github.com/Gheat1/NaviTui) — an animated terminal player for Navidrome, cover art and all
|
|
108
|
+
- yours next?
|
|
109
|
+
|
|
110
|
+
## license
|
|
111
|
+
|
|
112
|
+
[MIT](LICENSE) — made by [@Gheat1](https://github.com/Gheat1)
|
ricekit-0.2.0/README.md
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
|
|
3
|
+
# 🍚 ricekit
|
|
4
|
+
|
|
5
|
+
**A developer's TUI suite for [Textual](https://github.com/Textualize/textual).**
|
|
6
|
+
|
|
7
|
+
The themes, widgets, modals, icons, and design rules extracted from building
|
|
8
|
+
[ltui](https://github.com/runpantheon/ltui) — and now powering its whole family
|
|
9
|
+
(jtui, sctui, same repo) —
|
|
10
|
+
everything a fast, clean, rice-friendly terminal app needs, minus the app.
|
|
11
|
+
|
|
12
|
+
<img src="assets/gallery.png" alt="ricekit gallery" width="100%">
|
|
13
|
+
|
|
14
|
+
</div>
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## what's in the box
|
|
19
|
+
|
|
20
|
+
| module | contents |
|
|
21
|
+
| --- | --- |
|
|
22
|
+
| `ricekit.themes` | five themes — `mocha`, OLED-black `void`, monochrome `onyx`, **`clear`** (transparent: your terminal's blur shows through), **`system`** (your terminal's own ANSI palette) — all sharing one `$kit-*` CSS variable contract, with the scrollbar/selection fixes baked in |
|
|
23
|
+
| `ricekit.palette` | swappable chrome palette (`palette.text`, `.dim`, `.blue`, …) that flips to terminal ANSI colors under the `system` theme |
|
|
24
|
+
| `ricekit.app` | `KitApp` — registers the themes, flips `ansi_color` for transparent themes, injects CSS variables, replaces the palette's theme command with a live-preview picker |
|
|
25
|
+
| `ricekit.widgets` | `NavList` (vim keys, quiet cursor), `Splitter` (drag-to-resize with persistence hook, double-click reset), `KitScroll`, `pop_in` (the one sanctioned animation) |
|
|
26
|
+
| `ricekit.modals` | `PickerModal` (generic chooser), `ThemeModal` (restyles the app live as you scroll), `HelpModal` (keybinding cheatsheet from plain data) |
|
|
27
|
+
| `ricekit.icons` | curated nerd-font icons as `\uXXXX` escapes + unicode state glyphs (◌ ○ ◐ ◑ ● ⊘) + mini bar gauges |
|
|
28
|
+
| `ricekit.fx` | text effects — the letter wave (`Wave` + `wave_markup`: **G**heat → g**H**eat → …) and braille spinner frames, driven by one cheap shared ticker |
|
|
29
|
+
| `ricekit.storage` | `AppDirs` — XDG state/cache/config with merge-on-save state and chmod-600 secrets |
|
|
30
|
+
| [`DESIGN.md`](DESIGN.md) | the philosophy: cache-first speed, shape language, color roles, motion rules — and the appendix of Textual sharp edges that each cost a day to find |
|
|
31
|
+
|
|
32
|
+
## see it
|
|
33
|
+
|
|
34
|
+
```sh
|
|
35
|
+
uv tool install git+https://github.com/Gheat1/ricekit
|
|
36
|
+
ricekit-gallery
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
The gallery demos the palette, icons, glyphs, widgets, and modals — cycle
|
|
40
|
+
themes with `t`, live-preview every Textual theme with `ctrl+p`, drag the
|
|
41
|
+
divider, press `?`.
|
|
42
|
+
|
|
43
|
+
## use it
|
|
44
|
+
|
|
45
|
+
```sh
|
|
46
|
+
uv add git+https://github.com/Gheat1/ricekit # or pip install git+…
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
```python
|
|
50
|
+
from textual.binding import Binding
|
|
51
|
+
from textual.widgets import Footer
|
|
52
|
+
|
|
53
|
+
from ricekit import KitApp, icons, palette
|
|
54
|
+
from ricekit.storage import AppDirs
|
|
55
|
+
from ricekit.widgets import NavList
|
|
56
|
+
|
|
57
|
+
DIRS = AppDirs("myapp")
|
|
58
|
+
|
|
59
|
+
class MyApp(KitApp):
|
|
60
|
+
BINDINGS = [Binding("t", "cycle_kit_theme", "theme")]
|
|
61
|
+
CSS = """
|
|
62
|
+
NavList { border: round $kit-border; }
|
|
63
|
+
NavList:focus { border: round $kit-border-focus; }
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
def compose(self):
|
|
67
|
+
yield NavList(id="items")
|
|
68
|
+
yield Footer()
|
|
69
|
+
|
|
70
|
+
def on_mount(self):
|
|
71
|
+
self.init_kit(theme=DIRS.load_state().get("theme"))
|
|
72
|
+
|
|
73
|
+
def on_kit_theme_changed(self):
|
|
74
|
+
if not self.kit_theme_previewing:
|
|
75
|
+
DIRS.save_state({"theme": self.theme})
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
That's a themed, transparent-capable, live-previewing app in ~25 lines.
|
|
79
|
+
Read [`DESIGN.md`](DESIGN.md) before building anything serious — especially
|
|
80
|
+
the sharp-edges table.
|
|
81
|
+
|
|
82
|
+
## the suite
|
|
83
|
+
|
|
84
|
+
- [**the tracker TUI suite**](https://github.com/runpantheon/ltui) — ltui (Linear), jtui (Jira), sctui (Shortcut); where all of this came from, now backed by Pantheon
|
|
85
|
+
- [**NaviTui**](https://github.com/Gheat1/NaviTui) — an animated terminal player for Navidrome, cover art and all
|
|
86
|
+
- yours next?
|
|
87
|
+
|
|
88
|
+
## license
|
|
89
|
+
|
|
90
|
+
[MIT](LICENSE) — made by [@Gheat1](https://github.com/Gheat1)
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "ricekit"
|
|
7
|
+
version = "0.2.0"
|
|
8
|
+
description = "A developer's TUI suite for Textual — themes, widgets, modals, icons, and the design system behind ltui, jtui, and sctui"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = { text = "MIT" }
|
|
11
|
+
authors = [{ name = "Gheat" }]
|
|
12
|
+
requires-python = ">=3.11"
|
|
13
|
+
dependencies = ["textual>=1.0"]
|
|
14
|
+
keywords = ["tui", "textual", "terminal", "design-system", "widgets", "themes"]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Environment :: Console",
|
|
17
|
+
"Intended Audience :: Developers",
|
|
18
|
+
"License :: OSI Approved :: MIT License",
|
|
19
|
+
"Programming Language :: Python :: 3",
|
|
20
|
+
"Topic :: Terminals",
|
|
21
|
+
"Topic :: Software Development :: Libraries",
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
[project.urls]
|
|
25
|
+
Homepage = "https://github.com/Gheat1/ricekit"
|
|
26
|
+
Repository = "https://github.com/Gheat1/ricekit"
|
|
27
|
+
Issues = "https://github.com/Gheat1/ricekit/issues"
|
|
28
|
+
|
|
29
|
+
[project.scripts]
|
|
30
|
+
ricekit-gallery = "ricekit.gallery:main"
|
|
31
|
+
|
|
32
|
+
[tool.setuptools]
|
|
33
|
+
packages = ["ricekit"]
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""ricekit — a developer's TUI suite for Textual.
|
|
2
|
+
|
|
3
|
+
The themes, widgets, modals, icons, and design rules extracted from
|
|
4
|
+
building ltui (https://github.com/Gheat1/ltui) — and now shared by its
|
|
5
|
+
siblings jtui (Jira) and sctui (Shortcut): everything a fast,
|
|
6
|
+
clean, rice-friendly terminal app needs, minus the app.
|
|
7
|
+
|
|
8
|
+
from ricekit import KitApp, palette, icons
|
|
9
|
+
from ricekit.widgets import NavList, Splitter, KitScroll, pop_in
|
|
10
|
+
from ricekit.modals import PickerModal, ThemeModal, HelpModal
|
|
11
|
+
from ricekit.storage import AppDirs
|
|
12
|
+
|
|
13
|
+
See DESIGN.md for the philosophy and the sharp edges.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
__version__ = "0.2.0"
|
|
19
|
+
|
|
20
|
+
from ricekit import fx, icons
|
|
21
|
+
from ricekit.app import KitApp
|
|
22
|
+
from ricekit.palette import Palette, palette
|
|
23
|
+
from ricekit.storage import AppDirs
|
|
24
|
+
from ricekit.themes import KIT_THEMES, KIT_THEME_NAMES
|
|
25
|
+
|
|
26
|
+
__all__ = [
|
|
27
|
+
"KitApp",
|
|
28
|
+
"Palette",
|
|
29
|
+
"palette",
|
|
30
|
+
"icons",
|
|
31
|
+
"fx",
|
|
32
|
+
"AppDirs",
|
|
33
|
+
"KIT_THEMES",
|
|
34
|
+
"KIT_THEME_NAMES",
|
|
35
|
+
"__version__",
|
|
36
|
+
]
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"""KitApp — the base App that wires the whole kit together."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from textual.app import App
|
|
6
|
+
from textual.color import Color
|
|
7
|
+
|
|
8
|
+
from ricekit.modals import ThemeModal
|
|
9
|
+
from ricekit.palette import palette
|
|
10
|
+
from ricekit.themes import KIT_THEMES, KIT_THEME_NAMES
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class KitApp(App):
|
|
14
|
+
"""App base with the kit's theme machinery.
|
|
15
|
+
|
|
16
|
+
Call `self.init_kit(theme=...)` from your `on_mount`. Then:
|
|
17
|
+
|
|
18
|
+
- the five kit themes are registered and one is active
|
|
19
|
+
- `App.ansi_color` flips automatically for ansi-background themes
|
|
20
|
+
(clear, system, ansi-dark, …) so the terminal background shows
|
|
21
|
+
- the shared `palette` swaps to ANSI colors under the `system` theme
|
|
22
|
+
- `action_change_theme` opens the live-preview ThemeModal (this also
|
|
23
|
+
replaces the command palette's built-in "Change theme" flow)
|
|
24
|
+
- `action_cycle_kit_theme` cycles the five kit themes (bind it to `t`)
|
|
25
|
+
- override `on_kit_theme_changed()` to re-render palette-dependent
|
|
26
|
+
content and persist the choice — check `self.kit_theme_previewing`
|
|
27
|
+
before persisting
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
kit_theme_previewing: bool = False
|
|
31
|
+
|
|
32
|
+
def get_css_variables(self) -> dict[str, str]:
|
|
33
|
+
# kit-* variables must exist even before our themes register (the
|
|
34
|
+
# stylesheet is parsed while the default textual theme is active),
|
|
35
|
+
# and when a non-kit builtin theme is active
|
|
36
|
+
variables = super().get_css_variables()
|
|
37
|
+
theme = next((t for t in KIT_THEMES if t.name == self.theme), KIT_THEMES[0])
|
|
38
|
+
for name, value in theme.variables.items():
|
|
39
|
+
variables.setdefault(name, value)
|
|
40
|
+
return variables
|
|
41
|
+
|
|
42
|
+
def init_kit(self, theme: str | None = None) -> None:
|
|
43
|
+
for t in KIT_THEMES:
|
|
44
|
+
self.register_theme(t)
|
|
45
|
+
self.theme_changed_signal.subscribe(self, self._kit_theme_changed)
|
|
46
|
+
if theme and theme in self.available_themes:
|
|
47
|
+
self.theme = theme
|
|
48
|
+
else:
|
|
49
|
+
self.theme = KIT_THEME_NAMES[0]
|
|
50
|
+
self._kit_apply()
|
|
51
|
+
|
|
52
|
+
def _kit_theme_is_ansi(self) -> bool:
|
|
53
|
+
theme = self.available_themes.get(self.theme)
|
|
54
|
+
if theme is None or theme.background is None:
|
|
55
|
+
return False
|
|
56
|
+
try:
|
|
57
|
+
return Color.parse(theme.background).ansi is not None
|
|
58
|
+
except Exception:
|
|
59
|
+
return False
|
|
60
|
+
|
|
61
|
+
def _kit_apply(self) -> None:
|
|
62
|
+
self.ansi_color = self._kit_theme_is_ansi()
|
|
63
|
+
palette.set_ansi(self.theme == "system")
|
|
64
|
+
|
|
65
|
+
def _kit_theme_changed(self, _theme) -> None:
|
|
66
|
+
self._kit_apply()
|
|
67
|
+
self.on_kit_theme_changed()
|
|
68
|
+
|
|
69
|
+
def on_kit_theme_changed(self) -> None:
|
|
70
|
+
"""Override me: re-render Rich-text chrome, persist self.theme
|
|
71
|
+
(skip persisting while `self.kit_theme_previewing` is True)."""
|
|
72
|
+
|
|
73
|
+
def action_cycle_kit_theme(self) -> None:
|
|
74
|
+
idx = (
|
|
75
|
+
KIT_THEME_NAMES.index(self.theme)
|
|
76
|
+
if self.theme in KIT_THEME_NAMES
|
|
77
|
+
else -1
|
|
78
|
+
)
|
|
79
|
+
self.theme = KIT_THEME_NAMES[(idx + 1) % len(KIT_THEME_NAMES)]
|
|
80
|
+
|
|
81
|
+
def action_change_theme(self) -> None:
|
|
82
|
+
# overrides App.action_change_theme, so the command palette's
|
|
83
|
+
# "Change theme" command opens the live-preview picker too
|
|
84
|
+
if isinstance(self.screen, ThemeModal):
|
|
85
|
+
return
|
|
86
|
+
self.push_screen(ThemeModal())
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""Text effects: the letter wave and the braille spinner.
|
|
2
|
+
|
|
3
|
+
One shared ticker at ~8fps (0.12s) drives both; render only while a sweep
|
|
4
|
+
or spin is active — idle frames should cost nothing.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from rich.markup import escape
|
|
10
|
+
|
|
11
|
+
SPINNER_FRAMES = "\u280b\u2819\u2839\u2838\u283c\u2834\u2826\u2827\u2807\u280f"
|
|
12
|
+
|
|
13
|
+
FX_TICK = 0.12 # seconds per animation frame
|
|
14
|
+
FX_REST_TICKS = 26 # pause between wave sweeps (~3s)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def wave_markup(s: str, pos: int, base: str, hi: str) -> str:
|
|
18
|
+
"""One traveling letter, bolded + capitalized: gheatmc -> Gheatmc -> gHeatmc.
|
|
19
|
+
|
|
20
|
+
`pos` is the index of the highlighted letter (-1 or out of range = no
|
|
21
|
+
highlight). Advance pos on a timer; after the sweep, rest FX_REST_TICKS.
|
|
22
|
+
"""
|
|
23
|
+
out = []
|
|
24
|
+
for i, ch in enumerate(s):
|
|
25
|
+
e = escape(ch)
|
|
26
|
+
if i == pos:
|
|
27
|
+
out.append(f"[bold {hi}]{e.upper()}[/]")
|
|
28
|
+
else:
|
|
29
|
+
out.append(f"[{base}]{e}[/]")
|
|
30
|
+
return "".join(out)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class Wave:
|
|
34
|
+
"""Sweep/rest state machine for wave_markup.
|
|
35
|
+
|
|
36
|
+
tick() returns True when a re-render is needed this frame.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
def __init__(self, length: int, rest_ticks: int = FX_REST_TICKS) -> None:
|
|
40
|
+
self.length = length
|
|
41
|
+
self.rest_ticks = rest_ticks
|
|
42
|
+
self.pos = -1
|
|
43
|
+
self._rest = 0
|
|
44
|
+
|
|
45
|
+
def tick(self) -> bool:
|
|
46
|
+
if self._rest > 0:
|
|
47
|
+
self._rest -= 1
|
|
48
|
+
return False
|
|
49
|
+
self.pos += 1
|
|
50
|
+
if self.pos >= self.length:
|
|
51
|
+
self.pos = -1
|
|
52
|
+
self._rest = self.rest_ticks
|
|
53
|
+
return True
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
"""ricekit-gallery — a live demo of every piece of the kit.
|
|
2
|
+
|
|
3
|
+
Also serves as the kit's integration test: if the gallery runs, the
|
|
4
|
+
themes, palette, widgets, and modals all compose.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import sys
|
|
10
|
+
|
|
11
|
+
from rich.text import Text
|
|
12
|
+
from textual import on
|
|
13
|
+
from textual.app import ComposeResult
|
|
14
|
+
from textual.binding import Binding
|
|
15
|
+
from textual.containers import Horizontal, Vertical
|
|
16
|
+
from textual.widgets import Footer, OptionList, Static
|
|
17
|
+
from textual.widgets.option_list import Option
|
|
18
|
+
|
|
19
|
+
from ricekit import __version__, icons
|
|
20
|
+
from ricekit.app import KitApp
|
|
21
|
+
from ricekit.modals import HelpModal, PickerModal
|
|
22
|
+
from ricekit.palette import palette
|
|
23
|
+
from ricekit.storage import AppDirs
|
|
24
|
+
from ricekit.widgets import KitScroll, NavList, Splitter, pop_in
|
|
25
|
+
|
|
26
|
+
DIRS = AppDirs("ricekit-gallery")
|
|
27
|
+
|
|
28
|
+
HELP_SECTIONS = [
|
|
29
|
+
("navigate", [("j / k / arrows", "move"), ("enter", "show a demo"), ("esc", "close modal")]),
|
|
30
|
+
("kit", [("t", "cycle the five kit themes"), ("ctrl+p", "palette → Change theme (live preview)"),
|
|
31
|
+
("p", "open a PickerModal"), ("?", "this help")]),
|
|
32
|
+
("layout", [("drag the divider", "resize the sidebar"), ("double-click it", "reset")]),
|
|
33
|
+
("app", [("q", "quit")]),
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
DEMOS = ["palette", "icons", "state glyphs", "bars", "widgets", "philosophy"]
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class Gallery(KitApp):
|
|
40
|
+
TITLE = "ricekit gallery"
|
|
41
|
+
|
|
42
|
+
BINDINGS = [
|
|
43
|
+
Binding("t", "cycle_kit_theme", "theme"),
|
|
44
|
+
Binding("p", "picker_demo", "picker"),
|
|
45
|
+
Binding("question_mark", "help", "help"),
|
|
46
|
+
Binding("q", "quit", "quit"),
|
|
47
|
+
]
|
|
48
|
+
|
|
49
|
+
CSS = """
|
|
50
|
+
#side {
|
|
51
|
+
width: 26;
|
|
52
|
+
margin: 0 0 0 1;
|
|
53
|
+
border: round $kit-border;
|
|
54
|
+
border-title-color: $kit-border-alt;
|
|
55
|
+
}
|
|
56
|
+
#side:focus-within { border: round $kit-border-focus; }
|
|
57
|
+
#stage {
|
|
58
|
+
width: 1fr;
|
|
59
|
+
margin: 0 1 0 0;
|
|
60
|
+
border: round $kit-border;
|
|
61
|
+
border-title-color: $kit-border-alt;
|
|
62
|
+
}
|
|
63
|
+
#stage-body { height: auto; padding: 1 2; }
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
def compose(self) -> ComposeResult:
|
|
67
|
+
with Horizontal():
|
|
68
|
+
with Vertical(id="side"):
|
|
69
|
+
yield NavList(id="demos")
|
|
70
|
+
yield Splitter("#side", min_width=18, max_width=40,
|
|
71
|
+
on_resized=self._layout_changed, id="split")
|
|
72
|
+
with Vertical(id="stage"):
|
|
73
|
+
with KitScroll(id="stage-scroll"):
|
|
74
|
+
yield Static(id="stage-body")
|
|
75
|
+
yield Footer()
|
|
76
|
+
|
|
77
|
+
def on_mount(self) -> None:
|
|
78
|
+
state = DIRS.load_state()
|
|
79
|
+
self.init_kit(theme=state.get("theme"))
|
|
80
|
+
side = self.query_one("#side")
|
|
81
|
+
side.border_title = f" {icons.STAR} ricekit {__version__} "
|
|
82
|
+
if w := state.get("side_w"):
|
|
83
|
+
side.styles.width = int(w)
|
|
84
|
+
self.query_one("#stage").border_title = " gallery "
|
|
85
|
+
demos = self.query_one("#demos", NavList)
|
|
86
|
+
demos.add_options([Option(Text(f" {d}"), id=d) for d in DEMOS])
|
|
87
|
+
demos.highlighted = 0
|
|
88
|
+
demos.focus()
|
|
89
|
+
self._show("palette")
|
|
90
|
+
|
|
91
|
+
def _layout_changed(self, target: str, width: int | None) -> None:
|
|
92
|
+
DIRS.save_state({"side_w": width})
|
|
93
|
+
|
|
94
|
+
def on_kit_theme_changed(self) -> None:
|
|
95
|
+
# re-render palette-dependent content; persist unless previewing
|
|
96
|
+
current = self.query_one("#demos", NavList).highlighted or 0
|
|
97
|
+
self._show(DEMOS[current])
|
|
98
|
+
if not self.kit_theme_previewing:
|
|
99
|
+
DIRS.save_state({"theme": self.theme})
|
|
100
|
+
|
|
101
|
+
# ── demo panes ────────────────────────────────────────────────────
|
|
102
|
+
@on(OptionList.OptionHighlighted, "#demos")
|
|
103
|
+
def _pick(self, event: OptionList.OptionHighlighted) -> None:
|
|
104
|
+
if event.option.id:
|
|
105
|
+
self._show(event.option.id)
|
|
106
|
+
|
|
107
|
+
def _show(self, demo: str) -> None:
|
|
108
|
+
body = Text()
|
|
109
|
+
if demo == "palette":
|
|
110
|
+
body.append("the swappable chrome palette\n\n", style=f"bold {palette.text}")
|
|
111
|
+
for role in ("text", "sub", "dim", "faint", "vfaint", "blue", "lav",
|
|
112
|
+
"peach", "green", "red", "mauve", "yellow"):
|
|
113
|
+
value = getattr(palette, role)
|
|
114
|
+
body.append(" ██ ", style=value)
|
|
115
|
+
body.append(f"palette.{role}".ljust(18), style=palette.sub)
|
|
116
|
+
body.append(f"{value}\n", style=palette.dim)
|
|
117
|
+
body.append("\nswitch to the system theme (t) and watch these\n"
|
|
118
|
+
"become your terminal's own ANSI colors.", style=palette.dim)
|
|
119
|
+
elif demo == "icons":
|
|
120
|
+
body.append("nerd-font icons (as \\uXXXX escapes)\n\n", style=f"bold {palette.text}")
|
|
121
|
+
names = [n for n in dir(icons) if n.isupper() and isinstance(getattr(icons, n), str)
|
|
122
|
+
and n not in ("BULLET", "DOT_SEP")]
|
|
123
|
+
for name in sorted(names):
|
|
124
|
+
body.append(f" {getattr(icons, name)} ", style=palette.blue)
|
|
125
|
+
body.append(f"icons.{name}\n", style=palette.sub)
|
|
126
|
+
elif demo == "state glyphs":
|
|
127
|
+
body.append("workflow-state circles (plain unicode)\n\n", style=f"bold {palette.text}")
|
|
128
|
+
colors = {"triage": palette.mauve, "backlog": palette.dim,
|
|
129
|
+
"unstarted": palette.sub, "started": palette.yellow,
|
|
130
|
+
"review": palette.green, "completed": palette.blue,
|
|
131
|
+
"canceled": palette.dim}
|
|
132
|
+
for state, glyph in icons.STATE_GLYPHS.items():
|
|
133
|
+
body.append(f" {glyph} ", style=colors.get(state, palette.sub))
|
|
134
|
+
body.append(f"{state}\n", style=palette.sub)
|
|
135
|
+
elif demo == "bars":
|
|
136
|
+
body.append("mini bar gauges\n\n", style=f"bold {palette.text}")
|
|
137
|
+
for label, lit in (("urgent", 3), ("high", 3), ("medium", 2), ("low", 1), ("none", 0)):
|
|
138
|
+
body.append(" ")
|
|
139
|
+
body.append_text(icons.bars(lit, palette.sub, palette.vfaint))
|
|
140
|
+
body.append(f" {label}\n", style=palette.sub)
|
|
141
|
+
elif demo == "widgets":
|
|
142
|
+
body.append("what you're looking at\n\n", style=f"bold {palette.text}")
|
|
143
|
+
for name, desc in (
|
|
144
|
+
("NavList", "vim keys, quiet cursor via $kit-cursor"),
|
|
145
|
+
("Splitter", "drag the divider left of this pane; double-click resets"),
|
|
146
|
+
("KitScroll", "focusable scrolling with j/k"),
|
|
147
|
+
("PickerModal", "press p"),
|
|
148
|
+
("ThemeModal", "ctrl+p → Change theme — previews as you scroll"),
|
|
149
|
+
("HelpModal", "press ?"),
|
|
150
|
+
("pop_in", "every modal fades in (~150ms)"),
|
|
151
|
+
):
|
|
152
|
+
body.append(f" {name.ljust(14)}", style=palette.blue)
|
|
153
|
+
body.append(f"{desc}\n", style=palette.sub)
|
|
154
|
+
elif demo == "philosophy":
|
|
155
|
+
body.append("the short version\n\n", style=f"bold {palette.text}")
|
|
156
|
+
for line in (
|
|
157
|
+
"cache first — render instantly, refresh in the background",
|
|
158
|
+
"rounded borders, titles in the border, one accent at a time",
|
|
159
|
+
"chrome colors are roles, data colors are data",
|
|
160
|
+
"keyboard first, everything clickable anyway",
|
|
161
|
+
"motion is a fade, 150ms, out_cubic — never more",
|
|
162
|
+
"themes restyle chrome only; clear + system respect the rice",
|
|
163
|
+
):
|
|
164
|
+
body.append(f" {icons.CHECK} ", style=palette.green)
|
|
165
|
+
body.append(f"{line}\n", style=palette.sub)
|
|
166
|
+
body.append("\nfull document: DESIGN.md", style=palette.dim)
|
|
167
|
+
self.query_one("#stage-body", Static).update(body)
|
|
168
|
+
self.query_one("#stage").border_subtitle = f" {demo} "
|
|
169
|
+
|
|
170
|
+
# ── modal demos ───────────────────────────────────────────────────
|
|
171
|
+
def action_picker_demo(self) -> None:
|
|
172
|
+
opts = []
|
|
173
|
+
for i, (glyph, label) in enumerate([
|
|
174
|
+
(icons.CHECK_CIRCLE, "ship it"),
|
|
175
|
+
(icons.CLOCK, "later"),
|
|
176
|
+
(icons.CROSS_CIRCLE, "never"),
|
|
177
|
+
]):
|
|
178
|
+
row = Text()
|
|
179
|
+
row.append(f"{glyph} ", style=(palette.green, palette.yellow, palette.red)[i])
|
|
180
|
+
row.append(label, style=palette.text)
|
|
181
|
+
opts.append(Option(row, id=label))
|
|
182
|
+
|
|
183
|
+
def done(choice: str | None) -> None:
|
|
184
|
+
if choice:
|
|
185
|
+
self.notify(f"{icons.CHECK} you picked: {choice}")
|
|
186
|
+
|
|
187
|
+
self.push_screen(PickerModal("a PickerModal", opts), done)
|
|
188
|
+
|
|
189
|
+
def action_help(self) -> None:
|
|
190
|
+
if isinstance(self.screen, HelpModal):
|
|
191
|
+
return
|
|
192
|
+
self.push_screen(HelpModal(HELP_SECTIONS, title=f"{icons.KEYBOARD} ricekit gallery"))
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def main() -> None:
|
|
196
|
+
if "--version" in sys.argv or "-v" in sys.argv:
|
|
197
|
+
print(f"ricekit {__version__}")
|
|
198
|
+
return
|
|
199
|
+
Gallery().run()
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
if __name__ == "__main__":
|
|
203
|
+
main()
|