mcpField
W
writing-user-outputs

writing-user-outputs

github.com/max-sixty开发9.5k73 次安装今天更新
代码仓库

关于 writing-user-outputs

面向 Worktrunk 开发者的贡献技能,说明 CLI 输出架构与格式规范,涵盖 Shell 集成、标准输出与错误输出、JSON、分页及消息约定。

主要功能

  1. 01说明 Shell 指令集成
  2. 02按内容路由标准输出与错误输出
  3. 03规范 JSON 和 CLI 消息格式
  4. 04处理 ANSI 颜色与管道关闭
  5. 05分页显示较长的人类可读输出
  6. 06记录输出安全约定

适用场景

  1. 01实现 Worktrunk 命令的输出
  2. 02决定响应应写入标准输出还是错误输出
  3. 03检查颜色、分页或 JSON 行为

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/max-sixty/worktrunk --skill writing-user-outputs

安装后在 Claude Code 中输入/writing-user-outputs确认已加载。

详细介绍

输出系统架构

Shell 集成

Worktrunk 使用一个基于文件的指令来实现 Shell 集成:

  1. Shell 包装器通过 mktemp 创建临时文件
  2. Shell 包装器设置 WORKTRUNK_DIRECTIVE_CD_FILE
  3. wt 将原始路径写入文件
  4. wt 退出后,Shell 包装器切换到该路径对应的目录

--execute 始终由 wt 直接启动外部程序,并将所选工作树设为工作目录。它不使用指令。

输出函数

输出系统会自动处理 Shell 集成。只需调用输出函数——无论是否启用了 Shell 集成,它们都会正确执行。

// NEVER DO THIS - don't check mode in command code
if is_shell_integration_active() {
    // different behavior
}

// ALWAYS DO THIS - just call output functions
eprintln!("{}", success_message("Created worktree"));
output::change_directory(&path)?;  // Writes to directive file if set, else no-op

打印输出:

使用 worktrunk::styling 中的 eprintln! 和 println!(从 anstream 重新导出,以自动支持颜色并检测 TTY):

use worktrunk::styling::{eprintln, println, stderr};

// Status messages to stderr
eprintln!("{}", success_message("Created worktree"));

// Primary output to stdout (tables, shell code, pipeable)
println!("{}", table_output);

// Flush before interactive prompts
stderr().flush()?;

src/ 中有两个 crate,它们的路径有所不同:在二进制文件的模块中使用 worktrunk::styling——这些模块由 src/main.rs 声明(commands、cli、display 等),本技能中的示例也都针对这些模块编写;在库的模块中使用 crate::styling——这些模块由 src/lib.rs 声明(git、config、shell_exec 等),在这些模块中 worktrunk:: 根本无法解析。守卫测试接受这两种写法,因此只有编译器能告诉你文件中的路径是否错误。

当前作用域中的 println! 决定了管道关闭时是否会发生 panic:标准库的宏会在写入时遇到 BrokenPipe 错误后 panic,而 anstream 的宏会忽略该错误。wt … | head 会关闭管道,因此命令代码要导入 worktrunk::styling 中的宏,确保 src/ 中不再有 std::println!。

stderr 宏也遵循相同规则,但后果不同:当 stderr 不是终端时,anstream 的 eprint! / eprintln! 会去除 ANSI 转义序列,而标准库的宏会保留它们。因此,在 wt … 2>log 中,如果某个文件导入了其中一种宏而未导入另一种,消息块中的一行会带有转义序列,下一行却没有。容易漏掉的是 eprint!——它不会换行,因此在只导入了 eprintln 的文件中,消息块中途就可能调用它。src/ 下的每个裸 eprint! / eprintln! 都必须解析为 anstream 中的宏:导入它,或将调用限定为 styling::eprintln!(…)。tests/integration_tests/output_system_guard.rs 中的 check_stderr_macros_come_from_styling 会静态检查这一点,因为快照无法做到这一点——测试套件会强制设置 CLICOLOR_FORCE=1,因此无论当前作用域中是哪种宏,两种打印宏都会输出颜色,快照也就无法区分。它的 STD_STDERR_ALLOWED_PATHS 豁免的是整个文件,而不是单次调用,因此只有当标准库的宏在整个文件中都适用时,才应添加条目。

没有指定宏名称的写入会完全绕过该扫描,因为扫描会查找宏名称,例如:writeln!(std::io::stderr(), …)、std::io::stderr().write_all(…)、锁定的句柄,或绑定到变量的句柄。check_raw_stderr_writes_go_through_anstream 也会拒绝这些形式,它有自己的 RAW_STDERR_ALLOWED_PATHS——目前只有 progress.rs。该文件中的进度指示器会在一帧输出期间持有同一个锁(anstream 的 stderr() 不会持锁),且仅在 TTY 环境中启用,因此它的输出不会有任何内容被重定向到 stderr。需要留意的是,消息在一处渲染,却在下几层才打印的情况;此时打印代码并不知道自己处理的是说明性文本。Cmd::delayed_stream 的进度行就是一个例子,而那里使用原始句柄,曾让重定向到文件的 wt switch 日志中只有这一行带颜色。 ANSI 格式已确定的输出:在命令顶部调用 worktrunk::styling::ColorChoice::Always.write_global() 声明一次,然后通过相同的 anstream 宏打印——例如 shell 提示符或 Claude Code 渲染的状态行,以及文档流水线会将其转义序列转换为 HTML 的 --help-page 文档(--plain 和 --help-md 也以相同方式声明 Never)。这两种使用场景都不是 tty,因此如果不进行声明,anstream 每次都会剥除颜色——而测试套件不会发现这个问题,因为它会通过 CLICOLOR_FORCE=1 强制启用颜色;test_color_follows_the_consumer 则会固定未强制启用时的行为。只有在管道只是传递媒介而非最终目的地时,才声明 Always;供人直接阅读的内容仍使用普通的 anstream,它会在管道输出时剥除颜色,并遵循 NO_COLOR。

