diff --git a/PSReadLine/Accessibility.cs b/PSReadLine/Accessibility.cs index 4938da2..aae6c0f 100644 --- a/PSReadLine/Accessibility.cs +++ b/PSReadLine/Accessibility.cs @@ -2,6 +2,7 @@ Copyright (c) Microsoft Corporation. All rights reserved. --********************************************************************/ +using System.Diagnostics; using System.Runtime.InteropServices; namespace Microsoft.PowerShell.Internal @@ -10,14 +11,82 @@ namespace Microsoft.PowerShell.Internal { internal static bool IsScreenReaderActive() { - bool returnValue = false; - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { - PlatformWindows.SystemParametersInfo(PlatformWindows.SPI_GETSCREENREADER, 0, ref returnValue, 0); + return IsAnyWindowsScreenReaderEnabled(); } - return returnValue; + if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + { + return IsVoiceOverEnabled(); + } + + // TODO: Support Linux per https://code.visualstudio.com/docs/configure/accessibility/accessibility + return false; + } + + private static bool IsAnyWindowsScreenReaderEnabled() + { + // The supposedly official way to check for a screen reader on + // Windows is SystemParametersInfo(SPI_GETSCREENREADER, ...) but it + // doesn't detect the in-box Windows Narrator and is otherwise known + // to be problematic. + // + // Unfortunately, the alternative method used by Electron and + // Chromium, where the relevant screen reader libraries (modules) + // are checked for does not work in the context of PowerShell + // because it relies on those applications injecting themselves into + // the app. Which they do not because PowerShell is not a windowed + // app, so we're stuck using the known-to-be-buggy way. + bool spiScreenReader = false; + PlatformWindows.SystemParametersInfo(PlatformWindows.SPI_GETSCREENREADER, 0, ref spiScreenReader, 0); + if (spiScreenReader) + { + return true; + } + + // At least we can correctly check for Windows Narrator using the + // NarratorRunning mutex. Windows Narrator is mostly not broken with + // PSReadLine, not in the way that NVDA and VoiceOver are. + if (PlatformWindows.IsMutexPresent("NarratorRunning")) + { + return true; + } + + return false; + } + + private static bool IsVoiceOverEnabled() + { + try + { + // Use the 'defaults' command to check if VoiceOver is enabled + // This checks the com.apple.universalaccess preference for voiceOverOnOffKey + ProcessStartInfo startInfo = new() + { + FileName = "defaults", + Arguments = "read com.apple.universalaccess voiceOverOnOffKey", + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + CreateNoWindow = true + }; + + using Process process = Process.Start(startInfo); + process.WaitForExit(250); + if (process.HasExited && process.ExitCode == 0) + { + string output = process.StandardOutput.ReadToEnd().Trim(); + // VoiceOver is enabled if the value is 1 + return output == "1"; + } + } + catch + { + // If we can't determine the status, assume VoiceOver is not enabled + } + + return false; } } } diff --git a/PSReadLine/PlatformWindows.cs b/PSReadLine/PlatformWindows.cs index c7e0313..32cf653 100644 --- a/PSReadLine/PlatformWindows.cs +++ b/PSReadLine/PlatformWindows.cs @@ -79,6 +79,21 @@ static class PlatformWindows [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)] internal static extern IntPtr GetStdHandle(uint handleId); + internal const int ERROR_ALREADY_EXISTS = 0xB7; + + internal static bool IsMutexPresent(string name) + { + try + { + using var mutex = new System.Threading.Mutex(false, name); + return Marshal.GetLastWin32Error() == ERROR_ALREADY_EXISTS; + } + catch + { + return false; + } + } + [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)] static extern bool SetConsoleCtrlHandler(BreakHandler handlerRoutine, bool add);