forked from o2sh/onefetch
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
2 changed files
with
165 additions
and
0 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,161 @@ | ||
use image::{ | ||
imageops::{colorops, FilterType}, | ||
math::nq::NeuQuant, | ||
DynamicImage, GenericImageView, ImageBuffer, Pixel, Rgb, | ||
}; | ||
use libc::{ | ||
c_void, ioctl, poll, pollfd, read, tcgetattr, tcsetattr, termios, winsize, ECHO, ICANON, | ||
POLLIN, STDIN_FILENO, STDOUT_FILENO, TCSANOW, TIOCGWINSZ, | ||
}; | ||
use std::time::Instant; | ||
|
||
pub struct SixelBackend {} | ||
|
||
impl SixelBackend { | ||
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; | ||
new_attributes.c_lflag &= !ICANON; | ||
new_attributes.c_lflag &= !ECHO; | ||
tcsetattr(STDIN_FILENO, TCSANOW, &new_attributes); | ||
old_attributes | ||
}; | ||
|
||
// ask for the primary device attribute string | ||
println!("\x1B[c"); | ||
|
||
let start_time = Instant::now(); | ||
let mut stdin_pollfd = pollfd { | ||
fd: STDIN_FILENO, | ||
events: POLLIN, | ||
revents: 0, | ||
}; | ||
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 } { | ||
if start_time.elapsed().as_millis() > 50 { | ||
unsafe { | ||
tcsetattr(STDIN_FILENO, TCSANOW, &old_attributes); | ||
} | ||
return false; | ||
} | ||
} | ||
let mut byte = 0; | ||
unsafe { | ||
read(STDIN_FILENO, &mut byte as *mut _ as *mut c_void, 1); | ||
} | ||
buf.push(byte); | ||
if buf.starts_with(&[0x1B, b'[', b'?']) && buf.ends_with(&[b'c']) { | ||
unsafe { | ||
tcsetattr(STDIN_FILENO, TCSANOW, &old_attributes); | ||
} | ||
return true; | ||
} | ||
} | ||
} | ||
} | ||
|
||
impl super::ImageBackend for SixelBackend { | ||
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; | ||
|
||
let rgba_image = image.to_rgba(); // convert the image to rgba samples | ||
let flat_samples = rgba_image.as_flat_samples(); | ||
let mut rgba_image = rgba_image.clone(); | ||
// reduce the amount of colors using dithering | ||
colorops::dither( | ||
&mut rgba_image, | ||
&NeuQuant::new(10, 16, flat_samples.image_slice().unwrap()), | ||
); | ||
let rgb_image = ImageBuffer::from_fn(rgba_image.width(), rgba_image.height(), |x, y| { | ||
let rgba_pixel = rgba_image.get_pixel(x, y); | ||
let mut rgb_pixel = rgba_pixel.to_rgb(); | ||
for subpixel in &mut rgb_pixel.0 { | ||
*subpixel = (*subpixel as f32 / 255.0 * rgba_pixel[3] as f32) as u8; | ||
} | ||
rgb_pixel | ||
}); | ||
|
||
let mut image_data = Vec::<u8>::new(); | ||
image_data.extend("\x1BPq".as_bytes()); // start sixel data | ||
image_data.extend(format!("\"1;1;{};{}", image.width(), image.height()).as_bytes()); | ||
|
||
let mut colors = std::collections::HashMap::<Rgb<u8>, u8>::new(); | ||
// subtract 1 -> divide -> add 1 to round up the integer division | ||
for i in 0..((rgb_image.height() - 1) / 6 + 1) { | ||
let sixel_row = rgb_image.view( | ||
0, | ||
i * 6, | ||
rgb_image.width(), | ||
std::cmp::min(6, rgb_image.height() - i * 6), | ||
); | ||
for (_, _, pixel) in sixel_row.pixels() { | ||
if !colors.contains_key(&pixel) { | ||
// sixel uses percentages for rgb values | ||
let color_multiplier = 100.0 / 255.0; | ||
image_data.extend( | ||
format!( | ||
"#{};2;{};{};{};", | ||
colors.len(), | ||
(pixel[0] as f32 * color_multiplier) as u32, | ||
(pixel[1] as f32 * color_multiplier) as u32, | ||
(pixel[2] as f32 * color_multiplier) as u32 | ||
) | ||
.as_bytes(), | ||
); | ||
colors.insert(pixel, colors.len() as u8); | ||
} | ||
} | ||
for (color, color_index) in &colors { | ||
let mut sixel_samples = Vec::<u8>::with_capacity(sixel_row.width() as usize); | ||
sixel_samples.resize(sixel_row.width() as usize, 0); | ||
for (x, y, pixel) in sixel_row.pixels() { | ||
if color == &pixel { | ||
sixel_samples[x as usize] = sixel_samples[x as usize] | (1 << y); | ||
} | ||
} | ||
image_data.extend(format!("#{}", color_index).bytes()); | ||
image_data.extend(sixel_samples.iter().map(|x| x + 0x3F)); | ||
image_data.push('$' as u8); | ||
} | ||
image_data.push('-' as u8); | ||
} | ||
image_data.extend("\x1B\\".as_bytes()); | ||
|
||
image_data.extend(format!("\x1B[{}A", image_rows as u32 + 2).as_bytes()); // move cursor to top-left corner | ||
image_data.extend(format!("\x1B[{}C", image_columns as u32 + 1).as_bytes()); // move cursor to top-right corner 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() | ||
} | ||
} |