Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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`.
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "rfc-cli"
version = "0.1.3"
version = "0.1.4"
edition = "2021"

[dependencies]
Expand Down
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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 <number>` — current status

Displays the RFC status from the index (fast, without reading the file).
Expand Down
10 changes: 10 additions & 0 deletions README_ru.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ rfc-cli doctor
|------------|----------|--------------|
| `RFC_HOME` | Корневой каталог проекта | Текущая директория |
| `EDITOR` | Редактор для команды `edit` | — (обязателен для `edit`) |
| `RFC_VIEWER` | Внешний рендерер Markdown для команды `view` (получает содержимое через stdin) | — (сырой вывод в stdout) |

## Команды

Expand Down Expand Up @@ -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 <number>` — текущий статус

Выводит статус RFC из индекса (быстро, без чтения файла).
Expand Down
13 changes: 9 additions & 4 deletions docs/rfcs/.index.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
]
}
8 changes: 6 additions & 2 deletions docs/rfcs/0009.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
3 changes: 3 additions & 0 deletions src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
50 changes: 47 additions & 3 deletions src/commands/view.rs
Original file line number Diff line number Diff line change
@@ -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)?;

Expand All @@ -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(())
}
2 changes: 1 addition & 1 deletion src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 } => {
Expand Down
79 changes: 79 additions & 0 deletions tests/integration_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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!(
Expand Down Expand Up @@ -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
// ============================================================
Expand Down
Loading