tui.go 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214
  1. // TODO(chart): dynamically render charts to fit height on not set
  2. //
  3. // TODO(styling): allow end user to define their own styles when hacking.
  4. //
  5. // Pingo defines an extensible TUI based on the bubbletea framework (v2). An
  6. // executable entry point is defined via cmd/pingo.go. For more on this topic,
  7. // see the Readme.
  8. //
  9. // The pingo TUI is a fully fledged and extensible "bubble" (read: widget) that
  10. // can be fully implemented as an element in further terminal applications.
  11. //
  12. // NOTE: the bubbletea framework and subsequent "bubble" concept are beyond the
  13. // scope of this documentation. For more, see the Readme.
  14. //
  15. // Pingo defines a constructor function for the Peak bubble. It takes three
  16. // arguments:
  17. //
  18. // - addresses([]string): the hosts to ping. The length must be greater than
  19. // or equal to 1
  20. //
  21. // - speed(time.Duration): the polling interval
  22. //
  23. // - chartHeight(int): the desired height of the resulting charts. This
  24. // argument is integral to ensuring desired rendering of charts, when
  25. // displaying multiple hosts.
  26. //
  27. // NOTE: if chartHeight is 0, the chart will render to Model.Height
  28. //
  29. // NOTE: chartHeight is ignored when only one address or host is provided
  30. //
  31. // For more, please please see InitialPeakBubble()
  32. //
  33. // Pingo defines the following bubbletea.Cmd functions:
  34. //
  35. // - Model.Poll() tea.Msg: used to asynchronously call all Model.Addresses.Poll()
  36. // functions.
  37. //
  38. // NOTE: the project does not implement a timing mechanism for polling Addresses.
  39. // It is expected that the developer implements a timing mechanism.
  40. //
  41. // CRITICAL NOTE: it is possible to exceed rate limits from remote targets. It is
  42. // recommended to ensure that you implement a minimum time between calls to Poll.
  43. // As timing may be handled in a number of ways, it exceeds the scope of this
  44. // project to provide one.
  45. //
  46. // For an example implementation, see cmd/pingo.go
  47. package pingo
  48. import (
  49. "fmt"
  50. "slices"
  51. tea "charm.land/bubbletea/v2"
  52. "charm.land/lipgloss/v2"
  53. "github.com/NimbleMarkets/ntcharts/linechart/streamlinechart"
  54. )
  55. // Style Definitions
  56. var (
  57. // footer styles
  58. titleStyle = lipgloss.NewStyle().
  59. Align(lipgloss.Center). // implies consumer functions will apply a width
  60. Italic(true).
  61. Faint(true)
  62. // footer style
  63. footerStyle = lipgloss.NewStyle().
  64. Align(lipgloss.Center). // implies consumer functions will apply a width
  65. Italic(true).
  66. Faint(true)
  67. // A style for chart headers
  68. headerStyle = lipgloss.NewStyle().
  69. Bold(true).
  70. Italic(true)
  71. // A style for info text
  72. infoStyle = lipgloss.NewStyle().
  73. Italic(true).
  74. Faint(true)
  75. // A style for the secondary colour
  76. secondaryColor = lipgloss.NewStyle().
  77. Foreground(lipgloss.Color("#7b2d26"))
  78. // A style for the primary colour
  79. // primaryColor = lipgloss.NewStyle().
  80. // Foreground(lipgloss.Color("#f0f3f5"))
  81. // A style for handling center-aligning
  82. blockStyle = lipgloss.NewStyle().
  83. Align(lipgloss.Center)
  84. // borderStyle = lipgloss.NewStyle().
  85. // BorderForeground(lipgloss.Color("8")).
  86. // // Padding(1, 2).
  87. // BorderStyle(lipgloss.NormalBorder())
  88. )
  89. type ( // tea.Msg signatures
  90. pollResultMsg struct {
  91. results []float64
  92. index int
  93. err error
  94. }
  95. )
  96. // Bubbletea bubble
  97. type Peak struct {
  98. Addresses []Address // as defined in internal/tui/types.go
  99. ChartHeight int
  100. Height int
  101. Width int
  102. }
  103. // Instantiates a Peak bubble with the provided arguments.
  104. func InitialPeakBubble(addresses []string, width, height, chartHeight int) Peak {
  105. var model Peak
  106. model.ChartHeight = chartHeight
  107. model.Width = width
  108. model.Height = height
  109. for _, address := range addresses {
  110. var addr Address
  111. addr.MaxResults = 80
  112. addr.Address = address
  113. model.Addresses = append(model.Addresses, addr)
  114. }
  115. return model
  116. }
  117. // Provides initial polling for the bubble. Returns the result of Peak.Poll
  118. func (p Peak) Init() tea.Cmd {
  119. return p.Poll()
  120. }
  121. // The update function for the Peak bubble.
  122. func (p Peak) Update(msg tea.Msg) (Peak, tea.Cmd) {
  123. var cmd tea.Cmd
  124. var cmds []tea.Cmd
  125. switch msg := msg.(type) {
  126. case pollResultMsg:
  127. p.Addresses[msg.index].Results = msg.results
  128. }
  129. for i := range p.Addresses {
  130. p.Addresses[i].MaxResults = p.Width
  131. }
  132. cmds = append(cmds, cmd)
  133. return p, tea.Batch(cmds...)
  134. }
  135. func (p Peak) View() tea.View {
  136. var content string
  137. for _, address := range p.Addresses {
  138. if len(address.Results) == 0 {
  139. content = content + fmt.Sprintf("\n%s\tloading...", headerStyle.Render(address.Address))
  140. } else if p.Width != 0 && p.Height != 0 {
  141. if slices.Contains(address.Results, -1) {
  142. content = content + fmt.Sprintf("\n%s",
  143. blockStyle.Width(p.Width).Render(headerStyle.Render(
  144. secondaryColor.Render(address.Address),
  145. infoStyle.Render("(connection unstable)"),
  146. ),
  147. ))
  148. } else {
  149. content = content + fmt.Sprintf("\n%s",
  150. blockStyle.Width(p.Width).Render(headerStyle.Render(address.Address)))
  151. }
  152. // Linechart
  153. // set chartHeight - vertical margin
  154. // chartHeight := p.Height - 2 // p.getVerticalMargin()
  155. var slc streamlinechart.Model
  156. if p.ChartHeight == 0 && len(p.Addresses) == 1 { // catch user specified fullscreen
  157. // render chart at fullscreen
  158. slc = streamlinechart.New(p.Width, p.Height-1)
  159. } else if p.ChartHeight == 0 && len(p.Addresses) > 1 { // catch user specified fullscreen
  160. // render chart at fullscreen minus a few lines to hint at scrolling
  161. slc = streamlinechart.New(p.Width, p.Height-2)
  162. } else {
  163. slc = streamlinechart.New(p.Width, p.ChartHeight-1)
  164. }
  165. for _, v := range address.Results {
  166. slc.Push(v)
  167. }
  168. slc.Draw()
  169. content = content + fmt.Sprintf("\n%s", slc.View())
  170. }
  171. }
  172. var v tea.View
  173. v.SetContent(content)
  174. v.AltScreen = true
  175. return v
  176. }
  177. // Returns a batched set of tea.Cmd that call Address.Poll functions for each
  178. // address.
  179. func (p Peak) Poll() tea.Cmd {
  180. var cmds []tea.Cmd
  181. for i, element := range p.Addresses {
  182. cmds = append(cmds, func() tea.Msg {
  183. results, err := element.Poll()
  184. return pollResultMsg{results: results, err: err, index: i}
  185. })
  186. }
  187. return tea.Batch(cmds...)
  188. }