diff options
| author | Rose Hogenson <rosehogenson@posteo.net> | 2025-04-07 19:31:12 -0700 |
|---|---|---|
| committer | Rose Hogenson <rosehogenson@posteo.net> | 2025-04-07 19:31:12 -0700 |
| commit | 8cc90cc12a1319dd4fd4118fa2ad86f9910a61b9 (patch) | |
| tree | 6f5df946cd827f0a43770a74ce3861efd485aa46 /ccp.go | |
| download | ccp-8cc90cc12a1319dd4fd4118fa2ad86f9910a61b9.tar.zst | |
Initial commit
Diffstat (limited to 'ccp.go')
| -rw-r--r-- | ccp.go | 150 |
1 files changed, 150 insertions, 0 deletions
@@ -0,0 +1,150 @@ +package main + +import ( + "errors" + "flag" + "fmt" + "io" + "os" + "time" + + "github.com/charmbracelet/bubbles/progress" + tea "github.com/charmbracelet/bubbletea" + "gitlab.com/rhogenson/deque" +) + +type measurement struct { + t time.Time + i int64 +} + +type model struct { + progress progress.Model + + max int64 + measurements deque.Deque[measurement] +} + +func (m *model) Init() tea.Cmd { + return nil +} + +type ( + progressMsg int + doneMsg struct{} +) + +func (m *model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + switch msg := msg.(type) { + case tea.WindowSizeMsg: + m.progress.Width = msg.Width - 4 + return m, nil + case progressMsg: + n := int64(msg) + if m.measurements.Len() > 0 { + n += m.measurements.At(m.measurements.Len() - 1).i + } + if m.measurements.Len() > 0 && m.measurements.At(m.measurements.Len()-1).i > n { + m.measurements.Reset() + } + now := time.Now() + for m.measurements.Len() > 2 && now.Sub(m.measurements.At(0).t) > 2*time.Minute { + m.measurements.PopFront() + } + m.measurements.PushBack(measurement{now, n}) + return m, m.progress.SetPercent(float64(n) / float64(m.max)) + case doneMsg: + return m, tea.Sequence( + m.progress.SetPercent(1), + tea.Tick(500*time.Millisecond, func(time.Time) tea.Msg { return nil }), + tea.Quit) + // FrameMsg is sent when the progress bar wants to animate itself + case progress.FrameMsg: + progressModel, cmd := m.progress.Update(msg) + m.progress = progressModel.(progress.Model) + return m, cmd + default: + return m, nil + } +} + +func (m *model) View() string { + etaStr := "calculating..." + if m.measurements.Len() > 1 { + first := m.measurements.At(0) + last := m.measurements.At(m.measurements.Len() - 1) + deltaT := last.t.Sub(first.t) + delta := last.i - first.i + if delta != 0 { + etaStr = time.Duration(float64(m.max-last.i) / float64(delta) * float64(deltaT)).Round(time.Second).String() + } + } + return "\n" + + " " + m.progress.View() + "\n" + + " " + "ETA: " + etaStr + "\n" +} + +func copy(out, in *os.File, progress func(int64)) error { + for { + n, err := io.CopyN(out, in, 1024*1024) + if n > 0 { + progress(n) + } + if err != nil { + if err == io.EOF { + break + } + return err + } + } + if err := out.Close(); err != nil { + return err + } + return nil +} + +func run() error { + args := flag.Args() + if len(args) != 2 { + return errors.New("usage error") + } + in, err := os.Open(args[0]) + if err != nil { + return err + } + defer in.Close() + stat, err := in.Stat() + if err != nil { + return err + } + out, err := os.OpenFile(args[1], os.O_WRONLY|os.O_CREATE|os.O_TRUNC, stat.Mode().Perm()) + if err != nil { + return err + } + defer out.Close() + p := tea.NewProgram(&model{ + progress: progress.New(progress.WithDefaultGradient(), progress.WithoutPercentage()), + max: stat.Size() + 1, + }, tea.WithInput(nil), tea.WithOutput(os.Stderr)) + var copyErr error + go func() { + if copyErr = copy(out, in, func(n int64) { p.Send(progressMsg(n)) }); copyErr != nil { + p.Send(tea.QuitMsg{}) + return + } + p.Send(doneMsg{}) + }() + _, teaErr := p.Run() + if copyErr != nil { + return copyErr + } + return teaErr +} + +func main() { + flag.Parse() + + if err := run(); err != nil { + fmt.Fprintln(os.Stderr, err) + } +} |
