Skip to content

Latest commit

 

History

History
406 lines (297 loc) · 22 KB

plugins.md

File metadata and controls

406 lines (297 loc) · 22 KB

Plugins

The Kap plugin system lets you create custom share targets that appear in the editor export menu. You could, for example, create a plugin to share a screen recording on YouTube.

You can discover plugins or view installed ones by clicking the Kap menu, Preferences…, and selecting the Plugins pane.

Getting started

A Kap plugin is an npm package that exports one or more services. The plugin runs in the main Electron process (Node.js). That means you can use any npm package in your plugin. Kap plugins are published to npm, like any other npm package.

Take a look at existing plugin examples in each section to see how they work.

Tip: You can use modern JavaScript features like async/await in your plugin.

Requirements

  • Your package must be named with the kap- prefix. For example kap-giphy.

  • You must have the kap-plugin keyword in package.json. Add additional relevant keywords to improve discovery.

  • The "description" in package.json should succinctly describe what you can do with it. For example: Share GIFs on GIPHY. Not something like this: Kap plugin that uploads GIFs to GIPHY.

  • The readme should follow the style of kap-giphy.

  • Your plugin must be tested, preferably using kap-plugin-test and kap-plugin-mock-context. Example.

  • The package.json file can include a kap object with the following options:

    • version: a semver range of the Kap versions your plugin supports. Defaults to *.
    • macosVersion: a semver range of the macOS versions your plugin supports. Defaults to *.
  • DEPRECATED If your plugin only supports specific versions of Kap, include a kapVersion field in the package.json with a semver range. This is still supported but will be removed at some point in favor of kap.version.

Development

When you develop a plugin it’s useful to be able to try it out in Kap. In the directory of your plugin, run $ npm link, go to ~/Library/Application Support/Kap/plugins and run $ npm link plugin-name , and then add "plugin-name": "latest" to the "dependencies" in the package.json there. Your plugin should now be shown in Kap.

When Kap is built for production, it prunes dependencies at launch time. In order to avoid any issues, make sure to run $ npm link after launching Kap, and make sure to re-run it if you restart Kap. Alternatively, you can run Kap in dev mode by downloading the source and running $ yarn start.

Services

Kap currently supports three different types of services and a plugin can have multiple of each, although each plugin should focus on a specific area.

Share services

A share service lets you add an entry to the export menu in the Kap editor window and control what happens when the user clicks it.

| Save to Disk      |
| Upload to Dropbox |
| Share on GIPHY    |

In the above case, the second and third item are added by two different share services.

The share service is a plain object defining some metadata:

  • title: The title used in the export menu. For example: Share to GIPHY.
    The text should be in title case, for example, Save to Disk, not Save to disk.
  • configDescription: A description displayed at the top of the configuration window. You can use this to explain the config options or link to API docs. Any links in this description will be parsed into clickable links automatically.
  • formats: The file formats you support. Can be: gif, mp4, webm, apng
  • action: The function that is run when the user clicks the menu item. Read more below.
  • config: Definition of the config the plugins needs. Read more below.

The config and configDescription properties are optional.

Example:

const action = async context => {
  // Do something

  context.notify('Notify about something');
};

const config = {
  apiKey: {
    title: 'API key',
    type: 'string',
    minLength: 13,
    default: '',
    required: true
  }
};

const giphy = {
  title: 'Share to GIPHY',
  formats: [
    'gif'
  ],
  action,
  config
};

exports.shareServices = [giphy];

Action

