1mod app;
2mod event;
3mod radio_task;
4mod ui;
5
6use std::io;
7
8use app::App;
9use clap::Parser;
10use crossterm::execute;
11use crossterm::terminal::{
12 EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode,
13};
14use ratatui::Terminal;
15use ratatui::backend::CrosstermBackend;
16
17#[derive(Parser, Debug)]
19#[command(version, about)]
20struct Cli {
21 #[arg(short, long)]
23 port: Option<String>,
24
25 #[arg(short, long, default_value_t = 115_200)]
27 baud: u32,
28
29 #[arg(long, default_value = "safe")]
31 mcp_speed: String,
32}
33
34fn main() -> Result<(), Box<dyn std::error::Error>> {
35 type BtResult = Result<(String, kenwood_thd75::transport::EitherTransport), String>;
36
37 let cli = Cli::parse();
38
39 if std::env::var("RUST_LOG").is_ok() {
40 let log_file = std::fs::File::create("thd75-tui.log").expect("failed to create log file");
41 tracing_subscriber::fmt()
42 .with_env_filter(tracing_subscriber::EnvFilter::from_default_env())
43 .with_writer(log_file)
44 .with_ansi(false)
45 .init();
46 }
47
48 let original_hook = std::panic::take_hook();
49 std::panic::set_hook(Box::new(move |panic_info| {
50 let _ = disable_raw_mode();
51 let _ = execute!(io::stdout(), LeaveAlternateScreen);
52 original_hook(panic_info);
53 }));
54
55 let transport = radio_task::discover_and_open_transport(cli.port.as_deref(), cli.baud);
57
58 enable_raw_mode()?;
60 let mut stdout = io::stdout();
61 execute!(stdout, EnterAlternateScreen)?;
62 let backend = CrosstermBackend::new(stdout);
63 let mut terminal = Terminal::new(backend)?;
64
65 let (done_tx, done_rx) = std::sync::mpsc::channel::<Result<(), String>>();
66
67 let (bt_req_tx, bt_req_rx) = std::sync::mpsc::channel::<(Option<String>, u32)>();
71 let (bt_resp_tx, bt_resp_rx) = std::sync::mpsc::channel::<BtResult>();
72
73 let mcp_speed = cli.mcp_speed;
74
75 let _thread = std::thread::spawn(move || {
76 let rt = tokio::runtime::Builder::new_multi_thread()
77 .enable_all()
78 .build()
79 .expect("failed to build tokio runtime");
80
81 let result = rt.block_on(async {
82 run_app(&mut terminal, transport, mcp_speed, bt_req_tx, bt_resp_rx)
83 .await
84 .map_err(|e| e.to_string())
85 });
86
87 let _ = disable_raw_mode();
88 let _ = execute!(terminal.backend_mut(), LeaveAlternateScreen);
89 let _ = terminal.show_cursor();
90
91 let _ = done_tx.send(result);
92 });
93
94 loop {
96 #[cfg(target_os = "macos")]
97 #[allow(unsafe_code)]
98 unsafe {
99 unsafe extern "C" {
100 fn CFRunLoopRunInMode(
101 mode: *const std::ffi::c_void,
102 seconds: f64,
103 returnAfterSourceHandled: u8,
104 ) -> i32;
105 static kCFRunLoopDefaultMode: *const std::ffi::c_void;
106 }
107 let _ = CFRunLoopRunInMode(kCFRunLoopDefaultMode, 0.01, 0);
108 }
109
110 #[cfg(not(target_os = "macos"))]
111 std::thread::sleep(std::time::Duration::from_millis(10));
112
113 if let Ok((port, baud)) = bt_req_rx.try_recv() {
116 let result = radio_task::discover_and_open_transport(port.as_deref(), baud);
117 let _ = bt_resp_tx.send(result);
118 }
119
120 if let Ok(result) = done_rx.try_recv() {
121 if let Err(e) = result {
122 eprintln!("Error: {e}");
123 }
124 break;
125 }
126 }
127
128 Ok(())
129}
130
131async fn run_app(
132 terminal: &mut Terminal<CrosstermBackend<io::Stdout>>,
133 transport: Result<(String, kenwood_thd75::transport::EitherTransport), String>,
134 mcp_speed: String,
135 bt_req_tx: std::sync::mpsc::Sender<(Option<String>, u32)>,
136 bt_resp_rx: std::sync::mpsc::Receiver<
137 Result<(String, kenwood_thd75::transport::EitherTransport), String>,
138 >,
139) -> Result<(), Box<dyn std::error::Error>> {
140 let mut events = event::EventHandler::new();
141 let tx = events.sender();
142 let cmd_rx = events.take_command_receiver();
143
144 let (path, transport) = transport.map_err(|e| format!("Could not connect to radio: {e}"))?;
145
146 let port_display = match radio_task::spawn_with_transport(
147 path, transport, mcp_speed, tx, cmd_rx, bt_req_tx, bt_resp_rx,
148 )
149 .await
150 {
151 Ok(p) => p,
152 Err(e) => return Err(format!("Could not connect to radio: {e}").into()),
153 };
154
155 let mut app = App::new(port_display);
156 app.connected = true;
157 app.cmd_tx = Some(events.command_sender());
158
159 let _ = terminal.draw(|frame| ui::render(&app, frame))?;
160
161 loop {
162 let msg = events.next().await;
163 let needs_render = app.update(msg);
164 if app.should_quit {
165 break;
166 }
167 if needs_render {
168 let _ = terminal.draw(|frame| ui::render(&app, frame))?;
169 }
170 }
171
172 Ok(())
173}