use super::detect::{MultiplexerKind, TerminalKind};
use super::profiles::{merge_multiplexer_limits, profile_for};
use super::traits::{ColorSupport, TerminalCapabilities};
use std::io::{Read, Write};
use std::time::Duration;
mod sequences {
pub const DA1: &[u8] = b"\x1b[c";
pub const DECRPM_SYNC_OUTPUT: &[u8] = b"\x1b[?2026$p";
pub const KITTY_KEYBOARD_QUERY: &[u8] = b"\x1b[?u";
}
pub trait TerminalQuerier {
fn query_color_support(&mut self) -> Option<ColorSupport>;
fn query_synchronized_output(&mut self) -> Option<bool>;
fn query_kitty_keyboard(&mut self) -> Option<bool>;
}
pub struct LiveQuerier<R: Read, W: Write> {
reader: R,
writer: W,
#[allow(dead_code)] timeout: Duration,
}
impl<R: Read, W: Write> LiveQuerier<R, W> {
pub fn new(reader: R, writer: W, timeout: Duration) -> Self {
Self {
reader,
writer,
timeout,
}
}
fn query(&mut self, sequence: &[u8]) -> Option<Vec<u8>> {
if self.writer.write_all(sequence).is_err() {
return None;
}
if self.writer.flush().is_err() {
return None;
}
let mut buf = vec![0u8; 256];
match self.reader.read(&mut buf) {
Ok(n) if n > 0 => {
buf.truncate(n);
Some(buf)
}
_ => None,
}
}
}
impl<R: Read, W: Write> TerminalQuerier for LiveQuerier<R, W> {
fn query_color_support(&mut self) -> Option<ColorSupport> {
let response = self.query(sequences::DA1)?;
let response_str = String::from_utf8_lossy(&response);
if response_str.contains("64") || response_str.contains("62") {
Some(ColorSupport::TrueColor)
} else if response_str.contains("c") {
Some(ColorSupport::Extended256)
} else {
None
}
}
fn query_synchronized_output(&mut self) -> Option<bool> {
let response = self.query(sequences::DECRPM_SYNC_OUTPUT)?;
let response_str = String::from_utf8_lossy(&response);
if response_str.contains("2026;1")
|| response_str.contains("2026;2")
|| response_str.contains("2026;3")
{
Some(true)
} else if response_str.contains("2026;0") || response_str.contains("2026;4") {
Some(false)
} else {
None
}
}
fn query_kitty_keyboard(&mut self) -> Option<bool> {
let response = self.query(sequences::KITTY_KEYBOARD_QUERY)?;
let response_str = String::from_utf8_lossy(&response);
if response_str.contains('?') && response_str.contains('u') {
Some(true)
} else {
Some(false)
}
}
}
pub struct MockQuerier {
color_support: Option<ColorSupport>,
synchronized_output: Option<bool>,
kitty_keyboard: Option<bool>,
}
impl MockQuerier {
pub fn new() -> Self {
Self {
color_support: None,
synchronized_output: None,
kitty_keyboard: None,
}
}
pub fn with_color_support(mut self, support: ColorSupport) -> Self {
self.color_support = Some(support);
self
}
pub fn with_synchronized_output(mut self, supported: bool) -> Self {
self.synchronized_output = Some(supported);
self
}
pub fn with_kitty_keyboard(mut self, supported: bool) -> Self {
self.kitty_keyboard = Some(supported);
self
}
}
impl Default for MockQuerier {
fn default() -> Self {
Self::new()
}
}
impl TerminalQuerier for MockQuerier {
fn query_color_support(&mut self) -> Option<ColorSupport> {
self.color_support
}
fn query_synchronized_output(&mut self) -> Option<bool> {
self.synchronized_output
}
fn query_kitty_keyboard(&mut self) -> Option<bool> {
self.kitty_keyboard
}
}
pub fn detect_capabilities(
kind: TerminalKind,
multiplexer: MultiplexerKind,
querier: &mut dyn TerminalQuerier,
) -> TerminalCapabilities {
let mut caps = profile_for(kind);
if let Some(color) = querier.query_color_support() {
caps.color = color;
}
if let Some(sync_output) = querier.query_synchronized_output() {
caps.synchronized_output = sync_output;
}
if let Some(kitty_kb) = querier.query_kitty_keyboard() {
caps.kitty_keyboard = kitty_kb;
}
merge_multiplexer_limits(caps, multiplexer)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_mock_querier_default() {
let mut querier = MockQuerier::new();
assert!(querier.query_color_support().is_none());
assert!(querier.query_synchronized_output().is_none());
assert!(querier.query_kitty_keyboard().is_none());
}
#[test]
fn test_mock_querier_with_responses() {
let mut querier = MockQuerier::new()
.with_color_support(ColorSupport::TrueColor)
.with_synchronized_output(true)
.with_kitty_keyboard(false);
assert_eq!(querier.query_color_support(), Some(ColorSupport::TrueColor));
assert_eq!(querier.query_synchronized_output(), Some(true));
assert_eq!(querier.query_kitty_keyboard(), Some(false));
}
#[test]
fn test_detect_capabilities_fallback_to_static() {
let mut querier = MockQuerier::new();
let caps = detect_capabilities(TerminalKind::Kitty, MultiplexerKind::None, &mut querier);
assert_eq!(caps.color, ColorSupport::TrueColor);
assert!(caps.synchronized_output);
assert!(caps.kitty_keyboard);
}
#[test]
fn test_detect_capabilities_override_with_queries() {
let mut querier = MockQuerier::new().with_synchronized_output(false);
let caps = detect_capabilities(TerminalKind::Kitty, MultiplexerKind::None, &mut querier);
assert!(!caps.synchronized_output);
assert!(caps.kitty_keyboard);
}
#[test]
fn test_detect_capabilities_upgrade_unknown_terminal() {
let mut querier = MockQuerier::new()
.with_color_support(ColorSupport::TrueColor)
.with_synchronized_output(true)
.with_kitty_keyboard(true);
let caps = detect_capabilities(TerminalKind::Unknown, MultiplexerKind::None, &mut querier);
assert_eq!(caps.color, ColorSupport::TrueColor);
assert!(caps.synchronized_output);
assert!(caps.kitty_keyboard);
}
#[test]
fn test_detect_capabilities_multiplexer_limits_applied() {
let mut querier = MockQuerier::new().with_synchronized_output(true);
let caps = detect_capabilities(TerminalKind::Kitty, MultiplexerKind::Tmux, &mut querier);
assert!(!caps.synchronized_output);
}
#[test]
fn test_detect_capabilities_screen_downgrades_color() {
let mut querier = MockQuerier::new().with_color_support(ColorSupport::TrueColor);
let caps = detect_capabilities(TerminalKind::Kitty, MultiplexerKind::Screen, &mut querier);
assert_eq!(caps.color, ColorSupport::Extended256);
}
#[test]
fn test_detect_capabilities_partial_query_success() {
let mut querier = MockQuerier::new()
.with_color_support(ColorSupport::Extended256)
.with_kitty_keyboard(true);
let caps =
detect_capabilities(TerminalKind::Alacritty, MultiplexerKind::None, &mut querier);
assert_eq!(caps.color, ColorSupport::Extended256);
assert!(caps.kitty_keyboard);
assert!(!caps.synchronized_output); }
#[test]
fn test_live_querier_creation() {
let input: &[u8] = &[];
let output: Vec<u8> = Vec::new();
let timeout = Duration::from_millis(50);
let _querier = LiveQuerier::new(input, output, timeout);
}
}