--format=json 响应:在命令处理函数中使用 crate::output::print_json。内部操作返回类型化事实;如果工作可能在后续失败前完成,应在 CommandFailure<T> 中保留这些事实。外层处理函数可以使用 finish_output,仅输出一次可用结果,并返回原始错误,以便 main 输出易读的 stderr 诊断信息并进行原生退出处理。没有结果的路径不会输出任何内容;合法的 JSON null 仍然是一个结果。

Shell 集成功能(src/output/global.rs):

函数用途
change_directory(path)wt 退出后让 Shell 执行 cd(如果设置了指令文件,则写入该文件)
execute(argv)在所选工作树中运行外部程序
terminate_output()重置 stderr 上的 ANSI 状态
is_shell_integration_active()检查是否设置了指令文件(很少需要)
pre_hook_display_path(path)计算 pre-hook 的显示路径
post_hook_display_path(path)计算 post-hook 的显示路径

消息格式化函数(worktrunk::styling):

函数符号颜色
success_message()✓绿色
progress_message()◎青色
info_message()○符号变暗,文本保持普通
warning_message()▲黄色
hint_message()↳变暗
error_message()✗红色
prompt_message()❯青色

章节标题(worktrunk::styling):

use worktrunk::styling::format_heading;

// Plain heading
format_heading("BINARIES", None)  // => "BINARIES" (cyan)

// Heading with suffix
format_heading("USER CONFIG", Some("@ ~/.config/wt.toml"))
// => "USER CONFIG @ ~/.config/wt.toml" (title cyan, suffix plain)

stdout 与 stderr

判断原则: stdout 输出命令的答案;stderr 输出关于生成答案的说明。区分依据是答案与说明,而不是面向谁——wt list 是“给用户看的”,但它属于 stdout,因为它就是答案。“这是不是给用户看的消息?”无法作为区分依据,因为几乎所有输出都是给用户看的。

  • stdout → 以用户选择的格式输出答案。数据(表格、JSON、shell 代码、展开后的模板)和 --dry-run 预览都符合这一条件:不修改任何内容时,预览就是完整答案。面向人的格式化输出也属于这里。通过管道时,颜色会自动移除(anstream),因此 wt list | grep 仍然安全。
  • stderr → 关于执行过程的说明:进度、成功/警告/错误消息、提示、交互式提示,以及 -v/-vv 诊断信息。
  • 指令文件 → wt 退出后使用的原始 cd 路径。

同一行在不同模式下可能会切换输出流。wt config shell uninstall 会删除文件,因此 ✓ Removed … @ ~/.zshrc 只是说明已发生的副作用 → stderr(被编辑的文件才是答案;stdout 为空)。wt config shell uninstall --dry-run 不会修改任何内容,因此 ○ Will remove … @ ~/.zshrc 就是唯一的答案 → stdout。发生变化的不是措辞,而是是否存在可作为答案的副作用。

对于拆分输出的预览,--format=json 负载是判断依据:json 会包含的行输出到 stdout,json 会省略的说明则留在 stderr。wt step prune --dry-run 将删除计划输出到 stdout(与 json 输出相同的计划),但会将“Skipped young-branch (younger than 1d)”和“nothing to remove”保留在 stderr。有一种情况不适用以上规则:在交互式提示中显示的预览,例如执行 wt config shell install 时通过 ? 重新预览,这属于提示过程中的说明 → stderr。

示例:

  • wt list、wt config show → 人类可读的表格/转储,或 --format=json,两者都输出到 stdout
  • wt step prune --dry-run → 删除计划输出到 stdout(人类可读格式或 json);“nothing to remove”和跳过较年轻分支的提示输出到 stderr
  • wt config shell init → shell 代码输出到 stdout(供 eval 使用)
  • wt switch → 仅输出状态消息(没有内容可通过管道传递)

何时分页输出

将较长、面向人的 stdout 输出传递给 crate::help_pager::show_help_in_pager。该辅助函数会在内部检测 TTY,因此通过管道传递(wt … | grep)仍能正常工作。

当输出面向人阅读(包含标题、缩进、结构),且可能超过一屏时使用分页。不要对优先供管道处理的数据(表格、JSON、shell 代码)、简短输出或已由委托工具分页的输出(git diff)使用分页。

会分页的示例:--help、wt config show、wt hook show、wt step {commit,squash} --dry-run。不会分页的示例:wt list、wt step diff、wt step eval、--show-prompt(设计上优先供管道处理)。

先将完整输出构建为 String(不要流式输出),然后:

crate::help_pager::show_help_in_pager(&out, true)?;

分页被禁用或不可用、stdout 不是 TTY,或分页器无法启动时,该辅助函数会回退到普通 stdout。传递其返回结果,以便原生信号错误和取消操作能够触发命令清理。

安全性

  • WORKTRUNK_DIRECTIVE_CD_FILE 保存的是原始路径(不进行 shell 解析),因此可以安全地传递给别名/钩子的子进程——向其写入内容的主体,最多只能重定向 cd。

默认情况下,所有指令环境变量都会通过 shell_exec::scrub_directive_env_vars() 从生成的子进程中移除。若嵌套命令可能重定向父 shell,DirectivePassthrough::inherit_from_env() 会重新添加 CD 文件。

Windows 兼容性(Git Bash / MSYS2)

在 Windows 上使用 Git Bash 时,mktemp 会返回 POSIX 风格的路径,例如 /tmp/tmp.xxx。原生 Windows 二进制文件(wt.exe)需要 Windows 路径才能写入指令文件。

