1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
use crate::config::Config;
use crossterm::terminal::{Clear, ClearType};
use crossterm::{cursor, execute};
use image::{codecs::gif::GifDecoder, AnimationDecoder, DynamicImage};
use std::fs;
use std::io::{stdin, stdout, BufRead, BufReader, Cursor, Error, ErrorKind, Read, Seek};
use std::sync::mpsc;
use std::{thread, time::Duration};
use viuer::ViuResult;
type TxRx<'a> = (&'a mpsc::Sender<bool>, &'a mpsc::Receiver<bool>);
// TODO: Create a viu-specific result and error types, do not reuse viuer's
pub fn run(mut conf: Config) -> ViuResult {
//create two channels so that ctrlc-handler and the main thread can pass messages in order to
// communicate when printing must be stopped without distorting the current frame
let (tx_ctrlc, rx_print) = mpsc::channel();
let (tx_print, rx_ctrlc) = mpsc::channel();
//handle Ctrl-C in order to clean up after ourselves
ctrlc::set_handler(move || {
//if ctrlc is received tell the infinite gif loop to stop drawing
// or stop the next file from being drawn
tx_ctrlc
.send(true)
.expect("Could not send signal to stop drawing.");
//a message will be received when that has happened so we can clear leftover symbols
let _ = rx_ctrlc
.recv()
.expect("Could not receive signal to clean up terminal.");
if let Err(e) = execute!(stdout(), Clear(ClearType::FromCursorDown)) {
if e.kind() == ErrorKind::BrokenPipe {
//Do nothing. Output is probably piped to `head` or a similar tool
} else {
panic!("{}", e);
}
}
std::process::exit(0);
})
.map_err(|_| Error::other("Could not setup Ctrl-C handler."))?;
//TODO: handle multiple files
//read stdin if only one parameter is passed and it is "-"
if conf.files.len() == 1 && conf.files[0] == "-" {
let stdin = stdin();
let mut handle = stdin.lock();
let mut buf: Vec<u8> = Vec::new();
let _ = handle.read_to_end(&mut buf)?;
let cursor = Cursor::new(&buf);
//TODO: print_from_file if data is a gif and terminal is iTerm
if try_print_gif(&conf, cursor, (&tx_print, &rx_print)).is_err() {
//If stdin data is not a gif, treat it as a regular image
let img = image::load_from_memory(&buf)?;
viuer::print(&img, &conf.viuer_config)?;
};
Ok(())
} else {
view_passed_files(&mut conf, (&tx_print, &rx_print))
}
}
fn view_passed_files(conf: &mut Config, (tx, rx): TxRx) -> ViuResult {
//loop throught all files passed
for filename in &conf.files {
//check if Ctrl-C has been received. If yes, stop iterating
if rx.try_recv().is_ok() {
return tx
.send(true)
.map_err(|_| Error::other("Could not send signal to clean up.").into());
};
//if it's a directory, stop gif looping because there will probably be more files
if fs::metadata(filename)?.is_dir() {
conf.loop_gif = false;
view_directory(conf, filename, (tx, rx))?;
}
//if a file has been passed individually and fails, propagate the error
else {
view_file(conf, filename, (tx, rx))?;
}
}
Ok(())
}
fn view_directory(conf: &Config, dirname: &str, (tx, rx): TxRx) -> ViuResult {
for dir_entry_result in fs::read_dir(dirname)? {
//check if Ctrl-C has been received. If yes, stop iterating
if rx.try_recv().is_ok() {
return tx
.send(true)
.map_err(|_| Error::other("Could not send signal to clean up.").into());
};
let dir_entry = dir_entry_result?;
//check if the given file is a directory
if let Some(path_name) = dir_entry.path().to_str() {
//if -r is passed, continue down
if conf.recursive && dir_entry.metadata()?.is_dir() {
view_directory(conf, path_name, (tx, rx))?;
}
//if it is a regular file, viu it, but do not exit on error
else {
let _ = view_file(conf, path_name, (tx, rx));
}
} else {
eprintln!("Could not get path name, skipping...");
continue;
}
}
Ok(())
}
fn view_file(conf: &Config, filename: &str, (tx, rx): TxRx) -> ViuResult {
if conf.name {
println!("{}:", filename);
}
let mut file_in = fs::File::open(filename)?;
// Read some of the first bytes to guess the image format
let mut format_guess_buf: [u8; 20] = [0; 20];
let _ = file_in.read(&mut format_guess_buf)?;
// Reset the cursor
file_in.seek(std::io::SeekFrom::Start(0))?;
// If the file is a gif, let iTerm handle it natively
if conf.viuer_config.use_iterm
&& viuer::is_iterm_supported()
&& (image::guess_format(&format_guess_buf[..])?) == image::ImageFormat::Gif
{
viuer::print_from_file(filename, &conf.viuer_config)?;
} else {
let result = try_print_gif(conf, BufReader::new(file_in), (tx, rx));
//the provided image is not a gif so try to view it
if result.is_err() {
viuer::print_from_file(filename, &conf.viuer_config)?;
}
}
if conf.caption {
println!("{}", filename);
}
Ok(())
}
fn try_print_gif<R>(conf: &Config, input_stream: R, (tx, rx): TxRx) -> ViuResult
where
R: Read + BufRead + Seek,
{
//read all frames of the gif and resize them all at once before starting to print them
let resized_frames: Vec<(Duration, DynamicImage)> = GifDecoder::new(input_stream)?
.into_frames()
.collect_frames()?
.into_iter()
.map(|f| {
let delay = Duration::from(f.delay());
// Keep the image as it is for Kitty and iTerm, it will be printed in full resolution there
if (conf.viuer_config.use_iterm && viuer::is_iterm_supported())
|| (conf.viuer_config.use_kitty
&& viuer::get_kitty_support() != viuer::KittySupport::None)
{
(delay, DynamicImage::ImageRgba8(f.into_buffer()))
} else {
(
delay,
viuer::resize(
&DynamicImage::ImageRgba8(f.into_buffer()),
conf.viuer_config.width,
conf.viuer_config.height,
),
)
}
})
.collect();
'infinite: loop {
let mut iter = resized_frames.iter().peekable();
while let Some((delay, frame)) = iter.next() {
let (_print_width, print_height) = viuer::print(frame, &conf.viuer_config)?;
if conf.static_gif {
break 'infinite;
}
thread::sleep(match conf.frame_duration {
None => *delay,
Some(duration) => duration,
});
//if ctrlc is received then respond so the handler can clear the
// terminal from leftover colors
if rx.try_recv().is_ok() {
return tx
.send(true)
.map_err(|_| Error::other("Could not send signal to clean up.").into());
};
//keep replacing old pixels as the gif goes on so that scrollback
// buffer is not filled (do not do that if it is the last frame of the gif
// or a couple of files are being processed)
if iter.peek().is_some() || conf.loop_gif {
//since picture height is in pixel, we divide by 2 to get the height in
// terminal cells
if let Err(e) = execute!(stdout(), cursor::MoveUp(print_height as u16)) {
if e.kind() == ErrorKind::BrokenPipe {
//Stop printing. Output is probably piped to `head` or a similar tool
break 'infinite;
} else {
return Err(e.into());
}
}
}
}
if !conf.loop_gif {
break 'infinite;
}
}
Ok(())
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn test_view_without_extension() {
let conf = Config::test_config();
let (tx, rx) = mpsc::channel();
view_file(&conf, "img/bfa", (&tx, &rx)).unwrap();
}
}