Skip to content

Commit 5e72587

Browse files
committed
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.
1 parent 4907116 commit 5e72587

File tree

2 files changed

+88
-4
lines changed

2 files changed

+88
-4
lines changed

PSReadLine/Accessibility.cs

Lines changed: 73 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
Copyright (c) Microsoft Corporation. All rights reserved.
33
--********************************************************************/
44

5+
using System.Diagnostics;
56
using System.Runtime.InteropServices;
67

78
namespace Microsoft.PowerShell.Internal
@@ -10,14 +11,82 @@ internal class Accessibility
1011
{
1112
internal static bool IsScreenReaderActive()
1213
{
13-
bool returnValue = false;
14-
1514
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
1615
{
17-
PlatformWindows.SystemParametersInfo(PlatformWindows.SPI_GETSCREENREADER, 0, ref returnValue, 0);
16+
return IsAnyWindowsScreenReaderEnabled();
17+
}
18+
19+
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
20+
{
21+
return IsVoiceOverEnabled();
22+
}
23+
24+
// TODO: Support Linux per https://code.visualstudio.com/docs/configure/accessibility/accessibility
25+
return false;
26+
}
27+
28+
private static bool IsAnyWindowsScreenReaderEnabled()
29+
{
30+
// The supposedly official way to check for a screen reader on
31+
// Windows is SystemParametersInfo(SPI_GETSCREENREADER, ...) but it
32+
// doesn't detect the in-box Windows Narrator and is otherwise known
33+
// to be problematic.
34+
//
35+
// Unfortunately, the alternative method used by Electron and
36+
// Chromium, where the relevant screen reader libraries (modules)
37+
// are checked for does not work in the context of PowerShell
38+
// because it relies on those applications injecting themselves into
39+
// the app. Which they do not because PowerShell is not a windowed
40+
// app, so we're stuck using the known-to-be-buggy way.
41+
bool spiScreenReader = false;
42+
PlatformWindows.SystemParametersInfo(PlatformWindows.SPI_GETSCREENREADER, 0, ref spiScreenReader, 0);
43+
if (spiScreenReader)
44+
{
45+
return true;
46+
}
47+
48+
// At least we can correctly check for Windows Narrator using the
49+
// NarratorRunning mutex. Windows Narrator is mostly not broken with
50+
// PSReadLine, not in the way that NVDA and VoiceOver are.
51+
if (PlatformWindows.IsMutexPresent("NarratorRunning"))
52+
{
53+
return true;
54+
}
55+
56+
return false;
57+
}
58+
59+
private static bool IsVoiceOverEnabled()
60+
{
61+
try
62+
{
63+
// Use the 'defaults' command to check if VoiceOver is enabled
64+
// This checks the com.apple.universalaccess preference for voiceOverOnOffKey
65+
ProcessStartInfo startInfo = new()
66+
{
67+
FileName = "defaults",
68+
Arguments = "read com.apple.universalaccess voiceOverOnOffKey",
69+
UseShellExecute = false,
70+
RedirectStandardOutput = true,
71+
RedirectStandardError = true,
72+
CreateNoWindow = true
73+
};
74+
75+
using Process process = Process.Start(startInfo);
76+
process.WaitForExit(250);
77+
if (process.HasExited && process.ExitCode == 0)
78+
{
79+
string output = process.StandardOutput.ReadToEnd().Trim();
80+
// VoiceOver is enabled if the value is 1
81+
return output == "1";
82+
}
83+
}
84+
catch
85+
{
86+
// If we can't determine the status, assume VoiceOver is not enabled
1887
}
1988

20-
return returnValue;
89+
return false;
2190
}
2291
}
2392
}

PSReadLine/PlatformWindows.cs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,21 @@ IntPtr templateFileWin32Handle
7979
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
8080
internal static extern IntPtr GetStdHandle(uint handleId);
8181

82+
internal const int ERROR_ALREADY_EXISTS = 0xB7;
83+
84+
internal static bool IsMutexPresent(string name)
85+
{
86+
try
87+
{
88+
using var mutex = new System.Threading.Mutex(false, name);
89+
return Marshal.GetLastWin32Error() == ERROR_ALREADY_EXISTS;
90+
}
91+
catch
92+
{
93+
return false;
94+
}
95+
}
96+
8297
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
8398
static extern bool SetConsoleCtrlHandler(BreakHandler handlerRoutine, bool add);
8499

0 commit comments

Comments
 (0)