keys

package
v0.0.18 Latest Latest
Warning

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

Go to latest
Published: Sep 4, 2026 License: MIT Imports: 2 Imported by: 0

Documentation

Overview

Package keys holds the parts of the keymap that more than one package has to agree on: the key-event matcher every dispatcher uses, and the set of keys internal/app claims before any panel sees them.

It exists because the keymap spans packages but used to be validated in only one. internal/app dispatches some keys, chatview and chatlist dispatch others, and internal/config decides which of them a user may rebind — with no shared vocabulary, each could only reason about its own half. The concrete failure that produced this package: chatview accepted reply = "q", advertised it on the help card as Reply, and pressing it quit the application, because chatview had no way to know app.go had claimed "q" first.

It is a leaf. It imports nothing from this repository, so anyone may import it: app -> keys, app -> chatview -> keys, and config -> keys are all fine, and none of them can create a cycle.

Index

Constants

This section is empty.

Variables

View Source
var AppFixed = []string{

	"ctrl+q",

	"esc", "tab", "shift+tab",

	"h", "l",

	"ctrl+v",

	":",

	"`",
}

AppFixed lists every key internal/app's dispatcher claims with a HARDCODED spelling while one of the browsing panels (chat list, chat view) has focus. App-level dispatch runs before the focused panel sees the event, so a component that binds one of these never fires: the key is gone by the time it gets there.

This is the list that did not exist, and whose absence is why chatview could accept reply = "q" and quit the app when the user pressed it. It is deliberately a plain list rather than anything clever, because it has to be readable by the person deciding whether their rebind is safe — but it is not maintained by hand: TestAppFixedMatchesDispatcher in internal/app parses app.go's own source and fails when a Matches call names a key that is not here.

Not included, on purpose:

  • "n" and "N". app.go tests for them only to BREAK out of its own dispatch and hand them to chatview's search-hit cycling. Testing a key is not claiming it.
  • Everything configurable. Those depend on config.toml and arrive through AppReserved's arguments instead.
  • "q" on its own. It closes the help overlay, but only while that overlay is up — and while it is up the browsing panels are not receiving keys at all. From a browsing panel "q" is claimed only through keys.quit_browsing, which is configurable.

Functions

func AppReserved

func AppReserved(configured ...string) []string

AppReserved returns the complete set of keys internal/app claims from a focused browsing panel: AppFixed plus whatever the user's config resolved the configurable app-level bindings to.

Callers pass the resolved values — already defaulted and run through config.NormalizeKey — because this package cannot import internal/config (config imports this one) and must not carry a second copy of the defaults. Empty arguments are dropped, so an unset field reserves nothing. The result is sorted and deduplicated, so it is stable enough to compare in a test failure message.

Types

type Press

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

Press is the normalized view of a terminal key event that binding dispatch matches against.

Matching on tea.KeyPressMsg.String() alone is not sufficient. String() returns Key.Text whenever the terminal attached any, and only falls back to Keystroke() when it did not. Terminals speaking the Kitty keyboard protocol report alt-modified keys *with* their composed text on macOS — Option+1 arrives as CSI 49;3;161u, i.e. Code='1', Mod=ModAlt, Text="¡" — so String() yields "¡" while Keystroke() yields "alt+1". Keystroke() is derived from Key.Code/Key.BaseCode and the modifier bits only, so it is stable across every encoding the decoder handles (legacy ESC-prefix, Kitty CSI-u, XTerm modifyOtherKeys).

String() is still needed for the unmodified case, where it reports what the keyboard layout actually produced: shift+/ is "?" via String() but "shift+/" via Keystroke(), and a binding of "/" should match it.

The fields are unexported: a Press is built by NewPress and asked questions, never assembled by hand. Both spellings are captured once, so dispatch does not recompute them for every binding it tests.

func NewPress

func NewPress(msg tea.KeyPressMsg) Press

NewPress captures the two spellings of a key event once.

func (Press) Matches

func (p Press) Matches(bindings ...string) bool

Matches reports whether the key press is any of the given bindings. Bindings are expected in config.NormalizeKey / Keystroke() form. Empty bindings never match, so an unset config field is inert.

The Keystroke() spelling is authoritative. The String() spelling is only consulted for unmodified keys: allowing it for modified ones would let a Kitty-reported alt+/ (Text "/") fire a plain "/" binding.

func (Press) Modified

func (p Press) Modified() bool

Modified reports whether a modifier beyond shift/caps-lock was held. A modified key press is never text input, which is what lets a panel tell "the user typed a character" from "the user pressed a chord".

func (Press) Stroke

func (p Press) Stroke() string

Stroke returns the Keystroke() spelling, for diagnostics and test failure messages. Dispatch should use Matches, which knows when the other spelling is also admissible.

func (Press) Text

func (p Press) Text() string

Text returns the String() spelling: what the terminal reported the key produced, which for an unmodified key is what the layout actually typed. Dispatch should use Matches; this is for diagnostics and for tests that pin the difference between the two spellings.

Jump to

Keyboard shortcuts

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