├── .github ├── CONTRIBUTING.md └── ISSUE_TEMPLATE.md ├── COPYING ├── README.md ├── bsdinput.go ├── common.go ├── fallbackinput.go ├── go.mod ├── go.sum ├── input.go ├── input_darwin.go ├── input_linux.go ├── input_solaris.go ├── input_test.go ├── input_windows.go ├── line.go ├── line_test.go ├── output.go ├── output_solaris.go ├── output_unix.go ├── output_windows.go ├── prefix_test.go ├── race_test.go ├── unixmode.go ├── unixmode_solaris.go ├── width.go └── width_test.go /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | #### Liner is a scratch-your-own-itch project 2 | 3 | While I try my best to fix any bugs encountered in liner, I do not have 4 | sufficient time to implement feature requests on your behalf. 5 | 6 | If you are opening a feature request, you are implicitly volunteering to 7 | implement that feature. Obvious feature requests should be made via a pull 8 | request. Complex feature requests will be interpreted as a 9 | request-for-comments, and will be closed once comments are given. 10 | 11 | #### Liner must remain backwards compatible 12 | 13 | The API of Liner must not change in an incompatible way. When making 14 | changes to liner, please use the [Go 1 Compatibility Promise](https://golang.org/doc/go1compat) 15 | as a guideline. 16 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | If you have a feature request, please see the Contribution Guidelines before 2 | proceeding. 3 | 4 | If you have a bug report, please supply the following information: 5 | 6 | - Operating System (eg. Windows, Linux, Mac) 7 | - Terminal Emulator (eg. xterm, gnome-terminal, konsole, ConEmu, Terminal.app, Command Prompt) 8 | - Bug behaviour 9 | - Expected behaviour 10 | - Complete sample that reproduces the bug 11 | -------------------------------------------------------------------------------- /COPYING: -------------------------------------------------------------------------------- 1 | Copyright © 2012 Peter Harris 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a 4 | copy of this software and associated documentation files (the "Software"), 5 | to deal in the Software without restriction, including without limitation 6 | the rights to use, copy, modify, merge, publish, distribute, sublicense, 7 | and/or sell copies of the Software, and to permit persons to whom the 8 | Software is furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice (including the next 11 | paragraph) shall be included in all copies or substantial portions of the 12 | Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 17 | THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 19 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 20 | DEALINGS IN THE SOFTWARE. 21 | 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Liner 2 | ===== 3 | 4 | Liner is a command line editor with history. It was inspired by linenoise; 5 | everything Unix-like is a VT100 (or is trying very hard to be). If your 6 | terminal is not pretending to be a VT100, change it. Liner also support 7 | Windows. 8 | 9 | Liner is intended for use by cross-platform applications. Therefore, the 10 | decision was made to write it in pure Go, avoiding cgo, for ease of cross 11 | compilation. Furthermore, features only supported on some platforms have 12 | been intentionally omitted. For example, Ctrl-Z is "suspend" on Unix, but 13 | "EOF" on Windows. In the interest of making an application behave the same 14 | way on every supported platform, Ctrl-Z is ignored by Liner. 15 | 16 | Liner is released under the X11 license (which is similar to the new BSD 17 | license). 18 | 19 | Line Editing 20 | ------------ 21 | 22 | The following line editing commands are supported on platforms and terminals 23 | that Liner supports: 24 | 25 | Keystroke | Action 26 | --------- | ------ 27 | Ctrl-A, Home | Move cursor to beginning of line 28 | Ctrl-E, End | Move cursor to end of line 29 | Ctrl-B, Left | Move cursor one character left 30 | Ctrl-F, Right| Move cursor one character right 31 | Ctrl-Left, Alt-B | Move cursor to previous word 32 | Ctrl-Right, Alt-F | Move cursor to next word 33 | Ctrl-D, Del | (if line is *not* empty) Delete character under cursor 34 | Ctrl-D | (if line *is* empty) End of File - usually quits application 35 | Ctrl-C | Reset input (create new empty prompt) 36 | Ctrl-L | Clear screen (line is unmodified) 37 | Ctrl-T | Transpose previous character with current character 38 | Ctrl-H, BackSpace | Delete character before cursor 39 | Ctrl-W, Alt-BackSpace | Delete word leading up to cursor 40 | Alt-D | Delete word following cursor 41 | Ctrl-K | Delete from cursor to end of line 42 | Ctrl-U | Delete from start of line to cursor 43 | Ctrl-P, Up | Previous match from history 44 | Ctrl-N, Down | Next match from history 45 | Ctrl-R | Reverse Search history (Ctrl-S forward, Ctrl-G cancel) 46 | Ctrl-Y | Paste from Yank buffer (Alt-Y to paste next yank instead) 47 | Tab | Next completion 48 | Shift-Tab | (after Tab) Previous completion 49 | 50 | Note that "Previous" and "Next match from history" will retain the part of 51 | the line that the user has already typed, similar to zsh's 52 | "up-line-or-beginning-search" (which is the default on some systems) or 53 | bash's "history-search-backward" (which is my preferred behaviour, but does 54 | not appear to be the default `Up` keybinding on any system). 55 | 56 | Getting started 57 | ----------------- 58 | 59 | ```go 60 | package main 61 | 62 | import ( 63 | "log" 64 | "os" 65 | "path/filepath" 66 | "strings" 67 | 68 | "github.com/peterh/liner" 69 | ) 70 | 71 | var ( 72 | history_fn = filepath.Join(os.TempDir(), ".liner_example_history") 73 | names = []string{"john", "james", "mary", "nancy"} 74 | ) 75 | 76 | func main() { 77 | line := liner.NewLiner() 78 | defer line.Close() 79 | 80 | line.SetCtrlCAborts(true) 81 | 82 | line.SetCompleter(func(line string) (c []string) { 83 | for _, n := range names { 84 | if strings.HasPrefix(n, strings.ToLower(line)) { 85 | c = append(c, n) 86 | } 87 | } 88 | return 89 | }) 90 | 91 | if f, err := os.Open(history_fn); err == nil { 92 | line.ReadHistory(f) 93 | f.Close() 94 | } 95 | 96 | if name, err := line.Prompt("What is your name? "); err == nil { 97 | log.Print("Got: ", name) 98 | line.AppendHistory(name) 99 | } else if err == liner.ErrPromptAborted { 100 | log.Print("Aborted") 101 | } else { 102 | log.Print("Error reading line: ", err) 103 | } 104 | 105 | if f, err := os.Create(history_fn); err != nil { 106 | log.Print("Error writing history file: ", err) 107 | } else { 108 | line.WriteHistory(f) 109 | f.Close() 110 | } 111 | } 112 | ``` 113 | 114 | For documentation, see http://godoc.org/github.com/peterh/liner 115 | -------------------------------------------------------------------------------- /bsdinput.go: -------------------------------------------------------------------------------- 1 | //go:build openbsd || freebsd || netbsd 2 | // +build openbsd freebsd netbsd 3 | 4 | package liner 5 | 6 | import "syscall" 7 | 8 | const ( 9 | getTermios = syscall.TIOCGETA 10 | setTermios = syscall.TIOCSETA 11 | ) 12 | 13 | const ( 14 | // Input flags 15 | inpck = 0x010 16 | istrip = 0x020 17 | icrnl = 0x100 18 | ixon = 0x200 19 | 20 | // Output flags 21 | opost = 0x1 22 | 23 | // Control flags 24 | cs8 = 0x300 25 | 26 | // Local flags 27 | isig = 0x080 28 | icanon = 0x100 29 | iexten = 0x400 30 | ) 31 | 32 | type termios struct { 33 | Iflag uint32 34 | Oflag uint32 35 | Cflag uint32 36 | Lflag uint32 37 | Cc [20]byte 38 | Ispeed int32 39 | Ospeed int32 40 | } 41 | 42 | const cursorColumn = false 43 | -------------------------------------------------------------------------------- /common.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package liner implements a simple command line editor, inspired by linenoise 3 | (https://github.com/antirez/linenoise/). This package supports WIN32 in 4 | addition to the xterm codes supported by everything else. 5 | */ 6 | package liner 7 | 8 | import ( 9 | "bufio" 10 | "container/ring" 11 | "errors" 12 | "fmt" 13 | "io" 14 | "strings" 15 | "sync" 16 | "unicode/utf8" 17 | ) 18 | 19 | type commonState struct { 20 | terminalSupported bool 21 | outputRedirected bool 22 | inputRedirected bool 23 | history []string 24 | historyMutex sync.RWMutex 25 | completer WordCompleter 26 | columns int 27 | killRing *ring.Ring 28 | ctrlCAborts bool 29 | r *bufio.Reader 30 | tabStyle TabStyle 31 | multiLineMode bool 32 | cursorRows int 33 | maxRows int 34 | shouldRestart ShouldRestart 35 | noBeep bool 36 | needRefresh bool 37 | } 38 | 39 | // TabStyle is used to select how tab completions are displayed. 40 | type TabStyle int 41 | 42 | // Two tab styles are currently available: 43 | // 44 | // TabCircular cycles through each completion item and displays it directly on 45 | // the prompt 46 | // 47 | // TabPrints prints the list of completion items to the screen after a second 48 | // tab key is pressed. This behaves similar to GNU readline and BASH (which 49 | // uses readline) 50 | const ( 51 | TabCircular TabStyle = iota 52 | TabPrints 53 | ) 54 | 55 | // ErrPromptAborted is returned from Prompt or PasswordPrompt when the user presses Ctrl-C 56 | // if SetCtrlCAborts(true) has been called on the State 57 | var ErrPromptAborted = errors.New("prompt aborted") 58 | 59 | // ErrNotTerminalOutput is returned from Prompt or PasswordPrompt if the 60 | // platform is normally supported, but stdout has been redirected 61 | var ErrNotTerminalOutput = errors.New("standard output is not a terminal") 62 | 63 | // ErrInvalidPrompt is returned from Prompt or PasswordPrompt if the 64 | // prompt contains any unprintable runes (including substrings that could 65 | // be colour codes on some platforms). 66 | var ErrInvalidPrompt = errors.New("invalid prompt") 67 | 68 | // ErrInternal is returned when liner experiences an error that it cannot 69 | // handle. For example, if the number of colums becomes zero during an 70 | // active call to Prompt 71 | var ErrInternal = errors.New("liner: internal error") 72 | 73 | // KillRingMax is the max number of elements to save on the killring. 74 | const KillRingMax = 60 75 | 76 | // HistoryLimit is the maximum number of entries saved in the scrollback history. 77 | const HistoryLimit = 1000 78 | 79 | // ReadHistory reads scrollback history from r. Returns the number of lines 80 | // read, and any read error (except io.EOF). 81 | func (s *State) ReadHistory(r io.Reader) (num int, err error) { 82 | s.historyMutex.Lock() 83 | defer s.historyMutex.Unlock() 84 | 85 | in := bufio.NewReader(r) 86 | num = 0 87 | for { 88 | line, part, err := in.ReadLine() 89 | if err == io.EOF { 90 | break 91 | } 92 | if err != nil { 93 | return num, err 94 | } 95 | if part { 96 | return num, fmt.Errorf("line %d is too long", num+1) 97 | } 98 | if !utf8.Valid(line) { 99 | return num, fmt.Errorf("invalid string at line %d", num+1) 100 | } 101 | num++ 102 | s.history = append(s.history, string(line)) 103 | if len(s.history) > HistoryLimit { 104 | s.history = s.history[1:] 105 | } 106 | } 107 | return num, nil 108 | } 109 | 110 | // WriteHistory writes scrollback history to w. Returns the number of lines 111 | // successfully written, and any write error. 112 | // 113 | // Unlike the rest of liner's API, WriteHistory is safe to call 114 | // from another goroutine while Prompt is in progress. 115 | // This exception is to facilitate the saving of the history buffer 116 | // during an unexpected exit (for example, due to Ctrl-C being invoked) 117 | func (s *State) WriteHistory(w io.Writer) (num int, err error) { 118 | s.historyMutex.RLock() 119 | defer s.historyMutex.RUnlock() 120 | 121 | for _, item := range s.history { 122 | _, err := fmt.Fprintln(w, item) 123 | if err != nil { 124 | return num, err 125 | } 126 | num++ 127 | } 128 | return num, nil 129 | } 130 | 131 | // AppendHistory appends an entry to the scrollback history. AppendHistory 132 | // should be called iff Prompt returns a valid command. 133 | func (s *State) AppendHistory(item string) { 134 | s.historyMutex.Lock() 135 | defer s.historyMutex.Unlock() 136 | 137 | if len(s.history) > 0 { 138 | if item == s.history[len(s.history)-1] { 139 | return 140 | } 141 | } 142 | s.history = append(s.history, item) 143 | if len(s.history) > HistoryLimit { 144 | s.history = s.history[1:] 145 | } 146 | } 147 | 148 | // ClearHistory clears the scrollback history. 149 | func (s *State) ClearHistory() { 150 | s.historyMutex.Lock() 151 | defer s.historyMutex.Unlock() 152 | s.history = nil 153 | } 154 | 155 | // Returns the history lines starting with prefix 156 | func (s *State) getHistoryByPrefix(prefix string) (ph []string) { 157 | for _, h := range s.history { 158 | if strings.HasPrefix(h, prefix) { 159 | ph = append(ph, h) 160 | } 161 | } 162 | return 163 | } 164 | 165 | // Returns the history lines matching the intelligent search 166 | func (s *State) getHistoryByPattern(pattern string) (ph []string, pos []int) { 167 | if pattern == "" { 168 | return 169 | } 170 | for _, h := range s.history { 171 | if i := strings.Index(h, pattern); i >= 0 { 172 | ph = append(ph, h) 173 | pos = append(pos, i) 174 | } 175 | } 176 | return 177 | } 178 | 179 | // Completer takes the currently edited line content at the left of the cursor 180 | // and returns a list of completion candidates. 181 | // If the line is "Hello, wo!!!" and the cursor is before the first '!', "Hello, wo" is passed 182 | // to the completer which may return {"Hello, world", "Hello, Word"} to have "Hello, world!!!". 183 | type Completer func(line string) []string 184 | 185 | // WordCompleter takes the currently edited line with the cursor position and 186 | // returns the completion candidates for the partial word to be completed. 187 | // If the line is "Hello, wo!!!" and the cursor is before the first '!', ("Hello, wo!!!", 9) is passed 188 | // to the completer which may returns ("Hello, ", {"world", "Word"}, "!!!") to have "Hello, world!!!". 189 | type WordCompleter func(line string, pos int) (head string, completions []string, tail string) 190 | 191 | // SetCompleter sets the completion function that Liner will call to 192 | // fetch completion candidates when the user presses tab. 193 | func (s *State) SetCompleter(f Completer) { 194 | if f == nil { 195 | s.completer = nil 196 | return 197 | } 198 | s.completer = func(line string, pos int) (string, []string, string) { 199 | return "", f(string([]rune(line)[:pos])), string([]rune(line)[pos:]) 200 | } 201 | } 202 | 203 | // SetWordCompleter sets the completion function that Liner will call to 204 | // fetch completion candidates when the user presses tab. 205 | func (s *State) SetWordCompleter(f WordCompleter) { 206 | s.completer = f 207 | } 208 | 209 | // SetTabCompletionStyle sets the behvavior when the Tab key is pressed 210 | // for auto-completion. TabCircular is the default behavior and cycles 211 | // through the list of candidates at the prompt. TabPrints will print 212 | // the available completion candidates to the screen similar to BASH 213 | // and GNU Readline 214 | func (s *State) SetTabCompletionStyle(tabStyle TabStyle) { 215 | s.tabStyle = tabStyle 216 | } 217 | 218 | // ModeApplier is the interface that wraps a representation of the terminal 219 | // mode. ApplyMode sets the terminal to this mode. 220 | type ModeApplier interface { 221 | ApplyMode() error 222 | } 223 | 224 | // SetCtrlCAborts sets whether Prompt on a supported terminal will return an 225 | // ErrPromptAborted when Ctrl-C is pressed. The default is false (will not 226 | // return when Ctrl-C is pressed). Unsupported terminals typically raise SIGINT 227 | // (and Prompt does not return) regardless of the value passed to SetCtrlCAborts. 228 | func (s *State) SetCtrlCAborts(aborts bool) { 229 | s.ctrlCAborts = aborts 230 | } 231 | 232 | // SetMultiLineMode sets whether line is auto-wrapped. The default is false (single line). 233 | func (s *State) SetMultiLineMode(mlmode bool) { 234 | s.multiLineMode = mlmode 235 | } 236 | 237 | // ShouldRestart is passed the error generated by readNext and returns true if 238 | // the the read should be restarted or false if the error should be returned. 239 | type ShouldRestart func(err error) bool 240 | 241 | // SetShouldRestart sets the restart function that Liner will call to determine 242 | // whether to retry the call to, or return the error returned by, readNext. 243 | func (s *State) SetShouldRestart(f ShouldRestart) { 244 | s.shouldRestart = f 245 | } 246 | 247 | // SetBeep sets whether liner should beep the terminal at various times (output 248 | // ASCII BEL, 0x07). Default is true (will beep). 249 | func (s *State) SetBeep(beep bool) { 250 | s.noBeep = !beep 251 | } 252 | 253 | func (s *State) promptUnsupported(p string) (string, error) { 254 | if !s.inputRedirected || !s.terminalSupported { 255 | fmt.Print(p) 256 | } 257 | linebuf, _, err := s.r.ReadLine() 258 | if err != nil { 259 | return "", err 260 | } 261 | return string(linebuf), nil 262 | } 263 | -------------------------------------------------------------------------------- /fallbackinput.go: -------------------------------------------------------------------------------- 1 | //go:build !windows && !linux && !darwin && !openbsd && !freebsd && !netbsd && !solaris 2 | // +build !windows,!linux,!darwin,!openbsd,!freebsd,!netbsd,!solaris 3 | 4 | package liner 5 | 6 | import ( 7 | "bufio" 8 | "errors" 9 | "os" 10 | ) 11 | 12 | // State represents an open terminal 13 | type State struct { 14 | commonState 15 | } 16 | 17 | // Prompt displays p, and then waits for user input. Prompt does not support 18 | // line editing on this operating system. 19 | func (s *State) Prompt(p string) (string, error) { 20 | return s.promptUnsupported(p) 21 | } 22 | 23 | // PasswordPrompt is not supported in this OS. 24 | func (s *State) PasswordPrompt(p string) (string, error) { 25 | return "", errors.New("liner: function not supported in this terminal") 26 | } 27 | 28 | // NewLiner initializes a new *State 29 | // 30 | // Note that this operating system uses a fallback mode without line 31 | // editing. Patches welcome. 32 | func NewLiner() *State { 33 | var s State 34 | s.r = bufio.NewReader(os.Stdin) 35 | return &s 36 | } 37 | 38 | // Close returns the terminal to its previous mode 39 | func (s *State) Close() error { 40 | return nil 41 | } 42 | 43 | // TerminalSupported returns false because line editing is not 44 | // supported on this platform. 45 | func TerminalSupported() bool { 46 | return false 47 | } 48 | 49 | type noopMode struct{} 50 | 51 | func (n noopMode) ApplyMode() error { 52 | return nil 53 | } 54 | 55 | // TerminalMode returns a noop InputModeSetter on this platform. 56 | func TerminalMode() (ModeApplier, error) { 57 | return noopMode{}, nil 58 | } 59 | 60 | const cursorColumn = true 61 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/peterh/liner 2 | 3 | require ( 4 | github.com/mattn/go-runewidth v0.0.3 5 | golang.org/x/sys v0.0.0-20211117180635-dee7805ff2e1 6 | ) 7 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/mattn/go-runewidth v0.0.3 h1:a+kO+98RDGEfo6asOGMmpodZq4FNtnGP54yps8BzLR4= 2 | github.com/mattn/go-runewidth v0.0.3/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= 3 | golang.org/x/sys v0.0.0-20211117180635-dee7805ff2e1 h1:kwrAHlwJ0DUBZwQ238v+Uod/3eZ8B2K5rYsUHBQvzmI= 4 | golang.org/x/sys v0.0.0-20211117180635-dee7805ff2e1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 5 | -------------------------------------------------------------------------------- /input.go: -------------------------------------------------------------------------------- 1 | //go:build linux || darwin || openbsd || freebsd || netbsd || solaris 2 | // +build linux darwin openbsd freebsd netbsd solaris 3 | 4 | package liner 5 | 6 | import ( 7 | "bufio" 8 | "errors" 9 | "os" 10 | "os/signal" 11 | "strconv" 12 | "strings" 13 | "syscall" 14 | "time" 15 | ) 16 | 17 | type nexter struct { 18 | r rune 19 | err error 20 | } 21 | 22 | // State represents an open terminal 23 | type State struct { 24 | commonState 25 | origMode termios 26 | defaultMode termios 27 | next <-chan nexter 28 | winch chan os.Signal 29 | pending []rune 30 | useCHA bool 31 | } 32 | 33 | // NewLiner initializes a new *State, and sets the terminal into raw mode. To 34 | // restore the terminal to its previous state, call State.Close(). 35 | func NewLiner() *State { 36 | var s State 37 | s.r = bufio.NewReader(os.Stdin) 38 | 39 | s.terminalSupported = TerminalSupported() 40 | if m, err := TerminalMode(); err == nil { 41 | s.origMode = *m.(*termios) 42 | } else { 43 | s.inputRedirected = true 44 | } 45 | if _, err := getMode(syscall.Stdout); err != nil { 46 | s.outputRedirected = true 47 | } 48 | if s.inputRedirected && s.outputRedirected { 49 | s.terminalSupported = false 50 | } 51 | if s.terminalSupported && !s.inputRedirected && !s.outputRedirected { 52 | mode := s.origMode 53 | mode.Iflag &^= icrnl | inpck | istrip | ixon 54 | mode.Cflag |= cs8 55 | mode.Lflag &^= syscall.ECHO | icanon | iexten 56 | mode.Cc[syscall.VMIN] = 1 57 | mode.Cc[syscall.VTIME] = 0 58 | mode.ApplyMode() 59 | 60 | winch := make(chan os.Signal, 1) 61 | signal.Notify(winch, syscall.SIGWINCH) 62 | s.winch = winch 63 | 64 | s.checkOutput() 65 | } 66 | 67 | if !s.outputRedirected { 68 | s.outputRedirected = !s.getColumns() 69 | } 70 | 71 | return &s 72 | } 73 | 74 | var errTimedOut = errors.New("timeout") 75 | 76 | func (s *State) startPrompt() { 77 | if s.terminalSupported { 78 | if m, err := TerminalMode(); err == nil { 79 | s.defaultMode = *m.(*termios) 80 | mode := s.defaultMode 81 | mode.Lflag &^= isig 82 | mode.ApplyMode() 83 | } 84 | } 85 | s.restartPrompt() 86 | } 87 | 88 | func (s *State) inputWaiting() bool { 89 | return len(s.next) > 0 90 | } 91 | 92 | func (s *State) restartPrompt() { 93 | next := make(chan nexter, 200) 94 | go func() { 95 | for { 96 | var n nexter 97 | n.r, _, n.err = s.r.ReadRune() 98 | next <- n 99 | // Shut down nexter loop when an end condition has been reached 100 | if n.err != nil || n.r == '\n' || n.r == '\r' || n.r == ctrlC || n.r == ctrlD { 101 | close(next) 102 | return 103 | } 104 | } 105 | }() 106 | s.next = next 107 | } 108 | 109 | func (s *State) stopPrompt() { 110 | if s.terminalSupported { 111 | s.defaultMode.ApplyMode() 112 | } 113 | } 114 | 115 | func (s *State) nextPending(timeout <-chan time.Time) (rune, error) { 116 | select { 117 | case thing, ok := <-s.next: 118 | if !ok { 119 | return 0, ErrInternal 120 | } 121 | if thing.err != nil { 122 | return 0, thing.err 123 | } 124 | s.pending = append(s.pending, thing.r) 125 | return thing.r, nil 126 | case <-timeout: 127 | rv := s.pending[0] 128 | s.pending = s.pending[1:] 129 | return rv, errTimedOut 130 | } 131 | } 132 | 133 | func (s *State) readNext() (interface{}, error) { 134 | if len(s.pending) > 0 { 135 | rv := s.pending[0] 136 | s.pending = s.pending[1:] 137 | return rv, nil 138 | } 139 | var r rune 140 | select { 141 | case thing, ok := <-s.next: 142 | if !ok { 143 | return 0, ErrInternal 144 | } 145 | if thing.err != nil { 146 | return nil, thing.err 147 | } 148 | r = thing.r 149 | case <-s.winch: 150 | s.getColumns() 151 | return winch, nil 152 | } 153 | if r != esc { 154 | return r, nil 155 | } 156 | s.pending = append(s.pending, r) 157 | 158 | // Wait at most 50 ms for the rest of the escape sequence 159 | // If nothing else arrives, it was an actual press of the esc key 160 | timeout := time.After(50 * time.Millisecond) 161 | flag, err := s.nextPending(timeout) 162 | if err != nil { 163 | if err == errTimedOut { 164 | return flag, nil 165 | } 166 | return unknown, err 167 | } 168 | 169 | switch flag { 170 | case '[': 171 | code, err := s.nextPending(timeout) 172 | if err != nil { 173 | if err == errTimedOut { 174 | return code, nil 175 | } 176 | return unknown, err 177 | } 178 | switch code { 179 | case 'A': 180 | s.pending = s.pending[:0] // escape code complete 181 | return up, nil 182 | case 'B': 183 | s.pending = s.pending[:0] // escape code complete 184 | return down, nil 185 | case 'C': 186 | s.pending = s.pending[:0] // escape code complete 187 | return right, nil 188 | case 'D': 189 | s.pending = s.pending[:0] // escape code complete 190 | return left, nil 191 | case 'F': 192 | s.pending = s.pending[:0] // escape code complete 193 | return end, nil 194 | case 'H': 195 | s.pending = s.pending[:0] // escape code complete 196 | return home, nil 197 | case 'Z': 198 | s.pending = s.pending[:0] // escape code complete 199 | return shiftTab, nil 200 | case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9': 201 | num := []rune{code} 202 | for { 203 | code, err := s.nextPending(timeout) 204 | if err != nil { 205 | if err == errTimedOut { 206 | return code, nil 207 | } 208 | return nil, err 209 | } 210 | switch code { 211 | case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9': 212 | num = append(num, code) 213 | case ';': 214 | // Modifier code to follow 215 | // This only supports Ctrl-left and Ctrl-right for now 216 | x, _ := strconv.ParseInt(string(num), 10, 32) 217 | if x != 1 { 218 | // Can't be left or right 219 | rv := s.pending[0] 220 | s.pending = s.pending[1:] 221 | return rv, nil 222 | } 223 | num = num[:0] 224 | for { 225 | code, err = s.nextPending(timeout) 226 | if err != nil { 227 | if err == errTimedOut { 228 | rv := s.pending[0] 229 | s.pending = s.pending[1:] 230 | return rv, nil 231 | } 232 | return nil, err 233 | } 234 | switch code { 235 | case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9': 236 | num = append(num, code) 237 | case 'C', 'D': 238 | // right, left 239 | mod, _ := strconv.ParseInt(string(num), 10, 32) 240 | if mod != 5 { 241 | // Not bare Ctrl 242 | rv := s.pending[0] 243 | s.pending = s.pending[1:] 244 | return rv, nil 245 | } 246 | s.pending = s.pending[:0] // escape code complete 247 | if code == 'C' { 248 | return wordRight, nil 249 | } 250 | return wordLeft, nil 251 | default: 252 | // Not left or right 253 | rv := s.pending[0] 254 | s.pending = s.pending[1:] 255 | return rv, nil 256 | } 257 | } 258 | case '~': 259 | s.pending = s.pending[:0] // escape code complete 260 | x, _ := strconv.ParseInt(string(num), 10, 32) 261 | switch x { 262 | case 2: 263 | return insert, nil 264 | case 3: 265 | return del, nil 266 | case 5: 267 | return pageUp, nil 268 | case 6: 269 | return pageDown, nil 270 | case 1, 7: 271 | return home, nil 272 | case 4, 8: 273 | return end, nil 274 | case 15: 275 | return f5, nil 276 | case 17: 277 | return f6, nil 278 | case 18: 279 | return f7, nil 280 | case 19: 281 | return f8, nil 282 | case 20: 283 | return f9, nil 284 | case 21: 285 | return f10, nil 286 | case 23: 287 | return f11, nil 288 | case 24: 289 | return f12, nil 290 | default: 291 | return unknown, nil 292 | } 293 | default: 294 | // unrecognized escape code 295 | rv := s.pending[0] 296 | s.pending = s.pending[1:] 297 | return rv, nil 298 | } 299 | } 300 | } 301 | 302 | case 'O': 303 | code, err := s.nextPending(timeout) 304 | if err != nil { 305 | if err == errTimedOut { 306 | return code, nil 307 | } 308 | return nil, err 309 | } 310 | s.pending = s.pending[:0] // escape code complete 311 | switch code { 312 | case 'c': 313 | return wordRight, nil 314 | case 'd': 315 | return wordLeft, nil 316 | case 'H': 317 | return home, nil 318 | case 'F': 319 | return end, nil 320 | case 'P': 321 | return f1, nil 322 | case 'Q': 323 | return f2, nil 324 | case 'R': 325 | return f3, nil 326 | case 'S': 327 | return f4, nil 328 | default: 329 | return unknown, nil 330 | } 331 | case 'b': 332 | s.pending = s.pending[:0] // escape code complete 333 | return altB, nil 334 | case 'd': 335 | s.pending = s.pending[:0] // escape code complete 336 | return altD, nil 337 | case bs: 338 | s.pending = s.pending[:0] // escape code complete 339 | return altBs, nil 340 | case 'f': 341 | s.pending = s.pending[:0] // escape code complete 342 | return altF, nil 343 | case 'y': 344 | s.pending = s.pending[:0] // escape code complete 345 | return altY, nil 346 | default: 347 | rv := s.pending[0] 348 | s.pending = s.pending[1:] 349 | return rv, nil 350 | } 351 | 352 | // not reached 353 | return r, nil 354 | } 355 | 356 | // Close returns the terminal to its previous mode 357 | func (s *State) Close() error { 358 | signal.Stop(s.winch) 359 | if !s.inputRedirected { 360 | s.origMode.ApplyMode() 361 | } 362 | return nil 363 | } 364 | 365 | // TerminalSupported returns true if the current terminal supports 366 | // line editing features, and false if liner will use the 'dumb' 367 | // fallback for input. 368 | // Note that TerminalSupported does not check all factors that may 369 | // cause liner to not fully support the terminal (such as stdin redirection) 370 | func TerminalSupported() bool { 371 | bad := map[string]bool{"": true, "dumb": true, "cons25": true} 372 | return !bad[strings.ToLower(os.Getenv("TERM"))] 373 | } 374 | -------------------------------------------------------------------------------- /input_darwin.go: -------------------------------------------------------------------------------- 1 | //go:build darwin 2 | // +build darwin 3 | 4 | package liner 5 | 6 | import "syscall" 7 | 8 | const ( 9 | getTermios = syscall.TIOCGETA 10 | setTermios = syscall.TIOCSETA 11 | ) 12 | 13 | const ( 14 | // Input flags 15 | inpck = 0x010 16 | istrip = 0x020 17 | icrnl = 0x100 18 | ixon = 0x200 19 | 20 | // Output flags 21 | opost = 0x1 22 | 23 | // Control flags 24 | cs8 = 0x300 25 | 26 | // Local flags 27 | isig = 0x080 28 | icanon = 0x100 29 | iexten = 0x400 30 | ) 31 | 32 | type termios struct { 33 | Iflag uintptr 34 | Oflag uintptr 35 | Cflag uintptr 36 | Lflag uintptr 37 | Cc [20]byte 38 | Ispeed uintptr 39 | Ospeed uintptr 40 | } 41 | 42 | // Terminal.app needs a column for the cursor when the input line is at the 43 | // bottom of the window. 44 | const cursorColumn = true 45 | -------------------------------------------------------------------------------- /input_linux.go: -------------------------------------------------------------------------------- 1 | //go:build linux 2 | // +build linux 3 | 4 | package liner 5 | 6 | import "syscall" 7 | 8 | const ( 9 | getTermios = syscall.TCGETS 10 | setTermios = syscall.TCSETS 11 | ) 12 | 13 | const ( 14 | icrnl = syscall.ICRNL 15 | inpck = syscall.INPCK 16 | istrip = syscall.ISTRIP 17 | ixon = syscall.IXON 18 | opost = syscall.OPOST 19 | cs8 = syscall.CS8 20 | isig = syscall.ISIG 21 | icanon = syscall.ICANON 22 | iexten = syscall.IEXTEN 23 | ) 24 | 25 | type termios struct { 26 | syscall.Termios 27 | } 28 | 29 | const cursorColumn = false 30 | -------------------------------------------------------------------------------- /input_solaris.go: -------------------------------------------------------------------------------- 1 | package liner 2 | 3 | import ( 4 | "golang.org/x/sys/unix" 5 | ) 6 | 7 | const ( 8 | getTermios = unix.TCGETS 9 | setTermios = unix.TCSETS 10 | ) 11 | 12 | const ( 13 | icrnl = unix.ICRNL 14 | inpck = unix.INPCK 15 | istrip = unix.ISTRIP 16 | ixon = unix.IXON 17 | opost = unix.OPOST 18 | cs8 = unix.CS8 19 | isig = unix.ISIG 20 | icanon = unix.ICANON 21 | iexten = unix.IEXTEN 22 | ) 23 | 24 | type termios unix.Termios 25 | 26 | const cursorColumn = false 27 | -------------------------------------------------------------------------------- /input_test.go: -------------------------------------------------------------------------------- 1 | //go:build !windows 2 | // +build !windows 3 | 4 | package liner 5 | 6 | import ( 7 | "bufio" 8 | "bytes" 9 | "testing" 10 | ) 11 | 12 | func (s *State) expectRune(t *testing.T, r rune) { 13 | item, err := s.readNext() 14 | if err != nil { 15 | t.Fatalf("Expected rune '%c', got error %s\n", r, err) 16 | } 17 | if v, ok := item.(rune); !ok { 18 | t.Fatalf("Expected rune '%c', got non-rune %v\n", r, v) 19 | } else { 20 | if v != r { 21 | t.Fatalf("Expected rune '%c', got rune '%c'\n", r, v) 22 | } 23 | } 24 | } 25 | 26 | func (s *State) expectAction(t *testing.T, a action) { 27 | item, err := s.readNext() 28 | if err != nil { 29 | t.Fatalf("Expected Action %d, got error %s\n", a, err) 30 | } 31 | if v, ok := item.(action); !ok { 32 | t.Fatalf("Expected Action %d, got non-Action %v\n", a, v) 33 | } else { 34 | if v != a { 35 | t.Fatalf("Expected Action %d, got Action %d\n", a, v) 36 | } 37 | } 38 | } 39 | 40 | func TestTypes(t *testing.T) { 41 | input := []byte{'A', 27, 'B', 27, 91, 68, 27, '[', '1', ';', '5', 'D', 'e'} 42 | var s State 43 | s.r = bufio.NewReader(bytes.NewBuffer(input)) 44 | 45 | next := make(chan nexter) 46 | go func() { 47 | for { 48 | var n nexter 49 | n.r, _, n.err = s.r.ReadRune() 50 | next <- n 51 | } 52 | }() 53 | s.next = next 54 | 55 | s.expectRune(t, 'A') 56 | s.expectRune(t, 27) 57 | s.expectRune(t, 'B') 58 | s.expectAction(t, left) 59 | s.expectAction(t, wordLeft) 60 | 61 | s.expectRune(t, 'e') 62 | } 63 | -------------------------------------------------------------------------------- /input_windows.go: -------------------------------------------------------------------------------- 1 | package liner 2 | 3 | import ( 4 | "bufio" 5 | "os" 6 | "syscall" 7 | "unicode/utf16" 8 | "unsafe" 9 | ) 10 | 11 | var ( 12 | kernel32 = syscall.NewLazyDLL("kernel32.dll") 13 | 14 | procGetStdHandle = kernel32.NewProc("GetStdHandle") 15 | procReadConsoleInput = kernel32.NewProc("ReadConsoleInputW") 16 | procGetNumberOfConsoleInputEvents = kernel32.NewProc("GetNumberOfConsoleInputEvents") 17 | procGetConsoleMode = kernel32.NewProc("GetConsoleMode") 18 | procSetConsoleMode = kernel32.NewProc("SetConsoleMode") 19 | procSetConsoleCursorPosition = kernel32.NewProc("SetConsoleCursorPosition") 20 | procGetConsoleScreenBufferInfo = kernel32.NewProc("GetConsoleScreenBufferInfo") 21 | procFillConsoleOutputCharacter = kernel32.NewProc("FillConsoleOutputCharacterW") 22 | ) 23 | 24 | // These names are from the Win32 api, so they use underscores (contrary to 25 | // what golint suggests) 26 | const ( 27 | std_input_handle = uint32(-10 & 0xFFFFFFFF) 28 | std_output_handle = uint32(-11 & 0xFFFFFFFF) 29 | std_error_handle = uint32(-12 & 0xFFFFFFFF) 30 | invalid_handle_value = ^uintptr(0) 31 | ) 32 | 33 | type inputMode uint32 34 | 35 | // State represents an open terminal 36 | type State struct { 37 | commonState 38 | handle syscall.Handle 39 | hOut syscall.Handle 40 | origMode inputMode 41 | defaultMode inputMode 42 | key interface{} 43 | repeat uint16 44 | } 45 | 46 | const ( 47 | enableEchoInput = 0x4 48 | enableInsertMode = 0x20 49 | enableLineInput = 0x2 50 | enableMouseInput = 0x10 51 | enableProcessedInput = 0x1 52 | enableQuickEditMode = 0x40 53 | enableWindowInput = 0x8 54 | ) 55 | 56 | // NewLiner initializes a new *State, and sets the terminal into raw mode. To 57 | // restore the terminal to its previous state, call State.Close(). 58 | func NewLiner() *State { 59 | var s State 60 | hIn, _, _ := procGetStdHandle.Call(uintptr(std_input_handle)) 61 | s.handle = syscall.Handle(hIn) 62 | hOut, _, _ := procGetStdHandle.Call(uintptr(std_output_handle)) 63 | s.hOut = syscall.Handle(hOut) 64 | 65 | s.terminalSupported = true 66 | if m, err := TerminalMode(); err == nil { 67 | s.origMode = m.(inputMode) 68 | mode := s.origMode 69 | mode &^= enableEchoInput 70 | mode &^= enableInsertMode 71 | mode &^= enableLineInput 72 | mode &^= enableMouseInput 73 | mode |= enableWindowInput 74 | mode.ApplyMode() 75 | } else { 76 | s.inputRedirected = true 77 | s.r = bufio.NewReader(os.Stdin) 78 | } 79 | 80 | s.getColumns() 81 | s.outputRedirected = s.columns <= 0 82 | 83 | return &s 84 | } 85 | 86 | // These names are from the Win32 api, so they use underscores (contrary to 87 | // what golint suggests) 88 | const ( 89 | focus_event = 0x0010 90 | key_event = 0x0001 91 | menu_event = 0x0008 92 | mouse_event = 0x0002 93 | window_buffer_size_event = 0x0004 94 | ) 95 | 96 | type input_record struct { 97 | eventType uint16 98 | pad uint16 99 | blob [16]byte 100 | } 101 | 102 | type key_event_record struct { 103 | KeyDown int32 104 | RepeatCount uint16 105 | VirtualKeyCode uint16 106 | VirtualScanCode uint16 107 | Char uint16 108 | ControlKeyState uint32 109 | } 110 | 111 | // These names are from the Win32 api, so they use underscores (contrary to 112 | // what golint suggests) 113 | const ( 114 | vk_back = 0x08 115 | vk_tab = 0x09 116 | vk_menu = 0x12 // ALT key 117 | vk_prior = 0x21 118 | vk_next = 0x22 119 | vk_end = 0x23 120 | vk_home = 0x24 121 | vk_left = 0x25 122 | vk_up = 0x26 123 | vk_right = 0x27 124 | vk_down = 0x28 125 | vk_insert = 0x2d 126 | vk_delete = 0x2e 127 | vk_f1 = 0x70 128 | vk_f2 = 0x71 129 | vk_f3 = 0x72 130 | vk_f4 = 0x73 131 | vk_f5 = 0x74 132 | vk_f6 = 0x75 133 | vk_f7 = 0x76 134 | vk_f8 = 0x77 135 | vk_f9 = 0x78 136 | vk_f10 = 0x79 137 | vk_f11 = 0x7a 138 | vk_f12 = 0x7b 139 | bKey = 0x42 140 | dKey = 0x44 141 | fKey = 0x46 142 | yKey = 0x59 143 | ) 144 | 145 | const ( 146 | shiftPressed = 0x0010 147 | leftAltPressed = 0x0002 148 | leftCtrlPressed = 0x0008 149 | rightAltPressed = 0x0001 150 | rightCtrlPressed = 0x0004 151 | 152 | modKeys = shiftPressed | leftAltPressed | rightAltPressed | leftCtrlPressed | rightCtrlPressed 153 | ) 154 | 155 | // inputWaiting only returns true if the next call to readNext will return immediately. 156 | func (s *State) inputWaiting() bool { 157 | var num uint32 158 | ok, _, _ := procGetNumberOfConsoleInputEvents.Call(uintptr(s.handle), uintptr(unsafe.Pointer(&num))) 159 | if ok == 0 { 160 | // call failed, so we cannot guarantee a non-blocking readNext 161 | return false 162 | } 163 | 164 | // during a "paste" input events are always an odd number, and 165 | // the last one results in a blocking readNext, so return false 166 | // when num is 1 or 0. 167 | return num > 1 168 | } 169 | 170 | func (s *State) readNext() (interface{}, error) { 171 | if s.repeat > 0 { 172 | s.repeat-- 173 | return s.key, nil 174 | } 175 | 176 | var input input_record 177 | var rv uint32 178 | 179 | var surrogate uint16 180 | 181 | for { 182 | ok, _, err := procReadConsoleInput.Call(uintptr(s.handle), 183 | uintptr(unsafe.Pointer(&input)), 1, uintptr(unsafe.Pointer(&rv))) 184 | 185 | if ok == 0 { 186 | return nil, err 187 | } 188 | 189 | if input.eventType == window_buffer_size_event { 190 | xy := (*coord)(unsafe.Pointer(&input.blob[0])) 191 | s.columns = int(xy.x) 192 | return winch, nil 193 | } 194 | if input.eventType != key_event { 195 | continue 196 | } 197 | ke := (*key_event_record)(unsafe.Pointer(&input.blob[0])) 198 | if ke.KeyDown == 0 { 199 | if ke.VirtualKeyCode == vk_menu && ke.Char > 0 { 200 | // paste of unicode (eg. via ALT-numpad) 201 | if surrogate > 0 { 202 | return utf16.DecodeRune(rune(surrogate), rune(ke.Char)), nil 203 | } else if utf16.IsSurrogate(rune(ke.Char)) { 204 | surrogate = ke.Char 205 | continue 206 | } else { 207 | return rune(ke.Char), nil 208 | } 209 | } 210 | continue 211 | } 212 | 213 | if ke.VirtualKeyCode == vk_tab && ke.ControlKeyState&modKeys == shiftPressed { 214 | s.key = shiftTab 215 | } else if ke.VirtualKeyCode == vk_back && (ke.ControlKeyState&modKeys == leftAltPressed || 216 | ke.ControlKeyState&modKeys == rightAltPressed) { 217 | s.key = altBs 218 | } else if ke.VirtualKeyCode == bKey && (ke.ControlKeyState&modKeys == leftAltPressed || 219 | ke.ControlKeyState&modKeys == rightAltPressed) { 220 | s.key = altB 221 | } else if ke.VirtualKeyCode == dKey && (ke.ControlKeyState&modKeys == leftAltPressed || 222 | ke.ControlKeyState&modKeys == rightAltPressed) { 223 | s.key = altD 224 | } else if ke.VirtualKeyCode == fKey && (ke.ControlKeyState&modKeys == leftAltPressed || 225 | ke.ControlKeyState&modKeys == rightAltPressed) { 226 | s.key = altF 227 | } else if ke.VirtualKeyCode == yKey && (ke.ControlKeyState&modKeys == leftAltPressed || 228 | ke.ControlKeyState&modKeys == rightAltPressed) { 229 | s.key = altY 230 | } else if ke.Char > 0 { 231 | if surrogate > 0 { 232 | s.key = utf16.DecodeRune(rune(surrogate), rune(ke.Char)) 233 | } else if utf16.IsSurrogate(rune(ke.Char)) { 234 | surrogate = ke.Char 235 | continue 236 | } else { 237 | s.key = rune(ke.Char) 238 | } 239 | } else { 240 | switch ke.VirtualKeyCode { 241 | case vk_prior: 242 | s.key = pageUp 243 | case vk_next: 244 | s.key = pageDown 245 | case vk_end: 246 | s.key = end 247 | case vk_home: 248 | s.key = home 249 | case vk_left: 250 | s.key = left 251 | if ke.ControlKeyState&(leftCtrlPressed|rightCtrlPressed) != 0 { 252 | if ke.ControlKeyState&modKeys == ke.ControlKeyState&(leftCtrlPressed|rightCtrlPressed) { 253 | s.key = wordLeft 254 | } 255 | } 256 | case vk_right: 257 | s.key = right 258 | if ke.ControlKeyState&(leftCtrlPressed|rightCtrlPressed) != 0 { 259 | if ke.ControlKeyState&modKeys == ke.ControlKeyState&(leftCtrlPressed|rightCtrlPressed) { 260 | s.key = wordRight 261 | } 262 | } 263 | case vk_up: 264 | s.key = up 265 | case vk_down: 266 | s.key = down 267 | case vk_insert: 268 | s.key = insert 269 | case vk_delete: 270 | s.key = del 271 | case vk_f1: 272 | s.key = f1 273 | case vk_f2: 274 | s.key = f2 275 | case vk_f3: 276 | s.key = f3 277 | case vk_f4: 278 | s.key = f4 279 | case vk_f5: 280 | s.key = f5 281 | case vk_f6: 282 | s.key = f6 283 | case vk_f7: 284 | s.key = f7 285 | case vk_f8: 286 | s.key = f8 287 | case vk_f9: 288 | s.key = f9 289 | case vk_f10: 290 | s.key = f10 291 | case vk_f11: 292 | s.key = f11 293 | case vk_f12: 294 | s.key = f12 295 | default: 296 | // Eat modifier keys 297 | // TODO: return Action(Unknown) if the key isn't a 298 | // modifier. 299 | continue 300 | } 301 | } 302 | 303 | if ke.RepeatCount > 1 { 304 | s.repeat = ke.RepeatCount - 1 305 | } 306 | return s.key, nil 307 | } 308 | } 309 | 310 | // Close returns the terminal to its previous mode 311 | func (s *State) Close() error { 312 | s.origMode.ApplyMode() 313 | return nil 314 | } 315 | 316 | func (s *State) startPrompt() { 317 | if m, err := TerminalMode(); err == nil { 318 | s.defaultMode = m.(inputMode) 319 | mode := s.defaultMode 320 | mode &^= enableProcessedInput 321 | mode.ApplyMode() 322 | } 323 | } 324 | 325 | func (s *State) restartPrompt() { 326 | } 327 | 328 | func (s *State) stopPrompt() { 329 | s.defaultMode.ApplyMode() 330 | } 331 | 332 | // TerminalSupported returns true because line editing is always 333 | // supported on Windows. 334 | func TerminalSupported() bool { 335 | return true 336 | } 337 | 338 | func (mode inputMode) ApplyMode() error { 339 | hIn, _, err := procGetStdHandle.Call(uintptr(std_input_handle)) 340 | if hIn == invalid_handle_value || hIn == 0 { 341 | return err 342 | } 343 | ok, _, err := procSetConsoleMode.Call(hIn, uintptr(mode)) 344 | if ok != 0 { 345 | err = nil 346 | } 347 | return err 348 | } 349 | 350 | // TerminalMode returns the current terminal input mode as an InputModeSetter. 351 | // 352 | // This function is provided for convenience, and should 353 | // not be necessary for most users of liner. 354 | func TerminalMode() (ModeApplier, error) { 355 | var mode inputMode 356 | hIn, _, err := procGetStdHandle.Call(uintptr(std_input_handle)) 357 | if hIn == invalid_handle_value || hIn == 0 { 358 | return nil, err 359 | } 360 | ok, _, err := procGetConsoleMode.Call(hIn, uintptr(unsafe.Pointer(&mode))) 361 | if ok != 0 { 362 | err = nil 363 | } 364 | return mode, err 365 | } 366 | 367 | const cursorColumn = true 368 | -------------------------------------------------------------------------------- /line.go: -------------------------------------------------------------------------------- 1 | //go:build windows || linux || darwin || openbsd || freebsd || netbsd || solaris 2 | // +build windows linux darwin openbsd freebsd netbsd solaris 3 | 4 | package liner 5 | 6 | import ( 7 | "bufio" 8 | "container/ring" 9 | "errors" 10 | "fmt" 11 | "io" 12 | "os" 13 | "strings" 14 | "unicode" 15 | "unicode/utf8" 16 | ) 17 | 18 | type action int 19 | 20 | const ( 21 | left action = iota 22 | right 23 | up 24 | down 25 | home 26 | end 27 | insert 28 | del 29 | pageUp 30 | pageDown 31 | f1 32 | f2 33 | f3 34 | f4 35 | f5 36 | f6 37 | f7 38 | f8 39 | f9 40 | f10 41 | f11 42 | f12 43 | altB 44 | altBs // Alt+Backspace 45 | altD 46 | altF 47 | altY 48 | shiftTab 49 | wordLeft 50 | wordRight 51 | winch 52 | unknown 53 | ) 54 | 55 | const ( 56 | ctrlA = 1 57 | ctrlB = 2 58 | ctrlC = 3 59 | ctrlD = 4 60 | ctrlE = 5 61 | ctrlF = 6 62 | ctrlG = 7 63 | ctrlH = 8 64 | tab = 9 65 | lf = 10 66 | ctrlK = 11 67 | ctrlL = 12 68 | cr = 13 69 | ctrlN = 14 70 | ctrlO = 15 71 | ctrlP = 16 72 | ctrlQ = 17 73 | ctrlR = 18 74 | ctrlS = 19 75 | ctrlT = 20 76 | ctrlU = 21 77 | ctrlV = 22 78 | ctrlW = 23 79 | ctrlX = 24 80 | ctrlY = 25 81 | ctrlZ = 26 82 | esc = 27 83 | bs = 127 84 | ) 85 | 86 | const ( 87 | beep = "\a" 88 | ) 89 | 90 | type tabDirection int 91 | 92 | const ( 93 | tabForward tabDirection = iota 94 | tabReverse 95 | ) 96 | 97 | func (s *State) refresh(prompt []rune, buf []rune, pos int) error { 98 | if s.columns == 0 { 99 | return ErrInternal 100 | } 101 | 102 | s.needRefresh = false 103 | if s.multiLineMode { 104 | return s.refreshMultiLine(prompt, buf, pos) 105 | } 106 | return s.refreshSingleLine(prompt, buf, pos) 107 | } 108 | 109 | func (s *State) refreshSingleLine(prompt []rune, buf []rune, pos int) error { 110 | s.cursorPos(0) 111 | _, err := fmt.Print(string(prompt)) 112 | if err != nil { 113 | return err 114 | } 115 | 116 | pLen := countGlyphs(prompt) 117 | bLen := countGlyphs(buf) 118 | // on some OS / terminals extra column is needed to place the cursor char 119 | if cursorColumn { 120 | bLen++ 121 | } 122 | pos = countGlyphs(buf[:pos]) 123 | if pLen+bLen < s.columns { 124 | _, err = fmt.Print(string(buf)) 125 | s.eraseLine() 126 | s.cursorPos(pLen + pos) 127 | } else { 128 | // Find space available 129 | space := s.columns - pLen 130 | space-- // space for cursor 131 | start := pos - space/2 132 | end := start + space 133 | if end > bLen { 134 | end = bLen 135 | start = end - space 136 | } 137 | if start < 0 { 138 | start = 0 139 | end = space 140 | } 141 | pos -= start 142 | 143 | // Leave space for markers 144 | if start > 0 { 145 | start++ 146 | } 147 | if end < bLen { 148 | end-- 149 | } 150 | startRune := len(getPrefixGlyphs(buf, start)) 151 | line := getPrefixGlyphs(buf[startRune:], end-start) 152 | 153 | // Output 154 | if start > 0 { 155 | fmt.Print("{") 156 | } 157 | fmt.Print(string(line)) 158 | if end < bLen { 159 | fmt.Print("}") 160 | } 161 | 162 | // Set cursor position 163 | s.eraseLine() 164 | s.cursorPos(pLen + pos) 165 | } 166 | return err 167 | } 168 | 169 | func (s *State) refreshMultiLine(prompt []rune, buf []rune, pos int) error { 170 | promptColumns := countMultiLineGlyphs(prompt, s.columns, 0) 171 | totalColumns := countMultiLineGlyphs(buf, s.columns, promptColumns) 172 | // on some OS / terminals extra column is needed to place the cursor char 173 | // if cursorColumn { 174 | // totalColumns++ 175 | // } 176 | 177 | // it looks like Multiline mode always assume that a cursor need an extra column, 178 | // and always emit a newline if we are at the screen end, so no worarounds needed there 179 | 180 | totalRows := (totalColumns + s.columns - 1) / s.columns 181 | maxRows := s.maxRows 182 | if totalRows > s.maxRows { 183 | s.maxRows = totalRows 184 | } 185 | cursorRows := s.cursorRows 186 | if cursorRows == 0 { 187 | cursorRows = 1 188 | } 189 | 190 | /* First step: clear all the lines used before. To do so start by 191 | * going to the last row. */ 192 | if maxRows-cursorRows > 0 { 193 | s.moveDown(maxRows - cursorRows) 194 | } 195 | 196 | /* Now for every row clear it, go up. */ 197 | for i := 0; i < maxRows-1; i++ { 198 | s.cursorPos(0) 199 | s.eraseLine() 200 | s.moveUp(1) 201 | } 202 | 203 | /* Clean the top line. */ 204 | s.cursorPos(0) 205 | s.eraseLine() 206 | 207 | /* Write the prompt and the current buffer content */ 208 | if _, err := fmt.Print(string(prompt)); err != nil { 209 | return err 210 | } 211 | if _, err := fmt.Print(string(buf)); err != nil { 212 | return err 213 | } 214 | 215 | /* If we are at the very end of the screen with our prompt, we need to 216 | * emit a newline and move the prompt to the first column. */ 217 | cursorColumns := countMultiLineGlyphs(buf[:pos], s.columns, promptColumns) 218 | if cursorColumns == totalColumns && totalColumns%s.columns == 0 { 219 | s.emitNewLine() 220 | s.cursorPos(0) 221 | totalRows++ 222 | if totalRows > s.maxRows { 223 | s.maxRows = totalRows 224 | } 225 | } 226 | 227 | /* Move cursor to right position. */ 228 | cursorRows = (cursorColumns + s.columns) / s.columns 229 | if s.cursorRows > 0 && totalRows-cursorRows > 0 { 230 | s.moveUp(totalRows - cursorRows) 231 | } 232 | /* Set column. */ 233 | s.cursorPos(cursorColumns % s.columns) 234 | 235 | s.cursorRows = cursorRows 236 | return nil 237 | } 238 | 239 | func (s *State) resetMultiLine(prompt []rune, buf []rune, pos int) { 240 | columns := countMultiLineGlyphs(prompt, s.columns, 0) 241 | columns = countMultiLineGlyphs(buf[:pos], s.columns, columns) 242 | columns += 2 // ^C 243 | cursorRows := (columns + s.columns) / s.columns 244 | if s.maxRows-cursorRows > 0 { 245 | for i := 0; i < s.maxRows-cursorRows; i++ { 246 | fmt.Println() // always moves the cursor down or scrolls the window up as needed 247 | } 248 | } 249 | s.maxRows = 1 250 | s.cursorRows = 0 251 | } 252 | 253 | func longestCommonPrefix(strs []string) string { 254 | if len(strs) == 0 { 255 | return "" 256 | } 257 | longest := strs[0] 258 | 259 | for _, str := range strs[1:] { 260 | for !strings.HasPrefix(str, longest) { 261 | longest = longest[:len(longest)-1] 262 | } 263 | } 264 | // Remove trailing partial runes 265 | longest = strings.TrimRight(longest, "\uFFFD") 266 | return longest 267 | } 268 | 269 | func (s *State) circularTabs(items []string) func(tabDirection) (string, error) { 270 | item := -1 271 | return func(direction tabDirection) (string, error) { 272 | if direction == tabForward { 273 | if item < len(items)-1 { 274 | item++ 275 | } else { 276 | item = 0 277 | } 278 | } else if direction == tabReverse { 279 | if item > 0 { 280 | item-- 281 | } else { 282 | item = len(items) - 1 283 | } 284 | } 285 | return items[item], nil 286 | } 287 | } 288 | 289 | func calculateColumns(screenWidth int, items []string) (numColumns, numRows, maxWidth int) { 290 | for _, item := range items { 291 | if len(item) >= screenWidth { 292 | return 1, len(items), screenWidth - 1 293 | } 294 | if len(item) >= maxWidth { 295 | maxWidth = len(item) + 1 296 | } 297 | } 298 | 299 | numColumns = screenWidth / maxWidth 300 | numRows = len(items) / numColumns 301 | if len(items)%numColumns > 0 { 302 | numRows++ 303 | } 304 | 305 | if len(items) <= numColumns { 306 | maxWidth = 0 307 | } 308 | 309 | return 310 | } 311 | 312 | func (s *State) printedTabs(items []string) func(tabDirection) (string, error) { 313 | numTabs := 1 314 | prefix := longestCommonPrefix(items) 315 | return func(direction tabDirection) (string, error) { 316 | if len(items) == 1 { 317 | return items[0], nil 318 | } 319 | 320 | if numTabs == 2 { 321 | if len(items) > 100 { 322 | fmt.Printf("\nDisplay all %d possibilities? (y or n) ", len(items)) 323 | prompt: 324 | for { 325 | next, err := s.readNext() 326 | if err != nil { 327 | return prefix, err 328 | } 329 | 330 | if key, ok := next.(rune); ok { 331 | switch key { 332 | case 'n', 'N': 333 | return prefix, nil 334 | case 'y', 'Y': 335 | break prompt 336 | case ctrlC, ctrlD, cr, lf: 337 | s.restartPrompt() 338 | } 339 | } 340 | } 341 | } 342 | fmt.Println("") 343 | 344 | numColumns, numRows, maxWidth := calculateColumns(s.columns, items) 345 | 346 | for i := 0; i < numRows; i++ { 347 | for j := 0; j < numColumns*numRows; j += numRows { 348 | if i+j < len(items) { 349 | if maxWidth > 0 { 350 | fmt.Printf("%-*.[1]*s", maxWidth, items[i+j]) 351 | } else { 352 | fmt.Printf("%v ", items[i+j]) 353 | } 354 | } 355 | } 356 | fmt.Println("") 357 | } 358 | } else { 359 | numTabs++ 360 | } 361 | return prefix, nil 362 | } 363 | } 364 | 365 | func (s *State) tabComplete(p []rune, line []rune, pos int) ([]rune, int, interface{}, error) { 366 | if s.completer == nil { 367 | return line, pos, rune(esc), nil 368 | } 369 | head, list, tail := s.completer(string(line), pos) 370 | if len(list) <= 0 { 371 | return line, pos, rune(esc), nil 372 | } 373 | hl := utf8.RuneCountInString(head) 374 | if len(list) == 1 { 375 | err := s.refresh(p, []rune(head+list[0]+tail), hl+utf8.RuneCountInString(list[0])) 376 | return []rune(head + list[0] + tail), hl + utf8.RuneCountInString(list[0]), rune(esc), err 377 | } 378 | 379 | direction := tabForward 380 | tabPrinter := s.circularTabs(list) 381 | if s.tabStyle == TabPrints { 382 | tabPrinter = s.printedTabs(list) 383 | } 384 | 385 | for { 386 | pick, err := tabPrinter(direction) 387 | if err != nil { 388 | return line, pos, rune(esc), err 389 | } 390 | err = s.refresh(p, []rune(head+pick+tail), hl+utf8.RuneCountInString(pick)) 391 | if err != nil { 392 | return line, pos, rune(esc), err 393 | } 394 | 395 | next, err := s.readNext() 396 | if err != nil { 397 | return line, pos, rune(esc), err 398 | } 399 | if key, ok := next.(rune); ok { 400 | if key == tab { 401 | direction = tabForward 402 | continue 403 | } 404 | if key == esc { 405 | return line, pos, rune(esc), nil 406 | } 407 | } 408 | if a, ok := next.(action); ok && a == shiftTab { 409 | direction = tabReverse 410 | continue 411 | } 412 | return []rune(head + pick + tail), hl + utf8.RuneCountInString(pick), next, nil 413 | } 414 | } 415 | 416 | // reverse intelligent search, implements a bash-like history search. 417 | func (s *State) reverseISearch(origLine []rune, origPos int) ([]rune, int, interface{}, error) { 418 | p := "(reverse-i-search)`': " 419 | err := s.refresh([]rune(p), origLine, origPos) 420 | if err != nil { 421 | return origLine, origPos, rune(esc), err 422 | } 423 | 424 | line := []rune{} 425 | pos := 0 426 | foundLine := string(origLine) 427 | foundPos := origPos 428 | 429 | getLine := func() ([]rune, []rune, int) { 430 | search := string(line) 431 | prompt := "(reverse-i-search)`%s': " 432 | return []rune(fmt.Sprintf(prompt, search)), []rune(foundLine), foundPos 433 | } 434 | 435 | history, positions := s.getHistoryByPattern(string(line)) 436 | historyPos := len(history) - 1 437 | 438 | for { 439 | next, err := s.readNext() 440 | if err != nil { 441 | return []rune(foundLine), foundPos, rune(esc), err 442 | } 443 | 444 | switch v := next.(type) { 445 | case rune: 446 | switch v { 447 | case ctrlR: // Search backwards 448 | if historyPos > 0 && historyPos < len(history) { 449 | historyPos-- 450 | foundLine = history[historyPos] 451 | foundPos = positions[historyPos] 452 | } else { 453 | s.doBeep() 454 | } 455 | case ctrlS: // Search forward 456 | if historyPos < len(history)-1 && historyPos >= 0 { 457 | historyPos++ 458 | foundLine = history[historyPos] 459 | foundPos = positions[historyPos] 460 | } else { 461 | s.doBeep() 462 | } 463 | case ctrlH, bs: // Backspace 464 | if pos <= 0 { 465 | s.doBeep() 466 | } else { 467 | n := len(getSuffixGlyphs(line[:pos], 1)) 468 | line = append(line[:pos-n], line[pos:]...) 469 | pos -= n 470 | 471 | // For each char deleted, display the last matching line of history 472 | history, positions := s.getHistoryByPattern(string(line)) 473 | historyPos = len(history) - 1 474 | if len(history) > 0 { 475 | foundLine = history[historyPos] 476 | foundPos = positions[historyPos] 477 | } else { 478 | foundLine = "" 479 | foundPos = 0 480 | } 481 | } 482 | case ctrlG: // Cancel 483 | return origLine, origPos, rune(esc), err 484 | 485 | case tab, cr, lf, ctrlA, ctrlB, ctrlD, ctrlE, ctrlF, ctrlK, 486 | ctrlL, ctrlN, ctrlO, ctrlP, ctrlQ, ctrlT, ctrlU, ctrlV, ctrlW, ctrlX, ctrlY, ctrlZ: 487 | fallthrough 488 | case 0, ctrlC, esc, 28, 29, 30, 31: 489 | return []rune(foundLine), foundPos, next, err 490 | default: 491 | line = append(line[:pos], append([]rune{v}, line[pos:]...)...) 492 | pos++ 493 | 494 | // For each keystroke typed, display the last matching line of history 495 | history, positions = s.getHistoryByPattern(string(line)) 496 | historyPos = len(history) - 1 497 | if len(history) > 0 { 498 | foundLine = history[historyPos] 499 | foundPos = positions[historyPos] 500 | } else { 501 | foundLine = "" 502 | foundPos = 0 503 | } 504 | } 505 | case action: 506 | return []rune(foundLine), foundPos, next, err 507 | } 508 | err = s.refresh(getLine()) 509 | if err != nil { 510 | return []rune(foundLine), foundPos, rune(esc), err 511 | } 512 | } 513 | } 514 | 515 | // addToKillRing adds some text to the kill ring. If mode is 0 it adds it to a 516 | // new node in the end of the kill ring, and move the current pointer to the new 517 | // node. If mode is 1 or 2 it appends or prepends the text to the current entry 518 | // of the killRing. 519 | func (s *State) addToKillRing(text []rune, mode int) { 520 | // Don't use the same underlying array as text 521 | killLine := make([]rune, len(text)) 522 | copy(killLine, text) 523 | 524 | // Point killRing to a newNode, procedure depends on the killring state and 525 | // append mode. 526 | if mode == 0 { // Add new node to killRing 527 | if s.killRing == nil { // if killring is empty, create a new one 528 | s.killRing = ring.New(1) 529 | } else if s.killRing.Len() >= KillRingMax { // if killring is "full" 530 | s.killRing = s.killRing.Next() 531 | } else { // Normal case 532 | s.killRing.Link(ring.New(1)) 533 | s.killRing = s.killRing.Next() 534 | } 535 | } else { 536 | if s.killRing == nil { // if killring is empty, create a new one 537 | s.killRing = ring.New(1) 538 | s.killRing.Value = []rune{} 539 | } 540 | if mode == 1 { // Append to last entry 541 | killLine = append(s.killRing.Value.([]rune), killLine...) 542 | } else if mode == 2 { // Prepend to last entry 543 | killLine = append(killLine, s.killRing.Value.([]rune)...) 544 | } 545 | } 546 | 547 | // Save text in the current killring node 548 | s.killRing.Value = killLine 549 | } 550 | 551 | func (s *State) yank(p []rune, text []rune, pos int) ([]rune, int, interface{}, error) { 552 | if s.killRing == nil { 553 | return text, pos, rune(esc), nil 554 | } 555 | 556 | lineStart := text[:pos] 557 | lineEnd := text[pos:] 558 | var line []rune 559 | 560 | for { 561 | value := s.killRing.Value.([]rune) 562 | line = make([]rune, 0) 563 | line = append(line, lineStart...) 564 | line = append(line, value...) 565 | line = append(line, lineEnd...) 566 | 567 | pos = len(lineStart) + len(value) 568 | err := s.refresh(p, line, pos) 569 | if err != nil { 570 | return line, pos, 0, err 571 | } 572 | 573 | next, err := s.readNext() 574 | if err != nil { 575 | return line, pos, next, err 576 | } 577 | 578 | switch v := next.(type) { 579 | case rune: 580 | return line, pos, next, nil 581 | case action: 582 | switch v { 583 | case altY: 584 | s.killRing = s.killRing.Prev() 585 | default: 586 | return line, pos, next, nil 587 | } 588 | } 589 | } 590 | } 591 | 592 | // Prompt displays p and returns a line of user input, not including a trailing 593 | // newline character. An io.EOF error is returned if the user signals end-of-file 594 | // by pressing Ctrl-D. Prompt allows line editing if the terminal supports it. 595 | func (s *State) Prompt(prompt string) (string, error) { 596 | return s.PromptWithSuggestion(prompt, "", 0) 597 | } 598 | 599 | // PromptWithSuggestion displays prompt and an editable text with cursor at 600 | // given position. The cursor will be set to the end of the line if given position 601 | // is negative or greater than length of text (in runes). Returns a line of user input, not 602 | // including a trailing newline character. An io.EOF error is returned if the user 603 | // signals end-of-file by pressing Ctrl-D. 604 | func (s *State) PromptWithSuggestion(prompt string, text string, pos int) (string, error) { 605 | for _, r := range prompt { 606 | if unicode.Is(unicode.C, r) { 607 | return "", ErrInvalidPrompt 608 | } 609 | } 610 | if s.inputRedirected || !s.terminalSupported { 611 | return s.promptUnsupported(prompt) 612 | } 613 | p := []rune(prompt) 614 | const minWorkingSpace = 10 615 | if s.columns < countGlyphs(p)+minWorkingSpace { 616 | return s.tooNarrow(prompt) 617 | } 618 | if s.outputRedirected { 619 | return "", ErrNotTerminalOutput 620 | } 621 | 622 | s.historyMutex.RLock() 623 | defer s.historyMutex.RUnlock() 624 | 625 | fmt.Print(prompt) 626 | var line = []rune(text) 627 | historyEnd := "" 628 | var historyPrefix []string 629 | historyPos := 0 630 | historyStale := true 631 | historyAction := false // used to mark history related actions 632 | killAction := 0 // used to mark kill related actions 633 | 634 | defer s.stopPrompt() 635 | 636 | if pos < 0 || len(line) < pos { 637 | pos = len(line) 638 | } 639 | if len(line) > 0 { 640 | err := s.refresh(p, line, pos) 641 | if err != nil { 642 | return "", err 643 | } 644 | } 645 | 646 | restart: 647 | s.startPrompt() 648 | s.getColumns() 649 | 650 | mainLoop: 651 | for { 652 | next, err := s.readNext() 653 | haveNext: 654 | if err != nil { 655 | if s.shouldRestart != nil && s.shouldRestart(err) { 656 | goto restart 657 | } 658 | return "", err 659 | } 660 | 661 | historyAction = false 662 | switch v := next.(type) { 663 | case rune: 664 | switch v { 665 | case cr, lf: 666 | if s.needRefresh { 667 | err := s.refresh(p, line, pos) 668 | if err != nil { 669 | return "", err 670 | } 671 | } 672 | if s.multiLineMode { 673 | s.resetMultiLine(p, line, pos) 674 | } 675 | fmt.Println() 676 | break mainLoop 677 | case ctrlA: // Start of line 678 | pos = 0 679 | s.needRefresh = true 680 | case ctrlE: // End of line 681 | pos = len(line) 682 | s.needRefresh = true 683 | case ctrlB: // left 684 | if pos > 0 { 685 | pos -= len(getSuffixGlyphs(line[:pos], 1)) 686 | s.needRefresh = true 687 | } else { 688 | s.doBeep() 689 | } 690 | case ctrlF: // right 691 | if pos < len(line) { 692 | pos += len(getPrefixGlyphs(line[pos:], 1)) 693 | s.needRefresh = true 694 | } else { 695 | s.doBeep() 696 | } 697 | case ctrlD: // del 698 | if pos == 0 && len(line) == 0 { 699 | // exit 700 | return "", io.EOF 701 | } 702 | 703 | // ctrlD is a potential EOF, so the rune reader shuts down. 704 | // Therefore, if it isn't actually an EOF, we must re-startPrompt. 705 | s.restartPrompt() 706 | 707 | if pos >= len(line) { 708 | s.doBeep() 709 | } else { 710 | n := len(getPrefixGlyphs(line[pos:], 1)) 711 | line = append(line[:pos], line[pos+n:]...) 712 | s.needRefresh = true 713 | } 714 | case ctrlK: // delete remainder of line 715 | if pos >= len(line) { 716 | s.doBeep() 717 | } else { 718 | if killAction > 0 { 719 | s.addToKillRing(line[pos:], 1) // Add in apend mode 720 | } else { 721 | s.addToKillRing(line[pos:], 0) // Add in normal mode 722 | } 723 | 724 | killAction = 2 // Mark that there was a kill action 725 | line = line[:pos] 726 | s.needRefresh = true 727 | } 728 | case ctrlP: // up 729 | historyAction = true 730 | if historyStale { 731 | historyPrefix = s.getHistoryByPrefix(string(line)) 732 | historyPos = len(historyPrefix) 733 | historyStale = false 734 | } 735 | if historyPos > 0 { 736 | if historyPos == len(historyPrefix) { 737 | historyEnd = string(line) 738 | } 739 | historyPos-- 740 | line = []rune(historyPrefix[historyPos]) 741 | pos = len(line) 742 | s.needRefresh = true 743 | } else { 744 | s.doBeep() 745 | } 746 | case ctrlN: // down 747 | historyAction = true 748 | if historyStale { 749 | historyPrefix = s.getHistoryByPrefix(string(line)) 750 | historyPos = len(historyPrefix) 751 | historyStale = false 752 | } 753 | if historyPos < len(historyPrefix) { 754 | historyPos++ 755 | if historyPos == len(historyPrefix) { 756 | line = []rune(historyEnd) 757 | } else { 758 | line = []rune(historyPrefix[historyPos]) 759 | } 760 | pos = len(line) 761 | s.needRefresh = true 762 | } else { 763 | s.doBeep() 764 | } 765 | case ctrlT: // transpose prev glyph with glyph under cursor 766 | if len(line) < 2 || pos < 1 { 767 | s.doBeep() 768 | } else { 769 | if pos == len(line) { 770 | pos -= len(getSuffixGlyphs(line, 1)) 771 | } 772 | prev := getSuffixGlyphs(line[:pos], 1) 773 | next := getPrefixGlyphs(line[pos:], 1) 774 | scratch := make([]rune, len(prev)) 775 | copy(scratch, prev) 776 | copy(line[pos-len(prev):], next) 777 | copy(line[pos-len(prev)+len(next):], scratch) 778 | pos += len(next) 779 | s.needRefresh = true 780 | } 781 | case ctrlL: // clear screen 782 | s.eraseScreen() 783 | s.needRefresh = true 784 | case ctrlC: // reset 785 | fmt.Println("^C") 786 | if s.multiLineMode { 787 | s.resetMultiLine(p, line, pos) 788 | } 789 | if s.ctrlCAborts { 790 | return "", ErrPromptAborted 791 | } 792 | line = line[:0] 793 | pos = 0 794 | fmt.Print(prompt) 795 | s.restartPrompt() 796 | case ctrlH, bs: // Backspace 797 | if pos <= 0 { 798 | s.doBeep() 799 | } else { 800 | n := len(getSuffixGlyphs(line[:pos], 1)) 801 | line = append(line[:pos-n], line[pos:]...) 802 | pos -= n 803 | s.needRefresh = true 804 | } 805 | case ctrlU: // Erase line before cursor 806 | if killAction > 0 { 807 | s.addToKillRing(line[:pos], 2) // Add in prepend mode 808 | } else { 809 | s.addToKillRing(line[:pos], 0) // Add in normal mode 810 | } 811 | 812 | killAction = 2 // Mark that there was some killing 813 | line = line[pos:] 814 | pos = 0 815 | s.needRefresh = true 816 | case ctrlW: // Erase word 817 | pos, line, killAction = s.eraseWord(pos, line, killAction) 818 | case ctrlY: // Paste from Yank buffer 819 | line, pos, next, err = s.yank(p, line, pos) 820 | goto haveNext 821 | case ctrlR: // Reverse Search 822 | line, pos, next, err = s.reverseISearch(line, pos) 823 | s.needRefresh = true 824 | goto haveNext 825 | case tab: // Tab completion 826 | line, pos, next, err = s.tabComplete(p, line, pos) 827 | goto haveNext 828 | // Catch keys that do nothing, but you don't want them to beep 829 | case esc: 830 | // DO NOTHING 831 | // Unused keys 832 | case ctrlG, ctrlO, ctrlQ, ctrlS, ctrlV, ctrlX, ctrlZ: 833 | fallthrough 834 | // Catch unhandled control codes (anything <= 31) 835 | case 0, 28, 29, 30, 31: 836 | s.doBeep() 837 | default: 838 | if pos == len(line) && !s.multiLineMode && 839 | len(p)+len(line) < s.columns*4 && // Avoid countGlyphs on large lines 840 | countGlyphs(p)+countGlyphs(line) < s.columns-1 { 841 | line = append(line, v) 842 | fmt.Printf("%c", v) 843 | pos++ 844 | } else { 845 | line = append(line[:pos], append([]rune{v}, line[pos:]...)...) 846 | pos++ 847 | s.needRefresh = true 848 | } 849 | } 850 | case action: 851 | switch v { 852 | case del: 853 | if pos >= len(line) { 854 | s.doBeep() 855 | } else { 856 | n := len(getPrefixGlyphs(line[pos:], 1)) 857 | line = append(line[:pos], line[pos+n:]...) 858 | } 859 | case left: 860 | if pos > 0 { 861 | pos -= len(getSuffixGlyphs(line[:pos], 1)) 862 | } else { 863 | s.doBeep() 864 | } 865 | case wordLeft, altB: 866 | if pos > 0 { 867 | var spaceHere, spaceLeft, leftKnown bool 868 | for { 869 | pos-- 870 | if pos == 0 { 871 | break 872 | } 873 | if leftKnown { 874 | spaceHere = spaceLeft 875 | } else { 876 | spaceHere = unicode.IsSpace(line[pos]) 877 | } 878 | spaceLeft, leftKnown = unicode.IsSpace(line[pos-1]), true 879 | if !spaceHere && spaceLeft { 880 | break 881 | } 882 | } 883 | } else { 884 | s.doBeep() 885 | } 886 | case right: 887 | if pos < len(line) { 888 | pos += len(getPrefixGlyphs(line[pos:], 1)) 889 | } else { 890 | s.doBeep() 891 | } 892 | case wordRight, altF: 893 | if pos < len(line) { 894 | var spaceHere, spaceLeft, hereKnown bool 895 | for { 896 | pos++ 897 | if pos == len(line) { 898 | break 899 | } 900 | if hereKnown { 901 | spaceLeft = spaceHere 902 | } else { 903 | spaceLeft = unicode.IsSpace(line[pos-1]) 904 | } 905 | spaceHere, hereKnown = unicode.IsSpace(line[pos]), true 906 | if spaceHere && !spaceLeft { 907 | break 908 | } 909 | } 910 | } else { 911 | s.doBeep() 912 | } 913 | case up: 914 | historyAction = true 915 | if historyStale { 916 | historyPrefix = s.getHistoryByPrefix(string(line)) 917 | historyPos = len(historyPrefix) 918 | historyStale = false 919 | } 920 | if historyPos > 0 { 921 | if historyPos == len(historyPrefix) { 922 | historyEnd = string(line) 923 | } 924 | historyPos-- 925 | line = []rune(historyPrefix[historyPos]) 926 | pos = len(line) 927 | } else { 928 | s.doBeep() 929 | } 930 | case down: 931 | historyAction = true 932 | if historyStale { 933 | historyPrefix = s.getHistoryByPrefix(string(line)) 934 | historyPos = len(historyPrefix) 935 | historyStale = false 936 | } 937 | if historyPos < len(historyPrefix) { 938 | historyPos++ 939 | if historyPos == len(historyPrefix) { 940 | line = []rune(historyEnd) 941 | } else { 942 | line = []rune(historyPrefix[historyPos]) 943 | } 944 | pos = len(line) 945 | } else { 946 | s.doBeep() 947 | } 948 | case home: // Start of line 949 | pos = 0 950 | case end: // End of line 951 | pos = len(line) 952 | case altD: // Delete next word 953 | if pos == len(line) { 954 | s.doBeep() 955 | break 956 | } 957 | // Remove whitespace to the right 958 | var buf []rune // Store the deleted chars in a buffer 959 | for { 960 | if pos == len(line) || !unicode.IsSpace(line[pos]) { 961 | break 962 | } 963 | buf = append(buf, line[pos]) 964 | line = append(line[:pos], line[pos+1:]...) 965 | } 966 | // Remove non-whitespace to the right 967 | for { 968 | if pos == len(line) || unicode.IsSpace(line[pos]) { 969 | break 970 | } 971 | buf = append(buf, line[pos]) 972 | line = append(line[:pos], line[pos+1:]...) 973 | } 974 | // Save the result on the killRing 975 | if killAction > 0 { 976 | s.addToKillRing(buf, 2) // Add in prepend mode 977 | } else { 978 | s.addToKillRing(buf, 0) // Add in normal mode 979 | } 980 | killAction = 2 // Mark that there was some killing 981 | case altBs: // Erase word 982 | pos, line, killAction = s.eraseWord(pos, line, killAction) 983 | case winch: // Window change 984 | if s.multiLineMode { 985 | if s.maxRows-s.cursorRows > 0 { 986 | s.moveDown(s.maxRows - s.cursorRows) 987 | } 988 | for i := 0; i < s.maxRows-1; i++ { 989 | s.cursorPos(0) 990 | s.eraseLine() 991 | s.moveUp(1) 992 | } 993 | s.maxRows = 1 994 | s.cursorRows = 1 995 | } 996 | } 997 | s.needRefresh = true 998 | } 999 | if s.needRefresh && !s.inputWaiting() { 1000 | err := s.refresh(p, line, pos) 1001 | if err != nil { 1002 | return "", err 1003 | } 1004 | } 1005 | if !historyAction { 1006 | historyStale = true 1007 | } 1008 | if killAction > 0 { 1009 | killAction-- 1010 | } 1011 | } 1012 | return string(line), nil 1013 | } 1014 | 1015 | // PasswordPrompt displays p, and then waits for user input. The input typed by 1016 | // the user is not displayed in the terminal. 1017 | func (s *State) PasswordPrompt(prompt string) (string, error) { 1018 | for _, r := range prompt { 1019 | if unicode.Is(unicode.C, r) { 1020 | return "", ErrInvalidPrompt 1021 | } 1022 | } 1023 | if !s.terminalSupported || s.columns == 0 { 1024 | return "", errors.New("liner: function not supported in this terminal") 1025 | } 1026 | if s.inputRedirected { 1027 | return s.promptUnsupported(prompt) 1028 | } 1029 | if s.outputRedirected { 1030 | return "", ErrNotTerminalOutput 1031 | } 1032 | 1033 | p := []rune(prompt) 1034 | 1035 | defer s.stopPrompt() 1036 | 1037 | restart: 1038 | s.startPrompt() 1039 | s.getColumns() 1040 | 1041 | fmt.Print(prompt) 1042 | var line []rune 1043 | pos := 0 1044 | 1045 | mainLoop: 1046 | for { 1047 | next, err := s.readNext() 1048 | if err != nil { 1049 | if s.shouldRestart != nil && s.shouldRestart(err) { 1050 | goto restart 1051 | } 1052 | return "", err 1053 | } 1054 | 1055 | switch v := next.(type) { 1056 | case rune: 1057 | switch v { 1058 | case cr, lf: 1059 | fmt.Println() 1060 | break mainLoop 1061 | case ctrlD: // del 1062 | if pos == 0 && len(line) == 0 { 1063 | // exit 1064 | return "", io.EOF 1065 | } 1066 | 1067 | // ctrlD is a potential EOF, so the rune reader shuts down. 1068 | // Therefore, if it isn't actually an EOF, we must re-startPrompt. 1069 | s.restartPrompt() 1070 | case ctrlL: // clear screen 1071 | s.eraseScreen() 1072 | err := s.refresh(p, []rune{}, 0) 1073 | if err != nil { 1074 | return "", err 1075 | } 1076 | case ctrlH, bs: // Backspace 1077 | if pos <= 0 { 1078 | s.doBeep() 1079 | } else { 1080 | n := len(getSuffixGlyphs(line[:pos], 1)) 1081 | line = append(line[:pos-n], line[pos:]...) 1082 | pos -= n 1083 | } 1084 | case ctrlC: 1085 | fmt.Println("^C") 1086 | if s.ctrlCAborts { 1087 | return "", ErrPromptAborted 1088 | } 1089 | line = line[:0] 1090 | pos = 0 1091 | fmt.Print(prompt) 1092 | s.restartPrompt() 1093 | // Unused keys 1094 | case esc, tab, ctrlA, ctrlB, ctrlE, ctrlF, ctrlG, ctrlK, ctrlN, ctrlO, ctrlP, ctrlQ, ctrlR, ctrlS, 1095 | ctrlT, ctrlU, ctrlV, ctrlW, ctrlX, ctrlY, ctrlZ: 1096 | fallthrough 1097 | // Catch unhandled control codes (anything <= 31) 1098 | case 0, 28, 29, 30, 31: 1099 | s.doBeep() 1100 | default: 1101 | line = append(line[:pos], append([]rune{v}, line[pos:]...)...) 1102 | pos++ 1103 | } 1104 | } 1105 | } 1106 | return string(line), nil 1107 | } 1108 | 1109 | func (s *State) tooNarrow(prompt string) (string, error) { 1110 | // Docker and OpenWRT and etc sometimes return 0 column width 1111 | // Reset mode temporarily. Restore baked mode in case the terminal 1112 | // is wide enough for the next Prompt attempt. 1113 | m, merr := TerminalMode() 1114 | s.origMode.ApplyMode() 1115 | if merr == nil { 1116 | defer m.ApplyMode() 1117 | } 1118 | if s.r == nil { 1119 | // Windows does not always set s.r 1120 | s.r = bufio.NewReader(os.Stdin) 1121 | defer func() { s.r = nil }() 1122 | } 1123 | return s.promptUnsupported(prompt) 1124 | } 1125 | 1126 | func (s *State) eraseWord(pos int, line []rune, killAction int) (int, []rune, int) { 1127 | if pos == 0 { 1128 | s.doBeep() 1129 | return pos, line, killAction 1130 | } 1131 | // Remove whitespace to the left 1132 | var buf []rune // Store the deleted chars in a buffer 1133 | for { 1134 | if pos == 0 || !unicode.IsSpace(line[pos-1]) { 1135 | break 1136 | } 1137 | buf = append(buf, line[pos-1]) 1138 | line = append(line[:pos-1], line[pos:]...) 1139 | pos-- 1140 | } 1141 | // Remove non-whitespace to the left 1142 | for { 1143 | if pos == 0 || unicode.IsSpace(line[pos-1]) { 1144 | break 1145 | } 1146 | buf = append(buf, line[pos-1]) 1147 | line = append(line[:pos-1], line[pos:]...) 1148 | pos-- 1149 | } 1150 | // Invert the buffer and save the result on the killRing 1151 | var newBuf []rune 1152 | for i := len(buf) - 1; i >= 0; i-- { 1153 | newBuf = append(newBuf, buf[i]) 1154 | } 1155 | if killAction > 0 { 1156 | s.addToKillRing(newBuf, 2) // Add in prepend mode 1157 | } else { 1158 | s.addToKillRing(newBuf, 0) // Add in normal mode 1159 | } 1160 | killAction = 2 // Mark that there was some killing 1161 | 1162 | s.needRefresh = true 1163 | return pos, line, killAction 1164 | } 1165 | 1166 | func (s *State) doBeep() { 1167 | if !s.noBeep { 1168 | fmt.Print(beep) 1169 | } 1170 | } 1171 | -------------------------------------------------------------------------------- /line_test.go: -------------------------------------------------------------------------------- 1 | package liner 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "strings" 7 | "testing" 8 | ) 9 | 10 | func TestAppend(t *testing.T) { 11 | var s State 12 | s.AppendHistory("foo") 13 | s.AppendHistory("bar") 14 | 15 | var out bytes.Buffer 16 | num, err := s.WriteHistory(&out) 17 | if err != nil { 18 | t.Fatal("Unexpected error writing history", err) 19 | } 20 | if num != 2 { 21 | t.Fatalf("Expected 2 history entries, got %d", num) 22 | } 23 | 24 | s.AppendHistory("baz") 25 | num, err = s.WriteHistory(&out) 26 | if err != nil { 27 | t.Fatal("Unexpected error writing history", err) 28 | } 29 | if num != 3 { 30 | t.Fatalf("Expected 3 history entries, got %d", num) 31 | } 32 | 33 | s.AppendHistory("baz") 34 | num, err = s.WriteHistory(&out) 35 | if err != nil { 36 | t.Fatal("Unexpected error writing history", err) 37 | } 38 | if num != 3 { 39 | t.Fatalf("Expected 3 history entries after duplicate append, got %d", num) 40 | } 41 | 42 | s.AppendHistory("baz") 43 | 44 | } 45 | 46 | func TestHistory(t *testing.T) { 47 | input := `foo 48 | bar 49 | baz 50 | quux 51 | dingle` 52 | 53 | var s State 54 | num, err := s.ReadHistory(strings.NewReader(input)) 55 | if err != nil { 56 | t.Fatal("Unexpected error reading history", err) 57 | } 58 | if num != 5 { 59 | t.Fatal("Wrong number of history entries read") 60 | } 61 | 62 | var out bytes.Buffer 63 | num, err = s.WriteHistory(&out) 64 | if err != nil { 65 | t.Fatal("Unexpected error writing history", err) 66 | } 67 | if num != 5 { 68 | t.Fatal("Wrong number of history entries written") 69 | } 70 | if strings.TrimSpace(out.String()) != input { 71 | t.Fatal("Round-trip failure") 72 | } 73 | 74 | // clear the history and re-write 75 | s.ClearHistory() 76 | num, err = s.WriteHistory(&out) 77 | if err != nil { 78 | t.Fatal("Unexpected error writing history", err) 79 | } 80 | if num != 0 { 81 | t.Fatal("Wrong number of history entries written, expected none") 82 | } 83 | // Test reading with a trailing newline present 84 | var s2 State 85 | num, err = s2.ReadHistory(&out) 86 | if err != nil { 87 | t.Fatal("Unexpected error reading history the 2nd time", err) 88 | } 89 | if num != 5 { 90 | t.Fatal("Wrong number of history entries read the 2nd time") 91 | } 92 | 93 | num, err = s.ReadHistory(strings.NewReader(input + "\n\xff")) 94 | if err == nil { 95 | t.Fatal("Unexpected success reading corrupted history", err) 96 | } 97 | if num != 5 { 98 | t.Fatal("Wrong number of history entries read the 3rd time") 99 | } 100 | } 101 | 102 | func TestColumns(t *testing.T) { 103 | list := []string{"foo", "food", "This entry is quite a bit longer than the typical entry"} 104 | 105 | output := []struct { 106 | width, columns, rows, maxWidth int 107 | }{ 108 | {80, 1, 3, len(list[2]) + 1}, 109 | {120, 2, 2, len(list[2]) + 1}, 110 | {800, 14, 1, 0}, 111 | {8, 1, 3, 7}, 112 | } 113 | 114 | for i, o := range output { 115 | col, row, max := calculateColumns(o.width, list) 116 | if col != o.columns { 117 | t.Fatalf("Wrong number of columns, %d != %d, in TestColumns %d\n", col, o.columns, i) 118 | } 119 | if row != o.rows { 120 | t.Fatalf("Wrong number of rows, %d != %d, in TestColumns %d\n", row, o.rows, i) 121 | } 122 | if max != o.maxWidth { 123 | t.Fatalf("Wrong column width, %d != %d, in TestColumns %d\n", max, o.maxWidth, i) 124 | } 125 | } 126 | } 127 | 128 | // This example demonstrates a way to retrieve the current 129 | // history buffer without using a file. 130 | func ExampleState_WriteHistory() { 131 | var s State 132 | s.AppendHistory("foo") 133 | s.AppendHistory("bar") 134 | 135 | buf := new(bytes.Buffer) 136 | _, err := s.WriteHistory(buf) 137 | if err == nil { 138 | history := strings.Split(strings.TrimSpace(buf.String()), "\n") 139 | for i, line := range history { 140 | fmt.Println("History entry", i, ":", line) 141 | } 142 | } 143 | // Output: 144 | // History entry 0 : foo 145 | // History entry 1 : bar 146 | } 147 | -------------------------------------------------------------------------------- /output.go: -------------------------------------------------------------------------------- 1 | //go:build linux || darwin || openbsd || freebsd || netbsd || solaris 2 | // +build linux darwin openbsd freebsd netbsd solaris 3 | 4 | package liner 5 | 6 | import ( 7 | "fmt" 8 | "os" 9 | "strings" 10 | ) 11 | 12 | func (s *State) cursorPos(x int) { 13 | if s.useCHA { 14 | // 'G' is "Cursor Character Absolute (CHA)" 15 | fmt.Printf("\x1b[%dG", x+1) 16 | } else { 17 | // 'C' is "Cursor Forward (CUF)" 18 | fmt.Print("\r") 19 | if x > 0 { 20 | fmt.Printf("\x1b[%dC", x) 21 | } 22 | } 23 | } 24 | 25 | func (s *State) eraseLine() { 26 | fmt.Print("\x1b[0K") 27 | } 28 | 29 | func (s *State) eraseScreen() { 30 | fmt.Print("\x1b[H\x1b[2J") 31 | } 32 | 33 | func (s *State) moveUp(lines int) { 34 | fmt.Printf("\x1b[%dA", lines) 35 | } 36 | 37 | func (s *State) moveDown(lines int) { 38 | fmt.Printf("\x1b[%dB", lines) 39 | } 40 | 41 | func (s *State) emitNewLine() { 42 | fmt.Print("\n") 43 | } 44 | 45 | type winSize struct { 46 | row, col uint16 47 | xpixel, ypixel uint16 48 | } 49 | 50 | func (s *State) checkOutput() { 51 | // xterm is known to support CHA 52 | if strings.Contains(strings.ToLower(os.Getenv("TERM")), "xterm") { 53 | s.useCHA = true 54 | return 55 | } 56 | 57 | // The test for functional ANSI CHA is unreliable (eg the Windows 58 | // telnet command does not support reading the cursor position with 59 | // an ANSI DSR request, despite setting TERM=ansi) 60 | 61 | // Assume CHA isn't supported (which should be safe, although it 62 | // does result in occasional visible cursor jitter) 63 | s.useCHA = false 64 | } 65 | -------------------------------------------------------------------------------- /output_solaris.go: -------------------------------------------------------------------------------- 1 | package liner 2 | 3 | import ( 4 | "golang.org/x/sys/unix" 5 | ) 6 | 7 | func (s *State) getColumns() bool { 8 | ws, err := unix.IoctlGetWinsize(unix.Stdout, unix.TIOCGWINSZ) 9 | if err != nil { 10 | return false 11 | } 12 | s.columns = int(ws.Col) 13 | return true 14 | } 15 | -------------------------------------------------------------------------------- /output_unix.go: -------------------------------------------------------------------------------- 1 | //go:build linux || darwin || openbsd || freebsd || netbsd 2 | // +build linux darwin openbsd freebsd netbsd 3 | 4 | package liner 5 | 6 | import ( 7 | "syscall" 8 | "unsafe" 9 | ) 10 | 11 | func (s *State) getColumns() bool { 12 | var ws winSize 13 | ok, _, _ := syscall.Syscall(syscall.SYS_IOCTL, uintptr(syscall.Stdout), 14 | syscall.TIOCGWINSZ, uintptr(unsafe.Pointer(&ws))) 15 | if int(ok) < 0 { 16 | return false 17 | } 18 | s.columns = int(ws.col) 19 | return true 20 | } 21 | -------------------------------------------------------------------------------- /output_windows.go: -------------------------------------------------------------------------------- 1 | package liner 2 | 3 | import ( 4 | "unsafe" 5 | ) 6 | 7 | type coord struct { 8 | x, y int16 9 | } 10 | type smallRect struct { 11 | left, top, right, bottom int16 12 | } 13 | 14 | type consoleScreenBufferInfo struct { 15 | dwSize coord 16 | dwCursorPosition coord 17 | wAttributes int16 18 | srWindow smallRect 19 | dwMaximumWindowSize coord 20 | } 21 | 22 | func (s *State) cursorPos(x int) { 23 | var sbi consoleScreenBufferInfo 24 | procGetConsoleScreenBufferInfo.Call(uintptr(s.hOut), uintptr(unsafe.Pointer(&sbi))) 25 | procSetConsoleCursorPosition.Call(uintptr(s.hOut), 26 | uintptr(int(x)&0xFFFF|int(sbi.dwCursorPosition.y)<<16)) 27 | } 28 | 29 | func (s *State) eraseLine() { 30 | var sbi consoleScreenBufferInfo 31 | procGetConsoleScreenBufferInfo.Call(uintptr(s.hOut), uintptr(unsafe.Pointer(&sbi))) 32 | var numWritten uint32 33 | procFillConsoleOutputCharacter.Call(uintptr(s.hOut), uintptr(' '), 34 | uintptr(sbi.dwSize.x-sbi.dwCursorPosition.x), 35 | uintptr(int(sbi.dwCursorPosition.x)&0xFFFF|int(sbi.dwCursorPosition.y)<<16), 36 | uintptr(unsafe.Pointer(&numWritten))) 37 | } 38 | 39 | func (s *State) eraseScreen() { 40 | var sbi consoleScreenBufferInfo 41 | procGetConsoleScreenBufferInfo.Call(uintptr(s.hOut), uintptr(unsafe.Pointer(&sbi))) 42 | var numWritten uint32 43 | procFillConsoleOutputCharacter.Call(uintptr(s.hOut), uintptr(' '), 44 | uintptr(sbi.dwSize.x)*uintptr(sbi.dwSize.y), 45 | 0, 46 | uintptr(unsafe.Pointer(&numWritten))) 47 | procSetConsoleCursorPosition.Call(uintptr(s.hOut), 0) 48 | } 49 | 50 | func (s *State) moveUp(lines int) { 51 | var sbi consoleScreenBufferInfo 52 | procGetConsoleScreenBufferInfo.Call(uintptr(s.hOut), uintptr(unsafe.Pointer(&sbi))) 53 | procSetConsoleCursorPosition.Call(uintptr(s.hOut), 54 | uintptr(int(sbi.dwCursorPosition.x)&0xFFFF|(int(sbi.dwCursorPosition.y)-lines)<<16)) 55 | } 56 | 57 | func (s *State) moveDown(lines int) { 58 | var sbi consoleScreenBufferInfo 59 | procGetConsoleScreenBufferInfo.Call(uintptr(s.hOut), uintptr(unsafe.Pointer(&sbi))) 60 | procSetConsoleCursorPosition.Call(uintptr(s.hOut), 61 | uintptr(int(sbi.dwCursorPosition.x)&0xFFFF|(int(sbi.dwCursorPosition.y)+lines)<<16)) 62 | } 63 | 64 | func (s *State) emitNewLine() { 65 | // windows doesn't need to omit a new line 66 | } 67 | 68 | func (s *State) getColumns() { 69 | var sbi consoleScreenBufferInfo 70 | procGetConsoleScreenBufferInfo.Call(uintptr(s.hOut), uintptr(unsafe.Pointer(&sbi))) 71 | s.columns = int(sbi.dwSize.x) 72 | } 73 | -------------------------------------------------------------------------------- /prefix_test.go: -------------------------------------------------------------------------------- 1 | //go:build windows || linux || darwin || openbsd || freebsd || netbsd || solaris 2 | // +build windows linux darwin openbsd freebsd netbsd solaris 3 | 4 | package liner 5 | 6 | import "testing" 7 | 8 | type testItem struct { 9 | list []string 10 | prefix string 11 | } 12 | 13 | func TestPrefix(t *testing.T) { 14 | list := []testItem{ 15 | {[]string{"food", "foot"}, "foo"}, 16 | {[]string{"foo", "foot"}, "foo"}, 17 | {[]string{"food", "foo"}, "foo"}, 18 | {[]string{"food", "foe", "foot"}, "fo"}, 19 | {[]string{"food", "foot", "barbeque"}, ""}, 20 | {[]string{"cafeteria", "café"}, "caf"}, 21 | {[]string{"cafe", "café"}, "caf"}, 22 | {[]string{"cafè", "café"}, "caf"}, 23 | {[]string{"cafés", "café"}, "café"}, 24 | {[]string{"áéíóú", "áéíóú"}, "áéíóú"}, 25 | {[]string{"éclairs", "éclairs"}, "éclairs"}, 26 | {[]string{"éclairs are the best", "éclairs are great", "éclairs"}, "éclairs"}, 27 | {[]string{"éclair", "éclairs"}, "éclair"}, 28 | {[]string{"éclairs", "éclair"}, "éclair"}, 29 | {[]string{"éclair", "élan"}, "é"}, 30 | } 31 | 32 | for _, test := range list { 33 | lcp := longestCommonPrefix(test.list) 34 | if lcp != test.prefix { 35 | t.Errorf("%s != %s for %+v", lcp, test.prefix, test.list) 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /race_test.go: -------------------------------------------------------------------------------- 1 | //go:build race 2 | // +build race 3 | 4 | package liner 5 | 6 | import ( 7 | "io/ioutil" 8 | "os" 9 | "sync" 10 | "testing" 11 | ) 12 | 13 | func TestWriteHistory(t *testing.T) { 14 | oldout := os.Stdout 15 | defer func() { os.Stdout = oldout }() 16 | oldin := os.Stdout 17 | defer func() { os.Stdin = oldin }() 18 | 19 | newinr, newinw, err := os.Pipe() 20 | if err != nil { 21 | t.Fatal(err) 22 | } 23 | os.Stdin = newinr 24 | newoutr, newoutw, err := os.Pipe() 25 | if err != nil { 26 | t.Fatal(err) 27 | } 28 | defer newoutr.Close() 29 | os.Stdout = newoutw 30 | 31 | var wait sync.WaitGroup 32 | wait.Add(1) 33 | s := NewLiner() 34 | go func() { 35 | s.AppendHistory("foo") 36 | s.AppendHistory("bar") 37 | s.Prompt("") 38 | wait.Done() 39 | }() 40 | 41 | s.WriteHistory(ioutil.Discard) 42 | 43 | newinw.Close() 44 | wait.Wait() 45 | } 46 | -------------------------------------------------------------------------------- /unixmode.go: -------------------------------------------------------------------------------- 1 | //go:build linux || darwin || freebsd || openbsd || netbsd 2 | // +build linux darwin freebsd openbsd netbsd 3 | 4 | package liner 5 | 6 | import ( 7 | "syscall" 8 | "unsafe" 9 | ) 10 | 11 | func (mode *termios) ApplyMode() error { 12 | _, _, errno := syscall.Syscall(syscall.SYS_IOCTL, uintptr(syscall.Stdin), setTermios, uintptr(unsafe.Pointer(mode))) 13 | 14 | if errno != 0 { 15 | return errno 16 | } 17 | return nil 18 | } 19 | 20 | // TerminalMode returns the current terminal input mode as an InputModeSetter. 21 | // 22 | // This function is provided for convenience, and should 23 | // not be necessary for most users of liner. 24 | func TerminalMode() (ModeApplier, error) { 25 | return getMode(syscall.Stdin) 26 | } 27 | 28 | func getMode(handle int) (*termios, error) { 29 | var mode termios 30 | var err error 31 | _, _, errno := syscall.Syscall(syscall.SYS_IOCTL, uintptr(handle), getTermios, uintptr(unsafe.Pointer(&mode))) 32 | if errno != 0 { 33 | err = errno 34 | } 35 | 36 | return &mode, err 37 | } 38 | -------------------------------------------------------------------------------- /unixmode_solaris.go: -------------------------------------------------------------------------------- 1 | package liner 2 | 3 | import ( 4 | "golang.org/x/sys/unix" 5 | ) 6 | 7 | func (mode *termios) ApplyMode() error { 8 | return unix.IoctlSetTermios(unix.Stdin, setTermios, (*unix.Termios)(mode)) 9 | } 10 | 11 | // TerminalMode returns the current terminal input mode as an InputModeSetter. 12 | // 13 | // This function is provided for convenience, and should 14 | // not be necessary for most users of liner. 15 | func TerminalMode() (ModeApplier, error) { 16 | return getMode(unix.Stdin) 17 | } 18 | 19 | func getMode(handle int) (*termios, error) { 20 | tos, err := unix.IoctlGetTermios(handle, getTermios) 21 | return (*termios)(tos), err 22 | } 23 | -------------------------------------------------------------------------------- /width.go: -------------------------------------------------------------------------------- 1 | package liner 2 | 3 | import ( 4 | "unicode" 5 | 6 | "github.com/mattn/go-runewidth" 7 | ) 8 | 9 | // These character classes are mostly zero width (when combined). 10 | // A few might not be, depending on the user's font. Fixing this 11 | // is non-trivial, given that some terminals don't support 12 | // ANSI DSR/CPR 13 | var zeroWidth = []*unicode.RangeTable{ 14 | unicode.Mn, 15 | unicode.Me, 16 | unicode.Cc, 17 | unicode.Cf, 18 | } 19 | 20 | // countGlyphs considers zero-width characters to be zero glyphs wide, 21 | // and members of Chinese, Japanese, and Korean scripts to be 2 glyphs wide. 22 | func countGlyphs(s []rune) int { 23 | n := 0 24 | for _, r := range s { 25 | // speed up the common case 26 | if r < 127 { 27 | n++ 28 | continue 29 | } 30 | 31 | n += runewidth.RuneWidth(r) 32 | } 33 | return n 34 | } 35 | 36 | func countMultiLineGlyphs(s []rune, columns int, start int) int { 37 | n := start 38 | for _, r := range s { 39 | if r < 127 { 40 | n++ 41 | continue 42 | } 43 | switch runewidth.RuneWidth(r) { 44 | case 0: 45 | case 1: 46 | n++ 47 | case 2: 48 | n += 2 49 | // no room for a 2-glyphs-wide char in the ending 50 | // so skip a column and display it at the beginning 51 | if n%columns == 1 { 52 | n++ 53 | } 54 | } 55 | } 56 | return n 57 | } 58 | 59 | func getPrefixGlyphs(s []rune, num int) []rune { 60 | p := 0 61 | for n := 0; n < num && p < len(s); p++ { 62 | // speed up the common case 63 | if s[p] < 127 { 64 | n++ 65 | continue 66 | } 67 | if !unicode.IsOneOf(zeroWidth, s[p]) { 68 | n++ 69 | } 70 | } 71 | for p < len(s) && unicode.IsOneOf(zeroWidth, s[p]) { 72 | p++ 73 | } 74 | return s[:p] 75 | } 76 | 77 | func getSuffixGlyphs(s []rune, num int) []rune { 78 | p := len(s) 79 | for n := 0; n < num && p > 0; p-- { 80 | // speed up the common case 81 | if s[p-1] < 127 { 82 | n++ 83 | continue 84 | } 85 | if !unicode.IsOneOf(zeroWidth, s[p-1]) { 86 | n++ 87 | } 88 | } 89 | return s[p:] 90 | } 91 | -------------------------------------------------------------------------------- /width_test.go: -------------------------------------------------------------------------------- 1 | package liner 2 | 3 | import ( 4 | "strconv" 5 | "testing" 6 | ) 7 | 8 | func accent(in []rune) []rune { 9 | var out []rune 10 | for _, r := range in { 11 | out = append(out, r) 12 | out = append(out, '\u0301') 13 | } 14 | return out 15 | } 16 | 17 | type testCase struct { 18 | s []rune 19 | glyphs int 20 | } 21 | 22 | var testCases = []testCase{ 23 | {[]rune("query"), 5}, 24 | {[]rune("私"), 2}, 25 | {[]rune("hello『世界』"), 13}, 26 | } 27 | 28 | func TestCountGlyphs(t *testing.T) { 29 | for _, testCase := range testCases { 30 | count := countGlyphs(testCase.s) 31 | if count != testCase.glyphs { 32 | t.Errorf("ASCII count incorrect. %d != %d", count, testCase.glyphs) 33 | } 34 | count = countGlyphs(accent(testCase.s)) 35 | if count != testCase.glyphs { 36 | t.Errorf("Accent count incorrect. %d != %d", count, testCase.glyphs) 37 | } 38 | } 39 | } 40 | 41 | func compare(a, b []rune, name string, t *testing.T) { 42 | if len(a) != len(b) { 43 | t.Errorf(`"%s" != "%s" in %s"`, string(a), string(b), name) 44 | return 45 | } 46 | for i := range a { 47 | if a[i] != b[i] { 48 | t.Errorf(`"%s" != "%s" in %s"`, string(a), string(b), name) 49 | return 50 | } 51 | } 52 | } 53 | 54 | func TestPrefixGlyphs(t *testing.T) { 55 | for _, testCase := range testCases { 56 | for i := 0; i <= len(testCase.s); i++ { 57 | iter := strconv.Itoa(i) 58 | out := getPrefixGlyphs(testCase.s, i) 59 | compare(out, testCase.s[:i], "ascii prefix "+iter, t) 60 | out = getPrefixGlyphs(accent(testCase.s), i) 61 | compare(out, accent(testCase.s[:i]), "accent prefix "+iter, t) 62 | } 63 | out := getPrefixGlyphs(testCase.s, 999) 64 | compare(out, testCase.s, "ascii prefix overflow", t) 65 | out = getPrefixGlyphs(accent(testCase.s), 999) 66 | compare(out, accent(testCase.s), "accent prefix overflow", t) 67 | 68 | out = getPrefixGlyphs(testCase.s, -3) 69 | if len(out) != 0 { 70 | t.Error("ascii prefix negative") 71 | } 72 | out = getPrefixGlyphs(accent(testCase.s), -3) 73 | if len(out) != 0 { 74 | t.Error("accent prefix negative") 75 | } 76 | } 77 | } 78 | 79 | func TestSuffixGlyphs(t *testing.T) { 80 | for _, testCase := range testCases { 81 | for i := 0; i <= len(testCase.s); i++ { 82 | iter := strconv.Itoa(i) 83 | out := getSuffixGlyphs(testCase.s, i) 84 | compare(out, testCase.s[len(testCase.s)-i:], "ascii suffix "+iter, t) 85 | out = getSuffixGlyphs(accent(testCase.s), i) 86 | compare(out, accent(testCase.s[len(testCase.s)-i:]), "accent suffix "+iter, t) 87 | } 88 | out := getSuffixGlyphs(testCase.s, 999) 89 | compare(out, testCase.s, "ascii suffix overflow", t) 90 | out = getSuffixGlyphs(accent(testCase.s), 999) 91 | compare(out, accent(testCase.s), "accent suffix overflow", t) 92 | 93 | out = getSuffixGlyphs(testCase.s, -3) 94 | if len(out) != 0 { 95 | t.Error("ascii suffix negative") 96 | } 97 | out = getSuffixGlyphs(accent(testCase.s), -3) 98 | if len(out) != 0 { 99 | t.Error("accent suffix negative") 100 | } 101 | } 102 | } 103 | --------------------------------------------------------------------------------