diff --git a/lib/commands/install.js b/lib/commands/install.js index aaec388..4ba182d 100644 --- a/lib/commands/install.js +++ b/lib/commands/install.js @@ -5,11 +5,17 @@ const { loadOsModule } = require('../helpers/load-os-module'); module.exports = /** @type {import('yargs').CommandModule} */ ({ command: 'install', describe: 'Install text to speech extension and other support', - async handler() { + builder(yargs) { + return yargs.option('unattended', { + desc: 'Fail if installation requires human intervention', + boolean: true, + }); + }, + async handler({ unattended }) { const installDelegate = loadOsModule('install', { darwin: () => require('../install/macos'), win32: () => require('../install/win32'), }); - await installDelegate.install(); + await installDelegate.install({ unattended }); }, }); diff --git a/lib/install/macos.js b/lib/install/macos.js index efbff4d..5637f4e 100644 --- a/lib/install/macos.js +++ b/lib/install/macos.js @@ -3,6 +3,7 @@ const { exec: _exec } = require('child_process'); const { resolve } = require('path'); const { promisify } = require('util'); +const { 'interaction.pressKeys': pressKeys } = require('../modules/macos/interaction'); const LSREGISTER_EXECUTABLE_PATH = '/System/Library/Frameworks/CoreServices.framework/Versions/Current/Frameworks/LaunchServices.framework/Versions/Current/Support/lsregister'; @@ -22,15 +23,60 @@ const SYSTEM_VOICE_IDENTIFIER = 'com.apple.Fred'; const PLUGIN_TRIPLET_IDENTIFIER = 'ausp atdg BOCU'; const exec = promisify(_exec); +const enableKeyAutomationPrompt = `This tool can only be installed on systems which allow automated key pressing. +Please allow the Terminal application to control your computer (the setting is +controlled in System Settings > Privacy & Security > Accessibility).`; /** @typedef {import('child_process').ExecOptions} ExecOptions */ /** + * Prompt the user to press any key. Resolves when the user presses a key. + * * @returns {Promise} */ -exports.install = async function () { +const promptForManualKeyPress = async () => { + process.stdout.write('Press any key to continue... '); + const wasRaw = process.stdin.isRaw; + process.stdin.setRawMode(true); + process.stdin.resume(); + const byteArray = await new Promise(resolve => { + process.stdin.once('data', data => resolve(Array.from(data))); + }); + + process.stdin.pause(); + process.stdin.setRawMode(wasRaw); + process.stdout.write('\n'); + + // Honor "Control + C" motion by exiting. + if (byteArray[0] === 3) { + process.exit(1); + } +}; + +/** + * @param {object} options + * @param {boolean} options.unattended - Whether installation should fail if + * human intervention is required + * + * @returns {Promise} + */ +exports.install = async function ({ unattended }) { const options = await getExecOptions(); + if (!(await canPressKeys())) { + if (unattended) { + throw new Error('The system cannot automate key pressing.'); + } else { + console.error(enableKeyAutomationPrompt); + + await promptForManualKeyPress(); + + if (!(await canPressKeys())) { + throw new Error('The system cannot automate key pressing.'); + } + } + } + if (await isInstalled()) { throw new Error('Already installed'); } @@ -55,6 +101,21 @@ exports.uninstall = async function () { await unregisterExtensions(options); }; +/** + * Experimentally determine whether the current system supports automated key + * pressing by attempting to press an arbitrary key. + * + * @returns {Promise} + */ +const canPressKeys = async () => { + try { + await pressKeys(null, { keys: ['shift'] }); + } catch ({}) { + return false; + } + return true; +}; + const isInstalled = async function () { const { stdout } = await exec(`auval -v ${PLUGIN_TRIPLET_IDENTIFIER}`); return /ATDriverGenericMacOSExtension/.test(stdout);