5 Commits

Author SHA1 Message Date
Christian Rocha
9449cc7e41 Don't allow word-based deletion when input is masked in textinput
alt+d and ctrl+w will now delete all the way to the beginning and end,
respectively, if EchoMode is not EchoNormal.
2021-04-26 12:03:57 -04:00
Christian Rocha
58a177394e Don't allow word-to-word movement when input is masked in textinput
When EchoMode is not set to EchoNormal, alt+left/alt+b and
alt+right/alt+b jumps to the beginning and the end, respectively, so as
not to reveal word breaks in the masked text.
2021-04-26 12:03:57 -04:00
Christian Rocha
f016c31d83 Add method for returning cursor position in textinput
Previously, Cursor was a member on the model that did absolutely
nothing.
2021-04-26 12:03:57 -04:00
Christian Rocha
74436326b9 Public cursor movement functions no longer return values in textinput
Previously, textinput.SetCursor, textinput.CursotStart, and
textinput.CursorEnd returned bools used interally for managing cursor
blinking. Those methods have been replaced with private counterparts.
2021-04-26 12:03:57 -04:00
Christian Rocha
158097df66 Keep y-offset in bounds when setting content 2021-04-19 19:47:50 -04:00
2 changed files with 98 additions and 42 deletions

View File

@@ -61,7 +61,6 @@ type Model struct {
// General settings. // General settings.
Prompt string Prompt string
Placeholder string Placeholder string
Cursor string
BlinkSpeed time.Duration BlinkSpeed time.Duration
EchoMode EchoMode EchoMode EchoMode
EchoCharacter rune EchoCharacter rune
@@ -140,7 +139,7 @@ func (m *Model) SetValue(s string) {
m.value = runes m.value = runes
} }
if m.pos == 0 || m.pos > len(m.value) { if m.pos == 0 || m.pos > len(m.value) {
m.SetCursor(len(m.value)) m.setCursor(len(m.value))
} }
m.handleOverflow() m.handleOverflow()
} }
@@ -150,10 +149,21 @@ func (m Model) Value() string {
return string(m.value) return string(m.value)
} }
// SetCursor start moves the cursor to the given position. If the position is // Cursor returns the cursor position.
func (m Model) Cursor() int {
return m.pos
}
// 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. // out of bounds the cursor will be moved to the start or end accordingly.
// Returns whether or nor the cursor timer should be reset. func (m *Model) SetCursor(pos int) {
func (m *Model) SetCursor(pos int) bool { 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.pos = clamp(pos, 0, len(m.value))
m.handleOverflow() m.handleOverflow()
@@ -164,16 +174,26 @@ func (m *Model) SetCursor(pos int) bool {
return m.cursorMode == cursorBlink return m.cursorMode == cursorBlink
} }
// CursorStart moves the cursor to the start of the field. Returns whether or // CursorStart moves the cursor to the start of the input field.
// not the curosr blink should be reset. func (m *Model) CursorStart() {
func (m *Model) CursorStart() bool { m.cursorStart()
return m.SetCursor(0)
} }
// CursorEnd moves the cursor to the end of the field. Returns whether or not // cursorStart moves the cursor to the start of the input field and returns
// the cursor blink should be reset. // whether or not the curosr blink should be reset.
func (m *Model) CursorEnd() bool { func (m *Model) cursorStart() bool {
return m.SetCursor(len(m.value)) return m.setCursor(0)
}
// CursorEnd moves the cursor to the end of the input field
func (m *Model) CursorEnd() {
m.cursorEnd()
}
// cursorEnd moves the cursor to the end of the input field and returns whether
// or not
func (m *Model) cursorEnd() bool {
return m.setCursor(len(m.value))
} }
// Focused returns the focus state on the model. // Focused returns the focus state on the model.
@@ -197,7 +217,7 @@ func (m *Model) Blur() {
// or not the cursor blink should reset. // or not the cursor blink should reset.
func (m *Model) Reset() bool { func (m *Model) Reset() bool {
m.value = nil m.value = nil
return m.SetCursor(0) return m.setCursor(0)
} }
// handle a clipboard paste event, if supported. Returns whether or not the // handle a clipboard paste event, if supported. Returns whether or not the
@@ -243,7 +263,7 @@ func (m *Model) handlePaste(v string) bool {
m.value = append(head, tail...) m.value = append(head, tail...)
// Reset blink state if necessary and run overflow checks // Reset blink state if necessary and run overflow checks
return m.SetCursor(m.pos) return m.setCursor(m.pos)
} }
// If a max width is defined, perform some logic to treat the visible area // If a max width is defined, perform some logic to treat the visible area
@@ -291,6 +311,22 @@ 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 {
m.value = m.value[m.pos:]
m.offset = 0
return 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 {
m.value = m.value[:m.pos]
return m.setCursor(len(m.value))
}
// deleteWordLeft deletes the word left to the cursor. Returns whether or not // deleteWordLeft deletes the word left to the cursor. Returns whether or not
// the cursor blink should be reset. // the cursor blink should be reset.
func (m *Model) deleteWordLeft() bool { func (m *Model) deleteWordLeft() bool {
@@ -298,20 +334,24 @@ func (m *Model) deleteWordLeft() bool {
return false return false
} }
if m.EchoMode != EchoNormal {
return m.deleteBeforeCursor()
}
i := m.pos i := m.pos
blink := m.SetCursor(m.pos - 1) blink := m.setCursor(m.pos - 1)
for unicode.IsSpace(m.value[m.pos]) { for unicode.IsSpace(m.value[m.pos]) {
// ignore series of whitespace before cursor // ignore series of whitespace before cursor
blink = m.SetCursor(m.pos - 1) blink = m.setCursor(m.pos - 1)
} }
for m.pos > 0 { for m.pos > 0 {
if !unicode.IsSpace(m.value[m.pos]) { if !unicode.IsSpace(m.value[m.pos]) {
blink = m.SetCursor(m.pos - 1) blink = m.setCursor(m.pos - 1)
} else { } else {
if m.pos > 0 { if m.pos > 0 {
// keep the previous space // keep the previous space
blink = m.SetCursor(m.pos + 1) blink = m.setCursor(m.pos + 1)
} }
break break
} }
@@ -327,22 +367,27 @@ func (m *Model) deleteWordLeft() bool {
} }
// deleteWordRight deletes the word right to the cursor. Returns whether or not // deleteWordRight deletes the word right to the cursor. Returns whether or not
// the cursor blink should be reset. // 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 { func (m *Model) deleteWordRight() bool {
if m.pos >= len(m.value) || len(m.value) == 0 { if m.pos >= len(m.value) || len(m.value) == 0 {
return false return false
} }
if m.EchoMode != EchoNormal {
return m.deleteAfterCursor()
}
i := m.pos i := m.pos
m.SetCursor(m.pos + 1) m.setCursor(m.pos + 1)
for unicode.IsSpace(m.value[m.pos]) { for unicode.IsSpace(m.value[m.pos]) {
// ignore series of whitespace after cursor // ignore series of whitespace after cursor
m.SetCursor(m.pos + 1) m.setCursor(m.pos + 1)
} }
for m.pos < len(m.value) { for m.pos < len(m.value) {
if !unicode.IsSpace(m.value[m.pos]) { if !unicode.IsSpace(m.value[m.pos]) {
m.SetCursor(m.pos + 1) m.setCursor(m.pos + 1)
} else { } else {
break break
} }
@@ -354,21 +399,26 @@ func (m *Model) deleteWordRight() bool {
m.value = append(m.value[:i], m.value[m.pos:]...) m.value = append(m.value[:i], m.value[m.pos:]...)
} }
return m.SetCursor(i) return m.setCursor(i)
} }
// wordLeft moves the cursor one word to the left. Returns whether or not the // wordLeft moves the cursor one word to the left. Returns whether or not the
// cursor blink should be reset. // 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 { func (m *Model) wordLeft() bool {
if m.pos == 0 || len(m.value) == 0 { if m.pos == 0 || len(m.value) == 0 {
return false return false
} }
if m.EchoMode != EchoNormal {
return m.cursorStart()
}
blink := false blink := false
i := m.pos - 1 i := m.pos - 1
for i >= 0 { for i >= 0 {
if unicode.IsSpace(m.value[i]) { if unicode.IsSpace(m.value[i]) {
blink = m.SetCursor(m.pos - 1) blink = m.setCursor(m.pos - 1)
i-- i--
} else { } else {
break break
@@ -377,7 +427,7 @@ func (m *Model) wordLeft() bool {
for i >= 0 { for i >= 0 {
if !unicode.IsSpace(m.value[i]) { if !unicode.IsSpace(m.value[i]) {
blink = m.SetCursor(m.pos - 1) blink = m.setCursor(m.pos - 1)
i-- i--
} else { } else {
break break
@@ -388,17 +438,22 @@ func (m *Model) wordLeft() bool {
} }
// wordRight moves the cursor one word to the right. Returns whether or not the // wordRight moves the cursor one word to the right. Returns whether or not the
// cursor blink should be reset. // 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 { func (m *Model) wordRight() bool {
if m.pos >= len(m.value) || len(m.value) == 0 { if m.pos >= len(m.value) || len(m.value) == 0 {
return false return false
} }
if m.EchoMode != EchoNormal {
return m.cursorEnd()
}
blink := false blink := false
i := m.pos i := m.pos
for i < len(m.value) { for i < len(m.value) {
if unicode.IsSpace(m.value[i]) { if unicode.IsSpace(m.value[i]) {
blink = m.SetCursor(m.pos + 1) blink = m.setCursor(m.pos + 1)
i++ i++
} else { } else {
break break
@@ -407,7 +462,7 @@ func (m *Model) wordRight() bool {
for i < len(m.value) { for i < len(m.value) {
if !unicode.IsSpace(m.value[i]) { if !unicode.IsSpace(m.value[i]) {
blink = m.SetCursor(m.pos + 1) blink = m.setCursor(m.pos + 1)
i++ i++
} else { } else {
break break
@@ -448,7 +503,7 @@ func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
if len(m.value) > 0 { if len(m.value) > 0 {
m.value = append(m.value[:max(0, m.pos-1)], m.value[m.pos:]...) m.value = append(m.value[:max(0, m.pos-1)], m.value[m.pos:]...)
if m.pos > 0 { if m.pos > 0 {
resetBlink = m.SetCursor(m.pos - 1) resetBlink = m.setCursor(m.pos - 1)
} }
} }
} }
@@ -458,33 +513,30 @@ func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
break break
} }
if m.pos > 0 { // left arrow, ^F, back one character if m.pos > 0 { // left arrow, ^F, back one character
resetBlink = m.SetCursor(m.pos - 1) resetBlink = m.setCursor(m.pos - 1)
} }
case tea.KeyRight, tea.KeyCtrlF: case tea.KeyRight, tea.KeyCtrlF:
if msg.Alt { // alt+right arrow, forward one word if msg.Alt { // alt+right arrow, forward one word
resetBlink = m.wordRight() resetBlink = m.wordRight()
break break
} }
if m.pos < len(m.value) { // right arrow, ^F, forward one word if m.pos < len(m.value) { // right arrow, ^F, forward one character
resetBlink = m.SetCursor(m.pos + 1) resetBlink = m.setCursor(m.pos + 1)
} }
case tea.KeyCtrlW: // ^W, delete word left of cursor case tea.KeyCtrlW: // ^W, delete word left of cursor
resetBlink = m.deleteWordLeft() resetBlink = m.deleteWordLeft()
case tea.KeyHome, tea.KeyCtrlA: // ^A, go to beginning case tea.KeyHome, tea.KeyCtrlA: // ^A, go to beginning
resetBlink = m.CursorStart() resetBlink = m.cursorStart()
case tea.KeyDelete, tea.KeyCtrlD: // ^D, delete char under cursor case tea.KeyDelete, tea.KeyCtrlD: // ^D, delete char under cursor
if len(m.value) > 0 && m.pos < len(m.value) { if len(m.value) > 0 && m.pos < len(m.value) {
m.value = append(m.value[:m.pos], m.value[m.pos+1:]...) m.value = append(m.value[:m.pos], m.value[m.pos+1:]...)
} }
case tea.KeyCtrlE, tea.KeyEnd: // ^E, go to end case tea.KeyCtrlE, tea.KeyEnd: // ^E, go to end
resetBlink = m.CursorEnd() resetBlink = m.cursorEnd()
case tea.KeyCtrlK: // ^K, kill text after cursor case tea.KeyCtrlK: // ^K, kill text after cursor
m.value = m.value[:m.pos] resetBlink = m.deleteAfterCursor()
resetBlink = m.SetCursor(len(m.value))
case tea.KeyCtrlU: // ^U, kill text before cursor case tea.KeyCtrlU: // ^U, kill text before cursor
m.value = m.value[m.pos:] resetBlink = m.deleteBeforeCursor()
resetBlink = m.SetCursor(0)
m.offset = 0
case tea.KeyCtrlV: // ^V paste case tea.KeyCtrlV: // ^V paste
return m, Paste return m, Paste
case tea.KeyRunes: // input regular characters case tea.KeyRunes: // input regular characters
@@ -506,7 +558,7 @@ func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
// Input a regular character // Input a regular character
if m.CharLimit <= 0 || len(m.value) < m.CharLimit { if m.CharLimit <= 0 || len(m.value) < m.CharLimit {
m.value = append(m.value[:m.pos], append(msg.Runes, m.value[m.pos:]...)...) m.value = append(m.value[:m.pos], append(msg.Runes, m.value[m.pos:]...)...)
resetBlink = m.SetCursor(m.pos + len(msg.Runes)) resetBlink = m.setCursor(m.pos + len(msg.Runes))
} }
} }

View File

@@ -71,6 +71,10 @@ func (m Model) ScrollPercent() float64 {
func (m *Model) SetContent(s string) { func (m *Model) SetContent(s string) {
s = strings.Replace(s, "\r\n", "\n", -1) // normalize line endings s = strings.Replace(s, "\r\n", "\n", -1) // normalize line endings
m.lines = strings.Split(s, "\n") m.lines = strings.Split(s, "\n")
if m.YOffset > len(m.lines)-1 {
m.GotoBottom()
}
} }
// Return the lines that should currently be visible in the viewport. // Return the lines that should currently be visible in the viewport.