mirror of
https://github.com/aykhans/sarin.git
synced 2026-10-17 22:53:52 +00:00
306 lines
6.8 KiB
Go
306 lines
6.8 KiB
Go
package sarin
|
|
|
|
import (
|
|
"context"
|
|
"strconv"
|
|
"strings"
|
|
"sync/atomic"
|
|
"time"
|
|
|
|
"charm.land/bubbles/v2/progress"
|
|
"charm.land/bubbles/v2/spinner"
|
|
tea "charm.land/bubbletea/v2"
|
|
"charm.land/lipgloss/v2"
|
|
)
|
|
|
|
type tickMsg time.Time
|
|
|
|
var (
|
|
helpStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#d1d1d1"))
|
|
errorStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#FC5B5B")).Bold(true)
|
|
infoStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#5BC0FC")).Bold(true)
|
|
|
|
errorLabel = errorStyle.Render("ERROR: ")
|
|
infoLabel = infoStyle.Render("INFO: ")
|
|
|
|
logChannelStyle = lipgloss.NewStyle().
|
|
Border(lipgloss.ThickBorder(), false, false, false, true).
|
|
BorderForeground(lipgloss.Color("#757575")).
|
|
PaddingLeft(1).
|
|
Margin(1, 0, 0, 0).
|
|
Foreground(lipgloss.Color("#888888"))
|
|
)
|
|
|
|
// renderRuntimeLog builds a single styled log line for the TUI log box.
|
|
func renderRuntimeLog(log runtimeLog) string {
|
|
label := errorLabel
|
|
if log.level == runtimeLogLevelInfo {
|
|
label = infoLabel
|
|
}
|
|
return "[" + log.timestamp.Format("15:04:05") + "] " + label + log.text
|
|
}
|
|
|
|
// renderLogBox renders the styled log box, or "" when there are no lines.
|
|
func renderLogBox(logs []string) string {
|
|
var b strings.Builder
|
|
for i, line := range logs {
|
|
if len(line) > 0 {
|
|
b.WriteString(line)
|
|
if i < len(logs)-1 {
|
|
b.WriteString("\n")
|
|
}
|
|
}
|
|
}
|
|
if b.Len() == 0 {
|
|
return ""
|
|
}
|
|
return logChannelStyle.Render(b.String())
|
|
}
|
|
|
|
func helpLine(cancelling bool) string {
|
|
if cancelling {
|
|
return helpStyle.Render("Stopping... (Ctrl+C again to force)")
|
|
}
|
|
return helpStyle.Render("Press Ctrl+C to quit")
|
|
}
|
|
|
|
type progressModel struct {
|
|
progress progress.Model
|
|
startTime time.Time
|
|
logs []string
|
|
counter *atomic.Uint64
|
|
maxValue uint64
|
|
showBar bool
|
|
ctx context.Context //nolint:containedctx
|
|
stop func()
|
|
cancelling bool
|
|
}
|
|
|
|
func (m progressModel) Init() tea.Cmd {
|
|
return tea.Batch(progressTickCmd())
|
|
}
|
|
|
|
func (m progressModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|
switch msg := msg.(type) {
|
|
case tea.KeyPressMsg:
|
|
if msg.String() == "ctrl+c" {
|
|
m.cancelling = true
|
|
m.stop()
|
|
}
|
|
return m, nil
|
|
|
|
case tea.WindowSizeMsg:
|
|
m.progress.SetWidth(max(10, msg.Width-1))
|
|
if m.ctx.Err() != nil {
|
|
return m, tea.Quit
|
|
}
|
|
return m, nil
|
|
|
|
case runtimeLog:
|
|
m.logs = append(m.logs[1:], renderRuntimeLog(msg))
|
|
if m.ctx.Err() != nil {
|
|
return m, tea.Quit
|
|
}
|
|
return m, nil
|
|
|
|
case tickMsg:
|
|
if m.ctx.Err() != nil {
|
|
return m, tea.Quit
|
|
}
|
|
return m, progressTickCmd()
|
|
|
|
default:
|
|
if m.ctx.Err() != nil {
|
|
return m, tea.Quit
|
|
}
|
|
return m, nil
|
|
}
|
|
}
|
|
|
|
func (m progressModel) View() tea.View {
|
|
var b strings.Builder
|
|
if box := renderLogBox(m.logs); box != "" {
|
|
b.WriteString(box)
|
|
b.WriteString("\n")
|
|
}
|
|
|
|
if m.showBar {
|
|
current := m.counter.Load()
|
|
b.WriteString("\n ")
|
|
b.WriteString(strconv.FormatUint(current, 10))
|
|
b.WriteString("/")
|
|
b.WriteString(strconv.FormatUint(m.maxValue, 10))
|
|
b.WriteString(" - ")
|
|
b.WriteString(time.Since(m.startTime).Round(time.Second / 10).String())
|
|
b.WriteString("\n ")
|
|
b.WriteString(m.progress.ViewAs(float64(current) / float64(m.maxValue)))
|
|
}
|
|
|
|
b.WriteString("\n\n ")
|
|
b.WriteString(helpLine(m.cancelling))
|
|
return tea.NewView(b.String())
|
|
}
|
|
|
|
func progressTickCmd() tea.Cmd {
|
|
return tea.Tick(time.Millisecond*250, func(t time.Time) tea.Msg {
|
|
return tickMsg(t)
|
|
})
|
|
}
|
|
|
|
var infiniteProgressStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#00D4FF"))
|
|
|
|
type infiniteProgressModel struct {
|
|
spinner spinner.Model
|
|
startTime time.Time
|
|
counter *atomic.Uint64
|
|
logs []string
|
|
showBar bool
|
|
ctx context.Context //nolint:containedctx
|
|
quit bool
|
|
stop func()
|
|
cancelling bool
|
|
}
|
|
|
|
func (m infiniteProgressModel) Init() tea.Cmd {
|
|
return m.spinner.Tick
|
|
}
|
|
|
|
func (m infiniteProgressModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|
switch msg := msg.(type) {
|
|
case tea.KeyPressMsg:
|
|
if msg.String() == "ctrl+c" {
|
|
m.cancelling = true
|
|
m.stop()
|
|
}
|
|
return m, nil
|
|
|
|
case runtimeLog:
|
|
m.logs = append(m.logs[1:], renderRuntimeLog(msg))
|
|
if m.ctx.Err() != nil {
|
|
m.quit = true
|
|
return m, tea.Quit
|
|
}
|
|
return m, nil
|
|
|
|
default:
|
|
if m.ctx.Err() != nil {
|
|
m.quit = true
|
|
return m, tea.Quit
|
|
}
|
|
var cmd tea.Cmd
|
|
m.spinner, cmd = m.spinner.Update(msg)
|
|
return m, cmd
|
|
}
|
|
}
|
|
|
|
func (m infiniteProgressModel) View() tea.View {
|
|
var b strings.Builder
|
|
if box := renderLogBox(m.logs); box != "" {
|
|
b.WriteString(box)
|
|
b.WriteString("\n")
|
|
}
|
|
|
|
// Without a spinner, the view is just the log box (plus help until quit).
|
|
if !m.showBar {
|
|
if !m.quit {
|
|
b.WriteString("\n\n ")
|
|
b.WriteString(helpLine(m.cancelling))
|
|
}
|
|
return tea.NewView(b.String())
|
|
}
|
|
|
|
if m.quit {
|
|
b.WriteString("\n ")
|
|
b.WriteString(strconv.FormatUint(m.counter.Load(), 10))
|
|
b.WriteString(" ")
|
|
b.WriteString(infiniteProgressStyle.Render("∙∙∙∙∙"))
|
|
b.WriteString(" ")
|
|
b.WriteString(time.Since(m.startTime).Round(time.Second / 10).String())
|
|
b.WriteString("\n\n")
|
|
} else {
|
|
b.WriteString("\n ")
|
|
b.WriteString(strconv.FormatUint(m.counter.Load(), 10))
|
|
b.WriteString(" ")
|
|
b.WriteString(m.spinner.View())
|
|
b.WriteString(" ")
|
|
b.WriteString(time.Since(m.startTime).Round(time.Second / 10).String())
|
|
b.WriteString("\n\n ")
|
|
b.WriteString(helpLine(m.cancelling))
|
|
}
|
|
return tea.NewView(b.String())
|
|
}
|
|
|
|
func (s sarin) streamProgress(
|
|
ctx context.Context,
|
|
stopCtrl *StopController,
|
|
done chan<- struct{},
|
|
total uint64,
|
|
counter *atomic.Uint64,
|
|
logChannel <-chan runtimeLog,
|
|
showBar bool,
|
|
) {
|
|
var program *tea.Program
|
|
if total > 0 {
|
|
model := progressModel{
|
|
progress: progress.New(
|
|
progress.WithColors(lipgloss.Color("#151594"), lipgloss.Color("#00D4FF")),
|
|
progress.WithFillCharacters(progress.DefaultFullCharFullBlock, progress.DefaultEmptyCharBlock),
|
|
),
|
|
startTime: time.Now(),
|
|
logs: make([]string, 8),
|
|
counter: counter,
|
|
maxValue: total,
|
|
showBar: showBar,
|
|
ctx: ctx,
|
|
stop: stopCtrl.Stop,
|
|
}
|
|
|
|
program = tea.NewProgram(model)
|
|
} else {
|
|
model := infiniteProgressModel{
|
|
spinner: spinner.New(
|
|
spinner.WithSpinner(
|
|
spinner.Spinner{
|
|
Frames: []string{
|
|
"●∙∙∙∙",
|
|
"∙●∙∙∙",
|
|
"∙∙●∙∙",
|
|
"∙∙∙●∙",
|
|
"∙∙∙∙●",
|
|
"∙∙∙●∙",
|
|
"∙∙●∙∙",
|
|
"∙●∙∙∙",
|
|
},
|
|
FPS: time.Second / 8, //nolint:mnd
|
|
},
|
|
),
|
|
spinner.WithStyle(infiniteProgressStyle),
|
|
),
|
|
startTime: time.Now(),
|
|
counter: counter,
|
|
logs: make([]string, 8),
|
|
showBar: showBar,
|
|
ctx: ctx,
|
|
stop: stopCtrl.Stop,
|
|
quit: false,
|
|
}
|
|
|
|
program = tea.NewProgram(model)
|
|
}
|
|
|
|
stopCtrl.AttachProgram(program)
|
|
defer stopCtrl.AttachProgram(nil)
|
|
|
|
go func() {
|
|
for msg := range logChannel {
|
|
program.Send(msg)
|
|
}
|
|
}()
|
|
|
|
if _, err := program.Run(); err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
done <- struct{}{}
|
|
}
|