rskiller 0.2.1

Find and clean Rust project build artifacts and caches with parallel processing
Documentation
use crate::cli::{Cli, Color};
use crate::config::Config;
use crate::project::RustProject;
use crate::scanner::ProjectScanner;
use crate::utils;
use anyhow::Result;
use colored::Colorize;
use crossterm::{
    cursor,
    event::{self, Event, KeyCode, KeyEvent},
    execute,
    terminal::{self, ClearType},
};
use std::io::{self, Write};
use std::process;

/// Interactive terminal user interface for browsing and cleaning Rust projects
#[derive(Debug)]
pub struct InteractiveUI {
    cli: Cli,
    config: Config,
    projects: Vec<RustProject>,
    selected_index: usize,
    total_deleted_size: u64,
    deleted_count: usize,
}

impl InteractiveUI {
    pub fn new(cli: Cli, config: Config) -> Self {
        Self {
            cli,
            config,
            projects: Vec::new(),
            selected_index: 0,
            total_deleted_size: 0,
            deleted_count: 0,
        }
    }

    pub async fn run(&mut self) -> Result<()> {
        // Enable raw mode
        terminal::enable_raw_mode()?;
        
        // Clear screen and hide cursor
        execute!(
            io::stdout(),
            terminal::Clear(ClearType::All),
            cursor::Hide
        )?;

        let result = self.run_interactive_loop().await;

        // Cleanup
        execute!(io::stdout(), cursor::Show)?;
        terminal::disable_raw_mode()?;

        result
    }

    async fn run_interactive_loop(&mut self) -> Result<()> {
        // Initial scan
        let scanner = ProjectScanner::new(self.cli.clone(), self.config.clone());
        self.projects = scanner.scan().await?;

        if self.projects.is_empty() {
            println!("🚫 No Rust projects found.");
            return Ok(());
        }

        loop {
            self.display_interface()?;

            if let Event::Key(key_event) = event::read()? {
                match self.handle_key_event(key_event).await? {
                    ControlFlow::Exit => break,
                    ControlFlow::Continue => continue,
                }
            }
        }

        Ok(())
    }

    fn display_interface(&self) -> Result<()> {
        execute!(io::stdout(), terminal::Clear(ClearType::All), cursor::MoveTo(0, 0))?;

        // Header
        println!("{}", "🦀 RSKILL - Rust Project Cleaner".cyan().bold());
        println!("{}", "".repeat(80));
        println!();

        // Instructions
        println!("📋 {} | {} | {} | {} | {}",
            "↑↓ Navigate".blue(),
            "Space/Del Delete".red(),
            "o Open".green(),
            "r Refresh".yellow(),
            "q Quit".magenta()
        );
        println!();

        // Project list
        let start_row = 6;
        let max_visible = 15;
        let start_index = if self.selected_index >= max_visible {
            self.selected_index - max_visible + 1
        } else {
            0
        };

        for (i, project) in self.projects
            .iter()
            .skip(start_index)
            .take(max_visible)
            .enumerate()
        {
            let actual_index = start_index + i;
            let is_selected = actual_index == self.selected_index;
            
            self.display_project_row(project, is_selected, start_row + i)?;
        }

        // Footer with statistics
        self.display_footer()?;

        io::stdout().flush()?;
        Ok(())
    }

    fn display_project_row(&self, project: &RustProject, is_selected: bool, row: usize) -> Result<()> {
        execute!(io::stdout(), cursor::MoveTo(0, row as u16))?;

        let size_str = project.format_size(self.cli.gb);
        let path_str = utils::get_relative_path(&project.path);
        let path_display = utils::truncate_string(&path_str, 35);

        let last_mod = project
            .days_since_modified()
            .map(|days| {
                if days == 0 {
                    "Today".to_string()
                } else if days == 1 {
                    "1 day ago".to_string()
                } else {
                    format!("{} days ago", days)
                }
            })
            .unwrap_or_else(|| "Unknown".to_string());

        // Color coding based on project status
        let status_color = if project.is_likely_active() {
            "green"
        } else {
            "yellow"
        };

        let warning_indicator = if project.target_dir.is_none() {
            " (no target)".red().to_string()
        } else if !project.is_likely_active() && project.total_cleanable_size() > 1024 * 1024 * 100 { // > 100MB and stale
            " ⚠️".to_string()
        } else {
            "".to_string()
        };

        if is_selected {
            let _highlight_color = self.get_highlight_color();
            print!(
                "{} {:<25} {:<12} {:<35} {:<15}{}",
                "".bold(),
                project.name.bold(),
                size_str,
                path_display,
                last_mod,
                warning_indicator
            );
        } else {
            let last_mod_colored = if status_color == "green" {
                last_mod.green()
            } else {
                last_mod.yellow()
            };
            
            print!(
                "  {:<25} {:<12} {:<35} {:<15}{}",
                project.name,
                size_str.cyan(),
                path_display.bright_black(),
                last_mod_colored,
                warning_indicator
            );
        }

        Ok(())
    }

