// TODO(chart): dynamically render charts to fit height on not set // // TODO(styling): allow end user to define their own styles when hacking. // // Pingo defines an extensible TUI based on the bubbletea framework (v2). An // executable entry point is defined via cmd/pingo.go. For more on this topic, // see the Readme. // // The pingo TUI is a fully fledged and extensible "bubble" (read: widget) that // can be fully implemented as an element in further terminal applications. // // NOTE: the bubbletea framework and subsequent "bubble" concept are beyond the // scope of this documentation. For more, see the Readme. // // Pingo defines a constructor function for the Model. It takes three arguments: // // - addresses([]string): the hosts to ping. The length must be greater than // or equal to 1 // // - speed(time.Duration): the polling interval // // - chartHeight(int): the desired height of the resulting charts. This // argument is integral to ensuring desired rendering of charts, when // displaying multiple hosts. // // NOTE: if chartHeight is 0, the chart will render to Model.Height // // NOTE: chartHeight is ignored when only one address or host is provided // // For more, please please see InitialModel() // // Pingo defines two bubbletea.Cmd functions: // // - Model.Tick() tea.Cmd: emits a bubbletea.TickMsg after the time.Duration // specified via Model.UpdateSpeed // // NOTE: Model.Tick() is optional. If you choose not to use Model.Tick(), // it is recommended to enforce some minimum rate mechanism for calling // Poll(). Some servers maintain a ping rate limit, and is is possible to // exceed this rate trivially with the Poll() function. (Trust us, we know // from experience) // // NOTE: Model.Tick() is automatically emit by Model.Init() - therefore, // you can control the timing of polling by overloading the Init function. // // - Model.Poll() tea.Msg: used to asynchronously call all Model.Addresses.Poll() // functions. // // NOTE: Model.Poll() is automatically injected into the Model.Update() // life cycle after Model.Tick() resolves by Model.Update(). Functionally, // this means you can omit either Model.Tick() or Model.Poll(), respectively. // // For more, see the Readme or ./examples package pingo import ( "fmt" "slices" tea "charm.land/bubbletea/v2" "charm.land/lipgloss/v2" "github.com/NimbleMarkets/ntcharts/linechart/streamlinechart" ) // Style Definitions var ( // footer styles titleStyle = lipgloss.NewStyle(). Align(lipgloss.Center). // implies consumer functions will apply a width Italic(true). Faint(true) // footer style footerStyle = lipgloss.NewStyle(). Align(lipgloss.Center). // implies consumer functions will apply a width Italic(true). Faint(true) // 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")) // A style for handling center-aligning blockStyle = lipgloss.NewStyle(). Align(lipgloss.Center) // borderStyle = lipgloss.NewStyle(). // BorderForeground(lipgloss.Color("8")). // // Padding(1, 2). // BorderStyle(lipgloss.NormalBorder()) ) type ( // tea.Msg signatures pollResultMsg struct { results []float64 index int err error } ) // Bubbletea model type Model struct { Addresses []Address // as defined in internal/tui/types.go ChartHeight int Height int Width int } func InitialModel(addresses []string, width, height, chartHeight int) Model { var model Model model.ChartHeight = chartHeight model.Width = width model.Height = height for _, address := range addresses { var addr Address addr.MaxResults = 80 addr.Address = address model.Addresses = append(model.Addresses, addr) } return model } func (m Model) Init() tea.Cmd { return m.Poll() } func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) { var cmd tea.Cmd var cmds []tea.Cmd switch msg := msg.(type) { case pollResultMsg: m.Addresses[msg.index].Results = msg.results } for i := range m.Addresses { m.Addresses[i].MaxResults = m.Width } cmds = append(cmds, cmd) return m, tea.Batch(cmds...) } func (m Model) View() tea.View { var content string for _, address := range m.Addresses { if len(address.Results) == 0 { content = content + fmt.Sprintf("\n%s\tloading...", headerStyle.Render(address.Address)) } else if m.Width != 0 && m.Height != 0 { if slices.Contains(address.Results, -1) { content = content + fmt.Sprintf("\n%s", blockStyle.Width(m.Width).Render(headerStyle.Render( secondaryColor.Render(address.Address), infoStyle.Render("(connection unstable)"), ), )) } else { content = content + fmt.Sprintf("\n%s", blockStyle.Width(m.Width).Render(headerStyle.Render(address.Address))) } // Linechart // set chartHeight - vertical margin chartHeight := m.Height - 2 // m.getVerticalMargin() var slc streamlinechart.Model if m.ChartHeight == 0 && len(m.Addresses) == 1 { // catch user specified fullscreen // render chart at fullscreen slc = streamlinechart.New(m.Width, chartHeight) } else if m.ChartHeight == 0 && len(m.Addresses) > 1 { // catch user specified fullscreen // render chart at fullscreen minus a few lines to hint at scrolling slc = streamlinechart.New(m.Width, chartHeight-5) } else { slc = streamlinechart.New(m.Width, m.ChartHeight) } for _, v := range address.Results { slc.Push(v) } slc.Draw() content = content + fmt.Sprintf("\n%s", slc.View()) } } content = content + "\n" 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...) }