Changed code to support older Python versions
This commit is contained in:
parent
eb92d2d36f
commit
582458cdd0
5027 changed files with 794942 additions and 4 deletions
|
|
@ -0,0 +1,7 @@
|
|||
from .base import BaseStyle
|
||||
from .border import BorderedStyle
|
||||
from .fancy import FancyStyle
|
||||
from .minimal import MinimalStyle
|
||||
from .tagged import TaggedStyle
|
||||
|
||||
__all__ = ["BaseStyle", "BorderedStyle", "TaggedStyle", "FancyStyle", "MinimalStyle"]
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
467
venv/lib/python3.11/site-packages/rich_toolkit/styles/base.py
Normal file
467
venv/lib/python3.11/site-packages/rich_toolkit/styles/base.py
Normal file
|
|
@ -0,0 +1,467 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Dict, Optional, Type, TypeVar, Union
|
||||
|
||||
from rich.color import Color
|
||||
from rich.console import Console, ConsoleRenderable, Group, RenderableType
|
||||
from rich.text import Text
|
||||
from rich.theme import Theme
|
||||
from typing_extensions import Literal
|
||||
|
||||
from rich_toolkit.button import Button
|
||||
from rich_toolkit.container import Container
|
||||
from rich_toolkit.element import CursorOffset, Element
|
||||
from rich_toolkit.input import Input
|
||||
from rich_toolkit.menu import Menu
|
||||
from rich_toolkit.progress import Progress, ProgressLine
|
||||
from rich_toolkit.spacer import Spacer
|
||||
from rich_toolkit.utils.colors import (
|
||||
fade_text,
|
||||
get_terminal_background_color,
|
||||
get_terminal_text_color,
|
||||
lighten,
|
||||
)
|
||||
|
||||
ConsoleRenderableClass = TypeVar(
|
||||
"ConsoleRenderableClass", bound=Type[ConsoleRenderable]
|
||||
)
|
||||
|
||||
|
||||
class BaseStyle:
|
||||
brightness_multiplier = 0.1
|
||||
|
||||
base_theme = {
|
||||
"tag.title": "bold",
|
||||
"tag": "bold",
|
||||
"text": "#ffffff",
|
||||
"selected": "green",
|
||||
"result": "white",
|
||||
"progress": "on #893AE3",
|
||||
"error": "red",
|
||||
"cancelled": "red",
|
||||
# is there a way to make nested styles?
|
||||
# like label.active uses active style if not set?
|
||||
"active": "green",
|
||||
"title.error": "white",
|
||||
"title.cancelled": "white",
|
||||
"placeholder": "grey62",
|
||||
"placeholder.cancelled": "grey62 strike",
|
||||
}
|
||||
|
||||
_should_show_progress_title = True
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
theme: Optional[Dict[str, str]] = None,
|
||||
background_color: str = "#000000",
|
||||
text_color: str = "#FFFFFF",
|
||||
):
|
||||
self.background_color = get_terminal_background_color(background_color)
|
||||
self.text_color = get_terminal_text_color(text_color)
|
||||
self.animation_counter = 0
|
||||
|
||||
base_theme = Theme(self.base_theme)
|
||||
self.console = Console(theme=base_theme)
|
||||
|
||||
if theme:
|
||||
self.console.push_theme(Theme(theme))
|
||||
|
||||
def empty_line(self) -> RenderableType:
|
||||
return " "
|
||||
|
||||
def _get_animation_colors(
|
||||
self,
|
||||
steps: int = 5,
|
||||
breathe: bool = False,
|
||||
animation_status: Literal["started", "stopped", "error"] = "started",
|
||||
**metadata: Any,
|
||||
) -> list[Color]:
|
||||
animated = animation_status == "started"
|
||||
|
||||
if animation_status == "error":
|
||||
base_color = self.console.get_style("error").color
|
||||
|
||||
if base_color is None:
|
||||
base_color = Color.parse("red")
|
||||
|
||||
else:
|
||||
base_color = self.console.get_style("progress").bgcolor
|
||||
|
||||
if not base_color:
|
||||
base_color = Color.from_rgb(255, 255, 255)
|
||||
|
||||
if breathe:
|
||||
steps = steps // 2
|
||||
|
||||
if animated and base_color.triplet is not None:
|
||||
colors = [
|
||||
lighten(base_color, self.brightness_multiplier * i)
|
||||
for i in range(0, steps)
|
||||
]
|
||||
|
||||
else:
|
||||
colors = [base_color] * steps
|
||||
|
||||
if breathe:
|
||||
colors = colors + colors[::-1]
|
||||
|
||||
return colors
|
||||
|
||||
def get_cursor_offset_for_element(
|
||||
self, element: Element, parent: Optional[Element] = None
|
||||
) -> CursorOffset:
|
||||
return element.cursor_offset
|
||||
|
||||
def render_element(
|
||||
self,
|
||||
element: Any,
|
||||
is_active: bool = False,
|
||||
done: bool = False,
|
||||
parent: Optional[Element] = None,
|
||||
**kwargs: Any,
|
||||
) -> RenderableType:
|
||||
if isinstance(element, str):
|
||||
return self.render_string(element, is_active, done, parent)
|
||||
elif isinstance(element, Button):
|
||||
return self.render_button(element, is_active, done, parent)
|
||||
elif isinstance(element, Container):
|
||||
return self.render_container(element, is_active, done, parent)
|
||||
elif isinstance(element, Input):
|
||||
return self.render_input(element, is_active, done, parent)
|
||||
elif isinstance(element, Menu):
|
||||
return self.render_menu(element, is_active, done, parent)
|
||||
elif isinstance(element, Progress):
|
||||
self.animation_counter += 1
|
||||
|
||||
return self.render_progress(element, is_active, done, parent)
|
||||
elif isinstance(element, ProgressLine):
|
||||
return self.render_progress_log_line(
|
||||
element.text,
|
||||
parent=parent,
|
||||
index=kwargs.get("index", 0),
|
||||
max_lines=kwargs.get("max_lines", -1),
|
||||
total_lines=kwargs.get("total_lines", -1),
|
||||
)
|
||||
elif isinstance(element, Spacer):
|
||||
return self.render_spacer()
|
||||
elif isinstance(element, ConsoleRenderable):
|
||||
return element
|
||||
|
||||
raise ValueError(f"Unknown element type: {type(element)}")
|
||||
|
||||
def render_string(
|
||||
self,
|
||||
string: str,
|
||||
is_active: bool = False,
|
||||
done: bool = False,
|
||||
parent: Optional[Element] = None,
|
||||
) -> RenderableType:
|
||||
return string
|
||||
|
||||
def render_button(
|
||||
self,
|
||||
element: Button,
|
||||
is_active: bool = False,
|
||||
done: bool = False,
|
||||
parent: Optional[Element] = None,
|
||||
) -> RenderableType:
|
||||
style = "black on blue" if is_active else "white on black"
|
||||
return Text(f" {element.label} ", style=style)
|
||||
|
||||
def render_spacer(self) -> RenderableType:
|
||||
return ""
|
||||
|
||||
def render_container(
|
||||
self,
|
||||
container: Container,
|
||||
is_active: bool = False,
|
||||
done: bool = False,
|
||||
parent: Optional[Element] = None,
|
||||
) -> RenderableType:
|
||||
content = []
|
||||
|
||||
for i, element in enumerate(container.elements):
|
||||
content.append(
|
||||
self.render_element(
|
||||
element,
|
||||
is_active=i == container.active_element_index,
|
||||
done=done,
|
||||
parent=container,
|
||||
)
|
||||
)
|
||||
|
||||
return Group(*content, "\n" if not done else "")
|
||||
|
||||
def render_input(
|
||||
self,
|
||||
element: Input,
|
||||
is_active: bool = False,
|
||||
done: bool = False,
|
||||
parent: Optional[Element] = None,
|
||||
) -> RenderableType:
|
||||
label = self.render_input_label(element, is_active=is_active, parent=parent)
|
||||
text = self.render_input_value(
|
||||
element, is_active=is_active, parent=parent, done=done
|
||||
)
|
||||
|
||||
contents = []
|
||||
|
||||
if element.inline or done:
|
||||
if done and element.password:
|
||||
text = "*" * len(element.text)
|
||||
if label:
|
||||
text = f"{label} {text}"
|
||||
|
||||
contents.append(text)
|
||||
else:
|
||||
if label:
|
||||
contents.append(label)
|
||||
|
||||
contents.append(text)
|
||||
|
||||
if validation_message := self.render_validation_message(element):
|
||||
contents.append(validation_message)
|
||||
|
||||
# TODO: do we need this?
|
||||
element._height = len(contents)
|
||||
|
||||
return Group(*contents)
|
||||
|
||||
def render_validation_message(self, element: Union[Input, Menu]) -> Optional[str]:
|
||||
if element._cancelled:
|
||||
return "[cancelled]Cancelled.[/]"
|
||||
|
||||
if element.valid is False:
|
||||
return f"[error]{element.validation_message}[/]"
|
||||
|
||||
return None
|
||||
|
||||
# TODO: maybe don't reuse this for menus
|
||||
def render_input_value(
|
||||
self,
|
||||
input: Union[Menu, Input],
|
||||
is_active: bool = False,
|
||||
parent: Optional[Element] = None,
|
||||
done: bool = False,
|
||||
) -> RenderableType:
|
||||
text = input.text
|
||||
|
||||
# Check if this is a password field and mask it
|
||||
if isinstance(input, Input) and input.password and text:
|
||||
text = "*" * len(text)
|
||||
|
||||
if not text:
|
||||
placeholder = ""
|
||||
|
||||
if isinstance(input, Input):
|
||||
placeholder = input.placeholder
|
||||
|
||||
if input.default_as_placeholder and input.default:
|
||||
return f"[placeholder]{input.default}[/]"
|
||||
|
||||
if input._cancelled:
|
||||
return f"[placeholder.cancelled]{placeholder}[/]"
|
||||
elif not done:
|
||||
return f"[placeholder]{placeholder}[/]"
|
||||
|
||||
return f"[text]{text}[/]"
|
||||
|
||||
def render_input_label(
|
||||
self,
|
||||
input: Union[Input, Menu],
|
||||
is_active: bool = False,
|
||||
parent: Optional[Element] = None,
|
||||
) -> Union[str, Text, None]:
|
||||
from rich_toolkit.form import Form
|
||||
|
||||
label: Union[str, Text, None] = None
|
||||
|
||||
if input.label:
|
||||
label = input.label
|
||||
|
||||
if isinstance(parent, Form):
|
||||
if is_active:
|
||||
label = f"[active]{label}[/]"
|
||||
elif input.valid is False:
|
||||
label = f"[error]{label}[/]"
|
||||
|
||||
return label
|
||||
|
||||
def render_menu(
|
||||
self,
|
||||
element: Menu,
|
||||
is_active: bool = False,
|
||||
done: bool = False,
|
||||
parent: Optional[Element] = None,
|
||||
) -> RenderableType:
|
||||
menu = Text(justify="left")
|
||||
|
||||
selected_prefix = Text(element.current_selection_char + " ")
|
||||
not_selected_prefix = Text(element.selection_char + " ")
|
||||
|
||||
separator = Text("\t" if element.inline else "\n")
|
||||
|
||||
if done:
|
||||
result_content = Text()
|
||||
|
||||
result_content.append(
|
||||
self.render_input_label(element, is_active=is_active, parent=parent)
|
||||
)
|
||||
result_content.append(" ")
|
||||
|
||||
result_content.append(
|
||||
element.options[element.selected]["name"],
|
||||
style=self.console.get_style("result"),
|
||||
)
|
||||
|
||||
return result_content
|
||||
|
||||
# Get visible range for scrolling
|
||||
all_options = element.options
|
||||
start, end = element.visible_options_range
|
||||
visible_options = all_options[start:end]
|
||||
|
||||
# Check if scrolling is needed (to reserve consistent space for indicators)
|
||||
needs_scrolling = element._needs_scrolling()
|
||||
|
||||
# Always reserve space for "more above" indicator when scrolling is enabled
|
||||
# This prevents the menu from shifting when scrolling starts
|
||||
if needs_scrolling:
|
||||
if element.has_more_above:
|
||||
menu.append(Text(element.MORE_ABOVE_INDICATOR + "\n", style="dim"))
|
||||
else:
|
||||
# Empty line to reserve space (same length as indicator for consistency)
|
||||
menu.append(Text(" " * len(element.MORE_ABOVE_INDICATOR) + "\n"))
|
||||
|
||||
for idx, option in enumerate(visible_options):
|
||||
# Calculate actual index in full options list
|
||||
actual_idx = start + idx
|
||||
if actual_idx == element.selected:
|
||||
prefix = selected_prefix
|
||||
style = self.console.get_style("selected")
|
||||
else:
|
||||
prefix = not_selected_prefix
|
||||
style = self.console.get_style("text")
|
||||
|
||||
is_last = idx == len(visible_options) - 1
|
||||
|
||||
menu.append(
|
||||
Text.assemble(
|
||||
prefix,
|
||||
option["name"],
|
||||
separator if not is_last else "",
|
||||
style=style,
|
||||
)
|
||||
)
|
||||
|
||||
# Always reserve space for "more below" indicator when scrolling is enabled
|
||||
if needs_scrolling:
|
||||
if element.has_more_below:
|
||||
menu.append(Text("\n" + element.MORE_BELOW_INDICATOR, style="dim"))
|
||||
else:
|
||||
# Empty line to reserve space (same length as indicator for consistency)
|
||||
menu.append(Text("\n" + " " * len(element.MORE_BELOW_INDICATOR)))
|
||||
|
||||
if not element.options:
|
||||
menu = Text("No results found", style=self.console.get_style("text"))
|
||||
|
||||
filter = (
|
||||
[
|
||||
Text.assemble(
|
||||
(element.filter_prompt, self.console.get_style("text")),
|
||||
(element.text, self.console.get_style("text")),
|
||||
"\n",
|
||||
)
|
||||
]
|
||||
if element.allow_filtering
|
||||
else []
|
||||
)
|
||||
|
||||
content: list[RenderableType] = []
|
||||
|
||||
content.append(self.render_input_label(element))
|
||||
|
||||
content.extend(filter)
|
||||
content.append(menu)
|
||||
|
||||
if message := self.render_validation_message(element):
|
||||
content.append(Text(""))
|
||||
content.append(message)
|
||||
|
||||
return Group(*content)
|
||||
|
||||
def render_progress(
|
||||
self,
|
||||
element: Progress,
|
||||
is_active: bool = False,
|
||||
done: bool = False,
|
||||
parent: Optional[Element] = None,
|
||||
) -> RenderableType:
|
||||
content: str | Group | Text = element.current_message
|
||||
|
||||
if element.logs and element._inline_logs:
|
||||
lines_to_show = (
|
||||
element.logs[-element.lines_to_show :]
|
||||
if element.lines_to_show > 0
|
||||
else element.logs
|
||||
)
|
||||
|
||||
start_content = [element.title, ""]
|
||||
|
||||
if not self._should_show_progress_title:
|
||||
start_content = []
|
||||
|
||||
content = Group(
|
||||
*start_content,
|
||||
*[
|
||||
self.render_element(
|
||||
line,
|
||||
index=index,
|
||||
max_lines=element.lines_to_show,
|
||||
total_lines=len(element.logs),
|
||||
parent=element,
|
||||
)
|
||||
for index, line in enumerate(lines_to_show)
|
||||
],
|
||||
)
|
||||
|
||||
return content
|
||||
|
||||
def render_progress_log_line(
|
||||
self,
|
||||
line: str | Text,
|
||||
index: int,
|
||||
max_lines: int = -1,
|
||||
total_lines: int = -1,
|
||||
parent: Optional[Element] = None,
|
||||
) -> Text:
|
||||
line = Text.from_markup(line) if isinstance(line, str) else line
|
||||
if max_lines == -1:
|
||||
return line
|
||||
|
||||
shown_lines = min(total_lines, max_lines)
|
||||
|
||||
# this is the minimum brightness based on the max_lines
|
||||
min_brightness = 0.4
|
||||
# but we want to have a slightly higher brightness if there's less than max_lines
|
||||
# otherwise you could get the something like this:
|
||||
|
||||
# line 1 -> very dark
|
||||
# line 2 -> slightly darker
|
||||
# line 3 -> normal
|
||||
|
||||
# which is ok, but not great, so we we increase the brightness if there's less than max_lines
|
||||
# so that the last line is always the brightest
|
||||
current_min_brightness = min_brightness + abs(shown_lines - max_lines) * 0.1
|
||||
current_min_brightness = min(max(current_min_brightness, min_brightness), 1.0)
|
||||
|
||||
brightness_multiplier = ((index + 1) / shown_lines) * (
|
||||
1.0 - current_min_brightness
|
||||
) + current_min_brightness
|
||||
|
||||
return fade_text(
|
||||
line,
|
||||
text_color=Color.parse(self.text_color),
|
||||
background_color=self.background_color,
|
||||
brightness_multiplier=brightness_multiplier,
|
||||
)
|
||||
261
venv/lib/python3.11/site-packages/rich_toolkit/styles/border.py
Normal file
261
venv/lib/python3.11/site-packages/rich_toolkit/styles/border.py
Normal file
|
|
@ -0,0 +1,261 @@
|
|||
from typing import Any, Optional, Tuple, Union
|
||||
|
||||
from rich import box
|
||||
from rich.color import Color
|
||||
from rich.console import Group, RenderableType
|
||||
from rich.style import Style
|
||||
from rich.text import Text
|
||||
|
||||
from rich_toolkit._rich_components import Panel
|
||||
from rich_toolkit.container import Container
|
||||
from rich_toolkit.element import CursorOffset, Element
|
||||
from rich_toolkit.form import Form
|
||||
from rich_toolkit.input import Input
|
||||
from rich_toolkit.menu import Menu
|
||||
from rich_toolkit.progress import Progress
|
||||
|
||||
from .base import BaseStyle
|
||||
|
||||
|
||||
class BorderedStyle(BaseStyle):
|
||||
box = box.SQUARE
|
||||
|
||||
def empty_line(self) -> RenderableType:
|
||||
return ""
|
||||
|
||||
def _box(
|
||||
self,
|
||||
content: RenderableType,
|
||||
title: Union[str, Text, None],
|
||||
is_active: bool,
|
||||
border_color: Color,
|
||||
after: Tuple[str, ...] = (),
|
||||
) -> RenderableType:
|
||||
return Group(
|
||||
Panel(
|
||||
content,
|
||||
title=title,
|
||||
title_align="left",
|
||||
highlight=is_active,
|
||||
width=50,
|
||||
box=self.box,
|
||||
border_style=Style(color=border_color),
|
||||
),
|
||||
*after,
|
||||
)
|
||||
|
||||
def render_container(
|
||||
self,
|
||||
element: Container,
|
||||
is_active: bool = False,
|
||||
done: bool = False,
|
||||
parent: Optional[Element] = None,
|
||||
) -> RenderableType:
|
||||
content = super().render_container(element, is_active, done, parent)
|
||||
|
||||
if isinstance(element, Form):
|
||||
return self._box(content, element.title, is_active, Color.parse("white"))
|
||||
|
||||
return content
|
||||
|
||||
def render_input(
|
||||
self,
|
||||
element: Input,
|
||||
is_active: bool = False,
|
||||
done: bool = False,
|
||||
parent: Optional[Element] = None,
|
||||
**metadata: Any,
|
||||
) -> RenderableType:
|
||||
validation_message: Tuple[str, ...] = ()
|
||||
|
||||
if isinstance(parent, Form):
|
||||
return super().render_input(element, is_active, done, parent, **metadata)
|
||||
|
||||
if message := self.render_validation_message(element):
|
||||
validation_message = (message,)
|
||||
|
||||
title = self.render_input_label(
|
||||
element,
|
||||
is_active=is_active,
|
||||
parent=parent,
|
||||
)
|
||||
|
||||
# Determine border color based on validation state
|
||||
if element.valid is False:
|
||||
try:
|
||||
border_color = self.console.get_style("error").color or Color.parse(
|
||||
"red"
|
||||
)
|
||||
except Exception:
|
||||
# Fallback if error style is not defined
|
||||
border_color = Color.parse("red")
|
||||
else:
|
||||
border_color = Color.parse("white")
|
||||
|
||||
return self._box(
|
||||
self.render_input_value(element, is_active=is_active, parent=parent),
|
||||
title,
|
||||
is_active,
|
||||
border_color,
|
||||
after=validation_message,
|
||||
)
|
||||
|
||||
def render_menu(
|
||||
self,
|
||||
element: Menu,
|
||||
is_active: bool = False,
|
||||
done: bool = False,
|
||||
parent: Optional[Element] = None,
|
||||
**metadata: Any,
|
||||
) -> RenderableType:
|
||||
validation_message: Tuple[str, ...] = ()
|
||||
|
||||
menu = Text(justify="left")
|
||||
|
||||
selected_prefix = Text(element.current_selection_char + " ")
|
||||
not_selected_prefix = Text(element.selection_char + " ")
|
||||
|
||||
separator = Text("\t" if element.inline else "\n")
|
||||
|
||||
content: list[RenderableType] = []
|
||||
|
||||
if done:
|
||||
content.append(
|
||||
Text(
|
||||
element.options[element.selected]["name"],
|
||||
style=self.console.get_style("result"),
|
||||
)
|
||||
)
|
||||
|
||||
else:
|
||||
# Get visible range for scrolling
|
||||
all_options = element.options
|
||||
start, end = element.visible_options_range
|
||||
visible_options = all_options[start:end]
|
||||
|
||||
# Check if scrolling is needed (to reserve consistent space for indicators)
|
||||
needs_scrolling = element._needs_scrolling()
|
||||
|
||||
# Always reserve space for "more above" indicator when scrolling is enabled
|
||||
if needs_scrolling:
|
||||
if element.has_more_above:
|
||||
menu.append(Text(element.MORE_ABOVE_INDICATOR + "\n", style="dim"))
|
||||
else:
|
||||
menu.append(Text(" " * len(element.MORE_ABOVE_INDICATOR) + "\n"))
|
||||
|
||||
for idx, option in enumerate(visible_options):
|
||||
actual_idx = start + idx
|
||||
if actual_idx == element.selected:
|
||||
prefix = selected_prefix
|
||||
style = self.console.get_style("selected")
|
||||
else:
|
||||
prefix = not_selected_prefix
|
||||
style = self.console.get_style("text")
|
||||
|
||||
is_last = idx == len(visible_options) - 1
|
||||
|
||||
menu.append(
|
||||
Text.assemble(
|
||||
prefix,
|
||||
option["name"],
|
||||
separator if not is_last else "",
|
||||
style=style,
|
||||
)
|
||||
)
|
||||
|
||||
# Always reserve space for "more below" indicator when scrolling is enabled
|
||||
if needs_scrolling:
|
||||
if element.has_more_below:
|
||||
menu.append(Text("\n" + element.MORE_BELOW_INDICATOR, style="dim"))
|
||||
else:
|
||||
menu.append(Text("\n" + " " * len(element.MORE_BELOW_INDICATOR)))
|
||||
|
||||
if not element.options:
|
||||
menu = Text("No results found", style=self.console.get_style("text"))
|
||||
|
||||
filter = (
|
||||
[
|
||||
Text.assemble(
|
||||
(element.filter_prompt, self.console.get_style("text")),
|
||||
(element.text, self.console.get_style("text")),
|
||||
"\n",
|
||||
)
|
||||
]
|
||||
if element.allow_filtering
|
||||
else []
|
||||
)
|
||||
|
||||
content.extend(filter)
|
||||
content.append(menu)
|
||||
|
||||
if message := self.render_validation_message(element):
|
||||
validation_message = (message,)
|
||||
|
||||
result = Group(*content)
|
||||
|
||||
return self._box(
|
||||
result,
|
||||
self.render_input_label(element),
|
||||
is_active,
|
||||
Color.parse("white"),
|
||||
after=validation_message,
|
||||
)
|
||||
|
||||
def render_progress(
|
||||
self,
|
||||
element: Progress,
|
||||
is_active: bool = False,
|
||||
done: bool = False,
|
||||
parent: Optional[Element] = None,
|
||||
) -> RenderableType:
|
||||
content: str | Group | Text = element.current_message
|
||||
title: Union[str, Text, None] = None
|
||||
|
||||
title = element.title
|
||||
|
||||
if element.logs and element._inline_logs:
|
||||
lines_to_show = (
|
||||
element.logs[-element.lines_to_show :]
|
||||
if element.lines_to_show > 0
|
||||
else element.logs
|
||||
)
|
||||
|
||||
content = Group(
|
||||
*[
|
||||
self.render_element(
|
||||
line,
|
||||
index=index,
|
||||
max_lines=element.lines_to_show,
|
||||
total_lines=len(element.logs),
|
||||
)
|
||||
for index, line in enumerate(lines_to_show)
|
||||
]
|
||||
)
|
||||
|
||||
border_color = Color.parse("white")
|
||||
|
||||
if not done:
|
||||
colors = self._get_animation_colors(
|
||||
steps=10, animation_status="started", breathe=True
|
||||
)
|
||||
|
||||
border_color = colors[self.animation_counter % 10]
|
||||
|
||||
return self._box(content, title, is_active, border_color=border_color)
|
||||
|
||||
def get_cursor_offset_for_element(
|
||||
self, element: Element, parent: Optional[Element] = None
|
||||
) -> CursorOffset:
|
||||
top_offset = element.cursor_offset.top
|
||||
left_offset = element.cursor_offset.left + 2
|
||||
|
||||
if isinstance(element, Input) and element.inline:
|
||||
# we don't support inline inputs yet in border style
|
||||
top_offset += 1
|
||||
inline_left_offset = (len(element.label) - 1) if element.label else 0
|
||||
left_offset = element.cursor_offset.left - inline_left_offset
|
||||
|
||||
if isinstance(parent, Form):
|
||||
top_offset += 1
|
||||
|
||||
return CursorOffset(top=top_offset, left=left_offset)
|
||||
170
venv/lib/python3.11/site-packages/rich_toolkit/styles/fancy.py
Normal file
170
venv/lib/python3.11/site-packages/rich_toolkit/styles/fancy.py
Normal file
|
|
@ -0,0 +1,170 @@
|
|||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from rich._loop import loop_first_last
|
||||
from rich.console import Console, ConsoleOptions, RenderableType, RenderResult
|
||||
from rich.segment import Segment
|
||||
from rich.style import Style
|
||||
from rich.text import Text
|
||||
from typing_extensions import Literal
|
||||
|
||||
from rich_toolkit.container import Container
|
||||
from rich_toolkit.element import CursorOffset, Element
|
||||
from rich_toolkit.form import Form
|
||||
from rich_toolkit.progress import Progress
|
||||
from rich_toolkit.styles.base import BaseStyle
|
||||
|
||||
|
||||
class FancyPanel:
|
||||
def __init__(
|
||||
self,
|
||||
renderable: RenderableType,
|
||||
style: BaseStyle,
|
||||
title: Optional[str] = None,
|
||||
metadata: Optional[Dict[str, Any]] = None,
|
||||
is_animated: Optional[bool] = None,
|
||||
animation_counter: Optional[int] = None,
|
||||
done: bool = False,
|
||||
) -> None:
|
||||
self.renderable = renderable
|
||||
self._title = title
|
||||
self.metadata = metadata or {}
|
||||
self.width = None
|
||||
self.expand = True
|
||||
self.is_animated = is_animated
|
||||
self.counter = animation_counter or 0
|
||||
self.style = style
|
||||
self.done = done
|
||||
|
||||
def _get_decoration(self, suffix: str = "") -> Segment:
|
||||
char = "┌" if self.metadata.get("title") else "◆"
|
||||
|
||||
animated = not self.done and self.is_animated
|
||||
|
||||
animation_status: Literal["started", "stopped", "error"] = (
|
||||
"started" if animated else "stopped"
|
||||
)
|
||||
|
||||
color = self.style._get_animation_colors(
|
||||
steps=14, breathe=True, animation_status=animation_status
|
||||
)[self.counter % 14]
|
||||
|
||||
return Segment(char + suffix, style=Style.from_color(color))
|
||||
|
||||
def _strip_trailing_newlines(
|
||||
self, lines: List[List[Segment]]
|
||||
) -> List[List[Segment]]:
|
||||
# remove all empty lines from the end of the list
|
||||
|
||||
while lines and all(segment.text.strip() == "" for segment in lines[-1]):
|
||||
lines.pop()
|
||||
|
||||
return lines
|
||||
|
||||
def __rich_console__(
|
||||
self, console: "Console", options: "ConsoleOptions"
|
||||
) -> "RenderResult":
|
||||
renderable = self.renderable
|
||||
|
||||
lines = console.render_lines(renderable)
|
||||
lines = self._strip_trailing_newlines(lines)
|
||||
|
||||
line_start = self._get_decoration()
|
||||
|
||||
new_line = Segment.line()
|
||||
|
||||
if self._title is not None:
|
||||
yield line_start
|
||||
yield Segment(" ")
|
||||
yield self._title
|
||||
|
||||
for first, last, line in loop_first_last(lines):
|
||||
if first and not self._title:
|
||||
decoration = (
|
||||
Segment("┌ ")
|
||||
if self.metadata.get("title", False)
|
||||
else self._get_decoration(suffix=" ")
|
||||
)
|
||||
elif last and self.metadata.get("started", True):
|
||||
decoration = Segment("└ ")
|
||||
else:
|
||||
decoration = Segment("│ ")
|
||||
|
||||
yield decoration
|
||||
yield from line
|
||||
|
||||
if not last:
|
||||
yield new_line
|
||||
|
||||
|
||||
class FancyStyle(BaseStyle):
|
||||
_should_show_progress_title = False
|
||||
|
||||
def __init__(self, *args, **kwargs) -> None:
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.cursor_offset = 2
|
||||
self.decoration_size = 2
|
||||
|
||||
def _should_decorate(self, element: Any, parent: Optional[Element] = None) -> bool:
|
||||
return not isinstance(parent, (Progress, Container))
|
||||
|
||||
def render_element(
|
||||
self,
|
||||
element: Any,
|
||||
is_active: bool = False,
|
||||
done: bool = False,
|
||||
parent: Optional[Element] = None,
|
||||
**metadata: Any,
|
||||
) -> RenderableType:
|
||||
title: Optional[str] = None
|
||||
|
||||
is_animated = False
|
||||
|
||||
if isinstance(element, Progress):
|
||||
title = element.title
|
||||
is_animated = True
|
||||
|
||||
rendered = super().render_element(
|
||||
element=element, is_active=is_active, done=done, parent=parent, **metadata
|
||||
)
|
||||
|
||||
if self._should_decorate(element, parent):
|
||||
rendered = FancyPanel(
|
||||
rendered,
|
||||
title=title,
|
||||
metadata=metadata,
|
||||
is_animated=is_animated,
|
||||
done=done,
|
||||
animation_counter=self.animation_counter,
|
||||
style=self,
|
||||
)
|
||||
|
||||
return rendered
|
||||
|
||||
def empty_line(self) -> Text:
|
||||
"""Return an empty line with decoration.
|
||||
|
||||
Returns:
|
||||
A text object representing an empty line
|
||||
"""
|
||||
return Text("│", style="fancy.normal")
|
||||
|
||||
def get_cursor_offset_for_element(
|
||||
self, element: Element, parent: Optional[Element] = None
|
||||
) -> CursorOffset:
|
||||
"""Get the cursor offset for an element.
|
||||
|
||||
Args:
|
||||
element: The element to get the cursor offset for
|
||||
|
||||
Returns:
|
||||
The cursor offset
|
||||
"""
|
||||
|
||||
if isinstance(element, Form):
|
||||
return element.cursor_offset
|
||||
else:
|
||||
return CursorOffset(
|
||||
top=element.cursor_offset.top,
|
||||
left=self.decoration_size + element.cursor_offset.left,
|
||||
)
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
from .base import BaseStyle
|
||||
|
||||
|
||||
class MinimalStyle(BaseStyle):
|
||||
pass
|
||||
145
venv/lib/python3.11/site-packages/rich_toolkit/styles/tagged.py
Normal file
145
venv/lib/python3.11/site-packages/rich_toolkit/styles/tagged.py
Normal file
|
|
@ -0,0 +1,145 @@
|
|||
import re
|
||||
from typing import Any, Dict, List, Optional, Tuple
|
||||
|
||||
from rich.console import Group, RenderableType
|
||||
from rich.segment import Segment
|
||||
from rich.style import Style
|
||||
from rich.table import Column, Table
|
||||
from typing_extensions import Literal
|
||||
|
||||
from rich_toolkit.container import Container
|
||||
from rich_toolkit.element import CursorOffset, Element
|
||||
from rich_toolkit.progress import Progress, ProgressLine
|
||||
|
||||
from .base import BaseStyle
|
||||
|
||||
|
||||
def has_emoji(tag: str) -> bool:
|
||||
return bool(re.search(r"[\U0001F300-\U0001F9FF]", tag))
|
||||
|
||||
|
||||
class TaggedStyle(BaseStyle):
|
||||
block = "█"
|
||||
block_length = 5
|
||||
|
||||
def __init__(self, tag_width: int = 12, theme: Optional[Dict[str, str]] = None):
|
||||
self.tag_width = tag_width
|
||||
|
||||
theme = theme or {
|
||||
"tag.title": "bold",
|
||||
"tag": "bold",
|
||||
}
|
||||
|
||||
super().__init__(theme=theme)
|
||||
|
||||
def _get_tag_segments(
|
||||
self,
|
||||
metadata: Dict[str, Any],
|
||||
is_animated: bool = False,
|
||||
done: bool = False,
|
||||
) -> Tuple[List[Segment], int]:
|
||||
if tag := metadata.get("tag", ""):
|
||||
tag = f" {tag} "
|
||||
|
||||
style_name = "tag.title" if metadata.get("title", False) else "tag"
|
||||
|
||||
style = self.console.get_style(style_name)
|
||||
|
||||
if is_animated:
|
||||
animation_status: Literal["started", "stopped", "error"] = (
|
||||
"started" if not done else "stopped"
|
||||
)
|
||||
|
||||
tag = " " * self.block_length
|
||||
colors = self._get_animation_colors(
|
||||
steps=self.block_length, animation_status=animation_status
|
||||
)
|
||||
|
||||
if done:
|
||||
colors = [colors[-1]]
|
||||
|
||||
tag_segments = [
|
||||
Segment(
|
||||
self.block,
|
||||
style=Style(
|
||||
color=colors[(self.animation_counter + i) % len(colors)]
|
||||
),
|
||||
)
|
||||
for i in range(self.block_length)
|
||||
]
|
||||
else:
|
||||
tag_segments = [Segment(tag, style=style)]
|
||||
|
||||
left_padding = self.tag_width - len(tag)
|
||||
left_padding = max(0, left_padding)
|
||||
|
||||
return tag_segments, left_padding
|
||||
|
||||
def _get_tag(
|
||||
self,
|
||||
metadata: Dict[str, Any],
|
||||
is_animated: bool = False,
|
||||
done: bool = False,
|
||||
) -> Group:
|
||||
tag_segments, left_padding = self._get_tag_segments(metadata, is_animated, done)
|
||||
|
||||
left = [Segment(" " * left_padding), *tag_segments]
|
||||
|
||||
return Group(*left)
|
||||
|
||||
def _tag_element(
|
||||
self,
|
||||
child: RenderableType,
|
||||
is_animated: bool = False,
|
||||
done: bool = False,
|
||||
**metadata: Dict[str, Any],
|
||||
) -> RenderableType:
|
||||
table = Table.grid(
|
||||
# TODO: why do we add 2? :D we probably did this in the previous version
|
||||
Column(width=self.tag_width + 2, no_wrap=True),
|
||||
Column(no_wrap=False, overflow="fold"),
|
||||
padding=(0, 0, 0, 0),
|
||||
collapse_padding=True,
|
||||
pad_edge=False,
|
||||
)
|
||||
|
||||
table.add_row(self._get_tag(metadata, is_animated, done), Group(child))
|
||||
|
||||
return table
|
||||
|
||||
def render_element(
|
||||
self,
|
||||
element: Any,
|
||||
is_active: bool = False,
|
||||
done: bool = False,
|
||||
parent: Optional[Element] = None,
|
||||
**kwargs: Any,
|
||||
) -> RenderableType:
|
||||
is_animated = isinstance(element, Progress)
|
||||
should_tag = not isinstance(element, (ProgressLine, Container))
|
||||
|
||||
rendered = super().render_element(
|
||||
element=element, is_active=is_active, done=done, parent=parent, **kwargs
|
||||
)
|
||||
|
||||
metadata = kwargs
|
||||
if isinstance(element, Element) and element.metadata:
|
||||
metadata = {**element.metadata, **metadata}
|
||||
|
||||
if should_tag:
|
||||
rendered = self._tag_element(
|
||||
rendered,
|
||||
is_animated=is_animated,
|
||||
done=done,
|
||||
**metadata,
|
||||
)
|
||||
|
||||
return rendered
|
||||
|
||||
def get_cursor_offset_for_element(
|
||||
self, element: Element, parent: Optional[Element] = None
|
||||
) -> CursorOffset:
|
||||
return CursorOffset(
|
||||
top=element.cursor_offset.top,
|
||||
left=self.tag_width + element.cursor_offset.left + 2,
|
||||
)
|
||||
Loading…
Add table
Add a link
Reference in a new issue