Introducing kmjs

Hey everyone!

I'm excited to share kmjs, an open-source TypeScript toolkit I created for programmatically interfacing with Keyboard Maestro, designed for constructing and executing macros entirely in-memory.

It handles all the boilerplate required for constructing each 'virtual' macro's XML, and then running it, and returning the result. Each virtual action has been tested through an automated routine I created, which generates and imports its possible configurations into KM as a real macro, then extracts that, and then compares the expected result with the generated result.

Here's an example of creating a virtual macro and running it:

import { createVirtualNotification, createVirtualPause, runVirtualMacro, } from "kmjs";

// 1. Define your sequence of virtual actions

const actions = [

createVirtualNotification({

title: "Hello from kmjs!",

body: "This is a virtual macro.",

sound: "Glass",

}),

createVirtualPause({ time: 1 }), // Pause for 1 second

createVirtualNotification({

title: "Still here!",

body: "The virtual macro has finished.",

sound: "Ping",

}),

];

// 2. Run the sequence in-memory via KM

runVirtualMacro(actions, "My First Virtual Macro");

Virtual macros can return values, too, of course.

I also built a 'query' framework, which takes KM's tokens and wraps them in a single virtual macro, which then returns the relevant value. For example, getting the current mouse position in JS is now as simple as:

import { getMousePosition } from "kmjs/queries";

const positionString = getMousePosition();

// e.g. "1234,876"*

Beyond the virtual actions/macros, it's packed with various tools and functions that utilise KM, including workflows focused on working with variables and macros that exist in real macros in the GUI. It also includes various JavaScript utilities and tools conceptualised/created by the community, re-packaged as TypeScript functions as a part of this project.

I built this as part of a commercial JS project in which I wanted to use KM for building automated testing routines. Then, of course, as every KM user is too familiar with, the little side project ballooned exponentially into what it is now. As such, not every action has a virtual equivalent (yet). I focused on what would be immediately useful for GUI automation, plus a few extras.

I'd rather not inundate this forum post with everything in kmjs, but I also wanted to highlight the keyboard handling utilities. While creating this, I was bewildered that there are no centralised or complete examples of ready-to-use AppleScript keyboard code mappings. As such, I've built out a complete system for keystroke and keyboard shortcut normalisation (which the virtual actions for keystrokes use). You can input:

  • Human-Readable String : Common shortcuts like "Cmd+Shift+S".
  • JavaScript event.code String : Web-standard codes like "KeyS" or "Digit1".
  • Raw Numeric Key Code : A number representing an AppleScript key code, like 36 for the Return key.
  • AppleScript Map : An existing map object like { 256: 1 } (Cmd+S), which is passed through unmodified.

... and produce an object with a single key-value pair that represents a modifier combination and keycode: { [modifierMask]: [keyCode] }. These are then separated out for use in KM actions. I suspect this part of the project might be useful to many people beyond those using KM.

As someone who's benefited enormously from this community, I want to give back with something I hope helps you in your KM-related projects.

Cheers!