无需显式转换路径。 启动原生 Windows 二进制文件时,MSYS2 会自动转换环境变量中的 POSIX 路径——shell 包装脚本可以直接使用 $directive_file。参见: https://www.msys2.org/docs/filesystem-paths/


CLI 输出格式标准

用户消息原则

输出消息应通过在消息文本中反映用户提供的参数(标志、选项、值),确认这些参数。

// User runs: wt switch --create feature --base=main
// GOOD - acknowledges the base branch
"Created new worktree for feature from main @ /path/to/worktree"
// BAD - ignores the base argument
"Created new worktree for feature @ /path/to/worktree"

避免使用“你/你的”等代词: 消息应直接指明相关内容,而不是对用户说话。像“运行”“使用”“添加”这样的祈使表达是可以的——它们符合简洁的 CLI 用语习惯。

// BAD - "To rebase your changes onto main, run wt merge"
// GOOD - "To rebase onto main, run wt merge"

避免使用冗余的括号内容: 括号中的文本应提供新信息,而不是复述已表达的内容。

// BAD - parentheses restate "no changes"
"No changes after squashing 3 commits (commits resulted in no net changes)"
// GOOD - clear and concise
"No changes after squashing 3 commits"
// GOOD - parentheses add supplementary info
"Committing with default message... (3 files, +45, -12)"

