pitchfork_cli/tui/
mod.rs

1mod app;
2mod event;
3mod ui;
4
5use crate::ipc::client::IpcClient;
6use crate::Result;
7use crossterm::{
8    event::{DisableMouseCapture, EnableMouseCapture},
9    execute,
10    terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
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            if let Some(action) = event::handle_event(app)? {
79                match action {
80                    event::Action::Quit => break,
81                    event::Action::Start(id) => {
82                        app.start_loading(format!("Starting {}...", id));
83                        terminal.draw(|f| ui::draw(f, app)).into_diagnostic()?;
84                        // Handle start errors gracefully (don't crash TUI)
85                        if let Err(e) = app.start_daemon(client, &id).await {
86                            app.stop_loading();
87                            app.set_message(format!("Failed to start {}: {}", id, e));
88                        } else {
89                            app.stop_loading();
90                        }
91                        app.refresh(client).await?;
92                    }
93                    event::Action::Enable(id) => {
94                        app.start_loading(format!("Enabling {}...", id));
95                        terminal.draw(|f| ui::draw(f, app)).into_diagnostic()?;
96                        client.enable(id.clone()).await?;
97                        app.stop_loading();
98                        app.set_message(format!("Enabled {}", id));
99                        app.refresh(client).await?;
100                    }
101                    event::Action::BatchStart(ids) => {
102                        let count = ids.len();
103                        app.start_loading(format!("Starting {} daemons...", count));
104                        terminal.draw(|f| ui::draw(f, app)).into_diagnostic()?;
105                        let mut started = 0;
106                        for id in &ids {
107                            if app.start_daemon(client, id).await.is_ok() {
108                                started += 1;
109                            }
110                        }
111                        app.stop_loading();
112                        app.clear_selection();
113                        app.set_message(format!("Started {}/{} daemons", started, count));
114                        app.refresh(client).await?;
115                    }
116                    event::Action::BatchEnable(ids) => {
117                        let count = ids.len();
118                        app.start_loading(format!("Enabling {} daemons...", count));
119                        terminal.draw(|f| ui::draw(f, app)).into_diagnostic()?;
120                        for id in &ids {
121                            let _ = client.enable(id.clone()).await;
122                        }
123                        app.stop_loading();
124                        app.clear_selection();
125                        app.set_message(format!("Enabled {} daemons", count));
126                        app.refresh(client).await?;
127                    }
128                    event::Action::Refresh => {
129                        app.start_loading("Refreshing...");
130                        terminal.draw(|f| ui::draw(f, app)).into_diagnostic()?;
131                        app.refresh(client).await?;
132                        app.stop_loading();
133                    }
134                    event::Action::ConfirmPending => {
135                        if let Some(pending) = app.take_pending_action() {
136                            match pending {
137                                app::PendingAction::Stop(id) => {
138                                    app.start_loading(format!("Stopping {}...", id));
139                                    terminal.draw(|f| ui::draw(f, app)).into_diagnostic()?;
140                                    client.stop(id.clone()).await?;
141                                    app.stop_loading();
142                                    app.set_message(format!("Stopped {}", id));
143                                }
144                                app::PendingAction::Restart(id) => {
145                                    app.start_loading(format!("Restarting {}...", id));
146                                    terminal.draw(|f| ui::draw(f, app)).into_diagnostic()?;
147                                    client.stop(id.clone()).await?;
148                                    tokio::time::sleep(Duration::from_millis(500)).await;
149                                    // Handle start errors gracefully (don't crash TUI)
150                                    if let Err(e) = app.start_daemon(client, &id).await {
151                                        app.stop_loading();
152                                        app.set_message(format!(
153                                            "Stopped {} but failed to restart: {}",
154                                            id, e
155                                        ));
156                                    } else {
157                                        app.stop_loading();
158                                        app.set_message(format!("Restarted {}", id));
159                                    }
160                                }
161                                app::PendingAction::Disable(id) => {
162                                    app.start_loading(format!("Disabling {}...", id));
163                                    terminal.draw(|f| ui::draw(f, app)).into_diagnostic()?;
164                                    client.disable(id.clone()).await?;
165                                    app.stop_loading();
166                                    app.set_message(format!("Disabled {}", id));
167                                }
168                                app::PendingAction::BatchStop(ids) => {
169                                    let count = ids.len();
170                                    app.start_loading(format!("Stopping {} daemons...", count));
171                                    terminal.draw(|f| ui::draw(f, app)).into_diagnostic()?;
172                                    for id in &ids {
173                                        let _ = client.stop(id.clone()).await;
174                                    }
175                                    app.stop_loading();
176                                    app.clear_selection();
177                                    app.set_message(format!("Stopped {} daemons", count));
178                                }
179                                app::PendingAction::BatchRestart(ids) => {
180                                    let count = ids.len();
181                                    app.start_loading(format!("Restarting {} daemons...", count));
182                                    terminal.draw(|f| ui::draw(f, app)).into_diagnostic()?;
183                                    // Stop all first
184                                    for id in &ids {
185                                        let _ = client.stop(id.clone()).await;
186                                    }
187                                    tokio::time::sleep(Duration::from_millis(500)).await;
188                                    // Start all
189                                    let mut started = 0;
190                                    for id in &ids {
191                                        if app.start_daemon(client, id).await.is_ok() {
192                                            started += 1;
193                                        }
194                                    }
195                                    app.stop_loading();
196                                    app.clear_selection();
197                                    app.set_message(format!(
198                                        "Restarted {}/{} daemons",
199                                        started, count
200                                    ));
201                                }
202                                app::PendingAction::BatchDisable(ids) => {
203                                    let count = ids.len();
204                                    app.start_loading(format!("Disabling {} daemons...", count));
205                                    terminal.draw(|f| ui::draw(f, app)).into_diagnostic()?;
206                                    for id in &ids {
207                                        let _ = client.disable(id.clone()).await;
208                                    }
209                                    app.stop_loading();
210                                    app.clear_selection();
211                                    app.set_message(format!("Disabled {} daemons", count));
212                                }
213                            }
214                            app.refresh(client).await?;
215                        }
216                    }
217                }
218            }
219        }
220
221        // Auto-refresh daemon list
222        if last_refresh.elapsed() >= REFRESH_RATE {
223            app.refresh(client).await?;
224            last_refresh = std::time::Instant::now();
225        }
226    }
227
228    Ok(())
229}