README
¶
key-sequence-processor
WordStar/JOE-style key sequences and chords for Go: multi-key sequences, precedence levels, wildcards, aliases and context help topics. If you use this, please support me on ko-fi: https://ko-fi.com/jeffday
Pure Go, standard library only.
Features
- Multi-key sequences (
^K X,^Q F) with prefix disambiguation - Precedence levels (
capture/override) so a hosted layer can claim keys - Per-level wildcards, and specific bindings that shadow them
- A binding can decline a key and drop resolution to the level below
- Key aliases (
^H=back,^M=return, …) - Context-sensitive help topics per sequence prefix
- Completion listing for a partially typed sequence
- Optional macOS Option-character layer for unbound Meta keys, on any platform
- No opinion about your key names, your commands, or what an unbound key does
Installation
go get github.com/phroun/key-sequence-processor/keyseq
Quick Start
package main
import (
"fmt"
"github.com/phroun/key-sequence-processor/keyseq"
)
func main() {
p := keyseq.NewProcessor(func(key, command string) bool {
fmt.Printf("%s -> %s\n", key, command)
return true // handled; a clean false declines the key
})
p.SetMappings(map[string]string{
"^K X": "save_and_exit",
"^K B": "block_begin",
"^Q F": "find",
"^K help": "\"Block commands\"", // shown while ^K is pending
})
// An unbound key is the application's business, not the processor's.
p.SetDefaultHandler(func(key string) string {
if len([]rune(key)) == 1 {
return "insert '" + key + "'"
}
return ""
})
p.ProcessKey("^K") // opens the sequence, dispatches nothing
p.ProcessKey("X") // -> save_and_exit
p.ProcessKey("q") // -> insert 'q'
}
Key names
The processor never parses a keyboard; it resolves whatever names you feed it.
A key event is a string, and a sequence is those strings joined by spaces —
so "^K X" is Ctrl-K then X, and a name may not itself contain a space (spell
the spacebar space, and "^B space" is a chord ending on it).
Names are yours to choose. If you decode a terminal with
direct-key-handler, its
Options.KeyNames lets you emit exactly the vocabulary you bind against, so
nothing has to be translated in between.
Precedence levels
Prefix a mapping with capture (+1) or override (+2) to raise its level;
they compound, so a layer can always outbid another by writing one more word.
p.SetMappings(map[string]string{
"up": "cursor_up", // level 0: the base keymap
"capture *": "terminal_key", // level 1: a hosted terminal claims everything
"capture ^C": "false", // ...except ^C, which it hands back
})
Resolution runs from the highest level down:
- The longest live sequence wins across all levels — nothing single-key outranks a chord in progress.
- Among equal-length matches, the highest level goes first.
- Within a level, a specific binding shadows that level's wildcard.
- A candidate that declines (a clean
falsefrom the executor) drops one level and re-runs. - Exhausted candidates fall to the default handler.
Declining a key
CommandExecutor returns whether it handled the key. Only a clean false
means "not mine" — success, an async suspension, or an error all hold the key,
so a command that merely went wrong can never volunteer its keystroke to a
layer below it.
Resolution-only mode
Pass a nil executor and nothing is dispatched: ProcessKey reports the
top-precedence command in ProcessResult.Command instead. Useful for showing a
user what a key would do.
The Option-character layer
A terminal forces an either/or: Option is Meta, or Option types characters —
not both. SetMacOptionInsert(true) restores the missing half from the binding
side. An M- key no binding claimed reports the character Option composes, so
bindings take the combos they name and every other combo still types:
p.SetMacOptionInsert(true)
p.SetDefaultHandler(func(key string) string {
if ch, ok := p.MacOptionChar(key); ok { // "M-d" -> "∂"
return "insert '" + ch + "'"
}
return ""
})
It is a user setting, never a property of the host OS — nothing here reads
runtime.GOOS. Someone typing on a Mac keyboard through an SSH session to a
Linux box wants this layer, and the far end cannot see their keyboard. It also
gives a Linux or Windows terminal mac-style Option typing it never had.
The inverse — decoding the composed character back into an M- name so it can
be bound at all — is the input decoder's half, and
direct-key-handler carries it
as DecodeMacOSOption. The two are useful independently: with the decoder on,
M-x is bindable without writing ≈; with only this layer, a terminal already
in Meta mode can still type Option characters.
Help topics
Map the reserved pseudo-key help under a prefix and HelpTopic finds the
most specific one for the sequence in progress — "^K B help", then
"^K help", then "help" — which is how an on-screen helper narrates a chord
as the user types it.
License
MIT — see LICENSE.
Change Log
0.1.0
- Extracted from the mew editor, where this
ran as
internal/keys, and relicensed MIT. - Default handling for unbound keys became the application's, through
SetDefaultHandler, rather than a table of one editor's command names. SequenceProcessoris nowkeyseq.Processor.- The Option-character layer stayed, and no longer consults the host OS:
SetMacOptionInsert/MacOptionCharreport the character, the application spells the command.
Directories
¶
| Path | Synopsis |
|---|---|
|
Package keyseq resolves key events into application commands: multi-key sequences (WordStar/JOE-style chords such as ^K X), precedence levels, per-level wildcards, key aliases, and context-sensitive help topics.
|
Package keyseq resolves key events into application commands: multi-key sequences (WordStar/JOE-style chords such as ^K X), precedence levels, per-level wildcards, key aliases, and context-sensitive help topics. |