// TODO(doc): document TUI lifecycle package tui import ( "fmt" "time" "github.com/NimbleMarkets/ntcharts/linechart/streamlinechart" "github.com/charmbracelet/bubbles/viewport" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" ) // Bubbletea model type Model struct { width int Addresses []Address // as defined in internal/tui/types.go viewport viewport.Model UpdateSpeed time.Duration ChartHeight int } func InitialModel(addresses []string, speed time.Duration, chartHeight int) Model { var model Model model.viewport = viewport.New(0, 0) model.viewport.MouseWheelEnabled = true model.UpdateSpeed = speed model.ChartHeight = chartHeight for _, address := range addresses { var addr Address addr.max_results = 80 addr.Address = address model.Addresses = append(model.Addresses, addr) } return model } func (m Model) Init() tea.Cmd { return m.Tick() } type tickMsg time.Time func (m Model) Tick() tea.Cmd { return tea.Tick(time.Millisecond*m.UpdateSpeed, func(t time.Time) tea.Msg { return tickMsg(t) }) } func (m Model) content() string { var headerStyle = lipgloss.NewStyle(). Bold(true). Italic(true) var blockStyle = lipgloss.NewStyle(). Width(m.width). Align(lipgloss.Center) output := "\n" for _, address := range m.Addresses { if len(address.results) == 0 { output = output + fmt.Sprintf("\n%s\tloading...", headerStyle.Render(address.Address)) } else if m.viewport.Width != 0 && m.viewport.Height != 0 { output = output + fmt.Sprintf("\n%s", blockStyle.Render(headerStyle.Render(address.Address))) // Linechart var slc streamlinechart.Model if m.ChartHeight == 0 { slc = streamlinechart.New(m.width, m.viewport.Height-9) } else { slc = streamlinechart.New(m.width, m.ChartHeight) } for _, v := range address.results { slc.Push(v) } slc.Draw() output = output + blockStyle.Render(fmt.Sprintf("\n%s\n", slc.View())) } } return output } func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { var cmd tea.Cmd var cmds []tea.Cmd switch msg := msg.(type) { // if case is KeyMsg (keypress) case tea.WindowSizeMsg: m.width = msg.Width for i, address := range m.Addresses { address.max_results = m.width m.Addresses[i] = address } m.viewport.Height = msg.Height - 4 m.viewport.Width = msg.Width m.viewport.YPosition = 1 case tea.KeyMsg: if k := msg.String(); k == "j" { // scroll down m.viewport.LineDown(8) } else if k == "k" { // scroll up m.viewport.LineUp(8) } else { return m, tea.Quit } case tickMsg: cmds = append(cmds, m.Tick()) cmds = append(cmds, m.Poll) } m.viewport, cmd = m.viewport.Update(msg) m.viewport.SetContent(m.content()) cmds = append(cmds, cmd) // cmds = append(cmds, m.Poll) return m, tea.Batch(cmds...) } func (m Model) View() string { var headerStyle = lipgloss.NewStyle(). Width(m.width). Align(lipgloss.Center). Italic(true). Faint(true) var footerStyle = lipgloss.NewStyle(). Width(m.width). Align(lipgloss.Center). Italic(true). Faint(true) header := headerStyle.Render("pingo v0") footer := footerStyle.Render("\nj/k: down/up\t|\tq/ctrl-c/esc: quit\n") return fmt.Sprintf("\n%s\n%s\n%s", header, m.viewport.View(), footer) } // A wrapper for the underlying [tui.Address.Poll] function. For each address in // [tui.Model.Addresses], run its respective Poll function and update [tui.Model] // // NOTE(async): this function fully blocks execution of the current thread. func (m Model) Poll() tea.Msg { for i, element := range m.Addresses { element.Poll() // element.results = append(element.results, -1) m.Addresses[i] = element } return nil }