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 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)
@@ -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()