Home
Head's Up: I'm in the middle of upgrading my site. Most things are in place, but there are something missing and/or broken including image alt text. Please bear with me while I'm getting things fixed.

Send Keystrokes To A Mac App With JavaScript

Introduciton

I've been making videos with code samples. Typing and talking at the same time is hard work and tends to bring the final quality down since doing two things at once is hard. So, I wrote a script to do the typing for me.

Here's what it looks like when I send the output to Sublime Text

And, yes. I intentionlly put a typo in there so I could show using the option and shift keys to move and select text.

The Script

The script itself uses built - in macOS JavaScript For Automation (JXA) tooling via "osascript". It's bascially Apple Script, but in JavaScript. Here's the full thing in cluding the config I used to make the GIF.

osascript
#!/usr/bin/env osascript -l JavaScript

const config = {
  app_name: "Sublime Text",
  start_delay_ms: 0,
  end_delay_ms: 0,
  snippets: [
    { keys: `the `},
    { keys: `quick``, shift: true },
    { keys: `` brown fox`},
    { code: 76 },
    { keys: ``jumps over the lzay dog`` },
    { pause: 700 },
    { code: 123, option: true },
    { code: 123, shift: true, option: true },
    { pause: 100 },
    { keys: `lazy `},
    { code: 124, command: true },
    { code: 76 },
  ]
}

function get_current_app() {
  return Application(Application("System Events").processes.whose(
    { frontmost: { '=': true } })[0].name())
}

function get_target_app(config) {
  return Application(config.app_name)
}

function output_char(config, command_, option_, shift_) {
  sleepchar()
  const using = []
  if (command_) { using.push("command down") }
  if (shift_) { using.push("shift down") }
  if (option_) { using.push("option down") }
  config.target_app.activate()
  config.sys.keystroke(config.char, { using: using })
}

function output_code(config, command_, option_, shift_) {
  const using = []
  if (command_) { using.push("command down") }
  if (shift_) { using.push("shift down") }
  if (option_) { using.push("option down") }
  config.target_app.activate()
  config.sys.keyCode(config.code, { using: using })
}

function output_keys(config) {
  sleep(config.start_delay_ms)
  config.keep_going = true
  config.snippets.forEach((snippet) => {
    sleepblock()
    if (snippet.keys) {
      snippet.keys.split("").forEach((char) => {
        config.char = char
        output_char(config, snippet.command, snippet.option, snippet.shift)
      })
    } else if (snippet.code) {
      config.code = snippet.code
      output_code(config, snippet.command, snippet.option, snippet.shift)
    } else if (snippet.pause) {
      sleep(snippet.pause)
    }
  })
  sleep(config.end_delay_ms)
}

function type_stuff(config) {
  config.sys = Application('System Events')
  config.driver_app = get_current_app()
  config.target_app = get_target_app(config)
  output_keys(config)
  config.driver_app.activate()
}

function sleepblock() {
  sleep(Math.floor(Math.random() * 30) + 100)
}

function sleepchar() {
  sleep(Math.floor(Math.random() * 40) + 20)
}

function sleep(milliseconds) {
  const date = Date.now();
  let currentDate = null;
  do {
    currentDate = Date.now();
  } while (currentDate - date < milliseconds);
}

type_stuff(config)

I put a few random sleep timers between each character and each snippet to make it look a little more like someone typing. You can rip all that stuff out and have it type as fast as it can too, but I like this visual better.

- There are some apps out there that do this type of thing, but I like the control I have in the config to do different pauses and sending different mofidier keys

- The first time you run the script you'll be asked to allow it in the security and privacy preferences. The app will need to be added to the "Accessibility" list in the "Allow the apps below to control your computer (the specific details of what needs to be done may chance between OS versions)

- This script can be run by making it executable or directly on the command line with :

osascript - l JavaScript FILENAME.js

(and yes, the "J" an "S" in "JavaScript" have to be upper case)

- The script attempts to switch to the target app before sending each character. That's designed to prevent accidentally switching to another app and sending the keystrokes to it instead. I haven't had any problems with it but there's a chance of a race condition where a character ends up getting sent to the wrong place

- Because the script switches to the target app before each keystroke there's not an easy way to stop it once you've started it. It mostly has to play all the way through. (Killing the app the script is running in should work, but it may be hard to get to)

- The script doesn't have any sense of other characters added while it's typing. For example, if you're using a code editor that automatically adds closing parenthesis you'll end up with an extra one

- The script also can't tell if the characters its sending are doing the right thing or anything at all. For example, if you target a text editor but don't have a window open the keystrokes might make things happen in the menus

- The "command", "option", and "shift" flag will send those keys along if they are set to true

- Some things need to be sent via code instead of letter. For example, 76 is "Enter", 123 is "Left Arrow", and 124 is "Right Arrow". The link to the key code reference is below

Footnotes And References