diff --git a/CHANGELOG.md b/CHANGELOG.md
index d361777..be68496 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -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).
diff --git a/CLAUDE.md b/CLAUDE.md
index efa195e..690894d 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -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.
diff --git a/README.md b/README.md
index 1ea6e58..94287d0 100644
--- a/README.md
+++ b/README.md
@@ -7,7 +7,7 @@ Render Markdown with large-font headings in the terminal using the Kitty graphic
@@ -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.
@@ -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
@@ -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:
diff --git a/README_CN.md b/README_CN.md
index 3d6fc1a..4bef0bf 100644
--- a/README_CN.md
+++ b/README_CN.md
@@ -5,7 +5,7 @@
@@ -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 粗体文本渲染。不想让文档加入那么多种字重,那样反而损害可读性。
@@ -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
@@ -93,10 +99,10 @@ termdown --version
### TUI 模式
-阅读较长的文档时,可以用 `--tui` 进入类似 vim 的交互浏览器:
+当传入文件且 stdout 为真实终端时,自动进入 TUI:
```sh
-termdown --tui README.md
+termdown README.md
```
按键绑定:
diff --git a/src/main.rs b/src/main.rs
index b128866..460cf64 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -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");
@@ -29,7 +30,10 @@ fn main() {
println!(" -h, --help Show this help message");
println!(" -V, --version Show version");
println!(" --theme 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;
@@ -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();
@@ -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;
}
diff --git a/tests/cli.rs b/tests/cli.rs
index ba63e40..57dcf3e 100644
--- a/tests/cli.rs
+++ b/tests/cli.rs
@@ -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:?}");
}