diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..0a016b6 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,11 @@ +# Changelog + +All notable changes to this project are documented in this file. + +## [0.1.4] + +### Added +- `view` command now renders RFC content through an external Markdown renderer + when the `RFC_VIEWER` environment variable is set (content is piped on stdin), + mirroring how `edit` uses `$EDITOR` (RFC-0009). +- `--raw` flag for `view` to force raw Markdown output, ignoring `RFC_VIEWER`. diff --git a/Cargo.lock b/Cargo.lock index ab38b73..d5e0d0d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -239,7 +239,7 @@ dependencies = [ [[package]] name = "rfc-cli" -version = "0.1.3" +version = "0.1.4" dependencies = [ "clap", "clap_builder", diff --git a/Cargo.toml b/Cargo.toml index 813aff7..7d1e519 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rfc-cli" -version = "0.1.3" +version = "0.1.4" edition = "2021" [dependencies] diff --git a/README.md b/README.md index 2a6206b..610025f 100644 --- a/README.md +++ b/README.md @@ -57,6 +57,7 @@ rfc-cli doctor |----------|-------------|---------| | `RFC_HOME` | Project root directory | Current directory | | `EDITOR` | Editor for the `edit` command | — (required for `edit`) | +| `RFC_VIEWER` | External Markdown renderer for the `view` command (receives content on stdin) | — (raw output to stdout) | ## Commands @@ -107,6 +108,15 @@ rfc-cli view 1 # equivalent to: rfc-cli view 0001 ``` +If the `RFC_VIEWER` environment variable is set, the content is piped to that program (e.g. a terminal Markdown renderer) instead of being printed raw: + +```sh +export RFC_VIEWER="mdt" +rfc-cli view 1 # rendered through mdt + +rfc-cli view 1 --raw # force raw Markdown, ignoring RFC_VIEWER +``` + ### `status ` — current status Displays the RFC status from the index (fast, without reading the file). diff --git a/README_ru.md b/README_ru.md index 4db11eb..3746d78 100644 --- a/README_ru.md +++ b/README_ru.md @@ -50,6 +50,7 @@ rfc-cli doctor |------------|----------|--------------| | `RFC_HOME` | Корневой каталог проекта | Текущая директория | | `EDITOR` | Редактор для команды `edit` | — (обязателен для `edit`) | +| `RFC_VIEWER` | Внешний рендерер Markdown для команды `view` (получает содержимое через stdin) | — (сырой вывод в stdout) | ## Команды @@ -100,6 +101,15 @@ rfc-cli view 1 # эквивалентно: rfc-cli view 0001 ``` +Если задана переменная окружения `RFC_VIEWER`, содержимое передаётся этой программе (например, терминальному рендереру Markdown) вместо сырого вывода: + +```sh +export RFC_VIEWER="mdt" +rfc-cli view 1 # рендерится через mdt + +rfc-cli view 1 --raw # принудительно сырой Markdown, игнорируя RFC_VIEWER +``` + ### `status ` — текущий статус Выводит статус RFC из индекса (быстро, без чтения файла). diff --git a/docs/rfcs/.index.json b/docs/rfcs/.index.json index 1378511..036cc4f 100644 --- a/docs/rfcs/.index.json +++ b/docs/rfcs/.index.json @@ -157,14 +157,19 @@ { "number": "0009", "title": "RFC-0009: просмотр RFC через внешний рендерер в команде view", - "status": "draft", + "status": "implemented", "dependencies": [ "RFC-0003" ], "superseded_by": null, - "links": [], - "mtime": "1786337548", - "content_hash": null + "links": [ + "src/commands/view.rs", + "src/cli.rs", + "src/main.rs", + "tests/integration_test.rs" + ], + "mtime": "1786338690", + "content_hash": "57258823363c37be4463a51b3fe9b4dc23794f457c3f4c80392bc227335574d7" } ] } \ No newline at end of file diff --git a/docs/rfcs/0009.md b/docs/rfcs/0009.md index 10aefe6..102fed8 100644 --- a/docs/rfcs/0009.md +++ b/docs/rfcs/0009.md @@ -1,9 +1,13 @@ --- title: "RFC-0009: просмотр RFC через внешний рендерер в команде view" -status: draft +status: implemented dependencies: [RFC-0003] superseded_by: null -links: [] +links: + - src/commands/view.rs + - src/cli.rs + - src/main.rs + - tests/integration_test.rs --- ## Problem diff --git a/src/cli.rs b/src/cli.rs index 9e31dd4..5e2896c 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -33,6 +33,9 @@ pub enum Commands { View { /// RFC number number: String, + /// Print raw Markdown to stdout, ignoring RFC_VIEWER + #[arg(long)] + raw: bool, }, /// Show RFC status Status { diff --git a/src/commands/view.rs b/src/commands/view.rs index 97cea5b..5b04b89 100644 --- a/src/commands/view.rs +++ b/src/commands/view.rs @@ -1,10 +1,14 @@ +use std::env; use std::fs; +use std::io::Write; use std::path::Path; +use std::process::{Command, Stdio}; use crate::rfclib::rfc; -/// Executes the `view` command: prints RFC content to stdout -pub fn execute(project_root: &Path, number: &str) -> Result<(), String> { +/// Executes the `view` command: prints RFC content to stdout, +/// or renders it through an external viewer set via $RFC_VIEWER. +pub fn execute(project_root: &Path, number: &str, raw: bool) -> Result<(), String> { let normalized = rfc::normalize_number(number)?; let path = rfc::rfc_path(project_root, number)?; @@ -15,7 +19,47 @@ pub fn execute(project_root: &Path, number: &str) -> Result<(), String> { let content = fs::read_to_string(&path) .map_err(|e| format!("Failed to read {}: {}", path.display(), e))?; - print!("{}", content); + // --raw forces raw output and ignores RFC_VIEWER + if raw { + print!("{}", content); + return Ok(()); + } + + // RFC_VIEWER is set and non-empty — render through an external program + match env::var("RFC_VIEWER") { + Ok(viewer) if !viewer.trim().is_empty() => render_with_viewer(&viewer, &content), + _ => { + print!("{}", content); + Ok(()) + } + } +} + +/// Runs the external viewer, passing RFC content on stdin. +fn render_with_viewer(viewer: &str, content: &str) -> Result<(), String> { + let mut parts = viewer.split_whitespace(); + let program = parts.next().unwrap(); // non-empty string guaranteed by caller + let args: Vec<&str> = parts.collect(); + + let mut child = Command::new(program) + .args(&args) + .stdin(Stdio::piped()) + .spawn() + .map_err(|e| format!("Failed to launch viewer '{}': {}", viewer, e))?; + + if let Some(mut stdin) = child.stdin.take() { + stdin + .write_all(content.as_bytes()) + .map_err(|e| format!("Failed to write to viewer '{}': {}", viewer, e))?; + } + + let status = child + .wait() + .map_err(|e| format!("Failed to wait for viewer '{}': {}", viewer, e))?; + + if !status.success() { + return Err(format!("Viewer '{}' exited with error", viewer)); + } Ok(()) } diff --git a/src/main.rs b/src/main.rs index 81d9b5c..113c397 100644 --- a/src/main.rs +++ b/src/main.rs @@ -27,7 +27,7 @@ fn main() { Commands::Init => commands::init::execute(&project_root), Commands::New { title } => commands::new::execute(&project_root, &title), Commands::List { status } => commands::list::execute(&project_root, status.as_deref()), - Commands::View { number } => commands::view::execute(&project_root, &number), + Commands::View { number, raw } => commands::view::execute(&project_root, &number, raw), Commands::Status { number } => commands::status::execute(&project_root, &number), Commands::Edit { number, force } => commands::edit::execute(&project_root, &number, force), Commands::Set { number, status, by } => { diff --git a/tests/integration_test.rs b/tests/integration_test.rs index 9965d3d..2eeb894 100644 --- a/tests/integration_test.rs +++ b/tests/integration_test.rs @@ -52,6 +52,17 @@ fn run_rfc_cli_without_editor(project_dir: &Path, args: &[&str]) -> Output { .expect("Failed to execute rfc-cli") } +// Helper: run rfc-cli with a custom $RFC_VIEWER +fn run_rfc_cli_with_viewer(project_dir: &Path, args: &[&str], viewer: &str) -> Output { + let binary = env!("CARGO_BIN_EXE_rfc-cli"); + std::process::Command::new(binary) + .args(args) + .env("RFC_HOME", project_dir.as_os_str()) + .env("RFC_VIEWER", viewer) + .output() + .expect("Failed to execute rfc-cli") +} + // Helper: write an RFC file with given status AND update the index entry fn write_rfc_with_status(dir: &Path, number: &str, title: &str, status: &str) { let content = format!( @@ -782,6 +793,74 @@ fn test_view_invalid_number() { cleanup(&dir); } +#[test] +fn test_view_uses_rfc_viewer() { + let dir = create_temp_dir("view_viewer"); + + run_rfc_cli(&dir, &["init"]); + run_rfc_cli(&dir, &["new", "rendered RFC"]); + + // `cat` acts as a trivial viewer: it echoes stdin to stdout + let output = run_rfc_cli_with_viewer(&dir, &["view", "1"], "cat"); + let stdout = String::from_utf8_lossy(&output.stdout); + + assert!(output.status.success()); + assert!( + stdout.contains("RFC-0001: rendered RFC"), + "viewer should receive and emit RFC content, got: {}", + stdout + ); + + cleanup(&dir); +} + +#[test] +fn test_view_raw_ignores_rfc_viewer() { + let dir = create_temp_dir("view_raw"); + + run_rfc_cli(&dir, &["init"]); + run_rfc_cli(&dir, &["new", "raw RFC"]); + + // Even with RFC_VIEWER set, --raw must print the raw Markdown to stdout. + // Use a viewer that would produce different output if invoked. + let output = run_rfc_cli_with_viewer(&dir, &["view", "1", "--raw"], "true"); + let stdout = String::from_utf8_lossy(&output.stdout); + + assert!(output.status.success()); + assert!( + stdout.contains("RFC-0001: raw RFC"), + "--raw should print raw content regardless of RFC_VIEWER, got: {}", + stdout + ); + assert!( + stdout.contains("## Problem"), + "--raw should print full Markdown, got: {}", + stdout + ); + + cleanup(&dir); +} + +#[test] +fn test_view_viewer_launch_failure() { + let dir = create_temp_dir("view_viewer_fail"); + + run_rfc_cli(&dir, &["init"]); + run_rfc_cli(&dir, &["new", "viewer fail RFC"]); + + let output = run_rfc_cli_with_viewer(&dir, &["view", "1"], "definitely_not_a_real_program_xyz"); + let stderr = String::from_utf8_lossy(&output.stderr); + + assert!(!output.status.success()); + assert!( + stderr.contains("Failed to launch viewer"), + "should report viewer launch failure, got: {}", + stderr + ); + + cleanup(&dir); +} + // ============================================================ // Tests for `status` command // ============================================================