mirror of
https://github.com/mum4k/termdash.git
synced 2025-04-25 13:48:50 +08:00
Completing test coverage and most of the functionality.
Mouse support is outstanding.
This commit is contained in:
parent
1df5298809
commit
886f970586
@ -21,6 +21,8 @@ import (
|
||||
|
||||
"github.com/mum4k/termdash/align"
|
||||
"github.com/mum4k/termdash/cell"
|
||||
"github.com/mum4k/termdash/internal/runewidth"
|
||||
"github.com/mum4k/termdash/internal/wrap"
|
||||
"github.com/mum4k/termdash/linestyle"
|
||||
)
|
||||
|
||||
@ -57,8 +59,9 @@ type options struct {
|
||||
placeHolder string
|
||||
hideTextWith rune
|
||||
|
||||
filter FilterFn
|
||||
onSubmit SubmitFn
|
||||
filter FilterFn
|
||||
onSubmit SubmitFn
|
||||
clearOnSubmit bool
|
||||
}
|
||||
|
||||
// validate validates the provided options.
|
||||
@ -69,6 +72,14 @@ func (o *options) validate() error {
|
||||
if min, cells := 4, o.maxWidthCells; cells != nil && *cells < min {
|
||||
return fmt.Errorf("invalid MaxWidthCells(%d), must be value in range %d <= value", *cells, min)
|
||||
}
|
||||
if r := o.hideTextWith; r != 0 {
|
||||
if err := wrap.ValidText(string(r)); err != nil {
|
||||
return fmt.Errorf("invalid HideTextWidth rune %c(%d): %v", r, r, err)
|
||||
}
|
||||
if got, want := runewidth.RuneWidth(r), 1; got != want {
|
||||
return fmt.Errorf("invalid HideTextWidth rune %c(%d), has rune width of %d cells, only runes with width of %d are accepted", r, r, got, want)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -77,6 +88,7 @@ func newOptions() *options {
|
||||
return &options{
|
||||
fillColor: cell.ColorNumber(DefaultFillColorNumber),
|
||||
placeHolderColor: cell.ColorNumber(DefaultPlaceHolderColorNumber),
|
||||
highlightedColor: cell.ColorNumber(DefaultHighlightedColorNumber),
|
||||
cursorColor: cell.ColorNumber(DefaultCursorColorNumber),
|
||||
labelAlign: DefaultLabelAlign,
|
||||
}
|
||||
@ -191,7 +203,7 @@ func PlaceHolder(text string) Option {
|
||||
|
||||
// DefaultPlaceHolderColorNumber is the default color number for the
|
||||
// PlaceHolderColor option.
|
||||
const DefaultPlaceHolderColorNumber = 190
|
||||
const DefaultPlaceHolderColorNumber = 194
|
||||
|
||||
// PlaceHolderColor sets the color of the placeholder text.
|
||||
// Defaults to DefaultPlaceHolderColorNumber.
|
||||
@ -204,12 +216,18 @@ func PlaceHolderColor(c cell.Color) Option {
|
||||
// HideTextWith sets the rune that should be displayed instead of displaying
|
||||
// the text. Useful for fields that accept sensitive information like
|
||||
// passwords.
|
||||
// The rune must be a printable rune with cell width of one.
|
||||
func HideTextWith(r rune) Option {
|
||||
return option(func(opts *options) {
|
||||
opts.hideTextWith = r
|
||||
})
|
||||
}
|
||||
|
||||
// FilterFn if provided can be used to filter runes that are allowed in the
|
||||
// text input field. Any rune for which this function returns false will be
|
||||
// rejected.
|
||||
type FilterFn func(rune) bool
|
||||
|
||||
// Filter sets a function that will be used to filter characters the user can
|
||||
// input.
|
||||
func Filter(fn FilterFn) Option {
|
||||
@ -218,10 +236,30 @@ func Filter(fn FilterFn) Option {
|
||||
})
|
||||
}
|
||||
|
||||
// SubmitFn if provided is called when the user submits the content of the text
|
||||
// input field, the argument text contains all the text in the field.
|
||||
// Submitting the input field clears its content.
|
||||
//
|
||||
// The callback function must be thread-safe as the keyboard event that
|
||||
// triggers the submission comes from a separate goroutine.
|
||||
type SubmitFn func(text string) error
|
||||
|
||||
// OnSubmit sets a function that will be called with the text typed by the user
|
||||
// when they submit the content by pressing the Enter key.
|
||||
// The SubmitFn must not attempt to read from or modify the TextInput instance
|
||||
// in any way as while the SubmitFn is executing, the TextInput is mutex
|
||||
// locked. If the intention is to clear the content on submission, use the
|
||||
// ClearOnSubmit() option.
|
||||
func OnSubmit(fn SubmitFn) Option {
|
||||
return option(func(opts *options) {
|
||||
opts.onSubmit = fn
|
||||
})
|
||||
}
|
||||
|
||||
// ClearOnSubmit sets the text input to be cleared when a submit of the content
|
||||
// is triggered by the user pressing the Enter key.
|
||||
func ClearOnSubmit() Option {
|
||||
return option(func(opts *options) {
|
||||
opts.clearOnSubmit = true
|
||||
})
|
||||
}
|
||||
|
@ -17,6 +17,7 @@ package textinput
|
||||
|
||||
import (
|
||||
"image"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/mum4k/termdash/align"
|
||||
@ -33,19 +34,6 @@ import (
|
||||
"github.com/mum4k/termdash/widgetapi"
|
||||
)
|
||||
|
||||
// FilterFn if provided can be used to filter runes that are allowed in the
|
||||
// text input field. Any rune for which this function returns false will be
|
||||
// rejected.
|
||||
type FilterFn func(rune) bool
|
||||
|
||||
// SubmitFn if provided is called when the user submits the content of the text
|
||||
// input field, the argument text contains all the text in the field.
|
||||
// Submitting the input field clears its content.
|
||||
//
|
||||
// The callback function must be thread-safe as the keyboard event that
|
||||
// triggers the submission comes from a separate goroutine.
|
||||
type SubmitFn func(text string) error
|
||||
|
||||
// TextInput accepts text input from the user.
|
||||
//
|
||||
// Displays an input field where the user can edit text and an optional label.
|
||||
@ -160,6 +148,11 @@ func (ti *TextInput) Draw(cvs *canvas.Canvas, meta *widgetapi.Meta) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if ti.opts.hideTextWith != 0 {
|
||||
text = hideText(text, ti.opts.hideTextWith)
|
||||
}
|
||||
|
||||
if err := draw.Text(
|
||||
cvs, text, forField.Min,
|
||||
draw.TextMaxX(forField.Max.X),
|
||||
@ -223,11 +216,24 @@ func (ti *TextInput) Keyboard(k *terminalapi.Keyboard) error {
|
||||
case keyboard.KeyEnd, keyboard.KeyCtrlE:
|
||||
ti.editor.cursorEnd()
|
||||
|
||||
case keyboard.KeyEnter:
|
||||
text := ti.editor.content()
|
||||
if ti.opts.clearOnSubmit {
|
||||
ti.editor.reset()
|
||||
}
|
||||
if ti.opts.onSubmit != nil {
|
||||
return ti.opts.onSubmit(text)
|
||||
}
|
||||
|
||||
default:
|
||||
if err := wrap.ValidText(string(k.Key)); err != nil {
|
||||
// Ignore unsupported runes.
|
||||
return nil
|
||||
}
|
||||
if ti.opts.filter != nil && !ti.opts.filter(rune(k.Key)) {
|
||||
// Ignore filtered runes.
|
||||
return nil
|
||||
}
|
||||
ti.editor.insert(rune(k.Key))
|
||||
}
|
||||
|
||||
@ -311,3 +317,26 @@ func split(cvsAr image.Rectangle, label string, widthPerc *int) (labelAr, textAr
|
||||
return image.ZR, cvsAr, nil
|
||||
}
|
||||
}
|
||||
|
||||
// hideText returns the text with all runes replaced with hr.
|
||||
func hideText(text string, hr rune) string {
|
||||
var b strings.Builder
|
||||
|
||||
i := 0
|
||||
sw := runewidth.StringWidth(text)
|
||||
for _, r := range text {
|
||||
rw := runewidth.RuneWidth(r)
|
||||
switch {
|
||||
case i == 0 && r == '⇦':
|
||||
b.WriteRune(r)
|
||||
|
||||
case i == sw-1 && r == '⇨':
|
||||
b.WriteRune(r)
|
||||
|
||||
default:
|
||||
b.WriteString(strings.Repeat(string(hr), rw))
|
||||
}
|
||||
i++
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
@ -28,6 +28,7 @@ import (
|
||||
"github.com/mum4k/termdash/internal/draw"
|
||||
"github.com/mum4k/termdash/internal/draw/testdraw"
|
||||
"github.com/mum4k/termdash/internal/faketerm"
|
||||
"github.com/mum4k/termdash/keyboard"
|
||||
"github.com/mum4k/termdash/linestyle"
|
||||
"github.com/mum4k/termdash/terminal/terminalapi"
|
||||
"github.com/mum4k/termdash/widgetapi"
|
||||
@ -101,6 +102,20 @@ func TestTextInput(t *testing.T) {
|
||||
},
|
||||
wantNewErr: true,
|
||||
},
|
||||
{
|
||||
desc: "fails on HideTextWith control rune",
|
||||
opts: []Option{
|
||||
HideTextWith(0x007f),
|
||||
},
|
||||
wantNewErr: true,
|
||||
},
|
||||
{
|
||||
desc: "fails on HideTextWith full-width rune",
|
||||
opts: []Option{
|
||||
HideTextWith('世'),
|
||||
},
|
||||
wantNewErr: true,
|
||||
},
|
||||
{
|
||||
desc: "takes all space without label",
|
||||
canvas: image.Rect(0, 0, 10, 1),
|
||||
@ -206,6 +221,7 @@ func TestTextInput(t *testing.T) {
|
||||
image.Point{0, 0},
|
||||
cursorRune,
|
||||
cell.BgColor(cell.ColorNumber(DefaultCursorColorNumber)),
|
||||
cell.FgColor(cell.ColorNumber(DefaultHighlightedColorNumber)),
|
||||
)
|
||||
testcanvas.MustApply(cvs, ft)
|
||||
return ft
|
||||
@ -294,6 +310,7 @@ func TestTextInput(t *testing.T) {
|
||||
image.Point{0, 0},
|
||||
cursorRune,
|
||||
cell.BgColor(cell.ColorRed),
|
||||
cell.FgColor(cell.ColorNumber(DefaultHighlightedColorNumber)),
|
||||
)
|
||||
testcanvas.MustApply(cvs, ft)
|
||||
return ft
|
||||
@ -541,6 +558,729 @@ func TestTextInput(t *testing.T) {
|
||||
return ft
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "displays written text",
|
||||
canvas: image.Rect(0, 0, 10, 1),
|
||||
meta: &widgetapi.Meta{},
|
||||
events: []terminalapi.Event{
|
||||
&terminalapi.Keyboard{Key: 'a'},
|
||||
&terminalapi.Keyboard{Key: 'b'},
|
||||
&terminalapi.Keyboard{Key: 'c'},
|
||||
},
|
||||
want: func(size image.Point) *faketerm.Terminal {
|
||||
ft := faketerm.MustNew(size)
|
||||
cvs := testcanvas.MustNew(ft.Area())
|
||||
|
||||
testcanvas.MustSetAreaCells(
|
||||
cvs,
|
||||
image.Rect(0, 0, 10, 1),
|
||||
textFieldRune,
|
||||
cell.BgColor(cell.ColorNumber(DefaultFillColorNumber)),
|
||||
)
|
||||
testdraw.MustText(
|
||||
cvs,
|
||||
"abc",
|
||||
image.Point{0, 0},
|
||||
)
|
||||
testcanvas.MustApply(cvs, ft)
|
||||
return ft
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "submits written text on enter",
|
||||
canvas: image.Rect(0, 0, 10, 1),
|
||||
meta: &widgetapi.Meta{},
|
||||
events: []terminalapi.Event{
|
||||
&terminalapi.Keyboard{Key: 'a'},
|
||||
&terminalapi.Keyboard{Key: 'b'},
|
||||
&terminalapi.Keyboard{Key: 'c'},
|
||||
&terminalapi.Keyboard{Key: keyboard.KeyEnter},
|
||||
},
|
||||
callback: &callbackTracker{},
|
||||
want: func(size image.Point) *faketerm.Terminal {
|
||||
ft := faketerm.MustNew(size)
|
||||
cvs := testcanvas.MustNew(ft.Area())
|
||||
|
||||
testcanvas.MustSetAreaCells(
|
||||
cvs,
|
||||
image.Rect(0, 0, 10, 1),
|
||||
textFieldRune,
|
||||
cell.BgColor(cell.ColorNumber(DefaultFillColorNumber)),
|
||||
)
|
||||
testdraw.MustText(
|
||||
cvs,
|
||||
"abc",
|
||||
image.Point{0, 0},
|
||||
)
|
||||
testcanvas.MustApply(cvs, ft)
|
||||
return ft
|
||||
},
|
||||
wantCallback: &callbackTracker{
|
||||
text: "abc",
|
||||
count: 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "forwards error returned by SubmitFn",
|
||||
canvas: image.Rect(0, 0, 10, 1),
|
||||
meta: &widgetapi.Meta{},
|
||||
events: []terminalapi.Event{
|
||||
&terminalapi.Keyboard{Key: 'a'},
|
||||
&terminalapi.Keyboard{Key: 'b'},
|
||||
&terminalapi.Keyboard{Key: 'c'},
|
||||
&terminalapi.Keyboard{Key: keyboard.KeyEnter},
|
||||
},
|
||||
callback: &callbackTracker{
|
||||
wantErr: true,
|
||||
},
|
||||
want: func(size image.Point) *faketerm.Terminal {
|
||||
ft := faketerm.MustNew(size)
|
||||
cvs := testcanvas.MustNew(ft.Area())
|
||||
|
||||
testcanvas.MustSetAreaCells(
|
||||
cvs,
|
||||
image.Rect(0, 0, 10, 1),
|
||||
textFieldRune,
|
||||
cell.BgColor(cell.ColorNumber(DefaultFillColorNumber)),
|
||||
)
|
||||
testdraw.MustText(
|
||||
cvs,
|
||||
"abc",
|
||||
image.Point{0, 0},
|
||||
)
|
||||
testcanvas.MustApply(cvs, ft)
|
||||
return ft
|
||||
},
|
||||
wantCallback: &callbackTracker{
|
||||
text: "abc",
|
||||
count: 1,
|
||||
},
|
||||
wantEventErr: true,
|
||||
},
|
||||
{
|
||||
desc: "submits written text on enter and clears the text input field",
|
||||
opts: []Option{
|
||||
ClearOnSubmit(),
|
||||
},
|
||||
canvas: image.Rect(0, 0, 10, 1),
|
||||
meta: &widgetapi.Meta{},
|
||||
events: []terminalapi.Event{
|
||||
&terminalapi.Keyboard{Key: 'a'},
|
||||
&terminalapi.Keyboard{Key: 'b'},
|
||||
&terminalapi.Keyboard{Key: 'c'},
|
||||
&terminalapi.Keyboard{Key: keyboard.KeyEnter},
|
||||
},
|
||||
callback: &callbackTracker{},
|
||||
want: func(size image.Point) *faketerm.Terminal {
|
||||
ft := faketerm.MustNew(size)
|
||||
cvs := testcanvas.MustNew(ft.Area())
|
||||
|
||||
testcanvas.MustSetAreaCells(
|
||||
cvs,
|
||||
image.Rect(0, 0, 10, 1),
|
||||
textFieldRune,
|
||||
cell.BgColor(cell.ColorNumber(DefaultFillColorNumber)),
|
||||
)
|
||||
testcanvas.MustApply(cvs, ft)
|
||||
return ft
|
||||
},
|
||||
wantCallback: &callbackTracker{
|
||||
text: "abc",
|
||||
count: 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "clears the text input field when enter is pressed and ClearOnSubmit option given",
|
||||
opts: []Option{
|
||||
ClearOnSubmit(),
|
||||
},
|
||||
canvas: image.Rect(0, 0, 10, 1),
|
||||
meta: &widgetapi.Meta{},
|
||||
events: []terminalapi.Event{
|
||||
&terminalapi.Keyboard{Key: 'a'},
|
||||
&terminalapi.Keyboard{Key: 'b'},
|
||||
&terminalapi.Keyboard{Key: 'c'},
|
||||
&terminalapi.Keyboard{Key: keyboard.KeyEnter},
|
||||
},
|
||||
want: func(size image.Point) *faketerm.Terminal {
|
||||
ft := faketerm.MustNew(size)
|
||||
cvs := testcanvas.MustNew(ft.Area())
|
||||
|
||||
testcanvas.MustSetAreaCells(
|
||||
cvs,
|
||||
image.Rect(0, 0, 10, 1),
|
||||
textFieldRune,
|
||||
cell.BgColor(cell.ColorNumber(DefaultFillColorNumber)),
|
||||
)
|
||||
testcanvas.MustApply(cvs, ft)
|
||||
return ft
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "write ignores control or unsupported space runes",
|
||||
canvas: image.Rect(0, 0, 10, 1),
|
||||
meta: &widgetapi.Meta{},
|
||||
events: []terminalapi.Event{
|
||||
&terminalapi.Keyboard{Key: 'a'},
|
||||
&terminalapi.Keyboard{Key: '\t'},
|
||||
&terminalapi.Keyboard{Key: 0x007f},
|
||||
&terminalapi.Keyboard{Key: 'b'},
|
||||
},
|
||||
want: func(size image.Point) *faketerm.Terminal {
|
||||
ft := faketerm.MustNew(size)
|
||||
cvs := testcanvas.MustNew(ft.Area())
|
||||
|
||||
testcanvas.MustSetAreaCells(
|
||||
cvs,
|
||||
image.Rect(0, 0, 10, 1),
|
||||
textFieldRune,
|
||||
cell.BgColor(cell.ColorNumber(DefaultFillColorNumber)),
|
||||
)
|
||||
testdraw.MustText(
|
||||
cvs,
|
||||
"ab",
|
||||
image.Point{0, 0},
|
||||
)
|
||||
testcanvas.MustApply(cvs, ft)
|
||||
return ft
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "write filters runes with the provided FilterFn",
|
||||
opts: []Option{
|
||||
Filter(func(r rune) bool {
|
||||
return r != 'b' && r != 'c'
|
||||
}),
|
||||
},
|
||||
canvas: image.Rect(0, 0, 10, 1),
|
||||
meta: &widgetapi.Meta{},
|
||||
events: []terminalapi.Event{
|
||||
&terminalapi.Keyboard{Key: 'a'},
|
||||
&terminalapi.Keyboard{Key: 'b'},
|
||||
&terminalapi.Keyboard{Key: 'c'},
|
||||
&terminalapi.Keyboard{Key: 'd'},
|
||||
},
|
||||
want: func(size image.Point) *faketerm.Terminal {
|
||||
ft := faketerm.MustNew(size)
|
||||
cvs := testcanvas.MustNew(ft.Area())
|
||||
|
||||
testcanvas.MustSetAreaCells(
|
||||
cvs,
|
||||
image.Rect(0, 0, 10, 1),
|
||||
textFieldRune,
|
||||
cell.BgColor(cell.ColorNumber(DefaultFillColorNumber)),
|
||||
)
|
||||
testdraw.MustText(
|
||||
cvs,
|
||||
"ad",
|
||||
image.Point{0, 0},
|
||||
)
|
||||
testcanvas.MustApply(cvs, ft)
|
||||
return ft
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
desc: "displays written text with full-width runes",
|
||||
canvas: image.Rect(0, 0, 4, 1),
|
||||
meta: &widgetapi.Meta{},
|
||||
events: []terminalapi.Event{
|
||||
&terminalapi.Keyboard{Key: 'a'},
|
||||
&terminalapi.Keyboard{Key: 'b'},
|
||||
&terminalapi.Keyboard{Key: '世'},
|
||||
},
|
||||
want: func(size image.Point) *faketerm.Terminal {
|
||||
ft := faketerm.MustNew(size)
|
||||
cvs := testcanvas.MustNew(ft.Area())
|
||||
|
||||
testcanvas.MustSetAreaCells(
|
||||
cvs,
|
||||
image.Rect(0, 0, 4, 1),
|
||||
textFieldRune,
|
||||
cell.BgColor(cell.ColorNumber(DefaultFillColorNumber)),
|
||||
)
|
||||
testdraw.MustText(
|
||||
cvs,
|
||||
"⇦世",
|
||||
image.Point{0, 0},
|
||||
)
|
||||
testcanvas.MustApply(cvs, ft)
|
||||
return ft
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "hides text when requested",
|
||||
opts: []Option{
|
||||
HideTextWith('*'),
|
||||
},
|
||||
canvas: image.Rect(0, 0, 10, 1),
|
||||
meta: &widgetapi.Meta{},
|
||||
events: []terminalapi.Event{
|
||||
&terminalapi.Keyboard{Key: 'a'},
|
||||
&terminalapi.Keyboard{Key: 'b'},
|
||||
&terminalapi.Keyboard{Key: 'c'},
|
||||
},
|
||||
want: func(size image.Point) *faketerm.Terminal {
|
||||
ft := faketerm.MustNew(size)
|
||||
cvs := testcanvas.MustNew(ft.Area())
|
||||
|
||||
testcanvas.MustSetAreaCells(
|
||||
cvs,
|
||||
image.Rect(0, 0, 10, 1),
|
||||
textFieldRune,
|
||||
cell.BgColor(cell.ColorNumber(DefaultFillColorNumber)),
|
||||
)
|
||||
testdraw.MustText(
|
||||
cvs,
|
||||
"***",
|
||||
image.Point{0, 0},
|
||||
)
|
||||
testcanvas.MustApply(cvs, ft)
|
||||
return ft
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "hides text, but doesn't hide scrolling arrows",
|
||||
opts: []Option{
|
||||
HideTextWith('*'),
|
||||
},
|
||||
canvas: image.Rect(0, 0, 4, 1),
|
||||
meta: &widgetapi.Meta{},
|
||||
events: []terminalapi.Event{
|
||||
&terminalapi.Keyboard{Key: 'a'},
|
||||
&terminalapi.Keyboard{Key: 'b'},
|
||||
&terminalapi.Keyboard{Key: 'c'},
|
||||
&terminalapi.Keyboard{Key: 'd'},
|
||||
&terminalapi.Keyboard{Key: 'e'},
|
||||
&terminalapi.Keyboard{Key: keyboard.KeyArrowLeft},
|
||||
&terminalapi.Keyboard{Key: keyboard.KeyArrowLeft},
|
||||
},
|
||||
want: func(size image.Point) *faketerm.Terminal {
|
||||
ft := faketerm.MustNew(size)
|
||||
cvs := testcanvas.MustNew(ft.Area())
|
||||
|
||||
testcanvas.MustSetAreaCells(
|
||||
cvs,
|
||||
image.Rect(0, 0, 4, 1),
|
||||
textFieldRune,
|
||||
cell.BgColor(cell.ColorNumber(DefaultFillColorNumber)),
|
||||
)
|
||||
testdraw.MustText(
|
||||
cvs,
|
||||
"⇦**⇨",
|
||||
image.Point{0, 0},
|
||||
)
|
||||
testcanvas.MustApply(cvs, ft)
|
||||
return ft
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "hides text hides scrolling arrows that are part of the text",
|
||||
opts: []Option{
|
||||
HideTextWith('*'),
|
||||
},
|
||||
canvas: image.Rect(0, 0, 4, 1),
|
||||
meta: &widgetapi.Meta{},
|
||||
events: []terminalapi.Event{
|
||||
&terminalapi.Keyboard{Key: 'a'},
|
||||
&terminalapi.Keyboard{Key: 'b'},
|
||||
&terminalapi.Keyboard{Key: '⇦'},
|
||||
&terminalapi.Keyboard{Key: '⇨'},
|
||||
&terminalapi.Keyboard{Key: 'e'},
|
||||
&terminalapi.Keyboard{Key: keyboard.KeyArrowLeft},
|
||||
&terminalapi.Keyboard{Key: keyboard.KeyArrowLeft},
|
||||
},
|
||||
want: func(size image.Point) *faketerm.Terminal {
|
||||
ft := faketerm.MustNew(size)
|
||||
cvs := testcanvas.MustNew(ft.Area())
|
||||
|
||||
testcanvas.MustSetAreaCells(
|
||||
cvs,
|
||||
image.Rect(0, 0, 4, 1),
|
||||
textFieldRune,
|
||||
cell.BgColor(cell.ColorNumber(DefaultFillColorNumber)),
|
||||
)
|
||||
testdraw.MustText(
|
||||
cvs,
|
||||
"⇦**⇨",
|
||||
image.Point{0, 0},
|
||||
)
|
||||
testcanvas.MustApply(cvs, ft)
|
||||
return ft
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "hides full-width runes with two hide runes",
|
||||
opts: []Option{
|
||||
HideTextWith('*'),
|
||||
},
|
||||
canvas: image.Rect(0, 0, 4, 1),
|
||||
meta: &widgetapi.Meta{},
|
||||
events: []terminalapi.Event{
|
||||
&terminalapi.Keyboard{Key: 'a'},
|
||||
&terminalapi.Keyboard{Key: 'b'},
|
||||
&terminalapi.Keyboard{Key: '世'},
|
||||
},
|
||||
want: func(size image.Point) *faketerm.Terminal {
|
||||
ft := faketerm.MustNew(size)
|
||||
cvs := testcanvas.MustNew(ft.Area())
|
||||
|
||||
testcanvas.MustSetAreaCells(
|
||||
cvs,
|
||||
image.Rect(0, 0, 4, 1),
|
||||
textFieldRune,
|
||||
cell.BgColor(cell.ColorNumber(DefaultFillColorNumber)),
|
||||
)
|
||||
testdraw.MustText(
|
||||
cvs,
|
||||
"⇦**",
|
||||
image.Point{0, 0},
|
||||
)
|
||||
testcanvas.MustApply(cvs, ft)
|
||||
return ft
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "sets custom text color",
|
||||
opts: []Option{
|
||||
TextColor(cell.ColorRed),
|
||||
},
|
||||
canvas: image.Rect(0, 0, 10, 1),
|
||||
meta: &widgetapi.Meta{},
|
||||
events: []terminalapi.Event{
|
||||
&terminalapi.Keyboard{Key: 'a'},
|
||||
&terminalapi.Keyboard{Key: 'b'},
|
||||
&terminalapi.Keyboard{Key: 'c'},
|
||||
},
|
||||
want: func(size image.Point) *faketerm.Terminal {
|
||||
ft := faketerm.MustNew(size)
|
||||
cvs := testcanvas.MustNew(ft.Area())
|
||||
|
||||
testcanvas.MustSetAreaCells(
|
||||
cvs,
|
||||
image.Rect(0, 0, 10, 1),
|
||||
textFieldRune,
|
||||
cell.BgColor(cell.ColorNumber(DefaultFillColorNumber)),
|
||||
)
|
||||
testdraw.MustText(
|
||||
cvs,
|
||||
"abc",
|
||||
image.Point{0, 0},
|
||||
draw.TextCellOpts(cell.FgColor(cell.ColorRed)),
|
||||
)
|
||||
testcanvas.MustApply(cvs, ft)
|
||||
return ft
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "displays written text and cursor when focused",
|
||||
canvas: image.Rect(0, 0, 10, 1),
|
||||
meta: &widgetapi.Meta{
|
||||
Focused: true,
|
||||
},
|
||||
events: []terminalapi.Event{
|
||||
&terminalapi.Keyboard{Key: 'a'},
|
||||
&terminalapi.Keyboard{Key: 'b'},
|
||||
&terminalapi.Keyboard{Key: 'c'},
|
||||
},
|
||||
want: func(size image.Point) *faketerm.Terminal {
|
||||
ft := faketerm.MustNew(size)
|
||||
cvs := testcanvas.MustNew(ft.Area())
|
||||
|
||||
testcanvas.MustSetAreaCells(
|
||||
cvs,
|
||||
image.Rect(0, 0, 10, 1),
|
||||
textFieldRune,
|
||||
cell.BgColor(cell.ColorNumber(DefaultFillColorNumber)),
|
||||
)
|
||||
testdraw.MustText(
|
||||
cvs,
|
||||
"abc",
|
||||
image.Point{0, 0},
|
||||
)
|
||||
testcanvas.MustSetCell(
|
||||
cvs,
|
||||
image.Point{3, 0},
|
||||
cursorRune,
|
||||
cell.BgColor(cell.ColorNumber(DefaultCursorColorNumber)),
|
||||
cell.FgColor(cell.ColorNumber(DefaultHighlightedColorNumber)),
|
||||
)
|
||||
testcanvas.MustApply(cvs, ft)
|
||||
return ft
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "moves cursor left",
|
||||
canvas: image.Rect(0, 0, 10, 1),
|
||||
meta: &widgetapi.Meta{
|
||||
Focused: true,
|
||||
},
|
||||
events: []terminalapi.Event{
|
||||
&terminalapi.Keyboard{Key: 'a'},
|
||||
&terminalapi.Keyboard{Key: 'b'},
|
||||
&terminalapi.Keyboard{Key: 'c'},
|
||||
&terminalapi.Keyboard{Key: keyboard.KeyArrowLeft},
|
||||
},
|
||||
want: func(size image.Point) *faketerm.Terminal {
|
||||
ft := faketerm.MustNew(size)
|
||||
cvs := testcanvas.MustNew(ft.Area())
|
||||
|
||||
testcanvas.MustSetAreaCells(
|
||||
cvs,
|
||||
image.Rect(0, 0, 10, 1),
|
||||
textFieldRune,
|
||||
cell.BgColor(cell.ColorNumber(DefaultFillColorNumber)),
|
||||
)
|
||||
testdraw.MustText(
|
||||
cvs,
|
||||
"abc",
|
||||
image.Point{0, 0},
|
||||
)
|
||||
testcanvas.MustSetCell(
|
||||
cvs,
|
||||
image.Point{2, 0},
|
||||
cursorRune,
|
||||
cell.BgColor(cell.ColorNumber(DefaultCursorColorNumber)),
|
||||
cell.FgColor(cell.ColorNumber(DefaultHighlightedColorNumber)),
|
||||
)
|
||||
testcanvas.MustApply(cvs, ft)
|
||||
return ft
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "sets custom highlight color",
|
||||
opts: []Option{
|
||||
HighlightedColor(cell.ColorRed),
|
||||
},
|
||||
canvas: image.Rect(0, 0, 10, 1),
|
||||
meta: &widgetapi.Meta{
|
||||
Focused: true,
|
||||
},
|
||||
events: []terminalapi.Event{
|
||||
&terminalapi.Keyboard{Key: 'a'},
|
||||
&terminalapi.Keyboard{Key: 'b'},
|
||||
&terminalapi.Keyboard{Key: 'c'},
|
||||
&terminalapi.Keyboard{Key: keyboard.KeyArrowLeft},
|
||||
},
|
||||
want: func(size image.Point) *faketerm.Terminal {
|
||||
ft := faketerm.MustNew(size)
|
||||
cvs := testcanvas.MustNew(ft.Area())
|
||||
|
||||
testcanvas.MustSetAreaCells(
|
||||
cvs,
|
||||
image.Rect(0, 0, 10, 1),
|
||||
textFieldRune,
|
||||
cell.BgColor(cell.ColorNumber(DefaultFillColorNumber)),
|
||||
)
|
||||
testdraw.MustText(
|
||||
cvs,
|
||||
"abc",
|
||||
image.Point{0, 0},
|
||||
)
|
||||
testcanvas.MustSetCell(
|
||||
cvs,
|
||||
image.Point{2, 0},
|
||||
cursorRune,
|
||||
cell.BgColor(cell.ColorNumber(DefaultCursorColorNumber)),
|
||||
cell.FgColor(cell.ColorRed),
|
||||
)
|
||||
testcanvas.MustApply(cvs, ft)
|
||||
return ft
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "moves cursor to start",
|
||||
canvas: image.Rect(0, 0, 10, 1),
|
||||
meta: &widgetapi.Meta{
|
||||
Focused: true,
|
||||
},
|
||||
events: []terminalapi.Event{
|
||||
&terminalapi.Keyboard{Key: 'a'},
|
||||
&terminalapi.Keyboard{Key: 'b'},
|
||||
&terminalapi.Keyboard{Key: 'c'},
|
||||
&terminalapi.Keyboard{Key: keyboard.KeyHome},
|
||||
},
|
||||
want: func(size image.Point) *faketerm.Terminal {
|
||||
ft := faketerm.MustNew(size)
|
||||
cvs := testcanvas.MustNew(ft.Area())
|
||||
|
||||
testcanvas.MustSetAreaCells(
|
||||
cvs,
|
||||
image.Rect(0, 0, 10, 1),
|
||||
textFieldRune,
|
||||
cell.BgColor(cell.ColorNumber(DefaultFillColorNumber)),
|
||||
)
|
||||
testdraw.MustText(
|
||||
cvs,
|
||||
"abc",
|
||||
image.Point{0, 0},
|
||||
)
|
||||
testcanvas.MustSetCell(
|
||||
cvs,
|
||||
image.Point{0, 0},
|
||||
cursorRune,
|
||||
cell.BgColor(cell.ColorNumber(DefaultCursorColorNumber)),
|
||||
cell.FgColor(cell.ColorNumber(DefaultHighlightedColorNumber)),
|
||||
)
|
||||
testcanvas.MustApply(cvs, ft)
|
||||
return ft
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "moves cursor right",
|
||||
canvas: image.Rect(0, 0, 10, 1),
|
||||
meta: &widgetapi.Meta{
|
||||
Focused: true,
|
||||
},
|
||||
events: []terminalapi.Event{
|
||||
&terminalapi.Keyboard{Key: 'a'},
|
||||
&terminalapi.Keyboard{Key: 'b'},
|
||||
&terminalapi.Keyboard{Key: 'c'},
|
||||
&terminalapi.Keyboard{Key: keyboard.KeyHome},
|
||||
&terminalapi.Keyboard{Key: keyboard.KeyArrowRight},
|
||||
},
|
||||
want: func(size image.Point) *faketerm.Terminal {
|
||||
ft := faketerm.MustNew(size)
|
||||
cvs := testcanvas.MustNew(ft.Area())
|
||||
|
||||
testcanvas.MustSetAreaCells(
|
||||
cvs,
|
||||
image.Rect(0, 0, 10, 1),
|
||||
textFieldRune,
|
||||
cell.BgColor(cell.ColorNumber(DefaultFillColorNumber)),
|
||||
)
|
||||
testdraw.MustText(
|
||||
cvs,
|
||||
"abc",
|
||||
image.Point{0, 0},
|
||||
)
|
||||
testcanvas.MustSetCell(
|
||||
cvs,
|
||||
image.Point{1, 0},
|
||||
cursorRune,
|
||||
cell.BgColor(cell.ColorNumber(DefaultCursorColorNumber)),
|
||||
cell.FgColor(cell.ColorNumber(DefaultHighlightedColorNumber)),
|
||||
)
|
||||
testcanvas.MustApply(cvs, ft)
|
||||
return ft
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "moves cursor to end",
|
||||
canvas: image.Rect(0, 0, 10, 1),
|
||||
meta: &widgetapi.Meta{
|
||||
Focused: true,
|
||||
},
|
||||
events: []terminalapi.Event{
|
||||
&terminalapi.Keyboard{Key: 'a'},
|
||||
&terminalapi.Keyboard{Key: 'b'},
|
||||
&terminalapi.Keyboard{Key: 'c'},
|
||||
&terminalapi.Keyboard{Key: keyboard.KeyHome},
|
||||
&terminalapi.Keyboard{Key: keyboard.KeyEnd},
|
||||
},
|
||||
want: func(size image.Point) *faketerm.Terminal {
|
||||
ft := faketerm.MustNew(size)
|
||||
cvs := testcanvas.MustNew(ft.Area())
|
||||
|
||||
testcanvas.MustSetAreaCells(
|
||||
cvs,
|
||||
image.Rect(0, 0, 10, 1),
|
||||
textFieldRune,
|
||||
cell.BgColor(cell.ColorNumber(DefaultFillColorNumber)),
|
||||
)
|
||||
testdraw.MustText(
|
||||
cvs,
|
||||
"abc",
|
||||
image.Point{0, 0},
|
||||
)
|
||||
testcanvas.MustSetCell(
|
||||
cvs,
|
||||
image.Point{3, 0},
|
||||
cursorRune,
|
||||
cell.BgColor(cell.ColorNumber(DefaultCursorColorNumber)),
|
||||
cell.FgColor(cell.ColorNumber(DefaultHighlightedColorNumber)),
|
||||
)
|
||||
testcanvas.MustApply(cvs, ft)
|
||||
return ft
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "deletes rune the cursor is on",
|
||||
canvas: image.Rect(0, 0, 10, 1),
|
||||
meta: &widgetapi.Meta{
|
||||
Focused: true,
|
||||
},
|
||||
events: []terminalapi.Event{
|
||||
&terminalapi.Keyboard{Key: 'a'},
|
||||
&terminalapi.Keyboard{Key: 'b'},
|
||||
&terminalapi.Keyboard{Key: 'c'},
|
||||
&terminalapi.Keyboard{Key: keyboard.KeyHome},
|
||||
&terminalapi.Keyboard{Key: keyboard.KeyDelete},
|
||||
},
|
||||
want: func(size image.Point) *faketerm.Terminal {
|
||||
ft := faketerm.MustNew(size)
|
||||
cvs := testcanvas.MustNew(ft.Area())
|
||||
|
||||
testcanvas.MustSetAreaCells(
|
||||
cvs,
|
||||
image.Rect(0, 0, 10, 1),
|
||||
textFieldRune,
|
||||
cell.BgColor(cell.ColorNumber(DefaultFillColorNumber)),
|
||||
)
|
||||
testdraw.MustText(
|
||||
cvs,
|
||||
"bc",
|
||||
image.Point{0, 0},
|
||||
)
|
||||
testcanvas.MustSetCell(
|
||||
cvs,
|
||||
image.Point{0, 0},
|
||||
cursorRune,
|
||||
cell.BgColor(cell.ColorNumber(DefaultCursorColorNumber)),
|
||||
cell.FgColor(cell.ColorNumber(DefaultHighlightedColorNumber)),
|
||||
)
|
||||
testcanvas.MustApply(cvs, ft)
|
||||
return ft
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "deletes rune just before the cursor",
|
||||
canvas: image.Rect(0, 0, 10, 1),
|
||||
meta: &widgetapi.Meta{
|
||||
Focused: true,
|
||||
},
|
||||
events: []terminalapi.Event{
|
||||
&terminalapi.Keyboard{Key: 'a'},
|
||||
&terminalapi.Keyboard{Key: 'b'},
|
||||
&terminalapi.Keyboard{Key: 'c'},
|
||||
&terminalapi.Keyboard{Key: keyboard.KeyBackspace},
|
||||
},
|
||||
want: func(size image.Point) *faketerm.Terminal {
|
||||
ft := faketerm.MustNew(size)
|
||||
cvs := testcanvas.MustNew(ft.Area())
|
||||
|
||||
testcanvas.MustSetAreaCells(
|
||||
cvs,
|
||||
image.Rect(0, 0, 10, 1),
|
||||
textFieldRune,
|
||||
cell.BgColor(cell.ColorNumber(DefaultFillColorNumber)),
|
||||
)
|
||||
testdraw.MustText(
|
||||
cvs,
|
||||
"ab",
|
||||
image.Point{0, 0},
|
||||
)
|
||||
testcanvas.MustSetCell(
|
||||
cvs,
|
||||
image.Point{2, 0},
|
||||
cursorRune,
|
||||
cell.BgColor(cell.ColorNumber(DefaultCursorColorNumber)),
|
||||
cell.FgColor(cell.ColorNumber(DefaultHighlightedColorNumber)),
|
||||
)
|
||||
testcanvas.MustApply(cvs, ft)
|
||||
return ft
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
@ -558,24 +1298,38 @@ func TestTextInput(t *testing.T) {
|
||||
return
|
||||
}
|
||||
|
||||
for _, ev := range tc.events {
|
||||
for i, ev := range tc.events {
|
||||
switch e := ev.(type) {
|
||||
case *terminalapi.Mouse:
|
||||
err := ti.Mouse(e)
|
||||
if (err != nil) != tc.wantEventErr {
|
||||
t.Errorf("Mouse => unexpected error: %v, wantEventErr: %v", err, tc.wantEventErr)
|
||||
}
|
||||
if err != nil {
|
||||
return
|
||||
// Only the last event in test cases is the one that triggers the callback.
|
||||
if i == len(tc.events)-1 {
|
||||
if (err != nil) != tc.wantEventErr {
|
||||
t.Errorf("Mouse => unexpected error: %v, wantEventErr: %v", err, tc.wantEventErr)
|
||||
}
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
} else {
|
||||
if err != nil {
|
||||
t.Fatalf("Mouse => unexpected error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
case *terminalapi.Keyboard:
|
||||
err := ti.Keyboard(e)
|
||||
if (err != nil) != tc.wantEventErr {
|
||||
t.Errorf("Keyboard => unexpected error: %v, wantEventErr: %v", err, tc.wantEventErr)
|
||||
}
|
||||
if err != nil {
|
||||
return
|
||||
// Only the last event in test cases is the one that triggers the callback.
|
||||
if i == len(tc.events)-1 {
|
||||
if (err != nil) != tc.wantEventErr {
|
||||
t.Errorf("Keyboard => unexpected error: %v, wantEventErr: %v", err, tc.wantEventErr)
|
||||
}
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
} else {
|
||||
if err != nil {
|
||||
t.Fatalf("Keyboard => unexpected error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
default:
|
||||
@ -625,6 +1379,75 @@ func TestTextInput(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestTextInputRead(t *testing.T) {
|
||||
tests := []struct {
|
||||
desc string
|
||||
events []terminalapi.Event
|
||||
want string
|
||||
}{
|
||||
{
|
||||
desc: "reads empty without events",
|
||||
events: []terminalapi.Event{},
|
||||
want: "",
|
||||
},
|
||||
{
|
||||
desc: "reads written text",
|
||||
events: []terminalapi.Event{
|
||||
&terminalapi.Keyboard{Key: 'a'},
|
||||
&terminalapi.Keyboard{Key: 'b'},
|
||||
&terminalapi.Keyboard{Key: 'c'},
|
||||
},
|
||||
want: "abc",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
ti, err := New()
|
||||
if err != nil {
|
||||
t.Fatalf("New => unexpected error: %v", err)
|
||||
}
|
||||
|
||||
for _, ev := range tc.events {
|
||||
switch e := ev.(type) {
|
||||
case *terminalapi.Keyboard:
|
||||
err := ti.Keyboard(e)
|
||||
if err != nil {
|
||||
t.Fatalf("Keyboard => unexpected error: %v", err)
|
||||
}
|
||||
|
||||
default:
|
||||
t.Fatalf("unsupported event type: %T", ev)
|
||||
}
|
||||
}
|
||||
|
||||
got := ti.Read()
|
||||
if got != tc.want {
|
||||
t.Errorf("Read => %q, want %q", got, tc.want)
|
||||
}
|
||||
|
||||
gotRC := ti.ReadAndClear()
|
||||
if gotRC != tc.want {
|
||||
t.Errorf("ReadAndClear after clearing => %q, want %q", gotRC, tc.want)
|
||||
}
|
||||
|
||||
// Both should now return empty content.
|
||||
{
|
||||
want := ""
|
||||
got := ti.Read()
|
||||
if got != want {
|
||||
t.Errorf("Read after clearing => %q, want %q", got, want)
|
||||
}
|
||||
|
||||
gotRC := ti.ReadAndClear()
|
||||
if gotRC != want {
|
||||
t.Errorf("ReadAndClear after clearing => %q, want %q", gotRC, want)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestOptions(t *testing.T) {
|
||||
tests := []struct {
|
||||
desc string
|
||||
|
@ -25,7 +25,6 @@ import (
|
||||
"github.com/mum4k/termdash/container"
|
||||
"github.com/mum4k/termdash/container/grid"
|
||||
"github.com/mum4k/termdash/keyboard"
|
||||
"github.com/mum4k/termdash/linestyle"
|
||||
"github.com/mum4k/termdash/terminal/termbox"
|
||||
"github.com/mum4k/termdash/widgets/button"
|
||||
"github.com/mum4k/termdash/widgets/segmentdisplay"
|
||||
@ -129,7 +128,7 @@ func main() {
|
||||
input, err := textinput.New(
|
||||
textinput.Label("New text:", cell.FgColor(cell.ColorBlue)),
|
||||
textinput.MaxWidthCells(20),
|
||||
textinput.Border(linestyle.Light),
|
||||
//textinput.Border(linestyle.Light),
|
||||
textinput.PlaceHolder("Enter any text"),
|
||||
)
|
||||
if err != nil {
|
||||
|
Loading…
x
Reference in New Issue
Block a user