added press to run

This commit is contained in:
Will McGugan
2022-08-20 21:23:26 +01:00
parent bda9790a2a
commit 4e4d0b1bb9
5 changed files with 44 additions and 18 deletions

View File

@@ -248,7 +248,7 @@ You may have noticed that the stop button (`#stop` in the CSS) has `display: non
We want our Stopwatch widget to have two states. An _unstarted_ state with a Start and Reset button, and a _started_ state with a Stop button. We want our Stopwatch widget to have two states. An _unstarted_ state with a Start and Reset button, and a _started_ state with a Stop button.
There are other differences between the two states. It would be nice if the stopwatch turns green when it is started. And we could make the time text bold, so it is clear it is running. It's possible to do this in code, but There are other visual differences between the two states. When a stopwatch is running it should have a green background and bold text.
```{.textual path="docs/examples/introduction/stopwatch04.py" title="stopwatch04.py" press="tab,enter"} ```{.textual path="docs/examples/introduction/stopwatch04.py" title="stopwatch04.py" press="tab,enter"}

View File

@@ -57,6 +57,7 @@ from .drivers.headless_driver import HeadlessDriver
from .features import FeatureFlag, parse_features from .features import FeatureFlag, parse_features
from .file_monitor import FileMonitor from .file_monitor import FileMonitor
from .geometry import Offset, Region, Size from .geometry import Offset, Region, Size
from .messages import CallbackType
from .reactive import Reactive from .reactive import Reactive
from .renderables.blank import Blank from .renderables.blank import Blank
from .screen import Screen from .screen import Screen
@@ -560,6 +561,7 @@ class App(Generic[ReturnType], DOMNode):
quit_after (float | None, optional): Quit after a given number of seconds, or None quit_after (float | None, optional): Quit after a given number of seconds, or None
to run forever. Defaults to None. to run forever. Defaults to None.
headless (bool, optional): Run in "headless" mode (don't write to stdout). headless (bool, optional): Run in "headless" mode (don't write to stdout).
press (str, optional): An iterable of keys to simulate being pressed.
Returns: Returns:
ReturnType | None: The return value specified in `App.exit` or None if exit wasn't called. ReturnType | None: The return value specified in `App.exit` or None if exit wasn't called.
@@ -574,18 +576,25 @@ class App(Generic[ReturnType], DOMNode):
if quit_after is not None: if quit_after is not None:
self.set_timer(quit_after, self.shutdown) self.set_timer(quit_after, self.shutdown)
if press is not None: if press is not None:
app = self
async def press_keys(app: App): async def press_keys() -> None:
"""A task to send key events."""
assert press assert press
await asyncio.sleep(0.05) driver = app._driver
assert driver is not None
for key in press: for key in press:
print(f"press {key!r}") print(f"press {key!r}")
await app.post_message(events.Key(self, key)) driver.send_event(events.Key(self, key))
await asyncio.sleep(0.01) await asyncio.sleep(0.02)
self.call_later(lambda: asyncio.create_task(press_keys(self))) async def press_keys_task():
"""Press some keys in the background."""
asyncio.create_task(press_keys())
await self.process_messages() await self.process_messages(ready_callback=press_keys_task)
else:
await self.process_messages()
if _ASYNCIO_GET_EVENT_LOOP_IS_DEPRECATED: if _ASYNCIO_GET_EVENT_LOOP_IS_DEPRECATED:
# N.B. This doesn't work with Python<3.10, as we end up with 2 event loops: # N.B. This doesn't work with Python<3.10, as we end up with 2 event loops:
@@ -933,7 +942,9 @@ class App(Generic[ReturnType], DOMNode):
self.error_console.print(renderable) self.error_console.print(renderable)
self._exit_renderables.clear() self._exit_renderables.clear()
async def process_messages(self) -> None: async def process_messages(
self, ready_callback: CallbackType | None = None
) -> None:
self._set_active() self._set_active()
if self.devtools_enabled: if self.devtools_enabled:
@@ -975,6 +986,8 @@ class App(Generic[ReturnType], DOMNode):
self.refresh() self.refresh()
await self.animator.start() await self.animator.start()
await self._ready() await self._ready()
if ready_callback is not None:
await ready_callback()
await process_messages() await process_messages()
await self.animator.stop() await self.animator.stop()
await self.close_all() await self.close_all()
@@ -1210,11 +1223,14 @@ class App(Generic[ReturnType], DOMNode):
Returns: Returns:
bool: True if the key was handled by a binding, otherwise False bool: True if the key was handled by a binding, otherwise False
""" """
print("press", key)
try: try:
binding = self.bindings.get_key(key) binding = self.bindings.get_key(key)
except NoBinding: except NoBinding:
print("no binding")
return False return False
else: else:
print(binding)
await self.action(binding.action) await self.action(binding.action)
return True return True

View File

@@ -116,7 +116,8 @@ def import_app(import_name: str) -> App:
@run.command("run") @run.command("run")
@click.argument("import_name", metavar="FILE or FILE:APP") @click.argument("import_name", metavar="FILE or FILE:APP")
@click.option("--dev", "dev", help="Enable development mode", is_flag=True) @click.option("--dev", "dev", help="Enable development mode", is_flag=True)
def run_app(import_name: str, dev: bool) -> None: @click.option("--press", "press", help="Comma separated keys to simulate press")
def run_app(import_name: str, dev: bool, press: str) -> None:
"""Run a Textual app. """Run a Textual app.
The code to run may be given as a path (ending with .py) or as a Python The code to run may be given as a path (ending with .py) or as a Python
@@ -156,7 +157,8 @@ def run_app(import_name: str, dev: bool) -> None:
console.print(str(error)) console.print(str(error))
sys.exit(1) sys.exit(1)
app.run() press_keys = press.split(",") if press else None
app.run(press=press_keys)
@run.command("borders") @run.command("borders")

View File

@@ -228,7 +228,7 @@ class DOMNode(MessagePump):
while node and not isinstance(node, Screen): while node and not isinstance(node, Screen):
node = node._parent node = node._parent
if not isinstance(node, Screen): if not isinstance(node, Screen):
raise NoScreen("{self} has no screen") raise NoScreen(f"{self} has no screen")
return node return node
@property @property

View File

@@ -1,5 +1,6 @@
from __future__ import annotations from __future__ import annotations
from asyncio import Lock
from itertools import islice from itertools import islice
from fractions import Fraction from fractions import Fraction
from operator import attrgetter from operator import attrgetter
@@ -124,6 +125,8 @@ class Widget(DOMNode):
self._styles_cache = StylesCache() self._styles_cache = StylesCache()
self._lock = Lock()
super().__init__( super().__init__(
name=name, name=name,
id=id, id=id,
@@ -1159,13 +1162,18 @@ class Widget(DOMNode):
Args: Args:
event (events.Idle): Idle event. event (events.Idle): Idle event.
""" """
if self._parent is not None: if self._parent is not None and not self._closing:
if self._repaint_required: try:
self._repaint_required = False screen = self.screen
self.screen.post_message_no_wait(messages.Update(self, self)) except NoScreen:
if self._layout_required: pass
self._layout_required = False else:
self.screen.post_message_no_wait(messages.Layout(self)) if self._repaint_required:
self._repaint_required = False
screen.post_message_no_wait(messages.Update(self, self))
if self._layout_required:
self._layout_required = False
screen.post_message_no_wait(messages.Layout(self))
def focus(self) -> None: def focus(self) -> None:
"""Give input focus to this widget.""" """Give input focus to this widget."""