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.



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

def bind(context: DEFAULT_CONTEXT, key:, ctrl: false, meta: false, shift: false, action: nil)
  context = normalize_binding_symbol(context, field: :context)
  key = normalize_binding_symbol(key, field: :key)
  action = normalize_action(action)

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

  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.



250
251
252
253
254
255
256
257
258
259
260
261
262
# File 'lib/fatty/key_map.rb', line 250

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



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

def bind_mouse(context: DEFAULT_CONTEXT, button:, ctrl: false, meta: false, shift: false, action: nil)
  context = normalize_binding_symbol(context, field: :context)
  button = normalize_binding_symbol(button, field: :button)
  action = normalize_action(action)

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

  self.class.register_context(context)

  gest = MouseGesture.new(
    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.



266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
# File 'lib/fatty/key_map.rb', line 266

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.



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

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
  if event.printable?
    self_insert_contexts.each do |context|
      result[context] ||= [:self_insert, event.text]
    end
  end
  result
end

#contextsObject

Return all contexts currently used in this instance



227
228
229
# File 'lib/fatty/key_map.rb', line 227

def contexts
  @bindings.keys
end

#load_user_configObject

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



233
234
235
236
237
238
239
240
241
242
243
244
245
246
# File 'lib/fatty/key_map.rb', line 233

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



195
196
197
198
199
200
# File 'lib/fatty/key_map.rb', line 195

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).



203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
# File 'lib/fatty/key_map.rb', line 203

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