Skip to content

Commit d66e76f

Browse files
authored
Merge pull request #113 from CephalonRho/display-image
Add support for displaying a custom image instead of ascii art
2 parents a87ad8b + c8ed05f commit d66e76f

File tree

6 files changed

+219
-20
lines changed

6 files changed

+219
-20
lines changed

Diff for: Cargo.toml

+5
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,11 @@ bytecount = "0.5.1"
1717
clap = "2.33.0"
1818
strum = "0.16.0"
1919
strum_macros = "0.16.0"
20+
image = "0.22.3"
2021

2122
[target.'cfg(windows)'.dependencies]
2223
ansi_term = "0.12"
24+
25+
[target.'cfg(target_os = "linux")'.dependencies]
26+
libc = "0.2.65"
27+
base64 = "0.11.0"

Diff for: src/error.rs

+3
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ pub enum Error {
1212
NotGitRepo,
1313
/// Error while getting branch info
1414
ReferenceInfoError,
15+
/// Image probably doesn't exist or has wrong format
16+
ImageLoadError,
1517
}
1618

1719
impl std::fmt::Debug for Error {
@@ -23,6 +25,7 @@ impl std::fmt::Debug for Error {
2325
Error::ReadDirectory => "Could not read directory",
2426
Error::NotGitRepo => "This is not a Git Repo",
2527
Error::ReferenceInfoError => "Error while retrieving reference information",
28+
Error::ImageLoadError => "Could not load the specified image",
2629
};
2730
write!(f, "{}", content)
2831
}

Diff for: src/image_backends/kitty.rs

+139
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
use image::{imageops::FilterType, DynamicImage, GenericImageView};
2+
use libc::{
3+
ioctl, tcgetattr, tcsetattr, termios, winsize, ECHO, ICANON, STDIN_FILENO, STDOUT_FILENO,
4+
TCSANOW, TIOCGWINSZ,
5+
};
6+
use std::io::Read;
7+
use std::sync::mpsc::{self, TryRecvError};
8+
use std::time::Duration;
9+
10+
pub struct KittyBackend {}
11+
12+
impl KittyBackend {
13+
pub fn new() -> Self {
14+
Self {}
15+
}
16+
17+
pub fn supported() -> bool {
18+
// save terminal attributes and disable canonical input processing mode
19+
let old_attributes = unsafe {
20+
let mut old_attributes: termios = std::mem::zeroed();
21+
tcgetattr(STDIN_FILENO, &mut old_attributes);
22+
23+
let mut new_attributes = old_attributes.clone();
24+
new_attributes.c_lflag &= !ICANON;
25+
new_attributes.c_lflag &= !ECHO;
26+
tcsetattr(STDIN_FILENO, TCSANOW, &new_attributes);
27+
old_attributes
28+
};
29+
30+
// generate red rgba test image
31+
let mut test_image = Vec::<u8>::with_capacity(32 * 32 * 4);
32+
test_image.extend(
33+
std::iter::repeat([255, 0, 0, 255].iter())
34+
.take(32 * 32)
35+
.flatten(),
36+
);
37+
38+
// print the test image with the action set to query
39+
println!(
40+
"\x1B_Gi=1,f=32,s=32,v=32,a=q;{}\x1B\\",
41+
base64::encode(&test_image)
42+
);
43+
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, '_' as u8, 'G' as u8, '\\' as u8];
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, '_' as u8, 'G' as u8])
56+
&& buf.ends_with(&[0x1B, '\\' as u8])
57+
{
58+
sender.send(()).unwrap();
59+
return;
60+
}
61+
match stop_receiver.try_recv() {
62+
Err(TryRecvError::Empty) => {}
63+
_ => return,
64+
}
65+
}
66+
});
67+
if let Ok(_) = receiver.recv_timeout(Duration::from_millis(50)) {
68+
unsafe {
69+
tcsetattr(STDIN_FILENO, TCSANOW, &old_attributes);
70+
}
71+
true
72+
} else {
73+
unsafe {
74+
tcsetattr(STDIN_FILENO, TCSANOW, &old_attributes);
75+
}
76+
stop_sender.send(()).ok();
77+
false
78+
}
79+
}
80+
}
81+
82+
impl super::ImageBackend for KittyBackend {
83+
fn add_image(&self, lines: Vec<String>, image: &DynamicImage) -> String {
84+
let tty_size = unsafe {
85+
let tty_size: winsize = std::mem::zeroed();
86+
ioctl(STDOUT_FILENO, TIOCGWINSZ, &tty_size);
87+
tty_size
88+
};
89+
let width_ratio = tty_size.ws_col as f64 / tty_size.ws_xpixel as f64;
90+
let height_ratio = tty_size.ws_row as f64 / tty_size.ws_ypixel as f64;
91+
92+
// resize image to fit the text height with the Lanczos3 algorithm
93+
let image = image.resize(
94+
u32::max_value(),
95+
(lines.len() as f64 / height_ratio) as u32,
96+
FilterType::Lanczos3,
97+
);
98+
let _image_columns = width_ratio * image.width() as f64;
99+
let image_rows = height_ratio * image.height() as f64;
100+
101+
// convert the image to rgba samples
102+
let rgba_image = image.to_rgba();
103+
let flat_samples = rgba_image.as_flat_samples();
104+
let raw_image = flat_samples
105+
.image_slice()
106+
.expect("Conversion from image to rgba samples failed");
107+
assert_eq!(
108+
image.width() as usize * image.height() as usize * 4,
109+
raw_image.len()
110+
);
111+
112+
let encoded_image = base64::encode(&raw_image); // image data is base64 encoded
113+
let mut image_data = Vec::<u8>::new();
114+
for chunk in encoded_image.as_bytes().chunks(4096) {
115+
// send a 4096 byte chunk of base64 encoded rgba image data
116+
image_data.extend(
117+
format!(
118+
"\x1B_Gf=32,s={},v={},m=1,a=T;",
119+
image.width(),
120+
image.height()
121+
)
122+
.as_bytes(),
123+
);
124+
image_data.extend(chunk);
125+
image_data.extend("\x1B\\".as_bytes());
126+
}
127+
image_data.extend("\x1B_Gm=0;\x1B\\".as_bytes()); // write empty last chunk
128+
image_data.extend(format!("\x1B[{}A", image_rows as u32 - 1).as_bytes()); // move cursor to start of image
129+
let mut i = 0;
130+
for line in &lines {
131+
image_data.extend(format!("\x1B[s{}\x1B[u\x1B[1B", line).as_bytes());
132+
i += 1;
133+
}
134+
image_data
135+
.extend(format!("\n\x1B[{}B", lines.len().max(image_rows as usize) - i).as_bytes()); // move cursor to end of image
136+
137+
String::from_utf8(image_data).unwrap()
138+
}
139+
}

