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 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 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 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}