├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── absprompt.go ├── bytes_to_key.go ├── dumb_terminal.go ├── example ├── absprompt.go ├── dumb.go ├── goterm.go ├── keyreport.go ├── prompt.go └── simple.go ├── go.mod ├── key_constants.go ├── keys.go ├── line.go ├── prompt.go ├── reader.go ├── reader_test.go ├── runes_differ.go ├── runes_differ_test.go ├── scroller.go ├── terminal.go ├── terminal_test.go ├── util.go ├── util_bsd.go ├── util_linux.go ├── util_plan9.go ├── util_solaris.go └── util_windows.go /.gitignore: -------------------------------------------------------------------------------- 1 | /bin 2 | /example/actually_simple.go 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Portions Copyright (c) 2009- The Go Authors. All rights reserved. 2 | Portions Copyright (c) 2016- Jeremy Echols. All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are 6 | met: 7 | 8 | * Redistributions of source code must retain the above copyright 9 | notice, this list of conditions and the following disclaimer. 10 | * Redistributions in binary form must reproduce the above 11 | copyright notice, this list of conditions and the following disclaimer 12 | in the documentation and/or other materials provided with the 13 | distribution. 14 | * Neither the name of Google Inc. nor the names of its 15 | contributors may be used to endorse or promote products derived from 16 | this software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 19 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 20 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 21 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 22 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 23 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 24 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 25 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 26 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 27 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: all build fmt clean test 2 | 3 | # This builds everything except the goterm binary since that relies on external 4 | # packages which we don't need for this project specifically 5 | all: build bin/keyreport bin/absprompt bin/simple bin/dumb bin/prompt bin/actually_simple 6 | 7 | SRCS = *.go 8 | 9 | bin/keyreport: $(SRCS) example/keyreport.go 10 | go build -o bin/keyreport example/keyreport.go 11 | 12 | bin/absprompt: $(SRCS) example/absprompt.go 13 | go build -o bin/absprompt example/absprompt.go 14 | 15 | bin/prompt: $(SRCS) example/prompt.go 16 | go build -o bin/prompt example/prompt.go 17 | 18 | bin/simple: $(SRCS) example/simple.go 19 | go build -o bin/simple example/simple.go 20 | 21 | bin/dumb: $(SRCS) example/dumb.go 22 | go build -o bin/dumb example/dumb.go 23 | 24 | example/actually_simple.go: terminal_test.go 25 | cp terminal_test.go example/actually_simple.go 26 | sed -i "s|package terminal_test|package main|" example/actually_simple.go 27 | sed -i "s|func Example|func main|" example/actually_simple.go 28 | 29 | bin/actually_simple: $(SRCS) example/actually_simple.go 30 | go build -o bin/actually_simple example/actually_simple.go 31 | 32 | bin/goterm: $(SRCS) example/goterm.go 33 | go build -tags goterm -o bin/goterm example/goterm.go 34 | 35 | build: 36 | mkdir -p bin/ 37 | go build 38 | 39 | fmt: 40 | find . -name "*.go" | xargs gofmt -l -w -s 41 | 42 | clean: 43 | rm bin -rf 44 | 45 | test: 46 | go test . 47 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Terminal 2 | === 3 | 4 | This is a highly modified mini-fork of https://github.com/golang/crypto to 5 | create a more standalone terminal reader that gives more power to the app, adds 6 | more recognized key sequences, and leaves the output to the app. 7 | 8 | [See the godoc documentation](https://godoc.org/github.com/Nerdmaster/terminal) 9 | for complete API docs. 10 | 11 | Features 12 | --- 13 | 14 | - Completely standalone key / line reader: 15 | - Unlike the Go ssh/terminal package, this is pretty simple (no inclusion of 16 | all those other crypto libraries) 17 | - Unlike many all-in-one terminal packages (like termbox), this uses 18 | io.Reader instead of forcing raw terminal access, so you can listen to an 19 | SSH socket, read from a raw terminal, or convert any arbitrary stream of 20 | raw bytes to keystrokes 21 | - Unlike just about every key-reader I found, you're not tied to a specific 22 | output approach; this terminal package is designed to be output-agnostic. 23 | - Parses a wide variety of keys, tested in Windows and Linux, over ssh 24 | connections and local terminals 25 | - Handles unknown sequences without user getting "stuck" (after accidentally 26 | hitting Alt+left-square-bracket, for instance) 27 | - OnKeypress callback for handling more than just autocomplete-style situations 28 | - AfterKeypress callback for firing off events after the built-in processing 29 | has already occurred 30 | 31 | Readers 32 | --- 33 | 34 | This package contains multiple ways to gather input from a user: 35 | 36 | ### terminal.Reader 37 | 38 | `terminal.Reader` is very similar to the ssh terminal in Go's crypto package 39 | except that it doesn't do any output. It's useful for gathering input from a 40 | user in an asynchronous way while still having niceties like a command history 41 | and special key handling (e.g., CTRL+U deletes everything from the beginning of 42 | the line to the cursor). Specific needs can be addressed by wrapping this type 43 | with another type, such as the AbsPrompt. 44 | 45 | Internally uses KeyReader for parsing keys from the io.Reader. 46 | 47 | Have a look at the [simple reader example](example/simple.go) to get an idea 48 | how to use this type in an application which draws random output while 49 | prompting the user for input and printing their keystrokes to the screen. 50 | 51 | Note that the example has some special handling for a few keys to demonstrate 52 | (and verify correctness of) some key interception functionality. 53 | 54 | ### terminal.Prompt 55 | 56 | `terminal.Prompt` is the closest thing to the terminal which exists in the 57 | crypto package. It will draw a prompt and wait for input, handling arrow keys 58 | and other special keys to reposition the cursor, fetch history, etc. This 59 | should be used in cases where the crypto terminal would normally be used, but 60 | more complex handling is necessary, such as the on/after keypress handlers. 61 | 62 | ### terminal.AbsPrompt 63 | 64 | `terminal.AbsPrompt` offers simple output layer on top of a terminal.Reader for 65 | cases where a prompt should be displayed at a fixed location in the terminal. 66 | It is tied to a given io.Writer and can be asked to draw changes to the input 67 | since it was last drawn, or redraw itself fully, including all repositioning 68 | ANSI codes. Since drawing is done on command, there's no need to synchronize 69 | writes with other output. 70 | 71 | Internally uses KeyReader for parsing keys from the io.Reader. 72 | 73 | Have a look at the [absprompt example](example/absprompt.go) to get an idea how 74 | this type can simplify getting input from a user compared to building your code 75 | on top of the simpler Reader type. 76 | 77 | As mentioned in "features", this package isn't coupled to a particular output 78 | approach. Check out [the goterm example](example/goterm.go) to see how you can 79 | use a AbsPrompt with [goterm](https://github.com/buger/goterm) - or any output 80 | package which doesn't force its input layer on you. 81 | 82 | ### terminal.DT 83 | 84 | `terminal.DT` is for a very simple, no-frills terminal. It has no support for 85 | special keys other than backspace, and is meant to just gather printable keys 86 | from a user who may not have ANSI support. It writes directly to the given 87 | io.Writer with no synchronizing, as it is assumed that if you wanted complex 88 | output to happen, you wouldn't use this. 89 | 90 | Internally uses KeyReader for parsing keys from the io.Reader. 91 | 92 | Have a loot at the ["dumb" example](example/dumb.go) to see how a DT can be 93 | used for an extremely simple interface. 94 | 95 | ### terminal.KeyReader 96 | 97 | `terminal.KeyReader` lets you read keys as they're typed, giving extremely 98 | low-level control (for a terminal reader, anyway). The optional `Force` 99 | variable can be set to true if you need immediate key parsing despite the 100 | oddities that can bring. See the 101 | [`ParseKey` documentation](https://godoc.org/github.com/Nerdmaster/terminal#ParseKey) 102 | for an in-depth explanation of this. 103 | 104 | In normal mode (`Force` is false), special keys like Escape and 105 | Alt-left-bracket will not be properly parsed until another key is pressed due 106 | to limitations discussed in the ParseKey documentation and the Caveats section 107 | below. However, users won't get "stuck", as the parser will just force-parse 108 | sequences if more than 250ms separates one read from the next. 109 | 110 | Take a look at the [keyreport example](example/keyreport.go) to get an idea how 111 | to build a raw key parser using KeyReader. You can also run it directly (`go run 112 | example/keyreport.go`) to see what sequence of bytes a given key (or key 113 | combination) spits out. Note that this has special handling for Ctrl+C (exit 114 | program) and Ctrl+F (toggle "forced" parse mode). 115 | 116 | Caveats 117 | --- 118 | 119 | ### Terminals suck 120 | 121 | Please note that different terminals implement different key sequences in 122 | hilariously different ways. What's in this package may or may not actually 123 | handle your real-world use-case. Terminals are simply not the right medium for 124 | getting raw keys in any kind of consistent and guaranteed way. As an example, 125 | the key sequence for "Alt+F" is the same as hitting "Escape" and then "F" 126 | immediately after. The left arrow is the same as hitting alt+[+D. Try it on a 127 | command line! In linux, at least, you can fake quite a lot of special keys 128 | because the console is so ... weird. 129 | 130 | ### io.Reader is limited 131 | 132 | Go doesn't provide an easy mechanism for reading from an io.Reader in a 133 | "pollable" way. It's already impossible to tell if alt+[ is really alt+[ or 134 | the beginning of a longer sequence. With no way to poll the io.Reader, this 135 | package has to make a guess. I tried using goroutines and channels to try to 136 | determine when it had been long enough to force the parse, but that had its own 137 | problems, the worst of which was you couldn't cancel a read that was just 138 | sitting waiting. Which meant users would have to press at least one extra key 139 | before the app could stop listening - or else the app had to force-close an 140 | io.ReadCloser, which isn't how you want to handle something like an ssh 141 | connection that's meant to be persistent. 142 | 143 | In "forced" parse mode, alt+[ will work just fine, but a left arrow can get 144 | parsed as "alt+[" followed by "D" if the reader doesn't see the D at precisely 145 | the same moment as the "alt+[". But in normal mode, a user who hits alt-[ by 146 | mistake, and tries typing numbers can find themselves "stuck" for a moment 147 | until the reader sees that enough time has passed since their mistaken "alt+[" 148 | keystroke and the "real" keys. Or until they hit 8 bytes' worth of keys, at 149 | which point the key reader starts making assumptions that are likely incorrect. 150 | 151 | Low-level reading of the keyboard would solve this problem, but this package is 152 | meant to be as portable as possible, and able to parse input from ANYTHING 153 | readable. Not just a local console, but also SSH, telnet, etc. It may even be 154 | valuable to read keystrokes captured in a file (though I suspect that would 155 | break things in even more hilarious ways). 156 | 157 | ### Limited testing 158 | 159 | - Tested in Windows: cmd and PowerShell, Putty ssh into Ubuntu server 160 | - Tested in Linux: Konsole in Ubuntu VM, tmux on Debian and Ubuntu, and a raw 161 | GUI-less debian VM in VMWare 162 | 163 | Windows terminals (cmd and PowerShell) have very limited support for anything 164 | beyond ASCII as far as I can tell. Putty is a lot better. If you plan to 165 | write an application that needs to support even simple sequences like arrow 166 | keys, you should host it on a Linux system and have users ssh in. Shipping a 167 | Windows binary won't work with built-in tools. 168 | 169 | If you can test out the keyreport tool in other OSes, that would be super 170 | helpful. 171 | 172 | ### Therefore.... 173 | 174 | If you use this package for any kind of application, just make sure you 175 | understand the limitations. Parsing of keys is, in many cases, done just to be 176 | able to throw away absurd user input (like Meta+Ctrl+7) rather than end up 177 | making wrong guesses (my Linux terminal thinks certain Meta combos should print 178 | a list of local servers followed by the ASCII parts of the sequence). 179 | 180 | So while you may not be able to count on specific key sequences, this package 181 | might help you gather useful input while ignoring (many) completely absurd 182 | sequences. 183 | -------------------------------------------------------------------------------- /absprompt.go: -------------------------------------------------------------------------------- 1 | package terminal 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io" 7 | "strings" 8 | ) 9 | 10 | // AbsPrompt is a wrapper around a Reader which will write a prompt, wait for a 11 | // user's input, and return it. It will print whatever needs to be printed on 12 | // demand to an io.Writer, using ANSI to ensure the cursor is always at the 13 | // right screen location, allowing the AbsPrompt to be used concurrently with 14 | // other screen writing. AbsPrompt stores the Reader's prior state in order to 15 | // avoid unnecessary writes. 16 | type AbsPrompt struct { 17 | *Reader 18 | prompt string 19 | Out io.Writer 20 | buf bytes.Buffer 21 | x, y int 22 | promptWidth int 23 | line string 24 | pos int 25 | prompted bool 26 | } 27 | 28 | // NewAbsPrompt returns an AbsPrompt which will read lines from r, write its 29 | // prompt and current line to w, and use p as the prompt string. 30 | func NewAbsPrompt(r io.Reader, w io.Writer, p string) *AbsPrompt { 31 | var prompt = &AbsPrompt{Reader: NewReader(r), Out: w, buf: bytes.Buffer{}, x: 1, y: 1} 32 | prompt.SetPrompt(p) 33 | return prompt 34 | } 35 | 36 | // ReadLine delegates to the reader's ReadLine function 37 | func (p *AbsPrompt) ReadLine() (string, error) { 38 | line, err := p.Reader.ReadLine() 39 | return line, err 40 | } 41 | 42 | // SetPrompt changes the current prompt. This shouldn't be called while a 43 | // ReadLine is in progress. 44 | func (p *AbsPrompt) SetPrompt(s string) { 45 | p.prompt = s 46 | p.promptWidth = VisualLength(p.prompt) 47 | } 48 | 49 | // SetLocation changes the internal x and y coordinates. If this is called 50 | // while a ReadLine is in progress, you won't be happy. 51 | func (p *AbsPrompt) SetLocation(x, y int) { 52 | p.x = x + 1 53 | p.y = y + 1 54 | } 55 | 56 | // NeedWrite returns true if there are any pending changes to the line or 57 | // cursor position 58 | func (p *AbsPrompt) NeedWrite() bool { 59 | line, pos := p.LinePos() 60 | return line != p.line || pos != p.pos 61 | } 62 | 63 | // WriteAll forces a write of the entire prompt 64 | func (p *AbsPrompt) WriteAll() { 65 | line, pos := p.LinePos() 66 | 67 | p.printAt(0, p.prompt+p.line) 68 | p.pos = len(p.line) 69 | 70 | if p.line != line { 71 | prevLine := p.line 72 | 73 | lpl := len(prevLine) 74 | ll := len(line) 75 | bigger := lpl - ll 76 | if bigger > 0 { 77 | fmt.Fprintf(p.Out, strings.Repeat(" ", bigger)) 78 | p.pos += bigger 79 | } 80 | } 81 | 82 | if p.pos != pos { 83 | p.pos = pos 84 | p.PrintCursorMovement() 85 | } 86 | } 87 | 88 | // WriteChanges attempts to only write to the console when something has 89 | // changed (line text or the cursor position). It will also print the prompt 90 | // if that hasn't yet been printed. 91 | func (p *AbsPrompt) WriteChanges() { 92 | line, pos := p.LinePos() 93 | 94 | if !p.prompted { 95 | p.PrintPrompt() 96 | p.prompted = true 97 | } 98 | 99 | if p.line != line { 100 | prevLine := p.line 101 | p.line = line 102 | p.PrintLine() 103 | 104 | lpl := len(prevLine) 105 | ll := len(line) 106 | bigger := lpl - ll 107 | if bigger > 0 { 108 | fmt.Fprintf(p.Out, strings.Repeat(" ", bigger)) 109 | p.pos += bigger 110 | } 111 | } 112 | 113 | if p.pos != pos { 114 | p.pos = pos 115 | p.PrintCursorMovement() 116 | } 117 | } 118 | 119 | // WriteChangesNoCursor prints prompt and line if necessary, but doesn't 120 | // reposition the cursor in order to allow a frequently-updating app to write 121 | // the cursor change where it makes sense, regardless of changes to the user's 122 | // input. 123 | func (p *AbsPrompt) WriteChangesNoCursor() { 124 | line, pos := p.LinePos() 125 | p.pos = pos 126 | 127 | if !p.prompted { 128 | p.PrintPrompt() 129 | p.prompted = true 130 | } 131 | 132 | if p.line != line { 133 | prevLine := p.line 134 | p.line = line 135 | p.PrintLine() 136 | 137 | lpl := len(prevLine) 138 | ll := len(line) 139 | bigger := lpl - ll 140 | if bigger > 0 { 141 | fmt.Fprintf(p.Out, strings.Repeat(" ", bigger)) 142 | p.pos += bigger 143 | } 144 | } 145 | } 146 | 147 | // printAt moves to the position dx spaces from the start of the prompt's X 148 | // location and prints a string 149 | func (p *AbsPrompt) printAt(dx int, s string) { 150 | fmt.Fprintf(p.Out, "\x1b[%d;%dH%s", p.y, p.x+dx, s) 151 | } 152 | 153 | // PrintPrompt moves to the x/y coordinates of the prompt and prints the 154 | // prompt string 155 | func (p *AbsPrompt) PrintPrompt() { 156 | p.printAt(0, p.prompt) 157 | p.pos = 0 158 | } 159 | 160 | // PrintLine gets the current line and prints it to the screen just after the 161 | // prompt location 162 | func (p *AbsPrompt) PrintLine() { 163 | p.line, _ = p.LinePos() 164 | p.printAt(p.promptWidth, p.line) 165 | p.pos = len(p.line) 166 | } 167 | 168 | // PrintCursorMovement sends the ANSI escape sequence for moving the cursor 169 | func (p *AbsPrompt) PrintCursorMovement() { 170 | p.pos = p.Pos() 171 | p.printAt(p.promptWidth+p.pos, "") 172 | } 173 | -------------------------------------------------------------------------------- /bytes_to_key.go: -------------------------------------------------------------------------------- 1 | package terminal 2 | 3 | import ( 4 | "bytes" 5 | "unicode/utf8" 6 | ) 7 | 8 | // ParseKey tries to parse a key sequence from b. If successful, it returns the 9 | // key, the length in bytes of that key, and the modifier, if any. Otherwise it 10 | // returns utf8.RuneError, 0, and an undefined mod which should be ignored. 11 | // 12 | // ParseKey is the function on which all terminal's types rely, and offers the 13 | // lowest-level parsing in this package. Even though it's the base function 14 | // used by all types, the way it handles sequences is complex enough that it is 15 | // generally best to use one of the key/line parsers rather than calling this 16 | // directly. 17 | // 18 | // When used by the various types, "force" defaults to being false. This means 19 | // that we assume users are not typically typing key sequence prefixes like the 20 | // Escape key or Alt-left-bracket. This eases parsing in most typical cases, 21 | // but it means when a user *does* press one of these keys, the Parser treats 22 | // it as if nothing was pressed at all (returning utf8.RuneError and a length 23 | // of 0). It's then up to the caller to decide to either drop the bytes or 24 | // append to them with more data. 25 | // 26 | // When one of the readers (KeyReader, Reader, Prompter) gets this kind of "empty" 27 | // response, it will hold onto the bytes and try to append to them next time it 28 | // reads. It is basically assuming the sequence is incomplete. If the next read 29 | // doesn't happen soon enough, the reader will decide the sequence was in fact 30 | // complete, and then return the raw Escape or Alt+[, but this doesn't happen 31 | // until after the subsequent read, which means effectively a one-key "lag". 32 | // 33 | // This means applications really can't rely on getting raw Escape keys or 34 | // alt-left-bracket. And in some cases, this is not acceptable. Hence, the 35 | // "force" flag. 36 | // 37 | // If "force" is true, ParseKey will return immediately, even if the sequence is 38 | // nonsensical. This makes it a lot easier to get very special keys, but when 39 | // listening on a network there is probably a small chance a key sequence could be 40 | // broken up over multiple reads, and the result will be essentially "corrupt" 41 | // keys. As an example, if the left-arrow is sent, it's normally three bytes: 42 | // Escape followed by left-bracket followed by uppercase "D". If the Escape is 43 | // read separately from the rest of the sequence, the overall result will be an 44 | // Escape key followed by a left-bracket followed by a "D". If the Escape and 45 | // left-bracket are read separately from the "D", the result will be 46 | // Alt-left-bracket followed by "D". The weird variations can get worse with 47 | // longer key sequences. 48 | // 49 | // Additionally, if user input is serialized to a file or something, just as a raw 50 | // stream of bytes, the read operation won't read more than 256 at a time. This 51 | // will undoubtedly lead to all kinds of broken "keys" being parsed. 52 | // 53 | // The tl;dr is that terminals kind of suck at complex key parsing, so make 54 | // sure you go into it with your eyes wide open. 55 | func ParseKey(b []byte, force bool) (r rune, rl int, mod KeyModifier) { 56 | // Default to a rune error, since we use that in so many situations 57 | r = utf8.RuneError 58 | 59 | var l = len(b) 60 | if l == 0 { 61 | return 62 | } 63 | 64 | // Function keys F1-F4 are an even more ultra-super-special-case, because 65 | // they can get detected as alt+letter otherwise. ARGH. 66 | if l > 2 && b[0] == 0x1b && b[1] == 'O' { 67 | var b2 = b[2] 68 | if l > 3 && b[2] == '1' { 69 | rl++ 70 | b2 = b[3] 71 | mod |= ModMeta 72 | } 73 | switch b2 { 74 | case 'P': 75 | return KeyF1, rl + 3, mod 76 | case 'Q': 77 | return KeyF2, rl + 3, mod 78 | case 'R': 79 | return KeyF3, rl + 3, mod 80 | case 'S': 81 | return KeyF4, rl + 3, mod 82 | } 83 | } 84 | 85 | // Ultra-super-special-case handling for meta key 86 | if l > 3 && b[0] == 0x18 && b[1] == '@' && b[2] == 's' { 87 | b = b[3:] 88 | l -= 3 89 | rl += 3 90 | mod |= ModMeta 91 | } 92 | 93 | // Super-special-case handling for alt+esc and alt+left-bracket: these two 94 | // sequences are often just prefixes of other sequences, so when force is 95 | // true, if we have these and nothing else, we return immediately 96 | if l == 2 && force && b[0] == 0x1b { 97 | if b[1] == 0x1b { 98 | return KeyEscape, rl + 2, mod | ModAlt 99 | } 100 | if b[1] == '[' { 101 | return KeyLeftBracket, rl + 2, mod | ModAlt 102 | } 103 | } 104 | 105 | // Special case: some alt keys are "0x1b..." and need to be detected early 106 | if l > 1 && b[0] == 0x1b && b[1] != '[' { 107 | b = b[1:] 108 | l-- 109 | rl++ 110 | mod |= ModAlt 111 | } 112 | 113 | // Handle ctrl keys next. DecodeRune can do this, but it's a bit quicker to 114 | // handle this first (I'm assuming so, anyway, since the original 115 | // implementation did this first) 116 | if b[0] < KeyEscape { 117 | return rune(b[0]), rl + 1, mod 118 | } 119 | 120 | if b[0] != KeyEscape { 121 | if !utf8.FullRune(b) { 122 | if force { 123 | rl += len(b) 124 | return 125 | } 126 | return 127 | } 128 | var r, nrl = utf8.DecodeRune(b) 129 | return r, rl + nrl, mod 130 | } 131 | 132 | // From the above test we know the first key is escape. If that's all we 133 | // have, we are *probably* missing some bytes... but maybe not. 134 | if l == 1 { 135 | if force { 136 | return KeyEscape, rl + 1, mod 137 | } 138 | return keyUnknown(b, rl, force, mod) 139 | } 140 | 141 | // Everything else we know how to handle is at least 3 bytes 142 | if l < 3 { 143 | if force { 144 | rl += len(b) 145 | return 146 | } 147 | return keyUnknown(b, rl, force, mod) 148 | } 149 | 150 | // All sequences we know how to handle from here on start with "\x1b[" 151 | if b[1] != '[' { 152 | return keyUnknown(b, rl, force, mod) 153 | } 154 | 155 | // Local terminal alt keys are sometimes longer sequences that come through 156 | // as "\x1b[1;3" + some alpha 157 | if l >= 6 && b[2] == '1' && b[3] == ';' && b[4] == '3' { 158 | b = append([]byte{0x1b, '['}, b[5:]...) 159 | l -= 3 160 | rl += 3 161 | mod |= ModAlt 162 | } 163 | 164 | // ...and sometimes they're "\x1b[", some num, ";3~" 165 | if l >= 6 && b[3] == ';' && b[4] == '3' && b[5] == '~' { 166 | b = append([]byte{0x1b, '[', b[2]}, b[5:]...) 167 | l -= 2 168 | rl += 2 169 | mod |= ModAlt 170 | } 171 | 172 | // Since the buffer may have been manipulated, we re-check that we have 3+ 173 | // characters left 174 | if l < 3 { 175 | return keyUnknown(b, rl, force, mod) 176 | } 177 | 178 | // From here on, all known return values must be at least 3 characters 179 | switch b[2] { 180 | case 'A': 181 | return KeyUp, rl + 3, mod 182 | case 'B': 183 | return KeyDown, rl + 3, mod 184 | case 'C': 185 | return KeyRight, rl + 3, mod 186 | case 'D': 187 | return KeyLeft, rl + 3, mod 188 | case 'H': 189 | return KeyHome, rl + 3, mod 190 | case 'F': 191 | return KeyEnd, rl + 3, mod 192 | case 'P': 193 | return KeyPause, rl + 3, mod 194 | } 195 | 196 | if l < 4 { 197 | return keyUnknown(b, rl, force, mod) 198 | } 199 | 200 | // NOTE: these appear to be escape sequences I see in tmux, but some don't 201 | // actually seem to happen on a "direct" terminal! 202 | if b[3] == '~' { 203 | switch b[2] { 204 | case '1': 205 | return KeyHome, rl + 4, mod 206 | case '2': 207 | return KeyInsert, rl + 4, mod 208 | case '3': 209 | return KeyDelete, rl + 4, mod 210 | case '4': 211 | return KeyEnd, rl + 4, mod 212 | case '5': 213 | return KeyPgUp, rl + 4, mod 214 | case '6': 215 | return KeyPgDn, rl + 4, mod 216 | } 217 | } 218 | 219 | // "Raw terminal" function keys (VMWare non-gui debian) 220 | if b[2] == '[' { 221 | switch b[3] { 222 | case 'A': 223 | return KeyF1, rl + 4, mod 224 | case 'B': 225 | return KeyF2, rl + 4, mod 226 | case 'C': 227 | return KeyF3, rl + 4, mod 228 | case 'D': 229 | return KeyF4, rl + 4, mod 230 | case 'E': 231 | return KeyF5, rl + 4, mod 232 | } 233 | } 234 | 235 | if l < 5 { 236 | return keyUnknown(b, rl, force, mod) 237 | } 238 | 239 | // Meta + Function keys can be handled with a tiny bit of magic 240 | if len(b) > 6 && b[4] == ';' && b[5] == '1' && b[6] == '~' { 241 | b = append(b[:4], b[6:]...) 242 | l -= 2 243 | rl += 2 244 | mod |= ModMeta 245 | } 246 | 247 | // More function keys: these are shared across terminal and non-terminal 248 | // *except* F5, which is only seen this way when in a "non-raw" situation, 249 | // and F1-F4, which are only seen with these codes when sshed in from PuTTY 250 | if b[4] == '~' { 251 | switch b[2] { 252 | case '1': 253 | switch b[3] { 254 | case '1': 255 | return KeyF1, rl + 5, mod 256 | case '2': 257 | return KeyF2, rl + 5, mod 258 | case '3': 259 | return KeyF3, rl + 5, mod 260 | case '4': 261 | return KeyF4, rl + 5, mod 262 | case '5': 263 | return KeyF5, rl + 5, mod 264 | case '7': 265 | return KeyF6, rl + 5, mod 266 | case '8': 267 | return KeyF7, rl + 5, mod 268 | case '9': 269 | return KeyF8, rl + 5, mod 270 | } 271 | case '2': 272 | switch b[3] { 273 | case '0': 274 | return KeyF9, rl + 5, mod 275 | case '1': 276 | return KeyF10, rl + 5, mod 277 | case '3': 278 | return KeyF11, rl + 5, mod 279 | case '4': 280 | return KeyF12, rl + 5, mod 281 | } 282 | } 283 | } 284 | 285 | if l < 6 { 286 | return keyUnknown(b, rl, force, mod) 287 | } 288 | 289 | if len(b) >= 6 && bytes.Equal(b[:6], pasteEnd) { 290 | return KeyPasteEnd, rl + 6, mod 291 | } 292 | 293 | if len(b) >= 6 && bytes.Equal(b[:6], pasteStart) { 294 | return KeyPasteStart, rl + 6, mod 295 | } 296 | 297 | return keyUnknown(b, rl, force, mod) 298 | } 299 | 300 | // keyUnknown attempts to parse the unknown key and return its size. If the 301 | // key can't be figured out, it returns a RuneError. 302 | func keyUnknown(b []byte, rl int, force bool, mod KeyModifier) (rune, int, KeyModifier) { 303 | // This is a hack, and it's guaranteed to not work in quite a few situations, 304 | // but there's really not much to be done when our buffer starts getting too 305 | // big. Instead of trying to really make this awesome, we just throw away 306 | // the first character and call it an error. 307 | if len(b) > 8 && !force { 308 | return utf8.RuneError, 1, ModNone 309 | } 310 | 311 | for i, c := range b[0:] { 312 | // It's not clear how to find the end of a sequence without knowing them 313 | // all, but it seems that [a-zA-Z~] only appears at the end of a sequence 314 | if c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z' || c == '~' { 315 | return KeyUnknown, rl + i + 1, mod 316 | } 317 | } 318 | 319 | if force { 320 | return utf8.RuneError, rl + len(b), mod 321 | } 322 | 323 | return utf8.RuneError, 0, ModNone 324 | } 325 | -------------------------------------------------------------------------------- /dumb_terminal.go: -------------------------------------------------------------------------------- 1 | // Portions Copyright 2011- The Go Authors. All rights reserved. 2 | // Portions Copyright 2016- Jeremy Echols. All rights reserved. 3 | // Use of this source code is governed by a BSD-style 4 | // license that can be found in the LICENSE file. 5 | 6 | package terminal 7 | 8 | import ( 9 | "io" 10 | "sync" 11 | "unicode/utf8" 12 | ) 13 | 14 | // CRLF is the byte sequence all terminals use for moving to the beginning of 15 | // the next line 16 | var CRLF = []byte("\r\n") 17 | 18 | // DT contains the state for running a *very* basic terminal which operates 19 | // effectively like an old-school telnet connection: no ANSI, no special keys, 20 | // no history preservation, etc. This isn't actually useful in any way I can 21 | // see, but it's a decent example of lower-level key reading. 22 | type DT struct { 23 | keyReader *KeyReader 24 | w io.Writer 25 | outBuffer []byte 26 | 27 | sync.RWMutex 28 | 29 | // Echo is on by default; set it to false for things like password prompts 30 | Echo bool 31 | 32 | // input is the current line being entered 33 | input []rune 34 | } 35 | 36 | // Dumb runs a dumb terminal reader on the given io.Reader. If the terminal is 37 | // local, it must first have been put into raw mode. 38 | func Dumb(r io.Reader, w io.Writer) *DT { 39 | return &DT{keyReader: NewKeyReader(r), w: w, Echo: true} 40 | } 41 | 42 | // queue prepares bytes for printing 43 | func (dt *DT) queue(b []byte) { 44 | if dt.Echo { 45 | dt.outBuffer = b 46 | } 47 | } 48 | 49 | // handleKeypress processes the given keypress data and, optionally, returns a 50 | // line of text that the user has entered. 51 | func (dt *DT) handleKeypress(kp Keypress) (line string, ok bool) { 52 | dt.Lock() 53 | defer dt.Unlock() 54 | 55 | key := kp.Key 56 | switch key { 57 | case KeyBackspace, KeyCtrlH: 58 | if len(dt.input) == 0 { 59 | return 60 | } 61 | dt.input = dt.input[:len(dt.input)-1] 62 | dt.queue([]byte("\x08 \x08")) 63 | case KeyEnter: 64 | line = string(dt.input) 65 | ok = true 66 | dt.input = dt.input[:0] 67 | dt.queue(CRLF) 68 | default: 69 | if !isPrintable(key) { 70 | return 71 | } 72 | dt.input = append(dt.input, key) 73 | dt.queue(kp.Raw) 74 | } 75 | return 76 | } 77 | 78 | // flushOut attempts to write to the terminal. Output errors aren't something 79 | // we can easily handle here, so we ignore them rather than panic. 80 | func (dt *DT) flushOut() { 81 | if len(dt.outBuffer) == 0 { 82 | return 83 | } 84 | 85 | dt.w.Write(dt.outBuffer) 86 | dt.outBuffer = nil 87 | } 88 | 89 | // ReadLine returns a line of input from the terminal 90 | func (dt *DT) ReadLine() (line string, err error) { 91 | for { 92 | lineOk := false 93 | for !lineOk { 94 | var kp Keypress 95 | kp, err = dt.keyReader.ReadKeypress() 96 | if err != nil { 97 | return 98 | } 99 | 100 | key := kp.Key 101 | if key == utf8.RuneError { 102 | break 103 | } 104 | 105 | line, lineOk = dt.handleKeypress(kp) 106 | dt.flushOut() 107 | } 108 | 109 | if lineOk { 110 | return 111 | } 112 | } 113 | } 114 | 115 | // Line returns the current input line as a string; this can be useful for 116 | // interrupting a user's input for critical messages and then re-entering the 117 | // previous prompt and text. I don't know if doing this is accessible, but 118 | // I've seen apps that do it. 119 | func (dt *DT) Line() string { 120 | dt.RLock() 121 | defer dt.RUnlock() 122 | return string(dt.input) 123 | } 124 | -------------------------------------------------------------------------------- /example/absprompt.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "math/rand" 6 | "os" 7 | "time" 8 | 9 | "github.com/Nerdmaster/terminal" 10 | ) 11 | 12 | // CSI == Control Sequence Introducer, begins most ANSI commands 13 | const CSI = "\x1b[" 14 | const ClearScreen = CSI + "2J" + CSI + ";H" 15 | 16 | var done bool 17 | var noise [][]rune 18 | var nextNoise [][]rune 19 | var userInput string 20 | var p *terminal.AbsPrompt 21 | 22 | var validRunes = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890!@#$%^&*") 23 | 24 | func printAt(x, y int, output string) { 25 | fmt.Fprintf(os.Stdout, "%s%d;%dH%s", CSI, y, x, output) 26 | } 27 | 28 | func randomRune() rune { 29 | return validRunes[rand.Intn(len(validRunes))] 30 | } 31 | 32 | func initializeScreen() { 33 | // Clear everything 34 | fmt.Fprintf(os.Stdout, ClearScreen) 35 | 36 | // Print initial runes 37 | for y := 0; y < 10; y++ { 38 | printAt(1, y+1, string(noise[y])) 39 | } 40 | } 41 | 42 | func setupNoise() { 43 | noise = make([][]rune, 10) 44 | for y := range noise { 45 | noise[y] = make([]rune, 100) 46 | for x := range noise[y] { 47 | if y == 3 && x > 9 && x < 90 { 48 | noise[y][x] = ' ' 49 | } else { 50 | noise[y][x] = randomRune() 51 | } 52 | } 53 | } 54 | nextNoise = make([][]rune, 10) 55 | for y := range nextNoise { 56 | nextNoise[y] = make([]rune, 100) 57 | for x := range nextNoise[y] { 58 | nextNoise[y][x] = noise[y][x] 59 | } 60 | } 61 | } 62 | 63 | func main() { 64 | rand.Seed(time.Now().UTC().UnixNano()) 65 | 66 | oldState, err := terminal.MakeRaw(int(os.Stdin.Fd())) 67 | if err != nil { 68 | panic(err) 69 | } 70 | defer terminal.Restore(0, oldState) 71 | 72 | setupNoise() 73 | initializeScreen() 74 | 75 | p = terminal.NewAbsPrompt(os.Stdin, os.Stdout, "> ") 76 | p.SetLocation(10, 3) 77 | p.MaxLineLength = 70 78 | go readInput() 79 | go printOutput() 80 | 81 | for done == false { 82 | x := rand.Intn(100) 83 | y := rand.Intn(10) 84 | nextNoise[y][x] = randomRune() 85 | time.Sleep(time.Millisecond * 10) 86 | } 87 | } 88 | 89 | func readInput() { 90 | for { 91 | command, err := p.ReadLine() 92 | if command == "quit" { 93 | done = true 94 | return 95 | } 96 | if err != nil { 97 | done = true 98 | return 99 | } 100 | } 101 | } 102 | 103 | func printOutput() { 104 | for { 105 | // Print any changes to noise since last tick 106 | for y := 0; y < 10; y++ { 107 | for x := 0; x < 100; x++ { 108 | if y == 3 && x > 9 && x < 90 { 109 | nextNoise[y][x] = noise[y][x] 110 | } 111 | if noise[y][x] != nextNoise[y][x] { 112 | printAt(x+1, y+1, string(nextNoise[y][x])) 113 | noise[y][x] = nextNoise[y][x] 114 | } 115 | } 116 | } 117 | // Print any changes to user input since last tick 118 | p.WriteChangesNoCursor() 119 | p.PrintCursorMovement() 120 | 121 | time.Sleep(time.Millisecond * 50) 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /example/dumb.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "math/rand" 6 | "os" 7 | "strconv" 8 | "strings" 9 | "time" 10 | 11 | "github.com/Nerdmaster/terminal" 12 | ) 13 | 14 | func main() { 15 | rand.Seed(time.Now().UTC().UnixNano()) 16 | var number = rand.Intn(10) + 1 17 | 18 | oldState, err := terminal.MakeRaw(int(os.Stdin.Fd())) 19 | if err != nil { 20 | panic(err) 21 | } 22 | defer terminal.Restore(0, oldState) 23 | 24 | var dt = terminal.Dumb(os.Stdin, os.Stdout) 25 | 26 | fmt.Print("I'm thinking of a number from 1-10. Try to guess it!\r\n") 27 | fmt.Print("(Type 'QUIT' at any time to exit)\r\n\r\n") 28 | for { 29 | fmt.Print("Guess a number: ") 30 | guess, err := dt.ReadLine() 31 | if strings.ToLower(guess) == "quit" { 32 | fmt.Print("Quitter!\r\n") 33 | break 34 | } 35 | if err != nil { 36 | fmt.Print("Oh no, I got an error!\r\n") 37 | fmt.Printf("%s\r\n", err) 38 | break 39 | } 40 | 41 | var g int 42 | g, err = strconv.Atoi(guess) 43 | if err != nil { 44 | fmt.Printf("Your entry, '%s', doesn't appear to be a valid number\r\n", guess) 45 | continue 46 | } 47 | 48 | if g < 1 || g > 10 { 49 | fmt.Print("Please enter a number in the range of 1 to 10, inclusive\r\n") 50 | continue 51 | } 52 | 53 | if g == number { 54 | fmt.Print("Correct!!\r\n") 55 | break 56 | } 57 | 58 | fmt.Printf("%d is WRONG! Try again!\r\n", g) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /example/goterm.go: -------------------------------------------------------------------------------- 1 | //+build goterm 2 | 3 | package main 4 | 5 | import ( 6 | "os" 7 | "strings" 8 | "time" 9 | 10 | "github.com/Nerdmaster/terminal" 11 | "github.com/buger/goterm" 12 | ) 13 | 14 | var done bool 15 | var userInput string 16 | var p *terminal.AbsPrompt 17 | var cmdBox, sizeBox *goterm.Box 18 | 19 | func main() { 20 | oldState, err := terminal.MakeRaw(int(os.Stdin.Fd())) 21 | if err != nil { 22 | panic(err) 23 | } 24 | defer terminal.Restore(0, oldState) 25 | 26 | cmdBox = goterm.NewBox(90, 3, 0) 27 | sizeBox = goterm.NewBox(20, 10, 0) 28 | sizeBox.Write([]byte("I AM A REALLY COOL BOX")) 29 | 30 | p = terminal.NewAbsPrompt(os.Stdin, os.Stdout, "Command: ") 31 | p.SetLocation(3, 1) 32 | p.MaxLineLength = 70 33 | 34 | go readInput() 35 | go printOutput() 36 | 37 | for done == false { 38 | time.Sleep(time.Millisecond * 10) 39 | } 40 | 41 | goterm.Clear() 42 | goterm.Flush() 43 | } 44 | 45 | func readInput() { 46 | for { 47 | command, err := p.ReadLine() 48 | command = strings.ToLower(command) 49 | if command == "quit" { 50 | done = true 51 | return 52 | } 53 | if command == "bigger" { 54 | sizeBox.Width++ 55 | } 56 | if command == "smaller" { 57 | sizeBox.Width-- 58 | } 59 | if err != nil { 60 | done = true 61 | return 62 | } 63 | } 64 | } 65 | 66 | func printOutput() { 67 | goterm.Clear() 68 | goterm.Print(goterm.MoveTo(cmdBox.String(), 1, 1)) 69 | goterm.Flush() 70 | 71 | for { 72 | // Redraw the goterm stuff which isn't static 73 | goterm.Print(goterm.MoveTo(sizeBox.String(), 1, 20)) 74 | goterm.Flush() 75 | 76 | // Print any changes to user input since last tick 77 | p.WriteChangesNoCursor() 78 | p.PrintCursorMovement() 79 | 80 | time.Sleep(time.Millisecond * 50) 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /example/keyreport.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "os" 7 | 8 | "github.com/Nerdmaster/terminal" 9 | ) 10 | 11 | var keyText = map[rune]string{ 12 | terminal.KeyCtrlA: "KeyCtrlA", 13 | terminal.KeyCtrlB: "KeyCtrlB", 14 | terminal.KeyCtrlC: "KeyCtrlC", 15 | terminal.KeyCtrlD: "KeyCtrlD", 16 | terminal.KeyCtrlE: "KeyCtrlE", 17 | terminal.KeyCtrlF: "KeyCtrlF", 18 | terminal.KeyCtrlG: "KeyCtrlG", 19 | terminal.KeyCtrlH: "KeyCtrlH", 20 | terminal.KeyCtrlI: "KeyCtrlI", 21 | terminal.KeyCtrlJ: "KeyCtrlJ", 22 | terminal.KeyCtrlK: "KeyCtrlK", 23 | terminal.KeyCtrlL: "KeyCtrlL", 24 | terminal.KeyCtrlN: "KeyCtrlN", 25 | terminal.KeyCtrlO: "KeyCtrlO", 26 | terminal.KeyCtrlP: "KeyCtrlP", 27 | terminal.KeyCtrlQ: "KeyCtrlQ", 28 | terminal.KeyCtrlR: "KeyCtrlR", 29 | terminal.KeyCtrlS: "KeyCtrlS", 30 | terminal.KeyCtrlT: "KeyCtrlT", 31 | terminal.KeyCtrlU: "KeyCtrlU", 32 | terminal.KeyCtrlV: "KeyCtrlV", 33 | terminal.KeyCtrlW: "KeyCtrlW", 34 | terminal.KeyCtrlX: "KeyCtrlX", 35 | terminal.KeyCtrlY: "KeyCtrlY", 36 | terminal.KeyCtrlZ: "KeyCtrlZ", 37 | terminal.KeyEscape: "KeyEscape", 38 | terminal.KeyLeftBracket: "KeyLeftBracket", 39 | terminal.KeyRightBracket: "KeyRightBracket", 40 | terminal.KeyEnter: "KeyEnter", 41 | terminal.KeyBackspace: "KeyBackspace", 42 | terminal.KeyUnknown: "KeyUnknown", 43 | terminal.KeyUp: "KeyUp", 44 | terminal.KeyDown: "KeyDown", 45 | terminal.KeyLeft: "KeyLeft", 46 | terminal.KeyRight: "KeyRight", 47 | terminal.KeyHome: "KeyHome", 48 | terminal.KeyEnd: "KeyEnd", 49 | terminal.KeyPasteStart: "KeyPasteStart", 50 | terminal.KeyPasteEnd: "KeyPasteEnd", 51 | terminal.KeyInsert: "KeyInsert", 52 | terminal.KeyDelete: "KeyDelete", 53 | terminal.KeyPgUp: "KeyPgUp", 54 | terminal.KeyPgDn: "KeyPgDn", 55 | terminal.KeyPause: "KeyPause", 56 | terminal.KeyF1: "KeyF1", 57 | terminal.KeyF2: "KeyF2", 58 | terminal.KeyF3: "KeyF3", 59 | terminal.KeyF4: "KeyF4", 60 | terminal.KeyF5: "KeyF5", 61 | terminal.KeyF6: "KeyF6", 62 | terminal.KeyF7: "KeyF7", 63 | terminal.KeyF8: "KeyF8", 64 | terminal.KeyF9: "KeyF9", 65 | terminal.KeyF10: "KeyF10", 66 | terminal.KeyF11: "KeyF11", 67 | terminal.KeyF12: "KeyF12", 68 | } 69 | 70 | var done bool 71 | var r *terminal.KeyReader 72 | 73 | func printKey(kp terminal.Keypress) { 74 | if kp.Key == terminal.KeyCtrlF { 75 | r.ForceParse = !r.ForceParse 76 | fmt.Printf(" [ForceParse: %#v]\r\n", r.ForceParse) 77 | } 78 | 79 | if kp.Key == terminal.KeyCtrlC { 80 | fmt.Print("CTRL+C pressed; terminating\r\n") 81 | done = true 82 | return 83 | } 84 | 85 | var keyString = keyText[kp.Key] 86 | fmt.Printf("Key: %U [name: %s] [mod: %s] [raw: %#v (%#v)] [size: %d]\r\n", 87 | kp.Key, keyString, kp.Modifier.String(), string(kp.Raw), kp.Raw, kp.Size) 88 | } 89 | 90 | func main() { 91 | // It's possible this will error if one does `echo "foo" | bin/keyreport`, so 92 | // in order to test more interesting scenarios, we let errors through. 93 | oldState, err := terminal.MakeRaw(int(os.Stdin.Fd())) 94 | if err == nil { 95 | defer terminal.Restore(int(os.Stdin.Fd()), oldState) 96 | } 97 | 98 | r = terminal.NewKeyReader(os.Stdin) 99 | readInput() 100 | } 101 | 102 | func readInput() { 103 | for !done { 104 | var kp, err = r.ReadKeypress() 105 | if err != nil { 106 | if err == io.EOF { 107 | fmt.Println("EOF encountered; exiting") 108 | return 109 | } 110 | fmt.Printf("ERROR: %s", err) 111 | done = true 112 | } 113 | printKey(kp) 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /example/prompt.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "math/rand" 6 | "os" 7 | "strconv" 8 | "strings" 9 | "time" 10 | 11 | "github.com/Nerdmaster/terminal" 12 | ) 13 | 14 | func main() { 15 | rand.Seed(time.Now().UTC().UnixNano()) 16 | var number = rand.Intn(10) + 1 17 | 18 | oldState, err := terminal.MakeRaw(int(os.Stdin.Fd())) 19 | if err != nil { 20 | panic(err) 21 | } 22 | defer terminal.Restore(0, oldState) 23 | 24 | // Add a key logger callback 25 | var keyLogger []rune 26 | var p = terminal.NewPrompt(os.Stdin, os.Stdout, "Guess a number: ") 27 | p.AfterKeypress = func(e *terminal.KeyEvent) { 28 | keyLogger = append(keyLogger, e.Key) 29 | } 30 | 31 | fmt.Print("I'm thinking of a number from 1-10. Try to guess it!\r\n") 32 | fmt.Print("(Type 'QUIT' at any time to exit)\r\n\r\n") 33 | fmt.Print("(CTRL+X on a blank line will also quit)\r\n\r\n") 34 | p.Reader.CloseKey = terminal.KeyCtrlX 35 | 36 | for { 37 | var guess, err = p.ReadLine() 38 | if err != nil { 39 | fmt.Print("\r\nOh no, I got an error!\r\n") 40 | fmt.Printf("%s\r\n", err) 41 | break 42 | } 43 | 44 | if strings.ToLower(guess) == "quit" { 45 | fmt.Print("Quitter!\r\n") 46 | break 47 | } 48 | var g int 49 | g, err = strconv.Atoi(guess) 50 | if err != nil { 51 | fmt.Printf("Your entry, '%s', doesn't appear to be a valid number\r\n", guess) 52 | continue 53 | } 54 | 55 | if g < 1 || g > 10 { 56 | fmt.Print("Please enter a number in the range of 1 to 10, inclusive\r\n") 57 | continue 58 | } 59 | 60 | if g == number { 61 | fmt.Print("Correct!!\r\n") 62 | break 63 | } 64 | 65 | fmt.Printf("%d is WRONG! Try again!\r\n", g) 66 | } 67 | 68 | fmt.Printf("Your entire list of keystrokes was %d runes long\r\n", len(keyLogger)) 69 | fmt.Printf("The keystrokes were %#v\r\n", string(keyLogger)) 70 | } 71 | -------------------------------------------------------------------------------- /example/simple.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "math/rand" 6 | "os" 7 | "strings" 8 | "time" 9 | "unicode" 10 | 11 | "github.com/Nerdmaster/terminal" 12 | ) 13 | 14 | const CSI = "\x1b[" 15 | const ClearScreen = CSI + "2J" + CSI + ";H" 16 | 17 | var done bool 18 | var noise [][]rune 19 | var nextNoise [][]rune 20 | var userInput string 21 | var t *terminal.Reader 22 | 23 | var validRunes = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890!@#$%^&*") 24 | 25 | func onKeypress(e *terminal.KeyEvent) { 26 | // Ignoring a key entirely: how can I ki amias without the key? 27 | if e.Key == 'l' { 28 | e.IgnoreDefaultHandlers = true 29 | } 30 | 31 | // Tab-complete example: matches foobar and autocompletes it if: 32 | // - At least two characters have been entered 33 | // - All entered characters are prefix of "foobar" 34 | // - Cursor is at the end of the line 35 | // - Current line hasn't already printed out "foobar" 36 | if e.Key == 0x09 { 37 | var runes = []rune("foobar") 38 | if len(e.Line.Text) < 2 || len(e.Line.Text) >= len(runes) || e.Line.Pos != len(e.Line.Text) { 39 | return 40 | } 41 | 42 | for i, r := range e.Line.Text { 43 | if r != runes[i] { 44 | return 45 | } 46 | } 47 | 48 | e.Line.Text = runes 49 | e.Line.Pos = len(runes) 50 | e.IgnoreDefaultHandlers = true 51 | return 52 | } 53 | 54 | // Modifications to entire line based on use of Page Up key 55 | if e.Key == terminal.KeyPgUp && e.Modifier == terminal.ModNone { 56 | for i, r := range e.Line.Text { 57 | e.Line.Text[i] = unicode.SimpleFold(r) 58 | } 59 | e.IgnoreDefaultHandlers = true 60 | } 61 | 62 | // Replacing a key 63 | if e.Key == terminal.KeyPgUp && e.Modifier == terminal.ModAlt { 64 | e.Key = 'ģ' 65 | } 66 | 67 | // Ignoring a key while still implementing some kind of behavior 68 | if e.Key == terminal.KeyLeft && e.Modifier == terminal.ModNone { 69 | fmt.Print(ClearScreen) 70 | e.IgnoreDefaultHandlers = true 71 | } 72 | } 73 | 74 | func printAt(x, y int, output string) { 75 | fmt.Fprintf(os.Stdout, "%s%d;%dH%s", CSI, y, x, output) 76 | } 77 | 78 | func randomRune() rune { 79 | return validRunes[rand.Intn(len(validRunes))] 80 | } 81 | 82 | func initializeScreen() { 83 | // Clear everything 84 | fmt.Fprintf(os.Stdout, ClearScreen) 85 | 86 | // Print initial runes 87 | for y := 0; y < 10; y++ { 88 | printAt(1, y+1, string(noise[y])) 89 | } 90 | 91 | // Print prompt 92 | printAt(13, 4, "> ") 93 | } 94 | 95 | func setupNoise() { 96 | noise = make([][]rune, 10) 97 | for y := range noise { 98 | noise[y] = make([]rune, 100) 99 | for x := range noise[y] { 100 | if y == 3 && x > 10 && x < 90 { 101 | noise[y][x] = ' ' 102 | } else { 103 | noise[y][x] = randomRune() 104 | } 105 | } 106 | } 107 | nextNoise = make([][]rune, 10) 108 | for y := range nextNoise { 109 | nextNoise[y] = make([]rune, 100) 110 | for x := range nextNoise[y] { 111 | nextNoise[y][x] = noise[y][x] 112 | } 113 | } 114 | } 115 | 116 | func main() { 117 | rand.Seed(time.Now().UTC().UnixNano()) 118 | 119 | oldState, err := terminal.MakeRaw(int(os.Stdin.Fd())) 120 | if err != nil { 121 | panic(err) 122 | } 123 | defer terminal.Restore(0, oldState) 124 | 125 | setupNoise() 126 | initializeScreen() 127 | 128 | t = terminal.NewReader(os.Stdin) 129 | t.MaxLineLength = 70 130 | t.OnKeypress = onKeypress 131 | go readInput() 132 | go printOutput() 133 | 134 | for done == false { 135 | x := rand.Intn(100) 136 | y := rand.Intn(10) 137 | nextNoise[y][x] = randomRune() 138 | time.Sleep(time.Millisecond * 10) 139 | } 140 | } 141 | 142 | func readInput() { 143 | for { 144 | command, err := t.ReadLine() 145 | if command == "quit" { 146 | done = true 147 | return 148 | } 149 | if err != nil { 150 | done = true 151 | return 152 | } 153 | } 154 | } 155 | 156 | func printOutput() { 157 | lastLine := "" 158 | lastPos := 0 159 | 160 | for { 161 | // Print any changes to noise since last tick 162 | for y := 0; y < 10; y++ { 163 | for x := 0; x < 100; x++ { 164 | if y == 3 && x > 10 && x < 90 { 165 | nextNoise[y][x] = noise[y][x] 166 | } 167 | if noise[y][x] != nextNoise[y][x] { 168 | printAt(x+1, y+1, string(nextNoise[y][x])) 169 | noise[y][x] = nextNoise[y][x] 170 | } 171 | } 172 | } 173 | 174 | // Print any changes to user input since last tick 175 | newLine, newPos := t.LinePos() 176 | 177 | if lastLine != newLine { 178 | toPrint := newLine 179 | if len(lastLine) > len(newLine) { 180 | toPrint += strings.Repeat(" ", len(lastLine)-len(newLine)) 181 | } 182 | printAt(15, 4, toPrint) 183 | } 184 | 185 | if lastLine != newLine || lastPos != newPos { 186 | printAt(10, 20, strings.Repeat(" ", 100)) 187 | printAt(1, 20, fmt.Sprintf("Current line: '%s'; position: %d", newLine, newPos)) 188 | lastLine = newLine 189 | lastPos = newPos 190 | } 191 | 192 | printAt(15+newPos, 4, "") 193 | 194 | time.Sleep(time.Millisecond * 50) 195 | } 196 | } 197 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/Nerdmaster/terminal 2 | 3 | go 1.12 4 | -------------------------------------------------------------------------------- /key_constants.go: -------------------------------------------------------------------------------- 1 | package terminal 2 | 3 | // Giant list of key constants. Everything above KeyUnknown matches an actual 4 | // ASCII key value. After that, we have various pseudo-keys in order to 5 | // represent complex byte sequences that correspond to keys like Page up, Right 6 | // arrow, etc. 7 | const ( 8 | KeyCtrlA = 1 + iota 9 | KeyCtrlB 10 | KeyCtrlC 11 | KeyCtrlD 12 | KeyCtrlE 13 | KeyCtrlF 14 | KeyCtrlG 15 | KeyCtrlH 16 | KeyCtrlI 17 | KeyCtrlJ 18 | KeyCtrlK 19 | KeyCtrlL 20 | KeyCtrlM 21 | KeyCtrlN 22 | KeyCtrlO 23 | KeyCtrlP 24 | KeyCtrlQ 25 | KeyCtrlR 26 | KeyCtrlS 27 | KeyCtrlT 28 | KeyCtrlU 29 | KeyCtrlV 30 | KeyCtrlW 31 | KeyCtrlX 32 | KeyCtrlY 33 | KeyCtrlZ 34 | KeyEscape 35 | KeyLeftBracket = '[' 36 | KeyRightBracket = ']' 37 | KeyEnter = '\r' 38 | KeyBackspace = 127 39 | KeyUnknown = 0xd800 /* UTF-16 surrogate area */ + iota 40 | KeyUp 41 | KeyDown 42 | KeyLeft 43 | KeyRight 44 | KeyHome 45 | KeyEnd 46 | KeyPasteStart 47 | KeyPasteEnd 48 | KeyInsert 49 | KeyDelete 50 | KeyPgUp 51 | KeyPgDn 52 | KeyPause 53 | KeyF1 54 | KeyF2 55 | KeyF3 56 | KeyF4 57 | KeyF5 58 | KeyF6 59 | KeyF7 60 | KeyF8 61 | KeyF9 62 | KeyF10 63 | KeyF11 64 | KeyF12 65 | ) 66 | 67 | var pasteStart = []byte{KeyEscape, '[', '2', '0', '0', '~'} 68 | var pasteEnd = []byte{KeyEscape, '[', '2', '0', '1', '~'} 69 | -------------------------------------------------------------------------------- /keys.go: -------------------------------------------------------------------------------- 1 | package terminal 2 | 3 | import ( 4 | "io" 5 | "time" 6 | "unicode/utf8" 7 | ) 8 | 9 | // KeyModifier tells us what modifiers were pressed at the same time as a 10 | // normal key, such as CTRL, Alt, Meta, etc. 11 | type KeyModifier int 12 | 13 | // KeyModifier values. We don't include Shift in here because terminals don't 14 | // include shift for a great deal of keys that can exist; e.g., there is no 15 | // "SHIFT + PgUp". Similarly, CTRL doesn't make sense as a modifier in 16 | // terminals. CTRL+A is just ASCII character 1, whereas there is no CTRL+1, 17 | // and CTRL+Up is its own totally separate sequence from Up. So CTRL keys are 18 | // just defined on an as-needed basis. 19 | const ( 20 | ModNone KeyModifier = 0 21 | ModAlt = 1 22 | ModMeta = 2 23 | ) 24 | 25 | func (m KeyModifier) String() string { 26 | if m&ModAlt != 0 { 27 | if m&ModMeta != 0 { 28 | return "Meta+Alt" 29 | } 30 | return "Alt" 31 | } 32 | if m&ModMeta != 0 { 33 | return "Meta" 34 | } 35 | return "None" 36 | } 37 | 38 | // Keypress contains the data which made up a key: our internal KeyXXX constant 39 | // and the bytes which were parsed to get said constant. If the raw bytes need 40 | // to be held for any reason, they should be copied, not stored as-is, since 41 | // what's in here is a simple slice into the raw buffer. 42 | type Keypress struct { 43 | Key rune 44 | Modifier KeyModifier 45 | Size int 46 | Raw []byte 47 | } 48 | 49 | // KeyReader is the low-level type for reading raw keypresses from a given io 50 | // stream, usually stdin or an ssh socket. Stores raw bytes in a buffer so 51 | // that if many keys are read at once, they can still be parsed individually. 52 | type KeyReader struct { 53 | reader io.Reader 54 | 55 | // If ForceParse is true, the reader won't wait for certain sequences to 56 | // finish, which allows for things like ESC or Alt-left-bracket to be 57 | // detected properly 58 | ForceParse bool 59 | 60 | // remainder contains the remainder of any partial key sequences after 61 | // a read. It aliases into inBuf. 62 | remainder []byte 63 | inBuf [256]byte 64 | 65 | // firstRead tells us when a sequence started so we can properly "time out" a 66 | // previous sequence instead of keep adding to it indefinitely 67 | firstRead time.Time 68 | 69 | // offset stores the number of bytes in inBuf to skip next time a keypress is 70 | // read, allowing us to guarantee inBuf (and thus a Keypress's Raw bytes) 71 | // stays the same after returning. 72 | offset int 73 | 74 | // midRune is true when we believe we have a partial rune and need to read 75 | // more bytes 76 | midRune bool 77 | } 78 | 79 | // NewKeyReader returns a simple KeyReader set to read from r 80 | func NewKeyReader(r io.Reader) *KeyReader { 81 | return &KeyReader{reader: r} 82 | } 83 | 84 | // ReadKeypress reads the next key sequence, returning a Keypress object and 85 | // possibly an error if the input stream can't be read for some reason. This 86 | // will block if the buffer has no more data, which would obviously require a 87 | // direct Read call on the underlying io.Reader. 88 | func (r *KeyReader) ReadKeypress() (Keypress, error) { 89 | // Unshift from inBuf if we have an offset from a prior read 90 | if r.offset > 0 { 91 | var rest = r.remainder[r.offset:] 92 | if len(rest) > 0 { 93 | var n = copy(r.inBuf[:], rest) 94 | r.remainder = r.inBuf[:n] 95 | } else { 96 | r.remainder = nil 97 | } 98 | 99 | r.offset = 0 100 | } 101 | 102 | var remLen = len(r.remainder) 103 | if r.midRune || remLen == 0 { 104 | // r.remainder is a slice at the beginning of r.inBuf 105 | // containing a partial key sequence 106 | readBuf := r.inBuf[len(r.remainder):] 107 | 108 | n, err := r.reader.Read(readBuf) 109 | if err != nil { 110 | return Keypress{}, err 111 | } 112 | 113 | // After a read, we assume we are not mid-rune, and we adjust remainder to 114 | // include what was just read 115 | r.midRune = false 116 | r.remainder = r.inBuf[:n+len(r.remainder)] 117 | 118 | // If we had previous data, but it's been long enough since the first read 119 | // in that sequence (>250ms), we force-parse the previous sequence and 120 | // return it. We have a one-key "lag", but this allows things like Escape 121 | // + X to be handled properly and separately even without ForceParse. 122 | if remLen > 0 { 123 | if time.Since(r.firstRead) > time.Millisecond*250 { 124 | key, i, mod := ParseKey(r.remainder[:remLen], true) 125 | var kp = Keypress{Key: key, Size: i, Modifier: mod, Raw: r.remainder[:i]} 126 | r.offset = i 127 | return kp, nil 128 | } 129 | } else { 130 | // We can safely assume this is the first read 131 | r.firstRead = time.Now() 132 | } 133 | } 134 | 135 | // We must have bytes here; try to parse a key 136 | key, i, mod := ParseKey(r.remainder, r.ForceParse) 137 | 138 | // Rune errors combined with a zero-length character mean we've got a partial 139 | // rune; invalid bytes get treated by utf8.DecodeRune as a 1-byte RuneError 140 | if i == 0 && key == utf8.RuneError { 141 | r.midRune = true 142 | } 143 | 144 | var kp = Keypress{Key: key, Size: i, Modifier: mod, Raw: r.remainder[:i]} 145 | 146 | // Store new offset so we can adjust the buffer next loop 147 | r.offset = i 148 | 149 | return kp, nil 150 | } 151 | 152 | func isPrintable(key rune) bool { 153 | isInSurrogateArea := key >= 0xd800 && key <= 0xdbff 154 | return key >= 32 && !isInSurrogateArea 155 | } 156 | -------------------------------------------------------------------------------- /line.go: -------------------------------------------------------------------------------- 1 | package terminal 2 | 3 | // Line manages a very encapsulated version of a terminal line's state 4 | type Line struct { 5 | Text []rune 6 | Pos int 7 | } 8 | 9 | // Set overwrites Text and Pos with t and p, respectively 10 | func (l *Line) Set(t []rune, p int) { 11 | l.Text = t 12 | l.Pos = p 13 | } 14 | 15 | // Clear erases the input line 16 | func (l *Line) Clear() { 17 | l.Text = l.Text[:0] 18 | l.Pos = 0 19 | } 20 | 21 | // AddKeyToLine inserts the given key at the current position in the current 22 | // line. 23 | func (l *Line) AddKeyToLine(key rune) { 24 | if len(l.Text) == cap(l.Text) { 25 | newLine := make([]rune, len(l.Text), 2*(1+len(l.Text))) 26 | copy(newLine, l.Text) 27 | l.Text = newLine 28 | } 29 | l.Text = l.Text[:len(l.Text)+1] 30 | copy(l.Text[l.Pos+1:], l.Text[l.Pos:]) 31 | l.Text[l.Pos] = key 32 | l.Pos++ 33 | } 34 | 35 | // String just returns l.Text's runes as a single string 36 | func (l *Line) String() string { 37 | return string(l.Text) 38 | } 39 | 40 | // Split returns everything to the left of the cursor and everything at and to 41 | // the right of the cursor as two strings 42 | func (l *Line) Split() (string, string) { 43 | return string(l.Text[:l.Pos]), string(l.Text[l.Pos:]) 44 | } 45 | 46 | // EraseNPreviousChars deletes n characters from l.Text and updates l.Pos 47 | func (l *Line) EraseNPreviousChars(n int) { 48 | if l.Pos == 0 || n == 0 { 49 | return 50 | } 51 | 52 | if l.Pos < n { 53 | n = l.Pos 54 | } 55 | l.Pos -= n 56 | 57 | copy(l.Text[l.Pos:], l.Text[n+l.Pos:]) 58 | l.Text = l.Text[:len(l.Text)-n] 59 | } 60 | 61 | // DeleteLine removes all runes after the cursor position 62 | func (l *Line) DeleteLine() { 63 | l.Text = l.Text[:l.Pos] 64 | } 65 | 66 | // DeleteRuneUnderCursor erases the character under the current position 67 | func (l *Line) DeleteRuneUnderCursor() { 68 | if l.Pos < len(l.Text) { 69 | l.MoveRight() 70 | l.EraseNPreviousChars(1) 71 | } 72 | } 73 | 74 | // DeleteToBeginningOfLine removes everything behind the cursor 75 | func (l *Line) DeleteToBeginningOfLine() { 76 | l.EraseNPreviousChars(l.Pos) 77 | } 78 | 79 | // CountToLeftWord returns then number of characters from the cursor to the 80 | // start of the previous word 81 | func (l *Line) CountToLeftWord() int { 82 | if l.Pos == 0 { 83 | return 0 84 | } 85 | 86 | pos := l.Pos - 1 87 | for pos > 0 { 88 | if l.Text[pos] != ' ' { 89 | break 90 | } 91 | pos-- 92 | } 93 | for pos > 0 { 94 | if l.Text[pos] == ' ' { 95 | pos++ 96 | break 97 | } 98 | pos-- 99 | } 100 | 101 | return l.Pos - pos 102 | } 103 | 104 | // MoveToLeftWord moves pos to the first rune of the word to the left 105 | func (l *Line) MoveToLeftWord() { 106 | l.Pos -= l.CountToLeftWord() 107 | } 108 | 109 | // CountToRightWord returns then number of characters from the cursor to the 110 | // start of the next word 111 | func (l *Line) CountToRightWord() int { 112 | pos := l.Pos 113 | for pos < len(l.Text) { 114 | if l.Text[pos] == ' ' { 115 | break 116 | } 117 | pos++ 118 | } 119 | for pos < len(l.Text) { 120 | if l.Text[pos] != ' ' { 121 | break 122 | } 123 | pos++ 124 | } 125 | return pos - l.Pos 126 | } 127 | 128 | // MoveToRightWord moves pos to the first rune of the word to the right 129 | func (l *Line) MoveToRightWord() { 130 | l.Pos += l.CountToRightWord() 131 | } 132 | 133 | // MoveLeft moves pos one rune left 134 | func (l *Line) MoveLeft() { 135 | if l.Pos == 0 { 136 | return 137 | } 138 | 139 | l.Pos-- 140 | } 141 | 142 | // MoveRight moves pos one rune right 143 | func (l *Line) MoveRight() { 144 | if l.Pos == len(l.Text) { 145 | return 146 | } 147 | 148 | l.Pos++ 149 | } 150 | 151 | // MoveHome moves the cursor to the beginning of the line 152 | func (l *Line) MoveHome() { 153 | l.Pos = 0 154 | } 155 | 156 | // MoveEnd puts the cursor at the end of the line 157 | func (l *Line) MoveEnd() { 158 | l.Pos = len(l.Text) 159 | } 160 | -------------------------------------------------------------------------------- /prompt.go: -------------------------------------------------------------------------------- 1 | package terminal 2 | 3 | import ( 4 | "io" 5 | "strconv" 6 | ) 7 | 8 | // A Prompt is a wrapper around a Reader which will write a prompt, wait for 9 | // a user's input, and return it. It will print whatever needs to be printed 10 | // on demand to an io.Writer. The Prompt stores the Reader's prior state in 11 | // order to avoid unnecessary writes. 12 | type Prompt struct { 13 | *Reader 14 | prompt []byte 15 | Out io.Writer 16 | 17 | // lastOutput mirrors whatever was last printed to the console 18 | lastOutput []rune 19 | 20 | // nextOutput is built as we determine what needs printing, and then whatever 21 | // parts have changed from lastOutput to nextOutput are printed 22 | nextOutput []rune 23 | 24 | // lastCurPos stores the previous physical cursor position on the screen. 25 | // This is a screen position relative to the user's input, not the location 26 | // within the full string 27 | lastCurPos int 28 | 29 | // AfterKeypress shadows the Reader variable of the same name to allow custom 30 | // keypress listeners even though Prompt has to listen in order to write output 31 | AfterKeypress func(event *KeyEvent) 32 | 33 | // moveBytes just holds onto the byte slice we use for cursor movement to 34 | // avoid every cursor move requesting tiny bits of memory 35 | moveBytes []byte 36 | 37 | // Scroller processes the pending output to figure out if scrolling is 38 | // necessary and what should be printed if so 39 | Scroller *Scroller 40 | } 41 | 42 | // NewPrompt returns a prompt which will read lines from r, write its 43 | // prompt and current line to w, and use p as the prompt string. 44 | func NewPrompt(r io.Reader, w io.Writer, p string) *Prompt { 45 | var prompt = &Prompt{ 46 | Reader: NewReader(r), 47 | Out: w, 48 | moveBytes: make([]byte, 2, 16), 49 | } 50 | 51 | prompt.Scroller = NewScroller() 52 | 53 | // Default input width is "unlimited"; line length is set to the same value 54 | // to avoid scrolling 55 | prompt.Scroller.InputWidth = 9999 56 | prompt.Scroller.MaxLineLength = 9999 57 | 58 | prompt.Reader.AfterKeypress = prompt.afterKeyPress 59 | prompt.SetPrompt(p) 60 | 61 | // Set up the constant moveBytes prefix 62 | prompt.moveBytes[0] = '\x1b' 63 | prompt.moveBytes[1] = '[' 64 | 65 | return prompt 66 | } 67 | 68 | // ReadLine delegates to the reader's ReadLine function 69 | func (p *Prompt) ReadLine() (string, error) { 70 | p.lastOutput = p.lastOutput[:0] 71 | p.lastCurPos = 0 72 | p.Scroller.Reset() 73 | p.MaxLineLength = p.Scroller.MaxLineLength 74 | 75 | p.Out.Write(p.prompt) 76 | line, err := p.Reader.ReadLine() 77 | p.Out.Write(CRLF) 78 | 79 | return line, err 80 | } 81 | 82 | // SetPrompt changes the current prompt 83 | func (p *Prompt) SetPrompt(s string) { 84 | p.prompt = []byte(s) 85 | } 86 | 87 | // afterKeyPress calls Prompt's key handler to draw changes, then the user- 88 | // defined callback if present 89 | func (p *Prompt) afterKeyPress(e *KeyEvent) { 90 | // We never write changes when enter is pressed, because the line has been 91 | // cleared by the Reader, and is about to be returned 92 | if e.Key != KeyEnter { 93 | p.writeChanges(e) 94 | } 95 | if p.AfterKeypress != nil { 96 | p.AfterKeypress(e) 97 | } 98 | } 99 | 100 | // writeChanges checks for differences in whatever was previously written to 101 | // the console and the new line, attempting to draw the smallest amount of data 102 | // to get things back in sync 103 | func (p *Prompt) writeChanges(e *KeyEvent) { 104 | var out, curPos = p.Scroller.Filter(e.Line) 105 | p.nextOutput = append(p.nextOutput[:0], out...) 106 | 107 | // Pad output if it's shorter than last output 108 | var outputLen = len(p.nextOutput) 109 | for outputLen < len(p.lastOutput) { 110 | p.nextOutput = append(p.nextOutput, ' ') 111 | outputLen++ 112 | } 113 | 114 | // Compare last output with what we need to print next so we only redraw 115 | // starting from where they differ 116 | var index = runesDiffer(p.lastOutput, p.nextOutput) 117 | if index >= 0 { 118 | p.moveCursor(index) 119 | var out = p.nextOutput[index:] 120 | p.lastCurPos += len(out) 121 | p.Out.Write([]byte(string(out))) 122 | p.lastOutput = append(p.lastOutput[:0], p.nextOutput...) 123 | } 124 | 125 | // Make sure that after all the redrawing, the cursor gets back to where it should be 126 | p.moveCursor(curPos) 127 | } 128 | 129 | // moveCursor moves the cursor to the given x location (relative to the 130 | // beginning of the user's input area) 131 | func (p *Prompt) moveCursor(x int) { 132 | if x >= p.Scroller.InputWidth { 133 | x = p.Scroller.InputWidth - 1 134 | } 135 | 136 | var dx = x - p.lastCurPos 137 | p.lastCurPos = x 138 | 139 | if dx == 0 { 140 | return 141 | } 142 | 143 | var seq []byte = p.moveBytes[:2] 144 | 145 | var last byte 146 | if dx > 0 { 147 | last = 'C' 148 | } else { 149 | dx = -dx 150 | last = 'D' 151 | } 152 | 153 | // For the most common cases, let's make this simpler 154 | if dx == 1 { 155 | seq = append(seq, last) 156 | } else if dx < 10 { 157 | seq = append(seq, '0'+byte(dx), last) 158 | } else { 159 | var dxString = strconv.Itoa(dx) 160 | seq = append(seq, []byte(dxString)...) 161 | seq = append(seq, last) 162 | } 163 | p.Out.Write(seq) 164 | } 165 | -------------------------------------------------------------------------------- /reader.go: -------------------------------------------------------------------------------- 1 | // Portions Copyright 2011- The Go Authors. All rights reserved. 2 | // Portions Copyright 2016- Jeremy Echols. All rights reserved. 3 | // Use of this source code is governed by a BSD-style 4 | // license that can be found in the LICENSE file. 5 | 6 | package terminal 7 | 8 | import ( 9 | "io" 10 | "sync" 11 | "unicode/utf8" 12 | ) 13 | 14 | // DefaultMaxLineLength is the default MaxLineLength for a Reader; once a line 15 | // reaches this length, the Reader no longer accepts input which would increase 16 | // the line 17 | const DefaultMaxLineLength = 4096 18 | 19 | // KeyEvent is used for OnKeypress handlers to get the key and modify handler 20 | // state when the custom handler needs default handlers to be bypassed 21 | type KeyEvent struct { 22 | Keypress 23 | Line *Line 24 | IgnoreDefaultHandlers bool 25 | } 26 | 27 | // Reader contains the state for running a VT100 terminal that is capable of 28 | // reading lines of input. It is similar to the golang crypto/ssh/terminal 29 | // package except that it doesn't write, leaving that to the caller. The idea 30 | // is to store what the user is typing, and where the cursor should be, while 31 | // letting something else decide what to draw and where on the screen to draw 32 | // it. This separation enables more complex applications where there's other 33 | // real-time data being rendered at the same time as the input line. 34 | type Reader struct { 35 | // OnKeypress, if non-null, is called for each keypress with the key and 36 | // input line sent in 37 | OnKeypress func(event *KeyEvent) 38 | 39 | // AfterKeypress, if non-nil, is called after each keypress has been 40 | // processed. event should be considered read-only, as any changes will be 41 | // ignored since the key has already been processed. 42 | AfterKeypress func(event *KeyEvent) 43 | 44 | keyReader *KeyReader 45 | m sync.RWMutex 46 | 47 | // NoHistory is on when we don't want to preserve history, such as when a 48 | // password is being entered 49 | NoHistory bool 50 | 51 | // MaxLineLength tells us when to stop accepting input (other than things 52 | // like allowing up/down/left/right and other control keys) 53 | MaxLineLength int 54 | 55 | // CloseKey is the key which, when used on a terminal line by itself, closes 56 | // the terminal. Defaults to CTRL + D. 57 | CloseKey rune 58 | 59 | // line is the current line being entered, and the cursor position 60 | line *Line 61 | 62 | // pasteActive is true iff there is a bracketed paste operation in 63 | // progress. 64 | pasteActive bool 65 | 66 | // history contains previously entered commands so that they can be 67 | // accessed with the up and down keys. 68 | history stRingBuffer 69 | // historyIndex stores the currently accessed history entry, where zero 70 | // means the immediately previous entry. 71 | historyIndex int 72 | // When navigating up and down the history it's possible to return to 73 | // the incomplete, initial line. That value is stored in 74 | // historyPending. 75 | historyPending string 76 | } 77 | 78 | // NewReader runs a terminal reader on the given io.Reader. If the Reader is a 79 | // local terminal, that terminal must first have been put into raw mode. 80 | func NewReader(r io.Reader) *Reader { 81 | return &Reader{ 82 | keyReader: NewKeyReader(r), 83 | MaxLineLength: DefaultMaxLineLength, 84 | CloseKey: KeyCtrlD, 85 | historyIndex: -1, 86 | line: &Line{}, 87 | } 88 | } 89 | 90 | // handleKeypress processes the given keypress data and, optionally, returns a 91 | // line of text that the user has entered. 92 | func (r *Reader) handleKeypress(kp Keypress) (line string, ok bool) { 93 | r.m.Lock() 94 | defer r.m.Unlock() 95 | 96 | var e = &KeyEvent{Keypress: kp, Line: r.line} 97 | if r.OnKeypress != nil { 98 | r.OnKeypress(e) 99 | if e.IgnoreDefaultHandlers { 100 | return 101 | } 102 | kp.Key = e.Key 103 | } 104 | 105 | line, ok = r.processKeypress(kp) 106 | 107 | if r.AfterKeypress != nil { 108 | r.AfterKeypress(e) 109 | } 110 | return 111 | } 112 | 113 | // processKeypress applies all non-overrideable logic needed for various 114 | // keypresses to have their desired effects 115 | func (r *Reader) processKeypress(kp Keypress) (output string, ok bool) { 116 | var key = kp.Key 117 | var line = r.line 118 | if r.pasteActive && key != KeyEnter { 119 | line.AddKeyToLine(key) 120 | return 121 | } 122 | 123 | if kp.Modifier == ModAlt { 124 | switch key { 125 | case KeyLeft: 126 | line.MoveToLeftWord() 127 | case KeyRight: 128 | line.MoveToRightWord() 129 | } 130 | } 131 | 132 | if kp.Modifier != ModNone { 133 | return 134 | } 135 | 136 | switch key { 137 | case KeyBackspace, KeyCtrlH: 138 | line.EraseNPreviousChars(1) 139 | case KeyLeft: 140 | line.MoveLeft() 141 | case KeyRight: 142 | line.MoveRight() 143 | case KeyHome, KeyCtrlA: 144 | line.MoveHome() 145 | case KeyEnd, KeyCtrlE: 146 | line.MoveEnd() 147 | case KeyUp: 148 | fetched := r.fetchPreviousHistory() 149 | if !fetched { 150 | return "", false 151 | } 152 | case KeyDown: 153 | r.fetchNextHistory() 154 | case KeyEnter: 155 | output = line.String() 156 | ok = true 157 | line.Clear() 158 | case KeyCtrlW: 159 | line.EraseNPreviousChars(line.CountToLeftWord()) 160 | case KeyCtrlK: 161 | line.DeleteLine() 162 | case KeyCtrlD, KeyDelete: 163 | line.DeleteRuneUnderCursor() 164 | case KeyCtrlU: 165 | line.DeleteToBeginningOfLine() 166 | default: 167 | if !isPrintable(key) { 168 | return 169 | } 170 | if len(line.Text) == r.MaxLineLength { 171 | return 172 | } 173 | line.AddKeyToLine(key) 174 | } 175 | return 176 | } 177 | 178 | // ReadPassword temporarily reads a password without saving to history 179 | func (r *Reader) ReadPassword() (line string, err error) { 180 | oldNoHistory := r.NoHistory 181 | r.NoHistory = true 182 | line, err = r.ReadLine() 183 | r.NoHistory = oldNoHistory 184 | return 185 | } 186 | 187 | // ReadLine returns a line of input from the terminal. 188 | func (r *Reader) ReadLine() (line string, err error) { 189 | lineIsPasted := r.pasteActive 190 | 191 | for { 192 | lineOk := false 193 | for !lineOk { 194 | var kp Keypress 195 | kp, err = r.keyReader.ReadKeypress() 196 | if err != nil { 197 | return 198 | } 199 | 200 | key := kp.Key 201 | if key == utf8.RuneError { 202 | break 203 | } 204 | 205 | r.m.RLock() 206 | lineLen := len(r.line.Text) 207 | r.m.RUnlock() 208 | 209 | if !r.pasteActive { 210 | if key == r.CloseKey { 211 | if lineLen == 0 { 212 | return "", io.EOF 213 | } 214 | } 215 | if key == KeyPasteStart { 216 | r.pasteActive = true 217 | if lineLen == 0 { 218 | lineIsPasted = true 219 | } 220 | continue 221 | } 222 | } else if key == KeyPasteEnd { 223 | r.pasteActive = false 224 | continue 225 | } 226 | if !r.pasteActive { 227 | lineIsPasted = false 228 | } 229 | line, lineOk = r.handleKeypress(kp) 230 | } 231 | 232 | if lineOk { 233 | if !r.NoHistory { 234 | r.historyIndex = -1 235 | r.history.Add(line) 236 | } 237 | if lineIsPasted { 238 | err = ErrPasteIndicator 239 | } 240 | return 241 | } 242 | } 243 | } 244 | 245 | // LinePos returns the current input line and cursor position 246 | func (r *Reader) LinePos() (string, int) { 247 | r.m.RLock() 248 | defer r.m.RUnlock() 249 | return r.line.String(), r.line.Pos 250 | } 251 | 252 | // Pos returns the position of the cursor 253 | func (r *Reader) Pos() int { 254 | r.m.RLock() 255 | defer r.m.RUnlock() 256 | return r.line.Pos 257 | } 258 | 259 | // fetchPreviousHistory sets the input line to the previous entry in our history 260 | func (r *Reader) fetchPreviousHistory() bool { 261 | // lock has to be held here 262 | if r.NoHistory { 263 | return false 264 | } 265 | 266 | entry, ok := r.history.NthPreviousEntry(r.historyIndex + 1) 267 | if !ok { 268 | return false 269 | } 270 | if r.historyIndex == -1 { 271 | r.historyPending = string(r.line.Text) 272 | } 273 | r.historyIndex++ 274 | runes := []rune(entry) 275 | r.line.Set(runes, len(runes)) 276 | return true 277 | } 278 | 279 | // fetchNextHistory sets the input line to the next entry in our history 280 | func (r *Reader) fetchNextHistory() { 281 | // lock has to be held here 282 | if r.NoHistory { 283 | return 284 | } 285 | 286 | switch r.historyIndex { 287 | case -1: 288 | return 289 | case 0: 290 | runes := []rune(r.historyPending) 291 | r.line.Set(runes, len(runes)) 292 | r.historyIndex-- 293 | default: 294 | entry, ok := r.history.NthPreviousEntry(r.historyIndex - 1) 295 | if ok { 296 | r.historyIndex-- 297 | runes := []rune(entry) 298 | r.line.Set(runes, len(runes)) 299 | } 300 | } 301 | } 302 | 303 | type pasteIndicatorError struct{} 304 | 305 | func (pasteIndicatorError) Error() string { 306 | return "terminal: ErrPasteIndicator not correctly handled" 307 | } 308 | 309 | // ErrPasteIndicator may be returned from ReadLine as the error, in addition 310 | // to valid line data. It indicates that bracketed paste mode is enabled and 311 | // that the returned line consists only of pasted data. Programs may wish to 312 | // interpret pasted data more literally than typed data. 313 | var ErrPasteIndicator = pasteIndicatorError{} 314 | 315 | // stRingBuffer is a ring buffer of strings. 316 | type stRingBuffer struct { 317 | // entries contains max elements. 318 | entries []string 319 | max int 320 | // head contains the index of the element most recently added to the ring. 321 | head int 322 | // size contains the number of elements in the ring. 323 | size int 324 | } 325 | 326 | func (s *stRingBuffer) Add(a string) { 327 | if s.entries == nil { 328 | const defaultNumEntries = 100 329 | s.entries = make([]string, defaultNumEntries) 330 | s.max = defaultNumEntries 331 | } 332 | 333 | s.head = (s.head + 1) % s.max 334 | s.entries[s.head] = a 335 | if s.size < s.max { 336 | s.size++ 337 | } 338 | } 339 | 340 | // NthPreviousEntry returns the value passed to the nth previous call to Add. 341 | // If n is zero then the immediately prior value is returned, if one, then the 342 | // next most recent, and so on. If such an element doesn't exist then ok is 343 | // false. 344 | func (s *stRingBuffer) NthPreviousEntry(n int) (value string, ok bool) { 345 | if n >= s.size { 346 | return "", false 347 | } 348 | index := s.head - n 349 | if index < 0 { 350 | index += s.max 351 | } 352 | return s.entries[index], true 353 | } 354 | -------------------------------------------------------------------------------- /reader_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2011 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package terminal 6 | 7 | import ( 8 | "io" 9 | "os" 10 | "testing" 11 | ) 12 | 13 | type MockReader struct { 14 | toSend []byte 15 | bytesPerRead int 16 | received []byte 17 | } 18 | 19 | func (c *MockReader) Read(data []byte) (n int, err error) { 20 | n = len(data) 21 | if n == 0 { 22 | return 23 | } 24 | if n > len(c.toSend) { 25 | n = len(c.toSend) 26 | } 27 | if n == 0 { 28 | return 0, io.EOF 29 | } 30 | if c.bytesPerRead > 0 && n > c.bytesPerRead { 31 | n = c.bytesPerRead 32 | } 33 | copy(data, c.toSend[:n]) 34 | c.toSend = c.toSend[n:] 35 | return 36 | } 37 | 38 | func (c *MockReader) Write(data []byte) (n int, err error) { 39 | c.received = append(c.received, data...) 40 | return len(data), nil 41 | } 42 | 43 | func TestClose(t *testing.T) { 44 | c := &MockReader{} 45 | ss := NewReader(c) 46 | line, err := ss.ReadLine() 47 | if line != "" { 48 | t.Errorf("Expected empty line but got: %s", line) 49 | } 50 | if err != io.EOF { 51 | t.Errorf("Error should have been EOF but got: %s", err) 52 | } 53 | } 54 | 55 | var keyPressTests = []struct { 56 | in string 57 | line string 58 | err error 59 | throwAwayLines int 60 | }{ 61 | { 62 | err: io.EOF, 63 | }, 64 | { 65 | in: "\r", 66 | line: "", 67 | }, 68 | { 69 | in: "foo\r", 70 | line: "foo", 71 | }, 72 | { 73 | in: "a\x1b[Cb\r", // right 74 | line: "ab", 75 | }, 76 | { 77 | in: "a\x1b[Db\r", // left 78 | line: "ba", 79 | }, 80 | { 81 | in: "a\177b\r", // backspace 82 | line: "b", 83 | }, 84 | { 85 | in: "\x1b[A\r", // up 86 | }, 87 | { 88 | in: "\x1b[B\r", // down 89 | }, 90 | { 91 | in: "line\x1b[A\x1b[B\r", // up then down 92 | line: "line", 93 | }, 94 | { 95 | in: "line1\rline2\x1b[A\r", // recall previous line. 96 | line: "line1", 97 | throwAwayLines: 1, 98 | }, 99 | { 100 | // recall two previous lines and append. 101 | in: "line1\rline2\rline3\x1b[A\x1b[Axxx\r", 102 | line: "line1xxx", 103 | throwAwayLines: 2, 104 | }, 105 | { 106 | // Ctrl-A to move to beginning of line followed by ^K to kill 107 | // line. 108 | in: "a b \001\013\r", 109 | line: "", 110 | }, 111 | { 112 | // Ctrl-A to move to beginning of line, Ctrl-E to move to end, 113 | // finally ^K to kill nothing. 114 | in: "a b \001\005\013\r", 115 | line: "a b ", 116 | }, 117 | { 118 | // CTRL+A to move to beginning of line, right-arrow, add one character 119 | in: "tusting\001\x1b[Cr\r", 120 | line: "trusting", 121 | }, 122 | { 123 | // Home to move to beginning of line, right-arrow, add one character, End, add one character 124 | in: "tustin\x1b[1~\x1b[Cr\x1b[4~g\r", 125 | line: "trusting", 126 | }, 127 | { 128 | in: "\027\r", 129 | line: "", 130 | }, 131 | { 132 | in: "a\027\r", 133 | line: "", 134 | }, 135 | { 136 | in: "a \027\r", 137 | line: "", 138 | }, 139 | { 140 | in: "a b\027\r", 141 | line: "a ", 142 | }, 143 | { 144 | in: "a b \027\r", 145 | line: "a ", 146 | }, 147 | { 148 | in: "one two thr\x1b[D\027\r", 149 | line: "one two r", 150 | }, 151 | { 152 | in: "\013\r", 153 | line: "", 154 | }, 155 | { 156 | in: "a\013\r", 157 | line: "a", 158 | }, 159 | { 160 | in: "ab\x1b[D\013\r", 161 | line: "a", 162 | }, 163 | { 164 | in: "Ξεσκεπάζω\r", 165 | line: "Ξεσκεπάζω", 166 | }, 167 | { 168 | in: "£\r\x1b[A\177\r", // non-ASCII char, enter, up, backspace. 169 | line: "", 170 | throwAwayLines: 1, 171 | }, 172 | { 173 | in: "£\r££\x1b[A\x1b[B\177\r", // non-ASCII char, enter, 2x non-ASCII, up, down, backspace, enter. 174 | line: "£", 175 | throwAwayLines: 1, 176 | }, 177 | { 178 | // Ctrl-D at the end of the line should be ignored. 179 | in: "a\004\r", 180 | line: "a", 181 | }, 182 | { 183 | // a, b, left, Ctrl-D should erase the b. 184 | in: "ab\x1b[D\004\r", 185 | line: "a", 186 | }, 187 | { 188 | // a, b, c, d, left, left, ^U should erase to the beginning of 189 | // the line. 190 | in: "abcd\x1b[D\x1b[D\025\r", 191 | line: "cd", 192 | }, 193 | { 194 | // Bracketed paste mode: control sequences should be returned 195 | // verbatim in paste mode. 196 | in: "abc\x1b[200~de\177f\x1b[201~\177\r", 197 | line: "abcde\177", 198 | }, 199 | { 200 | // Enter in bracketed paste mode should still work. 201 | in: "abc\x1b[200~d\refg\x1b[201~h\r", 202 | line: "efgh", 203 | throwAwayLines: 1, 204 | }, 205 | { 206 | // Lines consisting entirely of pasted data should be indicated as such. 207 | in: "\x1b[200~a\r", 208 | line: "a", 209 | err: ErrPasteIndicator, 210 | }, 211 | } 212 | 213 | func TestKeyPresses(t *testing.T) { 214 | for i, test := range keyPressTests { 215 | for j := 1; j < len(test.in); j++ { 216 | c := &MockReader{ 217 | toSend: []byte(test.in), 218 | bytesPerRead: j, 219 | } 220 | ss := NewReader(c) 221 | for k := 0; k < test.throwAwayLines; k++ { 222 | _, err := ss.ReadLine() 223 | if err != nil { 224 | t.Errorf("Throwaway line %d from test %d resulted in error: %s", k, i, err) 225 | } 226 | } 227 | line, err := ss.ReadLine() 228 | if line != test.line { 229 | t.Errorf("Line resulting from test %d (%d bytes per read) was '%s', expected '%s'", i, j, line, test.line) 230 | break 231 | } 232 | if err != test.err { 233 | t.Errorf("Error resulting from test %d (%d bytes per read) was '%v', expected '%v'", i, j, err, test.err) 234 | break 235 | } 236 | } 237 | } 238 | } 239 | 240 | func TestPasswordNotSaved(t *testing.T) { 241 | c := &MockReader{ 242 | toSend: []byte("password\r\x1b[A\r"), 243 | bytesPerRead: 1, 244 | } 245 | ss := NewReader(c) 246 | pw, _ := ss.ReadPassword() 247 | if pw != "password" { 248 | t.Fatalf("failed to read password, got %s", pw) 249 | } 250 | line, _ := ss.ReadLine() 251 | if len(line) > 0 { 252 | t.Fatalf("password %s was saved in history", line) 253 | } 254 | } 255 | 256 | func TestReaderPassword(t *testing.T) { 257 | c := &MockReader{ 258 | toSend: []byte("password\r\x1b[A\r"), 259 | bytesPerRead: 1, 260 | } 261 | ss := NewReader(c) 262 | pw, _ := ss.ReadPassword() 263 | if pw != "password" { 264 | t.Fatalf("failed to read password, got %s", pw) 265 | } 266 | } 267 | 268 | func TestMaxLength(t *testing.T) { 269 | c := &MockReader{ 270 | toSend: []byte("this is too long blargh\r"), 271 | bytesPerRead: 1, 272 | } 273 | ss := NewReader(c) 274 | ss.MaxLineLength = 8 275 | line, _ := ss.ReadLine() 276 | if line != "this is " { 277 | t.Fatalf("failed to limit length, got '%s'", line) 278 | } 279 | } 280 | 281 | func TestMakeRawState(t *testing.T) { 282 | fd := int(os.Stdout.Fd()) 283 | if !IsTerminal(fd) { 284 | t.Skip("stdout is not a terminal; skipping test") 285 | } 286 | 287 | st, err := GetState(fd) 288 | if err != nil { 289 | t.Fatalf("failed to get terminal state from GetState: %s", err) 290 | } 291 | defer Restore(fd, st) 292 | raw, err := MakeRaw(fd) 293 | if err != nil { 294 | t.Fatalf("failed to get terminal state from MakeRaw: %s", err) 295 | } 296 | 297 | if *st != *raw { 298 | t.Errorf("states do not match; was %v, expected %v", raw, st) 299 | } 300 | } 301 | -------------------------------------------------------------------------------- /runes_differ.go: -------------------------------------------------------------------------------- 1 | package terminal 2 | 3 | // runesDiffer returns the first element where two slices of runes differ, or 4 | // -1 if the slices are the same length and each rune in each slice is the 5 | // same. If the two are equal up until one ends, the return will be wherever 6 | // the shortest one ended. 7 | func runesDiffer(a, b []rune) int { 8 | for i, r := range a { 9 | if len(b) <= i { 10 | return i 11 | } 12 | if r != b[i] { 13 | return i 14 | } 15 | } 16 | 17 | if len(b) > len(a) { 18 | return len(a) 19 | } 20 | 21 | return -1 22 | } 23 | -------------------------------------------------------------------------------- /runes_differ_test.go: -------------------------------------------------------------------------------- 1 | package terminal 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestRunesDiffer(t *testing.T) { 8 | var a = []rune("test 1") 9 | var b = []rune("test 2") 10 | 11 | if runesDiffer(a, b) != 5 { 12 | t.Error("Expected difference to occur at index 5") 13 | } 14 | 15 | b[5] = '1' 16 | if runesDiffer(a, b) != -1 { 17 | t.Error("Expected no differences") 18 | } 19 | 20 | b = []rune("test 123") 21 | if runesDiffer(a, b) != 6 { 22 | t.Error("Expected difference at index 6 (one character beyond a)") 23 | } 24 | 25 | b[3] = 'X' 26 | if runesDiffer(a, b) != 3 { 27 | t.Error("Expected difference at index 3 (mismatched length doesn't matter if they differ early)") 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /scroller.go: -------------------------------------------------------------------------------- 1 | package terminal 2 | 3 | // ScrollBy is the default value a scroller scrolls by when the cursor would 4 | // otherwise be outside the input area 5 | const ScrollBy = 10 6 | 7 | // A Scroller is a Line filter for taking the internal Line's state and giving 8 | // an output widget what should be drawn to the screen 9 | type Scroller struct { 10 | // InputWidth should be set to the terminal width or smaller. If this is 11 | // equal to or larger than MaxWidth, no scrolling will occur 12 | InputWidth int 13 | 14 | // MaxLineLength should be set to the maximum number of runes to allow in the 15 | // scrolling input. This should be set to the underlying Reader's 16 | // MaxLineLength or less, otherwise the Reader will block further input. 17 | MaxLineLength int 18 | 19 | // ScrollOffset is set to the number of characters which are "off-screen" to 20 | // the left of the input area; the input line displays just the characters 21 | // which are after this offset. This should typically not be adjusted 22 | // manually, but it may make sense to allow scrolling the input via a 23 | // keyboard shortcut that doesn't alter the line or cursor position. 24 | ScrollOffset int 25 | 26 | // LeftOverflow and RightOverflow are used to signify that the input is 27 | // scrolling left or right. They both default to the UTF ellipsis character, 28 | // but can be overridden as needed. If set to '', no overflow character will 29 | // be displayed when scrolling 30 | LeftOverflow, RightOverflow rune 31 | 32 | // ScrollBy is the number of runes we "shift" when the cursor would otherwise 33 | // leave the printable area; defaults to the ScrollBy package constant 34 | ScrollBy int 35 | 36 | // nextOutput is built as we determine what needs printing so we aren't 37 | // allocating memory all the time 38 | nextOutput []rune 39 | } 40 | 41 | // NewScroller creates a simple scroller instance with no limits. 42 | // MaxLineLength and InputWidth must be set before any of the scrolling logic 43 | // will kick in. Whatever uses this is responsible for setting InputWidth and 44 | // MaxLineLength to appropriate values. 45 | func NewScroller() *Scroller { 46 | return &Scroller{ 47 | InputWidth: -1, 48 | MaxLineLength: -1, 49 | ScrollBy: ScrollBy, 50 | LeftOverflow: '…', 51 | RightOverflow: '…', 52 | } 53 | } 54 | 55 | // Reset is called when a new line is being evaluated 56 | func (s *Scroller) Reset() { 57 | s.ScrollOffset = 0 58 | } 59 | 60 | // Filter looks at the Input's line and our scroll properties to figure out 61 | // if we should scroll, and what should be drawn in the input area 62 | func (s *Scroller) Filter(l *Line) ([]rune, int) { 63 | if s.InputWidth < 1 || s.MaxLineLength < 1 { 64 | return l.Text, l.Pos 65 | } 66 | 67 | // Check for new cursor location being off-screen 68 | var cursorLoc = l.Pos - s.ScrollOffset 69 | var lineLen = len(l.Text) 70 | 71 | // Too far left 72 | for cursorLoc <= 0 && s.ScrollOffset > 0 { 73 | s.ScrollOffset -= s.ScrollBy 74 | cursorLoc += s.ScrollBy 75 | } 76 | if s.ScrollOffset < 0 { 77 | s.ScrollOffset = 0 78 | } 79 | 80 | // Too far right 81 | var maxScroll = s.MaxLineLength - s.InputWidth 82 | for cursorLoc >= s.InputWidth-1 && s.ScrollOffset < maxScroll { 83 | s.ScrollOffset += s.ScrollBy 84 | cursorLoc -= s.ScrollBy 85 | } 86 | if s.ScrollOffset >= maxScroll { 87 | s.ScrollOffset = maxScroll 88 | } 89 | 90 | // Figure out what we need to output next by pulling just the parts of the 91 | // input runes that will be visible 92 | var end = s.ScrollOffset + s.InputWidth 93 | if end > lineLen { 94 | end = lineLen 95 | } 96 | s.nextOutput = append(s.nextOutput[:0], l.Text[s.ScrollOffset:end]...) 97 | if s.ScrollOffset > 0 && s.LeftOverflow != 0 { 98 | s.nextOutput[0] = s.LeftOverflow 99 | } 100 | if s.InputWidth+s.ScrollOffset < lineLen && s.RightOverflow != 0 { 101 | s.nextOutput[len(s.nextOutput)-1] = s.RightOverflow 102 | } 103 | 104 | return s.nextOutput, cursorLoc 105 | } 106 | -------------------------------------------------------------------------------- /terminal.go: -------------------------------------------------------------------------------- 1 | // Package terminal provides support functions for dealing with terminals, as 2 | // commonly found on UNIX systems. 3 | // 4 | // This is a completely standalone key / line reader. All types that read data 5 | // allow anything conforming to io.Reader. All types that write data allow 6 | // anything conforming to io.Writer. 7 | // 8 | // Putting a terminal into raw mode is the most common requirement, and can be 9 | // seen in the example. 10 | package terminal 11 | -------------------------------------------------------------------------------- /terminal_test.go: -------------------------------------------------------------------------------- 1 | package terminal_test 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "strings" 7 | 8 | "github.com/Nerdmaster/terminal" 9 | ) 10 | 11 | // Example of a very basic use of the Prompt type, which is probably the 12 | // simplest type available 13 | func Example() { 14 | // Put terminal in raw mode; this is almost always going to be required for 15 | // local terminals, but not necessary when connecting to an ssh terminal 16 | oldState, err := terminal.MakeRaw(int(os.Stdin.Fd())) 17 | if err != nil { 18 | panic(err) 19 | } 20 | defer terminal.Restore(0, oldState) 21 | 22 | // This is a simple example, so we just use Stdin and Stdout; but any reader 23 | // and writer will work. Note that the prompt can contain ANSI colors. 24 | var p = terminal.NewPrompt(os.Stdin, os.Stdout, "\x1b[34;1mCommand\x1b[0m: ") 25 | 26 | // This is a simple key interceptor; on CTRL-r we forcibly change the scroll to 20 27 | p.OnKeypress = func(e *terminal.KeyEvent) { 28 | if e.Key == terminal.KeyCtrlR { 29 | p.Scroller.ScrollOffset = 20 30 | } 31 | } 32 | 33 | // Make the input scroll at 40 characters, maxing out at a 120-char string 34 | p.Scroller.InputWidth = 40 35 | p.Scroller.MaxLineLength = 120 36 | 37 | // Loop forever until we get an error (typically EOF from user pressing 38 | // CTRL+D) or the "quit" command is entered. We echo each command unless the 39 | // user turns echoing off. 40 | var echo = true 41 | for { 42 | // Just for fun we can demonstrate that the cursor never touches anything 43 | // outside out 40-character input. First, we print padding for the prompt 44 | // ("Command: "). Then 40 spaces, a bar, and \r to return the cursor to 45 | // the beginning of the line. 46 | fmt.Printf(" ") 47 | fmt.Printf(strings.Repeat(" ", 40)) 48 | fmt.Printf("|\r") 49 | var cmd, err = p.ReadLine() 50 | if err != nil { 51 | fmt.Printf("%s\r\n", err) 52 | break 53 | } 54 | 55 | if strings.ToLower(cmd) == "quit" { 56 | fmt.Print("Quitter!\r\n") 57 | break 58 | } 59 | 60 | if strings.ToLower(cmd) == "echo on" { 61 | echo = true 62 | } 63 | if strings.ToLower(cmd) == "echo off" { 64 | echo = false 65 | } 66 | 67 | if echo { 68 | fmt.Printf("%#v\r\n", cmd) 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /util.go: -------------------------------------------------------------------------------- 1 | // Copyright 2011 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // +build darwin dragonfly freebsd linux,!appengine netbsd openbsd 6 | 7 | package terminal 8 | 9 | import ( 10 | "io" 11 | "syscall" 12 | "unsafe" 13 | ) 14 | 15 | // State contains the state of a terminal. 16 | type State struct { 17 | termios syscall.Termios 18 | } 19 | 20 | // VisualLength returns the number of visible glyphs in a string. This can be 21 | // useful for getting the length of a string which has ANSI color sequences, 22 | // but it doesn't count "wide" glyphs differently than others, and it won't 23 | // handle ANSI cursor commands; e.g., it ignores "\x1b[D" rather than knowing 24 | // that the cursor position moved to the left. 25 | func VisualLength(s string) int { 26 | runes := []rune(s) 27 | inEscapeSeq := false 28 | length := 0 29 | 30 | for _, r := range runes { 31 | switch { 32 | case inEscapeSeq: 33 | if (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') { 34 | inEscapeSeq = false 35 | } 36 | case r == '\x1b': 37 | inEscapeSeq = true 38 | default: 39 | length++ 40 | } 41 | } 42 | 43 | return length 44 | } 45 | 46 | // IsTerminal returns true if the given file descriptor is a terminal. 47 | func IsTerminal(fd int) bool { 48 | var termios syscall.Termios 49 | _, _, err := syscall.Syscall6(syscall.SYS_IOCTL, uintptr(fd), ioctlReadTermios, uintptr(unsafe.Pointer(&termios)), 0, 0, 0) 50 | return err == 0 51 | } 52 | 53 | // MakeRaw put the terminal connected to the given file descriptor into raw 54 | // mode and returns the previous state of the terminal so that it can be 55 | // restored. 56 | func MakeRaw(fd int) (*State, error) { 57 | var oldState State 58 | if _, _, err := syscall.Syscall6(syscall.SYS_IOCTL, uintptr(fd), ioctlReadTermios, uintptr(unsafe.Pointer(&oldState.termios)), 0, 0, 0); err != 0 { 59 | return nil, err 60 | } 61 | 62 | newState := oldState.termios 63 | // This attempts to replicate the behaviour documented for cfmakeraw in 64 | // the termios(3) manpage. 65 | newState.Iflag &^= syscall.IGNBRK | syscall.BRKINT | syscall.PARMRK | syscall.ISTRIP | syscall.INLCR | syscall.IGNCR | syscall.ICRNL | syscall.IXON 66 | newState.Oflag &^= syscall.OPOST 67 | newState.Lflag &^= syscall.ECHO | syscall.ECHONL | syscall.ICANON | syscall.ISIG | syscall.IEXTEN 68 | newState.Cflag &^= syscall.CSIZE | syscall.PARENB 69 | newState.Cflag |= syscall.CS8 70 | if _, _, err := syscall.Syscall6(syscall.SYS_IOCTL, uintptr(fd), ioctlWriteTermios, uintptr(unsafe.Pointer(&newState)), 0, 0, 0); err != 0 { 71 | return nil, err 72 | } 73 | 74 | return &oldState, nil 75 | } 76 | 77 | // GetState returns the current state of a terminal which may be useful to 78 | // restore the terminal after a signal. 79 | func GetState(fd int) (*State, error) { 80 | var oldState State 81 | if _, _, err := syscall.Syscall6(syscall.SYS_IOCTL, uintptr(fd), ioctlReadTermios, uintptr(unsafe.Pointer(&oldState.termios)), 0, 0, 0); err != 0 { 82 | return nil, err 83 | } 84 | 85 | return &oldState, nil 86 | } 87 | 88 | // Restore restores the terminal connected to the given file descriptor to a 89 | // previous state. 90 | func Restore(fd int, state *State) error { 91 | _, _, err := syscall.Syscall6(syscall.SYS_IOCTL, uintptr(fd), ioctlWriteTermios, uintptr(unsafe.Pointer(&state.termios)), 0, 0, 0) 92 | return err 93 | } 94 | 95 | // GetSize returns the dimensions of the given terminal. 96 | func GetSize(fd int) (width, height int, err error) { 97 | var dimensions [4]uint16 98 | 99 | if _, _, err := syscall.Syscall6(syscall.SYS_IOCTL, uintptr(fd), uintptr(syscall.TIOCGWINSZ), uintptr(unsafe.Pointer(&dimensions)), 0, 0, 0); err != 0 { 100 | return -1, -1, err 101 | } 102 | return int(dimensions[1]), int(dimensions[0]), nil 103 | } 104 | 105 | // ReadPassword reads a line of input from a terminal without local echo. This 106 | // is commonly used for inputting passwords and other sensitive data. The slice 107 | // returned does not include the \n. 108 | func ReadPassword(fd int) ([]byte, error) { 109 | var oldState syscall.Termios 110 | if _, _, err := syscall.Syscall6(syscall.SYS_IOCTL, uintptr(fd), ioctlReadTermios, uintptr(unsafe.Pointer(&oldState)), 0, 0, 0); err != 0 { 111 | return nil, err 112 | } 113 | 114 | newState := oldState 115 | newState.Lflag &^= syscall.ECHO 116 | newState.Lflag |= syscall.ICANON | syscall.ISIG 117 | newState.Iflag |= syscall.ICRNL 118 | if _, _, err := syscall.Syscall6(syscall.SYS_IOCTL, uintptr(fd), ioctlWriteTermios, uintptr(unsafe.Pointer(&newState)), 0, 0, 0); err != 0 { 119 | return nil, err 120 | } 121 | 122 | defer func() { 123 | syscall.Syscall6(syscall.SYS_IOCTL, uintptr(fd), ioctlWriteTermios, uintptr(unsafe.Pointer(&oldState)), 0, 0, 0) 124 | }() 125 | 126 | var buf [16]byte 127 | var ret []byte 128 | for { 129 | n, err := syscall.Read(fd, buf[:]) 130 | if err != nil { 131 | return nil, err 132 | } 133 | if n == 0 { 134 | if len(ret) == 0 { 135 | return nil, io.EOF 136 | } 137 | break 138 | } 139 | if buf[n-1] == '\n' { 140 | n-- 141 | } 142 | ret = append(ret, buf[:n]...) 143 | if n < len(buf) { 144 | break 145 | } 146 | } 147 | 148 | return ret, nil 149 | } 150 | -------------------------------------------------------------------------------- /util_bsd.go: -------------------------------------------------------------------------------- 1 | // Copyright 2013 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // +build darwin dragonfly freebsd netbsd openbsd 6 | 7 | package terminal 8 | 9 | import "syscall" 10 | 11 | const ioctlReadTermios = syscall.TIOCGETA 12 | const ioctlWriteTermios = syscall.TIOCSETA 13 | -------------------------------------------------------------------------------- /util_linux.go: -------------------------------------------------------------------------------- 1 | // Copyright 2013 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package terminal 6 | 7 | // These constants are declared here, rather than importing 8 | // them from the syscall package as some syscall packages, even 9 | // on linux, for example gccgo, do not declare them. 10 | const ioctlReadTermios = 0x5401 // syscall.TCGETS 11 | const ioctlWriteTermios = 0x5402 // syscall.TCSETS 12 | -------------------------------------------------------------------------------- /util_plan9.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package terminal 6 | 7 | import ( 8 | "fmt" 9 | "runtime" 10 | ) 11 | 12 | type State struct{} 13 | 14 | // IsTerminal returns true if the given file descriptor is a terminal. 15 | func IsTerminal(fd int) bool { 16 | return false 17 | } 18 | 19 | // MakeRaw put the terminal connected to the given file descriptor into raw 20 | // mode and returns the previous state of the terminal so that it can be 21 | // restored. 22 | func MakeRaw(fd int) (*State, error) { 23 | return nil, fmt.Errorf("terminal: MakeRaw not implemented on %s/%s", runtime.GOOS, runtime.GOARCH) 24 | } 25 | 26 | // GetState returns the current state of a terminal which may be useful to 27 | // restore the terminal after a signal. 28 | func GetState(fd int) (*State, error) { 29 | return nil, fmt.Errorf("terminal: GetState not implemented on %s/%s", runtime.GOOS, runtime.GOARCH) 30 | } 31 | 32 | // Restore restores the terminal connected to the given file descriptor to a 33 | // previous state. 34 | func Restore(fd int, state *State) error { 35 | return fmt.Errorf("terminal: Restore not implemented on %s/%s", runtime.GOOS, runtime.GOARCH) 36 | } 37 | 38 | // GetSize returns the dimensions of the given terminal. 39 | func GetSize(fd int) (width, height int, err error) { 40 | return 0, 0, fmt.Errorf("terminal: GetSize not implemented on %s/%s", runtime.GOOS, runtime.GOARCH) 41 | } 42 | 43 | // ReadPassword reads a line of input from a terminal without local echo. This 44 | // is commonly used for inputting passwords and other sensitive data. The slice 45 | // returned does not include the \n. 46 | func ReadPassword(fd int) ([]byte, error) { 47 | return nil, fmt.Errorf("terminal: ReadPassword not implemented on %s/%s", runtime.GOOS, runtime.GOARCH) 48 | } 49 | -------------------------------------------------------------------------------- /util_solaris.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // +build solaris 6 | 7 | package terminal 8 | 9 | import ( 10 | "golang.org/x/sys/unix" 11 | "io" 12 | "syscall" 13 | ) 14 | 15 | // State contains the state of a terminal. 16 | type State struct { 17 | termios syscall.Termios 18 | } 19 | 20 | // IsTerminal returns true if the given file descriptor is a terminal. 21 | func IsTerminal(fd int) bool { 22 | // see: http://src.illumos.org/source/xref/illumos-gate/usr/src/lib/libbc/libc/gen/common/isatty.c 23 | var termio unix.Termio 24 | err := unix.IoctlSetTermio(fd, unix.TCGETA, &termio) 25 | return err == nil 26 | } 27 | 28 | // ReadPassword reads a line of input from a terminal without local echo. This 29 | // is commonly used for inputting passwords and other sensitive data. The slice 30 | // returned does not include the \n. 31 | func ReadPassword(fd int) ([]byte, error) { 32 | // see also: http://src.illumos.org/source/xref/illumos-gate/usr/src/lib/libast/common/uwin/getpass.c 33 | val, err := unix.IoctlGetTermios(fd, unix.TCGETS) 34 | if err != nil { 35 | return nil, err 36 | } 37 | oldState := *val 38 | 39 | newState := oldState 40 | newState.Lflag &^= syscall.ECHO 41 | newState.Lflag |= syscall.ICANON | syscall.ISIG 42 | newState.Iflag |= syscall.ICRNL 43 | err = unix.IoctlSetTermios(fd, unix.TCSETS, &newState) 44 | if err != nil { 45 | return nil, err 46 | } 47 | 48 | defer unix.IoctlSetTermios(fd, unix.TCSETS, &oldState) 49 | 50 | var buf [16]byte 51 | var ret []byte 52 | for { 53 | n, err := syscall.Read(fd, buf[:]) 54 | if err != nil { 55 | return nil, err 56 | } 57 | if n == 0 { 58 | if len(ret) == 0 { 59 | return nil, io.EOF 60 | } 61 | break 62 | } 63 | if buf[n-1] == '\n' { 64 | n-- 65 | } 66 | ret = append(ret, buf[:n]...) 67 | if n < len(buf) { 68 | break 69 | } 70 | } 71 | 72 | return ret, nil 73 | } 74 | -------------------------------------------------------------------------------- /util_windows.go: -------------------------------------------------------------------------------- 1 | // Copyright 2011 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // +build windows 6 | 7 | package terminal 8 | 9 | import ( 10 | "io" 11 | "syscall" 12 | "unsafe" 13 | ) 14 | 15 | const ( 16 | enableLineInput = 2 17 | enableEchoInput = 4 18 | enableProcessedInput = 1 19 | enableWindowInput = 8 20 | enableMouseInput = 16 21 | enableInsertMode = 32 22 | enableQuickEditMode = 64 23 | enableExtendedFlags = 128 24 | enableAutoPosition = 256 25 | enableProcessedOutput = 1 26 | enableWrapAtEolOutput = 2 27 | ) 28 | 29 | var kernel32 = syscall.NewLazyDLL("kernel32.dll") 30 | 31 | var ( 32 | procGetConsoleMode = kernel32.NewProc("GetConsoleMode") 33 | procSetConsoleMode = kernel32.NewProc("SetConsoleMode") 34 | procGetConsoleScreenBufferInfo = kernel32.NewProc("GetConsoleScreenBufferInfo") 35 | ) 36 | 37 | type ( 38 | short int16 39 | word uint16 40 | 41 | coord struct { 42 | x short 43 | y short 44 | } 45 | smallRect struct { 46 | left short 47 | top short 48 | right short 49 | bottom short 50 | } 51 | consoleScreenBufferInfo struct { 52 | size coord 53 | cursorPosition coord 54 | attributes word 55 | window smallRect 56 | maximumWindowSize coord 57 | } 58 | ) 59 | 60 | type State struct { 61 | mode uint32 62 | } 63 | 64 | // VisualLength returns the number of visible glyphs in a string. This can be 65 | // useful for getting the length of a string which has ANSI color sequences, 66 | // but it doesn't count "wide" glyphs differently than others, and it won't 67 | // handle ANSI cursor commands; e.g., it ignores "\x1b[D" rather than knowing 68 | // that the cursor position moved to the left. 69 | func VisualLength(s string) int { 70 | runes := []rune(s) 71 | inEscapeSeq := false 72 | length := 0 73 | 74 | for _, r := range runes { 75 | switch { 76 | case inEscapeSeq: 77 | if (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') { 78 | inEscapeSeq = false 79 | } 80 | case r == '\x1b': 81 | inEscapeSeq = true 82 | default: 83 | length++ 84 | } 85 | } 86 | 87 | return length 88 | } 89 | 90 | // IsTerminal returns true if the given file descriptor is a terminal. 91 | func IsTerminal(fd int) bool { 92 | var st uint32 93 | r, _, e := syscall.Syscall(procGetConsoleMode.Addr(), 2, uintptr(fd), uintptr(unsafe.Pointer(&st)), 0) 94 | return r != 0 && e == 0 95 | } 96 | 97 | // MakeRaw put the terminal connected to the given file descriptor into raw 98 | // mode and returns the previous state of the terminal so that it can be 99 | // restored. 100 | func MakeRaw(fd int) (*State, error) { 101 | var st uint32 102 | _, _, e := syscall.Syscall(procGetConsoleMode.Addr(), 2, uintptr(fd), uintptr(unsafe.Pointer(&st)), 0) 103 | if e != 0 { 104 | return nil, error(e) 105 | } 106 | raw := st &^ (enableEchoInput | enableProcessedInput | enableLineInput | enableProcessedOutput) 107 | _, _, e = syscall.Syscall(procSetConsoleMode.Addr(), 2, uintptr(fd), uintptr(raw), 0) 108 | if e != 0 { 109 | return nil, error(e) 110 | } 111 | return &State{st}, nil 112 | } 113 | 114 | // GetState returns the current state of a terminal which may be useful to 115 | // restore the terminal after a signal. 116 | func GetState(fd int) (*State, error) { 117 | var st uint32 118 | _, _, e := syscall.Syscall(procGetConsoleMode.Addr(), 2, uintptr(fd), uintptr(unsafe.Pointer(&st)), 0) 119 | if e != 0 { 120 | return nil, error(e) 121 | } 122 | return &State{st}, nil 123 | } 124 | 125 | // Restore restores the terminal connected to the given file descriptor to a 126 | // previous state. 127 | func Restore(fd int, state *State) error { 128 | _, _, err := syscall.Syscall(procSetConsoleMode.Addr(), 2, uintptr(fd), uintptr(state.mode), 0) 129 | return err 130 | } 131 | 132 | // GetSize returns the dimensions of the given terminal. 133 | func GetSize(fd int) (width, height int, err error) { 134 | var info consoleScreenBufferInfo 135 | _, _, e := syscall.Syscall(procGetConsoleScreenBufferInfo.Addr(), 2, uintptr(fd), uintptr(unsafe.Pointer(&info)), 0) 136 | if e != 0 { 137 | return 0, 0, error(e) 138 | } 139 | return int(info.size.x), int(info.size.y), nil 140 | } 141 | 142 | // ReadPassword reads a line of input from a terminal without local echo. This 143 | // is commonly used for inputting passwords and other sensitive data. The slice 144 | // returned does not include the \n. 145 | func ReadPassword(fd int) ([]byte, error) { 146 | var st uint32 147 | _, _, e := syscall.Syscall(procGetConsoleMode.Addr(), 2, uintptr(fd), uintptr(unsafe.Pointer(&st)), 0) 148 | if e != 0 { 149 | return nil, error(e) 150 | } 151 | old := st 152 | 153 | st &^= (enableEchoInput) 154 | st |= (enableProcessedInput | enableLineInput | enableProcessedOutput) 155 | _, _, e = syscall.Syscall(procSetConsoleMode.Addr(), 2, uintptr(fd), uintptr(st), 0) 156 | if e != 0 { 157 | return nil, error(e) 158 | } 159 | 160 | defer func() { 161 | syscall.Syscall(procSetConsoleMode.Addr(), 2, uintptr(fd), uintptr(old), 0) 162 | }() 163 | 164 | var buf [16]byte 165 | var ret []byte 166 | for { 167 | n, err := syscall.Read(syscall.Handle(fd), buf[:]) 168 | if err != nil { 169 | return nil, err 170 | } 171 | if n == 0 { 172 | if len(ret) == 0 { 173 | return nil, io.EOF 174 | } 175 | break 176 | } 177 | if buf[n-1] == '\n' { 178 | n-- 179 | } 180 | if n > 0 && buf[n-1] == '\r' { 181 | n-- 182 | } 183 | ret = append(ret, buf[:n]...) 184 | if n < len(buf) { 185 | break 186 | } 187 | } 188 | 189 | return ret, nil 190 | } 191 | --------------------------------------------------------------------------------