Repository: manifoldco/promptui
Branch: master
Commit: c2e487d3597f
Files: 41
Total size: 86.1 KB
Directory structure:
gitextract_895fqpq3/
├── .github/
│ ├── CONTRIBUTING.md
│ └── listbot.md
├── .gitignore
├── .golangci.yml
├── .travis.yml
├── CHANGELOG.md
├── CODE_OF_CONDUCT.md
├── LICENSE.md
├── Makefile
├── README.md
├── _examples/
│ ├── confirm/
│ │ └── main.go
│ ├── custom_prompt/
│ │ └── main.go
│ ├── custom_select/
│ │ └── main.go
│ ├── prompt/
│ │ └── main.go
│ ├── prompt_default/
│ │ └── main.go
│ ├── prompt_password/
│ │ └── main.go
│ ├── select/
│ │ └── main.go
│ └── select_add/
│ └── main.go
├── codes.go
├── codes_test.go
├── cursor.go
├── cursor_test.go
├── example_main_test.go
├── example_prompt_test.go
├── example_select_test.go
├── example_selectwithadd_test.go
├── go.mod
├── go.sum
├── keycodes.go
├── keycodes_other.go
├── keycodes_windows.go
├── list/
│ ├── list.go
│ └── list_test.go
├── prompt.go
├── promptui.go
├── screenbuf/
│ ├── screenbuf.go
│ └── screenbuf_test.go
├── select.go
├── select_test.go
├── styles.go
└── styles_windows.go
================================================
FILE CONTENTS
================================================
================================================
FILE: .github/CONTRIBUTING.md
================================================
# Contributing Guidelines
Contributions are always welcome; however, please read this document in its
entirety before submitting a Pull Request or Reporting a bug.
### Table of Contents
- [Reporting a bug](#reporting-a-bug)
- [Security disclosure](#security-disclosure)
- [Creating an issue](#creating-an-issue)
- [Feature requests](#feature-requests)
- [Opening a pull request](#opening-a-pull-request)
- [Code of Conduct](#code-of-conduct)
- [License](#license)
- [Contributor license agreement](#contributor-license-agreement)
---------------
# Reporting a Bug
Think you've found a bug? Let us know!
### Security disclosure
Security is a top priority for us. If you have encountered a security issue
please responsibly disclose it by following our [security
disclosure](../docs/security.md) document.
# Creating an Issue
Your issue must follow these guidelines for it to be considered:
#### Before submitting
- Check you’re on the latest version, we may have already fixed your bug!
- [Search our issue
tracker](https://github.com/manifoldco/promptui/issues/search&type=issues)
for your problem, someone may have already reported it
# Opening a Pull Request
To contribute, [fork](https://help.github.com/articles/fork-a-repo/)
`promptui`, commit your changes, and [open a pull
request](https://help.github.com/articles/using-pull-requests/).
Your request will be reviewed as soon as possible. You may be asked to make
changes to your submission during the review process.
#### Before submitting
- Test your change thoroughly
- you can run `make bootstrap && make` to ensure that the continuous integration
build will succeed
# Code of Conduct
All community members are expected to adhere to our [code of
conduct](../CODE_OF_CONDUCT.md).
# License
Manifold's promptui is released under the [BSD 3-Clause
License](../LICENSE.md).
# Contributor license agreement
For legal purposes all contributors must sign a [contributor license
agreement](https://cla-assistant.io/manifoldco/promptui), either for an
individual or corporation, before a pull request can be accepted.
You will be prompted to sign the agreement by CLA Assistant (bot) when you open
a Pull Request for the first time.
================================================
FILE: .github/listbot.md
================================================
**Author**
- [ ] Changelog has been updated
================================================
FILE: .gitignore
================================================
vendor
all-cover.txt
bin/
================================================
FILE: .golangci.yml
================================================
run:
deadline: 5m
issues:
# Disable maximums so we see all issues
max-per-linter: 0
max-same-issues: 0
# golangci-lint ignores missing docstrings by default. That's no good!
exclude-use-default: false
linters:
disable-all: true
enable:
- misspell
- golint
- goimports
- ineffassign
- deadcode
- gofmt
- govet
- structcheck
- unconvert
- megacheck
- typecheck
- varcheck
================================================
FILE: .travis.yml
================================================
dist: bionic
language: go
go:
- "1.12.x"
- "1.13.x"
branches:
only:
- master
after_success:
# only report coverage for go-version 1.11
- if [[ $TRAVIS_GO_VERSION =~ ^1\.11 ]] ; then bash <(curl -s https://codecov.io/bash) -f all-cover.txt; fi
================================================
FILE: CHANGELOG.md
================================================
# CHANGELOG
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/)
and this project adheres to [Semantic Versioning](http://semver.org/).
## Unreleased
## [0.9.0] - 2021-10-30
### Fixed
- Resolve license incompatibility in tabwriter
## [0.8.0] - 2020-09-28
### Added
- Support ctrl-h for backspace
- Allow hiding entered data after submit
- Allow masking input with an empty rune to hide input length
### Fixed
- Fix echo of cursor after input is finished
- Better support for keycodes on Windows
## [0.7.0] - 2020-01-11
### Added
- Add support for configurable Stdin/Stdout on Prompt
- Add support for setting initial cursor position
- Switch to golangci-lint for linting
### Removed
- Removed support for Go 1.11
### Fixed
- Reduce tool-based deps, hopefully fixing any install issues
## [0.6.0] - 2019-11-29
### Added
- Support configurable stdin
### Fixed
- Correct the dep on go-i18n
## [0.5.0] - 2019-11-29
### Added
- Now building and testing on go 1.11, go 1.12, and go 1.13
### Removed
- Removed support for Go versions that don't include modules.
## [0.4.0] - 2019-02-19
### Added
- The text displayed when an item was successfully selected can be hidden
## [0.3.2] - 2018-11-26
### Added
- Support Go modules
### Fixed
- Fix typos in PromptTemplates documentation
## [0.3.1] - 2018-07-26
### Added
- Improved documentation for GoDoc
- Navigation keys information for Windows
### Fixed
- `success` template was not properly displayed after a successful prompt.
## [0.3.0] - 2018-05-22
### Added
- Background colors codes and template helpers
- `AllowEdit` for prompt to prevent deletion of the default value by any key
- Added `StartInSearchMode` to allow starting the prompt in search mode
### Fixed
- `<Enter>` key press on Windows
- `juju/ansiterm` dependency
- `chzyer/readline#136` new api with ReadCloser
- Deleting UTF-8 characters sequence
## [0.2.1] - 2017-11-30
### Fixed
- `SelectWithAdd` panicking on `.Run` due to lack of keys setup
- Backspace key on Windows
## [0.2.0] - 2017-11-16
### Added
- `Select` items can now be searched
## [0.1.0] - 2017-11-02
### Added
- extract `promptui` from [torus](https://github.com/manifoldco/torus-cli) as a
standalone lib.
- `promptui.Prompt` provides a single input line to capture user information.
- `promptui.Select` provides a list of options to choose from. Users can
navigate through the list either one item at time or by pagination
================================================
FILE: CODE_OF_CONDUCT.md
================================================
# Code of Conduct
## Our Pledge
In the interest of fostering an open and welcoming environment, we as
contributors and maintainers pledge to making participation in our project and
our community a harassment-free experience for everyone, regardless of age,
body size, disability, ethnicity, gender identity and expression, level of
experience, nationality, personal appearance, race, religion, or sexual
identity and orientation.
## Our Standards
Examples of behaviour that contributes to creating a positive environment
include:
- Using welcoming and inclusive language
- Being respectful of differing viewpoints and experiences
- Gracefully accepting constructive criticism
- Focusing on what is best for the community
- Showing empathy towards other community members
Examples of unacceptable behaviour by participants include:
- The use of sexualized language or imagery and unwelcome sexual attention or
advances
- Trolling, insulting/derogatory comments, and personal or political attacks
- Public or private harassment
- Publishing others' private information, such as a physical or electronic
address, without explicit permission
- Other conduct which could reasonably be considered inappropriate in a
professional setting
## Our Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable
behaviour and are expected to take appropriate and fair corrective action in
response to any instances of unacceptable behaviour.
Project maintainers have the right and responsibility to remove, edit, or
reject comments, commits, code, wiki edits, issues, and other contributions
that are not aligned to this Code of Conduct, or to ban temporarily or
permanently any contributor for other behaviours that they deem inappropriate,
threatening, offensive, or harmful.
## Scope
This Code of Conduct applies both within project spaces and in public spaces
when an individual is representing the project or its community. Examples of
representing a project or community include using an official project e-mail
address, posting via an official social media account, or acting as an
appointed representative at an online or offline event. Representation of a
project may be further defined and clarified by project maintainers.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported by contacting the project team at
[hello@manifold.co](mailto:hello@manifold.co). All complaints will be reviewed
and investigated and will result in a response that is deemed necessary and
appropriate to the circumstances. The project team is obligated to maintain
confidentiality with regard to the reporter of an incident. Further details of
specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good
faith may face temporary or permanent repercussions as determined by other
members of the project's leadership.
## Attribution
This Code of Conduct is adapted from the Contributor Covenant, version 1.4,
available at
[http://contributor-covenant.org/version/1/4](http://contributor-covenant.org/version/1/4).
================================================
FILE: LICENSE.md
================================================
BSD 3-Clause License
Copyright (c) 2017, Arigato Machine Inc.
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
* Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
================================================
FILE: Makefile
================================================
export GO111MODULE := on
export PATH := ./bin:$(PATH)
ci: bootstrap lint cover
.PHONY: ci
#################################################
# Bootstrapping for base golang package and tool deps
#################################################
bootstrap:
curl -sfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh| sh -s v1.21.0
.PHONY: bootstrap
mod-update:
go get -u -m
go mod tidy
mod-tidy:
go mod tidy
.PHONY: $(CMD_PKGS)
.PHONY: mod-update mod-tidy
#################################################
# Test and linting
#################################################
# Run all the linters
lint:
bin/golangci-lint run ./...
.PHONY: lint
test:
CGO_ENABLED=0 go test $$(go list ./... | grep -v generated)
.PHONY: test
COVER_TEST_PKGS:=$(shell find . -type f -name '*_test.go' | rev | cut -d "/" -f 2- | rev | grep -v generated | sort -u)
$(COVER_TEST_PKGS:=-cover): %-cover: all-cover.txt
@CGO_ENABLED=0 go test -v -coverprofile=$@.out -covermode=atomic ./$*
@if [ -f $@.out ]; then \
grep -v "mode: atomic" < $@.out >> all-cover.txt; \
rm $@.out; \
fi
all-cover.txt:
echo "mode: atomic" > all-cover.txt
cover: all-cover.txt $(COVER_TEST_PKGS:=-cover)
.PHONY: cover all-cover.txt
================================================
FILE: README.md
================================================
# promptui
Interactive prompt for command-line applications.
We built Promptui because we wanted to make it easy and fun to explore cloud
services with [manifold cli](https://github.com/manifoldco/manifold-cli).
[Code of Conduct](./CODE_OF_CONDUCT.md) |
[Contribution Guidelines](./.github/CONTRIBUTING.md)
[](https://github.com/manifoldco/promptui/releases)
[](https://godoc.org/github.com/manifoldco/promptui)
[](https://travis-ci.org/manifoldco/promptui)
[](https://goreportcard.com/report/github.com/manifoldco/promptui)
[](./LICENSE.md)
## Overview

Promptui is a library providing a simple interface to create command-line
prompts for go. It can be easily integrated into
[spf13/cobra](https://github.com/spf13/cobra),
[urfave/cli](https://github.com/urfave/cli) or any cli go application.
Promptui has two main input modes:
- `Prompt` provides a single line for user input. Prompt supports
optional live validation, confirmation and masking the input.
- `Select` provides a list of options to choose from. Select supports
pagination, search, detailed view and custom templates.
For a full list of options check [GoDoc](https://godoc.org/github.com/manifoldco/promptui).
## Basic Usage
### Prompt
```go
package main
import (
"errors"
"fmt"
"strconv"
"github.com/manifoldco/promptui"
)
func main() {
validate := func(input string) error {
_, err := strconv.ParseFloat(input, 64)
if err != nil {
return errors.New("Invalid number")
}
return nil
}
prompt := promptui.Prompt{
Label: "Number",
Validate: validate,
}
result, err := prompt.Run()
if err != nil {
fmt.Printf("Prompt failed %v\n", err)
return
}
fmt.Printf("You choose %q\n", result)
}
```
### Select
```go
package main
import (
"fmt"
"github.com/manifoldco/promptui"
)
func main() {
prompt := promptui.Select{
Label: "Select Day",
Items: []string{"Monday", "Tuesday", "Wednesday", "Thursday", "Friday",
"Saturday", "Sunday"},
}
_, result, err := prompt.Run()
if err != nil {
fmt.Printf("Prompt failed %v\n", err)
return
}
fmt.Printf("You choose %q\n", result)
}
```
### More Examples
See full list of [examples](https://github.com/manifoldco/promptui/tree/master/_examples)
================================================
FILE: _examples/confirm/main.go
================================================
package main
import (
"fmt"
"github.com/manifoldco/promptui"
)
func main() {
prompt := promptui.Prompt{
Label: "Delete Resource",
IsConfirm: true,
}
result, err := prompt.Run()
if err != nil {
fmt.Printf("Prompt failed %v\n", err)
return
}
fmt.Printf("You choose %q\n", result)
}
================================================
FILE: _examples/custom_prompt/main.go
================================================
package main
import (
"fmt"
"strconv"
"github.com/manifoldco/promptui"
)
type pepper struct {
Name string
HeatUnit int
Peppers int
}
func main() {
validate := func(input string) error {
_, err := strconv.ParseFloat(input, 64)
return err
}
templates := &promptui.PromptTemplates{
Prompt: "{{ . }} ",
Valid: "{{ . | green }} ",
Invalid: "{{ . | red }} ",
Success: "{{ . | bold }} ",
}
prompt := promptui.Prompt{
Label: "Spicy Level",
Templates: templates,
Validate: validate,
}
result, err := prompt.Run()
if err != nil {
fmt.Printf("Prompt failed %v\n", err)
return
}
fmt.Printf("You answered %s\n", result)
}
================================================
FILE: _examples/custom_select/main.go
================================================
package main
import (
"fmt"
"strings"
"github.com/manifoldco/promptui"
)
type pepper struct {
Name string
HeatUnit int
Peppers int
}
func main() {
peppers := []pepper{
{Name: "Bell Pepper", HeatUnit: 0, Peppers: 0},
{Name: "Banana Pepper", HeatUnit: 100, Peppers: 1},
{Name: "Poblano", HeatUnit: 1000, Peppers: 2},
{Name: "Jalapeño", HeatUnit: 3500, Peppers: 3},
{Name: "Aleppo", HeatUnit: 10000, Peppers: 4},
{Name: "Tabasco", HeatUnit: 30000, Peppers: 5},
{Name: "Malagueta", HeatUnit: 50000, Peppers: 6},
{Name: "Habanero", HeatUnit: 100000, Peppers: 7},
{Name: "Red Savina Habanero", HeatUnit: 350000, Peppers: 8},
{Name: "Dragon’s Breath", HeatUnit: 855000, Peppers: 9},
}
templates := &promptui.SelectTemplates{
Label: "{{ . }}?",
Active: "\U0001F336 {{ .Name | cyan }} ({{ .HeatUnit | red }})",
Inactive: " {{ .Name | cyan }} ({{ .HeatUnit | red }})",
Selected: "\U0001F336 {{ .Name | red | cyan }}",
Details: `
--------- Pepper ----------
{{ "Name:" | faint }} {{ .Name }}
{{ "Heat Unit:" | faint }} {{ .HeatUnit }}
{{ "Peppers:" | faint }} {{ .Peppers }}`,
}
searcher := func(input string, index int) bool {
pepper := peppers[index]
name := strings.Replace(strings.ToLower(pepper.Name), " ", "", -1)
input = strings.Replace(strings.ToLower(input), " ", "", -1)
return strings.Contains(name, input)
}
prompt := promptui.Select{
Label: "Spicy Level",
Items: peppers,
Templates: templates,
Size: 4,
Searcher: searcher,
}
i, _, err := prompt.Run()
if err != nil {
fmt.Printf("Prompt failed %v\n", err)
return
}
fmt.Printf("You choose number %d: %s\n", i+1, peppers[i].Name)
}
================================================
FILE: _examples/prompt/main.go
================================================
package main
import (
"errors"
"fmt"
"strconv"
"github.com/manifoldco/promptui"
)
func main() {
validate := func(input string) error {
_, err := strconv.ParseFloat(input, 64)
if err != nil {
return errors.New("Invalid number")
}
return nil
}
prompt := promptui.Prompt{
Label: "Number",
Validate: validate,
}
result, err := prompt.Run()
if err != nil {
fmt.Printf("Prompt failed %v\n", err)
return
}
fmt.Printf("You choose %q\n", result)
}
================================================
FILE: _examples/prompt_default/main.go
================================================
package main
import (
"errors"
"fmt"
"os/user"
"github.com/manifoldco/promptui"
)
func main() {
validate := func(input string) error {
if len(input) < 3 {
return errors.New("Username must have more than 3 characters")
}
return nil
}
var username string
u, err := user.Current()
if err == nil {
username = u.Username
}
prompt := promptui.Prompt{
Label: "Username",
Validate: validate,
Default: username,
}
result, err := prompt.Run()
if err != nil {
fmt.Printf("Prompt failed %v\n", err)
return
}
fmt.Printf("Your username is %q\n", result)
}
================================================
FILE: _examples/prompt_password/main.go
================================================
package main
import (
"errors"
"fmt"
"github.com/manifoldco/promptui"
)
func main() {
validate := func(input string) error {
if len(input) < 6 {
return errors.New("Password must have more than 6 characters")
}
return nil
}
prompt := promptui.Prompt{
Label: "Password",
Validate: validate,
Mask: '*',
}
result, err := prompt.Run()
if err != nil {
fmt.Printf("Prompt failed %v\n", err)
return
}
fmt.Printf("Your password is %q\n", result)
}
================================================
FILE: _examples/select/main.go
================================================
package main
import (
"fmt"
"github.com/manifoldco/promptui"
)
func main() {
prompt := promptui.Select{
Label: "Select Day",
Items: []string{"Monday", "Tuesday", "Wednesday", "Thursday", "Friday",
"Saturday", "Sunday"},
}
_, result, err := prompt.Run()
if err != nil {
fmt.Printf("Prompt failed %v\n", err)
return
}
fmt.Printf("You choose %q\n", result)
}
================================================
FILE: _examples/select_add/main.go
================================================
package main
import (
"fmt"
"github.com/manifoldco/promptui"
)
func main() {
items := []string{"Vim", "Emacs", "Sublime", "VSCode", "Atom"}
index := -1
var result string
var err error
for index < 0 {
prompt := promptui.SelectWithAdd{
Label: "What's your text editor",
Items: items,
AddLabel: "Other",
}
index, result, err = prompt.Run()
if index == -1 {
items = append(items, result)
}
}
if err != nil {
fmt.Printf("Prompt failed %v\n", err)
return
}
fmt.Printf("You choose %s\n", result)
}
================================================
FILE: codes.go
================================================
package promptui
import (
"fmt"
"strconv"
"strings"
"text/template"
)
const esc = "\033["
type attribute int
// The possible state of text inside the application, either Bold, faint, italic or underline.
//
// These constants are called through the use of the Styler function.
const (
reset attribute = iota
FGBold
FGFaint
FGItalic
FGUnderline
)
// The possible colors of text inside the application.
//
// These constants are called through the use of the Styler function.
const (
FGBlack attribute = iota + 30
FGRed
FGGreen
FGYellow
FGBlue
FGMagenta
FGCyan
FGWhite
)
// The possible background colors of text inside the application.
//
// These constants are called through the use of the Styler function.
const (
BGBlack attribute = iota + 40
BGRed
BGGreen
BGYellow
BGBlue
BGMagenta
BGCyan
BGWhite
)
// ResetCode is the character code used to reset the terminal formatting
var ResetCode = fmt.Sprintf("%s%dm", esc, reset)
const (
hideCursor = esc + "?25l"
showCursor = esc + "?25h"
clearLine = esc + "2K"
)
// FuncMap defines template helpers for the output. It can be extended as a regular map.
//
// The functions inside the map link the state, color and background colors strings detected in templates to a Styler
// function that applies the given style using the corresponding constant.
var FuncMap = template.FuncMap{
"black": Styler(FGBlack),
"red": Styler(FGRed),
"green": Styler(FGGreen),
"yellow": Styler(FGYellow),
"blue": Styler(FGBlue),
"magenta": Styler(FGMagenta),
"cyan": Styler(FGCyan),
"white": Styler(FGWhite),
"bgBlack": Styler(BGBlack),
"bgRed": Styler(BGRed),
"bgGreen": Styler(BGGreen),
"bgYellow": Styler(BGYellow),
"bgBlue": Styler(BGBlue),
"bgMagenta": Styler(BGMagenta),
"bgCyan": Styler(BGCyan),
"bgWhite": Styler(BGWhite),
"bold": Styler(FGBold),
"faint": Styler(FGFaint),
"italic": Styler(FGItalic),
"underline": Styler(FGUnderline),
}
func upLine(n uint) string {
return movementCode(n, 'A')
}
func movementCode(n uint, code rune) string {
return esc + strconv.FormatUint(uint64(n), 10) + string(code)
}
// Styler is a function that accepts multiple possible styling transforms from the state,
// color and background colors constants and transforms them into a templated string
// to apply those styles in the CLI.
//
// The returned styling function accepts a string that will be extended with
// the wrapping function's styling attributes.
func Styler(attrs ...attribute) func(interface{}) string {
attrstrs := make([]string, len(attrs))
for i, v := range attrs {
attrstrs[i] = strconv.Itoa(int(v))
}
seq := strings.Join(attrstrs, ";")
return func(v interface{}) string {
end := ""
s, ok := v.(string)
if !ok || !strings.HasSuffix(s, ResetCode) {
end = ResetCode
}
return fmt.Sprintf("%s%sm%v%s", esc, seq, v, end)
}
}
================================================
FILE: codes_test.go
================================================
package promptui
import "testing"
func TestStyler(t *testing.T) {
t.Run("renders a single code", func(t *testing.T) {
red := Styler(FGRed)("hi")
expected := "\033[31mhi\033[0m"
if red != expected {
t.Errorf("style did not match: %s != %s", red, expected)
}
})
t.Run("combines multiple codes", func(t *testing.T) {
boldRed := Styler(FGRed, FGBold)("hi")
expected := "\033[31;1mhi\033[0m"
if boldRed != expected {
t.Errorf("style did not match: %s != %s", boldRed, expected)
}
})
t.Run("should not repeat reset codes for nested styles", func(t *testing.T) {
red := Styler(FGRed)("hi")
boldRed := Styler(FGBold)(red)
expected := "\033[1m\033[31mhi\033[0m"
if boldRed != expected {
t.Errorf("style did not match: %s != %s", boldRed, expected)
}
})
}
================================================
FILE: cursor.go
================================================
package promptui
import (
"fmt"
"strings"
)
// Pointer is A specific type that translates a given set of runes into a given
// set of runes pointed at by the cursor.
type Pointer func(to []rune) []rune
func defaultCursor(ignored []rune) []rune {
return []rune("\u2588")
}
func blockCursor(input []rune) []rune {
return []rune(fmt.Sprintf("\\e[7m%s\\e[0m", string(input)))
}
func pipeCursor(input []rune) []rune {
marker := []rune("|")
out := []rune{}
out = append(out, marker...)
out = append(out, input...)
return out
}
var (
// DefaultCursor is a big square block character. Obscures whatever was
// input.
DefaultCursor Pointer = defaultCursor
// BlockCursor is a cursor which highlights a character by inverting colors
// on it.
BlockCursor Pointer = blockCursor
// PipeCursor is a pipe character "|" which appears before the input
// character.
PipeCursor Pointer = pipeCursor
)
// Cursor tracks the state associated with the movable cursor
// The strategy is to keep the prompt, input pristine except for requested
// modifications. The insertion of the cursor happens during a `format` call
// and we read in new input via an `Update` call
type Cursor struct {
// shows where the user inserts/updates text
Cursor Pointer
// what the user entered, and what we will echo back to them, after
// insertion of the cursor and prefixing with the prompt
input []rune
// Put the cursor before this slice
Position int
erase bool
}
// NewCursor create a new cursor, with the DefaultCursor, the specified input,
// and position at the end of the specified starting input.
func NewCursor(startinginput string, pointer Pointer, eraseDefault bool) Cursor {
if pointer == nil {
pointer = defaultCursor
}
cur := Cursor{Cursor: pointer, Position: len(startinginput), input: []rune(startinginput), erase: eraseDefault}
if eraseDefault {
cur.Start()
} else {
cur.End()
}
return cur
}
func (c *Cursor) String() string {
return fmt.Sprintf(
"Cursor: %s, input %s, Position %d",
string(c.Cursor([]rune(""))), string(c.input), c.Position)
}
// End is a convenience for c.Place(len(c.input)) so you don't have to know how I
// indexed.
func (c *Cursor) End() {
c.Place(len(c.input))
}
// Start is convenience for c.Place(0) so you don't have to know how I
// indexed.
func (c *Cursor) Start() {
c.Place(0)
}
// ensures we are in bounds.
func (c *Cursor) correctPosition() {
if c.Position > len(c.input) {
c.Position = len(c.input)
}
if c.Position < 0 {
c.Position = 0
}
}
// insert the cursor rune array into r before the provided index
func format(a []rune, c *Cursor) string {
i := c.Position
var b []rune
out := make([]rune, 0)
if i < len(a) {
b = c.Cursor(a[i : i+1])
out = append(out, a[:i]...) // does not include i
out = append(out, b...) // add the cursor
out = append(out, a[i+1:]...) // add the rest after i
} else {
b = c.Cursor([]rune{})
out = append(out, a...)
out = append(out, b...)
}
return string(out)
}
// Format renders the input with the Cursor appropriately positioned.
func (c *Cursor) Format() string {
r := c.input
// insert the cursor
return format(r, c)
}
// FormatMask replaces all input runes with the mask rune.
func (c *Cursor) FormatMask(mask rune) string {
if mask == ' ' {
return format([]rune{}, c)
}
r := make([]rune, len(c.input))
for i := range r {
r[i] = mask
}
return format(r, c)
}
// Update inserts newinput into the input []rune in the appropriate place.
// The cursor is moved to the end of the inputed sequence.
func (c *Cursor) Update(newinput string) {
a := c.input
b := []rune(newinput)
i := c.Position
a = append(a[:i], append(b, a[i:]...)...)
c.input = a
c.Move(len(b))
}
// Get returns a copy of the input
func (c *Cursor) Get() string {
return string(c.input)
}
// GetMask returns a mask string with length equal to the input
func (c *Cursor) GetMask(mask rune) string {
return strings.Repeat(string(mask), len(c.input))
}
// Replace replaces the previous input with whatever is specified, and moves the
// cursor to the end position
func (c *Cursor) Replace(input string) {
c.input = []rune(input)
c.End()
}
// Place moves the cursor to the absolute array index specified by position
func (c *Cursor) Place(position int) {
c.Position = position
c.correctPosition()
}
// Move moves the cursor over in relative terms, by shift indices.
func (c *Cursor) Move(shift int) {
// delete the current cursor
c.Position = c.Position + shift
c.correctPosition()
}
// Backspace removes the rune that precedes the cursor
//
// It handles being at the beginning or end of the row, and moves the cursor to
// the appropriate position.
func (c *Cursor) Backspace() {
a := c.input
i := c.Position
if i == 0 {
// Shrug
return
}
if i == len(a) {
c.input = a[:i-1]
} else {
c.input = append(a[:i-1], a[i:]...)
}
// now it's pointing to the i+1th element
c.Move(-1)
}
// Listen is a readline Listener that updates internal cursor state appropriately.
func (c *Cursor) Listen(line []rune, pos int, key rune) ([]rune, int, bool) {
if line != nil {
// no matter what, update our internal representation.
c.Update(string(line))
}
switch key {
case 0: // empty
case KeyEnter:
return []rune(c.Get()), c.Position, false
case KeyBackspace, KeyCtrlH:
if c.erase {
c.erase = false
c.Replace("")
}
c.Backspace()
case KeyForward:
// the user wants to edit the default, despite how we set it up. Let
// them.
c.erase = false
c.Move(1)
case KeyBackward:
c.Move(-1)
default:
if c.erase {
c.erase = false
c.Replace("")
c.Update(string(key))
}
}
return []rune(c.Get()), c.Position, true
}
================================================
FILE: cursor_test.go
================================================
package promptui
import "testing"
func TestDefinedCursors(t *testing.T) {
t.Run("pipeCursor", func(t *testing.T) {
p := string(pipeCursor([]rune{}))
if p != "|" {
t.Fatalf("%x!=%x", "|", p)
}
})
}
func TestCursor(t *testing.T) {
t.Run("empty", func(t *testing.T) {
cursor := Cursor{Cursor: pipeCursor}
cursor.End()
f := cursor.Format()
if f != "|" {
t.Errorf("% x!=% x", "|", cursor.Format())
}
cursor.Update("sup")
if cursor.Format() != "sup|" {
t.Errorf("% x!=% x", "sup|", cursor.Format())
}
})
t.Run("Cursor at end, append additional", func(t *testing.T) {
cursor := Cursor{input: []rune("a"), Cursor: pipeCursor}
cursor.End()
f := cursor.Format()
if f != "a|" {
t.Errorf("% x!=% x", "a|", cursor.Format())
}
cursor.Update(" hi")
if cursor.Format() != "a hi|" {
t.Errorf("% x!=% x", "a hi!", cursor.Format())
}
})
t.Run("Cursor at at end, backspace", func(t *testing.T) {
cursor := Cursor{input: []rune("default"), Cursor: pipeCursor}
cursor.Place(len(cursor.input))
cursor.Backspace()
if cursor.Format() != "defaul|" {
t.Errorf("expected defaul|; found %s", cursor.Format())
}
cursor.Update(" hi")
if cursor.Format() != "defaul hi|" {
t.Errorf("expected 'defaul hi|'; found '%s'", cursor.Format())
}
})
t.Run("Cursor at beginning, append additional", func(t *testing.T) {
cursor := Cursor{input: []rune("default"), Cursor: pipeCursor}
t.Log("init", cursor.String())
cursor.Backspace()
if cursor.Format() != "|default" {
t.Errorf("expected |default; found %s", cursor.Format())
}
cursor.Update("hi ")
t.Log("after add", cursor.String())
if cursor.Format() != "hi |default" {
t.Errorf("expected 'hi |default'; found '%s'", cursor.Format())
}
cursor.Backspace()
t.Log("after backspace", cursor.String())
if cursor.Format() != "hi|default" {
t.Errorf("expected 'hi|default'; found '%s'", cursor.Format())
}
cursor.Backspace()
t.Log("after backspace", cursor.String())
if cursor.Format() != "h|default" {
t.Errorf("expected 'h|default'; found '%s'", cursor.Format())
}
})
t.Run("Move", func(t *testing.T) {
cursor := Cursor{input: []rune("default"), Cursor: pipeCursor}
if cursor.Format() != "|default" {
t.Errorf("expected |default; found %s", cursor.Format())
}
cursor.Move(-1)
if cursor.Format() != "|default" {
t.Errorf("moved backwards from beginning |default; found %s", cursor.Format())
}
cursor.Move(1)
if cursor.Format() != "d|efault" {
t.Errorf("expected 'd|efault'; found '%s'", cursor.Format())
}
cursor.Move(10)
if cursor.Format() != "default|" {
t.Errorf("expected 'default|'; found '%s'", cursor.Format())
}
})
}
================================================
FILE: example_main_test.go
================================================
package promptui
import (
"errors"
"fmt"
"strconv"
)
// This is an example for the Prompt mode of promptui. In this example, a prompt is created
// with a validator function that validates the given value to make sure its a number.
// If successful, it will output the chosen number in a formatted message.
func Example_prompt() {
validate := func(input string) error {
_, err := strconv.ParseFloat(input, 64)
if err != nil {
return errors.New("Invalid number")
}
return nil
}
prompt := Prompt{
Label: "Number",
Validate: validate,
}
result, err := prompt.Run()
if err != nil {
fmt.Printf("Prompt failed %v\n", err)
return
}
fmt.Printf("You choose %q\n", result)
}
// This is an example for the Select mode of promptui. In this example, a select is created with
// the days of the week as its items. When an item is selected, the selected day will be displayed
// in a formatted message.
func Example_select() {
prompt := Select{
Label: "Select Day",
Items: []string{"Monday", "Tuesday", "Wednesday", "Thursday", "Friday",
"Saturday", "Sunday"},
}
_, result, err := prompt.Run()
if err != nil {
fmt.Printf("Prompt failed %v\n", err)
return
}
fmt.Printf("You choose %q\n", result)
}
================================================
FILE: example_prompt_test.go
================================================
package promptui
import (
"fmt"
"strconv"
)
// This example shows how to use the prompt validator and templates to create a stylized prompt.
// The validator will make sure the value entered is a parseable float while the templates will
// color the value to show validity.
func ExamplePrompt() {
// The validate function follows the required validator signature.
validate := func(input string) error {
_, err := strconv.ParseFloat(input, 64)
return err
}
// Each template displays the data received from the prompt with some formatting.
templates := &PromptTemplates{
Prompt: "{{ . }} ",
Valid: "{{ . | green }} ",
Invalid: "{{ . | red }} ",
Success: "{{ . | bold }} ",
}
prompt := Prompt{
Label: "Spicy Level",
Templates: templates,
Validate: validate,
}
result, err := prompt.Run()
if err != nil {
fmt.Printf("Prompt failed %v\n", err)
return
}
// The result of the prompt, if valid, is displayed in a formatted message.
fmt.Printf("You answered %s\n", result)
}
================================================
FILE: example_select_test.go
================================================
package promptui
import (
"fmt"
"strings"
)
// Any type can be given to the select's item as long as the templates properly implement the dot notation
// to display it.
type pepper struct {
Name string
HeatUnit int
Peppers int
}
// This examples shows a complex and customized select.
func ExampleSelect() {
// The select will show a series of peppers stored inside a slice of structs. To display the content of the struct,
// the usual dot notation is used inside the templates to select the fields and color them.
peppers := []pepper{
{Name: "Bell Pepper", HeatUnit: 0, Peppers: 0},
{Name: "Banana Pepper", HeatUnit: 100, Peppers: 1},
{Name: "Poblano", HeatUnit: 1000, Peppers: 2},
{Name: "Jalapeño", HeatUnit: 3500, Peppers: 3},
{Name: "Aleppo", HeatUnit: 10000, Peppers: 4},
{Name: "Tabasco", HeatUnit: 30000, Peppers: 5},
{Name: "Malagueta", HeatUnit: 50000, Peppers: 6},
{Name: "Habanero", HeatUnit: 100000, Peppers: 7},
{Name: "Red Savina Habanero", HeatUnit: 350000, Peppers: 8},
{Name: "Dragon’s Breath", HeatUnit: 855000, Peppers: 9},
}
// The Active and Selected templates set a small pepper icon next to the name colored and the heat unit for the
// active template. The details template is show at the bottom of the select's list and displays the full info
// for that pepper in a multi-line template.
templates := &SelectTemplates{
Label: "{{ . }}?",
Active: "\U0001F336 {{ .Name | cyan }} ({{ .HeatUnit | red }})",
Inactive: " {{ .Name | cyan }} ({{ .HeatUnit | red }})",
Selected: "\U0001F336 {{ .Name | red | cyan }}",
Details: `
--------- Pepper ----------
{{ "Name:" | faint }} {{ .Name }}
{{ "Heat Unit:" | faint }} {{ .HeatUnit }}
{{ "Peppers:" | faint }} {{ .Peppers }}`,
}
// A searcher function is implemented which enabled the search mode for the select. The function follows
// the required searcher signature and finds any pepper whose name contains the searched string.
searcher := func(input string, index int) bool {
pepper := peppers[index]
name := strings.Replace(strings.ToLower(pepper.Name), " ", "", -1)
input = strings.Replace(strings.ToLower(input), " ", "", -1)
return strings.Contains(name, input)
}
prompt := Select{
Label: "Spicy Level",
Items: peppers,
Templates: templates,
Size: 4,
Searcher: searcher,
}
i, _, err := prompt.Run()
if err != nil {
fmt.Printf("Prompt failed %v\n", err)
return
}
// The selected pepper will be displayed with its name and index in a formatted message.
fmt.Printf("You choose number %d: %s\n", i+1, peppers[i].Name)
}
================================================
FILE: example_selectwithadd_test.go
================================================
package promptui
import "fmt"
// This example shows how to create a SelectWithAdd that will add each new item it is given to the
// list of items until one is chosen.
func ExampleSelectWithAdd() {
items := []string{"Vim", "Emacs", "Sublime", "VSCode", "Atom"}
index := -1
var result string
var err error
for index < 0 {
prompt := SelectWithAdd{
Label: "What's your text editor",
Items: items,
AddLabel: "Add your own",
}
index, result, err = prompt.Run()
if index == -1 {
items = append(items, result)
}
}
if err != nil {
fmt.Printf("Prompt failed %v\n", err)
return
}
fmt.Printf("You choose %s\n", result)
}
================================================
FILE: go.mod
================================================
module github.com/manifoldco/promptui
go 1.12
require (
github.com/chzyer/logex v1.1.10 // indirect
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 // indirect
golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b // indirect
)
================================================
FILE: go.sum
================================================
github.com/chzyer/logex v1.1.10 h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e h1:fY5BOSpyZCqRo5OhCuC+XN+r/bBCmeuuJtjz+bCNIf8=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 h1:q763qf9huN11kDQavWsoZXJNW3xEE4JJyHa5Q25/sd8=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b h1:MQE+LT/ABUuuvEZ+YQAMSXindAdUh7slEmAkup74op4=
golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
================================================
FILE: keycodes.go
================================================
package promptui
import "github.com/chzyer/readline"
// These runes are used to identify the commands entered by the user in the command prompt. They map
// to specific actions of promptui in prompt mode and can be remapped if necessary.
var (
// KeyEnter is the default key for submission/selection.
KeyEnter rune = readline.CharEnter
// KeyCtrlH is the key for deleting input text.
KeyCtrlH rune = readline.CharCtrlH
// KeyPrev is the default key to go up during selection.
KeyPrev rune = readline.CharPrev
KeyPrevDisplay = "↑"
// KeyNext is the default key to go down during selection.
KeyNext rune = readline.CharNext
KeyNextDisplay = "↓"
// KeyBackward is the default key to page up during selection.
KeyBackward rune = readline.CharBackward
KeyBackwardDisplay = "←"
// KeyForward is the default key to page down during selection.
KeyForward rune = readline.CharForward
KeyForwardDisplay = "→"
)
================================================
FILE: keycodes_other.go
================================================
// +build !windows
package promptui
import "github.com/chzyer/readline"
var (
// KeyBackspace is the default key for deleting input text.
KeyBackspace rune = readline.CharBackspace
)
================================================
FILE: keycodes_windows.go
================================================
// +build windows
package promptui
// source: https://msdn.microsoft.com/en-us/library/aa243025(v=vs.60).aspx
var (
// KeyBackspace is the default key for deleting input text inside a command line prompt.
KeyBackspace rune = 8
)
================================================
FILE: list/list.go
================================================
package list
import (
"fmt"
"reflect"
"strings"
)
// Searcher is a base function signature that is used inside select when activating the search mode.
// If defined, it is called on each items of the select and should return a boolean for whether or not
// the item fits the searched term.
type Searcher func(input string, index int) bool
// NotFound is an index returned when no item was selected. This could
// happen due to a search without results.
const NotFound = -1
// List holds a collection of items that can be displayed with an N number of
// visible items. The list can be moved up, down by one item of time or an
// entire page (ie: visible size). It keeps track of the current selected item.
type List struct {
items []*interface{}
scope []*interface{}
cursor int // cursor holds the index of the current selected item
size int // size is the number of visible options
start int
Searcher Searcher
}
// New creates and initializes a list of searchable items. The items attribute must be a slice type with a
// size greater than 0. Error will be returned if those two conditions are not met.
func New(items interface{}, size int) (*List, error) {
if size < 1 {
return nil, fmt.Errorf("list size %d must be greater than 0", size)
}
if items == nil || reflect.TypeOf(items).Kind() != reflect.Slice {
return nil, fmt.Errorf("items %v is not a slice", items)
}
slice := reflect.ValueOf(items)
values := make([]*interface{}, slice.Len())
for i := range values {
item := slice.Index(i).Interface()
values[i] = &item
}
return &List{size: size, items: values, scope: values}, nil
}
// Prev moves the visible list back one item. If the selected item is out of
// view, the new select item becomes the last visible item. If the list is
// already at the top, nothing happens.
func (l *List) Prev() {
if l.cursor > 0 {
l.cursor--
}
if l.start > l.cursor {
l.start = l.cursor
}
}
// Search allows the list to be filtered by a given term. The list must
// implement the searcher function signature for this functionality to work.
func (l *List) Search(term string) {
term = strings.Trim(term, " ")
l.cursor = 0
l.start = 0
l.search(term)
}
// CancelSearch stops the current search and returns the list to its
// original order.
func (l *List) CancelSearch() {
l.cursor = 0
l.start = 0
l.scope = l.items
}
func (l *List) search(term string) {
var scope []*interface{}
for i, item := range l.items {
if l.Searcher(term, i) {
scope = append(scope, item)
}
}
l.scope = scope
}
// Start returns the current render start position of the list.
func (l *List) Start() int {
return l.start
}
// SetStart sets the current scroll position. Values out of bounds will be
// clamped.
func (l *List) SetStart(i int) {
if i < 0 {
i = 0
}
if i > l.cursor {
l.start = l.cursor
} else {
l.start = i
}
}
// SetCursor sets the position of the cursor in the list. Values out of bounds
// will be clamped.
func (l *List) SetCursor(i int) {
max := len(l.scope) - 1
if i >= max {
i = max
}
if i < 0 {
i = 0
}
l.cursor = i
if l.start > l.cursor {
l.start = l.cursor
} else if l.start+l.size <= l.cursor {
l.start = l.cursor - l.size + 1
}
}
// Next moves the visible list forward one item. If the selected item is out of
// view, the new select item becomes the first visible item. If the list is
// already at the bottom, nothing happens.
func (l *List) Next() {
max := len(l.scope) - 1
if l.cursor < max {
l.cursor++
}
if l.start+l.size <= l.cursor {
l.start = l.cursor - l.size + 1
}
}
// PageUp moves the visible list backward by x items. Where x is the size of the
// visible items on the list. The selected item becomes the first visible item.
// If the list is already at the bottom, the selected item becomes the last
// visible item.
func (l *List) PageUp() {
start := l.start - l.size
if start < 0 {
l.start = 0
} else {
l.start = start
}
cursor := l.start
if cursor < l.cursor {
l.cursor = cursor
}
}
// PageDown moves the visible list forward by x items. Where x is the size of
// the visible items on the list. The selected item becomes the first visible
// item.
func (l *List) PageDown() {
start := l.start + l.size
max := len(l.scope) - l.size
switch {
case len(l.scope) < l.size:
l.start = 0
case start > max:
l.start = max
default:
l.start = start
}
cursor := l.start
if cursor == l.cursor {
l.cursor = len(l.scope) - 1
} else if cursor > l.cursor {
l.cursor = cursor
}
}
// CanPageDown returns whether a list can still PageDown().
func (l *List) CanPageDown() bool {
max := len(l.scope)
return l.start+l.size < max
}
// CanPageUp returns whether a list can still PageUp().
func (l *List) CanPageUp() bool {
return l.start > 0
}
// Index returns the index of the item currently selected inside the searched list. If no item is selected,
// the NotFound (-1) index is returned.
func (l *List) Index() int {
selected := l.scope[l.cursor]
for i, item := range l.items {
if item == selected {
return i
}
}
return NotFound
}
// Items returns a slice equal to the size of the list with the current visible
// items and the index of the active item in this list.
func (l *List) Items() ([]interface{}, int) {
var result []interface{}
max := len(l.scope)
end := l.start + l.size
if end > max {
end = max
}
active := NotFound
for i, j := l.start, 0; i < end; i, j = i+1, j+1 {
if l.cursor == i {
active = j
}
result = append(result, *l.scope[i])
}
return result, active
}
================================================
FILE: list/list_test.go
================================================
package list
import (
"fmt"
"reflect"
"testing"
)
func TestListNew(t *testing.T) {
t.Run("when items a slice nil", func(t *testing.T) {
_, err := New([]int{1, 2, 3}, 3)
if err != nil {
t.Errorf("Expected no errors, error %v", err)
}
})
t.Run("when items is nil", func(t *testing.T) {
_, err := New(nil, 3)
if err == nil {
t.Errorf("Expected error got none")
}
})
t.Run("when items is not a slice", func(t *testing.T) {
_, err := New("1,2,3", 3)
if err == nil {
t.Errorf("Expected error got none")
}
})
}
func TestListMovement(t *testing.T) {
letters := []rune{'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j'}
l, err := New(letters, 4)
if err != nil {
t.Fatalf("Expected no error, got %v", err)
}
tcs := []struct {
expect []rune
move string
selected rune
}{
{move: "next", selected: 'b', expect: []rune{'a', 'b', 'c', 'd'}},
{move: "prev", selected: 'a', expect: []rune{'a', 'b', 'c', 'd'}},
{move: "prev", selected: 'a', expect: []rune{'a', 'b', 'c', 'd'}},
{move: "next", selected: 'b', expect: []rune{'a', 'b', 'c', 'd'}},
{move: "next", selected: 'c', expect: []rune{'a', 'b', 'c', 'd'}},
{move: "next", selected: 'd', expect: []rune{'a', 'b', 'c', 'd'}},
{move: "next", selected: 'e', expect: []rune{'b', 'c', 'd', 'e'}},
{move: "prev", selected: 'd', expect: []rune{'b', 'c', 'd', 'e'}},
{move: "up", selected: 'a', expect: []rune{'a', 'b', 'c', 'd'}},
{move: "up", selected: 'a', expect: []rune{'a', 'b', 'c', 'd'}},
{move: "down", selected: 'e', expect: []rune{'e', 'f', 'g', 'h'}},
{move: "down", selected: 'g', expect: []rune{'g', 'h', 'i', 'j'}},
{move: "down", selected: 'j', expect: []rune{'g', 'h', 'i', 'j'}},
}
for _, tc := range tcs {
t.Run(fmt.Sprintf("list %s", tc.move), func(t *testing.T) {
switch tc.move {
case "next":
l.Next()
case "prev":
l.Prev()
case "up":
l.PageUp()
case "down":
l.PageDown()
default:
t.Fatalf("unknown move %q", tc.move)
}
list, idx := l.Items()
got := castList(list)
if !reflect.DeepEqual(tc.expect, got) {
t.Errorf("expected %q, got %q", tc.expect, got)
}
selected := list[idx]
if tc.selected != selected {
t.Errorf("expected selected to be %q, got %q", tc.selected, selected)
}
})
}
}
func TestListPageDown(t *testing.T) {
t.Run("when list has fewer items than page size", func(t *testing.T) {
letters := []rune{'a', 'b'}
l, err := New(letters, 4)
if err != nil {
t.Fatalf("Expected no error, got %v", err)
}
l.PageDown()
list, idx := l.Items()
expected := 'b'
selected := list[idx]
if selected != expected {
t.Errorf("expected selected to be %q, got %q", expected, selected)
}
})
}
func TestListComparion(t *testing.T) {
t.Run("when item supports comparison", func(t *testing.T) {
type comparable struct {
Number int
}
structs := []comparable{
{Number: 1},
{Number: 2},
}
l, err := New(structs, 4)
if err != nil {
t.Fatalf("Expected no error, got %v", err)
}
idx := l.Index()
if idx != 0 {
t.Errorf("expected index to be first, got %d", idx)
}
})
t.Run("when item doesn't support comparison", func(t *testing.T) {
type uncomparable struct {
Numbers []int
}
structs := []uncomparable{
{Numbers: []int{1}},
{Numbers: []int{2}},
}
l, err := New(structs, 4)
if err != nil {
t.Fatalf("Expected no error, got %v", err)
}
idx := l.Index()
if idx != 0 {
t.Errorf("expected index to be first, got %d", idx)
}
})
}
func castList(list []interface{}) []rune {
result := make([]rune, len(list))
for i, l := range list {
result[i] = l.(rune)
}
return result
}
================================================
FILE: prompt.go
================================================
package promptui
import (
"fmt"
"io"
"strings"
"text/template"
"github.com/chzyer/readline"
"github.com/manifoldco/promptui/screenbuf"
)
// Prompt represents a single line text field input with options for validation and input masks.
type Prompt struct {
// Label is the value displayed on the command line prompt.
//
// The value for Label can be a simple string or a struct that will need to be accessed by dot notation
// inside the templates. For example, `{{ .Name }}` will display the name property of a struct.
Label interface{}
// Default is the initial value for the prompt. This value will be displayed next to the prompt's label
// and the user will be able to view or change it depending on the options.
Default string
// AllowEdit lets the user edit the default value. If false, any key press
// other than <Enter> automatically clears the default value.
AllowEdit bool
// Validate is an optional function that fill be used against the entered value in the prompt to validate it.
Validate ValidateFunc
// Mask is an optional rune that sets which character to display instead of the entered characters. This
// allows hiding private information like passwords.
Mask rune
// HideEntered sets whether to hide the text after the user has pressed enter.
HideEntered bool
// Templates can be used to customize the prompt output. If nil is passed, the
// default templates are used. See the PromptTemplates docs for more info.
Templates *PromptTemplates
// IsConfirm makes the prompt ask for a yes or no ([Y/N]) question rather than request an input. When set,
// most properties related to input will be ignored.
IsConfirm bool
// IsVimMode enables vi-like movements (hjkl) and editing.
IsVimMode bool
// the Pointer defines how to render the cursor.
Pointer Pointer
Stdin io.ReadCloser
Stdout io.WriteCloser
}
// PromptTemplates allow a prompt to be customized following stdlib
// text/template syntax. Custom state, colors and background color are available for use inside
// the templates and are documented inside the Variable section of the docs.
//
// Examples
//
// text/templates use a special notation to display programmable content. Using the double bracket notation,
// the value can be printed with specific helper functions. For example
//
// This displays the value given to the template as pure, unstylized text.
// '{{ . }}'
//
// This displays the value colored in cyan
// '{{ . | cyan }}'
//
// This displays the value colored in red with a cyan background-color
// '{{ . | red | cyan }}'
//
// See the doc of text/template for more info: https://golang.org/pkg/text/template/
type PromptTemplates struct {
// Prompt is a text/template for the prompt label displayed on the left side of the prompt.
Prompt string
// Prompt is a text/template for the prompt label when IsConfirm is set as true.
Confirm string
// Valid is a text/template for the prompt label when the value entered is valid.
Valid string
// Invalid is a text/template for the prompt label when the value entered is invalid.
Invalid string
// Success is a text/template for the prompt label when the user has pressed entered and the value has been
// deemed valid by the validation function. The label will keep using this template even when the prompt ends
// inside the console.
Success string
// Prompt is a text/template for the prompt label when the value is invalid due to an error triggered by
// the prompt's validation function.
ValidationError string
// FuncMap is a map of helper functions that can be used inside of templates according to the text/template
// documentation.
//
// By default, FuncMap contains the color functions used to color the text in templates. If FuncMap
// is overridden, the colors functions must be added in the override from promptui.FuncMap to work.
FuncMap template.FuncMap
prompt *template.Template
valid *template.Template
invalid *template.Template
validation *template.Template
success *template.Template
}
// Run executes the prompt. Its displays the label and default value if any, asking the user to enter a value.
// Run will keep the prompt alive until it has been canceled from the command prompt or it has received a valid
// value. It will return the value and an error if any occurred during the prompt's execution.
func (p *Prompt) Run() (string, error) {
var err error
err = p.prepareTemplates()
if err != nil {
return "", err
}
c := &readline.Config{
Stdin: p.Stdin,
Stdout: p.Stdout,
EnableMask: p.Mask != 0,
MaskRune: p.Mask,
HistoryLimit: -1,
VimMode: p.IsVimMode,
UniqueEditLine: true,
}
err = c.Init()
if err != nil {
return "", err
}
rl, err := readline.NewEx(c)
if err != nil {
return "", err
}
// we're taking over the cursor, so stop showing it.
rl.Write([]byte(hideCursor))
sb := screenbuf.New(rl)
validFn := func(x string) error {
return nil
}
if p.Validate != nil {
validFn = p.Validate
}
var inputErr error
input := p.Default
if p.IsConfirm {
input = ""
}
eraseDefault := input != "" && !p.AllowEdit
cur := NewCursor(input, p.Pointer, eraseDefault)
listen := func(input []rune, pos int, key rune) ([]rune, int, bool) {
_, _, keepOn := cur.Listen(input, pos, key)
err := validFn(cur.Get())
var prompt []byte
if err != nil {
prompt = render(p.Templates.invalid, p.Label)
} else {
prompt = render(p.Templates.valid, p.Label)
if p.IsConfirm {
prompt = render(p.Templates.prompt, p.Label)
}
}
echo := cur.Format()
if p.Mask != 0 {
echo = cur.FormatMask(p.Mask)
}
prompt = append(prompt, []byte(echo)...)
sb.Reset()
sb.Write(prompt)
if inputErr != nil {
validation := render(p.Templates.validation, inputErr)
sb.Write(validation)
inputErr = nil
}
sb.Flush()
return nil, 0, keepOn
}
c.SetListener(listen)
for {
_, err = rl.Readline()
inputErr = validFn(cur.Get())
if inputErr == nil {
break
}
if err != nil {
break
}
}
if err != nil {
switch err {
case readline.ErrInterrupt:
err = ErrInterrupt
case io.EOF:
err = ErrEOF
}
if err.Error() == "Interrupt" {
err = ErrInterrupt
}
sb.Reset()
sb.WriteString("")
sb.Flush()
rl.Write([]byte(showCursor))
rl.Close()
return "", err
}
echo := cur.Get()
if p.Mask != 0 {
echo = cur.GetMask(p.Mask)
}
prompt := render(p.Templates.success, p.Label)
prompt = append(prompt, []byte(echo)...)
if p.IsConfirm {
lowerDefault := strings.ToLower(p.Default)
if strings.ToLower(cur.Get()) != "y" && (lowerDefault != "y" || (lowerDefault == "y" && cur.Get() != "")) {
prompt = render(p.Templates.invalid, p.Label)
err = ErrAbort
}
}
if p.HideEntered {
clearScreen(sb)
} else {
sb.Reset()
sb.Write(prompt)
sb.Flush()
}
rl.Write([]byte(showCursor))
rl.Close()
return cur.Get(), err
}
func (p *Prompt) prepareTemplates() error {
tpls := p.Templates
if tpls == nil {
tpls = &PromptTemplates{}
}
if tpls.FuncMap == nil {
tpls.FuncMap = FuncMap
}
bold := Styler(FGBold)
if p.IsConfirm {
if tpls.Confirm == "" {
confirm := "y/N"
if strings.ToLower(p.Default) == "y" {
confirm = "Y/n"
}
tpls.Confirm = fmt.Sprintf(`{{ "%s" | bold }} {{ . | bold }}? {{ "[%s]" | faint }} `, IconInitial, confirm)
}
tpl, err := template.New("").Funcs(tpls.FuncMap).Parse(tpls.Confirm)
if err != nil {
return err
}
tpls.prompt = tpl
} else {
if tpls.Prompt == "" {
tpls.Prompt = fmt.Sprintf("%s {{ . | bold }}%s ", bold(IconInitial), bold(":"))
}
tpl, err := template.New("").Funcs(tpls.FuncMap).Parse(tpls.Prompt)
if err != nil {
return err
}
tpls.prompt = tpl
}
if tpls.Valid == "" {
tpls.Valid = fmt.Sprintf("%s {{ . | bold }}%s ", bold(IconGood), bold(":"))
}
tpl, err := template.New("").Funcs(tpls.FuncMap).Parse(tpls.Valid)
if err != nil {
return err
}
tpls.valid = tpl
if tpls.Invalid == "" {
tpls.Invalid = fmt.Sprintf("%s {{ . | bold }}%s ", bold(IconBad), bold(":"))
}
tpl, err = template.New("").Funcs(tpls.FuncMap).Parse(tpls.Invalid)
if err != nil {
return err
}
tpls.invalid = tpl
if tpls.ValidationError == "" {
tpls.ValidationError = `{{ ">>" | red }} {{ . | red }}`
}
tpl, err = template.New("").Funcs(tpls.FuncMap).Parse(tpls.ValidationError)
if err != nil {
return err
}
tpls.validation = tpl
if tpls.Success == "" {
tpls.Success = fmt.Sprintf("{{ . | faint }}%s ", Styler(FGFaint)(":"))
}
tpl, err = template.New("").Funcs(tpls.FuncMap).Parse(tpls.Success)
if err != nil {
return err
}
tpls.success = tpl
p.Templates = tpls
return nil
}
================================================
FILE: promptui.go
================================================
// Package promptui is a library providing a simple interface to create command-line prompts for go.
// It can be easily integrated into spf13/cobra, urfave/cli or any cli go application.
//
// promptui has two main input modes:
//
// Prompt provides a single line for user input. It supports optional live validation,
// confirmation and masking the input.
//
// Select provides a list of options to choose from. It supports pagination, search,
// detailed view and custom templates.
package promptui
import "errors"
// ErrEOF is the error returned from prompts when EOF is encountered.
var ErrEOF = errors.New("^D")
// ErrInterrupt is the error returned from prompts when an interrupt (ctrl-c) is
// encountered.
var ErrInterrupt = errors.New("^C")
// ErrAbort is the error returned when confirm prompts are supplied "n"
var ErrAbort = errors.New("")
// ValidateFunc is a placeholder type for any validation functions that validates a given input. It should return
// a ValidationError if the input is not valid.
type ValidateFunc func(string) error
================================================
FILE: screenbuf/screenbuf.go
================================================
package screenbuf
import (
"bytes"
"fmt"
"io"
)
const esc = "\033["
var (
clearLine = []byte(esc + "2K\r")
moveUp = []byte(esc + "1A")
moveDown = []byte(esc + "1B")
)
// ScreenBuf is a convenient way to write to terminal screens. It creates,
// clears and, moves up or down lines as needed to write the output to the
// terminal using ANSI escape codes.
type ScreenBuf struct {
w io.Writer
buf *bytes.Buffer
reset bool
cursor int
height int
}
// New creates and initializes a new ScreenBuf.
func New(w io.Writer) *ScreenBuf {
return &ScreenBuf{buf: &bytes.Buffer{}, w: w}
}
// Reset truncates the underlining buffer and marks all its previous lines to be
// cleared during the next Write.
func (s *ScreenBuf) Reset() {
s.buf.Reset()
s.reset = true
}
// Clear clears all previous lines and the output starts from the top.
func (s *ScreenBuf) Clear() error {
for i := 0; i < s.height; i++ {
_, err := s.buf.Write(moveUp)
if err != nil {
return err
}
_, err = s.buf.Write(clearLine)
if err != nil {
return err
}
}
s.cursor = 0
s.height = 0
s.reset = false
return nil
}
// Write writes a single line to the underlining buffer. If the ScreenBuf was
// previously reset, all previous lines are cleared and the output starts from
// the top. Lines with \r or \n will cause an error since they can interfere with the
// terminal ability to move between lines.
func (s *ScreenBuf) Write(b []byte) (int, error) {
if bytes.ContainsAny(b, "\r\n") {
return 0, fmt.Errorf("%q should not contain either \\r or \\n", b)
}
if s.reset {
if err := s.Clear(); err != nil {
return 0, err
}
}
switch {
case s.cursor == s.height:
n, err := s.buf.Write(clearLine)
if err != nil {
return n, err
}
n, err = s.buf.Write(b)
if err != nil {
return n, err
}
_, err = s.buf.Write([]byte("\n"))
if err != nil {
return n, err
}
s.height++
s.cursor++
return n, nil
case s.cursor < s.height:
n, err := s.buf.Write(clearLine)
if err != nil {
return n, err
}
n, err = s.buf.Write(b)
if err != nil {
return n, err
}
n, err = s.buf.Write(moveDown)
if err != nil {
return n, err
}
s.cursor++
return n, nil
default:
return 0, fmt.Errorf("Invalid write cursor position (%d) exceeded line height: %d", s.cursor, s.height)
}
}
// Flush writes any buffered data to the underlying io.Writer, ensuring that any pending data is displayed.
func (s *ScreenBuf) Flush() error {
for i := s.cursor; i < s.height; i++ {
if i < s.height {
_, err := s.buf.Write(clearLine)
if err != nil {
return err
}
}
_, err := s.buf.Write(moveDown)
if err != nil {
return err
}
}
_, err := s.buf.WriteTo(s.w)
if err != nil {
return err
}
s.buf.Reset()
for i := 0; i < s.height; i++ {
_, err := s.buf.Write(moveUp)
if err != nil {
return err
}
}
s.cursor = 0
return nil
}
// WriteString is a convenient function to write a new line passing a string.
// Check ScreenBuf.Write() for a detailed explanation of the function behaviour.
func (s *ScreenBuf) WriteString(str string) (int, error) {
return s.Write([]byte(str))
}
================================================
FILE: screenbuf/screenbuf_test.go
================================================
package screenbuf
import (
"bytes"
"testing"
)
func TestScreen(t *testing.T) {
// overwrite regular movement codes for easier visualization
clearLine = []byte("\\c")
moveUp = []byte("\\u")
moveDown = []byte("\\d")
var buf bytes.Buffer
s := New(&buf)
tcs := []struct {
scenario string
lines []string
expect string
cursor int
height int
flush bool
reset bool
clear bool
}{
{
scenario: "initial write",
lines: []string{"Line One"},
expect: "\\cLine One\n",
cursor: 1,
height: 1,
},
{
scenario: "write of with same number of lines",
lines: []string{"Line One"},
expect: "\\u\\cLine One\\d",
cursor: 1,
height: 1,
},
{
scenario: "write of with more lines",
lines: []string{"Line One", "Line Two"},
expect: "\\u\\cLine One\\d\\cLine Two\n",
cursor: 2,
height: 2,
},
{
scenario: "write of with fewer lines",
lines: []string{"line One"},
expect: "\\u\\u\\cline One\\d\\c\\d",
cursor: 1,
height: 2,
},
{
scenario: "write of way more lines",
lines: []string{"line one", "line two", "line three", "line four", "line five"},
expect: "\\u\\u\\cline one\\d\\cline two\\d\\cline three\n\\cline four\n\\cline five\n",
cursor: 5,
height: 5,
},
{
scenario: "write of way less lines",
lines: []string{"line one", "line two"},
expect: "\\u\\u\\u\\u\\u\\cline one\\d\\cline two\\d\\c\\d\\c\\d\\c\\d",
cursor: 2,
height: 5,
},
{
scenario: "write of way more lines",
lines: []string{"line one", "line two", "line three", "line four", "line five"},
expect: "\\u\\u\\u\\u\\u\\cline one\\d\\cline two\\d\\cline three\\d\\cline four\\d\\cline five\\d",
cursor: 5,
height: 5,
},
{
scenario: "reset and write",
lines: []string{"line one", "line two"},
expect: "\\u\\c\\u\\c\\u\\c\\u\\c\\u\\c\\cline one\n\\cline two\n",
cursor: 2,
height: 2,
reset: true,
},
{
scenario: "clear all previous lines",
lines: []string{"line one", "line two"},
expect: "\\u\\u\\cline one\\d\\cline two\\d\\u\\c\\u\\c",
cursor: 0,
height: 0,
clear: true,
},
}
for _, tc := range tcs {
t.Run(tc.scenario, func(t *testing.T) {
buf.Reset()
if tc.reset {
s.Reset()
}
for _, line := range tc.lines {
_, err := s.WriteString(line)
if err != nil {
t.Fatalf("expected no error, got %v", err)
}
}
if tc.clear {
if err := s.Clear(); err != nil {
t.Errorf("expected no error, got %d", err)
}
}
if tc.cursor != s.cursor {
t.Errorf("expected cursor %d, got %d", tc.cursor, s.cursor)
}
err := s.Flush()
if err != nil {
t.Fatalf("expected no error, got %v", err)
}
got := buf.String()
if tc.expect != got {
t.Errorf("expected %q, got %q", tc.expect, got)
}
if tc.height != s.height {
t.Errorf("expected height %d, got %d", tc.height, s.height)
}
})
}
}
================================================
FILE: select.go
================================================
package promptui
import (
"bytes"
"fmt"
"io"
"os"
"text/tabwriter"
"text/template"
"github.com/chzyer/readline"
"github.com/manifoldco/promptui/list"
"github.com/manifoldco/promptui/screenbuf"
)
// SelectedAdd is used internally inside SelectWithAdd when the add option is selected in select mode.
// Since -1 is not a possible selected index, this ensure that add mode is always unique inside
// SelectWithAdd's logic.
const SelectedAdd = -1
// Select represents a list of items used to enable selections, they can be used as search engines, menus
// or as a list of items in a cli based prompt.
type Select struct {
// Label is the text displayed on top of the list to direct input. The IconInitial value "?" will be
// appended automatically to the label so it does not need to be added.
//
// The value for Label can be a simple string or a struct that will need to be accessed by dot notation
// inside the templates. For example, `{{ .Name }}` will display the name property of a struct.
Label interface{}
// Items are the items to display inside the list. It expect a slice of any kind of values, including strings.
//
// If using a slice of strings, promptui will use those strings directly into its base templates or the
// provided templates. If using any other type in the slice, it will attempt to transform it into a string
// before giving it to its templates. Custom templates will override this behavior if using the dot notation
// inside the templates.
//
// For example, `{{ .Name }}` will display the name property of a struct.
Items interface{}
// Size is the number of items that should appear on the select before scrolling is necessary. Defaults to 5.
Size int
// CursorPos is the initial position of the cursor.
CursorPos int
// IsVimMode sets whether to use vim mode when using readline in the command prompt. Look at
// https://godoc.org/github.com/chzyer/readline#Config for more information on readline.
IsVimMode bool
// HideHelp sets whether to hide help information.
HideHelp bool
// HideSelected sets whether to hide the text displayed after an item is successfully selected.
HideSelected bool
// Templates can be used to customize the select output. If nil is passed, the
// default templates are used. See the SelectTemplates docs for more info.
Templates *SelectTemplates
// Keys is the set of keys used in select mode to control the command line interface. See the SelectKeys docs for
// more info.
Keys *SelectKeys
// Searcher is a function that can be implemented to refine the base searching algorithm in selects.
//
// Search is a function that will receive the searched term and the item's index and should return a boolean
// for whether or not the terms are alike. It is unimplemented by default and search will not work unless
// it is implemented.
Searcher list.Searcher
// StartInSearchMode sets whether or not the select mode should start in search mode or selection mode.
// For search mode to work, the Search property must be implemented.
StartInSearchMode bool
list *list.List
// A function that determines how to render the cursor
Pointer Pointer
Stdin io.ReadCloser
Stdout io.WriteCloser
}
// SelectKeys defines the available keys used by select mode to enable the user to move around the list
// and trigger search mode. See the Key struct docs for more information on keys.
type SelectKeys struct {
// Next is the key used to move to the next element inside the list. Defaults to down arrow key.
Next Key
// Prev is the key used to move to the previous element inside the list. Defaults to up arrow key.
Prev Key
// PageUp is the key used to jump back to the first element inside the list. Defaults to left arrow key.
PageUp Key
// PageUp is the key used to jump forward to the last element inside the list. Defaults to right arrow key.
PageDown Key
// Search is the key used to trigger the search mode for the list. Default to the "/" key.
Search Key
}
// Key defines a keyboard code and a display representation for the help menu.
type Key struct {
// Code is a rune that will be used to compare against typed keys with readline.
// Check https://github.com/chzyer/readline for a list of codes
Code rune
// Display is the string that will be displayed inside the help menu to help inform the user
// of which key to use on his keyboard for various functions.
Display string
}
// SelectTemplates allow a select list to be customized following stdlib
// text/template syntax. Custom state, colors and background color are available for use inside
// the templates and are documented inside the Variable section of the docs.
//
// Examples
//
// text/templates use a special notation to display programmable content. Using the double bracket notation,
// the value can be printed with specific helper functions. For example
//
// This displays the value given to the template as pure, unstylized text. Structs are transformed to string
// with this notation.
// '{{ . }}'
//
// This displays the name property of the value colored in cyan
// '{{ .Name | cyan }}'
//
// This displays the label property of value colored in red with a cyan background-color
// '{{ .Label | red | cyan }}'
//
// See the doc of text/template for more info: https://golang.org/pkg/text/template/
//
// Notes
//
// Setting any of these templates will remove the icons from the default templates. They must
// be added back in each of their specific templates. The styles.go constants contains the default icons.
type SelectTemplates struct {
// Label is a text/template for the main command line label. Defaults to printing the label as it with
// the IconInitial.
Label string
// Active is a text/template for when an item is currently active within the list.
Active string
// Inactive is a text/template for when an item is not currently active inside the list. This
// template is used for all items unless they are active or selected.
Inactive string
// Selected is a text/template for when an item was successfully selected.
Selected string
// Details is a text/template for when an item current active to show
// additional information. It can have multiple lines.
//
// Detail will always be displayed for the active element and thus can be used to display additional
// information on the element beyond its label.
//
// promptui will not trim spaces and tabs will be displayed if the template is indented.
Details string
// Help is a text/template for displaying instructions at the top. By default
// it shows keys for movement and search.
Help string
// FuncMap is a map of helper functions that can be used inside of templates according to the text/template
// documentation.
//
// By default, FuncMap contains the color functions used to color the text in templates. If FuncMap
// is overridden, the colors functions must be added in the override from promptui.FuncMap to work.
FuncMap template.FuncMap
label *template.Template
active *template.Template
inactive *template.Template
selected *template.Template
details *template.Template
help *template.Template
}
// SearchPrompt is the prompt displayed in search mode.
var SearchPrompt = "Search: "
// Run executes the select list. It displays the label and the list of items, asking the user to chose any
// value within to list. Run will keep the prompt alive until it has been canceled from
// the command prompt or it has received a valid value. It will return the value and an error if any
// occurred during the select's execution.
func (s *Select) Run() (int, string, error) {
return s.RunCursorAt(s.CursorPos, 0)
}
// RunCursorAt executes the select list, initializing the cursor to the given
// position. Invalid cursor positions will be clamped to valid values. It
// displays the label and the list of items, asking the user to chose any value
// within to list. Run will keep the prompt alive until it has been canceled
// from the command prompt or it has received a valid value. It will return
// the value and an error if any occurred during the select's execution.
func (s *Select) RunCursorAt(cursorPos, scroll int) (int, string, error) {
if s.Size == 0 {
s.Size = 5
}
l, err := list.New(s.Items, s.Size)
if err != nil {
return 0, "", err
}
l.Searcher = s.Searcher
s.list = l
s.setKeys()
err = s.prepareTemplates()
if err != nil {
return 0, "", err
}
return s.innerRun(cursorPos, scroll, ' ')
}
func (s *Select) innerRun(cursorPos, scroll int, top rune) (int, string, error) {
c := &readline.Config{
Stdin: s.Stdin,
Stdout: s.Stdout,
}
err := c.Init()
if err != nil {
return 0, "", err
}
c.Stdin = readline.NewCancelableStdin(c.Stdin)
if s.IsVimMode {
c.VimMode = true
}
c.HistoryLimit = -1
c.UniqueEditLine = true
rl, err := readline.NewEx(c)
if err != nil {
return 0, "", err
}
rl.Write([]byte(hideCursor))
sb := screenbuf.New(rl)
cur := NewCursor("", s.Pointer, false)
canSearch := s.Searcher != nil
searchMode := s.StartInSearchMode
s.list.SetCursor(cursorPos)
s.list.SetStart(scroll)
c.SetListener(func(line []rune, pos int, key rune) ([]rune, int, bool) {
switch {
case key == KeyEnter:
return nil, 0, true
case key == s.Keys.Next.Code || (key == 'j' && !searchMode):
s.list.Next()
case key == s.Keys.Prev.Code || (key == 'k' && !searchMode):
s.list.Prev()
case key == s.Keys.Search.Code:
if !canSearch {
break
}
if searchMode {
searchMode = false
cur.Replace("")
s.list.CancelSearch()
} else {
searchMode = true
}
case key == KeyBackspace || key == KeyCtrlH:
if !canSearch || !searchMode {
break
}
cur.Backspace()
if len(cur.Get()) > 0 {
s.list.Search(cur.Get())
} else {
s.list.CancelSearch()
}
case key == s.Keys.PageUp.Code || (key == 'h' && !searchMode):
s.list.PageUp()
case key == s.Keys.PageDown.Code || (key == 'l' && !searchMode):
s.list.PageDown()
default:
if canSearch && searchMode {
cur.Update(string(line))
s.list.Search(cur.Get())
}
}
if searchMode {
header := SearchPrompt + cur.Format()
sb.WriteString(header)
} else if !s.HideHelp {
help := s.renderHelp(canSearch)
sb.Write(help)
}
label := render(s.Templates.label, s.Label)
sb.Write(label)
items, idx := s.list.Items()
last := len(items) - 1
for i, item := range items {
page := " "
switch i {
case 0:
if s.list.CanPageUp() {
page = "↑"
} else {
page = string(top)
}
case last:
if s.list.CanPageDown() {
page = "↓"
}
}
output := []byte(page + " ")
if i == idx {
output = append(output, render(s.Templates.active, item)...)
} else {
output = append(output, render(s.Templates.inactive, item)...)
}
sb.Write(output)
}
if idx == list.NotFound {
sb.WriteString("")
sb.WriteString("No results")
} else {
active := items[idx]
details := s.renderDetails(active)
for _, d := range details {
sb.Write(d)
}
}
sb.Flush()
return nil, 0, true
})
for {
_, err = rl.Readline()
if err != nil {
switch {
case err == readline.ErrInterrupt, err.Error() == "Interrupt":
err = ErrInterrupt
case err == io.EOF:
err = ErrEOF
}
break
}
_, idx := s.list.Items()
if idx != list.NotFound {
break
}
}
if err != nil {
if err.Error() == "Interrupt" {
err = ErrInterrupt
}
sb.Reset()
sb.WriteString("")
sb.Flush()
rl.Write([]byte(showCursor))
rl.Close()
return 0, "", err
}
items, idx := s.list.Items()
item := items[idx]
if s.HideSelected {
clearScreen(sb)
} else {
sb.Reset()
sb.Write(render(s.Templates.selected, item))
sb.Flush()
}
rl.Write([]byte(showCursor))
rl.Close()
return s.list.Index(), fmt.Sprintf("%v", item), err
}
// ScrollPosition returns the current scroll position.
func (s *Select) ScrollPosition() int {
return s.list.Start()
}
func (s *Select) prepareTemplates() error {
tpls := s.Templates
if tpls == nil {
tpls = &SelectTemplates{}
}
if tpls.FuncMap == nil {
tpls.FuncMap = FuncMap
}
if tpls.Label == "" {
tpls.Label = fmt.Sprintf("%s {{.}}: ", IconInitial)
}
tpl, err := template.New("").Funcs(tpls.FuncMap).Parse(tpls.Label)
if err != nil {
return err
}
tpls.label = tpl
if tpls.Active == "" {
tpls.Active = fmt.Sprintf("%s {{ . | underline }}", IconSelect)
}
tpl, err = template.New("").Funcs(tpls.FuncMap).Parse(tpls.Active)
if err != nil {
return err
}
tpls.active = tpl
if tpls.Inactive == "" {
tpls.Inactive = " {{.}}"
}
tpl, err = template.New("").Funcs(tpls.FuncMap).Parse(tpls.Inactive)
if err != nil {
return err
}
tpls.inactive = tpl
if tpls.Selected == "" {
tpls.Selected = fmt.Sprintf(`{{ "%s" | green }} {{ . | faint }}`, IconGood)
}
tpl, err = template.New("").Funcs(tpls.FuncMap).Parse(tpls.Selected)
if err != nil {
return err
}
tpls.selected = tpl
if tpls.Details != "" {
tpl, err = template.New("").Funcs(tpls.FuncMap).Parse(tpls.Details)
if err != nil {
return err
}
tpls.details = tpl
}
if tpls.Help == "" {
tpls.Help = fmt.Sprintf(`{{ "Use the arrow keys to navigate:" | faint }} {{ .NextKey | faint }} ` +
`{{ .PrevKey | faint }} {{ .PageDownKey | faint }} {{ .PageUpKey | faint }} ` +
`{{ if .Search }} {{ "and" | faint }} {{ .SearchKey | faint }} {{ "toggles search" | faint }}{{ end }}`)
}
tpl, err = template.New("").Funcs(tpls.FuncMap).Parse(tpls.Help)
if err != nil {
return err
}
tpls.help = tpl
s.Templates = tpls
return nil
}
// SelectWithAdd represents a list for selecting a single item inside a list of items with the possibility to
// add new items to the list.
type SelectWithAdd struct {
// Label is the text displayed on top of the list to direct input. The IconInitial value "?" will be
// appended automatically to the label so it does not need to be added.
Label string
// Items are the items to display inside the list. Each item will be listed individually with the
// AddLabel as the first item of the list.
Items []string
// AddLabel is the label used for the first item of the list that enables adding a new item.
// Selecting this item in the list displays the add item prompt using promptui/prompt.
AddLabel string
// Validate is an optional function that fill be used against the entered value in the prompt to validate it.
// If the value is valid, it is returned to the callee to be added in the list.
Validate ValidateFunc
// IsVimMode sets whether to use vim mode when using readline in the command prompt. Look at
// https://godoc.org/github.com/chzyer/readline#Config for more information on readline.
IsVimMode bool
// a function that defines how to render the cursor
Pointer Pointer
// HideHelp sets whether to hide help information.
HideHelp bool
}
// Run executes the select list. Its displays the label and the list of items, asking the user to chose any
// value within to list or add his own. Run will keep the prompt alive until it has been canceled from
// the command prompt or it has received a valid value.
//
// If the addLabel is selected in the list, this function will return a -1 index with the added label and no error.
// Otherwise, it will return the index and the value of the selected item. In any case, if an error is triggered, it
// will also return the error as its third return value.
func (sa *SelectWithAdd) Run() (int, string, error) {
if len(sa.Items) > 0 {
newItems := append([]string{sa.AddLabel}, sa.Items...)
list, err := list.New(newItems, 5)
if err != nil {
return 0, "", err
}
s := Select{
Label: sa.Label,
Items: newItems,
IsVimMode: sa.IsVimMode,
HideHelp: sa.HideHelp,
Size: 5,
list: list,
Pointer: sa.Pointer,
}
s.setKeys()
err = s.prepareTemplates()
if err != nil {
return 0, "", err
}
selected, value, err := s.innerRun(1, 0, '+')
if err != nil || selected != 0 {
return selected - 1, value, err
}
// XXX run through terminal for windows
os.Stdout.Write([]byte(upLine(1) + "\r" + clearLine))
}
p := Prompt{
Label: sa.AddLabel,
Validate: sa.Validate,
IsVimMode: sa.IsVimMode,
Pointer: sa.Pointer,
}
value, err := p.Run()
return SelectedAdd, value, err
}
func (s *Select) setKeys() {
if s.Keys != nil {
return
}
s.Keys = &SelectKeys{
Prev: Key{Code: KeyPrev, Display: KeyPrevDisplay},
Next: Key{Code: KeyNext, Display: KeyNextDisplay},
PageUp: Key{Code: KeyBackward, Display: KeyBackwardDisplay},
PageDown: Key{Code: KeyForward, Display: KeyForwardDisplay},
Search: Key{Code: '/', Display: "/"},
}
}
func (s *Select) renderDetails(item interface{}) [][]byte {
if s.Templates.details == nil {
return nil
}
var buf bytes.Buffer
w := tabwriter.NewWriter(&buf, 0, 0, 8, ' ', 0)
err := s.Templates.details.Execute(w, item)
if err != nil {
fmt.Fprintf(w, "%v", item)
}
w.Flush()
output := buf.Bytes()
return bytes.Split(output, []byte("\n"))
}
func (s *Select) renderHelp(b bool) []byte {
keys := struct {
NextKey string
PrevKey string
PageDownKey string
PageUpKey string
Search bool
SearchKey string
}{
NextKey: s.Keys.Next.Display,
PrevKey: s.Keys.Prev.Display,
PageDownKey: s.Keys.PageDown.Display,
PageUpKey: s.Keys.PageUp.Display,
SearchKey: s.Keys.Search.Display,
Search: b,
}
return render(s.Templates.help, keys)
}
func render(tpl *template.Template, data interface{}) []byte {
var buf bytes.Buffer
err := tpl.Execute(&buf, data)
if err != nil {
return []byte(fmt.Sprintf("%v", data))
}
return buf.Bytes()
}
func clearScreen(sb *screenbuf.ScreenBuf) {
sb.Reset()
sb.Clear()
sb.Flush()
}
================================================
FILE: select_test.go
================================================
package promptui
import (
"bytes"
"testing"
"github.com/manifoldco/promptui/screenbuf"
)
func TestSelectTemplateRender(t *testing.T) {
t.Run("when using default style", func(t *testing.T) {
values := []string{"Zero"}
s := Select{
Label: "Select Number",
Items: values,
}
err := s.prepareTemplates()
if err != nil {
t.Fatalf("Unexpected error preparing templates %v", err)
}
result := string(render(s.Templates.label, s.Label))
exp := "\x1b[34m?\x1b[0m Select Number: "
if result != exp {
t.Errorf("Expected label to eq %q, got %q", exp, result)
}
result = string(render(s.Templates.active, values[0]))
exp = "\x1b[1m▸\x1b[0m \x1b[4mZero\x1b[0m"
if result != exp {
t.Errorf("Expected active item to eq %q, got %q", exp, result)
}
result = string(render(s.Templates.inactive, values[0]))
exp = " Zero"
if result != exp {
t.Errorf("Expected inactive item to eq %q, got %q", exp, result)
}
result = string(render(s.Templates.selected, values[0]))
exp = "\x1b[32m\x1b[32m✔\x1b[0m \x1b[2mZero\x1b[0m"
if result != exp {
t.Errorf("Expected selected item to eq %q, got %q", exp, result)
}
})
t.Run("when using custom style", func(t *testing.T) {
type pepper struct {
Name string
HeatUnit int
Peppers int
Description string
}
peppers := []pepper{
{
Name: "Bell Pepper",
HeatUnit: 0,
Peppers: 1,
Description: "Not very spicy!",
},
}
templates := &SelectTemplates{
Label: "{{ . }}?",
Active: "\U0001F525 {{ .Name | bold }} ({{ .HeatUnit | red | italic }})",
Inactive: " {{ .Name | bold }} ({{ .HeatUnit | red | italic }})",
Selected: "\U0001F525 {{ .Name | red | bold }}",
Details: `Name: {{.Name}}
Peppers: {{.Peppers}}
Description: {{.Description}}`,
}
s := Select{
Label: "Spicy Level",
Items: peppers,
Templates: templates,
}
err := s.prepareTemplates()
if err != nil {
t.Fatalf("Unexpected error preparing templates %v", err)
}
result := string(render(s.Templates.label, s.Label))
exp := "Spicy Level?"
if result != exp {
t.Errorf("Expected label to eq %q, got %q", exp, result)
}
result = string(render(s.Templates.active, peppers[0]))
exp = "🔥 \x1b[1mBell Pepper\x1b[0m (\x1b[3m\x1b[31m0\x1b[0m)"
if result != exp {
t.Errorf("Expected active item to eq %q, got %q", exp, result)
}
result = string(render(s.Templates.inactive, peppers[0]))
exp = " \x1b[1mBell Pepper\x1b[0m (\x1b[3m\x1b[31m0\x1b[0m)"
if result != exp {
t.Errorf("Expected inactive item to eq %q, got %q", exp, result)
}
result = string(render(s.Templates.selected, peppers[0]))
exp = "🔥 \x1b[1m\x1b[31mBell Pepper\x1b[0m"
if result != exp {
t.Errorf("Expected selected item to eq %q, got %q", exp, result)
}
result = string(render(s.Templates.details, peppers[0]))
exp = "Name: Bell Pepper\nPeppers: 1\nDescription: Not very spicy!"
if result != exp {
t.Errorf("Expected selected item to eq %q, got %q", exp, result)
}
})
t.Run("when a template is invalid", func(t *testing.T) {
templates := &SelectTemplates{
Label: "{{ . ",
}
s := Select{
Label: "Spicy Level",
Templates: templates,
}
err := s.prepareTemplates()
if err == nil {
t.Fatalf("Expected error got none")
}
})
t.Run("when a template render fails", func(t *testing.T) {
templates := &SelectTemplates{
Label: "{{ .InvalidName }}",
}
s := Select{
Label: struct{ Name string }{Name: "Pepper"},
Items: []string{},
Templates: templates,
}
err := s.prepareTemplates()
if err != nil {
t.Fatalf("Unexpected error preparing templates %v", err)
}
result := string(render(s.Templates.label, s.Label))
exp := "{Pepper}"
if result != exp {
t.Errorf("Expected label to eq %q, got %q", exp, result)
}
})
}
func TestClearScreen(t *testing.T) {
var buf bytes.Buffer
sb := screenbuf.New(&buf)
sb.WriteString("test")
clearScreen(sb)
got := buf.String()
except := "\x1b[1A\x1b[2K\r"
if except != got {
t.Errorf("expected %q, got %q", except, got)
}
}
================================================
FILE: styles.go
================================================
// +build !windows
package promptui
// These are the default icons used by promptui for select and prompts. These should not be overridden and instead
// customized through the use of custom templates
var (
// IconInitial is the icon used when starting in prompt mode and the icon next to the label when
// starting in select mode.
IconInitial = Styler(FGBlue)("?")
// IconGood is the icon used when a good answer is entered in prompt mode.
IconGood = Styler(FGGreen)("✔")
// IconWarn is the icon used when a good, but potentially invalid answer is entered in prompt mode.
IconWarn = Styler(FGYellow)("⚠")
// IconBad is the icon used when a bad answer is entered in prompt mode.
IconBad = Styler(FGRed)("✗")
// IconSelect is the icon used to identify the currently selected item in select mode.
IconSelect = Styler(FGBold)("▸")
)
================================================
FILE: styles_windows.go
================================================
package promptui
// These are the default icons used bu promptui for select and prompts. They can either be overridden directly
// from these variable or customized through the use of custom templates
var (
// IconInitial is the icon used when starting in prompt mode and the icon next to the label when
// starting in select mode.
IconInitial = Styler(FGBlue)("?")
// IconGood is the icon used when a good answer is entered in prompt mode.
IconGood = Styler(FGGreen)("v")
// IconWarn is the icon used when a good, but potentially invalid answer is entered in prompt mode.
IconWarn = Styler(FGYellow)("!")
// IconBad is the icon used when a bad answer is entered in prompt mode.
IconBad = Styler(FGRed)("x")
// IconSelect is the icon used to identify the currently selected item in select mode.
IconSelect = Styler(FGBold)(">")
)
gitextract_895fqpq3/ ├── .github/ │ ├── CONTRIBUTING.md │ └── listbot.md ├── .gitignore ├── .golangci.yml ├── .travis.yml ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── LICENSE.md ├── Makefile ├── README.md ├── _examples/ │ ├── confirm/ │ │ └── main.go │ ├── custom_prompt/ │ │ └── main.go │ ├── custom_select/ │ │ └── main.go │ ├── prompt/ │ │ └── main.go │ ├── prompt_default/ │ │ └── main.go │ ├── prompt_password/ │ │ └── main.go │ ├── select/ │ │ └── main.go │ └── select_add/ │ └── main.go ├── codes.go ├── codes_test.go ├── cursor.go ├── cursor_test.go ├── example_main_test.go ├── example_prompt_test.go ├── example_select_test.go ├── example_selectwithadd_test.go ├── go.mod ├── go.sum ├── keycodes.go ├── keycodes_other.go ├── keycodes_windows.go ├── list/ │ ├── list.go │ └── list_test.go ├── prompt.go ├── promptui.go ├── screenbuf/ │ ├── screenbuf.go │ └── screenbuf_test.go ├── select.go ├── select_test.go ├── styles.go └── styles_windows.go
SYMBOL INDEX (125 symbols across 24 files)
FILE: _examples/confirm/main.go
function main (line 9) | func main() {
FILE: _examples/custom_prompt/main.go
type pepper (line 10) | type pepper struct
function main (line 16) | func main() {
FILE: _examples/custom_select/main.go
type pepper (line 10) | type pepper struct
function main (line 16) | func main() {
FILE: _examples/prompt/main.go
function main (line 11) | func main() {
FILE: _examples/prompt_default/main.go
function main (line 11) | func main() {
FILE: _examples/prompt_password/main.go
function main (line 10) | func main() {
FILE: _examples/select/main.go
function main (line 9) | func main() {
FILE: _examples/select_add/main.go
function main (line 9) | func main() {
FILE: codes.go
constant esc (line 10) | esc = "\033["
type attribute (line 12) | type attribute
constant reset (line 18) | reset attribute = iota
constant FGBold (line 20) | FGBold
constant FGFaint (line 21) | FGFaint
constant FGItalic (line 22) | FGItalic
constant FGUnderline (line 23) | FGUnderline
constant FGBlack (line 30) | FGBlack attribute = iota + 30
constant FGRed (line 31) | FGRed
constant FGGreen (line 32) | FGGreen
constant FGYellow (line 33) | FGYellow
constant FGBlue (line 34) | FGBlue
constant FGMagenta (line 35) | FGMagenta
constant FGCyan (line 36) | FGCyan
constant FGWhite (line 37) | FGWhite
constant BGBlack (line 44) | BGBlack attribute = iota + 40
constant BGRed (line 45) | BGRed
constant BGGreen (line 46) | BGGreen
constant BGYellow (line 47) | BGYellow
constant BGBlue (line 48) | BGBlue
constant BGMagenta (line 49) | BGMagenta
constant BGCyan (line 50) | BGCyan
constant BGWhite (line 51) | BGWhite
constant hideCursor (line 58) | hideCursor = esc + "?25l"
constant showCursor (line 59) | showCursor = esc + "?25h"
constant clearLine (line 60) | clearLine = esc + "2K"
function upLine (line 90) | func upLine(n uint) string {
function movementCode (line 94) | func movementCode(n uint, code rune) string {
function Styler (line 104) | func Styler(attrs ...attribute) func(interface{}) string {
FILE: codes_test.go
function TestStyler (line 5) | func TestStyler(t *testing.T) {
FILE: cursor.go
type Pointer (line 10) | type Pointer
function defaultCursor (line 12) | func defaultCursor(ignored []rune) []rune {
function blockCursor (line 16) | func blockCursor(input []rune) []rune {
function pipeCursor (line 20) | func pipeCursor(input []rune) []rune {
type Cursor (line 44) | type Cursor struct
method String (line 70) | func (c *Cursor) String() string {
method End (line 78) | func (c *Cursor) End() {
method Start (line 84) | func (c *Cursor) Start() {
method correctPosition (line 89) | func (c *Cursor) correctPosition() {
method Format (line 119) | func (c *Cursor) Format() string {
method FormatMask (line 126) | func (c *Cursor) FormatMask(mask rune) string {
method Update (line 140) | func (c *Cursor) Update(newinput string) {
method Get (line 150) | func (c *Cursor) Get() string {
method GetMask (line 155) | func (c *Cursor) GetMask(mask rune) string {
method Replace (line 161) | func (c *Cursor) Replace(input string) {
method Place (line 167) | func (c *Cursor) Place(position int) {
method Move (line 173) | func (c *Cursor) Move(shift int) {
method Backspace (line 183) | func (c *Cursor) Backspace() {
method Listen (line 200) | func (c *Cursor) Listen(line []rune, pos int, key rune) ([]rune, int, ...
function NewCursor (line 57) | func NewCursor(startinginput string, pointer Pointer, eraseDefault bool)...
function format (line 100) | func format(a []rune, c *Cursor) string {
FILE: cursor_test.go
function TestDefinedCursors (line 5) | func TestDefinedCursors(t *testing.T) {
function TestCursor (line 14) | func TestCursor(t *testing.T) {
FILE: example_main_test.go
function Example_prompt (line 12) | func Example_prompt() {
function Example_select (line 39) | func Example_select() {
FILE: example_prompt_test.go
function ExamplePrompt (line 11) | func ExamplePrompt() {
FILE: example_select_test.go
type pepper (line 10) | type pepper struct
function ExampleSelect (line 17) | func ExampleSelect() {
FILE: example_selectwithadd_test.go
function ExampleSelectWithAdd (line 7) | func ExampleSelectWithAdd() {
FILE: list/list.go
type Searcher (line 12) | type Searcher
constant NotFound (line 16) | NotFound = -1
type List (line 21) | type List struct
method Prev (line 55) | func (l *List) Prev() {
method Search (line 67) | func (l *List) Search(term string) {
method CancelSearch (line 76) | func (l *List) CancelSearch() {
method search (line 82) | func (l *List) search(term string) {
method Start (line 95) | func (l *List) Start() int {
method SetStart (line 101) | func (l *List) SetStart(i int) {
method SetCursor (line 114) | func (l *List) SetCursor(i int) {
method Next (line 134) | func (l *List) Next() {
method PageUp (line 150) | func (l *List) PageUp() {
method PageDown (line 168) | func (l *List) PageDown() {
method CanPageDown (line 191) | func (l *List) CanPageDown() bool {
method CanPageUp (line 197) | func (l *List) CanPageUp() bool {
method Index (line 203) | func (l *List) Index() int {
method Items (line 217) | func (l *List) Items() ([]interface{}, int) {
function New (line 32) | func New(items interface{}, size int) (*List, error) {
FILE: list/list_test.go
function TestListNew (line 9) | func TestListNew(t *testing.T) {
function TestListMovement (line 32) | func TestListMovement(t *testing.T) {
function TestListPageDown (line 92) | func TestListPageDown(t *testing.T) {
function TestListComparion (line 112) | func TestListComparion(t *testing.T) {
function castList (line 158) | func castList(list []interface{}) []rune {
FILE: prompt.go
type Prompt (line 14) | type Prompt struct
method Run (line 115) | func (p *Prompt) Run() (string, error) {
method prepareTemplates (line 254) | func (p *Prompt) prepareTemplates() error {
type PromptTemplates (line 76) | type PromptTemplates struct
FILE: promptui.go
type ValidateFunc (line 27) | type ValidateFunc
FILE: screenbuf/screenbuf.go
constant esc (line 9) | esc = "\033["
type ScreenBuf (line 20) | type ScreenBuf struct
method Reset (line 35) | func (s *ScreenBuf) Reset() {
method Clear (line 41) | func (s *ScreenBuf) Clear() error {
method Write (line 62) | func (s *ScreenBuf) Write(b []byte) (int, error) {
method Flush (line 114) | func (s *ScreenBuf) Flush() error {
method WriteString (line 149) | func (s *ScreenBuf) WriteString(str string) (int, error) {
function New (line 29) | func New(w io.Writer) *ScreenBuf {
FILE: screenbuf/screenbuf_test.go
function TestScreen (line 8) | func TestScreen(t *testing.T) {
FILE: select.go
constant SelectedAdd (line 19) | SelectedAdd = -1
type Select (line 23) | type Select struct
method Run (line 190) | func (s *Select) Run() (int, string, error) {
method RunCursorAt (line 200) | func (s *Select) RunCursorAt(cursorPos, scroll int) (int, string, erro...
method innerRun (line 222) | func (s *Select) innerRun(cursorPos, scroll int, top rune) (int, strin...
method ScrollPosition (line 406) | func (s *Select) ScrollPosition() int {
method prepareTemplates (line 410) | func (s *Select) prepareTemplates() error {
method setKeys (line 571) | func (s *Select) setKeys() {
method renderDetails (line 584) | func (s *Select) renderDetails(item interface{}) [][]byte {
method renderHelp (line 605) | func (s *Select) renderHelp(b bool) []byte {
type SelectKeys (line 87) | type SelectKeys struct
type Key (line 105) | type Key struct
type SelectTemplates (line 140) | type SelectTemplates struct
type SelectWithAdd (line 492) | type SelectWithAdd struct
method Run (line 527) | func (sa *SelectWithAdd) Run() (int, string, error) {
function render (line 625) | func render(tpl *template.Template, data interface{}) []byte {
function clearScreen (line 634) | func clearScreen(sb *screenbuf.ScreenBuf) {
FILE: select_test.go
function TestSelectTemplateRender (line 10) | func TestSelectTemplateRender(t *testing.T) {
function TestClearScreen (line 155) | func TestClearScreen(t *testing.T) {
Condensed preview — 41 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (98K chars).
[
{
"path": ".github/CONTRIBUTING.md",
"chars": 2220,
"preview": "# Contributing Guidelines\n\nContributions are always welcome; however, please read this document in its\nentirety before s"
},
{
"path": ".github/listbot.md",
"chars": 45,
"preview": "**Author**\n\n- [ ] Changelog has been updated\n"
},
{
"path": ".gitignore",
"chars": 26,
"preview": "vendor\nall-cover.txt\nbin/\n"
},
{
"path": ".golangci.yml",
"chars": 437,
"preview": "run:\n deadline: 5m\n\nissues:\n # Disable maximums so we see all issues\n max-per-linter: 0\n max-same-issues: 0\n\n # gol"
},
{
"path": ".travis.yml",
"chars": 260,
"preview": "dist: bionic\nlanguage: go\n\ngo:\n - \"1.12.x\"\n - \"1.13.x\"\n\nbranches:\n only:\n - master\n\nafter_success:\n # only report"
},
{
"path": "CHANGELOG.md",
"chars": 2547,
"preview": "# CHANGELOG\n\nAll notable changes to this project will be documented in this file.\n\nThe format is based on [Keep a Change"
},
{
"path": "CODE_OF_CONDUCT.md",
"chars": 3163,
"preview": "# Code of Conduct\n\n## Our Pledge\n\nIn the interest of fostering an open and welcoming environment, we as\ncontributors and"
},
{
"path": "LICENSE.md",
"chars": 1520,
"preview": "BSD 3-Clause License\n\nCopyright (c) 2017, Arigato Machine Inc.\nAll rights reserved.\n\nRedistribution and use in source an"
},
{
"path": "Makefile",
"chars": 1234,
"preview": "export GO111MODULE := on\nexport PATH := ./bin:$(PATH)\n\nci: bootstrap lint cover\n.PHONY: ci\n\n############################"
},
{
"path": "README.md",
"chars": 2660,
"preview": "# promptui\n\nInteractive prompt for command-line applications.\n\nWe built Promptui because we wanted to make it easy and f"
},
{
"path": "_examples/confirm/main.go",
"chars": 306,
"preview": "package main\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/manifoldco/promptui\"\n)\n\nfunc main() {\n\tprompt := promptui.Prompt{\n\t\tLabel: "
},
{
"path": "_examples/custom_prompt/main.go",
"chars": 669,
"preview": "package main\n\nimport (\n\t\"fmt\"\n\t\"strconv\"\n\n\t\"github.com/manifoldco/promptui\"\n)\n\ntype pepper struct {\n\tName string\n\tHe"
},
{
"path": "_examples/custom_select/main.go",
"chars": 1687,
"preview": "package main\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\n\t\"github.com/manifoldco/promptui\"\n)\n\ntype pepper struct {\n\tName string\n\tHe"
},
{
"path": "_examples/prompt/main.go",
"chars": 480,
"preview": "package main\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"strconv\"\n\n\t\"github.com/manifoldco/promptui\"\n)\n\nfunc main() {\n\tvalidate := func"
},
{
"path": "_examples/prompt_default/main.go",
"chars": 591,
"preview": "package main\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"os/user\"\n\n\t\"github.com/manifoldco/promptui\"\n)\n\nfunc main() {\n\tvalidate := func"
},
{
"path": "_examples/prompt_password/main.go",
"chars": 483,
"preview": "package main\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\n\t\"github.com/manifoldco/promptui\"\n)\n\nfunc main() {\n\tvalidate := func(input stri"
},
{
"path": "_examples/select/main.go",
"chars": 381,
"preview": "package main\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/manifoldco/promptui\"\n)\n\nfunc main() {\n\tprompt := promptui.Select{\n\t\tLabel: \""
},
{
"path": "_examples/select_add/main.go",
"chars": 542,
"preview": "package main\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/manifoldco/promptui\"\n)\n\nfunc main() {\n\titems := []string{\"Vim\", \"Emacs\", \"Su"
},
{
"path": "codes.go",
"chars": 2905,
"preview": "package promptui\n\nimport (\n\t\"fmt\"\n\t\"strconv\"\n\t\"strings\"\n\t\"text/template\"\n)\n\nconst esc = \"\\033[\"\n\ntype attribute int\n\n// "
},
{
"path": "codes_test.go",
"chars": 791,
"preview": "package promptui\n\nimport \"testing\"\n\nfunc TestStyler(t *testing.T) {\n\tt.Run(\"renders a single code\", func(t *testing.T) {"
},
{
"path": "cursor.go",
"chars": 5695,
"preview": "package promptui\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n)\n\n// Pointer is A specific type that translates a given set of runes into "
},
{
"path": "cursor_test.go",
"chars": 2711,
"preview": "package promptui\n\nimport \"testing\"\n\nfunc TestDefinedCursors(t *testing.T) {\n\tt.Run(\"pipeCursor\", func(t *testing.T) {\n\t\t"
},
{
"path": "example_main_test.go",
"chars": 1242,
"preview": "package promptui\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"strconv\"\n)\n\n// This is an example for the Prompt mode of promptui. In this"
},
{
"path": "example_prompt_test.go",
"chars": 1018,
"preview": "package promptui\n\nimport (\n\t\"fmt\"\n\t\"strconv\"\n)\n\n// This example shows how to use the prompt validator and templates to c"
},
{
"path": "example_select_test.go",
"chars": 2603,
"preview": "package promptui\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n)\n\n// Any type can be given to the select's item as long as the templates p"
},
{
"path": "example_selectwithadd_test.go",
"chars": 657,
"preview": "package promptui\n\nimport \"fmt\"\n\n// This example shows how to create a SelectWithAdd that will add each new item it is gi"
},
{
"path": "go.mod",
"chars": 304,
"preview": "module github.com/manifoldco/promptui\n\ngo 1.12\n\nrequire (\n\tgithub.com/chzyer/logex v1.1.10 // indirect\n\tgithub.com/chzye"
},
{
"path": "go.sum",
"chars": 820,
"preview": "github.com/chzyer/logex v1.1.10 h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE=\ngithub.com/chzyer/logex v1.1.10/go.mod h"
},
{
"path": "keycodes.go",
"chars": 972,
"preview": "package promptui\n\nimport \"github.com/chzyer/readline\"\n\n// These runes are used to identify the commands entered by the u"
},
{
"path": "keycodes_other.go",
"chars": 188,
"preview": "// +build !windows\n\npackage promptui\n\nimport \"github.com/chzyer/readline\"\n\nvar (\n\t// KeyBackspace is the default key for"
},
{
"path": "keycodes_windows.go",
"chars": 234,
"preview": "// +build windows\n\npackage promptui\n\n// source: https://msdn.microsoft.com/en-us/library/aa243025(v=vs.60).aspx\n\nvar (\n\t"
},
{
"path": "list/list.go",
"chars": 5568,
"preview": "package list\n\nimport (\n\t\"fmt\"\n\t\"reflect\"\n\t\"strings\"\n)\n\n// Searcher is a base function signature that is used inside sele"
},
{
"path": "list/list_test.go",
"chars": 3691,
"preview": "package list\n\nimport (\n\t\"fmt\"\n\t\"reflect\"\n\t\"testing\"\n)\n\nfunc TestListNew(t *testing.T) {\n\tt.Run(\"when items a slice nil\","
},
{
"path": "prompt.go",
"chars": 8700,
"preview": "package promptui\n\nimport (\n\t\"fmt\"\n\t\"io\"\n\t\"strings\"\n\t\"text/template\"\n\n\t\"github.com/chzyer/readline\"\n\t\"github.com/manifold"
},
{
"path": "promptui.go",
"chars": 1057,
"preview": "// Package promptui is a library providing a simple interface to create command-line prompts for go.\n// It can be easily"
},
{
"path": "screenbuf/screenbuf.go",
"chars": 3153,
"preview": "package screenbuf\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"io\"\n)\n\nconst esc = \"\\033[\"\n\nvar (\n\tclearLine = []byte(esc + \"2K\\r\")\n\tmoveU"
},
{
"path": "screenbuf/screenbuf_test.go",
"chars": 3014,
"preview": "package screenbuf\n\nimport (\n\t\"bytes\"\n\t\"testing\"\n)\n\nfunc TestScreen(t *testing.T) {\n\t// overwrite regular movement codes "
},
{
"path": "select.go",
"chars": 17802,
"preview": "package promptui\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\t\"text/tabwriter\"\n\t\"text/template\"\n\n\t\"github.com/chzyer/readline\""
},
{
"path": "select_test.go",
"chars": 4129,
"preview": "package promptui\n\nimport (\n\t\"bytes\"\n\t\"testing\"\n\n\t\"github.com/manifoldco/promptui/screenbuf\"\n)\n\nfunc TestSelectTemplateRe"
},
{
"path": "styles.go",
"chars": 847,
"preview": "// +build !windows\n\npackage promptui\n\n// These are the default icons used by promptui for select and prompts. These shou"
},
{
"path": "styles_windows.go",
"chars": 846,
"preview": "package promptui\n\n// These are the default icons used bu promptui for select and prompts. They can either be overridden "
}
]
About this extraction
This page contains the full source code of the manifoldco/promptui GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 41 files (86.1 KB), approximately 25.4k tokens, and a symbol index with 125 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.