use std::io::{self, Stderr};
use crossterm::{
event::DisableMouseCapture,
execute,
terminal::{EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode},
};
use ratatui::{Terminal, backend::CrosstermBackend};
pub struct TerminalGuard {
terminal: Terminal<CrosstermBackend<Stderr>>,
active: bool,
}
impl TerminalGuard {
pub fn enter() -> io::Result<Self> {
enable_raw_mode()?;
let setup = (|| {
execute!(io::stderr(), EnterAlternateScreen)?;
execute!(io::stderr(), DisableMouseCapture)?;
Terminal::new(CrosstermBackend::new(io::stderr()))
})();
match setup {
Ok(terminal) => Ok(Self {
terminal,
active: true,
}),
Err(e) => {
let _ = disable_raw_mode();
let _ = execute!(io::stderr(), LeaveAlternateScreen);
Err(e)
}
}
}
pub fn terminal(&mut self) -> &mut Terminal<CrosstermBackend<Stderr>> {
&mut self.terminal
}
pub fn suspend(&mut self) {
let _ = disable_raw_mode();
let _ = execute!(
self.terminal.backend_mut(),
LeaveAlternateScreen,
DisableMouseCapture
);
}
pub fn resume(&mut self) {
let _ = enable_raw_mode();
let _ = execute!(self.terminal.backend_mut(), EnterAlternateScreen);
let _ = execute!(self.terminal.backend_mut(), DisableMouseCapture);
self.force_full_redraw();
}
fn force_full_redraw(&mut self) {
use crossterm::{
cursor::MoveTo,
terminal::{Clear, ClearType},
};
let _ = execute!(
self.terminal.backend_mut(),
Clear(ClearType::All),
MoveTo(0, 0)
);
if let Ok(fresh) = Terminal::new(CrosstermBackend::new(io::stderr())) {
self.terminal = fresh;
}
}
pub fn restore(&mut self) {
if !self.active {
return;
}
self.active = false;
let _ = disable_raw_mode();
let _ = execute!(
self.terminal.backend_mut(),
LeaveAlternateScreen,
DisableMouseCapture
);
let _ = self.terminal.show_cursor();
}
}
impl Drop for TerminalGuard {
fn drop(&mut self) {
self.restore();
}
}
pub fn build_image_picker() -> (ratatui_image::picker::Picker, String) {
use ratatui_image::picker::{Picker, ProtocolType};
let stdout_is_tty = crossterm::tty::IsTty::is_tty(&io::stdout());
let term = std::env::var("TERM").ok();
let term_program = std::env::var("TERM_PROGRAM").ok();
let protocol = if stdout_is_tty {
detect_image_protocol(term.as_deref(), term_program.as_deref(), |name| {
std::env::var_os(name).is_some()
})
} else {
ProtocolType::Halfblocks
};
let font_size = crossterm::terminal::window_size()
.ok()
.filter(|ws| ws.columns > 0 && ws.rows > 0 && ws.width > 0 && ws.height > 0)
.map(|ws| (ws.width / ws.columns, ws.height / ws.rows));
let desc = format!(
"image protocol: stdout_is_tty={stdout_is_tty}, TERM={term:?}, \
TERM_PROGRAM={term_program:?}, protocol={protocol:?}, font_size={font_size:?}"
);
let picker = match font_size {
Some(fs) if protocol != ProtocolType::Halfblocks => {
#[allow(deprecated)]
let mut picker = Picker::from_fontsize(fs.into());
picker.set_protocol_type(protocol);
picker
}
_ => Picker::halfblocks(),
};
(picker, desc)
}
fn detect_image_protocol(
term: Option<&str>,
term_program: Option<&str>,
env_present: impl Fn(&str) -> bool,
) -> ratatui_image::picker::ProtocolType {
use ratatui_image::picker::ProtocolType;
const KITTY_ENV_MARKERS: &[&str] = &[
"KITTY_WINDOW_ID", "GHOSTTY_RESOURCES_DIR", "KONSOLE_VERSION", ];
const ITERM2_ENV_MARKERS: &[&str] = &[
"WEZTERM_PANE", "WEZTERM_EXECUTABLE", "ITERM_SESSION_ID", ];
if KITTY_ENV_MARKERS.iter().any(|m| env_present(m)) {
return ProtocolType::Kitty;
}
if ITERM2_ENV_MARKERS.iter().any(|m| env_present(m)) {
return ProtocolType::Iterm2;
}
if let Some(tp) = term_program {
match tp {
"ghostty" => return ProtocolType::Kitty,
"WezTerm" | "iTerm.app" => return ProtocolType::Iterm2,
_ => {}
}
}
if let Some(t) = term {
let t = t.to_ascii_lowercase();
if t.contains("kitty") || t.contains("ghostty") {
return ProtocolType::Kitty;
}
if t.contains("wezterm") {
return ProtocolType::Iterm2;
}
if t.contains("foot") {
return ProtocolType::Sixel;
}
}
ProtocolType::Halfblocks
}
#[cfg(test)]
mod tests {
use super::detect_image_protocol;
use ratatui_image::picker::ProtocolType;
fn no_env(_: &str) -> bool {
false
}
#[test]
fn apple_terminal_falls_back_to_halfblocks() {
assert_eq!(
detect_image_protocol(Some("xterm-256color"), Some("Apple_Terminal"), no_env),
ProtocolType::Halfblocks,
);
}
#[test]
fn warp_and_unknown_terminals_use_halfblocks() {
assert_eq!(
detect_image_protocol(Some("xterm-256color"), Some("WarpTerminal"), no_env),
ProtocolType::Halfblocks,
);
assert_eq!(
detect_image_protocol(Some("xterm-256color"), None, no_env),
ProtocolType::Halfblocks,
);
assert_eq!(
detect_image_protocol(None, None, no_env),
ProtocolType::Halfblocks,
);
}
#[test]
fn kitty_is_detected_via_term_and_env() {
assert_eq!(
detect_image_protocol(Some("xterm-kitty"), None, no_env),
ProtocolType::Kitty,
);
assert_eq!(
detect_image_protocol(Some("xterm-256color"), None, |name| name
== "KITTY_WINDOW_ID"),
ProtocolType::Kitty,
);
}
#[test]
fn ghostty_uses_kitty_protocol() {
assert_eq!(
detect_image_protocol(Some("xterm-256color"), Some("ghostty"), no_env),
ProtocolType::Kitty,
);
assert_eq!(
detect_image_protocol(Some("xterm-ghostty"), None, no_env),
ProtocolType::Kitty,
);
assert_eq!(
detect_image_protocol(None, None, |name| name == "GHOSTTY_RESOURCES_DIR"),
ProtocolType::Kitty,
);
}
#[test]
fn wezterm_and_iterm_use_iterm2_protocol() {
for tp in ["WezTerm", "iTerm.app"] {
assert_eq!(
detect_image_protocol(Some("xterm-256color"), Some(tp), no_env),
ProtocolType::Iterm2,
"expected {tp} to use the iTerm2 protocol"
);
}
assert_eq!(
detect_image_protocol(Some("wezterm"), None, no_env),
ProtocolType::Iterm2,
);
}
#[test]
fn env_markers_map_to_expected_protocols() {
for marker in [
"KITTY_WINDOW_ID",
"GHOSTTY_RESOURCES_DIR",
"KONSOLE_VERSION",
] {
assert_eq!(
detect_image_protocol(None, None, |name| name == marker),
ProtocolType::Kitty,
"expected env marker {marker} to map to kitty"
);
}
for marker in ["WEZTERM_PANE", "WEZTERM_EXECUTABLE", "ITERM_SESSION_ID"] {
assert_eq!(
detect_image_protocol(None, None, |name| name == marker),
ProtocolType::Iterm2,
"expected env marker {marker} to map to iTerm2"
);
}
}
#[test]
fn foot_uses_sixel() {
assert_eq!(
detect_image_protocol(Some("foot"), None, no_env),
ProtocolType::Sixel,
);
}
}