8 Commits

Author SHA1 Message Date
5c22d85e2b Add custom .golangci.yaml config.
All checks were successful
Build & Test / Main (push) Successful in 1m0s
Release / Release (push) Successful in 1m35s
2024-03-29 19:29:29 -07:00
f67323c5f4 Support hardcoded starting prices.
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.
2024-03-29 19:24:10 -07:00
4d5dcc46d2 Misc small improvements.
All checks were successful
Build & Test / Main (push) Successful in 1m0s
2024-03-29 18:40:33 -07:00
9e6abb1112 Naming things is hard.
All checks were successful
Build & Test / Main (push) Successful in 2m26s
2024-03-29 01:15:59 -07:00
2d991880ce Schedule refreshes more consistently.
All checks were successful
Build & Test / Main (push) Successful in 1m1s
Instead of returning the `scheduleRefresh` command only after receiving
an `update` message, do it while handling the `refresh` message. For
this not to cause weird behavior, the refresh deadline should be shorter
than the refresh interval.
2024-03-24 23:01:51 -07:00
7b445a02a2 Refresh indicator goes red on error.
All checks were successful
Build & Test / Main (push) Successful in 1m0s
Release / Release (push) Successful in 1m3s
It will stay flashing red until the next refresh, at which point it goes
back to its normal color. On a successful refresh, it still stops.

Also, add a deadline of 15 seconds to the refresh command.
2024-03-24 02:29:45 -07:00
97f4793ec3 Write errors to log file.
All checks were successful
Build & Test / Main (push) Successful in 1m1s
2024-03-22 22:55:41 -07:00
270534c0d5 Pause spinner ticks when not refreshing.
All checks were successful
Build & Test / Main (push) Successful in 59s
Also, add a quick-and-dirty model for displaying basic performance
stats, currently just the number of calls to the root Update() and
View() methods.
2024-03-22 21:14:34 -07:00
9 changed files with 310 additions and 136 deletions

15
.golangci.yaml Normal file
View File

@@ -0,0 +1,15 @@
linters:
disable-all: true
enable:
- errcheck
- godot
- goimports
- gosimple
- govet
- ineffassign
- nilerr
- nilnil
- staticcheck
- typecheck
- unused
- usestdlibvars

View File

