@@ -123,7 +123,7 @@ def __init__(self, renderer: "Renderer", display: "TerminalDisplay", lock: Termi
123123 self ._pending_error : BaseException | None = None
124124 self ._prompt_anchor : int | None = None
125125 self ._resynchronization_reason : str | None = None
126- self ._pending_cpr : deque [int ] = deque ()
126+ self ._pending_cpr : deque [Generations ] = deque ()
127127
128128 # -- what is known ---------------------------------------------------------------------
129129
@@ -415,38 +415,59 @@ def resynchronize(self) -> None:
415415 if self ._reserved_emission_stopped :
416416 raise ReservedModeFailureError ("reserved emission has stopped; release before rendering again" )
417417
418+ # Resolved before the terminal is taken: this runs application filters, which the
419+ # wait contract keeps off the lock.
418420 policy = self ._desired_policy ()
419- origin = self ._usable_prompt_anchor ()
420- if origin is None :
421- if not self ._display .output .responds_to_cpr :
422- raise ReservedModeFailureError ("the prompt's origin is unknown and the terminal does not report its cursor" )
423- # The reply establishes the origin. Recovery stays owed until it arrives; guessing
424- # would repaint the prompt over committed output.
425- self .request_cursor_position ()
426- return
427421
428422 with self ._lock .transaction ("resynchronize" ):
429- output = self . _display . output
430- output . write_raw ( f" \x1b [ { origin } ;1H" )
431- # Upstream enables bracketed paste on every render and latches a flag beside the
432- # emission, so the policy here is not conditional: it is on, and the flag is made
433- # to agree with an enable that actually reached the terminal .
434- output . enable_bracketed_paste ()
435- if policy . mouse_support :
436- output .enable_mouse_support ()
423+ # The origin is read *here*, not before the wait. Recovery can queue behind
424+ # another writer for as long as that writer holds the terminal, and what it does
425+ # in the meantime -- emitting output, moving the prompt, resizing -- is exactly
426+ # what changes where the prompt now starts. An origin read beforehand describes a
427+ # terminal somebody else still owned .
428+ origin = self . _usable_prompt_anchor ()
429+ if origin is None :
430+ can_report = self . _display . output .responds_to_cpr
437431 else :
438- output . disable_mouse_support ( )
439- output . reset_cursor_key_mode ()
440- output . reset_attributes ()
441- output . enable_autowrap ()
442- output . reset_cursor_shape ( )
443- output . show_cursor ()
444- output .flush ()
445- self ._initialize_renderer ( policy , origin )
432+ self . _establish ( policy , origin )
433+ return
434+
435+ if not can_report :
436+ raise ReservedModeFailureError ( "the prompt's origin is unknown and the terminal does not report its cursor" )
437+ # The reply establishes the origin. Recovery stays owed until it arrives; guessing
438+ # would repaint the prompt over committed output.
439+ self .request_cursor_position ( )
446440
441+ def _establish (self , policy : TerminalModePolicy , origin : int ) -> None :
442+ """Put the terminal into the known state, from inside the transaction.
443+
444+ Recovery is marked complete here rather than after the lock is given back: whoever
445+ takes the terminal next must not find a recovery still owed against work that has
446+ already been done.
447+
448+ :param policy: the mode policy to establish
449+ :param origin: the physical row to place the cursor on
450+ """
451+ output = self ._display .output
452+ output .write_raw (f"\x1b [{ origin } ;1H" )
453+ # Upstream enables bracketed paste on every render and latches a flag beside the
454+ # emission, so the policy here is not conditional: it is on, and the flag is made
455+ # to agree with an enable that actually reached the terminal.
456+ output .enable_bracketed_paste ()
457+ if policy .mouse_support :
458+ output .enable_mouse_support ()
459+ else :
460+ output .disable_mouse_support ()
461+ output .reset_cursor_key_mode ()
462+ output .reset_attributes ()
463+ output .enable_autowrap ()
464+ output .reset_cursor_shape ()
465+ output .show_cursor ()
466+ output .flush ()
447467 self ._needs_resynchronization = False
448468 self ._resynchronization_reason = None
449469 self ._in_flight = None
470+ self ._initialize_renderer (policy , origin )
450471
451472 def _usable_rows (self ) -> int :
452473 """How many rows the application may use right now.
@@ -528,11 +549,14 @@ def request_cursor_position(self) -> bool:
528549 output = self ._display .output
529550 if not output .responds_to_cpr :
530551 return False
531- generation = self .generations (). geometry
532- with self ._lock .transaction ("cursor position request" , generation = generation ):
552+ generations = self .generations ()
553+ with self ._lock .transaction ("cursor position request" , generation = generations . geometry ):
533554 output .ask_for_cpr ()
534555 output .flush ()
535- self ._pending_cpr .append (generation )
556+ # The whole generation tuple, not just the geometry. The terminal samples the cursor
557+ # when it processes the request, so managed output written afterwards moves the very
558+ # thing the reply describes -- a resize is not the only way a reply goes stale.
559+ self ._pending_cpr .append (generations )
536560 return True
537561
538562 def report_cursor_row (self , row : int ) -> bool :
@@ -542,6 +566,10 @@ def report_cursor_row(self, row: int) -> bool:
542566 requests this bridge made. A reply from before a geometry change describes a screen
543567 that no longer exists and must not satisfy the request made after it.
544568
569+ A reply is stale when anything about the terminal has changed since the request went
570+ out -- a resize, an owner change, or managed output that moved the cursor the terminal
571+ was about to sample.
572+
545573 A row inside the reserved band is the failure named in the design: upstream would
546574 compute ``U - r + 1``, which is zero at the first reserved row and negative below it,
547575 and would leave the prompt's height silently invalid rather than raising.
@@ -554,8 +582,10 @@ def report_cursor_row(self, row: int) -> bool:
554582 # must not be allowed to answer a request that was never made.
555583 self ._settle_renderer_cpr ()
556584 return False
557- generation = self ._pending_cpr .popleft ()
558- if generation != self .generations ().geometry :
585+ # Popped whatever the outcome: replies correlate by order, so dropping one without
586+ # taking it off the queue would answer every later request with its predecessor.
587+ generations = self ._pending_cpr .popleft ()
588+ if generations != self .generations ():
559589 self ._settle_renderer_cpr ()
560590 return False
561591
0 commit comments