package tui import ( "strings" "time" tea "charm.land/bubbletea/v2" "charm.land/lipgloss/v2" "github.com/charmbracelet/harmonica" ) const ( LogoPhaseHidden = iota LogoPhaseAnimating LogoPhaseVisible LogoPhaseDone ) type LogoTickMsg struct{} type LogoModel struct { phase int alpha float64 vel float64 spring harmonica.Spring isDark bool frame int displayLogo bool } func logoLines() []string { return []string{ ``, ` ╔═╗╦ ╔═╗╔═╗╔═╗╔╗╔╔╦╗`, ` ╠═╣║ ╠═╣║ ╦║╣ ║║║ ║ `, ` ╩ ╩╩ ╩ ╩╚═╝╚═╝╝╚╝ ╩ `, ``, ` 100% local · Your data never leaves`, ``, } } func NewLogoModel(isDark bool) LogoModel { return LogoModel{ spring: harmonica.NewSpring(harmonica.FPS(60), 4.0, 0.9), isDark: isDark, phase: LogoPhaseHidden, } } func (m *LogoModel) Start() { m.phase = LogoPhaseAnimating m.alpha = 0 m.vel = 0 m.frame = 0 } func (m LogoModel) Init() tea.Cmd { return tea.Tick(16*time.Millisecond, func(time.Time) tea.Msg { return LogoTickMsg{} }) } func (m LogoModel) Update(msg tea.Msg) (LogoModel, tea.Cmd) { if _, ok := msg.(LogoTickMsg); ok { m.frame++ if m.phase == LogoPhaseAnimating { target := 1.0 m.alpha, m.vel = m.spring.Update(m.alpha, m.vel, target) if m.alpha >= 0.95 && m.frame > 60 { m.phase = LogoPhaseVisible m.displayLogo = true } if m.phase == LogoPhaseVisible && m.frame > 180 { m.phase = LogoPhaseDone m.displayLogo = false } } if m.phase < LogoPhaseDone { return m, tea.Tick(16*time.Millisecond, func(time.Time) tea.Msg { return LogoTickMsg{} }) } } return m, nil } func (m LogoModel) View() string { if m.phase == LogoPhaseHidden || m.phase == LogoPhaseDone || !m.displayLogo { return "" } lines := logoLines() var b strings.Builder for _, line := range lines { if m.alpha < 0.1 { b.WriteString(lipgloss.NewStyle(). Foreground(lipgloss.Color("#4c566a")). Render(line)) } else if m.alpha < 0.5 { b.WriteString(lipgloss.NewStyle(). Foreground(lipgloss.Color("#81a1c1")). Render(line)) } else { b.WriteString(lipgloss.NewStyle(). Foreground(lipgloss.Color("#88c0d0")). Bold(true). Render(line)) } b.WriteString("\n") } return b.String() } func (m LogoModel) IsDone() bool { return m.phase == LogoPhaseDone } func (m LogoModel) ShouldShow() bool { return m.displayLogo && m.phase < LogoPhaseDone }