// 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 Peak bubble. It takes three // arguments: // // - addresses([]string): the hosts to ping. The length must be greater than // or equal to 1 // // - width(int): the width of the bubble // // - height(int): the height of the bubble // // - 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 // // NOTE: if a specific chartHeight is specified, it is possible to exceeed // desired height. // // For more, please please see InitialPeakBubble() // // Pingo defines the following bubbletea.Cmd functions: // // - Peak.Poll() tea.Msg: used to asynchronously call all Model.Addresses.Poll() // functions. // // NOTE: the project does not implement a timing mechanism for polling Addresses. // It is expected that the developer implements a timing mechanism. // // CRITICAL NOTE: it is possible to exceed rate limits from remote targets. It is // recommended to ensure that you implement a minimum time between calls to Poll. // As timing may be handled in a number of ways, it exceeds the scope of this // project to provide one. // // For an example implementation, including a recommended timing mechanisim, // see cmd/pingo.go. // // TODO(chart): dynamically render charts to fit height on not set // // TODO(styling): allow end user to define their own styles when hacking. // // TODO(deprecate chart height): in future, specifying chart height's directly // will be deprecated in favour of a dynamically determined chart height. (see // "TODO(chart)") 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 bubble type Peak struct { Addresses []Address // as defined in internal/tui/types.go ChartHeight int // DEPRECATE Height int Width int } // Instantiates a Peak bubble with the provided arguments. func InitialPeakBubble(addresses []string, width, height, chartHeight int) Peak { var model Peak 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 } // Provides initial polling for the bubble. Returns the result of Peak.Poll func (p Peak) Init() tea.Cmd { return p.Poll() } // The update function for the Peak bubble. func (p Peak) Update(msg tea.Msg) (Peak, tea.Cmd) { var cmd tea.Cmd var cmds []tea.Cmd switch msg := msg.(type) { case pollResultMsg: p.Addresses[msg.index].Results = msg.results } for i := range p.Addresses { p.Addresses[i].MaxResults = p.Width } cmds = append(cmds, cmd) return p, tea.Batch(cmds...) } func (p Peak) View() tea.View { var content string for _, address := range p.Addresses { if len(address.Results) == 0 { content = content + fmt.Sprintf("\n%s\tloading...", headerStyle.Render(address.Address)) } else if p.Width != 0 && p.Height != 0 { if slices.Contains(address.Results, -1) { content = content + fmt.Sprintf("\n%s", blockStyle.Width(p.Width).Render(headerStyle.Render( secondaryColor.Render(address.Address), infoStyle.Render("(connection unstable)"), ), )) } else { content = content + fmt.Sprintf("\n%s", blockStyle.Width(p.Width).Render(headerStyle.Render(address.Address))) } // Linechart // set chartHeight - vertical margin // chartHeight := p.Height - 2 // p.getVerticalMargin() var slc streamlinechart.Model if p.ChartHeight == 0 && len(p.Addresses) == 1 { // catch user specified fullscreen // render chart at fullscreen slc = streamlinechart.New(p.Width, p.Height-1) } else if p.ChartHeight == 0 && len(p.Addresses) > 1 { // catch user specified fullscreen // render chart at fullscreen minus a few lines to hint at scrolling slc = streamlinechart.New(p.Width, p.Height-2) } else { slc = streamlinechart.New(p.Width, p.ChartHeight-1) // DEPRECATE } for _, v := range address.Results { slc.Push(v) } slc.Draw() content = content + fmt.Sprintf("\n%s", slc.View()) } } var v tea.View v.SetContent(content) v.AltScreen = true return v } // Returns a batched set of tea.Cmd that call Address.Poll functions for each // address. func (p Peak) Poll() tea.Cmd { var cmds []tea.Cmd for i, element := range p.Addresses { cmds = append(cmds, func() tea.Msg { results, err := element.Poll() return pollResultMsg{results: results, err: err, index: i} }) } return tea.Batch(cmds...) }