papr-pdf 0.1.0

A fast, keyboard-first CLI tool to compile Markdown, LaTeX, and plain text files into publication-grade PDFs.
//! Interactive terminal wizard for `papr`.
//!
//! Provides interactive prompts for choosing the document format and selecting
//! input files with Tab auto-completion.

use crate::cli::DocumentFormat;
use anyhow::{Context, Result};
use colored::*;
use inquire::{Autocomplete, CustomUserError, Select, Text};
use std::fs;
use std::path::{Path, PathBuf};

/// Autocompleter for file and directory paths with extension filtering.
#[derive(Clone, Default)]
pub struct FilePathCompleter {
    /// Allowed file extensions (e.g. `["md", "markdown"]`). If empty, all extensions match.
    pub allowed_extensions: Vec<String>,
}

impl FilePathCompleter {
    /// Creates a new completer restricted to the provided list of extensions.
    pub fn new(allowed_extensions: &[&str]) -> Self {
        Self {
            allowed_extensions: allowed_extensions.iter().map(|s| s.to_string()).collect(),
        }
    }
}

impl Autocomplete for FilePathCompleter {
    fn get_suggestions(&mut self, input: &str) -> Result<Vec<String>, CustomUserError> {
        let path = Path::new(input);
        let (dir_to_scan, prefix) = if input.is_empty() {
            (Path::new("."), "")
        } else if input.ends_with('/') || input.ends_with('\\') {
            (path, "")
        } else {
            let parent = path.parent().unwrap_or_else(|| Path::new("."));
            let parent = if parent.as_os_str().is_empty() {
                Path::new(".")
            } else {
                parent
            };
            let file_prefix = path
                .file_name()
                .and_then(|n| n.to_str())
                .unwrap_or("");
            (parent, file_prefix)
        };

        let mut suggestions = Vec::new();
        let read_dir = match fs::read_dir(dir_to_scan) {
            Ok(entries) => entries,
            Err(_) => return Ok(suggestions),
        };

        for entry in read_dir.flatten() {
            let name_os = entry.file_name();
            let name = match name_os.to_str() {
                Some(n) => n,
                None => continue,
            };

            // Ignore hidden files unless user explicitly typed a dot
            if name.starts_with('.') && !prefix.starts_with('.') {
                continue;
            }

            if name.to_lowercase().starts_with(&prefix.to_lowercase()) {
                let is_dir = entry.file_type().map(|t| t.is_dir()).unwrap_or(false);
                let full_path = entry.path();
                let full_str = full_path.to_string_lossy().to_string();

                if is_dir {
                    suggestions.push(format!("{}/", full_str));
                } else if self.allowed_extensions.is_empty() {
                    suggestions.push(full_str);
                } else {
                    let ext = full_path
                        .extension()
                        .and_then(|e| e.to_str())
                        .map(|e| e.to_lowercase())
                        .unwrap_or_default();
                    if self.allowed_extensions.iter().any(|allowed| allowed.eq_ignore_ascii_case(&ext)) {
                        suggestions.push(full_str);
                    }
                }
            }
        }

        suggestions.sort();
        Ok(suggestions)
    }

    fn get_completion(
        &mut self,
        _input: &str,
        highlighted_suggestion: Option<String>,
    ) -> Result<inquire::autocompletion::Replacement, CustomUserError> {
        Ok(highlighted_suggestion)
    }
}

/// Result gathered from running the interactive wizard.
pub struct WizardResult {
    /// Selected document format.
    pub format: DocumentFormat,
    /// Absolute or relative path to the verified input file.
    pub input_path: PathBuf,
}

/// Runs the interactive terminal wizard to select document type and input file.
pub fn run_wizard() -> Result<WizardResult> {
    println!("{}", "==================================================".bright_cyan());
    println!("{}", "               papr ยท Document to PDF             ".bold().bright_white());
    println!("{}", "==================================================".bright_cyan());
    println!();

    let options = vec![
        "1. Markdown (.md)",
        "2. LaTeX (.tex)",
        "3. Plain Text (.txt)",
    ];

    let choice = Select::new("Select document format to compile:", options)
        .prompt()
        .context("Failed to get format selection")?;

    let (format, default_ext, filter_exts) = match choice {
        s if s.starts_with("1") => (DocumentFormat::Markdown, "md", vec!["md", "markdown"]),
        s if s.starts_with("2") => (DocumentFormat::Latex, "tex", vec!["tex", "latex"]),
        _ => (DocumentFormat::PlainText, "txt", vec!["txt", "text", "log"]),
    };

    println!();
    println!("{}", format!("Selected: {:?} (.{})", format, default_ext).dimmed());

    let completer = FilePathCompleter::new(&filter_exts);

    let prompt_msg = format!("Enter path to input file (press [Tab] to autocomplete):");

    let input_str = Text::new(&prompt_msg)
        .with_autocomplete(completer)
        .with_help_message("e.g. document.md, chapters/main.tex, notes.txt")
        .prompt()
        .context("Failed to get file path")?;

    let path = PathBuf::from(input_str.trim());
    if !path.exists() {
        anyhow::bail!("File not found: {}", path.display());
    }

    Ok(WizardResult {
        format,
        input_path: path,
    })
}