1
0
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:
Jakub Sobon 2019-04-25 23:44:14 -04:00
parent 1df5298809
commit 886f970586
No known key found for this signature in database
GPG Key ID: F2451A77FB05D3B7
4 changed files with 918 additions and 29 deletions

View File

@ -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
})
}

View File

@ -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()
}

View File

@ -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

View File

@ -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 {