scope/
app.rs

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