pic/
utils.rs

1use crate::{result::Result, support};
2use ansi_colours::ansi256_from_rgb;
3use crossbeam_channel::{unbounded, Receiver, Sender};
4use image::{codecs::png::PngEncoder, DynamicImage, ImageEncoder};
5use std::{
6    fs::File,
7    io::{Error, Write},
8    path::PathBuf,
9};
10
11pub(crate) struct CtrlcHandler {
12    pub sender: Sender<bool>,
13    pub receiver: Receiver<bool>,
14}
15
16impl CtrlcHandler {
17    pub fn new() -> Result<Self> {
18        // We use two channels so that they can communicate
19        let (ctrlc_tx, preview_rx) = unbounded();
20        let (preview_tx, ctrlc_rx) = unbounded();
21
22        ctrlc::set_handler(move || {
23            ctrlc_tx
24                .send(true)
25                .expect("CTRL-C error: Unable to send message");
26
27            ctrlc_rx
28                .recv()
29                .expect("CTRL-C error: No response from main thread");
30            std::process::exit(0);
31        })?;
32
33        Ok(Self {
34            sender: preview_tx,
35            receiver: preview_rx,
36        })
37    }
38}
39
40/// Useful handle for terminal size
41#[derive(Clone, Default, Debug)]
42pub struct TermSize {
43    /// the amount of visible rows in the pty
44    pub(crate) rows: u32,
45    /// the amount of visible columns in the pty
46    pub(crate) cols: u32,
47    /// the width of the view in pixels
48    pub(crate) width: u32,
49    /// the height of the view in pixels
50    pub(crate) height: u32,
51}
52
53impl TermSize {
54    pub fn new(rows: u16, cols: u16, width: u16, height: u16) -> Self {
55        Self {
56            rows: u32::from(rows),
57            cols: u32::from(cols),
58            width: u32::from(width),
59            height: u32::from(height),
60        }
61    }
62
63    /// Retrieve the size of a terminal cell
64    pub fn get_cell_size(&self) -> Option<(u32, u32)> {
65        if self.cols == 0 || self.rows == 0 {
66            return None;
67        }
68        Some((self.width / self.cols, self.height / self.rows))
69    }
70
71    /// Create TermSize by getting the terminal size with an IOCTL
72    pub fn from_ioctl() -> Result<Self> {
73        // TODO: find a way to make that safe
74        unsafe {
75            let mut ws = libc::winsize {
76                ws_row: 0,
77                ws_col: 0,
78                ws_xpixel: 0,
79                ws_ypixel: 0,
80            };
81            let ret = libc::ioctl(0, libc::TIOCGWINSZ, &mut ws);
82            if ret == 0 {
83                Ok(TermSize::new(
84                    ws.ws_row,
85                    ws.ws_col,
86                    ws.ws_xpixel,
87                    ws.ws_ypixel,
88                ))
89            } else {
90                Err(Error::last_os_error().into())
91            }
92        }
93    }
94}
95
96/// Create a temporary file with the given prefix
97pub fn create_temp_file(prefix: &str) -> Result<(File, PathBuf)> {
98    let (tempfile, pathbuf) = tempfile::Builder::new()
99        .prefix(prefix)
100        .tempfile_in("/tmp/")?
101        .keep()?;
102
103    Ok((tempfile, pathbuf))
104}
105
106/// Save buffer in a temporary file
107pub fn save_in_temp_file(buffer: &[u8], file: &mut File) -> Result {
108    file.write_all(buffer)?;
109    file.flush()?;
110    Ok(())
111}
112
113/// Save terminal cursor position
114#[allow(dead_code)]
115pub fn save_cursor(stdout: &mut impl Write) -> Result {
116    stdout.write_all(b"\x1b[s")?;
117    stdout.flush()?;
118    Ok(())
119}
120
121/// Restore terminal cursor position
122#[allow(dead_code)]
123pub fn restore_cursor(stdout: &mut impl Write) -> Result {
124    stdout.write_all(b"\x1b[u")?;
125    stdout.flush()?;
126    Ok(())
127}
128
129/// Move terminal cursor up
130#[allow(dead_code)]
131pub fn move_cursor_up(stdout: &mut impl Write, x: u32) -> Result {
132    let binding = format!("\x1b[{}A", x + 1);
133    stdout.write_all(binding.as_bytes())?;
134    stdout.flush()?;
135    Ok(())
136}
137
138/// Move terminal cursor down
139#[allow(dead_code)]
140pub fn move_cursor_down(stdout: &mut impl Write, x: u32) -> Result {
141    let binding = format!("\x1b[{}B", x + 1);
142    stdout.write_all(binding.as_bytes())?;
143    stdout.flush()?;
144    Ok(())
145}
146
147/// Move terminal cursor to the given column
148pub fn move_cursor_column(stdout: &mut impl Write, col: u32) -> Result {
149    let binding = format!("\x1b[{}G", col + 1);
150    stdout.write_all(binding.as_bytes())?;
151    stdout.flush()?;
152    Ok(())
153}
154
155/// Move terminal cursor to the given row
156pub fn move_cursor_row(stdout: &mut impl Write, row: u32) -> Result {
157    let binding = format!("\x1b[{}d", row + 1);
158    stdout.write_all(binding.as_bytes())?;
159    stdout.flush()?;
160    Ok(())
161}
162
163/// Move terminal cursor to the given position
164pub fn move_cursor_pos(stdout: &mut impl Write, col: u32, row: u32) -> Result {
165    let binding = format!("\x1b[{};{}H", row + 1, col + 1);
166    stdout.write_all(binding.as_bytes())?;
167    stdout.flush()?;
168    Ok(())
169}
170
171/// Move terminal cursor eventually given x and y
172pub fn move_cursor(stdout: &mut impl Write, col: Option<u32>, row: Option<u32>) -> Result {
173    match (col, row) {
174        (None, None) => Ok(()),
175        (Some(x), None) => move_cursor_column(stdout, x),
176        (None, Some(y)) => move_cursor_row(stdout, y),
177        (Some(x), Some(y)) => move_cursor_pos(stdout, x, y),
178    }
179}
180
181/// Show terminal cursor
182pub fn show_cursor(stdout: &mut impl Write) -> Result {
183    stdout.write_all(b"\x1b[?25h")?;
184    stdout.flush()?;
185    Ok(())
186}
187
188/// Hide terminal cursor
189pub fn hide_cursor(stdout: &mut impl Write) -> Result {
190    stdout.write_all(b"\x1b[?25l")?;
191    stdout.flush()?;
192    Ok(())
193}
194
195/// Handle spacing between images
196pub fn handle_spacing(stdout: &mut impl Write, spacing: Option<u32>) -> Result {
197    if let Some(spacing) = spacing {
198        stdout.write_all(&b"\n".repeat(spacing as usize))?;
199        stdout.flush()?;
200    }
201    Ok(())
202}
203
204/// Fit an images into cols and rows bounds
205pub fn fit_in_bounds(
206    width: u32,
207    height: u32,
208    cols: Option<u32>,
209    rows: Option<u32>,
210    upscale: bool,
211) -> Result<(u32, u32)> {
212    let term_size = TermSize::from_ioctl()?;
213    let (col_size, row_size) = match term_size.get_cell_size() {
214        Some((0, 0)) | None => (15, 30),
215        Some((c, r)) => (c, r),
216    };
217    let cols = cols.unwrap_or(term_size.cols);
218    // Terminal prompt puts the image out of screen (rows - 1)
219    let rows = rows.unwrap_or(term_size.rows - 1);
220
221    let (bound_width, bound_height) = (cols * col_size, rows * row_size);
222
223    if !upscale && width < bound_width && height < bound_height {
224        return Ok((width / col_size, height / row_size));
225    }
226
227    let w_ratio = width * bound_height;
228    let h_ratio = bound_width * height;
229
230    if w_ratio >= h_ratio {
231        Ok((
232            cols,
233            std::cmp::max((height * bound_width) / (width * row_size), 1),
234        ))
235    } else {
236        Ok((
237            std::cmp::max((width * bound_height) / (height * col_size), 1),
238            rows,
239        ))
240    }
241}
242
243/// Resize an image
244pub fn resize(image: &DynamicImage, width: u32, height: u32) -> DynamicImage {
245    image.resize_exact(width, height, image::imageops::Triangle)
246}
247
248/// Convert an image to a png buffer
249/// image is mainly supposed to be a GIF here
250pub fn convert_to_image_buffer(image: &DynamicImage, width: u32, height: u32) -> Result<Vec<u8>> {
251    let mut image_buffer = Vec::new();
252    PngEncoder::new(&mut image_buffer).write_image(
253        image.as_bytes(),
254        width,
255        height,
256        image.color(),
257    )?;
258    Ok(image_buffer)
259}
260
261/// Assess the transparency of a pixel
262pub fn pixel_is_transparent(rgb: [u8; 4]) -> bool {
263    rgb[3] < 25
264}
265
266/// Convert rgb to ansi_rgb
267pub fn ansi_rgb(rgb: [u8; 4], bg: bool) -> String {
268    if bg {
269        format!("\x1b[48;2;{};{};{}m", rgb[0], rgb[1], rgb[2])
270    } else {
271        format!("\x1b[38;2;{};{};{}m", rgb[0], rgb[1], rgb[2])
272    }
273}
274
275/// Convert rgb to ansi_indexed
276pub fn ansi_indexed(rgb: [u8; 4], bg: bool) -> String {
277    let index = ansi256_from_rgb((rgb[0], rgb[1], rgb[2]));
278    if bg {
279        format!("\x1b[48;5;{index}m")
280    } else {
281        format!("\x1b[38;5;{index}m")
282    }
283}
284
285/// Convert rgb to ansi
286pub fn ansi_color(rgb: [u8; 4], bg: bool) -> String {
287    if support::truecolor() {
288        ansi_rgb(rgb, bg)
289    } else {
290        ansi_indexed(rgb, bg)
291    }
292}