1mod app;
2mod event;
3mod ui;
4
5use crate::Result;
6use crate::ipc::client::IpcClient;
7use crossterm::{
8 event::{DisableMouseCapture, EnableMouseCapture},
9 execute,
10 terminal::{EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode},
11};
12use log::LevelFilter;
13use miette::IntoDiagnostic;
14use ratatui::prelude::*;
15use std::io;
16use std::time::Duration;
17
18pub use app::App;
19
20const REFRESH_RATE: Duration = Duration::from_secs(2);
21const TICK_RATE: Duration = Duration::from_millis(100);
22
23pub async fn run() -> Result<()> {
24 let prev_log_level = log::max_level();
26 log::set_max_level(LevelFilter::Off);
27
28 enable_raw_mode().into_diagnostic()?;
30 let mut stdout = io::stdout();
31 execute!(stdout, EnterAlternateScreen, EnableMouseCapture).into_diagnostic()?;
32 let backend = CrosstermBackend::new(stdout);
33 let mut terminal = Terminal::new(backend).into_diagnostic()?;
34
35 let result = run_with_cleanup(&mut terminal).await;
37
38 let _ = disable_raw_mode();
40 let _ = execute!(
41 terminal.backend_mut(),
42 LeaveAlternateScreen,
43 DisableMouseCapture
44 );
45 let _ = terminal.show_cursor();
46
47 log::set_max_level(prev_log_level);
49
50 result
51}
52
53async fn run_with_cleanup<B: Backend>(terminal: &mut Terminal<B>) -> Result<()> {
54 let client = IpcClient::connect(true).await?;
56
57 let mut app = App::new();
59 app.refresh(&client).await?;
60
61 run_app(terminal, &mut app, &client).await
63}
64
65async fn run_app<B: Backend>(
66 terminal: &mut Terminal<B>,
67 app: &mut App,
68 client: &IpcClient,
69) -> Result<()> {
70 let mut last_refresh = std::time::Instant::now();
71
72 loop {
73 terminal.draw(|f| ui::draw(f, app)).into_diagnostic()?;
75
76 if crossterm::event::poll(TICK_RATE).into_diagnostic()?
78 && let Some(action) = event::handle_event(app)?
79 {
80 match action {
81 event::Action::Quit => break,
82 event::Action::Start(id) => {
83 app.start_loading(format!("Starting {}...", id));
84 terminal.draw(|f| ui::draw(f, app)).into_diagnostic()?;
85 if let Err(e) = app.start_daemon(client, &id).await {
87 app.stop_loading();
88 app.set_message(format!("Failed to start {}: {}", id, e));
89 } else {
90 app.stop_loading();
91 }
92 app.refresh(client).await?;
93 }
94 event::Action::Enable(id) => {
95 app.start_loading(format!("Enabling {}...", id));
96 terminal.draw(|f| ui::draw(f, app)).into_diagnostic()?;
97 client.enable(id.clone()).await?;
98 app.stop_loading();
99 app.set_message(format!("Enabled {}", id));
100 app.refresh(client).await?;
101 }
102 event::Action::BatchStart(ids) => {
103 let count = ids.len();
104 app.start_loading(format!("Starting {} daemons...", count));
105 terminal.draw(|f| ui::draw(f, app)).into_diagnostic()?;
106 let mut started = 0;
107 for id in &ids {
108 if app.start_daemon(client, id).await.is_ok() {
109 started += 1;
110 }
111 }
112 app.stop_loading();
113 app.clear_selection();
114 app.set_message(format!("Started {}/{} daemons", started, count));
115 app.refresh(client).await?;
116 }
117 event::Action::BatchEnable(ids) => {
118 let count = ids.len();
119 app.start_loading(format!("Enabling {} daemons...", count));
120 terminal.draw(|f| ui::draw(f, app)).into_diagnostic()?;
121 for id in &ids {
122 let _ = client.enable(id.clone()).await;
123 }
124 app.stop_loading();
125 app.clear_selection();
126 app.set_message(format!("Enabled {} daemons", count));
127 app.refresh(client).await?;
128 }
129 event::Action::Refresh => {
130 app.start_loading("Refreshing...");
131 terminal.draw(|f| ui::draw(f, app)).into_diagnostic()?;
132 app.refresh(client).await?;
133 app.stop_loading();
134 }
135 event::Action::OpenEditorNew => {
136 app.open_file_selector();
137 }
138 event::Action::OpenEditorEdit(id) => {
139 app.open_editor_edit(&id);
140 }
141 event::Action::SaveConfig => {
142 app.start_loading("Saving...");
143 terminal.draw(|f| ui::draw(f, app)).into_diagnostic()?;
144 match app.save_editor_config() {
145 Ok(true) => {
146 app.stop_loading();
148 app.close_editor();
149 app.refresh(client).await?;
150 }
151 Ok(false) => {
152 app.stop_loading();
154 }
155 Err(e) => {
156 app.stop_loading();
157 app.set_message(format!("Save failed: {}", e));
158 }
159 }
160 }
161 event::Action::DeleteDaemon { id, config_path } => {
162 app.confirm_action(app::PendingAction::DeleteDaemon { id, config_path });
163 }
164 event::Action::ConfirmPending => {
165 if let Some(pending) = app.take_pending_action() {
166 match pending {
167 app::PendingAction::Stop(id) => {
168 app.start_loading(format!("Stopping {}...", id));
169 terminal.draw(|f| ui::draw(f, app)).into_diagnostic()?;
170 client.stop(id.clone()).await?;
171 app.stop_loading();
172 app.set_message(format!("Stopped {}", id));
173 }
174 app::PendingAction::Restart(id) => {
175 app.start_loading(format!("Restarting {}...", id));
176 terminal.draw(|f| ui::draw(f, app)).into_diagnostic()?;
177 client.stop(id.clone()).await?;
178 tokio::time::sleep(Duration::from_millis(500)).await;
179 if let Err(e) = app.start_daemon(client, &id).await {
181 app.stop_loading();
182 app.set_message(format!(
183 "Stopped {} but failed to restart: {}",
184 id, e
185 ));
186 } else {
187 app.stop_loading();
188 app.set_message(format!("Restarted {}", id));
189 }
190 }
191 app::PendingAction::Disable(id) => {
192 app.start_loading(format!("Disabling {}...", id));
193 terminal.draw(|f| ui::draw(f, app)).into_diagnostic()?;
194 client.disable(id.clone()).await?;
195 app.stop_loading();
196 app.set_message(format!("Disabled {}", id));
197 }
198 app::PendingAction::BatchStop(ids) => {
199 let count = ids.len();
200 app.start_loading(format!("Stopping {} daemons...", count));
201 terminal.draw(|f| ui::draw(f, app)).into_diagnostic()?;
202 for id in &ids {
203 let _ = client.stop(id.clone()).await;
204 }
205 app.stop_loading();
206 app.clear_selection();
207 app.set_message(format!("Stopped {} daemons", count));
208 }
209 app::PendingAction::BatchRestart(ids) => {
210 let count = ids.len();
211 app.start_loading(format!("Restarting {} daemons...", count));
212 terminal.draw(|f| ui::draw(f, app)).into_diagnostic()?;
213 for id in &ids {
215 let _ = client.stop(id.clone()).await;
216 }
217 tokio::time::sleep(Duration::from_millis(500)).await;
218 let mut started = 0;
220 for id in &ids {
221 if app.start_daemon(client, id).await.is_ok() {
222 started += 1;
223 }
224 }
225 app.stop_loading();
226 app.clear_selection();
227 app.set_message(format!("Restarted {}/{} daemons", started, count));
228 }
229 app::PendingAction::BatchDisable(ids) => {
230 let count = ids.len();
231 app.start_loading(format!("Disabling {} daemons...", count));
232 terminal.draw(|f| ui::draw(f, app)).into_diagnostic()?;
233 for id in &ids {
234 let _ = client.disable(id.clone()).await;
235 }
236 app.stop_loading();
237 app.clear_selection();
238 app.set_message(format!("Disabled {} daemons", count));
239 }
240 app::PendingAction::DeleteDaemon { id, config_path } => {
241 app.start_loading(format!("Deleting {}...", id));
242 terminal.draw(|f| ui::draw(f, app)).into_diagnostic()?;
243 match app.delete_daemon_from_config(&id, &config_path) {
244 Ok(true) => {
245 app.stop_loading();
246 app.close_editor();
247 app.set_message(format!("Deleted {}", id));
248 }
249 Ok(false) => {
250 app.stop_loading();
251 app.set_message(format!(
252 "Daemon '{}' not found in config",
253 id
254 ));
255 }
256 Err(e) => {
257 app.stop_loading();
258 app.set_message(format!("Delete failed: {}", e));
259 }
260 }
261 }
262 app::PendingAction::DiscardEditorChanges => {
263 app.close_editor();
264 }
265 }
266 app.refresh(client).await?;
267 }
268 }
269 }
270 }
271
272 if last_refresh.elapsed() >= REFRESH_RATE {
274 app.refresh(client).await?;
275 last_refresh = std::time::Instant::now();
276 }
277 }
278
279 Ok(())
280}