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 constant_obtain_shared_app = Arc::clone(&app); 38 let location_update_shared_app = Arc::clone(&app); 39 let mut runtime_shared_app = Arc::clone(&app); 40 41 42 let get_constant = tokio::task::spawn(async move { 43 constant_obtain_shared_app.lock().unwrap().getting_location = true; 44 let constant = location::get_celerak_data().await; 45 if let (Some(cons),Some(epoch)) = constant { 46 constant_obtain_shared_app.lock().unwrap().iss_constant = Some(cons); 47 constant_obtain_shared_app.lock().unwrap().iss_constant_epoch = Some(epoch); 48 } 49 constant_obtain_shared_app.lock().unwrap().getting_location = false; 50 }); 51 52 let _ = get_constant; 53 54 55 56 57 58 59 60 let forever = tokio::task::spawn(async move { 61 let mut interval = tokio::time::interval(Duration::from_secs(2)); 62 63 loop { 64 interval.tick().await; 65 if location_update_shared_app.lock().unwrap().iss_constant.is_some() { 66 let now = Utc::now(); 67 let d_since_j2000 = julian_years_since_j2000(&now.naive_utc()) * 365.25; 68 let rad_diff_earth_rotation = location::earth_rotation_angle(d_since_j2000); 69 let time_diff = now - location_update_shared_app.lock().unwrap().iss_constant_epoch.unwrap(); 70 let prediction = location_update_shared_app.lock().unwrap().iss_constant.as_ref().unwrap() 71 .propagate(MinutesSinceEpoch(time_diff.num_seconds() as f64 / 60.0)).unwrap(); 72 73 let pred_spherical = location::polar_loc_transformer(prediction.position); 74 75 let long_value = if pred_spherical[0]-(rad_diff_earth_rotation as f32)*180.0/PI < -180.0 76 { 77 pred_spherical[0]-(rad_diff_earth_rotation as f32)*180.0/PI + 360.0 78 } else { 79 pred_spherical[0]-(rad_diff_earth_rotation as f32)*180.0/PI 80 }; 81 location_update_shared_app.lock().unwrap().iss_location = Some(location::FLocation { 82 83 latitude: pred_spherical[1], 84 longitude: long_value 85 }); 86 87 } 88 } ; 89 }); 90 91 92 93 let _ = forever; 94 95 let _res = run_app(&mut terminal, &mut runtime_shared_app).await; 96 // restore terminal 97 disable_raw_mode()?; 98 execute!( 99 terminal.backend_mut(), 100 LeaveAlternateScreen, 101 DisableMouseCapture 102 )?; 103 terminal.show_cursor()?; 104 105 106 Ok(()) 107} 108 109 110async fn run_app<B:Backend> (terminal: &mut Terminal<B>, mut app: &mut Arc<Mutex<App>>) -> Result<bool> { 111 loop { 112 113 114 let _ = terminal.draw(|f| ui::ui(f, &mut app)); 115 116 117 118 if poll(Duration::from_millis(15))? { 119 120 121 if let Event::Key(key) = event::read()? { 122 let app_shared = Arc::new(&app); 123 let mut app_shared = app_shared.lock().unwrap(); 124 if key.kind == event::KeyEventKind::Release { 125 continue; 126 } 127 match app_shared.current_screen { 128 app::CurrentScreen::Map => match key.code { 129 KeyCode::Char('c') => { 130 app_shared.current_screen = CurrentScreen::CityChoice; 131 } 132 KeyCode::Char('q') => { 133 app_shared.current_screen = CurrentScreen::Exiting; 134 } 135 _ => {} 136 137 } 138 app::CurrentScreen::Exiting => match key.code { 139 KeyCode::Char('n') | KeyCode::Esc => { 140 app_shared.current_screen = CurrentScreen::Map 141 } 142 KeyCode::Char('y') | KeyCode::Char('q') => { 143 return Ok(false) 144 } 145 _ => {} 146 } 147 148 //weird behaviour with the ListState being 1 longer then the list, resulting in 149 //index overflow for the cities list. do not know why. yes even after rendering 150 //when the ListState was supposed to update 151 app::CurrentScreen::CityChoice => match key.code { 152 KeyCode::Up => { 153 154 if app_shared.cities.state.selected() == Some(0) { 155 app_shared.cities.state.select(Some(CITIES.len()-1)); 156 } else { 157 app_shared.cities.state.scroll_up_by(1); 158 } 159 160 }, 161 KeyCode::Down => { 162 if app_shared.cities.state.selected() == Some(app_shared.cities.cities.len()-1) { 163 app_shared.cities.state.select(Some(0)); 164 } else { 165 166 app_shared.cities.state.scroll_down_by(1); 167 } 168 } 169 KeyCode::Enter => app_shared.current_screen = CurrentScreen::Map, 170 KeyCode::Esc => app_shared.current_screen = CurrentScreen::Map, 171 KeyCode::Char('q') => app_shared.current_screen = CurrentScreen::Map, 172 _ => {} 173 } 174 175 } 176 } 177 178 179 } 180 } 181} 182 183 184 185 186 187 188