use crate::base::PixelSize;
use crate::render::present::{ColorDepth, PresentCaps};
#[derive(Clone, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub struct Capabilities {
pub truecolor: bool,
pub colors_256: bool,
pub kitty_keyboard: bool,
pub kitty_graphics: bool,
pub iterm2_images: bool,
pub sixel: bool,
pub sixel_max_registers: Option<u16>,
pub graphics_wrap: Option<WrapKind>,
pub cell_pixel_size: Option<PixelSize>,
pub sgr_pixel_mouse: bool,
pub sync_output_2026: bool,
pub sgr_mouse: bool,
pub bracketed_paste: bool,
pub focus_events: bool,
pub hyperlinks: bool,
pub undercurl: bool,
pub underline_color: bool,
pub deferred_wrap: bool,
pub osc52_copy: bool,
pub osc9_notify: bool,
pub osc99_notify: bool,
pub needs_tmux_passthrough: bool,
pub tmux_version: Option<String>,
pub unicode_ok: bool,
pub no_color: bool,
pub dumb: bool,
pub in_tmux: bool,
pub term_version: Option<String>,
}
impl Default for Capabilities {
fn default() -> Self {
Capabilities {
truecolor: false,
colors_256: false,
kitty_keyboard: false,
kitty_graphics: false,
iterm2_images: false,
sixel: false,
sixel_max_registers: None,
graphics_wrap: None,
cell_pixel_size: None,
sgr_pixel_mouse: false,
sync_output_2026: false,
sgr_mouse: false,
bracketed_paste: false,
focus_events: false,
hyperlinks: false,
undercurl: false,
underline_color: false,
deferred_wrap: true, osc52_copy: false,
osc9_notify: false,
osc99_notify: false,
needs_tmux_passthrough: false,
tmux_version: None,
unicode_ok: false,
no_color: false,
dumb: false,
in_tmux: false,
term_version: None,
}
}
}
impl Capabilities {
pub fn with(f: impl FnOnce(&mut Self)) -> Self {
let mut caps = Self::default();
f(&mut caps);
caps
}
}
#[path = "caps_detect.rs"]
mod detect;
impl Capabilities {
pub fn present_caps(&self) -> PresentCaps {
PresentCaps::from(self)
}
pub fn summary(&self) -> String {
fn yn(b: bool) -> &'static str {
if b {
"yes"
} else {
"no"
}
}
let mut s = String::with_capacity(768);
let color = if self.no_color {
"disabled (NO_COLOR)"
} else if self.truecolor {
"truecolor (24-bit)"
} else if self.colors_256 {
"256-color"
} else {
"16-color"
};
s.push_str("terminal capabilities\n");
if let Some(v) = &self.term_version {
s.push_str(&format!(" terminal : {v}\n"));
}
if self.dumb {
s.push_str(" TERM : dumb — escapes suppressed, probe skipped\n");
}
if self.in_tmux {
let ver = self.tmux_version.as_deref().unwrap_or("version unknown");
s.push_str(&format!(" multiplexer : tmux ({ver})\n"));
}
s.push_str(&format!(" color : {color}\n"));
s.push_str(&format!(
" unicode : {}\n",
if self.unicode_ok {
"UTF-8"
} else {
"uncertain (locale not UTF-8)"
}
));
s.push_str(&format!(
" kitty keyboard : {}\n",
yn(self.kitty_keyboard)
));
s.push_str(&format!(
" mouse : SGR {} / pixel units {}\n",
yn(self.sgr_mouse),
yn(self.sgr_pixel_mouse)
));
s.push_str(&format!(
" paste/focus : bracketed {} / focus events {}\n",
yn(self.bracketed_paste),
yn(self.focus_events)
));
s.push_str(&format!(
" sync output : {} (DEC 2026)\n",
yn(self.sync_output_2026)
));
let gfx = match (self.kitty_graphics, self.iterm2_images, self.sixel) {
(false, false, false) => "none (unicode mosaic fallback)".to_string(),
_ => {
let mut v = Vec::new();
if self.kitty_graphics {
v.push("kitty");
}
if self.iterm2_images {
v.push("iTerm2");
}
if self.sixel {
v.push("sixel");
}
let mut t = v.join(" + ");
if self.graphics_wrap == Some(WrapKind::Tmux) {
t.push_str(" (via tmux passthrough)");
}
t
}
};
s.push_str(&format!(" graphics : {gfx}\n"));
if let Some(r) = self.sixel_max_registers {
s.push_str(&format!(" sixel registers : {r}\n"));
}
if let Some(px) = self.cell_pixel_size {
s.push_str(&format!(" cell size : {}x{} px\n", px.w, px.h));
}
s.push_str(&format!(
" styling : undercurl {} / underline color {} / hyperlinks {}\n",
yn(self.undercurl),
yn(self.underline_color),
yn(self.hyperlinks)
));
let notify = match self.notify_channel() {
crate::term::verbs::NotifyChannel::Osc9 => "OSC 9",
crate::term::verbs::NotifyChannel::Osc99 => "OSC 99 (kitty)",
crate::term::verbs::NotifyChannel::BellOnly => "bell only",
};
s.push_str(&format!(
" desktop niceties: clipboard copy {} / notify {}\n",
yn(self.osc52_copy),
notify
));
s.push_str(&format!(
" deferred wrap : {} (presenter last-column strategy)\n",
yn(self.deferred_wrap)
));
s
}
pub fn summary_line(&self) -> String {
let mut t: Vec<String> = Vec::with_capacity(12);
if self.dumb {
t.push("dumb".into());
}
t.push(
if self.no_color {
"no-color"
} else if self.truecolor {
"truecolor"
} else if self.colors_256 {
"256color"
} else {
"16color"
}
.into(),
);
if self.kitty_keyboard {
t.push("kitty-kbd".into());
}
if self.kitty_graphics {
t.push("kitty-gfx".into());
}
if self.iterm2_images {
t.push("iterm2-img".into());
}
if self.sixel {
match self.sixel_max_registers {
Some(r) => t.push(format!("sixel({r})")),
None => t.push("sixel".into()),
}
}
if self.sync_output_2026 {
t.push("sync".into());
}
if self.sgr_mouse {
t.push(
if self.sgr_pixel_mouse {
"mouse-sgr(+pixels)"
} else {
"mouse-sgr"
}
.into(),
);
}
if self.bracketed_paste {
t.push("paste".into());
}
if self.focus_events {
t.push("focus".into());
}
if self.undercurl {
t.push("undercurl".into());
}
if self.osc52_copy {
t.push("osc52".into());
}
if self.in_tmux {
t.push(
if self.graphics_wrap == Some(WrapKind::Tmux) {
"tmux(passthrough)"
} else {
"tmux"
}
.into(),
);
}
t.join(", ")
}
pub fn notify_channel(&self) -> crate::term::verbs::NotifyChannel {
use crate::term::verbs::NotifyChannel;
if self.osc99_notify {
NotifyChannel::Osc99
} else if self.osc9_notify {
NotifyChannel::Osc9
} else {
NotifyChannel::BellOnly
}
}
pub fn graphics(&self) -> GraphicsCaps {
GraphicsCaps {
kitty_graphics: self.kitty_graphics,
iterm2_images: self.iterm2_images,
sixel: self.sixel,
sixel_max_registers: self.sixel_max_registers,
cell_pixel_size: self.cell_pixel_size,
wrap: self.graphics_wrap,
}
}
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum WrapKind {
Tmux,
}
impl From<&Capabilities> for PresentCaps {
fn from(c: &Capabilities) -> PresentCaps {
let color = if c.truecolor {
ColorDepth::TrueColor
} else if c.colors_256 {
ColorDepth::Xterm256
} else {
ColorDepth::Ansi16
};
PresentCaps {
color,
sync_output_2026: c.sync_output_2026,
hyperlinks: c.hyperlinks,
undercurl: c.undercurl,
underline_color: c.underline_color,
}
}
}
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
#[non_exhaustive]
pub struct GraphicsCaps {
pub kitty_graphics: bool,
pub iterm2_images: bool,
pub sixel: bool,
pub sixel_max_registers: Option<u16>,
pub cell_pixel_size: Option<PixelSize>,
pub wrap: Option<WrapKind>,
}
impl GraphicsCaps {
pub fn with(f: impl FnOnce(&mut Self)) -> Self {
let mut caps = Self::default();
f(&mut caps);
caps
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
#[allow(missing_docs)] pub enum CapsReply {
KittyKeyboard { flags: u32 },
DecMode { mode: u32, status: u8 },
PrimaryDa { params: Vec<u32> },
XtVersion { text: String },
KittyGraphics { raw: Vec<u8> },
XtGetTcap { raw: Vec<u8> },
CursorPos { row: u32, col: u32 },
XtSmGraphics { item: u32, status: u32, value: u32 },
WindowOp { op: u32, a: u32, b: u32 },
Osc { raw: Vec<u8> },
}
#[cfg(test)]
#[path = "caps_tests.rs"]
mod tests;