myelin

package module
v0.2.0 Latest Latest
Warning

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

Go to latest
Published: Oct 30, 2026 License: Apache-2.0 Imports: 11 Imported by: 0

README

myelin

Context-bound keymaps and which-key menus for Go programs.

Go Reference CI Go version

myelin decides what a key does in your Go program, given what is focused right now. You declare the things that can be focused (a window, a list, a text field), bind keys in contexts over them, and layer the user's config over your defaults. Sequences like g g, a leader key and which-key menus are built in. It has no UI and reads no files, so it works the same in a TUI, a GUI or anything else that takes keys.

Highlights

  • 🧭 Contexts. A binding holds where its CEL expression holds, over the path to what is focused: list && !field, list.dir.startsWith('/tmp'). The deepest context wins. They run on cel-lite, CEL in Go's standard library alone.
  • 🧱 Layers. Defaults, extensions, the user's config: a higher layer overrides a lower one, and a config can start from nothing.
  • ⌨️ Keys named as Ghostty names them. ctrl+d, shift+g, page_down, g g, <leader> f n. A key is a character or a physical key, and every way of writing it comes out as one name.
  • 🗂️ Which-key menus. One level, or the whole tree, as data that a TUI, a GUI or a phone can draw.
  • ⏱️ Time is a value. now goes in and a deadline comes out. No clock, no goroutines, and tests that need no clock either.
  • 📦 Any config format. myelin reads no files. Decode JSON, KDL, TOML or Lua into plain Go values.
  • 🪶 Headless. No UI toolkit. Key names live in myelinkey, which needs only the standard library and builds with TinyGo.

Why myelin

Editors worked out key binding a long time ago.

  • In VS Code, each rule in keybindings.json has a key, a command and a when clause. Your rules go after the defaults, and the last rule that matches wins, so yours do.
  • In Zed, bindings sit in contexts, in a tree of what's focused: Workspace, Pane, Editor. A binding in a deeper context beats one higher up, and a later one beats an earlier one. Sequences like g g and a which-key popup come built in.

Most other programs, terminal ones especially, hard-code their keys in a switch: no overrides, no menus, and ctrl+d means something else in each. myelin takes the editors' model and makes it a Go library.

VS Code Zed myelin
A binding key, command, when, args a key and an action, in a context Keys, Action, When, Args
Context when over context keys a predicate over the context tree CEL over the path your program declares
Which wins the last rule that matches the deepest context, then the later binding the deepest context, then the layer, then the later binding
Your config appended after the defaults loaded after the defaults a layer above the defaults, which it can drop
Unbinding -command null Unbind, which wins like a binding
Sequences chords: ctrl+k ctrl+w g g, with a timeout g g, <leader> f n, with a deadline you wake at
Menus — a which-key popup which-key levels and the whole tree, as data

From VS Code it takes flat rules, and a user's config layered over the defaults. From Zed it takes contexts over a tree, the rule that the deepest wins, and unbinding that wins the same way. The menus under a leader key come from Spacemacs, and the key names from Ghostty.

Install

go get github.com/neuroplastio/myelin

myelin requires msgp's runtime, cel-lite and its own key names. Keys are named in github.com/neuroplastio/myelin/myelinkey, a module of its own that needs only the standard library. A program that only names keys can require it alone.

Quick start

A file manager: a window, a list of files, and a filter field over the list.

1. Declare what can be focused. Each kind of node can have typed attributes, and a node that takes text keeps the keys that type it.

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. A program usually reads them from its files and the user's; here they are literals.

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. Every mistake is reported at once, 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. The path is the focused node and the nodes around it, outermost first.

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

j does nothing in the filter because the field takes text: there, j types a j.

Bring your own config

myelin has no file format. A program decodes its config, in whatever format it likes, into documents: plain Go values.

Here the defaults are sections in the style of Zed's keymap.json, a context and the keys bound in it:

[
  { "bindings": { "j": "down", "k": "up", "g g": "top", "escape": "back" } },
  { "context": "list", "bindings": { "enter": "open", "d d": "delete" } },
  { "context": "field", "bindings": { "escape": "close-filter" } }
]

