tui.go 4.7 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. )
  12. // Style Defintions
  13. var (
  14. // A style for chart headers
  15. headerStyle = lipgloss.NewStyle().
  16. Bold(true).
  17. Italic(true)
  18. // A style for info text
  19. infoStyle = lipgloss.NewStyle().
  20. Italic(true).
  21. Faint(true)
  22. // A style for the secondary colour
  23. secondaryColor = lipgloss.NewStyle().
  24. Foreground(lipgloss.Color("#7b2d26"))
  25. // A style for the primary colour
  26. // primaryColor = lipgloss.NewStyle().
  27. // Foreground(lipgloss.Color("#f0f3f5"))
  28. )
  29. type ( // tea.Msg signatures
  30. tickMsg time.Time
  31. pollResultMsg struct {
  32. results []float64
  33. index int
  34. err error
  35. }
  36. )
  37. // Bubbletea model
  38. type Model struct {
  39. width int
  40. Addresses []Address // as defined in internal/tui/types.go
  41. viewport viewport.Model
  42. UpdateSpeed time.Duration
  43. ChartHeight int
  44. }
  45. func InitialModel(addresses []string, speed time.Duration, chartHeight int) Model {
  46. var model Model
  47. model.viewport.MouseWheelEnabled = true
  48. model.UpdateSpeed = speed
  49. model.ChartHeight = chartHeight
  50. for _, address := range addresses {
  51. var addr Address
  52. addr.max_results = 80
  53. addr.Address = address
  54. model.Addresses = append(model.Addresses, addr)
  55. }
  56. return model
  57. }
  58. func (m Model) Init() tea.Cmd {
  59. return m.Tick()
  60. }
  61. func (m Model) Tick() tea.Cmd {
  62. return tea.Tick(time.Millisecond*m.UpdateSpeed, func(t time.Time) tea.Msg {
  63. return tickMsg(t)
  64. })
  65. }
  66. func (m Model) content() string {
  67. var blockStyle = lipgloss.NewStyle().
  68. Width(m.width).
  69. Align(lipgloss.Center)
  70. output := "\n"
  71. for _, address := range m.Addresses {
  72. if len(address.results) == 0 {
  73. output = output + fmt.Sprintf("%s\tloading...", headerStyle.Render(address.Address))
  74. } else if m.viewport.Width() != 0 && m.viewport.Height() != 0 {
  75. if slices.Contains(address.results, -1) {
  76. output = output + blockStyle.Render(headerStyle.Render(
  77. fmt.Sprintf("\n%s\t%s",
  78. secondaryColor.Render(address.Address),
  79. infoStyle.Render("(connection unstable)"),
  80. ),
  81. ))
  82. } else {
  83. output = output + fmt.Sprintf("\n%s",
  84. blockStyle.Render(headerStyle.Render(address.Address)))
  85. }
  86. // Linechart
  87. // viewportHeight := m.viewport.Height() - 4
  88. // var slc streamlinechart.Model
  89. // if m.ChartHeight == 0 && len(m.Addresses) == 1 {
  90. // slc = streamlinechart.New(m.width, viewportHeight)
  91. // } else if m.ChartHeight == 0 && len(m.Addresses) > 1 {
  92. // slc = streamlinechart.New(m.width, viewportHeight-5)
  93. // } else {
  94. // slc = streamlinechart.New(m.width, m.ChartHeight)
  95. // }
  96. // for _, v := range address.results {
  97. // slc.Push(v)
  98. // }
  99. // slc.Draw()
  100. // output = output + blockStyle.Render(fmt.Sprintf("\n%s\n", slc.View()))
  101. output = output + blockStyle.Render(fmt.Sprintf("\n%f", address.Last()))
  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. }