From 3c62a15faade2056330c5b7a7915080d0a12febb Mon Sep 17 00:00:00 2001 From: Stephen Solka Date: Sun, 25 Oct 2020 12:13:53 -0400 Subject: [PATCH] code clean up and attempt to render animation --- main.go | 132 ++++++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 118 insertions(+), 14 deletions(-) diff --git a/main.go b/main.go index ab59548..1080296 100644 --- a/main.go +++ b/main.go @@ -1,14 +1,19 @@ package main import ( + "context" "fmt" "image" + "image/gif" _ "image/jpeg" _ "image/png" "io" + "mime" "net/http" "os" + "path/filepath" "strings" + "time" tea "github.com/charmbracelet/bubbletea" "github.com/lucasb-eyer/go-colorful" @@ -51,6 +56,8 @@ type model struct { image string height uint err error + + cancelAnimation context.CancelFunc } func (m model) Init() tea.Cmd { @@ -91,25 +98,100 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.err = msg return m, nil case loadMsg: - url := m.urls[m.selected] - if msg.resp != nil { - defer msg.resp.Body.Close() - img, err := readerToImage(m.height, url, msg.resp.Body) - if err != nil { - return m, func() tea.Msg { return errMsg{err} } + return handleLoadMsg(m, msg) + case gifMsg: + return handleGifMsg(m, msg) + } + return m, nil +} + +func handleGifMsg(m model, msg gifMsg) (model, tea.Cmd) { + m.image = msg.frames[msg.frame] + return m, func() tea.Msg { + nextFrame := msg.frame + 1 + if nextFrame == len(msg.gif.Image) { + nextFrame = 0 + } + select { + case <-msg.ctx.Done(): + return nil + case <-time.After(time.Duration(msg.gif.Delay[nextFrame]*10) * time.Millisecond): + return gifMsg{ + ctx: msg.ctx, + gif: msg.gif, + frames: msg.frames, + frame: nextFrame, } - m.image = img - return m, nil } - defer msg.file.Close() - img, err := readerToImage(m.height, url, msg.file) + + } +} + +func handleLoadMsg(m model, msg loadMsg) (model, tea.Cmd) { + if m.cancelAnimation != nil { + m.cancelAnimation() + } + + // blank out image so it says "loading..." + m.image = "" + + selected := m.urls[m.selected] + ext := filepath.Ext(selected) + t := mime.TypeByExtension(ext) + if strings.Contains(t, "gif") { + return handleLoadMsgAnimation(m, msg) + } + return handleLoadMsgStatic(m, msg) +} + +func handleLoadMsgStatic(m model, msg loadMsg) (model, tea.Cmd) { + defer msg.Close() + r := msg.Reader() + url := m.urls[m.selected] + img, err := readerToImage(m.height, url, r) + if err != nil { + return m, func() tea.Msg { return errMsg{err} } + } + m.image = img + return m, nil +} + +func handleLoadMsgAnimation(m model, msg loadMsg) (model, tea.Cmd) { + defer msg.Close() + r := msg.Reader() + + // decode the gif + gimg, err := gif.DecodeAll(r) + if err != nil { + return m, wrapErrCmd(err) + } + + ctx := context.Background() + ctx, cancel := context.WithCancel(ctx) + m.cancelAnimation = cancel + + // precompute the frames for performance reasons + var frames []string + for _, img := range gimg.Image { + str, err := imageToString(m.height, m.urls[m.selected], img) if err != nil { - return m, func() tea.Msg { return errMsg{err} } + return m, wrapErrCmd(err) + } + frames = append(frames, str) + } + + return m, func() tea.Msg { + return gifMsg{ + gif: gimg, + frames: frames, + frame: 0, + ctx: ctx, } - m.image = img - return m, nil } - return m, nil +} + +func wrapErrCmd(err error) tea.Cmd { + return func() tea.Msg { return errMsg{err} } } func (m model) View() string { @@ -122,11 +204,29 @@ func (m model) View() string { return m.image } +type gifMsg struct { + gif *gif.GIF + frame int + frames []string + ctx context.Context +} + type loadMsg struct { resp *http.Response file *os.File } +func (l loadMsg) Reader() io.ReadCloser { + if l.resp != nil { + return l.resp.Body + } + return l.file +} + +func (l loadMsg) Close() { + l.Reader().Close() +} + type errMsg struct{ error } func load(url string) tea.Cmd { @@ -154,6 +254,10 @@ func readerToImage(height uint, url string, r io.Reader) (string, error) { return "", err } + return imageToString(height, url, img) +} + +func imageToString(height uint, url string, img image.Image) (string, error) { img = resize.Resize(0, height*2, img, resize.Lanczos3) b := img.Bounds() w := b.Max.X