diff --git a/go.mod b/go.mod index d796df3..93fb591 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/Eun/bubbleviews go 1.19 require ( - github.com/charmbracelet/bubbles v0.14.0 + github.com/charmbracelet/bubbles v0.15.0 github.com/charmbracelet/bubbletea v0.23.2 github.com/charmbracelet/lipgloss v0.7.1 github.com/muesli/reflow v0.3.0 diff --git a/go.sum b/go.sum index f04b1a6..de981b8 100644 --- a/go.sum +++ b/go.sum @@ -1,15 +1,16 @@ github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= +github.com/aymanbagabas/go-osc52 v1.0.3/go.mod h1:zT8H+Rk4VSabYN90pWyugflM3ZhpTZNC7cASDfUCdT4= github.com/aymanbagabas/go-osc52 v1.2.1/go.mod h1:zT8H+Rk4VSabYN90pWyugflM3ZhpTZNC7cASDfUCdT4= github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= -github.com/charmbracelet/bubbles v0.14.0 h1:DJfCwnARfWjZLvMglhSQzo76UZ2gucuHPy9jLWX45Og= -github.com/charmbracelet/bubbles v0.14.0/go.mod h1:bbeTiXwPww4M031aGi8UK2HT9RDWoiNibae+1yCMtcc= -github.com/charmbracelet/bubbletea v0.21.0/go.mod h1:GgmJMec61d08zXsOhqRC/AiOx4K4pmz+VIcRIm1FKr4= +github.com/charmbracelet/bubbles v0.15.0 h1:c5vZ3woHV5W2b8YZI1q7v4ZNQaPetfHuoHzx+56Z6TI= +github.com/charmbracelet/bubbles v0.15.0/go.mod h1:Y7gSFbBzlMpUDR/XM9MhZI374Q+1p1kluf1uLl8iK74= +github.com/charmbracelet/bubbletea v0.23.1/go.mod h1:JAfGK/3/pPKHTnAS8JIE2u9f61BjWTQY57RbT25aMXU= github.com/charmbracelet/bubbletea v0.23.2 h1:vuUJ9HJ7b/COy4I30e8xDVQ+VRDUEFykIjryPfgsdps= github.com/charmbracelet/bubbletea v0.23.2/go.mod h1:FaP3WUivcTM0xOKNmhciz60M6I+weYLF76mr1JyI7sM= github.com/charmbracelet/harmonica v0.2.0/go.mod h1:KSri/1RMQOZLbw7AHqgcBycp8pgJnQMYYT8QZRqZ1Ao= -github.com/charmbracelet/lipgloss v0.5.0/go.mod h1:EZLha/HbzEt7cYqdFPovlqy5FZPj0xFhg5SaqxScmgs= +github.com/charmbracelet/lipgloss v0.6.0/go.mod h1:tHh2wr34xcHjC2HCXIlGSG1jaDF0S0atAUvBMP6Ppuk= github.com/charmbracelet/lipgloss v0.7.1 h1:17WMwi7N1b1rVWOjMT+rCh7sQkvDU75B2hbZpc5Kc1E= github.com/charmbracelet/lipgloss v0.7.1/go.mod h1:yG0k3giv8Qj8edTCbbg6AlQ5e8KNWpFujkNawKNhE2c= github.com/containerd/console v1.0.3 h1:lIr7SlA5PxZyMV30bDW0MGbiOPXwc63yRuCP0ARubLw= @@ -19,6 +20,7 @@ github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+ github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng= github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= @@ -31,14 +33,13 @@ github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b/go.mod h1:fQuZ0gauxyBcmsdE3ZT4NasjaRdxmbCS0jRHsrWu3Ho= github.com/muesli/ansi v0.0.0-20221106050444-61f0cd9a192a h1:jlDOeO5TU0pYlbc/y6PFguab5IjANI0Knrpg3u/ton4= github.com/muesli/ansi v0.0.0-20221106050444-61f0cd9a192a/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo= -github.com/muesli/cancelreader v0.2.0/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= github.com/muesli/reflow v0.2.1-0.20210115123740-9e1d0d53df68/go.mod h1:Xk+z4oIWdQqJzsxyjgl3P22oYZnHdZ8FFTHAQQt5BMQ= github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s= github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8= github.com/muesli/termenv v0.11.1-0.20220204035834-5ac8409525e0/go.mod h1:Bd5NYQ7pd+SrtBSrSNoBBmXlcY8+Xj4BMJgh8qcZrvs= -github.com/muesli/termenv v0.11.1-0.20220212125758-44cd13922739/go.mod h1:Bd5NYQ7pd+SrtBSrSNoBBmXlcY8+Xj4BMJgh8qcZrvs= +github.com/muesli/termenv v0.13.0/go.mod h1:sP1+uffeLaEYpyOTb8pLCUctGcGLnoFjSn4YJK5e2bc= github.com/muesli/termenv v0.14.0/go.mod h1:kG/pF1E7fh949Xhe156crRUrHNyK221IuGO7Ez60Uc8= github.com/muesli/termenv v0.15.1 h1:UzuTb/+hhlBugQz28rpzey4ZuKcZ03MeKsoG7IJZIxs= github.com/muesli/termenv v0.15.1/go.mod h1:HeAQPTzpfs016yGtA4g00CsdYnVLJvxsS4ANqrZs2sQ= @@ -54,7 +55,6 @@ golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220204135822-1c1b9b1eba6a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= diff --git a/vendor/github.com/charmbracelet/bubbles/cursor/cursor.go b/vendor/github.com/charmbracelet/bubbles/cursor/cursor.go new file mode 100644 index 0000000..cb246d5 --- /dev/null +++ b/vendor/github.com/charmbracelet/bubbles/cursor/cursor.go @@ -0,0 +1,207 @@ +package cursor + +import ( + "context" + "time" + + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" +) + +const defaultBlinkSpeed = time.Millisecond * 530 + +// initialBlinkMsg initializes cursor blinking. +type initialBlinkMsg struct{} + +// BlinkMsg signals that the cursor should blink. It contains metadata that +// allows us to tell if the blink message is the one we're expecting. +type BlinkMsg struct { + id int + tag int +} + +// blinkCanceled is sent when a blink operation is canceled. +type blinkCanceled struct{} + +// blinkCtx manages cursor blinking. +type blinkCtx struct { + ctx context.Context + cancel context.CancelFunc +} + +// Mode describes the behavior of the cursor. +type Mode int + +// Available cursor modes. +const ( + CursorBlink Mode = iota + CursorStatic + CursorHide +) + +// String returns the cursor mode in a human-readable format. This method is +// provisional and for informational purposes only. +func (c Mode) String() string { + return [...]string{ + "blink", + "static", + "hidden", + }[c] +} + +// Model is the Bubble Tea model for this cursor element. +type Model struct { + BlinkSpeed time.Duration + // Style for styling the cursor block. + Style lipgloss.Style + // TextStyle is the style used for the cursor when it is hidden (when blinking). + // I.e. displaying normal text. + TextStyle lipgloss.Style + + // char is the character under the cursor + char string + // The ID of this Model as it relates to other cursors + id int + // focus indicates whether the containing input is focused + focus bool + // Cursor Blink state. + Blink bool + // Used to manage cursor blink + blinkCtx *blinkCtx + // The ID of the blink message we're expecting to receive. + blinkTag int + // mode determines the behavior of the cursor + mode Mode +} + +// New creates a new model with default settings. +func New() Model { + return Model{ + BlinkSpeed: defaultBlinkSpeed, + + Blink: true, + mode: CursorBlink, + + blinkCtx: &blinkCtx{ + ctx: context.Background(), + }, + } +} + +// Update updates the cursor. +func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) { + switch msg := msg.(type) { + case initialBlinkMsg: + // We accept all initialBlinkMsgs generated by the Blink command. + + if m.mode != CursorBlink || !m.focus { + return m, nil + } + + cmd := m.BlinkCmd() + return m, cmd + + case BlinkMsg: + // We're choosy about whether to accept blinkMsgs so that our cursor + // only exactly when it should. + + // Is this model blink-able? + if m.mode != CursorBlink || !m.focus { + return m, nil + } + + // Were we expecting this blink message? + if msg.id != m.id || msg.tag != m.blinkTag { + return m, nil + } + + var cmd tea.Cmd + if m.mode == CursorBlink { + m.Blink = !m.Blink + cmd = m.BlinkCmd() + } + return m, cmd + + case blinkCanceled: // no-op + return m, nil + } + return m, nil +} + +// Mode returns the model's cursor mode. For available cursor modes, see +// type Mode. +func (m Model) Mode() Mode { + return m.mode +} + +// SetMode sets the model's cursor mode. This method returns a command. +// +// For available cursor modes, see type CursorMode. +func (m *Model) SetMode(mode Mode) tea.Cmd { + m.mode = mode + m.Blink = m.mode == CursorHide || !m.focus + if mode == CursorBlink { + return Blink + } + return nil +} + +// BlinkCmd is an command used to manage cursor blinking. +func (m *Model) BlinkCmd() tea.Cmd { + if m.mode != CursorBlink { + return nil + } + + if m.blinkCtx != nil && m.blinkCtx.cancel != nil { + m.blinkCtx.cancel() + } + + ctx, cancel := context.WithTimeout(m.blinkCtx.ctx, m.BlinkSpeed) + m.blinkCtx.cancel = cancel + + m.blinkTag++ + + return func() tea.Msg { + defer cancel() + <-ctx.Done() + if ctx.Err() == context.DeadlineExceeded { + return BlinkMsg{id: m.id, tag: m.blinkTag} + } + return blinkCanceled{} + } +} + +// Blink is a command used to initialize cursor blinking. +func Blink() tea.Msg { + return initialBlinkMsg{} +} + +// 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 + + if m.mode == CursorBlink && m.focus { + return m.BlinkCmd() + } + return nil +} + +// Blur blurs the cursor. +func (m *Model) Blur() { + m.focus = false + m.Blink = true +} + +// SetChar sets the character under the cursor. +func (m *Model) SetChar(char string) { + m.char = char +} + +// View displays the cursor. +func (m Model) View() string { + if m.Blink { + return m.TextStyle.Inline(true).Render(m.char) + } + return m.Style.Inline(true).Reverse(true).Render(m.char) +} diff --git a/vendor/github.com/charmbracelet/bubbles/help/help.go b/vendor/github.com/charmbracelet/bubbles/help/help.go index 90971ac..50d094d 100644 --- a/vendor/github.com/charmbracelet/bubbles/help/help.go +++ b/vendor/github.com/charmbracelet/bubbles/help/help.go @@ -92,7 +92,7 @@ func New() Model { // NewModel creates a new help view with some useful defaults. // -// Deprecated. Use New instead. +// Deprecated: use [New] instead. var NewModel = New // Update helps satisfy the Bubble Tea Model interface. It's a no-op. @@ -203,7 +203,7 @@ func (m Model) FullHelpView(groups [][]key.Binding) string { // Column totalWidth += lipgloss.Width(col) - if totalWidth > m.Width { + if m.Width > 0 && totalWidth > m.Width { break } @@ -212,7 +212,7 @@ func (m Model) FullHelpView(groups [][]key.Binding) string { // Separator if i < len(group)-1 { totalWidth += sepWidth - if totalWidth > m.Width { + if m.Width > 0 && totalWidth > m.Width { break } } diff --git a/vendor/github.com/charmbracelet/bubbles/key/key.go b/vendor/github.com/charmbracelet/bubbles/key/key.go index ac08194..c7888fa 100644 --- a/vendor/github.com/charmbracelet/bubbles/key/key.go +++ b/vendor/github.com/charmbracelet/bubbles/key/key.go @@ -2,35 +2,35 @@ // keymappings useful in Bubble Tea components. There are a few different ways // you can define a keymapping with this package. Here's one example: // -// type KeyMap struct { -// Up key.Binding -// Down key.Binding -// } +// type KeyMap struct { +// Up key.Binding +// Down key.Binding +// } // -// var DefaultKeyMap = KeyMap{ -// Up: key.NewBinding( -// key.WithKeys("k", "up"), // actual keybindings -// key.WithHelp("↑/k", "move up"), // corresponding help text -// ), -// Down: key.NewBinding( -// key.WithKeys("j", "down"), -// key.WithHelp("↓/j", "move down"), -// ), -// } +// var DefaultKeyMap = KeyMap{ +// Up: key.NewBinding( +// key.WithKeys("k", "up"), // actual keybindings +// key.WithHelp("↑/k", "move up"), // corresponding help text +// ), +// Down: key.NewBinding( +// key.WithKeys("j", "down"), +// key.WithHelp("↓/j", "move down"), +// ), +// } // -// func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { -// switch msg := msg.(type) { -// case tea.KeyMsg: -// switch { -// case key.Matches(msg, DefaultKeyMap.Up): -// // The user pressed up -// case key.Matches(msg, DefaultKeyMap.Down): -// // The user pressed down -// } -// } +// func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { +// switch msg := msg.(type) { +// case tea.KeyMsg: +// switch { +// case key.Matches(msg, DefaultKeyMap.Up): +// // The user pressed up +// case key.Matches(msg, DefaultKeyMap.Down): +// // The user pressed down +// } +// } // -// // ... -// } +// // ... +// } // // The help information, which is not used in the example above, can be used // to render help text for keystrokes in your views. diff --git a/vendor/github.com/charmbracelet/bubbles/list/list.go b/vendor/github.com/charmbracelet/bubbles/list/list.go index bec5e6f..68d0f27 100644 --- a/vendor/github.com/charmbracelet/bubbles/list/list.go +++ b/vendor/github.com/charmbracelet/bubbles/list/list.go @@ -183,18 +183,18 @@ type Model struct { func New(items []Item, delegate ItemDelegate, width, height int) Model { styles := DefaultStyles() - sp := spinner.NewModel() + sp := spinner.New() sp.Spinner = spinner.Line sp.Style = styles.Spinner - filterInput := textinput.NewModel() + filterInput := textinput.New() filterInput.Prompt = "Filter: " filterInput.PromptStyle = styles.FilterPrompt filterInput.CursorStyle = styles.FilterCursor filterInput.CharLimit = 64 filterInput.Focus() - p := paginator.NewModel() + p := paginator.New() p.Type = paginator.Dots p.ActiveDot = styles.ActivePaginationDot.String() p.InactiveDot = styles.InactivePaginationDot.String() @@ -221,7 +221,7 @@ func New(items []Item, delegate ItemDelegate, width, height int) Model { items: items, Paginator: p, spinner: sp, - Help: help.NewModel(), + Help: help.New(), } m.updatePagination() @@ -231,7 +231,7 @@ func New(items []Item, delegate ItemDelegate, width, height int) Model { // NewModel returns a new model with sensible defaults. // -// Deprecated. Use New instead. +// Deprecated: use [New] instead. var NewModel = New // SetFilteringEnabled enables or disables filtering. Note that this is different @@ -526,7 +526,7 @@ func (m Model) FilterValue() string { // SettingFilter returns whether or not the user is currently editing the // filter value. It's purely a convenience method for the following: // -// m.FilterState() == Filtering +// m.FilterState() == Filtering // // It's included here because it's a common thing to check for when // implementing this component. @@ -537,8 +537,7 @@ func (m Model) SettingFilter() bool { // IsFiltered returns whether or not the list is currently filtered. // It's purely a convenience method for the following: // -// m.FilterState() == FilterApplied -// +// m.FilterState() == FilterApplied func (m Model) IsFiltered() bool { return m.filterState == FilterApplied } @@ -570,7 +569,7 @@ func (m *Model) ToggleSpinner() tea.Cmd { // StartSpinner starts the spinner. Note that this returns a command. func (m *Model) StartSpinner() tea.Cmd { m.showSpinner = true - return spinner.Tick + return m.spinner.Tick } // StopSpinner stops the spinner. diff --git a/vendor/github.com/charmbracelet/bubbles/paginator/paginator.go b/vendor/github.com/charmbracelet/bubbles/paginator/paginator.go index a84819a..17a1950 100644 --- a/vendor/github.com/charmbracelet/bubbles/paginator/paginator.go +++ b/vendor/github.com/charmbracelet/bubbles/paginator/paginator.go @@ -7,6 +7,7 @@ package paginator import ( "fmt" + "github.com/charmbracelet/bubbles/key" tea "github.com/charmbracelet/bubbletea" ) @@ -19,20 +20,49 @@ const ( Dots ) +// KeyMap is the key bindings for different actions within the paginator. +type KeyMap struct { + PrevPage key.Binding + NextPage key.Binding +} + +// DefaultKeyMap is the default set of key bindings for navigating and acting +// upon the paginator. +var DefaultKeyMap = KeyMap{ + PrevPage: key.NewBinding(key.WithKeys("pgup", "left", "h")), + NextPage: key.NewBinding(key.WithKeys("pgdown", "right", "l")), +} + // Model is the Bubble Tea model for this user interface. type Model struct { - Type Type - Page int - PerPage int - TotalPages int - ActiveDot string - InactiveDot string - ArabicFormat string + // Type configures how the pagination is rendered (Arabic, Dots). + Type Type + // Page is the current page number. + Page int + // PerPage is the number of items per page. + PerPage int + // TotalPages is the total number of pages. + TotalPages int + // ActiveDot is used to mark the current page under the Dots display type. + ActiveDot string + // InactiveDot is used to mark inactive pages under the Dots display type. + InactiveDot string + // ArabicFormat is the printf-style format to use for the Arabic display type. + ArabicFormat string + + // KeyMap encodes the keybindings recognized by the widget. + KeyMap KeyMap + + // Deprecated: customize [KeyMap] instead. UsePgUpPgDownKeys bool - UseLeftRightKeys bool - UseUpDownKeys bool - UseHLKeys bool - UseJKKeys bool + // Deprecated: customize [KeyMap] instead. + UseLeftRightKeys bool + // Deprecated: customize [KeyMap] instead. + UseUpDownKeys bool + // Deprecated: customize [KeyMap] instead. + UseHLKeys bool + // Deprecated: customize [KeyMap] instead. + UseJKKeys bool } // SetTotalPages is a helper function for calculating the total number of pages @@ -51,8 +81,8 @@ func (m *Model) SetTotalPages(items int) int { return n } -// ItemsOnPage is a helper function for returning the numer of items on the -// current page given the total numer of items passed as an argument. +// ItemsOnPage is a helper function for returning the number of items on the +// current page given the total number of items passed as an argument. func (m Model) ItemsOnPage(totalItems int) int { if totalItems < 1 { return 0 @@ -65,10 +95,9 @@ func (m Model) ItemsOnPage(totalItems int) int { // of the slice you're rendering and you'll receive the start and end bounds // corresponding the to pagination. For example: // -// bunchOfStuff := []stuff{...} -// start, end := model.GetSliceBounds(len(bunchOfStuff)) -// sliceToRender := bunchOfStuff[start:end] -// +// bunchOfStuff := []stuff{...} +// start, end := model.GetSliceBounds(len(bunchOfStuff)) +// sliceToRender := bunchOfStuff[start:end] func (m *Model) GetSliceBounds(length int) (start int, end int) { start = m.Page * m.PerPage end = min(m.Page*m.PerPage+m.PerPage, length) @@ -99,69 +128,31 @@ func (m Model) OnLastPage() bool { // New creates a new model with defaults. func New() Model { return Model{ - Type: Arabic, - Page: 0, - PerPage: 1, - TotalPages: 1, - ActiveDot: "•", - InactiveDot: "○", - ArabicFormat: "%d/%d", - UsePgUpPgDownKeys: true, - UseLeftRightKeys: true, - UseUpDownKeys: false, - UseHLKeys: true, - UseJKKeys: false, + Type: Arabic, + Page: 0, + PerPage: 1, + TotalPages: 1, + KeyMap: DefaultKeyMap, + ActiveDot: "•", + InactiveDot: "○", + ArabicFormat: "%d/%d", } } // NewModel creates a new model with defaults. // -// Deprecated. Use New instead. +// Deprecated: use [New] instead. var NewModel = New // Update is the Tea update function which binds keystrokes to pagination. func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) { switch msg := msg.(type) { case tea.KeyMsg: - if m.UsePgUpPgDownKeys { - switch msg.String() { - case "pgup": - m.PrevPage() - case "pgdown": - m.NextPage() - } - } - if m.UseLeftRightKeys { - switch msg.String() { - case "left": - m.PrevPage() - case "right": - m.NextPage() - } - } - if m.UseUpDownKeys { - switch msg.String() { - case "up": - m.PrevPage() - case "down": - m.NextPage() - } - } - if m.UseHLKeys { - switch msg.String() { - case "h": - m.PrevPage() - case "l": - m.NextPage() - } - } - if m.UseJKKeys { - switch msg.String() { - case "j": - m.PrevPage() - case "k": - m.NextPage() - } + switch { + case key.Matches(msg, m.KeyMap.NextPage): + m.NextPage() + case key.Matches(msg, m.KeyMap.PrevPage): + m.PrevPage() } } diff --git a/vendor/github.com/charmbracelet/bubbles/runeutil/runeutil.go b/vendor/github.com/charmbracelet/bubbles/runeutil/runeutil.go new file mode 100644 index 0000000..3cef78a --- /dev/null +++ b/vendor/github.com/charmbracelet/bubbles/runeutil/runeutil.go @@ -0,0 +1,102 @@ +// Package runeutil provides a utility function for use in Bubbles +// that can process Key messages containing runes. +package runeutil + +import ( + "unicode" + "unicode/utf8" +) + +// Sanitizer is a helper for bubble widgets that want to process +// Runes from input key messages. +type Sanitizer interface { + // Sanitize removes control characters from runes in a KeyRunes + // message, and optionally replaces newline/carriage return/tabs by a + // specified character. + // + // The rune array is modified in-place if possible. In that case, the + // returned slice is the original slice shortened after the control + // characters have been removed/translated. + Sanitize(runes []rune) []rune +} + +// NewSanitizer constructs a rune sanitizer. +func NewSanitizer(opts ...Option) Sanitizer { + s := sanitizer{ + replaceNewLine: []rune("\n"), + replaceTab: []rune(" "), + } + for _, o := range opts { + s = o(s) + } + return &s +} + +// Option is the type of an option that can be passed to Sanitize(). +type Option func(sanitizer) sanitizer + +// ReplaceTabs replaces tabs by the specified string. +func ReplaceTabs(tabRepl string) Option { + return func(s sanitizer) sanitizer { + s.replaceTab = []rune(tabRepl) + return s + } +} + +// ReplaceNewlines replaces newline characters by the specified string. +func ReplaceNewlines(nlRepl string) Option { + return func(s sanitizer) sanitizer { + s.replaceNewLine = []rune(nlRepl) + return s + } +} + +func (s *sanitizer) Sanitize(runes []rune) []rune { + // dstrunes are where we are storing the result. + dstrunes := runes[:0:len(runes)] + // copied indicates whether dstrunes is an alias of runes + // or a copy. We need a copy when dst moves past src. + // We use this as an optimization to avoid allocating + // a new rune slice in the common case where the output + // is smaller or equal to the input. + copied := false + + for src := 0; src < len(runes); src++ { + r := runes[src] + switch { + case r == utf8.RuneError: + // skip + + case r == '\r' || r == '\n': + if len(dstrunes)+len(s.replaceNewLine) > src && !copied { + dst := len(dstrunes) + dstrunes = make([]rune, dst, len(runes)+len(s.replaceNewLine)) + copy(dstrunes, runes[:dst]) + copied = true + } + dstrunes = append(dstrunes, s.replaceNewLine...) + + case r == '\t': + if len(dstrunes)+len(s.replaceTab) > src && !copied { + dst := len(dstrunes) + dstrunes = make([]rune, dst, len(runes)+len(s.replaceTab)) + copy(dstrunes, runes[:dst]) + copied = true + } + dstrunes = append(dstrunes, s.replaceTab...) + + case unicode.IsControl(r): + // Other control characters: skip. + + default: + // Keep the character. + dstrunes = append(dstrunes, runes[src]) + } + } + return dstrunes +} + +type sanitizer struct { + replaceNewLine []rune + replaceTab []rune +} diff --git a/vendor/github.com/charmbracelet/bubbles/spinner/spinner.go b/vendor/github.com/charmbracelet/bubbles/spinner/spinner.go index e034112..bde7b7d 100644 --- a/vendor/github.com/charmbracelet/bubbles/spinner/spinner.go +++ b/vendor/github.com/charmbracelet/bubbles/spinner/spinner.go @@ -85,7 +85,7 @@ var ( } ) -// Model contains the state for the spinner. Use NewModel to create new models +// Model contains the state for the spinner. Use New to create new models // rather than using Model as a struct literal. type Model struct { // Spinner settings to use. See type Spinner. @@ -124,7 +124,7 @@ func New(opts ...Option) Model { // NewModel returns a model with default values. // -// Deprecated. Use New instead. +// Deprecated: use [New] instead. var NewModel = New // TickMsg indicates that the timer has ticked and we should render a frame. @@ -201,15 +201,14 @@ func (m Model) tick(id, tag int) tea.Cmd { // Tick is the command used to advance the spinner one frame. Use this command // to effectively start the spinner. // -// This method is deprecated. Use Model.Tick instead. +// Deprecated: Use [Model.Tick] instead. func Tick() tea.Msg { return TickMsg{Time: time.Now()} } // Option is used to set options in New. For example: // -// spinner := New(WithSpinner(Dot)) -// +// spinner := New(WithSpinner(Dot)) type Option func(*Model) // WithSpinner is an option to set the spinner. diff --git a/vendor/github.com/charmbracelet/bubbles/textinput/textinput.go b/vendor/github.com/charmbracelet/bubbles/textinput/textinput.go index 9692ed7..bfa7009 100644 --- a/vendor/github.com/charmbracelet/bubbles/textinput/textinput.go +++ b/vendor/github.com/charmbracelet/bubbles/textinput/textinput.go @@ -1,48 +1,19 @@ package textinput import ( - "context" "strings" - "sync" "time" "unicode" "github.com/atotto/clipboard" + "github.com/charmbracelet/bubbles/cursor" + "github.com/charmbracelet/bubbles/key" + "github.com/charmbracelet/bubbles/runeutil" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" rw "github.com/mattn/go-runewidth" ) -const defaultBlinkSpeed = time.Millisecond * 530 - -// Internal ID management for text inputs. Necessary for blink integrity when -// multiple text inputs are involved. -var ( - lastID int - idMtx sync.Mutex -) - -// Return the next ID we should use on the Model. -func nextID() int { - idMtx.Lock() - defer idMtx.Unlock() - lastID++ - return lastID -} - -// initialBlinkMsg initializes cursor blinking. -type initialBlinkMsg struct{} - -// blinkMsg signals that the cursor should blink. It contains metadata that -// allows us to tell if the blink message is the one we're expecting. -type blinkMsg struct { - id int - tag int -} - -// blinkCanceled is sent when a blink operation is canceled. -type blinkCanceled struct{} - // Internal messages for clipboard operations. type pasteMsg string type pasteErrMsg struct{ error } @@ -65,35 +36,44 @@ const ( // EchoOnEdit. ) -// blinkCtx manages cursor blinking. -type blinkCtx struct { - ctx context.Context - cancel context.CancelFunc -} - -// CursorMode describes the behavior of the cursor. -type CursorMode int - -// Available cursor modes. -const ( - CursorBlink CursorMode = iota - CursorStatic - CursorHide -) - -// String returns a the cursor mode in a human-readable format. This method is -// provisional and for informational purposes only. -func (c CursorMode) String() string { - return [...]string{ - "blink", - "static", - "hidden", - }[c] -} - // ValidateFunc is a function that returns an error if the input is invalid. type ValidateFunc func(string) error +// KeyMap is the key bindings for different actions within the textinput. +type KeyMap struct { + CharacterForward key.Binding + CharacterBackward key.Binding + WordForward key.Binding + WordBackward key.Binding + DeleteWordBackward key.Binding + DeleteWordForward key.Binding + DeleteAfterCursor key.Binding + DeleteBeforeCursor key.Binding + DeleteCharacterBackward key.Binding + DeleteCharacterForward key.Binding + LineStart key.Binding + LineEnd key.Binding + Paste key.Binding +} + +// DefaultKeyMap is the default set of key bindings for navigating and acting +// upon the textinput. +var DefaultKeyMap = KeyMap{ + CharacterForward: key.NewBinding(key.WithKeys("right", "ctrl+f")), + CharacterBackward: key.NewBinding(key.WithKeys("left", "ctrl+b")), + WordForward: key.NewBinding(key.WithKeys("alt+right", "alt+f")), + WordBackward: key.NewBinding(key.WithKeys("alt+left", "alt+b")), + DeleteWordBackward: key.NewBinding(key.WithKeys("alt+backspace", "ctrl+w")), + DeleteWordForward: key.NewBinding(key.WithKeys("alte+delete", "alt+d")), + DeleteAfterCursor: key.NewBinding(key.WithKeys("ctrl+k")), + DeleteBeforeCursor: key.NewBinding(key.WithKeys("ctrl+u")), + DeleteCharacterBackward: key.NewBinding(key.WithKeys("backspace", "ctrl+h")), + DeleteCharacterForward: key.NewBinding(key.WithKeys("delete", "ctrl+d")), + LineStart: key.NewBinding(key.WithKeys("home", "ctrl+a")), + LineEnd: key.NewBinding(key.WithKeys("end", "ctrl+e")), + Paste: key.NewBinding(key.WithKeys("ctrl+v")), +} + // Model is the Bubble Tea model for this text input element. type Model struct { Err error @@ -101,9 +81,12 @@ type Model struct { // General settings. Prompt string Placeholder string - BlinkSpeed time.Duration EchoMode EchoMode EchoCharacter rune + Cursor cursor.Model + + // Deprecated: use [cursor.BlinkSpeed] instead. + BlinkSpeed time.Duration // Styles. These will be applied as inline styles. // @@ -124,11 +107,8 @@ type Model struct { // viewport. If 0 or less this setting is ignored. Width int - // The ID of this Model as it relates to other textinput Models. - id int - - // The ID of the blink message we're expecting to receive. - blinkTag int + // KeyMap encodes the keybindings recognized by the widget. + KeyMap KeyMap // Underlying text value. value []rune @@ -137,9 +117,6 @@ type Model struct { // component. When false, ignore keyboard input and hide the cursor. focus bool - // Cursor blink state. - blink bool - // Cursor position. pos int @@ -148,65 +125,63 @@ type Model struct { offset int offsetRight int - // Used to manage cursor blink - blinkCtx *blinkCtx - - // cursorMode determines the behavior of the cursor - cursorMode CursorMode - // Validate is a function that checks whether or not the text within the // input is valid. If it is not valid, the `Err` field will be set to the // error returned by the function. If the function is not defined, all // input is considered valid. Validate ValidateFunc + + // rune sanitizer for input. + rsan runeutil.Sanitizer } // New creates a new model with default settings. func New() Model { return Model{ Prompt: "> ", - BlinkSpeed: defaultBlinkSpeed, EchoCharacter: '*', CharLimit: 0, PlaceholderStyle: lipgloss.NewStyle().Foreground(lipgloss.Color("240")), + Cursor: cursor.New(), + KeyMap: DefaultKeyMap, - id: nextID(), - value: nil, - focus: false, - blink: true, - pos: 0, - cursorMode: CursorBlink, - - blinkCtx: &blinkCtx{ - ctx: context.Background(), - }, + value: nil, + focus: false, + pos: 0, } } // NewModel creates a new model with default settings. // -// Deprecated. Use New instead. +// Deprecated: Use [New] instead. var NewModel = New // SetValue sets the value of the text input. func (m *Model) SetValue(s string) { + // Clean up any special characters in the input provided by the + // caller. This avoids bugs due to e.g. tab characters and whatnot. + runes := m.san().Sanitize([]rune(s)) + m.setValueInternal(runes) +} + +func (m *Model) setValueInternal(runes []rune) { if m.Validate != nil { - if err := m.Validate(s); err != nil { + if err := m.Validate(string(runes)); err != nil { m.Err = err return } } + empty := len(m.value) == 0 m.Err = nil - runes := []rune(s) if m.CharLimit > 0 && len(runes) > m.CharLimit { m.value = runes[:m.CharLimit] } else { m.value = runes } - if m.pos == 0 || m.pos > len(m.value) { - m.setCursor(len(m.value)) + if (m.pos == 0 && empty) || m.pos > len(m.value) { + m.SetCursor(len(m.value)) } m.handleOverflow() } @@ -216,74 +191,26 @@ func (m Model) Value() string { return string(m.value) } -// Cursor returns the cursor position. -func (m Model) Cursor() int { +// Position returns the cursor position. +func (m Model) Position() int { return m.pos } -// Blink returns whether or not to draw the cursor. -func (m Model) Blink() bool { - return m.blink -} - // SetCursor moves the cursor to the given position. If the position is // out of bounds the cursor will be moved to the start or end accordingly. func (m *Model) SetCursor(pos int) { - m.setCursor(pos) -} - -// setCursor moves the cursor to the given position and returns whether or not -// the cursor blink should be reset. If the position is out of bounds the -// cursor will be moved to the start or end accordingly. -func (m *Model) setCursor(pos int) bool { m.pos = clamp(pos, 0, len(m.value)) m.handleOverflow() - - // Show the cursor unless it's been explicitly hidden - m.blink = m.cursorMode == CursorHide - - // Reset cursor blink if necessary - return m.cursorMode == CursorBlink } // CursorStart moves the cursor to the start of the input field. func (m *Model) CursorStart() { - m.cursorStart() -} - -// cursorStart moves the cursor to the start of the input field and returns -// whether or not the curosr blink should be reset. -func (m *Model) cursorStart() bool { - return m.setCursor(0) + m.SetCursor(0) } // CursorEnd moves the cursor to the end of the input field. func (m *Model) CursorEnd() { - m.cursorEnd() -} - -// CursorMode returns the model's cursor mode. For available cursor modes, see -// type CursorMode. -func (m Model) CursorMode() CursorMode { - return m.cursorMode -} - -// SetCursorMode sets the model's cursor mode. This method returns a command. -// -// For available cursor modes, see type CursorMode. -func (m *Model) SetCursorMode(mode CursorMode) tea.Cmd { - m.cursorMode = mode - m.blink = m.cursorMode == CursorHide || !m.focus - if mode == CursorBlink { - return Blink - } - return nil -} - -// cursorEnd moves the cursor to the end of the input field and returns whether -// the cursor should blink should reset. -func (m *Model) cursorEnd() bool { - return m.setCursor(len(m.value)) + m.SetCursor(len(m.value)) } // Focused returns the focus state on the model. @@ -292,50 +219,56 @@ func (m Model) Focused() bool { } // Focus sets the focus state on the model. When the model is in focus it can -// receive keyboard input and the cursor will be hidden. +// receive keyboard input and the cursor will be shown. func (m *Model) Focus() tea.Cmd { m.focus = true - m.blink = m.cursorMode == CursorHide // show the cursor unless we've explicitly hidden it - - if m.cursorMode == CursorBlink && m.focus { - return m.blinkCmd() - } - return nil + return m.Cursor.Focus() } // Blur removes the focus state on the model. When the model is blurred it can // not receive keyboard input and the cursor will be hidden. func (m *Model) Blur() { m.focus = false - m.blink = true + m.Cursor.Blur() } -// Reset sets the input to its default state with no input. Returns whether -// or not the cursor blink should reset. -func (m *Model) Reset() bool { +// Reset sets the input to its default state with no input. +func (m *Model) Reset() { m.value = nil - return m.setCursor(0) + m.SetCursor(0) +} + +// rsan initializes or retrieves the rune sanitizer. +func (m *Model) san() runeutil.Sanitizer { + if m.rsan == nil { + // Textinput has all its input on a single line so collapse + // newlines/tabs to single spaces. + m.rsan = runeutil.NewSanitizer( + runeutil.ReplaceTabs(" "), runeutil.ReplaceNewlines(" ")) + } + return m.rsan } -// handle a clipboard paste event, if supported. Returns whether or not the -// cursor blink should reset. -func (m *Model) handlePaste(v string) bool { - paste := []rune(v) +func (m *Model) insertRunesFromUserInput(v []rune) { + // Clean up any special characters in the input provided by the + // clipboard. This avoids bugs due to e.g. tab characters and + // whatnot. + paste := m.san().Sanitize(v) var availSpace int if m.CharLimit > 0 { availSpace = m.CharLimit - len(m.value) - } - // If the char limit's been reached cancel - if m.CharLimit > 0 && availSpace <= 0 { - return false - } + // If the char limit's been reached, cancel. + if availSpace <= 0 { + return + } - // If there's not enough space to paste the whole thing cut the pasted - // runes down so they'll fit - if m.CharLimit > 0 && availSpace < len(paste) { - paste = paste[:len(paste)-availSpace] + // If there's not enough space to paste the whole thing cut the pasted + // runes down so they'll fit. + if availSpace < len(paste) { + paste = paste[:len(paste)-availSpace] + } } // Stuff before and after the cursor @@ -360,14 +293,11 @@ func (m *Model) handlePaste(v string) bool { // Put it all back together value := append(head, tail...) - m.SetValue(string(value)) + m.setValueInternal(value) if m.Err != nil { m.pos = oldPos } - - // Reset blink state if necessary and run overflow checks - return m.setCursor(m.pos) } // If a max width is defined, perform some logic to treat the visible area @@ -415,31 +345,30 @@ func (m *Model) handleOverflow() { } } -// deleteBeforeCursor deletes all text before the cursor. Returns whether or -// not the cursor blink should be reset. -func (m *Model) deleteBeforeCursor() bool { +// deleteBeforeCursor deletes all text before the cursor. +func (m *Model) deleteBeforeCursor() { m.value = m.value[m.pos:] m.offset = 0 - return m.setCursor(0) + m.SetCursor(0) } -// deleteAfterCursor deletes all text after the cursor. Returns whether or not -// the cursor blink should be reset. If input is masked delete everything after -// the cursor so as not to reveal word breaks in the masked input. -func (m *Model) deleteAfterCursor() bool { +// deleteAfterCursor deletes all text after the cursor. If input is masked +// delete everything after the cursor so as not to reveal word breaks in the +// masked input. +func (m *Model) deleteAfterCursor() { m.value = m.value[:m.pos] - return m.setCursor(len(m.value)) + m.SetCursor(len(m.value)) } -// deleteWordLeft deletes the word left to the cursor. Returns whether or not -// the cursor blink should be reset. -func (m *Model) deleteWordLeft() bool { +// deleteWordBackward deletes the word left to the cursor. +func (m *Model) deleteWordBackward() { if m.pos == 0 || len(m.value) == 0 { - return false + return } if m.EchoMode != EchoNormal { - return m.deleteBeforeCursor() + m.deleteBeforeCursor() + return } // Linter note: it's critical that we acquire the initial cursor position @@ -447,22 +376,22 @@ func (m *Model) deleteWordLeft() bool { // call into the corresponding if clause does not apply here. oldPos := m.pos //nolint:ifshort - blink := m.setCursor(m.pos - 1) + m.SetCursor(m.pos - 1) for unicode.IsSpace(m.value[m.pos]) { if m.pos <= 0 { break } // ignore series of whitespace before cursor - blink = m.setCursor(m.pos - 1) + m.SetCursor(m.pos - 1) } for m.pos > 0 { if !unicode.IsSpace(m.value[m.pos]) { - blink = m.setCursor(m.pos - 1) + m.SetCursor(m.pos - 1) } else { if m.pos > 0 { // keep the previous space - blink = m.setCursor(m.pos + 1) + m.SetCursor(m.pos + 1) } break } @@ -473,27 +402,26 @@ func (m *Model) deleteWordLeft() bool { } else { m.value = append(m.value[:m.pos], m.value[oldPos:]...) } - - return blink } -// deleteWordRight deletes the word right to the cursor. Returns whether or not -// the cursor blink should be reset. If input is masked delete everything after -// the cursor so as not to reveal word breaks in the masked input. -func (m *Model) deleteWordRight() bool { +// deleteWordForward deletes the word right to the cursor If input is masked +// delete everything after the cursor so as not to reveal word breaks in the +// masked input. +func (m *Model) deleteWordForward() { if m.pos >= len(m.value) || len(m.value) == 0 { - return false + return } if m.EchoMode != EchoNormal { - return m.deleteAfterCursor() + m.deleteAfterCursor() + return } oldPos := m.pos - m.setCursor(m.pos + 1) + m.SetCursor(m.pos + 1) for unicode.IsSpace(m.value[m.pos]) { // ignore series of whitespace after cursor - m.setCursor(m.pos + 1) + m.SetCursor(m.pos + 1) if m.pos >= len(m.value) { break @@ -502,7 +430,7 @@ func (m *Model) deleteWordRight() bool { for m.pos < len(m.value) { if !unicode.IsSpace(m.value[m.pos]) { - m.setCursor(m.pos + 1) + m.SetCursor(m.pos + 1) } else { break } @@ -514,26 +442,25 @@ func (m *Model) deleteWordRight() bool { m.value = append(m.value[:oldPos], m.value[m.pos:]...) } - return m.setCursor(oldPos) + m.SetCursor(oldPos) } -// wordLeft moves the cursor one word to the left. Returns whether or not the -// cursor blink should be reset. If input is masked, move input to the start -// so as not to reveal word breaks in the masked input. -func (m *Model) wordLeft() bool { +// wordBackward moves the cursor one word to the left. If input is masked, move +// input to the start so as not to reveal word breaks in the masked input. +func (m *Model) wordBackward() { if m.pos == 0 || len(m.value) == 0 { - return false + return } if m.EchoMode != EchoNormal { - return m.cursorStart() + m.CursorStart() + return } - blink := false i := m.pos - 1 for i >= 0 { if unicode.IsSpace(m.value[i]) { - blink = m.setCursor(m.pos - 1) + m.SetCursor(m.pos - 1) i-- } else { break @@ -542,33 +469,30 @@ func (m *Model) wordLeft() bool { for i >= 0 { if !unicode.IsSpace(m.value[i]) { - blink = m.setCursor(m.pos - 1) + m.SetCursor(m.pos - 1) i-- } else { break } } - - return blink } -// wordRight moves the cursor one word to the right. Returns whether or not the -// cursor blink should be reset. If the input is masked, move input to the end -// so as not to reveal word breaks in the masked input. -func (m *Model) wordRight() bool { +// wordForward moves the cursor one word to the right. If the input is masked, +// move input to the end so as not to reveal word breaks in the masked input. +func (m *Model) wordForward() { if m.pos >= len(m.value) || len(m.value) == 0 { - return false + return } if m.EchoMode != EchoNormal { - return m.cursorEnd() + m.CursorEnd() + return } - blink := false i := m.pos for i < len(m.value) { if unicode.IsSpace(m.value[i]) { - blink = m.setCursor(m.pos + 1) + m.SetCursor(m.pos + 1) i++ } else { break @@ -577,14 +501,12 @@ func (m *Model) wordRight() bool { for i < len(m.value) { if !unicode.IsSpace(m.value[i]) { - blink = m.setCursor(m.pos + 1) + m.SetCursor(m.pos + 1) i++ } else { break } } - - return blink } func (m Model) echoTransform(v string) string { @@ -602,138 +524,82 @@ func (m Model) echoTransform(v string) string { // Update is the Bubble Tea update loop. func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) { if !m.focus { - m.blink = true return m, nil } - var resetBlink bool + // Let's remember where the position of the cursor currently is so that if + // the cursor position changes, we can reset the blink. + oldPos := m.pos //nolint switch msg := msg.(type) { case tea.KeyMsg: - switch msg.Type { - case tea.KeyBackspace, tea.KeyCtrlH: // delete character before cursor + switch { + case key.Matches(msg, m.KeyMap.DeleteWordBackward): m.Err = nil - - if msg.Alt { - resetBlink = m.deleteWordLeft() - } else { - if len(m.value) > 0 { - m.value = append(m.value[:max(0, m.pos-1)], m.value[m.pos:]...) - if m.pos > 0 { - resetBlink = m.setCursor(m.pos - 1) - } + m.deleteWordBackward() + case key.Matches(msg, m.KeyMap.DeleteCharacterBackward): + m.Err = nil + if len(m.value) > 0 { + m.value = append(m.value[:max(0, m.pos-1)], m.value[m.pos:]...) + if m.pos > 0 { + m.SetCursor(m.pos - 1) } } - case tea.KeyLeft, tea.KeyCtrlB: - if msg.Alt { // alt+left arrow, back one word - resetBlink = m.wordLeft() - break - } - if m.pos > 0 { // left arrow, ^F, back one character - resetBlink = m.setCursor(m.pos - 1) - } - case tea.KeyRight, tea.KeyCtrlF: - if msg.Alt { // alt+right arrow, forward one word - resetBlink = m.wordRight() - break + case key.Matches(msg, m.KeyMap.WordBackward): + m.wordBackward() + case key.Matches(msg, m.KeyMap.CharacterBackward): + if m.pos > 0 { + m.SetCursor(m.pos - 1) } - if m.pos < len(m.value) { // right arrow, ^F, forward one character - resetBlink = m.setCursor(m.pos + 1) + case key.Matches(msg, m.KeyMap.WordForward): + m.wordForward() + case key.Matches(msg, m.KeyMap.CharacterForward): + if m.pos < len(m.value) { + m.SetCursor(m.pos + 1) } - case tea.KeyCtrlW: // ^W, delete word left of cursor - resetBlink = m.deleteWordLeft() - case tea.KeyHome, tea.KeyCtrlA: // ^A, go to beginning - resetBlink = m.cursorStart() - case tea.KeyDelete, tea.KeyCtrlD: // ^D, delete char under cursor + case key.Matches(msg, m.KeyMap.DeleteWordBackward): + m.deleteWordBackward() + case key.Matches(msg, m.KeyMap.LineStart): + 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:]...) } - case tea.KeyCtrlE, tea.KeyEnd: // ^E, go to end - resetBlink = m.cursorEnd() - case tea.KeyCtrlK: // ^K, kill text after cursor - resetBlink = m.deleteAfterCursor() - case tea.KeyCtrlU: // ^U, kill text before cursor - resetBlink = m.deleteBeforeCursor() - case tea.KeyCtrlV: // ^V paste + case key.Matches(msg, m.KeyMap.LineEnd): + m.CursorEnd() + case key.Matches(msg, m.KeyMap.DeleteAfterCursor): + m.deleteAfterCursor() + case key.Matches(msg, m.KeyMap.DeleteBeforeCursor): + m.deleteBeforeCursor() + case key.Matches(msg, m.KeyMap.Paste): return m, Paste - case tea.KeyRunes, tea.KeySpace: // input regular characters - if msg.Alt && len(msg.Runes) == 1 { - if msg.Runes[0] == 'd' { // alt+d, delete word right of cursor - resetBlink = m.deleteWordRight() - break - } - if msg.Runes[0] == 'b' { // alt+b, back one word - resetBlink = m.wordLeft() - break - } - if msg.Runes[0] == 'f' { // alt+f, forward one word - resetBlink = m.wordRight() - break - } - } - - // Input a regular character - if m.CharLimit <= 0 || len(m.value) < m.CharLimit { - runes := msg.Runes - - value := make([]rune, len(m.value)) - copy(value, m.value) - value = append(value[:m.pos], append(runes, value[m.pos:]...)...) - m.SetValue(string(value)) - if m.Err == nil { - resetBlink = m.setCursor(m.pos + len(runes)) - } - } - } - - case initialBlinkMsg: - // We accept all initialBlinkMsgs genrated by the Blink command. - - if m.cursorMode != CursorBlink || !m.focus { - return m, nil - } - - cmd := m.blinkCmd() - return m, cmd - - case blinkMsg: - // We're choosy about whether to accept blinkMsgs so that our cursor - // only exactly when it should. - - // Is this model blinkable? - if m.cursorMode != CursorBlink || !m.focus { - return m, nil - } - - // Were we expecting this blink message? - if msg.id != m.id || msg.tag != m.blinkTag { - return m, nil + case key.Matches(msg, m.KeyMap.DeleteWordForward): + m.deleteWordForward() + default: + // Input one or more regular characters. + m.insertRunesFromUserInput(msg.Runes) } - var cmd tea.Cmd - if m.cursorMode == CursorBlink { - m.blink = !m.blink - cmd = m.blinkCmd() - } - return m, cmd - - case blinkCanceled: // no-op - return m, nil - case pasteMsg: - resetBlink = m.handlePaste(string(msg)) + m.insertRunesFromUserInput([]rune(msg)) case pasteErrMsg: m.Err = msg } + var cmds []tea.Cmd var cmd tea.Cmd - if resetBlink { - cmd = m.blinkCmd() + + m.Cursor, cmd = m.Cursor.Update(msg) + cmds = append(cmds, cmd) + + if oldPos != m.pos { + m.Cursor.Blink = false + cmds = append(cmds, m.Cursor.BlinkCmd()) } m.handleOverflow() - return m, cmd + return m, tea.Batch(cmds...) } // View renders the textinput in its current state. @@ -750,10 +616,13 @@ func (m Model) View() string { v := styleText(m.echoTransform(string(value[:pos]))) if pos < len(value) { - v += m.cursorView(m.echoTransform(string(value[pos]))) // cursor and text under it + char := m.echoTransform(string(value[pos])) + m.Cursor.SetChar(char) + v += m.Cursor.View() // cursor and text under it v += styleText(m.echoTransform(string(value[pos+1:]))) // text after cursor } else { - v += m.cursorView(" ") + m.Cursor.SetChar(" ") + v += m.Cursor.View() } // If a max width and background color were set fill the empty spaces with @@ -778,12 +647,9 @@ func (m Model) placeholderView() string { style = m.PlaceholderStyle.Inline(true).Render ) - // Cursor - if m.blink { - v += m.cursorView(style(p[:1])) - } else { - v += m.cursorView(p[:1]) - } + m.Cursor.TextStyle = m.PlaceholderStyle + m.Cursor.SetChar(p[:1]) + v += m.Cursor.View() // The rest of the placeholder text v += style(p[1:]) @@ -791,42 +657,9 @@ func (m Model) placeholderView() string { return m.PromptStyle.Render(m.Prompt) + v } -// cursorView styles the cursor. -func (m Model) cursorView(v string) string { - if m.blink { - return m.TextStyle.Render(v) - } - return m.CursorStyle.Inline(true).Reverse(true).Render(v) -} - -// blinkCmd is an internal command used to manage cursor blinking. -func (m *Model) blinkCmd() tea.Cmd { - if m.cursorMode != CursorBlink { - return nil - } - - if m.blinkCtx != nil && m.blinkCtx.cancel != nil { - m.blinkCtx.cancel() - } - - ctx, cancel := context.WithTimeout(m.blinkCtx.ctx, m.BlinkSpeed) - m.blinkCtx.cancel = cancel - - m.blinkTag++ - - return func() tea.Msg { - defer cancel() - <-ctx.Done() - if ctx.Err() == context.DeadlineExceeded { - return blinkMsg{id: m.id, tag: m.blinkTag} - } - return blinkCanceled{} - } -} - // Blink is a command used to initialize cursor blinking. func Blink() tea.Msg { - return initialBlinkMsg{} + return cursor.Blink() } // Paste is a command for pasting from the clipboard into the text input. @@ -858,3 +691,31 @@ func max(a, b int) int { } return b } + +// Deprecated. + +// Deprecated: use cursor.Mode. +type CursorMode int + +const ( + // Deprecated: use cursor.CursorBlink. + CursorBlink = CursorMode(cursor.CursorBlink) + // Deprecated: use cursor.CursorStatic. + CursorStatic = CursorMode(cursor.CursorStatic) + // Deprecated: use cursor.CursorHide. + CursorHide = CursorMode(cursor.CursorHide) +) + +func (c CursorMode) String() string { + return cursor.Mode(c).String() +} + +// Deprecated: use cursor.Mode(). +func (m Model) CursorMode() CursorMode { + return CursorMode(m.Cursor.Mode()) +} + +// Deprecated: use cursor.SetMode(). +func (m *Model) SetCursorMode(mode CursorMode) tea.Cmd { + return m.Cursor.SetMode(cursor.Mode(mode)) +} diff --git a/vendor/github.com/charmbracelet/bubbles/viewport/viewport.go b/vendor/github.com/charmbracelet/bubbles/viewport/viewport.go index c4c3b5b..b2dfa2c 100644 --- a/vendor/github.com/charmbracelet/bubbles/viewport/viewport.go +++ b/vendor/github.com/charmbracelet/bubbles/viewport/viewport.go @@ -147,8 +147,7 @@ func (m *Model) ViewDown() []string { return nil } - m.SetYOffset(m.YOffset + m.Height) - return m.visibleLines() + return m.LineDown(m.Height) } // ViewUp moves the view up by one height of the viewport. Basically, "page up". @@ -157,8 +156,7 @@ func (m *Model) ViewUp() []string { return nil } - m.SetYOffset(m.YOffset - m.Height) - return m.visibleLines() + return m.LineUp(m.Height) } // HalfViewDown moves the view down by half the height of the viewport. @@ -167,8 +165,7 @@ func (m *Model) HalfViewDown() (lines []string) { return nil } - m.SetYOffset(m.YOffset + m.Height/2) - return m.visibleLines() + return m.LineDown(m.Height / 2) } // HalfViewUp moves the view up by half the height of the viewport. @@ -177,13 +174,12 @@ func (m *Model) HalfViewUp() (lines []string) { return nil } - m.SetYOffset(m.YOffset - m.Height/2) - return m.visibleLines() + return m.LineUp(m.Height / 2) } // LineDown moves the view down by the given number of lines. func (m *Model) LineDown(n int) (lines []string) { - if m.AtBottom() || n == 0 { + if m.AtBottom() || n == 0 || len(m.lines) == 0 { return nil } @@ -191,20 +187,38 @@ func (m *Model) LineDown(n int) (lines []string) { // greater than the number of lines we actually have left before we reach // the bottom. m.SetYOffset(m.YOffset + n) - return m.visibleLines() + + // Gather lines to send off for performance scrolling. + bottom := clamp(m.YOffset+m.Height, 0, len(m.lines)) + top := clamp(m.YOffset+m.Height-n, 0, bottom) + return m.lines[top:bottom] } // LineUp moves the view down by the given number of lines. Returns the new // lines to show. func (m *Model) LineUp(n int) (lines []string) { - if m.AtTop() || n == 0 { + if m.AtTop() || n == 0 || len(m.lines) == 0 { return nil } // Make sure the number of lines by which we're going to scroll isn't // greater than the number of lines we are from the top. m.SetYOffset(m.YOffset - n) - return m.visibleLines() + + // Gather lines to send off for performance scrolling. + top := max(0, m.YOffset) + bottom := clamp(m.YOffset+n, 0, m.maxYOffset()) + return m.lines[top:bottom] +} + +// TotalLineCount returns the total number of lines (both hidden and visible) within the viewport. +func (m Model) TotalLineCount() int { + return len(m.lines) +} + +// VisibleLineCount returns the number of the visible lines within the viewport. +func (m Model) VisibleLineCount() int { + return len(m.visibleLines()) } // GotoTop sets the viewport to the top position. @@ -240,9 +254,8 @@ func Sync(m Model) tea.Cmd { // number of lines. Use Model.ViewDown to get the lines that should be rendered. // For example: // -// lines := model.ViewDown(1) -// cmd := ViewDown(m, lines) -// +// lines := model.ViewDown(1) +// cmd := ViewDown(m, lines) func ViewDown(m Model, lines []string) tea.Cmd { if len(lines) == 0 { return nil diff --git a/vendor/modules.txt b/vendor/modules.txt index cf8d934..cac820e 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -4,12 +4,14 @@ github.com/atotto/clipboard # github.com/aymanbagabas/go-osc52/v2 v2.0.1 ## explicit; go 1.16 github.com/aymanbagabas/go-osc52/v2 -# github.com/charmbracelet/bubbles v0.14.0 +# github.com/charmbracelet/bubbles v0.15.0 ## explicit; go 1.13 +github.com/charmbracelet/bubbles/cursor github.com/charmbracelet/bubbles/help github.com/charmbracelet/bubbles/key github.com/charmbracelet/bubbles/list github.com/charmbracelet/bubbles/paginator +github.com/charmbracelet/bubbles/runeutil github.com/charmbracelet/bubbles/spinner github.com/charmbracelet/bubbles/textinput github.com/charmbracelet/bubbles/viewport