rich-prompt 0.2.0

A Rust CLI tool for rich prompts with file and directory selection.
use crate::domain::models::ContextOutput;
use clipboard::{ClipboardContext, ClipboardProvider};
use dialoguer::console::style;
use log::{debug, info, warn};
use std::fs;
use std::io::{self, Write};
use std::path::Path;

pub trait OutputWriter {
    fn write(&self, content: &str) -> anyhow::Result<()>;
}

pub struct FileWriter {
    path: String,
}

impl FileWriter {
    pub fn new(path: String) -> Self {
        Self { path }
    }
}

impl OutputWriter for FileWriter {
    fn write(&self, content: &str) -> anyhow::Result<()> {
        debug!("Writing output to file: {}", self.path);
        fs::write(Path::new(&self.path), content)?;
        info!("Output written to file: {}", self.path);
        Ok(())
    }
}

pub struct ConsoleWriter;

impl OutputWriter for ConsoleWriter {
    fn write(&self, content: &str) -> anyhow::Result<()> {
        debug!("Writing output to console");
        io::stdout().write_all(content.as_bytes())?;
        io::stdout().write_all(b"\n")?;
        Ok(())
    }
}

pub struct ClipboardWriter;

impl OutputWriter for ClipboardWriter {
    fn write(&self, content: &str) -> anyhow::Result<()> {
        debug!("Writing output to clipboard");

        let mut ctx: ClipboardContext = match ClipboardProvider::new() {
            Ok(ctx) => ctx,
            Err(e) => {
                warn!("Failed to access clipboard: {}", e);
                return Err(anyhow::anyhow!("Failed to access clipboard: {}", e));
            }
        };

        match ctx.set_contents(content.to_owned()) {
            Ok(_) => {
                info!("Output copied to clipboard (size: {} bytes)", content.len());
                Ok(())
            }
            Err(e) => {
                warn!("Failed to copy to clipboard: {}", e);
                Err(anyhow::anyhow!("Failed to copy to clipboard: {}", e))
            }
        }
    }
}

pub fn create_writer(
    output_path: &Option<String>,
    clipboard_output: bool,
) -> Box<dyn OutputWriter> {
    if clipboard_output {
        return Box::new(ClipboardWriter) as Box<dyn OutputWriter>;
    }

    match output_path {
        Some(path) => Box::new(FileWriter::new(path.clone())) as Box<dyn OutputWriter>,
        None => Box::new(ConsoleWriter) as Box<dyn OutputWriter>,
    }
}

pub fn write_output(
    output: &ContextOutput,
    formatted_content: &str,
    output_path: Option<String>,
    clipboard_output: bool,
) -> anyhow::Result<()> {
    println!(
        "{} {}",
        style("⚙️ Total tokens in output:").bold().blue(),
        output.token_count
    );

    let writer = create_writer(&output_path, clipboard_output);
    if let Err(e) = writer.write(formatted_content) {
        return Err(e);
    }

    if clipboard_output && output_path.is_none() {
        println!(
            "{}",
            style("📋 Content copied to clipboard!").bold().green()
        );
        println!("{}", style("Preview of copied content:").italic());

        let preview_length = 200;
        let preview = if formatted_content.chars().count() > preview_length {
            let safe_substring: String = formatted_content.chars().take(preview_length).collect();
            format!("{}...", safe_substring)
        } else {
            formatted_content.to_string()
        };

        println!("{}", preview);
    }

    Ok(())
}

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

    #[test]
    fn test_file_writer() {
        let temp_file = NamedTempFile::new().unwrap();
        let path = temp_file.path().to_string_lossy().to_string();
        let writer = FileWriter::new(path.clone());
        let content = "Test output";

        writer.write(content).unwrap();

        let read_content = fs::read_to_string(path).unwrap();
        assert_eq!(read_content, content);
    }

    #[test]
    fn test_create_writer() {
        let file_writer = create_writer(&Some("test.txt".to_string()), false);
        assert_eq!(
            std::any::type_name_of_val(&*file_writer),
            "dyn rich_prompt::infra::output::OutputWriter"
        );

        let console_writer = create_writer(&None, false);
        assert_eq!(
            std::any::type_name_of_val(&*console_writer),
            "dyn rich_prompt::infra::output::OutputWriter"
        );

        let clipboard_writer = create_writer(&None, true);
        assert_eq!(
            std::any::type_name_of_val(&*clipboard_writer),
            "dyn rich_prompt::infra::output::OutputWriter"
        );
    }

    #[test]
    fn test_utf8_safe_preview() {
        let content =
            "اهلا مرحب عبدالله 🚀 This string has UTF-8 characters like: ├── ./src/file.rs";

        let preview_length = 20;
        let preview: String = content.chars().take(preview_length).collect();

        assert_eq!(preview.chars().count(), preview_length);
    }
}

#[cfg(test)]
mod clipboard_tests {
    use tempfile::NamedTempFile;

    use super::*;
    use crate::domain::models::ContextOutput;

    #[test]
    #[ignore]
    fn test_clipboard_writer() {
        if std::env::var("ENABLE_CLIPBOARD_TESTS").is_err() {
            return;
        }

        let writer = ClipboardWriter;
        let test_content = "Test clipboard content";

        match writer.write(test_content) {
            Ok(_) => {
                println!("Clipboard write test passed");
            }
            Err(e) => {
                panic!("Failed to write to clipboard: {}", e);
            }
        }
    }

    #[test]
    fn test_write_output_with_clipboard() {
        let output = ContextOutput {
            file_map: "test_dir\n".to_string(),
            file_contents: "test_content\n".to_string(),
            user_instructions: "test_prompt".to_string(),
            token_count: 3,
        };

        let formatted_content = "<file_map>\ntest_dir\n</file_map>\n\n<file_contents>test_content\n</file_contents>\n\n<user_instructions>\ntest_prompt\n</user_instructions>";

        let temp_file = NamedTempFile::new().unwrap();
        let path = temp_file.path().to_string_lossy().to_string();

        let result = write_output(&output, formatted_content, Some(path), true);
        assert!(result.is_ok());

        let result = write_output(&output, formatted_content, None, true);
        assert!(result.is_ok());
    }
}