zzterm

package module
v0.4.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Apr 23, 2020 License: BSD-3-Clause Imports: 8 Imported by: 1

README

zzterm builds.sr.ht status GoDoc go.dev reference

Package zzterm efficiently reads and decodes terminal input keys and mouse events without any memory allocation. It is intended to be used with a terminal set in raw mode as its io.Reader. See the package documentation for details, API reference and usage example (alternatively, on pkg.go.dev).

You can also check out zztermtest for more usage examples and how to efficiently print output to an io.Writer (with a zero-allocation "echo" program example).

Note that at the moment, zzterm is only tested on macOS and linux. Mouse support is through the Xterm X11 mouse protocol with SGR enabled, the terminal has to support that mode for mouse (and focus) key events to be emitted.

See Also

Similar Go packages:

Benchmarks

The input processing is typically in the hot path of a terminal application. Zzterm is quite fast and does not allocate - not when decoding standard keys, not when decoding escape sequences, neither when decoding mouse events.

benchmark                                    iter     time/iter   bytes alloc        allocs
---------                                    ----     ---------   -----------        ------
BenchmarkInput_ReadKey/a-4               61804756   18.40 ns/op        0 B/op   0 allocs/op
BenchmarkInput_ReadKey/B-4               66716232   17.90 ns/op        0 B/op   0 allocs/op
BenchmarkInput_ReadKey/1-4               62950432   18.60 ns/op        0 B/op   0 allocs/op
BenchmarkInput_ReadKey/\x00-4            65492827   18.20 ns/op        0 B/op   0 allocs/op
BenchmarkInput_ReadKey/ø-4               60368734   19.90 ns/op        0 B/op   0 allocs/op
BenchmarkInput_ReadKey/👪-4              57783043   20.60 ns/op        0 B/op   0 allocs/op
BenchmarkInput_ReadKey/平-4              57067489   20.80 ns/op        0 B/op   0 allocs/op
BenchmarkInput_ReadKey/\x1b[B-4          26063134   45.90 ns/op        0 B/op   0 allocs/op
BenchmarkInput_ReadKey/\x1b[1;2C-4       26355364   45.40 ns/op        0 B/op   0 allocs/op
BenchmarkInput_ReadKey/\x1b[I-4          26530273   44.40 ns/op        0 B/op   0 allocs/op
BenchmarkInput_ReadKey/\x1b[<35;1;2M-4   21740397   55.30 ns/op        0 B/op   0 allocs/op
BenchmarkInput_ReadKey_Bytes-4           49141444   24.40 ns/op        0 B/op   0 allocs/op
BenchmarkInput_ReadKey_Mouse-4           19961526   60.70 ns/op        0 B/op   0 allocs/op

License

The BSD 3-Clause license.

Documentation

Overview

Package zzterm efficiently reads and decodes terminal input keys and mouse events without any memory allocation. It is intended to be used with a terminal set in raw mode as its io.Reader. Setting the terminal in raw mode is not handled by this package, there are a number of Go packages that can do this (see the example).

Basic usage

Set the terminal in raw mode, use NewInput to create the input key reader and read from the terminal:

func main() {
    // set the terminal in raw mode, e.g. with github.com/pkg/term
    t, err := term.Open("/dev/tty", term.RawMode)
    if err != nil {
        log.Panic(err)
    }
    defer t.Restore()

    input := zzterm.NewInput()
    for {
    	  k, err := input.ReadKey(t)
    	  if err != nil {
            log.Panic(err)
    	  }

    	  switch k.Type() {
    	  case zzterm.KeyRune:
            // k.Rune() returns the rune
    	  case zzterm.KeyESC, zzterm.KeyCtrlC:
            // quit on ESC or Ctrl-C
            return
        }
    }
}

Mouse and focus events

Mouse events are supported through the Xterm X11 mouse protocol in SGR mode, which is a complex way to call the "modern" handling of mouse events [1] (beyond the limits of 223 for mouse position coordinates in the old protocol). This should be widely supported by modern terminals, but the tracking of mouse events must be enabled on the terminal so that the escape sequences get sent to zzterm. It is the responsibility of the caller to enable this (with SGR mode) before using Input.ReadKey, but as a convenience zzterm provides the EnableMouse and DisableMouse functions:

t, err := term.Open("/dev/tty", term.RawMode)
// ...
defer t.Restore()

// Mouse events can be enabled only to report button presses (zzterm.MouseButton)
// or any mouse event (including mouse moves, zzterm.MouseAny).
zzterm.EnableMouse(t, zzterm.MouseAny)
defer zzterm.DisableMouse(t, zzterm.MouseAny)

And then mouse events will be reported (if supported by the terminal):

// The WithMouse option must be set to decode the mouse events, otherwise
// they would be reported as uninterpreted KeyESCSeq (escape sequence).
input := zzterm.NewInput(zzterm.WithMouse())
for {
    // ...
    switch k.Type() {
    case zzterm.KeyRune:
        // k.Rune() returns the rune
    case zzterm.KeyMouse:
        // k.Mod() returns the modifier flags (e.g. Shift) pressed during the event
        // input.Mouse() returns the mouse information, coordinates 1,1 is top-left
    // ...
    }
}

It works similarly to enable reporting focus in/out of the terminal:

zzterm.EnableFocus(t)
defer zzterm.DisableFocus(t)

// The WithFocus option must be set to decode the focus events, otherwise
// they would be reported as uninterpreted KeyESCSeq (escape sequence).
input := zzterm.NewInput(zzterm.WithMouse(), zzterm.WithFocus())
for {
    // ...
    switch k.Type() {
    // ...
    case zzterm.KeyFocusIn, zzterm.KeyFocusOut:
        // terminal has gained/lost focus
    // ...
    }
}

Terminfo

Different terminals sometimes understand different escape sequences to interpret special keys such as function keys (F1, F2, etc.) and arrows. That configuration is part of the terminfo database (at least on Unix-like systems). While zzterm does not read the terminfo database itself, it supports specifying a map of values where the key is the name of the special key and the value is the escape sequence that should map to this key.

escSeq := map[string]string{"KeyDown": "\x1b[B"}
input := zzterm.NewInput(zzterm.WithESCSeq(escSeq))

The github.com/gdamore/tcell repository has a good number of terminal configurations described in a Go struct and accessible via terminfo.LookupTermInfo [2]. To enable reuse of this, zzterm provides the FromTerminfo function to convert from those structs to the supported map format. It is the responsibility of the caller to detect the right terminfo to use for the terminal.

ti, err := terminfo.LookupTerminfo("termite")
// handle error
input := zzterm.NewInput(zzterm.WithESCSeq(zzterm.FromTerminfo(ti)))

Note, however, that the tcell package patches those terminfo descriptions before use due to some inconsistencies in behaviour - using the raw terminfo definitions may not always work as expected [3].

When no WithESCSeq option is provided (or if a nil map is passed), then a default mapping is used. If a non-nil but empty map is provided, then any escape sequence translation will be disabled (except for mouse and focus events if enabled), and all such sequences will be read as keys of type KeyESCSeq. The input.Bytes method can then be called to inspect the raw bytes of the sequence.

[1]: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h2-Mouse-Tracking
[2]: https://godoc.org/github.com/gdamore/tcell/terminfo#LookupTerminfo
[3]: https://github.com/gdamore/tcell/blob/8ec73b6fa6c543d5d067722c0444b07f7607ba2f/tscreen.go#L337-L367

Index

Constants

View Source
const ErrTimeout = timeoutError("zzterm: timetout")

ErrTimeout is the error returned when ReadKey fails to return a key due to the read timeout expiring.

Variables

This section is empty.

Functions

func DisableFocus

func DisableFocus(w io.Writer) error

DisableFocus sends the Control Sequence Introducer (CSI) function to w to disable sending focus escape sequences.

func DisableMouse

func DisableMouse(w io.Writer, eventType MouseEventType) error

DisableMouse sends the Control Sequence Introducer (CSI) function to w to disable tracking of the specified mouse event type and to disable SGR mode.

func EnableFocus

func EnableFocus(w io.Writer) error

EnableFocus sends the Control Sequence Introducer (CSI) function to w to enable sending focus escape sequences.

func EnableMouse

func EnableMouse(w io.Writer, eventType MouseEventType) error

EnableMouse sends the Control Sequence Introducer (CSI) function to w to enable tracking of the specified mouse event type in SGR mode.

func FromTerminfo

func FromTerminfo(v interface{}) map[string]string

FromTerminfo returns a terminfo map that can be used in the call to NewInput. The value v should be a tcell/terminfo.Terminfo struct, a pointer to such a struct, or a value that marshals to JSON with an equivalent structure.

It first marshals v to JSON and then unmarshals it in a map. It makes no validation that v is a valid terminfo, and it returns nil if there is any error when converting to and from the intermediate JSON representations.

Types

type Input

type Input struct {
	// contains filtered or unexported fields
}

Input reads input keys from a reader and returns the key pressed.

func NewInput

func NewInput(opts ...Option) *Input

NewInput creates an Input ready to use. Call Input.ReadKey to read a single key from an io.Reader - typically a terminal file descriptor set in raw mode. The translation of escape sequences to special keys is controlled by the WithESCSeq option.

func (*Input) Bytes

func (i *Input) Bytes() []byte

Bytes returns the uninterpreted bytes from the last key read. The bytes are valid only until the next call to ReadKey and should not be modified.

func (*Input) Mouse

func (i *Input) Mouse() MouseEvent

Mouse returns the mouse event corresponding to the last key of type KeyMouse. It should be called only after a key of type KeyMouse has been received from ReadKey, and before any other call to ReadKey.

func (*Input) ReadKey

func (i *Input) ReadKey(r io.Reader) (Key, error)

ReadKey reads a key from r which should be the reader of a terminal set in raw mode. It is recommended to set a read timeout on the raw terminal so that a Read does not block indefinitely. In that case, if a call to ReadKey times out witout data for a key, it returns the zero-value of Key and ErrTimeout.

type Key

type Key uint32

Key represents a single key. It contains the key type, the key modifier flags and the rune itself in a compact form. Use the Rune, Type and Mod methods to get information on the key.

func (Key) Mod

func (k Key) Mod() Mod

Mod returns the key modifier flags set for this key.

func (Key) Rune

func (k Key) Rune() rune

Rune returns the rune corresponding to this key. It returns -1 if the KeyType is not KeyRune.

func (Key) String

func (k Key) String() string

String returns the string representation of k.

func (Key) Type

func (k Key) Type() KeyType

Type returns the KeyType for this key.

type KeyType

type KeyType byte

KeyType represents the type of key.

const (
	KeyNUL KeyType = iota
	KeySOH
	KeySTX
	KeyETX
	KeyEOT
	KeyENQ
	KeyACK
	KeyBEL
	KeyBS
	KeyTAB
	KeyLF
	KeyVT
	KeyFF
	KeyCR
	KeySO
	KeySI
	KeyDLE
	KeyDC1
	KeyDC2
	KeyDC3
	KeyDC4
	KeyNAK
	KeySYN
	KeyETB
	KeyCAN
	KeyEM
	KeySUB
	KeyESC
	KeyFS
	KeyGS
	KeyRS
	KeyUS
	KeyRune // covers ASCII 32-126 + any other unicode code point - from this point on the key type does not match ASCII values
	KeyLeft
	KeyRight
	KeyUp
	KeyDown
	KeyInsert
	KeyBacktab
	KeyDelete
	KeyHome
	KeyEnd
	KeyPgUp
	KeyPgDn
	KeyF1
	KeyF2
	KeyF3
	KeyF4
	KeyF5
	KeyF6
	KeyF7
	KeyF8
	KeyF9
	KeyF10
	KeyF11
	KeyF12
	KeyF13
	KeyF14
	KeyF15
	KeyF16
	KeyF17
	KeyF18
	KeyF19
	KeyF20
	KeyF21
	KeyF22
	KeyF23
	KeyF24
	KeyF25
	KeyF26
	KeyF27
	KeyF28
	KeyF29
	KeyF30
	KeyF31
	KeyF32
	KeyF33
	KeyF34
	KeyF35
	KeyF36
	KeyF37
	KeyF38
	KeyF39
	KeyF40
	KeyF41
	KeyF42
	KeyF43
	KeyF44
	KeyF45
	KeyF46
	KeyF47
	KeyF48
	KeyF49
	KeyF50
	KeyF51
	KeyF52
	KeyF53
	KeyF54
	KeyF55
	KeyF56
	KeyF57
	KeyF58
	KeyF59
	KeyF60
	KeyF61
	KeyF62
	KeyF63
	KeyF64
	KeyHelp
	KeyExit
	KeyClear
	KeyCancel
	KeyPrint
	KeyESCSeq
	KeyMouse
	KeyFocusIn
	KeyFocusOut // 116

	KeyDEL KeyType = 127
)

List of supported key types.

const (
	KeyCtrlSpace KeyType = iota
	KeyCtrlA
	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
	KeyCtrlLeftSq
	KeyCtrlBackslash
	KeyCtrlRightSq
	KeyCtrlCarat
	KeyCtrlUnderscore

	KeyBackspace = KeyBS
	KeyEscape    = KeyESC
	KeyEnter     = KeyCR
)

List of some aliases to the key types. The KeyCtrl... constants match the ASCII keys at the same position (e.g. KeyCtrlSpace is KeyNUL, KeyCtrlLeftSq is KeyESC, etc.).

func (KeyType) String

func (k KeyType) String() string

String returns the string representation of the key type.

type Mod

type Mod byte

Mod represents a key modifier such as pressing alt or ctrl. Detection of such flags is limited.

const (
	ModAlt   Mod // 2
	ModShift     // 4
	ModMeta      // 8
	ModCtrl      // 16
	ModNone  Mod = 0
)

List of modifier flags. Values of Shift, Meta and Ctrl are the same as for the xterm mouse tracking. See https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Normal-tracking-mode

func (Mod) String

func (m Mod) String() string

String returns the string representation of m.

type MouseEvent

type MouseEvent struct {
	// contains filtered or unexported fields
}

MouseEvent describes a KeyMouse key type. While the Key returned by Input.ReadKey has the modifier flags information, the mouse-related properties are defined by the MouseEvent type.

func (MouseEvent) ButtonID

func (m MouseEvent) ButtonID() int

ButtonID returns the button pressed during the mouse event, starting at 1. A ButtonID of 0 means that no button was pressed - i.e. this is a mouse move event without any button pressed. Up to 11 buttons are supported by the X11 mouse protocol.

func (MouseEvent) ButtonPressed

func (m MouseEvent) ButtonPressed() bool

ButtonPressed returns true if the button identified by ButtonID was pressed during the event. It returns false if instead it was released. If ButtonID is 0 (no button for this mouse event), then ButtonPressed returns true as this is how the xterm X11 mouse tracking reports it.

func (MouseEvent) Coords

func (m MouseEvent) Coords() (x, y int)

Coords returns the screen coordinates of the mouse for this event. The upper left character position on the terminal is denoted as 1,1.

func (MouseEvent) String

func (m MouseEvent) String() string

String returns the string representation of a mouse event.

type MouseEventType

type MouseEventType int

MouseEventType represents a type of mouse events.

const (
	MouseButton MouseEventType = iota + 1 // CSI ? 1000 h

	MouseAny // CSI ? 1003 h
)

List of supported mouse event types.

type Option

type Option func(*Input)

Option defines the function signatures for options to apply when creating a new Input.

func WithESCSeq

func WithESCSeq(tinfo map[string]string) Option

WithESCSeq sets the terminfo-like map that defines the interpretation of escape sequences as special keys. The map has the same field names as those used in the github.com/gdamore/tcell/terminfo package for the Terminfo struct. Only the fields starting with "Key" are supported, and only the key sequences starting with ESC (0x1b) are considered.

If nil is passed (or if the option is not specified), common default values are used. To prevent any translation of escape sequences to special keys, pass a non-nil empty map. All escape sequences will be returned as KeyESCSeq and the raw bytes of the sequence can be retrieved by calling Input.Bytes.

If you want to use tcell's terminfo definitions directly, you can use the helper function FromTerminfo that accepts an interface{} and returns a map[string]string that can be used here, in order to avoid adding tcell as a dependency, and to support any value that marshals to JSON the same way as tcell/terminfo. Note, however, that tcell manually patches some escape sequences in its code, overriding the terminfo definitions in some cases. It is up to the caller to ensure the mappings are correct, zzterm does not apply any patching.

See https://github.com/gdamore/tcell/blob/8ec73b6fa6c543d5d067722c0444b07f7607ba2f/tscreen.go#L337-L367

func WithFocus

func WithFocus() Option

WithFocus enables reporting of focus in and focus out events when the terminal gets and loses focus. Such events will be reported as a key with type KeyFocusIn or KeyFocusOut. It is the responsibility of the caller to enable focus tracking for the terminal represented by the io.Reader passed to ReadKey. As a convenience, the package provides the EnableFocus and DisableFocus functions to enable and disable focus tracking on a terminal represented by an io.Writer. See https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-FocusIn_FocusOut

func WithMouse

func WithMouse() Option

WithMouse enables mouse event reporting. Such events will be reported as a key with type KeyMouse, and the mouse information can be retrieved by calling Input.Mouse before the next call to Input.RedKey. It is the responsibility of the caller to enable mouse tracking for the terminal represented by the io.Reader passed to ReadKey, and SGR Mouse Mode must be enabled. Not all tracking modes are supported, see MouseEventType constants for supported modes. As a convenience, the package provides the EnableMouse and DisableMouse functions to enable and disable mouse tracking on a terminal represented by an io.Writer.

Only X11 xterm mouse protocol in SGR mouse mode is supported. This should be widely supported by any recent terminal with mouse support. See https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h2-Mouse-Tracking

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL