1// Package cursor provides a virtual cursor to support the textinput and
2// textarea elements.
3package cursor
4
5import (
6 "context"
7 "sync/atomic"
8 "time"
9
10 tea "github.com/charmbracelet/bubbletea/v2"
11 "github.com/charmbracelet/lipgloss/v2"
12)
13
14const defaultBlinkSpeed = time.Millisecond * 530
15
16// Internal ID management. Used during animating to ensure that frame messages
17// are received only by spinner components that sent them.
18var lastID int64
19
20func nextID() int {
21 return int(atomic.AddInt64(&lastID, 1))
22}
23
24// initialBlinkMsg initializes cursor blinking.
25type initialBlinkMsg struct{}
26
27// BlinkMsg signals that the cursor should blink. It contains metadata that
28// allows us to tell if the blink message is the one we're expecting.
29type BlinkMsg struct {
30 id int
31 tag int
32}
33
34// blinkCanceled is sent when a blink operation is canceled.
35type blinkCanceled struct{}
36
37// blinkCtx manages cursor blinking.
38type blinkCtx struct {
39 ctx context.Context
40 cancel context.CancelFunc
41}
42
43// Mode describes the behavior of the cursor.
44type Mode int
45
46// Available cursor modes.
47const (
48 CursorBlink Mode = iota
49 CursorStatic
50 CursorHide
51)
52
53// String returns the cursor mode in a human-readable format. This method is
54// provisional and for informational purposes only.
55func (c Mode) String() string {
56 return [...]string{
57 "blink",
58 "static",
59 "hidden",
60 }[c]
61}
62
63// Model is the Bubble Tea model for this cursor element.
64type Model struct {
65 // Style styles the cursor block.
66 Style lipgloss.Style
67
68 // TextStyle is the style used for the cursor when it is blinking
69 // (hidden), i.e. displaying normal text.
70 TextStyle lipgloss.Style
71
72 // BlinkSpeed is the speed at which the cursor blinks. This has no effect
73 // unless [CursorMode] is not set to [CursorBlink].
74 BlinkSpeed time.Duration
75
76 // IsBlinked is the state of the cursor blink. When true, the cursor is
77 // hidden.
78 IsBlinked bool
79
80 // char is the character under the cursor
81 char string
82
83 // The ID of this Model as it relates to other cursors
84 id int
85
86 // focus indicates whether the containing input is focused
87 focus bool
88
89 // Used to manage cursor blink
90 blinkCtx *blinkCtx
91
92 // The ID of the blink message we're expecting to receive.
93 blinkTag int
94
95 // mode determines the behavior of the cursor
96 mode Mode
97}
98
99// New creates a new model with default settings.
100func New() Model {
101 return Model{
102 id: nextID(),
103 BlinkSpeed: defaultBlinkSpeed,
104 IsBlinked: true,
105 mode: CursorBlink,
106
107 blinkCtx: &blinkCtx{
108 ctx: context.Background(),
109 },
110 }
111}
112
113// Update updates the cursor.
114func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
115 switch msg := msg.(type) {
116 case initialBlinkMsg:
117 // We accept all initialBlinkMsgs generated by the Blink command.
118
119 if m.mode != CursorBlink || !m.focus {
120 return m, nil
121 }
122
123 cmd := m.Blink()
124 return m, cmd
125
126 case tea.FocusMsg:
127 return m, m.Focus()
128
129 case tea.BlurMsg:
130 m.Blur()
131 return m, nil
132
133 case BlinkMsg:
134 // We're choosy about whether to accept blinkMsgs so that our cursor
135 // only exactly when it should.
136
137 // Is this model blink-able?
138 if m.mode != CursorBlink || !m.focus {
139 return m, nil
140 }
141
142 // Were we expecting this blink message?
143 if msg.id != m.id || msg.tag != m.blinkTag {
144 return m, nil
145 }
146
147 var cmd tea.Cmd
148 if m.mode == CursorBlink {
149 m.IsBlinked = !m.IsBlinked
150 cmd = m.Blink()
151 }
152 return m, cmd
153
154 case blinkCanceled: // no-op
155 return m, nil
156 }
157 return m, nil
158}
159
160// Mode returns the model's cursor mode. For available cursor modes, see
161// type Mode.
162func (m Model) Mode() Mode {
163 return m.mode
164}
165
166// SetMode sets the model's cursor mode. This method returns a command.
167//
168// For available cursor modes, see type CursorMode.
169func (m *Model) SetMode(mode Mode) tea.Cmd {
170 // Adjust the mode value if it's value is out of range
171 if mode < CursorBlink || mode > CursorHide {
172 return nil
173 }
174 m.mode = mode
175 m.IsBlinked = m.mode == CursorHide || !m.focus
176 if mode == CursorBlink {
177 return Blink
178 }
179 return nil
180}
181
182// Blink is a command used to manage cursor blinking.
183func (m *Model) Blink() tea.Cmd {
184 if m.mode != CursorBlink {
185 return nil
186 }
187
188 if m.blinkCtx != nil && m.blinkCtx.cancel != nil {
189 m.blinkCtx.cancel()
190 }
191
192 ctx, cancel := context.WithTimeout(m.blinkCtx.ctx, m.BlinkSpeed)
193 m.blinkCtx.cancel = cancel
194
195 m.blinkTag++
196 blinkMsg := BlinkMsg{id: m.id, tag: m.blinkTag}
197
198 return func() tea.Msg {
199 defer cancel()
200 <-ctx.Done()
201 if ctx.Err() == context.DeadlineExceeded {
202 return blinkMsg
203 }
204 return blinkCanceled{}
205 }
206}
207
208// Blink is a command used to initialize cursor blinking.
209func Blink() tea.Msg {
210 return initialBlinkMsg{}
211}
212
213// Focus focuses the cursor to allow it to blink if desired.
214func (m *Model) Focus() tea.Cmd {
215 m.focus = true
216 m.IsBlinked = m.mode == CursorHide // show the cursor unless we've explicitly hidden it
217
218 if m.mode == CursorBlink && m.focus {
219 return m.Blink()
220 }
221 return nil
222}
223
224// Blur blurs the cursor.
225func (m *Model) Blur() {
226 m.focus = false
227 m.IsBlinked = true
228}
229
230// SetChar sets the character under the cursor.
231func (m *Model) SetChar(char string) {
232 m.char = char
233}
234
235// View displays the cursor.
236func (m Model) View() string {
237 if m.IsBlinked {
238 return m.TextStyle.Inline(true).Render(m.char)
239 }
240 return m.Style.Inline(true).Reverse(true).Render(m.char)
241}