The action function is where you implement the behavior of your service. The function receives a context argument with some metadata and utility methods.

  • .format: The file format the user chose in the editor window. Can be: gif, mp4, webm, apng
  • .prettyFormat: Prettified version of .format for use in notifications. Can be: GIF, MP4, WebM, APNG
  • .defaultFileName: Default file name for the recording. For example: Kapture 2017-05-30 at 1.03.49.gif
  • .filePath(): Convert the screen recording to the user chosen format and return a Promise for the file path.
    • If you want to overwrite the format that the user selected, you can pass a fileType option: .filePath({fileType: 'mp4'}). It can be one of mp4, gif, apng, webm. This can be useful if you, for example, need to handle the GIF conversion yourself.
  • .config: Get and set config for your plugin. It’s an instance of electron-store.
  • .request(): Do a network request, like uploading. It’s a wrapper around got.
  • .copyToClipboard(text): Copy text to the clipboard. If you for example copy a link to the uploaded recording to the clipboard, don’t forget to .notify() the user about it.
  • .notify(text, action): Show a notification. Optionally pass in a function that is called with the event when the notification is clicked.
  • .setProgress(text, percentage): Update progress information in the Kap export window. Use this whenever you have long-running jobs, like uploading. The percentage should be a number between 0 and 1.
  • .openConfigFile(): Open the plugin config file in the user’s editor.
  • .cancel(): Indicate that the plugin operation canceled for some reason. Example. If the cancelation was not the result of a user gesture, use .notify() to inform the user why it was canceled.
  • .waitForDeepLink(): Returns a Promise that resolves when a deep link for this plugin is opened. The link should be in the format kap://plugins/{pluginName}/{rest}, where pluginName is the npm package name and rest is the string the Promise will resolve with. This is useful for OAuth flows.

Notes

Use context.setProgress() whenever possible to keep the user updated on what's happening. The .filePath() method sets its own progress, so you should not do it for that step.

Example plugins: kap-giphy, kap-s3, kap-imgur, kap-streamable

Edit services

Only supported in Kap >= 3.2.0.

An edit service lets you add an entry to the edit menu in the Kap editor window and process the recording before it gets converted and exported. The edit service receives an mp4 file which is generated from the recording after trimming the duration and adjusting the size. It's expected to produce another mp4 file at the given output location, which will then be passed to the appropriate share service.

The edit service is a plain object defining some metadata:

  • title: The title used in the export menu. For example: Reverse.
    The text should be in title case, for example, Slow Down, not Slow down.
  • configDescription: A description displayed at the top of the configuration window. You can use this to explain the config options or link to API docs. Any links in this description will be parsed into clickable links automatically.
  • action: The function that is run when the user clicks the menu item. Read more below.
  • config: Definition of the config the plugins needs. Read more below.

The config and configDescription properties are optional.

Example:

const action = async context => {
  // Do something

  context.notify('Notify about something');
};

const config = {
  percent: {
    title: 'Slow Down Percentage',
  type: 'number',
  maximum: 1,
  minimum: 0,
    default: 0.5,
    required: true
  }
};

const slowDown = {
  title: 'Slow Down',
  action,
  config
};

exports.editServices = [slowDown];

Action

The action function is where you implement the behavior of your service. The function receives a context argument with some metadata and utility methods.

  • .inputPath: The path to the input trimmed mp4 file.
  • .outputPath: The path where the resulting mp4 file should be by the end of the action.
  • .exportOptions: An object containing info about the recording (note that the input video has already been resized and trimmed):
    • .width: Width of the input file.
    • .height: Height of the input file.
    • .format: The selected format in which the video will be converted to later on.
    • .fps: The selected FPS that will be used for the final conversion.
    • .duration: Duration of the trimmed input file.
    • .isMuted: Whether the video is muted or not.
    • .loop: Whether the resulting GIF or APNG file will be looped or not.
  • .convert(args, text): A utility function which accepts an array of ffmpeg arguments and handles executing the command, parsing the progress, generating time estimate and showing it to the user. The second argument is optional and defaults to Converting. It can be something more descriptive to your service like Reversing and will be used for the status reporting.

Example (reversing a video):

