mirror of
https://github.com/Textualize/textual.git
synced 2025-10-17 02:38:12 +03:00
added press to run
This commit is contained in:
@@ -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"}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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."""
|
||||||
|
|||||||
Reference in New Issue
Block a user