Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 24 additions & 6 deletions Sources/parrot/Input/HotkeyMonitor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,17 @@ final class HotkeyMonitor {
throw HotkeyError.tapCreateFailed
}

let mask: CGEventMask =
(1 << CGEventType.flagsChanged.rawValue)
| (1 << CGEventType.keyDown.rawValue)
| (1 << CGEventType.keyUp.rawValue)
// Only flagsChanged is ever acted on in `handle`. keyDown/keyUp are
// subscribed *only* under --debug-hotkey: listening to every keystroke
// system-wide costs main-thread work, raises the odds macOS disables
// the tap for timeout, and needlessly exposes typed content (including
// password fields) to an Accessibility-privileged process.
var mask: CGEventMask = (1 << CGEventType.flagsChanged.rawValue)
if debug {
mask |=
(1 << CGEventType.keyDown.rawValue)
| (1 << CGEventType.keyUp.rawValue)
}
let userInfo = Unmanaged.passUnretained(self).toOpaque()

// .cgSessionEventTap is the right level for an accessibility-granted
Expand Down Expand Up @@ -76,6 +83,17 @@ final class HotkeyMonitor {
onEvent = nil
}

/// Called from the tap callback when macOS disables our tap. Without this
/// the process keeps running, the menu bar icon stays put, and the hotkey
/// silently does nothing until the user restarts parrot.
fileprivate func reEnable() {
guard let tap else { return }
CGEvent.tapEnable(tap: tap, enable: true)
FileHandle.standardError.write(Data(
"event tap was disabled by the system — re-enabled\n".utf8
))
}

fileprivate func handle(type: CGEventType, event: CGEvent) {
if debug {
let flags = event.flags
Expand Down Expand Up @@ -104,8 +122,8 @@ private func hotkeyCallback(
let monitor = Unmanaged<HotkeyMonitor>.fromOpaque(userInfo).takeUnretainedValue()

if type == .tapDisabledByTimeout || type == .tapDisabledByUserInput {
// System disabled our tap; we'll need to re-enable. For now just no-op
// and let the user restart parrot.
// The tap must be re-enabled from the run loop that owns it.
DispatchQueue.main.async { monitor.reEnable() }
return Unmanaged.passUnretained(event)
}

Expand Down
10 changes: 10 additions & 0 deletions scripts/install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,16 @@ trap 'rm -rf "$TMP"' EXIT
dim "→ downloading ${ASSET}..."
curl -fsSL "$URL" -o "$TMP/${ASSET}"

# The release workflow publishes ${ASSET}.sha256 alongside the archive.
# Verify it: this script installs an unsigned binary and strips its
# quarantine flag, so the checksum is the only integrity check there is.
dim "→ verifying checksum..."
curl -fsSL "${URL}.sha256" -o "$TMP/${ASSET}.sha256"
( cd "$TMP" && shasum -a 256 -c "${ASSET}.sha256" >/dev/null ) || {
red "checksum mismatch for ${ASSET} — refusing to install"
exit 1
}

dim "→ extracting..."
tar -xzf "$TMP/${ASSET}" -C "$TMP"

Expand Down