10 Commits

Author SHA1 Message Date
Christian Rocha
5357dd61bd Fix bug where scroll wheel could create duplicate lines on top and bottom 2020-07-20 17:46:15 -04:00
Christian Rocha
5a26cb0d8e Use pgup/pgdown as default paginator keystrokes 2020-07-20 12:13:53 -04:00
Christian Rocha
88469a499e Tidy up the key case switch in viewport 2020-07-20 11:56:35 -04:00
Christian Rocha
b130d96434 Add methods for jumping to the tom and bottom of the viewport 2020-07-17 19:22:08 -04:00
Christian Rocha
185a19e56f Fix cases where pager would add blank lines to bottom of document 2020-07-17 18:38:38 -04:00
Christian Rocha
dbb1d93970 Remove redundant viewport commands 2020-07-17 18:22:51 -04:00
Christian Rocha
2f909886c1 Be more consistent with max() argument order 2020-07-17 18:13:48 -04:00
Christian Rocha
5720cfb35a Correct scroll percentage reporting 2020-07-17 18:13:05 -04:00
Christian Rocha
3c07b97d37 Correct typo, add potential TODO 2020-07-17 17:02:37 -04:00
Christian Rocha
9b3e5476c3 Update README a little. 2020-07-17 14:03:02 -04:00
3 changed files with 141 additions and 103 deletions

View File

