You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
203 lines
4.4 KiB
203 lines
4.4 KiB
package main
|
|
|
|
import (
|
|
tea "github.com/charmbracelet/bubbletea"
|
|
"github.com/muesli/reflow/wordwrap"
|
|
"github.com/muesli/termenv"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
func main() {
|
|
app := tea.NewProgram(model{startTime: time.Now()})
|
|
if err := app.Start(); err != nil {
|
|
panic(err)
|
|
}
|
|
}
|
|
|
|
// Queues the initial loading of the forecast and
|
|
func (m model) Init() tea.Cmd {
|
|
return tea.Batch(updateForecast, renderOften)
|
|
}
|
|
|
|
var updateForecast = func() tea.Msg {
|
|
forecast, err := NewForecast()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return forecast
|
|
}
|
|
|
|
var updateForecastOften = tea.Tick(20*time.Second, func(t time.Time) tea.Msg {
|
|
return updateForecast()
|
|
})
|
|
|
|
var makeSceneMsg = func(f Forecast) func() tea.Msg {
|
|
return func() tea.Msg {
|
|
scene, err := NewScene("scene1", f)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return scene
|
|
}
|
|
}
|
|
|
|
type fadeOut float32
|
|
type fadeIn float32
|
|
|
|
// Used to update the progress of a fade out transition.
|
|
var tickFadeOut = tea.Every(time.Millisecond*100, func(t time.Time) tea.Msg {
|
|
return fadeOut(0.05)
|
|
})
|
|
|
|
// Used to update the progress of a fade in transition.
|
|
var tickFadeIn = tea.Every(time.Millisecond*100, func(t time.Time) tea.Msg {
|
|
return fadeIn(0.05)
|
|
})
|
|
|
|
type renderTick struct{}
|
|
|
|
// Used to update the screen at least once per second.
|
|
var renderOften = tea.Tick(time.Second, func(t time.Time) tea.Msg {
|
|
return renderTick{}
|
|
})
|
|
|
|
// Update updates the internal state of the model and queues any events that
|
|
// need to be scheduled.
|
|
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|
cmds := []tea.Cmd{}
|
|
|
|
switch msg := msg.(type) {
|
|
case tea.KeyMsg:
|
|
switch msg.String() {
|
|
case "ctrl+c", "ctrl+d", "q":
|
|
return m, tea.Quit
|
|
case "esc":
|
|
// TODO: Menu for options. Maybe 100% randomized weather?
|
|
}
|
|
|
|
case tea.WindowSizeMsg:
|
|
m.width = msg.Width
|
|
m.height = msg.Height
|
|
|
|
case Forecast:
|
|
// Re-queue the forecast polling.
|
|
cmds = append(cmds, updateForecastOften)
|
|
|
|
// No action required if the forecast hasn't changed.
|
|
if msg == m.Forecast {
|
|
break
|
|
}
|
|
|
|
// If this isn't the initial startup forecast, we need to fade out the scene.
|
|
if m.Forecast != (Forecast{}) {
|
|
m.fading = true
|
|
cmds = append(cmds, tickFadeOut)
|
|
}
|
|
|
|
// In all cases, we need to load a scene with the forecast.
|
|
m.Forecast = msg
|
|
cmds = append(cmds, makeSceneMsg(m.Forecast))
|
|
|
|
case Scene:
|
|
if m.Scene.foreground == nil {
|
|
m.Scene = msg
|
|
} else {
|
|
m.nextScene = msg
|
|
}
|
|
|
|
case fadeOut:
|
|
m.fadeProgress += float32(msg)
|
|
if m.fadeProgress >= 1.0 {
|
|
m.fadeProgress = 1.0
|
|
// To unfade, we need a Scene ready to rock. If we don't have one, stall.
|
|
if m.nextScene.foreground == nil {
|
|
cmds = append(cmds, tickFadeOut)
|
|
break
|
|
}
|
|
|
|
m.Scene = m.nextScene
|
|
m.nextScene = Scene{}
|
|
cmds = append(cmds, tickFadeIn)
|
|
break
|
|
}
|
|
cmds = append(cmds, tickFadeOut)
|
|
|
|
case fadeIn:
|
|
m.fadeProgress -= float32(msg)
|
|
if m.fadeProgress <= 0.0 {
|
|
m.fadeProgress = 0.0
|
|
m.fading = false
|
|
break
|
|
}
|
|
cmds = append(cmds, tickFadeIn)
|
|
|
|
case error:
|
|
m.err = msg
|
|
|
|
case renderTick:
|
|
cmds = append(cmds, renderOften)
|
|
}
|
|
|
|
return m, tea.Batch(cmds...)
|
|
}
|
|
|
|
// View lays out the screen and queries the Scene for its contents.
|
|
// It also handles transitions when changing from Scene to Scene.
|
|
func (m model) View() string {
|
|
if m.err != nil {
|
|
return m.err.Error()
|
|
}
|
|
|
|
weather := wordwrap.String(m.Forecast.String(), m.width)
|
|
m.height -= strings.Count(weather, "\n")
|
|
weather = strings.TrimSpace(weather) // Trim trailing newline
|
|
|
|
output := ""
|
|
lastStyle := Style{}
|
|
profile := termenv.EnvColorProfile()
|
|
|
|
// Center scene in terminal
|
|
xOff := (m.Scene.Width - m.width) / 2
|
|
yOff := (m.Scene.Height - m.height) / 2
|
|
|
|
for y := yOff; y < yOff+m.height; y++ {
|
|
for x := xOff; x < xOff+m.width; x++ {
|
|
char, style := m.Scene.GetCell(x, y, int(time.Since(m.startTime).Seconds()))
|
|
style = style.Convert(profile)
|
|
|
|
// For wipe transitions, replace character with blank space.
|
|
if float32((x-xOff)/2+y-yOff) < m.fadeProgress*float32(m.width/2+m.height) {
|
|
char = ' '
|
|
style = Style{}
|
|
}
|
|
|
|
// Only output formatting escape codes if the style's changed since the last cell.
|
|
if lastStyle.String() != style.String() {
|
|
output += style.String()
|
|
lastStyle = style
|
|
}
|
|
|
|
output += string(char)
|
|
}
|
|
output += "\n"
|
|
}
|
|
|
|
output += termenv.CSI + termenv.ResetSeq + "m"
|
|
output += weather
|
|
|
|
return output
|
|
}
|
|
|
|
type model struct {
|
|
Forecast
|
|
Scene
|
|
err error
|
|
nextScene Scene
|
|
fading bool
|
|
fadeProgress float32
|
|
width int
|
|
height int
|
|
startTime time.Time
|
|
}
|