diff --git a/pyproject.toml b/pyproject.toml index a42acfe17..6bc416160 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -17,6 +17,9 @@ classifiers = [ "Programming Language :: Python :: 3.10", ] +[tool.poetry.scripts] +textual = "textual.cli.cli:run" + [tool.poetry.dependencies] python = "^3.7" rich = "^12.0.0" diff --git a/src/textual/__init__.py b/src/textual/__init__.py index e78bb2009..f85e41f2c 100644 --- a/src/textual/__init__.py +++ b/src/textual/__init__.py @@ -3,6 +3,7 @@ import inspect from rich.console import RenderableType __all__ = ["log", "panic"] +__version__ = "0.1.15" def log(*args: object, verbosity: int = 0, **kwargs) -> None: diff --git a/src/textual/cli/cli.py b/src/textual/cli/cli.py new file mode 100644 index 000000000..046363059 --- /dev/null +++ b/src/textual/cli/cli.py @@ -0,0 +1,13 @@ +import click + +from textual.devtools.server import _run_devtools + + +@click.group() +def run(): + pass + + +@run.command(help="Run the Textual Devtools console") +def console(): + _run_devtools() diff --git a/src/textual/devtools/renderables.py b/src/textual/devtools/renderables.py index 77cc5df0b..abbbdf9e6 100644 --- a/src/textual/devtools/renderables.py +++ b/src/textual/devtools/renderables.py @@ -5,9 +5,12 @@ from datetime import datetime, timezone from pathlib import Path from typing import Iterable +from rich.containers import Renderables from rich.style import Style from rich.text import Text +import textual + if sys.version_info >= (3, 8): from typing import Literal else: @@ -21,10 +24,31 @@ from rich.rule import Rule from rich.segment import Segment, Segments from rich.table import Table -DevtoolsMessageLevel = Literal["info", "warning", "error"] +DevConsoleMessageLevel = Literal["info", "warning", "error"] -class DevtoolsLogMessage: +class DevConsoleHeader: + def __rich_console__( + self, console: Console, options: ConsoleOptions + ) -> RenderResult: + lines = Renderables( + [ + f"[bold]Textual Development Console [#b169dd]v{textual.__version__}", + "[#967fa3]Run a Textual app with the environment variable [b]TEXTUAL_DEVTOOLS=1[/] to connect.", + "[#967fa3]Press [b]Ctrl+C[/] to quit.", + ] + ) + render_options = options.update(width=options.max_width - 4) + lines = console.render_lines(lines, render_options) + new_line = Segment("\n") + padding = Segment("▌", Style.parse("#b169dd")) + for line in lines: + yield padding + yield from line + yield new_line + + +class DevConsoleLog: """Renderable representing a single log message Args: @@ -71,8 +95,8 @@ class DevtoolsLogMessage: yield Segments(self.segments) -class DevtoolsInternalMessage: - """Renderable for messages written by the devtools server itself +class DevConsoleNotice: + """Renderable for messages written by the devtools console itself Args: message (str): The message to display @@ -80,7 +104,7 @@ class DevtoolsInternalMessage: Determines colors used to render the message and the perceived importance. """ - def __init__(self, message: str, *, level: DevtoolsMessageLevel = "info") -> None: + def __init__(self, message: str, *, level: DevConsoleMessageLevel = "info") -> None: self.message = message self.level = level diff --git a/src/textual/devtools/server.py b/src/textual/devtools/server.py index 011183318..a0ad3a149 100644 --- a/src/textual/devtools/server.py +++ b/src/textual/devtools/server.py @@ -38,7 +38,11 @@ async def _on_startup(app: Application) -> None: def _run_devtools() -> None: app = _make_devtools_aiohttp_app() - run_app(app, port=DEVTOOLS_PORT) + + def noop_print(_: str): + return None + + run_app(app, port=DEVTOOLS_PORT, print=noop_print) def _make_devtools_aiohttp_app( diff --git a/src/textual/devtools/service.py b/src/textual/devtools/service.py index 21f136005..d396fc49d 100644 --- a/src/textual/devtools/service.py +++ b/src/textual/devtools/service.py @@ -14,7 +14,11 @@ from aiohttp.web_ws import WebSocketResponse from rich.console import Console from rich.markup import escape -from textual.devtools.renderables import DevtoolsLogMessage, DevtoolsInternalMessage +from textual.devtools.renderables import ( + DevConsoleLog, + DevConsoleNotice, + DevConsoleHeader, +) QUEUEABLE_TYPES = {"client_log", "client_spillover"} @@ -38,6 +42,7 @@ class DevtoolsService: async def start(self): """Starts devtools tasks""" self.size_poll_task = asyncio.create_task(self._console_size_poller()) + self.console.print(DevConsoleHeader()) @property def clients_connected(self) -> bool: @@ -167,7 +172,7 @@ class ClientHandler: decoded_segments = base64.b64decode(encoded_segments) segments = pickle.loads(decoded_segments) self.service.console.print( - DevtoolsLogMessage( + DevConsoleLog( segments=segments, path=path, line_number=line_number, @@ -176,7 +181,7 @@ class ClientHandler: ) elif type == "client_spillover": spillover = int(message_json["payload"]["spillover"]) - info_renderable = DevtoolsInternalMessage( + info_renderable = DevConsoleNotice( f"Discarded {spillover} messages", level="warning" ) self.service.console.print(info_renderable) @@ -198,9 +203,7 @@ class ClientHandler: if self.request.remote: self.service.console.print( - DevtoolsInternalMessage( - f"Client '{escape(self.request.remote)}' connected" - ) + DevConsoleNotice(f"Client '{escape(self.request.remote)}' connected") ) try: await self.service.send_server_info(client_handler=self) @@ -223,20 +226,16 @@ class ClientHandler: await self.incoming_queue.put(message_json) elif message.type == WSMsgType.ERROR: self.service.console.print( - DevtoolsInternalMessage( - "Websocket error occurred", level="error" - ) + DevConsoleNotice("Websocket error occurred", level="error") ) break except Exception as error: - self.service.console.print( - DevtoolsInternalMessage(str(error), level="error") - ) + self.service.console.print(DevConsoleNotice(str(error), level="error")) finally: if self.request.remote: self.service.console.print( "\n", - DevtoolsInternalMessage( + DevConsoleNotice( f"Client '{escape(self.request.remote)}' disconnected" ), ) diff --git a/tests/devtools/test_devtools.py b/tests/devtools/test_devtools.py index c748da9e6..052871f30 100644 --- a/tests/devtools/test_devtools.py +++ b/tests/devtools/test_devtools.py @@ -7,7 +7,7 @@ from rich.console import Console from rich.segment import Segment from tests.utilities.render import wait_for_predicate -from textual.devtools.renderables import DevtoolsLogMessage, DevtoolsInternalMessage +from textual.devtools.renderables import DevConsoleLog, DevConsoleNotice TIMESTAMP = 1649166819 WIDTH = 40 @@ -31,7 +31,7 @@ def console(): @time_machine.travel(TIMESTAMP) def test_log_message_render(console): - message = DevtoolsLogMessage( + message = DevConsoleLog( [Segment("content")], path="abc/hello.py", line_number=123, @@ -62,7 +62,7 @@ def test_log_message_render(console): def test_internal_message_render(console): - message = DevtoolsInternalMessage("hello") + message = DevConsoleNotice("hello") rule = next(iter(message.__rich_console__(console, console.options))) assert rule.title == "hello" assert rule.characters == "─"