input

package
v0.0.3 Latest Latest
Warning

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

Go to latest
Published: Aug 6, 2026 License: Apache-2.0 Imports: 8 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 (
	// KittyDisambiguate 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.
	KittyDisambiguate = 1 << iota
	// KittyReportEvents 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.
	KittyReportEvents
	// KittyReportAlternates adds the key a different layout would have produced.
	KittyReportAlternates
	// KittyReportAllAsEscapes makes even plain letters arrive as sequences.
	KittyReportAllAsEscapes
	// KittyReportText adds the text a key produced, which the terminal knows and a
	// program guessing from a keycode does not.
	KittyReportText
)

The Kitty keyboard protocol's progressive enhancements, as the bits a terminal reports them in.

View Source
const DefaultKeyTimeout = time.Second

DefaultKeyTimeout is how long a partly-typed sequence waits for the rest of itself when a keymap does not say.

A second, which is what a text editor with sequences in it has settled on. It is long enough that nobody types "g g" too slowly by accident and short enough that a "g" left over from a minute ago does not turn the next one into something else.

Variables

This section is empty.

Functions

This section is empty.

Types

type Action added in v0.0.2

type Action string

Action is one thing something can be asked to do, under a name.

Why a name and not a keystroke

A widget that owns its keystrokes owns two things at once — what it can do, and what produces it — and can express neither of them on its own. There is nowhere to put a sequence like "g g", because a field holds one key; rebinding one chord means replacing the whole struct, because the struct is the binding; and the same widget in two places, where escape means "back" in one and "close" in the other, has to be told about both.

So a widget names what it can do and answers to the name, and a Keymap says which keystrokes produce it. The two halves can then be replaced independently, which is the whole point: a program keeps the widgets and rebinds the keys, or keeps the keys and swaps the widget.

Naming one

A name is an identifier and reads as one: lowercase words joined by hyphens. "delete-word-back", "select-all". Anything is allowed — an action nothing else has heard of is exactly what a program binds its own keys to — and the only rule is that the two halves agree on the spelling, which is what makes a constant worth declaring for one.

func (Action) Does added in v0.0.2

func (a Action) Does() string

Does is the action in the words a hint row shows it in: the name, with the hyphens that make it an identifier taken out.

There is no second field for a description, and no table mapping one to the other. A description held beside the name is a description that drifts from it, and the name already says what the thing does — that is what naming an action is. A program that wants other words, in another language or in its own house style, names its actions in them.

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

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

At is the same for a report that came with a time on it, which is what a terminal's reader stamps — see Mouse.At.

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.

func (*Advance) By added in v0.0.2

func (a *Advance) By(reports int) int

By is how many rows n reports in one direction come to.

A negative count is upwards, which is what the caller already has: a wheel event is one report in a direction, so this is called with plus or minus one.

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

type Binding struct {
	Keys   Keys
	Action Action
}

Binding is one entry of a map: a sequence of chords, and what it does.

func (Binding) String added in v0.0.2

func (b Binding) String() string

String writes the binding the way a keybinding file does: what to press, then what it does.

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.

It is the half of the round trip that makes keys configurable at run time: a keybinding read out of a file is a string, and turning it into something a keymap can hold is this. Nothing else in the repository needs it, which is why it is here rather than in whatever ends up reading the file.

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 whatever reads the file because it is the same parse either way, and because implementing it is what makes a keymap something any of the usual decoders can fill in without being told how.

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.

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 Handler

type Handler interface {
	Handle(ev Event) bool
}

Handler is anything that answers an event, reporting whether it consumed it.

It lives here for the same reason [grid.Drawer] lives with the view: the word belongs to the layer that owns the thing being handled. Everything further up that answers input says so by embedding this, so "consumed" means one thing across the whole repository rather than one thing per layer that happens to line up.

An unconsumed event carries on to whatever else might want it, which is how a key can mean one thing inside a text field and another outside it without either side knowing about the other.

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 — see [Keymap.Lookup].
	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.

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

type KeyboardFlags struct{ Flags int }

KeyboardFlags is a terminal's answer about which of the Kitty keyboard protocol's 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.

func (KeyboardFlags) Has added in v0.0.2

func (k KeyboardFlags) Has(flag int) bool

Has reports whether a flag is among those the terminal turned on.

type Keymap added in v0.0.2

type Keymap struct {
	// Timeout is how long a partly-typed sequence waits for the rest of itself. Zero
	// uses [DefaultKeyTimeout].
	Timeout time.Duration
	// contains filtered or unexported fields
}

Keymap says which keystrokes produce which actions.

It is a table and nothing else: it knows no widget, and every widget that reads keys through one answers the actions it recognises and ignores the rest. That is what lets one map serve a whole interface — a field, the container around it and the program's own keys, all in the same table — and what lets a program hand the same map to two widgets without either of them knowing.

Sequences

A binding can be more than one chord long. The chords have to arrive within Keymap.Timeout of each other, judged by when the terminal's reader stamped them; a chord that does not continue what was being typed abandons it and is read on its own.

A binding that is a proper prefix of a longer one is not reached. Nothing here can wake an interface up after a pause, so a chord that might still be the start of something longer can only be decided by what comes next — and deciding it as the shorter binding is deciding that the longer one never fires. The longer one wins, and the shorter is a binding that exists and never happens.

The zero Keymap is empty and binds nothing. Use it by pointer: a map with bindings added to a copy of it would leave the original quietly sharing them.

