Skip to content

Conversation

andyleejordan
Copy link
Member

@andyleejordan andyleejordan commented Jul 19, 2025

A new render implementation for use under screen readers that avoids redrawing the terminal buffer so as to avoid unnecessary and confusing output.

The differential rendering relies on calculating the common prefix of the buffer and previousRender strings. Nearly all necessary changes are consolidated in the new rendering function.

Features known not to be supported:

  • Colors: as this necessitates redrawing to insert color sequences after the input is received and the AST parsed.
  • Inline predictions: as this by definition changes the suffix and thus requires endless redrawing.
  • List view predictions: since the render implementation never calls into the prediction engine, this is not available either.
  • Menu completion: well, it "works" since it's not disabled and does its own rendering, but no effort has been made to improve DrawMenu(), so it's not accessible (and I'm not sure it could be given our current constraints).

Features known to be partially supported:

  • Text selection: mark and select commands work as intended, but provide no visual indication.
  • Multiple lines: as in newlines work fine, but there is no continuation prompt.
  • Visually wrapped lines: editing above a wrapped line redraws all subsequent lines and hence is noisy.
  • Status prompt based commands: what-is-key, digit-argument, and most notably, forward/backward incremental history search all render a "status prompt" on the line below the user's input buffer. This is supported; however, it can be noisy since it necessarily has to render the whole buffer when the input buffer changes, including the status prompt (and search text). But what it's reading is almost always going to be relevant.

Everything else should generally work, even Vi mode, and the tests pass. That said, this isn't perfect, and moreover the approach specifically doesn't attempt to enable things from the ground up. There may be features that are available but turn out not to be accessible (like MenuComplete) and I believe they should be left as-is.

Specifically tested with NVDA on Windows and VoiceOver on macOS within VS Code's integrated terminal, with shell integration loaded, and Code's screen reader optimizations enabled.

Is a start to resolving #2504.

@andyleejordan andyleejordan force-pushed the screenreader branch 4 times, most recently from 8b8a080 to 9ba544b Compare July 23, 2025 17:56
@andyleejordan andyleejordan changed the title WIP: Prototype screen reader support Prototype screen reader support Jul 23, 2025
@andyleejordan andyleejordan marked this pull request as ready for review July 23, 2025 18:39
@andyleejordan andyleejordan requested a review from Copilot July 23, 2025 18:58
Copy link

@Copilot Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR introduces prototype screen reader support by implementing a SafeRender abstraction that uses ANSI control codes when screen reader mode is enabled. The changes modify rendering behavior throughout the application to provide better accessibility for screen reader users.

Key changes:

  • Introduces SafeRender method that outputs ANSI escape sequences instead of full re-rendering when screen reader support is enabled
  • Adds screen reader detection for Windows (including Narrator) and macOS VoiceOver
  • Replaces many Render() calls with SafeRender() calls across editing operations, undo/redo, history, and completion functionality

Reviewed Changes

Copilot reviewed 13 out of 13 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
PSReadLine/Accessibility.cs Enhanced screen reader detection for Windows and macOS platforms
PSReadLine/Cmdlets.cs Added ScreenReader option with automatic detection as default
PSReadLine/Options.cs Added logic to disable predictions when screen reader is enabled
PSReadLine/Render.cs Implemented SafeRender method using ANSI control codes
PSReadLine/UndoRedo.cs Replaced Render calls with SafeRender for undo/redo operations
PSReadLine/PublicAPI.cs Updated Insert and Replace methods to use SafeRender
PSReadLine/BasicEditing.cs Modified editing operations to use SafeRender with appropriate ANSI codes
PSReadLine/History.cs Updated history navigation and search to use SafeRender
PSReadLine/KillYank.cs Modified kill operations to use SafeRender
PSReadLine/Completion.cs Added TODO for menu completion screen reader testing
PSReadLine/KeyBindings.cs Added TODO for WhatIsKey screen reader evaluation
PSReadLine/ReadLine.cs Added TODOs for screen reader evaluation in various render calls
PSReadLine/PlatformWindows.cs Added IsMutexPresent method for detecting Windows Narrator

Copy link
Member

@daxian-dbw daxian-dbw left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review in progress. Need to stop here today and want to share my comments so far.

@andyleejordan andyleejordan force-pushed the screenreader branch 7 times, most recently from 0f93014 to 9090264 Compare August 5, 2025 18:28
@andyleejordan andyleejordan changed the title Prototype screen reader support Add screen reader support Aug 5, 2025
@andyleejordan andyleejordan added Issue-Enhancement It's a feature request. Area-Accessibility Label for issues related to accessibility problems or improvements labels Aug 5, 2025
@andyleejordan andyleejordan force-pushed the screenreader branch 4 times, most recently from f606bf4 to 02914fe Compare August 16, 2025 00:39
@andyleejordan
Copy link
Member Author

When this is merged, please rebase or merge, not squash 🙇

@daxian-dbw
Copy link
Member

daxian-dbw commented Aug 19, 2025

When this is merged, please rebase or merge, not squash 🙇

@andyleejordan Why not squash? The commit history is not that clean (has changes reverted) and also this repo always do squash merge (like in PowerShell repo).

@andyleejordan
Copy link
Member Author

When this is merged, please rebase or merge, not squash 🙇

@andyleejordan Why not squash? The commit history is not that clean (has changes reverted) and also this repo always do squash merge (like in PowerShell repo).

Because one single commit is going to be way too big, and the commits are clean, where reverts/changes logically follow new information. (Such as testing the DLL-loaded approach for screen readers.)

@andyleejordan andyleejordan force-pushed the screenreader branch 2 times, most recently from ee18645 to fb7d377 Compare August 22, 2025 23:01
@daxian-dbw
Copy link
Member

daxian-dbw commented Aug 25, 2025

@andyleejordan can you please simply push new changes instead of force pushing all commits? It makes it hard for me to continue the view as I cannot view the diffs between a commit that I have reviewed and the subsequent new changes.

Because one single commit is going to be way too big, and the commits are clean, where reverts/changes logically follow new information. (Such as testing the DLL-loaded approach for screen readers.)

If the cost of keeping the commit history clean is to always force push all commits, then I'm afraid I disagree this is the way to go :) Since the PR already keeps all information, I think a squash merge should be fine.

This character was always meant to be an ellipsis. I'm unsure exactly
why Visual Studio interprets it so, but VS Code and GitHub do not. This
commit replaces it with the actual Unicode character which ensures the
tests continue to pass when these files are edited and saved in VS Code.
@andyleejordan
Copy link
Member Author

andyleejordan commented Aug 27, 2025

@daxian-dbw in order to facilitate getting this PR in with your requirement to squash PRs, this is now dependent on the separate but necessary PRs:

I finished up and tested your last requests above, and will rebase on master per this project's documentation after those dependent PRs are merged. Leaving this as a draft so it doesn't accidentally get merged before then. Thanks for your help!

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.
These tests were using a hard-coded continuation prompt (and its length)
which blocks tests for the upcoming screen reader mode.

This is also simply a reasonable refactor, thanks Claude (for trying).
This adds the `EnableScreenReaderMode` command-line switch which
defaults to true if an active screen reader is detected.

In screen reader mode, the existing `ForceRender()` function is replaced
by `RenderForScreenReader()` which uses a differential rendering
approach to minimize extraneous output to the terminal, allowing the use
of screen readers better than ever before.

The differential rendering relies on calculating the common prefix of
the `buffer` and `previousRender` strings. Nearly all necessary changes
are consolidated in the new rendering function.

Features known not to be supported:

* Colors: as this necessitates redrawing to insert color sequences after
  the input is received and the AST parsed.
* Inline predictions: as this by definition changes the suffix and thus
  requires endless redrawing.
* List view predictions: since the render implementation never calls
  into the prediction engine, this is not available either.
* Menu completion: well, it "works" since it's not disabled and does its
  own rendering, but no effort has been made to improve `DrawMenu()`, so
  it's not accessible (and I'm not sure it could be given our current
  constraints).