Diff for: src/image_backends/mod.rs

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
use image::DynamicImage;
2+
3+
#[cfg(target_os = "linux")]
4+
mod kitty;
5+
6+
pub trait ImageBackend {
7+
fn add_image(&self, lines: Vec<String>, image: &DynamicImage) -> String;
8+
}
9+
10+
#[cfg(target_os = "linux")]
11+
pub fn get_best_backend() -> Option<Box<dyn ImageBackend>> {
12+
if kitty::KittyBackend::supported() {
13+
Some(Box::new(kitty::KittyBackend::new()))
14+
} else {
15+
None
16+
}
17+
}
18+
19+
#[cfg(not(target_os = "linux"))]
20+
pub fn get_best_backend() -> Option<Box<dyn ImageBackend>> {
21+
None
22+
}

Diff for: src/info.rs

+32-19
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,11 @@ use std::str::FromStr;
77
use colored::{Color, Colorize, ColoredString};
88
use git2::Repository;
99
use license::License;
10+
use image::DynamicImage;
1011

1112
use crate::language::Language;
1213
use crate::{AsciiArt, CommitInfo, Configuration, Error, InfoFieldOn};
14+
use crate::image_backends;
1315

1416
type Result<T> = std::result::Result<T, crate::Error>;
1517

@@ -33,6 +35,7 @@ pub struct Info {
3335
custom_colors: Vec<String>,
3436
disable_fields: InfoFieldOn,
3537
bold_enabled: bool,
38+
custom_image: Option<DynamicImage>,
3639
}
3740

