Class: Fatty::KeyMap

Inherits:
Object
  • Object
show all
Defined in:
lib/fatty/key_map.rb

Overview

The KeyMap class maintains the mapping between KeyEvent's and action names that can be handled by Terminal. Terminal can delegate the action to any of its controlled components or handle the action itself. Currently, there are two contexts for KeyBinding: :input (for composing a line at the Terminal) and :paging (for controlling the display of output that is longer than the Viewport). The default keybinding context is :input.

Constant Summary collapse

DEFAULT_CONTEXT =
:text

Class Attribute Summary collapse

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initializeKeyMap

Returns a new instance of KeyMap.



143
144
145
146
# File 'lib/fatty/key_map.rb', line 143

def initialize
  @bindings = Hash.new { |h, ctx| h[ctx] = {} }
  self.class.active ||= self
end

Class Attribute Details

.activeObject

Returns the value of attribute active.



113
114
115
# File 'lib/fatty/key_map.rb', line 113

def active
  @active
end

Instance Attribute Details

#bindingsObject (readonly)

Returns the value of attribute bindings.



108
109
110
# File 'lib/fatty/key_map.rb', line 108

def bindings
  @bindings
end

Class Method Details

.register_context(ctx, before: nil, after: nil) ⇒ Object

Register a context (optionally before/after another for precedence/printing)



125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
# File 'lib/fatty/key_map.rb', line 125

def self.register_context(ctx, before: nil, after: nil)
  ctx = ctx.to_s.to_sym
  list = registered_contexts
  return ctx if list.include?(ctx)

  if before
    i = list.index(before.to_s.to_sym) || 0
    list.insert(i, ctx)
  elsif after
    i = list.index(after.to_s.to_sym)
    i ? list.insert(i + 1, ctx) : list << ctx
  else
    list << ctx
  end

  ctx
end

.registered_contextsObject



116
117
118
# File 'lib/fatty/key_map.rb', line 116

def self.registered_contexts
  @registered_contexts ||= [DEFAULT_CONTEXT, :input, :paging]
end

.valid_contextsObject



120
121
122
# File 'lib/fatty/key_map.rb', line 120

def self.valid_contexts
  registered_contexts.map(&:to_s).sort
end

Instance Method Details

#activate!Object



148
149
150
151
# File 'lib/fatty/key_map.rb', line 148

def activate!
  self.class.active = self
  self
end

#bind(context: DEFAULT_CONTEXT, key:, ctrl: false, meta: false, shift: false, action: nil) ⇒ Object

Bind a KeyEvent to an action in the given context.

Raises:

  • (ArgumentError)


154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
# File 'lib/fatty/key_map.rb', line 154

def bind(context: DEFAULT_CONTEXT, key:, ctrl: false, meta: false, shift: false, action: nil)
  bind_str = "#{KeyEvent.key_to_str(key:, ctrl:, meta:, shift:)} -> #{action} in context: #{context}"
  Fatty.info("KeyMap#bind: (#{bind_str})", tag: :keybinding)

  raise ArgumentError, "context must be a Symbol" unless context.is_a?(Symbol)
  raise ArgumentError, "key must be a Symbol" unless key.is_a?(Symbol)
  unless action.is_a?(Symbol) || (action.is_a?(Array) && action.first.is_a?(Symbol))
    raise ArgumentError, "action must be a Symbol or [Symbol, *args]"
  end

  self.class.register_context(context)

  evt = KeyEvent.new(
    key:,
    ctrl: truthy?(ctrl),
    meta: truthy?(meta),
    shift: truthy?(shift),
  )
  gest = KeyGesture.from_event(evt)
  @bindings[context][gest] = action
  self
end

#bind_digits(context: DEFAULT_CONTEXT, meta: nil, ctrl: nil) ⇒ Object

Bind the digits in the given context with either meta, ctrl, neither or both as the required modifier keys.



254
255
256
257
258
259
260
261
262
263
264
265
266
# File 'lib/fatty/key_map.rb', line 254

def bind_digits(context: DEFAULT_CONTEXT, meta: nil, ctrl: nil)
  meta = !!meta
  ctrl = !!ctrl
  (0..9).each do |n|
    bind(
      context: context,
      key: n.to_s.to_sym,
      meta: meta,
      ctrl: ctrl,
      action: [:count_digit, n],
    )
  end
end