and the user's overrides are rules in the style of VS Code's keybindings.json, with an unbind:

[
  { "keys": "enter", "action": "preview", "when": "list.dir.startsWith('/tmp')" },
  { "keys": "d d", "unbind": true, "when": "list" },
  { "keys": "ctrl+d", "action": "delete", "when": "list && !field" }
]

The sections become one bind per key:

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, &sections); 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})
	}
}

The rules decode straight into binds:

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

Then the keymap is built and asked as before:

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

KDL, TOML, YAML or Lua work the same way: myelin only sees the values.

Contexts: the deepest wins

A context's depth is how far along the path it has to look to hold: list holds at the list, field at the field inside it. A context that holds with nothing on the path, such as !preview, is as shallow as none.

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

When several bindings of the same keys hold, the deepest wins, then the higher layer, then the later binding. Keymaps has the whole rule.

Sequences and time

When g runs one thing and g g another, a g has to wait:

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

myelin starts no timer. Feed takes the time each key came and returns a deadline, and the program wakes at it and calls 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]

Which-key menus

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

A menu is data: its items, their labels and whether each applies here. The program draws it however it likes.

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

Keys

A key is named as Ghostty names it: its modifiers in the order super+ctrl+alt+shift+, then a character (g, ?), which follows the keyboard layout, or a physical key (page_down, key_w), which stays in its place. myelin reads keys as Ghostty does, and every way of writing a key comes out as one name, so a keymap, a menu and an error message show it the same way. A program that wants one spelling per key reads strictly, and any other spelling is refused with the one to use.

for _, s := range []string{"ctrl+d", "Shift+Ctrl+A", "cmd+s", "PageDown", "pgdn", "G", "ctrl+key_w", "?"} {
	k, err := myelinkey.Parse(s)
	if err != nil {
		fmt.Println(err)
		continue
	}
	fmt.Printf("%s is %s\n", s, k)
}
// Output:
// ctrl+d is ctrl+d
// Shift+Ctrl+A is ctrl+shift+a
// cmd+s is super+s
// PageDown is page_down
// pgdn is page_down
// G is g
// ctrl+key_w is ctrl+key_w
// ? is ?

Documentation

Status

Early: the API can still change.

License

Apache-2.0.

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:

  1. the one whose context reaches deepest into the path (Context.Depth);
  2. at the same depth, the one in the higher layer;
  3. 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, &sections); 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

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

func Expire

func Expire(km *Keymap, st Pending, now time.Time) (Pending, Result)

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

func Feed(km *Keymap, st Pending, k myelinkey.Key, p Path, now time.Time) (Pending, Result)

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

func FormatPath(p Path) string

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.

func Warn

func Warn(err error) error

Warn makes err a warning: Build loads the bind and reports it.

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.

func (*Bind) DecodeMsg

func (z *Bind) DecodeMsg(dc *msgp.Reader) (err error)

DecodeMsg implements msgp.Decodable

func (*Bind) EncodeMsg

func (z *Bind) EncodeMsg(en *msgp.Writer) (err error)

EncodeMsg implements msgp.Encodable

func (*Bind) Msgsize

func (z *Bind) Msgsize() (s int)

Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message

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.

func (*Binding) DecodeMsg

func (z *Binding) DecodeMsg(dc *msgp.Reader) (err error)

DecodeMsg implements msgp.Decodable

func (*Binding) EncodeMsg

func (z *Binding) EncodeMsg(en *msgp.Writer) (err error)

EncodeMsg implements msgp.Encodable

func (*Binding) Msgsize

func (z *Binding) Msgsize() (s int)

Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message

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.

func (*Candidate) DecodeMsg

func (z *Candidate) DecodeMsg(dc *msgp.Reader) (err error)

DecodeMsg implements msgp.Decodable

func (*Candidate) EncodeMsg

func (z *Candidate) EncodeMsg(en *msgp.Writer) (err error)

EncodeMsg implements msgp.Encodable

func (*Candidate) Msgsize

func (z *Candidate) Msgsize() (s int)

Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message

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

func (c *Context) Depth(p Path) int

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

func (c *Context) Holds(p Path) bool

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

