Files
Andy Jordan 5e72587cca Improve IsScreenReaderActive()
This supports checking for the built-in screen readers VoiceOver on
macOS and Windows Narrator, as well as the popular open-source option,
NVDA.

The VoiceOver check spawns a quick `defaults` process since in .NET
using the macOS events is difficult, but this is quick and easy.

The Windows Narrator check inspects a system mutex. Notably though this
screen reader handles re-rendering better than others.

The check for NVDA et. al. inspects the system parameter information.
While this approach is known to be buggy, the preferable and commonly
used algorithm (as implemented by Electron) which checks for loaded
libraries was tested and found to be unsupported for a non-windowed
program like PowerShell.

It's unknown if the SPI check will detect JAWS, Window-Eyes, or
ZoomText, so a command-line option for the upcoming screen reader mode
should also be provided.

Linux is not yet supported.
2025-08-27 16:51:48 -07:00

93 lines
3.6 KiB
C#

/********************************************************************++
Copyright (c) Microsoft Corporation. All rights reserved.
--********************************************************************/
using System.Diagnostics;
using System.Runtime.InteropServices;
namespace Microsoft.PowerShell.Internal
{
internal class Accessibility
{
internal static bool IsScreenReaderActive()
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
return IsAnyWindowsScreenReaderEnabled();
}
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;
}
}
}