Skip to content

Commit d06aa4a

Browse files
committed
Ensure that textinputs can only receive their own blink messages
1 parent 10962af commit d06aa4a

File tree

1 file changed

+69
-8
lines changed

1 file changed

+69
-8
lines changed

textinput/textinput.go

Lines changed: 69 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package textinput
33
import (
44
"context"
55
"strings"
6+
"sync"
67
"time"
78
"unicode"
89

@@ -14,11 +15,35 @@ import (
1415

1516
const defaultBlinkSpeed = time.Millisecond * 530
1617

17-
// blinkMsg and blinkCanceled are used to manage cursor blinking.
18-
type blinkMsg struct{}
18+
// Internal ID management for text inputs. Necessary for blink integrity when
19+
// multiple text inputs are involved.
20+
var (
21+
lastID int
22+
idMtx sync.Mutex
23+
)
24+
25+
// Return the next ID we should use on the Model.
26+
func nextID() int {
27+
idMtx.Lock()
28+
defer idMtx.Unlock()
29+
lastID++
30+
return lastID
31+
}
32+
33+
// initialBlinkMsg initializes cursor blinking.
34+
type initialBlinkMsg struct{}
35+
36+
// blinkMsg signals that the cursor should blink. It contains metadata that
37+
// allows us to tell if the blink message is the one we're expecting.
38+
type blinkMsg struct {
39+
id int
40+
tag int
41+
}
42+
43+
// blinkCanceled is sent when a blink operation is canceled.
1944
type blinkCanceled struct{}
2045

21-
// Messages for clipboard events.
46+
// Internal messages for clipboard operations.
2247
type pasteMsg string
2348
type pasteErrMsg struct{ error }
2449

@@ -96,6 +121,12 @@ type Model struct {
96121
// viewport. If 0 or less this setting is ignored.
97122
Width int
98123

124+
// The ID of this Model as it relates to other textinput Models.
125+
id int
126+
127+
// The ID of the blink message we're expecting to receive.
128+
blinkTag int
129+
99130
// Underlying text value.
100131
value []rune
101132

@@ -130,6 +161,7 @@ func NewModel() Model {
130161
CharLimit: 0,
131162
PlaceholderStyle: lipgloss.NewStyle().Foreground(lipgloss.Color("240")),
132163

164+
id: nextID(),
133165
value: nil,
134166
focus: false,
135167
blink: true,
@@ -221,7 +253,7 @@ func (m *Model) SetCursorMode(mode CursorMode) tea.Cmd {
221253
}
222254

223255
// cursorEnd moves the cursor to the end of the input field and returns whether
224-
// or not
256+
// the cursor should blink should reset.
225257
func (m *Model) cursorEnd() bool {
226258
return m.setCursor(len(m.value))
227259
}
@@ -258,7 +290,7 @@ func (m *Model) Reset() bool {
258290
}
259291

260292
// handle a clipboard paste event, if supported. Returns whether or not the
261-
// cursor blink should be reset.
293+
// cursor blink should reset.
262294
func (m *Model) handlePaste(v string) bool {
263295
paste := []rune(v)
264296

@@ -599,7 +631,30 @@ func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
599631
}
600632
}
601633

634+
case initialBlinkMsg:
635+
// We accept all initialBlinkMsgs genrated by the Blink command.
636+
637+
if m.cursorMode != CursorBlink || !m.focus {
638+
return m, nil
639+
}
640+
641+
cmd := m.blinkCmd()
642+
return m, cmd
643+
602644
case blinkMsg:
645+
// We're choosy about whether to accept blinkMsgs so that our cursor
646+
// only exactly when it should.
647+
648+
// Is this model blinkable?
649+
if m.cursorMode != CursorBlink || !m.focus {
650+
return m, nil
651+
}
652+
653+
// Were we expecting this blink message?
654+
if msg.id != m.id || msg.tag != m.blinkTag {
655+
return m, nil
656+
}
657+
603658
var cmd tea.Cmd
604659
if m.cursorMode == CursorBlink {
605660
m.blink = !m.blink
@@ -690,27 +745,33 @@ func (m Model) cursorView(v string) string {
690745
}
691746

692747
// blinkCmd is an internal command used to manage cursor blinking.
693-
func (m Model) blinkCmd() tea.Cmd {
748+
func (m *Model) blinkCmd() tea.Cmd {
749+
if m.cursorMode != CursorBlink {
750+
return nil
751+
}
752+
694753
if m.blinkCtx != nil && m.blinkCtx.cancel != nil {
695754
m.blinkCtx.cancel()
696755
}
697756

698757
ctx, cancel := context.WithTimeout(m.blinkCtx.ctx, m.BlinkSpeed)
699758
m.blinkCtx.cancel = cancel
700759

760+
m.blinkTag++
761+
701762
return func() tea.Msg {
702763
defer cancel()
703764
<-ctx.Done()
704765
if ctx.Err() == context.DeadlineExceeded {
705-
return blinkMsg{}
766+
return blinkMsg{id: m.id, tag: m.blinkTag}
706767
}
707768
return blinkCanceled{}
708769
}
709770
}
710771

711772
// Blink is a command used to initialize cursor blinking.
712773
func Blink() tea.Msg {
713-
return blinkMsg{}
774+
return initialBlinkMsg{}
714775
}
715776

716777
// Paste is a command for pasting from the clipboard into the text input.

0 commit comments

Comments
 (0)