// TODO(doc): document TUI lifecycle // TODO(test): write unittests for types.go package pingo import ( "fmt" "slices" "time" "charm.land/bubbles/v2/viewport" tea "charm.land/bubbletea/v2" "charm.land/lipgloss/v2" ) // Style Defintions var ( // A style for chart headers headerStyle = lipgloss.NewStyle(). Bold(true). Italic(true) // A style for info text infoStyle = lipgloss.NewStyle(). Italic(true). Faint(true) // A style for the secondary colour secondaryColor = lipgloss.NewStyle(). Foreground(lipgloss.Color("#7b2d26")) // A style for the primary colour // primaryColor = lipgloss.NewStyle(). // Foreground(lipgloss.Color("#f0f3f5")) ) type ( // tea.Msg signatures tickMsg time.Time pollResultMsg struct { results []float64 index int err error } ) // 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.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() } 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 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("%s\tloading...", headerStyle.Render(address.Address)) } else if m.viewport.Width() != 0 && m.viewport.Height() != 0 { if slices.Contains(address.results, -1) { output = output + blockStyle.Render(headerStyle.Render( fmt.Sprintf("\n%s\t%s", secondaryColor.Render(address.Address), infoStyle.Render("(connection unstable)"), ), )) } else { output = output + fmt.Sprintf("\n%s", blockStyle.Render(headerStyle.Render(address.Address))) } // Linechart // viewportHeight := m.viewport.Height() - 4 // var slc streamlinechart.Model // if m.ChartHeight == 0 && len(m.Addresses) == 1 { // slc = streamlinechart.New(m.width, viewportHeight) // } else if m.ChartHeight == 0 && len(m.Addresses) > 1 { // slc = streamlinechart.New(m.width, viewportHeight-5) // } 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())) output = output + blockStyle.Render(fmt.Sprintf("\n%f", address.Last())) } } 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.SetHeight(msg.Height - 4) m.viewport.SetWidth(msg.Width) m.viewport.YPosition = 1 case tea.KeyPressMsg: if k := msg.String(); k == "j" { // scroll down m.viewport.ScrollDown(1) } else if k == "k" { // scroll up m.viewport.ScrollUp(1) } else { if k == "ctrl+c" { cmds = append(cmds, tea.Quit) } } case tickMsg: cmds = append(cmds, m.Tick()) cmds = append(cmds, m.Poll()) case pollResultMsg: m.Addresses[msg.index].results = msg.results } m.viewport.SetContent(m.content()) m.viewport, cmd = m.viewport.Update(msg) cmds = append(cmds, cmd) // cmds = append(cmds, m.Poll) return m, tea.Batch(cmds...) } func (m Model) View() tea.View { 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") content := fmt.Sprintf("\n%s\n%s\n%s", header, m.viewport.View(), footer) var v tea.View v.SetContent(content) v.AltScreen = true return v } // Returns a batched set of tea.Cmd functions for each address. func (m Model) Poll() tea.Cmd { var cmds []tea.Cmd for i, element := range m.Addresses { cmds = append(cmds, func() tea.Msg { results, err := element.Poll() return pollResultMsg{results: results, err: err, index: i} }) } return tea.Batch(cmds...) }