Satellite tracking TUI using ratatui
1use chrono::Utc; 2use ratatui::crossterm::event::{self, poll, EnableMouseCapture, Event, KeyCode}; 3use ratatui::crossterm::execute; 4use ratatui::crossterm::terminal::{enable_raw_mode, EnterAlternateScreen}; 5use ratatui::crossterm::event::DisableMouseCapture; 6use ratatui::crossterm::terminal::{disable_raw_mode, LeaveAlternateScreen}; 7use ratatui::prelude::{Backend, CrosstermBackend}; 8use ratatui::Terminal; 9use sgp4::{julian_years_since_j2000, MinutesSinceEpoch}; 10use std::f32::consts::PI; 11use std::io::{self, Result}; 12use std::sync::{Arc, Mutex}; 13use std::time::Duration; 14 15mod app; 16mod ui; 17mod location; 18 19#[cfg(feature = "logging")] 20mod logging; 21 22use app::*; 23 24#[tokio::main] 25async fn main() -> Result<()> { 26 #[cfg(feature = "logging")] 27 let _ = logging::initialize_logging(); 28 enable_raw_mode()?; 29 let mut stderr = io::stderr(); // This is a special case. Normally using stdout is fine 30 execute!(stderr, EnterAlternateScreen, EnableMouseCapture)?; 31 let backend = CrosstermBackend::new(stderr); 32 let mut terminal = Terminal::new(backend)?; 33 34 // create app and run it 35 36 let app = Arc::new(Mutex::new(App::new())); 37 let location_shared_app = Arc::clone(&app); 38 let location_shared_app2 = Arc::clone(&app); 39 let mut runtime_shared_app = Arc::clone(&app); 40 41 42 let get_constant = tokio::task::spawn(async move { 43 location_shared_app.lock().unwrap().getting_location = true; 44 let constant = location::get_celerak_data().await; 45 if let (Some(cons),Some(epoch)) = constant { 46 location_shared_app.lock().unwrap().iss_constant = Some(cons); 47 location_shared_app.lock().unwrap().iss_constant_epoch = Some(epoch); 48 } 49 location_shared_app.lock().unwrap().getting_location = false; 50 }); 51 52 let _ = get_constant; 53 54 55 56 let forever = tokio::task::spawn(async move { 57 let mut interval = tokio::time::interval(Duration::from_secs(2)); 58 59 loop { 60 interval.tick().await; 61 if location_shared_app2.lock().unwrap().iss_constant.is_some() { 62 let now = Utc::now(); 63 let d_since_j2000 = julian_years_since_j2000(&now.naive_utc()) * 365.25; 64 let rad_diff_earth_rotation = location::earth_rotation_angle(d_since_j2000); 65 let time_diff = now - location_shared_app2.lock().unwrap().iss_constant_epoch.unwrap(); 66 let prediction = location_shared_app2.lock().unwrap().iss_constant.as_ref().unwrap() 67 .propagate(MinutesSinceEpoch(time_diff.num_seconds() as f64 / 60.0)).unwrap(); 68 69 let pred_spherical = location::polar_loc_transformer(prediction.position); 70 71 let long_value = if pred_spherical[0]-(rad_diff_earth_rotation as f32)*180.0/PI < 180.0 72 { 73 pred_spherical[0]-(rad_diff_earth_rotation as f32)*180.0/PI + 360.0 74 } else { 75 pred_spherical[0]-(rad_diff_earth_rotation as f32)*180.0/PI 76 }; 77 location_shared_app2.lock().unwrap().iss_location = Some(location::FLocation { 78 79 latitude: pred_spherical[1], 80 longitude: long_value 81 }); 82 83 } 84 } ; 85 }); 86 87 88 89 let _ = forever; 90 91 let _res = run_app(&mut terminal, &mut runtime_shared_app).await; 92 // restore terminal 93 disable_raw_mode()?; 94 execute!( 95 terminal.backend_mut(), 96 LeaveAlternateScreen, 97 DisableMouseCapture 98 )?; 99 terminal.show_cursor()?; 100 101 102 Ok(()) 103} 104 105 106async fn run_app<B:Backend> (terminal: &mut Terminal<B>, mut app: &mut Arc<Mutex<App>>) -> Result<bool> { 107 loop { 108 109 110 let _ = terminal.draw(|f| ui::ui(f, &mut app)); 111 112 113 114 if poll(Duration::from_millis(15))? { 115 116 117 if let Event::Key(key) = event::read()? { 118 let app_shared = Arc::new(&app); 119 let mut app_shared = app_shared.lock().unwrap(); 120 if key.kind == event::KeyEventKind::Release { 121 continue; 122 } 123 match app_shared.current_screen { 124 app::CurrentScreen::Map => match key.code { 125 KeyCode::Char('c') => { 126 app_shared.current_screen = CurrentScreen::CityChoice; 127 } 128 KeyCode::Char('q') => { 129 app_shared.current_screen = CurrentScreen::Exiting; 130 } 131 _ => {} 132 133 } 134 app::CurrentScreen::Exiting => match key.code { 135 KeyCode::Char('n') | KeyCode::Esc => { 136 app_shared.current_screen = CurrentScreen::Map 137 } 138 KeyCode::Char('y') | KeyCode::Char('q') => { 139 return Ok(false) 140 } 141 _ => {} 142 } 143 144 //weird behaviour with the ListState being 1 longer then the list, resulting in 145 //index overflow for the cities list. do not know why. yes even after rendering 146 //when the ListState was supposed to update 147 app::CurrentScreen::CityChoice => match key.code { 148 KeyCode::Up => { 149 150 if app_shared.cities.state.selected() == Some(0) { 151 app_shared.cities.state.select(Some(CITIES.len()-1)); 152 } else { 153 app_shared.cities.state.scroll_up_by(1); 154 } 155 156 }, 157 KeyCode::Down => { 158 if app_shared.cities.state.selected() == Some(app_shared.cities.cities.len()-1) { 159 app_shared.cities.state.select(Some(0)); 160 } else { 161 162 app_shared.cities.state.scroll_down_by(1); 163 } 164 } 165 KeyCode::Enter => app_shared.current_screen = CurrentScreen::Map, 166 KeyCode::Esc => app_shared.current_screen = CurrentScreen::Map, 167 KeyCode::Char('q') => app_shared.current_screen = CurrentScreen::Map, 168 _ => {} 169 } 170 171 } 172 } 173 174 175 } 176 } 177} 178 179 180 181 182 183 184