Add kiln-tui: a terminal player for kiln boards
New `kiln-tui` crate (ratatui 0.30 / crossterm) that plays a board from a .toml map named on the command line, letting the player walk around with the arrow keys or hjkl. It depends only on kiln-core — the engine needed zero changes, confirming it is genuinely UI-agnostic. - Text rendering: each Glyph.tile index is reinterpreted as a character via a CP437->Unicode table (cp437.rs); fg/bg map to ratatui Color::Rgb truecolor. - render.rs BoardWidget: per-axis viewport that centers a small board and scrolls a large one to keep the player visible. - term.rs TerminalCaps: detects truecolor and Kitty keyboard-protocol support, enabling the protocol when present (held-key repeat, modifier-only keys); shows a rainbow "truecolor" badge in the bottom border. - Input loop acts on Press and Repeat (continuous held-key movement) and ignores Release. Workspace: removed kiln-egui from members (files left untouched). Updated CLAUDE.md and ARCHITECTURE.md to document the workspace split and kiln-tui. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -23,28 +23,27 @@ When the user says **"finish the epic"**, do all of the following in order:
|
||||
## Commands
|
||||
|
||||
```bash
|
||||
cargo build # compile
|
||||
cargo run # build and run (loads maps/start.toml)
|
||||
cargo test # run all tests
|
||||
cargo test <name> # run a single test by name (substring match)
|
||||
cargo clippy # lint
|
||||
cargo fmt # format
|
||||
cargo build # compile the workspace (kiln-core + kiln-tui)
|
||||
cargo run -p kiln-tui -- maps/start.toml # play a board in the terminal
|
||||
cargo test # run all tests
|
||||
cargo test <name> # run a single test by name (substring match)
|
||||
cargo clippy # lint
|
||||
cargo fmt # format
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
The game is a single Rust binary using **eframe 0.33 / egui 0.33** for the GUI. eframe drives a retained-mode UI: the `App::update` method is called every frame and is responsible for both drawing and responding to input.
|
||||
`kiln` is a Cargo **workspace** that separates the engine from its front-ends so the same game data can be driven by different UIs:
|
||||
|
||||
`update` is structured in phases:
|
||||
- Input handling — arrow keys move the player (Play mode only)
|
||||
- `egui::TopBottomPanel::top` — menu bar (File → Exit, Play/Edit mode toggle)
|
||||
- `editor::show_editor_panel` — archetype palette panel (Edit mode only; declared before CentralPanel)
|
||||
- `egui::CentralPanel::default` — game viewport; rendered with `render::draw_board`; click-to-paint in Edit mode
|
||||
- `glyph_picker::show` — floating picker dialog (Edit mode only, when open)
|
||||
- **`kiln-core`** — the engine: all core game types (`Board`, `Glyph`, `Archetype`, `GameState`, …) plus `.toml` map-file load/save. No rendering or UI; every front-end depends on it.
|
||||
- **`kiln-tui`** — a terminal **player** (no editor yet) built on **ratatui 0.30 / crossterm**. Takes a map-file path on the command line and lets you walk the player around the board with the arrow keys.
|
||||
- **`kiln-egui`** — the original eframe/egui desktop app (player + editor). **Still in the tree but no longer a workspace member**: it was removed from the root `Cargo.toml` `members` and its files were left untouched, so it is not built at the root and is not guaranteed to compile against the current engine. Its notes below are retained for when it is revived.
|
||||
|
||||
### Modules
|
||||
Root `Cargo.toml`: `members = ["kiln-core", "kiln-tui"]`.
|
||||
|
||||
**`src/game.rs`** — all core game types:
|
||||
### kiln-core modules
|
||||
|
||||
**`kiln-core/src/game.rs`** — all core game types:
|
||||
- `Glyph` (`Copy`, `Eq`, `Hash`) — per-cell visual: `tile: u32` (tilesheet index), `fg/bg: Color32`. `Glyph::player()` is a `const fn`; all other glyphs come from the map file. Derives `Hash` so boards can deduplicate glyphs into a palette.
|
||||
- `FontSpec` — optional per-board bitmap font override: `path: String`, `tile_w: u32`, `tile_h: u32`. When `None`, the app default font is used.
|
||||
- `Behavior` — plain data struct of runtime behavioral properties: `passable: bool`, `opaque: bool`. Returned by `Archetype::behavior()`; new properties added here require no match arms elsewhere.
|
||||
@@ -56,7 +55,40 @@ The game is a single Rust binary using **eframe 0.33 / egui 0.33** for the GUI.
|
||||
- `PortalDef` — parsed from map files, stored on Board; not yet runtime-wired
|
||||
- `GameState` — holds `board: Board`; `try_move(dx, dy)` checks passability before moving the player
|
||||
|
||||
**`src/font.rs`** — bitmap font loading and UV mapping:
|
||||
**`kiln-core/src/map_file.rs`** — map file loading:
|
||||
- `MapFile` and friends — serde `Deserialize` types for TOML map files
|
||||
- `TileIndex` — `#[serde(untagged)]` enum accepting either `Num(u32)` or `Chr(char)`; lets map files use `tile = " "` (char) or `tile = 32` (integer) interchangeably
|
||||
- `FontHeader` — deserializes the optional `[font]` section (`path`, `tile_w`, `tile_h`)
|
||||
- `impl From<MapFile> for Board` — converts a parsed file into a ready-to-use `Board`; unknown archetype names produce `ErrorBlock`
|
||||
- `pub fn load(path: &str) -> Result<Board, Box<dyn std::error::Error>>` — reads and converts a `.toml` map file
|
||||
- `pub fn save(board: &Board, path: &Path) -> Result<(), Box<dyn std::error::Error>>` — serializes `Board` back to TOML and writes to disk
|
||||
|
||||
### kiln-tui modules
|
||||
|
||||
The terminal player. Renders the board as text: each `Glyph.tile` index is reinterpreted as a character (the default kiln font is CP437, where the tile index equals the code point) and drawn with the glyph's RGB colors. No bitmap font or UV mapping is involved.
|
||||
|
||||
**`kiln-tui/src/main.rs`** — entry point and play loop:
|
||||
- Parses a single positional arg (the map path); prints usage and exits non-zero if missing. Loads the board via `kiln_core::map_file::load` *before* touching the terminal so load errors print to a normal screen.
|
||||
- `ratatui::init()` → detect `TerminalCaps` → push Kitty flags when supported → `run` loop → pop Kitty flags → `ratatui::restore()`.
|
||||
- `run` blocks on `event::read()`; it acts on `Press` **and** `Repeat` key events (so holding a key moves continuously) and ignores `Release` (which the Kitty protocol also emits, and which would otherwise double-fire each move). Arrow keys or `hjkl` move the player via `GameState::try_move`; `q`/`Esc` quit.
|
||||
- `draw` wraps the board in a bordered `Block` titled with the board name and controls; `caps.summary_line()` is shown right-aligned in the bottom border.
|
||||
|
||||
**`kiln-tui/src/cp437.rs`** — CP437 → Unicode mapping:
|
||||
- `CP437: [char; 256]` — full code-page-437 table (graphic glyphs for 0x00–0x1F, box-drawing for 0xB0–0xDF, etc.).
|
||||
- `tile_to_char(tile: u32) -> char` — indexes the table for `tile < 256`; falls back to `char::from_u32` then a blank space. Unit-tested.
|
||||
|
||||
**`kiln-tui/src/render.rs`** — board → ratatui buffer:
|
||||
- `rgba8_to_color(Rgba8) -> Color::Rgb` — lossless RGB bridge (alpha dropped); truecolor terminals show exact colors, 256-color ones approximate.
|
||||
- `BoardWidget<'a>` — a `ratatui::widgets::Widget` that draws the board. Per axis, `axis(board_len, view, player) -> (offset, pad, count)`: a board smaller than the viewport is **centered** with screen padding, a larger one **scrolls** to keep the player on screen with no empty margins. Each cell resolves glyph priority player > object > grid floor, then writes char + fg/bg into the buffer.
|
||||
|
||||
**`kiln-tui/src/term.rs`** — terminal capability detection and Kitty setup:
|
||||
- `TerminalCaps { keyboard_enhancement: bool, truecolor: bool }` — detected once at startup. `keyboard_enhancement` is a real query/response (`supports_keyboard_enhancement()`); `truecolor` reads `$COLORTERM`. Intended to be handed to scripts so they can pick bindings the terminal actually supports.
|
||||
- `summary_line() -> Line` — styled one-line badge for the bottom border; when `truecolor`, the word "truecolor" is drawn with each letter a different rainbow color (`hue_to_rgb`, an HSV→RGB helper), else a plain "256-color".
|
||||
- `push_kitty_flags()` / `pop_kitty_flags()` — enable/disable the Kitty keyboard protocol (`DISAMBIGUATE_ESCAPE_CODES | REPORT_EVENT_TYPES | REPORT_ALTERNATE_KEYS | REPORT_ALL_KEYS_AS_ESCAPE_CODES`); the last flag makes modifier-only keypresses observable. Pushed only when supported, popped before restore. Enabling `REPORT_EVENT_TYPES` is why the input loop must filter out `Release` events.
|
||||
|
||||
### kiln-egui modules (no longer a workspace member)
|
||||
|
||||
**`kiln-egui/src/font.rs`** — bitmap font loading and UV mapping:
|
||||
- `BitmapFont` — wraps an egui `TextureHandle` (preprocessed to opaque-white / transparent) plus tile dimensions. The top-left pixel of the source PNG defines the "background" color; those pixels become `TRANSPARENT`, all others become `WHITE`. At render time, `painter.image(…, tile_uv, fg_color)` tints white pixels to `fg` and transparent pixels reveal the `bg` fill behind.
|
||||
- `BitmapFont::load(ctx, path, tile_w, tile_h)` — loads from disk
|
||||
- `BitmapFont::from_bytes(ctx, bytes, tile_w, tile_h, label)` — loads from embedded bytes
|
||||
@@ -65,20 +97,12 @@ The game is a single Rust binary using **eframe 0.33 / egui 0.33** for the GUI.
|
||||
- `BitmapFont::cell_size(zoom)` — pixel size (`Vec2`) of one rendered cell at the given integer zoom; centralizes the `tile_w * zoom` math used across rendering and hit-testing
|
||||
- `BitmapFont::tile_cols()`, `tile_count()`, `img_w()`, `img_h()` — geometry helpers used by the glyph picker and font dialog
|
||||
|
||||
**`src/font_dialog.rs`** — font picker dialog:
|
||||
**`kiln-egui/src/font_dialog.rs`** — font picker dialog:
|
||||
- `FontDialogState` — in-progress edit: `path: String`, `tile_w/tile_h: u32`, `preview: Option<BitmapFont>`
|
||||
- `FontDialogState::from_spec(spec)` — initializes from an existing `FontSpec` or defaults (8×16, empty path)
|
||||
- `show(ctx, open, state, font_spec)` — floating dialog with file browser (`rfd::FileDialog`), tile dimension `DragValue` controls, scrollable tilesheet preview with grid overlay, and Apply / Use default buttons. Uses a `should_close` flag to avoid the egui `.open()` borrow conflict.
|
||||
|
||||
**`src/map_file.rs`** — map file loading:
|
||||
- `MapFile` and friends — serde `Deserialize` types for TOML map files
|
||||
- `TileIndex` — `#[serde(untagged)]` enum accepting either `Num(u32)` or `Chr(char)`; lets map files use `tile = " "` (char) or `tile = 32` (integer) interchangeably
|
||||
- `FontHeader` — deserializes the optional `[font]` section (`path`, `tile_w`, `tile_h`)
|
||||
- `impl From<MapFile> for Board` — converts a parsed file into a ready-to-use `Board`; unknown archetype names produce `ErrorBlock`
|
||||
- `pub fn load(path: &str) -> Result<Board, Box<dyn std::error::Error>>` — reads and converts a `.toml` map file
|
||||
- `pub fn save(board: &Board, path: &Path) -> Result<(), Box<dyn std::error::Error>>` — serializes `Board` back to TOML and writes to disk
|
||||
|
||||
**`src/main.rs`** — app entry point and frame loop:
|
||||
**`kiln-egui/src/main.rs`** — app entry point and frame loop:
|
||||
- `AppMode` enum (`Play` | `Edit`) — gates arrow-key input; toggles the side panel and viewport mode
|
||||
- `App` holds `GameState`, `AppMode`, `EditorState`, `default_font: BitmapFont`, and `board_font: Option<BitmapFont>`
|
||||
- `App::new(board, egui_ctx)` — loads `assets/vga-font-8x16.png` as the default font (falls back to `create_placeholder`); loads per-board font from `board.font` if present
|
||||
@@ -90,7 +114,7 @@ The game is a single Rust binary using **eframe 0.33 / egui 0.33** for the GUI.
|
||||
- **Play mode**: arrow keys move player; `render::board_origin` centers or player-tracks the viewport
|
||||
- **Edit mode**: `ScrollArea::both()` wraps the board; click-to-paint stamps `(editor.palette.glyph, editor.palette.selected)`; calls `editor::show_editor_panel` and `glyph_picker::show`
|
||||
|
||||
**`src/render.rs`** — cell rendering and drawing primitives:
|
||||
**`kiln-egui/src/render.rs`** — cell rendering and drawing primitives:
|
||||
- Window sizing constants (`DEFAULT_WINDOW_W = 840`, `DEFAULT_WINDOW_H = 524`, `MIN_WINDOW_W/H`); no fixed `CELL_W/H` — cell size comes from the active `BitmapFont`
|
||||
- `rgba8_to_color32(c)` / `color32_to_rgba8(c)` — inverse bridges between core `color::Rgba8` and egui `Color32`
|
||||
- `paint_glyph(painter, rect, glyph, font)` — `rect_filled` with `glyph.bg`, then `painter.image` tinted by `glyph.fg`
|
||||
@@ -101,7 +125,7 @@ The game is a single Rust binary using **eframe 0.33 / egui 0.33** for the GUI.
|
||||
- `board_origin(available, board_w, board_h, player, font, zoom)` — centers board or clamps to player with no empty space
|
||||
- `pos_to_cell(origin, pos, tile_w, tile_h) -> (i32, i32)` — pixel → cell coordinates; negatives signal out-of-bounds
|
||||
|
||||
**`src/editor.rs`** — editor state and side panel:
|
||||
**`kiln-egui/src/editor.rs`** — editor state and side panel:
|
||||
- `EditorTab` enum (`Palette` | `Objects` | `Board` | `Scripts` | `World`) — which tab is active
|
||||
- `EditorState` — `tab: EditorTab` plus four concern-grouped sub-structs so each tab takes only the borrow it needs:
|
||||
- `palette: PaletteState` — `selected: Archetype`, `glyph: Glyph`, `picker_open: bool` (selecting a new archetype resets `glyph` to its default)
|
||||
@@ -110,7 +134,7 @@ The game is a single Rust binary using **eframe 0.33 / egui 0.33** for the GUI.
|
||||
- `scripts: ScriptEdit` — `editing: Option<String>`, `content: String`, `new_name: String`
|
||||
- `show_editor_panel(ctx, editor, board, active_font)` — resizable right-side panel (default 200 px); renders the tab bar then dispatches to a per-tab function (`palette_tab`, `objects_tab`, `board_tab`, `scripts_tab`; World is empty), passing the relevant sub-struct(s). Palette shows the archetype list and glyph preview button; Board shows the font path, "Font…" button, and zoom slider; Objects lists the selected object's glyph preview, **Passable/Opaque checkboxes**, and script combobox; Scripts creates/edits named scripts
|
||||
|
||||
**`src/glyph_picker.rs`** — floating glyph picker dialog:
|
||||
**`kiln-egui/src/glyph_picker.rs`** — floating glyph picker dialog:
|
||||
- `show(ctx, open, glyph, board_cells, font)` — takes `open: &mut bool` and `glyph: &mut Glyph` directly; no dependency on `EditorState`
|
||||
- Three sections: board palette (unique glyphs deduped via `HashSet<Glyph>`, click to select), FG/BG color pickers, scrollable tile grid (all tiles in the active font at `tile_w × tile_h` per cell)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user