func (c *Context) MayDepth(p Path) int

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.

func (*Context) MayHold

func (c *Context) MayHold(p Path) bool

MayHold reports whether the context can hold on the path when the attributes its steps do not give are unknown: it holds, or its answer depends on them. Asked of a path of nodes with no attributes, it says whether a binding can hold wherever those nodes have the focus.

func (*Context) String

func (c *Context) String() string

String is the context as written; "" for none.

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

func Mount(d Doc, when string) Doc

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.

func (*Doc) DecodeMsg

func (z *Doc) DecodeMsg(dc *msgp.Reader) (err error)

DecodeMsg implements msgp.Decodable

func (*Doc) EncodeMsg

func (z *Doc) EncodeMsg(en *msgp.Writer) (err error)

EncodeMsg implements msgp.Encodable

func (*Doc) Msgsize

func (z *Doc) Msgsize() (s int)

Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message

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

func NewEnv(s Schema) (*Env, error)

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

func (e *Env) Build(docs []Doc, check func(Binding) error) (*Keymap, []Problem)

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

func (e *Env) CheckSeq(s string) error

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

func (e *Env) Compile(when string) (*Context, error)

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

func (*Fence) DecodeMsg

func (z *Fence) DecodeMsg(dc *msgp.Reader) (err error)

DecodeMsg implements msgp.Decodable

func (*Fence) EncodeMsg

func (z *Fence) EncodeMsg(en *msgp.Writer) (err error)

EncodeMsg implements msgp.Encodable

func (*Fence) Msgsize

func (z *Fence) Msgsize() (s int)

Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message

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".

func (*Group) DecodeMsg

func (z *Group) DecodeMsg(dc *msgp.Reader) (err error)

DecodeMsg implements msgp.Decodable

func (*Group) EncodeMsg

func (z *Group) EncodeMsg(en *msgp.Writer) (err error)

EncodeMsg implements msgp.Encodable

func (*Group) Msgsize

func (z *Group) Msgsize() (s int)

Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message

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.

func (*Item) DecodeMsg

func (z *Item) DecodeMsg(dc *msgp.Reader) (err error)

DecodeMsg implements msgp.Decodable

func (*Item) EncodeMsg

func (z *Item) EncodeMsg(en *msgp.Writer) (err error)

EncodeMsg implements msgp.Encodable

func (*Item) Msgsize

func (z *Item) Msgsize() (s int)

Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message

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

func (km *Keymap) Alias(name string) ([]myelinkey.Key, bool)

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

func (km *Keymap) All() []Binding

All is every binding and unbind, its aliases resolved, in load order: what a listing prints.

func (*Keymap) Effective

func (km *Keymap) Effective(p Path) []Binding

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

func (km *Keymap) Explain(seq []myelinkey.Key, p Path) []Candidate

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

func (km *Keymap) Fence(p Path) *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

func (km *Keymap) KeyFor(action string, p Path) *Binding

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

func (km *Keymap) KeysFor(action string, args []string, p Path) [][]myelinkey.Key

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

func (km *Keymap) Match(seq []myelinkey.Key, p Path) Match

Match looks up a sequence on a path.

func (*Keymap) Menu

func (km *Keymap) Menu(seq []myelinkey.Key, p Path, applies func(Binding) bool) 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

func (km *Keymap) ParseSeq(s string) ([]myelinkey.Key, error)

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

func (km *Keymap) Shadowed(paths ...Path) []Problem

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.

func (*Keymap) Starts

func (km *Keymap) Starts(k myelinkey.Key) bool

Starts reports whether any binding, in any context, begins with k.

func (*Keymap) Tree

func (km *Keymap) Tree(seq []myelinkey.Key, p Path, applies func(Binding) bool) Menu

Tree is Menu with every level under the sequence: each group's items nested in it. After the leader it is a card of every binding under the leader.

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.

func (*Match) DecodeMsg

func (z *Match) DecodeMsg(dc *msgp.Reader) (err error)

DecodeMsg implements msgp.Decodable

func (*Match) EncodeMsg

func (z *Match) EncodeMsg(en *msgp.Writer) (err error)

