Skip to content

Commit e34ecb8

Browse files
feat: add multi visualization (#20)
1 parent 7fc203b commit e34ecb8

4 files changed

Lines changed: 195 additions & 57 deletions

File tree

docs/main.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ If FILE is not provided, see reads from standard input.
2222
| `--render-links` | Enable or disable clickable links |
2323
| `--render-table-borders` | Enable or disable table borders in rendered output |
2424
| `--show-line-numbers` | Show or hide line numbers when rendering code files |
25+
| `--show-filename` | Show or hide the filename before rendering content |
2526
| `--config <file>` | Specify a custom configuration file |
2627
| `--use-color` | Control color output |
2728

@@ -78,5 +79,11 @@ see --render-table-borders=true path/to/your/markdown_file.md
7879
Disable color output when piping to another command:
7980

8081
```bash
81-
see --use-color=false path/to/your/markdown_file.rs | echo
82+
see --use-color=false path/to/your/markdown_file.rs | less
83+
```
84+
85+
Render content without showing the filename:
86+
87+
```bash
88+
see --show-filename=false path/to/your/markdown_file.md
8289
```

src/config.rs

Lines changed: 32 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ pub struct AppConfig {
2020
pub render_links: bool,
2121
pub render_table_borders: bool,
2222
pub show_line_numbers: bool,
23+
pub show_filename: bool,
2324
pub debug_mode: bool,
2425
pub use_colors: bool,
2526
}
@@ -55,6 +56,7 @@ impl AppConfig {
5556
render_links: true,
5657
render_table_borders: false,
5758
show_line_numbers: true,
59+
show_filename: false,
5860
debug_mode: false,
5961
use_colors: true,
6062
}
@@ -82,8 +84,8 @@ pub fn get_config() -> &'static AppConfig {
8284
CONFIG.get().expect("Config not initialized")
8385
}
8486

