* Full screen ("alt framebuffer").
* Rounded borders.
* Spinner that starts during a refresh.
* Colored table headers.
* Table header have bottom border.
			
			
This commit is contained in:
		@@ -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)},
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -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)
 | 
			
		||||
	}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										120
									
								
								tui/tui.go
									
									
									
									
									
								
							
							
						
						
									
										120
									
								
								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"))
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user