    fn display_footer(&self) -> Result<()> {
        let footer_row = 23;
        execute!(io::stdout(), cursor::MoveTo(0, footer_row))?;

        println!("{}", "".repeat(80));
        
        let total_projects = self.projects.len();
        let total_size: u64 = self.projects.iter().map(|p| p.total_cleanable_size()).sum();
        let total_size_str = utils::format_size(total_size, self.cli.gb);

        println!(
            "📊 {} projects | 💾 {} cleanable | 🗑️  {} deleted ({})",
            total_projects.to_string().bold(),
            total_size_str.bold().green(),
            self.deleted_count.to_string().bold(),
            utils::format_size(self.total_deleted_size, self.cli.gb).bold().red()
        );

        if let Some(selected_project) = self.projects.get(self.selected_index) {
            println!();
            println!("📁 {}", selected_project.path.display().to_string().bright_blue());
            
            if let Some(target_dir) = &selected_project.target_dir {
                println!("🎯 Target: {}", target_dir.display().to_string().bright_black());
            }
            
            if selected_project.workspace_root {
                println!("🏗️  Workspace root");
            }
            
            if selected_project.dependencies_count > 0 {
                println!("📦 {} dependencies", selected_project.dependencies_count);
            }
        }

        Ok(())
    }

    async fn handle_key_event(&mut self, key_event: KeyEvent) -> Result<ControlFlow> {
        match key_event.code {
            KeyCode::Char('q') | KeyCode::Esc => Ok(ControlFlow::Exit),
            
            KeyCode::Up | KeyCode::Char('k') => {
                if self.selected_index > 0 {
                    self.selected_index -= 1;
                }
                Ok(ControlFlow::Continue)
            }
            
            KeyCode::Down | KeyCode::Char('j') => {
                if self.selected_index < self.projects.len().saturating_sub(1) {
                    self.selected_index += 1;
                }
                Ok(ControlFlow::Continue)
            }
            
            KeyCode::Delete | KeyCode::Char(' ') => {
                self.delete_selected_project().await?;
                Ok(ControlFlow::Continue)
            }
            
            KeyCode::Char('o') => {
                self.open_selected_project()?;
                Ok(ControlFlow::Continue)
            }
            
            KeyCode::Char('r') => {
                self.refresh_projects().await?;
                Ok(ControlFlow::Continue)
            }
            
            KeyCode::Char('a') => {
                self.delete_all_projects().await?;
                Ok(ControlFlow::Continue)
            }
            
            _ => Ok(ControlFlow::Continue),
        }
    }

    async fn delete_selected_project(&mut self) -> Result<()> {
        if let Some(project) = self.projects.get(self.selected_index) {
            if let Some(target_dir) = &project.target_dir {
                let size_before = project.total_cleanable_size();
                
                // Confirm deletion for large or active projects
                if !self.cli.delete_all && (project.is_likely_active() || size_before > 1024 * 1024 * 500) {
                    // For now, skip confirmation in interactive mode
                    // In a real implementation, you'd show a confirmation dialog
                }
                
                utils::remove_directory(target_dir, self.cli.dry_run)?;
                
                if !self.cli.dry_run {
                    self.total_deleted_size += size_before;
                    self.deleted_count += 1;
                    
                    // Update the project in our list
                    if let Some(project_mut) = self.projects.get_mut(self.selected_index) {
                        project_mut.target_dir = None;
                        project_mut.target_size = 0;
                        project_mut.build_artifacts.clear();
                    }
                }
            }
        }
        Ok(())
    }

    async fn delete_all_projects(&mut self) -> Result<()> {
        let mut total_deleted = 0u64;
        let mut count_deleted = 0;
        
        for project in &mut self.projects {
            if let Some(target_dir) = &project.target_dir {
                let size_before = project.target_size;
                
                utils::remove_directory(target_dir, self.cli.dry_run)?;
                
                if !self.cli.dry_run {
                    total_deleted += size_before;
                    count_deleted += 1;
                    
                    project.target_dir = None;
                    project.target_size = 0;
                    project.build_artifacts.clear();
                }
            }
        }
        
        self.total_deleted_size += total_deleted;
        self.deleted_count += count_deleted;
        
        Ok(())
    }

    fn open_selected_project(&self) -> Result<()> {
        if let Some(project) = self.projects.get(self.selected_index) {
            // Try to open the project directory
            let path = &project.path;
            
            #[cfg(target_os = "macos")]
            {
                process::Command::new("open").arg(path).spawn()?;
            }
            
            #[cfg(target_os = "linux")]
            {
                process::Command::new("xdg-open").arg(path).spawn()?;
            }
            
            #[cfg(target_os = "windows")]
            {
                process::Command::new("explorer").arg(path).spawn()?;
            }
        }
        Ok(())
    }

    async fn refresh_projects(&mut self) -> Result<()> {
        let scanner = ProjectScanner::new(self.cli.clone(), self.config.clone());
        self.projects = scanner.scan().await?;
        self.selected_index = 0;
        Ok(())
    }

    fn get_highlight_color(&self) -> &'static str {
        match self.cli.get_color() {
            Color::Blue => "blue",
            Color::Cyan => "cyan", 
            Color::Magenta => "magenta",
            Color::White => "white",
            Color::Red => "red",
            Color::Yellow => "yellow",
        }
    }
}

enum ControlFlow {
    Continue,
    Exit,
}