├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── feature_request.md │ ├── fix_request.md │ └── security-vulnerability.md └── workflows │ ├── lint.yml │ ├── python-publish.yml │ └── tests.yml ├── .gitignore ├── .pre-commit-config.yaml ├── .readthedocs.yaml ├── .run ├── Make vhs.run.xml ├── Sphinx Build.run.xml ├── Sphinx Coverage .run.xml ├── pytest in tests.run.xml └── run example.run.xml ├── CONTRIBUTING.md ├── ItsPrompt ├── data │ ├── __init__.py │ ├── checkbox.py │ ├── expand.py │ ├── select.py │ ├── style.py │ └── table.py ├── keyboard_handler.py ├── objects │ ├── prompts │ │ ├── option.py │ │ ├── options_with_separator.py │ │ ├── separator.py │ │ └── type.py │ └── table │ │ ├── table_base.py │ │ ├── table_df.py │ │ ├── table_dict.py │ │ └── table_list.py ├── prompt.py ├── prompts │ ├── __init__.py │ ├── checkbox.py │ ├── confirm.py │ ├── expand.py │ ├── input.py │ ├── raw_select.py │ ├── select.py │ └── table.py └── py.typed ├── LICENSE ├── README.md ├── SECURITY.md ├── docs ├── Makefile ├── make.bat ├── requirements.txt ├── scripts │ └── vhs │ │ ├── checkbox.tape │ │ ├── configuration │ │ ├── base_config.tape │ │ ├── big_window_config.tape │ │ ├── config.sh │ │ ├── gradient.png │ │ └── small_window_config.tape │ │ ├── confirm.tape │ │ ├── demo.tape │ │ ├── expand.tape │ │ ├── generate.sh │ │ ├── input.tape │ │ ├── raw_select.tape │ │ ├── select.tape │ │ ├── table.tape │ │ └── tape.template └── source │ ├── api │ ├── objects.rst │ └── prompt.rst │ ├── conf.py │ ├── development_guide │ ├── documentation.rst │ └── getting_started.rst │ ├── guide │ ├── getting_started.rst │ ├── options_and_data.rst │ ├── prompt_types.rst │ ├── styling.rst │ └── usage.rst │ ├── index.rst │ └── media │ ├── ItsPrompt.gif │ ├── checkbox.png │ ├── confirm.png │ ├── expand.png │ ├── input.png │ ├── raw_select.png │ ├── select.png │ ├── styling │ ├── error.png │ ├── grayout.png │ ├── option.png │ ├── question.png │ ├── question_mark.png │ ├── selected_option.png │ ├── separator.png │ ├── text.png │ └── tooltip.png │ ├── styling_input.png │ ├── styling_input_annotated.png │ ├── styling_raw_select.png │ ├── styling_raw_select_annotated.png │ └── table.png ├── example.py ├── examples ├── demo.py └── demos │ ├── checkbox_demo.py │ ├── confirm_demo.py │ ├── expand_demo.py │ ├── input_demo.py │ ├── raw_select_demo.py │ ├── select_demo.py │ └── table_demo.py ├── pyproject.toml ├── requirements-dev.txt ├── requirements.txt └── tests ├── __init__.py ├── conftest.py ├── data ├── test_checkbox.py ├── test_expand.py ├── test_select.py ├── test_style.py └── test_table.py └── prompts ├── __init__.py ├── test_checkbox_prompt.py ├── test_confirm_prompt.py ├── test_expand_prompt.py ├── test_input_prompt.py ├── test_raw_select_prompt.py ├── test_select_prompt.py └── test_table_prompt.py /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "[BUG]" 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 16 | 1. Go to '...' 17 | 2. Click on '....' 18 | 3. Scroll down to '....' 19 | 4. See error 20 | 21 | **Expected behavior** 22 | A clear and concise description of what you expected to happen. 23 | 24 | **Screenshots** 25 | If applicable, add screenshots to help explain your problem. 26 | 27 | **Device (please complete the following information):** 28 | 29 | - OS: [e.g. iOS] 30 | - Python [e.g. version, installed packages] 31 | - Version [e.g. 22] 32 | 33 | **Additional context** 34 | Add any other context about the problem here. 35 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: "[FEATURE]" 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Feature description** 11 | Describe what you want to be added. 12 | 13 | **Additional Information** 14 | Write some information, e.g. what we need to add your feature or how you think this should be done. 15 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/fix_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Fix request 3 | about: Point out a fix that needs to be addressed 4 | title: "[FIX]" 5 | labels: fix 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Fix description** 11 | Describe what you want to be added. 12 | 13 | **Additional Information** 14 | Write some information, e.g. what we need to fix that problem or how you think this should be done. 15 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/security-vulnerability.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Security Vulnerability 3 | about: Show us a security vulnerability you have found and which needs to be addressed 4 | title: "[SECURITY] " 5 | labels: security 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the security concern** 11 | A clear and concise description of what security concern you encountered and why it is so bad. 12 | 13 | **To Reproduce** 14 | *If it's a general security concern, leave this section empty* 15 | Steps to reproduce the behavior: 16 | 17 | 1. Go to '...' 18 | 2. Click on '....' 19 | 3. Scroll down to '....' 20 | 4. See error 21 | 22 | **Screenshots** 23 | *If it's a general security concern, leave this section empty* 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Device (please complete the following information):** 27 | *If it's a general security concern, leave this section empty* 28 | 29 | - OS: [e.g. iOS] 30 | - Python [e.g. version, installed packages] 31 | - Version [e.g. 22] 32 | 33 | **Additional context** 34 | Add any other context about the problem here. 35 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: linting 2 | on: 3 | - push 4 | - pull_request 5 | 6 | jobs: 7 | typing-check: 8 | name: mypy Typing Check 9 | runs-on: ubuntu-latest 10 | steps: 11 | # setup python 12 | - name: Checkout 13 | uses: actions/checkout@v2 14 | - name: Setup python 15 | uses: actions/setup-python@v4 16 | with: 17 | python-version: "3.x" 18 | cache: "pip" 19 | cache-dependency-path: "requirements*.txt" 20 | - name: Install modules 21 | run: pip install -r requirements.txt -r requirements-dev.txt 22 | 23 | # run mypy 24 | - name: Run mypy 25 | run: python -m mypy 26 | formatting-check: 27 | name: YAPF Formatting Check 28 | runs-on: ubuntu-latest 29 | steps: 30 | - uses: actions/checkout@v3 31 | - name: run YAPF to test if python code is correctly formatted 32 | uses: AlexanderMelde/yapf-action@v1.0 33 | with: 34 | args: --verbose 35 | -------------------------------------------------------------------------------- /.github/workflows/python-publish.yml: -------------------------------------------------------------------------------- 1 | # This workflow will upload a Python Package using Twine when a release is created 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python#publishing-to-package-registries 3 | 4 | # This workflow uses actions that are not certified by GitHub. 5 | # They are provided by a third-party and are governed by 6 | # separate terms of service, privacy policy, and support 7 | # documentation. 8 | 9 | name: Upload Python Package 10 | 11 | on: 12 | release: 13 | types: [ published ] 14 | 15 | permissions: 16 | contents: read 17 | 18 | jobs: 19 | deploy: 20 | 21 | runs-on: ubuntu-latest 22 | 23 | steps: 24 | - uses: actions/checkout@v3 25 | - name: Set up Python 26 | uses: actions/setup-python@v3 27 | with: 28 | python-version: '3.x' 29 | - name: Install dependencies 30 | run: | 31 | python -m pip install --upgrade pip 32 | pip install build 33 | - name: Build package 34 | run: python -m build 35 | - name: Publish package 36 | uses: pypa/gh-action-pypi-publish@27b31702a0e7fc50959f5ad993c78deac1bdfc29 37 | with: 38 | user: __token__ 39 | password: ${{ secrets.PYPI_API_TOKEN }} 40 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | on: 3 | - push 4 | - pull_request 5 | 6 | jobs: 7 | testing-check: 8 | name: Run all tests with pytest 9 | runs-on: ubuntu-latest 10 | steps: 11 | # setup python 12 | - name: Checkout 13 | uses: actions/checkout@v2 14 | - name: Setup python 15 | uses: actions/setup-python@v4 16 | with: 17 | python-version: "3.x" 18 | cache: "pip" 19 | cache-dependency-path: "requirements*.txt" 20 | - name: Install modules 21 | run: pip install -r requirements.txt -r requirements-dev.txt 22 | 23 | # run pytest 24 | - name: Run pytest 25 | run: python -m pytest 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # poetry 98 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 99 | # This is especially recommended for binary packages to ensure reproducibility, and is more 100 | # commonly ignored for libraries. 101 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 102 | #poetry.lock 103 | 104 | # pdm 105 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 106 | #pdm.lock 107 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 108 | # in version control. 109 | # https://pdm.fming.dev/#use-with-ide 110 | .pdm.toml 111 | 112 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 113 | __pypackages__/ 114 | 115 | # Celery stuff 116 | celerybeat-schedule 117 | celerybeat.pid 118 | 119 | # SageMath parsed files 120 | *.sage.py 121 | 122 | # Environments 123 | .env 124 | .venv 125 | env/ 126 | venv/ 127 | ENV/ 128 | env.bak/ 129 | venv.bak/ 130 | 131 | # Spyder project settings 132 | .spyderproject 133 | .spyproject 134 | 135 | # Rope project settings 136 | .ropeproject 137 | 138 | # mkdocs documentation 139 | /site 140 | 141 | # mypy 142 | .mypy_cache/ 143 | .dmypy.json 144 | dmypy.json 145 | 146 | # Pyre type checker 147 | .pyre/ 148 | 149 | # pytype static type analyzer 150 | .pytype/ 151 | 152 | # Cython debug symbols 153 | cython_debug/ 154 | 155 | # PyCharm 156 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 157 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 158 | # and can be added to the global gitignore or merged into this file. For a more nuclear 159 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 160 | .idea/ 161 | 162 | # others 163 | prototyping 164 | prototyping/* 165 | .vscode 166 | 167 | # pyenv 168 | .python-version 169 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | exclude: ".*tests\/fixtures.*" 2 | repos: 3 | - repo: https://github.com/google/yapf 4 | rev: v0.43.0 5 | hooks: 6 | - id: yapf 7 | args: [ --diff ] 8 | - repo: https://github.com/pre-commit/mirrors-mypy 9 | rev: v1.2.0 10 | hooks: 11 | - id: mypy 12 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | # .readthedocs.yaml 2 | # Read the Docs configuration file 3 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 4 | 5 | version: 2 6 | 7 | build: 8 | os: ubuntu-22.04 9 | tools: 10 | python: "3.11" 11 | 12 | sphinx: 13 | configuration: docs/source/conf.py 14 | 15 | formats: 16 | - pdf 17 | - epub 18 | 19 | python: 20 | install: 21 | - requirements: docs/requirements.txt 22 | - method: pip 23 | path: . 24 | -------------------------------------------------------------------------------- /.run/Make vhs.run.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /.run/Sphinx Build.run.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 21 | 22 | -------------------------------------------------------------------------------- /.run/Sphinx Coverage .run.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 21 | 22 | -------------------------------------------------------------------------------- /.run/pytest in tests.run.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 20 | -------------------------------------------------------------------------------- /.run/run example.run.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 26 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to ItsPrompt 2 | 3 | Thank you for your interest in contributing to ItsPrompt. We welcome and appreciate any contributions, whether it is 4 | code, documentation, bug reports, feature requests, or general feedback. 5 | 6 | 10 | 11 | ## How to Contribute 12 | 13 | There are many ways you can contribute to this project. Here are some guidelines to help you get started. 14 | 15 | If you want to read more about development, please refer to 16 | the [Development Guide](https://itsprompt.readthedocs.io/en/latest/development_guide/getting_started.html). 17 | 18 | ### Reporting Issues 19 | 20 | If you encounter any problems or have any suggestions for improvement, please open an issue on GitHub using the 21 | appropriate template. Be as descriptive as possible and include any relevant information such as error messages, 22 | screenshots, or steps to reproduce the issue. 23 | 24 | *If you need help with creating an Issue, join our [Discord](https://discord.gg/rP9Qke2jDs) and don't hesitate to 25 | contact us!* 26 | 27 | ### Submitting Pull Requests 28 | 29 | If you want to contribute code or documentation to this project, you can submit a pull request (PR) on GitHub. Before 30 | you do so, please make sure that: 31 | 32 | - You have tested your code locally and ensured that it works as expected (also be sure you have added tests). 33 | - You have written clear and concise commit messages and PR descriptions. 34 | - You have updated the documentation if necessary. 35 | - You have resolved any merge conflicts with the main branch. 36 | 37 | *If you need help with opening a PR, join our [Discord](https://discord.gg/rP9Qke2jDs) and don't hesitate to contact 38 | us!* 39 | 40 | To submit a PR, follow these steps: 41 | 42 | 1. Fork the repository and create a new branch from the main branch. 43 | 2. Set up your local clone using [Setting up the project for development](#setting-up-the-project-for-development). 44 | 3. Make your changes and commit them with a meaningful message. 45 | 4. Push your branch to your forked repository on GitHub. 46 | 5. Create a PR from your branch to the main branch of the original repository. 47 | 6. Wait for the project maintainers to review your PR and provide feedback or approval. 48 | 7. If requested, make any necessary changes and update your PR accordingly. 49 | 8. Once your PR is merged, delete your branch and fork. 50 | 51 | ### Setting up the project for development 52 | 53 | Before you can start working on ItsPrompt, you need to do a few things: 54 | 55 | 1. Install all required dependencies from `requirements.txt` and `requirements-dev.txt`. 56 | 2. Install our pre-commit hooks using `pre-commit install` in your main project directory. 57 | 58 | ### Providing Feedback 59 | 60 | We value your feedback and opinions on this project. You can share them with us by: 61 | 62 | - Joining the [Discussions](https://github.com/TheItsProjects/ItsPrompt/discussions) section on GitHub and starting or 63 | joining a conversation. 64 | - Joining our [Discord](https://discord.gg/rP9Qke2jDs) and letting us know what you think. 65 | 66 | ## Thank You 67 | 68 | We appreciate your time and effort in contributing to this project. We hope you enjoy using ItsPrompt and find it 69 | useful. Thank you for being part of our community! 70 | -------------------------------------------------------------------------------- /ItsPrompt/data/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheItsProjects/ItsPrompt/b2f959dba2337775420e1cd525002e1df72f8c99/ItsPrompt/data/__init__.py -------------------------------------------------------------------------------- /ItsPrompt/data/checkbox.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | 3 | from ..objects.prompts.option import Option 4 | from ..objects.prompts.options_with_separator import OptionsWithSeparator 5 | from ..objects.prompts.separator import Separator 6 | from ..objects.prompts.type import OptionsList 7 | 8 | 9 | @dataclass 10 | class CheckboxOption(Option): 11 | name: str 12 | id: str 13 | is_selected: bool 14 | is_disabled: bool 15 | 16 | 17 | def process_data(options: OptionsList) -> OptionsWithSeparator[CheckboxOption | Separator]: 18 | """ 19 | Processes the given `options` and returns the processed list 20 | 21 | :param options: A list of options to process 22 | :type options: tuple[str | OptionWithId | Separator, ...] 23 | :raises TypeError: If an option is not processable, a `TypeError` will be raised 24 | :return: a list of `CheckboxOption` 25 | :rtype: list[CheckboxOption] 26 | """ 27 | processed_options: list[CheckboxOption | Separator] = [] 28 | 29 | # process given options 30 | for option in options: 31 | if type(option) is str: 32 | processed_options.append(CheckboxOption(name=option, id=option, is_selected=False, is_disabled=False)) 33 | elif type(option) is tuple: 34 | processed_options.append(CheckboxOption(name=option[0], id=option[1], is_selected=False, is_disabled=False)) 35 | elif type(option) is Separator: 36 | processed_options.append(option) 37 | else: 38 | raise TypeError('Argument is not processable') 39 | 40 | return OptionsWithSeparator(*processed_options) 41 | -------------------------------------------------------------------------------- /ItsPrompt/data/expand.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | 3 | from ..objects.prompts.option import Option 4 | from ..objects.prompts.options_with_separator import OptionsWithSeparator 5 | from ..objects.prompts.separator import Separator 6 | from ..objects.prompts.type import OptionsList 7 | 8 | 9 | @dataclass 10 | class ExpandOption(Option): 11 | key: str 12 | name: str 13 | id: str 14 | is_disabled: bool 15 | 16 | 17 | class ExpandOptionsWithSeparator(OptionsWithSeparator[ExpandOption | Separator]): 18 | 19 | def __init__(self, *args: ExpandOption | Separator): 20 | super().__init__(*args) 21 | 22 | def get_option(self, key: str) -> ExpandOption | None: 23 | """ 24 | Get the option with the given key. 25 | 26 | :param key: The key of the option to get 27 | :type key: str 28 | :return: The option with the given key or None if not found 29 | :rtype: ExpandOption | None 30 | """ 31 | for option in self.with_separators: 32 | if isinstance(option, ExpandOption) and option.key == key: 33 | return option 34 | return None 35 | 36 | 37 | def process_data(options: OptionsList) -> ExpandOptionsWithSeparator: 38 | """ 39 | Processes the given `options` and returns the processed list 40 | 41 | :param options: A list of options to process 42 | :type options: tuple[str | OptionWithId | Separator, ...] 43 | :raises ValueError: If the keys are not unique 44 | :raises ValueError: If the keys are not of length 1 45 | :raises ValueError: If the keys are not ascii 46 | :raises ValueError: If the keys are using h 47 | :raises TypeError: If an option is not processable 48 | :return: A list of `ExpandOptions` 49 | :rtype: list[ExpandOption] 50 | """ 51 | # check if every key is unique, otherwise return error 52 | keys = [option[0] for option in options if type(option) is not Separator] # type: ignore 53 | if len(set(keys)) < len(keys): 54 | raise ValueError('Keys must be unique!') 55 | 56 | # check that every key string is only one char long 57 | if any( 58 | [ 59 | len(option[0]) > 1 or len(option[0]) < 1 for option in options if # type: ignore 60 | type(option) is not Separator 61 | ] 62 | ): 63 | raise ValueError('Keys must be of length 1!') 64 | 65 | # check that every key is ascii 66 | if any([not option[0].isascii() for option in options if type(option) is not Separator]): # type: ignore 67 | raise ValueError('Keys must be ascii!') 68 | 69 | # check if h is not assigned 70 | if any([option[0] == 'h' for option in options if type(option) is not Separator]): # type: ignore 71 | raise ValueError('The h-key is not assignable!') 72 | 73 | processed_options: list[ExpandOption] = [] 74 | 75 | # process given options 76 | for option in options: 77 | if type(option) is str: 78 | # use the first letter as the key, and str as name and id 79 | processed_options.append(ExpandOption(key=option[0], name=option, id=option, is_disabled=False)) 80 | elif type(option) is tuple: 81 | processed_options.append( 82 | ExpandOption(key=option[0], name=option[1], id=option[2], is_disabled=False) # type: ignore 83 | ) 84 | elif type(option) is Separator: 85 | processed_options.append(option) # type: ignore 86 | else: 87 | raise TypeError('Argument is not processable') 88 | 89 | # append a help option 90 | processed_options.append( 91 | ExpandOption(key='h', name='Help Menu, list or hide all options', id='', is_disabled=False) 92 | ) 93 | 94 | return ExpandOptionsWithSeparator(*processed_options) 95 | -------------------------------------------------------------------------------- /ItsPrompt/data/select.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | 3 | from ItsPrompt.objects.prompts.option import Option 4 | from ItsPrompt.objects.prompts.options_with_separator import OptionsWithSeparator 5 | from ItsPrompt.objects.prompts.separator import Separator 6 | from ItsPrompt.objects.prompts.type import OptionsList 7 | 8 | 9 | @dataclass 10 | class SelectOption(Option): 11 | name: str 12 | id: str 13 | is_disabled: bool 14 | 15 | 16 | def process_data(options: OptionsList) -> OptionsWithSeparator[SelectOption | Separator]: 17 | """ 18 | Processes the given `options` and returns the processed list 19 | 20 | :param options: A list of options to process 21 | :type options: tuple[str | OptionWithId | Separator, ...] 22 | :raises TypeError: If an option is not processable, a `TypeError` will be raised 23 | :return: a list of `SelectOptions` 24 | :rtype: list[SelectOption] 25 | """ 26 | processed_options: list[SelectOption | Separator] = [] 27 | 28 | # process given options 29 | for option in options: 30 | if type(option) is str: 31 | processed_options.append(SelectOption(name=option, id=option, is_disabled=False)) 32 | elif type(option) is tuple: 33 | processed_options.append(SelectOption(name=option[0], id=option[1], is_disabled=False)) 34 | elif type(option) is Separator: 35 | processed_options.append(option) 36 | else: 37 | raise TypeError('Argument is not processable') 38 | 39 | return OptionsWithSeparator(*processed_options) 40 | -------------------------------------------------------------------------------- /ItsPrompt/data/style.py: -------------------------------------------------------------------------------- 1 | import copy 2 | from dataclasses import asdict, dataclass 3 | 4 | from prompt_toolkit.styles import Style 5 | 6 | 7 | @dataclass 8 | class PromptStyle: 9 | """ 10 | The style object used for styling the prompts. 11 | 12 | Empty styles will not be styled, so they appear without any styling. 13 | """ 14 | question_mark: str = '' 15 | question: str = '' 16 | option: str = '' 17 | selected_option: str = '' 18 | tooltip: str = '' 19 | error: str = '' 20 | text: str = '' 21 | grayout: str = '' 22 | disabled: str = '' 23 | separator: str = '' 24 | 25 | 26 | default_style = PromptStyle( 27 | question_mark="fg:ansigreen", 28 | selected_option="fg:ansicyan", 29 | tooltip="fg:ansibrightblue bg:ansiwhite bold", 30 | error="fg:ansiwhite bg:ansired bold", 31 | grayout="fg:ansibrightblack", 32 | disabled="fg:ansibrightblack", 33 | separator="fg:ansibrightgreen", 34 | ) 35 | """ 36 | The default style for the prompts. 37 | """ 38 | 39 | 40 | def _convert_style(style: PromptStyle) -> Style: 41 | """ 42 | Converts the given `PromptStyle` to a usable `Style` object. 43 | 44 | :param style: The style to convert 45 | :type style: PromptStyle 46 | :return: The converted `Style` object 47 | :rtype: Style 48 | """ 49 | return Style.from_dict(asdict(style)) 50 | 51 | 52 | def create_from_default() -> PromptStyle: 53 | """ 54 | Returns a copy of the :data:`default_style` which can be edited without changing the default style. 55 | 56 | :return: An editable copy of the default style 57 | :rtype: PromptStyle 58 | """ 59 | return copy.deepcopy(default_style) 60 | -------------------------------------------------------------------------------- /ItsPrompt/data/table.py: -------------------------------------------------------------------------------- 1 | import os 2 | from typing import TYPE_CHECKING, Union 3 | 4 | from ..objects.prompts.type import TablePromptDict, TablePromptList 5 | from ..objects.table.table_base import TableDataBase 6 | from ..objects.table.table_dict import TableDataFromDict 7 | from ..objects.table.table_list import TableDataFromList 8 | 9 | # only import pandas and TableData if pandas is installed 10 | try: 11 | from pandas import DataFrame 12 | 13 | from ..objects.table.table_df import TableDataFromDF 14 | except ModuleNotFoundError: # pragma: no cover 15 | # pandas were not installed 16 | pass 17 | 18 | if TYPE_CHECKING: # pragma: no cover 19 | from pandas import DataFrame 20 | 21 | 22 | class Table: 23 | 24 | def __init__(self, data: Union["DataFrame", TablePromptDict, TablePromptList]) -> None: 25 | """ 26 | Creates a table object for storing the drawable table instance 27 | 28 | :param data: the data to display 29 | :type data: DataFrame 30 | """ 31 | # change type of all columns to str 32 | # TODO maybe later support more datatypes 33 | self.data: TableDataBase 34 | if type(data) is dict: 35 | self.data = TableDataFromDict(data) 36 | elif type(data) is list: 37 | self.data = TableDataFromList(data) 38 | elif type(data) is DataFrame: 39 | self.data = TableDataFromDF(data) 40 | 41 | # save amount of rows 42 | self.row_count = self.data.row_count 43 | self.col_count = self.data.col_count 44 | 45 | # get max cell width 46 | max_width = os.get_terminal_size().columns 47 | 48 | # get width for each cell 49 | self.cell_width = (max_width - 1) // self.col_count - 1 50 | 51 | # save current selected cell 52 | self.cur_cell = [0, 0] # ignoring header, as this can not be selected 53 | 54 | def _col_is_last(self, col) -> bool: 55 | """checks if the given column is the last column in the DataFrame""" 56 | return self.data.get_column_location(col) == self.col_count - 1 57 | 58 | def _char_if_last_else_otherchar( 59 | self, 60 | col, 61 | char: str, 62 | otherchar: str, 63 | ) -> str: 64 | """returns the `char` if the given column is the last in the DataFrame, otherwise returns the `otherchar`""" 65 | return char if self._col_is_last(col) else otherchar 66 | 67 | def get_table_as_str(self) -> str: 68 | """ 69 | Returns the table as a printable string 70 | 71 | Respects the width of the terminal at time of creation of the table object. Later this may be made dynamic. 72 | """ 73 | 74 | # set initial list (each entry represents a line to print) 75 | table_out = [''] * (self.row_count * 2 + 1) # 2 lines for every row + bottom border 76 | 77 | # append left border to every string 78 | table_out[0] += '┌' 79 | 80 | for i in range(1, len(table_out), 2): 81 | table_out[i] += '│' 82 | table_out[i + 1] += '├' 83 | 84 | table_out[-1] = '└' 85 | 86 | # add headers 87 | for header in self.data.columns: 88 | table_out[0] += '─' * self.cell_width + self._char_if_last_else_otherchar(header, '┐', '┬') 89 | 90 | if len(str(header)) > self.cell_width: 91 | # /\ convert the header to str in case the user did not give a header, so it is an integer 92 | header = header[:self.cell_width - 1] + '.' 93 | 94 | table_out[1] += f'{header:^{self.cell_width}}' + '│' 95 | 96 | # add values, iterate over each column 97 | for header, values in self.data.items(): 98 | # iterate over each row 99 | for i, val in zip(range(2, len(table_out), 2), values): # start at 2, because first two rows are header 100 | table_out[i] += '─' * self.cell_width + self._char_if_last_else_otherchar(header, '┤', '┼') 101 | table_out[i + 1] += f'{str(val):{self.cell_width}}' + '│' 102 | 103 | table_out[-1] += '─' * self.cell_width + self._char_if_last_else_otherchar(header, '┘', '┴') 104 | 105 | return '\n'.join(table_out) 106 | 107 | def on_up(self): 108 | """when up is pressed, select same column, but row one above (or last row if current is 0)""" 109 | 110 | self.cur_cell[1] = (self.cur_cell[1] - 1) % (self.row_count - 1) 111 | 112 | def on_down(self): 113 | """when down is pressed, select same column, but row one below (or first row if current is last)""" 114 | 115 | self.cur_cell[1] = (self.cur_cell[1] + 1) % (self.row_count - 1) 116 | 117 | def on_left(self): 118 | """when left is pressed, select same row, but column one to the left (or last column if current is 0)""" 119 | 120 | self.cur_cell[0] = (self.cur_cell[0] - 1) % self.col_count 121 | 122 | def on_right(self): 123 | """when right is pressed, select same row, but column one to the right (or first column if current is last)""" 124 | 125 | self.cur_cell[0] = (self.cur_cell[0] + 1) % self.col_count 126 | 127 | def add_key(self, key: str): 128 | """add key to current selected cell""" 129 | self.data.add_key(self.cur_cell[1], self.cur_cell[0], key) 130 | 131 | def del_key(self): 132 | """remove last key from current selected cell""" 133 | self.data.del_key(self.cur_cell[1], self.cur_cell[0]) 134 | 135 | def get_current_cursor_position(self) -> tuple[int, int]: 136 | """returns the current position of the cursor, relative to the top left corner character as (0, 0)""" 137 | y = self.cur_cell[1] * 2 + 3 # 3 is offset for header 138 | 139 | item_length = len(self.data.get_item_at(self.cur_cell[1], self.cur_cell[0])) 140 | # length of the item in the current cell 141 | 142 | x = (self.cell_width + 1) * self.cur_cell[0] + 1 + item_length 143 | # 1 is offset for left border, item_length is length of item 144 | return x, y 145 | -------------------------------------------------------------------------------- /ItsPrompt/keyboard_handler.py: -------------------------------------------------------------------------------- 1 | from prompt_toolkit import Application 2 | from prompt_toolkit.key_binding import KeyBindings, KeyPressEvent 3 | 4 | 5 | def generate_key_bindings(app: type[Application]) -> KeyBindings: 6 | """ 7 | Generates a KeyBindings object for use in a Prompt class 8 | 9 | The `app` is used to check whether specific functions for handling the key events exist. It has to be a Prompt 10 | object (e.g. InputPrompt). 11 | 12 | The available functions are: 13 | 14 | `on_up()` 15 | 16 | `on_down()` 17 | 18 | `on_left()` 19 | 20 | `on_right()` 21 | 22 | `on_enter()` 23 | 24 | `on_space()` 25 | 26 | `on_alt_enter()` 27 | 28 | `on_backspace()` 29 | 30 | `on_ctrl_backspace()` 31 | 32 | `on_key()` 33 | 34 | :param app: An application type to check whether one of the above functions exists 35 | :type app: type[Application] 36 | :return: An usable KeyBindings instance 37 | :rtype: KeyBindings 38 | """ 39 | 40 | kb = KeyBindings() 41 | 42 | # Each of the options gets the keyboard input and runs the equivalent function in the event.app class, 43 | # if the function exists. 44 | 45 | @kb.add('c-c') 46 | def quit(event: KeyPressEvent): 47 | event.app.exit() 48 | 49 | @kb.add('up', filter=hasattr(app, 'on_up')) 50 | def up(event: KeyPressEvent): 51 | event.app.on_up() # type: ignore 52 | 53 | @kb.add('down', filter=hasattr(app, 'on_down')) 54 | def down(event: KeyPressEvent): 55 | event.app.on_down() # type: ignore 56 | 57 | @kb.add('left', filter=hasattr(app, 'on_left')) 58 | def left(event: KeyPressEvent): 59 | event.app.on_left() # type: ignore 60 | 61 | @kb.add('right', filter=hasattr(app, 'on_right')) 62 | def right(event: KeyPressEvent): 63 | event.app.on_right() # type: ignore 64 | 65 | @kb.add('enter', filter=hasattr(app, 'on_enter')) 66 | def enter(event: KeyPressEvent): 67 | event.app.on_enter() # type: ignore 68 | 69 | @kb.add('space', filter=hasattr(app, 'on_space')) 70 | def space(event: KeyPressEvent): 71 | event.app.on_space() # type: ignore 72 | 73 | @kb.add('escape', 'enter', filter=hasattr(app, 'on_alt_enter')) 74 | # Vt100 terminals convert "alt+key" to "escape,key" 75 | def alt_enter(event: KeyPressEvent): 76 | event.app.on_alt_enter() # type: ignore 77 | 78 | # backspace is mapped to ctrl-h 79 | @kb.add('c-h', filter=hasattr(app, 'on_backspace')) 80 | def backspace(event: KeyPressEvent): 81 | event.app.on_backspace() # type: ignore 82 | 83 | # This Method is used nowhere, so it is commented out. If there is ever a need to use it, it is still there. 84 | # # ctrl-backspace is mapped to ctrl-w 85 | # @kb.add('c-w', filter=hasattr(app, 'on_ctrl_backspace')) 86 | # def ctrl_backspace(event: KeyPressEvent): 87 | # event.app.on_ctrl_backspace() # type: ignore 88 | 89 | @kb.add('', filter=hasattr(app, 'on_key')) 90 | def wildcard(event: KeyPressEvent): 91 | # wildcard function, used for prompts which need standard key presses like numbers or characters 92 | event.app.on_key([key.key for key in event.key_sequence]) # type: ignore 93 | 94 | return kb 95 | -------------------------------------------------------------------------------- /ItsPrompt/objects/prompts/option.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | 3 | 4 | @dataclass 5 | class Option: 6 | pass 7 | -------------------------------------------------------------------------------- /ItsPrompt/objects/prompts/options_with_separator.py: -------------------------------------------------------------------------------- 1 | from typing import Iterator, TypeVar, Generic 2 | 3 | from ItsPrompt.objects.prompts.option import Option 4 | from ItsPrompt.objects.prompts.separator import Separator 5 | 6 | OptionOrSeparator = TypeVar("OptionOrSeparator", bound=Option | Separator) 7 | 8 | 9 | class OptionsWithSeparator(Generic[OptionOrSeparator], list): 10 | """ 11 | The `OptionsWithSeparator` class is a subclass of the built-in list class in Python. 12 | 13 | This class is designed to hold a list of arguments, but with a specific behavior: 14 | it filters out all `Separator`s. The original list including the `Separator` instances is stored 15 | in the `with_separators` attribute. 16 | 17 | :ivar with_separators: A list that holds the original arguments passed including all `Separator`s. 18 | 19 | :param args: The items for the list 20 | 21 | Note: 22 | These `Separator` instances are excluded from the parent list object. To access the 23 | original list that includes the `Separator` instances, use the `with_separators` 24 | attribute. 25 | 26 | """ 27 | 28 | def __init__(self, *args: OptionOrSeparator | Separator): 29 | self.with_separators = list(args) 30 | super().__init__([x for x in args if not isinstance(x, Separator)]) 31 | 32 | def with_separators_enumerate(self) -> Iterator[tuple[int, OptionOrSeparator | Separator]]: 33 | """ 34 | Enumerate the items in the `OptionsWithSeperator` list, including separators. 35 | 36 | For every Separator, the index is not increased and instead -1 is returned. 37 | 38 | :return: An iterator that yields tuples containing the index and the item. 39 | """ 40 | index = 0 41 | 42 | for item in self.with_separators: 43 | if type(item) is Separator: 44 | yield -1, item 45 | else: 46 | yield index, item 47 | index += 1 48 | -------------------------------------------------------------------------------- /ItsPrompt/objects/prompts/separator.py: -------------------------------------------------------------------------------- 1 | class Separator: 2 | """ 3 | Used for creating distinctive sections in the prompt types: 4 | 5 | - :meth:`~ItsPrompt.prompt.Prompt.select` 6 | - :meth:`~ItsPrompt.prompt.Prompt.raw_select` 7 | - :meth:`~ItsPrompt.prompt.Prompt.checkbox` 8 | - :meth:`~ItsPrompt.prompt.Prompt.expand` 9 | 10 | It is purely cosmetic. 11 | """ 12 | 13 | def __init__(self, label: str): 14 | """ 15 | Initializes an instance of the Separator class with the given text. 16 | 17 | :param label: The text to be stored. 18 | """ 19 | self.label = label 20 | -------------------------------------------------------------------------------- /ItsPrompt/objects/prompts/type.py: -------------------------------------------------------------------------------- 1 | from ItsPrompt.objects.prompts.separator import Separator 2 | 3 | OptionWithId = tuple[str, str, str | None] 4 | """ 5 | The OptionWithId tuple is used to store an option, its id and an optional key (for expand prompt). 6 | 7 | For all prompts excluding :meth:`~ItsPrompt.prompt.Prompt.expand`, the tuple is structured as follows 8 | 9 | - The first element is the displayed option 10 | - The second element is the id of the option 11 | 12 | For the :meth:`~ItsPrompt.prompt.Prompt.expand` prompt, the tuple is structured as follows: 13 | 14 | - The first element is the key of the option 15 | - The second element is the displayed option 16 | - The third element is the id of the option 17 | """ 18 | 19 | OptionsList = tuple[str | OptionWithId | Separator, ...] 20 | """ 21 | Different types of options that can be used in a prompt. 22 | 23 | Can be given by either: 24 | 25 | - A :class:`str`, which is the displayed option and its id 26 | - A :class:`tuple` containing the displayed option, its id and an optional key (for expand prompt) 27 | - A :class:`~ItsPrompt.objects.prompts.separator.Separator` instance 28 | """ 29 | 30 | TablePromptList = list[list[str]] 31 | """ 32 | A type hint for the :class:`list` structure used to represent a table prompt. 33 | 34 | Each inner :class:`list` represents a row in the table. 35 | 36 | The cells are represented by the :class:`str` type. 37 | 38 | """ 39 | 40 | TablePromptDict = dict[str, list[str]] 41 | """ 42 | A type hint for the :class:`dict` structure used to represent a table prompt. 43 | 44 | The keys are the column names and the values are the cells in the column. 45 | """ 46 | 47 | CompletionDict = dict[str, "CompletionDict | None"] 48 | """ 49 | A type hint for the :class:`dict` structure used to represent the completion dictionary. 50 | """ 51 | -------------------------------------------------------------------------------- /ItsPrompt/objects/table/table_base.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | from typing import Generator 3 | 4 | 5 | class TableDataBase(ABC): # pragma: no cover 6 | 7 | @property 8 | @abstractmethod 9 | def row_count(self) -> int: 10 | """Returns the number of rows in the table, including the header""" 11 | pass 12 | 13 | @property 14 | @abstractmethod 15 | def col_count(self) -> int: 16 | """Returns the number of columns in the table""" 17 | pass 18 | 19 | @property 20 | @abstractmethod 21 | def columns(self) -> tuple[str, ...]: 22 | """Returns the column names in the table (the first row, headers)""" 23 | pass 24 | 25 | @abstractmethod 26 | def items(self) -> Generator[tuple[str, tuple[str, ...]], None, None]: 27 | """Gets all the items in the table (each tuple contains the header and the values)""" 28 | pass 29 | 30 | @abstractmethod 31 | def get_item_at(self, row: int, col: int) -> str: 32 | """Returns the item at the given position in the table""" 33 | pass 34 | 35 | @abstractmethod 36 | def set_item_at(self, row: int, col: int, val: str) -> None: 37 | """Sets the item at the given position in the table""" 38 | pass 39 | 40 | @abstractmethod 41 | def add_key(self, row: int, col: int, key: str): 42 | """Adds the key to the given cell""" 43 | pass 44 | 45 | @abstractmethod 46 | def del_key(self, row: int, col: int): 47 | """Deletes the key from the given cell""" 48 | pass 49 | 50 | @abstractmethod 51 | def get_column_location(self, val: str) -> int: 52 | """Returns the location of the column in the table""" 53 | pass 54 | 55 | @abstractmethod 56 | def get_data(self): 57 | """Returns the actual data""" 58 | pass 59 | -------------------------------------------------------------------------------- /ItsPrompt/objects/table/table_df.py: -------------------------------------------------------------------------------- 1 | from typing import Generator 2 | 3 | from pandas import DataFrame 4 | 5 | from .table_base import TableDataBase 6 | 7 | 8 | class TableDataFromDF(TableDataBase): 9 | 10 | def __init__(self, data: DataFrame) -> None: 11 | self.data = data.astype("str") # convert all values to str 12 | self.data = self.data.rename(columns=lambda x: str(x)) # convert all headers to str 13 | 14 | @property 15 | def row_count(self) -> int: 16 | return len(self.data) + 1 17 | 18 | @property 19 | def col_count(self) -> int: 20 | return len(self.data.columns) 21 | 22 | @property 23 | def columns(self) -> tuple[str, ...]: 24 | return tuple(self.data.columns) 25 | 26 | def items(self) -> Generator[tuple[str, tuple[str, ...]], None, None]: 27 | for header, values in self.data.items(): 28 | yield str(header), tuple(values) 29 | 30 | def get_item_at(self, row: int, col: int) -> str: 31 | return self.data.iat[row, col] 32 | 33 | def set_item_at(self, row: int, col: int, val: str) -> None: 34 | self.data.iat[row, col] = val 35 | 36 | def add_key(self, row: int, col: int, key: str): 37 | new = self.get_item_at(row, col) + key 38 | self.set_item_at(row, col, new) 39 | 40 | def del_key(self, row: int, col: int): 41 | new = self.get_item_at(row, col)[:-1] 42 | self.set_item_at(row, col, new) 43 | 44 | def get_column_location(self, val: str) -> int: 45 | return self.data.columns.get_loc(val) 46 | 47 | def get_data(self) -> DataFrame: 48 | return self.data 49 | -------------------------------------------------------------------------------- /ItsPrompt/objects/table/table_dict.py: -------------------------------------------------------------------------------- 1 | from typing import Generator 2 | 3 | from .table_base import TableDataBase 4 | from ..prompts.type import TablePromptDict 5 | 6 | 7 | class TableDataFromDict(TableDataBase): 8 | 9 | def __init__(self, data: TablePromptDict) -> None: 10 | # make sure every list has same length 11 | lengths = set([len(t) for t in data.values()]) 12 | if len(lengths) != 1: 13 | raise ValueError("Dictionary columns (lists) must be same length!") 14 | 15 | self.data = data 16 | 17 | @property 18 | def row_count(self) -> int: 19 | return len(next(iter(self.data.values()))) + 1 20 | 21 | @property 22 | def col_count(self) -> int: 23 | return len(self.data) 24 | 25 | @property 26 | def columns(self) -> tuple[str, ...]: 27 | return tuple(self.data.keys()) 28 | 29 | def items(self) -> Generator[tuple[str, tuple[str, ...]], None, None]: 30 | for header, values in self.data.items(): 31 | yield str(header), tuple(values) 32 | 33 | def get_item_at(self, row: int, col: int) -> str: 34 | # get the name of the column to edit (header) 35 | col_name = self.columns[col] 36 | 37 | return self.data[col_name][row] 38 | 39 | def set_item_at(self, row: int, col: int, val: str) -> None: 40 | # get the name of the column to edit (header) 41 | col_name = self.columns[col] 42 | 43 | self.data[col_name][row] = val 44 | 45 | def add_key(self, row: int, col: int, key: str): 46 | new = self.get_item_at(row, col) + key 47 | self.set_item_at(row, col, new) 48 | 49 | def del_key(self, row: int, col: int): 50 | new = self.get_item_at(row, col)[:-1] 51 | self.set_item_at(row, col, new) 52 | 53 | def get_column_location(self, val: str) -> int: 54 | return self.columns.index(val) 55 | 56 | def get_data(self) -> TablePromptDict: 57 | return self.data 58 | -------------------------------------------------------------------------------- /ItsPrompt/objects/table/table_list.py: -------------------------------------------------------------------------------- 1 | from .table_dict import TableDataFromDict 2 | from ..prompts.type import TablePromptDict, TablePromptList 3 | 4 | 5 | class TableDataFromList(TableDataFromDict): 6 | 7 | def __init__(self, data: TablePromptList): 8 | # convert list to dict 9 | dict_data: TablePromptDict = {f"{col}": rows for col, rows in enumerate(data)} 10 | 11 | super().__init__(dict_data) 12 | 13 | def get_data(self) -> TablePromptList: # type: ignore 14 | # convert dict to list 15 | list_data: TablePromptList = list(self.data.values()) 16 | 17 | return list_data 18 | -------------------------------------------------------------------------------- /ItsPrompt/prompt.py: -------------------------------------------------------------------------------- 1 | """ 2 | ItsPrompt 3 | ========= 4 | 5 | created by ItsNameless 6 | 7 | :copyright: (c) 2023-present ItsNameless 8 | :license: MIT, see LICENSE for more details. 9 | """ 10 | 11 | # mypy: disable-error-code=return-value 12 | 13 | from typing import Callable, TYPE_CHECKING, Union 14 | 15 | from prompt_toolkit import HTML 16 | from prompt_toolkit.buffer import Buffer 17 | from prompt_toolkit.completion import Completer 18 | from prompt_toolkit.layout.containers import ( 19 | Float, 20 | FloatContainer, 21 | HSplit, 22 | VSplit, 23 | Window, 24 | ) 25 | from prompt_toolkit.layout.controls import BufferControl, FormattedTextControl 26 | from prompt_toolkit.layout.layout import Layout 27 | from prompt_toolkit.layout.menus import ( 28 | CompletionsMenu, 29 | MultiColumnCompletionsMenu, 30 | ) 31 | 32 | from .data.style import PromptStyle, _convert_style, default_style 33 | from .keyboard_handler import generate_key_bindings 34 | from .objects.prompts.type import CompletionDict, TablePromptDict, TablePromptList, OptionsList 35 | from .prompts.checkbox import CheckboxPrompt 36 | from .prompts.confirm import ConfirmPrompt 37 | from .prompts.expand import ExpandPrompt 38 | from .prompts.input import InputPrompt 39 | from .prompts.raw_select import RawSelectPrompt 40 | from .prompts.select import SelectPrompt 41 | from .prompts.table import TablePrompt 42 | 43 | if TYPE_CHECKING: # pragma: no cover 44 | from pandas import DataFrame 45 | 46 | 47 | class Prompt: 48 | """ 49 | # Modern python prompter 50 | 51 | This tool is used to ask the user questions, the fancy way. 52 | 53 | Usage: 54 | ``` 55 | answer = Prompt.checkbox('option 1', 'option 2') 56 | ``` 57 | """ 58 | 59 | @classmethod 60 | def select( 61 | cls, 62 | question: str, 63 | options: OptionsList, 64 | default: str | None = None, 65 | disabled: tuple[str, ...] | None = None, 66 | style: PromptStyle | None = None, 67 | ) -> str: 68 | """ 69 | Ask the user for selecting **one** of the given `options`. 70 | 71 | This method shows the question alongside the `options` as a nice list. The user has the ability to use the 72 | up, down and enter keys to navigate between the options and select the one that is right. 73 | 74 | The `options` are either a string, which is used as the display value and the id, or a tuple[str, str], 75 | where the first string is the display value and the second is the option's id. 76 | 77 | :param question: The question to display 78 | :param options: A list of possible options 79 | :param default: The id of the default option to select (empty or None if the first should be default), defaults to None 80 | :param disabled: A list of ids, which should be disabled by default (empty if None) 81 | :param style: A separate style to style the prompt (empty or None for default style), defaults to None 82 | :raises KeyboardInterrupt: When the user presses ctrl-c, `KeyboardInterrupt` will be raised 83 | :return: The id of the selected option 84 | :rtype: str 85 | """ 86 | app = SelectPrompt( 87 | question, 88 | options, 89 | default, 90 | disabled, 91 | layout=Layout( 92 | HSplit( 93 | [ 94 | Window(FormattedTextControl(), always_hide_cursor=True), 95 | Window( 96 | FormattedTextControl(HTML('Use UP, DOWN to select, ENTER to submit')), 97 | char=' ', 98 | style='class:tooltip', 99 | height=1 100 | ) 101 | ] 102 | ) 103 | ), 104 | key_bindings=generate_key_bindings(SelectPrompt), 105 | erase_when_done=True, 106 | style=_convert_style(style) if style else _convert_style(default_style), 107 | ) 108 | ans = app.prompt() 109 | if ans == None: 110 | raise KeyboardInterrupt() 111 | return ans 112 | 113 | @classmethod 114 | def raw_select( 115 | cls, 116 | question: str, 117 | options: OptionsList, 118 | default: str | None = None, 119 | disabled: tuple[str, ...] | None = None, 120 | allow_keyboard: bool = False, 121 | style: PromptStyle | None = None, 122 | ) -> str: 123 | """ 124 | Ask the user for selecting **one** of the given `options`. 125 | 126 | This method shows the question alongside the `options` as a nice list. The user needs to type the index of 127 | the answer. If `allow_keyboard` is given, the user may use the keyboard as in the `select()` method. 128 | 129 | The `options` are either a string, which is used as the display value and the id, or a tuple[str, str], 130 | where the first string is the display value and the second is the option's id. 131 | 132 | :param question: The question to display 133 | :param options: A list of possible options 134 | :param default: The id of the default option to select (empty or None if the first should be default), defaults to None 135 | :param disabled: A list of ids, which should be disabled by default (empty if None) 136 | :param allow_keyboard: Whether the user should be able to select the answer with up and down, defaults to False 137 | :param style: A separate style to style the prompt (empty or None for default style), defaults to None 138 | :raises KeyboardInterrupt: When the user presses ctrl-c, `KeyboardInterrupt` will be raised 139 | :return: The id of the selected option 140 | :rtype: str 141 | """ 142 | app = RawSelectPrompt( 143 | question, 144 | options, 145 | default, 146 | disabled, 147 | allow_keyboard, 148 | layout=Layout( 149 | HSplit( 150 | [ 151 | Window(FormattedTextControl(), always_hide_cursor=True), 152 | Window( 153 | FormattedTextControl(HTML('Type the INDEX of your selection, ENTER to submit')), 154 | char=' ', 155 | style='class:tooltip', 156 | height=1 157 | ) 158 | ] 159 | ) 160 | ), 161 | key_bindings=generate_key_bindings(RawSelectPrompt), 162 | erase_when_done=True, 163 | style=_convert_style(style) if style else _convert_style(default_style), 164 | ) 165 | ans = app.prompt() 166 | if ans is None: 167 | raise KeyboardInterrupt() 168 | return ans 169 | 170 | @classmethod 171 | def expand( 172 | cls, 173 | question: str, 174 | options: OptionsList, 175 | default: str | None = None, 176 | disabled: tuple[str, ...] | None = None, 177 | allow_keyboard: bool = False, 178 | style: PromptStyle | None = None, 179 | ) -> str: 180 | """ 181 | Ask the user for selecting **one** of the given `options`. 182 | 183 | The user needs to type the key of the option. If the user types `h`, all options will be shown. 184 | 185 | The `options` are either a string, where `s[0]` will be the key to select and the string will be used as name 186 | and id, or a tuple[str, str, str] where `t[0]` will be the key, `t[1]` the name and `t[2]` the id of the option. 187 | 188 | Every key must be a unique ascii character and of length 1, and there may not be a key assigned to `h`. 189 | 190 | :param question: The question to display 191 | :param options: A list of possible options 192 | :param default: The id of the default option to select (empty or None if `h` should be default), defaults to None 193 | :param disabled: A list of ids, which should be disabled by default (empty if None) 194 | :param allow_keyboard: Whether the user should be able to select the answer with up and down, defaults to False 195 | :param style: A separate style to style the prompt (empty or None for default style), defaults to None 196 | :raises KeyboardInterrupt: When the user presses ctrl-c, `KeyboardInterrupt` will be raised 197 | :return: The id of the selected option 198 | :rtype: str 199 | """ 200 | app = ExpandPrompt( 201 | question, 202 | options, 203 | default, 204 | disabled, 205 | allow_keyboard, 206 | layout=Layout( 207 | HSplit( 208 | [ 209 | Window(FormattedTextControl(), always_hide_cursor=True), 210 | Window( 211 | FormattedTextControl( 212 | HTML('Type the KEY for your selection, ENTER to submit (use h to show all options)') 213 | ), 214 | char=' ', 215 | style='class:tooltip', 216 | height=1 217 | ) 218 | ] 219 | ) 220 | ), 221 | key_bindings=generate_key_bindings(ExpandPrompt), 222 | erase_when_done=True, 223 | style=_convert_style(style) if style else _convert_style(default_style), 224 | ) 225 | ans = app.prompt() 226 | if ans is None: 227 | raise KeyboardInterrupt() 228 | return ans 229 | 230 | @classmethod 231 | def checkbox( 232 | cls, 233 | question: str, 234 | options: OptionsList, 235 | pointer_at: int | None = None, 236 | default_checked: tuple[str, ...] | None = None, 237 | disabled: tuple[str, ...] | None = None, 238 | min_selections: int = 0, 239 | style: PromptStyle | None = None, 240 | ) -> list[str]: 241 | """ 242 | Ask the user for selecting **multiple** of the given `options`. 243 | 244 | The `options` will be shown as a nice list. The user may navigate with up and down, select or deselect with 245 | space and submit with enter. 246 | 247 | The `options` are either a string, which is used as the display value and the id, or a tuple[str, str], 248 | where the first string is the display value and the second is the option's id. 249 | 250 | :param question: The question to display 251 | :param options: A list of possible options 252 | :param pointer_at: A 0-indexed value, where the pointer should start (0 if None), defaults to None 253 | :param default_checked: A list of ids, which should be checked by default (empty if None) 254 | :param disabled: A list of ids, which should be disabled by default (empty if None) 255 | :param min_selections: A minimum amount of options that need to be checked before submitting (prohibits the user of submitting, if not enough are checked; 0 if None) 256 | :param style: A separate style to style the prompt (empty or None for default style), defaults to None 257 | :raises KeyboardInterrupt: When the user presses ctrl-c, `KeyboardInterrupt` will be raised 258 | :return: The ids of the selected options 259 | :rtype: list[str] 260 | """ 261 | app = CheckboxPrompt( 262 | question, 263 | options, 264 | pointer_at, 265 | default_checked, 266 | disabled, 267 | min_selections, 268 | layout=Layout( 269 | HSplit( 270 | [ 271 | Window(FormattedTextControl(), always_hide_cursor=True), 272 | Window( 273 | FormattedTextControl( 274 | HTML('Use UP, DOWN to change selection, SPACE to select, ENTER to submit') 275 | ), 276 | char=' ', 277 | style='class:tooltip', 278 | height=1 279 | ) 280 | ] 281 | ) 282 | ), 283 | key_bindings=generate_key_bindings(CheckboxPrompt), 284 | erase_when_done=True, 285 | style=_convert_style(style) if style else _convert_style(default_style), 286 | ) 287 | ans = app.prompt() 288 | if ans == None: 289 | raise KeyboardInterrupt() 290 | return ans 291 | 292 | @classmethod 293 | def confirm( 294 | cls, 295 | question: str, 296 | default: bool | None = None, 297 | style: PromptStyle | None = None, 298 | ) -> bool: 299 | """ 300 | Ask the user for confirming or denying your prompt. 301 | 302 | The user needs to type "y", "n" or enter (only if default is given). 303 | 304 | If `default` is `True`, the prompt will be in the style of (Y/n). 305 | 306 | If `default` is `False`, the prompt will be in the style of (n/Y). 307 | 308 | If `default` is `None` (or not given), the prompt will be in the style of (y/n). In this case, the user may 309 | not use enter to submit the default, as there is no default given. 310 | 311 | :param question: The question to display 312 | :type question: str 313 | :param default: The default answer to select when pressing enter, defaults to None 314 | :type default: bool | None, optional 315 | :param style: A separate style to style the prompt (empty or None for default style), defaults to None 316 | :type style: PromptStyle | None, optional 317 | :raises KeyboardInterrupt: When the user presses ctrl-c, `KeyboardInterrupt` will be raised 318 | :return: Whether the user selected "y" or "n" 319 | :rtype: bool 320 | """ 321 | app = ConfirmPrompt( 322 | question, 323 | default, 324 | layout=Layout( 325 | HSplit( 326 | [ 327 | Window(FormattedTextControl(), always_hide_cursor=True), 328 | Window( 329 | FormattedTextControl(HTML('Press Y or N, ENTER if default value is available')), 330 | char=' ', 331 | style='class:tooltip', 332 | height=1 333 | ) 334 | ] 335 | ) 336 | ), 337 | key_bindings=generate_key_bindings(ConfirmPrompt), 338 | erase_when_done=True, 339 | style=_convert_style(style) if style else _convert_style(default_style), 340 | ) 341 | 342 | ans = app.prompt() 343 | if ans == None: 344 | raise KeyboardInterrupt() 345 | return ans 346 | 347 | @classmethod 348 | def input( 349 | cls, 350 | question: str, 351 | default: str | None = None, 352 | multiline: bool = False, 353 | show_symbol: str | None = None, 354 | validate: Callable[[str], str | bool | None] | None = None, 355 | completions: list[str] | CompletionDict | None = None, 356 | completer: Completer | None = None, 357 | completion_show_multicolumn: bool = False, 358 | style: PromptStyle | None = None, 359 | ) -> str: 360 | """ 361 | Ask the user for typing an input. 362 | 363 | If :data:`default` is given, it will be returned if enter was pressed and no input was given by the user. If the user 364 | writes an input, the :data:`default` will be overwritten. 365 | 366 | If :data:`multiline` is activated, enter will not submit, but rather create a newline. Use ``alt+enter`` to submit. 367 | 368 | If :data:`show_symbol` is given, all chars (except newlines) will be replaced with this character in the interface. 369 | The result will still be the input the user typed, it just will not appear in the CLI. This is useful for 370 | password inputs. 371 | 372 | :data:`validate` takes a function which receives a :class:`str` (the current input of the user) and may 373 | return :class:`None`, a :class:`str` or simply a boolean value. 374 | 375 | If the function returns :class:`None` (or ``True``), the prompt may assume that the input is 376 | valid. 377 | 378 | If it returns a :class:`str`, this will be the error shown to the user. If it returns ``False``, the error 379 | shown will simply be a general error statement without additional information. The user will not be able to 380 | submit the input, if :data:`validate` returns an error. 381 | 382 | :data:`completions` may be a list of possible completion strings or a nested dictionary where the key is a 383 | completion string and the value is a new dict in the same style (more in the README.md). 384 | 385 | You can use your own :class:`Completer` as well (more in the README.md). 386 | 387 | :data:`completions` **and** :data:`completer` **are mutually exclusive!** You may not use both. If you use a :data:`completer`, you can not use 388 | :data:`show_symbol`! 389 | 390 | :param question: The question to display 391 | :type question: str 392 | :param default: The default value to fill in, defaults to None 393 | :type default: str | None, optional 394 | :param multiline: Whether to allow the user to type multiple lines, defaults to False 395 | :type multiline: bool, optional 396 | :param show_symbol: A symbol to show instead of the users input, defaults to None 397 | :type show_symbol: str | None, optional 398 | :param validate: A function to check the users input in real-time, defaults to None 399 | :type validate: Callable[[str], str | bool | None] | None, optional 400 | :param completions: The completions to use, defaults to None 401 | :type completions: list[str] | CompletionDict | None, optional 402 | :param completer: A completer to use, defaults to None 403 | :type completer: Completer | None, optional 404 | :param completion_show_multicolumn: if True, shows completions as multiple columns, defaults to False 405 | :type completion_show_multicolumn: bool, optional 406 | :param style: A separate style to style the prompt (empty or None for default style), defaults to None 407 | :type style: PromptStyle | None, optional 408 | :raises KeyboardInterrupt: When the user presses ctrl-c, :class:`KeyboardInterrupt` will be raised 409 | :return: The input of the user 410 | :rtype: str 411 | """ 412 | 413 | # extracting the body, so we can display a floating auto completion field 414 | body = HSplit( 415 | [ 416 | VSplit( 417 | [ 418 | Window( 419 | FormattedTextControl(), 420 | always_hide_cursor=True, 421 | dont_extend_width=True, 422 | ), 423 | Window(BufferControl(Buffer(complete_while_typing=True))), 424 | # the completer will be passed in the Application class 425 | ] 426 | ), 427 | Window( 428 | FormattedTextControl(HTML(f'Type your answer, {"ALT+ENTER" if multiline else "ENTER"} to submit')), 429 | char=' ', 430 | style='class:tooltip', 431 | height=1, 432 | ) 433 | ] 434 | ) 435 | 436 | app = InputPrompt( 437 | question, 438 | default, 439 | multiline, 440 | show_symbol, 441 | validate, 442 | completions, 443 | completer, 444 | layout=Layout( 445 | FloatContainer( 446 | content=body, 447 | floats=[ 448 | Float( 449 | MultiColumnCompletionsMenu(show_meta=False) 450 | if completion_show_multicolumn else CompletionsMenu(), 451 | xcursor=True, 452 | ycursor=True, 453 | ) 454 | ] 455 | ) 456 | ), 457 | key_bindings=generate_key_bindings(InputPrompt), 458 | erase_when_done=True, 459 | style=_convert_style(style) if style else _convert_style(default_style), 460 | ) 461 | 462 | ans = app.prompt() 463 | if ans is None: 464 | raise KeyboardInterrupt() 465 | return ans 466 | 467 | @classmethod 468 | def table( 469 | cls, 470 | question: str, 471 | data: Union["DataFrame", TablePromptDict, TablePromptList], 472 | style: PromptStyle | None = None, 473 | ) -> Union["DataFrame", TablePromptDict, TablePromptList]: 474 | """ 475 | Ask the user for filling out the displayed table. 476 | 477 | This method shows the question alongside a table, which the user may navigate with the arrow keys. The user 478 | can use the up, down and enter keys to navigate between the options and change the text in 479 | each cell. 480 | 481 | The `data` is either a :class:`pandas.DataFrame`, a :class:`list` or a :class:`dict` (more in the README.md). 482 | 483 | :param question: The question to display 484 | :type question: str 485 | :param data: The data to display 486 | :type data: DataFrame | TablePromptDict | TablePromptList 487 | :param style: A separate style to style the prompt (empty or None for default style), defaults to None 488 | :type style: PromptStyle | None, optional 489 | :raises KeyboardInterrupt: When the user presses ctrl-c, `KeyboardInterrupt` will be raised 490 | :return: The id of the selected option 491 | :rtype: DataFrame | TablePromptDict | TablePromptList 492 | """ 493 | app = TablePrompt( 494 | question, 495 | data, 496 | layout=Layout( 497 | HSplit( 498 | [ 499 | Window(FormattedTextControl()), 500 | Window( 501 | FormattedTextControl( 502 | HTML( 503 | 'Use UP, DOWN, LEFT, RIGHT to select a cell, TYPE to add char, BACKSPACE to ' 504 | 'delete char, ENTER to submit' 505 | ) 506 | ), 507 | char=' ', 508 | style='class:tooltip', 509 | height=1 510 | ) 511 | ] 512 | ) 513 | ), 514 | key_bindings=generate_key_bindings(TablePrompt), 515 | erase_when_done=True, 516 | style=_convert_style(style) if style else _convert_style(default_style), 517 | ) 518 | ans = app.prompt() 519 | # if type(ans) is type(None): 520 | if ans is None: 521 | raise KeyboardInterrupt() 522 | return ans # type: ignore 523 | -------------------------------------------------------------------------------- /ItsPrompt/prompts/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheItsProjects/ItsPrompt/b2f959dba2337775420e1cd525002e1df72f8c99/ItsPrompt/prompts/__init__.py -------------------------------------------------------------------------------- /ItsPrompt/prompts/checkbox.py: -------------------------------------------------------------------------------- 1 | from prompt_toolkit import Application, HTML 2 | from prompt_toolkit.layout.containers import Window 3 | from prompt_toolkit.layout.controls import FormattedTextControl 4 | 5 | from ..data.checkbox import process_data 6 | from ..objects.prompts.separator import Separator 7 | from ..objects.prompts.type import OptionsList 8 | 9 | 10 | class CheckboxPrompt(Application): 11 | CHECKED_SIGN = '\u25cf' 12 | UNCHECKED_SIGN = '\u25cb' 13 | 14 | def __init__( 15 | self, 16 | question: str, 17 | options: OptionsList, 18 | pointer_at: int | None = None, 19 | default_checked: tuple[str, ...] | None = None, 20 | disabled: tuple[str, ...] | None = None, 21 | min_selections: int = 0, 22 | *args, 23 | **kwargs, 24 | ): 25 | super().__init__(*args, **kwargs) 26 | 27 | # get prompt content box 28 | self.prompt_content: FormattedTextControl = self.layout.container.get_children()[0].content # type: ignore 29 | 30 | # save toolbar window and box (for showing errors) 31 | self.toolbar_window: Window = self.layout.container.get_children()[1] # type: ignore 32 | self.toolbar_content: FormattedTextControl = self.layout.container.get_children()[1].content # type: ignore 33 | 34 | # save standard toolbar content 35 | self.toolbar_content_default_text = self.toolbar_content.text 36 | 37 | # save whether error is currently shown 38 | self.is_error = False 39 | 40 | # save question 41 | self.question = question 42 | 43 | # save min selections 44 | self.min_selections = min_selections 45 | 46 | # process options 47 | self.options = process_data(options) 48 | 49 | # default check default option (or none if not given) 50 | if default_checked is not None: 51 | was_set = 0 # Keeping track of options which where selected. 52 | # If was_set is not as big as len(default_checked), 53 | # then we know that there was an invalid option which could not be checked, 54 | # so we raise an error. 55 | for option in self.options: 56 | if option.id in default_checked: 57 | option.is_selected = True 58 | was_set += 1 59 | 60 | if was_set != len(default_checked): 61 | raise ValueError('At least one of the given default_checked values is invalid.') 62 | 63 | # disable options (or none if not given) 64 | if disabled is not None: 65 | was_set = 0 # Keeping track of options which where disabled. 66 | # If was_set is not as big as len(disabled), 67 | # then we know that there was an invalid option which could not be disabled, 68 | # so we raise an error. 69 | for option in self.options: 70 | if option.id in disabled: 71 | option.is_disabled = True 72 | was_set += 1 73 | 74 | if was_set != len(disabled): 75 | raise ValueError("At least one of the given disabled values is invalid.") 76 | 77 | # set pointer selection 78 | self.selection = pointer_at if pointer_at else 0 79 | 80 | # if the current selection is disabled, we will skip it 81 | while self.options[self.selection].is_disabled: 82 | self.selection = (self.selection + 1) % len(self.options) 83 | 84 | def update(self): 85 | """update prompt content""" 86 | content = f'[?] {self.question}:' 87 | 88 | for i, option in self.options.with_separators_enumerate(): 89 | 90 | if type(option) is Separator: 91 | content += f"\n{option.label}" 92 | continue 93 | 94 | disabled = ("", "") if option.is_disabled else ("", "") # type: ignore 95 | # Disabled tags will only be inserted if the option is disabled, 96 | # otherwise there will only be an empty string inserted. 97 | selected = self.__class__.CHECKED_SIGN if option.is_selected else self.__class__.UNCHECKED_SIGN # type: ignore 98 | 99 | if i == self.selection: 100 | content += f"\n{disabled[0]} > {selected} {option.name}{disabled[1]}" # type: ignore 101 | else: 102 | content += f"\n{disabled[0]}{disabled[1]}" # type: ignore 103 | 104 | self.prompt_content.text = HTML(content) 105 | 106 | # show error, if error should be shown, else show normal prompt 107 | if not self.is_error: 108 | # show normal prompt, change style to standard toolbar 109 | self.toolbar_content.text = self.toolbar_content_default_text 110 | self.toolbar_window.style = 'class:tooltip' 111 | else: # pragma: no cover 112 | # show error prompt and error style 113 | # the only error that might occur is that not enough options are selected 114 | self.toolbar_content.text = f'ERROR: a minimum of {self.min_selections} options need to be ' \ 115 | f'selected!' 116 | self.toolbar_window.style = 'class:error' 117 | 118 | def prompt(self) -> list[str] | None: 119 | """start the application, returns the return value""" 120 | self.update() 121 | out: list[str] | None = self.run() 122 | 123 | return out 124 | 125 | def on_up(self): 126 | """when up is pressed, the previous indexed option will be selected""" 127 | self.selection = (self.selection - 1) % len(self.options) 128 | 129 | # if the current selection is disabled, we will skip it 130 | while self.options[self.selection].is_disabled: 131 | self.selection = (self.selection - 1) % len(self.options) 132 | 133 | # reset error 134 | self.is_error = False 135 | 136 | self.update() 137 | 138 | def on_down(self): 139 | """when down is pressed, the next indexed option will be selected""" 140 | self.selection = (self.selection + 1) % len(self.options) 141 | 142 | # if the current selection is disabled, we will skip it 143 | while self.options[self.selection].is_disabled: 144 | self.selection = (self.selection + 1) % len(self.options) 145 | 146 | # reset error 147 | self.is_error = False 148 | 149 | self.update() 150 | 151 | def on_space(self): 152 | """when space is pressed, select or deselect current selection""" 153 | self.options[self.selection].is_selected = not self.options[self.selection].is_selected 154 | 155 | # reset error 156 | self.is_error = False 157 | 158 | self.update() 159 | 160 | def on_enter(self): 161 | # get selected options 162 | selected_options: list[str] = [] 163 | 164 | for option in self.options: 165 | if option.is_selected: 166 | selected_options.append(option.id) 167 | 168 | # make sure that enough options are selected 169 | if len(selected_options) < self.min_selections: # pragma: no cover 170 | # show error 171 | self.is_error = True 172 | self.update() 173 | return 174 | 175 | self.exit(result=selected_options) 176 | -------------------------------------------------------------------------------- /ItsPrompt/prompts/confirm.py: -------------------------------------------------------------------------------- 1 | from prompt_toolkit import Application, HTML 2 | from prompt_toolkit.layout.containers import Window 3 | from prompt_toolkit.layout.controls import FormattedTextControl 4 | 5 | 6 | class ConfirmPrompt(Application): 7 | 8 | def __init__( 9 | self, 10 | question: str, 11 | default: bool | None = None, 12 | *args, 13 | **kwargs, 14 | ): 15 | super().__init__(*args, **kwargs) 16 | 17 | # get prompt content box 18 | self.prompt_content: FormattedTextControl = self.layout.container.get_children()[0].content # type: ignore 19 | 20 | # save toolbar window and box (for showing errors) 21 | self.toolbar_window: Window = self.layout.container.get_children()[1] # type: ignore 22 | self.toolbar_content: FormattedTextControl = self.layout.container.get_children()[1].content # type: ignore 23 | 24 | # save standard toolbar content 25 | self.toolbar_content_default_text = self.toolbar_content.text 26 | 27 | # save whether error is currently shown 28 | self.is_error = False 29 | 30 | # save question 31 | self.question = question 32 | 33 | # save default selection 34 | self.default = default 35 | 36 | def update(self): 37 | """update prompt content""" 38 | content = f'[?] {self.question}: (' \ 39 | f'{"Y" if self.default == True else "y"}/{"N" if self.default == False else "n"})' 40 | 41 | self.prompt_content.text = HTML(content) 42 | 43 | # show error, if error should be shown, else show normal prompt 44 | if not self.is_error: 45 | # show normal prompt, change style to standard toolbar 46 | self.toolbar_content.text = self.toolbar_content_default_text 47 | self.toolbar_window.style = 'class:tooltip' 48 | else: # pragma: no cover 49 | # show error prompt and error style 50 | # the only error that might occur is that not enough options are selected 51 | self.toolbar_content.text = f'ERROR: a selection must be made!' 52 | self.toolbar_window.style = 'class:error' 53 | 54 | def prompt(self) -> bool | None: 55 | """start the application, returns the return value""" 56 | self.update() 57 | out: bool | None = self.run() 58 | 59 | return out 60 | 61 | def on_key(self, key_sequence: list[str]): 62 | """when Y or N is pressed, select value and submit""" 63 | key = key_sequence[0] 64 | 65 | # return if key is not an available key 66 | if not key in ['y', 'n']: 67 | return 68 | 69 | # exit with answer 70 | self.exit(result=key == 'y') 71 | 72 | def on_enter(self): 73 | # if no default is present, user is not able to just submit 74 | if self.default is None: # pragma: no cover 75 | self.is_error = True 76 | self.update() 77 | return 78 | 79 | self.exit(result=self.default) 80 | -------------------------------------------------------------------------------- /ItsPrompt/prompts/expand.py: -------------------------------------------------------------------------------- 1 | from prompt_toolkit import Application, HTML 2 | from prompt_toolkit.layout.controls import FormattedTextControl 3 | 4 | from ..data.expand import process_data 5 | from ..objects.prompts.separator import Separator 6 | from ..objects.prompts.type import OptionsList 7 | 8 | 9 | class ExpandPrompt(Application): 10 | 11 | def __init__( 12 | self, 13 | question: str, 14 | options: OptionsList, 15 | default: str | None = None, 16 | disabled: tuple[str, ...] | None = None, 17 | allow_keyboard: bool = False, 18 | *args, 19 | **kwargs, 20 | ): 21 | super().__init__(*args, **kwargs) 22 | 23 | # get prompt content box 24 | self.prompt_content: FormattedTextControl = self.layout.container.get_children()[0].content # type: ignore 25 | 26 | # save question 27 | self.question = question 28 | 29 | # save keyboard option 30 | self.allow_keyboard = allow_keyboard 31 | 32 | # save whether view is expanded or not 33 | self.is_expanded = False 34 | 35 | # process options 36 | self.options = process_data(options) 37 | 38 | # save a string with all keys 39 | self.keys = ''.join([option.key for option in self.options]) 40 | 41 | # save disabled keys 42 | self.disabled = [key[0] for key in disabled] if disabled else () 43 | 44 | # default select default option (or "help" if not given) 45 | if default is None: 46 | self.selection = 'h' 47 | 48 | else: 49 | for i, option in enumerate(self.options): 50 | if option.id == default: 51 | self.selection = option.key 52 | break 53 | else: 54 | raise ValueError('Default value is not a valid id.') 55 | 56 | # if default is disabled, raise an error 57 | if disabled and default in disabled: 58 | raise ValueError("Default value can not be disabled.") 59 | 60 | # disable options (or none if not given) 61 | if disabled is not None: 62 | was_set = 0 # Keeping track of options which where disabled. 63 | # If was_set is not as big as len(disabled), 64 | # then we know that there was an invalid option which could not be disabled, 65 | # so we raise an error. 66 | for option in self.options: 67 | if option.id in disabled: 68 | option.is_disabled = True 69 | was_set += 1 70 | 71 | if was_set != len(disabled): 72 | raise ValueError("At least one of the given disabled values is invalid.") 73 | 74 | def update(self): 75 | """update prompt content""" 76 | # question 77 | content = f'[?] {self.question}: ({self.keys})' 78 | 79 | # options, show only if it is expanded 80 | if self.is_expanded: 81 | for _, option in self.options.with_separators_enumerate(): 82 | 83 | if type(option) is Separator: 84 | content += f"\n{option.label}" 85 | continue 86 | 87 | disabled = ("", "") if option.is_disabled else ("", "") # type: ignore 88 | # Disabled tags will only be inserted if the option is disabled, 89 | # otherwise there will only be an empty string inserted. 90 | 91 | if option.key == self.selection: # type: ignore 92 | content += f'\n{disabled[0]} {option.key}) {option.name}{disabled[1]}' # type: ignore 93 | else: 94 | content += f'\n{disabled[0]}{disabled[1]}' # type: ignore 95 | 96 | # text 97 | content += f'\n Answer: {self.selection} ({self.options.get_option(self.selection).name})' # type: ignore 98 | 99 | self.prompt_content.text = HTML(content) 100 | 101 | def prompt(self) -> str | None: 102 | """start the application, returns the return value""" 103 | self.update() 104 | out: str | None = self.run() 105 | 106 | return out 107 | 108 | def on_up(self): 109 | """when up is pressed, the previous indexed option will be selected""" 110 | if not self.allow_keyboard: 111 | return 112 | 113 | self.selection = self.keys[(self.keys.index(self.selection) - 1) % len(self.keys)] 114 | 115 | # if the current selection is disabled, we will skip it 116 | while self.options[(self.keys.index(self.selection)) % len(self.keys)].is_disabled: 117 | self.selection = self.keys[(self.keys.index(self.selection) - 1) % len(self.keys)] 118 | 119 | self.update() 120 | 121 | def on_down(self): 122 | """when down is pressed, the next indexed option will be selected""" 123 | if not self.allow_keyboard: 124 | return 125 | 126 | self.selection = self.keys[(self.keys.index(self.selection) + 1) % len(self.keys)] 127 | 128 | # if the current selection is disabled, we will skip it 129 | while self.options[(self.keys.index(self.selection)) % len(self.keys)].is_disabled: 130 | self.selection = self.keys[(self.keys.index(self.selection) + 1) % len(self.keys)] 131 | 132 | self.update() 133 | 134 | def on_key(self, key_sequence: list[str]): 135 | """when an index is pressed, which is available to select, select this index""" 136 | key = key_sequence[0] 137 | 138 | # return if key is not an available key 139 | if key not in self.keys: 140 | return 141 | 142 | # return if key is disabled 143 | if key in self.disabled: 144 | return 145 | 146 | # only expand if h is pressed, otherwise change selection 147 | if key == 'h': 148 | self.is_expanded = not self.is_expanded 149 | else: 150 | self.selection = key 151 | 152 | self.update() 153 | 154 | def on_enter(self): 155 | # if current selection is h-key, change is_expanded 156 | # otherwise return selected id 157 | if self.selection == 'h': 158 | self.is_expanded = not self.is_expanded 159 | self.update() 160 | return 161 | 162 | # get selected id 163 | self.exit(result=self.options[self.keys.index(self.selection)].id) 164 | -------------------------------------------------------------------------------- /ItsPrompt/prompts/input.py: -------------------------------------------------------------------------------- 1 | from typing import Callable 2 | 3 | from prompt_toolkit import Application, HTML 4 | from prompt_toolkit.buffer import Buffer 5 | from prompt_toolkit.completion import ( 6 | Completer, 7 | FuzzyCompleter, 8 | FuzzyWordCompleter, 9 | NestedCompleter, 10 | ) 11 | from prompt_toolkit.layout.containers import Window 12 | from prompt_toolkit.layout.controls import FormattedTextControl 13 | from prompt_toolkit.layout.processors import PasswordProcessor 14 | 15 | from ..objects.prompts.type import CompletionDict 16 | 17 | 18 | class InputPrompt(Application): 19 | 20 | def __init__( 21 | self, 22 | question: str, 23 | default: str | None = None, 24 | multiline: bool = False, 25 | show_symbol: str | None = None, 26 | validate: Callable[[str], str | bool | None] | None = None, 27 | completions: list[str] | CompletionDict | None = None, 28 | completer: Completer | None = None, 29 | *args, 30 | **kwargs, 31 | ): 32 | super().__init__(*args, **kwargs) 33 | 34 | # temp body variable for making object accessing clearer 35 | _body = self.layout.container.content.get_children() # type: ignore 36 | _vsplit = _body[0].get_children() 37 | 38 | # get prompt content box 39 | self.prompt_content: FormattedTextControl = _vsplit[0].content # type: ignore 40 | 41 | # get input buffer 42 | self.buffer: Buffer = _vsplit[1].content.buffer # type: ignore 43 | 44 | # save toolbar window and box (for showing errors) 45 | self.toolbar_window: Window = _body[1] # type: ignore 46 | self.toolbar_content: FormattedTextControl = self.toolbar_window.content # type: ignore 47 | 48 | # save standard toolbar content 49 | self.toolbar_content_default_text = self.toolbar_content.text # type: ignore 50 | 51 | # add updating function to buffer change event 52 | self.buffer.on_text_changed.add_handler(lambda _: self.update()) 53 | 54 | # save whether error is currently shown 55 | self.is_error = False 56 | 57 | # save question 58 | self.question = question 59 | 60 | # save default input 61 | self.default = default 62 | 63 | # save whether multiline is enabled 64 | self.multiline = multiline 65 | 66 | # save show_symbol 67 | self.show_symbol = show_symbol 68 | 69 | # when show_symbol is given, use PasswordProcessor to show the symbol instead of the text 70 | if self.show_symbol: 71 | _vsplit[1].content.input_processors = [PasswordProcessor(self.show_symbol)] 72 | 73 | # save validator function 74 | self.validate = validate 75 | 76 | # save the completer 77 | self.completer: None | Completer = None 78 | 79 | if completions and completer: 80 | raise ValueError("completions and completer are mutually exclusive! Please use only one of them!") 81 | 82 | if show_symbol and (completions or completer): 83 | # symbol and completer are mutually exclusive 84 | raise ValueError('Completions are not compatible with show_symbol!') 85 | 86 | if completions: # pragma: no cover 87 | # a list or a dict of completions to use is given 88 | if type(completions) is list: 89 | # we use FuzzyWordCompleter 90 | self.completer = FuzzyWordCompleter(list(completions)) 91 | elif type(completions) is dict: 92 | # we use FuzzyCompleter with NestedCompleter 93 | self.completer = FuzzyCompleter(NestedCompleter.from_nested_dict(completions)) 94 | 95 | elif completer: # pragma: no cover 96 | # a self-created completer is given 97 | self.completer = completer 98 | 99 | # assign the created completer to the buffer 100 | if self.completer: # pragma: no cover 101 | self.buffer.completer = self.completer 102 | 103 | def update(self): # pragma: no cover 104 | """update prompt content""" 105 | content = f'[?] {self.question}: ' 106 | 107 | if self.default and self.buffer.text == '': 108 | content += f'{self.default}' 109 | 110 | self.prompt_content.text = HTML(content) 111 | 112 | # run validation and show error, if validation gives error 113 | if not self.validate: 114 | return 115 | 116 | validation_result = self.validate(self.buffer.text) 117 | 118 | if validation_result in (None, True): 119 | self.is_error = False 120 | else: 121 | # validation_result is either a string (which will be the displayed error) or False (if the user used a 122 | # lambda statement) 123 | self.is_error = True 124 | 125 | # error is either the error returned by the validation function or a default error, if the validation 126 | # returned False (in case it is a lambda) 127 | error = validation_result if type(validation_result) is str else "Please check your Input!" 128 | 129 | # show error, if error should be shown, else show normal prompt 130 | if not self.is_error: 131 | # show normal prompt, change style to standard toolbar 132 | self.toolbar_content.text = self.toolbar_content_default_text 133 | self.toolbar_window.style = 'class:tooltip' 134 | else: 135 | # show error prompt and error style 136 | self.toolbar_content.text = error 137 | self.toolbar_window.style = 'class:error' 138 | 139 | # run completion 140 | # Since we take over control of the buffer and the keyboard, the completion 141 | # (and all of its commands) need to be run manually every time we press a key. 142 | # This does not in any way change the user experience, 143 | # as it does the exact same thing the standard completer does. 144 | if self.is_running: 145 | self.buffer.start_completion() 146 | 147 | def prompt(self) -> str | None: 148 | """start the application, returns the return value""" 149 | self.update() 150 | out: str | None = self.run() 151 | 152 | return out 153 | 154 | def _submit(self): 155 | """method for submitting result, as this is done by two functions""" 156 | # if an error is currently shown, prevent submit 157 | if self.is_error: # pragma: no cover 158 | return 159 | 160 | # return buffer if given, else default if given, else empty string 161 | if self.buffer.text != '': 162 | self.exit(result=self.buffer.text) 163 | elif self.default: 164 | self.exit(result=self.default) 165 | else: 166 | self.exit(result='') 167 | 168 | def on_alt_enter(self): 169 | """if multiline is enabled, this will submit""" 170 | if self.multiline: 171 | self._submit() 172 | 173 | def on_enter(self): 174 | """either submit key or in multiline, append new line""" 175 | # run completion 176 | if self.is_running and self.buffer.complete_state and ( 177 | completion := self.buffer.complete_state.current_completion 178 | ): # pragma: no cover 179 | self.buffer.apply_completion(completion) 180 | 181 | if self.multiline: 182 | self.buffer.text += '\n' 183 | self.buffer.cursor_down() 184 | self.update() 185 | return 186 | 187 | self._submit() 188 | -------------------------------------------------------------------------------- /ItsPrompt/prompts/raw_select.py: -------------------------------------------------------------------------------- 1 | from prompt_toolkit import Application, HTML 2 | from prompt_toolkit.layout.controls import FormattedTextControl 3 | 4 | from ..data.select import process_data 5 | from ..objects.prompts.separator import Separator 6 | from ..objects.prompts.type import OptionsList 7 | 8 | 9 | class RawSelectPrompt(Application): 10 | 11 | def __init__( 12 | self, 13 | question: str, 14 | options: OptionsList, 15 | default: str | None = None, 16 | disabled: tuple[str, ...] | None = None, 17 | allow_keyboard: bool = False, 18 | *args, 19 | **kwargs, 20 | ): 21 | super().__init__(*args, **kwargs) 22 | 23 | # get prompt content box 24 | self.prompt_content: FormattedTextControl = self.layout.container.get_children()[0].content # type: ignore 25 | 26 | # save question 27 | self.question = question 28 | 29 | # self keyboard option 30 | self.allow_keyboard = allow_keyboard 31 | 32 | # process options 33 | self.options = process_data(options) 34 | 35 | # disable options (or none if not given) 36 | if disabled is not None: 37 | was_set = 0 # Keeping track of options which where disabled. 38 | # If was_set is not as big as len(disabled), 39 | # then we know that there was an invalid option which could not be disabled, 40 | # so we raise an error. 41 | for option in self.options: 42 | if option.id in disabled: 43 | option.is_disabled = True 44 | was_set += 1 45 | 46 | if was_set != len(disabled): 47 | raise ValueError("At least one of the given disabled values is invalid.") 48 | 49 | # default select default option (or first if not given) 50 | if default is not None: 51 | for i, option in enumerate(self.options): 52 | if option.id == default: 53 | if option.is_disabled: 54 | raise ValueError("Default value must not be disabled.") 55 | self.selection = i 56 | break 57 | else: 58 | raise ValueError('Default value is not a valid id.') 59 | else: 60 | self.selection = 0 61 | 62 | # if the current selection is disabled, we will skip it 63 | while self.options[self.selection].is_disabled: 64 | self.selection = (self.selection + 1) % len(self.options) 65 | 66 | def update(self): 67 | """update prompt content""" 68 | # question 69 | content = f'[?] {self.question}:' 70 | 71 | # options 72 | for i, option in self.options.with_separators_enumerate(): 73 | 74 | if type(option) is Separator: 75 | content += f"\n{option.label}" 76 | continue 77 | 78 | disabled = ("", "") if option.is_disabled else ("", "") # type: ignore 79 | # Disabled tags will only be inserted if the option is disabled, 80 | # otherwise there will only be an empty string inserted. 81 | 82 | if i == self.selection: 83 | content += f'\n{disabled[0]} {i + 1}) {option.name}{disabled[1]}' # type: ignore 84 | else: 85 | content += f'\n{disabled[0]}{disabled[1]}' # type: ignore 86 | 87 | # text 88 | content += f'\n Answer: {self.selection + 1}' 89 | 90 | self.prompt_content.text = HTML(content) 91 | 92 | def prompt(self) -> str | None: 93 | """start the application, returns the return value""" 94 | self.update() 95 | out: str | None = self.run() 96 | 97 | return out 98 | 99 | def on_up(self): 100 | """when up is pressed, the previous indexed option will be selected""" 101 | if not self.allow_keyboard: 102 | return 103 | 104 | self.selection = (self.selection - 1) % len(self.options) 105 | 106 | # if the current selection is disabled, we will skip it 107 | while self.options[self.selection].is_disabled: 108 | self.selection = (self.selection - 1) % len(self.options) 109 | 110 | self.update() 111 | 112 | def on_down(self): 113 | """when down is pressed, the next indexed option will be selected""" 114 | if not self.allow_keyboard: 115 | return 116 | 117 | self.selection = (self.selection + 1) % len(self.options) 118 | 119 | # if the current selection is disabled, we will skip it 120 | while self.options[self.selection].is_disabled: 121 | self.selection = (self.selection + 1) % len(self.options) 122 | 123 | self.update() 124 | 125 | def on_key(self, key_sequence: list[str]): 126 | """when an index is pressed, which is available to select, select this index""" 127 | key = key_sequence[0] 128 | 129 | # return if key is not a number 130 | if not key.isnumeric(): 131 | return 132 | 133 | id = int(key) 134 | 135 | # and return if key is not in the range of possible indices 136 | if id <= 0 or id > len(self.options): 137 | return 138 | 139 | # and return if selection is disabled 140 | if self.options[id - 1].is_disabled: 141 | return 142 | 143 | self.selection = id - 1 144 | 145 | self.update() 146 | 147 | def on_enter(self): 148 | # get selected id 149 | self.exit(result=self.options[self.selection].id) 150 | -------------------------------------------------------------------------------- /ItsPrompt/prompts/select.py: -------------------------------------------------------------------------------- 1 | from prompt_toolkit import Application, HTML 2 | from prompt_toolkit.layout.controls import FormattedTextControl 3 | 4 | from ..data.select import process_data 5 | from ..objects.prompts.separator import Separator 6 | from ..objects.prompts.type import OptionsList 7 | 8 | 9 | class SelectPrompt(Application): 10 | 11 | def __init__( 12 | self, 13 | question: str, 14 | options: OptionsList, 15 | default: str | None = None, 16 | disabled: tuple[str, ...] | None = None, 17 | *args, 18 | **kwargs, 19 | ): 20 | super().__init__(*args, **kwargs) 21 | 22 | # get prompt content box 23 | self.prompt_content: FormattedTextControl = self.layout.container.get_children()[0].content # type: ignore 24 | 25 | # save question 26 | self.question = question 27 | 28 | # process options 29 | self.options = process_data(options) 30 | 31 | # disable options 32 | if disabled is not None: 33 | was_set = 0 # Keeping track of options which where disabled. 34 | # If was_set is not as big as len(disabled), 35 | # then we know that there was an invalid option which could not be disabled, 36 | # so we raise an error. 37 | for option in self.options: 38 | if option.id in disabled: 39 | option.is_disabled = True 40 | was_set += 1 41 | 42 | if was_set != len(disabled): 43 | raise ValueError("At least one of the given disabled values is invalid.") 44 | 45 | # default select default option (or first if not given) 46 | if default is None: 47 | for i, option in enumerate(self.options): 48 | if not option.is_disabled: 49 | self.selection = i 50 | break 51 | else: 52 | for i, option in enumerate(self.options): 53 | if option.id == default: 54 | if option.is_disabled: 55 | raise ValueError("Default value must not be disabled.") 56 | self.selection = i 57 | break 58 | else: 59 | raise ValueError('Default value is not a valid id.') 60 | 61 | def update(self): 62 | """update prompt content""" 63 | content = f'[?] {self.question}:' 64 | 65 | for i, option in self.options.with_separators_enumerate(): 66 | 67 | if type(option) is Separator: 68 | content += f"\n{option.label}" 69 | continue 70 | 71 | disabled = ("", "") if option.is_disabled else ("", "") # type: ignore 72 | # Disabled tags will only be inserted if the option is disabled, 73 | # otherwise there will only be an empty string inserted. 74 | if i == self.selection: 75 | content += \ 76 | f'\n{disabled[0]} > {option.name}{disabled[1]}' # type: ignore 77 | else: 78 | content += \ 79 | f'\n{disabled[0]}{disabled[1]}' # type: ignore 80 | 81 | self.prompt_content.text = HTML(content) 82 | 83 | def prompt(self) -> str | None: 84 | """start the application, returns the return value""" 85 | self.update() 86 | out: str | None = self.run() 87 | 88 | return out 89 | 90 | def on_up(self): 91 | """when up is pressed, the previous indexed option will be selected""" 92 | self.selection = (self.selection - 1) % len(self.options) 93 | 94 | # if the current selection is disabled, we will skip it 95 | while self.options[self.selection].is_disabled: 96 | self.selection = (self.selection - 1) % len(self.options) 97 | 98 | self.update() 99 | 100 | def on_down(self): 101 | """when down is pressed, the next indexed option will be selected""" 102 | self.selection = (self.selection + 1) % len(self.options) 103 | 104 | # if the current selection is disabled, we will skip it 105 | while self.options[self.selection].is_disabled: 106 | self.selection = (self.selection + 1) % len(self.options) 107 | 108 | self.update() 109 | 110 | def on_enter(self): 111 | # get selected id 112 | self.exit(result=self.options[self.selection].id) 113 | -------------------------------------------------------------------------------- /ItsPrompt/prompts/table.py: -------------------------------------------------------------------------------- 1 | import html 2 | from typing import TYPE_CHECKING, Union 3 | 4 | from prompt_toolkit import Application, HTML 5 | from prompt_toolkit.data_structures import Point 6 | from prompt_toolkit.layout.controls import FormattedTextControl 7 | 8 | from ..data.table import Table 9 | from ..objects.prompts.type import TablePromptDict, TablePromptList 10 | 11 | if TYPE_CHECKING: # pragma: no cover 12 | from pandas import DataFrame 13 | 14 | 15 | class TablePrompt(Application): 16 | 17 | def __init__( 18 | self, 19 | question: str, 20 | data: Union["DataFrame", TablePromptDict, TablePromptList], 21 | *args, 22 | **kwargs, 23 | ): 24 | super().__init__(*args, **kwargs) 25 | 26 | # get prompt content box 27 | self.prompt_content: FormattedTextControl = self.layout.container.get_children()[0].content # type: ignore 28 | 29 | # save question 30 | self.question = question 31 | 32 | # process data 33 | self.table = Table(data) 34 | 35 | # set cursor 36 | self.prompt_content.get_cursor_position = lambda: Point( 37 | self.table.get_current_cursor_position()[0], 38 | self.table.get_current_cursor_position()[1] + 1, # add 1 offset for height of question 39 | ) 40 | 41 | def update(self): 42 | """update prompt content""" 43 | content = f'[?] {self.question}:\n' 44 | 45 | # append table 46 | content += html.escape( 47 | self.table.get_table_as_str() 48 | ) # escaping is needed so formatting from PromptToolkit won't destroy the whole table 49 | 50 | self.prompt_content.text = HTML(content) 51 | 52 | def prompt(self) -> Union["DataFrame", TablePromptDict, None]: 53 | """start the application, returns the return value""" 54 | self.update() 55 | out: Union["DataFrame", TablePromptDict, None] = self.run() 56 | 57 | return out 58 | 59 | def on_up(self): 60 | """when up is pressed, the cell one above will be selected""" 61 | self.table.on_up() 62 | 63 | self.update() 64 | 65 | def on_down(self): 66 | """when down is pressed, the cell one below will be selected""" 67 | self.table.on_down() 68 | 69 | self.update() 70 | 71 | def on_left(self): 72 | """when left is pressed, the cell one to the left will be selected""" 73 | self.table.on_left() 74 | 75 | self.update() 76 | 77 | def on_right(self): 78 | """when left is pressed, the cell one to the right will be selected""" 79 | self.table.on_right() 80 | 81 | self.update() 82 | 83 | def on_key(self, key_sequence: list[str]): 84 | """when a key is pressed, the key will be added to the current cells content""" 85 | key = key_sequence[0] 86 | self.table.add_key(key) 87 | 88 | self.update() 89 | 90 | def on_backspace(self): 91 | """when backspace is pressed, the last char will be removed from the current cells content""" 92 | self.table.del_key() 93 | 94 | self.update() 95 | 96 | def on_enter(self): 97 | # return the modified table 98 | self.exit(result=self.table.data.get_data()) 99 | -------------------------------------------------------------------------------- /ItsPrompt/py.typed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheItsProjects/ItsPrompt/b2f959dba2337775420e1cd525002e1df72f8c99/ItsPrompt/py.typed -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023-present ItsNameless 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![PyPI version](https://badge.fury.io/py/ItsPrompt.svg)](https://badge.fury.io/py/ItsPrompt) 2 | [![linting](https://github.com/TheItsProjects/ItsPrompt/actions/workflows/lint.yml/badge.svg)](https://github.com/TheItsProjects/ItsPrompt/actions/workflows/lint.yml) 3 | [![Tests](https://github.com/TheItsProjects/ItsPrompt/actions/workflows/tests.yml/badge.svg)](https://github.com/TheItsProjects/ItsPrompt/actions/workflows/tests.yml) 4 | 5 | [![PyPI - Downloads](https://img.shields.io/pypi/dm/ItsPrompt)](https://pypi.org/project/ItsPrompt/) 6 | [![GitHub issues](https://img.shields.io/github/issues/TheItsProjects/ItsPrompt)](https://github.com/TheItsProjects/ItsPrompt/issues) 7 | [![GitHub Repo stars](https://img.shields.io/github/stars/TheItsProjects/ItsPrompt)](https://github.com/TheItsProjects/ItsPrompt/stargazers) 8 | [![GitHub](https://img.shields.io/github/license/TheitsProjects/ItsPrompt)](https://github.com/TheItsProjects/ItsPrompt/blob/main/LICENSE) 9 | [![Discord](https://img.shields.io/discord/1082381448624996514)](https://discord.gg/rP9Qke2jDs) 10 | 11 | [![Read the Docs](https://img.shields.io/readthedocs/itsprompt)](http://itsprompt.readthedocs.io/) 12 | 13 | ![Demonstration](https://raw.githubusercontent.com/TheItsProjects/ItsPrompt/main/docs/source/media/ItsPrompt.gif) 14 | 15 | # ItsPrompt 16 | 17 | Do you ever feel the need to ask a user of your code for an input? 18 | 19 | Using `input()` is easy, but is it great? 20 | 21 | Do you want to give the user a selection list, a yes-or-no question, or maybe a multiline input field? 22 | 23 | And do you think all of this should be done easily, without caring to much how it all works? 24 | 25 | Then you are right here! **ItsPrompt** allows you to ask the user for input, the *fancy* way. 26 | 27 | **ItsPrompt** tries to be an easy-to-use module for managing prompts for the user. Your task is to create a great 28 | program, not finding yourself questioning how to ask the user for input. That is why **ItsPrompt** is there to take care 29 | of this problem, so you can focus on the important things! 30 | 31 | ## TOC 32 | 33 | 34 | * [ItsPrompt](#itsprompt) 35 | * [TOC](#toc) 36 | * [A small, thankful note](#a-small-thankful-note) 37 | * [Features](#features) 38 | * [Installation](#installation) 39 | * [Quick Example](#quick-example) 40 | * [Usage](#usage) 41 | * [Further Information](#further-information) 42 | 43 | 44 | --- 45 | 46 | ### A small, thankful note 47 | 48 | This project is not the first to accomplish the above-mentioned tasks. There is another package, `PyInquirer`, which 49 | inspired me to build **ItsPrompt**. 50 | 51 | On my way to create a small program I came to a point were I needed a simple GUI, and I tried `PyInquirer`. 52 | Unfortunately, at the current time it is not actively maintained and a bit outdated. I thought of updating it, but then 53 | I thought "*Isn't it easier to just create my own version?*" - And so I did! 54 | 55 | **ItsPrompt** is not a copy or a fork of `PyInquirer`. I built this module from the ground up, without ever looking deep 56 | into the source code of `PyInquirer`. 57 | 58 | On my way to build this package, I learned a lot about `prompt-toolkit`, and all of this just because of `PyInquirer`! 59 | Thanks! 60 | 61 | --- 62 | 63 | ## Features 64 | 65 | - many prompt types: 66 | - select 67 | - raw_select 68 | - expand 69 | - checkbox 70 | - confirm 71 | - input 72 | - table 73 | - prompt autocompletion and validation 74 | - customizable style with `prompt_toolkit` 75 | - a helpful toolbar with error messages 76 | - simple, pythonic syntax 77 | 78 | --- 79 | 80 | ## Installation 81 | 82 | This package is hosted on pypi, so the installation is as simple as it can get: 83 | 84 | ```bash 85 | python3 -m pip install ItsPrompt 86 | ``` 87 | 88 | This will install `ItsPrompt` without pandas. If you want to use `TablePrompt` 89 | (see [table](https://itsprompt.readthedocs.io/en/latest/guide/prompt_types.html#table)) with 90 | `pandas.DataFrame`, you can install pandas support either by: 91 | 92 | - installing pandas separately 93 | - install `ItsPrompt` via `pip install ItsPrompt[df]` 94 | 95 | --- 96 | 97 | ## Quick Example 98 | 99 | Import the `Prompt` class: 100 | 101 | ```py 102 | from ItsPrompt.prompt import Prompt 103 | ``` 104 | 105 | Now you can ask the user any type of prompt by calling the specific function from the `Prompt` class, e.g.: 106 | 107 | ```py 108 | result = Prompt.input('What is your name?') 109 | print(result) 110 | ``` 111 | 112 | You see how easy it is? 113 | 114 | --- 115 | 116 | ## Usage 117 | 118 | To learn more about the usage, visit our [documentation](https://itsprompt.readthedocs.io). 119 | 120 | --- 121 | 122 | ## Further Information 123 | 124 | Visit our [documentation](https://itsprompt.readthedocs.io/) to learn more about the usage of **ItsPrompt**! 125 | 126 | If you need some easy examples, refer to [example.py](example.py)! 127 | 128 | If you want to contribute, check out the projects repository: [ItsPrompt](https://github.com/TheItsProjects/ItsPrompt)! 129 | 130 | If you got any other questions, or want to give an idea on how to improve **ItsPrompt**: 131 | 132 | - visit our discussions: [ItsPrompt Discussions](https://github.com/TheItsProjects/ItsPrompt/discussions)! 133 | - join our discord: [TheItsProjects](https://discord.gg/rP9Qke2jDs)! 134 | 135 | --- 136 | 137 | Puh, that was so much to read... But now, lets have fun with **ItsPrompt**! 138 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | This page helps you find out how and when to report security vulnerabilites you encoutered. 4 | 5 | ## Supported Versions 6 | 7 | Currently we do not have different versions of `ItsPrompt`, so all of the minor versions of `1.x` are supported. 8 | 9 | | Version | Supported | 10 | |---------|--------------------| 11 | | 1.x | :white_check_mark: | 12 | 13 | ## Reporting a Vulnerability 14 | 15 | You found a security vulnerability? 16 | 17 | No problem! Open an Issue over at [select an Issue type](https://github.com/TheItsProjects/ItsPrompt/issues/new/choose) 18 | and choose **Security Vulnerability**. Fill in the required information and then we will look into it! 19 | 20 | If you want to privately report an Issue, visit our [Discord Server](https://discord.gg/rP9Qke2jDs) for more info! 21 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = source 9 | BUILDDIR = build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | # test command 16 | vhs: 17 | @echo "Creating VHS Media" 18 | cd scripts/vhs && ./generate.sh 19 | 20 | 21 | .PHONY: help Makefile vhs 22 | 23 | # Catch-all target: route all unknown targets to Sphinx using the new 24 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 25 | %: Makefile 26 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 27 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=source 11 | set BUILDDIR=build 12 | 13 | %SPHINXBUILD% >NUL 2>NUL 14 | if errorlevel 9009 ( 15 | echo. 16 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 17 | echo.installed, then set the SPHINXBUILD environment variable to point 18 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 19 | echo.may add the Sphinx directory to PATH. 20 | echo. 21 | echo.If you don't have Sphinx installed, grab it from 22 | echo.https://www.sphinx-doc.org/ 23 | exit /b 1 24 | ) 25 | 26 | if "%1" == "" goto help 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | sphinx==7.2.6 2 | sphinx-rtd-theme==2.0.0 3 | -------------------------------------------------------------------------------- /docs/scripts/vhs/checkbox.tape: -------------------------------------------------------------------------------- 1 | # --- Set Up -------------------------- 2 | # Load Config 3 | Source ./configuration/base_config.tape 4 | Source ./configuration/small_window_config.tape 5 | 6 | # --- Main ---------------------------- 7 | # run example.py 8 | Type@0.1s "python3 demos/checkbox_demo.py" Enter Sleep 1s 9 | 10 | Screenshot ../../source/media/checkbox.png 11 | 12 | Sleep 1s 13 | 14 | Ctrl+C 15 | -------------------------------------------------------------------------------- /docs/scripts/vhs/configuration/base_config.tape: -------------------------------------------------------------------------------- 1 | # --- Settings ------------------------ 2 | # Font 3 | Set FontFamily "Cascadia Mono" 4 | 5 | # Behaviour 6 | Set TypingSpeed 0.5s 7 | Set Shell "bash" 8 | 9 | # --- Set Up Prompt ------------------- 10 | Hide 11 | Type@10ms ". ./configuration/config.sh" Enter 12 | Sleep 5s 13 | Show 14 | -------------------------------------------------------------------------------- /docs/scripts/vhs/configuration/big_window_config.tape: -------------------------------------------------------------------------------- 1 | # Configuration for big window tapes, like the demo 2 | # These settings include a big window with gradient background, a colorful window bar, and a big font size 3 | 4 | # --- Settings ------------------------ 5 | # Font 6 | Set FontSize 20 7 | 8 | # Terminal Size 9 | Set Width 1200 10 | Set Height 600 11 | 12 | # Window 13 | Set WindowBar "Colorful" 14 | Set WindowBarSize 80 15 | Set Padding 0 16 | 17 | # Background 18 | Set Margin 25 19 | Set MarginFill ./configuration/gradient.png 20 | 21 | # Window 22 | Set BorderRadius 15 23 | -------------------------------------------------------------------------------- /docs/scripts/vhs/configuration/config.sh: -------------------------------------------------------------------------------- 1 | # Configure path and venv 2 | cd ../../../ 3 | source venv/bin/activate 4 | 5 | # Install Package 6 | pip install -e . 7 | cd examples/ || exit 8 | 9 | # Configure Prompt Style 10 | CYAN='\[\e[36m\]' 11 | YELLOW='\[\e[33m\]' 12 | BOLD='\[\e[1m\]' 13 | RESET='\[\e[0m\]' 14 | 15 | export PS1="${CYAN}${BOLD}user@itsprompt ${YELLOW}${BOLD}> ${RESET}" 16 | 17 | # Clear the terminal 18 | clear 19 | -------------------------------------------------------------------------------- /docs/scripts/vhs/configuration/gradient.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheItsProjects/ItsPrompt/b2f959dba2337775420e1cd525002e1df72f8c99/docs/scripts/vhs/configuration/gradient.png -------------------------------------------------------------------------------- /docs/scripts/vhs/configuration/small_window_config.tape: -------------------------------------------------------------------------------- 1 | # Configuration for small window tapes, like the ones used in prompt types 2 | # These settings include a small window without margin and background, no window bar, and a small font size 3 | 4 | # --- Settings ------------------------ 5 | # Font 6 | Set FontSize 25 7 | 8 | # Terminal Size 9 | Set Width 900 10 | Set Height 400 11 | 12 | # Window 13 | Set WindowBarSize 0 14 | Set Padding 0 15 | 16 | # Background 17 | Set Margin 0 18 | 19 | # Window 20 | # Set BorderRadius 0 21 | -------------------------------------------------------------------------------- /docs/scripts/vhs/confirm.tape: -------------------------------------------------------------------------------- 1 | # --- Set Up -------------------------- 2 | # Load Config 3 | Source ./configuration/base_config.tape 4 | Source ./configuration/small_window_config.tape 5 | 6 | # --- Main ---------------------------- 7 | # run example.py 8 | Type@0.1s "python3 demos/confirm_demo.py" Enter Sleep 1s 9 | 10 | Screenshot ../../source/media/confirm.png 11 | 12 | Sleep 1s 13 | 14 | Ctrl+C 15 | -------------------------------------------------------------------------------- /docs/scripts/vhs/demo.tape: -------------------------------------------------------------------------------- 1 | # --- Set Up -------------------------- 2 | # Load Config 3 | Source ./configuration/base_config.tape 4 | Source ./configuration/big_window_config.tape 5 | 6 | # Set Output 7 | Output ../../source/media/ItsPrompt.gif 8 | 9 | # --- Main ---------------------------- 10 | # run example.py 11 | Type@0.1s "python3 demo.py" Enter Sleep 1s 12 | 13 | # Prompt 1 14 | Up 15 | Down 2 16 | Enter 17 | Sleep 1s 18 | 19 | # Prompt 2 20 | Up 2 21 | Enter 22 | Sleep 1s 23 | 24 | # Prompt 3 25 | Space 26 | Enter 27 | Down 28 | Space 29 | Enter 30 | Sleep 1s 31 | 32 | # Prompt 4 33 | Type "h" 34 | Type "m" 35 | Enter 36 | Sleep 1s 37 | 38 | # Prompt 5 39 | Type@0.1s "test" Enter 40 | Sleep 1s 41 | 42 | # Prompt 6 43 | Type@0.1s "ItsNameless" Enter 44 | Sleep 1s 45 | 46 | # Prompt 7 47 | Type@0.1s "Main" 48 | Tab 49 | Enter 50 | Sleep 1s 51 | 52 | # Prompt 8 53 | Type@0.1s "1234" Enter 54 | Sleep 1s 55 | 56 | # Prompt 9 57 | Type "y" 58 | Sleep 1s 59 | 60 | # Prompt 10 61 | Right 62 | Down 63 | Backspace 64 | Type "1" Enter 65 | Sleep 1s 66 | 67 | # Exit 68 | Hide 69 | Ctrl+C 70 | Show 71 | -------------------------------------------------------------------------------- /docs/scripts/vhs/expand.tape: -------------------------------------------------------------------------------- 1 | # --- Set Up -------------------------- 2 | # Load Config 3 | Source ./configuration/base_config.tape 4 | Source ./configuration/small_window_config.tape 5 | 6 | # --- Main ---------------------------- 7 | # run example.py 8 | Type@0.1s "python3 demos/expand_demo.py" Enter Sleep 1s 9 | 10 | Type "h" Sleep 1s 11 | 12 | Screenshot ../../source/media/expand.png 13 | 14 | Sleep 1s 15 | 16 | Ctrl+C 17 | -------------------------------------------------------------------------------- /docs/scripts/vhs/generate.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Run this file to create tapes for all .tape files in the directory. 4 | # The tapes will be saved to source/media. 5 | # To run this file, you need to have vhs installed. Read more in the development guide. 6 | 7 | trap "echo 'Script interrupted by user'; exit 1" SIGINT SIGTERM 8 | 9 | # Get the directory of the script 10 | DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" 11 | 12 | # Iterate over all .tape files in the directory 13 | for file in "$DIR"/*.tape 14 | do 15 | # Run the file with vhs 16 | echo "--- Creating media for $file ---" 17 | vhs "$file" 1> /dev/null 18 | done 19 | -------------------------------------------------------------------------------- /docs/scripts/vhs/input.tape: -------------------------------------------------------------------------------- 1 | # --- Set Up -------------------------- 2 | # Load Config 3 | Source ./configuration/base_config.tape 4 | Source ./configuration/small_window_config.tape 5 | 6 | # --- Main ---------------------------- 7 | # run example.py 8 | Type@0.1s "python3 demos/input_demo.py" Enter Sleep 1s 9 | 10 | Screenshot ../../source/media/input.png 11 | 12 | Sleep 1s 13 | 14 | Ctrl+C 15 | -------------------------------------------------------------------------------- /docs/scripts/vhs/raw_select.tape: -------------------------------------------------------------------------------- 1 | # --- Set Up -------------------------- 2 | # Load Config 3 | Source ./configuration/base_config.tape 4 | Source ./configuration/small_window_config.tape 5 | 6 | # --- Main ---------------------------- 7 | # run example.py 8 | Type@0.1s "python3 demos/raw_select_demo.py" Enter Sleep 1s 9 | 10 | Screenshot ../../source/media/raw_select.png 11 | 12 | Sleep 1s 13 | 14 | Ctrl+C 15 | -------------------------------------------------------------------------------- /docs/scripts/vhs/select.tape: -------------------------------------------------------------------------------- 1 | # --- Set Up -------------------------- 2 | # Load Config 3 | Source ./configuration/base_config.tape 4 | Source ./configuration/small_window_config.tape 5 | 6 | # --- Main ---------------------------- 7 | # run example.py 8 | Type@0.1s "python3 demos/select_demo.py" Enter Sleep 1s 9 | 10 | Screenshot ../../source/media/select.png 11 | 12 | Sleep 1s 13 | 14 | Ctrl+C 15 | -------------------------------------------------------------------------------- /docs/scripts/vhs/table.tape: -------------------------------------------------------------------------------- 1 | # --- Set Up -------------------------- 2 | # Load Config 3 | Source ./configuration/base_config.tape 4 | Source ./configuration/small_window_config.tape 5 | 6 | # --- Main ---------------------------- 7 | # run example.py 8 | Type@0.1s "python3 demos/table_demo.py" Enter Sleep 1s 9 | 10 | Screenshot ../../source/media/table.png 11 | 12 | Sleep 1s 13 | 14 | Ctrl+C 15 | -------------------------------------------------------------------------------- /docs/scripts/vhs/tape.template: -------------------------------------------------------------------------------- 1 | # --- Set Up -------------------------- 2 | # Load Config 3 | Source ./configuration/base_config.tape 4 | Source ./configuration/small_window_config.tape # use big_window_config.tape for bigger window 5 | 6 | # --- Main ---------------------------- 7 | # run example.py 8 | Type@0.1s "python3 demos/FILE.py" Enter Sleep 1s # replace FILE.py with the file you want to run 9 | 10 | Screenshot ../../source/media/MEDIA.png # replace MEDIA.png with the name of the file you want to save the screenshot to 11 | 12 | Sleep 1s 13 | 14 | Ctrl+C 15 | -------------------------------------------------------------------------------- /docs/source/api/objects.rst: -------------------------------------------------------------------------------- 1 | Objects API Reference 2 | ===================== 3 | 4 | This is the API reference for the ``objects`` used for the :doc:`prompt` methods. 5 | 6 | Prompt Input Objects 7 | -------------------- 8 | 9 | .. automodule:: ItsPrompt.objects.prompts.type 10 | :members: 11 | :undoc-members: 12 | 13 | .. autoclass:: ItsPrompt.objects.prompts.separator.Separator 14 | :members: 15 | :undoc-members: 16 | 17 | Styling Objects 18 | --------------- 19 | 20 | .. automodule:: ItsPrompt.data.style 21 | :members: 22 | :exclude-members: convert_style 23 | :undoc-members: 24 | -------------------------------------------------------------------------------- /docs/source/api/prompt.rst: -------------------------------------------------------------------------------- 1 | Prompt API Reference 2 | ==================== 3 | 4 | This is the API reference for the `Prompt` class. The `Prompt` class is the main class of the `ItsPrompt` package. It is used to create and display prompts to the user. 5 | 6 | To use any of the given methods, you must first import the ``Prompt`` class from the `ItsPrompt` package. Here is an example of how to import the ``Prompt`` class: 7 | 8 | .. code-block:: python 9 | 10 | from ItsPrompt.prompt import Prompt 11 | 12 | .. currentmodule:: ItsPrompt.prompt 13 | 14 | .. automethod:: Prompt.select 15 | 16 | .. automethod:: Prompt.raw_select 17 | 18 | .. automethod:: Prompt.expand 19 | 20 | .. automethod:: Prompt.checkbox 21 | 22 | .. automethod:: Prompt.confirm 23 | 24 | .. automethod:: Prompt.input 25 | 26 | .. automethod:: Prompt.table 27 | -------------------------------------------------------------------------------- /docs/source/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # For the full list of built-in configuration values, see the documentation: 4 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 5 | 6 | # -- Project information ----------------------------------------------------- 7 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information 8 | 9 | project = 'ItsPrompt' 10 | copyright = '2024-present, ItsNameless' 11 | author = 'ItsNameless' 12 | version = '1.5' 13 | 14 | # -- General configuration --------------------------------------------------- 15 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration 16 | 17 | extensions = [ 18 | 'sphinx.ext.autodoc', 'sphinx.ext.viewcode', 'sphinx.ext.intersphinx', 'sphinx.ext.doctest', 'sphinx.ext.coverage' 19 | ] 20 | 21 | intersphinx_mapping = { 22 | 'python': ('https://docs.python.org/3', None), 23 | 'pandas': ('https://pandas.pydata.org/pandas-docs/stable/', None), 24 | 'prompt_toolkit': ('https://python-prompt-toolkit.readthedocs.io/en/master/', None), 25 | } 26 | templates_path = ['_templates'] 27 | exclude_patterns = [] # type: ignore 28 | 29 | # -- Options for HTML output ------------------------------------------------- 30 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output 31 | 32 | html_theme = 'sphinx_rtd_theme' 33 | html_static_path = ['_static'] 34 | -------------------------------------------------------------------------------- /docs/source/development_guide/documentation.rst: -------------------------------------------------------------------------------- 1 | Writing Documentation for ItsPrompt 2 | =================================== 3 | 4 | This document describes how to write documentation for ItsPrompt. 5 | 6 | Basics of ItsPrompt Documentation 7 | --------------------------------- 8 | 9 | `ItsPrompt` uses `Sphinx` for documentation. The documentation is written in `reStructuredText` format. The source 10 | can be found in the `docs/source` directory. 11 | 12 | Creating Media Files using vhs 13 | ------------------------------ 14 | 15 | `ItsPrompt` uses `vhs `_ to record terminal sessions and create gif and image files 16 | for the documentation. Before running any command, refer to the `vhs` documentation to understand how to install and use it. 17 | 18 | If you simply want to update all the included media, you can run the following command: 19 | 20 | .. code-block:: bash 21 | 22 | make vhs 23 | 24 | The generated media files will be placed in the `docs/source/media` directory. 25 | 26 | Adding Media Files to the Documentation 27 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 28 | 29 | To add a new media file to the documentation, you need to create a new `.tape` file in the `docs/scripts/vhs` directory. 30 | 31 | You can use the content from `tape.template` for a quick guide on how to create a new `.tape` file. 32 | -------------------------------------------------------------------------------- /docs/source/development_guide/getting_started.rst: -------------------------------------------------------------------------------- 1 | ItsPrompt Development Guide 2 | =========================== 3 | 4 | This guide is intended to help developers get started with the Development for ItsPrompt. 5 | -------------------------------------------------------------------------------- /docs/source/guide/getting_started.rst: -------------------------------------------------------------------------------- 1 | Getting Started with ItsPrompt 2 | ============================== 3 | 4 | What is ItsPrompt? 5 | ------------------ 6 | 7 | **ItsPrompt** is a Python package that simplifies user interaction in command-line interfaces. It offers a variety of 8 | prompt types, including: 9 | 10 | - `select` 11 | - `raw_select` 12 | - `expand` 13 | - `checkbox` 14 | - `confirm` 15 | - `input` 16 | - `table` 17 | 18 | Each prompt type provides a unique way of gathering user input. The package is designed with simplicity in mind, 19 | offering a straightforward, Pythonic syntax. It also provides a range of customization options, allowing you to style 20 | prompts and validate user input. 21 | 22 | ItsPrompt is built on top of the `prompt-toolkit` library, leveraging its capabilities for prompt creation and styling. 23 | This makes it an excellent tool for developers who want to create interactive command-line applications without having 24 | to worry about the intricacies of user input and command-line rendering. 25 | 26 | Installation 27 | ------------ 28 | 29 | This package is hosted on pypi, so the installation is as simple as it can get: 30 | 31 | .. code-block:: bash 32 | 33 | python3 -m pip install ItsPrompt 34 | 35 | 36 | This will install `ItsPrompt` without pandas. If you want to use `TablePrompt` (see :ref:`prompt_types_table`) with 37 | `pandas.DataFrame`, you can install pandas support either by: 38 | 39 | - installing pandas separately 40 | - install `ItsPrompt` via ``pip install ItsPrompt[df]`` 41 | 42 | Basic Usage 43 | ----------- 44 | 45 | Import the `Prompt` class: 46 | 47 | .. code-block:: python 48 | 49 | >>> from ItsPrompt.prompt import Prompt 50 | 51 | Now you can ask the user any type of prompt by calling the specific function from the `Prompt` class, e.g.: 52 | 53 | .. code-block:: python 54 | 55 | >>> result = Prompt.input('What is your name?') 56 | [?] What is your name?: ItsNameless 57 | >>> print(f"Hello {result}!") 58 | Hello ItsNameless! 59 | 60 | You see how easy it is? 61 | 62 | To learn more about the usage of ItsPrompt, check out :doc:`usage`. 63 | -------------------------------------------------------------------------------- /docs/source/guide/options_and_data.rst: -------------------------------------------------------------------------------- 1 | Options and Data in ItsPrompt 2 | ============================= 3 | 4 | `ItsPrompt` has a number of ways to display and check options and data. 5 | 6 | Option Parameter 7 | ---------------- 8 | 9 | The `option` parameter is always a :class:`tuple` with the type annotation :const:`~ItsPrompt.objects.prompts.type.OptionsList`. 10 | 11 | Option as a String 12 | ~~~~~~~~~~~~~~~~~~ 13 | 14 | Options can be given as a :class:`str`. In this case, the string is displayed as the option and returned as the 15 | selected option, if the user selects it. 16 | 17 | .. note:: In case of :meth:`~ItsPrompt.prompt.Prompt.expand()`, the first character of the :class:`str` will be used as the key. 18 | 19 | Options as a Tuple of OptionWithId 20 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 21 | 22 | Options can be given as a :class:`tuple` with the type annotation :const:`~ItsPrompt.objects.prompts.type.OptionWithId`. 23 | 24 | For all prompts excluding :meth:`~ItsPrompt.prompt.Prompt.expand`, the tuple is structured as follows 25 | 26 | - The first element is the displayed option 27 | - The second element is the id of the option 28 | 29 | For the :meth:`~ItsPrompt.prompt.Prompt.expand` prompt, the tuple is structured as follows: 30 | 31 | - The first element is the key of the option 32 | - The second element is the displayed option 33 | - The third element is the id of the option 34 | 35 | Separators 36 | ~~~~~~~~~~ 37 | 38 | Options can be separated using a :class:`~ItsPrompt.objects.prompts.type.Separator`. A Separator name can be given using 39 | the `label` parameter. 40 | 41 | Separators are available in the prompt types: 42 | 43 | - :meth:`~ItsPrompt.prompt.Prompt.select` 44 | - :meth:`~ItsPrompt.prompt.Prompt.raw_select` 45 | - :meth:`~ItsPrompt.prompt.Prompt.checkbox` 46 | - :meth:`~ItsPrompt.prompt.Prompt.expand` 47 | 48 | .. note:: The separator cannot be selected as an option. It is purely for cosmetic purposes. 49 | 50 | Example: 51 | 52 | .. code-block:: python 53 | 54 | from ItsPrompt.prompt import Prompt 55 | from ItsPrompt.objects.prompts.separator import Separator 56 | 57 | ans = Prompt.select( 58 | 'What food would you like?', 59 | (Separator('The veggies'), 'Salad', Separator('The meaties'), 'Pizza', 'Burger'), 60 | default='Pizza', 61 | ) 62 | 63 | Data Parameter 64 | -------------- 65 | 66 | The :meth:`~ItsPrompt.prompt.Prompt.table` prompt has a `data` parameter instead of the `option` parameter. The `data` 67 | parameter has to be one of the following types: 68 | 69 | - a :class:`~ItsPrompt.objects.prompts.type.TablePromptList` 70 | - a :class:`~ItsPrompt.objects.prompts.type.TablePromptDict` 71 | - a :class:`pandas.DataFrame` 72 | 73 | .. note:: The type :class:`pandas.DataFrame` is only available if the `pandas` library is installed. 74 | 75 | The `data` is used as the content of the table. The user may change the fields of the table. The output of the `table` 76 | prompt is of the same type, as the input data is represented, with the user given values. 77 | 78 | .. note:: 79 | 80 | Currently, all fields are represented as strings and every field is editable by the user. This may be changed 81 | in the future. 82 | 83 | Data as a TablePromptList 84 | ~~~~~~~~~~~~~~~~~~~~~~~~~ 85 | 86 | A :class:`~ItsPrompt.objects.prompts.type.TablePromptList` is a :class:`list` of :class:`list` with each cell 87 | represented by a :class:`str`. 88 | 89 | Every sub-list represents a column in the table. 90 | 91 | .. code-block:: python 92 | 93 | data = [ 94 | ["field 1", "field 2"], 95 | ["field 3", "field 4"] 96 | ] 97 | 98 | will be rendered: 99 | 100 | +---------+---------+ 101 | | 0 | 1 | 102 | +=========+=========+ 103 | | field 1 | field 3 | 104 | +---------+---------+ 105 | | field 2 | field 4 | 106 | +---------+---------+ 107 | 108 | Data as a TablePromptDict 109 | ~~~~~~~~~~~~~~~~~~~~~~~~~ 110 | 111 | A :class:`~ItsPrompt.objects.prompts.type.TablePromptDict` is a :class:`dict` with the keys as the column names and the 112 | values as a :class:`list` of :class:`str`, where each :class:`list` represents a column in the table. 113 | 114 | .. code-block:: python 115 | 116 | data = { 117 | "column 1": ["field 1", "field 2"], 118 | "column 2": ["field 3", "field 4"] 119 | } 120 | 121 | +----------+----------+ 122 | | column 1 | column 2 | 123 | +==========+==========+ 124 | | field 1 | field 3 | 125 | +----------+----------+ 126 | | field 2 | field 4 | 127 | +----------+----------+ 128 | 129 | Data as a DataFrame 130 | ~~~~~~~~~~~~~~~~~~~ 131 | 132 | A :class:`pandas.DataFrame` can be used as well. Read more about them in the `pandas documentation `_. 133 | 134 | .. code-block:: python 135 | 136 | DataFrame(["field 1", "field 2"]) 137 | 138 | +---------+ 139 | | 0 | 140 | +=========+ 141 | | field 1 | 142 | +---------+ 143 | | field 2 | 144 | +---------+ 145 | 146 | .. note:: 147 | 148 | Currently, the `table` prompt cannot display styling in the `DataFrame` fields. All styling tags will be displayed 149 | as-is, so a `...` will not be underlined, but rather displayed as its shown. 150 | 151 | Input Prompt Parameters 152 | ----------------------- 153 | 154 | The :meth:`~ItsPrompt.prompt.Prompt.input` prompt has many options to check and complete the input. The following parameters are available: 155 | 156 | - `default`: The default value of the input. If the user does not enter anything, the default value is returned. 157 | - `multiline`: If set to :const:`True`, the user can enter multiple lines of text. 158 | - `show_symbol`: Can be set to a :class:`str` to show this symbol instead of the characters entered by the user. 159 | This is useful for password prompts. 160 | - `validate`: A function that validates the input. Read more about this in the :ref:`validation` section. 161 | - `completions`: A list of completions that the user can select from. Read more about this in the :ref:`completions` section. 162 | - `completer`: A function that returns a list of completions. Read more about this in the :ref:`completions` section. 163 | 164 | .. note:: 165 | 166 | Only the `validate`, `completions`, and `completer` parameters are described here in detail. The other parameters can 167 | be found in the :meth:`~ItsPrompt.prompt.Prompt.input` API documentation. 168 | 169 | .. _validation: 170 | 171 | Prompt Validation 172 | ~~~~~~~~~~~~~~~~~ 173 | 174 | The `validate` parameter can be used to validate the input. For every character entered by the user, the `validate` 175 | function is called with the current input as a :class:`str`. The function should return either 176 | 177 | - a :class:`str` to show the string as an error message 178 | - :const:`False` to show a default error message 179 | - :const:`None` (or :const:`True`) to accept the input 180 | 181 | .. code-block:: python 182 | 183 | # define a validation function 184 | def input_not_empty(input: str) -> str | None: 185 | if len(input) == 0: 186 | return 'Address can not be empty!' 187 | 188 | 189 | # using a function 190 | Prompt.input( 191 | ..., 192 | validate=input_not_empty, 193 | ..., 194 | ) 195 | 196 | # using lambda 197 | Prompt.input( 198 | ..., 199 | validate=lambda x: "test" in x, 200 | ..., 201 | ) 202 | 203 | The :class:`str` given to the function is the current input of the user. It cannot be changed. 204 | 205 | If you want to show that the validation succeeded, return :const:`None` (or nothing, or :const:`True`). This will not 206 | trigger any errors. 207 | 208 | If you want to show an error, return a :class:`str` with the errors text or :const:`False`. If you return a 209 | :class:`str`, your text will be shown in the toolbar. If you return :const:`False`, a general error message will be 210 | shown. As long as the validation returns a :class:`str` or :const:`False`, the user may not submit the input. 211 | 212 | .. _completions: 213 | 214 | Prompt Completion 215 | ~~~~~~~~~~~~~~~~~ 216 | 217 | The `completions` and `completer` parameters can be used to give the user a list of completions to choose from. 218 | 219 | .. note:: If you use `completions` or `completer`, you are unable to use `show_symbol`. 220 | 221 | .. note:: `completions` and `completer` are **mutually exclusive**!. You may only use one of them at a time. 222 | 223 | There are three ways to give completions to the user: 224 | 225 | - A :class:`list` of :class:`str` (read more about this in the :ref:`completions_as_a_list` section) 226 | - A nested :class:`dict` (read more about this in the :ref:`completions_as_a_dict` section) 227 | - A :class:`~prompt_toolkit.completion.Completer` given by `prompt_toolkit` (read more about this in the 228 | :ref:`completions_as_a_completer` section) 229 | 230 | .. _completions_as_a_list: 231 | 232 | Completions as a List 233 | ********************* 234 | 235 | :meth:`~ItsPrompt.prompt.Prompt.input` takes a :class:`list[str]` to use as simple word completions. Each :class:`str` in the list is a possible value to complete. 236 | 237 | .. code-block:: python 238 | 239 | prompt.input( 240 | ..., 241 | completions=['Mainstreet 4', 'Fifth way'], 242 | ..., 243 | ) 244 | 245 | .. _completions_as_a_dict: 246 | 247 | Completions as a Dict 248 | ********************* 249 | 250 | You can use a :class:`dict` for nested completions. Each "layer" will be a completion, after the first was accepted. The 251 | type annotation for the :class:`dict` can be found here: :const:`~ItsPrompt.objects.prompts.type.CompletionDict`. 252 | 253 | Example: 254 | 255 | .. code-block:: python 256 | 257 | completions = { 258 | '1': { 259 | '1.1': None, 260 | '1.2': { 261 | '1.2.1', '1.2.2' 262 | } 263 | }, 264 | '2': { 265 | '2.1': {'2.1.1'} 266 | } 267 | } 268 | 269 | prompt.input( 270 | ..., 271 | completions=completions, 272 | ..., 273 | ) 274 | 275 | In this example, the user can select `1` or `2` as the first completion. If the user selects `1`, they can select `1.1` 276 | or `1.2`, then `1.2.1` and so on. 277 | 278 | The key of each entry is the completion that will be shown. The key is either :const:`None` if there are no further 279 | completions or a new :class:`dict`, where the key is the completion and the value is the next "layer", and so on. 280 | 281 | .. _completions_as_a_completer: 282 | 283 | Completions as a Completer 284 | ************************** 285 | 286 | In the background your completions will be mapped to a :class:`~prompt_toolkit.completion.Completer`, provided by 287 | `prompt_toolkit`. 288 | 289 | If you need more customization, you can use a :class:`~prompt_toolkit.completion.Completer` given by `prompt-toolkit` or 290 | create your own completer. For more information on this process, read here: 291 | `Completions in prompt-toolkit `_. 292 | 293 | There are a number of completers available, for example: 294 | 295 | - :class:`~prompt_toolkit.completion.PathCompleter` 296 | - automatically complete file system paths 297 | - :class:`~prompt_toolkit.completion.ExecutableCompleter` 298 | - automatically complete executables in a file system 299 | - :class:`~prompt_toolkit.completion.WordCompleter` 300 | - As simple as it can get. Just completes the letters of the word, that are actually present (the `FuzzyCompleter` 301 | which `completions` uses in background completes based on a probability, and may show matches which are not 302 | exact). 303 | - ... 304 | 305 | To add your own completer to an :meth:`~ItsPrompt.prompt.Prompt.input` field, you can use the `completer` parameter: 306 | 307 | .. code-block:: python 308 | 309 | prompt.input( 310 | ..., 311 | completer=my_completer, 312 | ..., 313 | ) 314 | 315 | .. note:: `completions` and `completer` are **mutually exclusive**!. You may only use one of them at a time. 316 | -------------------------------------------------------------------------------- /docs/source/guide/prompt_types.rst: -------------------------------------------------------------------------------- 1 | Prompt Types in ItsPrompt 2 | ========================= 3 | 4 | The following prompt types are available in ItsPrompt: 5 | 6 | .. contents:: Available Prompt Types 7 | :local: 8 | :depth: 2 9 | 10 | select 11 | ------ 12 | 13 | .. image:: ../media/select.png 14 | 15 | .. code-block:: python 16 | 17 | ans = Prompt.select( 18 | question='What food would you like?', 19 | options=(Separator('The veggies'), 'Salad', Separator('The meaties'), 'Pizza', 'Burger'), 20 | default='Pizza', 21 | ) 22 | 23 | To read more about the parameters of the `select` prompt, see the definition of :meth:`~ItsPrompt.prompt.Prompt.select`. 24 | 25 | raw_select 26 | ---------- 27 | 28 | .. image:: ../media/raw_select.png 29 | 30 | .. code-block:: python 31 | 32 | ans = Prompt.raw_select( 33 | question='What pizza would you like?', 34 | options=('Salami', 'Hawaii', 'four-cheese'), 35 | allow_keyboard=True, 36 | ) 37 | 38 | To read more about the parameters of the `raw_select` prompt, see the definition of :meth:`~ItsPrompt.prompt.Prompt.raw_select`. 39 | 40 | expand 41 | ------ 42 | 43 | .. image:: ../media/expand.png 44 | 45 | .. code-block:: python 46 | 47 | ans = Prompt.expand( 48 | question='Where do you want your food to be delivered?', 49 | options=('my home', 'another home'), 50 | allow_keyboard=True, 51 | ) 52 | 53 | To read more about the parameters of the `expand` prompt, see the definition of :meth:`~ItsPrompt.prompt.Prompt.expand`. 54 | 55 | checkbox 56 | -------- 57 | 58 | .. image:: ../media/checkbox.png 59 | 60 | .. code-block:: python 61 | 62 | ans = Prompt.checkbox( 63 | question='What beverages would you like?', 64 | options=('Coke', 'Water', 'Juice'), 65 | default_checked=('Water',), 66 | disabled=("Coke",), 67 | min_selections=1, 68 | ) 69 | 70 | To read more about the parameters of the `checkbox` prompt, see the definition of :meth:`~ItsPrompt.prompt.Prompt.checkbox`. 71 | 72 | confirm 73 | ------- 74 | 75 | .. image:: ../media/confirm.png 76 | 77 | .. code-block:: python 78 | 79 | ans = Prompt.confirm( 80 | question='Is the information correct?', 81 | default=True, 82 | ) 83 | 84 | To read more about the parameters of the `confirm` prompt, see the definition of :meth:`~ItsPrompt.prompt.Prompt.confirm`. 85 | 86 | input 87 | ----- 88 | 89 | .. image:: ../media/input.png 90 | 91 | .. code-block:: python 92 | 93 | ans = Prompt.input( 94 | question='Please type your name', 95 | validate=input_not_empty, 96 | ) 97 | 98 | To read more about the parameters of the `input` prompt, see the definition of :meth:`~ItsPrompt.prompt.Prompt.input`. 99 | 100 | .. _prompt_types_table: 101 | 102 | table 103 | ----- 104 | 105 | .. image:: ../media/table.png 106 | 107 | .. code-block:: python 108 | 109 | data = DataFrame({ 110 | 'Food': ['Pizza', 'Burger', 'Salad'], 111 | 'Qty': [1, 0, 0], 112 | }) 113 | 114 | ans = Prompt.table( 115 | question='Please fill in your quantity', 116 | data=data, 117 | ) 118 | 119 | To read more about the parameters of the `table` prompt, see the definition of :meth:`~ItsPrompt.prompt.Prompt.table`. 120 | -------------------------------------------------------------------------------- /docs/source/guide/styling.rst: -------------------------------------------------------------------------------- 1 | Prompt Styling 2 | ============== 3 | 4 | `ItsPrompt` uses `prompt_toolkit` for its prompts. This module not only provides an easy way to interact with the 5 | command line, but also a full set of styling features. 6 | 7 | You can learn more about the available styling features in the 8 | `Styling Documentation `_ 9 | of `prompt_toolkit`. 10 | 11 | `ItsPrompt` makes it a bit easier for you to style each component of a prompt. For every component, we give a separate 12 | attribute in the `PromptStyle` class, which you can style with valid `prompt_toolkit` styling: 13 | 14 | .. code-block:: python 15 | 16 | # examples for the different styling class components 17 | Prompt.raw_select( 18 | question='question', 19 | options=( 20 | 'option', 21 | 'selected_option', 22 | ) 23 | ) 24 | 25 | Prompt.input( 26 | question='question', 27 | default='grayout', 28 | validate=lambda x: 'error', 29 | ) 30 | 31 | .. image:: ../media/styling_raw_select.png 32 | 33 | .. image:: ../media/styling_input.png 34 | 35 | +-------------------+---------------------------------------+-------------------------------------------------+ 36 | | styling tag | default style | default design | 37 | +===================+=======================================+=================================================+ 38 | | `question_mark` | `fg:ansigreen` | .. image:: ../media/styling/question_mark.png | 39 | +-------------------+---------------------------------------+-------------------------------------------------+ 40 | | `question` | **\*** | .. image:: ../media/styling/question.png | 41 | +-------------------+---------------------------------------+-------------------------------------------------+ 42 | | `option` | **\*** | .. image:: ../media/styling/option.png | 43 | +-------------------+---------------------------------------+-------------------------------------------------+ 44 | | `selected_option` | `fg:ansicyan` | .. image:: ../media/styling/selected_option.png | 45 | +-------------------+---------------------------------------+-------------------------------------------------+ 46 | | `tooltip` | `fg:ansibrightblue bg:ansiwhite bold` | .. image:: ../media/styling/tooltip.png | 47 | +-------------------+---------------------------------------+-------------------------------------------------+ 48 | | `text` | **\*** | .. image:: ../media/styling/text.png | 49 | +-------------------+---------------------------------------+-------------------------------------------------+ 50 | | `grayout` | `fg:ansibrightblack` | .. image:: ../media/styling/grayout.png | 51 | +-------------------+---------------------------------------+-------------------------------------------------+ 52 | | `error` | `fg:ansiwhite bg:ansired bold` | .. image:: ../media/styling/error.png | 53 | +-------------------+---------------------------------------+-------------------------------------------------+ 54 | | `separator` | `fg:ansibrightgreen` | .. image:: ../media/styling/separator.png | 55 | +-------------------+---------------------------------------+-------------------------------------------------+ 56 | 57 | .. note:: **\***\ These values are not changed from the default `prompt_toolkit` values. 58 | 59 | 60 | To create your own style, there are two ways: 61 | 62 | Changing the Default Style 63 | -------------------------- 64 | 65 | To change the default style, you need to import the :const:`~ItsPrompt.data.style.default_style` and change its values: 66 | 67 | .. code-block:: python 68 | 69 | from ItsPrompt.data.style import default_style 70 | 71 | default_style.error = 'fg:ansired bg:ansiwhite' 72 | 73 | This will automatically change the style of all prompts, which do not have an own style defined. 74 | 75 | Creating your own Style 76 | ----------------------- 77 | 78 | To define your own style for a specific prompt, import :class:`~ItsPrompt.data.style.PromptStyle` and create an object. 79 | Then assign it to the `style` argument of any prompt. 80 | 81 | .. code-block:: python 82 | 83 | from ItsPrompt.data.style import PromptStyle 84 | 85 | my_style = PromptStyle( 86 | question_mark='fg:ansiblue', 87 | error='fg:ansired bg:ansiwhite', 88 | ) 89 | 90 | .. note:: 91 | 92 | If you omit a style attribute of your :class:`~ItsPrompt.data.style.PromptStyle`, this attribute **will not** use 93 | the one given by the :const:`~ItsPrompt.data.style.default_style`. Instead, it will use the default style given by 94 | `prompt_toolkit`. 95 | 96 | If you want to create your own style from the :const:`~ItsPrompt.data.style.default_style`, you can use the 97 | :meth:`~ItsPrompt.data.style.create_from_default` method: 98 | 99 | .. code-block:: python 100 | 101 | from ItsPrompt.data.style import create_from_default 102 | 103 | my_style = create_from_default() 104 | 105 | my_style.error = 'fg:ansired bg:ansiwhite' 106 | 107 | This will create a copy of the :const:`~ItsPrompt.data.style.default_style` and change its `error` attribute. All other 108 | attributes will remain the same as the :const:`~ItsPrompt.data.style.default_style`. 109 | 110 | .. note:: 111 | 112 | Warning! Not copying the default style and changing it instead will result in all prompts using your changes, as a 113 | variable is by default not a copy, but a reference to the same object! 114 | -------------------------------------------------------------------------------- /docs/source/guide/usage.rst: -------------------------------------------------------------------------------- 1 | ItsPrompt Usage 2 | =============== 3 | 4 | Question and Options 5 | -------------------- 6 | 7 | Question 8 | ~~~~~~~~ 9 | 10 | All basic prompt types have a ``question`` parameter which is the question that will be asked to the user. 11 | 12 | .. code-block:: python 13 | 14 | from itsprompt.prompt import Prompt 15 | 16 | ans = Prompt.input(question="What is your name?") 17 | 18 | print(ans) 19 | 20 | The above code will ask the user "What is your name?" and will store the user's input in the variable ``ans``. 21 | 22 | Options 23 | ~~~~~~~ 24 | 25 | Prompt types which require the user to select an option from a list of options have an ``options`` parameter which is a 26 | list of options that the user can select from. 27 | 28 | .. code-block:: python 29 | 30 | from itsprompt.prompt import Prompt 31 | 32 | ans = Prompt.select(question="What is your favorite color?", options=["Red", "Green", "Blue"]) 33 | 34 | print(ans) 35 | 36 | The above code will ask the user "What is your favorite color?" and will store the user's selected option in the 37 | variable ``ans``. 38 | 39 | There are many different ways to create options, including adding separators. To read more about them see 40 | :doc:`options_and_data`. 41 | 42 | Default Option 43 | ~~~~~~~~~~~~~~ 44 | 45 | Prompt types which require options have the ability to have a default option. This is done by setting the ``default`` 46 | parameter to the name or id of the default option. 47 | 48 | .. code-block:: python 49 | 50 | from itsprompt.prompt import Prompt 51 | 52 | ans = Prompt.select(question="What is your favorite color?", options=["Red", "Green", "Blue"], default="Green") 53 | 54 | print(ans) 55 | 56 | The above code will ask the user "What is your favorite color?" and will store the user's selected option in the 57 | variable ``ans``. The pointer will start on the option "Green". 58 | 59 | Disabled Options 60 | ~~~~~~~~~~~~~~~~ 61 | 62 | Prompt types which require options have the ability to have disabled options. This is done by setting the ``disabled`` 63 | parameter to a list of the names or ids of the disabled options. 64 | 65 | .. code-block:: python 66 | 67 | from itsprompt.prompt import Prompt 68 | 69 | ans = Prompt.select(question="What is your favorite color?", options=["Red", "Green", "Blue"], disabled=["Green"]) 70 | 71 | print(ans) 72 | 73 | The above code will ask the user "What is your favorite color?" and will store the user's selected option in the 74 | variable ``ans``. The option "Green" will be grayed out and the user will not be able to select it. 75 | 76 | Text Input 77 | ~~~~~~~~~~ 78 | 79 | Prompt types which require text input have a ``default`` parameter which is the default text that will be displayed in 80 | the input field. 81 | 82 | .. code-block:: python 83 | 84 | from itsprompt.prompt import Prompt 85 | 86 | ans = Prompt.input(question="What is your name?", default="John") 87 | 88 | print(ans) 89 | 90 | The above code will ask the user "What is your name?" and will store the user's input in the variable ``ans``. The input 91 | field will have the text "John" already in it. 92 | 93 | Styling 94 | ------- 95 | 96 | Prompt types have a ``style`` parameter which defines the style of the prompt. To read more about styling see 97 | :doc:`styling`. 98 | -------------------------------------------------------------------------------- /docs/source/index.rst: -------------------------------------------------------------------------------- 1 | .. main page of ItsPrompt documentation 2 | 3 | Welcome to ItsPrompt's Documentation! 4 | ===================================== 5 | 6 | .. image:: ./media/ItsPrompt.gif 7 | 8 | `ItsPrompt` is an easy-to-use module for managing prompts for the user. It allows you to ask the user for input in a fancy 9 | way, taking care of the problem of user input so you can focus on creating a great program. 10 | 11 | `ItsPrompt` offers many prompt types such as select, raw_select, expand, checkbox, confirm, input, and table. It also 12 | provides prompt autocompletion and validation, customizable style with `prompt_toolkit`, and a helpful toolbar with 13 | error messages. 14 | 15 | This documentation will guide you through the features and usage of `ItsPrompt`, helping you to integrate it into your 16 | projects seamlessly. 17 | 18 | Enjoy exploring `ItsPrompt`! 19 | 20 | Getting Started 21 | =============== 22 | 23 | To get started with `ItsPrompt`, follow the instructions in the :doc:`Getting Started Guide `. 24 | 25 | Table of Contents 26 | ================= 27 | 28 | .. toctree:: 29 | :maxdepth: 2 30 | :caption: Guides: 31 | 32 | guide/getting_started 33 | guide/usage 34 | guide/prompt_types 35 | guide/options_and_data 36 | guide/styling 37 | 38 | .. toctree:: 39 | :maxdepth: 3 40 | :caption: API Documentation: 41 | 42 | api/prompt 43 | api/objects 44 | 45 | .. toctree:: 46 | :maxdepth: 2 47 | :caption: Development Guide: 48 | 49 | development_guide/getting_started 50 | development_guide/documentation 51 | -------------------------------------------------------------------------------- /docs/source/media/ItsPrompt.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheItsProjects/ItsPrompt/b2f959dba2337775420e1cd525002e1df72f8c99/docs/source/media/ItsPrompt.gif -------------------------------------------------------------------------------- /docs/source/media/checkbox.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheItsProjects/ItsPrompt/b2f959dba2337775420e1cd525002e1df72f8c99/docs/source/media/checkbox.png -------------------------------------------------------------------------------- /docs/source/media/confirm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheItsProjects/ItsPrompt/b2f959dba2337775420e1cd525002e1df72f8c99/docs/source/media/confirm.png -------------------------------------------------------------------------------- /docs/source/media/expand.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheItsProjects/ItsPrompt/b2f959dba2337775420e1cd525002e1df72f8c99/docs/source/media/expand.png -------------------------------------------------------------------------------- /docs/source/media/input.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheItsProjects/ItsPrompt/b2f959dba2337775420e1cd525002e1df72f8c99/docs/source/media/input.png -------------------------------------------------------------------------------- /docs/source/media/raw_select.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheItsProjects/ItsPrompt/b2f959dba2337775420e1cd525002e1df72f8c99/docs/source/media/raw_select.png -------------------------------------------------------------------------------- /docs/source/media/select.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheItsProjects/ItsPrompt/b2f959dba2337775420e1cd525002e1df72f8c99/docs/source/media/select.png -------------------------------------------------------------------------------- /docs/source/media/styling/error.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheItsProjects/ItsPrompt/b2f959dba2337775420e1cd525002e1df72f8c99/docs/source/media/styling/error.png -------------------------------------------------------------------------------- /docs/source/media/styling/grayout.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheItsProjects/ItsPrompt/b2f959dba2337775420e1cd525002e1df72f8c99/docs/source/media/styling/grayout.png -------------------------------------------------------------------------------- /docs/source/media/styling/option.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheItsProjects/ItsPrompt/b2f959dba2337775420e1cd525002e1df72f8c99/docs/source/media/styling/option.png -------------------------------------------------------------------------------- /docs/source/media/styling/question.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheItsProjects/ItsPrompt/b2f959dba2337775420e1cd525002e1df72f8c99/docs/source/media/styling/question.png -------------------------------------------------------------------------------- /docs/source/media/styling/question_mark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheItsProjects/ItsPrompt/b2f959dba2337775420e1cd525002e1df72f8c99/docs/source/media/styling/question_mark.png -------------------------------------------------------------------------------- /docs/source/media/styling/selected_option.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheItsProjects/ItsPrompt/b2f959dba2337775420e1cd525002e1df72f8c99/docs/source/media/styling/selected_option.png -------------------------------------------------------------------------------- /docs/source/media/styling/separator.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheItsProjects/ItsPrompt/b2f959dba2337775420e1cd525002e1df72f8c99/docs/source/media/styling/separator.png -------------------------------------------------------------------------------- /docs/source/media/styling/text.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheItsProjects/ItsPrompt/b2f959dba2337775420e1cd525002e1df72f8c99/docs/source/media/styling/text.png -------------------------------------------------------------------------------- /docs/source/media/styling/tooltip.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheItsProjects/ItsPrompt/b2f959dba2337775420e1cd525002e1df72f8c99/docs/source/media/styling/tooltip.png -------------------------------------------------------------------------------- /docs/source/media/styling_input.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheItsProjects/ItsPrompt/b2f959dba2337775420e1cd525002e1df72f8c99/docs/source/media/styling_input.png -------------------------------------------------------------------------------- /docs/source/media/styling_input_annotated.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheItsProjects/ItsPrompt/b2f959dba2337775420e1cd525002e1df72f8c99/docs/source/media/styling_input_annotated.png -------------------------------------------------------------------------------- /docs/source/media/styling_raw_select.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheItsProjects/ItsPrompt/b2f959dba2337775420e1cd525002e1df72f8c99/docs/source/media/styling_raw_select.png -------------------------------------------------------------------------------- /docs/source/media/styling_raw_select_annotated.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheItsProjects/ItsPrompt/b2f959dba2337775420e1cd525002e1df72f8c99/docs/source/media/styling_raw_select_annotated.png -------------------------------------------------------------------------------- /docs/source/media/table.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheItsProjects/ItsPrompt/b2f959dba2337775420e1cd525002e1df72f8c99/docs/source/media/table.png -------------------------------------------------------------------------------- /example.py: -------------------------------------------------------------------------------- 1 | # mypy: disable-error-code=assignment 2 | from ItsPrompt.objects.prompts.separator import Separator 3 | 4 | try: 5 | from pandas import DataFrame 6 | 7 | NO_PANDAS = False 8 | except ModuleNotFoundError: 9 | NO_PANDAS = True 10 | 11 | from ItsPrompt.prompt import Prompt 12 | 13 | # select 14 | ans = Prompt.select( 15 | 'What food would you like?', 16 | (Separator('The veggies'), 'Salad', Separator('The meaties'), 'Pizza', 'Burger'), 17 | default='Pizza', 18 | ) 19 | print(ans) 20 | 21 | # raw_select 22 | ans = Prompt.raw_select( 23 | 'What pizza would you like?', 24 | ('Salami', 'Hawaii', 'four-cheese'), 25 | allow_keyboard=True, 26 | ) 27 | print(ans) 28 | 29 | # checkbox 30 | ans = Prompt.checkbox( 31 | 'What beverages would you like?', 32 | ('Coke', 'Water', 'Juice'), 33 | default_checked=('Water',), 34 | disabled=("Coke",), 35 | min_selections=1, 36 | ) 37 | print(ans) 38 | 39 | # expand 40 | ans = Prompt.expand( 41 | 'Where do you want your food to be delivered?', 42 | ('my home', 'another home'), 43 | allow_keyboard=True, 44 | ) 45 | print(ans) 46 | 47 | # standard input with simple lambda validation 48 | ans = Prompt.input( 49 | 'Please type your id', 50 | validate=lambda x: "test" in x, 51 | ) 52 | 53 | print(ans) 54 | 55 | 56 | # standard input with validation 57 | def input_not_empty(input: str) -> str | None: 58 | if len(input) == 0: 59 | return 'Input field can not be empty!' 60 | 61 | return None 62 | 63 | 64 | ans = Prompt.input( 65 | 'Please type your name', 66 | validate=input_not_empty, 67 | ) 68 | 69 | print(ans) 70 | 71 | # standard input with validation and completion 72 | ans = Prompt.input( 73 | 'Please type your address', 74 | validate=input_not_empty, 75 | completions=['Mainstreet 4', 'Fifth way'], 76 | completion_show_multicolumn=True, 77 | ) 78 | print(ans) 79 | 80 | # standard input with password (show_symbol) 81 | ans = Prompt.input( 82 | 'Please type your password', 83 | show_symbol='*', 84 | ) 85 | 86 | print(ans) 87 | 88 | # confirm 89 | ans = Prompt.confirm( 90 | 'Is the information correct?', 91 | default=True, 92 | ) 93 | print(ans) 94 | 95 | # table 96 | if not NO_PANDAS: 97 | data = DataFrame({ 98 | 'Food': ['Pizza', 'Burger', 'Salad'], 99 | 'Qty': [1, 0, 0], 100 | }) 101 | 102 | ans = Prompt.table( 103 | 'Please fill in your quantity', 104 | data, 105 | ) 106 | 107 | print(ans) 108 | 109 | # styling 110 | 111 | # examples for the different styling class components 112 | Prompt.raw_select(question='question', options=( 113 | 'option', 114 | 'selected_option', 115 | )) 116 | 117 | Prompt.input( 118 | question='question', 119 | default='grayout', 120 | validate=lambda x: 'error', 121 | ) 122 | 123 | # change default style 124 | from ItsPrompt.data.style import default_style 125 | 126 | default_style.error = 'fg:ansired bg:ansiwhite' 127 | 128 | # create your own style 129 | from ItsPrompt.data.style import PromptStyle 130 | 131 | my_style = PromptStyle( 132 | question_mark='fg:ansiblue', 133 | error='fg:ansired bg:ansiwhite', 134 | ) 135 | 136 | # copy default style 137 | from ItsPrompt.data.style import create_from_default 138 | 139 | my_style = create_from_default() 140 | 141 | my_style.error = 'fg:ansired bg:ansiwhite' 142 | -------------------------------------------------------------------------------- /examples/demo.py: -------------------------------------------------------------------------------- 1 | # mypy: disable-error-code=assignment 2 | 3 | # Demo file for ItsPrompt 4 | # This file demonstrates the usage of all available prompts. 5 | from ItsPrompt.objects.prompts.separator import Separator 6 | 7 | try: 8 | from pandas import DataFrame 9 | 10 | NO_PANDAS = False 11 | except ModuleNotFoundError: 12 | NO_PANDAS = True 13 | 14 | from ItsPrompt.prompt import Prompt 15 | 16 | # select 17 | ans = Prompt.select( 18 | 'What food would you like?', 19 | (Separator('The veggies'), 'Salad', Separator('The meaties'), 'Pizza', 'Burger'), 20 | default='Pizza', 21 | ) 22 | print(ans) 23 | 24 | # raw_select 25 | ans = Prompt.raw_select( 26 | 'What pizza would you like?', 27 | ('Salami', 'Hawaii', 'four-cheese'), 28 | allow_keyboard=True, 29 | ) 30 | print(ans) 31 | 32 | # checkbox 33 | ans = Prompt.checkbox( 34 | 'What beverages would you like?', 35 | ('Coke', 'Water', 'Juice'), 36 | default_checked=('Water',), 37 | disabled=("Coke",), 38 | min_selections=1, 39 | ) 40 | print(ans) 41 | 42 | # expand 43 | ans = Prompt.expand( 44 | 'Where do you want your food to be delivered?', 45 | ('my home', 'another home'), 46 | allow_keyboard=True, 47 | ) 48 | print(ans) 49 | 50 | # standard input with simple lambda validation 51 | ans = Prompt.input( 52 | 'Please type your id', 53 | validate=lambda x: "test" in x, 54 | ) 55 | 56 | print(ans) 57 | 58 | 59 | # standard input with validation 60 | def input_not_empty(input: str) -> str | None: 61 | if len(input) == 0: 62 | return 'Input field can not be empty!' 63 | 64 | return None 65 | 66 | 67 | ans = Prompt.input( 68 | 'Please type your name', 69 | validate=input_not_empty, 70 | ) 71 | 72 | print(ans) 73 | 74 | # standard input with validation and completion 75 | ans = Prompt.input( 76 | 'Please type your address', 77 | validate=input_not_empty, 78 | completions=['Mainstreet 4', 'Fifth way'], 79 | completion_show_multicolumn=True, 80 | ) 81 | print(ans) 82 | 83 | # standard input with password (show_symbol) 84 | ans = Prompt.input( 85 | 'Please type your password', 86 | show_symbol='*', 87 | ) 88 | 89 | print(ans) 90 | 91 | # confirm 92 | ans = Prompt.confirm( 93 | 'Is the information correct?', 94 | default=True, 95 | ) 96 | print(ans) 97 | 98 | # table 99 | if not NO_PANDAS: 100 | data = DataFrame({ 101 | 'Food': ['Pizza', 'Burger', 'Salad'], 102 | 'Qty': [1, 0, 0], 103 | }) 104 | 105 | ans = Prompt.table( 106 | 'Please fill in your quantity', 107 | data, 108 | ) 109 | 110 | print(ans) 111 | -------------------------------------------------------------------------------- /examples/demos/checkbox_demo.py: -------------------------------------------------------------------------------- 1 | from ItsPrompt.prompt import Prompt 2 | 3 | # checkbox 4 | Prompt.checkbox( 5 | 'What beverages would you like?', 6 | ('Coke', 'Water', 'Juice'), 7 | default_checked=('Water',), 8 | disabled=("Coke",), 9 | min_selections=1, 10 | ) 11 | -------------------------------------------------------------------------------- /examples/demos/confirm_demo.py: -------------------------------------------------------------------------------- 1 | from ItsPrompt.prompt import Prompt 2 | 3 | # confirm 4 | Prompt.confirm( 5 | 'Is the information correct?', 6 | default=True, 7 | ) 8 | -------------------------------------------------------------------------------- /examples/demos/expand_demo.py: -------------------------------------------------------------------------------- 1 | from ItsPrompt.prompt import Prompt 2 | 3 | # expand 4 | Prompt.expand( 5 | 'Where do you want your food to be delivered?', 6 | ('my home', 'another home'), 7 | allow_keyboard=True, 8 | ) 9 | -------------------------------------------------------------------------------- /examples/demos/input_demo.py: -------------------------------------------------------------------------------- 1 | from ItsPrompt.prompt import Prompt 2 | 3 | 4 | # standard input validation 5 | def input_not_empty(input: str) -> str | None: 6 | if len(input) == 0: 7 | return 'Input field can not be empty!' 8 | 9 | return None 10 | 11 | 12 | # input 13 | Prompt.input( 14 | 'Please type your address', 15 | validate=input_not_empty, 16 | completions=['Mainstreet 4', 'Fifth way'], 17 | completion_show_multicolumn=True, 18 | ) 19 | -------------------------------------------------------------------------------- /examples/demos/raw_select_demo.py: -------------------------------------------------------------------------------- 1 | from ItsPrompt.prompt import Prompt 2 | 3 | # raw_select 4 | Prompt.raw_select( 5 | 'What pizza would you like?', 6 | ('Salami', 'Hawaii', 'four-cheese'), 7 | allow_keyboard=True, 8 | ) 9 | -------------------------------------------------------------------------------- /examples/demos/select_demo.py: -------------------------------------------------------------------------------- 1 | from ItsPrompt.objects.prompts.separator import Separator 2 | from ItsPrompt.prompt import Prompt 3 | 4 | Prompt.select( 5 | 'What food would you like?', 6 | (Separator('The veggies'), 'Salad', Separator('The meaties'), 'Pizza', 'Burger'), 7 | default='Pizza', 8 | ) 9 | -------------------------------------------------------------------------------- /examples/demos/table_demo.py: -------------------------------------------------------------------------------- 1 | from pandas import DataFrame 2 | 3 | from ItsPrompt.prompt import Prompt 4 | 5 | # table 6 | data = DataFrame({ 7 | 'Food': ['Pizza', 'Burger', 'Salad'], 8 | 'Qty': [1, 0, 0], 9 | }) 10 | 11 | Prompt.table( 12 | 'Please fill in your quantity', 13 | data, 14 | ) 15 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [project] 6 | name = "ItsPrompt" 7 | version = "1.5" 8 | authors = [ 9 | { name = "ItsNameless" }, 10 | ] 11 | description = "Prompting - the fancy way" 12 | readme = "README.md" 13 | requires-python = ">=3.8" 14 | dependencies = [ 15 | "prompt-toolkit>=3.0.37,<4.0", 16 | ] 17 | classifiers = [ 18 | "Programming Language :: Python :: 3", 19 | "License :: OSI Approved :: MIT License", 20 | "Operating System :: OS Independent", 21 | ] 22 | license = { file = "LICENSE" } 23 | 24 | [project.urls] 25 | Homepage = "https://github.com/TheItsProjects/ItsPrompt" 26 | Repository = "https://github.com/TheItsProjects/ItsPrompt" 27 | "Issue Tracker" = "https://github.com/TheItsProjects/ItsPrompt/issues" 28 | 29 | [tool.setuptools.packages.find] 30 | where = ["."] 31 | include = [ 32 | "ItsPrompt", 33 | "ItsPrompt.*", 34 | ] 35 | 36 | [options.extras_require] 37 | df = "pandas>=1.5.3" 38 | 39 | [tool.pytest.ini_options] 40 | addopts = "--cov=ItsPrompt --cov-report term-missing" 41 | testpaths = [ 42 | "tests", 43 | ] 44 | filterwarnings = [ 45 | # ignore warning, see https://github.com/prompt-toolkit/python-prompt-toolkit/issues/1696 46 | "ignore:There is no current event loop:DeprecationWarning", 47 | ] 48 | 49 | [tool.yapf] 50 | space_between_ending_comma_and_closing_bracket = false 51 | column_limit = 120 52 | each_dict_entry_on_separate_line = false 53 | dedent_closing_brackets = true 54 | 55 | [tool.mypy] 56 | packages = "ItsPrompt" 57 | explicit_package_bases = true 58 | check_untyped_defs = true 59 | exclude = ["docs", "venv"] 60 | -------------------------------------------------------------------------------- /requirements-dev.txt: -------------------------------------------------------------------------------- 1 | pre-commit==3.3.1 2 | pytest==7.3.1 3 | yapf==0.43.0 4 | mypy==1.2.0 5 | pytest-cov==4.0.0 6 | pandas-stubs==2.0.1.230501 7 | pandas>=1.5.3 8 | toml==0.10.2 9 | sphinx==7.2.6 10 | sphinx-rtd-theme==2.0.0 11 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | prompt-toolkit>=3.0.37,<4.0 2 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheItsProjects/ItsPrompt/b2f959dba2337775420e1cd525002e1df72f8c99/tests/__init__.py -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | import os 2 | from typing import NamedTuple 3 | 4 | import pytest 5 | from prompt_toolkit.application import create_app_session 6 | from prompt_toolkit.input import PipeInput, ansi_escape_sequences, create_pipe_input 7 | from prompt_toolkit.keys import Keys 8 | from prompt_toolkit.output import DummyOutput 9 | 10 | 11 | def key_converter(*keys: Keys | str) -> str: 12 | """ 13 | Convert a key sequence to a sequence of stringified keys 14 | 15 | The keys may either be a `prompt_toolkit.keys.Keys` or a string, which will be interpreted as text to type. 16 | 17 | The returned string is usable with `PipeInput.send_text()`. 18 | 19 | :return: all given keys combined to a single string 20 | :rtype: str 21 | """ 22 | out = "" 23 | 24 | for key in keys: 25 | if type(key) is Keys: 26 | out += ansi_escape_sequences.REVERSE_ANSI_SEQUENCES[key] 27 | else: 28 | out += key 29 | 30 | return out 31 | 32 | 33 | @pytest.fixture(autouse=True, scope="function") 34 | def mock_input(): 35 | """ 36 | Fixture for creating a dummy prompt session 37 | 38 | Terminal inputs can be created by using pipe_input. 39 | """ 40 | with create_pipe_input() as pipe_input: 41 | with create_app_session(input=pipe_input, output=DummyOutput()): 42 | yield pipe_input 43 | 44 | 45 | @pytest.fixture(autouse=True, scope="function") 46 | def send_keys(mock_input: PipeInput): 47 | """ 48 | Fixture for easily sending keys to the terminal session 49 | 50 | The returned callable can be called to convert a list of Keys or strings to one string, which will then be sent 51 | to the terminal session via PipeInput. 52 | """ 53 | yield lambda *x: mock_input.send_text(key_converter(*x)) 54 | 55 | 56 | class FakeTerminalSize(NamedTuple): 57 | columns: int 58 | lines: int 59 | 60 | 61 | @pytest.fixture(autouse=True) 62 | def mock_terminal_size(monkeypatch: pytest.MonkeyPatch): 63 | 64 | def get_fake_terminal_size(x=None): 65 | """ 66 | Fixture for mocking the output of the `os.get_terminal_size()` method 67 | 68 | This fixture is needed in order for `TablePrompt()` to work. 69 | """ 70 | return FakeTerminalSize(180, 60) 71 | 72 | monkeypatch.setattr(os, "get_terminal_size", get_fake_terminal_size) 73 | -------------------------------------------------------------------------------- /tests/data/test_checkbox.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from ItsPrompt.data.checkbox import CheckboxOption, process_data 4 | from ItsPrompt.objects.prompts.separator import Separator 5 | 6 | 7 | def test_process_data_standard_options(): 8 | options = ("first", "second", "third") 9 | 10 | result = [ 11 | CheckboxOption("first", "first", False, False), 12 | CheckboxOption("second", "second", False, False), 13 | CheckboxOption("third", "third", False, False), 14 | ] 15 | 16 | ans = process_data(options) 17 | 18 | assert ans == result 19 | 20 | 21 | def test_process_data_tuple_options(): 22 | options = ( 23 | ("first", "1"), 24 | ("second", "2"), 25 | ("third", "3"), 26 | ) 27 | 28 | result = [ 29 | CheckboxOption("first", "1", False, False), 30 | CheckboxOption("second", "2", False, False), 31 | CheckboxOption("third", "3", False, False), 32 | ] 33 | 34 | ans = process_data(options) # type: ignore 35 | 36 | assert ans == result 37 | 38 | 39 | def test_process_data_raises_type_error(): 40 | options = ("first", "second", 3) 41 | 42 | with pytest.raises(TypeError): 43 | ans = process_data(options) # type: ignore # mypy: ignore 44 | 45 | 46 | def test_process_data_with_separator(): 47 | separator = Separator("second") 48 | options = ("first", separator, "third") 49 | 50 | result = [CheckboxOption("first", "first", False, False), separator, CheckboxOption("third", "third", False, False)] 51 | 52 | ans = process_data(options) 53 | 54 | assert ans.with_separators == result 55 | -------------------------------------------------------------------------------- /tests/data/test_expand.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from ItsPrompt.data.expand import ExpandOption, process_data 4 | from ItsPrompt.objects.prompts.separator import Separator 5 | 6 | 7 | def test_process_data_standard_options(): 8 | options = ("first", "second", "third") 9 | 10 | result = [ 11 | ExpandOption("f", "first", "first", False), 12 | ExpandOption("s", "second", "second", False), 13 | ExpandOption("t", "third", "third", False), 14 | ExpandOption("h", "Help Menu, list or hide all options", "", False), 15 | ] 16 | 17 | ans = process_data(options) 18 | 19 | assert ans == result 20 | 21 | 22 | def test_process_data_tuple_options(): 23 | options = ( 24 | ("1", "first", "1"), 25 | ("2", "second", "2"), 26 | ("3", "third", "3"), 27 | ) 28 | 29 | result = [ 30 | ExpandOption("1", "first", "1", False), 31 | ExpandOption("2", "second", "2", False), 32 | ExpandOption("3", "third", "3", False), 33 | ExpandOption("h", "Help Menu, list or hide all options", "", False), 34 | ] 35 | 36 | ans = process_data(options) 37 | 38 | assert ans == result 39 | 40 | 41 | def test_process_data_raises_unique_keys(): 42 | options = ("double", "double") 43 | 44 | with pytest.raises(ValueError): 45 | ans = process_data(options) 46 | 47 | 48 | def test_process_data_raises_key_too_long(): 49 | options = (("tt", "too long", "tt"),) 50 | 51 | with pytest.raises(ValueError): 52 | ans = process_data(options) 53 | 54 | 55 | def test_process_data_raises_key_not_ascii(): 56 | options = (("❓", "not ascii", "na"),) 57 | 58 | with pytest.raises(ValueError): 59 | ans = process_data(options) 60 | 61 | 62 | def test_process_data_raises_h_given(): 63 | options = (("h", "h given", "h"),) 64 | 65 | with pytest.raises(ValueError): 66 | ans = process_data(options) 67 | 68 | 69 | def test_process_data_raises_type_error(): 70 | options = (["i", "invalid"],) 71 | 72 | with pytest.raises(TypeError): 73 | ans = process_data(options) # type: ignore # mypy: ignore 74 | 75 | 76 | def test_process_data_with_separator(): 77 | separator = Separator("second") 78 | options = ("first", separator, "third") 79 | 80 | result = [ 81 | ExpandOption("f", "first", "first", False), separator, 82 | ExpandOption("t", "third", "third", False), 83 | ExpandOption(key='h', name='Help Menu, list or hide all options', id='', is_disabled=False) 84 | ] 85 | 86 | ans = process_data(options) 87 | 88 | assert ans.with_separators == result 89 | -------------------------------------------------------------------------------- /tests/data/test_select.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from ItsPrompt.data.select import SelectOption, process_data 4 | from ItsPrompt.objects.prompts.separator import Separator 5 | 6 | 7 | def test_process_data_standard_options(): 8 | options = ("first", "second", "third") 9 | 10 | result = [ 11 | SelectOption("first", "first", False), 12 | SelectOption("second", "second", False), 13 | SelectOption("third", "third", False), 14 | ] 15 | 16 | ans = process_data(options) 17 | 18 | assert ans == result 19 | 20 | 21 | def test_process_data_tuple_options(): 22 | options = ( 23 | ("first", "1"), 24 | ("second", "2"), 25 | ("third", "3"), 26 | ) 27 | 28 | result = [ 29 | SelectOption("first", "1", False), 30 | SelectOption("second", "2", False), 31 | SelectOption("third", "3", False), 32 | ] 33 | 34 | ans = process_data(options) # type: ignore 35 | 36 | assert ans == result 37 | 38 | 39 | def test_process_data_raises_type_error(): 40 | options = ("first", "second", 3) 41 | 42 | with pytest.raises(TypeError): 43 | ans = process_data(options) # type: ignore # mypy: ignore 44 | 45 | 46 | def test_process_data_with_separator(): 47 | separator = Separator("second") 48 | options = ("first", separator, "third") 49 | 50 | result = [SelectOption("first", "first", False), separator, SelectOption("third", "third", False)] 51 | 52 | ans = process_data(options) 53 | 54 | assert ans.with_separators == result 55 | -------------------------------------------------------------------------------- /tests/data/test_style.py: -------------------------------------------------------------------------------- 1 | from ItsPrompt.data.style import create_from_default, default_style 2 | 3 | 4 | def test_create_from_default(): 5 | style = create_from_default() 6 | 7 | assert style == default_style 8 | -------------------------------------------------------------------------------- /tests/data/test_table.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from pandas import DataFrame 4 | 5 | from ItsPrompt.data.table import Table 6 | 7 | 8 | def test_table_shortens_long_header(mock_terminal_size): 9 | width = os.get_terminal_size().columns 10 | 11 | data = DataFrame({"a" * (width - 1): ["first"]}) 12 | 13 | table = Table(data) 14 | 15 | assert "." in table.get_table_as_str().split("\n")[1] 16 | -------------------------------------------------------------------------------- /tests/prompts/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheItsProjects/ItsPrompt/b2f959dba2337775420e1cd525002e1df72f8c99/tests/prompts/__init__.py -------------------------------------------------------------------------------- /tests/prompts/test_checkbox_prompt.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from prompt_toolkit.keys import Keys 3 | 4 | from ItsPrompt.objects.prompts.separator import Separator 5 | from ItsPrompt.prompt import Prompt 6 | 7 | 8 | # --- checkbox --- 9 | @pytest.mark.parametrize( 10 | "keys,i", 11 | [ 12 | [(Keys.Enter,), ()], 13 | [(" ", Keys.Enter), (0,)], 14 | [(Keys.Down, " ", Keys.Enter), (1,)], 15 | [(" ", Keys.Up, " ", Keys.Enter), (0, 2)], 16 | ], 17 | ) 18 | def test_checkbox(send_keys, keys: list[Keys | str], i: list[int]): 19 | options = ("first", "second", "third") 20 | 21 | send_keys(*keys) 22 | 23 | ans = Prompt.checkbox("", options) 24 | 25 | assert ans == [options[n] for n in i] 26 | 27 | 28 | @pytest.mark.parametrize( 29 | "keys,i", 30 | [ 31 | [(Keys.Enter,), (0,)], 32 | [(" ", Keys.Enter), ()], 33 | [(Keys.Down, " ", Keys.Enter), (0, 1)], 34 | [(" ", Keys.Up, " ", Keys.Enter), (2,)], 35 | ], 36 | ) 37 | def test_checkbox_with_default(send_keys, keys: list[Keys | str], i: list[int]): 38 | options = ("first", "second", "third") 39 | 40 | send_keys(*keys) 41 | 42 | ans = Prompt.checkbox("", options, default_checked=("first",)) 43 | 44 | assert ans == [options[n] for n in i] 45 | 46 | 47 | def test_checkbox_raises_invalid_default(): 48 | options = ("first", "second", "third") 49 | 50 | with pytest.raises(ValueError): 51 | ans = Prompt.checkbox("", options, default_checked=("invalid",)) 52 | 53 | 54 | def test_checkbox_raises_keyboard_interrupt(send_keys): 55 | send_keys(Keys.ControlC) 56 | 57 | options = ("first", "second", "third") 58 | 59 | with pytest.raises(KeyboardInterrupt): 60 | ans = Prompt.checkbox("", options) 61 | 62 | 63 | # yapf: disable 64 | @pytest.mark.parametrize( 65 | "keys,i", 66 | [ 67 | [(" ", Keys.Enter,), (1,)], 68 | [(Keys.Up, " ", Keys.Enter), (2,)], 69 | [(Keys.Down, Keys.Down, " ", Keys.Enter), (1,)] 70 | ] 71 | ) 72 | # yapf: enable 73 | def test_checkbox_with_disabled(send_keys, keys: list[Keys | str], i: list[int]): 74 | options = ("first", "second", "third") 75 | 76 | send_keys(*keys) 77 | 78 | ans = Prompt.checkbox("", options, disabled=("first",)) 79 | 80 | assert ans == [options[n] for n in i] 81 | 82 | 83 | def test_checkbox_with_separator(send_keys): 84 | options = ("first", "second", Separator("separator"), "third") 85 | 86 | send_keys(Keys.Down, Keys.Down, " ", Keys.Enter) 87 | 88 | ans = Prompt.checkbox("", options) 89 | 90 | assert ans == ["third"] 91 | 92 | 93 | def test_checkbox_raises_invalid_disabled(): 94 | options = ("first", "second", "third") 95 | with pytest.raises(ValueError): 96 | ans = Prompt.checkbox("", options, disabled=("invalid",)) 97 | 98 | 99 | # TODO check min selections with error box (visual) 100 | -------------------------------------------------------------------------------- /tests/prompts/test_confirm_prompt.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from prompt_toolkit.keys import Keys 3 | 4 | from ItsPrompt.prompt import Prompt 5 | 6 | 7 | # --- confirm --- 8 | @pytest.mark.parametrize( 9 | "key,a", 10 | [ 11 | ["y", True], 12 | ["n", False], 13 | ["ay", True], 14 | ], 15 | ) 16 | def test_confirm(send_keys, key: str, a: bool): 17 | send_keys(key) 18 | 19 | ans = Prompt.confirm("") 20 | 21 | assert ans == a 22 | 23 | 24 | @pytest.mark.parametrize( 25 | "key,a", 26 | [ 27 | ["y", True], 28 | ["n", False], 29 | [Keys.Enter, True], 30 | ], 31 | ) 32 | def test_confirm_with_default(send_keys, key: str, a: bool): 33 | send_keys(key) 34 | 35 | ans = Prompt.confirm("", default=True) 36 | 37 | assert ans == a 38 | 39 | 40 | def test_confirm_raises_keyboard_interrupt(send_keys): 41 | send_keys(Keys.ControlC) 42 | 43 | with pytest.raises(KeyboardInterrupt): 44 | ans = Prompt.confirm("") 45 | 46 | 47 | # TODO confirm selections with error box (visual) 48 | -------------------------------------------------------------------------------- /tests/prompts/test_expand_prompt.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from prompt_toolkit.keys import Keys 3 | 4 | from ItsPrompt.objects.prompts.separator import Separator 5 | from ItsPrompt.prompt import Prompt 6 | 7 | 8 | # --- Expand --- 9 | @pytest.mark.parametrize( 10 | "keys,i", 11 | [ 12 | [("f", Keys.Enter), 0], 13 | [("sft", Keys.Enter), 2], 14 | [(Keys.Enter, "hs", Keys.Enter), 1], 15 | [(Keys.Up, Keys.Down, "99", "s", Keys.Enter), 1], 16 | ], 17 | ) 18 | def test_expand(send_keys, keys: list[Keys | str], i: int): 19 | options = ("first", "second", "third") 20 | 21 | send_keys(*keys) 22 | 23 | ans = Prompt.expand("", options) 24 | 25 | assert ans == options[i] 26 | 27 | 28 | @pytest.mark.parametrize( 29 | "keys,i", 30 | [ 31 | [("f", Keys.Enter), 0], 32 | [("sft", Keys.Enter), 2], 33 | [(Keys.Enter, "hs", Keys.Enter), 1], 34 | ], 35 | ) 36 | def test_expand_with_default(send_keys, keys: list[Keys | str], i: int): 37 | options = ("first", "second", "third") 38 | 39 | send_keys(*keys) 40 | 41 | ans = Prompt.expand("", options, default="second") 42 | 43 | assert ans == options[i] 44 | 45 | 46 | @pytest.mark.parametrize( 47 | "keys,i", 48 | [ 49 | [(Keys.Up, Keys.Enter), 2], 50 | [(Keys.Down, "hs", Keys.Enter), 1], 51 | ], 52 | ) 53 | def test_expand_with_keyboard(send_keys, keys: list[Keys | str], i: int): 54 | options = ("first", "second", "third") 55 | 56 | send_keys(*keys) 57 | 58 | ans = Prompt.expand("", options, allow_keyboard=True) 59 | 60 | assert ans == options[i] 61 | 62 | 63 | def test_expand_raises_invalid_default(): 64 | options = ("first", "second", "third") 65 | 66 | with pytest.raises(ValueError): 67 | ans = Prompt.expand("", options, default="invalid") 68 | 69 | 70 | def test_expand_raises_keyboard_interrupt(send_keys): 71 | send_keys(Keys.ControlC) 72 | 73 | options = ("first", "second", "third") 74 | 75 | with pytest.raises(KeyboardInterrupt): 76 | ans = Prompt.expand("", options) 77 | 78 | 79 | # yapf: disable 80 | @pytest.mark.parametrize( 81 | "keys,i", 82 | [ 83 | [("f", Keys.Enter, "s", Keys.Enter), 1] 84 | ] 85 | ) 86 | # yapf: enable 87 | def test_expand_with_disabled(send_keys, keys: list[Keys | str], i: int): 88 | options = ("first", "second", "third") 89 | 90 | send_keys(*keys) 91 | 92 | ans = Prompt.expand("", options, disabled=("first",)) 93 | 94 | assert ans == options[i] 95 | 96 | 97 | def test_expand_raises_invalid_disabled(): 98 | options = ("first", "second", "third") 99 | with pytest.raises(ValueError): 100 | ans = Prompt.expand("", options, disabled=("invalid",)) 101 | 102 | 103 | def test_expand_raises_default_is_disabled(): 104 | options = ("first", "second", "third") 105 | with pytest.raises(ValueError): 106 | ans = Prompt.expand("", options, default="first", disabled=("first",)) 107 | 108 | 109 | @pytest.mark.parametrize( 110 | "keys,i", 111 | [ 112 | [(Keys.Down, Keys.Enter), 1], 113 | [("s", Keys.Up, Keys.Up, Keys.Enter), 2], # s, h, t 114 | [(Keys.Down, Keys.Enter), 1] 115 | ] 116 | ) 117 | def test_expand_with_disabled_and_keyboard(send_keys, keys: list[Keys | str], i: int): 118 | options = ("first", "second", "third") 119 | 120 | send_keys(*keys) 121 | 122 | ans = Prompt.expand("", options, disabled=("first",), allow_keyboard=True) 123 | 124 | assert ans == options[i] 125 | 126 | 127 | def test_expand_with_separator(send_keys): 128 | options = ("first", "second", Separator("separator"), "third") 129 | 130 | send_keys("h", "t", Keys.Enter) 131 | 132 | ans = Prompt.expand("", options) 133 | 134 | assert ans == "third" 135 | -------------------------------------------------------------------------------- /tests/prompts/test_input_prompt.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from prompt_toolkit.completion import FuzzyWordCompleter 3 | from prompt_toolkit.keys import Keys 4 | 5 | from ItsPrompt.prompt import Prompt 6 | 7 | 8 | # --- input --- 9 | @pytest.mark.parametrize( 10 | "keys,a", 11 | [ 12 | [(Keys.Enter,), ""], 13 | [("test", Keys.Enter), "test"], 14 | ], 15 | ) 16 | def test_input(send_keys, keys: list[Keys | str], a: str): 17 | send_keys(*keys) 18 | 19 | ans = Prompt.input("") 20 | 21 | assert ans == a 22 | 23 | 24 | @pytest.mark.parametrize( 25 | "keys,a", 26 | [ 27 | [(Keys.Enter,), "default"], 28 | [("test", Keys.Enter), "test"], 29 | ], 30 | ) 31 | def test_input_with_default(send_keys, keys: list[Keys | str], a: str): 32 | send_keys(*keys) 33 | 34 | ans = Prompt.input("", default="default") 35 | 36 | assert ans == a 37 | 38 | 39 | KeysAltEnter = Keys.Escape, Keys.Enter # Vt100 terminals convert "alt+key" to "escape,key" 40 | 41 | 42 | @pytest.mark.parametrize( 43 | "keys,a", 44 | [ 45 | [(*KeysAltEnter,), ""], 46 | [("test", Keys.Enter, *KeysAltEnter), "test\n"], 47 | ], 48 | ) 49 | def test_input_with_multiline(send_keys, keys: list[Keys | str], a: str): 50 | send_keys(*keys) 51 | 52 | ans = Prompt.input("", multiline=True) 53 | 54 | assert ans == a 55 | 56 | 57 | def test_input_raises_two_completers_given(): 58 | completions = ["first", "second", "third"] 59 | completer = FuzzyWordCompleter(completions) 60 | 61 | with pytest.raises(ValueError): 62 | ans = Prompt.input("", completions=completions, completer=completer) 63 | 64 | 65 | def test_input_raises_completer_and_show_symbol_given(): 66 | completions = ["first", "second", "third"] 67 | 68 | with pytest.raises(ValueError): 69 | ans = Prompt.input("", completions=completions, show_symbol="*") 70 | 71 | 72 | def test_input_raises_keyboard_interrupt(send_keys): 73 | send_keys(Keys.ControlC) 74 | 75 | with pytest.raises(KeyboardInterrupt): 76 | ans = Prompt.input("") 77 | 78 | 79 | # TODO input show_symbol is showing symbol (visual) 80 | # TODO input completer/completions is working (visual, functional) 81 | # TODO input validation is showing error (visual) 82 | -------------------------------------------------------------------------------- /tests/prompts/test_raw_select_prompt.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from prompt_toolkit.keys import Keys 3 | 4 | from ItsPrompt.objects.prompts.separator import Separator 5 | from ItsPrompt.prompt import Prompt 6 | 7 | 8 | # --- RawSelect --- 9 | @pytest.mark.parametrize( 10 | "keys,i", 11 | [ 12 | [(Keys.Enter,), 0], 13 | [("1", Keys.Enter), 0], 14 | [("2", Keys.Enter), 1], 15 | [(Keys.Up, Keys.Down, "abc", "99", Keys.Enter), 0], 16 | ], 17 | ) 18 | def test_raw_select(send_keys, keys: list[Keys | str], i: int): 19 | options = ("first", "second", "third") 20 | 21 | send_keys(*keys) 22 | 23 | ans = Prompt.raw_select("", options) 24 | 25 | assert ans == options[i] 26 | 27 | 28 | @pytest.mark.parametrize( 29 | "keys,i", 30 | [ 31 | [(Keys.Enter,), 1], 32 | [("1", Keys.Enter), 0], 33 | [("2", Keys.Enter), 1], 34 | ], 35 | ) 36 | def test_raw_select_with_default(send_keys, keys: list[Keys | str], i: int): 37 | options = ("first", "second", "third") 38 | 39 | send_keys(*keys) 40 | 41 | ans = Prompt.raw_select("", options, default="second") 42 | 43 | assert ans == options[i] 44 | 45 | 46 | @pytest.mark.parametrize( 47 | "keys,i", 48 | [ 49 | [(Keys.Enter,), 0], 50 | [(Keys.Down, Keys.Enter), 1], 51 | [(Keys.Up, Keys.Enter), 2], 52 | ], 53 | ) 54 | def test_raw_select_with_keyboard(send_keys, keys: list[Keys | str], i: int): 55 | options = ("first", "second", "third") 56 | 57 | send_keys(*keys) 58 | 59 | ans = Prompt.raw_select("", options, allow_keyboard=True) 60 | 61 | assert ans == options[i] 62 | 63 | 64 | def test_raw_select_raises_invalid_default(): 65 | options = ("first", "second", "third") 66 | 67 | with pytest.raises(ValueError): 68 | ans = Prompt.raw_select("", options, default="invalid") 69 | 70 | 71 | def test_raw_select_raises_keyboard_interrupt(send_keys): 72 | send_keys(Keys.ControlC) 73 | 74 | options = ("first", "second", "third") 75 | 76 | with pytest.raises(KeyboardInterrupt): 77 | ans = Prompt.raw_select("", options) 78 | 79 | 80 | # yapf: disable 81 | @pytest.mark.parametrize( 82 | "keys,i", 83 | [ 84 | [(Keys.Enter,), 1], 85 | [("1", Keys.Enter), 1] 86 | ] 87 | ) 88 | # yapf: enable 89 | def test_raw_select_with_disabled(send_keys, keys: list[Keys | str], i: int): 90 | options = ("first", "second", "third") 91 | 92 | send_keys(*keys) 93 | 94 | ans = Prompt.raw_select("", options, disabled=("first",)) 95 | 96 | assert ans == options[i] 97 | 98 | 99 | # yapf: disable 100 | @pytest.mark.parametrize( 101 | "keys,i", 102 | [ 103 | [(Keys.Enter,), 1], 104 | [(Keys.Up, Keys.Enter), 2], 105 | [(Keys.Down, Keys.Down, Keys.Enter), 1] 106 | ] 107 | ) 108 | # yapf: enable 109 | def test_raw_select_with_disabled_and_keyboard(send_keys, keys: list[Keys | str], i: int): 110 | options = ("first", "second", "third") 111 | 112 | send_keys(*keys) 113 | 114 | ans = Prompt.raw_select("", options, disabled=("first",), allow_keyboard=True) 115 | 116 | assert ans == options[i] 117 | 118 | 119 | def test_raw_select_with_separator(send_keys): 120 | options = ("first", "second", Separator("separator"), "third") 121 | 122 | send_keys("1", "2", Keys.Enter) 123 | 124 | ans = Prompt.raw_select("", options) 125 | 126 | assert ans == "second" 127 | 128 | 129 | def test_raw_select_raises_invalid_disabled(): 130 | options = ("first", "second", "third") 131 | with pytest.raises(ValueError): 132 | ans = Prompt.raw_select("", options, disabled=("invalid",)) 133 | 134 | 135 | def test_raw_select_raises_default_is_disabled(): 136 | options = ("first", "second", "third") 137 | with pytest.raises(ValueError): 138 | ans = Prompt.raw_select("", options, default="first", disabled=("first",)) 139 | -------------------------------------------------------------------------------- /tests/prompts/test_select_prompt.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from prompt_toolkit.keys import Keys 3 | 4 | from ItsPrompt.objects.prompts.separator import Separator 5 | from ItsPrompt.prompt import Prompt 6 | 7 | 8 | # --- Select --- 9 | @pytest.mark.parametrize( 10 | "keys,i", 11 | [ 12 | [(Keys.Enter,), 0], 13 | [(Keys.Down, Keys.Enter), 1], 14 | [(Keys.Down, Keys.Down, Keys.Enter), 2], 15 | [(Keys.Up, Keys.Enter), 2], 16 | ], 17 | ) 18 | def test_select(send_keys, keys: list[Keys | str], i: int): 19 | options = ("first", "second", "third") 20 | 21 | send_keys(*keys) 22 | 23 | ans = Prompt.select("", options) 24 | 25 | assert ans == options[i] 26 | 27 | 28 | @pytest.mark.parametrize( 29 | "keys,i", 30 | [ 31 | [(Keys.Enter,), 1], 32 | [(Keys.Down, Keys.Enter), 2], 33 | [(Keys.Down, Keys.Down, Keys.Enter), 0], 34 | [(Keys.Up, Keys.Enter), 0], 35 | ], 36 | ) 37 | def test_select_with_default(send_keys, keys: list[Keys | str], i: int): 38 | options = ("first", "second", "third") 39 | 40 | send_keys(*keys) 41 | 42 | ans = Prompt.select("", options, default="second") 43 | 44 | assert ans == options[i] 45 | 46 | 47 | def test_select_raises_invalid_default(): 48 | options = ("first", "second", "third") 49 | with pytest.raises(ValueError): 50 | ans = Prompt.select("", options, default="invalid") 51 | 52 | 53 | def test_select_raises_keyboard_interrupt(send_keys): 54 | send_keys(Keys.ControlC) 55 | 56 | options = ("first", "second", "third") 57 | 58 | with pytest.raises(KeyboardInterrupt): 59 | ans = Prompt.select("", options) 60 | 61 | 62 | # yapf: disable 63 | @pytest.mark.parametrize( 64 | "keys,i", 65 | [ 66 | [(Keys.Enter,), 1], 67 | [(Keys.Up, Keys.Enter), 2], 68 | [(Keys.Down, Keys.Down, Keys.Enter), 1] 69 | ] 70 | ) 71 | # yapf: enable 72 | def test_select_with_disabled(send_keys, keys: list[Keys | str], i: int): 73 | options = ("first", "second", "third") 74 | 75 | send_keys(*keys) 76 | 77 | ans = Prompt.select("", options, disabled=("first",)) 78 | 79 | assert ans == options[i] 80 | 81 | 82 | def test_select_with_separator(send_keys): 83 | options = ("first", "second", Separator("separator"), "third") 84 | 85 | send_keys(Keys.Down, Keys.Down, Keys.Enter) 86 | 87 | ans = Prompt.select("", options) 88 | 89 | assert ans == "third" 90 | 91 | 92 | def test_select_raises_invalid_disabled(): 93 | options = ("first", "second", "third") 94 | with pytest.raises(ValueError): 95 | ans = Prompt.select("", options, disabled=("invalid",)) 96 | 97 | 98 | def test_select_raises_default_is_disabled(): 99 | options = ("first", "second", "third") 100 | with pytest.raises(ValueError): 101 | ans = Prompt.select("", options, default="first", disabled=("first",)) 102 | -------------------------------------------------------------------------------- /tests/prompts/test_table_prompt.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from pandas import DataFrame 3 | from pandas.testing import assert_frame_equal 4 | from prompt_toolkit.keys import Keys 5 | 6 | from ItsPrompt.objects.prompts.type import TablePromptDict, TablePromptList 7 | from ItsPrompt.prompt import Prompt 8 | 9 | 10 | # --- table --- 11 | @pytest.mark.parametrize( 12 | "keys,a", 13 | [ 14 | [ 15 | (Keys.Enter,), 16 | DataFrame({"0": ["first", "second", "third"]}), 17 | ], 18 | [ 19 | (Keys.Down, "new", Keys.Enter), 20 | DataFrame({"0": ["first", "secondnew", "third"]}), 21 | ], 22 | [ 23 | (Keys.Left, Keys.Right, Keys.Up, Keys.Down, Keys.Enter), 24 | DataFrame({"0": ["first", "second", "third"]}), 25 | ], 26 | [ 27 | (Keys.Backspace, Keys.Enter), 28 | DataFrame({"0": ["firs", "second", "third"]}), 29 | ], 30 | ], 31 | ) 32 | def test_table_dataframe(send_keys, mock_terminal_size, keys: list[Keys | str], a: DataFrame): 33 | data = DataFrame(["first", "second", "third"]) 34 | 35 | send_keys(*keys) 36 | 37 | ans = Prompt.table("", data) 38 | 39 | assert_frame_equal(ans, a) 40 | 41 | 42 | @pytest.mark.parametrize( 43 | "keys,a", 44 | [ 45 | [ 46 | (Keys.Enter,), 47 | {"0": ["first", "second", "third"]}, 48 | ], 49 | [ 50 | (Keys.Down, "new", Keys.Enter), 51 | {"0": ["first", "secondnew", "third"]}, 52 | ], 53 | [ 54 | (Keys.Left, Keys.Right, Keys.Up, Keys.Down, Keys.Enter), 55 | {"0": ["first", "second", "third"]}, 56 | ], 57 | [ 58 | (Keys.Backspace, Keys.Enter), 59 | {"0": ["firs", "second", "third"]}, 60 | ], 61 | ], 62 | ) 63 | def test_table_dictionary(send_keys, mock_terminal_size, keys: list[Keys | str], a: TablePromptDict): 64 | data = {"0": ["first", "second", "third"]} 65 | 66 | send_keys(*keys) 67 | 68 | ans = Prompt.table("", data) 69 | 70 | assert ans == a 71 | 72 | 73 | @pytest.mark.parametrize( 74 | "keys,a", 75 | [ 76 | [ 77 | (Keys.Enter,), 78 | [["first", "second", "third"]], 79 | ], 80 | [ 81 | (Keys.Down, "new", Keys.Enter), 82 | [["first", "secondnew", "third"]], 83 | ], 84 | [ 85 | (Keys.Left, Keys.Right, Keys.Up, Keys.Down, Keys.Enter), 86 | [["first", "second", "third"]], 87 | ], 88 | [ 89 | (Keys.Backspace, Keys.Enter), 90 | [["firs", "second", "third"]], 91 | ], 92 | ], 93 | ) 94 | def test_table_list(send_keys, mock_terminal_size, keys: list[Keys | str], a: TablePromptList): 95 | data = [["first", "second", "third"]] 96 | 97 | send_keys(*keys) 98 | 99 | ans = Prompt.table("", data) 100 | 101 | assert ans == a 102 | 103 | 104 | def test_table_raises_keyboard_interrupt(send_keys): 105 | send_keys(Keys.ControlC) 106 | 107 | data = DataFrame(["first", "second", "third"]) 108 | 109 | with pytest.raises(KeyboardInterrupt): 110 | ans = Prompt.table("", data) 111 | 112 | 113 | def test_table_dictionary_raises_lists_not_same_lengths(send_keys): 114 | send_keys(Keys.ControlC) 115 | 116 | data = {"0": ["first", "second", "third"], "1": ["other first"]} 117 | 118 | with pytest.raises(ValueError): 119 | ans = Prompt.table("", data) 120 | --------------------------------------------------------------------------------