const reverseAction = async context => {
  return context.convert([
    '-i',
    context.inputPath,
    '-vf', 'reverse',
    context.outputPath
  ], 'Reversing');

  // Will call ffmpeg -i {inputPath} -vf reverse {outputPath}
};
  • .config: Get and set config for your plugin. It’s an instance of electron-store.
  • .request(): Do a network request, like uploading. It’s a wrapper around got.
  • .copyToClipboard(text): Copy text to the clipboard. If you for example copy a link to the uploaded recording to the clipboard, don’t forget to .notify() the user about it.
  • .notify(text, action): Show a notification. Optionally pass in a function that is called with the event when the notification is clicked.
  • .openConfigFile(): Open the plugin config file in the user’s editor.
  • .cancel(): Indicate that the plugin operation canceled for some reason. Example. If the cancelation was not the result of a user gesture, use .notify() to inform the user why it was canceled.
  • .waitForDeepLink(): Returns a Promise that resolves when a deep link for this plugin is opened. The link should be in the format kap://plugins/{pluginName}/{rest}, where pluginName is the npm package name and rest is the string the Promise will resolve with. This is useful for OAuth flows.

Notes

It is highly recomended that an edit service uses a PCancelable function as the action, so Kap can cancel it in case the user decides to cancel the export.

Example:

const PCancelable = require('p-cancelable');

const action = PCancelable.fn(async (context, onCancel) => {
  const process = context.convert([
    '-i',
    context.inputPath,
    '-vf', 'reverse',
    context.outputPath
  ], 'Reversing');

  onCancel(() => {
    process.cancel();
  });

  await process;
});

Example plugins: kap-playback-speed, kap-reverse

Record services

A record service lets you add an entry to the “Plugins” submenu of the main context menu of the cropper. A user can enable or disable the service and when enabled, the service can take action in different stages of the recording process.

Record services are different from share and edit services, since they don't have one action but many hooks.

The record service is a plain object defining some metadata and hooks:

  • title: The title used in the export menu. For example: Share to GIPHY.
    The text should be in title case, for example, Save to Disk, not Save to disk.
  • configDescription: A description displayed at the top of the configuration window. You can use this to explain the config options or link to API docs. Any links in this description will be parsed into clickable links automatically.
  • config: Definition of the config the plugins needs. Read more below.
  • willStartRecording: Function that is called before the recording starts. Read more below.
  • didStartRecording: Function that is called after the recording starts. Read more below.
  • didStopRecording: Function that is called after the recording stops. Read more below.
  • willEnable: Function that is called when the user enables the service. Read more below.
  • cleanUp: Function that is called if Kap exited unexpectedly last time it was run (for example, if it crashed), without the didStopRecording hook being called. This hook will only receive the persistedState object from the state passed to the rest of the hooks. Use this to clean up any effects introduced when the recording started and don't automatically clear out once Kap stops. For example, if your plugin killed a running app with intent to restart it after the recording was over, you can use cleanUp to ensure the app is properly restarted even in the event that Kap crashed, so the didStopRecording wasn't called.

The config, configDescription and hook properties are optional.

Example:

const willStartRecording = async context => {
  // Do something
  context.notify('Recording will start now!');
};

const didStopRecording = async context => {
  // Do something
  context.notify('Recording stopped!');
};

const config = {
  apiKey: {
    title: 'API Key',
    type: 'string',
    minLength: 13,
    default: '',
    required: true
  }
};

const doNotDisturb = {
  title: 'Silence Notifications',
  willStartRecording,
  didStopRecording,
  config
};

exports.recordServices = [doNotDisturb];

Hooks

Each hook is called as described above. Each function can be asynchronous and will be called with a context object described below. The only hook that behaves differently is willEnable. This hook will be called when a service is about to be enabled (including after installing the plugin if the config is valid). The hook can be an asynchronous function and if it returns or resolves with false, the hook will not be enabled.

You can use this to check if you have enough permissions for the service to work, and if not you can request the missing permissions and return false. This ensures that your other plugin hooks will not be called until willEnable returns true.

Hooks Context

