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