Skip to main content

scope/
app.rs

1use crossterm::event::{self, Event, KeyCode, KeyModifiers};
2use ratatui::{
3	backend::Backend,
4	layout::{Constraint, Rect},
5	style::{Modifier, Style},
6	symbols::Marker,
7	widgets::Chart,
8	widgets::{Cell, Row, Table},
9	Terminal,
10};
11use std::{
12	io,
13	time::{Duration, Instant},
14};
15
16use crate::{
17	display::{
18		oscilloscope::Oscilloscope, spectroscope::Spectroscope, update_value_f, update_value_i,
19		vectorscope::Vectorscope, Dimension, DisplayMode, GraphConfig,
20	},
21	input::{DataSource, Matrix},
22};
23
24pub enum CurrentDisplayMode {
25	Oscilloscope,
26	Vectorscope,
27	Spectroscope,
28}
29
30pub struct App {
31	#[allow(unused)]
32	channels: u8,
33	graph: GraphConfig,
34	oscilloscope: Oscilloscope,
35	vectorscope: Vectorscope,
36	spectroscope: Spectroscope,
37	mode: CurrentDisplayMode,
38}
39
40// TODO another way to build this that doesn't require getting cli args directly!!!
41impl App {
42	pub fn new(ui: &crate::cfg::UiOptions, source: &crate::cfg::SourceOptions) -> Self {
43		let graph = GraphConfig {
44			axis_color: ui.axis_color,
45			labels_color: ui.labels_color,
46			palette: ui.palette_color.clone(),
47			scale: ui.scale as f64,
48			width: source.buffer, // TODO also make bit depth customizable
49			samples: source.buffer,
50			sampling_rate: source.sample_rate,
51			references: !ui.no_reference,
52			show_ui: !ui.no_ui,
53			scatter: ui.scatter,
54			pause: false,
55			marker_type: if ui.no_braille {
56				Marker::Dot
57			} else {
58				Marker::Braille
59			},
60		};
61
62		let oscilloscope = Oscilloscope::default();
63		let vectorscope = Vectorscope::default();
64		let spectroscope = Spectroscope::from(source);
65
66		App {
67			graph,
68			oscilloscope,
69			vectorscope,
70			spectroscope,
71			mode: CurrentDisplayMode::Oscilloscope,
72			channels: source.channels as u8,
73		}
74	}
75
76	pub fn run<T: Backend>(
77		&mut self,
78		mut source: Box<dyn DataSource<f64>>,
79		mut terminal: Terminal<T>,
80	) -> Result<(), io::Error> {
81		let mut fps = 0;
82		let mut framerate = 0;
83		let mut last_poll = Instant::now();
84		let mut channels = Matrix::default();
85
86		loop {
87			let data = source.recv().ok_or(io::Error::new(
88				io::ErrorKind::BrokenPipe,
89				"data source returned null",
90			))?;
91
92			if !self.graph.pause {
93				channels = data;
94			}
95
96			fps += 1;
97
98			if last_poll.elapsed().as_secs() >= 1 {
99				framerate = fps;
100				fps = 0;
101				last_poll = Instant::now();
102			}
103
104			{
105				let mut datasets = Vec::new();
106				let graph = self.graph.clone(); // TODO cheap fix...
107				if self.graph.references {
108					datasets.append(&mut self.current_display_mut().references(&graph));
109				}
110				datasets.append(&mut self.current_display_mut().process(&graph, &channels));
111				terminal.draw(|f| {
112					let mut size = f.area();
113					if self.graph.show_ui {
114						f.render_widget(
115							make_header(
116								&self.graph,
117								&self.current_display().header(&self.graph),
118								self.current_display().mode_str(),
119								framerate,
120								self.graph.pause,
121							),
122							Rect {
123								x: size.x,
124								y: size.y,
125								width: size.width,
126								height: 1,
127							}, // a 1px line at the top
128						);
129						size.height -= 1;
130						size.y += 1;
131					}
132					let chart = Chart::new(datasets.iter().map(|x| x.into()).collect())
133						.x_axis(self.current_display().axis(&self.graph, Dimension::X)) // TODO allow to have axis sometimes?
134						.y_axis(self.current_display().axis(&self.graph, Dimension::Y));
135					f.render_widget(chart, size)
136				})?;
137			}
138
139			while event::poll(Duration::from_millis(0))? {
140				// process all enqueued events
141				let event = event::read()?;
142
143				if self.process_events(event.clone())? {
144					return Ok(());
145				}
146				self.current_display_mut().handle(event);
147			}
148		}
149	}
150
151	fn current_display_mut(&mut self) -> &mut dyn DisplayMode {
152		match self.mode {
153			CurrentDisplayMode::Oscilloscope => &mut self.oscilloscope as &mut dyn DisplayMode,
154			CurrentDisplayMode::Vectorscope => &mut self.vectorscope as &mut dyn DisplayMode,
155			CurrentDisplayMode::Spectroscope => &mut self.spectroscope as &mut dyn DisplayMode,
156		}
157	}
158
159	fn current_display(&self) -> &dyn DisplayMode {
160		match self.mode {
161			CurrentDisplayMode::Oscilloscope => &self.oscilloscope as &dyn DisplayMode,
162			CurrentDisplayMode::Vectorscope => &self.vectorscope as &dyn DisplayMode,
163			CurrentDisplayMode::Spectroscope => &self.spectroscope as &dyn DisplayMode,
164		}
165	}
166
167	fn process_events(&mut self, event: Event) -> Result<bool, io::Error> {
168		let mut quit = false;
169		if let Event::Key(key) = event {
170			if let KeyModifiers::CONTROL = key.modifiers {
171				match key.code {
172					// mimic other programs shortcuts to quit, for user friendlyness
173					KeyCode::Char('c') | KeyCode::Char('q') | KeyCode::Char('w') => quit = true,
174					_ => {}
175				}
176			}
177			let magnitude = match key.modifiers {
178				KeyModifiers::SHIFT => 10.0,
179				KeyModifiers::CONTROL => 5.0,
180				KeyModifiers::ALT => 0.2,
181				_ => 1.0,
182			};
183			match key.code {
184				KeyCode::Up => update_value_f(&mut self.graph.scale, 0.01, magnitude, 0.0..10.0), // inverted to act as zoom
185				KeyCode::Down => update_value_f(&mut self.graph.scale, -0.01, magnitude, 0.0..10.0), // inverted to act as zoom
186				KeyCode::Right => update_value_i(
187					&mut self.graph.samples,
188					true,
189					25,
190					magnitude,
191					0..self.graph.width * 2,
192				),
193				KeyCode::Left => update_value_i(
194					&mut self.graph.samples,
195					false,
196					25,
197					magnitude,
198					0..self.graph.width * 2,
199				),
200				KeyCode::Char('q') => quit = true,
201				KeyCode::Char(' ') => self.graph.pause = !self.graph.pause,
202				KeyCode::Char('s') => self.graph.scatter = !self.graph.scatter,
203				KeyCode::Char('h') => self.graph.show_ui = !self.graph.show_ui,
204				KeyCode::Char('r') => self.graph.references = !self.graph.references,
205				KeyCode::Tab => {
206					// switch modes
207					match self.mode {
208						CurrentDisplayMode::Oscilloscope => {
209							self.mode = CurrentDisplayMode::Vectorscope
210						}
211						CurrentDisplayMode::Vectorscope => {
212							self.mode = CurrentDisplayMode::Spectroscope
213						}
214						CurrentDisplayMode::Spectroscope => {
215							self.mode = CurrentDisplayMode::Oscilloscope
216						}
217					}
218				}
219				KeyCode::Esc => {
220					self.graph.samples = self.graph.width;
221					self.graph.scale = 1.;
222				}
223				_ => {}
224			}
225		};
226
227		Ok(quit)
228	}
229}
230
231// TODO can these be removed or merged somewhere else?
232
233fn make_header<'a>(
234	cfg: &GraphConfig,
235	module_header: &'a str,
236	kind_o_scope: &'static str,
237	fps: usize,
238	pause: bool,
239) -> Table<'a> {
240	Table::new(
241		vec![Row::new(vec![
242			Cell::from(format!("{}::scope-tui", kind_o_scope)).style(
243				Style::default()
244					.fg(*cfg.palette.first().expect("empty palette?"))
245					.add_modifier(Modifier::BOLD),
246			),
247			Cell::from(module_header),
248			Cell::from(format!("-{:.2}x+", cfg.scale)),
249			Cell::from(format!("{}/{} spf", cfg.samples, cfg.width)),
250			Cell::from(format!("{}fps", fps)),
251			Cell::from(if cfg.scatter { "***" } else { "---" }),
252			Cell::from(if pause { "||" } else { "|>" }),
253		])],
254		vec![
255			Constraint::Percentage(35),
256			Constraint::Percentage(25),
257			Constraint::Percentage(7),
258			Constraint::Percentage(13),
259			Constraint::Percentage(6),
260			Constraint::Percentage(6),
261			Constraint::Percentage(6),
262		],
263	)
264	.style(Style::default().fg(cfg.labels_color))
265}