├── _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 | ![vt100_debug](https://github.com/c-bata/assets/raw/master/go-prompt/tools/vt100_debug.gif) 6 | 7 | ### sigwinch 8 | 9 | ![sigwinch](https://github.com/c-bata/assets/raw/master/go-prompt/tools/sigwinch.gif) 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 | ![simple-input](https://github.com/c-bata/assets/raw/master/go-prompt/examples/input.gif) 9 | 10 | A simple echo example using `prompt.Input`. 11 | 12 | ## http-prompt 13 | 14 | ![http-prompt](https://github.com/c-bata/assets/raw/master/go-prompt/examples/http-prompt.gif) 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 | ![live-prefix](https://github.com/c-bata/assets/raw/master/go-prompt/examples/live-prefix.gif) 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 | [![Go Report Card](https://goreportcard.com/badge/github.com/c-bata/go-prompt)](https://goreportcard.com/report/github.com/c-bata/go-prompt) 4 | ![Software License](https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square) 5 | [![GoDoc](https://godoc.org/github.com/c-bata/go-prompt?status.svg)](https://godoc.org/github.com/c-bata/go-prompt) 6 | ![tests](https://github.com/c-bata/go-prompt/workflows/tests/badge.svg) 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 | [![demo](https://github.com/c-bata/assets/raw/master/go-prompt/kube-prompt.gif)](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 | [![options](https://github.com/c-bata/assets/raw/master/go-prompt/prompt-options.png)](#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](https://github.com/c-bata/assets/raw/master/go-prompt/keyboard-shortcuts.gif)](#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](https://github.com/c-bata/assets/raw/master/go-prompt/history.gif)](#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 | --------------------------------------------------------------------------------