Merge branch 'v2-exp' into v2-table

This commit is contained in:
Andrey Nering
2025-06-03 11:15:47 -03:00
11 changed files with 197 additions and 105 deletions
+25 -16
View File
@@ -4,6 +4,7 @@ package cursor
import (
"context"
"sync/atomic"
"time"
tea "github.com/charmbracelet/bubbletea/v2"
@@ -12,6 +13,14 @@ import (
const defaultBlinkSpeed = time.Millisecond * 530
// Internal ID management. Used during animating to ensure that frame messages
// are received only by spinner components that sent them.
var lastID int64
func nextID() int {
return int(atomic.AddInt64(&lastID, 1))
}
// initialBlinkMsg initializes cursor blinking.
type initialBlinkMsg struct{}
@@ -64,8 +73,9 @@ type Model struct {
// unless [CursorMode] is not set to [CursorBlink].
BlinkSpeed time.Duration
// Blink is the state of the cursor blink. When true, the cursor is hidden.
Blink bool
// IsBlinked is the state of the cursor blink. When true, the cursor is
// hidden.
IsBlinked bool
// char is the character under the cursor
char string
@@ -89,10 +99,10 @@ type Model struct {
// New creates a new model with default settings.
func New() Model {
return Model{
id: nextID(),
BlinkSpeed: defaultBlinkSpeed,
Blink: true,
mode: CursorBlink,
IsBlinked: true,
mode: CursorBlink,
blinkCtx: &blinkCtx{
ctx: context.Background(),
@@ -110,7 +120,7 @@ func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
return m, nil
}
cmd := m.BlinkCmd()
cmd := m.Blink()
return m, cmd
case tea.FocusMsg:
@@ -136,8 +146,8 @@ func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
var cmd tea.Cmd
if m.mode == CursorBlink {
m.Blink = !m.Blink
cmd = m.BlinkCmd()
m.IsBlinked = !m.IsBlinked
cmd = m.Blink()
}
return m, cmd
@@ -162,15 +172,15 @@ func (m *Model) SetMode(mode Mode) tea.Cmd {
return nil
}
m.mode = mode
m.Blink = m.mode == CursorHide || !m.focus
m.IsBlinked = m.mode == CursorHide || !m.focus
if mode == CursorBlink {
return Blink
}
return nil
}
// BlinkCmd is a command used to manage cursor blinking.
func (m *Model) BlinkCmd() tea.Cmd {
// Blink is a command used to manage cursor blinking.
func (m *Model) Blink() tea.Cmd {
if m.mode != CursorBlink {
return nil
}
@@ -183,7 +193,6 @@ func (m *Model) BlinkCmd() tea.Cmd {
m.blinkCtx.cancel = cancel
m.blinkTag++
blinkMsg := BlinkMsg{id: m.id, tag: m.blinkTag}
return func() tea.Msg {
@@ -204,10 +213,10 @@ func Blink() tea.Msg {
// Focus focuses the cursor to allow it to blink if desired.
func (m *Model) Focus() tea.Cmd {
m.focus = true
m.Blink = m.mode == CursorHide // show the cursor unless we've explicitly hidden it
m.IsBlinked = m.mode == CursorHide // show the cursor unless we've explicitly hidden it
if m.mode == CursorBlink && m.focus {
return m.BlinkCmd()
return m.Blink()
}
return nil
}
@@ -215,7 +224,7 @@ func (m *Model) Focus() tea.Cmd {
// Blur blurs the cursor.
func (m *Model) Blur() {
m.focus = false
m.Blink = true
m.IsBlinked = true
}
// SetChar sets the character under the cursor.
@@ -225,7 +234,7 @@ func (m *Model) SetChar(char string) {
// View displays the cursor.
func (m Model) View() string {
if m.Blink {
if m.IsBlinked {
return m.TextStyle.Inline(true).Render(m.char)
}
return m.Style.Inline(true).Reverse(true).Render(m.char)
+6 -6
View File
@@ -8,7 +8,7 @@ import (
// TestBlinkCmdDataRace tests for a race on [Cursor.blinkTag].
//
// The original [Model.BlinkCmd] implementation returned a closure over the pointer receiver:
// The original [Model.Blink] implementation returned a closure over the pointer receiver:
//
// return func() tea.Msg {
// defer cancel()
@@ -20,12 +20,12 @@ import (
// }
//
// A race on “m.blinkTag” will occur if:
// 1. [Model.BlinkCmd] is called e.g. by calling [Model.Focus] from
// 1. [Model.Blink] is called e.g. by calling [Model.Focus] from
// ["github.com/charmbracelet/bubbletea".Model.Update];
// 2. ["github.com/charmbracelet/bubbletea".handleCommands] is kept sufficiently busy that it does not recieve and
// execute the [Model.BlinkCmd] e.g. by other long running command or commands;
// execute the [Model.Blink] e.g. by other long running command or commands;
// 3. at least [Mode.BlinkSpeed] time elapses;
// 4. [Model.BlinkCmd] is called again;
// 4. [Model.Blink] is called again;
// 5. ["github.com/charmbracelet/bubbletea".handleCommands] gets around to receiving and executing the original
// closure.
//
@@ -33,7 +33,7 @@ import (
// current value rather than the value at the time the closure was created).
func TestBlinkCmdDataRace(t *testing.T) {
m := New()
cmd := m.BlinkCmd()
cmd := m.Blink()
var wg sync.WaitGroup
wg.Add(2)
go func() {
@@ -44,7 +44,7 @@ func TestBlinkCmdDataRace(t *testing.T) {
go func() {
defer wg.Done()
time.Sleep(m.BlinkSpeed * 2)
m.BlinkCmd()
m.Blink()
}()
wg.Wait()
}
+1 -1
View File
@@ -121,5 +121,5 @@ type HInt int
// Hash is a method that returns the hash of the integer.
func (h HInt) Hash() string {
return fmt.Sprintf("%x", sha256.Sum256([]byte(fmt.Sprintf("%d", h))))
return fmt.Sprintf("%x", sha256.Sum256(fmt.Appendf(nil, "%d", h)))
}
+6 -5
View File
@@ -4,6 +4,7 @@ import (
"encoding/binary"
"fmt"
"os"
"slices"
"testing"
)
@@ -17,8 +18,8 @@ const (
type cacheAction struct {
actionType actionType
key HString
value interface{}
expectedValue interface{}
value any
expectedValue any
}
type testCase struct {
@@ -121,7 +122,7 @@ func TestCache(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cache := NewMemoCache[HString, interface{}](tt.capacity)
cache := NewMemoCache[HString, any](tt.capacity)
for _, action := range tt.actions {
switch action.actionType {
case set:
@@ -174,7 +175,7 @@ func FuzzCache(f *testing.F) {
// If the key is already in accessOrder, we remove it and append it again later
for index, accessedKey := range accessOrder {
if accessedKey == key {
accessOrder = append(accessOrder[:index], accessOrder[index+1:]...)
accessOrder = slices.Delete(accessOrder, index, index+1)
break
}
}
@@ -206,7 +207,7 @@ func FuzzCache(f *testing.F) {
// If the key was accessed, move it to the end of the accessOrder to represent recent use
for index, accessedKey := range accessOrder {
if accessedKey == key {
accessOrder = append(accessOrder[:index], accessOrder[index+1:]...)
accessOrder = slices.Delete(accessOrder, index, index+1)
accessOrder = append(accessOrder, key)
break
}
+1 -1
View File
@@ -61,7 +61,7 @@ func (s *sanitizer) Sanitize(runes []rune) []rune {
// is smaller or equal to the input.
copied := false
for src := 0; src < len(runes); src++ {
for src := range runes {
r := runes[src]
switch {
case r == utf8.RuneError:
+1 -1
View File
@@ -185,7 +185,7 @@ func (m Model) View() string {
func (m Model) dotsView() string {
var s string
for i := 0; i < m.TotalPages; i++ {
for i := range m.TotalPages {
if i == m.Page {
s += m.ActiveDot
continue
+1 -1
View File
@@ -295,7 +295,7 @@ func (m Model) barView(b *strings.Builder, percent float64, textWidth int) {
if m.useRamp {
// Gradient fill
for i := 0; i < fw; i++ {
for i := range fw {
if fw == 1 {
// this is up for debate: in a gradient of width=1, should the
// single character rendered be the first color, the last color
+64 -31
View File
@@ -6,6 +6,7 @@ import (
"crypto/sha256"
"fmt"
"image/color"
"slices"
"strconv"
"strings"
"time"
@@ -260,17 +261,9 @@ type Model struct {
// KeyMap encodes the keybindings recognized by the widget.
KeyMap KeyMap
// Styling. FocusedStyle and BlurredStyle are used to style the textarea in
// focused and blurred states.
Styles Styles
// virtualCursor manages the virtual cursor.
virtualCursor cursor.Model
// VirtualCursor determines whether or not to use the virtual cursor. If
// set to false, use [Model.Cursor] to return a real cursor for rendering.
VirtualCursor bool
// CharLimit is the maximum number of characters this input element will
// accept. If 0 or less, there's no limit.
CharLimit int
@@ -283,6 +276,15 @@ type Model struct {
// there's no limit.
MaxWidth int
// Styling. Styles are defined in [Styles]. Use [SetStyles] and [GetStyles]
// to work with this value publicly.
styles Styles
// useVirtualCursor determines whether or not to use the virtual cursor.
// Use [SetVirtualCursor] and [VirtualCursor] to work with this this
// value publicly.
useVirtualCursor bool
// If promptFunc is set, it replaces Prompt as a generator for
// prompt strings at the beginning of each line.
promptFunc func(line int) string
@@ -337,11 +339,11 @@ func New() Model {
MaxHeight: defaultMaxHeight,
MaxWidth: defaultMaxWidth,
Prompt: lipgloss.ThickBorder().Left + " ",
Styles: styles,
styles: styles,
cache: memoization.NewMemoCache[line, [][]rune](maxLines),
EndOfBufferCharacter: ' ',
ShowLineNumbers: true,
VirtualCursor: true,
useVirtualCursor: true,
virtualCursor: cur,
KeyMap: DefaultKeyMap(),
@@ -403,21 +405,43 @@ func DefaultDarkStyles() Styles {
return DefaultStyles(true)
}
// Styles returns the current styles for the textarea.
func (m Model) Styles() Styles {
return m.styles
}
// SetStyles updates styling for the textarea.
func (m *Model) SetStyles(s Styles) {
m.styles = s
m.updateVirtualCursorStyle()
}
// VirtualCursor returns whether or not the virtual cursor is enabled.
func (m Model) VirtualCursor() bool {
return m.useVirtualCursor
}
// SetVirtualCursor sets whether or not to use the virtual cursor.
func (m *Model) SetVirtualCursor(v bool) {
m.useVirtualCursor = v
m.updateVirtualCursorStyle()
}
// updateVirtualCursorStyle sets styling on the virtual cursor based on the
// textarea's style settings.
func (m *Model) updateVirtualCursorStyle() {
if !m.VirtualCursor {
if !m.useVirtualCursor {
m.virtualCursor.SetMode(cursor.CursorHide)
return
}
m.virtualCursor.Style = lipgloss.NewStyle().Foreground(m.Styles.Cursor.Color)
m.virtualCursor.Style = lipgloss.NewStyle().Foreground(m.styles.Cursor.Color)
// By default, the blink speed of the cursor is set to a default
// internally.
if m.Styles.Cursor.Blink {
if m.Styles.Cursor.BlinkSpeed > 0 {
m.virtualCursor.BlinkSpeed = m.Styles.Cursor.BlinkSpeed
if m.styles.Cursor.Blink {
if m.styles.Cursor.BlinkSpeed > 0 {
m.virtualCursor.BlinkSpeed = m.styles.Cursor.BlinkSpeed
}
m.virtualCursor.SetMode(cursor.CursorBlink)
return
@@ -464,7 +488,7 @@ func (m *Model) insertRunesFromUserInput(runes []rune) {
// Split the input into lines.
var lines [][]rune
lstart := 0
for i := 0; i < len(runes); i++ {
for i := range runes {
if runes[i] == '\n' {
// Queue a line to become a new row in the text area below.
// Beware to clamp the max capacity of the slice, to ensure no
@@ -663,9 +687,9 @@ func (m Model) Focused() bool {
// whether the textarea is focused or blurred.
func (m Model) activeStyle() *StyleState {
if m.focus {
return &m.Styles.Focused
return &m.styles.Focused
}
return &m.Styles.Blurred
return &m.styles.Blurred
}
// Focus sets the focus state on the model. When the model is in focus it can
@@ -1102,7 +1126,7 @@ func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
}
case key.Matches(msg, m.KeyMap.DeleteCharacterForward):
if len(m.value[m.row]) > 0 && m.col < len(m.value[m.row]) {
m.value[m.row] = append(m.value[m.row][:m.col], m.value[m.row][m.col+1:]...)
m.value[m.row] = slices.Delete(m.value[m.row], m.col, m.col+1)
}
if m.col >= len(m.value[m.row]) {
m.mergeLineBelow(m.row)
@@ -1173,13 +1197,18 @@ func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
m.viewport = &vp
cmds = append(cmds, cmd)
newRow, newCol := m.cursorLineNumber(), m.col
m.virtualCursor, cmd = m.virtualCursor.Update(msg)
if (newRow != oldRow || newCol != oldCol) && m.virtualCursor.Mode() == cursor.CursorBlink {
m.virtualCursor.Blink = false
cmd = m.virtualCursor.BlinkCmd()
if m.useVirtualCursor {
m.virtualCursor, cmd = m.virtualCursor.Update(msg)
// If the cursor has moved, reset the blink state. This is a small UX
// nuance that makes cursor movement obvious and feel snappy.
newRow, newCol := m.cursorLineNumber(), m.col
if (newRow != oldRow || newCol != oldCol) && m.virtualCursor.Mode() == cursor.CursorBlink {
m.virtualCursor.IsBlinked = false
cmd = m.virtualCursor.Blink()
}
cmds = append(cmds, cmd)
}
cmds = append(cmds, cmd)
m.repositionView()
@@ -1188,7 +1217,6 @@ func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
// View renders the text area in its current state.
func (m Model) View() string {
m.updateVirtualCursorStyle()
if m.Value() == "" && m.row == 0 && m.col == 0 && m.Placeholder != "" {
return m.placeholderView()
}
@@ -1348,7 +1376,7 @@ func (m Model) placeholderView() string {
// split string by new lines
plines := strings.Split(strings.TrimSpace(pwrap), "\n")
for i := 0; i < m.height; i++ {
for i := range m.height {
isLineNumber := len(plines) > i
lineStyle := styles.computedPlaceholder()
@@ -1390,6 +1418,11 @@ func (m Model) placeholderView() string {
// the rest of the first line
s.WriteString(lineStyle.Render(styles.computedPlaceholder().Render(rest)))
// extend the first line with spaces to fill the width, so that
// the entire line is filled when cursorline is enabled.
gap := strings.Repeat(" ", max(0, m.width-lipgloss.Width(plines[0])))
s.WriteString(lineStyle.Render(gap))
// remaining lines
case len(plines) > i:
// current line placeholder text
@@ -1431,7 +1464,7 @@ func Blink() tea.Msg {
// f.Cursor.Position.X += offsetX
// f.Cursor.Position.Y += offsetY
func (m Model) Cursor() *tea.Cursor {
if m.VirtualCursor {
if m.useVirtualCursor {
return nil
}
@@ -1453,9 +1486,9 @@ func (m Model) Cursor() *tea.Cursor {
baseStyle.GetBorderTopSize()
c := tea.NewCursor(xOffset, yOffset)
c.Blink = m.Styles.Cursor.Blink
c.Color = m.Styles.Cursor.Color
c.Shape = m.Styles.Cursor.Shape
c.Blink = m.styles.Cursor.Blink
c.Color = m.styles.Cursor.Color
c.Shape = m.styles.Cursor.Shape
return c
}
+24 -8
View File
@@ -1032,7 +1032,9 @@ func TestView(t *testing.T) {
{
name: "set width with style",
modelFunc: func(m Model) Model {
m.Styles.Focused.Base = lipgloss.NewStyle().Border(lipgloss.NormalBorder())
s := m.Styles()
s.Focused.Base = lipgloss.NewStyle().Border(lipgloss.NormalBorder())
m.SetStyles(s)
m.Focus()
m.SetWidth(12)
@@ -1060,7 +1062,9 @@ func TestView(t *testing.T) {
{
name: "set width with style max width minus one",
modelFunc: func(m Model) Model {
m.Styles.Focused.Base = lipgloss.NewStyle().Border(lipgloss.NormalBorder())
s := m.Styles()
s.Focused.Base = lipgloss.NewStyle().Border(lipgloss.NormalBorder())
m.SetStyles(s)
m.Focus()
m.SetWidth(12)
@@ -1088,7 +1092,9 @@ func TestView(t *testing.T) {
{
name: "set width with style max width",
modelFunc: func(m Model) Model {
m.Styles.Focused.Base = lipgloss.NewStyle().Border(lipgloss.NormalBorder())
s := m.Styles()
s.Focused.Base = lipgloss.NewStyle().Border(lipgloss.NormalBorder())
m.SetStyles(s)
m.Focus()
m.SetWidth(12)
@@ -1116,7 +1122,9 @@ func TestView(t *testing.T) {
{
name: "set width with style max width plus one",
modelFunc: func(m Model) Model {
m.Styles.Focused.Base = lipgloss.NewStyle().Border(lipgloss.NormalBorder())
s := m.Styles()
s.Focused.Base = lipgloss.NewStyle().Border(lipgloss.NormalBorder())
m.SetStyles(s)
m.Focus()
m.SetWidth(12)
@@ -1144,7 +1152,9 @@ func TestView(t *testing.T) {
{
name: "set width without line numbers with style",
modelFunc: func(m Model) Model {
m.Styles.Focused.Base = lipgloss.NewStyle().Border(lipgloss.NormalBorder())
s := m.Styles()
s.Focused.Base = lipgloss.NewStyle().Border(lipgloss.NormalBorder())
m.SetStyles(s)
m.Focus()
m.ShowLineNumbers = false
@@ -1173,7 +1183,9 @@ func TestView(t *testing.T) {
{
name: "set width without line numbers with style max width minus one",
modelFunc: func(m Model) Model {
m.Styles.Focused.Base = lipgloss.NewStyle().Border(lipgloss.NormalBorder())
s := m.Styles()
s.Focused.Base = lipgloss.NewStyle().Border(lipgloss.NormalBorder())
m.SetStyles(s)
m.Focus()
m.ShowLineNumbers = false
@@ -1202,7 +1214,9 @@ func TestView(t *testing.T) {
{
name: "set width without line numbers with style max width",
modelFunc: func(m Model) Model {
m.Styles.Focused.Base = lipgloss.NewStyle().Border(lipgloss.NormalBorder())
s := m.Styles()
s.Focused.Base = lipgloss.NewStyle().Border(lipgloss.NormalBorder())
m.SetStyles(s)
m.Focus()
m.ShowLineNumbers = false
@@ -1231,7 +1245,9 @@ func TestView(t *testing.T) {
{
name: "set width without line numbers with style max width plus one",
modelFunc: func(m Model) Model {
m.Styles.Focused.Base = lipgloss.NewStyle().Border(lipgloss.NormalBorder())
s := m.Styles()
s.Focused.Base = lipgloss.NewStyle().Border(lipgloss.NormalBorder())
m.SetStyles(s)
m.Focus()
m.ShowLineNumbers = false
+67 -34
View File
@@ -4,6 +4,7 @@ package textinput
import (
"reflect"
"slices"
"strings"
"unicode"
@@ -95,19 +96,21 @@ type Model struct {
EchoMode EchoMode
EchoCharacter rune
// VirtualCursor determines whether or not to use the virtual cursor. If
// useVirtualCursor determines whether or not to use the virtual cursor. If
// set to false, use [Model.Cursor] to return a real cursor for rendering.
VirtualCursor bool
virtualCursor cursor.Model
useVirtualCursor bool
// Styling. FocusedStyle and BlurredStyle are used to style the textarea in
// focused and blurred states.
Styles Styles
// Virtual cursor manager.
virtualCursor cursor.Model
// CharLimit is the maximum amount of characters this input element will
// accept. If 0 or less, there's no limit.
CharLimit int
// Styling. FocusedStyle and BlurredStyle are used to style the textarea in
// focused and blurred states.
styles Styles
// Width is the maximum number of characters that can be displayed at once.
// It essentially treats the text field like a horizontally scrolling
// viewport. If 0 or less this setting is ignored.
@@ -152,19 +155,45 @@ type Model struct {
// New creates a new model with default settings.
func New() Model {
return Model{
Prompt: "> ",
EchoCharacter: '*',
CharLimit: 0,
Styles: DefaultDarkStyles(),
ShowSuggestions: false,
virtualCursor: cursor.New(),
KeyMap: DefaultKeyMap(),
suggestions: [][]rune{},
value: nil,
focus: false,
pos: 0,
m := Model{
Prompt: "> ",
EchoCharacter: '*',
CharLimit: 0,
styles: DefaultDarkStyles(),
ShowSuggestions: false,
useVirtualCursor: true,
virtualCursor: cursor.New(),
KeyMap: DefaultKeyMap(),
suggestions: [][]rune{},
value: nil,
focus: false,
pos: 0,
}
m.updateVirtualCursorStyle()
return m
}
// VirtualCursor returns whether the model is using a virtual cursor.
func (m Model) VirtualCursor() bool {
return m.useVirtualCursor
}
// SetVirtualCursor sets whether the model should use a virtual cursor. If
// disabled, use [Model.Cursor] to return a real cursor for rendering.
func (m *Model) SetVirtualCursor(v bool) {
m.useVirtualCursor = v
m.updateVirtualCursorStyle()
}
// Styles returns the current set of styles.
func (m Model) Styles() Styles {
return m.styles
}
// SetStyles sets the styles for the text input.
func (m *Model) SetStyles(s Styles) {
m.styles = s
m.updateVirtualCursorStyle()
}
// Width returns the width of the text input.
@@ -549,7 +578,6 @@ func (m Model) echoTransform(v string) string {
// Update is the Bubble Tea update loop.
func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
m.updateVirtualCursorStyle()
if !m.focus {
return m, nil
}
@@ -597,7 +625,7 @@ func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
m.CursorStart()
case key.Matches(msg, m.KeyMap.DeleteCharacterForward):
if len(m.value) > 0 && m.pos < len(m.value) {
m.value = append(m.value[:m.pos], m.value[m.pos+1:]...)
m.value = slices.Delete(m.value, m.pos, m.pos+1)
m.Err = m.validate(m.value)
}
case key.Matches(msg, m.KeyMap.LineEnd):
@@ -636,12 +664,16 @@ func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
var cmds []tea.Cmd
var cmd tea.Cmd
m.virtualCursor, cmd = m.virtualCursor.Update(msg)
cmds = append(cmds, cmd)
if m.useVirtualCursor {
m.virtualCursor, cmd = m.virtualCursor.Update(msg)
cmds = append(cmds, cmd)
if oldPos != m.pos && m.virtualCursor.Mode() == cursor.CursorBlink {
m.virtualCursor.Blink = false
cmds = append(cmds, m.virtualCursor.BlinkCmd())
// If the cursor position changed, reset the blink state. This is a
// small UX nuance that makes cursor movement obvious and feel snappy.
if oldPos != m.pos && m.virtualCursor.Mode() == cursor.CursorBlink {
m.virtualCursor.IsBlinked = false
cmds = append(cmds, m.virtualCursor.Blink())
}
}
m.handleOverflow()
@@ -882,7 +914,7 @@ func (m Model) validate(v []rune) error {
// f.Cursor.Position.X += offsetX
// f.Cursor.Position.Y += offsetY
func (m Model) Cursor() *tea.Cursor {
if m.VirtualCursor {
if m.useVirtualCursor {
return nil
}
@@ -895,7 +927,7 @@ func (m Model) Cursor() *tea.Cursor {
xOffset = min(xOffset, m.width+promptWidth)
}
style := m.Styles.Cursor
style := m.styles.Cursor
c := tea.NewCursor(xOffset, 0)
c.Blink = style.Blink
c.Color = style.Color
@@ -906,18 +938,19 @@ func (m Model) Cursor() *tea.Cursor {
// updateVirtualCursorStyle sets styling on the virtual cursor based on the
// textarea's style settings.
func (m *Model) updateVirtualCursorStyle() {
if !m.VirtualCursor {
if !m.useVirtualCursor {
// Hide the virtual cursor if we're using a real cursor.
m.virtualCursor.SetMode(cursor.CursorHide)
return
}
m.virtualCursor.Style = lipgloss.NewStyle().Foreground(m.Styles.Cursor.Color)
m.virtualCursor.Style = lipgloss.NewStyle().Foreground(m.styles.Cursor.Color)
// By default, the blink speed of the cursor is set to a default
// internally.
if m.Styles.Cursor.Blink {
if m.Styles.Cursor.BlinkSpeed > 0 {
m.virtualCursor.BlinkSpeed = m.Styles.Cursor.BlinkSpeed
if m.styles.Cursor.Blink {
if m.styles.Cursor.BlinkSpeed > 0 {
m.virtualCursor.BlinkSpeed = m.styles.Cursor.BlinkSpeed
}
m.virtualCursor.SetMode(cursor.CursorBlink)
return
@@ -929,7 +962,7 @@ func (m *Model) updateVirtualCursorStyle() {
// whether the textarea is focused or blurred.
func (m Model) activeStyle() *StyleState {
if m.focus {
return &m.Styles.Focused
return &m.styles.Focused
}
return &m.Styles.Blurred
return &m.styles.Blurred
}
+1 -1
View File
@@ -374,7 +374,7 @@ func TestRightOverscroll(t *testing.T) {
m := New(WithHeight(5), WithWidth(len(content)+1))
m.SetContent(content)
for i := 0; i < 10; i++ {
for range 10 {
m.ScrollRight(m.horizontalStep)
}