diff --git a/CHANGELOG.md b/CHANGELOG.md index d4a0a53d..6fe252fb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,14 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [10.3.0] - Unreleased + +### Added + +- Added Console.size setter +- Added Console.width setter +- Added Console.height setter + ## [10.2.2] - 2021-05-19 ### Fixed diff --git a/rich/console.py b/rich/console.py index 56de7719..e6873bbb 100644 --- a/rich/console.py +++ b/rich/console.py @@ -962,8 +962,16 @@ class Console: Returns: int: The width (in characters) of the console. """ - width, _ = self.size - return width + return self.size.width + + @width.setter + def width(self, width: int) -> None: + """Set width. + + Args: + width (int): New width. + """ + self._width = width @property def height(self) -> int: @@ -972,8 +980,16 @@ class Console: Returns: int: The height (in lines) of the console. """ - _, height = self.size - return height + return self.size.height + + @height.setter + def height(self, height: int) -> None: + """Set height. + + Args: + height (int): new height. + """ + self._height = height def bell(self) -> None: """Play a 'bell' sound (if supported by the terminal).""" diff --git a/rich/screen.py b/rich/screen.py index dfbff7f3..e1245467 100644 --- a/rich/screen.py +++ b/rich/screen.py @@ -24,6 +24,8 @@ class Screen: style (StyleType, optional): Optional background style. Defaults to None. """ + renderable: "RenderableType" + def __init__( self, *renderables: "RenderableType", diff --git a/rich/segment.py b/rich/segment.py index 094a7b25..62197a1c 100644 --- a/rich/segment.py +++ b/rich/segment.py @@ -7,7 +7,11 @@ from .style import Style from itertools import filterfalse from operator import attrgetter -from typing import Iterable, List, Sequence, Union, Tuple +from typing import Iterable, List, Sequence, Union, Tuple, TYPE_CHECKING + + +if TYPE_CHECKING: + from .console import Console, ConsoleOptions, RenderResult class ControlType(IntEnum): @@ -404,6 +408,30 @@ class Segment(NamedTuple): yield cls(text, None, control) +class Segments: + """A simple renderable to render an iterable of segments. + + Args: + segments (Iterable[Segment]): An iterable of segments. + new_lines (bool, optional): Add new lines between segments. Defaults to False. + """ + + def __init__(self, segments: Iterable[Segment], new_lines: bool = False) -> None: + self.segments = list(segments) + self.new_lines = new_lines + + def __rich_console__( + self, console: "Console", options: "ConsoleOptions" + ) -> "RenderResult": + if self.new_lines: + line = Segment.line() + for segment in self.segments: + yield segment + yield line + else: + yield from self.segments + + if __name__ == "__main__": # pragma: no cover from rich.syntax import Syntax from rich.text import Text diff --git a/rich/tui/app.py b/rich/tui/app.py index 207bde67..58629e24 100644 --- a/rich/tui/app.py +++ b/rich/tui/app.py @@ -4,8 +4,8 @@ import logging import signal from typing import Any, Dict, Set -from rich.live import Live from rich.control import Control +from rich.repr import rich_repr, RichReprResult from rich.screen import Screen from . import events @@ -22,23 +22,27 @@ log = logging.getLogger("rich") LayoutDefinition = Dict[str, Any] +@rich_repr class App(MessagePump): + view: View + def __init__( self, console: Console = None, view: View = None, screen: bool = True, - auto_refresh=4, title: str = "Megasoma Application", ): super().__init__() self.console = console or get_console() self._screen = screen - self._auto_refresh = auto_refresh self.title = title self.view = view or LayoutView() self.children: Set[MessagePump] = set() + def __rich_repr__(self) -> RichReprResult: + yield "title", self.title + @classmethod def run(cls, console: Console = None, screen: bool = True): async def run_app() -> None: @@ -99,8 +103,6 @@ class App(MessagePump): if __name__ == "__main__": import asyncio from logging import FileHandler - from rich.layout import Layout - from rich.panel import Panel from .widgets.header import Header diff --git a/rich/tui/events.py b/rich/tui/events.py index fd9e7ccc..975c803d 100644 --- a/rich/tui/events.py +++ b/rich/tui/events.py @@ -136,7 +136,7 @@ class Timer(Event, type=EventType.TIMER, priority=10): self.callback = callback def __rich_repr__(self) -> RichReprResult: - yield "timer", self.timer + yield self.timer.name class Focus(Event, type=EventType.FOCUS): diff --git a/rich/tui/manager.py b/rich/tui/manager.py deleted file mode 100644 index cd4173b8..00000000 --- a/rich/tui/manager.py +++ /dev/null @@ -1,6 +0,0 @@ -from rich.layout import Layout - - -class LayoutManager: - def __init__(self) -> None: - self.layout = Layout() \ No newline at end of file diff --git a/rich/tui/message_pump.py b/rich/tui/message_pump.py index 90ccc1cc..a271f412 100644 --- a/rich/tui/message_pump.py +++ b/rich/tui/message_pump.py @@ -105,7 +105,7 @@ class MessagePump: except Exception: log.exception("error getting message") break - log.debug(repr(message)) + log.debug("%r -> %r", message, self) await self.dispatch_message(message, priority) async def dispatch_message(self, message: Message, priority: int) -> Optional[bool]: @@ -150,16 +150,3 @@ class MessagePump: async def on_timer(self, event: events.Timer) -> None: if event.callback is not None: await event.callback() - - -if __name__ == "__main__": - - class Widget(MessagePump): - pass - - widget1 = Widget() - widget2 = Widget() - - import asyncio - - asyncio.get_event_loop().run_until_complete(widget1.run_message_loop()) \ No newline at end of file diff --git a/rich/tui/scrollbar.py b/rich/tui/scrollbar.py new file mode 100644 index 00000000..9a8d4a18 --- /dev/null +++ b/rich/tui/scrollbar.py @@ -0,0 +1,85 @@ +from typing import List, Optional + +from rich.segment import Segment +from rich.style import Style + + +def render_bar( + height: int = 25, + size: float = 100, + window_size: float = 25, + position: float = 0, + bar_style: Optional[Style] = None, + back_style: Optional[Style] = None, + ascii_only: bool = False, + vertical: bool = True, +) -> List[Segment]: + if vertical: + if ascii_only: + solid = "|" + half_start = "|" + half_end = "|" + else: + solid = "┃" + half_start = "╻" + half_end = "╹" + else: + if ascii_only: + solid = "-" + half_start = "-" + half_end = "-" + else: + solid = "━" + half_start = "╺" + half_end = "╸" + + _bar_style = bar_style or Style.parse("bright_magenta") + _back_style = back_style or Style.parse("#555555") + + _Segment = Segment + + start_bar_segment = _Segment(half_start, _bar_style) + end_bar_segment = _Segment(half_end, _bar_style) + bar_segment = _Segment(solid, _bar_style) + + start_back_segment = _Segment(half_end, _back_style) + end_back_segment = _Segment(half_end, _back_style) + back_segment = _Segment(solid, _back_style) + + segments = [back_segment] * height + + step_size = size / height + + start = position / step_size + end = (position + window_size) / step_size + + start_index = int(start) + end_index = int(end) + bar_height = (end_index - start_index) + 1 + + segments[start_index:end_index] = [bar_segment] * bar_height + + sub_position = start % 1.0 + if sub_position >= 0.5: + segments[start_index] = start_bar_segment + elif start_index: + segments[start_index - 1] = end_back_segment + + sub_position = end % 1.0 + if sub_position < 0.5: + segments[end_index] = end_bar_segment + elif end_index + 1 < len(segments): + segments[end_index + 1] = start_back_segment + + return segments + + +if __name__ == "__main__": + from rich.console import Console + from rich.segment import Segments + + console = Console() + + bar = render_bar(height=20, position=10, vertical=False, ascii_only=False) + + console.print(Segments(bar, new_lines=False)) diff --git a/rich/tui/view.py b/rich/tui/view.py index 807e5bc6..7de89dac 100644 --- a/rich/tui/view.py +++ b/rich/tui/view.py @@ -1,8 +1,9 @@ +from abc import ABC, abstractmethod from typing import TYPE_CHECKING -from rich.console import Console, RenderableType +from rich.console import Console, ConsoleOptions, RenderResult, RenderableType from rich.layout import Layout -from rich.live import Live +from rich.repr import rich_repr, RichReprResult from . import events from ._context import active_app @@ -14,7 +15,8 @@ if TYPE_CHECKING: from .app import App -class View(MessagePump): +@rich_repr +class View(ABC, MessagePump): @property def app(self) -> "App": return active_app.get() @@ -23,16 +25,34 @@ class View(MessagePump): def console(self) -> Console: return active_app.get().console + def __rich_console__( + self, console: Console, options: ConsoleOptions + ) -> RenderResult: + return + yield + + def __rich_repr__(self) -> RichReprResult: + return + yield + async def on_resize(self, event: events.Resize) -> None: pass + @abstractmethod + async def mount(self, widget: Widget, *, slot: str = "main") -> None: + ... + class LayoutView(View): layout: Layout def __init__( - self, layout: Layout = None, title: str = "Layout Application" + self, + layout: Layout = None, + name: str = "default", + title: str = "Layout Application", ) -> None: + self.name = name self.title = title if layout is None: layout = Layout() @@ -49,6 +69,9 @@ class LayoutView(View): self.layout = layout super().__init__() + def __rich_repr__(self) -> RichReprResult: + yield "name", self.name + def __rich__(self) -> RenderableType: return self.layout diff --git a/rich/tui/widgets/window.py b/rich/tui/widgets/window.py new file mode 100644 index 00000000..98595232 --- /dev/null +++ b/rich/tui/widgets/window.py @@ -0,0 +1,14 @@ +from typing import Optional + +from rich.console import RenderableType +from ..widget import Widget + + +class Window(Widget): + renderable: Optional[RenderableType] + + def __init__(self, renderable: RenderableType): + self.renderable = renderable + + def update(self, renderable: RenderableType) -> None: + self.renderable = renderable \ No newline at end of file