keychord

package module
v0.1.8 Latest Latest
Warning

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

Go to latest
Published: Sep 26, 2026 License: MIT Imports: 8 Imported by: 2

README

keychord

keychord is a flexible key binding management library for CUI / TUI applications, built on top of github.com/gdamore/tcell/v3.

  • Supports single keys and hierarchical key sequences (key chords)
  • Supports Ctrl / Alt / Shift / Meta modifiers
  • Prefix-key style bindings
  • Explicit state transitions (DFA-based dispatcher)
  • Designed for modal UI architectures
  • Directly processes tcell.EventKey

Locale-aware Input Strategy

Key chord interpretation is inherently locale and IME dependent.

For example, in Japanese IME environments, vowel keys may be immediately converted into preedit characters (i → い) before key chord matching occurs. Other languages such as Korean, Chinese, and Vietnamese have different composition behaviors that can also affect key sequence interpretation.

Because of this, the keychord package is designed with the assumption that key interpretation strategies should be replaceable per locale / input method.

keychord package は locale 別に交換されるべきです。

Applications are encouraged to provide locale-aware key normalization or input strategies depending on the user's keyboard and IME behavior.


Features

✅ Based on tcell/v3

This library uses gdamore/tcell (github.com/gdamore/tcell/v3) to handle:

  • Terminal-dependent key input
  • Modifier keys (Ctrl / Alt / Shift / Meta)
  • Special keys (Esc, Enter, Delete, etc.)

✅ Deterministic Key Dispatcher (DFA)

The dispatcher is implemented as a deterministic finite automaton (DFA).

It maintains one of two internal states:

  • Root
  • Prefix(n)

State transitions are explicit and predictable.

Returned transitions:

  • DispatchNotFound
  • DispatchPrefix
  • DispatchExecuted
  • DispatchInvalidAfterPrefix

This allows applications to precisely distinguish:

  • No binding exists
  • Waiting for next key
  • Action executed
  • Invalid continuation after a prefix

✅ Hierarchical Key Bindings (Key Chords)

You can define key sequences such as:

Ctrl+X Ctrl+S
g g
Ctrl+C Esc

When a prefix key is entered:

  • Internal state transitions to Prefix(n)
  • Dispatcher waits for the next key
  • Candidate keys can be queried
  • Invalid continuation returns DispatchInvalidAfterPrefix

Example:

C-x   → DispatchPrefix
C-x z → DispatchInvalidAfterPrefix

✅ Designed for Modal Applications

RootNode maintains internal state (current), making it easy to support modal application designs such as:

  • Normal / Insert / Visual
  • Command / Search
  • Application-specific operation modes

Prepare a separate RootNode per mode:

normalMode := keychord.NewRootNode()
insertMode := keychord.NewRootNode()

Switching modes simply means switching the active RootNode.

No global state required.


✅ Human-friendly Key Notation

Key bindings are defined using simple string notation:

"Ctrl+X"
"Alt+Enter"
"Esc"
"g"
"Ctrl+A"

These strings are decoded internally into:

  • tcell.Key
  • tcell.ModMask

Supported:

  • Special keys (from tcell)
  • ASCII characters
  • Single Unicode characters
  • Ctrl+A through Ctrl+Z
  • Control characters (^@, ^[, ^, ^], ^^, ^_)

Installation

go get github.com/ge-editor/keychord

Usage

Defining Key Bindings
root := keychord.NewRootNode()

root.Bind("Ctrl+X", "Ctrl+S").Do(func() {
    saveFile()
})

root.Bind("g", "g").Do(func() {
    goToTop()
})

Non-blocking bindings:

root.Bind("Ctrl+L").DoAlso(func() {
    refreshScreen()
})

Dispatching Events

Pass tcell.EventKey directly to the dispatcher.

status, result := root.Dispatch(ev)

switch result {
case keychord.DispatchExecuted:
    // action executed

case keychord.DispatchPrefix:
    // waiting for next key in a sequence

case keychord.DispatchInvalidAfterPrefix:
    // invalid continuation after prefix

case keychord.DispatchNotFound:
    // no matching binding
}

