216 lines
4.1 KiB
Go
216 lines
4.1 KiB
Go
package tui
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"strings"
|
|
"time"
|
|
|
|
"git.jingrow.com/charmbracelet/bubbles/progress"
|
|
tea "git.jingrow.com/charmbracelet/bubbletea"
|
|
"git.jingrow.com/charmbracelet/lipgloss"
|
|
)
|
|
|
|
var (
|
|
progressHelpStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#626262")).Render
|
|
)
|
|
|
|
const (
|
|
progressPadding = 2
|
|
progressMaxWidth = 80
|
|
)
|
|
|
|
type progressMsg struct {
|
|
ratio float64
|
|
uploadedGB string
|
|
totalGB string
|
|
}
|
|
|
|
type progressErrMsg struct{ err error }
|
|
|
|
type progressModel struct {
|
|
prompt string
|
|
progress progress.Model
|
|
err error
|
|
currentInfo string
|
|
quitting bool
|
|
onQuit func()
|
|
}
|
|
|
|
func finalPause() tea.Cmd {
|
|
return tea.Tick(time.Millisecond*750, func(_ time.Time) tea.Msg {
|
|
return nil
|
|
})
|
|
}
|
|
|
|
func (m progressModel) Init() tea.Cmd {
|
|
return tea.Batch(
|
|
tea.EnterAltScreen,
|
|
tea.HideCursor,
|
|
m.progress.Init(),
|
|
)
|
|
}
|
|
|
|
func (m progressModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|
switch msg := msg.(type) {
|
|
case tea.KeyMsg:
|
|
if msg.Type == tea.KeyCtrlQ {
|
|
m.quitting = true
|
|
if m.onQuit != nil {
|
|
m.onQuit()
|
|
}
|
|
return m, tea.Batch(
|
|
tea.ExitAltScreen,
|
|
tea.ShowCursor,
|
|
tea.Quit,
|
|
)
|
|
}
|
|
return m, nil
|
|
|
|
case tea.WindowSizeMsg:
|
|
m.progress.Width = msg.Width - progressPadding*2 - 4
|
|
if m.progress.Width > progressMaxWidth {
|
|
m.progress.Width = progressMaxWidth
|
|
}
|
|
return m, nil
|
|
|
|
case progressErrMsg:
|
|
m.err = msg.err
|
|
return m, nil
|
|
|
|
case progressMsg:
|
|
var cmds []tea.Cmd
|
|
if msg.ratio >= 1.0 {
|
|
m.quitting = true
|
|
cmds = append(cmds, tea.Sequence(
|
|
finalPause(),
|
|
tea.ExitAltScreen,
|
|
tea.ShowCursor,
|
|
tea.Quit,
|
|
))
|
|
}
|
|
|
|
m.currentInfo = fmt.Sprintf("%s / %s", msg.uploadedGB, msg.totalGB)
|
|
cmds = append(cmds, m.progress.SetPercent(msg.ratio))
|
|
return m, tea.Batch(cmds...)
|
|
|
|
case progress.FrameMsg:
|
|
progressModel, cmd := m.progress.Update(msg)
|
|
m.progress = progressModel.(progress.Model)
|
|
return m, cmd
|
|
|
|
default:
|
|
return m, nil
|
|
}
|
|
}
|
|
|
|
func (m progressModel) View() string {
|
|
if m.quitting {
|
|
return ""
|
|
}
|
|
|
|
if m.err != nil {
|
|
return "\n " + titleStyle.Render(m.prompt) + "\n\n Error: " + m.err.Error() + "\n\n " +
|
|
progressHelpStyle("press <ctrl+q> to quit") + "\n"
|
|
}
|
|
|
|
pad := strings.Repeat(" ", progressPadding)
|
|
progressView := m.progress.View()
|
|
|
|
return "\n " + titleStyle.Render(m.prompt) + "\n\n" +
|
|
pad + progressView + "\n" +
|
|
pad + m.currentInfo + "\n\n" +
|
|
pad + progressHelpStyle("press <ctrl+q> to quit") + "\n"
|
|
}
|
|
|
|
type ProgressUI struct {
|
|
updateChan chan progressMsg
|
|
errChan chan error
|
|
DoneChan chan struct{}
|
|
program *tea.Program
|
|
}
|
|
|
|
func ShowProgress(prompt string, onQuit func()) *ProgressUI {
|
|
prog := progress.New(
|
|
progress.WithGradient("#FF87D7", "#FF87D7"),
|
|
)
|
|
|
|
m := progressModel{
|
|
prompt: prompt,
|
|
progress: prog,
|
|
onQuit: onQuit,
|
|
}
|
|
|
|
p := tea.NewProgram(m)
|
|
|
|
updateChan := make(chan progressMsg)
|
|
errChan := make(chan error)
|
|
doneChan := make(chan struct{})
|
|
|
|
go func() {
|
|
if _, err := p.Run(); err != nil {
|
|
// Ensure terminal state is restored on error
|
|
fmt.Fprint(os.Stderr, "\033[?25h\033[?1049l")
|
|
}
|
|
close(doneChan)
|
|
}()
|
|
|
|
go func() {
|
|
for {
|
|
select {
|
|
case msg := <-updateChan:
|
|
p.Send(msg)
|
|
case err := <-errChan:
|
|
p.Send(progressErrMsg{err})
|
|
case <-doneChan:
|
|
return
|
|
}
|
|
}
|
|
}()
|
|
|
|
return &ProgressUI{
|
|
updateChan: updateChan,
|
|
errChan: errChan,
|
|
DoneChan: doneChan,
|
|
program: p,
|
|
}
|
|
}
|
|
|
|
func (p *ProgressUI) Update(ratio float64, uploadedGB, totalGB string) {
|
|
select {
|
|
case <-p.DoneChan:
|
|
return
|
|
default:
|
|
p.updateChan <- progressMsg{
|
|
ratio: ratio,
|
|
uploadedGB: uploadedGB,
|
|
totalGB: totalGB,
|
|
}
|
|
}
|
|
}
|
|
|
|
func (p *ProgressUI) Error(err error) {
|
|
select {
|
|
case <-p.DoneChan:
|
|
return
|
|
default:
|
|
p.errChan <- err
|
|
}
|
|
}
|
|
|
|
func (p *ProgressUI) Done() {
|
|
// Send Ctrl+Q to quit
|
|
p.program.Send(tea.KeyMsg{Type: tea.KeyCtrlQ})
|
|
|
|
// Add timeout to prevent hanging
|
|
select {
|
|
case <-p.DoneChan:
|
|
case <-time.After(2 * time.Second):
|
|
// Force cleanup if graceful quit times out
|
|
fmt.Fprint(os.Stdout, "\033[?25h\033[?1049l")
|
|
}
|
|
|
|
// Extra terminal reset to be safe
|
|
fmt.Fprint(os.Stdout, "\033[?25h\033[?1049l")
|
|
}
|