mirror of
https://github.com/Textualize/textual.git
synced 2025-10-17 02:38:12 +03:00
Merge branch 'main' into expose-node-tree
This commit is contained in:
@@ -11,6 +11,13 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
|
|||||||
|
|
||||||
- Added `TreeNode.tree` as a read-only public attribute https://github.com/Textualize/textual/issues/2413
|
- Added `TreeNode.tree` as a read-only public attribute https://github.com/Textualize/textual/issues/2413
|
||||||
|
|
||||||
|
## [0.22.1] - 2023-04-28
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fixed timer issue https://github.com/Textualize/textual/issues/2416
|
||||||
|
- Fixed `textual run` issue https://github.com/Textualize/textual/issues/2391
|
||||||
|
|
||||||
## [0.22.0] - 2023-04-27
|
## [0.22.0] - 2023-04-27
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
|
|
||||||
Textual comes with a command line application of the same name. The `textual` command is a super useful tool that will help you to build apps.
|
Textual comes with a command line application of the same name. The `textual` command is a super useful tool that will help you to build apps.
|
||||||
|
|
||||||
Take a moment to look through the available sub-commands. There will be even more helpful tools here in the future.
|
Take a moment to look through the available subcommands. There will be even more helpful tools here in the future.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
textual --help
|
textual --help
|
||||||
@@ -17,25 +17,48 @@ textual --help
|
|||||||
|
|
||||||
## Run
|
## Run
|
||||||
|
|
||||||
You can run Textual apps with the `run` subcommand. If you supply a path to a Python file it will load and run the application.
|
The `run` sub-command runs Textual apps. If you supply a path to a Python file it will load and run the app.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
textual run my_app.py
|
textual run my_app.py
|
||||||
```
|
```
|
||||||
|
|
||||||
The `run` sub-command will first look for a `App` instance called `app` in the global scope of your Python file. If there is no `app`, it will create an instance of the first `App` class it finds and run that.
|
This is equivalent to running `python my_app.py` from the command prompt, but will allow you to set various switches which can help you debug, such as `--dev` which enable the [Console](#console).
|
||||||
|
|
||||||
Alternatively, you can add the name of an `App` instance or class after a colon to run a specific app in the Python file. Here's an example:
|
See the `run` subcommand's help for details:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
textual run my_app.py:alternative_app
|
textual run --help
|
||||||
|
```
|
||||||
|
|
||||||
|
You can also run Textual apps from a python import.
|
||||||
|
The following command would import `music.play` and run a Textual app in that module:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
textual run music.play
|
||||||
|
```
|
||||||
|
|
||||||
|
This assumes you have a Textual app instance called `app` in `music.play`.
|
||||||
|
If your app has a different name, you can append it after a colon:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
textual run music.play:MusicPlayerApp
|
||||||
```
|
```
|
||||||
|
|
||||||
!!! note
|
!!! note
|
||||||
|
|
||||||
If the Python file contains a call to app.run() then you can launch the file as you normally would any other Python program. Running your app via `textual run` will give you access to a few Textual features such as live editing of CSS files.
|
This works for both Textual app *instances* and *classes*.
|
||||||
|
|
||||||
|
|
||||||
|
### Running from commands
|
||||||
|
|
||||||
|
If your app is installed as a command line script, you can use the `-c` switch to run it.
|
||||||
|
For instance, the following will run the `textual colors` command:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
textual run -c textual colors
|
||||||
|
```
|
||||||
|
|
||||||
## Live editing
|
## Live editing
|
||||||
|
|
||||||
If you combine the `run` command with the `--dev` switch your app will run in *development mode*.
|
If you combine the `run` command with the `--dev` switch your app will run in *development mode*.
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[tool.poetry]
|
[tool.poetry]
|
||||||
name = "textual"
|
name = "textual"
|
||||||
version = "0.22.0"
|
version = "0.22.1"
|
||||||
homepage = "https://github.com/Textualize/textual"
|
homepage = "https://github.com/Textualize/textual"
|
||||||
description = "Modern Text User Interface framework"
|
description = "Modern Text User Interface framework"
|
||||||
authors = ["Will McGugan <will@textualize.io>"]
|
authors = ["Will McGugan <will@textualize.io>"]
|
||||||
|
|||||||
@@ -17,7 +17,11 @@ from typing import NoReturn, Sequence
|
|||||||
EXEC_SCRIPT = Template(
|
EXEC_SCRIPT = Template(
|
||||||
"""\
|
"""\
|
||||||
from textual.app import App
|
from textual.app import App
|
||||||
from $MODULE import $APP as app;
|
try:
|
||||||
|
from $MODULE import $APP as app;
|
||||||
|
except ImportError:
|
||||||
|
raise SystemExit("Unable to import '$APP' from module '$MODULE'") from None
|
||||||
|
|
||||||
if isinstance(app, App):
|
if isinstance(app, App):
|
||||||
# If we imported an app, run it
|
# If we imported an app, run it
|
||||||
app.run()
|
app.run()
|
||||||
@@ -111,25 +115,6 @@ def exec_command(
|
|||||||
os.execvpe(command, [command, *args], environment)
|
os.execvpe(command, [command, *args], environment)
|
||||||
|
|
||||||
|
|
||||||
def check_import(module_name: str, app_name: str) -> bool:
|
|
||||||
"""Check if a symbol can be imported.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
module_name: Name of the module
|
|
||||||
app_name: Name of the app.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
True if the app may be imported from the module.
|
|
||||||
"""
|
|
||||||
|
|
||||||
try:
|
|
||||||
sys.path.insert(0, "")
|
|
||||||
module = importlib.import_module(module_name)
|
|
||||||
except ImportError as error:
|
|
||||||
return False
|
|
||||||
return hasattr(module, app_name)
|
|
||||||
|
|
||||||
|
|
||||||
def exec_import(
|
def exec_import(
|
||||||
import_name: str, args: Sequence[str], environment: dict[str, str]
|
import_name: str, args: Sequence[str], environment: dict[str, str]
|
||||||
) -> NoReturn:
|
) -> NoReturn:
|
||||||
@@ -147,9 +132,6 @@ def exec_import(
|
|||||||
module, _colon, app = import_name.partition(":")
|
module, _colon, app = import_name.partition(":")
|
||||||
app = app or "app"
|
app = app or "app"
|
||||||
|
|
||||||
if not check_import(module, app):
|
|
||||||
raise ExecImportError(f"Unable to import {app!r} from {import_name!r}")
|
|
||||||
|
|
||||||
script = EXEC_SCRIPT.substitute(MODULE=module, APP=app)
|
script = EXEC_SCRIPT.substitute(MODULE=module, APP=app)
|
||||||
# Compiling the script will raise a SyntaxError if there are any invalid symbols
|
# Compiling the script will raise a SyntaxError if there are any invalid symbols
|
||||||
compile(script, "textual-exec", "exec")
|
compile(script, "textual-exec", "exec")
|
||||||
|
|||||||
@@ -147,16 +147,16 @@ def _run_app(
|
|||||||
) -> None:
|
) -> 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 app to run may be given as a path (ending with .py) which will be equivalent to running the
|
||||||
import, which will load the code and run an app called "app". You may optionally
|
script with python, or as a Python import which will import and run an app called "app".
|
||||||
add a colon plus the class or class instance you want to run.
|
|
||||||
|
In the case of an import, you can import and run an alternative app by appending a colon followed
|
||||||
|
by the name of the app instance or class.
|
||||||
|
|
||||||
Here are some examples:
|
Here are some examples:
|
||||||
|
|
||||||
textual run foo.py
|
textual run foo.py
|
||||||
|
|
||||||
textual run foo.py:MyApp
|
|
||||||
|
|
||||||
textual run module.foo
|
textual run module.foo
|
||||||
|
|
||||||
textual run module.foo:MyApp
|
textual run module.foo:MyApp
|
||||||
|
|||||||
@@ -131,16 +131,16 @@ class Timer:
|
|||||||
wait_time = max(0, next_timer - now)
|
wait_time = max(0, next_timer - now)
|
||||||
await sleep(wait_time)
|
await sleep(wait_time)
|
||||||
count += 1
|
count += 1
|
||||||
try:
|
|
||||||
await self._tick(next_timer=next_timer, count=count)
|
|
||||||
except EventTargetGone:
|
|
||||||
break
|
|
||||||
await self._active.wait()
|
await self._active.wait()
|
||||||
if self._reset:
|
if self._reset:
|
||||||
start = _time.get_time()
|
start = _time.get_time()
|
||||||
count = 0
|
count = 0
|
||||||
self._reset = False
|
self._reset = False
|
||||||
continue
|
continue
|
||||||
|
try:
|
||||||
|
await self._tick(next_timer=next_timer, count=count)
|
||||||
|
except EventTargetGone:
|
||||||
|
break
|
||||||
|
|
||||||
async def _tick(self, *, next_timer: float, count: int) -> None:
|
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"""
|
"""Triggers the Timer's action: either call its callback, or sends an event to its target"""
|
||||||
|
|||||||
@@ -404,7 +404,9 @@ class Tabs(Widget, can_focus=True):
|
|||||||
active_tab = self.query_one(f"#tabs-list > #{active}", Tab)
|
active_tab = self.query_one(f"#tabs-list > #{active}", Tab)
|
||||||
self.query("#tabs-list > Tab.-active").remove_class("-active")
|
self.query("#tabs-list > Tab.-active").remove_class("-active")
|
||||||
active_tab.add_class("-active")
|
active_tab.add_class("-active")
|
||||||
self._highlight_active(animate=previously_active != "")
|
self.call_after_refresh(
|
||||||
|
self._highlight_active, animate=previously_active != ""
|
||||||
|
)
|
||||||
self.post_message(self.TabActivated(self, active_tab))
|
self.post_message(self.TabActivated(self, active_tab))
|
||||||
else:
|
else:
|
||||||
underline = self.query_one(Underline)
|
underline = self.query_one(Underline)
|
||||||
|
|||||||
Reference in New Issue
Block a user