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

View File

@@ -21,17 +21,18 @@ const (
// Model is the Tea model for this user interface.
type Model struct {
Type Type
Page int
PerPage int
TotalPages int
ActiveDot string
InactiveDot string
ArabicFormat string
UseLeftRightKeys bool
UseUpDownKeys bool
UseHLKeys bool
UseJKKeys bool
Type Type
Page int
PerPage int
TotalPages int
ActiveDot string
InactiveDot string
ArabicFormat string
UsePgUpPgDownKeys bool
UseLeftRightKeys bool
UseUpDownKeys bool
UseHLKeys bool
UseJKKeys bool
}
// SetTotalPages is a helper function for calculatng the total number of pages
@@ -98,17 +99,18 @@ func (m Model) OnLastPage() bool {
// NewModel creates a new model with defaults.
func NewModel() Model {
return Model{
Type: Arabic,
Page: 0,
PerPage: 1,
TotalPages: 1,
ActiveDot: "•",
InactiveDot: "○",
ArabicFormat: "%d/%d",
UseLeftRightKeys: true,
UseUpDownKeys: false,
UseHLKeys: true,
UseJKKeys: false,
Type: Arabic,
Page: 0,
PerPage: 1,
TotalPages: 1,
ActiveDot: "•",
InactiveDot: "○",
ArabicFormat: "%d/%d",
UsePgUpPgDownKeys: true,
UseLeftRightKeys: true,
UseUpDownKeys: false,
UseHLKeys: true,
UseJKKeys: false,
}
}
@@ -116,6 +118,14 @@ func NewModel() Model {
func Update(msg tea.Msg, m Model) (Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.KeyMsg:
if m.UsePgUpPgDownKeys {
switch msg.String() {
case "pgup":
m.PrevPage()
case "pgdown":
m.NextPage()
}
}
if m.UseLeftRightKeys {
switch msg.String() {
case "left":

View File

@@ -7,6 +7,10 @@ import (
tea "github.com/charmbracelet/bubbletea"
)
const (
spacebar = " "
)
// MODEL
type Model struct {
@@ -53,9 +57,16 @@ func (m Model) AtTop() bool {
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 {
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.
@@ -65,7 +76,7 @@ func (m Model) ScrollPercent() float64 {
}
y := float64(m.YOffset)
h := float64(m.Height)
t := float64(len(m.lines))
t := float64(len(m.lines) - 1)
v := y / (t - h)
return math.Max(0.0, math.Min(1.0, v))
}
@@ -95,8 +106,8 @@ func (m *Model) ViewDown() []string {
}
m.YOffset = min(
m.YOffset+m.Height, // target
len(m.lines)-m.Height, // fallback
m.YOffset+m.Height, // target
len(m.lines)-1-m.Height, // fallback
)
return m.visibleLines()
@@ -123,8 +134,8 @@ func (m *Model) HalfViewDown() (lines []string) {
}
m.YOffset = min(
m.YOffset+m.Height/2, // target
len(m.lines)-m.Height, // fallback
m.YOffset+m.Height/2, // target
len(m.lines)-1-m.Height, // fallback
)
if len(m.lines) > 0 {
@@ -156,20 +167,26 @@ func (m *Model) HalfViewUp() (lines []string) {
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) {
if m.AtBottom() || n == 0 {
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+n, // target
len(m.lines)-m.Height, // fallback
m.YOffset+n, // target
len(m.lines)-1-m.Height, // fallback
)
if len(m.lines) > 0 {
top := max(0, m.YOffset+m.Height-n)
bottom := min(len(m.lines)-1, m.YOffset+m.Height)
top := max(m.YOffset+m.Height-n, 0)
bottom := min(m.YOffset+m.Height, len(m.lines)-1)
lines = m.lines[top:bottom]
}
@@ -183,11 +200,45 @@ func (m *Model) LineUp(n int) (lines []string) {
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)
if len(m.lines) > 0 {
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]
}
@@ -206,6 +257,8 @@ func Sync(m Model) tea.Cmd {
return nil
}
// TODO: we should probably use m.visibleLines() rather than these two
// expressions.
top := max(m.YOffset, 0)
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
// viewport height. Use Model.ViewDown to get the lines that should be
// rendered. For example:
// ViewDown is a high performance command that moves the viewport up by a given
// numer of lines. Use Model.ViewDown to get the lines that should be rendered.
// For example:
//
// lines := model.ViewDown(1)
// 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)
}
// ViewUp is a high performance command the moves the viewport down by one
// viewport height. Use Model.ViewDown to get the lines that should be
// ViewUp is a high performance command the moves the viewport down by a given
// number of lines height. Use Model.ViewDown to get the lines that should be
// rendered.
func ViewUp(m Model, lines []string) tea.Cmd {
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)
}
// 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 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:
switch msg.String() {
// Down one page
case "pgdown":
fallthrough
case " ": // spacebar
fallthrough
case "f":
case "pgdown", spacebar, "f":
lines := m.ViewDown()
if m.HighPerformanceRendering {
cmd = ViewDown(m, lines)
}
// Up one page
case "pgup":
fallthrough
case "b":
case "pgup", "b":
lines := m.ViewUp()
if m.HighPerformanceRendering {
cmd = ViewUp(m, lines)
@@ -316,32 +323,28 @@ func Update(msg tea.Msg, m Model) (Model, tea.Cmd) {
case "d":
lines := m.HalfViewDown()
if m.HighPerformanceRendering {
cmd = HalfViewDown(m, lines)
cmd = ViewDown(m, lines)
}
// Up half page
case "u":
lines := m.HalfViewUp()
if m.HighPerformanceRendering {
cmd = HalfViewUp(m, lines)
cmd = ViewUp(m, lines)
}
// Down one line
case "down":
fallthrough
case "j":
case "down", "j":
lines := m.LineDown(1)
if m.HighPerformanceRendering {
cmd = LineDown(m, lines)
cmd = ViewDown(m, lines)
}
// Up one line
case "up":
fallthrough
case "k":
case "up", "k":
lines := m.LineUp(1)
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:
lines := m.LineUp(3)
if m.HighPerformanceRendering {
cmd = LineUp(m, lines)
cmd = ViewUp(m, lines)
}
case tea.MouseWheelDown:
lines := m.LineDown(3)
if m.HighPerformanceRendering {
cmd = LineDown(m, lines)
cmd = ViewDown(m, lines)
}
}
}