#!/bin/bash

# Wispr Flow's dictation shortcuts, on the Caps Lock key: Push to talk on F20,
# Command Mode on Ctrl+F20. Double-tap for hands-free comes from the Push to
# talk binding and needs none of its own.
#
#   wispr-keys
#
# Caps Lock sends F20 on these Macs (dotlib.capslock_f20, see AGENTS.md), and
# Flow's settings screen refuses F20 on its own -- "Shortcut must include a
# modifier key or a valid mouse button" -- while its key listener accepts it.
# So the bindings go into Flow's config.json instead, which is what this is for.
#
# Run it by hand: on a Mac that has just installed Flow, and again after
# re-binding either action in Flow's settings, which overwrites what this sets.
# Deliberately not a link/ part, though it looks like one: it has to quit and
# restart Flow, and nothing that runs unattended after every sync should be
# able to kill a dictation in progress. Nothing calls this on its own.
#
# How Flow stores shortcuts, read from Flow 1.6.1021's own app.asar rather than
# guessed: prefs.user.shortcuts maps a key to an action, where the key is the
# macOS key codes string-sorted and joined with "+" (Flow does
# e.sort().join("+")). Its key table has f20: 90 and ctrl: 59, so Ctrl+F20 is
# "59+90". ptt is Push to talk; lens is Command Mode -- the settings dialog
# draws that row from Lens, and the recorder picks Lens when isCommandMode.
# prefs.cache.splitKeybinds holds the same bindings as arrays, and is kept in
# step here so Flow does not read back a disagreeing pair.

set -euo pipefail

if [[ "$(uname)" != "Darwin" ]]
then
  >&2 echo "ERROR: macOS only"
  exit 1
fi

config="$HOME/Library/Application Support/Wispr Flow/config.json"
if [[ ! -f "$config" ]]
then
  >&2 echo "ERROR: no Flow config at ${config}"
  >&2 echo "Install Wispr Flow and start it once; it writes that file on first launch."
  exit 1
fi

if ! command -v jq >/dev/null
then
  >&2 echo "ERROR: jq is needed to edit Flow's config and is not on PATH"
  exit 1
fi

# Drop whatever ptt and lens are bound to now, and anything else holding the
# keys they are about to take, then bind them. splitKeybinds is absent from a
# config Flow has not written a cache into yet, and `map` over null is an
# error, hence the `// []`.
read -r -d '' filter <<'JQ' || true
.prefs.user.shortcuts |= (
    with_entries(select(
      .value != "ptt" and .value != "lens" and .key != "90" and .key != "59+90"))
    + {"90": "ptt", "59+90": "lens"})
| .prefs.cache.splitKeybinds |= ((. // [])
    | map(select(
      .value != "ptt" and .value != "lens"
      and .shortcut != [90] and .shortcut != [59, 90]))
    + [{"shortcut": [90], "value": "ptt"}, {"shortcut": [59, 90], "value": "lens"}])
JQ

# Just the two places that matter, in a stable order, so a reordering by Flow
# does not read as a change.
bindings() {
  jq -S '{u: .prefs.user.shortcuts, c: (.prefs.cache.splitKeybinds // [] | sort_by(.value))}' "$1"
}

wanted="$(mktemp)"
trap 'rm -f "$wanted"' EXIT
jq "$filter" "$config" > "$wanted"

if [[ "$(bindings "$config")" == "$(bindings "$wanted")" ]]
then
  echo "Wispr Flow: Push to talk = F20, Command Mode = Ctrl+F20 (already set)"
  exit 0
fi

# The main process only: the Swift helper inside the bundle has its own
# "Wispr Flow.app" path, under Contents/Resources, and goes when this one does.
bundle='^/Applications/Wispr Flow\.app/'
main="${bundle}Contents/MacOS/Wispr Flow( |\$)"
was_running=""
if pgrep -f "$main" >/dev/null
then
  was_running="y"
  # Flow ignores an AppleScript quit; a plain TERM shuts it down cleanly.
  echo "INFO: stopping Wispr Flow to edit its config"
  pkill -TERM -f "$main"
  # Every process in the bundle, helpers included: while any is still exiting,
  # the launch below finds the old instance and starts nothing.
  for _ in $(seq 1 60)
  do
    pgrep -f "$bundle" >/dev/null || break
    sleep 0.25
  done
  if pgrep -f "$bundle" >/dev/null
  then
    >&2 echo "ERROR: Wispr Flow did not quit; shortcuts not set"
    exit 1
  fi
fi

# Once, and never overwritten: the point is to keep the state from before this
# script first touched the file, account and all, not the state from last run.
if [[ ! -f "${config}.pre-wispr-keys" ]]
then
  cp -p "$config" "${config}.pre-wispr-keys"
  echo "INFO: kept the original as ${config}.pre-wispr-keys"
fi

# Again, from what Flow wrote as it quit. Written in place rather than moved
# over, so the file keeps its owner and mode.
jq "$filter" "$config" > "$wanted"
cat "$wanted" > "$config"
echo "Wispr Flow: Push to talk = F20, Command Mode = Ctrl+F20"

if [[ -n "$was_running" ]]
then
  # By bundle id: the Swift helper inside the bundle is also an app named
  # "Wispr Flow", and `open -a "Wispr Flow"` can start that one instead -- it
  # exits at once, leaving Flow down.
  open -b com.electron.wispr-flow
  for _ in $(seq 1 40)
  do
    pgrep -f "$main" >/dev/null && exit 0
    sleep 0.25
  done
  >&2 echo "ERROR: Wispr Flow did not start again; open it by hand"
  exit 1
fi
