Skip to content

Commit 9f93a35

Browse files
committed
no wrap
1 parent 02f8e0d commit 9f93a35

File tree

9 files changed

+182
-66
lines changed

9 files changed

+182
-66
lines changed

src/textual/command.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1101,13 +1101,16 @@ async def _gather_commands(self, search_value: str) -> None:
11011101
def build_prompt() -> Iterable[Content]:
11021102
"""Generator for prompt content."""
11031103
assert hit is not None
1104-
yield Content.from_rich_text(hit.prompt)
1104+
if isinstance(hit.prompt, Text):
1105+
yield Content.from_rich_text(hit.prompt)
1106+
else:
1107+
yield Content.from_markup(hit.prompt)
11051108
# Optional help text
11061109
if hit.help:
11071110
help_style = VisualStyle.from_styles(
11081111
self.get_component_styles("command-palette--help-text")
11091112
)
1110-
yield Content.from_rich_text(hit.help).stylize_before(help_style)
1113+
yield Content.from_markup(hit.help).stylize_before(help_style)
11111114

11121115
prompt = Content("\n").join(build_prompt())
11131116

src/textual/content.py

Lines changed: 28 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
import rich.repr
1818
from rich._wrap import divide_line
1919
from rich.cells import set_cell_size
20-
from rich.console import Console, OverflowMethod
20+
from rich.console import Console
2121
from rich.segment import Segment, Segments
2222
from rich.style import Style as RichStyle
2323
from rich.terminal_theme import TerminalTheme
@@ -28,12 +28,11 @@
2828
from textual._context import active_app
2929
from textual._loop import loop_last
3030
from textual.color import Color
31-
from textual.css.styles import RulesMap
32-
from textual.css.types import TextAlign
31+
from textual.css.types import TextAlign, TextOverflow
3332
from textual.selection import Selection
3433
from textual.strip import Strip
3534
from textual.style import Style
36-
from textual.visual import Visual
35+
from textual.visual import Rules, Visual
3736

