Sam Fredrickson
f67323c5f4
All checks were successful
Build & Test / Main (push) Successful in 1m0s
The Coindesk API doesn't have data going all the way back. But since history isn't changing, we can simply put in known prices. Also, extend the CDPR cells to have four digits instead of just two.
269 lines
5.5 KiB
Go
269 lines
5.5 KiB
Go
package asset
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"log/slog"
|
|
"time"
|
|
|
|
"code.humancabbage.net/sam/moonmath/coindesk"
|
|
"code.humancabbage.net/sam/moonmath/config"
|
|
"code.humancabbage.net/sam/moonmath/moon"
|
|
"github.com/charmbracelet/bubbles/spinner"
|
|
"github.com/charmbracelet/bubbles/table"
|
|
tea "github.com/charmbracelet/bubbletea"
|
|
"github.com/charmbracelet/lipgloss"
|
|
)
|
|
|
|
type Model struct {
|
|
math moon.Math
|
|
|
|
refreshing bool
|
|
indicator spinner.Model
|
|
|
|
properties table.Model
|
|
projections table.Model
|
|
}
|
|
|
|
type Msg struct {
|
|
Asset coindesk.Asset
|
|
inner tea.Msg
|
|
}
|
|
|
|
func New(cfg config.Asset) (m Model) {
|
|
m.math = moon.NewMath(
|
|
cfg.Asset,
|
|
cfg.Goals,
|
|
config.GetBases(&cfg),
|
|
)
|
|
|
|
tableStyle := table.DefaultStyles()
|
|
tableStyle.Selected = tableStyle.Cell.Copy().
|
|
Padding(0)
|
|
tableStyle.Header = tableStyle.Header.
|
|
Bold(true).
|
|
Foreground(lipgloss.Color("214")).
|
|
Border(baseStyle.GetBorderStyle(), false, false, true, false)
|
|
|
|
// properties table
|
|
|
|
m.properties = table.New(
|
|
table.WithColumns([]table.Column{
|
|
{Title: "Property", Width: 9},
|
|
{Title: "Value", Width: 9},
|
|
}),
|
|
table.WithHeight(2),
|
|
table.WithStyles(tableStyle),
|
|
)
|
|
|
|
// projections table
|
|
|
|
labelsWidth := 0
|
|
for _, l := range m.math.Labels {
|
|
if len(l) > labelsWidth {
|
|
labelsWidth = len(l)
|
|
}
|
|
}
|
|
projectionCols := []table.Column{
|
|
{Title: "Labels", Width: labelsWidth},
|
|
}
|
|
for _, c := range m.math.Columns {
|
|
projectionCols = append(projectionCols,
|
|
table.Column{
|
|
Title: c.Base.Label(),
|
|
Width: 10,
|
|
})
|
|
}
|
|
m.projections = table.New(
|
|
table.WithColumns(projectionCols),
|
|
table.WithHeight(len(m.math.Labels)),
|
|
table.WithStyles(tableStyle),
|
|
)
|
|
|
|
// indicator spinner
|
|
|
|
m.indicator = spinner.New()
|
|
m.indicator.Spinner = spinner.Points
|
|
m.indicator.Style = indicatorNormalStyle
|
|
|
|
return
|
|
}
|
|
|
|
func (m Model) Init() tea.Cmd {
|
|
return tea.Batch(
|
|
m.indicator.Tick,
|
|
func() tea.Msg {
|
|
return Msg{m.math.Asset, refresh{}}
|
|
},
|
|
)
|
|
}
|
|
|
|
func (m Model) Handles(a coindesk.Asset) bool {
|
|
return m.math.Asset == a
|
|
}
|
|
|
|
func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
|
|
switch msg := msg.(type) {
|
|
case Msg:
|
|
switch msg := msg.inner.(type) {
|
|
case refresh:
|
|
m.refreshing = true
|
|
m.indicator.Style = indicatorNormalStyle
|
|
return m, tea.Batch(
|
|
m.resumeIndicator,
|
|
m.refresh,
|
|
m.scheduleRefresh(),
|
|
)
|
|
case update:
|
|
var cmd tea.Cmd
|
|
if msg.err == nil {
|
|
m.math = msg.math
|
|
refillProperties(&m)
|
|
refillProjections(&m)
|
|
cmd = m.stopIndicator()
|
|
} else {
|
|
m.indicator.Style = indicatorErrorStyle
|
|
}
|
|
return m, cmd
|
|
case stopIndicator:
|
|
m.refreshing = false
|
|
return m, nil
|
|
}
|
|
case spinner.TickMsg:
|
|
if !m.refreshing {
|
|
return m, nil
|
|
}
|
|
var cmd tea.Cmd
|
|
m.indicator, cmd = m.indicator.Update(msg)
|
|
return m, cmd
|
|
}
|
|
return m, nil
|
|
}
|
|
|
|
type refresh struct{}
|
|
type update struct {
|
|
math moon.Math
|
|
err error
|
|
}
|
|
type stopIndicator struct{}
|
|
|
|
func (m Model) refresh() tea.Msg {
|
|
ctx, cancel := context.WithTimeout(
|
|
context.Background(),
|
|
refreshTimeout)
|
|
defer cancel()
|
|
err := m.math.Refresh(ctx)
|
|
if err != nil {
|
|
slog.Error("refresh",
|
|
"asset", m.math.Asset,
|
|
"err", err,
|
|
)
|
|
}
|
|
return Msg{m.math.Asset, update{m.math, err}}
|
|
}
|
|
|
|
func (m Model) resumeIndicator() tea.Msg {
|
|
return m.indicator.Tick()
|
|
}
|
|
|
|
func (m Model) scheduleRefresh() tea.Cmd {
|
|
return tea.Tick(refreshInterval,
|
|
func(t time.Time) tea.Msg {
|
|
return Msg{m.math.Asset, refresh{}}
|
|
})
|
|
}
|
|
|
|
func (m Model) stopIndicator() tea.Cmd {
|
|
// wait a bit to stop the indicator, so that it's more obvious
|
|
// even when the refresh completes quickly.
|
|
return tea.Tick(stopIndicatorDelay,
|
|
func(t time.Time) tea.Msg {
|
|
return Msg{m.math.Asset, stopIndicator{}}
|
|
})
|
|
}
|
|
|
|
var refreshInterval = time.Second * 30
|
|
var refreshTimeout = time.Second * 15
|
|
var stopIndicatorDelay = time.Millisecond * 500
|
|
|
|
func refillProperties(m *Model) {
|
|
rows := []table.Row{
|
|
{"Asset", string(m.math.Asset)},
|
|
{"Price", fmt.Sprintf("$%0.2f", m.math.CurrentPrice)},
|
|
}
|
|
m.properties.SetRows(rows)
|
|
}
|
|
|
|
func refillProjections(m *Model) {
|
|
cols := []table.Row{m.math.Labels}
|
|
for i := range m.math.Columns {
|
|
entries := renderEntries(m.math.Columns[i])
|
|
cols = append(cols, entries)
|
|
}
|
|
rows := transpose(cols)
|
|
m.projections.SetRows(rows)
|
|
}
|
|
|
|
func renderEntries(c moon.Column) (entries []string) {
|
|
entries = append(entries, fmt.Sprintf("$%.2f", c.StartingPrice))
|
|
entries = append(entries, fmt.Sprintf("%.4f%%", (c.CDPR-1)*100))
|
|
never := c.CDPR <= 1
|
|
for i := range c.Projections.Dates {
|
|
var cell string
|
|
if never {
|
|
cell = "NEVER!!!!!"
|
|
} else {
|
|
cell = c.
|
|
Projections.
|
|
Dates[i].
|
|
Format("2006-01-02")
|
|
}
|
|
entries = append(entries, cell)
|
|
}
|
|
return
|
|
}
|
|
|
|
func transpose(slice []table.Row) []table.Row {
|
|
xl := len(slice[0])
|
|
yl := len(slice)
|
|
result := make([]table.Row, xl)
|
|
for i := range result {
|
|
result[i] = make(table.Row, yl)
|
|
}
|
|
for i := 0; i < xl; i++ {
|
|
for j := 0; j < yl; j++ {
|
|
result[i][j] = slice[j][i]
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
func (m Model) View() string {
|
|
var s string
|
|
indicator := ""
|
|
if m.refreshing {
|
|
indicator = m.indicator.View()
|
|
}
|
|
right := lipgloss.JoinVertical(
|
|
lipgloss.Center,
|
|
baseStyle.Render(m.properties.View()),
|
|
indicator,
|
|
)
|
|
s += lipgloss.JoinHorizontal(
|
|
lipgloss.Center,
|
|
right,
|
|
baseStyle.Render(m.projections.View()),
|
|
)
|
|
return s + "\n"
|
|
}
|
|
|
|
var baseStyle = lipgloss.NewStyle().
|
|
BorderStyle(lipgloss.RoundedBorder()).
|
|
BorderForeground(lipgloss.Color("240"))
|
|
|
|
var indicatorNormalStyle = lipgloss.NewStyle().
|
|
Foreground(lipgloss.Color("69"))
|
|
|
|
var indicatorErrorStyle = lipgloss.NewStyle().
|
|
Foreground(lipgloss.Color("160"))
|