diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7f702f7..f350786 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -35,6 +35,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added
+#### Text input form functionality with keyboard navigation
+
+- added a new `formdemo` that demonstrates a text input form with keyboard
+ navigation.
+
#### Infrastructure changes
- `container` now allows users to configure keyboard keys that move focus to
diff --git a/README.md b/README.md
index 466df32..93cee5c 100644
--- a/README.md
+++ b/README.md
@@ -55,6 +55,7 @@ To install this library, run the following:
```go
go get -u github.com/mum4k/termdash
+cd github.com/mum4k/termdash
```
# Usage
@@ -63,7 +64,7 @@ The usage of most of these elements is demonstrated in
[termdashdemo.go](termdashdemo/termdashdemo.go). To execute the demo:
```go
-go run github.com/mum4k/termdash/termdashdemo/termdashdemo.go
+go run termdashdemo/termdashdemo.go
```
# Documentation
@@ -80,7 +81,7 @@ Run the
[buttondemo](widgets/button/buttondemo/buttondemo.go).
```go
-go run github.com/mum4k/termdash/widgets/button/buttondemo/buttondemo.go
+go run widgets/button/buttondemo/buttondemo.go
```
[
](widgets/button/buttondemo/buttondemo.go)
@@ -92,18 +93,26 @@ submitting text data. Run the
[textinputdemo](widgets/textinput/textinputdemo/textinputdemo.go).
```go
-go run github.com/mum4k/termdash/widgets/textinput/textinputdemo/textinputdemo.go
+go run widgets/textinput/textinputdemo/textinputdemo.go
```
[
](widgets/textinput/textinputdemo/textinputdemo.go)
+Can be used to create text input forms that support keyboard navigation:
+
+```go
+go run widgets/textinput/formdemo/formdemo.go
+```
+
+[
](widgets/textinput/formdemo/formdemo.go)
+
## The Gauge
Displays the progress of an operation. Run the
[gaugedemo](widgets/gauge/gaugedemo/gaugedemo.go).
```go
-go run github.com/mum4k/termdash/widgets/gauge/gaugedemo/gaugedemo.go
+go run widgets/gauge/gaugedemo/gaugedemo.go
```
[
](widgets/gauge/gaugedemo/gaugedemo.go)
@@ -114,7 +123,7 @@ Visualizes progress of an operation as a partial or a complete donut. Run the
[donutdemo](widgets/donut/donutdemo/donutdemo.go).
```go
-go run github.com/mum4k/termdash/widgets/donut/donutdemo/donutdemo.go
+go run widgets/donut/donutdemo/donutdemo.go
```
[
](widgets/donut/donutdemo/donutdemo.go)
@@ -125,7 +134,7 @@ Displays text content, supports trimming and scrolling of content. Run the
[textdemo](widgets/text/textdemo/textdemo.go).
```go
-go run github.com/mum4k/termdash/widgets/text/textdemo/textdemo.go
+go run widgets/text/textdemo/textdemo.go
```
[
](widgets/text/textdemo/textdemo.go)
@@ -137,7 +146,7 @@ sub-cell height. Run the
[sparklinedemo](widgets/sparkline/sparklinedemo/sparklinedemo.go).
```go
-go run github.com/mum4k/termdash/widgets/sparkline/sparklinedemo/sparklinedemo.go
+go run widgets/sparkline/sparklinedemo/sparklinedemo.go
```
[
](widgets/sparkline/sparklinedemo/sparklinedemo.go)
@@ -148,7 +157,7 @@ Displays multiple bars showing relative ratios of values. Run the
[barchartdemo](widgets/barchart/barchartdemo/barchartdemo.go).
```go
-go run github.com/mum4k/termdash/widgets/barchart/barchartdemo/barchartdemo.go
+go run widgets/barchart/barchartdemo/barchartdemo.go
```
[
](widgets/barchart/barchartdemo/barchartdemo.go)
@@ -160,7 +169,7 @@ events. Run the
[linechartdemo](widgets/linechart/linechartdemo/linechartdemo.go).
```go
-go run github.com/mum4k/termdash/widgets/linechart/linechartdemo/linechartdemo.go
+go run widgets/linechart/linechartdemo/linechartdemo.go
```
[
](widgets/linechart/linechartdemo/linechartdemo.go)
@@ -171,7 +180,7 @@ Displays text by simulating a 16-segment display. Run the
[segmentdisplaydemo](widgets/segmentdisplay/segmentdisplaydemo/segmentdisplaydemo.go).
```go
-go run github.com/mum4k/termdash/widgets/segmentdisplay/segmentdisplaydemo/segmentdisplaydemo.go
+go run widgets/segmentdisplay/segmentdisplaydemo/segmentdisplaydemo.go
```
[
](widgets/segmentdisplay/segmentdisplaydemo/segmentdisplaydemo.go)
diff --git a/doc/images/formdemo.gif b/doc/images/formdemo.gif
new file mode 100644
index 0000000..c11b5af
Binary files /dev/null and b/doc/images/formdemo.gif differ
diff --git a/widgets/textinput/formdemo/formdemo.go b/widgets/textinput/formdemo/formdemo.go
new file mode 100644
index 0000000..723318a
--- /dev/null
+++ b/widgets/textinput/formdemo/formdemo.go
@@ -0,0 +1,326 @@
+// Copyright 2020 Google Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Binary formdemo creates a form that accepts text inputs and supports
+// keyboard navigation.
+package main
+
+import (
+ "context"
+ "fmt"
+ "os/user"
+ "time"
+
+ "github.com/mum4k/termdash"
+ "github.com/mum4k/termdash/align"
+ "github.com/mum4k/termdash/cell"
+ "github.com/mum4k/termdash/container"
+ "github.com/mum4k/termdash/keyboard"
+ "github.com/mum4k/termdash/linestyle"
+ "github.com/mum4k/termdash/terminal/tcell"
+ "github.com/mum4k/termdash/widgets/button"
+ "github.com/mum4k/termdash/widgets/text"
+ "github.com/mum4k/termdash/widgets/textinput"
+)
+
+// buttonChunks creates the text chunks for a button from the provided text.
+func buttonChunks(text string) []*button.TextChunk {
+ if len(text) == 0 {
+ return nil
+ }
+ first := string(text[0])
+ rest := string(text[1:])
+
+ return []*button.TextChunk{
+ button.NewChunk(
+ "<",
+ button.TextCellOpts(cell.FgColor(cell.ColorWhite)),
+ button.FocusedTextCellOpts(cell.FgColor(cell.ColorBlack)),
+ button.PressedTextCellOpts(cell.FgColor(cell.ColorBlack)),
+ ),
+ button.NewChunk(
+ first,
+ button.TextCellOpts(cell.FgColor(cell.ColorRed)),
+ ),
+ button.NewChunk(
+ rest,
+ button.TextCellOpts(cell.FgColor(cell.ColorWhite)),
+ button.FocusedTextCellOpts(cell.FgColor(cell.ColorBlack)),
+ button.PressedTextCellOpts(cell.FgColor(cell.ColorBlack)),
+ ),
+ button.NewChunk(
+ ">",
+ button.TextCellOpts(cell.FgColor(cell.ColorWhite)),
+ button.FocusedTextCellOpts(cell.FgColor(cell.ColorBlack)),
+ button.PressedTextCellOpts(cell.FgColor(cell.ColorBlack)),
+ ),
+ }
+}
+
+// form contains the elements of a text input form.
+type form struct {
+ // userInput is a text input that accepts user name.
+ userInput *textinput.TextInput
+ // uidInput is a text input that accepts UID.
+ uidInput *textinput.TextInput
+ // gidInput is a text input that accepts GID.
+ gidInput *textinput.TextInput
+ // homeInput is a text input that accepts path to the home folder.
+ homeInput *textinput.TextInput
+
+ // submitB is a button that submits the form.
+ submitB *button.Button
+ // cancelB is a button that exist the application.
+ cancelB *button.Button
+}
+
+// newForm returns a new form instance.
+// The cancel argument is a function that terminates the application when called.
+func newForm(cancel context.CancelFunc) (*form, error) {
+ var username string
+ u, err := user.Current()
+ if err != nil {
+ username = "mum4k"
+ } else {
+ username = u.Username
+ }
+
+ userInput, err := textinput.New(
+ textinput.Label("Username: ", cell.FgColor(cell.ColorNumber(33))),
+ textinput.DefaultText(username),
+ textinput.MaxWidthCells(20),
+ textinput.ExclusiveKeyboardOnFocus(),
+ )
+ uidInput, err := textinput.New(
+ textinput.Label("UID: ", cell.FgColor(cell.ColorNumber(33))),
+ textinput.DefaultText("1000"),
+ textinput.MaxWidthCells(20),
+ textinput.ExclusiveKeyboardOnFocus(),
+ )
+ gidInput, err := textinput.New(
+ textinput.Label("GID: ", cell.FgColor(cell.ColorNumber(33))),
+ textinput.DefaultText("1000"),
+ textinput.MaxWidthCells(20),
+ textinput.ExclusiveKeyboardOnFocus(),
+ )
+ homeInput, err := textinput.New(
+ textinput.Label("Home: ", cell.FgColor(cell.ColorNumber(33))),
+ textinput.DefaultText(fmt.Sprintf("/home/%s", username)),
+ textinput.MaxWidthCells(20),
+ textinput.ExclusiveKeyboardOnFocus(),
+ )
+
+ submitB, err := button.NewFromChunks(buttonChunks("Submit"), nil,
+ button.Key(keyboard.KeyEnter),
+ button.GlobalKeys('s', 'S'),
+ button.DisableShadow(),
+ button.Height(1),
+ button.TextHorizontalPadding(0),
+ button.FillColor(cell.ColorBlack),
+ button.FocusedFillColor(cell.ColorNumber(117)),
+ button.PressedFillColor(cell.ColorNumber(220)),
+ )
+ if err != nil {
+ panic(err)
+ }
+
+ cancelB, err := button.NewFromChunks(buttonChunks("Cancel"), func() error {
+ cancel()
+ return nil
+ },
+ button.FillColor(cell.ColorNumber(220)),
+ button.Key(keyboard.KeyEnter),
+ button.GlobalKeys('c', 'C'),
+ button.DisableShadow(),
+ button.Height(1),
+ button.TextHorizontalPadding(0),
+ button.FillColor(cell.ColorBlack),
+ button.FocusedFillColor(cell.ColorNumber(117)),
+ button.PressedFillColor(cell.ColorNumber(220)),
+ )
+ if err != nil {
+ panic(err)
+ }
+
+ return &form{
+ userInput: userInput,
+ uidInput: uidInput,
+ gidInput: gidInput,
+ homeInput: homeInput,
+ submitB: submitB,
+ cancelB: cancelB,
+ }, nil
+}
+
+// formLayout updates the container into a layout with text inputs and buttons.
+func formLayout(c *container.Container, f *form) error {
+ return c.Update("root",
+ container.KeyFocusNext(keyboard.KeyTab),
+ container.KeyFocusGroupsNext(keyboard.KeyArrowDown, 1),
+ container.KeyFocusGroupsPrevious(keyboard.KeyArrowUp, 1),
+ container.KeyFocusGroupsNext(keyboard.KeyArrowRight, 2),
+ container.KeyFocusGroupsPrevious(keyboard.KeyArrowLeft, 2),
+ container.SplitHorizontal(
+ container.Top(
+ container.Border(linestyle.Light),
+ container.SplitHorizontal(
+ container.Top(
+ container.SplitHorizontal(
+ container.Top(
+ container.Focused(),
+ container.KeyFocusGroups(1),
+ container.PlaceWidget(f.userInput),
+ ),
+ container.Bottom(
+ container.KeyFocusGroups(1),
+ container.KeyFocusSkip(),
+ container.PlaceWidget(f.uidInput),
+ ),
+ ),
+ ),
+ container.Bottom(
+ container.SplitHorizontal(
+ container.Top(
+ container.KeyFocusGroups(1),
+ container.KeyFocusSkip(),
+ container.PlaceWidget(f.gidInput),
+ ),
+ container.Bottom(
+ container.KeyFocusGroups(1),
+ container.KeyFocusSkip(),
+ container.PlaceWidget(f.homeInput),
+ ),
+ ),
+ ),
+ ),
+ ),
+ container.Bottom(
+ container.SplitHorizontal(
+ container.Top(
+ container.SplitVertical(
+ container.Left(
+ container.KeyFocusGroups(1, 2),
+ container.PlaceWidget(f.submitB),
+ container.AlignHorizontal(align.HorizontalRight),
+ container.PaddingRight(5),
+ ),
+ container.Right(
+ container.KeyFocusGroups(1, 2),
+ container.PlaceWidget(f.cancelB),
+ container.AlignHorizontal(align.HorizontalLeft),
+ container.PaddingLeft(5),
+ ),
+ ),
+ ),
+ container.Bottom(
+ container.KeyFocusSkip(),
+ ),
+ container.SplitFixed(3),
+ ),
+ ),
+ container.SplitFixed(6),
+ ),
+ )
+}
+
+// submitLayout updates the container into a layout that displays the submitted data.
+// The cancel argument is a function that terminates Termdash when called.
+func submitLayout(c *container.Container, f *form, cancel context.CancelFunc) error {
+ t, err := text.New()
+ if err != nil {
+ return err
+ }
+
+ if err := t.Write("Submitted data:\n\n"); err != nil {
+ return err
+ }
+ if err := t.Write(fmt.Sprintf("Username: %s\n", f.userInput.Read())); err != nil {
+ return err
+ }
+ if err := t.Write(fmt.Sprintf("UID: %s\n", f.uidInput.Read())); err != nil {
+ return err
+ }
+ if err := t.Write(fmt.Sprintf("GID: %s\n", f.gidInput.Read())); err != nil {
+ return err
+ }
+ if err := t.Write(fmt.Sprintf("Home: %s\n", f.homeInput.Read())); err != nil {
+ return err
+ }
+
+ okB, err := button.NewFromChunks(buttonChunks("OK"), func() error {
+ cancel()
+ return nil
+ },
+ button.FillColor(cell.ColorNumber(220)),
+ button.Key(keyboard.KeyEnter),
+ button.GlobalKeys('o', 'O'),
+ button.DisableShadow(),
+ button.Height(1),
+ button.TextHorizontalPadding(0),
+ button.FillColor(cell.ColorBlack),
+ button.FocusedFillColor(cell.ColorNumber(117)),
+ button.PressedFillColor(cell.ColorNumber(220)),
+ )
+ if err != nil {
+ return err
+ }
+
+ return c.Update("root",
+ container.SplitHorizontal(
+ container.Top(
+ container.SplitVertical(
+ container.Left(),
+ container.Right(
+ container.PlaceWidget(t),
+ ),
+ container.SplitPercent(33),
+ ),
+ ),
+ container.Bottom(
+ container.Focused(),
+ container.PlaceWidget(okB),
+ ),
+ container.SplitFixed(7),
+ ),
+ )
+}
+
+func main() {
+ t, err := tcell.New()
+ if err != nil {
+ panic(err)
+ }
+ defer t.Close()
+
+ ctx, cancel := context.WithCancel(context.Background())
+ c, err := container.New(t, container.ID("root"))
+ if err != nil {
+ panic(err)
+ }
+
+ f, err := newForm(cancel)
+ if err != nil {
+ panic(err)
+ }
+ f.submitB.SetCallback(func() error {
+ return submitLayout(c, f, cancel)
+ })
+ if err := formLayout(c, f); err != nil {
+ panic(err)
+ }
+
+ if err := termdash.Run(ctx, t, c, termdash.RedrawInterval(100*time.Millisecond)); err != nil {
+ panic(err)
+ }
+}