- 开发工具
【免费下载链接】pterm
✨ PTerm is a modern Go module to easily beautify console output. Featuring charts, progressbars, tables, trees, text input, select menus and much more 🚀 It's completely configurable and 100% cross-platform compatible.
本指南围绕开源仓库 pterm 的_examples/spinner文档展开,系统讲解 Go 终端应用中最常用的SpinnerPrinter加载动画组件:从一行代码启动旋转动画,到用 Info/Success/Warning/Fail 把动画行“结算”为带前缀的结果消息,再到自定义动画帧、实时更新文案,以及借助MultiPrinter同时运行多个 spinner。读完本文,你将能独立为 CLI 工具接入进度反馈与任务状态回执,并理解其背后的动画循环、Writer 重定向与原始输出等底层机制。
文档概览:三个递进的实战示例
_examples/spinner/README.md按使用复杂度编排了三个独立示例,每个示例文件夹都包含可直接运行的main.go与说明文档:
| 示例目录 | 核心技术点 |
|---|---|
| _examples/spinner/demo | spinner 的启动、四种状态回执(Info/Success/Warning/Fail)、自定义回执打印机、运行中实时改文案 |
| _examples/spinner/custom | 自定义动画帧序列(Sequence)、动画样式(Style) |
| _examples/spinner/multiple | 用MultiPrinter同时运行多个 spinner,并逐个独立回执 |
三个示例可以直接在各自目录下运行:
cd _examples/spinner/demo && go run main.go cd _examples/spinner/custom && go run main.go cd _examples/spinner/multiple && go run main.go认识 SpinnerPrinter 与其默认配置
Spinner 在 pterm 中的实现是SpinnerPrinter——一个“未知进度”场景下的加载动画:动画循环可以携带文字,并在结束时支持抛出错误或警告。官方文档将其定义为"animation loop, which can have a text and supports throwing errors or warnings"。
直接使用的入口是包级变量DefaultSpinner,其默认配置定义在 spinner_printer.go 的 L14-L28,包含了你日常最关心的全部默认值:
var DefaultSpinner = SpinnerPrinter{ Sequence: []string{"⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"}, Style: &ThemeDefault.SpinnerStyle, // 动画符号样式,默认 FgLightCyan Delay: time.Millisecond * 100, // 每帧间隔 100ms ShowTimer: true, // 默认显示运行时长 TimerRoundingFactor: time.Second, // 计时取整到秒 TimerStyle: &ThemeDefault.TimerStyle, // 计时器样式,默认 FgGray MessageStyle: &ThemeDefault.SpinnerTextStyle, // 文案样式,默认 FgLightWhite InfoPrinter: &Info, // 四种状态回执打印机 SuccessPrinter: &Success, FailPrinter: &Error, WarningPrinter: &Warning, Writer: os.Stderr, // 默认输出到 stderr }从源码结构看,SpinnerPrinter的字段(spinner_printer.go L33-L67)决定了它的全部行为:Sequence是动画帧集合、Delay是帧间隔、Style负责给动画符号着色、MessageStyle负责文案、ShowTimer与TimerRoundingFactor控制计时显示、四个*Printer字段负责"结算"时的最终输出,而Writer则决定了动画写到哪个输出流。
状态回执背后的 PrefixPrinter 机制
Info、Success、Warning、Error这四者本质都是PrefixPrinter——一个带"前缀徽章"的文本打印机。以Info为例,它带有" INFO "前缀文本和对应配色;Success对应"SUCCESS"、Warning对应"WARNING"、Error对应" ERROR "。这也是spinner/demo注释中"resolve printers are plain PrefixPrinters, so they can be swapped out"(回执打印机就是普通 PrefixPrinter,所以可以被替换)的由来。
基础用法:启动动画并做状态回执
spinner/demo展示了 spinner 最核心的生命周期——启动、等待、回执。动画行在回执发生时被对应的前缀打印机输出整行替换,而不是简单消失。完整示例见 _examples/spinner/demo/main.go:
package main import ( "time" "github.com/pterm/pterm" ) func main() { // A spinner can resolve as Info, Success, Warning or Fail. The spinner // line is replaced by the matching prefix printer output. spinnerInfo, _ := pterm.DefaultSpinner.Start("Some informational action...") time.Sleep(time.Second * 2) spinnerInfo.Info() spinnerSuccess, _ := pterm.DefaultSpinner.Start("Doing something important... (will succeed)") time.Sleep(time.Second * 2) spinnerSuccess.Success() spinnerWarning, _ := pterm.DefaultSpinner.Start("Doing something important... (will warn)") time.Sleep(time.Second * 2) spinnerWarning.Warning() spinnerFail, _ := pterm.DefaultSpinner.Start("Doing something important... (will fail)") time.Sleep(time.Second * 2) spinnerFail.Fail() // ...(见下文:自定义回执与实时更新) }这里有几个值得注意的调用细节:
Start(text ...any)接收可变参数作为初始文案;返回值中第二个是 error,示例中统一用_忽略;Info()、Success()、Warning()、Fail()四个回执方法不传参时会自动复用 spinner 当前文案作为消息(见 spinner_printer.go L382-L448 中各方法的if len(message) == 0 { message = []any{s.Text} }逻辑),传入参数则覆盖文案;- 每次回执都会调用
fClearLine清空当前行,再打印前缀消息,最后内部调用Stop()终止动画 goroutine; - 测试 spinner_printer_test.go 的 TestSpinnerPrinter_ResultPrinters 验证了四种回执的最终输出行必须携带对应前缀(INFO/SUCCESS/WARNING/ERROR)与消息文本,而 TestSpinnerPrinter_ResultPrinterReusesTextAsMessage 则验证了"不传参复用 spinner 文本"的行为。
自定义回执打印机:NOCHG 示例
由于回执打印机只是PrefixPrinter,你可以在运行时直接替换。spinner/demo后半段演示了把InfoPrinter换成自定义的" NOCHG "徽章:
// The resolve printers are plain PrefixPrinters, so they can be swapped // out. Here Info resolves with a custom "NOCHG" prefix instead. spinnerNochange, _ := pterm.DefaultSpinner.Start("Checking something important... (will result in no change)") spinnerNochange.InfoPrinter = &pterm.PrefixPrinter{ MessageStyle: &pterm.Style{pterm.FgLightBlue}, Prefix: pterm.Prefix{ Style: &pterm.Style{pterm.FgBlack, pterm.BgLightBlue}, Text: " NOCHG ", }, } time.Sleep(time.Second * 2) spinnerNochange.Info("No changes were required")这里的pterm.Prefix结构体只有两个字段——Text与Style(见 prefix_printer.go L366-L370),分别定义前缀文本和它的前景/背景样式。pterm.NewStyle(color.go L270-L275)接受任意多个Color组合成一个Style,因此可以像示例中那样把"黑字蓝底"的前缀样式和"浅蓝"的消息样式组合起来。这也印证了文档注释:spinner 的四类回执是开放的、可完全定制的。
运行中实时更新文案:UpdateText
动画运行期间,文案可以随时更换而动画不中断。spinner/demo末尾的spinnerLiveText演示了这一点:
// The text can be updated while the spinner keeps running. spinnerLiveText, _ := pterm.DefaultSpinner.Start("Doing a lot of stuff...") time.Sleep(time.Second) spinnerLiveText.UpdateText("It's really much") time.Sleep(time.Second) spinnerLiveText.UpdateText("We're nearly done!") time.Sleep(time.Second) spinnerLiveText.Success("Finally!")UpdateText(text string)的实现位于 spinner_printer.go L204-L220:它在锁内更新Text字段,然后立即用Fprinto把当前动画帧与最新文案整行重绘("\033[K"清行符 + 样式化输出)。测试 TestSpinnerPrinter_UpdateTextReplacesText 验证了新文案会替换旧文案、动画帧符号保持不变。
自定义动画:帧序列与样式(spinner/custom)
spinner/custom示例传达了一个关键设计理念:spinner 动画本质上只是一串按顺序循环打印的字符串帧,因此任何字符串集合都可以当作动画。完整代码见 _examples/spinner/custom/main.go:
package main import ( "time" "github.com/pterm/pterm" ) func main() { // The spinner animation is just a sequence of frames, so any set of // strings works. WithStyle colors the animation. spinner, _ := pterm.DefaultSpinner. WithSequence("▁", "▃", "▅", "▇", "▅", "▃"). WithStyle(pterm.NewStyle(pterm.FgCyan)). Start("Uploading assets...") time.Sleep(time.Second * 2) // The text can change while the spinner keeps running. spinner.UpdateText("Finalizing upload...") time.Sleep(time.Second * 2) spinner.Success("Upload complete") }两个关键配置方法:
WithSequence(sequence ...string)(spinner_printer.go L123-L127):传入任意帧字符串。默认的 10 帧 Braille 字符⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏也仅仅是默认值,你可以用▁▃▅▇▅▃这类字符拼出自己的"呼吸条",甚至使用普通文本帧;WithStyle(style *Style)(spinner_printer.go L129-L133):给动画帧着色,示例中使用pterm.FgCyan(青色)。
WithSequence的参数是可变长,意味着你可以自由决定动画"有多少帧、循环多长"。动画循环的核心逻辑在 spinner_printer.go 的 runAnimation:外层循环持续检查IsActive,内层遍历Sequence逐帧打印,每帧之间以Delay间隔(默认 100ms)睡眠;若启用了ShowTimer,还会在帧后追加(运行时长)计时文本(默认按秒取整,见 TestSpinnerPrinter_ShowTimerRendersElapsedTime 中\(1m3\ds\)的正则断言)。
多 spinner 并发:MultiPrinter(spinner/multiple)
当 CLI 需要同时展示多个任务(如并行下载多个文件)时,使用MultiPrinter。示例见 _examples/spinner/multiple/main.go:
package main import ( "time" "github.com/pterm/pterm" ) func main() { // A MultiPrinter lets several spinners run at the same time. Each spinner // writes to its own writer obtained from the multi printer. multi := pterm.DefaultMultiPrinter spinner1, _ := pterm.DefaultSpinner.WithWriter(multi.NewWriter()).Start("Spinner 1") spinner2, _ := pterm.DefaultSpinner.WithWriter(multi.NewWriter()).Start("Spinner 2") spinner3, _ := pterm.DefaultSpinner.WithWriter(multi.NewWriter()).Start("Spinner 3") multi.Start() // Each spinner can resolve on its own while the others keep spinning. time.Sleep(time.Millisecond * 1000) spinner1.Success("Spinner 1 is done!") time.Sleep(time.Millisecond * 750) spinner2.Fail("Spinner 2 failed!") time.Sleep(time.Millisecond * 500) spinner3.Warning("Spinner 3 has a warning!") multi.Stop() }MultiPrinter 的工作原理
从源码看,MultiPrinter(multi_live_printer.go L49-L69)的关键机制是缓冲区 + 定时重绘:
NewWriter()(multi_live_printer.go L103-L113)为每个子 spinner 分配一个线程安全的multiPrinterBuffer,并把该 buffer 作为 spinner 的Writer。也就是说,每个 spinner 的动画输出先写进各自的缓冲区,而不是直接上屏;Start()(multi_live_printer.go L142-L178)启动所有已注册的 live printer,并启动一个按UpdateDelay(默认 200ms,见 DefaultMultiPrinter 定义 L14-L22)周期执行的调度任务,把各缓冲区的最终帧合并后交给底层的AreaPrinter整体重绘;- 各 spinner 的回执(Success/Fail/Warning)独立触发,彼此互不影响,最后调用
multi.Stop()(multi_live_printer.go L180-L208)停止重绘任务、终止所有子 printer 并做一次最终重绘,让所有回执结果同时可见。
因为SpinnerPrinter实现了LivePrinter接口(GenericStart/GenericStop/SetWriter,见 interface_live_printer.go),它才能被MultiPrinter统一调度——这也是 pterm 中 spinner 与 progressbar 可以混跑在同一块多任务区域的接口基础。
常用配置项速查
下表汇总了SpinnerPrinter的常用可配置项,均来自 spinner_printer.go 的 With 系列方法:
| 配置方法 | 作用 | 默认值(来源:DefaultSpinner) |
|---|---|---|
WithText(text) | 设置初始文案 | 无(由 Start 参数传入) |
WithSequence(seq...) | 设置动画帧序列 | ⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏共 10 帧 |
WithStyle(style) | 动画帧着色 | ThemeDefault.SpinnerStyle(FgLightCyan) |
WithDelay(delay) | 帧间隔时长 | 100ms |
WithMessageStyle(style) | 文案样式 | ThemeDefault.SpinnerTextStyle(FgLightWhite) |
WithShowTimer(b...) | 是否显示运行时长 | true |
WithTimerRoundingFactor(factor) | 计时取整粒度 | time.Second |
WithTimerStyle(style) | 计时文本样式 | ThemeDefault.TimerStyle(FgGray) |
WithRemoveWhenDone(b...) | 结束后清空整行(不留任何内容) | false |
WithWriter(writer) | 指定输出流 | os.Stderr |
WithStartedAt(t) | 手动设置计时起点 | 启动时刻time.Now() |
其中WithRemoveWhenDone的行为差异在测试中有明确刻画:开启时Stop会用 80 个空格抹掉整行并把光标归位(TestSpinnerPrinter_RemoveWhenDoneClearsFrame);关闭时保留最后一帧并以换行收尾(TestSpinnerPrinter_StopKeepsLastFrameWithoutRemoveWhenDone)。相关主题色(SpinnerStyle、SpinnerTextStyle、TimerStyle)都定义在默认主题 theme.go 的 ThemeDefault 中,可整体换肤。
两个值得注意的底层细节
1. 原始输出模式(RawOutput)
当调用pterm.DisableStyling()(见 pterm.go L74-L83)时,库进入 RawOutput 模式:runAnimation与UpdateText都会退化为逐行打印纯文本、不再输出任何 ANSI 转义码,Stop也能正确停用动画(见 TestSpinnerPrinter_RawOutput 对"raw 模式不包含\x1b[转义序列"的断言)。这意味着 spinner 的输出可以被安全地重定向到文本文件等不支持终端控制序列的目标。
2. 活动 spinner 的注册表
Start()会把 spinner 注册进包级切片activeSpinnerPrinters(见 sync.go L93-L118 的registerSpinner与snapshotSpinners),供其他组件并发巡检活跃 spinner 状态,且所有访问都在锁保护下完成;Stop则通过stopCh信号与WaitGroup确保动画 goroutine 完全退出后再返回,避免 goroutine 泄漏(spinner_printer.go L314-L360 的注释说明了这一同步设计)。
小结
从_examples/spinner/README.md的三个示例出发,本文覆盖了 pterm spinner 的完整使用面:用DefaultSpinner.Start启动动画,用Info/Success/Warning/Fail做状态回执,通过替换PrefixPrinter定制回执徽章,用UpdateText实时更新文案,用WithSequence/WithStyle自定义动画外观,以及用MultiPrinter实现多任务并行渲染。对照源码可以发现,这些看似简单的 API 背后是"帧序列循环 + 行内重绘 + Writer 缓冲 + 定时合并"的完整动画管线,理解它有助于你在真实 CLI 工程中正确地控制输出流、处理并发任务并适配无终端的输出环境。
- 开发工具
【免费下载链接】pterm
✨ PTerm is a modern Go module to easily beautify console output. Featuring charts, progressbars, tables, trees, text input, select menus and much more 🚀 It's completely configurable and 100% cross-platform compatible.
相关推荐
pterm 多进度条并行显示实战:基于 MultiPrinter 与 Progressbar 的终端多任务进度渲染
pterm 多进度条并行显示实战:基于 MultiPrinter 与 Progressbar 的终端多任务进度渲染 本文围绕 pterm 官方示例 progre
开发工具pterm MultiPrinter 实战:让多个 Spinner 并发运行并各自独立收尾
pterm MultiPrinter 实战:让多个 Spinner 并发运行并各自独立收尾 本篇围绕 _examples/spinner/multiple 这一
开发工具BootstrapBlazor加载中状态:Spinner组件使用场景与实战指南
BootstrapBlazor加载中状态:Spinner组件使用场景与实战指南 你还在为加载状态设计烦恼?一文掌握Spinner全场景应用 在现代Web应用开发
UI组件前端
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考