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