howl.bindings

after_each ->
  while #bindings.keymaps > 1
    bindings.pop!

  bindings.cancel_capture

push(map, options = {})

pushes <map> to the keymap stack at .keymaps

map = {}
bindings.push map
assert.equals map, bindings.keymaps[#bindings.keymaps]

pop()

pops the top-most keymap of the stack at .keymaps

stack_before = moon.copy bindings.keymaps
bindings.push {}
bindings.pop!
assert.same stack_before, bindings.keymaps

remove(map)

removes the specified map from the keymap stack

stack = moon.copy bindings.keymaps
m1 = {}
m2 = {}
bindings.push m1
bindings.push m2
bindings.remove m1
append stack, m2
assert.same stack, bindings.keymaps

translate_key(event)

adds special case translations for certain common keys

for_keynames = {
  kp_up: 'up'
  kp_down: 'down'
  kp_left: 'left'
  kp_right: 'right'
  kp_page_up: 'page_up'
  kp_page_down: 'page_down'
  iso_left_tab: 'tab' -- shifts are automatically prepended
  return: 'enter'
  altL: 'alt'
  altR: 'alt'
  shiftL: 'shift'
  shiftR: 'shift'
  ctrlL: 'ctrl'
  ctrlR: 'ctrl'
 }

for name, alternative in pairs for_keynames
  translations = bindings.translate_key key_code: 123, key_name: name
  assert.includes translations, alternative

substitutes certain key names to prevent ambiguity

for_keynames = {
  alt_l: 'altL'
  alt_r: 'altR'
  shift_l: 'shiftL'
  shift_r: 'shiftR'
  control_l: 'ctrlL'
  control_r: 'ctrlR'
}

for name, substitution in pairs for_keynames
  translations = bindings.translate_key key_code: 123, key_name: name
  assert.includes translations, substitution
  assert.not_includes translations, name

(for ordinary characters)

returns a table with the character, key name and key code string

tr = bindings.translate_key character: 'A', key_name: 'a', key_code: 65
assert.same tr, { 'A', 'a', '65' }

skips the translation for key name if it is the same as for character

tr = bindings.translate_key character: 'a', key_name: 'a', key_code: 65
assert.same tr, { 'a', '65' }

(when character is missing)

returns a table with key name and key code string

tr = bindings.translate_key key_name: 'down', key_code: 123
assert.same tr, { 'down', '123' }

(when only the code is available)

returns a table with the key code string

tr = bindings.translate_key key_code: 123
assert.same tr, { '123' }

(with modifiers)

prepends a modifier string representation to all translations for ctrl, meta and alt

tr = bindings.translate_key
  character: 'A', key_name: 'a', key_code: 123,
  control: true, alt: true, meta: true
mods = 'ctrl_meta_alt_'
assert.same tr, { mods .. 'A', mods .. 'a', mods .. '123' }

emits the shift modifier if the character is known

tr = bindings.translate_key
  character: 'A', key_name: 'a', key_code: 123,
  control: true, shift: true
assert.same tr, { 'ctrl_A', 'ctrl_shift_a', 'ctrl_shift_123' }

(when lock is on)

downcases the character for translations

tr = bindings.translate_key
  character: 'A', key_name: 'a', key_code: 123, lock: true
assert.same { 'a', '123' }, tr

(.. and shift is held)

upcases the character for translations

tr = bindings.translate_key
  character: 'A', key_name: 'a', key_code: 123, lock: true, shift: true

assert.same { 'A', 'shift_a', 'shift_123' }, tr

process(event, source, extra_keymaps, ..)

(when firing the key-press signal)

passes the event, translations, source and parameters

event = character: 'A', key_name: 'a', key_code: 65

with_signal_handler 'key-press', nil, (handler) ->
  pcall bindings.process, event, 'editor', {}, 'yowser'
  assert.spy(handler).was.called_with {
    :event
    source: 'editor'
    translations: { 'A', 'a', '65' }
    parameters: { 'yowser' }
  }

returns early with true if some handler says to abort

keymap = A: spy.new -> true
with_signal_handler 'key-press', signal.abort, (handler) ->
  _, ret = pcall bindings.process, { character: 'A', key_name: 'A', key_code: 65 }, 'editor', { keymap }
  assert.spy(handler).was.called!
  assert.spy(keymap.A).was.not_called!
  assert.is_true ret

continues processing keymaps unless aborted

keymap = A: spy.new -> true
with_signal_handler 'key-press', false, (handler) ->
  pcall bindings.process, { character: 'A', key_name: 'A', key_code: 65 }, 'editor', { keymap }
  assert.spy(keymap.A).was_called!

(when looking up handlers)

tries each translated key and .on_unhandled in order for a keymap, and optional source specific map

keymap = Spy!
bindings.process { character: 'A', key_name: 'a', key_code: 65 }, 'my_source', { keymap }
assert.same { 'my_source', 'binding_for', 'for_os', 'A', 'a', '65', 'on_unhandled' }, keymap.reads

prefers source specific bindings over generic ones

specific_map = A: spy.new -> nil
general_map = {
  A: spy.new -> nil
  my_source: specific_map
}
bindings.process { character: 'A', key_name: 'a', key_code: 65 }, 'my_source', { general_map }
assert.spy(specific_map.A).was_called(1)
assert.spy(general_map.A).was_not_called!

prefers OS specific bindings over generic ones

specific_map = A: spy.new -> nil
general_map = {
  A: spy.new -> nil
  for_os:
    [sys.info.os]: specific_map
}
bindings.process { character: 'A', key_name: 'a', key_code: 65 }, 'my_source', { general_map }
assert.spy(specific_map.A).was_called(1)
assert.spy(general_map.A).was_not_called!

supports source specific bindings in OS bindings

specific_map = A: spy.new -> nil
general_map = {
  for_os:
    [sys.info.os]:
      A: spy.new -> nil
      my_source: specific_map
}
bindings.process { character: 'A', key_name: 'a', key_code: 65 }, 'my_source', { general_map }
assert.spy(specific_map.A).was_called(1)
assert.spy(general_map.for_os[sys.info.os].A).was_not_called!

searches all extra keymaps and the bindings in the stack

key_args = character: 'A', key_name: 'a', key_code: 65
extra_map = Spy!
stack_map = Spy!
bindings.push stack_map
bindings.process key_args, 'editor', { extra_map }
assert.equal 7, #stack_map.reads
assert.same stack_map.reads, extra_map.reads

(.. when .on_unhandled is defined and keys are not found in a keymap)

is called with the event, source, translations and extra parameters

keymap = on_unhandled: spy.new ->
event = character: 'A', key_name: 'a', key_code: 65
bindings.process event, 'editor', {keymap}, 'hello!'
assert.spy(keymap.on_unhandled).was.called_with(event, 'editor', { 'A', 'a', '65' }, 'hello!')

any return is used as the handler

handler = spy.new ->
keymap = on_unhandled: -> handler
bindings.process { character: 'A', key_name: 'A', key_code: 65 }, 'editor', { keymap }
assert.spy(handler).was.called!

(.. when .binding_for is defined and keys are not found in a keymap)

is looked up by keys bound to the commands in .binding_for

handler = spy.new ->
bindings.push a: 'my-command'
bindings.push binding_for: ['my-command']: handler
bindings.process {character: 'a', key_name: 'a', key_code: 97}, ''
assert.spy(handler).was.called!

(.. when a keymap was pushed with options.block set to true)

looks no further down the stack than that keymap

base = k: spy.new -> nil
blocking = {}
bindings.push base
bindings.push blocking, block: true
bindings.process { character: 'k', key_code: 65 }, 'editor'
assert.spy(base.k).was_not_called!

(.. when a keymap was pushed with options.pop set to true)

is automatically popped after the next dispatch

pop_me = k: spy.new -> nil
bindings.push pop_me, pop: true
bindings.process { character: 'k', key_code: 65 }, 'editor'
assert.spy(pop_me.k).was_called!
assert.not_includes bindings.keymaps, pop_me

is popped regardless of whether it contained a matching binding or not

pop_me = {}
bindings.push pop_me, pop: true
bindings.process { character: 'k', key_code: 65 }, 'editor'
assert.not_includes bindings.keymaps, pop_me

is always blocking

base = k: spy.new -> nil
pop_me = k: spy.new -> nil
bindings.push base
bindings.push pop_me, pop: true
bindings.process { character: 'k', key_code: 65 }, 'editor'
assert.spy(pop_me.k).was_called!
assert.spy(base.k).was_not_called!

(when invoking handlers)

invokes handlers in their own coroutines

coros = {}
coro_register = ->
  co, main = coroutine.running!
  coros[co] = true unless main

keymap = k: coro_register
for _ = 1,2
  bindings.process { character: 'k', key_code: 65 }, 'editor', { keymap }

assert.equal 2, #[v for _, v in pairs coros]

returns false if no handlers are found

assert.is_false bindings.process { character: 'k', key_code: 65 }, 'editor'

invokes handlers in extra keymaps before the default keymap

bindings.keymap = k: spy.new -> nil
extra_map = k: spy.new -> nil
bindings.process { character: 'k', key_code: 65 }, 'editor', { extra_map }
assert.spy(extra_map.k).was_called(1)
assert.spy(bindings.keymap.k).was_not_called!

(.. when the handler is callable)

passes along any extra arguments

keymap = k: spy.new ->
bindings.process { character: 'k', key_code: 65 }, 'editor', { keymap }, 'reference'
assert.spy(keymap.k).was.called_with('reference')

returns early with true unless a handler explicitly returns false

first = k: spy.new ->
second = k: spy.new ->
assert.is_true bindings.process { character: 'k', key_code: 65 }, 'space', { first, second }
assert.spy(second.k).was.not_called!

(.. when the handler raises an error)

returns true

keymap = { k: -> error 'BOOM!' }
assert.is_true bindings.process { character: 'k', key_code: 65 }, 'mybad', { keymap }

logs an error to the log

keymap = { k: -> error 'a to the k log' }
bindings.process { character: 'k', key_code: 65 }, 'mybad', { keymap }
assert.is_not.equal #log.entries, 0
assert.equal log.entries[#log.entries].message, 'a to the k log'

(.. when the handler is a string)

runs the command with command.run() and returns true

cmd_run = spy.on(command, 'run')
keymap = k: 'spy'
assert.is_true bindings.process { character: 'k', key_code: 65 }, 'editor', { keymap }
command.run\revert!
assert.spy(cmd_run).was.called_with 'spy'

(.. when the handler is a non-callable table)

pushes the table as a new keymap and returns true

nr_bindings = #bindings.keymaps
submap = {}
keymap = k: submap
assert.is_true bindings.process { character: 'k', key_code: 65 }, 'editor', { keymap }
assert.equal nr_bindings + 1, #bindings.keymaps
assert.equal submap, bindings.keymaps[#bindings.keymaps]

pushes the table with the pop option

submap = {}
keymap = k: submap
bindings.process { character: 'k', key_code: 65 }, 'editor', { keymap }
bindings.process { character: 'k', key_code: 65 }, 'editor'
assert.not_includes bindings.keymaps, submap

capture(function)

causes <function> to be called exclusively with event, source, translations and any extra parameters

event = character: 'A', key_name: 'a', key_code: 65
thief = spy.new -> true
keymap = A: spy.new -> true
bindings.capture thief
bindings.process event, 'source', { keymap }, 'catch-me!'
assert.spy(keymap.A).was_not.called!
assert.spy(thief).was.called_with(event, 'source', { 'A', 'a', '65' }, 'catch-me!')

<function> continues to capture events as long as it returns false

ret = false
event = character: 'A', key_name: 'A', key_code: 65
thief = spy.new -> return ret
bindings.capture thief
bindings.process event, 'editor'
ret = nil
bindings.process event, 'editor'
bindings.process event, 'editor'
assert.spy(thief).was.called(2)

cancel_capture()

cancels any currently set capture

thief = spy.new -> nil
  bindings.capture thief
  bindings.cancel_capture!
  bindings.process { character: 'A', key_name: 'A', key_code: 65 }, 'editor'
  assert.spy(thief).was_not.called!

.is_capturing

is true when a capture is active and false otherwise

assert.is_false bindings.is_capturing
bindings.capture -> nil
assert.is_true bindings.is_capturing
bindings.process { character: 'A', key_name: 'A', key_code: 65 }, 'editor'
assert.is_false bindings.is_capturing

keystrokes_for(handler, source)

returns all the key bindings that maps to <handler>

bindings.push ctrl_y: 'my-command'
bindings.push ctrl_x: 'my-command'
assert.same {'ctrl_x', 'ctrl_y'}, bindings.keystrokes_for 'my-command'

returns an empty table if no binding was found

assert.same {}, bindings.keystrokes_for 'my-command'

action_for(translation)

local saved_keymaps

before_each ->
  saved_keymaps = bindings.keymaps
  bindings.keymaps = {}

after_each ->
  bindings.keymaps = saved_keymaps

returns the command bound to translation

bindings.push ctrl_x: 'my-old-command'
bindings.push ctrl_x: 'my-new-command'
assert.equals 'my-new-command', bindings.action_for 'ctrl_x'

prefers source specific bindings over generic ones

bindings.push {
  ctrl_x: 'my-old-command'
  my_source:
    ctrl_x: 'my-source-command'
}
assert.equals 'my-source-command', bindings.action_for('ctrl_x', 'my_source')

prefers OS specific bindings over generic ones

bindings.push {
  ctrl_x: 'my-old-command'
  for_os:
    [sys.info.os]:
      ctrl_x: 'my-os-command'
}
assert.equals 'my-os-command', bindings.action_for 'ctrl_x'

returns nil if no command was found

assert.is_nil bindings.action_for 'ctrl_x'