status contains the currently entered key sequence (e.g. C-x), suitable for:

  • Status bars
  • Key hints
  • Debug display

State Transition Diagram

            +-------------------+
            |       Root        |
            +-------------------+
             |   |         |
     NotFound|   |Action   |Prefix
             |   |         v
             |   |    +----------------+
             |   |    |   Prefix(n)    |
             |   |    +----------------+
             |   |      |   |        |
             |   |      |   |Action  |Prefix
             |   |      |   |        v
             |   |      |   |   Prefix(n')
             |   |      |   |
             |   |      |Invalid
             |   |      v
             |   |     Root
             v   v
            Root Root

Formal definition:

δ : S × Σ → S × O

Where:

S = { Root, Prefix(n) }
O = {
  DispatchNotFound,
  DispatchPrefix,
  DispatchExecuted,
  DispatchInvalidAfterPrefix
}

The dispatcher is deterministic.


Listing Candidate Keys
candidates := root.Candidates()

Useful for:

  • Contextual help
  • which-key–style UI
  • Interactive key discovery

Logging

Internal logging uses github.com/ge-editor/gelog.

  • Logging enabled in debug builds
  • No-op in release builds
  • Each dispatch cycle has a unique ID for traceability

Design Principles

  • Deterministic state machine
  • Explicit reset semantics
  • No hidden global state
  • Prefix failure is distinguishable
  • Safe for modal editor architectures

License

This package is licensed under the MIT License.

Documentation

Index

Constants

This section is empty.

Variables

View Source
var (
	ErrInvalidKeyEvent = errors.New("error invalid key event")
)

Functions

func NormalizeLocaleASCII added in v0.1.4

func NormalizeLocaleASCII(r rune) rune

Locale-specific heuristic for Japanese IME. Some IMEs immediately convert vowels into kana during key chord input:

Ctrl+X i → Ctrl+X い

To avoid chord mismatch, normalize five vowels and 'n' back to ASCII.

func ResetAllRootNodes

func ResetAllRootNodes()

Types

type DecodedKey

type DecodedKey struct {
	Key tcell.Key     // 特殊キー
	Str string        // printable 文字
	Mod tcell.ModMask // ctrl / alt / shift
}

func Decode

func Decode(s string) (DecodedKey, error)

func (DecodedKey) KeySpec

func (d DecodedKey) KeySpec() KeySpec

KeySpec を返すユーティリティ

type KeyAction

type KeyAction func() KeyDispatchTransition

type KeyDispatchTransition

type KeyDispatchTransition int
const (
	DispatchNotFound           KeyDispatchTransition = iota // 該当するキーがない
	DispatchPrefix                                          // プレフィックスキーで次の入力を待つ
	DispatchExecuted                                        // アクション実行済み
	DispatchInvalidAfterPrefix                              // プレフィックス後に無効なキー
)

func (KeyDispatchTransition) String

func (r KeyDispatchTransition) String() string

type KeyNode

type KeyNode struct {
	Next   map[KeySpec]*KeyNode
	Action KeyAction
	KeyStr string
}

type KeySpec

type KeySpec struct {
	Key tcell.Key
	Str string
	Mod tcell.ModMask
}

type RootNode

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

func NewRootNode

func NewRootNode() *RootNode

NewRootNode

func (*RootNode) Bind

func (r *RootNode) Bind(keys ...string) *binder

Bind 単一キーまたは階層キー列にアクションを登録

func (*RootNode) BindKeyEvent

func (r *RootNode) BindKeyEvent(f func(tcell.EventKey) (string, KeyDispatchTransition))

func (*RootNode) Candidates

func (r *RootNode) Candidates() []string

func (*RootNode) Dispatch

func (r *RootNode) Dispatch(ev tcell.EventKey) (string, KeyDispatchTransition)

Dispatch 入力イベントに応じてキーバインドを実行

func (*RootNode) Reset

func (r *RootNode) Reset()

Reset 現在のノードを初期化

Jump to

Keyboard shortcuts

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