Skip to main content

demo/
main.rs

1#[cfg(all(
2    not(feature = "crossterm"),
3    not(feature = "termion"),
4    not(feature = "termwiz")
5))]
6compile_error!("The demo needs one of the crossterm, termion, or termwiz features");
7
8#[cfg(feature = "crossterm")]
9mod crossterm;
10#[cfg(feature = "termion")]
11mod termion;
12#[cfg(feature = "termwiz")]
13mod termwiz;
14
15use std::{env, error::Error, num::Wrapping as w, path::PathBuf, sync::Once, time::Duration};
16
17use image::DynamicImage;
18use ratatui::{
19    Frame, Terminal,
20    backend::Backend,
21    layout::{Constraint, Direction, Layout, Rect},
22    style::{Color, Stylize},
23    text::{Line, Span, Text},
24    widgets::{Block, Borders, Paragraph, Wrap},
25};
26use ratatui_image::{
27    Image, Resize, StatefulImage,
28    picker::Picker,
29    protocol::{Protocol, StatefulProtocol},
30};
31
32fn main() -> Result<(), Box<dyn Error>> {
33    #[cfg(feature = "crossterm")]
34    crate::crossterm::run()?;
35    #[cfg(feature = "termion")]
36    crate::termion::run()?;
37    #[cfg(feature = "termwiz")]
38    crate::termwiz::run()?;
39    Ok(())
40}
41
42static READY: Once = Once::new();
43
44#[derive(Debug)]
45enum ShowImages {
46    All,
47    Fixed,
48    Resized,
49}
50
51struct App {
52    title: String,
53    should_quit: bool,
54    tick_rate: Duration,
55    background: String,
56    split_percent: u16,
57    show_images: ShowImages,
58
59    image_source_path: PathBuf,
60    image_static_offset: (u16, u16),
61
62    picker: Picker,
63    image_source: DynamicImage,
64    image_static: Protocol,
65    image_fit_state: StatefulProtocol,
66    image_crop_state: StatefulProtocol,
67    image_scale_state: StatefulProtocol,
68}
69
70fn size() -> Rect {
71    Rect::new(0, 0, 30, 16)
72}
73
74impl App {
75    pub fn new<B: Backend>(_: &mut Terminal<B>) -> Self {
76        let title = format!(
77            "Demo ({})",
78            env::var("TERM").unwrap_or("unknown".to_string())
79        );
80
81        let image = if env::args().any(|arg| arg == "--tmp-demo-ready") {
82            "./assets/Jenkins.png"
83        } else {
84            "./assets/Ada.png"
85        };
86        let image_source = image::ImageReader::open(image).unwrap().decode().unwrap();
87
88        let picker = Picker::from_query_stdio().unwrap();
89
90        let image_static = picker
91            .new_protocol(image_source.clone(), size(), Resize::Fit(None))
92            .expect("demo gets a protocol from image");
93        let image_fit_state = picker.new_resize_protocol(image_source.clone());
94        let image_crop_state = picker.new_resize_protocol(image_source.clone());
95        let image_scale_state = picker.new_resize_protocol(image_source.clone());
96
97        let mut background = String::new();
98
99        let mut r: [u64; 2] = [0x8a5cd789635d2dff, 0x121fd2155c472f96];
100        for _ in 0..5_000 {
101            let mut s1 = w(r[0]);
102            let s0 = w(r[1]);
103            let result = s0 + s1;
104            r[0] = s0.0;
105            s1 ^= s1 << 23;
106            r[1] = (s1 ^ s0 ^ (s1 >> 18) ^ (s0 >> 5)).0;
107            let c = match result.0 % 4 {
108                0 => '.',
109                1 => ' ',
110                _ => '…',
111            };
112            background.push(c);
113        }
114
115        Self {
116            title,
117            should_quit: false,
118            tick_rate: Duration::from_millis(1000),
119            background,
120            show_images: ShowImages::All,
121            split_percent: 70,
122            picker,
123            image_source,
124            image_source_path: image.into(),
125
126            image_static,
127            image_fit_state,
128            image_crop_state,
129            image_scale_state,
130
131            image_static_offset: (0, 0),
132        }
133    }
134    pub fn on_key(&mut self, c: char) {
135        match c {
136            'q' => {
137                self.should_quit = true;
138            }
139            't' => {
140                self.show_images = match self.show_images {
141                    ShowImages::All => ShowImages::Fixed,
142                    ShowImages::Fixed => ShowImages::Resized,
143                    ShowImages::Resized => ShowImages::All,
144                }
145            }
146            'i' => {
147                // Normally, we *never* would want to switch the detected protocol.
148                // This is for some debug session, where you want to test some other protocol than
149                // the detected.
150                // Changing "live" is also quite hazardous, as this will render some artifacts in
151                // between, or even trigger error messages, or crashes.
152                // If you need to "downgrade" e.g. to Halfblocks, then do it before any renders.
153                let next = self.picker.protocol_type().next();
154                self.picker.set_protocol_type(next);
155                self.reset_images();
156            }
157            'o' => {
158                let path = match self.image_source_path.to_str() {
159                    Some("./assets/Ada.png") => "./assets/Jenkins.png",
160                    Some("./assets/Jenkins.png") => "./assets/NixOS.png",
161                    _ => "./assets/Ada.png",
162                };
163                self.image_source = image::ImageReader::open(path).unwrap().decode().unwrap();
164                self.image_source_path = path.into();
165                self.reset_images();
166            }
167            'H' => {
168                if self.split_percent >= 10 {
169                    self.split_percent -= 10;
170                }
171            }
172            'L' => {
173                if self.split_percent <= 90 {
174                    self.split_percent += 10;
175                }
176            }
177            'h' => {
178                if self.image_static_offset.0 > 0 {
179                    self.image_static_offset.0 -= 1;
180                }
181            }
182            'j' => {
183                self.image_static_offset.1 += 1;
184            }
185            'k' => {
186                if self.image_static_offset.1 > 0 {
187                    self.image_static_offset.1 -= 1;
188                }
189            }
190            'l' => {
191                self.image_static_offset.0 += 1;
192            }
193            _ => {}
194        }
195    }
196
197    fn reset_images(&mut self) {
198        self.image_static = self
199            .picker
200            .new_protocol(self.image_source.clone(), size(), Resize::Fit(None))
201            .unwrap();
202        self.image_fit_state = self.picker.new_resize_protocol(self.image_source.clone());
203        self.image_crop_state = self.picker.new_resize_protocol(self.image_source.clone());
204        self.image_scale_state = self.picker.new_resize_protocol(self.image_source.clone());
205    }
206
207    pub fn on_tick(&mut self) {
208        READY.call_once(|| {
209            // This is normally only set by nixosTest.
210            if env::args().any(|arg| arg == "--tmp-demo-ready") {
211                if let Err(err) = std::fs::File::create("/tmp/demo-ready") {
212                    panic!("{err}");
213                }
214            }
215        });
216    }
217
218    fn render_resized_image(&mut self, f: &mut Frame<'_>, resize: Resize, area: Rect) {
219        let (state, name, color) = match resize {
220            Resize::Fit(_) => (&mut self.image_fit_state, "Fit", Color::Magenta),
221            Resize::Crop(_) => (&mut self.image_crop_state, "Crop", Color::Green),
222            Resize::Scale(_) => (&mut self.image_scale_state, "Scale", Color::Blue),
223        };
224        let block = block(name);
225        let inner_area = block.inner(area);
226        f.render_widget(paragraph(self.background.as_str().bg(color)), inner_area);
227        match self.show_images {
228            ShowImages::Fixed => (),
229            _ => f.render_stateful_widget(StatefulImage::new().resize(resize), inner_area, state),
230        };
231        f.render_widget(block, area);
232    }
233}
234
235fn ui(f: &mut Frame<'_>, app: &mut App) {
236    let outer_block = Block::default()
237        .borders(Borders::TOP)
238        .title(app.title.as_str());
239
240    let chunks = Layout::default()
241        .direction(Direction::Horizontal)
242        .constraints([
243            Constraint::Percentage(app.split_percent),
244            Constraint::Percentage(100 - app.split_percent),
245        ])
246        .split(outer_block.inner(f.area()));
247    f.render_widget(outer_block, f.area());
248
249    let left_chunks = vertical_layout().split(chunks[0]);
250    let right_chunks = vertical_layout().split(chunks[1]);
251
252    let block_left_top = block("Fixed");
253    let area = block_left_top.inner(left_chunks[0]);
254    f.render_widget(
255        paragraph(app.background.as_str()).style(Color::Yellow),
256        area,
257    );
258    f.render_widget(block_left_top, left_chunks[0]);
259    match app.show_images {
260        ShowImages::Resized => {}
261        _ => {
262            let image = Image::new(&app.image_static);
263            // Let it be surrounded by styled text.
264            let offset_area = Rect {
265                x: area.x + 1,
266                y: area.y + 1,
267                width: area.width.saturating_sub(2),
268                height: area.height.saturating_sub(2),
269            };
270            f.render_widget(image, offset_area);
271        }
272    }
273
274    let chunks_left_bottom = Layout::default()
275        .direction(Direction::Horizontal)
276        .constraints([Constraint::Percentage(50), Constraint::Percentage(50)])
277        .split(left_chunks[1]);
278
279    app.render_resized_image(f, Resize::Crop(None), chunks_left_bottom[0]);
280    app.render_resized_image(f, Resize::Scale(None), chunks_left_bottom[1]);
281    app.render_resized_image(f, Resize::Fit(None), right_chunks[0]);
282
283    let block_right_bottom = block("Help");
284    let area = block_right_bottom.inner(right_chunks[1]);
285    f.render_widget(
286        paragraph(vec![
287            Line::from("Key bindings:"),
288            Line::from(vec![
289                Span::from("H").green(),
290                Span::from("/"),
291                Span::from("L").green(),
292                Span::from(": resize"),
293            ]),
294            Line::from(vec![Span::from("o").green(), Span::from(": cycle image")]),
295            Line::from(vec![
296                Span::from("t").green(),
297                Span::from(format!(": toggle ({:?})", app.show_images)),
298            ]),
299            Line::from(format!("Font size: {:?}", app.picker.font_size())),
300            Line::from(format!("Protocol: {:?}", app.picker.protocol_type())),
301        ]),
302        area,
303    );
304    f.render_widget(block_right_bottom, right_chunks[1]);
305}
306
307fn paragraph<'a, T: Into<Text<'a>>>(str: T) -> Paragraph<'a> {
308    Paragraph::new(str).wrap(Wrap { trim: true })
309}
310
311fn vertical_layout() -> Layout {
312    Layout::default()
313        .direction(Direction::Vertical)
314        .constraints([Constraint::Percentage(50), Constraint::Percentage(50)])
315}
316
317fn block(name: &str) -> Block<'_> {
318    Block::default().borders(Borders::ALL).title(name)
319}