From 01aec09c858fbb0446d6241520b4eb5777fd7144 Mon Sep 17 00:00:00 2001 From: Guilherme Prokisch Date: Wed, 4 Sep 2024 00:01:07 +0200 Subject: [PATCH] feat: add config file --- Cargo.lock | 50 +++++++++++++++ Cargo.toml | 4 +- README.md | 35 +++++++++++ docs/main.md | 46 +++++++++++--- src/main.rs | 171 +++++++++++++++++++++++++++++++++++++++++++-------- 5 files changed, 268 insertions(+), 38 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f76231c..3d8e0f1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -411,6 +411,27 @@ dependencies = [ "crypto-common", ] +[[package]] +name = "dirs" +version = "5.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys 0.48.0", +] + [[package]] name = "either" version = "1.13.0" @@ -978,6 +999,16 @@ dependencies = [ "once_cell", ] +[[package]] +name = "libredox" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" +dependencies = [ + "bitflags 2.6.0", + "libc", +] + [[package]] name = "linked-hash-map" version = "0.5.6" @@ -1282,6 +1313,12 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + [[package]] name = "parking_lot" version = "0.12.3" @@ -1583,6 +1620,17 @@ dependencies = [ "bitflags 2.6.0", ] +[[package]] +name = "redox_users" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" +dependencies = [ + "getrandom", + "libredox", + "thiserror", +] + [[package]] name = "regex" version = "1.10.6" @@ -1940,6 +1988,7 @@ name = "smd" version = "0.2.8" dependencies = [ "crossterm 0.28.1", + "dirs", "emojis", "image 0.25.2", "include_dir", @@ -1956,6 +2005,7 @@ dependencies = [ "termcolor", "terminal_size", "textwrap", + "toml", "url", "viuer", ] diff --git a/Cargo.toml b/Cargo.toml index 9ee4460..dd031dc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -38,6 +38,8 @@ tempfile = "3.12.0" lazy_static = "1.5.0" include_dir = "0.7.4" openssl-sys = { version = "0.9", features = ["vendored"] } +toml = "0.8.19" +dirs = "5.0.1" # The profile that 'cargo dist' will build with @@ -45,8 +47,6 @@ openssl-sys = { version = "0.9", features = ["vendored"] } inherits = "release" lto = "thin" -[dist] -allow-dirty = ["ci", "msi"] # Config for 'cargo dist' [workspace.metadata.dist] diff --git a/README.md b/README.md index 3c030f3..2d02029 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,7 @@ As the project evolved, support for more complex Markdown features was added. Th - Nested list support - Blockquote styling - And more adding soon! + ## Installation There are several ways to install smd: @@ -143,6 +144,40 @@ smd --help This command will render smd's main documentation file `/docs`, giving you a practical example of smd in action and providing detailed information about its usage and features. +# smd (Simple Markdown Viewer) + +... + +## Configuration + +smd supports user-defined configuration files. You can customize various aspects of the rendering process by creating a `config.toml` file in the following location: + +- On Linux and macOS: `~/.config/smd/config.toml` +- On Windows: `C:\Users\\AppData\Roaming\smd\config.toml` + +You can generate a default configuration file by running: + +```bash +smd --generate-config +``` + +Here's an example of what you can configure: + +```toml +theme = "default" +code_highlight_theme = "Solarized (dark)" +max_image_width = 40 +max_image_height = 13 +disable_images = false +disable_links = false +``` + +- `theme`: Overall color scheme (default: "default") +- `code_highlight_theme`: Theme for code syntax highlighting (default: "Solarized (dark)") +- `max_image_width` and `max_image_height`: Maximum dimensions for rendered images +- `disable_images`: If true, images will not be rendered +- `disable_links`: If true, links will not be clickable + ## Contributing Contributions are welcome! Please feel free to submit a Pull Request. As this project is in alpha, your input and contributions can significantly shape its development. diff --git a/docs/main.md b/docs/main.md index 0f0aa9a..2a5ee4c 100644 --- a/docs/main.md +++ b/docs/main.md @@ -1,4 +1,4 @@ -smd: simple Markdown renderer +# smd: simple Markdown renderer ## Usage @@ -10,17 +10,45 @@ If FILE is not provided, smd reads from standard input. ## Options -| | | -| ------------- | ------------------------------------ | -| `--debug` | Enable debug mode for verbose output | -| `--no-images` | Disable image rendering | -| `--help` | Display this help information | -| `--version` | Display version information | +| | | +| ------------------- | ------------------------------------- | +| `--debug` | Enable debug mode for verbose output | +| `--help` | Display this help information | +| `--version` | Display version information | +| `--generate-config` | Generate a default configuration file | + +## Configuration + +smd uses a configuration file located at: + +- Linux/macOS: `~/.config/smd/config.toml` +- Windows: `%APPDATA%\smd\config.toml` + +You can generate a default configuration file using the `--generate-config` option. + +The configuration file allows you to customize various aspects of the rendering, including: + +- Enabling/disabling image rendering +- Setting maximum image dimensions +- Choosing the code highlighting theme +- Enabling/disabling clickable links ## Examples Render a Markdown file: -`smd path/to/your/markdown_file.md` + +```bash +smd path/to/your/markdown_file.md +``` Render from standard input: -`echo "# Hello, world" | smd` + +```bash +echo "# Hello, world" | smd +``` + +Generate a default configuration file: + +```bash +smd --generate-config +``` diff --git a/src/main.rs b/src/main.rs index f4caf4b..1f40ee5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,8 +1,10 @@ extern crate lazy_static; +use dirs::home_dir; use include_dir::{include_dir, Dir}; use lazy_static::lazy_static; use reqwest::blocking::Client; +use serde::{Deserialize, Serialize}; use serde_json::json; use serde_json::Value; use sha2::{Digest, Sha256}; @@ -17,20 +19,44 @@ use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::Arc; use std::sync::Mutex; use std::sync::OnceLock; +use std::sync::RwLock; use syntect::easy::HighlightLines; use syntect::highlighting::{Style, ThemeSet}; use syntect::parsing::SyntaxSet; use syntect::util::LinesWithEndings; use tempfile::TempDir; use termcolor::{Color, ColorChoice, ColorSpec, StandardStream, WriteColor}; + use url::Url; -use viuer::Config; static IMAGE_FOLDER: OnceLock = OnceLock::new(); static DEBUG_MODE: AtomicBool = AtomicBool::new(false); static NO_IMAGES: AtomicBool = AtomicBool::new(false); static DOCS_DIR: Dir = include_dir!("$CARGO_MANIFEST_DIR/docs"); +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct Config { + pub theme: String, + pub code_highlight_theme: String, + pub max_image_width: Option, + pub max_image_height: Option, + pub render_images: bool, + pub render_links: bool, +} + +impl Default for Config { + fn default() -> Self { + Config { + theme: "default".to_string(), + code_highlight_theme: "Solarized (dark)".to_string(), + max_image_width: Some(40), + max_image_height: Some(13), + render_images: true, + render_links: true, + } + } +} + // Global storage lazy_static! { static ref CURRENT_HEADING_LEVEL: Arc> = Arc::new(Mutex::new(0)); @@ -40,31 +66,83 @@ lazy_static! { static ref LINK_DEFINITIONS: Mutex)>> = Mutex::new(HashMap::new()); static ref FOOTNOTES: Mutex> = Mutex::new(HashMap::new()); + static ref GLOBAL_CONFIG: RwLock = RwLock::new(Config::default()); +} + +fn load_config() { + let config_dir = if cfg!(target_os = "macos") { + home_dir() + .map(|path| path.join(".config")) + .unwrap_or_else(|| PathBuf::from("~/.config")) + } else { + dirs::config_dir().unwrap_or_else(|| PathBuf::from("~/.config")) + }; + + let config_path = config_dir.join("smd").join("config.toml"); + + let config = if config_path.exists() { + match fs::read_to_string(&config_path) { + Ok(contents) => match toml::from_str(&contents) { + Ok(parsed_config) => parsed_config, + Err(e) => { + eprintln!( + "Failed to parse config file: {}. Using default configuration.", + e + ); + Config::default() + } + }, + Err(e) => { + eprintln!( + "Failed to read config file: {}. Using default configuration.", + e + ); + Config::default() + } + } + } else { + eprintln!( + "Config file not found at {:?}. Using default configuration.", + config_path + ); + Config::default() + }; + + let mut global_config = GLOBAL_CONFIG.write().unwrap(); + *global_config = config; +} + +fn get_config() -> Config { + GLOBAL_CONFIG.read().unwrap().clone() } fn main() -> io::Result<()> { let args: Vec = env::args().collect(); let mut debug_mode = false; - let mut no_images = false; let mut file_path = None; // Parse command-line arguments for arg in &args[1..] { match arg.as_str() { "--debug" => debug_mode = true, - "--no-images" => no_images = true, "--help" => { return render_help(); } "--version" => { return print_version(); } + "--generate-config" => { + return generate_default_config(); + } _ => file_path = Some(arg), } } + // Load the configuration + load_config(); + DEBUG_MODE.store(debug_mode, Ordering::Relaxed); - NO_IMAGES.store(no_images, Ordering::Relaxed); + NO_IMAGES.store(get_config().render_images, Ordering::Relaxed); let content = if let Some(path) = file_path { // Read from file @@ -248,10 +326,11 @@ fn render_code(node: &Value) -> io::Result<()> { let ps = SyntaxSet::load_defaults_newlines(); let ts = ThemeSet::load_defaults(); + let config = get_config(); let syntax = ps .find_syntax_by_extension(lang) .unwrap_or_else(|| ps.find_syntax_plain_text()); - let mut h = HighlightLines::new(syntax, &ts.themes["Solarized (dark)"]); + let mut h = HighlightLines::new(syntax, &ts.themes[&config.code_highlight_theme]); // Print language in italic gray # TODO: Make this optional // stdout.set_color( @@ -458,36 +537,41 @@ fn render_thematic_break() -> io::Result<()> { } fn render_link(node: &Value) -> io::Result<()> { + let config = get_config(); let mut stdout = StandardStream::stdout(ColorChoice::Always); let url = node["url"].as_str().unwrap_or(""); - // Add a space before the link reference - print!(" "); - // Start OSC 8 hyperlink - print!("\x1B]8;;{}\x1B\\", url); + if config.render_links { + render_children(node)?; + } else { + // Add a space before the link reference + print!(" "); + // Start OSC 8 hyperlink + print!("\x1B]8;;{}\x1B\\", url); - stdout.set_color( - ColorSpec::new() - .set_fg(Some(Color::Blue)) - .set_underline(true), - )?; + stdout.set_color( + ColorSpec::new() + .set_fg(Some(Color::Blue)) + .set_underline(true), + )?; - render_children(node)?; + render_children(node)?; - stdout.reset()?; + stdout.reset()?; - // End OSC 8 hyperlink - print!("\x1B]8;;\x1B\\"); + // End OSC 8 hyperlink + print!("\x1B]8;;\x1B\\"); - // Add a space after the link reference - print!(" "); + // Add a space after the link reference + print!(" "); + } Ok(()) } fn render_image(node: &Value) -> io::Result<()> { - if NO_IMAGES.load(Ordering::Relaxed) { - // If --no-images is set, just print the image alt text + let config = get_config(); + if !config.render_images { println!("[Image: {}]", node["alt"].as_str().unwrap_or("")); return Ok(()); } @@ -507,15 +591,14 @@ fn render_image(node: &Value) -> io::Result<()> { return Ok(()); // Silently ignore if the file doesn't exist } - // Attempt to render the image using viuer - let config = Config { + let viuer_config = viuer::Config { absolute_offset: false, - width: Some(40), - height: Some(13), + width: config.max_image_width, + height: config.max_image_height, ..Default::default() }; - if let Err(_) = viuer::print_from_file(local_path, &config) { + if let Err(_) = viuer::print_from_file(&local_path, &viuer_config) { // Silently ignore rendering errors } @@ -879,3 +962,37 @@ fn render_blockquote(node: &Value) -> io::Result<()> { Ok(()) } } + +fn generate_default_config() -> io::Result<()> { + let config_dir = if cfg!(target_os = "macos") { + home_dir() + .ok_or_else(|| { + io::Error::new(io::ErrorKind::NotFound, "Could not find home directory") + })? + .join(".config") + } else { + dirs::config_dir().ok_or_else(|| { + io::Error::new(io::ErrorKind::NotFound, "Could not find config directory") + })? + }; + + let smd_config_dir = config_dir.join("smd"); + + // Create all directories in the path if they don't exist + fs::create_dir_all(&smd_config_dir)?; + + let config_path = smd_config_dir.join("config.toml"); + + let default_config = Config::default(); + let toml = toml::to_string_pretty(&default_config).map_err(|e| { + io::Error::new( + io::ErrorKind::Other, + format!("Failed to serialize config: {}", e), + ) + })?; + + fs::write(&config_path, toml)?; + + println!("Default configuration file created at {:?}", config_path); + Ok(()) +}