Initial: Standalone mixer app with MixerPage, scene management, WS proxy
This commit is contained in:
@@ -0,0 +1,404 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import inspect
|
||||
import json
|
||||
import sys
|
||||
from collections.abc import Iterator
|
||||
from functools import wraps
|
||||
from typing import (
|
||||
Any,
|
||||
Callable,
|
||||
Dict,
|
||||
List,
|
||||
Literal,
|
||||
Optional,
|
||||
TypeVar,
|
||||
Union,
|
||||
cast,
|
||||
overload,
|
||||
)
|
||||
|
||||
from rich.console import ConsoleRenderable, RenderableType
|
||||
from rich.pretty import Pretty
|
||||
from rich.text import Text
|
||||
from rich.theme import Theme
|
||||
from typing_extensions import Concatenate, ParamSpec
|
||||
|
||||
from .input import Input
|
||||
from .menu import Menu, Option, ReturnValue
|
||||
from .progress import Progress
|
||||
from .styles.base import BaseStyle
|
||||
|
||||
OutputT = TypeVar("OutputT")
|
||||
ReturnT = TypeVar("ReturnT")
|
||||
P = ParamSpec("P")
|
||||
OutputRenderer = Union[
|
||||
Callable[[OutputT], Optional[RenderableType]],
|
||||
Callable[[OutputT, "RichToolkit"], Optional[RenderableType]],
|
||||
]
|
||||
|
||||
|
||||
def _unavailable_in_json_mode(
|
||||
method_name: str,
|
||||
) -> Callable[
|
||||
[Callable[Concatenate["RichToolkit", P], ReturnT]],
|
||||
Callable[Concatenate["RichToolkit", P], ReturnT],
|
||||
]:
|
||||
def decorator(
|
||||
method: Callable[Concatenate["RichToolkit", P], ReturnT],
|
||||
) -> Callable[Concatenate["RichToolkit", P], ReturnT]:
|
||||
@wraps(method)
|
||||
def wrapper(self: "RichToolkit", *args: P.args, **kwargs: P.kwargs) -> ReturnT:
|
||||
if self.mode == "json":
|
||||
raise RuntimeError(f"{method_name}() is not available in JSON mode")
|
||||
|
||||
return method(self, *args, **kwargs)
|
||||
|
||||
return wrapper
|
||||
|
||||
return decorator
|
||||
|
||||
|
||||
def _is_output_stream(data: Any) -> bool:
|
||||
return isinstance(data, Iterator)
|
||||
|
||||
|
||||
def _dump_output_data(data: Any) -> Any:
|
||||
model_dump = getattr(data, "model_dump", None)
|
||||
if callable(model_dump):
|
||||
return _dump_output_data(model_dump(mode="json"))
|
||||
|
||||
if isinstance(data, dict):
|
||||
return {key: _dump_output_data(value) for key, value in data.items()}
|
||||
|
||||
if isinstance(data, (list, tuple)):
|
||||
return [_dump_output_data(item) for item in data]
|
||||
|
||||
return data
|
||||
|
||||
|
||||
def _format_output_value(value: Any) -> str:
|
||||
if isinstance(value, (dict, list)):
|
||||
return json.dumps(value, ensure_ascii=False, allow_nan=False)
|
||||
|
||||
return str(value)
|
||||
|
||||
|
||||
def _record_lines(data: dict[Any, Any]) -> list[str]:
|
||||
return [f"{key}: {_format_output_value(value)}" for key, value in data.items()]
|
||||
|
||||
|
||||
def _default_output_renderable(data: Any) -> RenderableType:
|
||||
dumped = _dump_output_data(data)
|
||||
|
||||
if isinstance(dumped, dict):
|
||||
return Text("\n".join(_record_lines(dumped)))
|
||||
|
||||
if isinstance(dumped, list):
|
||||
lines: list[str] = []
|
||||
for index, item in enumerate(dumped):
|
||||
if not isinstance(item, dict):
|
||||
return Pretty(dumped)
|
||||
if index:
|
||||
lines.append("")
|
||||
lines.extend(_record_lines(item))
|
||||
return Text("\n".join(lines))
|
||||
|
||||
return Pretty(dumped)
|
||||
|
||||
|
||||
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,
|
||||
style: Optional[BaseStyle] = None,
|
||||
theme: Optional[RichToolkitTheme] = None,
|
||||
handle_keyboard_interrupts: bool = True,
|
||||
mode: Literal["human", "json"] = "human",
|
||||
) -> None:
|
||||
if mode not in ("human", "json"):
|
||||
raise ValueError("mode must be 'human' or 'json'")
|
||||
|
||||
self.mode = mode
|
||||
self._json_output_written = False
|
||||
|
||||
self.theme = theme
|
||||
if theme is not None:
|
||||
# TODO: deprecate
|
||||
self.style = theme.style
|
||||
self.style.theme = theme.rich_theme
|
||||
self.style.console.push_theme(theme.rich_theme)
|
||||
else:
|
||||
if style is None:
|
||||
from .styles import MinimalStyle
|
||||
|
||||
style = MinimalStyle()
|
||||
|
||||
self.style = style
|
||||
|
||||
self.console = self.style.console
|
||||
|
||||
self.handle_keyboard_interrupts = handle_keyboard_interrupts
|
||||
|
||||
def __enter__(self):
|
||||
if self.mode == "human":
|
||||
if (renderable := self.style.render_context_enter()) is not None:
|
||||
self.console.print(renderable)
|
||||
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
|
||||
|
||||
if self.mode == "human":
|
||||
if (renderable := self.style.render_context_exit()) is not None:
|
||||
self.console.print(renderable)
|
||||
|
||||
return None
|
||||
|
||||
def print_title(self, title: str, end: str = "\n", **metadata: Any) -> None:
|
||||
if self.mode == "json":
|
||||
return
|
||||
|
||||
self.console.print(
|
||||
self.style.render_element(title, title=True, **metadata), end=end
|
||||
)
|
||||
|
||||
def _print(
|
||||
self,
|
||||
*renderables: RenderableType,
|
||||
end: str = "\n",
|
||||
_force: bool = False,
|
||||
**metadata: Any,
|
||||
) -> None:
|
||||
if self.mode == "json" and not _force:
|
||||
return
|
||||
|
||||
self.console.print(
|
||||
*[
|
||||
self.style.render_element(renderable, **metadata)
|
||||
for renderable in renderables
|
||||
],
|
||||
end=end,
|
||||
)
|
||||
|
||||
def print(
|
||||
self, *renderables: RenderableType, end: str = "\n", **metadata: Any
|
||||
) -> None:
|
||||
self._print(*renderables, end=end, _force=False, **metadata)
|
||||
|
||||
def print_as_string(self, *renderables: RenderableType, **metadata: Any) -> str:
|
||||
with self.console.capture() as capture:
|
||||
self._print(*renderables, _force=True, **metadata)
|
||||
|
||||
return capture.get().rstrip()
|
||||
|
||||
def print_line(self) -> None:
|
||||
if self.mode == "json":
|
||||
return
|
||||
|
||||
self.console.print(self.style.empty_line())
|
||||
|
||||
def _write_json_line(self, data: Any) -> None:
|
||||
payload = json.dumps(
|
||||
_dump_output_data(data),
|
||||
ensure_ascii=False,
|
||||
allow_nan=False,
|
||||
)
|
||||
sys.stdout.write(payload + "\n")
|
||||
sys.stdout.flush()
|
||||
|
||||
def _render_custom_output(
|
||||
self, render_output: OutputRenderer[Any], data: Any
|
||||
) -> None:
|
||||
signature = inspect.signature(render_output)
|
||||
|
||||
if len(signature.parameters) == 1:
|
||||
render_one_arg = cast(
|
||||
Callable[[Any], Optional[RenderableType]],
|
||||
render_output,
|
||||
)
|
||||
renderable = render_one_arg(data)
|
||||
else:
|
||||
render_two_args = cast(
|
||||
Callable[[Any, RichToolkit], Optional[RenderableType]],
|
||||
render_output,
|
||||
)
|
||||
renderable = render_two_args(data, self)
|
||||
|
||||
if renderable is not None:
|
||||
self.print(renderable)
|
||||
|
||||
def _write_json_output(self, data: Any) -> None:
|
||||
if _is_output_stream(data):
|
||||
for item in data:
|
||||
self._write_json_line(item)
|
||||
return
|
||||
|
||||
self._write_json_line(data)
|
||||
|
||||
def _render_human_output(
|
||||
self,
|
||||
data: Any,
|
||||
render_output: Optional[Union[RenderableType, OutputRenderer[Any]]] = None,
|
||||
) -> None:
|
||||
if render_output is not None:
|
||||
if callable(render_output):
|
||||
self._render_custom_output(
|
||||
cast(OutputRenderer[Any], render_output), data
|
||||
)
|
||||
else:
|
||||
self.print(render_output)
|
||||
|
||||
return
|
||||
|
||||
if isinstance(data, (str, ConsoleRenderable)):
|
||||
self.print(data)
|
||||
else:
|
||||
self.print(_default_output_renderable(data))
|
||||
|
||||
@overload
|
||||
def output(self, data: OutputT, render_output: None = None) -> None: ...
|
||||
|
||||
@overload
|
||||
def output(self, data: OutputT, render_output: OutputRenderer[OutputT]) -> None: ...
|
||||
|
||||
@overload
|
||||
def output(self, data: Any, render_output: RenderableType) -> None: ...
|
||||
|
||||
def output(
|
||||
self,
|
||||
data: Any,
|
||||
render_output: Optional[Union[RenderableType, OutputRenderer[Any]]] = None,
|
||||
) -> None:
|
||||
if self.mode == "json":
|
||||
if self._json_output_written:
|
||||
raise RuntimeError("output() was already called in JSON mode")
|
||||
|
||||
self._write_json_output(data)
|
||||
self._json_output_written = True
|
||||
return
|
||||
|
||||
if _is_output_stream(data):
|
||||
for item in data:
|
||||
self._render_human_output(item, render_output=render_output)
|
||||
return
|
||||
|
||||
self._render_human_output(data, render_output=render_output)
|
||||
|
||||
@_unavailable_in_json_mode("confirm")
|
||||
def confirm(self, label: str, **metadata: Any) -> bool:
|
||||
options: List[Option[bool]] = [
|
||||
Option({"value": True, "name": "Yes"}),
|
||||
Option({"value": False, "name": "No"}),
|
||||
]
|
||||
|
||||
return self.ask(
|
||||
label=label,
|
||||
options=options,
|
||||
inline=True,
|
||||
**metadata,
|
||||
)
|
||||
|
||||
@overload
|
||||
def ask(
|
||||
self,
|
||||
label: str,
|
||||
options: List[Option[ReturnValue]],
|
||||
inline: bool = False,
|
||||
allow_filtering: bool = False,
|
||||
multiple: Literal[False] = False,
|
||||
**metadata: Any,
|
||||
) -> ReturnValue: ...
|
||||
|
||||
@overload
|
||||
def ask(
|
||||
self,
|
||||
label: str,
|
||||
options: List[Option[ReturnValue]],
|
||||
inline: bool = False,
|
||||
allow_filtering: bool = False,
|
||||
*,
|
||||
multiple: Literal[True],
|
||||
**metadata: Any,
|
||||
) -> List[ReturnValue]: ...
|
||||
|
||||
def ask(
|
||||
self,
|
||||
label: str,
|
||||
options: List[Option[ReturnValue]],
|
||||
inline: bool = False,
|
||||
allow_filtering: bool = False,
|
||||
multiple: bool = False,
|
||||
**metadata: Any,
|
||||
) -> Union[ReturnValue, List[ReturnValue]]:
|
||||
if self.mode == "json":
|
||||
raise RuntimeError("ask() is not available in JSON mode")
|
||||
|
||||
return Menu(
|
||||
label=label,
|
||||
options=options,
|
||||
console=self.console,
|
||||
style=self.style,
|
||||
inline=inline,
|
||||
allow_filtering=allow_filtering,
|
||||
multiple=multiple,
|
||||
**metadata,
|
||||
).ask()
|
||||
|
||||
@_unavailable_in_json_mode("input")
|
||||
def input(
|
||||
self,
|
||||
title: str,
|
||||
default: str = "",
|
||||
placeholder: str = "",
|
||||
password: bool = False,
|
||||
required: bool = False,
|
||||
required_message: str = "",
|
||||
inline: bool = False,
|
||||
value: str = "",
|
||||
**metadata: Any,
|
||||
) -> str:
|
||||
return Input(
|
||||
name=title,
|
||||
label=title,
|
||||
default=default,
|
||||
placeholder=placeholder,
|
||||
password=password,
|
||||
required=required,
|
||||
required_message=required_message,
|
||||
inline=inline,
|
||||
style=self.style,
|
||||
value=value,
|
||||
**metadata,
|
||||
).ask()
|
||||
|
||||
def progress(
|
||||
self,
|
||||
title: str,
|
||||
transient: bool = False,
|
||||
transient_on_error: bool = False,
|
||||
inline_logs: bool = False,
|
||||
lines_to_show: int = -1,
|
||||
**metadata: Any,
|
||||
) -> Progress:
|
||||
return Progress(
|
||||
title=title,
|
||||
console=self.console,
|
||||
style=self.style,
|
||||
transient=True if self.mode == "json" else transient,
|
||||
transient_on_error=transient_on_error,
|
||||
inline_logs=inline_logs,
|
||||
lines_to_show=lines_to_show,
|
||||
quiet=self.mode == "json",
|
||||
**metadata,
|
||||
)
|
||||
Reference in New Issue
Block a user