Skip to content

Commit

Permalink
feat(textinput): do not block input on validation
Browse files Browse the repository at this point in the history
This PR builds upon the excellent work in charmbracelet#167 and charmbracelet#114 and makes a
breaking change to the validation API.

Currently, validation will completely block text input if the Validate
function returns an error. This is now changed so the function no longer
blocks input if this is the case, thus handing this responsibility to
the clients.

This is helpful for cases where the user is requested to type an
existing system path, and the Validate function keeps asserting the
existence of the path. With the current implementation such a validation
is not possible.

For example:

    > /
    Err: nil

    > /t
    Err: /t: No such file or directory

    > /tm
    Err: /tm: No such file or directory

    > /tmp
    Err: nil
  • Loading branch information
GabrielNagy committed Feb 29, 2024
1 parent 6ebf50e commit 5b06afd
Showing 1 changed file with 19 additions and 19 deletions.
38 changes: 19 additions & 19 deletions textinput/textinput.go
Expand Up @@ -181,19 +181,14 @@ 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)
err := m.validateIfDefined(string(runes))
m.setValueInternal(runes, err)
}

func (m *Model) setValueInternal(runes []rune) {
if m.Validate != nil {
if err := m.Validate(string(runes)); err != nil {
m.Err = err
return
}
}
func (m *Model) setValueInternal(runes []rune, err error) {
m.Err = err

empty := len(m.value) == 0
m.Err = nil

if m.CharLimit > 0 && len(runes) > m.CharLimit {
m.value = runes[:m.CharLimit]
Expand Down Expand Up @@ -307,8 +302,6 @@ func (m *Model) insertRunesFromUserInput(v []rune) {
tail := make([]rune, len(tailSrc))
copy(tail, tailSrc)

oldPos := m.pos

// Insert pasted runes
for _, r := range paste {
head = append(head, r)
Expand All @@ -323,11 +316,8 @@ func (m *Model) insertRunesFromUserInput(v []rune) {

// Put it all back together
value := append(head, tail...)
m.setValueInternal(value)

if m.Err != nil {
m.pos = oldPos
}
inputErr := m.validateIfDefined(string(value))
m.setValueInternal(value, inputErr)
}

// If a max width is defined, perform some logic to treat the visible area
Expand Down Expand Up @@ -378,6 +368,7 @@ func (m *Model) handleOverflow() {
// deleteBeforeCursor deletes all text before the cursor.
func (m *Model) deleteBeforeCursor() {
m.value = m.value[m.pos:]
m.Err = m.validateIfDefined(string(m.value))
m.offset = 0
m.SetCursor(0)
}
Expand All @@ -387,6 +378,7 @@ func (m *Model) deleteBeforeCursor() {
// masked input.
func (m *Model) deleteAfterCursor() {
m.value = m.value[:m.pos]
m.Err = m.validateIfDefined(string(m.value))
m.SetCursor(len(m.value))
}

Expand Down Expand Up @@ -432,6 +424,7 @@ func (m *Model) deleteWordBackward() {
} else {
m.value = append(m.value[:m.pos], m.value[oldPos:]...)
}
m.Err = m.validateIfDefined(string(m.value))
}

// deleteWordForward deletes the word right to the cursor. If input is masked
Expand Down Expand Up @@ -471,6 +464,7 @@ func (m *Model) deleteWordForward() {
} else {
m.value = append(m.value[:oldPos], m.value[m.pos:]...)
}
m.Err = m.validateIfDefined(string(m.value))

m.SetCursor(oldPos)
}
Expand Down Expand Up @@ -575,12 +569,12 @@ func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
case tea.KeyMsg:
switch {
case key.Matches(msg, m.KeyMap.DeleteWordBackward):
m.Err = nil
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:]...)
m.Err = m.validateIfDefined(string(m.value))
if m.pos > 0 {
m.SetCursor(m.pos - 1)
}
Expand All @@ -597,13 +591,12 @@ func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
if m.pos < len(m.value) {
m.SetCursor(m.pos + 1)
}
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:]...)
m.Err = m.validateIfDefined(string(m.value))
}
case key.Matches(msg, m.KeyMap.LineEnd):
m.CursorEnd()
Expand Down Expand Up @@ -884,3 +877,10 @@ func (m *Model) previousSuggestion() {
m.currentSuggestionIndex = len(m.matchedSuggestions) - 1
}
}

func (m Model) validateIfDefined(v string) error {
if m.Validate != nil {
return m.Validate(v)
}
return nil
}

0 comments on commit 5b06afd

Please sign in to comment.