pingo.go 3.6 KB

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