@@ -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,128 @@ 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+
344+ if ( point . Y == bufferHeight )
345+ {
346+ // The cursor top exceeds the buffer height and it hasn't already wrapped,
347+ // so we need to scroll up the buffer by 1 line.
348+ if ( point . X == 0 && ! currentBuffer . EndsWith ( "\n " ) )
349+ {
350+ _console . Write ( "\n " ) ;
351+ }
352+
353+ // Adjust the initial cursor position and the to-be-set cursor position
354+ // after scrolling up the buffer.
355+ _initialY -= 1 ;
356+ point . Y -= 1 ;
357+ }
358+
359+ _console . SetCursorPosition ( point . X , point . Y ) ;
360+ _console . CursorVisible = true ;
361+
362+ _previousRender . UpdateConsoleInfo ( bufferWidth , bufferHeight , point . X , point . Y ) ;
363+ _previousRender . initialY = _initialY ;
364+
365+ _lastRenderTime . Restart ( ) ;
366+ _waitingToRender = false ;
251367 }
252368
253369 private void ForceRender ( )
@@ -261,7 +377,7 @@ private void ForceRender()
261377 // and minimize writing more than necessary on the next render.)
262378
263379 var renderLines = new RenderedLineData [ logicalLineCount ] ;
264- var renderData = new RenderData { lines = renderLines } ;
380+ var renderData = new RenderData { lines = renderLines } ;
265381 for ( var i = 0 ; i < logicalLineCount ; i ++ )
266382 {
267383 var line = _consoleBufferLines [ i ] . ToString ( ) ;
0 commit comments