From ac24e77ecfff2be073a289096a9f3738dfe5c42c Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Fri, 19 Aug 2022 14:13:57 +0100 Subject: [PATCH] fix screenshots --- docs/examples/introduction/timers.css | 20 +++++----- docs/examples/introduction/timers.py | 2 +- docs/introduction.md | 5 +++ src/textual/_doc.py | 2 + src/textual/_segment_tools.py | 2 +- src/textual/_timer.py | 54 +++++++++++++++----------- src/textual/app.py | 9 +++-- src/textual/dom.py | 2 +- src/textual/drivers/headless_driver.py | 34 +++++++++++++++- src/textual/message_pump.py | 5 ++- src/textual/screen.py | 2 - src/textual/widgets/_header.py | 2 +- 12 files changed, 93 insertions(+), 46 deletions(-) diff --git a/docs/examples/introduction/timers.css b/docs/examples/introduction/timers.css index f7030e623..a9fc25a31 100644 --- a/docs/examples/introduction/timers.css +++ b/docs/examples/introduction/timers.css @@ -1,12 +1,12 @@ TimerWidget { layout: horizontal; - height: 5; background: $panel-darken-1; border: tall $panel-darken-2; + height: 5; + min-width: 50; margin: 1; padding: 0 1; transition: background 300ms linear; - min-width: 50; } TimerWidget.started { @@ -35,6 +35,14 @@ Button { dock: left; } +#stop { + dock: left; + display: none; +} + +#reset { + dock: right; +} TimerWidget.started #start { display: none @@ -48,11 +56,3 @@ TimerWidget.started #reset { visibility: hidden } -#stop { - dock: left; - display: none; -} - -#reset { - dock: right; -} diff --git a/docs/examples/introduction/timers.py b/docs/examples/introduction/timers.py index d990631ef..183e50a66 100644 --- a/docs/examples/introduction/timers.py +++ b/docs/examples/introduction/timers.py @@ -91,6 +91,6 @@ class TimerApp(App): timers.last().remove() -app = TimerApp(title="TimerApp", css_path="timers.css") +app = TimerApp(css_path="timers.css") if __name__ == "__main__": app.run() diff --git a/docs/introduction.md b/docs/introduction.md index 6721a5d7e..1e855bfed 100644 --- a/docs/introduction.md +++ b/docs/introduction.md @@ -10,6 +10,11 @@ By the end of this page you should have a good idea of the steps involved in cre - Installed `textual` from Pypi. - Basic Python skills. + +```{.textual path="docs/examples/introduction/timers.py"} + +``` + ## A Simple App Let's looks at the simplest possible Textual app. diff --git a/src/textual/_doc.py b/src/textual/_doc.py index f201402e9..802dd2091 100644 --- a/src/textual/_doc.py +++ b/src/textual/_doc.py @@ -12,6 +12,7 @@ def format_svg(source, language, css_class, options, md, attrs, **kwargs): os.environ["LINES"] = attrs.get("lines", "24") path = attrs.get("path") + print(f"screenshotting {path!r}") if path: cwd = os.getcwd() examples_path, filename = os.path.split(path) @@ -34,4 +35,5 @@ def format_svg(source, language, css_class, options, md, attrs, **kwargs): app = app_vars["app"] app.run() svg = app._screenshot + return svg diff --git a/src/textual/_segment_tools.py b/src/textual/_segment_tools.py index 9863a9b59..b2e4a13f7 100644 --- a/src/textual/_segment_tools.py +++ b/src/textual/_segment_tools.py @@ -188,6 +188,6 @@ def align_lines( get_line_length = Segment.get_line_length for line in lines: left_space = width - get_line_length(line) - yield [*line, Segment(" " * left_space, style)] + yield [Segment(" " * left_space, style), *line] yield from blank_lines(bottom_blank_lines) diff --git a/src/textual/_timer.py b/src/textual/_timer.py index 3231fe016..4edf75c43 100644 --- a/src/textual/_timer.py +++ b/src/textual/_timer.py @@ -61,6 +61,7 @@ class Timer: self._repeat = repeat self._skip = skip self._active = Event() + self._task: Task | None = None if not pause: self._active.set() @@ -82,17 +83,21 @@ class Timer: Returns: Task: A Task instance for the timer. """ - self._task = asyncio.create_task(self._run()) + self._task = asyncio.create_task(self.run()) return self._task def stop_no_wait(self) -> None: """Stop the timer.""" - self._task.cancel() + if self._task is not None: + self._task.cancel() + self._task = None async def stop(self) -> None: """Stop the timer, and block until it exits.""" - self._task.cancel() - await self._task + if self._task is not None: + self._active.set() + self._task.cancel() + self._task = None def pause(self) -> None: """Pause the timer.""" @@ -102,31 +107,35 @@ class Timer: """Result a paused timer.""" self._active.set() + async def run(self) -> None: + """Run the timer task.""" + try: + await self._run() + except CancelledError: + pass + async def _run(self) -> None: """Run the timer.""" count = 0 _repeat = self._repeat _interval = self._interval start = _clock.get_time_no_wait() - try: - while _repeat is None or count <= _repeat: - next_timer = start + ((count + 1) * _interval) - now = await _clock.get_time() - if self._skip and next_timer < now: - count += 1 - continue - now = await _clock.get_time() - wait_time = max(0, next_timer - now) - if wait_time: - await _clock.sleep(wait_time) + while _repeat is None or count <= _repeat: + next_timer = start + ((count + 1) * _interval) + now = await _clock.get_time() + if self._skip and next_timer < now: count += 1 - try: - await self._tick(next_timer=next_timer, count=count) - except EventTargetGone: - break - await self._active.wait() - except CancelledError: - pass + continue + now = await _clock.get_time() + wait_time = max(0, next_timer - now) + if wait_time: + await _clock.sleep(wait_time) + count += 1 + try: + await self._tick(next_timer=next_timer, count=count) + except EventTargetGone: + break + await self._active.wait() async def _tick(self, *, next_timer: float, count: int) -> None: """Triggers the Timer's action: either call its callback, or sends an event to its target""" @@ -140,5 +149,4 @@ class Timer: count=count, callback=self._callback, ) - await self.target.post_priority_message(event) diff --git a/src/textual/app.py b/src/textual/app.py index 70020ceae..c0c574bd6 100644 --- a/src/textual/app.py +++ b/src/textual/app.py @@ -144,7 +144,7 @@ class App(Generic[ReturnType], DOMNode): log_color_system: Literal[ "auto", "standard", "256", "truecolor", "windows" ] = "auto", - title: str = "Textual Application", + title: str | None = None, css_path: str | PurePath | None = None, watch_css: bool = False, ): @@ -189,7 +189,10 @@ class App(Generic[ReturnType], DOMNode): self.animate = self._animator.bind(self) self.mouse_position = Offset(0, 0) self.bindings = Bindings() - self._title = title + if title is None: + self._title = f"{self.__class__.__name__}" + else: + self._title = title self._log_console: Console | None = None self._log_file: TextIO | None = None @@ -1015,7 +1018,7 @@ class App(Generic[ReturnType], DOMNode): self._screenshot = svg # type: ignore await self.shutdown() - self.set_timer(screenshot_timer, on_screenshot) + self.set_timer(screenshot_timer, on_screenshot, name="screenshot timer") def on_mount(self) -> None: widgets = self.compose() diff --git a/src/textual/dom.py b/src/textual/dom.py index 630c50aee..357520005 100644 --- a/src/textual/dom.py +++ b/src/textual/dom.py @@ -105,7 +105,7 @@ class DOMNode(MessagePump): self._auto_refresh_timer = None if interval is not None: self._auto_refresh_timer = self.set_interval( - interval, self._automatic_refresh + interval, self._automatic_refresh, name=f"auto refresh {self!r}" ) self._auto_refresh = interval diff --git a/src/textual/drivers/headless_driver.py b/src/textual/drivers/headless_driver.py index ede6c4e8a..cdde957b9 100644 --- a/src/textual/drivers/headless_driver.py +++ b/src/textual/drivers/headless_driver.py @@ -1,14 +1,44 @@ from __future__ import annotations - +import asyncio from ..driver import Driver +from ..geometry import Size +from .. import events class HeadlessDriver(Driver): """A do-nothing driver for testing.""" + def _get_terminal_size(self) -> tuple[int, int]: + width: int | None = 80 + height: int | None = 25 + import shutil + + try: + width, height = shutil.get_terminal_size() + except (AttributeError, ValueError, OSError): + try: + width, height = shutil.get_terminal_size() + except (AttributeError, ValueError, OSError): + pass + width = width or 80 + height = height or 25 + return width, height + def start_application_mode(self) -> None: - pass + loop = asyncio.get_running_loop() + + def send_size_event(): + terminal_size = self._get_terminal_size() + width, height = terminal_size + textual_size = Size(width, height) + event = events.Resize(self._target, textual_size, textual_size) + asyncio.run_coroutine_threadsafe( + self._target.post_message(event), + loop=loop, + ) + + send_size_event() def disable_input(self) -> None: pass diff --git a/src/textual/message_pump.py b/src/textual/message_pump.py index 38e013dd9..09e05cf7b 100644 --- a/src/textual/message_pump.py +++ b/src/textual/message_pump.py @@ -254,7 +254,8 @@ class MessagePump(metaclass=MessagePumpMeta): if self._closed or self._closing: return self._closing = True - for timer in self._timers: + stop_timers = list(self._timers) + for timer in stop_timers: await timer.stop() self._timers.clear() await self._message_queue.put(MessagePriority(None)) @@ -274,7 +275,7 @@ class MessagePump(metaclass=MessagePumpMeta): pass finally: self._running = False - for timer in self._timers: + for timer in list(self._timers): await timer.stop() async def _process_messages(self) -> None: diff --git a/src/textual/screen.py b/src/textual/screen.py index 993fdb3f5..b8f6af01f 100644 --- a/src/textual/screen.py +++ b/src/textual/screen.py @@ -33,8 +33,6 @@ class Screen(Widget): CSS = """ Screen { - color: $text-background; - background: $background; layout: vertical; overflow-y: auto; } diff --git a/src/textual/widgets/_header.py b/src/textual/widgets/_header.py index 39b3c8893..8374e4327 100644 --- a/src/textual/widgets/_header.py +++ b/src/textual/widgets/_header.py @@ -41,7 +41,7 @@ class HeaderClock(Widget): """ def on_mount(self) -> None: - self.set_interval(1, callback=self.refresh) + self.set_interval(1, callback=self.refresh, name=f"update header clock") def render(self): return Text(datetime.now().time().strftime("%X"))