Skip to content

Commit

Permalink
Add a gif animation
Browse files Browse the repository at this point in the history
  • Loading branch information
andrea committed Mar 21, 2024
1 parent fc16b22 commit 61b3878
Show file tree
Hide file tree
Showing 4 changed files with 316 additions and 0 deletions.
230 changes: 230 additions & 0 deletions components/image/image.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,230 @@
package image

import (
"image"
_ "image/jpeg"
"io"
"net/http"
"os"
"strings"

tea "github.com/charmbracelet/bubbletea"
"github.com/lucasb-eyer/go-colorful"
"github.com/muesli/termenv"
"github.com/nfnt/resize"
foam "github.com/remogatto/sugarfoam"
)

type DoneMsg struct {
done bool
}

type errMsg struct{ error }

type loadMsg struct {
io.ReadCloser
}

type redrawMsg struct {
width uint
height uint
url string
}

type Option func(*Model)

type Model struct {
foam.Common

textImage string
image image.Image
url string
focused bool
}

func New(opts ...Option) *Model {
img := new(Model)

img.Common.SetStyles(foam.DefaultStyles())

for _, opt := range opts {
opt(img)
}

return img
}

func WithStyles(styles *foam.Styles) Option {
return func(ti *Model) {
ti.Common.SetStyles(styles)
}
}

// Blur removes focus from the viewport.
func (m *Model) Blur() {
m.focused = false
}

// Focus sets the viewport to be focused.
func (m *Model) Focus() tea.Cmd {
m.focused = true

return nil
}

func (m *Model) Focused() bool {
return m.focused
}

func (m *Model) SetWidth(w int) {
m.Common.SetHeight(w)

if m.image != nil {
m.imageToString()
}
}

func (m *Model) SetHeight(h int) {
m.Common.SetHeight(h)

if m.image != nil {
m.imageToString()
}
}

func (m *Model) SetSize(w, h int) {
m.Common.SetSize(w, h)

if m.image != nil {
m.imageToString()
}
}

func (t *Model) Init() tea.Cmd {
return nil
}

func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case errMsg:
// m.err = msg
return m, nil
case redrawMsg:
return m, m.LoadURL(m.url)

case loadMsg:
return m.handleLoadMsg(msg)

}
return m, nil
}

func (m *Model) View() string {
if m.Focused() {
return m.GetStyles().Focused.Render(m.textImage)
}
return m.GetStyles().Blurred.Render(m.textImage)

}

func (m *Model) Redraw() tea.Cmd {
return func() tea.Msg {
return redrawMsg{
width: uint(m.GetWidth()),
height: uint(m.GetHeight()),
url: m.url,
}
}
}

func (m *Model) CanGrow() bool {
return true
}

func (m *Model) LoadURL(url string) tea.Cmd {
var r io.ReadCloser
var err error

if strings.HasPrefix(m.url, "http") {
var resp *http.Response
resp, err = http.Get(m.url)
r = resp.Body
} else {
r, err = os.Open(m.url)
}

if err != nil {
return func() tea.Msg {
return errMsg{err}
}
}

return load(r)
}

func (m *Model) SetURL(url string) {
m.url = url
}

func load(r io.ReadCloser) tea.Cmd {
return func() tea.Msg {
return loadMsg{r}
}
}

func (m *Model) handleLoadMsg(msg loadMsg) (*Model, tea.Cmd) {
// blank out image so it says "loading..."
m.textImage = ""

return m.handleLoadMsgStatic(msg)
}

func (m *Model) handleLoadMsgStatic(msg loadMsg) (*Model, tea.Cmd) {
defer msg.Close()

img, err := m.readerToimage(msg)
if err != nil {
return m, func() tea.Msg { return errMsg{err} }
}

m.textImage = img

return m, func() tea.Msg { return DoneMsg{true} }
}

