Files
kiln/kiln-tui/src/term.rs
T

130 lines
5.4 KiB
Rust
Raw Normal View History

//! 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.
2026-06-04 23:52:33 -05:00
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),
}
}
2026-06-03 22:46:54 -05:00
/// 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.
2026-06-03 22:46:54 -05:00
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);
2026-06-03 22:46:54 -05:00
line = line.push(ch.to_string(), Some(Rgba8 { r, g, b, a: 255 }), None);
}
} else {
2026-06-03 22:46:54 -05:00
line = line.push("256-color", None, None);
}
let input = if self.keyboard_enhancement {
"enhanced input"
} else {
"legacy input"
};
2026-06-03 22:46:54 -05:00
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)
}