19 Commits

Author SHA1 Message Date
Christian Rocha
d02641f6b5 Update textinput for multi-char (IME) input in Bubble Tea master 2020-11-01 09:11:18 -05:00
Christian Rocha
9b47f26bdd Doc and logic corrections per the linter 2020-10-28 21:27:15 -04:00
Christian Rocha
9c780011ff Underpinnings for always-on and always-hidden cursor modes 2020-10-28 21:27:15 -04:00
Christian Rocha
8148e61443 Correct ^F/^B keybindings for forward/back cursor movement 2020-10-28 21:27:15 -04:00
Christian Rocha
ce7d8da084 Fix lock-up that could occur with cursor blinking
Note that to do this we've replaced the blink timer with a context.
2020-10-28 21:27:15 -04:00
Christian Rocha
d14fdf585c Textinput Update and View functions are now methods on the model 2020-10-28 21:27:15 -04:00
Christian Rocha
bf7719e6c1 Handle paste via command/message since it's IO 2020-10-28 21:27:15 -04:00
Christian Rocha
1b530b293c Reset blink timer when moving the cursor 2020-10-28 21:27:15 -04:00
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
8 changed files with 336 additions and 153 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

2
go.mod
View File

@@ -4,7 +4,7 @@ go 1.13
require (
github.com/atotto/clipboard v0.1.2
github.com/charmbracelet/bubbletea v0.12.1
github.com/charmbracelet/bubbletea v0.12.2-0.20201101135743-116a0cfb8f37
github.com/mattn/go-runewidth v0.0.9
github.com/muesli/termenv v0.7.4
golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897 // indirect

4
go.sum
View File

@@ -1,7 +1,7 @@
github.com/atotto/clipboard v0.1.2 h1:YZCtFu5Ie8qX2VmVTBnrqLSiU9XOWwqNRmdT3gIQzbY=
github.com/atotto/clipboard v0.1.2/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
github.com/charmbracelet/bubbletea v0.12.1 h1:t21pkG2IDBRduPbt2J64Dx5yt8yIidAkXwhhrc11SzY=
github.com/charmbracelet/bubbletea v0.12.1/go.mod h1:3gZkYELUOiEUOp0bTInkxguucy/xRbGSOcbMs1geLxg=
github.com/charmbracelet/bubbletea v0.12.2-0.20201101135743-116a0cfb8f37 h1:BQLGyhKVE19a9XdNYcsnYlO9XHPlOVHIWM7+mmS014k=
github.com/charmbracelet/bubbletea v0.12.2-0.20201101135743-116a0cfb8f37/go.mod h1:3gZkYELUOiEUOp0bTInkxguucy/xRbGSOcbMs1geLxg=
github.com/containerd/console v1.0.1 h1:u7SFAJyRqWcG6ogaMAx3KjSTy1e3hT9QxqX7Jco7dRc=
github.com/containerd/console v1.0.1/go.mod h1:XUsP6YE/mKtz6bxc+I8UiKKTP04qjQL4qcS3XoQ5xkw=
github.com/google/goterm v0.0.0-20190703233501-fc88cf888a3f h1:5CjVwnuUcp5adK4gmY6i72gpVFVnZDP2h5TmPScB6u4=

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

@@ -1,6 +1,7 @@
package textinput
import (
"context"
"strings"
"time"
"unicode"
@@ -11,7 +12,21 @@ import (
"github.com/muesli/termenv"
)
const defaultBlinkSpeed = time.Millisecond * 600
const defaultBlinkSpeed = time.Millisecond * 530
// color is a helper for returning colors.
var color func(s string) termenv.Color = termenv.ColorProfile().Color
// blinkMsg and blinkCanceled are used to manage cursor blinking.
type blinkMsg struct{}
type blinkCanceled struct{}
// Messages for clipboard events.
type pasteMsg string
type pasteErrMsg struct{ error }
// EchoMode sets the input behavior of the text input field.
type EchoMode int
const (
// EchoNormal displays text as is. This is the default behavior.
@@ -28,29 +43,33 @@ const (
// EchoOnEdit
)
// EchoMode sets the input behavior of the text input field.
type EchoMode int
// blinkCtx manages cursor blinking.
type blinkCtx struct {
ctx context.Context
cancel context.CancelFunc
}
var (
// color is a helper for returning colors.
color func(s string) termenv.Color = termenv.ColorProfile().Color
type cursorMode int
const (
cursorBlink = iota
cursorStatic
cursorHide
)
// Model is the Bubble Tea model for this text input element.
type Model struct {
Err error
// General settings
Prompt string
Placeholder string
Cursor string
BlinkSpeed time.Duration
TextColor string
BackgroundColor string
PlaceholderColor string
CursorColor string
EchoMode EchoMode
EchoCharacter rune
@@ -80,6 +99,36 @@ type Model struct {
// overflowing.
offset int
offsetRight int
// Used to manage cursor blink
blinkCtx *blinkCtx
// cursorMode determines the behavior of the cursor
cursorMode cursorMode
}
// NewModel creates a new model with default settings.
func NewModel() Model {
return Model{
Prompt: "> ",
Placeholder: "",
BlinkSpeed: defaultBlinkSpeed,
TextColor: "",
PlaceholderColor: "240",
CursorColor: "",
EchoCharacter: '*',
CharLimit: 0,
value: nil,
focus: false,
blink: true,
pos: 0,
cursorMode: cursorBlink,
blinkCtx: &blinkCtx{
ctx: context.Background(),
},
}
}
// SetValue sets the value of the text input.
@@ -91,7 +140,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()
}
@@ -103,21 +152,28 @@ func (m Model) Value() string {
// SetCursor start moves the cursor to the given position. If the position is
// out of bounds the cursor will be moved to the start or end accordingly.
func (m *Model) SetCursor(pos int) {
// Returns whether or nor the cursor timer should be reset.
func (m *Model) SetCursor(pos int) bool {
m.pos = clamp(pos, 0, len(m.value))
m.handleOverflow()
// Show the cursor unless it's been explicitly hidden
m.blink = m.cursorMode == cursorHide
// Reset cursor blink if necessary
return m.cursorMode == cursorBlink
}
// CursorStart moves the cursor to the start of the field.
func (m *Model) CursorStart() {
m.pos = 0
m.handleOverflow()
// CursorStart moves the cursor to the start of the field. Returns whether or
// not the curosr blink should be reset.
func (m *Model) CursorStart() bool {
return m.SetCursor(0)
}
// CursorEnd moves the cursor to the end of the field.
func (m *Model) CursorEnd() {
m.pos = len(m.value)
m.handleOverflow()
// CursorEnd moves the cursor to the end of the field. Returns whether or not
// the cursor blink should be reset.
func (m *Model) CursorEnd() bool {
return m.SetCursor(len(m.value))
}
// Focused returns the focus state on the model.
@@ -128,7 +184,7 @@ func (m Model) Focused() bool {
// Focus sets the focus state on the model.
func (m *Model) Focus() {
m.focus = true
m.blink = false
m.blink = m.cursorMode == cursorHide // show the cursor unless we've explicitly hidden it
}
// Blur removes the focus state on the model.
@@ -137,22 +193,22 @@ func (m *Model) Blur() {
m.blink = true
}
// Reset sets the input to its default state with no input.
func (m *Model) Reset() {
// Reset sets the input to its default state with no input. Returns whether
// or not the cursor blink should reset.
func (m *Model) Reset() bool {
m.value = nil
m.pos = 0
m.blink = false
return m.SetCursor(0)
}
// 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)
// handle a clipboard paste event, if supported. Returns whether or not the
// cursor blink should be reset.
func (m *Model) handlePaste(v string) (blink bool) {
paste := []rune(v)
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 +217,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 +230,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 {
if m.CharLimit > 0 {
availSpace--
if availSpace <= 0 {
break
}
}
}
// Put it all back together
m.value = append(head, tail...)
// Reset blink state if necessary and run overflow checks
return m.SetCursor(m.pos)
}
// If a max width is defined, perform some logic to treat the visible area
@@ -250,7 +311,76 @@ func (m *Model) colorPlaceholder(s string) string {
String()
}
func (m *Model) wordLeft() {
// deleteWordLeft deletes the word left to the cursor. Returns whether or not
// the cursor blink should be reset.
func (m *Model) deleteWordLeft() (blink bool) {
if m.pos == 0 || len(m.value) == 0 {
return
}
i := m.pos
blink = m.SetCursor(m.pos - 1)
for unicode.IsSpace(m.value[m.pos]) {
// ignore series of whitespace before cursor
blink = m.SetCursor(m.pos - 1)
}
for m.pos > 0 {
if !unicode.IsSpace(m.value[m.pos]) {
blink = m.SetCursor(m.pos - 1)
} else {
if m.pos > 0 {
// keep the previous space
blink = 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:]...)
}
return
}
// deleteWordRight deletes the word right to the cursor. Returns whether or not
// the cursor blink should be reset.
func (m *Model) deleteWordRight() (blink bool) {
if m.pos >= len(m.value) || len(m.value) == 0 {
return
}
i := m.pos
blink = m.SetCursor(m.pos + 1)
for unicode.IsSpace(m.value[m.pos]) {
// ignore series of whitespace after cursor
blink = m.SetCursor(m.pos + 1)
}
for m.pos < len(m.value) {
if !unicode.IsSpace(m.value[m.pos]) {
blink = 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:]...)
}
blink = m.SetCursor(i)
return
}
// wordLeft moves the cursor one word to the left. Returns whether or not the
// cursor blink should be reset.
func (m *Model) wordLeft() (blink bool) {
if m.pos == 0 || len(m.value) == 0 {
return
}
@@ -259,7 +389,7 @@ func (m *Model) wordLeft() {
for i >= 0 {
if unicode.IsSpace(m.value[i]) {
m.pos--
blink = m.SetCursor(m.pos - 1)
i--
} else {
break
@@ -268,15 +398,19 @@ func (m *Model) wordLeft() {
for i >= 0 {
if !unicode.IsSpace(m.value[i]) {
m.pos--
blink = m.SetCursor(m.pos - 1)
i--
} else {
break
}
}
return
}
func (m *Model) wordRight() {
// wordRight moves the cursor one word to the right. Returns whether or not the
// cursor blink should be reset.
func (m *Model) wordRight() (blink bool) {
if m.pos >= len(m.value) || len(m.value) == 0 {
return
}
@@ -285,7 +419,7 @@ func (m *Model) wordRight() {
for i < len(m.value) {
if unicode.IsSpace(m.value[i]) {
m.pos++
blink = m.SetCursor(m.pos + 1)
i++
} else {
break
@@ -294,12 +428,14 @@ func (m *Model) wordRight() {
for i < len(m.value) {
if !unicode.IsSpace(m.value[i]) {
m.pos++
blink = m.SetCursor(m.pos + 1)
i++
} else {
break
}
}
return
}
func (m Model) echoTransform(v string) string {
@@ -314,121 +450,119 @@ func (m Model) echoTransform(v string) string {
}
}
// BlinkMsg is sent when the cursor should alternate it's blinking state.
type BlinkMsg struct{}
// NewModel creates a new model with default settings.
func NewModel() Model {
return Model{
Prompt: "> ",
Placeholder: "",
BlinkSpeed: defaultBlinkSpeed,
TextColor: "",
PlaceholderColor: "240",
CursorColor: "",
EchoCharacter: '*',
CharLimit: 0,
value: nil,
focus: false,
blink: true,
pos: 0,
}
}
// Update is the Tea update loop.
func Update(msg tea.Msg, m Model) (Model, tea.Cmd) {
// Update is the Bubble Tea update loop.
func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
if !m.focus {
m.blink = true
return m, nil
}
var resetBlink bool
switch msg := msg.(type) {
case tea.KeyMsg:
switch msg.Type {
case tea.KeyBackspace: // delete character before cursor
if msg.Alt {
resetBlink = 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.pos--
resetBlink = m.SetCursor(m.pos - 1)
}
}
case tea.KeyLeft:
}
case tea.KeyLeft, tea.KeyCtrlB:
if msg.Alt { // alt+left arrow, back one word
m.wordLeft()
resetBlink = m.wordLeft()
break
}
if m.pos > 0 {
m.pos--
if m.pos > 0 { // left arrow, ^F, back one character
resetBlink = m.SetCursor(m.pos - 1)
}
case tea.KeyRight:
case tea.KeyRight, tea.KeyCtrlF:
if msg.Alt { // alt+right arrow, forward one word
m.wordRight()
resetBlink = m.wordRight()
break
}
if m.pos < len(m.value) {
m.pos++
if m.pos < len(m.value) { // right arrow, ^F, forward one word
resetBlink = m.SetCursor(m.pos + 1)
}
case tea.KeyCtrlF: // ^F, forward one character
fallthrough
case tea.KeyCtrlB: // ^B, back one charcter
fallthrough
case tea.KeyCtrlW: // ^W, delete word left of cursor
resetBlink = m.deleteWordLeft()
case tea.KeyHome, tea.KeyCtrlA: // ^A, go to beginning
m.CursorStart()
resetBlink = m.CursorStart()
case tea.KeyDelete, tea.KeyCtrlD: // ^D, delete char under cursor
if len(m.value) > 0 && m.pos < len(m.value) {
m.value = append(m.value[:m.pos], m.value[m.pos+1:]...)
}
case tea.KeyCtrlE, tea.KeyEnd: // ^E, go to end
m.CursorEnd()
resetBlink = m.CursorEnd()
case tea.KeyCtrlK: // ^K, kill text after cursor
m.value = m.value[:m.pos]
m.pos = len(m.value)
resetBlink = m.SetCursor(len(m.value))
case tea.KeyCtrlU: // ^U, kill text before cursor
m.value = m.value[m.pos:]
m.pos = 0
resetBlink = 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 == 'b' { // alt+b, back one word
m.wordLeft()
return m, Paste
case tea.KeyRunes: // input regular characters
if msg.Alt && len(msg.Runes) == 1 {
if msg.Runes[0] == 'd' { // alt+d, delete word right of cursor
resetBlink = m.deleteWordRight()
break
}
if msg.Rune == 'f' { // alt+f, forward one word
m.wordRight()
if msg.Runes[0] == 'b' { // alt+b, back one word
resetBlink = m.wordLeft()
break
}
if msg.Runes[0] == 'f' { // alt+f, forward one word
resetBlink = m.wordRight()
break
}
}
// 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.value = append(m.value[:m.pos], append(msg.Runes, m.value[m.pos:]...)...)
resetBlink = m.SetCursor(m.pos + len(msg.Runes))
}
}
case BlinkMsg:
case blinkMsg:
var cmd tea.Cmd
if m.cursorMode == cursorBlink {
m.blink = !m.blink
return m, Blink(m)
cmd = m.blinkCmd()
}
return m, cmd
case blinkCanceled: // no-op
return m, nil
case pasteMsg:
resetBlink = m.handlePaste(string(msg))
case pasteErrMsg:
m.Err = msg
}
var cmd tea.Cmd
if resetBlink {
cmd = m.blinkCmd()
}
m.handleOverflow()
return m, nil
return m, cmd
}
// View renders the textinput in its current state.
func View(m Model) string {
func (m Model) View() string {
// Placeholder text
if len(m.value) == 0 && m.Placeholder != "" {
return placeholderView(m)
return m.placeholderView()
}
value := m.value[m.offset:m.offsetRight]
@@ -436,10 +570,10 @@ func View(m Model) string {
v := m.colorText(m.echoTransform(string(value[:pos])))
if pos < len(value) {
v += cursorView(m.echoTransform(string(value[pos])), m) // cursor and text under it
v += m.cursorView(m.echoTransform(string(value[pos]))) // cursor and text under it
v += m.colorText(m.echoTransform(string(value[pos+1:]))) // text after cursor
} else {
v += cursorView(" ", m)
v += m.cursorView(" ")
}
// If a max width and background color were set fill the empty spaces with
@@ -459,8 +593,8 @@ func View(m Model) string {
return m.Prompt + v
}
// placeholderView.
func placeholderView(m Model) string {
// placeholderView returns the prompt and placeholder view, if any.
func (m Model) placeholderView() string {
var (
v string
p = m.Placeholder
@@ -468,9 +602,9 @@ func placeholderView(m Model) string {
// Cursor
if m.blink && m.PlaceholderColor != "" {
v += cursorView(m.colorPlaceholder(p[:1]), m)
v += m.cursorView(m.colorPlaceholder(p[:1]))
} else {
v += cursorView(p[:1], m)
v += m.cursorView(p[:1])
}
// The rest of the placeholder text
@@ -480,29 +614,54 @@ func placeholderView(m Model) string {
}
// cursorView styles the cursor.
func cursorView(s string, m Model) string {
func (m Model) cursorView(v string) string {
if m.blink {
if m.TextColor != "" || m.BackgroundColor != "" {
return termenv.String(s).
return termenv.String(v).
Foreground(color(m.TextColor)).
Background(color(m.BackgroundColor)).
String()
}
return s
return v
}
return termenv.String(s).
return termenv.String(v).
Foreground(color(m.CursorColor)).
Background(color(m.BackgroundColor)).
Reverse().
String()
}
// Blink is a command used to time the cursor blinking.
func Blink(model Model) tea.Cmd {
return func() tea.Msg {
time.Sleep(model.BlinkSpeed)
return BlinkMsg{}
// blinkCmd is an internal command used to manage cursor blinking.
func (m Model) blinkCmd() tea.Cmd {
if m.blinkCtx != nil && m.blinkCtx.cancel != nil {
m.blinkCtx.cancel()
}
ctx, cancel := context.WithTimeout(m.blinkCtx.ctx, m.BlinkSpeed)
m.blinkCtx.cancel = cancel
return func() tea.Msg {
defer cancel()
<-ctx.Done()
if ctx.Err() == context.DeadlineExceeded {
return blinkMsg{}
}
return blinkCanceled{}
}
}
// Blink is a command used to initialize cursor blinking.
func Blink() tea.Msg {
return blinkMsg{}
}
// Paste is a command for pasting from the clipboard into the text input.
func Paste() tea.Msg {
str, err := clipboard.ReadAll()
if err != nil {
return pasteErrMsg{err}
}
return pasteMsg(str)
}
func clamp(v, low, high int) int {

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