package main // An example program demonstrating the pager component from the Bubbles // component library. import ( "fmt" "io/ioutil" "os" "strings" "github.com/charmbracelet/bubbles/viewport" tea "github.com/charmbracelet/bubbletea" "github.com/mattn/go-runewidth" ) const ( // You usually won't need this unless you're processing some pretty stuff // with some pretty complicated ANSI escape sequences. Turn it on if you // notice flickering. // // Also note that high performance rendering only works for programs that // use the full size of the terminal. We're enabling that below with // tea.AltScreen(). useHighPerformanceRenderer = false headerHeight = 3 footerHeight = 3 ) func main() { // Load some text to render content, err := ioutil.ReadFile("artichoke.md") if err != nil { fmt.Println("could not load file:", err) os.Exit(1) } // Set PAGER_LOG to a path to log to a file. For example: // // export PAGER_LOG=debug.log // // This becomes handy when debugging stuff since you can't debug to stdout // because the UI is occupying it! path := os.Getenv("PAGER_LOG") if path != "" { f, err := tea.LogToFile(path, "pager") if err != nil { fmt.Printf("Could not open file %s: %v", path, err) os.Exit(1) } defer f.Close() } p := tea.NewProgram(model{content: string(content)}) // Use the full size of the terminal in its "alternate screen buffer" p.EnterAltScreen() defer p.ExitAltScreen() // We also turn on mouse support so we can track the mouse wheel p.EnableMouseCellMotion() defer p.DisableMouseCellMotion() 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: // Ctrl+c exits if k := msg.String(); k == "ctrl+c" || k == "q" { return m, tea.Quit } case tea.WindowSizeMsg: verticalMargins := 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.Model{Width: msg.Width, Height: msg.Height - verticalMargins} m.viewport.YPosition = headerHeight m.viewport.HighPerformanceRendering = useHighPerformanceRenderer m.viewport.SetContent(m.content) m.ready = true } else { m.viewport.Width = msg.Width m.viewport.Height = msg.Height - verticalMargins } 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)) } } // Because we're using the viewport's default update function (with pager- // style navigation) it's important that the viewport's update function: // // * Recieves messages from the Bubble Tea runtime // * Returns commands to the Bubble Tea runtime // m.viewport, cmd = m.viewport.Update(msg) if useHighPerformanceRenderer { cmds = append(cmds, cmd) } return m, tea.Batch(cmds...) } func (m model) View() string { if !m.ready { return "\n Initalizing..." } headerTop := "╭───────────╮" headerMid := "│ Mr. Pager ├" headerBot := "╰───────────╯" headerMid += strings.Repeat("─", m.viewport.Width-runewidth.StringWidth(headerMid)) header := fmt.Sprintf("%s\n%s\n%s", headerTop, headerMid, headerBot) footerTop := "╭──────╮" footerMid := fmt.Sprintf("┤ %3.f%% │", m.viewport.ScrollPercent()*100) footerBot := "╰──────╯" gapSize := m.viewport.Width - runewidth.StringWidth(footerMid) footerTop = strings.Repeat(" ", gapSize) + footerTop footerMid = strings.Repeat("─", gapSize) + footerMid footerBot = strings.Repeat(" ", gapSize) + footerBot footer := fmt.Sprintf("%s\n%s\n%s", footerTop, footerMid, footerBot) return fmt.Sprintf("%s\n%s\n%s", header, m.viewport.View(), footer) }