npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

composable-keybindings

v1.1.0

Published

Create composable keybindings in Node or the browser.

Downloads

16

Readme

composable-keybindings

Define complex keybindings, inspired by Vim.

At a glance

import { Keybinder } = from 'composable-keybindings';

const keybindings = new Map([
  ['t', { name: 'test' }],
  ['ctrl+f', { name: 'find' }],
  ['z', {
    name: 'z',
    keybindings: new Map([
      ['t', { name: 'nested-t' }]
    ])
  }]
]);

const keybinder = new Keybinder(keybindings, (type, keybinding) => {
  if (type === 'keybinding') {
    console.log('found keybinding', keybinding);
  } else if (type === 'unrecognized') {
    console.log('unrecognized');
  }
});

keybinder.handleKey('t');
// {
//   keys: ['t'],
//   count: 1,
//   countChars: [],
//   action: { name: 'test' },
//   store: {}
// }

Keybinding map

Recommended to use strings as keys and objects as values. Keybinding actions (map values) accept 3 special fields:

Nested keybindings

Allows multiple keys to be inputted before firing an event.

new Map([
  ['b', {
    keybindings: new Map([
      ['r', {
        keybindings: new Map([
          ['b', { name: 'be-right-back' }]
        ])
      }]
    ])
  }]
]);

// inputting `brb` emits:
// keybinding
// {
//   keys: ['b', 'r', 'b'],
//   count: 1,
//   countChars: [],
//   action: { name: 'be-right-back' },
//   store: {}
// }

Keybinding count

Number characters are interpreted as count and by default do not fire keybindings when inputted.

new Map([['t', { name: 'test' }]])

// inputting `30t` emits:
// type 'keybinding'
// {
//   keys: ['3', '0', 't'],
//   count: 30,
//   countChars: ['3', '0'],
//   action: { name: 'test' },
//   store: {}
// }

isKeyNumber in keybinder options dictates whether number characters are interpreted as count.

Behavior Function

The behavior function is used for more dynamic behavior.

new Map(['g', {
  name: 'read-three',
  behavior: (instructions, kb) => {
    instructions.read(3, keys => {
      kb.store.readThree = keys;
      instructions.done();
    });
  }
}]);

keybinder.handleKeys(['g', 'x', 'y', 'z']);
// emits:
// type 'keybinding'
// {
//   keys: ['g', 'x', 'y', 'z'],
//   count: 1,
//   countChars: [],
//   action: { name: 'read-three', behavior: [Function: behavior] },
//   store: { readThree: ['x', 'y', 'z'] }
// }

The function takes in an instructions API and the current keybinding that's being generated. 3 instructions are available:

instructions.done() must be called to complete the behavior function.

Nested keybindings take priority over the behavior function. Extending the above example, if gx was defined as a nested keybinding the behavior function would not be called.

instructions.read

Waits for more keys to be inputted, then calls the callback. Requires 2 arguments: the count and callback function. Callback function is called with the array of inputted keys.

instructions.read(1, keys => {
  console.log(keys.length); // 1
  instructions.done();
});

instructions.interpret

Waits for the next inputted key, then checks if it points to an action in the keybinding map. If so, that action's behavior function is entered. It requires a callback function and optionally a filter function.

The callback function is called given the same arguments as the keybinder listener, the type and keybinding of the sub-event.

The filter function is called given the found sub-action and should return a boolean true if OK to continue and false if the keybinding should be cancelled.

instructions.interpret() will interpret any defined keybindings "above" the action it's defined in. To scope keybindings inside an action or override "above" actions, specify an interprets map.

Actions are not allowed to interpret themselves in order to avoid endless loops. In that case the 'cancel' event would emit.

new Map([
  // root 'a'
  ['a', {
    behavior: (instructions, kb) => {
      // `instructions.interpret()` here can look up all "above" actions (root
      // 'b' and root 'c'), as well as scoped `interprets` ('d'). However a
      // filter is given that explicity ignores 'c'
      instructions.interpret((type, subKeybinding) => {
        instructions.done();
      }, (action) => action.name !== 'c')
    },
    inteprets: new Map([
      // scoped and overrided 'b'
      ['b', {
        behavior: (instructions, kb) => {
          // inputting keys `ab` will begin to read 2 keys, not 1
          instructions.read(2, keys => instructions.done());
        }
      }],
      // scoped 'd'
      ['d', {
        behavior: (instructions, kb) => {
          instructions.done();
        }
      }]
    ])
  }],

  // root 'b'
  ['b', {
    behavior: (instructions, kb) => {
      instructions.read(1, keys => instructions.done());
    }
  }],

  // root 'c'
  ['c', {
    name: 'c',
    behavior: (instructions, kb) => {
      instructions.done();
    }
  }],
])

instructions.emit

Calls the listener function given with the given arguments. Similar to instructions.done but doesn't reset the key map.

instructions.done

Must be called to signal the behavior function is finished. If this keybinding is being "interpreted" by another behavior function, calling this will bubble up to the parent's interpret() callback function, otherwise an event will emitted with type either 'keybinding', 'unrecognized', or 'cancel'.

Optionally takes an options object:

  • type: 'keybinding', 'unrecognized', or 'cancel' (default 'keybinding')
  • status: one of the status enums (default STATUS.DONE)

If a string is given instead of an options object, it represents type.

type can also be 'resume' or STATUS.WAITING, which will avoid emitting a keybinding event and continue building on the current generated keybinding object.

Keybinding store

Any other data you want associated with the keybinding event used in conjuction with the behavior function. The store can have any data in it and is not used internally.

Keybinder listener

The listener is called whenever something happens. It's called with the event type and the associated generated keybinding object.

// type: 'keybinding', 'unrecognized', or 'cancel'
// keybinding: generated keybinding object
new Keybinder(map, (type, keybinding) => {
  if (type === 'keybinding') {
    console.log('keybinding event emitted', keybinding);
  } else if (type === 'unrecognized') {
    console.log('no defined keybinding for the inputted keys', keybinding);
  } else if (type === 'cancel') {
    console.log('keybinding was cancelled', keybinding);
  }
});

Keybinder options

Defaults, all optional:

const DEFAULTS = {
	getKeybinding: (key, map) => map.get(key),
	isKeyNumber: key => /\d/.test(key),
	isKeyEscape: key => key === 'escape',
	store: {},
	filter: null
};
  • getKeybinding: an intermediary step to map keypress info into the keys inside the keybinding map.
  • isKeyNumber: an intermediary step to map keypress info into whether it's a number.
  • isKeyEscape: an intermediary step to map keypress info into whether it's the escape key.
  • store: any associated data to be attached to keybinding events (used internally).
  • filter: a function to filter out unwanted keybindings (used internally), see instructions.interpret().

Keypress listeners

You will need to set up your own keypress listeners and call keybinder.handleKey() inside them.

The argument to handleKey() should be a key that can be looked up in the keybinding map, i.e. if the map keys are strings and a browser's KeyboardEvent is fired, you need to transform the event into a string.

Alternatively use the getKeybinding option in keybinder options.

There are a couple helpers to format keypress events in the browser and Node to serve as starting points. TODO complete these.

There is also a Node listener to help with the boilerplate of keypress in Node.

Playground

Playground