second commit
This commit is contained in:
3
env/lib/python3.11/site-packages/rich_toolkit/__init__.py
vendored
Normal file
3
env/lib/python3.11/site-packages/rich_toolkit/__init__.py
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
from .toolkit import RichToolkit, RichToolkitTheme
|
||||
|
||||
__all__ = ["RichToolkit", "RichToolkitTheme"]
|
BIN
env/lib/python3.11/site-packages/rich_toolkit/__pycache__/__init__.cpython-311.pyc
vendored
Normal file
BIN
env/lib/python3.11/site-packages/rich_toolkit/__pycache__/__init__.cpython-311.pyc
vendored
Normal file
Binary file not shown.
BIN
env/lib/python3.11/site-packages/rich_toolkit/__pycache__/input.cpython-311.pyc
vendored
Normal file
BIN
env/lib/python3.11/site-packages/rich_toolkit/__pycache__/input.cpython-311.pyc
vendored
Normal file
Binary file not shown.
BIN
env/lib/python3.11/site-packages/rich_toolkit/__pycache__/menu.cpython-311.pyc
vendored
Normal file
BIN
env/lib/python3.11/site-packages/rich_toolkit/__pycache__/menu.cpython-311.pyc
vendored
Normal file
Binary file not shown.
BIN
env/lib/python3.11/site-packages/rich_toolkit/__pycache__/progress.cpython-311.pyc
vendored
Normal file
BIN
env/lib/python3.11/site-packages/rich_toolkit/__pycache__/progress.cpython-311.pyc
vendored
Normal file
Binary file not shown.
BIN
env/lib/python3.11/site-packages/rich_toolkit/__pycache__/toolkit.cpython-311.pyc
vendored
Normal file
BIN
env/lib/python3.11/site-packages/rich_toolkit/__pycache__/toolkit.cpython-311.pyc
vendored
Normal file
Binary file not shown.
192
env/lib/python3.11/site-packages/rich_toolkit/input.py
vendored
Normal file
192
env/lib/python3.11/site-packages/rich_toolkit/input.py
vendored
Normal file
@ -0,0 +1,192 @@
|
||||
import string
|
||||
from typing import Any, Optional, Tuple
|
||||
from abc import ABC, abstractmethod
|
||||
|
||||
import click
|
||||
from rich.console import Console, Group, RenderableType
|
||||
from rich.control import Control
|
||||
from rich.live_render import LiveRender
|
||||
|
||||
from rich_toolkit.styles.base import BaseStyle
|
||||
|
||||
|
||||
class TextInputHandler:
|
||||
DOWN_KEY = "\x1b[B"
|
||||
UP_KEY = "\x1b[A"
|
||||
LEFT_KEY = "\x1b[D"
|
||||
RIGHT_KEY = "\x1b[C"
|
||||
BACKSPACE_KEY = "\x7f"
|
||||
DELETE_KEY = "\x1b[3~"
|
||||
|
||||
def __init__(self, cursor_offset: int = 0):
|
||||
self.text = ""
|
||||
self.cursor_position = 0
|
||||
self._cursor_offset = cursor_offset
|
||||
|
||||
def _move_cursor_left(self) -> None:
|
||||
self.cursor_position = max(0, self.cursor_position - 1)
|
||||
|
||||
def _move_cursor_right(self) -> None:
|
||||
self.cursor_position = min(len(self.text), self.cursor_position + 1)
|
||||
|
||||
def _insert_char(self, char: str) -> None:
|
||||
self.text = (
|
||||
self.text[: self.cursor_position] + char + self.text[self.cursor_position :]
|
||||
)
|
||||
self._move_cursor_right()
|
||||
|
||||
def _delete_char(self) -> None:
|
||||
if self.cursor_position == 0:
|
||||
return
|
||||
|
||||
self.text = (
|
||||
self.text[: self.cursor_position - 1] + self.text[self.cursor_position :]
|
||||
)
|
||||
self._move_cursor_left()
|
||||
|
||||
def _delete_forward(self) -> None:
|
||||
if self.cursor_position == len(self.text):
|
||||
return
|
||||
|
||||
self.text = (
|
||||
self.text[: self.cursor_position] + self.text[self.cursor_position + 1 :]
|
||||
)
|
||||
|
||||
def update_text(self, text: str) -> None:
|
||||
if text == self.BACKSPACE_KEY:
|
||||
self._delete_char()
|
||||
elif text == self.DELETE_KEY:
|
||||
self._delete_forward()
|
||||
elif text == self.LEFT_KEY:
|
||||
self._move_cursor_left()
|
||||
elif text == self.RIGHT_KEY:
|
||||
self.cursor_position = min(len(self.text), self.cursor_position + 1)
|
||||
elif text in (self.UP_KEY, self.DOWN_KEY):
|
||||
pass
|
||||
else:
|
||||
for char in text:
|
||||
if char in string.printable:
|
||||
self._insert_char(char)
|
||||
|
||||
def fix_cursor(self) -> Tuple[Control, ...]:
|
||||
return (Control.move_to_column(self._cursor_offset + self.cursor_position),)
|
||||
|
||||
|
||||
class LiveInput(ABC, TextInputHandler):
|
||||
def __init__(
|
||||
self,
|
||||
console: Console,
|
||||
style: Optional[BaseStyle] = None,
|
||||
cursor_offset: int = 0,
|
||||
**metadata: Any,
|
||||
):
|
||||
self.console = console
|
||||
self._live_render = LiveRender("")
|
||||
|
||||
if style is None:
|
||||
self._live_render = LiveRender("")
|
||||
else:
|
||||
self._live_render = style.decorate_class(LiveRender, **metadata)("")
|
||||
|
||||
self._padding_bottom = 1
|
||||
|
||||
super().__init__(cursor_offset=cursor_offset)
|
||||
|
||||
@abstractmethod
|
||||
def render_result(self) -> RenderableType:
|
||||
raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
def render_input(self) -> RenderableType:
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
def should_show_cursor(self) -> bool:
|
||||
return True
|
||||
|
||||
def position_cursor(self) -> Tuple[Control, ...]:
|
||||
return (self._live_render.position_cursor(),)
|
||||
|
||||
def _refresh(self, show_result: bool = False) -> None:
|
||||
renderable = self.render_result() if show_result else self.render_input()
|
||||
|
||||
self._live_render.set_renderable(renderable)
|
||||
|
||||
self._render(show_result)
|
||||
|
||||
def _render(self, show_result: bool = False) -> None:
|
||||
after = self.fix_cursor() if not show_result else ()
|
||||
|
||||
self.console.print(
|
||||
Control.show_cursor(self.should_show_cursor),
|
||||
*self.position_cursor(),
|
||||
self._live_render,
|
||||
*after,
|
||||
)
|
||||
|
||||
|
||||
class Input(LiveInput):
|
||||
def __init__(
|
||||
self,
|
||||
console: Console,
|
||||
title: str,
|
||||
style: Optional[BaseStyle] = None,
|
||||
default: str = "",
|
||||
cursor_offset: int = 0,
|
||||
password: bool = False,
|
||||
**metadata: Any,
|
||||
):
|
||||
self.title = title
|
||||
self.default = default
|
||||
self.password = password
|
||||
|
||||
self.console = console
|
||||
self.style = style
|
||||
|
||||
super().__init__(
|
||||
console=console, style=style, cursor_offset=cursor_offset, **metadata
|
||||
)
|
||||
|
||||
def render_result(self) -> RenderableType:
|
||||
if self.password:
|
||||
return self.title
|
||||
|
||||
return self.title + " [result]" + (self.text or self.default)
|
||||
|
||||
def render_input(self) -> Group:
|
||||
text = self.text
|
||||
|
||||
if self.password:
|
||||
text = "*" * len(self.text)
|
||||
|
||||
# if there's no default value, add a space to keep the cursor visible
|
||||
# and, most importantly, in the right place
|
||||
default = self.default or " "
|
||||
|
||||
text = f"[text]{text}[/]" if self.text else f"[placeholder]{default }[/]"
|
||||
|
||||
return Group(self.title, text)
|
||||
|
||||
def ask(self) -> str:
|
||||
self._refresh()
|
||||
|
||||
while True:
|
||||
try:
|
||||
key = click.getchar()
|
||||
|
||||
if key == "\r":
|
||||
break
|
||||
|
||||
self.update_text(key)
|
||||
|
||||
except KeyboardInterrupt:
|
||||
exit()
|
||||
|
||||
self._refresh()
|
||||
|
||||
self._refresh(show_result=True)
|
||||
|
||||
for _ in range(self._padding_bottom):
|
||||
self.console.print()
|
||||
|
||||
return self.text or self.default
|
302
env/lib/python3.11/site-packages/rich_toolkit/menu.py
vendored
Normal file
302
env/lib/python3.11/site-packages/rich_toolkit/menu.py
vendored
Normal file
@ -0,0 +1,302 @@
|
||||
from typing import Generic, List, Optional, Tuple, TypeVar, cast
|
||||
|
||||
import click
|
||||
from rich import get_console
|
||||
from rich.console import Console, Group, RenderableType
|
||||
from rich.control import Control
|
||||
from rich.segment import ControlType
|
||||
from rich.text import Text
|
||||
from rich.live_render import LiveRender
|
||||
from typing_extensions import Any, Literal, TypedDict
|
||||
|
||||
from .styles.base import BaseStyle
|
||||
from .input import TextInputHandler, LiveInput
|
||||
|
||||
ReturnValue = TypeVar("ReturnValue")
|
||||
|
||||
|
||||
class Option(TypedDict, Generic[ReturnValue]):
|
||||
name: str
|
||||
value: ReturnValue
|
||||
|
||||
|
||||
class Menu(Generic[ReturnValue], LiveInput):
|
||||
DOWN_KEYS = [TextInputHandler.DOWN_KEY, "j"]
|
||||
UP_KEYS = [TextInputHandler.UP_KEY, "k"]
|
||||
LEFT_KEYS = [TextInputHandler.LEFT_KEY, "h"]
|
||||
RIGHT_KEYS = [TextInputHandler.RIGHT_KEY, "l"]
|
||||
|
||||
current_selection_char = "●"
|
||||
selection_char = "○"
|
||||
filter_prompt = "Filter: "
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
title: str,
|
||||
options: List[Option[ReturnValue]],
|
||||
inline: bool = False,
|
||||
allow_filtering: bool = False,
|
||||
*,
|
||||
style: Optional[BaseStyle] = None,
|
||||
console: Optional[Console] = None,
|
||||
cursor_offset: int = 0,
|
||||
**metadata: Any,
|
||||
):
|
||||
self.console = console or get_console()
|
||||
|
||||
self.title = Text.from_markup(title)
|
||||
self.inline = inline
|
||||
self.allow_filtering = allow_filtering
|
||||
|
||||
self.selected = 0
|
||||
|
||||
self.metadata = metadata
|
||||
self.style = style
|
||||
|
||||
self._options = options
|
||||
|
||||
self._padding_bottom = 1
|
||||
|
||||
cursor_offset = cursor_offset + len(self.filter_prompt)
|
||||
|
||||
super().__init__(
|
||||
console=self.console,
|
||||
style=self.style,
|
||||
cursor_offset=cursor_offset,
|
||||
**metadata,
|
||||
)
|
||||
|
||||
def get_key(self) -> Optional[str]:
|
||||
char = click.getchar()
|
||||
|
||||
if char == "\r":
|
||||
return "enter"
|
||||
|
||||
if self.allow_filtering:
|
||||
left_keys, right_keys = [[self.LEFT_KEY], [self.RIGHT_KEY]]
|
||||
down_keys, up_keys = [[self.DOWN_KEY], [self.UP_KEY]]
|
||||
else:
|
||||
left_keys, right_keys = self.LEFT_KEYS, self.RIGHT_KEYS
|
||||
down_keys, up_keys = self.DOWN_KEYS, self.UP_KEYS
|
||||
|
||||
next_keys, prev_keys = (
|
||||
(right_keys, left_keys) if self.inline else (down_keys, up_keys)
|
||||
)
|
||||
|
||||
if char in next_keys:
|
||||
return "next"
|
||||
if char in prev_keys:
|
||||
return "prev"
|
||||
|
||||
if self.allow_filtering:
|
||||
return char
|
||||
|
||||
return None
|
||||
|
||||
@property
|
||||
def options(self) -> List[Option[ReturnValue]]:
|
||||
if self.allow_filtering:
|
||||
return [
|
||||
option
|
||||
for option in self._options
|
||||
if self.text.lower() in option["name"].lower()
|
||||
]
|
||||
|
||||
return self._options
|
||||
|
||||
def _update_selection(self, key: Literal["next", "prev"]) -> None:
|
||||
if key == "next":
|
||||
self.selected += 1
|
||||
elif key == "prev":
|
||||
self.selected -= 1
|
||||
|
||||
if self.selected < 0:
|
||||
self.selected = len(self.options) - 1
|
||||
|
||||
if self.selected >= len(self.options):
|
||||
self.selected = 0
|
||||
|
||||
def render_input(self) -> RenderableType:
|
||||
menu = Text(justify="left")
|
||||
|
||||
selected_prefix = Text(self.current_selection_char + " ")
|
||||
not_selected_prefix = Text(self.selection_char + " ")
|
||||
|
||||
separator = Text("\t" if self.inline else "\n")
|
||||
|
||||
for id_, option in enumerate(self.options):
|
||||
if id_ == self.selected:
|
||||
prefix = selected_prefix
|
||||
style = self.console.get_style("selected")
|
||||
else:
|
||||
prefix = not_selected_prefix
|
||||
style = self.console.get_style("text")
|
||||
|
||||
menu.append(Text.assemble(prefix, option["name"], separator, style=style))
|
||||
|
||||
# TODO: inline is not wrapped (maybe that's good?)
|
||||
|
||||
if not self.options:
|
||||
# menu.append("No results found", style=self.console.get_style("text"))
|
||||
menu = Text("No results found\n\n", style=self.console.get_style("text"))
|
||||
|
||||
h = 0
|
||||
if self._live_render._shape is not None:
|
||||
_, h = self._live_render._shape
|
||||
|
||||
filter = (
|
||||
[
|
||||
Text.assemble(
|
||||
(self.filter_prompt, self.console.get_style("text")),
|
||||
(self.text, self.console.get_style("text")),
|
||||
"\n",
|
||||
)
|
||||
]
|
||||
if self.allow_filtering
|
||||
else []
|
||||
)
|
||||
|
||||
return Group(self.title, *filter, *menu)
|
||||
|
||||
def render_result(self) -> RenderableType:
|
||||
result_text = Text()
|
||||
|
||||
result_text.append(self.title)
|
||||
result_text.append(" ")
|
||||
result_text.append(
|
||||
self.options[self.selected]["name"],
|
||||
style=self.console.get_style("result"),
|
||||
)
|
||||
|
||||
return result_text
|
||||
|
||||
def update_text(self, text: str) -> None:
|
||||
current_selection: Optional[str] = None
|
||||
|
||||
if self.options:
|
||||
current_selection = self.options[self.selected]["name"]
|
||||
|
||||
super().update_text(text)
|
||||
|
||||
if current_selection:
|
||||
matching_index = next(
|
||||
(
|
||||
index
|
||||
for index, option in enumerate(self.options)
|
||||
if option["name"] == current_selection
|
||||
),
|
||||
0,
|
||||
)
|
||||
|
||||
self.selected = matching_index
|
||||
|
||||
def _handle_enter(self) -> bool:
|
||||
if self.allow_filtering and self.text and len(self.options) == 0:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def reposition_cursor(self) -> Control:
|
||||
if self.allow_filtering:
|
||||
if self._live_render._shape is None:
|
||||
return Control()
|
||||
|
||||
_, height = self._live_render._shape
|
||||
|
||||
move_down = height - 2
|
||||
|
||||
return Control(
|
||||
(ControlType.CURSOR_DOWN, move_down),
|
||||
ControlType.CARRIAGE_RETURN,
|
||||
(ControlType.ERASE_IN_LINE, 2),
|
||||
*(
|
||||
(
|
||||
(ControlType.CURSOR_UP, 1),
|
||||
(ControlType.ERASE_IN_LINE, 2),
|
||||
)
|
||||
* (height - 1)
|
||||
),
|
||||
)
|
||||
|
||||
return self._live_render.position_cursor()
|
||||
|
||||
def position_cursor(self) -> Tuple[Control, ...]:
|
||||
"""Positions the cursor at the end of the menu.
|
||||
|
||||
It moves the cursor up based on the size of the menu when filtering
|
||||
is enabled. It does by taking into account the size of the menu
|
||||
and the fact that we moved the cursor up in the previous render.
|
||||
|
||||
We use the shape of the menu to calculate the number of times we
|
||||
need to move the cursor up, we do this because the options are
|
||||
dynamic and we need the current size* of the menu to calculate
|
||||
the correct position of the cursor.
|
||||
|
||||
* Current size means the previous size of the menu, but I say
|
||||
current because we haven't rendered the updated menu yet :)
|
||||
"""
|
||||
original = super().position_cursor()
|
||||
|
||||
if self._live_render._shape is None or not self.allow_filtering:
|
||||
return original
|
||||
|
||||
_, height = self._live_render._shape
|
||||
|
||||
move_down = height - 2
|
||||
|
||||
return (
|
||||
Control(
|
||||
(ControlType.CURSOR_DOWN, move_down),
|
||||
),
|
||||
*original,
|
||||
)
|
||||
|
||||
def fix_cursor(self) -> Tuple[Control, ...]:
|
||||
"""Fixes the position of cursor after rendering the menu.
|
||||
|
||||
It moves the cursor up based on the size of the menu, but
|
||||
only if allow_filtering is enabled. (We don't show the cursor
|
||||
when filtering is disabled.)
|
||||
"""
|
||||
move_cursor = ()
|
||||
|
||||
if self.allow_filtering:
|
||||
height = len(self.options) + 1 if self.options else 2
|
||||
|
||||
move_cursor = (Control((ControlType.CURSOR_UP, height)),)
|
||||
|
||||
return (*super().fix_cursor(), *move_cursor)
|
||||
|
||||
@property
|
||||
def should_show_cursor(self) -> bool:
|
||||
return self.allow_filtering
|
||||
|
||||
def ask(self) -> ReturnValue:
|
||||
self._refresh()
|
||||
|
||||
while True:
|
||||
try:
|
||||
key = self.get_key()
|
||||
|
||||
if key == "enter":
|
||||
if self._handle_enter():
|
||||
break
|
||||
|
||||
elif key is not None:
|
||||
if key in ["next", "prev"]:
|
||||
key = cast(Literal["next", "prev"], key)
|
||||
self._update_selection(key)
|
||||
else:
|
||||
self.update_text(key)
|
||||
|
||||
except KeyboardInterrupt:
|
||||
exit()
|
||||
|
||||
self._refresh()
|
||||
|
||||
self._refresh(show_result=True)
|
||||
|
||||
for _ in range(self._padding_bottom):
|
||||
self.console.print()
|
||||
|
||||
return self.options[self.selected]["value"]
|
56
env/lib/python3.11/site-packages/rich_toolkit/progress.py
vendored
Normal file
56
env/lib/python3.11/site-packages/rich_toolkit/progress.py
vendored
Normal file
@ -0,0 +1,56 @@
|
||||
from typing import Optional
|
||||
|
||||
from rich.console import Console
|
||||
from rich.live import Live
|
||||
from typing_extensions import Any, Literal
|
||||
|
||||
from .styles.base import BaseStyle
|
||||
|
||||
|
||||
class Progress(Live):
|
||||
def __init__(
|
||||
self,
|
||||
title: str,
|
||||
style: Optional[BaseStyle] = None,
|
||||
console: Optional[Console] = None,
|
||||
transient: bool = False,
|
||||
transient_on_error: bool = False,
|
||||
) -> None:
|
||||
self.current_message = title
|
||||
self.style = style
|
||||
self.is_error = False
|
||||
self._transient_on_error = transient_on_error
|
||||
|
||||
super().__init__(console=console, refresh_per_second=8, transient=transient)
|
||||
|
||||
# TODO: remove this once rich uses "Self"
|
||||
def __enter__(self) -> "Progress":
|
||||
self.start(refresh=self._renderable is not None)
|
||||
|
||||
return self
|
||||
|
||||
def get_renderable(self) -> Any:
|
||||
current_message = self.current_message
|
||||
|
||||
if not self.style:
|
||||
return current_message
|
||||
|
||||
animation_status: Literal["started", "stopped", "error"] = (
|
||||
"started" if self._started else "stopped"
|
||||
)
|
||||
|
||||
if self.is_error:
|
||||
animation_status = "error"
|
||||
|
||||
return self.style.with_decoration(
|
||||
current_message,
|
||||
animation_status=animation_status,
|
||||
)
|
||||
|
||||
def log(self, text: str) -> None:
|
||||
self.current_message = text
|
||||
|
||||
def set_error(self, text: str) -> None:
|
||||
self.current_message = text
|
||||
self.is_error = True
|
||||
self.transient = self._transient_on_error
|
0
env/lib/python3.11/site-packages/rich_toolkit/py.typed
vendored
Normal file
0
env/lib/python3.11/site-packages/rich_toolkit/py.typed
vendored
Normal file
9
env/lib/python3.11/site-packages/rich_toolkit/styles/__init__.py
vendored
Normal file
9
env/lib/python3.11/site-packages/rich_toolkit/styles/__init__.py
vendored
Normal file
@ -0,0 +1,9 @@
|
||||
from .fancy import FancyStyle
|
||||
from .tagged import TaggedStyle
|
||||
from .minimal import MinimalStyle
|
||||
|
||||
__all__ = [
|
||||
"FancyStyle",
|
||||
"TaggedStyle",
|
||||
"MinimalStyle",
|
||||
]
|
BIN
env/lib/python3.11/site-packages/rich_toolkit/styles/__pycache__/__init__.cpython-311.pyc
vendored
Normal file
BIN
env/lib/python3.11/site-packages/rich_toolkit/styles/__pycache__/__init__.cpython-311.pyc
vendored
Normal file
Binary file not shown.
BIN
env/lib/python3.11/site-packages/rich_toolkit/styles/__pycache__/base.cpython-311.pyc
vendored
Normal file
BIN
env/lib/python3.11/site-packages/rich_toolkit/styles/__pycache__/base.cpython-311.pyc
vendored
Normal file
Binary file not shown.
BIN
env/lib/python3.11/site-packages/rich_toolkit/styles/__pycache__/fancy.cpython-311.pyc
vendored
Normal file
BIN
env/lib/python3.11/site-packages/rich_toolkit/styles/__pycache__/fancy.cpython-311.pyc
vendored
Normal file
Binary file not shown.
BIN
env/lib/python3.11/site-packages/rich_toolkit/styles/__pycache__/minimal.cpython-311.pyc
vendored
Normal file
BIN
env/lib/python3.11/site-packages/rich_toolkit/styles/__pycache__/minimal.cpython-311.pyc
vendored
Normal file
Binary file not shown.
BIN
env/lib/python3.11/site-packages/rich_toolkit/styles/__pycache__/tagged.cpython-311.pyc
vendored
Normal file
BIN
env/lib/python3.11/site-packages/rich_toolkit/styles/__pycache__/tagged.cpython-311.pyc
vendored
Normal file
Binary file not shown.
125
env/lib/python3.11/site-packages/rich_toolkit/styles/base.py
vendored
Normal file
125
env/lib/python3.11/site-packages/rich_toolkit/styles/base.py
vendored
Normal file
@ -0,0 +1,125 @@
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Any, Generator, Iterable, List, Type, TypeVar
|
||||
from typing_extensions import Literal
|
||||
|
||||
from rich.color import Color
|
||||
from rich.console import (
|
||||
Console,
|
||||
ConsoleOptions,
|
||||
ConsoleRenderable,
|
||||
RenderableType,
|
||||
RenderResult,
|
||||
)
|
||||
from rich.segment import Segment
|
||||
from rich.text import Text
|
||||
|
||||
|
||||
from rich_toolkit.utils.colors import lighten
|
||||
|
||||
|
||||
ConsoleRenderableClass = TypeVar(
|
||||
"ConsoleRenderableClass", bound=Type[ConsoleRenderable]
|
||||
)
|
||||
|
||||
|
||||
ANIMATION_STATUS = Literal["started", "stopped", "error", "no_animation"]
|
||||
|
||||
|
||||
class BaseStyle(ABC):
|
||||
result_color: Color
|
||||
decoration_size: int
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.padding = 2
|
||||
self.cursor_offset = 0
|
||||
|
||||
self._animation_counter = 0
|
||||
self.decoration_size = 2
|
||||
|
||||
def empty_line(self) -> Text:
|
||||
return Text(" ")
|
||||
|
||||
def with_decoration(
|
||||
self,
|
||||
*renderables: RenderableType,
|
||||
animation_status: ANIMATION_STATUS = "no_animation",
|
||||
**metadata: Any,
|
||||
) -> ConsoleRenderable:
|
||||
class WithDecoration:
|
||||
@staticmethod
|
||||
def __rich_console__(
|
||||
console: Console, options: ConsoleOptions
|
||||
) -> RenderResult:
|
||||
for content in renderables:
|
||||
# our styles are potentially adding some paddings on the left
|
||||
# and right sides, so we need to adjust the max_width to
|
||||
# make sure that rich takes that into account
|
||||
options = options.update(
|
||||
max_width=console.width - self.decoration_size
|
||||
)
|
||||
|
||||
lines = console.render_lines(content, options, pad=False)
|
||||
|
||||
for line in Segment.split_lines(
|
||||
self.decorate(
|
||||
lines=lines,
|
||||
console=console,
|
||||
animation_status=animation_status,
|
||||
**metadata,
|
||||
)
|
||||
):
|
||||
yield from line
|
||||
yield Segment.line()
|
||||
|
||||
return WithDecoration()
|
||||
|
||||
def decorate_class(
|
||||
self, klass: ConsoleRenderableClass, **metadata: Any
|
||||
) -> ConsoleRenderableClass:
|
||||
style = self
|
||||
|
||||
class Decorated(klass): # type: ignore[valid-type,misc]
|
||||
def __rich_console__(
|
||||
self, console: Console, options: ConsoleOptions
|
||||
) -> RenderResult:
|
||||
lines = Segment.split_lines(super().__rich_console__(console, options)) # type: ignore
|
||||
|
||||
yield from style.decorate(lines=lines, console=console, **metadata)
|
||||
|
||||
return Decorated # type: ignore
|
||||
|
||||
@abstractmethod
|
||||
def decorate(
|
||||
self,
|
||||
console: Console,
|
||||
lines: Iterable[List[Segment]],
|
||||
animation_status: ANIMATION_STATUS = "no_animation",
|
||||
**kwargs: Any,
|
||||
) -> Generator[Segment, None, None]:
|
||||
raise NotImplementedError()
|
||||
|
||||
def _get_animation_colors(
|
||||
self,
|
||||
console: Console,
|
||||
steps: int = 5,
|
||||
animation_status: ANIMATION_STATUS = "started",
|
||||
**metadata: Any,
|
||||
) -> List[Color]:
|
||||
animated = animation_status == "started"
|
||||
|
||||
if animation_status == "error":
|
||||
base_color = console.get_style("error").color
|
||||
|
||||
if base_color is None:
|
||||
base_color = Color.parse("red")
|
||||
|
||||
else:
|
||||
base_color = console.get_style("progress").bgcolor
|
||||
|
||||
if not base_color:
|
||||
base_color = Color.parse("white")
|
||||
|
||||
if animated and base_color.triplet is not None:
|
||||
return [lighten(base_color, 0.1 * i) for i in range(0, steps)]
|
||||
|
||||
return [base_color] * steps
|
63
env/lib/python3.11/site-packages/rich_toolkit/styles/fancy.py
vendored
Normal file
63
env/lib/python3.11/site-packages/rich_toolkit/styles/fancy.py
vendored
Normal file
@ -0,0 +1,63 @@
|
||||
from typing import Any, Generator, Iterable, List
|
||||
|
||||
from rich._loop import loop_first_last
|
||||
from rich.console import (
|
||||
Console,
|
||||
)
|
||||
from rich.segment import Segment
|
||||
from rich.style import Style
|
||||
from rich.text import Text
|
||||
|
||||
|
||||
from .base import ANIMATION_STATUS, BaseStyle
|
||||
|
||||
|
||||
class FancyStyle(BaseStyle):
|
||||
def __init__(self, *args, **kwargs) -> None:
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.cursor_offset = 2
|
||||
self.decoration_size = 2
|
||||
|
||||
def decorate(
|
||||
self,
|
||||
console: Console,
|
||||
lines: Iterable[List[Segment]],
|
||||
animation_status: ANIMATION_STATUS = "no_animation",
|
||||
**metadata: Any,
|
||||
) -> Generator[Segment, None, None]:
|
||||
if animation_status != "no_animation":
|
||||
colors = self._get_animation_colors(
|
||||
console, animation_status=animation_status, **metadata
|
||||
)
|
||||
|
||||
self._animation_counter += 1
|
||||
|
||||
color_index = self._animation_counter % len(colors)
|
||||
|
||||
for first, last, line in loop_first_last(lines):
|
||||
if first:
|
||||
yield Segment("◆ ", style=Style(color=colors[color_index]))
|
||||
else:
|
||||
yield Segment("│ ")
|
||||
yield from line
|
||||
yield Segment.line()
|
||||
|
||||
return
|
||||
|
||||
for first, last, line in loop_first_last(lines):
|
||||
if first:
|
||||
decoration = "┌ " if metadata.get("title", False) else "◆ "
|
||||
elif last:
|
||||
decoration = "└ "
|
||||
else:
|
||||
decoration = "│ "
|
||||
|
||||
yield Segment(decoration)
|
||||
yield from line
|
||||
|
||||
if not last:
|
||||
yield Segment.line()
|
||||
|
||||
def empty_line(self) -> Text:
|
||||
return Text("│")
|
59
env/lib/python3.11/site-packages/rich_toolkit/styles/minimal.py
vendored
Normal file
59
env/lib/python3.11/site-packages/rich_toolkit/styles/minimal.py
vendored
Normal file
@ -0,0 +1,59 @@
|
||||
from typing import Any, Generator, Iterable, List
|
||||
|
||||
from rich._loop import loop_first_last
|
||||
from rich.console import (
|
||||
Console,
|
||||
)
|
||||
from rich.segment import Segment
|
||||
from rich.style import Style
|
||||
from rich.text import Text
|
||||
|
||||
|
||||
from .base import ANIMATION_STATUS, BaseStyle
|
||||
|
||||
|
||||
class MinimalStyle(BaseStyle):
|
||||
def __init__(self, *args, **kwargs) -> None:
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.cursor_offset = 2
|
||||
self.decoration_size = 2
|
||||
|
||||
def decorate(
|
||||
self,
|
||||
console: Console,
|
||||
lines: Iterable[List[Segment]],
|
||||
animation_status: ANIMATION_STATUS = "no_animation",
|
||||
**metadata: Any,
|
||||
) -> Generator[Segment, None, None]:
|
||||
if animation_status != "no_animation":
|
||||
block = "●"
|
||||
colors = self._get_animation_colors(
|
||||
console, animation_status=animation_status, **metadata
|
||||
)
|
||||
|
||||
self._animation_counter += 1
|
||||
|
||||
color_index = self._animation_counter % len(colors)
|
||||
|
||||
for first, last, line in loop_first_last(lines):
|
||||
if first:
|
||||
yield Segment(f"{block} ", style=Style(color=colors[color_index]))
|
||||
else:
|
||||
yield Segment(" " * 2)
|
||||
yield from line
|
||||
yield Segment.line()
|
||||
|
||||
return
|
||||
|
||||
for first, last, line in loop_first_last(lines):
|
||||
decoration = " " * 2
|
||||
|
||||
yield Segment(decoration)
|
||||
yield from line
|
||||
|
||||
if not last:
|
||||
yield Segment.line()
|
||||
|
||||
def empty_line(self) -> Text:
|
||||
return Text("")
|
100
env/lib/python3.11/site-packages/rich_toolkit/styles/tagged.py
vendored
Normal file
100
env/lib/python3.11/site-packages/rich_toolkit/styles/tagged.py
vendored
Normal file
@ -0,0 +1,100 @@
|
||||
from typing import Any, Generator, Iterable, List
|
||||
|
||||
from rich._loop import loop_first_last
|
||||
from rich.console import Console
|
||||
from rich.segment import Segment
|
||||
from rich.style import Style
|
||||
|
||||
from .base import ANIMATION_STATUS, BaseStyle
|
||||
|
||||
|
||||
class TaggedStyle(BaseStyle):
|
||||
def __init__(self, *args, **kwargs) -> None:
|
||||
self.tag_width = kwargs.pop("tag_width", 14)
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.padding = 2
|
||||
self.cursor_offset = self.tag_width + self.padding
|
||||
self.decoration_size = self.tag_width + self.padding
|
||||
|
||||
def _render_tag(
|
||||
self,
|
||||
text: str,
|
||||
console: Console,
|
||||
**metadata: Any,
|
||||
) -> Generator[Segment, None, None]:
|
||||
style_name = "tag.title" if metadata.get("title", False) else "tag"
|
||||
|
||||
style = console.get_style(style_name)
|
||||
|
||||
if text:
|
||||
text = f" {text} "
|
||||
|
||||
left_padding = self.tag_width - len(text)
|
||||
left_padding = max(0, left_padding)
|
||||
|
||||
yield Segment(" " * left_padding)
|
||||
yield Segment(text, style=style)
|
||||
yield Segment(" " * self.padding)
|
||||
|
||||
def decorate(
|
||||
self,
|
||||
console: Console,
|
||||
lines: Iterable[List[Segment]],
|
||||
animation_status: ANIMATION_STATUS = "no_animation",
|
||||
**metadata: Any,
|
||||
) -> Generator[Segment, None, None]:
|
||||
if animation_status != "no_animation":
|
||||
yield from self.decorate_with_animation(
|
||||
lines=lines,
|
||||
console=console,
|
||||
animation_status=animation_status,
|
||||
**metadata,
|
||||
)
|
||||
|
||||
return
|
||||
|
||||
tag = metadata.get("tag", "")
|
||||
|
||||
for first, last, line in loop_first_last(lines):
|
||||
text = tag if first else ""
|
||||
yield from self._render_tag(text, console=console, **metadata)
|
||||
yield from line
|
||||
|
||||
if not last:
|
||||
yield Segment.line()
|
||||
|
||||
def decorate_with_animation(
|
||||
self,
|
||||
console: Console,
|
||||
lines: Iterable[List[Segment]],
|
||||
animation_status: ANIMATION_STATUS = "no_animation",
|
||||
**metadata: Any,
|
||||
) -> Generator[Segment, None, None]:
|
||||
block = "█"
|
||||
block_length = 5
|
||||
colors = self._get_animation_colors(
|
||||
console, steps=block_length, animation_status=animation_status, **metadata
|
||||
)
|
||||
|
||||
left_padding = self.tag_width - block_length
|
||||
left_padding = max(0, left_padding)
|
||||
|
||||
self._animation_counter += 1
|
||||
|
||||
for first, _, line in loop_first_last(lines):
|
||||
if first:
|
||||
yield Segment(" " * left_padding)
|
||||
|
||||
for j in range(block_length):
|
||||
color_index = (j + self._animation_counter) % len(colors)
|
||||
yield Segment(block, style=Style(color=colors[color_index]))
|
||||
|
||||
else:
|
||||
yield Segment(" " * self.tag_width)
|
||||
|
||||
yield Segment(" " * self.padding)
|
||||
|
||||
yield from line
|
||||
yield Segment.line()
|
111
env/lib/python3.11/site-packages/rich_toolkit/toolkit.py
vendored
Normal file
111
env/lib/python3.11/site-packages/rich_toolkit/toolkit.py
vendored
Normal file
@ -0,0 +1,111 @@
|
||||
from typing import Any, Dict, List, Union
|
||||
|
||||
from rich.console import Console, RenderableType
|
||||
from rich.theme import Theme
|
||||
|
||||
from .styles.base import BaseStyle
|
||||
from .input import Input
|
||||
from .menu import Menu, Option, ReturnValue
|
||||
from .progress import Progress
|
||||
|
||||
|
||||
class RichToolkitTheme:
|
||||
def __init__(self, style: BaseStyle, theme: Dict[str, str]) -> None:
|
||||
self.style = style
|
||||
self.rich_theme = Theme(theme)
|
||||
|
||||
|
||||
class RichToolkit:
|
||||
def __init__(
|
||||
self,
|
||||
theme: RichToolkitTheme,
|
||||
handle_keyboard_interrupts: bool = True,
|
||||
) -> None:
|
||||
self.console = Console(theme=theme.rich_theme)
|
||||
self.theme = theme
|
||||
self.handle_keyboard_interrupts = handle_keyboard_interrupts
|
||||
|
||||
def __enter__(self):
|
||||
self.console.print()
|
||||
return self
|
||||
|
||||
def __exit__(
|
||||
self, exc_type: Any, exc_value: Any, traceback: Any
|
||||
) -> Union[bool, None]:
|
||||
if self.handle_keyboard_interrupts and exc_type is KeyboardInterrupt:
|
||||
# we want to handle keyboard interrupts gracefully, instead of showing a traceback
|
||||
# or any other error message
|
||||
return True
|
||||
|
||||
self.console.print()
|
||||
|
||||
def print_title(self, title: str, **metadata: Any) -> None:
|
||||
self.console.print(
|
||||
self.theme.style.with_decoration(title, title=True, **metadata)
|
||||
)
|
||||
|
||||
def print(self, *renderables: RenderableType, **metadata: Any) -> None:
|
||||
self.console.print(self.theme.style.with_decoration(*renderables, **metadata))
|
||||
|
||||
def print_as_string(self, *renderables: RenderableType, **metadata: Any) -> str:
|
||||
with self.console.capture() as capture:
|
||||
self.print(*renderables, **metadata)
|
||||
|
||||
return capture.get().rstrip()
|
||||
|
||||
def print_line(self) -> None:
|
||||
self.console.print(self.theme.style.empty_line())
|
||||
|
||||
def confirm(self, title: str, **metadata: Any) -> bool:
|
||||
return self.ask(
|
||||
title=title,
|
||||
options=[
|
||||
Option({"value": True, "name": "Yes"}),
|
||||
Option({"value": False, "name": "No"}),
|
||||
],
|
||||
inline=True,
|
||||
**metadata,
|
||||
)
|
||||
|
||||
def ask(
|
||||
self,
|
||||
title: str,
|
||||
options: List[Option[ReturnValue]],
|
||||
inline: bool = False,
|
||||
allow_filtering: bool = False,
|
||||
**metadata: Any,
|
||||
) -> ReturnValue:
|
||||
return Menu(
|
||||
title=title,
|
||||
options=options,
|
||||
console=self.console,
|
||||
style=self.theme.style,
|
||||
inline=inline,
|
||||
allow_filtering=allow_filtering,
|
||||
cursor_offset=self.theme.style.cursor_offset,
|
||||
**metadata,
|
||||
).ask()
|
||||
|
||||
def input(
|
||||
self, title: str, default: str = "", password: bool = False, **metadata: Any
|
||||
) -> str:
|
||||
return Input(
|
||||
console=self.console,
|
||||
style=self.theme.style,
|
||||
title=title,
|
||||
default=default,
|
||||
cursor_offset=self.theme.style.cursor_offset,
|
||||
password=password,
|
||||
**metadata,
|
||||
).ask()
|
||||
|
||||
def progress(
|
||||
self, title: str, transient: bool = False, transient_on_error: bool = False
|
||||
) -> Progress:
|
||||
return Progress(
|
||||
title=title,
|
||||
console=self.console,
|
||||
style=self.theme.style,
|
||||
transient=transient,
|
||||
transient_on_error=transient_on_error,
|
||||
)
|
0
env/lib/python3.11/site-packages/rich_toolkit/utils/__init__.py
vendored
Normal file
0
env/lib/python3.11/site-packages/rich_toolkit/utils/__init__.py
vendored
Normal file
BIN
env/lib/python3.11/site-packages/rich_toolkit/utils/__pycache__/__init__.cpython-311.pyc
vendored
Normal file
BIN
env/lib/python3.11/site-packages/rich_toolkit/utils/__pycache__/__init__.cpython-311.pyc
vendored
Normal file
Binary file not shown.
BIN
env/lib/python3.11/site-packages/rich_toolkit/utils/__pycache__/colors.cpython-311.pyc
vendored
Normal file
BIN
env/lib/python3.11/site-packages/rich_toolkit/utils/__pycache__/colors.cpython-311.pyc
vendored
Normal file
Binary file not shown.
14
env/lib/python3.11/site-packages/rich_toolkit/utils/colors.py
vendored
Normal file
14
env/lib/python3.11/site-packages/rich_toolkit/utils/colors.py
vendored
Normal file
@ -0,0 +1,14 @@
|
||||
from rich.color import Color
|
||||
from rich.color_triplet import ColorTriplet
|
||||
|
||||
|
||||
def lighten(color: Color, amount: float) -> Color:
|
||||
assert color.triplet
|
||||
|
||||
r, g, b = color.triplet
|
||||
|
||||
r = int(r + (255 - r) * amount)
|
||||
g = int(g + (255 - g) * amount)
|
||||
b = int(b + (255 - b) * amount)
|
||||
|
||||
return Color.from_triplet(ColorTriplet(r, g, b))
|
Reference in New Issue
Block a user