85-
pub fn initialize_app() -> io::Result<(AppConfig, Option<PathBuf>)> {
86-
let (mut config, file_path) = parse_cli_args()?;
87+
pub fn initialize_app() -> io::Result<(AppConfig, Option<Vec<PathBuf>>)> {
88+
let (mut config, file_paths) = parse_cli_args()?;
8789

8890
if !std::io::stdout().is_terminal() {
8991
config.use_colors = false;
@@ -98,28 +100,13 @@ pub fn initialize_app() -> io::Result<(AppConfig, Option<PathBuf>)> {
98100
.set(config.clone())
99101
.map_err(|_| io::Error::new(io::ErrorKind::AlreadyExists, "Config already initialized"))?;
100102

101-
Ok((config, file_path))
103+
Ok((config, file_paths))
102104
}
103105

104-
fn parse_bool(value: Option<&str>) -> bool {
105-
match value {
106-
Some(v) => match v.to_lowercase().as_str() {
107-
"true" | "1" => true,
108-
"false" | "0" => false,
109-
_ => true, // Default to true if the value is not recognized
110-
},
111-
None => true, // Default to true if no value is provided
112-
}
113-
}
114-
115-
fn parse_u32(value: Option<&str>) -> Option<u32> {
116-
value.and_then(|v| v.parse().ok())
117-
}
118-
119-
fn parse_cli_args() -> io::Result<(AppConfig, Option<PathBuf>)> {
106+
fn parse_cli_args() -> io::Result<(AppConfig, Option<Vec<PathBuf>>)> {
120107
let args: Vec<String> = env::args().collect();
121108
let mut config = AppConfig::default();
122-
let mut file_path = None;
109+
let mut file_paths = Vec::new();
123110
let mut i = 1;
124111

125112
while i < args.len() {
@@ -132,12 +119,13 @@ fn parse_cli_args() -> io::Result<(AppConfig, Option<PathBuf>)> {
132119
"max-image-height" => config.max_image_height = parse_u32(parts.get(1).map(|s| *s)),
133120
"render-images" => config.render_images = parse_bool(parts.get(1).map(|s| *s)),
134121
"render-links" => config.render_links = parse_bool(parts.get(1).map(|s| *s)),
135-
"render-table_borders" => {
122+
"render-table-borders" => {
136123
config.render_table_borders = parse_bool(parts.get(1).map(|s| *s))
137124
}
138125
"show-line-numbers" => {
139126
config.show_line_numbers = parse_bool(parts.get(1).map(|s| *s))
140127
}
128+
"show-filename" => config.show_filename = parse_bool(parts.get(1).map(|s| *s)),
141129
"use-colors" => config.use_colors = parse_bool(parts.get(1).map(|s| *s)),
142130
"config" => {
143131
if let Some(path) = parts.get(1) {
@@ -171,12 +159,33 @@ fn parse_cli_args() -> io::Result<(AppConfig, Option<PathBuf>)> {
171159
}
172160
}
173161
} else {
174-
file_path = Some(PathBuf::from(arg));
162+
file_paths.push(PathBuf::from(arg));
175163
}
176164
i += 1;
177165
}
178166

179-
Ok((config, file_path))
167+
let file_paths = if file_paths.is_empty() {
168+
None
169+
} else {
170+
Some(file_paths)
171+
};
172+
173+
Ok((config, file_paths))
174+
}
175+
176+
fn parse_bool(value: Option<&str>) -> bool {
177+
match value {
178+
Some(v) => match v.to_lowercase().as_str() {
179+
"true" | "1" => true,
180+
"false" | "0" => false,
181+
_ => true, // Default to true if the value is not recognized
182+
},
183+
None => true, // Default to true if no value is provided
184+
}
185+
}
186+
187+
fn parse_u32(value: Option<&str>) -> Option<u32> {
188+
value.and_then(|v| v.parse().ok())
180189
}
181190

182191
fn render_help() -> io::Result<()> {

src/main.rs

Lines changed: 32 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
use crate::config::initialize_app;
2-
use crate::render::{render_code_file, render_image_file, render_markdown};
3-
use crate::utils::detect_language;
2+
use crate::viewers::{determine_viewer, ViewerManager};
43
use std::path::Path;
54

65
mod app;
@@ -9,48 +8,48 @@ mod constants;
98
mod directory_tree;
109
mod render;
1110
mod utils;
11+
mod viewers;
1212

1313
fn main() -> std::io::Result<()> {
14-
let (config, file_path) = initialize_app()?;
15-
14+
let (config, file_paths) = initialize_app()?;
1615
if config.debug_mode {
1716
eprintln!("Debug mode enabled");
1817
eprintln!("Configuration: {:?}", config);
1918
}
2019

21-
if let Some(path) = file_path {
22-
let path = Path::new(&path);
23-
24-
if path.is_dir() {
25-
// Handle directory
26-
directory_tree::handle_directory(path)?;
27-
} else {
28-
let extension = path
29-
.extension()
30-
.and_then(std::ffi::OsStr::to_str)
31-
.unwrap_or("");
20+
let viewer_manager = ViewerManager::new();
3221

33-
match extension.to_lowercase().as_str() {
34-
"md" => {
35-
let content = app::read_content(Some(path.to_str().unwrap().to_string()))?;
36-
let json = app::parse_and_process_markdown(&content)?;
37-
render_markdown(&json)?;
38-
}
39-
"jpg" | "jpeg" | "png" | "gif" | "bmp" => {
40-
render_image_file(path.to_str().unwrap())?;
41-
}
42-
_ => {
43-
let content = app::read_content(Some(path.to_str().unwrap().to_string()))?;
44-
let language = detect_language(path.to_str().unwrap());
45-
render_code_file(&content, &language)?;
22+
match file_paths {
23+
Some(paths) => {
24+
for path in paths {
25+
let path = Path::new(&path);
26+
if path.is_dir() {
27+
directory_tree::handle_directory(path)?;
28+
} else {
29+
let viewer = determine_viewer(path);
30+
if viewer.contains(&"image".to_string()) {
31+
viewer_manager.visualize(&viewer, "", path.to_str())?;
32+
} else {
33+
match app::read_content(Some(path.to_str().unwrap().to_string())) {
34+
Ok(content) => {
35+
viewer_manager.visualize(&viewer, &content, path.to_str())?;
36+
}
37+
Err(e) => {
38+
eprintln!("Error reading file {}: {}", path.display(), e);
39+
}
40+
}
41+
}
4642
}
4743
}
4844
}
49-
} else {
50-
// Handle stdin input (assuming it's always Markdown)
51-
let content = app::read_content(None)?;
52-
let json = app::parse_and_process_markdown(&content)?;
53-
render_markdown(&json)?;
45+
None => {
46+
let content = app::read_content(None)?;
47+
viewer_manager.visualize(
48+
&["markdown".to_string(), "code".to_string()],
49+
&content,
50+
None,
51+
)?;
52+
}
5453
}
5554

5655
Ok(())

src/viewers.rs

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
use crate::app;
2+
use crate::config::get_config;
3+
use crate::render::{render_code_file, render_image_file, render_markdown};
4+
use crate::utils::detect_language;
5+
use devicons::{icon_for_file, File, Theme};
6+
use std::collections::HashMap;
7+
use std::io::{self, Write};
8+
use std::path::Path;
9+
use termcolor::{Color, ColorChoice, ColorSpec, StandardStream, WriteColor};
10+
11+
pub struct ViewerManager {
12+
viewers: HashMap<String, Box<dyn Viewer>>,
13+
}
14+
15+
impl ViewerManager {
16+
pub fn new() -> Self {
17+
let mut viewer_manager = ViewerManager {
18+
viewers: HashMap::new(),
19+
};
20+
// Register default viewers
21+
viewer_manager.register_viewer("markdown", Box::new(MarkdownViewer));
22+
viewer_manager.register_viewer("code", Box::new(CodeViewer));
23+
viewer_manager.register_viewer("image", Box::new(ImageViewer));
24+
viewer_manager
25+
}
26+
27+
pub fn register_viewer(&mut self, name: &str, viewer: Box<dyn Viewer>) {
28+
self.viewers.insert(name.to_string(), viewer);
29+
}
30+
31+
pub fn visualize(
32+
&self,
33+
viewer_names: &[String],
34+
content: &str,
35+
file_path: Option<&str>,
36+
) -> io::Result<()> {
37+
let mut stdout = StandardStream::stdout(ColorChoice::Always);
38+
let config = get_config();
39+
40+
// Display file name if available and show_filename is true
41+
if config.show_filename {
42+
if let Some(path) = file_path {
43+
let file_name = Path::new(path)
44+
.file_name()
45+
.unwrap_or_default()
46+
.to_string_lossy();
47+
48+
let file = File::new(Path::new(path));
49+
let icon = icon_for_file(&file, Some(Theme::Dark));
50+
stdout.set_color(ColorSpec::new().set_fg(Some(Color::Blue)).set_bold(true))?;
51+
writeln!(stdout)?;
52+
stdout.set_color(ColorSpec::new().set_fg(Some(Color::Cyan)))?;
53+
writeln!(stdout, "{} {}", icon.icon, file_name)?;
54+
stdout.reset()?;
55+
writeln!(stdout)?;
56+
}
57+
}
58+
59+
for (index, viewer_name) in viewer_names.iter().enumerate() {
60+
if index > 0 {
61+
writeln!(stdout)?;
62+
}
63+
if let Some(viewer) = self.viewers.get(viewer_name) {
64+
viewer.visualize(content, file_path)?;
65+
} else {
66+
eprintln!("Unknown viewer: {}", viewer_name);
67+
}
68+
}
69+
Ok(())
70+
}
71+
}
72+
73+
pub trait Viewer {
74+
fn visualize(&self, content: &str, file_path: Option<&str>) -> io::Result<()>;
75+
}
76+
77+
struct MarkdownViewer;
78+
79+
impl Viewer for MarkdownViewer {
80+
fn visualize(&self, content: &str, _file_path: Option<&str>) -> io::Result<()> {
81+
let json = app::parse_and_process_markdown(content)?;
82+
render_markdown(&json)
83+
}
84+
}
85+
86+
struct CodeViewer;
87+
88+
impl Viewer for CodeViewer {
89+
fn visualize(&self, content: &str, file_path: Option<&str>) -> io::Result<()> {
90+
let language = file_path
91+
.map(|path| detect_language(path))
92+
.unwrap_or_else(|| "txt".to_string());
93+
render_code_file(content, &language)
94+
}
95+
}
96+
97+
struct ImageViewer;
98+
99+
impl Viewer for ImageViewer {
100+
fn visualize(&self, _content: &str, file_path: Option<&str>) -> io::Result<()> {
101+
if let Some(path) = file_path {
102+
render_image_file(path)
103+
} else {
104+
Err(io::Error::new(
105+
io::ErrorKind::InvalidInput,
106+
"No file path provided for image rendering",
107+
))
108+
}
109+
}
110+
}
111+
112+
pub fn determine_viewer(file_path: &Path) -> Vec<String> {
113+
let extension = file_path
114+
.extension()
115+
.and_then(std::ffi::OsStr::to_str)
116+
.unwrap_or("")
117+
.to_lowercase();
118+
match extension.as_str() {
119+
"md" => vec!["markdown".to_string()],
120+
"jpg" | "jpeg" | "png" | "gif" | "bmp" | "webp" => vec!["image".to_string()],
121+
_ => vec!["code".to_string()],
122+
}
123+
}

0 commit comments

Comments
 (0)