pitchfork_cli/tui/
mod.rs

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    // Suppress terminal logging while TUI is active (logs still go to file)
25    let prev_log_level = log::max_level();
26    log::set_max_level(LevelFilter::Off);
27
28    // Setup terminal
29    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    // Run with cleanup guaranteed
36    let result = run_with_cleanup(&mut terminal).await;
37
38    // Restore terminal (always runs)
39    let _ = disable_raw_mode();
40    let _ = execute!(
41        terminal.backend_mut(),
42        LeaveAlternateScreen,
43        DisableMouseCapture
44    );
45    let _ = terminal.show_cursor();
46
47    // Restore log level
48    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    // Connect to supervisor (auto-start if needed)
55    let client = IpcClient::connect(true).await?;
56
57    // Create app state
58    let mut app = App::new();
59    app.refresh(&client).await?;
60
61    // Run main loop
62    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        // Draw UI
74        terminal.draw(|f| ui::draw(f, app)).into_diagnostic()?;
75
76        // Handle events with timeout
77        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                    // Handle start errors gracefully (don't crash TUI)
86                    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::ConfirmPending => {
136                    if let Some(pending) = app.take_pending_action() {
137                        match pending {
138                            app::PendingAction::Stop(id) => {
139                                app.start_loading(format!("Stopping {}...", id));
140                                terminal.draw(|f| ui::draw(f, app)).into_diagnostic()?;
141                                client.stop(id.clone()).await?;
142                                app.stop_loading();
143                                app.set_message(format!("Stopped {}", id));
144                            }
145                            app::PendingAction::Restart(id) => {
146                                app.start_loading(format!("Restarting {}...", id));
147                                terminal.draw(|f| ui::draw(f, app)).into_diagnostic()?;
148                                client.stop(id.clone()).await?;
149                                tokio::time::sleep(Duration::from_millis(500)).await;
150                                // Handle start errors gracefully (don't crash TUI)
151                                if let Err(e) = app.start_daemon(client, &id).await {
152                                    app.stop_loading();
153                                    app.set_message(format!(
154                                        "Stopped {} but failed to restart: {}",
155                                        id, e
156                                    ));
157                                } else {
158                                    app.stop_loading();
159                                    app.set_message(format!("Restarted {}", id));
160                                }
161                            }
162                            app::PendingAction::Disable(id) => {
163                                app.start_loading(format!("Disabling {}...", id));
164                                terminal.draw(|f| ui::draw(f, app)).into_diagnostic()?;
165                                client.disable(id.clone()).await?;
166                                app.stop_loading();
167                                app.set_message(format!("Disabled {}", id));
168                            }
169                            app::PendingAction::BatchStop(ids) => {
170                                let count = ids.len();
171                                app.start_loading(format!("Stopping {} daemons...", count));
172                                terminal.draw(|f| ui::draw(f, app)).into_diagnostic()?;
173                                for id in &ids {
174                                    let _ = client.stop(id.clone()).await;
175                                }
176                                app.stop_loading();
177                                app.clear_selection();
178                                app.set_message(format!("Stopped {} daemons", count));
179                            }
180                            app::PendingAction::BatchRestart(ids) => {
181                                let count = ids.len();
182                                app.start_loading(format!("Restarting {} daemons...", count));
183                                terminal.draw(|f| ui::draw(f, app)).into_diagnostic()?;
184                                // Stop all first
185                                for id in &ids {
186                                    let _ = client.stop(id.clone()).await;
187                                }
188                                tokio::time::sleep(Duration::from_millis(500)).await;
189                                // Start all
190                                let mut started = 0;
191                                for id in &ids {
192                                    if app.start_daemon(client, id).await.is_ok() {
193                                        started += 1;
194                                    }
195                                }
196                                app.stop_loading();
197                                app.clear_selection();
198                                app.set_message(format!("Restarted {}/{} daemons", started, count));
199                            }
200                            app::PendingAction::BatchDisable(ids) => {
201                                let count = ids.len();
202                                app.start_loading(format!("Disabling {} daemons...", count));
203                                terminal.draw(|f| ui::draw(f, app)).into_diagnostic()?;
204                                for id in &ids {
205                                    let _ = client.disable(id.clone()).await;
206                                }
207                                app.stop_loading();
208                                app.clear_selection();
209                                app.set_message(format!("Disabled {} daemons", count));
210                            }
211                        }
212                        app.refresh(client).await?;
213                    }
214                }
215            }
216        }
217
218        // Auto-refresh daemon list
219        if last_refresh.elapsed() >= REFRESH_RATE {
220            app.refresh(client).await?;
221            last_refresh = std::time::Instant::now();
222        }
223    }
224
225    Ok(())
226}