Features known to be partially supported:

* Text selection: mark and select commands work as intended, but provide
  no visual indication.
* Multiple lines: as in newlines work fine, but there is no continuation
  prompt.
* Visually wrapped lines: editing above a wrapped line redraws all
  subsequent lines and hence is noisy.
* Status prompt based commands: what-is-key, digit-argument, and most
  notably, forward/backward incremental history search all render a
  "status prompt" on the line below the user's input buffer. This _is_
  supported; however, it can be noisy since it necessarily has to render
  the whole buffer when the input buffer changes, including the status
  prompt (and search text). But what it's reading is almost always going
  to be relevant.

Everything else should generally work, even Vi mode, and the tests pass.
That said, this isn't perfect, and moreover the approach specifically
doesn't attempt to enable things from the ground up. There may be
features that are available but turn out not to be accessible (like
`MenuComplete`) and I believe they should be left as-is.

Specifically tested with NVDA on Windows and VoiceOver on macOS within
VS Code's integrated terminal, with shell integration loaded, and Code's
screen reader optimizations enabled.
Tests that depend on supported features such as menu completions, list
view, inline predictions, continuation prompt, and colors are skipped.

The "helper" module for running tests from the command-line and in
AppVeyor is minimally updated to not skip the new fixture.

A `BlankRestOfBuffer()` function implements the semantics of the `0J`
control sequence in the mock console.
Copy link
Member

@daxian-dbw daxian-dbw left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM.

@andyleejordan Regarding PR reviews:

  1. It's most efficient to only push new changes and squash in the end, because for each iteration
    • the author can just focus on addressing feedback.
    • the reviewers can just focus on the new diffs.
  2. Commit history is preserved in the PR, along with all discussions.
  3. This is how we do PR reviews in this repo, in PowerShell repo, and most repos out there I believe.

However, since you've been insisting on it so much, I will merge this PR with a merge node as you expect.

@@ -210,7 +210,14 @@ function Start-TestRun

function RunXunitTestsInNewProcess ([string] $Layout, [string] $OperatingSystem)
{
$filter = "FullyQualifiedName~Test.{0}_{1}" -f ($Layout -replace '-','_'), $OperatingSystem
$filter = if ($Layout) {
Write-Log "Testing $Layout on $OperatingSystem...`n"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This cleaner. Thanks!

@daxian-dbw daxian-dbw merged commit 4e2cb60 into PowerShell:master Aug 28, 2025
2 checks passed
@andyleejordan
Copy link
Member Author

Thanks Dongbo!

@andyleejordan andyleejordan deleted the screenreader branch August 28, 2025 18:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Area-Accessibility Label for issues related to accessibility problems or improvements Issue-Enhancement It's a feature request.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants