Files
HxClaw/cmd/hxclaw/internal/spinner.go
titor bdad44de7d
Some checks failed
Release / build (push) Failing after 22s
feat: 优化流式输出,添加按行延迟和配置化
- 使用 ProcessDirect 替代 ChatStream,支持工具调用结果显示
- 新增 project.config.yml 统一配置(Logo、用户前缀、流式延迟、Markdown等)
- Markdown 渲染支持自动终端宽度换行
- 按行输出文本,每行延迟可配置
- 简化状态栏,只显示耗时(图标颜色 #f0c75e,文字颜色 #2b2e32)
- spinner 动画右移两个字符
- 用户输入前缀可配置化
2026-04-15 06:13:52 +08:00

91 lines
1.4 KiB
Go

package internal
import (
"fmt"
"os"
"time"
"charm.land/bubbles/v2/spinner"
"charm.land/lipgloss/v2"
)
type SpinnerState int
const (
StateThinking SpinnerState = iota
StateAnswering
StateDone
)
type Spinner struct {
text string
state SpinnerState
spinner spinner.Model
stopCh chan struct{}
doneCh chan struct{}
}
func NewSpinner(text string) *Spinner {
s := spinner.New(
spinner.WithSpinner(spinner.MiniDot),
spinner.WithStyle(lipgloss.NewStyle().Foreground(lipgloss.Color("243"))),
)
return &Spinner{
text: text,
state: StateThinking,
spinner: s,
stopCh: make(chan struct{}),
doneCh: make(chan struct{}),
}
}
func (s *Spinner) Start() {
go s.run()
s.tick()
}
func (s *Spinner) Update(text string) {
s.text = text
s.state = StateAnswering
}
func (s *Spinner) Stop() {
close(s.stopCh)
<-s.doneCh
}
func (s *Spinner) tick() {
msg := s.spinner.Tick()
if msg, ok := msg.(spinner.TickMsg); ok {
s.spinner, _ = s.spinner.Update(msg)
}
}
func (s *Spinner) run() {
defer close(s.doneCh)
ticker := time.NewTicker(time.Second / 12)
defer ticker.Stop()
for {
select {
case <-ticker.C:
s.tick()
s.render()
case <-s.stopCh:
s.clear()
return
}
}
}
func (s *Spinner) render() {
fmt.Printf("\r %s %s", s.spinner.View(), s.text)
os.Stdout.Sync()
}
func (s *Spinner) clear() {
fmt.Printf("\r %s 思考完成.\n", s.spinner.View())
os.Stdout.Sync()
}