Skip to content

Commit df27716

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 ceaad10 commit df27716

File tree

2 files changed

+90
-4
lines changed

2 files changed

+90
-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: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
using System.Diagnostics;
99
using System.Linq;
1010
using System.Runtime.InteropServices;
11+
using System.Threading;
1112
using Microsoft.PowerShell;
1213
using Microsoft.PowerShell.Internal;
1314
using Microsoft.Win32.SafeHandles;
@@ -79,6 +80,22 @@ IntPtr templateFileWin32Handle
7980
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
8081
internal static extern IntPtr GetStdHandle(uint handleId);
8182

83+
internal const int ERROR_ALREADY_EXISTS = 0xB7;
84+
85+
internal static bool IsMutexPresent(string name)
86+
{
87+
try
88+
{
89+
if (Mutex.TryOpenExisting(name, out var tempMutex))
90+
{
91+
tempMutex.Dispose();
92+
return true;
93+
}
94+
}
95+
catch { }
96+
return false;
97+
}
98+
8299
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
83100
static extern bool SetConsoleCtrlHandler(BreakHandler handlerRoutine, bool add);
84101

0 commit comments

Comments
 (0)