package ui import ( "fmt" "strings" "time" "github.com/BioHazard786/Warpdrop/cli/internal/utils" "github.com/charmbracelet/bubbles/progress" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" ) // ProgressItem represents a single file transfer progress type ProgressItem struct { ID int Name string Total int64 Current int64 StartTime time.Time Started bool Speed float64 IsComplete bool HasError bool ErrorMsg string } // ProgressModel handles multiple file progress bars type ProgressModel struct { items []*ProgressItem progresses []progress.Model width int } // NewProgressModel creates a new multi-file progress model func NewProgressModel(fileNames []string, fileSizes []int64) ProgressModel { items := make([]*ProgressItem, len(fileNames)) progresses := make([]progress.Model, len(fileNames)) for i := range fileNames { items[i] = &ProgressItem{ ID: i, Name: fileNames[i], Total: fileSizes[i], } p := progress.New( progress.WithGradient(ProgressStart, ProgressEnd), progress.WithWidth(30), progress.WithoutPercentage(), ) progresses[i] = p } return ProgressModel{ items: items, progresses: progresses, width: 80, } } func (m ProgressModel) Init() tea.Cmd { return tickCmd() } type TickMsg time.Time // ProgressMsg updates the progress of a specific file type ProgressMsg struct { ID int Current int64 } // ProgressCompleteMsg marks a file as complete type ProgressCompleteMsg struct { ID int } // ProgressErrorMsg marks a file as errored type ProgressErrorMsg struct { ID int Err error } func tickCmd() tea.Cmd { return tea.Tick(100*time.Millisecond, func(t time.Time) tea.Msg { return TickMsg(t) }) } // AllComplete returns true if all files are complete func (m ProgressModel) AllComplete() bool { for _, item := range m.items { if !item.IsComplete && !item.HasError { return false } } return true } func (m ProgressModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { case TickMsg: if m.AllComplete() { return m, tea.Quit } return m, tickCmd() case tea.KeyMsg: if msg.Type == tea.KeyCtrlC { return m, tea.Quit } return m, nil case tea.WindowSizeMsg: m.width = msg.Width for i := range m.progresses { m.progresses[i].Width = min(30, msg.Width-50) } return m, nil case progress.FrameMsg: var cmds []tea.Cmd for i := range m.progresses { var cmd tea.Cmd newModel, cmd := m.progresses[i].Update(msg) m.progresses[i] = newModel.(progress.Model) cmds = append(cmds, cmd) } return m, tea.Batch(cmds...) case ProgressMsg: if msg.ID >= 0 && msg.ID < len(m.items) { item := m.items[msg.ID] if !item.Started && msg.Current > 0 { item.Started = true item.StartTime = time.Now() } if item.Started { elapsed := time.Since(item.StartTime).Seconds() if elapsed > 0 { item.Speed = float64(msg.Current) / elapsed } } item.Current = msg.Current if item.Current >= item.Total { item.IsComplete = true } } return m, nil case ProgressCompleteMsg: if msg.ID >= 0 && msg.ID < len(m.items) { m.items[msg.ID].IsComplete = true m.items[msg.ID].Current = m.items[msg.ID].Total } if m.AllComplete() { return m, tea.Quit } return m, nil case ProgressErrorMsg: if msg.ID >= 0 && msg.ID < len(m.items) { m.items[msg.ID].HasError = true m.items[msg.ID].ErrorMsg = msg.Err.Error() } if m.AllComplete() { return m, tea.Quit } return m, nil } return m, nil } func (m ProgressModel) View() string { var b strings.Builder for i, item := range m.items { var icon string var nameStyle lipgloss.Style if item.HasError { icon = IconError nameStyle = ErrorStyle } else if item.IsComplete { icon = IconSuccess nameStyle = SuccessStyle } else { icon = IconFile nameStyle = lipgloss.NewStyle() } name := utils.TruncateString(item.Name, 30) b.WriteString(fmt.Sprintf("%s %s ", icon, nameStyle.Render(name))) if item.Total > 0 { percent := float64(item.Current) / float64(item.Total) b.WriteString(m.progresses[i].ViewAs(percent)) } if item.Total > 0 { percent := float64(item.Current) / float64(item.Total) * 100 b.WriteString(fmt.Sprintf(" %5.1f%%", percent)) } if !item.IsComplete && !item.HasError && item.Speed > 0 { b.WriteString(MutedStyle.Render(fmt.Sprintf(" %s", utils.FormatSpeed(item.Speed)))) remaining := item.Total - item.Current if remaining > 0 && item.Speed > 0 { etaSeconds := float64(remaining) / item.Speed b.WriteString(MutedStyle.Render(fmt.Sprintf(" ETA: %s", utils.FormatTimeDuration(time.Duration(etaSeconds*float64(time.Second)))))) } } b.WriteString(MutedStyle.Render(fmt.Sprintf(" (%s/%s)", utils.FormatSize(item.Current), utils.FormatSize(item.Total)))) b.WriteString("\n") } return b.String() } // GetTotalProgress returns overall progress information func (m ProgressModel) GetTotalProgress() (percent float64, current, total int64, speed float64) { var totalSpeed float64 for _, item := range m.items { current += item.Current total += item.Total if !item.IsComplete { totalSpeed += item.Speed } } if total > 0 { percent = float64(current) / float64(total) * 100 } return percent, current, total, totalSpeed }