From 4a4089965349e162ab9f8dea22910617d77d7cb5 Mon Sep 17 00:00:00 2001 From: Sam Fredrickson Date: Thu, 21 Mar 2024 02:28:56 -0700 Subject: [PATCH] Spice up the UI. * Full screen ("alt framebuffer"). * Rounded borders. * Spinner that starts during a refresh. * Colored table headers. * Table header have bottom border. --- moon/moon.go | 2 - moonmath.go | 3 +- tui/tui.go | 120 +++++++++++++++++++++++++++++++++------------------ 3 files changed, 78 insertions(+), 47 deletions(-) diff --git a/moon/moon.go b/moon/moon.go index e524f5c..f3986e2 100644 --- a/moon/moon.go +++ b/moon/moon.go @@ -146,8 +146,6 @@ var DefaultGoals = []Goal{ var DefaultConstantBases = []ConstantBase{ {"2020-", time.Unix(1577836800, 0)}, - {"2019-", time.Unix(1546300800, 0)}, - {"2018-", time.Unix(1514764800, 0)}, {"2017-", time.Unix(1483228800, 0)}, } diff --git a/moonmath.go b/moonmath.go index 35f0319..c1c91b1 100644 --- a/moonmath.go +++ b/moonmath.go @@ -7,7 +7,6 @@ import ( "code.humancabbage.net/sam/moonmath/config" "code.humancabbage.net/sam/moonmath/tui" "github.com/alecthomas/kong" - tea "github.com/charmbracelet/bubbletea" ) var CLI struct { @@ -23,7 +22,7 @@ func main() { if err != nil { fail(err) } - p := tea.NewProgram(tui.New(cfg)) + p := tui.New(cfg) if _, err := p.Run(); err != nil { fail(err) } diff --git a/tui/tui.go b/tui/tui.go index a495187..2e5c98a 100644 --- a/tui/tui.go +++ b/tui/tui.go @@ -13,60 +13,83 @@ import ( "github.com/charmbracelet/lipgloss" ) +func New(cfg config.Data) (p *tea.Program) { + p = tea.NewProgram( + newModel(cfg), + tea.WithAltScreen(), + tea.WithFPS(30), + ) + return +} + type Model struct { math moon.Math - reloading bool - indicator spinner.Model + refreshing bool + indicator spinner.Model - prices table.Model + properties table.Model projections table.Model } -func New(cfg config.Data) Model { - math := moon.NewMath( +func newModel(cfg config.Data) (m Model) { + m.math = moon.NewMath( cfg.Asset, cfg.Goals, - config.GetBases(&cfg)) + config.GetBases(&cfg), + ) tableStyle := table.DefaultStyles() - tableStyle.Selected = lipgloss.NewStyle() - prices := table.New( + 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: "Asset", Width: 6}, - {Title: "Price", Width: 9}, + {Title: "Property", Width: 9}, + {Title: "Value", Width: 9}, }), - table.WithHeight(1), + table.WithHeight(2), table.WithStyles(tableStyle), ) - projectionCols := []table.Column{ - {Title: "Labels", Width: 8}, + // projections table + + labelsWidth := 0 + for _, l := range m.math.Labels { + if len(l) > labelsWidth { + labelsWidth = len(l) + } } - for i := range math.Columns { + projectionCols := []table.Column{ + {Title: "Labels", Width: labelsWidth}, + } + for _, c := range m.math.Columns { projectionCols = append(projectionCols, table.Column{ - Title: math.Columns[i].Base.Label(), + Title: c.Base.Label(), Width: 10, }) } - projections := table.New( + m.projections = table.New( table.WithColumns(projectionCols), - table.WithHeight(len(math.Labels)), + table.WithHeight(len(m.math.Labels)), table.WithStyles(tableStyle), ) - indicator := spinner.New() - indicator.Spinner = spinner.Points - indicator.Style = lipgloss.NewStyle(). + // indicator spinner + + m.indicator = spinner.New() + m.indicator.Spinner = spinner.Points + m.indicator.Style = lipgloss.NewStyle(). Foreground(lipgloss.Color("69")) - return Model{ - math: math, - indicator: indicator, - prices: prices, - projections: projections, - } + return } func (m Model) Init() tea.Cmd { @@ -81,20 +104,32 @@ func (m Model) Init() tea.Cmd { func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { case refresh: - m.reloading = true + m.refreshing = true return m, func() tea.Msg { + // TODO: log errors _ = m.math.Refresh(context.TODO()) return m.math } case moon.Math: m.math = msg - m.reloading = false - refillPrice(&m) + refillProperties(&m) refillProjections(&m) - return m, tea.Tick(time.Second*30, - func(t time.Time) tea.Msg { - return refresh{} - }) + return m, tea.Batch( + // schedule the next refresh + tea.Tick(time.Second*30, + func(t time.Time) tea.Msg { + return 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 stopIndicator{} + }), + ) + case stopIndicator: + m.refreshing = false + return m, nil case spinner.TickMsg: var cmd tea.Cmd m.indicator, cmd = m.indicator.Update(msg) @@ -109,15 +144,14 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } type refresh struct{} +type stopIndicator struct{} -func refillPrice(m *Model) { +func refillProperties(m *Model) { rows := []table.Row{ - []string{ - string(m.math.Asset), - fmt.Sprintf("$%0.2f", m.math.CurrentPrice), - }, + {"Asset", string(m.math.Asset)}, + {"Price", fmt.Sprintf("$%0.2f", m.math.CurrentPrice)}, } - m.prices.SetRows(rows) + m.properties.SetRows(rows) } func refillProjections(m *Model) { @@ -147,16 +181,16 @@ func transpose(slice []table.Row) []table.Row { func (m Model) View() string { var s string indicator := "" - if m.reloading { + if m.refreshing { indicator = m.indicator.View() } right := lipgloss.JoinVertical( lipgloss.Center, - baseStyle.Render(m.prices.View()), + baseStyle.Render(m.properties.View()), indicator, ) s += lipgloss.JoinHorizontal( - lipgloss.Top, + lipgloss.Center, right, baseStyle.Render(m.projections.View()), ) @@ -164,5 +198,5 @@ func (m Model) View() string { } var baseStyle = lipgloss.NewStyle(). - BorderStyle(lipgloss.NormalBorder()). + BorderStyle(lipgloss.RoundedBorder()). BorderForeground(lipgloss.Color("240"))