Example
package main

import (
	"fmt"

	"github.com/Tangerg/oolong/core/input"
)

func main() {
	// A widget names what it can do; the map says which keystrokes produce the name.
	// Neither knows the other, so either can be replaced without touching the other.
	keys := &input.Keymap{}
	keys.Bind("delete-word-back", input.Ctrl.Rune('w'))
	keys.Bind("delete-word-back", input.Alt.With(input.Backspace))
	keys.Bind("submit", input.Chord{Code: input.Enter})

	var pending input.Pending
	for _, key := range []input.Key{
		{Code: input.Character, Rune: 'w', Mods: input.Ctrl},
		{Code: input.Enter},
		{Code: input.Character, Rune: 'q'},
	} {
		action, mine := keys.Lookup(key, &pending)
		fmt.Printf("%-9s %-18s mine=%v\n", key, "\""+string(action)+"\"", mine)
	}

}
Output:
ctrl+w    "delete-word-back" mine=true
enter     "submit"           mine=true
q         ""                 mine=false
Example (Sequences)
package main

import (
	"fmt"

	"github.com/Tangerg/oolong/core/input"
)

func main() {
	// A binding can be more than one chord long. The first chord is the map's and
	// names nothing yet, which the caller has to consume rather than pass on.
	keys := &input.Keymap{}
	keys.Bind("go-to-top", input.Chord{Rune: 'g'}, input.Chord{Rune: 'g'})

	var pending input.Pending
	for range 2 {
		action, mine := keys.Lookup(input.Key{Rune: 'g'}, &pending)
		fmt.Printf("%q taken=%v waiting=%q\n", action, mine, pending.Keys().String())
	}

}
Output:
"" taken=true waiting="g"
"go-to-top" taken=true waiting=""

func (*Keymap) Action added in v0.0.2

func (m *Keymap) Action(keys ...Chord) (Action, bool)

Action is what a sequence names on its own, and whether it names anything.

It is the lookup with no sequence under way and nothing remembered afterwards, which is what asking a question about the map — rather than reading keys through it — wants.

func (*Keymap) Bind added in v0.0.2

func (m *Keymap) Bind(a Action, keys ...Chord)

Bind makes a sequence of chords produce an action, replacing whatever those chords produced before.

An action may have several sequences bound to it — Ctrl+W and Alt+Backspace both delete a word — and the order they are bound in is the order Keymap.Keys gives them back, so the first is the one a hint row shows.

func (*Keymap) Bindings added in v0.0.2

func (m *Keymap) Bindings() []Binding

Bindings is every binding in the map, in the order it was made. It is what a program showing its own keybinding list, or writing one back out to a file, asks for.

func (*Keymap) Keys added in v0.0.2

func (m *Keymap) Keys(a Action) []Keys

Keys are the sequences bound to an action, in the order they were bound. Nothing bound to it is no sequences, which is what a hint row skips.

Example
package main

import (
	"fmt"

	"github.com/Tangerg/oolong/core/input"
)

func main() {
	// What a hint row asks: the keystrokes bound to an action, in the order they were
	// bound, so the one that works everywhere can be put first.
	keys := &input.Keymap{}
	keys.Bind("delete-word-back", input.Ctrl.Rune('w'))
	keys.Bind("delete-word-back", input.Alt.With(input.Backspace))

	for _, bound := range keys.Keys("delete-word-back") {
		fmt.Println(bound)
	}
	fmt.Println(input.Action("delete-word-back").Does())

}
Output:
ctrl+w
alt+backspace
delete word back

func (*Keymap) Lookup added in v0.0.2

func (m *Keymap) Lookup(k Key, p *Pending) (Action, bool)

Lookup is what a keystroke means, following any sequence already under way.

It reports the action the keystroke completed, and whether the keystroke was this map's at all. Those are not the same question: a chord that begins a sequence is the map's without naming an action yet, and comes back as an empty action that was nonetheless taken. A caller that passed it on instead would let the first half of a sequence act somewhere else as well.

switch action, mine := keys.Lookup(key, &w.pending); {
case !mine:
	return false // not ours; let it go past
case action == "":
	return true // the start of a sequence, waiting for the rest
}

A nil Pending has nowhere to remember a half-typed sequence, so only what a chord names on its own is reachable through one.

Releases are not keystrokes and name nothing: a map answers a key going down.

func (*Keymap) Unbind added in v0.0.2

func (m *Keymap) Unbind(keys ...Chord) bool

Unbind takes a sequence out, reporting whether it was there.

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.

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.

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.

Not safe for concurrent use: it belongs to whichever goroutine reads the terminal.

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 loop 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 Pending added in v0.0.2

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

Pending is how far into a sequence the chords typed so far have got.

It belongs to whoever is reading keys and not to the map they are read against. A map is a table, and tables are shared: two fields with the same bindings are two places a sequence can be half typed, and one of them finishing it must not finish the other's.

The zero value is nothing typed yet.

func (*Pending) Clear added in v0.0.2

func (p *Pending) Clear()

Clear abandons a partly-typed sequence, which is what to do when the keyboard goes somewhere else.

func (*Pending) Keys added in v0.0.2

func (p *Pending) Keys() Keys

Keys are the chords typed so far that have not yet named an action, which is what an interface showing a "waiting for the rest of it" hint would draw.

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(getenv func(string) string, 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