12 Commits

Author SHA1 Message Date
Christian Rocha
f11ca377f4 Add clipboard paste support to textarea 2020-07-23 11:53:58 -04:00
Christian Rocha
10022c964c Fix slice out of bounds errors that could happen when deleting text 2020-07-21 18:22:18 -04:00
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
6 changed files with 193 additions and 106 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

1
go.mod
View File

@@ -3,6 +3,7 @@ module github.com/charmbracelet/bubbles
go 1.13
require (
github.com/atotto/clipboard v0.1.2 // indirect
github.com/charmbracelet/bubbletea v0.9.1-0.20200713153904-2f53eeb54b90
github.com/mattn/go-runewidth v0.0.9
github.com/muesli/termenv v0.5.3-0.20200625163851-04b5c30e4c04

2
go.sum
View File

@@ -1,3 +1,5 @@
github.com/atotto/clipboard v0.1.2 h1:YZCtFu5Ie8qX2VmVTBnrqLSiU9XOWwqNRmdT3gIQzbY=
github.com/atotto/clipboard v0.1.2/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
github.com/google/goterm v0.0.0-20190703233501-fc88cf888a3f h1:5CjVwnuUcp5adK4gmY6i72gpVFVnZDP2h5TmPScB6u4=
github.com/google/goterm v0.0.0-20190703233501-fc88cf888a3f/go.mod h1:nOFQdrUlIlx6M6ODdSpBj1NVA+VgLC6kmw60mkw34H4=
github.com/lucasb-eyer/go-colorful v1.0.3 h1:QIbQXiugsb+q10B+MI+7DI1oQLdmnep86tWFlaaUAac=

View File

@@ -28,6 +28,7 @@ type Model struct {
ActiveDot string
InactiveDot string
ArabicFormat string
UsePgUpPgDownKeys bool
UseLeftRightKeys bool
UseUpDownKeys bool
UseHLKeys bool
@@ -105,6 +106,7 @@ func NewModel() Model {
ActiveDot: "•",
InactiveDot: "○",
ArabicFormat: "%d/%d",
UsePgUpPgDownKeys: true,
UseLeftRightKeys: true,
UseUpDownKeys: false,
UseHLKeys: true,
@@ -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

@@ -5,6 +5,7 @@ import (
"time"
"unicode"
"github.com/atotto/clipboard"
tea "github.com/charmbracelet/bubbletea"
rw "github.com/mattn/go-runewidth"
"github.com/muesli/termenv"
@@ -126,6 +127,47 @@ func (m *Model) Reset() {
m.blink = false
}
// Paste pastes the contents of the clipboard into the text area (if supported).
func (m *Model) Paste() {
pasteString, err := clipboard.ReadAll()
if err != nil {
m.Err = err
}
paste := []rune(pasteString)
availSpace := m.CharLimit - len(m.value)
// If the char limit's been reached cancel
if m.CharLimit > 0 && availSpace <= 0 {
return
}
// If there's not enough space to paste the whole thing cut the pasted
// runes down so they'll fit
if availSpace < len(paste) {
paste = paste[:len(paste)-availSpace]
}
// Stuff before and after the cursor
head := m.value[:m.pos]
tailSrc := m.value[m.pos:]
tail := make([]rune, len(tailSrc))
copy(tail, tailSrc)
// Insert pasted runes
for _, r := range paste {
head = append(head, r)
availSpace--
m.pos++
if m.CharLimit > 0 && availSpace <= 0 {
break
}
}
// Put it all back together
m.value = append(head, tail...)
}
// If a max width is defined, perform some logic to treat the visible area
// as a horizontally scrolling viewport.
func (m *Model) handleOverflow() {
@@ -282,9 +324,11 @@ func Update(msg tea.Msg, m Model) (Model, tea.Cmd) {
fallthrough
case tea.KeyDelete:
if len(m.value) > 0 {
m.value = append(m.value[:m.pos-1], m.value[m.pos:]...)
m.value = append(m.value[:max(0, m.pos-1)], m.value[m.pos:]...)
if m.pos > 0 {
m.pos--
}
}
case tea.KeyLeft:
if msg.Alt { // alt+left arrow, back one word
m.wordLeft()
@@ -320,6 +364,8 @@ func Update(msg tea.Msg, m Model) (Model, tea.Cmd) {
m.value = m.value[m.pos:]
m.pos = 0
m.offset = 0
case tea.KeyCtrlV: // ^V paste
m.Paste()
case tea.KeyRune: // input a regular character
if msg.Alt {
@@ -366,7 +412,7 @@ func View(model tea.Model) string {
}
value := m.value[m.offset:m.offsetRight]
pos := m.pos - m.offset
pos := max(0, m.pos-m.offset)
v := m.colorText(string(value[:pos]))

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))
}
@@ -96,7 +107,7 @@ func (m *Model) ViewDown() []string {
m.YOffset = min(
m.YOffset+m.Height, // target
len(m.lines)-m.Height, // fallback
len(m.lines)-1-m.Height, // fallback
)
return m.visibleLines()
@@ -124,7 +135,7 @@ func (m *Model) HalfViewDown() (lines []string) {
m.YOffset = min(
m.YOffset+m.Height/2, // target
len(m.lines)-m.Height, // fallback
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
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)
}
}
}