Class: Legion::TTY::KeybindingManager

Inherits:
Object
  • Object
show all
Includes:
Logging::Helper
Defined in:
lib/legion/tty/keybinding_manager.rb

Overview

KeybindingManager — context-aware keybinding system with chord support and user customization.

Named contexts:

:global, :chat, :dashboard, :extensions, :config, :command_palette, :session_picker, :history

Chord sequences: two-key combos stored as “key1+key2” strings. Set @pending_chord between key presses; the second key resolves the chord action.

User overrides loaded from ~/.legionio/keybindings.json at boot.

Constant Summary collapse

CONTEXTS =
%i[global chat dashboard extensions config command_palette session_picker history].freeze
OVERRIDES_PATH =
File.expand_path('~/.legionio/keybindings.json')
DEFAULT_BINDINGS =
{
  ctrl_d: { contexts: %i[global chat], action: :toggle_dashboard, description: 'Toggle dashboard (Ctrl+D)' },
  ctrl_k: { contexts: %i[global chat], action: :command_palette, description: 'Open command palette (Ctrl+K)' },
  ctrl_s: { contexts: %i[global chat], action: :session_picker, description: 'Open session picker (Ctrl+S)' },
  ctrl_l: { contexts: %i[global chat dashboard], action: :refresh, description: 'Refresh screen (Ctrl+L)' },
  escape: { contexts: CONTEXTS, action: :back, description: 'Go back / dismiss overlay (Escape)' },
  tab: { contexts: %i[chat], action: :autocomplete, description: 'Auto-complete (Tab)' },
  ctrl_c: { contexts: CONTEXTS, action: :interrupt, description: 'Interrupt / quit (Ctrl+C)' }
}.freeze

Instance Method Summary collapse

Constructor Details

#initialize(overrides_path: OVERRIDES_PATH) ⇒ KeybindingManager

Returns a new instance of KeybindingManager.



35
36
37
38
39
40
41
# File 'lib/legion/tty/keybinding_manager.rb', line 35

def initialize(overrides_path: OVERRIDES_PATH)
  @overrides_path = overrides_path
  @bindings = {}
  @pending_chord = nil
  load_defaults
  load_user_overrides
end

Instance Method Details

#bind(key, action:, contexts: [:global], description: '') ⇒ Object

Register or override a single binding.

Parameters:

  • key (Symbol, String)

    normalized key

  • action (Symbol)

    action name

  • contexts (Array<Symbol>) (defaults to: [:global])

    applicable contexts (:global means all)

  • description (String) (defaults to: '')


82
83
84
# File 'lib/legion/tty/keybinding_manager.rb', line 82

def bind(key, action:, contexts: [:global], description: '')
  @bindings[key.to_s.to_sym] = { contexts: contexts, action: action, description: description }
end

#cancel_chordObject

Cancel any in-progress chord sequence.



68
69
70
# File 'lib/legion/tty/keybinding_manager.rb', line 68

def cancel_chord
  @pending_chord = nil
end

#chord_pending?Boolean

Whether a chord is currently waiting for its second key.

Returns:

  • (Boolean)


73
74
75
# File 'lib/legion/tty/keybinding_manager.rb', line 73

def chord_pending?
  !@pending_chord.nil?
end

#listObject

All registered bindings as an array of hashes.



92
93
94
95
96
# File 'lib/legion/tty/keybinding_manager.rb', line 92

def list
  @bindings.map do |key, b|
    { key: key, action: b[:action], contexts: b[:contexts], description: b[:description] }
  end
end

#load_defaultsObject

Reload default bindings (resets user overrides).



99
100
101
102
103
104
# File 'lib/legion/tty/keybinding_manager.rb', line 99

def load_defaults
  @bindings = {}
  DEFAULT_BINDINGS.each do |key, binding|
    @bindings[key] = binding.dup
  end
end

#load_user_overridesObject

Load user overrides from ~/.legionio/keybindings.json. File format: { “ctrl_d”: { “action”: “toggle_dashboard”, “contexts”: [“global”], “description”: “…” } }



108
109
110
111
112
113
114
115
# File 'lib/legion/tty/keybinding_manager.rb', line 108

def load_user_overrides
  return unless File.exist?(@overrides_path)

  raw = Legion::JSON.parse(File.read(@overrides_path), symbolize_names: true)
  raw.each { |key, cfg| apply_override(key, cfg) }
rescue Legion::JSON::ParseError => e
  log.warn { "keybindings load failed: #{e.message}" }
end

#resolve(key, active_contexts: [:global]) ⇒ Symbol?

Resolve a key press given the currently active contexts.

Parameters:

  • key (Symbol, String)

    normalized key (e.g. :ctrl_d, :escape)

  • active_contexts (Array<Symbol>) (defaults to: [:global])

    contexts currently in scope (most specific last)

Returns:

  • (Symbol, nil)

    action name, or nil if no binding matches



48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
# File 'lib/legion/tty/keybinding_manager.rb', line 48

def resolve(key, active_contexts: [:global])
  key_sym = key.to_s.to_sym

  # Chord resolution: if a chord is pending, try to complete it
  if @pending_chord
    chord = :"#{@pending_chord}+#{key_sym}"
    @pending_chord = nil
    return action_for(chord, active_contexts)
  end

  # Check if this key starts a chord
  if chord_starter?(key_sym)
    @pending_chord = key_sym
    return :chord_pending
  end

  action_for(key_sym, active_contexts)
end

#unbind(key) ⇒ Object

Remove a binding.



87
88
89
# File 'lib/legion/tty/keybinding_manager.rb', line 87

def unbind(key)
  @bindings.delete(key.to_s.to_sym)
end