fast image operations
Diffstat (limited to 'src/term.rs')
| -rw-r--r-- | src/term.rs | 199 |
1 files changed, 199 insertions, 0 deletions
diff --git a/src/term.rs b/src/term.rs new file mode 100644 index 0000000..aeae9fb --- /dev/null +++ b/src/term.rs @@ -0,0 +1,199 @@ +//! terminal outputs +//! produces output for any terminal supporting one of the +//! ```text +//! Kitty Graphics Protocol +//! Iterm2 Inline Image Protocol +//! Sixel Bitmap Graphics Format +//! ``` +//! with a fallback for dumb terminals. +//! +//! the (second?) best way to debug your images. +mod bloc; +mod kitty; +mod sixel; +mod size; +use crate::Image; +pub use bloc::Bloc; +pub use iterm2::Iterm2; +pub use kitty::Kitty; +pub use sixel::Sixel; +use std::fmt::{Result, Write}; + +mod seal { + pub trait Sealed {} +} +use seal::Sealed; +#[doc(hidden)] +pub trait Basic: Sealed {} +impl Sealed for [(); 1] {} +impl Basic for [(); 1] {} +impl Sealed for [(); 2] {} +impl Basic for [(); 2] {} +impl Sealed for [(); 3] {} +impl Basic for [(); 3] {} +impl Sealed for [(); 4] {} +impl Basic for [(); 4] {} + +mod b64; +mod iterm2; + +impl<'a, const N: usize> std::fmt::Display for Image<&'a [u8], N> +where + [(); N]: Basic, +{ + /// Display an image in the terminal. + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result { + Display(*self).write(f) + } +} + +/// Print an image in the terminal. +/// +/// This is a wrapper for `print!("{}", term::Display(image))` +pub fn print<T: AsRef<[u8]>, const N: usize>(i: Image<T, N>) +where + [(); N]: Basic, + Display<Image<T, N>>: std::fmt::Display, +{ + print!("{}", Display(i)) +} + +#[derive(Copy, Clone)] +/// Display an image in the terminal. +/// This type implements [`Display`](std::fmt::Display) and [`Debug`](std::fmt::Debug). +pub struct Display<T>(pub T); + +impl<T> std::ops::Deref for Display<T> { + type Target = T; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl<T: AsRef<[u8]>, const N: usize> std::fmt::Debug for Display<Image<T, N>> +where + [(); N]: Basic, +{ + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result { + Display(self.as_ref()).write(f) + } +} + +impl<const N: usize> Display<Image<&[u8], N>> +where + [(); N]: Basic, +{ + /// Write $TERM protocol encoded image data. + pub fn write(self, f: &mut impl Write) -> Result { + if let Ok(term) = std::env::var("TERM") { + match &*term { + "mlterm" | "yaft-256color" => return Sixel(self.0).write(f), + x if x.contains("kitty") => return Kitty(self.0).write(f), + _ => (), + } + } + if let Ok(term_program) = std::env::var("TERM_PROGRAM") { + match &*term_program { + "MacTerm" => return Sixel(self.0).write(f), + "iTerm" | "WezTerm" => return Iterm2(self.0).write(f), + _ => (), + } + } + if let Ok("iTerm") = std::env::var("LC_TERMINAL").as_deref() { + return Iterm2(self.0).write(f); + } + #[cfg(unix)] + return self + .guess_harder(f) + .unwrap_or_else(|| Bloc(self.0).write(f)); + #[cfg(not(unix))] + return Bloc(*self).write(f); + } + + #[cfg(unix)] + // https://github.com/benjajaja/ratatui-image/blob/eeb2a1b26fbe360259f213d6d5eb5449c8ae1d6e/src/picker.rs#L226 + fn guess_harder(&self, to: &mut impl Write) -> Option<Result> { + // contains a kitty gfx and sixel query, the `\x1b[c` is for sixels + let buf = query(r"_Gi=31,s=1,v=1,a=q,t=d,f=24;AAAA\[c")?; + if buf.contains("_Gi=31;OK") { + Some(Kitty(self.as_ref()).write(to)) + } else if buf.contains(";4;") + || buf.contains("?4;") + || buf.contains(";4c") + || buf.contains("?4c") + { + Some(Sixel(self.as_ref()).write(to)) + } else { + None + } + } +} +#[cfg(not(unix))] +fn query(device_query_code: &'static str) -> Option<String> { + None +} +#[cfg(unix)] +// https://github.com/benjajaja/ratatui-image/blob/master/src/picker.rs#L226 +fn query(device_query_code: &'static str) -> Option<String> { + extern crate libc; + use std::mem::MaybeUninit; + fn r(result: i32) -> Option<()> { + (result != -1).then_some(()) + } + + let mut termios = MaybeUninit::<libc::termios>::uninit(); + // SAFETY: get termios of stdin + r(unsafe { libc::tcgetattr(0, termios.as_mut_ptr()) })?; + // SAFETY: gotten + let termios = unsafe { termios.assume_init() }; + + // SAFETY: turn off echo and canonical (requires enter before stdin reads) modes + unsafe { + libc::tcsetattr( + 0, + libc::TCSADRAIN, + &libc::termios { + c_lflag: termios.c_lflag & !libc::ICANON & !libc::ECHO, + ..termios + }, + ) + }; + + let buf = try { + // SAFETY: linux time out'd reading + unsafe { + println!("{device_query_code}"); + let mut buf = Vec::new(); + let mut tmp = [0; 1 << 5]; + loop { + let mut x: libc::fd_set = std::mem::zeroed::<libc::fd_set>(); + libc::FD_SET(0, &mut x); + match libc::select( + 1, + &mut x, + 0 as _, + 0 as _, + &mut libc::timeval { + tv_sec: 0, + tv_usec: 5e5 as _, + }, + ) { + 0 => break, + -1 => return None, + _ => {} + } + match libc::read(libc::STDIN_FILENO, tmp.as_mut_ptr().cast(), tmp.len()) { + 0 => continue, + -1 => return None, + n => buf.extend_from_slice(&tmp[..n as _]), + } + } + String::from_utf8(buf).ok()? + } + }; + + // SAFETY: reset attrs to what they were before we became nosy + unsafe { libc::tcsetattr(0, libc::TCSADRAIN, &termios) }; + buf +} |