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:
strategy:
matrix:
go-version: [1.13.x, 1.14.x, 1.15.x]
go-version: [~1.13, ^1]
os: [ubuntu-latest, macos-latest, windows-latest]
runs-on: ${{ matrix.os }}
env:
GO111MODULE: "on"
steps:
- name: Install Go
uses: actions/setup-go@v1
uses: actions/setup-go@v2
with:
go-version: ${{ matrix.go-version }}
@@ -26,12 +26,3 @@ jobs:
- name: 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
=======
[![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)
[![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.
@@ -32,7 +33,7 @@ pasting, in-place scrolling when the value exceeds the width of the element and
the common, and many customization options.
* [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

View File

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

View File

@@ -11,7 +11,7 @@ import (
"github.com/muesli/termenv"
)
const defaultBlinkSpeed = time.Millisecond * 600
const defaultBlinkSpeed = time.Millisecond * 530
const (
// EchoNormal displays text as is. This is the default behavior.
@@ -91,7 +91,7 @@ func (m *Model) SetValue(s string) {
m.value = runes
}
if m.pos > len(m.value) {
m.pos = len(m.value)
m.SetCursor(len(m.value))
}
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.
func (m *Model) SetCursor(pos int) {
m.pos = clamp(pos, 0, len(m.value))
m.blink = false
m.handleOverflow()
}
// CursorStart moves the cursor to the start of the field.
func (m *Model) CursorStart() {
m.pos = 0
m.handleOverflow()
m.SetCursor(0)
}
// CursorEnd moves the cursor to the end of the field.
func (m *Model) CursorEnd() {
m.pos = len(m.value)
m.handleOverflow()
m.SetCursor(len(m.value))
}
// 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.
func (m *Model) Reset() {
m.value = nil
m.pos = 0
m.blink = false
m.SetCursor(0)
}
// Paste pastes the contents of the clipboard into the text area (if supported).
@@ -152,7 +150,10 @@ func (m *Model) Paste() {
}
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 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
// runes down so they'll fit
if availSpace < len(paste) {
if m.CharLimit > 0 && availSpace < len(paste) {
paste = paste[:len(paste)-availSpace]
}
@@ -174,15 +175,20 @@ func (m *Model) Paste() {
// Insert pasted runes
for _, r := range paste {
head = append(head, r)
availSpace--
m.pos++
if m.CharLimit > 0 && availSpace <= 0 {
break
if m.CharLimit > 0 {
availSpace--
if availSpace <= 0 {
break
}
}
}
// Put it all back together
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
@@ -250,6 +256,67 @@ func (m *Model) colorPlaceholder(s 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() {
if m.pos == 0 || len(m.value) == 0 {
return
@@ -259,7 +326,7 @@ func (m *Model) wordLeft() {
for i >= 0 {
if unicode.IsSpace(m.value[i]) {
m.pos--
m.SetCursor(m.pos - 1)
i--
} else {
break
@@ -268,7 +335,7 @@ func (m *Model) wordLeft() {
for i >= 0 {
if !unicode.IsSpace(m.value[i]) {
m.pos--
m.SetCursor(m.pos - 1)
i--
} else {
break
@@ -285,7 +352,7 @@ func (m *Model) wordRight() {
for i < len(m.value) {
if unicode.IsSpace(m.value[i]) {
m.pos++
m.SetCursor(m.pos + 1)
i++
} else {
break
@@ -294,7 +361,7 @@ func (m *Model) wordRight() {
for i < len(m.value) {
if !unicode.IsSpace(m.value[i]) {
m.pos++
m.SetCursor(m.pos + 1)
i++
} else {
break
@@ -351,10 +418,14 @@ func Update(msg tea.Msg, m Model) (Model, tea.Cmd) {
case tea.KeyMsg:
switch msg.Type {
case tea.KeyBackspace: // delete character before cursor
if len(m.value) > 0 {
m.value = append(m.value[:max(0, m.pos-1)], m.value[m.pos:]...)
if m.pos > 0 {
m.pos--
if msg.Alt {
m.deleteWordLeft()
} else {
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:
@@ -363,7 +434,7 @@ func Update(msg tea.Msg, m Model) (Model, tea.Cmd) {
break
}
if m.pos > 0 {
m.pos--
m.SetCursor(m.pos - 1)
}
case tea.KeyRight:
if msg.Alt { // alt+right arrow, forward one word
@@ -371,8 +442,10 @@ func Update(msg tea.Msg, m Model) (Model, tea.Cmd) {
break
}
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
fallthrough
case tea.KeyCtrlB: // ^B, back one charcter
@@ -387,16 +460,19 @@ func Update(msg tea.Msg, m Model) (Model, tea.Cmd) {
m.CursorEnd()
case tea.KeyCtrlK: // ^K, kill text after cursor
m.value = m.value[:m.pos]
m.pos = len(m.value)
m.SetCursor(len(m.value))
case tea.KeyCtrlU: // ^U, kill text before cursor
m.value = m.value[m.pos:]
m.pos = 0
m.SetCursor(0)
m.offset = 0
case tea.KeyCtrlV: // ^V paste
m.Paste()
case tea.KeyRune: // input a regular character
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
m.wordLeft()
break
@@ -410,7 +486,7 @@ func Update(msg tea.Msg, m Model) (Model, tea.Cmd) {
// Input a regular character
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.pos++
m.SetCursor(m.pos + 1)
}
}

View File

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