10 Commits

Author SHA1 Message Date
Christian Rocha
d06aa4aa48 Ensure that textinputs can only receive their own blink messages 2021-06-02 10:49:43 -04:00
Christian Rocha
10962af2f2 Textinput's focus method now returns a command to initiate cursor blink 2021-06-02 10:49:43 -04:00
Christian Rocha
5082ae6f31 Add API for blink, static and hidden cursor modes to textinput
Closes #53.
2021-06-02 10:49:43 -04:00
Christian Rocha
4365990396 Fix typo in a spinner comment 2021-06-02 10:49:43 -04:00
Christian Rocha
1405b09ec8 Update footer in README for consistency with newer Charm repos 2021-06-02 10:49:43 -04:00
Christian Muehlhaeuser
f53c742d6c Fix goreportcard badge 2021-05-18 20:56:27 +02:00
Christian Rocha
9449cc7e41 Don't allow word-based deletion when input is masked in textinput
alt+d and ctrl+w will now delete all the way to the beginning and end,
respectively, if EchoMode is not EchoNormal.
2021-04-26 12:03:57 -04:00
Christian Rocha
58a177394e Don't allow word-to-word movement when input is masked in textinput
When EchoMode is not set to EchoNormal, alt+left/alt+b and
alt+right/alt+b jumps to the beginning and the end, respectively, so as
not to reveal word breaks in the masked text.
2021-04-26 12:03:57 -04:00
Christian Rocha
f016c31d83 Add method for returning cursor position in textinput
Previously, Cursor was a member on the model that did absolutely
nothing.
2021-04-26 12:03:57 -04:00
Christian Rocha
74436326b9 Public cursor movement functions no longer return values in textinput
Previously, textinput.SetCursor, textinput.CursotStart, and
textinput.CursorEnd returned bools used interally for managing cursor
blinking. Those methods have been replaced with private counterparts.
2021-04-26 12:03:57 -04:00
3 changed files with 217 additions and 68 deletions

View File

@@ -8,7 +8,7 @@ Bubbles
[![Latest Release](https://img.shields.io/github/release/charmbracelet/bubbles.svg)](https://github.com/charmbracelet/bubbles/releases) [![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) [![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](https://goreportcard.com/badge/charmbracelet/bubbles)](https://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.
@@ -91,4 +91,4 @@ Part of [Charm](https://charm.sh).
<a href="https://charm.sh/"><img alt="The Charm logo" src="https://stuff.charm.sh/charm-badge-unrounded.jpg" width="400"></a> <a href="https://charm.sh/"><img alt="The Charm logo" src="https://stuff.charm.sh/charm-badge-unrounded.jpg" width="400"></a>
Charm热爱开源! / Charm loves open source! Charm热爱开源 Charm loves open source

View File

@@ -188,10 +188,9 @@ type TickMsg struct {
func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) { func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
switch msg := msg.(type) { switch msg := msg.(type) {
case TickMsg: case TickMsg:
// If a tag is set, and it's not the one we expect, reject the message. // If a tag is set, and it's not the one we expect, reject the message.
// This prevents the spinner from receiving too many messages and // This prevents the spinner from receiving too many messages and
// this spinning too fast. // thus spinning too fast.
if msg.tag > 0 && msg.tag != m.tag { if msg.tag > 0 && msg.tag != m.tag {
return m, nil return m, nil
} }

View File

@@ -3,6 +3,7 @@ package textinput
import ( import (
"context" "context"
"strings" "strings"
"sync"
"time" "time"
"unicode" "unicode"
@@ -14,11 +15,35 @@ import (
const defaultBlinkSpeed = time.Millisecond * 530 const defaultBlinkSpeed = time.Millisecond * 530
// blinkMsg and blinkCanceled are used to manage cursor blinking. // Internal ID management for text inputs. Necessary for blink integrity when
type blinkMsg struct{} // multiple text inputs are involved.
var (
lastID int
idMtx sync.Mutex
)
// Return the next ID we should use on the Model.
func nextID() int {
idMtx.Lock()
defer idMtx.Unlock()
lastID++
return lastID
}
// initialBlinkMsg initializes cursor blinking.
type initialBlinkMsg struct{}
// blinkMsg signals that the cursor should blink. It contains metadata that
// allows us to tell if the blink message is the one we're expecting.
type blinkMsg struct {
id int
tag int
}
// blinkCanceled is sent when a blink operation is canceled.
type blinkCanceled struct{} type blinkCanceled struct{}
// Messages for clipboard events. // Internal messages for clipboard operations.
type pasteMsg string type pasteMsg string
type pasteErrMsg struct{ error } type pasteErrMsg struct{ error }
@@ -46,14 +71,26 @@ type blinkCtx struct {
cancel context.CancelFunc cancel context.CancelFunc
} }
type cursorMode int // CursorMode describes the behavior of the cursor.
type CursorMode int
// Available cursor modes.
const ( const (
cursorBlink = iota CursorBlink CursorMode = iota
cursorStatic CursorStatic
cursorHide CursorHide
) )
// String returns a the cursor mode in a human-readable format. This method is
// provisional and for informational purposes only.
func (c CursorMode) String() string {
return [...]string{
"blink",
"static",
"hidden",
}[c]
}
// 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
@@ -61,7 +98,6 @@ type Model struct {
// General settings. // General settings.
Prompt string Prompt string
Placeholder string Placeholder string
Cursor string
BlinkSpeed time.Duration BlinkSpeed time.Duration
EchoMode EchoMode EchoMode EchoMode
EchoCharacter rune EchoCharacter rune
@@ -85,11 +121,17 @@ 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
// The ID of this Model as it relates to other textinput Models.
id int
// The ID of the blink message we're expecting to receive.
blinkTag 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, ignore keyboard input and hide the cursor.
focus bool focus bool
// Cursor blink state. // Cursor blink state.
@@ -107,7 +149,7 @@ type Model struct {
blinkCtx *blinkCtx blinkCtx *blinkCtx
// cursorMode determines the behavior of the cursor // cursorMode determines the behavior of the cursor
cursorMode cursorMode cursorMode CursorMode
} }
// NewModel creates a new model with default settings. // NewModel creates a new model with default settings.
@@ -119,11 +161,12 @@ func NewModel() Model {
CharLimit: 0, CharLimit: 0,
PlaceholderStyle: lipgloss.NewStyle().Foreground(lipgloss.Color("240")), PlaceholderStyle: lipgloss.NewStyle().Foreground(lipgloss.Color("240")),
id: nextID(),
value: nil, value: nil,
focus: false, focus: false,
blink: true, blink: true,
pos: 0, pos: 0,
cursorMode: cursorBlink, cursorMode: CursorBlink,
blinkCtx: &blinkCtx{ blinkCtx: &blinkCtx{
ctx: context.Background(), ctx: context.Background(),
@@ -140,7 +183,7 @@ func (m *Model) SetValue(s string) {
m.value = runes m.value = runes
} }
if m.pos == 0 || m.pos > len(m.value) { if m.pos == 0 || m.pos > len(m.value) {
m.SetCursor(len(m.value)) m.setCursor(len(m.value))
} }
m.handleOverflow() m.handleOverflow()
} }
@@ -150,30 +193,69 @@ func (m Model) Value() string {
return string(m.value) return string(m.value)
} }
// SetCursor start moves the cursor to the given position. If the position is // Cursor returns the cursor position.
func (m Model) Cursor() int {
return m.pos
}
// SetCursor 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.
// Returns whether or nor the cursor timer should be reset. func (m *Model) SetCursor(pos int) {
func (m *Model) SetCursor(pos int) bool { m.setCursor(pos)
}
// setCursor moves the cursor to the given position and returns whether or not
// the cursor blink should be reset. If the position is out of bounds the
// cursor will be moved to the start or end accordingly.
func (m *Model) setCursor(pos int) bool {
m.pos = clamp(pos, 0, len(m.value)) m.pos = clamp(pos, 0, len(m.value))
m.handleOverflow() m.handleOverflow()
// Show the cursor unless it's been explicitly hidden // Show the cursor unless it's been explicitly hidden
m.blink = m.cursorMode == cursorHide m.blink = m.cursorMode == CursorHide
// Reset cursor blink if necessary // Reset cursor blink if necessary
return m.cursorMode == cursorBlink return m.cursorMode == CursorBlink
} }
// CursorStart moves the cursor to the start of the field. Returns whether or // CursorStart moves the cursor to the start of the input field.
// not the curosr blink should be reset. func (m *Model) CursorStart() {
func (m *Model) CursorStart() bool { m.cursorStart()
return m.SetCursor(0)
} }
// CursorEnd moves the cursor to the end of the field. Returns whether or not // cursorStart moves the cursor to the start of the input field and returns
// the cursor blink should be reset. // whether or not the curosr blink should be reset.
func (m *Model) CursorEnd() bool { func (m *Model) cursorStart() bool {
return m.SetCursor(len(m.value)) return m.setCursor(0)
}
// CursorEnd moves the cursor to the end of the input field
func (m *Model) CursorEnd() {
m.cursorEnd()
}
// CursorMode returns the model's cursor mode. For available cursor modes, see
// type CursorMode.
func (m Model) CursorMode() CursorMode {
return m.cursorMode
}
// CursorMode sets the model's cursor mode. This method returns a command.
//
// For available cursor modes, see type CursorMode.
func (m *Model) SetCursorMode(mode CursorMode) tea.Cmd {
m.cursorMode = mode
m.blink = m.cursorMode == CursorHide || !m.focus
if mode == CursorBlink {
return Blink
}
return nil
}
// cursorEnd moves the cursor to the end of the input field and returns whether
// the cursor should blink should reset.
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.
@@ -181,13 +263,20 @@ func (m Model) Focused() bool {
return m.focus return m.focus
} }
// Focus sets the focus state on the model. // Focus sets the focus state on the model. When the model is in focus it can
func (m *Model) Focus() { // receive keyboard input and the cursor will be hidden.
func (m *Model) Focus() tea.Cmd {
m.focus = true m.focus = true
m.blink = m.cursorMode == cursorHide // show the cursor unless we've explicitly hidden it m.blink = m.cursorMode == CursorHide // show the cursor unless we've explicitly hidden it
if m.cursorMode == CursorBlink && m.focus {
return m.blinkCmd()
}
return nil
} }
// Blur removes the focus state on the model. // Blur removes the focus state on the model. When the model is blurred it can
// not receive keyboard input and the cursor will be hidden.
func (m *Model) Blur() { func (m *Model) Blur() {
m.focus = false m.focus = false
m.blink = true m.blink = true
@@ -197,11 +286,11 @@ func (m *Model) Blur() {
// or not the cursor blink should reset. // or not the cursor blink should reset.
func (m *Model) Reset() bool { func (m *Model) Reset() bool {
m.value = nil m.value = nil
return m.SetCursor(0) return m.setCursor(0)
} }
// handle a clipboard paste event, if supported. Returns whether or not the // handle a clipboard paste event, if supported. Returns whether or not the
// cursor blink should be reset. // cursor blink should reset.
func (m *Model) handlePaste(v string) bool { func (m *Model) handlePaste(v string) bool {
paste := []rune(v) paste := []rune(v)
@@ -243,7 +332,7 @@ func (m *Model) handlePaste(v string) bool {
m.value = append(head, tail...) m.value = append(head, tail...)
// Reset blink state if necessary and run overflow checks // Reset blink state if necessary and run overflow checks
return 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
@@ -291,6 +380,22 @@ func (m *Model) handleOverflow() {
} }
} }
// deleteBeforeCursor deletes all text before the cursor. Returns whether or
// not the cursor blink should be reset.
func (m *Model) deleteBeforeCursor() bool {
m.value = m.value[m.pos:]
m.offset = 0
return m.setCursor(0)
}
// deleteAfterCursor deletes all text after the cursor. Returns whether or not
// the cursor blink should be reset. If input is masked delete everything after
// the cursor so as not to reveal word breaks in the masked input.
func (m *Model) deleteAfterCursor() bool {
m.value = m.value[:m.pos]
return m.setCursor(len(m.value))
}
// deleteWordLeft deletes the word left to the cursor. Returns whether or not // deleteWordLeft deletes the word left to the cursor. Returns whether or not
// the cursor blink should be reset. // the cursor blink should be reset.
func (m *Model) deleteWordLeft() bool { func (m *Model) deleteWordLeft() bool {
@@ -298,20 +403,24 @@ func (m *Model) deleteWordLeft() bool {
return false return false
} }
if m.EchoMode != EchoNormal {
return m.deleteBeforeCursor()
}
i := m.pos i := m.pos
blink := 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
blink = 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]) {
blink = 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
blink = m.SetCursor(m.pos + 1) blink = m.setCursor(m.pos + 1)
} }
break break
} }
@@ -327,22 +436,27 @@ func (m *Model) deleteWordLeft() bool {
} }
// deleteWordRight deletes the word right to the cursor. Returns whether or not // deleteWordRight deletes the word right to the cursor. Returns whether or not
// the cursor blink should be reset. // the cursor blink should be reset. If input is masked delete everything after
// the cursor so as not to reveal word breaks in the masked input.
func (m *Model) deleteWordRight() bool { func (m *Model) deleteWordRight() bool {
if m.pos >= len(m.value) || len(m.value) == 0 { if m.pos >= len(m.value) || len(m.value) == 0 {
return false return false
} }
if m.EchoMode != EchoNormal {
return m.deleteAfterCursor()
}
i := m.pos i := m.pos
m.SetCursor(m.pos + 1) 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) 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) m.setCursor(m.pos + 1)
} else { } else {
break break
} }
@@ -354,21 +468,26 @@ func (m *Model) deleteWordRight() bool {
m.value = append(m.value[:i], m.value[m.pos:]...) m.value = append(m.value[:i], m.value[m.pos:]...)
} }
return m.SetCursor(i) return m.setCursor(i)
} }
// wordLeft moves the cursor one word to the left. Returns whether or not the // wordLeft moves the cursor one word to the left. Returns whether or not the
// cursor blink should be reset. // cursor blink should be reset. If input is masked, move input to the start
// so as not to reveal word breaks in the masked input.
func (m *Model) wordLeft() bool { func (m *Model) wordLeft() bool {
if m.pos == 0 || len(m.value) == 0 { if m.pos == 0 || len(m.value) == 0 {
return false return false
} }
if m.EchoMode != EchoNormal {
return m.cursorStart()
}
blink := false blink := false
i := m.pos - 1 i := m.pos - 1
for i >= 0 { for i >= 0 {
if unicode.IsSpace(m.value[i]) { if unicode.IsSpace(m.value[i]) {
blink = m.SetCursor(m.pos - 1) blink = m.setCursor(m.pos - 1)
i-- i--
} else { } else {
break break
@@ -377,7 +496,7 @@ func (m *Model) wordLeft() bool {
for i >= 0 { for i >= 0 {
if !unicode.IsSpace(m.value[i]) { if !unicode.IsSpace(m.value[i]) {
blink = m.SetCursor(m.pos - 1) blink = m.setCursor(m.pos - 1)
i-- i--
} else { } else {
break break
@@ -388,17 +507,22 @@ func (m *Model) wordLeft() bool {
} }
// wordRight moves the cursor one word to the right. Returns whether or not the // wordRight moves the cursor one word to the right. Returns whether or not the
// cursor blink should be reset. // cursor blink should be reset. If the input is masked, move input to the end
// so as not to reveal word breaks in the masked input.
func (m *Model) wordRight() bool { func (m *Model) wordRight() bool {
if m.pos >= len(m.value) || len(m.value) == 0 { if m.pos >= len(m.value) || len(m.value) == 0 {
return false return false
} }
if m.EchoMode != EchoNormal {
return m.cursorEnd()
}
blink := false blink := false
i := m.pos i := m.pos
for i < len(m.value) { for i < len(m.value) {
if unicode.IsSpace(m.value[i]) { if unicode.IsSpace(m.value[i]) {
blink = m.SetCursor(m.pos + 1) blink = m.setCursor(m.pos + 1)
i++ i++
} else { } else {
break break
@@ -407,7 +531,7 @@ func (m *Model) wordRight() bool {
for i < len(m.value) { for i < len(m.value) {
if !unicode.IsSpace(m.value[i]) { if !unicode.IsSpace(m.value[i]) {
blink = m.SetCursor(m.pos + 1) blink = m.setCursor(m.pos + 1)
i++ i++
} else { } else {
break break
@@ -448,7 +572,7 @@ func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
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 {
resetBlink = m.SetCursor(m.pos - 1) resetBlink = m.setCursor(m.pos - 1)
} }
} }
} }
@@ -458,33 +582,30 @@ func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
break break
} }
if m.pos > 0 { // left arrow, ^F, back one character if m.pos > 0 { // left arrow, ^F, back one character
resetBlink = m.SetCursor(m.pos - 1) resetBlink = m.setCursor(m.pos - 1)
} }
case tea.KeyRight, tea.KeyCtrlF: case tea.KeyRight, tea.KeyCtrlF:
if msg.Alt { // alt+right arrow, forward one word if msg.Alt { // alt+right arrow, forward one word
resetBlink = m.wordRight() resetBlink = m.wordRight()
break break
} }
if m.pos < len(m.value) { // right arrow, ^F, forward one word if m.pos < len(m.value) { // right arrow, ^F, forward one character
resetBlink = 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
resetBlink = m.deleteWordLeft() resetBlink = m.deleteWordLeft()
case tea.KeyHome, tea.KeyCtrlA: // ^A, go to beginning case tea.KeyHome, tea.KeyCtrlA: // ^A, go to beginning
resetBlink = 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
resetBlink = 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] resetBlink = m.deleteAfterCursor()
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:] resetBlink = m.deleteBeforeCursor()
resetBlink = m.SetCursor(0)
m.offset = 0
case tea.KeyCtrlV: // ^V paste case tea.KeyCtrlV: // ^V paste
return m, Paste return m, Paste
case tea.KeyRunes: // input regular characters case tea.KeyRunes: // input regular characters
@@ -506,13 +627,36 @@ func (m Model) Update(msg tea.Msg) (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(msg.Runes, m.value[m.pos:]...)...) m.value = append(m.value[:m.pos], append(msg.Runes, m.value[m.pos:]...)...)
resetBlink = m.SetCursor(m.pos + len(msg.Runes)) resetBlink = m.setCursor(m.pos + len(msg.Runes))
} }
} }
case initialBlinkMsg:
// We accept all initialBlinkMsgs genrated by the Blink command.
if m.cursorMode != CursorBlink || !m.focus {
return m, nil
}
cmd := m.blinkCmd()
return m, cmd
case blinkMsg: case blinkMsg:
// We're choosy about whether to accept blinkMsgs so that our cursor
// only exactly when it should.
// Is this model blinkable?
if m.cursorMode != CursorBlink || !m.focus {
return m, nil
}
// Were we expecting this blink message?
if msg.id != m.id || msg.tag != m.blinkTag {
return m, nil
}
var cmd tea.Cmd var cmd tea.Cmd
if m.cursorMode == cursorBlink { if m.cursorMode == CursorBlink {
m.blink = !m.blink m.blink = !m.blink
cmd = m.blinkCmd() cmd = m.blinkCmd()
} }
@@ -601,7 +745,11 @@ func (m Model) cursorView(v string) string {
} }
// blinkCmd is an internal command used to manage cursor blinking. // blinkCmd is an internal command used to manage cursor blinking.
func (m Model) blinkCmd() tea.Cmd { func (m *Model) blinkCmd() tea.Cmd {
if m.cursorMode != CursorBlink {
return nil
}
if m.blinkCtx != nil && m.blinkCtx.cancel != nil { if m.blinkCtx != nil && m.blinkCtx.cancel != nil {
m.blinkCtx.cancel() m.blinkCtx.cancel()
} }
@@ -609,11 +757,13 @@ func (m Model) blinkCmd() tea.Cmd {
ctx, cancel := context.WithTimeout(m.blinkCtx.ctx, m.BlinkSpeed) ctx, cancel := context.WithTimeout(m.blinkCtx.ctx, m.BlinkSpeed)
m.blinkCtx.cancel = cancel m.blinkCtx.cancel = cancel
m.blinkTag++
return func() tea.Msg { return func() tea.Msg {
defer cancel() defer cancel()
<-ctx.Done() <-ctx.Done()
if ctx.Err() == context.DeadlineExceeded { if ctx.Err() == context.DeadlineExceeded {
return blinkMsg{} return blinkMsg{id: m.id, tag: m.blinkTag}
} }
return blinkCanceled{} return blinkCanceled{}
} }
@@ -621,7 +771,7 @@ func (m Model) blinkCmd() tea.Cmd {
// Blink is a command used to initialize cursor blinking. // Blink is a command used to initialize cursor blinking.
func Blink() tea.Msg { func Blink() tea.Msg {
return blinkMsg{} return initialBlinkMsg{}
} }
// Paste is a command for pasting from the clipboard into the text input. // Paste is a command for pasting from the clipboard into the text input.