howl.bindings

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

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'
}

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

(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 and alt

tr = bindings.translate_key
  character: 'A', key_name: 'a', key_code: 123,
  control: true, alt: true
mods = 'ctrl_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' }

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) ->
  status, ret = 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 the handler does

keymap = A: spy.new -> true
with_signal_handler 'key-press', true, (handler) ->
  status, 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 if the handler returns false

keymap = A: spy.new -> true
with_signal_handler 'key-press', false, (handler) ->
  status, ret = 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', 'A', 'a', '65', 'on_unhandled' }, keymap.reads

prefers source specific bindings

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!

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 5, #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 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 i = 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 -> return ret
  bindings.capture thief
  bindings.cancel_capture!
  bindings.process { character: 'A', key_name: 'A', key_code: 65 }, 'editor'
  assert.spy(thief).was_not.called!