//! 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 color::Rgba8; use kiln_core::log::LogLine; use ratatui::crossterm::event::{ KeyboardEnhancementFlags, PopKeyboardEnhancementFlags, PushKeyboardEnhancementFlags, }; use ratatui::crossterm::execute; use ratatui::crossterm::terminal::supports_keyboard_enhancement; 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 of terminal capabilities as a [`LogLine`], /// e.g. `"truecolor · enhanced input"`, seeded into the game log at startup. /// /// 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 status_logline(&self) -> LogLine { let mut line = LogLine::new(); 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); line = line.push(ch.to_string(), Some(Rgba8 { r, g, b, a: 255 }), None); } } else { line = line.push("256-color", None, None); } let input = if self.keyboard_enhancement { "enhanced input" } else { "legacy input" }; line.push(format!(" · {input}"), None, None) } } /// 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) }