From 8cc90cc12a1319dd4fd4118fa2ad86f9910a61b9 Mon Sep 17 00:00:00 2001 From: Rose Hogenson Date: Mon, 7 Apr 2025 19:31:12 -0700 Subject: Initial commit --- .gitignore | 1 + ccp.go | 150 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ go.mod | 29 ++++++++++++ go.sum | 43 ++++++++++++++++++ 4 files changed, 223 insertions(+) create mode 100644 .gitignore create mode 100644 ccp.go create mode 100644 go.mod create mode 100644 go.sum diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7eac19e --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/ccp diff --git a/ccp.go b/ccp.go new file mode 100644 index 0000000..2dc59c7 --- /dev/null +++ b/ccp.go @@ -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) + } +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..af28621 --- /dev/null +++ b/go.mod @@ -0,0 +1,29 @@ +module gitlab.com/rhogenson/ccp + +go 1.24.1 + +require ( + github.com/charmbracelet/bubbles v0.20.0 + github.com/charmbracelet/bubbletea v1.3.4 + gitlab.com/rhogenson/deque v0.0.0-20250406161547-43e0f85d8030 +) + +require ( + github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect + github.com/charmbracelet/harmonica v0.2.0 // indirect + github.com/charmbracelet/lipgloss v1.0.0 // indirect + github.com/charmbracelet/x/ansi v0.8.0 // indirect + github.com/charmbracelet/x/term v0.2.1 // indirect + github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect + github.com/lucasb-eyer/go-colorful v1.2.0 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-localereader v0.0.1 // indirect + github.com/mattn/go-runewidth v0.0.16 // indirect + github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect + github.com/muesli/cancelreader v0.2.2 // indirect + github.com/muesli/termenv v0.15.2 // indirect + github.com/rivo/uniseg v0.4.7 // indirect + golang.org/x/sync v0.12.0 // indirect + golang.org/x/sys v0.31.0 // indirect + golang.org/x/text v0.23.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..8c77e10 --- /dev/null +++ b/go.sum @@ -0,0 +1,43 @@ +github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= +github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= +github.com/charmbracelet/bubbles v0.20.0 h1:jSZu6qD8cRQ6k9OMfR1WlM+ruM8fkPWkHvQWD9LIutE= +github.com/charmbracelet/bubbles v0.20.0/go.mod h1:39slydyswPy+uVOHZ5x/GjwVAFkCsV8IIVy+4MhzwwU= +github.com/charmbracelet/bubbletea v1.3.4 h1:kCg7B+jSCFPLYRA52SDZjr51kG/fMUEoPoZrkaDHyoI= +github.com/charmbracelet/bubbletea v1.3.4/go.mod h1:dtcUCyCGEX3g9tosuYiut3MXgY/Jsv9nKVdibKKRRXo= +github.com/charmbracelet/harmonica v0.2.0 h1:8NxJWRWg/bzKqqEaaeFNipOu77YR5t8aSwG4pgaUBiQ= +github.com/charmbracelet/harmonica v0.2.0/go.mod h1:KSri/1RMQOZLbw7AHqgcBycp8pgJnQMYYT8QZRqZ1Ao= +github.com/charmbracelet/lipgloss v1.0.0 h1:O7VkGDvqEdGi93X+DeqsQ7PKHDgtQfF8j8/O2qFMQNg= +github.com/charmbracelet/lipgloss v1.0.0/go.mod h1:U5fy9Z+C38obMs+T+tJqst9VGzlOYGj4ri9reL3qUlo= +github.com/charmbracelet/x/ansi v0.8.0 h1:9GTq3xq9caJW8ZrBTe0LIe2fvfLR/bYXKTx2llXn7xE= +github.com/charmbracelet/x/ansi v0.8.0/go.mod h1:wdYl/ONOLHLIVmQaxbIYEC/cRKOQyjTkowiI4blgS9Q= +github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ= +github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg= +github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= +github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= +github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= +github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= +github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= +github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= +github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI= +github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo= +github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= +github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= +github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo= +github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +gitlab.com/rhogenson/deque v0.0.0-20250406161547-43e0f85d8030 h1:ElfzUwCcfm4HHxJkMfoEn5w8VOoI+XRYfa9Vdo7Plno= +gitlab.com/rhogenson/deque v0.0.0-20250406161547-43e0f85d8030/go.mod h1:+JcaVhyJ7VgoRMo4PsMgOCDoVnIOLts4CkDEAYkU4o4= +golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw= +golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= +golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= +golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= -- cgit v1.3.1