Skip to content

Commit 6b145a5

Browse files
authored
Merge pull request #154 from CephalonRho/sixel-backend
Add sixel backend
2 parents f74f741 + 745982d commit 6b145a5

File tree

5 files changed

+237
-36
lines changed

5 files changed

+237
-36
lines changed

Diff for: src/image_backends/kitty.rs

+29-32
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,9 @@
11
use image::{imageops::FilterType, DynamicImage, GenericImageView};
22
use libc::{
3-
ioctl, tcgetattr, tcsetattr, termios, winsize, ECHO, ICANON, STDIN_FILENO, STDOUT_FILENO,
4-
TCSANOW, TIOCGWINSZ,
3+
c_void, ioctl, poll, pollfd, read, tcgetattr, tcsetattr, termios, winsize, ECHO, ICANON,
4+
POLLIN, STDIN_FILENO, STDOUT_FILENO, TCSANOW, TIOCGWINSZ,
55
};
6-
use std::io::Read;
7-
use std::sync::mpsc::{self, TryRecvError};
8-
use std::time::Duration;
6+
use std::time::Instant;
97

108
pub struct KittyBackend {}
119

@@ -41,38 +39,37 @@ impl KittyBackend {
4139
base64::encode(&test_image)
4240
);
4341

44-
// a new thread is required to avoid blocking the main thread if the terminal doesn't respond
45-
let (sender, receiver) = mpsc::channel::<()>();
46-
let (stop_sender, stop_receiver) = mpsc::channel::<()>();
47-
std::thread::spawn(move || {
48-
let mut buf = Vec::<u8>::new();
49-
let allowed_bytes = [0x1B, b'_', b'G', b'\\'];
50-
for byte in std::io::stdin().lock().bytes() {
51-
let byte = byte.unwrap();
52-
if allowed_bytes.contains(&byte) {
53-
buf.push(byte);
54-
}
55-
if buf.starts_with(&[0x1B, b'_', b'G']) && buf.ends_with(&[0x1B, b'\\']) {
56-
sender.send(()).unwrap();
57-
return;
58-
}
59-
match stop_receiver.try_recv() {
60-
Err(TryRecvError::Empty) => {}
61-
_ => return,
42+
let start_time = Instant::now();
43+
let mut stdin_pollfd = pollfd {
44+
fd: STDIN_FILENO,
45+
events: POLLIN,
46+
revents: 0,
47+
};
48+
let allowed_bytes = [0x1B, b'_', b'G', b'\\'];
49+
let mut buf = Vec::<u8>::new();
50+
loop {
51+
// check for timeout while polling to avoid blocking the main thread
52+
while unsafe { poll(&mut stdin_pollfd, 1, 0) < 1 } {
53+
if start_time.elapsed().as_millis() > 50 {
54+
unsafe {
55+
tcsetattr(STDIN_FILENO, TCSANOW, &old_attributes);
56+
}
57+
return false;
6258
}
6359
}
64-
});
65-
if receiver.recv_timeout(Duration::from_millis(50)).is_ok() {
60+
let mut byte = 0;
6661
unsafe {
67-
tcsetattr(STDIN_FILENO, TCSANOW, &old_attributes);
62+
read(STDIN_FILENO, &mut byte as *mut _ as *mut c_void, 1);
6863
}
69-
true
70-
} else {
71-
unsafe {
72-
tcsetattr(STDIN_FILENO, TCSANOW, &old_attributes);
64+
if allowed_bytes.contains(&byte) {
65+
buf.push(byte);
66+
}
67+
if buf.starts_with(&[0x1B, b'_', b'G']) && buf.ends_with(&[0x1B, b'\\']) {
68+
unsafe {
69+
tcsetattr(STDIN_FILENO, TCSANOW, &old_attributes);
70+
}
71+
return true;
7372
}
74-
stop_sender.send(()).ok();
75-
false
7673
}
7774
}
7875
}

Diff for: src/image_backends/mod.rs

+5-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
use image::DynamicImage;
22

33
#[cfg(target_os = "linux")]
4-
mod kitty;
4+
pub mod kitty;
5+
#[cfg(target_os = "linux")]
6+
pub mod sixel;
57

68
pub trait ImageBackend {
79
fn add_image(&self, lines: Vec<String>, image: &DynamicImage) -> String;
@@ -11,6 +13,8 @@ pub trait ImageBackend {
1113
pub fn get_best_backend() -> Option<Box<dyn ImageBackend>> {
1214
if kitty::KittyBackend::supported() {
1315
Some(Box::new(kitty::KittyBackend::new()))
16+
} else if sixel::SixelBackend::supported() {
17+
Some(Box::new(sixel::SixelBackend::new()))
1418
} else {
1519
None
1620
}

Diff for: src/image_backends/sixel.rs

+165
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
use image::{
2+
imageops::{colorops, FilterType},
3+
math::nq::NeuQuant,
4+
DynamicImage, GenericImageView, ImageBuffer, Pixel, Rgb,
5+
};
6+
use libc::{
7+
c_void, ioctl, poll, pollfd, read, tcgetattr, tcsetattr, termios, winsize, ECHO, ICANON,
8+
POLLIN, STDIN_FILENO, STDOUT_FILENO, TCSANOW, TIOCGWINSZ,
9+
};
10+
use std::time::Instant;
11+
12+
pub struct SixelBackend {}
13+
14+
impl SixelBackend {
15+
pub fn new() -> Self {
16+
Self {}
17+
}
18+
19+
pub fn supported() -> bool {
20+
// save terminal attributes and disable canonical input processing mode
21+
let old_attributes = unsafe {
22+
let mut old_attributes: termios = std::mem::zeroed();
23+
tcgetattr(STDIN_FILENO, &mut old_attributes);
24+
25+
let mut new_attributes = old_attributes;
26+
new_attributes.c_lflag &= !ICANON;
27+
new_attributes.c_lflag &= !ECHO;
28+
tcsetattr(STDIN_FILENO, TCSANOW, &new_attributes);
29+
old_attributes
30+
};
31+
32+
// ask for the primary device attribute string
33+
println!("\x1B[c");
34+
35+
let start_time = Instant::now();
36+
let mut stdin_pollfd = pollfd {
37+
fd: STDIN_FILENO,
38+
events: POLLIN,
39+
revents: 0,
40+
};
41+
let mut buf = Vec::<u8>::new();
42+
loop {
43+
// check for timeout while polling to avoid blocking the main thread
44+
while unsafe { poll(&mut stdin_pollfd, 1, 0) < 1 } {
45+
if start_time.elapsed().as_millis() > 50 {
46+
unsafe {
47+
tcsetattr(STDIN_FILENO, TCSANOW, &old_attributes);
48+
}
49+
return false;
50+
}
51+
}
52+
let mut byte = 0;
53+
unsafe {
54+
read(STDIN_FILENO, &mut byte as *mut _ as *mut c_void, 1);
55+
}
56+
buf.push(byte);
57+
if buf.starts_with(&[0x1B, b'[', b'?']) && buf.ends_with(&[b'c']) {
58+
for attribute in buf[3..(buf.len() - 1)].split(|x| *x == b';') {
59+
if attribute == &[b'4'] {
60+
unsafe {
61+
tcsetattr(STDIN_FILENO, TCSANOW, &old_attributes);
62+
}
63+
return true;
64+
}
65+
}
66+
}
67+
}
68+
}
69+
}
70+
71+
impl super::ImageBackend for SixelBackend {
72+
fn add_image(&self, lines: Vec<String>, image: &DynamicImage) -> String {
73+
let tty_size = unsafe {
74+
let tty_size: winsize = std::mem::zeroed();
75+
ioctl(STDOUT_FILENO, TIOCGWINSZ, &tty_size);
76+
tty_size
77+
};
78+
let width_ratio = tty_size.ws_col as f64 / tty_size.ws_xpixel as f64;
79+
let height_ratio = tty_size.ws_row as f64 / tty_size.ws_ypixel as f64;
80+
81+
// resize image to fit the text height with the Lanczos3 algorithm
82+
let image = image.resize(
83+
u32::max_value(),
84+
(lines.len() as f64 / height_ratio) as u32,
85+
FilterType::Lanczos3,
86+
);
87+
let image_columns = width_ratio * image.width() as f64;
88+
let image_rows = height_ratio * image.height() as f64;
89+
90+
let rgba_image = image.to_rgba(); // convert the image to rgba samples
91+
let flat_samples = rgba_image.as_flat_samples();
92+
let mut rgba_image = rgba_image.clone();
93+
// reduce the amount of colors using dithering
94+
colorops::dither(
95+
&mut rgba_image,
96+
&NeuQuant::new(10, 16, flat_samples.image_slice().unwrap()),
97+
);
98+
let rgb_image = ImageBuffer::from_fn(rgba_image.width(), rgba_image.height(), |x, y| {
99+
let rgba_pixel = rgba_image.get_pixel(x, y);
100+
let mut rgb_pixel = rgba_pixel.to_rgb();
101+
for subpixel in &mut rgb_pixel.0 {
102+
*subpixel = (*subpixel as f32 / 255.0 * rgba_pixel[3] as f32) as u8;
103+
}
104+
rgb_pixel
105+
});
106+
107+
let mut image_data = Vec::<u8>::new();
108+
image_data.extend("\x1BPq".as_bytes()); // start sixel data
109+
image_data.extend(format!("\"1;1;{};{}", image.width(), image.height()).as_bytes());
110+
111+
let mut colors = std::collections::HashMap::<Rgb<u8>, u8>::new();
112+
// subtract 1 -> divide -> add 1 to round up the integer division
113+
for i in 0..((rgb_image.height() - 1) / 6 + 1) {
114+
let sixel_row = rgb_image.view(
115+
0,
116+
i * 6,
117+
rgb_image.width(),
118+
std::cmp::min(6, rgb_image.height() - i * 6),
119+
);
120+
for (_, _, pixel) in sixel_row.pixels() {
121+
if !colors.contains_key(&pixel) {
122+
// sixel uses percentages for rgb values
123+
let color_multiplier = 100.0 / 255.0;
124+
image_data.extend(
125+
format!(
126+
"#{};2;{};{};{}",
127+
colors.len(),
128+
(pixel[0] as f32 * color_multiplier) as u32,
129+
(pixel[1] as f32 * color_multiplier) as u32,
130+
(pixel[2] as f32 * color_multiplier) as u32
131+
)
132+
.as_bytes(),
133+
);
134+
colors.insert(pixel, colors.len() as u8);
135+
}
136+
}
137+
for (color, color_index) in &colors {
138+
let mut sixel_samples = Vec::<u8>::with_capacity(sixel_row.width() as usize);
139+
sixel_samples.resize(sixel_row.width() as usize, 0);
140+
for (x, y, pixel) in sixel_row.pixels() {
141+
if color == &pixel {
142+
sixel_samples[x as usize] = sixel_samples[x as usize] | (1 << y);
143+
}
144+
}
145+
image_data.extend(format!("#{}", color_index).bytes());
146+
image_data.extend(sixel_samples.iter().map(|x| x + 0x3F));
147+
image_data.push('$' as u8);
148+
}
149+
image_data.push('-' as u8);
150+
}
151+
image_data.extend("\x1B\\".as_bytes());
152+
153+
image_data.extend(format!("\x1B[{}A", image_rows as u32 + 2).as_bytes()); // move cursor to top-left corner
154+
image_data.extend(format!("\x1B[{}C", image_columns as u32 + 1).as_bytes()); // move cursor to top-right corner of image
155+
let mut i = 0;
156+
for line in &lines {
157+
image_data.extend(format!("\x1B[s{}\x1B[u\x1B[1B", line).as_bytes());
158+
i += 1;
159+
}
160+
image_data
161+
.extend(format!("\n\x1B[{}B", lines.len().max(image_rows as usize) - i).as_bytes()); // move cursor to end of image
162+
163+
String::from_utf8(image_data).unwrap()
164+
}
165+
}

Diff for: src/info.rs

+6-3
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ use git2::Repository;
88
use image::DynamicImage;
99
use license::Detector;
1010

11-
use crate::image_backends;
11+
use crate::image_backends::ImageBackend;
1212
use crate::language::Language;
1313
use crate::{AsciiArt, CommitInfo, Configuration, Error, InfoFieldOn};
1414

@@ -36,6 +36,7 @@ pub struct Info {
3636
bold_enabled: bool,
3737
no_color_blocks: bool,
3838
custom_image: Option<DynamicImage>,
39+
image_backend: Option<Box<dyn ImageBackend>>
3940
}
4041

4142
impl std::fmt::Display for Info {
@@ -241,11 +242,11 @@ impl std::fmt::Display for Info {
241242
let mut info_lines = buf.lines();
242243

243244
if let Some(custom_image) = &self.custom_image {
244-
if let Some(backend) = image_backends::get_best_backend() {
245+
if let Some(image_backend) = &self.image_backend {
245246
writeln!(
246247
f,
247248
"{}",
248-
backend.add_image(
249+
image_backend.add_image(
249250
info_lines.map(|s| format!("{}{}", center_pad, s)).collect(),
250251
custom_image
251252
)
@@ -289,6 +290,7 @@ impl Info {
289290
disabled: InfoFieldOn,
290291
bold_flag: bool,
291292
custom_image: Option<DynamicImage>,
293+
image_backend: Option<Box<dyn ImageBackend>>,
292294
no_merges: bool,
293295
color_blocks_flag: bool,
294296
) -> Result<Info> {
@@ -331,6 +333,7 @@ impl Info {
331333
bold_enabled: bold_flag,
332334
no_color_blocks: color_blocks_flag,
333335
custom_image,
336+
image_backend,
334337
})
335338
}
336339

Diff for: src/main.rs

+32
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ mod license;
3838
use ascii_art::AsciiArt;
3939
use commit_info::CommitInfo;
4040
use error::Error;
41+
#[cfg(target_os = "linux")]
42+
use image_backends::ImageBackend;
4143
use info::Info;
4244
use language::Language;
4345

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

104+
#[cfg(target_os = "linux")]
105+
let possible_backends = ["kitty", "sixel"];
106+
#[cfg(not(target_os = "linux"))]
107+
let possible_backends = [];
108+
102109
let matches = App::new(crate_name!())
103110
.version(crate_version!())
104111
.author("o2sh <[email protected]>")
@@ -193,6 +200,13 @@ Possible values: [{0}{1}{2}{3}{4}{5}{6}{7}{8}{9}{10}{11}{12}{13}{14}{15}]",
193200
.takes_value(true)
194201
.help("Sets a custom image to use instead of the ascii logo"),
195202
)
203+
.arg(
204+
Arg::with_name("image-backend")
205+
.long("image-backend")
206+
.takes_value(true)
207+
.possible_values(&possible_backends)
208+
.help("Overrides image backend detection"),
209+
)
196210
.arg(
197211
Arg::with_name("no-merges")
198212
.short("m")
@@ -265,6 +279,23 @@ Possible values: [{0}{1}{2}{3}{4}{5}{6}{7}{8}{9}{10}{11}{12}{13}{14}{15}]",
265279
} else {
266280
None
267281
};
282+
let image_backend = if custom_image.is_some() {
283+
if let Some(backend_name) = matches.value_of("image-backend") {
284+
#[cfg(target_os = "linux")]
285+
let backend = Some(match backend_name {
286+
"kitty" => Box::new(image_backends::kitty::KittyBackend::new()) as Box<dyn ImageBackend>,
287+
"sixel" => Box::new(image_backends::sixel::SixelBackend::new()) as Box<dyn ImageBackend>,
288+
_ => unreachable!()
289+
});
290+
#[cfg(not(target_os = "linux"))]
291+
let backend = None;
292+
backend
293+
} else {
294+
crate::image_backends::get_best_backend()
295+
}
296+
} else {
297+
None
298+
};
268299

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

@@ -277,6 +308,7 @@ Possible values: [{0}{1}{2}{3}{4}{5}{6}{7}{8}{9}{10}{11}{12}{13}{14}{15}]",
277308
disable_fields,
278309
bold_flag,
279310
custom_image,
311+
image_backend,
280312
no_merges,
281313
color_blocks_flag,
282314
)?;

0 commit comments

Comments
 (0)