├── _tools
├── README.md
├── complete_file
│ └── main.go
├── vt100_debug
│ └── main.go
└── sigwinch
│ └── main.go
├── .github
├── ISSUE_TEMPLATE
│ ├── feature_request.md
│ └── bug_report.md
└── workflows
│ └── test.yml
├── go.mod
├── .gitignore
├── internal
├── bisect
│ ├── bisect.go
│ └── bisect_test.go
├── term
│ ├── term.go
│ └── raw.go
├── debug
│ ├── assert.go
│ └── log.go
└── strings
│ ├── strings_test.go
│ └── strings.go
├── _example
├── http-prompt
│ ├── api.py
│ └── main.go
├── build.sh
├── exec-command
│ └── main.go
├── simple-echo
│ ├── cjk-cyrillic
│ │ └── main.go
│ └── main.go
├── README.md
└── live-prefix
│ └── main.go
├── input_test.go
├── emacs_test.go
├── signal_windows.go
├── signal_posix.go
├── LICENSE
├── output_windows.go
├── shortcut.go
├── Makefile
├── key_bind.go
├── key_string.go
├── key_bind_func.go
├── history_test.go
├── output_vt100_test.go
├── history.go
├── output_posix.go
├── input_posix.go
├── input_windows.go
├── go.sum
├── filter.go
├── key.go
├── completer
└── file.go
├── CHANGELOG.md
├── emacs.go
├── render_test.go
├── filter_test.go
├── output.go
├── completion.go
├── buffer_test.go
├── buffer.go
├── README.md
├── completion_test.go
├── input.go
├── prompt.go
├── output_vt100.go
├── render.go
├── option.go
├── document.go
└── document_test.go
/_tools/README.md:
--------------------------------------------------------------------------------
1 | ## Tools of go-prompt
2 |
3 | ### vt100_debug
4 |
5 | 
6 |
7 | ### sigwinch
8 |
9 | 
10 |
11 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: "Feature request"
3 | about: Suggest an idea for new features in go-prompt.
4 | title: "[Feature Request]"
5 | labels: enhancement
6 | assignees: ''
7 |
8 | ---
9 |
10 | # Feature Request
11 |
12 | *Please write your suggestion here.*
13 |
14 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/c-bata/go-prompt
2 |
3 | go 1.14
4 |
5 | require (
6 | github.com/mattn/go-colorable v0.1.7
7 | github.com/mattn/go-runewidth v0.0.9
8 | github.com/mattn/go-tty v0.0.3
9 | github.com/pkg/term v1.2.0-beta.2
10 | golang.org/x/sys v0.0.0-20200918174421-af09f7315aff
11 | )
12 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Compiled Object files, Static and Dynamic libs (Shared Objects)
2 | *.o
3 | *.a
4 | *.so
5 | bin/
6 |
7 | # Folders
8 | pkg/
9 | _obj
10 | _test
11 |
12 | # Architecture specific extensions/prefixes
13 | *.cgo1.go
14 | *.cgo2.c
15 | _cgo_defun.c
16 | _cgo_gotypes.go
17 | _cgo_export.*
18 |
19 | _testmain.go
20 |
21 | *.exe
22 | *.test
23 | *.prof
24 |
25 | # Glide
26 | vendor/
27 |
--------------------------------------------------------------------------------
/internal/bisect/bisect.go:
--------------------------------------------------------------------------------
1 | package bisect
2 |
3 | import "sort"
4 |
5 | // Right to locate the insertion point for v in a to maintain sorted order.
6 | func Right(a []int, v int) int {
7 | return bisectRightRange(a, v, 0, len(a))
8 | }
9 |
10 | func bisectRightRange(a []int, v int, lo, hi int) int {
11 | s := a[lo:hi]
12 | return sort.Search(len(s), func(i int) bool {
13 | return s[i] > v
14 | })
15 | }
16 |
--------------------------------------------------------------------------------
/_example/http-prompt/api.py:
--------------------------------------------------------------------------------
1 | from bottle import route, run, request
2 |
3 | @route('/')
4 | def hello():
5 | return "Hello World!"
6 |
7 | @route('/ping')
8 | def hello():
9 | return "pong!"
10 |
11 | @route('/register', method='POST')
12 | def register():
13 | name = request.json.get("name")
14 | return "Hello %s!" % name
15 |
16 | if __name__ == "__main__":
17 | run(host='localhost', port=8000, debug=True)
18 |
--------------------------------------------------------------------------------
/_example/build.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | export GO111MODULE=on
4 | DIR=$(cd $(dirname $0); pwd)
5 | BIN_DIR=$(cd $(dirname $(dirname $0)); pwd)/bin
6 |
7 | mkdir -p ${BIN_DIR}
8 | go build -o ${BIN_DIR}/exec-command ${DIR}/exec-command/main.go
9 | go build -o ${BIN_DIR}/http-prompt ${DIR}/http-prompt/main.go
10 | go build -o ${BIN_DIR}/live-prefix ${DIR}/live-prefix/main.go
11 | go build -o ${BIN_DIR}/simple-echo ${DIR}/simple-echo/main.go
12 | go build -o ${BIN_DIR}/simple-echo-cjk-cyrillic ${DIR}/simple-echo/cjk-cyrillic/main.go
13 |
--------------------------------------------------------------------------------
/_example/exec-command/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "os"
5 | "os/exec"
6 |
7 | prompt "github.com/c-bata/go-prompt"
8 | )
9 |
10 | func executor(t string) {
11 | if t == "bash" {
12 | cmd := exec.Command("bash")
13 | cmd.Stdin = os.Stdin
14 | cmd.Stdout = os.Stdout
15 | cmd.Stderr = os.Stderr
16 | cmd.Run()
17 | }
18 | return
19 | }
20 |
21 | func completer(t prompt.Document) []prompt.Suggest {
22 | return []prompt.Suggest{
23 | {Text: "bash"},
24 | }
25 | }
26 |
27 | func main() {
28 | p := prompt.New(
29 | executor,
30 | completer,
31 | )
32 | p.Run()
33 | }
34 |
--------------------------------------------------------------------------------
/input_test.go:
--------------------------------------------------------------------------------
1 | package prompt
2 |
3 | import (
4 | "testing"
5 | )
6 |
7 | func TestPosixParserGetKey(t *testing.T) {
8 | scenarioTable := []struct {
9 | name string
10 | input []byte
11 | expected Key
12 | }{
13 | {
14 | name: "escape",
15 | input: []byte{0x1b},
16 | expected: Escape,
17 | },
18 | {
19 | name: "undefined",
20 | input: []byte{'a'},
21 | expected: NotDefined,
22 | },
23 | }
24 |
25 | for _, s := range scenarioTable {
26 | t.Run(s.name, func(t *testing.T) {
27 | key := GetKey(s.input)
28 | if key != s.expected {
29 | t.Errorf("Should be %s, but got %s", key, s.expected)
30 | }
31 | })
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/internal/term/term.go:
--------------------------------------------------------------------------------
1 | // +build !windows
2 |
3 | package term
4 |
5 | import (
6 | "sync"
7 |
8 | "github.com/pkg/term/termios"
9 | "golang.org/x/sys/unix"
10 | )
11 |
12 | var (
13 | saveTermios *unix.Termios
14 | saveTermiosFD int
15 | saveTermiosOnce sync.Once
16 | )
17 |
18 | func getOriginalTermios(fd int) (*unix.Termios, error) {
19 | var err error
20 | saveTermiosOnce.Do(func() {
21 | saveTermiosFD = fd
22 | saveTermios, err = termios.Tcgetattr(uintptr(fd))
23 | })
24 | return saveTermios, err
25 | }
26 |
27 | // Restore terminal's mode.
28 | func Restore() error {
29 | o, err := getOriginalTermios(saveTermiosFD)
30 | if err != nil {
31 | return err
32 | }
33 | return termios.Tcsetattr(uintptr(saveTermiosFD), termios.TCSANOW, o)
34 | }
35 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: "Bug report"
3 | about: Create a bug report to improve go-prompt
4 | title: "[Bug]"
5 | labels: bug
6 | assignees: ''
7 |
8 | ---
9 |
10 | # Bug reports
11 |
12 | *Please file a bug report here.*
13 |
14 | ## Expected Behavior
15 |
16 | *Please describe the behavior you are expecting*
17 |
18 | ## Current Behavior and Steps to Reproduce
19 |
20 | *What is the current behavior? Please provide detailed steps for reproducing the issue.*
21 | *A picture or gif animation tells a thousand words*
22 |
23 | ## Context
24 |
25 | Please provide any relevant information about your setup. This is important in case the issue is not reproducible except for under certain conditions.
26 |
27 | * Operating System:
28 | * Terminal Emulator: (i.e. iTerm2)
29 | * tag of go-prompt or commit revision:
30 |
31 |
--------------------------------------------------------------------------------
/internal/term/raw.go:
--------------------------------------------------------------------------------
1 | // +build !windows
2 |
3 | package term
4 |
5 | import (
6 | "syscall"
7 |
8 | "github.com/pkg/term/termios"
9 | "golang.org/x/sys/unix"
10 | )
11 |
12 | // SetRaw put terminal into a raw mode
13 | func SetRaw(fd int) error {
14 | n, err := getOriginalTermios(fd)
15 | if err != nil {
16 | return err
17 | }
18 |
19 | n.Iflag &^= syscall.IGNBRK | syscall.BRKINT | syscall.PARMRK |
20 | syscall.ISTRIP | syscall.INLCR | syscall.IGNCR |
21 | syscall.ICRNL | syscall.IXON
22 | n.Lflag &^= syscall.ECHO | syscall.ICANON | syscall.IEXTEN | syscall.ISIG | syscall.ECHONL
23 | n.Cflag &^= syscall.CSIZE | syscall.PARENB
24 | n.Cflag |= syscall.CS8 // Set to 8-bit wide. Typical value for displaying characters.
25 | n.Cc[syscall.VMIN] = 1
26 | n.Cc[syscall.VTIME] = 0
27 |
28 | return termios.Tcsetattr(uintptr(fd), termios.TCSANOW, (*unix.Termios)(n))
29 | }
30 |
--------------------------------------------------------------------------------
/emacs_test.go:
--------------------------------------------------------------------------------
1 | package prompt
2 |
3 | import "testing"
4 |
5 | func TestEmacsKeyBindings(t *testing.T) {
6 | buf := NewBuffer()
7 | buf.InsertText("abcde", false, true)
8 | if buf.cursorPosition != len("abcde") {
9 | t.Errorf("Want %d, but got %d", len("abcde"), buf.cursorPosition)
10 | }
11 |
12 | // Go to the beginning of the line
13 | applyEmacsKeyBind(buf, ControlA)
14 | if buf.cursorPosition != 0 {
15 | t.Errorf("Want %d, but got %d", 0, buf.cursorPosition)
16 | }
17 |
18 | // Go to the end of the line
19 | applyEmacsKeyBind(buf, ControlE)
20 | if buf.cursorPosition != len("abcde") {
21 | t.Errorf("Want %d, but got %d", len("abcde"), buf.cursorPosition)
22 | }
23 | }
24 |
25 | func applyEmacsKeyBind(buf *Buffer, key Key) {
26 | for i := range emacsKeyBindings {
27 | kb := emacsKeyBindings[i]
28 | if kb.Key == key {
29 | kb.Fn(buf)
30 | }
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/_example/simple-echo/cjk-cyrillic/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 |
6 | prompt "github.com/c-bata/go-prompt"
7 | )
8 |
9 | func executor(in string) {
10 | fmt.Println("Your input: " + in)
11 | }
12 |
13 | func completer(in prompt.Document) []prompt.Suggest {
14 | s := []prompt.Suggest{
15 | {Text: "こんにちは", Description: "'こんにちは' means 'Hello' in Japanese"},
16 | {Text: "감사합니다", Description: "'안녕하세요' means 'Hello' in Korean."},
17 | {Text: "您好", Description: "'您好' means 'Hello' in Chinese."},
18 | {Text: "Добрый день", Description: "'Добрый день' means 'Hello' in Russian."},
19 | }
20 | return prompt.FilterHasPrefix(s, in.GetWordBeforeCursor(), true)
21 | }
22 |
23 | func main() {
24 | p := prompt.New(
25 | executor,
26 | completer,
27 | prompt.OptionPrefix(">>> "),
28 | prompt.OptionTitle("sql-prompt for multi width characters"),
29 | )
30 | p.Run()
31 | }
32 |
--------------------------------------------------------------------------------
/signal_windows.go:
--------------------------------------------------------------------------------
1 | // +build windows
2 |
3 | package prompt
4 |
5 | import (
6 | "os"
7 | "os/signal"
8 | "syscall"
9 |
10 | "github.com/c-bata/go-prompt/internal/debug"
11 | )
12 |
13 | func (p *Prompt) handleSignals(exitCh chan int, winSizeCh chan *WinSize, stop chan struct{}) {
14 | sigCh := make(chan os.Signal, 1)
15 | signal.Notify(
16 | sigCh,
17 | syscall.SIGINT,
18 | syscall.SIGTERM,
19 | syscall.SIGQUIT,
20 | )
21 |
22 | for {
23 | select {
24 | case <-stop:
25 | debug.Log("stop handleSignals")
26 | return
27 | case s := <-sigCh:
28 | switch s {
29 |
30 | case syscall.SIGINT: // kill -SIGINT XXXX or Ctrl+c
31 | debug.Log("Catch SIGINT")
32 | exitCh <- 0
33 |
34 | case syscall.SIGTERM: // kill -SIGTERM XXXX
35 | debug.Log("Catch SIGTERM")
36 | exitCh <- 1
37 |
38 | case syscall.SIGQUIT: // kill -SIGQUIT XXXX
39 | debug.Log("Catch SIGQUIT")
40 | exitCh <- 0
41 | }
42 | }
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/_tools/complete_file/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "os"
6 | "strings"
7 |
8 | prompt "github.com/c-bata/go-prompt"
9 | "github.com/c-bata/go-prompt/completer"
10 | )
11 |
12 | var filePathCompleter = completer.FilePathCompleter{
13 | IgnoreCase: true,
14 | Filter: func(fi os.FileInfo) bool {
15 | return fi.IsDir() || strings.HasSuffix(fi.Name(), ".go")
16 | },
17 | }
18 |
19 | func executor(in string) {
20 | fmt.Println("Your input: " + in)
21 | }
22 |
23 | func completerFunc(d prompt.Document) []prompt.Suggest {
24 | t := d.GetWordBeforeCursor()
25 | if strings.HasPrefix(t, "--") {
26 | return []prompt.Suggest{
27 | {"--foo", ""},
28 | {"--bar", ""},
29 | {"--baz", ""},
30 | }
31 | }
32 | return filePathCompleter.Complete(d)
33 | }
34 |
35 | func main() {
36 | p := prompt.New(
37 | executor,
38 | completerFunc,
39 | prompt.OptionPrefix(">>> "),
40 | prompt.OptionCompletionWordSeparator(completer.FilePathCompletionSeparator),
41 | )
42 | p.Run()
43 | }
44 |
--------------------------------------------------------------------------------
/_tools/vt100_debug/main.go:
--------------------------------------------------------------------------------
1 | // +build !windows
2 |
3 | package main
4 |
5 | import (
6 | "fmt"
7 | "syscall"
8 |
9 | prompt "github.com/c-bata/go-prompt"
10 | "github.com/c-bata/go-prompt/internal/term"
11 | )
12 |
13 | func main() {
14 | if err := term.SetRaw(syscall.Stdin); err != nil {
15 | fmt.Println(err)
16 | return
17 | }
18 | defer term.Restore()
19 |
20 | bufCh := make(chan []byte, 128)
21 | go readBuffer(bufCh)
22 | fmt.Print("> ")
23 |
24 | for {
25 | b := <-bufCh
26 | if key := prompt.GetKey(b); key == prompt.NotDefined {
27 | fmt.Printf("Key '%s' data:'%#v'\n", string(b), b)
28 | } else {
29 | if key == prompt.ControlC {
30 | fmt.Println("exit.")
31 | return
32 | }
33 | fmt.Printf("Key '%s' data:'%#v'\n", key, b)
34 | }
35 | fmt.Print("> ")
36 | }
37 | }
38 |
39 | func readBuffer(bufCh chan []byte) {
40 | buf := make([]byte, 1024)
41 |
42 | for {
43 | if n, err := syscall.Read(syscall.Stdin, buf); err == nil {
44 | bufCh <- buf[:n]
45 | }
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/_example/simple-echo/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 |
6 | prompt "github.com/c-bata/go-prompt"
7 | )
8 |
9 | func completer(in prompt.Document) []prompt.Suggest {
10 | s := []prompt.Suggest{
11 | {Text: "users", Description: "Store the username and age"},
12 | {Text: "articles", Description: "Store the article text posted by user"},
13 | {Text: "comments", Description: "Store the text commented to articles"},
14 | {Text: "groups", Description: "Combine users with specific rules"},
15 | }
16 | return prompt.FilterHasPrefix(s, in.GetWordBeforeCursor(), true)
17 | }
18 |
19 | func main() {
20 | in := prompt.Input(">>> ", completer,
21 | prompt.OptionTitle("sql-prompt"),
22 | prompt.OptionHistory([]string{"SELECT * FROM users;"}),
23 | prompt.OptionPrefixTextColor(prompt.Yellow),
24 | prompt.OptionPreviewSuggestionTextColor(prompt.Blue),
25 | prompt.OptionSelectedSuggestionBGColor(prompt.LightGray),
26 | prompt.OptionSuggestionBGColor(prompt.DarkGray))
27 | fmt.Println("Your input: " + in)
28 | }
29 |
--------------------------------------------------------------------------------
/internal/debug/assert.go:
--------------------------------------------------------------------------------
1 | package debug
2 |
3 | import (
4 | "fmt"
5 | "os"
6 | )
7 |
8 | const (
9 | envAssertPanic = "GO_PROMPT_ENABLE_ASSERT"
10 | )
11 |
12 | var (
13 | enableAssert bool
14 | )
15 |
16 | func init() {
17 | if e := os.Getenv(envAssertPanic); e == "true" || e == "1" {
18 | enableAssert = true
19 | }
20 | }
21 |
22 | // Assert ensures expected condition.
23 | func Assert(cond bool, msg interface{}) {
24 | if cond {
25 | return
26 | }
27 | if enableAssert {
28 | panic(msg)
29 | }
30 | writeWithSync(2, "[ASSERT] "+toString(msg))
31 | }
32 |
33 | func toString(v interface{}) string {
34 | switch a := v.(type) {
35 | case func() string:
36 | return a()
37 | case string:
38 | return a
39 | case fmt.Stringer:
40 | return a.String()
41 | default:
42 | return fmt.Sprintf("unexpected type, %t", v)
43 | }
44 | }
45 |
46 | // AssertNoError ensures err is nil.
47 | func AssertNoError(err error) {
48 | if err == nil {
49 | return
50 | }
51 | if enableAssert {
52 | panic(err)
53 | }
54 | writeWithSync(2, "[ASSERT] "+err.Error())
55 | }
56 |
--------------------------------------------------------------------------------
/signal_posix.go:
--------------------------------------------------------------------------------
1 | // +build !windows
2 |
3 | package prompt
4 |
5 | import (
6 | "os"
7 | "os/signal"
8 | "syscall"
9 |
10 | "github.com/c-bata/go-prompt/internal/debug"
11 | )
12 |
13 | func (p *Prompt) handleSignals(exitCh chan int, winSizeCh chan *WinSize, stop chan struct{}) {
14 | in := p.in
15 | sigCh := make(chan os.Signal, 1)
16 | signal.Notify(
17 | sigCh,
18 | syscall.SIGINT,
19 | syscall.SIGTERM,
20 | syscall.SIGQUIT,
21 | syscall.SIGWINCH,
22 | )
23 |
24 | for {
25 | select {
26 | case <-stop:
27 | debug.Log("stop handleSignals")
28 | return
29 | case s := <-sigCh:
30 | switch s {
31 | case syscall.SIGINT: // kill -SIGINT XXXX or Ctrl+c
32 | debug.Log("Catch SIGINT")
33 | exitCh <- 0
34 |
35 | case syscall.SIGTERM: // kill -SIGTERM XXXX
36 | debug.Log("Catch SIGTERM")
37 | exitCh <- 1
38 |
39 | case syscall.SIGQUIT: // kill -SIGQUIT XXXX
40 | debug.Log("Catch SIGQUIT")
41 | exitCh <- 0
42 |
43 | case syscall.SIGWINCH:
44 | debug.Log("Catch SIGWINCH")
45 | winSizeCh <- in.GetWinSize()
46 | }
47 | }
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/internal/debug/log.go:
--------------------------------------------------------------------------------
1 | package debug
2 |
3 | import (
4 | "io/ioutil"
5 | "log"
6 | "os"
7 | )
8 |
9 | const (
10 | envEnableLog = "GO_PROMPT_ENABLE_LOG"
11 | logFileName = "go-prompt.log"
12 | )
13 |
14 | var (
15 | logfile *os.File
16 | logger *log.Logger
17 | )
18 |
19 | func init() {
20 | if e := os.Getenv(envEnableLog); e == "true" || e == "1" {
21 | var err error
22 | logfile, err = os.OpenFile(logFileName, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0666)
23 | if err == nil {
24 | logger = log.New(logfile, "", log.Llongfile)
25 | return
26 | }
27 | }
28 | logger = log.New(ioutil.Discard, "", log.Llongfile)
29 | }
30 |
31 | // Teardown to close logfile
32 | func Teardown() {
33 | if logfile == nil {
34 | return
35 | }
36 | _ = logfile.Close()
37 | }
38 |
39 | func writeWithSync(calldepth int, msg string) {
40 | calldepth++
41 | if logfile == nil {
42 | return
43 | }
44 | _ = logger.Output(calldepth, msg)
45 | _ = logfile.Sync() // immediately write msg
46 | }
47 |
48 | // Log to output message
49 | func Log(msg string) {
50 | calldepth := 2
51 | writeWithSync(calldepth, msg)
52 | }
53 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2017 Masashi SHIBATA
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/internal/strings/strings_test.go:
--------------------------------------------------------------------------------
1 | package strings_test
2 |
3 | import (
4 | "fmt"
5 |
6 | "github.com/c-bata/go-prompt/internal/strings"
7 | )
8 |
9 | func ExampleIndexNotByte() {
10 | fmt.Println(strings.IndexNotByte("golang", 'g'))
11 | fmt.Println(strings.IndexNotByte("golang", 'x'))
12 | fmt.Println(strings.IndexNotByte("gggggg", 'g'))
13 | // Output:
14 | // 1
15 | // 0
16 | // -1
17 | }
18 |
19 | func ExampleLastIndexNotByte() {
20 | fmt.Println(strings.LastIndexNotByte("golang", 'g'))
21 | fmt.Println(strings.LastIndexNotByte("golang", 'x'))
22 | fmt.Println(strings.LastIndexNotByte("gggggg", 'g'))
23 | // Output:
24 | // 4
25 | // 5
26 | // -1
27 | }
28 |
29 | func ExampleIndexNotAny() {
30 | fmt.Println(strings.IndexNotAny("golang", "glo"))
31 | fmt.Println(strings.IndexNotAny("golang", "gl"))
32 | fmt.Println(strings.IndexNotAny("golang", "golang"))
33 | // Output:
34 | // 3
35 | // 1
36 | // -1
37 | }
38 |
39 | func ExampleLastIndexNotAny() {
40 | fmt.Println(strings.LastIndexNotAny("golang", "agn"))
41 | fmt.Println(strings.LastIndexNotAny("golang", "an"))
42 | fmt.Println(strings.LastIndexNotAny("golang", "golang"))
43 | // Output:
44 | // 2
45 | // 5
46 | // -1
47 | }
48 |
--------------------------------------------------------------------------------
/_example/README.md:
--------------------------------------------------------------------------------
1 | # Examples of go-prompt
2 |
3 | This directory includes some examples using go-prompt.
4 | These examples are useful to know the usage of go-prompt and check behavior for development.
5 |
6 | ## simple-echo
7 |
8 | 
9 |
10 | A simple echo example using `prompt.Input`.
11 |
12 | ## http-prompt
13 |
14 | 
15 |
16 | A simple [http-prompt](https://github.com/eliangcs/http-prompt) implementation using go-prompt in less than 200 lines of Go.
17 |
18 | ## live-prefix
19 |
20 | 
21 |
22 | A example application which changes a prefix string dynamically.
23 | This feature is used like [ktr0731/evans](https://github.com/ktr0731/evans) which is interactive gRPC client using go-prompt.
24 |
25 | ## exec-command
26 |
27 | Run another CLI tool via `os/exec` package.
28 | More practical example is [a source code of kube-prompt](https://github.com/c-bata/kube-prompt).
29 | I recommend you to look this if you want to create tools like kube-prompt.
30 |
31 |
--------------------------------------------------------------------------------
/output_windows.go:
--------------------------------------------------------------------------------
1 | // +build windows
2 |
3 | package prompt
4 |
5 | import (
6 | "io"
7 |
8 | colorable "github.com/mattn/go-colorable"
9 | )
10 |
11 | // WindowsWriter is a ConsoleWriter implementation for Win32 console.
12 | // Output is converted from VT100 escape sequences by mattn/go-colorable.
13 | type WindowsWriter struct {
14 | VT100Writer
15 | out io.Writer
16 | }
17 |
18 | // Flush to flush buffer
19 | func (w *WindowsWriter) Flush() error {
20 | _, err := w.out.Write(w.buffer)
21 | if err != nil {
22 | return err
23 | }
24 | w.buffer = []byte{}
25 | return nil
26 | }
27 |
28 | var _ ConsoleWriter = &WindowsWriter{}
29 |
30 | var (
31 | // NewStandardOutputWriter is Deprecated: Please use NewStdoutWriter
32 | NewStandardOutputWriter = NewStdoutWriter
33 | )
34 |
35 | // NewStdoutWriter returns ConsoleWriter object to write to stdout.
36 | // This generates win32 control sequences.
37 | func NewStdoutWriter() ConsoleWriter {
38 | return &WindowsWriter{
39 | out: colorable.NewColorableStdout(),
40 | }
41 | }
42 |
43 | // NewStderrWriter returns ConsoleWriter object to write to stderr.
44 | // This generates win32 control sequences.
45 | func NewStderrWriter() ConsoleWriter {
46 | return &WindowsWriter{
47 | out: colorable.NewColorableStderr(),
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/shortcut.go:
--------------------------------------------------------------------------------
1 | package prompt
2 |
3 | func dummyExecutor(in string) {}
4 |
5 | // Input get the input data from the user and return it.
6 | func Input(prefix string, completer Completer, opts ...Option) string {
7 | pt := New(dummyExecutor, completer)
8 | pt.renderer.prefixTextColor = DefaultColor
9 | pt.renderer.prefix = prefix
10 |
11 | for _, opt := range opts {
12 | if err := opt(pt); err != nil {
13 | panic(err)
14 | }
15 | }
16 | return pt.Input()
17 | }
18 |
19 | // Choose to the shortcut of input function to select from string array.
20 | // Deprecated: Maybe anyone want to use this.
21 | func Choose(prefix string, choices []string, opts ...Option) string {
22 | completer := newChoiceCompleter(choices, FilterHasPrefix)
23 | pt := New(dummyExecutor, completer)
24 | pt.renderer.prefixTextColor = DefaultColor
25 | pt.renderer.prefix = prefix
26 |
27 | for _, opt := range opts {
28 | if err := opt(pt); err != nil {
29 | panic(err)
30 | }
31 | }
32 | return pt.Input()
33 | }
34 |
35 | func newChoiceCompleter(choices []string, filter Filter) Completer {
36 | s := make([]Suggest, len(choices))
37 | for i := range choices {
38 | s[i] = Suggest{Text: choices[i]}
39 | }
40 | return func(x Document) []Suggest {
41 | return filter(s, x.GetWordBeforeCursor(), true)
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/internal/bisect/bisect_test.go:
--------------------------------------------------------------------------------
1 | package bisect_test
2 |
3 | import (
4 | "fmt"
5 | "math/rand"
6 | "testing"
7 |
8 | "github.com/c-bata/go-prompt/internal/bisect"
9 | )
10 |
11 | func Example() {
12 | in := []int{1, 2, 3, 3, 3, 6, 7}
13 | fmt.Println("Insertion position for 0 in the slice is", bisect.Right(in, 0))
14 | fmt.Println("Insertion position for 4 in the slice is", bisect.Right(in, 4))
15 |
16 | // Output:
17 | // Insertion position for 0 in the slice is 0
18 | // Insertion position for 4 in the slice is 5
19 | }
20 |
21 | func TestBisectRight(t *testing.T) {
22 | // Thanks!! https://play.golang.org/p/y9NRj_XVIW
23 | in := []int{1, 2, 3, 3, 3, 6, 7}
24 |
25 | r := bisect.Right(in, 0)
26 | if r != 0 {
27 | t.Errorf("number 0 should inserted at 0 position, but got %d", r)
28 | }
29 |
30 | r = bisect.Right(in, 4)
31 | if r != 5 {
32 | t.Errorf("number 4 should inserted at 5 position, but got %d", r)
33 | }
34 | }
35 |
36 | func BenchmarkRight(b *testing.B) {
37 | rand.Seed(0)
38 |
39 | for _, l := range []int{10, 1e2, 1e3, 1e4} {
40 | x := rand.Perm(l)
41 | insertion := rand.Int()
42 |
43 | b.Run(fmt.Sprintf("arrayLength=%d", l), func(b *testing.B) {
44 | b.ResetTimer()
45 | for n := 0; n < b.N; n++ {
46 | bisect.Right(x, insertion)
47 | }
48 | })
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/_example/live-prefix/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 |
6 | prompt "github.com/c-bata/go-prompt"
7 | )
8 |
9 | var LivePrefixState struct {
10 | LivePrefix string
11 | IsEnable bool
12 | }
13 |
14 | func executor(in string) {
15 | fmt.Println("Your input: " + in)
16 | if in == "" {
17 | LivePrefixState.IsEnable = false
18 | LivePrefixState.LivePrefix = in
19 | return
20 | }
21 | LivePrefixState.LivePrefix = in + "> "
22 | LivePrefixState.IsEnable = true
23 | }
24 |
25 | func completer(in prompt.Document) []prompt.Suggest {
26 | s := []prompt.Suggest{
27 | {Text: "users", Description: "Store the username and age"},
28 | {Text: "articles", Description: "Store the article text posted by user"},
29 | {Text: "comments", Description: "Store the text commented to articles"},
30 | {Text: "groups", Description: "Combine users with specific rules"},
31 | }
32 | return prompt.FilterHasPrefix(s, in.GetWordBeforeCursor(), true)
33 | }
34 |
35 | func changeLivePrefix() (string, bool) {
36 | return LivePrefixState.LivePrefix, LivePrefixState.IsEnable
37 | }
38 |
39 | func main() {
40 | p := prompt.New(
41 | executor,
42 | completer,
43 | prompt.OptionPrefix(">>> "),
44 | prompt.OptionLivePrefix(changeLivePrefix),
45 | prompt.OptionTitle("live-prefix-example"),
46 | )
47 | p.Run()
48 | }
49 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | .DEFAULT_GOAL := help
2 |
3 | SOURCES := $(shell find . -prune -o -name "*.go" -not -name '*_test.go' -print)
4 |
5 | GOIMPORTS ?= goimports
6 | GOCILINT ?= golangci-lint
7 |
8 | .PHONY: setup
9 | setup: ## Setup for required tools.
10 | go get -u golang.org/x/tools/cmd/goimports
11 | go get -u github.com/golangci/golangci-lint/cmd/golangci-lint
12 | go get -u golang.org/x/tools/cmd/stringer
13 |
14 | .PHONY: fmt
15 | fmt: $(SOURCES) ## Formatting source codes.
16 | @$(GOIMPORTS) -w $^
17 |
18 | .PHONY: lint
19 | lint: ## Run golangci-lint.
20 | @$(GOCILINT) run --no-config --disable-all --enable=goimports --enable=misspell ./...
21 |
22 | .PHONY: test
23 | test: ## Run tests with race condition checking.
24 | @go test -race ./...
25 |
26 | .PHONY: bench
27 | bench: ## Run benchmarks.
28 | @go test -bench=. -run=- -benchmem ./...
29 |
30 | .PHONY: coverage
31 | cover: ## Run the tests.
32 | @go test -coverprofile=coverage.o
33 | @go tool cover -func=coverage.o
34 |
35 | .PHONY: generate
36 | generate: ## Run go generate
37 | @go generate ./...
38 |
39 | .PHONY: build
40 | build: ## Build example command lines.
41 | ./_example/build.sh
42 |
43 | .PHONY: help
44 | help: ## Show help text
45 | @echo "Commands:"
46 | @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf " \033[36m%-10s\033[0m %s\n", $$1, $$2}'
47 |
--------------------------------------------------------------------------------
/key_bind.go:
--------------------------------------------------------------------------------
1 | package prompt
2 |
3 | // KeyBindFunc receives buffer and processed it.
4 | type KeyBindFunc func(*Buffer)
5 |
6 | // KeyBind represents which key should do what operation.
7 | type KeyBind struct {
8 | Key Key
9 | Fn KeyBindFunc
10 | }
11 |
12 | // ASCIICodeBind represents which []byte should do what operation
13 | type ASCIICodeBind struct {
14 | ASCIICode []byte
15 | Fn KeyBindFunc
16 | }
17 |
18 | // KeyBindMode to switch a key binding flexibly.
19 | type KeyBindMode string
20 |
21 | const (
22 | // CommonKeyBind is a mode without any keyboard shortcut
23 | CommonKeyBind KeyBindMode = "common"
24 | // EmacsKeyBind is a mode to use emacs-like keyboard shortcut
25 | EmacsKeyBind KeyBindMode = "emacs"
26 | )
27 |
28 | var commonKeyBindings = []KeyBind{
29 | // Go to the End of the line
30 | {
31 | Key: End,
32 | Fn: GoLineEnd,
33 | },
34 | // Go to the beginning of the line
35 | {
36 | Key: Home,
37 | Fn: GoLineBeginning,
38 | },
39 | // Delete character under the cursor
40 | {
41 | Key: Delete,
42 | Fn: DeleteChar,
43 | },
44 | // Backspace
45 | {
46 | Key: Backspace,
47 | Fn: DeleteBeforeChar,
48 | },
49 | // Right allow: Forward one character
50 | {
51 | Key: Right,
52 | Fn: GoRightChar,
53 | },
54 | // Left allow: Backward one character
55 | {
56 | Key: Left,
57 | Fn: GoLeftChar,
58 | },
59 | }
60 |
--------------------------------------------------------------------------------
/key_string.go:
--------------------------------------------------------------------------------
1 | // Code generated by "stringer -type=Key"; DO NOT EDIT.
2 |
3 | package prompt
4 |
5 | import "strconv"
6 |
7 | const _Key_name = "EscapeControlAControlBControlCControlDControlEControlFControlGControlHControlIControlJControlKControlLControlMControlNControlOControlPControlQControlRControlSControlTControlUControlVControlWControlXControlYControlZControlSpaceControlBackslashControlSquareCloseControlCircumflexControlUnderscoreControlLeftControlRightControlUpControlDownUpDownRightLeftShiftLeftShiftUpShiftDownShiftRightHomeEndDeleteShiftDeleteControlDeletePageUpPageDownBackTabInsertBackspaceTabEnterF1F2F3F4F5F6F7F8F9F10F11F12F13F14F15F16F17F18F19F20F21F22F23F24AnyCPRResponseVt100MouseEventWindowsMouseEventBracketedPasteIgnoreNotDefined"
8 |
9 | var _Key_index = [...]uint16{0, 6, 14, 22, 30, 38, 46, 54, 62, 70, 78, 86, 94, 102, 110, 118, 126, 134, 142, 150, 158, 166, 174, 182, 190, 198, 206, 214, 226, 242, 260, 277, 294, 305, 317, 326, 337, 339, 343, 348, 352, 361, 368, 377, 387, 391, 394, 400, 411, 424, 430, 438, 445, 451, 460, 463, 468, 470, 472, 474, 476, 478, 480, 482, 484, 486, 489, 492, 495, 498, 501, 504, 507, 510, 513, 516, 519, 522, 525, 528, 531, 534, 545, 560, 577, 591, 597, 607}
10 |
11 | func (i Key) String() string {
12 | if i < 0 || i >= Key(len(_Key_index)-1) {
13 | return "Key(" + strconv.FormatInt(int64(i), 10) + ")"
14 | }
15 | return _Key_name[_Key_index[i]:_Key_index[i+1]]
16 | }
17 |
--------------------------------------------------------------------------------
/key_bind_func.go:
--------------------------------------------------------------------------------
1 | package prompt
2 |
3 | // GoLineEnd Go to the End of the line
4 | func GoLineEnd(buf *Buffer) {
5 | x := []rune(buf.Document().TextAfterCursor())
6 | buf.CursorRight(len(x))
7 | }
8 |
9 | // GoLineBeginning Go to the beginning of the line
10 | func GoLineBeginning(buf *Buffer) {
11 | x := []rune(buf.Document().TextBeforeCursor())
12 | buf.CursorLeft(len(x))
13 | }
14 |
15 | // DeleteChar Delete character under the cursor
16 | func DeleteChar(buf *Buffer) {
17 | buf.Delete(1)
18 | }
19 |
20 | // DeleteWord Delete word before the cursor
21 | func DeleteWord(buf *Buffer) {
22 | buf.DeleteBeforeCursor(len([]rune(buf.Document().TextBeforeCursor())) - buf.Document().FindStartOfPreviousWordWithSpace())
23 | }
24 |
25 | // DeleteBeforeChar Go to Backspace
26 | func DeleteBeforeChar(buf *Buffer) {
27 | buf.DeleteBeforeCursor(1)
28 | }
29 |
30 | // GoRightChar Forward one character
31 | func GoRightChar(buf *Buffer) {
32 | buf.CursorRight(1)
33 | }
34 |
35 | // GoLeftChar Backward one character
36 | func GoLeftChar(buf *Buffer) {
37 | buf.CursorLeft(1)
38 | }
39 |
40 | // GoRightWord Forward one word
41 | func GoRightWord(buf *Buffer) {
42 | buf.CursorRight(buf.Document().FindEndOfCurrentWordWithSpace())
43 | }
44 |
45 | // GoLeftWord Backward one word
46 | func GoLeftWord(buf *Buffer) {
47 | buf.CursorLeft(len([]rune(buf.Document().TextBeforeCursor())) - buf.Document().FindStartOfPreviousWordWithSpace())
48 | }
49 |
--------------------------------------------------------------------------------
/history_test.go:
--------------------------------------------------------------------------------
1 | package prompt
2 |
3 | import (
4 | "reflect"
5 | "testing"
6 | )
7 |
8 | func TestHistoryClear(t *testing.T) {
9 | h := NewHistory()
10 | h.Add("foo")
11 | h.Clear()
12 | expected := &History{
13 | histories: []string{"foo"},
14 | tmp: []string{"foo", ""},
15 | selected: 1,
16 | }
17 | if !reflect.DeepEqual(expected, h) {
18 | t.Errorf("Should be %#v, but got %#v", expected, h)
19 | }
20 | }
21 |
22 | func TestHistoryAdd(t *testing.T) {
23 | h := NewHistory()
24 | h.Add("echo 1")
25 | expected := &History{
26 | histories: []string{"echo 1"},
27 | tmp: []string{"echo 1", ""},
28 | selected: 1,
29 | }
30 | if !reflect.DeepEqual(h, expected) {
31 | t.Errorf("Should be %v, but got %v", expected, h)
32 | }
33 | }
34 |
35 | func TestHistoryOlder(t *testing.T) {
36 | h := NewHistory()
37 | h.Add("echo 1")
38 |
39 | // Prepare buffer
40 | buf := NewBuffer()
41 | buf.InsertText("echo 2", false, true)
42 |
43 | // [1 time] Call Older function
44 | buf1, changed := h.Older(buf)
45 | if !changed {
46 | t.Error("Should be changed history but not changed.")
47 | }
48 | if buf1.Text() != "echo 1" {
49 | t.Errorf("Should be %#v, but got %#v", "echo 1", buf1.Text())
50 | }
51 |
52 | // [2 times] Call Older function
53 | buf = NewBuffer()
54 | buf.InsertText("echo 1", false, true)
55 | buf2, changed := h.Older(buf)
56 | if changed {
57 | t.Error("Should be not changed history but changed.")
58 | }
59 | if !reflect.DeepEqual("echo 1", buf2.Text()) {
60 | t.Errorf("Should be %#v, but got %#v", "echo 1", buf2.Text())
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/output_vt100_test.go:
--------------------------------------------------------------------------------
1 | package prompt
2 |
3 | import (
4 | "bytes"
5 | "testing"
6 | )
7 |
8 | func TestVT100WriterWrite(t *testing.T) {
9 | scenarioTable := []struct {
10 | input []byte
11 | expected []byte
12 | }{
13 | {
14 | input: []byte{0x1b},
15 | expected: []byte{'?'},
16 | },
17 | {
18 | input: []byte{'a'},
19 | expected: []byte{'a'},
20 | },
21 | }
22 |
23 | for _, s := range scenarioTable {
24 | pw := &VT100Writer{}
25 | pw.Write(s.input)
26 |
27 | if !bytes.Equal(pw.buffer, s.expected) {
28 | t.Errorf("Should be %+#v, but got %+#v", pw.buffer, s.expected)
29 | }
30 | }
31 | }
32 |
33 | func TestVT100WriterWriteStr(t *testing.T) {
34 | scenarioTable := []struct {
35 | input string
36 | expected []byte
37 | }{
38 | {
39 | input: "\x1b",
40 | expected: []byte{'?'},
41 | },
42 | {
43 | input: "a",
44 | expected: []byte{'a'},
45 | },
46 | }
47 |
48 | for _, s := range scenarioTable {
49 | pw := &VT100Writer{}
50 | pw.WriteStr(s.input)
51 |
52 | if !bytes.Equal(pw.buffer, s.expected) {
53 | t.Errorf("Should be %+#v, but got %+#v", pw.buffer, s.expected)
54 | }
55 | }
56 | }
57 |
58 | func TestVT100WriterWriteRawStr(t *testing.T) {
59 | scenarioTable := []struct {
60 | input string
61 | expected []byte
62 | }{
63 | {
64 | input: "\x1b",
65 | expected: []byte{0x1b},
66 | },
67 | {
68 | input: "a",
69 | expected: []byte{'a'},
70 | },
71 | }
72 |
73 | for _, s := range scenarioTable {
74 | pw := &VT100Writer{}
75 | pw.WriteRawStr(s.input)
76 |
77 | if !bytes.Equal(pw.buffer, s.expected) {
78 | t.Errorf("Should be %+#v, but got %+#v", pw.buffer, s.expected)
79 | }
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/history.go:
--------------------------------------------------------------------------------
1 | package prompt
2 |
3 | // History stores the texts that are entered.
4 | type History struct {
5 | histories []string
6 | tmp []string
7 | selected int
8 | }
9 |
10 | // Add to add text in history.
11 | func (h *History) Add(input string) {
12 | h.histories = append(h.histories, input)
13 | h.Clear()
14 | }
15 |
16 | // Clear to clear the history.
17 | func (h *History) Clear() {
18 | h.tmp = make([]string, len(h.histories))
19 | for i := range h.histories {
20 | h.tmp[i] = h.histories[i]
21 | }
22 | h.tmp = append(h.tmp, "")
23 | h.selected = len(h.tmp) - 1
24 | }
25 |
26 | // Older saves a buffer of current line and get a buffer of previous line by up-arrow.
27 | // The changes of line buffers are stored until new history is created.
28 | func (h *History) Older(buf *Buffer) (new *Buffer, changed bool) {
29 | if len(h.tmp) == 1 || h.selected == 0 {
30 | return buf, false
31 | }
32 | h.tmp[h.selected] = buf.Text()
33 |
34 | h.selected--
35 | new = NewBuffer()
36 | new.InsertText(h.tmp[h.selected], false, true)
37 | return new, true
38 | }
39 |
40 | // Newer saves a buffer of current line and get a buffer of next line by up-arrow.
41 | // The changes of line buffers are stored until new history is created.
42 | func (h *History) Newer(buf *Buffer) (new *Buffer, changed bool) {
43 | if h.selected >= len(h.tmp)-1 {
44 | return buf, false
45 | }
46 | h.tmp[h.selected] = buf.Text()
47 |
48 | h.selected++
49 | new = NewBuffer()
50 | new.InsertText(h.tmp[h.selected], false, true)
51 | return new, true
52 | }
53 |
54 | // NewHistory returns new history object.
55 | func NewHistory() *History {
56 | return &History{
57 | histories: []string{},
58 | tmp: []string{""},
59 | selected: 0,
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/_tools/sigwinch/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "os"
6 | "os/signal"
7 | "syscall"
8 | "unsafe"
9 | )
10 |
11 | // Winsize is winsize struct got from the ioctl(2) system call.
12 | type Winsize struct {
13 | Row uint16
14 | Col uint16
15 | X uint16 // pixel value
16 | Y uint16 // pixel value
17 | }
18 |
19 | // GetWinSize returns winsize struct which is the response of ioctl(2).
20 | func GetWinSize(fd int) *Winsize {
21 | ws := &Winsize{}
22 | retCode, _, errno := syscall.Syscall(
23 | syscall.SYS_IOCTL,
24 | uintptr(fd),
25 | uintptr(syscall.TIOCGWINSZ),
26 | uintptr(unsafe.Pointer(ws)))
27 |
28 | if int(retCode) == -1 {
29 | panic(errno)
30 | }
31 | return ws
32 | }
33 |
34 | func main() {
35 | signalChan := make(chan os.Signal, 1)
36 | signal.Notify(
37 | signalChan,
38 | syscall.SIGHUP,
39 | syscall.SIGINT,
40 | syscall.SIGTERM,
41 | syscall.SIGQUIT,
42 | syscall.SIGWINCH,
43 | )
44 | ws := GetWinSize(syscall.Stdin)
45 | fmt.Printf("Row %d : Col %d\n", ws.Row, ws.Col)
46 |
47 | exitChan := make(chan int)
48 | go func() {
49 | for {
50 | s := <-signalChan
51 | switch s {
52 | // kill -SIGHUP XXXX
53 | case syscall.SIGHUP:
54 | exitChan <- 0
55 |
56 | // kill -SIGINT XXXX or Ctrl+c
57 | case syscall.SIGINT:
58 | exitChan <- 0
59 |
60 | // kill -SIGTERM XXXX
61 | case syscall.SIGTERM:
62 | exitChan <- 0
63 |
64 | // kill -SIGQUIT XXXX
65 | case syscall.SIGQUIT:
66 | exitChan <- 0
67 |
68 | case syscall.SIGWINCH:
69 | ws := GetWinSize(syscall.Stdin)
70 | fmt.Printf("Row %d : Col %d\n", ws.Row, ws.Col)
71 |
72 | default:
73 | exitChan <- 1
74 | }
75 | }
76 | }()
77 |
78 | code := <-exitChan
79 | os.Exit(code)
80 | }
81 |
--------------------------------------------------------------------------------
/output_posix.go:
--------------------------------------------------------------------------------
1 | // +build !windows
2 |
3 | package prompt
4 |
5 | import (
6 | "syscall"
7 | )
8 |
9 | const flushMaxRetryCount = 3
10 |
11 | // PosixWriter is a ConsoleWriter implementation for POSIX environment.
12 | // To control terminal emulator, this outputs VT100 escape sequences.
13 | type PosixWriter struct {
14 | VT100Writer
15 | fd int
16 | }
17 |
18 | // Flush to flush buffer
19 | func (w *PosixWriter) Flush() error {
20 | l := len(w.buffer)
21 | offset := 0
22 | retry := 0
23 | for {
24 | n, err := syscall.Write(w.fd, w.buffer[offset:])
25 | if err != nil {
26 | if retry < flushMaxRetryCount {
27 | retry++
28 | continue
29 | }
30 | return err
31 | }
32 | offset += n
33 | if offset == l {
34 | break
35 | }
36 | }
37 | w.buffer = []byte{}
38 | return nil
39 | }
40 |
41 | var _ ConsoleWriter = &PosixWriter{}
42 |
43 | var (
44 | // NewStandardOutputWriter returns ConsoleWriter object to write to stdout.
45 | // This generates VT100 escape sequences because almost terminal emulators
46 | // in POSIX OS built on top of a VT100 specification.
47 | // Deprecated: Please use NewStdoutWriter
48 | NewStandardOutputWriter = NewStdoutWriter
49 | )
50 |
51 | // NewStdoutWriter returns ConsoleWriter object to write to stdout.
52 | // This generates VT100 escape sequences because almost terminal emulators
53 | // in POSIX OS built on top of a VT100 specification.
54 | func NewStdoutWriter() ConsoleWriter {
55 | return &PosixWriter{
56 | fd: syscall.Stdout,
57 | }
58 | }
59 |
60 | // NewStderrWriter returns ConsoleWriter object to write to stderr.
61 | // This generates VT100 escape sequences because almost terminal emulators
62 | // in POSIX OS built on top of a VT100 specification.
63 | func NewStderrWriter() ConsoleWriter {
64 | return &PosixWriter{
65 | fd: syscall.Stderr,
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/.github/workflows/test.yml:
--------------------------------------------------------------------------------
1 | name: tests
2 | on:
3 | pull_request:
4 | branches:
5 | - master
6 |
7 | jobs:
8 | test:
9 | name: Run tests
10 | runs-on: ${{ matrix.os }}
11 | strategy:
12 | matrix:
13 | os: [ubuntu-latest, windows-latest, macos-latest]
14 | steps:
15 | - name: Set up Go 1.16
16 | uses: actions/setup-go@v2
17 | with:
18 | go-version: 1.16
19 | id: go
20 | - name: Check out code into the Go module directory
21 | uses: actions/checkout@master
22 | - name: Running go tests
23 | env:
24 | GO111MODULE: on
25 | run: make test
26 |
27 | examples:
28 | name: Build examples
29 | runs-on: ubuntu-latest
30 | steps:
31 | - name: Set up Go 1.16
32 | uses: actions/setup-go@v2
33 | with:
34 | go-version: 1.16
35 | id: go
36 | - name: Check out code into the Go module directory
37 | uses: actions/checkout@master
38 | - name: Building go examples
39 | env:
40 | GO111MODULE: on
41 | run: ./_example/build.sh
42 |
43 | lint:
44 | name: Run lint checks
45 | runs-on: ubuntu-latest
46 | steps:
47 | - name: Set up Go 1.16
48 | uses: actions/setup-go@v2
49 | with:
50 | go-version: 1.16
51 | id: go
52 | - name: Check out code into the Go module directory
53 | uses: actions/checkout@master
54 | - name: Download golangci-lint
55 | run: |
56 | wget https://github.com/golangci/golangci-lint/releases/download/v1.31.0/golangci-lint-1.31.0-linux-amd64.tar.gz
57 | tar -xvf ./golangci-lint-1.31.0-linux-amd64.tar.gz
58 | - name: Running golangci-lint
59 | env:
60 | GO111MODULE: on
61 | GOPATH: /home/runner/work/
62 | run: GOCILINT=./golangci-lint-1.31.0-linux-amd64/golangci-lint make lint
63 |
--------------------------------------------------------------------------------
/input_posix.go:
--------------------------------------------------------------------------------
1 | // +build !windows
2 |
3 | package prompt
4 |
5 | import (
6 | "syscall"
7 |
8 | "github.com/c-bata/go-prompt/internal/term"
9 | "golang.org/x/sys/unix"
10 | )
11 |
12 | const maxReadBytes = 1024
13 |
14 | // PosixParser is a ConsoleParser implementation for POSIX environment.
15 | type PosixParser struct {
16 | fd int
17 | origTermios syscall.Termios
18 | }
19 |
20 | // Setup should be called before starting input
21 | func (t *PosixParser) Setup() error {
22 | // Set NonBlocking mode because if syscall.Read block this goroutine, it cannot receive data from stopCh.
23 | if err := syscall.SetNonblock(t.fd, true); err != nil {
24 | return err
25 | }
26 | if err := term.SetRaw(t.fd); err != nil {
27 | return err
28 | }
29 | return nil
30 | }
31 |
32 | // TearDown should be called after stopping input
33 | func (t *PosixParser) TearDown() error {
34 | if err := syscall.SetNonblock(t.fd, false); err != nil {
35 | return err
36 | }
37 | if err := term.Restore(); err != nil {
38 | return err
39 | }
40 | return nil
41 | }
42 |
43 | // Read returns byte array.
44 | func (t *PosixParser) Read() ([]byte, error) {
45 | buf := make([]byte, maxReadBytes)
46 | n, err := syscall.Read(t.fd, buf)
47 | if err != nil {
48 | return []byte{}, err
49 | }
50 | return buf[:n], nil
51 | }
52 |
53 | // GetWinSize returns WinSize object to represent width and height of terminal.
54 | func (t *PosixParser) GetWinSize() *WinSize {
55 | ws, err := unix.IoctlGetWinsize(t.fd, unix.TIOCGWINSZ)
56 | if err != nil {
57 | panic(err)
58 | }
59 | return &WinSize{
60 | Row: ws.Row,
61 | Col: ws.Col,
62 | }
63 | }
64 |
65 | var _ ConsoleParser = &PosixParser{}
66 |
67 | // NewStandardInputParser returns ConsoleParser object to read from stdin.
68 | func NewStandardInputParser() *PosixParser {
69 | in, err := syscall.Open("/dev/tty", syscall.O_RDONLY, 0)
70 | if err != nil {
71 | panic(err)
72 | }
73 |
74 | return &PosixParser{
75 | fd: in,
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/input_windows.go:
--------------------------------------------------------------------------------
1 | // +build windows
2 |
3 | package prompt
4 |
5 | import (
6 | "errors"
7 | "syscall"
8 | "unicode/utf8"
9 | "unsafe"
10 |
11 | tty "github.com/mattn/go-tty"
12 | )
13 |
14 | const maxReadBytes = 1024
15 |
16 | var kernel32 = syscall.NewLazyDLL("kernel32.dll")
17 |
18 | var procGetNumberOfConsoleInputEvents = kernel32.NewProc("GetNumberOfConsoleInputEvents")
19 |
20 | // WindowsParser is a ConsoleParser implementation for Win32 console.
21 | type WindowsParser struct {
22 | tty *tty.TTY
23 | }
24 |
25 | // Setup should be called before starting input
26 | func (p *WindowsParser) Setup() error {
27 | t, err := tty.Open()
28 | if err != nil {
29 | return err
30 | }
31 | p.tty = t
32 | return nil
33 | }
34 |
35 | // TearDown should be called after stopping input
36 | func (p *WindowsParser) TearDown() error {
37 | return p.tty.Close()
38 | }
39 |
40 | // Read returns byte array.
41 | func (p *WindowsParser) Read() ([]byte, error) {
42 | var ev uint32
43 | r0, _, err := procGetNumberOfConsoleInputEvents.Call(p.tty.Input().Fd(), uintptr(unsafe.Pointer(&ev)))
44 | if r0 == 0 {
45 | return nil, err
46 | }
47 | if ev == 0 {
48 | return nil, errors.New("EAGAIN")
49 | }
50 |
51 | r, err := p.tty.ReadRune()
52 | if err != nil {
53 | return nil, err
54 | }
55 |
56 | buf := make([]byte, maxReadBytes)
57 | n := utf8.EncodeRune(buf[:], r)
58 | for p.tty.Buffered() && n < maxReadBytes {
59 | r, err := p.tty.ReadRune()
60 | if err != nil {
61 | break
62 | }
63 | n += utf8.EncodeRune(buf[n:], r)
64 | }
65 | return buf[:n], nil
66 | }
67 |
68 | // GetWinSize returns WinSize object to represent width and height of terminal.
69 | func (p *WindowsParser) GetWinSize() *WinSize {
70 | w, h, err := p.tty.Size()
71 | if err != nil {
72 | panic(err)
73 | }
74 | return &WinSize{
75 | Row: uint16(h),
76 | Col: uint16(w),
77 | }
78 | }
79 |
80 | // NewStandardInputParser returns ConsoleParser object to read from stdin.
81 | func NewStandardInputParser() *WindowsParser {
82 | return &WindowsParser{}
83 | }
84 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
2 | github.com/mattn/go-colorable v0.1.7 h1:bQGKb3vps/j0E9GfJQ03JyhRuxsvdAanXlT9BTw3mdw=
3 | github.com/mattn/go-colorable v0.1.7/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
4 | github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
5 | github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84=
6 | github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
7 | github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
8 | github.com/mattn/go-runewidth v0.0.6/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
9 | github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0=
10 | github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
11 | github.com/mattn/go-tty v0.0.3 h1:5OfyWorkyO7xP52Mq7tB36ajHDG5OHrmBGIS/DtakQI=
12 | github.com/mattn/go-tty v0.0.3/go.mod h1:ihxohKRERHTVzN+aSVRwACLCeqIoZAWpoICkkvrWyR0=
13 | github.com/pkg/term v1.2.0-beta.2 h1:L3y/h2jkuBVFdWiJvNfYfKmzcCnILw7mJWm2JQuMppw=
14 | github.com/pkg/term v1.2.0-beta.2/go.mod h1:E25nymQcrSllhX42Ok8MRm1+hyBdHY0dCeiKZ9jpNGw=
15 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
16 | golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
17 | golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
18 | golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
19 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
20 | golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
21 | golang.org/x/sys v0.0.0-20200909081042-eff7692f9009/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
22 | golang.org/x/sys v0.0.0-20200918174421-af09f7315aff h1:1CPUrky56AcgSpxz/KfgzQWzfG09u5YOL8MvPYBlrL8=
23 | golang.org/x/sys v0.0.0-20200918174421-af09f7315aff/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
24 |
--------------------------------------------------------------------------------
/filter.go:
--------------------------------------------------------------------------------
1 | package prompt
2 |
3 | import "strings"
4 |
5 | // Filter is the type to filter the prompt.Suggestion array.
6 | type Filter func([]Suggest, string, bool) []Suggest
7 |
8 | // FilterHasPrefix checks whether the string completions.Text begins with sub.
9 | func FilterHasPrefix(completions []Suggest, sub string, ignoreCase bool) []Suggest {
10 | return filterSuggestions(completions, sub, ignoreCase, strings.HasPrefix)
11 | }
12 |
13 | // FilterHasSuffix checks whether the completion.Text ends with sub.
14 | func FilterHasSuffix(completions []Suggest, sub string, ignoreCase bool) []Suggest {
15 | return filterSuggestions(completions, sub, ignoreCase, strings.HasSuffix)
16 | }
17 |
18 | // FilterContains checks whether the completion.Text contains sub.
19 | func FilterContains(completions []Suggest, sub string, ignoreCase bool) []Suggest {
20 | return filterSuggestions(completions, sub, ignoreCase, strings.Contains)
21 | }
22 |
23 | // FilterFuzzy checks whether the completion.Text fuzzy matches sub.
24 | // Fuzzy searching for "dog" is equivalent to "*d*o*g*". This search term
25 | // would match, for example, "Good food is gone"
26 | // ^ ^ ^
27 | func FilterFuzzy(completions []Suggest, sub string, ignoreCase bool) []Suggest {
28 | return filterSuggestions(completions, sub, ignoreCase, fuzzyMatch)
29 | }
30 |
31 | func fuzzyMatch(s, sub string) bool {
32 | sChars := []rune(s)
33 | sIdx := 0
34 |
35 | // https://staticcheck.io/docs/checks#S1029
36 | for _, c := range sub {
37 | found := false
38 | for ; sIdx < len(sChars); sIdx++ {
39 | if sChars[sIdx] == c {
40 | found = true
41 | sIdx++
42 | break
43 | }
44 | }
45 | if !found {
46 | return false
47 | }
48 | }
49 | return true
50 | }
51 |
52 | func filterSuggestions(suggestions []Suggest, sub string, ignoreCase bool, function func(string, string) bool) []Suggest {
53 | if sub == "" {
54 | return suggestions
55 | }
56 | if ignoreCase {
57 | sub = strings.ToUpper(sub)
58 | }
59 |
60 | ret := make([]Suggest, 0, len(suggestions))
61 | for i := range suggestions {
62 | c := suggestions[i].Text
63 | if ignoreCase {
64 | c = strings.ToUpper(c)
65 | }
66 | if function(c, sub) {
67 | ret = append(ret, suggestions[i])
68 | }
69 | }
70 | return ret
71 | }
72 |
--------------------------------------------------------------------------------
/key.go:
--------------------------------------------------------------------------------
1 | // Code generated by hand; DO NOT EDIT.
2 | // This is a little bit stupid, but there are many public constants which is no value for writing godoc comment.
3 |
4 | package prompt
5 |
6 | // Key is the type express the key inserted from user.
7 | //go:generate stringer -type=Key
8 | type Key int
9 |
10 | // ASCIICode is the type contains Key and it's ascii byte array.
11 | type ASCIICode struct {
12 | Key Key
13 | ASCIICode []byte
14 | }
15 |
16 | const (
17 | Escape Key = iota
18 |
19 | ControlA
20 | ControlB
21 | ControlC
22 | ControlD
23 | ControlE
24 | ControlF
25 | ControlG
26 | ControlH
27 | ControlI
28 | ControlJ
29 | ControlK
30 | ControlL
31 | ControlM
32 | ControlN
33 | ControlO
34 | ControlP
35 | ControlQ
36 | ControlR
37 | ControlS
38 | ControlT
39 | ControlU
40 | ControlV
41 | ControlW
42 | ControlX
43 | ControlY
44 | ControlZ
45 |
46 | ControlSpace
47 | ControlBackslash
48 | ControlSquareClose
49 | ControlCircumflex
50 | ControlUnderscore
51 | ControlLeft
52 | ControlRight
53 | ControlUp
54 | ControlDown
55 |
56 | Up
57 | Down
58 | Right
59 | Left
60 |
61 | ShiftLeft
62 | ShiftUp
63 | ShiftDown
64 | ShiftRight
65 |
66 | Home
67 | End
68 | Delete
69 | ShiftDelete
70 | ControlDelete
71 | PageUp
72 | PageDown
73 | BackTab
74 | Insert
75 | Backspace
76 |
77 | // Aliases.
78 | Tab
79 | Enter
80 | // Actually Enter equals ControlM, not ControlJ,
81 | // However, in prompt_toolkit, we made the mistake of translating
82 | // \r into \n during the input, so everyone is now handling the
83 | // enter key by binding ControlJ.
84 |
85 | // From now on, it's better to bind `ASCII_SEQUENCES.Enter` everywhere,
86 | // because that's future compatible, and will still work when we
87 | // stop replacing \r by \n.
88 |
89 | F1
90 | F2
91 | F3
92 | F4
93 | F5
94 | F6
95 | F7
96 | F8
97 | F9
98 | F10
99 | F11
100 | F12
101 | F13
102 | F14
103 | F15
104 | F16
105 | F17
106 | F18
107 | F19
108 | F20
109 | F21
110 | F22
111 | F23
112 | F24
113 |
114 | // Matches any key.
115 | Any
116 |
117 | // Special
118 | CPRResponse
119 | Vt100MouseEvent
120 | WindowsMouseEvent
121 | BracketedPaste
122 |
123 | // Key which is ignored. (The key binding for this key should not do anything.)
124 | Ignore
125 |
126 | // Key is not defined
127 | NotDefined
128 | )
129 |
--------------------------------------------------------------------------------
/internal/strings/strings.go:
--------------------------------------------------------------------------------
1 | package strings
2 |
3 | import "unicode/utf8"
4 |
5 | // IndexNotByte is similar with strings.IndexByte but showing the opposite behavior.
6 | func IndexNotByte(s string, c byte) int {
7 | n := len(s)
8 | for i := 0; i < n; i++ {
9 | if s[i] != c {
10 | return i
11 | }
12 | }
13 | return -1
14 | }
15 |
16 | // LastIndexNotByte is similar with strings.LastIndexByte but showing the opposite behavior.
17 | func LastIndexNotByte(s string, c byte) int {
18 | for i := len(s) - 1; i >= 0; i-- {
19 | if s[i] != c {
20 | return i
21 | }
22 | }
23 | return -1
24 | }
25 |
26 | type asciiSet [8]uint32
27 |
28 | func (as *asciiSet) notContains(c byte) bool {
29 | return (as[c>>5] & (1 << uint(c&31))) == 0
30 | }
31 |
32 | func makeASCIISet(chars string) (as asciiSet, ok bool) {
33 | for i := 0; i < len(chars); i++ {
34 | c := chars[i]
35 | if c >= utf8.RuneSelf {
36 | return as, false
37 | }
38 | as[c>>5] |= 1 << uint(c&31)
39 | }
40 | return as, true
41 | }
42 |
43 | // IndexNotAny is similar with strings.IndexAny but showing the opposite behavior.
44 | func IndexNotAny(s, chars string) int {
45 | if len(chars) > 0 {
46 | if len(s) > 8 {
47 | if as, isASCII := makeASCIISet(chars); isASCII {
48 | for i := 0; i < len(s); i++ {
49 | if as.notContains(s[i]) {
50 | return i
51 | }
52 | }
53 | return -1
54 | }
55 | }
56 |
57 | LabelFirstLoop:
58 | for i, c := range s {
59 | for j, m := range chars {
60 | if c != m && j == len(chars)-1 {
61 | return i
62 | } else if c != m {
63 | continue
64 | } else {
65 | continue LabelFirstLoop
66 | }
67 | }
68 | }
69 | }
70 | return -1
71 | }
72 |
73 | // LastIndexNotAny is similar with strings.LastIndexAny but showing the opposite behavior.
74 | func LastIndexNotAny(s, chars string) int {
75 | if len(chars) > 0 {
76 | if len(s) > 8 {
77 | if as, isASCII := makeASCIISet(chars); isASCII {
78 | for i := len(s) - 1; i >= 0; i-- {
79 | if as.notContains(s[i]) {
80 | return i
81 | }
82 | }
83 | return -1
84 | }
85 | }
86 | LabelFirstLoop:
87 | for i := len(s); i > 0; {
88 | r, size := utf8.DecodeLastRuneInString(s[:i])
89 | i -= size
90 | for j, m := range chars {
91 | if r != m && j == len(chars)-1 {
92 | return i
93 | } else if r != m {
94 | continue
95 | } else {
96 | continue LabelFirstLoop
97 | }
98 | }
99 | }
100 | }
101 | return -1
102 | }
103 |
--------------------------------------------------------------------------------
/completer/file.go:
--------------------------------------------------------------------------------
1 | package completer
2 |
3 | import (
4 | "io/ioutil"
5 | "os"
6 | "os/user"
7 | "path/filepath"
8 | "runtime"
9 |
10 | prompt "github.com/c-bata/go-prompt"
11 | "github.com/c-bata/go-prompt/internal/debug"
12 | )
13 |
14 | var (
15 | // FilePathCompletionSeparator holds separate characters.
16 | FilePathCompletionSeparator = string([]byte{' ', os.PathSeparator})
17 | )
18 |
19 | // FilePathCompleter is a completer for your local file system.
20 | // Please caution that you need to set OptionCompletionWordSeparator(completer.FilePathCompletionSeparator)
21 | // when you use this completer.
22 | type FilePathCompleter struct {
23 | Filter func(fi os.FileInfo) bool
24 | IgnoreCase bool
25 | fileListCache map[string][]prompt.Suggest
26 | }
27 |
28 | func cleanFilePath(path string) (dir, base string, err error) {
29 | if path == "" {
30 | return ".", "", nil
31 | }
32 |
33 | var endsWithSeparator bool
34 | if len(path) >= 1 && path[len(path)-1] == os.PathSeparator {
35 | endsWithSeparator = true
36 | }
37 |
38 | if runtime.GOOS != "windows" && len(path) >= 2 && path[0:2] == "~/" {
39 | me, err := user.Current()
40 | if err != nil {
41 | return "", "", err
42 | }
43 | path = filepath.Join(me.HomeDir, path[1:])
44 | }
45 | path = filepath.Clean(os.ExpandEnv(path))
46 | dir = filepath.Dir(path)
47 | base = filepath.Base(path)
48 |
49 | if endsWithSeparator {
50 | dir = path + string(os.PathSeparator) // Append slash(in POSIX) if path ends with slash.
51 | base = "" // Set empty string if path ends with separator.
52 | }
53 | return dir, base, nil
54 | }
55 |
56 | // Complete returns suggestions from your local file system.
57 | func (c *FilePathCompleter) Complete(d prompt.Document) []prompt.Suggest {
58 | if c.fileListCache == nil {
59 | c.fileListCache = make(map[string][]prompt.Suggest, 4)
60 | }
61 |
62 | path := d.GetWordBeforeCursor()
63 | dir, base, err := cleanFilePath(path)
64 | if err != nil {
65 | debug.Log("completer: cannot get current user:" + err.Error())
66 | return nil
67 | }
68 |
69 | if cached, ok := c.fileListCache[dir]; ok {
70 | return prompt.FilterHasPrefix(cached, base, c.IgnoreCase)
71 | }
72 |
73 | files, err := ioutil.ReadDir(dir)
74 | if err != nil && os.IsNotExist(err) {
75 | return nil
76 | } else if err != nil {
77 | debug.Log("completer: cannot read directory items:" + err.Error())
78 | return nil
79 | }
80 |
81 | suggests := make([]prompt.Suggest, 0, len(files))
82 | for _, f := range files {
83 | if c.Filter != nil && !c.Filter(f) {
84 | continue
85 | }
86 | suggests = append(suggests, prompt.Suggest{Text: f.Name()})
87 | }
88 | c.fileListCache[dir] = suggests
89 | return prompt.FilterHasPrefix(suggests, base, c.IgnoreCase)
90 | }
91 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Change Log
2 |
3 | ## v0.3.0 (2018/??/??)
4 |
5 | next release.
6 |
7 | ## v0.2.3 (2018/10/25)
8 |
9 | ### What's new?
10 |
11 | * Add `prompt.FuzzyFilter` for fuzzy matching at [#92](https://github.com/c-bata/go-prompt/pull/92).
12 | * Add `OptionShowCompletionAtStart` to show completion at start at [#100](https://github.com/c-bata/go-prompt/pull/100).
13 | * Add `prompt.NewStderrWriter` at [#102](https://github.com/c-bata/go-prompt/pull/102).
14 |
15 | ### Fixed
16 |
17 | * Fix resetting display attributes (please see [pull #104](https://github.com/c-bata/go-prompt/pull/104) for more details).
18 | * Fix error handling of Flush function in ConsoleWriter (please see [pull #97](https://github.com/c-bata/go-prompt/pull/97) for more details).
19 | * Fix panic problem when reading from stdin before starting the prompt (please see [issue #88](https://github.com/c-bata/go-prompt/issues/88) for more details).
20 |
21 | ### Removed or Deprecated
22 |
23 | * `prompt.NewStandardOutputWriter` is deprecated. Please use `prompt.NewStdoutWriter`.
24 |
25 | ## v0.2.2 (2018/06/28)
26 |
27 | ### What's new?
28 |
29 | * Support CJK(Chinese, Japanese and Korean) and Cyrillic characters.
30 | * Add OptionCompletionWordSeparator(x string) to customize insertion points for completions.
31 | * To support this, text query functions by arbitrary word separator are added in Document (please see [here](https://github.com/c-bata/go-prompt/pull/79) for more details).
32 | * Add FilePathCompleter to complete file path on your system.
33 | * Add option to customize ascii code key bindings.
34 | * Add GetWordAfterCursor method in Document.
35 |
36 | ### Removed or Deprecated
37 |
38 | * prompt.Choose shortcut function is deprecated.
39 |
40 | ## v0.2.1 (2018/02/14)
41 |
42 | ### What's New?
43 |
44 | * ~~It seems that windows support is almost perfect.~~
45 | * A critical bug is found :( When you change a terminal window size, the layout will be broken because current implementation cannot catch signal for updating window size on Windows.
46 |
47 | ### Fixed
48 |
49 | * Fix a Shift+Tab handling on Windows.
50 | * Fix 4-dimension arrow keys handling on Windows.
51 |
52 | ## v0.2.0 (2018/02/13)
53 |
54 | ### What's New?
55 |
56 | * Supports scrollbar when there are too many matched suggestions
57 | * Windows support (but please caution because this is still not perfect).
58 | * Add OptionLivePrefix to update the prefix dynamically
59 | * Implement clear screen by `Ctrl+L`.
60 |
61 | ### Fixed
62 |
63 | * Fix the behavior of `Ctrl+W` keybind.
64 | * Fix the panic because when running on a docker container (please see [here](https://github.com/c-bata/go-prompt/pull/32) for details).
65 | * Fix panic when making terminal window small size after input 2 lines of texts. See [here](https://github.com/c-bata/go-prompt/issues/37) for details).
66 | * And also fixed many bugs that layout is broken when using Terminal.app, GNU Terminal and a Goland(IntelliJ).
67 |
68 | ### News
69 |
70 | New core developers are joined (alphabetical order).
71 |
72 | * Nao Yonashiro (Github @orisano)
73 | * Ryoma Abe (Github @Allajah)
74 | * Yusuke Nakamura (Github @unasuke)
75 |
76 |
77 | ## v0.1.0 (2017/08/15)
78 |
79 | Initial Release
80 |
--------------------------------------------------------------------------------
/emacs.go:
--------------------------------------------------------------------------------
1 | package prompt
2 |
3 | import "github.com/c-bata/go-prompt/internal/debug"
4 |
5 | /*
6 |
7 | ========
8 | PROGRESS
9 | ========
10 |
11 | Moving the cursor
12 | -----------------
13 |
14 | * [x] Ctrl + a Go to the beginning of the line (Home)
15 | * [x] Ctrl + e Go to the End of the line (End)
16 | * [x] Ctrl + p Previous command (Up arrow)
17 | * [x] Ctrl + n Next command (Down arrow)
18 | * [x] Ctrl + f Forward one character
19 | * [x] Ctrl + b Backward one character
20 | * [x] Ctrl + xx Toggle between the start of line and current cursor position
21 |
22 | Editing
23 | -------
24 |
25 | * [x] Ctrl + L Clear the Screen, similar to the clear command
26 | * [x] Ctrl + d Delete character under the cursor
27 | * [x] Ctrl + h Delete character before the cursor (Backspace)
28 |
29 | * [x] Ctrl + w Cut the Word before the cursor to the clipboard.
30 | * [x] Ctrl + k Cut the Line after the cursor to the clipboard.
31 | * [x] Ctrl + u Cut/delete the Line before the cursor to the clipboard.
32 |
33 | * [ ] Ctrl + t Swap the last two characters before the cursor (typo).
34 | * [ ] Esc + t Swap the last two words before the cursor.
35 |
36 | * [ ] ctrl + y Paste the last thing to be cut (yank)
37 | * [ ] ctrl + _ Undo
38 |
39 | */
40 |
41 | var emacsKeyBindings = []KeyBind{
42 | // Go to the End of the line
43 | {
44 | Key: ControlE,
45 | Fn: func(buf *Buffer) {
46 | x := []rune(buf.Document().TextAfterCursor())
47 | buf.CursorRight(len(x))
48 | },
49 | },
50 | // Go to the beginning of the line
51 | {
52 | Key: ControlA,
53 | Fn: func(buf *Buffer) {
54 | x := []rune(buf.Document().TextBeforeCursor())
55 | buf.CursorLeft(len(x))
56 | },
57 | },
58 | // Cut the Line after the cursor
59 | {
60 | Key: ControlK,
61 | Fn: func(buf *Buffer) {
62 | x := []rune(buf.Document().TextAfterCursor())
63 | buf.Delete(len(x))
64 | },
65 | },
66 | // Cut/delete the Line before the cursor
67 | {
68 | Key: ControlU,
69 | Fn: func(buf *Buffer) {
70 | x := []rune(buf.Document().TextBeforeCursor())
71 | buf.DeleteBeforeCursor(len(x))
72 | },
73 | },
74 | // Delete character under the cursor
75 | {
76 | Key: ControlD,
77 | Fn: func(buf *Buffer) {
78 | if buf.Text() != "" {
79 | buf.Delete(1)
80 | }
81 | },
82 | },
83 | // Backspace
84 | {
85 | Key: ControlH,
86 | Fn: func(buf *Buffer) {
87 | buf.DeleteBeforeCursor(1)
88 | },
89 | },
90 | // Right allow: Forward one character
91 | {
92 | Key: ControlF,
93 | Fn: func(buf *Buffer) {
94 | buf.CursorRight(1)
95 | },
96 | },
97 | // Left allow: Backward one character
98 | {
99 | Key: ControlB,
100 | Fn: func(buf *Buffer) {
101 | buf.CursorLeft(1)
102 | },
103 | },
104 | // Cut the Word before the cursor.
105 | {
106 | Key: ControlW,
107 | Fn: func(buf *Buffer) {
108 | buf.DeleteBeforeCursor(len([]rune(buf.Document().GetWordBeforeCursorWithSpace())))
109 | },
110 | },
111 | // Clear the Screen, similar to the clear command
112 | {
113 | Key: ControlL,
114 | Fn: func(buf *Buffer) {
115 | consoleWriter.EraseScreen()
116 | consoleWriter.CursorGoTo(0, 0)
117 | debug.AssertNoError(consoleWriter.Flush())
118 | },
119 | },
120 | }
121 |
--------------------------------------------------------------------------------
/render_test.go:
--------------------------------------------------------------------------------
1 | // +build !windows
2 |
3 | package prompt
4 |
5 | import (
6 | "reflect"
7 | "syscall"
8 | "testing"
9 | )
10 |
11 | func TestFormatCompletion(t *testing.T) {
12 | scenarioTable := []struct {
13 | scenario string
14 | completions []Suggest
15 | prefix string
16 | suffix string
17 | expected []Suggest
18 | maxWidth int
19 | expectedWidth int
20 | }{
21 | {
22 | scenario: "",
23 | completions: []Suggest{
24 | {Text: "select"},
25 | {Text: "from"},
26 | {Text: "insert"},
27 | {Text: "where"},
28 | },
29 | prefix: " ",
30 | suffix: " ",
31 | expected: []Suggest{
32 | {Text: " select "},
33 | {Text: " from "},
34 | {Text: " insert "},
35 | {Text: " where "},
36 | },
37 | maxWidth: 20,
38 | expectedWidth: 8,
39 | },
40 | {
41 | scenario: "",
42 | completions: []Suggest{
43 | {Text: "select", Description: "select description"},
44 | {Text: "from", Description: "from description"},
45 | {Text: "insert", Description: "insert description"},
46 | {Text: "where", Description: "where description"},
47 | },
48 | prefix: " ",
49 | suffix: " ",
50 | expected: []Suggest{
51 | {Text: " select ", Description: " select description "},
52 | {Text: " from ", Description: " from description "},
53 | {Text: " insert ", Description: " insert description "},
54 | {Text: " where ", Description: " where description "},
55 | },
56 | maxWidth: 40,
57 | expectedWidth: 28,
58 | },
59 | }
60 |
61 | for _, s := range scenarioTable {
62 | ac, width := formatSuggestions(s.completions, s.maxWidth)
63 | if !reflect.DeepEqual(ac, s.expected) {
64 | t.Errorf("Should be %#v, but got %#v", s.expected, ac)
65 | }
66 | if width != s.expectedWidth {
67 | t.Errorf("Should be %#v, but got %#v", s.expectedWidth, width)
68 | }
69 | }
70 | }
71 |
72 | func TestBreakLineCallback(t *testing.T) {
73 | var i int
74 | r := &Render{
75 | prefix: "> ",
76 | out: &PosixWriter{
77 | fd: syscall.Stdin, // "write" to stdin just so we don't mess with the output of the tests
78 | },
79 | livePrefixCallback: func() (string, bool) { return "", false },
80 | prefixTextColor: Blue,
81 | prefixBGColor: DefaultColor,
82 | inputTextColor: DefaultColor,
83 | inputBGColor: DefaultColor,
84 | previewSuggestionTextColor: Green,
85 | previewSuggestionBGColor: DefaultColor,
86 | suggestionTextColor: White,
87 | suggestionBGColor: Cyan,
88 | selectedSuggestionTextColor: Black,
89 | selectedSuggestionBGColor: Turquoise,
90 | descriptionTextColor: Black,
91 | descriptionBGColor: Turquoise,
92 | selectedDescriptionTextColor: White,
93 | selectedDescriptionBGColor: Cyan,
94 | scrollbarThumbColor: DarkGray,
95 | scrollbarBGColor: Cyan,
96 | col: 1,
97 | }
98 | b := NewBuffer()
99 | r.BreakLine(b)
100 |
101 | if i != 0 {
102 | t.Errorf("i should initially be 0, before applying a break line callback")
103 | }
104 |
105 | r.breakLineCallback = func(doc *Document) {
106 | i++
107 | }
108 | r.BreakLine(b)
109 | r.BreakLine(b)
110 | r.BreakLine(b)
111 |
112 | if i != 3 {
113 | t.Errorf("BreakLine callback not called, i should be 3")
114 | }
115 | }
116 |
--------------------------------------------------------------------------------
/filter_test.go:
--------------------------------------------------------------------------------
1 | package prompt
2 |
3 | import (
4 | "reflect"
5 | "testing"
6 | )
7 |
8 | func TestFilter(t *testing.T) {
9 | var scenarioTable = []struct {
10 | scenario string
11 | filter Filter
12 | list []Suggest
13 | substr string
14 | ignoreCase bool
15 | expected []Suggest
16 | }{
17 | {
18 | scenario: "Contains don't ignore case",
19 | filter: FilterContains,
20 | list: []Suggest{
21 | {Text: "abcde"},
22 | {Text: "fghij"},
23 | {Text: "ABCDE"},
24 | },
25 | substr: "cd",
26 | ignoreCase: false,
27 | expected: []Suggest{
28 | {Text: "abcde"},
29 | },
30 | },
31 | {
32 | scenario: "Contains ignore case",
33 | filter: FilterContains,
34 | list: []Suggest{
35 | {Text: "abcde"},
36 | {Text: "fghij"},
37 | {Text: "ABCDE"},
38 | },
39 | substr: "cd",
40 | ignoreCase: true,
41 | expected: []Suggest{
42 | {Text: "abcde"},
43 | {Text: "ABCDE"},
44 | },
45 | },
46 | {
47 | scenario: "HasPrefix don't ignore case",
48 | filter: FilterHasPrefix,
49 | list: []Suggest{
50 | {Text: "abcde"},
51 | {Text: "fghij"},
52 | {Text: "ABCDE"},
53 | },
54 | substr: "abc",
55 | ignoreCase: false,
56 | expected: []Suggest{
57 | {Text: "abcde"},
58 | },
59 | },
60 | {
61 | scenario: "HasPrefix ignore case",
62 | filter: FilterHasPrefix,
63 | list: []Suggest{
64 | {Text: "abcde"},
65 | {Text: "fabcj"},
66 | {Text: "ABCDE"},
67 | },
68 | substr: "abc",
69 | ignoreCase: true,
70 | expected: []Suggest{
71 | {Text: "abcde"},
72 | {Text: "ABCDE"},
73 | },
74 | },
75 | {
76 | scenario: "HasSuffix don't ignore case",
77 | filter: FilterHasSuffix,
78 | list: []Suggest{
79 | {Text: "abcde"},
80 | {Text: "fcdej"},
81 | {Text: "ABCDE"},
82 | },
83 | substr: "cde",
84 | ignoreCase: false,
85 | expected: []Suggest{
86 | {Text: "abcde"},
87 | },
88 | },
89 | {
90 | scenario: "HasSuffix ignore case",
91 | filter: FilterHasSuffix,
92 | list: []Suggest{
93 | {Text: "abcde"},
94 | {Text: "fcdej"},
95 | {Text: "ABCDE"},
96 | },
97 | substr: "cde",
98 | ignoreCase: true,
99 | expected: []Suggest{
100 | {Text: "abcde"},
101 | {Text: "ABCDE"},
102 | },
103 | },
104 | {
105 | scenario: "Fuzzy don't ignore case",
106 | filter: FilterFuzzy,
107 | list: []Suggest{
108 | {Text: "abcde"},
109 | {Text: "fcdej"},
110 | {Text: "ABCDE"},
111 | },
112 | substr: "ae",
113 | ignoreCase: false,
114 | expected: []Suggest{
115 | {Text: "abcde"},
116 | },
117 | },
118 | {
119 | scenario: "Fuzzy ignore case",
120 | filter: FilterFuzzy,
121 | list: []Suggest{
122 | {Text: "abcde"},
123 | {Text: "fcdej"},
124 | {Text: "ABCDE"},
125 | },
126 | substr: "ae",
127 | ignoreCase: true,
128 | expected: []Suggest{
129 | {Text: "abcde"},
130 | {Text: "ABCDE"},
131 | },
132 | },
133 | }
134 |
135 | for _, s := range scenarioTable {
136 | if actual := s.filter(s.list, s.substr, s.ignoreCase); !reflect.DeepEqual(actual, s.expected) {
137 | t.Errorf("%s: Should be %#v, but got %#v", s.scenario, s.expected, actual)
138 | }
139 | }
140 | }
141 |
142 | func TestFuzzyMatch(t *testing.T) {
143 | tests := []struct {
144 | s string
145 | sub string
146 | match bool
147 | }{
148 | {"dog house", "dog", true},
149 | {"dog house", "", true},
150 | {"", "", true},
151 | {"this is much longer", "hhg", true},
152 | {"this is much longer", "hhhg", false},
153 | {"long", "longer", false},
154 | {"can we do unicode 文字 with this 今日", "文字今日", true},
155 | {"can we do unicode 文字 with this 今日", "d文字tt今日", true},
156 | {"can we do unicode 文字 with this 今日", "d文字ttt今日", false},
157 | }
158 |
159 | for _, test := range tests {
160 | if fuzzyMatch(test.s, test.sub) != test.match {
161 | t.Errorf("fuzzymatch, %s in %s: expected %v, got %v", test.sub, test.s, test.match, !test.match)
162 | }
163 | }
164 | }
165 |
--------------------------------------------------------------------------------
/output.go:
--------------------------------------------------------------------------------
1 | package prompt
2 |
3 | import "sync"
4 |
5 | var (
6 | consoleWriterMu sync.Mutex
7 | consoleWriter ConsoleWriter
8 | )
9 |
10 | func registerConsoleWriter(f ConsoleWriter) {
11 | consoleWriterMu.Lock()
12 | defer consoleWriterMu.Unlock()
13 | consoleWriter = f
14 | }
15 |
16 | // DisplayAttribute represents display attributes like Blinking, Bold, Italic and so on.
17 | type DisplayAttribute int
18 |
19 | const (
20 | // DisplayReset reset all display attributes.
21 | DisplayReset DisplayAttribute = iota
22 | // DisplayBold set bold or increases intensity.
23 | DisplayBold
24 | // DisplayLowIntensity decreases intensity. Not widely supported.
25 | DisplayLowIntensity
26 | // DisplayItalic set italic. Not widely supported.
27 | DisplayItalic
28 | // DisplayUnderline set underline
29 | DisplayUnderline
30 | // DisplayBlink set blink (less than 150 per minute).
31 | DisplayBlink
32 | // DisplayRapidBlink set blink (more than 150 per minute). Not widely supported.
33 | DisplayRapidBlink
34 | // DisplayReverse swap foreground and background colors.
35 | DisplayReverse
36 | // DisplayInvisible set invisible. Not widely supported.
37 | DisplayInvisible
38 | // DisplayCrossedOut set characters legible, but marked for deletion. Not widely supported.
39 | DisplayCrossedOut
40 | // DisplayDefaultFont set primary(default) font
41 | DisplayDefaultFont
42 | )
43 |
44 | // Color represents color on terminal.
45 | type Color int
46 |
47 | const (
48 | // DefaultColor represents a default color.
49 | DefaultColor Color = iota
50 |
51 | // Low intensity
52 |
53 | // Black represents a black.
54 | Black
55 | // DarkRed represents a dark red.
56 | DarkRed
57 | // DarkGreen represents a dark green.
58 | DarkGreen
59 | // Brown represents a brown.
60 | Brown
61 | // DarkBlue represents a dark blue.
62 | DarkBlue
63 | // Purple represents a purple.
64 | Purple
65 | // Cyan represents a cyan.
66 | Cyan
67 | // LightGray represents a light gray.
68 | LightGray
69 |
70 | // High intensity
71 |
72 | // DarkGray represents a dark gray.
73 | DarkGray
74 | // Red represents a red.
75 | Red
76 | // Green represents a green.
77 | Green
78 | // Yellow represents a yellow.
79 | Yellow
80 | // Blue represents a blue.
81 | Blue
82 | // Fuchsia represents a fuchsia.
83 | Fuchsia
84 | // Turquoise represents a turquoise.
85 | Turquoise
86 | // White represents a white.
87 | White
88 | )
89 |
90 | // ConsoleWriter is an interface to abstract output layer.
91 | type ConsoleWriter interface {
92 | /* Write */
93 |
94 | // WriteRaw to write raw byte array.
95 | WriteRaw(data []byte)
96 | // Write to write safety byte array by removing control sequences.
97 | Write(data []byte)
98 | // WriteStr to write raw string.
99 | WriteRawStr(data string)
100 | // WriteStr to write safety string by removing control sequences.
101 | WriteStr(data string)
102 | // Flush to flush buffer.
103 | Flush() error
104 |
105 | /* Erasing */
106 |
107 | // EraseScreen erases the screen with the background colour and moves the cursor to home.
108 | EraseScreen()
109 | // EraseUp erases the screen from the current line up to the top of the screen.
110 | EraseUp()
111 | // EraseDown erases the screen from the current line down to the bottom of the screen.
112 | EraseDown()
113 | // EraseStartOfLine erases from the current cursor position to the start of the current line.
114 | EraseStartOfLine()
115 | // EraseEndOfLine erases from the current cursor position to the end of the current line.
116 | EraseEndOfLine()
117 | // EraseLine erases the entire current line.
118 | EraseLine()
119 |
120 | /* Cursor */
121 |
122 | // ShowCursor stops blinking cursor and show.
123 | ShowCursor()
124 | // HideCursor hides cursor.
125 | HideCursor()
126 | // CursorGoTo sets the cursor position where subsequent text will begin.
127 | CursorGoTo(row, col int)
128 | // CursorUp moves the cursor up by 'n' rows; the default count is 1.
129 | CursorUp(n int)
130 | // CursorDown moves the cursor down by 'n' rows; the default count is 1.
131 | CursorDown(n int)
132 | // CursorForward moves the cursor forward by 'n' columns; the default count is 1.
133 | CursorForward(n int)
134 | // CursorBackward moves the cursor backward by 'n' columns; the default count is 1.
135 | CursorBackward(n int)
136 | // AskForCPR asks for a cursor position report (CPR).
137 | AskForCPR()
138 | // SaveCursor saves current cursor position.
139 | SaveCursor()
140 | // UnSaveCursor restores cursor position after a Save Cursor.
141 | UnSaveCursor()
142 |
143 | /* Scrolling */
144 |
145 | // ScrollDown scrolls display down one line.
146 | ScrollDown()
147 | // ScrollUp scroll display up one line.
148 | ScrollUp()
149 |
150 | /* Title */
151 |
152 | // SetTitle sets a title of terminal window.
153 | SetTitle(title string)
154 | // ClearTitle clears a title of terminal window.
155 | ClearTitle()
156 |
157 | /* Font */
158 |
159 | // SetColor sets text and background colors. and specify whether text is bold.
160 | SetColor(fg, bg Color, bold bool)
161 | }
162 |
--------------------------------------------------------------------------------
/completion.go:
--------------------------------------------------------------------------------
1 | package prompt
2 |
3 | import (
4 | "strings"
5 |
6 | "github.com/c-bata/go-prompt/internal/debug"
7 | runewidth "github.com/mattn/go-runewidth"
8 | )
9 |
10 | const (
11 | shortenSuffix = "..."
12 | leftPrefix = " "
13 | leftSuffix = " "
14 | rightPrefix = " "
15 | rightSuffix = " "
16 | )
17 |
18 | var (
19 | leftMargin = runewidth.StringWidth(leftPrefix + leftSuffix)
20 | rightMargin = runewidth.StringWidth(rightPrefix + rightSuffix)
21 | completionMargin = leftMargin + rightMargin
22 | )
23 |
24 | // Suggest is printed when completing.
25 | type Suggest struct {
26 | Text string
27 | Description string
28 | }
29 |
30 | // CompletionManager manages which suggestion is now selected.
31 | type CompletionManager struct {
32 | selected int // -1 means nothing one is selected.
33 | tmp []Suggest
34 | max uint16
35 | completer Completer
36 |
37 | verticalScroll int
38 | wordSeparator string
39 | showAtStart bool
40 | }
41 |
42 | // GetSelectedSuggestion returns the selected item.
43 | func (c *CompletionManager) GetSelectedSuggestion() (s Suggest, ok bool) {
44 | if c.selected == -1 {
45 | return Suggest{}, false
46 | } else if c.selected < -1 {
47 | debug.Assert(false, "must not reach here")
48 | c.selected = -1
49 | return Suggest{}, false
50 | }
51 | return c.tmp[c.selected], true
52 | }
53 |
54 | // GetSuggestions returns the list of suggestion.
55 | func (c *CompletionManager) GetSuggestions() []Suggest {
56 | return c.tmp
57 | }
58 |
59 | // Reset to select nothing.
60 | func (c *CompletionManager) Reset() {
61 | c.selected = -1
62 | c.verticalScroll = 0
63 | c.Update(*NewDocument())
64 | }
65 |
66 | // Update to update the suggestions.
67 | func (c *CompletionManager) Update(in Document) {
68 | c.tmp = c.completer(in)
69 | }
70 |
71 | // Previous to select the previous suggestion item.
72 | func (c *CompletionManager) Previous() {
73 | if c.verticalScroll == c.selected && c.selected > 0 {
74 | c.verticalScroll--
75 | }
76 | c.selected--
77 | c.update()
78 | }
79 |
80 | // Next to select the next suggestion item.
81 | func (c *CompletionManager) Next() {
82 | if c.verticalScroll+int(c.max)-1 == c.selected {
83 | c.verticalScroll++
84 | }
85 | c.selected++
86 | c.update()
87 | }
88 |
89 | // Completing returns whether the CompletionManager selects something one.
90 | func (c *CompletionManager) Completing() bool {
91 | return c.selected != -1
92 | }
93 |
94 | func (c *CompletionManager) update() {
95 | max := int(c.max)
96 | if len(c.tmp) < max {
97 | max = len(c.tmp)
98 | }
99 |
100 | if c.selected >= len(c.tmp) {
101 | c.Reset()
102 | } else if c.selected < -1 {
103 | c.selected = len(c.tmp) - 1
104 | c.verticalScroll = len(c.tmp) - max
105 | }
106 | }
107 |
108 | func deleteBreakLineCharacters(s string) string {
109 | s = strings.Replace(s, "\n", "", -1)
110 | s = strings.Replace(s, "\r", "", -1)
111 | return s
112 | }
113 |
114 | func formatTexts(o []string, max int, prefix, suffix string) (new []string, width int) {
115 | l := len(o)
116 | n := make([]string, l)
117 |
118 | lenPrefix := runewidth.StringWidth(prefix)
119 | lenSuffix := runewidth.StringWidth(suffix)
120 | lenShorten := runewidth.StringWidth(shortenSuffix)
121 | min := lenPrefix + lenSuffix + lenShorten
122 | for i := 0; i < l; i++ {
123 | o[i] = deleteBreakLineCharacters(o[i])
124 |
125 | w := runewidth.StringWidth(o[i])
126 | if width < w {
127 | width = w
128 | }
129 | }
130 |
131 | if width == 0 {
132 | return n, 0
133 | }
134 | if min >= max {
135 | return n, 0
136 | }
137 | if lenPrefix+width+lenSuffix > max {
138 | width = max - lenPrefix - lenSuffix
139 | }
140 |
141 | for i := 0; i < l; i++ {
142 | x := runewidth.StringWidth(o[i])
143 | if x <= width {
144 | spaces := strings.Repeat(" ", width-x)
145 | n[i] = prefix + o[i] + spaces + suffix
146 | } else if x > width {
147 | x := runewidth.Truncate(o[i], width, shortenSuffix)
148 | // When calling runewidth.Truncate("您好xxx您好xxx", 11, "...") returns "您好xxx..."
149 | // But the length of this result is 10. So we need fill right using runewidth.FillRight.
150 | n[i] = prefix + runewidth.FillRight(x, width) + suffix
151 | }
152 | }
153 | return n, lenPrefix + width + lenSuffix
154 | }
155 |
156 | func formatSuggestions(suggests []Suggest, max int) (new []Suggest, width int) {
157 | num := len(suggests)
158 | new = make([]Suggest, num)
159 |
160 | left := make([]string, num)
161 | for i := 0; i < num; i++ {
162 | left[i] = suggests[i].Text
163 | }
164 | right := make([]string, num)
165 | for i := 0; i < num; i++ {
166 | right[i] = suggests[i].Description
167 | }
168 |
169 | left, leftWidth := formatTexts(left, max, leftPrefix, leftSuffix)
170 | if leftWidth == 0 {
171 | return []Suggest{}, 0
172 | }
173 | right, rightWidth := formatTexts(right, max-leftWidth, rightPrefix, rightSuffix)
174 |
175 | for i := 0; i < num; i++ {
176 | new[i] = Suggest{Text: left[i], Description: right[i]}
177 | }
178 | return new, leftWidth + rightWidth
179 | }
180 |
181 | // NewCompletionManager returns initialized CompletionManager object.
182 | func NewCompletionManager(completer Completer, max uint16) *CompletionManager {
183 | return &CompletionManager{
184 | selected: -1,
185 | max: max,
186 | completer: completer,
187 |
188 | verticalScroll: 0,
189 | }
190 | }
191 |
--------------------------------------------------------------------------------
/buffer_test.go:
--------------------------------------------------------------------------------
1 | package prompt
2 |
3 | import (
4 | "reflect"
5 | "testing"
6 | )
7 |
8 | func TestNewBuffer(t *testing.T) {
9 | b := NewBuffer()
10 | if b.workingIndex != 0 {
11 | t.Errorf("workingIndex should be %#v, got %#v", 0, b.workingIndex)
12 | }
13 | if !reflect.DeepEqual(b.workingLines, []string{""}) {
14 | t.Errorf("workingLines should be %#v, got %#v", []string{""}, b.workingLines)
15 | }
16 | }
17 |
18 | func TestBuffer_InsertText(t *testing.T) {
19 | b := NewBuffer()
20 | b.InsertText("some_text", false, true)
21 |
22 | if b.Text() != "some_text" {
23 | t.Errorf("Text should be %#v, got %#v", "some_text", b.Text())
24 | }
25 |
26 | if b.cursorPosition != len("some_text") {
27 | t.Errorf("cursorPosition should be %#v, got %#v", len("some_text"), b.cursorPosition)
28 | }
29 | }
30 |
31 | func TestBuffer_CursorMovement(t *testing.T) {
32 | b := NewBuffer()
33 | b.InsertText("some_text", false, true)
34 |
35 | b.CursorLeft(1)
36 | b.CursorLeft(2)
37 | b.CursorRight(1)
38 | b.InsertText("A", false, true)
39 | if b.Text() != "some_teAxt" {
40 | t.Errorf("Text should be %#v, got %#v", "some_teAxt", b.Text())
41 | }
42 | if b.cursorPosition != len("some_teA") {
43 | t.Errorf("Text should be %#v, got %#v", len("some_teA"), b.cursorPosition)
44 | }
45 |
46 | // Moving over left character counts.
47 | b.CursorLeft(100)
48 | b.InsertText("A", false, true)
49 | if b.Text() != "Asome_teAxt" {
50 | t.Errorf("Text should be %#v, got %#v", "some_teAxt", b.Text())
51 | }
52 | if b.cursorPosition != len("A") {
53 | t.Errorf("Text should be %#v, got %#v", len("some_teA"), b.cursorPosition)
54 | }
55 |
56 | // TODO: Going right already at right end.
57 | }
58 |
59 | func TestBuffer_CursorMovement_WithMultiByte(t *testing.T) {
60 | b := NewBuffer()
61 | b.InsertText("あいうえお", false, true)
62 | b.CursorLeft(1)
63 | if l := b.Document().TextAfterCursor(); l != "お" {
64 | t.Errorf("Should be 'お', but got %s", l)
65 | }
66 | }
67 |
68 | func TestBuffer_CursorUp(t *testing.T) {
69 | b := NewBuffer()
70 | b.InsertText("long line1\nline2", false, true)
71 | b.CursorUp(1)
72 | if b.Document().cursorPosition != 5 {
73 | t.Errorf("Should be %#v, got %#v", 5, b.Document().cursorPosition)
74 | }
75 |
76 | // Going up when already at the top.
77 | b.CursorUp(1)
78 | if b.Document().cursorPosition != 5 {
79 | t.Errorf("Should be %#v, got %#v", 5, b.Document().cursorPosition)
80 | }
81 |
82 | // Going up to a line that's shorter.
83 | b.setDocument(&Document{})
84 | b.InsertText("line1\nlong line2", false, true)
85 | b.CursorUp(1)
86 | if b.Document().cursorPosition != 5 {
87 | t.Errorf("Should be %#v, got %#v", 5, b.Document().cursorPosition)
88 | }
89 | }
90 |
91 | func TestBuffer_CursorDown(t *testing.T) {
92 | b := NewBuffer()
93 | b.InsertText("line1\nline2", false, true)
94 | b.cursorPosition = 3
95 |
96 | // Normally going down
97 | b.CursorDown(1)
98 | if b.Document().cursorPosition != len("line1\nlin") {
99 | t.Errorf("Should be %#v, got %#v", len("line1\nlin"), b.Document().cursorPosition)
100 | }
101 |
102 | // Going down to a line that's storter.
103 | b = NewBuffer()
104 | b.InsertText("long line1\na\nb", false, true)
105 | b.cursorPosition = 3
106 | b.CursorDown(1)
107 | if b.Document().cursorPosition != len("long line1\na") {
108 | t.Errorf("Should be %#v, got %#v", len("long line1\na"), b.Document().cursorPosition)
109 | }
110 | }
111 |
112 | func TestBuffer_DeleteBeforeCursor(t *testing.T) {
113 | b := NewBuffer()
114 | b.InsertText("some_text", false, true)
115 | b.CursorLeft(2)
116 | deleted := b.DeleteBeforeCursor(1)
117 |
118 | if b.Text() != "some_txt" {
119 | t.Errorf("Should be %#v, got %#v", "some_txt", b.Text())
120 | }
121 | if deleted != "e" {
122 | t.Errorf("Should be %#v, got %#v", deleted, "e")
123 | }
124 | if b.cursorPosition != len("some_t") {
125 | t.Errorf("Should be %#v, got %#v", len("some_t"), b.cursorPosition)
126 | }
127 |
128 | // Delete over the characters length before cursor.
129 | deleted = b.DeleteBeforeCursor(100)
130 | if deleted != "some_t" {
131 | t.Errorf("Should be %#v, got %#v", "some_t", deleted)
132 | }
133 | if b.Text() != "xt" {
134 | t.Errorf("Should be %#v, got %#v", "xt", b.Text())
135 | }
136 |
137 | // If cursor position is a beginning of line, it has no effect.
138 | deleted = b.DeleteBeforeCursor(1)
139 | if deleted != "" {
140 | t.Errorf("Should be empty, got %#v", deleted)
141 | }
142 | }
143 |
144 | func TestBuffer_NewLine(t *testing.T) {
145 | b := NewBuffer()
146 | b.InsertText(" hello", false, true)
147 | b.NewLine(false)
148 | ac := b.Text()
149 | ex := " hello\n"
150 | if ac != ex {
151 | t.Errorf("Should be %#v, got %#v", ex, ac)
152 | }
153 |
154 | b = NewBuffer()
155 | b.InsertText(" hello", false, true)
156 | b.NewLine(true)
157 | ac = b.Text()
158 | ex = " hello\n "
159 | if ac != ex {
160 | t.Errorf("Should be %#v, got %#v", ex, ac)
161 | }
162 | }
163 |
164 | func TestBuffer_JoinNextLine(t *testing.T) {
165 | b := NewBuffer()
166 | b.InsertText("line1\nline2\nline3", false, true)
167 | b.CursorUp(1)
168 | b.JoinNextLine(" ")
169 |
170 | ac := b.Text()
171 | ex := "line1\nline2 line3"
172 | if ac != ex {
173 | t.Errorf("Should be %#v, got %#v", ex, ac)
174 | }
175 |
176 | // Test when there is no '\n' in the text
177 | b = NewBuffer()
178 | b.InsertText("line1", false, true)
179 | b.cursorPosition = 0
180 | b.JoinNextLine(" ")
181 | ac = b.Text()
182 | ex = "line1"
183 | if ac != ex {
184 | t.Errorf("Should be %#v, got %#v", ex, ac)
185 | }
186 | }
187 |
188 | func TestBuffer_SwapCharactersBeforeCursor(t *testing.T) {
189 | b := NewBuffer()
190 | b.InsertText("hello world", false, true)
191 | b.CursorLeft(2)
192 | b.SwapCharactersBeforeCursor()
193 | ac := b.Text()
194 | ex := "hello wrold"
195 | if ac != ex {
196 | t.Errorf("Should be %#v, got %#v", ex, ac)
197 | }
198 | }
199 |
--------------------------------------------------------------------------------
/_example/http-prompt/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "io/ioutil"
6 | "log"
7 | "net/http"
8 | "net/url"
9 | "os"
10 | "path"
11 | "strings"
12 |
13 | prompt "github.com/c-bata/go-prompt"
14 | )
15 |
16 | type RequestContext struct {
17 | url *url.URL
18 | header http.Header
19 | client *http.Client
20 | }
21 |
22 | var ctx *RequestContext
23 |
24 | // See https://github.com/eliangcs/http-prompt/blob/master/http_prompt/completion.py
25 | var suggestions = []prompt.Suggest{
26 | // Command
27 | {"cd", "Change URL/path"},
28 | {"exit", "Exit http-prompt"},
29 |
30 | // HTTP Method
31 | {"delete", "DELETE request"},
32 | {"get", "GET request"},
33 | {"patch", "GET request"},
34 | {"post", "POST request"},
35 | {"put", "PUT request"},
36 |
37 | // HTTP Header
38 | {"Accept", "Acceptable response media type"},
39 | {"Accept-Charset", "Acceptable response charsets"},
40 | {"Accept-Encoding", "Acceptable response content codings"},
41 | {"Accept-Language", "Preferred natural languages in response"},
42 | {"ALPN", "Application-layer protocol negotiation to use"},
43 | {"Alt-Used", "Alternative host in use"},
44 | {"Authorization", "Authentication information"},
45 | {"Cache-Control", "Directives for caches"},
46 | {"Connection", "Connection options"},
47 | {"Content-Encoding", "Content codings"},
48 | {"Content-Language", "Natural languages for content"},
49 | {"Content-Length", "Anticipated size for payload body"},
50 | {"Content-Location", "Where content was obtained"},
51 | {"Content-MD5", "Base64-encoded MD5 sum of content"},
52 | {"Content-Type", "Content media type"},
53 | {"Cookie", "Stored cookies"},
54 | {"Date", "Datetime when message was originated"},
55 | {"Depth", "Applied only to resource or its members"},
56 | {"DNT", "Do not track user"},
57 | {"Expect", "Expected behaviors supported by server"},
58 | {"Forwarded", "Proxies involved"},
59 | {"From", "Sender email address"},
60 | {"Host", "Target URI"},
61 | {"HTTP2-Settings", "HTTP/2 connection parameters"},
62 | {"If", "Request condition on state tokens and ETags"},
63 | {"If-Match", "Request condition on target resource"},
64 | {"If-Modified-Since", "Request condition on modification date"},
65 | {"If-None-Match", "Request condition on target resource"},
66 | {"If-Range", "Request condition on Range"},
67 | {"If-Schedule-Tag-Match", "Request condition on Schedule-Tag"},
68 | {"If-Unmodified-Since", "Request condition on modification date"},
69 | {"Max-Forwards", "Max number of times forwarded by proxies"},
70 | {"MIME-Version", "Version of MIME protocol"},
71 | {"Origin", "Origin(s} issuing the request"},
72 | {"Pragma", "Implementation-specific directives"},
73 | {"Prefer", "Preferred server behaviors"},
74 | {"Proxy-Authorization", "Proxy authorization credentials"},
75 | {"Proxy-Connection", "Proxy connection options"},
76 | {"Range", "Request transfer of only part of data"},
77 | {"Referer", "Previous web page"},
78 | {"TE", "Transfer codings willing to accept"},
79 | {"Transfer-Encoding", "Transfer codings applied to payload body"},
80 | {"Upgrade", "Invite server to upgrade to another protocol"},
81 | {"User-Agent", "User agent string"},
82 | {"Via", "Intermediate proxies"},
83 | {"Warning", "Possible incorrectness with payload body"},
84 | {"WWW-Authenticate", "Authentication scheme"},
85 | {"X-Csrf-Token", "Prevent cross-site request forgery"},
86 | {"X-CSRFToken", "Prevent cross-site request forgery"},
87 | {"X-Forwarded-For", "Originating client IP address"},
88 | {"X-Forwarded-Host", "Original host requested by client"},
89 | {"X-Forwarded-Proto", "Originating protocol"},
90 | {"X-Http-Method-Override", "Request method override"},
91 | {"X-Requested-With", "Used to identify Ajax requests"},
92 | {"X-XSRF-TOKEN", "Prevent cross-site request forgery"},
93 | }
94 |
95 | func livePrefix() (string, bool) {
96 | if ctx.url.Path == "/" {
97 | return "", false
98 | }
99 | return ctx.url.String() + "> ", true
100 | }
101 |
102 | func executor(in string) {
103 | in = strings.TrimSpace(in)
104 |
105 | var method, body string
106 | blocks := strings.Split(in, " ")
107 | switch blocks[0] {
108 | case "exit":
109 | fmt.Println("Bye!")
110 | os.Exit(0)
111 | case "cd":
112 | if len(blocks) < 2 {
113 | ctx.url.Path = "/"
114 | } else {
115 | ctx.url.Path = path.Join(ctx.url.Path, blocks[1])
116 | }
117 | return
118 | case "get", "delete":
119 | method = strings.ToUpper(blocks[0])
120 | case "post", "put", "patch":
121 | if len(blocks) < 2 {
122 | fmt.Println("please set request body.")
123 | return
124 | }
125 | body = strings.Join(blocks[1:], " ")
126 | method = strings.ToUpper(blocks[0])
127 | }
128 | if method != "" {
129 | req, err := http.NewRequest(method, ctx.url.String(), strings.NewReader(body))
130 | if err != nil {
131 | fmt.Println("err: " + err.Error())
132 | return
133 | }
134 | req.Header = ctx.header
135 | res, err := ctx.client.Do(req)
136 | if err != nil {
137 | fmt.Println("err: " + err.Error())
138 | return
139 | }
140 | result, err := ioutil.ReadAll(res.Body)
141 | if err != nil {
142 | fmt.Println("err: " + err.Error())
143 | return
144 | }
145 | fmt.Printf("%s\n", result)
146 | ctx.header = http.Header{}
147 | return
148 | }
149 |
150 | if h := strings.Split(in, ":"); len(h) == 2 {
151 | // Handling HTTP Header
152 | ctx.header.Add(strings.TrimSpace(h[0]), strings.Trim(h[1], ` '"`))
153 | } else {
154 | fmt.Println("Sorry, I don't understand.")
155 | }
156 | }
157 |
158 | func completer(in prompt.Document) []prompt.Suggest {
159 | w := in.GetWordBeforeCursor()
160 | if w == "" {
161 | return []prompt.Suggest{}
162 | }
163 | return prompt.FilterHasPrefix(suggestions, w, true)
164 | }
165 |
166 | func main() {
167 | var baseURL = "http://localhost:8000/"
168 | if len(os.Args) == 2 {
169 | baseURL = os.Args[1]
170 | if strings.HasSuffix(baseURL, "/") {
171 | baseURL += "/"
172 | }
173 | }
174 | u, err := url.Parse(baseURL)
175 | if err != nil {
176 | log.Fatal(err)
177 | }
178 | ctx = &RequestContext{
179 | url: u,
180 | header: http.Header{},
181 | client: &http.Client{},
182 | }
183 |
184 | p := prompt.New(
185 | executor,
186 | completer,
187 | prompt.OptionPrefix(u.String()+"> "),
188 | prompt.OptionLivePrefix(livePrefix),
189 | prompt.OptionTitle("http-prompt"),
190 | )
191 | p.Run()
192 | }
193 |
--------------------------------------------------------------------------------
/buffer.go:
--------------------------------------------------------------------------------
1 | package prompt
2 |
3 | import (
4 | "strings"
5 |
6 | "github.com/c-bata/go-prompt/internal/debug"
7 | )
8 |
9 | // Buffer emulates the console buffer.
10 | type Buffer struct {
11 | workingLines []string // The working lines. Similar to history
12 | workingIndex int
13 | cursorPosition int
14 | cacheDocument *Document
15 | preferredColumn int // Remember the original column for the next up/down movement.
16 | lastKeyStroke Key
17 | }
18 |
19 | // Text returns string of the current line.
20 | func (b *Buffer) Text() string {
21 | return b.workingLines[b.workingIndex]
22 | }
23 |
24 | // Document method to return document instance from the current text and cursor position.
25 | func (b *Buffer) Document() (d *Document) {
26 | if b.cacheDocument == nil ||
27 | b.cacheDocument.Text != b.Text() ||
28 | b.cacheDocument.cursorPosition != b.cursorPosition {
29 | b.cacheDocument = &Document{
30 | Text: b.Text(),
31 | cursorPosition: b.cursorPosition,
32 | }
33 | }
34 | b.cacheDocument.lastKey = b.lastKeyStroke
35 | return b.cacheDocument
36 | }
37 |
38 | // DisplayCursorPosition returns the cursor position on rendered text on terminal emulators.
39 | // So if Document is "日本(cursor)語", DisplayedCursorPosition returns 4 because '日' and '本' are double width characters.
40 | func (b *Buffer) DisplayCursorPosition() int {
41 | return b.Document().DisplayCursorPosition()
42 | }
43 |
44 | // InsertText insert string from current line.
45 | func (b *Buffer) InsertText(v string, overwrite bool, moveCursor bool) {
46 | or := []rune(b.Text())
47 | oc := b.cursorPosition
48 |
49 | if overwrite {
50 | overwritten := string(or[oc : oc+len(v)])
51 | if strings.Contains(overwritten, "\n") {
52 | i := strings.IndexAny(overwritten, "\n")
53 | overwritten = overwritten[:i]
54 | }
55 | b.setText(string(or[:oc]) + v + string(or[oc+len(overwritten):]))
56 | } else {
57 | b.setText(string(or[:oc]) + v + string(or[oc:]))
58 | }
59 |
60 | if moveCursor {
61 | b.cursorPosition += len([]rune(v))
62 | }
63 | }
64 |
65 | // SetText method to set text and update cursorPosition.
66 | // (When doing this, make sure that the cursor_position is valid for this text.
67 | // text/cursor_position should be consistent at any time, otherwise set a Document instead.)
68 | func (b *Buffer) setText(v string) {
69 | debug.Assert(b.cursorPosition <= len([]rune(v)), "length of input should be shorter than cursor position")
70 | b.workingLines[b.workingIndex] = v
71 | }
72 |
73 | // Set cursor position. Return whether it changed.
74 | func (b *Buffer) setCursorPosition(p int) {
75 | if p > 0 {
76 | b.cursorPosition = p
77 | } else {
78 | b.cursorPosition = 0
79 | }
80 | }
81 |
82 | func (b *Buffer) setDocument(d *Document) {
83 | b.cacheDocument = d
84 | b.setCursorPosition(d.cursorPosition) // Call before setText because setText check the relation between cursorPosition and line length.
85 | b.setText(d.Text)
86 | }
87 |
88 | // CursorLeft move to left on the current line.
89 | func (b *Buffer) CursorLeft(count int) {
90 | l := b.Document().GetCursorLeftPosition(count)
91 | b.cursorPosition += l
92 | }
93 |
94 | // CursorRight move to right on the current line.
95 | func (b *Buffer) CursorRight(count int) {
96 | l := b.Document().GetCursorRightPosition(count)
97 | b.cursorPosition += l
98 | }
99 |
100 | // CursorUp move cursor to the previous line.
101 | // (for multi-line edit).
102 | func (b *Buffer) CursorUp(count int) {
103 | orig := b.preferredColumn
104 | if b.preferredColumn == -1 { // -1 means nil
105 | orig = b.Document().CursorPositionCol()
106 | }
107 | b.cursorPosition += b.Document().GetCursorUpPosition(count, orig)
108 |
109 | // Remember the original column for the next up/down movement.
110 | b.preferredColumn = orig
111 | }
112 |
113 | // CursorDown move cursor to the next line.
114 | // (for multi-line edit).
115 | func (b *Buffer) CursorDown(count int) {
116 | orig := b.preferredColumn
117 | if b.preferredColumn == -1 { // -1 means nil
118 | orig = b.Document().CursorPositionCol()
119 | }
120 | b.cursorPosition += b.Document().GetCursorDownPosition(count, orig)
121 |
122 | // Remember the original column for the next up/down movement.
123 | b.preferredColumn = orig
124 | }
125 |
126 | // DeleteBeforeCursor delete specified number of characters before cursor and return the deleted text.
127 | func (b *Buffer) DeleteBeforeCursor(count int) (deleted string) {
128 | debug.Assert(count >= 0, "count should be positive")
129 | r := []rune(b.Text())
130 |
131 | if b.cursorPosition > 0 {
132 | start := b.cursorPosition - count
133 | if start < 0 {
134 | start = 0
135 | }
136 | deleted = string(r[start:b.cursorPosition])
137 | b.setDocument(&Document{
138 | Text: string(r[:start]) + string(r[b.cursorPosition:]),
139 | cursorPosition: b.cursorPosition - len([]rune(deleted)),
140 | })
141 | }
142 | return
143 | }
144 |
145 | // NewLine means CR.
146 | func (b *Buffer) NewLine(copyMargin bool) {
147 | if copyMargin {
148 | b.InsertText("\n"+b.Document().leadingWhitespaceInCurrentLine(), false, true)
149 | } else {
150 | b.InsertText("\n", false, true)
151 | }
152 | }
153 |
154 | // Delete specified number of characters and Return the deleted text.
155 | func (b *Buffer) Delete(count int) (deleted string) {
156 | r := []rune(b.Text())
157 | if b.cursorPosition < len(r) {
158 | deleted = b.Document().TextAfterCursor()[:count]
159 | b.setText(string(r[:b.cursorPosition]) + string(r[b.cursorPosition+len(deleted):]))
160 | }
161 | return
162 | }
163 |
164 | // JoinNextLine joins the next line to the current one by deleting the line ending after the current line.
165 | func (b *Buffer) JoinNextLine(separator string) {
166 | if !b.Document().OnLastLine() {
167 | b.cursorPosition += b.Document().GetEndOfLinePosition()
168 | b.Delete(1)
169 | // Remove spaces
170 | b.setText(b.Document().TextBeforeCursor() + separator + strings.TrimLeft(b.Document().TextAfterCursor(), " "))
171 | }
172 | }
173 |
174 | // SwapCharactersBeforeCursor swaps the last two characters before the cursor.
175 | func (b *Buffer) SwapCharactersBeforeCursor() {
176 | if b.cursorPosition >= 2 {
177 | x := b.Text()[b.cursorPosition-2 : b.cursorPosition-1]
178 | y := b.Text()[b.cursorPosition-1 : b.cursorPosition]
179 | b.setText(b.Text()[:b.cursorPosition-2] + y + x + b.Text()[b.cursorPosition:])
180 | }
181 | }
182 |
183 | // NewBuffer is constructor of Buffer struct.
184 | func NewBuffer() (b *Buffer) {
185 | b = &Buffer{
186 | workingLines: []string{""},
187 | workingIndex: 0,
188 | preferredColumn: -1, // -1 means nil
189 | }
190 | return
191 | }
192 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # go-prompt
2 |
3 | [](https://goreportcard.com/report/github.com/c-bata/go-prompt)
4 | 
5 | [](https://godoc.org/github.com/c-bata/go-prompt)
6 | 
7 |
8 | A library for building powerful interactive prompts inspired by [python-prompt-toolkit](https://github.com/jonathanslenders/python-prompt-toolkit),
9 | making it easier to build cross-platform command line tools using Go.
10 |
11 | ```go
12 | package main
13 |
14 | import (
15 | "fmt"
16 | "github.com/c-bata/go-prompt"
17 | )
18 |
19 | func completer(d prompt.Document) []prompt.Suggest {
20 | s := []prompt.Suggest{
21 | {Text: "users", Description: "Store the username and age"},
22 | {Text: "articles", Description: "Store the article text posted by user"},
23 | {Text: "comments", Description: "Store the text commented to articles"},
24 | }
25 | return prompt.FilterHasPrefix(s, d.GetWordBeforeCursor(), true)
26 | }
27 |
28 | func main() {
29 | fmt.Println("Please select table.")
30 | t := prompt.Input("> ", completer)
31 | fmt.Println("You selected " + t)
32 | }
33 | ```
34 |
35 | #### Projects using go-prompt
36 |
37 | * [c-bata/kube-prompt : An interactive kubernetes client featuring auto-complete written in Go.](https://github.com/c-bata/kube-prompt)
38 | * [rancher/cli : The Rancher Command Line Interface (CLI)is a unified tool to manage your Rancher server](https://github.com/rancher/cli)
39 | * [kubicorn/kubicorn : Simple, cloud native infrastructure for Kubernetes.](https://github.com/kubicorn/kubicorn)
40 | * [cch123/asm-cli : Interactive shell of assembly language(X86/X64) based on unicorn and rasm2](https://github.com/cch123/asm-cli)
41 | * [ktr0731/evans : more expressive universal gRPC client](https://github.com/ktr0731/evans)
42 | * [CrushedPixel/moshpit: A Command-line tool for datamoshing.](https://github.com/CrushedPixel/moshpit)
43 | * [last-ent/testy-go: Testy Go: A tool for easy testing!](https://github.com/last-ent/testy-go)
44 | * [tiagorlampert/CHAOS: a PoC that allow generate payloads and control remote operating systems.](https://github.com/tiagorlampert/CHAOS)
45 | * [abs-lang/abs: ABS is a scripting language that works best on terminal. It tries to combine the elegance of languages such as Python, or Ruby, to the convenience of Bash.](https://github.com/abs-lang/abs)
46 | * [takashabe/btcli: btcli is a CLI client for the Bigtable. Has many read options and auto-completion.](https://github.com/takashabe/btcli)
47 | * [ysn2233/kafka-prompt: An interactive kafka-prompt(kafka-shell) built on existing kafka command client](https://github.com/ysn2233/kafka-prompt)
48 | * [fishi0x01/vsh: HashiCorp Vault interactive shell](https://github.com/fishi0x01/vsh)
49 | * [mstrYoda/docker-shell: A simple interactive prompt for docker](https://github.com/mstrYoda/docker-shell)
50 | * [c-bata/gh-prompt: An interactive GitHub CLI featuring auto-complete.](https://github.com/c-bata/gh-prompt)
51 | * [docker-slim/docker-slim: Don't change anything in your Docker container image and minify it by up to 30x (and for compiled languages even more) making it secure too! (free and open source)](https://github.com/docker-slim/docker-slim)
52 | * [rueyaa332266/ezcron: Ezcron is a CLI tool, helping you deal with cron expression easier.](https://github.com/rueyaa332266/ezcron)
53 | * [qingstor/qsctl: Advanced command line tool for QingStor Object Storage.](https://github.com/qingstor/qsctl)
54 | * [segmentio/topicctl: Tool for declarative management of Kafka topics](https://github.com/segmentio/topicctl)
55 | * [chriswalz/bit: Bit is a modern Git CLI](https://github.com/chriswalz/bit)
56 | * (If you create a CLI utility using go-prompt and want your own project to be listed here, please submit a GitHub issue.)
57 |
58 | ## Features
59 |
60 | ### Powerful auto-completion
61 |
62 | [](https://github.com/c-bata/kube-prompt)
63 |
64 | (This is a GIF animation of kube-prompt.)
65 |
66 | ### Flexible options
67 |
68 | go-prompt provides many options. Please check [option section of GoDoc](https://godoc.org/github.com/c-bata/go-prompt#Option) for more details.
69 |
70 | [](#flexible-options)
71 |
72 | ### Keyboard Shortcuts
73 |
74 | Emacs-like keyboard shortcuts are available by default (these also are the default shortcuts in Bash shell).
75 | You can customize and expand these shortcuts.
76 |
77 | [](#keyboard-shortcuts)
78 |
79 | Key Binding | Description
80 | ---------------------|---------------------------------------------------------
81 | Ctrl + A | Go to the beginning of the line (Home)
82 | Ctrl + E | Go to the end of the line (End)
83 | Ctrl + P | Previous command (Up arrow)
84 | Ctrl + N | Next command (Down arrow)
85 | Ctrl + F | Forward one character
86 | Ctrl + B | Backward one character
87 | Ctrl + D | Delete character under the cursor
88 | Ctrl + H | Delete character before the cursor (Backspace)
89 | Ctrl + W | Cut the word before the cursor to the clipboard
90 | Ctrl + K | Cut the line after the cursor to the clipboard
91 | Ctrl + U | Cut the line before the cursor to the clipboard
92 | Ctrl + L | Clear the screen
93 |
94 | ### History
95 |
96 | You can use Up arrow and Down arrow to walk through the history of commands executed.
97 |
98 | [](#history)
99 |
100 | ### Multiple platform support
101 |
102 | We have confirmed go-prompt works fine in the following terminals:
103 |
104 | * iTerm2 (macOS)
105 | * Terminal.app (macOS)
106 | * Command Prompt (Windows)
107 | * gnome-terminal (Ubuntu)
108 |
109 | ## Links
110 |
111 | * [Change Log](./CHANGELOG.md)
112 | * [GoDoc](http://godoc.org/github.com/c-bata/go-prompt)
113 | * [gocover.io](https://gocover.io/github.com/c-bata/go-prompt)
114 |
115 | ## Author
116 |
117 | Masashi Shibata
118 |
119 | * Twitter: [@c\_bata\_](https://twitter.com/c_bata_/)
120 | * Github: [@c-bata](https://github.com/c-bata/)
121 |
122 | ## License
123 |
124 | This software is licensed under the MIT license, see [LICENSE](./LICENSE) for more information.
125 |
126 |
--------------------------------------------------------------------------------
/completion_test.go:
--------------------------------------------------------------------------------
1 | package prompt
2 |
3 | import (
4 | "reflect"
5 | "testing"
6 | )
7 |
8 | func TestFormatShortSuggestion(t *testing.T) {
9 | var scenarioTable = []struct {
10 | in []Suggest
11 | expected []Suggest
12 | max int
13 | exWidth int
14 | }{
15 | {
16 | in: []Suggest{
17 | {Text: "foo"},
18 | {Text: "bar"},
19 | {Text: "fuga"},
20 | },
21 | expected: []Suggest{
22 | {Text: " foo "},
23 | {Text: " bar "},
24 | {Text: " fuga "},
25 | },
26 | max: 100,
27 | exWidth: 6,
28 | },
29 | {
30 | in: []Suggest{
31 | {Text: "apple", Description: "This is apple."},
32 | {Text: "banana", Description: "This is banana."},
33 | {Text: "coconut", Description: "This is coconut."},
34 | },
35 | expected: []Suggest{
36 | {Text: " apple ", Description: " This is apple. "},
37 | {Text: " banana ", Description: " This is banana. "},
38 | {Text: " coconut ", Description: " This is coconut. "},
39 | },
40 | max: 100,
41 | exWidth: len(" apple " + " This is apple. "),
42 | },
43 | {
44 | in: []Suggest{
45 | {Text: "This is apple."},
46 | {Text: "This is banana."},
47 | {Text: "This is coconut."},
48 | },
49 | expected: []Suggest{
50 | {Text: " Thi... "},
51 | {Text: " Thi... "},
52 | {Text: " Thi... "},
53 | },
54 | max: 8,
55 | exWidth: 8,
56 | },
57 | {
58 | in: []Suggest{
59 | {Text: "This is apple."},
60 | {Text: "This is banana."},
61 | {Text: "This is coconut."},
62 | },
63 | expected: []Suggest{},
64 | max: 3,
65 | exWidth: 0,
66 | },
67 | {
68 | in: []Suggest{
69 | {Text: "--all-namespaces", Description: "-------------------------------------------------------------------------------------------------------------------------------------------"},
70 | {Text: "--allow-missing-template-keys", Description: "-----------------------------------------------------------------------------------------------------------------------------------------------"},
71 | {Text: "--export", Description: "----------------------------------------------------------------------------------------------------------"},
72 | {Text: "-f", Description: "-----------------------------------------------------------------------------------"},
73 | {Text: "--filename", Description: "-----------------------------------------------------------------------------------"},
74 | {Text: "--include-extended-apis", Description: "------------------------------------------------------------------------------------"},
75 | },
76 | expected: []Suggest{
77 | {Text: " --all-namespaces ", Description: " --------------... "},
78 | {Text: " --allow-missing-template-keys ", Description: " --------------... "},
79 | {Text: " --export ", Description: " --------------... "},
80 | {Text: " -f ", Description: " --------------... "},
81 | {Text: " --filename ", Description: " --------------... "},
82 | {Text: " --include-extended-apis ", Description: " --------------... "},
83 | },
84 | max: 50,
85 | exWidth: len(" --include-extended-apis " + " ---------------..."),
86 | },
87 | {
88 | in: []Suggest{
89 | {Text: "--all-namespaces", Description: "If present, list the requested object(s) across all namespaces. Namespace in current context is ignored even if specified with --namespace."},
90 | {Text: "--allow-missing-template-keys", Description: "If true, ignore any errors in templates when a field or map key is missing in the template. Only applies to golang and jsonpath output formats."},
91 | {Text: "--export", Description: "If true, use 'export' for the resources. Exported resources are stripped of cluster-specific information."},
92 | {Text: "-f", Description: "Filename, directory, or URL to files identifying the resource to get from a server."},
93 | {Text: "--filename", Description: "Filename, directory, or URL to files identifying the resource to get from a server."},
94 | {Text: "--include-extended-apis", Description: "If true, include definitions of new APIs via calls to the API server. [default true]"},
95 | },
96 | expected: []Suggest{
97 | {Text: " --all-namespaces ", Description: " If present, list the requested object(s) across all namespaces. Namespace in current context is ignored even if specified with --namespace. "},
98 | {Text: " --allow-missing-template-keys ", Description: " If true, ignore any errors in templates when a field or map key is missing in the template. Only applies to golang and jsonpath output formats. "},
99 | {Text: " --export ", Description: " If true, use 'export' for the resources. Exported resources are stripped of cluster-specific information. "},
100 | {Text: " -f ", Description: " Filename, directory, or URL to files identifying the resource to get from a server. "},
101 | {Text: " --filename ", Description: " Filename, directory, or URL to files identifying the resource to get from a server. "},
102 | {Text: " --include-extended-apis ", Description: " If true, include definitions of new APIs via calls to the API server. [default true] "},
103 | },
104 | max: 500,
105 | exWidth: len(" --include-extended-apis " + " If true, include definitions of new APIs via calls to the API server. [default true] "),
106 | },
107 | }
108 |
109 | for i, s := range scenarioTable {
110 | actual, width := formatSuggestions(s.in, s.max)
111 | if width != s.exWidth {
112 | t.Errorf("[scenario %d] Want %d but got %d\n", i, s.exWidth, width)
113 | }
114 | if !reflect.DeepEqual(actual, s.expected) {
115 | t.Errorf("[scenario %d] Want %#v, but got %#v\n", i, s.expected, actual)
116 | }
117 | }
118 | }
119 |
120 | func TestFormatText(t *testing.T) {
121 | var scenarioTable = []struct {
122 | in []string
123 | expected []string
124 | max int
125 | exWidth int
126 | }{
127 | {
128 | in: []string{
129 | "",
130 | "",
131 | },
132 | expected: []string{
133 | "",
134 | "",
135 | },
136 | max: 10,
137 | exWidth: 0,
138 | },
139 | {
140 | in: []string{
141 | "apple",
142 | "banana",
143 | "coconut",
144 | },
145 | expected: []string{
146 | "",
147 | "",
148 | "",
149 | },
150 | max: 2,
151 | exWidth: 0,
152 | },
153 | {
154 | in: []string{
155 | "apple",
156 | "banana",
157 | "coconut",
158 | },
159 | expected: []string{
160 | "",
161 | "",
162 | "",
163 | },
164 | max: len(" " + " " + shortenSuffix),
165 | exWidth: 0,
166 | },
167 | {
168 | in: []string{
169 | "apple",
170 | "banana",
171 | "coconut",
172 | },
173 | expected: []string{
174 | " apple ",
175 | " banana ",
176 | " coconut ",
177 | },
178 | max: 100,
179 | exWidth: len(" coconut "),
180 | },
181 | {
182 | in: []string{
183 | "apple",
184 | "banana",
185 | "coconut",
186 | },
187 | expected: []string{
188 | " a... ",
189 | " b... ",
190 | " c... ",
191 | },
192 | max: 6,
193 | exWidth: 6,
194 | },
195 | }
196 |
197 | for i, s := range scenarioTable {
198 | actual, width := formatTexts(s.in, s.max, " ", " ")
199 | if width != s.exWidth {
200 | t.Errorf("[scenario %d] Want %d but got %d\n", i, s.exWidth, width)
201 | }
202 | if !reflect.DeepEqual(actual, s.expected) {
203 | t.Errorf("[scenario %d] Want %#v, but got %#v\n", i, s.expected, actual)
204 | }
205 | }
206 | }
207 |
--------------------------------------------------------------------------------
/input.go:
--------------------------------------------------------------------------------
1 | package prompt
2 |
3 | import "bytes"
4 |
5 | // WinSize represents the width and height of terminal.
6 | type WinSize struct {
7 | Row uint16
8 | Col uint16
9 | }
10 |
11 | // ConsoleParser is an interface to abstract input layer.
12 | type ConsoleParser interface {
13 | // Setup should be called before starting input
14 | Setup() error
15 | // TearDown should be called after stopping input
16 | TearDown() error
17 | // GetWinSize returns WinSize object to represent width and height of terminal.
18 | GetWinSize() *WinSize
19 | // Read returns byte array.
20 | Read() ([]byte, error)
21 | }
22 |
23 | // GetKey returns Key correspond to input byte codes.
24 | func GetKey(b []byte) Key {
25 | for _, k := range ASCIISequences {
26 | if bytes.Equal(k.ASCIICode, b) {
27 | return k.Key
28 | }
29 | }
30 | return NotDefined
31 | }
32 |
33 | // ASCIISequences holds mappings of the key and byte array.
34 | var ASCIISequences = []*ASCIICode{
35 | {Key: Escape, ASCIICode: []byte{0x1b}},
36 |
37 | {Key: ControlSpace, ASCIICode: []byte{0x00}},
38 | {Key: ControlA, ASCIICode: []byte{0x1}},
39 | {Key: ControlB, ASCIICode: []byte{0x2}},
40 | {Key: ControlC, ASCIICode: []byte{0x3}},
41 | {Key: ControlD, ASCIICode: []byte{0x4}},
42 | {Key: ControlE, ASCIICode: []byte{0x5}},
43 | {Key: ControlF, ASCIICode: []byte{0x6}},
44 | {Key: ControlG, ASCIICode: []byte{0x7}},
45 | {Key: ControlH, ASCIICode: []byte{0x8}},
46 | //{Key: ControlI, ASCIICode: []byte{0x9}},
47 | //{Key: ControlJ, ASCIICode: []byte{0xa}},
48 | {Key: ControlK, ASCIICode: []byte{0xb}},
49 | {Key: ControlL, ASCIICode: []byte{0xc}},
50 | {Key: ControlM, ASCIICode: []byte{0xd}},
51 | {Key: ControlN, ASCIICode: []byte{0xe}},
52 | {Key: ControlO, ASCIICode: []byte{0xf}},
53 | {Key: ControlP, ASCIICode: []byte{0x10}},
54 | {Key: ControlQ, ASCIICode: []byte{0x11}},
55 | {Key: ControlR, ASCIICode: []byte{0x12}},
56 | {Key: ControlS, ASCIICode: []byte{0x13}},
57 | {Key: ControlT, ASCIICode: []byte{0x14}},
58 | {Key: ControlU, ASCIICode: []byte{0x15}},
59 | {Key: ControlV, ASCIICode: []byte{0x16}},
60 | {Key: ControlW, ASCIICode: []byte{0x17}},
61 | {Key: ControlX, ASCIICode: []byte{0x18}},
62 | {Key: ControlY, ASCIICode: []byte{0x19}},
63 | {Key: ControlZ, ASCIICode: []byte{0x1a}},
64 |
65 | {Key: ControlBackslash, ASCIICode: []byte{0x1c}},
66 | {Key: ControlSquareClose, ASCIICode: []byte{0x1d}},
67 | {Key: ControlCircumflex, ASCIICode: []byte{0x1e}},
68 | {Key: ControlUnderscore, ASCIICode: []byte{0x1f}},
69 | {Key: Backspace, ASCIICode: []byte{0x7f}},
70 |
71 | {Key: Up, ASCIICode: []byte{0x1b, 0x5b, 0x41}},
72 | {Key: Down, ASCIICode: []byte{0x1b, 0x5b, 0x42}},
73 | {Key: Right, ASCIICode: []byte{0x1b, 0x5b, 0x43}},
74 | {Key: Left, ASCIICode: []byte{0x1b, 0x5b, 0x44}},
75 | {Key: Home, ASCIICode: []byte{0x1b, 0x5b, 0x48}},
76 | {Key: Home, ASCIICode: []byte{0x1b, 0x30, 0x48}},
77 | {Key: End, ASCIICode: []byte{0x1b, 0x5b, 0x46}},
78 | {Key: End, ASCIICode: []byte{0x1b, 0x30, 0x46}},
79 |
80 | {Key: Enter, ASCIICode: []byte{0xa}},
81 | {Key: Delete, ASCIICode: []byte{0x1b, 0x5b, 0x33, 0x7e}},
82 | {Key: ShiftDelete, ASCIICode: []byte{0x1b, 0x5b, 0x33, 0x3b, 0x32, 0x7e}},
83 | {Key: ControlDelete, ASCIICode: []byte{0x1b, 0x5b, 0x33, 0x3b, 0x35, 0x7e}},
84 | {Key: Home, ASCIICode: []byte{0x1b, 0x5b, 0x31, 0x7e}},
85 | {Key: End, ASCIICode: []byte{0x1b, 0x5b, 0x34, 0x7e}},
86 | {Key: PageUp, ASCIICode: []byte{0x1b, 0x5b, 0x35, 0x7e}},
87 | {Key: PageDown, ASCIICode: []byte{0x1b, 0x5b, 0x36, 0x7e}},
88 | {Key: Home, ASCIICode: []byte{0x1b, 0x5b, 0x37, 0x7e}},
89 | {Key: End, ASCIICode: []byte{0x1b, 0x5b, 0x38, 0x7e}},
90 | {Key: Tab, ASCIICode: []byte{0x9}},
91 | {Key: BackTab, ASCIICode: []byte{0x1b, 0x5b, 0x5a}},
92 | {Key: Insert, ASCIICode: []byte{0x1b, 0x5b, 0x32, 0x7e}},
93 |
94 | {Key: F1, ASCIICode: []byte{0x1b, 0x4f, 0x50}},
95 | {Key: F2, ASCIICode: []byte{0x1b, 0x4f, 0x51}},
96 | {Key: F3, ASCIICode: []byte{0x1b, 0x4f, 0x52}},
97 | {Key: F4, ASCIICode: []byte{0x1b, 0x4f, 0x53}},
98 |
99 | {Key: F1, ASCIICode: []byte{0x1b, 0x4f, 0x50, 0x41}}, // Linux console
100 | {Key: F2, ASCIICode: []byte{0x1b, 0x5b, 0x5b, 0x42}}, // Linux console
101 | {Key: F3, ASCIICode: []byte{0x1b, 0x5b, 0x5b, 0x43}}, // Linux console
102 | {Key: F4, ASCIICode: []byte{0x1b, 0x5b, 0x5b, 0x44}}, // Linux console
103 | {Key: F5, ASCIICode: []byte{0x1b, 0x5b, 0x5b, 0x45}}, // Linux console
104 |
105 | {Key: F1, ASCIICode: []byte{0x1b, 0x5b, 0x11, 0x7e}}, // rxvt-unicode
106 | {Key: F2, ASCIICode: []byte{0x1b, 0x5b, 0x12, 0x7e}}, // rxvt-unicode
107 | {Key: F3, ASCIICode: []byte{0x1b, 0x5b, 0x13, 0x7e}}, // rxvt-unicode
108 | {Key: F4, ASCIICode: []byte{0x1b, 0x5b, 0x14, 0x7e}}, // rxvt-unicode
109 |
110 | {Key: F5, ASCIICode: []byte{0x1b, 0x5b, 0x31, 0x35, 0x7e}},
111 | {Key: F6, ASCIICode: []byte{0x1b, 0x5b, 0x31, 0x37, 0x7e}},
112 | {Key: F7, ASCIICode: []byte{0x1b, 0x5b, 0x31, 0x38, 0x7e}},
113 | {Key: F8, ASCIICode: []byte{0x1b, 0x5b, 0x31, 0x39, 0x7e}},
114 | {Key: F9, ASCIICode: []byte{0x1b, 0x5b, 0x32, 0x30, 0x7e}},
115 | {Key: F10, ASCIICode: []byte{0x1b, 0x5b, 0x32, 0x31, 0x7e}},
116 | {Key: F11, ASCIICode: []byte{0x1b, 0x5b, 0x32, 0x32, 0x7e}},
117 | {Key: F12, ASCIICode: []byte{0x1b, 0x5b, 0x32, 0x34, 0x7e, 0x8}},
118 | {Key: F13, ASCIICode: []byte{0x1b, 0x5b, 0x25, 0x7e}},
119 | {Key: F14, ASCIICode: []byte{0x1b, 0x5b, 0x26, 0x7e}},
120 | {Key: F15, ASCIICode: []byte{0x1b, 0x5b, 0x28, 0x7e}},
121 | {Key: F16, ASCIICode: []byte{0x1b, 0x5b, 0x29, 0x7e}},
122 | {Key: F17, ASCIICode: []byte{0x1b, 0x5b, 0x31, 0x7e}},
123 | {Key: F18, ASCIICode: []byte{0x1b, 0x5b, 0x32, 0x7e}},
124 | {Key: F19, ASCIICode: []byte{0x1b, 0x5b, 0x33, 0x7e}},
125 | {Key: F20, ASCIICode: []byte{0x1b, 0x5b, 0x34, 0x7e}},
126 |
127 | // Xterm
128 | {Key: F13, ASCIICode: []byte{0x1b, 0x5b, 0x31, 0x3b, 0x32, 0x50}},
129 | {Key: F14, ASCIICode: []byte{0x1b, 0x5b, 0x31, 0x3b, 0x32, 0x51}},
130 | // &ASCIICode{Key: F15, ASCIICode: []byte{0x1b, 0x5b, 0x31, 0x3b, 0x32, 0x52}}, // Conflicts with CPR response
131 | {Key: F16, ASCIICode: []byte{0x1b, 0x5b, 0x31, 0x3b, 0x32, 0x52}},
132 | {Key: F17, ASCIICode: []byte{0x1b, 0x5b, 0x15, 0x3b, 0x32, 0x7e}},
133 | {Key: F18, ASCIICode: []byte{0x1b, 0x5b, 0x17, 0x3b, 0x32, 0x7e}},
134 | {Key: F19, ASCIICode: []byte{0x1b, 0x5b, 0x18, 0x3b, 0x32, 0x7e}},
135 | {Key: F20, ASCIICode: []byte{0x1b, 0x5b, 0x19, 0x3b, 0x32, 0x7e}},
136 | {Key: F21, ASCIICode: []byte{0x1b, 0x5b, 0x20, 0x3b, 0x32, 0x7e}},
137 | {Key: F22, ASCIICode: []byte{0x1b, 0x5b, 0x21, 0x3b, 0x32, 0x7e}},
138 | {Key: F23, ASCIICode: []byte{0x1b, 0x5b, 0x23, 0x3b, 0x32, 0x7e}},
139 | {Key: F24, ASCIICode: []byte{0x1b, 0x5b, 0x24, 0x3b, 0x32, 0x7e}},
140 |
141 | {Key: ControlUp, ASCIICode: []byte{0x1b, 0x5b, 0x31, 0x3b, 0x35, 0x41}},
142 | {Key: ControlDown, ASCIICode: []byte{0x1b, 0x5b, 0x31, 0x3b, 0x35, 0x42}},
143 | {Key: ControlRight, ASCIICode: []byte{0x1b, 0x5b, 0x31, 0x3b, 0x35, 0x43}},
144 | {Key: ControlLeft, ASCIICode: []byte{0x1b, 0x5b, 0x31, 0x3b, 0x35, 0x44}},
145 |
146 | {Key: ShiftUp, ASCIICode: []byte{0x1b, 0x5b, 0x31, 0x3b, 0x32, 0x41}},
147 | {Key: ShiftDown, ASCIICode: []byte{0x1b, 0x5b, 0x31, 0x3b, 0x32, 0x42}},
148 | {Key: ShiftRight, ASCIICode: []byte{0x1b, 0x5b, 0x31, 0x3b, 0x32, 0x43}},
149 | {Key: ShiftLeft, ASCIICode: []byte{0x1b, 0x5b, 0x31, 0x3b, 0x32, 0x44}},
150 |
151 | // Tmux sends following keystrokes when control+arrow is pressed, but for
152 | // Emacs ansi-term sends the same sequences for normal arrow keys. Consider
153 | // it a normal arrow press, because that's more important.
154 | {Key: Up, ASCIICode: []byte{0x1b, 0x4f, 0x41}},
155 | {Key: Down, ASCIICode: []byte{0x1b, 0x4f, 0x42}},
156 | {Key: Right, ASCIICode: []byte{0x1b, 0x4f, 0x43}},
157 | {Key: Left, ASCIICode: []byte{0x1b, 0x4f, 0x44}},
158 |
159 | {Key: ControlUp, ASCIICode: []byte{0x1b, 0x5b, 0x35, 0x41}},
160 | {Key: ControlDown, ASCIICode: []byte{0x1b, 0x5b, 0x35, 0x42}},
161 | {Key: ControlRight, ASCIICode: []byte{0x1b, 0x5b, 0x35, 0x43}},
162 | {Key: ControlLeft, ASCIICode: []byte{0x1b, 0x5b, 0x35, 0x44}},
163 |
164 | {Key: ControlRight, ASCIICode: []byte{0x1b, 0x5b, 0x4f, 0x63}}, // rxvt
165 | {Key: ControlLeft, ASCIICode: []byte{0x1b, 0x5b, 0x4f, 0x64}}, // rxvt
166 |
167 | {Key: Ignore, ASCIICode: []byte{0x1b, 0x5b, 0x45}}, // Xterm
168 | {Key: Ignore, ASCIICode: []byte{0x1b, 0x5b, 0x46}}, // Linux console
169 | }
170 |
--------------------------------------------------------------------------------
/prompt.go:
--------------------------------------------------------------------------------
1 | package prompt
2 |
3 | import (
4 | "bytes"
5 | "os"
6 | "time"
7 |
8 | "github.com/c-bata/go-prompt/internal/debug"
9 | )
10 |
11 | // Executor is called when user input something text.
12 | type Executor func(string)
13 |
14 | // ExitChecker is called after user input to check if prompt must stop and exit go-prompt Run loop.
15 | // User input means: selecting/typing an entry, then, if said entry content matches the ExitChecker function criteria:
16 | // - immediate exit (if breakline is false) without executor called
17 | // - exit after typing (meaning breakline is true), and the executor is called first, before exit.
18 | // Exit means exit go-prompt (not the overall Go program)
19 | type ExitChecker func(in string, breakline bool) bool
20 |
21 | // Completer should return the suggest item from Document.
22 | type Completer func(Document) []Suggest
23 |
24 | // Prompt is core struct of go-prompt.
25 | type Prompt struct {
26 | in ConsoleParser
27 | buf *Buffer
28 | renderer *Render
29 | executor Executor
30 | history *History
31 | completion *CompletionManager
32 | keyBindings []KeyBind
33 | ASCIICodeBindings []ASCIICodeBind
34 | keyBindMode KeyBindMode
35 | completionOnDown bool
36 | exitChecker ExitChecker
37 | skipTearDown bool
38 | }
39 |
40 | // Exec is the struct contains user input context.
41 | type Exec struct {
42 | input string
43 | }
44 |
45 | // Run starts prompt.
46 | func (p *Prompt) Run() {
47 | p.skipTearDown = false
48 | defer debug.Teardown()
49 | debug.Log("start prompt")
50 | p.setUp()
51 | defer p.tearDown()
52 |
53 | if p.completion.showAtStart {
54 | p.completion.Update(*p.buf.Document())
55 | }
56 |
57 | p.renderer.Render(p.buf, p.completion)
58 |
59 | bufCh := make(chan []byte, 128)
60 | stopReadBufCh := make(chan struct{})
61 | go p.readBuffer(bufCh, stopReadBufCh)
62 |
63 | exitCh := make(chan int)
64 | winSizeCh := make(chan *WinSize)
65 | stopHandleSignalCh := make(chan struct{})
66 | go p.handleSignals(exitCh, winSizeCh, stopHandleSignalCh)
67 |
68 | for {
69 | select {
70 | case b := <-bufCh:
71 | if shouldExit, e := p.feed(b); shouldExit {
72 | p.renderer.BreakLine(p.buf)
73 | stopReadBufCh <- struct{}{}
74 | stopHandleSignalCh <- struct{}{}
75 | return
76 | } else if e != nil {
77 | // Stop goroutine to run readBuffer function
78 | stopReadBufCh <- struct{}{}
79 | stopHandleSignalCh <- struct{}{}
80 |
81 | // Unset raw mode
82 | // Reset to Blocking mode because returned EAGAIN when still set non-blocking mode.
83 | debug.AssertNoError(p.in.TearDown())
84 | p.executor(e.input)
85 |
86 | p.completion.Update(*p.buf.Document())
87 |
88 | p.renderer.Render(p.buf, p.completion)
89 |
90 | if p.exitChecker != nil && p.exitChecker(e.input, true) {
91 | p.skipTearDown = true
92 | return
93 | }
94 | // Set raw mode
95 | debug.AssertNoError(p.in.Setup())
96 | go p.readBuffer(bufCh, stopReadBufCh)
97 | go p.handleSignals(exitCh, winSizeCh, stopHandleSignalCh)
98 | } else {
99 | p.completion.Update(*p.buf.Document())
100 | p.renderer.Render(p.buf, p.completion)
101 | }
102 | case w := <-winSizeCh:
103 | p.renderer.UpdateWinSize(w)
104 | p.renderer.Render(p.buf, p.completion)
105 | case code := <-exitCh:
106 | p.renderer.BreakLine(p.buf)
107 | p.tearDown()
108 | os.Exit(code)
109 | default:
110 | time.Sleep(10 * time.Millisecond)
111 | }
112 | }
113 | }
114 |
115 | func (p *Prompt) feed(b []byte) (shouldExit bool, exec *Exec) {
116 | key := GetKey(b)
117 | p.buf.lastKeyStroke = key
118 | // completion
119 | completing := p.completion.Completing()
120 | p.handleCompletionKeyBinding(key, completing)
121 |
122 | switch key {
123 | case Enter, ControlJ, ControlM:
124 | p.renderer.BreakLine(p.buf)
125 |
126 | exec = &Exec{input: p.buf.Text()}
127 | p.buf = NewBuffer()
128 | if exec.input != "" {
129 | p.history.Add(exec.input)
130 | }
131 | case ControlC:
132 | p.renderer.BreakLine(p.buf)
133 | p.buf = NewBuffer()
134 | p.history.Clear()
135 | case Up, ControlP:
136 | if !completing { // Don't use p.completion.Completing() because it takes double operation when switch to selected=-1.
137 | if newBuf, changed := p.history.Older(p.buf); changed {
138 | p.buf = newBuf
139 | }
140 | }
141 | case Down, ControlN:
142 | if !completing { // Don't use p.completion.Completing() because it takes double operation when switch to selected=-1.
143 | if newBuf, changed := p.history.Newer(p.buf); changed {
144 | p.buf = newBuf
145 | }
146 | return
147 | }
148 | case ControlD:
149 | if p.buf.Text() == "" {
150 | shouldExit = true
151 | return
152 | }
153 | case NotDefined:
154 | if p.handleASCIICodeBinding(b) {
155 | return
156 | }
157 | p.buf.InsertText(string(b), false, true)
158 | }
159 |
160 | shouldExit = p.handleKeyBinding(key)
161 | return
162 | }
163 |
164 | func (p *Prompt) handleCompletionKeyBinding(key Key, completing bool) {
165 | switch key {
166 | case Down:
167 | if completing || p.completionOnDown {
168 | p.completion.Next()
169 | }
170 | case Tab, ControlI:
171 | p.completion.Next()
172 | case Up:
173 | if completing {
174 | p.completion.Previous()
175 | }
176 | case BackTab:
177 | p.completion.Previous()
178 | default:
179 | if s, ok := p.completion.GetSelectedSuggestion(); ok {
180 | w := p.buf.Document().GetWordBeforeCursorUntilSeparator(p.completion.wordSeparator)
181 | if w != "" {
182 | p.buf.DeleteBeforeCursor(len([]rune(w)))
183 | }
184 | p.buf.InsertText(s.Text, false, true)
185 | }
186 | p.completion.Reset()
187 | }
188 | }
189 |
190 | func (p *Prompt) handleKeyBinding(key Key) bool {
191 | shouldExit := false
192 | for i := range commonKeyBindings {
193 | kb := commonKeyBindings[i]
194 | if kb.Key == key {
195 | kb.Fn(p.buf)
196 | }
197 | }
198 |
199 | if p.keyBindMode == EmacsKeyBind {
200 | for i := range emacsKeyBindings {
201 | kb := emacsKeyBindings[i]
202 | if kb.Key == key {
203 | kb.Fn(p.buf)
204 | }
205 | }
206 | }
207 |
208 | // Custom key bindings
209 | for i := range p.keyBindings {
210 | kb := p.keyBindings[i]
211 | if kb.Key == key {
212 | kb.Fn(p.buf)
213 | }
214 | }
215 | if p.exitChecker != nil && p.exitChecker(p.buf.Text(), false) {
216 | shouldExit = true
217 | }
218 | return shouldExit
219 | }
220 |
221 | func (p *Prompt) handleASCIICodeBinding(b []byte) bool {
222 | checked := false
223 | for _, kb := range p.ASCIICodeBindings {
224 | if bytes.Equal(kb.ASCIICode, b) {
225 | kb.Fn(p.buf)
226 | checked = true
227 | }
228 | }
229 | return checked
230 | }
231 |
232 | // Input just returns user input text.
233 | func (p *Prompt) Input() string {
234 | defer debug.Teardown()
235 | debug.Log("start prompt")
236 | p.setUp()
237 | defer p.tearDown()
238 |
239 | if p.completion.showAtStart {
240 | p.completion.Update(*p.buf.Document())
241 | }
242 |
243 | p.renderer.Render(p.buf, p.completion)
244 | bufCh := make(chan []byte, 128)
245 | stopReadBufCh := make(chan struct{})
246 | go p.readBuffer(bufCh, stopReadBufCh)
247 |
248 | for {
249 | select {
250 | case b := <-bufCh:
251 | if shouldExit, e := p.feed(b); shouldExit {
252 | p.renderer.BreakLine(p.buf)
253 | stopReadBufCh <- struct{}{}
254 | return ""
255 | } else if e != nil {
256 | // Stop goroutine to run readBuffer function
257 | stopReadBufCh <- struct{}{}
258 | return e.input
259 | } else {
260 | p.completion.Update(*p.buf.Document())
261 | p.renderer.Render(p.buf, p.completion)
262 | }
263 | default:
264 | time.Sleep(10 * time.Millisecond)
265 | }
266 | }
267 | }
268 |
269 | func (p *Prompt) readBuffer(bufCh chan []byte, stopCh chan struct{}) {
270 | debug.Log("start reading buffer")
271 | for {
272 | select {
273 | case <-stopCh:
274 | debug.Log("stop reading buffer")
275 | return
276 | default:
277 | if b, err := p.in.Read(); err == nil && !(len(b) == 1 && b[0] == 0) {
278 | bufCh <- b
279 | }
280 | }
281 | time.Sleep(10 * time.Millisecond)
282 | }
283 | }
284 |
285 | func (p *Prompt) setUp() {
286 | debug.AssertNoError(p.in.Setup())
287 | p.renderer.Setup()
288 | p.renderer.UpdateWinSize(p.in.GetWinSize())
289 | }
290 |
291 | func (p *Prompt) tearDown() {
292 | if !p.skipTearDown {
293 | debug.AssertNoError(p.in.TearDown())
294 | }
295 | p.renderer.TearDown()
296 | }
297 |
--------------------------------------------------------------------------------
/output_vt100.go:
--------------------------------------------------------------------------------
1 | package prompt
2 |
3 | import (
4 | "bytes"
5 | "strconv"
6 | )
7 |
8 | // VT100Writer generates VT100 escape sequences.
9 | type VT100Writer struct {
10 | buffer []byte
11 | }
12 |
13 | // WriteRaw to write raw byte array
14 | func (w *VT100Writer) WriteRaw(data []byte) {
15 | w.buffer = append(w.buffer, data...)
16 | }
17 |
18 | // Write to write safety byte array by removing control sequences.
19 | func (w *VT100Writer) Write(data []byte) {
20 | w.WriteRaw(bytes.Replace(data, []byte{0x1b}, []byte{'?'}, -1))
21 | }
22 |
23 | // WriteRawStr to write raw string
24 | func (w *VT100Writer) WriteRawStr(data string) {
25 | w.WriteRaw([]byte(data))
26 | }
27 |
28 | // WriteStr to write safety string by removing control sequences.
29 | func (w *VT100Writer) WriteStr(data string) {
30 | w.Write([]byte(data))
31 | }
32 |
33 | /* Erase */
34 |
35 | // EraseScreen erases the screen with the background colour and moves the cursor to home.
36 | func (w *VT100Writer) EraseScreen() {
37 | w.WriteRaw([]byte{0x1b, '[', '2', 'J'})
38 | }
39 |
40 | // EraseUp erases the screen from the current line up to the top of the screen.
41 | func (w *VT100Writer) EraseUp() {
42 | w.WriteRaw([]byte{0x1b, '[', '1', 'J'})
43 | }
44 |
45 | // EraseDown erases the screen from the current line down to the bottom of the screen.
46 | func (w *VT100Writer) EraseDown() {
47 | w.WriteRaw([]byte{0x1b, '[', 'J'})
48 | }
49 |
50 | // EraseStartOfLine erases from the current cursor position to the start of the current line.
51 | func (w *VT100Writer) EraseStartOfLine() {
52 | w.WriteRaw([]byte{0x1b, '[', '1', 'K'})
53 | }
54 |
55 | // EraseEndOfLine erases from the current cursor position to the end of the current line.
56 | func (w *VT100Writer) EraseEndOfLine() {
57 | w.WriteRaw([]byte{0x1b, '[', 'K'})
58 | }
59 |
60 | // EraseLine erases the entire current line.
61 | func (w *VT100Writer) EraseLine() {
62 | w.WriteRaw([]byte{0x1b, '[', '2', 'K'})
63 | }
64 |
65 | /* Cursor */
66 |
67 | // ShowCursor stops blinking cursor and show.
68 | func (w *VT100Writer) ShowCursor() {
69 | w.WriteRaw([]byte{0x1b, '[', '?', '1', '2', 'l', 0x1b, '[', '?', '2', '5', 'h'})
70 | }
71 |
72 | // HideCursor hides cursor.
73 | func (w *VT100Writer) HideCursor() {
74 | w.WriteRaw([]byte{0x1b, '[', '?', '2', '5', 'l'})
75 | }
76 |
77 | // CursorGoTo sets the cursor position where subsequent text will begin.
78 | func (w *VT100Writer) CursorGoTo(row, col int) {
79 | if row == 0 && col == 0 {
80 | // If no row/column parameters are provided (ie. [H), the cursor will move to the home position.
81 | w.WriteRaw([]byte{0x1b, '[', 'H'})
82 | return
83 | }
84 | r := strconv.Itoa(row)
85 | c := strconv.Itoa(col)
86 | w.WriteRaw([]byte{0x1b, '['})
87 | w.WriteRaw([]byte(r))
88 | w.WriteRaw([]byte{';'})
89 | w.WriteRaw([]byte(c))
90 | w.WriteRaw([]byte{'H'})
91 | }
92 |
93 | // CursorUp moves the cursor up by 'n' rows; the default count is 1.
94 | func (w *VT100Writer) CursorUp(n int) {
95 | if n == 0 {
96 | return
97 | } else if n < 0 {
98 | w.CursorDown(-n)
99 | return
100 | }
101 | s := strconv.Itoa(n)
102 | w.WriteRaw([]byte{0x1b, '['})
103 | w.WriteRaw([]byte(s))
104 | w.WriteRaw([]byte{'A'})
105 | }
106 |
107 | // CursorDown moves the cursor down by 'n' rows; the default count is 1.
108 | func (w *VT100Writer) CursorDown(n int) {
109 | if n == 0 {
110 | return
111 | } else if n < 0 {
112 | w.CursorUp(-n)
113 | return
114 | }
115 | s := strconv.Itoa(n)
116 | w.WriteRaw([]byte{0x1b, '['})
117 | w.WriteRaw([]byte(s))
118 | w.WriteRaw([]byte{'B'})
119 | }
120 |
121 | // CursorForward moves the cursor forward by 'n' columns; the default count is 1.
122 | func (w *VT100Writer) CursorForward(n int) {
123 | if n == 0 {
124 | return
125 | } else if n < 0 {
126 | w.CursorBackward(-n)
127 | return
128 | }
129 | s := strconv.Itoa(n)
130 | w.WriteRaw([]byte{0x1b, '['})
131 | w.WriteRaw([]byte(s))
132 | w.WriteRaw([]byte{'C'})
133 | }
134 |
135 | // CursorBackward moves the cursor backward by 'n' columns; the default count is 1.
136 | func (w *VT100Writer) CursorBackward(n int) {
137 | if n == 0 {
138 | return
139 | } else if n < 0 {
140 | w.CursorForward(-n)
141 | return
142 | }
143 | s := strconv.Itoa(n)
144 | w.WriteRaw([]byte{0x1b, '['})
145 | w.WriteRaw([]byte(s))
146 | w.WriteRaw([]byte{'D'})
147 | }
148 |
149 | // AskForCPR asks for a cursor position report (CPR).
150 | func (w *VT100Writer) AskForCPR() {
151 | // CPR: Cursor Position Request.
152 | w.WriteRaw([]byte{0x1b, '[', '6', 'n'})
153 | }
154 |
155 | // SaveCursor saves current cursor position.
156 | func (w *VT100Writer) SaveCursor() {
157 | w.WriteRaw([]byte{0x1b, '[', 's'})
158 | }
159 |
160 | // UnSaveCursor restores cursor position after a Save Cursor.
161 | func (w *VT100Writer) UnSaveCursor() {
162 | w.WriteRaw([]byte{0x1b, '[', 'u'})
163 | }
164 |
165 | /* Scrolling */
166 |
167 | // ScrollDown scrolls display down one line.
168 | func (w *VT100Writer) ScrollDown() {
169 | w.WriteRaw([]byte{0x1b, 'D'})
170 | }
171 |
172 | // ScrollUp scroll display up one line.
173 | func (w *VT100Writer) ScrollUp() {
174 | w.WriteRaw([]byte{0x1b, 'M'})
175 | }
176 |
177 | /* Title */
178 |
179 | // SetTitle sets a title of terminal window.
180 | func (w *VT100Writer) SetTitle(title string) {
181 | titleBytes := []byte(title)
182 | patterns := []struct {
183 | from []byte
184 | to []byte
185 | }{
186 | {
187 | from: []byte{0x13},
188 | to: []byte{},
189 | },
190 | {
191 | from: []byte{0x07},
192 | to: []byte{},
193 | },
194 | }
195 | for i := range patterns {
196 | titleBytes = bytes.Replace(titleBytes, patterns[i].from, patterns[i].to, -1)
197 | }
198 |
199 | w.WriteRaw([]byte{0x1b, ']', '2', ';'})
200 | w.WriteRaw(titleBytes)
201 | w.WriteRaw([]byte{0x07})
202 | }
203 |
204 | // ClearTitle clears a title of terminal window.
205 | func (w *VT100Writer) ClearTitle() {
206 | w.WriteRaw([]byte{0x1b, ']', '2', ';', 0x07})
207 | }
208 |
209 | /* Font */
210 |
211 | // SetColor sets text and background colors. and specify whether text is bold.
212 | func (w *VT100Writer) SetColor(fg, bg Color, bold bool) {
213 | if bold {
214 | w.SetDisplayAttributes(fg, bg, DisplayBold)
215 | } else {
216 | // If using `DisplayDefualt`, it will be broken in some environment.
217 | // Details are https://github.com/c-bata/go-prompt/pull/85
218 | w.SetDisplayAttributes(fg, bg, DisplayReset)
219 | }
220 | }
221 |
222 | // SetDisplayAttributes to set VT100 display attributes.
223 | func (w *VT100Writer) SetDisplayAttributes(fg, bg Color, attrs ...DisplayAttribute) {
224 | w.WriteRaw([]byte{0x1b, '['}) // control sequence introducer
225 | defer w.WriteRaw([]byte{'m'}) // final character
226 |
227 | var separator byte = ';'
228 | for i := range attrs {
229 | p, ok := displayAttributeParameters[attrs[i]]
230 | if !ok {
231 | continue
232 | }
233 | w.WriteRaw(p)
234 | w.WriteRaw([]byte{separator})
235 | }
236 |
237 | f, ok := foregroundANSIColors[fg]
238 | if !ok {
239 | f = foregroundANSIColors[DefaultColor]
240 | }
241 | w.WriteRaw(f)
242 | w.WriteRaw([]byte{separator})
243 | b, ok := backgroundANSIColors[bg]
244 | if !ok {
245 | b = backgroundANSIColors[DefaultColor]
246 | }
247 | w.WriteRaw(b)
248 | }
249 |
250 | var displayAttributeParameters = map[DisplayAttribute][]byte{
251 | DisplayReset: {'0'},
252 | DisplayBold: {'1'},
253 | DisplayLowIntensity: {'2'},
254 | DisplayItalic: {'3'},
255 | DisplayUnderline: {'4'},
256 | DisplayBlink: {'5'},
257 | DisplayRapidBlink: {'6'},
258 | DisplayReverse: {'7'},
259 | DisplayInvisible: {'8'},
260 | DisplayCrossedOut: {'9'},
261 | DisplayDefaultFont: {'1', '0'},
262 | }
263 |
264 | var foregroundANSIColors = map[Color][]byte{
265 | DefaultColor: {'3', '9'},
266 |
267 | // Low intensity.
268 | Black: {'3', '0'},
269 | DarkRed: {'3', '1'},
270 | DarkGreen: {'3', '2'},
271 | Brown: {'3', '3'},
272 | DarkBlue: {'3', '4'},
273 | Purple: {'3', '5'},
274 | Cyan: {'3', '6'},
275 | LightGray: {'3', '7'},
276 |
277 | // High intensity.
278 | DarkGray: {'9', '0'},
279 | Red: {'9', '1'},
280 | Green: {'9', '2'},
281 | Yellow: {'9', '3'},
282 | Blue: {'9', '4'},
283 | Fuchsia: {'9', '5'},
284 | Turquoise: {'9', '6'},
285 | White: {'9', '7'},
286 | }
287 |
288 | var backgroundANSIColors = map[Color][]byte{
289 | DefaultColor: {'4', '9'},
290 |
291 | // Low intensity.
292 | Black: {'4', '0'},
293 | DarkRed: {'4', '1'},
294 | DarkGreen: {'4', '2'},
295 | Brown: {'4', '3'},
296 | DarkBlue: {'4', '4'},
297 | Purple: {'4', '5'},
298 | Cyan: {'4', '6'},
299 | LightGray: {'4', '7'},
300 |
301 | // High intensity
302 | DarkGray: {'1', '0', '0'},
303 | Red: {'1', '0', '1'},
304 | Green: {'1', '0', '2'},
305 | Yellow: {'1', '0', '3'},
306 | Blue: {'1', '0', '4'},
307 | Fuchsia: {'1', '0', '5'},
308 | Turquoise: {'1', '0', '6'},
309 | White: {'1', '0', '7'},
310 | }
311 |
--------------------------------------------------------------------------------
/render.go:
--------------------------------------------------------------------------------
1 | package prompt
2 |
3 | import (
4 | "runtime"
5 |
6 | "github.com/c-bata/go-prompt/internal/debug"
7 | runewidth "github.com/mattn/go-runewidth"
8 | )
9 |
10 | // Render to render prompt information from state of Buffer.
11 | type Render struct {
12 | out ConsoleWriter
13 | prefix string
14 | livePrefixCallback func() (prefix string, useLivePrefix bool)
15 | breakLineCallback func(*Document)
16 | title string
17 | row uint16
18 | col uint16
19 |
20 | previousCursor int
21 |
22 | // colors,
23 | prefixTextColor Color
24 | prefixBGColor Color
25 | inputTextColor Color
26 | inputBGColor Color
27 | previewSuggestionTextColor Color
28 | previewSuggestionBGColor Color
29 | suggestionTextColor Color
30 | suggestionBGColor Color
31 | selectedSuggestionTextColor Color
32 | selectedSuggestionBGColor Color
33 | descriptionTextColor Color
34 | descriptionBGColor Color
35 | selectedDescriptionTextColor Color
36 | selectedDescriptionBGColor Color
37 | scrollbarThumbColor Color
38 | scrollbarBGColor Color
39 | }
40 |
41 | // Setup to initialize console output.
42 | func (r *Render) Setup() {
43 | if r.title != "" {
44 | r.out.SetTitle(r.title)
45 | debug.AssertNoError(r.out.Flush())
46 | }
47 | }
48 |
49 | // getCurrentPrefix to get current prefix.
50 | // If live-prefix is enabled, return live-prefix.
51 | func (r *Render) getCurrentPrefix() string {
52 | if prefix, ok := r.livePrefixCallback(); ok {
53 | return prefix
54 | }
55 | return r.prefix
56 | }
57 |
58 | func (r *Render) renderPrefix() {
59 | r.out.SetColor(r.prefixTextColor, r.prefixBGColor, false)
60 | r.out.WriteStr(r.getCurrentPrefix())
61 | r.out.SetColor(DefaultColor, DefaultColor, false)
62 | }
63 |
64 | // TearDown to clear title and erasing.
65 | func (r *Render) TearDown() {
66 | r.out.ClearTitle()
67 | r.out.EraseDown()
68 | debug.AssertNoError(r.out.Flush())
69 | }
70 |
71 | func (r *Render) prepareArea(lines int) {
72 | for i := 0; i < lines; i++ {
73 | r.out.ScrollDown()
74 | }
75 | for i := 0; i < lines; i++ {
76 | r.out.ScrollUp()
77 | }
78 | }
79 |
80 | // UpdateWinSize called when window size is changed.
81 | func (r *Render) UpdateWinSize(ws *WinSize) {
82 | r.row = ws.Row
83 | r.col = ws.Col
84 | }
85 |
86 | func (r *Render) renderWindowTooSmall() {
87 | r.out.CursorGoTo(0, 0)
88 | r.out.EraseScreen()
89 | r.out.SetColor(DarkRed, White, false)
90 | r.out.WriteStr("Your console window is too small...")
91 | }
92 |
93 | func (r *Render) renderCompletion(buf *Buffer, completions *CompletionManager) {
94 | suggestions := completions.GetSuggestions()
95 | if len(completions.GetSuggestions()) == 0 {
96 | return
97 | }
98 | prefix := r.getCurrentPrefix()
99 | formatted, width := formatSuggestions(
100 | suggestions,
101 | int(r.col)-runewidth.StringWidth(prefix)-1, // -1 means a width of scrollbar
102 | )
103 | // +1 means a width of scrollbar.
104 | width++
105 |
106 | windowHeight := len(formatted)
107 | if windowHeight > int(completions.max) {
108 | windowHeight = int(completions.max)
109 | }
110 | formatted = formatted[completions.verticalScroll : completions.verticalScroll+windowHeight]
111 | r.prepareArea(windowHeight)
112 |
113 | cursor := runewidth.StringWidth(prefix) + runewidth.StringWidth(buf.Document().TextBeforeCursor())
114 | x, _ := r.toPos(cursor)
115 | if x+width >= int(r.col) {
116 | cursor = r.backward(cursor, x+width-int(r.col))
117 | }
118 |
119 | contentHeight := len(completions.tmp)
120 |
121 | fractionVisible := float64(windowHeight) / float64(contentHeight)
122 | fractionAbove := float64(completions.verticalScroll) / float64(contentHeight)
123 |
124 | scrollbarHeight := int(clamp(float64(windowHeight), 1, float64(windowHeight)*fractionVisible))
125 | scrollbarTop := int(float64(windowHeight) * fractionAbove)
126 |
127 | isScrollThumb := func(row int) bool {
128 | return scrollbarTop <= row && row <= scrollbarTop+scrollbarHeight
129 | }
130 |
131 | selected := completions.selected - completions.verticalScroll
132 | r.out.SetColor(White, Cyan, false)
133 | for i := 0; i < windowHeight; i++ {
134 | r.out.CursorDown(1)
135 | if i == selected {
136 | r.out.SetColor(r.selectedSuggestionTextColor, r.selectedSuggestionBGColor, true)
137 | } else {
138 | r.out.SetColor(r.suggestionTextColor, r.suggestionBGColor, false)
139 | }
140 | r.out.WriteStr(formatted[i].Text)
141 |
142 | if i == selected {
143 | r.out.SetColor(r.selectedDescriptionTextColor, r.selectedDescriptionBGColor, false)
144 | } else {
145 | r.out.SetColor(r.descriptionTextColor, r.descriptionBGColor, false)
146 | }
147 | r.out.WriteStr(formatted[i].Description)
148 |
149 | if isScrollThumb(i) {
150 | r.out.SetColor(DefaultColor, r.scrollbarThumbColor, false)
151 | } else {
152 | r.out.SetColor(DefaultColor, r.scrollbarBGColor, false)
153 | }
154 | r.out.WriteStr(" ")
155 | r.out.SetColor(DefaultColor, DefaultColor, false)
156 |
157 | r.lineWrap(cursor + width)
158 | r.backward(cursor+width, width)
159 | }
160 |
161 | if x+width >= int(r.col) {
162 | r.out.CursorForward(x + width - int(r.col))
163 | }
164 |
165 | r.out.CursorUp(windowHeight)
166 | r.out.SetColor(DefaultColor, DefaultColor, false)
167 | }
168 |
169 | // Render renders to the console.
170 | func (r *Render) Render(buffer *Buffer, completion *CompletionManager) {
171 | // In situations where a pseudo tty is allocated (e.g. within a docker container),
172 | // window size via TIOCGWINSZ is not immediately available and will result in 0,0 dimensions.
173 | if r.col == 0 {
174 | return
175 | }
176 | defer func() { debug.AssertNoError(r.out.Flush()) }()
177 | r.move(r.previousCursor, 0)
178 |
179 | line := buffer.Text()
180 | prefix := r.getCurrentPrefix()
181 | cursor := runewidth.StringWidth(prefix) + runewidth.StringWidth(line)
182 |
183 | // prepare area
184 | _, y := r.toPos(cursor)
185 |
186 | h := y + 1 + int(completion.max)
187 | if h > int(r.row) || completionMargin > int(r.col) {
188 | r.renderWindowTooSmall()
189 | return
190 | }
191 |
192 | // Rendering
193 | r.out.HideCursor()
194 | defer r.out.ShowCursor()
195 |
196 | r.renderPrefix()
197 | r.out.SetColor(r.inputTextColor, r.inputBGColor, false)
198 | r.out.WriteStr(line)
199 | r.out.SetColor(DefaultColor, DefaultColor, false)
200 | r.lineWrap(cursor)
201 |
202 | r.out.EraseDown()
203 |
204 | cursor = r.backward(cursor, runewidth.StringWidth(line)-buffer.DisplayCursorPosition())
205 |
206 | r.renderCompletion(buffer, completion)
207 | if suggest, ok := completion.GetSelectedSuggestion(); ok {
208 | cursor = r.backward(cursor, runewidth.StringWidth(buffer.Document().GetWordBeforeCursorUntilSeparator(completion.wordSeparator)))
209 |
210 | r.out.SetColor(r.previewSuggestionTextColor, r.previewSuggestionBGColor, false)
211 | r.out.WriteStr(suggest.Text)
212 | r.out.SetColor(DefaultColor, DefaultColor, false)
213 | cursor += runewidth.StringWidth(suggest.Text)
214 |
215 | rest := buffer.Document().TextAfterCursor()
216 | r.out.WriteStr(rest)
217 | cursor += runewidth.StringWidth(rest)
218 | r.lineWrap(cursor)
219 |
220 | cursor = r.backward(cursor, runewidth.StringWidth(rest))
221 | }
222 | r.previousCursor = cursor
223 | }
224 |
225 | // BreakLine to break line.
226 | func (r *Render) BreakLine(buffer *Buffer) {
227 | // Erasing and Render
228 | cursor := runewidth.StringWidth(buffer.Document().TextBeforeCursor()) + runewidth.StringWidth(r.getCurrentPrefix())
229 | r.clear(cursor)
230 | r.renderPrefix()
231 | r.out.SetColor(r.inputTextColor, r.inputBGColor, false)
232 | r.out.WriteStr(buffer.Document().Text + "\n")
233 | r.out.SetColor(DefaultColor, DefaultColor, false)
234 | debug.AssertNoError(r.out.Flush())
235 | if r.breakLineCallback != nil {
236 | r.breakLineCallback(buffer.Document())
237 | }
238 |
239 | r.previousCursor = 0
240 | }
241 |
242 | // clear erases the screen from a beginning of input
243 | // even if there is line break which means input length exceeds a window's width.
244 | func (r *Render) clear(cursor int) {
245 | r.move(cursor, 0)
246 | r.out.EraseDown()
247 | }
248 |
249 | // backward moves cursor to backward from a current cursor position
250 | // regardless there is a line break.
251 | func (r *Render) backward(from, n int) int {
252 | return r.move(from, from-n)
253 | }
254 |
255 | // move moves cursor to specified position from the beginning of input
256 | // even if there is a line break.
257 | func (r *Render) move(from, to int) int {
258 | fromX, fromY := r.toPos(from)
259 | toX, toY := r.toPos(to)
260 |
261 | r.out.CursorUp(fromY - toY)
262 | r.out.CursorBackward(fromX - toX)
263 | return to
264 | }
265 |
266 | // toPos returns the relative position from the beginning of the string.
267 | func (r *Render) toPos(cursor int) (x, y int) {
268 | col := int(r.col)
269 | return cursor % col, cursor / col
270 | }
271 |
272 | func (r *Render) lineWrap(cursor int) {
273 | if runtime.GOOS != "windows" && cursor > 0 && cursor%int(r.col) == 0 {
274 | r.out.WriteRaw([]byte{'\n'})
275 | }
276 | }
277 |
278 | func clamp(high, low, x float64) float64 {
279 | switch {
280 | case high < x:
281 | return high
282 | case x < low:
283 | return low
284 | default:
285 | return x
286 | }
287 | }
288 |
--------------------------------------------------------------------------------
/option.go:
--------------------------------------------------------------------------------
1 | package prompt
2 |
3 | // Option is the type to replace default parameters.
4 | // prompt.New accepts any number of options (this is functional option pattern).
5 | type Option func(prompt *Prompt) error
6 |
7 | // OptionParser to set a custom ConsoleParser object. An argument should implement ConsoleParser interface.
8 | func OptionParser(x ConsoleParser) Option {
9 | return func(p *Prompt) error {
10 | p.in = x
11 | return nil
12 | }
13 | }
14 |
15 | // OptionWriter to set a custom ConsoleWriter object. An argument should implement ConsoleWriter interface.
16 | func OptionWriter(x ConsoleWriter) Option {
17 | return func(p *Prompt) error {
18 | registerConsoleWriter(x)
19 | p.renderer.out = x
20 | return nil
21 | }
22 | }
23 |
24 | // OptionTitle to set title displayed at the header bar of terminal.
25 | func OptionTitle(x string) Option {
26 | return func(p *Prompt) error {
27 | p.renderer.title = x
28 | return nil
29 | }
30 | }
31 |
32 | // OptionPrefix to set prefix string.
33 | func OptionPrefix(x string) Option {
34 | return func(p *Prompt) error {
35 | p.renderer.prefix = x
36 | return nil
37 | }
38 | }
39 |
40 | // OptionInitialBufferText to set the initial buffer text
41 | func OptionInitialBufferText(x string) Option {
42 | return func(p *Prompt) error {
43 | p.buf.InsertText(x, false, true)
44 | return nil
45 | }
46 | }
47 |
48 | // OptionCompletionWordSeparator to set word separators. Enable only ' ' if empty.
49 | func OptionCompletionWordSeparator(x string) Option {
50 | return func(p *Prompt) error {
51 | p.completion.wordSeparator = x
52 | return nil
53 | }
54 | }
55 |
56 | // OptionLivePrefix to change the prefix dynamically by callback function
57 | func OptionLivePrefix(f func() (prefix string, useLivePrefix bool)) Option {
58 | return func(p *Prompt) error {
59 | p.renderer.livePrefixCallback = f
60 | return nil
61 | }
62 | }
63 |
64 | // OptionPrefixTextColor change a text color of prefix string
65 | func OptionPrefixTextColor(x Color) Option {
66 | return func(p *Prompt) error {
67 | p.renderer.prefixTextColor = x
68 | return nil
69 | }
70 | }
71 |
72 | // OptionPrefixBackgroundColor to change a background color of prefix string
73 | func OptionPrefixBackgroundColor(x Color) Option {
74 | return func(p *Prompt) error {
75 | p.renderer.prefixBGColor = x
76 | return nil
77 | }
78 | }
79 |
80 | // OptionInputTextColor to change a color of text which is input by user
81 | func OptionInputTextColor(x Color) Option {
82 | return func(p *Prompt) error {
83 | p.renderer.inputTextColor = x
84 | return nil
85 | }
86 | }
87 |
88 | // OptionInputBGColor to change a color of background which is input by user
89 | func OptionInputBGColor(x Color) Option {
90 | return func(p *Prompt) error {
91 | p.renderer.inputBGColor = x
92 | return nil
93 | }
94 | }
95 |
96 | // OptionPreviewSuggestionTextColor to change a text color which is completed
97 | func OptionPreviewSuggestionTextColor(x Color) Option {
98 | return func(p *Prompt) error {
99 | p.renderer.previewSuggestionTextColor = x
100 | return nil
101 | }
102 | }
103 |
104 | // OptionPreviewSuggestionBGColor to change a background color which is completed
105 | func OptionPreviewSuggestionBGColor(x Color) Option {
106 | return func(p *Prompt) error {
107 | p.renderer.previewSuggestionBGColor = x
108 | return nil
109 | }
110 | }
111 |
112 | // OptionSuggestionTextColor to change a text color in drop down suggestions.
113 | func OptionSuggestionTextColor(x Color) Option {
114 | return func(p *Prompt) error {
115 | p.renderer.suggestionTextColor = x
116 | return nil
117 | }
118 | }
119 |
120 | // OptionSuggestionBGColor change a background color in drop down suggestions.
121 | func OptionSuggestionBGColor(x Color) Option {
122 | return func(p *Prompt) error {
123 | p.renderer.suggestionBGColor = x
124 | return nil
125 | }
126 | }
127 |
128 | // OptionSelectedSuggestionTextColor to change a text color for completed text which is selected inside suggestions drop down box.
129 | func OptionSelectedSuggestionTextColor(x Color) Option {
130 | return func(p *Prompt) error {
131 | p.renderer.selectedSuggestionTextColor = x
132 | return nil
133 | }
134 | }
135 |
136 | // OptionSelectedSuggestionBGColor to change a background color for completed text which is selected inside suggestions drop down box.
137 | func OptionSelectedSuggestionBGColor(x Color) Option {
138 | return func(p *Prompt) error {
139 | p.renderer.selectedSuggestionBGColor = x
140 | return nil
141 | }
142 | }
143 |
144 | // OptionDescriptionTextColor to change a background color of description text in drop down suggestions.
145 | func OptionDescriptionTextColor(x Color) Option {
146 | return func(p *Prompt) error {
147 | p.renderer.descriptionTextColor = x
148 | return nil
149 | }
150 | }
151 |
152 | // OptionDescriptionBGColor to change a background color of description text in drop down suggestions.
153 | func OptionDescriptionBGColor(x Color) Option {
154 | return func(p *Prompt) error {
155 | p.renderer.descriptionBGColor = x
156 | return nil
157 | }
158 | }
159 |
160 | // OptionSelectedDescriptionTextColor to change a text color of description which is selected inside suggestions drop down box.
161 | func OptionSelectedDescriptionTextColor(x Color) Option {
162 | return func(p *Prompt) error {
163 | p.renderer.selectedDescriptionTextColor = x
164 | return nil
165 | }
166 | }
167 |
168 | // OptionSelectedDescriptionBGColor to change a background color of description which is selected inside suggestions drop down box.
169 | func OptionSelectedDescriptionBGColor(x Color) Option {
170 | return func(p *Prompt) error {
171 | p.renderer.selectedDescriptionBGColor = x
172 | return nil
173 | }
174 | }
175 |
176 | // OptionScrollbarThumbColor to change a thumb color on scrollbar.
177 | func OptionScrollbarThumbColor(x Color) Option {
178 | return func(p *Prompt) error {
179 | p.renderer.scrollbarThumbColor = x
180 | return nil
181 | }
182 | }
183 |
184 | // OptionScrollbarBGColor to change a background color of scrollbar.
185 | func OptionScrollbarBGColor(x Color) Option {
186 | return func(p *Prompt) error {
187 | p.renderer.scrollbarBGColor = x
188 | return nil
189 | }
190 | }
191 |
192 | // OptionMaxSuggestion specify the max number of displayed suggestions.
193 | func OptionMaxSuggestion(x uint16) Option {
194 | return func(p *Prompt) error {
195 | p.completion.max = x
196 | return nil
197 | }
198 | }
199 |
200 | // OptionHistory to set history expressed by string array.
201 | func OptionHistory(x []string) Option {
202 | return func(p *Prompt) error {
203 | p.history.histories = x
204 | p.history.Clear()
205 | return nil
206 | }
207 | }
208 |
209 | // OptionSwitchKeyBindMode set a key bind mode.
210 | func OptionSwitchKeyBindMode(m KeyBindMode) Option {
211 | return func(p *Prompt) error {
212 | p.keyBindMode = m
213 | return nil
214 | }
215 | }
216 |
217 | // OptionCompletionOnDown allows for Down arrow key to trigger completion.
218 | func OptionCompletionOnDown() Option {
219 | return func(p *Prompt) error {
220 | p.completionOnDown = true
221 | return nil
222 | }
223 | }
224 |
225 | // SwitchKeyBindMode to set a key bind mode.
226 | // Deprecated: Please use OptionSwitchKeyBindMode.
227 | var SwitchKeyBindMode = OptionSwitchKeyBindMode
228 |
229 | // OptionAddKeyBind to set a custom key bind.
230 | func OptionAddKeyBind(b ...KeyBind) Option {
231 | return func(p *Prompt) error {
232 | p.keyBindings = append(p.keyBindings, b...)
233 | return nil
234 | }
235 | }
236 |
237 | // OptionAddASCIICodeBind to set a custom key bind.
238 | func OptionAddASCIICodeBind(b ...ASCIICodeBind) Option {
239 | return func(p *Prompt) error {
240 | p.ASCIICodeBindings = append(p.ASCIICodeBindings, b...)
241 | return nil
242 | }
243 | }
244 |
245 | // OptionShowCompletionAtStart to set completion window is open at start.
246 | func OptionShowCompletionAtStart() Option {
247 | return func(p *Prompt) error {
248 | p.completion.showAtStart = true
249 | return nil
250 | }
251 | }
252 |
253 | // OptionBreakLineCallback to run a callback at every break line
254 | func OptionBreakLineCallback(fn func(*Document)) Option {
255 | return func(p *Prompt) error {
256 | p.renderer.breakLineCallback = fn
257 | return nil
258 | }
259 | }
260 |
261 | // OptionSetExitCheckerOnInput set an exit function which checks if go-prompt exits its Run loop
262 | func OptionSetExitCheckerOnInput(fn ExitChecker) Option {
263 | return func(p *Prompt) error {
264 | p.exitChecker = fn
265 | return nil
266 | }
267 | }
268 |
269 | // New returns a Prompt with powerful auto-completion.
270 | func New(executor Executor, completer Completer, opts ...Option) *Prompt {
271 | defaultWriter := NewStdoutWriter()
272 | registerConsoleWriter(defaultWriter)
273 |
274 | pt := &Prompt{
275 | in: NewStandardInputParser(),
276 | renderer: &Render{
277 | prefix: "> ",
278 | out: defaultWriter,
279 | livePrefixCallback: func() (string, bool) { return "", false },
280 | prefixTextColor: Blue,
281 | prefixBGColor: DefaultColor,
282 | inputTextColor: DefaultColor,
283 | inputBGColor: DefaultColor,
284 | previewSuggestionTextColor: Green,
285 | previewSuggestionBGColor: DefaultColor,
286 | suggestionTextColor: White,
287 | suggestionBGColor: Cyan,
288 | selectedSuggestionTextColor: Black,
289 | selectedSuggestionBGColor: Turquoise,
290 | descriptionTextColor: Black,
291 | descriptionBGColor: Turquoise,
292 | selectedDescriptionTextColor: White,
293 | selectedDescriptionBGColor: Cyan,
294 | scrollbarThumbColor: DarkGray,
295 | scrollbarBGColor: Cyan,
296 | },
297 | buf: NewBuffer(),
298 | executor: executor,
299 | history: NewHistory(),
300 | completion: NewCompletionManager(completer, 6),
301 | keyBindMode: EmacsKeyBind, // All the above assume that bash is running in the default Emacs setting
302 | }
303 |
304 | for _, opt := range opts {
305 | if err := opt(pt); err != nil {
306 | panic(err)
307 | }
308 | }
309 | return pt
310 | }
311 |
--------------------------------------------------------------------------------
/document.go:
--------------------------------------------------------------------------------
1 | package prompt
2 |
3 | import (
4 | "strings"
5 | "unicode/utf8"
6 |
7 | "github.com/c-bata/go-prompt/internal/bisect"
8 | istrings "github.com/c-bata/go-prompt/internal/strings"
9 | runewidth "github.com/mattn/go-runewidth"
10 | )
11 |
12 | // Document has text displayed in terminal and cursor position.
13 | type Document struct {
14 | Text string
15 | // This represents a index in a rune array of Document.Text.
16 | // So if Document is "日本(cursor)語", cursorPosition is 2.
17 | // But DisplayedCursorPosition returns 4 because '日' and '本' are double width characters.
18 | cursorPosition int
19 | lastKey Key
20 | }
21 |
22 | // NewDocument return the new empty document.
23 | func NewDocument() *Document {
24 | return &Document{
25 | Text: "",
26 | cursorPosition: 0,
27 | }
28 | }
29 |
30 | // LastKeyStroke return the last key pressed in this document.
31 | func (d *Document) LastKeyStroke() Key {
32 | return d.lastKey
33 | }
34 |
35 | // DisplayCursorPosition returns the cursor position on rendered text on terminal emulators.
36 | // So if Document is "日本(cursor)語", DisplayedCursorPosition returns 4 because '日' and '本' are double width characters.
37 | func (d *Document) DisplayCursorPosition() int {
38 | var position int
39 | runes := []rune(d.Text)[:d.cursorPosition]
40 | for i := range runes {
41 | position += runewidth.RuneWidth(runes[i])
42 | }
43 | return position
44 | }
45 |
46 | // GetCharRelativeToCursor return character relative to cursor position, or empty string
47 | func (d *Document) GetCharRelativeToCursor(offset int) (r rune) {
48 | s := d.Text
49 | cnt := 0
50 |
51 | for len(s) > 0 {
52 | cnt++
53 | r, size := utf8.DecodeRuneInString(s)
54 | if cnt == d.cursorPosition+offset {
55 | return r
56 | }
57 | s = s[size:]
58 | }
59 | return 0
60 | }
61 |
62 | // TextBeforeCursor returns the text before the cursor.
63 | func (d *Document) TextBeforeCursor() string {
64 | r := []rune(d.Text)
65 | return string(r[:d.cursorPosition])
66 | }
67 |
68 | // TextAfterCursor returns the text after the cursor.
69 | func (d *Document) TextAfterCursor() string {
70 | r := []rune(d.Text)
71 | return string(r[d.cursorPosition:])
72 | }
73 |
74 | // GetWordBeforeCursor returns the word before the cursor.
75 | // If we have whitespace before the cursor this returns an empty string.
76 | func (d *Document) GetWordBeforeCursor() string {
77 | x := d.TextBeforeCursor()
78 | return x[d.FindStartOfPreviousWord():]
79 | }
80 |
81 | // GetWordAfterCursor returns the word after the cursor.
82 | // If we have whitespace after the cursor this returns an empty string.
83 | func (d *Document) GetWordAfterCursor() string {
84 | x := d.TextAfterCursor()
85 | return x[:d.FindEndOfCurrentWord()]
86 | }
87 |
88 | // GetWordBeforeCursorWithSpace returns the word before the cursor.
89 | // Unlike GetWordBeforeCursor, it returns string containing space
90 | func (d *Document) GetWordBeforeCursorWithSpace() string {
91 | x := d.TextBeforeCursor()
92 | return x[d.FindStartOfPreviousWordWithSpace():]
93 | }
94 |
95 | // GetWordAfterCursorWithSpace returns the word after the cursor.
96 | // Unlike GetWordAfterCursor, it returns string containing space
97 | func (d *Document) GetWordAfterCursorWithSpace() string {
98 | x := d.TextAfterCursor()
99 | return x[:d.FindEndOfCurrentWordWithSpace()]
100 | }
101 |
102 | // GetWordBeforeCursorUntilSeparator returns the text before the cursor until next separator.
103 | func (d *Document) GetWordBeforeCursorUntilSeparator(sep string) string {
104 | x := d.TextBeforeCursor()
105 | return x[d.FindStartOfPreviousWordUntilSeparator(sep):]
106 | }
107 |
108 | // GetWordAfterCursorUntilSeparator returns the text after the cursor until next separator.
109 | func (d *Document) GetWordAfterCursorUntilSeparator(sep string) string {
110 | x := d.TextAfterCursor()
111 | return x[:d.FindEndOfCurrentWordUntilSeparator(sep)]
112 | }
113 |
114 | // GetWordBeforeCursorUntilSeparatorIgnoreNextToCursor returns the word before the cursor.
115 | // Unlike GetWordBeforeCursor, it returns string containing space
116 | func (d *Document) GetWordBeforeCursorUntilSeparatorIgnoreNextToCursor(sep string) string {
117 | x := d.TextBeforeCursor()
118 | return x[d.FindStartOfPreviousWordUntilSeparatorIgnoreNextToCursor(sep):]
119 | }
120 |
121 | // GetWordAfterCursorUntilSeparatorIgnoreNextToCursor returns the word after the cursor.
122 | // Unlike GetWordAfterCursor, it returns string containing space
123 | func (d *Document) GetWordAfterCursorUntilSeparatorIgnoreNextToCursor(sep string) string {
124 | x := d.TextAfterCursor()
125 | return x[:d.FindEndOfCurrentWordUntilSeparatorIgnoreNextToCursor(sep)]
126 | }
127 |
128 | // FindStartOfPreviousWord returns an index relative to the cursor position
129 | // pointing to the start of the previous word. Return 0 if nothing was found.
130 | func (d *Document) FindStartOfPreviousWord() int {
131 | x := d.TextBeforeCursor()
132 | i := strings.LastIndexByte(x, ' ')
133 | if i != -1 {
134 | return i + 1
135 | }
136 | return 0
137 | }
138 |
139 | // FindStartOfPreviousWordWithSpace is almost the same as FindStartOfPreviousWord.
140 | // The only difference is to ignore contiguous spaces.
141 | func (d *Document) FindStartOfPreviousWordWithSpace() int {
142 | x := d.TextBeforeCursor()
143 | end := istrings.LastIndexNotByte(x, ' ')
144 | if end == -1 {
145 | return 0
146 | }
147 |
148 | start := strings.LastIndexByte(x[:end], ' ')
149 | if start == -1 {
150 | return 0
151 | }
152 | return start + 1
153 | }
154 |
155 | // FindStartOfPreviousWordUntilSeparator is almost the same as FindStartOfPreviousWord.
156 | // But this can specify Separator. Return 0 if nothing was found.
157 | func (d *Document) FindStartOfPreviousWordUntilSeparator(sep string) int {
158 | if sep == "" {
159 | return d.FindStartOfPreviousWord()
160 | }
161 |
162 | x := d.TextBeforeCursor()
163 | i := strings.LastIndexAny(x, sep)
164 | if i != -1 {
165 | return i + 1
166 | }
167 | return 0
168 | }
169 |
170 | // FindStartOfPreviousWordUntilSeparatorIgnoreNextToCursor is almost the same as FindStartOfPreviousWordWithSpace.
171 | // But this can specify Separator. Return 0 if nothing was found.
172 | func (d *Document) FindStartOfPreviousWordUntilSeparatorIgnoreNextToCursor(sep string) int {
173 | if sep == "" {
174 | return d.FindStartOfPreviousWordWithSpace()
175 | }
176 |
177 | x := d.TextBeforeCursor()
178 | end := istrings.LastIndexNotAny(x, sep)
179 | if end == -1 {
180 | return 0
181 | }
182 | start := strings.LastIndexAny(x[:end], sep)
183 | if start == -1 {
184 | return 0
185 | }
186 | return start + 1
187 | }
188 |
189 | // FindEndOfCurrentWord returns an index relative to the cursor position.
190 | // pointing to the end of the current word. Return 0 if nothing was found.
191 | func (d *Document) FindEndOfCurrentWord() int {
192 | x := d.TextAfterCursor()
193 | i := strings.IndexByte(x, ' ')
194 | if i != -1 {
195 | return i
196 | }
197 | return len(x)
198 | }
199 |
200 | // FindEndOfCurrentWordWithSpace is almost the same as FindEndOfCurrentWord.
201 | // The only difference is to ignore contiguous spaces.
202 | func (d *Document) FindEndOfCurrentWordWithSpace() int {
203 | x := d.TextAfterCursor()
204 |
205 | start := istrings.IndexNotByte(x, ' ')
206 | if start == -1 {
207 | return len(x)
208 | }
209 |
210 | end := strings.IndexByte(x[start:], ' ')
211 | if end == -1 {
212 | return len(x)
213 | }
214 |
215 | return start + end
216 | }
217 |
218 | // FindEndOfCurrentWordUntilSeparator is almost the same as FindEndOfCurrentWord.
219 | // But this can specify Separator. Return 0 if nothing was found.
220 | func (d *Document) FindEndOfCurrentWordUntilSeparator(sep string) int {
221 | if sep == "" {
222 | return d.FindEndOfCurrentWord()
223 | }
224 |
225 | x := d.TextAfterCursor()
226 | i := strings.IndexAny(x, sep)
227 | if i != -1 {
228 | return i
229 | }
230 | return len(x)
231 | }
232 |
233 | // FindEndOfCurrentWordUntilSeparatorIgnoreNextToCursor is almost the same as FindEndOfCurrentWordWithSpace.
234 | // But this can specify Separator. Return 0 if nothing was found.
235 | func (d *Document) FindEndOfCurrentWordUntilSeparatorIgnoreNextToCursor(sep string) int {
236 | if sep == "" {
237 | return d.FindEndOfCurrentWordWithSpace()
238 | }
239 |
240 | x := d.TextAfterCursor()
241 |
242 | start := istrings.IndexNotAny(x, sep)
243 | if start == -1 {
244 | return len(x)
245 | }
246 |
247 | end := strings.IndexAny(x[start:], sep)
248 | if end == -1 {
249 | return len(x)
250 | }
251 |
252 | return start + end
253 | }
254 |
255 | // CurrentLineBeforeCursor returns the text from the start of the line until the cursor.
256 | func (d *Document) CurrentLineBeforeCursor() string {
257 | s := strings.Split(d.TextBeforeCursor(), "\n")
258 | return s[len(s)-1]
259 | }
260 |
261 | // CurrentLineAfterCursor returns the text from the cursor until the end of the line.
262 | func (d *Document) CurrentLineAfterCursor() string {
263 | return strings.Split(d.TextAfterCursor(), "\n")[0]
264 | }
265 |
266 | // CurrentLine return the text on the line where the cursor is. (when the input
267 | // consists of just one line, it equals `text`.
268 | func (d *Document) CurrentLine() string {
269 | return d.CurrentLineBeforeCursor() + d.CurrentLineAfterCursor()
270 | }
271 |
272 | // Array pointing to the start indexes of all the lines.
273 | func (d *Document) lineStartIndexes() []int {
274 | // TODO: Cache, because this is often reused.
275 | // (If it is used, it's often used many times.
276 | // And this has to be fast for editing big documents!)
277 | lc := d.LineCount()
278 | lengths := make([]int, lc)
279 | for i, l := range d.Lines() {
280 | lengths[i] = len(l)
281 | }
282 |
283 | // Calculate cumulative sums.
284 | indexes := make([]int, lc+1)
285 | indexes[0] = 0 // https://github.com/jonathanslenders/python-prompt-toolkit/blob/master/prompt_toolkit/document.py#L189
286 | pos := 0
287 | for i, l := range lengths {
288 | pos += l + 1
289 | indexes[i+1] = pos
290 | }
291 | if lc > 1 {
292 | // Pop the last item. (This is not a new line.)
293 | indexes = indexes[:lc]
294 | }
295 | return indexes
296 | }
297 |
298 | // For the index of a character at a certain line, calculate the index of
299 | // the first character on that line.
300 | func (d *Document) findLineStartIndex(index int) (pos int, lineStartIndex int) {
301 | indexes := d.lineStartIndexes()
302 | pos = bisect.Right(indexes, index) - 1
303 | lineStartIndex = indexes[pos]
304 | return
305 | }
306 |
307 | // CursorPositionRow returns the current row. (0-based.)
308 | func (d *Document) CursorPositionRow() (row int) {
309 | row, _ = d.findLineStartIndex(d.cursorPosition)
310 | return
311 | }
312 |
313 | // CursorPositionCol returns the current column. (0-based.)
314 | func (d *Document) CursorPositionCol() (col int) {
315 | // Don't use self.text_before_cursor to calculate this. Creating substrings
316 | // and splitting is too expensive for getting the cursor position.
317 | _, index := d.findLineStartIndex(d.cursorPosition)
318 | col = d.cursorPosition - index
319 | return
320 | }
321 |
322 | // GetCursorLeftPosition returns the relative position for cursor left.
323 | func (d *Document) GetCursorLeftPosition(count int) int {
324 | if count < 0 {
325 | return d.GetCursorRightPosition(-count)
326 | }
327 | if d.CursorPositionCol() > count {
328 | return -count
329 | }
330 | return -d.CursorPositionCol()
331 | }
332 |
333 | // GetCursorRightPosition returns relative position for cursor right.
334 | func (d *Document) GetCursorRightPosition(count int) int {
335 | if count < 0 {
336 | return d.GetCursorLeftPosition(-count)
337 | }
338 | if len(d.CurrentLineAfterCursor()) > count {
339 | return count
340 | }
341 | return len(d.CurrentLineAfterCursor())
342 | }
343 |
344 | // GetCursorUpPosition return the relative cursor position (character index) where we would be
345 | // if the user pressed the arrow-up button.
346 | func (d *Document) GetCursorUpPosition(count int, preferredColumn int) int {
347 | var col int
348 | if preferredColumn == -1 { // -1 means nil
349 | col = d.CursorPositionCol()
350 | } else {
351 | col = preferredColumn
352 | }
353 |
354 | row := d.CursorPositionRow() - count
355 | if row < 0 {
356 | row = 0
357 | }
358 | return d.TranslateRowColToIndex(row, col) - d.cursorPosition
359 | }
360 |
361 | // GetCursorDownPosition return the relative cursor position (character index) where we would be if the
362 | // user pressed the arrow-down button.
363 | func (d *Document) GetCursorDownPosition(count int, preferredColumn int) int {
364 | var col int
365 | if preferredColumn == -1 { // -1 means nil
366 | col = d.CursorPositionCol()
367 | } else {
368 | col = preferredColumn
369 | }
370 | row := d.CursorPositionRow() + count
371 | return d.TranslateRowColToIndex(row, col) - d.cursorPosition
372 | }
373 |
374 | // Lines returns the array of all the lines.
375 | func (d *Document) Lines() []string {
376 | // TODO: Cache, because this one is reused very often.
377 | return strings.Split(d.Text, "\n")
378 | }
379 |
380 | // LineCount return the number of lines in this document. If the document ends
381 | // with a trailing \n, that counts as the beginning of a new line.
382 | func (d *Document) LineCount() int {
383 | return len(d.Lines())
384 | }
385 |
386 | // TranslateIndexToPosition given an index for the text, return the corresponding (row, col) tuple.
387 | // (0-based. Returns (0, 0) for index=0.)
388 | func (d *Document) TranslateIndexToPosition(index int) (row int, col int) {
389 | row, rowIndex := d.findLineStartIndex(index)
390 | col = index - rowIndex
391 | return
392 | }
393 |
394 | // TranslateRowColToIndex given a (row, col), return the corresponding index.
395 | // (Row and col params are 0-based.)
396 | func (d *Document) TranslateRowColToIndex(row int, column int) (index int) {
397 | indexes := d.lineStartIndexes()
398 | if row < 0 {
399 | row = 0
400 | } else if row > len(indexes) {
401 | row = len(indexes) - 1
402 | }
403 | index = indexes[row]
404 | line := d.Lines()[row]
405 |
406 | // python) result += max(0, min(col, len(line)))
407 | if column > 0 || len(line) > 0 {
408 | if column > len(line) {
409 | index += len(line)
410 | } else {
411 | index += column
412 | }
413 | }
414 |
415 | // Keep in range. (len(self.text) is included, because the cursor can be
416 | // right after the end of the text as well.)
417 | // python) result = max(0, min(result, len(self.text)))
418 | if index > len(d.Text) {
419 | index = len(d.Text)
420 | }
421 | if index < 0 {
422 | index = 0
423 | }
424 | return index
425 | }
426 |
427 | // OnLastLine returns true when we are at the last line.
428 | func (d *Document) OnLastLine() bool {
429 | return d.CursorPositionRow() == (d.LineCount() - 1)
430 | }
431 |
432 | // GetEndOfLinePosition returns relative position for the end of this line.
433 | func (d *Document) GetEndOfLinePosition() int {
434 | return len([]rune(d.CurrentLineAfterCursor()))
435 | }
436 |
437 | func (d *Document) leadingWhitespaceInCurrentLine() (margin string) {
438 | trimmed := strings.TrimSpace(d.CurrentLine())
439 | margin = d.CurrentLine()[:len(d.CurrentLine())-len(trimmed)]
440 | return
441 | }
442 |
--------------------------------------------------------------------------------
/document_test.go:
--------------------------------------------------------------------------------
1 | package prompt
2 |
3 | import (
4 | "fmt"
5 | "reflect"
6 | "testing"
7 | "unicode/utf8"
8 | )
9 |
10 | func ExampleDocument_CurrentLine() {
11 | d := &Document{
12 | Text: `Hello! my name is c-bata.
13 | This is a example of Document component.
14 | This component has texts displayed in terminal and cursor position.
15 | `,
16 | cursorPosition: len(`Hello! my name is c-bata.
17 | This is a exam`),
18 | }
19 | fmt.Println(d.CurrentLine())
20 | // Output:
21 | // This is a example of Document component.
22 | }
23 |
24 | func ExampleDocument_DisplayCursorPosition() {
25 | d := &Document{
26 | Text: `Hello! my name is c-bata.`,
27 | cursorPosition: len(`Hello`),
28 | }
29 | fmt.Println("DisplayCursorPosition", d.DisplayCursorPosition())
30 | // Output:
31 | // DisplayCursorPosition 5
32 | }
33 |
34 | func ExampleDocument_CursorPositionRow() {
35 | d := &Document{
36 | Text: `Hello! my name is c-bata.
37 | This is a example of Document component.
38 | This component has texts displayed in terminal and cursor position.
39 | `,
40 | cursorPosition: len(`Hello! my name is c-bata.
41 | This is a exam`),
42 | }
43 | fmt.Println("CursorPositionRow", d.CursorPositionRow())
44 | // Output:
45 | // CursorPositionRow 1
46 | }
47 |
48 | func ExampleDocument_CursorPositionCol() {
49 | d := &Document{
50 | Text: `Hello! my name is c-bata.
51 | This is a example of Document component.
52 | This component has texts displayed in terminal and cursor position.
53 | `,
54 | cursorPosition: len(`Hello! my name is c-bata.
55 | This is a exam`),
56 | }
57 | fmt.Println("CursorPositionCol", d.CursorPositionCol())
58 | // Output:
59 | // CursorPositionCol 14
60 | }
61 |
62 | func ExampleDocument_TextBeforeCursor() {
63 | d := &Document{
64 | Text: `Hello! my name is c-bata.
65 | This is a example of Document component.
66 | This component has texts displayed in terminal and cursor position.
67 | `,
68 | cursorPosition: len(`Hello! my name is c-bata.
69 | This is a exam`),
70 | }
71 | fmt.Println(d.TextBeforeCursor())
72 | // Output:
73 | // Hello! my name is c-bata.
74 | // This is a exam
75 | }
76 |
77 | func ExampleDocument_TextAfterCursor() {
78 | d := &Document{
79 | Text: `Hello! my name is c-bata.
80 | This is a example of Document component.
81 | This component has texts displayed in terminal and cursor position.
82 | `,
83 | cursorPosition: len(`Hello! my name is c-bata.
84 | This is a exam`),
85 | }
86 | fmt.Println(d.TextAfterCursor())
87 | // Output:
88 | // ple of Document component.
89 | // This component has texts displayed in terminal and cursor position.
90 | }
91 |
92 | func ExampleDocument_DisplayCursorPosition_withJapanese() {
93 | d := &Document{
94 | Text: `こんにちは、芝田 将です。`,
95 | cursorPosition: 3,
96 | }
97 | fmt.Println("DisplayCursorPosition", d.DisplayCursorPosition())
98 | // Output:
99 | // DisplayCursorPosition 6
100 | }
101 |
102 | func ExampleDocument_CurrentLineBeforeCursor() {
103 | d := &Document{
104 | Text: `Hello! my name is c-bata.
105 | This is a example of Document component.
106 | This component has texts displayed in terminal and cursor position.
107 | `,
108 | cursorPosition: len(`Hello! my name is c-bata.
109 | This is a exam`),
110 | }
111 | fmt.Println(d.CurrentLineBeforeCursor())
112 | // Output:
113 | // This is a exam
114 | }
115 |
116 | func ExampleDocument_CurrentLineAfterCursor() {
117 | d := &Document{
118 | Text: `Hello! my name is c-bata.
119 | This is a example of Document component.
120 | This component has texts displayed in terminal and cursor position.
121 | `,
122 | cursorPosition: len(`Hello! my name is c-bata.
123 | This is a exam`),
124 | }
125 | fmt.Println(d.CurrentLineAfterCursor())
126 | // Output:
127 | // ple of Document component.
128 | }
129 |
130 | func ExampleDocument_GetWordBeforeCursor() {
131 | d := &Document{
132 | Text: `Hello! my name is c-bata.
133 | This is a example of Document component.
134 | `,
135 | cursorPosition: len(`Hello! my name is c-bata.
136 | This is a exam`),
137 | }
138 | fmt.Println(d.GetWordBeforeCursor())
139 | // Output:
140 | // exam
141 | }
142 |
143 | func ExampleDocument_GetWordAfterCursor() {
144 | d := &Document{
145 | Text: `Hello! my name is c-bata.
146 | This is a example of Document component.
147 | `,
148 | cursorPosition: len(`Hello! my name is c-bata.
149 | This is a exam`),
150 | }
151 | fmt.Println(d.GetWordAfterCursor())
152 | // Output:
153 | // ple
154 | }
155 |
156 | func ExampleDocument_GetWordBeforeCursorWithSpace() {
157 | d := &Document{
158 | Text: `Hello! my name is c-bata.
159 | This is a example of Document component.
160 | `,
161 | cursorPosition: len(`Hello! my name is c-bata.
162 | This is a example `),
163 | }
164 | fmt.Println(d.GetWordBeforeCursorWithSpace())
165 | // Output:
166 | // example
167 | }
168 |
169 | func ExampleDocument_GetWordAfterCursorWithSpace() {
170 | d := &Document{
171 | Text: `Hello! my name is c-bata.
172 | This is a example of Document component.
173 | `,
174 | cursorPosition: len(`Hello! my name is c-bata.
175 | This is a`),
176 | }
177 | fmt.Println(d.GetWordAfterCursorWithSpace())
178 | // Output:
179 | // example
180 | }
181 |
182 | func ExampleDocument_GetWordBeforeCursorUntilSeparator() {
183 | d := &Document{
184 | Text: `hello,i am c-bata`,
185 | cursorPosition: len(`hello,i am c`),
186 | }
187 | fmt.Println(d.GetWordBeforeCursorUntilSeparator(","))
188 | // Output:
189 | // i am c
190 | }
191 |
192 | func ExampleDocument_GetWordAfterCursorUntilSeparator() {
193 | d := &Document{
194 | Text: `hello,i am c-bata,thank you for using go-prompt`,
195 | cursorPosition: len(`hello,i a`),
196 | }
197 | fmt.Println(d.GetWordAfterCursorUntilSeparator(","))
198 | // Output:
199 | // m c-bata
200 | }
201 |
202 | func ExampleDocument_GetWordBeforeCursorUntilSeparatorIgnoreNextToCursor() {
203 | d := &Document{
204 | Text: `hello,i am c-bata,thank you for using go-prompt`,
205 | cursorPosition: len(`hello,i am c-bata,`),
206 | }
207 | fmt.Println(d.GetWordBeforeCursorUntilSeparatorIgnoreNextToCursor(","))
208 | // Output:
209 | // i am c-bata,
210 | }
211 |
212 | func ExampleDocument_GetWordAfterCursorUntilSeparatorIgnoreNextToCursor() {
213 | d := &Document{
214 | Text: `hello,i am c-bata,thank you for using go-prompt`,
215 | cursorPosition: len(`hello`),
216 | }
217 | fmt.Println(d.GetWordAfterCursorUntilSeparatorIgnoreNextToCursor(","))
218 | // Output:
219 | // ,i am c-bata
220 | }
221 |
222 | func TestDocument_DisplayCursorPosition(t *testing.T) {
223 | patterns := []struct {
224 | document *Document
225 | expected int
226 | }{
227 | {
228 | document: &Document{
229 | Text: "hello",
230 | cursorPosition: 2,
231 | },
232 | expected: 2,
233 | },
234 | {
235 | document: &Document{
236 | Text: "こんにちは",
237 | cursorPosition: 2,
238 | },
239 | expected: 4,
240 | },
241 | {
242 | // If you're facing test failure on this test case and your terminal is iTerm2,
243 | // please check 'Profile -> Text' configuration. 'Use Unicode version 9 widths'
244 | // must be checked.
245 | // https://github.com/c-bata/go-prompt/pull/99
246 | document: &Document{
247 | Text: "Добрый день",
248 | cursorPosition: 3,
249 | },
250 | expected: 3,
251 | },
252 | }
253 |
254 | for _, p := range patterns {
255 | ac := p.document.DisplayCursorPosition()
256 | if ac != p.expected {
257 | t.Errorf("Should be %#v, got %#v", p.expected, ac)
258 | }
259 | }
260 | }
261 |
262 | func TestDocument_GetCharRelativeToCursor(t *testing.T) {
263 | patterns := []struct {
264 | document *Document
265 | expected string
266 | }{
267 | {
268 | document: &Document{
269 | Text: "line 1\nline 2\nline 3\nline 4\n",
270 | cursorPosition: len([]rune("line 1\n" + "lin")),
271 | },
272 | expected: "e",
273 | },
274 | {
275 | document: &Document{
276 | Text: "あいうえお\nかきくけこ\nさしすせそ\nたちつてと\n",
277 | cursorPosition: 8,
278 | },
279 | expected: "く",
280 | },
281 | {
282 | document: &Document{
283 | Text: "Добрый\nдень\nДобрый день",
284 | cursorPosition: 9,
285 | },
286 | expected: "н",
287 | },
288 | }
289 |
290 | for i, p := range patterns {
291 | ac := p.document.GetCharRelativeToCursor(1)
292 | ex, _ := utf8.DecodeRuneInString(p.expected)
293 | if ac != ex {
294 | t.Errorf("[%d] Should be %s, got %s", i, string(ex), string(ac))
295 | }
296 | }
297 | }
298 |
299 | func TestDocument_TextBeforeCursor(t *testing.T) {
300 | patterns := []struct {
301 | document *Document
302 | expected string
303 | }{
304 | {
305 | document: &Document{
306 | Text: "line 1\nline 2\nline 3\nline 4\n",
307 | cursorPosition: len("line 1\n" + "lin"),
308 | },
309 | expected: "line 1\nlin",
310 | },
311 | {
312 | document: &Document{
313 | Text: "あいうえお\nかきくけこ\nさしすせそ\nたちつてと\n",
314 | cursorPosition: 8,
315 | },
316 | expected: "あいうえお\nかき",
317 | },
318 | {
319 | document: &Document{
320 | Text: "Добрый\nдень\nДобрый день",
321 | cursorPosition: 9,
322 | },
323 | expected: "Добрый\nде",
324 | },
325 | }
326 | for i, p := range patterns {
327 | ac := p.document.TextBeforeCursor()
328 | if ac != p.expected {
329 | t.Errorf("[%d] Should be %s, got %s", i, p.expected, ac)
330 | }
331 | }
332 | }
333 |
334 | func TestDocument_TextAfterCursor(t *testing.T) {
335 | pattern := []struct {
336 | document *Document
337 | expected string
338 | }{
339 | {
340 | document: &Document{
341 | Text: "line 1\nline 2\nline 3\nline 4\n",
342 | cursorPosition: len("line 1\n" + "lin"),
343 | },
344 | expected: "e 2\nline 3\nline 4\n",
345 | },
346 | {
347 | document: &Document{
348 | Text: "",
349 | cursorPosition: 0,
350 | },
351 | expected: "",
352 | },
353 | {
354 | document: &Document{
355 | Text: "あいうえお\nかきくけこ\nさしすせそ\nたちつてと\n",
356 | cursorPosition: 8,
357 | },
358 | expected: "くけこ\nさしすせそ\nたちつてと\n",
359 | },
360 | {
361 | document: &Document{
362 | Text: "Добрый\nдень\nДобрый день",
363 | cursorPosition: 9,
364 | },
365 | expected: "нь\nДобрый день",
366 | },
367 | }
368 |
369 | for i, p := range pattern {
370 | ac := p.document.TextAfterCursor()
371 | if ac != p.expected {
372 | t.Errorf("[%d] Should be %#v, got %#v", i, p.expected, ac)
373 | }
374 | }
375 | }
376 |
377 | func TestDocument_GetWordBeforeCursor(t *testing.T) {
378 | pattern := []struct {
379 | document *Document
380 | expected string
381 | sep string
382 | }{
383 | {
384 | document: &Document{
385 | Text: "apple bana",
386 | cursorPosition: len("apple bana"),
387 | },
388 | expected: "bana",
389 | },
390 | {
391 | document: &Document{
392 | Text: "apply -f ./file/foo.json",
393 | cursorPosition: len("apply -f ./file/foo.json"),
394 | },
395 | expected: "foo.json",
396 | sep: " /",
397 | },
398 | {
399 | document: &Document{
400 | Text: "apple banana orange",
401 | cursorPosition: len("apple ba"),
402 | },
403 | expected: "ba",
404 | },
405 | {
406 | document: &Document{
407 | Text: "apply -f ./file/foo.json",
408 | cursorPosition: len("apply -f ./fi"),
409 | },
410 | expected: "fi",
411 | sep: " /",
412 | },
413 | {
414 | document: &Document{
415 | Text: "apple ",
416 | cursorPosition: len("apple "),
417 | },
418 | expected: "",
419 | },
420 | {
421 | document: &Document{
422 | Text: "あいうえお かきくけこ さしすせそ",
423 | cursorPosition: 8,
424 | },
425 | expected: "かき",
426 | },
427 | {
428 | document: &Document{
429 | Text: "Добрый день Добрый день",
430 | cursorPosition: 9,
431 | },
432 | expected: "де",
433 | },
434 | }
435 |
436 | for i, p := range pattern {
437 | if p.sep == "" {
438 | ac := p.document.GetWordBeforeCursor()
439 | if ac != p.expected {
440 | t.Errorf("[%d] Should be %#v, got %#v", i, p.expected, ac)
441 | }
442 | ac = p.document.GetWordBeforeCursorUntilSeparator("")
443 | if ac != p.expected {
444 | t.Errorf("[%d] Should be %#v, got %#v", i, p.expected, ac)
445 | }
446 | } else {
447 | ac := p.document.GetWordBeforeCursorUntilSeparator(p.sep)
448 | if ac != p.expected {
449 | t.Errorf("[%d] Should be %#v, got %#v", i, p.expected, ac)
450 | }
451 | }
452 | }
453 | }
454 |
455 | func TestDocument_GetWordBeforeCursorWithSpace(t *testing.T) {
456 | pattern := []struct {
457 | document *Document
458 | expected string
459 | sep string
460 | }{
461 | {
462 | document: &Document{
463 | Text: "apple bana ",
464 | cursorPosition: len("apple bana "),
465 | },
466 | expected: "bana ",
467 | },
468 | {
469 | document: &Document{
470 | Text: "apply -f /path/to/file/",
471 | cursorPosition: len("apply -f /path/to/file/"),
472 | },
473 | expected: "file/",
474 | sep: " /",
475 | },
476 | {
477 | document: &Document{
478 | Text: "apple ",
479 | cursorPosition: len("apple "),
480 | },
481 | expected: "apple ",
482 | },
483 | {
484 | document: &Document{
485 | Text: "path/",
486 | cursorPosition: len("path/"),
487 | },
488 | expected: "path/",
489 | sep: " /",
490 | },
491 | {
492 | document: &Document{
493 | Text: "あいうえお かきくけこ ",
494 | cursorPosition: 12,
495 | },
496 | expected: "かきくけこ ",
497 | },
498 | {
499 | document: &Document{
500 | Text: "Добрый день ",
501 | cursorPosition: 12,
502 | },
503 | expected: "день ",
504 | },
505 | }
506 |
507 | for _, p := range pattern {
508 | if p.sep == "" {
509 | ac := p.document.GetWordBeforeCursorWithSpace()
510 | if ac != p.expected {
511 | t.Errorf("Should be %#v, got %#v", p.expected, ac)
512 | }
513 | ac = p.document.GetWordBeforeCursorUntilSeparatorIgnoreNextToCursor("")
514 | if ac != p.expected {
515 | t.Errorf("Should be %#v, got %#v", p.expected, ac)
516 | }
517 | } else {
518 | ac := p.document.GetWordBeforeCursorUntilSeparatorIgnoreNextToCursor(p.sep)
519 | if ac != p.expected {
520 | t.Errorf("Should be %#v, got %#v", p.expected, ac)
521 | }
522 | }
523 | }
524 | }
525 |
526 | func TestDocument_FindStartOfPreviousWord(t *testing.T) {
527 | pattern := []struct {
528 | document *Document
529 | expected int
530 | sep string
531 | }{
532 | {
533 | document: &Document{
534 | Text: "apple bana",
535 | cursorPosition: len("apple bana"),
536 | },
537 | expected: len("apple "),
538 | },
539 | {
540 | document: &Document{
541 | Text: "apply -f ./file/foo.json",
542 | cursorPosition: len("apply -f ./file/foo.json"),
543 | },
544 | expected: len("apply -f ./file/"),
545 | sep: " /",
546 | },
547 | {
548 | document: &Document{
549 | Text: "apple ",
550 | cursorPosition: len("apple "),
551 | },
552 | expected: len("apple "),
553 | },
554 | {
555 | document: &Document{
556 | Text: "apply -f ./file/foo.json",
557 | cursorPosition: len("apply -f ./"),
558 | },
559 | expected: len("apply -f ./"),
560 | sep: " /",
561 | },
562 | {
563 | document: &Document{
564 | Text: "あいうえお かきくけこ さしすせそ",
565 | cursorPosition: 8, // between 'き' and 'く'
566 | },
567 | expected: len("あいうえお "), // this function returns index byte in string
568 | },
569 | {
570 | document: &Document{
571 | Text: "Добрый день Добрый день",
572 | cursorPosition: 9,
573 | },
574 | expected: len("Добрый "), // this function returns index byte in string
575 | },
576 | }
577 |
578 | for _, p := range pattern {
579 | if p.sep == "" {
580 | ac := p.document.FindStartOfPreviousWord()
581 | if ac != p.expected {
582 | t.Errorf("Should be %#v, got %#v", p.expected, ac)
583 | }
584 | ac = p.document.FindStartOfPreviousWordUntilSeparator("")
585 | if ac != p.expected {
586 | t.Errorf("Should be %#v, got %#v", p.expected, ac)
587 | }
588 | } else {
589 | ac := p.document.FindStartOfPreviousWordUntilSeparator(p.sep)
590 | if ac != p.expected {
591 | t.Errorf("Should be %#v, got %#v", p.expected, ac)
592 | }
593 | }
594 | }
595 | }
596 |
597 | func TestDocument_FindStartOfPreviousWordWithSpace(t *testing.T) {
598 | pattern := []struct {
599 | document *Document
600 | expected int
601 | sep string
602 | }{
603 | {
604 | document: &Document{
605 | Text: "apple bana ",
606 | cursorPosition: len("apple bana "),
607 | },
608 | expected: len("apple "),
609 | },
610 | {
611 | document: &Document{
612 | Text: "apply -f /file/foo/",
613 | cursorPosition: len("apply -f /file/foo/"),
614 | },
615 | expected: len("apply -f /file/"),
616 | sep: " /",
617 | },
618 | {
619 | document: &Document{
620 | Text: "apple ",
621 | cursorPosition: len("apple "),
622 | },
623 | expected: len(""),
624 | },
625 | {
626 | document: &Document{
627 | Text: "file/",
628 | cursorPosition: len("file/"),
629 | },
630 | expected: len(""),
631 | sep: " /",
632 | },
633 | {
634 | document: &Document{
635 | Text: "あいうえお かきくけこ ",
636 | cursorPosition: 12, // cursor points to last
637 | },
638 | expected: len("あいうえお "), // this function returns index byte in string
639 | },
640 | {
641 | document: &Document{
642 | Text: "Добрый день ",
643 | cursorPosition: 12,
644 | },
645 | expected: len("Добрый "), // this function returns index byte in string
646 | },
647 | }
648 |
649 | for _, p := range pattern {
650 | if p.sep == "" {
651 | ac := p.document.FindStartOfPreviousWordWithSpace()
652 | if ac != p.expected {
653 | t.Errorf("Should be %#v, got %#v", p.expected, ac)
654 | }
655 | ac = p.document.FindStartOfPreviousWordUntilSeparatorIgnoreNextToCursor("")
656 | if ac != p.expected {
657 | t.Errorf("Should be %#v, got %#v", p.expected, ac)
658 | }
659 | } else {
660 | ac := p.document.FindStartOfPreviousWordUntilSeparatorIgnoreNextToCursor(p.sep)
661 | if ac != p.expected {
662 | t.Errorf("Should be %#v, got %#v", p.expected, ac)
663 | }
664 | }
665 | }
666 | }
667 |
668 | func TestDocument_GetWordAfterCursor(t *testing.T) {
669 | pattern := []struct {
670 | document *Document
671 | expected string
672 | sep string
673 | }{
674 | {
675 | document: &Document{
676 | Text: "apple bana",
677 | cursorPosition: len("apple bana"),
678 | },
679 | expected: "",
680 | },
681 | {
682 | document: &Document{
683 | Text: "apply -f ./file/foo.json",
684 | cursorPosition: len("apply -f ./fi"),
685 | },
686 | expected: "le",
687 | sep: " /",
688 | },
689 | {
690 | document: &Document{
691 | Text: "apple bana",
692 | cursorPosition: len("apple "),
693 | },
694 | expected: "bana",
695 | },
696 | {
697 | document: &Document{
698 | Text: "apple bana",
699 | cursorPosition: len("apple"),
700 | },
701 | expected: "",
702 | },
703 | {
704 | document: &Document{
705 | Text: "apply -f ./file/foo.json",
706 | cursorPosition: len("apply -f ."),
707 | },
708 | expected: "",
709 | sep: " /",
710 | },
711 | {
712 | document: &Document{
713 | Text: "apple bana",
714 | cursorPosition: len("ap"),
715 | },
716 | expected: "ple",
717 | },
718 | {
719 | document: &Document{
720 | Text: "あいうえお かきくけこ さしすせそ",
721 | cursorPosition: 8,
722 | },
723 | expected: "くけこ",
724 | },
725 | {
726 | document: &Document{
727 | Text: "Добрый день Добрый день",
728 | cursorPosition: 9,
729 | },
730 | expected: "нь",
731 | },
732 | }
733 |
734 | for k, p := range pattern {
735 | if p.sep == "" {
736 | ac := p.document.GetWordAfterCursor()
737 | if ac != p.expected {
738 | t.Errorf("[%d] Should be %#v, got %#v", k, p.expected, ac)
739 | }
740 | ac = p.document.GetWordAfterCursorUntilSeparator("")
741 | if ac != p.expected {
742 | t.Errorf("[%d] Should be %#v, got %#v", k, p.expected, ac)
743 | }
744 | } else {
745 | ac := p.document.GetWordAfterCursorUntilSeparator(p.sep)
746 | if ac != p.expected {
747 | t.Errorf("[%d] Should be %#v, got %#v", k, p.expected, ac)
748 | }
749 | }
750 | }
751 | }
752 |
753 | func TestDocument_GetWordAfterCursorWithSpace(t *testing.T) {
754 | pattern := []struct {
755 | document *Document
756 | expected string
757 | sep string
758 | }{
759 | {
760 | document: &Document{
761 | Text: "apple bana",
762 | cursorPosition: len("apple bana"),
763 | },
764 | expected: "",
765 | },
766 | {
767 | document: &Document{
768 | Text: "apple bana",
769 | cursorPosition: len("apple "),
770 | },
771 | expected: "bana",
772 | },
773 | {
774 | document: &Document{
775 | Text: "/path/to",
776 | cursorPosition: len("/path/"),
777 | },
778 | expected: "to",
779 | sep: " /",
780 | },
781 | {
782 | document: &Document{
783 | Text: "/path/to/file",
784 | cursorPosition: len("/path/"),
785 | },
786 | expected: "to",
787 | sep: " /",
788 | },
789 | {
790 | document: &Document{
791 | Text: "apple bana",
792 | cursorPosition: len("apple"),
793 | },
794 | expected: " bana",
795 | },
796 | {
797 | document: &Document{
798 | Text: "path/to",
799 | cursorPosition: len("path"),
800 | },
801 | expected: "/to",
802 | sep: " /",
803 | },
804 | {
805 | document: &Document{
806 | Text: "apple bana",
807 | cursorPosition: len("ap"),
808 | },
809 | expected: "ple",
810 | },
811 | {
812 | document: &Document{
813 | Text: "あいうえお かきくけこ さしすせそ",
814 | cursorPosition: 5,
815 | },
816 | expected: " かきくけこ",
817 | },
818 | {
819 | document: &Document{
820 | Text: "Добрый день Добрый день",
821 | cursorPosition: 6,
822 | },
823 | expected: " день",
824 | },
825 | }
826 |
827 | for k, p := range pattern {
828 | if p.sep == "" {
829 | ac := p.document.GetWordAfterCursorWithSpace()
830 | if ac != p.expected {
831 | t.Errorf("[%d] Should be %#v, got %#v", k, p.expected, ac)
832 | }
833 | ac = p.document.GetWordAfterCursorUntilSeparatorIgnoreNextToCursor("")
834 | if ac != p.expected {
835 | t.Errorf("[%d] Should be %#v, got %#v", k, p.expected, ac)
836 | }
837 | } else {
838 | ac := p.document.GetWordAfterCursorUntilSeparatorIgnoreNextToCursor(p.sep)
839 | if ac != p.expected {
840 | t.Errorf("[%d] Should be %#v, got %#v", k, p.expected, ac)
841 | }
842 | }
843 | }
844 | }
845 |
846 | func TestDocument_FindEndOfCurrentWord(t *testing.T) {
847 | pattern := []struct {
848 | document *Document
849 | expected int
850 | sep string
851 | }{
852 | {
853 | document: &Document{
854 | Text: "apple bana",
855 | cursorPosition: len("apple bana"),
856 | },
857 | expected: len(""),
858 | },
859 | {
860 | document: &Document{
861 | Text: "apple bana",
862 | cursorPosition: len("apple "),
863 | },
864 | expected: len("bana"),
865 | },
866 | {
867 | document: &Document{
868 | Text: "apply -f ./file/foo.json",
869 | cursorPosition: len("apply -f ./"),
870 | },
871 | expected: len("file"),
872 | sep: " /",
873 | },
874 | {
875 | document: &Document{
876 | Text: "apple bana",
877 | cursorPosition: len("apple"),
878 | },
879 | expected: len(""),
880 | },
881 | {
882 | document: &Document{
883 | Text: "apply -f ./file/foo.json",
884 | cursorPosition: len("apply -f ."),
885 | },
886 | expected: len(""),
887 | sep: " /",
888 | },
889 | {
890 | document: &Document{
891 | Text: "apple bana",
892 | cursorPosition: len("ap"),
893 | },
894 | expected: len("ple"),
895 | },
896 | {
897 | // りん(cursor)ご ばなな
898 | document: &Document{
899 | Text: "りんご ばなな",
900 | cursorPosition: 2,
901 | },
902 | expected: len("ご"),
903 | },
904 | {
905 | document: &Document{
906 | Text: "りんご ばなな",
907 | cursorPosition: 3,
908 | },
909 | expected: 0,
910 | },
911 | {
912 | // Доб(cursor)рый день
913 | document: &Document{
914 | Text: "Добрый день",
915 | cursorPosition: 3,
916 | },
917 | expected: len("рый"),
918 | },
919 | }
920 |
921 | for k, p := range pattern {
922 | if p.sep == "" {
923 | ac := p.document.FindEndOfCurrentWord()
924 | if ac != p.expected {
925 | t.Errorf("[%d] Should be %#v, got %#v", k, p.expected, ac)
926 | }
927 | ac = p.document.FindEndOfCurrentWordUntilSeparator("")
928 | if ac != p.expected {
929 | t.Errorf("[%d] Should be %#v, got %#v", k, p.expected, ac)
930 | }
931 | } else {
932 | ac := p.document.FindEndOfCurrentWordUntilSeparator(p.sep)
933 | if ac != p.expected {
934 | t.Errorf("[%d] Should be %#v, got %#v", k, p.expected, ac)
935 | }
936 | }
937 | }
938 | }
939 |
940 | func TestDocument_FindEndOfCurrentWordWithSpace(t *testing.T) {
941 | pattern := []struct {
942 | document *Document
943 | expected int
944 | sep string
945 | }{
946 | {
947 | document: &Document{
948 | Text: "apple bana",
949 | cursorPosition: len("apple bana"),
950 | },
951 | expected: len(""),
952 | },
953 | {
954 | document: &Document{
955 | Text: "apple bana",
956 | cursorPosition: len("apple "),
957 | },
958 | expected: len("bana"),
959 | },
960 | {
961 | document: &Document{
962 | Text: "apply -f /file/foo.json",
963 | cursorPosition: len("apply -f /"),
964 | },
965 | expected: len("file"),
966 | sep: " /",
967 | },
968 | {
969 | document: &Document{
970 | Text: "apple bana",
971 | cursorPosition: len("apple"),
972 | },
973 | expected: len(" bana"),
974 | },
975 | {
976 | document: &Document{
977 | Text: "apply -f /path/to",
978 | cursorPosition: len("apply -f /path"),
979 | },
980 | expected: len("/to"),
981 | sep: " /",
982 | },
983 | {
984 | document: &Document{
985 | Text: "apple bana",
986 | cursorPosition: len("ap"),
987 | },
988 | expected: len("ple"),
989 | },
990 | {
991 | document: &Document{
992 | Text: "あいうえお かきくけこ",
993 | cursorPosition: 6,
994 | },
995 | expected: len("かきくけこ"),
996 | },
997 | {
998 | document: &Document{
999 | Text: "あいうえお かきくけこ",
1000 | cursorPosition: 5,
1001 | },
1002 | expected: len(" かきくけこ"),
1003 | },
1004 | {
1005 | document: &Document{
1006 | Text: "Добрый день",
1007 | cursorPosition: 6,
1008 | },
1009 | expected: len(" день"),
1010 | },
1011 | }
1012 |
1013 | for k, p := range pattern {
1014 | if p.sep == "" {
1015 | ac := p.document.FindEndOfCurrentWordWithSpace()
1016 | if ac != p.expected {
1017 | t.Errorf("[%d] Should be %#v, got %#v", k, p.expected, ac)
1018 | }
1019 | ac = p.document.FindEndOfCurrentWordUntilSeparatorIgnoreNextToCursor("")
1020 | if ac != p.expected {
1021 | t.Errorf("[%d] Should be %#v, got %#v", k, p.expected, ac)
1022 | }
1023 | } else {
1024 | ac := p.document.FindEndOfCurrentWordUntilSeparatorIgnoreNextToCursor(p.sep)
1025 | if ac != p.expected {
1026 | t.Errorf("[%d] Should be %#v, got %#v", k, p.expected, ac)
1027 | }
1028 | }
1029 | }
1030 | }
1031 |
1032 | func TestDocument_CurrentLineBeforeCursor(t *testing.T) {
1033 | d := &Document{
1034 | Text: "line 1\nline 2\nline 3\nline 4\n",
1035 | cursorPosition: len("line 1\n" + "lin"),
1036 | }
1037 | ac := d.CurrentLineBeforeCursor()
1038 | ex := "lin"
1039 | if ac != ex {
1040 | t.Errorf("Should be %#v, got %#v", ex, ac)
1041 | }
1042 | }
1043 |
1044 | func TestDocument_CurrentLineAfterCursor(t *testing.T) {
1045 | d := &Document{
1046 | Text: "line 1\nline 2\nline 3\nline 4\n",
1047 | cursorPosition: len("line 1\n" + "lin"),
1048 | }
1049 | ac := d.CurrentLineAfterCursor()
1050 | ex := "e 2"
1051 | if ac != ex {
1052 | t.Errorf("Should be %#v, got %#v", ex, ac)
1053 | }
1054 | }
1055 |
1056 | func TestDocument_CurrentLine(t *testing.T) {
1057 | d := &Document{
1058 | Text: "line 1\nline 2\nline 3\nline 4\n",
1059 | cursorPosition: len("line 1\n" + "lin"),
1060 | }
1061 | ac := d.CurrentLine()
1062 | ex := "line 2"
1063 | if ac != ex {
1064 | t.Errorf("Should be %#v, got %#v", ex, ac)
1065 | }
1066 | }
1067 |
1068 | func TestDocument_CursorPositionRowAndCol(t *testing.T) {
1069 | var cursorPositionTests = []struct {
1070 | document *Document
1071 | expectedRow int
1072 | expectedCol int
1073 | }{
1074 | {
1075 | document: &Document{Text: "line 1\nline 2\nline 3\n", cursorPosition: len("line 1\n" + "lin")},
1076 | expectedRow: 1,
1077 | expectedCol: 3,
1078 | },
1079 | {
1080 | document: &Document{Text: "", cursorPosition: 0},
1081 | expectedRow: 0,
1082 | expectedCol: 0,
1083 | },
1084 | }
1085 | for _, test := range cursorPositionTests {
1086 | ac := test.document.CursorPositionRow()
1087 | if ac != test.expectedRow {
1088 | t.Errorf("Should be %#v, got %#v", test.expectedRow, ac)
1089 | }
1090 | ac = test.document.CursorPositionCol()
1091 | if ac != test.expectedCol {
1092 | t.Errorf("Should be %#v, got %#v", test.expectedCol, ac)
1093 | }
1094 | }
1095 | }
1096 |
1097 | func TestDocument_GetCursorLeftPosition(t *testing.T) {
1098 | d := &Document{
1099 | Text: "line 1\nline 2\nline 3\nline 4\n",
1100 | cursorPosition: len("line 1\n" + "line 2\n" + "lin"),
1101 | }
1102 | ac := d.GetCursorLeftPosition(2)
1103 | ex := -2
1104 | if ac != ex {
1105 | t.Errorf("Should be %#v, got %#v", ex, ac)
1106 | }
1107 | ac = d.GetCursorLeftPosition(10)
1108 | ex = -3
1109 | if ac != ex {
1110 | t.Errorf("Should be %#v, got %#v", ex, ac)
1111 | }
1112 | }
1113 |
1114 | func TestDocument_GetCursorUpPosition(t *testing.T) {
1115 | d := &Document{
1116 | Text: "line 1\nline 2\nline 3\nline 4\n",
1117 | cursorPosition: len("line 1\n" + "line 2\n" + "lin"),
1118 | }
1119 | ac := d.GetCursorUpPosition(2, -1)
1120 | ex := len("lin") - len("line 1\n"+"line 2\n"+"lin")
1121 | if ac != ex {
1122 | t.Errorf("Should be %#v, got %#v", ex, ac)
1123 | }
1124 |
1125 | ac = d.GetCursorUpPosition(100, -1)
1126 | ex = len("lin") - len("line 1\n"+"line 2\n"+"lin")
1127 | if ac != ex {
1128 | t.Errorf("Should be %#v, got %#v", ex, ac)
1129 | }
1130 | }
1131 |
1132 | func TestDocument_GetCursorDownPosition(t *testing.T) {
1133 | d := &Document{
1134 | Text: "line 1\nline 2\nline 3\nline 4\n",
1135 | cursorPosition: len("lin"),
1136 | }
1137 | ac := d.GetCursorDownPosition(2, -1)
1138 | ex := len("line 1\n"+"line 2\n"+"lin") - len("lin")
1139 | if ac != ex {
1140 | t.Errorf("Should be %#v, got %#v", ex, ac)
1141 | }
1142 |
1143 | ac = d.GetCursorDownPosition(100, -1)
1144 | ex = len("line 1\n"+"line 2\n"+"line 3\n"+"line 4\n") - len("lin")
1145 | if ac != ex {
1146 | t.Errorf("Should be %#v, got %#v", ex, ac)
1147 | }
1148 | }
1149 |
1150 | func TestDocument_GetCursorRightPosition(t *testing.T) {
1151 | d := &Document{
1152 | Text: "line 1\nline 2\nline 3\nline 4\n",
1153 | cursorPosition: len("line 1\n" + "line 2\n" + "lin"),
1154 | }
1155 | ac := d.GetCursorRightPosition(2)
1156 | ex := 2
1157 | if ac != ex {
1158 | t.Errorf("Should be %#v, got %#v", ex, ac)
1159 | }
1160 | ac = d.GetCursorRightPosition(10)
1161 | ex = 3
1162 | if ac != ex {
1163 | t.Errorf("Should be %#v, got %#v", ex, ac)
1164 | }
1165 | }
1166 |
1167 | func TestDocument_Lines(t *testing.T) {
1168 | d := &Document{
1169 | Text: "line 1\nline 2\nline 3\nline 4\n",
1170 | cursorPosition: len("line 1\n" + "lin"),
1171 | }
1172 | ac := d.Lines()
1173 | ex := []string{"line 1", "line 2", "line 3", "line 4", ""}
1174 | if !reflect.DeepEqual(ac, ex) {
1175 | t.Errorf("Should be %#v, got %#v", ex, ac)
1176 | }
1177 | }
1178 |
1179 | func TestDocument_LineCount(t *testing.T) {
1180 | d := &Document{
1181 | Text: "line 1\nline 2\nline 3\nline 4\n",
1182 | cursorPosition: len("line 1\n" + "lin"),
1183 | }
1184 | ac := d.LineCount()
1185 | ex := 5
1186 | if ac != ex {
1187 | t.Errorf("Should be %#v, got %#v", ex, ac)
1188 | }
1189 | }
1190 |
1191 | func TestDocument_TranslateIndexToPosition(t *testing.T) {
1192 | d := &Document{
1193 | Text: "line 1\nline 2\nline 3\nline 4\n",
1194 | cursorPosition: len("line 1\n" + "lin"),
1195 | }
1196 | row, col := d.TranslateIndexToPosition(len("line 1\nline 2\nlin"))
1197 | if row != 2 {
1198 | t.Errorf("Should be %#v, got %#v", 2, row)
1199 | }
1200 | if col != 3 {
1201 | t.Errorf("Should be %#v, got %#v", 3, col)
1202 | }
1203 | row, col = d.TranslateIndexToPosition(0)
1204 | if row != 0 {
1205 | t.Errorf("Should be %#v, got %#v", 0, row)
1206 | }
1207 | if col != 0 {
1208 | t.Errorf("Should be %#v, got %#v", 0, col)
1209 | }
1210 | }
1211 |
1212 | func TestDocument_TranslateRowColToIndex(t *testing.T) {
1213 | d := &Document{
1214 | Text: "line 1\nline 2\nline 3\nline 4\n",
1215 | cursorPosition: len("line 1\n" + "lin"),
1216 | }
1217 | ac := d.TranslateRowColToIndex(2, 3)
1218 | ex := len("line 1\nline 2\nlin")
1219 | if ac != ex {
1220 | t.Errorf("Should be %#v, got %#v", ex, ac)
1221 | }
1222 | ac = d.TranslateRowColToIndex(0, 0)
1223 | ex = 0
1224 | if ac != ex {
1225 | t.Errorf("Should be %#v, got %#v", ex, ac)
1226 | }
1227 | }
1228 |
1229 | func TestDocument_OnLastLine(t *testing.T) {
1230 | d := &Document{
1231 | Text: "line 1\nline 2\nline 3",
1232 | cursorPosition: len("line 1\nline"),
1233 | }
1234 | ac := d.OnLastLine()
1235 | if ac {
1236 | t.Errorf("Should be %#v, got %#v", false, ac)
1237 | }
1238 | d.cursorPosition = len("line 1\nline 2\nline")
1239 | ac = d.OnLastLine()
1240 | if !ac {
1241 | t.Errorf("Should be %#v, got %#v", true, ac)
1242 | }
1243 | }
1244 |
1245 | func TestDocument_GetEndOfLinePosition(t *testing.T) {
1246 | d := &Document{
1247 | Text: "line 1\nline 2\nline 3",
1248 | cursorPosition: len("line 1\nli"),
1249 | }
1250 | ac := d.GetEndOfLinePosition()
1251 | ex := len("ne 2")
1252 | if ac != ex {
1253 | t.Errorf("Should be %#v, got %#v", ex, ac)
1254 | }
1255 | }
1256 |
--------------------------------------------------------------------------------