括号内容分为两种类型,样式各不相同:

  1. 统计数据括号 → 灰色([90m 亮黑色):可省略而不影响含义的补充数字信息。

    ✓ Merged to main (1 commit, 1 file, +1)
    ◎ Squashing 2 commits into a single commit (2 files, +2)...
    
  2. 原因括号 → 消息颜色:解释操作发生的原因;这是理解消息不可或缺的内容。

    ◎ Removing feature worktree & branch in background (same commit as main, _)
    

统计数据是真正可选的上下文信息。原因回答“为什么这样做是安全的/为什么会发生这种情况?”,因此应与主要消息放在一起。原因括号中的符号仍以其原生样式呈现(参见下文“符号样式”)。

钩子在不同目录中运行时显示路径: 如果钩子运行所在的工作树不同于用户当前所在(或之后将所在)的位置,应显示路径。使用相应的辅助函数:

  1. 前置钩子和手动执行 wt hook —— 用户位于当前工作目录,不会发生目录切换。 使用 output::pre_hook_display_path(hooks_run_at)。 示例:pre-commit、pre-merge、pre-remove、手动执行 wt hook post-merge。

  2. 后置钩子 —— 如果 shell 集成已启用,用户将切换到目标目录。 使用 output::post_hook_display_path(destination)。 示例:pre-start、post-switch、post-start、post-merge(移除后)。

// Pre-hooks: user is at cwd, no cd happens
run_hook_with_filter(..., crate::output::pre_hook_display_path(ctx.worktree_path))?;

// Post-hooks: user will cd to destination if shell integration active
ctx.spawn_post_create_commands(crate::output::post_hook_display_path(&destination))?;

避免使用指代其他消息内容的代词: 提示会作为独立于错误消息的单独消息出现。不要使用“它”之类的代词来指代错误消息中提到的内容。

// BAD - "it" refers to branch name in error message
// Error: "Branch 'feature' not found"
// Hint:  "To create it, use --create"
// GOOD - self-contained hint
// Error: "Branch 'feature' not found"
// Hint:  "To create a new branch, use --create"

标题大小写

帮助文本标题使用句子式大小写:“配置文件”“JSON 输出”“LLM 提交消息”。

消息一致性模式

相关消息应使用一致的标点和结构。

组合操作使用 &: 单个操作包含多个动作时,使用 &:

"Removing feature worktree & branch in background"
"Commands approved & saved to config"

使用分号连接分句: 使用分号连接相关信息:

"Removing feature worktree in background; retaining branch (--no-delete-branch)"
"Branch unmerged; to delete, run <underline>wt remove -D</>"  // hint uses underline
"{tool} not authenticated; run <bold>{tool} auth login</>"       // warning uses bold

明确确认标志: 标志会改变行为时,在括号中显示:

// GOOD - shows the flag explicitly
"Removing feature worktree in background; retaining branch (--no-delete-branch)"
// BAD - doesn't acknowledge user's explicit choice
"Removing feature worktree in background; retaining branch"

标志就近放置: 将标志指示符放在其修饰的概念旁边。 标志应紧跟在它所影响的名词或动作之后,而不是放在消息末尾:

// GOOD - (--force) is adjacent to "worktree" which it modifies
"Removing feature worktree (--force) & branch in background (same commit as main, _)"
// BAD - (--force) at end, disconnected from the worktree removal it enables
"Removing feature worktree & branch in background (same commit as main, _) (--force)"

这一原则可确保读者立即了解每个注释修饰的内容。

平行结构: 相关消息应遵循相同模式:

// GOOD - parallel structure with integration reason explaining branch deletion
// Target branch is bold; symbol uses its standard styling (dim for _ and ⊂)
"Removing feature worktree & branch in background (same commit as <bold>main</>, <dim>_</>)"  // Integrated
"Removing feature worktree in background; retaining unmerged branch"                          // Unmerged
"Removing feature worktree in background; retaining branch (--no-delete-branch)"              // User flag

符号样式: 符号与其颜色不可分割——样式是符号身份的一部分,而非呈现方式的选择。每个符号都有明确的外观,必须在所有上下文中保持一致:

  • _ 和 ⊂ — 暗色(集成/可安全删除指示符)
  • +N 和 -N — 绿色/红色(差异指示符)

符号出现在带颜色的消息中(青色进度、绿色成功)时,应在符号前关闭消息颜色,以便符号以其原生样式呈现。这需要退出消息颜色,并在符号后重新启用。请参阅 src/output/handlers.rs 中的 FlagNote 示例——它通过其 after(color) 方法处理标志确认说明(例如集成原因),并在符号后重新启用消息颜色,从而正确切换颜色。

使用逗号 +“但”+ 破折号说明限制: 陈述带有限制及其原因的结果时:

// Outcome, but limitation — reason
"Worktree for feature @ ~/repo.feature, but cannot change directory — shell integration not installed"

此模式:

  • 说明成功的内容(工作树位于指定路径)
  • 使用“但”引出未能实现的内容(无法执行 cd)
  • 使用破折号解释原因(Shell 集成状态)

请参阅 src/output/shell_integration.rs 中的 compute_shell_warning_reason(),了解 Shell 集成警告消息和提示的完整规范。

只计算一次决策: 对于后台操作,应预先检查条件、显示消息,然后显式传递决策,而不是在后台脚本中重新检查:

// GOOD - check once, pass decision
let should_delete = check_if_merged();
show_message_based_on(should_delete);
spawn_background(build_command(should_delete));

// BAD - check twice (once for message, again in background script)
let is_merged = check_if_merged();
show_message_based_on(is_merged);
spawn_background(build_command_that_checks_merge_again());  // Duplicate check!

警告顺序

核心原则: 在评估过程中发现状态的消息(警告、信息提示)应出现在由该评估引发的后续操作消息之前。

当命令评估状态、发现意外情况并仍继续执行时,应先显示该消息:

▲ Auto-staging 1 untracked path:
  notes.md
◎ Generating commit message...

而不是:

◎ Generating commit message...
▲ Auto-staging 1 untracked path:
  notes.md

由操作本身引发的警告(执行期间发生故障)自然应放在操作之后。

消息类型

成功与信息: 成功(✓)表示创建或更改了某些内容。信息(○) 表示确认状态,但没有任何更改。

成功 ✓信息 ○
"已为 feature 创建工作树""已切换到 feature 的工作树"
"已为 feature 创建新的工作树""已位于 feature 的工作树中"
"已批准并保存命令""所有命令均已获批准"

同一规则也适用于列表中用作逐行标记的独立符号: ✓ 表示已完成的操作,而非状态。报告逐项状态的列表使用 ○(确认状态)或 ❯(等待批准/用户输入)标记——这是 wt hook show 和 wt config approvals list 共用的一套符号。逐行 ✓ 仅用于表示命令刚刚执行的操作结果(shell-install 操作行、-v 子进程跟踪符号)。

提示与信息: 提示建议用户采取操作,或提供额外的非必要背景信息(用户不需要这些补充细节,但可能会觉得有用)。信息表示确认状态,但没有任何更改。

提示 ↳信息 ○
"要继续,请运行 wt merge""已与 main 保持同步"
"请先提交或暂存更改""正在跳过钩子(--no-hooks)"
"可以删除分支""已保留工作树(主工作树)"
"命令失败,退出代码 128:"

警告的放置位置: 发生意外情况时,应在某处发出警告。具体位置取决于问题的性质:

Is it unexpected?
├── No → Silent (e.g., gh not installed when no GitHub remote)
└── Yes → Warn somewhere:
    ├── Immediate impact OR temporary → Inline (warning_message or in-band indicator)
    ├── Persists until user action → wt config show (can be checked later)
    └── Not user-fixable → log::warn! (developer diagnostics)

内联警告用于影响当前命令的问题:

问题为什么使用内联警告
CI 获取期间触发速率限制暂时性问题——下次不会出现
网络超时暂时性问题——重试可能有效
操作期间钩子失败会立即影响此命令

使用 wt config show 显示在用户修复前会持续存在的问题。这些问题无需在每条命令中都提醒用户——用户排查问题时可以查看诊断信息:

问题为什么使用 config show
gh 未通过身份验证用户运行 gh auth login
Shell 集成配置错误用户更新 shell 配置
配置语法错误用户修复配置文件

使用 log::warn!() 记录用户无法修复的问题。这些信息有助于开发者调试,但不应让用户输出变得杂乱:

问题为什么使用 log::warn!
JSON 解析错误(API 已更改)需要修复代码
违反内部不变量开发者错误

提示中的命令建议: 提示包含可运行的命令时,使用“要执行 X,请运行 Y”的模式。将命令放在末尾,方便复制:

// GOOD - command at end for easy copying
"To delete the unmerged branch, run wt remove feature -D"
"To rebase onto main, run wt step rebase or wt merge"

// GOOD - recovery command after shadowing a remote branch
"To switch to the remote branch, delete this branch and run without --create: wt remove --foreground feature && wt switch feature"

// BAD - command without context
"wt remove feature -D deletes unmerged branches"

// BAD - command not at end (hard to copy)
"Run wt switch feature (without --create) to switch to the remote branch"

对于不包含特定命令的一般操作指导,直接使用祈使句更清晰:

// GOOD - direct imperative for general guidance
"Commit or stash changes first"
"Run from inside a worktree, or specify a branch name"

// VERBOSE - "To proceed" adds nothing
"To proceed, commit or stash changes first"

在单条消息中包含说明和命令: 对于包含恢复命令的警告/错误,用分号连接,并像提示一样按“要执行 X,请运行 Y”的顺序组织恢复说明,这样命令仍位于行尾。警告/错误中的命令使用 <bold>(只有提示使用 <underline>):

// Warning with inline recovery command (bold for commands)
warning_message("Failed to restore stash; to restore manually, run <bold>git stash pop {ref}</>")
warning_message("{tool} not authenticated; run <bold>{tool} auth login</>")

// For longer suggestions, use separate hint message (underline for commands)
warning_message("Failed to restore stash")
hint_message("To restore manually, run <underline>git stash pop {ref}</>")

一条提示中包含多项建议: 用分号组合多项建议时,将最常用的命令放在最后,方便在终端中复制:

// GOOD - common action (create) last, easy to select and copy
"To list branches, run wt list --branches; to create a new branch, run wt switch feature --create"

// BAD - common action buried, harder to copy
"To create a new branch, run wt switch feature --create; to list branches, run wt list --branches"

使用 worktrunk::styling 中的 suggest_command() 正确转义 shell 命令。

每条面向用户的消息都必须带有符号或边栏。

章节标题: 对于分节输出(wt hook show、wt config show),使用 worktrunk::styling 中的 format_heading()(见上文说明)。

交互式提示与非交互式提示信息

提示词和提示信息的用途不同,对 --yes 的行为也不同。

提示词是工作流中预期会发生的步骤——用户运行命令时知道会被要求确认。例如,在 wt merge 期间批准钩子、确认更新配置、确认安装 shell。--yes 会跳过这些提示词,因为用户预料到了这个问题,并希望提前作答(例如,在 CI 中)。

设置提示词则是意料之外的——用户运行 wt merge 后,被询问是否要设置他们并不了解的 LLM 配置或 shell 集成。--yes 不得跳过这些提示词。用户传入 --yes 来跳过钩子审批,并不表示同意自动配置 shell。在非交互模式下(没有 TTY),这些提示应降级为提示信息或静默跳过——绝不能报错。

类型示例--yes非 TTY 行为
工作流提示词钩子审批、更新配置跳过报错(NotInteractive)
设置提示词LLM 配置、shell 集成无影响提示信息或静默跳过

非 TTY 降级模式:

  • 提示信息——每次运行都让用户知道该选项会带来好处时使用。 shell 集成提示信息:↳ To enable automatic cd, run wt config shell install
  • 静默跳过——提示信息会造成干扰时使用。提交生成设置提示词会静默跳过,因为另一个单独的回退提示信息(emit_hint_if_needed)已在每次提交时覆盖未配置的情况。
  • 报错——仅用于未经同意继续操作并不安全的工作流提示词(钩子审批)。错误中会包含解决方法提示信息: ↳ To skip prompts in CI/CD, add --yes

关键不变量:

  • 在非 TTY 模式下显示提示信息时,不得设置跳过标志——提示信息不是提示词,应该在每次非 TTY 运行时重复显示
  • --yes 表示“我预料到了这个提示词”——它适用于用户主动调用的工作流提示词,不适用于设置或配置发现提示词

空行原则

核心原则: 向用户展示需要阅读和考虑的文本时,应添加间距以便于阅读。通过管道传输输出(stdout)时,应保持紧凑,以便解析。

具体规则:

  • 开头和结尾不留空行——立即开始,干净结束

  • 提示词前留空行,提示词后不留——在提示词前表明“请暂停,即将进行交互”;用户作答后,输出应连续流动。空行属于它所分隔的叙述,因此由调用方输出,而不是由 prompt_yes_no_preview 输出:作为命令输出开头的提示词(wt config shell install、wt config plugins claude install、wt merge 开头的提交生成询问)应紧接开头,因为前面留空行就成了开头空行

  • 阶段之间留一个空行——子操作完成并开始不同操作时,添加一个空行以作视觉分隔

  • 绝不连续留两个空行——元素之间最多留一个空行

  • 提示信息紧随其说明对象——提示信息与其展开说明的消息之间绝不留空行。提示信息(↳)是从属内容,应紧接在父消息下方,中间不留空行。

    // GOOD - hint directly follows its subject
    ↳ fish: Not configured shell extension
    ↳ To configure, run wt config shell install
    
    // BAD - blank line detaches hint from subject
    ↳ fish: Not configured shell extension
    
    ↳ To configure, run wt config shell install
    

提示词间距: 叙述之后的提示词前留一个空行,表示“即将进行不同的操作”,并让用户在阅读和作答前有一个自然的停顿点。提示词后不留空行——用户输入结束交互环节,后续输出会自然承接该选择。如果提示词上方没有内容,例如 wt step commit 开头的设置询问,就没有内容需要与之分隔,因此应紧接开头。

❯ Configure claude for commit messages? [y/N/?] y
✓ Added to user config:
  [commit.generation]
  command = "..."
↳ View config: wt config show

▲ Auto-staging 1 untracked path:
  a
◎ Generating commit message...

阶段分隔: “阶段”是逻辑上独立的操作。上方提示信息与警告之间的空行表示“配置设置已完成,现在开始执行主命令工作流”。

交互式提示词在等待 stdin 输入前,必须刷新 stderr:

eprint!("❯ Allow and remember? [y/N] ");
stderr().flush()?;
io::stdin().read_line(&mut response)?;

时间邻近性:输出应紧贴操作

输出应紧邻其描述的操作。 进度消息仅适用于耗时较长(>400ms)的操作:git 操作、 网络请求、构建。

顺序操作应立即显示反馈:

for item in items {
    eprintln!("{}", progress_message(format!("Removing {item}...")));
    perform_operation(item)?;
    eprintln!("{}", success_message(format!("Removed {item}")));  // Immediate feedback
}

反面示例(输出与操作脱节):

◎ Removing worktree for feature...
◎ Removing worktree for bugfix...
                                    ← Long delay, no feedback
Removed worktree for feature        ← All output at the end
Removed worktree for bugfix

时间邻近性较差的迹象:将消息收集到缓冲区中、批量操作只显示一条成功 消息、耗时较长的操作开始前没有进度提示。

长时间单操作的旋转提示器

如果不适合为每个项目显示 progress_message,例如递归复制数千个 文件,或运行一个没有有用流式输出的长时间子进程,那么 stderr 旋转提示器 可以让用户了解进度。参见 src/progress.rs 中的 Progress 模式: 动词("Copying"、"Removing")在 Progress::start(verb) 时固定, 仅在 TTY 环境启用;当 verbosity() >= 1 或使用 --dry-run 时跳过; 启动延迟 ≥300 ms,确保快速操作保持静默;使用 IEC 字节单位(KiB/MiB); 打印摘要前清除该行,并确保摘要重复旋转提示器显示的任何计数器 (对灰色的 (N files · X MiB) 后缀使用 format_stats_paren)。

将非必要工作推迟到主要输出之后

即发即弃的清理和缓存清扫应在命令最后一条面向用户的消息之后运行,绝不能提前。 若将它们放在开头,会让用户等待本不需要的工作,从而延迟首次输出。

Worktrunk 使用 WORKTRUNK_FIRST_OUTPUT 标记这一边界:处理程序会在输出 开始处提前返回,因此返回前的工作属于关键路径,返回后的工作则不属于。参见 handle_remove_command — run_internal_sweep 会在 handle_remove_output 之后运行。

信息显示:显示一次,不要重复

进度消息应包含所有相关细节(正在执行的操作、 数量、统计数据、上下文)。成功消息应尽量简短,确认 操作完成并提供参考信息(哈希、路径)。

// GOOD - detailed progress, minimal success
eprintln!("{}", progress_message("Squashing 3 commits & working tree changes into a single commit (5 files, +60)..."));
perform_squash()?;
eprintln!("{}", success_message("Squashed @ a1b2c3d"));

区段与符号间距

组合内容紧凑、且相邻部分各有不同含义的输出时(状态栏区段、@+1、↓1、 ?^| 这类前缀字符指示符),应将各区段紧密相连,不留分隔空格。标记符及其值 作为一个整体阅读,视觉上的紧凑也能缩短行长度。

唯一的例外是 emoji:emoji 符号后应留一个空格。 大多数终端会将 emoji 渲染为双倍宽度,并使其溢入右侧单元格,因此紧贴的 🌔65% 会让月亮符号与数字相撞。加上空格 (🌔 65%)可确保它们清晰可读。ASCII 和窄宽度的 Unicode 标记(@、↓、●、 ⊂)没有这个问题,应保持紧贴。

// GOOD - ASCII marker flush with its value
format!("@{added}");          // @+1
// GOOD - emoji gets a trailing space to clear the next cell
format!("{moon} {pct:.0}%");  // 🌔 65%
// BAD - emoji runs into the digits; terminals overlap the glyph
format!("{moon}{pct:.0}%");   // 🌔65%

样式常量

表格渲染仅有三个 anstyle 常量(src/styling/constants.rs):

  • ADDITION:绿色(差异)
  • DELETION:红色(差异)
  • GUTTER:亮白色背景

其他所有内容均使用 cformat! 标签。

命令代码中的样式

使用带格式化函数的 eprintln!。内部样式使用 cformat!:

eprintln!("{}", success_message(cformat!("Created <bold>{branch}</> from <bold>{base}</>")));
eprintln!("{}", hint_message(cformat!("To continue, run <underline>wt merge</>")));

color-print 标签: <bold>、<dim>、<underline>、<bright-black>、<red>、 <green>、<yellow>、<cyan>、<magenta>

分支名称和状态值应在消息中加粗。

**cformat! 中的符号常量:**对于绕过 output:: 函数的消息 (例如,GitError 的 Display 实现),直接使用符号常量:

cformat!("{ERROR_SYMBOL} <red>Branch <bold>{branch}</> not found</>")

消息中的命令和分支

绝不要给命令或分支名称加引号。使用样式突出显示:

  • 在常规字体上下文中:命令和分支使用 <bold>
  • 在提示中:命令和数据值(路径、 分支)使用 <underline>。在 <dim> 中使用下划线是安全的——关闭 [24m 只会重置 下划线,保留暗淡样式。避免在提示中使用 <bold>——关闭 [22m 会同时重置粗体和暗淡样式,因此 </bold> 之后的文本会失去暗淡样式。
// GOOD - bold in normal context
eprintln!("{}", info_message(cformat!("To continue, use <bold>wt merge</>")));
// GOOD - underline for commands in hints
eprintln!("{}", hint_message(cformat!("To see worktrees, run <underline>wt list</>")));
// BAD - quoted commands
eprintln!("{}", hint_message("To see worktrees, run 'wt list'"));

超链接

每个 OSC 8 链接都会加下划线。链接文本会调整大小以适配列(#3604、 :11486),并显示为普通内容;颜色已经表示状态(CI 判定结果,或对无人响应的端口使用暗淡颜色),因此用下划线标记可点击的文本范围。

worktrunk::styling::hyperlink(url, text) 会同时输出链接及其下划线,这样每个调用位置都能遵循这一规则。它以 [24m 结尾,不会影响周围的颜色或暗淡效果。

// GOOD - the helper carries the underline
let cell = hyperlink(url, &format!(":{port}"));
// GOOD - a caller's own style wraps the link
format!("{style}{}{style:#}", hyperlink(url, text))
// BAD - hand-assembled sequences, unmarked link
format!("{}{text}{}", osc8::Hyperlink::new(url), osc8::Hyperlink::END)

非链接文本保持普通样式;某段文本是否为链接由渲染过程决定,而非由输出该文本的单元格决定:LayoutConfig::link_style 会针对整行一次性判定,因此 CI 引用和开发服务器端口不会产生不一致。两种目标不会包含链接,因此也不会有下划线:一种是不支持 OSC 8 的终端,此时 wt list 会完整打印开发服务器 URL;另一种是选择器的行,Destination::picker 会将其渲染为 LinkStyle::Unlinked(该变体的文档字符串说明了原因)。选择器中的内容无论通过哪种方式都不可点击:行的链接样式在渲染时决定,因此一开始就不会包含 OSC 8;预览窗格则由 skim 通过 ansi_to_tui 解析,该解析会移除所有 OSC 8。这里的下划线可以表示其他含义,预览窗格中就用了两次:一次用于 URL,表示引用而非链接(pr_pane::url_line);另一次用于标签栏中的活动标签(items::render_preview_tabs)。

设计原则

  • 使用 cformat! 设置样式 — 切勿手动编写转义码(\x1b[...])
  • cformat! 变量是安全的 — <bold> 之类的标签仅在编译时处理。运行时变量值不会被解析为标记,因此可以直接插入用户内容(分支名称、提交消息、路径、带有 </> 重定向符的 shell 命令),无需转义。不要转义变量中的 </>,否则会增加多余字符。
  • YAGNI — 大多数输出无需设置样式
  • 优雅降级 — 样式会根据使用方调整(NO_COLOR、TTY 检测)。纯文本输出通过措辞、间距和现有语义符号保留含义。仅仅缺少装饰并不足以成为添加新标记的理由;只有在信息会因此丢失时才添加文本。
  • 支持 Unicode — 宽度计算会考虑符号和中日韩字符(通过 StyledLine)

表格渲染使用 StyledLine,以正确计算宽度:

use worktrunk::styling::StyledLine;
use anstyle::{AnsiColor, Color, Style};

let mut line = StyledLine::new();
line.push_styled("Branch", Style::new().dimmed());
line.push_raw("  ");
line.push_styled("main", Style::new().fg_color(Some(Color::Ansi(AnsiColor::Cyan))));
println!("{}", line.render());

高级用法请参见 src/commands/list/render.rs。

文档示例

所有文档、帮助文本和配置模板中的示例都应保持一致。

标准示例设置
元素值说明
仓库目录myproject通用占位符
仓库路径~/code/myproject符合实际开发场景的路径
分支feature/auth展示 sanitize 过滤器
工作树路径~/code/myproject.feature-auth默认模板的结果
模板变量示例

所有示例都使用上表中的标准值:

{{ repo }}           — Repository directory name (e.g., `myproject`)
{{ branch }}         — Branch name (e.g., `feature/auth`)
{{ worktree_path }}  — Absolute path to worktree (e.g., `/path/to/myproject.feature-auth`)

TOML 注释中:

#   {{ repo }}           - Repository directory name (e.g., "myproject")
#   {{ branch }}         - Raw branch name (e.g., "feature/auth")
#   {{ worktree_name }}  - Worktree directory name (e.g., "myproject.feature-auth")
工作树路径示例

展示工作树路径模板示例时:

# Default — siblings in parent directory
# Creates: ~/code/myproject.feature-auth
worktree-path = "../{{ repo }}.{{ branch | sanitize }}"

# Inside the repository
# Creates: ~/code/myproject/.worktrees/feature-auth
worktree-path = ".worktrees/{{ branch | sanitize }}"

缩进栏格式

对引用内容使用缩进栏(git 输出、提交消息、待复制的配置、展示的钩子命令)。

缩进栏由一列带背景色的空白和其后一个空格组成。纯文本输出保留两个空格的缩进。本技能中的缩进栏示例展示的就是这种纯文本形式。

  • format_bash_with_gutter() — shell 命令(暗淡显示并进行语法高亮)
  • format_with_gutter() — 其他内容

缩进栏与表格: 表格用于结构化应用数据;缩进栏用于引用外部内容。

缩进栏与提示: 提示中的命令建议使用内联 <underline>,而非缩进栏。缩进栏用于展示内容(将要执行的命令、待复制的配置);提示则建议用户运行什么命令。

换行约定

核心原则: 所有格式化函数返回的内容都不带尾随换行符。由调用方处理元素分隔。

适用于:

  • 消息函数:error_message()、success_message()、hint_message() 等。
  • Gutter 函数:format_with_gutter()、format_bash_with_gutter()

使用 eprintln!: 会自动添加尾随换行符。

eprintln!("{}", progress_message("Merging..."));
eprintln!("{}", format_with_gutter(&log, None));

在 Display 实现中: 使用显式换行符分隔元素。

// Pattern: leading \n separates from previous element
write!(f, "{}", error_message(...))?;           // first element, no leading \n
write!(f, "\n{}", format_with_gutter(...))?;    // gutter, separated by \n
write!(f, "\n{}", hint_message(...))            // hint, separated by \n

// For blank line between elements, add extra \n
write!(f, "\n{}\n", format_with_gutter(...))?;  // trailing \n creates blank line
write!(f, "\n{}", hint_message(...))            // hint after blank line

不要在内容末尾添加 \n:

// GOOD - eprintln! adds newline
eprintln!("{}", progress_message("Merging..."));

// BAD - double newline
eprintln!("{}", progress_message("Merging...\n"));

避免使用项目符号,改用 gutter。 不要使用 "\n - {}: {}" 进行项目符号格式化,而应使用 format_with_gutter() 来呈现列表。

错误格式化

错误消息结构

错误和警告消息应传达以下四点:

  1. 发生了什么 — 实际状态或结果
  2. 预期是什么 — 正确或期望的状态
  3. 影响是什么 — 这为何重要(显而易见时可省略)
  4. 如何解决 — 用户应采取的操作(可以通过单独的提示消息说明)
// GOOD - states actual, expected, and impact in main message
"Shell probe: wt is binary at /path, not function — won't auto-cd"
//                 ^^^^^^^^^^^^^^^   ^^^^^^^^^^^^   ^^^^^^^^^^^^^^
//                 actual            expected       impact
// Resolution in separate hint: "Restart shell to activate"

// GOOD - actual vs expected with resolution inline
"Config file has 3 errors, expected valid TOML; run wt config validate for details"
//              ^^^^^^^    ^^^^^^^^                  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
//              actual     expected                  resolution

// BAD - only states actual, no expected or impact
"Shell probe: wt resolves to binary at /path"
// Missing: what should it be instead? why does it matter?

// BAD - vague, no actionable information
"Shell integration problem detected"
// Missing: what's wrong? what should it be? what to do?

如果预期状态可从上下文中明显看出,可以不明说:

// OK - expected state (file should exist) is obvious
"Config file not found at ~/.config/wt/config.toml"

// OK - expected state (should succeed) is obvious
"Failed to read config: permission denied"

诊断消息(例如 wt config show)尤其应遵循此模式——用户会通过诊断信息了解问题所在。

单行与多行

包含变量的单行错误消息没有问题:

// GOOD - single-line with path variable
.map_err(|e| format!("Failed to read {}: {}", format_path_for_display(path), e))?

// GOOD - using .context() for simple errors
std::fs::read_to_string(&path).context("Failed to read config")?

多行外部输出(git、钩子、大模型)需要使用 gutter:

  1. 显示运行的命令(含参数)
  2. 将多行输出放入 gutter
✗ Commit generation command 'llm --model claude' failed
  Error: [Errno 8] nodename nor servname provided

// NOT: ✗ ... failed: LLM command failed: Error: [Errno 8]...

参见 src/git/error.rs 中 GitError Display 实现的相关示例。

详细输出(-v 和 -vv)

-v(详细): 面向用户的诊断输出。必须遵循这些指南。 显示模板展开结果和用户调试配置时可能需要的其他详情。

模板展开会将源模板和渲染结果显示为两个带标签的区块。每个区块都是一个 bash gutter(通过 format_bash_with_gutter 实现调暗显示和语法高亮),并带有各自的信息标题(○ 符号、加粗名称,后接 source 或 result):

○ name source
  template
○ name result
  result

这两个标题用于区分输入和输出,因此两个区块共用相同的 gutter,内容始终从 gutter 的第 2 列开始(gutter 内没有标记符号或额外缩进)。多行模板和结果遵循相同的格式,每行源内容对应一行 gutter。

expand_template 不会输出尾随空行,因此独立调用方需要添加一个空行,以便将该区块与后续内容分开(wt step eval 会在视图内容之后执行此操作)。 在命令管道中,执行器已会将该区块与后续输出分开,因此展开过程不添加任何内容。

-vv(调试): 面向开发者的日志输出。可以不遵循这些指南。 使用 log::debug!() 和结构化格式进行深入调试,不面向普通用户。

路径格式化

所有面向用户的路径都必须使用 worktrunk::path 中的 format_path_for_display()。此函数会将主目录前缀替换为 ~,以提高可读性(例如,/Users/alex/projects/repo → ~/projects/repo)。

在所有面向用户的输出中,路径前必须使用 @(而不是“at”)。 这是代码库中用于将实体与位置关联的约定,适用于状态消息、章节标题、提示以及其他所有场景:

// GOOD - @ before path
"Created worktree for feature @ ~/code/repo.feature"
"Squashed @ a1b2c3d"
"Worktree for feature @ ~/repo.feature, but cannot change directory..."
format_heading("USER HOOKS", Some(&format!("@ {}", format_path_for_display(p))))

// BAD - "at" before path
"Created worktree for feature at ~/code/repo.feature"
// BAD - heading without @
format_heading("USER HOOKS", Some(&format_path_for_display(p)))

例外: 在行文语境中(文档注释、帮助文本)使用“at”——@ 仅用于简洁输出。

use worktrunk::path::format_path_for_display;

// GOOD - uses format_path_for_display
eprintln!("{}", success_message(cformat!(
    "Created worktree @ {}",
    format_path_for_display(&worktree_path)
)));

// GOOD - error messages too
.map_err(|e| format!("Failed to read {}: {}", format_path_for_display(path), e))?

// BAD - raw path.display()
eprintln!("{}", success_message(format!(
    "Created worktree @ {}",
    worktree_path.display()  // Shows /Users/alex/... instead of ~/...
)));

适用范围:

  • 成功、信息、警告和错误消息
  • 章节标题(带路径后缀的 format_heading)
  • 提示路径的提示信息
  • 进度消息
  • 演练模式预览

例外:

  • 调试日志(log::debug!)——完整路径有助于调试
  • 传递给 git 命令的路径——必须使用真实路径

表格列对齐

  • 文本列(Branch、Path、Message):左对齐
  • 单值数值列(Age):右对齐,让 now 和 4m 按单位 对齐
  • 差异列(HEAD±、main↕):分隔符(+999 -999)两侧各有一半,均右对齐;表示整个字段状态的内容(例如加载中或同步状态标记)居中

表头跟随其内容,但差异列除外,表头会居中: 若推到任一边缘,表头就会位于其中一半上,并读作该半部分的标签, 使孤立的 +1 落在 HEAD± 左侧。

行末不带填充空格。填充只用于定位单元格;超过最后一个单元格后不再添加填充,而选择该行的读者仍会看到填充。

快照测试

每条命令的输出都必须有快照测试(tests/integration_tests/)。 请参阅 tests/integration_tests/remove.rs,了解使用 setup_snapshot_settings()、make_snapshot_cmd() 和 assert_cmd_snapshot!() 的标准模式。

覆盖成功和错误状态、有数据和无数据,以及各种标志组合。

评论

支持 Markdown