1mod app;
2mod event;
3mod ui;
4
5use crate::Result;
6use crate::ipc::batch::StartOptions;
7use crate::ipc::client::IpcClient;
8use crate::settings::settings;
9use crossterm::{
10 event::{DisableMouseCapture, EnableMouseCapture},
11 execute,
12 terminal::{EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode},
13};
14use log::LevelFilter;
15use miette::IntoDiagnostic;
16use ratatui::prelude::*;
17use std::io;
18use std::sync::Arc;
19
20pub use app::App;
21
22pub async fn run() -> Result<()> {
23 let prev_log_level = log::max_level();
25 log::set_max_level(LevelFilter::Off);
26
27 enable_raw_mode().into_diagnostic()?;
29 let mut stdout = io::stdout();
30 execute!(stdout, EnterAlternateScreen, EnableMouseCapture).into_diagnostic()?;
31 let backend = CrosstermBackend::new(stdout);
32 let mut terminal = Terminal::new(backend).into_diagnostic()?;
33
34 let result = run_with_cleanup(&mut terminal).await;
36
37 let _ = disable_raw_mode();
39 let _ = execute!(
40 terminal.backend_mut(),
41 LeaveAlternateScreen,
42 DisableMouseCapture
43 );
44 let _ = terminal.show_cursor();
45
46 log::set_max_level(prev_log_level);
48
49 result
50}
51
52async fn run_with_cleanup<B: Backend>(terminal: &mut Terminal<B>) -> Result<()> {
53 let client = Arc::new(IpcClient::connect(true).await?);
55
56 let mut app = App::new();
58 app.refresh(&client).await?;
59
60 run_app(terminal, &mut app, &client).await
62}
63
64async fn run_app<B: Backend>(
65 terminal: &mut Terminal<B>,
66 app: &mut App,
67 client: &Arc<IpcClient>,
68) -> Result<()> {
69 let s = settings();
70 let tick_rate = s.tui_tick_rate();
71 let refresh_rate = s.tui_refresh_rate();
72 let mut last_refresh = std::time::Instant::now();
73
74 loop {
75 terminal.draw(|f| ui::draw(f, app)).into_diagnostic()?;
77
78 if crossterm::event::poll(tick_rate).into_diagnostic()?
80 && let Some(action) = event::handle_event(app)?
81 {
82 match action {
83 event::Action::Quit => break,
84 event::Action::Start(id) => {
85 app.start_loading(format!("Starting {id}..."));
86 terminal.draw(|f| ui::draw(f, app)).into_diagnostic()?;
87
88 let result = client
89 .start_daemons(std::slice::from_ref(&id), StartOptions::default())
90 .await;
91
92 app.stop_loading();
93 match result {
94 Ok(r) if r.any_failed => {
95 app.set_message(format!("Failed to start {id}"));
96 }
97 Ok(r) if !r.started.is_empty() => {
98 app.set_message(format!("Started {id}"));
99 }
100 Ok(_) => {
101 app.set_message(format!("No daemons were started for {id}"));
102 }
103 Err(e) => {
104 app.set_message(format!("Failed to start {id}: {e}"));
105 }
106 }
107 app.refresh(client).await?;
108 }
109 event::Action::Enable(id) => {
110 app.start_loading(format!("Enabling {id}..."));
111 terminal.draw(|f| ui::draw(f, app)).into_diagnostic()?;
112 client.enable(id.clone()).await?;
113 app.stop_loading();
114 app.set_message(format!("Enabled {id}"));
115 app.refresh(client).await?;
116 }
117 event::Action::BatchStart(ids) => {
118 let count = ids.len();
119 app.start_loading(format!("Starting {count} daemons..."));
120 terminal.draw(|f| ui::draw(f, app)).into_diagnostic()?;
121
122 let result = client.start_daemons(&ids, StartOptions::default()).await;
123
124 app.stop_loading();
125 app.clear_selection();
126 match result {
127 Ok(r) => {
128 let started = r.started.len();
129 if r.any_failed {
130 app.set_message(format!(
131 "Started {started}/{count} daemons (some failed)"
132 ));
133 } else {
134 app.set_message(format!("Started {started} daemons"));
135 }
136 }
137 Err(e) => {
138 app.set_message(format!("Failed to start daemons: {e}"));
139 }
140 }
141 app.refresh(client).await?;
142 }
143 event::Action::BatchEnable(ids) => {
144 let count = ids.len();
145 app.start_loading(format!("Enabling {count} daemons..."));
146 terminal.draw(|f| ui::draw(f, app)).into_diagnostic()?;
147 for id in &ids {
148 let _ = client.enable(id.clone()).await;
149 }
150 app.stop_loading();
151 app.clear_selection();
152 app.set_message(format!("Enabled {count} daemons"));
153 app.refresh(client).await?;
154 }
155 event::Action::Refresh => {
156 app.start_loading("Refreshing...");
157 terminal.draw(|f| ui::draw(f, app)).into_diagnostic()?;
158 app.refresh(client).await?;
159 app.stop_loading();
160 }
161 event::Action::OpenEditorNew => {
162 app.open_file_selector();
163 }
164 event::Action::OpenEditorEdit(id) => {
165 app.open_editor_edit(&id);
166 }
167 event::Action::SaveConfig => {
168 app.start_loading("Saving...");
169 terminal.draw(|f| ui::draw(f, app)).into_diagnostic()?;
170 match app.save_editor_config() {
171 Ok(true) => {
172 app.stop_loading();
174 app.close_editor();
175 app.refresh(client).await?;
176 }
177 Ok(false) => {
178 app.stop_loading();
180 }
181 Err(e) => {
182 app.stop_loading();
183 app.set_message(format!("Save failed: {e}"));
184 }
185 }
186 }
187 event::Action::DeleteDaemon { id, config_path } => {
188 app.confirm_action(app::PendingAction::DeleteDaemon { id, config_path });
189 }
190 event::Action::ConfirmPending => {
191 if let Some(pending) = app.take_pending_action() {
192 match pending {
193 app::PendingAction::Stop(id) => {
194 app.start_loading(format!("Stopping {id}..."));
195 terminal.draw(|f| ui::draw(f, app)).into_diagnostic()?;
196 let result = client.stop(id.clone()).await;
197 app.stop_loading();
198 match result {
199 Ok(true) => app.set_message(format!("Stopped {id}")),
200 Ok(false) => {
201 app.set_message(format!("Daemon {id} was not running"))
202 }
203 Err(e) => app.set_message(format!("Failed to stop {id}: {e}")),
204 }
205 }
206 app::PendingAction::Restart(id) => {
207 app.start_loading(format!("Restarting {id}..."));
208 terminal.draw(|f| ui::draw(f, app)).into_diagnostic()?;
209
210 let opts = StartOptions {
212 force: true,
213 ..Default::default()
214 };
215 let result =
216 client.start_daemons(std::slice::from_ref(&id), opts).await;
217
218 app.stop_loading();
219 match result {
220 Ok(r) if r.any_failed => {
221 app.set_message(format!("Failed to restart {id}"));
222 }
223 Ok(_) => {
224 app.set_message(format!("Restarted {id}"));
225 }
226 Err(e) => {
227 app.set_message(format!("Failed to restart {id}: {e}"));
228 }
229 }
230 }
231 app::PendingAction::Disable(id) => {
232 app.start_loading(format!("Disabling {id}..."));
233 terminal.draw(|f| ui::draw(f, app)).into_diagnostic()?;
234 client.disable(id.clone()).await?;
235 app.stop_loading();
236 app.set_message(format!("Disabled {id}"));
237 }
238 app::PendingAction::BatchStop(ids) => {
239 let count = ids.len();
240 app.start_loading(format!("Stopping {count} daemons..."));
241 terminal.draw(|f| ui::draw(f, app)).into_diagnostic()?;
242 let result = client.stop_daemons(&ids).await;
243 app.stop_loading();
244 app.clear_selection();
245 match result {
246 Ok(r) if r.any_failed => {
247 app.set_message(format!(
248 "Stopped {count} daemons (some failed)"
249 ));
250 }
251 Ok(_) => {
252 app.set_message(format!("Stopped {count} daemons"));
253 }
254 Err(e) => {
255 app.set_message(format!("Failed to stop daemons: {e}"));
256 }
257 }
258 }
259 app::PendingAction::BatchRestart(ids) => {
260 let count = ids.len();
261 app.start_loading(format!("Restarting {count} daemons..."));
262 terminal.draw(|f| ui::draw(f, app)).into_diagnostic()?;
263
264 let opts = StartOptions {
266 force: true,
267 ..Default::default()
268 };
269 let result = client.start_daemons(&ids, opts).await;
270
271 app.stop_loading();
272 app.clear_selection();
273 match result {
274 Ok(r) => {
275 let restarted = r.started.len();
276 if r.any_failed {
277 app.set_message(format!("Restarted {restarted}/{count} daemons (some failed)"));
278 } else {
279 app.set_message(format!(
280 "Restarted {restarted} daemons"
281 ));
282 }
283 }
284 Err(e) => {
285 app.set_message(format!("Failed to restart daemons: {e}"));
286 }
287 }
288 }
289 app::PendingAction::BatchDisable(ids) => {
290 let count = ids.len();
291 app.start_loading(format!("Disabling {count} daemons..."));
292 terminal.draw(|f| ui::draw(f, app)).into_diagnostic()?;
293 for id in &ids {
294 let _ = client.disable(id.clone()).await;
295 }
296 app.stop_loading();
297 app.clear_selection();
298 app.set_message(format!("Disabled {count} daemons"));
299 }
300 app::PendingAction::DeleteDaemon { id, config_path } => {
301 app.start_loading(format!("Deleting {id}..."));
302 terminal.draw(|f| ui::draw(f, app)).into_diagnostic()?;
303 match app.delete_daemon_from_config(&id, &config_path) {
304 Ok(true) => {
305 app.stop_loading();
306 app.close_editor();
307 app.set_message(format!("Deleted {id}"));
308 }
309 Ok(false) => {
310 app.stop_loading();
311 app.set_message(format!(
312 "Daemon '{id}' not found in config"
313 ));
314 }
315 Err(e) => {
316 app.stop_loading();
317 app.set_message(format!("Delete failed: {e}"));
318 }
319 }
320 }
321 app::PendingAction::DiscardEditorChanges => {
322 app.close_editor();
323 }
324 }
325 app.refresh(client).await?;
326 }
327 }
328 }
329 }
330
331 if last_refresh.elapsed() >= refresh_rate {
333 app.refresh(client).await?;
334
335 if app.view == app::View::Network {
337 app.refresh_network().await;
338 }
339
340 last_refresh = std::time::Instant::now();
341 }
342 }
343
344 Ok(())
345}