8 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
3 changed files with 219 additions and 136 deletions

2
go.mod
View File

@@ -4,7 +4,7 @@ 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.1 github.com/charmbracelet/bubbletea v0.12.2-0.20201101135743-116a0cfb8f37
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/crypto v0.0.0-20201016220609-9e8e0b390897 // indirect 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 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.1 h1:t21pkG2IDBRduPbt2J64Dx5yt8yIidAkXwhhrc11SzY= github.com/charmbracelet/bubbletea v0.12.2-0.20201101135743-116a0cfb8f37 h1:BQLGyhKVE19a9XdNYcsnYlO9XHPlOVHIWM7+mmS014k=
github.com/charmbracelet/bubbletea v0.12.1/go.mod h1:3gZkYELUOiEUOp0bTInkxguucy/xRbGSOcbMs1geLxg= 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 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=

View File

@@ -1,6 +1,7 @@
package textinput package textinput
import ( import (
"context"
"strings" "strings"
"time" "time"
"unicode" "unicode"
@@ -13,6 +14,20 @@ import (
const defaultBlinkSpeed = time.Millisecond * 530 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 ( const (
// EchoNormal displays text as is. This is the default behavior. // EchoNormal displays text as is. This is the default behavior.
EchoNormal EchoMode = iota EchoNormal EchoMode = iota
@@ -28,29 +43,33 @@ const (
// EchoOnEdit // EchoOnEdit
) )
// EchoMode sets the input behavior of the text input field. // blinkCtx manages cursor blinking.
type EchoMode int type blinkCtx struct {
ctx context.Context
cancel context.CancelFunc
}
var ( type cursorMode int
// color is a helper for returning colors.
color func(s string) termenv.Color = termenv.ColorProfile().Color const (
cursorBlink = iota
cursorStatic
cursorHide
) )
// Model is the Bubble 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
// General settings
Prompt string Prompt string
Placeholder string Placeholder string
Cursor string Cursor string
BlinkSpeed time.Duration BlinkSpeed time.Duration
TextColor string TextColor string
BackgroundColor string BackgroundColor string
PlaceholderColor string PlaceholderColor string
CursorColor string CursorColor string
EchoMode EchoMode EchoMode EchoMode
EchoCharacter rune EchoCharacter rune
@@ -80,6 +99,36 @@ type Model struct {
// overflowing. // overflowing.
offset int offset int
offsetRight 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. // SetValue sets the value of the text input.
@@ -103,20 +152,28 @@ func (m Model) Value() string {
// SetCursor start moves the cursor to the given position. If the position is // 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. // 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.pos = clamp(pos, 0, len(m.value))
m.blink = false
m.handleOverflow() 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. // CursorStart moves the cursor to the start of the field. Returns whether or
func (m *Model) CursorStart() { // not the curosr blink should be reset.
m.SetCursor(0) func (m *Model) CursorStart() bool {
return m.SetCursor(0)
} }
// CursorEnd moves the cursor to the end of the field. // CursorEnd moves the cursor to the end of the field. Returns whether or not
func (m *Model) CursorEnd() { // the cursor blink should be reset.
m.SetCursor(len(m.value)) func (m *Model) CursorEnd() bool {
return m.SetCursor(len(m.value))
} }
// Focused returns the focus state on the model. // Focused returns the focus state on the model.
@@ -127,7 +184,7 @@ func (m Model) Focused() bool {
// Focus sets the focus state on the model. // Focus sets the focus state on the model.
func (m *Model) Focus() { func (m *Model) Focus() {
m.focus = true 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. // Blur removes the focus state on the model.
@@ -136,19 +193,17 @@ func (m *Model) Blur() {
m.blink = true m.blink = true
} }
// Reset sets the input to its default state with no input. // Reset sets the input to its default state with no input. Returns whether
func (m *Model) Reset() { // or not the cursor blink should reset.
func (m *Model) Reset() bool {
m.value = nil m.value = nil
m.SetCursor(0) return m.SetCursor(0)
} }
// Paste pastes the contents of the clipboard into the text area (if supported). // handle a clipboard paste event, if supported. Returns whether or not the
func (m *Model) Paste() { // cursor blink should be reset.
pasteString, err := clipboard.ReadAll() func (m *Model) handlePaste(v string) (blink bool) {
if err != nil { paste := []rune(v)
m.Err = err
}
paste := []rune(pasteString)
var availSpace int var availSpace int
if m.CharLimit > 0 { if m.CharLimit > 0 {
@@ -187,8 +242,8 @@ func (m *Model) Paste() {
// 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 // Reset blink state if necessary and run overflow checks
m.SetCursor(m.pos) return 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
@@ -256,26 +311,27 @@ func (m *Model) colorPlaceholder(s string) string {
String() String()
} }
// deleteWordLeft deletes the word left to the cursor. // deleteWordLeft deletes the word left to the cursor. Returns whether or not
func (m *Model) deleteWordLeft() { // the cursor blink should be reset.
func (m *Model) deleteWordLeft() (blink bool) {
if m.pos == 0 || len(m.value) == 0 { if m.pos == 0 || len(m.value) == 0 {
return return
} }
i := m.pos i := m.pos
m.SetCursor(m.pos - 1) blink = m.SetCursor(m.pos - 1)
for unicode.IsSpace(m.value[m.pos]) { for unicode.IsSpace(m.value[m.pos]) {
// ignore series of whitespace before cursor // ignore series of whitespace before cursor
m.SetCursor(m.pos - 1) blink = m.SetCursor(m.pos - 1)
} }
for m.pos > 0 { for m.pos > 0 {
if !unicode.IsSpace(m.value[m.pos]) { if !unicode.IsSpace(m.value[m.pos]) {
m.SetCursor(m.pos - 1) blink = m.SetCursor(m.pos - 1)
} else { } else {
if m.pos > 0 { if m.pos > 0 {
// keep the previous space // keep the previous space
m.SetCursor(m.pos + 1) blink = m.SetCursor(m.pos + 1)
} }
break break
} }
@@ -286,24 +342,27 @@ func (m *Model) deleteWordLeft() {
} else { } else {
m.value = append(m.value[:m.pos], m.value[i:]...) m.value = append(m.value[:m.pos], m.value[i:]...)
} }
return
} }
// deleteWordRight deletes the word right to the cursor. // deleteWordRight deletes the word right to the cursor. Returns whether or not
func (m *Model) deleteWordRight() { // the cursor blink should be reset.
func (m *Model) deleteWordRight() (blink bool) {
if m.pos >= len(m.value) || len(m.value) == 0 { if m.pos >= len(m.value) || len(m.value) == 0 {
return return
} }
i := m.pos i := m.pos
m.SetCursor(m.pos + 1) blink = m.SetCursor(m.pos + 1)
for unicode.IsSpace(m.value[m.pos]) { for unicode.IsSpace(m.value[m.pos]) {
// ignore series of whitespace after cursor // ignore series of whitespace after cursor
m.SetCursor(m.pos + 1) blink = m.SetCursor(m.pos + 1)
} }
for m.pos < len(m.value) { for m.pos < len(m.value) {
if !unicode.IsSpace(m.value[m.pos]) { if !unicode.IsSpace(m.value[m.pos]) {
m.SetCursor(m.pos + 1) blink = m.SetCursor(m.pos + 1)
} else { } else {
break break
} }
@@ -314,10 +373,14 @@ func (m *Model) deleteWordRight() {
} else { } else {
m.value = append(m.value[:i], m.value[m.pos:]...) m.value = append(m.value[:i], m.value[m.pos:]...)
} }
m.SetCursor(i) blink = m.SetCursor(i)
return
} }
func (m *Model) wordLeft() { // 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 { if m.pos == 0 || len(m.value) == 0 {
return return
} }
@@ -326,7 +389,7 @@ func (m *Model) wordLeft() {
for i >= 0 { for i >= 0 {
if unicode.IsSpace(m.value[i]) { if unicode.IsSpace(m.value[i]) {
m.SetCursor(m.pos - 1) blink = m.SetCursor(m.pos - 1)
i-- i--
} else { } else {
break break
@@ -335,15 +398,19 @@ func (m *Model) wordLeft() {
for i >= 0 { for i >= 0 {
if !unicode.IsSpace(m.value[i]) { if !unicode.IsSpace(m.value[i]) {
m.SetCursor(m.pos - 1) blink = m.SetCursor(m.pos - 1)
i-- i--
} else { } else {
break 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 { if m.pos >= len(m.value) || len(m.value) == 0 {
return return
} }
@@ -352,7 +419,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.SetCursor(m.pos + 1) blink = m.SetCursor(m.pos + 1)
i++ i++
} else { } else {
break break
@@ -361,12 +428,14 @@ 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.SetCursor(m.pos + 1) blink = m.SetCursor(m.pos + 1)
i++ i++
} else { } else {
break break
} }
} }
return
} }
func (m Model) echoTransform(v string) string { func (m Model) echoTransform(v string) string {
@@ -381,130 +450,119 @@ func (m Model) echoTransform(v string) string {
} }
} }
// BlinkMsg is sent when the cursor should alternate it's blinking state. // Update is the Bubble Tea update loop.
type BlinkMsg struct{} func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
// 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) {
if !m.focus { if !m.focus {
m.blink = true m.blink = true
return m, nil return m, nil
} }
var resetBlink bool
switch msg := msg.(type) { switch msg := msg.(type) {
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 msg.Alt { if msg.Alt {
m.deleteWordLeft() resetBlink = m.deleteWordLeft()
} else { } else {
if len(m.value) > 0 { if len(m.value) > 0 {
m.value = append(m.value[:max(0, 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 { if m.pos > 0 {
m.SetCursor(m.pos - 1) resetBlink = m.SetCursor(m.pos - 1)
} }
} }
} }
case tea.KeyLeft: case tea.KeyLeft, tea.KeyCtrlB:
if msg.Alt { // alt+left arrow, back one word if msg.Alt { // alt+left arrow, back one word
m.wordLeft() resetBlink = m.wordLeft()
break break
} }
if m.pos > 0 { if m.pos > 0 { // left arrow, ^F, back one character
m.SetCursor(m.pos - 1) resetBlink = m.SetCursor(m.pos - 1)
} }
case tea.KeyRight: case tea.KeyRight, tea.KeyCtrlF:
if msg.Alt { // alt+right arrow, forward one word if msg.Alt { // alt+right arrow, forward one word
m.wordRight() resetBlink = m.wordRight()
break break
} }
if m.pos < len(m.value) { if m.pos < len(m.value) { // right arrow, ^F, forward one word
m.SetCursor(m.pos + 1) resetBlink = m.SetCursor(m.pos + 1)
} }
case tea.KeyCtrlW: // ^W, delete word left of cursor case tea.KeyCtrlW: // ^W, delete word left of cursor
m.deleteWordLeft() resetBlink = m.deleteWordLeft()
case tea.KeyCtrlF: // ^F, forward one character
fallthrough
case tea.KeyCtrlB: // ^B, back one charcter
fallthrough
case tea.KeyHome, tea.KeyCtrlA: // ^A, go to beginning case tea.KeyHome, tea.KeyCtrlA: // ^A, go to beginning
m.CursorStart() resetBlink = m.CursorStart()
case tea.KeyDelete, 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, tea.KeyEnd: // ^E, go to end case tea.KeyCtrlE, tea.KeyEnd: // ^E, go to end
m.CursorEnd() resetBlink = 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.SetCursor(len(m.value)) resetBlink = 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.SetCursor(0) resetBlink = m.SetCursor(0)
m.offset = 0 m.offset = 0
case tea.KeyCtrlV: // ^V paste case tea.KeyCtrlV: // ^V paste
m.Paste() return m, Paste
case tea.KeyRune: // input a regular character case tea.KeyRunes: // input regular characters
if msg.Alt { if msg.Alt && len(msg.Runes) == 1 {
if msg.Rune == 'd' { // alt+d, delete word right of cursor if msg.Runes[0] == 'd' { // alt+d, delete word right of cursor
m.deleteWordRight() resetBlink = m.deleteWordRight()
break break
} }
if msg.Rune == 'b' { // alt+b, back one word if msg.Runes[0] == 'b' { // alt+b, back one word
m.wordLeft() resetBlink = m.wordLeft()
break break
} }
if msg.Rune == 'f' { // alt+f, forward one word if msg.Runes[0] == 'f' { // alt+f, forward one word
m.wordRight() resetBlink = m.wordRight()
break break
} }
} }
// 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(msg.Runes, m.value[m.pos:]...)...)
m.SetCursor(m.pos + 1) resetBlink = m.SetCursor(m.pos + len(msg.Runes))
} }
} }
case BlinkMsg: case blinkMsg:
var cmd tea.Cmd
if m.cursorMode == cursorBlink {
m.blink = !m.blink 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() m.handleOverflow()
return m, cmd
return m, nil
} }
// View renders the textinput in its current state. // View renders the textinput in its current state.
func View(m Model) string { func (m Model) View() string {
// Placeholder text // Placeholder text
if len(m.value) == 0 && m.Placeholder != "" { if len(m.value) == 0 && m.Placeholder != "" {
return placeholderView(m) return m.placeholderView()
} }
value := m.value[m.offset:m.offsetRight] value := m.value[m.offset:m.offsetRight]
@@ -512,10 +570,10 @@ func View(m Model) string {
v := m.colorText(m.echoTransform(string(value[:pos]))) v := m.colorText(m.echoTransform(string(value[:pos])))
if pos < len(value) { 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 v += m.colorText(m.echoTransform(string(value[pos+1:]))) // text after cursor
} else { } else {
v += cursorView(" ", m) v += m.cursorView(" ")
} }
// If a max width and background color were set fill the empty spaces with // If a max width and background color were set fill the empty spaces with
@@ -535,8 +593,8 @@ func View(m Model) string {
return m.Prompt + v return m.Prompt + v
} }
// placeholderView. // placeholderView returns the prompt and placeholder view, if any.
func placeholderView(m Model) string { func (m Model) placeholderView() string {
var ( var (
v string v string
p = m.Placeholder p = m.Placeholder
@@ -544,9 +602,9 @@ func placeholderView(m Model) string {
// Cursor // Cursor
if m.blink && m.PlaceholderColor != "" { if m.blink && m.PlaceholderColor != "" {
v += cursorView(m.colorPlaceholder(p[:1]), m) v += m.cursorView(m.colorPlaceholder(p[:1]))
} else { } else {
v += cursorView(p[:1], m) v += m.cursorView(p[:1])
} }
// The rest of the placeholder text // The rest of the placeholder text
@@ -556,29 +614,54 @@ func placeholderView(m Model) string {
} }
// cursorView styles the cursor. // cursorView styles the cursor.
func cursorView(s string, m Model) string { func (m Model) cursorView(v string) string {
if m.blink { if m.blink {
if m.TextColor != "" || m.BackgroundColor != "" { if m.TextColor != "" || m.BackgroundColor != "" {
return termenv.String(s). return termenv.String(v).
Foreground(color(m.TextColor)). Foreground(color(m.TextColor)).
Background(color(m.BackgroundColor)). Background(color(m.BackgroundColor)).
String() String()
} }
return s return v
} }
return termenv.String(s). return termenv.String(v).
Foreground(color(m.CursorColor)). Foreground(color(m.CursorColor)).
Background(color(m.BackgroundColor)). Background(color(m.BackgroundColor)).
Reverse(). Reverse().
String() String()
} }
// Blink is a command used to time the cursor blinking. // blinkCmd is an internal command used to manage cursor blinking.
func Blink(model Model) tea.Cmd { func (m Model) blinkCmd() tea.Cmd {
return func() tea.Msg { if m.blinkCtx != nil && m.blinkCtx.cancel != nil {
time.Sleep(model.BlinkSpeed) m.blinkCtx.cancel()
return BlinkMsg{}
} }
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 { func clamp(v, low, high int) int {