input

package
v0.16.0 Latest Latest
Warning

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

Go to latest
Published: Aug 25, 2026 License: Apache-2.0 Imports: 9 Imported by: 0

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

Examples

Constants

View Source
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

func (a *Advance) Rows(when time.Time, reports int) int

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.

func (*Advance) Wheel added in v0.0.2

func (a *Advance) Wheel(w Wheel)

Wheel replaces what the reports are worth, keeping any part of a row already accumulated. It is what a caller does once, having asked WheelFor.

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

type Chord struct {
	Code Code
	Rune rune
	Mods Mods
}

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

func ParseChord(s string) (Chord, bool)

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

func (c Chord) MarshalText() ([]byte, error)

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

func (c Chord) String() string

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

func (c *Chord) UnmarshalText(b []byte) error

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.

func (Code) String

func (c Code) String() string

String names the key the way a help line would print it.

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

func Stamp(events []Event, at time.Time) []Event

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 FocusIn

type FocusIn struct{}

FocusIn reports that the terminal window took focus.

type FocusOut

type FocusOut struct{}

FocusOut reports that the terminal window lost focus.

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) Chord added in v0.0.2

func (k Key) Chord() Chord

Chord is the keystroke this event is one of.

func (Key) Down

func (k Key) Down() bool

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

func (k Key) Is(code Code, mods Mods) bool

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) IsRune

func (k Key) IsRune(r rune, mods Mods) bool

IsRune reports whether the key is the character r with exactly mods held.

func (Key) String

func (k Key) String() 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

func ParseKeys(s string) (Keys, bool)

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

func (k Keys) MarshalText() ([]byte, error)

MarshalText writes the sequence as Keys.String does.

func (Keys) String added in v0.0.2

func (k Keys) String() string

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

func (k *Keys) UnmarshalText(b []byte) error

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) Has

func (m Mods) Has(want Mods) bool

Has reports whether every modifier in want is held.

func (Mods) Rune added in v0.0.2

func (m Mods) Rune(r rune) Chord

Rune is the chord of a character key held with these modifiers:

input.Ctrl.Rune('w')

func (Mods) String

func (m Mods) String() string

String names the modifiers in the order a keybinding is conventionally written.

func (Mods) With added in v0.0.2

func (m Mods) With(code Code) Chord

With is the chord of a named key held with these modifiers:

input.Alt.With(input.Enter)

A key with nothing held is a chord literal, because there is no modifier to hang the call off: input.Chord{Code: input.Enter}.

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

func (o OSC) Paste(channel *clipboard.Channel) (Paste, bool)

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) Feed

func (p *Parser) Feed(b []byte) []Event

Feed adds bytes and returns everything now decodable.

func (*Parser) Flush

func (p *Parser) Flush() []Event

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

func (p *Parser) Pending() bool

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

func WheelFor(lookup func(string) (string, bool), name string) Wheel

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

func (w Wheel) Distance() float64

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

func (w Wheel) TrackpadDistance() float64

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.

Jump to

Keyboard shortcuts

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