The hook functions receive a context argument with some metadata and utility methods.

  • .state: An object that will be shared and passed to all hooks in the same recording process. It can be useful to persist data between the different hooks.
    • state.persistedState: An object under state which should only contain serializable fields. It will be passed to the cleanUp hook if Kap didn't shut down correctly last time it was run. Use this to store fields necessary to clean up remaining effects.
  • .apertureOptions: An object with the options passed to Aperture. The API is described here.
  • .config: Get and set config for your plugin. It’s an instance of electron-store.
  • .request(): Do a network request, like uploading. It’s a wrapper around got.
  • .copyToClipboard(text): Copy text to the clipboard. If you for example copy a link to the uploaded recording to the clipboard, don’t forget to .notify() the user about it.
  • .notify(text, action): Show a notification. Optionally pass in a function that is called with the event when the notification is clicked.
  • .openConfigFile(): Open the plugin config file in the user’s editor.
  • .cancel(): Indicate that the plugin operation canceled for some reason. Example. If the cancelation was not the result of a user gesture, use .notify() to inform the user why it was canceled.
  • .waitForDeepLink(): Returns a Promise that resolves when a deep link for this plugin is opened. The link should be in the format kap://plugins/{pluginName}/{rest}, where pluginName is the npm package name and rest is the string the Promise will resolve with. This is useful for OAuth flows.

Example plugins: kap-do-not-disturb, kap-hide-desktop-icons

Config

The config system uses JSON Schema which lets you describe the config your plugin supports and have it validated and enforced. For example, you can define that some config key is required, or that it should be a string with a minimum length of 10. Kap will notify the user of invalid config. If you define required config, Kap will open the config file automatically on install so the user can fill out the required fields.

It’s recommended to set an empty default property for required config keys, so the user can just fill them out.

The title property must be defined for each config key. (We’ll use it in the future to render your config directly in the UI)

Example:

config: {
  username: {
    title: 'Username',
    type: 'string',
    minLength: 5,
    default: '',
    required: true
  },
  hasUnicorn: {
    title: 'Do you have a unicorn?',
    type: 'boolean',
    default: false,
    required: true
  }
}

Read more about JSON Schema

Custom Types

Kap offers a few custom types which can be displayed in a better way to the user.

hexColor

const config = {
  barColor: {
    title: 'Color',
    customType: 'hexColor',
    required: true,
    default: '#007aff'
  }
};

keyboardShortcut

List of possible values

const config = {
  keyboardShortcut: {
    title: 'Toggle Unicorn Mode',
    customType: 'keyboardShortcut',
    required: true,
    default: 'Command+Shift+5'
  }
};

// Later

electron.globalShortcut.register(config.get('shortcut'), () => { /* ... */ });

Note: Kap will not register any action for the keyboard shortcut. That is up to the plugin implementation.

General APIs

Every type of plugin and service can additionally export the following:

  • didInstall(config): A hook that will be called when the plugin is first installed.
  • didConfigChange(newValues, oldValues, config): A hook that will be called whenever the config of the plugin is changed.
  • willUninstall(config): A hook that will be called when a plugin is being uninstalled. It can be used to clean up artifacts.

In addition to these, each plugin needs to export at least one of the following:

  • shareServices: an array of share services and described above
  • editServices: an array of edit services and described above
  • recordServices: an array of record services and described above

OAuth

Sometimes services require an OAuth flow to retrieve a token. These flows are often required to be completed in the browser and not in a webview. For this reason, Kap provides deep linking support. Follow these steps to support OAuth in your plugin:

  • When the export starts, check if the accessToken is available (if you have already authenticated) in the plugin config. This should not be listed in the JSON Schema options mentioned above unless the user is meant to edit them.
  • If it's not available, open an external link to the OAuth provider's page with the correct parameters. Usually client ID.
  • When registering the app, provide something like kap://plugins/{pluginName}/auth as the callback URL.
  • Call context.waitForDeepLink() and wait for the user to go through the process.
  • When the above call resolves, you'll have the remaining path, along with any extra info the API added. In the above example, it would be something like auth?code=###.
  • You can now exchange the code for a token, and then store that in the config, so you can use it for future exports.

For an example of this flow in action, check out kap-dropbox.

If the API provider only allows HTTP/HTTPS URLs, or if you don't want to do the code exchange in the plugin (to avoid having the secret in the code), you might need to create a proxy, similar to the one used for kap-dropbox, to trigger the deep link.

Removing your Kap plugin

Since npm doesn't allow you to remove packages from the registry, Kap filters out deprecated packages in the plugin list.

When you are ready to retire your Kap plugin, simply run npm deprecate kap-plugin "Deprecated".

Read more about the npm-deprecate command