diff --git a/.github/workflows/resolutionFix.ts b/.github/workflows/resolutionFix.ts index 4c5dcee..0cbbbaa 100644 --- a/.github/workflows/resolutionFix.ts +++ b/.github/workflows/resolutionFix.ts @@ -3,7 +3,8 @@ import { execSync } from "child_process"; if (process.platform === "darwin") { try { execSync( - `"/Library/Application Support/VMware Tools/vmware-resolutionSet" 1920 1080` + `"/Library/Application Support/VMware Tools/vmware-resolutionSet" 1920 1080`, + { encoding: "utf8" } ); } catch (_) { // swallow diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index ab27f3b..5e9bed6 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -13,7 +13,17 @@ jobs: runs-on: ${{ matrix.os }} strategy: matrix: - os: [macos-14, macos-15, macos-26, windows-2022, windows-2025] + os: + [ + macos-14, + macos-15, + macos-15-intel, + macos-26, + macos-26-intel, + windows-2022, + windows-2025, + windows-11-arm, + ] steps: - uses: actions/checkout@v5 - uses: actions/setup-node@v6 @@ -31,7 +41,7 @@ jobs: runs-on: ${{ matrix.os }} strategy: matrix: - os: [macos-14] + os: [macos-latest] steps: - uses: actions/checkout@v5 - uses: actions/setup-node@v6 @@ -49,7 +59,7 @@ jobs: runs-on: ${{ matrix.os }} strategy: matrix: - os: [windows-2022, windows-2025] + os: [windows-2022, windows-2025, windows-11-arm] steps: - uses: actions/checkout@v5 - uses: actions/setup-node@v6 diff --git a/src/errors.ts b/src/errors.ts index feece95..edf7732 100644 --- a/src/errors.ts +++ b/src/errors.ts @@ -7,6 +7,10 @@ export const ERR_MACOS_UNSUPPORTED_VERSION = "Require macOS version 11 or later"; export const ERR_MACOS_UNABLE_UPDATE_SYSTEM_DEFAULTS = "Unable to update system defaults"; +export const ERR_MACOS_UNABLE_DISABLE_NOTIFICATION_CENTER = + "Unable to disable the notification center"; +export const ERR_MACOS_UNABLE_UPDATE_VOICE_OVER_SANDBOXED_DEFAULTS = + "Unable to update VoiceOver sandboxed defaults - SIP might not be disabled"; export const ERR_MACOS_UNABLE_TO_VERIFY_SIP = "Unable to verify macOS SIP status"; export const ERR_MACOS_UNABLE_TO_WRITE_DATABASE_FILE = diff --git a/src/macOS/disableDictationInputAutoEnable.ts b/src/macOS/disableDictationInputAutoEnable.ts index fee3235..e584799 100644 --- a/src/macOS/disableDictationInputAutoEnable.ts +++ b/src/macOS/disableDictationInputAutoEnable.ts @@ -4,9 +4,12 @@ import { ERR_MACOS_UNABLE_UPDATE_SYSTEM_DEFAULTS } from "../errors"; export function disableDictationInputAutoEnable(): void { try { execSync( - "defaults write com.apple.HIToolbox AppleDictationAutoEnable -bool false" + "defaults write com.apple.HIToolbox AppleDictationAutoEnable -bool false", + { encoding: "utf8" } ); } catch (e) { - throw new Error(`${ERR_MACOS_UNABLE_UPDATE_SYSTEM_DEFAULTS}\n\n${e.message}`); + throw new Error( + `${ERR_MACOS_UNABLE_UPDATE_SYSTEM_DEFAULTS}\n\n${e.message}` + ); } } diff --git a/src/macOS/disableNotificationCenter.ts b/src/macOS/disableNotificationCenter.ts new file mode 100644 index 0000000..1578744 --- /dev/null +++ b/src/macOS/disableNotificationCenter.ts @@ -0,0 +1,15 @@ +import { execSync } from "child_process"; +import { ERR_MACOS_UNABLE_DISABLE_NOTIFICATION_CENTER } from "../errors"; + +export function disableNotificationCenter(): void { + try { + execSync( + "launchctl unload -w /System/Library/LaunchAgents/com.apple.notificationcenterui.plist", + { encoding: "utf8" }, + ); + } catch (e) { + throw new Error( + `${ERR_MACOS_UNABLE_DISABLE_NOTIFICATION_CENTER}\n\n${e.message}`, + ); + } +} diff --git a/src/macOS/disableSplashScreenSystemDefaults.ts b/src/macOS/disableSplashScreenSystemDefaults.ts index 5800be8..80ee8ff 100644 --- a/src/macOS/disableSplashScreenSystemDefaults.ts +++ b/src/macOS/disableSplashScreenSystemDefaults.ts @@ -4,9 +4,12 @@ import { ERR_MACOS_UNABLE_UPDATE_SYSTEM_DEFAULTS } from "../errors"; export function disableSplashScreenSystemDefaults(): void { try { execSync( - "defaults write com.apple.VoiceOverTraining doNotShowSplashScreen -bool true" + "defaults write com.apple.VoiceOverTraining doNotShowSplashScreen -bool true", + { encoding: "utf8" } ); } catch (e) { - throw new Error(`${ERR_MACOS_UNABLE_UPDATE_SYSTEM_DEFAULTS}\n\n${e.message}`); + throw new Error( + `${ERR_MACOS_UNABLE_UPDATE_SYSTEM_DEFAULTS}\n\n${e.message}` + ); } } diff --git a/src/macOS/enableAppleScriptControlSystemDefaults.ts b/src/macOS/enableAppleScriptControlSystemDefaults.ts index 29fc4ba..c2a64e5 100644 --- a/src/macOS/enableAppleScriptControlSystemDefaults.ts +++ b/src/macOS/enableAppleScriptControlSystemDefaults.ts @@ -1,12 +1,17 @@ import { execSync } from "child_process"; import { ERR_MACOS_UNABLE_UPDATE_SYSTEM_DEFAULTS } from "../errors"; +const VOICE_OVER_APPLESCRIPT_ENABLED_DEFAULTS = + "defaults write com.apple.VoiceOver4/default SCREnableAppleScript -bool true"; + export function enableAppleScriptControlSystemDefaults(): void { try { - execSync( - "defaults write com.apple.VoiceOver4/default SCREnableAppleScript -bool true" - ); + execSync(VOICE_OVER_APPLESCRIPT_ENABLED_DEFAULTS, { encoding: "utf8" }); + + return; } catch (e) { - throw new Error(`${ERR_MACOS_UNABLE_UPDATE_SYSTEM_DEFAULTS}\n\n${e.message}`); + throw new Error( + `${ERR_MACOS_UNABLE_UPDATE_SYSTEM_DEFAULTS}\n\n${e.message}`, + ); } } diff --git a/src/macOS/enableDoNotDisturb.ts b/src/macOS/enableDoNotDisturb.ts index 676d654..b9e791f 100644 --- a/src/macOS/enableDoNotDisturb.ts +++ b/src/macOS/enableDoNotDisturb.ts @@ -1,9 +1,9 @@ import { exec } from "child_process"; -import * as os from "os"; +import { promisify } from "util"; import { ERR_MACOS_FAILED_TO_ENABLE_DO_NOT_DISTURB } from "../errors"; import { runAppleScript } from "./runAppleScript"; -import { promisify } from "util"; import { retryOnError } from "./retryOnError"; +import { getPlatformVersionMajor } from "./getPlatformVersionMajor"; // REF: https://github.com/sindresorhus/do-not-disturb/issues/9 const enableFocusModeShellscript = `defaults write com.apple.ncprefs.plist dnd_prefs -data 62706C6973743030D60102030405060708080A08085B646E644D6972726F7265645F100F646E64446973706C6179536C6565705F101E72657065617465644661636574696D6543616C6C73427265616B73444E445875736572507265665E646E64446973706C61794C6F636B5F10136661636574696D6543616E427265616B444E44090808D30B0C0D070F1057656E61626C6564546461746556726561736F6E093341C2B41C4FC9D3891001080808152133545D6C828384858C9499A0A1AAACAD00000000000001010000000000000013000000000000000000000000000000AE && killall usernoted && killall ControlCenter`; @@ -39,7 +39,7 @@ my withTimeout(command, timeoutSeconds) const enableFocusModeVenturaAppleScript = ( locale: string, - platformMajor: number + platformMajor: number, ) => { // TODO: attempt to rewrite scripts without any locale specific instructions // so this setup can be used regardless of user locale preferences. @@ -97,7 +97,7 @@ my withTimeout(command, timeoutSeconds) }; export async function enableDoNotDisturb() { - const platformMajor = Number(os.version().split("Version ")[1].split(".")[0]); + const platformMajor = getPlatformVersionMajor(); try { if (platformMajor <= 20) { @@ -110,12 +110,14 @@ export async function enableDoNotDisturb() { // From MacOS 13 Ventura (Darwin 22) there is no known way to enable DND via system settings await retryOnError(() => - runAppleScript(enableFocusModeVenturaAppleScript(locale, platformMajor)) + runAppleScript( + enableFocusModeVenturaAppleScript(locale, platformMajor), + ), ); } } catch (e) { throw new Error( - `${ERR_MACOS_FAILED_TO_ENABLE_DO_NOT_DISTURB}\n\n${e.message}` + `${ERR_MACOS_FAILED_TO_ENABLE_DO_NOT_DISTURB}\n\n${e.message}`, ); } } diff --git a/src/macOS/getPlatformVersionMajor.ts b/src/macOS/getPlatformVersionMajor.ts new file mode 100644 index 0000000..2c3097b --- /dev/null +++ b/src/macOS/getPlatformVersionMajor.ts @@ -0,0 +1,4 @@ +import { version } from "os"; + +export const getPlatformVersionMajor = () => + Number(version().split("Version ")[1].split(".")[0]); diff --git a/src/macOS/isAppleScriptControlEnabled/enabledDefaults.ts b/src/macOS/isAppleScriptControlEnabled/enabledDefaults.ts index 92191e5..1ff18b6 100644 --- a/src/macOS/isAppleScriptControlEnabled/enabledDefaults.ts +++ b/src/macOS/isAppleScriptControlEnabled/enabledDefaults.ts @@ -1,16 +1,16 @@ -import { exec } from "child_process"; +import { execSync } from "child_process"; const VOICE_OVER_APPLESCRIPT_ENABLED_DEFAULTS = "defaults read com.apple.VoiceOver4/default SCREnableAppleScript"; -export async function enabledDefaults(): Promise { - return await new Promise((resolve) => { - exec(VOICE_OVER_APPLESCRIPT_ENABLED_DEFAULTS, (err, stdout) => { - if (err) { - resolve(false); - } else { - resolve(stdout.trim() === "1"); - } +export function enabledDefaults(): boolean { + try { + const result = execSync(VOICE_OVER_APPLESCRIPT_ENABLED_DEFAULTS, { + encoding: "utf8", }); - }); + + return result.trim() === "1"; + } catch { + return false; + } } diff --git a/src/macOS/setup.ts b/src/macOS/setup.ts index a7b44ad..9604f7e 100644 --- a/src/macOS/setup.ts +++ b/src/macOS/setup.ts @@ -12,6 +12,7 @@ import { isAppleScriptControlEnabled } from "./isAppleScriptControlEnabled"; import { handleWarning, logInfo } from "../logging"; import { ERR_MACOS_REQUIRES_MANUAL_USER_INTERACTION } from "../errors"; import { enableDoNotDisturb } from "./enableDoNotDisturb"; +import { disableNotificationCenter } from "./disableNotificationCenter"; import { enabledDbFile } from "./isAppleScriptControlEnabled/enabledDbFile"; const isCi = process.argv.includes("--ci"); @@ -36,7 +37,7 @@ export async function setup(): Promise { } else { handleWarning( "Ignoring TCC.db updates", - "If the necessary permissions have not been granted by other means, using this flag may result in your environment not being set up for reliable screen reader automation." + "If the necessary permissions have not been granted by other means, using this flag may result in your environment not being set up for reliable screen reader automation.", ); } @@ -45,7 +46,7 @@ export async function setup(): Promise { const stopRecording = isRecorded ? macOSRecord( - `./recordings/macos-guidepup-setup-${osName}-${osVersion}-${+new Date()}.mov` + `./recordings/macos-guidepup-setup-${osName}-${osVersion}-${+new Date()}.mov`, ) : () => null; @@ -57,6 +58,7 @@ export async function setup(): Promise { if (isCi) { await enableDoNotDisturb(); + await disableNotificationCenter(); } if (!isSipEnabled() && !(await enabledDbFile())) { @@ -77,9 +79,9 @@ export async function setup(): Promise { "Please complete remaining setup by following this guide:\n\n--> " + chalk.underline( chalk.bold( - "https://www.guidepup.dev/docs/guides/manual-voiceover-setup" - ) - ) + "https://www.guidepup.dev/docs/guides/manual-voiceover-setup", + ), + ), ); } finally { stopRecording(); diff --git a/src/macOS/updateTccDb.ts b/src/macOS/updateTccDb.ts index 389b0ff..85ba72c 100644 --- a/src/macOS/updateTccDb.ts +++ b/src/macOS/updateTccDb.ts @@ -194,7 +194,9 @@ export function updateTccDb(path: string): void { });`; try { - execSync(`sqlite3 "${path}" "${query}" >/dev/null 2>&1`); + execSync(`sqlite3 "${path}" "${query}" >/dev/null 2>&1`, { + encoding: "utf8", + }); } catch (e) { throw new Error( `${ERR_MACOS_UNABLE_TO_WRITE_USER_TCC_DB}\n\n${e.message}`,