2828from dataclasses import dataclass , field , fields
2929
3030from . import commands , console , input
31+ from .render import RenderCell , RenderLine , RenderedScreen
3132from .utils import wlen , unbracket , disp_str , gen_colors , THEME
3233from .trace import trace
3334
@@ -207,7 +208,7 @@ class Reader:
207208 keymap : tuple [tuple [str , str ], ...] = ()
208209 input_trans : input .KeymapTranslator = field (init = False )
209210 input_trans_stack : list [input .KeymapTranslator ] = field (default_factory = list )
210- screen : list [ str ] = field (default_factory = list )
211+ rendered_screen : RenderedScreen = field (init = False )
211212 screeninfo : list [tuple [int , list [int ]]] = field (init = False )
212213 cxy : tuple [int , int ] = field (init = False )
213214 lxy : tuple [int , int ] = field (init = False )
@@ -218,7 +219,7 @@ class Reader:
218219 ## cached metadata to speed up screen refreshes
219220 @dataclass
220221 class RefreshCache :
221- screen : list [str ] = field (default_factory = list )
222+ render_lines : list [RenderLine ] = field (default_factory = list )
222223 screeninfo : list [tuple [int , list [int ]]] = field (init = False )
223224 line_end_offsets : list [int ] = field (default_factory = list )
224225 pos : int = field (init = False )
@@ -228,11 +229,13 @@ class RefreshCache:
228229
229230 def update_cache (self ,
230231 reader : Reader ,
231- screen : list [str ],
232+ render_lines : list [RenderLine ],
232233 screeninfo : list [tuple [int , list [int ]]],
234+ line_end_offsets : list [int ],
233235 ) -> None :
234- self .screen = screen .copy ()
236+ self .render_lines = render_lines .copy ()
235237 self .screeninfo = screeninfo .copy ()
238+ self .line_end_offsets = line_end_offsets .copy ()
236239 self .pos = reader .pos
237240 self .cxy = reader .cxy
238241 self .dimensions = reader .console .width , reader .console .height
@@ -273,18 +276,23 @@ def __post_init__(self) -> None:
273276 self .screeninfo = [(0 , [])]
274277 self .cxy = self .pos2xy ()
275278 self .lxy = (self .pos , 0 )
279+ self .rendered_screen = RenderedScreen .empty ()
276280 self .can_colorize = _colorize .can_colorize ()
277281
278282 self .last_refresh_cache .screeninfo = self .screeninfo
279283 self .last_refresh_cache .pos = self .pos
280284 self .last_refresh_cache .cxy = self .cxy
281285 self .last_refresh_cache .dimensions = (0 , 0 )
282286
287+ @property
288+ def screen (self ) -> list [str ]:
289+ return list (self .rendered_screen .screen_lines )
290+
283291 def collect_keymap (self ) -> tuple [tuple [KeySpec , CommandName ], ...]:
284292 return default_keymap
285293
286- def calc_screen (self ) -> list [ str ] :
287- """Translate changes in self.buffer into changes in self.console. screen."""
294+ def calc_screen (self ) -> RenderedScreen :
295+ """Translate changes in self.buffer into a structured rendered screen."""
288296 # Since the last call to calc_screen:
289297 # screen and screeninfo may differ due to a completion menu being shown
290298 # pos and cxy may differ due to edits, cursor movements, or completion menus
@@ -297,14 +305,9 @@ def calc_screen(self) -> list[str]:
297305 if self .last_refresh_cache .valid (self ):
298306 offset , num_common_lines = self .last_refresh_cache .get_cached_location (self )
299307
300- screen = self .last_refresh_cache .screen
301- del screen [num_common_lines :]
302-
303- screeninfo = self .last_refresh_cache .screeninfo
304- del screeninfo [num_common_lines :]
305-
306- last_refresh_line_end_offsets = self .last_refresh_cache .line_end_offsets
307- del last_refresh_line_end_offsets [num_common_lines :]
308+ render_lines = self .last_refresh_cache .render_lines [:num_common_lines ]
309+ screeninfo = self .last_refresh_cache .screeninfo [:num_common_lines ]
310+ last_refresh_line_end_offsets = self .last_refresh_cache .line_end_offsets [:num_common_lines ]
308311
309312 pos = self .pos
310313 pos -= offset
@@ -339,7 +342,7 @@ def calc_screen(self) -> list[str]:
339342 while "\n " in prompt :
340343 pre_prompt , _ , prompt = prompt .partition ("\n " )
341344 last_refresh_line_end_offsets .append (offset )
342- screen .append (pre_prompt )
345+ render_lines .append (RenderLine . from_rendered_text ( pre_prompt ) )
343346 screeninfo .append ((0 , []))
344347 pos -= line_len + 1
345348 prompt , prompt_len = self .process_prompt (prompt )
@@ -348,7 +351,8 @@ def calc_screen(self) -> list[str]:
348351 if wrapcount == 0 or not char_widths :
349352 offset += line_len + 1 # Takes all of the line plus the newline
350353 last_refresh_line_end_offsets .append (offset )
351- screen .append (prompt + "" .join (chars ))
354+ render_line = self ._render_line (prompt , chars , char_widths )
355+ render_lines .append (render_line )
352356 screeninfo .append ((prompt_len , char_widths ))
353357 else :
354358 pre = prompt
@@ -370,9 +374,14 @@ def calc_screen(self) -> list[str]:
370374 post = ""
371375 after = []
372376 last_refresh_line_end_offsets .append (offset )
373- render = pre + "" .join (chars [:index_to_wrap_before ]) + post
377+ render_line = self ._render_line (
378+ pre ,
379+ chars [:index_to_wrap_before ],
380+ char_widths [:index_to_wrap_before ],
381+ post ,
382+ )
374383 render_widths = char_widths [:index_to_wrap_before ] + after
375- screen .append (render )
384+ render_lines .append (render_line )
376385 screeninfo .append ((prelen , render_widths ))
377386 chars = chars [index_to_wrap_before :]
378387 char_widths = char_widths [index_to_wrap_before :]
@@ -382,11 +391,35 @@ def calc_screen(self) -> list[str]:
382391 self .cxy = self .pos2xy ()
383392 if self .msg :
384393 for mline in self .msg .split ("\n " ):
385- screen .append (mline )
394+ render_lines .append (RenderLine . from_rendered_text ( mline ) )
386395 screeninfo .append ((0 , []))
387396
388- self .last_refresh_cache .update_cache (self , screen , screeninfo )
389- return screen
397+ self .rendered_screen = RenderedScreen (tuple (render_lines ), self .cxy )
398+ self .last_refresh_cache .update_cache (
399+ self ,
400+ render_lines ,
401+ screeninfo ,
402+ last_refresh_line_end_offsets ,
403+ )
404+ return self .rendered_screen
405+
406+ @staticmethod
407+ def _render_line (
408+ prefix : str ,
409+ chars : list [str ],
410+ char_widths : list [int ],
411+ suffix : str = "" ,
412+ ) -> RenderLine :
413+ cells : list [RenderCell ] = []
414+ if prefix :
415+ cells .extend (RenderLine .from_rendered_text (prefix ).cells )
416+ cells .extend (
417+ RenderCell (text , width , "\x1b " in text )
418+ for text , width in zip (chars , char_widths )
419+ )
420+ if suffix :
421+ cells .append (RenderCell (suffix , wlen (suffix ), "\x1b " in suffix ))
422+ return RenderLine .from_cells (cells )
390423
391424 @staticmethod
392425 def process_prompt (prompt : str ) -> tuple [str , int ]:
@@ -645,8 +678,17 @@ def update_screen(self) -> None:
645678 def refresh (self ) -> None :
646679 """Recalculate and refresh the screen."""
647680 # this call sets up self.cxy, so call it first.
648- self .screen = self .calc_screen ()
649- self .console .refresh (self .screen , self .cxy )
681+ rendered_screen = self .calc_screen ()
682+ trace (
683+ "reader.refresh cursor={cursor} lines={lines} "
684+ "dims=({width},{height}) dirty={dirty}" ,
685+ cursor = self .cxy ,
686+ lines = len (rendered_screen .lines ),
687+ width = self .console .width ,
688+ height = self .console .height ,
689+ dirty = self .dirty ,
690+ )
691+ self .console .refresh (rendered_screen )
650692 self .dirty = False
651693
652694 def do_cmd (self , cmd : tuple [str , list [str ]]) -> None :
0 commit comments