11 Commits

Author SHA1 Message Date
Nicolas Martin
7d1c04164e Return the value of paginator.TotalPages in SetTotalPages...
when the given number of items is below 1.
2020-10-28 10:40:23 -04:00
Christian Muehlhaeuser
97020cd0d2 Support Alt-Backspace to delete previous word 2020-10-27 08:50:22 +01:00
Christian Rocha
5dbcf95877 Fix situation where short viewports would panic (closes #20) 2020-10-25 15:21:49 -04:00
Christian Muehlhaeuser
f58fead10d Support Ctrl-W and Alt-D to delete words left/right of cursor
Ctrl-W: deletes word left of cursor
Alt-D: deletes word right of cursor
2020-10-25 14:52:16 -04:00
Christian Rocha
703de11da4 Fix regression where cursor was misplaced after a paste 2020-10-24 19:51:13 -04:00
Christian Rocha
03461d6804 Fix panic when pasting into a textinput with no char limit 2020-10-24 19:51:13 -04:00
Christian Muehlhaeuser
4445acbace Use the same badge order as in our other repos 2020-10-24 08:43:30 +02:00
Christian Muehlhaeuser
bd161e8ded Add release badge to README 2020-10-24 08:39:00 +02:00
Christian Muehlhaeuser
d9716a97f6 Fix link to example in README 2020-10-24 08:36:29 +02:00
Christian Muehlhaeuser
a0fe547fdb Make textinput cursor visible whenever it changes its position
This improves the UX because you never lose track of the cursor
moving around while it's currently hidden.
2020-10-24 00:12:27 -04:00
Christian Muehlhaeuser
1cb36774ed Split up workflows and automatically pick latest Go version 2020-10-23 13:18:52 +02:00
6 changed files with 149 additions and 49 deletions

View File

@@ -4,14 +4,14 @@ jobs:
test: test:
strategy: strategy:
matrix: matrix:
go-version: [1.13.x, 1.14.x, 1.15.x] go-version: [~1.13, ^1]
os: [ubuntu-latest, macos-latest, windows-latest] os: [ubuntu-latest, macos-latest, windows-latest]
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
env: env:
GO111MODULE: "on" GO111MODULE: "on"
steps: steps:
- name: Install Go - name: Install Go
uses: actions/setup-go@v1 uses: actions/setup-go@v2
with: with:
go-version: ${{ matrix.go-version }} go-version: ${{ matrix.go-version }}
@@ -26,12 +26,3 @@ jobs:
- name: Test - name: Test
run: go test ./... run: go test ./...
- name: Coverage
env:
COVERALLS_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
go test -race -covermode atomic -coverprofile=profile.cov ./...
GO111MODULE=off go get github.com/mattn/goveralls
$(go env GOPATH)/bin/goveralls -coverprofile=profile.cov -service=github
if: matrix.go-version == '1.15.x' && matrix.os == 'ubuntu-latest'

28
.github/workflows/coverage.yml vendored Normal file
View File

@@ -0,0 +1,28 @@
name: coverage
on: [push, pull_request]
jobs:
coverage:
strategy:
matrix:
go-version: [^1]
os: [ubuntu-latest]
runs-on: ${{ matrix.os }}
env:
GO111MODULE: "on"
steps:
- name: Install Go
uses: actions/setup-go@v2
with:
go-version: ${{ matrix.go-version }}
- name: Checkout code
uses: actions/checkout@v2
- name: Coverage
env:
COVERALLS_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
go test -race -covermode atomic -coverprofile=profile.cov ./...
GO111MODULE=off go get github.com/mattn/goveralls
$(go env GOPATH)/bin/goveralls -coverprofile=profile.cov -service=github

View File

@@ -1,9 +1,10 @@
Bubbles Bubbles
======= =======
[![Latest Release](https://img.shields.io/github/release/charmbracelet/bubbles.svg)](https://github.com/charmbracelet/bubbles/releases)
[![GoDoc](https://godoc.org/github.com/golang/gddo?status.svg)](https://pkg.go.dev/github.com/charmbracelet/bubbles)
[![Build Status](https://github.com/charmbracelet/bubbles/workflows/build/badge.svg)](https://github.com/charmbracelet/bubbles/actions) [![Build Status](https://github.com/charmbracelet/bubbles/workflows/build/badge.svg)](https://github.com/charmbracelet/bubbles/actions)
[![Go ReportCard](http://goreportcard.com/badge/charmbracelet/bubbles)](http://goreportcard.com/report/charmbracelet/bubbles) [![Go ReportCard](http://goreportcard.com/badge/charmbracelet/bubbles)](http://goreportcard.com/report/charmbracelet/bubbles)
[![GoDoc](https://godoc.org/github.com/golang/gddo?status.svg)](https://pkg.go.dev/github.com/charmbracelet/bubbles)
Some components for [Bubble Tea](https://github.com/charmbracelet/bubbletea) applications. Some components for [Bubble Tea](https://github.com/charmbracelet/bubbletea) applications.
@@ -32,7 +33,7 @@ pasting, in-place scrolling when the value exceeds the width of the element and
the common, and many customization options. the common, and many customization options.
* [Example code, one field](https://github.com/charmbracelet/tea/tree/master/examples/textinput/main.go) * [Example code, one field](https://github.com/charmbracelet/tea/tree/master/examples/textinput/main.go)
* [Example code, many fields](https://github.com/charmbracelet/tea/tree/master/examples/textinput/main.go) * [Example code, many fields](https://github.com/charmbracelet/tea/tree/master/examples/textinputs/main.go)
## Paginator ## Paginator

View File

@@ -40,8 +40,8 @@ type Model struct {
// used for other things beyond navigating sets. Note that it both returns the // used for other things beyond navigating sets. Note that it both returns the
// number of total pages and alters the model. // number of total pages and alters the model.
func (m *Model) SetTotalPages(items int) int { func (m *Model) SetTotalPages(items int) int {
if items == 0 { if items < 1 {
return 0 return m.TotalPages
} }
n := items / m.PerPage n := items / m.PerPage
if items%m.PerPage > 0 { if items%m.PerPage > 0 {

View File

@@ -11,7 +11,7 @@ import (
"github.com/muesli/termenv" "github.com/muesli/termenv"
) )
const defaultBlinkSpeed = time.Millisecond * 600 const defaultBlinkSpeed = time.Millisecond * 530
const ( const (
// EchoNormal displays text as is. This is the default behavior. // EchoNormal displays text as is. This is the default behavior.
@@ -91,7 +91,7 @@ func (m *Model) SetValue(s string) {
m.value = runes m.value = runes
} }
if m.pos > len(m.value) { if m.pos > len(m.value) {
m.pos = len(m.value) m.SetCursor(len(m.value))
} }
m.handleOverflow() m.handleOverflow()
} }
@@ -105,19 +105,18 @@ func (m Model) Value() string {
// out of bounds the cursor will be moved to the start or end accordingly. // out of bounds the cursor will be moved to the start or end accordingly.
func (m *Model) SetCursor(pos int) { func (m *Model) SetCursor(pos int) {
m.pos = clamp(pos, 0, len(m.value)) m.pos = clamp(pos, 0, len(m.value))
m.blink = false
m.handleOverflow() m.handleOverflow()
} }
// CursorStart moves the cursor to the start of the field. // CursorStart moves the cursor to the start of the field.
func (m *Model) CursorStart() { func (m *Model) CursorStart() {
m.pos = 0 m.SetCursor(0)
m.handleOverflow()
} }
// CursorEnd moves the cursor to the end of the field. // CursorEnd moves the cursor to the end of the field.
func (m *Model) CursorEnd() { func (m *Model) CursorEnd() {
m.pos = len(m.value) m.SetCursor(len(m.value))
m.handleOverflow()
} }
// Focused returns the focus state on the model. // Focused returns the focus state on the model.
@@ -140,8 +139,7 @@ func (m *Model) Blur() {
// Reset sets the input to its default state with no input. // Reset sets the input to its default state with no input.
func (m *Model) Reset() { func (m *Model) Reset() {
m.value = nil m.value = nil
m.pos = 0 m.SetCursor(0)
m.blink = false
} }
// Paste pastes the contents of the clipboard into the text area (if supported). // Paste pastes the contents of the clipboard into the text area (if supported).
@@ -152,7 +150,10 @@ func (m *Model) Paste() {
} }
paste := []rune(pasteString) paste := []rune(pasteString)
availSpace := m.CharLimit - len(m.value) var availSpace int
if m.CharLimit > 0 {
availSpace = m.CharLimit - len(m.value)
}
// If the char limit's been reached cancel // If the char limit's been reached cancel
if m.CharLimit > 0 && availSpace <= 0 { if m.CharLimit > 0 && availSpace <= 0 {
@@ -161,7 +162,7 @@ func (m *Model) Paste() {
// If there's not enough space to paste the whole thing cut the pasted // If there's not enough space to paste the whole thing cut the pasted
// runes down so they'll fit // runes down so they'll fit
if availSpace < len(paste) { if m.CharLimit > 0 && availSpace < len(paste) {
paste = paste[:len(paste)-availSpace] paste = paste[:len(paste)-availSpace]
} }
@@ -174,15 +175,20 @@ func (m *Model) Paste() {
// Insert pasted runes // Insert pasted runes
for _, r := range paste { for _, r := range paste {
head = append(head, r) head = append(head, r)
availSpace--
m.pos++ m.pos++
if m.CharLimit > 0 && availSpace <= 0 { if m.CharLimit > 0 {
break availSpace--
if availSpace <= 0 {
break
}
} }
} }
// Put it all back together // Put it all back together
m.value = append(head, tail...) m.value = append(head, tail...)
// Reset blink state and run overflow checks
m.SetCursor(m.pos)
} }
// If a max width is defined, perform some logic to treat the visible area // If a max width is defined, perform some logic to treat the visible area
@@ -250,6 +256,67 @@ func (m *Model) colorPlaceholder(s string) string {
String() String()
} }
// deleteWordLeft deletes the word left to the cursor.
func (m *Model) deleteWordLeft() {
if m.pos == 0 || len(m.value) == 0 {
return
}
i := m.pos
m.SetCursor(m.pos - 1)
for unicode.IsSpace(m.value[m.pos]) {
// ignore series of whitespace before cursor
m.SetCursor(m.pos - 1)
}
for m.pos > 0 {
if !unicode.IsSpace(m.value[m.pos]) {
m.SetCursor(m.pos - 1)
} else {
if m.pos > 0 {
// keep the previous space
m.SetCursor(m.pos + 1)
}
break
}
}
if i > len(m.value) {
m.value = m.value[:m.pos]
} else {
m.value = append(m.value[:m.pos], m.value[i:]...)
}
}
// deleteWordRight deletes the word right to the cursor.
func (m *Model) deleteWordRight() {
if m.pos >= len(m.value) || len(m.value) == 0 {
return
}
i := m.pos
m.SetCursor(m.pos + 1)
for unicode.IsSpace(m.value[m.pos]) {
// ignore series of whitespace after cursor
m.SetCursor(m.pos + 1)
}
for m.pos < len(m.value) {
if !unicode.IsSpace(m.value[m.pos]) {
m.SetCursor(m.pos + 1)
} else {
break
}
}
if m.pos > len(m.value) {
m.value = m.value[:i]
} else {
m.value = append(m.value[:i], m.value[m.pos:]...)
}
m.SetCursor(i)
}
func (m *Model) wordLeft() { func (m *Model) wordLeft() {
if m.pos == 0 || len(m.value) == 0 { if m.pos == 0 || len(m.value) == 0 {
return return
@@ -259,7 +326,7 @@ func (m *Model) wordLeft() {
for i >= 0 { for i >= 0 {
if unicode.IsSpace(m.value[i]) { if unicode.IsSpace(m.value[i]) {
m.pos-- m.SetCursor(m.pos - 1)
i-- i--
} else { } else {
break break
@@ -268,7 +335,7 @@ func (m *Model) wordLeft() {
for i >= 0 { for i >= 0 {
if !unicode.IsSpace(m.value[i]) { if !unicode.IsSpace(m.value[i]) {
m.pos-- m.SetCursor(m.pos - 1)
i-- i--
} else { } else {
break break
@@ -285,7 +352,7 @@ func (m *Model) wordRight() {
for i < len(m.value) { for i < len(m.value) {
if unicode.IsSpace(m.value[i]) { if unicode.IsSpace(m.value[i]) {
m.pos++ m.SetCursor(m.pos + 1)
i++ i++
} else { } else {
break break
@@ -294,7 +361,7 @@ func (m *Model) wordRight() {
for i < len(m.value) { for i < len(m.value) {
if !unicode.IsSpace(m.value[i]) { if !unicode.IsSpace(m.value[i]) {
m.pos++ m.SetCursor(m.pos + 1)
i++ i++
} else { } else {
break break
@@ -351,10 +418,14 @@ func Update(msg tea.Msg, m Model) (Model, tea.Cmd) {
case tea.KeyMsg: case tea.KeyMsg:
switch msg.Type { switch msg.Type {
case tea.KeyBackspace: // delete character before cursor case tea.KeyBackspace: // delete character before cursor
if len(m.value) > 0 { if msg.Alt {
m.value = append(m.value[:max(0, m.pos-1)], m.value[m.pos:]...) m.deleteWordLeft()
if m.pos > 0 { } else {
m.pos-- if len(m.value) > 0 {
m.value = append(m.value[:max(0, m.pos-1)], m.value[m.pos:]...)
if m.pos > 0 {
m.SetCursor(m.pos - 1)
}
} }
} }
case tea.KeyLeft: case tea.KeyLeft:
@@ -363,7 +434,7 @@ func Update(msg tea.Msg, m Model) (Model, tea.Cmd) {
break break
} }
if m.pos > 0 { if m.pos > 0 {
m.pos-- m.SetCursor(m.pos - 1)
} }
case tea.KeyRight: case tea.KeyRight:
if msg.Alt { // alt+right arrow, forward one word if msg.Alt { // alt+right arrow, forward one word
@@ -371,8 +442,10 @@ func Update(msg tea.Msg, m Model) (Model, tea.Cmd) {
break break
} }
if m.pos < len(m.value) { if m.pos < len(m.value) {
m.pos++ m.SetCursor(m.pos + 1)
} }
case tea.KeyCtrlW: // ^W, delete word left of cursor
m.deleteWordLeft()
case tea.KeyCtrlF: // ^F, forward one character case tea.KeyCtrlF: // ^F, forward one character
fallthrough fallthrough
case tea.KeyCtrlB: // ^B, back one charcter case tea.KeyCtrlB: // ^B, back one charcter
@@ -387,16 +460,19 @@ func Update(msg tea.Msg, m Model) (Model, tea.Cmd) {
m.CursorEnd() m.CursorEnd()
case tea.KeyCtrlK: // ^K, kill text after cursor case tea.KeyCtrlK: // ^K, kill text after cursor
m.value = m.value[:m.pos] m.value = m.value[:m.pos]
m.pos = len(m.value) m.SetCursor(len(m.value))
case tea.KeyCtrlU: // ^U, kill text before cursor case tea.KeyCtrlU: // ^U, kill text before cursor
m.value = m.value[m.pos:] m.value = m.value[m.pos:]
m.pos = 0 m.SetCursor(0)
m.offset = 0 m.offset = 0
case tea.KeyCtrlV: // ^V paste case tea.KeyCtrlV: // ^V paste
m.Paste() m.Paste()
case tea.KeyRune: // input a regular character case tea.KeyRune: // input a regular character
if msg.Alt { if msg.Alt {
if msg.Rune == 'd' { // alt+d, delete word right of cursor
m.deleteWordRight()
break
}
if msg.Rune == 'b' { // alt+b, back one word if msg.Rune == 'b' { // alt+b, back one word
m.wordLeft() m.wordLeft()
break break
@@ -410,7 +486,7 @@ func Update(msg tea.Msg, m Model) (Model, tea.Cmd) {
// Input a regular character // Input a regular character
if m.CharLimit <= 0 || len(m.value) < m.CharLimit { if m.CharLimit <= 0 || len(m.value) < m.CharLimit {
m.value = append(m.value[:m.pos], append([]rune{msg.Rune}, m.value[m.pos:]...)...) m.value = append(m.value[:m.pos], append([]rune{msg.Rune}, m.value[m.pos:]...)...)
m.pos++ m.SetCursor(m.pos + 1)
} }
} }

View File

@@ -77,7 +77,7 @@ func (m *Model) SetContent(s string) {
func (m Model) visibleLines() (lines []string) { func (m Model) visibleLines() (lines []string) {
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), m.YOffset+m.Height) bottom := clamp(m.YOffset+m.Height, top, len(m.lines))
lines = m.lines[top:bottom] lines = m.lines[top:bottom]
} }
return lines return lines
@@ -125,7 +125,7 @@ func (m *Model) HalfViewDown() (lines []string) {
if len(m.lines) > 0 { if len(m.lines) > 0 {
top := max(m.YOffset+m.Height/2, 0) top := max(m.YOffset+m.Height/2, 0)
bottom := min(m.YOffset+m.Height, len(m.lines)-1) bottom := clamp(m.YOffset+m.Height, top, len(m.lines)-1)
lines = m.lines[top:bottom] lines = m.lines[top:bottom]
} }
@@ -145,7 +145,7 @@ func (m *Model) HalfViewUp() (lines []string) {
if len(m.lines) > 0 { if len(m.lines) > 0 {
top := max(m.YOffset, 0) top := max(m.YOffset, 0)
bottom := min(m.YOffset+m.Height/2, len(m.lines)-1) bottom := clamp(m.YOffset+m.Height/2, top, len(m.lines)-1)
lines = m.lines[top:bottom] lines = m.lines[top:bottom]
} }
@@ -171,7 +171,7 @@ func (m *Model) LineDown(n int) (lines []string) {
if len(m.lines) > 0 { if len(m.lines) > 0 {
top := max(m.YOffset+m.Height-n, 0) top := max(m.YOffset+m.Height-n, 0)
bottom := min(m.YOffset+m.Height, len(m.lines)-1) bottom := clamp(m.YOffset+m.Height, top, len(m.lines)-1)
lines = m.lines[top:bottom] lines = m.lines[top:bottom]
} }
@@ -193,7 +193,7 @@ func (m *Model) LineUp(n int) (lines []string) {
if len(m.lines) > 0 { if len(m.lines) > 0 {
top := max(0, m.YOffset) top := max(0, m.YOffset)
bottom := min(m.YOffset+n, len(m.lines)-1) bottom := clamp(m.YOffset+n, top, len(m.lines)-1)
lines = m.lines[top:bottom] lines = m.lines[top:bottom]
} }
@@ -210,7 +210,7 @@ func (m *Model) GotoTop() (lines []string) {
if len(m.lines) > 0 { if len(m.lines) > 0 {
top := m.YOffset top := m.YOffset
bottom := min(m.YOffset+m.Height, len(m.lines)-1) bottom := clamp(m.YOffset+m.Height, top, len(m.lines)-1)
lines = m.lines[top:bottom] lines = m.lines[top:bottom]
} }
@@ -245,7 +245,7 @@ func Sync(m Model) tea.Cmd {
// TODO: we should probably use m.visibleLines() rather than these two // TODO: we should probably use m.visibleLines() rather than these two
// expressions. // expressions.
top := max(m.YOffset, 0) top := max(m.YOffset, 0)
bottom := min(m.YOffset+m.Height, len(m.lines)-1) bottom := clamp(m.YOffset+m.Height, 0, len(m.lines)-1)
return tea.SyncScrollArea( return tea.SyncScrollArea(
m.lines[top:bottom], m.lines[top:bottom],
@@ -376,6 +376,10 @@ func View(m Model) string {
// ETC // ETC
func clamp(v, low, high int) int {
return min(high, max(low, v))
}
func min(a, b int) int { func min(a, b int) int {
if a < b { if a < b {
return a return a