#bind_mouse(context: DEFAULT_CONTEXT, button:, ctrl: false, meta: false, shift: false, action: nil) ⇒ Object

Raises:

  • (ArgumentError)


177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
# File 'lib/fatty/key_map.rb', line 177

def bind_mouse(context: DEFAULT_CONTEXT, button:, ctrl: false, meta: false, shift: false, action: nil)
  bind_str = "#{KeyEvent.key_to_str(key: button, ctrl:, meta:, shift:)} -> #{action} in context: #{context}"
  Fatty.info("KeyMap#bind_mouse(#{bind_str})", tag: :keybinding)

  raise ArgumentError, "context must be a Symbol" unless context.is_a?(Symbol)
  raise ArgumentError, "button must be a Symbol" unless button.is_a?(Symbol)
  unless action.is_a?(Symbol) || (action.is_a?(Array) && action.first.is_a?(Symbol))
    raise ArgumentError, "action must be a Symbol or [Symbol, *args]"
  end

  self.class.register_context(context)

  gest = MouseGesture.new(
    button: button,
    ctrl: truthy?(ctrl),
    meta: truthy?(meta),
    shift: truthy?(shift),
  )
  @bindings[context][gest] = action
  self
end

#binding_for(event, contexts: []) ⇒ Object

Return the keybinding for event in contexts without executing. For reporting purposes, especially in Session::KeyTest.



270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
# File 'lib/fatty/key_map.rb', line 270

def binding_for(event, contexts: [])
  return unless event

  ctxs = normalize_contexts(contexts)
  gest = gesture_from_event(event)

  result = nil
  ctxs.each do |ctx|
    map = @bindings.fetch(ctx, nil)
    next unless map

    result = map[gest]
    break if result
  end
  result
end

#bindings_for(event) ⇒ Object

Return all keybindings for event across every context actually loaded without executing.



289
290
291
292
293
294
295
296
297
298
299
300
# File 'lib/fatty/key_map.rb', line 289

def bindings_for(event)
  result = {}
  return result unless event

  gest = gesture_from_event(event)

  @bindings.each do |context, map|
    binding = map[gest]
    result[context] = binding if binding
  end
  result
end

#contextsObject

Return all contexts currently used in this instance



231
232
233
# File 'lib/fatty/key_map.rb', line 231

def contexts
  @bindings.keys
end

#load_user_configObject

Make the bindings from the user's config file, usually at ~/.config/fatty/keybindings.yml



237
238
239
240
241
242
243
244
245
246
247
248
249
250
# File 'lib/fatty/key_map.rb', line 237

def load_user_config
  Fatty.info("Read keybindings from #{Config.user_keybindings_path}", tag: :keybinding)
  data = Fatty::Config.keybindings
  return self unless data.is_a?(Array)

  Fatty.info("User keybindings", config: data, tag: :keybinding)
  data.each_with_index do |entry, idx|
    bind_entry(entry, idx)
  end
  self
rescue Psych::SyntaxError => e
  Fatty.error("KeyMap#load_config syntax error in keybindings: #{e.message}", tag: :keybinding)
  self
end

#resolve(event, contexts: []) ⇒ Object



199
200
201
202
203
204
# File 'lib/fatty/key_map.rb', line 199

def resolve(event, contexts: [])
  result = binding_for(event, contexts: contexts)
  map_str = "#{event} -> #{result.inspect} in contexts: #{contexts} "
  Fatty.debug("KeyMap#resolve: #{map_str}", tag: :keybinding)
  result
end

#resolve_action(event, contexts: []) ⇒ Object

Return [action, args] where args is always an Array (possibly empty).



207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
# File 'lib/fatty/key_map.rb', line 207

def resolve_action(event, contexts: [])
  binding = resolve(event, contexts: contexts)

  if binding
    if binding.is_a?(Array)
      action = binding[0]
      args = binding[1..] || []
      Fatty.debug("KeyMap.resolve_action: action: #{action.inspect}, args: #{args.inspect}", tag: :keybinding)
      [action, args]
    else
      Fatty.debug("KeyMap.resolve_action: action: #{binding.inspect}, args: []", tag: :keybinding)
      [binding, []]
    end
  elsif event&.printable? && (contexts.include?(:text) || contexts.include?(:pager_input))
    # Default: treat printable characters as a self-insert but only in
    # those contexts designed to take text input, not contexts like
    # :paging where keys are meant for control.
    [:self_insert, [event.text]]
  else
    [nil, []]
  end
end