(This is my first GitHub / npm project. Please go easy on me - I'd also greatly appreciate any advice about going about hosting and collaborating on repos)!

5 Likes

I'm teetering on the edge of my seat, but before I get too excited, could you provide any visual use-case examples? What kind of problems might kmjs solve for us tweakers?

I'm really interested in this. I'd love to create a tighter integration between my Keyboard Maestro macros and the other tools I use!

Yeah definitely!

There are many use-cases with kmjs, ranging from:

  • Using a few convenience functions on the JS side to make it easy to interface with KM variables/macros
  • Extracting KM macros to edit the XML of programmatically, and then re-importing into KM
  • Only using virtual macros and actions that exist in your JavaScript.

I got heavy into mixing JavaScript into my KM macros for a project last year, to the point many of the macros I were making were essentially wrappers for JS scripts (many of which executed KM macros themselves). Kmjs allows you to flip this around so everything can live in the JS side, which can be easier to scale for larger projects. Managing the control flow is much simpler and easier to structure (especially if a project involves multiple people).

Keyboard Maestro itself interfaces with many low-level hooks that would be incredibly involved to plug into yourself for hobby projects (such as finding mouse position or window names OS-wide), which the queries module of kmjs makes easy to do using KM as a conduit. Given that kmjs requires a valid km license to use it works well for personal / non-distributed projects of that nature.

This also enables the creation of a factory function that produces real KM macros. Let's say you wanted to produce a dozen real KM macros that do subtly different things - you could easily do this in kmjs by constructing a virtual macro in a loop of some kind and altering values/variables/actions for each iteration, and then exporting each of these directly to KM.

I included a few examples on the repo, the one below illustrating:

  • Constructing a virtual macro out of virtual actions,
  • Producing the equivalent XML for Keyboard Maestro
  • And presenting this in a format that KM can actually import as a valid macro, and then importing it into a macro group
#!/usr/bin/env node

/**
 * Demo script showing how to use the generateMacro function.
 *
 * This example demonstrates all the different ways to use generateMacro:
 * - Generate raw XML
 * - Add plist wrapping
 * - Display in text window
 * - Export to file
 * - Import to KM group
 */

const {
  generateMacro,
  createVirtualNotification,
  createVirtualPause,
  createVirtualComment,
  createVirtualInsertText,
  createVirtualSetVariable,
} = require("../bundle/kmjs.js");

console.log("🚀 generateMacro Demo\n");

// Create some sample virtual actions
const sampleActions = [
  createVirtualComment({
    title: "Demo Macro",
    text: "This macro was generated using the generateMacro function",
  }),
  createVirtualSetVariable({
    variable: "DemoStatus",
    text: "Starting demo...",
    scope: "local",
  }),
  createVirtualNotification({
    title: "Demo Started",
    body: "This is a generated macro demo",
    sound: "Glass",
  }),
  createVirtualPause({ time: 2 }),
  createVirtualInsertText({
    text: "Hello from generateMacro! Status: %Variable%DemoStatus%",
  }),
  createVirtualNotification({
    title: "Demo Complete",
    body: "The generated macro has finished executing",
    sound: "Ping",
  }),
];

console.log("📝 Example 1: Generate raw XML");
console.log("=".repeat(50));
const rawXml = generateMacro(sampleActions);
console.log("Raw XML generated (first 200 chars):");
console.log(rawXml.substring(0, 200) + "...\n");

console.log("📦 Example 2: Generate XML with plist wrapping");
console.log("=".repeat(50));
const wrappedXml = generateMacro(sampleActions, {
  addPlistWrapping: true,
  macroName: "Demo Macro with Plist",
});
console.log("Plist-wrapped XML generated (first 200 chars):");
console.log(wrappedXml.substring(0, 200) + "...\n");

console.log("🖥️  Example 3: Display XML in Keyboard Maestro text window");
console.log("=".repeat(50));
generateMacro(sampleActions, {
  exportTarget: { displayInTextWindow: true },
  macroName: "Display Demo",
});
console.log("XML displayed in KM text window\n");

console.log(
  "💾 Example 4: Export to file (commented out to avoid file creation)",
);
console.log("=".repeat(50));
console.log("// generateMacro(sampleActions, {");
console.log("//   exportTarget: { filePath: './demo-macro.kmmacros' },");
console.log("//   macroName: 'File Export Demo'");
console.log("// });");
console.log("File export example (commented out)\n");

console.log("🎯 Example 5: Import directly to Keyboard Maestro group");
console.log("=".repeat(50));
generateMacro(sampleActions, {
  exportTarget: { toKMGroup: "kmjs-demos" },
  macroName: "generateMacro Demo",
});
console.log("Macro imported to 'kmjs-demos' group in Keyboard Maestro\n");

console.log("🔄 Example 6: Multiple export targets");
console.log("=".repeat(50));
generateMacro(sampleActions, {
  exportTarget: {
    displayInTextWindow: true,
    toKMGroup: "kmjs-demos",
  },
  macroName: "Multi-Export Demo",
});
console.log("Macro displayed in text window AND imported to KM group\n");

console.log("✅ Demo complete!");
console.log("\nCheck your Keyboard Maestro editor for the 'kmjs-demos' group");
console.log(
  "to see the imported macros. You can run them to test the functionality.",
);
6 Likes