EncodeMsg implements msgp.Encodable

func (*Match) Msgsize

func (z *Match) Msgsize() (s int)

Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message

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.

func (z *Menu) DecodeMsg(dc *msgp.Reader) (err error)

DecodeMsg implements msgp.Decodable

func (z *Menu) EncodeMsg(en *msgp.Writer) (err error)

EncodeMsg implements msgp.Encodable

func (z *Menu) Msgsize() (s int)

Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message

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.

func (*NodeType) DecodeMsg

func (z *NodeType) DecodeMsg(dc *msgp.Reader) (err error)

DecodeMsg implements msgp.Decodable

func (*NodeType) EncodeMsg

func (z *NodeType) EncodeMsg(en *msgp.Writer) (err error)

EncodeMsg implements msgp.Encodable

func (*NodeType) Msgsize

func (z *NodeType) Msgsize() (s int)

Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message

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.

func (*Path) DecodeMsg

func (z *Path) DecodeMsg(dc *msgp.Reader) (err error)

DecodeMsg implements msgp.Decodable

func (Path) EncodeMsg

func (z Path) EncodeMsg(en *msgp.Writer) (err error)

EncodeMsg implements msgp.Encodable

func (Path) Msgsize

func (z Path) Msgsize() (s int)

Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message

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

func Arm(km *Keymap, alias string, p Path, now time.Time) Pending

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.

func (*Pending) DecodeMsg

func (z *Pending) DecodeMsg(dc *msgp.Reader) (err error)

DecodeMsg implements msgp.Decodable

func (*Pending) EncodeMsg

func (z *Pending) EncodeMsg(en *msgp.Writer) (err error)

EncodeMsg implements msgp.Encodable

func (*Pending) Msgsize

func (z *Pending) Msgsize() (s int)

Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message

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.

func (*Problem) DecodeMsg

func (z *Problem) DecodeMsg(dc *msgp.Reader) (err error)

DecodeMsg implements msgp.Decodable

func (Problem) EncodeMsg

func (z Problem) EncodeMsg(en *msgp.Writer) (err error)

EncodeMsg implements msgp.Encodable

func (Problem) Msgsize

func (z Problem) Msgsize() (s int)

Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message

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.

func (*Result) DecodeMsg

func (z *Result) DecodeMsg(dc *msgp.Reader) (err error)

DecodeMsg implements msgp.Decodable

func (*Result) EncodeMsg

func (z *Result) EncodeMsg(en *msgp.Writer) (err error)

EncodeMsg implements msgp.Encodable

func (*Result) Msgsize

func (z *Result) Msgsize() (s int)

Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message

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.

func (*Schema) DecodeMsg

func (z *Schema) DecodeMsg(dc *msgp.Reader) (err error)

DecodeMsg implements msgp.Decodable

func (*Schema) EncodeMsg

func (z *Schema) EncodeMsg(en *msgp.Writer) (err error)

EncodeMsg implements msgp.Encodable

func (*Schema) Msgsize

func (z *Schema) Msgsize() (s int)

Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message

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.

func (*Step) DecodeMsg

func (z *Step) DecodeMsg(dc *msgp.Reader) (err error)

DecodeMsg implements msgp.Decodable

func (*Step) EncodeMsg

func (z *Step) EncodeMsg(en *msgp.Writer) (err error)

EncodeMsg implements msgp.Encodable

func (*Step) Msgsize

func (z *Step) Msgsize() (s int)

Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message

type Type

type Type string

Type is an attribute's CEL type: "string", "bool", "int", "uint", "double", "bytes", "dyn", "list<T>" or "map<K,V>", such as "map<string,string>".

func (*Type) DecodeMsg

func (z *Type) DecodeMsg(dc *msgp.Reader) (err error)

DecodeMsg implements msgp.Decodable

func (Type) EncodeMsg

func (z Type) EncodeMsg(en *msgp.Writer) (err error)

EncodeMsg implements msgp.Encodable

func (Type) Msgsize

func (z Type) Msgsize() (s int)

Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message

Directories

Path Synopsis
key module
myelinkey module
myelintea module

Jump to

Keyboard shortcuts

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