2026-06-03 20:10:02 -05:00
|
|
|
//! 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;
|
2026-06-03 20:10:02 -05:00
|
|
|
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.
|
2026-06-03 20:10:02 -05:00
|
|
|
///
|
|
|
|
|
/// 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();
|
2026-06-03 20:10:02 -05:00
|
|
|
|
|
|
|
|
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);
|
2026-06-03 20:10:02 -05:00
|
|
|
}
|
|
|
|
|
} else {
|
2026-06-03 22:46:54 -05:00
|
|
|
line = line.push("256-color", None, None);
|
2026-06-03 20:10:02 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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)
|
2026-06-03 20:10:02 -05:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// 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)
|
|
|
|
|
}
|