func (m *Model) imageToString() (string, error) {
m.image = resize.Thumbnail(uint(m.GetWidth()), uint(m.GetHeight()*2-4), m.image, resize.Lanczos3)
b := m.image.Bounds()
w := b.Max.X
h := b.Max.Y
p := termenv.ColorProfile()
str := strings.Builder{}
for y := 0; y < h; y += 2 {
for x := w; x < int(m.GetWidth()); x = x + 2 {
str.WriteString(" ")
}
for x := 0; x < w; x++ {
c1, _ := colorful.MakeColor(m.image.At(x, y))
color1 := p.Color(c1.Hex())
c2, _ := colorful.MakeColor(m.image.At(x, y+1))
color2 := p.Color(c2.Hex())
str.WriteString(termenv.String("▀").
Foreground(color1).
Background(color2).
String())
}
str.WriteString("\n")
}
return str.String(), nil
}

func (m *Model) readerToimage(r io.Reader) (string, error) {
img, _, err := image.Decode(r)
if err != nil {
return "", err
}

m.image = img

return m.imageToString()
}
Binary file added examples/starwars/starwars.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
77 changes: 77 additions & 0 deletions examples/starwars/starwars.tape
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
# VHS documentation
#
# Output:
# Output <path>.gif Create a GIF output at the given <path>
# Output <path>.mp4 Create an MP4 output at the given <path>
# Output <path>.webm Create a WebM output at the given <path>
#
# Require:
# Require <string> Ensure a program is on the $PATH to proceed
#
# Settings:
# Set FontSize <number> Set the font size of the terminal
# Set FontFamily <string> Set the font family of the terminal
# Set Height <number> Set the height of the terminal
# Set Width <number> Set the width of the terminal
# Set LetterSpacing <float> Set the font letter spacing (tracking)
# Set LineHeight <float> Set the font line height
# Set LoopOffset <float>% Set the starting frame offset for the GIF loop
# Set Theme <json|string> Set the theme of the terminal
# Set Padding <number> Set the padding of the terminal
# Set Framerate <number> Set the framerate of the recording
# Set PlaybackSpeed <float> Set the playback speed of the recording
# Set MarginFill <file|#000000> Set the file or color the margin will be filled with.
# Set Margin <number> Set the size of the margin. Has no effect if MarginFill isn't set.
# Set BorderRadius <number> Set terminal border radius, in pixels.
# Set WindowBar <string> Set window bar type. (one of: Rings, RingsRight, Colorful, ColorfulRight)
# Set WindowBarSize <number> Set window bar size, in pixels. Default is 40.
# Set TypingSpeed <time> Set the typing speed of the terminal. Default is 50ms.
#
# Sleep:
# Sleep <time> Sleep for a set amount of <time> in seconds
#
# Type:
# Type[@<time>] "<characters>" Type <characters> into the terminal with a
# <time> delay between each character
#
# Keys:
# Escape[@<time>] [number] Press the Escape key
# Backspace[@<time>] [number] Press the Backspace key
# Delete[@<time>] [number] Press the Delete key
# Insert[@<time>] [number] Press the Insert key
# Down[@<time>] [number] Press the Down key
# Enter[@<time>] [number] Press the Enter key
# Space[@<time>] [number] Press the Space key
# Tab[@<time>] [number] Press the Tab key
# Left[@<time>] [number] Press the Left Arrow key
# Right[@<time>] [number] Press the Right Arrow key
# Up[@<time>] [number] Press the Up Arrow key
# Down[@<time>] [number] Press the Down Arrow key
# PageUp[@<time>] [number] Press the Page Up key
# PageDown[@<time>] [number] Press the Page Down key
# Ctrl+<key> Press the Control key + <key> (e.g. Ctrl+C)
#
# Display:
# Hide Hide the subsequent commands from the output
# Show Show the subsequent commands in the output

Output starwars.gif

Require echo

Set Shell "bash"
Set FontSize 12
Set Width 1200
Set Height 600

Type "./starwars"
Enter
Sleep 5000ms
Down
Sleep 500ms
Down
Down
Sleep 500ms
Down

Sleep 5s
9 changes: 9 additions & 0 deletions examples/starwars/templates.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package main

var (
formats = map[int][]string{
BrowseState: []string{"BROWSE 📖", "Browse the results using the arrow keys - Item %d/%d", "API 🟢"},
CheckConnectionState: []string{"CONNECTING", "Checking the connection with the API endpoint...", "API 🔴"},
DownloadingState: []string{"DOWNLOAD %s", "Fetching results from the endpoint", "API 🟢"},
}
)

0 comments on commit 61b3878

Please sign in to comment.