Skip to content
94 changes: 68 additions & 26 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion image/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,4 @@ image.workspace = true
[target.'cfg(not(windows))'.dependencies]
color_quant = "1.1.0"
base64 = "0.22.1"
libc = "0.2.177"
rustix = { version = "1.1.2", features = ["termios", "event"] }
16 changes: 3 additions & 13 deletions image/src/iterm.rs
Original file line number Diff line number Diff line change
@@ -1,37 +1,27 @@
use crate::get_dimensions;
use anyhow::Result;
use base64::{engine, Engine};
use image::{imageops::FilterType, DynamicImage};
use rustix::termios::tcgetwinsize;
use std::env;
use std::io::Cursor;

pub struct ITermBackend {}
pub struct ITermBackend;

impl ITermBackend {
pub fn new() -> Self {
ITermBackend {}
}

pub fn supported() -> bool {
let term_program = env::var("TERM_PROGRAM").unwrap_or_else(|_| "".to_string());
term_program == "iTerm.app"
}
}

impl Default for ITermBackend {
fn default() -> Self {
Self::new()
}
}

impl super::ImageBackend for ITermBackend {
fn add_image(
&self,
lines: Vec<String>,
image: &DynamicImage,
_colors: usize,
) -> Result<String> {
let tty_size = unsafe { get_dimensions() };
let tty_size = tcgetwinsize(std::io::stdin())?;
let width_ratio = f64::from(tty_size.ws_col) / f64::from(tty_size.ws_xpixel);
let height_ratio = f64::from(tty_size.ws_row) / f64::from(tty_size.ws_ypixel);

Expand Down
81 changes: 33 additions & 48 deletions image/src/kitty.rs
Original file line number Diff line number Diff line change
@@ -1,32 +1,30 @@
use crate::get_dimensions;
use anyhow::Result;
use anyhow::{Context as _, Result};
use base64::{engine, Engine};
use image::{imageops::FilterType, DynamicImage};
use libc::{
c_void, poll, pollfd, read, tcgetattr, tcsetattr, termios, ECHO, ICANON, POLLIN, STDIN_FILENO,
TCSANOW,
};

use rustix::event::{poll, PollFd, PollFlags, Timespec};
use rustix::io::read;
use rustix::termios::{tcgetattr, tcgetwinsize, tcsetattr, LocalModes, OptionalActions};

use std::io::{stdout, Write};
use std::os::fd::AsFd as _;
use std::time::Instant;

pub struct KittyBackend {}
pub struct KittyBackend;

impl KittyBackend {
pub fn new() -> Self {
Self {}
}

pub fn supported() -> bool {
pub fn supported() -> Result<bool> {
let stdin = std::io::stdin();
// save terminal attributes and disable canonical input processing mode
let old_attributes = unsafe {
let mut old_attributes: termios = std::mem::zeroed();
tcgetattr(STDIN_FILENO, &mut old_attributes);
let old_attributes = {
let old = tcgetattr(&stdin).context("Failed to recieve terminal attibutes")?;

let mut new_attributes = old_attributes;
new_attributes.c_lflag &= !ICANON;
new_attributes.c_lflag &= !ECHO;
tcsetattr(STDIN_FILENO, TCSANOW, &new_attributes);
old_attributes
let mut new = old.clone();
new.local_modes &= !LocalModes::ICANON;
new.local_modes &= !LocalModes::ECHO;
tcsetattr(&stdin, OptionalActions::Now, &new)
.context("Failed to update terminal attributes")?;
old
};

// generate red rgba test image
Expand All @@ -38,57 +36,44 @@ impl KittyBackend {
"\x1B_Gi=1,f=32,s=32,v=32,a=q;{}\x1B\\",
engine::general_purpose::STANDARD.encode(&test_image)
);
stdout().flush().unwrap();
stdout().flush()?;

let start_time = Instant::now();
let mut stdin_pollfd = pollfd {
fd: STDIN_FILENO,
events: POLLIN,
revents: 0,
};
let stdin_fd = stdin.as_fd();
let mut stdin_pollfd = [PollFd::new(&stdin_fd, PollFlags::IN)];
let allowed_bytes = [0x1B, b'_', b'G', b'\\'];
let mut buf = Vec::<u8>::new();
loop {
// check for timeout while polling to avoid blocking the main thread
while unsafe { poll(&mut stdin_pollfd, 1, 0) < 1 } {
while poll(&mut stdin_pollfd, Some(&Timespec::default()))? < 1 {
if start_time.elapsed().as_millis() > 50 {
unsafe {
tcsetattr(STDIN_FILENO, TCSANOW, &old_attributes);
}
return false;
tcsetattr(&stdin, OptionalActions::Now, &old_attributes)
.context("Failed to update terminal attributes")?;
return Ok(false);
}
}
let mut byte = 0;
unsafe {
read(STDIN_FILENO, &mut byte as *mut _ as *mut c_void, 1);
}
if allowed_bytes.contains(&byte) {
buf.push(byte);
let mut byte = [0];
read(&stdin, &mut byte)?;
if allowed_bytes.contains(&byte[0]) {
buf.push(byte[0]);
}
if buf.starts_with(&[0x1B, b'_', b'G']) && buf.ends_with(&[0x1B, b'\\']) {
unsafe {
tcsetattr(STDIN_FILENO, TCSANOW, &old_attributes);
}
return true;
tcsetattr(&stdin, OptionalActions::Now, &old_attributes)
.context("Failed to update terminal attributes")?;
return Ok(true);
}
}
}
}

impl Default for KittyBackend {
fn default() -> Self {
Self::new()
}
}

impl super::ImageBackend for KittyBackend {
fn add_image(
&self,
lines: Vec<String>,
image: &DynamicImage,
_colors: usize,
) -> Result<String> {
let tty_size = unsafe { get_dimensions() };
let tty_size = tcgetwinsize(std::io::stdin())?;
let width_ratio = f64::from(tty_size.ws_col) / f64::from(tty_size.ws_xpixel);
let height_ratio = f64::from(tty_size.ws_row) / f64::from(tty_size.ws_ypixel);

Expand Down
Loading