Skip to content

Commit

Permalink
Pretty loading animation
Browse files Browse the repository at this point in the history
  • Loading branch information
kognise committed Jul 26, 2024
1 parent 9d136e8 commit f6ca45b
Show file tree
Hide file tree
Showing 6 changed files with 73 additions and 40 deletions.
2 changes: 1 addition & 1 deletion flake.nix
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

outputs = { self, nixpkgs }:
let
version = "0.0.1";
version = "0.0.2";

# System types to support.
supportedSystems = [ "x86_64-linux" "x86_64-darwin" "aarch64-linux" "aarch64-darwin" ];
Expand Down
35 changes: 21 additions & 14 deletions tsui.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package main
import (
"context"
"fmt"
"os"
"time"

tea "github.com/charmbracelet/bubbletea"
Expand Down Expand Up @@ -70,6 +71,10 @@ type model struct {
// Current "generation" number for the status. Incremented every time the status
// is updated and used to keep track of status expiration messages.
statusGen int

// Frame counter for the loading animation. This is always running in the background,
// even if the animation is not visible.
animationT int
}

// Initialize the application state.
Expand Down Expand Up @@ -108,6 +113,9 @@ func (m model) Init() tea.Cmd {
tea.Tick(pingTickInterval, func(_ time.Time) tea.Msg {
return pingTickMsg{}
}),
tea.Tick(ui.PoggersAnimationInterval, func(_ time.Time) tea.Msg {
return animationTickMsg{}
}),
)
}

Expand All @@ -118,18 +126,17 @@ func renderMainError(err error) string {
}

func main() {
fmt.Println(ui.Go())
// m, err := initialModel()
// if err != nil {
// fmt.Fprintln(os.Stderr, renderMainError(err))
// os.Exit(1)
// }

// // Enable "alternate screen" mode, a terminal convention designed for rendering
// // full-screen, interactive UIs.
// p := tea.NewProgram(m, tea.WithAltScreen())
// if _, err := p.Run(); err != nil {
// fmt.Fprintln(os.Stderr, renderMainError(err))
// os.Exit(1)
// }
m, err := initialModel()
if err != nil {
fmt.Fprintln(os.Stderr, renderMainError(err))
os.Exit(1)
}

// Enable "alternate screen" mode, a terminal convention designed for rendering
// full-screen, interactive UIs.
p := tea.NewProgram(m, tea.WithAltScreen())
if _, err := p.Run(); err != nil {
fmt.Fprintln(os.Stderr, renderMainError(err))
os.Exit(1)
}
}
51 changes: 38 additions & 13 deletions ui/animation.go
Original file line number Diff line number Diff line change
@@ -1,31 +1,47 @@
package ui

import "github.com/charmbracelet/lipgloss"
import (
"time"

var animLines = []string{
"github.com/charmbracelet/lipgloss"
)

const animWidth = 22
const animHeight = 11

var animLines = [animHeight]string{
` .... .... .... `,
`...... ...... ......`,
` .... .... .... `,
` `,
` TTTT SSSS UUUU `,
`TTTTTT SSSSSS UUUUUU`,
` TTTT SSSS UUUU `,
` TTTT UUUU IIII `,
`TTTTTT UUUUUU IIIIII`,
` TTTT UUUU IIII `,
` `,
` .... IIII .... `,
`...... IIIIII ......`,
` .... IIII .... `,
` .... SSSS .... `,
`...... SSSSSS ......`,
` .... SSSS .... `,
}
var animWidth = len(animLines[0])
var animHeight = len(animLines)

func Go() string {
var targetLetters = []byte{'T', 'S', 'U', 'I'}

// Designed rendering rate of the PoggersAnimationFrame animation.
const PoggersAnimationInterval = 80 * time.Millisecond

// Render a frame of the cool loading animation I designed.
func PoggersAnimationFrame(t int) string {
frame := ""

targetLetter := targetLetters[(t/animWidth)%len(targetLetters)]

for y := 0; y < animHeight; y++ {
if y > 0 {
frame += "\n"
}

// Line equation determined by bruteforce.
waveX := ((t+6)%animWidth-y)*2 - 4

for x := 0; x < animWidth; x++ {
char := animLines[y][x]
style := lipgloss.NewStyle()
Expand All @@ -36,8 +52,17 @@ func Go() string {
Faint(true)

case 'T', 'S', 'U', 'I':
style = style.
Foreground(Primary)
if char == targetLetter {
style = style.Foreground(Secondary)
}

isWave := x == waveX || x == waveX+1 || x == waveX+2 || x == waveX+3 // Thick
if isWave {
style = style.Bold(true)
} else {
// Make lowercase.
char = char - 'A' + 'a'
}
}

frame += style.Render(string(char))
Expand Down
2 changes: 1 addition & 1 deletion ui/appmenu.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ func (i *AppmenuItem) render(isSelected bool, isAnySubmenuOpen bool) string {
}

content := RenderSplit(
i.Label,
" "+i.Label,
style.
Faint(true).
Render(i.AdditionalLabel),
Expand Down
15 changes: 9 additions & 6 deletions update.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
tea "github.com/charmbracelet/bubbletea"
"github.com/neuralink/tsui/browser"
"github.com/neuralink/tsui/libts"
"github.com/neuralink/tsui/ui"
"tailscale.com/ipn"
"tailscale.com/ipn/ipnstate"
"tailscale.com/tailcfg"
Expand All @@ -18,6 +19,9 @@ type tickMsg struct{}
// Message triggered on each ping poller tick.
type pingTickMsg struct{}

// Message to increment the animation frame counter.
type animationTickMsg struct{}

// Message containing the result of a successful Tailscale state update.
type stateMsg libts.State

Expand Down Expand Up @@ -194,17 +198,16 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return pingTickMsg{}
}),
)
case animationTickMsg:
m.animationT++
return m, tea.Tick(ui.PoggersAnimationInterval, func(_ time.Time) tea.Msg {
return animationTickMsg{}
})

// When our updaters return, update our model and refresh the menus.
case stateMsg:
m.state = libts.State(msg)
m.updateMenus()
if m.state.BackendState == ipn.NoState.String() {
// Do updates more frequently if we have no state because it should load soon.
return m, tea.Tick(500*time.Millisecond, func(_ time.Time) tea.Msg {
return tickMsg{}
})
}
case pingResultsMsg:
m.pings = msg
m.updateMenus()
Expand Down
8 changes: 3 additions & 5 deletions view.go
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ func renderHeader(m *model) string {
MarginRight(4).
Render(logo)

status := "Tailscale Status: "
status := "Status: "
status += renderStatusButton(m.state.BackendState, m.state.CurrentExitNode != nil)
if m.state.BackendState == ipn.Running.String() {
status += lipgloss.NewStyle().
Expand Down Expand Up @@ -274,13 +274,11 @@ func (m model) View() string {
}, "\n"))

case ipn.NoState.String():
middle = renderMiddleBanner(&m, middleHeight,
`Please wait, loading...`)
middle = renderMiddleBanner(&m, middleHeight, ui.PoggersAnimationFrame(m.animationT))

case ipn.Starting.String():
if m.state.AuthURL == "" {
middle = renderMiddleBanner(&m, middleHeight,
`Tailscale is starting...`)
middle = renderMiddleBanner(&m, middleHeight, ui.PoggersAnimationFrame(m.animationT))
} else {
// If we have an AuthURL in the Starting state, that means the user is reauthenticating!
lines := []string{
Expand Down

0 comments on commit f6ca45b

Please sign in to comment.