use crate::app;
use crate::config::get_config;
use crate::render::{render_code_file, render_image_file, render_markdown};
use crate::utils::detect_language;
use base64::{engine::general_purpose, Engine as _};
use devicons::{icon_for_file, File, Theme};
use std::collections::HashMap;
use std::io::{self, Write};
use std::path::Path;
use termcolor::{Color, ColorChoice, ColorSpec, StandardStream, WriteColor};
pub struct ViewerManager {
viewers: HashMap<String, Box<dyn Viewer>>,
}
impl ViewerManager {
pub fn new() -> Self {
let mut viewer_manager = ViewerManager {
viewers: HashMap::new(),
};
viewer_manager.register_viewer("markdown", Box::new(MarkdownViewer));
viewer_manager.register_viewer("code", Box::new(CodeViewer));
viewer_manager.register_viewer("image", Box::new(ImageViewer));
viewer_manager
}
pub fn register_viewer(&mut self, name: &str, viewer: Box<dyn Viewer>) {
self.viewers.insert(name.to_string(), viewer);
}
pub fn visualize(
&self,
viewer_names: &[String],
content: &str,
file_path: Option<&str>,
) -> io::Result<()> {
let mut stdout = StandardStream::stdout(ColorChoice::Always);
let config = get_config();
if config.show_filename {
if let Some(path) = file_path {
let file_name = Path::new(path)
.file_name()
.unwrap_or_default()
.to_string_lossy();
let file = File::new(Path::new(path));
let icon = icon_for_file(&file, Some(Theme::Dark));
stdout.set_color(ColorSpec::new().set_fg(Some(Color::Blue)).set_bold(true))?;
writeln!(stdout)?;
stdout.set_color(ColorSpec::new().set_fg(Some(Color::Cyan)))?;
writeln!(stdout, "{} {}", icon.icon, file_name)?;
stdout.reset()?;
writeln!(stdout)?;
}
}
for (index, viewer_name) in viewer_names.iter().enumerate() {
if index > 0 {
writeln!(stdout)?;
}
if let Some(viewer) = self.viewers.get(viewer_name) {
viewer.visualize(content, file_path)?;
} else {
eprintln!("Unknown viewer: {}", viewer_name);
}
}
Ok(())
}
}
pub trait Viewer {
fn visualize(&self, content: &str, file_path: Option<&str>) -> io::Result<()>;
}
struct MarkdownViewer;
impl Viewer for MarkdownViewer {
fn visualize(&self, content: &str, _file_path: Option<&str>) -> io::Result<()> {
let json = app::parse_and_process_markdown(content)?;
render_markdown(&json)
}
}
struct CodeViewer;
impl Viewer for CodeViewer {
fn visualize(&self, content: &str, file_path: Option<&str>) -> io::Result<()> {
let language = file_path
.map(|path| detect_language(path))
.unwrap_or_else(|| "txt".to_string());
render_code_file(content, &language)
}
}
struct ImageViewer;
impl Viewer for ImageViewer {
fn visualize(&self, content: &str, file_path: Option<&str>) -> io::Result<()> {
if let Some(path) = file_path {
render_image_file(path)
} else if content.starts_with("data:image") {
let parts: Vec<&str> = content.split(',').collect();
if parts.len() == 2 {
let b64_data = parts[1];
let decoded = general_purpose::STANDARD
.decode(b64_data)
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
let mut temp_file = tempfile::NamedTempFile::new()?;
temp_file.write_all(&decoded)?;
let temp_path = temp_file.into_temp_path();
render_image_file(temp_path.to_str().unwrap())
} else {
Err(io::Error::new(
io::ErrorKind::InvalidData,
"Invalid base64 image data",
))
}
} else {
Err(io::Error::new(
io::ErrorKind::InvalidInput,
"No file path or valid base64 image data provided for image rendering",
))
}
}
}
pub fn determine_viewer(file_path: &Path) -> Vec<String> {
let extension = file_path
.extension()
.and_then(std::ffi::OsStr::to_str)
.unwrap_or("")
.to_lowercase();
match extension.as_str() {
"md" => vec!["markdown".to_string()],
"jpg" | "jpeg" | "png" | "gif" | "bmp" | "webp" => vec!["image".to_string()],
_ => vec!["code".to_string()],
}
}