Documentation
¶
Overview ¶
Package input turns the bytes a terminal sends into events.
A terminal reports input as a stream that mixes plain text with escape sequences, and it splits that stream wherever the read happens to land: half a sequence in one read and half in the next is normal, not an error. Parser is therefore incremental — it is fed whatever arrived and returns whatever is now unambiguous.
Nothing here touches a terminal. The parser is a function of its bytes, which is what lets every sequence this package claims to understand be stated as a test.
Index ¶
- Constants
- type Advance
- type Button
- type Chord
- type Code
- type DCS
- type DeviceAttributes
- type DeviceVersion
- type Event
- type FocusIn
- type FocusOut
- type Key
- type KeyboardFeatures
- type KeyboardFlags
- type Keys
- type Mods
- type Mouse
- type MouseAction
- type OSC
- type Parser
- type Paste
- type Resize
- type Transition
- type Wheel
Examples ¶
Constants ¶
const DefaultEscapeTimeout = 30 * time.Millisecond
DefaultEscapeTimeout is how long a byte stream waits before treating a lone escape byte as the Escape key rather than the beginning of a terminal sequence.
The parser itself has no clock: a transport feeds bytes into Parser and flushes a pending parse after this interval. Keeping the default beside the protocol makes a local terminal and a remote terminal interpret the same byte stream the same way.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type Advance ¶ added in v0.0.2
type Advance struct {
// contains filtered or unexported fields
}
Advance turns a run of wheel reports into whole rows, keeping what is left over.
The remainder is the whole reason this is a type and not a division. On a terminal that sends three reports to a notch worth three rows, each report is worth exactly one and nothing is left; on one that sends three reports to a notch worth one row, each is worth a third, and rounding each to zero would mean the view never moved at all while the wheel turned.
Zero rows is an ordinary answer. A caller scrolls by what it gets and asks again on the next report.
func (*Advance) Reset ¶ added in v0.0.2
func (a *Advance) Reset()
Reset drops any part of a row accumulated and forgets the gesture, which a caller does when the view moves for a reason that had nothing to do with scrolling.
func (*Advance) Rows ¶ added in v0.11.0
Rows converts reports in one direction into whole rows at when.
The time is what tells a finger from the wheel. Both send the same report, and only how fast they arrive is different: a wheel's notches come as far apart as a hand can turn them, and a finger's motion arrives as fast as the terminal can report it. A zero time deliberately disables temporal classification for synthetic input.
A negative report count is upwards, which is what a caller already has: a wheel event is one report in a direction, so this is normally called with plus or minus one.
type Button ¶
type Button uint8
Button identifies which mouse button an action belongs to.
const ( // ButtonNone is the zero value, which is right for a bare move and for a // wheel: neither belongs to a button. ButtonNone Button = iota ButtonLeft ButtonMiddle ButtonRight )
The buttons a terminal reports. There is no fourth: the higher button numbers in the protocol are the wheel, which arrives as an action instead.
type Chord ¶ added in v0.0.2
Chord is one keystroke: a key with the modifiers held down with it.
It is a Key with everything that is not part of the identity taken off — which transition it was, what text the terminal said it produced, when it arrived. Those describe one occurrence of a keystroke; a chord describes which keystroke it was, so only a chord can be written down in advance and bound to something.
func ParseChord ¶ added in v0.0.2
ParseChord reads what Chord.String writes, and reports whether it was a keystroke this package can name.
Together with String it supplies a stable configuration representation without coupling this package to a particular decoder or configuration format.
Example ¶
A chord is a keystroke with the occurrence taken off — no timestamp, no transition, no text the terminal happened to report. That is what makes it the thing you can write down in advance, put in a configuration file, and bind to an action.
package main
import (
"fmt"
"github.com/Tangerg/oolong/core/input"
)
func main() {
for _, s := range []string{"ctrl+c", "shift+tab", "f5", "not a key"} {
chord, ok := input.ParseChord(s)
if !ok {
fmt.Printf("%-12q unrecognised\n", s)
continue
}
fmt.Printf("%-12q %s\n", s, chord)
}
}
Output: "ctrl+c" ctrl+c "shift+tab" shift+tab "f5" f5 "not a key" unrecognised
func (Chord) MarshalText ¶ added in v0.0.2
MarshalText writes the chord as Chord.String does, so a keybinding survives being written to a configuration file and read back.
func (Chord) String ¶ added in v0.0.2
String writes the chord the way a keybinding is conventionally written, and the way ParseChord reads one back: the modifiers, then the key.
func (*Chord) UnmarshalText ¶ added in v0.0.2
UnmarshalText reads what MarshalText wrote.
It is here rather than left to a configuration reader because the parse is part of the chord's text representation and can therefore be reused by any decoder.
type Code ¶
type Code int
Code identifies which key was pressed. Character means the key produced text, carried in Key.Rune.
const ( // Character is the zero value, so a Key literal with only a rune in it is a // character press — which is what most of them are. Character Code = iota Enter Esc Backspace Tab Up Down Left Right Home End PageUp PageDown Delete Insert F1 F2 F3 F4 F5 F6 F7 F8 F9 F10 F11 F12 )
The keys a terminal can report: a character press, then the named keys in the order a keyboard is usually described — what finishes a line, what cancels, what edits, then movement, then the function row.
type DCS ¶ added in v0.0.2
type DCS struct{ Body string }
DCS is a device control string the terminal sent.
It is the other shape an answer comes in, and the one that carries a terminal's own name and version — the reply to the version query is ">|kitty(0.32.2)". There is no command number and no single grammar, so the body comes back as it was written and what it means is decided by whoever asked.
Only the shapes a terminal actually replies in are decoded as one. See the package's own notes on why: the introducer is also Alt+Shift+P.
type DeviceAttributes ¶ added in v0.0.2
type DeviceAttributes struct {
// Class is the terminal class the answer led with: 62 for a VT220, 64 for a
// VT420. Little depends on it, and terminals that emulate one of those are
// not otherwise alike.
Class int
// contains filtered or unexported fields
}
DeviceAttributes is a terminal's answer to being asked what it is.
Every terminal answers this one, which makes it useful for more than what it says. A question a terminal might not understand can be followed by this one, and this answer arriving without the other is how a terminal says it did not understand — which is not something any terminal says out loud.
func Attributes ¶ added in v0.0.2
func Attributes(class int, features ...int) DeviceAttributes
Attributes is an answer with the given class and extensions, for anything standing in for a terminal.
func (DeviceAttributes) Features ¶ added in v0.0.2
func (d DeviceAttributes) Features() []int
Features are the numbered extensions the terminal claimed. Sixel graphics is 4. There is no authority over the list and a terminal may claim what it does not do, so this is evidence rather than proof.
func (DeviceAttributes) Has ¶ added in v0.0.2
func (d DeviceAttributes) Has(n int) bool
Has reports whether the terminal claimed extension n.
It reads the claims rather than building the slice, because this is asked once per capability and a session asks about two of them.
type DeviceVersion ¶ added in v0.0.2
type DeviceVersion struct {
// Kind is the terminal class number, which says very little.
Kind int
// Version and Patch are what it reported.
Version, Patch int
}
DeviceVersion is a terminal's answer to being asked which version of itself it is.
It is the query for the terminals that answer nothing else. Alacritty exports no version in the environment and declines the version string on principle; this is what it does answer.
The numbers are as the terminal sent them, because what they mean is the terminal's convention and not a standard: most pack a version as major, minor and patch into DeviceVersion.Version, and reading it as anything is a bet on which terminal is being asked.
type Event ¶
type Event interface {
// contains filtered or unexported methods
}
Event is one thing the terminal reported. The set is closed by the unexported method: a consumer's switch over events is exhaustive by construction.
func Stamp ¶ added in v0.11.0
Stamp fills missing arrival times in events and returns the same slice.
A Parser owns protocol decoding and therefore produces zero times. The transport that read its bytes owns when they arrived; calling Stamp once at that boundary keeps key-sequence expiry, clicks, and wheel gestures on one time model. Existing non-zero times are preserved so replayed and synthetic events retain the clock chosen by their producer.
Stamp replaces value events in place. The caller owns slices returned by Parser.Feed and Parser.Flush, so no copy is needed at the ordinary call site.
type Key ¶
type Key struct {
Code Code
Rune rune
Mods Mods
// Transition is Press unless the terminal speaks the Kitty keyboard protocol,
// which is the only way repeats and releases are ever reported.
Transition Transition
// Text is what the key produced, when the terminal was able to say. It can
// hold more than one code point, and is empty on terminals that do not report
// it — Rune is the fallback and the common case.
Text string
// At is when the keystroke arrived, as whatever read it saw. It is zero when
// nothing timed it, which is what a parser fed bytes directly produces.
//
// It is here for the same reason it is on [Mouse]: a key means different things
// depending on when it came. Two chords typed in one burst are a sequence and a
// terminal never says so; the same two with a pause between them are two
// keystrokes that happen to be adjacent. Only the goroutine that did the reading
// knows, so it is stamped there rather than left for every caller to supply a
// clock for a fact the library already had. Sequence matchers can therefore judge
// elapsed time without inventing a second clock at the call site.
At time.Time
}
Key is a keyboard event.
A character key arrives as Character with the rune in Rune. Ctrl held with a letter also arrives as a character — the letter, lowercased, with Ctrl in Mods — because that is what the terminal actually sends and inventing a separate representation for it would mean two ways to ask the same question.
Example ¶
A component is handed events and says whether it wanted them. Matching one is a type switch and then a comparison — there is nothing to register and nothing to unbind.
package main
import (
"fmt"
"github.com/Tangerg/oolong/core/input"
)
func main() {
handle := func(event input.Event) string {
switch e := event.(type) {
case input.Key:
switch {
case e.IsRune('c', input.Ctrl):
return "interrupt"
case e.Code == input.Enter:
return "submit"
case e.Code == input.Character:
return "typed " + string(e.Rune)
}
case input.Resize:
return fmt.Sprintf("resized to %dx%d", e.Width, e.Height)
}
return "ignored"
}
fmt.Println(handle(input.Key{Rune: 'c', Mods: input.Ctrl}))
fmt.Println(handle(input.Key{Code: input.Enter}))
fmt.Println(handle(input.Key{Rune: 'a'}))
fmt.Println(handle(input.Resize{Width: 80, Height: 24}))
}
Output: interrupt submit typed a resized to 80x24
func (Key) Down ¶
Down reports whether the key is going down — pressed or auto-repeating. Most handlers want this rather than Press alone, or holding a key stops working on terminals that report repeats.
func (Key) Is ¶
Is reports whether the key is code with exactly mods held.
Exactly, not at least: a binding on Ctrl+C that also fired for Ctrl+Shift+C would swallow a keystroke its owner never claimed.
func (Key) String ¶
String names the keystroke the way a help line or a keybinding file writes it.
It is Chord.String: what a key event is called is what was pressed, and nothing about this particular occurrence of it.
type KeyboardFeatures ¶ added in v0.7.0
type KeyboardFeatures int
KeyboardFeatures is a set of progressive enhancements requested from or reported by a terminal speaking the Kitty keyboard protocol.
It is shared by terminal configuration and the negotiated result so there cannot be two vocabularies for one protocol. Zero is the historical keyboard encoding.
const ( // KeyboardDisambiguate makes every key arrive as an unambiguous code rather than as // whatever byte it historically produced. It is what makes Shift+Enter and // Ctrl+Enter tellable apart from Enter. KeyboardDisambiguate KeyboardFeatures = 1 << iota // KeyboardReportEvents adds key releases and repeats. Without it a key going down is // all there is, and anything held cannot be known to have been let go. KeyboardReportEvents // KeyboardReportAlternates adds the key a different layout would have produced. KeyboardReportAlternates // KeyboardReportAllAsEscapes makes even plain letters arrive as sequences. KeyboardReportAllAsEscapes // KeyboardReportText adds the text a key produced, which the terminal knows and a // program guessing from a keycode does not. KeyboardReportText // KeyboardAll is every enhancement this version understands. Unknown bits a // future terminal reports remain in KeyboardFlags, but are never emitted by a // request built by this version. KeyboardAll = KeyboardDisambiguate | KeyboardReportEvents | KeyboardReportAlternates | KeyboardReportAllAsEscapes | KeyboardReportText )
The protocol's progressive enhancements, as the bits on the wire name them.
func (KeyboardFeatures) Has ¶ added in v0.7.0
func (k KeyboardFeatures) Has(features KeyboardFeatures) bool
Has reports whether every requested feature is in k.
type KeyboardFlags ¶ added in v0.0.2
type KeyboardFlags struct{ Features KeyboardFeatures }
KeyboardFlags is a terminal's answer about which keyboard protocol enhancements are turned on.
Asking is not the same as being answered, and being answered is not the same as having asked. A terminal may accept the request for unambiguous key codes and give nothing for key releases — the protocol is live, the teardown still owes a pop, and no release ever arrives. Nothing in the events themselves distinguishes that from a user who simply has not lifted a key, so the only way to know is to read back what took.
type Keys ¶ added in v0.0.2
type Keys []Chord
Keys is a sequence of chords: one keystroke, or several typed one after another.
func ParseKeys ¶ added in v0.0.2
ParseKeys reads a sequence: chords separated by spaces. A chord that is itself the space bar is written "space", so there is nothing ambiguous to split on.
Example ¶
Several chords typed one after another are one binding. A terminal never says so — it reports two keystrokes that happen to be adjacent — which is why the time a key arrived travels with it.
package main
import (
"fmt"
"github.com/Tangerg/oolong/core/input"
)
func main() {
keys, ok := input.ParseKeys("ctrl+x ctrl+s")
fmt.Println(ok, len(keys), keys)
}
Output: true 2 ctrl+x ctrl+s
func (Keys) MarshalText ¶ added in v0.0.2
MarshalText writes the sequence as Keys.String does.
func (Keys) String ¶ added in v0.0.2
String writes the sequence with a space between the chords, which is how a keybinding file spells one and what ParseKeys reads back.
func (*Keys) UnmarshalText ¶ added in v0.0.2
UnmarshalText reads what MarshalText wrote.
type Mods ¶
type Mods uint8
Mods is the set of modifier keys held during an event.
const ( Shift Mods = 1 << iota Alt Ctrl // Super is the platform's own modifier — Command on macOS, the Windows key // elsewhere. Only terminals speaking the Kitty keyboard protocol report it. Super )
The modifiers a terminal can report. Super is last because it is the only one that needs the Kitty protocol to arrive at all.
func (Mods) Rune ¶ added in v0.0.2
Rune is the chord of a character key held with these modifiers:
input.Ctrl.Rune('w')
type Mouse ¶
type Mouse struct {
Pos image.Point
Action MouseAction
Button Button
Mods Mods
// At is when the report arrived, as whatever read it saw. It is zero when nothing
// timed it, which is what a parser fed bytes directly produces.
//
// A mouse report means different things depending on when it came. Two presses
// close together are a double-click and a terminal never says so; a run of wheel
// reports without a gap is a trackpad and not the wheel. Both questions are about
// arrival, and the only thing that knows the answer is the goroutine that did the
// reading — so it is stamped there rather than left for every caller to supply a
// clock for a fact the library already had.
At time.Time
}
Mouse is a mouse event, positioned in cells with the origin at the top left.
type MouseAction ¶
type MouseAction uint8
MouseAction is what the mouse did.
const ( MouseDown MouseAction = iota MouseUp MouseDrag MouseMove WheelUp WheelDown )
What the mouse did. Drag is a move with a button held, and the two wheel directions are actions rather than buttons because no button is involved.
type OSC ¶ added in v0.0.2
type OSC struct {
// Command is the number the sequence names itself by: 11 for the colour the
// terminal draws on, 52 for its clipboard.
Command int
// Params is everything after the command number and its semicolon, left as
// the terminal wrote it apart from invalid UTF-8 being replaced.
//
// What it means depends on the command, which this package deliberately does
// not work out. Reading a background colour belongs to whatever owns colours;
// this package owns bytes.
Params string
}
OSC is an operating system command the terminal sent.
This is how a terminal answers a question. A program writes a query, and the answer comes back on the input stream mixed in with whatever the user is typing — asking what colour the terminal draws on and reading its clipboard both work this way. A session that asks nothing never sees one.
func (OSC) Paste ¶ added in v0.7.0
Paste settles o as clipboard text when channel owns the live OSC 52 request. Keeping the interpretation on the decoded command gives every terminal adapter one path from the same wire event to Paste, while the lower clipboard package remains independent of the event model.
type Parser ¶
type Parser struct {
// contains filtered or unexported fields
}
Parser decodes terminal bytes into events, incrementally.
Bytes are handed to Parser.Feed exactly as they arrived, at whatever boundaries the read produced. Anything not yet unambiguous stays buffered: escape sequences and multi-byte characters routinely arrive in pieces, and a decoder that assumed otherwise would drop keys under load.
One case cannot be resolved by waiting. A lone escape byte is either the Escape key or the start of a sequence whose remainder has not arrived, and only time tells the difference. Parser.Pending reports that something is waiting, and Parser.Flush declares the wait over.
A Parser belongs to whichever goroutine reads the terminal and must not be copied after first use. It is not safe for concurrent use.
func (*Parser) Flush ¶
Flush resolves what only time could resolve and returns the result.
A buffered escape becomes the Escape key, and anything after it is re-read as ordinary input. A half-arrived character is dropped, since the rest is never coming. A paste in progress is left alone: it is incomplete rather than ambiguous, and cutting it short would corrupt the text.
func (*Parser) Pending ¶
Pending reports whether anything is waiting for more input to make sense of it: bytes that might yet become a sequence, or a runaway one still being dropped. It is what tells a caller to arm the timer that will call Parser.Flush, and the runaway counts because the state has to end somewhere — otherwise the next keystroke that happened to be a parameter byte would vanish into it.
type Paste ¶
type Paste struct{ Text string }
Paste is a block of text the terminal delivered as a paste rather than as keystrokes, so it can be inserted whole instead of being interpreted a character at a time.
type Resize ¶
type Resize struct{ Width, Height int }
Resize reports the terminal's new size in cells.
type Transition ¶
type Transition uint8
Transition is what happened to a key.
const ( // Press is the zero value: an ordinary terminal only ever reports presses, // and a Key literal that says nothing about its transition means one. Press Transition = iota Repeat Release )
What happened to a key. Repeat and Release only ever arrive from a terminal speaking the Kitty keyboard protocol; everything else reports presses.
type Wheel ¶ added in v0.0.2
type Wheel struct {
// Reports is how many wheel events the terminal sends for one physical notch.
Reports int
// Rows is how far one notch should move a view.
Rows int
// Trackpad is how far a notch's worth of continuous scrolling should move it.
//
// A finger is not a notch. A terminal that coalesces a swipe into a few reports
// has to make each worth more, or a swipe crawls; one that reports every scrap of
// motion has to make each worth less, or a swipe flings. The two cannot be told
// apart from a report — only from how fast they arrive, which is why a mouse
// event carries when it came.
//
// It is divided by [trackpadReports] rather than by Reports, because a notch is
// what Reports counts and a finger does not have notches.
Trackpad int
}
Wheel says what a terminal's wheel reports are worth.
Why this is not a constant ¶
A wheel report carries a direction and no magnitude, and terminals disagree about how many reports one notch of the wheel is. Apple Terminal, kitty, Ghostty and alacritty send three; iTerm2 and WezTerm send one; an editor's embedded terminal sends one and means three rows by it. So the same code, scrolling a fixed number of rows per report, moves three times as far on one terminal as on another — and there is no way to ask, because the protocol does not carry it.
The zero value is the commoner arrangement: three reports to a notch, three rows to a notch, which comes to one row a report. It is a reasonable answer everywhere and the right one on most terminals.
func WheelFor ¶ added in v0.0.2
WheelFor is what a terminal does with its wheel.
name is what the terminal said it was when asked, and outranks everything else: an environment describes the terminal a session was started from, which over ssh, in a container, or under a multiplexer is not the terminal it is talking to. An empty name means nothing was asked, or nothing answered, and the environment is all there is.
Multiplexers ¶
A multiplexer reads the mouse reports and writes its own, so whatever the outer terminal batched is gone by the time the program sees anything: tmux, screen and zellij all forward one report per notch regardless of what arrived. Their answer therefore replaces the outer terminal's rather than being combined with it, and checking for them has to come first.
The lookup is passed in rather than read, for the same reason it is everywhere else in this library: this package is a function of its inputs, and a test that could not say what terminal it was in could not check any of these answers.
func (Wheel) Distance ¶ added in v0.0.2
Distance is how many rows one report of the wheel is worth, as a fraction of a row.
A fraction, because a report is very often worth less than a row and rounding each one to zero would stop the view moving at all — see Advance.
func (Wheel) TrackpadDistance ¶ added in v0.0.2
TrackpadDistance is how many rows one report of continuous scrolling is worth.
A terminal that says nothing about it is taken to treat a finger like the wheel, which is what nearly all of them do: the ones where it differs are the ones that coalesce a swipe hardest.