Render dynamic weather for ASCII landscapes. Inspired and powered by ~iajrz's `climate` program.
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.
Atmosphere/main.go

202 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
}