@@ -16,62 +16,58 @@ import (
var k = koanf.New(".") var k = koanf.New(".")
func Load(path string) (all All, err error) { func Load(filePath string) (r Root, err error) {
err = k.Load(structs.Provider(Default, "koanf"), nil) err = k.Load(structs.Provider(Default, "koanf"), nil)
if err != nil { if err != nil {
return return
} }
if path != "" { if filePath != "" {
err = k.Load(file.Provider(path), yaml.Parser()) err = k.Load(file.Provider(filePath), yaml.Parser())
if err != nil { if err != nil {
return return
} }
} }
err = k.Unmarshal("", &all) err = k.Unmarshal("", &r)
if err != nil {
return
}
return return
} }
var Default All var Default Root
type All struct { type Root struct {
Defaults Data `koanf:"defaults"` Defaults Asset `koanf:"defaults"`
Assets map[coindesk.Asset]Data `koanf:"assets"` Assets map[coindesk.Asset]Asset `koanf:"assets"`
} }
type Data struct { type Asset struct {
Asset coindesk.Asset `koanf:"asset"` Asset coindesk.Asset `koanf:"asset"`
Goals []moon.Goal `koanf:"goals"` Goals []moon.Goal `koanf:"goals"`
ConstantBases []moon.ConstantBase `koanf:"constantBases"` ConstantBases []moon.ConstantBase `koanf:"constantBases"`
RelativeBases []moon.RelativeBase `koanf:"relativeBases"` RelativeBases []moon.RelativeBase `koanf:"relativeBases"`
} }
func (all All) GetData(asset coindesk.Asset) (data Data) { func (r Root) ForAsset(a coindesk.Asset) (cfg Asset) {
data, ok := all.Assets[asset] cfg = merge(r.Assets[a], r.Defaults)
if !ok { cfg.Asset = a
data = all.Defaults
}
if data.Asset == "" {
data.Asset = asset
}
if data.Goals == nil || len(data.Goals) == 0 {
data.Goals = all.Defaults.Goals
}
if data.ConstantBases == nil || len(data.ConstantBases) == 0 {
data.ConstantBases = all.Defaults.ConstantBases
}
if data.RelativeBases == nil || len(data.RelativeBases) == 0 {
data.RelativeBases = all.Defaults.RelativeBases
}
return return
} }
func merge(dst, src Asset) Asset {
if len(dst.Goals) == 0 {
dst.Goals = src.Goals
}
if len(dst.ConstantBases) == 0 {
dst.ConstantBases = src.ConstantBases
}
if len(dst.RelativeBases) == 0 {
dst.RelativeBases = src.RelativeBases
}
return dst
}
// GetBases returns the concatenation of the constant and relative bases, sorted // GetBases returns the concatenation of the constant and relative bases, sorted
// from most recent to least recent in time. // from most recent to least recent in time.
func GetBases(d *Data) (bases []moon.Base) { func GetBases(d *Asset) (bases []moon.Base) {
for _, b := range d.ConstantBases { for _, b := range d.ConstantBases {
bases = append(bases, b) bases = append(bases, b)
} }

View File

@@ -28,6 +28,12 @@ defaults:
time: 2019-12-31T16:00:00-08:00 time: 2019-12-31T16:00:00-08:00
- name: 2017- - name: 2017-
time: 2016-12-31T16:00:00-08:00 time: 2016-12-31T16:00:00-08:00
- name: 2013-
time: 2012-12-31T16:00:00-08:00
startingPrice: 13.30
- name: 2011-
time: 2010-12-31T16:00:00-08:00
startingPrice: 0.30
assets: assets:
ETH: ETH:
@@ -44,6 +50,11 @@ assets:
value: 20000 value: 20000
- name: $25k - name: $25k
value: 25000 value: 25000
constantBases:
- name: 2020-
time: 2019-12-31T16:00:00-08:00
- name: 2017-
time: 2016-12-31T16:00:00-08:00
LTC: LTC:
goals: goals:
- name: $100 - name: $100

View File

@@ -2,6 +2,7 @@ package moon
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"math" "math"
"time" "time"
@@ -75,30 +76,10 @@ func (m *Math) Refresh(ctx context.Context) (err error) {
tasks.WithMaxGoroutines(len(m.Columns)) tasks.WithMaxGoroutines(len(m.Columns))
//tasks.WithMaxGoroutines(1) //tasks.WithMaxGoroutines(1)
now := time.Now() now := time.Now()
for i := range m.Columns { for i := range m.Columns {
c := &m.Columns[i] c := &m.Columns[i]
tasks.Go(func() error { tasks.Go(func() error {
c.StartingDate = c.Base.From(now) return c.project(ctx, m, now)
nextDay := c.StartingDate.Add(time.Hour * 24)
resp, err := coindesk.GetPriceValues(ctx,
m.Asset, c.StartingDate, nextDay)
if err != nil {
return err
}
if len(resp.Data.Entries) == 0 {
c.Projections.Dates = nil
return nil
}
c.StartingPrice = resp.Data.Entries[0].Price
c.Gain = float64(m.CurrentPrice) / float64(c.StartingPrice)
days := now.Sub(c.StartingDate).Hours() / 24
c.CDPR = CDPR(days, c.Gain)
c.Projections = ProjectDates(
now, float64(m.CurrentPrice),
c.CDPR, m.Goals,
)
return nil
}) })
} }
err = tasks.Wait() err = tasks.Wait()
@@ -115,46 +96,49 @@ type Column struct {
Projections Projection Projections Projection
} }
func (c *Column) Column() (entries []string) { func (c *Column) project(ctx context.Context, m *Math, now time.Time) (err error) {
entries = append(entries, fmt.Sprintf("$%.2f", c.StartingPrice)) err = c.fillStartingPrice(ctx, m.Asset, now)
entries = append(entries, fmt.Sprintf("%.2f%%", (c.CDPR-1)*100)) if err != nil {
never := c.CDPR <= 1 return
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)
} }
c.Gain = float64(m.CurrentPrice) / float64(c.StartingPrice)
days := now.Sub(c.StartingDate).Hours() / 24
c.CDPR = CDPR(days, c.Gain)
c.Projections = ProjectDates(
now, float64(m.CurrentPrice),
c.CDPR, m.Goals,
)
return return
} }
var DefaultGoals = []Goal{ func (c *Column) fillStartingPrice(
{"$100k", 100000}, ctx context.Context, asset coindesk.Asset, now time.Time,
{"$150k", 150000}, ) error {
{"$200k", 200000}, // if base provides a hardcoded starting price, use it
{"$250k", 250000}, c.StartingDate = c.Base.From(now)
{"$300k", 300000}, c.StartingPrice = coindesk.Price(c.Base.GetStartingPrice())
{"$500k", 500000}, if c.StartingPrice != 0 {
{"$1m", 1000000}, return nil
}
// otherwise, look up the starting price via Coindesk
nextDay := c.StartingDate.Add(time.Hour * 24)
resp, err := coindesk.GetPriceValues(ctx, asset, c.StartingDate, nextDay)
if err != nil {
err = fmt.Errorf("getting price for %s on %v: %w",
asset, c.StartingDate, err)
return err
}
if len(resp.Data.Entries) == 0 {
c.Projections.Dates = nil
return errEmptyPriceEntries
}
c.StartingPrice = resp.Data.Entries[0].Price
return nil
} }
var DefaultConstantBases = []ConstantBase{ var errEmptyPriceEntries = errors.New("price values response has no entries")
{"2020-", time.Unix(1577836800, 0)},
{"2017-", time.Unix(1483228800, 0)},
}
var DefaultRelativeBases = []RelativeBase{
{"Month", time.Duration(-30) * time.Hour * 24},
{"Quarter", time.Duration(-90) * time.Hour * 24},
{"Half-Year", time.Duration(-182) * time.Hour * 24},
{"Year", time.Duration(-365) * time.Hour * 24},
}
type Goal struct { type Goal struct {
Name string `koanf:"name"` Name string `koanf:"name"`
@@ -165,12 +149,14 @@ type Goal struct {
type Base interface { type Base interface {
From(now time.Time) time.Time From(now time.Time) time.Time
Label() string Label() string
GetStartingPrice() float64
} }
// ConstantBase is a base that is a constant time, e.g. 2020-01-01. // ConstantBase is a base that is a constant time, e.g. 2020-01-01.
type ConstantBase struct { type ConstantBase struct {
Name string `koanf:"name"` Name string `koanf:"name"`
Time time.Time `koanf:"time"` Time time.Time `koanf:"time"`
StartingPrice float64 `koanf:"startingPrice"`
} }
func (cb ConstantBase) From(_ time.Time) time.Time { func (cb ConstantBase) From(_ time.Time) time.Time {
@@ -181,7 +167,11 @@ func (cb ConstantBase) Label() string {
return cb.Name return cb.Name
} }
// RelativeBase is a base that is relative, e.g. "90 days ago." func (cb ConstantBase) GetStartingPrice() float64 {
return cb.StartingPrice
}
// RelativeBase is a base that is relative, e.g. "90 days ago".
type RelativeBase struct { type RelativeBase struct {
Name string `koanf:"name"` Name string `koanf:"name"`
Offset time.Duration `koanf:"offset"` Offset time.Duration `koanf:"offset"`
@@ -195,3 +185,7 @@ func (rb RelativeBase) From(now time.Time) time.Time {
func (rb RelativeBase) Label() string { func (rb RelativeBase) Label() string {
return rb.Name return rb.Name
} }
func (rb RelativeBase) GetStartingPrice() float64 {
return 0
}

View File

@@ -11,6 +11,14 @@ func TestCDPR(t *testing.T) {
} }
func TestProjection(t *testing.T) { func TestProjection(t *testing.T) {
p := moon.ProjectDates(time.Now(), 68900, 1.0055, moon.DefaultGoals) p := moon.ProjectDates(time.Now(), 68900, 1.0055, []moon.Goal{
{"$100k", 100000},
{"$150k", 150000},
{"$200k", 200000},
{"$250k", 250000},
{"$300k", 300000},
{"$500k", 500000},
{"$1m", 1000000},
})
_ = p _ = p
} }

View File

@@ -2,21 +2,30 @@ package main
import ( import (
"fmt" "fmt"
"io"
"log/slog"
"os" "os"
"path/filepath"
"strings" "strings"
"code.humancabbage.net/sam/moonmath/coindesk" "code.humancabbage.net/sam/moonmath/coindesk"
"code.humancabbage.net/sam/moonmath/config" "code.humancabbage.net/sam/moonmath/config"
"code.humancabbage.net/sam/moonmath/tui" "code.humancabbage.net/sam/moonmath/tui"
"github.com/alecthomas/kong" "github.com/alecthomas/kong"
tea "github.com/charmbracelet/bubbletea"
) )
var CLI struct { var CLI struct {
Asset []string `short:"a" default:"BTC" help:"Asset(s) to project."` Asset []string `short:"a" default:"BTC" help:"Asset(s) to project."`
ConfigFile string `short:"c" help:"Path to YAML configuration file."` ConfigFile string `short:"c" help:"Path to YAML configuration file."`
Perf bool `help:"Display internal performance stats."`
} }
func main() { func main() {
logFile := setupLogging()
defer func() {
_ = logFile.Close()
}()
ctx := kong.Parse(&CLI) ctx := kong.Parse(&CLI)
if ctx.Error != nil { if ctx.Error != nil {
fail(ctx.Error) fail(ctx.Error)
@@ -30,12 +39,45 @@ func main() {
asset := coindesk.Asset(strings.ToUpper(CLI.Asset[i])) asset := coindesk.Asset(strings.ToUpper(CLI.Asset[i]))
assets = append(assets, asset) assets = append(assets, asset)
} }
p := tui.New(assets, allCfg) m := tui.New(assets, allCfg, CLI.Perf)
p := tea.NewProgram(m,
tea.WithAltScreen(),
tea.WithFPS(30),
)
if _, err := p.Run(); err != nil { if _, err := p.Run(); err != nil {
fail(err) fail(err)
} }
} }
func setupLogging() io.Closer {
homePath, err := os.UserHomeDir()
if err != nil {
panic(err)
}
programConfigPath := filepath.Join(homePath, ".moonmath")
err = os.MkdirAll(programConfigPath, 0755)
if err != nil {
panic(err)
}
errLogPath := filepath.Join(programConfigPath, "errors.log")
errLogFile, err := os.OpenFile(
errLogPath,
os.O_CREATE|os.O_APPEND|os.O_WRONLY,
0600,
)
if err != nil {
panic(err)
}
slog.SetDefault(slog.New(
slog.NewTextHandler(errLogFile, &slog.HandlerOptions{
Level: slog.LevelError,
})))
return errLogFile
}
func fail(err error) { func fail(err error) {
fmt.Printf("program error: %v\n", err) fmt.Printf("program error: %v\n", err)
os.Exit(1) os.Exit(1)

View File

@@ -3,6 +3,7 @@ package asset
import ( import (
"context" "context"
"fmt" "fmt"
"log/slog"
"time" "time"
"code.humancabbage.net/sam/moonmath/coindesk" "code.humancabbage.net/sam/moonmath/coindesk"
@@ -29,7 +30,7 @@ type Msg struct {
inner tea.Msg inner tea.Msg
} }
func New(cfg config.Data) (m Model) { func New(cfg config.Asset) (m Model) {
m.math = moon.NewMath( m.math = moon.NewMath(
cfg.Asset, cfg.Asset,
cfg.Goals, cfg.Goals,
@@ -83,16 +84,11 @@ func New(cfg config.Data) (m Model) {
m.indicator = spinner.New() m.indicator = spinner.New()
m.indicator.Spinner = spinner.Points m.indicator.Spinner = spinner.Points
m.indicator.Style = lipgloss.NewStyle(). m.indicator.Style = indicatorNormalStyle
Foreground(lipgloss.Color("69"))
return return
} }
func (m Model) Handles(a coindesk.Asset) bool {
return m.math.Asset == a
}
func (m Model) Init() tea.Cmd { func (m Model) Init() tea.Cmd {
return tea.Batch( return tea.Batch(
m.indicator.Tick, m.indicator.Tick,
@@ -102,39 +98,41 @@ func (m Model) Init() tea.Cmd {
) )
} }
func (m Model) Handles(a coindesk.Asset) bool {
return m.math.Asset == a
}
func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) { func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
switch msg := msg.(type) { switch msg := msg.(type) {
case Msg: case Msg:
switch msg := msg.inner.(type) { switch msg := msg.inner.(type) {
case refresh: case refresh:
m.refreshing = true m.refreshing = true
return m, func() tea.Msg { m.indicator.Style = indicatorNormalStyle
// TODO: log errors
_ = m.math.Refresh(context.TODO())
return Msg{m.math.Asset, m.math}
}
case moon.Math:
m.math = msg
refillProperties(&m)
refillProjections(&m)
return m, tea.Batch( return m, tea.Batch(
// schedule the next refresh m.resumeIndicator,
tea.Tick(time.Second*30, m.refresh,
func(t time.Time) tea.Msg { m.scheduleRefresh(),
return Msg{m.math.Asset, refresh{}}
}),
// wait a bit to stop the indicator, so that it's more obvious
// even when the refresh completes quickly.
tea.Tick(time.Millisecond*500,
func(t time.Time) tea.Msg {
return Msg{m.math.Asset, stopIndicator{}}
}),
) )
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: case stopIndicator:
m.refreshing = false m.refreshing = false
return m, nil return m, nil
} }
case spinner.TickMsg: case spinner.TickMsg:
if !m.refreshing {
return m, nil
}
var cmd tea.Cmd var cmd tea.Cmd
m.indicator, cmd = m.indicator.Update(msg) m.indicator, cmd = m.indicator.Update(msg)
return m, cmd return m, cmd
@@ -143,8 +141,51 @@ func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
} }
type refresh struct{} type refresh struct{}
type update struct {
math moon.Math
err error
}
type stopIndicator struct{} 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) { func refillProperties(m *Model) {
rows := []table.Row{ rows := []table.Row{
{"Asset", string(m.math.Asset)}, {"Asset", string(m.math.Asset)},
@@ -154,14 +195,34 @@ func refillProperties(m *Model) {
} }
func refillProjections(m *Model) { func refillProjections(m *Model) {
rows := []table.Row{m.math.Labels} cols := []table.Row{m.math.Labels}
for i := range m.math.Columns { for i := range m.math.Columns {
rows = append(rows, m.math.Columns[i].Column()) entries := renderEntries(m.math.Columns[i])
cols = append(cols, entries)
} }
rows = transpose(rows) rows := transpose(cols)
m.projections.SetRows(rows) 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 { func transpose(slice []table.Row) []table.Row {
xl := len(slice[0]) xl := len(slice[0])
yl := len(slice) yl := len(slice)
@@ -199,3 +260,9 @@ func (m Model) View() string {
var baseStyle = lipgloss.NewStyle(). var baseStyle = lipgloss.NewStyle().
BorderStyle(lipgloss.RoundedBorder()). BorderStyle(lipgloss.RoundedBorder()).
BorderForeground(lipgloss.Color("240")) BorderForeground(lipgloss.Color("240"))
var indicatorNormalStyle = lipgloss.NewStyle().
Foreground(lipgloss.Color("69"))
var indicatorErrorStyle = lipgloss.NewStyle().
Foreground(lipgloss.Color("160"))

42
tui/perf/perf.go Normal file
View File

@@ -0,0 +1,42 @@
package perf
import (
"fmt"
"sync/atomic"
tea "github.com/charmbracelet/bubbletea"
)
type Model struct {
updates *atomic.Int64
views *atomic.Int64
}
func New() (m Model) {
m.updates = new(atomic.Int64)
m.views = new(atomic.Int64)
return
}
func (m Model) Init() tea.Cmd {
return nil
}
func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
return m, nil
}
func (m Model) View() string {
updates := m.updates.Load()
views := m.views.Load()
s := fmt.Sprintf("updates: %d\tviews: %d", updates, views)
return s
}
func (m Model) AddUpdate() {
m.updates.Add(1)
}
func (m Model) AddView() {
m.views.Add(1)
}

View File

@@ -6,33 +6,27 @@ import (
"code.humancabbage.net/sam/moonmath/coindesk" "code.humancabbage.net/sam/moonmath/coindesk"
"code.humancabbage.net/sam/moonmath/config" "code.humancabbage.net/sam/moonmath/config"
"code.humancabbage.net/sam/moonmath/tui/asset" "code.humancabbage.net/sam/moonmath/tui/asset"
"code.humancabbage.net/sam/moonmath/tui/perf"
tea "github.com/charmbracelet/bubbletea" tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss" "github.com/charmbracelet/lipgloss"
) )
type Model struct { type Model struct {
assets []asset.Model assets []asset.Model
stats perf.Model
displayStats bool
} }
func New(assets []coindesk.Asset, cfg config.All) (p *tea.Program) { func New(assets []coindesk.Asset, cfg config.Root, displayStats bool) (m Model) {
model := newModel(assets, cfg) m.stats = perf.New()
p = tea.NewProgram( m.displayStats = displayStats
model, // construct models for each asset, but remove dupes
tea.WithAltScreen(),
tea.WithFPS(30),
)
return
}
func newModel(assets []coindesk.Asset, cfg config.All) (m Model) {
// construct models for each asset, but don't filter out dupes
seen := map[coindesk.Asset]struct{}{} seen := map[coindesk.Asset]struct{}{}
for _, a := range assets { for _, a := range assets {
_, ok := seen[a] if _, ok := seen[a]; ok {
if ok {
continue continue
} }
assetCfg := cfg.GetData(a) assetCfg := cfg.ForAsset(a)
assetModel := asset.New(assetCfg) assetModel := asset.New(assetCfg)
m.assets = append(m.assets, assetModel) m.assets = append(m.assets, assetModel)
seen[a] = struct{}{} seen[a] = struct{}{}
@@ -52,6 +46,7 @@ func (m Model) Init() tea.Cmd {
} }
func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
m.stats.AddUpdate()
switch msg := msg.(type) { switch msg := msg.(type) {
// handle keys for quitting // handle keys for quitting
case tea.KeyMsg: case tea.KeyMsg:
@@ -78,11 +73,15 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
} }
func (m Model) View() string { func (m Model) View() string {
m.stats.AddView()
var ss []string var ss []string
for i := range m.assets { for i := range m.assets {
s := m.assets[i].View() s := m.assets[i].View()
ss = append(ss, s) ss = append(ss, s)
} }
if m.displayStats {
ss = append(ss, m.stats.View())
}
r := lipgloss.JoinVertical(lipgloss.Center, ss...) r := lipgloss.JoinVertical(lipgloss.Center, ss...)
return r return r
} }