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 - +
termdown rendering the Chinese READMEtermdown --tui rendering the English READMEtermdown rendering the English README in TUI mode
@@ -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 @@ - +
termdown 渲染中文 READMEtermdown --tui 渲染英文 READMEtermdown 在 TUI 模式下渲染英文 README
@@ -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:?}"); }