pingo.go 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165
  1. // A simple TUI application that charts latency times to specified hosts.
  2. //
  3. // The TUI uses the bubbletea framework for its lifecycle and the lipgloss
  4. // framework for stylized text rendering, for more on these concepts, see their
  5. // respective documentation.
  6. //
  7. // The bubbletea Model consists of a scroll aware container for the pingo.Peak
  8. // bubble, the minimum polling rate to enforce, and a pingo.Peak instance.
  9. //
  10. // The Model.Update function simply interprets ctrl+c signals as quit commands,
  11. // updates viewport/bubble sizes, and runs the viewport and pingo.Peak update
  12. // functions.
  13. //
  14. // The Model.View function prepends and appends a title and footer to the
  15. // viewport, as derived by Model.header() and Model.footer()
  16. //
  17. // The main function first captures the following arguments from the user:
  18. //
  19. // -s [as time.Duration]: the polling interval
  20. // -h [as int]: the desired chart height
  21. // addresses [as []string]: the addresses to poll.
  22. //
  23. // The main function will exit 1 if any of these arguments fail to validate.
  24. //
  25. // Finally, the main function executes bubbletea.Model.Run, exiting 1 upon any
  26. // errors.
  27. package main
  28. import (
  29. "flag"
  30. "fmt"
  31. "os"
  32. "pingo"
  33. "time"
  34. "charm.land/bubbles/v2/viewport"
  35. tea "charm.land/bubbletea/v2"
  36. "charm.land/lipgloss/v2"
  37. )
  38. // the bubbletea.Model for the executable
  39. type Model struct {
  40. viewport viewport.Model
  41. speed time.Duration
  42. p pingo.Peak
  43. }
  44. // timingMsg is used for tracking Peak.Poll timing intervals, emitted by an
  45. // anonymous function in the update function.
  46. type timingMsg time.Time
  47. // lipgloss styles for the tui
  48. var (
  49. // footer styles
  50. titleStyle = lipgloss.NewStyle().
  51. Align(lipgloss.Center). // implies consumer functions will apply a width
  52. Italic(true).
  53. Faint(true)
  54. // footer style
  55. footerStyle = lipgloss.NewStyle().
  56. Align(lipgloss.Center). // implies consumer functions will apply a width
  57. Italic(true).
  58. Faint(true)
  59. )
  60. // The tea.Cmd command to be called upon model initialization by bubbletea.
  61. func (m Model) Init() tea.Cmd {
  62. return tea.Tick(m.speed*time.Millisecond, func(t time.Time) tea.Msg { return timingMsg(t) })
  63. }
  64. // The core Update loop function.
  65. func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
  66. // data prep
  67. var cmds []tea.Cmd // final return
  68. var cmd tea.Cmd
  69. switch msg := msg.(type) {
  70. case tea.WindowSizeMsg: // handle window resizing
  71. // update pingo.Peak bubble
  72. m.p.Width = msg.Width
  73. m.p.Height = msg.Height - m.getVerticalMargin() - 1
  74. // if the viewport has not been initialized
  75. if m.viewport.Width() == 0 && m.viewport.Height() == 0 {
  76. m.viewport = viewport.New( // init viewport
  77. viewport.WithHeight(msg.Height-m.getVerticalMargin()),
  78. viewport.WithWidth(msg.Width),
  79. )
  80. m.viewport.YPosition = 1 // tell viewport to render after an empty line
  81. } else {
  82. m.viewport.SetHeight(msg.Height - m.getVerticalMargin())
  83. m.viewport.SetWidth(msg.Width)
  84. }
  85. case tea.KeyPressMsg: // handle quit
  86. k := msg.String()
  87. if k == "ctrl+c" {
  88. return m, tea.Quit
  89. }
  90. case timingMsg: // handle timingMsg (timing loop)
  91. cmd = tea.Tick(m.speed*time.Millisecond, func(t time.Time) tea.Msg { return timingMsg(t) })
  92. cmds = append(cmds, tea.Sequence(m.p.Poll(), cmd))
  93. }
  94. // update viewport bubble
  95. m.viewport.SetContent(m.p.View().Content)
  96. m.viewport, cmd = m.viewport.Update(msg)
  97. cmds = append(cmds, cmd)
  98. // update Peak bubble
  99. m.p, cmd = m.p.Update(msg)
  100. cmds = append(cmds, cmd)
  101. return m, tea.Batch(cmds...)
  102. }
  103. func (m Model) View() tea.View {
  104. m.viewport.SetContent(m.p.View().Content)
  105. content := fmt.Sprintf("%s%s\n%s", m.header(), m.viewport.View(), m.footer())
  106. var v tea.View
  107. v.SetContent(content)
  108. v.AltScreen = true
  109. return v
  110. }
  111. func (m Model) header() string { return titleStyle.Width(m.viewport.Width()).Render("pingo v0") }
  112. func (m Model) footer() string {
  113. return footerStyle.Width(m.viewport.Width()).Render("j/k: down/up\t|\tctrl+c: quit")
  114. }
  115. // get lipgloss.Height value of header and footer
  116. func (m Model) getVerticalMargin() int { return lipgloss.Height(m.header() + m.footer()) }
  117. func main() {
  118. // get timing interval in milliseconds
  119. speed := flag.Int("s", 80, "the interval in milliseconds between pings")
  120. chartHeight := flag.Int("h", 0,
  121. "the height of the latency chart. set to 0 to render charts full screen.")
  122. flag.Parse()
  123. hosts := flag.Args()
  124. if len(hosts) == 0 {
  125. fmt.Println("Must specify hosts!")
  126. return
  127. }
  128. if *speed < 1 {
  129. fmt.Println("speed must not be below 1")
  130. os.Exit(1)
  131. }
  132. p := tea.NewProgram(Model{
  133. p: pingo.InitialPeakBubble(hosts, 20, 10, *chartHeight),
  134. speed: time.Duration(*speed),
  135. })
  136. if _, err := p.Run(); err != nil {
  137. fmt.Printf("Alas, there's been an error: %v", err)
  138. os.Exit(1)
  139. }
  140. }