package tui import ( "context" "fmt" "time" "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" ) var baseStyle = lipgloss.NewStyle(). BorderStyle(lipgloss.NormalBorder()). BorderForeground(lipgloss.Color("240")) type Model struct { math moon.Math reloading bool indicator spinner.Model prices table.Model projections table.Model } func New(cfg config.Data) Model { math := moon.NewMath( cfg.Asset, cfg.Goals, config.GetBases(&cfg)) tableStyle := table.DefaultStyles() tableStyle.Selected = lipgloss.NewStyle() prices := table.New( table.WithColumns([]table.Column{ {Title: "Asset", Width: 6}, {Title: "Price", Width: 9}, }), table.WithHeight(1), table.WithStyles(tableStyle), ) projectionCols := []table.Column{ {Title: "Labels", Width: 8}, } for i := range math.Columns { projectionCols = append(projectionCols, table.Column{ Title: math.Columns[i].Base.Label(), Width: 10, }) } projections := table.New( table.WithColumns(projectionCols), table.WithHeight(len(math.Labels)), table.WithStyles(tableStyle), ) indicator := spinner.New() indicator.Spinner = spinner.Points indicator.Style = lipgloss.NewStyle().Foreground(lipgloss.Color("69")) return Model{ math: math, indicator: indicator, prices: prices, projections: projections, } } func (m Model) Init() tea.Cmd { return tea.Batch( m.indicator.Tick, func() tea.Msg { return refresh{} }, ) } func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { case refresh: m.reloading = true return m, func() tea.Msg { _ = m.math.Refresh(context.TODO()) return m.math } case spinner.TickMsg: var cmd tea.Cmd m.indicator, cmd = m.indicator.Update(msg) return m, cmd case moon.Math: m.math = msg m.reloading = false refillPrice(&m) refillProjections(&m) return m, tea.Tick(time.Second*30, func(t time.Time) tea.Msg { return refresh{} }) case tea.KeyMsg: switch msg.String() { case "ctrl+c", "q", "esc": return m, tea.Quit } } return m, nil } type refresh struct{} func refillPrice(m *Model) { rows := []table.Row{ []string{string(m.math.Asset), fmt.Sprintf("$%0.2f", m.math.CurrentPrice)}, } m.prices.SetRows(rows) } func refillProjections(m *Model) { rows := []table.Row{m.math.Labels} for i := range m.math.Columns { rows = append(rows, m.math.Columns[i].Column()) } rows = transpose(rows) m.projections.SetRows(rows) } 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.reloading { indicator = m.indicator.View() } right := lipgloss.JoinVertical( lipgloss.Center, baseStyle.Render(m.prices.View()), indicator, ) s += lipgloss.JoinHorizontal( lipgloss.Top, right, baseStyle.Render(m.projections.View()), ) return s + "\n" }