1
0

tui.go 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203
  1. // TODO(doc): document TUI lifecycle
  2. // TODO(test): write unittests for types.go
  3. package pingo
  4. import (
  5. "fmt"
  6. "slices"
  7. "time"
  8. "charm.land/bubbles/v2/viewport"
  9. tea "charm.land/bubbletea/v2"
  10. "charm.land/lipgloss/v2"
  11. "github.com/NimbleMarkets/ntcharts/linechart/streamlinechart"
  12. )
  13. // Style Defintions
  14. var (
  15. // A style for chart headers
  16. headerStyle = lipgloss.NewStyle().
  17. Bold(true).
  18. Italic(true)
  19. // A style for info text
  20. infoStyle = lipgloss.NewStyle().
  21. Italic(true).
  22. Faint(true)
  23. // A style for the secondary colour
  24. secondaryColor = lipgloss.NewStyle().
  25. Foreground(lipgloss.Color("#7b2d26"))
  26. // A style for the primary colour
  27. // primaryColor = lipgloss.NewStyle().
  28. // Foreground(lipgloss.Color("#f0f3f5"))
  29. )
  30. type ( // tea.Msg signatures
  31. tickMsg time.Time
  32. pollResultMsg struct {
  33. results []float64
  34. index int
  35. err error
  36. }
  37. )
  38. // Bubbletea model
  39. type Model struct {
  40. width int
  41. Addresses []Address // as defined in internal/tui/types.go
  42. viewport viewport.Model
  43. UpdateSpeed time.Duration
  44. ChartHeight int
  45. }
  46. func InitialModel(addresses []string, speed time.Duration, chartHeight int) Model {
  47. var model Model
  48. model.viewport.MouseWheelEnabled = true
  49. model.UpdateSpeed = speed
  50. model.ChartHeight = chartHeight
  51. for _, address := range addresses {
  52. var addr Address
  53. addr.max_results = 80
  54. addr.Address = address
  55. model.Addresses = append(model.Addresses, addr)
  56. }
  57. return model
  58. }
  59. func (m Model) Init() tea.Cmd {
  60. return m.Tick()
  61. }
  62. func (m Model) Tick() tea.Cmd {
  63. return tea.Tick(time.Millisecond*m.UpdateSpeed, func(t time.Time) tea.Msg {
  64. return tickMsg(t)
  65. })
  66. }
  67. func (m Model) content() string {
  68. var blockStyle = lipgloss.NewStyle().
  69. Width(m.width).
  70. Align(lipgloss.Center)
  71. output := "\n"
  72. for _, address := range m.Addresses {
  73. if len(address.results) == 0 {
  74. output = output + fmt.Sprintf("%s\tloading...", headerStyle.Render(address.Address))
  75. } else if m.viewport.Width() != 0 && m.viewport.Height() != 0 {
  76. if slices.Contains(address.results, -1) {
  77. output = output + blockStyle.Render(headerStyle.Render(
  78. fmt.Sprintf("\n%s\t%s",
  79. secondaryColor.Render(address.Address),
  80. infoStyle.Render("(connection unstable)"),
  81. ),
  82. ))
  83. } else {
  84. output = output + fmt.Sprintf("\n%s",
  85. blockStyle.Render(headerStyle.Render(address.Address)))
  86. }
  87. // Linechart
  88. viewportHeight := m.viewport.Height() - 4
  89. var slc streamlinechart.Model
  90. if m.ChartHeight == 0 && len(m.Addresses) == 1 {
  91. slc = streamlinechart.New(m.width, viewportHeight)
  92. } else if m.ChartHeight == 0 && len(m.Addresses) > 1 {
  93. slc = streamlinechart.New(m.width, viewportHeight-5)
  94. } else {
  95. slc = streamlinechart.New(m.width, m.ChartHeight)
  96. }
  97. for _, v := range address.results {
  98. slc.Push(v)
  99. }
  100. slc.Draw()
  101. output = output + blockStyle.Render(fmt.Sprintf("\n%s\n", slc.View()))
  102. }
  103. }
  104. return output
  105. }
  106. func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
  107. var cmd tea.Cmd
  108. var cmds []tea.Cmd
  109. switch msg := msg.(type) {
  110. // if case is KeyMsg (keypress)
  111. case tea.WindowSizeMsg:
  112. m.width = msg.Width
  113. for i, address := range m.Addresses {
  114. address.max_results = m.width
  115. m.Addresses[i] = address
  116. }
  117. m.viewport.SetHeight(msg.Height - 4)
  118. m.viewport.SetWidth(msg.Width)
  119. m.viewport.YPosition = 1
  120. case tea.KeyPressMsg:
  121. if k := msg.String(); k == "j" { // scroll down
  122. m.viewport.ScrollDown(1)
  123. } else if k == "k" { // scroll up
  124. m.viewport.ScrollUp(1)
  125. } else {
  126. if k == "ctrl+c" {
  127. cmds = append(cmds, tea.Quit)
  128. }
  129. }
  130. case tickMsg:
  131. cmds = append(cmds, m.Tick())
  132. cmds = append(cmds, m.Poll())
  133. case pollResultMsg:
  134. m.Addresses[msg.index].results = msg.results
  135. }
  136. m.viewport.SetContent(m.content())
  137. m.viewport, cmd = m.viewport.Update(msg)
  138. cmds = append(cmds, cmd)
  139. // cmds = append(cmds, m.Poll)
  140. return m, tea.Batch(cmds...)
  141. }
  142. func (m Model) View() tea.View {
  143. var headerStyle = lipgloss.NewStyle().
  144. Width(m.width).
  145. Align(lipgloss.Center).
  146. Italic(true).
  147. Faint(true)
  148. var footerStyle = lipgloss.NewStyle().
  149. Width(m.width).
  150. Align(lipgloss.Center).
  151. Italic(true).
  152. Faint(true)
  153. header := headerStyle.Render("pingo v0")
  154. footer := footerStyle.Render("\nj/k: down/up\t|\tq/ctrl-c/esc: quit\n")
  155. content := fmt.Sprintf("\n%s\n%s\n%s", header, m.viewport.View(), footer)
  156. var v tea.View
  157. v.SetContent(content)
  158. v.AltScreen = true
  159. return v
  160. }
  161. // Returns a batched set of tea.Cmd functions for each address.
  162. func (m Model) Poll() tea.Cmd {
  163. var cmds []tea.Cmd
  164. for i, element := range m.Addresses {
  165. cmds = append(cmds, func() tea.Msg {
  166. results, err := element.Poll()
  167. return pollResultMsg{results: results, err: err, index: i}
  168. })
  169. }
  170. return tea.Batch(cmds...)
  171. }