diff --git a/CHANGELOG.md b/CHANGELOG.md index fb1ad4ed9..154c41a7b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,17 +9,20 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ### Changed -- Breaking change: standard keyboard scrollable navigation bindings have been moved off `Widget` and onto a new base class for scrollable containers (see also below addition) https://github.com/Textualize/textual/issues/2332 -- `ScrollView` now inherits from `ScrollableContainer` rather than `Widget` https://github.com/Textualize/textual/issues/2332 -- Containers no longer inherit any bindings from `Widget` https://github.com/Textualize/textual/issues/2331 +- `textual run` execs apps in a new context. ### Added +- Added `-c` switch to `textual run` which runs commands in a Textual dev environment. +- Breaking change: standard keyboard scrollable navigation bindings have been moved off `Widget` and onto a new base class for scrollable containers (see also below addition) https://github.com/Textualize/textual/issues/2332 +- `ScrollView` now inherits from `ScrollableContainer` rather than `Widget` https://github.com/Textualize/textual/issues/2332 +- Containers no longer inherit any bindings from `Widget` https://github.com/Textualize/textual/issues/2331 - Added `ScrollableContainer`; a container class that binds the common navigation keys to scroll actions (see also above breaking change) https://github.com/Textualize/textual/issues/2332 ### Fixed - Fixed dark mode toggles in a "child" screen not updating a "parent" screen https://github.com/Textualize/textual/issues/1999 +- Fixed "panel" border not exposed via CSS ## [0.20.1] - 2023-04-18 diff --git a/src/textual/app.py b/src/textual/app.py index 77311a522..5efe4e380 100644 --- a/src/textual/app.py +++ b/src/textual/app.py @@ -1048,9 +1048,19 @@ class App(Generic[ReturnType], DOMNode): auto_pilot_task: Task | None = None + if auto_pilot is None and constants.PRESS: + keys = constants.PRESS.split(",") + + async def press_keys(pilot: Pilot) -> None: + """Auto press keys.""" + await pilot.press(*keys) + + auto_pilot = press_keys + async def app_ready() -> None: """Called by the message loop when the app is ready.""" nonlocal auto_pilot_task + if auto_pilot is not None: async def run_auto_pilot( @@ -1762,23 +1772,16 @@ class App(Generic[ReturnType], DOMNode): May be used as a hook for any operations that should run first. """ - try: - screenshot_timer = float(os.environ.get("TEXTUAL_SCREENSHOT", "0")) - except ValueError: - return - screenshot_title = os.environ.get("TEXTUAL_SCREENSHOT_TITLE") - - if not screenshot_timer: - return - - async def on_screenshot(): - """Used by docs plugin.""" - svg = self.export_screenshot(title=screenshot_title) - self._screenshot = svg # type: ignore + async def take_screenshot() -> None: + """Take a screenshot and exit.""" + self.save_screenshot() self.exit() - self.set_timer(screenshot_timer, on_screenshot, name="screenshot timer") + if constants.SCREENSHOT_DELAY >= 0: + self.set_timer( + constants.SCREENSHOT_DELAY, take_screenshot, name="screenshot timer" + ) async def _on_compose(self) -> None: try: @@ -1966,6 +1969,14 @@ class App(Generic[ReturnType], DOMNode): self._print_error_renderables() + if constants.SHOW_RETURN: + from rich.console import Console + from rich.pretty import Pretty + + console = Console() + console.print("[b]The app returned:") + console.print(Pretty(self._return_value)) + async def _on_exit_app(self) -> None: self._begin_batch() # Prevent repaint / layout while shutting down await self._message_queue.put(None) diff --git a/src/textual/cli/_run.py b/src/textual/cli/_run.py new file mode 100644 index 000000000..b00818e85 --- /dev/null +++ b/src/textual/cli/_run.py @@ -0,0 +1,163 @@ +""" + +Functions to run Textual apps with an updated environment. + +Note that these methods will execute apps in a new process, and abandon the current process. +This means that (if they succeed) they will never return. + +""" + +from __future__ import annotations + +import importlib +import os +import platform +import shlex +import sys +from string import Template +from typing import NoReturn + +WINDOWS = platform.system() == "Windows" +"""True if we're running on Windows.""" + +EXEC_SCRIPT = Template( + """\ +from textual.app import App +from $MODULE import $APP as app; +if isinstance(app, App): + # If we imported an app, run it + app.run() +else: + # Otherwise it is assumed to be a class + app().run() +""" +) +"""A template script to import and run an app.""" + + +class ExecImportError(Exception): + """Raised if a Python import is invalid.""" + + +def run_app(command_args: str, environment: dict[str, str] | None = None) -> None: + """Run a textual app. + + Note: + The current process is abandoned. + + Args: + command_args: Arguments to pass to the Textual app. + environment: Environment variables, or None to use current process. + """ + import_name, *args = shlex.split(command_args, posix=not WINDOWS) + if environment is None: + app_environment = dict(os.environ) + else: + app_environment = environment + + if _is_python_path(import_name): + # If it is a Python path we can exec it now + exec_python([import_name, *args], app_environment) + + else: + # Otherwise it is assumed to be a Python import + try: + exec_import(import_name, args, app_environment) + except (SyntaxError, ExecImportError): + print(f"Unable to import Textual app from {import_name!r}") + + +def _is_python_path(path: str) -> bool: + """Does the given file look like it's run with Python? + + Args: + path: The path to check. + + Returns: + True if the path references Python code, False it it doesn't. + """ + if path.endswith(".py"): + return True + try: + with open(path, "r") as source: + first_line = source.readline() + except IOError: + return False + return first_line.startswith("#!") and "py" in first_line + + +def _flush() -> None: + """Flush output before exec.""" + sys.stderr.flush() + sys.stdout.flush() + + +def exec_python(args: list[str], environment: dict[str, str]) -> NoReturn: + """Execute a Python script. + + Args: + args: Additional arguments. + environment: Environment variables. + + """ + _flush() + os.execvpe(sys.executable, ["python", *args], environment) + + +def exec_command(command: str, environment: dict[str, str]) -> NoReturn: + """Execute a command with the given environment. + + Args: + command: Command to execute. + environment: Environment variables. + """ + _flush() + command, *args = shlex.split(command, posix=not WINDOWS) + 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( + import_name: str, args: list[str], environment: dict[str, str] +) -> NoReturn: + """Import and execute an app. + + Raises: + SyntaxError: If any imports are not valid Python symbols. + ExecImportError: If the module could not be imported. + + Args: + import_name: The Python import. + args: Command line arguments. + environment: Environment variables. + + """ + module, _colon, app = import_name.partition(":") + 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) + # Compiling the script will raise a SyntaxError if there are any invalid symbols + compile(script, "textual-exec", "exec") + _flush() + exec_python(["-c", script, *args], environment) diff --git a/src/textual/cli/cli.py b/src/textual/cli/cli.py index c1186f99d..9ecff8573 100644 --- a/src/textual/cli/cli.py +++ b/src/textual/cli/cli.py @@ -2,7 +2,8 @@ from __future__ import annotations import sys -from ..constants import DEFAULT_DEVTOOLS_PORT, DEVTOOLS_PORT_ENVIRON_VARIABLE +from ..constants import DEVTOOLS_PORT +from ._run import exec_command, run_app try: import click @@ -12,9 +13,6 @@ except ImportError: from importlib_metadata import version -from textual._import_app import AppFail, import_app -from textual.pilot import Pilot - @click.group() @click.version_option(version("textual")) @@ -29,31 +27,27 @@ def run(): type=int, default=None, metavar="PORT", - help=f"Port to use for the development mode console. Defaults to {DEFAULT_DEVTOOLS_PORT}.", + help=f"Port to use for the development mode console. Defaults to {DEVTOOLS_PORT}.", ) @click.option("-v", "verbose", help="Enable verbose logs.", is_flag=True) @click.option("-x", "--exclude", "exclude", help="Exclude log group(s)", multiple=True) def console(port: int | None, verbose: bool, exclude: list[str]) -> None: """Launch the textual console.""" - import os from rich.console import Console from textual.devtools.server import _run_devtools - if port is not None: - os.environ[DEVTOOLS_PORT_ENVIRON_VARIABLE] = str(port) - console = Console() console.clear() console.show_cursor(False) try: - _run_devtools(verbose=verbose, exclude=exclude) + _run_devtools(verbose=verbose, exclude=exclude, port=port) finally: console.show_cursor(True) -def _post_run_warnings() -> None: +def _pre_run_warnings() -> None: """Look for and report any issues with the environment. This is the right place to add code that looks at the terminal, or other @@ -73,8 +67,10 @@ def _post_run_warnings() -> None: # second item is a message to show the user on exit from `textual run`. warnings = [ ( - platform.system() == "Darwin" - and os.environ.get("TERM_PROGRAM") == "Apple_Terminal", + ( + platform.system() == "Darwin" + and os.environ.get("TERM_PROGRAM") == "Apple_Terminal" + ), "The default terminal app on macOS is limited to 256 colors. See our FAQ for more details:\n\n" "https://github.com/Textualize/textual/blob/main/FAQ.md#why-doesn't-textual-look-good-on-macos", ) @@ -82,7 +78,11 @@ def _post_run_warnings() -> None: for concerning, concern in warnings: if concerning: - console.print(Panel.fit(f"⚠️ [bold green] {concern}[/]", style="cyan")) + console.print( + Panel.fit( + f"⚠️ {concern}", style="yellow", border_style="red", padding=(1, 2) + ) + ) @run.command( @@ -92,25 +92,51 @@ def _post_run_warnings() -> None: }, ) @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) @click.option( "--port", "port", type=int, default=None, metavar="PORT", - help=f"Port to use for the development mode console. Defaults to {DEFAULT_DEVTOOLS_PORT}.", + help=f"Port to use for the development mode console. Defaults to {DEVTOOLS_PORT}.", +) +@click.option( + "--press", "press", default=None, help="Comma separated keys to simulate press." ) -@click.option("--press", "press", help="Comma separated keys to simulate press") @click.option( "--screenshot", type=int, default=None, metavar="DELAY", - help="Take screenshot after DELAY seconds", + help="Take screenshot after DELAY seconds.", ) -def run_app( - import_name: str, dev: bool, port: int | None, press: str, screenshot: int | None +@click.option( + "-c", + "--command", + "command", + type=bool, + default=False, + help="Run as command rather that a file / module.", + is_flag=True, +) +@click.option( + "-r", + "--show-return", + "show_return", + type=bool, + default=False, + help="Show any return value on exit.", + is_flag=True, +) +def _run_app( + import_name: str, + dev: bool, + port: int | None, + press: str | None, + screenshot: int | None, + command: bool = False, + show_return: bool = False, ) -> None: """Run a Textual app. @@ -132,53 +158,39 @@ def run_app( in quotes: textual run "foo.py arg --option" + + Use the -c switch to run a command that launches a Textual app. + + textual run -c "textual colors" """ import os - import sys - from asyncio import sleep from textual.features import parse_features - if port is not None: - os.environ[DEVTOOLS_PORT_ENVIRON_VARIABLE] = str(port) + environment = dict(os.environ) - features = set(parse_features(os.environ.get("TEXTUAL", ""))) + features = set(parse_features(environment.get("TEXTUAL", ""))) if dev: features.add("debug") features.add("devtools") - os.environ["TEXTUAL"] = ",".join(sorted(features)) - try: - app = import_app(import_name) - except AppFail as error: - from rich.console import Console + environment["TEXTUAL"] = ",".join(sorted(features)) + if port is not None: + environment["TEXTUAL_DEVTOOLS_PORT"] = str(port) + if press is not None: + environment["TEXTUAL_PRESS"] = str(press) + if screenshot is not None: + environment["TEXTUAL_SCREENSHOT"] = str(screenshot) + if show_return: + environment["TEXTUAL_SHOW_RETURN"] = "1" - console = Console(stderr=True) - console.print(str(error)) - sys.exit(1) + _pre_run_warnings() - press_keys = press.split(",") if press else None - - async def run_press_keys(pilot: Pilot) -> None: - if press_keys is not None: - await pilot.press(*press_keys) - if screenshot is not None: - await sleep(screenshot) - filename = pilot.app.save_screenshot() - pilot.app.exit(message=f"Saved {filename!r}") - - result = app.run(auto_pilot=run_press_keys) - - if result is not None: - from rich.console import Console - from rich.pretty import Pretty - - console = Console() - console.print("[b]The app returned:") - console.print(Pretty(result)) - - _post_run_warnings() + if command: + exec_command(import_name, environment) + else: + run_app(import_name, environment) @run.command("borders") @@ -215,7 +227,7 @@ def keys(): @run.command("diagnose") def run_diagnose(): - """Print information about the Textual environment""" + """Print information about the Textual environment.""" from textual.cli.tools.diagnose import diagnose diagnose() diff --git a/src/textual/cli/previews/borders.py b/src/textual/cli/previews/borders.py index 52ce7f73d..282a42826 100644 --- a/src/textual/cli/previews/borders.py +++ b/src/textual/cli/previews/borders.py @@ -1,6 +1,6 @@ from textual.app import App, ComposeResult -from textual.constants import BORDERS from textual.containers import VerticalScroll +from textual.css.constants import VALID_BORDER from textual.widgets import Button, Label TEXT = """I must not fear. @@ -26,7 +26,7 @@ class BorderButtons(VerticalScroll): """ def compose(self) -> ComposeResult: - for border in BORDERS: + for border in sorted(VALID_BORDER): if border: yield Button(border, id=border) diff --git a/src/textual/cli/run.py b/src/textual/cli/run.py new file mode 100644 index 000000000..da3c9ea63 --- /dev/null +++ b/src/textual/cli/run.py @@ -0,0 +1,5 @@ +import sys + +from ._run import run + +run(sys.argv[1], sys.argv[1:]) diff --git a/src/textual/constants.py b/src/textual/constants.py index a29c1b312..eae559c9b 100644 --- a/src/textual/constants.py +++ b/src/textual/constants.py @@ -9,10 +9,6 @@ import os from typing_extensions import Final -from ._border import BORDER_CHARS - -__all__ = ["BORDERS"] - get_environ = os.environ.get @@ -42,16 +38,18 @@ def get_environ_int(name: str, default: int) -> int: or the default value otherwise. """ try: - return int(get_environ(name, default)) + return int(os.environ[name]) + except KeyError: + return default except ValueError: return default -BORDERS = list(BORDER_CHARS) - DEBUG: Final[bool] = get_environ_bool("TEXTUAL_DEBUG") +"""Enable debug mode.""" DRIVER: Final[str | None] = get_environ("TEXTUAL_DRIVER", None) +"""Import for replacement driver.""" FILTERS: Final[str] = get_environ("TEXTUAL_FILTERS", "") """A list of filters to apply to renderables.""" @@ -59,12 +57,14 @@ FILTERS: Final[str] = get_environ("TEXTUAL_FILTERS", "") LOG_FILE: Final[str | None] = get_environ("TEXTUAL_LOG", None) """A last resort log file that appends all logs, when devtools isn't working.""" - -DEVTOOLS_PORT_ENVIRON_VARIABLE: Final[str] = "TEXTUAL_CONSOLE_PORT" -"""The name of the environment variable that sets the port for the devtools.""" -DEFAULT_DEVTOOLS_PORT: Final[int] = 8081 -"""The default port to use for the devtools.""" -DEVTOOLS_PORT: Final[int] = get_environ_int( - DEVTOOLS_PORT_ENVIRON_VARIABLE, DEFAULT_DEVTOOLS_PORT -) +DEVTOOLS_PORT: Final[int] = get_environ_int("TEXTUAL_DEVTOOLS_PORT", 8081) """Constant with the port that the devtools will connect to.""" + +SCREENSHOT_DELAY: Final[int] = get_environ_int("TEXTUAL_SCREENSHOT", -1) +"""Seconds delay before taking screenshot.""" + +PRESS: Final[str] = get_environ("TEXTUAL_PRESS", "") +"""Keys to automatically press.""" + +SHOW_RETURN: Final[bool] = get_environ_bool("TEXTUAL_SHOW_RETURN") +"""Write the return value on exit.""" diff --git a/src/textual/css/constants.py b/src/textual/css/constants.py index d89129daf..84f874813 100644 --- a/src/textual/css/constants.py +++ b/src/textual/css/constants.py @@ -20,6 +20,7 @@ VALID_BORDER: Final = { "inner", "none", "outer", + "panel", "round", "solid", "tall", diff --git a/src/textual/devtools/server.py b/src/textual/devtools/server.py index 4e7f66814..11f631f00 100644 --- a/src/textual/devtools/server.py +++ b/src/textual/devtools/server.py @@ -8,8 +8,9 @@ from aiohttp.web_request import Request from aiohttp.web_routedef import get from aiohttp.web_ws import WebSocketResponse -from textual.devtools.client import DEVTOOLS_PORT -from textual.devtools.service import DevtoolsService +from ..constants import DEVTOOLS_PORT +from .client import DEVTOOLS_PORT +from .service import DevtoolsService DEFAULT_SIZE_CHANGE_POLL_DELAY_SECONDS = 2 @@ -38,7 +39,9 @@ async def _on_startup(app: Application) -> None: await service.start() -def _run_devtools(verbose: bool, exclude: list[str] | None = None) -> None: +def _run_devtools( + verbose: bool, exclude: list[str] | None = None, port: int | None = None +) -> None: app = _make_devtools_aiohttp_app(verbose=verbose, exclude=exclude) def noop_print(_: str) -> None: @@ -47,7 +50,7 @@ def _run_devtools(verbose: bool, exclude: list[str] | None = None) -> None: try: run_app( app, - port=DEVTOOLS_PORT, + port=DEVTOOLS_PORT if port is None else port, print=noop_print, loop=asyncio.get_event_loop(), ) diff --git a/tests/devtools/test_devtools_client.py b/tests/devtools/test_devtools_client.py index b67f558ba..cc5231de7 100644 --- a/tests/devtools/test_devtools_client.py +++ b/tests/devtools/test_devtools_client.py @@ -10,7 +10,7 @@ from rich.console import ConsoleDimensions from rich.panel import Panel from tests.utilities.render import wait_for_predicate -from textual.constants import DEFAULT_DEVTOOLS_PORT +from textual.constants import DEVTOOLS_PORT from textual.devtools.client import DevtoolsClient from textual.devtools.redirect_output import DevtoolsLog @@ -22,7 +22,7 @@ TIMESTAMP = 1649166819 def test_devtools_client_initialize_defaults(): devtools = DevtoolsClient() - assert devtools.url == f"ws://127.0.0.1:{DEFAULT_DEVTOOLS_PORT}" + assert devtools.url == f"ws://127.0.0.1:{DEVTOOLS_PORT}" async def test_devtools_client_is_connected(devtools): diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots.ambr b/tests/snapshot_tests/__snapshots__/test_snapshots.ambr index c80298cc6..1fe2e6519 100644 --- a/tests/snapshot_tests/__snapshots__/test_snapshots.ambr +++ b/tests/snapshot_tests/__snapshots__/test_snapshots.ambr @@ -893,141 +893,141 @@ font-weight: 700; } - .terminal-4039043553-matrix { + .terminal-1095690712-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-4039043553-title { + .terminal-1095690712-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-4039043553-r1 { fill: #05080f } - .terminal-4039043553-r2 { fill: #e1e1e1 } - .terminal-4039043553-r3 { fill: #c5c8c6 } - .terminal-4039043553-r4 { fill: #1e2226;font-weight: bold } - .terminal-4039043553-r5 { fill: #35393d } - .terminal-4039043553-r6 { fill: #454a50 } - .terminal-4039043553-r7 { fill: #fea62b } - .terminal-4039043553-r8 { fill: #e2e3e3;font-weight: bold } - .terminal-4039043553-r9 { fill: #000000 } - .terminal-4039043553-r10 { fill: #e2e3e3 } - .terminal-4039043553-r11 { fill: #14191f } + .terminal-1095690712-r1 { fill: #05080f } + .terminal-1095690712-r2 { fill: #e1e1e1 } + .terminal-1095690712-r3 { fill: #c5c8c6 } + .terminal-1095690712-r4 { fill: #1e2226;font-weight: bold } + .terminal-1095690712-r5 { fill: #35393d } + .terminal-1095690712-r6 { fill: #454a50 } + .terminal-1095690712-r7 { fill: #fea62b } + .terminal-1095690712-r8 { fill: #e2e3e3;font-weight: bold } + .terminal-1095690712-r9 { fill: #000000 } + .terminal-1095690712-r10 { fill: #e2e3e3 } + .terminal-1095690712-r11 { fill: #14191f } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - BorderApp + BorderApp - - - - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - ascii - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔+------------------- ascii --------------------+ - none|| - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁|| - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔|I must not fear.| - hidden|Fear is the mind-killer.| - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁|Fear is the little-death that brings | - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔|total obliteration.| - blank|I will face my fear.| - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▅▅|I will permit it to pass over me and | - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔|through me.| - round|And when it has gone past, I will turn| - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁|the inner eye to see its path.| - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔|Where the fear has gone there will be | - solid|nothing. Only I will remain.| - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁|| - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔|| - double+----------------------------------------------+ - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - dashed - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + ascii + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔+------------------- ascii --------------------+ + blank|| + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁|| + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔|I must not fear.| + dashed|Fear is the mind-killer.| + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁|Fear is the little-death that brings | + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔|total obliteration.| + double|I will face my fear.| + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▅▅|I will permit it to pass over me and | + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔|through me.| + heavy|And when it has gone past, I will turn| + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁|the inner eye to see its path.| + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔|Where the fear has gone there will be | + hidden|nothing. Only I will remain.| + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁|| + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔|| + hkey+----------------------------------------------+ + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + inner + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