Documentation
¶
Overview ¶
Package myelin is a key-binding library for Go programs: keymaps whose bindings hold in contexts, key sequences, and which-key menus, all as plain data.
A program reads its keymap from its own files, in its own format, and hands it to myelin as values: one Doc per layer, holding Bind and Group nodes. It gets data back: what a sequence runs (Match), what can follow it (Menu), why a binding wins (Candidate), and the state of a sequence in progress (Pending). myelin reads no files and knows no file format. It does no IO, reads no clock and starts no goroutine.
Keys ¶
A myelinkey.Key is a keystroke's canonical name, in Ghostty's vocabulary: "ctrl+d", "shift+g", "page_down", "ctrl+key_w", "?". A key is a character, which follows the keyboard layout, or a physical key, named by its W3C code value, which stays in its place; the program decides which it sends. A modifier tapped on its own is a modifier key: "shift", and a double tap of Shift is the sequence "shift shift". Two keys are the same keystroke exactly when they are ==, whether they came from a terminal, from Bubble Tea or from a HOTTY host.
People write keys as they would for Ghostty: modifiers in any order and case, W3C code values, a letter in either case. A Schema can read them strictly instead, with one spelling per key (StrictKeys), or read a capital as Shift with its letter, as vim does (ShiftedCapitals). A sequence is keys separated by spaces, "g g", and "<name>" stands for an alias's keys, such as "<leader>" (Schema.Aliases, Doc.Aliases). myelinkey.Display writes a key for a menu, with glyphs for the arrows and Tab. myelinkey/SPEC.md in the repository specifies the names.
Paths and contexts ¶
A program declares what can be focused in a Schema: kinds of node, each with typed attributes, and the keymap's layers. At any moment the focus is a Path: the focused node and the nodes around it, outermost first, such as a window, a list in it, and a filter field over the list.
A context is a CEL expression (https://cel.dev) over the path. A node's name is true while the node is on the path, its attributes are node.attribute, and focus is the innermost node's name:
list && !field
list.dir.startsWith('/tmp')
focus == 'list'
Env.Compile checks a context against the schema, so a mistake fails when the keymap loads and a keymap that loads never fails on a key.
Contexts run on cel-lite (github.com/neuroplastio/cel-lite), which implements the part of CEL they need with Go's standard library alone: comparisons, the logical operators, in, size, contains, startsWith, endsWith and the conversions. What it doesn't implement, such as the macros (exists, all), matches, timestamps and durations, fails at load and says what it is: "regular expressions (matches) are not supported".
Precedence ¶
When more than one binding of the same keys holds on a path, the winner is:
- the one whose context reaches deepest into the path (Context.Depth);
- at the same depth, the one in the higher layer;
- in the same layer, the later one.
An unbind is a binding to nothing, and wins the same way. A key that types text goes to a node that takes text (NodeType.Text) unless a binding at least as deep as that node takes it.
Building and querying ¶
NewEnv compiles a schema into an Env, and Env.Build resolves a program's documents into a Keymap, checking each binding with the program's own rules. A keymap answers questions about a path: Keymap.Match, Keymap.Effective, Keymap.KeysFor, Keymap.KeyFor, Keymap.Explain and Keymap.Shadowed.
Keymap.Menu is the which-key level after a sequence: an Item for each key that can follow it, a group or the binding that wins there. Keymap.Tree nests every level under it. Both are data that any front end can draw.
The sequencer ¶
Feed, Expire and Arm are pure functions over Pending. A key and the time go in. The new state and a Result come out: a binding to run, keys that matched nothing, or a deadline. The program keeps the state, and when a deadline comes, its own event loop calls Expire.
Data and handles ¶
The data types have exported fields and no methods except their generated msgpack codec (EncodeMsg and DecodeMsg, from github.com/tinylib/msgp), so a keymap, a path or a menu can cross a process as a msgpack map. Times are msgpack timestamps. Env, Keymap and Context are handles: they hold compiled CEL, are never serialized, and are safe for concurrent use.
DecodeMsg is bounded for bytes a program did not write itself: an array holds at most 1,024 elements, a map 256 entries, and a Doc 16,384 Binds. More fails with msgp.ErrLimitExceeded before anything is allocated for it. Strings, and the values of a Step's Attrs, are the reader's to bound: a program that reads a peer's bytes sets SetMaxStringLength, SetMaxElements and SetMaxRecursionDepth on its msgp.Reader.
Keys are named in the package github.com/neuroplastio/myelin/myelinkey, a module of its own that needs only the standard library and builds with TinyGo, so a program or a library that reads input can name keys without taking on CEL.
docs/keymaps.md in the repository explains, for people writing keymaps, how keys are spelled, how contexts work and which binding wins.
Example ¶
A file manager's keymap: its defaults and a user's config, matched where the focus is.
package main
import (
"fmt"
"slices"
"time"
"github.com/neuroplastio/myelin"
"github.com/neuroplastio/myelin/myelinkey"
)
func main() {
// 1. Declare what can be focused: a window, a list of files in a
// directory, and a filter field that takes text.
env, err := myelin.NewEnv(myelin.Schema{
Nodes: []myelin.NodeType{
{Name: "window"},
{Name: "list", Attrs: map[string]myelin.Type{"dir": "string"}},
{Name: "field", Text: true},
},
Layers: []string{"defaults", "user"},
Timeout: 500 * time.Millisecond,
})
if err != nil {
panic(err)
}
// 2. Write the bindings: the program's defaults, and a user's override
// for lists under /tmp.
defaults := myelin.Doc{Layer: "defaults", Binds: []myelin.Bind{
{Keys: "j", Action: "down"},
{Keys: "g g", Action: "top"},
{Keys: "escape", Action: "back"},
{Keys: "escape", Action: "close-filter", When: "field"},
{Keys: "enter", Action: "open", When: "list"},
}}
user := myelin.Doc{Layer: "user", Binds: []myelin.Bind{
{Keys: "enter", Action: "preview", When: "list.dir.startsWith('/tmp')"},
}}
// 3. Build the keymap. Problems are errors and warnings, each with where
// it was written.
km, problems := env.Build([]myelin.Doc{defaults, user}, nil)
if km == nil {
panic(fmt.Sprint(problems))
}
// 4. Ask what keys do where the focus is.
home := myelin.Path{{Name: "window"}, {Name: "list", Attrs: map[string]any{"dir": "/home/me"}}}
tmp := myelin.Path{{Name: "window"}, {Name: "list", Attrs: map[string]any{"dir": "/tmp"}}}
filter := append(slices.Clone(home), myelin.Step{Name: "field"})
match := func(keys string, p myelin.Path) string {
seq, err := myelinkey.ParseSeq(keys)
if err != nil {
return err.Error()
}
switch m := km.Match(seq, p); {
case m.Prefix:
return "wait for more"
case m.Exact != nil:
return m.Exact.Action
}
return "nothing"
}
fmt.Println("enter in a list:", match("enter", home))
fmt.Println("enter in /tmp:", match("enter", tmp))
fmt.Println("escape in the filter:", match("escape", filter))
fmt.Println("j in the filter:", match("j", filter))
fmt.Println("g:", match("g", home))
}
Output: enter in a list: open enter in /tmp: preview escape in the filter: close-filter j in the filter: nothing g: wait for more
Example (Config) ¶
myelin reads no files: a program decodes its own config, in its own format, into documents. Here the defaults are sections in the style of Zed's keymap.json, and the user's overrides are rules in the style of VS Code's keybindings.json.
package main
import (
"encoding/json"
"fmt"
"maps"
"os"
"slices"
"github.com/neuroplastio/myelin"
"github.com/neuroplastio/myelin/myelinkey"
)
func main() {
env, err := myelin.NewEnv(myelin.Schema{
Nodes: []myelin.NodeType{
{Name: "window"},
{Name: "list", Attrs: map[string]myelin.Type{"dir": "string"}},
{Name: "field", Text: true},
},
Layers: []string{"defaults", "user"},
})
if err != nil {
panic(err)
}
// 1. The defaults: a context, and the keys bound in it.
var sections []struct {
Context string
Bindings map[string]string
}
data, err := os.ReadFile("testdata/config/defaults.json")
if err != nil {
panic(err)
}
if err := json.Unmarshal(data, §ions); err != nil {
panic(err)
}
defaults := myelin.Doc{Layer: "defaults", Source: "defaults.json"}
for _, s := range sections {
for _, keys := range slices.Sorted(maps.Keys(s.Bindings)) {
defaults.Binds = append(defaults.Binds, myelin.Bind{Keys: keys, Action: s.Bindings[keys], When: s.Context})
}
}
// 2. The user's rules decode straight into binds: keys, action, when,
// args, unbind.
user := myelin.Doc{Layer: "user", Source: "user.json"}
if data, err = os.ReadFile("testdata/config/user.json"); err != nil {
panic(err)
}
if err := json.Unmarshal(data, &user.Binds); err != nil {
panic(err)
}
// 3. Build, and ask.
km, problems := env.Build([]myelin.Doc{defaults, user}, nil)
if km == nil {
panic(fmt.Sprint(problems))
}
home := myelin.Path{{Name: "window"}, {Name: "list", Attrs: map[string]any{"dir": "/home/me"}}}
tmp := myelin.Path{{Name: "window"}, {Name: "list", Attrs: map[string]any{"dir": "/tmp"}}}
filter := append(slices.Clone(home), myelin.Step{Name: "field"})
for _, q := range []struct {
keys, where string
p myelin.Path
}{
{"enter", "a list", home},
{"enter", "/tmp", tmp},
{"d d", "a list", home},
{"ctrl+d", "a list", home},
{"escape", "the filter", filter},
{"g g", "a list", home},
} {
seq, _ := myelinkey.ParseSeq(q.keys)
action := "nothing"
if m := km.Match(seq, q.p); m.Exact != nil {
action = m.Exact.Action
}
fmt.Printf("%s in %s: %s\n", q.keys, q.where, action)
}
}
Output: enter in a list: open enter in /tmp: preview d d in a list: nothing ctrl+d in a list: delete escape in the filter: close-filter g g in a list: top
Index ¶
- func Expire(km *Keymap, st Pending, now time.Time) (Pending, Result)
- func Feed(km *Keymap, st Pending, k myelinkey.Key, p Path, now time.Time) (Pending, Result)
- func FormatPath(p Path) string
- func Warn(err error) error
- type Bind
- type Binding
- type Candidate
- type Context
- type Doc
- type Env
- type Fence
- type Group
- type Item
- type Keymap
- func (km *Keymap) Alias(name string) ([]myelinkey.Key, bool)
- func (km *Keymap) All() []Binding
- func (km *Keymap) Effective(p Path) []Binding
- func (km *Keymap) Explain(seq []myelinkey.Key, p Path) []Candidate
- func (km *Keymap) Fence(p Path) *Fence
- func (km *Keymap) KeyFor(action string, p Path) *Binding
- func (km *Keymap) KeysFor(action string, args []string, p Path) [][]myelinkey.Key
- func (km *Keymap) Match(seq []myelinkey.Key, p Path) Match
- func (km *Keymap) Menu(seq []myelinkey.Key, p Path, applies func(Binding) bool) Menu
- func (km *Keymap) ParseSeq(s string) ([]myelinkey.Key, error)
- func (km *Keymap) Shadowed(paths ...Path) []Problem
- func (km *Keymap) Starts(k myelinkey.Key) bool
- func (km *Keymap) Tree(seq []myelinkey.Key, p Path, applies func(Binding) bool) Menu
- type Match
- type Menu
- type NodeType
- type Path
- type Pending
- type Problem
- type Result
- type Schema
- type Step
- type Type
Examples ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func Expire ¶
Expire is the pending keys timing out. Before the state's deadline it does nothing, so a stale timer is harmless. After it, the binding the pending keys finish runs, on the path they were typed on; with none they drop, or a fence there swallows them. Either way the state is cleared.
func Feed ¶
Feed is one key typed on a path at now.
- A key a longer binding continues, at least as deep as any binding it finishes, is buffered: the Result's Deadline is now plus the schema's Timeout, and so is the new state's.
- A key that finishes a binding, with nothing as deep continuing it, runs it.
- A key that does neither drops the keys pending with it, and the state is cleared. Whether they drop or are swallowed is decided on the path the first of them was typed on: the state's, or p.
With nothing pending, a key no binding starts with is one map lookup.
Example ¶
The sequencer holds no clock: the time goes in with each key, and a deadline comes out for the program's loop to wake at.
package main
import (
"fmt"
"time"
"github.com/neuroplastio/myelin"
)
func main() {
// 1. A keymap where g runs one thing and g g another: after g, it waits.
env, err := myelin.NewEnv(myelin.Schema{
Nodes: []myelin.NodeType{{Name: "list"}},
Layers: []string{"defaults"},
Timeout: 500 * time.Millisecond,
})
if err != nil {
panic(err)
}
km, problems := env.Build([]myelin.Doc{{Layer: "defaults", Binds: []myelin.Bind{
{Keys: "g", Action: "go"},
{Keys: "g g", Action: "top"},
}}}, nil)
if km == nil {
panic(fmt.Sprint(problems))
}
// 2. Feed keys with the time they came. A deadline means: wake then,
// and call Expire.
p := myelin.Path{{Name: "list"}}
now := time.Date(2026, 10, 11, 12, 0, 0, 0, time.UTC)
var st myelin.Pending
st, res := myelin.Feed(km, st, "g", p, now)
fmt.Println("g waits until", res.Deadline.Format("15:04:05.000"))
st, res = myelin.Feed(km, st, "g", p, now.Add(200*time.Millisecond))
fmt.Println("g g runs", res.Run.Action)
st, _ = myelin.Feed(km, st, "g", p, now)
st, res = myelin.Expire(km, st, now.Add(100*time.Millisecond))
fmt.Println("before the deadline, Expire does nothing:", res.Run == nil)
st, res = myelin.Expire(km, st, now.Add(500*time.Millisecond))
fmt.Println("at the deadline, g runs", res.Run.Action)
_, res = myelin.Feed(km, st, "x", p, now)
fmt.Println("x matches nothing:", res.Dropped)
}
Output: g waits until 12:00:00.500 g g runs top before the deadline, Expire does nothing: true at the deadline, g runs go x matches nothing: [x]
func FormatPath ¶
FormatPath writes a path from the outside in, each step with the attributes it sets, sorted: `window(name="main") > list(dir="/tmp", hidden) > field`. An empty string, a false bool and an empty map or list are left out.
Types ¶
type Bind ¶
type Bind struct {
// Keys are the sequence as written: "g g", "<leader> n".
Keys string `msg:"keys"`
// Action is what the keys run, as the program names it. It is empty
// for an unbind.
Action string `msg:"action,omitempty"`
// Unbind binds the keys to nothing, taking them from the bindings it
// wins over.
Unbind bool `msg:"unbind,omitempty"`
// Args are the action's values. Absent (nil or empty) means none, and
// [""] means one empty value; a program can tell them apart.
Args []string `msg:"args,omitempty"`
// Label is how menus show the binding; empty leaves it to the program.
Label string `msg:"label,omitempty"`
// When is the bind's context, one CEL expression; empty holds
// everywhere.
When string `msg:"when,omitempty"`
// Source is where the bind was written, as the program words it:
// "config.kdl:12", an extension's manifest.
Source string `msg:"source,omitempty"`
// Props are the program's own properties, such as a flag its check
// reads. myelin passes them back and never reads them.
Props map[string]string `msg:"props,omitempty"`
}
Bind binds a key sequence to an action, or with Unbind takes a binding away. Binds and unbinds keep one order.
type Binding ¶
type Binding struct {
Bind `msg:"bind"`
// Seq is the sequence's keys, its aliases resolved.
Seq []myelinkey.Key `msg:"seq"`
// Layer is the layer of the document the bind came from.
Layer string `msg:"layer"`
}
Binding is a Bind as the keymap holds it: its sequence read, its aliases resolved, and its layer named.
type Candidate ¶
type Candidate struct {
// Binding is the binding considered: one of the sequence's, or of a
// longer sequence it begins.
Binding Binding `msg:"binding"`
// Holds is set when the binding's context holds on the path.
Holds bool `msg:"holds"`
// Depth is how deep into the path its context reaches; -1 when it
// doesn't hold.
Depth int `msg:"depth"`
// Wins is set on the binding that runs: the one [Keymap.Match] returns.
Wins bool `msg:"wins"`
// Fenced is set when the binding holds but the path's fence shadows it:
// its context is shallower than the fence's ([Keymap.Fence]).
Fenced bool `msg:"fenced,omitempty"`
}
Candidate is one binding Keymap.Explain considered for a sequence.
type Context ¶
type Context struct {
// contains filtered or unexported fields
}
Context is a compiled context: a CEL boolean over the path. A nil Context holds everywhere, at depth 0.
func (*Context) Depth ¶
Depth is how deep into the path the context reaches: the shortest prefix of the path it holds on, given that it holds on the whole path; -1 when it does not hold. A nil context is depth 0, and so is one that holds with nothing on the path (`!dialog`): saying what is not there adds no depth.
Example ¶
A context's depth is how far into the path it reaches: the deepest wins.
package main
import (
"fmt"
"github.com/neuroplastio/myelin"
)
func main() {
env, err := myelin.NewEnv(myelin.Schema{
Nodes: []myelin.NodeType{{Name: "window"}, {Name: "list"}, {Name: "preview"}, {Name: "field", Text: true}},
Layers: []string{"defaults"},
})
if err != nil {
panic(err)
}
p := myelin.Path{{Name: "window"}, {Name: "list"}, {Name: "field"}}
for _, when := range []string{"", "window", "list", "field", "list && !preview", "!preview", "preview"} {
c, err := env.Compile(when)
if err != nil {
panic(err)
}
fmt.Printf("%q reaches %d\n", when, c.Depth(p))
}
}
Output: "" reaches 0 "window" reaches 1 "list" reaches 2 "field" reaches 3 "list && !preview" reaches 2 "!preview" reaches 0 "preview" reaches -1
func (*Context) Holds ¶
Holds reports whether the context holds on the whole path. A context that reads an attribute of a node not on the path, or one the path's step does not give, does not hold.
func (*Context) MayDepth ¶
MayDepth is Depth with the attributes the path's steps do not give unknown: the shortest prefix the context may hold on, given that it may hold on the whole path; -1 when it cannot.
type Doc ¶
type Doc struct {
// Layer is one of the schema's layers. Several documents may share one.
Layer string `msg:"layer"`
// Aliases name key sequences, as the schema's do. For each name the
// highest layer's wins, and within a layer the later document's; the
// schema's is the lowest.
Aliases map[string]string `msg:"aliases,omitempty"`
// Drops are the lower layers this one leaves out, such as a user's
// keymap that starts from nothing dropping ["defaults"].
Drops []string `msg:"drops,omitempty"`
// Binds are in order: at equal depth and layer the later one wins.
// DecodeMsg reads at most 16,384.
Binds []Bind `msg:"binds,omitempty,limit=16384"`
// Groups label key prefixes for the menus.
Groups []Group `msg:"groups,omitempty"`
// Fences say what happens to the keys their contexts don't bind, for a
// program's modes.
Fences []Fence `msg:"fences,omitempty"`
// Source is where Aliases and Drops were written.
Source string `msg:"source,omitempty"`
}
Doc is one layer's keymap: the tree a program builds from its own files, such as its defaults or the user's config.
func Mount ¶
Mount is a document for a widget another program embeds, such as a pager, put under the node its host shows it at: a copy of d with every bind's, group's and fence's context joined to when, as "(when) && (its own)". One with no context of its own gets when. An empty when changes nothing.
type Env ¶
type Env struct {
// contains filtered or unexported fields
}
Env is the CEL environment a schema declares: the handle every context is compiled in, and every keymap built with. It is safe for concurrent use.
func NewEnv ¶
NewEnv declares a schema's path and layers to CEL. Every node is a bool variable (is it on the path?), every attribute a typed variable under its node ("list.dir"), and focus the innermost node's name. A name that isn't a CEL identifier, a node named with a reserved word (an attribute may be: `cluster.namespace`), a node declared twice, a type it can't read, no layers, a layer named twice and an alias that doesn't read are errors.
func (*Env) Build ¶
Build resolves documents into one keymap, in the order given: the order All and the menus list bindings in. Precedence is the layer's, not the position's.
check is the program's rules for a binding, its sequence resolved and its layer named: whether its action exists and takes its values, or which keys a layer may bind where. An error refuses the keymap and Warn(err) loads it with a warning. nil checks nothing. Every problem carries the Source of what it is about. With any error the Keymap is nil; warnings load.
Example ¶
Build checks each binding with the program's own rules: an error refuses the keymap, and a warning loads it.
package main
import (
"errors"
"fmt"
"github.com/neuroplastio/myelin"
)
func main() {
env, err := myelin.NewEnv(myelin.Schema{
Nodes: []myelin.NodeType{{Name: "list"}},
Layers: []string{"defaults", "user"},
})
if err != nil {
panic(err)
}
actions := map[string]bool{"down": true, "up": true, "scroll": true}
check := func(b myelin.Binding) error {
switch {
case !actions[b.Action]:
return fmt.Errorf("%q is not an action", b.Action)
case b.Action == "scroll":
return myelin.Warn(errors.New(`"scroll" is going away: use "down"`))
}
return nil
}
user := myelin.Doc{Layer: "user", Binds: []myelin.Bind{
{Keys: "J", Action: "scroll", Source: "config:2"},
{Keys: "meta+k", Action: "up", Source: "config:3"},
{Keys: "x", Action: "explode", Source: "config:4"},
{Keys: "y", Action: "up", When: "lsit", Source: "config:5"},
}}
km, problems := env.Build([]myelin.Doc{user}, check)
for _, p := range problems {
kind := "error"
if p.Warning {
kind = "warning"
}
fmt.Printf("%s: %s: %s\n", p.Source, kind, p.Text)
}
fmt.Println("loaded:", km != nil)
}
Output: config:2: warning: "scroll" is going away: use "down" config:3: error: "meta+k" is not a key (meta is no modifier: write super+ for the command or Windows key, alt+ for Emacs's Meta) config:4: error: "explode" is not an action config:5: error: context "lsit": undeclared reference to 'lsit' (a context reads focus, list) loaded: false
func (*Env) CheckSeq ¶
CheckSeq checks a sequence as a Bind writes it, with no keymap: for a program that reports every mistake in its file in one pass, before it builds. Its keys are read as Build reads them. An alias is taken on trust, since a document may name it; Build reports one that none does.
Example ¶
A program checks its file's sequences while it parses it, before it has a keymap. An alias is taken on trust: the keymap may name it.
package main
import (
"fmt"
"github.com/neuroplastio/myelin"
)
func main() {
env, err := myelin.NewEnv(myelin.Schema{Nodes: []myelin.NodeType{{Name: "list"}}, Layers: []string{"defaults"}})
if err != nil {
panic(err)
}
for _, s := range []string{"<leader> f n", "Ctrl+X g", "meta+x"} {
fmt.Printf("%s: %v\n", s, env.CheckSeq(s))
}
}
Output: <leader> f n: <nil> Ctrl+X g: <nil> meta+x: "meta+x" is not a key (meta is no modifier: write super+ for the command or Windows key, alt+ for Emacs's Meta)
func (*Env) Compile ¶
Compile compiles a context. A syntax error, a name the schema does not declare, a type error, what cel-lite doesn't support and an expression that is not a boolean are all errors here, so a keymap that loads never fails on a key. An empty context is nil, which holds everywhere.
type Fence ¶
type Fence struct {
// When is the fence's context; empty holds everywhere.
When string `msg:"when,omitempty"`
// Shadow leaves out the bindings whose contexts are shallower than the
// fence's: only those at least as deep apply, and menus show only them.
Shadow bool `msg:"shadow,omitempty"`
// Swallow sends a key that nothing takes nowhere: Feed and Expire put
// it in Result.Swallowed, not Dropped. A key that types text still
// drops where a node that takes text is deeper than the fence.
Swallow bool `msg:"swallow,omitempty"`
// Source is where the fence was written.
Source string `msg:"source,omitempty"`
}
Fence says, where its context holds, what happens to the keys the bindings at least as deep don't take: a program's mode, such as one that passes every key to a terminal but its own, or a dialog that lets no key past it. On a path the deepest fence that holds decides, then the higher layer, then the later fence, as for bindings. A fence with neither option set gives a deeper context the default back: a palette in a mode.
Example ¶
A program's modes are fences. In the locked mode only the mode's own keys apply, and every other key goes to the terminal; a dialog lets no key past it.
package main
import (
"fmt"
"time"
"github.com/neuroplastio/myelin"
"github.com/neuroplastio/myelin/myelinkey"
)
func main() {
// 1. A window in a mode, its pane, and a dialog over it.
env, err := myelin.NewEnv(myelin.Schema{
Nodes: []myelin.NodeType{
{Name: "window", Attrs: map[string]myelin.Type{"mode": "string"}},
{Name: "pane"}, {Name: "dialog"},
},
Layers: []string{"defaults"},
})
if err != nil {
panic(err)
}
locked := "window.mode == 'locked'"
km, problems := env.Build([]myelin.Doc{{
Layer: "defaults",
Binds: []myelin.Bind{
{Keys: "ctrl+q", Action: "quit"},
{Keys: "ctrl+g", Action: "unlock", When: locked},
{Keys: "escape", Action: "close", When: "dialog"},
},
Fences: []myelin.Fence{
{When: locked, Shadow: true, Source: "locked"},
{When: "dialog", Shadow: true, Swallow: true, Source: "dialog"},
},
}}, nil)
if km == nil {
panic(fmt.Sprint(problems))
}
// 2. Feed keys where the focus is. The Result says what to do.
feed := func(where string, p myelin.Path, k myelinkey.Key) {
_, res := myelin.Feed(km, myelin.Pending{}, k, p, time.Time{})
switch {
case res.Run != nil:
fmt.Println(where, k, "runs", res.Run.Action)
case res.Dropped != nil:
fmt.Println(where, k, "goes to the terminal")
case res.Swallowed != nil:
fmt.Println(where, k, "does nothing")
}
}
window := func(mode string) myelin.Step {
return myelin.Step{Name: "window", Attrs: map[string]any{"mode": mode}}
}
normal := myelin.Path{window("normal"), {Name: "pane"}}
lockedPane := myelin.Path{window("locked"), {Name: "pane"}}
dialog := myelin.Path{window("normal"), {Name: "dialog"}}
feed("normal:", normal, "ctrl+q")
feed("normal:", normal, "x")
feed("locked:", lockedPane, "ctrl+q")
feed("locked:", lockedPane, "ctrl+g")
feed("dialog:", dialog, "ctrl+q")
feed("dialog:", dialog, "escape")
// 3. The fence that decides names the mode, for a status line.
fmt.Println("the mode:", km.Fence(lockedPane).Source)
}
Output: normal: ctrl+q runs quit normal: x goes to the terminal locked: ctrl+q goes to the terminal locked: ctrl+g runs unlock dialog: ctrl+q does nothing dialog: escape runs close the mode: locked
type Group ¶
type Group struct {
// Keys are the prefix as written.
Keys string `msg:"keys"`
// Label is how menus show the group.
Label string `msg:"label"`
// When is the group's context; where several groups of the same keys
// hold, the label follows the bindings' precedence.
When string `msg:"when,omitempty"`
// Source is where the group was written.
Source string `msg:"source,omitempty"`
// Props are the program's own properties, such as a flag that says
// what a level of its menu does. myelin passes them back on menus and
// never reads them; where several groups of the same keys hold, each
// property is the winning one's among those that set it.
Props map[string]string `msg:"props,omitempty"`
}
Group labels a key prefix, for menus: "<leader> f" is "+file".
type Item ¶
type Item struct {
// Key is the key that follows the menu's sequence.
Key myelinkey.Key `msg:"key"`
// Label is the group's or the binding's. A binding with none is the
// program's to word.
Label string `msg:"label,omitempty"`
// Group is set when more keys follow.
Group bool `msg:"group,omitempty"`
// Props are a group's properties, as Menu.Props; a binding's are its
// Binding's.
Props map[string]string `msg:"props,omitempty"`
// Binding is the binding that wins at the key; nil for a group.
Binding *Binding `msg:"binding,omitempty"`
// Enabled is the program's verdict on a leaf; a group is enabled when
// anything under it is.
Enabled bool `msg:"enabled"`
// Items are a group's own, in a tree.
Items []Item `msg:"items,omitempty"`
}
Item is one key that can follow a menu's sequence: a group, or the binding that wins there.
type Keymap ¶
type Keymap struct {
// contains filtered or unexported fields
}
Keymap is a built keymap: every layer's bindings, their aliases resolved, in load order. It is immutable and safe for concurrent use. The Bindings and props its queries return are its own: read them, don't change them.
func (*Keymap) Alias ¶
Alias is the keys an alias stands for, by its name ("leader"): the highest layer's, else the schema's. false when none names it. The keys are the keymap's own: read them, don't change them.
func (*Keymap) All ¶
All is every binding and unbind, its aliases resolved, in load order: what a listing prints.
func (*Keymap) Effective ¶
Effective is every binding that wins its keys on the path, what typing them there runs, in load order. A binding a shorter one runs before, one that does not wait for it, is not there: where the leader key itself closes a dialog, nothing under the leader is effective in the dialog.
func (*Keymap) Explain ¶
Explain is what a sequence does on a path, and why: every binding of the sequence or longer, the bindings of the sequence first, ranked by precedence with the winner first, then the longer ones, then those whose context does not hold.
func (*Keymap) Fence ¶
Fence is the fence that decides on the path, or nil: the deepest that holds, then the higher layer, then the later. Its Source names the mode a program shows. It is the keymap's own, as a Match's binding is: read it, don't change it.
func (*Keymap) KeyFor ¶
KeyFor is the binding a palette's key column shows for action on the path, or nil: of the bindings that run it there, one that runs it as it is before one that gives it values, then the shorter, then the first. It is the keymap's own, as a Match's is: read it, don't change it.
func (*Keymap) KeysFor ¶
KeysFor is every sequence that runs action with exactly args on the path, in load order: what a hint shows for it there. nil args are the action's bindings with no values. The sequences are the keymap's own: read them, don't change them.
func (*Keymap) Menu ¶
Menu is the which-key level after a sequence on the path: one item per key that can follow it, in the order the keymap first binds each, a group (more keys follow) or the binding that wins there. With no keys typed it is every first key; after the leader, the leader's menu.
applies is the program's verdict on a binding: whether it would do anything now. nil means every binding applies. A group is enabled when anything under it is. The program draws the menu, dimming what is not enabled, and words a binding with no label.
A group's props (Group.Props) come back on its item and on the menu after it, decided as its label is, so a program can ask each level of a sequence what it does, such as whether it stays open after a binding runs while the leader's modifier is held.
Example ¶
The leader's menu: one item per key, a group where more keys follow, and what the program says does nothing now dimmed.
package main
import (
"fmt"
"slices"
"github.com/neuroplastio/myelin"
"github.com/neuroplastio/myelin/myelinkey"
)
func main() {
// 1. A leader key, which bindings write as the alias <leader>, a group
// that labels a prefix, and labelled bindings.
env, err := myelin.NewEnv(myelin.Schema{
Nodes: []myelin.NodeType{{Name: "list"}},
Layers: []string{"defaults"},
Aliases: map[string]string{"leader": "space"},
})
if err != nil {
panic(err)
}
km, problems := env.Build([]myelin.Doc{{Layer: "defaults",
Groups: []myelin.Group{{Keys: "<leader> f", Label: "+file"}},
Binds: []myelin.Bind{
{Keys: "<leader> f n", Action: "file.new", Label: "New file"},
{Keys: "<leader> f d", Action: "file.delete", Label: "Delete"},
{Keys: "<leader> q", Action: "quit", Label: "Quit"},
{Keys: "<leader> arrow_left", Action: "back"},
},
}}, nil)
if km == nil {
panic(fmt.Sprint(problems))
}
// 2. Draw the menu after the leader, and after the leader and f. The
// program says what applies here: in a read-only list, not editing.
readOnly := func(b myelin.Binding) bool { return b.Action != "file.new" && b.Action != "file.delete" }
leader, _ := km.Alias("leader")
show := func(m myelin.Menu) {
fmt.Printf("%s (%s)\n", myelinkey.FormatSeq(m.Seq), m.Label)
for _, it := range m.Items {
label := it.Label
if label == "" {
label = it.Binding.Action // the program words a binding with no label
}
fmt.Printf(" %s %s enabled=%v\n", myelinkey.Display(it.Key), label, it.Enabled)
}
}
p := myelin.Path{{Name: "list"}}
show(km.Menu(leader, p, readOnly))
show(km.Menu(slices.Concat(leader, []myelinkey.Key{"f"}), p, readOnly))
}
Output: space () f +file enabled=false q Quit enabled=true ← back enabled=true space f (+file) n New file enabled=false d Delete enabled=false
func (*Keymap) ParseSeq ¶
ParseSeq reads a sequence as a Bind writes it, its aliases resolved, into this keymap's keys: for a sequence the program meets outside the keymap, such as an extension's manifest, to compare with the keymap's.
func (*Keymap) Shadowed ¶
Shadowed is the bindings that a deeper binding of a lower layer shadows on any of the paths given: where both hold, the lower one's context reaches deeper, so the higher one never runs there. A program passes the shapes its path takes, such as a list, a panel and each of them under a dialog. Each problem is a warning at the shadowed binding's Source.
type Match ¶
type Match struct {
// Exact is the binding that wins at the sequence; absent when nothing
// binds it or an unbind won.
Exact *Binding `msg:"exact,omitempty"`
// Prefix is set when a longer binding, at least as deep as Exact,
// continues the sequence: wait for the next key.
Prefix bool `msg:"prefix,omitempty"`
}
Match is what a sequence matches on a path.
type Menu ¶
type Menu struct {
// Seq are the keys typed so far.
Seq []myelinkey.Key `msg:"seq"`
// Label is the group's label: "+file".
Label string `msg:"label,omitempty"`
// Props are the group's properties (Group.Props): each one from the
// winning group of these keys that sets it.
Props map[string]string `msg:"props,omitempty"`
// Items are the keys that can follow, in the order the keymap first
// binds each.
Items []Item `msg:"items"`
}
Menu is a which-key level, or with nested items every level under a sequence.
type NodeType ¶
type NodeType struct {
// Name is the node's name in contexts: a CEL identifier, not "focus".
Name string `msg:"name"`
// Attrs are the node's attributes and their types, which contexts read
// as name.attr.
Attrs map[string]Type `msg:"attrs,omitempty"`
// Text is set on a node that takes typed text, such as a text field:
// while one is on the path, a key that types text is the node's unless
// a binding at least as deep takes it.
Text bool `msg:"text,omitempty"`
}
NodeType is one kind of node on the path.
type Path ¶
type Path []Step
Path is where the focus is: the focused node and the nodes around it, outermost first. The last step is the focus.
type Pending ¶
type Pending struct {
// Seq are the keys typed so far.
Seq []myelinkey.Key `msg:"seq,omitempty"`
// Path is where they were typed: an expiry runs what they finish
// there.
Path Path `msg:"path,omitempty"`
// Deadline is when the buffered keys expire; zero when nothing is
// buffered.
Deadline time.Time `msg:"deadline,omitempty"`
}
Pending is the sequencer's state: the keys typed so far, where, and until when.
func Arm ¶
Arm is an alias's keys as if they had been typed on the path at now, by the alias's name: a tap, or a phone's button where there is no ctrl to press, arms "leader". With no such alias it is the empty state.
type Problem ¶
type Problem struct {
// Source is the Source of what the problem is about.
Source string `msg:"source"`
// Text says what is wrong, and how to write it instead where there is
// one way.
Text string `msg:"text"`
// Warning is set on a problem the keymap loads with.
Warning bool `msg:"warning,omitempty"`
}
Problem is something wrong with a keymap, where it was written.
type Result ¶
type Result struct {
// Run is the binding to run.
Run *Binding `msg:"run,omitempty"`
// Deadline is set when keys are buffered: wake then and call [Expire].
Deadline time.Time `msg:"deadline,omitempty"`
// Dropped are keys that matched nothing: the program types, forwards
// or swallows them.
Dropped []myelinkey.Key `msg:"dropped,omitempty"`
// Swallowed are keys that matched nothing where a fence sends them
// nowhere (Fence.Swallow): the program does nothing with them.
Swallowed []myelinkey.Key `msg:"swallowed,omitempty"`
}
Result is what one key, or an expiry, did. At most one field is set.
type Schema ¶
type Schema struct {
// Nodes are the path's vocabulary: the kinds of node that can be
// focused or hold the focus, outermost first.
Nodes []NodeType `msg:"nodes"`
// Layers are the keymap's layers, lowest first: ["defaults", "user"],
// or ["extensions", "defaults", "user"].
Layers []string `msg:"layers"`
// Aliases name key sequences, which a sequence writes as <name>:
// {"leader": "space"} makes "<leader> f" "space f". A Doc's alias of
// the same name wins. An alias's keys name no alias.
Aliases map[string]string `msg:"aliases,omitempty"`
// StrictKeys reads keys only in their canonical spelling, and refuses
// any other with it: "Ctrl+D" asks for "ctrl+d". Without it keys are
// read as Ghostty reads them (myelinkey.Parse).
StrictKeys bool `msg:"strict_keys,omitempty"`
// ShiftedCapitals reads a capital letter as Shift with its letter, as
// vim does: "G" is "shift+g". Without it a letter's case does not
// matter.
ShiftedCapitals bool `msg:"shifted_capitals,omitempty"`
// Timeout is how long a buffered sequence waits for its next key. Half
// a second suits vim-like sequences; a menu people read wants seconds.
Timeout time.Duration `msg:"timeout"`
}
Schema is what a program declares once, for NewEnv: what can be focused, the keymap's layers, and how its keys are read.
type Step ¶
type Step struct {
// Name is one of the schema's node types.
Name string `msg:"name"`
// Attrs are values of the attributes the node's type declares, by
// name. One a step doesn't give is absent: a context that reads it
// doesn't hold, and [Context.MayHold] treats it as unknown. An
// attribute declared "int" takes an integer of any Go kind.
Attrs map[string]any `msg:"attrs,omitempty"`
}
Step is one node on the path.