From 06c70c84d887b49a1e96f90cbfe0c9e46a8f667b Mon Sep 17 00:00:00 2001
From: Andy Jordan <2226434+andyleejordan@users.noreply.github.com>
Date: Wed, 2 Jul 2025 16:40:23 -0700
Subject: [PATCH 1/7] Add `ScreenReader` support CLI option
That should default to enabled when one is detected on startup,
but also allows the support to be forcibly enabled.
---
PSReadLine/Accessibility.cs | 3 ++-
PSReadLine/Cmdlets.cs | 17 +++++++++++++++++
PSReadLine/Options.cs | 4 ++++
3 files changed, 23 insertions(+), 1 deletion(-)
diff --git a/PSReadLine/Accessibility.cs b/PSReadLine/Accessibility.cs
index 4938da233..c1dfe897a 100644
--- a/PSReadLine/Accessibility.cs
+++ b/PSReadLine/Accessibility.cs
@@ -14,8 +14,9 @@ internal static bool IsScreenReaderActive()
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
+ // NOTE: This API can detect if a third-party screen reader is active, such as NVDA, but not the in-box Windows Narrator.
PlatformWindows.SystemParametersInfo(PlatformWindows.SPI_GETSCREENREADER, 0, ref returnValue, 0);
- }
+ } // TODO: Support other platforms per https://code.visualstudio.com/docs/configure/accessibility/accessibility
return returnValue;
}
diff --git a/PSReadLine/Cmdlets.cs b/PSReadLine/Cmdlets.cs
index 7fecf4b4e..af4d633f5 100644
--- a/PSReadLine/Cmdlets.cs
+++ b/PSReadLine/Cmdlets.cs
@@ -15,6 +15,7 @@
using System.Runtime.InteropServices;
using System.Threading;
using Microsoft.PowerShell.PSReadLine;
+using Microsoft.PowerShell.Internal;
using AllowNull = System.Management.Automation.AllowNullAttribute;
namespace Microsoft.PowerShell
@@ -163,6 +164,11 @@ public class PSConsoleReadLineOptions
///
public const int DefaultAnsiEscapeTimeout = 100;
+ ///
+ /// If screen reader support is enabled, which enables safe rendering using ANSI control codes.
+ ///
+ public static readonly bool DefaultScreenReader = Accessibility.IsScreenReaderActive();
+
static PSConsoleReadLineOptions()
{
// For inline-view suggestion text, we use the new FG color 'dim white italic' when possible, because it provides
@@ -224,6 +230,7 @@ public PSConsoleReadLineOptions(string hostName, bool usingLegacyConsole)
: PredictionSource.HistoryAndPlugin;
PredictionViewStyle = DefaultPredictionViewStyle;
MaximumHistoryCount = 0;
+ ScreenReader = DefaultScreenReader;
var historyFileName = hostName + "_history.txt";
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
@@ -533,6 +540,8 @@ public object ListPredictionTooltipColor
public bool TerminateOrphanedConsoleApps { get; set; }
+ public bool ScreenReader { get; set; }
+
internal string _defaultTokenColor;
internal string _commentColor;
internal string _keywordColor;
@@ -847,6 +856,14 @@ public SwitchParameter TerminateOrphanedConsoleApps
}
internal SwitchParameter? _terminateOrphanedConsoleApps;
+ [Parameter]
+ public SwitchParameter ScreenReader
+ {
+ get => _screenReader.GetValueOrDefault();
+ set => _screenReader = value;
+ }
+ internal SwitchParameter? _screenReader;
+
[ExcludeFromCodeCoverage]
protected override void EndProcessing()
{
diff --git a/PSReadLine/Options.cs b/PSReadLine/Options.cs
index 7485154b4..b8b3efad5 100644
--- a/PSReadLine/Options.cs
+++ b/PSReadLine/Options.cs
@@ -185,6 +185,10 @@ private void SetOptionsInternal(SetPSReadLineOption options)
nameof(Options.TerminateOrphanedConsoleApps)));
}
}
+ if (options._screenReader.HasValue)
+ {
+ Options.ScreenReader = options.ScreenReader;
+ }
}
private void SetKeyHandlerInternal(string[] keys, Action handler, string briefDescription, string longDescription, ScriptBlock scriptBlock)
From 4baae08fb7e6b5ae371a0fbfcdb1fb050ce0a2e2 Mon Sep 17 00:00:00 2001
From: Andy Jordan <2226434+andyleejordan@users.noreply.github.com>
Date: Wed, 2 Jul 2025 18:57:12 -0700
Subject: [PATCH 2/7] Improve screen reader detection on Windows by checking
modules
Borrows the "better" algorithm from Electron, with attribution.
---
PSReadLine/Accessibility.cs | 40 +++++++++++++++++++++++++++++------
PSReadLine/PlatformWindows.cs | 37 ++++++++++++++++++++++++++++++++
2 files changed, 71 insertions(+), 6 deletions(-)
diff --git a/PSReadLine/Accessibility.cs b/PSReadLine/Accessibility.cs
index c1dfe897a..fa000555b 100644
--- a/PSReadLine/Accessibility.cs
+++ b/PSReadLine/Accessibility.cs
@@ -10,15 +10,43 @@ internal class Accessibility
{
internal static bool IsScreenReaderActive()
{
- bool returnValue = false;
+ // TODO: Support other platforms per https://code.visualstudio.com/docs/configure/accessibility/accessibility
+ if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
+ return false;
- if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
+ // 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.
+ //
+ // The following is adapted from the Electron project, under the MIT License.
+ // Hence this is also how VS Code detects screenreaders.
+ // See: https://github.com/electron/electron/pull/39988
+
+ // Check for Windows Narrator using the NarratorRunning mutex
+ if (PlatformWindows.IsMutexPresent("NarratorRunning"))
+ return true;
+
+ // Check for various screen reader libraries
+ string[] screenReaderLibraries = {
+ // NVDA
+ "nvdaHelperRemote.dll",
+ // JAWS
+ "jhook.dll",
+ // Window-Eyes
+ "gwhk64.dll",
+ "gwmhook.dll",
+ // ZoomText
+ "AiSquared.Infuser.HookLib.dll"
+ };
+
+ foreach (string library in screenReaderLibraries)
{
- // NOTE: This API can detect if a third-party screen reader is active, such as NVDA, but not the in-box Windows Narrator.
- PlatformWindows.SystemParametersInfo(PlatformWindows.SPI_GETSCREENREADER, 0, ref returnValue, 0);
- } // TODO: Support other platforms per https://code.visualstudio.com/docs/configure/accessibility/accessibility
+ if (PlatformWindows.IsLibraryLoaded(library))
+ return true;
+ }
- return returnValue;
+ return false;
}
}
}
diff --git a/PSReadLine/PlatformWindows.cs b/PSReadLine/PlatformWindows.cs
index c7e0313b9..a28e33368 100644
--- a/PSReadLine/PlatformWindows.cs
+++ b/PSReadLine/PlatformWindows.cs
@@ -79,6 +79,43 @@ IntPtr templateFileWin32Handle
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
internal static extern IntPtr GetStdHandle(uint handleId);
+ internal const int ERROR_ALREADY_EXISTS = 0xB7;
+
+ internal static bool IsMutexPresent(string name)
+ {
+ try
+ {
+ using var mutex = new System.Threading.Mutex(false, name);
+ return Marshal.GetLastWin32Error() == ERROR_ALREADY_EXISTS;
+ }
+ catch
+ {
+ return false;
+ }
+ }
+
+ [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
+ [return: MarshalAs(UnmanagedType.Bool)]
+ internal static extern bool GetModuleHandleEx(uint dwFlags, string lpModuleName, out IntPtr phModule);
+
+ internal static bool IsLibraryLoaded(string name)
+ {
+ // Prevents the reference count for the module being incremented.
+ const uint GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT = 0x00000002;
+ try
+ {
+ bool retValue = GetModuleHandleEx(
+ GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT,
+ name,
+ out IntPtr phModule);
+ return phModule != IntPtr.Zero;
+ }
+ catch
+ {
+ return false;
+ }
+ }
+
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
static extern bool SetConsoleCtrlHandler(BreakHandler handlerRoutine, bool add);
From a6dc61674553cda5dd3fb12b6111534a45b5571b Mon Sep 17 00:00:00 2001
From: Andy Jordan <2226434+andyleejordan@users.noreply.github.com>
Date: Thu, 17 Jul 2025 16:09:10 -0700
Subject: [PATCH 3/7] Check for VoiceOver screen reader on macOS
Spawns a quick `defaults` process since in .NET
using the macOS events is difficult, but this is
quick and easy.
---
PSReadLine/Accessibility.cs | 50 +++++++++++++++++++++++++++++++++++--
1 file changed, 48 insertions(+), 2 deletions(-)
diff --git a/PSReadLine/Accessibility.cs b/PSReadLine/Accessibility.cs
index fa000555b..e9a54a4ed 100644
--- a/PSReadLine/Accessibility.cs
+++ b/PSReadLine/Accessibility.cs
@@ -2,6 +2,7 @@
Copyright (c) Microsoft Corporation. All rights reserved.
--********************************************************************/
+using System.Diagnostics;
using System.Runtime.InteropServices;
namespace Microsoft.PowerShell.Internal
@@ -10,10 +11,22 @@ internal class Accessibility
{
internal static bool IsScreenReaderActive()
{
- // TODO: Support other platforms per https://code.visualstudio.com/docs/configure/accessibility/accessibility
- if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
+ 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
@@ -48,5 +61,38 @@ internal static bool IsScreenReaderActive()
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;
+ }
}
}
From 207e358c29ecd4e93ea8b82e44441eef54aba91f Mon Sep 17 00:00:00 2001
From: Andy Jordan <2226434+andyleejordan@users.noreply.github.com>
Date: Fri, 18 Jul 2025 15:04:29 -0700
Subject: [PATCH 4/7] Revert to system parameter screen reader check on Windows
That algorithm doesn't work for a non-windowed app like PowerShell.
---
PSReadLine/Accessibility.cs | 40 +++++++++++++++--------------------
PSReadLine/PlatformWindows.cs | 22 -------------------
2 files changed, 17 insertions(+), 45 deletions(-)
diff --git a/PSReadLine/Accessibility.cs b/PSReadLine/Accessibility.cs
index e9a54a4ed..aae6c0f52 100644
--- a/PSReadLine/Accessibility.cs
+++ b/PSReadLine/Accessibility.cs
@@ -22,7 +22,7 @@ internal static bool IsScreenReaderActive()
}
// TODO: Support Linux per https://code.visualstudio.com/docs/configure/accessibility/accessibility
- return false;
+ return false;
}
private static bool IsAnyWindowsScreenReaderEnabled()
@@ -32,31 +32,25 @@ private static bool IsAnyWindowsScreenReaderEnabled()
// doesn't detect the in-box Windows Narrator and is otherwise known
// to be problematic.
//
- // The following is adapted from the Electron project, under the MIT License.
- // Hence this is also how VS Code detects screenreaders.
- // See: https://github.com/electron/electron/pull/39988
-
- // Check for Windows Narrator using the NarratorRunning mutex
- if (PlatformWindows.IsMutexPresent("NarratorRunning"))
+ // 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;
+ }
- // Check for various screen reader libraries
- string[] screenReaderLibraries = {
- // NVDA
- "nvdaHelperRemote.dll",
- // JAWS
- "jhook.dll",
- // Window-Eyes
- "gwhk64.dll",
- "gwmhook.dll",
- // ZoomText
- "AiSquared.Infuser.HookLib.dll"
- };
-
- foreach (string library in screenReaderLibraries)
+ // 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"))
{
- if (PlatformWindows.IsLibraryLoaded(library))
- return true;
+ return true;
}
return false;
diff --git a/PSReadLine/PlatformWindows.cs b/PSReadLine/PlatformWindows.cs
index a28e33368..32cf653fb 100644
--- a/PSReadLine/PlatformWindows.cs
+++ b/PSReadLine/PlatformWindows.cs
@@ -94,28 +94,6 @@ internal static bool IsMutexPresent(string name)
}
}
- [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
- [return: MarshalAs(UnmanagedType.Bool)]
- internal static extern bool GetModuleHandleEx(uint dwFlags, string lpModuleName, out IntPtr phModule);
-
- internal static bool IsLibraryLoaded(string name)
- {
- // Prevents the reference count for the module being incremented.
- const uint GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT = 0x00000002;
- try
- {
- bool retValue = GetModuleHandleEx(
- GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT,
- name,
- out IntPtr phModule);
- return phModule != IntPtr.Zero;
- }
- catch
- {
- return false;
- }
- }
-
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
static extern bool SetConsoleCtrlHandler(BreakHandler handlerRoutine, bool add);
From c38dbaeeaa2ef034f9c7044a2827cf42ebe652b0 Mon Sep 17 00:00:00 2001
From: Andy Jordan <2226434+andyleejordan@users.noreply.github.com>
Date: Thu, 17 Jul 2025 18:31:23 -0700
Subject: [PATCH 5/7] Disable predictions for screen readers
Because they'll be rendered and it's useless noise.
---
PSReadLine/Options.cs | 6 ++++++
1 file changed, 6 insertions(+)
diff --git a/PSReadLine/Options.cs b/PSReadLine/Options.cs
index b8b3efad5..19ba6b3bd 100644
--- a/PSReadLine/Options.cs
+++ b/PSReadLine/Options.cs
@@ -188,6 +188,12 @@ private void SetOptionsInternal(SetPSReadLineOption options)
if (options._screenReader.HasValue)
{
Options.ScreenReader = options.ScreenReader;
+
+ // Disable prediction for better accessibility
+ if (Options.ScreenReader)
+ {
+ Options.PredictionSource = PredictionSource.None;
+ }
}
}
From 2c3b9cf96100ffe7d62b2db16cdee57055b8c5a7 Mon Sep 17 00:00:00 2001
From: Andy Jordan <2226434+andyleejordan@users.noreply.github.com>
Date: Wed, 30 Jul 2025 22:05:40 -0700
Subject: [PATCH 6/7] Implement `SafeRender()` for use under a screen reader
---
PSReadLine/BasicEditing.cs | 4 +-
PSReadLine/Options.cs | 4 +-
PSReadLine/Render.Helper.cs | 1 +
PSReadLine/Render.cs | 165 +++++++++++++++++++++++++++++++-----
4 files changed, 148 insertions(+), 26 deletions(-)
diff --git a/PSReadLine/BasicEditing.cs b/PSReadLine/BasicEditing.cs
index 33445ed36..74c76ac00 100644
--- a/PSReadLine/BasicEditing.cs
+++ b/PSReadLine/BasicEditing.cs
@@ -86,7 +86,7 @@ public static void CancelLine(ConsoleKeyInfo? key = null, object arg = null)
_singleton._current = _singleton._buffer.Length;
using var _ = _singleton._prediction.DisableScoped();
- _singleton.ForceRender();
+ _singleton.Render(force: true);
_singleton._console.Write("\x1b[91m^C\x1b[0m");
@@ -335,7 +335,7 @@ private bool AcceptLineImpl(bool validate)
if (renderNeeded)
{
- ForceRender();
+ Render(force: true);
}
// Only run validation if we haven't before. If we have and status line shows an error,
diff --git a/PSReadLine/Options.cs b/PSReadLine/Options.cs
index 19ba6b3bd..0416dba3e 100644
--- a/PSReadLine/Options.cs
+++ b/PSReadLine/Options.cs
@@ -189,10 +189,12 @@ private void SetOptionsInternal(SetPSReadLineOption options)
{
Options.ScreenReader = options.ScreenReader;
- // Disable prediction for better accessibility
if (Options.ScreenReader)
{
+ // Disable prediction for better accessibility.
Options.PredictionSource = PredictionSource.None;
+ // Disable continuation prompt as multi-line is not available.
+ Options.ContinuationPrompt = "";
}
}
}
diff --git a/PSReadLine/Render.Helper.cs b/PSReadLine/Render.Helper.cs
index 8fc56bea1..584ce7956 100644
--- a/PSReadLine/Render.Helper.cs
+++ b/PSReadLine/Render.Helper.cs
@@ -96,6 +96,7 @@ internal static int LengthInBufferCells(char c)
if (c < 256)
{
// We render ^C for Ctrl+C, so return 2 for control characters
+ // TODO: Do we care about this under a screen reader?
return Char.IsControl(c) ? 2 : 1;
}
diff --git a/PSReadLine/Render.cs b/PSReadLine/Render.cs
index fe688481a..d1af0c388 100644
--- a/PSReadLine/Render.cs
+++ b/PSReadLine/Render.cs
@@ -218,36 +218,155 @@ private void RenderWithPredictionQueryPaused()
Render();
}
- private void Render()
+ private void Render(bool force = false)
{
// If there are a bunch of keys queued up, skip rendering if we've rendered very recently.
long elapsedMs = _lastRenderTime.ElapsedMilliseconds;
- if (_queuedKeys.Count > 10 && elapsedMs < 50)
- {
- // We won't render, but most likely the tokens will be different, so make
- // sure we don't use old tokens, also allow garbage to get collected.
- _tokens = null;
- _ast = null;
- _parseErrors = null;
- _waitingToRender = true;
- return;
+ if (!force)
+ {
+ if (_queuedKeys.Count > 10 && elapsedMs < 50)
+ {
+ // We won't render, but most likely the tokens will be different, so make
+ // sure we don't use old tokens, also allow garbage to get collected.
+ _tokens = null;
+ _ast = null;
+ _parseErrors = null;
+ _waitingToRender = true;
+ return;
+ }
+
+ // If we've rendered very recently, skip the terminal window resizing check as it's unlikely
+ // to happen in such a short time interval.
+ // We try to avoid unnecessary resizing check because it requires getting the cursor position
+ // which would force a network round trip in an environment where front-end xtermjs talking to
+ // a server-side PTY via websocket. Without querying for cursor position, content written on
+ // the server side could be buffered, which is much more performant.
+ // See the following 2 GitHub issues for more context:
+ // - https://github.com/PowerShell/PSReadLine/issues/3879#issuecomment-2573996070
+ // - https://github.com/PowerShell/PowerShell/issues/24696
+ if (elapsedMs < 50)
+ {
+ _handlePotentialResizing = false;
+ }
}
- // If we've rendered very recently, skip the terminal window resizing check as it's unlikely
- // to happen in such a short time interval.
- // We try to avoid unnecessary resizing check because it requires getting the cursor position
- // which would force a network round trip in an environment where front-end xtermjs talking to
- // a server-side PTY via websocket. Without querying for cursor position, content written on
- // the server side could be buffered, which is much more performant.
- // See the following 2 GitHub issues for more context:
- // - https://github.com/PowerShell/PSReadLine/issues/3879#issuecomment-2573996070
- // - https://github.com/PowerShell/PowerShell/issues/24696
- if (elapsedMs < 50)
+ // Use simplified rendering for screen readers
+ if (Options.ScreenReader)
{
- _handlePotentialResizing = false;
+ SafeRender();
}
+ else
+ {
+ ForceRender();
+ }
+ }
+
+ private void SafeRender()
+ {
+ int bufferWidth = _console.BufferWidth;
+ int bufferHeight = _console.BufferHeight;
+
+ static int FindCommonPrefixLength(string leftStr, string rightStr)
+ {
+ if (string.IsNullOrEmpty(leftStr) || string.IsNullOrEmpty(rightStr))
+ {
+ return 0;
+ }
- ForceRender();
+ int i = 0;
+ int minLength = Math.Min(leftStr.Length, rightStr.Length);
+
+ while (i < minLength && leftStr[i] == rightStr[i])
+ {
+ i++;
+ }
+
+ return i;
+ }
+
+ // For screen readers, we are just comparing the previous and current buffer text
+ // (without colors) and only writing the differences.
+ string currentBuffer = ParseInput();
+ string previousBuffer = _previousRender.lines[0].Line;
+
+ // In case the buffer was resized.
+ RecomputeInitialCoords(isTextBufferUnchanged: false);
+
+ // Make cursor invisible while we're rendering.
+ _console.CursorVisible = false;
+
+ // Calculate what to render and where to start the rendering.
+ // TODO: Short circuit optimization when currentBuffer == previousBuffer.
+ int commonPrefixLength = FindCommonPrefixLength(previousBuffer, currentBuffer);
+
+ if (commonPrefixLength > 0 && commonPrefixLength == previousBuffer.Length)
+ {
+ // Previous buffer is a complete prefix of current buffer.
+ // Just append the new data.
+ var appendedData = currentBuffer.Substring(commonPrefixLength);
+ _console.Write(appendedData);
+ }
+ else if (commonPrefixLength > 0)
+ {
+ // Buffers share a common prefix but previous buffer has additional content.
+ // Move cursor to where the difference starts, clear forward, and write the data.
+ var diffPoint = ConvertOffsetToPoint(commonPrefixLength);
+ _console.SetCursorPosition(diffPoint.X, diffPoint.Y);
+ var changedData = currentBuffer.Substring(commonPrefixLength);
+ _console.Write("\x1b[0J");
+ _console.Write(changedData);
+ }
+ else
+ {
+ // No common prefix, rewrite entire buffer.
+ _console.SetCursorPosition(_initialX, _initialY);
+ _console.Write("\x1b[0J");
+ _console.Write(currentBuffer);
+ }
+
+ // If we had to wrap to render everything, update _initialY
+ var endPoint = ConvertOffsetToPoint(currentBuffer.Length);
+ int physicalLine = endPoint.Y - _initialY;
+ if (_initialY + physicalLine > bufferHeight)
+ {
+ // We had to scroll to render everything, update _initialY.
+ _initialY = bufferHeight - physicalLine;
+ }
+
+ // Preserve the current render data.
+ var renderData = new RenderData
+ {
+ lines = new RenderedLineData[] { new(currentBuffer, isFirstLogicalLine: true) },
+ errorPrompt = (_parseErrors != null && _parseErrors.Length > 0) // Not yet used.
+ };
+ _previousRender = renderData;
+
+ // Calculate the coord to place the cursor for the next input.
+ var point = ConvertOffsetToPoint(_current);
+
+ if (point.Y == bufferHeight)
+ {
+ // The cursor top exceeds the buffer height and it hasn't already wrapped,
+ // so we need to scroll up the buffer by 1 line.
+ if (point.X == 0)
+ {
+ _console.Write("\n");
+ }
+
+ // Adjust the initial cursor position and the to-be-set cursor position
+ // after scrolling up the buffer.
+ _initialY -= 1;
+ point.Y -= 1;
+ }
+
+ _console.SetCursorPosition(point.X, point.Y);
+ _console.CursorVisible = true;
+
+ _previousRender.UpdateConsoleInfo(bufferWidth, bufferHeight, point.X, point.Y);
+ _previousRender.initialY = _initialY;
+
+ _lastRenderTime.Restart();
+ _waitingToRender = false;
}
private void ForceRender()
@@ -261,7 +380,7 @@ private void ForceRender()
// and minimize writing more than necessary on the next render.)
var renderLines = new RenderedLineData[logicalLineCount];
- var renderData = new RenderData {lines = renderLines};
+ var renderData = new RenderData { lines = renderLines };
for (var i = 0; i < logicalLineCount; i++)
{
var line = _consoleBufferLines[i].ToString();
From 14fc559ff9be75031f0bc190b70ef3a7c6ee41c8 Mon Sep 17 00:00:00 2001
From: Andy Jordan <2226434+andyleejordan@users.noreply.github.com>
Date: Tue, 5 Aug 2025 16:45:22 -0700
Subject: [PATCH 7/7] Correctly handle edge case with exactly filled line
---
PSReadLine/Render.cs | 17 ++++++++++++-----
1 file changed, 12 insertions(+), 5 deletions(-)
diff --git a/PSReadLine/Render.cs b/PSReadLine/Render.cs
index d1af0c388..f13d15b96 100644
--- a/PSReadLine/Render.cs
+++ b/PSReadLine/Render.cs
@@ -326,11 +326,18 @@ static int FindCommonPrefixLength(string leftStr, string rightStr)
// If we had to wrap to render everything, update _initialY
var endPoint = ConvertOffsetToPoint(currentBuffer.Length);
- int physicalLine = endPoint.Y - _initialY;
- if (_initialY + physicalLine > bufferHeight)
+ if (endPoint.Y >= bufferHeight)
{
+
// We had to scroll to render everything, update _initialY.
- _initialY = bufferHeight - physicalLine;
+ int offset = 1; // Base case to handle zero-indexing.
+ if (endPoint.X == 0)
+ {
+ // The line hasn't actually wrapped yet because we have exactly filled the line.
+ offset -= 1;
+ }
+ int scrolledLines = endPoint.Y - bufferHeight + offset;
+ _initialY -= scrolledLines;
}
// Preserve the current render data.
@@ -347,8 +354,8 @@ static int FindCommonPrefixLength(string leftStr, string rightStr)
if (point.Y == bufferHeight)
{
// The cursor top exceeds the buffer height and it hasn't already wrapped,
- // so we need to scroll up the buffer by 1 line.
- if (point.X == 0)
+ // (because we have exactly filled the line) so we need to scroll up the buffer by 1 line.
+ if (point.X == 0 && !currentBuffer.EndsWith("\n"))
{
_console.Write("\n");
}