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
19 changes: 19 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,25 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

### Changed
- **BREAKING:** Passing a Markdown file now opens the interactive TUI by
default instead of printing cat-style output. Use `--cat` to force the
previous non-interactive renderer. Pipes still work transparently:
`termdown FILE.md | less` and `cat FILE.md | termdown` automatically
fall back to cat-style output when stdout is not a terminal or input
comes from stdin.

### Added
- `--cat` flag to force non-interactive cat-style output regardless of
whether stdout is a terminal.

### Removed
- **BREAKING:** The `--tui` flag is gone. With TUI as the default it was
either a no-op (TTY case) or a footgun (forcing TUI when stdout is not
a terminal, which ratatui can't drive). Drop it from scripts.

## [0.4.0] - 2026-04-22

First release published to [crates.io](https://crates.io/crates/termdown).
Expand Down
14 changes: 14 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,17 @@ Add it as a Makefile target first, then reference the target from CI. Never let
3. Never run `git commit` while `HEAD` is on `master`, and never run `git push origin master` — even for "small" doc tweaks. Committing to local master (even without pushing) tends to contaminate the merge base of later feature branches.

If you notice you're on `master` with uncommitted changes, stash them, switch to a new branch, and pop the stash before committing.

### Exception: version bumps

Version bumps are the **only** allowed direct-to-`master` commits. They must not ride along inside a feature/fix PR — keep them as standalone commits so the release history stays readable.

When the feature/fix PRs that make up a release have all merged into `master`:

1. `git checkout master && git pull` to land on the merged tip.
2. Edit `Cargo.toml` (and `Cargo.lock` — `cargo build` will refresh it) to the new version.
3. Lock the `CHANGELOG.md` `[Unreleased]` section to `[X.Y.Z] - YYYY-MM-DD`.
4. `git commit -m "chore: bump version to X.Y.Z"` on `master`.
5. `git tag vX.Y.Z` and `git push origin master --follow-tags`.

The `release.yml` workflow triggers on `v*` tag pushes and produces the GitHub Release (cross-platform binaries + checksums). Pushing the tag is therefore both the version stamp **and** the release trigger — no extra step.
21 changes: 14 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ Render Markdown with large-font headings in the terminal using the Kitty graphic
<table>
<tr>
<td><img src="https://raw.githubusercontent.com/rrbe/termdown/v0.4.0/docs/screenshots/termdown_render_cn_demo.png" width="380" alt="termdown rendering the Chinese README" /></td>
<td><img src="https://raw.githubusercontent.com/rrbe/termdown/v0.4.0/docs/screenshots/termdown_render_en_tui_demo.png" width="380" alt="termdown --tui rendering the English README" /></td>
<td><img src="https://raw.githubusercontent.com/rrbe/termdown/v0.4.0/docs/screenshots/termdown_render_en_tui_demo.png" width="380" alt="termdown rendering the English README in TUI mode" /></td>
</tr>
</table>

Expand All @@ -19,8 +19,8 @@ glow is a great terminal Markdown renderer, but headings are only distinguished

termdown rasterizes H1-H3 headings as PNG and paints them via the Kitty graphics protocol. Two modes share the same renderer:

- **Direct output** -- `cat`-like, pipe-friendly; dump rendered Markdown straight into your terminal.
- **Interactive TUI** (`--tui`) -- vim-style browser with search, Table of Contents, and link-follow navigation for longer documents.
- **Interactive TUI** (default when a file is given) -- vim-style browser with search, Table of Contents, and link-follow navigation for longer documents.
- **Direct output** (`--cat`, or automatic when stdout is piped / input comes from stdin) -- dump rendered Markdown straight into your terminal.

H4-H6 headings always fall back to ANSI bold text.

Expand Down Expand Up @@ -84,12 +84,18 @@ rm -rf ~/.termdown
## Usage

```sh
# Render a file
# Open a file in the interactive TUI (default)
termdown README.md

# Pipe from stdin
# Force plain cat-style output (non-interactive, pipe-friendly)
termdown --cat README.md

# Pipe from stdin (always cat-style — TUI needs a real file)
cat notes.md | termdown

# Piped or redirected stdout also falls back to cat
termdown README.md | less

# Use a specific theme instead of auto-detect
termdown --theme light README.md

Expand All @@ -100,10 +106,11 @@ termdown --version

### TUI mode

For long files, use `--tui` for a vim-style interactive browser:
The TUI launches automatically whenever you pass a file and stdout is a real
terminal:

```sh
termdown --tui README.md
termdown README.md
```

Key bindings:
Expand Down
20 changes: 13 additions & 7 deletions README_CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<table>
<tr>
<td><img src="https://raw.githubusercontent.com/rrbe/termdown/v0.4.0/docs/screenshots/termdown_render_cn_demo.png" width="380" alt="termdown 渲染中文 README" /></td>
<td><img src="https://raw.githubusercontent.com/rrbe/termdown/v0.4.0/docs/screenshots/termdown_render_en_tui_demo.png" width="380" alt="termdown --tui 渲染英文 README" /></td>
<td><img src="https://raw.githubusercontent.com/rrbe/termdown/v0.4.0/docs/screenshots/termdown_render_en_tui_demo.png" width="380" alt="termdown 在 TUI 模式下渲染英文 README" /></td>
</tr>
</table>

Expand All @@ -18,8 +18,8 @@

termdown 将 H1-H3 标题栅格化为 PNG 图片,通过 Kitty 图形协议直接绘制到终端。提供两种使用模式:

- **直接输出** —— `termdown README.md`, 像 `cat` 一样轻量、管道友好,把渲染后的 Markdown 直接打到终端。适合快速查看短文档
- **交互式 TUI** —— `termdown --tui README.md`,类 vim/less 的体验,支持常见的翻页、搜索等快捷键,支持查看 TOC、链接跳转,适合阅读较长文档
- **交互式 TUI**(默认)—— `termdown README.md`,类 vim/less 的体验,支持常见的翻页、搜索等快捷键,支持查看 TOC、链接跳转,适合阅读较长文档
- **直接输出**(`--cat`,或当 stdout 被管道/重定向、输入来自 stdin 时自动启用)—— 像 `cat` 一样轻量、管道友好,把渲染后的 Markdown 直接打到终端

H4-H6 标题始终以 ANSI 粗体文本渲染。不想让文档加入那么多种字重,那样反而损害可读性。

Expand Down Expand Up @@ -77,12 +77,18 @@ rm -rf ~/.termdown
## 使用

```sh
# 渲染文件
# 默认进入交互式 TUI
termdown README.md

# 从 stdin 管道输入
# 强制使用 cat 风格的纯输出(非交互、管道友好)
termdown --cat README.md

# 从 stdin 管道输入(始终是 cat 模式 —— TUI 需要真实文件)
cat notes.md | termdown

# stdout 被管道/重定向时也会自动回退到 cat
termdown README.md | less

# 指定主题(不使用终端亮色、暗色主题自动检测)
termdown --theme light README.md

Expand All @@ -93,10 +99,10 @@ termdown --version

### TUI 模式

阅读较长的文档时,可以用 `--tui` 进入类似 vim 的交互浏览器
当传入文件且 stdout 为真实终端时,自动进入 TUI

```sh
termdown --tui README.md
termdown README.md
```

按键绑定:
Expand Down
24 changes: 14 additions & 10 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ mod tui;
use std::fs;
use std::io::{self, Read};

use crossterm::tty::IsTty;
use terminal_size::{terminal_size, Width};

const VERSION: &str = env!("CARGO_PKG_VERSION");
Expand All @@ -29,7 +30,10 @@ fn main() {
println!(" -h, --help Show this help message");
println!(" -V, --version Show version");
println!(" --theme <auto|dark|light> Color theme (default: auto-detect)");
println!(" --tui Open FILE in interactive TUI mode");
println!(" --cat Force non-interactive cat-style output");
println!();
println!("By default, passing FILE opens it in the interactive TUI.");
println!("Piped/redirected stdout and stdin input automatically use cat mode.");
println!();
println!("Config: ~/.termdown/config.toml");
return;
Expand All @@ -40,7 +44,7 @@ fn main() {
return;
}

let tui_mode = args.iter().any(|a| a == "--tui");
let cat_flag = args.iter().any(|a| a == "--cat");

check_terminal_support();

Expand Down Expand Up @@ -72,14 +76,14 @@ fn main() {
found
};

if tui_mode {
let path = match file_arg.as_deref() {
Some("-") | None => {
eprintln!("termdown: --tui requires a FILE argument (stdin is not supported)");
std::process::exit(2);
}
Some(p) => p.to_string(),
};
// TUI is the default when we have a real file path and stdout is a
// terminal; --cat, piping/redirecting, or stdin input all fall through
// to cat mode so scripts like `termdown foo.md | less` keep working.
let want_tui =
!cat_flag && matches!(file_arg.as_deref(), Some(p) if p != "-") && io::stdout().is_tty();

if want_tui {
let path = file_arg.expect("want_tui implies a file path");
tui::run(&path, &config, theme);
return;
}
Expand Down
40 changes: 24 additions & 16 deletions tests/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -233,28 +233,36 @@ fn unsupported_terminal_emits_warning_on_stderr() {
}

#[test]
fn tui_without_file_fails_with_error() {
let output = run_termdown(&["--tui"], None, &[("TERM_PROGRAM", "ghostty")], &[]);
assert!(!output.status.success());
assert!(
stderr_text(&output).contains("--tui requires a FILE"),
"stderr: {}",
stderr_text(&output)
fn file_arg_with_piped_stdout_falls_back_to_cat() {
// Default is TUI when a FILE is given, but only if stdout is a TTY.
// Tests pipe stdout, so we should get plain cat output, not a TUI error.
let file = TempMarkdownFile::new("hello from file\n");
let output = run_termdown(
&[file.path().to_str().expect("path should be valid UTF-8")],
None,
&[("TERM_PROGRAM", "ghostty")],
&[],
);
let stdout = strip_ansi(&stdout_text(&output));

assert!(output.status.success(), "stderr: {}", stderr_text(&output));
assert!(stdout.contains("hello from file"), "stdout was: {stdout:?}");
}

#[test]
fn tui_with_stdin_sentinel_fails() {
fn cat_flag_forces_cat_output_with_file() {
let file = TempMarkdownFile::new("plain content\n");
let output = run_termdown(
&["--tui", "-"],
Some("# hi\n"),
&[
"--cat",
file.path().to_str().expect("path should be valid UTF-8"),
],
None,
&[("TERM_PROGRAM", "ghostty")],
&[],
);
assert!(!output.status.success());
assert!(
stderr_text(&output).contains("--tui requires a FILE"),
"stderr: {}",
stderr_text(&output)
);
let stdout = strip_ansi(&stdout_text(&output));

assert!(output.status.success(), "stderr: {}", stderr_text(&output));
assert!(stdout.contains("plain content"), "stdout was: {stdout:?}");
}
Loading