package cmd // pager setup using bubbletea // file shamlelessly copied from: // https://github.com/charmbracelet/bubbletea/tree/main/examples/pager import ( "fmt" "os" "strings" "github.com/charmbracelet/bubbles/viewport" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" "github.com/muesli/reflow/wordwrap" "golang.org/x/term" ) var ( titleStyle = func() lipgloss.Style { b := lipgloss.RoundedBorder() b.Right = "├" return lipgloss.NewStyle().BorderStyle(b).Padding(0, 1) }() infoStyle = func() lipgloss.Style { b := lipgloss.RoundedBorder() b.Left = "┤" return titleStyle.BorderStyle(b) }() viewstyle = lipgloss.NewStyle() ) type Meta struct { lines int currentline int initialprogress int document string } type Doc struct { content string title string ready bool viewport viewport.Model initialwidth int meta *Meta } func (m Doc) Init() tea.Cmd { return nil } func (m Doc) 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.New(msg.Width, msg.Height-verticalMarginHeight) m.viewport.YPosition = headerHeight m.viewport.SetContent(wordwrap.String(m.content, m.initialwidth)) m.viewport.ScrollDown(m.meta.initialprogress) m.ready = true } else { m.viewport.Width = msg.Width m.viewport.Height = msg.Height - verticalMarginHeight m.viewport.SetContent(wordwrap.String(m.content, msg.Width)) } } // 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 Doc) View() string { if !m.ready { return "\n Initializing..." } // update current line for later saving // FIXME: doesn't work correctly yet m.meta.currentline = int(float64(m.meta.lines) * m.viewport.ScrollPercent()) return fmt.Sprintf("%s\n%s\n%s", m.headerView(), m.viewport.View(), m.footerView()) } func (m Doc) headerView() string { title := titleStyle.Render(m.title) line := strings.Repeat("─", max(0, m.viewport.Width-lipgloss.Width(title))) return lipgloss.JoinHorizontal(lipgloss.Center, title, line) } func (m Doc) footerView() string { info := infoStyle.Render(fmt.Sprintf("%3.f%%", m.viewport.ScrollPercent()*100)) line := strings.Repeat("─", max(0, m.viewport.Width-lipgloss.Width(info))) return lipgloss.JoinHorizontal(lipgloss.Center, line, info) } func max(a, b int) int { if a > b { return a } return b } func Pager(conf *Config, title, message string) (int, error) { width := 80 scrollto := 0 if term.IsTerminal(int(os.Stdout.Fd())) { w, _, err := term.GetSize(0) if err == nil { width = w } } if conf.StoreProgress { scrollto = conf.InitialProgress } meta := Meta{ initialprogress: scrollto, lines: len(strings.Split(message, "\r\n")), } p := tea.NewProgram( Doc{content: message, title: title, initialwidth: width, meta: &meta}, 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.Run(); err != nil { return 0, fmt.Errorf("could not run pager:", err) } if conf.Debug { fmt.Printf("scrollto: %d, last: %d, diff: %d\n", scrollto, meta.currentline, scrollto-meta.currentline) } return meta.currentline, nil }