pax_global_header00006660000000000000000000000064141372227760014525gustar00rootroot0000000000000052 comment=c2e487d3597f59bcf76b24c9e80679740a72212b promptui-0.9.0/000077500000000000000000000000001413722277600134125ustar00rootroot00000000000000promptui-0.9.0/.github/000077500000000000000000000000001413722277600147525ustar00rootroot00000000000000promptui-0.9.0/.github/CONTRIBUTING.md000066400000000000000000000042561413722277600172120ustar00rootroot00000000000000# 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. promptui-0.9.0/.github/listbot.md000066400000000000000000000000551413722277600167540ustar00rootroot00000000000000**Author** - [ ] Changelog has been updated promptui-0.9.0/.gitignore000066400000000000000000000000321413722277600153750ustar00rootroot00000000000000vendor all-cover.txt bin/ promptui-0.9.0/.golangci.yml000066400000000000000000000006651413722277600160050ustar00rootroot00000000000000run: 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 promptui-0.9.0/.travis.yml000066400000000000000000000004041413722277600155210ustar00rootroot00000000000000dist: 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 promptui-0.9.0/CHANGELOG.md000066400000000000000000000047631413722277600152350ustar00rootroot00000000000000# 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 - `` 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 promptui-0.9.0/CODE_OF_CONDUCT.md000066400000000000000000000061331413722277600162140ustar00rootroot00000000000000# 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). promptui-0.9.0/LICENSE.md000066400000000000000000000027601413722277600150230ustar00rootroot00000000000000BSD 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. promptui-0.9.0/Makefile000066400000000000000000000023221413722277600150510ustar00rootroot00000000000000export 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 promptui-0.9.0/README.md000066400000000000000000000051441413722277600146750ustar00rootroot00000000000000# 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) [![GitHub release](https://img.shields.io/github/tag/manifoldco/promptui.svg?label=latest)](https://github.com/manifoldco/promptui/releases) [![GoDoc](https://img.shields.io/badge/godoc-reference-blue.svg)](https://godoc.org/github.com/manifoldco/promptui) [![Travis](https://img.shields.io/travis/manifoldco/promptui/master.svg)](https://travis-ci.org/manifoldco/promptui) [![Go Report Card](https://goreportcard.com/badge/github.com/manifoldco/promptui)](https://goreportcard.com/report/github.com/manifoldco/promptui) [![License](https://img.shields.io/badge/license-BSD-blue.svg)](./LICENSE.md) ## Overview ![promptui](https://media.giphy.com/media/xUNda0Ngb5qsogLsBi/giphy.gif) 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) promptui-0.9.0/_examples/000077500000000000000000000000001413722277600153675ustar00rootroot00000000000000promptui-0.9.0/_examples/confirm/000077500000000000000000000000001413722277600170245ustar00rootroot00000000000000promptui-0.9.0/_examples/confirm/main.go000066400000000000000000000004621413722277600203010ustar00rootroot00000000000000package 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) } promptui-0.9.0/_examples/custom_prompt/000077500000000000000000000000001413722277600203025ustar00rootroot00000000000000promptui-0.9.0/_examples/custom_prompt/main.go000066400000000000000000000012351413722277600215560ustar00rootroot00000000000000package 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) } promptui-0.9.0/_examples/custom_select/000077500000000000000000000000001413722277600202405ustar00rootroot00000000000000promptui-0.9.0/_examples/custom_select/main.go000066400000000000000000000032321413722277600215130ustar00rootroot00000000000000package 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) } promptui-0.9.0/_examples/prompt/000077500000000000000000000000001413722277600167105ustar00rootroot00000000000000promptui-0.9.0/_examples/prompt/main.go000066400000000000000000000007401413722277600201640ustar00rootroot00000000000000package 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) } promptui-0.9.0/_examples/prompt_default/000077500000000000000000000000001413722277600204145ustar00rootroot00000000000000promptui-0.9.0/_examples/prompt_default/main.go000066400000000000000000000011171413722277600216670ustar00rootroot00000000000000package 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) } promptui-0.9.0/_examples/prompt_password/000077500000000000000000000000001413722277600206325ustar00rootroot00000000000000promptui-0.9.0/_examples/prompt_password/main.go000066400000000000000000000007431413722277600221110ustar00rootroot00000000000000package 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) } promptui-0.9.0/_examples/select/000077500000000000000000000000001413722277600166465ustar00rootroot00000000000000promptui-0.9.0/_examples/select/main.go000066400000000000000000000005751413722277600201300ustar00rootroot00000000000000package 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) } promptui-0.9.0/_examples/select_add/000077500000000000000000000000001413722277600174565ustar00rootroot00000000000000promptui-0.9.0/_examples/select_add/main.go000066400000000000000000000010361413722277600207310ustar00rootroot00000000000000package 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) } promptui-0.9.0/codes.go000066400000000000000000000055311413722277600150420ustar00rootroot00000000000000package 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) } } promptui-0.9.0/codes_test.go000066400000000000000000000014271413722277600161010ustar00rootroot00000000000000package 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) } }) } promptui-0.9.0/cursor.go000066400000000000000000000130771413722277600152660ustar00rootroot00000000000000package 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 } promptui-0.9.0/cursor_test.go000066400000000000000000000052271413722277600163230ustar00rootroot00000000000000package 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()) } }) } promptui-0.9.0/example_main_test.go000066400000000000000000000023321413722277600174370ustar00rootroot00000000000000package 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) } promptui-0.9.0/example_prompt_test.go000066400000000000000000000017721413722277600200430ustar00rootroot00000000000000package 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) } promptui-0.9.0/example_select_test.go000066400000000000000000000050561413722277600200000ustar00rootroot00000000000000package 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) } promptui-0.9.0/example_selectwithadd_test.go000066400000000000000000000012211413722277600213330ustar00rootroot00000000000000package 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) } promptui-0.9.0/go.mod000066400000000000000000000004601413722277600145200ustar00rootroot00000000000000module 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 ) promptui-0.9.0/go.sum000066400000000000000000000014641413722277600145520ustar00rootroot00000000000000github.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= promptui-0.9.0/keycodes.go000066400000000000000000000017241413722277600155530ustar00rootroot00000000000000package 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 = "→" ) promptui-0.9.0/keycodes_other.go000066400000000000000000000002741413722277600167530ustar00rootroot00000000000000// +build !windows package promptui import "github.com/chzyer/readline" var ( // KeyBackspace is the default key for deleting input text. KeyBackspace rune = readline.CharBackspace ) promptui-0.9.0/keycodes_windows.go000066400000000000000000000003521413722277600173210ustar00rootroot00000000000000// +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 ) promptui-0.9.0/list/000077500000000000000000000000001413722277600143655ustar00rootroot00000000000000promptui-0.9.0/list/list.go000066400000000000000000000127001413722277600156670ustar00rootroot00000000000000package 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 } promptui-0.9.0/list/list_test.go000066400000000000000000000071531413722277600167340ustar00rootroot00000000000000package 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 } promptui-0.9.0/prompt.go000066400000000000000000000207741413722277600152740ustar00rootroot00000000000000package 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 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 } promptui-0.9.0/promptui.go000066400000000000000000000020411413722277600156150ustar00rootroot00000000000000// 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 promptui-0.9.0/screenbuf/000077500000000000000000000000001413722277600153665ustar00rootroot00000000000000promptui-0.9.0/screenbuf/screenbuf.go000066400000000000000000000061211413722277600176710ustar00rootroot00000000000000package 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)) } promptui-0.9.0/screenbuf/screenbuf_test.go000066400000000000000000000057061413722277600207400ustar00rootroot00000000000000package 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) } }) } } promptui-0.9.0/select.go000066400000000000000000000426161413722277600152310ustar00rootroot00000000000000package 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() } promptui-0.9.0/select_test.go000066400000000000000000000100531413722277600162560ustar00rootroot00000000000000package 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) } } promptui-0.9.0/styles.go000066400000000000000000000015271413722277600152710ustar00rootroot00000000000000// +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)("▸") ) promptui-0.9.0/styles_windows.go000066400000000000000000000015161413722277600170410ustar00rootroot00000000000000package 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)(">") )