diff --git a/docs/_layouts/default.html b/docs/_layouts/default.html
index 0569452f..f72b5525 100644
--- a/docs/_layouts/default.html
+++ b/docs/_layouts/default.html
@@ -37,6 +37,7 @@
Guide
Blacklist
Search engines
Properties
+ SendMessage
diff --git a/docs/sendmessage.md b/docs/sendmessage.md
new file mode 100644
index 00000000..50a38b33
--- /dev/null
+++ b/docs/sendmessage.md
@@ -0,0 +1,31 @@
+---
+title: SendMessage
+---
+
+# SendMessage
+
+Vim Vixen can send messages to other add-ons to controll their functionality by keyboard, if they support. To use this feature, you need to specify `addon.sendmessage` as `type`, `extensionId` and `message` (this can be string or object) for keymaps.
+
+Note that, currently this feature can be set only when using "Use plain JSON".
+
+## Example
+
+Following example enables to toggle collapsed state of [Tree Style Tab](https://addons.mozilla.org/firefox/addon/tree-style-tab/)'s active tab by pressing zc. This kind of API might be described in add-ons' web site, for example you can find Tree Style Tab's API reference [here](https://github.com/piroor/treestyletab/wiki/API-for-other-addons).
+
+```json
+{
+ "keymaps": {
+ "zc": {
+ "type": "addon.sendmessage",
+ "extensionId": "treestyletab@piro.sakura.ne.jp",
+ "message": {
+ "type": "toggle-tree-collapsed",
+ "tab": "active"
+ }
+ }
+ }
+}
+```
+
+You may want to see [the Wiki page for the same feature on Gesturefy](https://twitter.com/tomo_ahm/status/1297849816907575296) for more example.
+
diff --git a/src/content/controllers/KeymapController.ts b/src/content/controllers/KeymapController.ts
index 092e55ce..72ef46d9 100644
--- a/src/content/controllers/KeymapController.ts
+++ b/src/content/controllers/KeymapController.ts
@@ -2,6 +2,7 @@ import { injectable, inject } from "tsyringe";
import * as operations from "../../shared/operations";
import KeymapUseCase from "../usecases/KeymapUseCase";
import AddonEnabledUseCase from "../usecases/AddonEnabledUseCase";
+import AddonSendmessageUseCase from "../usecases/AddonSendmessageUseCase";
import FindSlaveUseCase from "../usecases/FindSlaveUseCase";
import ScrollUseCase from "../usecases/ScrollUseCase";
import FocusUseCase from "../usecases/FocusUseCase";
@@ -16,6 +17,7 @@ export default class KeymapController {
constructor(
private keymapUseCase: KeymapUseCase,
private addonEnabledUseCase: AddonEnabledUseCase,
+ private addonSendmessageUseCase: AddonSendmessageUseCase,
private findSlaveUseCase: FindSlaveUseCase,
private scrollUseCase: ScrollUseCase,
private focusUseCase: FocusUseCase,
@@ -48,6 +50,12 @@ export default class KeymapController {
return () => this.addonEnabledUseCase.disable();
case operations.ADDON_TOGGLE_ENABLED:
return () => this.addonEnabledUseCase.toggle();
+ case operations.ADDON_SENDMESSAGE:
+ return () =>
+ this.addonSendmessageUseCase.sendMessage(
+ op.extensionId,
+ op.message
+ );
case operations.FIND_NEXT:
return () => this.findSlaveUseCase.findNext();
case operations.FIND_PREV:
diff --git a/src/content/usecases/AddonSendmessageUseCase.ts b/src/content/usecases/AddonSendmessageUseCase.ts
new file mode 100644
index 00000000..b5d50d3b
--- /dev/null
+++ b/src/content/usecases/AddonSendmessageUseCase.ts
@@ -0,0 +1,17 @@
+import { injectable, inject } from "tsyringe";
+import ConsoleClient from "../client/ConsoleClient";
+
+@injectable()
+export default class AddonSendmessageUseCase {
+ constructor(@inject("ConsoleClient") private consoleClient: ConsoleClient) {}
+
+ async sendMessage(extensionId: string, message: any) {
+ const sending = browser.runtime.sendMessage(extensionId, message);
+ sending.catch((reason: any) => {
+ this.consoleClient.error(
+ `Error on sending message to ${extensionId}: ${reason}`
+ );
+ });
+ return sending;
+ }
+}
diff --git a/src/shared/operations.ts b/src/shared/operations.ts
index 35445020..21653df6 100644
--- a/src/shared/operations.ts
+++ b/src/shared/operations.ts
@@ -5,6 +5,7 @@ export const CANCEL = "cancel";
export const ADDON_ENABLE = "addon.enable";
export const ADDON_DISABLE = "addon.disable";
export const ADDON_TOGGLE_ENABLED = "addon.toggle.enabled";
+export const ADDON_SENDMESSAGE = "addon.sendmessage";
// Command
export const COMMAND_SHOW = "command.show";
@@ -97,6 +98,12 @@ export interface AddonToggleEnabledOperation {
type: typeof ADDON_TOGGLE_ENABLED;
}
+export interface AddonSendmessageOperation {
+ type: typeof ADDON_SENDMESSAGE;
+ extensionId: string;
+ message: string | Record;
+}
+
export interface CommandShowOperation {
type: typeof COMMAND_SHOW;
}
@@ -315,6 +322,7 @@ export type Operation =
| AddonEnableOperation
| AddonDisableOperation
| AddonToggleEnabledOperation
+ | AddonSendmessageOperation
| CommandShowOperation
| CommandShowOpenOperation
| CommandShowTabopenOperation
@@ -409,12 +417,29 @@ const assertRequiredString = (obj: any, name: string) => {
}
};
+const assertRequiredObjectOrString = (obj: any, name: string) => {
+ if (
+ !Object.prototype.hasOwnProperty.call(obj, name) ||
+ !(typeof obj[name] === "string" || typeof obj[name] == "object")
+ ) {
+ throw new TypeError(`Missing object or string parameter: '${name}`);
+ }
+};
+
// eslint-disable-next-line complexity, max-lines-per-function
export const valueOf = (o: any): Operation => {
if (!Object.prototype.hasOwnProperty.call(o, "type")) {
throw new TypeError(`Missing 'type' field`);
}
switch (o.type) {
+ case ADDON_SENDMESSAGE:
+ assertRequiredString(o, "extensionId");
+ assertRequiredObjectOrString(o, "message");
+ return {
+ type: o.type,
+ extensionId: o.extensionId,
+ message: o.message,
+ };
case COMMAND_SHOW_OPEN:
case COMMAND_SHOW_TABOPEN:
case COMMAND_SHOW_WINOPEN: