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 }