diff --git a/.gitignore b/.gitignore index 84b6579..4624582 100644 --- a/.gitignore +++ b/.gitignore @@ -25,3 +25,4 @@ Thumbs.db # Local scratch /tmp/ +/cli/tmp/ diff --git a/README.md b/README.md index ffb5616..fb14fcf 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,10 @@ Give AI agents eyes into ArcGIS Pro. ```bash pip install arcgispro-cli + +# Optional: install TUI dependencies +pip install arcgispro-cli[tui] + arcgis install ``` diff --git a/cli/arcgispro_cli/cli.py b/cli/arcgispro_cli/cli.py index 02ddd44..c2af8e0 100644 --- a/cli/arcgispro_cli/cli.py +++ b/cli/arcgispro_cli/cli.py @@ -32,7 +32,7 @@ from . import __version__ from .commands import clean, open_project, install, query, launch, notebooks, tui, diagram -from .tui.banner import _colorize_logo +from .logo import colorize_logo # Ensure Unicode output on Windows if sys.stdout.encoding != "utf-8": @@ -62,7 +62,7 @@ def main(ctx): """ ctx.ensure_object(dict) if ctx.invoked_subcommand is None: - console.print(_colorize_logo()) + console.print(colorize_logo()) console.print() console.print(ctx.get_help()) diff --git a/cli/arcgispro_cli/commands/tui.py b/cli/arcgispro_cli/commands/tui.py index 4d5aed3..dc96995 100644 --- a/cli/arcgispro_cli/commands/tui.py +++ b/cli/arcgispro_cli/commands/tui.py @@ -6,6 +6,14 @@ @click.option("--no-banner", is_flag=True, help="Disable the ASCII banner") def tui_cmd(repo: str, no_banner: bool) -> None: """Start the ArcGIS Pro TUI.""" - from arcgispro_cli.tui.app import ArcGISProCLIApp + try: + from arcgispro_cli.tui.app import ArcGISProCLIApp + except ModuleNotFoundError as e: + # The most common case is missing optional TUI deps (textual). + if (e.name or "").startswith("textual"): + raise click.ClickException( + "TUI dependencies are not installed. Install with: pip install arcgispro-cli[tui]" + ) + raise ArcGISProCLIApp(repo_path=repo, show_banner=(not no_banner)).run() diff --git a/cli/arcgispro_cli/logo.py b/cli/arcgispro_cli/logo.py new file mode 100644 index 0000000..7142a01 --- /dev/null +++ b/cli/arcgispro_cli/logo.py @@ -0,0 +1,50 @@ +from __future__ import annotations + +from rich.text import Text + +LOGO_LINES = [ + " █████╗ ██████╗ ██████╗ ██████╗ ██╗███████╗", + "██╔══██╗██╔══██╗██╔════╝ ██╔════╝ ██║██╔════╝", + "███████║██████╔╝██║ ██║ ███╗██║███████╗", + "██╔══██║██╔══██╗██║ ██║ ██║██║╚════██║", + "██║ ██║██║ ██║╚██████╗ ╚██████╔╝██║███████║", + "╚═╝ ╚═╝╚═╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═╝╚══════╝", +] + +BLUES = [ + "#9dd8ff", + "#87cefa", + "#6fbff6", + "#55aef0", + "#3f9be8", + "#2f88e0", +] + +GRAYS = [ + "color(250)", + "color(248)", + "color(245)", + "color(243)", + "color(240)", + "color(238)", +] + +SHADOW_CHARS = set("╔╗╚╝═║") + + +def colorize_logo() -> Text: + """Render ARCGIS logo with blue gradient fills and gray shadow strokes.""" + text = Text() + for row_index, line in enumerate(LOGO_LINES): + block_color = BLUES[min(row_index, len(BLUES) - 1)] + shadow_color = GRAYS[min(row_index, len(GRAYS) - 1)] + for ch in line: + if ch in SHADOW_CHARS: + text.append(ch, style=shadow_color) + elif ch == " ": + text.append(ch) + else: + text.append(ch, style=block_color) + if row_index < len(LOGO_LINES) - 1: + text.append("\n") + return text diff --git a/cli/arcgispro_cli/tui/banner.py b/cli/arcgispro_cli/tui/banner.py index 77f5cb1..4dd2a79 100644 --- a/cli/arcgispro_cli/tui/banner.py +++ b/cli/arcgispro_cli/tui/banner.py @@ -1,56 +1,9 @@ from __future__ import annotations -from rich.text import Text from textual.widget import Widget from textual.widgets import Static -LOGO_LINES = [ - " █████╗ ██████╗ ██████╗ ██████╗ ██╗███████╗", - "██╔══██╗██╔══██╗██╔════╝ ██╔════╝ ██║██╔════╝", - "███████║██████╔╝██║ ██║ ███╗██║███████╗", - "██╔══██║██╔══██╗██║ ██║ ██║██║╚════██║", - "██║ ██║██║ ██║╚██████╗ ╚██████╔╝██║███████║", - "╚═╝ ╚═╝╚═╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═╝╚══════╝", -] - -BLUES = [ - "#9dd8ff", - "#87cefa", - "#6fbff6", - "#55aef0", - "#3f9be8", - "#2f88e0", -] - -GRAYS = [ - "color(250)", - "color(248)", - "color(245)", - "color(243)", - "color(240)", - "color(238)", -] - -SHADOW_CHARS = set("╔╗╚╝═║") - - -def _colorize_logo() -> Text: - """Render ARCGIS logo with blue gradient fills and gray shadow strokes.""" - text = Text() - for row_index, line in enumerate(LOGO_LINES): - block_color = BLUES[min(row_index, len(BLUES) - 1)] - shadow_color = GRAYS[min(row_index, len(GRAYS) - 1)] - for ch in line: - if ch in SHADOW_CHARS: - text.append(ch, style=shadow_color) - elif ch == " ": - text.append(ch) - else: - text.append(ch, style=block_color) - if row_index < len(LOGO_LINES) - 1: - text.append("\n") - return text - +from arcgispro_cli.logo import colorize_logo class Banner(Widget): """Top-of-screen banner with oh-my-logo rendering.""" @@ -80,4 +33,4 @@ def _refresh(self) -> None: self._body.update("") return - self._body.update(_colorize_logo()) + self._body.update(colorize_logo()) diff --git a/cli/pyproject.toml b/cli/pyproject.toml index 37834b9..c625e3a 100644 --- a/cli/pyproject.toml +++ b/cli/pyproject.toml @@ -27,10 +27,15 @@ dependencies = [ "click>=8.0", "pillow>=10.0.0", "rich>=13.0", - "textual>=0.56", ] [project.optional-dependencies] +# The Textual UI is optional so `pip install arcgispro-cli` stays lightweight +# and avoids pulling in heavy TUI deps unless you want them. +tui = [ + "textual>=0.56", +] + dev = [ "pytest>=7.0", "pytest-cov>=4.0", diff --git a/cli/tests/test_cli.py b/cli/tests/test_cli.py index 1ec25ac..bea01c9 100644 --- a/cli/tests/test_cli.py +++ b/cli/tests/test_cli.py @@ -31,6 +31,16 @@ def test_install_help(): assert "ProExporter" in result.output +def test_tui_without_optional_deps_gives_helpful_error(): + runner = CliRunner() + result = runner.invoke(main, ["tui"]) + + # Without the optional Textual dependency, this should fail fast with guidance. + # (In dev envs where textual is installed, this test may need updating.) + assert result.exit_code != 0 + assert "arcgispro-cli[tui]" in result.output + + def test_layers_no_folder(): """Test layers command when no .arcgispro folder exists.""" runner = CliRunner()