3841
impl std::fmt::Display for Info {
@@ -177,27 +180,35 @@ impl std::fmt::Display for Info {
177180
" ".on_bright_white(),
178181
)?;
179182

180-
let mut logo_lines = AsciiArt::new(self.get_ascii(), self.colors(), self.bold_enabled);
183+
let center_pad = " ";
181184
let mut info_lines = buf.lines();
182185

183-
let center_pad = " ";
184-
loop {
185-
match (logo_lines.next(), info_lines.next()) {
186-
(Some(logo_line), Some(info_line)) => {
187-
writeln!(f, "{}{}{:^}", logo_line, center_pad, info_line)?
188-
}
189-
(Some(logo_line), None) => writeln!(f, "{}", logo_line)?,
190-
(None, Some(info_line)) => writeln!(
191-
f,
192-
"{:<width$}{}{:^}",
193-
"",
194-
center_pad,
195-
info_line,
196-
width = logo_lines.width()
197-
)?,
198-
(None, None) => {
199-
writeln!(f, "\n")?;
200-
break;
186+
if let Some(custom_image) = &self.custom_image {
187+
if let Some(backend) = image_backends::get_best_backend() {
188+
writeln!(f, "{}", backend.add_image(info_lines.map(|s| format!("{}{}", center_pad, s)).collect(), custom_image))?;
189+
} else {
190+
panic!("No image backend found")
191+
}
192+
} else {
193+
let mut logo_lines = AsciiArt::new(self.get_ascii(), self.colors(), self.bold_enabled);
194+
loop {
195+
match (logo_lines.next(), info_lines.next()) {
196+
(Some(logo_line), Some(info_line)) => {
197+
writeln!(f, "{}{}{:^}", logo_line, center_pad, info_line)?
198+
}
199+
(Some(logo_line), None) => writeln!(f, "{}", logo_line)?,
200+
(None, Some(info_line)) => writeln!(
201+
f,
202+
"{:<width$}{}{:^}",
203+
"",
204+
center_pad,
205+
info_line,
206+
width = logo_lines.width()
207+
)?,
208+
(None, None) => {
209+
writeln!(f, "\n")?;
210+
break;
211+
}
201212
}
202213
}
203214
}
@@ -213,6 +224,7 @@ impl Info {
213224
colors: Vec<String>,
214225
disabled: InfoFieldOn,
215226
bold_flag: bool,
227+
custom_image: Option<DynamicImage>
216228
) -> Result<Info> {
217229
let authors = Info::get_authors(&dir, 3);
218230
let (git_v, git_user) = Info::get_git_info(&dir);
@@ -247,6 +259,7 @@ impl Info {
247259
custom_colors: colors,
248260
disable_fields: disabled,
249261
bold_enabled: bold_flag,
262+
custom_image,
250263
})
251264
}
252265

Diff for: src/main.rs

+18-1
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,14 @@ extern crate clap;
88
extern crate strum;
99
#[macro_use]
1010
extern crate strum_macros;
11+
extern crate image;
1112

1213
#[cfg(target = "windows")]
1314
extern crate ansi_term;
1415

16+
#[cfg(target_os = "linux")]
17+
extern crate libc;
18+
1519
use clap::{App, Arg};
1620
use colored::*;
1721
use std::{
@@ -25,6 +29,7 @@ use strum::{EnumCount, IntoEnumIterator};
2529
mod ascii_art;
2630
mod commit_info;
2731
mod error;
32+
mod image_backends;
2833
mod info;
2934
mod language;
3035

@@ -165,6 +170,12 @@ Possible values: [{0}{1}{2}{3}{4}{5}{6}{7}{8}{9}{10}{11}{12}{13}{14}{15}]",
165170
.short("l")
166171
.long("languages")
167172
.help("Prints out supported languages"),
173+
).arg(
174+
Arg::with_name("image")
175+
.short("i")
176+
.long("image")
177+
.takes_value(true)
178+
.help("Sets a custom image to use instead of the ascii logo"),
168179
)
169180
.get_matches();
170181

@@ -220,7 +231,13 @@ Possible values: [{0}{1}{2}{3}{4}{5}{6}{7}{8}{9}{10}{11}{12}{13}{14}{15}]",
220231

221232
let bold_flag = !matches.is_present("no-bold");
222233

223-
let info = Info::new(&dir, custom_logo, custom_colors, disable_fields, bold_flag)?;
234+
let custom_image = if let Some(image_path) = matches.value_of("image") {
235+
Some(image::open(image_path).map_err(|_| Error::ImageLoadError)?)
236+
} else {
237+
None
238+
};
239+
240+
let info = Info::new(&dir, custom_logo, custom_colors, disable_fields, bold_flag, custom_image)?;
224241

225242
print!("{}", info);
226243
Ok(())

0 commit comments

Comments
 (0)