2022-09-30 19:09:55 +03:00
|
|
|
// Package pager provides a pager (similar to less) for the terminal.
|
|
|
|
//
|
|
|
|
// $ cat file.txt | gum page
|
|
|
|
package pager
|
|
|
|
|
|
|
|
import (
|
2022-09-30 21:30:52 +03:00
|
|
|
"fmt"
|
|
|
|
"strings"
|
|
|
|
|
2022-09-30 19:09:55 +03:00
|
|
|
"github.com/charmbracelet/bubbles/viewport"
|
|
|
|
tea "github.com/charmbracelet/bubbletea"
|
2023-05-15 06:19:07 +03:00
|
|
|
"github.com/charmbracelet/gum/internal/utils"
|
2022-09-30 19:09:55 +03:00
|
|
|
"github.com/charmbracelet/lipgloss"
|
|
|
|
)
|
|
|
|
|
|
|
|
type model struct {
|
2023-05-15 06:19:07 +03:00
|
|
|
content string
|
|
|
|
origContent string
|
|
|
|
viewport viewport.Model
|
|
|
|
helpStyle lipgloss.Style
|
|
|
|
showLineNumbers bool
|
|
|
|
lineNumberStyle lipgloss.Style
|
|
|
|
softWrap bool
|
|
|
|
search search
|
|
|
|
matchStyle lipgloss.Style
|
|
|
|
matchHighlightStyle lipgloss.Style
|
|
|
|
maxWidth int
|
2022-09-30 19:09:55 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
func (m model) Init() tea.Cmd {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|
|
|
switch msg := msg.(type) {
|
|
|
|
case tea.WindowSizeMsg:
|
2023-05-15 06:19:07 +03:00
|
|
|
m.ProcessText(msg)
|
|
|
|
case tea.KeyMsg:
|
|
|
|
return m.KeyHandler(msg)
|
|
|
|
}
|
2022-12-06 19:40:53 +03:00
|
|
|
|
2023-05-15 06:19:07 +03:00
|
|
|
return m, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (m *model) ProcessText(msg tea.WindowSizeMsg) {
|
|
|
|
m.viewport.Height = msg.Height - lipgloss.Height(m.helpStyle.Render("?")) - 1
|
|
|
|
m.viewport.Width = msg.Width
|
|
|
|
textStyle := lipgloss.NewStyle().Width(m.viewport.Width)
|
|
|
|
var text strings.Builder
|
|
|
|
|
|
|
|
// Determine max width of a line.
|
|
|
|
m.maxWidth = m.viewport.Width
|
|
|
|
if m.softWrap {
|
|
|
|
vpStyle := m.viewport.Style
|
|
|
|
m.maxWidth -= vpStyle.GetHorizontalBorderSize() + vpStyle.GetHorizontalMargins() + vpStyle.GetHorizontalPadding()
|
|
|
|
if m.showLineNumbers {
|
|
|
|
m.maxWidth -= lipgloss.Width(" │ ")
|
2022-12-06 19:40:53 +03:00
|
|
|
}
|
2023-05-15 06:19:07 +03:00
|
|
|
}
|
2022-12-06 19:40:53 +03:00
|
|
|
|
2023-05-15 06:19:07 +03:00
|
|
|
for i, line := range strings.Split(m.content, "\n") {
|
|
|
|
line = strings.ReplaceAll(line, "\t", " ")
|
|
|
|
if m.showLineNumbers {
|
|
|
|
text.WriteString(m.lineNumberStyle.Render(fmt.Sprintf("%4d │ ", i+1)))
|
|
|
|
}
|
|
|
|
for m.softWrap && lipgloss.Width(line) > m.maxWidth {
|
|
|
|
truncatedLine := utils.LipglossTruncate(line, m.maxWidth)
|
|
|
|
text.WriteString(textStyle.Render(truncatedLine))
|
|
|
|
text.WriteString("\n")
|
2022-09-30 21:30:52 +03:00
|
|
|
if m.showLineNumbers {
|
2023-05-15 06:19:07 +03:00
|
|
|
text.WriteString(m.lineNumberStyle.Render(" │ "))
|
2022-09-30 21:30:52 +03:00
|
|
|
}
|
2023-05-15 06:19:07 +03:00
|
|
|
line = strings.Replace(line, truncatedLine, "", 1)
|
2022-09-30 21:30:52 +03:00
|
|
|
}
|
2023-05-15 06:19:07 +03:00
|
|
|
text.WriteString(textStyle.Render(utils.LipglossTruncate(line, m.maxWidth)))
|
|
|
|
text.WriteString("\n")
|
|
|
|
}
|
2022-09-30 21:30:52 +03:00
|
|
|
|
2023-05-15 06:19:07 +03:00
|
|
|
diffHeight := m.viewport.Height - lipgloss.Height(text.String())
|
|
|
|
if diffHeight > 0 && m.showLineNumbers {
|
|
|
|
remainingLines := " ~ │ " + strings.Repeat("\n ~ │ ", diffHeight-1)
|
|
|
|
text.WriteString(m.lineNumberStyle.Render(remainingLines))
|
|
|
|
}
|
|
|
|
m.viewport.SetContent(text.String())
|
|
|
|
}
|
|
|
|
|
|
|
|
func (m model) KeyHandler(key tea.KeyMsg) (model, func() tea.Msg) {
|
|
|
|
var cmd tea.Cmd
|
|
|
|
if m.search.active {
|
|
|
|
switch key.String() {
|
|
|
|
case "enter":
|
|
|
|
if m.search.input.Value() != "" {
|
|
|
|
m.content = m.origContent
|
|
|
|
m.search.Execute(&m)
|
|
|
|
|
|
|
|
// Trigger a view update to highlight the found matches.
|
|
|
|
m.search.NextMatch(&m)
|
|
|
|
m.ProcessText(tea.WindowSizeMsg{Height: m.viewport.Height + 2, Width: m.viewport.Width})
|
|
|
|
} else {
|
|
|
|
m.search.Done()
|
|
|
|
}
|
|
|
|
case "ctrl+d", "ctrl+c", "esc":
|
|
|
|
m.search.Done()
|
|
|
|
default:
|
|
|
|
m.search.input, cmd = m.search.input.Update(key)
|
2022-09-30 21:30:52 +03:00
|
|
|
}
|
2023-05-15 06:19:07 +03:00
|
|
|
} else {
|
|
|
|
switch key.String() {
|
2022-09-30 21:30:52 +03:00
|
|
|
case "g":
|
|
|
|
m.viewport.GotoTop()
|
|
|
|
case "G":
|
|
|
|
m.viewport.GotoBottom()
|
2023-05-15 06:19:07 +03:00
|
|
|
case "/":
|
|
|
|
m.search.Begin()
|
|
|
|
case "p", "N":
|
|
|
|
m.search.PrevMatch(&m)
|
|
|
|
m.ProcessText(tea.WindowSizeMsg{Height: m.viewport.Height + 2, Width: m.viewport.Width})
|
|
|
|
case "n":
|
|
|
|
m.search.NextMatch(&m)
|
|
|
|
m.ProcessText(tea.WindowSizeMsg{Height: m.viewport.Height + 2, Width: m.viewport.Width})
|
2022-09-30 19:09:55 +03:00
|
|
|
case "q", "ctrl+c", "esc":
|
|
|
|
return m, tea.Quit
|
|
|
|
}
|
2023-05-15 06:19:07 +03:00
|
|
|
m.viewport, cmd = m.viewport.Update(key)
|
2022-09-30 19:09:55 +03:00
|
|
|
}
|
2023-05-15 06:19:07 +03:00
|
|
|
|
2022-09-30 19:09:55 +03:00
|
|
|
return m, cmd
|
|
|
|
}
|
|
|
|
|
|
|
|
func (m model) View() string {
|
2023-05-15 06:19:07 +03:00
|
|
|
helpMsg := "\n ↑/↓: Navigate • q: Quit • /: Search "
|
|
|
|
if m.search.query != nil {
|
|
|
|
helpMsg += "• n: Next Match "
|
|
|
|
helpMsg += "• N: Prev Match "
|
|
|
|
}
|
|
|
|
if m.search.active {
|
|
|
|
return m.viewport.View() + "\n " + m.search.input.View()
|
|
|
|
}
|
|
|
|
|
|
|
|
return m.viewport.View() + m.helpStyle.Render(helpMsg)
|
2022-09-30 19:09:55 +03:00
|
|
|
}
|