@@ -362,14 +362,11 @@ def _build_body(self) -> Group:
362362 rows : list [Any ] = []
363363 if self ._peek_visible :
364364 visible = self ._scroll_lines [- _MAX_VISIBLE_SCROLL :]
365- if visible :
366- for line in visible :
367- line .no_wrap = True
368- line .overflow = "ellipsis"
369- rows .append (line )
370- elif self ._peek_message is not None :
371- rows .append (self ._peek_message )
372- elif self ._peek_message is not None :
365+ for line in visible :
366+ line .no_wrap = True
367+ line .overflow = "ellipsis"
368+ rows .append (line )
369+ if not rows and self ._peek_message is not None :
373370 rows .append (self ._peek_message )
374371 rows .append (Text ("" )) # spacer above footer
375372 rows .append (self ._build_footer ())
@@ -596,10 +593,10 @@ def _build_footer(self) -> Table:
596593# ── Full-screen peek ─────────────────────────────────────────────────
597594
598595# Chrome rows that the fullscreen peek reserves on top of its viewport:
599- # panel top border + header row + header gap + footer gap + footer row +
600- # panel bottom border. Used to size the visible viewport to the terminal .
601- _FULLSCREEN_CHROME_ROWS = 6
602- _FULLSCREEN_MIN_VISIBLE = 5
596+ # panel top border (1) + panel bottom border (1). Header lives in the
597+ # panel title and footer in the subtitle, so they cost no extra rows .
598+ _FULLSCREEN_CHROME_ROWS = 2
599+ _FULLSCREEN_MIN_VISIBLE = 3
603600
604601
605602@dataclass (slots = True )
@@ -704,12 +701,13 @@ def _build_header(self, total: int, visible: int) -> Text:
704701 end = total - self ._offset
705702 header .append (" · " , style = "dim" )
706703 header .append (f"lines { start } –{ end } " , style = f"italic { _brand .LAVENDER } " )
704+ header .append (" " , style = "dim" )
707705 return header
708706
709707 def _build_footer (self ) -> Text :
710708 hint = Text (no_wrap = True , overflow = "ellipsis" )
711709 hint .append (
712- " ↑/k up · ↓/j down · b page up · space page down · g/G top/bottom · q/" ,
710+ " ↑/↓ scroll · space/ b page · g/G top/bottom · q/" ,
713711 style = "dim" ,
714712 )
715713 hint .append (FULLSCREEN_PEEK_KEY , style = f"bold { _brand .PURPLE } " )
@@ -738,18 +736,20 @@ def __rich_console__(
738736
739737 sb = _scrollbar_metrics (total , visible , self ._offset )
740738
741- rows : list [Any ] = []
742- rows .append (self ._build_header (total , visible ))
743- rows .append (Text ("" ))
744-
745- # Content area with optional scrollbar column
739+ # Body is exactly `visible` rows tall: a single Table.grid that
740+ # always pads to the viewport height (with empty Text rows when
741+ # the buffer is shorter than the viewport) so the panel's height
742+ # never depends on how full the buffer is.
746743 content = Table .grid (expand = True )
747744 content .add_column (ratio = 1 , no_wrap = True , overflow = "ellipsis" )
748745 if sb .show :
749746 content .add_column (width = 1 , no_wrap = True )
750747
748+ show_waiting = not window and not sb .show
751749 for i in range (visible ):
752- if i < len (window ):
750+ if show_waiting and i == 0 :
751+ line : Text = Text (" (waiting for activity…)" , style = "dim italic" )
752+ elif i < len (window ):
753753 line = window [i ]
754754 line .no_wrap = True
755755 line .overflow = "ellipsis"
@@ -765,22 +765,16 @@ def __rich_console__(
765765 else :
766766 content .add_row (line )
767767
768- if not window and not sb .show :
769- # Replace the empty grid with a waiting message
770- rows .append (Text (" (waiting for activity…)" , style = "dim italic" ))
771- for _ in range (max (0 , visible - 1 )):
772- rows .append (Text ("" ))
773- else :
774- rows .append (content )
775-
776- rows .append (Text ("" ))
777- rows .append (self ._build_footer ())
778-
779768 panel = Panel (
780- Group ( * rows ) ,
769+ content ,
781770 box = box .ROUNDED ,
771+ title = self ._build_header (total , visible ),
772+ title_align = "left" ,
773+ subtitle = self ._build_footer (),
774+ subtitle_align = "left" ,
782775 border_style = _brand .PURPLE ,
783- padding = (0 , 2 ),
776+ padding = (0 , 1 ),
777+ height = visible + _FULLSCREEN_CHROME_ROWS ,
784778 )
785779 yield panel
786780
@@ -1035,6 +1029,7 @@ def enter_fullscreen(self) -> bool:
10351029 console = self ._console ,
10361030 screen = True ,
10371031 refresh_per_second = _LIVE_REFRESH_RATE ,
1032+ vertical_overflow = "crop" ,
10381033 )
10391034 try :
10401035 self ._fullscreen_live .start ()
@@ -1078,7 +1073,11 @@ def _restart_compact_unlocked(self) -> None:
10781073 self ._live .start ()
10791074
10801075 def _fullscreen_page_size (self ) -> int :
1081- """Lines to jump on space/b (page down/up)."""
1076+ """Lines to jump on space/b (page down/up).
1077+
1078+ One viewport minus a 2-line overlap so the user can keep their
1079+ place across page jumps.
1080+ """
10821081 try :
10831082 height = self ._console .size .height
10841083 except Exception :
@@ -1110,10 +1109,7 @@ def _handle_fullscreen_key(self, key: str) -> None:
11101109 view = self ._fullscreen_view
11111110 if view is None :
11121111 return # raced with exit
1113- if key in ("q" , FULLSCREEN_PEEK_KEY ):
1114- # Release lock before exit_fullscreen re-acquires it.
1115- pass
1116- else :
1112+ if key not in ("q" , FULLSCREEN_PEEK_KEY ):
11171113 page = self ._fullscreen_page_size ()
11181114 handled = True
11191115 if key == "j" :
0 commit comments