| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211 |
- package app
- import (
- "fmt"
- "time"
- "fbtop/internal/api"
- "fbtop/internal/detail"
- "fbtop/internal/diagview"
- "fbtop/internal/footer"
- "fbtop/internal/header"
- "fbtop/internal/shared"
- "fbtop/internal/tableview"
- tea "charm.land/bubbletea/v2"
- "charm.land/lipgloss/v2"
- )
- type tickMsg struct{}
- type errMsg struct{ err error }
- type Model struct {
- state shared.State
- url string
- interval time.Duration
- width int
- height int
- cursor int
- humanize bool
- showDetail bool
- showDiag bool
- sortCol shared.SortCol
- sortAsc bool
- filterKind shared.Kind
- err error
- }
- func New(url string, interval time.Duration, humanize bool, sortCol shared.SortCol, sortAsc bool, filterKind shared.Kind) Model {
- return Model{
- url: url,
- interval: interval,
- humanize: humanize,
- showDetail: true,
- showDiag: true,
- sortCol: sortCol,
- sortAsc: sortAsc,
- filterKind: filterKind,
- }
- }
- func (m Model) Init() tea.Cmd {
- return tea.Batch(m.fetch(), tick(m.interval))
- }
- func tick(d time.Duration) tea.Cmd {
- return func() tea.Msg {
- time.Sleep(d)
- return tickMsg{}
- }
- }
- func (m Model) fetch() tea.Cmd {
- prev := m.state.Comps
- return func() tea.Msg {
- s, err := api.Fetch(m.url)
- if err != nil {
- return errMsg{err}
- }
- s.PrevComps = prev
- return s
- }
- }
- func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
- switch msg := msg.(type) {
- case tea.WindowSizeMsg:
- m.width = msg.Width
- m.height = msg.Height
- case shared.State:
- m.state = msg
- m.err = nil
- return m, tick(m.interval)
- case errMsg:
- m.err = msg.err
- m.state.Connected = false
- return m, tick(m.interval)
- case tickMsg:
- return m, m.fetch()
- case tea.KeyPressMsg:
- switch msg.String() {
- case "q", "ctrl+c":
- return m, tea.Quit
- case "up", "k":
- if m.cursor > 0 {
- m.cursor--
- }
- case "down", "j":
- if m.cursor < len(m.filteredAndSorted())-1 {
- m.cursor++
- }
- case "tab": // cycle filter: all→input→filter→output
- m.filterKind = (m.filterKind + 1) % 4
- m.cursor = 0
- case "s":
- m.sortCol = m.sortCol.Next()
- case "S":
- m.sortAsc = !m.sortAsc
- case "h":
- m.humanize = !m.humanize
- case "enter":
- m.showDetail = !m.showDetail
- case "esc":
- m.showDetail = false
- case "d":
- m.showDiag = !m.showDiag
- }
- }
- return m, nil
- }
- func (m Model) View() tea.View {
- v := tea.NewView("")
- v.AltScreen = true
- if m.width == 0 {
- v.SetContent("Initializing...")
- return v
- }
- if m.err != nil {
- v.SetContent(fmt.Sprintf("Error: %v\n\nPress [q] to quit.", m.err))
- return v
- }
- showDetail := m.showDetail && len(m.filteredAndSorted()) > 0
- tableW := m.width
- rightW := 0
- if showDetail && m.width > 100 {
- rightW = m.width * 2 / 5
- tableW = m.width - rightW
- } else if showDetail {
- rightW = m.width / 2
- tableW = m.width - rightW
- }
- comps := m.filteredAndSorted()
- // Header
- headerLine := header.Render(m.state, m.url, m.width)
- // Left pane: table + diagnostics stacked vertically
- leftParts := []string{
- tableview.Render(tableview.Params{
- Comps: comps,
- PrevComps: m.state.PrevComps,
- Width: tableW,
- Cursor: m.cursor,
- Humanize: m.humanize,
- }),
- }
- if m.showDiag {
- diags := shared.AnalyzeDiagnostics(m.state, m.state.PrevComps)
- leftParts = append(leftParts, diagview.Render(diags, tableW))
- }
- leftContent := lipgloss.JoinVertical(lipgloss.Top, leftParts...)
- // Right pane: detail only
- rightContent := ""
- if showDetail {
- idx := m.cursor
- if idx >= len(comps) {
- idx = len(comps) - 1
- }
- prev := prevComp(comps[idx].ID, m.state.PrevComps)
- rightContent = detail.Render(comps[idx], prev, rightW)
- }
- // Main area
- var main string
- if rightContent != "" {
- main = lipgloss.JoinHorizontal(lipgloss.Top, leftContent, rightContent)
- } else {
- main = leftContent
- }
- // Footer
- foot := footer.Render(m.sortCol.String(), m.filterKind.String(), m.humanize, m.showDiag, m.width)
- v.SetContent(lipgloss.JoinVertical(lipgloss.Top, headerLine, main, foot))
- return v
- }
- func (m Model) filteredAndSorted() []shared.ComponentMetrics {
- comps := make([]shared.ComponentMetrics, 0, len(m.state.Comps))
- for _, c := range m.state.Comps {
- if m.filterKind == shared.KindAll || c.Kind == m.filterKind {
- comps = append(comps, c)
- }
- }
- shared.SortBy(comps, m.sortCol, m.sortAsc)
- return comps
- }
- func prevComp(id string, prevComps []shared.ComponentMetrics) shared.ComponentMetrics {
- for _, c := range prevComps {
- if c.ID == id {
- return c
- }
- }
- return shared.ComponentMetrics{}
- }
|