diff --git a/environments-and-requirements/requirements-base.txt b/environments-and-requirements/requirements-base.txt index b7a3a2a7cf..bff12d499a 100644 --- a/environments-and-requirements/requirements-base.txt +++ b/environments-and-requirements/requirements-base.txt @@ -24,6 +24,7 @@ opencv-python picklescan pillow pip>=22 +prompt-toolkit pudb pyreadline3 pytorch-lightning==1.7.7 diff --git a/installer/installer.py b/installer/installer.py index 6862468eae..165a67d5f6 100644 --- a/installer/installer.py +++ b/installer/installer.py @@ -11,7 +11,7 @@ from pathlib import Path from tempfile import TemporaryDirectory, TemporaryFile SUPPORTED_PYTHON = ">=3.9.0,<3.11" -INSTALLER_REQS = ["rich", "semver", "requests", "plumbum"] +INSTALLER_REQS = ["rich", "semver", "requests", "plumbum", "prompt-toolkit"] OS = platform.uname().system ARCH = platform.uname().machine @@ -128,6 +128,8 @@ class Installer: Obtain the InvokeAI installation payload """ + pass + def install(self, path: str = "~/invokeai", version: str = "latest") -> None: """ Install the InvokeAI application into the given runtime path diff --git a/installer/main.py b/installer/main.py index 53b41fd28d..cc743a2afe 100644 --- a/installer/main.py +++ b/installer/main.py @@ -2,12 +2,20 @@ InvokeAI Installer """ +import argparse from installer import Installer if __name__ == "__main__": + parser = argparse.ArgumentParser() + + parser.add_argument("-d", "--destination", type=str, help="Destination path for installation", default="~/invokeai") + + args = parser.parse_args() + inst = Installer() + try: - inst.install() + inst.install(path=args.destination) except KeyboardInterrupt as exc: print("\n") print("Ctrl-C pressed. Aborting.") diff --git a/installer/messages.py b/installer/messages.py index 3d84bfc607..d83e29ac89 100644 --- a/installer/messages.py +++ b/installer/messages.py @@ -4,8 +4,10 @@ Installer user interaction import platform from pathlib import Path -from tkinter.filedialog import askdirectory +from prompt_toolkit import prompt +from prompt_toolkit.completion import PathCompleter +from prompt_toolkit.shortcuts import CompleteStyle from rich import box, print from rich.console import Console from rich.panel import Panel @@ -49,26 +51,56 @@ def dest_path(init_path=None) -> Path: :rtype: Path """ + # TODO: feels like this could be refactored for clarity (@ebr) + dest = init_path + if dest is not None: + dest = Path(dest).expanduser().resolve() + dest_confirmed = False while not dest_confirmed: console.line() - if dest is not None: - dest = Path(dest).expanduser().resolve() - print(f"InvokeAI will be installed at {dest}") - dest_confirmed = Confirm.ask(f"Continue?") + + print(f"InvokeAI will be installed at {dest}") + dest_confirmed = Confirm.ask(f"Is this correct?", default="y") + if not dest_confirmed: - print(f"Please select the destination directory for the installation") - resp = askdirectory(initialdir=dest) - if resp == (): - continue - dest = Path(resp).expanduser().resolve() + + # needs more thought into how to handle this nicely + # so that the first selected destination continues to shows up as + # default until the user is done selecting (potentially multiple times) + old_dest = dest + + path_completer = PathCompleter( + only_directories=True, + expanduser=True, + get_paths=lambda: [Path(dest).parent], + file_filter=lambda n: not n.startswith("."), + ) + print(f"Please select the destination directory for the installation \[{dest}]: ") + selected = prompt( + "[Tab] to complete ⮞ ", + complete_in_thread=True, + completer=path_completer, + complete_style=CompleteStyle.READLINE_LIKE, + ) + print(selected) + if Path(selected).is_absolute(): + # use the absolute path directly + dest = Path(selected) + else: + # the user entered a relative path - offer to create it as a sibling to the original destination + dest = dest.parent / Path(selected) + dest = dest.expanduser().resolve() + if dest.exists(): print(f":exclamation: Directory {dest} already exists.") dest_confirmed = Confirm.ask( ":question: Are you sure you want to (re)install in this location?", default="y" ) + if not dest_confirmed: + dest = old_dest try: dest.mkdir(exist_ok=True, parents=True)