Skip to content

Add sixel backend #154

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
Nov 9, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Cargo.lock

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

61 changes: 29 additions & 32 deletions src/image_backends/kitty.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
use image::{imageops::FilterType, DynamicImage, GenericImageView};
use libc::{
ioctl, tcgetattr, tcsetattr, termios, winsize, ECHO, ICANON, STDIN_FILENO, STDOUT_FILENO,
TCSANOW, TIOCGWINSZ,
c_void, ioctl, poll, pollfd, read, tcgetattr, tcsetattr, termios, winsize, ECHO, ICANON,
POLLIN, STDIN_FILENO, STDOUT_FILENO, TCSANOW, TIOCGWINSZ,
};
use std::io::Read;
use std::sync::mpsc::{self, TryRecvError};
use std::time::Duration;
use std::time::Instant;

pub struct KittyBackend {}

Expand Down Expand Up @@ -41,38 +39,37 @@ impl KittyBackend {
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, b'_', b'G', b'\\'];
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, b'_', b'G']) && buf.ends_with(&[0x1B, b'\\']) {
sender.send(()).unwrap();
return;
}
match stop_receiver.try_recv() {
Err(TryRecvError::Empty) => {}
_ => return,
let start_time = Instant::now();
let mut stdin_pollfd = pollfd {
fd: STDIN_FILENO,
events: POLLIN,
revents: 0,
};
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 } {
if start_time.elapsed().as_millis() > 50 {
unsafe {
tcsetattr(STDIN_FILENO, TCSANOW, &old_attributes);
}
return false;
}
}
});
if receiver.recv_timeout(Duration::from_millis(50)).is_ok() {
let mut byte = 0;
unsafe {
tcsetattr(STDIN_FILENO, TCSANOW, &old_attributes);
read(STDIN_FILENO, &mut byte as *mut _ as *mut c_void, 1);
}
true
} else {
unsafe {
tcsetattr(STDIN_FILENO, TCSANOW, &old_attributes);
if allowed_bytes.contains(&byte) {
buf.push(byte);
}
if buf.starts_with(&[0x1B, b'_', b'G']) && buf.ends_with(&[0x1B, b'\\']) {
unsafe {
tcsetattr(STDIN_FILENO, TCSANOW, &old_attributes);
}
return true;
}
stop_sender.send(()).ok();
false
}
}
}
Expand Down
6 changes: 5 additions & 1 deletion src/image_backends/mod.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
use image::DynamicImage;

#[cfg(target_os = "linux")]
mod kitty;
pub mod kitty;
#[cfg(target_os = "linux")]
pub mod sixel;

pub trait ImageBackend {
fn add_image(&self, lines: Vec<String>, image: &DynamicImage) -> String;
Expand All @@ -11,6 +13,8 @@ pub trait ImageBackend {
pub fn get_best_backend() -> Option<Box<dyn ImageBackend>> {
if kitty::KittyBackend::supported() {
Some(Box::new(kitty::KittyBackend::new()))
} else if sixel::SixelBackend::supported() {
Some(Box::new(sixel::SixelBackend::new()))
} else {
None
}
Expand Down
165 changes: 165 additions & 0 deletions src/image_backends/sixel.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
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']) {
for attribute in buf[3..(buf.len() - 1)].split(|x| *x == b';') {
if attribute == &[b'4'] {
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()
}
}
9 changes: 6 additions & 3 deletions src/info.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use git2::Repository;
use image::DynamicImage;
use license::Detector;

use crate::image_backends;
use crate::image_backends::ImageBackend;
use crate::language::Language;
use crate::{AsciiArt, CommitInfo, Configuration, Error, InfoFieldOn};

Expand All @@ -35,6 +35,7 @@ pub struct Info {
disable_fields: InfoFieldOn,
bold_enabled: bool,
custom_image: Option<DynamicImage>,
image_backend: Option<Box<dyn ImageBackend>>
}

impl std::fmt::Display for Info {
Expand Down Expand Up @@ -238,11 +239,11 @@ impl std::fmt::Display for Info {
let mut info_lines = buf.lines();

if let Some(custom_image) = &self.custom_image {
if let Some(backend) = image_backends::get_best_backend() {
if let Some(image_backend) = &self.image_backend {
writeln!(
f,
"{}",
backend.add_image(
image_backend.add_image(
info_lines.map(|s| format!("{}{}", center_pad, s)).collect(),
custom_image
)
Expand Down Expand Up @@ -286,6 +287,7 @@ impl Info {
disabled: InfoFieldOn,
bold_flag: bool,
custom_image: Option<DynamicImage>,
image_backend: Option<Box<dyn ImageBackend>>,
no_merges: bool,
) -> Result<Info> {
let repo = Repository::discover(&dir).map_err(|_| Error::NotGitRepo)?;
Expand Down Expand Up @@ -326,6 +328,7 @@ impl Info {
disable_fields: disabled,
bold_enabled: bold_flag,
custom_image,
image_backend,
})
}

Expand Down
32 changes: 32 additions & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ mod license;
use ascii_art::AsciiArt;
use commit_info::CommitInfo;
use error::Error;
#[cfg(target_os = "linux")]
use image_backends::ImageBackend;
use info::Info;
use language::Language;

Expand Down Expand Up @@ -99,6 +101,11 @@ fn main() -> Result<()> {
.map(|language| language.to_string().to_lowercase())
.collect();

#[cfg(target_os = "linux")]
let possible_backends = ["kitty", "sixel"];
#[cfg(not(target_os = "linux"))]
let possible_backends = [];

let matches = App::new(crate_name!())
.version(crate_version!())
.author("o2sh <[email protected]>")
Expand Down Expand Up @@ -192,6 +199,13 @@ Possible values: [{0}{1}{2}{3}{4}{5}{6}{7}{8}{9}{10}{11}{12}{13}{14}{15}]",
.takes_value(true)
.help("Sets a custom image to use instead of the ascii logo"),
)
.arg(
Arg::with_name("image-backend")
.long("image-backend")
.takes_value(true)
.possible_values(&possible_backends)
.help("Overrides image backend detection"),
)
.arg(
Arg::with_name("no-merges")
.short("m")
Expand Down Expand Up @@ -258,6 +272,23 @@ Possible values: [{0}{1}{2}{3}{4}{5}{6}{7}{8}{9}{10}{11}{12}{13}{14}{15}]",
} else {
None
};
let image_backend = if custom_image.is_some() {
if let Some(backend_name) = matches.value_of("image-backend") {
#[cfg(target_os = "linux")]
let backend = Some(match backend_name {
"kitty" => Box::new(image_backends::kitty::KittyBackend::new()) as Box<dyn ImageBackend>,
"sixel" => Box::new(image_backends::sixel::SixelBackend::new()) as Box<dyn ImageBackend>,
_ => unreachable!()
});
#[cfg(not(target_os = "linux"))]
let backend = None;
backend
} else {
crate::image_backends::get_best_backend()
}
} else {
None
};

let no_merges = matches.is_present("no-merges");

Expand All @@ -268,6 +299,7 @@ Possible values: [{0}{1}{2}{3}{4}{5}{6}{7}{8}{9}{10}{11}{12}{13}{14}{15}]",
disable_fields,
bold_flag,
custom_image,
image_backend,
no_merges,
)?;

Expand Down