Note: This site is currently "Under construction". I'm migrating to a new version of my site building software. Lots of things are in a state of disrepair as a result (for example, footnote links aren't working). It's all part of the process of building in public. Most things should still be readable though.

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

A text editor with text being written to it automatically including highlighting a misspelling and correcting it

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.

Code

#!/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.

References