cursor.go 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207
  1. package cursor
  2. import (
  3. "context"
  4. "time"
  5. tea "github.com/charmbracelet/bubbletea"
  6. "github.com/charmbracelet/lipgloss"
  7. )
  8. const defaultBlinkSpeed = time.Millisecond * 530
  9. // initialBlinkMsg initializes cursor blinking.
  10. type initialBlinkMsg struct{}
  11. // BlinkMsg signals that the cursor should blink. It contains metadata that
  12. // allows us to tell if the blink message is the one we're expecting.
  13. type BlinkMsg struct {
  14. id int
  15. tag int
  16. }
  17. // blinkCanceled is sent when a blink operation is canceled.
  18. type blinkCanceled struct{}
  19. // blinkCtx manages cursor blinking.
  20. type blinkCtx struct {
  21. ctx context.Context
  22. cancel context.CancelFunc
  23. }
  24. // Mode describes the behavior of the cursor.
  25. type Mode int
  26. // Available cursor modes.
  27. const (
  28. CursorBlink Mode = iota
  29. CursorStatic
  30. CursorHide
  31. )
  32. // String returns the cursor mode in a human-readable format. This method is
  33. // provisional and for informational purposes only.
  34. func (c Mode) String() string {
  35. return [...]string{
  36. "blink",
  37. "static",
  38. "hidden",
  39. }[c]
  40. }
  41. // Model is the Bubble Tea model for this cursor element.
  42. type Model struct {
  43. BlinkSpeed time.Duration
  44. // Style for styling the cursor block.
  45. Style lipgloss.Style
  46. // TextStyle is the style used for the cursor when it is hidden (when blinking).
  47. // I.e. displaying normal text.
  48. TextStyle lipgloss.Style
  49. // char is the character under the cursor
  50. char string
  51. // The ID of this Model as it relates to other cursors
  52. id int
  53. // focus indicates whether the containing input is focused
  54. focus bool
  55. // Cursor Blink state.
  56. Blink bool
  57. // Used to manage cursor blink
  58. blinkCtx *blinkCtx
  59. // The ID of the blink message we're expecting to receive.
  60. blinkTag int
  61. // mode determines the behavior of the cursor
  62. mode Mode
  63. }
  64. // New creates a new model with default settings.
  65. func New() Model {
  66. return Model{
  67. BlinkSpeed: defaultBlinkSpeed,
  68. Blink: true,
  69. mode: CursorBlink,
  70. blinkCtx: &blinkCtx{
  71. ctx: context.Background(),
  72. },
  73. }
  74. }
  75. // Update updates the cursor.
  76. func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
  77. switch msg := msg.(type) {
  78. case initialBlinkMsg:
  79. // We accept all initialBlinkMsgs generated by the Blink command.
  80. if m.mode != CursorBlink || !m.focus {
  81. return m, nil
  82. }
  83. cmd := m.BlinkCmd()
  84. return m, cmd
  85. case BlinkMsg:
  86. // We're choosy about whether to accept blinkMsgs so that our cursor
  87. // only exactly when it should.
  88. // Is this model blink-able?
  89. if m.mode != CursorBlink || !m.focus {
  90. return m, nil
  91. }
  92. // Were we expecting this blink message?
  93. if msg.id != m.id || msg.tag != m.blinkTag {
  94. return m, nil
  95. }
  96. var cmd tea.Cmd
  97. if m.mode == CursorBlink {
  98. m.Blink = !m.Blink
  99. cmd = m.BlinkCmd()
  100. }
  101. return m, cmd
  102. case blinkCanceled: // no-op
  103. return m, nil
  104. }
  105. return m, nil
  106. }
  107. // Mode returns the model's cursor mode. For available cursor modes, see
  108. // type Mode.
  109. func (m Model) Mode() Mode {
  110. return m.mode
  111. }
  112. // SetMode sets the model's cursor mode. This method returns a command.
  113. //
  114. // For available cursor modes, see type CursorMode.
  115. func (m *Model) SetMode(mode Mode) tea.Cmd {
  116. m.mode = mode
  117. m.Blink = m.mode == CursorHide || !m.focus
  118. if mode == CursorBlink {
  119. return Blink
  120. }
  121. return nil
  122. }
  123. // BlinkCmd is an command used to manage cursor blinking.
  124. func (m *Model) BlinkCmd() tea.Cmd {
  125. if m.mode != CursorBlink {
  126. return nil
  127. }
  128. if m.blinkCtx != nil && m.blinkCtx.cancel != nil {
  129. m.blinkCtx.cancel()
  130. }
  131. ctx, cancel := context.WithTimeout(m.blinkCtx.ctx, m.BlinkSpeed)
  132. m.blinkCtx.cancel = cancel
  133. m.blinkTag++
  134. return func() tea.Msg {
  135. defer cancel()
  136. <-ctx.Done()
  137. if ctx.Err() == context.DeadlineExceeded {
  138. return BlinkMsg{id: m.id, tag: m.blinkTag}
  139. }
  140. return blinkCanceled{}
  141. }
  142. }
  143. // Blink is a command used to initialize cursor blinking.
  144. func Blink() tea.Msg {
  145. return initialBlinkMsg{}
  146. }
  147. // Focus focuses the cursor to allow it to blink if desired.
  148. func (m *Model) Focus() tea.Cmd {
  149. m.focus = true
  150. m.Blink = m.mode == CursorHide // show the cursor unless we've explicitly hidden it
  151. if m.mode == CursorBlink && m.focus {
  152. return m.BlinkCmd()
  153. }
  154. return nil
  155. }
  156. // Blur blurs the cursor.
  157. func (m *Model) Blur() {
  158. m.focus = false
  159. m.Blink = true
  160. }
  161. // SetChar sets the character under the cursor.
  162. func (m *Model) SetChar(char string) {
  163. m.char = char
  164. }
  165. // View displays the cursor.
  166. func (m Model) View() string {
  167. if m.Blink {
  168. return m.TextStyle.Inline(true).Render(m.char)
  169. }
  170. return m.Style.Inline(true).Reverse(true).Render(m.char)
  171. }