interact/ui
April 28, 2026 ยท View on GitHub
interact/ui provides a new interaction abstraction layer for terminal components.
It is designed for backend-driven interactive flows, and currently includes:
InputConfirmSelectMultiSelect
Current status:
- stable component models and result types
plainbackend is available- raw-terminal
readlinebackend is available - existing
interactpackage APIs remain unchanged Inputsupports UTF-8 text editing and common line-editing shortcuts- selection components keep validation errors visible until the next user input
- filtering and resize events are not implemented yet
Packages
github.com/gookit/cliui/interact/uigithub.com/gookit/cliui/interact/backendgithub.com/gookit/cliui/interact/backend/plaingithub.com/gookit/cliui/interact/backend/readlinegithub.com/gookit/cliui/interact/backend/fake
Install
go get github.com/gookit/cliui/interact/ui
Quick Example
Input
Input reads one text line. With the readline backend it supports cursor movement, deletion, UTF-8 editing, and other real-terminal editing behavior; with the plain backend it reads one submitted line.
package main
import (
"context"
"fmt"
"github.com/gookit/cliui/interact/backend/plain"
"github.com/gookit/cliui/interact/ui"
)
func main() {
be := plain.New()
input := ui.NewInput("Your name")
input.Default = "guest"
name, err := input.Run(context.Background(), be)
if err != nil {
panic(err)
}
fmt.Println("name:", name)
}
Minimal form:
be := plain.New()
name, err := ui.NewInput("Your name").Run(context.Background(), be)
Output preview:
Your name [guest]: tom
name: tom
Confirm
Confirm handles a yes/no choice. It shows the default option, accepts y/n, and can be toggled with arrow keys when using the readline backend.
package main
import (
"context"
"fmt"
"github.com/gookit/cliui/interact/backend/plain"
"github.com/gookit/cliui/interact/ui"
)
func main() {
be := plain.New()
confirm := ui.NewConfirm("Continue", true)
ok, err := confirm.Run(context.Background(), be)
if err != nil {
panic(err)
}
fmt.Println("confirmed:", ok)
}
Minimal form:
be := plain.New()
ok, err := ui.NewConfirm("Continue", true).Run(context.Background(), be)
Output preview:
Continue [Y/n]: y
confirmed: true
Select
Select chooses one result from an Item list. Each item can provide a stable Key, display Label, and application-facing Value.
package main
import (
"context"
"fmt"
"github.com/gookit/cliui/interact/backend/plain"
"github.com/gookit/cliui/interact/ui"
)
func main() {
be := plain.New()
selectOne := ui.NewSelect("Choose env", []ui.Item{
{Key: "dev", Label: "Development", Value: "dev"},
{Key: "prod", Label: "Production", Value: "prod"},
})
selectOne.DefaultKey = "dev"
selectOne.Filterable = true
selectOne.PageSize = 10
result, err := selectOne.Run(context.Background(), be)
if err != nil {
panic(err)
}
fmt.Println("selected:", result.Key, result.Value)
}
Minimal form:
be := plain.New()
result, err := ui.NewSelect("Choose env", []ui.Item{
{Key: "dev", Label: "Development", Value: "dev"},
{Key: "prod", Label: "Production", Value: "prod"},
}).Run(context.Background(), be)
When filtering is enabled with the readline backend, type to narrow the options, use Backspace to delete filter text, and use Ctrl+U to clear the filter.
Output preview:
Choose env
> dev Development
prod Production
selected: dev dev
MultiSelect
MultiSelect chooses multiple results from an Item list. It supports defaults, minimum selection count, and disabled items.
package main
import (
"context"
"fmt"
"github.com/gookit/cliui/interact/backend/plain"
"github.com/gookit/cliui/interact/ui"
)
func main() {
be := plain.New()
selectMany := ui.NewMultiSelect("Choose services", []ui.Item{
{Key: "api", Label: "API", Value: "api"},
{Key: "job", Label: "Job Worker", Value: "job"},
{Key: "web", Label: "Web", Value: "web"},
})
selectMany.DefaultKeys = []string{"api", "web"}
selectMany.MinSelected = 1
selectMany.Filterable = true
selectMany.PageSize = 10
result, err := selectMany.Run(context.Background(), be)
if err != nil {
panic(err)
}
fmt.Println("selected keys:", result.Keys)
}
Minimal form:
be := plain.New()
result, err := ui.NewMultiSelect("Choose services", []ui.Item{
{Key: "api", Label: "API", Value: "api"},
{Key: "web", Label: "Web", Value: "web"},
}).Run(context.Background(), be)
Filtering only changes which options are visible. Already selected items are preserved when the filter changes.
Output preview:
Choose services
> [x] api API
[ ] job Job Worker
[x] web Web
selected keys: [api web]
Readline Backend
Use readline backend when you want event-driven interaction on a real terminal.
readline.New() will fall back to the plain backend automatically when stdin is not a TTY.
If you want strict TTY-only behavior, use readline.NewStrict().
package main
import (
"context"
"fmt"
"github.com/gookit/cliui/interact/backend/readline"
"github.com/gookit/cliui/interact/ui"
)
func main() {
be := readline.New()
input := ui.NewInput("Your name")
input.Default = "guest"
name, err := input.Run(context.Background(), be)
if err != nil {
panic(err)
}
selectOne := ui.NewSelect("Choose env", []ui.Item{
{Key: "dev", Label: "Development", Value: "dev"},
{Key: "prod", Label: "Production", Value: "prod"},
})
selectOne.DefaultKey = "dev"
env, err := selectOne.Run(context.Background(), be)
if err != nil {
panic(err)
}
fmt.Println("name:", name)
fmt.Println("env:", env.Key)
}
Output preview:
Your name [guest]: tom
Choose env
> dev Development
prod Production
name: tom
env: dev
Fake Backend
Use fake backend in tests to feed normalized events without a real terminal:
be := fake.New(
backend.Event{Type: backend.EventKey, Text: "tom"},
backend.Event{Type: backend.EventKey, Key: backend.KeyEnter},
)
name, err := ui.NewInput("Your name").Run(context.Background(), be)
Output preview:
Your name: tom
Notes
plainbackend uses line-based input and works with ordinary stdin/stdout streams.plainbackend does not provide per-key navigation; select components accept item keys, and multi-select accepts comma-separated item keys.readlinebackend uses raw terminal input and supports UTF-8 text, arrow keys, Home/End, Delete, Backspace, Tab, Shift+Tab, PageUp/PageDown, Space, Enter, Esc and Ctrl+C.readline.New()falls back toplainwhen a real terminal is unavailable.readline.NewStrict()returns an error when stdin is not a real terminal.fakebackend is intended for deterministic component tests.Selectuses single-key selection by item key.MultiSelectuses comma-separated item keys.ErrAbortedis returned when the current interaction is canceled.SelectandMultiSelectsupport disabled items and default values.SelectandMultiSelectcan enable per-key filtering withFilterable.PageSizelimits the number of visible option rows; when it is0, components calculate it from terminal height.Selectshows the current highlighted item in a dedicated status line.MultiSelectshows both the current highlighted item and the selected key summary.- Validation and selection errors stay visible until the next input event changes the component state.
Inputcursor placement accounts for display width, so CJK text is handled correctly in supported terminals.- The
readlinebackend emits resize events when terminal size changes. Components recalculate visible rows while preserving the current filter and selection state.
Key Bindings
For the current readline backend:
Input: type to insert,Left/Rightto move,Home/EndorCtrl+A/Ctrl+Eto jump,Backspace/Deleteto edit,Ctrl+Uto delete before cursor,Ctrl+Kto delete after cursor,Ctrl+Wto delete previous word,Enterto submitConfirm:Left/Rightto switch,y/nto choose,Enterto submit current valueSelect:Up/DownorTab/Shift+Tabto move,PageUp/PageDownto jump,Enterto confirm; when filtering is enabled, normal text appends to the filter,Backspacedeletes filter text, andCtrl+Uclears the filterMultiSelect:Up/DownorTab/Shift+Tabto move,PageUp/PageDownto jump,Spaceto toggle,Enterto confirm; when filtering is enabled, normal text appends to the filter,Backspacedeletes filter text, andCtrl+Uclears the filter
Backend Behavior
plain
plain is intended for non-TTY input, tests, redirected stdin and simple command-line flows. It reads one line at a time:
Inputreads the submitted line as the value.Confirmacceptsyes/no,y/n, or an empty line for the default.Selectaccepts an item key, or the default key on an empty line.MultiSelectaccepts comma-separated item keys, or default keys on an empty line.plaindoes not provide per-key filtering. Even whenFilterableis set, input is still parsed as a submitted line.
readline
readline is intended for real terminal interaction. It normalizes terminal events for the UI components:
- UTF-8 input is read as runes instead of raw bytes.
- Common CSI and SS3 escape sequences are mapped for arrows, Home/End, Delete, Shift+Tab and PageUp/PageDown.
EscandCtrl+Ccancel the current component withErrAborted.- Filterable
SelectandMultiSelectupdate the filter on each text key event. - Resize events trigger a redraw while keeping the current filter, highlighted item, and selected multi-select items.
readline.New()falls back toplainautomatically outside a TTY; usereadline.NewStrict()for TTY-only behavior.
Current Limits
- The
plainbackend does not support live filtering or resize events. - Very delayed escape sequences may be treated as a standalone
Esc; common terminal key sequences are handled when available in the input buffer.
Demo
Run the interactive demo command:
go run ./examples/interact-ui-demo
If you run it in a non-TTY environment, readline.New() will fall back to the plain backend automatically.
Example Test
Run the package examples, including the readline fallback example:
go test ./interact/ui -run Example -v