debtmap/tui/
mod.rs

1//! Terminal User Interface (TUI) for debtmap analysis progress.
2//!
3//! This module provides a beautiful zen minimalist TUI using `ratatui` that visualizes
4//! the entire analysis pipeline with hierarchical progress, smooth animations, and rich context.
5//!
6//! # Features
7//!
8//! - **Full pipeline visibility**: All 7 analysis stages displayed at once
9//! - **Hierarchical progress**: Active stages expand to show sub-tasks
10//! - **Rich context**: Counts, percentages, and real-time statistics
11//! - **Smooth animations**: 60 FPS rendering with progress bars and sliding arrows
12//! - **Responsive**: Adapts to terminal size gracefully
13//! - **Zen minimalist design**: Clean, spacious, with subtle visual hierarchy
14//!
15//! # Usage
16//!
17//! ```rust,no_run
18//! use debtmap::tui::TuiManager;
19//!
20//! // Create and initialize TUI
21//! let mut tui = TuiManager::new()?;
22//!
23//! // Render a frame
24//! tui.render()?;
25//!
26//! // TUI cleanup happens automatically on drop
27//! # Ok::<(), std::io::Error>(())
28//! ```
29
30pub mod animation;
31pub mod app;
32pub mod layout;
33pub mod renderer;
34pub mod results;
35pub mod theme;
36
37use crossterm::{
38    event::{self, Event, KeyCode, KeyModifiers},
39    execute,
40    terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
41};
42use ratatui::{backend::CrosstermBackend, Terminal};
43use std::io;
44use std::sync::atomic::{AtomicBool, Ordering};
45use std::sync::Arc;
46
47use app::App;
48use layout::render_adaptive;
49
50/// TUI manager for rendering the analysis progress interface
51pub struct TuiManager {
52    terminal: Arc<std::sync::Mutex<Terminal<CrosstermBackend<io::Stdout>>>>,
53    app: Arc<std::sync::Mutex<App>>,
54    should_exit: Arc<AtomicBool>,
55    render_thread: Option<std::thread::JoinHandle<()>>,
56}
57
58impl TuiManager {
59    /// Create a new TUI manager and initialize the terminal
60    pub fn new() -> io::Result<Self> {
61        enable_raw_mode()?;
62        let mut stdout = io::stdout();
63        execute!(stdout, EnterAlternateScreen)?;
64
65        let backend = CrosstermBackend::new(stdout);
66        let terminal = Terminal::new(backend)?;
67
68        let should_exit = Arc::new(AtomicBool::new(false));
69        let terminal = Arc::new(std::sync::Mutex::new(terminal));
70        let app = Arc::new(std::sync::Mutex::new(App::new()));
71
72        // Setup signal handlers for Ctrl+C and Ctrl+Z
73        let exit_flag = should_exit.clone();
74        std::thread::spawn(move || {
75            loop {
76                if exit_flag.load(Ordering::Relaxed) {
77                    break;
78                }
79
80                // Poll for events with a timeout
81                if event::poll(std::time::Duration::from_millis(100)).unwrap_or(false) {
82                    if let Ok(Event::Key(key)) = event::read() {
83                        // Handle Ctrl+C or Ctrl+Z
84                        if key.code == KeyCode::Char('c')
85                            && key.modifiers.contains(KeyModifiers::CONTROL)
86                        {
87                            // Attempt cleanup before exiting
88                            let _ = disable_raw_mode();
89                            let _ = execute!(io::stdout(), LeaveAlternateScreen);
90                            eprintln!("\nInterrupted by user");
91                            std::process::exit(130); // Standard exit code for Ctrl+C
92                        }
93                        if key.code == KeyCode::Char('z')
94                            && key.modifiers.contains(KeyModifiers::CONTROL)
95                        {
96                            // Attempt cleanup before exiting
97                            let _ = disable_raw_mode();
98                            let _ = execute!(io::stdout(), LeaveAlternateScreen);
99                            eprintln!("\nSuspended by user");
100                            std::process::exit(148); // Standard exit code for Ctrl+Z
101                        }
102                    }
103                }
104            }
105        });
106
107        // Start background render thread for smooth 60 FPS updates
108        let render_terminal = terminal.clone();
109        let render_app = app.clone();
110        let render_exit_flag = should_exit.clone();
111
112        let render_thread = std::thread::spawn(move || {
113            let frame_duration = std::time::Duration::from_millis(16); // ~60 FPS
114
115            loop {
116                if render_exit_flag.load(Ordering::Relaxed) {
117                    break;
118                }
119
120                // Render frame
121                if let (Ok(mut terminal), Ok(mut app)) = (render_terminal.lock(), render_app.lock())
122                {
123                    app.tick();
124                    let _ = terminal.draw(|f| render_adaptive(f, &app));
125                }
126
127                std::thread::sleep(frame_duration);
128            }
129        });
130
131        Ok(Self {
132            terminal,
133            app,
134            should_exit,
135            render_thread: Some(render_thread),
136        })
137    }
138
139    /// Render the current frame (now handled by background thread, kept for compatibility)
140    pub fn render(&mut self) -> io::Result<()> {
141        // Background render thread handles continuous rendering at 60 FPS
142        // This method is now a no-op but kept for API compatibility
143        Ok(())
144    }
145
146    /// Get mutable reference to the application state
147    pub fn app_mut(&mut self) -> std::sync::MutexGuard<'_, App> {
148        self.app.lock().unwrap()
149    }
150
151    /// Get immutable reference to the application state (clone the Arc for read access)
152    pub fn app(&self) -> Arc<std::sync::Mutex<App>> {
153        self.app.clone()
154    }
155
156    /// Clean up and restore terminal
157    pub fn cleanup(&mut self) -> io::Result<()> {
158        // Signal all threads to stop
159        self.should_exit.store(true, Ordering::Relaxed);
160
161        // Wait for render thread to finish
162        if let Some(handle) = self.render_thread.take() {
163            let _ = handle.join();
164        }
165
166        // Give event thread a moment to exit
167        std::thread::sleep(std::time::Duration::from_millis(50));
168
169        disable_raw_mode()?;
170        if let Ok(mut terminal) = self.terminal.lock() {
171            execute!(terminal.backend_mut(), LeaveAlternateScreen)?;
172            terminal.show_cursor()?;
173        }
174        Ok(())
175    }
176}
177
178impl Drop for TuiManager {
179    fn drop(&mut self) {
180        let _ = self.cleanup();
181    }
182}
183
184#[cfg(test)]
185mod tests {
186    use super::*;
187
188    #[test]
189    fn test_app_initialization() {
190        let app = App::new();
191        assert_eq!(app.stages.len(), 6); // 6 stages
192        assert_eq!(app.overall_progress, 0.0);
193    }
194}