#![allow(dead_code)]
use std::env;
use std::io::{self, Write};
use std::time::Duration;
use crossterm::event::{self, Event, KeyCode, KeyEventKind};
use crossterm::terminal;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct TerminalCaps {
pub sync_output: bool,
pub undercurl: bool,
pub underline_color: bool,
pub hyperlinks: bool,
pub kitty_graphics: bool,
pub cell_px: u32,
pub cell_px_h: u32,
pub modern: bool,
pub name: &'static str,
}
impl Default for TerminalCaps {
fn default() -> Self {
Self {
sync_output: false,
undercurl: false,
underline_color: false,
hyperlinks: false,
kitty_graphics: false,
cell_px: 0,
cell_px_h: 0,
modern: false,
name: "unknown",
}
}
}
impl TerminalCaps {
pub fn detect() -> Self {
let term = env::var("TERM").unwrap_or_default().to_ascii_lowercase();
let program = env::var("TERM_PROGRAM")
.unwrap_or_default()
.to_ascii_lowercase();
let term_program_version = env::var("TERM_PROGRAM_VERSION").unwrap_or_default();
let _ = term_program_version;
let ghostty = program.contains("ghostty")
|| env::var("GHOSTTY_RESOURCES_DIR").is_ok()
|| env::var("GHOSTTY_SHELL_INTEGRATION_NO_SUDO").is_ok();
let kitty = program.contains("kitty")
|| env::var("KITTY_WINDOW_ID").is_ok()
|| env::var("KITTY_PID").is_ok()
|| term.contains("kitty");
let wez = program.contains("wezterm") || term.contains("wezterm");
let iterm = program.contains("iterm") || env::var("ITERM_SESSION_ID").is_ok();
let foot = term.contains("foot") || program.contains("foot");
let alacritty = program.contains("alacritty") || term.contains("alacritty");
let windows_terminal = program.contains("vscode") || env::var("WT_SESSION").is_ok();
let (name, modern) = if ghostty {
("ghostty", true)
} else if kitty {
("kitty", true)
} else if wez {
("wezterm", true)
} else if iterm {
("iterm2", true)
} else if foot {
("foot", true)
} else if alacritty {
("alacritty", true)
} else if windows_terminal {
("windows-terminal", true)
} else if term.contains("xterm-256") || term.contains("xterm-ghostty") {
("xterm-256color", false)
} else {
("generic", false)
};
let mut caps = Self {
name,
modern,
sync_output: modern || ghostty || kitty || wez || foot,
undercurl: ghostty || kitty || wez || iterm || foot,
underline_color: modern || ghostty || kitty || wez || iterm || foot || alacritty,
hyperlinks: ghostty || kitty || wez || iterm || foot,
kitty_graphics: kitty || ghostty || wez, ..Self::default()
};
if term.contains("direct") || term.contains("truecolor") {
caps.underline_color = true;
}
let (cw, ch) = probe_cell_px();
caps.cell_px = cw;
caps.cell_px_h = ch;
caps
}
pub fn query_sync_support(&mut self) {
if self.sync_output {
}
let mut stdout = io::stdout();
let _ = write!(stdout, "\x1b[?2026$p");
let _ = stdout.flush();
let deadline = Duration::from_millis(40);
let start = std::time::Instant::now();
while start.elapsed() < deadline {
if event::poll(Duration::from_millis(5)).unwrap_or(false) {
if let Ok(Event::Key(k)) = event::read() {
let _ = k;
}
} else {
break;
}
}
let _ = terminal::is_raw_mode_enabled();
}
pub fn summary(self) -> String {
if !self.modern && self.name == "generic" {
return "term: basic".into();
}
let mut flags = Vec::new();
if self.sync_output {
flags.push("sync");
}
if self.undercurl {
flags.push("curl");
}
if self.underline_color {
flags.push("ul");
}
if self.hyperlinks {
flags.push("link");
}
if self.kitty_graphics {
flags.push("gfx");
}
if flags.is_empty() {
format!("term: {}", self.name)
} else {
format!("term: {} [{}]", self.name, flags.join("+"))
}
}
}
pub fn hyperlink_open(url: &str) -> String {
format!("\x1b]8;;{url}\x1b\\")
}
pub fn hyperlink_end() -> &'static str {
"\x1b]8;;\x1b\\"
}
pub fn file_url(path: &str) -> String {
let p = std::path::Path::new(path);
let abs = if p.is_absolute() {
p.to_path_buf()
} else {
env::current_dir().unwrap_or_default().join(p)
};
let s = abs.display().to_string();
let encoded = s.replace(' ', "%20");
format!("file://{encoded}")
}
pub fn gpu_features_active(gpu_acc: bool, caps: &TerminalCaps) -> bool {
gpu_acc && (caps.modern || caps.sync_output || caps.underline_color)
}
pub fn drain_input_noise() {
let start = std::time::Instant::now();
while start.elapsed() < Duration::from_millis(30) {
match event::poll(Duration::from_millis(2)) {
Ok(true) => {
let _ = event::read();
}
_ => break,
}
}
let _ = KeyCode::Esc;
let _ = KeyEventKind::Press;
}
#[cfg(unix)]
fn probe_cell_px() -> (u32, u32) {
#[repr(C)]
#[derive(Default)]
struct WinSize {
row: libc::c_ushort,
col: libc::c_ushort,
xpixel: libc::c_ushort,
ypixel: libc::c_ushort,
}
let mut ws = WinSize::default();
let ok = unsafe { libc::ioctl(0, libc::TIOCGWINSZ, &mut ws as *mut WinSize) } == 0;
if ok && ws.col > 0 && ws.xpixel > 0 {
let px = ws.xpixel as u32 / ws.col as u32;
let ph = if ws.row > 0 && ws.ypixel > 0 {
ws.ypixel as u32 / ws.row as u32
} else {
px * 2
};
if (4..=72).contains(&px) {
return (px, ph.clamp(6, 144));
}
}
(0, 0)
}
#[cfg(not(unix))]
fn probe_cell_px() -> (u32, u32) {
(0, 0)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn detect_never_panics() {
let c = TerminalCaps::detect();
assert!(!c.name.is_empty());
}
#[test]
fn file_url_abs() {
let u = file_url("/tmp/foo bar.rs");
assert!(u.starts_with("file://"));
assert!(u.contains("%20") || u.contains("foo"));
}
#[test]
fn gpu_gate() {
let mut caps = TerminalCaps::default();
assert!(!gpu_features_active(true, &caps));
caps.modern = true;
assert!(gpu_features_active(true, &caps));
assert!(!gpu_features_active(false, &caps));
}
}