@@ -1,8 +1,33 @@
# Tea Party # Bubbles
Components for [Tea](https://github.com/charmbraclet/tea) Some components for [Bubble Tea](https://github.com/charmbraclet/bubbletea):
⚠️ This project is a pre-release! Check back later. * Spinner
* Text Input
* Paginator
* Viewport
## Spinner
A spinner, useful for indicating that some kind of invisible operation is
happening. There are a couple default ones, but you can also pass your own
”frames.”
## Text Input
A text input field, akin to an `<input type="text">` in HTML.
## Paginator
A component for handling pagination logic and optionally drawing pagination UI.
## Viewport
A viewport for vertically scrolling content which optionally includes standard
pager keybindings and mouse wheel support. A high performance mode is available
for applications which make use of the alterate screen buffer. This is
generally only necessary when dealing with content with a very large amount of
ANSI escape sequences.
## License ## License

View File

@@ -28,6 +28,7 @@ type Model struct {
ActiveDot string ActiveDot string
InactiveDot string InactiveDot string
ArabicFormat string ArabicFormat string
UsePgUpPgDownKeys bool
UseLeftRightKeys bool UseLeftRightKeys bool
UseUpDownKeys bool UseUpDownKeys bool
UseHLKeys bool UseHLKeys bool
@@ -105,6 +106,7 @@ func NewModel() Model {
ActiveDot: "•", ActiveDot: "•",
InactiveDot: "○", InactiveDot: "○",
ArabicFormat: "%d/%d", ArabicFormat: "%d/%d",
UsePgUpPgDownKeys: true,
UseLeftRightKeys: true, UseLeftRightKeys: true,
UseUpDownKeys: false, UseUpDownKeys: false,
UseHLKeys: true, UseHLKeys: true,
@@ -116,6 +118,14 @@ func NewModel() Model {
func Update(msg tea.Msg, m Model) (Model, tea.Cmd) { func Update(msg tea.Msg, m Model) (Model, tea.Cmd) {
switch msg := msg.(type) { switch msg := msg.(type) {
case tea.KeyMsg: case tea.KeyMsg:
if m.UsePgUpPgDownKeys {
switch msg.String() {
case "pgup":
m.PrevPage()
case "pgdown":
m.NextPage()
}
}
if m.UseLeftRightKeys { if m.UseLeftRightKeys {
switch msg.String() { switch msg.String() {
case "left": case "left":

View File

@@ -7,6 +7,10 @@ import (
tea "github.com/charmbracelet/bubbletea" tea "github.com/charmbracelet/bubbletea"
) )
const (
spacebar = " "
)
// MODEL // MODEL
type Model struct { type Model struct {
@@ -53,9 +57,16 @@ func (m Model) AtTop() bool {
return m.YOffset <= 0 return m.YOffset <= 0
} }
// AtBottom returns whether or not the viewport is at the very botom position. // AtBottom returns whether or not the viewport is at or past the very bottom
// position.
func (m Model) AtBottom() bool { func (m Model) AtBottom() bool {
return m.YOffset >= len(m.lines)-m.Height-1 return m.YOffset >= len(m.lines)-1-m.Height
}
// PastBottom returns whether or not the viewport is scrolled beyond the last
// line. This can happen when adjusting the viewport height.
func (m Model) PastBottom() bool {
return m.YOffset > len(m.lines)-1-m.Height
} }
// Scrollpercent returns the amount scrolled as a float between 0 and 1. // Scrollpercent returns the amount scrolled as a float between 0 and 1.
@@ -65,7 +76,7 @@ func (m Model) ScrollPercent() float64 {
} }
y := float64(m.YOffset) y := float64(m.YOffset)
h := float64(m.Height) h := float64(m.Height)
t := float64(len(m.lines)) t := float64(len(m.lines) - 1)
v := y / (t - h) v := y / (t - h)
return math.Max(0.0, math.Min(1.0, v)) return math.Max(0.0, math.Min(1.0, v))
} }
@@ -96,7 +107,7 @@ func (m *Model) ViewDown() []string {
m.YOffset = min( m.YOffset = min(
m.YOffset+m.Height, // target m.YOffset+m.Height, // target
len(m.lines)-m.Height, // fallback len(m.lines)-1-m.Height, // fallback
) )
return m.visibleLines() return m.visibleLines()
@@ -124,7 +135,7 @@ func (m *Model) HalfViewDown() (lines []string) {
m.YOffset = min( m.YOffset = min(
m.YOffset+m.Height/2, // target m.YOffset+m.Height/2, // target
len(m.lines)-m.Height, // fallback len(m.lines)-1-m.Height, // fallback
) )
if len(m.lines) > 0 { if len(m.lines) > 0 {
@@ -156,20 +167,26 @@ func (m *Model) HalfViewUp() (lines []string) {
return lines return lines
} }
// LineDown moves the view up by the given number of lines. // LineDown moves the view down by the given number of lines.
func (m *Model) LineDown(n int) (lines []string) { func (m *Model) LineDown(n int) (lines []string) {
if m.AtBottom() || n == 0 { if m.AtBottom() || n == 0 {
return nil return nil
} }
// Make sure the number of lines by which we're going to scroll isn't
// greater than the number of lines we actually have left before we reach
// the bottom.
maxDelta := (len(m.lines) - 1) - (m.YOffset + m.Height) // number of lines - viewport bottom edge
n = min(n, maxDelta)
m.YOffset = min( m.YOffset = min(
m.YOffset+n, // target m.YOffset+n, // target
len(m.lines)-m.Height, // fallback len(m.lines)-1-m.Height, // fallback
) )
if len(m.lines) > 0 { if len(m.lines) > 0 {
top := max(0, m.YOffset+m.Height-n) top := max(m.YOffset+m.Height-n, 0)
bottom := min(len(m.lines)-1, m.YOffset+m.Height) bottom := min(m.YOffset+m.Height, len(m.lines)-1)
lines = m.lines[top:bottom] lines = m.lines[top:bottom]
} }
@@ -183,11 +200,45 @@ func (m *Model) LineUp(n int) (lines []string) {
return nil return nil
} }
// Make sure the number of lines by which we're going to scroll isn't
// greater than the number of lines we are from the top.
n = min(n, m.YOffset)
m.YOffset = max(m.YOffset-n, 0) m.YOffset = max(m.YOffset-n, 0)
if len(m.lines) > 0 { if len(m.lines) > 0 {
top := max(0, m.YOffset) top := max(0, m.YOffset)
bottom := min(len(m.lines)-1, m.YOffset+n) bottom := min(m.YOffset+n, len(m.lines)-1)
lines = m.lines[top:bottom]
}
return lines
}
// GotoTop sets the viewport to the top position.
func (m *Model) GotoTop() (lines []string) {
if m.AtTop() {
return nil
}
m.YOffset = 0
if len(m.lines) > 0 {
top := m.YOffset
bottom := min(m.YOffset+m.Height, len(m.lines)-1)
lines = m.lines[top:bottom]
}
return lines
}
// GotoTop sets the viewport to the bottom position.
func (m *Model) GotoBottom() (lines []string) {
m.YOffset = max(len(m.lines)-1-m.Height, 0)
if len(m.lines) > 0 {
top := m.YOffset
bottom := max(len(m.lines)-1, 0)
lines = m.lines[top:bottom] lines = m.lines[top:bottom]
} }
@@ -206,6 +257,8 @@ func Sync(m Model) tea.Cmd {
return nil return nil
} }
// TODO: we should probably use m.visibleLines() rather than these two
// expressions.
top := max(m.YOffset, 0) top := max(m.YOffset, 0)
bottom := min(m.YOffset+m.Height, len(m.lines)-1) bottom := min(m.YOffset+m.Height, len(m.lines)-1)
@@ -216,9 +269,9 @@ func Sync(m Model) tea.Cmd {
) )
} }
// ViewDown is a high performance command that moves the viewport up by one // ViewDown is a high performance command that moves the viewport up by a given
// viewport height. Use Model.ViewDown to get the lines that should be // numer of lines. Use Model.ViewDown to get the lines that should be rendered.
// rendered. For example: // For example:
// //
// lines := model.ViewDown(1) // lines := model.ViewDown(1)
// cmd := ViewDown(m, lines) // cmd := ViewDown(m, lines)
@@ -230,8 +283,8 @@ func ViewDown(m Model, lines []string) tea.Cmd {
return tea.ScrollDown(lines, m.YPosition, m.YPosition+m.Height) return tea.ScrollDown(lines, m.YPosition, m.YPosition+m.Height)
} }
// ViewUp is a high performance command the moves the viewport down by one // ViewUp is a high performance command the moves the viewport down by a given
// viewport height. Use Model.ViewDown to get the lines that should be // number of lines height. Use Model.ViewDown to get the lines that should be
// rendered. // rendered.
func ViewUp(m Model, lines []string) tea.Cmd { func ViewUp(m Model, lines []string) tea.Cmd {
if len(lines) == 0 { if len(lines) == 0 {
@@ -240,46 +293,6 @@ func ViewUp(m Model, lines []string) tea.Cmd {
return tea.ScrollUp(lines, m.YPosition, m.YPosition+m.Height) return tea.ScrollUp(lines, m.YPosition, m.YPosition+m.Height)
} }
// HalfViewDown is a high performance command the moves the viewport down by
// half of the height of the viewport. Use Model.HalfViewDown to get the lines
// that should be rendered.
func HalfViewDown(m Model, lines []string) tea.Cmd {
if len(lines) == 0 {
return nil
}
return tea.ScrollDown(lines, m.YPosition, m.YPosition+m.Height)
}
// HalfViewUp is a high performance command the moves the viewport up by
// half of the height of the viewport. Use Model.HalfViewUp to get the lines
// that should be rendered.
func HalfViewUp(m Model, lines []string) tea.Cmd {
if len(lines) == 0 {
return nil
}
return tea.ScrollUp(lines, m.YPosition, m.YPosition+m.Height)
}
// LineDown is a high performance command the moves the viewport down by
// a given number of lines. Use Model.LineDown to get the lines that should be
// rendered.
func LineDown(m Model, lines []string) tea.Cmd {
if len(lines) == 0 {
return nil
}
return tea.ScrollDown(lines, m.YPosition, m.YPosition+m.Height)
}
// LineDown is a high performance command the moves the viewport up by a given
// number of lines. Use Model.LineDown to get the lines that should be
// rendered.
func LineUp(m Model, lines []string) tea.Cmd {
if len(lines) == 0 {
return nil
}
return tea.ScrollUp(lines, m.YPosition, m.YPosition+m.Height)
}
// UPDATE // UPDATE
// Update runs the update loop with default keybindings similar to popular // Update runs the update loop with default keybindings similar to popular
@@ -293,20 +306,14 @@ func Update(msg tea.Msg, m Model) (Model, tea.Cmd) {
case tea.KeyMsg: case tea.KeyMsg:
switch msg.String() { switch msg.String() {
// Down one page // Down one page
case "pgdown": case "pgdown", spacebar, "f":
fallthrough
case " ": // spacebar
fallthrough
case "f":
lines := m.ViewDown() lines := m.ViewDown()
if m.HighPerformanceRendering { if m.HighPerformanceRendering {
cmd = ViewDown(m, lines) cmd = ViewDown(m, lines)
} }
// Up one page // Up one page
case "pgup": case "pgup", "b":
fallthrough
case "b":
lines := m.ViewUp() lines := m.ViewUp()
if m.HighPerformanceRendering { if m.HighPerformanceRendering {
cmd = ViewUp(m, lines) cmd = ViewUp(m, lines)
@@ -316,32 +323,28 @@ func Update(msg tea.Msg, m Model) (Model, tea.Cmd) {
case "d": case "d":
lines := m.HalfViewDown() lines := m.HalfViewDown()
if m.HighPerformanceRendering { if m.HighPerformanceRendering {
cmd = HalfViewDown(m, lines) cmd = ViewDown(m, lines)
} }
// Up half page // Up half page
case "u": case "u":
lines := m.HalfViewUp() lines := m.HalfViewUp()
if m.HighPerformanceRendering { if m.HighPerformanceRendering {
cmd = HalfViewUp(m, lines) cmd = ViewUp(m, lines)
} }
// Down one line // Down one line
case "down": case "down", "j":
fallthrough
case "j":
lines := m.LineDown(1) lines := m.LineDown(1)
if m.HighPerformanceRendering { if m.HighPerformanceRendering {
cmd = LineDown(m, lines) cmd = ViewDown(m, lines)
} }
// Up one line // Up one line
case "up": case "up", "k":
fallthrough
case "k":
lines := m.LineUp(1) lines := m.LineUp(1)
if m.HighPerformanceRendering { if m.HighPerformanceRendering {
cmd = LineUp(m, lines) cmd = ViewUp(m, lines)
} }
} }
@@ -351,13 +354,13 @@ func Update(msg tea.Msg, m Model) (Model, tea.Cmd) {
case tea.MouseWheelUp: case tea.MouseWheelUp:
lines := m.LineUp(3) lines := m.LineUp(3)
if m.HighPerformanceRendering { if m.HighPerformanceRendering {
cmd = LineUp(m, lines) cmd = ViewUp(m, lines)
} }
case tea.MouseWheelDown: case tea.MouseWheelDown:
lines := m.LineDown(3) lines := m.LineDown(3)
if m.HighPerformanceRendering { if m.HighPerformanceRendering {
cmd = LineDown(m, lines) cmd = ViewDown(m, lines)
} }
} }
} }