[
  {
    "path": ".circleci/config.yml",
    "content": "version: 2.1\n\norbs:\n  python: circleci/python@2.1.1\n\nworkflows:\n  build_and_test:\n    jobs:\n      - build_and_test\n\njobs:\n  build_and_test:\n    executor: python/default\n    steps:\n      - checkout\n      - python/install-packages:\n          pkg-manager: pip\n      - run:\n          name: Run tests\n          command: make tests\n      - persist_to_workspace:\n          root: ~/project\n          paths:\n            - ."
  },
  {
    "path": ".gitignore",
    "content": "._*\n\n__pycache__/\n*.pyc\n.mypy_cache/\nhtmlcov/\n.coverage\n\n\n.DS_Store\n.Trashes\n\nbuild\ncutie.egg-info\ndist\n"
  },
  {
    "path": "Makefile",
    "content": ".PHONY: tests, coverage, lint, release\n\ntests:\n\tpython -m unittest\n\ncoverage:\n\tpython -m coverage erase\n\tpython -m coverage run --source=cutie -m unittest\n\tpython -m coverage report\n\tpython -m coverage html\n\tcoveralls\n\nblack:\n\tblack *.py\n\nrelease: tests, black\n\tpython setup.py sdist bdist_wheel\n\ttwine upload dist/*\n"
  },
  {
    "path": "cutie.py",
    "content": "#! /usr/bin/env python3\n\"\"\"\nCommandline User Tools for Input Easification\n\"\"\"\n\n__version__ = \"0.3.2\"\n__author__ = \"Hans / Kamik423\"\n__license__ = \"MIT\"\n\n\nimport getpass\nfrom typing import List, Optional\n\nimport readchar\nfrom colorama import init\n\ninit()\n\n\nclass DefaultKeys:\n    \"\"\"List of default keybindings.\n\n    Attributes:\n        interrupt(List[str]): Keys that cause a keyboard interrupt.\n        select(List[str]): Keys that trigger list element selection.\n        confirm(List[str]): Keys that trigger list confirmation.\n        delete(List[str]): Keys that trigger character deletion.\n        down(List[str]): Keys that select the element below.\n        up(List[str]): Keys that select the element above.\n    \"\"\"\n\n    interrupt: List[str] = [readchar.key.CTRL_C, readchar.key.CTRL_D]\n    select: List[str] = [readchar.key.SPACE]\n    confirm: List[str] = [readchar.key.ENTER]\n    delete: List[str] = [readchar.key.BACKSPACE]\n    down: List[str] = [readchar.key.DOWN, \"j\"]\n    up: List[str] = [readchar.key.UP, \"k\"]\n\n\ndef get_number(\n    prompt: str,\n    min_value: Optional[float] = None,\n    max_value: Optional[float] = None,\n    allow_float: bool = True,\n) -> float:\n    \"\"\"Get a number from user input.\n    If an invalid number is entered the user will be prompted again.\n\n    Args:\n        prompt (str): The prompt asking the user to input.\n        min_value (float, optional): The [inclusive] minimum value.\n        max_value (float, optional): The [inclusive] maximum value.\n        allow_float (bool, optional): Allow floats or force integers.\n\n    Returns:\n        float: The number input by the user.\n    \"\"\"\n    return_value: Optional[float] = None\n    while return_value is None:\n        input_value = input(prompt + \" \")\n        try:\n            return_value = float(input_value)\n        except ValueError:\n            print(\"Not a valid number.\\033[K\\033[1A\\r\\033[K\", end=\"\")\n        if not allow_float and return_value is not None:\n            if return_value != int(return_value):\n                print(\"Has to be an integer.\\033[K\\033[1A\\r\\033[K\", end=\"\")\n                return_value = None\n        if min_value is not None and return_value is not None:\n            if return_value < min_value:\n                print(f\"Has to be at least {min_value}.\\033[K\\033[1A\\r\\033[K\", end=\"\")\n                return_value = None\n        if max_value is not None and return_value is not None:\n            if return_value > max_value:\n                print(f\"Has to be at most {max_value}.\\033[1A\\r\\033[K\", end=\"\")\n                return_value = None\n        if return_value is not None:\n            break\n    print(\"\\033[K\", end=\"\")\n    if allow_float:\n        return return_value\n    return int(return_value)\n\n\ndef secure_input(prompt: str) -> str:\n    \"\"\"Get secure input without showing it in the command line.\n\n    Args:\n        prompt (str): The prompt asking the user to input.\n\n    Returns:\n        str: The secure input.\n    \"\"\"\n    return getpass.getpass(prompt + \" \")\n\n\ndef select(\n    options: List[str],\n    caption_indices: Optional[List[int]] = None,\n    deselected_prefix: str = \"\\033[1m[ ]\\033[0m \",\n    selected_prefix: str = \"\\033[1m[\\033[32;1mx\\033[0;1m]\\033[0m \",\n    caption_prefix: str = \"\",\n    selected_index: int = 0,\n    confirm_on_select: bool = True,\n) -> int:\n    \"\"\"Select an option from a list.\n\n    Args:\n        options (List[str]): The options to select from.\n        caption_indices (List[int], optional): Non-selectable indices.\n        deselected_prefix (str, optional): Prefix for deselected option ([ ]).\n        selected_prefix (str, optional): Prefix for selected option ([x]).\n        caption_prefix (str, optional): Prefix for captions ().\n        selected_index (int, optional): The index to be selected at first.\n        confirm_on_select (bool, optional): Select keys also confirm.\n\n    Returns:\n        int: The index that has been selected.\n    \"\"\"\n    print(\"\\n\" * (len(options) - 1))\n    if caption_indices is None:\n        caption_indices = []\n    while True:\n        print(f\"\\033[{len(options) + 1}A\")\n        for i, option in enumerate(options):\n            if i not in caption_indices:\n                print(\n                    \"\\033[K{}{}\".format(\n                        selected_prefix if i == selected_index else deselected_prefix,\n                        option,\n                    )\n                )\n            elif i in caption_indices:\n                print(\"\\033[K{}{}\".format(caption_prefix, options[i]))\n        keypress = readchar.readkey()\n        if keypress in DefaultKeys.up:\n            new_index = selected_index\n            while new_index > 0:\n                new_index -= 1\n                if new_index not in caption_indices:\n                    selected_index = new_index\n                    break\n        elif keypress in DefaultKeys.down:\n            new_index = selected_index\n            while new_index < len(options) - 1:\n                new_index += 1\n                if new_index not in caption_indices:\n                    selected_index = new_index\n                    break\n        elif (\n            keypress in DefaultKeys.confirm\n            or confirm_on_select\n            and keypress in DefaultKeys.select\n        ):\n            break\n        elif keypress in DefaultKeys.interrupt:\n            raise KeyboardInterrupt\n    return selected_index\n\n\ndef select_multiple(\n    options: List[str],\n    caption_indices: Optional[List[int]] = None,\n    deselected_unticked_prefix: str = \"\\033[1m( )\\033[0m \",\n    deselected_ticked_prefix: str = \"\\033[1m(\\033[32mx\\033[0;1m)\\033[0m \",\n    selected_unticked_prefix: str = \"\\033[32;1m{ }\\033[0m \",\n    selected_ticked_prefix: str = \"\\033[32;1m{x}\\033[0m \",\n    caption_prefix: str = \"\",\n    ticked_indices: Optional[List[int]] = None,\n    cursor_index: int = 0,\n    minimal_count: int = 0,\n    maximal_count: Optional[int] = None,\n    hide_confirm: bool = True,\n    deselected_confirm_label: str = \"\\033[1m(( confirm ))\\033[0m\",\n    selected_confirm_label: str = \"\\033[1;32m{{ confirm }}\\033[0m\",\n) -> List[int]:\n    \"\"\"Select multiple options from a list.\n\n    Args:\n        options (List[str]): The options to select from.\n        caption_indices (List[int], optional): Non-selectable indices.\n        deselected_unticked_prefix (str, optional): Prefix for lines that are\n            not selected and not ticked (( )).\n        deselected_ticked_prefix (str, optional): Prefix for lines that are\n            not selected but ticked ((x)).\n        selected_unticked_prefix (str, optional): Prefix for lines that are\n            selected but not ticked ({ }).\n        selected_ticked_prefix (str, optional): Prefix for lines that are\n            selected and ticked ({x}).\n        caption_prefix (str, optional): Prefix for captions ().\n        ticked_indices (List[int], optional): Indices that are\n            ticked initially.\n        cursor_index (int, optional): The index the cursor starts at.\n        minimal_count (int, optional): The minimal amount of lines\n            that have to be ticked.\n        maximal_count (int, optional): The maximal amount of lines\n            that have to be ticked.\n        hide_confirm (bool, optional): Hide the confirm button.\n            This causes <ENTER> to confirm the entire selection and not just\n            tick the line.\n        deselected_confirm_label (str, optional): The confirm label\n            if not selected ((( confirm ))).\n        selected_confirm_label (str, optional): The confirm label\n            if selected ({{ confirm }}).\n\n    Returns:\n        List[int]: The indices that have been selected\n    \"\"\"\n    print(\"\\n\" * (len(options) - 1))\n    if caption_indices is None:\n        caption_indices = []\n    if ticked_indices is None:\n        ticked_indices = []\n    max_index = len(options) - (1 if hide_confirm else 0)\n    error_message = \"\"\n    while True:\n        print(f\"\\033[{len(options) + 1}A\")\n        for i, option in enumerate(options):\n            prefix = \"\"\n            if i in caption_indices:\n                prefix = caption_prefix\n            elif i == cursor_index:\n                if i in ticked_indices:\n                    prefix = selected_ticked_prefix\n                else:\n                    prefix = selected_unticked_prefix\n            else:\n                if i in ticked_indices:\n                    prefix = deselected_ticked_prefix\n                else:\n                    prefix = deselected_unticked_prefix\n            print(\"\\033[K{}{}\".format(prefix, option))\n        if hide_confirm:\n            print(f\"{error_message}\\033[K\", end=\"\", flush=True)\n        else:\n            if cursor_index == max_index:\n                print(\n                    f\"{selected_confirm_label} {error_message}\\033[K\",\n                    end=\"\",\n                    flush=True,\n                )\n            else:\n                print(\n                    f\"{deselected_confirm_label} {error_message}\\033[K\",\n                    end=\"\",\n                    flush=True,\n                )\n        error_message = \"\"\n        keypress = readchar.readkey()\n        if keypress in DefaultKeys.up:\n            new_index = cursor_index\n            while new_index > 0:\n                new_index -= 1\n                if new_index not in caption_indices:\n                    cursor_index = new_index\n                    break\n        elif keypress in DefaultKeys.down:\n            new_index = cursor_index\n            while new_index + 1 <= max_index:\n                new_index += 1\n                if new_index not in caption_indices:\n                    cursor_index = new_index\n                    break\n        elif (\n            hide_confirm\n            and keypress in DefaultKeys.confirm\n            or not hide_confirm\n            and cursor_index == max_index\n        ):\n            if minimal_count > len(ticked_indices):\n                error_message = f\"Must select at least {minimal_count} options\"\n            elif maximal_count is not None and maximal_count < len(ticked_indices):\n                error_message = f\"Must select at most {maximal_count} options\"\n            else:\n                break\n        elif (\n            keypress in DefaultKeys.select\n            or not hide_confirm\n            and keypress in DefaultKeys.confirm\n        ):\n            if cursor_index in ticked_indices:\n                ticked_indices.remove(cursor_index)\n            else:\n                ticked_indices.append(cursor_index)\n        elif keypress in DefaultKeys.interrupt:\n            raise KeyboardInterrupt\n    print(\"\\r\\033[K\", end=\"\", flush=True)\n    return ticked_indices\n\n\ndef prompt_yes_or_no(\n    question: str,\n    yes_text: str = \"Yes\",\n    no_text: str = \"No\",\n    has_to_match_case: bool = False,\n    enter_empty_confirms: bool = True,\n    default_is_yes: bool = False,\n    deselected_prefix: str = \"  \",\n    selected_prefix: str = \"\\033[31m>\\033[0m \",\n    char_prompt: bool = True,\n) -> Optional[bool]:\n    \"\"\"Prompt the user to input yes or no.\n\n    Args:\n        question (str): The prompt asking the user to input.\n        yes_text (str, optional): The text corresponding to 'yes'.\n        no_text (str, optional): The text corresponding to 'no'.\n        has_to_match_case (bool, optional): Does the case have to match.\n        enter_empty_confirms (bool, optional): Does enter on empty string work.\n        default_is_yes (bool, optional): Is yes selected by default (no).\n        deselected_prefix (str, optional): Prefix if something is deselected.\n        selected_prefix (str, optional): Prefix if something is selected (> )\n        char_prompt (bool, optional): Add a [Y/N] to the prompt.\n\n    Returns:\n        Optional[bool]: The bool what has been selected.\n    \"\"\"\n    is_yes = default_is_yes\n    is_selected = enter_empty_confirms\n    current_message = \"\"\n    yn_prompt = f\" ({yes_text[0]}/{no_text[0]}) \" if char_prompt else \": \"\n    print()\n    while True:\n        yes = is_yes and is_selected\n        no = not is_yes and is_selected\n        print(\"\\033[K\" f\"{selected_prefix if yes else deselected_prefix}{yes_text}\")\n        print(\"\\033[K\" f\"{selected_prefix if no else deselected_prefix}{no_text}\")\n        print(\n            \"\\033[3A\\r\\033[K\" f\"{question}{yn_prompt}{current_message}\",\n            end=\"\",\n            flush=True,\n        )\n        keypress = readchar.readkey()\n        if keypress in DefaultKeys.down or keypress in DefaultKeys.up:\n            is_yes = not is_yes\n            is_selected = True\n            current_message = yes_text if is_yes else no_text\n        elif keypress in DefaultKeys.delete:\n            if current_message:\n                current_message = current_message[:-1]\n        elif keypress in DefaultKeys.interrupt:\n            raise KeyboardInterrupt\n        elif keypress in DefaultKeys.confirm:\n            if is_selected:\n                break\n        elif keypress in \"\\t\":\n            if is_selected:\n                current_message = yes_text if is_yes else no_text\n        else:\n            current_message += keypress\n            match_yes = yes_text\n            match_no = no_text\n            match_text = current_message\n            if not has_to_match_case:\n                match_yes = match_yes.upper()\n                match_no = match_no.upper()\n                match_text = match_text.upper()\n            if match_no.startswith(match_text):\n                is_selected = True\n                is_yes = False\n            elif match_yes.startswith(match_text):\n                is_selected = True\n                is_yes = True\n            else:\n                is_selected = False\n        print()\n    print(\"\\033[K\\n\\033[K\\n\\033[K\\n\\033[3A\")\n    return is_selected and is_yes\n"
  },
  {
    "path": "example.py",
    "content": "#! /usr/bin/env python3\n\"\"\"Example script demonstrating usage of cutie.\n\"\"\"\n\nimport cutie\n\n\ndef main():\n    \"\"\"Main.\"\"\"\n    if cutie.prompt_yes_or_no(\"Are you brave enough to continue?\"):\n        # List of names to select from, including some captions\n        names = [\n            \"Kings:\",\n            \"Arthur, King of the Britons\",\n            \"Knights of the Round Table:\",\n            \"Sir Lancelot the Brave\",\n            \"Sir Robin the Not-Quite-So-Brave-as-Sir-Lancelot\",\n            \"Sir Bedevere the Wise\",\n            \"Sir Galahad the Pure\",\n            \"Swedish captions:\",\n            \"Møøse\",\n        ]\n        # Names which are captions and thus not selectable\n        captions = [0, 2, 7]\n        # Get the name\n        name = names[cutie.select(names, caption_indices=captions, selected_index=8)]\n        print(f\"Welcome, {name}\")\n        # Get an integer greater or equal to 0\n        age = cutie.get_number(\"What is your age?\", min_value=0, allow_float=False)\n        nemeses_options = [\n            \"The French\",\n            \"The Police\",\n            \"The Knights Who Say Ni\",\n            \"Women\",\n            \"The Black Knight\",\n            \"The Bridge Keeper\",\n            \"Especially terrifying:\",\n            \"The Rabbit of Caerbannog\",\n        ]\n        print(\"Choose your nemeses\")\n        # Choose multiple options from a list\n        nemeses_indices = cutie.select_multiple(\n            nemeses_options, caption_indices=[6], hide_confirm=False\n        )\n        nemeses = [\n            nemesis\n            for nemesis_index, nemesis in enumerate(nemeses_options)\n            if nemesis_index in nemeses_indices\n        ]\n        # Get input without showing it being typed\n        quest = cutie.secure_input(\"What is your quest?\")\n        print(f\"{name}'s quest (who is {age}) is {quest}.\")\n        if nemeses:\n            if len(nemeses) == 1:\n                print(f\"His nemesis is {nemeses[0]}.\")\n            else:\n                print(f'His nemeses are {\" and \".join(nemeses)}.')\n        else:\n            print(\"He has no nemesis.\")\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "license.md",
    "content": "# The MIT License (MIT)\n\nCopyright © 2018 Hans Schülein\n\nPermission is hereby granted, free of charge, to any person\nobtaining a copy of this software and associated documentation\nfiles (the “Software”), to deal in the Software without\nrestriction, including without limitation the rights to use,\ncopy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the\nSoftware is furnished to do so, subject to the following\nconditions:\n\nThe above copyright notice and this permission notice shall be\nincluded in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND,\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES\nOF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND\nNONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT\nHOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,\nWHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\nFROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR\nOTHER DEALINGS IN THE SOFTWARE.\n"
  },
  {
    "path": "readme.md",
    "content": "# CUTIE\n\n*Command line User Tools for Input Easification*\n\n[![CircleCI](https://dl.circleci.com/status-badge/img/gh/Kamik423/cutie/tree/main.svg?style=shield)](https://dl.circleci.com/status-badge/redirect/gh/Kamik423/cutie/tree/main)\n[![Coverage Status](https://coveralls.io/repos/github/Kamik423/cutie/badge.svg?branch=coveralls_integration)](https://coveralls.io/github/Kamik423/cutie?branch=coveralls_integration)\n[![PRs Welcome](https://img.shields.io/badge/Homepage-GitHub-green.svg)](https://github.com/kamik423/cutie)\n[![PyPI version](https://badge.fury.io/py/cutie.svg)](https://badge.fury.io/py/cutie)\n[![PyPI license](https://img.shields.io/pypi/l/cutie.svg)](https://pypi.python.org/pypi/cutie/)\n[![PyPI pyversions](https://img.shields.io/pypi/pyversions/cutie.svg)](https://pypi.python.org/pypi/cutie/)\n[![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)\n[![GitHub contributors](https://img.shields.io/github/contributors/Kamik423/cutie.svg)](https://GitHub.com/Kamik423/cutie/graphs/contributors/)\n\nA tool for handling common user input functions in an elegant way.\nIt supports asking yes or no questions, selecting an element from a list with arrow keys or vim arrow keys, forcing the user to input a number and secure text entry while having many customization options.\n\nFor example the yes or no input supports forcing the user to match case, tab autocomplete and switching option with the arrow keys.\nThe number input allows setting a minum and a maximum, entering floats or forcing the user to use integers.\nIt will only return once the user inputs a number in that format, showing a warning to them if it does not conform.\n\nIt should work on all major operating systems (Mac, Linux, Windows).\n\n![example](https://github.com/Kamik423/cutie/blob/main/example.gif?raw=true)\n\n## Usage\n\nThese are the main functions of cutie.\n[example.py](https://github.com/Kamik423/cutie/blob/main/example.py) contains an extended version of this also showing off the `select_multiple` option.\n\n```python\nimport cutie\n\nif cutie.prompt_yes_or_no(\"Are you brave enough to continue?\"):\n    # List of names to select from, including some captions\n    names = [\n        \"Kings:\",\n        \"Arthur, King of the Britons\",\n        \"Knights of the Round Table:\",\n        \"Sir Lancelot the Brave\",\n        \"Sir Robin the Not-Quite-So-Brave-as-Sir-Lancelot\",\n        \"Sir Bedevere the Wise\",\n        \"Sir Galahad the Pure\",\n        \"Swedish captions:\",\n        \"Møøse\",\n    ]\n    # Names which are captions and thus not selectable\n    captions = [0, 2, 7]\n    # Get the name\n    name = names[cutie.select(names, caption_indices=captions, selected_index=8)]\n    print(f\"Welcome, {name}\")\n    # Get an integer greater or equal to 0\n    age = cutie.get_number(\"What is your age?\", min_value=0, allow_float=False)\n    # Get input without showing it being typed\n    quest = cutie.secure_input(\"What is your quest?\")\n    print(f\"{name}'s quest (who is {age}) is {quest}.\")\n```\n\nWhen run, as demonstrated in the gif above it yields this output:\n\n```\nAre you brave enough to continue? (Y/N) Yes\nKings:\n[ ] Arthur, King of the Britons\nKnights of the Round Table:\n[ ] Sir Lancelot the Brave\n[x] Sir Robin the Not-Quite-So-Brave-as-Sir-Lancelot\n[ ] Sir Bedevere the Wise\n[ ] Sir Galahad the Pure\nSwedish captions:\n[ ] Møøse\nWelcome, Sir Robin the Not-Quite-So-Brave-as-Sir-Lancelot\nWhat is your age? 31\nWhat is your quest?\nSir Robin the Not-Quite-So-Brave-as-Sir-Lancelot's quest (who is 31) is to find the holy grail.\n```\n\n## Installation\n\nWith pip from pypi:\n\n```bash\npip3 install cutie\n```\n\nWith pip from source or in a virtual environment:\n\n```bash\npip3 install -r requirements.txt\n```\n\n## Documentation\n\nAll functions of cutie are explained here.\nIf something is still unclear or you have questions about the implementation just take a look at [cutie.py](https://github.com/Kamik423/cutie/blob/main/cutie.py).\nThe implementation is rather straight forward.\n\n### get\\_number\n\nGet a number from user input.\n\nIf an invalid number is entered the user will be prompted again.\nA minimum and maximum value can be supplied. They are inclusive.\nIf the `allow_float` option, which is `True` by default is set to `False` it forces the user to enter an integer.\n\nGetting any three digit number for example could be done like that:\n\n```python\nnumber = cutie.get_number(\n    \"Please enter a three digit number:\",\n    min_value=100,\n    max_value=999,\n    allow_float=False\n)\n# which is equivalent to\nnumber = cutie.get_number(\"Please enter a three digit number\", 100, 999, False)\n```\n\n#### Arguments\n\n| argument      | type            | default    | description                          |\n|:--------------|:----------------|:-----------|:-------------------------------------|\n| `prompt`      | str             |            | The prompt asking the user to input. |\n| `min_value`   | float, optional | - infinity | The [inclusive] minimum value.       |\n| `max_value`   | float, optional | infinity   | The [inclusive] maximum value.       |\n| `allow_float` | bool, optional  | True       | Allow floats or force integers.      |\n\n#### Returns\n\nThe number input by the user.\n\n### secure\\_input\n\nGet secure input without showing it in the command line.\n\nThis could be used for passwords:\n\n```python\npassword = cutie.secure_input(\"Please enter your password:\")\n```\n\n#### Arguments\n\n| argument | type | description                          |\n|:---------|:-----|:-------------------------------------|\n| `prompt` | str  | The prompt asking the user to input. |\n\n#### Returns\n\nThe secure input.\n\n### select\n\nSelect an option from a list.\n\nCaptions or separators can be included between options by adding them as an option and including their index in `caption_indices`.\nA preselected index can be supplied.\n\nIn its simplest case it could be used like this:\n\n```python\ncolors = [\"red\", \"green\", \"blue\", \"yellow\"]\nprint(\"What is your favorite color?\")\nfavorite_color = colors[cutie.select(colors)]\n```\n\nWith the high degree of customizability, however it is possible to do things like:\n\n```python\nprint(\"Select server to ping\")\nserver_id = cutie.select(\n    servers,\n    deselected_prefix=\"    \",\n    selected_prefix=\"PING\",\n    selected_index=default_server_ip\n)\n```\n\n#### Arguments\n\n| argument            | type                | default | description                        |\n|:--------------------|:--------------------|:--------|:-----------------------------------|\n| `options`           | List[str]           |         | The options to select from.        |\n| `caption_indices`   | List[int], optional | `None`  | Non-selectable indices.            |\n| `deselected_prefix` | str, optional       | `[ ]`   | Prefix for deselected option.      |\n| `selected_prefix`   | str, optional       | `[x]`   | Prefix for selected option.        |\n| `caption_prefix`    | str, optional       | ` `     | Prefix for captions.               |\n| `selected_index`    | int, optional       | 0       | The index to be selected at first. |\n| `confirm_on_select` | bool, optional      | True    | Select keys also confirm.          |\n\n#### Returns\n\nThe index that has been selected.\n\n### select\\_multiple\n\nSelect multiple options from a list.\n\nIt per default shows a \"confirm\" button.\nIn that case space bar and enter select a line.\nThe button can be hidden.\nIn that case space bar selects the line and enter confirms the selection.\n\nThis is not in the example in this readme, but in [example.py](https://github.com/Kamik423/cutie/blob/main/example.py).\n\n```python\npackages_to_update = cutie.select_multiple(\n    outdated_packages,\n    deselected_unticked_prefix=\"  KEEP  \",\n    deselected_ticked_prefix=\" UPDATE \",\n    selected_unticked_prefix=\"[ KEEP ]\",\n    selected_ticked_prefix=\"[UPDATE]\",\n    ticked_indices=list(range(len(outdated_packages))),\n    deselected_confirm_label=\"  [[[[ UPDATE ]]]]  \",\n    selected_confirm_label=\"[ [[[[ UPDATE ]]]] ]\"\n)\n```\n\n#### Arguments\n\n| argument                     | type                | default         | description                                                                                                |\n|:-----------------------------|:--------------------|:----------------|:-----------------------------------------------------------------------------------------------------------|\n| `options`                    | List[str]           |                 | The options to select from.                                                                                |\n| `caption_indices`            | List[int], optional |                 | Non-selectable indices.                                                                                    |\n| `deselected_unticked_prefix` | str, optional       | `( )`           | Prefix for lines that are not selected and not ticked .                                                    |\n| `deselected_ticked_prefix`   | str, optional       | `(x)`           | Prefix for lines that are not selected but ticked .                                                        |\n| `selected_unticked_prefix`   | str, optional       | `{ }`           | Prefix for lines that are selected but not ticked .                                                        |\n| `selected_ticked_prefix`     | str, optional       | `{x}`           | Prefix for lines that are selected and ticked .                                                            |\n| `caption_prefix`             | str, optional       | ` `             | Prefix for captions.                                                                                       |\n| `ticked_indices`             | List[int], optional | `[]`            | Indices that are ticked initially.                                                                         |\n| `cursor_index`               | int, optional       | 0               | The index the cursor starts at.                                                                            |\n| `minimal_count`              | int, optional       | 0               | The minimal amount of lines that have to be ticked.                                                        |\n| `maximal_count`              | int, optional       | infinity        | The maximal amount of lines that have to be ticked.                                                        |\n| `hide_confirm`               | bool, optional      | `True`          | Hide the confirm button. This causes `<ENTER>` to confirm the entire selection and not just tick the line. |\n| `deselected_confirm_label`   | str, optional       | `(( confirm ))` | The confirm label if not selected.                                                                         |\n| `selected_confirm_label`     | str, optional       | `{{ confirm }}` | The confirm label if selected.                                                                             |\n\n#### Returns\n\nA list of indices that have been selected.\n\n### prompt\\_yes\\_or\\_no\n\nPrompt the user to input yes or no.\n\nThis again can range from very simple to very highly customized:\n\n```python\nif cutie.prompt_yes_or_no(\"Do you want to continue?\"):\n    do_continue()\n```\n\n```python\nif cutie.prompt_yes_or_no(\n    \"Do you want to hear ze funniest joke in ze world? Proceed at your own risk.\",\n    yes_text=\"JA\",\n    no_text=\"nein\",\n    has_to_match_case=True, # The user has to type the exact case\n    enter_empty_confirms=False, # An answer has to be selected\n    )\n```\n\n#### Arguments\n\n| argument               | type           | default | description                          |\n|:-----------------------|:---------------|:--------|:-------------------------------------|\n| `question`             | str            |         | The prompt asking the user to input. |\n| `yes_text`             | str, optional  | `Yes`   | The text corresponding to \"yes\".     |\n| `no_text`              | str, optional  | `No`    | The text corresponding to \"no\".      |\n| `has_to_match_case`    | bool, optional | `False` | Does the case have to match.         |\n| `enter_empty_confirms` | bool, optional | True    | Does enter on empty string work.     |\n| `default_is_yes`       | bool, optional | False   | Is yes selected by default           |\n| `deselected_prefix`    | str, optional  | `  `    | Prefix if something is deselected.   |\n| `selected_prefix`      | str, optional  | `> `    | Prefix if something is selected      |\n| `char_prompt`          | bool, optional | `True`  | Add a [Y/N] to the prompt.           |\n\n#### Returns\n\nThe bool what has been selected.\n\n## Changelog\n\n### 0.3.2\n\n* CircleCI Integration\n\n### 0.3.1\n\n* Readme fixes for PyPi\n\n### 0.3.0\n\n* Unittests by [provinzkraut](https://github.com/provinzkraut)\n* Travis CI integration\n* Vim Arrow keys (`jk`)\n* Also showing error messages with `hide_confirm` option enabled in `select_multiple`\n* Consistenly crash on keyboard interrupt (Removes `prompt_yes_or_no`'s `abort_value`)\n* Set `hide_confirm` to default in `select_multiple` ([#9](https://github.com/Kamik423/cutie/issues/9))\n* Black code style\n\n### 0.2.2\n\n* Fixed Python in examples\n* PEP8 Compliance by [Christopher Bilger](https://github.com/ChristopherBilg)\n* Fixed critical issue with pypi download ([#15](https://github.com/Kamik423/cutie/issues/15))\n\n### 0.2.1\n\n* Expanded readme descriptions\n\n### 0.2.0\n\n* `select_multiple`\n* Tweaks to the readme\n\n### 0.1.1\n\n* Fixed pypi download not working\n\n### 0.1.0\n\n* `caption_indices` option by [dherrada](https://github.com/dherrada)\n\n### 0.0.7\n\n* Windows support by [Lhitrom](https://github.com/Lhitrom)\n\n### 0.0.x\n\n* Initial upload and got everything working\n\n\n## Contributing\n\nIf you want to contribute, please feel free to suggest features or implement them yourself.\n\nAlso **please report any issues and bugs you might find!**\n\nIf you have a project that uses cutie please let me know and I'll link it here!\n\n## Authors\n\n* Main project by [me](https://github.com/Kamik423).\n* Unittests, issues and advice by [provinzkraut](https://github.com/provinzkraut).\n* Windows support by [Lhitrom](https://github.com/Lhitrom).\n* `caption_indices` and tidbits by [dherrada](https://github.com/dherrada).\n* PEP8 Compliance by [Christopher Bilger](https://github.com/ChristopherBilg).\n\n## License\n\nThe project is licensed under the [MIT-License](https://github.com/Kamik423/cutie/blob/main/license.md).\n\n## Acknowledgments\n\n* This project uses the module [Readchar](https://pypi.org/project/readchar/) for direct input handling.\n\n---\n\n*GNU Terry Pratchett*\n"
  },
  {
    "path": "requirements.txt",
    "content": "colorama\nreadchar != 3.0.5\n"
  },
  {
    "path": "setup.py",
    "content": "\"\"\"Setup module for PyPI / pip integration.\n\"\"\"\n\nimport imp\n\nimport setuptools\n\nwith open(\"readme.md\", encoding=\"utf-8\") as file:\n    LONG_DESCRIPTION = file.read()\n\nwith open(\"cutie.py\", encoding=\"utf-8\") as file:\n    # only go to first import since that module is not yet installed\n    CUTIE_CONTENTS = file.read().split(\"import\")[0]\n\ncutie = imp.new_module(\"cutie\")\nexec(CUTIE_CONTENTS, cutie.__dict__)\n\nsetuptools.setup(\n    name=\"cutie\",\n    version=cutie.__version__,\n    author=cutie.__author__,\n    author_email=\"contact.kamik423@gmail.com\",\n    description=cutie.__doc__,\n    long_description=LONG_DESCRIPTION,\n    long_description_content_type=\"text/markdown\",\n    url=\"https://github.com/kamik423/cutie\",\n    py_modules=[\"cutie\"],\n    license=cutie.__license__,\n    install_requires=[\"colorama\", \"readchar!=3.0.5\"],\n    python_requires=\">=3.6\",\n    classifiers=[\n        \"Programming Language :: Python :: 3.6\",\n        \"Programming Language :: Python :: 3.7\",\n        \"Programming Language :: Python :: 3.8\",\n        \"Programming Language :: Python :: 3.9\",\n        \"Programming Language :: Python :: 3.10\",\n        \"Programming Language :: Python :: 3 :: Only\",\n        \"License :: OSI Approved :: MIT License\",\n        \"Operating System :: OS Independent\",\n    ],\n)\n"
  },
  {
    "path": "test/__init__.py",
    "content": "import readchar\n\nimport cutie\n\n\ndef PrintCall(states):\n\n    def func(msg=None, state=\"selectable\"):\n        state_ = states[state]\n        state_name: str\n        kwargs = {}\n        if isinstance(state_, str):\n            state_name = state_\n        elif isinstance(state_, tuple):\n            state_name = state_[0]\n            if len(state) > 1:\n                kwargs = state_[1]\n\n        if msg:\n            return ((state_name + msg,), kwargs)\n        else:\n            return ((state_name,), kwargs)\n\n    return func\n\n\ndef yield_input(*data, raise_on_empty=False):\n    \"\"\"\n    Closure that returns predefined data.\n\n    If the data is exhausted raise a MockException or reraise the IndexError\n    \"\"\"\n    data = list(data)\n\n    def func(*a, **kw):\n        try:\n            return data.pop(0)\n        except IndexError as e:\n            if raise_on_empty:\n                raise MockException()\n            else:\n                raise e\n\n    return func\n\n\nclass InputContext:\n    \"\"\"\n    Context manager to simulate keyboard input returned by `readchar.readkey`,\n    by replacing it in `cutie` with `yield_input`\n\n    When the supplied keystrokes are exhausted a `MockException` will be raised.\n    This can be used to terminate the execution at any desired point, rather than\n    relying on internal control mechanisms.\n\n    Usage:\n        with InputContext(\" \", \"\\r\"):\n            cutie.select([\"foo\", \"bar\"])\n    This will issue a space and enter keypress, selecting the first item and\n    confirming.\n    \"\"\"\n\n    def __init__(self, *data, raise_on_empty=True):\n        cutie.readchar.readkey = yield_input(*data, raise_on_empty=raise_on_empty)\n\n    def __enter__(self):\n        pass\n\n    def __exit__(self, *a):\n        cutie.readchar.readkey = readchar.readkey\n\n\nclass MockException(Exception):\n    pass\n"
  },
  {
    "path": "test/test_get_number.py",
    "content": "import unittest\nfrom unittest import mock\n\nimport cutie\n\nfrom . import MockException\n\n\nclass TestCutieGetNumber(unittest.TestCase):\n    @mock.patch(\"cutie.print\", side_effect=MockException)\n    def test_invalid_number(self, mock_print):\n        with mock.patch(\"cutie.input\", return_value=\"foo\"):\n            with self.assertRaises(MockException):\n                cutie.get_number(\"bar\")\n            mock_print.assert_called_once_with(\n                \"Not a valid number.\\033[K\\033[1A\\r\\033[K\", end=\"\"\n            )\n\n    @mock.patch(\"cutie.print\", side_effect=MockException)\n    def test_not_allow_float(self, mock_print):\n        with mock.patch(\"cutie.input\", return_value=\"1.2\"):\n            with self.assertRaises(MockException):\n                cutie.get_number(\"foo\", allow_float=False)\n            mock_print.assert_called_once_with(\n                \"Has to be an integer.\\033[K\\033[1A\\r\\033[K\", end=\"\"\n            )\n\n    def test_allow_float_returns_float(self):\n        with mock.patch(\"cutie.input\", return_value=\"1.2\"):\n            val = cutie.get_number(\"foo\")\n            self.assertIsInstance(val, float)\n            self.assertEqual(val, 1.2)\n\n    def test_not_allow_float_returns_int(self):\n        with mock.patch(\"cutie.input\", return_value=\"1\"):\n            val = cutie.get_number(\"foo\", allow_float=False)\n            self.assertIsInstance(val, int)\n            self.assertEqual(val, 1)\n\n    @mock.patch(\"cutie.print\", side_effect=MockException)\n    def test_min_value_float_too_low(self, mock_print):\n        with mock.patch(\"cutie.input\", return_value=\"1.2\"):\n            with self.assertRaises(MockException):\n                cutie.get_number(\"foo\", min_value=1.3)\n            mock_print.assert_called_once_with(\n                \"Has to be at least 1.3.\\033[K\\033[1A\\r\\033[K\", end=\"\"\n            )\n\n    def test_min_value_float_equal(self):\n        with mock.patch(\"cutie.input\", return_value=\"1.2\"):\n            self.assertEqual(cutie.get_number(\"foo\", min_value=1.2), 1.2)\n\n    def test_min_value_float_greater(self):\n        with mock.patch(\"cutie.input\", return_value=\"1.3\"):\n            self.assertEqual(cutie.get_number(\"foo\", min_value=1.2), 1.3)\n\n    @mock.patch(\"cutie.print\", side_effect=MockException)\n    def test_min_value_int_too_low(self, mock_print):\n        with mock.patch(\"cutie.input\", return_value=\"1\"):\n            with self.assertRaises(MockException):\n                cutie.get_number(\"foo\", min_value=2)\n            mock_print.assert_called_once_with(\n                \"Has to be at least 2.\\033[K\\033[1A\\r\\033[K\", end=\"\"\n            )\n\n    def test_min_value_int_equal(self):\n        with mock.patch(\"cutie.input\", return_value=\"1\"):\n            self.assertEqual(cutie.get_number(\"foo\", min_value=1), 1)\n\n    def test_min_value_int_greater(self):\n        with mock.patch(\"cutie.input\", return_value=\"2\"):\n            self.assertEqual(cutie.get_number(\"foo\", min_value=1), 2)\n\n    @mock.patch(\"cutie.print\", side_effect=MockException)\n    def test_max_value_float_too_high(self, mock_print):\n        with mock.patch(\"cutie.input\", return_value=\"1.2\"):\n            with self.assertRaises(MockException):\n                cutie.get_number(\"foo\", max_value=1.1)\n            mock_print.assert_called_once_with(\n                \"Has to be at most 1.1.\\033[1A\\r\\033[K\", end=\"\"\n            )\n\n    def test_max_value_float_equal(self):\n        with mock.patch(\"cutie.input\", return_value=\"1.1\"):\n            self.assertEqual(cutie.get_number(\"foo\", max_value=1.1), 1.1)\n\n    def test_max_value_float_smaller(self):\n        with mock.patch(\"cutie.input\", return_value=\"1.1\"):\n            self.assertEqual(cutie.get_number(\"foo\", max_value=1.2), 1.1)\n\n    @mock.patch(\"cutie.print\", side_effect=MockException)\n    def test_max_value_int_too_high(self, mock_print):\n        with mock.patch(\"cutie.input\", return_value=\"2\"):\n            with self.assertRaises(MockException):\n                cutie.get_number(\"foo\", max_value=1)\n            mock_print.assert_called_once_with(\n                \"Has to be at most 1.\\033[1A\\r\\033[K\", end=\"\"\n            )\n\n    def test_max_value_int_equal(self):\n        with mock.patch(\"cutie.input\", return_value=\"1\"):\n            self.assertEqual(cutie.get_number(\"foo\", max_value=1), 1)\n\n    def test_max_value_int_smaller(self):\n        with mock.patch(\"cutie.input\", return_value=\"1\"):\n            self.assertEqual(cutie.get_number(\"foo\", max_value=2), 1)\n\n    @mock.patch(\"cutie.print\")\n    def test_print_finalize(self, mock_print):\n        with mock.patch(\"cutie.input\", return_value=\"1\"):\n            cutie.get_number(\"foo\")\n        mock_print.assert_called_once_with(\"\\033[K\", end=\"\")\n"
  },
  {
    "path": "test/test_prompt_yes_or_no.py",
    "content": "import unittest\nfrom unittest import mock\n\nimport readchar\n\nfrom . import InputContext, MockException, PrintCall, cutie\n\nprint_call = PrintCall(\n    {\n        \"selected\": \"\\x1b[K\\x1b[31m>\\x1b[0m \",\n        \"selectable\": \"\\x1b[K  \",\n    }\n)\n\n\nclass TestPromtYesOrNo(unittest.TestCase):\n\n    default_yes_print_calls = [\n        (tuple(),),\n        ((\"\\x1b[K\\x1b[31m>\\x1b[0m Yes\",),),\n        ((\"\\x1b[K  No\",),),\n        ((\"\\x1b[3A\\r\\x1b[Kfoo (Y/N) Yes\",), {\"end\": \"\", \"flush\": True}),\n        ((\"\\x1b[K\\n\\x1b[K\\n\\x1b[K\\n\\x1b[3A\",),),\n    ]\n\n    default_no_print_calls = [\n        (tuple(),),\n        ((\"\\x1b[K  Yes\",),),\n        ((\"\\x1b[K\\x1b[31m>\\x1b[0m No\",),),\n        ((\"\\x1b[3A\\r\\x1b[Kfoo (Y/N) No\",), {\"end\": \"\", \"flush\": True}),\n        ((\"\\x1b[K\\n\\x1b[K\\n\\x1b[K\\n\\x1b[3A\",),),\n    ]\n\n    @mock.patch(\"cutie.print\")\n    def test_print_message(self, mock_print):\n        expected_calls = [\n            (tuple(),),\n            ((\"\\x1b[K  Yes\",),),\n            ((\"\\x1b[K\\x1b[31m>\\x1b[0m No\",),),\n            (\n                (\"\\x1b[3A\\r\\x1b[Kfoo (Y/N) \",),\n                {\"end\": \"\", \"flush\": True},\n            ),\n            ((\"\\x1b[K\\n\\x1b[K\\n\\x1b[K\\n\\x1b[3A\",),),\n        ]\n        with InputContext(readchar.key.ENTER):\n            cutie.prompt_yes_or_no(\"foo\")\n            self.assertEqual(mock_print.call_args_list, expected_calls)\n\n    @mock.patch(\"cutie.print\")\n    def test_print_message_custom_prefixes(self, mock_print):\n        expected_calls = [\n            ((\"\\x1b[K+Yes\",),),\n            ((\"\\x1b[K*No\",),),\n        ]\n        with InputContext(readchar.key.ENTER):\n            cutie.prompt_yes_or_no(\"foo\", selected_prefix=\"*\", deselected_prefix=\"+\")\n            self.assertEqual(mock_print.call_args_list[1:3], expected_calls)\n\n    @mock.patch(\"cutie.print\")\n    def test_print_message_custom_yes_no_text(self, mock_print):\n        expected_calls = [\n            ((\"\\x1b[K  bar\",),),\n            ((\"\\x1b[K\\x1b[31m>\\x1b[0m baz\",),),\n        ]\n        with InputContext(readchar.key.ENTER):\n            cutie.prompt_yes_or_no(\"foo\", yes_text=\"bar\", no_text=\"baz\")\n            self.assertEqual(mock_print.call_args_list[1:3], expected_calls)\n\n    @mock.patch(\"cutie.print\")\n    def test_print_message_default_is_yes(self, mock_print):\n        expected_calls = [\n            ((\"\\x1b[K\\x1b[31m>\\x1b[0m Yes\",),),\n            ((\"\\x1b[K  No\",),),\n        ]\n        with InputContext(readchar.key.ENTER):\n            cutie.prompt_yes_or_no(\"foo\", default_is_yes=True)\n            self.assertEqual(mock_print.call_args_list[1:3], expected_calls)\n\n    @mock.patch(\"cutie.print\")\n    def test_move_up(self, mock_print):\n        with InputContext(readchar.key.UP, readchar.key.ENTER):\n            self.assertTrue(cutie.prompt_yes_or_no(\"foo\"))\n            self.assertEqual(\n                mock_print.call_args_list[-5:], self.default_yes_print_calls\n            )\n\n    @mock.patch(\"cutie.print\")\n    def test_move_up_over_boundary(self, mock_print):\n        with InputContext(readchar.key.UP, readchar.key.UP, readchar.key.ENTER):\n            self.assertFalse(cutie.prompt_yes_or_no(\"foo\"))\n            self.assertEqual(\n                mock_print.call_args_list[-5:], self.default_no_print_calls\n            )\n\n    @mock.patch(\"cutie.print\")\n    def test_move_down(self, mock_print):\n        with InputContext(readchar.key.DOWN, readchar.key.ENTER):\n            self.assertFalse(cutie.prompt_yes_or_no(\"foo\", default_is_yes=True))\n            self.assertEqual(\n                mock_print.call_args_list[-5:], self.default_no_print_calls\n            )\n\n    @mock.patch(\"cutie.print\")\n    def test_move_down_over_boundary(self, mock_print):\n        with InputContext(readchar.key.DOWN, readchar.key.ENTER):\n            self.assertTrue(cutie.prompt_yes_or_no(\"foo\"))\n            self.assertEqual(\n                mock_print.call_args_list[-5:], self.default_yes_print_calls\n            )\n\n    @mock.patch(\"cutie.print\")\n    def test_backspace_delete_char(self, mock_print):\n        expected_calls = [\n            (tuple(),),\n            print_call(\"Yes\", \"selected\"),\n            print_call(\"No\"),\n            (\n                (\"\\x1b[3A\\r\\x1b[Kfoo (Y/N) Ye\",),\n                {\"end\": \"\", \"flush\": True},\n            ),\n            ((\"\\x1b[K\\n\\x1b[K\\n\\x1b[K\\n\\x1b[3A\",),),\n        ]\n        with InputContext(readchar.key.UP, readchar.key.BACKSPACE, readchar.key.ENTER):\n            cutie.prompt_yes_or_no(\"foo\")\n            self.assertEqual(mock_print.call_args_list[-5:], expected_calls)\n\n    @mock.patch(\"cutie.print\")\n    def test_ctrl_c_abort(self, *m):\n        with InputContext(readchar.key.CTRL_C):\n            with self.assertRaises(KeyboardInterrupt):\n                cutie.prompt_yes_or_no(\"\")\n\n    @mock.patch(\"cutie.print\")\n    def test_ctrl_c_abort_with_input(self, *m):\n        with InputContext(readchar.key.UP, readchar.key.CTRL_D):\n            with self.assertRaises(KeyboardInterrupt):\n                cutie.prompt_yes_or_no(\"\")\n\n    @mock.patch(\"cutie.print\")\n    def test_ctrl_d_abort(self, *m):\n        with InputContext(readchar.key.CTRL_D):\n            with self.assertRaises(KeyboardInterrupt):\n                cutie.prompt_yes_or_no(\"\")\n\n    @mock.patch(\"cutie.print\")\n    def test_ctrl_d_abort_with_input(self, *m):\n        with InputContext(readchar.key.UP, readchar.key.CTRL_D):\n            with self.assertRaises(KeyboardInterrupt):\n                cutie.prompt_yes_or_no(\"\")\n\n    @mock.patch(\"cutie.print\")\n    def test_enter_confirm_default(self, *m):\n        with InputContext(readchar.key.ENTER):\n            self.assertFalse(cutie.prompt_yes_or_no(\"\"))\n\n    @mock.patch(\"cutie.print\")\n    def test_enter_confirm_selection(self, *m):\n        with InputContext(readchar.key.UP, readchar.key.ENTER):\n            self.assertTrue(cutie.prompt_yes_or_no(\"\"))\n\n    @mock.patch(\"cutie.print\")\n    def test_tab_select(self, mock_print):\n        expected_calls = [\n            (tuple(),),\n            print_call(\"Yes\"),\n            print_call(\"No\", \"selected\"),\n            (\n                (\"\\x1b[3A\\r\\x1b[Kfoo (Y/N) No\",),\n                {\"end\": \"\", \"flush\": True},\n            ),\n            ((\"\\x1b[K\\n\\x1b[K\\n\\x1b[K\\n\\x1b[3A\",),),\n        ]\n        with InputContext(\"\\t\", readchar.key.ENTER):\n            cutie.prompt_yes_or_no(\"foo\")\n            self.assertEqual(mock_print.call_args_list[-5:], expected_calls)\n\n    @mock.patch(\"cutie.print\")\n    def test_write_keypress_to_terminal(self, mock_print):\n        expected_calls = [\n            (tuple(),),\n            print_call(\"Yes\"),\n            print_call(\"No\", \"selected\"),\n            (\n                (\"\\x1b[3A\\r\\x1b[Kfoo (Y/N) \",),\n                {\"end\": \"\", \"flush\": True},\n            ),\n            (tuple(),),\n            print_call(\"Yes\"),\n            print_call(\"No\"),\n            (\n                (\"\\x1b[3A\\r\\x1b[Kfoo (Y/N) f\",),\n                {\"end\": \"\", \"flush\": True},\n            ),\n            (tuple(),),\n            print_call(\"Yes\"),\n            print_call(\"No\"),\n            (\n                (\"\\x1b[3A\\r\\x1b[Kfoo (Y/N) fo\",),\n                {\"end\": \"\", \"flush\": True},\n            ),\n            (tuple(),),\n            print_call(\"Yes\"),\n            print_call(\"No\"),\n            (\n                (\"\\x1b[3A\\r\\x1b[Kfoo (Y/N) foo\",),\n                {\"end\": \"\", \"flush\": True},\n            ),\n        ]\n        with InputContext(\"f\", \"o\", \"o\", readchar.key.CTRL_C):\n            with self.assertRaises(KeyboardInterrupt):\n                cutie.prompt_yes_or_no(\"foo\")\n            self.assertEqual(mock_print.call_args_list, expected_calls)\n\n    @mock.patch(\"cutie.print\")\n    def test_write_keypress_to_terminal_resume_selection(self, mock_print):\n        expected_calls = [\n            (tuple(),),\n            print_call(\"Yes\", \"selected\"),\n            print_call(\"No\"),\n            (\n                (\"\\x1b[3A\\r\\x1b[Kfoo (Y/N) Yes\",),\n                {\"end\": \"\", \"flush\": True},\n            ),\n            ((\"\\x1b[K\\n\\x1b[K\\n\\x1b[K\\n\\x1b[3A\",),),\n        ]\n        with InputContext(\"f\", readchar.key.DOWN, readchar.key.ENTER):\n            self.assertTrue(cutie.prompt_yes_or_no(\"foo\"))\n            self.assertEqual(mock_print.call_args_list[-5:], expected_calls)\n\n    @mock.patch(\"cutie.print\")\n    def test_evaluate_written_input_yes_ignorecase(self, mock_print):\n        expected_calls = [\n            (tuple(),),\n            print_call(\"Yes\", \"selected\"),\n            print_call(\"No\"),\n            (\n                (\"\\x1b[3A\\r\\x1b[Kfoo (Y/N) yes\",),\n                {\"end\": \"\", \"flush\": True},\n            ),\n            ((\"\\x1b[K\\n\\x1b[K\\n\\x1b[K\\n\\x1b[3A\",),),\n        ]\n        with InputContext(\"y\", \"e\", \"s\", readchar.key.ENTER):\n            self.assertTrue(cutie.prompt_yes_or_no(\"foo\"))\n            self.assertEqual(mock_print.call_args_list[-5:], expected_calls)\n\n    @mock.patch(\"cutie.print\")\n    def test_evaluate_written_input_yes_case_sensitive(self, mock_print):\n        expected_calls = (\n            (\"\\x1b[3A\\r\\x1b[Kfoo (Y/N) yes\",),\n            {\"end\": \"\", \"flush\": True},\n        )\n\n        with InputContext(\"y\", \"e\", \"s\", readchar.key.CTRL_C):\n            res = None\n            with self.assertRaises(KeyboardInterrupt):\n                res = cutie.prompt_yes_or_no(\"foo\", has_to_match_case=True)\n            self.assertIsNone(res)\n            self.assertEqual(mock_print.call_args_list[-1], expected_calls)\n\n    @mock.patch(\"cutie.print\")\n    def test_evaluate_written_input_no_ignorecase(self, mock_print):\n        expected_calls = [\n            (tuple(),),\n            print_call(\"Yes\"),\n            print_call(\"No\", \"selected\"),\n            (\n                (\"\\x1b[3A\\r\\x1b[Kfoo (Y/N) no\",),\n                {\"end\": \"\", \"flush\": True},\n            ),\n            ((\"\\x1b[K\\n\\x1b[K\\n\\x1b[K\\n\\x1b[3A\",),),\n        ]\n        with InputContext(\"n\", \"o\", readchar.key.ENTER):\n            self.assertFalse(cutie.prompt_yes_or_no(\"foo\"))\n            self.assertEqual(mock_print.call_args_list[-5:], expected_calls)\n\n    @mock.patch(\"cutie.print\")\n    def test_evaluate_written_input_no_case_sensitive(self, mock_print):\n        expected_calls = (\n            (\"\\x1b[3A\\r\\x1b[Kfoo (Y/N) no\",),\n            {\"end\": \"\", \"flush\": True},\n        )\n\n        with InputContext(\"n\", \"o\", readchar.key.CTRL_C):\n            res = None\n            with self.assertRaises(KeyboardInterrupt):\n                res = cutie.prompt_yes_or_no(\"foo\", has_to_match_case=True)\n            self.assertIsNone(res)\n            self.assertEqual(mock_print.call_args_list[-1], expected_calls)\n"
  },
  {
    "path": "test/test_secure_input.py",
    "content": "import unittest\nfrom unittest import mock\n\nimport cutie\n\n\nclass TestSecureInput(unittest.TestCase):\n    def test_secure_input(self):\n        with mock.patch(\"cutie.getpass.getpass\", return_value=\"foo\") as mock_getpass:\n            self.assertEqual(cutie.secure_input(\"foo\"), \"foo\")\n            mock_getpass.assert_called_once_with(\"foo \")\n"
  },
  {
    "path": "test/test_select.py",
    "content": "import string\nimport unittest\nfrom unittest import mock\n\nimport readchar\n\nfrom . import InputContext, MockException, PrintCall, cutie\n\nprint_call = PrintCall(\n    {\n        \"selectable\": \"\\x1b[K\\x1b[1m[ ]\\x1b[0m \",\n        \"selected\": \"\\x1b[K\\x1b[1m[\\x1b[32;1mx\\x1b[0;1m]\\x1b[0m \",\n        \"caption\": \"\\x1b[K\",\n    }\n)\n\n\nclass TestSelect(unittest.TestCase):\n    @mock.patch(\"cutie.print\", side_effect=MockException)\n    def test_print_list_newlines(self, mock_print):\n        args_list = [\"foo\", \"bar\"]\n        with self.assertRaises(MockException):\n            cutie.select(args_list)\n        mock_print.assert_called_once_with(\"\\n\" * (len(args_list) - 1))\n\n    @mock.patch(\"cutie.readchar.readkey\", side_effect=MockException)\n    @mock.patch(\"cutie.print\")\n    def test_print_move_to_first_item(self, mock_print, *m):\n        args_list = [\"foo\", \"bar\"]\n        with self.assertRaises(MockException):\n            cutie.select(args_list)\n        self.assertEqual(\n            mock_print.call_args_list[1], ((f\"\\033[{len(args_list) + 1}A\",),)\n        )\n\n    @mock.patch(\"cutie.readchar.readkey\", side_effect=MockException)\n    @mock.patch(\"cutie.print\")\n    def test_print_options(self, mock_print, *m):\n        args_list = [\"foo\", \"bar\"]\n        expected_calls = [print_call(\"foo\", \"selected\"), print_call(\"bar\")]\n        with self.assertRaises(MockException):\n            cutie.select(args_list)\n        self.assertEqual(mock_print.call_args_list[2:], expected_calls)\n\n    @mock.patch(\"cutie.readchar.readkey\", side_effect=MockException)\n    @mock.patch(\"cutie.print\")\n    def test_print_options_selected_index_set(self, mock_print, *m):\n        args_list = [\"foo\", \"bar\"]\n        expected_calls = [print_call(\"foo\"), print_call(\"bar\", \"selected\")]\n        with self.assertRaises(MockException):\n            cutie.select(args_list, selected_index=1)\n        self.assertEqual(mock_print.call_args_list[2:], expected_calls)\n\n    @mock.patch(\"cutie.readchar.readkey\", side_effect=MockException)\n    @mock.patch(\"cutie.print\")\n    def test_print_non_selectable(self, mock_print, *m):\n        args_list = [\"foo\", \"bar\"]\n        expected_calls = [print_call(\"foo\", \"selected\"), print_call(\"bar\", \"caption\")]\n        with self.assertRaises(MockException):\n            cutie.select(args_list, caption_indices=[1])\n        self.assertEqual(mock_print.call_args_list[2:], expected_calls)\n\n    @mock.patch(\"cutie.readchar.readkey\", side_effect=MockException)\n    @mock.patch(\"cutie.print\")\n    def test_print_options_custom_prefixes(self, mock_print, *m):\n        args_list = [\"foo\", \"bar\", \"baz\"]\n        expected_calls = [((\"\\x1b[K*foo\",),), ((\"\\x1b[K+bar\",),), ((\"\\x1b[K$baz\",),)]\n        with self.assertRaises(MockException):\n            cutie.select(\n                args_list,\n                caption_indices=[2],\n                selected_prefix=\"*\",\n                deselected_prefix=\"+\",\n                caption_prefix=\"$\",\n            )\n        self.assertEqual(mock_print.call_args_list[2:], expected_calls)\n\n    @mock.patch(\"cutie.print\")\n    def test_ignore_unrecognized_key(self, mock_print):\n        exclude = [\n            \"__builtins__\",\n            \"__cached__\",\n            \"__doc__\",\n            \"__file__\",\n            \"__loader__\",\n            \"__name__\",\n            \"__package__\",\n            \"__spec__\",\n            \"UP\",\n            \"DOWN\",\n            \"ENTER\",\n            \"CTRL_C\",\n            \"CTRL_D\",\n        ]\n        all_keys = [\n            getattr(readchar.key, k) for k in dir(readchar.key) if k not in exclude\n        ]\n        all_keys.extend(string.printable)\n        expected_calls = [\n            ((\"\",),),\n            ((\"\\x1b[2A\",),),\n            ((\"\\x1b[K\\x1b[1m[\\x1b[32;1mx\\x1b[0;1m]\\x1b[0m foo\",),),\n        ]\n\n        for key in all_keys:\n            with InputContext(readchar.key.DOWN, key, readchar.key.ENTER):\n                selindex = cutie.select([\"foo\"])\n                self.assertEqual(selindex, 0)\n                self.assertEqual(mock_print.call_args_list[:3], expected_calls)\n                mock_print.reset_mock()\n\n    @mock.patch(\"cutie.print\")\n    def test_move_up(self, *m):\n        with InputContext(readchar.key.UP, readchar.key.ENTER):\n            args_list = [\"foo\", \"bar\"]\n            selindex = cutie.select(args_list, selected_index=1)\n            self.assertEqual(selindex, 0)\n\n    @mock.patch(\"cutie.print\")\n    def test_move_up_skip_caption(self, *m):\n        with InputContext(readchar.key.UP, readchar.key.ENTER):\n            args_list = [\"foo\", \"bar\", \"baz\"]\n            selindex = cutie.select(args_list, selected_index=2, caption_indices=[1])\n            self.assertEqual(selindex, 0)\n\n    @mock.patch(\"cutie.print\")\n    def test_move_down(self, *m):\n        with InputContext(readchar.key.DOWN, readchar.key.ENTER):\n            args_list = [\"foo\", \"bar\"]\n            selindex = cutie.select(args_list)\n            self.assertEqual(selindex, 1)\n\n    @mock.patch(\"cutie.print\")\n    def test_move_down_skip_caption(self, *m):\n        with InputContext(readchar.key.DOWN, readchar.key.ENTER):\n            args_list = [\"foo\", \"bar\", \"baz\"]\n            selindex = cutie.select(args_list, caption_indices=[1])\n            self.assertEqual(selindex, 2)\n\n    @mock.patch(\"cutie.print\")\n    def test_keyboard_interrupt_ctrl_c_no_input(self, *m):\n        with InputContext(readchar.key.CTRL_C):\n            with self.assertRaises(KeyboardInterrupt):\n                cutie.select([\"foo\"])\n\n    @mock.patch(\"cutie.print\")\n    def test_keyboard_interrupt_ctrl_c_selected(self, *m):\n        with InputContext(readchar.key.DOWN, readchar.key.CTRL_C):\n            with self.assertRaises(KeyboardInterrupt):\n                cutie.select([\"foo\"], selected_index=0)\n\n    @mock.patch(\"cutie.print\")\n    def test_keyboard_interrupt_ctrl_d_no_input(self, *m):\n        with InputContext(readchar.key.CTRL_D):\n            with self.assertRaises(KeyboardInterrupt):\n                cutie.select([\"foo\"])\n\n    @mock.patch(\"cutie.print\")\n    def test_keyboard_interrupt_ctrl_d_selected(self, *m):\n        with InputContext(readchar.key.DOWN, readchar.key.CTRL_D):\n            with self.assertRaises(KeyboardInterrupt):\n                cutie.select([\"foo\"], selected_index=0)\n"
  },
  {
    "path": "test/test_select_multiple.py",
    "content": "import string\nimport unittest\nfrom unittest import mock\n\nimport readchar\n\nfrom . import InputContext, MockException, PrintCall, cutie, yield_input\n\nprint_call = PrintCall(\n    {\n        \"selectable\": \"\\x1b[K\\x1b[1m( )\\x1b[0m \",\n        \"selected\": \"\\x1b[K\\x1b[1m(\\x1b[32mx\\x1b[0;1m)\\x1b[0m \",\n        \"caption\": \"\\x1b[K\",\n        \"active\": \"\\x1b[K\\x1b[32;1m{ }\\x1b[0m \",\n        \"active-selected\": \"\\x1b[K\\x1b[32;1m{x}\\x1b[0m \",\n        \"confirm\": (\"\\x1b[1m(( confirm ))\\x1b[0m \\x1b[K\", {\"end\": \"\", \"flush\": True}),\n        \"confirm-active\": (\n            \"\\x1b[1;32m{{ confirm }}\\x1b[0m \\x1b[K\",\n            {\"end\": \"\", \"flush\": True},\n        ),\n        \"no_confirm_line\": (\"\\033[K\", {\"end\": \"\", \"flush\": True}),\n    }\n)\n\n\nPRINT_CALL_END = ((\"\\r\\x1b[K\",), {\"end\": \"\", \"flush\": True})\n\n\nclass TestSelectMultiplePrint(unittest.TestCase):\n    @mock.patch(\"cutie.print\", side_effect=MockException)\n    def test_list_newlines(self, mock_print):\n        args_list = [\"foo\", \"bar\"]\n        with self.assertRaises(MockException):\n            cutie.select_multiple(args_list)\n        mock_print.assert_called_once_with(\"\\n\" * (len(args_list) - 1))\n\n    @mock.patch(\"cutie.readchar.readkey\", side_effect=MockException)\n    @mock.patch(\"cutie.print\")\n    def test_move_to_first_item(self, mock_print, *m):\n        args_list = [\"foo\", \"bar\"]\n        with self.assertRaises(MockException):\n            cutie.select_multiple(args_list)\n        self.assertEqual(\n            mock_print.call_args_list[1], ((f\"\\033[{len(args_list) + 1}A\",),)\n        )\n\n    @mock.patch(\"cutie.readchar.readkey\", side_effect=MockException)\n    @mock.patch(\"cutie.print\")\n    def test_print_options(self, mock_print, *m):\n        args_list = [\"foo\", \"bar\"]\n        expected_calls = [\n            print_call(\"foo\", \"active\"),\n            print_call(\"bar\", \"selectable\"),\n            print_call(state=\"confirm\"),\n        ]\n        with self.assertRaises(MockException):\n            cutie.select_multiple(args_list)\n\n    @mock.patch(\"cutie.readchar.readkey\", side_effect=MockException)\n    @mock.patch(\"cutie.print\")\n    def test_print_options_caption_indices(self, mock_print, *m):\n        args_list = [\"foo\", \"bar\"]\n        expected_calls = [\n            print_call(\"foo\", \"caption\"),\n            print_call(\"bar\"),\n            print_call(state=\"no_confirm_line\"),\n        ]\n        with self.assertRaises(MockException):\n            cutie.select_multiple(args_list, caption_indices=[0])\n        self.assertEqual(mock_print.call_args_list[-3:], expected_calls)\n\n    @mock.patch(\"cutie.readchar.readkey\", side_effect=MockException)\n    @mock.patch(\"cutie.print\")\n    def test_print_options_selected(self, mock_print, *m):\n        args_list = [\"foo\", \"bar\"]\n        expected_calls = [\n            print_call(\"foo\"),\n            print_call(\"bar\", \"active\"),\n            print_call(state=\"no_confirm_line\"),\n        ]\n        with self.assertRaises(MockException):\n            cutie.select_multiple(args_list, cursor_index=1)\n        self.assertEqual(mock_print.call_args_list[-3:], expected_calls)\n\n    @mock.patch(\"cutie.readchar.readkey\", side_effect=MockException)\n    @mock.patch(\"cutie.print\")\n    def test_print_options_selected_and_ticked(self, mock_print, *m):\n        args_list = [\"foo\", \"bar\"]\n        expected_calls = [\n            print_call(\"foo\", \"active-selected\"),\n            print_call(\"bar\"),\n            print_call(state=\"no_confirm_line\"),\n        ]\n        with self.assertRaises(MockException):\n            cutie.select_multiple(args_list, ticked_indices=[0])\n        self.assertEqual(mock_print.call_args_list[-3:], expected_calls)\n\n    @mock.patch(\"cutie.readchar.readkey\", side_effect=MockException)\n    @mock.patch(\"cutie.print\")\n    def test_print_options_deselected_unticked(self, mock_print, *m):\n        args_list = [\"foo\", \"bar\"]\n        expected_calls = [\n            print_call(\"foo\"),\n            print_call(\"bar\"),\n            print_call(state=\"no_confirm_line\"),\n        ]\n        with self.assertRaises(MockException):\n            cutie.select_multiple(args_list, cursor_index=2)\n        self.assertEqual(mock_print.call_args_list[-3:], expected_calls)\n\n    @mock.patch(\"cutie.readchar.readkey\", side_effect=MockException)\n    @mock.patch(\"cutie.print\")\n    def test_print_deselected_confirm(self, mock_print, *m):\n        expected_call = print_call(state=\"confirm\")\n        with self.assertRaises(MockException):\n            cutie.select_multiple([], cursor_index=1, hide_confirm=False)\n        self.assertEqual(mock_print.call_args_list[-1], expected_call)\n\n    @mock.patch(\"cutie.readchar.readkey\", side_effect=MockException)\n    @mock.patch(\"cutie.print\")\n    def test_print_selected_confirm(self, mock_print, *m):\n        expected_call = print_call(state=\"confirm-active\")\n        with self.assertRaises(MockException):\n            cutie.select_multiple([], hide_confirm=False)\n        self.assertEqual(mock_print.call_args_list[-1], expected_call)\n\n    @mock.patch(\"cutie.readchar.readkey\", side_effect=MockException)\n    @mock.patch(\"cutie.print\")\n    def test_print_show_confirm(self, mock_print, *m):\n        expected_calls = [print_call(\"foo\", \"active\"), print_call(state=\"confirm\")]\n        with self.assertRaises(MockException):\n            cutie.select_multiple([\"foo\"], hide_confirm=False)\n        self.assertEqual(mock_print.call_args_list[2:], expected_calls)\n\n\nclass TestSelectMultipleMoveAndSelect(unittest.TestCase):\n    @mock.patch(\"cutie.print\")\n    def test_move_up(self, mock_print):\n        call_args = [\"foo\", \"bar\"]\n        expected_calls = [\n            print_call(\"foo\", \"active\"),\n            print_call(\"bar\"),\n            print_call(state=\"no_confirm_line\"),\n            PRINT_CALL_END,\n        ]\n        with InputContext(readchar.key.UP, readchar.key.ENTER):\n            cutie.select_multiple(call_args, cursor_index=1)\n        self.assertEqual(mock_print.call_args_list[-4:], expected_calls)\n\n    @mock.patch(\"cutie.print\")\n    def test_move_up_skip_caption(self, mock_print):\n        call_args = [\"foo\", \"bar\", \"baz\"]\n        expected_calls = [\n            print_call(\"foo\", \"active\"),\n            print_call(\"bar\", \"caption\"),\n            print_call(\"baz\"),\n            print_call(state=\"no_confirm_line\"),\n            PRINT_CALL_END,\n        ]\n        with InputContext(readchar.key.UP, readchar.key.ENTER):\n            cutie.select_multiple(call_args, cursor_index=2, caption_indices=[1])\n        self.assertEqual(mock_print.call_args_list[-5:], expected_calls)\n\n    @mock.patch(\"cutie.print\")\n    def test_move_down(self, mock_print):\n        call_args = [\"foo\", \"bar\"]\n        expected_calls = [\n            print_call(\"foo\"),\n            print_call(\"bar\", \"active\"),\n            print_call(state=\"no_confirm_line\"),\n            PRINT_CALL_END,\n        ]\n        with InputContext(readchar.key.DOWN, readchar.key.ENTER):\n            cutie.select_multiple(call_args)\n        self.assertEqual(mock_print.call_args_list[-4:], expected_calls)\n\n    @mock.patch(\"cutie.print\")\n    def test_move_down_skip_caption(self, mock_print):\n        call_args = [\"foo\", \"bar\", \"baz\"]\n        expected_calls = [\n            print_call(\"foo\"),\n            print_call(\"bar\", \"caption\"),\n            print_call(\"baz\", \"active\"),\n            print_call(state=\"no_confirm_line\"),\n            PRINT_CALL_END,\n        ]\n        with InputContext(readchar.key.DOWN, readchar.key.ENTER):\n            cutie.select_multiple(call_args, caption_indices=[1])\n        self.assertEqual(mock_print.call_args_list[-5:], expected_calls)\n\n    @mock.patch(\"cutie.print\")\n    def test_select(self, mock_print):\n        call_args = [\"foo\", \"bar\"]\n        expected_calls = [\n            print_call(\"foo\", \"selected\"),\n            print_call(\"bar\", \"active-selected\"),\n            print_call(state=\"no_confirm_line\"),\n            PRINT_CALL_END,\n        ]\n        with InputContext(\" \", readchar.key.DOWN, \" \", readchar.key.ENTER):\n            selected_indices = cutie.select_multiple(call_args)\n        self.assertEqual(mock_print.call_args_list[-4:], expected_calls)\n        self.assertEqual(selected_indices, [0, 1])\n\n    @mock.patch(\"cutie.print\")\n    def test_select_min_too_few(self, mock_print):\n        call_args = [\"foo\"]\n        expected_call = (\n            (\"Must select at least 1 options\\x1b[K\",),\n            {\"end\": \"\", \"flush\": True},\n        )\n        with InputContext(readchar.key.DOWN, readchar.key.ENTER):\n            with self.assertRaises(MockException):\n                cutie.select_multiple(call_args, minimal_count=1)\n            self.assertEqual(mock_print.call_args_list[-1], expected_call)\n\n    @mock.patch(\"cutie.print\")\n    def test_select_max_too_many(self, mock_print):\n        call_args = [\"foo\"]\n        expected_call = (\n            (\"Must select at most 0 options\\x1b[K\",),\n            {\"end\": \"\", \"flush\": True},\n        )\n        with InputContext(readchar.key.ENTER):\n            with self.assertRaises(MockException):\n                cutie.select_multiple(call_args, maximal_count=0, ticked_indices=[0])\n            self.assertEqual(mock_print.call_args_list[-1], expected_call)\n\n    @mock.patch(\"cutie.print\")\n    def test_select_min_sufficient(self, mock_print):\n        call_args = [\"foo\"]\n        expected_calls = [\n            print_call(\"foo\", \"active-selected\"),\n            print_call(state=\"no_confirm_line\"),\n            PRINT_CALL_END,\n        ]\n        with InputContext(\" \", readchar.key.ENTER):\n            selected_indices = cutie.select_multiple(call_args, minimal_count=1)\n            self.assertEqual(mock_print.call_args_list[-3:], expected_calls)\n            self.assertEqual(selected_indices, [0])\n\n    @mock.patch(\"cutie.print\")\n    def test_deselect_on_min_sufficient(self, mock_print):\n        call_args = [\"foo\", \"bar\"]\n        expected_calls = [\n            print_call(\"foo\", \"selectable\"),\n            print_call(\"bar\", \"active-selected\"),\n            print_call(state=\"no_confirm_line\"),\n            PRINT_CALL_END,\n        ]\n        with InputContext(\" \", readchar.key.DOWN, readchar.key.ENTER):\n            selected_indices = cutie.select_multiple(\n                call_args, minimal_count=1, ticked_indices=[0, 1]\n            )\n            self.assertEqual(mock_print.call_args_list[-4:], expected_calls)\n            self.assertEqual(selected_indices, [1])\n\n    @mock.patch(\"cutie.print\")\n    def test_select_max_okay(self, mock_print):\n        call_args = [\"foo\"]\n        expected_calls = [\n            print_call(\"foo\", \"active-selected\"),\n            print_call(state=\"no_confirm_line\"),\n            PRINT_CALL_END,\n        ]\n        with InputContext(\" \", readchar.key.ENTER):\n            selected_indices = cutie.select_multiple(call_args, maximal_count=1)\n            self.assertEqual(mock_print.call_args_list[-3:], expected_calls)\n            self.assertEqual(selected_indices, [0])\n\n    @mock.patch(\"cutie.print\")\n    def test_select_min_too_few_hide_confirm(self, mock_print):\n        \"\"\"\n        This should prompt the user with an error message\n        \"\"\"\n        call_args = [\"foo\"]\n        expected_call = (\n            (\"Must select at least 1 options\\x1b[K\",),\n            {\"end\": \"\", \"flush\": True},\n        )\n        with InputContext(readchar.key.ENTER):\n            with self.assertRaises(MockException):\n                cutie.select_multiple(call_args, minimal_count=1)\n            self.assertEqual(mock_print.call_args_list[-1], expected_call)\n\n    @mock.patch(\"cutie.print\")\n    def test_select_max_too_many_show_confirm(self, mock_print):\n        \"\"\"\n        This should prompt the user with an error message\n        \"\"\"\n        call_args = [\"foo\"]\n        expected_call = (\n            (\"\\x1b[1;32m{{ confirm }}\\x1b[0m Must select at most 0 options\\x1b[K\",),\n            {\"end\": \"\", \"flush\": True},\n        )\n        with InputContext(readchar.key.DOWN, readchar.key.ENTER):\n            with self.assertRaises(MockException):\n                cutie.select_multiple(\n                    call_args, maximal_count=0, ticked_indices=[0], hide_confirm=False\n                )\n            self.assertEqual(mock_print.call_args_list[-1], expected_call)\n\n\nclass TestSelectMultipleMisc(unittest.TestCase):\n    @mock.patch(\"cutie.print\")\n    def test_keyboard_interrupt(self, mock_print):\n        call_args = [\"foo\", \"bar\"]\n        with InputContext(readchar.key.CTRL_C):\n            with self.assertRaises(KeyboardInterrupt):\n                cutie.select_multiple(call_args)\n"
  }
]