fast image operations
Diffstat (limited to 'src/term.rs')
-rw-r--r--src/term.rs199
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\")?;
+ 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
+}