package main // An example program demonstrating the pager component from the Bubbles // component library. import ( "fmt" "os" "strings" "github.com/charmbracelet/bubbles/viewport" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" "github.com/mattn/go-runewidth" ) // You generally won't need this unless you're processing stuff with // complicated ANSI escape sequences. Turn it on if you notice flickering. // // Also keep in mind that high performance rendering only works for programs // that use the full size of the terminal. We're enabling that below with // tea.EnterAltScreen(). const useHighPerformanceRenderer = false func main() { // Load some text for our viewport content, err := os.ReadFile("artichoke.md") if err != nil { fmt.Println("could not load file:", err) os.Exit(1) } p := tea.NewProgram( model{content: string(content)}, tea.WithAltScreen(), // use the full size of the terminal in its "alternate screen buffer" tea.WithMouseCellMotion(), // turn on mouse support so we can track the mouse wheel ) if err := p.Start(); err != nil { fmt.Println("could not run program:", err) os.Exit(1) } } type model struct { content string ready bool viewport viewport.Model } func (m model) Init() tea.Cmd { return nil } func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { var ( cmd tea.Cmd cmds []tea.Cmd ) switch msg := msg.(type) { case tea.KeyMsg: if k := msg.String(); k == "ctrl+c" || k == "q" || k == "esc" { return m, tea.Quit } case tea.WindowSizeMsg: headerHeight := lipgloss.Height(m.headerView()) footerHeight := lipgloss.Height(m.footerView()) verticalMarginHeight := headerHeight + footerHeight if !m.ready { // Since this program is using the full size of the viewport we // need to wait until we've received the window dimensions before // we can initialize the viewport. The initial dimensions come in // quickly, though asynchronously, which is why we wait for them // here. m.viewport = viewport.NewModel(msg.Width, msg.Height-verticalMarginHeight) m.viewport.YPosition = headerHeight m.viewport.HighPerformanceRendering = useHighPerformanceRenderer m.viewport.SetContent(m.content) m.ready = true // This is only necessary for high performance rendering, which in // most cases you won't need. // // Render the viewport one line below the header. m.viewport.YPosition = headerHeight + 1 } else { m.viewport.Width = msg.Width m.viewport.Height = msg.Height - verticalMarginHeight } if useHighPerformanceRenderer { // Render (or re-render) the whole viewport. Necessary both to // initialize the viewport and when the window is resized. // // This is needed for high-performance rendering only. cmds = append(cmds, viewport.Sync(m.viewport)) } } // Handle keyboard and mouse events in the viewport m.viewport, cmd = m.viewport.Update(msg) cmds = append(cmds, cmd) return m, tea.Batch(cmds...) } func (m model) View() string { if !m.ready { return "\n Initializing..." } return fmt.Sprintf("%s\n%s\n%s", m.headerView(), m.viewport.View(), m.footerView()) } func (m model) headerView() string { headerTop := "╭───────────╮" headerMid := "│ Mr. Pager ├" headerBot := "╰───────────╯" headerMid += strings.Repeat("─", max(0, m.viewport.Width-runewidth.StringWidth(headerMid))) return fmt.Sprintf("%s\n%s\n%s", headerTop, headerMid, headerBot) } func (m model) footerView() string { footerTop := "╭──────╮" footerMid := fmt.Sprintf("┤ %3.f%% │", m.viewport.ScrollPercent()*100) footerBot := "╰──────╯" gapSize := max(0, m.viewport.Width-runewidth.StringWidth(footerMid)) footerTop = strings.Repeat(" ", gapSize) + footerTop footerMid = strings.Repeat("─", gapSize) + footerMid footerBot = strings.Repeat(" ", gapSize) + footerBot return fmt.Sprintf("%s\n%s\n%s", footerTop, footerMid, footerBot) } func max(a, b int) int { if a > b { return a } return b }