pub mod formatter;
pub mod formats;
pub mod streaming;
use crate::cli::{OutputFormat as CliOutputFormat, ColorChoice};
use crate::processor::SearchMatch;
use std::path::Path;
pub use formatter::OutputFormatter;
pub use formats::*;
pub struct OutputManager {
formatters: std::collections::HashMap<String, Box<dyn OutputFormatterTrait>>,
}
pub trait OutputFormatterTrait: Send + Sync {
fn format(&self, matches: &[SearchMatch], query: &str, path: &Path) -> String;
fn format_with_timing(&self, matches: &[SearchMatch], query: &str, path: &Path, execution_time_ms: f64) -> String {
self.format(matches, query, path)
}
fn name(&self) -> &str;
fn supports_streaming(&self) -> bool { false }
}
impl OutputManager {
pub fn new() -> crate::error::Result<Self> {
let mut manager = Self {
formatters: std::collections::HashMap::new(),
};
manager.register_formatter("text", Box::new(TextFormatter::new()));
manager.register_formatter("json", Box::new(JsonFormatter::new()));
manager.register_formatter("xml", Box::new(XmlFormatter::new()));
manager.register_formatter("html", Box::new(HtmlFormatter::new()));
manager.register_formatter("markdown", Box::new(MarkdownFormatter::new()));
manager.register_formatter("csv", Box::new(CsvFormatter::new()));
manager.register_formatter("yaml", Box::new(YamlFormatter::new()));
manager.register_formatter("junit", Box::new(JunitFormatter::new()));
Ok(manager)
}
pub fn register_formatter(&mut self, name: &str, formatter: Box<dyn OutputFormatterTrait>) {
self.formatters.insert(name.to_string(), formatter);
}
pub fn format_results(
&self,
matches: &[SearchMatch],
query: &str,
path: &Path,
format: CliOutputFormat,
ndjson: bool,
color: ColorChoice,
) -> crate::error::Result<String> {
self.format_results_with_timing(matches, query, path, format, ndjson, color, 0.0)
}
pub fn format_results_with_timing(
&self,
matches: &[SearchMatch],
query: &str,
path: &Path,
format: CliOutputFormat,
ndjson: bool,
color: ColorChoice,
execution_time_ms: f64,
) -> crate::error::Result<String> {
let formatter_name = match format {
CliOutputFormat::Text => "text",
CliOutputFormat::Json => "json",
CliOutputFormat::Xml => "xml",
CliOutputFormat::Html => "html",
CliOutputFormat::Markdown => "markdown",
};
let formatter = self.formatters.get(formatter_name)
.ok_or_else(|| crate::error::RfgrepError::Other(format!("Unknown format: {formatter_name}")))?;
let mut output = if execution_time_ms > 0.0 {
formatter.format_with_timing(matches, query, path, execution_time_ms)
} else {
formatter.format(matches, query, path)
};
if matches!(color, ColorChoice::Always) ||
(matches!(color, ColorChoice::Auto) && is_terminal::is_terminal(&std::io::stdout())) {
output = self.apply_color(&output, query);
}
Ok(output)
}
pub fn create_formatter(&self, format: OutputFormat, ndjson: bool) -> OutputFormatter {
OutputFormatter::new(format, ndjson)
}
pub fn copy_to_clipboard(&self, content: &str) -> crate::error::Result<()> {
let can_use_clipboard = std::env::var("DISPLAY").is_ok() ||
std::env::var("WAYLAND_DISPLAY").is_ok();
if can_use_clipboard {
match arboard::Clipboard::new() {
Ok(mut clipboard) => {
clipboard.set_text(content.to_string())
.map_err(|e| crate::error::RfgrepError::Other(format!("Clipboard error: {e}")))?;
println!("\n{}", "Results copied to clipboard!".green());
}
Err(e) => {
self.fallback_to_file(content)?;
eprintln!("Clipboard init failed: {e}");
}
}
} else {
self.fallback_to_file(content)?;
}
Ok(())
}
fn fallback_to_file(&self, content: &str) -> crate::error::Result<()> {
let tmp = std::env::temp_dir().join("rfgrep_results.txt");
std::fs::write(&tmp, content)
.map_err(|e| crate::error::RfgrepError::Io(e))?;
println!("\n{} {}", "Results written to".green(), tmp.display());
Ok(())
}
fn apply_color(&self, output: &str, query: &str) -> String {
use colored::*;
output
.lines()
.map(|line| {
if line.contains(query) {
line.replace(query, &query.yellow().bold().to_string())
} else {
line.to_string()
}
})
.collect::<Vec<_>>()
.join("\n")
}
pub fn get_available_formatters(&self) -> Vec<&str> {
self.formatters.keys().map(|k| k.as_str()).collect()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum OutputFormat {
Text,
Json,
Xml,
Html,
Markdown,
Csv,
Yaml,
Junit,
}
impl OutputFormat {
pub fn from_str(s: &str) -> Option<Self> {
match s.to_lowercase().as_str() {
"text" | "txt" => Some(Self::Text),
"json" => Some(Self::Json),
"xml" => Some(Self::Xml),
"html" => Some(Self::Html),
"markdown" | "md" => Some(Self::Markdown),
"csv" => Some(Self::Csv),
"yaml" | "yml" => Some(Self::Yaml),
"junit" | "junit-xml" => Some(Self::Junit),
_ => None,
}
}
pub fn as_str(&self) -> &'static str {
match self {
Self::Text => "text",
Self::Json => "json",
Self::Xml => "xml",
Self::Html => "html",
Self::Markdown => "markdown",
Self::Csv => "csv",
Self::Yaml => "yaml",
Self::Junit => "junit",
}
}
}
impl Default for OutputFormat {
fn default() -> Self {
Self::Text
}
}