Skip to content

Commit

Permalink
visual prompt
Browse files Browse the repository at this point in the history
  • Loading branch information
willmcgugan committed Nov 5, 2024
1 parent b3c8467 commit 6c57e6f
Show file tree
Hide file tree
Showing 6 changed files with 125 additions and 19 deletions.
4 changes: 3 additions & 1 deletion src/textual/color.py
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,7 @@ class Color(NamedTuple):
"""ANSI color index. `-1` means default color. `None` if not an ANSI color."""

@classmethod
def from_rich_color(cls, rich_color: RichColor) -> Color:
def from_rich_color(cls, rich_color: RichColor | None) -> Color:
"""Create a new color from Rich's Color class.
Args:
Expand All @@ -178,6 +178,8 @@ def from_rich_color(cls, rich_color: RichColor) -> Color:
Returns:
A new Color instance.
"""
if rich_color is None:
return TRANSPARENT
r, g, b = rich_color.get_truecolor()
return cls(r, g, b)

Expand Down
34 changes: 23 additions & 11 deletions src/textual/command.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,21 +25,23 @@

import rich.repr
from rich.align import Align
from rich.console import Group, RenderableType
from rich.style import Style
from rich.text import Text
from typing_extensions import Final, TypeAlias

from textual import on, work
from textual.binding import Binding, BindingType
from textual.containers import Horizontal, Vertical
from textual.content import Content
from textual.events import Click, Mount
from textual.fuzzy import Matcher
from textual.message import Message
from textual.reactive import var
from textual.screen import Screen, SystemModalScreen
from textual.timer import Timer
from textual.types import IgnoreReturnCallbackType
from textual.visual import Style as VisualStyle
from textual.visual import VisualType
from textual.widget import Widget
from textual.widgets import Button, Input, LoadingIndicator, OptionList, Static
from textual.widgets.option_list import Option
Expand Down Expand Up @@ -68,7 +70,7 @@ class Hit:
The value should be between 0 (no match) and 1 (complete match).
"""

match_display: RenderableType
match_display: VisualType
"""A string or Rich renderable representation of the hit."""

command: IgnoreReturnCallbackType
Expand All @@ -85,7 +87,7 @@ class Hit:
"""Optional help text for the command."""

@property
def prompt(self) -> RenderableType:
def prompt(self) -> VisualType:
"""The prompt to use when displaying the hit in the command palette."""
return self.match_display

Expand Down Expand Up @@ -116,7 +118,7 @@ def __post_init__(self) -> None:
class DiscoveryHit:
"""Holds the details of a single command search hit."""

display: RenderableType
display: VisualType
"""A string or Rich renderable representation of the hit."""

command: IgnoreReturnCallbackType
Expand All @@ -133,7 +135,7 @@ class DiscoveryHit:
"""Optional help text for the command."""

@property
def prompt(self) -> RenderableType:
def prompt(self) -> VisualType:
"""The prompt to use when displaying the discovery hit in the command palette."""
return self.display

Expand Down Expand Up @@ -326,7 +328,7 @@ class Command(Option):

def __init__(
self,
prompt: RenderableType,
prompt: VisualType,
hit: DiscoveryHit | Hit,
id: str | None = None,
disabled: bool = False,
Expand Down Expand Up @@ -412,7 +414,7 @@ class SearchIcon(Static, inherit_css=False):
icon: var[str] = var("🔎")
"""The icon to display."""

def render(self) -> RenderableType:
def render(self) -> VisualType:
"""Render the icon.
Returns:
Expand Down Expand Up @@ -476,7 +478,9 @@ class CommandPalette(SystemModalScreen):
}
CommandPalette > .command-palette--help-text {
text-style: dim not bold;
# text-style: dim not bold;
text-style: underline;
color: $text-muted;
}
CommandPalette:dark > .command-palette--highlight {
Expand Down Expand Up @@ -1015,11 +1019,19 @@ async def _gather_commands(self, search_value: str) -> None:
while hit:
# Turn the command into something for display, and add it to the
# list of commands that have been gathered so far.

prompt = hit.prompt

content = Content(prompt)
if hit.help:
help_text = Text.from_markup(hit.help)
help_text.stylize(help_style)
prompt = Group(prompt, help_text)
prompt = content.append("\n").append(
Content.styled(hit.help, VisualStyle.from_rich_style(help_style))
)

# if hit.help:
# help_text = Text.from_markup(hit.help)
# help_text.stylize(help_style)
# prompt = Group(prompt, help_text)
gathered_commands.append(Command(prompt, hit, id=str(command_id)))

# Before we go making any changes to the UI, we do a quick
Expand Down
6 changes: 3 additions & 3 deletions src/textual/content.py
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ def __init__(
self._cell_length = cell_length

@classmethod
def styled_text(
def styled(
cls, text: str, style: Style | str = "", cell_length: int | None = None
) -> Content:
if not text:
Expand Down Expand Up @@ -293,7 +293,7 @@ def append(self, content: Content | str) -> Content:
"""
if isinstance(content, str):
return Content(
self.plain,
f"{self.plain}{content}",
self._spans,
(
None
Expand All @@ -304,7 +304,7 @@ def append(self, content: Content | str) -> Content:
return Content("").join([self, content])

def append_text(self, text: str, style: Style | str = "") -> Content:
return self.append(Content.styled_text(text, style))
return self.append(Content.styled(text, style))

def join(self, lines: Iterable[Content]) -> Content:
"""Join an iterable of content.
Expand Down
5 changes: 2 additions & 3 deletions src/textual/strip.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@
from textual.constants import DEBUG
from textual.css.types import AlignHorizontal, AlignVertical
from textual.filter import LineFilter
from textual.geometry import Size


def get_line_length(segments: Iterable[Segment]) -> int:
Expand Down Expand Up @@ -164,13 +163,13 @@ def align(
cls,
strips: list[Strip],
style: Style,
size: Size,
width: int,
height: int,
horizontal: AlignHorizontal,
vertical: AlignVertical,
) -> Iterable[Strip]:
if not strips:
return
width, height = size
line_lengths = [strip.cell_length for strip in strips]
shape_width = max(line_lengths)
shape_height = len(line_lengths)
Expand Down
94 changes: 93 additions & 1 deletion src/textual/visual.py
Original file line number Diff line number Diff line change
@@ -1,25 +1,46 @@
from __future__ import annotations

import sys
from abc import ABC, abstractmethod
from dataclasses import dataclass
from functools import cached_property, lru_cache
from marshal import loads
from typing import Any, Iterable, Protocol, cast

import rich.repr
from rich.console import JustifyMethod, OverflowMethod
from rich.console import (
Console,
ConsoleOptions,
JustifyMethod,
OverflowMethod,
RenderableType,
RenderResult,
)
from rich.measure import Measurement
from rich.segment import Segment
from rich.style import Style as RichStyle

from textual.color import TRANSPARENT, Color
from textual.strip import Strip

if sys.version_info >= (3, 8):
from typing import Literal
else:
from typing_extensions import Literal # pragma: no cover


_NULL_RICH_STYLE = RichStyle()


class SupportsTextualize(Protocol):
"""An object that supports the textualize protocol."""

def textualize(self, obj: object) -> Visual | None: ...


VisualType = RenderableType | SupportsTextualize


def textualize(obj: object) -> Visual | None:
"""Get a visual instance from an object.
Expand Down Expand Up @@ -79,6 +100,19 @@ def __add__(self, other: object) -> Style:
)
return new_style

@classmethod
@lru_cache(maxsize=1024)
def from_rich_style(cls, rich_style: RichStyle) -> Style:
return Style(
Color.from_rich_color(rich_style.bgcolor),
Color.from_rich_color(rich_style.color),
bold=rich_style.bold,
dim=rich_style.dim,
italic=rich_style.italic,
underline=rich_style.underline,
strike=rich_style.strike,
)

@cached_property
def rich_style(self) -> RichStyle:
return RichStyle(
Expand Down Expand Up @@ -176,3 +210,61 @@ def get_height(self, width: int) -> int:
Returns:
A height in lines.
"""

@classmethod
def render(
cls,
visual: Visual,
width: int,
height: int,
style: Style,
justify: Literal["default", "left", "center", "right", "full"],
align_horizontal: Literal["left", "center", "right"],
align_vertical: Literal["top", "middle", "bottom"],
) -> list[Strip]:
strips = visual.render_strips(
width,
height=height,
base_style=style,
justify=justify,
)
strips = list(
Strip.align(
strips,
_NULL_RICH_STYLE,
width,
height,
align_horizontal,
align_vertical,
)
)
return strips

def __rich_measure__(
self, console: "Console", options: "ConsoleOptions"
) -> Measurement:
tab_size = console.tab_size
return Measurement(
self.get_minimal_width(tab_size),
self.get_optimal_width(tab_size),
)

def __rich_console__(
self, console: Console, options: ConsoleOptions
) -> RenderResult:
width = options.max_width
tab_size = console.tab_size
height = options.height

strips = self.render_strips(
width,
height=height,
justify=options.justify,
overflow=options.overflow,
no_wrap=options.no_wrap,
tab_size=tab_size,
)
new_line = Segment.line()
for strip in strips:
yield from Segment.adjust_line_length(strip._segments, width)
yield new_line
1 change: 1 addition & 0 deletions src/textual/widgets/_option_list.py
Original file line number Diff line number Diff line change
Expand Up @@ -468,6 +468,7 @@ def _render_option_content(
Returns:
A list of strips.
"""

cache_key = (option_index, style, width)
if (strips := self._content_render_cache.get(cache_key, None)) is not None:
return strips
Expand Down

0 comments on commit 6c57e6f

Please sign in to comment.