Files
tidal-dl-ng-webui/env/lib/python3.11/site-packages/rich_toolkit/input.py
2024-12-27 22:31:23 +09:00

193 lines
5.2 KiB
Python

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