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:
@@ -0,0 +1,133 @@
|
||||
//! Terminal capability detection and Kitty keyboard-protocol setup.
|
||||
//!
|
||||
//! kiln-tui detects what the host terminal can do once at startup and, when the
|
||||
//! terminal supports the Kitty keyboard protocol, enables it so richer input
|
||||
//! (modifier-only keys, key release/repeat, unambiguous Ctrl combos) becomes
|
||||
//! available. The detected [`TerminalCaps`] are intended to be handed to scripts
|
||||
//! so they can pick key bindings that actually work on this terminal.
|
||||
|
||||
use ratatui::crossterm::event::{
|
||||
KeyboardEnhancementFlags, PopKeyboardEnhancementFlags, PushKeyboardEnhancementFlags,
|
||||
};
|
||||
use ratatui::crossterm::execute;
|
||||
use ratatui::crossterm::terminal::supports_keyboard_enhancement;
|
||||
use ratatui::style::{Color, Style};
|
||||
use ratatui::text::{Line, Span};
|
||||
use std::io;
|
||||
|
||||
/// The input capabilities of the host terminal, detected once at startup.
|
||||
///
|
||||
/// Scripts should branch on these capabilities rather than on the terminal's
|
||||
/// program name: capabilities stay correct on terminals nobody has special-cased
|
||||
/// (and through tmux/SSH), whereas brand names are brittle and often unset.
|
||||
pub struct TerminalCaps {
|
||||
/// Whether the Kitty keyboard protocol is supported (and has been enabled).
|
||||
///
|
||||
/// When true, bindings that legacy terminals cannot express are available:
|
||||
/// a bare modifier keypress (e.g. Ctrl by itself), key release/repeat events,
|
||||
/// `Ctrl+I` distinct from `Tab`, and reliable `Ctrl`+digit/symbol combos.
|
||||
pub keyboard_enhancement: bool,
|
||||
/// Whether the terminal advertises 24-bit truecolor (via `$COLORTERM`).
|
||||
/// When false, RGB glyph colors are approximated to the 256-color palette.
|
||||
pub truecolor: bool,
|
||||
}
|
||||
|
||||
impl TerminalCaps {
|
||||
/// Detects the host terminal's capabilities.
|
||||
///
|
||||
/// `keyboard_enhancement` is a real query/response round-trip with the
|
||||
/// terminal (it may block briefly, and relies on a timeout over slow links),
|
||||
/// so call this once and cache the result. `truecolor` reads an environment
|
||||
/// variable, which is heuristic.
|
||||
pub fn detect() -> Self {
|
||||
Self {
|
||||
keyboard_enhancement: matches!(supports_keyboard_enhancement(), Ok(true)),
|
||||
truecolor: std::env::var("COLORTERM")
|
||||
.map(|v| v == "truecolor" || v == "24bit")
|
||||
.unwrap_or(false),
|
||||
}
|
||||
}
|
||||
|
||||
/// A one-line styled summary for the UI, e.g. `"truecolor · enhanced input"`.
|
||||
///
|
||||
/// When truecolor is available the word "truecolor" is rendered with each
|
||||
/// letter a different rainbow color — a fitting flex, since only a terminal
|
||||
/// that can show per-cell RGB can display it. Otherwise the color word is the
|
||||
/// plain `"256-color"`. The input descriptor is always drawn in the default
|
||||
/// color.
|
||||
pub fn summary_line(&self) -> Line<'static> {
|
||||
let mut spans: Vec<Span<'static>> = vec![Span::raw(" ")];
|
||||
|
||||
if self.truecolor {
|
||||
// Rotate the hue across the letters so each gets a distinct spectrum color.
|
||||
let word = "truecolor";
|
||||
let n = word.chars().count();
|
||||
for (i, ch) in word.chars().enumerate() {
|
||||
let (r, g, b) = hue_to_rgb(i as f32 / n as f32 * 360.0);
|
||||
spans.push(Span::styled(
|
||||
ch.to_string(),
|
||||
Style::default().fg(Color::Rgb(r, g, b)),
|
||||
));
|
||||
}
|
||||
} else {
|
||||
spans.push(Span::raw("256-color"));
|
||||
}
|
||||
|
||||
let input = if self.keyboard_enhancement {
|
||||
"enhanced input"
|
||||
} else {
|
||||
"legacy input"
|
||||
};
|
||||
spans.push(Span::raw(format!(" · {input} ")));
|
||||
|
||||
Line::from(spans)
|
||||
}
|
||||
}
|
||||
|
||||
/// Converts a hue in degrees to an RGB triple at full saturation and value.
|
||||
///
|
||||
/// Standard 6-segment HSV→RGB conversion (S = V = 1); used to spread a rainbow
|
||||
/// across the letters of the "truecolor" badge.
|
||||
fn hue_to_rgb(hue: f32) -> (u8, u8, u8) {
|
||||
let h = (hue % 360.0) / 60.0;
|
||||
// `x` ramps the secondary channel up/down within each 60° segment.
|
||||
let x = 1.0 - (h % 2.0 - 1.0).abs();
|
||||
let (r, g, b) = match h as u32 {
|
||||
0 => (1.0, x, 0.0),
|
||||
1 => (x, 1.0, 0.0),
|
||||
2 => (0.0, 1.0, x),
|
||||
3 => (0.0, x, 1.0),
|
||||
4 => (x, 0.0, 1.0),
|
||||
_ => (1.0, 0.0, x),
|
||||
};
|
||||
((r * 255.0) as u8, (g * 255.0) as u8, (b * 255.0) as u8)
|
||||
}
|
||||
|
||||
/// The Kitty keyboard-protocol features kiln-tui requests when supported:
|
||||
/// unambiguous escape codes, press/release/repeat event types, alternate-key
|
||||
/// reporting, and reporting every key as an escape code (which is what makes
|
||||
/// modifier-only keypresses observable).
|
||||
fn enhancement_flags() -> KeyboardEnhancementFlags {
|
||||
KeyboardEnhancementFlags::DISAMBIGUATE_ESCAPE_CODES
|
||||
| KeyboardEnhancementFlags::REPORT_EVENT_TYPES
|
||||
| KeyboardEnhancementFlags::REPORT_ALTERNATE_KEYS
|
||||
| KeyboardEnhancementFlags::REPORT_ALL_KEYS_AS_ESCAPE_CODES
|
||||
}
|
||||
|
||||
/// Enables the Kitty keyboard protocol on stdout.
|
||||
///
|
||||
/// Only call this after the terminal is in raw mode (e.g. after `ratatui::init`)
|
||||
/// and only when [`TerminalCaps::keyboard_enhancement`] is true. Pair every call
|
||||
/// with [`pop_kitty_flags`] before restoring the terminal.
|
||||
pub fn push_kitty_flags() -> io::Result<()> {
|
||||
execute!(
|
||||
io::stdout(),
|
||||
PushKeyboardEnhancementFlags(enhancement_flags())
|
||||
)
|
||||
}
|
||||
|
||||
/// Disables the Kitty keyboard protocol previously enabled by [`push_kitty_flags`].
|
||||
/// Call this before `ratatui::restore` so the terminal is left as we found it.
|
||||
pub fn pop_kitty_flags() -> io::Result<()> {
|
||||
execute!(io::stdout(), PopKeyboardEnhancementFlags)
|
||||
}
|
||||
Reference in New Issue
Block a user