model.go 3.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164
  1. package app
  2. import (
  3. "fmt"
  4. "time"
  5. "fbtop/internal/api"
  6. "fbtop/internal/detail"
  7. "fbtop/internal/footer"
  8. "fbtop/internal/header"
  9. "fbtop/internal/shared"
  10. "fbtop/internal/tableview"
  11. tea "charm.land/bubbletea/v2"
  12. "charm.land/lipgloss/v2"
  13. )
  14. type tickMsg struct{}
  15. type errMsg struct{ err error }
  16. type Model struct {
  17. state shared.State
  18. url string
  19. interval time.Duration
  20. width int
  21. height int
  22. cursor int
  23. humanize bool
  24. showDetail bool
  25. sortCol shared.SortCol
  26. sortAsc bool
  27. filterKind shared.Kind
  28. err error
  29. }
  30. func New(url string, interval time.Duration, humanize bool, sortCol shared.SortCol, sortAsc bool, filterKind shared.Kind) Model {
  31. return Model{
  32. url: url,
  33. interval: interval,
  34. humanize: humanize,
  35. sortCol: sortCol,
  36. sortAsc: sortAsc,
  37. filterKind: filterKind,
  38. }
  39. }
  40. func (m Model) Init() tea.Cmd {
  41. return tea.Batch(m.fetch(), tick(m.interval))
  42. }
  43. func tick(d time.Duration) tea.Cmd {
  44. return func() tea.Msg {
  45. time.Sleep(d)
  46. return tickMsg{}
  47. }
  48. }
  49. func (m Model) fetch() tea.Cmd {
  50. prev := m.state.Comps
  51. return func() tea.Msg {
  52. s, err := api.Fetch(m.url)
  53. if err != nil {
  54. return errMsg{err}
  55. }
  56. s.PrevComps = prev
  57. return s
  58. }
  59. }
  60. func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
  61. switch msg := msg.(type) {
  62. case tea.WindowSizeMsg:
  63. m.width = msg.Width
  64. m.height = msg.Height
  65. case shared.State:
  66. m.state = msg
  67. m.err = nil
  68. return m, tick(m.interval)
  69. case errMsg:
  70. m.err = msg.err
  71. m.state.Connected = false
  72. return m, tick(m.interval)
  73. case tickMsg:
  74. return m, m.fetch()
  75. case tea.KeyPressMsg:
  76. switch msg.String() {
  77. case "q", "ctrl+c":
  78. return m, tea.Quit
  79. case "up", "k":
  80. if m.cursor > 0 {
  81. m.cursor--
  82. }
  83. case "down", "j":
  84. if m.cursor < len(m.filteredAndSorted())-1 {
  85. m.cursor++
  86. }
  87. case "tab": // cycle filter: all→input→filter→output
  88. m.filterKind = (m.filterKind + 1) % 4
  89. m.cursor = 0
  90. case "s":
  91. m.sortCol = m.sortCol.Next()
  92. case "S":
  93. m.sortAsc = !m.sortAsc
  94. case "h":
  95. m.humanize = !m.humanize
  96. case "enter":
  97. m.showDetail = !m.showDetail
  98. case "esc":
  99. m.showDetail = false
  100. }
  101. }
  102. return m, nil
  103. }
  104. func (m Model) View() tea.View {
  105. v := tea.NewView("")
  106. v.AltScreen = true
  107. if m.width == 0 {
  108. v.SetContent("Initializing...")
  109. return v
  110. }
  111. if m.err != nil {
  112. v.SetContent(fmt.Sprintf("Error: %v\n\nPress [q] to quit.", m.err))
  113. return v
  114. }
  115. comps := m.filteredAndSorted()
  116. parts := []string{
  117. header.Render(m.state, m.url, m.width),
  118. tableview.Render(tableview.Params{
  119. Comps: comps,
  120. PrevComps: m.state.PrevComps,
  121. Width: m.width,
  122. Cursor: m.cursor,
  123. Humanize: m.humanize,
  124. }),
  125. }
  126. if m.showDetail && len(comps) > 0 {
  127. idx := m.cursor
  128. if idx >= len(comps) {
  129. idx = len(comps) - 1
  130. }
  131. parts = append(parts, detail.Render(comps[idx]))
  132. }
  133. parts = append(parts, footer.Render(m.sortCol.String(), m.filterKind.String(), m.humanize, m.width))
  134. v.SetContent(lipgloss.JoinVertical(lipgloss.Top, parts...))
  135. return v
  136. }
  137. func (m Model) filteredAndSorted() []shared.ComponentMetrics {
  138. comps := make([]shared.ComponentMetrics, 0, len(m.state.Comps))
  139. for _, c := range m.state.Comps {
  140. if m.filterKind == shared.KindAll || c.Kind == m.filterKind {
  141. comps = append(comps, c)
  142. }
  143. }
  144. shared.SortBy(comps, m.sortCol, m.sortAsc)
  145. return comps
  146. }