use image::{DynamicImage, Rgb};
use ratatui::layout::Rect;
#[cfg(feature = "rustix")]
use rustix::termios::Winsize;
#[cfg(all(feature = "sixel", feature = "rustix"))]
use rustix::termios::{LocalModes, OptionalActions};
#[cfg(feature = "serde")]
use serde::Deserialize;
#[cfg(feature = "sixel")]
use crate::backend::sixel::{resizeable::SixelState, FixedSixel};
use crate::{
backend::{
halfblocks::{resizeable::HalfblocksState, FixedHalfblocks},
kitty::{FixedKitty, KittyState},
FixedBackend, ResizeBackend,
},
FontSize, ImageSource, Resize, Result,
};
#[derive(Clone, Copy)]
pub struct Picker {
font_size: FontSize,
background_color: Option<Rgb<u8>>,
backend_type: BackendType,
kitty_counter: u8,
}
#[derive(PartialEq, Clone, Debug, Copy)]
#[cfg_attr(
feature = "serde",
derive(Deserialize),
serde(rename_all = "lowercase")
)]
pub enum BackendType {
Halfblocks,
#[cfg(feature = "sixel")]
Sixel,
Kitty,
}
impl BackendType {
pub fn next(&self) -> BackendType {
match self {
#[cfg(not(feature = "sixel"))]
BackendType::Halfblocks => BackendType::Kitty,
#[cfg(feature = "sixel")]
BackendType::Halfblocks => BackendType::Sixel,
#[cfg(feature = "sixel")]
BackendType::Sixel => BackendType::Kitty,
BackendType::Kitty => BackendType::Halfblocks,
}
}
}
impl Picker {
pub fn new(
font_size: FontSize,
backend_type: BackendType,
background_color: Option<Rgb<u8>>,
) -> Result<Picker> {
Ok(Picker {
font_size,
background_color,
backend_type,
kitty_counter: 0,
})
}
#[cfg(feature = "rustix")]
pub fn from_termios(background_color: Option<Rgb<u8>>) -> Result<Picker> {
let stdout = rustix::stdio::stdout();
let font_size = font_size(rustix::termios::tcgetwinsize(stdout)?)?;
Picker::new(font_size, guess_backend(), background_color)
}
pub fn set(&mut self, r#type: BackendType) {
self.backend_type = r#type;
}
pub fn cycle_backends(&mut self) -> BackendType {
self.backend_type = self.backend_type.next();
self.backend_type
}
pub fn new_static_fit(
&mut self,
image: DynamicImage,
size: Rect,
resize: Resize,
) -> Result<Box<dyn FixedBackend>> {
let source = ImageSource::new(image, self.font_size);
match self.backend_type {
BackendType::Halfblocks => Ok(Box::new(FixedHalfblocks::from_source(
&source,
resize,
self.background_color,
size,
)?)),
#[cfg(feature = "sixel")]
BackendType::Sixel => Ok(Box::new(FixedSixel::from_source(
&source,
resize,
self.background_color,
size,
)?)),
BackendType::Kitty => {
self.kitty_counter += 1;
Ok(Box::new(FixedKitty::from_source(
&source,
resize,
self.background_color,
size,
self.kitty_counter,
)?))
}
}
}
pub fn new_state(&mut self) -> Box<dyn ResizeBackend> {
match self.backend_type {
BackendType::Halfblocks => Box::<HalfblocksState>::default(),
#[cfg(feature = "sixel")]
BackendType::Sixel => Box::<SixelState>::default(),
BackendType::Kitty => {
self.kitty_counter += 1;
Box::new(KittyState::new(self.kitty_counter))
}
}
}
pub fn backend_type(&self) -> &BackendType {
&self.backend_type
}
pub fn font_size(&self) -> FontSize {
self.font_size
}
}
#[cfg(feature = "rustix")]
pub fn font_size(winsize: Winsize) -> Result<FontSize> {
let Winsize {
ws_xpixel: x,
ws_ypixel: y,
ws_col: cols,
ws_row: rows,
} = winsize;
if x == 0 || y == 0 || cols == 0 || rows == 0 {
return Err(String::from("font_size zero value").into());
}
Ok((x / cols, y / rows))
}
#[cfg(feature = "rustix")]
fn guess_backend() -> BackendType {
if let Ok(term) = std::env::var("TERM") {
match term.as_str() {
#[cfg(all(feature = "sixel", feature = "rustix"))]
"mlterm" | "yaft-256color" => {
return BackendType::Sixel;
}
term => {
#[cfg(all(feature = "sixel", feature = "rustix"))]
match check_device_attrs() {
Ok(t) => return t,
Err(err) => eprintln!("{err}"),
};
if term.contains("kitty") {
return BackendType::Kitty;
}
#[cfg(all(feature = "sixel", feature = "rustix"))]
if let Ok(term_program) = std::env::var("TERM_PROGRAM") {
if term_program == "MacTerm" {
return BackendType::Sixel;
}
}
}
}
}
BackendType::Halfblocks
}
#[cfg(all(feature = "sixel", feature = "rustix"))]
fn check_device_attrs() -> Result<BackendType> {
let stdin = rustix::stdio::stdin();
let termios_original = rustix::termios::tcgetattr(stdin)?;
let mut termios = termios_original.clone();
termios.local_modes &= !LocalModes::ICANON;
termios.local_modes &= !LocalModes::ECHO;
rustix::termios::tcsetattr(stdin, OptionalActions::Drain, &termios)?;
rustix::io::write(rustix::stdio::stdout(), b"\x1b[c")?;
let mut buf = String::new();
loop {
let mut charbuf = [0; 1];
rustix::io::read(stdin, &mut charbuf)?;
if charbuf[0] == 0 {
continue;
}
buf.push(char::from(charbuf[0]));
if charbuf[0] == b'c' {
break;
}
}
rustix::termios::tcsetattr(stdin, OptionalActions::Now, &termios_original)?;
if buf.contains(";4;") || buf.contains("?4;") || buf.contains(";4c") || buf.contains("?4c") {
Ok(BackendType::Sixel)
} else {
Err(format!(
"CSI sixel support not detected: ^[{}",
if buf.len() > 1 {
&buf[1..]
} else {
"(nothing)"
}
)
.into())
}
}
#[cfg(all(test, feature = "rustix", feature = "sixel"))]
mod tests {
use std::assert_eq;
use crate::picker::{font_size, BackendType, Picker};
use rustix::termios::Winsize;
#[test]
fn test_font_size() {
assert!(font_size(Winsize {
ws_row: 0,
ws_col: 0,
ws_xpixel: 10,
ws_ypixel: 10
})
.is_err());
assert!(font_size(Winsize {
ws_row: 10,
ws_col: 10,
ws_xpixel: 0,
ws_ypixel: 0
})
.is_err());
}
#[test]
fn test_cycle_backends() {
let mut picker = Picker::new((1, 1), BackendType::Halfblocks, None).unwrap();
#[cfg(feature = "sixel")]
assert_eq!(picker.cycle_backends(), BackendType::Sixel);
assert_eq!(picker.cycle_backends(), BackendType::Kitty);
assert_eq!(picker.cycle_backends(), BackendType::Halfblocks);
}
}