Browse and listen to thousands of radio stations across the globe right from your terminal ๐ŸŒŽ ๐Ÿ“ป ๐ŸŽตโœจ
radio rust tokio web-radio command-line-tool tui
at main 6.3 kB view raw
1use crossterm::event::{Event, KeyCode, KeyModifiers}; 2use ratatui::{ 3 style::Style, 4 text::Span, 5 widgets::{Axis, GraphType}, 6}; 7 8use crate::{ 9 app::{update_value_f, update_value_i}, 10 input::Matrix, 11}; 12 13use super::{DataSet, Dimension, DisplayMode, GraphConfig}; 14 15#[derive(Default)] 16pub struct Oscilloscope { 17 pub triggering: bool, 18 pub falling_edge: bool, 19 pub threshold: f64, 20 pub depth: u32, 21 pub peaks: bool, 22} 23 24impl DisplayMode for Oscilloscope { 25 fn from_args(_opts: &crate::cfg::SourceOptions) -> Self { 26 Oscilloscope::default() 27 } 28 29 fn mode_str(&self) -> &'static str { 30 "oscillo" 31 } 32 33 fn channel_name(&self, index: usize) -> String { 34 match index { 35 0 => "L".into(), 36 1 => "R".into(), 37 _ => format!("{}", index), 38 } 39 } 40 41 fn header(&self, _: &GraphConfig) -> String { 42 if self.triggering { 43 format!( 44 "{} {:.0}{} trigger", 45 if self.falling_edge { "v" } else { "^" }, 46 self.threshold, 47 if self.depth > 1 { 48 format!(":{}", self.depth) 49 } else { 50 "".into() 51 }, 52 ) 53 } else { 54 "live".into() 55 } 56 } 57 58 fn axis(&self, cfg: &GraphConfig, dimension: Dimension) -> Axis<'_> { 59 let (name, bounds) = match dimension { 60 Dimension::X => ("time -", [0.0, cfg.samples as f64]), 61 Dimension::Y => ("| amplitude", [-cfg.scale, cfg.scale]), 62 }; 63 let mut a = Axis::default(); 64 if cfg.show_ui { 65 // TODO don't make it necessary to check show_ui inside here 66 a = a.title(Span::styled(name, Style::default().fg(cfg.labels_color))); 67 } 68 a.style(Style::default().fg(cfg.axis_color)).bounds(bounds) 69 } 70 71 fn references(&self, cfg: &GraphConfig) -> Vec<DataSet> { 72 vec![DataSet::new( 73 None, 74 vec![(0.0, 0.0), (cfg.samples as f64, 0.0)], 75 cfg.marker_type, 76 GraphType::Line, 77 cfg.axis_color, 78 )] 79 } 80 81 fn process(&mut self, cfg: &GraphConfig, data: &Matrix<f64>) -> Vec<DataSet> { 82 let mut out = Vec::new(); 83 84 let mut trigger_offset = 0; 85 if self.depth == 0 { 86 self.depth = 1 87 } 88 if self.triggering { 89 for i in 0..data[0].len() { 90 if triggered(&data[0], i, self.threshold, self.depth, self.falling_edge) { 91 // triggered 92 break; 93 } 94 trigger_offset += 1; 95 } 96 } 97 98 if self.triggering { 99 out.push(DataSet::new( 100 Some("T".into()), 101 vec![(0.0, self.threshold)], 102 cfg.marker_type, 103 GraphType::Scatter, 104 cfg.labels_color, 105 )); 106 } 107 108 for (n, channel) in data.iter().enumerate().rev() { 109 let (mut min, mut max) = (0.0, 0.0); 110 let mut tmp = Vec::new(); 111 for (i, sample) in channel.iter().enumerate() { 112 if *sample < min { 113 min = *sample 114 }; 115 if *sample > max { 116 max = *sample 117 }; 118 if i >= trigger_offset { 119 tmp.push(((i - trigger_offset) as f64, *sample)); 120 } 121 } 122 123 if self.peaks { 124 out.push(DataSet::new( 125 None, 126 vec![(0.0, min), (0.0, max)], 127 cfg.marker_type, 128 GraphType::Scatter, 129 cfg.palette(n), 130 )) 131 } 132 133 out.push(DataSet::new( 134 Some(self.channel_name(n)), 135 tmp, 136 cfg.marker_type, 137 if cfg.scatter { 138 GraphType::Scatter 139 } else { 140 GraphType::Line 141 }, 142 cfg.palette(n), 143 )); 144 } 145 146 out 147 } 148 149 fn handle(&mut self, event: Event) { 150 if let Event::Key(key) = event { 151 let magnitude = match key.modifiers { 152 KeyModifiers::SHIFT => 10.0, 153 KeyModifiers::CONTROL => 5.0, 154 KeyModifiers::ALT => 0.2, 155 _ => 1.0, 156 }; 157 match key.code { 158 KeyCode::PageUp => { 159 update_value_f(&mut self.threshold, 250.0, magnitude, 0.0..32768.0) 160 } 161 KeyCode::PageDown => { 162 update_value_f(&mut self.threshold, -250.0, magnitude, 0.0..32768.0) 163 } 164 KeyCode::Char('t') => self.triggering = !self.triggering, 165 KeyCode::Char('e') => self.falling_edge = !self.falling_edge, 166 KeyCode::Char('p') => self.peaks = !self.peaks, 167 KeyCode::Char('=') => update_value_i(&mut self.depth, true, 1, 1.0, 1..65535), 168 KeyCode::Char('-') => update_value_i(&mut self.depth, false, 1, 1.0, 1..65535), 169 KeyCode::Char('+') => update_value_i(&mut self.depth, true, 10, 1.0, 1..65535), 170 KeyCode::Char('_') => update_value_i(&mut self.depth, false, 10, 1.0, 1..65535), 171 KeyCode::Esc => { 172 self.triggering = false; 173 } 174 _ => {} 175 } 176 } 177 } 178} 179 180#[allow(clippy::collapsible_else_if)] // TODO can this be made nicer? 181fn triggered(data: &[f64], index: usize, threshold: f64, depth: u32, falling_edge: bool) -> bool { 182 if data.len() < index + (1 + depth as usize) { 183 return false; 184 } 185 if falling_edge { 186 if data[index] >= threshold { 187 for i in 1..=depth as usize { 188 if data[index + i] >= threshold { 189 return false; 190 } 191 } 192 true 193 } else { 194 false 195 } 196 } else { 197 if data[index] <= threshold { 198 for i in 1..=depth as usize { 199 if data[index + i] <= threshold { 200 return false; 201 } 202 } 203 true 204 } else { 205 false 206 } 207 } 208}