19 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
Christian Rocha
bdd909a5d7 Textinput: bind home/end and fix behavior of delete key 2020-10-22 15:36:54 -04:00
Will Bradley
b08b3efa02 add a couple alternate key bindings to make the scrolling slightly more intuitive 2020-10-21 22:54:58 -04:00
Christian Muehlhaeuser
83b6a2205f Add an EchoMode and EchoCharacter property to textinputs (#11)
EchoModes are:

- EchoNormal, displays text as is
- EchoPassword, displays EchoCharacter mask instead of actual characters
- EchoNone, displays nothing

Note that EchoOnEdit, which should displays character as they are entered and is otherwise identical to EchoPassword, is not yet implemented.
2020-10-21 18:17:41 -04:00
Christian Rocha
c06af8962d Update footer in README 2020-10-20 10:17:26 -04:00
Christian Muehlhaeuser
3a65be950a Remove release badge from README 2020-10-19 06:20:37 +02:00
Christian Muehlhaeuser
2dd6e0c80b Update README badges 2020-10-19 06:19:24 +02:00
Christian Muehlhaeuser
1e50e6d291 Add GitHub workflows 2020-10-19 06:16:01 +02:00
John Diego
573f90a876 Fix typo 2020-10-19 06:02:37 +02:00
10 changed files with 301 additions and 79 deletions

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

@@ -0,0 +1,28 @@
name: build
on: [push, pull_request]
jobs:
test:
strategy:
matrix:
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@v2
with:
go-version: ${{ matrix.go-version }}
- name: Checkout code
uses: actions/checkout@v2
- name: Download Go modules
run: go mod download
- name: Build
run: go build -v ./...
- name: Test
run: go test ./...

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

20
.github/workflows/lint.yml vendored Normal file
View File

@@ -0,0 +1,20 @@
name: lint
on: [push, pull_request]
jobs:
golangci:
name: lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: golangci-lint
uses: golangci/golangci-lint-action@v2
with:
# Required: the version of golangci-lint is required and must be specified without patch version: we always use the latest patch version.
version: v1.30
# Optional: golangci-lint command line arguments.
args: --issues-exit-code=0
# Optional: working directory, useful for monorepos
# working-directory: somedir
# Optional: show only new issues if it's a pull request. The default value is `false`.
only-new-issues: true

26
.golangci.yml Normal file
View File

@@ -0,0 +1,26 @@
run:
tests: false
issues:
max-issues-per-linter: 0
max-same-issues: 0
linters:
enable:
- bodyclose
- dupl
- exportloopref
- goconst
- godot
- godox
- goimports
- gomnd
- goprintffuncname
- gosec
- misspell
- prealloc
- rowserrcheck
- sqlclosecheck
- unconvert
- unparam
- whitespace

View File

@@ -1,10 +1,10 @@
Bubbles Bubbles
======= =======
<p> [![Latest Release](https://img.shields.io/github/release/charmbracelet/bubbles.svg)](https://github.com/charmbracelet/bubbles/releases)
<a href="https://pkg.go.dev/github.com/charmbracelet/bubbles?tab=doc"><img src="https://godoc.org/github.com/golang/gddo?status.svg" alt="GoDoc"></a> [![GoDoc](https://godoc.org/github.com/golang/gddo?status.svg)](https://pkg.go.dev/github.com/charmbracelet/bubbles)
<a href="https://github.com/charmbracelet/bubbles/actions"><img src="https://github.com/charmbracelet/glow/workflows/build/badge.svg" alt="Build Status"></a> [![Build Status](https://github.com/charmbracelet/bubbles/workflows/build/badge.svg)](https://github.com/charmbracelet/bubbles/actions)
</p> [![Go ReportCard](http://goreportcard.com/badge/charmbracelet/bubbles)](http://goreportcard.com/report/charmbracelet/bubbles)
Some components for [Bubble Tea](https://github.com/charmbracelet/bubbletea) applications. Some components for [Bubble Tea](https://github.com/charmbracelet/bubbletea) applications.
@@ -33,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
@@ -59,7 +59,7 @@ for applications which make use of the alterate screen buffer.
* [Example code](https://github.com/charmbracelet/tea/tree/master/examples/pager/main.go) * [Example code](https://github.com/charmbracelet/tea/tree/master/examples/pager/main.go)
This compoent is well complimented with [Reflow][reflow] for ANSI-aware This component is well complimented with [Reflow][reflow] for ANSI-aware
indenting and text wrapping. indenting and text wrapping.
[reflow]: https://github.com/muesli/reflow [reflow]: https://github.com/muesli/reflow
@@ -72,8 +72,9 @@ indenting and text wrapping.
*** ***
A [Charm](https://charm.sh) project. Part of [Charm](https://charm.sh).
<img alt="the Charm logo" src="https://stuff.charm.sh/charm-badge.jpg" width="400"> <a href="https://charm.sh/"><img alt="the Charm logo" src="https://stuff.charm.sh/charm-badge.jpg" width="400"></a>
Charm热爱开源! / Charm loves open source!
Charm热爱开源!

5
go.mod
View File

@@ -4,8 +4,9 @@ go 1.13
require ( require (
github.com/atotto/clipboard v0.1.2 github.com/atotto/clipboard v0.1.2
github.com/charmbracelet/bubbletea v0.12.0 github.com/charmbracelet/bubbletea v0.12.1
github.com/mattn/go-runewidth v0.0.9 github.com/mattn/go-runewidth v0.0.9
github.com/muesli/termenv v0.7.4 github.com/muesli/termenv v0.7.4
golang.org/x/sys v0.0.0-20201015000850-e3ed0017c211 // indirect golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897 // indirect
golang.org/x/sys v0.0.0-20201020230747-6e5568b54d1a // indirect
) )

10
go.sum
View File

@@ -1,7 +1,7 @@
github.com/atotto/clipboard v0.1.2 h1:YZCtFu5Ie8qX2VmVTBnrqLSiU9XOWwqNRmdT3gIQzbY= github.com/atotto/clipboard v0.1.2 h1:YZCtFu5Ie8qX2VmVTBnrqLSiU9XOWwqNRmdT3gIQzbY=
github.com/atotto/clipboard v0.1.2/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= github.com/atotto/clipboard v0.1.2/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
github.com/charmbracelet/bubbletea v0.12.0 h1:/pUHp1GWRDyK1TJAWkXrnRH1u8Xc5076oH/J0NHxH+M= github.com/charmbracelet/bubbletea v0.12.1 h1:t21pkG2IDBRduPbt2J64Dx5yt8yIidAkXwhhrc11SzY=
github.com/charmbracelet/bubbletea v0.12.0/go.mod h1:3gZkYELUOiEUOp0bTInkxguucy/xRbGSOcbMs1geLxg= github.com/charmbracelet/bubbletea v0.12.1/go.mod h1:3gZkYELUOiEUOp0bTInkxguucy/xRbGSOcbMs1geLxg=
github.com/containerd/console v1.0.1 h1:u7SFAJyRqWcG6ogaMAx3KjSTy1e3hT9QxqX7Jco7dRc= github.com/containerd/console v1.0.1 h1:u7SFAJyRqWcG6ogaMAx3KjSTy1e3hT9QxqX7Jco7dRc=
github.com/containerd/console v1.0.1/go.mod h1:XUsP6YE/mKtz6bxc+I8UiKKTP04qjQL4qcS3XoQ5xkw= github.com/containerd/console v1.0.1/go.mod h1:XUsP6YE/mKtz6bxc+I8UiKKTP04qjQL4qcS3XoQ5xkw=
github.com/google/goterm v0.0.0-20190703233501-fc88cf888a3f h1:5CjVwnuUcp5adK4gmY6i72gpVFVnZDP2h5TmPScB6u4= github.com/google/goterm v0.0.0-20190703233501-fc88cf888a3f h1:5CjVwnuUcp5adK4gmY6i72gpVFVnZDP2h5TmPScB6u4=
@@ -21,6 +21,8 @@ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20201012173705-84dcc777aaee h1:4yd7jl+vXjalO5ztz6Vc1VADv+S/80LGJmyl1ROJ2AI= golang.org/x/crypto v0.0.0-20201012173705-84dcc777aaee h1:4yd7jl+vXjalO5ztz6Vc1VADv+S/80LGJmyl1ROJ2AI=
golang.org/x/crypto v0.0.0-20201012173705-84dcc777aaee/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20201012173705-84dcc777aaee/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897 h1:pLI5jrR7OSLijeIDcmRxNmw2api+jEfxLoykJVice/E=
golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -28,6 +30,6 @@ golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20200916030750-2334cc1a136f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200916030750-2334cc1a136f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201009025420-dfb3f7c4e634 h1:bNEHhJCnrwMKNMmOx3yAynp5vs5/gRy+XWFtZFu7NBM= golang.org/x/sys v0.0.0-20201009025420-dfb3f7c4e634 h1:bNEHhJCnrwMKNMmOx3yAynp5vs5/gRy+XWFtZFu7NBM=
golang.org/x/sys v0.0.0-20201009025420-dfb3f7c4e634/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201009025420-dfb3f7c4e634/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201015000850-e3ed0017c211 h1:9UQO31fZ+0aKQOFldThf7BKPMJTiBfWycGh/u3UoO88= golang.org/x/sys v0.0.0-20201020230747-6e5568b54d1a h1:e3IU37lwO4aq3uoRKINC7JikojFmE5gO7xhfxs8VC34=
golang.org/x/sys v0.0.0-20201015000850-e3ed0017c211/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201020230747-6e5568b54d1a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=

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,27 +11,49 @@ import (
"github.com/muesli/termenv" "github.com/muesli/termenv"
) )
const defaultBlinkSpeed = time.Millisecond * 530
const ( const (
defaultBlinkSpeed = time.Millisecond * 600 // EchoNormal displays text as is. This is the default behavior.
EchoNormal EchoMode = iota
// EchoPassword displays the EchoCharacter mask instead of actual
// characters. This is commonly used for password fields.
EchoPassword
// EchoNone displays nothing as characters are entered. This is commonly
// seen for password fields on the command line.
EchoNone
// EchoOnEdit
) )
// EchoMode sets the input behavior of the text input field.
type EchoMode int
var ( var (
// color is a helper for returning colors. // color is a helper for returning colors.
color func(s string) termenv.Color = termenv.ColorProfile().Color color func(s string) termenv.Color = termenv.ColorProfile().Color
) )
// Model is the Tea model for this text input element. // Model is the Bubble Tea model for this text input element.
type Model struct { type Model struct {
Err error Err error
Prompt string
Cursor string Prompt string
BlinkSpeed time.Duration Placeholder string
Placeholder string
Cursor string
BlinkSpeed time.Duration
TextColor string TextColor string
BackgroundColor string BackgroundColor string
PlaceholderColor string PlaceholderColor string
CursorColor string CursorColor string
EchoMode EchoMode
EchoCharacter rune
// CharLimit is the maximum amount of characters this input element will // CharLimit is the maximum amount of characters this input element will
// accept. If 0 or less, there's no limit. // accept. If 0 or less, there's no limit.
CharLimit int CharLimit int
@@ -41,21 +63,21 @@ type Model struct {
// viewport. If 0 or less this setting is ignored. // viewport. If 0 or less this setting is ignored.
Width int Width int
// Underlying text value // Underlying text value.
value []rune value []rune
// Focus indicates whether user input focus should be on this input // Focus indicates whether user input focus should be on this input
// component. When false, don't blink and ignore keyboard input. // component. When false, don't blink and ignore keyboard input.
focus bool focus bool
// Cursor blink state // Cursor blink state.
blink bool blink bool
// Cursor position // Cursor position.
pos int pos int
// Used to emulate a viewport when width is set and the content is // Used to emulate a viewport when width is set and the content is
// overflowing // overflowing.
offset int offset int
offsetRight int offsetRight int
} }
@@ -69,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()
} }
@@ -79,23 +101,22 @@ func (m Model) Value() string {
return string(m.value) return string(m.value)
} }
// Cursor start moves the cursor to the given position. If the position is out // SetCursor start moves the cursor to the given position. If the position is
// 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.
@@ -118,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).
@@ -130,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 {
@@ -139,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]
} }
@@ -152,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
@@ -228,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
@@ -237,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
@@ -246,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
@@ -263,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
@@ -272,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
@@ -280,19 +369,36 @@ func (m *Model) wordRight() {
} }
} }
func (m Model) echoTransform(v string) string {
switch m.EchoMode {
case EchoPassword:
return strings.Repeat(string(m.EchoCharacter), rw.StringWidth(v))
case EchoNone:
return ""
default:
return v
}
}
// BlinkMsg is sent when the cursor should alternate it's blinking state. // BlinkMsg is sent when the cursor should alternate it's blinking state.
type BlinkMsg struct{} type BlinkMsg struct{}
// NewModel creates a new model with default settings. // NewModel creates a new model with default settings.
func NewModel() Model { func NewModel() Model {
return Model{ return Model{
Prompt: "> ", Prompt: "> ",
BlinkSpeed: defaultBlinkSpeed, Placeholder: "",
Placeholder: "",
BlinkSpeed: defaultBlinkSpeed,
TextColor: "", TextColor: "",
PlaceholderColor: "240", PlaceholderColor: "240",
CursorColor: "", CursorColor: "",
CharLimit: 0,
EchoCharacter: '*',
CharLimit: 0,
value: nil, value: nil,
focus: false, focus: false,
@@ -311,13 +417,15 @@ func Update(msg tea.Msg, m Model) (Model, tea.Cmd) {
switch msg := msg.(type) { switch msg := msg.(type) {
case tea.KeyMsg: case tea.KeyMsg:
switch msg.Type { switch msg.Type {
case tea.KeyBackspace: case tea.KeyBackspace: // delete character before cursor
fallthrough if msg.Alt {
case tea.KeyDelete: m.deleteWordLeft()
if len(m.value) > 0 { } else {
m.value = append(m.value[:max(0, m.pos-1)], m.value[m.pos:]...) if len(m.value) > 0 {
if m.pos > 0 { m.value = append(m.value[:max(0, m.pos-1)], m.value[m.pos:]...)
m.pos-- if m.pos > 0 {
m.SetCursor(m.pos - 1)
}
} }
} }
case tea.KeyLeft: case tea.KeyLeft:
@@ -326,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
@@ -334,32 +442,37 @@ 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
fallthrough fallthrough
case tea.KeyCtrlA: // ^A, go to beginning case tea.KeyHome, tea.KeyCtrlA: // ^A, go to beginning
m.CursorStart() m.CursorStart()
case tea.KeyCtrlD: // ^D, delete char under cursor case tea.KeyDelete, tea.KeyCtrlD: // ^D, delete char under cursor
if len(m.value) > 0 && m.pos < len(m.value) { if len(m.value) > 0 && m.pos < len(m.value) {
m.value = append(m.value[:m.pos], m.value[m.pos+1:]...) m.value = append(m.value[:m.pos], m.value[m.pos+1:]...)
} }
case tea.KeyCtrlE: // ^E, go to end case tea.KeyCtrlE, tea.KeyEnd: // ^E, go to end
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
@@ -373,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)
} }
} }
@@ -396,12 +509,11 @@ func View(m Model) string {
value := m.value[m.offset:m.offsetRight] value := m.value[m.offset:m.offsetRight]
pos := max(0, m.pos-m.offset) pos := max(0, m.pos-m.offset)
v := m.colorText(m.echoTransform(string(value[:pos])))
v := m.colorText(string(value[:pos]))
if pos < len(value) { if pos < len(value) {
v += cursorView(string(value[pos]), m) // cursor and text under it v += cursorView(m.echoTransform(string(value[pos])), m) // cursor and text under it
v += m.colorText(string(value[pos+1:])) // text after cursor v += m.colorText(m.echoTransform(string(value[pos+1:]))) // text after cursor
} else { } else {
v += cursorView(" ", m) v += cursorView(" ", m)
} }

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],
@@ -304,14 +304,14 @@ func Update(msg tea.Msg, m Model) (Model, tea.Cmd) {
} }
// Down half page // Down half page
case "d": case "d", "ctrl+d":
lines := m.HalfViewDown() lines := m.HalfViewDown()
if m.HighPerformanceRendering { if m.HighPerformanceRendering {
cmd = ViewDown(m, lines) cmd = ViewDown(m, lines)
} }
// Up half page // Up half page
case "u": case "u", "ctrl+u":
lines := m.HalfViewUp() lines := m.HalfViewUp()
if m.HighPerformanceRendering { if m.HighPerformanceRendering {
cmd = ViewUp(m, lines) cmd = ViewUp(m, lines)
@@ -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