-
Notifications
You must be signed in to change notification settings - Fork 284
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add support for displaying a custom image instead of ascii art
- Loading branch information
Showing
6 changed files
with
215 additions
and
21 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,139 @@ | ||
use image::{imageops::FilterType, DynamicImage, GenericImageView}; | ||
use libc::{ | ||
ioctl, tcgetattr, tcsetattr, termios, winsize, ECHO, ICANON, STDIN_FILENO, STDOUT_FILENO, | ||
TCSANOW, TIOCGWINSZ, | ||
}; | ||
use std::io::Read; | ||
use std::sync::mpsc::{self, TryRecvError}; | ||
use std::time::Duration; | ||
|
||
pub struct KittyBackend {} | ||
|
||
impl KittyBackend { | ||
pub fn new() -> Self { | ||
Self {} | ||
} | ||
|
||
pub fn supported() -> bool { | ||
// 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 mut new_attributes = old_attributes.clone(); | ||
new_attributes.c_lflag &= !ICANON; | ||
new_attributes.c_lflag &= !ECHO; | ||
tcsetattr(STDIN_FILENO, TCSANOW, &new_attributes); | ||
old_attributes | ||
}; | ||
|
||
// generate red rgba test image | ||
let mut test_image = Vec::<u8>::with_capacity(32 * 32 * 4); | ||
test_image.extend( | ||
std::iter::repeat([255, 0, 0, 255].iter()) | ||
.take(32 * 32) | ||
.flatten(), | ||
); | ||
|
||
// print the test image with the action set to query | ||
println!( | ||
"\x1B_Gi=1,f=32,s=32,v=32,a=q;{}\x1B\\", | ||
base64::encode(&test_image) | ||
); | ||
|
||
// a new thread is required to avoid blocking the main thread if the terminal doesn't respond | ||
let (sender, receiver) = mpsc::channel::<()>(); | ||
let (stop_sender, stop_receiver) = mpsc::channel::<()>(); | ||
std::thread::spawn(move || { | ||
let mut buf = Vec::<u8>::new(); | ||
let allowed_bytes = [0x1B, '_' as u8, 'G' as u8, '\\' as u8]; | ||
for byte in std::io::stdin().lock().bytes() { | ||
let byte = byte.unwrap(); | ||
if allowed_bytes.contains(&byte) { | ||
buf.push(byte); | ||
} | ||
if buf.starts_with(&[0x1B, '_' as u8, 'G' as u8]) | ||
&& buf.ends_with(&[0x1B, '\\' as u8]) | ||
{ | ||
sender.send(()).unwrap(); | ||
return; | ||
} | ||
match stop_receiver.try_recv() { | ||
Err(TryRecvError::Empty) => {} | ||
_ => return, | ||
} | ||
} | ||
}); | ||
if let Ok(_) = receiver.recv_timeout(Duration::from_millis(50)) { | ||
unsafe { | ||
tcsetattr(STDIN_FILENO, TCSANOW, &old_attributes); | ||
} | ||
true | ||
} else { | ||
unsafe { | ||
tcsetattr(STDIN_FILENO, TCSANOW, &old_attributes); | ||
} | ||
stop_sender.send(()).ok(); | ||
false | ||
} | ||
} | ||
} | ||
|
||
impl super::ImageBackend for KittyBackend { | ||
fn add_image(&self, lines: Vec<String>, image: &DynamicImage) -> String { | ||
let tty_size = unsafe { | ||
let tty_size: winsize = std::mem::zeroed(); | ||
ioctl(STDOUT_FILENO, TIOCGWINSZ, &tty_size); | ||
tty_size | ||
}; | ||
let width_ratio = tty_size.ws_col as f64 / tty_size.ws_xpixel as f64; | ||
let height_ratio = tty_size.ws_row as f64 / tty_size.ws_ypixel as f64; | ||
|
||
// resize image to fit the text height with the Lanczos3 algorithm | ||
let image = image.resize( | ||
u32::max_value(), | ||
(lines.len() as f64 / height_ratio) as u32, | ||
FilterType::Lanczos3, | ||
); | ||
let _image_columns = width_ratio * image.width() as f64; | ||
let image_rows = height_ratio * image.height() as f64; | ||
|
||
// convert the image to rgba samples | ||
let rgba_image = image.to_rgba(); | ||
let flat_samples = rgba_image.as_flat_samples(); | ||
let raw_image = flat_samples | ||
.image_slice() | ||
.expect("Conversion from image to rgba samples failed"); | ||
assert_eq!( | ||
image.width() as usize * image.height() as usize * 4, | ||
raw_image.len() | ||
); | ||
|
||
let encoded_image = base64::encode(&raw_image); // image data is base64 encoded | ||
let mut image_data = Vec::<u8>::new(); | ||
for chunk in encoded_image.as_bytes().chunks(4096) { | ||
// send a 4096 byte chunk of base64 encoded rgba image data | ||
image_data.extend( | ||
format!( | ||
"\x1B_Gf=32,s={},v={},m=1,a=T;", | ||
image.width(), | ||
image.height() | ||
) | ||
.as_bytes(), | ||
); | ||
image_data.extend(chunk); | ||
image_data.extend("\x1B\\".as_bytes()); | ||
} | ||
image_data.extend("\x1B_Gm=0;\x1B\\".as_bytes()); // write empty last chunk | ||
image_data.extend(format!("\x1B[{}A", image_rows as u32 - 1).as_bytes()); // move cursor to start of image | ||
let mut i = 0; | ||
for line in &lines { | ||
image_data.extend(format!("\x1B[s{}\x1B[u\x1B[1B", line).as_bytes()); | ||
i += 1; | ||
} | ||
image_data | ||
.extend(format!("\n\x1B[{}B", lines.len().max(image_rows as usize) - i).as_bytes()); // move cursor to end of image | ||
|
||
String::from_utf8(image_data).unwrap() | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
use image::DynamicImage; | ||
|
||
#[cfg(target_os = "linux")] | ||
mod kitty; | ||
|
||
pub trait ImageBackend { | ||
fn add_image(&self, lines: Vec<String>, image: &DynamicImage) -> String; | ||
} | ||
|
||
#[cfg(target_os = "linux")] | ||
pub fn get_best_backend() -> Option<Box<dyn ImageBackend>> { | ||
if kitty::KittyBackend::supported() { | ||
Some(Box::new(kitty::KittyBackend::new())) | ||
} else { | ||
None | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters