debtmap 0.16.4

Code complexity and technical debt analyzer
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
//! Progress feedback infrastructure for debtmap analysis.
//!
//! This module provides two complementary progress reporting systems:
//!
//! 1. **Effects-based progress** (new): [`ProgressSink`] trait with [`HasProgress`]
//!    environment extension for composable, testable progress reporting.
//!
//! 2. **Legacy progress** (existing): [`ProgressManager`] using `indicatif` for
//!    direct progress bar management.
//!
//! # Effects-Based Progress (Recommended)
//!
//! The effects-based system follows the "Pure Core, Imperative Shell" principle:
//!
//! ```rust,ignore
//! use debtmap::effects::progress::{with_stage, traverse_with_progress};
//! use debtmap::progress::implementations::RecordingProgressSink;
//!
//! // Pure analysis function wrapped with progress reporting
//! fn analyze_files(files: Vec<PathBuf>) -> AnalysisEffect<Vec<FileMetrics>> {
//!     traverse_with_progress(files, "File Analysis", |path| {
//!         analyze_file_effect(path)
//!     })
//! }
//!
//! // In tests, use RecordingProgressSink to verify behavior
//! let recorder = Arc::new(RecordingProgressSink::new());
//! let env = RealEnv::with_progress(config, recorder.clone());
//! let result = analyze_files(files).run(&env).await?;
//! assert!(recorder.stages().contains(&"File Analysis".to_string()));
//! ```
//!
//! ## Progress Sink Implementations
//!
//! | Implementation | Use Case |
//! |---------------|----------|
//! | [`SilentProgressSink`] | Testing, CI, benchmarks |
//! | [`CliProgressSink`] | Command-line tools |
//! | [`RecordingProgressSink`] | Test assertions |
//!
//! # Legacy Progress System
//!
//! The [`ProgressManager`] provides direct control over `indicatif` progress bars.
//! This is still used for TUI integration and will be gradually migrated to
//! the effects-based system.
//!
//! ```rust,no_run
//! use debtmap::progress::{ProgressConfig, ProgressManager, TEMPLATE_CALL_GRAPH};
//!
//! let config = ProgressConfig::from_env(false, 0);
//! let manager = ProgressManager::new(config);
//!
//! let progress = manager.create_bar(100, TEMPLATE_CALL_GRAPH);
//! progress.set_message("Building call graph");
//!
//! for _i in 0..100 {
//!     progress.inc(1);
//! }
//!
//! progress.finish_with_message("Call graph complete");
//! ```
//!
//! # Progress Behavior
//!
//! - **Quiet Mode**: No progress output (respects `DEBTMAP_QUIET` env var and `--quiet` flag)
//! - **Non-TTY**: Gracefully disables progress bars in CI and piped output
//! - **Verbosity Levels**:
//!   - Level 0 (default): Main progress bars only
//!   - Level 1 (-v): Sub-phase progress and timing
//!   - Level 2 (-vv): Detailed per-phase metrics

pub mod implementations;
pub mod traits;

// Re-export traits for convenience
pub use implementations::{
    CliProgressSink, ProgressEvent, RecordingProgressSink, SilentProgressSink,
};
pub use traits::{HasProgress, ProgressSink};

// Legacy progress system - kept for backwards compatibility
use indicatif::{MultiProgress, ProgressBar, ProgressStyle};
use once_cell::sync::Lazy;
use parking_lot::Mutex;
use std::sync::Arc;

// Progress bar templates - text-based for terminal compatibility
pub const TEMPLATE_CALL_GRAPH: &str = "{msg} {pos}/{len} files ({percent}%) - {eta}";
pub const TEMPLATE_TRAIT_RESOLUTION: &str = "{msg} {pos}/{len} traits - {eta}";
pub const TEMPLATE_COVERAGE: &str = "{msg} {pos}/{len} files - {eta}";
pub const TEMPLATE_FUNCTION_ANALYSIS: &str =
    "{msg} {pos}/{len} functions ({percent}%) - {per_sec}/sec - {eta}";
pub const TEMPLATE_FILE_ANALYSIS: &str = "{msg} {pos}/{len} files ({percent}%) - {eta}";
pub const TEMPLATE_SPINNER: &str = "{spinner} {msg}";

/// Configuration for progress display behavior
#[derive(Debug, Clone, Default)]
pub struct ProgressConfig {
    /// Whether to suppress all progress output
    pub quiet_mode: bool,
    /// Verbosity level (0 = basic, 1 = detailed, 2 = very detailed)
    pub verbosity: u8,
}

impl ProgressConfig {
    /// Create progress configuration from environment and CLI arguments
    pub fn from_env(quiet: bool, verbosity: u8) -> Self {
        let env_quiet = std::env::var("DEBTMAP_QUIET").is_ok();
        Self {
            quiet_mode: quiet || env_quiet,
            verbosity,
        }
    }

    /// Determine if progress bars should be displayed
    pub fn should_show_progress(&self) -> bool {
        // Check if we're in quiet mode
        if self.quiet_mode {
            return false;
        }

        // Check if stderr is a TTY using std::io::IsTerminal
        use std::io::IsTerminal;
        std::io::stderr().is_terminal()
    }
}

/// Global progress manager instance
static GLOBAL_PROGRESS: Lazy<Arc<Mutex<Option<ProgressManager>>>> =
    Lazy::new(|| Arc::new(Mutex::new(None)));

/// Centralized progress manager for coordinating multiple progress bars
#[derive(Clone)]
pub struct ProgressManager {
    multi: Arc<MultiProgress>,
    config: ProgressConfig,
    tui_manager: Arc<Mutex<Option<crate::tui::TuiManager>>>,
    tui_active: Arc<Mutex<bool>>,
}

impl ProgressManager {
    /// Create a new progress manager with the given configuration
    pub fn new(config: ProgressConfig) -> Self {
        // Initialize TUI if terminal is interactive and not in quiet mode
        let tui_manager = if config.should_show_progress() {
            crate::tui::TuiManager::new().ok()
        } else {
            None
        };

        let tui_active = tui_manager.is_some();

        Self {
            multi: Arc::new(MultiProgress::new()),
            config,
            tui_manager: Arc::new(Mutex::new(tui_manager)),
            tui_active: Arc::new(Mutex::new(tui_active)),
        }
    }

    /// Initialize the global progress manager
    pub fn init_global(config: ProgressConfig) {
        let manager = Self::new(config);
        // parking_lot::Mutex::lock() never fails (no poisoning)
        *GLOBAL_PROGRESS.lock() = Some(manager);
    }

    /// Get a reference to the global progress manager
    pub fn global() -> Option<Self> {
        // parking_lot::Mutex::lock() never fails (no poisoning)
        GLOBAL_PROGRESS.lock().clone()
    }

    /// Create a progress bar with the given length and template
    ///
    /// Returns a hidden progress bar if progress should not be shown or if TUI is active
    pub fn create_bar(&self, len: u64, template: &str) -> ProgressBar {
        // Hide indicatif bars if TUI is active or if progress should not be shown
        // parking_lot::Mutex::lock() never fails (no poisoning)
        if !self.config.should_show_progress() || *self.tui_active.lock() {
            return ProgressBar::hidden();
        }

        let pb = self.multi.add(ProgressBar::new(len));
        pb.set_style(
            ProgressStyle::default_bar()
                .template(template)
                .expect("Invalid progress bar template")
                .progress_chars("█▓▒░  "),
        );
        pb
    }

    /// Create a spinner progress bar with the given message
    ///
    /// Returns a hidden progress bar if progress should not be shown or if TUI is active
    pub fn create_spinner(&self, msg: &str) -> ProgressBar {
        // Hide indicatif spinners if TUI is active or if progress should not be shown
        // parking_lot::Mutex::lock() never fails (no poisoning)
        if !self.config.should_show_progress() || *self.tui_active.lock() {
            return ProgressBar::hidden();
        }

        let pb = self.multi.add(ProgressBar::new_spinner());
        pb.set_style(
            ProgressStyle::default_spinner()
                .template(TEMPLATE_SPINNER)
                .expect("Invalid spinner template")
                .tick_chars("|/-\\"),
        );
        pb.set_message(msg.to_string());
        pb.enable_steady_tick(std::time::Duration::from_millis(100));
        pb
    }

    /// Create a progress bar that shows counts without a known total
    pub fn create_counter(&self, template: &str, msg: &str) -> ProgressBar {
        // Hide indicatif counters if TUI is active or if progress should not be shown
        // parking_lot::Mutex::lock() never fails (no poisoning)
        if !self.config.should_show_progress() || *self.tui_active.lock() {
            return ProgressBar::hidden();
        }

        let pb = self.multi.add(ProgressBar::new_spinner());
        pb.set_style(
            ProgressStyle::default_spinner()
                .template(template)
                .expect("Invalid counter template")
                .tick_chars("|/-\\"),
        );
        pb.set_message(msg.to_string());
        pb.enable_steady_tick(std::time::Duration::from_millis(100));
        pb
    }

    /// Get the verbosity level
    pub fn verbosity(&self) -> u8 {
        self.config.verbosity
    }

    /// Check if TUI is currently active
    pub fn is_tui_active(&self) -> Option<bool> {
        // parking_lot::Mutex::lock() never fails (no poisoning)
        Some(*self.tui_active.lock())
    }

    /// Clear all progress bars from the display
    ///
    /// This should be called before printing final output to ensure progress bars
    /// don't interfere with the terminal display.
    pub fn clear(&self) -> std::io::Result<()> {
        self.multi.clear()
    }

    /// Start a pipeline stage in TUI
    pub fn tui_start_stage(&self, stage_index: usize) {
        // parking_lot::Mutex::lock() never fails (no poisoning)
        let guard = self.tui_manager.lock();
        if let Some(ref tui) = *guard {
            // Bind the Arc first to avoid temporary dropping
            let app_arc = tui.app();
            let mut app = app_arc.lock();
            app.start_stage(stage_index);
        }
    }

    /// Complete a pipeline stage in TUI
    pub fn tui_complete_stage(&self, stage_index: usize, metric: impl Into<String>) {
        let guard = self.tui_manager.lock();
        if let Some(ref tui) = *guard {
            let app_arc = tui.app();
            let mut app = app_arc.lock();
            app.complete_stage(stage_index, metric);
        }
    }

    /// Update stage metric in TUI
    pub fn tui_update_metric(&self, stage_index: usize, metric: impl Into<String>) {
        let guard = self.tui_manager.lock();
        if let Some(ref tui) = *guard {
            let app_arc = tui.app();
            let mut app = app_arc.lock();
            app.update_stage_metric(stage_index, metric);
        }
    }

    /// Update subtask status in TUI
    pub fn tui_update_subtask(
        &self,
        stage_index: usize,
        subtask_index: usize,
        status: crate::tui::app::StageStatus,
        progress: Option<(usize, usize)>,
    ) {
        let guard = self.tui_manager.lock();
        if let Some(ref tui) = *guard {
            let app_arc = tui.app();
            let mut app = app_arc.lock();
            app.update_subtask(stage_index, subtask_index, status, progress);
        }
    }

    /// Update overall progress in TUI
    pub fn tui_set_progress(&self, progress: f64) {
        let guard = self.tui_manager.lock();
        if let Some(ref tui) = *guard {
            let app_arc = tui.app();
            let mut app = app_arc.lock();
            app.set_overall_progress(progress);
        }
    }

    /// Update statistics in TUI
    pub fn tui_update_stats(&self, functions: usize, debt: usize, coverage: f64) {
        let guard = self.tui_manager.lock();
        if let Some(ref tui) = *guard {
            let app_arc = tui.app();
            let mut app = app_arc.lock();
            app.update_stats(functions, debt, coverage);
        }
    }

    /// Update only function and debt counts in TUI
    pub fn tui_update_counts(&self, functions: usize, debt: usize) {
        let guard = self.tui_manager.lock();
        if let Some(ref tui) = *guard {
            let app_arc = tui.app();
            let mut app = app_arc.lock();
            app.functions_count = functions;
            app.debt_count = debt;
        }
    }

    /// Update only coverage percentage in TUI
    pub fn tui_update_coverage(&self, coverage: f64) {
        let guard = self.tui_manager.lock();
        if let Some(ref tui) = *guard {
            let app_arc = tui.app();
            let mut app = app_arc.lock();
            app.coverage_percent = coverage;
        }
    }

    /// Render a TUI frame (now no-op, background thread handles rendering at 60 FPS)
    pub fn tui_render(&self) {
        // Background render thread in TuiManager handles continuous rendering
        // This method kept for backwards compatibility
    }

    /// Cleanup TUI on completion
    pub fn tui_cleanup(&self) {
        // parking_lot::Mutex::lock() never fails (no poisoning)
        let mut guard = self.tui_manager.lock();
        if let Some(ref mut tui) = *guard {
            let _ = tui.cleanup();
        }
        // Mark TUI as inactive
        *self.tui_active.lock() = false;
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_quiet_mode_disables_progress() {
        std::env::set_var("DEBTMAP_QUIET", "1");
        let config = ProgressConfig::from_env(false, 0);
        assert!(!config.should_show_progress());
        std::env::remove_var("DEBTMAP_QUIET");
    }

    #[test]
    fn test_explicit_quiet_flag() {
        let config = ProgressConfig::from_env(true, 0);
        assert!(!config.should_show_progress());
    }

    #[test]
    fn test_verbosity_levels() {
        let config = ProgressConfig::from_env(false, 0);
        assert_eq!(config.verbosity, 0);

        let config = ProgressConfig::from_env(false, 2);
        assert_eq!(config.verbosity, 2);
    }

    #[test]
    fn test_progress_manager_creates_hidden_bars_in_quiet_mode() {
        let config = ProgressConfig {
            quiet_mode: true,
            verbosity: 0,
        };
        let manager = ProgressManager::new(config);

        let pb = manager.create_bar(100, TEMPLATE_CALL_GRAPH);
        assert!(pb.is_hidden());

        let spinner = manager.create_spinner("Test");
        assert!(spinner.is_hidden());
    }
}