1mod app;
2mod event;
3mod ui;
4
5use crate::Result;
6use crate::ipc::batch::StartOptions;
7use crate::ipc::client::IpcClient;
8use crossterm::{
9 event::{DisableMouseCapture, EnableMouseCapture},
10 execute,
11 terminal::{EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode},
12};
13use log::LevelFilter;
14use miette::IntoDiagnostic;
15use ratatui::prelude::*;
16use std::io;
17use std::sync::Arc;
18use std::time::Duration;
19
20pub use app::App;
21
22const REFRESH_RATE: Duration = Duration::from_secs(2);
23const TICK_RATE: Duration = Duration::from_millis(100);
24
25pub async fn run() -> Result<()> {
26 let prev_log_level = log::max_level();
28 log::set_max_level(LevelFilter::Off);
29
30 enable_raw_mode().into_diagnostic()?;
32 let mut stdout = io::stdout();
33 execute!(stdout, EnterAlternateScreen, EnableMouseCapture).into_diagnostic()?;
34 let backend = CrosstermBackend::new(stdout);
35 let mut terminal = Terminal::new(backend).into_diagnostic()?;
36
37 let result = run_with_cleanup(&mut terminal).await;
39
40 let _ = disable_raw_mode();
42 let _ = execute!(
43 terminal.backend_mut(),
44 LeaveAlternateScreen,
45 DisableMouseCapture
46 );
47 let _ = terminal.show_cursor();
48
49 log::set_max_level(prev_log_level);
51
52 result
53}
54
55async fn run_with_cleanup<B: Backend>(terminal: &mut Terminal<B>) -> Result<()> {
56 let client = Arc::new(IpcClient::connect(true).await?);
58
59 let mut app = App::new();
61 app.refresh(&client).await?;
62
63 run_app(terminal, &mut app, &client).await
65}
66
67async fn run_app<B: Backend>(
68 terminal: &mut Terminal<B>,
69 app: &mut App,
70 client: &Arc<IpcClient>,
71) -> Result<()> {
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 last_refresh = std::time::Instant::now();
335 }
336 }
337
338 Ok(())
339}