forked from Mirrors/bubbletea
Support bounded width in text input
This commit is contained in:
parent
9fc0d0ea82
commit
3868858947
|
@ -1,6 +1,7 @@
|
||||||
package textinput
|
package textinput
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/charmbracelet/boba"
|
"github.com/charmbracelet/boba"
|
||||||
|
@ -34,12 +35,24 @@ type Model struct {
|
||||||
// accept. If 0 or less, there's no limit.
|
// accept. If 0 or less, there's no limit.
|
||||||
CharLimit int
|
CharLimit int
|
||||||
|
|
||||||
|
// 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.
|
||||||
|
Width int
|
||||||
|
|
||||||
// Focus indicates whether user input focus should be on this input
|
// Focus indicates whether user input focus should be on this input
|
||||||
// component. When false, don't blink and ignore keyboard input.
|
// component. When false, don't blink and ignore keyboard input.
|
||||||
focus bool
|
focus bool
|
||||||
|
|
||||||
|
// Cursor blink state
|
||||||
blink bool
|
blink bool
|
||||||
pos int
|
|
||||||
|
// Cursor position
|
||||||
|
pos int
|
||||||
|
|
||||||
|
// Used to emulate a viewport when width is set and the content is
|
||||||
|
// overflowing
|
||||||
|
offset int
|
||||||
}
|
}
|
||||||
|
|
||||||
// Focused returns the focus state on the model
|
// Focused returns the focus state on the model
|
||||||
|
@ -118,61 +131,60 @@ func Update(msg boba.Msg, m Model) (Model, boba.Cmd) {
|
||||||
m.Value = m.Value[:m.pos-1] + m.Value[m.pos:]
|
m.Value = m.Value[:m.pos-1] + m.Value[m.pos:]
|
||||||
m.pos--
|
m.pos--
|
||||||
}
|
}
|
||||||
return m, nil
|
|
||||||
case boba.KeyLeft:
|
case boba.KeyLeft:
|
||||||
if m.pos > 0 {
|
if m.pos > 0 {
|
||||||
m.pos--
|
m.pos--
|
||||||
}
|
}
|
||||||
return m, nil
|
|
||||||
case boba.KeyRight:
|
case boba.KeyRight:
|
||||||
if m.pos < len(m.Value) {
|
if m.pos < len(m.Value) {
|
||||||
m.pos++
|
m.pos++
|
||||||
}
|
}
|
||||||
return m, nil
|
|
||||||
case boba.KeyCtrlF: // ^F, forward one character
|
case boba.KeyCtrlF: // ^F, forward one character
|
||||||
fallthrough
|
fallthrough
|
||||||
case boba.KeyCtrlB: // ^B, back one charcter
|
case boba.KeyCtrlB: // ^B, back one charcter
|
||||||
fallthrough
|
fallthrough
|
||||||
case boba.KeyCtrlA: // ^A, go to beginning
|
case boba.KeyCtrlA: // ^A, go to beginning
|
||||||
m.pos = 0
|
m.pos = 0
|
||||||
return m, nil
|
|
||||||
case boba.KeyCtrlD: // ^D, delete char under cursor
|
case boba.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 = m.Value[:m.pos] + m.Value[m.pos+1:]
|
m.Value = m.Value[:m.pos] + m.Value[m.pos+1:]
|
||||||
}
|
}
|
||||||
return m, nil
|
|
||||||
case boba.KeyCtrlE: // ^E, go to end
|
case boba.KeyCtrlE: // ^E, go to end
|
||||||
m.pos = len(m.Value)
|
m.pos = len(m.Value)
|
||||||
return m, nil
|
|
||||||
case boba.KeyCtrlK: // ^K, kill text after cursor
|
case boba.KeyCtrlK: // ^K, kill text after cursor
|
||||||
m.Value = m.Value[:m.pos]
|
m.Value = m.Value[:m.pos]
|
||||||
m.pos = len(m.Value)
|
m.pos = len(m.Value)
|
||||||
return m, nil
|
|
||||||
case boba.KeyCtrlU: // ^U, kill text before cursor
|
case boba.KeyCtrlU: // ^U, kill text before cursor
|
||||||
m.Value = m.Value[m.pos:]
|
m.Value = m.Value[m.pos:]
|
||||||
m.pos = 0
|
m.pos = 0
|
||||||
return m, nil
|
m.offset = 0
|
||||||
case boba.KeyRune: // input a regular character
|
case boba.KeyRune: // input a regular character
|
||||||
if m.CharLimit <= 0 || len(m.Value) < m.CharLimit {
|
if m.CharLimit <= 0 || len(m.Value) < m.CharLimit {
|
||||||
m.Value = m.Value[:m.pos] + string(msg.Rune) + m.Value[m.pos:]
|
m.Value = m.Value[:m.pos] + string(msg.Rune) + m.Value[m.pos:]
|
||||||
m.pos++
|
m.pos++
|
||||||
}
|
}
|
||||||
return m, nil
|
|
||||||
default:
|
|
||||||
return m, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
case ErrMsg:
|
case ErrMsg:
|
||||||
m.Err = msg
|
m.Err = msg
|
||||||
return m, nil
|
|
||||||
|
|
||||||
case BlinkMsg:
|
case BlinkMsg:
|
||||||
m.blink = !m.blink
|
m.blink = !m.blink
|
||||||
return m, Blink(m)
|
return m, Blink(m)
|
||||||
|
|
||||||
default:
|
|
||||||
return m, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If a max width is defined, perform some logic to treat the visible area
|
||||||
|
// as a horizontally scrolling mini viewport.
|
||||||
|
if m.Width > 0 {
|
||||||
|
overflow := max(0, len(m.Value)-m.Width)
|
||||||
|
if overflow > 0 && m.pos < m.offset {
|
||||||
|
m.offset = max(0, min(len(m.Value), m.pos))
|
||||||
|
} else if overflow > 0 && m.pos >= m.offset+m.Width {
|
||||||
|
m.offset = max(0, m.pos-m.Width)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return m, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// View renders the textinput in its current state
|
// View renders the textinput in its current state
|
||||||
|
@ -187,14 +199,38 @@ func View(model boba.Model) string {
|
||||||
return placeholderView(m)
|
return placeholderView(m)
|
||||||
}
|
}
|
||||||
|
|
||||||
v := m.colorText(m.Value[:m.pos])
|
left := m.offset
|
||||||
|
right := 0
|
||||||
|
if m.Width > 0 {
|
||||||
|
right = min(len(m.Value), m.offset+m.Width+1)
|
||||||
|
} else {
|
||||||
|
right = len(m.Value)
|
||||||
|
}
|
||||||
|
value := m.Value[left:right]
|
||||||
|
pos := m.pos - m.offset
|
||||||
|
|
||||||
if m.pos < len(m.Value) {
|
v := m.colorText(value[:pos])
|
||||||
v += cursorView(string(m.Value[m.pos]), m)
|
|
||||||
v += m.colorText(m.Value[m.pos+1:])
|
if pos < len(value) {
|
||||||
|
v += cursorView(string(value[pos]), m) // cursor and text under it
|
||||||
|
v += m.colorText(value[pos+1:]) // text after cursor
|
||||||
} else {
|
} else {
|
||||||
v += cursorView(" ", m)
|
v += cursorView(" ", m)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If a max width and background color were set fill the empty spaces with
|
||||||
|
// the background color.
|
||||||
|
if m.Width > 0 && len(m.BackgroundColor) > 0 && len(value) <= m.Width {
|
||||||
|
padding := m.Width - len(value)
|
||||||
|
if len(value)+padding <= m.Width && pos < len(value) {
|
||||||
|
padding++
|
||||||
|
}
|
||||||
|
v += strings.Repeat(
|
||||||
|
termenv.String(" ").Background(color(m.BackgroundColor)).String(),
|
||||||
|
padding,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
return m.Prompt + v
|
return m.Prompt + v
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -221,13 +257,20 @@ func placeholderView(m Model) string {
|
||||||
return m.Prompt + v
|
return m.Prompt + v
|
||||||
}
|
}
|
||||||
|
|
||||||
// cursorView style the cursor
|
// cursorView styles the cursor
|
||||||
func cursorView(s string, m Model) string {
|
func cursorView(s string, m Model) string {
|
||||||
if m.blink {
|
if m.blink {
|
||||||
|
if m.TextColor != "" || m.BackgroundColor != "" {
|
||||||
|
return termenv.String(s).
|
||||||
|
Foreground(color(m.TextColor)).
|
||||||
|
Background(color(m.BackgroundColor)).
|
||||||
|
String()
|
||||||
|
}
|
||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
return termenv.String(s).
|
return termenv.String(s).
|
||||||
Foreground(color(m.CursorColor)).
|
Foreground(color(m.CursorColor)).
|
||||||
|
Background(color(m.BackgroundColor)).
|
||||||
Reverse().
|
Reverse().
|
||||||
String()
|
String()
|
||||||
}
|
}
|
||||||
|
@ -239,3 +282,17 @@ func Blink(model Model) boba.Cmd {
|
||||||
return BlinkMsg{}
|
return BlinkMsg{}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func min(a, b int) int {
|
||||||
|
if a < b {
|
||||||
|
return a
|
||||||
|
}
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
func max(a, b int) int {
|
||||||
|
if a > b {
|
||||||
|
return a
|
||||||
|
}
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue