ai-station/.venv/lib/python3.12/site-packages/textual/_segment_tools.py

308 lines
8.5 KiB
Python
Raw Normal View History

2025-12-25 14:54:33 +00:00
"""
Tools for processing Segments, or lists of Segments.
"""
from __future__ import annotations
import re
from functools import lru_cache
from typing import Iterable
from rich.segment import Segment
from rich.style import Style
from textual._cells import cell_len
from textual.css.types import AlignHorizontal, AlignVertical
from textual.geometry import Size
@lru_cache(1024 * 8)
def make_blank(width, style: Style) -> Segment:
"""Make a blank segment.
Args:
width: Width of blank.
style: Style of blank.
Returns:
A single segment
"""
return Segment(" " * width, style)
class NoCellPositionForIndex(Exception):
pass
def index_to_cell_position(segments: Iterable[Segment], index: int) -> int:
"""Given a character index, return the cell position of that character within
an Iterable of Segments. This is the sum of the cell lengths of all the characters
*before* the character at `index`.
Args:
segments: The segments to find the cell position within.
index: The index to convert into a cell position.
Returns:
The cell position of the character at `index`.
Raises:
NoCellPositionForIndex: If the supplied index doesn't fall within the given segments.
"""
if not segments:
raise NoCellPositionForIndex
if index == 0:
return 0
cell_position_end = 0
segment_length = 0
segment_end_index = 0
segment_cell_length = 0
text = ""
iter_segments = iter(segments)
try:
while segment_end_index < index:
segment = next(iter_segments)
text = segment.text
segment_length = len(text)
segment_cell_length = cell_len(text)
cell_position_end += segment_cell_length
segment_end_index += segment_length
except StopIteration:
raise NoCellPositionForIndex
# Check how far into this segment the target index is
segment_index_start = segment_end_index - segment_length
index_within_segment = index - segment_index_start
segment_cell_start = cell_position_end - segment_cell_length
return segment_cell_start + cell_len(text[:index_within_segment])
def line_crop(
segments: list[Segment], start: int, end: int, total: int
) -> list[Segment]:
"""Crops a list of segments between two cell offsets.
Args:
segments: A list of Segments for a line.
start: Start offset (cells)
end: End offset (cells, exclusive)
total: Total cell length of segments.
Returns:
A new shorter list of segments
"""
# This is essentially a specialized version of Segment.divide
# The following line has equivalent functionality (but a little slower)
# return list(Segment.divide(segments, [start, end]))[1]
_cell_len = cell_len
pos = 0
output_segments: list[Segment] = []
add_segment = output_segments.append
iter_segments = iter(segments)
segment: Segment | None = None
for segment in iter_segments:
end_pos = pos + _cell_len(segment.text)
if end_pos > start:
segment = segment.split_cells(start - pos)[1]
break
pos = end_pos
else:
return []
if end >= total:
# The end crop is the end of the segments, so we can collect all remaining segments
if segment:
add_segment(segment)
output_segments.extend(iter_segments)
return output_segments
pos = start
while segment is not None:
end_pos = pos + _cell_len(segment.text)
if end_pos < end:
add_segment(segment)
else:
add_segment(segment.split_cells(end - pos)[0])
break
pos = end_pos
segment = next(iter_segments, None)
return output_segments
def line_trim(segments: list[Segment], start: bool, end: bool) -> list[Segment]:
"""Optionally remove a cell from the start and / or end of a list of segments.
Args:
segments: A line (list of Segments)
start: Remove cell from start.
end: Remove cell from end.
Returns:
A new list of segments.
"""
segments = segments.copy()
if segments and start:
_, first_segment = segments[0].split_cells(1)
if first_segment.text:
segments[0] = first_segment
else:
segments.pop(0)
if segments and end:
last_segment = segments[-1]
last_segment, _ = last_segment.split_cells(len(last_segment.text) - 1)
if last_segment.text:
segments[-1] = last_segment
else:
segments.pop()
return segments
def line_pad(
segments: Iterable[Segment], pad_left: int, pad_right: int, style: Style
) -> list[Segment]:
"""Adds padding to the left and / or right of a list of segments.
Args:
segments: A line of segments.
pad_left: Cells to pad on the left.
pad_right: Cells to pad on the right.
style: Style of padded cells.
Returns:
A new line with padding.
"""
if pad_left and pad_right:
return [
make_blank(pad_left, style),
*segments,
make_blank(pad_right, style),
]
elif pad_left:
return [
make_blank(pad_left, style),
*segments,
]
elif pad_right:
return [
*segments,
make_blank(pad_right, style),
]
return list(segments)
def align_lines(
lines: list[list[Segment]],
style: Style,
size: Size,
horizontal: AlignHorizontal,
vertical: AlignVertical,
) -> Iterable[list[Segment]]:
"""Align lines.
Args:
lines: A list of lines.
style: Background style.
size: Size of container.
horizontal: Horizontal alignment.
vertical: Vertical alignment.
Returns:
Aligned lines.
"""
if not lines:
return
width, height = size
get_line_length = Segment.get_line_length
line_lengths = [get_line_length(line) for line in lines]
shape_width = max(line_lengths)
shape_height = len(line_lengths)
def blank_lines(count: int) -> list[list[Segment]]:
"""Create blank lines.
Args:
count: Desired number of blank lines.
Returns:
A list of blank lines.
"""
return [[make_blank(width, style)]] * count
top_blank_lines = bottom_blank_lines = 0
vertical_excess_space = max(0, height - shape_height)
if vertical == "top":
bottom_blank_lines = vertical_excess_space
elif vertical == "middle":
top_blank_lines = vertical_excess_space // 2
bottom_blank_lines = vertical_excess_space - top_blank_lines
elif vertical == "bottom":
top_blank_lines = vertical_excess_space
if top_blank_lines:
yield from blank_lines(top_blank_lines)
if horizontal == "left":
for cell_length, line in zip(line_lengths, lines):
if cell_length == width:
yield line
else:
yield line_pad(line, 0, width - cell_length, style)
elif horizontal == "center":
left_space = max(0, width - shape_width) // 2
for cell_length, line in zip(line_lengths, lines):
if cell_length == width:
yield line
else:
yield line_pad(
line, left_space, width - cell_length - left_space, style
)
elif horizontal == "right":
for cell_length, line in zip(line_lengths, lines):
if width == cell_length:
yield line
else:
yield line_pad(line, width - cell_length, 0, style)
if bottom_blank_lines:
yield from blank_lines(bottom_blank_lines)
_re_spaces = re.compile(r"(\s+|\S+)")
def apply_hatch(
segments: Iterable[Segment],
character: str,
hatch_style: Style,
_split=_re_spaces.split,
) -> Iterable[Segment]:
"""Replace run of spaces with another character + style.
Args:
segments: Segments to process.
character: Character to replace spaces.
hatch_style: Style of replacement characters.
Yields:
Segments.
"""
_Segment = Segment
for segment in segments:
if " " not in segment.text:
yield segment
else:
text, style, _ = segment
for token in _split(text):
if token:
if token.isspace():
yield _Segment(character * len(token), hatch_style)
else:
yield _Segment(token, style)