use crate::backend::tty::{get_terminal_attr, make_raw, set_terminal_attr, Termios};
use std::env;
use std::io::{self, Write};
use std::os::fd::{AsFd, BorrowedFd};
pub const RESTORE_SEQ: &str =
"\x1b[<u\x1b[?25h\x1b[?1l\x1b[?2026l\x1b[?1000l\x1b[?1002l\x1b[?1003l\x1b[?1006l\x1b[?1007l\x1b[?7h\x1b[?1049l\x1b[?2004l";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CursorShape {
Block,
Underline,
Bar,
BlinkingBlock,
BlinkingUnderline,
BlinkingBar,
}
pub struct Capabilities {
term: String,
colorterm: Option<String>,
term_program: Option<String>,
kitty_window_id: bool,
}
impl Capabilities {
pub fn detect() -> Self {
let term = env::var("TERM").unwrap_or_else(|_| "dumb".to_string());
let colorterm = env::var("COLORTERM").ok();
let term_program = env::var("TERM_PROGRAM").ok();
let kitty_window_id = env::var("KITTY_WINDOW_ID").is_ok();
Self {
term,
colorterm,
term_program,
kitty_window_id,
}
}
pub fn term(&self) -> &str {
&self.term
}
pub fn supports_truecolor(&self) -> bool {
if let Some(ref ct) = self.colorterm {
let ct_lower = ct.to_lowercase();
if ct_lower == "truecolor" || ct_lower == "24bit" {
return true;
}
}
if let Some(ref tp) = self.term_program {
let tp_lower = tp.to_lowercase();
if tp_lower.contains("iterm")
|| tp_lower.contains("vscode")
|| tp_lower.contains("hyper")
|| tp_lower.contains("wezterm")
|| tp_lower.contains("ghostty")
{
return true;
}
}
if self.kitty_window_id {
return true;
}
let term_lower = self.term.to_lowercase();
if term_lower.contains("256color") || term_lower.contains("truecolor") {
return true;
}
if term_lower == "dumb" || term_lower.starts_with("vt100") || term_lower == "ansi" {
return false;
}
let truecolor_terms = [
"xterm", "screen", "tmux", "rxvt", "kitty", "alacritty", "wezterm",
"ghostty", "foot", "konsole", "gnome", "terminology", "eterm",
];
truecolor_terms.iter().any(|t| term_lower.contains(t))
}
pub fn supports_mouse(&self) -> bool {
let term_lower = self.term.to_lowercase();
if term_lower.contains("xterm") || term_lower.contains("screen")
|| term_lower.contains("tmux") || term_lower.contains("rxvt")
|| term_lower.contains("kitty") || term_lower.contains("wezterm")
|| term_lower.contains("foot") || term_lower.contains("alacritty")
|| term_lower.contains("ghostty") || term_lower.contains("konsole")
|| term_lower.contains("gnome") || term_lower.contains("terminology") {
return true;
}
!term_lower.is_empty() && term_lower != "dumb"
}
pub fn supports_unicode_width(&self) -> bool {
let term_lower = self.term.to_lowercase();
term_lower != "dumb" && !term_lower.starts_with("vt100")
}
pub fn supports_title(&self) -> bool {
let term_lower = self.term.to_lowercase();
if term_lower == "dumb" || term_lower.starts_with("vt100") {
return false;
}
true
}
}
pub struct Terminal<W: Write + AsFd> {
original_termios: Option<Termios>,
output: W,
capabilities: Capabilities,
is_null_mode: bool,
}
impl<W: Write + AsFd> Drop for Terminal<W> {
fn drop(&mut self) {
let _ = write!(self.output, "{}", RESTORE_SEQ);
let _ = self.output.flush();
if !self.is_null_mode {
if let Some(ref termios) = self.original_termios {
let _ = set_terminal_attr(self.output.as_fd(), termios);
}
}
}
}
impl<W: Write + AsFd> Terminal<W> {
pub fn new(mut writer: W) -> io::Result<Self> {
let fd = writer.as_fd();
let original_termios = match get_terminal_attr(fd) {
Ok(t) => t,
Err(e) if e.raw_os_error() == Some(25) => {
return Self::new_null_mode(writer);
}
Err(e) => return Err(e),
};
let mut termios = original_termios;
make_raw(&mut termios);
set_terminal_attr(fd, &termios)?;
write!(
writer,
"\x1b[>1u\x1b[?1049h\x1b[?1003h\x1b[?1006h\x1b[?1007l\x1b[?7l\x1b[?25l\x1b[?2004h"
)?;
write!(writer, "\x1b[2J\x1b[H")?;
writer.flush()?;
Ok(Self {
original_termios: Some(original_termios),
output: writer,
capabilities: Capabilities::detect(),
is_null_mode: false,
})
}
fn new_null_mode(writer: W) -> io::Result<Self> {
Ok(Self {
original_termios: None,
output: writer,
capabilities: Capabilities::detect(),
is_null_mode: true,
})
}
pub fn inner(&mut self) -> &mut W {
&mut self.output
}
pub fn show_cursor(&mut self) -> io::Result<()> {
write!(self.output, "\x1b[?25h").map_err(io::Error::other)
}
pub fn hide_cursor(&mut self) -> io::Result<()> {
write!(self.output, "\x1b[?25l").map_err(io::Error::other)
}
pub fn set_cursor(&mut self, x: u16, y: u16) -> io::Result<()> {
write!(
self.output,
"\x1b[{};{}H",
y.saturating_add(1),
x.saturating_add(1)
)
.map_err(io::Error::other)
}
pub fn suspend(&mut self) -> io::Result<()> {
let _ = write!(self.output, "{}", RESTORE_SEQ);
let _ = self.output.flush();
if !self.is_null_mode {
if let Some(ref termios) = self.original_termios {
let _ = set_terminal_attr(self.output.as_fd(), termios);
}
}
Ok(())
}
pub fn resume(&mut self) -> io::Result<()> {
if !self.is_null_mode {
let fd = self.output.as_fd();
if let Some(ref termios) = self.original_termios {
let mut raw = *termios;
make_raw(&mut raw);
set_terminal_attr(fd, &raw)?;
}
}
write!(
self.output,
"\x1b[>1u\x1b[?1049h\x1b[?1003h\x1b[?1006h\x1b[?1007l\x1b[?7l\x1b[?25l\x1b[?2004h"
)?;
write!(self.output, "\x1b[2J\x1b[H")?;
self.output.flush()?;
Ok(())
}
pub fn capabilities(&self) -> &Capabilities {
&self.capabilities
}
pub fn emit(&mut self, seq: &str) -> io::Result<()> {
self.output.write_all(seq.as_bytes()).map_err(io::Error::other)
}
pub fn set_title(&mut self, title: &str) -> io::Result<()> {
if !self.capabilities.supports_title() {
return Ok(());
}
write!(self.output, "\x1b]0;{title}\x07").map_err(io::Error::other)
}
pub fn set_icon(&mut self, icon: &str) -> io::Result<()> {
write!(self.output, "\x1b]1;{icon}\x07").map_err(io::Error::other)
}
pub fn set_cursor_style(&mut self, shape: CursorShape) -> io::Result<()> {
let code = match shape {
CursorShape::Block => 2,
CursorShape::Underline => 4,
CursorShape::Bar => 6,
CursorShape::BlinkingBlock => 1,
CursorShape::BlinkingUnderline => 3,
CursorShape::BlinkingBar => 5,
};
write!(self.output, "\x1b[{code} q").map_err(io::Error::other)
}
}
impl<W: Write + AsFd> Write for Terminal<W> {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
self.output.write(buf)
}
fn flush(&mut self) -> io::Result<()> {
self.output.flush()
}
}
impl<W: Write + AsFd> AsFd for Terminal<W> {
fn as_fd(&self) -> BorrowedFd<'_> {
self.output.as_fd()
}
}