Skip to content

Commit 518ff7d

Browse files
feat(textarea): add Cursor method (#712)
* feat(textarea): add CursorPosition method This returns the current cursor position accounting any soft-wrapped lines and multi-rune characters. * chore(textarea): redefine how real cursor properties are accessed (#714) --------- Co-authored-by: Christian Rocha <[email protected]>
1 parent 0c83e6f commit 518ff7d

File tree

3 files changed

+40
-19
lines changed

3 files changed

+40
-19
lines changed

go.mod

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ go 1.18
55
require (
66
github.com/MakeNowJust/heredoc v1.0.0
77
github.com/atotto/clipboard v0.1.4
8-
github.com/charmbracelet/bubbletea/v2 v2.0.0-alpha.2.0.20250114201644-43a5b4dd0af0
8+
github.com/charmbracelet/bubbletea/v2 v2.0.0-alpha.2.0.20250123211610-443afa6fa0c1
99
github.com/charmbracelet/harmonica v0.2.0
1010
github.com/charmbracelet/lipgloss/v2 v2.0.0-alpha.2.0.20250114171829-b67eb015d607
1111
github.com/charmbracelet/x/ansi v0.7.0

go.sum

+8
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,14 @@ github.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWp
66
github.com/aymanbagabas/go-udiff v0.2.0/go.mod h1:RE4Ex0qsGkTAJoQdQQCA0uG+nAzJO/pI/QwceO5fgrA=
77
github.com/charmbracelet/bubbletea/v2 v2.0.0-alpha.2.0.20250114201644-43a5b4dd0af0 h1:BWjXQRSwBjoCpLeNu8zT93n+NHhZZhkQQLveXMmnkYc=
88
github.com/charmbracelet/bubbletea/v2 v2.0.0-alpha.2.0.20250114201644-43a5b4dd0af0/go.mod h1:hT2875Ank3ylgW13kqu6cjDc9XIk9sE5JsOFYdl09b8=
9+
github.com/charmbracelet/bubbletea/v2 v2.0.0-alpha.2.0.20250123204203-55f6f9f70bf6 h1:L2+Kl71AsucUpl32AqmbjVv/4Ha7dwlSFwqrU4sAeTE=
10+
github.com/charmbracelet/bubbletea/v2 v2.0.0-alpha.2.0.20250123204203-55f6f9f70bf6/go.mod h1:hT2875Ank3ylgW13kqu6cjDc9XIk9sE5JsOFYdl09b8=
11+
github.com/charmbracelet/bubbletea/v2 v2.0.0-alpha.2.0.20250123210727-80fa20da7d7b h1:QqN3KApDbHJl+B1lVSir6GyRbxH7EA6U1SCDoxz8xYU=
12+
github.com/charmbracelet/bubbletea/v2 v2.0.0-alpha.2.0.20250123210727-80fa20da7d7b/go.mod h1:hT2875Ank3ylgW13kqu6cjDc9XIk9sE5JsOFYdl09b8=
13+
github.com/charmbracelet/bubbletea/v2 v2.0.0-alpha.2.0.20250123210853-99e3bbf892cd h1:1WsMNlPUaDXgJprIvWg+ZsXmc4GiL4KsBEFNZ3ymKeA=
14+
github.com/charmbracelet/bubbletea/v2 v2.0.0-alpha.2.0.20250123210853-99e3bbf892cd/go.mod h1:hT2875Ank3ylgW13kqu6cjDc9XIk9sE5JsOFYdl09b8=
15+
github.com/charmbracelet/bubbletea/v2 v2.0.0-alpha.2.0.20250123211610-443afa6fa0c1 h1:tktnM4YimEWSYd58iZlPDB3Xz25/r94VYZZsHK5zWL0=
16+
github.com/charmbracelet/bubbletea/v2 v2.0.0-alpha.2.0.20250123211610-443afa6fa0c1/go.mod h1:hT2875Ank3ylgW13kqu6cjDc9XIk9sE5JsOFYdl09b8=
917
github.com/charmbracelet/colorprofile v0.1.9 h1:5JnfvX+I9D6rRNu8xK3pgIqknaBVTXHU9pGu1jkZxLw=
1018
github.com/charmbracelet/colorprofile v0.1.9/go.mod h1:+jpmObxZl1Dab3H3IMVIPSZTsKcFpjJUv97G0dLqM60=
1119
github.com/charmbracelet/harmonica v0.2.0 h1:8NxJWRWg/bzKqqEaaeFNipOu77YR5t8aSwG4pgaUBiQ=

textarea/textarea.go

+31-18
Original file line numberDiff line numberDiff line change
@@ -231,8 +231,8 @@ type Model struct {
231231
// when switching focus states.
232232
activeStyle *StyleState
233233

234-
// Cursor is the text area cursor.
235-
Cursor cursor.Model
234+
// VirtualCursor is the text area cursor.
235+
VirtualCursor cursor.Model
236236

237237
// CharLimit is the maximum number of characters this input element will
238238
// accept. If 0 or less, there's no limit.
@@ -305,7 +305,7 @@ func New() Model {
305305
cache: memoization.NewMemoCache[line, [][]rune](maxLines),
306306
EndOfBufferCharacter: ' ',
307307
ShowLineNumbers: true,
308-
Cursor: cur,
308+
VirtualCursor: cur,
309309
KeyMap: DefaultKeyMap(),
310310

311311
value: make([][]rune, minHeight, maxLines),
@@ -600,15 +600,15 @@ func (m Model) Focused() bool {
600600
func (m *Model) Focus() tea.Cmd {
601601
m.focus = true
602602
m.activeStyle = &m.Styles.Focused
603-
return m.Cursor.Focus()
603+
return m.VirtualCursor.Focus()
604604
}
605605

606606
// Blur removes the focus state on the model. When the model is blurred it can
607607
// not receive keyboard input and the cursor will be hidden.
608608
func (m *Model) Blur() {
609609
m.focus = false
610610
m.activeStyle = &m.Styles.Blurred
611-
m.Cursor.Blur()
611+
m.VirtualCursor.Blur()
612612
}
613613

614614
// Reset sets the input to its default state with no input.
@@ -976,7 +976,7 @@ func (m *Model) SetHeight(h int) {
976976
// Update is the Bubble Tea update loop.
977977
func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
978978
if !m.focus {
979-
m.Cursor.Blur()
979+
m.VirtualCursor.Blur()
980980
return m, nil
981981
}
982982

@@ -1098,10 +1098,10 @@ func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
10981098
cmds = append(cmds, cmd)
10991099

11001100
newRow, newCol := m.cursorLineNumber(), m.col
1101-
m.Cursor, cmd = m.Cursor.Update(msg)
1102-
if (newRow != oldRow || newCol != oldCol) && m.Cursor.Mode() == cursor.CursorBlink {
1103-
m.Cursor.Blink = false
1104-
cmd = m.Cursor.BlinkCmd()
1101+
m.VirtualCursor, cmd = m.VirtualCursor.Update(msg)
1102+
if (newRow != oldRow || newCol != oldCol) && m.VirtualCursor.Mode() == cursor.CursorBlink {
1103+
m.VirtualCursor.Blink = false
1104+
cmd = m.VirtualCursor.BlinkCmd()
11051105
}
11061106
cmds = append(cmds, cmd)
11071107

@@ -1115,7 +1115,7 @@ func (m Model) View() string {
11151115
if m.Value() == "" && m.row == 0 && m.col == 0 && m.Placeholder != "" {
11161116
return m.placeholderView()
11171117
}
1118-
m.Cursor.TextStyle = m.activeStyle.computedCursorLine()
1118+
m.VirtualCursor.TextStyle = m.activeStyle.computedCursorLine()
11191119

11201120
var (
11211121
s strings.Builder
@@ -1184,11 +1184,11 @@ func (m Model) View() string {
11841184
if m.row == l && lineInfo.RowOffset == wl {
11851185
s.WriteString(style.Render(string(wrappedLine[:lineInfo.ColumnOffset])))
11861186
if m.col >= len(line) && lineInfo.CharOffset >= m.width {
1187-
m.Cursor.SetChar(" ")
1188-
s.WriteString(m.Cursor.View())
1187+
m.VirtualCursor.SetChar(" ")
1188+
s.WriteString(m.VirtualCursor.View())
11891189
} else {
1190-
m.Cursor.SetChar(string(wrappedLine[lineInfo.ColumnOffset]))
1191-
s.WriteString(style.Render(m.Cursor.View()))
1190+
m.VirtualCursor.SetChar(string(wrappedLine[lineInfo.ColumnOffset]))
1191+
s.WriteString(style.Render(m.VirtualCursor.View()))
11921192
s.WriteString(style.Render(string(wrappedLine[lineInfo.ColumnOffset+1:])))
11931193
}
11941194
} else {
@@ -1291,9 +1291,9 @@ func (m Model) placeholderView() string {
12911291
// first line
12921292
case i == 0:
12931293
// first character of first line as cursor with character
1294-
m.Cursor.TextStyle = m.activeStyle.computedPlaceholder()
1295-
m.Cursor.SetChar(string(plines[0][0]))
1296-
s.WriteString(lineStyle.Render(m.Cursor.View()))
1294+
m.VirtualCursor.TextStyle = m.activeStyle.computedPlaceholder()
1295+
m.VirtualCursor.SetChar(string(plines[0][0]))
1296+
s.WriteString(lineStyle.Render(m.VirtualCursor.View()))
12971297

12981298
// the rest of the first line
12991299
s.WriteString(lineStyle.Render(style.Render(plines[0][1:] + strings.Repeat(" ", max(0, m.width-uniseg.StringWidth(plines[0]))))))
@@ -1322,6 +1322,19 @@ func Blink() tea.Msg {
13221322
return cursor.Blink()
13231323
}
13241324

1325+
// Cursor returns the current cursor position accounting any
1326+
// soft-wrapped lines.
1327+
func (m Model) Cursor() *tea.Cursor {
1328+
lineInfo := m.LineInfo()
1329+
x, y := lineInfo.CharOffset, m.cursorLineNumber()-m.viewport.YOffset
1330+
1331+
// TODO: sort out where these properties live.
1332+
c := tea.NewCursor(x, y)
1333+
c.Blink = true
1334+
c.Color = m.VirtualCursor.Style.GetForeground()
1335+
return c
1336+
}
1337+
13251338
func (m Model) memoizedWrap(runes []rune, width int) [][]rune {
13261339
input := line{runes: runes, width: width}
13271340
if v, ok := m.cache.Get(input); ok {

0 commit comments

Comments
 (0)