11from __future__ import annotations
22
33from dataclasses import dataclass
4+ from typing import Self
45
56from .content import ContentFragment , ContentLine
7+ from .types import CursorXY , ScreenInfoRow
68
79
810@dataclass (frozen = True , slots = True )
@@ -17,7 +19,7 @@ def width(self) -> int:
1719 return self .prompt_width + sum (self .char_widths ) + self .suffix_width
1820
1921 @property
20- def screeninfo (self ) -> tuple [ int , list [ int ]] :
22+ def screeninfo (self ) -> ScreenInfoRow :
2123 widths = list (self .char_widths )
2224 if self .suffix_width :
2325 widths .append (self .suffix_width )
@@ -29,11 +31,11 @@ class LayoutMap:
2931 rows : tuple [LayoutRow , ...]
3032
3133 @classmethod
32- def empty (cls ) -> LayoutMap :
34+ def empty (cls ) -> Self :
3335 return cls ((LayoutRow (0 , ()),))
3436
3537 @property
36- def screeninfo (self ) -> list [tuple [ int , list [ int ]] ]:
38+ def screeninfo (self ) -> list [ScreenInfoRow ]:
3739 return [row .screeninfo for row in self .rows ]
3840
3941 def max_column (self , y : int ) -> int :
@@ -42,13 +44,18 @@ def max_column(self, y: int) -> int:
4244 def max_row (self ) -> int :
4345 return len (self .rows ) - 1
4446
45- def pos_to_xy (self , pos : int ) -> tuple [ int , int ] :
47+ def pos_to_xy (self , pos : int ) -> CursorXY :
4648 if not self .rows :
4749 return 0 , 0
4850
4951 remaining = pos
5052 for y , row in enumerate (self .rows ):
5153 if remaining <= len (row .char_widths ):
54+ # Prompt-only leading rows are terminal scenery, not real
55+ # buffer positions. Treating them as real just manufactures
56+ # bugs.
57+ if remaining == 0 and not row .char_widths and row .buffer_advance == 0 and y < len (self .rows ) - 1 :
58+ continue
5259 x = row .prompt_width
5360 for width in row .char_widths [:remaining ]:
5461 x += width
@@ -58,15 +65,26 @@ def pos_to_xy(self, pos: int) -> tuple[int, int]:
5865 return last_row .width - last_row .suffix_width , len (self .rows ) - 1
5966
6067 def xy_to_pos (self , x : int , y : int ) -> int :
68+ if not self .rows :
69+ return 0
70+
6171 pos = 0
6272 for row in self .rows [:y ]:
6373 pos += row .buffer_advance
6474
6575 row = self .rows [y ]
6676 cur_x = row .prompt_width
67- for width in row .char_widths :
77+ char_widths = row .char_widths
78+ i = 0
79+ for i , width in enumerate (char_widths ):
6880 if cur_x >= x :
69- break
81+ # Include trailing zero-width (combining) chars at this position
82+ for trailing_width in char_widths [i :]:
83+ if trailing_width == 0 :
84+ pos += 1
85+ else :
86+ break
87+ return pos
7088 if width == 0 :
7189 pos += 1
7290 continue
@@ -99,12 +117,16 @@ def layout_content_lines(
99117 width : int ,
100118 start_offset : int ,
101119) -> LayoutResult :
120+ if width <= 0 :
121+ return LayoutResult ((), LayoutMap (()), ())
122+
102123 offset = start_offset
103124 wrapped_rows : list [WrappedRow ] = []
104125 layout_rows : list [LayoutRow ] = []
105126 line_end_offsets : list [int ] = []
106127
107128 for line in lines :
129+ newline_advance = int (line .source .has_newline )
108130 for leading in line .prompt .leading_lines :
109131 line_end_offsets .append (offset )
110132 wrapped_rows .append (
@@ -117,56 +139,62 @@ def layout_content_lines(
117139
118140 prompt_text = line .prompt .text
119141 prompt_width = line .prompt .width
120- remaining = list (line .body )
121- remaining_widths = [ fragment .width for fragment in remaining ]
142+ body = tuple (line .body )
143+ body_widths = tuple ( fragment .width for fragment in body )
122144
123- if not remaining_widths or (sum (remaining_widths ) + prompt_width ) // width == 0 :
124- offset += len (remaining ) + ( 1 if line . source . has_newline else 0 )
145+ if not body_widths or (sum (body_widths ) + prompt_width ) < width :
146+ offset += len (body ) + newline_advance
125147 line_end_offsets .append (offset )
126148 wrapped_rows .append (
127149 WrappedRow (
128150 prompt_text = prompt_text ,
129151 prompt_width = prompt_width ,
130- fragments = tuple ( remaining ) ,
131- layout_widths = tuple ( remaining_widths ) ,
132- buffer_advance = len (remaining ) + ( 1 if line . source . has_newline else 0 ) ,
152+ fragments = body ,
153+ layout_widths = body_widths ,
154+ buffer_advance = len (body ) + newline_advance ,
133155 line_end_offset = offset ,
134156 )
135157 )
136158 layout_rows .append (
137159 LayoutRow (
138160 prompt_width ,
139- tuple ( remaining_widths ) ,
140- buffer_advance = len (remaining ) + ( 1 if line . source . has_newline else 0 ) ,
161+ body_widths ,
162+ buffer_advance = len (body ) + newline_advance ,
141163 )
142164 )
143165 continue
144166
145167 current_prompt = prompt_text
146168 current_prompt_width = prompt_width
169+ start = 0
170+ total = len (body )
147171 while True :
148172 index_to_wrap_before = 0
149173 column = 0
150- for char_width in remaining_widths :
174+ for char_width in body_widths [ start :] :
151175 if column + char_width + current_prompt_width >= width :
152176 break
153177 index_to_wrap_before += 1
154178 column += char_width
155179
156- at_line_end = len (remaining ) <= index_to_wrap_before
180+ if index_to_wrap_before == 0 and start < total :
181+ index_to_wrap_before = 1 # force progress
182+
183+ at_line_end = (start + index_to_wrap_before ) >= total
157184 if at_line_end :
158- offset += index_to_wrap_before + ( 1 if line . source . has_newline else 0 )
185+ offset += index_to_wrap_before + newline_advance
159186 suffix = ""
160187 suffix_width = 0
161- buffer_advance = index_to_wrap_before + ( 1 if line . source . has_newline else 0 )
188+ buffer_advance = index_to_wrap_before + newline_advance
162189 else :
163190 offset += index_to_wrap_before
164191 suffix = "\\ "
165192 suffix_width = 1
166193 buffer_advance = index_to_wrap_before
167194
168- row_fragments = tuple (remaining [:index_to_wrap_before ])
169- row_widths = tuple (remaining_widths [:index_to_wrap_before ])
195+ end = start + index_to_wrap_before
196+ row_fragments = body [start :end ]
197+ row_widths = body_widths [start :end ]
170198 line_end_offsets .append (offset )
171199 wrapped_rows .append (
172200 WrappedRow (
@@ -189,8 +217,7 @@ def layout_content_lines(
189217 )
190218 )
191219
192- remaining = remaining [index_to_wrap_before :]
193- remaining_widths = remaining_widths [index_to_wrap_before :]
220+ start = end
194221 current_prompt = ""
195222 current_prompt_width = 0
196223 if at_line_end :
0 commit comments