Skip to content

Commit eac0d72

Browse files
committed
Implement SafeRender() for use under a screen reader
1 parent b960600 commit eac0d72

File tree

4 files changed

+123
-11
lines changed

4 files changed

+123
-11
lines changed

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/Options.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -189,10 +189,12 @@ private void SetOptionsInternal(SetPSReadLineOption options)
189189
{
190190
Options.ScreenReader = options.ScreenReader;
191191

192-
// Disable prediction for better accessibility
193192
if (Options.ScreenReader)
194193
{
194+
// Disable prediction for better accessibility.
195195
Options.PredictionSource = PredictionSource.None;
196+
// Disable continuation prompt as multi-line is not available.
197+
Options.ContinuationPrompt = "";
196198
}
197199
}
198200
}

PSReadLine/Render.Helper.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ internal static int LengthInBufferCells(char c)
9696
if (c < 256)
9797
{
9898
// We render ^C for Ctrl+C, so return 2 for control characters
99+
// TODO: Do we care about this under a screen reader?
99100
return Char.IsControl(c) ? 2 : 1;
100101
}
101102

PSReadLine/Render.cs

Lines changed: 117 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -218,11 +218,11 @@ private void RenderWithPredictionQueryPaused()
218218
Render();
219219
}
220220

221-
private void Render()
221+
private void Render(bool force = false)
222222
{
223223
// If there are a bunch of keys queued up, skip rendering if we've rendered very recently.
224224
long elapsedMs = _lastRenderTime.ElapsedMilliseconds;
225-
if (_queuedKeys.Count > 10 && elapsedMs < 50)
225+
if (!force && _queuedKeys.Count > 10 && elapsedMs < 50)
226226
{
227227
// We won't render, but most likely the tokens will be different, so make
228228
// sure we don't use old tokens, also allow garbage to get collected.
@@ -242,12 +242,123 @@ private void Render()
242242
// See the following 2 GitHub issues for more context:
243243
// - https://github.com/PowerShell/PSReadLine/issues/3879#issuecomment-2573996070
244244
// - https://github.com/PowerShell/PowerShell/issues/24696
245-
if (elapsedMs < 50)
245+
if (!force && elapsedMs < 50)
246246
{
247247
_handlePotentialResizing = false;
248248
}
249249

250-
ForceRender();
250+
// Use simplified rendering for screen readers
251+
if (Options.ScreenReader)
252+
{
253+
SafeRender();
254+
}
255+
else
256+
{
257+
ForceRender();
258+
}
259+
}
260+
261+
private void SafeRender()
262+
{
263+
int bufferWidth = _console.BufferWidth;
264+
int bufferHeight = _console.BufferHeight;
265+
266+
static int FindCommonPrefixLength(string leftStr, string rightStr)
267+
{
268+
if (string.IsNullOrEmpty(leftStr) || string.IsNullOrEmpty(rightStr))
269+
{
270+
return 0;
271+
}
272+
273+
int i = 0;
274+
int minLength = Math.Min(leftStr.Length, rightStr.Length);
275+
276+
while (i < minLength && leftStr[i] == rightStr[i])
277+
{
278+
i++;
279+
}
280+
281+
return i;
282+
}
283+
284+
// For screen readers, we are just comparing the previous and current buffer text
285+
// (without colors) and only writing the differences.
286+
string currentBuffer = ParseInput();
287+
string previousBuffer = _previousRender.lines[0].Line;
288+
289+
// In case the buffer was resized.
290+
RecomputeInitialCoords(isTextBufferUnchanged: false);
291+
292+
// Make cursor invisible while we're rendering.
293+
_console.CursorVisible = false;
294+
295+
// Calculate what to render and where to start the rendering.
296+
// TODO: Short circuit optimization when currentBuffer == previousBuffer.
297+
int commonPrefixLength = FindCommonPrefixLength(previousBuffer, currentBuffer);
298+
299+
if (commonPrefixLength > 0 && commonPrefixLength == previousBuffer.Length)
300+
{
301+
// Previous buffer is a complete prefix of current buffer.
302+
// Just append the new data.
303+
var appendedData = currentBuffer.Substring(commonPrefixLength);
304+
_console.Write(appendedData);
305+
}
306+
else if (commonPrefixLength > 0)
307+
{
308+
// Buffers share a common prefix but previous buffer has additional content.
309+
// Move cursor to where the difference starts, clear forward, and write the data.
310+
var diffPoint = ConvertOffsetToPoint(commonPrefixLength);
311+
_console.SetCursorPosition(diffPoint.X, diffPoint.Y);
312+
var changedData = currentBuffer.Substring(commonPrefixLength);
313+
_console.Write("\x1b[0J");
314+
_console.Write(changedData);
315+
}
316+
else
317+
{
318+
// No common prefix, rewrite entire buffer.
319+
_console.SetCursorPosition(_initialX, _initialY);
320+
_console.Write("\x1b[0J");
321+
_console.Write(currentBuffer);
322+
}
323+
324+
// If we had to wrap to render everything, update _initialY
325+
var endPoint = ConvertOffsetToPoint(currentBuffer.Length);
326+
int physicalLine = endPoint.Y - _initialY;
327+
if (_initialY + physicalLine > bufferHeight)
328+
{
329+
// We had to scroll to render everything, update _initialY.
330+
_initialY = bufferHeight - physicalLine;
331+
}
332+
333+
// Preserve the current render data.
334+
var renderData = new RenderData
335+
{
336+
lines = new RenderedLineData[] { new(currentBuffer, isFirstLogicalLine: true) },
337+
errorPrompt = (_parseErrors != null && _parseErrors.Length > 0) // Not yet used.
338+
};
339+
_previousRender = renderData;
340+
341+
// Calculate the coord to place the cursor for the next input.
342+
var point = ConvertOffsetToPoint(_current);
343+
if (point.X == 0 && point.Y == bufferHeight)
344+
{
345+
// The cursor top exceeds the buffer height and it hasn't already wrapped,
346+
// so we need to scroll up the buffer by 1 line.
347+
_console.Write("\n");
348+
349+
// Adjust the initial cursor position and the to-be-set cursor position
350+
// after scrolling up the buffer.
351+
_initialY -= 1;
352+
point.Y -= 1;
353+
}
354+
_console.SetCursorPosition(point.X, point.Y);
355+
_console.CursorVisible = true;
356+
357+
_previousRender.UpdateConsoleInfo(bufferWidth, bufferHeight, point.X, point.Y);
358+
_previousRender.initialY = _initialY;
359+
360+
_lastRenderTime.Restart();
361+
_waitingToRender = false;
251362
}
252363

253364
private void ForceRender()
@@ -261,7 +372,7 @@ private void ForceRender()
261372
// and minimize writing more than necessary on the next render.)
262373

263374
var renderLines = new RenderedLineData[logicalLineCount];
264-
var renderData = new RenderData {lines = renderLines};
375+
var renderData = new RenderData { lines = renderLines };
265376
for (var i = 0; i < logicalLineCount; i++)
266377
{
267378
var line = _consoleBufferLines[i].ToString();
@@ -910,7 +1021,6 @@ void UpdateColorsIfNecessary(string newColor)
9101021

9111022
// Calculate the coord to place the cursor for the next input.
9121023
var point = ConvertOffsetToPoint(_current);
913-
9141024
if (point.Y == bufferHeight)
9151025
{
9161026
// The cursor top exceeds the buffer height, so we need to
@@ -946,7 +1056,6 @@ void UpdateColorsIfNecessary(string newColor)
9461056
_current = ConvertLineAndColumnToOffset(point);
9471057
point = ConvertOffsetToPoint(_current);
9481058
}
949-
9501059
_console.SetCursorPosition(point.X, point.Y);
9511060
_console.CursorVisible = true;
9521061

@@ -1827,6 +1936,6 @@ public static void ScrollDisplayToCursor(ConsoleKeyInfo? key = null, object arg
18271936
newTop = (console.BufferHeight - console.WindowHeight);
18281937
}
18291938
console.SetWindowPosition(0, newTop);
1830-
}
1939+
}
18311940
}
18321941
}

0 commit comments

Comments
 (0)