cursor.go

  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}