Skip to content

Commit 4e2cb60

Browse files
authored
Add screen reader support to PSReadLine (#4854)
2 parents ceaad10 + 06e3b4b commit 4e2cb60

23 files changed

+679
-311
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/BasicEditing.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ public static void CancelLine(ConsoleKeyInfo? key = null, object arg = null)
8686
_singleton._current = _singleton._buffer.Length;
8787

8888
using var _ = _singleton._prediction.DisableScoped();
89-
_singleton.ForceRender();
89+
_singleton.Render(force: true);
9090

9191
_singleton._console.Write("\x1b[91m^C\x1b[0m");
9292

@@ -335,7 +335,7 @@ private bool AcceptLineImpl(bool validate)
335335

336336
if (renderNeeded)
337337
{
338-
ForceRender();
338+
Render(force: true);
339339
}
340340

341341
// Only run validation if we haven't before. If we have and status line shows an error,

PSReadLine/Cmdlets.cs

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
using System.Runtime.InteropServices;
1616
using System.Threading;
1717
using Microsoft.PowerShell.PSReadLine;
18+
using Microsoft.PowerShell.Internal;
1819
using AllowNull = System.Management.Automation.AllowNullAttribute;
1920

2021
namespace Microsoft.PowerShell
@@ -150,11 +151,6 @@ public class PSConsoleReadLineOptions
150151

151152
public const HistorySaveStyle DefaultHistorySaveStyle = HistorySaveStyle.SaveIncrementally;
152153

153-
/// <summary>
154-
/// The predictive suggestion feature is disabled by default.
155-
/// </summary>
156-
public const PredictionSource DefaultPredictionSource = PredictionSource.None;
157-
158154
public const PredictionViewStyle DefaultPredictionViewStyle = PredictionViewStyle.InlineView;
159155

160156
/// <summary>
@@ -201,6 +197,7 @@ public PSConsoleReadLineOptions(string hostName, bool usingLegacyConsole)
201197
{
202198
ResetColors();
203199
EditMode = DefaultEditMode;
200+
ScreenReaderModeEnabled = Accessibility.IsScreenReaderActive();
204201
ContinuationPrompt = DefaultContinuationPrompt;
205202
ContinuationPromptColor = Console.ForegroundColor;
206203
ExtraPromptLineCount = DefaultExtraPromptLineCount;
@@ -533,6 +530,8 @@ public object ListPredictionTooltipColor
533530

534531
public bool TerminateOrphanedConsoleApps { get; set; }
535532

533+
public bool ScreenReaderModeEnabled { get; set; }
534+
536535
internal string _defaultTokenColor;
537536
internal string _commentColor;
538537
internal string _keywordColor;
@@ -847,6 +846,14 @@ public SwitchParameter TerminateOrphanedConsoleApps
847846
}
848847
internal SwitchParameter? _terminateOrphanedConsoleApps;
849848

849+
[Parameter]
850+
public SwitchParameter EnableScreenReaderMode
851+
{
852+
get => _enableScreenReaderMode.GetValueOrDefault();
853+
set => _enableScreenReaderMode = value;
854+
}
855+
internal SwitchParameter? _enableScreenReaderMode;
856+
850857
[ExcludeFromCodeCoverage]
851858
protected override void EndProcessing()
852859
{

PSReadLine/Options.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,10 @@ private void SetOptionsInternal(SetPSReadLineOption options)
185185
nameof(Options.TerminateOrphanedConsoleApps)));
186186
}
187187
}
188+
if (options._enableScreenReaderMode.HasValue)
189+
{
190+
Options.ScreenReaderModeEnabled = options.EnableScreenReaderMode;
191+
}
188192
}
189193

190194
private void SetKeyHandlerInternal(string[] keys, Action<ConsoleKeyInfo?, object> handler, string briefDescription, string longDescription, ScriptBlock scriptBlock)

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)