1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
|
// Package pager provides a pager (similar to less) for the terminal.
//
// $ cat file.txt | gum pager
package pager
import (
"fmt"
"strings"
"time"
"github.com/charmbracelet/gum/timeout"
"github.com/charmbracelet/bubbles/viewport"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
"github.com/muesli/reflow/truncate"
)
type model struct {
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
timeout time.Duration
hasTimeout bool
}
func (m model) Init() tea.Cmd {
return timeout.Init(m.timeout, nil)
}
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case timeout.TickTimeoutMsg:
if msg.TimeoutValue <= 0 {
return m, tea.Quit
}
m.timeout = msg.TimeoutValue
return m, timeout.Tick(msg.TimeoutValue, msg.Data)
case tea.WindowSizeMsg:
m.ProcessText(msg)
case tea.KeyMsg:
return m.KeyHandler(msg)
}
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(" │ ")
}
}
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 := truncate.String(line, uint(m.maxWidth))
text.WriteString(textStyle.Render(truncatedLine))
text.WriteString("\n")
if m.showLineNumbers {
text.WriteString(m.lineNumberStyle.Render(" │ "))
}
line = strings.Replace(line, truncatedLine, "", 1)
}
text.WriteString(textStyle.Render(truncate.String(line, uint(m.maxWidth))))
text.WriteString("\n")
}
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())
}
const heightOffset = 2
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 + heightOffset, 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)
}
} else {
switch key.String() {
case "g":
m.viewport.GotoTop()
case "G":
m.viewport.GotoBottom()
case "/":
m.search.Begin()
case "p", "N":
m.search.PrevMatch(&m)
m.ProcessText(tea.WindowSizeMsg{Height: m.viewport.Height + heightOffset, Width: m.viewport.Width})
case "n":
m.search.NextMatch(&m)
m.ProcessText(tea.WindowSizeMsg{Height: m.viewport.Height + heightOffset, Width: m.viewport.Width})
case "q", "ctrl+c", "esc":
return m, tea.Quit
}
m.viewport, cmd = m.viewport.Update(key)
}
return m, cmd
}
func (m model) View() string {
var timeoutStr string
if m.hasTimeout {
timeoutStr = timeout.Str(m.timeout) + " "
}
helpMsg := "\n" + timeoutStr + " ↑/↓: 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" + timeoutStr + " " + m.search.input.View()
}
return m.viewport.View() + m.helpStyle.Render(helpMsg)
}
|