3837
if TYPE_CHECKING:
3938
pass
@@ -132,9 +131,6 @@ def __init__(
132131
text: text content.
133132
spans: Optional list of spans.
134133
cell_length: Cell length of text if known, otherwise `None`.
135-
align: Align method.
136-
no_wrap: Disable wrapping.
137-
ellipsis: Add ellipsis when wrapping is disabled and text is cropped.
138134
"""
139135
self._text: str = _strip_control_codes(text)
140136
self._spans: list[Span] = [] if spans is None else spans
@@ -174,19 +170,23 @@ def markup(self) -> str:
174170
return markup
175171

176172
@classmethod
177-
def from_markup(cls, markup: str) -> Content:
173+
def from_markup(cls, markup: str | Content) -> Content:
178174
"""Create content from Textual markup.
179175
176+
If `markup` is already Content, return it unmodified.
177+
180178
!!! note
181179
Textual markup is not the same as Rich markup. Use [Text.parse] to parse Rich Console markup.
182180
183181
184182
Args:
185-
markup: Textual Markup
183+
markup: Textual markup, or Content.
186184
187185
Returns:
188186
New Content instance.
189187
"""
188+
if isinstance(markup, Content):
189+
return markup
190190
from textual.markup import to_content
191191

192192
content = to_content(markup)
@@ -286,7 +286,7 @@ def __lt__(self, other: object) -> bool:
286286

287287
def get_optimal_width(
288288
self,
289-
rules: RulesMap,
289+
rules: Rules,
290290
container_width: int,
291291
) -> int:
292292
"""Get optimal width of the visual to display its content. Part of the Textual Visual protocol.
@@ -302,7 +302,7 @@ def get_optimal_width(
302302
lines = self.without_spans.split("\n")
303303
return max(line.cell_length for line in lines)
304304

305-
def get_height(self, rules: RulesMap, width: int) -> int:
305+
def get_height(self, rules: Rules, width: int) -> int:
306306
"""Get the height of the visual if rendered with the given width. Part of the Textual Visual protocol.
307307
308308
Args:
@@ -312,14 +312,16 @@ def get_height(self, rules: RulesMap, width: int) -> int:
312312
Returns:
313313
A height in lines.
314314
"""
315-
lines = self.without_spans._wrap_and_format(width)
315+
lines = self.without_spans._wrap_and_format(
316+
width, no_wrap=rules.get("text_wrap") == "nowrap"
317+
)
316318
return len(lines)
317319

318320
def _wrap_and_format(
319321
self,
320322
width: int,
321323
align: TextAlign = "left",
322-
overflow: OverflowMethod = "fold",
324+
overflow: TextOverflow = "fold",
323325
no_wrap: bool = False,
324326
tab_size: int = 8,
325327
selection: Selection | None = None,
@@ -355,13 +357,15 @@ def get_span(y: int) -> tuple[int, int] | None:
355357
end = len(line.plain)
356358
line = line.stylize(selection_style, start, end)
357359

358-
content_line = FormattedLine(
359-
line.expand_tabs(tab_size), width, y=y, align=align
360-
)
360+
line = line.expand_tabs(tab_size)
361361

362362
if no_wrap:
363+
if overflow == "ellipsis" and no_wrap:
364+
line = line.truncate(width, ellipsis=True)
365+
content_line = FormattedLine(line, width, y=y, align=align)
363366
new_lines = [content_line]
364367
else:
368+
content_line = FormattedLine(line, width, y=y, align=align)
365369
offsets = divide_line(line.plain, width, fold=overflow == "fold")
366370
divided_lines = content_line.content.divide(offsets)
367371
new_lines = [
@@ -378,7 +382,7 @@ def get_span(y: int) -> tuple[int, int] | None:
378382

379383
def render_strips(
380384
self,
381-
rules: RulesMap,
385+
rules: Rules,
382386
width: int,
383387
height: int | None,
384388
style: Style,
@@ -391,8 +395,8 @@ def render_strips(
391395
lines = self._wrap_and_format(
392396
width,
393397
align=rules.get("text_align", "left"),
394-
overflow="fold",
395-
no_wrap=False,
398+
overflow=rules.get("text_overflow", "fold"),
399+
no_wrap=rules.get("text_wrap", "wrap") == "nowrap",
396400
tab_size=8,
397401
selection=selection,
398402
selection_style=selection_style,
@@ -414,8 +418,11 @@ def __hash__(self) -> int:
414418
return hash(self._text)
415419

416420
def __rich_repr__(self) -> rich.repr.Result:
417-
yield self._text
418-
yield "spans", self._spans, []
421+
try:
422+
yield self._text
423+
yield "spans", self._spans, []
424+
except AttributeError:
425+
pass
419426

420427
@property
421428
def spans(self) -> Sequence[Span]:

src/textual/css/_styles_builder.py

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,8 @@
5252
VALID_SCROLLBAR_GUTTER,
5353
VALID_STYLE_FLAGS,
5454
VALID_TEXT_ALIGN,
55+
VALID_TEXT_OVERFLOW,
56+
VALID_TEXT_WRAP,
5557
VALID_VISIBILITY,
5658
)
5759
from textual.css.errors import DeclarationError, StyleValueError
@@ -67,7 +69,15 @@
6769
from textual.css.styles import Styles
6870
from textual.css.tokenize import Token
6971
from textual.css.transition import Transition
70-
from textual.css.types import BoxSizing, Display, EdgeType, Overflow, Visibility
72+
from textual.css.types import (
73+
BoxSizing,
74+
Display,
75+
EdgeType,
76+
Overflow,
77+
TextOverflow,
78+
TextWrap,
79+
Visibility,
80+
)
7181
from textual.geometry import Spacing, SpacingDimensions, clamp
7282
from textual.suggestions import get_suggestion
7383

@@ -353,6 +363,52 @@ def process_visibility(self, name: str, tokens: list[Token]) -> None:
353363
"visibility", valid_values=list(VALID_VISIBILITY), context="css"
354364
)
355365

366+
def process_text_wrap(self, name: str, tokens: list[Token]) -> None:
367+
for token in tokens:
368+
name, value, _, _, location, _ = token
369+
if name == "token":
370+
value = value.lower()
371+
if value in VALID_TEXT_WRAP:
372+
self.styles._rules["text_wrap"] = cast(TextWrap, value)
373+
else:
374+
self.error(
375+
name,
376+
token,
377+
string_enum_help_text(
378+
"text-wrap",
379+
valid_values=list(VALID_TEXT_WRAP),
380+
context="css",
381+
),
382+
)
383+
else:
384+
string_enum_help_text(
385+
"text-wrap", valid_values=list(VALID_TEXT_WRAP), context="css"
386+
)
387+
388+
def process_text_overflow(self, name: str, tokens: list[Token]) -> None:
389+
for token in tokens:
390+
name, value, _, _, location, _ = token
391+
if name == "token":
392+
value = value.lower()
393+
if value in VALID_TEXT_OVERFLOW:
394+
self.styles._rules["text_overflow"] = cast(TextOverflow, value)
395+
else:
396+
self.error(
397+
name,
398+
token,
399+
string_enum_help_text(
400+
"text-overflow",
401+
valid_values=list(VALID_TEXT_OVERFLOW),
402+
context="css",
403+
),
404+
)
405+
else:
406+
string_enum_help_text(
407+
"text-overflow",
408+
valid_values=list(VALID_TEXT_OVERFLOW),
409+
context="css",
410+
)
411+
356412
def _process_fractional(self, name: str, tokens: list[Token]) -> None:
357413
if not tokens:
358414
return

src/textual/css/constants.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,9 @@
8383
VALID_CONSTRAIN: Final = {"inflect", "inside", "none"}
8484
VALID_KEYLINE: Final = {"none", "thin", "heavy", "double"}
8585
VALID_HATCH: Final = {"left", "right", "cross", "vertical", "horizontal"}
86+
VALID_TEXT_WRAP: Final = {"wrap", "nowrap"}
87+
VALID_TEXT_OVERFLOW: Final = {"clip", "fold", "ellipsis"}
88+
8689
HATCHES: Final = {
8790
"left": "╲",
8891
"right": "╱",

0 commit comments

Comments
 (0)