Documentation ¶
Overview ¶
Package terminal provides support functions for dealing with terminals, as commonly found on UNIX systems.
This is a completely standalone key / line reader. All types that read data allow anything conforming to io.Reader. All types that write data allow anything conforming to io.Writer.
Putting a terminal into raw mode is the most common requirement, and can be seen in the example.
Example ¶
Example of a very basic use of the Prompt type, which is probably the simplest type available
package main import ( "fmt" "os" "strings" "github.com/Nerdmaster/terminal" ) func main() { // Put terminal in raw mode; this is almost always going to be required for // local terminals, but not necessary when connecting to an ssh terminal oldState, err := terminal.MakeRaw(int(os.Stdin.Fd())) if err != nil { panic(err) } defer terminal.Restore(0, oldState) // This is a simple example, so we just use Stdin and Stdout; but any reader // and writer will work. Note that the prompt can contain ANSI colors. var p = terminal.NewPrompt(os.Stdin, os.Stdout, "\x1b[34;1mCommand\x1b[0m: ") // This is a simple key interceptor; on CTRL-r we forcibly change the scroll to 20 p.OnKeypress = func(e *terminal.KeyEvent) { if e.Key == terminal.KeyCtrlR { p.Scroller.ScrollOffset = 20 } } // Make the input scroll at 40 characters, maxing out at a 120-char string p.Scroller.InputWidth = 40 p.Scroller.MaxLineLength = 120 // Loop forever until we get an error (typically EOF from user pressing // CTRL+D) or the "quit" command is entered. We echo each command unless the // user turns echoing off. var echo = true for { // Just for fun we can demonstrate that the cursor never touches anything // outside out 40-character input. First, we print padding for the prompt // ("Command: "). Then 40 spaces, a bar, and \r to return the cursor to // the beginning of the line. fmt.Printf(" ") fmt.Printf(strings.Repeat(" ", 40)) fmt.Printf("|\r") var cmd, err = p.ReadLine() if err != nil { fmt.Printf("%s\r\n", err) break } if strings.ToLower(cmd) == "quit" { fmt.Print("Quitter!\r\n") break } if strings.ToLower(cmd) == "echo on" { echo = true } if strings.ToLower(cmd) == "echo off" { echo = false } if echo { fmt.Printf("%#v\r\n", cmd) } } }
Output:
Index ¶
- Constants
- Variables
- func GetSize(fd int) (width, height int, err error)
- func IsTerminal(fd int) bool
- func ReadPassword(fd int) ([]byte, error)
- func Restore(fd int, state *State) error
- func VisualLength(s string) int
- type AbsPrompt
- func (p *AbsPrompt) NeedWrite() bool
- func (p *AbsPrompt) PrintCursorMovement()
- func (p *AbsPrompt) PrintLine()
- func (p *AbsPrompt) PrintPrompt()
- func (p *AbsPrompt) ReadLine() (string, error)
- func (p *AbsPrompt) SetLocation(x, y int)
- func (p *AbsPrompt) SetPrompt(s string)
- func (p *AbsPrompt) WriteAll()
- func (p *AbsPrompt) WriteChanges()
- func (p *AbsPrompt) WriteChangesNoCursor()
- type DT
- type KeyEvent
- type KeyModifier
- type KeyReader
- type Keypress
- type Line
- func (l *Line) AddKeyToLine(key rune)
- func (l *Line) Clear()
- func (l *Line) CountToLeftWord() int
- func (l *Line) CountToRightWord() int
- func (l *Line) DeleteLine()
- func (l *Line) DeleteRuneUnderCursor()
- func (l *Line) DeleteToBeginningOfLine()
- func (l *Line) EraseNPreviousChars(n int)
- func (l *Line) MoveEnd()
- func (l *Line) MoveHome()
- func (l *Line) MoveLeft()
- func (l *Line) MoveRight()
- func (l *Line) MoveToLeftWord()
- func (l *Line) MoveToRightWord()
- func (l *Line) Set(t []rune, p int)
- func (l *Line) Split() (string, string)
- func (l *Line) String() string
- type Prompt
- type Reader
- type Scroller
- type State
Examples ¶
Constants ¶
const ( KeyCtrlA = 1 + iota KeyCtrlB KeyCtrlC KeyCtrlD KeyCtrlE KeyCtrlF KeyCtrlG KeyCtrlH KeyCtrlI KeyCtrlJ KeyCtrlK KeyCtrlL KeyCtrlM KeyCtrlN KeyCtrlO KeyCtrlP KeyCtrlQ KeyCtrlR KeyCtrlS KeyCtrlT KeyCtrlU KeyCtrlV KeyCtrlW KeyCtrlX KeyCtrlY KeyCtrlZ KeyEscape KeyLeftBracket = '[' KeyRightBracket = ']' KeyEnter = '\r' KeyBackspace = 127 KeyUnknown = 0xd800 + iota KeyUp KeyDown KeyLeft KeyRight KeyHome KeyEnd KeyPasteStart KeyPasteEnd KeyInsert KeyDelete KeyPgUp KeyPgDn KeyPause KeyF1 KeyF2 KeyF3 KeyF4 KeyF5 KeyF6 KeyF7 KeyF8 KeyF9 KeyF10 KeyF11 KeyF12 )
Giant list of key constants. Everything above KeyUnknown matches an actual ASCII key value. After that, we have various pseudo-keys in order to represent complex byte sequences that correspond to keys like Page up, Right arrow, etc.
const ( ModNone KeyModifier = 0 ModAlt = 1 ModMeta = 2 )
KeyModifier values. We don't include Shift in here because terminals don't include shift for a great deal of keys that can exist; e.g., there is no "SHIFT + PgUp". Similarly, CTRL doesn't make sense as a modifier in terminals. CTRL+A is just ASCII character 1, whereas there is no CTRL+1, and CTRL+Up is its own totally separate sequence from Up. So CTRL keys are just defined on an as-needed basis.
const DefaultMaxLineLength = 4096
DefaultMaxLineLength is the default MaxLineLength for a Reader; once a line reaches this length, the Reader no longer accepts input which would increase the line
const ScrollBy = 10
ScrollBy is the default value a scroller scrolls by when the cursor would otherwise be outside the input area
Variables ¶
var CRLF = []byte("\r\n")
CRLF is the byte sequence all terminals use for moving to the beginning of the next line
var ErrPasteIndicator = pasteIndicatorError{}
ErrPasteIndicator may be returned from ReadLine as the error, in addition to valid line data. It indicates that bracketed paste mode is enabled and that the returned line consists only of pasted data. Programs may wish to interpret pasted data more literally than typed data.
Functions ¶
func IsTerminal ¶
IsTerminal returns true if the given file descriptor is a terminal.
func ReadPassword ¶
ReadPassword reads a line of input from a terminal without local echo. This is commonly used for inputting passwords and other sensitive data. The slice returned does not include the \n.
func Restore ¶
Restore restores the terminal connected to the given file descriptor to a previous state.
func VisualLength ¶ added in v0.10.0
VisualLength returns the number of visible glyphs in a string. This can be useful for getting the length of a string which has ANSI color sequences, but it doesn't count "wide" glyphs differently than others, and it won't handle ANSI cursor commands; e.g., it ignores "\x1b[D" rather than knowing that the cursor position moved to the left.
Types ¶
type AbsPrompt ¶ added in v0.10.0
AbsPrompt is a wrapper around a Reader which will write a prompt, wait for a user's input, and return it. It will print whatever needs to be printed on demand to an io.Writer, using ANSI to ensure the cursor is always at the right screen location, allowing the AbsPrompt to be used concurrently with other screen writing. AbsPrompt stores the Reader's prior state in order to avoid unnecessary writes.
func NewAbsPrompt ¶ added in v0.10.0
NewAbsPrompt returns an AbsPrompt which will read lines from r, write its prompt and current line to w, and use p as the prompt string.
func (*AbsPrompt) NeedWrite ¶ added in v0.10.0
NeedWrite returns true if there are any pending changes to the line or cursor position
func (*AbsPrompt) PrintCursorMovement ¶ added in v0.10.0
func (p *AbsPrompt) PrintCursorMovement()
PrintCursorMovement sends the ANSI escape sequence for moving the cursor
func (*AbsPrompt) PrintLine ¶ added in v0.10.0
func (p *AbsPrompt) PrintLine()
PrintLine gets the current line and prints it to the screen just after the prompt location
func (*AbsPrompt) PrintPrompt ¶ added in v0.10.0
func (p *AbsPrompt) PrintPrompt()
PrintPrompt moves to the x/y coordinates of the prompt and prints the prompt string
func (*AbsPrompt) SetLocation ¶ added in v0.10.0
SetLocation changes the internal x and y coordinates. If this is called while a ReadLine is in progress, you won't be happy.
func (*AbsPrompt) SetPrompt ¶ added in v0.10.0
SetPrompt changes the current prompt. This shouldn't be called while a ReadLine is in progress.
func (*AbsPrompt) WriteAll ¶ added in v0.10.0
func (p *AbsPrompt) WriteAll()
WriteAll forces a write of the entire prompt
func (*AbsPrompt) WriteChanges ¶ added in v0.10.0
func (p *AbsPrompt) WriteChanges()
WriteChanges attempts to only write to the console when something has changed (line text or the cursor position). It will also print the prompt if that hasn't yet been printed.
func (*AbsPrompt) WriteChangesNoCursor ¶ added in v0.10.0
func (p *AbsPrompt) WriteChangesNoCursor()
WriteChangesNoCursor prints prompt and line if necessary, but doesn't reposition the cursor in order to allow a frequently-updating app to write the cursor change where it makes sense, regardless of changes to the user's input.
type DT ¶
type DT struct { sync.RWMutex // Echo is on by default; set it to false for things like password prompts Echo bool // contains filtered or unexported fields }
DT contains the state for running a *very* basic terminal which operates effectively like an old-school telnet connection: no ANSI, no special keys, no history preservation, etc. This isn't actually useful in any way I can see, but it's a decent example of lower-level key reading.
func Dumb ¶
Dumb runs a dumb terminal reader on the given io.Reader. If the terminal is local, it must first have been put into raw mode.
type KeyEvent ¶
KeyEvent is used for OnKeypress handlers to get the key and modify handler state when the custom handler needs default handlers to be bypassed
type KeyModifier ¶
type KeyModifier int
KeyModifier tells us what modifiers were pressed at the same time as a normal key, such as CTRL, Alt, Meta, etc.
func ParseKey ¶
func ParseKey(b []byte, force bool) (r rune, rl int, mod KeyModifier)
ParseKey tries to parse a key sequence from b. If successful, it returns the key, the length in bytes of that key, and the modifier, if any. Otherwise it returns utf8.RuneError, 0, and an undefined mod which should be ignored.
ParseKey is the function on which all terminal's types rely, and offers the lowest-level parsing in this package. Even though it's the base function used by all types, the way it handles sequences is complex enough that it is generally best to use one of the key/line parsers rather than calling this directly.
When used by the various types, "force" defaults to being false. This means that we assume users are not typically typing key sequence prefixes like the Escape key or Alt-left-bracket. This eases parsing in most typical cases, but it means when a user *does* press one of these keys, the Parser treats it as if nothing was pressed at all (returning utf8.RuneError and a length of 0). It's then up to the caller to decide to either drop the bytes or append to them with more data.
When one of the readers (KeyReader, Reader, Prompter) gets this kind of "empty" response, it will hold onto the bytes and try to append to them next time it reads. It is basically assuming the sequence is incomplete. If the next read doesn't happen soon enough, the reader will decide the sequence was in fact complete, and then return the raw Escape or Alt+[, but this doesn't happen until after the subsequent read, which means effectively a one-key "lag".
This means applications really can't rely on getting raw Escape keys or alt-left-bracket. And in some cases, this is not acceptable. Hence, the "force" flag.
If "force" is true, ParseKey will return immediately, even if the sequence is nonsensical. This makes it a lot easier to get very special keys, but when listening on a network there is probably a small chance a key sequence could be broken up over multiple reads, and the result will be essentially "corrupt" keys. As an example, if the left-arrow is sent, it's normally three bytes: Escape followed by left-bracket followed by uppercase "D". If the Escape is read separately from the rest of the sequence, the overall result will be an Escape key followed by a left-bracket followed by a "D". If the Escape and left-bracket are read separately from the "D", the result will be Alt-left-bracket followed by "D". The weird variations can get worse with longer key sequences.
Additionally, if user input is serialized to a file or something, just as a raw stream of bytes, the read operation won't read more than 256 at a time. This will undoubtedly lead to all kinds of broken "keys" being parsed.
The tl;dr is that terminals kind of suck at complex key parsing, so make sure you go into it with your eyes wide open.
func (KeyModifier) String ¶
func (m KeyModifier) String() string
type KeyReader ¶
type KeyReader struct { // If ForceParse is true, the reader won't wait for certain sequences to // finish, which allows for things like ESC or Alt-left-bracket to be // detected properly ForceParse bool // contains filtered or unexported fields }
KeyReader is the low-level type for reading raw keypresses from a given io stream, usually stdin or an ssh socket. Stores raw bytes in a buffer so that if many keys are read at once, they can still be parsed individually.
func NewKeyReader ¶
NewKeyReader returns a simple KeyReader set to read from r
func (*KeyReader) ReadKeypress ¶
ReadKeypress reads the next key sequence, returning a Keypress object and possibly an error if the input stream can't be read for some reason. This will block if the buffer has no more data, which would obviously require a direct Read call on the underlying io.Reader.
type Keypress ¶
type Keypress struct { Key rune Modifier KeyModifier Size int Raw []byte }
Keypress contains the data which made up a key: our internal KeyXXX constant and the bytes which were parsed to get said constant. If the raw bytes need to be held for any reason, they should be copied, not stored as-is, since what's in here is a simple slice into the raw buffer.
type Line ¶ added in v0.12.0
Line manages a very encapsulated version of a terminal line's state
func (*Line) AddKeyToLine ¶ added in v0.12.0
AddKeyToLine inserts the given key at the current position in the current line.
func (*Line) CountToLeftWord ¶ added in v0.12.0
CountToLeftWord returns then number of characters from the cursor to the start of the previous word
func (*Line) CountToRightWord ¶ added in v0.12.0
CountToRightWord returns then number of characters from the cursor to the start of the next word
func (*Line) DeleteLine ¶ added in v0.12.0
func (l *Line) DeleteLine()
DeleteLine removes all runes after the cursor position
func (*Line) DeleteRuneUnderCursor ¶ added in v0.12.0
func (l *Line) DeleteRuneUnderCursor()
DeleteRuneUnderCursor erases the character under the current position
func (*Line) DeleteToBeginningOfLine ¶ added in v0.12.0
func (l *Line) DeleteToBeginningOfLine()
DeleteToBeginningOfLine removes everything behind the cursor
func (*Line) EraseNPreviousChars ¶ added in v0.12.0
EraseNPreviousChars deletes n characters from l.Text and updates l.Pos
func (*Line) MoveEnd ¶ added in v0.12.0
func (l *Line) MoveEnd()
MoveEnd puts the cursor at the end of the line
func (*Line) MoveHome ¶ added in v0.12.0
func (l *Line) MoveHome()
MoveHome moves the cursor to the beginning of the line
func (*Line) MoveRight ¶ added in v0.12.0
func (l *Line) MoveRight()
MoveRight moves pos one rune right
func (*Line) MoveToLeftWord ¶ added in v0.12.0
func (l *Line) MoveToLeftWord()
MoveToLeftWord moves pos to the first rune of the word to the left
func (*Line) MoveToRightWord ¶ added in v0.12.0
func (l *Line) MoveToRightWord()
MoveToRightWord moves pos to the first rune of the word to the right
type Prompt ¶ added in v0.10.0
type Prompt struct { *Reader Out io.Writer // AfterKeypress shadows the Reader variable of the same name to allow custom // keypress listeners even though Prompt has to listen in order to write output AfterKeypress func(event *KeyEvent) // Scroller processes the pending output to figure out if scrolling is // necessary and what should be printed if so Scroller *Scroller // contains filtered or unexported fields }
A Prompt is a wrapper around a Reader which will write a prompt, wait for a user's input, and return it. It will print whatever needs to be printed on demand to an io.Writer. The Prompt stores the Reader's prior state in order to avoid unnecessary writes.
func NewPrompt ¶ added in v0.10.0
NewPrompt returns a prompt which will read lines from r, write its prompt and current line to w, and use p as the prompt string.
type Reader ¶
type Reader struct { // OnKeypress, if non-null, is called for each keypress with the key and // input line sent in OnKeypress func(event *KeyEvent) // AfterKeypress, if non-nil, is called after each keypress has been // processed. event should be considered read-only, as any changes will be // ignored since the key has already been processed. AfterKeypress func(event *KeyEvent) // NoHistory is on when we don't want to preserve history, such as when a // password is being entered NoHistory bool // MaxLineLength tells us when to stop accepting input (other than things // like allowing up/down/left/right and other control keys) MaxLineLength int // contains filtered or unexported fields }
Reader contains the state for running a VT100 terminal that is capable of reading lines of input. It is similar to the golang crypto/ssh/terminal package except that it doesn't write, leaving that to the caller. The idea is to store what the user is typing, and where the cursor should be, while letting something else decide what to draw and where on the screen to draw it. This separation enables more complex applications where there's other real-time data being rendered at the same time as the input line.
func NewReader ¶
NewReader runs a terminal reader on the given io.Reader. If the Reader is a local terminal, that terminal must first have been put into raw mode.
func (*Reader) ReadPassword ¶
ReadPassword temporarily reads a password without saving to history
type Scroller ¶ added in v0.12.0
type Scroller struct { // InputWidth should be set to the terminal width or smaller. If this is // equal to or larger than MaxWidth, no scrolling will occur InputWidth int // MaxLineLength should be set to the maximum number of runes to allow in the // scrolling input. This should be set to the underlying Reader's // MaxLineLength or less, otherwise the Reader will block further input. MaxLineLength int // ScrollOffset is set to the number of characters which are "off-screen" to // the left of the input area; the input line displays just the characters // which are after this offset. This should typically not be adjusted // manually, but it may make sense to allow scrolling the input via a // keyboard shortcut that doesn't alter the line or cursor position. ScrollOffset int // LeftOverflow and RightOverflow are used to signify that the input is // scrolling left or right. They both default to the UTF ellipsis character, // but can be overridden as needed. If set to ”, no overflow character will // be displayed when scrolling LeftOverflow, RightOverflow rune // ScrollBy is the number of runes we "shift" when the cursor would otherwise // leave the printable area; defaults to the ScrollBy package constant ScrollBy int // contains filtered or unexported fields }
A Scroller is a Line filter for taking the internal Line's state and giving an output widget what should be drawn to the screen
func NewScroller ¶ added in v0.12.0
func NewScroller() *Scroller
NewScroller creates a simple scroller instance with no limits. MaxLineLength and InputWidth must be set before any of the scrolling logic will kick in. Whatever uses this is responsible for setting InputWidth and MaxLineLength to appropriate values.