├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── ansi_windows.go ├── complete.go ├── complete_helper.go ├── complete_segment.go ├── complete_segment_test.go ├── doc └── shortcut.md ├── example ├── readline-demo │ └── readline-demo.go ├── readline-im │ ├── README.md │ └── readline-im.go ├── readline-multiline │ └── readline-multiline.go ├── readline-pass-strength │ └── readline-pass-strength.go └── readline-remote │ ├── readline-remote-client │ └── client.go │ └── readline-remote-server │ └── server.go ├── go.mod ├── go.sum ├── history.go ├── operation.go ├── password.go ├── rawreader_windows.go ├── readline.go ├── readline_test.go ├── remote.go ├── runebuf.go ├── runes.go ├── runes ├── runes.go └── runes_test.go ├── runes_test.go ├── search.go ├── std.go ├── std_windows.go ├── term.go ├── term_bsd.go ├── term_linux.go ├── term_nosyscall6.go ├── term_unix.go ├── term_windows.go ├── terminal.go ├── utils.go ├── utils_test.go ├── utils_unix.go ├── utils_windows.go ├── vim.go └── windows_api.go /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode/* 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | go: 3 | - 1.x 4 | script: 5 | - GOOS=windows go install github.com/chzyer/readline/example/... 6 | - GOOS=linux go install github.com/chzyer/readline/example/... 7 | - GOOS=darwin go install github.com/chzyer/readline/example/... 8 | - go test -race -v 9 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # ChangeLog 2 | 3 | ### 1.4 - 2016-07-25 4 | 5 | * [#60][60] Support dynamic autocompletion 6 | * Fix ANSI parser on Windows 7 | * Fix wrong column width in complete mode on Windows 8 | * Remove dependent package "golang.org/x/crypto/ssh/terminal" 9 | 10 | ### 1.3 - 2016-05-09 11 | 12 | * [#38][38] add SetChildren for prefix completer interface 13 | * [#42][42] improve multiple lines compatibility 14 | * [#43][43] remove sub-package(runes) for gopkg compatibility 15 | * [#46][46] Auto complete with space prefixed line 16 | * [#48][48] support suspend process (ctrl+Z) 17 | * [#49][49] fix bug that check equals with previous command 18 | * [#53][53] Fix bug which causes integer divide by zero panicking when input buffer is empty 19 | 20 | ### 1.2 - 2016-03-05 21 | 22 | * Add a demo for checking password strength [example/readline-pass-strength](https://github.com/chzyer/readline/blob/master/example/readline-pass-strength/readline-pass-strength.go), , written by [@sahib](https://github.com/sahib) 23 | * [#23][23], support stdin remapping 24 | * [#27][27], add a `UniqueEditLine` to `Config`, which will erase the editing line after user submited it, usually use in IM. 25 | * Add a demo for multiline [example/readline-multiline](https://github.com/chzyer/readline/blob/master/example/readline-multiline/readline-multiline.go) which can submit one SQL by multiple lines. 26 | * Supports performs even stdin/stdout is not a tty. 27 | * Add a new simple apis for single instance, check by [here](https://github.com/chzyer/readline/blob/master/std.go). It need to save history manually if using this api. 28 | * [#28][28], fixes the history is not working as expected. 29 | * [#33][33], vim mode now support `c`, `d`, `x (delete character)`, `r (replace character)` 30 | 31 | ### 1.1 - 2015-11-20 32 | 33 | * [#12][12] Add support for key ``/``/`` 34 | * Only enter raw mode as needed (calling `Readline()`), program will receive signal(e.g. Ctrl+C) if not interact with `readline`. 35 | * Bugs fixed for `PrefixCompleter` 36 | * Press `Ctrl+D` in empty line will cause `io.EOF` in error, Press `Ctrl+C` in anytime will cause `ErrInterrupt` instead of `io.EOF`, this will privodes a shell-like user experience. 37 | * Customable Interrupt/EOF prompt in `Config` 38 | * [#17][17] Change atomic package to use 32bit function to let it runnable on arm 32bit devices 39 | * Provides a new password user experience(`readline.ReadPasswordEx()`). 40 | 41 | ### 1.0 - 2015-10-14 42 | 43 | * Initial public release. 44 | 45 | [12]: https://github.com/chzyer/readline/pull/12 46 | [17]: https://github.com/chzyer/readline/pull/17 47 | [23]: https://github.com/chzyer/readline/pull/23 48 | [27]: https://github.com/chzyer/readline/pull/27 49 | [28]: https://github.com/chzyer/readline/pull/28 50 | [33]: https://github.com/chzyer/readline/pull/33 51 | [38]: https://github.com/chzyer/readline/pull/38 52 | [42]: https://github.com/chzyer/readline/pull/42 53 | [43]: https://github.com/chzyer/readline/pull/43 54 | [46]: https://github.com/chzyer/readline/pull/46 55 | [48]: https://github.com/chzyer/readline/pull/48 56 | [49]: https://github.com/chzyer/readline/pull/49 57 | [53]: https://github.com/chzyer/readline/pull/53 58 | [60]: https://github.com/chzyer/readline/pull/60 59 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Chzyer 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 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/chzyer/readline.svg?branch=master)](https://travis-ci.org/chzyer/readline) 2 | [![Software License](https://img.shields.io/badge/license-MIT-brightgreen.svg)](LICENSE.md) 3 | [![Version](https://img.shields.io/github/tag/chzyer/readline.svg)](https://github.com/chzyer/readline/releases) 4 | [![GoDoc](https://pkg.go.dev/github.com/chzyer/readline?status.svg)](https://pkg.go.dev/github.com/chzyer/readline) 5 | 6 | The most popular multi-platform readline library for Go, featuring powerful line editing capabilities. 7 | 8 | ## Features 9 | 10 | - Multi-platform support 11 | - Line editing with common shortcut keys 12 | - History support with customizable persistence 13 | - Completion support 14 | - Custom prompt support 15 | 16 | ## Guide 17 | 18 | * [Demo](example/readline-demo/readline-demo.go) 19 | * [Keyboard Shortcuts](doc/shortcut.md) 20 | * [API Documentation](https://pkg.go.dev/github.com/chzyer/readline) 21 | 22 | ## Star History 23 | 24 | [![Star History Chart](https://api.star-history.com/svg?repos=chzyer/readline&type=Date)](https://star-history.com/#chzyer/readline&Date) 25 | 26 | ## Contributors 27 | 28 | 29 | Readline project contributors 30 | -------------------------------------------------------------------------------- /ansi_windows.go: -------------------------------------------------------------------------------- 1 | // +build windows 2 | 3 | package readline 4 | 5 | import ( 6 | "bufio" 7 | "io" 8 | "strconv" 9 | "strings" 10 | "sync" 11 | "unicode/utf8" 12 | "unsafe" 13 | ) 14 | 15 | const ( 16 | _ = uint16(0) 17 | COLOR_FBLUE = 0x0001 18 | COLOR_FGREEN = 0x0002 19 | COLOR_FRED = 0x0004 20 | COLOR_FINTENSITY = 0x0008 21 | 22 | COLOR_BBLUE = 0x0010 23 | COLOR_BGREEN = 0x0020 24 | COLOR_BRED = 0x0040 25 | COLOR_BINTENSITY = 0x0080 26 | 27 | COMMON_LVB_UNDERSCORE = 0x8000 28 | COMMON_LVB_BOLD = 0x0007 29 | ) 30 | 31 | var ColorTableFg = []word{ 32 | 0, // 30: Black 33 | COLOR_FRED, // 31: Red 34 | COLOR_FGREEN, // 32: Green 35 | COLOR_FRED | COLOR_FGREEN, // 33: Yellow 36 | COLOR_FBLUE, // 34: Blue 37 | COLOR_FRED | COLOR_FBLUE, // 35: Magenta 38 | COLOR_FGREEN | COLOR_FBLUE, // 36: Cyan 39 | COLOR_FRED | COLOR_FBLUE | COLOR_FGREEN, // 37: White 40 | } 41 | 42 | var ColorTableBg = []word{ 43 | 0, // 40: Black 44 | COLOR_BRED, // 41: Red 45 | COLOR_BGREEN, // 42: Green 46 | COLOR_BRED | COLOR_BGREEN, // 43: Yellow 47 | COLOR_BBLUE, // 44: Blue 48 | COLOR_BRED | COLOR_BBLUE, // 45: Magenta 49 | COLOR_BGREEN | COLOR_BBLUE, // 46: Cyan 50 | COLOR_BRED | COLOR_BBLUE | COLOR_BGREEN, // 47: White 51 | } 52 | 53 | type ANSIWriter struct { 54 | target io.Writer 55 | wg sync.WaitGroup 56 | ctx *ANSIWriterCtx 57 | sync.Mutex 58 | } 59 | 60 | func NewANSIWriter(w io.Writer) *ANSIWriter { 61 | a := &ANSIWriter{ 62 | target: w, 63 | ctx: NewANSIWriterCtx(w), 64 | } 65 | return a 66 | } 67 | 68 | func (a *ANSIWriter) Close() error { 69 | a.wg.Wait() 70 | return nil 71 | } 72 | 73 | type ANSIWriterCtx struct { 74 | isEsc bool 75 | isEscSeq bool 76 | arg []string 77 | target *bufio.Writer 78 | wantFlush bool 79 | } 80 | 81 | func NewANSIWriterCtx(target io.Writer) *ANSIWriterCtx { 82 | return &ANSIWriterCtx{ 83 | target: bufio.NewWriter(target), 84 | } 85 | } 86 | 87 | func (a *ANSIWriterCtx) Flush() { 88 | a.target.Flush() 89 | } 90 | 91 | func (a *ANSIWriterCtx) process(r rune) bool { 92 | if a.wantFlush { 93 | if r == 0 || r == CharEsc { 94 | a.wantFlush = false 95 | a.target.Flush() 96 | } 97 | } 98 | if a.isEscSeq { 99 | a.isEscSeq = a.ioloopEscSeq(a.target, r, &a.arg) 100 | return true 101 | } 102 | 103 | switch r { 104 | case CharEsc: 105 | a.isEsc = true 106 | case '[': 107 | if a.isEsc { 108 | a.arg = nil 109 | a.isEscSeq = true 110 | a.isEsc = false 111 | break 112 | } 113 | fallthrough 114 | default: 115 | a.target.WriteRune(r) 116 | a.wantFlush = true 117 | } 118 | return true 119 | } 120 | 121 | func (a *ANSIWriterCtx) ioloopEscSeq(w *bufio.Writer, r rune, argptr *[]string) bool { 122 | arg := *argptr 123 | var err error 124 | 125 | if r >= 'A' && r <= 'D' { 126 | count := short(GetInt(arg, 1)) 127 | info, err := GetConsoleScreenBufferInfo() 128 | if err != nil { 129 | return false 130 | } 131 | switch r { 132 | case 'A': // up 133 | info.dwCursorPosition.y -= count 134 | case 'B': // down 135 | info.dwCursorPosition.y += count 136 | case 'C': // right 137 | info.dwCursorPosition.x += count 138 | case 'D': // left 139 | info.dwCursorPosition.x -= count 140 | } 141 | SetConsoleCursorPosition(&info.dwCursorPosition) 142 | return false 143 | } 144 | 145 | switch r { 146 | case 'J': 147 | killLines() 148 | case 'K': 149 | eraseLine() 150 | case 'm': 151 | color := word(0) 152 | for _, item := range arg { 153 | var c int 154 | c, err = strconv.Atoi(item) 155 | if err != nil { 156 | w.WriteString("[" + strings.Join(arg, ";") + "m") 157 | break 158 | } 159 | if c >= 30 && c < 40 { 160 | color ^= COLOR_FINTENSITY 161 | color |= ColorTableFg[c-30] 162 | } else if c >= 40 && c < 50 { 163 | color ^= COLOR_BINTENSITY 164 | color |= ColorTableBg[c-40] 165 | } else if c == 4 { 166 | color |= COMMON_LVB_UNDERSCORE | ColorTableFg[7] 167 | } else if c == 1 { 168 | color |= COMMON_LVB_BOLD | COLOR_FINTENSITY 169 | } else { // unknown code treat as reset 170 | color = ColorTableFg[7] 171 | } 172 | } 173 | if err != nil { 174 | break 175 | } 176 | kernel.SetConsoleTextAttribute(stdout, uintptr(color)) 177 | case '\007': // set title 178 | case ';': 179 | if len(arg) == 0 || arg[len(arg)-1] != "" { 180 | arg = append(arg, "") 181 | *argptr = arg 182 | } 183 | return true 184 | default: 185 | if len(arg) == 0 { 186 | arg = append(arg, "") 187 | } 188 | arg[len(arg)-1] += string(r) 189 | *argptr = arg 190 | return true 191 | } 192 | *argptr = nil 193 | return false 194 | } 195 | 196 | func (a *ANSIWriter) Write(b []byte) (int, error) { 197 | a.Lock() 198 | defer a.Unlock() 199 | 200 | off := 0 201 | for len(b) > off { 202 | r, size := utf8.DecodeRune(b[off:]) 203 | if size == 0 { 204 | return off, io.ErrShortWrite 205 | } 206 | off += size 207 | a.ctx.process(r) 208 | } 209 | a.ctx.Flush() 210 | return off, nil 211 | } 212 | 213 | func killLines() error { 214 | sbi, err := GetConsoleScreenBufferInfo() 215 | if err != nil { 216 | return err 217 | } 218 | 219 | size := (sbi.dwCursorPosition.y - sbi.dwSize.y) * sbi.dwSize.x 220 | size += sbi.dwCursorPosition.x 221 | 222 | var written int 223 | kernel.FillConsoleOutputAttribute(stdout, uintptr(ColorTableFg[7]), 224 | uintptr(size), 225 | sbi.dwCursorPosition.ptr(), 226 | uintptr(unsafe.Pointer(&written)), 227 | ) 228 | return kernel.FillConsoleOutputCharacterW(stdout, uintptr(' '), 229 | uintptr(size), 230 | sbi.dwCursorPosition.ptr(), 231 | uintptr(unsafe.Pointer(&written)), 232 | ) 233 | } 234 | 235 | func eraseLine() error { 236 | sbi, err := GetConsoleScreenBufferInfo() 237 | if err != nil { 238 | return err 239 | } 240 | 241 | size := sbi.dwSize.x 242 | sbi.dwCursorPosition.x = 0 243 | var written int 244 | return kernel.FillConsoleOutputCharacterW(stdout, uintptr(' '), 245 | uintptr(size), 246 | sbi.dwCursorPosition.ptr(), 247 | uintptr(unsafe.Pointer(&written)), 248 | ) 249 | } 250 | -------------------------------------------------------------------------------- /complete.go: -------------------------------------------------------------------------------- 1 | package readline 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "fmt" 7 | "io" 8 | ) 9 | 10 | type AutoCompleter interface { 11 | // Readline will pass the whole line and current offset to it 12 | // Completer need to pass all the candidates, and how long they shared the same characters in line 13 | // Example: 14 | // [go, git, git-shell, grep] 15 | // Do("g", 1) => ["o", "it", "it-shell", "rep"], 1 16 | // Do("gi", 2) => ["t", "t-shell"], 2 17 | // Do("git", 3) => ["", "-shell"], 3 18 | Do(line []rune, pos int) (newLine [][]rune, length int) 19 | } 20 | 21 | type TabCompleter struct{} 22 | 23 | func (t *TabCompleter) Do([]rune, int) ([][]rune, int) { 24 | return [][]rune{[]rune("\t")}, 0 25 | } 26 | 27 | type opCompleter struct { 28 | w io.Writer 29 | op *Operation 30 | width int 31 | 32 | inCompleteMode bool 33 | inSelectMode bool 34 | candidate [][]rune 35 | candidateSource []rune 36 | candidateOff int 37 | candidateChoise int 38 | candidateColNum int 39 | } 40 | 41 | func newOpCompleter(w io.Writer, op *Operation, width int) *opCompleter { 42 | return &opCompleter{ 43 | w: w, 44 | op: op, 45 | width: width, 46 | } 47 | } 48 | 49 | func (o *opCompleter) doSelect() { 50 | if len(o.candidate) == 1 { 51 | o.op.buf.WriteRunes(o.candidate[0]) 52 | o.ExitCompleteMode(false) 53 | return 54 | } 55 | o.nextCandidate(1) 56 | o.CompleteRefresh() 57 | } 58 | 59 | func (o *opCompleter) nextCandidate(i int) { 60 | o.candidateChoise += i 61 | o.candidateChoise = o.candidateChoise % len(o.candidate) 62 | if o.candidateChoise < 0 { 63 | o.candidateChoise = len(o.candidate) + o.candidateChoise 64 | } 65 | } 66 | 67 | func (o *opCompleter) OnComplete() bool { 68 | if o.width == 0 { 69 | return false 70 | } 71 | if o.IsInCompleteSelectMode() { 72 | o.doSelect() 73 | return true 74 | } 75 | 76 | buf := o.op.buf 77 | rs := buf.Runes() 78 | 79 | if o.IsInCompleteMode() && o.candidateSource != nil && runes.Equal(rs, o.candidateSource) { 80 | o.EnterCompleteSelectMode() 81 | o.doSelect() 82 | return true 83 | } 84 | 85 | o.ExitCompleteSelectMode() 86 | o.candidateSource = rs 87 | newLines, offset := o.op.cfg.AutoComplete.Do(rs, buf.idx) 88 | if len(newLines) == 0 { 89 | o.ExitCompleteMode(false) 90 | return true 91 | } 92 | 93 | // only Aggregate candidates in non-complete mode 94 | if !o.IsInCompleteMode() { 95 | if len(newLines) == 1 { 96 | buf.WriteRunes(newLines[0]) 97 | o.ExitCompleteMode(false) 98 | return true 99 | } 100 | 101 | same, size := runes.Aggregate(newLines) 102 | if size > 0 { 103 | buf.WriteRunes(same) 104 | o.ExitCompleteMode(false) 105 | return true 106 | } 107 | } 108 | 109 | o.EnterCompleteMode(offset, newLines) 110 | return true 111 | } 112 | 113 | func (o *opCompleter) IsInCompleteSelectMode() bool { 114 | return o.inSelectMode 115 | } 116 | 117 | func (o *opCompleter) IsInCompleteMode() bool { 118 | return o.inCompleteMode 119 | } 120 | 121 | func (o *opCompleter) HandleCompleteSelect(r rune) bool { 122 | next := true 123 | switch r { 124 | case CharEnter, CharCtrlJ: 125 | next = false 126 | o.op.buf.WriteRunes(o.op.candidate[o.op.candidateChoise]) 127 | o.ExitCompleteMode(false) 128 | case CharLineStart: 129 | num := o.candidateChoise % o.candidateColNum 130 | o.nextCandidate(-num) 131 | case CharLineEnd: 132 | num := o.candidateColNum - o.candidateChoise%o.candidateColNum - 1 133 | o.candidateChoise += num 134 | if o.candidateChoise >= len(o.candidate) { 135 | o.candidateChoise = len(o.candidate) - 1 136 | } 137 | case CharBackspace: 138 | o.ExitCompleteSelectMode() 139 | next = false 140 | case CharTab, CharForward: 141 | o.doSelect() 142 | case CharBell, CharInterrupt: 143 | o.ExitCompleteMode(true) 144 | next = false 145 | case CharNext: 146 | tmpChoise := o.candidateChoise + o.candidateColNum 147 | if tmpChoise >= o.getMatrixSize() { 148 | tmpChoise -= o.getMatrixSize() 149 | } else if tmpChoise >= len(o.candidate) { 150 | tmpChoise += o.candidateColNum 151 | tmpChoise -= o.getMatrixSize() 152 | } 153 | o.candidateChoise = tmpChoise 154 | case CharBackward: 155 | o.nextCandidate(-1) 156 | case CharPrev: 157 | tmpChoise := o.candidateChoise - o.candidateColNum 158 | if tmpChoise < 0 { 159 | tmpChoise += o.getMatrixSize() 160 | if tmpChoise >= len(o.candidate) { 161 | tmpChoise -= o.candidateColNum 162 | } 163 | } 164 | o.candidateChoise = tmpChoise 165 | default: 166 | next = false 167 | o.ExitCompleteSelectMode() 168 | } 169 | if next { 170 | o.CompleteRefresh() 171 | return true 172 | } 173 | return false 174 | } 175 | 176 | func (o *opCompleter) getMatrixSize() int { 177 | line := len(o.candidate) / o.candidateColNum 178 | if len(o.candidate)%o.candidateColNum != 0 { 179 | line++ 180 | } 181 | return line * o.candidateColNum 182 | } 183 | 184 | func (o *opCompleter) OnWidthChange(newWidth int) { 185 | o.width = newWidth 186 | } 187 | 188 | func (o *opCompleter) CompleteRefresh() { 189 | if !o.inCompleteMode { 190 | return 191 | } 192 | lineCnt := o.op.buf.CursorLineCount() 193 | colWidth := 0 194 | for _, c := range o.candidate { 195 | w := runes.WidthAll(c) 196 | if w > colWidth { 197 | colWidth = w 198 | } 199 | } 200 | colWidth += o.candidateOff + 1 201 | same := o.op.buf.RuneSlice(-o.candidateOff) 202 | 203 | // -1 to avoid reach the end of line 204 | width := o.width - 1 205 | colNum := width / colWidth 206 | if colNum != 0 { 207 | colWidth += (width - (colWidth * colNum)) / colNum 208 | } 209 | 210 | o.candidateColNum = colNum 211 | buf := bufio.NewWriter(o.w) 212 | buf.Write(bytes.Repeat([]byte("\n"), lineCnt)) 213 | 214 | colIdx := 0 215 | lines := 1 216 | buf.WriteString("\033[J") 217 | for idx, c := range o.candidate { 218 | inSelect := idx == o.candidateChoise && o.IsInCompleteSelectMode() 219 | if inSelect { 220 | buf.WriteString("\033[30;47m") 221 | } 222 | buf.WriteString(string(same)) 223 | buf.WriteString(string(c)) 224 | buf.Write(bytes.Repeat([]byte(" "), colWidth-runes.WidthAll(c)-runes.WidthAll(same))) 225 | 226 | if inSelect { 227 | buf.WriteString("\033[0m") 228 | } 229 | 230 | colIdx++ 231 | if colIdx == colNum { 232 | buf.WriteString("\n") 233 | lines++ 234 | colIdx = 0 235 | } 236 | } 237 | 238 | // move back 239 | fmt.Fprintf(buf, "\033[%dA\r", lineCnt-1+lines) 240 | fmt.Fprintf(buf, "\033[%dC", o.op.buf.idx+o.op.buf.PromptLen()) 241 | buf.Flush() 242 | } 243 | 244 | func (o *opCompleter) aggCandidate(candidate [][]rune) int { 245 | offset := 0 246 | for i := 0; i < len(candidate[0]); i++ { 247 | for j := 0; j < len(candidate)-1; j++ { 248 | if i > len(candidate[j]) { 249 | goto aggregate 250 | } 251 | if candidate[j][i] != candidate[j+1][i] { 252 | goto aggregate 253 | } 254 | } 255 | offset = i 256 | } 257 | aggregate: 258 | return offset 259 | } 260 | 261 | func (o *opCompleter) EnterCompleteSelectMode() { 262 | o.inSelectMode = true 263 | o.candidateChoise = -1 264 | o.CompleteRefresh() 265 | } 266 | 267 | func (o *opCompleter) EnterCompleteMode(offset int, candidate [][]rune) { 268 | o.inCompleteMode = true 269 | o.candidate = candidate 270 | o.candidateOff = offset 271 | o.CompleteRefresh() 272 | } 273 | 274 | func (o *opCompleter) ExitCompleteSelectMode() { 275 | o.inSelectMode = false 276 | o.candidate = nil 277 | o.candidateChoise = -1 278 | o.candidateOff = -1 279 | o.candidateSource = nil 280 | } 281 | 282 | func (o *opCompleter) ExitCompleteMode(revent bool) { 283 | o.inCompleteMode = false 284 | o.ExitCompleteSelectMode() 285 | } 286 | -------------------------------------------------------------------------------- /complete_helper.go: -------------------------------------------------------------------------------- 1 | package readline 2 | 3 | import ( 4 | "bytes" 5 | "strings" 6 | ) 7 | 8 | // Caller type for dynamic completion 9 | type DynamicCompleteFunc func(string) []string 10 | 11 | type PrefixCompleterInterface interface { 12 | Print(prefix string, level int, buf *bytes.Buffer) 13 | Do(line []rune, pos int) (newLine [][]rune, length int) 14 | GetName() []rune 15 | GetChildren() []PrefixCompleterInterface 16 | SetChildren(children []PrefixCompleterInterface) 17 | } 18 | 19 | type DynamicPrefixCompleterInterface interface { 20 | PrefixCompleterInterface 21 | IsDynamic() bool 22 | GetDynamicNames(line []rune) [][]rune 23 | } 24 | 25 | type PrefixCompleter struct { 26 | Name []rune 27 | Dynamic bool 28 | Callback DynamicCompleteFunc 29 | Children []PrefixCompleterInterface 30 | } 31 | 32 | func (p *PrefixCompleter) Tree(prefix string) string { 33 | buf := bytes.NewBuffer(nil) 34 | p.Print(prefix, 0, buf) 35 | return buf.String() 36 | } 37 | 38 | func Print(p PrefixCompleterInterface, prefix string, level int, buf *bytes.Buffer) { 39 | if strings.TrimSpace(string(p.GetName())) != "" { 40 | buf.WriteString(prefix) 41 | if level > 0 { 42 | buf.WriteString("├") 43 | buf.WriteString(strings.Repeat("─", (level*4)-2)) 44 | buf.WriteString(" ") 45 | } 46 | buf.WriteString(string(p.GetName()) + "\n") 47 | level++ 48 | } 49 | for _, ch := range p.GetChildren() { 50 | ch.Print(prefix, level, buf) 51 | } 52 | } 53 | 54 | func (p *PrefixCompleter) Print(prefix string, level int, buf *bytes.Buffer) { 55 | Print(p, prefix, level, buf) 56 | } 57 | 58 | func (p *PrefixCompleter) IsDynamic() bool { 59 | return p.Dynamic 60 | } 61 | 62 | func (p *PrefixCompleter) GetName() []rune { 63 | return p.Name 64 | } 65 | 66 | func (p *PrefixCompleter) GetDynamicNames(line []rune) [][]rune { 67 | var names = [][]rune{} 68 | for _, name := range p.Callback(string(line)) { 69 | names = append(names, []rune(name+" ")) 70 | } 71 | return names 72 | } 73 | 74 | func (p *PrefixCompleter) GetChildren() []PrefixCompleterInterface { 75 | return p.Children 76 | } 77 | 78 | func (p *PrefixCompleter) SetChildren(children []PrefixCompleterInterface) { 79 | p.Children = children 80 | } 81 | 82 | func NewPrefixCompleter(pc ...PrefixCompleterInterface) *PrefixCompleter { 83 | return PcItem("", pc...) 84 | } 85 | 86 | func PcItem(name string, pc ...PrefixCompleterInterface) *PrefixCompleter { 87 | name += " " 88 | return &PrefixCompleter{ 89 | Name: []rune(name), 90 | Dynamic: false, 91 | Children: pc, 92 | } 93 | } 94 | 95 | func PcItemDynamic(callback DynamicCompleteFunc, pc ...PrefixCompleterInterface) *PrefixCompleter { 96 | return &PrefixCompleter{ 97 | Callback: callback, 98 | Dynamic: true, 99 | Children: pc, 100 | } 101 | } 102 | 103 | func (p *PrefixCompleter) Do(line []rune, pos int) (newLine [][]rune, offset int) { 104 | return doInternal(p, line, pos, line) 105 | } 106 | 107 | func Do(p PrefixCompleterInterface, line []rune, pos int) (newLine [][]rune, offset int) { 108 | return doInternal(p, line, pos, line) 109 | } 110 | 111 | func doInternal(p PrefixCompleterInterface, line []rune, pos int, origLine []rune) (newLine [][]rune, offset int) { 112 | line = runes.TrimSpaceLeft(line[:pos]) 113 | goNext := false 114 | var lineCompleter PrefixCompleterInterface 115 | for _, child := range p.GetChildren() { 116 | childNames := make([][]rune, 1) 117 | 118 | childDynamic, ok := child.(DynamicPrefixCompleterInterface) 119 | if ok && childDynamic.IsDynamic() { 120 | childNames = childDynamic.GetDynamicNames(origLine) 121 | } else { 122 | childNames[0] = child.GetName() 123 | } 124 | 125 | for _, childName := range childNames { 126 | if len(line) >= len(childName) { 127 | if runes.HasPrefix(line, childName) { 128 | if len(line) == len(childName) { 129 | newLine = append(newLine, []rune{' '}) 130 | } else { 131 | newLine = append(newLine, childName) 132 | } 133 | offset = len(childName) 134 | lineCompleter = child 135 | goNext = true 136 | } 137 | } else { 138 | if runes.HasPrefix(childName, line) { 139 | newLine = append(newLine, childName[len(line):]) 140 | offset = len(line) 141 | lineCompleter = child 142 | } 143 | } 144 | } 145 | } 146 | 147 | if len(newLine) != 1 { 148 | return 149 | } 150 | 151 | tmpLine := make([]rune, 0, len(line)) 152 | for i := offset; i < len(line); i++ { 153 | if line[i] == ' ' { 154 | continue 155 | } 156 | 157 | tmpLine = append(tmpLine, line[i:]...) 158 | return doInternal(lineCompleter, tmpLine, len(tmpLine), origLine) 159 | } 160 | 161 | if goNext { 162 | return doInternal(lineCompleter, nil, 0, origLine) 163 | } 164 | return 165 | } 166 | -------------------------------------------------------------------------------- /complete_segment.go: -------------------------------------------------------------------------------- 1 | package readline 2 | 3 | type SegmentCompleter interface { 4 | // a 5 | // |- a1 6 | // |--- a11 7 | // |- a2 8 | // b 9 | // input: 10 | // DoTree([], 0) [a, b] 11 | // DoTree([a], 1) [a] 12 | // DoTree([a, ], 0) [a1, a2] 13 | // DoTree([a, a], 1) [a1, a2] 14 | // DoTree([a, a1], 2) [a1] 15 | // DoTree([a, a1, ], 0) [a11] 16 | // DoTree([a, a1, a], 1) [a11] 17 | DoSegment([][]rune, int) [][]rune 18 | } 19 | 20 | type dumpSegmentCompleter struct { 21 | f func([][]rune, int) [][]rune 22 | } 23 | 24 | func (d *dumpSegmentCompleter) DoSegment(segment [][]rune, n int) [][]rune { 25 | return d.f(segment, n) 26 | } 27 | 28 | func SegmentFunc(f func([][]rune, int) [][]rune) AutoCompleter { 29 | return &SegmentComplete{&dumpSegmentCompleter{f}} 30 | } 31 | 32 | func SegmentAutoComplete(completer SegmentCompleter) *SegmentComplete { 33 | return &SegmentComplete{ 34 | SegmentCompleter: completer, 35 | } 36 | } 37 | 38 | type SegmentComplete struct { 39 | SegmentCompleter 40 | } 41 | 42 | func RetSegment(segments [][]rune, cands [][]rune, idx int) ([][]rune, int) { 43 | ret := make([][]rune, 0, len(cands)) 44 | lastSegment := segments[len(segments)-1] 45 | for _, cand := range cands { 46 | if !runes.HasPrefix(cand, lastSegment) { 47 | continue 48 | } 49 | ret = append(ret, cand[len(lastSegment):]) 50 | } 51 | return ret, idx 52 | } 53 | 54 | func SplitSegment(line []rune, pos int) ([][]rune, int) { 55 | segs := [][]rune{} 56 | lastIdx := -1 57 | line = line[:pos] 58 | pos = 0 59 | for idx, l := range line { 60 | if l == ' ' { 61 | pos = 0 62 | segs = append(segs, line[lastIdx+1:idx]) 63 | lastIdx = idx 64 | } else { 65 | pos++ 66 | } 67 | } 68 | segs = append(segs, line[lastIdx+1:]) 69 | return segs, pos 70 | } 71 | 72 | func (c *SegmentComplete) Do(line []rune, pos int) (newLine [][]rune, offset int) { 73 | 74 | segment, idx := SplitSegment(line, pos) 75 | 76 | cands := c.DoSegment(segment, idx) 77 | newLine, offset = RetSegment(segment, cands, idx) 78 | for idx := range newLine { 79 | newLine[idx] = append(newLine[idx], ' ') 80 | } 81 | return newLine, offset 82 | } 83 | -------------------------------------------------------------------------------- /complete_segment_test.go: -------------------------------------------------------------------------------- 1 | package readline 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/chzyer/test" 8 | ) 9 | 10 | func rs(s [][]rune) []string { 11 | ret := make([]string, len(s)) 12 | for idx, ss := range s { 13 | ret[idx] = string(ss) 14 | } 15 | return ret 16 | } 17 | 18 | func sr(s ...string) [][]rune { 19 | ret := make([][]rune, len(s)) 20 | for idx, ss := range s { 21 | ret[idx] = []rune(ss) 22 | } 23 | return ret 24 | } 25 | 26 | func TestRetSegment(t *testing.T) { 27 | defer test.New(t) 28 | // a 29 | // |- a1 30 | // |--- a11 31 | // |--- a12 32 | // |- a2 33 | // |--- a21 34 | // b 35 | // add 36 | // adddomain 37 | ret := []struct { 38 | Segments [][]rune 39 | Cands [][]rune 40 | idx int 41 | Ret [][]rune 42 | pos int 43 | }{ 44 | {sr(""), sr("a", "b", "add", "adddomain"), 0, sr("a", "b", "add", "adddomain"), 0}, 45 | {sr("a"), sr("a", "add", "adddomain"), 1, sr("", "dd", "dddomain"), 1}, 46 | {sr("a", ""), sr("a1", "a2"), 0, sr("a1", "a2"), 0}, 47 | {sr("a", "a"), sr("a1", "a2"), 1, sr("1", "2"), 1}, 48 | {sr("a", "a1"), sr("a1"), 2, sr(""), 2}, 49 | {sr("add"), sr("add", "adddomain"), 2, sr("", "domain"), 2}, 50 | } 51 | for idx, r := range ret { 52 | ret, pos := RetSegment(r.Segments, r.Cands, r.idx) 53 | test.Equal(ret, r.Ret, fmt.Errorf("%v", idx)) 54 | test.Equal(pos, r.pos, fmt.Errorf("%v", idx)) 55 | } 56 | } 57 | 58 | func TestSplitSegment(t *testing.T) { 59 | defer test.New(t) 60 | // a 61 | // |- a1 62 | // |--- a11 63 | // |--- a12 64 | // |- a2 65 | // |--- a21 66 | // b 67 | ret := []struct { 68 | Line string 69 | Pos int 70 | Segments [][]rune 71 | Idx int 72 | }{ 73 | {"", 0, sr(""), 0}, 74 | {"a", 1, sr("a"), 1}, 75 | {"a ", 2, sr("a", ""), 0}, 76 | {"a a", 3, sr("a", "a"), 1}, 77 | {"a a1", 4, sr("a", "a1"), 2}, 78 | {"a a1 ", 5, sr("a", "a1", ""), 0}, 79 | } 80 | 81 | for i, r := range ret { 82 | ret, idx := SplitSegment([]rune(r.Line), r.Pos) 83 | test.Equal(rs(ret), rs(r.Segments), fmt.Errorf("%v", i)) 84 | test.Equal(idx, r.Idx, fmt.Errorf("%v", i)) 85 | } 86 | } 87 | 88 | type Tree struct { 89 | Name string 90 | Children []Tree 91 | } 92 | 93 | func TestSegmentCompleter(t *testing.T) { 94 | defer test.New(t) 95 | 96 | tree := Tree{"", []Tree{ 97 | {"a", []Tree{ 98 | {"a1", []Tree{ 99 | {"a11", nil}, 100 | {"a12", nil}, 101 | }}, 102 | {"a2", []Tree{ 103 | {"a21", nil}, 104 | }}, 105 | }}, 106 | {"b", nil}, 107 | {"route", []Tree{ 108 | {"add", nil}, 109 | {"adddomain", nil}, 110 | }}, 111 | }} 112 | s := SegmentFunc(func(ret [][]rune, n int) [][]rune { 113 | tree := tree 114 | main: 115 | for level := 0; level < len(ret)-1; { 116 | name := string(ret[level]) 117 | for _, t := range tree.Children { 118 | if t.Name == name { 119 | tree = t 120 | level++ 121 | continue main 122 | } 123 | } 124 | } 125 | 126 | ret = make([][]rune, len(tree.Children)) 127 | for idx, r := range tree.Children { 128 | ret[idx] = []rune(r.Name) 129 | } 130 | return ret 131 | }) 132 | 133 | // a 134 | // |- a1 135 | // |--- a11 136 | // |--- a12 137 | // |- a2 138 | // |--- a21 139 | // b 140 | ret := []struct { 141 | Line string 142 | Pos int 143 | Ret [][]rune 144 | Share int 145 | }{ 146 | {"", 0, sr("a", "b", "route"), 0}, 147 | {"a", 1, sr(""), 1}, 148 | {"a ", 2, sr("a1", "a2"), 0}, 149 | {"a a", 3, sr("1", "2"), 1}, 150 | {"a a1", 4, sr(""), 2}, 151 | {"a a1 ", 5, sr("a11", "a12"), 0}, 152 | {"a a1 a", 6, sr("11", "12"), 1}, 153 | {"a a1 a1", 7, sr("1", "2"), 2}, 154 | {"a a1 a11", 8, sr(""), 3}, 155 | {"route add", 9, sr("", "domain"), 3}, 156 | } 157 | for _, r := range ret { 158 | for idx, rr := range r.Ret { 159 | r.Ret[idx] = append(rr, ' ') 160 | } 161 | } 162 | for i, r := range ret { 163 | newLine, length := s.Do([]rune(r.Line), r.Pos) 164 | test.Equal(rs(newLine), rs(r.Ret), fmt.Errorf("%v", i)) 165 | test.Equal(length, r.Share, fmt.Errorf("%v", i)) 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /doc/shortcut.md: -------------------------------------------------------------------------------- 1 | ## Readline Shortcut 2 | 3 | `Meta`+`B` means press `Esc` and `n` separately. 4 | Users can change that in terminal simulator(i.e. iTerm2) to `Alt`+`B` 5 | Notice: `Meta`+`B` is equals with `Alt`+`B` in windows. 6 | 7 | * Shortcut in normal mode 8 | 9 | | Shortcut | Comment | 10 | | ------------------ | --------------------------------- | 11 | | `Ctrl`+`A` | Beginning of line | 12 | | `Ctrl`+`B` / `←` | Backward one character | 13 | | `Meta`+`B` | Backward one word | 14 | | `Ctrl`+`C` | Send io.EOF | 15 | | `Ctrl`+`D` | Delete one character | 16 | | `Meta`+`D` | Delete one word | 17 | | `Ctrl`+`E` | End of line | 18 | | `Ctrl`+`F` / `→` | Forward one character | 19 | | `Meta`+`F` | Forward one word | 20 | | `Ctrl`+`G` | Cancel | 21 | | `Ctrl`+`H` | Delete previous character | 22 | | `Ctrl`+`I` / `Tab` | Command line completion | 23 | | `Ctrl`+`J` | Line feed | 24 | | `Ctrl`+`K` | Cut text to the end of line | 25 | | `Ctrl`+`L` | Clear screen | 26 | | `Ctrl`+`M` | Same as Enter key | 27 | | `Ctrl`+`N` / `↓` | Next line (in history) | 28 | | `Ctrl`+`P` / `↑` | Prev line (in history) | 29 | | `Ctrl`+`R` | Search backwards in history | 30 | | `Ctrl`+`S` | Search forwards in history | 31 | | `Ctrl`+`T` | Transpose characters | 32 | | `Meta`+`T` | Transpose words (TODO) | 33 | | `Ctrl`+`U` | Cut text to the beginning of line | 34 | | `Ctrl`+`W` | Cut previous word | 35 | | `Backspace` | Delete previous character | 36 | | `Meta`+`Backspace` | Cut previous word | 37 | | `Enter` | Line feed | 38 | 39 | 40 | * Shortcut in Search Mode (`Ctrl`+`S` or `Ctrl`+`r` to enter this mode) 41 | 42 | | Shortcut | Comment | 43 | | ----------------------- | --------------------------------------- | 44 | | `Ctrl`+`S` | Search forwards in history | 45 | | `Ctrl`+`R` | Search backwards in history | 46 | | `Ctrl`+`C` / `Ctrl`+`G` | Exit Search Mode and revert the history | 47 | | `Backspace` | Delete previous character | 48 | | Other | Exit Search Mode | 49 | 50 | * Shortcut in Complete Select Mode (double `Tab` to enter this mode) 51 | 52 | | Shortcut | Comment | 53 | | ----------------------- | ---------------------------------------- | 54 | | `Ctrl`+`F` | Move Forward | 55 | | `Ctrl`+`B` | Move Backward | 56 | | `Ctrl`+`N` | Move to next line | 57 | | `Ctrl`+`P` | Move to previous line | 58 | | `Ctrl`+`A` | Move to the first candicate in current line | 59 | | `Ctrl`+`E` | Move to the last candicate in current line | 60 | | `Tab` / `Enter` | Use the word on cursor to complete | 61 | | `Ctrl`+`C` / `Ctrl`+`G` | Exit Complete Select Mode | 62 | | Other | Exit Complete Select Mode | -------------------------------------------------------------------------------- /example/readline-demo/readline-demo.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "log" 7 | "strconv" 8 | "strings" 9 | "time" 10 | 11 | "github.com/chzyer/readline" 12 | ) 13 | 14 | func usage(w io.Writer) { 15 | io.WriteString(w, "commands:\n") 16 | io.WriteString(w, completer.Tree(" ")) 17 | } 18 | 19 | // Function constructor - constructs new function for listing given directory 20 | func listFiles(path string) func(string) []string { 21 | return func(line string) []string { 22 | names := make([]string, 0) 23 | files, _ := os.ReadDir(path) 24 | for _, f := range files { 25 | names = append(names, f.Name()) 26 | } 27 | return names 28 | } 29 | } 30 | 31 | var completer = readline.NewPrefixCompleter( 32 | readline.PcItem("mode", 33 | readline.PcItem("vi"), 34 | readline.PcItem("emacs"), 35 | ), 36 | readline.PcItem("login"), 37 | readline.PcItem("say", 38 | readline.PcItemDynamic(listFiles("./"), 39 | readline.PcItem("with", 40 | readline.PcItem("following"), 41 | readline.PcItem("items"), 42 | ), 43 | ), 44 | readline.PcItem("hello"), 45 | readline.PcItem("bye"), 46 | ), 47 | readline.PcItem("setprompt"), 48 | readline.PcItem("setpassword"), 49 | readline.PcItem("bye"), 50 | readline.PcItem("help"), 51 | readline.PcItem("go", 52 | readline.PcItem("build", readline.PcItem("-o"), readline.PcItem("-v")), 53 | readline.PcItem("install", 54 | readline.PcItem("-v"), 55 | readline.PcItem("-vv"), 56 | readline.PcItem("-vvv"), 57 | ), 58 | readline.PcItem("test"), 59 | ), 60 | readline.PcItem("sleep"), 61 | ) 62 | 63 | func filterInput(r rune) (rune, bool) { 64 | switch r { 65 | // block CtrlZ feature 66 | case readline.CharCtrlZ: 67 | return r, false 68 | } 69 | return r, true 70 | } 71 | 72 | func main() { 73 | l, err := readline.NewEx(&readline.Config{ 74 | Prompt: "\033[31m»\033[0m ", 75 | HistoryFile: "/tmp/readline.tmp", 76 | AutoComplete: completer, 77 | InterruptPrompt: "^C", 78 | EOFPrompt: "exit", 79 | 80 | HistorySearchFold: true, 81 | FuncFilterInputRune: filterInput, 82 | }) 83 | if err != nil { 84 | panic(err) 85 | } 86 | defer l.Close() 87 | l.CaptureExitSignal() 88 | 89 | setPasswordCfg := l.GenPasswordConfig() 90 | setPasswordCfg.SetListener(func(line []rune, pos int, key rune) (newLine []rune, newPos int, ok bool) { 91 | l.SetPrompt(fmt.Sprintf("Enter password(%v): ", len(line))) 92 | l.Refresh() 93 | return nil, 0, false 94 | }) 95 | 96 | log.SetOutput(l.Stderr()) 97 | for { 98 | line, err := l.Readline() 99 | if err == readline.ErrInterrupt { 100 | if len(line) == 0 { 101 | break 102 | } else { 103 | continue 104 | } 105 | } else if err == io.EOF { 106 | break 107 | } 108 | 109 | line = strings.TrimSpace(line) 110 | switch { 111 | case strings.HasPrefix(line, "mode "): 112 | switch line[5:] { 113 | case "vi": 114 | l.SetVimMode(true) 115 | case "emacs": 116 | l.SetVimMode(false) 117 | default: 118 | println("invalid mode:", line[5:]) 119 | } 120 | case line == "mode": 121 | if l.IsVimMode() { 122 | println("current mode: vim") 123 | } else { 124 | println("current mode: emacs") 125 | } 126 | case line == "login": 127 | pswd, err := l.ReadPassword("please enter your password: ") 128 | if err != nil { 129 | break 130 | } 131 | println("you enter:", strconv.Quote(string(pswd))) 132 | case line == "help": 133 | usage(l.Stderr()) 134 | case line == "setpassword": 135 | pswd, err := l.ReadPasswordWithConfig(setPasswordCfg) 136 | if err == nil { 137 | println("you set:", strconv.Quote(string(pswd))) 138 | } 139 | case strings.HasPrefix(line, "setprompt"): 140 | if len(line) <= 10 { 141 | log.Println("setprompt ") 142 | break 143 | } 144 | l.SetPrompt(line[10:]) 145 | case strings.HasPrefix(line, "say"): 146 | line := strings.TrimSpace(line[3:]) 147 | if len(line) == 0 { 148 | log.Println("say what?") 149 | break 150 | } 151 | go func() { 152 | for range time.Tick(time.Second) { 153 | log.Println(line) 154 | } 155 | }() 156 | case line == "bye": 157 | goto exit 158 | case line == "sleep": 159 | log.Println("sleep 4 second") 160 | time.Sleep(4 * time.Second) 161 | case line == "": 162 | default: 163 | log.Println("you said:", strconv.Quote(line)) 164 | } 165 | } 166 | exit: 167 | } 168 | -------------------------------------------------------------------------------- /example/readline-im/README.md: -------------------------------------------------------------------------------- 1 | # readline-im 2 | 3 | ![readline-im](https://dl.dropboxusercontent.com/s/52hc7bo92g3pgi5/03F93B8D-9B4B-4D35-BBAA-22FBDAC7F299-26173-000164AA33980001.gif?dl=0) 4 | -------------------------------------------------------------------------------- /example/readline-im/readline-im.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "math/rand" 6 | "time" 7 | 8 | "github.com/chzyer/readline" 9 | ) 10 | import "log" 11 | 12 | func main() { 13 | rl, err := readline.NewEx(&readline.Config{ 14 | UniqueEditLine: true, 15 | }) 16 | if err != nil { 17 | panic(err) 18 | } 19 | defer rl.Close() 20 | 21 | rl.SetPrompt("username: ") 22 | username, err := rl.Readline() 23 | if err != nil { 24 | return 25 | } 26 | rl.ResetHistory() 27 | log.SetOutput(rl.Stderr()) 28 | 29 | fmt.Fprintln(rl, "Hi,", username+"! My name is Dave.") 30 | rl.SetPrompt(username + "> ") 31 | 32 | done := make(chan struct{}) 33 | go func() { 34 | rand.Seed(time.Now().Unix()) 35 | loop: 36 | for { 37 | select { 38 | case <-time.After(time.Duration(rand.Intn(20)) * 100 * time.Millisecond): 39 | case <-done: 40 | break loop 41 | } 42 | log.Println("Dave:", "hello") 43 | } 44 | log.Println("Dave:", "bye") 45 | done <- struct{}{} 46 | }() 47 | 48 | for { 49 | ln := rl.Line() 50 | if ln.CanContinue() { 51 | continue 52 | } else if ln.CanBreak() { 53 | break 54 | } 55 | log.Println(username+":", ln.Line) 56 | } 57 | rl.Clean() 58 | done <- struct{}{} 59 | <-done 60 | } 61 | -------------------------------------------------------------------------------- /example/readline-multiline/readline-multiline.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/chzyer/readline" 7 | ) 8 | 9 | func main() { 10 | rl, err := readline.NewEx(&readline.Config{ 11 | Prompt: "> ", 12 | HistoryFile: "/tmp/readline-multiline", 13 | DisableAutoSaveHistory: true, 14 | }) 15 | if err != nil { 16 | panic(err) 17 | } 18 | defer rl.Close() 19 | 20 | var cmds []string 21 | for { 22 | line, err := rl.Readline() 23 | if err != nil { 24 | break 25 | } 26 | line = strings.TrimSpace(line) 27 | if len(line) == 0 { 28 | continue 29 | } 30 | cmds = append(cmds, line) 31 | if !strings.HasSuffix(line, ";") { 32 | rl.SetPrompt(">>> ") 33 | continue 34 | } 35 | cmd := strings.Join(cmds, " ") 36 | cmds = cmds[:0] 37 | rl.SetPrompt("> ") 38 | rl.SaveHistory(cmd) 39 | println(cmd) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /example/readline-pass-strength/readline-pass-strength.go: -------------------------------------------------------------------------------- 1 | // This is a small example using readline to read a password 2 | // and check it's strength while typing using the zxcvbn library. 3 | // Depending on the strength the prompt is colored nicely to indicate strength. 4 | // 5 | // This file is licensed under the WTFPL: 6 | // 7 | // DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE 8 | // Version 2, December 2004 9 | // 10 | // Copyright (C) 2004 Sam Hocevar 11 | // 12 | // Everyone is permitted to copy and distribute verbatim or modified 13 | // copies of this license document, and changing it is allowed as long 14 | // as the name is changed. 15 | // 16 | // DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE 17 | // TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 18 | // 19 | // 0. You just DO WHAT THE FUCK YOU WANT TO. 20 | package main 21 | 22 | import ( 23 | "fmt" 24 | 25 | "github.com/chzyer/readline" 26 | ) 27 | 28 | const ( 29 | Cyan = 36 30 | Green = 32 31 | Magenta = 35 32 | Red = 31 33 | Yellow = 33 34 | BackgroundRed = 41 35 | ) 36 | 37 | // Reset sequence 38 | var ColorResetEscape = "\033[0m" 39 | 40 | // ColorResetEscape translates a ANSI color number to a color escape. 41 | func ColorEscape(color int) string { 42 | return fmt.Sprintf("\033[0;%dm", color) 43 | } 44 | 45 | // Colorize the msg using ANSI color escapes 46 | func Colorize(msg string, color int) string { 47 | return ColorEscape(color) + msg + ColorResetEscape 48 | } 49 | 50 | func createStrengthPrompt(password []rune) string { 51 | symbol, color := "", Red 52 | 53 | switch { 54 | case len(password) <= 1: 55 | symbol = "✗" 56 | color = Red 57 | case len(password) <= 3: 58 | symbol = "⚡" 59 | color = Magenta 60 | case len(password) <= 5: 61 | symbol = "⚠" 62 | color = Yellow 63 | default: 64 | symbol = "✔" 65 | color = Green 66 | } 67 | 68 | prompt := Colorize(symbol, color) 69 | prompt += Colorize(" ENT", Cyan) 70 | 71 | prompt += Colorize(" New Password: ", color) 72 | return prompt 73 | } 74 | 75 | func main() { 76 | rl, err := readline.New("") 77 | if err != nil { 78 | return 79 | } 80 | defer rl.Close() 81 | 82 | setPasswordCfg := rl.GenPasswordConfig() 83 | setPasswordCfg.SetListener(func(line []rune, pos int, key rune) (newLine []rune, newPos int, ok bool) { 84 | rl.SetPrompt(createStrengthPrompt(line)) 85 | rl.Refresh() 86 | return nil, 0, false 87 | }) 88 | 89 | pswd, err := rl.ReadPasswordWithConfig(setPasswordCfg) 90 | if err != nil { 91 | return 92 | } 93 | 94 | fmt.Println("Your password was:", string(pswd)) 95 | } 96 | -------------------------------------------------------------------------------- /example/readline-remote/readline-remote-client/client.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "github.com/chzyer/readline" 4 | 5 | func main() { 6 | if err := readline.DialRemote("tcp", ":12344"); err != nil { 7 | println(err.Error()) 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /example/readline-remote/readline-remote-server/server.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/chzyer/readline" 7 | ) 8 | 9 | func main() { 10 | cfg := &readline.Config{ 11 | Prompt: "readline-remote: ", 12 | } 13 | handleFunc := func(rl *readline.Instance) { 14 | for { 15 | line, err := rl.Readline() 16 | if err != nil { 17 | break 18 | } 19 | fmt.Fprintln(rl.Stdout(), "receive:"+line) 20 | } 21 | } 22 | err := readline.ListenRemote("tcp", ":12344", cfg, handleFunc) 23 | if err != nil { 24 | println(err.Error()) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/chzyer/readline 2 | 3 | go 1.15 4 | 5 | require ( 6 | github.com/chzyer/test v1.0.0 7 | golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5 8 | ) 9 | 10 | require github.com/chzyer/logex v1.2.1 11 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/chzyer/logex v1.2.1 h1:XHDu3E6q+gdHgsdTPH6ImJMIp436vR6MPtH8gP05QzM= 2 | github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ= 3 | github.com/chzyer/test v1.0.0 h1:p3BQDXSxOhOG0P9z6/hGnII4LGiEPOYBhs8asl/fC04= 4 | github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8= 5 | golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5 h1:y/woIyUBFbpQGKS0u1aHF/40WUDnek3fPOyD08H5Vng= 6 | golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 7 | -------------------------------------------------------------------------------- /history.go: -------------------------------------------------------------------------------- 1 | package readline 2 | 3 | import ( 4 | "bufio" 5 | "container/list" 6 | "fmt" 7 | "os" 8 | "strings" 9 | "sync" 10 | ) 11 | 12 | type hisItem struct { 13 | Source []rune 14 | Version int64 15 | Tmp []rune 16 | } 17 | 18 | func (h *hisItem) Clean() { 19 | h.Source = nil 20 | h.Tmp = nil 21 | } 22 | 23 | type opHistory struct { 24 | cfg *Config 25 | history *list.List 26 | historyVer int64 27 | current *list.Element 28 | fd *os.File 29 | fdLock sync.Mutex 30 | enable bool 31 | } 32 | 33 | func newOpHistory(cfg *Config) (o *opHistory) { 34 | o = &opHistory{ 35 | cfg: cfg, 36 | history: list.New(), 37 | enable: true, 38 | } 39 | return o 40 | } 41 | 42 | func (o *opHistory) Reset() { 43 | o.history = list.New() 44 | o.current = nil 45 | } 46 | 47 | func (o *opHistory) IsHistoryClosed() bool { 48 | o.fdLock.Lock() 49 | defer o.fdLock.Unlock() 50 | return o.fd.Fd() == ^(uintptr(0)) 51 | } 52 | 53 | func (o *opHistory) Init() { 54 | if o.IsHistoryClosed() { 55 | o.initHistory() 56 | } 57 | } 58 | 59 | func (o *opHistory) initHistory() { 60 | if o.cfg.HistoryFile != "" { 61 | o.historyUpdatePath(o.cfg.HistoryFile) 62 | } 63 | } 64 | 65 | // only called by newOpHistory 66 | func (o *opHistory) historyUpdatePath(path string) { 67 | o.fdLock.Lock() 68 | defer o.fdLock.Unlock() 69 | f, err := os.OpenFile(path, os.O_APPEND|os.O_CREATE|os.O_RDWR, 0666) 70 | if err != nil { 71 | return 72 | } 73 | o.fd = f 74 | r := bufio.NewReader(o.fd) 75 | total := 0 76 | for ; ; total++ { 77 | line, err := r.ReadString('\n') 78 | if err != nil { 79 | break 80 | } 81 | // ignore the empty line 82 | line = strings.TrimSpace(line) 83 | if len(line) == 0 { 84 | continue 85 | } 86 | o.Push([]rune(line)) 87 | o.Compact() 88 | } 89 | if total > o.cfg.HistoryLimit { 90 | o.rewriteLocked() 91 | } 92 | o.historyVer++ 93 | o.Push(nil) 94 | return 95 | } 96 | 97 | func (o *opHistory) Compact() { 98 | for o.history.Len() > o.cfg.HistoryLimit && o.history.Len() > 0 { 99 | o.history.Remove(o.history.Front()) 100 | } 101 | } 102 | 103 | func (o *opHistory) Rewrite() { 104 | o.fdLock.Lock() 105 | defer o.fdLock.Unlock() 106 | o.rewriteLocked() 107 | } 108 | 109 | func (o *opHistory) rewriteLocked() { 110 | if o.cfg.HistoryFile == "" { 111 | return 112 | } 113 | 114 | tmpFile := o.cfg.HistoryFile + ".tmp" 115 | fd, err := os.OpenFile(tmpFile, os.O_CREATE|os.O_WRONLY|os.O_TRUNC|os.O_APPEND, 0666) 116 | if err != nil { 117 | return 118 | } 119 | 120 | buf := bufio.NewWriter(fd) 121 | for elem := o.history.Front(); elem != nil; elem = elem.Next() { 122 | buf.WriteString(string(elem.Value.(*hisItem).Source) + "\n") 123 | } 124 | buf.Flush() 125 | 126 | // replace history file 127 | if err = os.Rename(tmpFile, o.cfg.HistoryFile); err != nil { 128 | fd.Close() 129 | return 130 | } 131 | 132 | if o.fd != nil { 133 | o.fd.Close() 134 | } 135 | // fd is write only, just satisfy what we need. 136 | o.fd = fd 137 | } 138 | 139 | func (o *opHistory) Close() { 140 | o.fdLock.Lock() 141 | defer o.fdLock.Unlock() 142 | if o.fd != nil { 143 | o.fd.Close() 144 | } 145 | } 146 | 147 | func (o *opHistory) FindBck(isNewSearch bool, rs []rune, start int) (int, *list.Element) { 148 | for elem := o.current; elem != nil; elem = elem.Prev() { 149 | item := o.showItem(elem.Value) 150 | if isNewSearch { 151 | start += len(rs) 152 | } 153 | if elem == o.current { 154 | if len(item) >= start { 155 | item = item[:start] 156 | } 157 | } 158 | idx := runes.IndexAllBckEx(item, rs, o.cfg.HistorySearchFold) 159 | if idx < 0 { 160 | continue 161 | } 162 | return idx, elem 163 | } 164 | return -1, nil 165 | } 166 | 167 | func (o *opHistory) FindFwd(isNewSearch bool, rs []rune, start int) (int, *list.Element) { 168 | for elem := o.current; elem != nil; elem = elem.Next() { 169 | item := o.showItem(elem.Value) 170 | if isNewSearch { 171 | start -= len(rs) 172 | if start < 0 { 173 | start = 0 174 | } 175 | } 176 | if elem == o.current { 177 | if len(item)-1 >= start { 178 | item = item[start:] 179 | } else { 180 | continue 181 | } 182 | } 183 | idx := runes.IndexAllEx(item, rs, o.cfg.HistorySearchFold) 184 | if idx < 0 { 185 | continue 186 | } 187 | if elem == o.current { 188 | idx += start 189 | } 190 | return idx, elem 191 | } 192 | return -1, nil 193 | } 194 | 195 | func (o *opHistory) showItem(obj interface{}) []rune { 196 | item := obj.(*hisItem) 197 | if item.Version == o.historyVer { 198 | return item.Tmp 199 | } 200 | return item.Source 201 | } 202 | 203 | func (o *opHistory) Prev() []rune { 204 | if o.current == nil { 205 | return nil 206 | } 207 | current := o.current.Prev() 208 | if current == nil { 209 | return nil 210 | } 211 | o.current = current 212 | return runes.Copy(o.showItem(current.Value)) 213 | } 214 | 215 | func (o *opHistory) Next() ([]rune, bool) { 216 | if o.current == nil { 217 | return nil, false 218 | } 219 | current := o.current.Next() 220 | if current == nil { 221 | return nil, false 222 | } 223 | 224 | o.current = current 225 | return runes.Copy(o.showItem(current.Value)), true 226 | } 227 | 228 | // Disable the current history 229 | func (o *opHistory) Disable() { 230 | o.enable = false 231 | } 232 | 233 | // Enable the current history 234 | func (o *opHistory) Enable() { 235 | o.enable = true 236 | } 237 | 238 | func (o *opHistory) debug() { 239 | Debug("-------") 240 | for item := o.history.Front(); item != nil; item = item.Next() { 241 | Debug(fmt.Sprintf("%+v", item.Value)) 242 | } 243 | } 244 | 245 | // save history 246 | func (o *opHistory) New(current []rune) (err error) { 247 | 248 | // history deactivated 249 | if !o.enable { 250 | return nil 251 | } 252 | 253 | current = runes.Copy(current) 254 | 255 | // if just use last command without modify 256 | // just clean lastest history 257 | if back := o.history.Back(); back != nil { 258 | prev := back.Prev() 259 | if prev != nil { 260 | if runes.Equal(current, prev.Value.(*hisItem).Source) { 261 | o.current = o.history.Back() 262 | o.current.Value.(*hisItem).Clean() 263 | o.historyVer++ 264 | return nil 265 | } 266 | } 267 | } 268 | 269 | if len(current) == 0 { 270 | o.current = o.history.Back() 271 | if o.current != nil { 272 | o.current.Value.(*hisItem).Clean() 273 | o.historyVer++ 274 | return nil 275 | } 276 | } 277 | 278 | if o.current != o.history.Back() { 279 | // move history item to current command 280 | currentItem := o.current.Value.(*hisItem) 281 | // set current to last item 282 | o.current = o.history.Back() 283 | 284 | current = runes.Copy(currentItem.Tmp) 285 | } 286 | 287 | // err only can be a IO error, just report 288 | err = o.Update(current, true) 289 | 290 | // push a new one to commit current command 291 | o.historyVer++ 292 | o.Push(nil) 293 | return 294 | } 295 | 296 | func (o *opHistory) Revert() { 297 | o.historyVer++ 298 | o.current = o.history.Back() 299 | } 300 | 301 | func (o *opHistory) Update(s []rune, commit bool) (err error) { 302 | o.fdLock.Lock() 303 | defer o.fdLock.Unlock() 304 | s = runes.Copy(s) 305 | if o.current == nil { 306 | o.Push(s) 307 | o.Compact() 308 | return 309 | } 310 | r := o.current.Value.(*hisItem) 311 | r.Version = o.historyVer 312 | if commit { 313 | r.Source = s 314 | if o.fd != nil { 315 | // just report the error 316 | _, err = o.fd.Write([]byte(string(r.Source) + "\n")) 317 | } 318 | } else { 319 | r.Tmp = append(r.Tmp[:0], s...) 320 | } 321 | o.current.Value = r 322 | o.Compact() 323 | return 324 | } 325 | 326 | func (o *opHistory) Push(s []rune) { 327 | s = runes.Copy(s) 328 | elem := o.history.PushBack(&hisItem{Source: s}) 329 | o.current = elem 330 | } 331 | -------------------------------------------------------------------------------- /operation.go: -------------------------------------------------------------------------------- 1 | package readline 2 | 3 | import ( 4 | "errors" 5 | "io" 6 | "sync" 7 | ) 8 | 9 | var ( 10 | ErrInterrupt = errors.New("Interrupt") 11 | ) 12 | 13 | type InterruptError struct { 14 | Line []rune 15 | } 16 | 17 | func (*InterruptError) Error() string { 18 | return "Interrupted" 19 | } 20 | 21 | type Operation struct { 22 | m sync.Mutex 23 | cfg *Config 24 | t *Terminal 25 | buf *RuneBuffer 26 | outchan chan []rune 27 | errchan chan error 28 | w io.Writer 29 | 30 | history *opHistory 31 | *opSearch 32 | *opCompleter 33 | *opPassword 34 | *opVim 35 | } 36 | 37 | func (o *Operation) SetBuffer(what string) { 38 | o.buf.Set([]rune(what)) 39 | } 40 | 41 | type wrapWriter struct { 42 | r *Operation 43 | t *Terminal 44 | target io.Writer 45 | } 46 | 47 | func (w *wrapWriter) Write(b []byte) (int, error) { 48 | if !w.t.IsReading() { 49 | return w.target.Write(b) 50 | } 51 | 52 | var ( 53 | n int 54 | err error 55 | ) 56 | w.r.buf.Refresh(func() { 57 | n, err = w.target.Write(b) 58 | }) 59 | 60 | if w.r.IsSearchMode() { 61 | w.r.SearchRefresh(-1) 62 | } 63 | if w.r.IsInCompleteMode() { 64 | w.r.CompleteRefresh() 65 | } 66 | return n, err 67 | } 68 | 69 | func NewOperation(t *Terminal, cfg *Config) *Operation { 70 | width := cfg.FuncGetWidth() 71 | op := &Operation{ 72 | t: t, 73 | buf: NewRuneBuffer(t, cfg.Prompt, cfg, width), 74 | outchan: make(chan []rune), 75 | errchan: make(chan error, 1), 76 | } 77 | op.w = op.buf.w 78 | op.SetConfig(cfg) 79 | op.opVim = newVimMode(op) 80 | op.opCompleter = newOpCompleter(op.buf.w, op, width) 81 | op.opPassword = newOpPassword(op) 82 | op.cfg.FuncOnWidthChanged(func() { 83 | newWidth := cfg.FuncGetWidth() 84 | op.opCompleter.OnWidthChange(newWidth) 85 | op.opSearch.OnWidthChange(newWidth) 86 | op.buf.OnWidthChange(newWidth) 87 | }) 88 | go op.ioloop() 89 | return op 90 | } 91 | 92 | func (o *Operation) SetPrompt(s string) { 93 | o.buf.SetPrompt(s) 94 | } 95 | 96 | func (o *Operation) SetMaskRune(r rune) { 97 | o.buf.SetMask(r) 98 | } 99 | 100 | func (o *Operation) GetConfig() *Config { 101 | o.m.Lock() 102 | cfg := *o.cfg 103 | o.m.Unlock() 104 | return &cfg 105 | } 106 | 107 | func (o *Operation) ioloop() { 108 | for { 109 | keepInSearchMode := false 110 | keepInCompleteMode := false 111 | r := o.t.ReadRune() 112 | 113 | if o.GetConfig().FuncFilterInputRune != nil { 114 | var process bool 115 | r, process = o.GetConfig().FuncFilterInputRune(r) 116 | if !process { 117 | o.t.KickRead() 118 | o.buf.Refresh(nil) // to refresh the line 119 | continue // ignore this rune 120 | } 121 | } 122 | 123 | if r == 0 { // io.EOF 124 | if o.buf.Len() == 0 { 125 | o.buf.Clean() 126 | select { 127 | case o.errchan <- io.EOF: 128 | } 129 | break 130 | } else { 131 | // if stdin got io.EOF and there is something left in buffer, 132 | // let's flush them by sending CharEnter. 133 | // And we will got io.EOF int next loop. 134 | r = CharEnter 135 | } 136 | } 137 | isUpdateHistory := true 138 | 139 | if o.IsInCompleteSelectMode() { 140 | keepInCompleteMode = o.HandleCompleteSelect(r) 141 | if keepInCompleteMode { 142 | continue 143 | } 144 | 145 | o.buf.Refresh(nil) 146 | switch r { 147 | case CharEnter, CharCtrlJ: 148 | o.history.Update(o.buf.Runes(), false) 149 | fallthrough 150 | case CharInterrupt: 151 | o.t.KickRead() 152 | fallthrough 153 | case CharBell: 154 | continue 155 | } 156 | } 157 | 158 | if o.IsEnableVimMode() { 159 | r = o.HandleVim(r, o.t.ReadRune) 160 | if r == 0 { 161 | continue 162 | } 163 | } 164 | 165 | switch r { 166 | case CharBell: 167 | if o.IsSearchMode() { 168 | o.ExitSearchMode(true) 169 | o.buf.Refresh(nil) 170 | } 171 | if o.IsInCompleteMode() { 172 | o.ExitCompleteMode(true) 173 | o.buf.Refresh(nil) 174 | } 175 | case CharTab: 176 | if o.GetConfig().AutoComplete == nil { 177 | o.t.Bell() 178 | break 179 | } 180 | if o.OnComplete() { 181 | keepInCompleteMode = true 182 | } else { 183 | o.t.Bell() 184 | break 185 | } 186 | 187 | case CharBckSearch: 188 | if !o.SearchMode(S_DIR_BCK) { 189 | o.t.Bell() 190 | break 191 | } 192 | keepInSearchMode = true 193 | case CharCtrlU: 194 | o.buf.KillFront() 195 | case CharFwdSearch: 196 | if !o.SearchMode(S_DIR_FWD) { 197 | o.t.Bell() 198 | break 199 | } 200 | keepInSearchMode = true 201 | case CharKill: 202 | o.buf.Kill() 203 | keepInCompleteMode = true 204 | case MetaForward: 205 | o.buf.MoveToNextWord() 206 | case CharTranspose: 207 | o.buf.Transpose() 208 | case MetaBackward: 209 | o.buf.MoveToPrevWord() 210 | case MetaDelete: 211 | o.buf.DeleteWord() 212 | case CharLineStart: 213 | o.buf.MoveToLineStart() 214 | case CharLineEnd: 215 | o.buf.MoveToLineEnd() 216 | case CharBackspace, CharCtrlH: 217 | if o.IsSearchMode() { 218 | o.SearchBackspace() 219 | keepInSearchMode = true 220 | break 221 | } 222 | 223 | if o.buf.Len() == 0 { 224 | o.t.Bell() 225 | break 226 | } 227 | o.buf.Backspace() 228 | if o.IsInCompleteMode() { 229 | o.OnComplete() 230 | } 231 | case CharCtrlZ: 232 | o.buf.Clean() 233 | o.t.SleepToResume() 234 | o.Refresh() 235 | case CharCtrlL: 236 | ClearScreen(o.w) 237 | o.Refresh() 238 | case MetaBackspace, CharCtrlW: 239 | o.buf.BackEscapeWord() 240 | case CharCtrlY: 241 | o.buf.Yank() 242 | case CharEnter, CharCtrlJ: 243 | if o.IsSearchMode() { 244 | o.ExitSearchMode(false) 245 | } 246 | o.buf.MoveToLineEnd() 247 | var data []rune 248 | if !o.GetConfig().UniqueEditLine { 249 | o.buf.WriteRune('\n') 250 | data = o.buf.Reset() 251 | data = data[:len(data)-1] // trim \n 252 | } else { 253 | o.buf.Clean() 254 | data = o.buf.Reset() 255 | } 256 | o.outchan <- data 257 | if !o.GetConfig().DisableAutoSaveHistory { 258 | // ignore IO error 259 | _ = o.history.New(data) 260 | } else { 261 | isUpdateHistory = false 262 | } 263 | case CharBackward: 264 | o.buf.MoveBackward() 265 | case CharForward: 266 | o.buf.MoveForward() 267 | case CharPrev: 268 | buf := o.history.Prev() 269 | if buf != nil { 270 | o.buf.Set(buf) 271 | } else { 272 | o.t.Bell() 273 | } 274 | case CharNext: 275 | buf, ok := o.history.Next() 276 | if ok { 277 | o.buf.Set(buf) 278 | } else { 279 | o.t.Bell() 280 | } 281 | case CharDelete: 282 | if o.buf.Len() > 0 || !o.IsNormalMode() { 283 | o.t.KickRead() 284 | if !o.buf.Delete() { 285 | o.t.Bell() 286 | } 287 | break 288 | } 289 | 290 | // treat as EOF 291 | if !o.GetConfig().UniqueEditLine { 292 | o.buf.WriteString(o.GetConfig().EOFPrompt + "\n") 293 | } 294 | o.buf.Reset() 295 | isUpdateHistory = false 296 | o.history.Revert() 297 | o.errchan <- io.EOF 298 | if o.GetConfig().UniqueEditLine { 299 | o.buf.Clean() 300 | } 301 | case CharInterrupt: 302 | if o.IsSearchMode() { 303 | o.t.KickRead() 304 | o.ExitSearchMode(true) 305 | break 306 | } 307 | if o.IsInCompleteMode() { 308 | o.t.KickRead() 309 | o.ExitCompleteMode(true) 310 | o.buf.Refresh(nil) 311 | break 312 | } 313 | o.buf.MoveToLineEnd() 314 | o.buf.Refresh(nil) 315 | hint := o.GetConfig().InterruptPrompt + "\n" 316 | if !o.GetConfig().UniqueEditLine { 317 | o.buf.WriteString(hint) 318 | } 319 | remain := o.buf.Reset() 320 | if !o.GetConfig().UniqueEditLine { 321 | remain = remain[:len(remain)-len([]rune(hint))] 322 | } 323 | isUpdateHistory = false 324 | o.history.Revert() 325 | o.errchan <- &InterruptError{remain} 326 | default: 327 | if o.IsSearchMode() { 328 | o.SearchChar(r) 329 | keepInSearchMode = true 330 | break 331 | } 332 | o.buf.WriteRune(r) 333 | if o.IsInCompleteMode() { 334 | o.OnComplete() 335 | keepInCompleteMode = true 336 | } 337 | } 338 | 339 | listener := o.GetConfig().Listener 340 | if listener != nil { 341 | newLine, newPos, ok := listener.OnChange(o.buf.Runes(), o.buf.Pos(), r) 342 | if ok { 343 | o.buf.SetWithIdx(newPos, newLine) 344 | } 345 | } 346 | 347 | o.m.Lock() 348 | if !keepInSearchMode && o.IsSearchMode() { 349 | o.ExitSearchMode(false) 350 | o.buf.Refresh(nil) 351 | } else if o.IsInCompleteMode() { 352 | if !keepInCompleteMode { 353 | o.ExitCompleteMode(false) 354 | o.Refresh() 355 | } else { 356 | o.buf.Refresh(nil) 357 | o.CompleteRefresh() 358 | } 359 | } 360 | if isUpdateHistory && !o.IsSearchMode() { 361 | // it will cause null history 362 | o.history.Update(o.buf.Runes(), false) 363 | } 364 | o.m.Unlock() 365 | } 366 | } 367 | 368 | func (o *Operation) Stderr() io.Writer { 369 | return &wrapWriter{target: o.GetConfig().Stderr, r: o, t: o.t} 370 | } 371 | 372 | func (o *Operation) Stdout() io.Writer { 373 | return &wrapWriter{target: o.GetConfig().Stdout, r: o, t: o.t} 374 | } 375 | 376 | func (o *Operation) String() (string, error) { 377 | r, err := o.Runes() 378 | return string(r), err 379 | } 380 | 381 | func (o *Operation) Runes() ([]rune, error) { 382 | o.t.EnterRawMode() 383 | defer o.t.ExitRawMode() 384 | 385 | listener := o.GetConfig().Listener 386 | if listener != nil { 387 | listener.OnChange(nil, 0, 0) 388 | } 389 | 390 | o.buf.Refresh(nil) // print prompt 391 | o.t.KickRead() 392 | select { 393 | case r := <-o.outchan: 394 | return r, nil 395 | case err := <-o.errchan: 396 | if e, ok := err.(*InterruptError); ok { 397 | return e.Line, ErrInterrupt 398 | } 399 | return nil, err 400 | } 401 | } 402 | 403 | func (o *Operation) PasswordEx(prompt string, l Listener) ([]byte, error) { 404 | cfg := o.GenPasswordConfig() 405 | cfg.Prompt = prompt 406 | cfg.Listener = l 407 | return o.PasswordWithConfig(cfg) 408 | } 409 | 410 | func (o *Operation) GenPasswordConfig() *Config { 411 | return o.opPassword.PasswordConfig() 412 | } 413 | 414 | func (o *Operation) PasswordWithConfig(cfg *Config) ([]byte, error) { 415 | if err := o.opPassword.EnterPasswordMode(cfg); err != nil { 416 | return nil, err 417 | } 418 | defer o.opPassword.ExitPasswordMode() 419 | return o.Slice() 420 | } 421 | 422 | func (o *Operation) Password(prompt string) ([]byte, error) { 423 | return o.PasswordEx(prompt, nil) 424 | } 425 | 426 | func (o *Operation) SetTitle(t string) { 427 | o.w.Write([]byte("\033[2;" + t + "\007")) 428 | } 429 | 430 | func (o *Operation) Slice() ([]byte, error) { 431 | r, err := o.Runes() 432 | if err != nil { 433 | return nil, err 434 | } 435 | return []byte(string(r)), nil 436 | } 437 | 438 | func (o *Operation) Close() { 439 | select { 440 | case o.errchan <- io.EOF: 441 | default: 442 | } 443 | o.history.Close() 444 | } 445 | 446 | func (o *Operation) SetHistoryPath(path string) { 447 | if o.history != nil { 448 | o.history.Close() 449 | } 450 | o.cfg.HistoryFile = path 451 | o.history = newOpHistory(o.cfg) 452 | } 453 | 454 | func (o *Operation) IsNormalMode() bool { 455 | return !o.IsInCompleteMode() && !o.IsSearchMode() 456 | } 457 | 458 | func (op *Operation) SetConfig(cfg *Config) (*Config, error) { 459 | op.m.Lock() 460 | defer op.m.Unlock() 461 | if op.cfg == cfg { 462 | return op.cfg, nil 463 | } 464 | if err := cfg.Init(); err != nil { 465 | return op.cfg, err 466 | } 467 | old := op.cfg 468 | op.cfg = cfg 469 | op.SetPrompt(cfg.Prompt) 470 | op.SetMaskRune(cfg.MaskRune) 471 | op.buf.SetConfig(cfg) 472 | width := op.cfg.FuncGetWidth() 473 | 474 | if cfg.opHistory == nil { 475 | op.SetHistoryPath(cfg.HistoryFile) 476 | cfg.opHistory = op.history 477 | cfg.opSearch = newOpSearch(op.buf.w, op.buf, op.history, cfg, width) 478 | } 479 | op.history = cfg.opHistory 480 | 481 | // SetHistoryPath will close opHistory which already exists 482 | // so if we use it next time, we need to reopen it by `InitHistory()` 483 | op.history.Init() 484 | 485 | if op.cfg.AutoComplete != nil { 486 | op.opCompleter = newOpCompleter(op.buf.w, op, width) 487 | } 488 | 489 | op.opSearch = cfg.opSearch 490 | return old, nil 491 | } 492 | 493 | func (o *Operation) ResetHistory() { 494 | o.history.Reset() 495 | } 496 | 497 | // if err is not nil, it just mean it fail to write to file 498 | // other things goes fine. 499 | func (o *Operation) SaveHistory(content string) error { 500 | return o.history.New([]rune(content)) 501 | } 502 | 503 | func (o *Operation) Refresh() { 504 | if o.t.IsReading() { 505 | o.buf.Refresh(nil) 506 | } 507 | } 508 | 509 | func (o *Operation) Clean() { 510 | o.buf.Clean() 511 | } 512 | 513 | func FuncListener(f func(line []rune, pos int, key rune) (newLine []rune, newPos int, ok bool)) Listener { 514 | return &DumpListener{f: f} 515 | } 516 | 517 | type DumpListener struct { 518 | f func(line []rune, pos int, key rune) (newLine []rune, newPos int, ok bool) 519 | } 520 | 521 | func (d *DumpListener) OnChange(line []rune, pos int, key rune) (newLine []rune, newPos int, ok bool) { 522 | return d.f(line, pos, key) 523 | } 524 | 525 | type Listener interface { 526 | OnChange(line []rune, pos int, key rune) (newLine []rune, newPos int, ok bool) 527 | } 528 | 529 | type Painter interface { 530 | Paint(line []rune, pos int) []rune 531 | } 532 | 533 | type defaultPainter struct{} 534 | 535 | func (p *defaultPainter) Paint(line []rune, _ int) []rune { 536 | return line 537 | } 538 | -------------------------------------------------------------------------------- /password.go: -------------------------------------------------------------------------------- 1 | package readline 2 | 3 | type opPassword struct { 4 | o *Operation 5 | backupCfg *Config 6 | } 7 | 8 | func newOpPassword(o *Operation) *opPassword { 9 | return &opPassword{o: o} 10 | } 11 | 12 | func (o *opPassword) ExitPasswordMode() { 13 | o.o.SetConfig(o.backupCfg) 14 | o.backupCfg = nil 15 | } 16 | 17 | func (o *opPassword) EnterPasswordMode(cfg *Config) (err error) { 18 | o.backupCfg, err = o.o.SetConfig(cfg) 19 | return 20 | } 21 | 22 | func (o *opPassword) PasswordConfig() *Config { 23 | return &Config{ 24 | EnableMask: true, 25 | InterruptPrompt: "\n", 26 | EOFPrompt: "\n", 27 | HistoryLimit: -1, 28 | Painter: &defaultPainter{}, 29 | 30 | Stdout: o.o.cfg.Stdout, 31 | Stderr: o.o.cfg.Stderr, 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /rawreader_windows.go: -------------------------------------------------------------------------------- 1 | // +build windows 2 | 3 | package readline 4 | 5 | import "unsafe" 6 | 7 | const ( 8 | VK_CANCEL = 0x03 9 | VK_BACK = 0x08 10 | VK_TAB = 0x09 11 | VK_RETURN = 0x0D 12 | VK_SHIFT = 0x10 13 | VK_CONTROL = 0x11 14 | VK_MENU = 0x12 15 | VK_ESCAPE = 0x1B 16 | VK_LEFT = 0x25 17 | VK_UP = 0x26 18 | VK_RIGHT = 0x27 19 | VK_DOWN = 0x28 20 | VK_DELETE = 0x2E 21 | VK_LSHIFT = 0xA0 22 | VK_RSHIFT = 0xA1 23 | VK_LCONTROL = 0xA2 24 | VK_RCONTROL = 0xA3 25 | ) 26 | 27 | // RawReader translate input record to ANSI escape sequence. 28 | // To provides same behavior as unix terminal. 29 | type RawReader struct { 30 | ctrlKey bool 31 | altKey bool 32 | } 33 | 34 | func NewRawReader() *RawReader { 35 | r := new(RawReader) 36 | return r 37 | } 38 | 39 | // only process one action in one read 40 | func (r *RawReader) Read(buf []byte) (int, error) { 41 | ir := new(_INPUT_RECORD) 42 | var read int 43 | var err error 44 | next: 45 | err = kernel.ReadConsoleInputW(stdin, 46 | uintptr(unsafe.Pointer(ir)), 47 | 1, 48 | uintptr(unsafe.Pointer(&read)), 49 | ) 50 | if err != nil { 51 | return 0, err 52 | } 53 | if ir.EventType != EVENT_KEY { 54 | goto next 55 | } 56 | ker := (*_KEY_EVENT_RECORD)(unsafe.Pointer(&ir.Event[0])) 57 | if ker.bKeyDown == 0 { // keyup 58 | if r.ctrlKey || r.altKey { 59 | switch ker.wVirtualKeyCode { 60 | case VK_RCONTROL, VK_LCONTROL: 61 | r.ctrlKey = false 62 | case VK_MENU: //alt 63 | r.altKey = false 64 | } 65 | } 66 | goto next 67 | } 68 | 69 | if ker.unicodeChar == 0 { 70 | var target rune 71 | switch ker.wVirtualKeyCode { 72 | case VK_RCONTROL, VK_LCONTROL: 73 | r.ctrlKey = true 74 | case VK_MENU: //alt 75 | r.altKey = true 76 | case VK_LEFT: 77 | target = CharBackward 78 | case VK_RIGHT: 79 | target = CharForward 80 | case VK_UP: 81 | target = CharPrev 82 | case VK_DOWN: 83 | target = CharNext 84 | } 85 | if target != 0 { 86 | return r.write(buf, target) 87 | } 88 | goto next 89 | } 90 | char := rune(ker.unicodeChar) 91 | if r.ctrlKey { 92 | switch char { 93 | case 'A': 94 | char = CharLineStart 95 | case 'E': 96 | char = CharLineEnd 97 | case 'R': 98 | char = CharBckSearch 99 | case 'S': 100 | char = CharFwdSearch 101 | } 102 | } else if r.altKey { 103 | switch char { 104 | case VK_BACK: 105 | char = CharBackspace 106 | } 107 | return r.writeEsc(buf, char) 108 | } 109 | return r.write(buf, char) 110 | } 111 | 112 | func (r *RawReader) writeEsc(b []byte, char rune) (int, error) { 113 | b[0] = '\033' 114 | n := copy(b[1:], []byte(string(char))) 115 | return n + 1, nil 116 | } 117 | 118 | func (r *RawReader) write(b []byte, char rune) (int, error) { 119 | n := copy(b, []byte(string(char))) 120 | return n, nil 121 | } 122 | 123 | func (r *RawReader) Close() error { 124 | return nil 125 | } 126 | -------------------------------------------------------------------------------- /readline.go: -------------------------------------------------------------------------------- 1 | // Readline is a pure go implementation for GNU-Readline kind library. 2 | // 3 | // example: 4 | // rl, err := readline.New("> ") 5 | // if err != nil { 6 | // panic(err) 7 | // } 8 | // defer rl.Close() 9 | // 10 | // for { 11 | // line, err := rl.Readline() 12 | // if err != nil { // io.EOF 13 | // break 14 | // } 15 | // println(line) 16 | // } 17 | // 18 | package readline 19 | 20 | import ( 21 | "io" 22 | ) 23 | 24 | type Instance struct { 25 | Config *Config 26 | Terminal *Terminal 27 | Operation *Operation 28 | } 29 | 30 | type Config struct { 31 | // prompt supports ANSI escape sequence, so we can color some characters even in windows 32 | Prompt string 33 | 34 | // readline will persist historys to file where HistoryFile specified 35 | HistoryFile string 36 | // specify the max length of historys, it's 500 by default, set it to -1 to disable history 37 | HistoryLimit int 38 | DisableAutoSaveHistory bool 39 | // enable case-insensitive history searching 40 | HistorySearchFold bool 41 | 42 | // AutoCompleter will called once user press TAB 43 | AutoComplete AutoCompleter 44 | 45 | // Any key press will pass to Listener 46 | // NOTE: Listener will be triggered by (nil, 0, 0) immediately 47 | Listener Listener 48 | 49 | Painter Painter 50 | 51 | // If VimMode is true, readline will in vim.insert mode by default 52 | VimMode bool 53 | 54 | InterruptPrompt string 55 | EOFPrompt string 56 | 57 | FuncGetWidth func() int 58 | 59 | Stdin io.ReadCloser 60 | StdinWriter io.Writer 61 | Stdout io.Writer 62 | Stderr io.Writer 63 | 64 | EnableMask bool 65 | MaskRune rune 66 | 67 | // erase the editing line after user submited it 68 | // it use in IM usually. 69 | UniqueEditLine bool 70 | 71 | // filter input runes (may be used to disable CtrlZ or for translating some keys to different actions) 72 | // -> output = new (translated) rune and true/false if continue with processing this one 73 | FuncFilterInputRune func(rune) (rune, bool) 74 | 75 | // force use interactive even stdout is not a tty 76 | FuncIsTerminal func() bool 77 | FuncMakeRaw func() error 78 | FuncExitRaw func() error 79 | FuncOnWidthChanged func(func()) 80 | ForceUseInteractive bool 81 | 82 | // private fields 83 | inited bool 84 | opHistory *opHistory 85 | opSearch *opSearch 86 | } 87 | 88 | func (c *Config) useInteractive() bool { 89 | if c.ForceUseInteractive { 90 | return true 91 | } 92 | return c.FuncIsTerminal() 93 | } 94 | 95 | func (c *Config) Init() error { 96 | if c.inited { 97 | return nil 98 | } 99 | c.inited = true 100 | if c.Stdin == nil { 101 | c.Stdin = NewCancelableStdin(Stdin) 102 | } 103 | 104 | c.Stdin, c.StdinWriter = NewFillableStdin(c.Stdin) 105 | 106 | if c.Stdout == nil { 107 | c.Stdout = Stdout 108 | } 109 | if c.Stderr == nil { 110 | c.Stderr = Stderr 111 | } 112 | if c.HistoryLimit == 0 { 113 | c.HistoryLimit = 500 114 | } 115 | 116 | if c.InterruptPrompt == "" { 117 | c.InterruptPrompt = "^C" 118 | } else if c.InterruptPrompt == "\n" { 119 | c.InterruptPrompt = "" 120 | } 121 | if c.EOFPrompt == "" { 122 | c.EOFPrompt = "^D" 123 | } else if c.EOFPrompt == "\n" { 124 | c.EOFPrompt = "" 125 | } 126 | 127 | if c.AutoComplete == nil { 128 | c.AutoComplete = &TabCompleter{} 129 | } 130 | if c.FuncGetWidth == nil { 131 | c.FuncGetWidth = GetScreenWidth 132 | } 133 | if c.FuncIsTerminal == nil { 134 | c.FuncIsTerminal = DefaultIsTerminal 135 | } 136 | rm := new(RawMode) 137 | if c.FuncMakeRaw == nil { 138 | c.FuncMakeRaw = rm.Enter 139 | } 140 | if c.FuncExitRaw == nil { 141 | c.FuncExitRaw = rm.Exit 142 | } 143 | if c.FuncOnWidthChanged == nil { 144 | c.FuncOnWidthChanged = DefaultOnWidthChanged 145 | } 146 | 147 | return nil 148 | } 149 | 150 | func (c Config) Clone() *Config { 151 | c.opHistory = nil 152 | c.opSearch = nil 153 | return &c 154 | } 155 | 156 | func (c *Config) SetListener(f func(line []rune, pos int, key rune) (newLine []rune, newPos int, ok bool)) { 157 | c.Listener = FuncListener(f) 158 | } 159 | 160 | func (c *Config) SetPainter(p Painter) { 161 | c.Painter = p 162 | } 163 | 164 | func NewEx(cfg *Config) (*Instance, error) { 165 | t, err := NewTerminal(cfg) 166 | if err != nil { 167 | return nil, err 168 | } 169 | rl := t.Readline() 170 | if cfg.Painter == nil { 171 | cfg.Painter = &defaultPainter{} 172 | } 173 | return &Instance{ 174 | Config: cfg, 175 | Terminal: t, 176 | Operation: rl, 177 | }, nil 178 | } 179 | 180 | func New(prompt string) (*Instance, error) { 181 | return NewEx(&Config{Prompt: prompt}) 182 | } 183 | 184 | func (i *Instance) ResetHistory() { 185 | i.Operation.ResetHistory() 186 | } 187 | 188 | func (i *Instance) SetPrompt(s string) { 189 | i.Operation.SetPrompt(s) 190 | } 191 | 192 | func (i *Instance) SetMaskRune(r rune) { 193 | i.Operation.SetMaskRune(r) 194 | } 195 | 196 | // change history persistence in runtime 197 | func (i *Instance) SetHistoryPath(p string) { 198 | i.Operation.SetHistoryPath(p) 199 | } 200 | 201 | // readline will refresh automatic when write through Stdout() 202 | func (i *Instance) Stdout() io.Writer { 203 | return i.Operation.Stdout() 204 | } 205 | 206 | // readline will refresh automatic when write through Stdout() 207 | func (i *Instance) Stderr() io.Writer { 208 | return i.Operation.Stderr() 209 | } 210 | 211 | // switch VimMode in runtime 212 | func (i *Instance) SetVimMode(on bool) { 213 | i.Operation.SetVimMode(on) 214 | } 215 | 216 | func (i *Instance) IsVimMode() bool { 217 | return i.Operation.IsEnableVimMode() 218 | } 219 | 220 | func (i *Instance) GenPasswordConfig() *Config { 221 | return i.Operation.GenPasswordConfig() 222 | } 223 | 224 | // we can generate a config by `i.GenPasswordConfig()` 225 | func (i *Instance) ReadPasswordWithConfig(cfg *Config) ([]byte, error) { 226 | return i.Operation.PasswordWithConfig(cfg) 227 | } 228 | 229 | func (i *Instance) ReadPasswordEx(prompt string, l Listener) ([]byte, error) { 230 | return i.Operation.PasswordEx(prompt, l) 231 | } 232 | 233 | func (i *Instance) ReadPassword(prompt string) ([]byte, error) { 234 | return i.Operation.Password(prompt) 235 | } 236 | 237 | type Result struct { 238 | Line string 239 | Error error 240 | } 241 | 242 | func (l *Result) CanContinue() bool { 243 | return len(l.Line) != 0 && l.Error == ErrInterrupt 244 | } 245 | 246 | func (l *Result) CanBreak() bool { 247 | return !l.CanContinue() && l.Error != nil 248 | } 249 | 250 | func (i *Instance) Line() *Result { 251 | ret, err := i.Readline() 252 | return &Result{ret, err} 253 | } 254 | 255 | // err is one of (nil, io.EOF, readline.ErrInterrupt) 256 | func (i *Instance) Readline() (string, error) { 257 | return i.Operation.String() 258 | } 259 | 260 | func (i *Instance) ReadlineWithDefault(what string) (string, error) { 261 | i.Operation.SetBuffer(what) 262 | return i.Operation.String() 263 | } 264 | 265 | func (i *Instance) SaveHistory(content string) error { 266 | return i.Operation.SaveHistory(content) 267 | } 268 | 269 | // same as readline 270 | func (i *Instance) ReadSlice() ([]byte, error) { 271 | return i.Operation.Slice() 272 | } 273 | 274 | // we must make sure that call Close() before process exit. 275 | // if there has a pending reading operation, that reading will be interrupted. 276 | // so you can capture the signal and call Instance.Close(), it's thread-safe. 277 | func (i *Instance) Close() error { 278 | i.Config.Stdin.Close() 279 | i.Operation.Close() 280 | if err := i.Terminal.Close(); err != nil { 281 | return err 282 | } 283 | return nil 284 | } 285 | 286 | // call CaptureExitSignal when you want readline exit gracefully. 287 | func (i *Instance) CaptureExitSignal() { 288 | CaptureExitSignal(func() { 289 | i.Close() 290 | }) 291 | } 292 | 293 | func (i *Instance) Clean() { 294 | i.Operation.Clean() 295 | } 296 | 297 | func (i *Instance) Write(b []byte) (int, error) { 298 | return i.Stdout().Write(b) 299 | } 300 | 301 | // WriteStdin prefill the next Stdin fetch 302 | // Next time you call ReadLine() this value will be writen before the user input 303 | // ie : 304 | // i := readline.New() 305 | // i.WriteStdin([]byte("test")) 306 | // _, _= i.Readline() 307 | // 308 | // gives 309 | // 310 | // > test[cursor] 311 | func (i *Instance) WriteStdin(val []byte) (int, error) { 312 | return i.Terminal.WriteStdin(val) 313 | } 314 | 315 | func (i *Instance) SetConfig(cfg *Config) *Config { 316 | if i.Config == cfg { 317 | return cfg 318 | } 319 | old := i.Config 320 | i.Config = cfg 321 | i.Operation.SetConfig(cfg) 322 | i.Terminal.SetConfig(cfg) 323 | return old 324 | } 325 | 326 | func (i *Instance) Refresh() { 327 | i.Operation.Refresh() 328 | } 329 | 330 | // HistoryDisable the save of the commands into the history 331 | func (i *Instance) HistoryDisable() { 332 | i.Operation.history.Disable() 333 | } 334 | 335 | // HistoryEnable the save of the commands into the history (default on) 336 | func (i *Instance) HistoryEnable() { 337 | i.Operation.history.Enable() 338 | } 339 | -------------------------------------------------------------------------------- /readline_test.go: -------------------------------------------------------------------------------- 1 | package readline 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | ) 7 | 8 | func TestRace(t *testing.T) { 9 | rl, err := NewEx(&Config{}) 10 | if err != nil { 11 | t.Fatal(err) 12 | return 13 | } 14 | 15 | go func() { 16 | for range time.Tick(time.Millisecond) { 17 | rl.SetPrompt("hello") 18 | } 19 | }() 20 | 21 | go func() { 22 | time.Sleep(100 * time.Millisecond) 23 | rl.Close() 24 | }() 25 | 26 | rl.Readline() 27 | } 28 | -------------------------------------------------------------------------------- /remote.go: -------------------------------------------------------------------------------- 1 | package readline 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "encoding/binary" 7 | "fmt" 8 | "io" 9 | "net" 10 | "os" 11 | "sync" 12 | "sync/atomic" 13 | ) 14 | 15 | type MsgType int16 16 | 17 | const ( 18 | T_DATA = MsgType(iota) 19 | T_WIDTH 20 | T_WIDTH_REPORT 21 | T_ISTTY_REPORT 22 | T_RAW 23 | T_ERAW // exit raw 24 | T_EOF 25 | ) 26 | 27 | type RemoteSvr struct { 28 | eof int32 29 | closed int32 30 | width int32 31 | reciveChan chan struct{} 32 | writeChan chan *writeCtx 33 | conn net.Conn 34 | isTerminal bool 35 | funcWidthChan func() 36 | stopChan chan struct{} 37 | 38 | dataBufM sync.Mutex 39 | dataBuf bytes.Buffer 40 | } 41 | 42 | type writeReply struct { 43 | n int 44 | err error 45 | } 46 | 47 | type writeCtx struct { 48 | msg *Message 49 | reply chan *writeReply 50 | } 51 | 52 | func newWriteCtx(msg *Message) *writeCtx { 53 | return &writeCtx{ 54 | msg: msg, 55 | reply: make(chan *writeReply), 56 | } 57 | } 58 | 59 | func NewRemoteSvr(conn net.Conn) (*RemoteSvr, error) { 60 | rs := &RemoteSvr{ 61 | width: -1, 62 | conn: conn, 63 | writeChan: make(chan *writeCtx), 64 | reciveChan: make(chan struct{}), 65 | stopChan: make(chan struct{}), 66 | } 67 | buf := bufio.NewReader(rs.conn) 68 | 69 | if err := rs.init(buf); err != nil { 70 | return nil, err 71 | } 72 | 73 | go rs.readLoop(buf) 74 | go rs.writeLoop() 75 | return rs, nil 76 | } 77 | 78 | func (r *RemoteSvr) init(buf *bufio.Reader) error { 79 | m, err := ReadMessage(buf) 80 | if err != nil { 81 | return err 82 | } 83 | // receive isTerminal 84 | if m.Type != T_ISTTY_REPORT { 85 | return fmt.Errorf("unexpected init message") 86 | } 87 | r.GotIsTerminal(m.Data) 88 | 89 | // receive width 90 | m, err = ReadMessage(buf) 91 | if err != nil { 92 | return err 93 | } 94 | if m.Type != T_WIDTH_REPORT { 95 | return fmt.Errorf("unexpected init message") 96 | } 97 | r.GotReportWidth(m.Data) 98 | 99 | return nil 100 | } 101 | 102 | func (r *RemoteSvr) HandleConfig(cfg *Config) { 103 | cfg.Stderr = r 104 | cfg.Stdout = r 105 | cfg.Stdin = r 106 | cfg.FuncExitRaw = r.ExitRawMode 107 | cfg.FuncIsTerminal = r.IsTerminal 108 | cfg.FuncMakeRaw = r.EnterRawMode 109 | cfg.FuncExitRaw = r.ExitRawMode 110 | cfg.FuncGetWidth = r.GetWidth 111 | cfg.FuncOnWidthChanged = func(f func()) { 112 | r.funcWidthChan = f 113 | } 114 | } 115 | 116 | func (r *RemoteSvr) IsTerminal() bool { 117 | return r.isTerminal 118 | } 119 | 120 | func (r *RemoteSvr) checkEOF() error { 121 | if atomic.LoadInt32(&r.eof) == 1 { 122 | return io.EOF 123 | } 124 | return nil 125 | } 126 | 127 | func (r *RemoteSvr) Read(b []byte) (int, error) { 128 | r.dataBufM.Lock() 129 | n, err := r.dataBuf.Read(b) 130 | r.dataBufM.Unlock() 131 | if n == 0 { 132 | if err := r.checkEOF(); err != nil { 133 | return 0, err 134 | } 135 | } 136 | 137 | if n == 0 && err == io.EOF { 138 | <-r.reciveChan 139 | r.dataBufM.Lock() 140 | n, err = r.dataBuf.Read(b) 141 | r.dataBufM.Unlock() 142 | } 143 | if n == 0 { 144 | if err := r.checkEOF(); err != nil { 145 | return 0, err 146 | } 147 | } 148 | 149 | return n, err 150 | } 151 | 152 | func (r *RemoteSvr) writeMsg(m *Message) error { 153 | ctx := newWriteCtx(m) 154 | r.writeChan <- ctx 155 | reply := <-ctx.reply 156 | return reply.err 157 | } 158 | 159 | func (r *RemoteSvr) Write(b []byte) (int, error) { 160 | ctx := newWriteCtx(NewMessage(T_DATA, b)) 161 | r.writeChan <- ctx 162 | reply := <-ctx.reply 163 | return reply.n, reply.err 164 | } 165 | 166 | func (r *RemoteSvr) EnterRawMode() error { 167 | return r.writeMsg(NewMessage(T_RAW, nil)) 168 | } 169 | 170 | func (r *RemoteSvr) ExitRawMode() error { 171 | return r.writeMsg(NewMessage(T_ERAW, nil)) 172 | } 173 | 174 | func (r *RemoteSvr) writeLoop() { 175 | defer r.Close() 176 | 177 | loop: 178 | for { 179 | select { 180 | case ctx, ok := <-r.writeChan: 181 | if !ok { 182 | break 183 | } 184 | n, err := ctx.msg.WriteTo(r.conn) 185 | ctx.reply <- &writeReply{n, err} 186 | case <-r.stopChan: 187 | break loop 188 | } 189 | } 190 | } 191 | 192 | func (r *RemoteSvr) Close() error { 193 | if atomic.CompareAndSwapInt32(&r.closed, 0, 1) { 194 | close(r.stopChan) 195 | r.conn.Close() 196 | } 197 | return nil 198 | } 199 | 200 | func (r *RemoteSvr) readLoop(buf *bufio.Reader) { 201 | defer r.Close() 202 | for { 203 | m, err := ReadMessage(buf) 204 | if err != nil { 205 | break 206 | } 207 | switch m.Type { 208 | case T_EOF: 209 | atomic.StoreInt32(&r.eof, 1) 210 | select { 211 | case r.reciveChan <- struct{}{}: 212 | default: 213 | } 214 | case T_DATA: 215 | r.dataBufM.Lock() 216 | r.dataBuf.Write(m.Data) 217 | r.dataBufM.Unlock() 218 | select { 219 | case r.reciveChan <- struct{}{}: 220 | default: 221 | } 222 | case T_WIDTH_REPORT: 223 | r.GotReportWidth(m.Data) 224 | case T_ISTTY_REPORT: 225 | r.GotIsTerminal(m.Data) 226 | } 227 | } 228 | } 229 | 230 | func (r *RemoteSvr) GotIsTerminal(data []byte) { 231 | if binary.BigEndian.Uint16(data) == 0 { 232 | r.isTerminal = false 233 | } else { 234 | r.isTerminal = true 235 | } 236 | } 237 | 238 | func (r *RemoteSvr) GotReportWidth(data []byte) { 239 | atomic.StoreInt32(&r.width, int32(binary.BigEndian.Uint16(data))) 240 | if r.funcWidthChan != nil { 241 | r.funcWidthChan() 242 | } 243 | } 244 | 245 | func (r *RemoteSvr) GetWidth() int { 246 | return int(atomic.LoadInt32(&r.width)) 247 | } 248 | 249 | // ----------------------------------------------------------------------------- 250 | 251 | type Message struct { 252 | Type MsgType 253 | Data []byte 254 | } 255 | 256 | func ReadMessage(r io.Reader) (*Message, error) { 257 | m := new(Message) 258 | var length int32 259 | if err := binary.Read(r, binary.BigEndian, &length); err != nil { 260 | return nil, err 261 | } 262 | if err := binary.Read(r, binary.BigEndian, &m.Type); err != nil { 263 | return nil, err 264 | } 265 | m.Data = make([]byte, int(length)-2) 266 | if _, err := io.ReadFull(r, m.Data); err != nil { 267 | return nil, err 268 | } 269 | return m, nil 270 | } 271 | 272 | func NewMessage(t MsgType, data []byte) *Message { 273 | return &Message{t, data} 274 | } 275 | 276 | func (m *Message) WriteTo(w io.Writer) (int, error) { 277 | buf := bytes.NewBuffer(make([]byte, 0, len(m.Data)+2+4)) 278 | binary.Write(buf, binary.BigEndian, int32(len(m.Data)+2)) 279 | binary.Write(buf, binary.BigEndian, m.Type) 280 | buf.Write(m.Data) 281 | n, err := buf.WriteTo(w) 282 | return int(n), err 283 | } 284 | 285 | // ----------------------------------------------------------------------------- 286 | 287 | type RemoteCli struct { 288 | conn net.Conn 289 | raw RawMode 290 | receiveChan chan struct{} 291 | inited int32 292 | isTerminal *bool 293 | 294 | data bytes.Buffer 295 | dataM sync.Mutex 296 | } 297 | 298 | func NewRemoteCli(conn net.Conn) (*RemoteCli, error) { 299 | r := &RemoteCli{ 300 | conn: conn, 301 | receiveChan: make(chan struct{}), 302 | } 303 | return r, nil 304 | } 305 | 306 | func (r *RemoteCli) MarkIsTerminal(is bool) { 307 | r.isTerminal = &is 308 | } 309 | 310 | func (r *RemoteCli) init() error { 311 | if !atomic.CompareAndSwapInt32(&r.inited, 0, 1) { 312 | return nil 313 | } 314 | 315 | if err := r.reportIsTerminal(); err != nil { 316 | return err 317 | } 318 | 319 | if err := r.reportWidth(); err != nil { 320 | return err 321 | } 322 | 323 | // register sig for width changed 324 | DefaultOnWidthChanged(func() { 325 | r.reportWidth() 326 | }) 327 | return nil 328 | } 329 | 330 | func (r *RemoteCli) writeMsg(m *Message) error { 331 | r.dataM.Lock() 332 | _, err := m.WriteTo(r.conn) 333 | r.dataM.Unlock() 334 | return err 335 | } 336 | 337 | func (r *RemoteCli) Write(b []byte) (int, error) { 338 | m := NewMessage(T_DATA, b) 339 | r.dataM.Lock() 340 | _, err := m.WriteTo(r.conn) 341 | r.dataM.Unlock() 342 | return len(b), err 343 | } 344 | 345 | func (r *RemoteCli) reportWidth() error { 346 | screenWidth := GetScreenWidth() 347 | data := make([]byte, 2) 348 | binary.BigEndian.PutUint16(data, uint16(screenWidth)) 349 | msg := NewMessage(T_WIDTH_REPORT, data) 350 | 351 | if err := r.writeMsg(msg); err != nil { 352 | return err 353 | } 354 | return nil 355 | } 356 | 357 | func (r *RemoteCli) reportIsTerminal() error { 358 | var isTerminal bool 359 | if r.isTerminal != nil { 360 | isTerminal = *r.isTerminal 361 | } else { 362 | isTerminal = DefaultIsTerminal() 363 | } 364 | data := make([]byte, 2) 365 | if isTerminal { 366 | binary.BigEndian.PutUint16(data, 1) 367 | } else { 368 | binary.BigEndian.PutUint16(data, 0) 369 | } 370 | msg := NewMessage(T_ISTTY_REPORT, data) 371 | if err := r.writeMsg(msg); err != nil { 372 | return err 373 | } 374 | return nil 375 | } 376 | 377 | func (r *RemoteCli) readLoop() { 378 | buf := bufio.NewReader(r.conn) 379 | for { 380 | msg, err := ReadMessage(buf) 381 | if err != nil { 382 | break 383 | } 384 | switch msg.Type { 385 | case T_ERAW: 386 | r.raw.Exit() 387 | case T_RAW: 388 | r.raw.Enter() 389 | case T_DATA: 390 | os.Stdout.Write(msg.Data) 391 | } 392 | } 393 | } 394 | 395 | func (r *RemoteCli) ServeBy(source io.Reader) error { 396 | if err := r.init(); err != nil { 397 | return err 398 | } 399 | 400 | go func() { 401 | defer r.Close() 402 | for { 403 | n, _ := io.Copy(r, source) 404 | if n == 0 { 405 | break 406 | } 407 | } 408 | }() 409 | defer r.raw.Exit() 410 | r.readLoop() 411 | return nil 412 | } 413 | 414 | func (r *RemoteCli) Close() { 415 | r.writeMsg(NewMessage(T_EOF, nil)) 416 | } 417 | 418 | func (r *RemoteCli) Serve() error { 419 | return r.ServeBy(os.Stdin) 420 | } 421 | 422 | func ListenRemote(n, addr string, cfg *Config, h func(*Instance), onListen ...func(net.Listener) error) error { 423 | ln, err := net.Listen(n, addr) 424 | if err != nil { 425 | return err 426 | } 427 | if len(onListen) > 0 { 428 | if err := onListen[0](ln); err != nil { 429 | return err 430 | } 431 | } 432 | for { 433 | conn, err := ln.Accept() 434 | if err != nil { 435 | break 436 | } 437 | go func() { 438 | defer conn.Close() 439 | rl, err := HandleConn(*cfg, conn) 440 | if err != nil { 441 | return 442 | } 443 | h(rl) 444 | }() 445 | } 446 | return nil 447 | } 448 | 449 | func HandleConn(cfg Config, conn net.Conn) (*Instance, error) { 450 | r, err := NewRemoteSvr(conn) 451 | if err != nil { 452 | return nil, err 453 | } 454 | r.HandleConfig(&cfg) 455 | 456 | rl, err := NewEx(&cfg) 457 | if err != nil { 458 | return nil, err 459 | } 460 | return rl, nil 461 | } 462 | 463 | func DialRemote(n, addr string) error { 464 | conn, err := net.Dial(n, addr) 465 | if err != nil { 466 | return err 467 | } 468 | defer conn.Close() 469 | 470 | cli, err := NewRemoteCli(conn) 471 | if err != nil { 472 | return err 473 | } 474 | return cli.Serve() 475 | } 476 | -------------------------------------------------------------------------------- /runebuf.go: -------------------------------------------------------------------------------- 1 | package readline 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "io" 7 | "strconv" 8 | "strings" 9 | "sync" 10 | ) 11 | 12 | type runeBufferBck struct { 13 | buf []rune 14 | idx int 15 | } 16 | 17 | type RuneBuffer struct { 18 | buf []rune 19 | idx int 20 | prompt []rune 21 | w io.Writer 22 | 23 | hadClean bool 24 | interactive bool 25 | cfg *Config 26 | 27 | width int 28 | 29 | bck *runeBufferBck 30 | 31 | offset string 32 | 33 | lastKill []rune 34 | 35 | sync.Mutex 36 | } 37 | 38 | func (r *RuneBuffer) pushKill(text []rune) { 39 | r.lastKill = append([]rune{}, text...) 40 | } 41 | 42 | func (r *RuneBuffer) OnWidthChange(newWidth int) { 43 | r.Lock() 44 | r.width = newWidth 45 | r.Unlock() 46 | } 47 | 48 | func (r *RuneBuffer) Backup() { 49 | r.Lock() 50 | r.bck = &runeBufferBck{r.buf, r.idx} 51 | r.Unlock() 52 | } 53 | 54 | func (r *RuneBuffer) Restore() { 55 | r.Refresh(func() { 56 | if r.bck == nil { 57 | return 58 | } 59 | r.buf = r.bck.buf 60 | r.idx = r.bck.idx 61 | }) 62 | } 63 | 64 | func NewRuneBuffer(w io.Writer, prompt string, cfg *Config, width int) *RuneBuffer { 65 | rb := &RuneBuffer{ 66 | w: w, 67 | interactive: cfg.useInteractive(), 68 | cfg: cfg, 69 | width: width, 70 | } 71 | rb.SetPrompt(prompt) 72 | return rb 73 | } 74 | 75 | func (r *RuneBuffer) SetConfig(cfg *Config) { 76 | r.Lock() 77 | r.cfg = cfg 78 | r.interactive = cfg.useInteractive() 79 | r.Unlock() 80 | } 81 | 82 | func (r *RuneBuffer) SetMask(m rune) { 83 | r.Lock() 84 | r.cfg.MaskRune = m 85 | r.Unlock() 86 | } 87 | 88 | func (r *RuneBuffer) CurrentWidth(x int) int { 89 | r.Lock() 90 | defer r.Unlock() 91 | return runes.WidthAll(r.buf[:x]) 92 | } 93 | 94 | func (r *RuneBuffer) PromptLen() int { 95 | r.Lock() 96 | width := r.promptLen() 97 | r.Unlock() 98 | return width 99 | } 100 | 101 | func (r *RuneBuffer) promptLen() int { 102 | return runes.WidthAll(runes.ColorFilter(r.prompt)) 103 | } 104 | 105 | func (r *RuneBuffer) RuneSlice(i int) []rune { 106 | r.Lock() 107 | defer r.Unlock() 108 | 109 | if i > 0 { 110 | rs := make([]rune, i) 111 | copy(rs, r.buf[r.idx:r.idx+i]) 112 | return rs 113 | } 114 | rs := make([]rune, -i) 115 | copy(rs, r.buf[r.idx+i:r.idx]) 116 | return rs 117 | } 118 | 119 | func (r *RuneBuffer) Runes() []rune { 120 | r.Lock() 121 | newr := make([]rune, len(r.buf)) 122 | copy(newr, r.buf) 123 | r.Unlock() 124 | return newr 125 | } 126 | 127 | func (r *RuneBuffer) Pos() int { 128 | r.Lock() 129 | defer r.Unlock() 130 | return r.idx 131 | } 132 | 133 | func (r *RuneBuffer) Len() int { 134 | r.Lock() 135 | defer r.Unlock() 136 | return len(r.buf) 137 | } 138 | 139 | func (r *RuneBuffer) MoveToLineStart() { 140 | r.Refresh(func() { 141 | if r.idx == 0 { 142 | return 143 | } 144 | r.idx = 0 145 | }) 146 | } 147 | 148 | func (r *RuneBuffer) MoveBackward() { 149 | r.Refresh(func() { 150 | if r.idx == 0 { 151 | return 152 | } 153 | r.idx-- 154 | }) 155 | } 156 | 157 | func (r *RuneBuffer) WriteString(s string) { 158 | r.WriteRunes([]rune(s)) 159 | } 160 | 161 | func (r *RuneBuffer) WriteRune(s rune) { 162 | r.WriteRunes([]rune{s}) 163 | } 164 | 165 | func (r *RuneBuffer) WriteRunes(s []rune) { 166 | r.Refresh(func() { 167 | tail := append(s, r.buf[r.idx:]...) 168 | r.buf = append(r.buf[:r.idx], tail...) 169 | r.idx += len(s) 170 | }) 171 | } 172 | 173 | func (r *RuneBuffer) MoveForward() { 174 | r.Refresh(func() { 175 | if r.idx == len(r.buf) { 176 | return 177 | } 178 | r.idx++ 179 | }) 180 | } 181 | 182 | func (r *RuneBuffer) IsCursorInEnd() bool { 183 | r.Lock() 184 | defer r.Unlock() 185 | return r.idx == len(r.buf) 186 | } 187 | 188 | func (r *RuneBuffer) Replace(ch rune) { 189 | r.Refresh(func() { 190 | r.buf[r.idx] = ch 191 | }) 192 | } 193 | 194 | func (r *RuneBuffer) Erase() { 195 | r.Refresh(func() { 196 | r.idx = 0 197 | r.pushKill(r.buf[:]) 198 | r.buf = r.buf[:0] 199 | }) 200 | } 201 | 202 | func (r *RuneBuffer) Delete() (success bool) { 203 | r.Refresh(func() { 204 | if r.idx == len(r.buf) { 205 | return 206 | } 207 | r.pushKill(r.buf[r.idx : r.idx+1]) 208 | r.buf = append(r.buf[:r.idx], r.buf[r.idx+1:]...) 209 | success = true 210 | }) 211 | return 212 | } 213 | 214 | func (r *RuneBuffer) DeleteWord() { 215 | if r.idx == len(r.buf) { 216 | return 217 | } 218 | init := r.idx 219 | for init < len(r.buf) && IsWordBreak(r.buf[init]) { 220 | init++ 221 | } 222 | for i := init + 1; i < len(r.buf); i++ { 223 | if !IsWordBreak(r.buf[i]) && IsWordBreak(r.buf[i-1]) { 224 | r.pushKill(r.buf[r.idx : i-1]) 225 | r.Refresh(func() { 226 | r.buf = append(r.buf[:r.idx], r.buf[i-1:]...) 227 | }) 228 | return 229 | } 230 | } 231 | r.Kill() 232 | } 233 | 234 | func (r *RuneBuffer) MoveToPrevWord() (success bool) { 235 | r.Refresh(func() { 236 | if r.idx == 0 { 237 | return 238 | } 239 | 240 | for i := r.idx - 1; i > 0; i-- { 241 | if !IsWordBreak(r.buf[i]) && IsWordBreak(r.buf[i-1]) { 242 | r.idx = i 243 | success = true 244 | return 245 | } 246 | } 247 | r.idx = 0 248 | success = true 249 | }) 250 | return 251 | } 252 | 253 | func (r *RuneBuffer) KillFront() { 254 | r.Refresh(func() { 255 | if r.idx == 0 { 256 | return 257 | } 258 | 259 | length := len(r.buf) - r.idx 260 | r.pushKill(r.buf[:r.idx]) 261 | copy(r.buf[:length], r.buf[r.idx:]) 262 | r.idx = 0 263 | r.buf = r.buf[:length] 264 | }) 265 | } 266 | 267 | func (r *RuneBuffer) Kill() { 268 | r.Refresh(func() { 269 | r.pushKill(r.buf[r.idx:]) 270 | r.buf = r.buf[:r.idx] 271 | }) 272 | } 273 | 274 | func (r *RuneBuffer) Transpose() { 275 | r.Refresh(func() { 276 | if len(r.buf) == 1 { 277 | r.idx++ 278 | } 279 | 280 | if len(r.buf) < 2 { 281 | return 282 | } 283 | 284 | if r.idx == 0 { 285 | r.idx = 1 286 | } else if r.idx >= len(r.buf) { 287 | r.idx = len(r.buf) - 1 288 | } 289 | r.buf[r.idx], r.buf[r.idx-1] = r.buf[r.idx-1], r.buf[r.idx] 290 | r.idx++ 291 | }) 292 | } 293 | 294 | func (r *RuneBuffer) MoveToNextWord() { 295 | r.Refresh(func() { 296 | for i := r.idx + 1; i < len(r.buf); i++ { 297 | if !IsWordBreak(r.buf[i]) && IsWordBreak(r.buf[i-1]) { 298 | r.idx = i 299 | return 300 | } 301 | } 302 | 303 | r.idx = len(r.buf) 304 | }) 305 | } 306 | 307 | func (r *RuneBuffer) MoveToEndWord() { 308 | r.Refresh(func() { 309 | // already at the end, so do nothing 310 | if r.idx == len(r.buf) { 311 | return 312 | } 313 | // if we are at the end of a word already, go to next 314 | if !IsWordBreak(r.buf[r.idx]) && IsWordBreak(r.buf[r.idx+1]) { 315 | r.idx++ 316 | } 317 | 318 | // keep going until at the end of a word 319 | for i := r.idx + 1; i < len(r.buf); i++ { 320 | if IsWordBreak(r.buf[i]) && !IsWordBreak(r.buf[i-1]) { 321 | r.idx = i - 1 322 | return 323 | } 324 | } 325 | r.idx = len(r.buf) 326 | }) 327 | } 328 | 329 | func (r *RuneBuffer) BackEscapeWord() { 330 | r.Refresh(func() { 331 | if r.idx == 0 { 332 | return 333 | } 334 | for i := r.idx - 1; i > 0; i-- { 335 | if !IsWordBreak(r.buf[i]) && IsWordBreak(r.buf[i-1]) { 336 | r.pushKill(r.buf[i:r.idx]) 337 | r.buf = append(r.buf[:i], r.buf[r.idx:]...) 338 | r.idx = i 339 | return 340 | } 341 | } 342 | 343 | r.buf = r.buf[:0] 344 | r.idx = 0 345 | }) 346 | } 347 | 348 | func (r *RuneBuffer) Yank() { 349 | if len(r.lastKill) == 0 { 350 | return 351 | } 352 | r.Refresh(func() { 353 | buf := make([]rune, 0, len(r.buf)+len(r.lastKill)) 354 | buf = append(buf, r.buf[:r.idx]...) 355 | buf = append(buf, r.lastKill...) 356 | buf = append(buf, r.buf[r.idx:]...) 357 | r.buf = buf 358 | r.idx += len(r.lastKill) 359 | }) 360 | } 361 | 362 | func (r *RuneBuffer) Backspace() { 363 | r.Refresh(func() { 364 | if r.idx == 0 { 365 | return 366 | } 367 | 368 | r.idx-- 369 | r.buf = append(r.buf[:r.idx], r.buf[r.idx+1:]...) 370 | }) 371 | } 372 | 373 | func (r *RuneBuffer) MoveToLineEnd() { 374 | r.Refresh(func() { 375 | if r.idx == len(r.buf) { 376 | return 377 | } 378 | 379 | r.idx = len(r.buf) 380 | }) 381 | } 382 | 383 | func (r *RuneBuffer) LineCount(width int) int { 384 | if width == -1 { 385 | width = r.width 386 | } 387 | return LineCount(width, 388 | runes.WidthAll(r.buf)+r.PromptLen()) 389 | } 390 | 391 | func (r *RuneBuffer) MoveTo(ch rune, prevChar, reverse bool) (success bool) { 392 | r.Refresh(func() { 393 | if reverse { 394 | for i := r.idx - 1; i >= 0; i-- { 395 | if r.buf[i] == ch { 396 | r.idx = i 397 | if prevChar { 398 | r.idx++ 399 | } 400 | success = true 401 | return 402 | } 403 | } 404 | return 405 | } 406 | for i := r.idx + 1; i < len(r.buf); i++ { 407 | if r.buf[i] == ch { 408 | r.idx = i 409 | if prevChar { 410 | r.idx-- 411 | } 412 | success = true 413 | return 414 | } 415 | } 416 | }) 417 | return 418 | } 419 | 420 | func (r *RuneBuffer) isInLineEdge() bool { 421 | if isWindows { 422 | return false 423 | } 424 | sp := r.getSplitByLine(r.buf) 425 | return len(sp[len(sp)-1]) == 0 426 | } 427 | 428 | func (r *RuneBuffer) getSplitByLine(rs []rune) []string { 429 | return SplitByLine(r.promptLen(), r.width, rs) 430 | } 431 | 432 | func (r *RuneBuffer) IdxLine(width int) int { 433 | r.Lock() 434 | defer r.Unlock() 435 | return r.idxLine(width) 436 | } 437 | 438 | func (r *RuneBuffer) idxLine(width int) int { 439 | if width == 0 { 440 | return 0 441 | } 442 | sp := r.getSplitByLine(r.buf[:r.idx]) 443 | return len(sp) - 1 444 | } 445 | 446 | func (r *RuneBuffer) CursorLineCount() int { 447 | return r.LineCount(r.width) - r.IdxLine(r.width) 448 | } 449 | 450 | func (r *RuneBuffer) Refresh(f func()) { 451 | r.Lock() 452 | defer r.Unlock() 453 | 454 | if !r.interactive { 455 | if f != nil { 456 | f() 457 | } 458 | return 459 | } 460 | 461 | r.clean() 462 | if f != nil { 463 | f() 464 | } 465 | r.print() 466 | } 467 | 468 | func (r *RuneBuffer) SetOffset(offset string) { 469 | r.Lock() 470 | r.offset = offset 471 | r.Unlock() 472 | } 473 | 474 | func (r *RuneBuffer) print() { 475 | r.w.Write(r.output()) 476 | r.hadClean = false 477 | } 478 | 479 | func (r *RuneBuffer) output() []byte { 480 | buf := bytes.NewBuffer(nil) 481 | buf.WriteString(string(r.prompt)) 482 | if r.cfg.EnableMask && len(r.buf) > 0 { 483 | buf.Write([]byte(strings.Repeat(string(r.cfg.MaskRune), len(r.buf)-1))) 484 | if r.buf[len(r.buf)-1] == '\n' { 485 | buf.Write([]byte{'\n'}) 486 | } else { 487 | buf.Write([]byte(string(r.cfg.MaskRune))) 488 | } 489 | if len(r.buf) > r.idx { 490 | buf.Write(r.getBackspaceSequence()) 491 | } 492 | 493 | } else { 494 | for _, e := range r.cfg.Painter.Paint(r.buf, r.idx) { 495 | if e == '\t' { 496 | buf.WriteString(strings.Repeat(" ", TabWidth)) 497 | } else { 498 | buf.WriteRune(e) 499 | } 500 | } 501 | if r.isInLineEdge() { 502 | buf.Write([]byte(" \b")) 503 | } 504 | } 505 | // cursor position 506 | if len(r.buf) > r.idx { 507 | buf.Write(r.getBackspaceSequence()) 508 | } 509 | return buf.Bytes() 510 | } 511 | 512 | func (r *RuneBuffer) getBackspaceSequence() []byte { 513 | var sep = map[int]bool{} 514 | 515 | var i int 516 | for { 517 | if i >= runes.WidthAll(r.buf) { 518 | break 519 | } 520 | 521 | if i == 0 { 522 | i -= r.promptLen() 523 | } 524 | i += r.width 525 | 526 | sep[i] = true 527 | } 528 | var buf []byte 529 | for i := len(r.buf); i > r.idx; i-- { 530 | // move input to the left of one 531 | buf = append(buf, '\b') 532 | if sep[i] { 533 | // up one line, go to the start of the line and move cursor right to the end (r.width) 534 | buf = append(buf, "\033[A\r"+"\033["+strconv.Itoa(r.width)+"C"...) 535 | } 536 | } 537 | 538 | return buf 539 | 540 | } 541 | 542 | func (r *RuneBuffer) Reset() []rune { 543 | ret := runes.Copy(r.buf) 544 | r.buf = r.buf[:0] 545 | r.idx = 0 546 | return ret 547 | } 548 | 549 | func (r *RuneBuffer) calWidth(m int) int { 550 | if m > 0 { 551 | return runes.WidthAll(r.buf[r.idx : r.idx+m]) 552 | } 553 | return runes.WidthAll(r.buf[r.idx+m : r.idx]) 554 | } 555 | 556 | func (r *RuneBuffer) SetStyle(start, end int, style string) { 557 | if end < start { 558 | panic("end < start") 559 | } 560 | 561 | // goto start 562 | move := start - r.idx 563 | if move > 0 { 564 | r.w.Write([]byte(string(r.buf[r.idx : r.idx+move]))) 565 | } else { 566 | r.w.Write(bytes.Repeat([]byte("\b"), r.calWidth(move))) 567 | } 568 | r.w.Write([]byte("\033[" + style + "m")) 569 | r.w.Write([]byte(string(r.buf[start:end]))) 570 | r.w.Write([]byte("\033[0m")) 571 | // TODO: move back 572 | } 573 | 574 | func (r *RuneBuffer) SetWithIdx(idx int, buf []rune) { 575 | r.Refresh(func() { 576 | r.buf = buf 577 | r.idx = idx 578 | }) 579 | } 580 | 581 | func (r *RuneBuffer) Set(buf []rune) { 582 | r.SetWithIdx(len(buf), buf) 583 | } 584 | 585 | func (r *RuneBuffer) SetPrompt(prompt string) { 586 | r.Lock() 587 | r.prompt = []rune(prompt) 588 | r.Unlock() 589 | } 590 | 591 | func (r *RuneBuffer) cleanOutput(w io.Writer, idxLine int) { 592 | buf := bufio.NewWriter(w) 593 | 594 | if r.width == 0 { 595 | buf.WriteString(strings.Repeat("\r\b", len(r.buf)+r.promptLen())) 596 | buf.Write([]byte("\033[J")) 597 | } else { 598 | buf.Write([]byte("\033[J")) // just like ^k :) 599 | if idxLine == 0 { 600 | buf.WriteString("\033[2K") 601 | buf.WriteString("\r") 602 | } else { 603 | for i := 0; i < idxLine; i++ { 604 | io.WriteString(buf, "\033[2K\r\033[A") 605 | } 606 | io.WriteString(buf, "\033[2K\r") 607 | } 608 | } 609 | buf.Flush() 610 | return 611 | } 612 | 613 | func (r *RuneBuffer) Clean() { 614 | r.Lock() 615 | r.clean() 616 | r.Unlock() 617 | } 618 | 619 | func (r *RuneBuffer) clean() { 620 | r.cleanWithIdxLine(r.idxLine(r.width)) 621 | } 622 | 623 | func (r *RuneBuffer) cleanWithIdxLine(idxLine int) { 624 | if r.hadClean || !r.interactive { 625 | return 626 | } 627 | r.hadClean = true 628 | r.cleanOutput(r.w, idxLine) 629 | } 630 | -------------------------------------------------------------------------------- /runes.go: -------------------------------------------------------------------------------- 1 | package readline 2 | 3 | import ( 4 | "bytes" 5 | "unicode" 6 | "unicode/utf8" 7 | ) 8 | 9 | var runes = Runes{} 10 | var TabWidth = 4 11 | 12 | type Runes struct{} 13 | 14 | func (Runes) EqualRune(a, b rune, fold bool) bool { 15 | if a == b { 16 | return true 17 | } 18 | if !fold { 19 | return false 20 | } 21 | if a > b { 22 | a, b = b, a 23 | } 24 | if b < utf8.RuneSelf && 'A' <= a && a <= 'Z' { 25 | if b == a+'a'-'A' { 26 | return true 27 | } 28 | } 29 | return false 30 | } 31 | 32 | func (r Runes) EqualRuneFold(a, b rune) bool { 33 | return r.EqualRune(a, b, true) 34 | } 35 | 36 | func (r Runes) EqualFold(a, b []rune) bool { 37 | if len(a) != len(b) { 38 | return false 39 | } 40 | for i := 0; i < len(a); i++ { 41 | if r.EqualRuneFold(a[i], b[i]) { 42 | continue 43 | } 44 | return false 45 | } 46 | 47 | return true 48 | } 49 | 50 | func (Runes) Equal(a, b []rune) bool { 51 | if len(a) != len(b) { 52 | return false 53 | } 54 | for i := 0; i < len(a); i++ { 55 | if a[i] != b[i] { 56 | return false 57 | } 58 | } 59 | return true 60 | } 61 | 62 | func (rs Runes) IndexAllBckEx(r, sub []rune, fold bool) int { 63 | for i := len(r) - len(sub); i >= 0; i-- { 64 | found := true 65 | for j := 0; j < len(sub); j++ { 66 | if !rs.EqualRune(r[i+j], sub[j], fold) { 67 | found = false 68 | break 69 | } 70 | } 71 | if found { 72 | return i 73 | } 74 | } 75 | return -1 76 | } 77 | 78 | // Search in runes from end to front 79 | func (rs Runes) IndexAllBck(r, sub []rune) int { 80 | return rs.IndexAllBckEx(r, sub, false) 81 | } 82 | 83 | // Search in runes from front to end 84 | func (rs Runes) IndexAll(r, sub []rune) int { 85 | return rs.IndexAllEx(r, sub, false) 86 | } 87 | 88 | func (rs Runes) IndexAllEx(r, sub []rune, fold bool) int { 89 | for i := 0; i < len(r); i++ { 90 | found := true 91 | if len(r[i:]) < len(sub) { 92 | return -1 93 | } 94 | for j := 0; j < len(sub); j++ { 95 | if !rs.EqualRune(r[i+j], sub[j], fold) { 96 | found = false 97 | break 98 | } 99 | } 100 | if found { 101 | return i 102 | } 103 | } 104 | return -1 105 | } 106 | 107 | func (Runes) Index(r rune, rs []rune) int { 108 | for i := 0; i < len(rs); i++ { 109 | if rs[i] == r { 110 | return i 111 | } 112 | } 113 | return -1 114 | } 115 | 116 | func (Runes) ColorFilter(r []rune) []rune { 117 | newr := make([]rune, 0, len(r)) 118 | for pos := 0; pos < len(r); pos++ { 119 | if r[pos] == '\033' && r[pos+1] == '[' { 120 | idx := runes.Index('m', r[pos+2:]) 121 | if idx == -1 { 122 | continue 123 | } 124 | pos += idx + 2 125 | continue 126 | } 127 | newr = append(newr, r[pos]) 128 | } 129 | return newr 130 | } 131 | 132 | var zeroWidth = []*unicode.RangeTable{ 133 | unicode.Mn, 134 | unicode.Me, 135 | unicode.Cc, 136 | unicode.Cf, 137 | } 138 | 139 | var doubleWidth = []*unicode.RangeTable{ 140 | unicode.Han, 141 | unicode.Hangul, 142 | unicode.Hiragana, 143 | unicode.Katakana, 144 | } 145 | 146 | func (Runes) Width(r rune) int { 147 | if r == '\t' { 148 | return TabWidth 149 | } 150 | if unicode.IsOneOf(zeroWidth, r) { 151 | return 0 152 | } 153 | if unicode.IsOneOf(doubleWidth, r) { 154 | return 2 155 | } 156 | return 1 157 | } 158 | 159 | func (Runes) WidthAll(r []rune) (length int) { 160 | for i := 0; i < len(r); i++ { 161 | length += runes.Width(r[i]) 162 | } 163 | return 164 | } 165 | 166 | func (Runes) Backspace(r []rune) []byte { 167 | return bytes.Repeat([]byte{'\b'}, runes.WidthAll(r)) 168 | } 169 | 170 | func (Runes) Copy(r []rune) []rune { 171 | n := make([]rune, len(r)) 172 | copy(n, r) 173 | return n 174 | } 175 | 176 | func (Runes) HasPrefixFold(r, prefix []rune) bool { 177 | if len(r) < len(prefix) { 178 | return false 179 | } 180 | return runes.EqualFold(r[:len(prefix)], prefix) 181 | } 182 | 183 | func (Runes) HasPrefix(r, prefix []rune) bool { 184 | if len(r) < len(prefix) { 185 | return false 186 | } 187 | return runes.Equal(r[:len(prefix)], prefix) 188 | } 189 | 190 | func (Runes) Aggregate(candicate [][]rune) (same []rune, size int) { 191 | for i := 0; i < len(candicate[0]); i++ { 192 | for j := 0; j < len(candicate)-1; j++ { 193 | if i >= len(candicate[j]) || i >= len(candicate[j+1]) { 194 | goto aggregate 195 | } 196 | if candicate[j][i] != candicate[j+1][i] { 197 | goto aggregate 198 | } 199 | } 200 | size = i + 1 201 | } 202 | aggregate: 203 | if size > 0 { 204 | same = runes.Copy(candicate[0][:size]) 205 | for i := 0; i < len(candicate); i++ { 206 | n := runes.Copy(candicate[i]) 207 | copy(n, n[size:]) 208 | candicate[i] = n[:len(n)-size] 209 | } 210 | } 211 | return 212 | } 213 | 214 | func (Runes) TrimSpaceLeft(in []rune) []rune { 215 | firstIndex := len(in) 216 | for i, r := range in { 217 | if unicode.IsSpace(r) == false { 218 | firstIndex = i 219 | break 220 | } 221 | } 222 | return in[firstIndex:] 223 | } 224 | -------------------------------------------------------------------------------- /runes/runes.go: -------------------------------------------------------------------------------- 1 | // deprecated. 2 | // see https://github.com/chzyer/readline/issues/43 3 | // use github.com/chzyer/readline/runes.go 4 | package runes 5 | 6 | import ( 7 | "bytes" 8 | "unicode" 9 | ) 10 | 11 | func Equal(a, b []rune) bool { 12 | if len(a) != len(b) { 13 | return false 14 | } 15 | for i := 0; i < len(a); i++ { 16 | if a[i] != b[i] { 17 | return false 18 | } 19 | } 20 | return true 21 | } 22 | 23 | // Search in runes from end to front 24 | func IndexAllBck(r, sub []rune) int { 25 | for i := len(r) - len(sub); i >= 0; i-- { 26 | found := true 27 | for j := 0; j < len(sub); j++ { 28 | if r[i+j] != sub[j] { 29 | found = false 30 | break 31 | } 32 | } 33 | if found { 34 | return i 35 | } 36 | } 37 | return -1 38 | } 39 | 40 | // Search in runes from front to end 41 | func IndexAll(r, sub []rune) int { 42 | for i := 0; i < len(r); i++ { 43 | found := true 44 | if len(r[i:]) < len(sub) { 45 | return -1 46 | } 47 | for j := 0; j < len(sub); j++ { 48 | if r[i+j] != sub[j] { 49 | found = false 50 | break 51 | } 52 | } 53 | if found { 54 | return i 55 | } 56 | } 57 | return -1 58 | } 59 | 60 | func Index(r rune, rs []rune) int { 61 | for i := 0; i < len(rs); i++ { 62 | if rs[i] == r { 63 | return i 64 | } 65 | } 66 | return -1 67 | } 68 | 69 | func ColorFilter(r []rune) []rune { 70 | newr := make([]rune, 0, len(r)) 71 | for pos := 0; pos < len(r); pos++ { 72 | if r[pos] == '\033' && r[pos+1] == '[' { 73 | idx := Index('m', r[pos+2:]) 74 | if idx == -1 { 75 | continue 76 | } 77 | pos += idx + 2 78 | continue 79 | } 80 | newr = append(newr, r[pos]) 81 | } 82 | return newr 83 | } 84 | 85 | var zeroWidth = []*unicode.RangeTable{ 86 | unicode.Mn, 87 | unicode.Me, 88 | unicode.Cc, 89 | unicode.Cf, 90 | } 91 | 92 | var doubleWidth = []*unicode.RangeTable{ 93 | unicode.Han, 94 | unicode.Hangul, 95 | unicode.Hiragana, 96 | unicode.Katakana, 97 | } 98 | 99 | func Width(r rune) int { 100 | if unicode.IsOneOf(zeroWidth, r) { 101 | return 0 102 | } 103 | if unicode.IsOneOf(doubleWidth, r) { 104 | return 2 105 | } 106 | return 1 107 | } 108 | 109 | func WidthAll(r []rune) (length int) { 110 | for i := 0; i < len(r); i++ { 111 | length += Width(r[i]) 112 | } 113 | return 114 | } 115 | 116 | func Backspace(r []rune) []byte { 117 | return bytes.Repeat([]byte{'\b'}, WidthAll(r)) 118 | } 119 | 120 | func Copy(r []rune) []rune { 121 | n := make([]rune, len(r)) 122 | copy(n, r) 123 | return n 124 | } 125 | 126 | func HasPrefix(r, prefix []rune) bool { 127 | if len(r) < len(prefix) { 128 | return false 129 | } 130 | return Equal(r[:len(prefix)], prefix) 131 | } 132 | 133 | func Aggregate(candicate [][]rune) (same []rune, size int) { 134 | for i := 0; i < len(candicate[0]); i++ { 135 | for j := 0; j < len(candicate)-1; j++ { 136 | if i >= len(candicate[j]) || i >= len(candicate[j+1]) { 137 | goto aggregate 138 | } 139 | if candicate[j][i] != candicate[j+1][i] { 140 | goto aggregate 141 | } 142 | } 143 | size = i + 1 144 | } 145 | aggregate: 146 | if size > 0 { 147 | same = Copy(candicate[0][:size]) 148 | for i := 0; i < len(candicate); i++ { 149 | n := Copy(candicate[i]) 150 | copy(n, n[size:]) 151 | candicate[i] = n[:len(n)-size] 152 | } 153 | } 154 | return 155 | } 156 | -------------------------------------------------------------------------------- /runes/runes_test.go: -------------------------------------------------------------------------------- 1 | package runes 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | ) 7 | 8 | type twidth struct { 9 | r []rune 10 | length int 11 | } 12 | 13 | func TestRuneWidth(t *testing.T) { 14 | runes := []twidth{ 15 | {[]rune("☭"), 1}, 16 | {[]rune("a"), 1}, 17 | {[]rune("你"), 2}, 18 | {ColorFilter([]rune("☭\033[13;1m你")), 3}, 19 | } 20 | for _, r := range runes { 21 | if w := WidthAll(r.r); w != r.length { 22 | t.Fatal("result not expect", r.r, r.length, w) 23 | } 24 | } 25 | } 26 | 27 | type tagg struct { 28 | r [][]rune 29 | e [][]rune 30 | length int 31 | } 32 | 33 | func TestAggRunes(t *testing.T) { 34 | runes := []tagg{ 35 | { 36 | [][]rune{[]rune("ab"), []rune("a"), []rune("abc")}, 37 | [][]rune{[]rune("b"), []rune(""), []rune("bc")}, 38 | 1, 39 | }, 40 | { 41 | [][]rune{[]rune("addb"), []rune("ajkajsdf"), []rune("aasdfkc")}, 42 | [][]rune{[]rune("ddb"), []rune("jkajsdf"), []rune("asdfkc")}, 43 | 1, 44 | }, 45 | { 46 | [][]rune{[]rune("ddb"), []rune("ajksdf"), []rune("aasdfkc")}, 47 | [][]rune{[]rune("ddb"), []rune("ajksdf"), []rune("aasdfkc")}, 48 | 0, 49 | }, 50 | { 51 | [][]rune{[]rune("ddb"), []rune("ddajksdf"), []rune("ddaasdfkc")}, 52 | [][]rune{[]rune("b"), []rune("ajksdf"), []rune("aasdfkc")}, 53 | 2, 54 | }, 55 | } 56 | for _, r := range runes { 57 | same, off := Aggregate(r.r) 58 | if off != r.length { 59 | t.Fatal("result not expect", off) 60 | } 61 | if len(same) != off { 62 | t.Fatal("result not expect", same) 63 | } 64 | if !reflect.DeepEqual(r.r, r.e) { 65 | t.Fatal("result not expect") 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /runes_test.go: -------------------------------------------------------------------------------- 1 | package readline 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | ) 7 | 8 | type twidth struct { 9 | r []rune 10 | length int 11 | } 12 | 13 | func TestRuneWidth(t *testing.T) { 14 | rs := []twidth{ 15 | {[]rune("☭"), 1}, 16 | {[]rune("a"), 1}, 17 | {[]rune("你"), 2}, 18 | {runes.ColorFilter([]rune("☭\033[13;1m你")), 3}, 19 | } 20 | for _, r := range rs { 21 | if w := runes.WidthAll(r.r); w != r.length { 22 | t.Fatal("result not expect", r.r, r.length, w) 23 | } 24 | } 25 | } 26 | 27 | type tagg struct { 28 | r [][]rune 29 | e [][]rune 30 | length int 31 | } 32 | 33 | func TestAggRunes(t *testing.T) { 34 | rs := []tagg{ 35 | { 36 | [][]rune{[]rune("ab"), []rune("a"), []rune("abc")}, 37 | [][]rune{[]rune("b"), []rune(""), []rune("bc")}, 38 | 1, 39 | }, 40 | { 41 | [][]rune{[]rune("addb"), []rune("ajkajsdf"), []rune("aasdfkc")}, 42 | [][]rune{[]rune("ddb"), []rune("jkajsdf"), []rune("asdfkc")}, 43 | 1, 44 | }, 45 | { 46 | [][]rune{[]rune("ddb"), []rune("ajksdf"), []rune("aasdfkc")}, 47 | [][]rune{[]rune("ddb"), []rune("ajksdf"), []rune("aasdfkc")}, 48 | 0, 49 | }, 50 | { 51 | [][]rune{[]rune("ddb"), []rune("ddajksdf"), []rune("ddaasdfkc")}, 52 | [][]rune{[]rune("b"), []rune("ajksdf"), []rune("aasdfkc")}, 53 | 2, 54 | }, 55 | } 56 | for _, r := range rs { 57 | same, off := runes.Aggregate(r.r) 58 | if off != r.length { 59 | t.Fatal("result not expect", off) 60 | } 61 | if len(same) != off { 62 | t.Fatal("result not expect", same) 63 | } 64 | if !reflect.DeepEqual(r.r, r.e) { 65 | t.Fatal("result not expect") 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /search.go: -------------------------------------------------------------------------------- 1 | package readline 2 | 3 | import ( 4 | "bytes" 5 | "container/list" 6 | "fmt" 7 | "io" 8 | ) 9 | 10 | const ( 11 | S_STATE_FOUND = iota 12 | S_STATE_FAILING 13 | ) 14 | 15 | const ( 16 | S_DIR_BCK = iota 17 | S_DIR_FWD 18 | ) 19 | 20 | type opSearch struct { 21 | inMode bool 22 | state int 23 | dir int 24 | source *list.Element 25 | w io.Writer 26 | buf *RuneBuffer 27 | data []rune 28 | history *opHistory 29 | cfg *Config 30 | markStart int 31 | markEnd int 32 | width int 33 | } 34 | 35 | func newOpSearch(w io.Writer, buf *RuneBuffer, history *opHistory, cfg *Config, width int) *opSearch { 36 | return &opSearch{ 37 | w: w, 38 | buf: buf, 39 | cfg: cfg, 40 | history: history, 41 | width: width, 42 | } 43 | } 44 | 45 | func (o *opSearch) OnWidthChange(newWidth int) { 46 | o.width = newWidth 47 | } 48 | 49 | func (o *opSearch) IsSearchMode() bool { 50 | return o.inMode 51 | } 52 | 53 | func (o *opSearch) SearchBackspace() { 54 | if len(o.data) > 0 { 55 | o.data = o.data[:len(o.data)-1] 56 | o.search(true) 57 | } 58 | } 59 | 60 | func (o *opSearch) findHistoryBy(isNewSearch bool) (int, *list.Element) { 61 | if o.dir == S_DIR_BCK { 62 | return o.history.FindBck(isNewSearch, o.data, o.buf.idx) 63 | } 64 | return o.history.FindFwd(isNewSearch, o.data, o.buf.idx) 65 | } 66 | 67 | func (o *opSearch) search(isChange bool) bool { 68 | if len(o.data) == 0 { 69 | o.state = S_STATE_FOUND 70 | o.SearchRefresh(-1) 71 | return true 72 | } 73 | idx, elem := o.findHistoryBy(isChange) 74 | if elem == nil { 75 | o.SearchRefresh(-2) 76 | return false 77 | } 78 | o.history.current = elem 79 | 80 | item := o.history.showItem(o.history.current.Value) 81 | start, end := 0, 0 82 | if o.dir == S_DIR_BCK { 83 | start, end = idx, idx+len(o.data) 84 | } else { 85 | start, end = idx, idx+len(o.data) 86 | idx += len(o.data) 87 | } 88 | o.buf.SetWithIdx(idx, item) 89 | o.markStart, o.markEnd = start, end 90 | o.SearchRefresh(idx) 91 | return true 92 | } 93 | 94 | func (o *opSearch) SearchChar(r rune) { 95 | o.data = append(o.data, r) 96 | o.search(true) 97 | } 98 | 99 | func (o *opSearch) SearchMode(dir int) bool { 100 | if o.width == 0 { 101 | return false 102 | } 103 | alreadyInMode := o.inMode 104 | o.inMode = true 105 | o.dir = dir 106 | o.source = o.history.current 107 | if alreadyInMode { 108 | o.search(false) 109 | } else { 110 | o.SearchRefresh(-1) 111 | } 112 | return true 113 | } 114 | 115 | func (o *opSearch) ExitSearchMode(revert bool) { 116 | if revert { 117 | o.history.current = o.source 118 | o.buf.Set(o.history.showItem(o.history.current.Value)) 119 | } 120 | o.markStart, o.markEnd = 0, 0 121 | o.state = S_STATE_FOUND 122 | o.inMode = false 123 | o.source = nil 124 | o.data = nil 125 | } 126 | 127 | func (o *opSearch) SearchRefresh(x int) { 128 | if x == -2 { 129 | o.state = S_STATE_FAILING 130 | } else if x >= 0 { 131 | o.state = S_STATE_FOUND 132 | } 133 | if x < 0 { 134 | x = o.buf.idx 135 | } 136 | x = o.buf.CurrentWidth(x) 137 | x += o.buf.PromptLen() 138 | x = x % o.width 139 | 140 | if o.markStart > 0 { 141 | o.buf.SetStyle(o.markStart, o.markEnd, "4") 142 | } 143 | 144 | lineCnt := o.buf.CursorLineCount() 145 | buf := bytes.NewBuffer(nil) 146 | buf.Write(bytes.Repeat([]byte("\n"), lineCnt)) 147 | buf.WriteString("\033[J") 148 | if o.state == S_STATE_FAILING { 149 | buf.WriteString("failing ") 150 | } 151 | if o.dir == S_DIR_BCK { 152 | buf.WriteString("bck") 153 | } else if o.dir == S_DIR_FWD { 154 | buf.WriteString("fwd") 155 | } 156 | buf.WriteString("-i-search: ") 157 | buf.WriteString(string(o.data)) // keyword 158 | buf.WriteString("\033[4m \033[0m") // _ 159 | fmt.Fprintf(buf, "\r\033[%dA", lineCnt) // move prev 160 | if x > 0 { 161 | fmt.Fprintf(buf, "\033[%dC", x) // move forward 162 | } 163 | o.w.Write(buf.Bytes()) 164 | } 165 | -------------------------------------------------------------------------------- /std.go: -------------------------------------------------------------------------------- 1 | package readline 2 | 3 | import ( 4 | "io" 5 | "os" 6 | "sync" 7 | "sync/atomic" 8 | ) 9 | 10 | var ( 11 | Stdin io.ReadCloser = os.Stdin 12 | Stdout io.WriteCloser = os.Stdout 13 | Stderr io.WriteCloser = os.Stderr 14 | ) 15 | 16 | var ( 17 | std *Instance 18 | stdOnce sync.Once 19 | ) 20 | 21 | // global instance will not submit history automatic 22 | func getInstance() *Instance { 23 | stdOnce.Do(func() { 24 | std, _ = NewEx(&Config{ 25 | DisableAutoSaveHistory: true, 26 | }) 27 | }) 28 | return std 29 | } 30 | 31 | // let readline load history from filepath 32 | // and try to persist history into disk 33 | // set fp to "" to prevent readline persisting history to disk 34 | // so the `AddHistory` will return nil error forever. 35 | func SetHistoryPath(fp string) { 36 | ins := getInstance() 37 | cfg := ins.Config.Clone() 38 | cfg.HistoryFile = fp 39 | ins.SetConfig(cfg) 40 | } 41 | 42 | // set auto completer to global instance 43 | func SetAutoComplete(completer AutoCompleter) { 44 | ins := getInstance() 45 | cfg := ins.Config.Clone() 46 | cfg.AutoComplete = completer 47 | ins.SetConfig(cfg) 48 | } 49 | 50 | // add history to global instance manually 51 | // raise error only if `SetHistoryPath` is set with a non-empty path 52 | func AddHistory(content string) error { 53 | ins := getInstance() 54 | return ins.SaveHistory(content) 55 | } 56 | 57 | func Password(prompt string) ([]byte, error) { 58 | ins := getInstance() 59 | return ins.ReadPassword(prompt) 60 | } 61 | 62 | // readline with global configs 63 | func Line(prompt string) (string, error) { 64 | ins := getInstance() 65 | ins.SetPrompt(prompt) 66 | return ins.Readline() 67 | } 68 | 69 | type CancelableStdin struct { 70 | r io.Reader 71 | mutex sync.Mutex 72 | stop chan struct{} 73 | closed int32 74 | notify chan struct{} 75 | data []byte 76 | read int 77 | err error 78 | } 79 | 80 | func NewCancelableStdin(r io.Reader) *CancelableStdin { 81 | c := &CancelableStdin{ 82 | r: r, 83 | notify: make(chan struct{}), 84 | stop: make(chan struct{}), 85 | } 86 | go c.ioloop() 87 | return c 88 | } 89 | 90 | func (c *CancelableStdin) ioloop() { 91 | loop: 92 | for { 93 | select { 94 | case <-c.notify: 95 | c.read, c.err = c.r.Read(c.data) 96 | select { 97 | case c.notify <- struct{}{}: 98 | case <-c.stop: 99 | break loop 100 | } 101 | case <-c.stop: 102 | break loop 103 | } 104 | } 105 | } 106 | 107 | func (c *CancelableStdin) Read(b []byte) (n int, err error) { 108 | c.mutex.Lock() 109 | defer c.mutex.Unlock() 110 | if atomic.LoadInt32(&c.closed) == 1 { 111 | return 0, io.EOF 112 | } 113 | 114 | c.data = b 115 | select { 116 | case c.notify <- struct{}{}: 117 | case <-c.stop: 118 | return 0, io.EOF 119 | } 120 | select { 121 | case <-c.notify: 122 | return c.read, c.err 123 | case <-c.stop: 124 | return 0, io.EOF 125 | } 126 | } 127 | 128 | func (c *CancelableStdin) Close() error { 129 | if atomic.CompareAndSwapInt32(&c.closed, 0, 1) { 130 | close(c.stop) 131 | } 132 | return nil 133 | } 134 | 135 | // FillableStdin is a stdin reader which can prepend some data before 136 | // reading into the real stdin 137 | type FillableStdin struct { 138 | sync.Mutex 139 | stdin io.Reader 140 | stdinBuffer io.ReadCloser 141 | buf []byte 142 | bufErr error 143 | } 144 | 145 | // NewFillableStdin gives you FillableStdin 146 | func NewFillableStdin(stdin io.Reader) (io.ReadCloser, io.Writer) { 147 | r, w := io.Pipe() 148 | s := &FillableStdin{ 149 | stdinBuffer: r, 150 | stdin: stdin, 151 | } 152 | s.ioloop() 153 | return s, w 154 | } 155 | 156 | func (s *FillableStdin) ioloop() { 157 | go func() { 158 | for { 159 | bufR := make([]byte, 100) 160 | var n int 161 | n, s.bufErr = s.stdinBuffer.Read(bufR) 162 | if s.bufErr != nil { 163 | if s.bufErr == io.ErrClosedPipe { 164 | break 165 | } 166 | } 167 | s.Lock() 168 | s.buf = append(s.buf, bufR[:n]...) 169 | s.Unlock() 170 | } 171 | }() 172 | } 173 | 174 | // Read will read from the local buffer and if no data, read from stdin 175 | func (s *FillableStdin) Read(p []byte) (n int, err error) { 176 | s.Lock() 177 | i := len(s.buf) 178 | if len(p) < i { 179 | i = len(p) 180 | } 181 | if i > 0 { 182 | n := copy(p, s.buf) 183 | s.buf = s.buf[:0] 184 | cerr := s.bufErr 185 | s.bufErr = nil 186 | s.Unlock() 187 | return n, cerr 188 | } 189 | s.Unlock() 190 | n, err = s.stdin.Read(p) 191 | return n, err 192 | } 193 | 194 | func (s *FillableStdin) Close() error { 195 | s.stdinBuffer.Close() 196 | return nil 197 | } 198 | -------------------------------------------------------------------------------- /std_windows.go: -------------------------------------------------------------------------------- 1 | // +build windows 2 | 3 | package readline 4 | 5 | func init() { 6 | Stdin = NewRawReader() 7 | Stdout = NewANSIWriter(Stdout) 8 | Stderr = NewANSIWriter(Stderr) 9 | } 10 | -------------------------------------------------------------------------------- /term.go: -------------------------------------------------------------------------------- 1 | // Copyright 2011 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // +build aix darwin dragonfly freebsd linux,!appengine netbsd openbsd os400 solaris 6 | 7 | // Package terminal provides support functions for dealing with terminals, as 8 | // commonly found on UNIX systems. 9 | // 10 | // Putting a terminal into raw mode is the most common requirement: 11 | // 12 | // oldState, err := terminal.MakeRaw(0) 13 | // if err != nil { 14 | // panic(err) 15 | // } 16 | // defer terminal.Restore(0, oldState) 17 | package readline 18 | 19 | import ( 20 | "io" 21 | "syscall" 22 | ) 23 | 24 | // State contains the state of a terminal. 25 | type State struct { 26 | termios Termios 27 | } 28 | 29 | // IsTerminal returns true if the given file descriptor is a terminal. 30 | func IsTerminal(fd int) bool { 31 | _, err := getTermios(fd) 32 | return err == nil 33 | } 34 | 35 | // MakeRaw put the terminal connected to the given file descriptor into raw 36 | // mode and returns the previous state of the terminal so that it can be 37 | // restored. 38 | func MakeRaw(fd int) (*State, error) { 39 | var oldState State 40 | 41 | if termios, err := getTermios(fd); err != nil { 42 | return nil, err 43 | } else { 44 | oldState.termios = *termios 45 | } 46 | 47 | newState := oldState.termios 48 | // This attempts to replicate the behaviour documented for cfmakeraw in 49 | // the termios(3) manpage. 50 | newState.Iflag &^= syscall.IGNBRK | syscall.BRKINT | syscall.PARMRK | syscall.ISTRIP | syscall.INLCR | syscall.IGNCR | syscall.ICRNL | syscall.IXON 51 | // newState.Oflag &^= syscall.OPOST 52 | newState.Lflag &^= syscall.ECHO | syscall.ECHONL | syscall.ICANON | syscall.ISIG | syscall.IEXTEN 53 | newState.Cflag &^= syscall.CSIZE | syscall.PARENB 54 | newState.Cflag |= syscall.CS8 55 | 56 | newState.Cc[syscall.VMIN] = 1 57 | newState.Cc[syscall.VTIME] = 0 58 | 59 | return &oldState, setTermios(fd, &newState) 60 | } 61 | 62 | // GetState returns the current state of a terminal which may be useful to 63 | // restore the terminal after a signal. 64 | func GetState(fd int) (*State, error) { 65 | termios, err := getTermios(fd) 66 | if err != nil { 67 | return nil, err 68 | } 69 | 70 | return &State{termios: *termios}, nil 71 | } 72 | 73 | // Restore restores the terminal connected to the given file descriptor to a 74 | // previous state. 75 | func restoreTerm(fd int, state *State) error { 76 | return setTermios(fd, &state.termios) 77 | } 78 | 79 | // ReadPassword reads a line of input from a terminal without local echo. This 80 | // is commonly used for inputting passwords and other sensitive data. The slice 81 | // returned does not include the \n. 82 | func ReadPassword(fd int) ([]byte, error) { 83 | oldState, err := getTermios(fd) 84 | if err != nil { 85 | return nil, err 86 | } 87 | 88 | newState := oldState 89 | newState.Lflag &^= syscall.ECHO 90 | newState.Lflag |= syscall.ICANON | syscall.ISIG 91 | newState.Iflag |= syscall.ICRNL 92 | if err := setTermios(fd, newState); err != nil { 93 | return nil, err 94 | } 95 | 96 | defer func() { 97 | setTermios(fd, oldState) 98 | }() 99 | 100 | var buf [16]byte 101 | var ret []byte 102 | for { 103 | n, err := syscall.Read(fd, buf[:]) 104 | if err != nil { 105 | return nil, err 106 | } 107 | if n == 0 { 108 | if len(ret) == 0 { 109 | return nil, io.EOF 110 | } 111 | break 112 | } 113 | if buf[n-1] == '\n' { 114 | n-- 115 | } 116 | ret = append(ret, buf[:n]...) 117 | if n < len(buf) { 118 | break 119 | } 120 | } 121 | 122 | return ret, nil 123 | } 124 | -------------------------------------------------------------------------------- /term_bsd.go: -------------------------------------------------------------------------------- 1 | // Copyright 2013 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // +build darwin dragonfly freebsd netbsd openbsd 6 | 7 | package readline 8 | 9 | import ( 10 | "syscall" 11 | "unsafe" 12 | ) 13 | 14 | func getTermios(fd int) (*Termios, error) { 15 | termios := new(Termios) 16 | _, _, err := syscall.Syscall6(syscall.SYS_IOCTL, uintptr(fd), syscall.TIOCGETA, uintptr(unsafe.Pointer(termios)), 0, 0, 0) 17 | if err != 0 { 18 | return nil, err 19 | } 20 | return termios, nil 21 | } 22 | 23 | func setTermios(fd int, termios *Termios) error { 24 | _, _, err := syscall.Syscall6(syscall.SYS_IOCTL, uintptr(fd), syscall.TIOCSETA, uintptr(unsafe.Pointer(termios)), 0, 0, 0) 25 | if err != 0 { 26 | return err 27 | } 28 | return nil 29 | } 30 | -------------------------------------------------------------------------------- /term_linux.go: -------------------------------------------------------------------------------- 1 | // Copyright 2013 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package readline 6 | 7 | import ( 8 | "syscall" 9 | "unsafe" 10 | ) 11 | 12 | // These constants are declared here, rather than importing 13 | // them from the syscall package as some syscall packages, even 14 | // on linux, for example gccgo, do not declare them. 15 | const ioctlReadTermios = 0x5401 // syscall.TCGETS 16 | const ioctlWriteTermios = 0x5402 // syscall.TCSETS 17 | 18 | func getTermios(fd int) (*Termios, error) { 19 | termios := new(Termios) 20 | _, _, err := syscall.Syscall6(syscall.SYS_IOCTL, uintptr(fd), ioctlReadTermios, uintptr(unsafe.Pointer(termios)), 0, 0, 0) 21 | if err != 0 { 22 | return nil, err 23 | } 24 | return termios, nil 25 | } 26 | 27 | func setTermios(fd int, termios *Termios) error { 28 | _, _, err := syscall.Syscall6(syscall.SYS_IOCTL, uintptr(fd), ioctlWriteTermios, uintptr(unsafe.Pointer(termios)), 0, 0, 0) 29 | if err != 0 { 30 | return err 31 | } 32 | return nil 33 | } 34 | -------------------------------------------------------------------------------- /term_nosyscall6.go: -------------------------------------------------------------------------------- 1 | // Copyright 2013 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // +build aix os400 solaris 6 | 7 | package readline 8 | 9 | import "golang.org/x/sys/unix" 10 | 11 | // GetSize returns the dimensions of the given terminal. 12 | func GetSize(fd int) (int, int, error) { 13 | ws, err := unix.IoctlGetWinsize(fd, unix.TIOCGWINSZ) 14 | if err != nil { 15 | return 0, 0, err 16 | } 17 | return int(ws.Col), int(ws.Row), nil 18 | } 19 | 20 | type Termios unix.Termios 21 | 22 | func getTermios(fd int) (*Termios, error) { 23 | termios, err := unix.IoctlGetTermios(fd, unix.TCGETS) 24 | if err != nil { 25 | return nil, err 26 | } 27 | return (*Termios)(termios), nil 28 | } 29 | 30 | func setTermios(fd int, termios *Termios) error { 31 | return unix.IoctlSetTermios(fd, unix.TCSETSF, (*unix.Termios)(termios)) 32 | } 33 | -------------------------------------------------------------------------------- /term_unix.go: -------------------------------------------------------------------------------- 1 | // Copyright 2011 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // +build darwin dragonfly freebsd linux,!appengine netbsd openbsd 6 | 7 | package readline 8 | 9 | import ( 10 | "syscall" 11 | "unsafe" 12 | ) 13 | 14 | type Termios syscall.Termios 15 | 16 | // GetSize returns the dimensions of the given terminal. 17 | func GetSize(fd int) (int, int, error) { 18 | var dimensions [4]uint16 19 | _, _, err := syscall.Syscall6(syscall.SYS_IOCTL, uintptr(fd), uintptr(syscall.TIOCGWINSZ), uintptr(unsafe.Pointer(&dimensions)), 0, 0, 0) 20 | if err != 0 { 21 | return 0, 0, err 22 | } 23 | return int(dimensions[1]), int(dimensions[0]), nil 24 | } 25 | -------------------------------------------------------------------------------- /term_windows.go: -------------------------------------------------------------------------------- 1 | // Copyright 2011 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // +build windows 6 | 7 | // Package terminal provides support functions for dealing with terminals, as 8 | // commonly found on UNIX systems. 9 | // 10 | // Putting a terminal into raw mode is the most common requirement: 11 | // 12 | // oldState, err := terminal.MakeRaw(0) 13 | // if err != nil { 14 | // panic(err) 15 | // } 16 | // defer terminal.Restore(0, oldState) 17 | package readline 18 | 19 | import ( 20 | "io" 21 | "syscall" 22 | "unsafe" 23 | ) 24 | 25 | const ( 26 | enableLineInput = 2 27 | enableEchoInput = 4 28 | enableProcessedInput = 1 29 | enableWindowInput = 8 30 | enableMouseInput = 16 31 | enableInsertMode = 32 32 | enableQuickEditMode = 64 33 | enableExtendedFlags = 128 34 | enableAutoPosition = 256 35 | enableProcessedOutput = 1 36 | enableWrapAtEolOutput = 2 37 | ) 38 | 39 | var kernel32 = syscall.NewLazyDLL("kernel32.dll") 40 | 41 | var ( 42 | procGetConsoleMode = kernel32.NewProc("GetConsoleMode") 43 | procSetConsoleMode = kernel32.NewProc("SetConsoleMode") 44 | procGetConsoleScreenBufferInfo = kernel32.NewProc("GetConsoleScreenBufferInfo") 45 | ) 46 | 47 | type ( 48 | coord struct { 49 | x short 50 | y short 51 | } 52 | smallRect struct { 53 | left short 54 | top short 55 | right short 56 | bottom short 57 | } 58 | consoleScreenBufferInfo struct { 59 | size coord 60 | cursorPosition coord 61 | attributes word 62 | window smallRect 63 | maximumWindowSize coord 64 | } 65 | ) 66 | 67 | type State struct { 68 | mode uint32 69 | } 70 | 71 | // IsTerminal returns true if the given file descriptor is a terminal. 72 | func IsTerminal(fd int) bool { 73 | var st uint32 74 | r, _, e := syscall.Syscall(procGetConsoleMode.Addr(), 2, uintptr(fd), uintptr(unsafe.Pointer(&st)), 0) 75 | return r != 0 && e == 0 76 | } 77 | 78 | // MakeRaw put the terminal connected to the given file descriptor into raw 79 | // mode and returns the previous state of the terminal so that it can be 80 | // restored. 81 | func MakeRaw(fd int) (*State, error) { 82 | var st uint32 83 | _, _, e := syscall.Syscall(procGetConsoleMode.Addr(), 2, uintptr(fd), uintptr(unsafe.Pointer(&st)), 0) 84 | if e != 0 { 85 | return nil, error(e) 86 | } 87 | raw := st &^ (enableEchoInput | enableProcessedInput | enableLineInput | enableProcessedOutput) 88 | _, _, e = syscall.Syscall(procSetConsoleMode.Addr(), 2, uintptr(fd), uintptr(raw), 0) 89 | if e != 0 { 90 | return nil, error(e) 91 | } 92 | return &State{st}, nil 93 | } 94 | 95 | // GetState returns the current state of a terminal which may be useful to 96 | // restore the terminal after a signal. 97 | func GetState(fd int) (*State, error) { 98 | var st uint32 99 | _, _, e := syscall.Syscall(procGetConsoleMode.Addr(), 2, uintptr(fd), uintptr(unsafe.Pointer(&st)), 0) 100 | if e != 0 { 101 | return nil, error(e) 102 | } 103 | return &State{st}, nil 104 | } 105 | 106 | // Restore restores the terminal connected to the given file descriptor to a 107 | // previous state. 108 | func restoreTerm(fd int, state *State) error { 109 | _, _, err := syscall.Syscall(procSetConsoleMode.Addr(), 2, uintptr(fd), uintptr(state.mode), 0) 110 | return err 111 | } 112 | 113 | // GetSize returns the dimensions of the given terminal. 114 | func GetSize(fd int) (width, height int, err error) { 115 | var info consoleScreenBufferInfo 116 | _, _, e := syscall.Syscall(procGetConsoleScreenBufferInfo.Addr(), 2, uintptr(fd), uintptr(unsafe.Pointer(&info)), 0) 117 | if e != 0 { 118 | return 0, 0, error(e) 119 | } 120 | return int(info.size.x), int(info.size.y), nil 121 | } 122 | 123 | // ReadPassword reads a line of input from a terminal without local echo. This 124 | // is commonly used for inputting passwords and other sensitive data. The slice 125 | // returned does not include the \n. 126 | func ReadPassword(fd int) ([]byte, error) { 127 | var st uint32 128 | _, _, e := syscall.Syscall(procGetConsoleMode.Addr(), 2, uintptr(fd), uintptr(unsafe.Pointer(&st)), 0) 129 | if e != 0 { 130 | return nil, error(e) 131 | } 132 | old := st 133 | 134 | st &^= (enableEchoInput) 135 | st |= (enableProcessedInput | enableLineInput | enableProcessedOutput) 136 | _, _, e = syscall.Syscall(procSetConsoleMode.Addr(), 2, uintptr(fd), uintptr(st), 0) 137 | if e != 0 { 138 | return nil, error(e) 139 | } 140 | 141 | defer func() { 142 | syscall.Syscall(procSetConsoleMode.Addr(), 2, uintptr(fd), uintptr(old), 0) 143 | }() 144 | 145 | var buf [16]byte 146 | var ret []byte 147 | for { 148 | n, err := syscall.Read(syscall.Handle(fd), buf[:]) 149 | if err != nil { 150 | return nil, err 151 | } 152 | if n == 0 { 153 | if len(ret) == 0 { 154 | return nil, io.EOF 155 | } 156 | break 157 | } 158 | if buf[n-1] == '\n' { 159 | n-- 160 | } 161 | if n > 0 && buf[n-1] == '\r' { 162 | n-- 163 | } 164 | ret = append(ret, buf[:n]...) 165 | if n < len(buf) { 166 | break 167 | } 168 | } 169 | 170 | return ret, nil 171 | } 172 | -------------------------------------------------------------------------------- /terminal.go: -------------------------------------------------------------------------------- 1 | package readline 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "io" 7 | "strings" 8 | "sync" 9 | "sync/atomic" 10 | ) 11 | 12 | type Terminal struct { 13 | m sync.Mutex 14 | cfg *Config 15 | outchan chan rune 16 | closed int32 17 | stopChan chan struct{} 18 | kickChan chan struct{} 19 | wg sync.WaitGroup 20 | isReading int32 21 | sleeping int32 22 | 23 | sizeChan chan string 24 | } 25 | 26 | func NewTerminal(cfg *Config) (*Terminal, error) { 27 | if err := cfg.Init(); err != nil { 28 | return nil, err 29 | } 30 | t := &Terminal{ 31 | cfg: cfg, 32 | kickChan: make(chan struct{}, 1), 33 | outchan: make(chan rune), 34 | stopChan: make(chan struct{}, 1), 35 | sizeChan: make(chan string, 1), 36 | } 37 | 38 | go t.ioloop() 39 | return t, nil 40 | } 41 | 42 | // SleepToResume will sleep myself, and return only if I'm resumed. 43 | func (t *Terminal) SleepToResume() { 44 | if !atomic.CompareAndSwapInt32(&t.sleeping, 0, 1) { 45 | return 46 | } 47 | defer atomic.StoreInt32(&t.sleeping, 0) 48 | 49 | t.ExitRawMode() 50 | ch := WaitForResume() 51 | SuspendMe() 52 | <-ch 53 | t.EnterRawMode() 54 | } 55 | 56 | func (t *Terminal) EnterRawMode() (err error) { 57 | return t.cfg.FuncMakeRaw() 58 | } 59 | 60 | func (t *Terminal) ExitRawMode() (err error) { 61 | return t.cfg.FuncExitRaw() 62 | } 63 | 64 | func (t *Terminal) Write(b []byte) (int, error) { 65 | return t.cfg.Stdout.Write(b) 66 | } 67 | 68 | // WriteStdin prefill the next Stdin fetch 69 | // Next time you call ReadLine() this value will be writen before the user input 70 | func (t *Terminal) WriteStdin(b []byte) (int, error) { 71 | return t.cfg.StdinWriter.Write(b) 72 | } 73 | 74 | type termSize struct { 75 | left int 76 | top int 77 | } 78 | 79 | func (t *Terminal) GetOffset(f func(offset string)) { 80 | go func() { 81 | f(<-t.sizeChan) 82 | }() 83 | t.Write([]byte("\033[6n")) 84 | } 85 | 86 | func (t *Terminal) Print(s string) { 87 | fmt.Fprintf(t.cfg.Stdout, "%s", s) 88 | } 89 | 90 | func (t *Terminal) PrintRune(r rune) { 91 | fmt.Fprintf(t.cfg.Stdout, "%c", r) 92 | } 93 | 94 | func (t *Terminal) Readline() *Operation { 95 | return NewOperation(t, t.cfg) 96 | } 97 | 98 | // return rune(0) if meet EOF 99 | func (t *Terminal) ReadRune() rune { 100 | ch, ok := <-t.outchan 101 | if !ok { 102 | return rune(0) 103 | } 104 | return ch 105 | } 106 | 107 | func (t *Terminal) IsReading() bool { 108 | return atomic.LoadInt32(&t.isReading) == 1 109 | } 110 | 111 | func (t *Terminal) KickRead() { 112 | select { 113 | case t.kickChan <- struct{}{}: 114 | default: 115 | } 116 | } 117 | 118 | func (t *Terminal) ioloop() { 119 | t.wg.Add(1) 120 | defer func() { 121 | t.wg.Done() 122 | close(t.outchan) 123 | }() 124 | 125 | var ( 126 | isEscape bool 127 | isEscapeEx bool 128 | isEscapeSS3 bool 129 | expectNextChar bool 130 | ) 131 | 132 | buf := bufio.NewReader(t.getStdin()) 133 | for { 134 | if !expectNextChar { 135 | atomic.StoreInt32(&t.isReading, 0) 136 | select { 137 | case <-t.kickChan: 138 | atomic.StoreInt32(&t.isReading, 1) 139 | case <-t.stopChan: 140 | return 141 | } 142 | } 143 | expectNextChar = false 144 | r, _, err := buf.ReadRune() 145 | if err != nil { 146 | if strings.Contains(err.Error(), "interrupted system call") { 147 | expectNextChar = true 148 | continue 149 | } 150 | break 151 | } 152 | 153 | if isEscape { 154 | isEscape = false 155 | if r == CharEscapeEx { 156 | // ^][ 157 | expectNextChar = true 158 | isEscapeEx = true 159 | continue 160 | } else if r == CharO { 161 | // ^]O 162 | expectNextChar = true 163 | isEscapeSS3 = true 164 | continue 165 | } 166 | r = escapeKey(r, buf) 167 | } else if isEscapeEx { 168 | isEscapeEx = false 169 | if key := readEscKey(r, buf); key != nil { 170 | r = escapeExKey(key) 171 | // offset 172 | if key.typ == 'R' { 173 | if _, _, ok := key.Get2(); ok { 174 | select { 175 | case t.sizeChan <- key.attr: 176 | default: 177 | } 178 | } 179 | expectNextChar = true 180 | continue 181 | } 182 | } 183 | if r == 0 { 184 | expectNextChar = true 185 | continue 186 | } 187 | } else if isEscapeSS3 { 188 | isEscapeSS3 = false 189 | if key := readEscKey(r, buf); key != nil { 190 | r = escapeSS3Key(key) 191 | } 192 | if r == 0 { 193 | expectNextChar = true 194 | continue 195 | } 196 | } 197 | 198 | expectNextChar = true 199 | switch r { 200 | case CharEsc: 201 | if t.cfg.VimMode { 202 | t.outchan <- r 203 | break 204 | } 205 | isEscape = true 206 | case CharInterrupt, CharEnter, CharCtrlJ, CharDelete: 207 | expectNextChar = false 208 | fallthrough 209 | default: 210 | t.outchan <- r 211 | } 212 | } 213 | 214 | } 215 | 216 | func (t *Terminal) Bell() { 217 | fmt.Fprintf(t, "%c", CharBell) 218 | } 219 | 220 | func (t *Terminal) Close() error { 221 | if atomic.SwapInt32(&t.closed, 1) != 0 { 222 | return nil 223 | } 224 | if closer, ok := t.cfg.Stdin.(io.Closer); ok { 225 | closer.Close() 226 | } 227 | close(t.stopChan) 228 | t.wg.Wait() 229 | return t.ExitRawMode() 230 | } 231 | 232 | func (t *Terminal) GetConfig() *Config { 233 | t.m.Lock() 234 | cfg := *t.cfg 235 | t.m.Unlock() 236 | return &cfg 237 | } 238 | 239 | func (t *Terminal) getStdin() io.Reader { 240 | t.m.Lock() 241 | r := t.cfg.Stdin 242 | t.m.Unlock() 243 | return r 244 | } 245 | 246 | func (t *Terminal) SetConfig(c *Config) error { 247 | if err := c.Init(); err != nil { 248 | return err 249 | } 250 | t.m.Lock() 251 | t.cfg = c 252 | t.m.Unlock() 253 | return nil 254 | } 255 | -------------------------------------------------------------------------------- /utils.go: -------------------------------------------------------------------------------- 1 | package readline 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "container/list" 7 | "fmt" 8 | "os" 9 | "os/signal" 10 | "strconv" 11 | "strings" 12 | "sync" 13 | "syscall" 14 | "time" 15 | "unicode" 16 | ) 17 | 18 | var ( 19 | isWindows = false 20 | ) 21 | 22 | const ( 23 | CharLineStart = 1 24 | CharBackward = 2 25 | CharInterrupt = 3 26 | CharDelete = 4 27 | CharLineEnd = 5 28 | CharForward = 6 29 | CharBell = 7 30 | CharCtrlH = 8 31 | CharTab = 9 32 | CharCtrlJ = 10 33 | CharKill = 11 34 | CharCtrlL = 12 35 | CharEnter = 13 36 | CharNext = 14 37 | CharPrev = 16 38 | CharBckSearch = 18 39 | CharFwdSearch = 19 40 | CharTranspose = 20 41 | CharCtrlU = 21 42 | CharCtrlW = 23 43 | CharCtrlY = 25 44 | CharCtrlZ = 26 45 | CharEsc = 27 46 | CharO = 79 47 | CharEscapeEx = 91 48 | CharBackspace = 127 49 | ) 50 | 51 | const ( 52 | MetaBackward rune = -iota - 1 53 | MetaForward 54 | MetaDelete 55 | MetaBackspace 56 | MetaTranspose 57 | ) 58 | 59 | // WaitForResume need to call before current process got suspend. 60 | // It will run a ticker until a long duration is occurs, 61 | // which means this process is resumed. 62 | func WaitForResume() chan struct{} { 63 | ch := make(chan struct{}) 64 | var wg sync.WaitGroup 65 | wg.Add(1) 66 | go func() { 67 | ticker := time.NewTicker(10 * time.Millisecond) 68 | t := time.Now() 69 | wg.Done() 70 | for { 71 | now := <-ticker.C 72 | if now.Sub(t) > 100*time.Millisecond { 73 | break 74 | } 75 | t = now 76 | } 77 | ticker.Stop() 78 | ch <- struct{}{} 79 | }() 80 | wg.Wait() 81 | return ch 82 | } 83 | 84 | func Restore(fd int, state *State) error { 85 | err := restoreTerm(fd, state) 86 | if err != nil { 87 | // errno 0 means everything is ok :) 88 | if err.Error() == "errno 0" { 89 | return nil 90 | } else { 91 | return err 92 | } 93 | } 94 | return nil 95 | } 96 | 97 | func IsPrintable(key rune) bool { 98 | isInSurrogateArea := key >= 0xd800 && key <= 0xdbff 99 | return key >= 32 && !isInSurrogateArea 100 | } 101 | 102 | // translate Esc[X 103 | func escapeExKey(key *escapeKeyPair) rune { 104 | var r rune 105 | switch key.typ { 106 | case 'D': 107 | r = CharBackward 108 | case 'C': 109 | r = CharForward 110 | case 'A': 111 | r = CharPrev 112 | case 'B': 113 | r = CharNext 114 | case 'H': 115 | r = CharLineStart 116 | case 'F': 117 | r = CharLineEnd 118 | case '~': 119 | if key.attr == "3" { 120 | r = CharDelete 121 | } 122 | default: 123 | } 124 | return r 125 | } 126 | 127 | // translate EscOX SS3 codes for up/down/etc. 128 | func escapeSS3Key(key *escapeKeyPair) rune { 129 | var r rune 130 | switch key.typ { 131 | case 'D': 132 | r = CharBackward 133 | case 'C': 134 | r = CharForward 135 | case 'A': 136 | r = CharPrev 137 | case 'B': 138 | r = CharNext 139 | case 'H': 140 | r = CharLineStart 141 | case 'F': 142 | r = CharLineEnd 143 | default: 144 | } 145 | return r 146 | } 147 | 148 | type escapeKeyPair struct { 149 | attr string 150 | typ rune 151 | } 152 | 153 | func (e *escapeKeyPair) Get2() (int, int, bool) { 154 | sp := strings.Split(e.attr, ";") 155 | if len(sp) < 2 { 156 | return -1, -1, false 157 | } 158 | s1, err := strconv.Atoi(sp[0]) 159 | if err != nil { 160 | return -1, -1, false 161 | } 162 | s2, err := strconv.Atoi(sp[1]) 163 | if err != nil { 164 | return -1, -1, false 165 | } 166 | return s1, s2, true 167 | } 168 | 169 | func readEscKey(r rune, reader *bufio.Reader) *escapeKeyPair { 170 | p := escapeKeyPair{} 171 | buf := bytes.NewBuffer(nil) 172 | for { 173 | if r == ';' { 174 | } else if unicode.IsNumber(r) { 175 | } else { 176 | p.typ = r 177 | break 178 | } 179 | buf.WriteRune(r) 180 | r, _, _ = reader.ReadRune() 181 | } 182 | p.attr = buf.String() 183 | return &p 184 | } 185 | 186 | // translate EscX to Meta+X 187 | func escapeKey(r rune, reader *bufio.Reader) rune { 188 | switch r { 189 | case 'b': 190 | r = MetaBackward 191 | case 'f': 192 | r = MetaForward 193 | case 'd': 194 | r = MetaDelete 195 | case CharTranspose: 196 | r = MetaTranspose 197 | case CharBackspace: 198 | r = MetaBackspace 199 | case 'O': 200 | d, _, _ := reader.ReadRune() 201 | switch d { 202 | case 'H': 203 | r = CharLineStart 204 | case 'F': 205 | r = CharLineEnd 206 | default: 207 | reader.UnreadRune() 208 | } 209 | case CharEsc: 210 | 211 | } 212 | return r 213 | } 214 | 215 | func SplitByLine(start, screenWidth int, rs []rune) []string { 216 | var ret []string 217 | buf := bytes.NewBuffer(nil) 218 | currentWidth := start 219 | for _, r := range rs { 220 | w := runes.Width(r) 221 | currentWidth += w 222 | buf.WriteRune(r) 223 | if currentWidth >= screenWidth { 224 | ret = append(ret, buf.String()) 225 | buf.Reset() 226 | currentWidth = 0 227 | } 228 | } 229 | ret = append(ret, buf.String()) 230 | return ret 231 | } 232 | 233 | // calculate how many lines for N character 234 | func LineCount(screenWidth, w int) int { 235 | r := w / screenWidth 236 | if w%screenWidth != 0 { 237 | r++ 238 | } 239 | return r 240 | } 241 | 242 | func IsWordBreak(i rune) bool { 243 | switch { 244 | case i >= 'a' && i <= 'z': 245 | case i >= 'A' && i <= 'Z': 246 | case i >= '0' && i <= '9': 247 | default: 248 | return true 249 | } 250 | return false 251 | } 252 | 253 | func GetInt(s []string, def int) int { 254 | if len(s) == 0 { 255 | return def 256 | } 257 | c, err := strconv.Atoi(s[0]) 258 | if err != nil { 259 | return def 260 | } 261 | return c 262 | } 263 | 264 | type RawMode struct { 265 | state *State 266 | } 267 | 268 | func (r *RawMode) Enter() (err error) { 269 | r.state, err = MakeRaw(GetStdin()) 270 | return err 271 | } 272 | 273 | func (r *RawMode) Exit() error { 274 | if r.state == nil { 275 | return nil 276 | } 277 | return Restore(GetStdin(), r.state) 278 | } 279 | 280 | // ----------------------------------------------------------------------------- 281 | 282 | func sleep(n int) { 283 | Debug(n) 284 | time.Sleep(2000 * time.Millisecond) 285 | } 286 | 287 | // print a linked list to Debug() 288 | func debugList(l *list.List) { 289 | idx := 0 290 | for e := l.Front(); e != nil; e = e.Next() { 291 | Debug(idx, fmt.Sprintf("%+v", e.Value)) 292 | idx++ 293 | } 294 | } 295 | 296 | // append log info to another file 297 | func Debug(o ...interface{}) { 298 | f, _ := os.OpenFile("debug.tmp", os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666) 299 | fmt.Fprintln(f, o...) 300 | f.Close() 301 | } 302 | 303 | func CaptureExitSignal(f func()) { 304 | cSignal := make(chan os.Signal, 1) 305 | signal.Notify(cSignal, os.Interrupt, syscall.SIGTERM) 306 | go func() { 307 | for range cSignal { 308 | f() 309 | } 310 | }() 311 | } 312 | -------------------------------------------------------------------------------- /utils_test.go: -------------------------------------------------------------------------------- 1 | package readline 2 | -------------------------------------------------------------------------------- /utils_unix.go: -------------------------------------------------------------------------------- 1 | // +build aix darwin dragonfly freebsd linux,!appengine netbsd openbsd os400 solaris 2 | 3 | package readline 4 | 5 | import ( 6 | "io" 7 | "os" 8 | "os/signal" 9 | "sync" 10 | "syscall" 11 | ) 12 | 13 | type winsize struct { 14 | Row uint16 15 | Col uint16 16 | Xpixel uint16 17 | Ypixel uint16 18 | } 19 | 20 | // SuspendMe use to send suspend signal to myself, when we in the raw mode. 21 | // For OSX it need to send to parent's pid 22 | // For Linux it need to send to myself 23 | func SuspendMe() { 24 | p, _ := os.FindProcess(os.Getppid()) 25 | p.Signal(syscall.SIGTSTP) 26 | p, _ = os.FindProcess(os.Getpid()) 27 | p.Signal(syscall.SIGTSTP) 28 | } 29 | 30 | // get width of the terminal 31 | func getWidth(stdoutFd int) int { 32 | cols, _, err := GetSize(stdoutFd) 33 | if err != nil { 34 | return -1 35 | } 36 | return cols 37 | } 38 | 39 | func GetScreenWidth() int { 40 | w := getWidth(syscall.Stdout) 41 | if w < 0 { 42 | w = getWidth(syscall.Stderr) 43 | } 44 | return w 45 | } 46 | 47 | // ClearScreen clears the console screen 48 | func ClearScreen(w io.Writer) (int, error) { 49 | return w.Write([]byte("\033[H")) 50 | } 51 | 52 | func DefaultIsTerminal() bool { 53 | return IsTerminal(syscall.Stdin) && (IsTerminal(syscall.Stdout) || IsTerminal(syscall.Stderr)) 54 | } 55 | 56 | func GetStdin() int { 57 | return syscall.Stdin 58 | } 59 | 60 | // ----------------------------------------------------------------------------- 61 | 62 | var ( 63 | widthChange sync.Once 64 | widthChangeCallback func() 65 | ) 66 | 67 | func DefaultOnWidthChanged(f func()) { 68 | widthChangeCallback = f 69 | widthChange.Do(func() { 70 | ch := make(chan os.Signal, 1) 71 | signal.Notify(ch, syscall.SIGWINCH) 72 | 73 | go func() { 74 | for { 75 | _, ok := <-ch 76 | if !ok { 77 | break 78 | } 79 | widthChangeCallback() 80 | } 81 | }() 82 | }) 83 | } 84 | -------------------------------------------------------------------------------- /utils_windows.go: -------------------------------------------------------------------------------- 1 | // +build windows 2 | 3 | package readline 4 | 5 | import ( 6 | "io" 7 | "syscall" 8 | ) 9 | 10 | func SuspendMe() { 11 | } 12 | 13 | func GetStdin() int { 14 | return int(syscall.Stdin) 15 | } 16 | 17 | func init() { 18 | isWindows = true 19 | } 20 | 21 | // get width of the terminal 22 | func GetScreenWidth() int { 23 | info, _ := GetConsoleScreenBufferInfo() 24 | if info == nil { 25 | return -1 26 | } 27 | return int(info.dwSize.x) 28 | } 29 | 30 | // ClearScreen clears the console screen 31 | func ClearScreen(_ io.Writer) error { 32 | return SetConsoleCursorPosition(&_COORD{0, 0}) 33 | } 34 | 35 | func DefaultIsTerminal() bool { 36 | return true 37 | } 38 | 39 | func DefaultOnWidthChanged(func()) { 40 | 41 | } 42 | -------------------------------------------------------------------------------- /vim.go: -------------------------------------------------------------------------------- 1 | package readline 2 | 3 | const ( 4 | VIM_NORMAL = iota 5 | VIM_INSERT 6 | VIM_VISUAL 7 | ) 8 | 9 | type opVim struct { 10 | cfg *Config 11 | op *Operation 12 | vimMode int 13 | } 14 | 15 | func newVimMode(op *Operation) *opVim { 16 | ov := &opVim{ 17 | cfg: op.cfg, 18 | op: op, 19 | } 20 | ov.SetVimMode(ov.cfg.VimMode) 21 | return ov 22 | } 23 | 24 | func (o *opVim) SetVimMode(on bool) { 25 | if o.cfg.VimMode && !on { // turn off 26 | o.ExitVimMode() 27 | } 28 | o.cfg.VimMode = on 29 | o.vimMode = VIM_INSERT 30 | } 31 | 32 | func (o *opVim) ExitVimMode() { 33 | o.vimMode = VIM_INSERT 34 | } 35 | 36 | func (o *opVim) IsEnableVimMode() bool { 37 | return o.cfg.VimMode 38 | } 39 | 40 | func (o *opVim) handleVimNormalMovement(r rune, readNext func() rune) (t rune, handled bool) { 41 | rb := o.op.buf 42 | handled = true 43 | switch r { 44 | case 'h': 45 | t = CharBackward 46 | case 'j': 47 | t = CharNext 48 | case 'k': 49 | t = CharPrev 50 | case 'l': 51 | t = CharForward 52 | case '0', '^': 53 | rb.MoveToLineStart() 54 | case '$': 55 | rb.MoveToLineEnd() 56 | case 'x': 57 | rb.Delete() 58 | if rb.IsCursorInEnd() { 59 | rb.MoveBackward() 60 | } 61 | case 'r': 62 | rb.Replace(readNext()) 63 | case 'd': 64 | next := readNext() 65 | switch next { 66 | case 'd': 67 | rb.Erase() 68 | case 'w': 69 | rb.DeleteWord() 70 | case 'h': 71 | rb.Backspace() 72 | case 'l': 73 | rb.Delete() 74 | } 75 | case 'p': 76 | rb.Yank() 77 | case 'b', 'B': 78 | rb.MoveToPrevWord() 79 | case 'w', 'W': 80 | rb.MoveToNextWord() 81 | case 'e', 'E': 82 | rb.MoveToEndWord() 83 | case 'f', 'F', 't', 'T': 84 | next := readNext() 85 | prevChar := r == 't' || r == 'T' 86 | reverse := r == 'F' || r == 'T' 87 | switch next { 88 | case CharEsc: 89 | default: 90 | rb.MoveTo(next, prevChar, reverse) 91 | } 92 | default: 93 | return r, false 94 | } 95 | return t, true 96 | } 97 | 98 | func (o *opVim) handleVimNormalEnterInsert(r rune, readNext func() rune) (t rune, handled bool) { 99 | rb := o.op.buf 100 | handled = true 101 | switch r { 102 | case 'i': 103 | case 'I': 104 | rb.MoveToLineStart() 105 | case 'a': 106 | rb.MoveForward() 107 | case 'A': 108 | rb.MoveToLineEnd() 109 | case 's': 110 | rb.Delete() 111 | case 'S': 112 | rb.Erase() 113 | case 'c': 114 | next := readNext() 115 | switch next { 116 | case 'c': 117 | rb.Erase() 118 | case 'w': 119 | rb.DeleteWord() 120 | case 'h': 121 | rb.Backspace() 122 | case 'l': 123 | rb.Delete() 124 | } 125 | default: 126 | return r, false 127 | } 128 | 129 | o.EnterVimInsertMode() 130 | return 131 | } 132 | 133 | func (o *opVim) HandleVimNormal(r rune, readNext func() rune) (t rune) { 134 | switch r { 135 | case CharEnter, CharInterrupt: 136 | o.ExitVimMode() 137 | return r 138 | } 139 | 140 | if r, handled := o.handleVimNormalMovement(r, readNext); handled { 141 | return r 142 | } 143 | 144 | if r, handled := o.handleVimNormalEnterInsert(r, readNext); handled { 145 | return r 146 | } 147 | 148 | // invalid operation 149 | o.op.t.Bell() 150 | return 0 151 | } 152 | 153 | func (o *opVim) EnterVimInsertMode() { 154 | o.vimMode = VIM_INSERT 155 | } 156 | 157 | func (o *opVim) ExitVimInsertMode() { 158 | o.vimMode = VIM_NORMAL 159 | } 160 | 161 | func (o *opVim) HandleVim(r rune, readNext func() rune) rune { 162 | if o.vimMode == VIM_NORMAL { 163 | return o.HandleVimNormal(r, readNext) 164 | } 165 | if r == CharEsc { 166 | o.ExitVimInsertMode() 167 | return 0 168 | } 169 | 170 | switch o.vimMode { 171 | case VIM_INSERT: 172 | return r 173 | case VIM_VISUAL: 174 | } 175 | return r 176 | } 177 | -------------------------------------------------------------------------------- /windows_api.go: -------------------------------------------------------------------------------- 1 | // +build windows 2 | 3 | package readline 4 | 5 | import ( 6 | "reflect" 7 | "syscall" 8 | "unsafe" 9 | ) 10 | 11 | var ( 12 | kernel = NewKernel() 13 | stdout = uintptr(syscall.Stdout) 14 | stdin = uintptr(syscall.Stdin) 15 | ) 16 | 17 | type Kernel struct { 18 | SetConsoleCursorPosition, 19 | SetConsoleTextAttribute, 20 | FillConsoleOutputCharacterW, 21 | FillConsoleOutputAttribute, 22 | ReadConsoleInputW, 23 | GetConsoleScreenBufferInfo, 24 | GetConsoleCursorInfo, 25 | GetStdHandle CallFunc 26 | } 27 | 28 | type short int16 29 | type word uint16 30 | type dword uint32 31 | type wchar uint16 32 | 33 | type _COORD struct { 34 | x short 35 | y short 36 | } 37 | 38 | func (c *_COORD) ptr() uintptr { 39 | return uintptr(*(*int32)(unsafe.Pointer(c))) 40 | } 41 | 42 | const ( 43 | EVENT_KEY = 0x0001 44 | EVENT_MOUSE = 0x0002 45 | EVENT_WINDOW_BUFFER_SIZE = 0x0004 46 | EVENT_MENU = 0x0008 47 | EVENT_FOCUS = 0x0010 48 | ) 49 | 50 | type _KEY_EVENT_RECORD struct { 51 | bKeyDown int32 52 | wRepeatCount word 53 | wVirtualKeyCode word 54 | wVirtualScanCode word 55 | unicodeChar wchar 56 | dwControlKeyState dword 57 | } 58 | 59 | // KEY_EVENT_RECORD KeyEvent; 60 | // MOUSE_EVENT_RECORD MouseEvent; 61 | // WINDOW_BUFFER_SIZE_RECORD WindowBufferSizeEvent; 62 | // MENU_EVENT_RECORD MenuEvent; 63 | // FOCUS_EVENT_RECORD FocusEvent; 64 | type _INPUT_RECORD struct { 65 | EventType word 66 | Padding uint16 67 | Event [16]byte 68 | } 69 | 70 | type _CONSOLE_SCREEN_BUFFER_INFO struct { 71 | dwSize _COORD 72 | dwCursorPosition _COORD 73 | wAttributes word 74 | srWindow _SMALL_RECT 75 | dwMaximumWindowSize _COORD 76 | } 77 | 78 | type _SMALL_RECT struct { 79 | left short 80 | top short 81 | right short 82 | bottom short 83 | } 84 | 85 | type _CONSOLE_CURSOR_INFO struct { 86 | dwSize dword 87 | bVisible bool 88 | } 89 | 90 | type CallFunc func(u ...uintptr) error 91 | 92 | func NewKernel() *Kernel { 93 | k := &Kernel{} 94 | kernel32 := syscall.NewLazyDLL("kernel32.dll") 95 | v := reflect.ValueOf(k).Elem() 96 | t := v.Type() 97 | for i := 0; i < t.NumField(); i++ { 98 | name := t.Field(i).Name 99 | f := kernel32.NewProc(name) 100 | v.Field(i).Set(reflect.ValueOf(k.Wrap(f))) 101 | } 102 | return k 103 | } 104 | 105 | func (k *Kernel) Wrap(p *syscall.LazyProc) CallFunc { 106 | return func(args ...uintptr) error { 107 | var r0 uintptr 108 | var e1 syscall.Errno 109 | size := uintptr(len(args)) 110 | if len(args) <= 3 { 111 | buf := make([]uintptr, 3) 112 | copy(buf, args) 113 | r0, _, e1 = syscall.Syscall(p.Addr(), size, 114 | buf[0], buf[1], buf[2]) 115 | } else { 116 | buf := make([]uintptr, 6) 117 | copy(buf, args) 118 | r0, _, e1 = syscall.Syscall6(p.Addr(), size, 119 | buf[0], buf[1], buf[2], buf[3], buf[4], buf[5], 120 | ) 121 | } 122 | 123 | if int(r0) == 0 { 124 | if e1 != 0 { 125 | return error(e1) 126 | } else { 127 | return syscall.EINVAL 128 | } 129 | } 130 | return nil 131 | } 132 | 133 | } 134 | 135 | func GetConsoleScreenBufferInfo() (*_CONSOLE_SCREEN_BUFFER_INFO, error) { 136 | t := new(_CONSOLE_SCREEN_BUFFER_INFO) 137 | err := kernel.GetConsoleScreenBufferInfo( 138 | stdout, 139 | uintptr(unsafe.Pointer(t)), 140 | ) 141 | return t, err 142 | } 143 | 144 | func GetConsoleCursorInfo() (*_CONSOLE_CURSOR_INFO, error) { 145 | t := new(_CONSOLE_CURSOR_INFO) 146 | err := kernel.GetConsoleCursorInfo(stdout, uintptr(unsafe.Pointer(t))) 147 | return t, err 148 | } 149 | 150 | func SetConsoleCursorPosition(c *_COORD) error { 151 | return kernel.SetConsoleCursorPosition(stdout, c.ptr()) 152 | } 153 | --------------------------------------------------------------------------------