329 lines
7.6 KiB
Go
329 lines
7.6 KiB
Go
package main
|
|
|
|
import (
|
|
"strings"
|
|
"time"
|
|
|
|
tea "github.com/charmbracelet/bubbletea"
|
|
"github.com/charmbracelet/lipgloss"
|
|
)
|
|
|
|
const (
|
|
diffletWidth = 9
|
|
diffletHeight = 4
|
|
diffletGap = 2
|
|
diffletMinimumHeaderWidth = 10
|
|
diffletFallbackRightPadding = 1
|
|
)
|
|
|
|
type DiffletExpression string
|
|
|
|
const (
|
|
DiffletIdle DiffletExpression = "idle"
|
|
DiffletFocused DiffletExpression = "focused"
|
|
DiffletHappy DiffletExpression = "happy"
|
|
DiffletApproved DiffletExpression = "approved"
|
|
DiffletSurprised DiffletExpression = "surprised"
|
|
DiffletConcerned DiffletExpression = "concerned"
|
|
DiffletConfused DiffletExpression = "confused"
|
|
DiffletSad DiffletExpression = "sad"
|
|
DiffletAnnoyed DiffletExpression = "annoyed"
|
|
DiffletError DiffletExpression = "error"
|
|
DiffletSleeping DiffletExpression = "sleeping"
|
|
DiffletCurious DiffletExpression = "curious"
|
|
)
|
|
|
|
var diffletFaces = map[DiffletExpression]string{
|
|
DiffletIdle: "•_•",
|
|
DiffletFocused: "-_-",
|
|
DiffletHappy: "^_^",
|
|
DiffletApproved: "^o^",
|
|
DiffletSurprised: "•o•",
|
|
DiffletConcerned: "•^•",
|
|
DiffletConfused: "•~•",
|
|
DiffletSad: ";_;",
|
|
DiffletAnnoyed: ">_<",
|
|
DiffletError: "x_x",
|
|
DiffletSleeping: "u_u",
|
|
DiffletCurious: "o_o",
|
|
}
|
|
|
|
type DiffletFrame struct {
|
|
Expression DiffletExpression
|
|
Feet string
|
|
}
|
|
|
|
func (frame DiffletFrame) lines() []string {
|
|
feet := frame.Feet
|
|
if feet == "" {
|
|
feet = " ▝ ▘"
|
|
}
|
|
return []string{
|
|
"▄███████▄",
|
|
"█+ " + diffletFaces[frame.Expression] + " -█",
|
|
"▀███████▀",
|
|
feet + strings.Repeat(" ", max(0, diffletWidth-lipgloss.Width(feet))),
|
|
}
|
|
}
|
|
|
|
func (frame DiffletFrame) render() []string {
|
|
lines := frame.lines()
|
|
lines[1] = "█" + okStyle.Render("+") + " " + diffletFaces[frame.Expression] +
|
|
" " + badStyle.Render("-") + "█"
|
|
return lines
|
|
}
|
|
|
|
type diffletAnimation struct {
|
|
frames []DiffletFrame
|
|
interval time.Duration
|
|
loop bool
|
|
settle DiffletExpression
|
|
}
|
|
|
|
type diffletState int
|
|
|
|
const (
|
|
diffletIdle diffletState = iota
|
|
diffletLoading
|
|
diffletFocused
|
|
diffletConcerned
|
|
diffletHappy
|
|
diffletApproved
|
|
diffletSleeping
|
|
diffletNewComment
|
|
diffletSuccess
|
|
diffletRecoverableError
|
|
diffletFatalError
|
|
diffletSad
|
|
)
|
|
|
|
type diffletTickMsg struct {
|
|
generation uint64
|
|
}
|
|
|
|
type diffletModel struct {
|
|
enabled bool
|
|
visible bool
|
|
expressive bool
|
|
animated bool
|
|
state diffletState
|
|
expression DiffletExpression
|
|
animation diffletAnimation
|
|
frame int
|
|
generation uint64
|
|
blinking bool
|
|
blinkCycle uint64
|
|
}
|
|
|
|
func newDifflet(enabled, expressive, animated bool) diffletModel {
|
|
model := diffletModel{
|
|
enabled: enabled, visible: true, expressive: expressive, animated: animated,
|
|
state: diffletLoading, expression: DiffletIdle,
|
|
}
|
|
_ = model.setState(diffletLoading)
|
|
return model
|
|
}
|
|
|
|
func (d *diffletModel) start() tea.Cmd {
|
|
if !d.enabled || !d.visible || !d.animated {
|
|
return nil
|
|
}
|
|
if d.state == diffletIdle {
|
|
return d.tick(d.blinkDelay())
|
|
}
|
|
if len(d.animation.frames) > 0 {
|
|
return d.tick(d.animation.interval)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (d *diffletModel) setState(state diffletState) tea.Cmd {
|
|
d.state = state
|
|
d.generation++
|
|
d.frame = 0
|
|
d.blinking = false
|
|
d.animation = diffletAnimation{}
|
|
d.expression = d.staticExpression(state)
|
|
if !d.enabled || !d.visible || !d.animated {
|
|
return nil
|
|
}
|
|
|
|
switch state {
|
|
case diffletIdle:
|
|
return d.tick(d.blinkDelay())
|
|
case diffletLoading:
|
|
d.animation = diffletAnimation{
|
|
frames: []DiffletFrame{
|
|
{Expression: DiffletIdle, Feet: " ▝ ▘"},
|
|
{Expression: DiffletIdle, Feet: " ▝ ▘"},
|
|
{Expression: DiffletIdle, Feet: " ▝ ▘"},
|
|
{Expression: DiffletIdle, Feet: " ▝ ▘"},
|
|
},
|
|
interval: 180 * time.Millisecond,
|
|
loop: true,
|
|
settle: DiffletIdle,
|
|
}
|
|
case diffletNewComment:
|
|
d.animation = diffletAnimation{
|
|
frames: []DiffletFrame{
|
|
{Expression: DiffletIdle},
|
|
{Expression: DiffletSurprised},
|
|
{Expression: DiffletConcerned},
|
|
},
|
|
interval: 200 * time.Millisecond,
|
|
settle: DiffletConcerned,
|
|
}
|
|
case diffletSuccess:
|
|
d.animation = diffletAnimation{
|
|
frames: []DiffletFrame{
|
|
{Expression: DiffletIdle},
|
|
{Expression: DiffletHappy},
|
|
{Expression: DiffletApproved},
|
|
{Expression: DiffletHappy},
|
|
},
|
|
interval: 175 * time.Millisecond,
|
|
settle: DiffletHappy,
|
|
}
|
|
case diffletRecoverableError:
|
|
if d.expressive {
|
|
d.animation = diffletAnimation{
|
|
frames: []DiffletFrame{
|
|
{Expression: DiffletIdle},
|
|
{Expression: DiffletAnnoyed},
|
|
{Expression: DiffletSad},
|
|
},
|
|
interval: 220 * time.Millisecond,
|
|
settle: DiffletSad,
|
|
}
|
|
}
|
|
case diffletFatalError:
|
|
if d.expressive {
|
|
d.animation = diffletAnimation{
|
|
frames: []DiffletFrame{
|
|
{Expression: DiffletIdle},
|
|
{Expression: DiffletAnnoyed},
|
|
{Expression: DiffletError},
|
|
},
|
|
interval: 220 * time.Millisecond,
|
|
settle: DiffletError,
|
|
}
|
|
}
|
|
}
|
|
if len(d.animation.frames) == 0 {
|
|
return nil
|
|
}
|
|
d.expression = d.animation.frames[0].Expression
|
|
return d.tick(d.animation.interval)
|
|
}
|
|
|
|
func (d *diffletModel) setVisible(visible bool) tea.Cmd {
|
|
if d.visible == visible {
|
|
return nil
|
|
}
|
|
d.visible = visible
|
|
if !visible {
|
|
d.generation++
|
|
d.animation = diffletAnimation{}
|
|
d.blinking = false
|
|
return nil
|
|
}
|
|
return d.setState(d.state)
|
|
}
|
|
|
|
func (d *diffletModel) staticExpression(state diffletState) DiffletExpression {
|
|
switch state {
|
|
case diffletFocused:
|
|
return DiffletFocused
|
|
case diffletConcerned, diffletNewComment:
|
|
return DiffletConcerned
|
|
case diffletHappy, diffletSuccess:
|
|
return DiffletHappy
|
|
case diffletApproved:
|
|
return DiffletApproved
|
|
case diffletSleeping:
|
|
return DiffletSleeping
|
|
case diffletRecoverableError:
|
|
if d.expressive {
|
|
return DiffletAnnoyed
|
|
}
|
|
return DiffletConcerned
|
|
case diffletFatalError:
|
|
return DiffletError
|
|
case diffletSad:
|
|
if d.expressive {
|
|
return DiffletSad
|
|
}
|
|
return DiffletConcerned
|
|
default:
|
|
return DiffletIdle
|
|
}
|
|
}
|
|
|
|
func (d *diffletModel) update(msg diffletTickMsg) tea.Cmd {
|
|
if !d.enabled || !d.visible || !d.animated || msg.generation != d.generation {
|
|
return nil
|
|
}
|
|
if d.state == diffletIdle {
|
|
if !d.blinking {
|
|
d.blinking = true
|
|
d.expression = DiffletFocused
|
|
return d.tick(125 * time.Millisecond)
|
|
}
|
|
d.blinking = false
|
|
d.expression = DiffletIdle
|
|
d.blinkCycle++
|
|
return d.tick(d.blinkDelay())
|
|
}
|
|
if len(d.animation.frames) == 0 {
|
|
return nil
|
|
}
|
|
next := d.frame + 1
|
|
if next >= len(d.animation.frames) {
|
|
if !d.animation.loop {
|
|
settle := d.animation.settle
|
|
d.animation = diffletAnimation{}
|
|
d.expression = settle
|
|
return nil
|
|
}
|
|
next = 0
|
|
}
|
|
d.frame = next
|
|
d.expression = d.animation.frames[next].Expression
|
|
return d.tick(d.animation.interval)
|
|
}
|
|
|
|
func (d diffletModel) frameLines() []string {
|
|
if !d.enabled || !d.visible {
|
|
return nil
|
|
}
|
|
if len(d.animation.frames) > 0 && d.frame < len(d.animation.frames) {
|
|
frame := d.animation.frames[d.frame]
|
|
frame.Expression = d.expression
|
|
return frame.render()
|
|
}
|
|
return (DiffletFrame{Expression: d.expression}).render()
|
|
}
|
|
|
|
func diffletHeaderWidth(width int) int {
|
|
centeredLeft := max(0, (width-diffletWidth)/2)
|
|
if centeredLeft-diffletGap >= diffletMinimumHeaderWidth {
|
|
return centeredLeft - diffletGap
|
|
}
|
|
rightPadding := 0
|
|
if width-diffletWidth-diffletGap > 1 {
|
|
rightPadding = diffletFallbackRightPadding
|
|
}
|
|
return max(1, width-diffletWidth-diffletGap-rightPadding)
|
|
}
|
|
|
|
func (d diffletModel) tick(after time.Duration) tea.Cmd {
|
|
generation := d.generation
|
|
return tea.Tick(after, func(time.Time) tea.Msg {
|
|
return diffletTickMsg{generation: generation}
|
|
})
|
|
}
|
|
|
|
func (d diffletModel) blinkDelay() time.Duration {
|
|
return 5*time.Second + time.Duration((d.generation+d.blinkCycle*3)%6)*650*time.Millisecond
|
|
}
|