keychord

package module
v0.1.1 Latest Latest
Warning

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

Go to latest
Published: May 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

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

MIT License


Documentation

Index

Constants

This section is empty.

Variables

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

Functions

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